浏览代码

Merge branch 'next' into next

yangxiaolu1993 3 年之前
父节点
当前提交
6cbeba3af9

+ 9 - 0
src/config.json

@@ -1166,6 +1166,15 @@
           "exportEmpty": true,
           "author": "liqiong43",
           "taro": true
+        },{
+          "name": "Comment",
+          "cType": "特色组件",
+          "cName": "商品评论",
+          "desc": "商品评论",
+          "show": true,
+          "tarodoc": false,
+          "type": "component",
+          "author": "yangxiaolu"
         }
       ]
     }

+ 44 - 0
src/packages/__VUE/comment/__tests__/comment.spec.ts

@@ -0,0 +1,44 @@
+import { config, mount } from '@vue/test-utils';
+import { nextTick, toRefs, reactive } from 'vue';
+import NutIcon from '../../icon/index.vue';
+import NutRate from '../../rate/index.vue';
+import Comment from '../index.vue';
+// import { Sku as SkuData, Goods } from '../data';
+import CommentData from '../comment_data.json';
+
+beforeAll(() => {
+  config.global.components = {
+    NutIcon,
+    NutRate
+  };
+});
+
+afterAll(() => {
+  config.global.components = {};
+});
+
+test('comment init render', async () => {
+  const wrapper = mount(Comment, {
+    props: {
+      images: CommentData.Comment.images,
+      videos: CommentData.Comment.videos,
+      info: CommentData.Comment.info
+    }
+  });
+  await nextTick();
+  expect(wrapper.find('.nut-comment__main').exists()).toBeTruthy();
+});
+
+test('comment mutli images', async () => {
+  const wrapper = mount(Comment, {
+    props: {
+      headerType: 'complex',
+      imagesRows: 'multi',
+      images: CommentData.Comment.images,
+      videos: CommentData.Comment.videos,
+      info: CommentData.Comment.info
+    }
+  });
+  await nextTick();
+  expect(wrapper.find('.nut-comment-images__mask').exists()).toBeTruthy();
+});

文件差异内容过多而无法显示
+ 1 - 0
src/packages/__VUE/comment/comment_data.json


+ 82 - 0
src/packages/__VUE/comment/components/CmtBottom.vue

@@ -0,0 +1,82 @@
+<template>
+  <view class="nut-comment-bottom">
+    <view @click="handleClick" class="nut-comment-bottom__lable">
+      <span v-if="type != 'complex'">{{ info.size }}</span></view
+    >
+
+    <view class="nut-comment-bottom__cpx">
+      <template v-for="(name, i) in mergeOp" :key="i">
+        <view :class="['nut-comment-bottom__cpx-item', `nut-comment-bottom__cpx-item--${name}`]" @click="operate(name)">
+          <template v-if="name != 'more'">
+            <span>{{ info[name] }}</span>
+            <nut-icon :name="name == 'like' ? 'fabulous' : 'comment'"></nut-icon>
+          </template>
+          <template v-if="name == 'more'">
+            <nut-icon name="more-x"></nut-icon>
+            <view class="nut-comment-bottom__cpx-item-popover" v-if="showPopver" @click="operate('popover')">{{
+              translate('complaintsText')
+            }}</view>
+          </template>
+        </view>
+      </template>
+    </view>
+  </view>
+</template>
+<script lang="ts">
+import { ref, watch, onMounted } from 'vue';
+
+import { createComponent } from '../../../utils/create';
+const { componentName, create, translate } = createComponent('comment-bottom');
+
+export default create({
+  props: {
+    type: {
+      type: String,
+      default: 'base' // simple,base,complex
+    },
+    info: {
+      type: Object,
+      default: () => {}
+    },
+
+    operation: {
+      type: Array,
+      default: ['replay', 'like', 'more']
+    }
+  },
+  components: {},
+  emits: ['clickOperate', 'handleClick'],
+
+  setup(props, { emit }) {
+    const showPopver = ref(false);
+
+    const mergeOp = ref([]);
+
+    onMounted(() => {
+      const deOp = ['replay', 'like', 'more'];
+
+      if (props.operation) {
+        props.operation.forEach((name: string) => {
+          if (deOp.includes(name)) {
+            (mergeOp.value as any).push(name);
+          }
+        });
+      }
+    });
+
+    const operate = (type: string) => {
+      if (type == 'more') {
+        showPopver.value = !showPopver.value;
+      }
+
+      emit('clickOperate', type);
+    };
+
+    const handleClick = () => {
+      emit('handleClick');
+    };
+
+    return { showPopver, operate, mergeOp, handleClick, translate };
+  }
+});
+</script>

+ 66 - 0
src/packages/__VUE/comment/components/CmtHeader.vue

