Explorar o código

feat: 新增移动端Card组件

cuiyalong %!s(int64=4) %!d(string=hai) anos
pai
achega
4c2fe00a06

+ 1 - 2
jydocs-mobile/src/api/index.ts

@@ -1,9 +1,8 @@
 import request from '@/api/ajax'
 import mock from '@/api/mock'
 
-let $request = request
+let $request: any = request
 if (process.env.NODE_ENV === 'development' && process.env.VUE_APP_MOCK === 'true') {
-  // @ts-ignore
   $request = mock
 }
 

+ 6 - 13
jydocs-mobile/src/components/Search.vue

@@ -1,20 +1,19 @@
 <template>
-  <van-sticky class="van-fade-an" :offset-top="Offset">
+  <div class="search-container van-fade-an">
       <div class="click-pop" v-if="type === 'click'"  @click="onClick"></div>
       <van-search @input="$emit('input', $event)" @search="onSearch" class="my-search" left-icon="diy-search" v-model.trim="input" placeholder="搜索文档" />
-  </van-sticky>
+  </div>
 </template>
 
 <script lang="ts">
 import { Component, Vue, Prop } from 'vue-property-decorator'
-import { Icon, Search, Sticky } from 'vant'
+import { Icon, Search } from 'vant'
 
 // @ is an alias to /src
   @Component({
     name: 'Search',
     components: {
       [Search.name]: Search,
-      [Sticky.name]: Sticky,
       [Icon.name]: Icon
     }
   })
@@ -31,15 +30,6 @@ export default class Empty extends Vue {
         this.$emit('click')
       }
     }
-
-    get Offset () {
-      const tempN = document.querySelector('.j-header.jy-app-header') as HTMLDivElement
-      if (tempN) {
-        return tempN.offsetHeight - 1
-      } else {
-        return 0
-      }
-    }
 }
 </script>
 
@@ -62,6 +52,9 @@ export default class Empty extends Vue {
   z-index: 99;
   opacity: 0;
 }
