Browse Source

feat: 新增 Ellipsis、Image 组件 (#1394)

* feat: datepicker 国际化

* feat: popover 功能补全

* feat: 添加更新版本号

* docs: datepicker 版本号添加

* feat: addresslist/category 组件主题定制修改

* fix: picker问题修改

* feat: picker国际化

* docs: countdown 国际化添加

* docs: countdown 国际化添加

* feat: popup 在 Taro 中遮罩层底部滑动

* feat: picker 新增demo

* feat: audio 进行国际化修改

* feat: address 添加国际化

* feat: sku 国际化

* feat: comment 国际化

* feat: 组件Image实现H5

* feat: 新增组件 image

* feat: 新增组件ellipse

* feat: taro小程序实现ellipse

* feat: 新增组件Ellipsis
yangxiaolu1993 3 years ago
parent
commit
aa16bfc4ed
31 changed files with 1681 additions and 27 deletions
  1. 24 1
      src/config.json
  2. 1 0
      src/packages/__VUE/ellipsis/__tests__/ellipsis.spec.ts
  3. 70 0
      src/packages/__VUE/ellipsis/demo.vue
  4. 102 0
      src/packages/__VUE/ellipsis/doc.en-US.md
  5. 102 0
      src/packages/__VUE/ellipsis/doc.md
  6. 12 0
      src/packages/__VUE/ellipsis/index.scss
  7. 275 0
      src/packages/__VUE/ellipsis/index.taro.vue
  8. 230 0
      src/packages/__VUE/ellipsis/index.vue
  9. 1 0
      src/packages/__VUE/image/__tests__/image.spec.ts
  10. 128 0
      src/packages/__VUE/image/demo.vue
  11. 173 0
      src/packages/__VUE/image/doc.en-US.md
  12. 172 0
      src/packages/__VUE/image/doc.md
  13. 7 0
      src/packages/__VUE/image/doc.taro.md
  14. 39 0
      src/packages/__VUE/image/index.scss
  15. 38 0
      src/packages/__VUE/image/index.taro.vue
  16. 134 0
      src/packages/__VUE/image/index.vue
  17. 3 1
      src/packages/styles/font/config.json
  18. 49 3
      src/packages/styles/font/demo_index.html
  19. 11 3
      src/packages/styles/font/iconfont.css
  20. 15 15
      src/packages/styles/font/iconfont.js
  21. 14 0
      src/packages/styles/font/iconfont.json
  22. BIN
      src/packages/styles/font/iconfont.ttf
  23. BIN
      src/packages/styles/font/iconfont.woff
  24. BIN
      src/packages/styles/font/iconfont.woff2
  25. 3 0
      src/packages/styles/variables.scss
  26. 3 2
      src/sites/mobile-taro/vue/project.private.config.json
  27. 10 2
      src/sites/mobile-taro/vue/src/app.config.ts
  28. 1 0
      src/sites/mobile-taro/vue/src/base/pages/image/index.config.ts
  29. 17 0
      src/sites/mobile-taro/vue/src/base/pages/image/index.vue
  30. 1 0
      src/sites/mobile-taro/vue/src/exhibition/pages/ellipsis/index.config.ts
  31. 46 0
      src/sites/mobile-taro/vue/src/exhibition/pages/ellipsis/index.vue

+ 24 - 1
src/config.json

@@ -127,6 +127,17 @@
           "show": true,
           "desc": "弹出层容器,用于展示弹窗、信息提示等内容,支持多个弹出层叠加展示",
           "author": "szg2008"
+        },
+        {
+          "version": "3.0.0",
+          "name": "Image",
+          "cType": "基础组件",
+          "cName": "图片",
+          "desc": "图片展示",
+          "show": true,
+          "tarodoc": false,
+          "type": "component",
+          "author": "yangxiaolu"
         }
       ]
     },
@@ -1032,6 +1043,17 @@
           "sort": 9,
           "show": true,
           "author": "szg2008"
+        },
+        {
+          "version": "3.0.0",
+          "name": "Ellipsis",
+          "cType": "展示组件",
+          "cName": "文本省略",
+          "desc": "文本省略",
+          "show": true,
+          "tarodoc": false,
+          "type": "component",
+          "author": "yangxiaolu3"
         }
       ]
     },
@@ -1171,7 +1193,8 @@
           "exportEmpty": true,
           "author": "liqiong43",
           "taro": true