@@ -0,0 +1,66 @@
+<template>
+  <view>
+    <view class="nut-comment-header" @click="handleClick" v-if="info">
+      <view class="nut-comment-header__user">
+        <view class="nut-comment-header__user-avter">
+          <img v-if="info.avatar" :src="info.avatar" />
+        </view>
+
+        <view :class="[`nut-comment-header__user-${type}`]" v-if="type == 'default'">
+          <view :class="[`nut-comment-header__user-${type}-name`]">
+            <span>{{ info.nickName }}</span>
+            <slot name="labels"></slot>
+          </view>
+
+          <view class="nut-comment-header__user-score">
+            <nut-rate v-model="info.score" icon-size="10" spacing="5" readOnly @change="handleClick" />
+          </view>
+        </view>
+
+        <view :class="[`nut-comment-header__user-${type}`]" v-else>
+          <span :class="[`nut-comment-header__user-${type}-name`]">{{ info.nickName }}</span>
+          <slot name="labels"></slot>
+        </view>
+      </view>
+      <view class="nut-comment-header__time" v-if="info.time">{{ info.time }}</view>
+    </view>
+    <view :class="[`nut-comment-header__${type}-score`]" v-if="type == 'complex'">
+      <nut-rate v-model="info.score" icon-size="12" spacing="3" />
+      <i :class="[`nut-comment-header__${type}-score-i`]"></i>
+      <view :class="[`nut-comment-header__${type}-score-size`]">{{ info.size }}</view>
+    </view>
+  </view>
+</template>
+<script lang="ts">
+import { ref, watch, onMounted } from 'vue';
+
+import { createComponent } from '../../../utils/create';
+const { componentName, create } = createComponent('comment-header');
+
+export default create({
+  props: {
+    type: {
+      type: String,
+      default: 'default' // default,complex
+    },
+    info: {
+      type: Object,
+      default: () => {}
+    },
+    labels: {
+      type: Function,
+      default: () => ''
+    }
+  },
+  components: {},
+  emits: ['handleClick'],
+
+  setup(props, { emit }) {
+    const handleClick = () => {
+      emit('handleClick');
+    };
+
+    return { handleClick };
+  }
+});
+</script>

+ 98 - 0
src/packages/__VUE/comment/components/CmtImages.vue

@@ -0,0 +1,98 @@
+<template>
+  <view :class="`nut-comment-images nut-comment-images--${type}`">
+    <!-- videos -->
+    <view
+      class="nut-comment-images__item nut-comment-images__item--video"
+      v-for="(itV, index) in videos"
+      :key="itV.id"
+      @click="showImages('video', index, index)"
+    >
+      <img :src="itV.mainUrl" />
+      <view class="nut-comment-images__play"></view>
+    </view>
+    <!-- images -->
+    <template v-for="(itI, index) in images" :key="itI.id">
+      <view
+        class="nut-comment-images__item nut-comment-images__item--imgbox"
+        v-if="(type == 'multi' && videos.length + index < 9) || type != 'multi'"
+        @click="showImages('img', index + videos.length)"
+      >
+        <img :src="itI.smallImgUrl ? itI.smallImgUrl : itI.imgUrl" />
+
+        <view
+          class="nut-comment-images__mask"
+          v-if="type == 'multi' && totalImages.length > 9 && videos.length + index > 7"
+        >
+          <span>共 {{ totalImages.length }} 张</span>
+          <nut-icon name="right" size="12"></nut-icon>
+        </view>
+      </view>
+    </template>
+  </view>
+</template>
+<script lang="ts">
+import { ref, watch, onMounted } from 'vue';
+
+import { createComponent } from '../../../utils/create';
+const { componentName, create } = createComponent('comment-images');
+
+export default create({
+  props: {
+    type: {
+      type: String,
+      default: 'one' // one multi
+    },
+    videos: {
+      type: Array,
+      default: () => []
+    },
+    images: {
+      type: Array,
+      default: () => []
+    }
+  },
+  components: {},
+  emits: ['click', 'clickImages'],
+
+  setup(props, { emit }) {
+    const isShowImage = ref(false);
+    const initIndex = ref(1);
+    const totalImages = ref([]);
+
+    watch(
+      () => [props.videos, props.images],
+      (value) => {
+        if (value[0].length > 0) {
+          value[0].forEach((el: any) => {
+            el.type = 'video';
+          });
+        }
+        totalImages.value = value[0].concat(value[1]);
+      },
+      { deep: true }
+    );
+
+    onMounted(() => {
+      if (props.videos.length > 0) {
+        props.videos.forEach((el: any) => {
+          el.type = 'video';
+        });
+      }
+      totalImages.value = props.videos.concat(props.images);
+    });
+
+    const showImages = (type: string, index: string | number) => {
+      const { videos, images } = props;
+
+      const i = type == 'img' ? (index as number) - videos.length : index;
+      emit('clickImages', {
+        type,
+        index: i,
+        value: type == 'img' ? images[i] : videos[i]
+      });
+    };
+
+    return { isShowImage, initIndex, showImages, totalImages };
+  }
+});
+</script>

+ 109 - 0
src/packages/__VUE/comment/demo.vue

