Browse Source

Merge remote-tracking branch origin/next into theme

richard1015 3 years ago
parent
commit
5b5fd4d59d

+ 12 - 4
jd/createComponentMode.js

@@ -16,11 +16,12 @@ let spinner;
 let componentConfig = {
   version: '3.0.0', //版本
   name: '', //组件名称
-  type: '', //组件属于哪种类型
+  cType: '', //组件属于哪种类型
   cName: '', //组件中文名称
   desc: '', //组件描述
   show: '', //组件是否显示在demo/文档中
   tarodoc: false, //是否显示taro文档
+  type: 'component',
   // taro: true, //是否生成.taro.vue文件,因为目前默认组件都会生成,所以,此项目前用不到
   // exportEmpty: true, //表示是否要在生成运行时文件时导出组件模块,目前用不到
   // exportEmptyTaro: true, //表示是否要在生成taro运行文件时导出组件模块,目前用不到
@@ -66,7 +67,7 @@ const questions = [
   },
   {
     type: 'rawlist',
-    name: 'type',
+    name: 'cType',
     message: '请选择组件的分类',
     choices: nav.map((item) => `${item.name}`),
     validate(value) {
@@ -77,6 +78,12 @@ const questions = [
   },
   {
     type: 'confrim',
+    name: 'type',
+    message: '组件是否支持函数式调用(y/n)',
+    default: 'n'
+  },
+  {
+    type: 'confrim',
     name: 'show',
     message: '组件是否显示在文档和demo中(y/n),如:SwiperItem则不需要',
     default: 'y'
@@ -171,7 +178,7 @@ const createTest = (paths) => {
 
 const updateConfig = () => {
   /**更新 config 文件 */
-  const componentTypeItem = nav.find((navitem) => navitem.name === componentConfig.type);
+  const componentTypeItem = nav.find((navitem) => navitem.name === componentConfig.cType);
   if (!componentTypeItem.packages.find((item) => item.name === componentConfig.name)) {
     componentTypeItem.packages.push(componentConfig);
   }
@@ -182,7 +189,7 @@ const updateConfig = () => {
 
 const createDir = () => {
   const componentName = componentConfig.name.toLowerCase();
-  const componentType = nav.find((navitem) => navitem.name === componentConfig.type).enName;
+  const componentType = nav.find((navitem) => navitem.name === componentConfig.cType).enName;
   const sourcePath = path.join(`src/packages/__VUE/${componentName}`);
   const taroPath = path.join(`src/sites/mobile-taro/vue/src/${componentType}`);
   if (!fs.existsSync(sourcePath)) fs.mkdirSync(sourcePath);
@@ -223,6 +230,7 @@ const create = () => {
 const init = () => {
   inquirer.prompt(questions).then((answers) => {
     answers.show = answers.show === 'y' ? true : false;
+    answers.type = answers.type === 'y' ? 'methods' : 'component';
     componentConfig = Object.assign(componentConfig, answers);
     spinner = ora('正在生成组件模版,请稍后...').start();
     create();

+ 2 - 2
jd/generate-nutui.js

@@ -7,8 +7,8 @@ let importScssStr = `\n`;
 const packages = [];
 config.nav.map((item) => {
   item.packages.forEach((element) => {
-    let { name } = element;
-    importStr += `import ${name} from './__VUE/${name.toLowerCase()}/index.vue';\n`;
+    let { name, type } = element;
+    importStr += `import ${name} from './__VUE/${name.toLowerCase()}/index${type === 'methods' ? '' : '.vue'}';\n`;
     importScssStr += `import './__VUE/${name.toLowerCase()}/index.scss';\n`;
     packages.push(name);
   });

+ 1 - 0
package.json

@@ -81,6 +81,7 @@
     "@vue/eslint-config-typescript": "^7.0.0",
     "@vue/test-utils": "^2.0.0-rc.18",
     "autoprefixer": "^10.3.4",
+    "remark-codesandbox": "^0.10.1",
     "axios": "^0.21.0",
     "eslint": "^7.23.2",
     "eslint-plugin-prettier": "^3.3.1",

+ 23 - 0
src/config.json

@@ -198,6 +198,7 @@
           "tarodoc": true,
           "author": "zongyue3"
         }
+        
       ]
     },
     {
@@ -479,6 +480,28 @@
           "sort": 24,
           "show": true,
           "author": "wujia8"
+        },
+        {
+          "version": "3.0.0",
+          "name": "Audio",
+          "taro": true,
+          "tarodoc": true,
+          "type": "component",
+          "cName": "音频播放器",
+          "desc": "音频播放器",
+          "sort": 25,
+          "show": true,
+          "author": "yangxiaolu"
+        },
+        {
+          "version": "3.0.0",
+          "name": "AudioOperate",
+          "type": "component",
+          "cName": "音频操作按钮",
+          "desc": "音频操作按钮",
+          "sort": 26,
+          "show": false,
+          "author": "yangxiaolu"
         }
       ]
     },

+ 4 - 0
src/packages/__VUE/actionsheet/__test__/index.spec.ts

@@ -0,0 +1,4 @@
+import { mount } from '@vue/test-utils';
+import ActionSheet from '../index.vue';
+
+test('');

+ 131 - 0
src/packages/__VUE/audio/demo.vue

@@ -0,0 +1,131 @@
+<template>
+  <div class="demo">
+    <h2>基础用法</h2>
+    <nut-audio
+      style="margin-left: 20px"
+      url="http://storage.360buyimg.com/jdcdkh/SMB/VCG231024564.wav"
+      :muted="muted"
+      :autoplay="autoplay"
+      :loop="true"
+      type="icon"
+    ></nut-audio>
+
+    <h2>语音播放</h2>
+    <nut-audio
+      style="margin-left: 20px"
+      url="http://storage.360buyimg.com/jdcdkh/SMB/VCG231024564.wav"
+      :muted="muted"
+      :autoplay="autoplay"
+      :loop="false"
+      type="none"
+      ref="audioDemo"
+    >
+      <div class="nut-voice">
+        <div><nut-icon name="voice"></nut-icon></div>
+        <div>{{ duration }}"</div>
+      </div>
+    </nut-audio>
+
+    <h2>进度条展示</h2>
+    <nut-audio
+      style="margin-left: 20px"
+      url="http://storage.360buyimg.com/jdcdkh/SMB/VCG231024564.wav"
+      :muted="muted"
+      :autoplay="autoplay"
+      :loop="true"
+      type="progress"
+    >
+      <div class="nut-audio-operate-group">
+        <nut-audio-operate type="back"></nut-audio-operate>
+        <nut-audio-operate type="play"></nut-audio-operate>
+        <nut-audio-operate type="forward"></nut-audio-operate>
+        <nut-audio-operate type="mute"></nut-audio-operate>
+      </div>
+    </nut-audio>
+
+    <h2>自定义操作按钮</h2>
+    <nut-audio
+      style="margin-left: 20px"
+      url="http://storage.360buyimg.com/jdcdkh/SMB/VCG231024564.wav"
+      :muted="muted"
+      :autoplay="autoplay"
+      :loop="false"
+      type="progress"
+      @forward="forward"
+      @fastBack="fastBack"
+      @play="changeStatus"
+      @ended="ended"
+      @changeProgress="changeProgress"
+    >
+      <div class="nut-audio-operate-group">
+        <nut-audio-operate type="back"><nut-icon name="play-double-back" size="35"></nut-icon></nut-audio-operate>
+        <nut-audio-operate type="play"
+          ><nut-icon :name="!playing ? 'play-start' : 'play-stop'" size="35"></nut-icon
+        ></nut-audio-operate>
+        <nut-audio-operate type="forward"><nut-icon name="play-double-forward" size="35"></nut-icon></nut-audio-operate>
+      </div>
+    </nut-audio>
+  </div>
+</template>
+
+<script lang="ts">
+import { reactive, toRefs, ref } from '@vue/reactivity';
+import { onMounted } from '@vue/runtime-core';
+import { createComponent } from '../../utils/create';
+const { createDemo } = createComponent('audio');
+export default createDemo({
+  props: {},
+  setup() {
+    const audioDemo = ref(null);
+    const playing = ref(false);
+    const duration = ref(0);
+    const data = reactive({
+      muted: false,
+      autoplay: false
+    });
+
+    const fastBack = () => {
+      console.log('倒退');
+    };
+
+    const forward = (progress) => {
+      console.log('快进', '当前时间' + progress);
+    };
+
+    const changeStatus = (status) => {
+      console.log('当前播放状态', status);
+      playing.value = status;
+    };
+
+    const ended = () => {
+      console.log('播放结束');
+    };
+
+    const changeProgress = (val) => {
+      console.log('改变进度条', val);
+    };
+
+    onMounted(() => {
+      console.log(audioDemo.value);
+      setTimeout(() => {
+        duration.value = audioDemo.value.second.toFixed();
+      }, 500);
+    });
+
+    return { ...toRefs(data), playing, fastBack, forward, changeStatus, audioDemo, ended, duration, changeProgress };
+  }
+});
+</script>
+<style lang="scss" scoped>
+.demo {
+  .nut-voice {
+    display: flex;
+    justify-content: space-between;
+    width: 100px;
+    height: 20px;
+    padding: 8px;
+    border: 1px solid rgba(0, 0, 0, 0.6);
+    border-radius: 18px;
+  }
+}
+</style>

+ 236 - 0
src/packages/__VUE/audio/doc.md

@@ -0,0 +1,236 @@
+# Audio组件
+
+### 介绍
+
+用于音频播放
+
+### 安装
+
+
+```javascript
+import { createApp } from 'vue';
+// vue
+import { Audio} from '@nutui/nutui';
+
+const app = createApp();
+app.use(Audio);
+```
+
+### 基础用法
+
+:::demo
+
+```html
+<template>
+    <nut-audio
+      url="http://storage.360buyimg.com/jdcdkh/SMB/VCG231024564.wav"
+      :muted="muted"
+      :autoplay="autoplay"
+      :loop="true"
+      type="icon"
+    ></nut-audio>
+</template>
+<script lang="ts">
+import { reactive, toRefs } from 'vue';
+export default {
+  setup() {
+    const data = reactive({
+      muted: false,
+      autoplay: false
+    });
+    return {
+      ...toRefs(data)
+    };
+  }
+};
+</script>
+```
+:::
+
+### 语音播放
+
+:::demo
+
+```html
+<template>
+    <nut-audio
+      url="http://storage.360buyimg.com/jdcdkh/SMB/VCG231024564.wav"
+      :muted="muted"
+      :autoplay="autoplay"
+      :loop="false"
+      type="none"
+    >
+      <div class="nut-voice">
+        <div><nut-icon name="voice"></nut-icon></div>
+        <div>{{ duration }}"</div>
+      </div>
+    </nut-audio>
+</template>
+<script lang="ts">
+import { reactive, toRefs, onMounted } from 'vue';
+export default {
+  setup() {
+    const data = reactive({
+      muted: false,
+      autoplay: false
+    });
+    const duration = ref(0);
+    onMounted(() => {
+      console.log(audioDemo.value);
+      setTimeout(() => {
+        duration.value = audioDemo.value.second.toFixed();
+      }, 500);
+    });
+
+    return {
+      ...toRefs(data),
+      duration
+    };
+  }
+};
+</script>
+
+<style>
+  .nut-voice {
+    display: flex;
+    justify-content: space-between;
+    width: 100px;
+    height: 20px;
+    padding: 8px;
+    border: 1px solid rgba(0, 0, 0, 0.6);
+    border-radius: 18px;
+  }
+</style>
+```
+:::
+
+### 进度条展示
+
+:::demo
+
+```html
+<template>
+    <nut-audio
+      url="http://storage.360buyimg.com/jdcdkh/SMB/VCG231024564.wav"
+      :muted="muted"
+      :autoplay="autoplay"
+      :loop="true"
+      type="progress"
+    >
+      <div class="nut-audio-operate-group">
+        <nut-audio-operate type="back"></nut-audio-operate>
+        <nut-audio-operate type="play"></nut-audio-operate>
+        <nut-audio-operate type="forward"></nut-audio-operate>
+        <nut-audio-operate type="mute"></nut-audio-operate>
+      </div>
+    </nut-audio>
+</template>
+<script lang="ts">
+import { reactive, toRefs } from 'vue';
+export default {
+  setup() {
+    const data = reactive({
+      muted: false,
+      autoplay: false
+    });
+    return {
+      ...toRefs(data)
+    };
+  }
+};
+</script>
+```
+:::
+
+### 自定义操作按钮
+
+:::demo
+
+```html
+<template>
+    <nut-audio
+      url="http://storage.360buyimg.com/jdcdkh/SMB/VCG231024564.wav"
+      :muted="muted"
+      :autoplay="autoplay"
+      :loop="false"
+      type="progress"
+      @forward="forward"
+      @fastBack="fastBack"
+      @play="changeStatus"
+      @ended="ended"
+      @changeProgress="changeProgress"
+    >
+      <div class="nut-audio-operate-group">
+        <nut-audio-operate type="back"><nut-icon name="play-double-back" size="35"></nut-icon></nut-audio-operate>
+        <nut-audio-operate type="play"
+          ><nut-icon :name="!playing ? 'play-start' : 'play-stop'" size="35"></nut-icon
+        ></nut-audio-operate>
+        <nut-audio-operate type="forward"><nut-icon name="play-double-forward" size="35"></nut-icon></nut-audio-operate>
+      </div>
+    </nut-audio>
+</template>
+<script lang="ts">
+import { reactive, toRefs } from 'vue';
+export default {
+  setup() {
+    const data = reactive({
+      muted: false,
+      autoplay: false
+    });
+    const playing = ref(false);
+
+    const fastBack = () => {
+      console.log('倒退');
+    };
+
+    const forward = (progress) => {
+      console.log('快进', '当前时间' + progress);
+    };
+
+    const changeStatus = (status) => {
+      console.log('当前播放状态', status);
+      playing.value = status;
+    };
+
+    const ended = () => {
+      console.log('播放结束');
+    };
+
+    const changeProgress = (val) => {
+      console.log('改变进度条', val);
+    };
+    return {
+      ...toRefs(data),playing, fastBack, forward, changeStatus, audioDemo, ended, duration, changeProgress
+    };
+  }
+};
+</script>
+```
+:::
+
+## API
+
+### Props
+
+| 参数         | 说明                             | 类型   | 默认值           |
+|--------------|----------------------------------|--------|------------------|
+| url         | 语音播放的连接               | String | ''              |
+| muted        | 是否静音                         | Boolean | false             |
+| autoplay         | 是否自动播放 | Boolean | false               |
+| loop | 是否循环播放     | Boolean | false |
+| preload          | 是否预加载语音                        | String | 'auto'              |
+| type         | 展示形式,可选值:controls 控制面板   progress 进度条  icon 图标 none 自定义 | String | 'progress'              |
+
+
+### Events
+
+| 事件名 | 说明           | 回调参数     |
+|--------|----------------|--------------|
+| fastBack  | 触发语音快退 | 返回当前播放时长(单位:毫秒) |
+| forward  | 触发语音快进 | 返回当前播放时长(单位:毫秒) |
+| play  | 触发播放/暂停语音 | 返回当前播放状态 |
+| ended  | 语音播放完成,当loop设置为false时生效 | —— |
+| mute  | 触发静音 | —— |
+| changeProgress  | 当进度条改变时触发 | 返回当前播放时长(单位:毫秒) |
+
+    

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

@@ -0,0 +1,7 @@
+# Audio组件
+
+### 介绍
+
+用于音频播放
+
+#### 直接使用 Taro 现有的 Taro.createInnerAudioContext 接口开发 [参考文档](https://taro-docs.jd.com/taro/docs/apis/media/audio/createInnerAudioContext)

+ 66 - 0
src/packages/__VUE/audio/index.scss

@@ -0,0 +1,66 @@
+.nut-audio {
+  padding: 0;
+  .progress-wrapper {
+    display: flex;
+    align-items: center;
+    width: 100%;
+    margin: 0px auto;
+    padding: 10px 0;
+
+    .progress-bar-wrapper {
+      flex: 1;
+      margin: 0 10px;
+    }
+
+    .time {
+      min-width: 50px;
+      font-size: 12px;
+      text-align: center;
+    }
+  }
+
+  .nut-audio-icon {
+    position: relative;
+    display: inline-block;
+
+    .nut-audio-icon-box {
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      width: 30px;
+      height: 30px;
+      background: #fff;
+      border-radius: 50%;
+      box-shadow: 0 0 8px rgba($color: $text-color, $alpha: 0.5);
+      &.nut-audio-icon-stop {
+        &::after {
+          position: absolute;
+          left: 50%;
+          top: 50%;
+          transform: translateX(-15px);
+          content: '';
+          height: 2px;
+          width: 30px;
+          background: $disable-color;
+          transform: rotate(45deg);
+          transform-origin: 8px -18px;
+        }
+      }
+    }
+  }
+
+  .audioMain {
+    margin-top: 30px;
+  }
+
+  .custom-button {
+    width: 8px;
+    height: 8px;
+    color: #fff;
+    font-size: 10px;
+    line-height: 18px;
+    text-align: center;
+    background-color: #ee0a24;
+    border-radius: 100px;
+  }
+}

+ 302 - 0
src/packages/__VUE/audio/index.taro.vue

@@ -0,0 +1,302 @@
+<template>
+  <!-- 显示进度条 、 播放时长、 兼容是否支持 、暂停、 开启-->
+
+  <view class="nut-audio">
+    <!-- 进度条 -->
+    <view class="progress-wrapper" v-if="type == 'progress'">
+      <!-- 时间显示 -->
+      <view class="time">{{ currentDuration }}</view>
+      <view class="progress-bar-wrapper">
+        <nut-range
+          v-model="percent"
+          hidden-range
+          @change="progressChange"
+          inactive-color="#cccccc"
+          active-color="#fa2c19"
+        >
+          <template #button>
+            <view class="custom-button"></view>
+          </template>
+        </nut-range>
+      </view>
+
+      <view class="time">{{ duration }}</view>
+    </view>
+
+    <!-- 自定义 -->
+    <view class="nut-audio-icon" v-if="type == 'icon'">
+      <view
+        :class="['nut-audio-icon-box', playing ? 'nut-audio-icon-play' : 'nut-audio-icon-stop']"
+        @click="changeStatus"
+      >
+        <nut-icon v-if="playing" name="service" class="nut-icon-am-rotate nut-icon-am-infinite"></nut-icon>
+        <nut-icon v-if="!playing" name="service"></nut-icon>
+      </view>
+    </view>
+
+    <view v-if="type == 'none'" @click="changeStatus">
+      <slot></slot>
+    </view>
+
+    <!-- 操作按钮 -->
+    <template v-if="type != 'none'">
+      <slot></slot>
+    </template>
+
+    <audio
+      class="audioMain"
+      :controls="type == 'controls'"
+      ref="audioRef"
+      :src="url"
+      :preload="preload"
+      :autoplay="autoplay"
+      :loop="loop"
+      @timeupdate="onTimeupdate"
+      @ended="audioEnd"
+      :muted="hanMuted"
+    >
+    </audio>
+  </view>
+</template>
+<script lang="ts">
+import { toRefs, ref, onMounted, reactive, watch, provide } from 'vue';
+import { createComponent } from '../../utils/create';
+const { componentName, create } = createComponent('audio');
+
+export default create({
+  props: {
+    url: {
+      type: String,
+      default() {
+        return '';
+      }
+    },
+    // 静音
+    muted: {
+      type: Boolean,
+      default() {
+        return false;
+      }
+    },
+    // 自动播放
+    autoplay: {
+      type: Boolean,
+      default() {
+        return false;
+      }
+    },
+
+    // 循环播放
+    loop: {
+      type: Boolean,
+      default() {
+        return false;
+      }
+    },
+
+    // 是否预加载音频
+    preload: {
+      type: String,
+      default() {
+        return 'auto';
+      }
+    },
+    /* 总时长秒数 */
+    second: {
+      type: Number,
+      default() {
+        return 0;
+      }
+    },
+
+    // 展示的形式   controls 控制面板   progress 进度条  icon 图标 none 自定义
+    type: {
+      type: String,
+      default() {
+        return 'progress';
+      }
+    }
+  },
+  components: {},
+  emits: ['fastBack', 'play', 'forward', 'ended', 'changeProgress', 'mute'],
+
+  setup(props, { emit }) {
+    const audioRef = ref(null);
+
+    const audioData = reactive({
+      currentTime: 0,
+      currentDuration: '00:00:00',
+      percent: 0,
+      duration: '00:00:00',
+      second: 0,
+      hanMuted: props.muted,
+      playing: props.autoplay
+    });
+
+    onMounted(() => {
+      // 播放的兼容性
+      var arr = ['webkitVisibilityState', 'visibilitychange'];
+      try {
+        for (let i = 0; i < arr.length; i++) {
+          document.addEventListener(arr[i], () => {
+            if (document.hidden) {
+              // 页面被挂起
+              // 这里要根据用户当前播放状态,做音频暂停操作
+              (audioRef.value as any).pause();
+            } else {
+              // 页面呼出
+              if (audioData.playing) {
+                setTimeout(() => {
+                  // 这里要 根据页面挂起前音频的播放状态,做音频播放操作
+                  (audioRef.value as any).play();
+                }, 200);
+              }
+            }
+          });
+        }
+      } catch (e) {
+        console.log((e as any).message);
+      }
+
+      // 获取当前音频播放时长
+      setTimeout(() => {
+        // 自动播放
+        if (props.autoplay) {
+          if (audioRef.value && audioRef.value.paused) {
+            audioRef.value.play();
+          }
+        }
+        audioData.second = audioRef.value.duration;
+        audioData.duration = formatSeconds(audioRef.value.duration);
+      }, 500);
+    });
+
+    //播放时间
+    const onTimeupdate = (e) => {
+      audioData.currentTime = parseInt(e.target.currentTime);
+    };
+
+    //后退
+    const fastBack = () => {
+      audioData.currentTime--;
+      audioRef.value.currentTime = audioData.currentTime;
+
+      emit('fastBack', audioData.currentTime);
+    };
+
+    //改变播放状态
+    const changeStatus = () => {
+      if (audioData.playing) {
+        audioRef.value.pause();
+
+        audioData.handPlaying = false;
+      } else {
+        audioRef.value.play();
+        audioData.handPlaying = true;
+      }
+      audioData.playing = !audioData.playing;
+
+      emit('play', audioData.playing);
+    };
+
+    //快进
+    const forward = () => {
+      audioData.currentTime++;
+      audioRef.value.currentTime = audioData.currentTime;
+
+      emit('forward', audioData.currentTime);
+    };
+
+    //处理
+    const handle = (val) => {
+      //毫秒数转为时分秒
+      audioData.currentDuration = formatSeconds(val);
+      audioData.percent = (val / audioData.second) * 100;
+    };
+    //播放结束 修改播放状态
+    const audioEnd = () => {
+      audioData.playing = false;
+      emit('ended');
+    };
+
+    //点击进度条
+    const progressChange = (val) => {
+      audioRef.value.currentTime = (audioData.second * val) / 100;
+
+      emit('changeProgress', audioRef.value.currentTime);
+    };
+
+    // 静音
+    const handleMute = () => {
+      audioData.hanMuted = !audioData.hanMuted;
+
+      emit('mute', audioData.hanMuted);
+    };
+
+    const formatSeconds = (value) => {
+      let theTime = parseInt(value); // 秒
+      let theTime1 = 0; // 分
+      let theTime2 = 0; // 小时
+      if (theTime > 60) {
+        theTime1 = parseInt(theTime / 60);
+        theTime = parseInt(theTime % 60);
+        if (theTime1 > 60) {
+          theTime2 = parseInt(theTime1 / 60);
+          theTime1 = parseInt(theTime1 % 60);
+        }
+      }
+      let result = '' + parseInt(theTime);
+      if (result < 10) {
+        result = '0' + result;
+      }
+      if (theTime1 > 0) {
+        result = '' + parseInt(theTime1) + ':' + result;
+        if (theTime1 < 10) {
+          result = '0' + result;
+        }
+      } else {
+        result = '00:' + result;
+      }
+      if (theTime2 > 0) {
+        result = '' + parseInt(theTime2) + ':' + result;
+        if (theTime2 < 10) {
+          result = '0' + result;
+        }
+      } else {
+        result = '00:' + result;
+      }
+      return result;
+    };
+
+    watch(
+      () => audioData.currentTime,
+      (value) => {
+        handle(value);
+      }
+    );
+
+    provide('audioParent', {
+      children: [],
+      props,
+      audioData,
+      handleMute,
+      forward,
+      fastBack,
+      changeStatus
+    });
+
+    return {
+      ...toRefs(props),
+      ...toRefs(audioData),
+      audioRef,
+      fastBack,
+      forward,
+      changeStatus,
+      progressChange,
+      audioEnd,
+      onTimeupdate,
+      handleMute
+    };
+  }
+});
+</script>

+ 302 - 0
src/packages/__VUE/audio/index.vue

@@ -0,0 +1,302 @@
+<template>
+  <!-- 显示进度条 、 播放时长、 兼容是否支持 、暂停、 开启-->
+
+  <div class="nut-audio">
+    <!-- 进度条 -->
+    <div class="progress-wrapper" v-if="type == 'progress'">
+      <!-- 时间显示 -->
+      <div class="time">{{ currentDuration }}</div>
+      <div class="progress-bar-wrapper">
+        <nut-range
+          v-model="percent"
+          hidden-range
+          @change="progressChange"
+          inactive-color="#cccccc"
+          active-color="#fa2c19"
+        >
+          <template #button>
+            <div class="custom-button"></div>
+          </template>
+        </nut-range>
+      </div>
+
+      <div class="time">{{ duration }}</div>
+    </div>
+
+    <!-- 自定义 -->
+    <div class="nut-audio-icon" v-if="type == 'icon'">
+      <div
+        :class="['nut-audio-icon-box', playing ? 'nut-audio-icon-play' : 'nut-audio-icon-stop']"
+        @click="changeStatus"
+      >
+        <nut-icon v-if="playing" name="service" class="nut-icon-am-rotate nut-icon-am-infinite"></nut-icon>
+        <nut-icon v-if="!playing" name="service"></nut-icon>
+      </div>
+    </div>
+
+    <div v-if="type == 'none'" @click="changeStatus">
+      <slot></slot>
+    </div>
+
+    <!-- 操作按钮 -->
+    <template v-if="type != 'none'">
+      <slot></slot>
+    </template>
+
+    <audio
+      class="audioMain"
+      :controls="type == 'controls'"
+      ref="audioRef"
+      :src="url"
+      :preload="preload"
+      :autoplay="autoplay"
+      :loop="loop"
+      @timeupdate="onTimeupdate"
+      @ended="audioEnd"
+      :muted="hanMuted"
+    >
+    </audio>
+  </div>
+</template>
+<script lang="ts">
+import { toRefs, ref, onMounted, reactive, watch, provide } from 'vue';
+import { createComponent } from '../../utils/create';
+const { componentName, create } = createComponent('audio');
+
+export default create({
+  props: {
+    url: {
+      type: String,
+      default() {
+        return '';
+      }
+    },
+    // 静音
+    muted: {
+      type: Boolean,
+      default() {
+        return false;
+      }
+    },
+    // 自动播放
+    autoplay: {
+      type: Boolean,
+      default() {
+        return false;
+      }
+    },
+
+    // 循环播放
+    loop: {
+      type: Boolean,
+      default() {
+        return false;
+      }
+    },
+
+    // 是否预加载音频
+    preload: {
+      type: String,
+      default() {
+        return 'auto';
+      }
+    },
+    /* 总时长秒数 */
+    second: {
+      type: Number,
+      default() {
+        return 0;
+      }
+    },
+
+    // 展示的形式   controls 控制面板   progress 进度条  icon 图标 none 自定义
+    type: {
+      type: String,
+      default() {
+        return 'progress';
+      }
+    }
+  },
+  components: {},
+  emits: ['fastBack', 'play', 'forward', 'ended', 'changeProgress', 'mute'],
+
+  setup(props, { emit }) {
+    const audioRef = ref(null);
+
+    const audioData = reactive({
+      currentTime: 0,
+      currentDuration: '00:00:00',
+      percent: 0,
+      duration: '00:00:00',
+      second: 0,
+      hanMuted: props.muted,
+      playing: props.autoplay
+    });
+
+    onMounted(() => {
+      // 播放的兼容性
+      var arr = ['webkitVisibilityState', 'visibilitychange'];
+      try {
+        for (let i = 0; i < arr.length; i++) {
+          document.addEventListener(arr[i], () => {
+            if (document.hidden) {
+              // 页面被挂起
+              // 这里要根据用户当前播放状态,做音频暂停操作
+              (audioRef.value as any).pause();
+            } else {
+              // 页面呼出
+              if (audioData.playing) {
+                setTimeout(() => {
+                  // 这里要 根据页面挂起前音频的播放状态,做音频播放操作
+                  (audioRef.value as any).play();
+                }, 200);
+              }
+            }
+          });
+        }
+      } catch (e) {
+        console.log((e as any).message);
+      }
+
+      // 获取当前音频播放时长
+      setTimeout(() => {
+        // 自动播放
+        if (props.autoplay) {
+          if (audioRef.value && audioRef.value.paused) {
+            audioRef.value.play();
+          }
+        }
+        audioData.second = audioRef.value.duration;
+        audioData.duration = formatSeconds(audioRef.value.duration);
+      }, 500);
+    });
+
+    //播放时间
+    const onTimeupdate = (e) => {
+      audioData.currentTime = parseInt(e.target.currentTime);
+    };
+
+    //后退
+    const fastBack = () => {
+      audioData.currentTime--;
+      audioRef.value.currentTime = audioData.currentTime;
+
+      emit('fastBack', audioData.currentTime);
+    };
+
+    //改变播放状态
+    const changeStatus = () => {
+      if (audioData.playing) {
+        audioRef.value.pause();
+
+        audioData.handPlaying = false;
+      } else {
+        audioRef.value.play();
+        audioData.handPlaying = true;
+      }
+      audioData.playing = !audioData.playing;
+
+      emit('play', audioData.playing);
+    };
+
+    //快进
+    const forward = () => {
+      audioData.currentTime++;
+      audioRef.value.currentTime = audioData.currentTime;
+
+      emit('forward', audioData.currentTime);
+    };
+
+    //处理
+    const handle = (val) => {
+      //毫秒数转为时分秒
+      audioData.currentDuration = formatSeconds(val);
+      audioData.percent = (val / audioData.second) * 100;
+    };
+    //播放结束 修改播放状态
+    const audioEnd = () => {
+      audioData.playing = false;
+      emit('ended');
+    };
+
+    //点击进度条
+    const progressChange = (val) => {
+      audioRef.value.currentTime = (audioData.second * val) / 100;
+
+      emit('changeProgress', audioRef.value.currentTime);
+    };
+
+    // 静音
+    const handleMute = () => {
+      audioData.hanMuted = !audioData.hanMuted;
+
+      emit('mute', audioData.hanMuted);
+    };
+
+    const formatSeconds = (value) => {
+      let theTime = parseInt(value); // 秒
+      let theTime1 = 0; // 分
+      let theTime2 = 0; // 小时
+      if (theTime > 60) {
+        theTime1 = parseInt(theTime / 60);
+        theTime = parseInt(theTime % 60);
+        if (theTime1 > 60) {
+          theTime2 = parseInt(theTime1 / 60);
+          theTime1 = parseInt(theTime1 % 60);
+        }
+      }
+      let result = '' + parseInt(theTime);
+      if (result < 10) {
+        result = '0' + result;
+      }
+      if (theTime1 > 0) {
+        result = '' + parseInt(theTime1) + ':' + result;
+        if (theTime1 < 10) {
+          result = '0' + result;
+        }
+      } else {
+        result = '00:' + result;
+      }
+      if (theTime2 > 0) {
+        result = '' + parseInt(theTime2) + ':' + result;
+        if (theTime2 < 10) {
+          result = '0' + result;
+        }
+      } else {
+        result = '00:' + result;
+      }
+      return result;
+    };
+
+    watch(
+      () => audioData.currentTime,
+      (value) => {
+        handle(value);
+      }
+    );
+
+    provide('audioParent', {
+      children: [],
+      props,
+      audioData,
+      handleMute,
+      forward,
+      fastBack,
+      changeStatus
+    });
+
+    return {
+      ...toRefs(props),
+      ...toRefs(audioData),
+      audioRef,
+      fastBack,
+      forward,
+      changeStatus,
+      progressChange,
+      audioEnd,
+      onTimeupdate,
+      handleMute
+    };
+  }
+});
+</script>

+ 32 - 0
src/packages/__VUE/audiooperate/doc.md

@@ -0,0 +1,32 @@
+# audiooperate组件
+
+    ### 介绍
+    
+    基于 xxxxxxx
+    
+    ### 安装
+    
+    
+    
+    ### 基础用法
+    
+
+    
+    ## API
+    
+    ### Props
+    
+    | 参数         | 说明                             | 类型   | 默认值           |
+    |--------------|----------------------------------|--------|------------------|
+    | name         | 图标名称或图片链接               | String | -                |
+    | color        | 图标颜色                         | String | -                |
+    | size         | 图标大小,如 '20px' '2em' '2rem' | String | -                |
+    | class-prefix | 类名前缀,用于使用自定义图标     | String | 'nutui-iconfont' |
+    | tag          | HTML 标签                        | String | 'i'              |
+    
+    ### Events
+    
+    | 事件名 | 说明           | 回调参数     |
+    |--------|----------------|--------------|
+    | click  | 点击图标时触发 | event: Event |
+    

+ 13 - 0
src/packages/__VUE/audiooperate/index.scss

@@ -0,0 +1,13 @@
+.nut-audio-operate-group {
+  display: flex;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  margin-top: 10px;
+
+  .nut-audio-operate {
+    .nut-audio-operate-item {
+      margin: 0 5px;
+    }
+  }
+}

+ 48 - 0
src/packages/__VUE/audiooperate/index.vue

@@ -0,0 +1,48 @@
+<template>
+  <!--配合进度条使用 播放时长、 兼容是否支持 、暂停、 开启-->
+  <div class="nut-audio-operate">
+    <div class="nut-audio-operate-item" @click="fastBack" v-if="type == 'back'"
+      ><nut-button type="primary" size="small" v-if="!customSlot">倒退</nut-button><slot></slot
+    ></div>
+    <div class="nut-audio-operate-item" @click="changeStatus" v-if="type == 'play'"
+      ><nut-button type="primary" size="small" v-if="!customSlot">{{
+        !audioData.playing ? '开始' : '暂停'
+      }}</nut-button>
+      <slot></slot
+    ></div>
+    <div class="nut-audio-operate-item" @click="forward" v-if="type == 'forward'"
+      ><nut-button type="primary" size="small" v-if="!customSlot">快进</nut-button><slot></slot
+    ></div>
+    <div class="nut-audio-operate-item" @click="handleMute" v-if="type == 'mute'"
+      ><nut-button :type="!audioData.hanMuted ? 'primary' : 'default'" size="small" v-if="!customSlot">静音</nut-button
+      ><slot></slot
+    ></div>
+  </div>
+</template>
+<script lang="ts">
+import { toRefs, ref, useSlots, onMounted, reactive, inject } from 'vue';
+import { createComponent } from '../../utils/create';
+const { componentName, create } = createComponent('audio-operate');
+
+export default create({
+  props: {
+    // 展示的形式   back 倒退   play 开始 or 暂停  forward 快进 mute 静音
+    type: {
+      type: Array,
+      default() {
+        return 'play';
+      }
+    }
+  },
+  components: {},
+  emits: ['click'],
+
+  setup(props, { emit }) {
+    const audio: any = inject('audioParent');
+    const parent: any = reactive(audio);
+    const customSlot = ref(useSlots().default);
+
+    return { ...toRefs(props), ...toRefs(parent), customSlot };
+  }
+});
+</script>

+ 11 - 0
src/packages/__VUE/drag/__test__/index.spec.ts

@@ -0,0 +1,11 @@
+import { mount } from '@vue/test-utils';
+import Drag from '../index.vue';
+
+test('should render default slot correctly', () => {
+  const wrapper = mount(Drag, {
+    slots: {
+      default: () => 'Custom Message'
+    }
+  });
+  expect(wrapper.find('.nut-drag').html()).toMatchSnapshot();
+});

+ 1 - 1
src/packages/__VUE/drag/doc.md

@@ -85,9 +85,9 @@ app.use(Drag);
   border: 1px solid red;
 }
 </style>
-:::
 
 ```
+:::
 ## Prop
 
 | 字段      | 说明                                              | 类型           | 默认值                              |

+ 12 - 2
src/packages/__VUE/input/__tests__/input.spec.ts

@@ -1,6 +1,16 @@
-import { mount } from '@vue/test-utils';
+import { config, mount } from '@vue/test-utils';
 import Input from '../index.vue';
-import Icon from '../../icon/index.vue';
+import NutIcon from '../../icon/index.vue';
+
+beforeAll(() => {
+  config.global.components = {
+    NutIcon
+  };
+});
+
+afterAll(() => {
+  config.global.components = {};
+});
 
 test('base', () => {
   const wrapper = mount(Input, { props: { modelValue: 3 } });

+ 108 - 0
src/packages/__VUE/shortpassword/__tests__/index.spec.ts

@@ -0,0 +1,108 @@
+import { config, DOMWrapper, mount } from '@vue/test-utils';
+import ShortPassword from '../index.vue';
+import { nextTick } from 'vue';
+import NutIcon from '../../icon/index.vue';
+import NutPopup from '../../icon/index.vue';
+
+beforeAll(() => {
+  config.global.components = {
+    NutIcon,
+    NutPopup
+  };
+});
+
+afterAll(() => {
+  config.global.components = {};
+});
+
+test('should render shortpassword when visible is true', async () => {
+  const wrapper = mount(ShortPassword, {
+    props: {
+      visible: true,
+      modelValue: '123'
+    }
+  });
+  const input: DOMWrapper<Element> = wrapper.find('.nut-input-real');
+  expect(input.exists()).toBe(true);
+  expect(input.attributes('maxlength')).toBe('6');
+  const psdLength = wrapper.findAll('.nut-shortpsd-li');
+  expect(psdLength.length).toBe(6);
+  expect((input.element as HTMLInputElement).value).toBe('123');
+});
+test('should render buttonShortpassword and error msg when noButton is false ', () => {
+  const wrapper = mount(ShortPassword, {
+    props: {
+      visible: true,
+      errorMsg: '错误信息',
+      length: 4,
+      modelValue: '123',
+      noButton: false
+    }
+  });
+  const input = wrapper.find('.nut-input-real');
+  expect(input.exists()).toBe(true);
+  const psdLength = wrapper.findAll('.nut-shortpsd-li');
+  expect(psdLength.length).toBe(4);
+  const error = wrapper.find('.nut-shortpsd-error');
+  expect(error.exists()).toBe(true);
+  expect(error.text()).toBe('错误信息');
+  const cancle = wrapper.find('.nut-shortpsd-cancle');
+  expect(cancle.exists()).toBe(true);
+  const sure = wrapper.find('.nut-shortpsd-sure');
+  expect(sure.exists()).toBe(true);
+  cancle.trigger('click');
+  expect(wrapper.emitted('cancel')).toBeTruthy();
+  sure.trigger('click');
+  expect((wrapper.emitted('ok') as any)[0][0]).toBe('123');
+});
+
+test('should allow to format value with formatter prop', async () => {
+  const wrapper = mount(ShortPassword, {
+    props: {
+      visible: true,
+      modelValue: '777'
+    }
+  });
+  expect((wrapper.emitted('update:modelValue') as any)[0][0]).toEqual('777');
+  await wrapper.setProps({ modelValue: '456' });
+  expect((wrapper.emitted('update:modelValue') as any)[1][0]).toEqual('456');
+});
+
+test('should format input value when input', async () => {
+  const wrapper = mount(ShortPassword, {
+    props: {
+      visible: true,
+      modelValue: ''
+    }
+  });
+  const div: DOMWrapper<Element> = wrapper.find('.nut-shortpsd-fake');
+  const input: DOMWrapper<Element> = wrapper.find('.nut-input-real');
+  (input.element as HTMLInputElement).value = '123';
+  div.trigger('click');
+  input.trigger('input');
+  await nextTick();
+  expect((wrapper.emitted('update:modelValue') as any)[0][0]).toEqual('');
+  expect((wrapper.emitted('update:modelValue') as any)[1][0]).toEqual('123');
+  (input.element as HTMLInputElement).value = '456';
+  input.trigger('input');
+  expect((wrapper.emitted('update:modelValue') as any)[2][0]).toEqual('456');
+  (input.element as HTMLInputElement).value = 'abc';
+  input.trigger('input');
+  expect((wrapper.emitted('update:modelValue') as any)[3][0]).toEqual('');
+});
+
+test('should output input value when finish', async () => {
+  const wrapper = mount(ShortPassword, {
+    props: {
+      visible: true,
+      modelValue: ''
+    }
+  });
+  const div: DOMWrapper<Element> = wrapper.find('.nut-shortpsd-fake');
+  const input: DOMWrapper<Element> = wrapper.find('.nut-input-real');
+  (input.element as HTMLInputElement).value = '321123';
+  div.trigger('click');
+  input.trigger('input');
+  await nextTick();
+  expect((wrapper.emitted('complete') as any)[0][0]).toEqual('321123');
+});

+ 0 - 79
src/packages/__VUE/shortpassword/__tests__/shortpassword.spec.ts

@@ -1,79 +0,0 @@
-import { mount } from '@vue/test-utils';
-import ShortPassword from '../index.vue';
-import Icon from '../../icon/index.vue';
-
-test('base', () => {
-  const wrapper = mount(ShortPassword, {
-    props: {
-      visible: true,
-      value: 123
-    }
-  });
-  const input: any = wrapper.find('.nut-input-real');
-  expect(input.exists()).toBe(true);
-  expect(input.attributes('maxlength')).toBe('6');
-  //expect(input.element.value).toBe('123');
-  //代码还没处理modelvalue值
-});
-test('base length and error', () => {
-  const wrapper = mount(ShortPassword, {
-    props: {
-      visible: true,
-      length: 4,
-      errorMsg: '错误信息',
-      noButton: false,
-      value: 'qwe'
-    }
-  });
-  const input = wrapper.find('.nut-input-real');
-  expect(input.exists()).toBe(true);
-  // expect(input.attributes("maxlength")).toBe("4");
-  const error = wrapper.find('.nut-shortpsd-error');
-  expect(error.exists()).toBe(true);
-  expect(error.text()).toBe('错误信息');
-  const cancle = wrapper.find('.nut-shortpsd-cancle');
-  expect(cancle.exists()).toBe(true);
-  const sure = wrapper.find('.nut-shortpsd-sure');
-  expect(sure.exists()).toBe(true);
-  cancle.trigger('click');
-  expect(wrapper.emitted('cancel')).toBeTruthy();
-  sure.trigger('click');
-  // expect((wrapper.emitted('ok') as any)[0][0]).toBe('qwe');
-});
-
-test('event callback', async () => {
-  const wrapper = mount(ShortPassword, {
-    props: {
-      visible: true
-    }
-  });
-  const input: any = wrapper.find('.nut-input-real');
-  await input.trigger('click');
-  await input.trigger('input');
-  await wrapper.trigger('keydown', {
-    key: '123'
-  });
-  setTimeout(() => {
-    expect(wrapper.emitted('change') as any).toBe('123');
-  }, 400);
-
-  //代码还没处理modelvalue值
-});
-test('event callback', async () => {
-  const wrapper = mount(ShortPassword, {
-    props: {
-      visible: true
-    }
-  });
-  const input: any = wrapper.find('.nut-input-real');
-  await input.trigger('click');
-  await input.trigger('input');
-  await wrapper.trigger('keydown', {
-    key: '123456'
-  });
-  setTimeout(() => {
-    expect(wrapper.emitted('complete') as any).toBe('123456');
-  }, 400);
-
-  //代码还没处理modelvalue值
-});

+ 5 - 1
src/packages/__VUE/shortpassword/index.vue

@@ -112,7 +112,11 @@ export default create({
       () => props.modelValue,
       (value) => {
         realInput.value = value;
-        // console.log('watch', value);
+        emit('update:modelValue', value);
+      },
+      {
+        deep: true,
+        immediate: true
       }
     );
     function changeValue(e: Event) {

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

@@ -68,7 +68,12 @@
         "horizontal",
         "date",
         "photograph",
-        "more-s"
+        "more-s",
+        "play-stop",
+        "play-start",
+        "play-double-back",
+        "play-double-forward",
+        "voice"
       ]
     },
     {

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

@@ -55,6 +55,36 @@
           <ul class="icon_lists dib-box">
           
             <li class="dib">
+              <span class="icon nutui-iconfont">&#xe608;</span>
+                <div class="name">voice</div>
+                <div class="code-name">&amp;#xe608;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon nutui-iconfont">&#xe604;</span>
+                <div class="name">play-stop</div>
+                <div class="code-name">&amp;#xe604;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon nutui-iconfont">&#xe605;</span>
+                <div class="name">play-start</div>
+                <div class="code-name">&amp;#xe605;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon nutui-iconfont">&#xe606;</span>
+                <div class="name">play-double-back</div>
+                <div class="code-name">&amp;#xe606;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon nutui-iconfont">&#xe607;</span>
+                <div class="name">play-double-forward</div>
+                <div class="code-name">&amp;#xe607;</div>
+              </li>
+          
+            <li class="dib">
               <span class="icon nutui-iconfont">&#xe603;</span>
                 <div class="name">dou-arrow-up</div>
                 <div class="code-name">&amp;#xe603;</div>
@@ -744,9 +774,9 @@
 <pre><code class="language-css"
 >@font-face {
   font-family: 'nutui-iconfont';
-  src: url('iconfont.woff2?t=1642470100417') format('woff2'),
-       url('iconfont.woff?t=1642470100417') format('woff'),
-       url('iconfont.ttf?t=1642470100417') format('truetype');
+  src: url('iconfont.woff2?t=1644572435352') format('woff2'),
+       url('iconfont.woff?t=1644572435352') format('woff'),
+       url('iconfont.ttf?t=1644572435352') format('truetype');
 }
 </code></pre>
           <h3 id="-iconfont-">第二步:定义使用 iconfont 的样式</h3>
@@ -773,6 +803,51 @@
         <ul class="icon_lists dib-box">
           
           <li class="dib">
+            <span class="icon nutui-iconfont nut-icon-voice"></span>
+            <div class="name">
+              voice
+            </div>
+            <div class="code-name">.nut-icon-voice
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon nutui-iconfont nut-icon-play-stop"></span>
+            <div class="name">
+              play-stop
+            </div>
+            <div class="code-name">.nut-icon-play-stop
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon nutui-iconfont nut-icon-play-start"></span>
+            <div class="name">
+              play-start
+            </div>
+            <div class="code-name">.nut-icon-play-start
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon nutui-iconfont nut-icon-play-double-back"></span>
+            <div class="name">
+              play-double-back
+            </div>
+            <div class="code-name">.nut-icon-play-double-back
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon nutui-iconfont nut-icon-play-double-forward"></span>
+            <div class="name">
+              play-double-forward
+            </div>
+            <div class="code-name">.nut-icon-play-double-forward
+            </div>
+          </li>
+          
+          <li class="dib">
             <span class="icon nutui-iconfont nut-icon-dou-arrow-up"></span>
             <div class="name">
               dou-arrow-up
@@ -1809,6 +1884,46 @@
           
             <li class="dib">
                 <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#nut-icon-voice"></use>
+                </svg>
+                <div class="name">voice</div>
+                <div class="code-name">#nut-icon-voice</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#nut-icon-play-stop"></use>
+                </svg>
+                <div class="name">play-stop</div>
+                <div class="code-name">#nut-icon-play-stop</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#nut-icon-play-start"></use>
+                </svg>
+                <div class="name">play-start</div>
+                <div class="code-name">#nut-icon-play-start</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#nut-icon-play-double-back"></use>
+                </svg>
+                <div class="name">play-double-back</div>
+                <div class="code-name">#nut-icon-play-double-back</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#nut-icon-play-double-forward"></use>
+                </svg>
+                <div class="name">play-double-forward</div>
+                <div class="code-name">#nut-icon-play-double-forward</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
                   <use xlink:href="#nut-icon-dou-arrow-up"></use>
                 </svg>
                 <div class="name">dou-arrow-up</div>

+ 23 - 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=1642470100417') format('woff2'),
-       url('iconfont.woff?t=1642470100417') format('woff'),
-       url('iconfont.ttf?t=1642470100417') format('truetype');
+  src: url('iconfont.woff2?t=1644572435352') format('woff2'),
+       url('iconfont.woff?t=1644572435352') format('woff'),
+       url('iconfont.ttf?t=1644572435352') format('truetype');
 }
 
 .nutui-iconfont {
@@ -13,6 +13,26 @@
   -moz-osx-font-smoothing: grayscale;
 }
 
+.nut-icon-voice:before {
+  content: "\e608";
+}
+
+.nut-icon-play-stop:before {
+  content: "\e604";
+}
+
+.nut-icon-play-start:before {
+  content: "\e605";
+}
+
+.nut-icon-play-double-back:before {
+  content: "\e606";
+}
+
+.nut-icon-play-double-forward:before {
+  content: "\e607";
+}
+
 .nut-icon-dou-arrow-up:before {
   content: "\e603";
 }

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


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

@@ -6,6 +6,41 @@
   "description": "nutui 3.0字体管理",
   "glyphs": [
     {
+      "icon_id": "27579944",
+      "name": "voice",
+      "font_class": "voice",
+      "unicode": "e608",
+      "unicode_decimal": 58888
+    },
+    {
+      "icon_id": "27574732",
+      "name": "play-stop",
+      "font_class": "play-stop",
+      "unicode": "e604",
+      "unicode_decimal": 58884
+    },
+    {
+      "icon_id": "27574733",
+      "name": "play-start",
+      "font_class": "play-start",
+      "unicode": "e605",
+      "unicode_decimal": 58885
+    },
+    {
+      "icon_id": "27574749",
+      "name": "play-double-back",
+      "font_class": "play-double-back",
+      "unicode": "e606",
+      "unicode_decimal": 58886
+    },
+    {
+      "icon_id": "27574750",
+      "name": "play-double-forward",
+      "font_class": "play-double-forward",
+      "unicode": "e607",
+      "unicode_decimal": 58887
+    },
+    {
       "icon_id": "27276580",
       "name": "dou-arrow-up",
       "font_class": "dou-arrow-up",

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


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


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


+ 39 - 0
src/sites/doc/components/demo-block/demoBlock.vue

@@ -2,6 +2,13 @@
   <div class="online-code" ref="onlineCode">
     <slot></slot>
     <div class="online-part">
+      <a class="list" :href="jumpHref1" target="_blank">
+        <img
+          class="online-icon"
+          src="https://img13.360buyimg.com/imagetools/jfs/t1/125518/28/24027/2723/6204ae85E8bf8b7e9/af2d55aabeb6bbb6.png"
+        />
+        <div class="online-tips">codesandbox</div>
+      </a>
       <a class="list" :href="jumpHref" target="_blank">
         <img
           class="online-icon"
@@ -22,6 +29,10 @@
 <script>
 import { ref, getCurrentInstance, onMounted, computed } from 'vue';
 import { compressText, copyCodeHtml, decompressText } from './basedUtil';
+
+import { getParameters } from 'codesandbox/lib/api/define';
+import codesandboxPackage from './demoCodePackage.json'; // 引入josn文件
+
 export default {
   setup(props, ctx) {
     const sourceMainReactJsStr = `//import VConsole from "vconsole";
@@ -44,6 +55,12 @@ import "./app.scss";
 import "@nutui/nutui/dist/style.css";
 createApp(App).use(NutUI).mount("#app");`;
 
+    const MainJsStr = `import { createApp } from "vue";
+import App from "./App.vue";
+import NutUI from "@nutui/nutui";
+import "@nutui/nutui/dist/style.css";
+createApp(App).use(NutUI).mount("#app");`;
+
     const onlineCode = ref(null);
     const sourceMainJs = compressText(sourceMainJsStr);
     const mainJs = ref(sourceMainJs);
@@ -52,10 +69,31 @@ createApp(App).use(NutUI).mount("#app");`;
     const mainReactJs = ref(sourceMainReactJs);
 
     const jumpHref = ref(``);
+    const jumpHref1 = ref(``);
     onMounted(() => {
+      console.log('codesandboxPackage', codesandboxPackage);
+      console.log('onlineCode', onlineCode.value.dataset);
+      const sourceValue = decompressText(onlineCode.value.dataset.value);
+      console.log('sourceValue', sourceValue);
+
+      const parameters = getParameters({
+        files: {
+          'package.json': {
+            content: codesandboxPackage
+          },
+          'src/main.js': {
+            content: MainJsStr
+          },
+          'src/App.vue': {
+            content: sourceValue
+          }
+        }
+      });
+
       if (onlineCode.value.dataset.type === 'react') {
         jumpHref.value = `https://codehouse.jd.com/?source=share&type=react&mainJs=${mainReactJs.value}&appValue=${onlineCode.value.dataset.value}&scssValue=`;
       } else {
+        jumpHref1.value = `https://codesandbox.io/api/v1/sandboxes/define?parameters=${parameters}`;
         jumpHref.value = `https://codehouse.jd.com/?source=share&type=vue&mainJs=${mainJs.value}&appValue=${onlineCode.value.dataset.value}&scssValue=`;
       }
     });
@@ -67,6 +105,7 @@ createApp(App).use(NutUI).mount("#app");`;
     };
     return {
       jumpHref,
+      jumpHref1,
       onlineCode,
       copyCode
     };

+ 47 - 0
src/sites/doc/components/demo-block/demoCodePackage.json

@@ -0,0 +1,47 @@
+{
+  "name": "nutui3-demo",
+  "version": "0.1.0",
+  "private": true,
+  "scripts": {
+    "serve": "vue-cli-service serve",
+    "build": "vue-cli-service build",
+    "lint": "vue-cli-service lint"
+  },
+  "dependencies": {
+    "@nutui/nutui": "3.1.16",
+    "@vue/babel-plugin-jsx": "1.1.1",
+    "core-js": "^3.6.5",
+    "typescript": "4.5.5",
+    "vue": "^3.0.0-0"
+  },
+  "devDependencies": {
+    "@vue/cli-plugin-babel": "~4.5.0",
+    "@vue/cli-plugin-eslint": "~4.5.0",
+    "@vue/cli-service": "~4.5.0",
+    "@vue/compiler-sfc": "^3.0.0-0",
+    "babel-eslint": "^10.1.0",
+    "eslint": "^6.7.2",
+    "eslint-plugin-vue": "^7.0.0-0"
+  },
+  "eslintConfig": {
+    "root": true,
+    "env": {
+      "node": true
+    },
+    "extends": [
+      "plugin:vue/vue3-essential",
+      "eslint:recommended"
+    ],
+    "parserOptions": {
+      "parser": "babel-eslint"
+    },
+    "rules": {}
+  },
+  "browserslist": [
+    "> 1%",
+    "last 2 versions",
+    "not dead"
+  ],
+  "keywords": [],
+  "description": ""
+}