Pārlūkot izejas kodu

feat: 移动端标讯详情页数据对接

cuiyalong 1 gadu atpakaļ
vecāks
revīzija
61761928f9
24 mainītis faili ar 1329 papildinājumiem un 396 dzēšanām
  1. 12 0
      apps/mobile/src/api/modules/bigmember.js
  2. BIN
      apps/mobile/src/assets/image/icon/big-member/icon-ai.png
  3. 8 0
      apps/mobile/src/assets/style/pic-icon.scss
  4. 10 0
      apps/mobile/src/data/links.js
  5. 0 1
      apps/mobile/src/utils/mixins/modules/points.js
  6. 36 14
      apps/mobile/src/views/article/components/AbstractEnt.vue
  7. 23 6
      apps/mobile/src/views/article/components/ActionCollection.vue
  8. 7 1
      apps/mobile/src/views/article/components/ActionInBidding.vue
  9. 0 1
      apps/mobile/src/views/article/components/ActionShareToWorkmate.vue
  10. 76 0
      apps/mobile/src/views/article/components/AttachmentDownloadCard.vue
  11. 192 49
      apps/mobile/src/views/article/components/ContentAbstract.vue
  12. 87 7
      apps/mobile/src/views/article/components/ContentAbstractEntList.vue
  13. 158 46
      apps/mobile/src/views/article/components/ContentCustomerRecommend.vue
  14. 151 0
      apps/mobile/src/views/article/components/ContentHeader.vue
  15. 72 0
      apps/mobile/src/views/article/components/ContentHeaderBannerTip.vue
  16. 75 63
      apps/mobile/src/views/article/components/ContentMainText.vue
  17. 66 33
      apps/mobile/src/views/article/components/ContentProjectTimeline.vue
  18. 16 5
      apps/mobile/src/views/article/components/DataExportBanner.vue
  19. 27 8
      apps/mobile/src/views/article/components/OriginLink.vue
  20. 43 37
      apps/mobile/src/views/article/components/TabActions.vue
  21. 164 117
      apps/mobile/src/views/article/content.vue
  22. 94 0
      apps/mobile/src/views/article/ui/FollowPopoverContentCard.vue
  23. 2 1
      apps/mobile/src/views/article/ui/MaskCard.vue
  24. 10 7
      apps/mobile/src/views/article/ui/ServiceIntroCard.vue

+ 12 - 0
apps/mobile/src/api/modules/bigmember.js

@@ -117,3 +117,15 @@ export function projectDetailApi(data) {
     data
   })
 }
+
+// 获取采购 buyer、 中标 winner 企业基础画像信息
+export function ajaxGetMiniEntInfo(type, id) {
+  return request({
+    url: `/bigmember/portrait/${type}/miniData`,
+    method: 'post',
+    noToast: true,
+    data: qs.stringify({
+      [type === 'buyer' ? 'buyer' : 'entId']: id
+    })
+  })
+}

BIN
apps/mobile/src/assets/image/icon/big-member/icon-ai.png


+ 8 - 0
apps/mobile/src/assets/style/pic-icon.scss

@@ -3,6 +3,10 @@
   width: 20px;
   height: 20px;
 }
+.j-base-icon {
+  background-repeat: no-repeat;
+  background-size: contain;
+}
 
 .wh24 {
   width: 24px;
@@ -173,3 +177,7 @@
   background-image: url(@/assets/image/icon/ent/ent-info@2x.png);
   background-size: contain;
 }
+
+.icon-ai {
+  background-image: url(@/assets/image/icon/big-member/icon-ai.png);
+}

+ 10 - 0
apps/mobile/src/data/links.js