@@ -0,0 +1,109 @@
+<template>
+  <div class="demo">
+    <h2>评论图片单行展示</h2>
+    <nut-cell>
+      <nut-comment
+        :images="cmt.images"
+        :videos="cmt.videos"
+        :info="cmt.info"
+        :labels="labels"
+        @click="handleclick"
+        @clickImages="clickImages"
+        :operation="['replay']"
+      >
+        <template #comment-labels>
+          <img
+            class="nut-comment-header__labels--item"
+            src="https://img11.360buyimg.com/imagetools/jfs/t1/211858/17/4258/12101/618e6f78Ed0edcadc/e83a673555edf59f.jpg"
+          />
+        </template>
+      </nut-comment>
+    </nut-cell>
+
+    <h2>评论图片多行展示</h2>
+    <nut-cell>
+      <nut-comment
+        headerType="complex"
+        imagesRows="multi"
+        :images="cmt.images"
+        :videos="cmt.videos"
+        :info="cmt.info"
+        ellipsis="6"
+        @clickImages="clickImages"
+      >
+        <template #comment-labels>
+          <img
+            class="nut-comment-header__labels--item"
+            src="https://storage.360buyimg.com/imgtools/78925d9440-f9e874d0-e93d-11eb-8e5c-0da9e18a13b1.png"
+            style="height: 12px"
+          />
+        </template>
+
+        <template #comment-shop-reply>
+          <div class="nut-comment-shop">
+            <span>京东美妆国际:</span
+            >尊敬的客户您好,非常抱歉给您带来不愉快的购物体验,关于过敏,什么成分都不存在个别性和普遍性。
+          </div>
+        </template>
+      </nut-comment>
+    </nut-cell>
+
+    <h2>追评展示</h2>
+    <nut-cell>
+      <nut-comment
+        imagesRows="multi"
+        :images="cmt.images"
+        :videos="cmt.videos"
+        :info="cmt.info"
+        :follow="cmt.follow"
+        @clickImages="clickImages"
+      ></nut-comment>
+    </nut-cell>
+  </div>
+</template>
+<script lang="ts">
+import { onMounted, ref } from '@vue/runtime-core';
+import { createComponent } from '../../utils/create';
+const { createDemo } = createComponent('comment');
+export default createDemo({
+  props: {},
+  setup() {
+    let cmt = ref({});
+    const labels = () => {
+      return '<nut-icon name="dongdong" color="#fa2c19"></nut-icon>';
+    };
+
+    const handleclick = (info: any) => {
+      console.log('进行跳转', info);
+    };
+
+    onMounted(() => {
+      getData();
+    });
+
+    const getData = () => {
+      fetch('//storage.360buyimg.com/nutui/3x/comment_data.json')
+        .then((response) => response.json())
+        .then((res) => {
+          cmt.value = res.Comment;
+        }) //执行结果是 resolve就调用then方法
+        .catch((err) => console.log('Oh, error', err)); //执行结果是 reject就调用catch方法
+    };
+
+    const clickImages = (imgs: any) => {
+      console.log('进行图片展示', imgs);
+    };
+
+    return {
+      cmt,
+      labels,
+      handleclick,
+      clickImages
+    };
+  }
+});
+</script>
+<style lang="scss" scoped>
+.demo {
+}
+</style>

+ 247 - 0
src/packages/__VUE/comment/doc.md

