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

Merge branch 'main' into feature/v1.0.36

lianbingjie 1 жил өмнө
parent
commit
0e25bc8a84
100 өөрчлөгдсөн 9945 нэмэгдсэн , 938 устгасан
  1. 3 1
      apps/bigmember_pc/config/proxy.js
  2. 1 1
      apps/bigmember_pc/index.html
  3. 5 1
      apps/bigmember_pc/package.json
  4. 1 0
      apps/bigmember_pc/src/App.vue
  5. 3 81
      apps/bigmember_pc/src/api/index.js
  6. 82 0
      apps/bigmember_pc/src/api/interceptors-anti.js
  7. 4 0
      apps/bigmember_pc/src/api/interceptors-data-models.js
  8. 111 0
      apps/bigmember_pc/src/api/modules/detail.js
  9. 26 0
      apps/bigmember_pc/src/api/modules/nps.js
  10. 9 0
      apps/bigmember_pc/src/api/modules/subscribe.js
  11. BIN
      apps/bigmember_pc/src/assets/fonts/iconfont.ttf
  12. BIN
      apps/bigmember_pc/src/assets/fonts/iconfont.woff
  13. BIN
      apps/bigmember_pc/src/assets/fonts/iconfont.woff2
  14. BIN
      apps/bigmember_pc/src/assets/images/article-mask/icon-aaa.png
  15. BIN
      apps/bigmember_pc/src/assets/images/article-mask/pc-cq-example.png
  16. BIN
      apps/bigmember_pc/src/assets/images/article-mask/pc-cq-mmt.png
  17. BIN
      apps/bigmember_pc/src/assets/images/article-mask/pc-nj-example.png
  18. BIN
      apps/bigmember_pc/src/assets/images/article-mask/pc_mh.png
  19. BIN
      apps/bigmember_pc/src/assets/images/article-mask/pc_zzt.png
  20. BIN
      apps/bigmember_pc/src/assets/images/article-mask/pc_zzt_new.jpg
  21. BIN
      apps/bigmember_pc/src/assets/images/blue-duihao.png
  22. BIN
      apps/bigmember_pc/src/assets/images/dialog/reward-bgi.png
  23. BIN
      apps/bigmember_pc/src/assets/images/icon/icon-reward-close.png
  24. BIN
      apps/bigmember_pc/src/assets/images/icon/re-info.png
  25. BIN
      apps/bigmember_pc/src/assets/images/icon/re-unit.png
  26. BIN
      apps/bigmember_pc/src/assets/images/icon/verify-check.png
  27. BIN
      apps/bigmember_pc/src/assets/images/icon/verify-logo.png
  28. BIN
      apps/bigmember_pc/src/assets/images/nps_bg.png
  29. BIN
      apps/bigmember_pc/src/assets/images/tel.png
  30. BIN
      apps/bigmember_pc/src/assets/images/vip/bg/mask/qzkh.png
  31. BIN
      apps/bigmember_pc/src/assets/images/vip/qzkh.png
  32. 9 1
      apps/bigmember_pc/src/assets/style/_variables.scss
  33. 30 1
      apps/bigmember_pc/src/assets/style/common.scss
  34. 0 762
      apps/bigmember_pc/src/assets/style/iconfont.css
  35. 275 0
      apps/bigmember_pc/src/assets/style/page/article.scss
  36. 33 2
      apps/bigmember_pc/src/assets/style/reset-ele.scss
  37. 24 0
      apps/bigmember_pc/src/assets/style/uno.common.scss
  38. 1 1
      apps/bigmember_pc/src/components/article-item/ArticleItem.vue
  39. 159 32
      apps/bigmember_pc/src/components/collect-info/CollectInfo.vue
  40. 16 13
      apps/bigmember_pc/src/components/common/ContentLayout.vue
  41. 2 2
      apps/bigmember_pc/src/components/common/Monitor.vue
  42. 164 0
      apps/bigmember_pc/src/components/common/MonitorPopover.vue
  43. 0 1
      apps/bigmember_pc/src/components/crm-info/crmAction.vue
  44. 1 1
      apps/bigmember_pc/src/components/forecast/ForeCast.vue
  45. 1 1
      apps/bigmember_pc/src/components/home/FloatSide.vue
  46. 1 0
      apps/bigmember_pc/src/components/mask-card/MaskCard.vue
  47. 1 1
      apps/bigmember_pc/src/components/medical/FollowList.vue
  48. 657 0
      apps/bigmember_pc/src/components/push-list/ArticleStar.vue
  49. 1 2
      apps/bigmember_pc/src/components/push-list/PotentialList.vue
  50. 62 12
      apps/bigmember_pc/src/components/time-line/PoverTimeLine.vue
  51. 14 4
      apps/bigmember_pc/src/components/time-line/TimeLine.vue
  52. 0 1
      apps/bigmember_pc/src/components/work-desktop/Slidebar.vue
  53. 456 0
      apps/bigmember_pc/src/composables/attachment-download/component/AttachmentDownload.vue
  54. 87 0
      apps/bigmember_pc/src/composables/attachment-download/index.js
  55. 20 0
      apps/bigmember_pc/src/composables/down-project-report/README.md
  56. 80 0
      apps/bigmember_pc/src/composables/down-project-report/component/DownProjectReport.vue
  57. 75 0
      apps/bigmember_pc/src/composables/down-project-report/index.js
  58. 16 0
      apps/bigmember_pc/src/composables/quick-join-bid/README.md
  59. 130 0
      apps/bigmember_pc/src/composables/quick-join-bid/index.js
  60. 54 0
      apps/bigmember_pc/src/composables/quick-monitor/README.md
  61. 145 0
      apps/bigmember_pc/src/composables/quick-monitor/component/QuickMonitor.vue
  62. 47 0
      apps/bigmember_pc/src/composables/quick-monitor/index.js
  63. 71 0
      apps/bigmember_pc/src/composables/quick-monitor/use/base.js
  64. 264 0
      apps/bigmember_pc/src/composables/quick-monitor/use/client.js
  65. 247 0
      apps/bigmember_pc/src/composables/quick-monitor/use/ent.js
  66. 241 0
      apps/bigmember_pc/src/composables/quick-monitor/use/porject.js
  67. 1 0
      apps/bigmember_pc/src/main.js
  68. 3 1
      apps/bigmember_pc/src/router/router-interceptors.js
  69. 7 0
      apps/bigmember_pc/src/router/routers.js
  70. 4 1
      apps/bigmember_pc/src/store/index.js
  71. 13 1
      apps/bigmember_pc/src/store/user.js
  72. 33 3
      apps/bigmember_pc/src/utils/globalDirectives.js
  73. 3 0
      apps/bigmember_pc/src/views/BidrenewalDialog/index.vue
  74. 93 11
      apps/bigmember_pc/src/views/PotentialList.vue
  75. 543 0
      apps/bigmember_pc/src/views/article-content/components/ContentBIActions.vue
  76. 403 0
      apps/bigmember_pc/src/views/article-content/components/ContentHeader.vue
  77. 100 0
      apps/bigmember_pc/src/views/article-content/components/ContentHeaderSkeleton.vue
  78. 513 0
      apps/bigmember_pc/src/views/article-content/components/ContentMask.vue
  79. 239 0
      apps/bigmember_pc/src/views/article-content/components/ContentRightTimeLine.vue
  80. 290 0
      apps/bigmember_pc/src/views/article-content/components/ContentSummary.vue
  81. 284 0
      apps/bigmember_pc/src/views/article-content/components/ContentThirdPopover.vue
  82. 108 0
      apps/bigmember_pc/src/views/article-content/components/FooterAd.vue
  83. 277 0
      apps/bigmember_pc/src/views/article-content/components/Nps.vue
  84. 330 0
      apps/bigmember_pc/src/views/article-content/components/OriginLink.vue
  85. 467 0
      apps/bigmember_pc/src/views/article-content/components/RecommendCustomers.vue
  86. 266 0
      apps/bigmember_pc/src/views/article-content/components/RecommendEnt.vue
  87. 445 0
      apps/bigmember_pc/src/views/article-content/components/RecommendOpportunities.vue
  88. 114 0
      apps/bigmember_pc/src/views/article-content/components/RecommendServes.vue
  89. 142 0
      apps/bigmember_pc/src/views/article-content/components/RecommendServesCard.vue
  90. 142 0
      apps/bigmember_pc/src/views/article-content/components/Reward.vue
  91. 80 0
      apps/bigmember_pc/src/views/article-content/composables/README.md
  92. 344 0
      apps/bigmember_pc/src/views/article-content/composables/useArticleContentPageModel.js
  93. 29 0
      apps/bigmember_pc/src/views/article-content/composables/useArticleStar.js
  94. 73 0
      apps/bigmember_pc/src/views/article-content/composables/useArticleUtil.js
  95. 111 0
      apps/bigmember_pc/src/views/article-content/composables/useContentStore.js
  96. 40 0
      apps/bigmember_pc/src/views/article-content/composables/useDistribute.js
  97. 154 0
      apps/bigmember_pc/src/views/article-content/composables/useHoverElementClientRect.js
  98. 25 0
      apps/bigmember_pc/src/views/article-content/composables/useShare.js
  99. 0 0
      apps/bigmember_pc/src/views/article-content/composables/useTabs.js
  100. 640 0
      apps/bigmember_pc/src/views/article-content/pages/Article.vue

+ 3 - 1
apps/bigmember_pc/config/proxy.js

@@ -19,7 +19,9 @@ const PrefixAPIS = [
   '/message',
   // 资源
   '/commonFunctions',
-  '/common-module'
+  '/common-module',
+  '/page_pc_social',
+  '/biddetail/normal/qr'
 ]
 
 exports.getProxyOfDomain = function (domain) {

+ 1 - 1
apps/bigmember_pc/index.html

@@ -28,7 +28,7 @@
     <% } %>
 
       <!-- 使用CDN的CSS文件 -->
-      <link rel="stylesheet" href="https://cdn-common.jianyu360.com/cdn/assets/iconfont/pc/23.9.28/iconfont.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) { %>

+ 5 - 1
apps/bigmember_pc/package.json

@@ -10,6 +10,8 @@
     "format": "prettier --write \"./**/*.{,vue,ts,js,json,md}\""
   },
   "dependencies": {
+    "@jy/util": "workspace:^",
+    "@jy/data-models": "workspace:^",
     "@jianyu/easy-fix-sub-app": "^0.0.2",
     "@jianyu/easy-inject-qiankun": "^0.1.11",
     "@jianyu/icon": "^0.1.7",
@@ -30,9 +32,9 @@
   },
   "devDependencies": {
     "@rushstack/eslint-patch": "^1.1.0",
+    "@unocss/transformer-variant-group": "^0.58.5",
     "@vitejs/plugin-legacy": "^4.0.4",
     "@vitejs/plugin-vue2": "^2.2.0",
-    "vite-plugin-eslint": "^1.8.1",
     "@vue/eslint-config-prettier": "^7.0.0",
     "autoprefixer": "^10.4.14",
     "eslint": "^8.5.0",
@@ -41,9 +43,11 @@
     "prettier": "^2.5.1",
     "sass": "^1.63.2",
     "terser": "^5.14.2",
+    "unocss": "^0.58.5",
     "unplugin-vue-components": "^0.25.1",
     "vite": "^4.3.9",
     "vite-plugin-ejs": "^1.6.4",
+    "vite-plugin-eslint": "^1.8.1",
     "vite-plugin-externals": "^0.6.2",
     "vite-plugin-legacy-qiankun": "^0.0.12"
   }

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

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

+ 3 - 81
apps/bigmember_pc/src/api/index.js

@@ -1,82 +1,4 @@
-import axios from './axios'
-import $ from 'jquery'
-import qs from 'qs'
+import service from './interceptors-anti'
+import './interceptors-data-models'
 
-// 此处添加全局请求拦截
-// $.ajaxPrefilter((options, originalOptions, jqXHR) => {
-//   console.log('options', options)
-//   console.log('originalOptions', originalOptions)
-//   console.log('jqXHR', jqXHR)
-// })
-// jquery请求
-const ajax = (config) =>
-  new Promise((resolve, reject) => {
-    // 获取url参数
-    const url = config.baseURL
-      ? config.baseURL + config.url
-      : import.meta.env.VITE_APP_BASE_API + config.url
-
-    const ajaxConf = {
-      url: url,
-      method: config.method
-    }
-
-    // 获取data数据
-    const data = config.data
-    const params = config.params
-    const headers = config.headers
-
-    if (ajaxConf.method.toLowerCase() === 'post') {
-      if (data) {
-        if (typeof data === 'string') {
-          // formData
-          ajaxConf.data = qs.parse(data)
-        } else {
-          // json
-          ajaxConf.contentType = 'application/json;charset=UTF-8'
-          ajaxConf.data = JSON.stringify(data)
-        }
-      }
-    } else if (ajaxConf.method.toLowerCase() === 'get') {
-      if (params) {
-        ajaxConf.data = params
-      }
-    }
-    if (headers) {
-      ajaxConf.headers = headers
-    }
-
-    // 此处数据预处理
-    // 此处添加请求拦截(请求发送前处理参数)
-
-    $.ajax({
-      ...ajaxConf,
-      beforeSend: (xhr) => {
-        // 此处请求预处理
-        // 此处添加请求拦截(请求发送前处理参数)
-      },
-      success: (res) => {
-        // 此处添加响应拦截
-        if (
-          import.meta.env.NODE_ENV === 'production' &&
-          config &&
-          !config.noIntercept &&
-          !window.$noIntercept
-        ) {
-          const noPermissionText = ['未登录', '需要登录', '需要登录!']
-          const noPermission = noPermissionText.includes(res.error_msg)
-          if (noPermission) {
-            location.href = '/notin/page'
-            return
-          }
-        }
-        resolve(res)
-      },
-      error: (err) => {
-        reject(err)
-      }
-    })
-  })
-
-const useJQueryAjax = !!window.antiAdd
-export default useJQueryAjax ? ajax : axios
+export default service

+ 82 - 0
apps/bigmember_pc/src/api/interceptors-anti.js

@@ -0,0 +1,82 @@
+import axios from './axios'
+import $ from 'jquery'
+import qs from 'qs'
+
+// 此处添加全局请求拦截
+// $.ajaxPrefilter((options, originalOptions, jqXHR) => {
+//   console.log('options', options)
+//   console.log('originalOptions', originalOptions)
+//   console.log('jqXHR', jqXHR)
+// })
+// jquery请求
+const ajax = (config) =>
+  new Promise((resolve, reject) => {
+    // 获取url参数
+    const url = config.baseURL
+      ? config.baseURL + config.url
+      : import.meta.env.VITE_APP_BASE_API + config.url
+
+    const ajaxConf = {
+      url: url,
+      method: config.method || 'get'
+    }
+
+    // 获取data数据
+    const data = config.data
+    const params = config.params
+    const headers = config.headers
+
+    if (ajaxConf.method.toLowerCase() === 'post') {
+      if (data) {
+        if (typeof data === 'string') {
+          // formData
+          ajaxConf.data = qs.parse(data)
+        } else {
+          // json
+          ajaxConf.contentType = 'application/json;charset=UTF-8'
+          ajaxConf.data = JSON.stringify(data)
+        }
+      }
+    } else if (ajaxConf.method.toLowerCase() === 'get') {
+      if (params) {
+        ajaxConf.data = params
+      }
+    }
+    if (headers) {
+      ajaxConf.headers = headers
+    }
+
+    // 此处数据预处理
+    // 此处添加请求拦截(请求发送前处理参数)
+
+    $.ajax({
+      ...ajaxConf,
+      beforeSend: (xhr) => {
+        // 此处请求预处理
+        // 此处添加请求拦截(请求发送前处理参数)
+      },
+      success: (res) => {
+        // 此处添加响应拦截
+        if (
+          import.meta.env.NODE_ENV === 'production' &&
+          config &&
+          !config.noIntercept &&
+          !window.$noIntercept
+        ) {
+          const noPermissionText = ['未登录', '需要登录', '需要登录!']
+          const noPermission = noPermissionText.includes(res.error_msg)
+          if (noPermission) {
+            location.href = '/notin/page'
+            return
+          }
+        }
+        resolve(res)
+      },
+      error: (err) => {
+        reject(err)
+      }
+    })
+  })
+
+const useJQueryAjax = !!window.antiAdd
+export default useJQueryAjax ? ajax : axios

+ 4 - 0
apps/bigmember_pc/src/api/interceptors-data-models.js

@@ -0,0 +1,4 @@
+import service from './interceptors-anti'
+import { injectRequest } from '@jy/data-models'
+
+injectRequest(service)

+ 111 - 0
apps/bigmember_pc/src/api/modules/detail.js

@@ -0,0 +1,111 @@
+import request from '@/api'
+import qs from 'qs'
+
+// 获取打赏文字内容
+export function getRewardText() {
+  return request({
+    method: 'get',
+    url: '/front/rewardText'
+  })
+}
+
+// 三级页前置接口
+export function ajaxGetArticlePreAgentInfo(params) {
+  return request({
+    url: '/publicapply/detail/preAgent',
+    method: 'get',
+    params
+  })
+}
+
+// 获取详情页基本信息
+export function ajaxGetContentInfo(data) {
+  data = qs.stringify(data)
+  return request({
+    method: 'post',
+    url: '/publicapply/detail/baseInfo',
+    data
+  })
+}
+
+// 获取详情页关联、推荐等信息
+export function ajaxGetContentOtherInfo(data) {
+  data = qs.stringify(data)
+  return request({
+    method: 'post',
+    url: '/publicapply/detail/advancedInfo',
+    data
+  })
+}
+
+// 获取采购 buyer、 中标 winner 企业基础画像信息
+export function ajaxGetMiniEntInfo(type, data) {
+  return request({
+    method: 'post',
+    url: `/bigmember/portrait/${type}/miniData`,
+    data: qs.stringify({
+      [type === 'buyer' ? 'buyer' : 'entId']: data
+    })
+  })
+}
+
+// 三级页查看原文接口
+export function getArticleOriginalText(data) {
+  return request({
+    url: '/publicapply/userbase/getOriginalText',
+    method: 'post',
+    data
+  })
+}
+
+// 获取标讯详情页附件信息
+export function ajaxGetAttachmentList(data) {
+  return request({
+    url: '/bigmember/attachment/get',
+    method: 'post',
+    noToast: true,
+    data: qs.stringify(data)
+  })
+}
+
+// 获取各种资源包余额信息
+export function getResourcePackAccount(data) {
+  return request({
+    url: '/jypay/resourcePack/account',
+    method: 'post',
+    data: qs.stringify(data)
+  })
+}
+
+// 资源包兑换接口
+export function useResourcePack(data) {
+  return request({
+    url: '/jypay/resourcePack/consumePack',
+    method: 'post',
+    noToast: true,
+    data: qs.stringify(data)
+  })
+}
+
+// 保存筛选条件到后端SESSION,用于回显
+export function ajaxSetSearchFilterForSession(data) {
+  return request({
+    url: '/front/dataExport/superSearchExport',
+    method: 'post',
+    noToast: true,
+    headers: {
+      jump_source: '1'
+    },
+    data: qs.stringify(data)
+  })
+}
+
+// 三方认证悬浮窗信息
+export function ajaxGetThirdPopoverInfo(data) {
+  return request({
+    url: '/commercial/customer/info',
+    method: 'get',
+    noToast: true,
+    params: data
+  })
+}

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

@@ -0,0 +1,26 @@
+// nps
+import qs from 'qs'
+import request from '@/api'
+
+export function getNpsData() {
+  return request({
+    url: '/publicapply/nps/getNpsData',
+    method: 'get'
+  })
+}
+export function getSeeNps(data) {
+  data = qs.stringify(data)
+  return request({
+    url: '/publicapply/nps/seeNps',
+    method: 'post',
+    data
+  })
+}
+export function collectionNps(data) {
+  data = qs.stringify(data)
+  return request({
+    url: '/publicapply/nps/collectNps',
+    method: 'post',
+    data
+  })
+}

+ 9 - 0
apps/bigmember_pc/src/api/modules/subscribe.js

@@ -180,6 +180,15 @@ export function getMsgDistributor(data) {
   })
 }
 
+// 标讯详情手动分发
+export function ajaxSetDidDistributor(data) {
+  return request({
+    url: '/jyapi/jybx/subscribe/bidDistributor',
+    method: 'post',
+    data: data
+  })
+}
+
 // 用户绑定信息获取
 export function getUserBindInfo(data) {
   return request({

BIN
apps/bigmember_pc/src/assets/fonts/iconfont.ttf


BIN
apps/bigmember_pc/src/assets/fonts/iconfont.woff


BIN
apps/bigmember_pc/src/assets/fonts/iconfont.woff2


BIN
apps/bigmember_pc/src/assets/images/article-mask/icon-aaa.png


BIN
apps/bigmember_pc/src/assets/images/article-mask/pc-cq-example.png


BIN
apps/bigmember_pc/src/assets/images/article-mask/pc-cq-mmt.png


BIN
apps/bigmember_pc/src/assets/images/article-mask/pc-nj-example.png


BIN
apps/bigmember_pc/src/assets/images/article-mask/pc_mh.png


BIN
apps/bigmember_pc/src/assets/images/article-mask/pc_zzt.png


BIN
apps/bigmember_pc/src/assets/images/article-mask/pc_zzt_new.jpg


BIN
apps/bigmember_pc/src/assets/images/blue-duihao.png


BIN
apps/bigmember_pc/src/assets/images/dialog/reward-bgi.png


BIN
apps/bigmember_pc/src/assets/images/icon/icon-reward-close.png


BIN
apps/bigmember_pc/src/assets/images/icon/re-info.png


BIN
apps/bigmember_pc/src/assets/images/icon/re-unit.png


BIN
apps/bigmember_pc/src/assets/images/icon/verify-check.png


BIN
apps/bigmember_pc/src/assets/images/icon/verify-logo.png


BIN
apps/bigmember_pc/src/assets/images/nps_bg.png


BIN
apps/bigmember_pc/src/assets/images/tel.png


BIN
apps/bigmember_pc/src/assets/images/vip/bg/mask/qzkh.png


BIN
apps/bigmember_pc/src/assets/images/vip/qzkh.png


+ 9 - 1
apps/bigmember_pc/src/assets/style/_variables.scss

@@ -6,6 +6,10 @@ $footerHeight: 364px;
 
 $color_main: #2cb7ca;
 
+// Background
+// 透明背景色使用时,需要配合白色背景使用
+$color_main_background: rgb($color_main,.1);
+
 $bg-retrieve: #010c28;
 $bg-button--default: linear-gradient(84deg, #af9552 0%, #efda98 100%);
 $bg-card--default: linear-gradient(#031242 0%, #010e36 100%);
@@ -17,11 +21,15 @@ $bg-color-4: linear-gradient(90deg, #f83f4f 0%, #f38797 100%);
 $bg-color-5: linear-gradient(90deg, #41af92 0%, #84ceb7 100%);
 $bg-color-6: linear-gradient(90deg, #7446a0 0%, #a380c4 100%);
 
+$bg-less: #f5feff;
+
 $font-text--title: 17px;
 
 $color-text--default: #1d1d1d;
 $color-text--active: $color_main;
-$color-input--default: #1d1d1d;
 $color-text--highlight: $color_main;
+$color-text--less: #2abed1;
+
+$color-input--default: #1d1d1d;
 
 $bg-main-color: #fff;

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

@@ -89,7 +89,7 @@ input[type='number'] {
   -moz-appearance: textfield;
 }
 
-.flex {
+.flex-w-100 {
   width: 100%;
   flex: 1;
 }
@@ -140,3 +140,32 @@ input[type='number'] {
     border: none !important;
   }
 }
+
+.show-underline {
+  .keyword.hide-underline {
+    border-bottom-width: 1px;
+  }
+}
+.keyword-underline {
+  border-bottom: 1px solid $color-main;
+  padding-bottom: 1px;
+  &.project-name,
+  &.winner-name {
+    cursor: pointer;
+  }
+}
+.keyword.hide-underline {
+  border-width: 0;
+}
+
+.iconfont {
+  &.icon-jiankong {
+    color: #9b9ca3;
+  }
+  &.icon-yijiankong {
+    color: #ff9f40;
+  }
+}
+
+
+

+ 0 - 762
apps/bigmember_pc/src/assets/style/iconfont.css

@@ -1,762 +0,0 @@
-@font-face {
-  font-family: 'iconfont'; /* Project id 624651 */
-  src: url('../fonts/iconfont.woff2?t=1706074759422') format('woff2'),
-    url('../fonts/iconfont.woff?t=1706074759422') format('woff'),
-    url('../fonts/iconfont.ttf?t=1706074759422') format('truetype');
-}
-
-.iconfont {
-  font-family: 'iconfont' !important;
-  font-size: 16px;
-  font-style: normal;
-  -webkit-font-smoothing: antialiased;
-  -moz-osx-font-smoothing: grayscale;
-}
-
-.icon-jiankong:before {
-  content: '\e6f8';
-}
-
-.icon-yijiankong:before {
-  content: '\e6f9';
-}
-
-.icon-shenfenweihu:before {
-  content: '\e6f7';
-}
-
-.icon-a-Property1shujudingzhiProperty2normal-wodezichan:before {
-  content: '\e645';
-}
-
-.icon-a-Property1shufuProperty2grey:before {
-  content: '\e644';
-}
-
-.icon-renwuguanli:before {
-  content: '\e6f3';
-}
-
-.icon-renwufupan:before {
-  content: '\e6f4';
-}
-
-.icon-renwugenjin:before {
-  content: '\e6f5';
-}
-
-.icon-yingxiao:before {
-  content: '\e6f6';
-}
-
-.icon-a-Property1hulve:before {
-  content: '\e643';
-}
-
-.icon-chuangjianxiaoshoujihui:before {
-  content: '\e638';
-}
-
-.icon-chuangjianxiaoshouxiansuo:before {
-  content: '\e63b';
-}
-
-.icon-a-Property1shoulu:before {
-  content: '\e63f';
-}
-
-.icon-a-Property1yishoulu:before {
-  content: '\e640';
-}
-
-.icon-a-Property1yihulve:before {
-  content: '\e641';
-}
-
-.icon-chuangjiankehu:before {
-  content: '\e642';
-}
-
-.icon-biyan:before {
-  content: '\e633';
-}
-
-.icon-a-Property1gray2:before {
-  content: '\e6ef';
-}
-
-.icon-a-Property1gray:before {
-  content: '\e6f0';
-}
-
-.icon-weixin_miam1:before {
-  content: '\e6f1';
-}
-
-.icon-a-Property1gray1:before {
-  content: '\e6f2';
-}
-
-.icon-caigoudanwei:before {
-  content: '\e632';
-}
-
-.icon-fenxiang:before {
-  content: '\e705';
-}
-
-.icon-ren:before {
-  content: '\e706';
-}
-
-.icon-remenzhaobiao:before {
-  content: '\e6ec';
-}
-
-.icon-shiyongtuijian:before {
-  content: '\e6ed';
-}
-
-.icon-biaodewu:before {
-  content: '\e6eb';
-}
-
-.icon-canbiao:before {
-  content: '\e704';
-}
-
-.icon-yuangongdingyuezonglan:before {
-  content: '\e6ea';
-}
-
-.icon-xiansuoguanli:before {
-  content: '\e701';
-}
-
-.icon-xiaoshou:before {
-  content: '\e702';
-}
-
-.icon-zuoxidianxiao:before {
-  content: '\e703';
-}
-
-.icon-tuisongshezhi:before {
-  content: '\e6fc';
-}
-
-.icon-dianzan:before {
-  content: '\e659';
-}
-
-.icon-icon:before {
-  content: '\e6e9';
-}
-
-.icon-shuju:before {
-  content: '\e6e7';
-}
-
-.icon-shoudongfenfa:before {
-  content: '\e6e6';
-}
-
-.icon-tishi:before {
-  content: '\e6e5';
-}
-
-.icon-hangyezhanhui:before {
-  content: '\e6ee';
-}
-
-.icon-a-Property1fuwushang:before {
-  content: '\e629';
-}
-
-.icon-a-Property1gongzuozhuomianProperty2huaxiangfenxi1:before {
-  content: '\e6e4';
-}
-
-.icon-a-Property1gongzuozhuomianProperty2jigouguanli1:before {
-  content: '\e628';
-}
-
-.icon-a-Property1gongzuozhuomianProperty2wodezichan2:before {
-  content: '\e636';
-}
-
-.icon-a-Property1gongzuozhuomianProperty2fuwushangzhuanxiang1:before {
-  content: '\e619';
-}
-
-.icon-shichangfenxi:before {
-  content: '\e630';
-}
-
-.icon-tableBox_null:before {
-  content: '\e6e8';
-}
-
-.icon-a-Property1gongzuozhuomianProperty2jiaoyiguanli:before {
-  content: '\e62f';
-}
-
-.icon-a-Property1Default:before {
-  content: '\e621';
-}
-
-.icon-a-Property1gongzuozhuomianProperty2wenku:before {
-  content: '\e622';
-}
-
-.icon-a-Property1gongzuozhuomianProperty2biaoshuzhizuo:before {
-  content: '\e624';
-}
-
-.icon-a-Property1gongzuozhuomianProperty2kechengpeixun:before {
-  content: '\e627';
-}
-
-.icon-a-yiliaolingyuhuablack:before {
-  content: '\e610';
-}
-
-.icon-a-Property1gongzuozhuomianProperty2juecefenxi:before {
-  content: '\e612';
-}
-
-.icon-a-Property1gongzuozhuomianProperty2shujukanban:before {
-  content: '\e618';
-}
-
-.icon-a-Property1gongzuozhuomianProperty2shangjiwajue:before {
-  content: '\e609';
-}
-
-.icon-a-Property1gongzuozhuomianProperty2shujushichang:before {
-  content: '\e607';
-}
-
-.icon-a-Property1gongzuozhuomianProperty2xiaoxizhongxinProperty3grey:before {
-  content: '\e606';
-}
-
-.icon-a-Property1fuwuProperty2grey:before {
-  content: '\e605';
-}
-
-.icon-a-lingyuhuagray:before {
-  content: '\e603';
-}
-
-.icon-yiban:before {
-  content: '\e6de';
-}
-
-.icon-bumanyi-cai:before {
-  content: '\e6df';
-}
-
-.icon-yiban-cai:before {
-  content: '\e6e0';
-}
-
-.icon-manyi-cai:before {
-  content: '\e6e1';
-}
-
-.icon-henmanyi-cai:before {
-  content: '\e6e2';
-}
-
-.icon-henbumanyi-cai:before {
-  content: '\e6e3';
-}
-
-.icon-bumanyi:before {
-  content: '\e6da';
-}
-
-.icon-manyi:before {
-  content: '\e6db';
-}
-
-.icon-henmanyi:before {
-  content: '\e6dc';
-}
-
-.icon-henbumanyi:before {
-  content: '\e6dd';
-}
-
-.icon-green:before {
-  content: '\e6d9';
-}
-
-.icon-fangda:before {
-  content: '\e6d6';
-}
-
-.icon-zanting:before {
-  content: '\e6d7';
-}
-
-.icon-bofang:before {
-  content: '\e6d8';
-}
-
-.icon-huo:before {
-  content: '\e6d5';
-}
-
-.icon-wenku_excel:before {
-  content: '\e6d1';
-}
-
-.icon-wenku_word:before {
-  content: '\e6d2';
-}
-
-.icon-wenku_pdf:before {
-  content: '\e6d3';
-}
-
-.icon-wenku_ppt:before {
-  content: '\e6d4';
-}
-
-.icon-houtui:before {
-  content: '\e6d0';
-}
-
-.icon-danxuan_yixuan:before {
-  content: '\e6cf';
-}
-
-.icon-jiaru_hover:before {
-  content: '\e6cd';
-}
-
-.icon-tuihui_hover:before {
-  content: '\e6ce';
-}
-
-.icon-jiaru:before {
-  content: '\e6cb';
-}
-
-.icon-tuihui:before {
-  content: '\e6cc';
-}
-
-.icon-Setting:before {
-  content: '\e6c7';
-}
-
-.icon-xiala:before {
-  content: '\e6c6';
-}
-
-.icon-dizhi:before {
-  content: '\e6c5';
-}
-
-.icon-danweileixing:before {
-  content: '\e6c4';
-}
-
-.icon-chakanhuaxiang:before {
-  content: '\e6c3';
-}
-
-.icon-a-kaiguanon:before {
-  content: '\e6c2';
-}
-
-.icon-a-kaiguanoff:before {
-  content: '\e6c1';
-}
-
-.icon-shujudaochu:before {
-  content: '\e6be';
-}
-
-.icon-biaoge:before {
-  content: '\e6bf';
-}
-
-.icon-liebiao:before {
-  content: '\e6c0';
-}
-
-.icon-telphone_mian:before {
-  content: '\e6bb';
-}
-
-.icon-weixin_miam:before {
-  content: '\e6bc';
-}
-
-.icon-telphone_line:before {
-  content: '\e6bd';
-}
-
-.icon-weixin_line:before {
-  content: '\e6ba';
-}
-
-.icon-close:before {
-  content: '\e6b9';
-}
-
-.icon-search:before {
-  content: '\e6b8';
-}
-
-.icon-shouqikuang:before {
-  content: '\e6b6';
-}
-
-.icon-zhankaikuang:before {
-  content: '\e6b7';
-}
-
-.icon-time:before {
-  content: '\e69f';
-}
-
-.icon-qiehuan:before {
-  content: '\e6b5';
-}
-
-.icon-guanzhu_yiguanzhu:before {
-  content: '\e67d';
-}
-
-.icon-guanzhu_jiaguanzhu:before {
-  content: '\e67e';
-}
-
-.icon-guanzhu_weiguanzhu:before {
-  content: '\e67f';
-}
-
-.icon-renling_yirenling:before {
-  content: '\e672';
-}
-
-.icon-renling_weirenling:before {
-  content: '\e673';
-}
-
-.icon-shoucang:before {
-  content: '\e684';
-}
-
-.icon-shoucang_weishoucang:before {
-  content: '\e685';
-}
-
-.icon-top:before {
-  content: '\e6b4';
-}
-
-.icon-code:before {
-  content: '\e6a4';
-}
-
-.icon-byEmail:before {
-  content: '\e6a5';
-}
-
-.icon-date:before {
-  content: '\e6a6';
-}
-
-.icon-shouqi2:before {
-  content: '\e6a7';
-}
-
-.icon-delete:before {
-  content: '\e6a8';
-}
-
-.icon-jiangxu:before {
-  content: '\e6a9';
-}
-
-.icon-more:before {
-  content: '\e6aa';
-}
-
-.icon-name:before {
-  content: '\e6ab';
-}
-
-.icon-shengxu:before {
-  content: '\e6ac';
-}
-
-.icon-zhankai:before {
-  content: '\e6ad';
-}
-
-.icon-phone:before {
-  content: '\e6ae';
-}
-
-.icon-password:before {
-  content: '\e6af';
-}
-
-.icon-help:before {
-  content: '\e6b0';
-}
-
-.icon-edit:before {
-  content: '\e6b1';
-}
-
-.icon-zhengyan:before {
-  content: '\e6b2';
-}
-
-.icon-company:before {
-  content: '\e6b3';
-}
-
-.icon-kefu_xian:before {
-  content: '\e6a2';
-}
-
-.icon-kefu_mian:before {
-  content: '\e6a3';
-}
-
-.icon-hui10:before {
-  content: '\e65f';
-}
-
-.icon-hui9:before {
-  content: '\e65e';
-}
-
-.icon-hui8:before {
-  content: '\e65b';
-}
-
-.icon-a-Property1xiazaixiangmubaogaoProperty2nor:before {
-  content: '\e65a';
-}
-
-.icon-duihao:before {
-  content: '\e658';
-}
-
-.icon-box:before {
-  content: '\e6c8';
-}
-
-.icon-home:before {
-  content: '\e6c9';
-}
-
-.icon-book:before {
-  content: '\e6ca';
-}
-
-.icon-shouqi1:before {
-  content: '\e657';
-}
-
-.icon-a-zhankai1:before {
-  content: '\e656';
-}
-
-.icon-a-Frame380:before {
-  content: '\e655';
-}
-
-.icon-hui7:before {
-  content: '\e654';
-}
-
-.icon-hui6:before {
-  content: '\e653';
-}
-
-.icon-hui5:before {
-  content: '\e652';
-}
-
-.icon-hui4:before {
-  content: '\e651';
-}
-
-.icon-hui3:before {
-  content: '\e650';
-}
-
-.icon-hui2:before {
-  content: '\e64f';
-}
-
-.icon-hui1:before {
-  content: '\e64e';
-}
-
-.icon-hui:before {
-  content: '\e64d';
-}
-
-.icon-cuowutishi:before {
-  content: '\e63e';
-}
-
-.icon-zhengquetishi:before {
-  content: '\e63d';
-}
-
-.icon-zhifubao:before {
-  content: '\e63c';
-}
-
-.icon-logo:before {
-  content: '\e639';
-}
-
-.icon-warning:before {
-  content: '\e63a';
-}
-
-.icon-zhifuwancheng:before {
-  content: '\e637';
-}
-
-.icon-76:before {
-  content: '\e686';
-}
-
-.icon-windows2:before {
-  content: '\e600';
-}
-
-.icon-weixinzhifu:before {
-  content: '\e635';
-}
-
-.icon-shaixuan-xuanzhong:before {
-  content: '\e634';
-}
-
-.icon-huangguan:before {
-  content: '\e604';
-}
-
-.icon-tianjia:before {
-  content: '\e631';
-}
-
-.icon-yixuan:before {
-  content: '\e62d';
-}
-
-.icon-yulan:before {
-  content: '\e62e';
-}
-
-.icon-zishangxianyilai:before {
-  content: '\e62a';
-}
-
-.icon-jintian:before {
-  content: '\e62b';
-}
-
-.icon-zuijinsanshitian:before {
-  content: '\e62c';
-}
-
-.icon-hangye:before {
-  content: '\e613';
-}
-
-.icon-zhaobiaodingyue:before {
-  content: '\e614';
-}
-
-.icon-xiangmuguanzhu:before {
-  content: '\e615';
-}
-
-.icon-nijianxiangmu:before {
-  content: '\e616';
-}
-
-.icon-zhaobiaoshequ:before {
-  content: '\e617';
-}
-
-.icon-shujuguizeziyoudingyi:before {
-  content: '\e61a';
-}
-
-.icon-shujukaifang:before {
-  content: '\e61b';
-}
-
-.icon-mianfei:before {
-  content: '\e61c';
-}
-
-.icon-APIjiekou:before {
-  content: '\e61d';
-}
-
-.icon-chakanyuanwen:before {
-  content: '\e61e';
-}
-
-.icon-xiayiye:before {
-  content: '\e61f';
-}
-
-.icon-shangyiye:before {
-  content: '\e620';
-}
-
-.icon-shouqi:before {
-  content: '\e623';
-}
-
-.icon-gengduo:before {
-  content: '\e625';
-}
-
-.icon-shiyanshi:before {
-  content: '\e626';
-}
-
-.icon-zhidingarrow:before {
-  content: '\e60b';
-}
-
-.icon-iOS:before {
-  content: '\e60c';
-}
-
-.icon-erweima:before {
-  content: '\e60d';
-}
-
-.icon-anzhuo:before {
-  content: '\e60e';
-}
-
-.icon-QQ:before {
-  content: '\e60f';
-}
-
-.icon-weixin:before {
-  content: '\e611';
-}

+ 275 - 0
apps/bigmember_pc/src/assets/style/page/article.scss

@@ -0,0 +1,275 @@
+// 页面原有样式
+
+.article-page-container {
+  ::v-deep {
+
+    .free-view {
+      cursor: pointer;
+    }
+
+    .step-items:hover .item-label {
+      text-decoration: underline;
+      color: #fe7379;
+      .highlight-text {
+        color: inherit;
+      }
+    }
+
+    .article-content-footer-container {
+      .adsense {
+        margin-top: 16px;
+        padding: 0;
+        cursor: pointer;
+        .content {
+          border: none;
+          padding-top: 0;
+        }
+      }
+    }
+
+    .article-container {
+      .step-circle[data-tip='']::before {
+        transform: translate(0, -50%) scale(1.6);
+      }
+
+      .is-active-time-item {
+        height: 22px;
+        line-height: 22px;
+        font-size: 12px;
+        margin-left: 8px;
+        padding: 0 12px;
+        border-radius: 16px;
+        border-bottom-left-radius: 0;
+        background-color: rgba(255, 58, 32, 0.1);
+        color: #ff3a20;
+      }
+      .content-block-header {
+        position: relative;
+        color: #1d1d1d;
+        font-size: 20px;
+        font-style: normal;
+        font-weight: 400;
+        line-height: 32px;
+        &::before {
+          content: '';
+          position: absolute;
+          top: 4px;
+          left: -40px;
+          display: inline-block;
+          width: 3px;
+          height: 24px;
+          border-radius: 0px 2px 2px 0px;
+          background: #2abed1;
+        }
+      }
+
+      .content-detail-container {
+        pre {
+          display: inherit;
+          padding: 0;
+          margin: 0;
+          font-size: 14px !important;
+          word-break: break-word;
+          white-space: pre-wrap;
+          color: #333;
+          background-color: #fff;
+          border: 0px;
+          border-radius: 4px;
+          line-height: 25px;
+          overflow-x: hidden;
+        }
+
+        .content-table-container {
+          max-width: 100%;
+          overflow-x: scroll;
+        }
+
+        table {
+          white-space-collapse: collapse;
+          border-collapse: collapse !important;
+          border-spacing: 0px !important;
+          line-height: 21px;
+          background: transparent;
+          max-width: 100%;
+
+          th,
+          tr td {
+            border: 1px solid #ebebeb;
+            padding: 10px;
+            &:empty {
+              border-width: 0;
+              padding: 0;
+            }
+          }
+        }
+      }
+    }
+  }
+
+  .content-card {
+    margin-top: 16px;
+    border-radius: 8px;
+    padding: 32px 40px;
+    background-color: #fff;
+    &.is-plain {
+      border-radius: 0;
+    }
+  }
+
+  .origin-detail-action {
+    width: 132px;
+    height: 34px;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    border-radius: 4px;
+    border: 1px solid #e0e0e0;
+    color: #2abed1;
+    font-size: 14px;
+    font-weight: 400;
+    .iconfont {
+      font-size: 18px;
+      margin-right: 2px;
+    }
+  }
+
+  .watch-tab-header {
+    margin-top: 16px;
+  }
+  .content-tabs-fixed {
+    position: relative;
+    display: flex;
+    flex-direction: row;
+    align-items: center;
+    height: 45px;
+    background-color: #fff;
+    color: #1d1d1d;
+    font-size: 16px;
+    font-style: normal;
+    font-weight: 400;
+    line-height: normal;
+    &::before {
+      content: '';
+      position: absolute;
+      top: 0;
+      right: 0;
+      width: 1px;
+      height: 100%;
+      background-color: #fff;
+      z-index: 3;
+    }
+    &::after {
+      content: '';
+      position: absolute;
+      bottom: 0;
+      left: 0;
+      width: 100%;
+      height: 1px;
+      background-color: #ebebeb;
+      z-index: 1;
+    }
+
+    &.is-fixed-top {
+      position: fixed;
+      top: 0;
+      z-index: 9;
+    }
+
+    .content-tab-label {
+      z-index: 2;
+      flex: 1;
+      max-width: 200px;
+      height: 100%;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      border-top: 3px solid transparent;
+      border-bottom: 1px solid #ebebeb;
+      cursor: pointer;
+      &.is-active {
+        color: #2cb7ca;
+        border-bottom: none;
+        border-top: 3px solid #2cb7ca;
+        border-right: 1px solid #ebebeb;
+        border-left: 1px solid #ebebeb;
+        border-bottom: 1px solid #fff;
+        &:first-child {
+          border-left-color: transparent;
+        }
+      }
+    }
+  }
+  .content-header-tip {
+    margin-top: 16px;
+    display: flex;
+    flex-direction: row;
+    align-items: center;
+    justify-content: space-between;
+    border-radius: 4px;
+    background: rgba(42, 190, 209, 0.1);
+    padding: 9px 40px;
+    color: #2abed1;
+    font-size: 14px;
+    font-style: normal;
+    font-weight: 400;
+    line-height: 22px;
+    .action-button {
+      padding: 3px 20px;
+      color: #2abed1;
+      text-align: center;
+      font-size: 14px;
+      font-style: normal;
+      font-weight: 400;
+      line-height: 22px;
+      border: 1px solid #2abed1;
+      background-color: transparent;
+    }
+  }
+
+  .article-content-container {
+    padding: 33px 40px;
+    background-color: #fff;
+  }
+
+  .default-article-container {
+    min-width: 880px;
+  }
+  .article-container {
+    margin-top: 0;
+
+    .content-detail-container {
+      margin: 16px 0;
+      color: #1d1d1d;
+      font-size: 14px;
+      font-style: normal;
+      font-weight: 400;
+      line-height: 22px;
+    }
+
+    .first-content-container {
+      background-color: #fff;
+      .content-card {
+        margin-top: 0;
+      }
+      .content-card[name='公告摘要'] + .content-card {
+        padding-top: 0;
+      }
+      border-radius: 0 0 8px 8px;
+    }
+
+    .report-actions {
+      font-size: 14px;
+      color: #686868;
+    }
+    .more-text {
+      display: flex;
+      flex-direction: row;
+      align-items: center;
+      cursor: pointer;
+      color: #686868;
+      font-size: 14px;
+      line-height: normal;
+    }
+  }
+}
+

+ 33 - 2
apps/bigmember_pc/src/assets/style/reset-ele.scss

@@ -46,8 +46,8 @@
     }
   }
 
-  .el-button--main {
-    font-family: Microsoft YaHei, Microsoft YaHei-Regular;
+  .el-button--main,
+  .el-button--confirm {
     border-color: $color-text--highlight;
     background: $color-text--highlight;
     border-radius: 6px;
@@ -65,6 +65,25 @@
     }
   }
 
+  .el-button--less,
+  .el-button--cancel {
+    border-color: $color-text--less;
+    background: $bg-less;
+    border-radius: 6px;
+    padding: 6px 16px;
+    box-sizing: border-box;
+    font-size: 16px;
+    line-height: 22px;
+    font-weight: 400;
+    color: $color-text--less;
+    &:hover,
+    &:focus {
+      border-color: $color-text--highlight;
+      background: $color-text--highlight;
+      color: #fff;
+    }
+  }
+
   .el-link {
     &.el-link--default {
       &:hover {
@@ -92,6 +111,18 @@
       background: #2cb7ca;
     }
   }
+
+  .custom-cancel-btn,
+  .custom-cancel-btn:hover,
+  .custom-cancel-btn:focus{
+    width: 132px;
+    height: 34px;
+    background-color: #fff;
+    border: 1px solid #DCDFE6;
+    color: #686868;
+    font-size: 16px;
+  }
+
   .el-message-box__message,
   .message-text {
     font-size: 14px;

+ 24 - 0
apps/bigmember_pc/src/assets/style/uno.common.scss

@@ -0,0 +1,24 @@
+/**
+* 常用颜色定义,对应 _variables.scss $color-text--*
+* 编译后: .color-highlight
+*/
+
+$colors: (
+  default: $color-text--default,
+  highlight: $color-text--highlight,
+);
+
+/**
+* :hover 时改变颜色
+* 编译后: .hover-color-highlight:hover
+*/
+
+@each $c, $color in $colors {
+  .color-#{$c},
+  .hover-color-#{$c}:hover {
+    color: $color;
+  }
+}
+
+
+

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

@@ -39,7 +39,7 @@
             article.site === '剑鱼信息发布平台' ||
             article.spidercode === 'a_jyxxfbpt_gg'
           "
-          >用户发布</span
+          >业主委托项目</span
         >
         <span class="tag tag-own" v-if="buySubject && article.source === 1"
           >个人订阅</span

+ 159 - 32
apps/bigmember_pc/src/components/collect-info/CollectInfo.vue

@@ -104,7 +104,7 @@
                     ></div>
                   </div>
                 </div>
-                <div class="long-control">
+                <div class="long-control" v-if="moduleShow.companyType">
                   <el-form-item
                     label="公司类型 :"
                     prop="companyType"
@@ -121,7 +121,7 @@
                     </el-checkbox-group>
                   </el-form-item>
                 </div>
-                <div class="long-control">
+                <div class="long-control" v-if="moduleShow.job">
                   <el-form-item label="职位 :" prop="job">
                     <el-select
                       popper-class="custom-select"
@@ -173,14 +173,20 @@
                     <el-input type="textarea" autosize resize="none" v-model="form.business" placeholder="请输入业务范围,让合作伙伴充分了解公司业务内容"></el-input>
                   </el-form-item>
                 </div> -->
-                <!-- <div class="long-control" v-if="moduleShow.need">
-                  <el-form-item label="合作需求 :">
-                    <el-input type="textarea" autosize resize="none" v-model="form.need" placeholder="请输入合作需求,从而创造并匹配更多合作交流机会"></el-input>
+                <div class="long-control" v-if="moduleShow.dataInput">
+                  <el-form-item label="数据需求 :">
+                    <el-input
+                      type="textarea"
+                      :autosize="{ minRows: 3, maxRows: 4 }"
+                      resize="none"
+                      v-model="form.dataInput"
+                      placeholder="请描述下您需要的数据"
+                    ></el-input>
                   </el-form-item>
-                </div> -->
+                </div>
               </div>
             </div>
-            <div class="warm-prompt">
+            <div class="warm-prompt" v-if="moduleShow.tip">
               <span class="icon-warning"></span>
               <span class="warm-text"
                 >温馨提示:请提供准确的信息,我们将为您推荐更准确、更个性化的商机和服务</span
@@ -387,7 +393,8 @@ export default {
         business: '', // 业务范围
         need: '', // 合作需求
         agreeChecked: true,
-        branch: '' // 部门
+        branch: '', // 部门
+        dataInput: ''
       },
       rules: {
         name: [
@@ -447,7 +454,10 @@ export default {
         scale: false,
         business: true,
         need: true,
-        agree: true
+        agree: true,
+        job: true,
+        dataInput: false,
+        tip: true
       },
       source: '',
       sourceTitleMap: {
@@ -511,43 +521,94 @@ export default {
           '请留下联系方式,立即免费体验【采购单位全景分析】1次,帮你全面洞察采购单位,开发新客户!',
         buyer_portrait_cooperative_ent_register_area:
           '请留下联系方式,立即免费体验【采购单位全景分析】1次,帮你全面洞察采购单位,开发新客户!',
-        ent_portrait_:
-          '请留下联系方式,我们会尽快联系您!体验企业画像分析,帮你透视企业!',
-        buyer_portrait_:
-          '请留下联系方式,我们会尽快联系您!体验采购单位画像分析,为你挖掘客户!',
-        member_attach:
-          '请留下联系方式,我们会尽快联系您!体验附件下载特权,挖掘更多项目情报!',
-        member_freeuse: '请留下联系方式,我们会尽快联系您体验大会员全部功能!',
+
         pc_index_bottom_adv:
           '请留下联系方式,我们会尽快联系您体验:市场分析周报/月报、竞对实时监控和分析、业主采购趋势/客户分析等',
-        month_: '请留下联系方式,我们会尽快联系您!体验市场分析周报/月报!',
-        week_: '请留下联系方式,我们会尽快联系您!体验市场分析周报/月报!',
+
         article_collection:
           '重要项目一键创建标签收藏!请完善个人信息,为您匹配精准服务',
         pc_buyer_monitor_more:
           '请升级大会员,可实时监控最多500个业主采购动态,洞察潜在商机。',
         pc_buyer_monitor_freeuser:
-          '您当前权限不足,请升级大会员,可实时监控最多500个业主采购动态,洞察潜在商机。',
+        '您当前权限不足,请升级大会员,可实时监控最多500个业主采购动态,洞察潜在商机。',
         pc_list_monitor_more:
           '请升级大会员,可实时监控最多500个业主采购动态,洞察潜在商机。',
         pc_buyer_monitor_limit:
           '监控业主数量已达上限,请升级大会员,可实时监控最多500个业主采购动态,洞察潜在商机。',
-        pc_project_businessDetails:
-          '请留下联系方式,我们会尽快联系您体验大会员全部功能!'
+        peugeot_supplier_regist: '请完善您的个人信息,抢先对接采购单位。',
+        pc_article_project_more: '请升级大会员,可实时监控最多500个项目动态,不错过任何一个重要商机。',
+        pc_article_project_limit: '您当前权限不足,请升级大会员,可实时监控最多500个项目动态,不错过任何一个重要商机。',
+        pc_article_cqxmmore: '请升级大会员,提前1-3个月获取项目采购计划,获取采购内容,提前运作提高中标率。',
+        pc_article_customization: '请留下您的联系方式及定制数据字段需求,我们将安排专业的数据经理与您对接,为您打造专属的数据服务方案,可快速交付!',
+        jyarticle_see3_plus_pc: '请完善个人信息,即刻享无限次查看标讯的权益,如需查看超前项目请联系客服:400-108-6670',
+        article_purchase_intention: '留下您的联系方式,我们会尽快和您联系。提前介入项目,助您轻松中标。', // 三级页-采购意向
+        article_proposed_project: '留下您的联系方式,我们会尽快和您联系。提前介入项目,助您轻松中标。', // 三级页-拟建项目
+        pc_article_original_one: '请填写以下信息升级大会员获得更多查看原文链接权限,同时可查看超前商机、联系人电话,85%用户已升级!',
+        pc_article_original_more: '请填写以下信息升级大会员获得更多查看原文链接权限,同时可查看超前商机、联系人电话,85%用户已升级!',
+        pc_project_businessDetails: '请留下联系方式,我们会尽快联系您体验大会员全部功能!',
+        pc_article_member_freeuse: '请升级大会员无限制查看标讯、超前项目,优先对接项目负责人,抢占绝对先机!',
+        peugeot_view_infor: '请留下联系方式,我们会尽快联系您体验大会员全部功能!',
+        pc_article_BidPreparation: ' ',
+        pc_article_certificateServices: ' ',
+        'certificateServices-pc-biddingDetailPage-content': ' ',
+        pc_article_BidDecision: '请升级大会员,为您分析同类项目采购明细,帮助投标人员编制标书、投标报价参考,辅助投标决策。',
+        pc_article_CustomerRecommend: '请升级大会员,为您推荐潜在业务需求客户并提供联系方式。',
+        pc_article_ent_more: '请升级大会员,可实时监控最多500个企业中标动态,帮助你了解竞争对手和合作伙伴的动向。',
+        pc_article_ent_limit: '您当前权限不足,请升级大会员,可实时监控最多500个企业中标动态,帮助你了解竞争对手和合作伙伴的动向。',
+        // 模糊匹配
+        month_: '请留下联系方式,我们会尽快联系您!体验市场分析周报/月报!',
+        week_: '请留下联系方式,我们会尽快联系您!体验市场分析周报/月报!',
+        ent_portrait_:
+          '请留下联系方式,我们会尽快联系您!体验企业画像分析,帮你透视企业!',
+        buyer_portrait_:
+          '请留下联系方式,我们会尽快联系您!体验采购单位画像分析,为你挖掘客户!',
+        member_attach:
+          '请留下联系方式,我们会尽快联系您!体验附件下载特权,挖掘更多项目情报!',
+        member_freeuse: '请留下联系方式,我们会尽快联系您体验大会员全部功能!',
       },
       sourceDescMap: {
         pc_buyer_monitor_more: '采购单位画像页-超级订阅用户申请监控更多业主',
         pc_buyer_monitor_freeuser: '采购单位画像页-免费用户申请监控业主',
         pc_list_monitor_more: '业主监控页-超级订阅用户申请监控更多业主',
         pc_buyer_monitor_limit:
-          '采购单位画像页-超级订阅申请监控更多业主(已达上限)'
+          '采购单位画像页-超级订阅申请监控更多业主(已达上限)',
+        peugeot_supplier_regist: '供应商报名',
+        pc_article_project_more: '标讯详情页-申请监控更多项目',
+        pc_article_project_limit: '标讯详情页-申请监控更多项目(已达上限)',
+        pc_article_ent_more: '标讯详情页-申请监控更多企业',
+        pc_article_ent_limit: '标讯详情页-申请监控更多企业(已达上限)',
+        pc_article_cqxmmore: '标讯详情页-申请查看更多超前项目',
+        pc_article_BidDecision: '标讯详情页-申请体验投标决策分析',
+        pc_article_BidPreparation: '标讯详情页-咨询标书制作',
+        pc_article_certificateServices: '标讯详情页-咨询企业认证服务',
+        pc_article_CustomerRecommend: '标讯详情页-申请客户推荐',
+        pc_article_customization: '标讯详情页-申请数据定制',
+        jyarticle_see3_plus_pc: '免费享无限次查看标讯体验',
+        pc_article_original_one: '标讯详情页-免费用户获取1次查看原文链接机会',
+        pc_article_original_more: '标讯详情页-获取更多查看原文链接机会',
       },
       sourceTitleTopMap: {
         article_collection: '为给您匹配精准的推荐信息,请完善个人信息',
         pc_buyer_monitor_more: '申请监控更多业主',
         pc_buyer_monitor_freeuser: '申请业主监控权限',
         pc_list_monitor_more: '申请监控更多业主',
-        pc_buyer_monitor_limit: '申请监控更多业主'
+        pc_buyer_monitor_limit: '申请监控更多业主',
+        peugeot_supplier_regist: '供应商报名',
+        pc_article_project_more: '申请监控更多项目',
+        pc_article_project_limit: '申请监控更多项目',
+        pc_article_cqxmmore: '申请查看超前项目',
+        pc_article_customization: '量身定制专属的数据解决方案',
+        jyarticle_see3_plus_pc: '免费享无限次查看标讯体验',
+        pc_article_original_one: '申请更多查看原文链接权限',
+        pc_article_original_more: '申请更多查看原文链接权限',
+        pc_article_member_freeuse: '免费体验大会员功能权益',
+        pc_article_BidPreparation: '请留下您的信息,我们会尽快和您联系',
+        pc_article_certificateServices: '请留下您的信息,我们会尽快和您联系',
+        pc_article_BidDecision: '申请投标决策分析权限',
+        pc_article_CustomerRecommend: '申请客户推荐权限',
+        pc_article_ent_more: '申请监控更多企业',
+        pc_article_ent_limit: '申请监控更多企业',
+        'certificateServices-pc-biddingDetailPage-content': '请留下您的信息,我们会尽快和您联系',
       },
       isRefresh: false
     }
@@ -612,13 +673,24 @@ export default {
           text +
           '】权益1次。如需查看更多,请开通超级订阅,为您提供最新的商业情报,抢占先机。'
         )
-      } else if (this.source === 'pc_list_monitor_more') {
+      } else if (this.source === 'pc_list_monitor_more' || this.source === 'pc_article_member_freeuse') {
         return '已收到您提交的升级大会员申请,我们会尽快联系您并预约演示时间。'
       } else if (
         this.source === 'pc_buyer_monitor_limit' ||
-        this.source === 'pc_buyer_monitor_more'
+        this.source === 'pc_buyer_monitor_more' ||
+        this.source === 'pc_article_cqxmmore' ||
+        this.source === 'pc_article_project_limit' ||
+        this.source === 'pc_article_project_more'
       ) {
         return '已收到您提交的升级大会员申请,我们会尽快联系您并预约演示时间。'
+      } else if (this.source === 'pc_article_customization') {
+        return '已收到您提交的数据定制申请,我们的数据经理会尽快联系您~'
+      } else if (this.source === 'peugeot_supplier_regist') {
+        return '我们会尽快联系您完成供应商报名,请耐心等待。'
+      } else if (this.source.indexOf('jyarticle_see3_plus_pc') > -1) {
+        return '您已获得无限次免费查看标讯的权益,如需查看超前项目请联系客服:400-108-6670'
+      } else if (this.source === 'pc_article_BidPreparation' || this.source === 'pc_article_certificateServices' || this.source === 'certificateServices-pc-biddingDetailPage-content') {
+        return '专业老师将尽快和您联系!'
       } else {
         return '我们会尽快联系您并预约演示时间,请耐心等待~您将获得免费体验大会员全部功能!'
       }
@@ -672,6 +744,53 @@ export default {
       this.getOldInfo()
       this.isRefresh = isRefresh
     },
+    // 特殊留资字段展示
+    calcShowMoreInput (source) {
+      // 留资基本都需要填写公司类型信息、职位信息
+      this.moduleShow.companyType = true
+      this.moduleShow.job = true
+      this.moduleShow.tip = true
+
+      switch (source) {
+        case 'pc_article_customization': {
+          this.moduleShow.companyType = false
+          this.moduleShow.job = false
+          this.moduleShow.dataInput = true
+          break
+        }
+        case 'pc_article_certificateServices':
+        case 'pc_article_BidPreparation': {
+          this.moduleShow.job = false
+          this.moduleShow.tip = false
+          break
+        }
+        case 'certificateServices-pc-biddingDetailPage-content': {
+          this.moduleShow.job = false
+          this.moduleShow.tip = false
+          break
+        }
+        case 'jyarticle_see3_plus_pc': {
+          this.moduleShow.companyType = true
+          this.moduleShow.job = true
+          break
+        }
+      }
+    },
+    // 特殊字段参数处理
+    addMoreParams (source, params, type = true) {
+      let result = params
+      switch (source) {
+        case 'pc_article_customization': {
+          if (type) {
+            result.data_requirement = this.form.dataInput
+          } else {
+
+          }
+          break
+        }
+      }
+      return result
+    },
     calcTitleText(source) {
       if (!source) return
       let text = ''
@@ -684,7 +803,7 @@ export default {
 
       if (text) {
         if (
-          source.indexOf('_freeuser') > -1 ||
+          (source.indexOf('_freeuser') > -1 && text.indexOf('【') > -1) ||
           source.indexOf('buyer_portrait_bidInfo') > -1 ||
           (source.indexOf('buyer_portrait_cooperative') > -1 &&
             text.indexOf('【') > -1)
@@ -702,6 +821,8 @@ export default {
           this.moduleShow[k] = false
         }
       }
+      // 特殊字段处理
+      this.calcShowMoreInput(source)
     },
     nameFocus() {
       this.$refs.ruleForm.clearValidate(['name'])
@@ -806,6 +927,8 @@ export default {
             : this.form.branch,
         source_desc: this.sourceDescMap[source]
       }
+      // 特殊字段处理
+      params = this.addMoreParams(source, params)
       var _this = this
       $.ajax({
         type: 'POST',
@@ -881,7 +1004,8 @@ export default {
             _this.form.need = res.data.partnerNeeds ? res.data.partnerNeeds : ''
             _this.form.agreeChecked =
               res.data.agree === undefined ? true : res.data.agree
-            _this.form.branch = res.data.branch
+            _this.form.branch = res.data.branch || ''
+            _this.form.dataInput = res.data.data_requirement || ''
             // 判断当前信息否在其他页面留资  如果全部留资 直接弹窗提交成功
             // const result = checkRequiredKeys(['name', 'phone', 'company', 'branch', 'position', 'companyType'], res.data)
             // if (result) {
@@ -959,6 +1083,7 @@ export default {
       this.form.scale = ''
       this.form.business = ''
       this.form.need = ''
+      this.form.dataInput = ''
     },
     setEchoInfo(data) {
       if (data) {
@@ -992,7 +1117,8 @@ export default {
         this.form.business = data.workScope ? data.workScope : ''
         this.form.need = data.partnerNeeds ? data.partnerNeeds : ''
         this.form.agreeChecked = data.agree === undefined ? true : data.agree
-        this.form.branch = data.branch
+        this.form.branch = data.branch || ''
+        this.form.dataInput = data.data_requirement || ''
       }
     }
   }
@@ -1128,6 +1254,7 @@ export default {
   }
 }
 .custom-select {
+  z-index: 3100 !important;
   margin-top: 0 !important;
   border: 1px solid #2cb7ca;
   .el-select-dropdown__wrap {
@@ -1156,7 +1283,7 @@ export default {
     top: 0;
     bottom: 0;
     background: rgba(0, 0, 0, 0.5);
-    z-index: 1031;
+    z-index: 2031;
   }
   /* 滚动条样式 */
   .user-data-dialog ::-webkit-scrollbar {
@@ -1191,7 +1318,7 @@ export default {
     background: #fff;
     border-radius: 8px;
     transform: translate(-50%, -50%);
-    z-index: 2000;
+    z-index: 3000;
     box-sizing: border-box;
     overflow-y: auto;
   }
@@ -1235,7 +1362,7 @@ export default {
     max-height: 152px;
     background-color: #fff;
     border: 1px solid #2cb7ca;
-    z-index: 100;
+    z-index: 2100;
     // border-radius: 4px;
     overflow-y: auto;
   }
@@ -1360,7 +1487,7 @@ export default {
     background: #ffffff;
     border-radius: 8px;
     transition: all 2s linear;
-    z-index: 1038;
+    z-index: 2038;
   }
   .success-title {
     padding: 12px 0 20px;

+ 16 - 13
apps/bigmember_pc/src/components/common/ContentLayout.vue

@@ -6,21 +6,23 @@
     <div class="content-right ad-container" :class="{ nothing: adShow }">
       <slot name="right">
         <slot name="right-top"></slot>
-        <div class="ad-list" :id="adCodeMap[routerName] || routerName">
-          <div
-            class="ad-item-container"
-            v-for="(item, index) in adList"
-            :key="index"
-          >
-            <a
-              :href="item.s_link"
-              target="_blank"
-              :id="(adCodeMap[routerName] || routerName) + '-' + index"
+        <slot name="right-main">
+          <div class="ad-list" :id="adCodeMap[routerName] || routerName">
+            <div
+              class="ad-item-container"
+              v-for="(item, index) in adList"
+              :key="index"
             >
-              <img :src="item.s_pic" />
-            </a>
+              <a
+                :href="item.s_link"
+                target="_blank"
+                :id="(adCodeMap[routerName] || routerName) + '-' + index"
+              >
+                <img :src="item.s_pic" />
+              </a>
+            </div>
           </div>
-        </div>
+        </slot>
         <slot name="right-bottom"></slot>
       </slot>
     </div>
@@ -69,6 +71,7 @@ export default {
     return {
       routerName: '',
       adCodeMap: {
+        article_detail: 'jy-pccontent-right', // 标讯详情页右侧广告位
         pro_follow_detail: 'jy-pc-bigmember-project-content-right', // 项目详情页右侧广告位code
         ent_portrait: 'jy-pc-bigmember-entportrayal-content-right', // 企业情报详情页右侧广告位code
         unit_portrayal: 'jy-pc-bigmember-unitportrayal-content-right', // 采购单位全景分析详情页右侧广告位code

+ 2 - 2
apps/bigmember_pc/src/components/common/Monitor.vue

@@ -8,7 +8,7 @@
         监控业主一旦发布与“我的订阅”相关的招标动态,会推送业主的招标项目、时间等公告信息。
       </li>
       <li
-        :class="{ 'border-2': !showList.includes(1) }"
+        :class="{ 'b-style-none': !showList.includes(1) }"
         v-show="showList.length && showList.includes(2)"
         @click="$emit('monitorStatus')"
       >
@@ -105,7 +105,7 @@ export default {
       &:last-child {
         padding: 12px 0 0;
       }
-      &.border-2 {
+      &.b-style-none {
         border: none;
       }
       .list-top {

+ 164 - 0
apps/bigmember_pc/src/components/common/MonitorPopover.vue

@@ -0,0 +1,164 @@
+<template>
+  <div class="monitor-popover-content">
+    <ul class="monitor-ul">
+      <li class="first-bottom" v-if="showTip">{{ textConfig.tip }}</li>
+      <li
+        class="monitor-more-actions"
+        :class="{ 'b-style-none': !showTip }"
+        v-if="showMore"
+        @click="$emit('click', 'more')"
+      >
+        {{ textConfig.more }}
+      </li>
+      <li v-if="showList">
+        <div class="list-top" @click="$emit('click', 'list')">
+          <span>{{ textConfig.list }}</span>
+          <i class="el-icon-arrow-right"></i>
+        </div>
+        <div class="list-center">
+          <slot>
+            {{ textConfig.listNumTip }}
+            <span style="color: #2abed1; font-weight: 700">{{
+              alreadyNum
+            }}</span>
+            个,剩余
+            <span style="color: #2abed1; font-weight: 700">{{
+              remainNum
+            }}</span>
+            个
+          </slot>
+        </div>
+        <div class="list-bottom" @click="$emit('click', 'apply')">
+          {{ textConfig.apply }}
+        </div>
+      </li>
+      <li @click="$emit('click', 'cancel')" v-if="showCancel">
+        {{ textConfig.cancel }}
+      </li>
+    </ul>
+  </div>
+</template>
+
+<script>
+export default {
+  props: {
+    alreadyNum: {
+      type: Number,
+      default: 0
+    },
+    // 剩余
+    remainNum: {
+      type: Number,
+      default: 0
+    },
+    // 是否展示对应模块
+    showTip: {
+      type: Boolean,
+      default: false
+    },
+    showMore: {
+      type: Boolean,
+      default: false
+    },
+    showList: {
+      type: Boolean,
+      default: false
+    },
+    showCancel: {
+      type: Boolean,
+      default: false
+    },
+    // 文案配置
+    config: {
+      type: Object,
+      default: () => {}
+    },
+    textType: {
+      type: String,
+      default: 'unit'
+    }
+  },
+  computed: {
+    textConfig() {
+      const textConfigMap = {
+        client: {
+          tip: '监控业主一旦发布与“我的订阅”相关的招标动态,会推送业主的招标项目、时间等公告信息。',
+          more: '查看监控动态',
+          list: '查看监控列表',
+          listNumTip: '已监控',
+          apply: '申请监控更多业主',
+          cancel: '取消监控'
+        },
+        project: {
+          tip: '招标/采购进度实时监控,包含项目招标、中标等最新动态,及时跟踪项目。',
+          more: '查看监控动态',
+          list: '查看监控列表',
+          listNumTip: '已监控',
+          apply: '申请监控更多项目',
+          cancel: '取消监控'
+        },
+        ent: {
+          tip: '关注企业一旦中标,会推送企业的中标项目、时间等公告信息。',
+          more: '查看监控动态',
+          list: '查看监控列表',
+          listNumTip: '已监控',
+          apply: '申请监控更多企业',
+          cancel: '取消监控'
+        }
+      }
+      return Object.assign(textConfigMap[this.textType] || {}, this.config)
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.monitor-popover-content {
+  .monitor-ul {
+    li {
+      padding: 12px 0;
+      border-top: 1px solid #ececec;
+      font-size: 16px;
+      color: #1d1d1d;
+      cursor: pointer;
+      &:first-child {
+        border: none;
+        padding: 0 0 12px;
+        font-size: 14px;
+        color: #686868;
+      }
+      &:last-child {
+        padding: 12px 0 0;
+      }
+      &.monitor-more-actions {
+        font-size: 16px;
+      }
+      &.b-style-none {
+        border: none;
+      }
+      .list-top {
+        display: flex;
+        justify-content: space-between;
+        align-items: center;
+        color: #2abed1;
+      }
+      .list-center {
+        margin: 8px 0;
+        color: #686868;
+        font-size: 14px;
+      }
+      .list-bottom {
+        display: flex;
+        justify-content: center;
+        align-items: center;
+        width: 100%;
+        height: 32px;
+        color: #2abed1;
+        font-size: 14px;
+        background-color: #eaf8fa;
+        border-radius: 4px;
+      }
+    }
+  }
+}
+</style>

+ 0 - 1
apps/bigmember_pc/src/components/crm-info/crmAction.vue

@@ -29,7 +29,6 @@
 </template>
 
 <script>
-import '@/assets/style/iconfont.css'
 import iframeDialog from '@/components/crm-info/IframeDialog.vue'
 import {
   ajaxIgnoreOperate,

+ 1 - 1
apps/bigmember_pc/src/components/forecast/ForeCast.vue

@@ -856,7 +856,7 @@ export default {
       if (num === '') {
         setFollowEnt({ entId: id }).then((res) => {
           if (res.error_code === 0) {
-            if (res.data === 'success') {
+            if (res.data?.status) {
               this.entSearch.forEach(function (item, i) {
                 if (id === item.entId) {
                   item.isFollow = '1'

+ 1 - 1
apps/bigmember_pc/src/components/home/FloatSide.vue

@@ -34,7 +34,7 @@
       </div>
       <div class="content-group flex-c-c center">
         <div
-          class="flex info-card-group"
+          class="flex-w-100 info-card-group"
           v-if="floatSide.list && floatSide.list.length"
         >
           <div

+ 1 - 0
apps/bigmember_pc/src/components/mask-card/MaskCard.vue

@@ -21,6 +21,7 @@
         :style="{ minHeight: imgH + 'px' }"
       />
       <div class="flex-c-c center upgrade-module-group" ref="boxDom">
+        <slot name="module-top"></slot>
         <div
           class="module-img-card"
           :class="{ 'bg-size-small': item.bgSize === 'small' }"

+ 1 - 1
apps/bigmember_pc/src/components/medical/FollowList.vue

@@ -245,7 +245,7 @@ export default {
       if (num === '') {
         setFollowEnt({ entId: id }).then((res) => {
           if (res.error_code === 0) {
-            if (res.data === 'success') {
+            if (res.data?.status) {
               this.entSearch.forEach(function (item, i) {
                 if (id === item.entId) {
                   item.isFollow = '1'

+ 657 - 0
apps/bigmember_pc/src/components/push-list/ArticleStar.vue

@@ -0,0 +1,657 @@
+<template>
+  <div class="article-star-module">
+    <div class="right-actions" v-if="canStar" @click.stop="doChangeStar">
+      <i class="icon-collect" :class="{ checked: star }" :dataid="id"></i>
+      <span name="right-action" style="margin-left: 4px">{{
+        star ? '已收藏' : '收藏'
+      }}</span>
+    </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>
+
+    <!-- 留资弹窗 -->
+    <CollectInfo ref="collectRef"></CollectInfo>
+  </div>
+</template>
+
+<script>
+import CollectInfo from '@/components/collect-info/CollectInfo.vue'
+import { getEventTarget } from '@/utils/jq-help'
+
+import { mapGetters } from 'vuex'
+import {
+  bidCollAction,
+  getBidCollTagList,
+  saveBidCollAddTag,
+  createBidTag
+} from '@/api/modules/'
+/* eslint-disable */
+window.pushIdInfoIdRelationshipMap = {}
+export default {
+  name: 'article-star',
+  components: {
+    CollectInfo
+  },
+  props: {
+    id: {
+      type: String,
+      default: ''
+    },
+    star: {
+      type: Boolean,
+      default: false
+    }
+  },
+  computed: {
+    canStar () {
+      return true
+    },
+    ...mapGetters('user', ['free']),
+    isFree() {
+      return this.free
+    }
+  },
+  created() {},
+  mounted() {
+    this.initCollectEvent()
+  },
+  methods: {
+    doChangeStar(event) {
+      const $ = this.$querySelector.bind(this)
+      let binfo = [{ bid: this.id }]
+      var _this = this
+
+      if (this.star) {
+        // 单个取消收藏行为
+        this.ajaxForCollectChange('R', binfo, function (res) {
+          if (res.data) {
+            _this.$emit('change', false)
+            _this.$toast('已取消收藏', 800)
+          } else {
+            _this.$toast(res.error_msg, 1000)
+          }
+        })
+      } else {
+        if (this.isFree) {
+          this.$refs.collectRef.isNeedSubmit('article_collection', () => {
+            // 将本次收藏的标讯id缓存起来 用于绑定标签时使用
+            sessionStorage.setItem('$save-tags-binfo', JSON.stringify(binfo))
+            this.ajaxForCollectChange('C', binfo, function (res) {
+              if (res.data) {
+                _this.$toast('收藏成功', 1500)
+                _this.$emit('change', true)
+
+                $('.tags-box')
+                  .show(function () {
+                    window.pushListActiveTags = []
+                    $('.tag-labels').empty()
+                    $('.clear-input').val('')
+                    $('.tags-list')
+                      .find('.tags-item')
+                      .removeClass('tags-active')
+                    $('.tag-placeholder').show()
+                  })
+                  .css({
+                    top: '26px',
+                    right: 0
+                  })
+                window.getUserTags()
+              } else {
+                if (res.error_msg.indexOf('付费') > -1) {
+                  _this.$toast(
+                    '您的标讯收藏上限为5000条,请联系客服人员。',
+                    1500
+                  )
+                }
+              }
+            })
+          })
+        } else {
+          // 将本次收藏的标讯id缓存起来 用于绑定标签时使用
+          sessionStorage.setItem('$save-tags-binfo', JSON.stringify(binfo))
+          this.ajaxForCollectChange('C', binfo, function (res) {
+            if (res.data) {
+              _this.$toast('收藏成功', 1500)
+              _this.$emit('change', true)
+              $('.tags-box')
+                .show(function () {
+                  window.pushListActiveTags = []
+                  $('.tag-labels').empty()
+                  $('.clear-input').val('')
+                  $('.tags-list').find('.tags-item').removeClass('tags-active')
+                  $('.tag-placeholder').show()
+                })
+                .css({
+                  top: '26px',
+                  right: '0'
+                })
+              window.getUserTags()
+            } else {
+              if (res.error_msg.indexOf('付费') > -1) {
+                _this.$toast('您的标讯收藏上限为5000条,请联系客服人员。', 1500)
+              }
+            }
+          })
+        }
+      }
+    },
+    ajaxForCollectChange(baction, binfo, callback) {
+      /*
+        收藏或取消收藏ajax
+        params: {
+          baction: String, //用户行为:R:移除收藏;C:收藏(默认)非必填
+          binfo: Array, // 招标信息数组 必填
+          bid: String, // 招标信息加密后id 必填
+        }
+        参数示例 (baction=R binfo数组只需要bid即可)onSizeChange
+      */
+      const params = {
+        baction: baction,
+        binfo: binfo
+      }
+      bidCollAction(params).then((r) => {
+        callback && callback(r)
+      })
+    },
+    calcCardTopLeft(e, batch) {
+      const $ = this.$querySelector.bind(this)
+      const containerWidth = this.$el.clientWidth
+      const containerHeight = this.$el.clientHeight
+      const cardWidth = 332
+      const cardHeight = 362
+      var top = parseInt($(getEventTarget(e)).position().top) + 30 + 50
+      if (batch) {
+        top -= 30
+      }
+      var left = parseInt($(getEventTarget(e)).position().left) - 300
+
+      if (top >= containerHeight - cardHeight) {
+        top = containerHeight - cardHeight
+      }
+      if (left >= containerWidth - cardWidth) {
+        left = containerWidth - cardWidth
+      }
+
+      left += 'px'
+      top += 'px'
+      return {
+        top,
+        left
+      }
+    },
+    initCollectEvent() {
+      const _this = this
+      const $ = this.$querySelector.bind(this)
+
+      function toastFn(text, duration) {
+        _this.$toast(text, duration)
+      }
+
+      // 自定义标签
+      // 标签输入框事件
+      $('.tags-box').click(function (e) {
+        e.stopPropagation()
+      })
+
+      $('.tag-input').click(function (e) {
+        e.stopPropagation()
+        $(this).children('.tag-placeholder').hide()
+        $(this).children('input').focus()
+      })
+      // 标签输入框回车事件
+      $('.tag-input .clear-input').keydown(function (event) {
+        event.stopPropagation()
+        if (event.keyCode == 13) {
+          if (!$('.tags-box').is(':hidden')) {
+            $('.tags-inputs .add-tag-button').trigger('click')
+          }
+        }
+      })
+      // 标签输入框失去焦点事件
+      $('.tag-input .clear-input').blur(function () {
+        if ($('.tag-labels').children().length == 0 && $(this).val() == '') {
+          $('.tag-placeholder').show()
+        }
+      })
+      // 添加标签按钮事件
+      $('.tags-inputs .add-tag-button').on('click', function () {
+        var input = $('.tag-input .clear-input')
+        if (input.val().length >= 2 && input.val().length < 11) {
+          // ajax提交自定义标签
+          addTagsAjax(input.val())
+        }
+      })
+      // 点击确定按钮,绑定标签
+      $('.tags-footer .button-confirm').on('click', function () {
+        if (!$('.tags-box').is(':hidden')) {
+          var lids = ''
+          var lname = ''
+          $('.tags-item.tags-active').each(function () {
+            if ($(this).attr('data-id')) {
+              if (lids != '') {
+                lids += ','
+              }
+              if (lname != '') {
+                lname += ','
+              }
+              lids += $(this).attr('data-id')
+              lname += $(this).text()
+            }
+          })
+          var params = {
+            lids: lids,
+            laction: 'S',
+            binfo: JSON.parse(sessionStorage.getItem('$save-tags-binfo'))
+          }
+
+          // 执行保存绑定标签操作
+          if (params.lids !== '') {
+            saveChooseTags(params, function () {
+              $('.tags-footer .button-cancel').trigger('click')
+            })
+          }
+        }
+      })
+
+      $('.tags-footer .button-cancel').on('click', function () {
+        $('.tags-box').hide(function () {
+          // 标签弹框消失时 清除上次选择的标签分类
+          pushListActiveTags = []
+          $('.tag-labels').empty()
+          $('.clear-input').val('')
+          $('.tags-list').find('.tags-item').removeClass('tags-active')
+          $('.tag-placeholder').show()
+        })
+      })
+
+      window.pushListActiveTags = [] // 选中的自定义标签 作为全局变量使用
+      // 解绑自定义标签
+      function deleteInputTag(item) {
+        var index = $(item).parent().attr('data-index')
+        var id = $(item).parent().attr('data-id')
+        pushListActiveTags.splice(index, 1)
+        inputTagList()
+        $('.tags-item[data-id="' + id + '"]').removeClass('tags-active')
+      }
+
+      function inputTagList() {
+        var ht = ''
+        $('.tag-labels').html(ht)
+        pushListActiveTags.forEach(function (v, i) {
+          ht +=
+            '<span class="tag-label" data-index=' +
+            i +
+            ' data-id="' +
+            v.lid +
+            '">'
+          ht += '<em>' + v.lname + '</em>'
+          ht += '<i class="tag-close"></i>'
+          ht += '</span>'
+        })
+        $('.tag-labels')
+          .html(ht)
+          .off('click')
+          .on('click', '.tag-close', function (e) {
+            const target = getEventTarget(e)
+            deleteInputTag(target)
+          })
+        if ($('.tag-labels').children('.tag-label').length > 0) {
+          $('.tag-placeholder').hide()
+        }
+        checkTagDisabled()
+      }
+
+      // 渲染标签列表数据
+      function renderTagsList(data) {
+        if (data && data.length > 0) {
+          var ht = ''
+          data.forEach(function (v, i) {
+            ht +=
+              '<span class="tags-item" data-count=' +
+              v.count +
+              ' data-id=' +
+              v.lid +
+              '>' +
+              v.lanme +
+              '</span>'
+          })
+          $('.tags-list').html(ht)
+          pushListActiveTags.forEach(function (s, j) {
+            $('.tags-list .tags-item[data-id="' + s.lid + '"]').addClass(
+              'tags-active'
+            )
+          })
+          $('.tags-item').click(function (e) {
+            e.stopPropagation()
+            if ($(this).hasClass('disabled')) return
+            var id = $(this).attr('data-id')
+            var name = $(this).text()
+            $(this).toggleClass('tags-active')
+            if ($(this).hasClass('tags-active')) {
+              pushListActiveTags.push({
+                lid: id,
+                lname: name
+              })
+              inputTagList()
+            } else {
+              var newArr = pushListActiveTags.filter(function (item) {
+                return item.lid != id
+              })
+              pushListActiveTags = newArr
+              inputTagList()
+            }
+          })
+        }
+        inputTagList()
+      }
+
+      // 获取用户自定义标签
+      function getUserTags() {
+        getBidCollTagList().then((r) => {
+          if (r.error_code == 0 && Array.isArray(r.data)) {
+            renderTagsList(r.data.reverse())
+          }
+        })
+      }
+
+      window.getUserTags = getUserTags
+
+      /*
+        保存或清除标签 ajax
+        params: {
+          lids: String 标签id(加密后),  非必传
+          lname: String 标签名称,  非必传
+          laction: String  用户行为:S添加或绑定标签;D删除标签  非必传
+          binfo: Array 招标信息数组(已收藏的招标信息) 非必传
+          bid: String 招标信息加密后id  必传
+        }
+        1:lids为空;lname不为空;laction=”S”;binfo数组不为空->新增标签并且绑定收藏信息
+        2:lids不为空;laction=”S”;binfo数组不为空->收藏信息绑定标签
+        3:lids不为空;laction=”D”;->删除标签 并解绑收藏的信息
+      */
+      function saveChooseTags(params, callback) {
+        saveBidCollAddTag(params).then((r) => {
+          if (r.data) {
+            toastFn('标签绑定成功', 1000)
+            _this.$emit('change-labels')
+            callback && callback()
+          }
+        })
+      }
+
+      // 新增标签
+      function addTagsAjax(name) {
+        createBidTag({ name }).then((r) => {
+          if (r.data) {
+            $('.tag-input .clear-input').val('')
+            // 添加标签成功后 绑定标签
+            if (pushListActiveTags.length < 3) {
+              pushListActiveTags.push({
+                lid: r.data,
+                lname: name
+              })
+            }
+            getUserTags()
+          } else {
+            // toastFn(r.error_msg, 1000)
+            toastFn('标签已经存在,无需添加', 1000)
+          }
+        })
+      }
+
+      function checkTagDisabled() {
+        if (pushListActiveTags.length >= 3) {
+          // 禁用标签
+          $('.tags-list')
+            .find('.tags-item:not(.tags-active)')
+            .addClass('disabled')
+        } else {
+          // 解除禁用
+          $('.tags-list').find('.disabled').removeClass('disabled')
+        }
+      }
+
+      getUserTags()
+    }
+  }
+}
+/* eslint-enable */
+</script>
+<style lang="scss">
+.article-star-module {
+  position: relative;
+  .right-actions {
+    display: flex;
+    align-items: center;
+    cursor: pointer;
+  }
+  .icon-collect {
+    display: inline-block;
+    width: 20px;
+    height: 20px;
+    background: transparent
+      url(https://cdn-ali.jianyu360.com/images/collect.png) center no-repeat;
+    background-size: contain;
+    cursor: pointer;
+    vertical-align: sub;
+    ::before {
+      content: '' !important;
+    }
+  }
+  .icon-collect.checked {
+    background: transparent
+      url(https://cdn-ali.jianyu360.com/images/collected.png) center no-repeat;
+    background-size: contain;
+    ::before {
+      content: '' !important;
+    }
+  }
+
+  .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-box .tags-inputs {
+    position: relative;
+    width: 100%;
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+  }
+
+  .tags-box .tags-inputs .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;
+  }
+
+  .tags-inputs .tag-input::-webkit-scrollbar {
+    width: 8px;
+  }
+
+  .tags-inputs .tag-input::-webkit-scrollbar-thumb {
+    border-radius: 4px;
+    background-color: #ececec;
+  }
+
+  .tag-input > .tag-labels {
+    display: inline;
+    vertical-align: middle;
+  }
+
+  .tag-input > .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;
+  }
+
+  .tag-input > .tag-labels > .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;
+  }
+
+  .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;
+  }
+
+  .tag-placeholder {
+    position: absolute;
+    top: 12px;
+    left: 16px;
+    color: #bbb;
+    font-size: 14px;
+  }
+
+  .tags-box .tags-list {
+    margin-top: 12px;
+    overflow-y: auto;
+    flex: 1;
+  }
+
+  .tags-box .tags-list::-webkit-scrollbar {
+    width: 8px;
+  }
+
+  .tags-box .tags-list::-webkit-scrollbar-thumb {
+    border-radius: 4px;
+    background-color: #ececec;
+  }
+
+  .tags-box .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;
+  }
+
+  .tag-label em {
+    font-style: normal;
+  }
+
+  .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;
+  }
+}
+</style>

+ 1 - 2
apps/bigmember_pc/src/components/push-list/PotentialList.vue

@@ -162,7 +162,6 @@
 </template>
 
 <script>
-import '@/assets/style/iconfont.css'
 import commonDialog from '@/components/dialog/Dialog.vue'
 import CollectInfo from '@/components/collect-info/CollectInfo.vue'
 import { Pagination, Card, Button, Dialog, Popover } from 'element-ui'
@@ -401,7 +400,7 @@ export default {
     // 获取监控数量
     async getListStatus(list) {
       const nameList = []
-      if (!list) return
+      if (!list || list.length === 0) return
       list.forEach((v, i) => {
         if (i === 0) {
           nameList.push(v.Buyer)

+ 62 - 12
apps/bigmember_pc/src/components/time-line/PoverTimeLine.vue

@@ -1,21 +1,29 @@
 <template>
-  <div class="j-pover-step">
+  <div class="j-pover-step" @mouseenter="isHover = true" @mouseleave="doHide">
     <el-popover
-      popper-class="poverStep"
-      placement="bottom-start"
+      :popper-class="poperClass"
+      :placement="poperPlacement"
       :open-delay="300"
       :append-to-body="false"
       :width="poperWidth"
       @show="show"
-      trigger="hover"
+      :trigger="trigger"
+      v-model="popoverShow"
     >
       <slot name="content" slot="reference"></slot>
-      <el-card class="project-content" v-show="stepList.length !== 0">
-        <div slot="header" class="p-h-title">项目公告</div>
-        <div class="p-c-main">
-          <TimeLine poverType="card" :stepList="stepList" />
-        </div>
-      </el-card>
+      <slot name="main">
+        <el-card class="project-content" v-show="stepList.length !== 0">
+          <div slot="header" class="p-h-title">{{ title }}</div>
+          <div class="p-c-main">
+            <TimeLine
+              poverType="card"
+              :custom-event="customEvent"
+              :stepList="stepList"
+              @open="$emit('open', $event)"
+            />
+          </div>
+        </el-card>
+      </slot>
     </el-popover>
   </div>
 </template>
@@ -32,6 +40,14 @@ export default {
     TimeLine
   },
   props: {
+    trigger: {
+      type: String,
+      default: 'hover'
+    },
+    title: {
+      type: String,
+      default: '项目公告'
+    },
     stepList: {
       type: Array,
       default() {
@@ -43,11 +59,43 @@ export default {
       default() {
         return '720'
       }
+    },
+    poperClass: {
+      type: String,
+      default() {
+        return 'poverStep fixed-left'
+      }
+    },
+    poperPlacement: {
+      type: String,
+      default() {
+        return 'bottom-start'
+      }
+    },
+    customEvent: {
+      type: Boolean,
+      default: false
+    }
+  },
+  data() {
+    return {
+      popoverShow: false,
+      isHover: false
     }
   },
   methods: {
     show() {
       this.$emit('show')
+    },
+    doHide() {
+      this.isHover = false
+      this.popoverShow = false
+    },
+    doChangePopover(state) {
+      if (this.isHover) {
+        return
+      }
+      this.popoverShow = state
     }
   }
 }
@@ -57,8 +105,10 @@ export default {
 .el-popover.poverStep {
   padding: 0;
   border: 0;
-  .popper__arrow {
-    left: 100px !important;
+  &.fixed-left {
+    .popper__arrow {
+      left: 100px !important;
+    }
   }
 }
 </style>

+ 14 - 4
apps/bigmember_pc/src/components/time-line/TimeLine.vue

@@ -17,14 +17,16 @@
             <span
               class="step-tag"
               v-for="(tag, index) in item.tags"
+              v-show="tag"
               :key="index"
               >{{ tag }}</span
             >
             <span class="item-time">{{ item.time }}</span>
+            <slot name="after" :item="item"></slot>
           </div>
-          <span class="step-tag grey" v-show="item.bidamount && poverTypes">{{
-            moneyUnit(item.bidamount)
-          }}</span>
+          <span class="step-tag grey" v-show="item.bidamount && poverTypes">
+            {{ moneyUnit(item.bidamount) }}
+          </span>
         </div>
         <div class="step-item cursor" @click="linkTo(item)">
           <span class="item-label" v-html="item.content"></span>
@@ -50,6 +52,10 @@ export default {
       default() {
         return ''
       }
+    },
+    customEvent: {
+      type: Boolean,
+      default: false
     }
   },
   computed: {
@@ -60,7 +66,11 @@ export default {
   methods: {
     moneyUnit,
     linkTo(item) {
-      window.open(`/nologin/content/${item.s_id}.html`)
+      if (this.customEvent) {
+        return this.$emit('open', item)
+      } else {
+        window.open(`/nologin/content/${item.s_id}.html`)
+      }
     }
   }
 }

+ 0 - 1
apps/bigmember_pc/src/components/work-desktop/Slidebar.vue

@@ -37,7 +37,6 @@
 </template>
 
 <script>
-import '@/assets/style/iconfont.css'
 import { Menu, Submenu, MenuItem, MenuItemGroup, MessageBox } from 'element-ui'
 import { getLeftMenu } from '@/api/modules'
 export default {

+ 456 - 0
apps/bigmember_pc/src/composables/attachment-download/component/AttachmentDownload.vue

@@ -0,0 +1,456 @@
+<template>
+  <section class="attachment-download-container" v-if="renderAttachList.length">
+    <div class="others-header flex flex-(items-center)">
+      <div class="content-file-attachment-left flex flex-items-center">
+        <span class="left-icon flex flex-items-center">
+          <span class="file-attachment-text text-nowrap">附件下载</span>
+        </span>
+        <div class="right-content flex flex-items-center">
+          <!-- 免费用户,无体验次数(没体验过) -->
+          <template v-if="isFree && freeFileNum === 0">
+            <span class="attachment-tag text-nowrap">
+              <span class="attachment-tag-text">
+                免费用户享有{{ freeFileNum || 1 }}次附件下载权益
+              </span>
+            </span>
+          </template>
+          <!-- 免费用户,无体验次数(体验过) -->
+          <template v-else-if="isFree && freeFileNum < 0">
+            <span class="attachment-tag text-nowrap">
+              <span class="attachment-tag-text">下载更多附件</span>
+              <button class="open-vip-btn" @click="toBuySvip">
+                开通超级订阅
+              </button>
+            </span>
+          </template>
+          <!-- 新超级订阅,并且不是大会员(或者是大会员没有附件下载权限) -->
+          <template v-else-if="isNewSuper && !memberHasAttachPower">
+            <span class="attachment-tag text-nowrap">
+              <span class="attachment-tag-text">本月剩余:{{ fileNum }}个</span>
+            </span>
+            <i class="iconfont icon-help" @click="fileDownloadHelp"></i>
+          </template>
+        </div>
+      </div>
+      <div class="content-file-attachment-actions">
+        <span
+          class="action-button"
+          @click="chargeFilePack"
+          v-if="isNewSuper && !memberHasAttachPower"
+        >
+          立即充值
+        </span>
+      </div>
+    </div>
+    <div class="file-attachment-list">
+      <div
+        class="file-attachment-item highlight-text underline clickable"
+        v-for="(attach, index) in renderAttachList"
+        @click="startDownloadFile(attach)"
+        :key="index"
+      >
+        {{ attach.name }}
+      </div>
+    </div>
+  </section>
+</template>
+
+<script>
+import { mapState, mapGetters } from 'vuex'
+import { useGetContentAttachment } from '@/composables/attachment-download/'
+
+export default {
+  name: 'AttachmentDownload',
+  props: {
+    id: {
+      type: String,
+      required: true,
+      default: ''
+    },
+    type: {
+      type: String,
+      default: ''
+    },
+    title: {
+      type: String,
+      required: true,
+      default: ''
+    },
+    attachmentList: {
+      type: Array,
+      default() {
+        return [
+          // {
+          //   fileName: '附件1.pdf',
+          //   fileSize: '1.9 M',
+          //   fileType: 'pdf'
+          // },
+          // {
+          //   fileName: '附件2.pdf',
+          //   fileSize: '129 KB',
+          //   fileType: 'pdf'
+          // }
+        ]
+      }
+    }
+  },
+  data() {
+    return {
+      loading: false,
+      attachment: {
+        fileUrl: '', // 当前附件真实url
+        file: {}, // 当前需要下载的附件信息
+        downloaded: false
+      },
+      resourcePack: {
+        exchangeNum: 0,
+        freeNum: 0,
+        grantNum: 0,
+        name: '附件下载包',
+        number: 0,
+        purchaseNum: 0,
+        resourceType: '附件下载包',
+        thirtyNum: 0
+      }
+    }
+  },
+  computed: {
+    ...mapGetters('user', [
+      'isFree',
+      'isSuper',
+      'isMember',
+      'bigMemberPower',
+      'isBusiness'
+    ]),
+    ...mapState({
+      power: (state) => state.user.info
+    }),
+    // 免费用户免费体验次数
+    freeFileNum() {
+      if (this.resourcePack.number > 0) {
+        return this.resourcePack.number
+      } else {
+        if (this.attachment.downloaded) {
+          return -1
+        } else {
+          return this.power?.freeFile
+        }
+      }
+    },
+    fileNum() {
+      if (this.resourcePack.number > 0) {
+        return this.resourcePack.number
+      }
+      return this.power?.fileNum || 0
+    },
+    isNewSuper() {
+      return this.power.viper && this.isSuper
+    },
+    memberHasAttachPower() {
+      return this.isMember && this.bigMemberPower.indexOf(3) !== -1
+    },
+    renderAttachList() {
+      return this.attachmentList.map((a) => {
+        let size = a.fileSize
+        return {
+          name: a.fileName,
+          size: size,
+          type: a.fileType
+        }
+      })
+    }
+  },
+  created() {
+    this.getInfo()
+  },
+  methods: {
+    replaceSpace(n) {
+      return n.trim().replace(/\s+/g, '')
+    },
+    async getInfo() {
+      const { attachment } = useGetContentAttachment({ id: this.id })
+      this.attachmentInstance = attachment
+      await this.attachmentInstance.getResourcePackAccount()
+      this.resourcePack = this.attachmentInstance.resourcePack
+    },
+    async refreshResourcePackCount() {
+      if (!this.attachmentInstance) {
+        return this.getInfo()
+      }
+      await this.attachmentInstance.getResourcePackAccount()
+      this.resourcePack = this.attachmentInstance.resourcePack
+    },
+    showDialog(conf = {}) {
+      const defaultConf = {
+        title: '',
+        message: '',
+        customClass: 'custom-message-box',
+        confirmButtonText: '我知道了',
+        confirmButtonClass: 'custom-confirm-btn',
+        cancelButtonClass: 'custom-cancel-btn',
+        showClose: false,
+        showCancelButton: false,
+        closeOnClickModal: false,
+        center: true
+      }
+      Object.assign(defaultConf, conf)
+      return this.$confirm(defaultConf.message, defaultConf.title, defaultConf)
+    },
+    fileDownloadHelp() {
+      this.$alert(
+        '点击附件即为下载,系统会扣除当月附件下载个数;每月1号上月余额清零重新计算,请合理使用。',
+        '',
+        {
+          confirmButtonText: '我知道了',
+          confirmButtonColor: '#2ABDD1',
+          showClose: false,
+          center: true
+        }
+      )
+    },
+    async startDownloadFile(file) {
+      // 大客户直接下载
+      if (location.pathname.indexOf('entservice') !== -1) {
+        return this.downloadFile(file)
+      }
+      if (this.isFree) {
+        // 免费用户
+        // 判断有无体验过 0:未体验过
+        if (this.freeFileNum === 0) {
+          // TODO 判断有无留过资 且未体验过 - 去留资 source: 'article_attach_freeuser'
+          this.$emit('doOpenCollect', {
+            source: 'article_attach_freeuser',
+            reload: true
+          })
+        } else if (this.freeFileNum < 0 && this.resourcePack.number <= 0) {
+          // 免费用户 体验过 下载次数为-1 弹框提醒跳至超级订阅购买页
+          // 并且剑鱼币兑换的附件下载权益没有余额
+          return this.showDialog({
+            title: '开通超级订阅',
+            message:
+              '您的免费【附件下载】次数已使用完,暂无免费查看权限。如需查看更多,请开通超级订阅获取更多权限。',
+            showCancelButton: true,
+            confirmButtonText: '去开通'
+          })
+            .then(() => {
+              this.toBuySvip()
+            })
+            .catch(() => {})
+        } else {
+          // P317版本改为免费用户只要有下载次数,均可正常下载
+          this.downloadFile(file)
+          this.attachment.downloaded = true
+        }
+      } else {
+        // 付费用户
+        // 大会员用户 有下载个数
+        if (this.memberHasAttachPower) {
+          return this.downloadFile(file)
+        }
+        // 超级订阅用户
+        if (this.isSuper) {
+          // 新超级订阅用户
+          if (this.isNewSuper) {
+            // 是否用完弹窗放到请求之后,根据请求返回值进行判断
+            return this.downloadFile(file)
+            // if (this.fileNum > 0) {
+            //   this.downloadFile(file)
+            // } else {
+            //   // 次数用完
+            //   return this.showDialog({
+            //     message:
+            //       '您本月附件下载机会已消耗完毕,如需下载更多附件,请前往充值。',
+            //     showCancelButton: true,
+            //     confirmButtonText: '立即充值'
+            //   })
+            //     .then(() => {
+            //       // this.concatKf()
+            //       this.chargeFilePack()
+            //     })
+            //     .catch(() => {})
+            // }
+          } else {
+            // 老超级订阅用户 提醒升级
+            return this.showDialog({
+              title: '升级超级订阅',
+              message: '对不起,暂无权限,您可升级超级订阅解锁附件下载',
+              showCancelButton: true,
+              confirmButtonText: '前往升级'
+            })
+              .then(() => {
+                this.toUpgradeSvip()
+              })
+              .catch(() => {})
+          }
+        }
+
+        // 商机管理只要有个数就能下载
+        if (this.isBusiness && this.resourcePack.number > 0) {
+          return this.downloadFile(file)
+        }
+
+        // 大会员自定义版本没有下载权限 或 非超级订阅的商机管理用户 (弹框提醒联系客服)
+        const isMemberButNoPower = this.isMember && !this.memberHasAttachPower
+        const noAttachmentDownloadPower = isMemberButNoPower && !this.isNewSuper
+        if (noAttachmentDownloadPower || (!this.isSuper && this.isBusiness)) {
+          // 老超级订阅用户 提醒升级
+          return this.showDialog({
+            message:
+              '您未购买此服务,如需使用请联系您的客户经理或客服升级套餐,谢谢!',
+            confirmButtonText: '我知道了'
+          })
+            .then(() => {})
+            .catch(() => {})
+        }
+      }
+    },
+    async downloadFile(file) {
+      this.attachment.file = file
+      // downUrl: 原始url
+      // fileUrl: 下载地址
+      const { downUrl, fileUrl } = await this.getAttachmentInfo(file)
+      this.refreshResourcePackCount()
+
+      if (downUrl && fileUrl) {
+        location.href = fileUrl
+      } else {
+        console.log('获取附件fid失败')
+      }
+    },
+    async getAttachmentInfo(file) {
+      this.loading = true
+      if (!this.loading) return
+      const params = {
+        id: this.id, // 附件详情页id
+        fileName: file.name, // 附件名称
+        infoType: this.type === 'issued' ? 'S' : '', // 信息类型:默认为空; 供应信息:S
+        productName: '附件下载包',
+        platform: 'PC', // 平台:PC;APP;WX
+        title: this.title // 附件详情页标题附件详情页标题
+      }
+      try {
+        const { r: data, m: msg } =
+          await this.attachmentInstance.useResourcePack(params)
+        // 各种余额不足提示
+        if (data) {
+          if (data.code && data.code < 0) {
+            if (this.isFree) {
+              this.showDialog({
+                title: '开通超级订阅',
+                message:
+                  '您的免费【附件下载】次数已使用完,暂无免费查看权限。如需查看更多,请开通超级订阅获取更多权限。',
+                showCancelButton: true,
+                confirmButtonText: '去开通'
+              })
+                .then(() => {
+                  this.toBuySvip()
+                })
+                .catch(() => {})
+            } else if (this.isSuper) {
+              this.showDialog({
+                title: '开通超级订阅',
+                message:
+                  '您本月附件下载机会已消耗完毕,如需下载更多附件,请前往充值。',
+                showCancelButton: true,
+                confirmButtonText: '立即充值'
+              })
+                .then(() => {
+                  this.chargeFilePack()
+                })
+                .catch(() => {})
+            } else {
+              this.$toast(msg || '获取附件地址失败')
+            }
+          } else if (!msg && data.downUrl) {
+            const downUrl = data.downUrl
+            const fileUrl = downUrl
+              ? `${downUrl}?response-content-type=application/octet-stream`
+              : ''
+            return {
+              ...data,
+              fileUrl // 真实下载地址
+            }
+          } else {
+            this.$toast(msg || '获取附件地址失败')
+          }
+        } else {
+          this.$toast(msg || '获取附件地址失败')
+        }
+      } catch (error) {
+        console.log(error)
+      } finally {
+        this.loading = false
+      }
+    },
+    toBuySvip() {
+      window.open('/swordfish/page_big_pc/free/svip/buy', '_blank')
+    },
+    chargeFilePack() {
+      window.open('/swordfish/page_big_pc/free/filePack/buy', '_blank')
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.attachment-download-container {
+  margin-top: 40px;
+  margin-bottom: 70px;
+}
+.file-attachment-text {
+  margin-right: 12px;
+  font-size: 16px;
+  line-height: 24px;
+}
+.attachment-tag {
+  margin: 0 8px;
+  padding: 2px 0;
+  font-size: 12px;
+  line-height: 18px;
+  color: $color-main;
+  background: $color_main_background;
+  border-radius: 10px;
+  &-text {
+    padding: 0 8px;
+    color: $color-main;
+  }
+}
+.action-button {
+  flex-shrink: 0;
+  display: inline-block;
+  margin-left: 24px;
+  text-align: center;
+  cursor: pointer;
+  padding: 0 16px;
+  min-width: 64px;
+  font-size: 12px;
+  line-height: 30px;
+  border-radius: 4px;
+  color: #fff;
+  background-color: $color-main;
+}
+.file-attachment-list {
+  margin-top: 16px;
+  font-size: 15px;
+  line-height: 22px;
+  .file-attachment-item {
+    display: block;
+    cursor: pointer;
+    margin-bottom: 16px;
+  }
+}
+
+.right-content {
+  .iconfont {
+    cursor: pointer;
+    color: $color-main;
+  }
+}
+
+.open-vip-btn {
+  color: #fff;
+  padding: 0 8px;
+  border-radius: inherit;
+  background-color: $color-main;
+}
+</style>

+ 87 - 0
apps/bigmember_pc/src/composables/attachment-download/index.js

@@ -0,0 +1,87 @@
+import {
+  ajaxGetAttachmentList,
+  getResourcePackAccount,
+  useResourcePack
+} from '@/api/modules/detail'
+
+export class Attachment {
+  constructor(name, size, downUrl, originUrl) {
+    this.name = name || ''
+    this.size = size || ''
+    this.downUrl = downUrl || ''
+    this.originUrl = originUrl || ''
+  }
+}
+
+class AttachmentList {
+  constructor({ id }) {
+    // 标讯id,必传
+    this.id = id
+    if (!id) {
+      return console.error('id必传')
+    }
+    // 附件列表
+    this.attachmentList = []
+    this.resourcePack = null
+  }
+
+  replaceSpace(n) {
+    return n.trim().replace(/\s+/g, '')
+  }
+
+  async getAttachList() {
+    const id = this.id
+    try {
+      const {
+        error_code: code,
+        error_msg: msg,
+        data
+      } = await ajaxGetAttachmentList({ infoId: id })
+      if (code === 0 && data) {
+        if (Array.isArray(data.attachment) && data.attachment.length > 0) {
+          this.attachmentList = data.attachment.map((a) => {
+            let size = a.size
+            return new Attachment(a.filename, size, a.downurl, a.org_url)
+          })
+          await this.getResourcePackAccount()
+        }
+      } else {
+        console.log(msg)
+      }
+    } catch (error) {
+      console.log(error)
+    }
+  }
+
+  async getResourcePackAccount() {
+    const params = {
+      product: 'attachmentDownPack'
+    }
+    try {
+      const { data } = await getResourcePackAccount(params)
+      if (data && Array.isArray(data.data)) {
+        this.resourcePack = data.data[0]
+        return this.resourcePack
+      }
+    } catch (error) {
+      console.log(error)
+    }
+  }
+
+  async useResourcePack({ platform, fileName, id, title }) {
+    const params = {
+      productName: '附件下载包',
+      platform: platform.toLowerCase(),
+      fileName,
+      id,
+      title
+    }
+    return useResourcePack(params)
+  }
+}
+
+export function useGetContentAttachment({ id }) {
+  return {
+    attachment: new AttachmentList({ id })
+  }
+}

+ 20 - 0
apps/bigmember_pc/src/composables/down-project-report/README.md

@@ -0,0 +1,20 @@
+# 下载项目报告
+
+## 组合式函数使用
+
+```
+const { doClickDown } = useDownProjectReportModel({
+  id: props.id,
+  name: props.name
+})
+```
+
+## 组件使用
+
+```
+<down-project-report
+  v-if="projectName"
+  :id="contentId"
+  :name="projectName"
+></down-project-report>
+```

+ 80 - 0
apps/bigmember_pc/src/composables/down-project-report/component/DownProjectReport.vue

@@ -0,0 +1,80 @@
+<script setup>
+import { useDownProjectReportModel } from '@/composables/down-project-report'
+
+const props = defineProps({
+  label: {
+    type: String,
+    default: '下载项目报告'
+  },
+  showVip: {
+    type: Boolean,
+    default: false
+  },
+  id: String,
+  name: String
+})
+
+const { doClickDown, loading } = useDownProjectReportModel({
+  id: props.id,
+  name: props.name
+})
+</script>
+<template>
+  <div
+    class="flex-r-c center left down-project-report"
+    @click="doClickDown"
+    v-loading="loading"
+  >
+    <i class="el-icon-jy-report"></i>
+    <i class="el-icon-jy-report-active"></i>
+    <span>{{ label }}</span>
+    <i class="el-icon-jy-vip" v-if="showVip"></i>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+@include diy-icon('vip', 38, 18);
+@include diy-icon('report', 18, 18);
+@include diy-icon('report-active', 18, 18);
+.down-project-report {
+  cursor: pointer;
+  font-size: 14px;
+  font-weight: 400;
+  color: #686868;
+  line-height: 22px;
+
+  ::v-deep .el-loading-spinner {
+    margin-top: -12px;
+
+    .circular {
+      width: 24px;
+      height: 24px;
+
+      .path {
+        stroke: #999;
+      }
+    }
+  }
+
+  .el-icon-jy-report-active {
+    display: none;
+  }
+
+  &:hover {
+    color: #2cb7ca;
+
+    .el-icon-jy-report-active {
+      display: inline-block;
+    }
+
+    .el-icon-jy-report {
+      display: none;
+    }
+  }
+
+  > span {
+    display: inline-block;
+    margin: 0 4px;
+  }
+}
+</style>

+ 75 - 0
apps/bigmember_pc/src/composables/down-project-report/index.js

@@ -0,0 +1,75 @@
+import { getProjectReport } from '@/api/modules'
+import { computed, getCurrentInstance, ref } from 'vue'
+import { useStore } from '@/store'
+
+/**
+ * 下载文件
+ * @param name
+ * @param url
+ */
+function downFile(name, url) {
+  const a = document.createElement('a')
+  const filename = name + '.pdf'
+  a.href = url
+  a.target = '_blank'
+  a.download = filename
+  a.click()
+}
+
+export function useDownProjectReportModel({ name, id }) {
+  const loading = ref(false)
+  const reportInfo = ref({
+    link: ''
+  })
+  const that = getCurrentInstance().proxy
+  const getters = useStore().getters
+  const isFree = computed(() => {
+    return useStore().getters['user/isFree']
+  })
+
+  /**
+   * 下载项目报告
+   * 1. 判断是否有权限
+   * 2. 请求下载地址,下载
+   * @return {Promise<void>}
+   */
+
+  function doClickDown() {
+    if (isFree.value) {
+      return that.$router.push('/free/svip/buy')
+    }
+    if (loading.value) {
+      that.$message.warning('正在生成,请稍后~')
+      return
+    }
+    if (reportInfo.value.link) {
+      return downFile(name, reportInfo.value.link)
+    }
+    // 权益判断
+    return doDownProjectReport({ name, id })
+  }
+
+  async function doDownProjectReport({ name, id }) {
+    loading.value = true
+    return getProjectReport({ sid: id })
+      .then((res) => {
+        if (res && res.error_code === 0) {
+          reportInfo.value.link = res.data.path
+          downFile(name, reportInfo.value.link)
+        } else {
+          that.$message.warning(res.error_msg)
+        }
+        loading.value = false
+      })
+      .catch((err) => {
+        console.warn(err)
+        that.$message.warning('请稍后重试')
+        loading.value = false
+      })
+  }
+
+  return {
+    doClickDown,
+    loading
+  }
+}

+ 16 - 0
apps/bigmember_pc/src/composables/quick-join-bid/README.md

@@ -0,0 +1,16 @@
+# 参标业务
+
+## 组合式函数
+```
+// 参标
+const {
+  BidrenewalDialogElement,
+  JoinBidModel,
+  doFetchJoinBid,
+  doChangeJoinBid
+} = useQuickJoinBidModel({ id: contentId })
+
+
+doFetchJoinBid()
+```
+

+ 130 - 0
apps/bigmember_pc/src/composables/quick-join-bid/index.js

@@ -0,0 +1,130 @@
+/**
+ * 参标业务模型
+ * 1. 获取参标状态、参标人、倒计时信息
+ * 2. 操作,参标、终止参标 更新状态
+ */
+import { getDetailBidIsJoin, joinBidAction } from '@/api/modules'
+import { getCountDown } from '@/utils'
+import { computed, getCurrentInstance, reactive, ref, toRefs } from 'vue'
+
+class JoinBidAPIModel {
+  constructor({ id, $toast, onUpdate }) {
+    this.id = id
+    this.$toast = $toast
+    this.onUpdate = onUpdate
+    // 状态信息
+    this.projectInfo = {
+      joinBidInfo: {}
+    }
+    // 投标倒计时
+    this.bidCountdown = null
+    this.bidTimer = null
+  }
+
+  // 获取当前项目是否参标
+  async getJoinBidInfo() {
+    const _this = this
+    try {
+      const { error_code: code, data = {} } = await getDetailBidIsJoin({
+        sid: this.id
+      })
+      if (code === 0) {
+        const rData = data || {}
+        // 处理参标人
+        let nameStr = ''
+        if (rData?.userName?.split(',').length > 1) {
+          nameStr = rData?.userName?.split(',').slice(0, 1) + ' 等'
+        } else {
+          nameStr = rData?.userName
+        }
+        rData.nameStr = nameStr
+        if (this.bidTimer) {
+          clearInterval(this.bidTimer)
+        }
+        _this.bidCountdown = null
+        // 开标倒计时
+        const bidEndTime = rData.bidEndTime ? rData.bidEndTime * 1000 : null
+        let currentTime = rData.currentTime
+          ? rData.currentTime * 1000
+          : Date.now()
+        if (bidEndTime && bidEndTime > currentTime && bidEndTime > Date.now()) {
+          this.bidCountdown = getCountDown(bidEndTime, currentTime)
+          this.bidTimer = setInterval(function () {
+            currentTime += 1000
+            _this.bidCountdown = getCountDown(bidEndTime, currentTime)
+            if (bidEndTime <= currentTime) {
+              _this.bidCountdown = null
+              rData.showParticipate = false
+              rData.showStopParticipate = false
+              if (_this.bidTimer) {
+                clearInterval(_this.bidTimer)
+              }
+            }
+          }, 1000)
+        }
+        this.projectInfo.joinBidInfo = {
+          ...rData
+        }
+      }
+    } catch (e) {
+      console.log(e)
+    }
+  }
+
+  // 参标or终止参标
+  async joinBidHandle(action) {
+    const {
+      error_code: code,
+      error_msg: msg,
+      data
+    } = await joinBidAction(action, { projectIds: this.id })
+    if (code === 0 && data) {
+      if (action === 'out') {
+        this.$toast('终止参标操作成功!')
+      } else {
+        // this.$toast('已参标,请前往我的参标项目列表查看。')
+        // 弹出更新弹窗
+        this.onUpdate()
+      }
+      // 更新操作记录列表
+
+      // 获取当前项目是否参标
+      await this.getJoinBidInfo()
+    } else if (code === -1) {
+      this.$toast(msg || '操作错误,请稍后重试')
+    }
+  }
+}
+
+export function useQuickJoinBidModel({ id }) {
+  const that = getCurrentInstance().proxy
+  const BidrenewalDialogElement = ref(null)
+  const JoinBidAPI = new JoinBidAPIModel({
+    id,
+    $toast: that.$toast,
+    onUpdate: () => {
+      if (BidrenewalDialogElement?.value) {
+        BidrenewalDialogElement?.value.openDialog()
+        BidrenewalDialogElement?.value.setid(id)
+        BidrenewalDialogElement?.value.refreshData()
+      }
+    }
+  })
+  const { projectInfo, bidCountdown } = toRefs(reactive(JoinBidAPI))
+
+  const doFetchJoinBid = () => JoinBidAPI.getJoinBidInfo()
+  const doChangeJoinBid = (action) => JoinBidAPI.joinBidHandle(action)
+  const JoinBidModel = computed(() => {
+    return {
+      projectInfo: projectInfo.value,
+      bidCountdown: bidCountdown.value
+    }
+  })
+
+  return {
+    JoinBidModel,
+    BidrenewalDialogElement,
+    doFetchJoinBid,
+    doChangeJoinBid
+  }
+}

+ 54 - 0
apps/bigmember_pc/src/composables/quick-monitor/README.md

@@ -0,0 +1,54 @@
+# useQuickMonitorModel
+> 监控(项目监控、企业监控、客户监控)业务,需要配合 @jy/data-models/quick-monitor/model 使用
+
+## 前置要求
+
+* 引入 @jy/data-models
+
+
+## 业务模型使用
+```vue
+// 导入组件
+import QuickMonitor from '@/composables/quick-monitor/component/QuickMonitor.vue'
+
+<quick-monitor
+  class="action-item"
+  type="project"
+  :params="contentId"
+/>
+```
+
+### QuickMonitor
+#### Props 参数文档
+
+|      参数       |  描述   |                        类型                         |     默认值      |
+|:-------------:|:-----:|:-------------------------------------------------:|:------------:|
+| cache  |            是否缓存数据,开启后,当数据变化时,会重新获取数据            | Boolean | false |
+|  type  | 用于监控的类型 (project - 项目 / ent - 企业 / client - 客户) | String | - |
+| params |               对应的监控项目ID、企业ID、客户ID               | String | - |
+|  auto  |                 是否自动获取当前监控状态信息                  | Boolean | true |
+
+
+
+#### 单例模式
+> 用于页面存在多个监控按钮,但是对应同一个 type + id 时。实现数据共享,避免重复请求。
+
+```
+// 第一个监控按钮,调用接口
+<quick-monitor
+  class="action-item"
+  :cache="true"
+  :auto="true"
+  type="project"
+  :params="contentId"
+/>
+
+// 第二个监控按钮,不调用接口获取数据
+<quick-monitor
+  class="action-item"
+  :cache="true"
+  :auto="false"
+  type="project"
+  :params="contentId"
+/>
+```

+ 145 - 0
apps/bigmember_pc/src/composables/quick-monitor/component/QuickMonitor.vue

@@ -0,0 +1,145 @@
+<script setup>
+import commonDialog from '@/components/dialog/Dialog.vue'
+import CollectInfo from '@/components/collect-info/CollectInfo.vue'
+import MonitorPopover from '@/components/common/MonitorPopover.vue'
+import { useQuickMonitorModel } from '@/composables/quick-monitor'
+
+const props = defineProps({
+  type: String,
+  params: String,
+  // 如果需要多个监控复用一个 model,需要传递 true
+  cache: {
+    type: Boolean,
+    default: false
+  },
+  // 自动调用 doFetch 获取状态接口
+  auto: {
+    type: Boolean,
+    default: true
+  }
+})
+
+const {
+  // 基础展示信息
+  model,
+  doFetch,
+  doAddFollow,
+  TextConfig,
+  // popover 悬浮信息
+  monitorPopoverConfig,
+  doClickMonitorActions,
+  // dialog 提示弹窗
+  dialogConfig,
+  // 留资
+  collectElement
+} = useQuickMonitorModel({
+  type: props.type,
+  id: props.params,
+  cache: props.cache
+})
+
+if (props.auto) {
+  doFetch()
+}
+
+function getParams() {
+  return props.params
+}
+
+defineExpose({
+  model,
+  getParams
+})
+</script>
+<template>
+  <div class="quick-monitor" v-if="model.canFollow">
+    <!--  icon + popover  -->
+    <div class="quick-monitor-popover">
+      <el-popover
+        popper-class="monitor-popover"
+        placement="bottom"
+        :append-to-body="false"
+        width="224"
+        trigger="hover"
+      >
+        <div
+          slot="reference"
+          class="flex-r-c center action-icon"
+          @click.stop="doAddFollow"
+        >
+          <i
+            class="icon iconfont"
+            :class="{
+              'icon-yijiankong': model.follow,
+              'icon-jiankong': !model.follow
+            }"
+          ></i>
+          <span>{{
+            model.follow ? TextConfig.follow : TextConfig.default
+          }}</span>
+        </div>
+        <monitor-popover
+          v-bind="monitorPopoverConfig"
+          @click="doClickMonitorActions"
+        ></monitor-popover>
+      </el-popover>
+    </div>
+    <!-- 提示弹窗 -->
+    <common-dialog
+      center
+      custom-class="monitor-class"
+      width="380px"
+      :visible="dialogConfig.show"
+      :title="dialogConfig.title"
+    >
+      <template #footer>
+        <button
+          v-for="(item, index) in dialogConfig.footerActions"
+          :key="index"
+          class="action-button"
+          :class="item.class"
+          @click="doClickMonitorActions(item.action || 'doCloseDialog')"
+        >
+          {{ item.label }}
+        </button>
+      </template>
+      {{ dialogConfig.content }}
+    </common-dialog>
+    <!-- 留资 -->
+    <collect-info ref="collectElement"></collect-info>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+.quick-monitor-popover {
+  position: relative;
+}
+.action-icon {
+  cursor: pointer;
+  .iconfont {
+    font-size: 18px;
+    color: #9b9ca3;
+    &.icon-yijiankong {
+      color: #ff9f40;
+    }
+    & + span {
+      margin-left: 2px;
+    }
+  }
+}
+::v-deep {
+  .monitor-class {
+    padding: 32px;
+    .el-dialog__header {
+      padding: 0;
+    }
+    .el-dialog__body {
+      padding: 20px 0 32px;
+      text-align: center;
+    }
+    .el-dialog__footer {
+      padding: 0;
+    }
+  }
+}
+</style>

+ 47 - 0
apps/bigmember_pc/src/composables/quick-monitor/index.js

@@ -0,0 +1,47 @@
+import useProjectQuickMonitorModel from './use/porject'
+import useEntQuickMonitorModel from './use/ent'
+import useClientQuickMonitorModel from './use/client'
+
+const GlobalModelCache = {}
+
+export function removeGlobalCache(key = 'all') {
+  if (key === 'all') {
+    Object.keys(GlobalModelCache).forEach((key) => {
+      delete GlobalModelCache[key]
+    })
+    return true
+  } else {
+    delete GlobalModelCache[key]
+    return true
+  }
+}
+
+export function useQuickMonitorModel(params) {
+  const { type, cache = false, id = '' } = params
+  const useSingleModel = cache === true
+
+  let createModel = () => ({})
+  switch (type) {
+    case 'project':
+      createModel = useProjectQuickMonitorModel
+      break
+    case 'ent':
+      createModel = useEntQuickMonitorModel
+      break
+    case 'client':
+      createModel = useClientQuickMonitorModel
+      break
+    default:
+      break
+  }
+
+  if (useSingleModel) {
+    const key = `${type}-${id}`
+    if (!GlobalModelCache[key]) {
+      GlobalModelCache[key] = createModel(params)
+    }
+    return GlobalModelCache[key]
+  } else {
+    return createModel(params)
+  }
+}

+ 71 - 0
apps/bigmember_pc/src/composables/quick-monitor/use/base.js

@@ -0,0 +1,71 @@
+import { computed, reactive, ref, toRefs } from 'vue'
+import useQuickMonitor from '@jy/data-models/modules/quick-monitor/model'
+
+/**
+ * 提供监控提示弹窗 双向绑定 ref
+ * @param dialogDataMap - 文案等配置项
+ */
+export function useMonitorTipDialog(dialogDataMap) {
+  const dialogShow = ref(false)
+  const dialogShowType = ref('success-monitor')
+  const dialogParams = ref({
+    count: 0
+  })
+  const dialogConfig = computed(() => {
+    const dialogOptions = dialogDataMap[dialogShowType.value]
+    const overOptions = {
+      content: dialogOptions.content
+    }
+    if (dialogShowType.value === 'max-monitor') {
+      overOptions.content = overOptions.content.replace(
+        '$1',
+        dialogParams.value.count
+      )
+    }
+    return Object.assign(
+      {
+        type: dialogShowType.value,
+        params: dialogParams.value,
+        show: dialogShow.value
+      },
+      dialogOptions,
+      overOptions
+    )
+  })
+
+  function doOpenDialog(type, params = {}) {
+    dialogShowType.value = type
+    dialogShow.value = true
+    dialogParams.value = params
+  }
+  function doCloseDialog() {
+    dialogShow.value = false
+  }
+
+  return {
+    dialogConfig,
+    doOpenDialog,
+    doCloseDialog
+  }
+}
+
+/**
+ * 转换监控 @data-models/quick-monitor 数据模型为双向绑定 ref
+ * @param type
+ * @param id
+ */
+export function useMonitorModel({ type, id }) {
+  const useMonitor = useQuickMonitor({
+    type: type,
+    params: {
+      id: id
+    }
+  })
+  const { doFetch, doChange } = useMonitor
+  function getId() {
+    return useMonitor.id
+  }
+  const { model } = toRefs(reactive(useMonitor))
+
+  return { model, doChange, doFetch, getId }
+}

+ 264 - 0
apps/bigmember_pc/src/composables/quick-monitor/use/client.js

@@ -0,0 +1,264 @@
+import { computed, ref, getCurrentInstance } from 'vue'
+import { useStore } from '@/store'
+import { useMonitorModel, useMonitorTipDialog } from './base'
+import {
+  doOpenBuyerListPage,
+  doOpenBuyerPage,
+  doOpenPushSettingPage
+} from '@/views/article-content/composables/useArticleUtil'
+
+/**
+ * 文案项目配置
+ */
+const TextConfig = {
+  default: '监控',
+  follow: '已监控'
+}
+/**
+ * 弹窗相关配置表
+ */
+const DialogDataMap = {
+  'success-monitor': {
+    title: '监控成功',
+    content:
+      '您可前往“工作台-商机-业主监控”查看业主最新招标动态。为保证您能及时获取新增监控信息推送,请前往开启推送提醒。',
+    footerActions: [
+      {
+        label: '暂不开启',
+        class: 'cancel'
+      },
+      {
+        label: '去开启',
+        class: 'confirm',
+        action: 'doOpenPushSetting'
+      }
+    ]
+  },
+  'max-monitor': {
+    title: '监控业主个数已达上限',
+    content: '您最多可监控$1个业主,可联系客服,申请监控更多业主',
+    footerActions: [
+      {
+        label: '我再想想',
+        class: 'cancel'
+      },
+      {
+        label: '联系客服',
+        class: 'confirm',
+        action: 'doOpenCustomer'
+      }
+    ]
+  },
+  'apply-monitor': {
+    title: '申请监控更多业主',
+    content: '您可联系客服,申请升级产品套餐,监控更多业主',
+    footerActions: [
+      {
+        label: '我再想想',
+        class: 'cancel'
+      },
+      {
+        label: '联系客服',
+        class: 'confirm',
+        action: 'doOpenCustomer'
+      }
+    ]
+  },
+  'cancel-monitor': {
+    title: '确定不再监控?',
+    content: '取消监控,将错过业主最新动态推送',
+    footerActions: [
+      {
+        label: '确认取消',
+        class: 'cancel',
+        action: 'doRemoveFollow'
+      },
+      {
+        label: '我再想想',
+        class: 'confirm'
+      }
+    ]
+  },
+  'success-toast': '监控成功,您可前往“工作台-商机-业主监控”查看'
+}
+
+function useClientQuickMonitorModel({ type, id }) {
+  const that = getCurrentInstance().proxy
+  const collectElement = ref(null)
+  const { dialogConfig, doOpenDialog, doCloseDialog } =
+    useMonitorTipDialog(DialogDataMap)
+  const HasPowerBigmemberOrEnt = computed(() => {
+    const store = useStore()
+    return store.getters['user/entniche'] || store.getters['user/bigmember']
+  })
+  const IsFreeUser = computed(() => {
+    const store = useStore()
+    return store.getters['user/isFree']
+  })
+  const loading = ref(false)
+  const { model, doChange, doFetch, getId } = useMonitorModel({ type, id })
+  const monitorPopoverConfig = computed(() => {
+    // 历史业务逻辑,免费用户不显示查看监控列表、详情
+    if (!model.value.follow && IsFreeUser.value) {
+      return {
+        showTip: !model.value.follow,
+        showMore: false,
+        showList: false,
+        showCancel: model.value.follow,
+        alreadyNum: model.value.expands.used,
+        remainNum: model.value.expands.surplus,
+        textType: type
+      }
+    }
+    return {
+      showTip: !model.value.follow,
+      showMore: model.value.follow,
+      showList: true,
+      showCancel: model.value.follow,
+      alreadyNum: model.value.expands.used,
+      remainNum: model.value.expands.surplus,
+      textType: type
+    }
+  })
+
+  // 打开留资弹窗
+  function doOpenCollectDialog(key) {
+    collectElement.value?.noCallApiFn(key, false)
+  }
+
+  // 联系客服
+  function doOpenCustomer() {
+    that?.contactCustomer(that)
+  }
+
+  /**
+   * 监控操作业务流程
+   * 0. 前置权益判断
+   *  0.1 判断是否非大会员、非商机管理用户
+   *    > 否,留资弹窗
+   * 1. 监控成功
+   *  1.1 判断是否开启推送提醒
+   *    > 是,提醒开启推送提醒 success-monitor
+   *    > 否,toast 提醒监控成功 success-toast
+   * 2. 监控失败
+   *  2.1 超出可监控项目个数
+   *    2.1.1 判断是否非大会员、非商机管理用户
+   *      > 是,弹窗提醒 max-monitor
+   *      > 否,留资弹窗
+   *  2.2 其他错误,toast 提醒
+   * @return {Promise<void>}
+   */
+  async function doAddFollow() {
+    if (loading.value) return
+    loading.value = true
+    // 业务流程
+    if (!model.value.follow) {
+      // 无权限用户弹窗留资
+      if (IsFreeUser.value) {
+        loading.value = false
+        return doOpenCollectDialog('pc_buyer_monitor_freeuser')
+      }
+
+      await doChange()
+        .then((res) => {
+          if (res.success) {
+            // 判断是否开启推送提醒
+            if (!res.data?.msg_open) {
+              doOpenDialog('success-monitor')
+            } else {
+              that.$toast(DialogDataMap['success-toast'])
+            }
+          } else {
+            // 判断是否超出可监控项目个数
+            if (res.data?.limit_count) {
+              if (IsFreeUser.value) {
+                return doOpenCollectDialog('pc_buyer_monitor_limit')
+              } else {
+                return doOpenDialog('max-monitor', {
+                  count: res.data?.limit_count
+                })
+              }
+            }
+            that.$toast(res.msg)
+          }
+        })
+        .finally(() => {
+          loading.value = false
+        })
+    }
+    loading.value = false
+  }
+
+  /**
+   * 统一处理 dialog, popover emit 事件
+   * @param {string} type
+   *  // popover 事务
+   *  - more 查看监控动态
+   *  - list 查看监控列表
+   *  - apply 申请监控更多
+   *  - cancel 取消监控确认弹窗
+   *  // dialog 事务
+   *  - doCloseDialog 关闭弹窗
+   *  - doRemoveFollow 提交取消监控事务
+   *  - doOpenCustomer 联系客服
+   *  - doOpenPushSetting 打开推送提醒设置
+   */
+  function doClickMonitorActions(type) {
+    switch (type) {
+      case 'doCloseDialog':
+        doCloseDialog()
+        break
+      case 'doOpenCustomer':
+        doCloseDialog()
+        doOpenCustomer()
+        break
+      case 'doOpenPushSetting':
+        doCloseDialog()
+        doOpenPushSettingPage()
+        break
+      case 'doRemoveFollow':
+        doCloseDialog()
+        doChange().then((res) => {
+          if (!res.success) {
+            that.$toast(res.msg)
+          }
+        })
+        break
+      case 'cancel':
+        doOpenDialog('cancel-monitor')
+        break
+      case 'apply':
+        if (IsFreeUser.value) {
+          doOpenCollectDialog('pc_buyer_monitor_more')
+        } else {
+          doOpenDialog('apply-monitor')
+        }
+        break
+      case 'more':
+        doOpenBuyerPage({ name: getId(), query: { active: 2 } })
+        break
+      case 'list':
+        doOpenBuyerListPage()
+        break
+      default:
+        break
+    }
+  }
+
+  return {
+    // 基础展示信息
+    model,
+    doFetch,
+    doAddFollow,
+    TextConfig,
+    // popover 悬浮信息
+    monitorPopoverConfig,
+    doClickMonitorActions,
+    // dialog 提示弹窗
+    dialogConfig,
+    // 留资
+    collectElement
+  }
+}
+
+export default useClientQuickMonitorModel

+ 247 - 0
apps/bigmember_pc/src/composables/quick-monitor/use/ent.js

@@ -0,0 +1,247 @@
+import { computed, ref, getCurrentInstance } from 'vue'
+import { useStore } from '@/store'
+import { useMonitorModel, useMonitorTipDialog } from './base'
+import {
+  doOpenPushSettingPage,
+  doOpenWinnerListPage,
+  doOpenWinnerPage
+} from '@/views/article-content/composables/useArticleUtil'
+
+/**
+ * 文案项目配置
+ */
+const TextConfig = {
+  default: '监控',
+  follow: '已监控'
+}
+/**
+ * 弹窗相关配置表
+ */
+const DialogDataMap = {
+  'success-monitor': {
+    title: '监控成功',
+    content:
+      '您可前往“工作台-商机-企业情报监控”查看企业最新动态。为保证您能及时获取新增监控信息推送,请前往开启推送提醒。',
+    footerActions: [
+      {
+        label: '暂不开启',
+        class: 'cancel'
+      },
+      {
+        label: '去开启',
+        class: 'confirm',
+        action: 'doOpenPushSetting'
+      }
+    ]
+  },
+  'max-monitor': {
+    title: '监控企业个数已达上限',
+    content: '您最多可监控$1个企业,可联系客服,申请监控更多企业',
+    footerActions: [
+      {
+        label: '我再想想',
+        class: 'cancel'
+      },
+      {
+        label: '联系客服',
+        class: 'confirm',
+        action: 'doOpenCustomer'
+      }
+    ]
+  },
+  'apply-monitor': {
+    title: '申请监控更多企业',
+    content: '您可联系客服,申请升级产品套餐,监控更多企业',
+    footerActions: [
+      {
+        label: '我再想想',
+        class: 'cancel'
+      },
+      {
+        label: '联系客服',
+        class: 'confirm',
+        action: 'doOpenCustomer'
+      }
+    ]
+  },
+  'cancel-monitor': {
+    title: '确定不再监控?',
+    content: '取消监控,将错过企业最新动态推送',
+    footerActions: [
+      {
+        label: '确认取消',
+        class: 'cancel',
+        action: 'doRemoveFollow'
+      },
+      {
+        label: '我再想想',
+        class: 'confirm'
+      }
+    ]
+  },
+  'success-toast': '监控成功,您可前往“工作台-商机-企业情报监控”查看'
+}
+
+function useEntQuickMonitorModel({ type, id }) {
+  const that = getCurrentInstance().proxy
+  const collectElement = ref(null)
+  const { dialogConfig, doOpenDialog, doCloseDialog } =
+    useMonitorTipDialog(DialogDataMap)
+  const IsFreeUser = computed(() => {
+    const store = useStore()
+    return store.getters['user/isFree']
+  })
+  const loading = ref(false)
+  const { model, doChange, doFetch, getId } = useMonitorModel({ type, id })
+  const monitorPopoverConfig = computed(() => {
+    return {
+      showTip: !model.value.follow,
+      showMore: model.value.follow,
+      showList: true,
+      showCancel: model.value.follow,
+      alreadyNum: model.value.expands.used,
+      remainNum: model.value.expands.surplus,
+      textType: type
+    }
+  })
+
+  // 打开留资弹窗
+  function doOpenCollectDialog(key) {
+    collectElement.value?.noCallApiFn(key || 'pc_article_ent_more', false)
+  }
+
+  // 联系客服
+  function doOpenCustomer() {
+    that?.contactCustomer(that)
+  }
+
+  /**
+   * 监控操作业务流程
+   * 0. 前置权益判断
+   *  0.1 判断是否非大会员、非商机管理用户
+   *    > 否,留资弹窗
+   * 1. 监控成功
+   *  1.1 判断是否开启推送提醒
+   *    > 是,提醒开启推送提醒 success-monitor
+   *    > 否,toast 提醒监控成功 success-toast
+   * 2. 监控失败
+   *  2.1 超出可监控项目个数
+   *    2.1.1 判断是否非大会员、非商机管理用户
+   *      > 是,弹窗提醒 max-monitor
+   *      > 否,留资弹窗
+   *  2.2 其他错误,toast 提醒
+   * @return {Promise<void>}
+   */
+  async function doAddFollow() {
+    if (loading.value) return
+    loading.value = true
+    // 业务流程
+    if (!model.value.follow) {
+      await doChange()
+        .then((res) => {
+          if (res.success) {
+            // 判断是否开启推送提醒
+            if (!res.data?.msg_open) {
+              doOpenDialog('success-monitor')
+            } else {
+              that.$toast(DialogDataMap['success-toast'])
+            }
+          } else {
+            // 判断是否超出可监控项目个数
+            if (res.data?.limit_count) {
+              if (IsFreeUser.value) {
+                return doOpenCollectDialog('pc_article_ent_limit')
+              } else {
+                return doOpenDialog('max-monitor', {
+                  count: res.data?.limit_count
+                })
+              }
+            }
+            that.$toast(res.msg)
+          }
+        })
+        .finally(() => {
+          loading.value = false
+        })
+    }
+    loading.value = false
+  }
+
+  /**
+   * 统一处理 dialog, popover emit 事件
+   * @param {string} type
+   *  // popover 事务
+   *  - more 查看监控动态
+   *  - list 查看监控列表
+   *  - apply 申请监控更多
+   *  - cancel 取消监控确认弹窗
+   *  // dialog 事务
+   *  - doCloseDialog 关闭弹窗
+   *  - doRemoveFollow 提交取消监控事务
+   *  - doOpenCustomer 联系客服
+   *  - doOpenPushSetting 打开推送提醒设置
+   */
+  function doClickMonitorActions(type) {
+    switch (type) {
+      case 'doCloseDialog':
+        doCloseDialog()
+        break
+      case 'doOpenCustomer':
+        doCloseDialog()
+        doOpenCustomer()
+        break
+      case 'doOpenPushSetting':
+        doCloseDialog()
+        doOpenPushSettingPage()
+        break
+      case 'doRemoveFollow':
+        doCloseDialog()
+        doChange().then((res) => {
+          if (!res.success) {
+            that.$toast(res.msg)
+          }
+        })
+        break
+      case 'cancel':
+        doOpenDialog('cancel-monitor')
+        break
+      case 'apply':
+        if (IsFreeUser.value) {
+          doOpenCollectDialog('pc_article_ent_more')
+        } else {
+          doOpenDialog('apply-monitor')
+        }
+        break
+      case 'more':
+        doOpenWinnerPage({
+          id: getId(),
+          query: {
+            active: 4
+          }
+        })
+        break
+      case 'list':
+        doOpenWinnerListPage()
+        break
+      default:
+        break
+    }
+  }
+
+  return {
+    // 基础展示信息
+    model,
+    doFetch,
+    doAddFollow,
+    TextConfig,
+    // popover 悬浮信息
+    monitorPopoverConfig,
+    doClickMonitorActions,
+    // dialog 提示弹窗
+    dialogConfig,
+    // 留资
+    collectElement
+  }
+}
+
+export default useEntQuickMonitorModel

+ 241 - 0
apps/bigmember_pc/src/composables/quick-monitor/use/porject.js

@@ -0,0 +1,241 @@
+import { computed, ref, getCurrentInstance } from 'vue'
+import { useStore } from '@/store'
+import { useMonitorModel, useMonitorTipDialog } from './base'
+import {
+  doOpenProjectDetailPage,
+  doOpenProjectProgressListPage,
+  doOpenPushSettingPage
+} from '@/views/article-content/composables/useArticleUtil'
+
+/**
+ * 文案项目配置
+ */
+const TextConfig = {
+  default: '监控',
+  follow: '已监控'
+}
+/**
+ * 弹窗相关配置表
+ */
+const DialogDataMap = {
+  'success-monitor': {
+    title: '监控成功',
+    content:
+      '您可前往“工作台-商机-项目进度监控”查看项目最新招标/采购进度。为保证您能及时获取新增监控信息推送,请前往开启推送提醒。',
+    footerActions: [
+      {
+        label: '暂不开启',
+        class: 'cancel'
+      },
+      {
+        label: '去开启',
+        class: 'confirm',
+        action: 'doOpenPushSetting'
+      }
+    ]
+  },
+  'max-monitor': {
+    title: '监控项目个数已达上限',
+    content: '您最多可监控$1个项目,可联系客服,申请监控更多项目',
+    footerActions: [
+      {
+        label: '我再想想',
+        class: 'cancel'
+      },
+      {
+        label: '联系客服',
+        class: 'confirm',
+        action: 'doOpenCustomer'
+      }
+    ]
+  },
+  'apply-monitor': {
+    title: '申请监控更多项目',
+    content: '您可联系客服,申请升级产品套餐,监控更多项目',
+    footerActions: [
+      {
+        label: '我再想想',
+        class: 'cancel'
+      },
+      {
+        label: '联系客服',
+        class: 'confirm',
+        action: 'doOpenCustomer'
+      }
+    ]
+  },
+  'cancel-monitor': {
+    title: '确定不再监控?',
+    content: '取消监控,将错过项目最新动态推送',
+    footerActions: [
+      {
+        label: '确认取消',
+        class: 'cancel',
+        action: 'doRemoveFollow'
+      },
+      {
+        label: '我再想想',
+        class: 'confirm'
+      }
+    ]
+  },
+  'success-toast': '监控成功,您可前往“工作台-商机-项目进度监控”查看'
+}
+
+function useProjectQuickMonitorModel({ type, id }) {
+  const that = getCurrentInstance().proxy
+  const collectElement = ref(null)
+  const { dialogConfig, doOpenDialog, doCloseDialog } =
+    useMonitorTipDialog(DialogDataMap)
+  const HasPowerBigmemberOrEnt = computed(() => {
+    const store = useStore()
+    return store.getters['user/entniche'] || store.getters['user/bigmember']
+  })
+  const loading = ref(false)
+  const { model, doChange, doFetch, getId } = useMonitorModel({ type, id })
+  const monitorPopoverConfig = computed(() => {
+    return {
+      showTip: !model.value.follow,
+      showMore: model.value.follow,
+      showList: true,
+      showCancel: model.value.follow,
+      alreadyNum: model.value.expands.used,
+      remainNum: model.value.expands.surplus,
+      textType: type
+    }
+  })
+
+  // 打开留资弹窗
+  function doOpenCollectDialog(key) {
+    collectElement.value?.noCallApiFn(key || 'pc_article_project_more', false)
+  }
+
+  // 联系客服
+  function doOpenCustomer() {
+    that?.contactCustomer(that)
+  }
+
+  /**
+   * 监控操作业务流程
+   * 0. 前置权益判断
+   *  0.1 判断是否非大会员、非商机管理用户
+   *    > 否,留资弹窗
+   * 1. 监控成功
+   *  1.1 判断是否开启推送提醒
+   *    > 是,提醒开启推送提醒 success-monitor
+   *    > 否,toast 提醒监控成功 success-toast
+   * 2. 监控失败
+   *  2.1 超出可监控项目个数
+   *    2.1.1 判断是否非大会员、非商机管理用户
+   *      > 是,弹窗提醒 max-monitor
+   *      > 否,留资弹窗
+   *  2.2 其他错误,toast 提醒
+   * @return {Promise<void>}
+   */
+  async function doAddFollow() {
+    if (loading.value) return
+    loading.value = true
+    // 业务流程
+    if (!model.value.follow) {
+      await doChange()
+        .then((res) => {
+          if (res.success) {
+            // 判断是否开启推送提醒
+            if (res.data?.msg_open) {
+              that.$toast(DialogDataMap['success-toast'])
+            } else {
+              doOpenDialog('success-monitor')
+            }
+          } else {
+            // 判断是否超出可监控项目个数
+            if (res.data?.limit_count) {
+              if (HasPowerBigmemberOrEnt.value) {
+                return doOpenDialog('max-monitor', {
+                  count: res.data?.limit_count
+                })
+              } else {
+                return doOpenCollectDialog('pc_article_project_limit')
+              }
+            }
+            that.$toast(res.msg)
+          }
+        })
+        .finally(() => {
+          loading.value = false
+        })
+    }
+    loading.value = false
+  }
+
+  /**
+   * 统一处理 dialog, popover emit 事件
+   * @param {string} type
+   *  // popover 事务
+   *  - more 查看监控动态
+   *  - list 查看监控列表
+   *  - apply 申请监控更多
+   *  - cancel 取消监控确认弹窗
+   *  // dialog 事务
+   *  - doCloseDialog 关闭弹窗
+   *  - doRemoveFollow 提交取消监控事务
+   *  - doOpenCustomer 联系客服
+   *  - doOpenPushSetting 打开推送提醒设置
+   */
+  function doClickMonitorActions(type) {
+    switch (type) {
+      case 'doCloseDialog':
+        doCloseDialog()
+        break
+      case 'doOpenCustomer':
+        doCloseDialog()
+        doOpenCustomer()
+        break
+      case 'doOpenPushSetting':
+        doCloseDialog()
+        doOpenPushSettingPage()
+        break
+      case 'doRemoveFollow':
+        doCloseDialog()
+        doChange().then((res) => {
+          if (!res.success) {
+            that.$toast(res.msg)
+          }
+        })
+        break
+      case 'cancel':
+        doOpenDialog('cancel-monitor')
+        break
+      case 'apply':
+        if (HasPowerBigmemberOrEnt.value) {
+          doOpenDialog('apply-monitor')
+        } else {
+          doOpenCollectDialog('pc_article_project_more')
+        }
+        break
+      case 'more':
+        doOpenProjectDetailPage({ id: getId(), mark: 1 })
+        break
+      case 'list':
+        doOpenProjectProgressListPage()
+      default:
+        break
+    }
+  }
+
+  return {
+    // 基础展示信息
+    model,
+    doFetch,
+    doAddFollow,
+    TextConfig,
+    // popover 悬浮信息
+    monitorPopoverConfig,
+    doClickMonitorActions,
+    // dialog 提示弹窗
+    dialogConfig,
+    // 留资
+    collectElement
+  }
+}
+
+export default useProjectQuickMonitorModel

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

@@ -1,4 +1,5 @@
 // import '@jianyu/easy-inject-qiankun/src/pre-mount.js'
+import 'virtual:uno.css'
 import Vue from 'vue'
 import App from './App.vue'
 import store from './store/'

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

@@ -11,7 +11,8 @@ const powerCheckPathWhiteRegList = [
   /set-/,
   new RegExp('(^/dw)|(^/qy)'), // seo画像路由
   /filepack/,
-  /order/
+  /order/,
+  /article/
 ]
 // 权限控制白名单-路由名
 const powerCheckWhiteList = [
@@ -40,6 +41,7 @@ const powerCheckWhiteList = [
   'unit_portrayal_id',
   'set-password',
   'portrayal_loading',
+  'article_detail',
   'recommen-list',
   'business_detail'
 ]

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

@@ -246,6 +246,13 @@ export default [
     name: 'set-identity-info',
     component: () => import('@/views/accountInfo/SetIdentityInfo.vue')
   },
+  // 标讯详情页
+  {
+    path: '/article/:content/:id',
+    alias: ['/article/:content/:id', '/nologin/:content/:id'],
+    name: 'article_detail',
+    component: () => import('@/views/article-content/pages/Article.vue')
+  },
   // 推荐标讯列表
   {
     path: '/recommen-list',

+ 4 - 1
apps/bigmember_pc/src/store/index.js

@@ -12,7 +12,7 @@ if (process.env.NODE_ENV !== 'production') {
   Vue.use(Vuex)
 }
 
-export default new Vuex.Store({
+const store = new Vuex.Store({
   state: {
     env: process.env.NODE_ENV
   },
@@ -28,3 +28,6 @@ export default new Vuex.Store({
     department
   }
 })
+export default store
+
+export const useStore = () => store

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

@@ -467,6 +467,18 @@ export default {
     // 个人版
     isPersonalVersion: (state) => state.userIdentity.positionType === 0,
     // 企业版
-    isEntVersion: (state) => state.userIdentity.positionType === 1
+    isEntVersion: (state) => state.userIdentity.positionType === 1,
+    // 兼容用法
+    isFree: (_, getters) => getters.free,
+    isSuper: (_, getters) => getters.svip,
+    isMember: (_, getters) => getters.bigmember,
+    isBusiness: (_, getters) => {
+      const { entniche, bigmember } = getters
+      const vip = entniche || bigmember
+      return !vip
+    },
+    bigMemberPower: (_, getters) => {
+      return getters.power
+    }
   }
 }

+ 33 - 3
apps/bigmember_pc/src/utils/globalDirectives.js

@@ -12,11 +12,11 @@ Vue.directive('clickoutside', clickoutside)
 // 一般用在解决el-dialog双层嵌套问题
 Vue.directive('component-change-mount', {
   inserted: function (el, { value }) {
-    if (!value) return console.error('未绑定参数')
+    if (!value) return console.warn('未绑定参数')
     const context = el.__vue__
-    if (!context) return console.error('该指令需要绑定到vue组件上')
+    if (!context) return console.warn('该指令需要绑定到vue组件上')
     const { selector } = value
-    if (!selector) return console.error('需要绑定selector目标选择器')
+    if (!selector) return console.warn('需要绑定selector目标选择器')
     const target = context.$root.$el.querySelector(selector)
     target.appendChild(el)
   }
@@ -92,3 +92,33 @@ Vue.directive('stickyed', {
       )
   }
 })
+
+// 事件委托
+Vue.directive('event-listener', {
+  inserted(el, binding) {
+    // 获取指令的参数
+    const { value: eventFn, arg: eventName } = binding
+    const selector = el.dataset?.eventSelector || '*'
+
+    // 创建事件处理函数
+    const eventHandler = (event) => {
+      // 检查点击的目标是否匹配选择器
+      if (event.target.matches(selector)) {
+        eventFn(event) // 调用传入的方法
+      }
+    }
+
+    // 添加事件监听
+    el.addEventListener(eventName, eventHandler)
+
+    // 在组件销毁时,移除事件监听
+    const destroyListener = () => {
+      el.removeEventListener(eventName, eventHandler)
+    }
+
+    // 注册销毁钩子,确保在组件销毁时移除事件监听
+    if (binding.instance) {
+      binding.instance.$once('hook:beforeDestroy', destroyListener)
+    }
+  }
+})

+ 3 - 0
apps/bigmember_pc/src/views/BidrenewalDialog/index.vue

@@ -128,6 +128,9 @@ export default {
         }
       }
     },
+    openDialog() {
+      this.passVisible = true
+    },
     initFunction() {
       // 注册方法供外部调用
       const this_ = this

+ 93 - 11
apps/bigmember_pc/src/views/PotentialList.vue

@@ -55,7 +55,7 @@
             class="ex-line-2"
             @onChange="changeBusiness"
             ref="businessScopeSelector"
-            :initList="getScopeKeyList"
+            :initList="getScopeKeyListFull"
             selectorType="line"
           >
             <div slot="header">业务范围:</div>
@@ -185,10 +185,22 @@ export default {
         showD3: false,
         alreadyNum: 0,
         remainNum: 0
+      },
+      cacheFilter: {
+        business_scope: []
       }
     }
   },
   methods: {
+    formatScopeKey(list) {
+      return list.map((v) => {
+        let str = v.key.join(' ')
+        if (v?.appendkey) {
+          str = str + ' ' + v?.appendkey.join(' ')
+        }
+        return str
+      })
+    },
     showMonitorDialog(data) {
       const { remainNum, alreadyNum } = data
       this.monitorInfo.remainNum = remainNum
@@ -258,7 +270,7 @@ export default {
       } else {
         // 潜在竞争对手 / 合作伙伴挖掘 -- 关注
         setFollowEnt({ entId: item.entId, group: item.group }).then((res) => {
-          if (!(res && res.error_code === 0 && res.data === 'success')) {
+          if (!(res && res.error_code === 0 && res.data?.status)) {
             this.$toast(res.error_msg)
           } else {
             this.$toast('关注成功,已添加至企业情报监控内')
@@ -363,7 +375,6 @@ export default {
     },
     changeBusiness(item) {
       item = item.map((v) => v.split(' ')[0])
-      console.log(item, 'xx')
       let tempArr = []
       this.scope.forEach((v) => {
         if (item.includes(v.key.join(' '))) {
@@ -390,6 +401,59 @@ export default {
         }
       })
       this.filters.industry = tempArr
+    },
+    doResetArticleCacheFilter() {
+      try {
+        const cacheFilter = JSON.parse(
+          sessionStorage.getItem('potential_cor_list_search') || '{}'
+        )
+        console.log(cacheFilter)
+
+        if (cacheFilter?.area) {
+          this.filters.area = { [cacheFilter.area]: [] } || {}
+        }
+        if (cacheFilter?.industry) {
+          if (cacheFilter?.industry?.indexOf('_') > -1) {
+            try {
+              const splitResult = cacheFilter.industry.split('_')
+              const result = {
+                [splitResult[0]]: [splitResult[1]]
+              }
+              this.filters.industry = result
+            } catch (e) {
+              console.warn(e)
+            }
+          } else {
+            this.filters.industry = [cacheFilter?.industry] || []
+          }
+        }
+        if (cacheFilter?.buyerClass) {
+          this.filters.buyerclass = [cacheFilter?.buyerClass] || []
+        }
+        if (cacheFilter?.keywords) {
+          const tempKey = [
+            {
+              key: [cacheFilter?.keywords],
+              notkey: [],
+              appendkey: null,
+              matchway: 1,
+              updatetime: Date.now()
+            }
+          ]
+          this.filters.business_scope = [].concat(tempKey)
+          this.cacheFilter.business_scope = [].concat(tempKey)
+        }
+      } catch (e) {
+        console.warn(e)
+      }
+    },
+    doResetSelectorForCahce() {
+      this.$refs.areaSelector.setCitySelected(this.filters.area)
+      this.$refs.industrySelector.setIndustryState(this.filters.industry)
+      this.$refs.buyerclassSelector.setCateState(this.filters.buyerclass)
+      this.$refs.businessScopeSelector.setState(
+        this.formatScopeKey(this.filters.business_scope)
+      )
     }
   },
   computed: {
@@ -402,27 +466,45 @@ export default {
       return this.topInfo[this.$route.params.type || 'c']
     },
     getScopeKeyList() {
-      return this.scope.map((v) => {
-        let str = v.key.join(' ')
-        if (v?.appendkey) {
-          str = str + ' ' + v?.appendkey.join(' ')
-        }
-        return str
-      })
+      return this.formatScopeKey(this.scope)
+    },
+    getCacheScopeKeyList() {
+      return this.formatScopeKey(this.cacheFilter.business_scope)
+    },
+    getScopeKeyListFull() {
+      if (this.isUseCache) {
+        return [].concat(this.getCacheScopeKeyList, this.getScopeKeyList)
+      } else {
+        return this.getScopeKeyList
+      }
+    },
+    isUseCache() {
+      return this.$route.query.mark === '1'
     }
   },
   mounted() {
     this.changeBusiness([])
     this.recoverCreate()
+    if (this.isUseCache) {
+      this.doResetSelectorForCahce()
+    }
   },
   activated() {
     this.changeBusiness([])
+    if (this.isUseCache) {
+      this.doResetSelectorForCahce()
+    }
   },
   async created() {
+    // 从缓存中获取详情页筛选项
+    if (this.isUseCache) {
+      this.doResetArticleCacheFilter()
+    } else {
+      this.filters.business_scope = this.scope
+    }
     if (!this.isDeleteAllScope) {
       await this.$store.dispatch('user/getKeywordsList')
     }
-    this.filters.business_scope = this.scope
   }
 }
 </script>

+ 543 - 0
apps/bigmember_pc/src/views/article-content/components/ContentBIActions.vue

@@ -0,0 +1,543 @@
+<template>
+  <div class="content-bi-actions">
+    <div class="com-statusbar-BI" id="statusbar-BI" v-cloak>
+      <div class="crm-action">
+        <div class="action-content" v-for="(item, i) in getList" :key="i">
+          <div
+            @click="setActionEvent(item)"
+            class="action-list"
+            :class="'action-' + item.class"
+          >
+            <i
+              class="icon iconfont"
+              :class="[
+                'icon-' + item['icon-' + item.active],
+                { checked: !!item.active }
+              ]"
+            >
+              <div class="msg" v-if="item.msg">{{ item.msg }}</div>
+            </i>
+            <span> {{ item.active ? '已' : '' }}{{ item.title }}</span>
+          </div>
+        </div>
+      </div>
+      <el-dialog
+        custom-class="property-employ-dialog"
+        :visible.sync="showPropertyDialog"
+      >
+        <iframe
+          width="600"
+          height="650"
+          :src="IframeSrc"
+          frameborder="0"
+        ></iframe>
+      </el-dialog>
+      <div class="Iframe-dialog">
+        <el-dialog :visible.sync="dialogVisible" width="70%" height="80%">
+          <iframe
+            :src="IframeSrc"
+            width="100%"
+            height="80%"
+            frameborder="0"
+          ></iframe>
+        </el-dialog>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'ContentBIActions',
+  data() {
+    return {
+      list: [
+        {
+          title: '收录',
+          'icon-0': 'a-Property1shoulu',
+          'icon-1': 'a-Property1yishoulu',
+          class: 'employ',
+          active: 0
+        },
+        {
+          title: '忽略',
+          'icon-0': 'a-Property1hulve',
+          'icon-1': 'a-Property1yihulve',
+          class: 'ignore',
+          active: 0
+        },
+        {
+          title: '创建销售线索',
+          'icon-0': 'chuangjianxiaoshouxiansuo',
+          'icon-1': 'chuangjianxiaoshouxiansuo',
+          class: 'xiansuo',
+          active: 0,
+          msg: 0
+        },
+        {
+          title: '创建销售机会',
+          'icon-0': 'chuangjianxiaoshoujihui',
+          'icon-1': 'chuangjianxiaoshoujihui',
+          class: 'jihui',
+          active: 0,
+          msg: 0
+        },
+        {
+          title: '创建客户',
+          'icon-0': 'chuangjiankehu',
+          'icon-1': 'chuangjiankehu',
+          class: 'custom',
+          active: 0,
+          msg: 0
+        }
+      ],
+      pageType: '', // 营销专版参数
+      porperty: '', // 物业专版参数
+      fromJhfp: '', // 判断是从机会复盘还是从招标搜索进
+      employInfo: [], // 收录情况
+      dialogVisible: false,
+      IframeSrc: '',
+      getEntData: {},
+      showPropertyDialog: false
+    }
+  },
+  created() {
+    this.getEntInfo()
+    this.getParams()
+  },
+  props: {
+    id: {
+      type: String,
+      required: true,
+      default: ''
+    }
+  },
+  computed: {
+    getList() {
+      if (this.list[0].active === 0) {
+        return this.list.slice(0, 1)
+      } else if (this.property === 'BIProperty') {
+        this.list = this.list.filter((v) => {
+          return v.title !== '创建销售线索'
+        })
+        return this.list
+      } else {
+        return this.list
+      }
+    }
+  },
+  watch: {
+    dialogVisible(newval) {
+      if (!newval) {
+        this.getEmployData()
+      }
+    }
+  },
+  mounted() {
+    const _this = this
+    window.addEventListener('storage', function (event) {
+      console.log(event.storageArea)
+      if (event.storageArea === sessionStorage) {
+        if (event.key === 'Op-upState') {
+          sessionStorage.removeItem('Op-upState', '0')
+          _this.dialogVisible = false
+          _this.showPropertyDialog = false
+          _this.getEmployData()
+        }
+      }
+    })
+  },
+  methods: {
+    getacount(bidamount, budget) {
+      if (
+        typeof bidamount != 'undefined' &&
+        bidamount != null &&
+        bidamount != ''
+      ) {
+        return bidamount
+      }
+      if (typeof budget != 'undefined' && budget != null && budget != '') {
+        return budget
+      }
+      return ''
+    },
+    //金额转化   金额:0-万元以下单位为元  ,万元以上至亿元以下单位为万元 ,亿元以上单位为亿元。保留 小数点后 2 位,不进行四舍五入。
+    conversionMoeny(money) {
+      var m = '' + money
+      var m_arr = m.split('.')
+      var m_1 = m_arr[0]
+      var len_m1 = m_1.length
+      if (len_m1 >= 9) {
+        m =
+          m_1.substring(0, len_m1 - 8) +
+          '.' +
+          m_1.substring(len_m1 - 8, len_m1 - 6) +
+          '亿元'
+      } else if (len_m1 >= 5) {
+        m =
+          m_1.substring(0, len_m1 - 4) +
+          '.' +
+          m_1.substring(len_m1 - 4, len_m1 - 2) +
+          '万元'
+      } else {
+        if (m_arr.length == 1) {
+          return m + '.00元'
+        }
+        var m_2 = m_arr[1]
+        if (m_2.length > 1) {
+          m_2 = m_2.substring(0, 2)
+        } else {
+          m_2 = m_2.substring(0, 1) + '0'
+        }
+        m = m_1 + '.' + m_2 + '元'
+      }
+      return m
+    },
+    getParams() {
+      const urlParams = new URLSearchParams(window.location.search)
+      this.pageType = urlParams.get('resource')
+      this.property = urlParams.get('property')
+      this.fromJhfp = urlParams.get('from')
+      if (this.pageType === 'BI' || this.property === 'BIProperty') {
+        $('.com-tagsbar').hide()
+        $('.com-statusbar').hide()
+        this.getEmployData()
+      } else {
+        $('#statusbar-BI').hide()
+      }
+    },
+    setActionEvent(data) {
+      var employInfoItem = this.employInfo[0]
+      var employId = ''
+      if (employInfoItem) {
+        employId = employInfoItem.employId
+      }
+      const isHavaRoot =
+        this.getEntData.niche_dis === 1 || this.getEntData.niche_dis === 2
+      switch (data.class) {
+        case 'employ':
+          // 收录
+          if (this.property === 'BIProperty') {
+            this.IframeSrc = `${
+              location.origin
+            }/succbi/crm_system/app/crm.app/%E9%80%9A%E7%94%A8%E5%88%9B%E5%BB%BA/create_intelligence.spg?t=${new Date().getTime()}`
+            const { bidamount, budget, title, area, buyer } =
+              window.goTemplateData.params.obj
+            const propertyData = {
+              _id: this.id,
+              title: title,
+              buyer: buyer,
+              area: area,
+              bidamount: bidamount,
+              budget: budget
+            }
+            // 将propertyData存入本地,用于BI创建情报回显数据
+            localStorage.setItem('property-data', JSON.stringify(propertyData))
+            this.setOpEvent(data)
+          } else {
+            this.setEmployEvent(data)
+          }
+          break
+        case 'ignore':
+          // 忽略
+          this.setIgnoreEvent(data)
+          break
+        case 'custom':
+          // 创建客户
+          this.isCanAdd('more_create_custom').then((res) => {
+            if (res.data.status === 1) {
+              if (!isHavaRoot) {
+                sessionStorage.setItem('Op-upState', '0')
+                this.IframeSrc = `${
+                  location.origin
+                }/succbi/crm_system/app/crm.app/%E9%80%9A%E7%94%A8%E5%88%9B%E5%BB%BA/create_customer.spg?E_employ_info_id=${
+                  this.employInfo[0].employId
+                }&E_create_type=1&t=${new Date().getTime()}`
+                this.dialogVisible = true
+              }
+            } else {
+              this.$toast(res.error_msg, 1000)
+            }
+          })
+          break
+        case 'xiansuo':
+          // 创建销售线索
+          this.isCanAdd('more_create_clue').then((res) => {
+            if (res.data.status === 1) {
+              if (!isHavaRoot) {
+                sessionStorage.setItem('Op-upState', '0')
+                this.IframeSrc = `${
+                  location.origin
+                }/succbi/crm_system/app/crm.app/%E9%80%9A%E7%94%A8%E5%88%9B%E5%BB%BA/create_clues.spg?E_employ_info_id=${
+                  this.employInfo[0].employId
+                }&t=${new Date().getTime()}`
+                this.dialogVisible = true
+              }
+            } else {
+              this.$toast(res.error_msg, 1000)
+            }
+          })
+          break
+        case 'jihui':
+          // 创建销售机会
+          this.isCanAdd('more_create_chance').then((res) => {
+            if (res.data.status === 1) {
+              if (!isHavaRoot) {
+                sessionStorage.setItem('Op-upState', '0')
+                this.IframeSrc = `${
+                  location.origin
+                }/succbi/crm_system/app/crm.app/%E9%80%9A%E7%94%A8%E5%88%9B%E5%BB%BA/create_%20%20opportunity.spg?E_employ_info_id=${
+                  this.employInfo[0].employId
+                }&M_source_id=${
+                  this.employInfo[0].id
+                }&t=${new Date().getTime()}`
+                this.dialogVisible = true
+              }
+            } else {
+              this.$toast(res.error_msg, 1000)
+            }
+          })
+          break
+        default:
+          break
+      }
+    },
+    // 判断是否能创建
+    isCanAdd(type) {
+      const url = '/jyapi/crmApplication/info/canAdd'
+      const params = {
+        employInfoId: Number(this.employInfo[0].employId),
+        employCustomId: 0,
+        key: type
+      }
+      return new Promise((resolve, reject) => {
+        this.ajaxComponent(url, params)
+          .then((res) => {
+            resolve(res)
+          })
+          .catch((err) => {
+            reject(err)
+          })
+      })
+    },
+    // 查询企业信息
+    getEntInfo() {
+      const url = '/entbase/ent/entinfo'
+      this.ajaxComponent(url).then((res) => {
+        if (res.error_code === 0) {
+          this.getEntData = res.data
+          // niche_dis: 0:销售 1:企业资讯分配 2:部门资讯分配 3:企业资讯分配+销售 4:部门资讯分配+销售
+          if (res.data.niche_dis === 1 || res.data.niche_dis === 2) {
+            this.list.splice(1, 1)
+          }
+        }
+      })
+    },
+    // 查询收录情况
+    getEmployData() {
+      const url = '/jyapi/crmApplication/employ/info'
+      const params = {
+        employType: 1,
+        idArr: this.id,
+        from: this.fromJhfp ? this.fromJhfp : ''
+      }
+      this.ajaxComponent(url, params).then((res) => {
+        console.info(res)
+        if (res.error_code === 0) {
+          this.employInfo = res.data
+          var employItem = res.data[0]
+          const filteredList = []
+          if (employItem.isEmploy) {
+            this.list.forEach((v, index) => {
+              if (v.class === 'employ') {
+                v.active = employItem.isEmploy ? 1 : 0
+                filteredList.push(v)
+              }
+              if (v.class === 'ignore') {
+                v.active = employItem.isIgnore ? 1 : 0
+                filteredList.push(v)
+              }
+              if (v.class === 'jihui') {
+                v.msg = employItem.chanceCount
+                if (employItem.type === 2 || employItem.type === 1) {
+                  filteredList.push(v)
+                }
+              } else if (v.class === 'xiansuo') {
+                v.msg = employItem.clueCount
+                if (employItem.type === 1) {
+                  filteredList.push(v)
+                }
+              } else if (v.class === 'custom') {
+                v.msg = employItem.customCount
+                if (employItem.type !== 3) {
+                  filteredList.push(v)
+                }
+              }
+            })
+            this.list = filteredList
+          }
+        }
+      })
+    },
+    // 物业专版收录操作
+    setOpEvent(item) {
+      let url = '/jyapi/crmApplication/employ/operate'
+      let info = this.employInfo[0]
+      let params = {
+        idArr: this.id,
+        isEmploy: !info.isEmploy,
+        sourceType: 1,
+        employType: 1
+      }
+      if (item.active && this.fromJhfp) {
+        params.from = this.fromJhfp
+      }
+      if (!item.active) {
+        url = '/jyNewApi/property/information/exist'
+        info = this.employInfo[0]
+        params = {
+          id: id
+        }
+        this.ajaxComponent(url, params).then((res) => {
+          if (!item.active) {
+            if (res.code === 2) {
+              // 已经创建情报信息,直接收录
+              item.active = 1
+            } else if (res.code === 1) {
+              // 未创建情报信息,需要手动创建
+              this.showPropertyDialog = true
+            } else {
+              this.$toast(res.msg, 1000)
+            }
+          }
+        })
+      } else {
+        this.ajaxComponent(url, params).then((res) => {
+          if (res.error_code === 0) {
+            if (res.data.status) {
+              item.active = item.active === 0 ? 1 : 0
+            } else {
+              this.$toast(res.data.msg, 1000)
+            }
+          }
+        })
+      }
+      this.getEmployData()
+      console.info(this.list)
+    },
+    // 收录操作
+    setEmployEvent(item) {
+      const url = '/jyapi/crmApplication/employ/operate'
+      const info = this.employInfo[0]
+      const params = {
+        idArr: this.id,
+        isEmploy: !info.isEmploy,
+        sourceType: 1,
+        employType: 1
+      }
+      this.ajaxComponent(url, params).then((res) => {
+        if (res.error_code === 0) {
+          if (res.data.status) {
+            item.active = item.active === 0 ? 1 : 0
+          } else {
+            this.$toast(res.data.msg, 1000)
+          }
+          this.getEmployData()
+          console.info(this.list)
+        }
+      })
+    },
+    // 忽略操作
+    setIgnoreEvent(item) {
+      var info = this.employInfo[0]
+      var params = {
+        idArr: this.id,
+        isIgnore: !info.isIgnore,
+        employType: 1
+      }
+      var url = '/jyapi/crmApplication/ignore/operate'
+      this.ajaxComponent(url, params).then((res) => {
+        if (res && res.error_code === 0) {
+          if (res.data && res.data.status) {
+            item.active = item.active === 0 ? 1 : 0
+          } else {
+            this.$toast(res.data.msg, 1000)
+          }
+        }
+      })
+    },
+    // 封装公共ajax
+    ajaxComponent(url, params) {
+      return new Promise((resolve, reject) => {
+        $.ajax({
+          type: 'POST',
+          url: url,
+          contentType: 'application/json',
+          data: JSON.stringify(params) || {},
+          success: function (res) {
+            resolve(res)
+          },
+          error: function (err) {
+            reject(err)
+          }
+        })
+      })
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.content-bi-actions {
+  iframe {
+    min-height: 60vh;
+  }
+  .com-statusbar-BI {
+    margin-top: 16px;
+  }
+  .crm-action {
+    float: right;
+    display: flex;
+    align-items: center;
+    line-height: 20px;
+  }
+  .action-content {
+    margin-left: 16px;
+    cursor: pointer;
+  }
+  .action-content .iconfont {
+    position: relative;
+    font-size: 20px;
+    font-style: normal;
+    color: #2abed1;
+  }
+
+  .action-content .iconfont .msg {
+    position: absolute;
+    top: -8px;
+    right: -6px;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    width: 16px;
+    height: 16px;
+    background: #ff3a20;
+    border-radius: 50%;
+    color: #fff;
+    font-size: 12px;
+  }
+
+  .action-employ .iconfont,
+  .action-ignore .iconfont {
+    color: #aaa;
+  }
+
+  .action-employ .iconfont.checked {
+    color: #2abed1;
+  }
+
+  .action-ignore .iconfont.checked {
+    color: #ff9f40;
+  }
+}
+</style>

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

@@ -0,0 +1,403 @@
+<script setup>
+import shareBox from '@/components/shareBox/index.vue'
+import powerPerson from '@/components/subscribe-manager/powerPerson.vue'
+import QuickMonitor from '@/composables/quick-monitor/component/QuickMonitor.vue'
+import ArticleStar from '@/components/push-list/ArticleStar.vue'
+import {
+  ContentModel,
+  ContentId
+} from '@/views/article-content/composables/useContentStore'
+import { computed, getCurrentInstance, onMounted, ref } from 'vue'
+import { useRoute } from 'vue-router/composables'
+import { useStore } from '@/store'
+import { useDistribute } from '@/views/article-content/composables/useDistribute'
+import { useShare } from '@/views/article-content/composables/useShare'
+import { useArticleStarModel } from '@/views/article-content/composables/useArticleStar'
+import BidrenewalDialog from '@/views/BidrenewalDialog/index.vue'
+import { useQuickJoinBidModel } from '@/composables/quick-join-bid'
+import WorkspaceButtonGroup from '@/components/dialog/WorkspaceButtonGroup.vue'
+import ContentBIActions from '@/views/article-content/components/ContentBIActions.vue'
+
+const getters = useStore().getters
+const vt = computed(() => {
+  return useStore().getters['user/vt']
+})
+
+const headerType = '业主委托项目'
+
+const keepLabel = [
+  {
+    label: '采购',
+    link: ''
+  },
+  {
+    label: '政务',
+    link: ''
+  }
+]
+
+const params = useRoute().params
+const wxShareImgURL = '/biddetail/normal/qr/' + params.id
+
+// 分享
+const { openShare, useShareRef } = useShare()
+// 分发
+const { doSubmitDistribute, openDistribute, usePowerRef } = useDistribute()
+// 监控
+const { starModel, doFetchStarState } = useArticleStarModel(ContentId.value)
+doFetchStarState()
+
+// 参标
+const {
+  BidrenewalDialogElement,
+  JoinBidModel,
+  doFetchJoinBid,
+  doChangeJoinBid
+} = useQuickJoinBidModel({ id: ContentId.value })
+doFetchJoinBid()
+
+const JoinBidInfo = computed(() => {
+  return JoinBidModel.value.projectInfo.joinBidInfo
+})
+
+const canShowJoinBidTip = computed(() => {
+  return JoinBidInfo.value.nameStr
+})
+const bidCountdown = computed(() => {
+  return JoinBidModel.value.bidCountdown
+})
+
+// 判断在哪个容器
+const InWhichContainer = window.parent !== window ? 'in-app' : 'in-web'
+
+// 判断是否兼容BI
+const isUseBIActions =
+  useRoute().query.resource === 'BI' ||
+  useRoute().query.property === 'BIProperty'
+</script>
+
+<template>
+  <div class="common-content-header">
+    <WorkspaceButtonGroup
+      v-if="InWhichContainer === 'in-web'"
+      class="right-work-actions"
+    ></WorkspaceButtonGroup>
+    <div class="before-type" v-if="ContentModel.isSelfSite">
+      {{ headerType }}
+    </div>
+    <h1 class="title">
+      <span v-html="ContentModel.titleHighlighted"></span>
+    </h1>
+    <div class="tags">
+      <a
+        :href="item.link"
+        v-for="(item, index) in ContentModel.tags"
+        :key="index"
+        >{{ item.label }}</a
+      >
+    </div>
+    <div class="actions-info">
+      <div class="time-label">{{ ContentModel.time }}</div>
+      <div class="actions" v-if="!isUseBIActions">
+        <div class="action-item" @click="openDistribute" v-if="vt === 'q'">
+          <span class="iconfont icon-shoudongfenfa"></span>
+          <span class="text">分发</span>
+        </div>
+        <div class="action-item" @click="openShare">
+          <span class="iconfont icon-fenxiang"></span>
+          <span class="text">转给同事</span>
+        </div>
+
+        <div class="action-item">
+          <el-popover
+            class="action-wx-popover"
+            placement="bottom"
+            title="微信扫一扫"
+            width="120"
+            :append-to-body="false"
+            trigger="hover"
+          >
+            <img :src="wxShareImgURL" alt="微信分享" />
+            <div class="action-item" slot="reference">
+              <span class="iconfont icon-weixin_line"></span>
+              <span class="text">微信分享</span>
+            </div>
+          </el-popover>
+        </div>
+
+        <quick-monitor
+          class="action-item"
+          :cache="true"
+          type="project"
+          :params="ContentId"
+        ></quick-monitor>
+
+        <div
+          class="action-item"
+          v-if="JoinBidInfo.showParticipate"
+          @click="doChangeJoinBid('in')"
+        >
+          <span class="iconfont icon-canbiao"></span>
+          <span class="text">参标</span>
+        </div>
+        <div
+          class="action-item"
+          v-if="JoinBidInfo.showStopParticipate"
+          @click="doChangeJoinBid('out')"
+        >
+          <span class="iconfont icon-canbiao icon-stop-canbiao"></span>
+          <span class="text">终止参标</span>
+        </div>
+
+        <div class="action-item">
+          <article-star
+            :id="ContentId"
+            :star="starModel.star"
+            @change="doFetchStarState"
+            @change-labels="doFetchStarState"
+          ></article-star>
+        </div>
+      </div>
+      <div class="actions" v-if="isUseBIActions">
+        <content-b-i-actions :id="ContentId"></content-b-i-actions>
+      </div>
+    </div>
+    <div class="expands-info">
+      <div class="join-bid-actions flex flex-(row items-center)">
+        <el-tooltip
+          class="item"
+          effect="dark"
+          placement="bottom-end"
+          v-if="canShowJoinBidTip"
+        >
+          <div
+            slot="content"
+            style="max-width: 200px"
+            v-if="JoinBidInfo.userName"
+          >
+            {{ JoinBidInfo.userName.replace(/,/g, '、') }}
+          </div>
+          <div class="ent-features">
+            <span class="iconfont icon-ren"></span>
+            <div class="join-users">参标人:{{ JoinBidInfo.nameStr }}</div>
+          </div>
+        </el-tooltip>
+        <div class="count-down-time" v-if="bidCountdown">
+          投标截止倒计时:{{ bidCountdown }}
+        </div>
+      </div>
+      <div>
+        <div
+          class="favorite-tags"
+          v-if="starModel.labels && starModel.labels.length"
+        >
+          <span class="grey-text">个人标签:</span>
+          <div
+            style="display: inline-block"
+            v-for="(item, index) in starModel.labels"
+            :key="index"
+          >
+            <a :href="item.link" target="_blank">
+              {{ item.label }}
+            </a>
+            <span v-if="index < starModel.labels.length - 1">,</span>
+          </div>
+        </div>
+      </div>
+    </div>
+    <!-- 分享弹窗 -->
+    <shareBox ref="useShareRef"></shareBox>
+    <!-- 分发人员弹窗 -->
+    <powerPerson
+      ref="usePowerRef"
+      :vt="vt"
+      @manualDiatribution="doSubmitDistribute"
+    ></powerPerson>
+    <!-- 参标更新状态弹窗 -->
+    <BidrenewalDialog
+      ref="BidrenewalDialogElement"
+      @saveCallback="doFetchJoinBid"
+    ></BidrenewalDialog>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+.common-content-header {
+  position: relative;
+  margin: 0 auto;
+  background-color: #fff;
+  padding: 32px 40px;
+  border-radius: 8px;
+
+  .ent-features {
+    display: flex;
+    flex-direction: row;
+    align-items: center;
+  }
+  .join-users {
+    margin-left: 4px;
+    font-size: 14px;
+  }
+
+  .favorite-tags .grey-text {
+    color: #686868;
+  }
+
+  .right-work-actions {
+    position: absolute;
+    right: 0;
+    top: 0;
+  }
+
+  .before-type {
+    position: absolute;
+    top: 0;
+    left: 0;
+    z-index: 1;
+    display: inline-block;
+    color: #ff3a20;
+    font-size: 14px;
+    font-style: normal;
+    font-weight: 400;
+    line-height: 22px;
+    padding: 2px 12px;
+    background-color: rgba(255, 58, 32, 0.1);
+    border-top-left-radius: 8px;
+    border-bottom-right-radius: 12px;
+  }
+  .title {
+    color: #252627;
+    font-size: 24px;
+    font-style: normal;
+    font-weight: 400;
+    line-height: 36px;
+    ::v-deep {
+      .keyword.keyword-underline {
+        border-width: 0;
+        cursor: unset;
+      }
+    }
+  }
+  .tags {
+    margin-top: 12px;
+  }
+  .tags a {
+    border-radius: 4px;
+    border: 1px solid #ececec;
+    background: #f5f5fb;
+    padding: 1px 8px;
+    color: #686868;
+    text-align: center;
+    font-size: 12px;
+    font-style: normal;
+    font-weight: 400;
+    line-height: 18px;
+    text-decoration: none;
+    &[href*='javascript'] {
+      cursor: unset;
+    }
+    & + a {
+      margin-left: 8px;
+    }
+  }
+  .base-flex {
+    display: flex;
+    flex-direction: row;
+    align-items: center;
+    justify-content: space-between;
+  }
+  .actions-info {
+    @extend .base-flex;
+    margin-top: 8px;
+    .time-label {
+      color: #999;
+      font-size: 12px;
+      font-style: normal;
+      font-weight: 400;
+      line-height: 18px;
+    }
+    .actions {
+      display: flex;
+      flex-direction: row;
+      align-items: center;
+      color: #1d1d1d;
+      font-size: 13px;
+      .iconfont {
+        font-size: 18px;
+        color: #9b9ca3;
+
+        &.icon-stop-canbiao {
+          color: #ff3b20;
+        }
+      }
+
+      .action-item {
+        display: flex;
+        flex-direction: row;
+        align-items: center;
+        cursor: pointer;
+        & + .action-item {
+          margin-left: 16px;
+        }
+        span + span {
+          margin-left: 2px;
+        }
+      }
+
+      .action-wx-popover {
+        ::v-deep {
+          .el-popover {
+            width: 120px;
+            min-width: unset;
+            display: flex;
+            flex-direction: column;
+            align-items: center;
+            padding: 0;
+            padding-top: 12px;
+          }
+          .el-popover__title {
+            font-size: 14px;
+            line-height: 18px;
+            color: #1d1d1d;
+            font-weight: bold;
+            margin-bottom: 0;
+          }
+        }
+
+        img {
+          min-width: 120px;
+          height: 120px;
+        }
+      }
+    }
+  }
+  .expands-info {
+    margin-top: 8px;
+    @extend .base-flex;
+    .icon-ren {
+      font-size: 18px;
+      color: #2abed1;
+    }
+  }
+
+  .join-bid-actions {
+    position: relative;
+    .count-down-time {
+      margin-left: 16px;
+      color: #ff3a20;
+      font-size: 12px;
+      line-height: 18px;
+      background: rgba(254, 115, 122, 0.16);
+      border-radius: 4px;
+      padding: 3px 6px;
+    }
+  }
+
+  ::v-deep {
+    .article-star-module .icon-collect {
+      width: 18px;
+      height: 18px;
+    }
+  }
+}
+</style>

+ 100 - 0
apps/bigmember_pc/src/views/article-content/components/ContentHeaderSkeleton.vue

@@ -0,0 +1,100 @@
+<template>
+  <div class="common-content-header-skeleton">
+    <h1 class="title">
+      <el-skeleton-item variant="text" style="width: 30em" />
+    </h1>
+    <div class="tags">
+      <el-skeleton-item variant="text" style="width: 6em; margin-left: 4px" />
+      <el-skeleton-item variant="text" style="width: 4em; margin-left: 4px" />
+      <el-skeleton-item variant="text" style="width: 4em; margin-left: 4px" />
+    </div>
+    <div class="actions-info">
+      <div class="time-label">
+        <el-skeleton-item variant="text" style="width: 4em" />
+      </div>
+      <div class="actions">
+        <div class="action-item">
+          <el-skeleton-item variant="text" style="width: 1em" />
+          <el-skeleton-item
+            variant="text"
+            style="width: 3em; margin-left: 4px"
+          />
+        </div>
+        <div class="action-item">
+          <el-skeleton-item variant="text" style="width: 1em" />
+          <el-skeleton-item
+            variant="text"
+            style="width: 2em; margin-left: 4px"
+          />
+        </div>
+        <div class="action-item">
+          <el-skeleton-item variant="text" style="width: 1em" />
+          <el-skeleton-item
+            variant="text"
+            style="width: 4em; margin-left: 4px"
+          />
+        </div>
+        <div class="action-item">
+          <el-skeleton-item variant="text" style="width: 1em" />
+          <el-skeleton-item
+            variant="text"
+            style="width: 2em; margin-left: 4px"
+          />
+        </div>
+      </div>
+    </div>
+    <div class="expands-info">
+      <div>
+        <el-skeleton-item variant="text" style="width: 3em" />
+        <el-skeleton-item variant="text" style="width: 2em; margin-left: 4px" />
+        <el-skeleton-item variant="text" style="width: 2em; margin-left: 4px" />
+      </div>
+      <div>
+        <el-skeleton-item variant="text" style="width: 3em" />
+        <el-skeleton-item variant="text" style="width: 2em; margin-left: 4px" />
+        <el-skeleton-item variant="text" style="width: 2em; margin-left: 4px" />
+      </div>
+    </div>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+.common-content-header-skeleton {
+  position: relative;
+  margin: 0 auto;
+  background-color: #fff;
+  padding: 32px 40px;
+  border-radius: 8px;
+
+  .tags {
+    margin-top: 12px;
+  }
+  .base-flex {
+    display: flex;
+    flex-direction: row;
+    align-items: center;
+    justify-content: space-between;
+  }
+  .actions-info {
+    @extend .base-flex;
+    margin-top: 8px;
+    .actions {
+      display: flex;
+      flex-direction: row;
+      align-items: center;
+
+      .action-item {
+        margin: 0 6px;
+        display: flex;
+        flex-direction: row;
+        align-items: center;
+        cursor: pointer;
+      }
+    }
+  }
+  .expands-info {
+    margin-top: 8px;
+    @extend .base-flex;
+  }
+}
+</style>

+ 513 - 0
apps/bigmember_pc/src/views/article-content/components/ContentMask.vue

@@ -0,0 +1,513 @@
+<script setup>
+import { computed } from 'vue'
+// 三个类型的遮罩 free-max、proposed、purchase
+/**
+ *   if(!canRead){
+ *     sourceKey="jyarticle_see3_plus_pc"
+ *   }
+ *   if (subType == '拟建') {
+ *     sourceKey = 'article_proposed_project'
+ *   }
+ *   if (subType == '采购意向') {
+ *     sourceKey = 'article_purchase_intention'
+ *   }
+ */
+
+const props = defineProps({
+  type: {
+    type: String,
+    default: ''
+  }
+})
+
+const nowActiveMaskType = computed(() => {
+  return props.type
+})
+</script>
+<template>
+  <div
+    class="content-mask-container com-prebuilt"
+    v-if="nowActiveMaskType"
+    :class="nowActiveMaskType"
+    style="position: relative"
+  >
+    <div
+      v-if="nowActiveMaskType === 'proposed'"
+      class="mask-zz"
+      style="
+        position: absolute;
+        width: 100%;
+        padding: 0;
+        height: 408px;
+        background-color: white;
+        z-index: 1;
+      "
+    >
+      <img
+        style="width: 100%; height: 100%; margin-top: 27px"
+        src="@/assets/images/article-mask/pc_mh.png"
+      />
+      <div
+        class="mask-zz"
+        style="
+          position: absolute;
+          left: 50%;
+          top: 50%;
+          margin-top: -184px;
+          margin-left: -320px;
+          width: 650px;
+          height: 220px;
+          background-color: white;
+          z-index: 10;
+          border-radius: 10px;
+        "
+      >
+        <div style="position: relative">
+          <img
+            style="width: 100%; height: 128px"
+            src="@/assets/images/article-mask/pc-cq-mmt.png"
+          />
+          <div
+            id="tip-title"
+            style="
+              position: absolute;
+              top: 50%;
+              left: 50%;
+              margin-top: -38px;
+              transform: translateX(-50%);
+              height: 26px;
+              color: antiquewhite;
+              font-size: 14px;
+            "
+          >
+            超前项目抢先知,中标更容易
+          </div>
+        </div>
+        <div class="tip-box">
+          <div
+            class="tip-box-example"
+            style="width: 602px; height: 357px; margin-top: -88px; z-index: 9"
+          >
+            <img
+              style="width: 100%; height: 100%"
+              src="@/assets/images/article-mask/pc-nj-example.png"
+              alt=""
+            />
+          </div>
+          <div class="tip-text" style="margin-top: 12px">
+            提前3-12个月获取审批中的新项目,超前项目抢先介入,商机提前掌控。
+          </div>
+          <button
+            class="detail-nj-btn"
+            style="
+              background: #2cb7ca;
+              color: white;
+              border: none;
+              width: 132px;
+              height: 36px;
+              border-radius: 6px;
+              margin-bottom: 20px;
+              margin-top: 24px;
+            "
+            @click="$emit('doOpenCollect', 'article_proposed_project')"
+          >
+            点击进入
+          </button>
+        </div>
+      </div>
+    </div>
+
+    <div
+      v-if="nowActiveMaskType === 'purchase'"
+      class="mask-zzz"
+      style="padding: 0; height: 408px; background-color: white; z-index: 1"
+    >
+      <img
+        style="width: 100%; height: 100%"
+        src="@/assets/images/article-mask/pc_mh.png"
+      />
+      <div
+        class="mask-zzz"
+        style="
+          position: absolute;
+          left: 50%;
+          top: 50%;
+          transform: translate3d(-50%, -50%, 0);
+          width: 500px;
+          background-color: white;
+          z-index: 10;
+          border-radius: 10px;
+        "
+      >
+        <div style="position: relative">
+          <img
+            style="width: 100%; height: 35%"
+            src="@/assets/images/article-mask/pc_zzt.png"
+          />
+          <div
+            style="
+              position: absolute;
+              top: 50%;
+              left: 50%;
+              transform: translate3d(-50%, -50%, 0);
+              height: 26px;
+              color: antiquewhite;
+              font-size: 16px;
+            "
+          >
+            项目提前介入,中标更轻松
+          </div>
+        </div>
+        <div class="tip-box">
+          <div class="tip-text">
+            提前1-3个月获取项目信息,及早介入准备更充分
+          </div>
+          <button
+            class="detail-nj-btn"
+            style="
+              background: #2cb7ca;
+              color: white;
+              border: none;
+              width: 132px;
+              height: 36px;
+              border-radius: 6px;
+              margin-bottom: 12px;
+              margin-top: 10px;
+            "
+            @click="$emit('doOpenCollect', 'article_purchase_intention')"
+          >
+            点击进入
+          </button>
+        </div>
+      </div>
+    </div>
+
+    <div
+      v-if="nowActiveMaskType === 'free-max'"
+      class="mask-zzz free-equity-mask"
+      style="
+        top: 0;
+        padding: 0;
+        height: 587px;
+        background-color: white;
+        z-index: 1;
+      "
+    >
+      <img
+        style="width: 100%; height: 100%"
+        src="@/assets/images/article-mask/pc_mh.png"
+      />
+      <div
+        class="mask-zzz"
+        style="
+          position: absolute;
+          left: 50%;
+          top: 50%;
+          transform: translate3d(-50%, -50%, 0);
+          width: 632px;
+          background-color: white;
+          z-index: 10;
+          border-radius: 10px;
+        "
+      >
+        <div style="position: relative">
+          <img
+            style="width: 100%; height: 35%; border-radius: 10px 10px 0 0"
+            src="@/assets/images/article-mask/pc_zzt_new.jpg"
+          />
+          <div
+            style="
+              position: absolute;
+              top: 50%;
+              left: 50%;
+              margin-top: -13px;
+              width: 100%;
+              height: 26px;
+              color: #1d1d1d;
+              font-size: 18px;
+              transform: translateX(-50%);
+              text-align: center;
+            "
+          >
+            您尚未完善信息,今日查看公告权限用完啦
+          </div>
+        </div>
+        <div
+          class="tip-box"
+          style="padding: 12px 32px 32px; border: 0; border-radius: 0 0 8px 8px"
+        >
+          <!-- <div class="tip-text">请完善个人信息,获取更多免费查看公告权限</div>
+          <button class="detail-nj-btn  jyarticle_see3" style="background: #2CB7CA;color: white;border:none;width: 132px;height: 36px;border-radius: 6px;margin-bottom: 12px;margin-top: 10px">立即解锁</button> -->
+          <div class="free-equity-contrast" style="display: flex">
+            <div class="table-columns">
+              <div class="item-td"></div>
+              <div class="item-td">标讯权益</div>
+              <div class="item-td">超前项目权益</div>
+              <div class="item-td">每日查看数量</div>
+              <div class="item-td"></div>
+            </div>
+            <div class="table-columns">
+              <div class="item-td">
+                <strong
+                  style="font-size: 14px; line-height: 22px; color: #1d1d1d"
+                  >免费注册用户</strong
+                >
+              </div>
+              <div
+                class="item-td"
+                style="
+                  border-right: 0;
+                  align-items: flex-start;
+                  padding-left: 16px;
+                "
+              >
+                可查看招标预告、招标公告、招标结果、招标信用信息
+              </div>
+              <div class="item-td">
+                <img
+                  src="@/assets/images/article-mask/icon-aaa.png"
+                  alt=""
+                  width="24"
+                  height="24"
+                />
+              </div>
+              <div class="item-td">3条</div>
+              <div class="item-td"></div>
+            </div>
+            <div class="table-columns">
+              <div class="item-td main-bg">
+                <strong>免费注册用户</strong
+                ><span style="font-size: 12px">(完善信息)</span>
+              </div>
+              <div
+                class="item-td"
+                style="border-right: 0; align-items: flex-end"
+              ></div>
+              <div class="item-td">
+                <img
+                  src="@/assets/images/article-mask/icon-aaa.png"
+                  alt=""
+                  width="24"
+                  height="24"
+                />
+              </div>
+              <div class="item-td main-color">不限</div>
+              <div class="item-td">
+                <span
+                  class="detail-nj-btn jyarticle_see3 perfect-btn"
+                  @click="
+                    $emit('doOpenCollect', {
+                      source: 'jyarticle_see3_plus_pc',
+                      reload: true
+                    })
+                  "
+                  >完善个人信息</span
+                >
+              </div>
+            </div>
+            <div class="table-columns">
+              <div class="item-td gold-bg">
+                <strong>大会员</strong>
+                <span style="font-size: 12px; line-height: 18px"
+                  >大数据赋能企业数字化营销</span
+                >
+                <a
+                  href="/big/page/index"
+                  target="_blank"
+                  style="
+                    font-size: 12px;
+                    text-decoration: underline;
+                    color: rgba(89, 57, 19, 1);
+                  "
+                  >全面了解></a
+                >
+              </div>
+              <div
+                class="item-td"
+                style="border-right: 0; align-items: flex-start"
+              ></div>
+              <div class="item-td gold-color">
+                采购意向
+                <br />
+                拟建项目
+                <br />
+                AI智能预测
+              </div>
+              <div class="item-td gold-color">不限</div>
+              <div class="item-td gold-color">
+                <span
+                  class="free-taste-btn"
+                  @click="$emit('doOpenCollect', 'pc_article_member_freeuse')"
+                  >免费体验</span
+                >
+                <span
+                  class="open-customer contact-kf"
+                  @click="$emit('doOpenCustomer')"
+                  >咨询客服</span
+                >
+              </div>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+.content-mask-container {
+  &.proposed {
+    height: 620px;
+    background-color: #fff;
+  }
+  &.purchase {
+  }
+  &.free-max {
+  }
+
+  .tip-box {
+    display: flex;
+    flex-direction: column;
+    justify-content: center;
+    align-items: center;
+    padding: 20px 35px;
+    background: #ffffff;
+    box-shadow: 0px 0px 28px 1px rgba(0, 0, 0, 0.07999999821186066);
+    border-radius: 8px 8px 8px 8px;
+    border: 1px solid #ececec;
+  }
+  .tip-text {
+    font-size: 16px;
+    font-family: Microsoft YaHei-Regular, Microsoft YaHei, sans-serif;
+    font-weight: 400;
+    color: #1d1d1d;
+    line-height: 24px;
+  }
+
+  .free-equity-contrast {
+    display: flex;
+    width: 100%;
+    border: 1px solid #ececec;
+    border-radius: 8px;
+  }
+  .free-equity-contrast .gold-bg {
+    border-radius: 0 8px 0 0;
+    border-right: 0;
+  }
+  .table-columns:nth-child(1) {
+    width: 116px;
+  }
+  .table-columns:nth-child(2) {
+    width: 116px;
+    color: #686868;
+  }
+  .table-columns:nth-child(3) {
+    width: 154px;
+    color: #686868;
+  }
+  .table-columns:nth-child(4) {
+    width: 184px;
+    color: #686868;
+  }
+  .table-columns .item-td {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+    border-bottom: 1px solid #ececec;
+    border-right: 1px solid #ececec;
+    white-space: nowrap;
+    transition: all 0.5s ease;
+    text-align: center;
+  }
+  .table-columns:nth-child(3):hover .item-td:nth-child(1),
+  .table-columns:nth-child(4):hover .item-td:nth-child(1) {
+    margin-top: -12px;
+    height: 84px;
+    border-radius: 8px 8px 0 0;
+    transition: all 0.5s ease;
+    overflow: hidden;
+  }
+  .table-columns:nth-child(3):hover .item-td:nth-child(5),
+  .table-columns:nth-child(4):hover .item-td:nth-child(5) {
+    margin-bottom: -12px;
+    height: 84px;
+    border-radius: 0 0 8px 8px;
+    background: #fff;
+    box-shadow: 0 6px 12px rgba(0, 0, 0, 0.1);
+    transition: all 0.5s ease;
+  }
+  .table-columns .item-td:nth-child(1) {
+    height: 72px;
+  }
+  .table-columns .item-td:nth-child(2) {
+    height: 48px;
+  }
+  .table-columns .item-td:nth-child(3) {
+    height: 82px;
+  }
+  .table-columns .item-td:nth-child(4) {
+    height: 48px;
+  }
+  .table-columns .item-td:nth-child(5) {
+    height: 72px;
+    border-bottom: 0;
+  }
+  .table-columns:nth-child(4) .item-td {
+    border-right: 0;
+  }
+  .table-columns:nth-child(1) .item-td,
+  .table-columns:nth-child(2) .item-td:nth-child(1) {
+    background: #f7f9fc;
+  }
+  .free-equity-contrast .item-td strong {
+    font-weight: 700;
+    font-size: 16px;
+    line-height: 22px;
+  }
+  .free-equity-contrast .item-td.main-bg {
+    background: #2cb7ca;
+    color: #fff;
+  }
+  .free-equity-contrast .item-td.gold-bg {
+    color: #593913;
+    background: linear-gradient(to right, #fff3e0, #f4deb1);
+  }
+  .free-equity-contrast .item-td.main-color {
+    color: #2cb7ca;
+  }
+  .free-equity-contrast .item-td.gold-color {
+    color: #b1700e;
+  }
+  .free-equity-contrast .perfect-btn {
+    display: inline-block;
+    padding: 5px 14px;
+    background: #2abed1;
+    border-radius: 6px;
+    color: #fff;
+    font-size: 14px;
+    line-height: 22px;
+    text-align: center;
+    cursor: pointer;
+  }
+  .free-equity-contrast .free-taste-btn {
+    display: inline-block;
+    padding: 5px 28px;
+    border: 1px solid #e7c28a;
+    background: linear-gradient(to right, #fff9f0, #fef0d7);
+    color: #b1700e;
+    font-size: 14px;
+    text-align: center;
+    border-radius: 6px;
+    cursor: pointer;
+  }
+  .free-equity-contrast .contact-kf {
+    margin-top: 2px;
+    text-decoration: underline;
+    color: #b1700e;
+    font-size: 12px;
+    line-height: 18px;
+    cursor: pointer;
+  }
+}
+</style>

+ 239 - 0
apps/bigmember_pc/src/views/article-content/components/ContentRightTimeLine.vue

@@ -0,0 +1,239 @@
+<script setup>
+import DownProjectReport from '@/composables/down-project-report/component/DownProjectReport.vue'
+import QuickMonitor from '@/composables/quick-monitor/component/QuickMonitor.vue'
+import { ContentModel } from '@/views/article-content/composables/useContentStore'
+import { useRouter } from 'vue-router/composables'
+const props = defineProps({
+  contentId: {
+    type: String,
+    default: ''
+  },
+  timeLineList: {
+    type: Array,
+    default: () => []
+  }
+})
+
+const router = useRouter()
+
+function doOpen(item) {
+  if (item.isActive) {
+    return
+  } else {
+    // 获取当前路由信息
+    const currentRoute = router.currentRoute
+    // 构建新的路由对象,复用除了ID以外的其他参数
+    const newRoute = {
+      name: currentRoute.name,
+      params: {
+        content: currentRoute.params.content,
+        id: item.id + '.html'
+      }
+    }
+    router.push(newRoute)
+  }
+}
+</script>
+<template>
+  <div class="right-time-line-container">
+    <div class="flex flex-col right-time-line-header-container">
+      <div class="content-block-header">招标/采购进度</div>
+      <div class="report-actions flex flex-(row items-center)">
+        <down-project-report
+          v-if="ContentModel.projectName"
+          :id="contentId"
+          :name="ContentModel.projectName"
+        ></down-project-report>
+        <quick-monitor
+          class="m-l-10px"
+          :cache="true"
+          :auto="false"
+          type="project"
+          :params="contentId"
+        />
+      </div>
+    </div>
+    <div class="right-style-time-line-container">
+      <div
+        class="right-style-time-item flex flex-(row justify-between)"
+        :class="{ 'is-active': item.isActive }"
+        v-for="(item, index) in timeLineList"
+        :key="index"
+        @click="doOpen(item)"
+      >
+        <div class="item-container flex flex-col flex-items-start">
+          <span class="item-time-label" v-if="item.time">{{ item.time }}</span>
+          <div
+            class="item-type-label flex flex-row flex-items-center"
+            v-if="item.contentType && item.contentType.length"
+          >
+            <span>{{ item.contentType[0] }}</span>
+            <span
+              class="item-type-label--line"
+              v-if="item.contentType[1]"
+            ></span>
+            <span v-if="item.contentType[1]">
+              {{ item.contentType[1] }}
+            </span>
+          </div>
+          <div class="item-money-label" v-if="item.money">{{ item.money }}</div>
+        </div>
+        <div class="item-after">
+          <div
+            class="item-after-line item-after-line--fill"
+            :class="{ 'empty-line': index === 0 }"
+          ></div>
+          <div class="item-after-tag" v-if="index === 0">最新</div>
+          <div class="item-after-circle" v-else></div>
+          <div
+            class="item-after-line"
+            :class="{ 'gray-line': !item.isActive }"
+          ></div>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+.right-time-line-container {
+  border-radius: 8px;
+  background: #fff;
+  margin-bottom: 20px;
+  .report-actions {
+    font-size: 13px;
+    color: #686868;
+    ::v-deep {
+      .down-project-report {
+        font-size: inherit;
+      }
+    }
+  }
+  .right-time-line-header-container {
+    padding: 16px;
+    padding-right: 0;
+    padding-bottom: 8px;
+    .content-block-header {
+      font-size: 16px;
+      font-weight: bold;
+      color: #1d1d1d;
+      margin-bottom: 8px;
+    }
+  }
+
+  .right-style-time-item {
+    cursor: pointer;
+    padding: 0 16px;
+    &.is-active,
+    &:hover {
+      background: rgba(42, 190, 209, 0.08);
+      .item-time-label,
+      .item-type-label {
+        color: #2abed1;
+      }
+      .item-type-label--line {
+        background-color: #2abed1;
+      }
+    }
+    &.is-active {
+      .item-after-line--fill {
+        background-color: #2abed1;
+      }
+    }
+    &:last-child .item-container {
+      border-color: transparent;
+    }
+    .item-container {
+      flex: 1;
+      padding-top: 10px;
+      padding-bottom: 10px;
+      border-bottom: 1px dashed #ececec;
+    }
+    .item-time-label {
+      color: #999;
+      font-size: 13px;
+      line-height: 20px;
+    }
+    .item-type-label {
+      margin-top: 8px;
+      font-size: 14px;
+      color: #1d1d1d;
+      line-height: 22px;
+      &--line {
+        width: 1px;
+        height: 12px;
+        background: #999;
+        margin: 0 8px;
+      }
+    }
+    .item-money-label {
+      margin-top: 6px;
+      display: inline-block;
+      padding: 1px 8px;
+      border-radius: 4px;
+      border: 1px solid #2abed1;
+      color: #2abed1;
+      text-align: center;
+      font-size: 12px;
+      line-height: 18px;
+    }
+
+    .item-after {
+      display: flex;
+      flex-direction: column;
+      align-items: center;
+      min-width: 32px;
+      margin-left: 6px;
+      &-tag {
+        margin-top: 10px;
+        padding: 1px 4px;
+        border-radius: 4px;
+        background: #2cb7ca;
+        color: #fff;
+        text-align: center;
+        font-size: 12px;
+        line-height: 18px;
+      }
+      &-circle {
+        flex-shrink: 0;
+        width: 16px;
+        height: 16px;
+        border-radius: 50%;
+        background-color: $color-text--highlight;
+        border: 3px solid #d2f6fc;
+        margin: 2px 0;
+      }
+      &-line {
+        width: 1px;
+        height: 100%;
+        background-color: #2cb7ca;
+        &--fill {
+          width: 1px;
+          height: 10px;
+          background-color: #ececec;
+        }
+        &.gray-line {
+          background-color: #ececec;
+        }
+        &.empty-line {
+          background-color: transparent;
+        }
+      }
+    }
+  }
+
+  .right-style-time-line {
+    ::v-deep {
+      .j-step .step-items {
+        flex-direction: row-reverse;
+      }
+      .step-item.cursor {
+        display: none;
+      }
+      .step-circle[data-tip='']::before {
+        transform: translate(0, -50%) scale(1.6);
+      }
+    }
+  }
+}
+</style>

+ 290 - 0
apps/bigmember_pc/src/views/article-content/components/ContentSummary.vue

@@ -0,0 +1,290 @@
+<script setup>
+import { computed, getCurrentInstance, ref } from 'vue'
+import CollectInfo from '@/components/collect-info/CollectInfo.vue'
+import { SummaryModel } from '@/views/article-content/composables/useContentStore'
+import { chunk, fill, padEnd } from 'lodash'
+import {
+  doOpenBuyerPage,
+  doOpenWinnerPage
+} from '@/views/article-content/composables/useArticleUtil'
+
+const tableConfig = ref({
+  column: [
+    {
+      width: '140px'
+    },
+    {},
+    {
+      width: '140px'
+    },
+    {}
+  ]
+})
+
+const contentSummaryTable = computed(() => {
+  const list = SummaryModel.value.list.filter((v) => v.value) || []
+  let result = []
+
+  if (!list.length) {
+    return []
+  }
+
+  for (let i = 0; i < list.length; i++) {
+    const item = list[i]
+    let type = item?.type || ''
+    if (item?.link) {
+      type = 'unit'
+    }
+    if (type === 'contact') {
+      if (!item?.expand || item?.expand?.link === false) {
+        type = ''
+      }
+    }
+    if (item?.isFreeView) {
+      type = 'free-view'
+    }
+    result = result.concat([
+      {
+        label: item.label
+      },
+      {
+        type: type,
+        label: item.value,
+        data: item
+      }
+    ])
+  }
+  // 按 table column 数量分组
+  result = chunk(result, 4)
+
+  // 不足的数组需要补全
+  const lastArr = result[result.length - 1]
+  let fillArr = new Array(4 - lastArr.length)
+  fill(fillArr, { label: '' })
+  result[result.length - 1] = lastArr.concat(fillArr)
+
+  return result
+})
+
+// 单元格样式
+function getCellClassName({ row, column, rowIndex, columnIndex }) {
+  if (columnIndex === 0 || columnIndex === 2) {
+    return 'label-col'
+  }
+}
+
+// 单元格合并
+function arraySpanMethod({ row, column, rowIndex, columnIndex }) {
+  // 合并空单元格
+  // if (row[2].label === '') {
+  //   if (columnIndex === 1) {
+  //     return [1, 3]
+  //   }
+  //   if (columnIndex >= 2) return [0, 0]
+  // }
+  // if (row[0].label === '中标金额(元)') {
+  //   if (columnIndex === 1) {
+  //     return [1, 3]
+  //   }
+  //   if (columnIndex >= 2) return [0, 0]
+  // }
+  return [1, 1]
+}
+
+// 打开留资弹窗
+const collectElement = ref(null)
+function doOpenCollectDialog(key) {
+  collectElement.value?.noCallApiFn(key || 'pc_article_customization', false)
+}
+// 联系客服
+const that = getCurrentInstance().proxy
+function doOpenCustomer() {
+  that?.contactCustomer(that)
+}
+
+function doOpenItem(item, type = '') {
+  if (item.type === 'free-view') {
+    return doOpenCollectDialog('peugeot_view_infor')
+  }
+  const key = item.data.key
+  if (key.indexOf('winner') !== -1) {
+    const id = item.type === 'contact' ? item.data.expand.id : item.data.id
+    const query =
+      type === 'contact'
+        ? {
+            active: 3
+          }
+        : {}
+    return doOpenWinnerPage({ id, query })
+  }
+  if (key.indexOf('buyer') !== -1) {
+    const name =
+      item.type === 'contact' ? item.data.expand.value : item.data.value
+    const query =
+      type === 'contact'
+        ? {
+            active: 1
+          }
+        : {}
+    return doOpenBuyerPage({ name, query })
+  }
+}
+</script>
+<template>
+  <div class="common-content-summary">
+    <div class="flex flex-(row items-center justify-between)">
+      <div class="content-block-header">公告摘要</div>
+
+      <div
+        class="summary-header-tip color-highlight flex flex-(row items-center)"
+        @click="doOpenCollectDialog('pc_article_customization')"
+      >
+        最近5年招标采购数据均可导出下载,如需更多年份和行业字段您可申请数据定制
+        <i class="iconfont icon-more"></i>
+      </div>
+    </div>
+
+    <el-table
+      class="summary-table"
+      :span-method="arraySpanMethod"
+      :data="contentSummaryTable"
+      border
+      :show-header="false"
+      :cell-class-name="getCellClassName"
+      style="width: 100%"
+    >
+      <el-table-column
+        v-for="(item, index) in tableConfig.column"
+        :key="index"
+        :width="item.width"
+      >
+        <template slot-scope="scope">
+          <div class="ellipsis-2" v-if="!scope.row[index]?.type">
+            {{ scope.row[index].label }}
+          </div>
+          <div v-else>
+            <div class="td-unit" v-if="scope.row[index].type === 'unit'">
+              <span
+                @click="doOpenItem(scope.row[index])"
+                class="text--line ellipsis-2"
+                >{{ scope.row[index].label }}</span
+              >
+              <span
+                @click="doOpenItem(scope.row[index])"
+                class="go-more-action m-l-16px flex flex-(row items-center shrink-0)"
+                v-if="scope.row[index].label"
+              >
+                查看详情
+                <i class="iconfont icon-more"></i>
+              </span>
+            </div>
+            <div
+              class="td-free-view"
+              v-if="scope.row[index].type === 'free-view'"
+            >
+              <span class="ellipsis-2" @click="doOpenItem(scope.row[index])">
+                {{ scope.row[index].label }}
+              </span>
+            </div>
+            <div class="td-phone" v-if="scope.row[index].type === 'contact'">
+              <span class="text--line">
+                {{ scope.row[index].label }}
+              </span>
+              <span
+                @click="doOpenItem(scope.row[index], 'contact')"
+                class="go-more-action flex m-l-16px flex-(row items-center shrink-0)"
+                v-if="scope.row[index].label"
+              >
+                更多联系人
+                <i class="iconfont icon-more"></i>
+              </span>
+            </div>
+          </div>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <div class="table-footer-tip">
+      *以上摘要信息由剑鱼标讯智能提取,仅供参考。如有误差,请
+      <span class="highlight-label" @click="doOpenCustomer">联系客服</span>
+      进行处理。
+    </div>
+    <!-- 留资 -->
+    <collect-info ref="collectElement"></collect-info>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+.common-content-summary {
+  .summary-header-tip {
+    font-size: 14px;
+    cursor: pointer;
+  }
+  .table-footer-tip {
+    margin-bottom: 8px;
+    color: #888;
+    font-size: 14px;
+    font-style: normal;
+    font-weight: 400;
+    line-height: normal;
+    .highlight-label {
+      cursor: pointer;
+      color: #2abed1;
+      text-decoration-line: underline;
+    }
+  }
+
+  ::v-deep {
+    .el-table.summary-table {
+      margin: 16px 0;
+      font-size: 14px;
+      font-style: normal;
+      font-weight: 400;
+      line-height: 22px;
+      color: #1d1d1d;
+      background-color: #fff;
+
+      tr:hover > td.el-table__cell {
+        background-color: unset;
+      }
+
+      tr:hover > td.el-table__cell.label-col,
+      .label-col {
+        color: #5c5d61;
+        background: #f7f9fc;
+      }
+      .td-free-view,
+      .td-phone {
+        display: flex;
+        flex-direction: row;
+        align-items: center;
+        justify-content: space-between;
+        span {
+          color: #2abed1;
+          cursor: pointer;
+        }
+      }
+      .td-phone .text--line {
+        color: #1d1d1d;
+        cursor: unset;
+      }
+      .td-unit {
+        display: flex;
+        flex-direction: row;
+        align-items: center;
+        justify-content: space-between;
+        color: #2abed1;
+        .text--line {
+          cursor: pointer;
+          text-decoration-line: underline;
+        }
+        .go-more-action {
+          flex-shrink: 0;
+          margin-left: 34px;
+          cursor: pointer;
+        }
+      }
+    }
+  }
+}
+</style>

+ 284 - 0
apps/bigmember_pc/src/views/article-content/components/ContentThirdPopover.vue

@@ -0,0 +1,284 @@
+<script setup>
+import { ajaxGetThirdPopoverInfo } from '@/api/modules/detail'
+import { onMounted, ref } from 'vue'
+
+const emit = defineEmits(['open-collect'])
+function doOpenCollect() {
+  emit('open-collect', 'certificateServices-pc-biddingDetailPage-content')
+}
+
+const popoverInfo = ref({
+  tel: '',
+  img: ''
+})
+
+function getInfo() {
+  ajaxGetThirdPopoverInfo({
+    module: 'tripartiteAuth'
+  }).then((res) => {
+    if (res && res.data) {
+      popoverInfo.value.tel = res.data.phone
+      // /common-module/bidedoc/image/third-party-verify-customer.png
+      popoverInfo.value.img = res.data.wxCodeImg
+    }
+  })
+}
+
+onMounted(() => {
+  getInfo()
+})
+</script>
+<template>
+  <div class="third-party-popover-content-template">
+    <div class="third-party popover-content-header">
+      <div class="p-c-h-title">剑鱼认证服务</div>
+      <div class="p-c-h-content">
+        <div class="p-c-h-c-list">
+          <div class="p-c-h-c-item">
+            <span class="icon-verify-checked"></span>&nbsp;&nbsp;认监委官网可查
+          </div>
+          <div class="p-c-h-c-item">
+            <span class="icon-verify-checked"></span>&nbsp;&nbsp;认证品类齐全
+          </div>
+          <div class="p-c-h-c-item">
+            <span class="icon-verify-checked"></span>&nbsp;&nbsp;量身定制方案
+          </div>
+        </div>
+        <div>招投标必备 · 品牌提升 · 奖励补贴 · 吸引投资</div>
+        <div>ISO体系认证丨信用评定丨服务体系认证丨其他认证证书</div>
+      </div>
+    </div>
+    <div class="third-party popover-content-main bidcontent">
+      <div class="bid_tel flex">
+        <img
+          src="@/assets/images/tel.png"
+          alt=""
+          style="width: 20px; height: 20px; margin-right: 2px"
+        />
+        <span class="bid_phonetext">咨询 {{ popoverInfo.tel }} 了解更多</span>
+      </div>
+      <div class="p-c-m-content">
+        <div class="p-c-m-content-l bid_classfun">
+          <div class="classfun_list">
+            <img
+              src="@/assets/images/blue-duihao.png"
+              alt=""
+              style="width: 20px; height: 20px"
+            />
+            <span class="classs_text"
+              >体系认证:品牌提升,投标加分,提升企业竞争力</span
+            >
+          </div>
+          <div class="classfun_list">
+            <img
+              src="@/assets/images/blue-duihao.png"
+              alt=""
+              style="width: 20px; height: 20px"
+            />
+            <span class="classs_text">信用认证:企业信用名片,招投标必备</span>
+          </div>
+          <div class="classfun_list">
+            <img
+              src="@/assets/images/blue-duihao.png"
+              alt=""
+              style="width: 20px; height: 20px"
+            />
+            <span class="classs_text">服务体系认证:实力认证,竞争有优势</span>
+          </div>
+        </div>
+        <div class="p-c-m-content-r">
+          <div class="t-p-verify-customer-qr" title="客服企业微信二维码">
+            <img :src="popoverInfo.img" alt="客服企业微信二维码" />
+          </div>
+          <div class="p-c-m-c-r-text">添加客服微信<br />备注认证服务</div>
+        </div>
+      </div>
+    </div>
+    <div class="third-party popover-content-footer bidfoot">
+      <a
+        class="bid_button_cancel bid_btn"
+        href="/swordfish/frontPage/enterpriseCertificatio/free/index"
+        target="_blank"
+        >了解详情</a
+      >
+      <div
+        class="bid_button_confirm bid_btn third-party-apply-for-button"
+        @click="doOpenCollect"
+      >
+        申请认证
+      </div>
+    </div>
+  </div>
+</template>
+<style lang="scss">
+.content-detail-container {
+  .third-party-verify-button {
+    position: relative;
+    margin: 0 6px;
+    display: inline-flex;
+    align-items: center;
+    color: #2abed1;
+    cursor: pointer;
+  }
+  .third-party-verify-button > .icon {
+    position: absolute;
+    left: 0;
+    top: 50%;
+    transform: translate(0, -50%);
+  }
+  .third-party-verify-button .third-party-button-text {
+    margin-left: 20px;
+    line-height: 20px;
+    font-size: 14px;
+  }
+}
+.icon-third-party-verify-logo {
+  display: inline-block;
+  width: 18px;
+  height: 18px;
+  background: url(~@/assets/images/icon/verify-logo.png) no-repeat center center;
+  background-size: contain;
+}
+.icon-verify-checked {
+  display: inline-block;
+  width: 18px;
+  height: 18px;
+  background: url(~@/assets/images/icon/verify-check.png) no-repeat center
+    center;
+  background-size: contain;
+}
+
+.third-party-popover-content-template {
+  width: 623px;
+  background-color: #fff;
+  padding: 24px;
+  border-radius: 12px;
+
+  .center-card-container {
+    display: flex;
+    padding: 15px 0;
+  }
+  .center-card-container .center-card-item:not(:last-of-type) {
+    margin-right: 20px;
+  }
+
+  .center-card-item {
+    display: flex;
+    flex-direction: column;
+    flex: 1;
+    padding: 8px 24px;
+    border: 1px solid #ececec;
+    border-radius: 6px;
+  }
+  .center-card-item .cci-title {
+    margin-bottom: 4px;
+    font-size: 16px;
+    line-height: 24px;
+    color: #1d1d1d;
+  }
+  .center-card-item .cci-content .cci-text:not(:last-of-type) {
+    margin-right: 16px;
+  }
+  .center-card-item .cci-text {
+    position: relative;
+    font-size: 14px;
+    cursor: pointer;
+  }
+  .center-card-item .cci-text.cci-light {
+    color: #2cb7ca;
+  }
+  .center-card-item .cci-text.cci-dark {
+    color: #686868;
+  }
+  .center-card-item .cci-text.suffix-right::after {
+    content: '>';
+    position: absolute;
+    right: 0;
+    top: 50%;
+    transform: translate(120%, -50%);
+    color: inherit;
+  }
+  .third-party-popover-content a,
+  .center-card-item a {
+    text-decoration: none;
+  }
+
+  .bid_button_cancel:hover {
+    text-decoration: none !important;
+  }
+
+  .lead-btn .join {
+    margin-left: 0;
+    width: unset;
+    height: unset;
+    text-align: left;
+    background-color: transparent;
+  }
+
+  .biddetail-content .popover[role='tooltip'][id^='popover'] {
+    padding: 12px;
+    max-width: unset;
+    width: 620px;
+    border-color: #ebeef5;
+  }
+  .biddetail-content .arrow {
+    display: none;
+  }
+  .third-party.popover-content {
+    padding: 20px;
+  }
+  .third-party.popover-content-main {
+    border-top: 1px solid #ececec;
+  }
+  .third-party.popover-content-header {
+    padding-bottom: 16px;
+  }
+  .third-party.popover-content-header .p-c-h-title {
+    font-size: 18px;
+    line-height: 28px;
+    color: #1d1d1d;
+  }
+  .third-party.popover-content-header .p-c-h-content {
+    margin-top: 10px;
+    font-size: 14px;
+    line-height: 22px;
+    color: #686868;
+  }
+  .third-party.popover-content-header .p-c-h-c-list {
+    display: flex;
+    align-items: center;
+  }
+  .third-party.popover-content-header .p-c-h-c-item {
+    margin-right: 32px;
+    display: flex;
+    align-items: center;
+  }
+  .third-party.popover-content-main .p-c-m-content {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+  }
+  .third-party.popover-content-main .p-c-m-content .classfun_list {
+    margin-bottom: 8px;
+    font-size: 14px;
+    line-height: 22px;
+    color: #1d1d1d;
+  }
+  .third-party.popover-content-main .p-c-m-content-r {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+  }
+  .third-party.popover-content-main .p-c-m-content-r img {
+    display: block;
+    width: 100%;
+  }
+  .t-p-verify-customer-qr {
+    padding: 4px;
+    width: 82px;
+    border-radius: 6px;
+    border: 1px solid #2abed1;
+  }
+}
+</style>

+ 108 - 0
apps/bigmember_pc/src/views/article-content/components/FooterAd.vue

@@ -0,0 +1,108 @@
+<script setup>
+import { ref, onMounted, onBeforeUnmount, nextTick } from 'vue'
+import adsense from '@/views/order/components/adsense/index.vue'
+
+const footerElement = ref(null)
+const needMoreAdShow = ref(false)
+function windowScrollFn() {
+  // 底部横幅固定
+  var stickyFooter = footerElement.value
+  if (needMoreAdShow.value) {
+    // 吸底
+    // 如果距离底部
+    const bottomFooter = document.querySelector('.j-bottom')
+    var ob = { top: 0 }
+    if (bottomFooter?.getBoundingClientRect) {
+      ob = bottomFooter.getBoundingClientRect()
+    }
+    // 页面高度预留
+    const pageDom = document.querySelector('.article-page-container')
+    pageDom.style.paddingBottom =
+      parseInt(32 + stickyFooter.offsetHeight) + 'px'
+    // bottom出现在视口
+    var bottom = window.innerHeight - ob.top
+    if (bottom > 0 && ob.top !== 0) {
+      stickyFooter.style.bottom = parseInt(bottom) + 'px'
+    } else {
+      stickyFooter.style.bottom = 0
+    }
+  } else {
+    const pageDom = document.querySelector('.article-page-container')
+    pageDom.style.paddingBottom = 0
+  }
+}
+onMounted(() => {
+  $(window).on('scroll', windowScrollFn)
+  $(window).on('resize', windowScrollFn)
+})
+
+onBeforeUnmount(() => {
+  $(window).off('scroll', windowScrollFn)
+  $(window).off('resize', windowScrollFn)
+})
+
+function close() {
+  needMoreAdShow.value = false
+  nextTick(() => {
+    windowScrollFn()
+  })
+}
+
+function doShow() {
+  needMoreAdShow.value = true
+  nextTick(() => {
+    windowScrollFn()
+  })
+}
+
+const props = defineProps({
+  code: String
+})
+</script>
+<template>
+  <div
+    class="article-page-footer-container"
+    ref="footerElement"
+    v-show="needMoreAdShow"
+  >
+    <div class="close-action" @click="close">
+      <i class="iconfont icon-close"></i>
+    </div>
+    <adsense :code="code" @show="doShow"></adsense>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+.article-page-footer-container {
+  position: fixed;
+  width: 100%;
+  bottom: 0;
+  left: 0;
+  z-index: 103;
+  ::v-deep {
+    .adsense {
+      margin-top: 0;
+      padding: 0;
+      cursor: pointer;
+      .content {
+        border: none;
+        padding-top: 0;
+      }
+    }
+  }
+  .close-action {
+    position: absolute;
+    right: 0;
+    top: 50%;
+    transform: translateY(-50%);
+    z-index: 102;
+    padding: 20px;
+    color: #fff;
+    cursor: pointer;
+    .icon-close {
+      display: inline-block;
+      background: unset;
+    }
+  }
+}
+</style>

+ 277 - 0
apps/bigmember_pc/src/views/article-content/components/Nps.vue

@@ -0,0 +1,277 @@
+<template>
+  <div
+    id="npsMain"
+    class="npsMain npsPc"
+    v-show="showModule"
+    @mousemove="getIsView"
+  >
+    <div class="gray-div"></div>
+    <div class="nps-content">
+      <div class="nps-head">
+        {{ question }}
+      </div>
+      <div class="nps-main" v-if="!showTextArea">
+        <div class="nps-bot">
+          <span>肯定不会</span>
+          <div
+            class="bran-list"
+            v-for="item in 10"
+            :key="item"
+            @click="onChangeRate(item)"
+          >
+            {{ item }}
+          </div>
+          <span>非常满意</span>
+        </div>
+      </div>
+      <div class="nps-main" v-else>
+        <div class="pcTextArea">
+          <el-input
+            maxlength="200"
+            type="textarea"
+            placeholder="输入内容,200字以内"
+            v-model="params.answer"
+          >
+          </el-input>
+          <span class="show-word-limit">
+            <span class="iptActive">{{ getWordLen }}</span
+            >/200
+          </span>
+        </div>
+        <div class="nps-footer">
+          <button @click="onSubmit" class="nps-submit">提交</button>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import { getNpsData, getSeeNps, collectionNps } from '@/api/modules/nps'
+
+export default {
+  data() {
+    return {
+      showModule: false,
+      showTextArea: false, // 默认显示文本区域
+      question: '你有多大可能将剑鱼标讯推荐给朋友?',
+      params: {
+        score: '',
+        questionId: '',
+        answer: '',
+        platform: 'PC', // 硬编码为PC
+        nid: ''
+      },
+      wordLimit: 200,
+      requestConf: {
+        viewLoaded: false,
+        viewLoading: false
+      }
+    }
+  },
+  computed: {
+    getWordLen() {
+      return this.params.answer.length
+    }
+  },
+  created() {
+    this.getNpsData()
+  },
+  methods: {
+    getNpsData() {
+      getNpsData().then((res) => {
+        if (res && res.data) {
+          this.showModule = res.data.isShowNps
+        }
+      })
+    },
+    // emoji表情转为字符
+    utf16toEntities(str) {
+      const patt = /[\ud800-\udbff][\udc00-\udfff]/g // 检测utf16字符正则
+      str = str.replace(patt, function (char) {
+        let H, L, code
+        if (char.length === 2) {
+          H = char.charCodeAt(0) // 取出高位
+          L = char.charCodeAt(1) // 取出低位
+          code = (H - 0xd800) * 0x400 + 0x10000 + L - 0xdc00 // 转换算法
+          return '&#' + code + ';'
+        } else {
+          return char
+        }
+      })
+      return str
+    },
+    // 是否查看过(是否进入视口)
+    getIsView() {
+      const { npsid } = this.params
+      const { viewLoaded, viewLoading } = this.requestConf
+      if (!this.showModule) {
+        return
+      }
+      if (npsid || viewLoaded || viewLoading) {
+        return
+      }
+      this.requestConf.viewLoading = true
+      getSeeNps({ platform: this.params.platform })
+        .then((res) => {
+          if (res && res.data) {
+            this.params.npsid = res.data.npsid
+          }
+        })
+        .finally(() => {
+          this.requestConf.viewLoaded = true
+          this.requestConf.viewLoading = false
+        })
+    },
+    // 评分
+    onChangeRate(data) {
+      this.params.score = data
+      this.confirmRate()
+    },
+    onSubmit() {
+      this.params.answer = this.utf16toEntities(this.params.answer)
+      this.confirmRate()
+    },
+    confirmRate() {
+      collectionNps(this.params).then((res) => {
+        if (res && res.data) {
+          if (!this.showTextArea) {
+            this.params.nid = res.data.nid
+            this.params.questionId = res.data.question.id
+            this.params.mold = res.data.question.mold
+            this.question = res.data.question.question
+            this.showTextArea = true
+          } else {
+            this.showModule = false
+            this.$toast('感谢你的反馈~')
+          }
+        }
+      })
+    }
+  }
+}
+</script>
+
+<style scoped>
+/* pc端样式 */
+#npsMain.npsPc {
+  margin: 0 auto;
+}
+#npsMain.npsPc .nps-content {
+  height: auto;
+  padding: 32px 0;
+  background: url(~@/assets/images/nps_bg.png) no-repeat;
+  background-size: 100% 100%;
+}
+#npsMain.npsPc .gray-div {
+  width: 100%;
+  height: 16px;
+  background: #f5f6f7;
+}
+
+#npsMain.npsPc .nps-head {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  font-size: 16px;
+  font-weight: 500;
+  color: #1d1d1d;
+  line-height: 24px;
+}
+
+#npsMain.npsPc .nps-main {
+  margin-top: 16px;
+}
+
+#npsMain.npsPc .nps-main .pcTextArea {
+  position: relative;
+  margin: 0 auto;
+  width: 590px;
+  height: 72px;
+}
+#npsMain.npsPc .nps-main .pcTextArea .el-textarea,
+#npsMain.npsPc .nps-main .pcTextArea .el-textarea__inner {
+  height: 100% !important;
+}
+#npsMain.npsPc .nps-top {
+  display: flex;
+  justify-content: space-between;
+  font-size: 12px;
+  font-weight: 500;
+  color: #9b9ca3;
+  line-height: 18px;
+}
+#npsMain.npsPc .nps-bot {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+}
+#npsMain.npsPc .nps-bot > span {
+  font-size: 14px;
+  font-weight: 500;
+  color: #686868;
+}
+#npsMain.npsPc .bran-list {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  margin: 0 4px;
+  width: 32px;
+  height: 32px;
+  background: #ffffff;
+  border-radius: 2px;
+  opacity: 1;
+  border: 1px solid #ececec;
+  font-size: 14px;
+  font-weight: 400;
+  color: #1d1d1d;
+}
+
+#npsMain.npsPc .bran-list:active,
+#npsMain.npsPc .bran-list:hover {
+  cursor: pointer;
+  background: #2abed1;
+  color: #fff;
+}
+
+#npsMain.npsPc .nps-content .van-cell.van-field {
+  border-radius: 4px;
+  border: 1px solid rgba(0, 0, 0, 0.1);
+}
+#npsMain.npsPc .nps-content .van-field__word-num {
+  color: #2abed1;
+}
+
+#npsMain.npsPc .nps-content .nps-footer {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  margin-top: 20px;
+  width: 100%;
+}
+#npsMain.npsPc .nps-content .nps-submit {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  width: 132px;
+  height: 36px;
+  font-size: 16px;
+  font-weight: 500;
+  color: #f7f9fa;
+  background: #2abed1;
+  border-radius: 6px;
+  border: none;
+}
+#npsMain.npsPc .nps-content .show-word-limit {
+  position: absolute;
+  right: 16px;
+  bottom: 6px;
+  font-size: 12px;
+  line-height: 18px;
+  color: #999999;
+}
+
+#npsMain.npsPc .nps-content .show-word-limit .iptActive {
+  color: #2cb7ca;
+}
+</style>

+ 330 - 0
apps/bigmember_pc/src/views/article-content/components/OriginLink.vue

@@ -0,0 +1,330 @@
+<template>
+  <div class="origin-link-module">
+    <div class="dialog-container" id="dialog-container">
+      <el-dialog
+        title="暂无更多查看原文链接权限"
+        custom-class="confirmed-dialog"
+        :visible.sync="dialog.originalMax"
+        :show-close="false"
+        top="25vh"
+        width="380"
+        center
+      >
+        <p>如需开通更多权限获得商机,请联系客服。</p>
+        <div slot="footer" class="dialog-footer">
+          <button class="dialog-button confirm" @click="concatKf">
+            联系客服
+          </button>
+          <button
+            class="dialog-button plain"
+            @click="dialog.originalMax = false"
+          >
+            取消
+          </button>
+        </div>
+      </el-dialog>
+      <el-dialog
+        title="提交成功"
+        custom-class="confirmed-dialog"
+        :visible.sync="dialog.originalSubmitSuccess"
+        :show-close="false"
+        top="25vh"
+        width="380"
+        center
+      >
+        <p style="text-align: left">
+          恭喜您获得<span class="highlight-text">1次</span
+          >免费查看原文链接的机会,如需查看更多请联系客服:<span
+            class="highlight-text"
+            >400-108-6670</span
+          >。
+        </p>
+        <div slot="footer" class="dialog-footer">
+          <div class="left-groups">
+            <button class="dialog-button confirm" @click="goToOriginalLink">
+              查看原文链接
+            </button>
+            <p class="left-tip">(本次查看消耗1次机会)</p>
+          </div>
+          <button
+            class="dialog-button plain"
+            @click="dialog.originalSubmitSuccess = false"
+          >
+            返回
+          </button>
+        </div>
+      </el-dialog>
+      <el-dialog
+        title="查看原文链接"
+        custom-class="confirmed-dialog"
+        :visible.sync="dialog.originalDeductConfirm"
+        :show-close="false"
+        top="25vh"
+        width="380"
+        center
+      >
+        <p>
+          确定消耗<span class="highlight-text">1次</span>查看原文链接的机会吗?
+        </p>
+        <div slot="footer" class="footer-container">
+          <div class="dialog-footer">
+            <button class="dialog-button confirm" @click="goToOriginalLink">
+              确定
+            </button>
+            <button
+              class="dialog-button plain"
+              @click="dialog.originalDeductConfirm = false"
+            >
+              返回
+            </button>
+          </div>
+          <div class="dialog-footer-tip-container">
+            您当前是免费用户,有1次查看原文链接的机会,如需更多查看次数,您可点击
+            <span class="highlight-text pointer" @click="deductTipAction"
+              >升级大会员></span
+            >
+          </div>
+        </div>
+      </el-dialog>
+    </div>
+  </div>
+</template>
+<script>
+import { mapState, mapGetters } from 'vuex'
+export default {
+  name: 'OriginLink',
+  props: {
+    id: {
+      type: String,
+      required: true,
+      default: ''
+    }
+  },
+  data() {
+    return {
+      dialog: {
+        // 查看原文付费用户上限
+        originalMax: false,
+        // 留资成功,获得一次机会
+        originalSubmitSuccess: false,
+        // 原文链接扣除二次确认
+        originalDeductConfirm: false
+      }
+    }
+  },
+  computed: {
+    ...mapGetters('user', ['isFree']),
+    isEntService() {
+      return location.pathname.indexOf('entservice') !== -1
+    }
+  },
+  methods: {
+    concatKf: function () {
+      this.dialog.originalMax = false
+      try {
+        if (window.goTemplateData.inIframe) {
+          window.$BRACE.$emit('open-customer')
+        } else {
+          // 打开客服弹窗
+          window.checkCustomerService()
+        }
+      } catch (e) {
+        console.warn(e)
+      }
+    },
+    closeDialog: function () {
+      this.dialog.originalSubmitSuccess = false
+      this.dialog.originalDeductConfirm = false
+    },
+    goToOriginalLink: function () {
+      this.closeDialog()
+      this.doConfirmLinkAction()
+    },
+    deductTipAction: function () {
+      this.closeDialog()
+      this.leaveInfoMore2()
+    },
+    // 原有逻辑
+    openOriginLink: function (url) {
+      const originalUrl = this.calcOriginUrl(url)
+      window.open(originalUrl)
+    },
+    calcOriginUrl: function (url) {
+      //获取原文百度统计跳转
+      let originalhref
+
+      if (url) {
+        var originalUrl = url.replace('http://https://', 'https://')
+        if (window.location.href.indexOf('mailprivate') > 0) {
+          originalhref =
+            '/front/transfer?url=' + encodeURIComponent(originalUrl)
+        } else {
+          var link =
+            document.location.protocol +
+            '//' +
+            window.location.host +
+            '/front/transfer?url=' +
+            encodeURIComponent(originalUrl)
+          originalhref = link
+        }
+      }
+      return originalhref
+    },
+    getOriginalText: function (p) {
+      // p.use
+      // p.success
+      // p.error
+      // p.complete
+      var params = {
+        id: this.id
+      }
+      if (p.use) {
+        params.use = p.use
+      }
+      $.ajax({
+        type: 'POST',
+        contentType: 'application/json',
+        data: JSON.stringify(params),
+        url: '/publicapply/userbase/getOriginalText',
+        success: function (res) {
+          p && p.success && p.success(res)
+        },
+        error: function () {
+          p && p.error && p.error()
+        },
+        complete: function () {
+          p && p.complete && p.complete()
+        }
+      })
+    },
+    doConfirmLinkAction: function () {
+      const _this = this
+      this.getOriginalText({
+        use: true,
+        success: function (r) {
+          if (r && r.data && r.data.url) {
+            _this.openOriginLink(r.data.url)
+          } else {
+            _this.$toast('获取原文链接失败')
+          }
+        }
+      })
+    },
+    leaveInfoOne: function () {
+      this.dialogTitleTop = '请完善个人信息'
+      this.dialogTitle =
+        '即刻获得<span class="highlight-text">1次</span>免费查看原文链接的机会,如需查看更多请联系客服:<span class="highlight-text">400-108-6670</span>'
+      this.isNeedSubmit('pc_article_original_one')
+    },
+    // 免费用户查看原文链接权限次数余额为0
+    leaveInfoMore: function () {
+      this.dialogTitleTop = '申请更多查看原文链接权限'
+      this.dialogTitle =
+        '<p style="text-align:left">查看原文链接次数已用完,请填写以下信息升级大会员获得更多查看原文链接权限,同时可查看超前商机、联系人电话,85%用户已升级!</p>'
+      this.isNeedSubmit('pc_article_original_more')
+    },
+    // 免费用户查看原文链接权限次数余额>0
+    leaveInfoMore2: function () {
+      this.dialogTitleTop = '申请更多查看原文链接权限'
+      this.dialogTitle =
+        '请填写以下信息升级大会员获得更多查看原文链接权限,同时可查看超前商机、联系人电话,85%用户已升级!'
+      this.isNeedSubmit('pc_article_original_more')
+    },
+    doGetLinkAction: function () {
+      var _this = this
+      this.getOriginalText({
+        success: function (r) {
+          if (r && r.data) {
+            const status = r.data.status
+            // entService企业级用户
+            if (r.data.url || _this.isEntService) {
+              _this.openOriginLink(r.data.url)
+            } else if (status === 1) {
+              // 免费用户,未留资,需要留资  留资提示即刻获得一次查看原文机会
+              _this.leaveInfoOne()
+            } else if (status === 2) {
+              // 免费用户,不需要留资(余额>0)  弹框提示用户确定消耗一次查看原网站的机会   用户如果选择确认,再次调用该接口 `use` 传true
+              _this.dialog.originalDeductConfirm = true
+            } else if (status === 3) {
+              // 需要留资  留资提示已经消耗过查看原网站链接的机会、升级为大会员
+              _this.leaveInfoMore()
+            } else if (status === 4) {
+              // 付费用户次数达上限,提示暂无更多查看原文权限,联系客服
+              _this.dialog.originalMax = true
+            } else {
+              _this.$toast('获取原文链接失败: 未定义状态')
+            }
+          } else {
+            _this.$toast('获取原文链接失败')
+          }
+        }
+      })
+    },
+    isNeedSubmit(key) {
+      console.log(key)
+      this.$emit('click-collect', key)
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.origin-link-module {
+  ::v-deep {
+    .confirmed-dialog.el-dialog {
+      max-width: 380px;
+      border-radius: 8px;
+    }
+    .confirmed-dialog.el-dialog .el-dialog__header {
+      padding: 32px 32px 0;
+    }
+    .confirmed-dialog.el-dialog .el-dialog__body {
+      padding: 20px 32px 32px;
+      text-align: center;
+      line-height: 22px;
+    }
+    .confirmed-dialog.el-dialog .el-dialog__footer {
+      padding: 0 32px 32px;
+    }
+    .confirmed-dialog.el-dialog .el-dialog__title {
+      color: #1d1d1d;
+      font-size: 18px;
+      line-height: 28px;
+    }
+    .confirmed-dialog.el-dialog .dialog-footer {
+      display: flex;
+      justify-content: space-between;
+    }
+    .dialog-button {
+      width: 132px;
+      height: 36px;
+      font-size: 16px;
+      line-height: 24px;
+      border-radius: 6px;
+    }
+    .dialog-button.confirm {
+      color: #fff;
+      background-color: #2abed1;
+      border: 1px solid #2abed1;
+    }
+    .dialog-button.plain {
+      color: #686868;
+      background-color: transparent;
+      border: 1px solid #e0e0e0;
+    }
+
+    .confirmed-dialog.el-dialog .dialog-footer .left-tip {
+      margin-top: 4px;
+      color: #ff9f40;
+      font-size: 12px;
+    }
+    .confirmed-dialog.el-dialog .dialog-footer-tip-container {
+      margin-top: 12px;
+      color: #686868;
+      font-size: 14px;
+      line-height: 22px;
+      text-align: left;
+    }
+  }
+}
+</style>

+ 467 - 0
apps/bigmember_pc/src/views/article-content/components/RecommendCustomers.vue

@@ -0,0 +1,467 @@
+<script setup>
+import QuickMonitor from '@/composables/quick-monitor/component/QuickMonitor.vue'
+import { getStatusCustomer, setRemoveCustomer } from '@/api/modules'
+import { computed, getCurrentInstance, ref } from 'vue'
+import { ContentExpandsModel } from '@/views/article-content/composables/useContentStore'
+import { getAssetsFile, moneyUnit } from '@/utils'
+import CollectInfo from '@/components/collect-info/CollectInfo.vue'
+import MaskCard from '@/components/mask-card/MaskCard.vue'
+import { doOpenBuyerPage } from '@/views/article-content/composables/useArticleUtil'
+
+const that = getCurrentInstance().proxy
+
+const canShowMask = computed(() => {
+  return !ContentExpandsModel.value.recommendCustomers?.isPower
+})
+const customerList = computed(() => {
+  const result = ContentExpandsModel.value.recommendCustomers?.list || []
+  return result.map((v) => {
+    const item = {
+      Buyer: v?.buyer,
+      Budget: v?.totalAmount ? moneyUnit(v.totalAmount) : '',
+      PNCount: v?.projectCount,
+      WCity: v?.city,
+      WProvince: v?.area,
+      entId: ''
+    }
+    return item
+  })
+})
+
+const getListData = computed(() => {
+  const result = customerList.value
+  if (result.length > 0) {
+    getListStatus(result)
+  } else if (!canShowMask.value) {
+    that.$emit('doHide')
+  }
+  return result
+})
+
+const globalShareCountModel = ref({
+  used: 0,
+  surplus: 0
+})
+const globalShareModel = ref({
+  loading: false,
+  follow: {},
+  remove: {}
+})
+
+// 获取监控数量
+async function getListStatus(list) {
+  if (globalShareModel.value.loading) return
+  const nameList = list?.map((v) => v.Buyer)
+  if (!list) return
+  globalShareModel.value.loading = true
+  const { error_code: code, data } = await getStatusCustomer({
+    name_list: nameList
+  })
+  if (code === 0) {
+    globalShareCountModel.value.used = data.count.use
+    globalShareCountModel.value.surplus = data.count.surplus
+    globalShareModel.value.follow = data.follow
+
+    // 处理列表
+    quickMonitorElement.value.forEach((v, i) => {
+      v.model.canFollow = true
+      v.model.follow = globalShareModel.value.follow[v.getParams()]
+      v.model.expands = globalShareCountModel.value
+    })
+  }
+}
+
+// 弹窗
+function getImgForVipUpgrade(name, bg = false, suffix = '.png') {
+  return getAssetsFile('vip/' + (bg ? 'bg/mask/' : '') + name + suffix)
+}
+const entContactTip = {
+  title: '开通大会员',
+  subtitle:
+    '根据区域、业务范围、客户类型帮助企业快速找到目标地区<br>潜在业务需求客户及联系方式。',
+  button: '免费体验',
+  source: 'pc_article_CustomerRecommend',
+  img: getImgForVipUpgrade('qzkh'),
+  bg: getImgForVipUpgrade('qzkh', true)
+}
+function doClickMore() {
+  window.open('/big/page/index', '_blank')
+}
+
+// 不是我的客户
+function changeDelete(item) {
+  // 不是我的客户
+  setRemoveCustomer({
+    name: item.Buyer,
+    province: item.WProvince,
+    city: item.WCity
+  }).then((res) => {
+    if (res && res.error_code === 0 && res.data && res.data) {
+      globalShareModel.value.remove[item.Buyer] = true
+      if (
+        Object.keys(globalShareModel.value.remove).length ===
+        getListData.value.length
+      ) {
+        that.$emit('doHide')
+      }
+      that.$forceUpdate()
+      that.$toast('“不是我的客户”操作成功')
+    }
+  })
+}
+
+// 打开留资弹窗
+const collectElement = ref(null)
+function doOpenCollectDialog(key) {
+  collectElement.value?.noCallApiFn(key, false)
+}
+
+// 列表兼容
+const quickMonitorElement = ref(null)
+</script>
+<template>
+  <el-card class="recommend-customer-list-card" shadow="never">
+    <div class="info-list">
+      <div
+        v-for="(item, index) in getListData"
+        :key="index"
+        class="flex-r-c center sb card-list-item"
+        :class="{ 'is-remove': globalShareModel.remove[item.Buyer] }"
+      >
+        <div
+          class="flex-c-c left cursor-pointer"
+          @click="doOpenBuyerPage({ name: item.Buyer })"
+        >
+          <div class="flex-r-c center sb">
+            <div class="flex-r-c center">
+              <i class="el-icon-jy-icon-company"></i>
+              <span class="text--title">{{ item.Buyer }}</span>
+            </div>
+          </div>
+          <div class="flex-r-c card-bottom-info">
+            <span class="text--sm-name">项目数量:</span>
+            <span class="text--sm-value">{{ item.PNCount }}</span>
+            <span class="text--sm-name">项目总金额:</span>
+            <span class="text--sm-value">{{ item.Budget }}</span>
+            <span class="text--sm-name">所在地:</span>
+            <span class="text--sm-value" v-if="item.WProvince || item.WCity"
+              >{{ item.WProvince }}&nbsp;&nbsp;{{
+                item.WProvince.slice(0, 2) !== item.WCity.slice(0, 2)
+                  ? item.WCity
+                  : ''
+              }}</span
+            >
+            <span class="text--sm-value" v-else>-</span>
+          </div>
+        </div>
+        <div class="pcor-right-group flex-c-c right">
+          <quick-monitor
+            ref="quickMonitorElement"
+            type="client"
+            :params="item.Buyer"
+            :auto="false"
+          ></quick-monitor>
+          <span class="m-t-6px cursor-pointer" @click.stop="changeDelete(item)"
+            >不是我的客户</span
+          >
+        </div>
+      </div>
+    </div>
+
+    <!-- 留资 -->
+    <collect-info ref="collectElement"></collect-info>
+    <MaskCard
+      v-if="canShowMask"
+      class="contact-mask-card"
+      :islogin="true"
+      @click="doOpenCollectDialog(entContactTip.source)"
+      :item="entContactTip"
+    >
+      <div
+        slot="module-top"
+        class="contact-mask-card-tip flex flex-(row items-center)"
+      >
+        <span class="title-text">- 大会员专家版权益 -</span>
+        <div class="more-group flex-r-c flex-items-center" @click="doClickMore">
+          <span class="title-text more-text">查看服务介绍</span>
+          <i class="iconfont icon-more title-text"></i>
+        </div>
+      </div>
+    </MaskCard>
+  </el-card>
+</template>
+
+<style lang="scss" scoped>
+.recommend-customer-list-card {
+  @include diy-icon('edit', 20, 20);
+  @include diy-icon('icon-company', 24, 24);
+  @include diy-icon('heart_stroke', 18, 18);
+  @include diy-icon('heart_solid', 18, 18);
+  ::v-deep .el-icon-jy-icon-company {
+    margin-right: 8px;
+  }
+  // card样式重置
+  ::v-deep {
+    .el-card__header {
+      margin: 0 40px;
+      padding-left: 0;
+      padding-right: 0;
+    }
+    .el-card__body {
+      padding: 0;
+    }
+    .el-dialog__header {
+      padding: 0;
+    }
+    .el-dialog__body {
+      padding: 0;
+    }
+    .empty-container {
+      margin-top: 60px;
+    }
+    .get-more {
+      display: flex;
+      .el-icon-arrow-right {
+        margin-left: 4px;
+        order: 2;
+      }
+    }
+  }
+  ::v-deep {
+    .monitor-class {
+      padding: 32px;
+      .el-dialog__header {
+        padding: 0;
+      }
+      .el-dialog__body {
+        padding: 20px 0 32px;
+      }
+      .el-dialog__footer {
+        padding: 0;
+      }
+    }
+  }
+  .sub-manager {
+    display: flex;
+    align-items: center;
+    padding: 8px 16px;
+    font-size: 14px;
+    line-height: 24px;
+    color: #1d1d1d;
+    border-color: #e0e0e0;
+    &.el-button:focus,
+    &.el-button:hover {
+      color: inherit;
+      background-color: inherit;
+    }
+  }
+}
+
+.recommend-customer-list-card {
+  border: none;
+  overflow: initial;
+
+  .header-box {
+    padding-left: 40px;
+    padding-right: 40px;
+  }
+
+  .card-title {
+    color: #1d1d1d;
+    line-height: 28px;
+    color: #1d1d1d;
+  }
+
+  .pcor-right-group {
+    > span {
+      font-size: 12px;
+      line-height: 23px;
+      color: #aaaaaa;
+
+      &:hover {
+        color: #2cb7ca;
+      }
+    }
+
+    i + span {
+      margin-left: 4px;
+      font-size: 14px;
+      line-height: 22px;
+      color: #686868;
+    }
+  }
+
+  .link-text {
+    font-size: 14px;
+    line-height: 24px;
+    text-decoration-line: underline;
+    color: #1d1d1d;
+    cursor: pointer;
+
+    & + .link-text {
+      margin-left: 20px;
+    }
+
+    &:hover,
+    &.active {
+      color: #2cb7ca;
+    }
+  }
+
+  .text-- {
+    &sm-value {
+      color: #1d1d1d;
+    }
+
+    &title {
+      font-size: 16px;
+      line-height: 24px;
+      color: #1d1d1d;
+    }
+
+    &sm-time {
+      font-size: 12px;
+      line-height: 20px;
+      color: #999999;
+    }
+  }
+
+  .card-list-item {
+    padding: 16px 0;
+    box-sizing: border-box;
+    box-shadow: inset 0px -1px 0px rgba(0, 0, 0, 0.05);
+
+    &:last-of-type {
+      box-shadow: unset;
+    }
+
+    &:hover {
+      .text--title,
+      .text--sm-value {
+        color: #2cb7ca;
+      }
+    }
+
+    &.is-remove {
+      display: none;
+    }
+  }
+
+  .card-bottom-info {
+    margin-top: 12px;
+    justify-content: flex-start;
+    text-align: left;
+    font-size: 14px;
+    line-height: 22px;
+    color: #999999;
+
+    .text--sm-value {
+      margin-right: 40px;
+
+      &:last-child {
+        margin-right: 0;
+      }
+    }
+  }
+
+  .sub-manager {
+    float: right;
+  }
+
+  .info-list {
+    border-top: 1px solid transparent;
+    position: relative;
+  }
+
+  .add-key-button {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    margin-top: 32px;
+    padding: 8px 16px;
+    color: #f7f9fa;
+    border-radius: 6px;
+    background-color: #2abed1;
+    cursor: pointer;
+
+    .icon-chahao {
+      margin-right: 4px;
+      transform: rotate(-45deg);
+    }
+
+    .button-text {
+      margin-left: 4px;
+      white-space: nowrap;
+    }
+  }
+
+  .icon-chahao {
+    position: relative;
+    display: inline-block;
+    width: 14px;
+    height: 14px;
+
+    &:before,
+    &:after {
+      position: absolute;
+      content: '' !important;
+      background-color: #fff;
+      top: 50%;
+      left: 50%;
+      width: 14px;
+      height: 2px;
+      border-radius: 2px;
+    }
+
+    &:before {
+      transform: translate(-50%, -50%) rotate(45deg);
+    }
+
+    &:after {
+      transform: translate(-50%, -50%) rotate(-45deg);
+    }
+  }
+
+  .el-pagination-container {
+    margin-right: 40px;
+  }
+
+  ::v-deep {
+    .upgrade-mask-group .module-img-card {
+      padding: 50px 24px 0px 24px;
+    }
+
+    .upgrade-mask-group .top-tip-card {
+      height: auto;
+      padding-top: 16px;
+      padding-bottom: 32px;
+    }
+  }
+
+  .contact-mask-card-tip {
+    position: absolute;
+    width: 100%;
+    top: 0;
+    left: 0;
+    height: 50px;
+    justify-content: center;
+    .title-text,
+    .more-text {
+      font-size: 14px;
+      background: linear-gradient(270deg, #f1d090 0%, #fae7ca 100%);
+      background-clip: text;
+      -webkit-background-clip: text;
+      -webkit-text-fill-color: transparent;
+    }
+    .title-text.more-text {
+      font-size: 12px;
+    }
+    .more-group {
+      position: absolute;
+      height: 50px;
+      right: 24px;
+      cursor: pointer;
+    }
+  }
+}
+</style>

+ 266 - 0
apps/bigmember_pc/src/views/article-content/components/RecommendEnt.vue

@@ -0,0 +1,266 @@
+<script setup>
+import { SummaryModel } from '@/views/article-content/composables/useContentStore'
+import QuickMonitor from '@/composables/quick-monitor/component/QuickMonitor.vue'
+import { computed, ref, watch } from 'vue'
+import { ajaxGetMiniEntInfo } from '@/api/modules/detail'
+import { formatMoney } from '@/utils'
+import {
+  doOpenBuyerPage,
+  doOpenWinnerPage
+} from '@/views/article-content/composables/useArticleUtil'
+
+const recommendList = ref([])
+function getRecommendEntInfo(list) {
+  const resultList = []
+  for (let i = 0; i < list.length; i++) {
+    const item = list[i]
+    const type = item?.type
+    const key = item?.id || item?.name || ''
+    ajaxGetMiniEntInfo(type, key).then((res) => {
+      if (res.error_code === 0) {
+        const resItem = res.data
+        resultList[i] = {
+          type,
+          _key: type + key + Date.now(),
+          id: resItem?.winnerId || '',
+          name: resItem.companyName || item?.name || '',
+          time: resItem.timeFrame,
+          link: item.link,
+          monitor: {
+            type: type === 'buyer' ? 'client' : 'ent',
+            params: item.type === 'buyer' ? item.name : item.id
+          },
+          tags: [
+            {
+              value: resItem.contactCount || '-',
+              unit: '个',
+              label: '联系人'
+            },
+            {
+              value: resItem.biddingCount || '-',
+              unit: '条',
+              label: type === 'buyer' ? '招标动态' : '中标动态'
+            },
+            {
+              value: resItem.projectCount || '-',
+              unit: '个',
+              label: type === 'buyer' ? '采购项目数量' : '中标项目数量'
+            },
+            {
+              value:
+                formatMoney(resItem.bidamountCount, { type: 'number' }) || '-',
+              unit: formatMoney(resItem.bidamountCount, { type: 'unit' }) || '',
+              label: type === 'buyer' ? '采购规模' : '中标项目金额'
+            },
+            {
+              value: resItem.cooperate || '-',
+              unit: '个',
+              label: type === 'buyer' ? '合作企业' : '合作客户'
+            }
+          ],
+          origin: res.data
+        }
+        recommendList.value = [].concat(resultList)
+      }
+    })
+  }
+}
+
+const list = computed(() => {
+  const result = []
+    .concat(SummaryModel.value.buyers, SummaryModel.value.winners.slice(0, 3))
+    .filter((v) => v.link)
+  return result
+})
+
+getRecommendEntInfo(list.value)
+
+const recommendEntList = computed(() => {
+  return recommendList.value.filter((v) => {
+    const hasTwoTag = v?.tags?.filter((v) => v.value !== '-').length >= 2
+    return v.name && hasTwoTag
+  })
+})
+
+function doOpen(item) {
+  console.log(item)
+  if (item.type === 'winner') {
+    doOpenWinnerPage(item)
+  } else {
+    doOpenBuyerPage(item)
+  }
+}
+</script>
+
+<template>
+  <div v-if="recommendEntList.length">
+    <div
+      class="recommend-info-card"
+      v-for="(item, index) in recommendEntList"
+      :key="index"
+    >
+      <div class="type-info-header">
+        <div class="type-infos">
+          <span
+            class="type-label"
+            :class="{ 'blue-type': (item && item.type) !== 'buyer' }"
+          >
+            {{ item?.type === 'buyer' ? '采购单位' : '中标单位' }}画像
+          </span>
+          <h3 class="ellipsis" style="max-width: 25em">{{ item.name }}</h3>
+          <div
+            class="monitor-action"
+            v-if="item._key && item.monitor && item.monitor.params"
+          >
+            <quick-monitor
+              class="action-item"
+              :key="item._key"
+              :cache="true"
+              :type="item.monitor.type"
+              :params="item.monitor.params"
+            ></quick-monitor>
+          </div>
+        </div>
+        <span class="type-info-time">数据统计范围:{{ item.time }}</span>
+      </div>
+      <div class="content-number-info">
+        <div class="number-infos">
+          <div class="number-info-item" v-for="(tag, i) in item.tags" :key="i">
+            <div class="highlight-number">
+              <span class="highlight-number-label">{{ tag.value }}</span>
+              <span v-if="tag.value !== '-'">{{ tag.unit }}</span>
+            </div>
+            <span>{{ tag.label }}</span>
+          </div>
+        </div>
+        <el-button
+          class="detail-action"
+          type="primary"
+          @click="doOpen(item)"
+          v-if="item.link"
+        >
+          查看详情
+        </el-button>
+      </div>
+    </div>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+.recommend-info-card {
+  margin-top: 16px;
+  border-radius: 8px;
+  background: #f7f9fc;
+  padding: 16px 20px;
+  .type-info-header {
+    display: flex;
+    flex-direction: row;
+    align-items: flex-end;
+    justify-content: space-between;
+  }
+  .type-info-time {
+    color: #999;
+    text-align: right;
+    font-size: 12px;
+    font-style: normal;
+    font-weight: 400;
+    line-height: 18px;
+  }
+
+  .content-number-info {
+    display: flex;
+    flex-direction: row;
+    align-items: center;
+    justify-content: space-between;
+    font-size: 14px;
+    line-height: 22px;
+    color: #686868;
+
+    .detail-action {
+      width: 160px;
+      height: 46px;
+      font-size: 16px;
+    }
+
+    .number-infos {
+      display: flex;
+      flex-direction: row;
+      align-items: center;
+      height: 78px;
+    }
+    .number-info-item {
+      position: relative;
+      min-width: 136px;
+      display: flex;
+      flex-direction: column;
+      align-items: center;
+      &:last-child::after {
+        content: unset;
+      }
+      &::after {
+        content: '';
+        position: absolute;
+        right: 0;
+        top: 4px;
+        display: inline-block;
+        width: 1px;
+        height: 46px;
+        background: #ececec;
+      }
+    }
+    .highlight-number {
+      color: #2abed1;
+    }
+    .highlight-number-label {
+      font-size: 20px;
+      line-height: 32px;
+    }
+  }
+
+  .type-infos {
+    display: flex;
+    flex-direction: row;
+    align-items: center;
+
+    .type-label {
+      margin-right: 16px;
+      border-radius: 4px;
+      border: 1px solid #ff9f40;
+      padding: 1px 10px;
+      background: rgba(255, 159, 64, 0.1);
+      color: #ff9f40;
+      text-align: center;
+      font-size: 14px;
+      font-style: normal;
+      font-weight: 400;
+      line-height: 22px;
+      &.blue-type {
+        border-color: #05a5f2;
+        background: rgba(5, 166, 243, 0.1);
+        color: #05a5f2;
+      }
+    }
+    h3 {
+      color: #1d1d1d;
+      font-size: 16px;
+      font-style: normal;
+      font-weight: 400;
+      line-height: normal;
+    }
+    .monitor-action {
+      margin-left: 12px;
+      color: #1d1d1d;
+      font-size: 13px;
+      font-style: normal;
+      font-weight: 400;
+      line-height: 20px;
+      cursor: pointer;
+      .iconfont {
+        margin-right: 2px;
+        font-size: 18px;
+        color: #9b9ca3;
+      }
+    }
+  }
+}
+</style>

+ 445 - 0
apps/bigmember_pc/src/views/article-content/components/RecommendOpportunities.vue

@@ -0,0 +1,445 @@
+<script setup>
+import { computed, ref } from 'vue'
+import QuickMonitor from '@/composables/quick-monitor/component/QuickMonitor.vue'
+import { ContentExpandsModel } from '@/views/article-content/composables/useContentStore'
+import {
+  doOpenArticlePage,
+  doOpenBuyerPage,
+  doOpenWinnerPage
+} from '@/views/article-content/composables/useArticleUtil'
+import ArticleItem from '@/components/article-item/ArticleItem.vue'
+import { dateFromNow } from '@/utils'
+import CollectInfo from '@/components/collect-info/CollectInfo.vue'
+import RecommendServesCard from '@/views/article-content/components/RecommendServesCard.vue'
+import { ajaxSetSearchFilterForSession } from '@/api/modules/detail'
+
+function dateFormat(str) {
+  if (str) {
+    return dateFromNow(str * 1000)
+  }
+  return ''
+}
+
+/**
+ * 统一数据结构
+ * @param modelName
+ * @return {{total, more, list: *, title}}
+ */
+function createRecommendation(modelName) {
+  const model = ContentExpandsModel.value[modelName]
+  const list = model.list?.map((item) => ({
+    ...item,
+    _id: item.id,
+    publishTime: item._o?.publishTime,
+    matchKeys: []
+  }))
+  return {
+    list,
+    name: model.name || '',
+    title: model.title,
+    total: model.total,
+    more: model.more
+  }
+}
+
+const recommendProjects = computed(() => {
+  const result = createRecommendation('recommendProjects')
+  if (canShowProjectMask.value) {
+    result.list = new Array(5).fill(1).map((v) => result.list[0])
+  }
+  return result
+})
+
+const recommendBuyers = computed(() => {
+  return createRecommendation('recommendBuyers')
+})
+
+const recommendWinners = computed(() => {
+  let result = createRecommendation('recommendWinners')
+  result.id = ContentExpandsModel.value.recommendWinners.winnerId
+  return result
+})
+
+const canShowModule = computed(() => {
+  const result =
+    ContentExpandsModel.value.recommendProjects.total > 0 ||
+    ContentExpandsModel.value.recommendBuyers.total > 0 ||
+    ContentExpandsModel.value.recommendWinners.total > 0
+  return result
+})
+
+const recommendInfo = {
+  config: {
+    gray: true,
+    table: false,
+    collect: false
+  }
+}
+
+const canShowProjectMask = computed(() => {
+  return ContentExpandsModel.value.recommendProjects?.popup
+})
+
+function getRecentThreeMonthsTimestamps() {
+  const oneDay = 24 * 60 * 60 * 1000 // 一天的毫秒数
+  const threeMonths = 3 * 30 * oneDay // 三个月的毫秒数(大致估算)
+
+  // 获取当前时间的时间戳
+  const now = new Date().getTime()
+  // 获取三个月前的时间戳
+  const threeMonthsAgo = now - threeMonths
+
+  // 转换为UTC时间的起始秒数
+  const nowUTCStart = Math.floor(now / 1000)
+  const threeMonthsAgoUTCStart = Math.floor(threeMonthsAgo / 1000)
+
+  // 返回格式化的字符串
+  return `${threeMonthsAgoUTCStart}_${nowUTCStart}`
+}
+
+/**
+ * 打开超前项目查看更多
+ * 1.1 判断是否有超前项目权限
+ *  > 是,带入条件进入超前项目搜索
+ *  > 否,留资弹窗
+ */
+async function doOpenProjectMore() {
+  if (canShowProjectMask.value) {
+    doOpenCollectDialog()
+  } else {
+    const searchFilterData =
+      ContentExpandsModel.value.recommendProjects?.search || {}
+    const searchFilter = Object.assign({}, searchFilterData, {
+      selectType: 'content',
+      searchGroup: '2',
+      subtype: '',
+      publishtime: getRecentThreeMonthsTimestamps(),
+      timeslot: getRecentThreeMonthsTimestamps(),
+      regionMap: searchFilterData?.regionMap
+        ? JSON.stringify(searchFilterData.regionMap)
+        : ''
+    })
+    await ajaxSetSearchFilterForSession(searchFilter)
+    const link = location.origin + '/jylab/supsearch/index.html'
+    window.open(
+      '/page_workDesktop/work-bench/page?link=' +
+        encodeURIComponent(link + '?goback&tab=2&searchGroup=2'),
+      '_blank'
+    )
+  }
+}
+
+function doOpenMore(type) {
+  if (type === 'winner') {
+    return doOpenWinnerPage({
+      id: recommendWinners.value.id,
+      query: { active: 4 }
+    })
+  } else {
+    return doOpenBuyerPage({
+      name: recommendBuyers.value.name,
+      query: { active: 2 }
+    })
+  }
+}
+
+// 打开留资弹窗
+const collectElement = ref(null)
+function doOpenCollectDialog() {
+  collectElement.value?.noCallApiFn('pc_article_cqxmmore', false)
+}
+
+const recommendMaskConfig = {
+  headerTitle: '大会员权益',
+  headerMore: '查看服务介绍',
+  clickMore: () => {
+    window.open('/big/page/index', '_blank')
+  },
+  title: '项目提前介入,中标更轻松',
+  content:
+    '提前1-3个月获取项目采购计划,获取采购内容、预算金额、预计采购时间等,提前运作提高中标率。',
+  buttons: [
+    {
+      label: '免费体验',
+      type: 'primary',
+      action: () => {
+        doOpenCollectDialog()
+      }
+    }
+  ]
+}
+</script>
+<template>
+  <div class="recommend-opportunities" v-if="canShowModule">
+    <!--   超前项目     -->
+    <div v-if="recommendProjects.total > 0">
+      <div
+        class="recommend-info-header flex flex-(row items-end justify-between)"
+      >
+        <div class="flex flex-(row items-end justify-between)">
+          <div class="flex flex-(row items-center)">
+            <span class="el-icon-jy-re-info"></span>
+            <span>超前项目推荐</span>
+          </div>
+          <span class="number-text">
+            {{ recommendProjects.total }}
+          </span>
+        </div>
+        <span
+          class="more-text"
+          v-if="recommendProjects.more"
+          @click="doOpenProjectMore"
+        >
+          查看更多
+          <i class="iconfont icon-more"></i>
+        </span>
+      </div>
+
+      <div class="article-info-list">
+        <div>
+          <article-item
+            model="S"
+            :grid-data="[]"
+            class="article-info-item"
+            v-for="(item, index) in recommendProjects.list"
+            :class="{ visited: item.visited || item.ca_isvisit }"
+            :key="index"
+            :index="index + 1"
+            :article="item"
+            :config="recommendInfo.config"
+            @onClick="doOpenArticlePage(item)"
+            :vt="'f'"
+          >
+            <div slot="right-time"></div>
+            <div class="time-text" slot="right-handle-container">
+              {{ dateFormat(item.publishTime) }}
+            </div>
+          </article-item>
+        </div>
+        <div class="mask-drainage" v-if="canShowProjectMask">
+          <recommend-serves-card v-bind="recommendMaskConfig">
+          </recommend-serves-card>
+        </div>
+      </div>
+    </div>
+    <!--   采购单位     -->
+    <div v-if="recommendBuyers.total > 0">
+      <div
+        class="recommend-info-header flex flex-(row items-end justify-between)"
+      >
+        <div class="flex flex-(row items-end justify-between)">
+          <div class="flex flex-(row items-center)">
+            <span class="el-icon-jy-re-unit"></span>
+            <span>{{ recommendBuyers.title }}</span>
+          </div>
+          <span class="number-text">
+            {{ recommendBuyers.total }}
+          </span>
+        </div>
+        <div class="right-quick-more-actions flex flex-(row items-center)">
+          <quick-monitor
+            v-if="recommendBuyers.name"
+            :cache="true"
+            type="client"
+            :params="recommendBuyers.name"
+          />
+          <span
+            class="more-text"
+            v-if="recommendBuyers.more"
+            @click="doOpenMore('buyer')"
+          >
+            查看更多 <i class="iconfont icon-more"></i>
+          </span>
+        </div>
+      </div>
+
+      <div class="article-info-list">
+        <article-item
+          model="S"
+          :grid-data="[]"
+          class="article-info-item"
+          v-for="(item, index) in recommendBuyers.list"
+          :class="{ visited: item.visited || item.ca_isvisit }"
+          :key="index"
+          :index="index + 1"
+          :article="item"
+          :config="recommendInfo.config"
+          @onClick="doOpenArticlePage(item)"
+          :vt="'f'"
+        >
+          <div slot="right-time"></div>
+          <div class="time-text" slot="right-handle-container">
+            {{ dateFormat(item.publishTime) }}
+          </div>
+        </article-item>
+      </div>
+    </div>
+
+    <!--   中标单位     -->
+    <div v-if="recommendWinners.total > 0">
+      <div
+        class="recommend-info-header flex flex-(row items-end justify-between)"
+      >
+        <div class="flex flex-(row items-end justify-between)">
+          <div class="flex flex-(row items-center)">
+            <span class="el-icon-jy-re-unit"></span>
+            <span>{{ recommendWinners.title }}</span>
+          </div>
+          <span class="number-text">
+            {{ recommendWinners.total }}
+          </span>
+        </div>
+        <div class="right-quick-more-actions flex flex-(row items-center)">
+          <quick-monitor
+            v-if="recommendWinners.id"
+            :cache="true"
+            type="ent"
+            :params="recommendWinners.id"
+          />
+          <span
+            class="more-text"
+            v-if="recommendWinners.more"
+            @click="doOpenMore('winner')"
+          >
+            查看更多
+            <i class="iconfont icon-more"></i>
+          </span>
+        </div>
+      </div>
+
+      <div class="article-info-list">
+        <article-item
+          model="S"
+          :grid-data="[]"
+          class="article-info-item"
+          v-for="(item, index) in recommendWinners.list"
+          :class="{ visited: item.visited || item.ca_isvisit }"
+          :key="index"
+          :index="index + 1"
+          :article="item"
+          :config="recommendInfo.config"
+          @onClick="doOpenArticlePage(item)"
+          :vt="'f'"
+        >
+          <div slot="right-time"></div>
+          <div class="time-text" slot="right-handle-container">
+            {{ dateFormat(item.publishTime) }}
+          </div>
+        </article-item>
+      </div>
+    </div>
+    <collect-info ref="collectElement"></collect-info>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+.recommend-opportunities {
+  .article-info-list {
+    position: relative;
+    .article-info-item {
+      display: flex;
+      flex-direction: row;
+      justify-content: space-between;
+      padding: 16px 0;
+      &:hover {
+        color: $color-main;
+      }
+      &:last-of-type {
+        border: none;
+      }
+      ::v-deep {
+        > .flex-center {
+          flex: 1;
+          width: 0;
+        }
+        .content-item {
+          width: calc(100% - 32px);
+        }
+        .tag.orange {
+          order: 2;
+        }
+        .tag.green {
+          order: 1;
+        }
+
+        .tag.tag-ent,
+        .haveFile {
+          display: none;
+        }
+        .time-container {
+          display: none;
+        }
+        .tag:last-child {
+          margin-right: 8px;
+        }
+      }
+
+      .time-text {
+        color: #999;
+        text-align: right;
+        font-size: 12px;
+        line-height: 18px;
+      }
+    }
+    .mask-drainage {
+      position: absolute;
+      top: 56px;
+      width: 100%;
+      height: calc(100% - 56px);
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      background: rgba(255, 255, 255, 0.65);
+      backdrop-filter: blur(3px);
+      .recommend-serve-card {
+        width: 770px;
+        .recommend-serve-content {
+          padding: 24px 96px;
+        }
+      }
+    }
+  }
+  .right-quick-more-actions {
+    color: #686868;
+    font-size: 14px;
+    .more-text {
+      margin-left: 16px;
+    }
+  }
+
+  .recommend-serve-card.has-header {
+    min-height: 205px;
+  }
+
+  .recommend-info-header {
+    @include diy-icon('re-unit', 24, 24);
+    @include diy-icon('re-info', 24, 24);
+    color: #1d1d1d;
+    font-size: 18px;
+    line-height: 28px;
+    margin-top: 16px;
+
+    .el-icon-jy-re-unit,
+    .el-icon-jy-re-info {
+      margin-right: 6px;
+    }
+
+    .number-text {
+      margin-left: 12px;
+      color: #2abed1;
+      font-size: 14px;
+      line-height: 22px;
+    }
+
+    .more-text {
+      display: flex;
+      flex-direction: row;
+      align-items: center;
+      cursor: pointer;
+      color: #686868;
+      font-size: 14px;
+      line-height: normal;
+    }
+  }
+}
+</style>

+ 114 - 0
apps/bigmember_pc/src/views/article-content/components/RecommendServes.vue

@@ -0,0 +1,114 @@
+<script setup>
+import { computed } from 'vue'
+import RecommendServesCard from '@/views/article-content/components/RecommendServesCard.vue'
+import { ContentExpandsModel } from '@/views/article-content/composables/useContentStore'
+
+const emit = defineEmits(['click-view-origin', 'open-collect'])
+
+const recommendServeList = computed(() => {
+  const list = ContentExpandsModel.value.services?.map((v) => {
+    return {
+      headerTitle: v?.header?.tip || '',
+      headerMore: v?.header?.more?.label || '',
+      clickMore: () => {
+        const link = v?.header?.more?.url?.pc || ''
+        if (link) {
+          window.open(link, '_blank')
+        }
+      },
+      title: v.title,
+      content: v.content,
+      buttons: v.buttons?.map((btn) => {
+        const btnItem = {
+          ...btn,
+          link: btn?.url?.pc || '',
+          label: btn.name,
+          type: btn.class,
+          action: () => doCommonButtonAction(btnItem)
+        }
+        return btnItem
+      })
+    }
+  })
+  return list
+})
+const isThreeClass = computed(() => {
+  return recommendServeList.value.length % 3 === 0
+})
+
+function doCommonButtonAction(btn) {
+  // 支持的事件定义,link 链接、leave 留资、origin 查看原文
+  let actionType = btn.mold
+  if (btn?.source?.pc && btn?.source?.pc?.indexOf('article_original') !== -1) {
+    actionType = 'origin'
+  }
+
+  switch (actionType) {
+    case 'link': {
+      // 链接
+      if (btn.link) {
+        window.open(btn.link, '_blank')
+      }
+      break
+    }
+    case 'leave': {
+      // 留资
+      const source = btn?.source?.pc || ''
+      if (source) {
+        emit('open-collect', source)
+      }
+      break
+    }
+    case 'origin': {
+      emit('click-view-origin')
+      break
+    }
+  }
+}
+</script>
+<template>
+  <div
+    class="recommend-serves flex flex-(row items-center wrap)"
+    :class="{ 'is-three-class': isThreeClass }"
+  >
+    <recommend-serves-card
+      v-for="(card, index) in recommendServeList"
+      :key="index"
+      v-bind="card"
+    ></recommend-serves-card>
+  </div>
+</template>
+
+<style lang="scss">
+.is-three-class {
+  margin-top: 14px;
+  justify-content: space-between;
+  .recommend-serve-card {
+    width: calc(33.33% - 8px);
+    margin: 0;
+    padding-top: 38px;
+    height: 231px;
+    &.has-header {
+      padding-top: 0;
+    }
+    .recommend-serve-content {
+      width: 100%;
+      padding: 16px 12px 24px 12px;
+    }
+    .action-btns {
+      padding: 0 8px;
+      &.has-only {
+        .el-button {
+          min-width: 132px;
+        }
+      }
+      .el-button {
+        min-width: 118px;
+        & + .el-button {
+          margin-left: 20px;
+        }
+      }
+    }
+  }
+}
+</style>

+ 142 - 0
apps/bigmember_pc/src/views/article-content/components/RecommendServesCard.vue

@@ -0,0 +1,142 @@
+<script setup>
+const props = defineProps({
+  headerTitle: {
+    type: String,
+    default: ''
+  },
+  headerMore: {
+    type: String,
+    default: ''
+  },
+  clickMore: {
+    type: Function,
+    default: () => {}
+  },
+  title: {
+    type: String,
+    default: ''
+  },
+  content: {
+    type: String,
+    default: ''
+  },
+  buttons: {
+    type: Array,
+    default: () => {
+      return [
+        {
+          label: '立即查看',
+          class: 'less',
+          action: () => {
+            console.log('立即查看')
+          }
+        }
+      ]
+    }
+  }
+})
+</script>
+<template>
+  <div
+    class="recommend-serve-card flex flex-(col items-center justify-between)"
+    :class="{ 'has-header': headerTitle || headerMore }"
+  >
+    <div
+      class="recommend-serve-header w-full flex flex-(row items-center justify-between)"
+      v-if="headerTitle || headerMore"
+    >
+      <span>{{ headerTitle }}</span>
+      <span class="action-text flex flex-(row items-center)" @click="clickMore"
+        >{{ headerMore }} <i class="iconfont icon-more"></i
+      ></span>
+    </div>
+    <div
+      class="recommend-serve-content flex flex-(1 col items-center justify-between)"
+    >
+      <div>
+        <span class="title-text">{{ title }}</span>
+        <div class="text-center">
+          {{ content }}
+        </div>
+      </div>
+      <div
+        class="action-btns flex flex-(row items-center)"
+        :class="{ 'has-only': buttons.length === 1 }"
+      >
+        <el-button
+          v-for="(button, index) in buttons"
+          :key="index"
+          :type="button.type"
+          @click="button.action"
+          >{{ button.label }}</el-button
+        >
+      </div>
+    </div>
+  </div>
+</template>
+
+<style lang="scss">
+.recommend-serve-card {
+  &:nth-of-type(odd) {
+    margin-left: 0;
+  }
+  &:nth-of-type(even) {
+    margin-right: 0;
+  }
+  margin: 16px 8px 0 8px;
+  width: calc(50% - 8px);
+  min-height: 178px;
+  overflow: hidden;
+  border-radius: 8px;
+  border: 1px solid rgba(0, 0, 0, 0.05);
+  background: linear-gradient(180deg, #fff 0%, #f3f7f9 100%);
+  box-shadow: 0px 4px 8px 0px rgba(129, 134, 136, 0.06);
+  &.has-header {
+    min-height: 231px;
+  }
+
+  .recommend-serve-header {
+    padding: 8px 16px;
+    background: linear-gradient(270deg, #fae7ca 0%, #f1d090 100%);
+    color: #1d1d1d;
+    font-size: 14px;
+    line-height: 22px;
+    .action-text {
+      color: #b1700e;
+      font-size: 12px;
+      cursor: pointer;
+    }
+
+    & + .recommend-serve-content {
+      padding-top: 16px;
+    }
+  }
+  .recommend-serve-content {
+    color: #686868;
+    text-align: center;
+    font-size: 14px;
+    line-height: 22px;
+    padding: 24px 42px;
+    .title-text {
+      display: inline-block;
+      color: #1d1d1d;
+      font-size: 16px;
+      line-height: 24px;
+      margin-bottom: 10px;
+    }
+  }
+
+  .action-btns {
+    .el-button {
+      min-width: 132px;
+      height: 36px;
+      padding: 6px 16px;
+      font-size: 16px;
+      line-height: 22px;
+      & + .el-button {
+        margin-left: 32px;
+      }
+    }
+  }
+}
+</style>

+ 142 - 0
apps/bigmember_pc/src/views/article-content/components/Reward.vue

@@ -0,0 +1,142 @@
+<template>
+  <div class="user-reward">
+    <div class="user-reward-text">{{ rewardText }}</div>
+    <div class="user-reward-action">
+      <button
+        type="button"
+        class="user-reward-button"
+        @click="openRewardDialog(true)"
+      >
+        打赏
+      </button>
+    </div>
+    <el-dialog
+      :visible.sync="rewardDialog"
+      custom-class="v-reward-dialog"
+      :show-close="false"
+      title="请微信扫码"
+      top="30vh"
+    >
+      <el-image :src="rewardImgSrc"></el-image>
+      <div class="close-position">
+        <span
+          class="j-icon icon-reward-close"
+          @click="openRewardDialog(false)"
+        ></span>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import { getRewardText } from '@/api/modules/detail'
+export default {
+  name: 'Reward',
+  prop: {
+    id: {
+      type: String,
+      default: ''
+    }
+  },
+  data() {
+    return {
+      rewardDialog: false,
+      rewardText: '',
+      rewardImgSrcBase: '/jypay/weixin/reward/qr/'
+    }
+  },
+  computed: {
+    rewardImgSrc() {
+      return `${this.rewardImgSrcBase}${this.id}`
+    }
+  },
+  created() {
+    this.getRewardText()
+  },
+  methods: {
+    async getRewardText() {
+      this.rewardText = await getRewardText()
+    },
+    openRewardDialog(f) {
+      this.rewardDialog = f
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.user-reward {
+  margin: 20px auto;
+  text-align: center;
+  &-text {
+    margin-bottom: 16px;
+    font-size: 16px;
+    color: #1d1d1d;
+  }
+  &-action {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+  }
+  &-button {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    width: 70px;
+    height: 70px;
+    color: #fff;
+    font-size: 18px;
+    background-color: #ea5a44;
+    border-radius: 50%;
+  }
+}
+.el-image {
+  width: 170px;
+}
+.close-position {
+  position: absolute;
+  right: 20px;
+  bottom: 40px;
+  cursor: pointer;
+}
+.icon-reward-close {
+  display: inline-block;
+  width: 46px;
+  height: 46px;
+}
+
+.icon-reward-close {
+  background: transparent url(~@/assets/images/icon/icon-reward-close.png)
+    no-repeat center;
+  background-size: contain;
+}
+::v-deep {
+  .v-reward-dialog {
+    width: 497px;
+    height: 374px;
+    text-align: center;
+    box-shadow: none;
+    background: transparent url(~@/assets/images/dialog/reward-bgi.png)
+      no-repeat;
+    background-size: contain;
+    .el-dialog__ {
+      &header {
+        padding-top: 100px;
+      }
+      &title {
+        color: #0ab70e;
+        font-size: 26px;
+        font-weight: 700;
+        letter-spacing: 2px;
+      }
+      &body {
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        padding: 0;
+        min-height: 165px;
+      }
+    }
+  }
+}
+</style>

+ 80 - 0
apps/bigmember_pc/src/views/article-content/composables/README.md

@@ -0,0 +1,80 @@
+# PC标讯详情页 - 涉及组合式函数说明文档
+
+## 目录
+```
+├── useArticleStar.js               - 详情收藏
+├── useArticleUtil.js               - 通用工具
+├── useContentStore.js              - 业务数据模型
+├── useDistribute.js                - 详情分发
+├── useHoverElementClientRect.js    - 获取位置,及鼠标 hover、click 事件委托
+└── useShare.js                     - 详情分享  
+
+```
+
+## useHoverElementClientRect
+> 获取位置,及鼠标 hover、click 事件委托
+
+### 示例
+```
+import { useHoverHighlightTextPopover } from '@/xxx/useHoverElementClientRect'
+
+const { elementState } = useHoverHighlightTextPopover({
+  // 委托元素选择器
+  parentSelector: '.article-page-container',
+  // 事件绑定元素 className
+  hasClass: 'keyword-underline',
+  // 绑定元素 hover change 事件
+  onChangeHover: (isHover) => {},
+  // 绑定元素 click 事件
+  onClick: (target) => {}
+})
+```
+
+### elementState
+```
+const elementState = computed(() => {
+    return {
+      // innerText  
+      text: elementInfo.value.text,
+      className: elementInfo.value.className,
+      // data-eid
+      winnerId: elementInfo.value.winnerId,
+      
+      isHover: isHover.value,
+      rect: rect.value,
+      style: rect.value
+        ? {
+            position: 'fixed',
+            background: 'transparent',
+            zIndex: -1,
+            top: `${rect.value.y}px`,
+            left: `${rect.value.x}px`,
+            width: `${rect.value.width}px`,
+            height: `${rect.value.height}px`
+          }
+        : {}
+    }
+})
+```
+
+
+## useArticleStar
+> 标讯收藏业务模型
+
+### 示例
+```
+const { starModel, doFetchStarState } = useArticleStar(id)
+```
+
+### startModel
+```
+{
+ // 是否收藏
+ star: false,
+ // 收藏标签信息
+ labels: []
+}
+```
+
+### doFetchStarState
+> 接口请求

+ 344 - 0
apps/bigmember_pc/src/views/article-content/composables/useArticleContentPageModel.js

@@ -0,0 +1,344 @@
+import {
+  ref,
+  onMounted,
+  onBeforeMount,
+  computed,
+  getCurrentInstance
+} from 'vue'
+import { useRoute } from 'vue-router/composables'
+import { debounce, throttle } from 'lodash'
+import { replaceKeyword } from '@/utils'
+import { dateFromNow } from '@jy/util'
+// 引入组件
+import ContentHeader from '@/views/article-content/components/ContentHeader.vue'
+import CollectInfo from '@/components/collect-info/CollectInfo.vue'
+import Reward from '@/views/article-content/components/Reward.vue'
+import TimeLine from '@/components/time-line/TimeLine.vue'
+import ContentMask from '@/views/article-content/components/ContentMask.vue'
+import Nps from '../components/Nps.vue'
+import adsense from '@/views/order/components/adsense/index.vue'
+import ContentLayout from '@/components/common/ContentLayout.vue'
+
+import RecommendCustomersList from '@/views/article-content/components/RecommendCustomers.vue'
+import ContentSummary from '@/views/article-content/components/ContentSummary.vue'
+import RecommendEnt from '@/views/article-content/components/RecommendEnt.vue'
+import QuickMonitor from '@/composables/quick-monitor/component/QuickMonitor.vue'
+import {
+  useContentStore,
+  ContentExpandsModel,
+  ContentModel,
+  ContentPageLoading,
+  SummaryModel
+} from '@/views/article-content/composables/useContentStore'
+import DownProjectReport from '@/composables/down-project-report/component/DownProjectReport.vue'
+import {
+  doOpenArticlePage,
+  doOpenCorListPage,
+  doOpenProjectDetailPage,
+  doOpenWinnerPage
+} from '@/views/article-content/composables/useArticleUtil'
+import RecommendOpportunities from '@/views/article-content/components/RecommendOpportunities.vue'
+import FooterAd from '@/views/article-content/components/FooterAd.vue'
+import OriginLink from '@/views/article-content/components/OriginLink.vue'
+import RecommendServes from '@/views/article-content/components/RecommendServes.vue'
+import ContentHeaderSkeleton from '@/views/article-content/components/ContentHeaderSkeleton.vue'
+import PoverTimeLine from '@/components/time-line/PoverTimeLine.vue'
+import { useHoverHighlightTextPopover } from '@/views/article-content/composables/useHoverElementClientRect'
+import attachmentDownload from '@/composables/attachment-download/component/AttachmentDownload.vue'
+
+// 使用内容存储
+useContentStore()
+// 判断在哪个容器中
+const InWhichContainer = window.parent !== window ? 'in-app' : 'in-web'
+
+// 注入右侧DOM
+onMounted(() => {
+  if (InWhichContainer === 'in-web') {
+    try {
+      $('#inWebRightElement').append($('#in-web-detail-right-group').clone())
+      $('#in-web-detail-right-group').show()
+    } catch (e) {
+      console.warn('not inject detail-right-group')
+    }
+  }
+})
+
+// 初始化内容标签和状态
+const activeContentTab = ref('公告摘要')
+const contentTabs = [
+  {
+    label: '公告摘要'
+  },
+  {
+    label: '公告正文'
+  },
+  {
+    label: '招标/采购进度'
+  },
+  {
+    label: '投标服务'
+  },
+  {
+    label: '商机推荐'
+  },
+  {
+    label: '客户推荐'
+  }
+]
+
+const tabContentState = ref({
+  公告摘要: true,
+  公告正文: true,
+  '招标/采购进度': true,
+  投标服务: true,
+  商机推荐: true,
+  客户推荐: true
+})
+
+// 计算哪些标签的内容应该显示
+const tabContentShow = computed(() => {
+  const computedState = {
+    商机推荐:
+      ContentExpandsModel.value.recommendProjects.total > 0 ||
+      ContentExpandsModel.value.recommendBuyers.total > 0 ||
+      ContentExpandsModel.value.recommendWinners.total > 0,
+    投标服务: ContentExpandsModel.value.services.length > 0,
+    '招标/采购进度':
+      ContentExpandsModel.value.projectProgress?.list?.length > 0,
+    公告摘要: SummaryModel.value.list.filter((v) => v.value).length > 0,
+    公告正文: ContentModel.value.content
+  }
+  const result = Object.assign({}, tabContentState.value, computedState)
+  handleScroll()
+  return result
+})
+// 计算应显示的标签列表
+const tabContentHeaderList = computed(() => {
+  return contentTabs.filter((v) => {
+    return tabContentShow.value[v.label]
+  })
+})
+
+// 隐藏某个标签的内容
+function doHideTabContent(label) {
+  tabContentState.value[label] = false
+}
+
+// 滚动到顶部
+function scrollToTop(element, diff = 0) {
+  if (element) {
+    const topOffset = element.getBoundingClientRect().top + window.pageYOffset
+    window.scrollTo({
+      top: topOffset - diff,
+      behavior: 'instant'
+    })
+  }
+}
+
+// 选择某个标签
+function doSelectTab(item) {
+  activeContentTab.value = item.label
+  const goElement = document.querySelector(
+    '.watch-tab-content[name="' + item.label + '"]'
+  )
+  scrollToTop(goElement, 45)
+}
+
+// 招标、采购进度数据
+const timeLineList = computed(() => {
+  return ContentExpandsModel.value.projectProgress.list?.map((v) => {
+    return {
+      content: replaceKeyword(
+        v.title,
+        ContentExpandsModel.value.projectProgress.name,
+        ['<span class="highlight-text">', '</span>']
+      ),
+      id: v.id,
+      s_id: v.id,
+      tags: [v.tag],
+      time: v.time,
+      isActive: v.isActive
+    }
+  })
+})
+
+// 控制头部是否固定
+const isFixedHeader = ref(false)
+
+// 滚动处理,用于更新激活的标签和固定头部
+const handleScroll = throttle((e) => {
+  const watchElements = document.querySelectorAll('.watch-tab-content')
+  let lastVisibleElement = null
+
+  for (let i = 0; i < watchElements.length; i++) {
+    const element = watchElements[i]
+    const rect = element.getBoundingClientRect()
+    const watchHeight = window.innerHeight
+    const visible = rect.top >= 0 && rect.top <= watchHeight * 0.6
+    if (visible) {
+      lastVisibleElement = element
+      break
+    }
+  }
+  if (lastVisibleElement) {
+    activeContentTab.value = lastVisibleElement.getAttribute('name')
+  }
+
+  const headerElement = document.querySelector('.watch-tab-header')
+  const fixedHeaderElement = document.querySelector(
+    '.watch-tab-header .is-fixed-top'
+  )
+  if (fixedHeaderElement) {
+    fixedHeaderElement.style.width = headerElement.clientWidth + 'px'
+  }
+
+  const canShow = headerElement?.getBoundingClientRect().top <= 0
+  isFixedHeader.value = canShow
+}, 240)
+
+// 页面加载时的处理
+onMounted(() => {
+  window.addEventListener('scroll', handleScroll)
+  handleScroll()
+})
+
+// 组件即将挂载前的处理,移除滚动事件监听
+onBeforeMount(() => {
+  window.removeEventListener('scroll', handleScroll)
+})
+
+// 获取内容ID
+const contentId = useRoute().params.id.replace('.html', '')
+
+// 打开留资弹窗
+const collectElement = ref(null)
+function doOpenCollectDialog(key) {
+  if (typeof key === 'string') {
+    collectElement.value?.noCallApiFn(key, false)
+  } else if (typeof key === 'object') {
+    const { source, reload = false } = key
+    collectElement.value?.noCallApiFn(source, reload)
+  }
+}
+
+// 查看原文
+const originLinkElement = ref(null)
+function doOpenOriginLink() {
+  originLinkElement.value?.doGetLinkAction()
+}
+
+// 客户推荐操作
+function doOpenMore(key) {
+  switch (key) {
+    case 'recommendCustomers': {
+      doOpenCorListPage()
+      break
+    }
+  }
+}
+
+// 是否有项目进度信息
+const hasProject = computed(() => {
+  return ContentExpandsModel.value.projectProgress.name
+})
+
+// 联系客服
+const that = getCurrentInstance().proxy
+function doOpenCustomer() {
+  that?.contactCustomer(that)
+}
+
+// 是否显示遮罩
+const canShowMask = computed(() => {
+  let type = 'free-max'
+  if (ContentModel.value.isNiJian) {
+    type = 'proposed'
+  } else if (ContentModel.value.isCaigouyixiang) {
+    type = 'purchase'
+  }
+  return {
+    show: !ContentModel.value.isCanRead,
+    type: type
+  }
+})
+
+const popoverElement = ref(null)
+const popoverTriggerElement = ref(null)
+
+// 招标、采购进度数据
+const winnerTimeLineList = computed(() => {
+  return ContentExpandsModel.value.recommendWinners.list.map((v) => {
+    const tag = v._o.subtype || v._o.toptype || ''
+    return {
+      content: replaceKeyword(
+        v.title,
+        ContentExpandsModel.value.recommendWinners.name,
+        ['<span class="highlight-text">', '</span>']
+      ),
+      id: v.id,
+      s_id: v.id,
+      tags: tag ? [tag] : [],
+      time: v?.time ? dateFromNow(v?.time) : '',
+      isActive: v.isActive
+    }
+  })
+})
+
+// 根据触发元素确定悬浮窗口内容
+const popoverElementType = computed(() => {
+  if (elementState.value.className.indexOf('project-name') !== -1) {
+    return 'project-name'
+  }
+  if (elementState.value.className.indexOf('winner-name') !== -1) {
+    return 'winner-name'
+  }
+  return ''
+})
+
+// 项目名称、企业名称悬浮窗口内容
+const popoverTimeLine = computed(() => {
+  if (popoverElementType.value === 'project-name') {
+    return {
+      title: '项目公告',
+      list: timeLineList.value
+    }
+  }
+  if (
+    elementState.value.text ===
+      ContentExpandsModel.value.recommendWinners.name &&
+    popoverElementType.value === 'winner-name'
+  ) {
+    return {
+      title: '企业最新信息',
+      list: winnerTimeLineList.value
+    }
+  }
+  return {
+    title: '',
+    list: []
+  }
+})
+
+// 使用悬停高亮文本 popover
+const { elementState } = useHoverHighlightTextPopover({
+  parentSelector: '.article-page-container',
+  hasClass: 'keyword-underline',
+  onChangeHover: debounce((isHover) => {
+    popoverElement.value.doChangePopover(isHover)
+  }, 150),
+  onClick: () => {
+    if (popoverElementType.value === 'project-name') {
+      doOpenProjectDetailPage({ id: contentId })
+    }
+    if (popoverElementType.value === 'winner-name') {
+      if (elementState.value.winnerId) {
+        doOpenWinnerPage({ id: elementState.value.winnerId })
+      }
+    }
+  }
+})
+
+// 免费查看操作
+function doClickFreeView() {
+  doOpenCollectDialog('peugeot_view_infor')
+}

+ 29 - 0
apps/bigmember_pc/src/views/article-content/composables/useArticleStar.js

@@ -0,0 +1,29 @@
+import { checkBidsIsColl } from '@/api/modules'
+import { ref } from 'vue'
+
+export function useArticleStarModel(id) {
+  const starModel = ref({
+    star: false,
+    labels: []
+  })
+
+  function doFetchStarState() {
+    return checkBidsIsColl({ bids: id, label: 'info' }).then((r) => {
+      if (r.data) {
+        starModel.value.star = r.data?.iscoll
+        starModel.value.labels = r.data?.labels?.map((v) => {
+          return {
+            id: v.id,
+            label: v.labelname,
+            link: `/swordfish/frontPage/collection/sess/index?tag=${v.id}`
+          }
+        })
+      }
+    })
+  }
+
+  return {
+    starModel,
+    doFetchStarState
+  }
+}

+ 73 - 0
apps/bigmember_pc/src/views/article-content/composables/useArticleUtil.js

@@ -0,0 +1,73 @@
+import router from '@/router'
+
+// 打开标讯详情页
+export function doOpenArticlePage({ id }) {
+  const link = router.resolve({
+    name: 'article_detail',
+    params: {
+      id: id,
+      content: 'content'
+    }
+  })
+  window.open(
+    link.href.replace('/swordfish/page_big_pc', '') + '.html',
+    '_blank'
+  )
+}
+
+// 打开采购单位详情页
+export function doOpenBuyerPage({ name, query = {} }) {
+  const link = router.resolve({
+    path: '/unit_portrayal/' + name,
+    query: query
+  })
+  window.open(link.href, '_blank')
+}
+
+// 打开采购单位监控列表页
+export function doOpenBuyerListPage() {
+  window.open('/swordfish/page_big_pc/my_client', '_blank')
+}
+
+// 打开中标企业详情页
+export function doOpenWinnerPage({ id, query = {} }) {
+  const link = router.resolve({
+    path: '/ent_portrait/' + id,
+    query: query
+  })
+  window.open(link.href, '_blank')
+}
+
+// 打开企业监控列表页
+export function doOpenWinnerListPage() {
+  window.open('/swordfish/page_big_pc/free/ent_follow', '_blank')
+}
+
+// 打开更多客户监控列表
+export function doOpenCorListPage(query = {}) {
+  const link = router.resolve({
+    path: '/potential_cor_list/c',
+    query
+  })
+  window.open(link.href, '_blank')
+}
+
+// 打开项目监控详情页
+export function doOpenProjectDetailPage({ id, mark }) {
+  window.open(
+    `/swordfish/page_big_pc/pro_follow_detail?sid=${id}${
+      mark ? `&mark=${mark}` : ''
+    }`,
+    '_blank'
+  )
+}
+
+// 打开项目监控列表页
+export function doOpenProjectProgressListPage() {
+  window.open('/swordfish/page_big_pc/free/project_progress', '_blank')
+}
+
+// 打开推送提醒设置
+export function doOpenPushSettingPage() {
+  window.open('/swordfish/page_big_pc/push_setting')
+}

+ 111 - 0
apps/bigmember_pc/src/views/article-content/composables/useContentStore.js

@@ -0,0 +1,111 @@
+import {
+  ajaxGetArticlePreAgentInfo,
+  ajaxGetContentInfo,
+  ajaxGetContentOtherInfo
+} from '@/api/modules/detail'
+import useContentModel from '@jy/data-models/modules/article/model/content'
+import { computed, reactive, ref } from 'vue'
+import useContentExpandModel from '@jy/data-models/modules/article/model/expand'
+
+function setPageTDK({ title, description, keywords }) {
+  const descriptionDOM =
+    document.querySelector('meta[name=description]') ||
+    document.querySelector('meta[name=Description]')
+  const keywordsDOM =
+    document.querySelector('meta[name=keywords]') ||
+    document.querySelector('meta[name=Keywords]')
+
+  if (title) {
+    document.title = title
+  }
+  try {
+    if (description) {
+      descriptionDOM.content = description
+    }
+    if (keywords) {
+      keywordsDOM.content = keywords
+    }
+  } catch (e) {
+    console.warn(e)
+  }
+}
+
+const AgentInfo = ref({
+  token: '',
+  baseToken: ''
+})
+const Content = reactive(useContentModel())
+const ContentExpands = reactive(useContentExpandModel())
+
+const ContentPageLoading = ref(true)
+const ContentPageExpandsLoading = ref(true)
+
+const ContentModel = computed(() => {
+  return Content.model.content
+})
+const ContentId = computed(() => {
+  return ContentModel.value.id
+})
+
+const SummaryModel = computed(() => {
+  return Content.model.summary
+})
+
+const ContentExpandsModel = computed(() => {
+  return ContentExpands.model
+})
+async function useContentStore() {
+  ContentPageLoading.value = true
+  await ajaxGetArticlePreAgentInfo().then((res) => {
+    if (res?.error_code === 0 && res?.data) {
+      AgentInfo.value.token = res.data?.token
+    }
+  })
+
+  await ajaxGetContentInfo({ token: AgentInfo.value.token })
+    .then((res) => {
+      if (res.error_code === 0) {
+        AgentInfo.value.baseToken = res.data?.token
+        return res.data
+      }
+    })
+    .then(Content.transformModel)
+    .then((model) => {
+      ContentPageLoading.value = false
+      setPageTDK(model.content.tdk)
+    })
+
+  if (AgentInfo.value.baseToken && ContentModel.value.isCanRead) {
+    ContentPageExpandsLoading.value = true
+    await ajaxGetContentOtherInfo({ token: AgentInfo.value.baseToken })
+      .then((res) => {
+        ContentPageExpandsLoading.value = false
+        if (res.error_code === 0) {
+          return res.data
+        }
+      })
+      .then(ContentExpands.transformModel)
+  } else {
+    ContentPageExpandsLoading.value = false
+  }
+
+  return {
+    useContentStore,
+    ContentModel,
+    SummaryModel,
+    ContentId,
+    ContentExpandsModel,
+    ContentPageLoading,
+    ContentPageExpandsLoading
+  }
+}
+
+export {
+  useContentStore,
+  ContentModel,
+  SummaryModel,
+  ContentId,
+  ContentExpandsModel,
+  ContentPageLoading,
+  ContentPageExpandsLoading
+}

+ 40 - 0
apps/bigmember_pc/src/views/article-content/composables/useDistribute.js

@@ -0,0 +1,40 @@
+import { getCurrentInstance, ref } from 'vue'
+import { ajaxSetDidDistributor } from '@/api/modules'
+import { useRoute } from 'vue-router/composables'
+
+export function useDistribute() {
+  const params = useRoute().params
+  const usePowerRef = ref(null)
+  function openDistribute(e) {
+    usePowerRef.value.titleMsg = '选择接收人员'
+    usePowerRef.value.searchVal = ''
+    usePowerRef.value.centerDialogVisible = true
+    usePowerRef.value.selectDataIds = [params.id]
+    usePowerRef.value.getData('yes')
+  }
+
+  // 提交分发
+  function doSubmitDistribute(data) {
+    ajaxSetDidDistributor({
+      infoids: [params.id],
+      staffs: data
+    }).then((res) => {
+      const $message = getCurrentInstance().proxy.$message
+      if (res.error_code === 0) {
+        if (res.data === 1) {
+          $message({ message: '分发成功', type: 'success' })
+        } else {
+          $message({ message: res.error_msg, type: 'warning' })
+        }
+      } else {
+        $message({ message: res.error_msg, type: 'warning' })
+      }
+    })
+  }
+
+  return {
+    usePowerRef,
+    openDistribute,
+    doSubmitDistribute
+  }
+}

+ 154 - 0
apps/bigmember_pc/src/views/article-content/composables/useHoverElementClientRect.js

@@ -0,0 +1,154 @@
+import { ref, onMounted, onUnmounted, computed } from 'vue'
+
+/**
+ * 查询父级节点是否包含指定 ClassName
+ * @param event
+ * @param hasClass
+ * @param maxDepth - 最大查询层级
+ * @param onCustomClass - 自定义判断函数
+ */
+
+function checkAncestorClass(
+  event,
+  hasClass,
+  maxDepth = 3,
+  onCustomClass = null
+) {
+  let target = event.target
+  let depth = 0 // 添加一个深度计数器
+
+  while (target && target.nodeType === Node.ELEMENT_NODE && depth < maxDepth) {
+    let result = {
+      status: target.className.indexOf(hasClass) !== -1,
+      target
+    }
+
+    if (typeof onCustomClass === 'function') {
+      result.status = onCustomClass(target.className)
+    }
+    if (result.status) {
+      return result
+    }
+
+    target = target.parentNode
+    depth++ // 每次循环增加深度计数
+  }
+
+  return {
+    status: false,
+    target
+  }
+}
+
+/**
+ * 委托监听指定 parentSelector 节点下的指定 hasClass 的hover事件,返回双向绑定的坐标信息及 hover状态
+ * @param parentSelector - 委托父级选择器
+ * @param hasClass  - 事件判断类名
+ * @param onChangeHover - 回调事件
+ * @return {{elementState: ComputedRef<{rect: *, isHover: boolean, className: string, style: {top: string, left: string, background: string, width: string, position: string, zIndex: number, height: string}|{}}>}}
+ */
+
+export function useHoverHighlightTextPopover({
+  parentSelector,
+  hasClass,
+  onCustomClass = null,
+  onChangeHover = () => {},
+  onClick = () => {}
+}) {
+  // 创建响应式引用
+  const rect = ref(null)
+  const isHover = ref(false)
+  const elementInfo = ref({
+    className: '',
+    text: '',
+    winnerId: ''
+  })
+  const elementState = computed(() => {
+    return {
+      text: elementInfo.value.text,
+      className: elementInfo.value.className,
+      winnerId: elementInfo.value.winnerId,
+      isHover: isHover.value,
+      rect: rect.value,
+      style: rect.value
+        ? {
+            position: 'fixed',
+            background: 'transparent',
+            zIndex: -1,
+            top: `${rect.value.y}px`,
+            left: `${rect.value.x}px`,
+            width: `${rect.value.width}px`,
+            height: `${rect.value.height}px`
+          }
+        : {}
+    }
+  })
+
+  const handleClick = (event) => {
+    const { status, target } = checkAncestorClass(
+      event,
+      hasClass,
+      3,
+      onCustomClass
+    )
+    if (status) {
+      tranElementInfo(target)
+      onClick(target)
+    }
+  }
+
+  function tranElementInfo(activeElement) {
+    elementInfo.value.className = activeElement.className
+    elementInfo.value.text = activeElement.innerText
+    elementInfo.value.winnerId = activeElement.getAttribute('data-eid')
+  }
+
+  const handleMouseOver = (event) => {
+    const { status, target } = checkAncestorClass(
+      event,
+      hasClass,
+      3,
+      onCustomClass
+    )
+    if (status) {
+      // 检测到指定的类名
+      const activeElement = target
+      const activeElementRect = activeElement?.getBoundingClientRect()
+      tranElementInfo(activeElement)
+      if (activeElementRect) {
+        rect.value = activeElementRect
+        isHover.value = true
+        onChangeHover(isHover.value, elementState.value)
+      } else {
+        isHover.value = false
+        onChangeHover(isHover.value, elementState.value)
+      }
+    } else {
+      isHover.value = false
+      onChangeHover(isHover.value, elementState.value)
+    }
+  }
+
+  function useElementListener() {
+    // 使用事件委托,将事件监听绑定到 parentElement 上
+    const parentElement = document.querySelector(parentSelector)
+    if (parentElement) {
+      parentElement.addEventListener('mouseover', handleMouseOver)
+      parentElement.addEventListener('click', handleClick)
+    }
+  }
+
+  onUnmounted(() => {
+    // 移除事件监听
+    const parentElement = document.querySelector(parentSelector)
+    if (parentElement) {
+      parentElement.removeEventListener('mouseover', handleMouseOver)
+      parentElement.removeEventListener('click', handleClick)
+    }
+  })
+
+  return {
+    elementState,
+    useElementListener
+  }
+}

+ 25 - 0
apps/bigmember_pc/src/views/article-content/composables/useShare.js

@@ -0,0 +1,25 @@
+import { onMounted, ref } from 'vue'
+import { useRoute } from 'vue-router/composables'
+
+export function useShare() {
+  // 打开弹框
+  const useShareRef = ref(null)
+
+  const params = useRoute().params
+  onMounted(() => {
+    useShareRef.value.sendData({
+      code: 1,
+      stype: params.content,
+      id: params.id,
+      link: window.location.href
+    })
+  })
+  function openShare() {
+    useShareRef.value.showNewuserDialog = true
+  }
+
+  return {
+    useShareRef,
+    openShare
+  }
+}

+ 0 - 0
apps/bigmember_pc/src/views/article-content/composables/useTabs.js


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

@@ -0,0 +1,640 @@
+<script setup>
+import {
+  ref,
+  onMounted,
+  onBeforeMount,
+  computed,
+  getCurrentInstance
+} from 'vue'
+import { useRoute } from 'vue-router/composables'
+import { debounce, throttle } from 'lodash'
+import { moneyUnit, replaceKeyword } from '@/utils'
+import { dateFromNow } from '@jy/util'
+// 组件引入
+import ContentHeader from '@/views/article-content/components/ContentHeader.vue'
+import CollectInfo from '@/components/collect-info/CollectInfo.vue'
+import Reward from '@/views/article-content/components/Reward.vue'
+import TimeLine from '@/components/time-line/TimeLine.vue'
+import ContentMask from '@/views/article-content/components/ContentMask.vue'
+import Nps from '../components/Nps.vue'
+import adsense from '@/views/order/components/adsense/index.vue'
+import ContentLayout from '@/components/common/ContentLayout.vue'
+
+import RecommendCustomersList from '@/views/article-content/components/RecommendCustomers.vue'
+import ContentSummary from '@/views/article-content/components/ContentSummary.vue'
+import RecommendEnt from '@/views/article-content/components/RecommendEnt.vue'
+import QuickMonitor from '@/composables/quick-monitor/component/QuickMonitor.vue'
+import {
+  useContentStore,
+  ContentExpandsModel,
+  ContentModel,
+  ContentId,
+  ContentPageLoading,
+  ContentPageExpandsLoading,
+  SummaryModel
+} from '@/views/article-content/composables/useContentStore'
+import DownProjectReport from '@/composables/down-project-report/component/DownProjectReport.vue'
+import {
+  doOpenArticlePage,
+  doOpenCorListPage,
+  doOpenProjectDetailPage,
+  doOpenWinnerPage
+} from '@/views/article-content/composables/useArticleUtil'
+import RecommendOpportunities from '@/views/article-content/components/RecommendOpportunities.vue'
+import FooterAd from '@/views/article-content/components/FooterAd.vue'
+import OriginLink from '@/views/article-content/components/OriginLink.vue'
+import RecommendServes from '@/views/article-content/components/RecommendServes.vue'
+import ContentHeaderSkeleton from '@/views/article-content/components/ContentHeaderSkeleton.vue'
+import PoverTimeLine from '@/components/time-line/PoverTimeLine.vue'
+import { useHoverHighlightTextPopover } from '@/views/article-content/composables/useHoverElementClientRect'
+import attachmentDownload from '@/composables/attachment-download/component/AttachmentDownload.vue'
+import ContentRightTimeLine from '@/views/article-content/components/ContentRightTimeLine.vue'
+import ContentThirdPopover from '@/views/article-content/components/ContentThirdPopover.vue'
+
+useContentStore()
+// 判断在哪个容器
+const InWhichContainer = window.parent !== window ? 'in-app' : 'in-web'
+
+// 注入右侧DOM
+onMounted(() => {
+  if (InWhichContainer === 'in-web') {
+    try {
+      $('#inWebRightElement').append($('#in-web-detail-right-group').clone())
+      $('#in-web-detail-right-group').show()
+    } catch (e) {
+      console.warn('not inject detail-right-group')
+    }
+  }
+})
+
+const activeContentTab = ref('公告摘要')
+const contentTabs = [
+  {
+    label: '公告摘要'
+  },
+  {
+    label: '公告正文'
+  },
+  {
+    label: '投标服务'
+  },
+  {
+    label: '商机推荐'
+  },
+  {
+    label: '客户推荐'
+  }
+]
+
+const tabContentState = ref({
+  公告摘要: true,
+  公告正文: true,
+  投标服务: true,
+  商机推荐: true,
+  客户推荐: true
+})
+
+const tabContentShow = computed(() => {
+  const computedState = {
+    商机推荐:
+      ContentExpandsModel.value.recommendProjects.total > 0 ||
+      ContentExpandsModel.value.recommendBuyers.total > 0 ||
+      ContentExpandsModel.value.recommendWinners.total > 0,
+    投标服务: ContentExpandsModel.value.services.length > 0,
+    公告摘要: SummaryModel.value.list.filter((v) => v.value).length > 0,
+    公告正文: ContentModel.value.content
+  }
+  const result = Object.assign({}, tabContentState.value, computedState)
+  handleScroll()
+  return result
+})
+const tabContentHeaderList = computed(() => {
+  return contentTabs.filter((v) => {
+    return tabContentShow.value[v.label]
+  })
+})
+
+function doHideTabContent(label) {
+  tabContentState.value[label] = false
+}
+
+function scrollToTop(element, diff = 0) {
+  if (element) {
+    const topOffset = element.getBoundingClientRect().top + window.pageYOffset
+    window.scrollTo({
+      top: topOffset - diff,
+      behavior: 'instant'
+    })
+  }
+}
+
+function doSelectTab(item) {
+  activeContentTab.value = item.label
+  const goElement = document.querySelector(
+    '.watch-tab-content[name="' + item.label + '"]'
+  )
+  scrollToTop(goElement, 45)
+}
+
+// 招标、采购进度
+const timeLineList = computed(() => {
+  return (
+    ContentExpandsModel.value.projectProgress.list?.map((v) => {
+      return {
+        content: replaceKeyword(
+          v.title,
+          ContentExpandsModel.value.projectProgress.name,
+          ['<span class="highlight-text">', '</span>']
+        ),
+        contentType: [v._data?.toptype, v._data?.subtype].filter((v) => v),
+        money: v?.bidAmount
+          ? moneyUnit(v?.bidAmount).replace('元', '') + '元'
+          : '',
+        id: v.id,
+        s_id: v.id,
+        tags: [v.tag],
+        time: v.time,
+        isActive: v.isActive
+      }
+    }) || []
+  )
+})
+
+const isFixedHeader = ref(false)
+
+const handleScroll = throttle((e) => {
+  const watchElements = document.querySelectorAll('.watch-tab-content')
+  let lastVisibleElement = null
+
+  for (let i = 0; i < watchElements.length; i++) {
+    const element = watchElements[i]
+    const rect = element.getBoundingClientRect()
+    const watchHeight = window.innerHeight
+    const visible = rect.top >= 0 && rect.top <= watchHeight * 0.6
+    if (visible) {
+      lastVisibleElement = element
+      break
+    }
+  }
+  if (lastVisibleElement) {
+    activeContentTab.value = lastVisibleElement.getAttribute('name')
+  }
+
+  const headerElement = document.querySelector('.watch-tab-header')
+  const fixedHeaderElement = document.querySelector(
+    '.watch-tab-header .is-fixed-top'
+  )
+  if (fixedHeaderElement) {
+    fixedHeaderElement.style.width = headerElement.clientWidth + 'px'
+  }
+
+  const canShow = headerElement?.getBoundingClientRect().top <= 0
+  isFixedHeader.value = canShow
+}, 240)
+const handleFocus = () => {
+  const activeElement = document.activeElement
+  if (activeElement) {
+    activeElement.blur()
+  }
+}
+
+onMounted(() => {
+  window.addEventListener('scroll', handleScroll)
+  handleScroll()
+
+  document.addEventListener('visibilitychange', handleFocus)
+})
+
+onBeforeMount(() => {
+  window.removeEventListener('scroll', handleScroll)
+  window.removeEventListener('visibilitychange', handleFocus)
+})
+
+// 打开留资弹窗
+const collectElement = ref(null)
+function doOpenCollectDialog(key) {
+  if (typeof key === 'string') {
+    collectElement.value?.noCallApiFn(key, false)
+  } else if (typeof key === 'object') {
+    const { source, reload = false } = key
+    collectElement.value?.noCallApiFn(source, reload)
+  }
+}
+
+// 查看原文
+const originLinkElement = ref(null)
+function doOpenOriginLink() {
+  originLinkElement.value?.doGetLinkAction()
+}
+
+// 客户推荐
+function doOpenMore(key) {
+  switch (key) {
+    case 'recommendCustomers': {
+      sessionStorage.setItem(
+        'potential_cor_list_search',
+        JSON.stringify(
+          ContentExpandsModel.value.recommendCustomers?.search || {}
+        )
+      )
+      doOpenCorListPage({
+        mark: 1
+      })
+      break
+    }
+  }
+}
+
+// 是否有项目进度
+const hasProject = computed(() => {
+  return ContentExpandsModel.value.projectProgress.name
+})
+
+// 联系客服
+const that = getCurrentInstance().proxy
+function doOpenCustomer() {
+  that?.contactCustomer(that)
+}
+// 是否显示遮罩
+const canShowMask = computed(() => {
+  let type = 'free-max'
+  if (ContentModel.value.isNiJian) {
+    type = 'proposed'
+  } else if (ContentModel.value.isCaigouyixiang) {
+    type = 'purchase'
+  }
+  return {
+    show: !ContentModel.value.isCanRead,
+    type: type
+  }
+})
+
+const popoverElement = ref(null)
+
+// 招标、采购进度
+const winnerTimeLineList = computed(() => {
+  return ContentExpandsModel.value.recommendWinners.list.map((v) => {
+    const tag = v._o.subtype || v._o.toptype || ''
+    return {
+      content: replaceKeyword(
+        v.title,
+        ContentExpandsModel.value.recommendWinners.name,
+        ['<span class="highlight-text">', '</span>']
+      ),
+      id: v.id,
+      s_id: v.id,
+      tags: tag ? [tag] : [],
+      time: v?.time ? dateFromNow(v?.time) : '',
+      isActive: v.isActive
+    }
+  })
+})
+
+const popoverElementType = computed(() => {
+  if (elementState.value.className.indexOf('project-name') !== -1) {
+    return 'project-name'
+  }
+  if (elementState.value.className.indexOf('winner-name') !== -1) {
+    return 'winner-name'
+  }
+  if (
+    elementState.value.className.indexOf('third-party-verify-button') !== -1
+  ) {
+    return 'third-verify'
+  }
+  return ''
+})
+
+// 项目名称、企业名称悬浮窗口
+const popoverTimeLine = computed(() => {
+  if (popoverElementType.value === 'project-name') {
+    return {
+      width: '720',
+      contentType: 'time-line',
+      title: '项目公告',
+      list: timeLineList.value
+    }
+  }
+  if (
+    elementState.value.text ===
+      ContentExpandsModel.value.recommendWinners.name &&
+    popoverElementType.value === 'winner-name'
+  ) {
+    return {
+      width: '720',
+      contentType: 'time-line',
+      title: '企业最新信息',
+      list: winnerTimeLineList.value
+    }
+  }
+  if (popoverElementType.value === 'third-verify') {
+    return {
+      width: '623',
+      contentType: 'third-verify',
+      title: '',
+      list: []
+    }
+  }
+  return {
+    width: '',
+    contentType: '',
+    title: '',
+    list: []
+  }
+})
+
+const { elementState, useElementListener } = useHoverHighlightTextPopover({
+  parentSelector: '.content-main-container',
+  hasClass: 'keyword-underline',
+  onCustomClass: (className) => {
+    // 项目名称
+    if (className.indexOf('keyword-underline') !== -1) {
+      return true
+    }
+    // 认证服务
+    if (className.indexOf('third-party-verify-button') !== -1) {
+      return true
+    }
+    return false
+  },
+  onChangeHover: debounce((isHover) => {
+    popoverElement.value.doChangePopover(isHover)
+  }, 150),
+  onClick: () => {
+    if (popoverElementType.value === 'project-name') {
+      doOpenProjectDetailPage({ id: ContentId.value })
+    }
+    if (popoverElementType.value === 'winner-name') {
+      if (elementState.value.winnerId) {
+        doOpenWinnerPage({ id: elementState.value.winnerId })
+      }
+    }
+  }
+})
+
+// 事件延迟绑定
+const canAddContentMainEvent = computed(() => {
+  const result = !ContentPageLoading.value && tabContentShow.value['公告正文']
+  if (result) {
+    useElementListener()
+  }
+  return result
+})
+
+// 免费查看
+function doClickFreeView() {
+  doOpenCollectDialog('peugeot_view_infor')
+}
+</script>
+<template>
+  <ContentLayout :need-ad="true" class="article-page-container">
+    <el-skeleton
+      class="default-article-container"
+      :loading="ContentPageLoading"
+      animated
+      :throttle="500"
+    >
+      <template slot="template">
+        <div class="article-container">
+          <content-header-skeleton />
+        </div>
+      </template>
+      <template>
+        <div
+          v-if="!ContentPageLoading"
+          class="article-container"
+          :class="{ 'show-underline': hasProject }"
+        >
+          <!--  标题  -->
+          <content-header></content-header>
+          <!--  无权益遮罩  -->
+          <content-mask
+            v-if="canShowMask.show"
+            :type="canShowMask.type"
+            @doOpenCollect="doOpenCollectDialog"
+            @doOpenCustomer="doOpenCustomer"
+          ></content-mask>
+
+          <div v-if="!canShowMask.show">
+            <!--  顶部提示 -->
+            <div class="content-header-tip" v-if="ContentModel.isSelfSite">
+              <span>
+                该公告由业主方/采购单位直接发布,急寻供应商,立即报名直接联系业主方/采购单位参与投标采购。
+              </span>
+              <el-button
+                class="action-button"
+                @click="doOpenCollectDialog('peugeot_supplier_regist')"
+                >立即报名</el-button
+              >
+            </div>
+            <!--  Tab + 吸顶 Tab  -->
+            <div class="watch-tab-header">
+              <div class="content-tabs-fixed">
+                <div
+                  class="content-tab-label"
+                  :class="{ 'is-active': item.label === activeContentTab }"
+                  v-for="item in tabContentHeaderList"
+                  :key="item.label"
+                  @click="doSelectTab(item)"
+                >
+                  {{ item.label }}
+                </div>
+              </div>
+              <div
+                class="content-tabs-fixed is-fixed-top"
+                v-show="isFixedHeader"
+              >
+                <div
+                  class="content-tab-label"
+                  :class="{ 'is-active': item.label === activeContentTab }"
+                  v-for="item in tabContentHeaderList"
+                  :key="item.label"
+                  @click="doSelectTab(item)"
+                >
+                  {{ item.label }}
+                </div>
+              </div>
+            </div>
+            <!--  摘要 + 正文  -->
+            <div class="first-content-container">
+              <div
+                class="content-card watch-tab-content"
+                name="公告摘要"
+                v-if="tabContentShow['公告摘要']"
+              >
+                <content-summary />
+                <recommend-ent />
+              </div>
+
+              <div
+                class="content-card watch-tab-content"
+                name="公告正文"
+                v-if="tabContentShow['公告正文']"
+              >
+                <div
+                  class="content-main-container"
+                  :data-content="canAddContentMainEvent"
+                >
+                  <div class="content-block-header">公告正文</div>
+                  <div
+                    class="content-detail-container"
+                    v-event-listener:click="doClickFreeView"
+                    data-event-selector=".free-view"
+                  >
+                    <pre v-html="ContentModel.contentHighlighted"></pre>
+                  </div>
+
+                  <el-button
+                    v-if="ContentModel.originalShow"
+                    class="origin-detail-action"
+                    @click="doOpenOriginLink"
+                  >
+                    <span class="iconfont icon-chakanyuanwen"></span>
+                    查看原文链接
+                  </el-button>
+
+                  <div>
+                    <attachment-download
+                      :id="ContentId"
+                      :title="ContentModel.title"
+                      :attachment-list="ContentModel.attachments"
+                      @doOpenCollect="doOpenCollectDialog"
+                    ></attachment-download>
+                  </div>
+
+                  <Reward />
+                </div>
+              </div>
+            </div>
+            <!--  投标服务  -->
+            <div
+              class="content-card watch-tab-content"
+              name="投标服务"
+              v-loading="ContentPageExpandsLoading"
+              v-if="tabContentShow['投标服务']"
+            >
+              <div class="content-block-header">投标服务</div>
+              <recommend-serves
+                @open-collect="doOpenCollectDialog"
+                @click-view-origin="doOpenOriginLink"
+              />
+            </div>
+            <!--  商机推荐  -->
+            <div
+              class="content-card watch-tab-content"
+              name="商机推荐"
+              v-loading="ContentPageExpandsLoading"
+              v-if="tabContentShow['商机推荐']"
+            >
+              <div class="content-block-header">商机推荐</div>
+              <recommend-opportunities></recommend-opportunities>
+            </div>
+            <!--  客户推荐  -->
+            <div
+              class="content-card watch-tab-content"
+              name="客户推荐"
+              v-loading="ContentPageExpandsLoading"
+              v-if="!ContentPageExpandsLoading && tabContentShow['客户推荐']"
+            >
+              <div class="flex flex-(row items-center justify-between)">
+                <div class="content-block-header">客户推荐</div>
+                <span
+                  class="more-text"
+                  v-if="ContentExpandsModel.recommendCustomers.more"
+                  @click="doOpenMore('recommendCustomers')"
+                >
+                  查看更多 <i class="iconfont icon-more"></i>
+                </span>
+              </div>
+              <recommend-customers-list
+                @doHide="doHideTabContent('客户推荐')"
+              />
+            </div>
+          </div>
+          <!--  评分  -->
+          <Nps></Nps>
+          <!--  内容底部广告  -->
+          <div class="article-content-footer-container">
+            <adsense code="jy-pccontent-bottom"></adsense>
+          </div>
+        </div>
+      </template>
+    </el-skeleton>
+    <!--  hover 项目、企业名称时展示 popover 最新标讯  -->
+    <pover-time-line
+      class="article-content-popover"
+      trigger="manual"
+      poperClass="poverStep"
+      poperPlacement="bottom"
+      :title="popoverTimeLine.title"
+      :custom-event="true"
+      @open="doOpenArticlePage"
+      :stepList="popoverTimeLine.list"
+      :poper-width="popoverTimeLine.width"
+      ref="popoverElement"
+    >
+      <div slot="content" :style="elementState.style"></div>
+      <div slot="main" v-if="popoverTimeLine.contentType === 'third-verify'">
+        <!--  认证服务悬浮弹窗  -->
+        <content-third-popover
+          @open-collect="doOpenCollectDialog"
+        ></content-third-popover>
+      </div>
+    </pover-time-line>
+    <!--  留资弹窗  -->
+    <collect-info ref="collectElement"></collect-info>
+    <!--  底部悬浮广告  -->
+    <footer-ad
+      v-if="!canShowMask.show"
+      code="jy-pc-article-bottom-fixed"
+    ></footer-ad>
+    <!--  查看原文链接  -->
+    <origin-link
+      v-if="!canShowMask.show"
+      ref="originLinkElement"
+      :id="ContentId"
+      @click-collect="doOpenCollectDialog"
+    ></origin-link>
+    <template #right-top v-if="timeLineList.length > 0">
+      <!--  招标/采购进度  -->
+      <content-right-time-line
+        :content-id="ContentId"
+        :time-line-list="timeLineList"
+        @open="doOpenArticlePage"
+      ></content-right-time-line>
+    </template>
+    <!--  in-web 容器时右侧注入侧边栏  -->
+    <template #right-main v-if="InWhichContainer === 'in-web'">
+      <div id="inWebRightElement"></div>
+    </template>
+  </ContentLayout>
+</template>
+
+<style lang="scss">
+// 兼容不同容器
+.in-app .article-page-container.v-w1200 {
+  width: 1200px;
+}
+.article-page-container.v-w1200 {
+  margin-top: 32px;
+}
+.in-app .article-page-container.v-w1200 {
+  .content-container {
+    &.calc {
+      .content-main {
+        display: block;
+        width: calc(100% - 200px);
+        margin: 0 auto;
+      }
+    }
+  }
+  .content-right {
+    flex-shrink: 0;
+    width: 200px;
+    margin-left: 16px;
+  }
+}
+</style>
+<style lang="scss" scoped>
+@import 'src/assets/style/page/article.scss';
+</style>

Энэ ялгаанд хэт олон файл өөрчлөгдсөн тул зарим файлыг харуулаагүй болно