Przeglądaj źródła

feat: 新增我的剑鱼币页面

cuiyalong 1 rok temu
rodzic
commit
aeadd4436f

+ 21 - 0
jypoints-pc/src/App.vue

@@ -29,7 +29,28 @@
   }
 </script>
 <style lang="scss">
+@import "~@/assets/style/j-icons.scss";
 @import "~@/assets/style/reset-ele.scss";
+@import "~@/assets/style/base.scss";
+.in-app {
+  min-width: 1060px;
+
+  .v-w1200 {
+    width: 100%;
+  }
+
+  // 兼容工作桌面下a链接
+  a:focus,
+  a:hover {
+    text-decoration: underline;
+  }
+
+  a:active,
+  a:hover {
+    outline: 0;
+  }
+}
+
 .home{
   height: 100%;
   .link-item {

+ 1 - 0
jypoints-pc/src/api/modules/index.js

@@ -0,0 +1 @@
+export * from './user'

BIN
jypoints-pc/src/assets/image/download-gold@2x.png


BIN
jypoints-pc/src/assets/image/gift-in-hand@2x.png


BIN
jypoints-pc/src/assets/image/icon/icon-exchange-file@2x.png


BIN
jypoints-pc/src/assets/image/point-overview-bg@2x.png


+ 2 - 0
jypoints-pc/src/assets/style/_variables.scss

@@ -4,6 +4,8 @@ $topNavHeight: 77px;
 // 底部栏
 $footerHeight: 364px;
 
+$color_main: #2CB7CA;
+
 $bg-retrieve: #010C28;
 $bg-button--default: linear-gradient(84deg, #AF9552 0%, #EFDA98 100%);
 $bg-card--default: linear-gradient(#031242 0%, #010E36 100%);

+ 15 - 0
jypoints-pc/src/assets/style/j-icons.scss

@@ -0,0 +1,15 @@
+@import './_mixin';
+@import './_variables';
+
+.j-icon {
+  display: inline-block;
+  width: 24px;
+  height: 24px;
+}
+
+.icon-points {
+  background: url(~@/assets/image/jianyuIcon@2x.png) no-repeat left center;
+  background-size: 24px 24px;
+}
+        
+        

+ 277 - 0
jypoints-pc/src/components/DocsExchange.vue

@@ -0,0 +1,277 @@
+<template>
+  <section class="docs-exchange-container clearfix" @click="goDetail(docInfo.docId)">
+    <div class="fl thumbnail-container">
+      <div class="thumbnail-img" :style="{'background-image': 'url('+ docInfo.docImg + ')' }"></div>
+      <div :class="['mask', 'm-'+docFileTypeStr]"></div>
+    </div>
+    <div class="fl docs-exchange-main">
+      <div class="clearfix doc-title">
+        <span class="fl title-text">{{ docInfo.docTitle }}</span>
+        <div class="fr price">
+          <div class="price-num">{{ docInfo.costPrice }}</div>
+          <div class="price-tip">
+            <span class="state-yet" v-if="docInfo.isBuy">已兑换</span>
+            <span class="state-short" v-else-if="!docInfo.isBuy && userInfo.balance < docInfo.costPrice">剑鱼币不足</span>
+          </div>
+        </div>
+      </div>
+      <div class="doc-author" v-if="docInfo.sourceUserId">贡献者: {{ docInfo.sourceUserId }}</div>
+      <div class="clearfix doc-info">
+        <div class="fl">
+          <span>{{ docInfo.downTimes }}下载</span>
+          <span class="small-line">|</span>
+          <span>共{{ docInfo.docPageSize }}页</span>
+          <span class="small-line">|</span>
+          <span>{{ formatFileSize(docInfo.docFileSize) }}</span>
+        </div>
+        <div class="fr"></div>
+      </div>
+      <div class="doc-desc">
+        <div class="fl ellipsis-2 digest">{{ docInfo.docSummary }}</div>
+        <span class="fr">
+          <button class="confirm-button download" v-if="docInfo.isBuy" @click.stop="downLoad(docInfo.docId)">下载</button>
+          <button class="confirm-button state-convert" v-if="!docInfo.isBuy && userInfo.balance > docInfo.costPrice" @click.stop="goExchange(docInfo.docId)">立即兑换</button>
+          <button class="confirm-button recharge" v-else-if="!docInfo.isBuy && userInfo.balance < docInfo.costPrice" @click.stop="goRecharge">去充值</button>
+        </span>
+      </div>
+    </div>
+  </section>
+</template>
+
+<script>
+export default {
+  name: 'DocsExchange',
+  props: {
+    docInfo: {
+      // required: true,
+      type: Object,
+      default () {
+        return {
+          isBuy: false,
+          activityId: 4,
+          costPrice: 876,
+          docFileSize: 36855,
+          sourceUserId: '12312312',
+          docFileType: 1, // 1:word 2:pdf 3:excel 4:ppt
+          docId: "00395c6c-8874-11eb-8699-0050568f51e7",
+          docImg: "https://jydocs-previewimg.oss-cn-beijing.aliyuncs.com/00027ad0-8874-11eb-87e9-000060543698.png",
+          docPageSize: 28,
+          docSummary: "中华人民共和国招标投标法实施条例  (201 年12 月20 日中华人民共和国务院令第613 号公布 根据 2017 年 3 月 1 日《国务院关于修改和废止部分行政法规的决定》第一次修订 根据 2018 年 3 月 19 日《国务院关于修改和废止部分行政法规的决定》第二次修订 根据 2019 年 3 月 2 日《国务院关于修改部分行政法规的决定》第三次修订) 第一章 总则 第一条 为了规范招标投标活动,根据《中华人民共和国招标投标法》(以下简称招标投标法),制定本条例。 第二条 招标投标法第三条所称工程建设项目,是指工程以及与工程建设有关的货物、服务。 前款所称工程,是指建设工程,包括建筑物和构筑物的新建、改建、扩建及其相关的装修、拆除、修缮等;所称与工程建设有关的货物,是指构成工程不可分割的组成部分,且为实现工程基本功能所必需的设备、材料等;所称与工程建设有关的服务,是指为完成工程所需的勘察、设计、监理等服务。 第三条 依法必须进行招标的工程建设项目的具体范围和规模标准,由国务院发展改革部门会同国务院有关部门制订,报 - 1 - 国务院批准后公布施行。 第四条 国务院发展改革部门指",
+          docTitle: "中华人民共和国招标投标法实施条例",
+          downTimes: 1788,
+          price: 876,
+          uploadDate: "2021-03-19 16:49:22"
+        }
+      }
+    },
+    // 1:word 2:pdf 3:excel 4:ppt
+    userInfo: {
+      type: Object,
+      default () {
+        return {
+          // 余额
+          balance: 1,
+          // 30天内到期数量
+          expire: 0
+        }
+      }
+    }
+  },
+  data () {
+    return {
+      docFileTypeMap: {
+        1: 'word',
+        2: 'pdf',
+        3: 'excel',
+        4: 'ppt'
+      }
+    }
+  },
+  computed: {
+    docFileTypeStr () {
+      return this.docFileTypeMap[this.docInfo.docFileType]
+    }
+  },
+  created () {},
+  methods: {
+    formatFileSize (size) {
+      if (size) {
+        return (size / 1024).toFixed(2) + 'k'
+      } else {
+        return size
+      }
+    },
+    goDetail (id) {
+      const href = '/swordfish/docs/content/' + id
+      window.open(href)
+    }
+  }
+}
+</script>
+
+<style scoped lang="scss">
+$vip_color: #B1700E;
+
+.docs-exchange-container {
+  padding: 20px;
+  // margin-bottom: 20px;
+  border-radius: 16px;
+  background: #FFF;
+  cursor: pointer;
+}
+.thumbnail-container {
+  position: relative;
+  width: 110px;
+  height: 150px;
+  margin-right: 20px;
+  border-radius: 4px;
+  background: #FFFFFF;
+  border: 1px solid #ECECEC;
+  cursor: pointer;
+  overflow: hidden;
+  box-sizing: border-box;
+  .thumbnail-img{
+    width: 100%;
+    height: 100%;
+    background-repeat: no-repeat;
+    background-position: center center;
+    background-size: cover;
+    transform-origin: center center;
+    transition: all .3s;
+    &:hover{
+      transform: scale(1.3);
+    }
+  }
+  .mask{
+    position: absolute;
+    right: 0;
+    bottom: 0;
+    width: 24px;
+    height: 24px;
+    z-index: 10;
+  }
+  .m-pdf{
+    background: url("~@/assets/image/pdf@2x.png") no-repeat center center;
+    background-size: contain;
+  }
+  .m-word{
+    background: url("~@/assets/image/word@2x.png") no-repeat center center;
+    background-size: contain;
+  }
+  .m-excel{
+    background: url("~@/assets/image/excel@2x.png") no-repeat center center;
+    background-size: contain;
+  }
+  .m-ppt{
+    background: url("~@/assets/image/ppt@2x.png") no-repeat center center;
+    background-size: contain;
+  }
+}
+
+.confirm-button {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  width: 90px;
+  height: 30px;
+  line-height: 30px;
+  font-size: 13px;
+  border-radius: 4px;
+  text-align: center;
+  color: #1D1D1D;
+  background: linear-gradient(270deg, #F1D090 0%, #FAE7CA 100%);
+}
+
+.docs-exchange-main{
+  width: calc(100% - 130px);
+  .title-text{
+    display: inline-block;
+    width: calc(100% - 120px);
+    margin-right: 24px;
+    color: #1D1D1D;
+    font-size: 16px;
+    line-height: 24px;
+    &:hover{
+      color: #2CB7CA;
+    }
+  }
+  .state-short{
+    color: #999999;
+    font-size: 14px;
+  }
+  .state-yet{
+    color: #2CB7CA;
+    font-size: 14px;
+  }
+
+  .doc-author{
+    margin-top: 2px;
+    color: #2CB7CA;
+    font-size: 14px;
+    line-height: 24px;
+  }
+  .doc-info{
+    padding-top: 4px;
+    padding-bottom: 4px;
+    color: #686868;
+    font-size: 14px;
+    line-height: 32px;
+    border-bottom: 1px solid#eee;
+    .small-line{
+      margin: 0 6px;
+      color: #eee;
+    }
+  }
+  
+
+  .download{
+    color: $vip_color;
+    border: 1px solid $vip_color;
+    padding-left: 24px;
+    background: url("~@/assets/image/download-gold@2x.png") 8px center no-repeat;
+    background-position: 18px center;
+    background-size: 20px 20px;
+
+    &:hover{
+      background: url("~@/assets/image/downloadHover@2x.png") 8px center no-repeat $vip_color;
+      background-position: 18px center;
+      background-size: 20px 20px;
+      color: #fff;
+    }
+  }
+
+  .doc-desc{
+    margin-top: 8px;
+  }
+  .digest{
+    max-width: 580px;
+    color: #999999;
+    font-size: 12px;
+    line-height: 20px;
+  }
+  .price{
+    position: relative;
+    text-align: right;
+    .price-num{
+      display: inline-block;
+      color: #FF3A20;
+      font-size: 16px;
+      line-height: 24px;
+      padding-left: 28px;
+      background: url("~@/assets/image/jianyuIcon@2x.png") no-repeat left center;
+      background-size: 24px 24px;
+    }
+    .price-tip{
+      position: absolute;
+      bottom: -20px;
+      right: 0;
+      color: #999;
+      font-size: 13px;
+      line-height: 20px;
+      text-align: right;
+      white-space: nowrap;
+    }
+  }
+}
+</style>
+  

+ 83 - 0
jypoints-pc/src/components/FileExchange.vue

@@ -0,0 +1,83 @@
+<template>
+  <div class="file-exchange">
+    <Exchange
+      :title="title"
+      :priceNum="350"
+      :pointBalance="pointBalance"
+      @exchange="doExchange">
+      <template #hd-img>
+        <img src="@/assets/image/icon/icon-exchange-file@2x.png" alt="">
+      </template>
+      <template #desc>招标文件自主下载,帮助企业投标、客户分析、市场挖掘更高效。</template>
+      <template #sub-desc>
+        <p>兑换须知:兑换后30天(含兑换当天)内有效,请及时使用。</p>
+        <p>使用说明:在标讯详情下载附件可使用该权益。</p>
+      </template>
+    </Exchange>
+    <Dialog title="兑换附件下载" :visible.sync="dialog.show" @confirm="confirm" @cancel="showDialog(false)">
+      <p class="dialog-text">
+        即将消耗您
+        <span class="highlight-text">{{ exchange.totalPayCount }}</span>
+        个剑鱼币兑换
+        <span class="highlight-text">{{ exchange.count }}</span>
+        个附件下载权益?
+      </p>
+    </Dialog>
+  </div>
+</template>
+
+<script>
+import Exchange from '@/components/common/exchange.vue'
+import Dialog from '@/components/common/dialog.vue'
+
+export default {
+  name: 'FileExchangeItem',
+  components: {
+    Dialog,
+    Exchange
+  },
+  props: {
+    // 剑鱼币余额
+    pointBalance: {
+      type: Number,
+      default: 99990
+    }
+  },
+  data () {
+    return {
+      title: '附件下载',
+      exchange: {
+        // 兑换n个权益
+        count: 0,
+        // 兑换n个权益,总共需要花费的剑鱼币数量
+        totalPayCount: 0
+      },
+      dialog: {
+        show: false
+      }
+    }
+  },
+  created () {},
+  methods: {
+    showDialog (f = false) {
+      this.dialog.show = f
+    },
+    doExchange (e) {
+      Object.assign(this.exchange, e)
+      this.showDialog(true)
+    },
+    confirm () {
+      console.log('确认兑换')
+    }
+  }
+}
+</script>
+
+<style scoped lang="scss">
+.dialog-text {
+  color: #686868;
+  text-align: center;
+  font-size: 14px;
+  line-height: 22px; 
+}
+</style>

+ 133 - 0
jypoints-pc/src/components/GetMission.vue

@@ -0,0 +1,133 @@
+<template>
+  <section class="get-mission-container">
+    <div class="get-mission-hd">
+      <p class="get-mission-title">做任务赚好礼</p>
+    </div>
+    <div class="get-mission-bd">
+      <div class="get-mission-bd-content">
+        <div class="g-m-b-content-l">
+          <div class="gift-hands">
+            <img src="@/assets/image/gift-in-hand@2x.png" alt="">
+          </div>
+        </div>
+        <div class="g-m-b-content-c">
+          <div class="j-tag tag-red">新用户专享</div>
+          <p class="g-m-b-content-c-text">限时福利:7天内完成全部新手任务,可额外获得七天超级订阅会员。</p>
+        </div>
+        <div class="g-m-b-content-r">
+          <button class="j-button confirm">确认挑战</button>
+          <button class="j-button cancel plain">再看看</button>
+        </div>
+      </div>
+    </div>
+  </section>
+</template>
+
+<script>
+export default {
+  name: 'GetMission',
+  data () {
+    return {
+    }
+  },
+  created () {},
+  methods: {}
+}
+</script>
+
+<style scoped lang="scss">
+.get-mission-container {
+  padding: 4px;
+  border-radius: 8px;
+  background: linear-gradient(90deg, #F55F1D 0%, #F5302D 100%);
+}
+.get-mission-title {
+  color: #fff;
+  font-size: 18px;
+  font-weight: 700;
+  line-height: 28px;
+  padding-top: 2px;
+  padding-left: 16px;
+  padding-bottom: 4px;
+}
+.get-mission-bd {
+  height: 88px;
+  border-radius: 8px;
+  background: #fff;
+}
+.get-mission-bd-content {
+  position: relative;
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  height: 100%;
+  padding: 0 16px;
+  overflow: hidden;
+}
+.gift-hands {
+  display: inline-block;
+  width: 80px;
+  height: 80px;
+  img {
+    display: block;
+    width: 100%;
+  }
+}
+.g-m-b-content-l {
+  display: flex;
+  align-items: flex-end;
+  height: 100%;
+}
+.g-m-b-content-c,
+.g-m-b-content-r {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+}
+.g-m-b-content-c-text {
+  margin-left: 8px;
+  color: #1D1D1D;
+  font-size: 16px;
+  line-height: 24px;
+}
+.g-m-b-content-r {
+  padding-right: 20px;
+}
+
+.g-m-b-content-r {
+  button:not(:last-of-type) {
+    margin-right: 24px;
+  }
+}
+
+.j-tag {
+  display: inline-block;
+  padding: 0 8px;
+  font-size: 12px;
+  line-height: 18px;
+  border: 1px solid #000;
+  border-radius: 4px;
+  &.tag-red {
+    color: #FF3A20;
+    border-color: #FF3A20;
+  }
+}
+
+.j-button {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  padding: 6px 0;
+  width: 132px;
+  color: #fff;
+  font-size: 16px;
+  line-height: 24px;
+  border-radius: 6px;
+  background: linear-gradient(90deg, #F55F1D 0%, #F5302D 100%);
+  border: 1px solid #FF3A20;
+  &.plain {
+    color: #FF3A20;
+    background: rgba(255, 58, 32, 0.08);
+  }
+}
+</style>

+ 185 - 0
jypoints-pc/src/components/PointOverview.vue

@@ -0,0 +1,185 @@
+<template>
+  <section class="point-overview">
+    <img class="overview-bg" src="@/assets/image/point-overview-bg@2x.png" alt="">
+    <div class="point-overview-content">
+      <div class="p-o-content-left">
+        <div class="content-left-l">
+          <p class="content-left-title">我的剑鱼币</p>
+          <div class="content-left-points">
+            <div class="content-count-container">
+              <span class="total-count">{{ points.balance }}</span>
+              <span class="j-icon icon-points"></span>
+              <span class="deduct-count">可抵扣<strong>1.05</strong>元</span>
+              <span class="content-left-expire">30天内到期:{{ points.expire }}</span>
+            </div>
+            <p class="content-left-tip">含200充值</p>
+          </div>
+        </div>
+        <p class="content-left-r">
+          <button class="recharge-button" @click="toRechargePage">立即充值</button>
+        </p>
+      </div>
+      <div class="p-o-content-right">
+        <el-link type="info" :underline="false" @click="toDetailPage">积分明细</el-link>
+        <el-link type="info" :underline="false" target="_blank" href="/swordfish/frontPage/integral/sess/jypoints_deduct_rules">积分规则</el-link>
+      </div>
+    </div>
+  </section>
+</template>
+
+<script>
+import { Tooltip, Button, Link } from 'element-ui'
+import { ajaxGetPoints } from '../api/modules/user'
+
+export default {
+  name: 'PointOverview',
+  components: {
+    [Tooltip.name]: Tooltip,
+    [Button.name]: Button,
+    [Link.name]: Link
+  },
+  data () {
+    return {
+      points: {
+        // 余额
+        balance: 1,
+        // 30天内到期数量
+        expire: 0
+      }
+    }
+  },
+  created () {
+    this.getPointsCount()
+  },
+  methods: {
+    getPointsCount () {
+      ajaxGetPoints({ B: true }).then(({ data: res }) => {
+        if (res.error_code == 0 && res.data) {
+          Object.assign(this.points, res.data)
+        }
+      }).catch(err => {
+        console.log(err)
+      })
+    },
+    toRechargePage () {
+      window.open('/swordfish/integral/index/recharge')
+    },
+    toDetailPage () {
+      this.$router.push('/detail')
+    }
+  }
+}
+</script>
+
+<style scoped lang="scss">
+::v-deep {
+  .el-link.el-link--info {
+    color: rgba(255, 255, 255, 0.8);
+  }
+}
+.point-overview {
+  position: relative;
+  width: 100%;
+  border-radius: 8px;
+  // background: linear-gradient(91deg, #0C0F4F 0.06%, #091732 99.23%);
+}
+.overview-bg {
+  display: block;
+  width: 100%;
+}
+.point-overview-content {
+  position: absolute;
+  left: 0;
+  top: 0;
+  width: 100%;
+  height: 84%;
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  font-size: 14px;
+  line-height: 22px;
+  color: #fff;
+  z-index: 2;
+  .icon-points {
+    width: 28px;
+    height: 28px;
+  }
+}
+.p-o-content-left {
+  display: flex;
+  align-items: center;
+  padding: 20px 40px;
+  color: #F7E1BC;
+  // background: var(--vip, linear-gradient(270deg, #F1D090 0%, #FAE7CA 100%));
+  // background-clip: text;
+  // -webkit-background-clip: text;
+  // -webkit-text-fill-color: transparent;
+}
+.content-left-points {
+  margin-top: 6px;
+}
+.content-count-container {
+  display: flex;
+  align-items: center;
+  height: 40px;
+}
+.deduct-count {
+  margin-left: 8px;
+  margin-right: 24px;
+  padding: 0 8px;
+  height: 28px;
+  position: relative;
+  display: flex;
+  align-items: center;
+  border-radius: 4px;
+  border: 2px solid #F1D090;
+  strong {
+    font-weight: bold;
+  }
+  &::before {
+    position: absolute;
+    left: -5px;
+    top: 10px;
+    content: '';
+    display: inline-block;
+    width: 6px;
+    height: 6px;
+    background-color: #0C0F4F;
+    border-left: 2px solid #F1D090;
+    border-bottom: 2px solid #F1D090;
+    transform: rotateZ(45deg);
+  }
+}
+
+.content-left-c {
+  color: rgba(255, 255, 255, 0.8);
+}
+.content-left-r {
+  margin-left: 72px;
+}
+
+.content-left-tip {
+  font-size: 12px;
+  line-height: 18px;
+}
+.recharge-button {
+  padding: 8px 34px;
+  color: #1D1D1D;
+  font-size: 16px;
+  border-radius: 6px;
+  background: linear-gradient(270deg, #F1D090 0%, #FAE7CA 100%);
+}
+.p-o-content-right {
+  padding: 12px 24px;
+  align-self: flex-start;
+  .el-link {
+    margin-left: 16px;
+  }
+}
+.total-count {
+  margin-right: 4px;
+  min-width: 30px;
+  font-size: 32px;
+  text-align: center;
+}
+</style>

+ 126 - 0
jypoints-pc/src/components/common/dialog.vue

@@ -0,0 +1,126 @@
+<template>
+  <el-dialog
+    :custom-class="customClass"
+    v-bind="$props"
+    :visible="visible"
+    @update:visible="update"
+    @open="$emit('open')"
+    @opened="$emit('opened')"
+    @close="$emit('close')"
+    @closed="$emit('closed')"
+  >
+    <slot name="default"></slot>
+    <span slot="footer" class="dialog-footer">
+      <slot name="footer">
+        <button class="action-button confirm"  :disabled="disabled" @click="onClickConfirm">确定</button>
+        <button class="action-button cancel" @click="onClickCancel">取消</button>
+      </slot>
+    </span>
+    </el-dialog>
+</template>
+
+<script>
+import { Dialog, Button } from 'element-ui'
+
+export default {
+  name: 'CustomDialog',
+  components: {
+    [Dialog.name]: Dialog,
+    [Button.name]: Button
+  },
+  props: {
+    visible: Boolean,
+    top: String,
+    title: {
+      type: String,
+      default: ''
+    },
+    width: {
+      type: String,
+      default: '30%'
+    },
+    'show-close': {
+      type: Boolean,
+      default: false
+    },
+    center: {
+      type: Boolean,
+      default: true
+    },
+    customClass: {
+      type: String,
+      default: ''
+    },
+    disabled: Boolean
+  },
+  methods: {
+    update (e) {
+      this.$emit('update:visible', e)
+    },
+    onClickCancel () {
+      this.$emit('cancel')
+    },
+    onClickConfirm () {
+      this.$emit('confirm')
+    }
+  }
+}
+</script>
+
+<style scoped lang="scss">
+::v-deep {
+  .el-dialog {
+    border-radius: 8px;
+  }
+  .el-dialog__header {
+    padding: 32px 32px 0;
+  }
+  .el-dialog__body {
+    padding: 20px 32px 32px;
+    color: #686868;
+    font-size: 14px;
+    line-height: 22px;
+  }
+  .el-dialog__footer {
+    padding: 0 32px 32px;
+  }
+  .dialog-footer {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+  }
+  .action-button {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    flex: 1;
+    height: 36px;
+    font-size: 16px;
+    line-height: 24px;
+    border-radius: 6px;
+    &.cancel {
+      border: 1px solid #e0e0e0;
+      background-color: #fff;
+      color: #686868;
+    }
+    &.confirm {
+      border: 1px solid $color_main;
+      background-color: $color_main;
+      color: #fff;
+      &:disabled{
+        opacity: 0.5;
+      }
+    }
+    &:not(:last-of-type) {
+      margin-right: 48px;
+    }
+  }
+}
+.text-center {
+  ::v-deep {
+    .el-dialog__body {
+      text-align: center;
+    }
+  }
+}
+</style>

+ 251 - 0
jypoints-pc/src/components/common/exchange.vue

@@ -0,0 +1,251 @@
+<template>
+  <section class="exchange-container">
+    <div class="icon-img-container">
+      <slot name="hd-img"></slot>
+    </div>
+    <div class="exchange-main">
+      <div class="exchange-main-row exchange-title">
+        <div class="title-text">{{ title }}</div>
+        <div class="fr price">
+          <div class="price-num">{{ priceNum }}{{ priceUnit }}</div>
+        </div>
+      </div>
+      <div class="exchange-main-row exchange-main-desc">
+        <p class="exchange-main-desc-text">
+          <slot name="desc"></slot>
+        </p>
+        <div class="exchange-set-count">
+          <span class="exchange-set-count-l">数量</span>
+          <span class="exchange-set-count-c">
+            <JStepper
+              v-model="exchange.count"
+              @change="onExchangeCountChange($event)"
+              integer>
+            </JStepper>
+          </span>
+          <span class="exchange-set-count-r">
+            <span>合计</span>
+            <span class="total-container">
+              <span class="total-num">{{ useTotalCount }}</span>
+              <span class="total-unit">&nbsp;剑鱼币</span>
+            </span>
+          </span>
+        </div>
+      </div>
+      <div class="exchange-main-row exchange-main-sub-desc">
+        <div class="sub-desc-text">
+          <slot name="sub-desc"></slot>
+        </div>
+        <span class="sub-desc-actions">
+          <span class="no-point-tip">当前剩余 {{ pointBalance }} 剑鱼币,余额不足。去 <span class="highlight-text" @click="toEarn">赚剑鱼币></span></span>
+          <button class="confirm-button" :disabled="confirmDisabled" @click.stop="doExchange">兑换</button>
+        </span>
+      </div>
+    </div>
+  </section>
+</template>
+
+<script>
+import JStepper from '@/components/common/stepper.vue'
+
+export default {
+  name: 'ExchangeItem',
+  components: {
+    JStepper
+  },
+  props: {
+    // 剑鱼币余额
+    pointBalance: {
+      type: Number,
+      default: 0
+    },
+    title: {
+      type: String,
+      default: ''
+    },
+    priceNum: {
+      type: Number,
+      default: 350
+    },
+    priceUnit: {
+      type: String,
+      default: '/个'
+    }
+  },
+  data () {
+    return {
+      exchange: {
+        // 兑换n个权益
+        count: 1
+      }
+    }
+  },
+  computed: {
+    useTotalCount () {
+      return this.priceNum * this.exchange.count
+    },
+    canBuy () {
+      return this.pointBalance >= this.useTotalCount
+    },
+    confirmDisabled () {
+      return !this.canBuy
+    }
+  },
+  created () {},
+  methods: {
+    onExchangeCountChange (e) {},
+    doExchange () {
+      const payload = {
+        // 兑换n个权益
+        count: this.exchange.count,
+        // 兑换n个权益,总共需要花费的剑鱼币数量
+        totalPayCount: this.useTotalCount
+      }
+      this.$emit('exchange', payload)
+    },
+    toEarn () {
+      this.$router.push('/earn')
+    }
+  }
+}
+</script>
+
+<style scoped lang="scss">
+$red: #FF3A20;
+$vip_color: #B1700E;
+
+.j-stepper-container {
+  $height: 24px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  height: $height;
+  ::v-deep {
+    .j-stepper-button,
+    .j-stepper-content {
+      height: $height;
+    }
+  }
+}
+
+.confirm-button {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  width: 90px;
+  height: 30px;
+  line-height: 30px;
+  font-size: 13px;
+  border-radius: 4px;
+  text-align: center;
+  color: #1D1D1D;
+  background: linear-gradient(270deg, #F1D090 0%, #FAE7CA 100%);
+  &:disabled {
+    opacity: .6;
+  }
+}
+
+.exchange-container {
+  display: flex;
+  justify-content: space-between;
+  padding: 20px;
+  border-radius: 16px;
+  background: #FFF;
+}
+.icon-img-container {
+  position: relative;
+  width: 48px;
+  height: 48px;
+  border-radius: 4px;
+  background: #FFf;
+  box-sizing: border-box;
+  .icon-img,
+  img {
+    width: 100%;
+    display: block;
+  }
+}
+
+.exchange-main{
+  margin-left: 20px;
+  flex: 1;
+
+  .exchange-main-row,
+  .sub-desc-actions {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+  }
+  .title-text{
+    display: inline-block;
+    color: #1D1D1D;
+    font-size: 16px;
+    line-height: 24px;
+  }
+
+  .exchange-main-desc {
+    margin-top: 2px;
+    margin-bottom: 8px;
+  }
+  .exchange-main-desc-text {
+    color:  #686868;
+    font-size: 14px;
+    line-height: 22px;
+  }
+
+  .exchange-set-count {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    color: #686868;
+    font-size: 14px;
+    line-height: 22px;
+  }
+  .exchange-set-count-c {
+    margin-left: 8px;
+    margin-right: 20px;
+  }
+  .exchange-set-count-r {
+    display: inline-block;
+    min-width: 140px;
+  }
+  .total-container {
+    margin-left: 8px;
+    color: $red;
+    font-size: 14px;
+    line-height: 22px;
+    .total-num {
+      font-size: 18px;
+      line-height: 28px;
+    }
+  }
+
+  .sub-desc-text {
+    color: #999;
+    font-size: 12px; 
+    line-height: 18px;
+  }
+
+  .no-point-tip {
+    margin-right: 34px;
+    color: #999;
+    font-size: 14px;
+    line-height: 22px;
+  }
+
+  .price{
+    position: relative;
+    text-align: right;
+    .price-num{
+      display: inline-block;
+      color: $red;
+      font-size: 16px;
+      line-height: 24px;
+      padding-left: 28px;
+      background: url("~@/assets/image/jianyuIcon@2x.png") no-repeat left center;
+      background-size: 24px 24px;
+    }
+  }
+}
+</style>
+  

+ 78 - 0
jypoints-pc/src/components/common/number.js

@@ -0,0 +1,78 @@
+/**
+ * 取中位数
+ * @param {number} num
+ * @param {number} min
+ * @param {number} max
+ * @returns number
+ */
+export function range (num, min, max) {
+  return Math.min(Math.max(num, min), max)
+}
+
+/**
+ * 移除多余的字符
+ * @param {string} value
+ * @param {string} char
+ * @param {RegExp} regExp
+ * @returns string
+ */
+function trimExtraChar (value, char, regExp) {
+  const index = value.indexOf(char)
+  let prefix = ''
+
+  if (index === -1) {
+    return value
+  }
+
+  if (char === '-' && index !== 0) {
+    return value.slice(0, index)
+  }
+
+  if (char === '.' && value.match(/^(\.|-\.)/)) {
+    prefix = index ? '-0' : '0'
+  }
+
+  return (
+    prefix + value.slice(0, index + 1) + value.slice(index).replace(regExp, '')
+  )
+}
+
+/**
+ * 格式化数字字符串
+ * @param {string} value
+ * @param {Boolean} allowDot
+ * @param {Boolean} allowMinus
+ * @returns string
+ */
+export function formatNumber (value, allowDot = true, allowMinus = true) {
+  if (allowDot) {
+    value = trimExtraChar(value, '.', /\./g)
+  } else {
+    value = value.split('.')[0]
+  }
+
+  if (allowMinus) {
+    value = trimExtraChar(value, '-', /-/g)
+  } else {
+    value = value.replace(/-/, '')
+  }
+
+  const regExp = allowDot ? /[^-0-9.]/g : /[^-0-9]/g
+
+  return value.replace(regExp, '')
+}
+
+/**
+ * add num and avoid float number
+ * @param {number} num1
+ * @param {number} num2
+ * @returns number
+ */
+export function addNumber (num1, num2) {
+  const cardinal = 10 ** 10
+  return Math.round((num1 + num2) * cardinal) / cardinal
+}
+
+export function equal (value1, value2) {
+  return String(value1) === String(value2)
+}

+ 291 - 0
jypoints-pc/src/components/common/stepper.vue

@@ -0,0 +1,291 @@
+<template>
+  <!-- 参考https://github.com/youzan/vant/blob/2.x/src/stepper/index.js -->
+  <!-- 当前组件仅支持整数,不支持小数 -->
+  <span class="j-stepper-container">
+    <div class="j-stepper">
+      <el-button
+        class="j-stepper-button"
+        icon="el-icon-minus"
+        plain
+        @click="minusAction"
+        :disabled="minusDisabled"
+      ></el-button>
+      <div class="j-stepper-content">
+        <input
+          :type="integer ? 'tel' : 'text'"
+          v-show="showInput"
+          :name="name"
+          :value="currentValue"
+          :readonly="disableInput"
+          @input="onInput"
+          @focus="onFocus"
+          @blur="onBlur"
+          class="j-stepper-input-inner"
+        />
+        <div class="content-suffix">
+          <slot name="content-suffix-text"></slot>
+        </div>
+      </div>
+      <el-button
+        class="j-stepper-button"
+        plain
+        icon="el-icon-plus"
+        @click="plusAction"
+        :disabled="plusDisabled"
+      ></el-button>
+    </div>
+  </span>
+</template>
+
+<script>
+import { Button } from 'element-ui'
+import { addNumber, formatNumber, equal } from './number'
+
+function isDef (val) {
+  return val !== undefined && val !== null
+}
+
+export default {
+  name: 'JStepper',
+  components: {
+    [Button.name]: Button
+  },
+  props: {
+    name: [String],
+    value: [Number, String],
+    // 只允许输入整数
+    integer: Boolean,
+    // 是否禁用步进器
+    disabled: Boolean,
+    // 是否禁用增加按钮
+    disablePlus: Boolean,
+    // 是否禁用减少按钮
+    disableMinus: Boolean,
+    // 是否禁用输入框
+    disableInput: Boolean,
+    defaultValue: {
+      type: [Number, String],
+      default: 1
+    },
+    // 步长
+    step: {
+      type: [Number],
+      default: 1
+    },
+    // 最小值
+    min: {
+      type: [Number],
+      default: 1
+    },
+    // 最大值
+    max: {
+      type: [Number],
+      default: Infinity
+    },
+    // 是否显示输入框
+    showInput: {
+      type: Boolean,
+      default: true
+    }
+  },
+  data () {
+    const defaultValue = this.value ?? this.defaultValue
+    const value = this.format(defaultValue)
+
+    if (!equal(value, this.value)) {
+      this.$emit('input', value)
+    }
+
+    return {
+      type: '',
+      currentValue: value
+    }
+  },
+  computed: {
+    minusDisabled () {
+      return (
+        // +this.min 隐式转换为数字
+        this.disabled || this.disableMinus || this.currentValue <= +this.min
+      )
+    },
+    plusDisabled () {
+      return (
+        this.disabled || this.disablePlus || this.currentValue >= +this.max
+      )
+    }
+  },
+  watch: {
+    max: 'check',
+    min: 'check',
+    integer: 'check',
+    decimalLength: 'check',
+
+    value (val) {
+      if (!equal(val, this.currentValue)) {
+        this.currentValue = this.format(val)
+      }
+    },
+
+    currentValue (val) {
+      this.$emit('input', val)
+      this.$emit('change', val, { name: this.name })
+    }
+  },
+  methods: {
+    minusAction () {
+      this.type = 'minus'
+      this.onChange()
+    },
+    plusAction () {
+      this.type = 'plus'
+      this.onChange()
+    },
+    check () {
+      const val = this.format(this.currentValue)
+      if (!equal(val, this.currentValue)) {
+        this.currentValue = val
+      }
+    },
+    // formatNumber illegal characters
+    formatNumber (value) {
+      return formatNumber(String(value), !this.integer)
+    },
+
+    format (value) {
+      if (this.allowEmpty && value === '') {
+        return value
+      }
+
+      value = this.formatNumber(value)
+
+      // format range
+      value = value === '' ? 0 : +value
+      value = isNaN(value) ? this.min : value
+      value = Math.max(Math.min(this.max, value), this.min)
+
+      // format decimal
+      if (isDef(this.decimalLength)) {
+        value = value.toFixed(this.decimalLength)
+      }
+
+      return value
+    },
+
+    onInput (event) {
+      const { value } = event.target
+
+      let formatted = this.formatNumber(value)
+
+      // limit max decimal length
+      if (isDef(this.decimalLength) && formatted.indexOf('.') !== -1) {
+        const pair = formatted.split('.')
+        formatted = `${pair[0]}.${pair[1].slice(0, this.decimalLength)}`
+      }
+
+      if (!equal(value, formatted)) {
+        event.target.value = formatted
+      }
+
+      // prefer number type
+      if (formatted === String(+formatted)) {
+        formatted = +formatted
+      }
+
+      this.emitChange(formatted)
+    },
+
+    emitChange (value) {
+      if (this.asyncChange) {
+        this.$emit('input', value)
+        this.$emit('change', value, { name: this.name })
+      } else {
+        this.currentValue = value
+      }
+    },
+
+    onChange () {
+      const { type } = this
+
+      if (this[`${type}Disabled`]) {
+        this.$emit('overlimit', type)
+        return
+      }
+
+      const diff = type === 'minus' ? -this.step : +this.step
+
+      const value = this.format(addNumber(+this.currentValue, diff))
+
+      this.emitChange(value)
+      this.$emit(type)
+    },
+
+    onFocus (event) {
+      // readonly not work in legacy mobile safari
+      if (this.disableInput && this.$refs.input) {
+        this.$refs.input.blur()
+      } else {
+        this.$emit('focus', event)
+      }
+    },
+
+    onBlur (event) {
+      const value = this.format(event.target.value)
+      event.target.value = value
+      this.emitChange(value)
+      this.$emit('blur', event)
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+$main: #2cb7ca;
+$white: #fff;
+$height: 30px;
+
+.j-stepper-container {
+  display: inline-block;
+  height: $height + 4px;
+  background-color: $white;
+}
+.j-stepper {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  &-content {
+    display: flex;
+    align-items: center;
+    margin: 0 2px;
+    padding: 0 8px;
+    height: $height;
+    font-size: 18px;
+    line-height: 26px;
+    color: #171826;
+    border: 1px solid #E0E0E0;
+  }
+  &-input-inner {
+    height: $height;
+    width: 32px;
+    font-size: 14px;
+    text-align: center;
+    background-color: transparent;
+  }
+  &-button {
+    padding: 0;
+    height: $height;
+    width: $height;
+    border: 1px solid #E0E0E0;
+    border-radius: 2px;
+    &:active {
+      border-color: $main;
+      color: $main;
+      background-color: #F7F7F7;
+    }
+  }
+}
+.content-suffix {
+  font-size: 14px;
+  line-height: 22px;
+  white-space: nowrap;
+}
+</style>

+ 7 - 1
jypoints-pc/src/main.js

@@ -1,11 +1,13 @@
 import '@jianyu/easy-inject-qiankun/src/pre-mount'
 import Vue from 'vue';
 import App from './App.vue';
+import store from './store/'
 import router from './router';
 import { easySubAppRegister } from '@jianyu/easy-inject-qiankun'
 import { fixGetComputedStyle } from '@jianyu/easy-fix-sub-app/lib/getComputedStyle'
 import './utils/jq-help'
 import infiniteScroll from "vue-infinite-scroll";
+import { Loading, Message, MessageBox } from 'element-ui'
 
 // 正式环境下屏蔽console.log
 if (process.env.NODE_ENV === 'production') {
@@ -18,8 +20,12 @@ if (process.env.NODE_ENV === 'production') {
 }
 
 Vue.use(infiniteScroll);
+Vue.use(Loading.directive)
 
-Vue.config.productionTip = false;
+Vue.prototype.$message = Message
+Vue.prototype.$alert = MessageBox.alert
+Vue.prototype.$confirm = MessageBox.confirm
+Vue.config.productionTip = false
 
 export const { bootstrap, mount, unmount } = easySubAppRegister({
   Vue,

+ 11 - 2
jypoints-pc/src/router.js

@@ -1,6 +1,5 @@
 import Vue from 'vue'
 import Router from 'vue-router'
-import Home from './views/Home.vue'
 
 Vue.use(Router)
 
@@ -10,8 +9,18 @@ export default new Router({
   routes: [
     {
       path: '/',
+      name: 'point',
+      component: () => import('@/views/MyPoint.vue') // 我的剑鱼币
+    },
+    {
+      path: '/earn',
+      name: 'earn',
+      component: () => import('@/views/Earn.vue') // 赚剑鱼币
+    },
+    {
+      path: '/old',
       name: 'home',
-      component: Home
+      component: () => import('@/views/Home.vue')
     },
     {
       path: '/detail',

+ 22 - 0
jypoints-pc/src/store/index.js

@@ -0,0 +1,22 @@
+import Vue from 'vue'
+import Vuex from 'vuex'
+
+import user from './user'
+import points from './points'
+
+if (process.env.NODE_ENV !== 'production') {
+  Vue.use(Vuex)
+}
+
+export default new Vuex.Store({
+  state: {
+    env: process.env.NODE_ENV
+  },
+  mutations: {},
+  actions: {},
+  getters: {},
+  modules: {
+    user,
+    points
+  }
+})

+ 30 - 0
jypoints-pc/src/store/points.js

@@ -0,0 +1,30 @@
+import { ajaxGetPoints } from '@/api/modules'
+
+export default {
+  namespaced: true,
+  state: () => ({
+    pointInfo: {
+      balance: 0, // 余额
+      expire: 0, // 30天内到期数量
+    }
+  }),
+  mutations: {
+    setPointInfo (state, i) {
+      state.pointInfo = i
+    }
+  },
+  actions: {
+    // 获取用户基本信息
+    async getUserPointInfo ({ commit }) {
+      try {
+        const { data, error_code: code } = await ajaxGetPoints()
+        if (code === 0 && data) {
+          commit('setPointInfo', data)
+        }
+        return data || {}
+      } catch (error) {
+        return {}
+      }
+    }
+  }
+}

+ 29 - 0
jypoints-pc/src/store/user.js

@@ -0,0 +1,29 @@
+import { defaultLocalPageData } from '@/utils/'
+import { getUserInfo } from '@/api/modules'
+
+export default {
+  namespaced: true,
+  state: () => ({
+    // 用户基本信息
+    userAccountInfo: {}
+  }),
+  mutations: {
+    setUserAccountInfo (state, i) {
+      state.userAccountInfo = i
+    }
+  },
+  actions: {
+    // 获取用户基本信息
+    async getUserAccountInfo ({ commit }) {
+      try {
+        const { data, error_code: code } = await getUserInfo()
+        if (code === 0 && data) {
+          commit('setUserAccountInfo', data)
+        }
+        return data || {}
+      } catch (error) {
+        return {}
+      }
+    }
+  }
+}

+ 146 - 0
jypoints-pc/src/views/MyPoint.vue

@@ -0,0 +1,146 @@
+<template>
+  <section class="my-point-container">
+    <section class="point-module my-point-overview">
+      <PointOverview></PointOverview>
+    </section>
+    <section class="point-module point-earn-module">
+      <GetMission></GetMission>
+    </section>
+    <section class="point-module exchange-module">
+      <el-tabs class="exchange-tabs" v-model="activeName" @tab-click="tabClick">
+        <el-tab-pane label="兑换附件下载" name="file">
+          <FileExchange></FileExchange>
+        </el-tab-pane>
+        <el-tab-pane label="兑换剑鱼文库" name="docs">
+          <DocsExchange></DocsExchange>
+        </el-tab-pane>
+      </el-tabs>
+    </section>
+  </section>
+</template>
+
+<script>
+import { Button, Tabs, TabPane } from 'element-ui'
+import detailPart from '@/components/detailPart.vue'
+import PointOverview from '@/components/PointOverview.vue'
+import GetMission from '@/components/GetMission.vue'
+import DocsExchange from '@/components/DocsExchange.vue'
+import FileExchange from '@/components/FileExchange.vue'
+
+export default {
+  name: 'detail',
+  components: {
+    detailPart,
+    [Button.name]: Button,
+    [Tabs.name]: Tabs,
+    [TabPane.name]: TabPane,
+    PointOverview,
+    GetMission,
+    DocsExchange,
+    FileExchange
+  },
+  data () {
+    return {
+      activeName: 'file', // file/docs
+      flag:'0',
+      pageObj:{
+        page:0,
+        pageSize: 10,
+      },
+      pageData:{
+        detailed:{
+          data:[],
+          count:10
+        }
+      },
+      loading:true
+    }
+  },
+  created(){
+      this.getData()
+  },
+  
+  methods: {
+    getData(){
+      // ajaxGetPoints({L:1,searchType:this.flag,page:this.pageObj.page,pageSize:this.pageObj.pageSize}).then(res =>{
+      //   this.loading = false;
+      //   this.pageData = res.data.data;
+      //   this.pageData.detailed.count = res.data.data.detailed.count
+      //   if (this.pageData.detailed.data && this.pageData.detailed.data.length > 0) {
+      //     this.pageData.detailed.data.forEach(v =>{
+      //       if(v.abstract){
+      //         v.msgData = JSON.parse(v.abstract)
+      //       }
+      //     })
+      //   }
+      // })
+    },
+    tabClick() {
+      this.pageObj.page = 1;
+      this.loading = true;
+      this.pageData = { 
+        detailed:{
+          data:[],
+          count:0
+        }
+      }
+      if(this.activeName == 'first'){
+        this.flag = '0'
+        this.getData()
+      }else if(this.activeName == 'second'){
+        this.flag = '1'
+        this.getData()
+      }else{
+        this.flag = '-1'
+        this.getData()
+      }
+    },
+    changePage(val){
+      this.pageObj.page = val
+      this.getData()
+    },
+    onSizeChange (size) {
+      this.pageObj.pageSize = size
+      this.pageObj.page = 1
+      this.getData()
+    }
+  }
+}
+</script>
+
+<style scoped lang="scss">
+$vip_color: #B1700E;
+
+.my-point-container {
+  padding: 24px;
+}
+.point-module {
+  margin-bottom: 26px;
+}
+.exchange-tabs {
+  ::v-deep {
+    .el-tabs__nav-wrap {
+      &::after {
+        content: unset;
+      }
+    }
+    .el-tabs__nav-scroll {
+      display: flex;
+      justify-content: center;
+    }
+
+    .el-tabs__item {
+      color: #686868;
+      &:hover {
+        color: $vip_color;
+      }
+      &.is-active {
+        color: $vip_color;
+      }
+    }
+    .el-tabs__active-bar {
+      background-color: $vip_color;
+    }
+  }
+}
+</style>