@@ -0,0 +1,247 @@
+# comment 
+
+### 介绍
+
+用于进行评论列表的展示。
+
+### 安装
+
+```javascript
+
+import { createApp } from 'vue';
+// vue
+import { Comment,Icon,Rate } from '@nutui/nutui';
+// taro
+import { Comment,Icon,Rate } from '@nutui/nutui-taro';
+
+const app = createApp();
+app.use(Comment).use(Icon).use(Rate);
+
+```
+
+### 评论图片单行展示
+
+默认情况下,单个商品的评论的图片是按照单行滑动进行展示的。
+
+:::demo
+```html
+<template>
+<nut-comment
+    :images="cmt.images"
+    :videos="cmt.videos"
+    :info="cmt.info"
+    :labels="labels"
+    @click="handleclick"
+    @clickImages="clickImages"
+    :operation="['replay']"
+  >
+    <template #comment-labels>
+      <img
+        class="nut-comment-header__labels--item"
+        src="https://img11.360buyimg.com/imagetools/jfs/t1/211858/17/4258/12101/618e6f78Ed0edcadc/e83a673555edf59f.jpg"
+      />
+    </template>
+</nut-comment>
+</template>
+<script>
+import { reactive, ref,onMounted } from 'vue';
+export default {
+  setup() {
+    let cmt = ref({});
+    const labels = () => {
+      return '<nut-icon name="dongdong" color="#fa2c19"></nut-icon>';
+    };
+    onMounted(()=>{
+      fetch('//storage.360buyimg.com/nutui/3x/comment_data.json')
+        .then((response) => response.json())
+        .then((res) => {
+          cmt.value = res.Comment;
+        }) 
+        .catch((err) => console.log('Oh, error', err)); 
+    })
+    const handleclick = (info: any) => {
+      console.log('进行跳转', info);
+    };
+    const clickImages = (imgs)=>{
+      console.log('进行图片展示',imgs)
+    }
+    return {
+      cmt,
+      labels,
+      handleclick,
+      clickImages
+    };
+  }
+}
+
+```
+:::
+
+### 评论图片多行展示
+
+通过 headerType 的值可以设置图片多行展示。
+
+:::demo 
+```html
+<template>
+<nut-comment
+  headerType="complex"
+  imagesRows="multi"
+  :images="cmt.images"
+  :videos="cmt.videos"
+  :info="cmt.info"
+  ellipsis="6"
+  @clickImages="clickImages"
+>
+  <template #comment-labels>
+    <img
+      class="nut-comment-header__labels--item"
+      src="https://storage.360buyimg.com/imgtools/78925d9440-f9e874d0-e93d-11eb-8e5c-0da9e18a13b1.png"
+      style="height: 12px"
+    />
+  </template>
+
+  <template #comment-shop-reply>
+    <div class="nut-comment-shop">
+      <span>京东美妆国际:</span
+      >尊敬的客户您好,非常抱歉给您带来不愉快的购物体验,关于过敏,什么成分都不存在个别性和普遍性。
+    </div>
+  </template>
+</nut-comment>
+
+</template>
+<script>
+import { reactive, ref,onMounted } from 'vue';
+export default {
+  setup() {
+    let cmt = ref({});
+    onMounted(()=>{
+      fetch('//storage.360buyimg.com/nutui/3x/comment_data.json')
+        .then((response) => response.json())
+        .then((res) => {
+          cmt.value = res.Comment;
+        }) 
+        .catch((err) => console.log('Oh, error', err)); 
+    })
+    const clickImages = (imgs)=>{
+      console.log('进行图片展示',imgs)
+    }
+    return {
+      cmt,
+      clickImages
+    };
+  }
+}
+
+```
+:::
+
+### 追评展示
+
+:::demo 
+```html
+<template>
+<nut-comment
+  imagesRows="multi"
+  :images="cmt.images"
+  :videos="cmt.videos"
+  :info="cmt.info"
+  :follow="cmt.follow"
+  @clickImages="clickImages"
+></nut-comment>
+
+</template>
+<script>
+import { reactive, ref,onMounted } from 'vue';
+export default {
+  setup() {
+    let cmt = ref({});
+    onMounted(()=>{
+      fetch('//storage.360buyimg.com/nutui/3x/comment_data.json')
+        .then((response) => response.json())
+        .then((res) => {
+          cmt.value = res.Comment;
+        }) 
+        .catch((err) => console.log('Oh, error', err)); 
+    })
+    const clickImages = (imgs)=>{
+      console.log('进行图片展示',imgs)
+    }
+    return {
+      cmt,
+      clickImages
+    };
+  }
+}
+
+```
+:::
+## API
+
+### Props
+
+| 参数         | 说明                             | 类型   | 默认值           |
+|--------------|----------------------------------|--------|------------------|
+| headerType         | 头部样式展示类型,可选: default,complex      | String | default               |
+| imagesRows         | 评论图片展示行数,可选: one,multi     | String | one               |
+| ellipsis        | 设置评论内容省略行数       | [String,Number] | 2               |
+| videos         | 视频信息 | Array | []              |
+| images | 图片信息    | Array | [] |
+| info     | 评论详情                      | Object | {}             |
+| follow     | 追评内容                      | Object | {}             |
+| operation | 配置底部按钮   | Array | ["replay", "like", "more"] |
+
+### Events
+
+| 事件名 | 说明           | 回调参数     |
+|--------|----------------|--------------|
+| click-operate  | 点击底部操作按钮回调函数 | type |
+| click  | 点击评论内容回调函数 | type |
+| click-images | 点击图片或视频触发 | {type,index,value}|
+
+### Slots
+
+Comment 组件在某些区域定义了插槽,可以按照需求进行替换。
+
+| 事件名 | 说明           | 
+|--------|----------------|
+| comment-labels  | 评论用户的标签 | 
+| comment-shop-reply  | 评论最底部,用于展示商家回复| 
+
+### images 数组
+
+images 数组中存放的是图片对象。
+
+```javascript
+const images = [{
+    "smallImgUrl": '', // 小图,列表展示时使用
+    "bigImgUrl": '', // 大图,大图展示使用
+    "imgUrl": "", // 兜底图
+}]
+```
+
+### videos 数组
+
+```javascript
+const videos = [{
+    "mainUrl": "", // 视频遮罩图片
+    "videoUrl": "", // 视频链接
+}]
+```
+
+### info 对象
+
+用于存放评论相关的信息。
+
+```javascript
+const info = [{
+  "content": "",  // 评论详情
+  "nickName": "", // 评论人的姓名
+  "score": 5, // 评论星星数
+  "avatar": "",  // 评论人头像
+  "time": "", // 评论时间
+  "size": "", // 评论人购买的商品规格
+  "replay":23, // 此评论的回复数
+  "like":1, // 此评论的点赞数
+}]
+```

+ 353 - 0
src/packages/__VUE/comment/index.scss