+.search-container {
+  width: 100%;
+}
 .my-search {
   &::v-deep.van-search {
     padding: 7px 16px;

+ 211 - 0
jydocs-mobile/src/components/docs-card/Card.vue

@@ -0,0 +1,211 @@
+<template>
+  <div class="doc-container" @click="clickCard">
+    <div class="docs-card oneline" v-if="cardType === 'oneline'" key="docs-card">
+      <div class="docs-header flex-r-c center">
+        <van-icon :name="docTypeIcon" />
+        <div class="d-title flex van-ellipsis" v-html="hightLightTitle"></div>
+        <Price :price="price" />
+      </div>
+    </div>
+    <div class="docs-card image flex-r-c" v-if="cardType === 'image'" key="docs-card">
+      <div class="image-container">
+        <img v-lazy="imageSrc" />
+        <van-icon class="doc-type-icon" :name="docTypeIcon" />
+      </div>
+      <div class="image-info-container flex-c-c flex">
+        <div class="d-title van-multi-ellipsis--l2">{{ title }}</div>
+        <div class="card-info-item">
+          <div>
+            <div class="card-info-item uploader" v-if="uploader">贡献者:{{ uploader }}</div>
+            <div class="subinfo-container subinfo-items">
+              <span
+                class="subinfo-item"
+                :class="index === subInfo.length - 1 ? 'last' : ''"
+                v-for="(item, index) in subInfo"
+                :key="index"
+              >{{ item }}</span>
+            </div>
+          </div>
+          <div class="card-info-item price">
+            <Price :price="price" />
+          </div>
+        </div>
+      </div>
+    </div>
+    <div class="docs-card" v-else key="docs-card">
+      <div class="docs-header flex-r-c center">
+        <van-icon :name="docTypeIcon" />
+        <div class="d-title flex van-ellipsis" v-html="hightLightTitle"></div>
+      </div>
+      <div class="docs-content docs-desc van-multi-ellipsis--l2" v-if="hightLightDesc" v-html="hightLightDesc"></div>
+      <div class="docs-footer flex-r-c">
+        <div class="c-f-left subinfo-container">
+          <span
+            class="f-l-item subinfo-item card-time"
+            :class="index === subInfo.length - 1 ? 'last' : ''"
+            v-for="(item, index) in subInfo"
+            :key="index"
+          >{{ item }}</span>
+        </div>
+        <div class="c-f-right flex-r-c">
+          <Price :price="price" />
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+<script lang="ts">
+import { Component, Vue, Prop } from 'vue-property-decorator'
+import Price from '@/components/docs-card/Price.vue'
+import { Icon } from 'vant'
+import { replaceKeyword } from '@/utils/globalFunctions'
+
+@Component({
+  name: 'docs-card',
+  components: {
+    [Icon.name]: Icon,
+    Price
+  }
+})
+
+export default class DocsCard extends Vue {
+  @Prop({ default: '' }) title!: string;
+  @Prop({ default: '' }) cardType?: string | undefined; // oneline, image
+  @Prop({ default: [] }) highlightKey?: Array<string> | undefined;
+  @Prop({ default: '' }) desc?: string | undefined;
+  @Prop({ default: 'pdf' }) docType?: string | undefined; // 文档类型
+  @Prop({ default: [] }) subInfo?: Array<string> | undefined;
+  @Prop({ default: '' }) imageSrc?: string | undefined;
+  @Prop({ default: '' }) uploader?: string | undefined;
+  @Prop({ default: 0 }) price?: string | number | undefined;
+
+  created () {
+    console.log(123)
+  }
+
+  get docTypeIcon () {
+    return `diy-${this.docType}`
+  }
+
+  get hightLightTitle () {
+    return replaceKeyword(this.title, this.highlightKey, [
+      '<span class="highlight-text">',
+      '</span>'
+    ])
+  }
+
+  get hightLightDesc () {
+    return replaceKeyword(this.desc, this.highlightKey, [
+      '<span class="highlight-text">',
+      '</span>'
+    ])
+  }
+
+  clickCard () {
+    this.$emit('onClick')
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.docs-card {
+  padding: 16px;
+  box-sizing: border-box;
+  background-color: #fff;
+  &.oneline {
+    padding: 8px 16px;
+    .docs-header {
+      .d-title {
+        margin: 0 6px;
+      }
+    }
+  }
+
+  @include diy-icon('pdf', 24);
+  @include diy-icon('word', 24);
+  @include diy-icon('excel', 24);
+  @include diy-icon('ppt', 24);
+
+  .docs-header {
+    .d-title {
+      margin-left: 4px;
+      font-size: 16px;
+      line-height: 24px;
+      color: #171826;
+    }
+  }
+
+  .docs-content {
+    margin-top: 8px;
+    font-size: 13px;
+    line-height: 20px;
+    color: #5F5E64;
+  }
+
+  .docs-footer {
+    margin-top: 8px;
+  }
+
+  .subinfo-container {
+    color: #999;
+    font-size: 12px;
+    line-height: 18px;
+    .subinfo-item {
+      position: relative;
+      margin-right: 14px;
+
+      &.noline:after,
+      &.last:after {
+        content: unset;
+      }
+
+      &:after {
+        content: "";
+        position: absolute;
+        top: 50%;
+        right: -8px;
+        margin-top: -6px;
+        width: 1px;
+        height: 12px;
+        background-color: rgba($color: #000, $alpha: 0.05);
+      }
+    }
+  }
+
+  .image-container {
+    position: relative;
+    width: 100px;
+    height: 124px;
+    border: 1px solid rgba($color: #000, $alpha: 0.05);
+    border-radius: 6px;
+    overflow: hidden;
+    img {
+      width: 100%;
+      height: 100%;
+    }
+    .doc-type-icon {
+      position: absolute;
+      right: 2px;
+      bottom: 3px;
+    }
+  }
+  .image-info-container {
+    margin-left: 17px;
+    .card-info-item {
+      margin-top: 2px;
+    }
+    .uploader {
+      color: #999;
+      font-size: 12px;
+      line-height: 18px;
+    }
+    .price {
+      margin-top: 8px;
+    }
+  }
+
+  &:active {
+    background-color: #f5f6f7;
+  }
+}
+</style>

+ 35 - 0
jydocs-mobile/src/components/docs-card/Price.vue

@@ -0,0 +1,35 @@
+<template>
+  <div class="price-container">
+    <van-icon name="diy-iconLogoLight" />
+    <span class="p-r">{{ price }}</span>
+  </div>
+</template>
+<script lang="ts">
+import { Component, Vue, Prop } from 'vue-property-decorator'
+import { Icon } from 'vant'
+
+@Component({
+  name: 'docs-price',
+  components: {
+    [Icon.name]: Icon
+  }
+})
+
+export default class Price extends Vue {
+  @Prop({ default: '' }) price?: string | undefined;
+}
+</script>
+
+<style lang="scss" scoped>
+.price-container {
+  display: flex;
+  align-items: center;
+  @include diy-icon('iconLogoLight', 22, 22);
+
+  .p-r {
+    margin-left: 4px;
+    font-size: 18px;
+    color: #ff3a20;
+  }
+}
+</style>

+ 78 - 0
jydocs-mobile/src/utils/globalFunctions.ts

@@ -311,3 +311,81 @@ export function removeSpace (str: string) {
   }
   return str.replace(/\s+/g, '')
 }
+
+/**
+ * 通用关键字高亮替换
+ * @param {String} value 要高亮的字符串
+ * @param {String|Array} oldChar 要被替换的字符串(或数组)
+ * @param {String|Array} newChar 要替换成的字符串(或数组)
+ *
+ * 比如:要将 - `剑鱼标讯工具函数` 字符串中的 `工具` 高亮
+ * 则此时 value -> `剑鱼标讯工具函数`
+ *        oldChar -> `工具`
+ *        newChar -> `<span class="highlight-text">工具</span>`
+ *
+ * 批量高亮-----
+ * 比如:要将 - `剑鱼标讯工具函数` 字符串中的 `工具` `剑鱼` 高亮
+ * 则此时 value -> `剑鱼标讯工具函数批量高亮`
+ *        oldChar -> ['工具', '剑鱼']
+ *        newChar -> ['<span class="highlight-text">', '</span>']
+ *
+ *   注意:此时newChar为一个长度为2的数组,数组中为高亮标签的起始标签和结束标签
+ *
+ */
+export function replaceKeyword (value: any | undefined, oldChar: any, newChar: any) {
+  if (!oldChar || !newChar) return value
+  // oldChar的字符串数组,用来循环替换
+  let oldCharArr = []
+
+  if (Array.isArray(oldChar)) {
+    oldCharArr = oldChar.concat()
+  } else {
+    oldCharArr.push(oldChar)
+  }
+
+  // 数组去重
+  oldCharArr = Array.from(new Set(oldCharArr))
+
+  try {
+    oldCharArr.forEach(function (item) {
+      // 去空格之后为空字符串,则直接跳过当前替换
+      if (item.replace(/\s+/g, '')) {
+        let oc = item
+        oc = oc.replace(/\$/g, '\\$')
+          .replace(/\(/g, '\\(')
+          .replace(/\)/g, '\\)')
+          .replace(/\*/g, '\\*')
+          .replace(/\+/g, '\\+')
+          .replace(/\./g, '\\.')
+          .replace(/\[/g, '\\[')
+          .replace(/\]/g, '\\]')
+          .replace(/\?/g, '\\?')
+          .replace(/\\/g, '\\')
+          .replace(/\//g, '\\/')
+          .replace(/\^/g, '\\^')
+          .replace(/\{/g, '\\{')
+          .replace(/\}/g, '\\}')
+          .replace(/\|/g, '\\|')
+
+        if (Array.isArray(newChar)) {
+          // 批量高亮
+          const tempV = value
+          value = value.replace(new RegExp('(' + oc + ')', 'gmi'), newChar[0] + oc + newChar[1])
+          if (value === tempV && oc.indexOf('+') !== -1) {
+            const splitReg = oc.split('\\+')
+            splitReg.map(function (v: any) {
+              value = value.replace(new RegExp('(' + v + ')', 'gmi'), newChar[0] + v + newChar[1])
+            })
+          }
+        } else {
+          // 普通单个高亮
+          value = value.replace(new RegExp('(' + oc + ')', 'gmi'), newChar)
+        }
+      }
+    })
+  } catch (e) {
+    console.log(e)
+    return value
+  }
+  return value
+}

+ 4 - 4
jydocs-pc/src/components/doc-item-card/Card.vue

@@ -154,10 +154,10 @@ export default {
   border-bottom: 1px solid rgba($color: #000, $alpha: 0.05);
   cursor: pointer;
 
-  @include diy-icon("word", 24);
-  @include diy-icon("excel", 24);
-  @include diy-icon("ppt", 24);
-  @include diy-icon("pdf", 24);
+  @include diy-icon("word");
+  @include diy-icon("excel");
+  @include diy-icon("ppt");
+  @include diy-icon("pdf");
 
   .docs-card-header {
     font-size: 16px;

+ 1 - 1
jydocs-pc/src/components/doc-item-card/Price.vue

@@ -21,7 +21,7 @@ export default {
 .price-container {
   display: flex;
   align-items: center;
-  @include diy-icon("iconLogoLight", 24);
+  @include diy-icon("iconLogoLight");
 
   .p-r {
     margin-left: 4px;