@@ -5,6 +5,11 @@
  */
 
 export const LINKS = {
+  下载APP: {
+    app: '',
+    h5: 'https://wx.jianyu360.cn/front/downloadapppage/normal', // 需要参数 ?source=H5 / source=WX
+    wx: 'https://wx.jianyu360.cn/front/downloadapppage/normal'
+  },
   客服: {
     app: '/jyapp/free/customer',
     h5: '/jyapp/free/customer',
@@ -152,6 +157,11 @@ export const LINKS = {
     h5: '/jyapp/big/page/landingPage',
     wx: '/big/wx/page/landingPage'
   },
+  投标决策分析筛选页面: {
+    app: '/jyapp/big/page/analysis_result',
+    h5: '/jyapp/big/page/analysis_result',
+    wx: ''
+  },
   大会员首页: {
     app: '/jyapp/big/page/main_root',
     h5: '/jyapp/big/page/main_root',

+ 0 - 1
apps/mobile/src/utils/mixins/modules/points.js

@@ -1,4 +1,3 @@
-console.log(222)
 // 是剑鱼币任务跳转进入页面,路由判断参数跳转
 export const mixinPoints = {
   data() {

+ 36 - 14
apps/mobile/src/views/article/components/AbstractEnt.vue

@@ -1,5 +1,9 @@
 <template>
-  <section class="abstract-ent-container clickable bg-white">
+  <section
+    class="abstract-ent-container bg-white"
+    :class="{ clickable: isLink }"
+    v-on="$listeners"
+  >
     <div class="abstract-ent-title-container flex">
       <span class="j-tag-item border round blue" v-if="entType === 'winner'">
         中标单位画像
@@ -8,7 +12,7 @@
         采购单位画像
       </span>
       <p class="abstract-ent-title van-ellipsis flex-1">
-        大通回族土族自治县住房和城乡建设局
+        {{ title }}
       </p>
     </div>
     <div
@@ -16,18 +20,24 @@
     >
       <div class="abstract-ent-info label-value">
         <span class="ent-info-label">联系人</span>
-        <span class="ent-info-value"><i class="value-number">20</i> 个</span>
+        <span class="ent-info-value">
+          <i class="value-number">{{ contactCount }}</i> 个
+        </span>
       </div>
       <div class="abstract-ent-info label-value">
         <span class="ent-info-label">合作企业</span>
-        <span class="ent-info-value"><i class="value-number">20</i> 个</span>
-      </div>
-      <div class="abstract-ent-info more value-number highlight-text">
-        <span class="ent-info-label">查看详情</span>
         <span class="ent-info-value">
-          <van-icon name="arrow" />
+          <i class="value-number">{{ cooperateCount }}</i> 个
         </span>
       </div>
+      <div class="abstract-ent-info more value-number highlight-text">
+        <template v-if="isLink">
+          <span class="ent-info-label">查看详情</span>
+          <span class="ent-info-value">
+            <van-icon name="arrow" />
+          </span>
+        </template>
+      </div>
     </div>
   </section>
 </template>
@@ -46,13 +56,24 @@ export default {
       validator(value) {
         return ['buyer', 'winner'].includes(value)
       }
+    },
+    title: {
+      type: String,
+      default: ''
+    },
+    contactCount: {
+      type: [String, Number],
+      default: 0
+    },
+    cooperateCount: {
+      type: [String, Number],
+      default: 0
+    },
+    isLink: {
+      type: Boolean,
+      default: false
     }
-  },
-  data() {
-    return {}
-  },
-  created() {},
-  methods: {}
+  }
 }
 </script>
 
@@ -71,6 +92,7 @@ export default {
   margin-top: 8px;
 }
 .abstract-ent-info {
+  min-width: 66px;
   font-size: 13px;
   line-height: 20px;
   .value-number {

+ 23 - 6
apps/mobile/src/views/article/components/ActionCollection.vue

@@ -17,6 +17,7 @@
 <script>
 import TabActionItem from '@/views/article/ui/TabActionItem.vue'
 import { AppIcon } from '@/ui'
+import { checkArticleIsCollect } from '@/api/modules/'
 
 export default {
   name: 'ActionCollection',
@@ -30,10 +31,10 @@ export default {
       default: '',
       required: true
     },
-    value: {
-      type: Boolean,
-      default: false
-    },
+    // value: {
+    //   type: Boolean,
+    //   default: false
+    // },
     direction: {
       type: String,
       default: 'column',
@@ -44,8 +45,23 @@ export default {
     beforeAction: Function,
     beforeRedirect: Function
   },
-  created() {},
+  data() {
+    return {
+      value: false
+    }
+  },
+  created() {
+    this.checkArticleIsColl()
+  },
   methods: {
+    checkArticleIsColl() {
+      if (!this.id) return
+      checkArticleIsCollect({ bids: this.id, label: 'info' }).then((res) => {
+        if (res.data && res.error_code === 0) {
+          this.syncValue(res.data.iscoll)
+        }
+      })
+    },
     // 收藏
     async doCollection() {
       if (this.beforeAction) {
@@ -72,7 +88,8 @@ export default {
       })
     },
     syncValue(v) {
-      this.$emit('input', v)
+      this.value = v
+      // this.$emit('input', v)
     }
   }
 }

+ 7 - 1
apps/mobile/src/views/article/components/ActionInBidding.vue

@@ -107,4 +107,10 @@ export default {
 }
 </script>
 
-<style lang="scss" scoped></style>
+<style lang="scss" scoped>
+::v-deep {
+  .tab-action-item {
+    height: 100%;
+  }
+}
+</style>

+ 0 - 1
apps/mobile/src/views/article/components/ActionShareToWorkmate.vue

@@ -57,7 +57,6 @@ export default {
     }
   },
   created() {
-    window.t = this
     window.__shareWork = this.onCallback.bind(this)
   },
   mounted() {

+ 76 - 0
apps/mobile/src/views/article/components/AttachmentDownloadCard.vue

@@ -0,0 +1,76 @@
+<template>
+  <section class="attachment-download-container">
+    <div class="others-header flex flex-(items-center justify-between)">
+      <div class="content-file-attachment-left flex flex-items-center">
+        <span class="j-icon icon-data-download"></span>
+        <span class="file-attachment-text">附件下载</span>
+        <span class="j-tag-item main round attachment-tag">本月剩余:8个</span>
+      </div>
+      <div class="content-file-attachment-actions">
+        <span class="charge-button">立即充值</span>
+      </div>
+    </div>
+    <div class="file-attachment-list" v-if="content.attachments?.length">
+      <div
+        class="file-attachment-item highlight-text underline clickable"
+        v-for="(attach, index) in content.attachments"
+        :key="index"
+      >
+        {{ attach.fileName }}
+      </div>
+    </div>
+  </section>
+</template>
+<script>
+import { Icon } from 'vant'
+import { mapState } from 'vuex'
+import { LINKS } from '@/data'
+import { openAppOrWxPage } from '@/utils/'
+
+export default {
+  name: 'AttachmentDownloadCard',
+  components: {
+    [Icon.name]: Icon
+  },
+  data() {
+    return {}
+  },
+  computed: {
+    ...mapState({
+      content: (state) => state.article.mainModel.content
+    })
+  },
+  created() {},
+  methods: {}
+}
+</script>
+
+<style lang="scss" scoped>
+.file-attachment-text {
+  margin: 0 12px;
+  font-size: 16px;
+  line-height: 24px;
+}
+.attachment-tag {
+  padding: 0 8px;
+  font-size: 11px;
+  line-height: 18px;
+  border-radius: 10px;
+}
+.charge-button {
+  padding: 3px 8px;
+  font-size: 12px;
+  line-height: 18px;
+  border-radius: 8px;
+  color: $white;
+  background-color: $main;
+}
+.file-attachment-list {
+  margin-top: 16px;
+  font-size: 15px;
+  line-height: 22px;
+  .file-attachment-item {
+    margin-bottom: 16px;
+  }
+}
+</style>

+ 192 - 49
apps/mobile/src/views/article/components/ContentAbstract.vue

@@ -1,12 +1,12 @@
 <template>
   <section class="abstract-container bg-white">
     <!-- 超前项目 -->
-    <template v-if="futureProject">
+    <template v-if="summary.isProposed">
       <div class="abstract-line">
         <div class="abstract-line-label">项目名称</div>
         <div class="abstract-line-value">
           <span class="abstract-line-value-text">
-            江西省长征医院医疗管理系统更新升级项目
+            {{ summaryMap.projectName?.value }}
           </span>
         </div>
       </div>
@@ -14,33 +14,48 @@
         <div class="abstract-line">
           <div class="abstract-line-label">省份</div>
           <div class="abstract-line-value">
-            <span class="abstract-line-value-text">江西</span>
+            <span class="abstract-line-value-text">
+              {{ summaryMap.area?.value }}
+            </span>
           </div>
         </div>
         <div class="abstract-line">
           <div class="abstract-line-label">业主类型</div>
           <div class="abstract-line-value">
-            <span class="abstract-line-value-text">信息技术</span>
+            <span class="abstract-line-value-text">
+              {{ summaryMap.buyerClass?.value }}
+            </span>
           </div>
         </div>
       </div>
       <div class="abstract-line">
         <div class="abstract-line-label">业主单位</div>
         <div class="abstract-line-value">
-          <span class="highlight-text underline clickable">江西省长征医院</span>
+          <span
+            :class="{
+              'highlight-text underline clickable': summaryMap.buyer?.link
+            }"
+            @click="goToBuyerPortrait(summaryMap.buyer, 'link')"
+          >
+            {{ summaryMap.buyer?.value }}
+          </span>
         </div>
       </div>
       <div class="abstract-line-container">
         <div class="abstract-line">
           <div class="abstract-line-label">总投资</div>
           <div class="abstract-line-value">
-            <span class="abstract-line-value-text">1182万元</span>
+            <span class="abstract-line-value-text">
+              {{ summaryMap.totalInvestment?.value }}
+            </span>
           </div>
         </div>
         <div class="abstract-line">
           <div class="abstract-line-label">建设年份</div>
           <div class="abstract-line-value">
-            <span class="abstract-line-value-text">xxx</span>
+            <span class="abstract-line-value-text">
+              {{ summaryMap.projectPeriod?.value }}
+            </span>
           </div>
         </div>
       </div>
@@ -48,7 +63,7 @@
         <div class="abstract-line-label">建设地点</div>
         <div class="abstract-line-value">
           <span class="abstract-line-value-text">
-            江西省XXXXXXXXXXX嘻嘻嘻嘻嘻嘻嘻
+            {{ summaryMap.address?.value }}
           </span>
         </div>
       </div>
@@ -56,7 +71,7 @@
         <div class="abstract-line-label">审批机关</div>
         <div class="abstract-line-value">
           <span class="abstract-line-value-text">
-            江西省XXXXXXXXXXX嘻嘻嘻嘻嘻嘻嘻2
+            {{ summaryMap.approveDept?.value }}
           </span>
         </div>
       </div>
@@ -64,7 +79,7 @@
         <div class="abstract-line-label">审批事项</div>
         <div class="abstract-line-value">
           <span class="abstract-line-value-text">
-            江西省XXXXXXXXXXX嘻嘻嘻嘻嘻嘻嘻2
+            {{ summaryMap.approveContent?.value }}
           </span>
         </div>
       </div>
@@ -72,7 +87,7 @@
         <div class="abstract-line-label">批准文号</div>
         <div class="abstract-line-value">
           <span class="abstract-line-value-text">
-            江西省XXXXXXXXXXX嘻嘻嘻嘻嘻嘻嘻2
+            {{ summaryMap.approvalNumber?.value }}
           </span>
         </div>
       </div>
@@ -80,13 +95,17 @@
         <div class="abstract-line">
           <div class="abstract-line-label">审批时间</div>
           <div class="abstract-line-value">
-            <span class="abstract-line-value-text">2024-2-2</span>
+            <span class="abstract-line-value-text">
+              {{ summaryMap.approveTime?.value }}
+            </span>
           </div>
         </div>
         <div class="abstract-line">
           <div class="abstract-line-label">审批结果</div>
           <div class="abstract-line-value">
-            <span class="abstract-line-value-text">办结(通过)</span>
+            <span class="abstract-line-value-text">
+              {{ summaryMap.approveStatus?.value }}
+            </span>
           </div>
         </div>
       </div>
@@ -94,7 +113,7 @@
         <div class="abstract-line-label">建设内容</div>
         <div class="abstract-line-value">
           <span class="abstract-line-value-text">
-            江西省XXXXXXXXXXX嘻嘻嘻嘻嘻嘻嘻2
+            {{ summaryMap.content?.value }}
           </span>
         </div>
       </div>
@@ -104,19 +123,38 @@
       <div class="abstract-line">
         <div class="abstract-line-label">采购单位</div>
         <div class="abstract-line-value">
-          <span class="highlight-text underline clickable">
-            122222213333333333322222222222
+          <span
+            :class="{
+              'highlight-text underline clickable': summaryMap.buyer?.link
+            }"
+            @click="goToBuyerPortrait(summaryMap.buyer, 'link')"
+          >
+            {{ summaryMap.buyer?.value }}
           </span>
         </div>
       </div>
       <div class="abstract-line">
         <div class="abstract-line-label">采购联系人 / 联系电话</div>
         <div class="abstract-line-value">
-          <span class="abstract-line-value-text">xx / 13283800000</span>
-          <span class="abstract-line-value-actions clickable color-main">
-            <span class="j-icon icon-phone-blue"></span>
-            <span class="a-l-v-a-text">更多联系人</span>
-            <van-icon name="arrow" />
+          <span class="abstract-line-value-text">
+            {{ summaryMap.buyerContactInfo?.value }}
+          </span>
+          <span class="abstract-line-value-actions color-main">
+            <span
+              class="abstract-line-value-action call-phone clickable"
+              v-if="summaryMap.buyerContactInfo?.tel"
+              @click="doCallPhone(summaryMap.buyerContactInfo?.tel)"
+            >
+              <span class="j-icon icon-phone-blue"></span>
+            </span>
+            <span
+              class="abstract-line-value-action more-tel clickable"
+              v-if="summaryMap.buyer?.link && summaryMap.buyerContactInfo?.tel"
+              @click="goToBuyerPortrait(summaryMap.buyer, 'tel')"
+            >
+              <span class="a-l-v-a-text">更多联系人</span>
+              <van-icon name="arrow" />
+            </span>
           </span>
         </div>
       </div>
@@ -124,18 +162,31 @@
         <div class="abstract-line-label">招标代理机构</div>
         <div class="abstract-line-value">
           <span class="abstract-line-value-text">
-            大通回族土族自治县住房和城XXXXX
+            {{ summaryMap.agency?.value }}
           </span>
         </div>
       </div>
       <div class="abstract-line">
         <div class="abstract-line-label">代理联系人 / 联系电话</div>
         <div class="abstract-line-value">
-          <span class="abstract-line-value-text">xx / 13283800000</span>
-          <span class="abstract-line-value-actions clickable color-main">
-            <span class="j-icon icon-phone-blue"></span>
-            <span class="a-l-v-a-text">更多联系人</span>
-            <van-icon name="arrow" />
+          <span class="abstract-line-value-text">
+            {{ summaryMap.agencyContactInfo?.value }}
+          </span>
+          <span class="abstract-line-value-actions color-main">
+            <span
+              class="abstract-line-value-action call-phone clickable"
+              v-if="summaryMap.agencyContactInfo?.tel"
+              @click="doCallPhone(summaryMap.agencyContactInfo?.tel)"
+            >
+              <span class="j-icon icon-phone-blue"></span>
+            </span>
+            <span
+              class="abstract-line-value-action more-tel clickable"
+              v-if="summaryMap.buyer?.link && summaryMap.agencyContactInfo?.tel"
+            >
+              <span class="a-l-v-a-text">更多联系人</span>
+              <van-icon name="arrow" />
+            </span>
           </span>
         </div>
       </div>
@@ -143,39 +194,67 @@
         <div class="abstract-line">
           <div class="abstract-line-label">报名截止日期</div>
           <div class="abstract-line-value">
-            <span class="abstract-line-value-text">2023-09-23</span>
+            <span class="abstract-line-value-text">
+              {{ summaryMap.signEndTime?.value }}
+            </span>
           </div>
         </div>
         <div class="abstract-line">
           <div class="abstract-line-label">投标截止日期</div>
           <div class="abstract-line-value">
-            <span class="abstract-line-value-text">2023-10-23</span>
+            <span class="abstract-line-value-text">
+              {{ summaryMap.bidEndTime?.value }}
+            </span>
           </div>
         </div>
       </div>
-      <div class="abstract-line">
-        <div class="abstract-line-label">中标单位</div>
-        <div class="abstract-line-value">
-          <span class="highlight-text underline clickable">
-            122222213333333333322222222222
-          </span>
+      <section v-for="(winner, index) in summary.winners || []" :key="index">
+        <div class="abstract-line">
+          <div class="abstract-line-label">中标单位</div>
+          <div class="abstract-line-value">
+            <span
+              :class="{
+                'highlight-text': winner.id,
+                underline: winner.id,
+                clickable: winner.id
+              }"
+            >
+              {{ winner.name }}
+            </span>
+          </div>
         </div>
-      </div>
-      <div class="abstract-line">
-        <div class="abstract-line-label">中标联系人 / 联系电话</div>
-        <div class="abstract-line-value">
-          <span class="abstract-line-value-text">xx / 13283800000</span>
-          <span class="abstract-line-value-actions clickable color-main">
-            <span class="j-icon icon-phone-blue"></span>
-            <span class="a-l-v-a-text">更多联系人</span>
-            <van-icon name="arrow" />
-          </span>
+        <div class="abstract-line">
+          <div class="abstract-line-label">中标联系人 / 联系电话</div>
+          <div class="abstract-line-value">
+            <span class="abstract-line-value-text">{{
+              winner?.personTel?.value
+            }}</span>
+            <span class="abstract-line-value-actions color-main">
+              <span
+                class="abstract-line-value-action call-phone clickable"
+                v-if="winner?.personTel?.tel"
+                @click="doCallPhone(winner?.personTel?.tel)"
+              >
+                <span class="j-icon icon-phone-blue"></span>
+              </span>
+              <span
+                class="abstract-line-value-action more-tel clickable"
+                v-if="winner.id && winner?.personTel?.tel"
+                @click="goToEntPortrait(winner, 'tel')"
+              >
+                <span class="a-l-v-a-text">更多联系人</span>
+                <van-icon name="arrow" />
+              </span>
+            </span>
+          </div>
         </div>
-      </div>
+      </section>
       <div class="abstract-line">
         <div class="abstract-line-label">中标金额(元)</div>
         <div class="abstract-line-value">
-          <span class="abstract-line-value-text">1,314</span>
+          <span class="abstract-line-value-text">
+            {{ summaryMap.bidAmount?.value || ' ' }}
+          </span>
         </div>
       </div>
     </template>
@@ -188,6 +267,11 @@
 </template>
 <script>
 import { Icon } from 'vant'
+import { mapState } from 'vuex'
+import { LINKS } from '@/data'
+import { openAppOrWxPage } from '@/utils/'
+import { callPhone } from '@/utils/callFn'
+
 export default {
   name: 'ContentAbstract',
   components: {
@@ -202,9 +286,60 @@ export default {
   data() {
     return {}
   },
+  computed: {
+    ...mapState({
+      summary: (state) => state.article.mainModel.summary
+    }),
+    summaryList() {
+      return this.summary.list
+    },
+    summaryMap() {
+      const map = {}
+      this.summaryList.forEach((s) => {
+        if (s.key) {
+          map[s.key] = s
+        }
+      })
+      return map
+    }
+  },
   created() {},
   methods: {
-    concatKf() {}
+    goToBuyerPortrait(item, anchor) {
+      if (anchor === 'link') {
+        if (!item.link) {
+          return
+        }
+      }
+      openAppOrWxPage(LINKS.大会员超级订阅采购单位画像页面, {
+        query: {
+          entName: encodeURIComponent(item.value),
+          anchor: anchor
+        }
+      })
+    },
+    goToEntPortrait(winner, anchor) {
+      if (!winner.id) {
+        return
+      }
+      openAppOrWxPage(LINKS.企业画像页面, {
+        query: {
+          eId: winner.id,
+          anchor: anchor
+        }
+      })
+    },
+    doCallPhone(tel) {
+      if (!tel) return
+      callPhone(tel)
+    },
+    concatKf() {
+      openAppOrWxPage(LINKS.客服, {
+        query: {
+          from: encodeURIComponent(location.href)
+        }
+      })
+    }
   }
 }
 </script>
@@ -236,16 +371,24 @@ export default {
     display: flex;
     align-items: center;
     justify-content: space-between;
+    min-height: 20px;
     color: #171826;
   }
   .abstract-line-value-actions {
     display: flex;
     align-items: center;
+    .abstract-line-value-action {
+      display: flex;
+      align-items: center;
+    }
     &.color-main {
       color: $main;
     }
+    .more-tel {
+      margin-left: 8px;
+    }
     .a-l-v-a-text {
-      margin: 0 8px;
+      margin-right: 8px;
     }
   }
 }

+ 87 - 7
apps/mobile/src/views/article/components/ContentAbstractEntList.vue

@@ -1,28 +1,108 @@
 <template>
   <section class="abstract-ent-list">
-    <AbstractEnt class="abstract-ent-item"></AbstractEnt>
-    <AbstractEnt class="abstract-ent-item"></AbstractEnt>
-    <AbstractEnt class="abstract-ent-item"></AbstractEnt>
+    <AbstractEnt
+      class="abstract-ent-item"
+      v-for="(ent, index) in entDetailList"
+      :entType="ent.type"
+      :title="ent.name"
+      :key="index"
+      :isLink="ent.link"
+      :contactCount="ent?.detail?.contactCount"
+      :cooperateCount="ent?.detail?.cooperate"
+      @click="toDetail(ent)"
+    ></AbstractEnt>
   </section>
 </template>
 <script>
 import AbstractEnt from '@/views/article/components/AbstractEnt.vue'
+import { mapState } from 'vuex'
+import { LINKS } from '@/data'
+import { openAppOrWxPage } from '@/utils/'
+import { ajaxGetMiniEntInfo } from '@/api/modules'
+
 export default {
   name: 'ContentAbstractEntList',
   components: {
     AbstractEnt
   },
   data() {
-    return {}
+    return {
+      req: {}
+    }
+  },
+  computed: {
+    ...mapState({
+      summary: (state) => state.article.mainModel.summary
+    }),
+    entList() {
+      const { winners, buyers } = this.summary
+      return buyers.concat(winners)
+    },
+    entDetailList() {
+      return this.entList.map((e) => {
+        const detail = this.req[e.id] || {}
+        return {
+          ...e,
+          detail
+        }
+      })
+    }
   },
-  created() {},
-  methods: {}
+  watch: {
+    entList(eList) {
+      if (Array.isArray(eList) && eList.length > 0) {
+        eList.forEach((ent) => {
+          this.getEntInfo(ent.type, ent.id)
+        })
+      }
+    }
+  },
+  methods: {
+    async getEntInfo(type, id) {
+      const { data, error_code: code } = await ajaxGetMiniEntInfo(type, id)
+      if (code === 0 && data) {
+        data._type = type
+        this.$set(this.req, id, data)
+      }
+    },
+    toDetail(ent) {
+      if (!ent.link) {
+        return
+      }
+      if (ent.type === 'buyer') {
+        this.goToBuyerPortrait(ent)
+      } else {
+        this.goToEntPortrait(ent)
+      }
+    },
+    goToBuyerPortrait(item) {
+      if (!item.link) {
+        return
+      }
+      openAppOrWxPage(LINKS.大会员超级订阅采购单位画像页面, {
+        query: {
+          entName: encodeURIComponent(item.value)
+        }
+      })
+    },
+    goToEntPortrait(winner) {
+      if (!winner.id) {
+        return
+      }
+      openAppOrWxPage(LINKS.企业画像页面, {
+        query: {
+          eId: winner.id
+        }
+      })
+    }
+  }
 }
 </script>
 
 <style lang="scss" scoped>
 .abstract-ent-list {
-  padding: 8px 12px;
+  margin: 8px 0;
+  padding: 0 12px;
   .abstract-ent-item:not(:last-of-type) {
     margin-bottom: 8px;
   }

+ 158 - 46
apps/mobile/src/views/article/components/ContentCustomerRecommend.vue

@@ -1,71 +1,182 @@
 <template>
-  <ContentModuleCard title="客户推荐" class="content-customer-recommend">
-    <div class="customer-list">
-      <CustomerCell
-        v-for="(item, index) in entList"
-        :key="index"
-        :name="item.name"
-        :location="item.location"
-        :subInfoList="item.subInfoList"
-        v-visited:ent="item.id"
-        @click="toDetail(item)"
+  <ContentModuleCard
+    title="客户推荐"
+    class="content-customer-recommend"
+    v-if="showModule"
+  >
+    <MaskCard v-if="showMask"></MaskCard>
+    <template v-else>
+      <div class="customer-list">
+        <CustomerCell
+          v-for="(item, index) in entList"
+          :key="index"
+          :name="item.name"
+          :location="item.location"
+          :subInfoList="item.subInfoList"
+          v-visited:ent="item.id"
+          @click="toDetail(item)"
+        >
+        </CustomerCell>
+      </div>
+      <div
+        class="show-more clickable bg-white"
+        slot="footer"
+        v-if="showMore"
+        @click="toSeeMore"
       >
-      </CustomerCell>
-    </div>
-    <div class="show-more clickable bg-white" slot="footer">查看更多</div>
+        查看更多
+      </div>
+    </template>
   </ContentModuleCard>
 </template>
 <script>
 import { Icon } from 'vant'
 import { CustomerCell } from '@/ui'
 import ContentModuleCard from '@/views/article/ui/ContentModuleCard.vue'
+import MaskCard from '@/views/article/ui/MaskCard.vue'
+import { mapState, mapGetters } from 'vuex'
+import { LINKS } from '@/data'
+import { openAppOrWxPage, openLinkOfOther } from '@/utils/'
 
 export default {
   name: 'ContentCustomerRecommend',
   components: {
     [Icon.name]: Icon,
     ContentModuleCard,
-    CustomerCell
+    CustomerCell,
+    MaskCard
   },
   data() {
     return {
-      entList: [
-        {
-          id: 'e-1111',
-          name: '中铁电气化局集团有限公司',
-          location: '北京 北京市',
-          subInfoList: [
-            {
-              label: '项目数量',
-              value: '8248'
-            },
-            {
-              label: '项目总金额',
-              value: '24.07亿元'
-            }
-          ]
-        },
-        {
-          id: 'e-1111',
-          name: '中铁电气化局集团有限公司',
-          location: '北京 北京市',
-          subInfoList: [
-            {
-              label: '项目数量',
-              value: '8248'
-            },
-            {
-              label: '项目总金额',
-              value: '24.07亿元'
-            }
-          ]
+      // entList: [
+      //   {
+      //     id: 'e-1111',
+      //     name: '中铁电气化局集团有限公司',
+      //     location: '北京 北京市',
+      //     subInfoList: [
+      //       {
+      //         label: '项目数量',
+      //         value: '8248'
+      //       },
+      //       {
+      //         label: '项目总金额',
+      //         value: '24.07亿元'
+      //       }
+      //     ]
+      //   },
+      //   {
+      //     id: 'e-1111',
+      //     name: '中铁电气化局集团有限公司',
+      //     location: '北京 北京市',
+      //     subInfoList: [
+      //       {
+      //         label: '项目数量',
+      //         value: '8248'
+      //       },
+      //       {
+      //         label: '项目总金额',
+      //         value: '24.07亿元'
+      //       }
+      //     ]
+      //   }
+      // ]
+    }
+  },
+  computed: {
+    ...mapState({
+      recommendCustomers: (state) =>
+        state.article.expandModel?.recommendCustomers || {}
+    }),
+    ...mapGetters('user', ['bigMemberPower', 'isBusiness']),
+    entList() {
+      const list = this.recommendCustomers?.customer || []
+      return list.map((item) => {
+        const { buyer, areaCity } = item
+        const subInfoList = [
+          {
+            label: '项目数量',
+            value: item.projectCount || '-'
+          },
+          {
+            label: '项目总金额',
+            value: item.totalAmountFormatted || '-'
+          }
+        ]
+
+        return {
+          id: buyer,
+          name: buyer,
+          location: areaCity,
+          subInfoList
         }
-      ]
+      })
+    },
+    hasBigMemberPower7() {
+      // 潜在客户挖掘权限。权限7
+      return this.bigMemberPower.includes(7)
+    },
+    showModule() {
+      if (this.hasBigMemberPower7) {
+        return this.entList.length
+      } else {
+        return true
+      }
+    },
+    showMore() {
+      if (this.hasBigMemberPower7) {
+        return this.recommendCustomers?.more
+      } else {
+        return false
+      }
+    },
+    showMask() {
+      return !this.hasBigMemberPower7
     }
   },
+  created() {
+    window.t = this
+  },
   methods: {
     toDetail(item) {
-      console.log(item)
+      const query = {
+        entName: item.id
+      }
+      if (this.isBusiness) {
+        openAppOrWxPage(LINKS.商机管理采购单位画像页面, { query })
+      } else {
+        openAppOrWxPage(LINKS.大会员超级订阅采购单位画像页面, { query })
+      }
+    },
+    toSeeMore() {
+      if (this.$envs.inWX) {
+        this.showDownloadAppDialog()
+      } else {
+        // 带入挖掘条件,业务范围自动添加关键词?
+        openLinkOfOther('/jyapp/big/page/potential_cor_list?type=1')
+      }
+    },
+    async showDownloadAppDialog() {
+      try {
+        await this.$dialog.confirm({
+          title: '功能使用引导',
+          message:
+            '您可前往剑鱼标讯APP或电脑端获取更多潜在客户,访问入口:工作台-商机-潜在客户挖掘',
+          className: 'j-confirm-dialog',
+          showCancelButton: true,
+          confirmButtonText: '下载APP',
+          cancelButtonText: '暂不前往',
+          confirmButtonColor: '#2ABDD1',
+          cancelButtonColor: '#171826',
+          width: 303
+        })
+        openAppOrWxPage(LINKS.下载APP, {
+          query: {
+            source: this.$env.platform.toUpperCase()
+          }
+        })
+      } catch (error) {
+        console.log(error)
+      }
     }
   }
 }
@@ -85,5 +196,6 @@ export default {
   font-size: 14px;
   line-height: 20px;
   color: $main;
+  border-top: 1px solid rgba(0, 0, 0, 0.05);
 }
 </style>

+ 151 - 0
apps/mobile/src/views/article/components/ContentHeader.vue

@@ -0,0 +1,151 @@
+<template>
+  <section class="content-title-container bg-white">
+    <span
+      class="j-tag-item border red fixed-header-top-left"
+      v-if="content.isSelfSite"
+    >
+      业主委托项目
+    </span>
+    <!-- <span class="forecast fixed-header-right" v-if="otherModel.forecastShow">
+      <span class="j-icon j-base-icon icon-ai"></span>
+      <span class="forecast-text">中标预测</span>
+    </span> -->
+    <h2
+      class="article-title"
+      :class="{ 'van-multi-ellipsis--l2': false }"
+      @click="toProjectDetail"
+      v-html="content.titleHighlighted"
+    ></h2>
+    <div class="tag-list">
+      <span
+        class="j-tag-item border gray round"
+        v-for="(tag, index) in content.tags"
+        :key="index"
+        :title="tag.label"
+        >{{ tag.label }}
+      </span>
+    </div>
+    <div class="sub-info-line">
+      <span class="info-publish-time">{{ content.time }}</span>
+      <span class="info-canbiao-persons">参标人:张xxxxxx等</span>
+    </div>
+  </section>
+</template>
+<script>
+import { Icon } from 'vant'
+import { mapState, mapGetters } from 'vuex'
+
+export default {
+  name: 'ContentHeader',
+  components: {
+    [Icon.name]: Icon
+  },
+  computed: {
+    ...mapState({
+      content: (state) => state.article.mainModel.content,
+      summary: (state) => state.article.mainModel.summary,
+      otherModel: (state) => state.article.otherModel
+    }),
+    ...mapGetters('user', ['isLogin', 'isSuper', 'isMember', 'isNewBusiness'])
+  },
+  created() {},
+  methods: {
+    toProjectDetail() {
+      // if (this.isNewBusiness) {
+      // }
+      // function goMemberFollowPage (url) {
+      //   var objId = {
+      //     fid:'',
+      //     sid:''
+      //   }
+      //   if (followId && jumpFlag){
+      //     objId.fid = followId
+      //     objId.sid = id
+      //     sessionStorage.setItem('bigvip-fid',JSON.stringify(objId))
+      //   }else{
+      //     objId.fid = ''
+      //     objId.sid = id
+      //     sessionStorage.setItem('bigvip-fid',JSON.stringify(objId))
+      //   }
+      //   location.href = url
+      // }
+      // if (newEntNiche) {
+      //   goMemberFollowPage('/jyapp/big/page/client_follow_detail?from=client&industry=' + "交通")
+      // }else{
+      //   goMemberFollowPage('/jyapp/big/page/pro_follow_detail')
+      // }
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.fixed-header-top-left {
+  position: absolute;
+  left: -1px;
+  top: 1px;
+}
+
+.content-title-container {
+  position: relative;
+  padding: 24px 16px 12px;
+}
+
+.article-title {
+  font-size: 18px;
+  font-weight: 400;
+  line-height: 26px;
+  color: #171826;
+}
+
+.tag-list {
+  margin: 8px 0 4px;
+  display: flex;
+  flex-wrap: wrap;
+  .j-tag-item {
+    margin-bottom: 4px;
+    &:not(:last-of-type) {
+      margin-right: 4px;
+    }
+  }
+}
+
+.sub-info-line {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  font-size: 12px;
+  line-height: 18px;
+  color: #9b9ca3;
+}
+.info-canbiao-persons {
+  font-size: 14px;
+  left: 20px;
+  color: #5f5e64;
+}
+
+.fixed-header-right {
+  position: absolute;
+  right: 0;
+  bottom: 38px;
+}
+
+.forecast {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  width: 84px;
+  height: 28px;
+  background: $main;
+  border-radius: 14px 0px 0px 14px;
+  .icon-ai {
+    width: 16px;
+    height: 16px;
+  }
+  .forecast-text {
+    font-size: 13px;
+    line-height: 20px;
+    color: $white;
+  }
+}
+</style>

+ 72 - 0
apps/mobile/src/views/article/components/ContentHeaderBannerTip.vue

@@ -0,0 +1,72 @@
+<template>
+  <div class="content-top-guide-container">
+    <p class="content-top-guide-content">
+      该公告由业主方/采购单位直接发布,急寻供应商,立即报名直接联系业主方/采购单位参与投标采购。
+    </p>
+    <button
+      class="content-top-guide-action-button clickable"
+      @click="reportNow"
+    >
+      立即报名
+    </button>
+  </div>
+</template>
+<script>
+import { LINKS } from '@/data'
+import { openAppOrWxPage } from '@/utils/'
+
+export default {
+  name: 'ContentHeaderBannerTip',
+  props: {
+    id: {
+      type: String,
+      default: ''
+    }
+  },
+  created() {},
+  methods: {
+    reportNow() {
+      openAppOrWxPage(LINKS.留资, {
+        query: {
+          source: 'peugeot_supplier_regist',
+          infoid: this.id
+        }
+      })
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.content-top-guide-container {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding: 6px 16px;
+  font-size: 12px;
+  line-height: 18px;
+  color: $main;
+  background-color: #eaf8fa;
+  .content-top-guide-content {
+    flex: 1;
+  }
+  .content-top-guide-action-button {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    padding: 0 10px;
+    font-size: 13px;
+    line-height: 24px;
+    height: 28px;
+    color: #fff;
+    border-radius: 30px 30px 30px 0;
+    background: linear-gradient(
+      180deg,
+      #9cebf5 7.14%,
+      #47dfe9 40.18%,
+      #20c0d6 70.54%,
+      #70e4ec 92.86%
+    );
+  }
+}
+</style>

+ 75 - 63
apps/mobile/src/views/article/components/ContentMainText.vue

@@ -1,51 +1,52 @@
 <template>
   <ContentModuleCard title="正文" class="content-main-text-container">
     <section class="content-main-text">
-      <p v-for="index in 20" :key="index">正文正文正文</p>
+      <pre v-html="content.contentHighlighted"></pre>
     </section>
-    <section class="content-main-others-container">
-      <div class="others-header flex flex-(items-center justify-between)">
-        <div class="content-file-attachment-left flex flex-items-center">
-          <span class="j-icon icon-data-download"></span>
-          <span class="file-attachment-text">附件下载</span>
-          <span class="j-tag-item main round attachment-tag"
-            >本月剩余:8个</span
-          >
-        </div>
-        <div class="content-file-attachment-actions">
-          <span class="charge-button">立即充值</span>
-        </div>
-      </div>
-      <div class="file-attachment-list">
-        <div class="file-attachment-item highlight-text underline clickable">
-          颍东区职业教育中心二期、区委党校人防防化设备采购及安装项目清单、控制价.rar
-        </div>
-        <div class="file-attachment-item highlight-text underline clickable">
-          颍东区职业教育中心二期、区委党校人防防化设备采购及安装项目清单、控制价.rar
-        </div>
-        <div class="file-attachment-item highlight-text underline clickable">
-          颍东区职业教育中心二期、区委党校人防防化设备采购及安装项目清单、控制价.rar
-        </div>
-      </div>
-      <div class="others-footer">
-        <span class="highlight-text origin-link clickable">查看原文链接</span>
-        <button class="feedback-button clickable">意见反馈</button>
+    <AttachmentDownloadCard class="attachment-download-section" />
+    <section class="others-footer">
+      <div class="origin-link-container">
+        <OriginLink :id="content.id" v-if="content.originalShow" />
       </div>
+      <button class="feedback-button clickable" @click="feedback">
+        意见反馈
+      </button>
     </section>
   </ContentModuleCard>
 </template>
 <script>
 import ContentModuleCard from '@/views/article/ui/ContentModuleCard.vue'
+import AttachmentDownloadCard from '@/views/article/components/AttachmentDownloadCard.vue'
+import OriginLink from '@/views/article/components/OriginLink.vue'
+import { mapState, mapGetters } from 'vuex'
+import { LINKS } from '@/data'
+import { openAppOrWxPage } from '@/utils/'
+
 export default {
   name: 'ContentMainText',
   components: {
+    AttachmentDownloadCard,
+    OriginLink,
     ContentModuleCard
   },
   data() {
     return {}
   },
-  created() {},
-  methods: {}
+  computed: {
+    ...mapState({
+      content: (state) => state.article.mainModel.content
+    }),
+    ...mapGetters('user', ['isLogin'])
+  },
+  methods: {
+    feedback() {
+      openAppOrWxPage(LINKS.用户反馈, {
+        query: {
+          fkid: this.content.id
+        }
+      })
+    }
+  }
 }
 </script>
 
@@ -56,49 +57,60 @@ export default {
     background-color: $white;
   }
 }
+.content-main-text {
+  margin-bottom: 16px;
+  ::v-deep {
+    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 {
+      border-collapse: collapse !important;
+      white-space-collapse: collapse;
+      border-spacing: 0px !important;
+      line-height: 21px;
+      background: transparent;
+      max-width: 100%;
+
+      th,
+      tr td {
+        border: 1px solid #ebebeb;
+        padding: 2px 4px;
+      }
+    }
+  }
+}
+
+.attachment-download-section {
+  margin-bottom: 10px;
+}
+
 .icon-data-download {
   width: 24px;
   height: 24px;
 }
-.content-main-others-container {
-  margin-top: 10px;
-}
-.file-attachment-text {
-  margin: 0 12px;
-  font-size: 16px;
-  line-height: 24px;
-}
-.attachment-tag {
-  padding: 0 8px;
-  font-size: 11px;
-  line-height: 18px;
-  border-radius: 10px;
-}
-.charge-button {
-  padding: 3px 8px;
-  font-size: 12px;
-  line-height: 18px;
-  border-radius: 8px;
-  color: $white;
-  background-color: $main;
-}
-.file-attachment-list {
-  margin-top: 16px;
-  font-size: 15px;
-  line-height: 22px;
-  .file-attachment-item {
-    margin-bottom: 16px;
-  }
-}
 .others-footer {
   display: flex;
   align-items: center;
   justify-content: space-between;
   font-size: 15px;
   line-height: 22px;
-  .origin-link {
-    color: #05a6f3;
-  }
   .feedback-button {
     color: #9b9ca3;
     background: transparent;

+ 66 - 33
apps/mobile/src/views/article/components/ContentProjectTimeline.vue

@@ -1,19 +1,21 @@
 <template>
-  <ContentModuleCard title="招标/采购进度" class="content-project-timeline">
+  <ContentModuleCard
+    title="招标/采购进度"
+    class="content-project-timeline"
+    v-if="stepList.length"
+  >
     <div class="action-right-container" slot="header-actions">
       <span class="action-item clickable download-project-doc">
         <AppIcon name="xiazaixiangmubaogao"></AppIcon>
         <span class="action-text">下载项目报告</span>
       </span>
       <span class="action-item clickable follow-project">
-        <AppIcon name="jiankong" v-if="true" color="#9b9ca3"></AppIcon>
-        <AppIcon name="yijiankong" v-else color="#9b9ca3"></AppIcon>
-        <span class="action-text">监控</span>
+        <ActionMonitor :id="content.id" direction="row" />
       </span>
     </div>
     <div class="bg-white">
       <TimeLine
-        :markedSameId="articleId"
+        :markedSameId="content.id"
         :stepList="stepList"
         @itemClick="itemClick"
       ></TimeLine>
@@ -22,50 +24,81 @@
 </template>
 <script>
 import ContentModuleCard from '@/views/article/ui/ContentModuleCard.vue'
+import ActionMonitor from '@/views/article/components/ActionMonitor.vue'
 import { AppIcon, TimeLine } from '@/ui'
+import { mapState, mapGetters } from 'vuex'
+import { replaceKeyword, openAppOrWxPage } from '@/utils'
+import { LINKS } from '@/data'
 
 export default {
   name: 'ContentProjectTimeline',
   components: {
     AppIcon,
     TimeLine,
+    ActionMonitor,
     ContentModuleCard
   },
-  props: {
-    articleId: {
-      type: String,
-      default: '226'
-    }
-  },
   data() {
     return {
-      stepList: [
-        {
-          id: '226',
-          step_time: '2024-02-26',
-          // step_bidamount: 'moneyUnit(111)',
-          tags: [111],
-          content: '226-内容内容内容内容内容内容'
-        },
-        {
-          id: '225',
-          step_time: '2024-02-25',
-          tags: [111, 222, 333],
-          content: '225-内容内容内容内容内容内容'
-        },
-        {
-          id: '224',
-          step_time: '2024-02-24',
-          tags: [111, 222, 333],
-          content: '224-内容内容内容内容内容内容'
+      // stepList: [
+      //   {
+      //     id: '226',
+      //     step_time: '2024-02-26',
+      //     // step_bidamount: 'moneyUnit(111)',
+      //     tags: [111],
+      //     content: '226-内容内容内容内容内容内容'
+      //   },
+      //   {
+      //     id: '225',
+      //     step_time: '2024-02-25',
+      //     tags: [111, 222, 333],
+      //     content: '225-内容内容内容内容内容内容'
+      //   },
+      //   {
+      //     id: '224',
+      //     step_time: '2024-02-24',
+      //     tags: [111, 222, 333],
+      //     content: '224-内容内容内容内容内容内容'
+      //   }
+      // ]
+    }
+  },
+  computed: {
+    ...mapState({
+      content: (state) => state.article.mainModel.content,
+      expandModel: (state) => state.article.expandModel
+    }),
+    ...mapGetters('user', ['isNewBusiness']),
+    stepList() {
+      const projectProgress = this.expandModel.projectProgress || {}
+      const projectName = projectProgress.name || ''
+      const progressList = projectProgress?.list || []
+      return progressList.map((p) => {
+        return {
+          ...p,
+          step_time: p.time,
+          tags: [p.tag],
+          content: replaceKeyword(p.title, projectName, [
+            '<span class="highlight-text">',
+            '</span>'
+          ])
         }
-      ]
+      })
     }
   },
-  created() {},
   methods: {
     itemClick(item) {
-      console.log(item)
+      const id = item.id
+      sessionStorage.setItem('bigvip-fid', JSON.stringify({ sid: id }))
+      if (this.isNewBusiness) {
+        openAppOrWxPage(LINKS.项目信息, {
+          query: {}
+        })
+      } else {
+        openAppOrWxPage(LINKS.项目详情页, {
+          query: {}
+        })
+      }
     }
   }
 }

+ 16 - 5
apps/mobile/src/views/article/components/DataExportBanner.vue

@@ -1,5 +1,8 @@
 <template>
-  <section class="data-export-banner-container clickable bg-white">
+  <section
+    class="data-export-banner-container clickable bg-white"
+    @click="toDataExport"
+  >
     <div class="banner-left">
       <span class="j-icon icon-data-download"></span>
     </div>
@@ -16,6 +19,8 @@
 </template>
 <script>
 import { Icon } from 'vant'
+import { LINKS } from '@/data'
+import { openAppOrWxPage } from '@/utils/'
 export default {
   name: 'DataExportBanner',
   components: {
@@ -30,11 +35,17 @@ export default {
       }
     }
   },
-  data() {
-    return {}
-  },
   created() {},
-  methods: {}
+  methods: {
+    toDataExport() {
+      const { platform } = this.$env
+      openAppOrWxPage(LINKS.数据导出页面, {
+        query: {
+          adv_from: `${platform}_articel_win_sj_1`
+        }
+      })
+    }
+  }
 }
 </script>
 

+ 27 - 8
apps/mobile/src/views/article/components/OriginLink.vue

@@ -1,6 +1,6 @@
 <template>
   <span class="origin-link-container">
-    <span class="highlight-text origin-link clickable" @click="seeOrigin">
+    <span class="highlight-text origin-link clickable" @click="viewOriginLink">
       查看原文链接
     </span>
     <van-dialog
@@ -43,8 +43,9 @@ import { mapState, mapGetters } from 'vuex'
 import { LINKS } from '@/data'
 import { AppIcon } from '@/ui/'
 import { appCallOpenWindow } from '@/utils/callFn/appFn'
-import { openAppOrWxPage, openLinkOfOther } from '@/utils/'
+import { openAppOrWxPage } from '@/utils/'
 import { getArticleOriginalText } from '@/api/modules/article'
+import EventBus from '@/utils/eventBus'
 
 export default {
   name: 'OriginLink',
@@ -73,13 +74,31 @@ export default {
     }),
     ...mapGetters('user', ['isFree'])
   },
+  created() {
+    this.eventBusListening()
+  },
   methods: {
-    async seeOrigin() {
+    eventBusListening() {
+      EventBus.$on('originLink:view', () => {
+        this.viewOriginLink()
+      })
+      // 及时解绑 EventBus 的事件监听
+      this.$once('hook:beforeDestroy', () => {
+        EventBus.$off('originLink:view')
+      })
+    },
+    async viewOriginLink() {
+      if (this.url) {
+        // 如果已经获取过链接,则可以直接打开
+        return this.openUrl()
+      }
       const { inH5 } = this.$envs
       if (inH5) {
-        openLinkOfOther(
-          'https://wx.jianyu360.cn/front/downloadapppage/normal?source=H5'
-        )
+        openAppOrWxPage(LINKS.下载APP, {
+          query: {
+            source: this.$env.platform.toUpperCase()
+          }
+        })
       } else {
         const loading = this.$toast.loading()
         try {
@@ -113,7 +132,7 @@ export default {
                   className: 'j-confirm-dialog',
                   showCancelButton: true,
                   confirmButtonText: '联系客服',
-                  CancelButtonText: '返回',
+                  cancelButtonText: '返回',
                   confirmButtonColor: '#2ABDD1',
                   cancelButtonColor: '#171826',
                   width: 303
@@ -180,7 +199,7 @@ export default {
         console.log(error)
       }
     },
-    async openUrl() {
+    openUrl() {
       if (!this.url) return
       const { inApp } = this.$envs
       if (inApp) {

+ 43 - 37
apps/mobile/src/views/article/components/TabActions.vue

@@ -1,29 +1,14 @@
 <template>
   <section class="tab-actions safe-area-inside-bottom bg-white">
-    <div class="tab-action">
-      <ActionCollection
-        class="action-content"
-        v-model="tabState.star"
-        :id="tabState.id"
-      />
-    </div>
-    <div class="tab-action">
-      <ActionMonitor
-        class="action-content"
-        v-model="tabState.monitor"
-        :id="tabState.id"
-      />
-    </div>
-    <div class="tab-action">
-      <ActionShareToWorkmate class="action-content" :id="tabState.id" />
-    </div>
-    <div class="tab-action">
-      <ActionInBidding
-        class="action-content"
-        v-model="tabState.inBidding"
-        :id="tabState.id"
-      />
-    </div>
+    <ActionCollection class="tab-action" :id="id" />
+    <ActionMonitor
+      class="tab-action"
+      :id="id"
+      popover
+      @afterFetch="afterMonitorFetch"
+    />
+    <ActionShareToWorkmate class="tab-action" :id="id" />
+    <ActionInBidding class="tab-action" :id="id" />
   </section>
 </template>
 <script>
@@ -31,6 +16,7 @@ import ActionCollection from '@/views/article/components/ActionCollection.vue'
 import ActionMonitor from '@/views/article/components/ActionMonitor.vue'
 import ActionShareToWorkmate from '@/views/article/components/ActionShareToWorkmate.vue'
 import ActionInBidding from '@/views/article/components/ActionInBidding.vue'
+import { mapState, mapMutations } from 'vuex'
 
 export default {
   name: 'TabActions',
@@ -52,15 +38,42 @@ export default {
   data() {
     return {
       tabState: {
-        id: 'ABCY1xBfTwFNys4JHN4cE8JIzAvFj1jcXNlKCgWPC8ec3lwAStUCXs%3D',
-        star: true,
-        monitor: false,
         inBidding: false
       }
     }
   },
-  created() {},
-  methods: {}
+  computed: {
+    ...mapState({
+      content: (state) => state.article.mainModel.content
+    }),
+    id() {
+      return this.content.id
+    }
+  },
+  created() {
+    // this.checkCanFollow()
+  },
+  methods: {
+    ...mapMutations('article', ['setOtherModelChild']),
+    removeHideUnderline() {
+      this.setOtherModelChild({
+        key: 'hasProject',
+        data: true
+      })
+      // 移除下划线
+      this.setOtherModelChild({
+        key: 'forecastShow',
+        data: true
+      })
+    },
+    showForecast() {},
+    afterMonitorFetch(payload) {
+      if (payload.canFollow) {
+        this.showForecast()
+        this.removeHideUnderline()
+      }
+    }
+  }
 }
 </script>
 
@@ -75,18 +88,11 @@ export default {
   display: flex;
   align-items: center;
   justify-content: space-between;
+  height: 48px;
   box-shadow: 0px -2px 8px 0px rgba(54, 147, 179, 0.05);
 }
 .tab-action {
-  display: flex;
-  flex-direction: column;
-  align-items: center;
-  justify-content: center;
   flex: 1;
-  height: 48px;
-}
-.action-content {
-  width: 100%;
   height: 100%;
 }
 </style>

+ 164 - 117
apps/mobile/src/views/article/content.vue

@@ -3,36 +3,15 @@
     <div class="j-container page-container">
       <div
         class="j-main article-content-main"
+        :class="{ 'show-underline': otherModel.hasProject }"
         ref="scrollWrapper"
-        @scroll="onScroll"
+        @scroll.passive="onScroll"
       >
-        <div class="content-title-container bg-white">
-          <span class="j-tag-item border red fixed-header-top-left">
-            业主委托项目
-          </span>
-          <h2
-            class="article-title"
-            :class="{ 'van-multi-ellipsis--l2': false }"
-          >
-            阜阳市颍东区职业教育中心二期、区委党校人防防化设备采购及安装项目补充公告目补充公告
-          </h2>
-          <div class="tag-list">
-            <span class="j-tag-item border gray round">河南-郑州市-金水区</span>
-            <span class="j-tag-item border gray round">招标</span>
-            <span class="j-tag-item border gray round">建筑工程</span>
-            <span class="j-tag-item border gray round">265.63万元</span>
-          </div>
-          <div class="sub-info-line">
-            <span class="info-publish-time">12小时前</span>
-            <span class="info-canbiao-persons">参标人:张xx等</span>
-          </div>
-        </div>
-        <div class="content-top-guide-container">
-          <p class="content-top-guide-content">
-            该公告由业主方/采购单位直接发布,急寻供应商,立即报名直接联系业主方/采购单位参与投标采购。
-          </p>
-          <button class="content-top-guide-action-button">立即报名</button>
-        </div>
+        <ContentHeader @beforeLeave="beforeLeaveVue"></ContentHeader>
+        <ContentHeaderBannerTip
+          :id="pageInfo.id"
+          v-if="content.recommendedService"
+        ></ContentHeaderBannerTip>
         <van-tabs
           v-model="pageInfo.tabActive"
           :offset-top="offsetTop"
@@ -86,7 +65,7 @@
     </div>
     <appShareSheet
       v-model="shareShow"
-      @share="doShareAjax"
+      @share="calcAppShareInfo"
       popup-title="分享有礼"
       :share-title="shareConf.title"
       :share-content="shareConf.content"
@@ -102,14 +81,16 @@
         </a>
       </div>
     </appShareSheet>
-    <wxShareGuide></wxShareGuide>
+    <wxShareGuide v-model="guideMask"></wxShareGuide>
   </div>
 </template>
 <script>
 import { Tabs, Tab, Icon } from 'vant'
 import { mixinHeader } from '@/utils/mixins/header'
 import { appWxShareMixin } from '@/utils/mixins/modules/app-wx-share'
-import { isElementInScrollArea } from '@/utils/dom-utils'
+import { isElementInScrollArea } from '@jy/util'
+import ContentHeader from '@/views/article/components/ContentHeader.vue'
+import ContentHeaderBannerTip from '@/views/article/components/ContentHeaderBannerTip.vue'
 import ContentAbstract from '@/views/article/components/ContentAbstract.vue'
 import DataExportBanner from '@/views/article/components/DataExportBanner.vue'
 import ContentAbstractEntList from '@/views/article/components/ContentAbstractEntList.vue'
@@ -120,7 +101,11 @@ import ContentBusinessRecommend from '@/views/article/components/ContentBusiness
 import ContentCustomerRecommend from '@/views/article/components/ContentCustomerRecommend.vue'
 import NpsCard from '@/views/article/components/NpsCard.vue'
 import TabActions from '@/views/article/components/TabActions.vue'
-import AdSingle from '@/components/ad/Ad'
+import AdSingle from '@/components/ad/Ad.vue'
+import { throttle } from 'lodash'
+import { mapState, mapMutations, mapActions, mapGetters } from 'vuex'
+
+import { getArticleShareInfo } from '@/api/modules/article'
 
 export default {
   name: 'ArticleContent',
@@ -129,6 +114,8 @@ export default {
     [Tabs.name]: Tabs,
     [Tab.name]: Tab,
     [Icon.name]: Icon,
+    ContentHeader,
+    ContentHeaderBannerTip,
     ContentAbstract,
     DataExportBanner,
     ContentMainText,
@@ -153,15 +140,20 @@ export default {
           lineHeight: '18px'
         }
       },
+      shareEnableConf: {
+        shareActionImmediate: false,
+        app: true, // 开启app分享
+        wx: true // 开启wx分享
+      },
       shareConf: {
-        // execShare
-        title: '剑鱼双11,百万补贴限量抢',
-        content:
-          '快来和我一起参与吧!剑鱼双11,百万补贴限量抢,快速获取商机信息',
-        pathname: '/weixin/frontPage/activity/sess/task-20221111',
+        fetchDomain: true,
+        title: '',
+        content: '',
+        pathname: '',
         origin: location.origin,
         link: ''
       },
+      shareInfoRes: {},
       offsetTop: 0,
       // tabList: [
       //   {
@@ -190,25 +182,54 @@ export default {
       //   }
       // ],
       pageInfo: {
+        id: '',
         tabActive: ''
       }
     }
   },
   computed: {
+    ...mapState({
+      content: (state) => state.article.mainModel.content,
+      summary: (state) => state.article.mainModel.summary,
+      expandModel: (state) => state.article.expandModel,
+      otherModel: (state) => state.article.otherModel
+    }),
+    ...mapGetters('user', ['isLogin', 'isSuper', 'isMember', 'isNewBusiness']),
     getContentAdID() {
       return (this.$envs.inWX ? 'jy' : 'jyapp') + '-wxcontent-bottom'
     }
   },
   created() {
     this.appHeaderActions()
+    this.getPageData()
+    this.initAppWxShare()
   },
   mounted() {
     this.getStickyOffset()
+    this.restoreScrollTop()
   },
   methods: {
+    ...mapMutations('article', ['setMainContentModelChild', 'saveToStorage']),
+    ...mapActions('article', [
+      'getPreAgentInfo',
+      'getBaseInfo',
+      'getAdvancedInfo'
+    ]),
+    async getPageData() {
+      this.getParams()
+      await this.getPreAgentInfo()
+      await this.getBaseInfo()
+      this.checkProject()
+      this.getAdvancedInfo()
+    },
+    getParams() {
+      const { params } = this.$route
+      this.pageInfo.id = params.id
+      this.setMainContentModelChild({ key: 'id', data: params.id })
+    },
     appHeaderActions() {
       const { $envs } = this
-      if ($envs.inWX) return
+      if (!$envs.inApp) return
       const actionRightConf = {
         actionRightText:
           '<div class="j-icon icon-points" style="width:24px;height:24px"></div><p>分享有礼</p>',
@@ -228,15 +249,94 @@ export default {
         }
       }
     },
+    beforeLeaveVue() {
+      this.saveToStorage()
+    },
     clickRight() {
       this.doShare()
     },
     doShare() {
       this.shareShow = true
     },
-    doShareAjax() {},
-    onScroll() {
+    initAppWxShare() {
+      if (this.$envs.inWX) {
+        this.calcWxShareInfo()
+        this.initShareMixin()
+      } else {
+        this.getShareInfoReq()
+      }
+    },
+    async getShareInfoReq() {
+      const {
+        data = {},
+        error_code: code,
+        error_msg: msg = ''
+      } = await getArticleShareInfo()
+      if (code === 0 && data) {
+        this.shareInfoRes = data
+      }
+    },
+    calcWxShareInfo() {
+      this.shareConf.title = this.getRandomShareText()
+      this.shareConf.content = '全国招标信息免费看,不遮挡'
+      this.shareConf.pathname = '/swordfish/about'
+
+      console.log(this.shareConf.title)
+    },
+    calcAppShareInfo(t) {
+      const shareType = t.id
+
+      const titleList = ['您的好友']
+      // 分享到微信或者朋友圈带上昵称
+      if (this.shareInfoRes.nickname && shareType !== 2) {
+        titleList.push(this.shareInfoRes.nickname)
+      }
+      titleList.push('向您推荐了剑鱼标讯')
+      const content = titleList.join('')
+
+      let link = `/swordfish/about?source=app_infocontentshare&from=${
+        this.shareInfoRes.userId || ''
+      }`
+      let shareTitle = this.content.title
+        .replace(/<\/?.+?>/g, '')
+        .replace(/ /g, '')
+      if (shareType === 1) {
+        // 分享给微信好友
+        link = location.pathname.replace(import.meta.env.VITE_APP_BASE_URL, '')
+        if (location.search) {
+          link += `${location.search}&source=app_infocontentshare&from=${
+            this.shareInfoRes.userId || ''
+          }`
+        } else {
+          link += `?source=app_infocontentshare&from=${
+            this.shareInfoRes.userId || ''
+          }`
+        }
+      } else if (shareType === 3) {
+        // 分享到朋友圈
+        link += '&qrcodeType=app_infocontent_timeline_z'
+        shareTitle = this.getRandomShareText()
+      }
+
+      this.shareConf.title = shareTitle
+      this.shareConf.content = content
+      this.shareConf.pathname = link
+      // this.shareConf.link = link
+      this.refreshShareLink()
+    },
+    onScroll: throttle(function () {
       this.checkNpsView()
+      this.saveScrollTop()
+    }, 300),
+    saveScrollTop() {
+      const scrollWrapper = this.$refs.scrollWrapper
+      const scrollTop = scrollWrapper.scrollTop
+      console.log(scrollTop, Date.now(), '-----------')
+      // this.$storage.set(this.cacheKey, scrollTop, { storage: sessionStorage })
+    },
+    restoreScrollTop() {
+      // const toDayShow = this.$storage.get(this.cacheKey, false)
+      // const scrollWrapper = this.$refs.scrollWrapper
     },
     checkNpsView() {
       const targetVm = this.$refs.npsCard
@@ -246,6 +346,17 @@ export default {
       if (visible) {
         targetVm.getIsView()
       }
+    },
+    checkProject() {},
+    async dialog1() {
+      return await this.$dialog.alert({
+        title: '申请监控更多项目',
+        messageAlign: 'left',
+        className: 'j-confirm-dialog',
+        confirmButtonText: '我知道了',
+        cancelButtonText: '我再想想',
+        message: '您可联系客服,申请升级产品套餐,监控更多项目'
+      })
     }
   }
 }
@@ -268,90 +379,26 @@ export default {
   .tab-module.content-module-card {
     margin-top: 8px;
   }
-}
-
-.page-container {
-  height: calc(100% - 1px - 1px);
-}
-
-.fixed-header-top-left {
-  position: absolute;
-  left: -1px;
-  top: 1px;
-}
-
-.content-title-container {
-  position: relative;
-  padding: 24px 16px 12px;
-}
-
-.article-title {
-  font-size: 18px;
-  font-weight: 400;
-  line-height: 26px;
-  color: #171826;
-}
 
-.tag-list {
-  margin: 8px 0 4px;
-  display: flex;
-  .j-tag-item {
-    margin-bottom: 4px;
-    &:not(:last-of-type) {
-      margin-right: 4px;
-    }
+  .keyword-underline {
+    border-bottom: 1px solid $main;
+    padding-bottom: 1px;
+  }
+  .keyword.hide-underline {
+    border-bottom-width: 0;
   }
 }
 
-.sub-info-line {
-  display: flex;
-  align-items: center;
-  justify-content: space-between;
-  font-size: 12px;
-  line-height: 18px;
-  color: #9b9ca3;
-}
-.info-canbiao-persons {
-  font-size: 14px;
-  left: 20px;
-  color: #5f5e64;
-}
-
-.content-top-guide-container {
-  display: flex;
-  align-items: center;
-  justify-content: space-between;
-  padding: 6px 16px;
-  font-size: 12px;
-  line-height: 18px;
-  color: $main;
-  background-color: #eaf8fa;
-  .content-top-guide-content {
-    flex: 1;
-  }
-  .content-top-guide-action-button {
-    display: flex;
-    align-items: center;
-    justify-content: center;
-    padding: 0 10px;
-    font-size: 13px;
-    line-height: 24px;
-    height: 28px;
-    color: #fff;
-    border-radius: 30px 30px 30px 0;
-    background: linear-gradient(
-      180deg,
-      #9cebf5 7.14%,
-      #47dfe9 40.18%,
-      #20c0d6 70.54%,
-      #70e4ec 92.86%
-    );
+.show-underline {
+  ::v-deep {
+    .hide-underline {
+      border-bottom-width: 1px;
+    }
   }
 }
 
-.icon-phone-blue {
-  width: 16px;
-  height: 16px;
+.page-container {
+  height: calc(100% - 1px - 1px);
 }
 
 .nps-module {

+ 94 - 0
apps/mobile/src/views/article/ui/FollowPopoverContentCard.vue

@@ -0,0 +1,94 @@
+<template>
+  <section class="follow-popover-content-card menu-action-list bg-white">
+    <div class="menu-action-item clickable" @click="toProjectTimeline">
+      查看监控动态
+    </div>
+    <div class="menu-action-item">
+      <p class="to-monitor-list clickable" @click="toMyFollowList">
+        查看监控列表
+      </p>
+      <div class="monitor-state">
+        <span>已监控</span>
+        <span class="highlight-text">{{ used }}</span>
+        <span>个,剩余</span>
+        <span class="highlight-text">{{ surplus }}</span>
+        <span>个</span>
+      </div>
+      <div
+        class="apply-for-more-monitor highlight-text flex clickable"
+        @click.stop="applyMore"
+      >
+        <span>申请监控更多项目</span>
+        <AppIcon name="youbian" svg size="18"></AppIcon>
+      </div>
+    </div>
+    <div
+      class="menu-action-item monitor-cancel clickable"
+      @click="cancelFollow"
+    >
+      取消监控
+    </div>
+  </section>
+</template>
+<script>
+import { Icon } from 'vant'
+import { AppIcon } from '@/ui'
+
+export default {
+  name: 'FollowPopoverContentCard',
+  components: {
+    [Icon.name]: Icon,
+    AppIcon
+  },
+  props: {
+    used: {
+      type: [String, Number],
+      default: ''
+    },
+    surplus: {
+      type: [String, Number],
+      default: ''
+    }
+  },
+  methods: {
+    toProjectTimeline() {
+      this.$emit('toTimeline')
+    },
+    toMyFollowList() {
+      this.$emit('toMyFollow')
+    },
+    applyMore() {
+      this.$emit('applyMore')
+    },
+    cancelFollow() {
+      this.$emit('cancelFollow')
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.menu-action-item {
+  padding: 12px 16px;
+  width: 168px;
+  font-size: 14px;
+  line-height: 22px;
+  text-align: center;
+  &:not(:last-of-type) {
+    border-bottom: 1px solid $border_color_3;
+  }
+}
+.monitor-state {
+  font-size: 12px;
+  .highlight-text {
+    font-size: 14px;
+  }
+}
+.apply-for-more-monitor {
+  padding: 1px 12px;
+  font-size: 12px;
+  align-items: center;
+  white-space: nowrap;
+  background-color: $color_main_background;
+}
+</style>

+ 2 - 1
apps/mobile/src/views/article/ui/MaskCard.vue

@@ -58,7 +58,8 @@ export default {
     },
     desc: {
       type: String,
-      default: '根据区域、业务范围、客户类型帮助企业快速找到目标地区潜在业务需求客户及联系方式。'
+      default:
+        '根据区域、业务范围、客户类型帮助企业快速找到目标地区潜在业务需求客户及联系方式。'
     },
     footerButtonText: {
       type: String,

+ 10 - 7
apps/mobile/src/views/article/ui/ServiceIntroCard.vue

@@ -9,7 +9,7 @@
       </h5>
       <div class="service-intro-header-actions">
         <slot name="header-actions">
-          <span class="header-action-item">
+          <span class="header-action-item clickable" @click="learnMore">
             <span class="header-action-item-text">查看服务介绍</span>
             <van-icon name="arrow" />
           </span>
@@ -27,7 +27,7 @@
             class="action-button plain"
             type="primary"
             plain
-            @click="leftButtonClick"
+            @click="plainButtonClick"
             v-if="plainButtonShow"
           >
             {{ plainButtonText }}
@@ -35,7 +35,7 @@
           <van-button
             class="action-button"
             type="primary"
-            @click="rightButtonClick"
+            @click="confirmButtonClick"
             v-if="confirmButtonShow"
           >
             {{ confirmButtonText }}
@@ -89,11 +89,14 @@ export default {
   },
   created() {},
   methods: {
-    leftButtonClick() {
-      this.$emit('leftButtonClick')
+    learnMore() {
+      this.$emit('learnMore')
     },
-    rightButtonClick() {
-      this.$emit('rightButtonClick')
+    plainButtonClick() {
+      this.$emit('plainButtonClick')
+    },
+    confirmButtonClick() {
+      this.$emit('confirmButtonClick')
     }
   }
 }