@@ -0,0 +1,353 @@
+.nut-comment {
+  width: 100%;
+  font-size: 12px;
+
+  &-header {
+    margin-bottom: 10px;
+    display: flex;
+    justify-content: space-between;
+
+    &__user {
+      flex: 1;
+      display: flex;
+      align-items: center;
+
+      &-avter {
+        width: 20px;
+        height: 20px;
+        border-radius: 50%;
+        margin-right: 10px;
+        overflow: hidden;
+
+        img {
+          width: 20px;
+          height: 20px;
+        }
+      }
+
+      &-name {
+        @include oneline-ellipsis();
+        margin-right: 5px;
+        font-size: 12px;
+        color: $cmt-header-user-name-color;
+        width: auto;
+        max-width: 80px;
+      }
+
+      &-default {
+        flex: 1;
+        &-name {
+          display: flex;
+          align-items: center;
+          margin-bottom: 3px;
+
+          @include oneline-ellipsis();
+          font-size: 12px;
+          color: $cmt-header-user-name-color;
+
+          > span {
+            margin-right: 8px;
+          }
+        }
+      }
+
+      &-complex {
+        display: flex;
+        align-items: center;
+        font-weight: bold;
+
+        &-name {
+          margin-right: 10px;
+          @include text-ellipsis();
+          max-width: 80px;
+        }
+      }
+
+      &-score {
+        .nut-rate-item {
+          display: block !important;
+          line-height: 10px;
+
+          .nut-icon {
+            line-height: 10px;
+          }
+        }
+      }
+    }
+
+    &__time {
+      width: 100px;
+      text-align: right;
+      font-size: 12px;
+      color: $cmt-header-time-color;
+    }
+
+    &__complex-score {
+      display: flex;
+      align-items: center;
+      margin-bottom: 10px;
+
+      .nut-rate-item {
+        display: block !important;
+        line-height: 12px;
+
+        .nut-icon {
+          line-height: 12px;
+        }
+      }
+
+      &-i {
+        margin: 0 8px 0 6px;
+        display: inline-block;
+        width: 1px;
+        height: 6px;
+        background: $text-color;
+        opacity: 0.4;
+        font-style: inherit;
+      }
+      &-size {
+        @include oneline-ellipsis();
+      }
+    }
+
+    &__labels--item {
+      display: inline-block;
+      height: 16px;
+      margin-right: 4px;
+      &:last-child {
+        margin-right: 0;
+      }
+    }
+  }
+
+  &__main {
+    word-break: break-all;
+    display: -webkit-box;
+    -webkit-box-orient: vertical;
+    overflow: hidden;
+    word-break: break-all;
+    white-space: pre-wrap;
+  }
+
+  &-images {
+    display: flex;
+    margin: 10px 0 12px;
+    overflow-x: auto;
+    overflow-y: hidden;
+
+    &__item {
+      position: relative;
+      width: 80px;
+      height: 80px;
+      margin-right: 5px;
+      border-radius: 6px;
+      overflow: hidden;
+      flex-shrink: 0;
+
+      img {
+        width: 80px;
+        height: 80px;
+      }
+
+      &--imgbox {
+        //   background: #f00;
+      }
+
+      &--video {
+        img {
+          position: absolute;
+          top: 50%;
+          left: 50%;
+          transform: translate(-50%, -50%);
+          // height: auto;
+        }
+      }
+    }
+
+    &__mask {
+      position: absolute;
+      top: 0;
+      left: 0;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      width: 105px;
+      height: 105px;
+      line-height: 105px;
+      background: rgba(0, 0, 0, 0.5);
+      font-size: 12px;
+      color: rgba(255, 255, 255, 1);
+    }
+  }
+
+  &-images--multi {
+    flex-wrap: wrap;
+    justify-content: space-between;
+    overflow: hidden;
+    width: 331px;
+    margin: 10px auto 15px;
+
+    .nut-comment-images__item {
+      margin: 8px 8px 0 0;
+
+      width: 105px;
+      height: 105px;
+
+      img {
+        width: 100%;
+        height: 100%;
+      }
+
+      .svg-demo {
+        width: 40px;
+        height: 40px;
+      }
+
+      &:nth-child(3n) {
+        margin-right: 0;
+      }
+    }
+
+    &:after {
+      content: '';
+      display: block;
+      width: 105px;
+    }
+  }
+
+  &__follow {
+    &-title {
+      position: relative;
+      // display: flex;
+      // align-items: center;
+      font-size: 14px;
+      font-weight: bold;
+      color: $black;
+      padding-left: 8px;
+
+      i {
+        position: absolute;
+        left: 0;
+        top: 13%;
+        color: $primary-color;
+        transform: rotate(90deg);
+        opacity: 0.4;
+      }
+    }
+    &-com {
+      margin: 8px 0px 8px 8px;
+      @include moreline-ellipsis(6);
+    }
+    &-img {
+      margin: 0 0 8px 8px;
+      display: flex;
+      align-items: center;
+    }
+  }
+
+  &-bottom {
+    display: flex;
+    justify-content: space-between;
+    color: $cmt-bottom-label-color;
+    margin-right: 5px;
+
+    &__lable {
+      flex: 1;
+      margin-right: 10px;
+      @include oneline-ellipsis();
+    }
+    &__cpx {
+      display: flex;
+      align-items: center;
+      justify-content: flex-end;
+      color: $black;
+
+      &-item {
+        position: relative;
+        margin-right: 18px;
+        display: flex;
+        align-items: center;
+
+        span {
+          color: $black;
+          margin-right: 5px;
+        }
+
+        &:last-child {
+          margin-right: 0;
+        }
+
+        &-popover {
+          position: absolute;
+          top: 35px;
+          right: 18px;
+          width: max-content;
+          background: $white;
+          padding: 10px;
+          box-shadow: 0 0 6px $disable-color;
+          border-radius: 5px 0 5px 5px;
+
+          &::after {
+            content: '';
+            position: absolute;
+            top: -20px;
+            right: 0px;
+            width: 0;
+            height: 0;
+            border-left: 14px solid transparent;
+            border-right: 0px solid transparent;
+            border-top: 10px solid transparent;
+            border-bottom: 10px solid $white;
+          }
+
+          &::before {
+            content: '';
+            position: absolute;
+            top: -22px;
+            right: -1px;
+            width: 0;
+            height: 0;
+            border-left: 14px solid transparent;
+            border-right: 0px solid transparent;
+            border-top: 10px solid transparent;
+            border-bottom: 10px solid rgba(114, 113, 113, 0.1);
+          }
+        }
+      }
+    }
+  }
+
+  &-images__play {
+    width: 40px;
+    height: 40px;
+    position: absolute;
+    left: 50%;
+    top: 50%;
+    transform: translate(-50%);
+    transform: translate(-50%, -50%);
+    background: rgba(0, 0, 0, 0.5);
+    border-radius: 50%;
+
+    &::after {
+      display: block;
+      content: '';
+      position: absolute;
+      left: 15px;
+      top: 11px;
+      border-top: 9px solid transparent;
+      border-bottom: 9px solid transparent;
+      border-left: 15px solid #fff;
+    }
+  }
+
+  .nut-comment-shop {
+    width: 100%;
+    margin-top: 20px;
+    padding-top: 10px;
+    border-top: 1px solid rgba(0, 0, 0, 0.1);
+    @include moreline-ellipsis(6);
+    span {
+      color: $cmt-shop-color;
+    }
+  }
+}