-        },{
+        },
+        {
           "name": "Comment",
           "cType": "特色组件",
           "cName": "商品评论",

+ 1 - 0
src/packages/__VUE/ellipsis/__tests__/ellipsis.spec.ts

@@ -0,0 +1 @@
+import { mount } from '@vue/test-utils';

+ 70 - 0
src/packages/__VUE/ellipsis/demo.vue

@@ -0,0 +1,70 @@
+<template>
+  <div class="demo">
+    <h2>{{ translate('header') }}</h2>
+    <nut-cell>
+      <nut-ellipsis :content="content" direction="start" class="elli"></nut-ellipsis>
+    </nut-cell>
+
+    <h2>{{ translate('end') }}</h2>
+    <nut-cell>
+      <nut-ellipsis :content="content" direction="end"></nut-ellipsis>
+    </nut-cell>
+
+    <h2>{{ translate('middle') }}</h2>
+    <nut-cell>
+      <nut-ellipsis :content="content" direction="middle"></nut-ellipsis>
+    </nut-cell>
+
+    <h2>{{ translate('rows') }}</h2>
+    <nut-cell>
+      <nut-ellipsis :content="content" direction="start" rows="3"></nut-ellipsis>
+    </nut-cell>
+
+    <h2>{{ translate('expand') }}</h2>
+    <nut-cell>
+      <nut-ellipsis :content="content" direction="start" expandText="展开" collapseText="收起"></nut-ellipsis>
+    </nut-cell>
+    <nut-cell>
+      <nut-ellipsis :content="content" direction="middle" expandText="展开" collapseText="收起"></nut-ellipsis>
+    </nut-cell>
+    <nut-cell>
+      <nut-ellipsis :content="content" direction="end" rows="3" expandText="展开" collapseText="收起"></nut-ellipsis>
+    </nut-cell>
+  </div>
+</template>
+<script lang="ts">
+import { ref } from 'vue';
+import { createComponent } from '@/packages/utils/create';
+const { createDemo, translate } = createComponent('ellipsis');
+import { useTranslate } from '@/sites/assets/util/useTranslate';
+useTranslate({
+  'zh-CN': {
+    header: '头部省略',
+    end: '尾部省略',
+    middle: '中间省略',
+    rows: '多行省略',
+    expand: '展开收起'
+  },
+  'en-US': {
+    basic: 'Basic Usage',
+    header: 'Leading',
+    end: 'Tailing',
+    middle: 'Middle',
+    rows: 'Multi-line',
+    expand: 'Expand & Collapse'
+  }
+});
+export default createDemo({
+  props: {},
+  setup() {
+    const content = ref(
+      'NutUI3.0上线后我们研发团队也在不断的优化、测试、使用、迭代 Vue3 的相关组件,但是在跨端小程序的开发过程中,发现没有合适的组件库可以支持多端开发。为了填补这一空白,同时为了优化开发者体验,让 NutUI 能够为更多的开发者带来便利,我们决定在 NutUI 中增加小程序多端适配的能力。'
+    );
+    return { content, translate };
+  }
+});
+</script>
+<style lang="scss" scoped>
+.demo {
+}
+</style>

+ 102 - 0
src/packages/__VUE/ellipsis/doc.en-US.md

@@ -0,0 +1,102 @@
+# ellipsis 
+
+### Intro
+
+展示空间不足时,隐去部分内容并用“...”替代。
+
+### Install
+
+```javascript
+
+import { createApp } from 'vue';
+// vue
+import { Ellipsis } from '@nutui/nutui';
+// taro
+import { Ellipsis } from '@nutui/nutui-taro';
+
+const app = createApp();
+app.use();
+
+```
+
+### Leading
+
+:::demo
+
+```html
+<template>
+  <nut-ellipsis content="NutUI3.0上线后我们研发团队也在不断的优化、测试、使用、迭代 Vue3 的相关组件,但是在跨端小程序的开发过程中,发现没有合适的组件库可以支持多端开发。为了填补这一空白,同时为了优化开发者体验,让 NutUI 能够为更多的开发者带来便利,我们决定在 NutUI 中增加小程序多端适配的能力。" direction="start" ></nut-ellipsis>
+</template>
+```
+:::
+
+### Tailing
+
+:::demo
+
+```html
+<template>
+  <nut-ellipsis content="NutUI3.0上线后我们研发团队也在不断的优化、测试、使用、迭代 Vue3 的相关组件,但是在跨端小程序的开发过程中,发现没有合适的组件库可以支持多端开发。为了填补这一空白,同时为了优化开发者体验,让 NutUI 能够为更多的开发者带来便利,我们决定在 NutUI 中增加小程序多端适配的能力。" direction="end" ></nut-ellipsis>
+</template>
+```
+:::
+
+### Middle
+
+:::demo
+
+```html
+<template>
+  <nut-ellipsis content="NutUI3.0上线后我们研发团队也在不断的优化、测试、使用、迭代 Vue3 的相关组件,但是在跨端小程序的开发过程中,发现没有合适的组件库可以支持多端开发。为了填补这一空白,同时为了优化开发者体验,让 NutUI 能够为更多的开发者带来便利,我们决定在 NutUI 中增加小程序多端适配的能力。" direction="middle" ></nut-ellipsis>
+</template>
+```
+:::
+
+### Multi-line
+
+:::demo
+
+```html
+<template>
+  <nut-ellipsis 
+    content="NutUI3.0上线后我们研发团队也在不断的优化、测试、使用、迭代 Vue3 的相关组件,但是在跨端小程序的开发过程中,发现没有合适的组件库可以支持多端开发。为了填补这一空白,同时为了优化开发者体验,让 NutUI 能够为更多的开发者带来便利,我们决定在 NutUI 中增加小程序多端适配的能力。" 
+    direction="start" 
+    rows="3"></nut-ellipsis>
+</template>
+```
+:::
+
+### Expand & Collapse
+
+:::demo
+
+```html
+<template>
+  <nut-ellipsis 
+    direction="start" 
+    expandText="Expand" 
+    collapseText="Collapse"
+    content="NutUI3.0上线后我们研发团队也在不断的优化、测试、使用、迭代 Vue3 的相关组件,但是在跨端小程序的开发过程中,发现没有合适的组件库可以支持多端开发。为了填补这一空白,同时为了优化开发者体验,让 NutUI 能够为更多的开发者带来便利,我们决定在 NutUI 中增加小程序多端适配的能力。" ></nut-ellipsis>
+</template>
+```
+:::
+
+## API
+
+### Props
+
+| Attribute         | Description                             | Type   | Default           |
+|--------------|----------------------------------|--------|------------------|
+| content         | Content               | String | -                |
+| direction         | Direction                | 'start' | 'end' | 'middle' | 'end'               |
+| rows         | Rows               | Number | 1              |
+| expandText         | Expand text               | String | ''              |
+| collapseText         | Collapse text               | String | ''               |
+| symbol         | Symbol     | String | '...'       |
+
+### Events
+
+| Event  | Description     | Arguments    |
+|--------|----------------|--------------|
+| click  | Emitted when the content is clicked | -- |
+| change  | Emitted when expand or collapse is clicked | -- |

+ 102 - 0
src/packages/__VUE/ellipsis/doc.md

@@ -0,0 +1,102 @@
+# ellipsis 
+
+### 介绍
+
+展示空间不足时,隐去部分内容并用“...”替代。
+
+### 安装
+
+```javascript
+
+import { createApp } from 'vue';
+// vue
+import { Ellipsis } from '@nutui/nutui';
+// taro
+import { Ellipsis } from '@nutui/nutui-taro';
+
+const app = createApp();
+app.use();
+
+```
+
+### 头部省略
+
+:::demo
+
+```html
+<template>
+  <nut-ellipsis content="NutUI3.0上线后我们研发团队也在不断的优化、测试、使用、迭代 Vue3 的相关组件,但是在跨端小程序的开发过程中,发现没有合适的组件库可以支持多端开发。为了填补这一空白,同时为了优化开发者体验,让 NutUI 能够为更多的开发者带来便利,我们决定在 NutUI 中增加小程序多端适配的能力。" direction="start" ></nut-ellipsis>
+</template>
+```
+:::
+
+### 尾部省略
+
+:::demo
+
+```html
+<template>
+  <nut-ellipsis content="NutUI3.0上线后我们研发团队也在不断的优化、测试、使用、迭代 Vue3 的相关组件,但是在跨端小程序的开发过程中,发现没有合适的组件库可以支持多端开发。为了填补这一空白,同时为了优化开发者体验,让 NutUI 能够为更多的开发者带来便利,我们决定在 NutUI 中增加小程序多端适配的能力。" direction="end" ></nut-ellipsis>
+</template>
+```
+:::
+
+### 中间省略
+
+:::demo
+
+```html
+<template>
+  <nut-ellipsis content="NutUI3.0上线后我们研发团队也在不断的优化、测试、使用、迭代 Vue3 的相关组件,但是在跨端小程序的开发过程中,发现没有合适的组件库可以支持多端开发。为了填补这一空白,同时为了优化开发者体验,让 NutUI 能够为更多的开发者带来便利,我们决定在 NutUI 中增加小程序多端适配的能力。" direction="middle" ></nut-ellipsis>
+</template>
+```
+:::
+
+### 多行省略
+
+:::demo
+
+```html
+<template>
+  <nut-ellipsis 
+    content="NutUI3.0上线后我们研发团队也在不断的优化、测试、使用、迭代 Vue3 的相关组件,但是在跨端小程序的开发过程中,发现没有合适的组件库可以支持多端开发。为了填补这一空白,同时为了优化开发者体验,让 NutUI 能够为更多的开发者带来便利,我们决定在 NutUI 中增加小程序多端适配的能力。" 
+    direction="start" 
+    rows="3"></nut-ellipsis>
+</template>
+```
+:::
+
+### 展开收起
+
+:::demo
+
+```html
+<template>
+  <nut-ellipsis 
+    direction="start" 
+    expandText="展开" 
+    collapseText="收起"
+    content="NutUI3.0上线后我们研发团队也在不断的优化、测试、使用、迭代 Vue3 的相关组件,但是在跨端小程序的开发过程中,发现没有合适的组件库可以支持多端开发。为了填补这一空白,同时为了优化开发者体验,让 NutUI 能够为更多的开发者带来便利,我们决定在 NutUI 中增加小程序多端适配的能力。" ></nut-ellipsis>
+</template>
+```
+:::
+
+## API
+
+### Props
+
+| 参数         | 说明                             | 类型   | 默认值           |
+|--------------|----------------------------------|--------|------------------|
+| content         | 文本内容               | String | -                |
+| direction         | 省略位置               | 'start' | 'end' | 'middle' | 'end'               |
+| rows         | 展示几行               | Number | 1              |
+| expandText         | 展开操作的文案               | String | ''              |
+| collapseText         | 收起操作的文案               | String | ''               |
+| symbol         | 省略的符号     | String | '...'       |
+
+### Events
+
+| 事件名 | 说明           | 回调参数     |
+|--------|----------------|--------------|
+| click  | 文本点击是触发 | -- |
+| change  | 点击展开收起时触发 | -- |

+ 12 - 0
src/packages/__VUE/ellipsis/index.scss

@@ -0,0 +1,12 @@
+.nut-ellipsis {
+  .nut-ellipsis-text {
+    cursor: hand;
+    color: $ellipsis-expand-collapse-color;
+    display: inline;
+  }
+}
+
+.nut-ellipsis-copy {
+  position: absolute;
+  top: -999999px;
+}

+ 275 - 0
src/packages/__VUE/ellipsis/index.taro.vue

@@ -0,0 +1,275 @@
+<template>
+  <view :class="classes" @click="handleClick" ref="root" :id="'root' + refRandomId">
+    <view v-if="!exceeded">{{ content }}</view>
+
+    <view v-if="exceeded && !expanded">
+      {{ ellipsis.leading
+      }}<view class="nut-ellipsis-text" v-if="expandText" @click.stop="clickHandle(1)">{{ expandText }}</view
+      >{{ ellipsis.tailing }}
+    </view>
+    <view v-if="exceeded && expanded">
+      {{ content }}
+      <span class="nut-ellipsis-text" v-if="expandText" @click.stop="clickHandle(2)">{{ collapseText }}</span>
+    </view>
+  </view>
+
+  <view
+    class="nut-ellipsis-copy"
+    @click="handleClick"
+    ref="rootContain"
+    :id="'rootContain' + refRandomId"
+    :style="{ width: widthRef }"
+  >
+    <view>{{ contantCopy }}</view>
+  </view>
+</template>
+
+<script lang="ts">
+import { ref, reactive, toRefs, computed, onMounted, PropType, watch, unref } from 'vue';
+import { createComponent } from '@/packages/utils/create';
+import { useTaroRect } from '@/packages/utils/useTaroRect';
+import Taro from '@tarojs/taro';
+const { componentName, create } = createComponent('ellipsis');
+export type Direction = 'start' | 'end' | 'middle';
+
+type EllipsisedValue = {
+  leading?: string;
+  tailing?: string;
+};
+
+export default create({
+  props: {
+    content: {
+      type: String,
+      default: ''
+    },
+    direction: {
+      type: String as PropType<Direction>,
+      default: 'start'
+    },
+    rows: {
+      type: [Number, String],
+      default: 1
+    },
+    expandText: {
+      type: String,
+      default: ''
+    },
+    collapseText: {
+      type: String,
+      default: ''
+    },
+    symbol: {
+      type: String,
+      default: '...'
+    }
+  },
+  emits: ['click', 'change'],
+
+  setup(props, { emit }) {
+    const root = ref(null);
+    const rootContain = ref(null);
+    let contantCopy = ref(props.content);
+    let container: any = null;
+    let maxHeight = 0; // 超出的最大高度
+    let lineHeight = 0; // 当行的最大高度
+    let originHeight = 0; // 原始高度
+    const ellipsis = ref<EllipsisedValue>();
+    const refRandomId = Math.random().toString(36).slice(-8);
+    let widthRef = ref('auto');
+    const state = reactive({
+      exceeded: false, //是否超出
+      expanded: false //是否折叠
+    });
+
+    const classes = computed(() => {
+      const prefixCls = componentName;
+      return {
+        [prefixCls]: true
+      };
+    });
+
+    watch(
+      () => props.content,
+      (newV, oldVal) => {
+        if (newV != oldVal) {
+          if (container) {
+            // document.body.appendChild(container);
+          }
+          // createContainer();
+        }
+      }
+    );
+
+    onMounted(() => {
+      setTimeout(() => {
+        getReference();
+      }, 500);
+    });
+
+    const getReference = async () => {
+      let element = unref(root);
+
+      const query = Taro.createSelectorQuery();
+      query.select(`#${(element as any).id}`) &&
+        query
+          .select(`#${(element as any).id}`)
+          .fields(
+            {
+              computedStyle: ['width', 'height', 'lineHeight', 'paddingTop', 'paddingBottom']
+            },
+            (res) => {
+              lineHeight = pxToNumber(res.lineHeight);
+              maxHeight = Math.floor(
+                lineHeight * (Number(props.rows) + 0.5) + pxToNumber(res.paddingTop) + pxToNumber(res.paddingBottom)
+              );
+
+              originHeight = pxToNumber(res.height);
+
+              widthRef.value = res.width;
+
+              calcEllipse();
+            }
+          )
+          .exec();
+    };
+
+    // 计算省略号的位置
+    const calcEllipse = async () => {
+      const refe = await useTaroRect(rootContain, Taro);
+
+      if (refe.height <= maxHeight) {
+        state.exceeded = false;
+      } else {
+        const rowNum = Math.floor(props.content.length / (originHeight / lineHeight - 1)); // 每行的字数
+
+        if (props.direction === 'middle') {
+          const end = props.content.length;
+          tailorMiddle(
+            [0, rowNum * (Number(props.rows) + 0.5)],
+            [props.content.length - rowNum * (Number(props.rows) + 0.5), end]
+          );
+        } else if (props.direction === 'end') {
+          const end = rowNum * (Number(props.rows) + 0.5);
+          tailor(0, end);
+        } else {
+          const start = props.content.length - rowNum * (Number(props.rows) + 0.5) - 5;
+
+          tailor(start, props.content.length);
+        }
+      }
+    };
+
+    // 计算 start/end 省略
+    const tailor = async (left: number, right: number) => {
+      const actionText = state.expanded ? props.collapseText : props.expandText;
+
+      console.log(actionText);
+      const end = props.content.length;
+
+      if (right - left <= 1) {
+        state.exceeded = true;
+        if (props.direction === 'end') {
+          (ellipsis as any).value = {
+            leading: props.content.slice(0, left) + props.symbol
+          };
+        } else {
+          (ellipsis as any).value = {
+            tailing: props.symbol + props.content.slice(right, end)
+          };
+        }
+        return false;
+      }
+      const middle = Math.round((left + right) / 2);
+
+      if (props.direction === 'end') {
+        contantCopy.value = props.content.slice(0, middle) + props.symbol + actionText;
+      } else {
+        contantCopy.value = actionText + props.symbol + props.content.slice(middle, end);
+      }
+      setTimeout(async () => {
+        const refe = await useTaroRect(rootContain, Taro);
+        if (refe.height <= maxHeight) {
+          if (props.direction === 'end') {
+            tailor(middle, right);
+          } else {
+            tailor(left, middle);
+          }
+        } else {
+          if (props.direction === 'end') {
+            tailor(left, middle);
+          } else {
+            tailor(middle, right);
+          }
+        }
+      }, 10);
+    };
+    // 计算 middle 省略
+    const tailorMiddle = async (leftPart: [number, number], rightPart: [number, number]) => {
+      const actionText = state.expanded ? props.collapseText : props.expandText;
+      const end = props.content.length;
+      if (leftPart[1] - leftPart[0] <= 1 && rightPart[1] - rightPart[0] <= 1) {
+        state.exceeded = true;
+        (ellipsis as any).value = {
+          leading: props.content.slice(0, leftPart[0]) + props.symbol,
+          tailing: props.symbol + props.content.slice(rightPart[1], end)
+        };
+        return false;
+      }
+      const leftPartMiddle = Math.floor((leftPart[0] + leftPart[1]) / 2);
+      const rightPartMiddle = Math.ceil((rightPart[0] + rightPart[1]) / 2);
+
+      contantCopy.value =
+        props.content.slice(0, leftPartMiddle) +
+        props.symbol +
+        actionText +
+        props.symbol +
+        props.content.slice(rightPartMiddle, end);
+
+      setTimeout(async () => {
+        const refe = await useTaroRect(rootContain, Taro);
+        if (refe.height <= maxHeight) {
+          tailorMiddle([leftPartMiddle, leftPart[1]], [rightPart[0], rightPartMiddle]);
+        } else {
+          tailorMiddle([leftPart[0], leftPartMiddle], [rightPartMiddle, rightPart[1]]);
+        }
+      }, 10);
+    };
+
+    const pxToNumber = (value: string | null) => {
+      if (!value) return 0;
+      const match = value.match(/^\d*(\.\d*)?/);
+      return match ? Number(match[0]) : 0;
+    };
+
+    // 展开收起
+    const clickHandle = (type: number) => {
+      if (type == 1) {
+        state.expanded = true;
+        emit('change', 'expand');
+      } else {
+        state.expanded = false;
+        emit('change', 'collapse');
+      }
+    };
+
+    // 文本点击
+    const handleClick = () => {
+      emit('click');
+    };
+
+    return {
+      ...toRefs(state),
+      root,
+      rootContain,
+      ellipsis,
+      classes,
+      contantCopy,
+      clickHandle,
+      handleClick,
+      refRandomId,
+      widthRef
+    };
+  }
+});
+</script>

+ 230 - 0
src/packages/__VUE/ellipsis/index.vue

@@ -0,0 +1,230 @@
+<template>
+  <view :class="classes" @click="handleClick" ref="root">
+    <view v-if="!exceeded">{{ content }}</view>
+    <view v-if="exceeded && !expanded">
+      {{ ellipsis.leading
+      }}<span class="nut-ellipsis-text" v-if="expandText" @click.stop="clickHandle(1)">{{ expandText }}</span
+      >{{ ellipsis.tailing }}
+    </view>
+    <view v-if="exceeded && expanded">
+      {{ content }}
+      <span class="nut-ellipsis-text" v-if="expandText" @click.stop="clickHandle(2)">{{ collapseText }}</span>
+    </view>
+  </view>
+</template>
+<script lang="ts">
+import { ref, reactive, toRefs, computed, onMounted, PropType, watch } from 'vue';
+import { createComponent } from '@/packages/utils/create';
+const { componentName, create } = createComponent('ellipsis');
+export type Direction = 'start' | 'end' | 'middle';
+
+type EllipsisedValue = {
+  leading?: string;
+  tailing?: string;
+};
+
+export default create({
+  props: {
+    content: {
+      type: String,
+      default: ''
+    },
+    direction: {
+      type: String as PropType<Direction>,
+      default: 'end'
+    },
+    rows: {
+      type: [Number, String],
+      default: 1
+    },
+    expandText: {
+      type: String,
+      default: ''
+    },
+    collapseText: {
+      type: String,
+      default: ''
+    },
+    symbol: {
+      type: String,
+      default: '...'
+    }
+  },
+  emits: ['click', 'change'],
+
+  setup(props, { emit }) {
+    const root = ref(null);
+    let container: any = null;
+    let maxHeight = 0; // 当行的最大高度
+    const ellipsis = ref<EllipsisedValue>();
+    const state = reactive({
+      exceeded: false, //是否超出
+      expanded: false //是否折叠
+    });
+
+    const classes = computed(() => {
+      const prefixCls = componentName;
+      return {
+        [prefixCls]: true
+      };
+    });
+
+    watch(
+      () => props.content,
+      (newV, oldVal) => {
+        if (newV != oldVal) {
+          if (container) {
+            document.body.appendChild(container);
+          }
+          createContainer();
+        }
+      }
+    );
+
+    onMounted(() => {
+      createContainer();
+    });
+
+    // 创建虚拟 container,内容为 props.contant 的内容
+    const createContainer = () => {
+      if (!root.value) return;
+      const originStyle = window.getComputedStyle(root.value);
+      container = document.createElement('div');
+      const styleNames: string[] = Array.prototype.slice.apply(originStyle);
+      styleNames.forEach((name) => {
+        container.style.setProperty(name, originStyle.getPropertyValue(name));
+      });
+      container.style.position = 'fixed';
+      container.style.left = '999999px';
+      container.style.top = '999999px';
+      container.style.zIndex = '-1000';
+      container.style.height = 'auto';
+      container.style.minHeight = 'auto';
+      container.style.maxHeight = 'auto';
+      container.style.textOverflow = 'clip';
+      container.style.whiteSpace = 'normal';
+      container.style.webkitLineClamp = 'unset';
+      container.style.display = 'block';
+      const lineHeight = pxToNumber(originStyle.lineHeight);
+      maxHeight = Math.floor(
+        lineHeight * (Number(props.rows) + 0.5) +
+          pxToNumber(originStyle.paddingTop) +
+          pxToNumber(originStyle.paddingBottom)
+      );
+
+      container.innerText = props.content;
+      document.body.appendChild(container);
+
+      calcEllipse();
+    };
+
+    // 计算省略号的位置
+    const calcEllipse = () => {
+      if (container.offsetHeight <= maxHeight) {
+        state.exceeded = false;
+      } else {
+        state.exceeded = true;
+        const end = props.content.length;
+
+        const middle = Math.floor((0 + end) / 2);
+
+        const ellipsised = props.direction === 'middle' ? tailorMiddle([0, middle], [middle, end]) : tailor(0, end);
+
+        (ellipsis as any).value = ellipsised;
+
+        document.body.removeChild(container);
+      }
+    };
+    // 计算 start/end 省略
+    const tailor: (left: number, right: number) => EllipsisedValue = (left: number, right: number) => {
+      const actionText = state.expanded ? props.collapseText : props.expandText;
+      const end = props.content.length;
+
+      if (right - left <= 1) {
+        if (props.direction === 'end') {
+          return {
+            leading: props.content.slice(0, left) + props.symbol
+          };
+        } else {
+          return {
+            tailing: props.symbol + props.content.slice(right, end)
+          };
+        }
+      }
+      const middle = Math.round((left + right) / 2);
+      if (props.direction === 'end') {
+        container.innerText = props.content.slice(0, middle) + props.symbol + actionText;
+      } else {
+        container.innerText = actionText + props.symbol + props.content.slice(middle, end);
+      }
+
+      if (container.offsetHeight <= maxHeight) {
+        if (props.direction === 'end') {
+          return tailor(middle, right);
+        } else {
+          return tailor(left, middle);
+        }
+      } else {
+        if (props.direction === 'end') {
+          return tailor(left, middle);
+        } else {
+          return tailor(middle, right);
+        }
+      }
+    };
+    // 计算 middle 省略
+    const tailorMiddle: (leftPart: [number, number], rightPart: [number, number]) => EllipsisedValue = (
+      leftPart: [number, number],
+      rightPart: [number, number]
+    ) => {
+      const actionText = state.expanded ? props.collapseText : props.expandText;
+      const end = props.content.length;
+      if (leftPart[1] - leftPart[0] <= 1 && rightPart[1] - rightPart[0] <= 1) {
+        return {
+          leading: props.content.slice(0, leftPart[0]) + props.symbol,
+          tailing: props.symbol + props.content.slice(rightPart[1], end)
+        };
+      }
+      const leftPartMiddle = Math.floor((leftPart[0] + leftPart[1]) / 2);
+      const rightPartMiddle = Math.ceil((rightPart[0] + rightPart[1]) / 2);
+
+      container.innerText =
+        props.content.slice(0, leftPartMiddle) +
+        props.symbol +
+        actionText +
+        props.symbol +
+        props.content.slice(rightPartMiddle, end);
+
+      if (container.offsetHeight <= maxHeight) {
+        return tailorMiddle([leftPartMiddle, leftPart[1]], [rightPart[0], rightPartMiddle]);
+      } else {
+        return tailorMiddle([leftPart[0], leftPartMiddle], [rightPartMiddle, rightPart[1]]);
+      }
+    };
+
+    const pxToNumber = (value: string | null) => {
+      if (!value) return 0;
+      const match = value.match(/^\d*(\.\d*)?/);
+      return match ? Number(match[0]) : 0;
+    };
+
+    // 展开收起
+    const clickHandle = (type: number) => {
+      if (type == 1) {
+        state.expanded = true;
+        emit('change', 'expand');
+      } else {
+        state.expanded = false;
+        emit('change', 'collapse');
+      }
+    };
+
+    // 文本点击
+    const handleClick = () => {
+      emit('click');
+    };
+
+    return { ...toRefs(state), root, ellipsis, classes, clickHandle, handleClick };
+  }
+});
+</script>

+ 1 - 0
src/packages/__VUE/image/__tests__/image.spec.ts

@@ -0,0 +1 @@
+import { mount } from '@vue/test-utils';

+ 128 - 0
src/packages/__VUE/image/demo.vue

@@ -0,0 +1,128 @@
+<template>
+  <div class="demo">
+    <h2>{{ translate('basic') }}</h2>
+    <nut-image :src="src" width="100" height="100"></nut-image>
+
+    <h2>{{ translate('fill') }}</h2>
+    <nut-row :gutter="10" flex-wrap="wrap">
+      <nut-col :span="8" v-for="fit in fits" :key="fit">
+        <nut-image :src="src" width="100" height="100" :fit="fit"></nut-image>
+        <div class="text">{{ fit }}</div>
+      </nut-col>
+    </nut-row>
+
+    <h2>{{ translate('position') }}</h2>
+    <nut-row :gutter="10" flex-wrap="wrap">
+      <nut-col :span="8" v-for="pos in position2" :key="pos">
+        <nut-image :src="src" width="100" height="100" fit="contain" :position="pos"></nut-image>
+        <div class="text">contain</div>
+        <div class="text">{{ pos }}</div>
+      </nut-col>
+
+      <nut-col :span="8" v-for="pos in position1" :key="pos">
+        <nut-image :src="src" width="100" height="100" fit="cover" :position="pos"></nut-image>
+        <div class="text">cover</div>
+        <div class="text">{{ pos }}</div>
+      </nut-col>
+    </nut-row>
+
+    <h2>{{ translate('circle') }}</h2>
+    <nut-row :gutter="10">
+      <nut-col :span="8">
+        <nut-image :src="src" width="100" height="100" fit="contain" round></nut-image>
+        <div class="text">contain</div>
+      </nut-col>
+      <nut-col :span="8">
+        <nut-image :src="src" width="100" height="100" fit="cover" round></nut-image>
+        <div class="text">cover</div>
+      </nut-col>
+      <nut-col :span="8">
+        <nut-image :src="src" width="100" height="100" fit="cover" radius="10" round></nut-image>
+        <div class="text">cover</div>
+      </nut-col>
+    </nut-row>
+
+    <h2>{{ translate('loading') }}</h2>
+    <nut-cell>
+      <nut-row :gutter="10">
+        <nut-col :span="8">
+          <nut-image width="100" height="100" showLoading></nut-image>
+          <div class="text">默认</div>
+        </nut-col>
+        <nut-col :span="8">
+          <nut-image width="100" height="100" showLoading>
+            <template #loading>
+              <nut-icon name="loading"></nut-icon>
+            </template>
+          </nut-image>
+          <div class="text">自定义</div>
+        </nut-col>
+      </nut-row>
+    </nut-cell>
+
+    <h2>{{ translate('error') }}</h2>
+    <nut-cell>
+      <nut-row :gutter="10">
+        <nut-col :span="8">
+          <nut-image src="https://x" width="100" height="100" showError></nut-image>
+          <div class="text">默认</div>
+        </nut-col>
+        <nut-col :span="8">
+          <nut-image src="https://x" width="100" height="100" showLoading>
+            <nut-icon name="circle-close"></nut-icon>
+          </nut-image>
+          <div class="text">自定义</div>
+        </nut-col>
+      </nut-row>
+    </nut-cell>
+  </div>
+</template>
+<script lang="ts">
+import { ref } from 'vue';
+import { createComponent } from '@/packages/utils/create';
+const { createDemo, translate } = createComponent('image');
+import { useTranslate } from '@/sites/assets/util/useTranslate';
+useTranslate({
+  'zh-CN': {
+    basic: '基本用法',
+    fill: '填充模式',
+    position: '图片位置',
+    circle: '圆形图片',
+    loading: '加载中提示',
+    error: '加载失败'
+  },
+  'en-US': {
+    basic: 'Basic Usage',
+    fill: 'Object Fill',
+    position: 'Object Position',
+    circle: 'Round',
+    loading: 'Loading',
+    error: 'Error'
+  }
+});
+export default createDemo({
+  props: {},
+  setup() {
+    const src = ref('//img10.360buyimg.com/ling/jfs/t1/181258/24/10385/53029/60d04978Ef21f2d42/92baeb21f907cd24.jpg');
+    const fits = ref(['contain', 'cover', 'fill', 'none', 'scale-down']);
+    const position1 = ref(['left', 'center', 'right']);
+    const position2 = ref(['top', 'center', 'bottom']);
+    return { translate, fits, position1, position2, src };
+  }
+});
+</script>
+<style lang="scss" scoped>
+.demo {
+  // background: #fff !important;
+  .text {
+    margin-top: 5px;
+    text-align: center;
+    color: #999;
+  }
+  .nut-row-flex-wrap {
+    .nut-col {
+      margin-bottom: 20px;
+    }
+  }
+}
+</style>

+ 173 - 0
src/packages/__VUE/image/doc.en-US.md

@@ -0,0 +1,173 @@
+# image 
+
+### Intro
+
+Enhanced img tag with multiple image fill modes, support for loading hint, loading failure hint.
+
+### Install
+
+```javascript
+
+import { createApp } from 'vue';
+import { Image } from '@nutui/nutui';
+
+const app = createApp();
+app.use();
+
+```
+
+### Basic Usage
+
+
+The basic usage is the same as that of the native IMG tag. You can set the native attributes such as SRC, width, height, and Alt.
+
+:::demo
+
+```html
+<template>
+  <nut-image src="//img10.360buyimg.com/ling/jfs/t1/181258/24/10385/53029/60d04978Ef21f2d42/92baeb21f907cd24.jpg" width="100" height="100"></nut-image>
+</template>
+```
+
+:::
+
+### Object Fill
+
+The `fit` attribute is used to set the image filling mode, which is equivalent to the original `Object-fit` attribute.
+
+:::demo
+
+```html
+<template>
+  <nut-image 
+    src="//img10.360buyimg.com/ling/jfs/t1/181258/24/10385/53029/60d04978Ef21f2d42/92baeb21f907cd24.jpg" 
+    width="100" 
+    height="100"
+    fit="contain"/>
+</template>
+```
+
+:::
+
+### Object Position
+
+The position property can be used to set the position of the picture, which is equivalent to the original Object-position property when combined with the FIT property.
+
+:::demo
+
+```html
+<template>
+  <nut-image 
+    src="//img10.360buyimg.com/ling/jfs/t1/181258/24/10385/53029/60d04978Ef21f2d42/92baeb21f907cd24.jpg" 
+    width="100" 
+    height="100"
+    fit="contain"
+    postion="left"/>
+</template>
+```
+
+:::
+
+### Round
+
+The round attribute allows you to set the image to be round. Note that if the image is not contained and fit is contained or scale-down, a full circle cannot be contained.
+
+:::demo
+
+```html
+<template>
+  <nut-image 
+    src="//img10.360buyimg.com/ling/jfs/t1/181258/24/10385/53029/60d04978Ef21f2d42/92baeb21f907cd24.jpg" 
+    width="100" 
+    height="100"
+    round/>
+</template>
+```
+
+:::
+
+### Loading
+
+The Image component provides a default loading prompt and supports custom content through the loading slot.
+
+:::demo
+
+```html
+<template>
+  <nut-image width="100" height="100" showLoading>
+    <template #loading>
+      <nut-icon name="loading"></nut-icon>
+    </template>
+  </nut-image>
+</template>
+```
+
+:::
+
+### Error
+
+The Image component provides a default loading failure warning and supports custom content through the error slot.
+
+:::demo
+
+```html
+<template>
+  <nut-image src="https://x" width="100" height="100" showLoading>
+    <template #error> <nut-icon name="circle-close"></nut-icon> </template>
+  </nut-image>
+</template>
+```
+
+:::
+
+## API
+
+### Props
+
+| Attribute         | Description                             | Type   | Default           |
+|--------------|----------------------------------|--------|------------------|
+| src         | Src               | String | -                |
+| fit         | Fit mode, same as object-fit     | ImageFit | 'fill'                |
+| position    | Position, same as object-position  | ImagePosition | 'center'              |
+| alt         | Alt               | String | -                |
+| width         | Width,Default unit px             | String | -                |
+| height         | Height,Default unit px              | String | -                |
+| round         | Whether to be round               | Boolean | false              |
+| radius         | Border Raduis               | String \| Numer | -                |
+| showError         | Whether to show error placeholder | Boolean | false              |
+| showLoading         | Whether to show loading placeholder | Boolean | true              |
+
+### ImageFit 
+
+| Attribute         | Description                             |
+|--------------|----------------------------------|
+| contain         | Keep aspect ratio, fully display the long side of the image    |
+| cover         | Keep aspect ratio, fully display the short side of the image, cutting the long side     |
+| fill    | Stretch and resize image to fill the content box  |
+| none    | Not resize image  |
+| scale-down    | Take the smaller of none or contain  |
+
+### ImagePosition 
+
+| Attribute         | Description                             |
+|--------------|----------------------------------|
+| center         | Align Center    |
+| top         | Align Top     |
+| right    | Align Right  |
+| bottom    | Align Bottom  |
+| left   | Align Left  |
+
+### Slots
+| Attribute         | Description                             |
+|--------------|----------------------------------|
+| loading      | Custom loading placeholder     |
+| error    | Custom error placeholder  |
+
+### Events
+
+| Event | Description           | Arguments     |
+|--------|----------------|--------------|
+| click  | Emitted when image is clicked | event: Event |
+| load  | Emitted when image loaded | - |
+| error  | Emitted when image load failed | event: Event |
+

+ 172 - 0
src/packages/__VUE/image/doc.md

@@ -0,0 +1,172 @@
+# image
+
+### 介绍
+
+增强版的 img 标签,提供多种图片填充模式,支持图片加载中提示、加载失败提示。
+
+### 安装
+
+```javascript
+
+import { createApp } from 'vue';
+import { Image } from '@nutui/nutui';
+
+const app = createApp();
+app.use();
+
+```
+
+### 基础用法
+
+基础用法与原生 img 标签一致,可以设置 src、width、height、alt 等原生属性。
+
+:::demo
+
+```html
+<template>
+  <nut-image src="//img10.360buyimg.com/ling/jfs/t1/181258/24/10385/53029/60d04978Ef21f2d42/92baeb21f907cd24.jpg" width="100" height="100"></nut-image>
+</template>
+```
+
+:::
+
+### 填充模式
+
+通过 fit 属性可以设置图片填充模式,等同于原生的 object-fit 属性,可选值见下方表格。
+
+:::demo
+
+```html
+<template>
+  <nut-image 
+    src="//img10.360buyimg.com/ling/jfs/t1/181258/24/10385/53029/60d04978Ef21f2d42/92baeb21f907cd24.jpg" 
+    width="100" 
+    height="100"
+    fit="contain"/>
+</template>
+```
+
+:::
+
+### 图片位置
+
+通过 position 属性可以设置图片位置,结合 fit 属性使用,等同于原生的 object-position 属性。
+
+:::demo
+
+```html
+<template>
+  <nut-image 
+    src="//img10.360buyimg.com/ling/jfs/t1/181258/24/10385/53029/60d04978Ef21f2d42/92baeb21f907cd24.jpg" 
+    width="100" 
+    height="100"
+    fit="contain"
+    postion="left"/>
+</template>
+```
+
+:::
+
+### 圆形图片
+
+通过 round 属性可以设置图片变圆,注意当图片宽高不相等且 fit 为 contain 或 scale-down 时,将无法填充一个完整的圆形。
+
+:::demo
+
+```html
+<template>
+  <nut-image 
+    src="//img10.360buyimg.com/ling/jfs/t1/181258/24/10385/53029/60d04978Ef21f2d42/92baeb21f907cd24.jpg" 
+    width="100" 
+    height="100"
+    round/>
+</template>
+```
+
+:::
+
+### 加载中图片
+
+`Image` 组件提供了默认的加载中提示,支持通过 `loading` 插槽自定义内容。
+
+:::demo
+
+```html
+<template>
+  <nut-image width="100" height="100" showLoading>
+    <template #loading>
+      <nut-icon name="loading"></nut-icon>
+    </template>
+  </nut-image>
+</template>
+```
+
+:::
+
+### 加载失败
+
+`Image` 组件提供了默认的加载失败提示,支持通过 `error` 插槽自定义内容。
+
+:::demo
+
+```html
+<template>
+  <nut-image src="https://x" width="100" height="100" showLoading>
+    <template #error> 加载失败 </template>
+  </nut-image>
+</template>
+```
+
+:::
+
+## API
+
+### Props
+
+| 参数         | 说明                             | 类型   | 默认值           |
+|--------------|----------------------------------|--------|------------------|
+| src         | 图片链接               | String | -                |
+| fit         | 图片填充模式,等同于原生的 object-fit 属性     | ImageFit | 'fill'                |
+| position    | 图片位置,等同于原生的 object-position 属性  | ImagePosition | 'center'              |
+| alt         | 替代文本               | String | -                |
+| width         | 宽度,默认单位`px`               | String | -                |
+| height         | 高度,默认单位`px`               | String | -                |
+| round         | 是否显示为圆角               | Boolean | false              |
+| radius         | 圆角大小               | String \| Numer | -                |
+| showError         | 是否展示图片加载失败| Boolean | false              |
+| showLoading         | 是否展示加载中图片               | Boolean | true              |
+
+### ImageFit 图片填充模式
+
+| 参数         | 说明                             |
+|--------------|----------------------------------|
+| contain         | 保持宽高缩放图片,使图片的长边能完全显示出来    |
+| cover         | 保持宽高缩放图片,使图片的短边能完全显示出来,裁剪长边     |
+| fill    | 拉伸图片,使图片填满元素  |
+| none    | 保持图片原有尺寸  |
+| scale-down    | 取 none 或 contain 中较小的一个  |
+
+### ImagePosition 图片位置
+
+| 参数         | 说明                             |
+|--------------|----------------------------------|
+| center         | 居中对齐    |
+| top         | 顶部对齐     |
+| right    | 右侧对齐  |
+| bottom    | 底部对齐  |
+| left   | 左侧对齐  |
+
+
+### Slots
+| 参数         | 说明                             |
+|--------------|----------------------------------|
+| loading      | 自定义加载中的提示内容     |
+| error    | 自定义记载失败的提示内容  |
+
+### Events
+
+| 事件名 | 说明           | 回调参数     |
+|--------|----------------|--------------|
+| click  | 点击图片时触发 | event: Event |
+| load  | 图片加载完后触发 | -- |
+| error  | 图片加载失败后触发 | -- |

+ 7 - 0
src/packages/__VUE/image/doc.taro.md

@@ -0,0 +1,7 @@
+#  Image 图片
+
+### 介绍
+
+增强版的 img 标签,提供多种图片填充模式,支持图片加载中提示、加载失败提示。
+
+#### 直接使用 Taro 现有 Image 组件开发 [参考文档](https://taro-docs.jd.com/taro/docs/components/media/image)

+ 39 - 0
src/packages/__VUE/image/index.scss

@@ -0,0 +1,39 @@
+.nut-image {
+  display: block;
+  position: relative;
+  .nut-img {
+    display: block;
+    width: 100%;
+    height: 100%;
+  }
+
+  &.nut-image-round {
+    border-radius: 50%;
+    overflow: hidden;
+  }
+
+  .nut-img-loading {
+    width: 100%;
+    height: 100%;
+    position: absolute;
+    top: 0;
+    left: 0;
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+    background: #f7f8fa;
+  }
+  .nut-img-error {
+    width: 100%;
+    height: 100%;
+    position: absolute;
+    top: 0;
+    left: 0;
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+    background: #f7f8fa;
+  }
+}

+ 38 - 0
src/packages/__VUE/image/index.taro.vue

@@ -0,0 +1,38 @@
+<template>
+  <view :class="classes" @click="handleClick">
+    <view>{{ data }}</view>
+  </view>
+</template>
+<script lang="ts">
+import { reactive, toRefs, computed } from 'vue';
+import { createComponent } from '@/packages/utils/create';
+const { componentName, create } = createComponent('image');
+export default create({
+  props: {
+    name: {
+      type: String,
+      default: ''
+    }
+  },
+  emits: ['click'],
+
+  setup(props, { emit }) {
+    const state = reactive({
+      data: 'Welcome to developing components'
+    });
+
+    const classes = computed(() => {
+      const prefixCls = componentName;
+      return {
+        [prefixCls]: true
+      };
+    });
+
+    const handleClick = (event: Event) => {
+      emit('click', event);
+    };
+
+    return { ...toRefs(state), classes, handleClick };
+  }
+});
+</script>

+ 134 - 0
src/packages/__VUE/image/index.vue

@@ -0,0 +1,134 @@
+<template>
+  <view :class="classes" :style="stylebox" @click="imageClick">
+    <img class="nut-img" :src="src" :alt="alt" @load="load" @error="error" :style="styles" />
+
+    <view class="nut-img-loading" v-if="loading">
+      <nut-icon name="image" v-if="!slotLoding"></nut-icon>
+      <slot name="loading"></slot>
+    </view>
+
+    <view class="nut-img-error" v-if="isError && !loading">
+      <nut-icon name="image-error" v-if="!slotError"></nut-icon>
+      <slot name="error"></slot>
+    </view>
+  </view>
+</template>
+<script lang="ts">
+import { reactive, toRefs, computed, PropType, useSlots, watch } from 'vue';
+import { createComponent } from '@/packages/utils/create';
+import { pxCheck } from '../../utils/pxCheck';
+const { componentName, create } = createComponent('image');
+
+export type ImageFit = 'contain' | 'cover' | 'fill' | 'none' | 'scale-down';
+export type ImagePosition = 'center' | 'top' | 'right' | 'bottom' | 'left' | string;
+
+export default create({
+  props: {
+    src: String,
+    fit: {
+      type: String as PropType<ImageFit>,
+      default: 'fill'
+    },
+    position: {
+      type: String as PropType<ImagePosition>,
+      default: 'center'
+    },
+    alt: {
+      type: String,
+      default: ''
+    },
+    width: {
+      type: String,
+      default: 'center'
+    },
+    height: {
+      type: String,
+      default: ''
+    },
+    round: {
+      type: Boolean,
+      default: false
+    },
+    radius: [String, Number],
+    showError: {
+      type: Boolean,
+      default: true
+    },
+    showLoading: {
+      type: Boolean,
+      default: true
+    }
+  },
+  emits: ['click', 'load', 'error'],
+
+  setup(props, { emit }) {
+    const state = reactive({
+      loading: true,
+      isError: false,
+      slotLoding: useSlots().loading,
+      slotError: useSlots().error
+    });
+
+    const classes = computed(() => {
+      const prefixCls = componentName;
+      return {
+        [prefixCls]: true,
+        [`${prefixCls}-round`]: props.round
+      };
+    });
+    const stylebox = computed(() => {
+      let style: {
+        height?: string;
+        width?: string;
+        overflow?: string;
+        borderRadius?: any;
+      } = {};
+
+      if (props.width) style.width = pxCheck(props.width);
+      if (props.height) style.height = pxCheck(props.height);
+
+      if (props.radius !== undefined && props.radius !== null) {
+        style.overflow = 'hidden';
+        style.borderRadius = pxCheck(props.radius);
+      }
+
+      return style;
+    });
+    const styles = computed(() => {
+      let styless: {
+        objectFit: string;
+        objectPosition: string;
+      } = {
+        objectFit: props.fit,
+        objectPosition: props.position
+      };
+
+      return styless;
+    });
+
+    watch(
+      () => props.src,
+      (val) => {
+        (state.isError = false), (state.loading = true);
+      }
+    );
+    // 图片加载
+    const load = () => {
+      state.loading = false;
+      emit('load');
+    };
+    // 图片加载失败
+    const error = () => {
+      state.isError = true;
+      state.loading = false;
+      emit('error');
+    };
+
+    const imageClick = (event: Event) => {
+      emit('click', event);
+    };
+
+    return { ...toRefs(state), imageClick, classes, styles, stylebox, error, load };
+  }
+});
+</script>

+ 3 - 1
src/packages/styles/font/config.json

@@ -75,7 +75,9 @@
         "play-start",
         "play-double-back",
         "play-double-forward",
-        "voice"
+        "voice",
+        "image",
+        "image-error"
       ]
     },
     {

+ 49 - 3
src/packages/styles/font/demo_index.html

@@ -55,6 +55,18 @@
           <ul class="icon_lists dib-box">
           
             <li class="dib">
+              <span class="icon nutui-iconfont">&#xe60a;</span>
+                <div class="name">image-error</div>
+                <div class="code-name">&amp;#xe60a;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon nutui-iconfont">&#xe609;</span>
+                <div class="name">image</div>
+                <div class="code-name">&amp;#xe609;</div>
+              </li>
+          
+            <li class="dib">
               <span class="icon nutui-iconfont">&#xe608;</span>
                 <div class="name">voice</div>
                 <div class="code-name">&amp;#xe608;</div>
@@ -774,9 +786,9 @@
 <pre><code class="language-css"
 >@font-face {
   font-family: 'nutui-iconfont';
-  src: url('iconfont.woff2?t=1644572435352') format('woff2'),
-       url('iconfont.woff?t=1644572435352') format('woff'),
-       url('iconfont.ttf?t=1644572435352') format('truetype');
+  src: url('iconfont.woff2?t=1654497552263') format('woff2'),
+       url('iconfont.woff?t=1654497552263') format('woff'),
+       url('iconfont.ttf?t=1654497552263') format('truetype');
 }
 </code></pre>
           <h3 id="-iconfont-">第二步:定义使用 iconfont 的样式</h3>
@@ -803,6 +815,24 @@
         <ul class="icon_lists dib-box">
           
           <li class="dib">
+            <span class="icon nutui-iconfont nut-icon-image-error"></span>
+            <div class="name">
+              image-error
+            </div>
+            <div class="code-name">.nut-icon-image-error
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon nutui-iconfont nut-icon-image"></span>
+            <div class="name">
+              image
+            </div>
+            <div class="code-name">.nut-icon-image
+            </div>
+          </li>
+          
+          <li class="dib">
             <span class="icon nutui-iconfont nut-icon-voice"></span>
             <div class="name">
               voice
@@ -1884,6 +1914,22 @@
           
             <li class="dib">
                 <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#nut-icon-image-error"></use>
+                </svg>
+                <div class="name">image-error</div>
+                <div class="code-name">#nut-icon-image-error</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#nut-icon-image"></use>
+                </svg>
+                <div class="name">image</div>
+                <div class="code-name">#nut-icon-image</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
                   <use xlink:href="#nut-icon-voice"></use>
                 </svg>
                 <div class="name">voice</div>

+ 11 - 3
src/packages/styles/font/iconfont.css

@@ -1,8 +1,8 @@
 @font-face {
   font-family: "nutui-iconfont"; /* Project id 2166874 */
-  src: url('iconfont.woff2?t=1644572435352') format('woff2'),
-       url('iconfont.woff?t=1644572435352') format('woff'),
-       url('iconfont.ttf?t=1644572435352') format('truetype');
+  src: url('iconfont.woff2?t=1654497552263') format('woff2'),
+       url('iconfont.woff?t=1654497552263') format('woff'),
+       url('iconfont.ttf?t=1654497552263') format('truetype');
 }
 
 .nutui-iconfont {
@@ -13,6 +13,14 @@
   -moz-osx-font-smoothing: grayscale;
 }
 
+.nut-icon-image-error:before {
+  content: "\e60a";
+}
+
+.nut-icon-image:before {
+  content: "\e609";
+}
+
 .nut-icon-voice:before {
   content: "\e608";
 }

File diff suppressed because it is too large
+ 15 - 15
src/packages/styles/font/iconfont.js


+ 14 - 0
src/packages/styles/font/iconfont.json

@@ -6,6 +6,20 @@
   "description": "nutui 3.0字体管理",
   "glyphs": [
     {
+      "icon_id": "30188161",
+      "name": "image-error",
+      "font_class": "image-error",
+      "unicode": "e60a",
+      "unicode_decimal": 58890
+    },
+    {
+      "icon_id": "30187558",
+      "name": "image",
+      "font_class": "image",
+      "unicode": "e609",
+      "unicode_decimal": 58889
+    },
+    {
       "icon_id": "27579944",
       "name": "voice",
       "font_class": "voice",

BIN
src/packages/styles/font/iconfont.ttf


BIN
src/packages/styles/font/iconfont.woff


BIN
src/packages/styles/font/iconfont.woff2


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

@@ -788,5 +788,8 @@ $comment-header-time-color: rgba(153, 153, 153, 1) !default;
 $comment-bottom-label-color: rgba(153, 153, 153, 1) !default;
 $comment-shop-color: $primary-color !default;
 
+// Ellipsis
+$ellipsis-expand-collapse-color: #3460fa !default;
+
 @import './mixins/index';
 @import './animation/index';

+ 3 - 2
src/sites/mobile-taro/vue/project.private.config.json

@@ -28,9 +28,10 @@
           "scene": null
         },
         {
-          "name": "feedback/pages/progress/index",
-          "pathName": "feedback/pages/progress/index",
+          "name": "exhibition/pages/ellipsis/index",
+          "pathName": "exhibition/pages/ellipsis/index",
           "query": "",
+          "launchMode": "default",
           "scene": null
         },
         {

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

@@ -1,7 +1,14 @@
 const subPackages = [
   {
     root: 'base',
-    pages: ['pages/button/index', 'pages/cell/index', 'pages/icon/index', 'pages/overlay/index', 'pages/popup/index']
+    pages: [
+      'pages/button/index',
+      'pages/cell/index',
+      'pages/icon/index',
+      'pages/overlay/index',
+      'pages/popup/index',
+      'pages/image/index'
+    ]
   },
   {
     root: 'layout',
@@ -76,7 +83,8 @@ const subPackages = [
       'pages/popover/index',
       'pages/skeleton/index',
       'pages/collapse/index',
-      'pages/table/index'
+      'pages/table/index',
+      'pages/ellipsis/index'
     ]
   },
   {

+ 1 - 0
src/sites/mobile-taro/vue/src/base/pages/image/index.config.ts

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

+ 17 - 0
src/sites/mobile-taro/vue/src/base/pages/image/index.vue

@@ -0,0 +1,17 @@
+<template>
+  <div class="demo">
+    <h2>基础用法</h2>
+    <nut-cell>
+      <nut-image></nut-image>
+    </nut-cell>
+  </div>
+</template>
+<script lang="ts">
+import { defineComponent } from 'vue';
+export default defineComponent({
+  props: {},
+  setup() {
+    return {};
+  }
+});
+</script>

+ 1 - 0
src/sites/mobile-taro/vue/src/exhibition/pages/ellipsis/index.config.ts

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

+ 46 - 0
src/sites/mobile-taro/vue/src/exhibition/pages/ellipsis/index.vue

@@ -0,0 +1,46 @@
+<template>
+  <div class="demo">
+    <h2>头部省略</h2>
+    <nut-cell>
+      <nut-ellipsis :content="content" direction="start" class="elli"></nut-ellipsis>
+    </nut-cell>
+
+    <h2>尾部省略</h2>
+    <nut-cell>
+      <nut-ellipsis :content="content" direction="end" class="elli"></nut-ellipsis>
+    </nut-cell>
+
+    <h2>中间省略</h2>
+    <nut-cell>
+      <nut-ellipsis :content="content" direction="middle" class="elli"></nut-ellipsis>
+    </nut-cell>
+
+    <h2>多行省略</h2>
+    <nut-cell>
+      <nut-ellipsis :content="content" direction="start" rows="3"></nut-ellipsis>
+    </nut-cell>
+
+    <h2>展开收起</h2>
+    <nut-cell>
+      <nut-ellipsis :content="content" direction="start" expandText="展开" collapseText="收起"></nut-ellipsis>
+    </nut-cell>
+    <nut-cell>
+      <nut-ellipsis :content="content" direction="middle" expandText="展开" collapseText="收起"></nut-ellipsis>
+    </nut-cell>
+    <nut-cell>
+      <nut-ellipsis :content="content" direction="end" rows="3" expandText="展开" collapseText="收起"></nut-ellipsis>
+    </nut-cell>
+  </div>
+</template>
+<script lang="ts">
+import { defineComponent, ref } from 'vue';
+export default defineComponent({
+  props: {},
+  setup() {
+    const content = ref(
+      'NutUI3.0上线后我们研发团队也在不断的优化、测试、使用、迭代 Vue3 的相关组件,但是在跨端小程序的开发过程中,发现没有合适的组件库可以支持多端开发。为了填补这一空白,同时为了优化开发者体验,让 NutUI 能够为更多的开发者带来便利,我们决定在 NutUI 中增加小程序多端适配的能力。'
+    );
+    return { content };
+  }
+});
+</script>