+ 132 - 0
src/packages/__VUE/comment/index.taro.vue

@@ -0,0 +1,132 @@
+<template>
+  <view :class="classes" v-if="info && Object.keys(info)">
+    <!-- 根据展示信息的多少,分为3种展示风格:simple,base,complex -->
+    <comment-header :type="headerType" :info="info" :labels="labels" @handleClick="handleClick">
+      <template #labels>
+        <slot name="comment-labels"></slot>
+      </template>
+    </comment-header>
+
+    <slot name="feature"></slot>
+
+    <view
+      class="nut-comment__main"
+      :style="`-webkit-line-clamp:${conEllipsis}`"
+      @click="handleClick"
+      v-html="info.content.replace(/\n/g, '<br>')"
+    ></view>
+
+    <comment-images :images="images" :videos="videos" :type="imagesRows" @clickImages="clickImages"></comment-images>
+
+    <view class="nut-comment__follow" v-if="follow && follow.days > 0" @click="handleClick">
+      <view class="nut-comment__follow-title"
+        ><nut-icon size="14" name="joy-smile" />购买{{ follow.days }}天后追评</view
+      >
+      <view class="nut-comment__follow-com">{{ follow.content }}</view>
+      <view class="nut-comment__follow-img" v-if="follow.images && follow.images.length > 0"
+        >{{ follow.images.length }} 张追评图片 <nut-icon size="12" name="right"
+      /></view>
+    </view>
+
+    <comment-bottom
+      :type="headerType"
+      :info="info"
+      :operation="operation"
+      @clickOperate="clickOperate"
+      @handleClick="handleClick"
+    ></comment-bottom>
+
+    <slot name="cmt-shop-reply"></slot>
+  </view>
+</template>
+<script lang="ts">
+import { ref, onMounted, computed, watch } from 'vue';
+import { createComponent } from '../../utils/create';
+const { componentName, create } = createComponent('comment');
+
+import CommentHeader from './components/CmtHeader.vue';
+import CommentImages from './components/CmtImages.vue';
+import CommentBottom from './components/CmtBottom.vue';
+
+export default create({
+  props: {
+    headerType: {
+      type: String,
+      default: 'default' //头部展示风格 default,complex
+    },
+
+    imagesRows: {
+      type: String,
+      default: 'one' // 'one'  'multi'
+    },
+
+    ellipsis: {
+      type: [String, Number, Boolean],
+      default: false
+    },
+
+    videos: {
+      type: Array,
+      default: () => []
+    },
+    images: {
+      type: Array,
+      default: () => []
+    },
+
+    info: {
+      type: Object,
+      default: () => {}
+    },
+
+    follow: {
+      type: Object,
+      default: () => {}
+    },
+
+    labels: {
+      type: Function,
+      default: () => ''
+    },
+
+    operation: {
+      type: Array,
+      default: ['replay', 'like', 'more']
+    }
+  },
+  components: {
+    CommentHeader,
+    CommentImages,
+    CommentBottom
+  },
+  emits: ['click', 'clickImages', 'clickOperate'],
+
+  setup(props, { emit }) {
+    const classes = computed(() => {
+      const prefixCls = componentName;
+      return {
+        [prefixCls]: true
+      };
+    });
+
+    const conEllipsis = computed(() => {
+      if (props.ellipsis) return props.ellipsis;
+
+      return props.headerType == 'complex' ? 6 : 2;
+    });
+    const clickOperate = (t: string) => {
+      emit('clickOperate', t);
+    };
+
+    const handleClick = () => {
+      emit('click', props.info);
+    };
+
+    const clickImages = (value: any) => {
+      emit('clickImages', value);
+    };
+
+    return { classes, conEllipsis, clickOperate, handleClick, clickImages };
+  }
+});
+</script>

+ 132 - 0
src/packages/__VUE/comment/index.vue

@@ -0,0 +1,132 @@
+<template>
+  <view :class="classes" v-if="info && Object.keys(info)">
+    <!-- 根据展示信息的多少,分为3种展示风格:simple,base,complex -->
+    <comment-header :type="headerType" :info="info" :labels="labels" @handleClick="handleClick">
+      <template #labels>
+        <slot name="comment-labels"></slot>
+      </template>
+    </comment-header>
+
+    <slot name="feature"></slot>
+
+    <view
+      class="nut-comment__main"
+      :style="`-webkit-line-clamp:${conEllipsis}`"
+      @click="handleClick"
+      v-html="info.content.replace(/\n/g, '<br>')"
+    ></view>
+
+    <comment-images :images="images" :videos="videos" :type="imagesRows" @clickImages="clickImages"></comment-images>
+
+    <view class="nut-comment__follow" v-if="follow && follow.days > 0" @click="handleClick">
+      <view class="nut-comment__follow-title"
+        ><nut-icon size="14" name="joy-smile" />{{ translate('additionalReview', follow.days) }}</view
+      >
+      <view class="nut-comment__follow-com">{{ follow.content }}</view>
+      <view class="nut-comment__follow-img" v-if="follow.images && follow.images.length > 0"
+        >{{ translate('additionalImages', follow.images.length) }} <nut-icon size="12" name="right"
+      /></view>
+    </view>
+
+    <comment-bottom
+      :type="headerType"
+      :info="info"
+      :operation="operation"
+      @clickOperate="clickOperate"
+      @handleClick="handleClick"
+    ></comment-bottom>
+
+    <slot name="comment-shop-reply"></slot>
+  </view>
+</template>
+<script lang="ts">
+import { ref, onMounted, computed, watch } from 'vue';
+import { createComponent } from '../../utils/create';
+const { componentName, create, translate } = createComponent('comment');
+
+import CommentHeader from './components/CmtHeader.vue';
+import CommentImages from './components/CmtImages.vue';
+import CommentBottom from './components/CmtBottom.vue';
+
+export default create({
+  props: {
+    headerType: {
+      type: String,
+      default: 'default' //头部展示风格 default,complex
+    },
+
+    imagesRows: {
+      type: String,
+      default: 'one' // 'one'  'multi'
+    },
+
+    ellipsis: {
+      type: [String, Number, Boolean],
+      default: false
+    },
+
+    videos: {
+      type: Array,
+      default: () => []
+    },
+    images: {
+      type: Array,
+      default: () => []
+    },
+
+    info: {
+      type: Object,
+      default: () => {}
+    },
+
+    follow: {
+      type: Object,
+      default: () => {}
+    },
+
+    labels: {
+      type: Function,
+      default: () => ''
+    },
+
+    operation: {
+      type: Array,
+      default: ['replay', 'like', 'more']
+    }
+  },
+  components: {
+    CommentHeader,
+    CommentImages,
+    CommentBottom
+  },
+  emits: ['click', 'clickImages', 'clickOperate'],
+
+  setup(props, { emit }) {
+    const classes = computed(() => {
+      const prefixCls = componentName;
+      return {
+        [prefixCls]: true
+      };
+    });
+
+    const conEllipsis = computed(() => {
+      if (props.ellipsis) return props.ellipsis;
+
+      return props.headerType == 'complex' ? 6 : 2;
+    });
+    const clickOperate = (t: string) => {
+      emit('clickOperate', t);
+    };
+
+    const handleClick = () => {
+      emit('click', props.info);
+    };
+
+    const clickImages = (value: any) => {
+      emit('clickImages', value);
+    };
+
+    return { classes, conEllipsis, clickOperate, handleClick, clickImages, translate };
+  }
+});
+</script>

+ 5 - 0
src/packages/locale/lang/baseLang.ts

@@ -73,4 +73,9 @@ export interface BaseLang {
   addresslist: {
     addAddress: string;
   };
+  comment: {
+    complaintsText: string;
+    additionalReview: Function;
+    additionalImages: Function;
+  };
 }

+ 5 - 0
src/packages/locale/lang/en-US.ts

@@ -73,6 +73,11 @@ const lang: BaseLang = {
   },
   addresslist: {
     addAddress: 'Add New Address'
+  },
+  comment: {
+    complaintsText: 'I have a complaint',
+    additionalReview: (day: number) => `Review after ${day} days of purchase`,
+    additionalImages: (length: number) => `There are ${length} follow-up comments`
   }
 };
 export default lang;

+ 5 - 0
src/packages/locale/lang/zh-CN.ts

@@ -73,6 +73,11 @@ const lang: BaseLang = {
   },
   addresslist: {
     addAddress: '新建地址'
+  },
+  comment: {
+    complaintsText: '我要投诉',
+    additionalReview: (day: number) => `购买${day}天后追评`,
+    additionalImages: (length: number) => `${length}张追评图片`
   }
 };
 export default lang;

+ 7 - 0
src/packages/styles/variables.scss

@@ -765,5 +765,12 @@ $circle-progress-path-color: #e5e9f2 !default;
 $circle-progress-text-color: #000000 !default;
 $circle-progress-text-size: $font-size-3 !default;
 
+// Comment
+$cmt-header-user-name-color: rgba(51, 51, 51, 1) !default;
+$cmt-header-time-color: rgba(153, 153, 153, 1) !default;
+$cmt-bottom-label-color: rgba(153, 153, 153, 1) !default;
+$cmt-shop-color: $primary-color !default;
+
+
 @import './mixins/index';
 @import './animation/index';

+ 2 - 1
src/sites/mobile-taro/vue/src/app.config.ts

@@ -87,7 +87,8 @@ export default {
         'pages/timeselect/index',
         'pages/sku/index',
         'pages/card/index',
-        'pages/ecard/index'
+        'pages/ecard/index',
+        'pages/comment/index'
       ]
     }
   ],

+ 1 - 0
src/sites/mobile-taro/vue/src/business/pages/comment/index.config.ts

@@ -0,0 +1 @@
+export default { navigationBarTitleText: 'Comment' };

+ 106 - 0
src/sites/mobile-taro/vue/src/business/pages/comment/index.vue

@@ -0,0 +1,106 @@
+<template>
+  <div class="demo">
+    <h2>评论图片单行展示</h2>
+    <nut-cell>
+      <nut-comment
+        :images="cmt.images"
+        :videos="cmt.videos"
+        :info="cmt.info"
+        :labels="labels"
+        @click="handleclick"
+        @clickImages="clickImages"
+        :operation="['replay']"
+      >
+        <template #comment-labels>
+          <img
+            class="nut-comment-header__labels--item"
+            src="https://img11.360buyimg.com/imagetools/jfs/t1/211858/17/4258/12101/618e6f78Ed0edcadc/e83a673555edf59f.jpg"
+            style="width: 100rpx"
+          />
+        </template>
+      </nut-comment>
+    </nut-cell>
+
+    <h2>评论图片多行展示</h2>
+    <nut-cell>
+      <nut-comment
+        headerType="complex"
+        imagesRows="multi"
+        :images="cmt.images"
+        :videos="cmt.videos"
+        :info="cmt.info"
+        ellipsis="6"
+        @clickImages="clickImages"
+      >
+        <template #comment-labels>
+          <img
+            class="nut-comment-header__labels--item"
+            src="https://storage.360buyimg.com/imgtools/78925d9440-f9e874d0-e93d-11eb-8e5c-0da9e18a13b1.png"
+            style="height: 12px; width: 60rpx"
+          />
+        </template>
+
+        <template #comment-shop-reply>
+          <div class="nut-comment-shop">
+            <span>京东美妆国际:</span
+            >尊敬的客户您好,非常抱歉给您带来不愉快的购物体验,关于过敏,什么成分都不存在个别性和普遍性。
+          </div>
+        </template>
+      </nut-comment>
+    </nut-cell>
+
+    <h2>追评</h2>
+    <nut-cell>
+      <nut-comment
+        imagesRows="multi"
+        :images="cmt.images"
+        :videos="cmt.videos"
+        :info="cmt.info"
+        :follow="cmt.follow"
+        @clickImages="clickImages"
+      ></nut-comment>
+    </nut-cell>
+  </div>
+</template>
+<script lang="ts">
+import { defineComponent, onMounted, ref } from 'vue';
+import Taro from '@tarojs/taro';
+export default defineComponent({
+  props: {},
+  setup() {
+    let cmt = ref({});
+    const labels = () => {
+      return '<nut-icon name="dongdong" color="#fa2c19"></nut-icon>';
+    };
+
+    const handleclick = (info: any) => {
+      console.log('进行跳转', info);
+    };
+
+    onMounted(() => {
+      getData();
+    });
+
+    const getData = () => {
+      Taro.request({
+        method: 'GET',
+        url: 'https://storage.360buyimg.com/nutui/3x/comment_data.json',
+        success: (res) => {
+          cmt.value = res.data.Comment;
+        }
+      });
+    };
+
+    const clickImages = (imgs: any) => {
+      console.log('进行图片展示', imgs);
+    };
+
+    return {
+      cmt,
+      labels,
+      handleclick,
+      clickImages
+    };
+  }
+});
+</script>