Browse Source

feat: 优化

ailululu 5 years ago
parent
commit
52716220b7

+ 319 - 0
jd/createComponentMode.js

@@ -0,0 +1,319 @@
+// 创建模板
+
+const inquirer = require('inquirer');
+// import {  ROOT_PACKAGE_PATH } from '../util/dic';
+//const conf = require(ROOT_PACKAGE_PATH('src/config.json'));
+//import {a} from "./a.js"
+// import  {nav}  from '../src/config.js';
+// import { from } from 'core-js/fn/array';
+const path = require('path');
+const fs = require('fs');
+// const copy = require('copy');
+//const createPkgDeclare = require('./createPkgDeclare');
+// const t = require('@babel/types');
+// const { parse } = require('@babel/parser');
+// const { default: traverse } = require('@babel/traverse');
+// const { default: generate } = require('@babel/generator');
+
+var newCpt = {
+  version: '1.0.0',
+  name: '',
+  type: '',
+  chnName: '',
+  desc: '',
+  sort: '',
+  showDemo: true,
+  author: '',
+  star: undefined
+};
+function init() {
+  inquirer
+    .prompt([
+      {
+        type: 'input',
+        name: 'name',
+        message: '组件英文名(每个单词的首字母都大写,如TextBox):',
+        validate(value) {
+          //   let repeat = false;
+          //   for (var i = 0; i < nav.length; i++) {
+          //     for (var j = 0; j < nav[i].packages.length; j++) {
+          //       if (nav[i].packages[j].name === value) {
+          //         repeat = true;
+          //       }
+          //     }
+          //   }
+
+          //   if (repeat) {
+          //     return '该组件名已存在!';
+          //   }
+          const pass = value && value.match(/^[A-Z]/);
+          if (pass) {
+            return true;
+          }
+          return '不能为空,且每个单词的首字母都要大写,如TextBox';
+        }
+      },
+      {
+        type: 'input',
+        name: 'chnName',
+        message: '组件中文名(十个字以内):',
+        validate(value) {
+          const pass = value && value.length <= 10;
+          if (pass) {
+            return true;
+          }
+          return '不能为空,且不能超过十个字符';
+        }
+      },
+      {
+        type: 'input',
+        name: 'desc',
+        message: '组件描述(五十个字以内):'
+      },
+      {
+        type: 'rawlist',
+        name: 'type',
+        message: '请选择组件类型(输入编号):目前只支持组建模板',
+        choices: ['component', 'filter', 'directive', 'method'],
+        validate(value) {
+          const pass = value && /^[1-4]$/.test(value);
+          if (pass) {
+            return true;
+          }
+          return '输入有误!请输入选项前编号';
+        }
+      },
+      //   {
+      //     type: 'rawlist',
+      //     name: 'sort',
+      //     message: '请选择组件分类(输入编号):',
+      //     // choices: sorts,
+      //     validate(value) {
+      //       const pass = /^[1-7]$/.test(value);
+      //       if (pass) {
+      //         return true;
+      //       }
+      //       return '输入有误!请输入选项前编号';
+      //     }
+      //   },
+      //   {
+      //     type: 'confirm',
+      //     name: 'showDemo',
+      //     message: '是否需要DEMO页面?',
+      //     default: true
+      //   },
+      //   {
+      //     type: 'confirm',
+      //     name: 'showTest',
+      //     message: '是否需要单元测试页面?',
+      //     default: true
+      //   },
+      {
+        type: 'input',
+        name: 'author',
+        message: '组件作者(可署化名):'
+      }
+    ])
+    .then(function(answers) {
+      // answers.sort = String(sorts.indexOf(answers.sort));
+      newCpt = Object.assign(newCpt, answers);
+      createNew();
+    });
+}
+function createIndexJs() {
+  const nameLc = newCpt.name.toLowerCase();
+  const destPath = path.join('src/packages/' + nameLc);
+  if (!fs.existsSync(destPath)) {
+    fs.mkdirSync(destPath);
+  }
+  // copy(path.join(__dirname, './__template__/**.*'), destPath, function (err: any, file: any) {
+  // 	if (err) {
+  // 		console.log('拷贝__template__目录失败!');
+  // 	}
+  // 	createNew();
+  // });
+
+  if (newCpt.type == 'method') return;
+  return new Promise((resolve, reject) => {
+    //         let content = `import ${newCpt.name} from './src/${nameLc}.vue';
+    // ${newCpt.name}.install = function(Vue) {
+    //   Vue.${newCpt.type}(${newCpt.name}.name, ${newCpt.name});
+    // };
+    // export default ${newCpt.name}`;
+    //         let content2 = `${newCpt.name}.install = function(Vue) {
+    // Vue.${newCpt.type}(${newCpt.name}.name, ${newCpt.name});
+    // };
+    // export default ${newCpt.name}`;
+
+    // const dirPath = path.join(__dirname, `../src/packages/${nameLc}/`);
+
+    // const filePath = path.join(dirPath, `index.js`);
+    // if (!fs.existsSync(dirPath)) {
+    // 	fs.mkdirSync(filePath);
+    // }
+    // if (newCpt.type == 'filter' || newCpt.type == 'directive'){
+    //     content = content2;
+    // }
+    // fs.writeFile(filePath,  content, (err) => {
+    //     if (err) throw err;
+    resolve(`生成index.js文件成功`);
+    // });
+  });
+}
+
+function createVue() {
+  return new Promise((resolve, reject) => {
+    const nameLc = newCpt.name.toLowerCase();
+    let content = `<template>
+		<view :class="classes" @click="handleClick">
+		  <view>{{ name }}</view>
+		  <view>{{ txt }}</view>
+		</view>
+	  </template>
+	  <script lang="ts">
+	  import { toRefs } from 'vue';
+	  import { createComponent } from '@/utils/create';
+	  const { componentName, create } = createComponent('temp');
+	  
+	  export default create({
+		props: {
+		  name: {
+			type: String,
+			default: ''
+		  },
+		  txt: {
+			type: String,
+			default: ''
+		  }
+		},
+		components: {},
+		emits: ['click'],
+	  
+		setup(props, { emit }) {
+		  console.log('componentName', componentName);
+	  
+		  const { name, txt } = toRefs(props);
+	  
+		  const handleClick = (event: Event) => {
+			emit('click', event);
+		  };
+	  
+		  return { name, txt, handleClick };
+		}
+	  });
+	  </script>
+	  
+	  <style lang="scss">
+	  @import 'index.scss';
+	  </style>
+	  `;
+    const dirPath = path.join(__dirname, `../src/packages/${nameLc}/`);
+    const filePath = path.join(dirPath, `index.vue`);
+    if (!fs.existsSync(dirPath)) {
+      fs.mkdirSync(filePath);
+    }
+    fs.writeFile(filePath, content, err => {
+      if (err) throw err;
+      resolve(`生成${newCpt.name}.vue文件成功`);
+    });
+  });
+}
+
+function createDemo() {
+  return new Promise((resolve, reject) => {
+    const nameLc = newCpt.name.toLowerCase();
+    let content = `<template>
+    <div class="demo-list"></div>
+</template>
+<script>
+export default {
+    data() {
+        return {};
+    },
+    methods: {
+    }
+}
+</script>`;
+    const dirPath = path.join(__dirname, '../src/packages/' + nameLc);
+    const filePath = path.join(dirPath, `demo.vue`);
+    if (!fs.existsSync(dirPath)) {
+      fs.mkdirSync(filePath);
+    }
+    fs.writeFile(filePath, content, err => {
+      if (err) throw err;
+      resolve(`生成demo.vue文件成功`);
+    });
+  });
+}
+
+function addToPackageJson() {
+  // return new Promise((resolve, reject) => {
+  //     conf.packages.push(newCpt);
+  //     const dirPath = path.join(__dirname, `../`);
+  //     const filePath = path.join(dirPath, `config.json`);
+  //     fs.writeFile(filePath, JSON.stringify(conf, null, 2), (err) => {
+  //         if (err) throw err;
+  //         resolve(`修改config.json文件成功`);
+  //     });
+  // });
+}
+function createScss() {
+  return new Promise((resolve, reject) => {
+    const nameLc = newCpt.name.toLowerCase();
+    let content = `.nut-temp {}`;
+    const dirPath = path.join(__dirname, '../src/packages/' + nameLc);
+    const filePath = path.join(dirPath, `index.scss`);
+    if (!fs.existsSync(dirPath)) {
+      fs.mkdirSync(filePath);
+    }
+    fs.writeFile(filePath, content, err => {
+      if (err) throw err;
+      resolve(`index.scss文件成功`);
+    });
+  });
+}
+function createDoc() {
+  return new Promise((resolve, reject) => {
+    const nameLc = newCpt.name.toLowerCase();
+    let content = `组建使用说明文件`;
+    const dirPath = path.join(__dirname, '../src/packages/' + nameLc);
+    const filePath = path.join(dirPath, `doc.md`);
+    if (!fs.existsSync(dirPath)) {
+      fs.mkdirSync(filePath);
+    }
+    fs.writeFile(filePath, content, err => {
+      if (err) throw err;
+      resolve(`doc.md文件成功`);
+    });
+  });
+}
+function createNew() {
+  createIndexJs()
+    .then(() => {
+      if (newCpt.type == 'component' || newCpt.type == 'method') {
+        return createVue();
+      } else {
+        return;
+      }
+    })
+    .then(() => {
+      return createScss();
+    })
+    .then(() => {
+      return createDemo();
+    })
+    .then(() => {
+      return createDoc();
+    })
+    .then(() => {
+      // return addToPackageJson();
+    })
+    .then(() => {
+      console.log('组件模板生成完毕,请开始你的表演~');
+      process.exit();
+    });
+}
+function createComponent() {
+  init();
+}
+createComponent();

File diff suppressed because it is too large
+ 14944 - 0
package-lock.json


+ 4 - 1
package.json

@@ -35,12 +35,15 @@
     "serve": "vue-cli-service serve",
     "build": "vue-cli-service build",
     "lint": "vue-cli-service lint",
-    "upload": "vue-cli-service build && node ./jd/upload.js"
+    "upload": "vue-cli-service build && node ./jd/upload.js",
+    "add": "node jd/createComponentMode.js"
   },
   "dependencies": {
+    "@types/swiper": "^5.4.1",
     "core-js": "^3.6.5",
     "sass": "^1.27.0",
     "sass-loader": "^10.0.4",
+    "swiper": "^6.3.5",
     "vue": "^3.0.0",
     "vue-router": "^4.0.0-rc.1"
   },

+ 116 - 0
src/config.js

@@ -0,0 +1,116 @@
+export const versions = [
+  { name: '1.x', link: '/1x/' },
+  { name: '2.x', link: '/' },
+  { name: '3.x', link: '/3x/' }
+];
+
+export const docs = {
+  name: '指南',
+  packages: [
+    {
+      name: 'intro',
+      cName: '介绍',
+      show: true
+    },
+    {
+      name: 'start',
+      cName: '快速上手',
+      show: true
+    },
+    {
+      name: 'theme',
+      cName: '主题定制',
+      show: true
+    },
+    {
+      name: 'international',
+      cName: '国际化',
+      show: true
+    },
+    {
+      name: 'https://github.com/jdf2e/nutui/releases',
+      cName: '更新日志',
+      show: true,
+      isLink: true
+    }
+  ]
+};
+
+export const nav = [
+  {
+    name: '布局组件',
+    packages: [
+      {
+        name: 'Button',
+        sort: 1,
+        cName: '按钮组件',
+        type: 'component',
+        show: true,
+        desc: '按钮用于触发一个操作,如提交表单。',
+        author: 'richard1015'
+      }
+    ]
+  },
+  {
+    name: '操作反馈',
+    packages: []
+  },
+  {
+    name: '基础组件',
+    packages: [
+      {
+        name: 'Temp',
+        sort: 1,
+        cName: '模板组件',
+        type: 'component',
+        show: true,
+        desc: '组件模板示例',
+        author: 'richard1015'
+      },
+      {
+        name: 'Cell',
+        sort: 1,
+        cName: '单元格组件',
+        type: 'component',
+        show: true,
+        desc: '展示列表',
+        author: 'richard1015'
+      },
+      {
+        name: 'Uploader',
+        sort: 2,
+        cName: '上传组件',
+        type: 'component',
+        show: true,
+        desc: '上传文件、图片',
+        author: 'richard1015'
+      },
+      {
+        name: 'Icon',
+        sort: 3,
+        cName: '图标组件',
+        type: 'component',
+        show: true,
+        desc: '图标',
+        author: 'richard1015'
+      },
+      {
+        name: 'Price',
+        sort: 4,
+        cName: '价格组件',
+        type: 'component',
+        show: true,
+        desc: '价格组件',
+        author: 'ailululu'
+      }
+    ]
+  },
+  {
+    name: '导航组件',
+    packages: []
+  },
+  {
+    name: '业务组件',
+    packages: []
+  }
+];

+ 20 - 1
src/config.ts

@@ -107,7 +107,26 @@ export const nav = [
   },
   {
     name: '导航组件',
-    packages: []
+    packages: [
+      {
+        name: 'Navbar',
+        sort: 3,
+        cName: '导航组件',
+        type: 'componment',
+        show: true,
+        desc: '导航组件',
+        author: 'liqiong43'
+      },
+      {
+        name: 'tab',
+        sort: 1,
+        cName: '标签组件',
+        type: 'component',
+        show: true,
+        desc: '标签组件',
+        author: 'zhenyulei'
+      }
+    ]
   },
   {
     name: '业务组件',

+ 5 - 5
src/packages/icon/index.scss

@@ -1,13 +1,13 @@
 @import '../../styles/font/iconfont.css';
 .nut-icon {
-  width: 20px;
-  height: 20px;
-  line-height: 20px;
+  width: $icon-width;
+  height: $icon-height;
+  line-height: $icon-line-height;
   text-align: right;
 
   &__img {
-    width: 20px;
-    height: 20px;
+    width: $icon-width;
+    height: $icon-height;
     object-fit: contain;
   }
 }

+ 9 - 23
src/packages/icon/index.vue

@@ -5,26 +5,11 @@ const { componentName, create } = createComponent('icon');
 
 export default create({
   props: {
-    name: {
-      type: String,
-      default: ''
-    },
-    size: {
-      type: String,
-      default: ''
-    },
-    classPrefix: {
-      type: String,
-      default: 'nutui-iconfont'
-    },
-    color: {
-      type: String,
-      default: ''
-    },
-    tag: {
-      type: String as PropType<keyof HTMLElementTagNameMap>,
-      default: 'i'
-    }
+    name: { type: String, default: '' },
+    size: { type: String, default: '' },
+    classPrefix: { type: String, default: 'nutui-iconfont' },
+    color: { type: String, default: '' },
+    tag: { type: String as PropType<keyof HTMLElementTagNameMap>, default: 'i' }
   },
   emits: ['click'],
 
@@ -37,9 +22,10 @@ export default create({
     };
     const styleOptions = {
       class: `${props.classPrefix} ${componentName}-${props.name}`,
-      style: { color: props.color, fontSize: props.size },
-      onClick: handleClick
-    } as any;
+      style: { color: props.color, fontSize: props.size, width: '', height: '' },
+      onClick: handleClick,
+      src: ''
+    };
     if (isImage()) {
       styleOptions.class = `${componentName}__img`;
       styleOptions.src = props.name;

+ 29 - 0
src/packages/navbar/demo.vue

@@ -0,0 +1,29 @@
+<template>
+  <div class="demo">
+    <h2>基础用法</h2>
+    <nut-navbar leftShow title="订单详情" icon="share"></nut-navbar>
+    <nut-navbar leftShow title="浏览记录" desc="清空"></nut-navbar>
+    <!-- 是否支持自定义属性titIcon -->
+    <nut-navbar :leftShow="false" title="购物车" titIcon="locationg3" desc="编辑" icon="more"></nut-navbar>
+    <h2>增加tab及右侧按钮</h2>
+    <nut-navbar title="商品" desc="编辑" icon="horizontal"></nut-navbar>
+    <h2>多tab切换导航</h2>
+    <nut-navbar title="商品" icon="more"></nut-navbar>
+  </div>
+</template>
+
+<script lang="ts">
+import { createComponent } from '@/utils/create';
+const { createDemo } = createComponent('navbar');
+export default createDemo({
+  props: {},
+  setup() {
+    const testClick = (event: Event) => {
+      alert(event);
+    };
+    return { testClick };
+  }
+});
+</script>
+
+<style lang="scss" scoped></style>

+ 57 - 0
src/packages/navbar/doc.md

@@ -0,0 +1,57 @@
+# Navbar 头部导航
+
+### 介绍 
+
+
+提供导航功能。
+
+### 安装
+
+```javascript
+
+import { createApp } from 'vue';
+import { Navbar } from '@nutui/nutui';
+
+const app = createApp();
+app.use(Navbar);
+
+```
+
+### 代码示例
+
+### 基本用法
+
+```html
+    <nut-navbar leftShow title="订单详情" icon="share"></nut-navbar>
+    <nut-navbar leftShow title="浏览记录" desc="清空"></nut-navbar>
+    <nut-navbar :leftShow="false" title="购物车" titIcon="locationg3" desc="编辑" icon="more"></nut-navbar>
+```
+
+### 增加tab及右侧按钮
+
+```html
+    <nut-navbar title="商品" titIcon="locationg3" desc="编辑" icon="horizontal"></nut-navbar>
+```
+
+### 多tab切换导航
+
+```html
+    <nut-navbar title="商品" titIcon="locationg3" icon="more"></nut-navbar>
+```
+
+### 链接
+
+### 展示图标
+
+### API
+
+### Prop
+
+| 字段            | 说明                                                                                           | 类型    | 默认值  |
+|-----------------|------------------------------------------------------------------------------------------------|---------|---------|
+| title           | 标题名称                                                                                       | String  | -       |
+| desc            | 右侧描述                                                                                       | String  | -       |
+| leftShow        | 是否展示左侧箭头                                                                              | Boolean | false   |
+| icon            | 左侧 [图标名称](#/icon) 或图片链接                                                             | String  | -       |
+
+### Event

+ 83 - 0
src/packages/navbar/index.scss

@@ -0,0 +1,83 @@
+.nut-navbar {
+  position: relative;
+  display: -webkit-box;
+  display: -webkit-flex;
+  display: flex;
+  -webkit-box-align: center;
+  -webkit-align-items: center;
+  align-items: center;
+  height: 44px;
+  padding: 13px 16px;
+  background: $white;
+  box-shadow: 0px 1px 7px 0px rgba(237, 238, 241, 1);
+  font-size: $cell-title-font;
+  color: $cell-color;
+  margin: 10px 0;
+  &:active::before {
+    opacity: 0.1;
+  }
+  &--clickable {
+    cursor: pointer;
+    &::before {
+      position: absolute;
+      top: 50%;
+      left: 50%;
+      width: 100%;
+      height: 100%;
+      background-color: $black;
+      border: inherit;
+      border-color: $black;
+      border-radius: inherit;
+      transform: translate(-50%, -50%);
+      opacity: 0;
+      content: ' ';
+    }
+  }
+
+  .nutui-iconfont {
+    .nut-icon-left {
+      text-align: left;
+    }
+  }
+
+  &__title {
+    max-width: 60%;
+    margin: 0 auto;
+    &.icon {
+      .icon {
+        margin-right: 10px;
+        margin-left: 8px;
+      }
+    }
+    &-desc {
+      font-size: $cell-title-desc-font;
+    }
+  }
+  &__left {
+    font-size: $cell-desc-font;
+    color: $cell-desc-color;
+    position: absolute;
+    top: 0;
+    bottom: 0;
+    display: flex;
+    align-items: center;
+    cursor: pointer;
+    padding: 0 16px;
+
+    left: 0;
+  }
+  &__right {
+    font-size: $cell-desc-font;
+    color: $cell-desc-color;
+
+    position: absolute;
+    top: 0;
+    bottom: 0;
+    display: flex;
+    align-items: center;
+
+    right: 0;
+    padding: 0 16px;
+    cursor: pointer;
+  }
+}

+ 62 - 0
src/packages/navbar/index.vue

@@ -0,0 +1,62 @@
+<template>
+  <view :class="classes" @click="handleClick">
+    <slot>
+      <!-- 左侧  icon-->
+      <view class="nut-navbar__left">
+        <nut-icon v-if="leftShow" color="#979797" name="left"></nut-icon>
+      </view>
+
+      <view class="nut-navbar__title" :class="{ icon: icon }" v-if="title || titIcon">
+        <view v-if="title">{{ title }}</view>
+        <nut-icon v-if="titIcon" class="icon" :name="titIcon"></nut-icon>
+      </view>
+
+      <!-- 右侧  title/icon/多个tit/多个icon-->
+      <view class="nut-navbar__right" :class="{ icon: icon }" v-if="desc || icon">
+        <view v-if="desc" :style="{ 'text-align': descTextAlign }">{{ desc }}</view>
+        <view> <nut-icon v-if="icon" class="icon" :name="icon"></nut-icon></view>
+      </view>
+    </slot>
+  </view>
+</template>
+
+<script lang="ts">
+import { computed } from 'vue';
+import { createComponent } from '@/utils/create';
+import { useRouter } from 'vue-router';
+import Icon from '@/packages/icon/index.vue';
+const { componentName, create } = createComponent('navbar');
+
+export default create({
+  props: {
+    title: { type: String, default: '' },
+    leftShow: { type: Boolean, default: true },
+    icon: { type: String, default: '' },
+    desc: { type: String, default: '' },
+    titIcon: { type: String, default: '' }
+  },
+  components: {
+    [Icon.name]: Icon
+  },
+  emits: ['click'],
+  setup(props, { emit }) {
+    const classes = computed(() => {
+      const prefixCls = componentName;
+      return {
+        [prefixCls]: true,
+        [`${prefixCls}--clickable`]: props.leftShow
+      };
+    });
+    const router = useRouter();
+
+    return {
+      //handleClick,
+      classes
+    };
+  }
+});
+</script>
+
+<style lang="scss">
+@import 'index.scss';
+</style>

+ 118 - 0
src/packages/tab/demo.vue

@@ -0,0 +1,118 @@
+<template>
+  <div class="demo">
+    <h2>基础用法</h2>
+    <nut-tab>
+      <nut-tab-panel tab-title="全部"><p class="content">这里是页签全部内容</p></nut-tab-panel>
+      <nut-tab-panel tab-title="待付款"><p class="content">这里是页签待付款内容</p></nut-tab-panel>
+      <nut-tab-panel tab-title="待收获"><p class="content">这里是页签待收获内容</p></nut-tab-panel>
+      <nut-tab-panel tab-title="已完成"><p class="content">这里是页签已完成内容</p></nut-tab-panel>
+    </nut-tab>
+
+    <h2>defaultIndex设置默认显示tab</h2>
+    <h2>switchTab监听切换tab返回事件</h2>
+    <nut-tab :default-index="1" @switch-tab="switchTab">
+      <nut-tab-panel tab-title="全部"><p class="content">这里是页签全部内容</p></nut-tab-panel>
+      <nut-tab-panel tab-title="待付款"><p class="content">这里是页签待付款内容</p></nut-tab-panel>
+      <nut-tab-panel tab-title="待收获"><p class="content">这里是页签待收获内容</p></nut-tab-panel>
+      <nut-tab-panel tab-title="已完成"><p class="content">这里是页签已完成内容</p></nut-tab-panel>
+    </nut-tab>
+
+    <h2> animatedTime 开启切换标签内容时的转场动画时间</h2>
+    <nut-tab :animated-time="500">
+      <nut-tab-panel tab-title="全部"><p class="content">这里是页签全部内容</p></nut-tab-panel>
+      <nut-tab-panel tab-title="待付款"><p class="content">这里是页签待付款内容</p></nut-tab-panel>
+      <nut-tab-panel tab-title="待收获"><p class="content">这里是页签待收获内容</p></nut-tab-panel>
+      <nut-tab-panel tab-title="已完成"><p class="content">这里是页签已完成内容</p></nut-tab-panel>
+    </nut-tab>
+
+    <h2> 禁止tab内容滑动</h2>
+    <nut-tab :no-swiping="true">
+      <nut-tab-panel tab-title="全部"><p class="content">这里是页签全部内容</p></nut-tab-panel>
+      <nut-tab-panel tab-title="待付款"><p class="content">这里是页签待付款内容</p></nut-tab-panel>
+      <nut-tab-panel tab-title="待收获"><p class="content">这里是页签待收获内容</p></nut-tab-panel>
+      <nut-tab-panel tab-title="已完成"><p class="content">这里是页签已完成内容</p></nut-tab-panel>
+    </nut-tab>
+
+    <h2>标签数量超过 5 个时,标签栏可以在水平方向上滚动,切换时会自动将当前标签居中。</h2>
+    <nut-tab :animated-time="500">
+      <nut-tab-panel tab-title="全部"><p class="content">这里是页签全部内容</p></nut-tab-panel>
+      <nut-tab-panel tab-title="待付款"><p class="content">这里是页签待付款内容</p></nut-tab-panel>
+      <nut-tab-panel tab-title="待收获"><p class="content">这里是页签待收获内容</p></nut-tab-panel>
+      <nut-tab-panel tab-title="已完成"><p class="content">这里是页签已完成内容</p></nut-tab-panel>
+      <nut-tab-panel tab-title="已取消"><p class="content">这里是页签已取消内容</p></nut-tab-panel>
+      <nut-tab-panel tab-title="待评价"><p class="content">这里是页签待评价内容</p></nut-tab-panel>
+    </nut-tab>
+
+    <h2>设置slot:header可以自定义标签</h2>
+    <nut-tab>
+      <nut-tab-panel tab-title="全部">
+        <template v-slot:header><nut-icon name="dongdong"></nut-icon></template>
+        <p class="content">这里是页签全部内容</p>
+      </nut-tab-panel>
+      <nut-tab-panel tab-title="待付款"><p class="content">这里是页签待付款内容</p></nut-tab-panel>
+      <nut-tab-panel tab-title="待收获"><p class="content">这里是页签待收获内容</p></nut-tab-panel>
+      <nut-tab-panel tab-title="已完成"><p class="content">这里是页签已完成内容</p></nut-tab-panel>
+      <nut-tab-panel tab-title="已取消"><p class="content">这里是页签已取消内容</p></nut-tab-panel>
+      <nut-tab-panel tab-title="待评价"><p class="content">这里是页签待评价内容</p></nut-tab-panel>
+    </nut-tab>
+
+    <h2>左右tab布局</h2>
+    <nut-tab direction="vertical" :animated-time="500" :default-index="2">
+      <nut-tab-panel tab-title="页签一"><p class="content">这里是页签一内容</p></nut-tab-panel>
+      <nut-tab-panel tab-title="页签二"><p class="content">这里是页签二内容</p></nut-tab-panel>
+      <nut-tab-panel tab-title="页签三"><p class="content">这里是页签三内容</p></nut-tab-panel>
+      <nut-tab-panel tab-title="页签四"><p class="content">这里是页签四内容</p></nut-tab-panel>
+      <nut-tab-panel tab-title="页签五"><p class="content">这里是页签五内容</p></nut-tab-panel>
+      <nut-tab-panel tab-title="页签六"><p class="content">这里是页签六内容</p></nut-tab-panel>
+      <nut-tab-panel tab-title="页签七"><p class="content">这里是页签七内容</p></nut-tab-panel>
+    </nut-tab>
+
+    <h2>异步操作</h2>
+    <nut-tab :animated-time="500" v-if="editList.length > 0">
+      <nut-tab-panel :tab-title="item.title" v-for="(item, index) in editList" :key="index">
+        <p class="content">这里是页签{{ index }}内容</p>
+      </nut-tab-panel>
+    </nut-tab>
+    <nut-button type="primary" @click="changeList">改变数据</nut-button>
+  </div>
+</template>
+
+<script lang="ts">
+import { ref, reactive, toRefs } from 'vue';
+import { createComponent } from '@/utils/create';
+const { createDemo } = createComponent('tab');
+export default createDemo({
+  props: {},
+  setup() {
+    const resData = reactive({
+      editList: [
+        {
+          title: '标签一'
+        },
+        {
+          title: '标签二'
+        }
+      ]
+    });
+    function changeList() {
+      resData.editList.push({
+        title: '标签' + resData.editList.length
+      });
+    }
+    function switchTab(activeInddex: number, event: MouseEvent) {
+      console.log(activeInddex, event);
+    }
+    return {
+      ...toRefs(resData),
+      changeList,
+      switchTab
+    };
+  }
+});
+</script>
+
+<style lang="scss" scoped>
+.content {
+  padding: 10px;
+}
+</style>

+ 171 - 0
src/packages/tab/doc.md

@@ -0,0 +1,171 @@
+# Tab 选项卡
+
+常用于平级区域大块内容的的收纳和展现,支持内嵌标签形式和渲染循环数据形式。
+
+
+## 基础样式
+
+```html
+<nut-tab>
+  <nut-tab-panel tab-title="全部"><p class="content">这里是页签全部内容</p></nut-tab-panel>
+  <nut-tab-panel tab-title="待付款"><p class="content">这里是页签待付款内容</p></nut-tab-panel>
+  <nut-tab-panel tab-title="待收获"><p class="content">这里是页签待收获内容</p></nut-tab-panel>
+  <nut-tab-panel tab-title="已完成"><p class="content">这里是页签已完成内容</p></nut-tab-panel>
+</nut-tab>
+```
+
+## 默认显示tab:
+defaultIndex设置默认显示tab
+switchTab监听切换tab返回事件
+
+```html
+<nut-tab  :defaultIndex="1" @switchTab="switchTab">
+  <nut-tab-panel tab-title="全部"><p class="content">这里是页签全部内容</p></nut-tab-panel>
+  <nut-tab-panel tab-title="待付款"><p class="content">这里是页签待付款内容</p></nut-tab-panel>
+  <nut-tab-panel tab-title="待收获"><p class="content">这里是页签待收获内容</p></nut-tab-panel>
+  <nut-tab-panel tab-title="已完成"><p class="content">这里是页签已完成内容</p></nut-tab-panel>
+</nut-tab>
+<script lang="ts">
+import { createComponent } from '@/utils/create';
+const { createDemo } = createComponent('tab');
+export default createDemo({
+  setup() {
+    function switchTab(activeInddex:number,event:MouseEvent){
+      console.log(activeInddex,event);
+    }
+    return {
+      switchTab
+    };
+  }
+});
+</script>
+```
+
+## animatedTime 开启切换标签内容时的转场动画时间
+```html
+<nut-tab :animatedTime="500">
+  <nut-tab-panel tab-title="全部"><p class="content">这里是页签全部内容</p></nut-tab-panel>
+  <nut-tab-panel tab-title="待付款"><p class="content">这里是页签待付款内容</p></nut-tab-panel>
+  <nut-tab-panel tab-title="待收获"><p class="content">这里是页签待收获内容</p></nut-tab-panel>
+  <nut-tab-panel tab-title="已完成"><p class="content">这里是页签已完成内容</p></nut-tab-panel>
+</nut-tab>
+```
+
+
+## 禁止tab内容滑动
+
+```html
+<nut-tab :no-swiping="true">
+  <nut-tab-panel tab-title="全部"><p class="content">这里是页签全部内容</p></nut-tab-panel>
+  <nut-tab-panel tab-title="待付款"><p class="content">这里是页签待付款内容</p></nut-tab-panel>
+  <nut-tab-panel tab-title="待收获"><p class="content">这里是页签待收获内容</p></nut-tab-panel>
+  <nut-tab-panel tab-title="已完成"><p class="content">这里是页签已完成内容</p></nut-tab-panel>
+</nut-tab>
+```
+
+## 标签数量超过 5 个时,标签栏可以在水平方向上滚动
+切换时会自动将当前标签居中。
+
+```html
+<nut-tab :animatedTime="500">
+  <nut-tab-panel tab-title="全部"><p class="content">这里是页签全部内容</p></nut-tab-panel>
+  <nut-tab-panel tab-title="待付款"><p class="content">这里是页签待付款内容</p></nut-tab-panel>
+  <nut-tab-panel tab-title="待收获"><p class="content">这里是页签待收获内容</p></nut-tab-panel>
+  <nut-tab-panel tab-title="已完成"><p class="content">这里是页签已完成内容</p></nut-tab-panel>
+  <nut-tab-panel tab-title="已取消"><p class="content">这里是页签已取消内容</p></nut-tab-panel>
+  <nut-tab-panel tab-title="待评价"><p class="content">这里是页签待评价内容</p></nut-tab-panel>
+</nut-tab>
+```
+
+## 设置slot:header可以自定义标签
+```html
+<nut-tab>
+  <nut-tab-panel tab-title="全部">
+    <template v-slot:header><nut-icon name="dongdong"></nut-icon></template>
+    <p class="content">这里是页签全部内容</p>
+  </nut-tab-panel>
+  <nut-tab-panel tab-title="待付款"><p class="content">这里是页签待付款内容</p></nut-tab-panel>
+  <nut-tab-panel tab-title="待收获"><p class="content">这里是页签待收获内容</p></nut-tab-panel>
+  <nut-tab-panel tab-title="已完成"><p class="content">这里是页签已完成内容</p></nut-tab-panel>
+  <nut-tab-panel tab-title="已取消"><p class="content">这里是页签已取消内容</p></nut-tab-panel>
+  <nut-tab-panel tab-title="待评价"><p class="content">这里是页签待评价内容</p></nut-tab-panel>
+</nut-tab>
+```
+## 左右tab布局
+```html
+<nut-tab direction="vertical" :animatedTime="500" :defaultIndex="2">
+  <nut-tab-panel tab-title="页签一"><p class="content">这里是页签一内容</p></nut-tab-panel>
+  <nut-tab-panel tab-title="页签二"><p class="content">这里是页签二内容</p></nut-tab-panel>
+  <nut-tab-panel tab-title="页签三"><p class="content">这里是页签三内容</p></nut-tab-panel>
+  <nut-tab-panel tab-title="页签四"><p class="content">这里是页签四内容</p></nut-tab-panel>
+  <nut-tab-panel tab-title="页签五"><p class="content">这里是页签五内容</p></nut-tab-panel>
+  <nut-tab-panel tab-title="页签六"><p class="content">这里是页签六内容</p></nut-tab-panel>
+  <nut-tab-panel tab-title="页签七"><p class="content">这里是页签七内容</p></nut-tab-panel>
+</nut-tab>
+```
+## 异步操作
+
+```html
+<nut-tab :animatedTime="500" v-if="editList.length>0">
+    <nut-tab-panel :tab-title="item.title" v-for="(item,index) in editList" :key="index">
+      <p class="content">这里是页签{{index}}内容</p>
+    </nut-tab-panel>
+</nut-tab>
+<nut-button type="primary" @click="changeList">改变数据</nut-button>
+</div>
+<script lang="ts">
+import { ref , reactive,toRefs } from "vue";
+import { createComponent } from '@/utils/create';
+const { createDemo } = createComponent('tab');
+export default createDemo({
+  setup() {
+    const resData = reactive({
+      editList:[
+        {
+          title:'标签一'
+        },
+        {
+          title:'标签二'
+        }
+      ]
+    });
+    function changeList(){
+      resData.editList.push({
+        title:'标签'+resData.editList.length
+      })
+    }
+    return {
+      ...toRefs(resData),
+      changeList
+    };
+  }
+});
+</script>
+```
+
+
+### Prop
+
+### nut-tab
+
+| 字段 | 说明 | 类型 | 默认值
+|----- | ----- | ----- | ----- 
+| direction | 页签栏的分布,可选值 horizontal/vertical | String | horizontal
+| default-index | 默认选中的页签栏 | Number | 0
+| animated-time | 开启切换标签内容时的转场动画时间 | Number | 0
+| no-swiping |禁止tab内容滑动|Boolean|false
+
+
+### nut-tab-panel
+
+| 字段 | 说明 | 类型 | 默认值
+|----- | ----- | ----- | ----- 
+| tab-title| 页签的标题 | String | ''
+| slot:header | 页签title的自定义slot | vnode | --
+
+### Event
+
+| 事件名称 | 说明 | 回调参数 
+|----- | ----- | ----- 
+| switch-tab | 切换页签时触发事件 | 点击的索引值和触发元素
+

+ 102 - 0
src/packages/tab/index.scss

@@ -0,0 +1,102 @@
+.nutui-tab {
+  .tab-title {
+    width: 100%;
+    height: 46px;
+    overflow-x: scroll;
+    display: flex;
+    flex-wrap: nowrap;
+    scroll-behavior: smooth;
+    background: #f5f5f5;
+    position: relative;
+    &::-webkit-scrollbar {
+      display: none;
+    }
+    .underline {
+      position: absolute;
+    }
+    .tab-title-box {
+      min-width: 75px;
+      height: 100%;
+      display: flex;
+      flex: 1;
+      justify-content: center;
+      align-items: center;
+      box-sizing: border-box;
+      text-align: center;
+      font-size: 14px;
+    }
+    .nut-tab-active {
+      color: #1a1a1a;
+      font-weight: bold;
+      font-size: 16px;
+      position: relative;
+      &::after {
+        content: '';
+        position: absolute;
+        bottom: 5px;
+        left: 50%;
+        transform: translateX(-50%);
+        width: 12px;
+        height: 4px;
+        background-image: url('https://img12.360buyimg.com/imagetools/jfs/t1/127200/40/18747/536/5fb36b5aE61cac2d8/638032e8da9b93f4.png');
+        background-size: 100% 100%;
+      }
+    }
+  }
+  .nutui-tab-swiper {
+    overflow: hidden;
+    display: block;
+    width: 100%;
+    height: 200px;
+    background: #fff;
+    box-sizing: border-box;
+  }
+}
+.vertical-tab {
+  display: flex;
+  height: 150px;
+  .tab-title {
+    width: 70px;
+    height: 100%;
+    display: flex;
+    flex-direction: column;
+    overflow-y: scroll;
+    &::-webkit-scrollbar {
+      display: none;
+    }
+    .tab-title-box {
+      width: 100%;
+      flex: 1;
+      display: flex;
+      justify-content: flex-start;
+      align-items: center;
+      min-height: 35px;
+    }
+    .nut-tab-active {
+      color: #1a1a1a;
+      font-weight: bold;
+      font-size: 16px;
+      position: relative;
+      &::after {
+        content: '';
+        position: absolute;
+        right: 5px;
+        top: 50%;
+        left: auto;
+        transform: translateY(-50%) rotate(270deg);
+        width: 12px;
+        height: 4px;
+        background-image: url('https://img12.360buyimg.com/imagetools/jfs/t1/127200/40/18747/536/5fb36b5aE61cac2d8/638032e8da9b93f4.png');
+        background-size: 100% 100%;
+      }
+    }
+  }
+  .nutui-tab-swiper {
+    overflow: hidden;
+    display: block;
+    flex: 1;
+    height: 100%;
+    background: #fff;
+    box-sizing: border-box;
+  }
+}

+ 150 - 0
src/packages/tab/index.vue

@@ -0,0 +1,150 @@
+<template>
+  <div :class="[direction === 'vertical' ? 'vertical-tab' : 'nutui-tab']">
+    <div class="tab-title" ref="navlist">
+      <div
+        :class="['tab-title-box', { 'nut-tab-active': activeIndex == index }]"
+        v-for="(item, index) in titles"
+        :key="index"
+        @click="switchTitle(index, $event)"
+      >
+        {{ item.title }}
+        <TabTitle v-bind:slots="item.content" v-if="item.content"></TabTitle>
+      </div>
+      <div class="underline"></div>
+    </div>
+    <div :class="['nutui-tab-swiper', swiperClassName]">
+      <div :class="['swiper-wrapper', { 'swiper-no-swiping': noSwiping }]">
+        <slot></slot>
+      </div>
+    </div>
+  </div>
+</template>
+<script lang="ts">
+// @ts-nocheck
+import { PropType, h, toRefs, reactive, computed, ref, onMounted, nextTick, watch, watchEffect } from 'vue';
+import { createComponent } from '@/utils/create';
+const { create } = createComponent('tab');
+import TabTitle from './tabTitle';
+import Swiper from 'swiper';
+import 'swiper/swiper-bundle.css';
+type TabDirection = 'horizontal' | 'vertical';
+
+export default create({
+  props: {
+    defaultIndex: {
+      type: Number,
+      default: 0
+    },
+    animatedTime: {
+      type: Number,
+      default: 0
+    },
+    direction: {
+      type: String as PropType<TabDirection>,
+      default: 'horizontal'
+    },
+    noSwiping: {
+      type: Boolean,
+      default: false
+    }
+  },
+  components: {
+    TabTitle
+  },
+  setup(props, ctx) {
+    const titles: any = reactive([]);
+    let mySwiper: any = reactive({});
+    const isLock = ref(false);
+    const activeIndex = ref(props.defaultIndex);
+    const navlist: any = ref(null);
+    // 生成随机的id
+    function createHash() {
+      return Array.from(Array(10), () => Math.floor(Math.random() * 36).toString(36)).join('');
+    }
+
+    const swiperClassName = ref('swiper-' + createHash());
+    //title点击后居中显示
+    function centerTitle(index: number) {
+      const currEle = navlist.value.querySelectorAll('.tab-title-box')[index];
+      if (props.direction === 'vertical') {
+        const currTitleTop = navlist.value.offsetTop;
+        const currTop = currEle.offsetTop;
+        const currHeight = currEle.offsetHeight;
+        const tapHeight = navlist.value.offsetHeight;
+        navlist.value.scroll(0, currTop - currTitleTop - tapHeight / 2 + currHeight / 2);
+      } else {
+        const currLeft = currEle.offsetLeft;
+        const currWidth = currEle.offsetWidth;
+        const tapWidth = navlist.value.offsetWidth;
+        navlist.value.scroll(currLeft - tapWidth / 2 + currWidth / 2, 0);
+      }
+    }
+
+    //切换tab
+    function switchTitle(index: number) {
+      activeIndex.value = index;
+      centerTitle(index);
+      mySwiper.slideToLoop(index, props.animatedTime, false);
+    }
+    function initSwiper(currIndex: number) {
+      mySwiper = new Swiper('.' + swiperClassName.value, {
+        loop: true /** 循环模式选项 */,
+        noSwiping: true,
+        observer: true, //修改swiper自己或子元素时,自动初始化swiper
+        observeParents: true, //修改swiper的父元素时,自动初始化swiper
+        setWrapperSize: true,
+        direction: props.direction,
+        initialSlide: currIndex,
+        on: {
+          touchStart: function() {
+            isLock.value = true;
+          },
+          transitionEnd: function(swiper: Swiper): void {
+            ctx.emit('switchTab', swiper.realIndex, swiper);
+            if (isLock.value) {
+              activeIndex.value = swiper.realIndex;
+              centerTitle(swiper.realIndex);
+            }
+          }
+        }
+      });
+    }
+    function initTitle() {
+      titles.length = 0;
+      if (ctx.slots.default) {
+        const slots: any[] = ctx.slots.default().length === 1 ? ctx.slots.default()[0].children : ctx.slots.default();
+        slots &&
+          slots.forEach((item, index) => {
+            titles.push({
+              title: item.props['tab-title'],
+              content: item.children && item.children.header ? item.children.header() : null
+            });
+          });
+      }
+      setTimeout(() => {
+        initSwiper(activeIndex.value);
+      }, 0);
+    }
+    onMounted(() => {
+      initTitle();
+    });
+    watch(
+      () => ctx.slots.default(),
+      () => {
+        initTitle();
+      }
+    );
+    return {
+      swiperClassName,
+      titles,
+      navlist,
+      activeIndex,
+      switchTitle
+    };
+  }
+});
+</script>
+
+<style lang="scss">
+@import 'index.scss';
+</style>

+ 9 - 0
src/packages/tab/tabTitle.ts

@@ -0,0 +1,9 @@
+import { h } from 'vue';
+export default {
+  setup(props: any) {
+    return () => h(`view`, {}, props.slots);
+  },
+  props: {
+    slots: Object
+  }
+};

+ 2 - 0
src/packages/tabpanel/index.scss

@@ -0,0 +1,2 @@
+.nut-temp {
+}

+ 26 - 0
src/packages/tabpanel/index.vue

@@ -0,0 +1,26 @@
+<template>
+  <div class="swiper-slide">
+    <slot></slot>
+  </div>
+</template>
+<script lang="ts">
+import { createComponent } from '@/utils/create';
+const { create } = createComponent('tab-panel');
+
+export default create({
+  props: {
+    tabTitle: {
+      type: String,
+      default: ''
+    }
+  },
+  components: {},
+  setup(props, ctx) {
+    //
+  }
+});
+</script>
+
+<style lang="scss">
+@import 'index.scss';
+</style>

+ 16 - 0
src/packages/uploader/demo.vue

@@ -0,0 +1,16 @@
+<template>
+  <div class="demo bg-w">
+    <h2>基础用法</h2>
+    <nut-uploader></nut-uploader>
+  </div>
+</template>
+
+<script lang="ts">
+import { createComponent } from '@/utils/create';
+const { createDemo } = createComponent('uploader');
+export default createDemo({
+  props: {}
+});
+</script>
+
+<style lang="scss" scoped></style>

+ 33 - 227
src/packages/uploader/doc.md

@@ -1,237 +1,54 @@
 # Uploader 上传
 
-> 文件上传组件
+### 介绍
 
-### 基本用法
+用于将本地的图片或文件上传至服务器。
+
+### 安装
 
 ``` javascript
 import { createApp } from 'vue';
-import { Button } from '@nutui/nutui';
+import { Uploader } from '@nutui/nutui';
 
 const app = createApp();
-app.use(Button);
+app.use(Uploader);
 
 ```
 
-```html
-<nut-uploader
-    name="uploader-demo"
-    :url="url"
-    :isPreview="true"
-    :acceptType = "['image/jpeg', 'image/png', 'image/gif', 'image/bmp']"
-    @start="onStart"
-    @success="onSuccess"
-    @fail="onFail"
-    @progress="onProgress"
-    @preview="onPreview"
-    @showMsg="showMsgFn"
-    typeError="对不起,不支持上传该类型文件!"
-    limitError="对不起,文件大小超过限制!"
->
-上传
-</nut-uploader>   
-```
-
-```javascript
-export default { 
-  data() {
-    return {
-      url:'https://my-json-server.typicode.com/linrufeng/demo/posts',    
-    };
-  },
-  methods:{
-      onStart(){
-        console.log('上传开始');
-      },
-      onSuccess(file,res){
-        alert('上传成功!');
-      },
-      onFail(file,res){
-        alert('上传失败!');
-      },
-      onProgress(file, loaded, total) {
-        console.log('上传进度:'+parseInt((100 * loaded) / total)+'%');
-      },
-      onPreview(file) {
-        this.previewImg = file;
-      },
-      showMsgFn(msg){
-        alert(msg);
-      },
-  }
-```
-
-### 高级用法
-
-与吐司 **Toast** 组件结合使用
-
-```html
-<nut-uploader
-    name="uploader-demo"
-    :url="url"
-    @success="onSuccess"
-    @fail="onFail"
-    @showMsg="showMsgFn"
->
-上传
-</nut-uploader>   
-```
-
-```javascript
-export default { 
-  data() {
-    return {
-      url:'https://my-json-server.typicode.com/linrufeng/demo/posts',    
-    };
-  },
-  methods:{
-      onSuccess(file,res){
-        this.$toast.success('上传成功');
-      },
-      onFail(file,res){
-        this.$toast.fail('上传失败!');
-      },
-      showMsgFn(msg){
-        this.$toast.text(msg);
-      },
-  }
-
-```
-
-与按钮 **Button** 组件结合使用
-
-```html
-<nut-uploader
-  :name="name"
-  :url="url"    
-  >
-  <nut-button small>上传</nut-button>
-</nut-uploader>   
-```
-
-自定义 headers & formData 使用
-```html
-<nut-uploader
-  :name="name"
-  :url="url"
-  :headers="headers"
-  :attach="formData"
-  >
-  <nut-button small>上传</nut-button>
-</nut-uploader>   
-```
-```javascript
-export default { 
-  data() {
-    return {
-      url:'https://my-json-server.typicode.com/linrufeng/demo/posts', 
-      name:'testname',
-      headers:{
-        token:'test'
-      },
-      formData:{
-        f1:'test',
-        f2:'test1'
-      },
-    };
-  },
-}
-```
-
-与进度条组件 **Progress** 结合使用
-
-```html
-<nut-uploader
-    :name="name"
-    :url="url"
-    @progress="progress"    
-    > 上传
-</nut-uploader>  
-<nut-progress :percentage="progressNum" :showText="false" strokeWidth="24"/>
-```
-预览上传图片
-```html
- <nut-uploader
-    :name="name"
-    :url="url"
-    :xhrState="stateNum"
-    :isPreview="true"
-    @success="demoSuccess"
-    @fail="demoFail"
-    @preview="preview"
-    @showMsg="showMsg1"
-  >
-    <nut-button small>上传</nut-button>
-  </nut-uploader>
-```
-```js
-preview(file) {
-  this.previewImg = file;
-},
-```
-
-上传图片前处理图片内容
-
-```html
- <nut-uploader
-    :beforeUpload="test"
-    :name="name"
-    :url="url"
-    :xhrState="stateNum"
-    :acceptType = "['image/jpeg', 'image/png', 'image/gif', 'image/bmp']"
-    @success="demo1Success"
-    @failure="demo1Fail"
-    @start="demo1UploadStart"
-    @showMsg="showMsg"
-  ><nut-button small>上传图片前处理图片内容</nut-button></nut-uploader>
-```
-```js
-test($ev){   
-  console.log($ev,'可以处理input选择的内容')  
-  return {
-    event:$ev,
-    data:''
-  }
-},
-```
-
-自定义增加上传图片数据
-```html
-<nut-uploader
-    :selfData="selfData"          
-    :name="name"
-    :url="url"
-    :xhrState="stateNum"
-    :acceptType = "['image/jpeg', 'image/png', 'image/gif', 'image/bmp']"
-    @success="demo1Success"
-    @failure="demo1Fail"
-    @start="demo1UploadStart"
-    @showMsg="showMsg"
-  ><nut-button small>自定义增加上传图片数据</nut-button></nut-uploader>
-
-```
+## 代码示例
 
+### 基本用法
 
+## API
 
 ### Prop
 
 | 字段 | 说明 | 类型 | 默认值
 |----- | ----- | ----- | ----- 
-| name | input name的名称 | String | ""
+| name | `input` 标签 `name` 的名称,发到后台的文件参数名 | String | "file"
 | url | 上传服务器的接口地址 | String | -
-| isPreview | 是否需要预览 | Boolean | false
-| clearInput | 是否需要清空input内容,设为true支持重复选择上传同一个文件 | Boolean | false
-| maxSize | 可以设定最大上传文件的大小(字节) | Number | 5242880
-| acceptType | 可以上传文件的类型 | Array | ['image/jpeg', 'image/png', 'image/gif', 'image/bmp']
-| attach | 附加上传的信息formData | Object | {}
-| headers | 自定义headers | Object | {}
+| default-file-list | 默认已经上传的文件列表 | object[] | -
+| file-list | 默认已经上传的文件列表 | object[] | -
+| custom-request | 通过覆盖默认的上传行为,可以自定义自己的上传实现 | Function | -
+| is-preview | 是否上传成功后展示预览图 | Boolean | true
+| is-deletable | 是否展示删除按钮 | Boolean | true
+| method | 上传请求的 http method | String | "post"
+| capture | 图片[选取模式](https://developer.mozilla.org/zh-CN/docs/Web/HTML/Element/input#htmlattrdefcapture),可选值为 camera (直接调起摄像头) | String | "camera"
+| max-size | 可以设定最大上传文件的大小(字节) | Number丨String | 5242880 (5M)
+| max-count | 文件上传数量限制 | Number丨String | 1
+| clear-input | 是否需要清空`input`内容,设为`true`支持重复选择上传同一个文件 | Boolean | false
+| accept-type | 允许上传的文件类型,[详细说明](https://developer.mozilla.org/zh-CN/docs/Web/HTML/Element/Input/file#%E9%99%90%E5%88%B6%E5%85%81%E8%AE%B8%E7%9A%84%E6%96%87%E4%BB%B6%E7%B1%BB%E5%9E%8B) | String | * ]
+| headers | 设置上传的请求头部 | Object | {}
+| form-data | 附加上传的信息 formData | Object | {}
+| upload-icon | 上传区域[图标名称](#/zh-CN/icon)或图片链接 | String | photograph
 | xhrState | 接口响应的成功状态(status)值 | Number | 200
-| typeError | 文件类型错误提示文案 | String | "不支持上传该类型文件"
-| limitError | 文件大小超过限制提示文案 | String | "文件大小超过限制"
-| xmlError | 浏览器不支持本组件时的提示文案 | String | "对不起,您的浏览器不支持本组件!"
 | withCredentials | 支持发送 cookie 凭证信息 | Boolean | fasle
-| beforeUpload | 上传前的函数需要返回一个对象  | Function | {event:$event} $event为点击事件必传
-| selfData | 自定义增加上传的数据 | Object | {}
+| multiple | 是否支持文件多选 | Boolean | fasle
+| disabled | 是否禁用文件上传 | Boolean | fasle
+| before-upload | 上传前的函数需要返回一个对象  | Function | {event:$event} $event为点击事件必传
+| before-delete | 除文件时的回调,返回值为 false 时不移除。支持返回一个 Promise 对象,Promise 对象 resolve(false) 或 reject 时不移除      | Function(file): boolean 丨Promise | -
+| on-change | 上传文件改变时的状态,详见     | Function(fileList) 丨 Promise | -
+| custom-request | 通过覆盖默认的上传行为,可以自定义自己的上传实现     | Function  | -
 
 ### Event
 
@@ -239,18 +56,7 @@ test($ev){
 |----- | ----- | ----- 
 | start | 文件上传开始 | -
 | progress | 文件上传的进度 | 上传文件、已上传数据量、总数据量
-| preview | isPreview为true时可通过此方法获文件的Base64编码,一般用于预览 | 文件的Base64编码
-| success | 上传成功 | 文件、responseText
-| failure | 上传失败 | 文件、responseText
-| showMsg | 组件抛出信息的处理函数 | 组件抛出的提示信息
+| oversize | 	文件大小超过限制时触发 | fileItem
+| success | 上传成功 | fileList
+| failure | 上传失败 | fileList
 
-### tips
-
-使用 beforeUpload 一定要返回一个JSON 对象且 event 为必传字段
-```js
-beforeUpload($e){
-  return {
-    event:$e
-  }
-}
-```

+ 20 - 0
src/packages/uploader/index.scss

@@ -0,0 +1,20 @@
+.nut-uploader {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  position: relative;
+  overflow: hidden;
+  background: $uploader-babackground;
+  width: $uploader-width;
+  height: $uploader-height;
+  input {
+    position: absolute;
+    top: 0;
+    left: 0;
+    width: 100%;
+    height: 100%;
+    overflow: hidden;
+    cursor: pointer;
+    opacity: 0;
+  }
+}

+ 67 - 0
src/packages/uploader/index.vue

@@ -0,0 +1,67 @@
+<template>
+  <view :class="classes">
+    <nut-icon color="#808080" :name="uploadIcon"></nut-icon>
+    <input type="file" :name="name" @change="onChange" />
+  </view>
+</template>
+
+<script lang="ts">
+import { computed } from 'vue';
+import { createComponent } from '@/utils/create';
+import Icon from '@/packages/icon/index.vue';
+const { componentName, create } = createComponent('uploader');
+
+export default create({
+  props: {
+    name: { type: String, default: 'file' },
+    url: { type: String, default: '' },
+    defaultFileList: { type: Array, default: [] },
+    fileList: { type: Array, default: [] },
+    isPreview: { type: Boolean, default: true },
+    isDeletable: { type: Boolean, default: true },
+    method: { type: String, default: 'post' },
+    capture: { type: String, default: '' },
+    maxSize: { type: [Number, String], default: 1024 * 1024 * 5 },
+    maxCount: { type: [Number, String], default: 1 },
+    clearInput: { type: Boolean, default: false },
+    acceptType: { type: String, default: '' },
+    headers: { type: Object, default: {} },
+    formData: { type: Object, default: {} },
+    uploadIcon: { type: String, default: 'photograph' },
+    xhrState: { type: [Number, String], default: 200 },
+    withCredentials: { type: Boolean, default: false },
+    multiple: { type: Boolean, default: false },
+    disabled: { type: Boolean, default: false },
+    beforeUpload: { type: Function },
+    beforeDelete: { type: Function },
+    onChange: { type: Function },
+    customRequest: { type: Function }
+  },
+  components: {
+    [Icon.name]: Icon
+  },
+  emits: ['start', 'progress', 'oversize', 'success', 'failure', 'on-change'],
+  setup(props, { emit }) {
+    console.log(props);
+    const classes = computed(() => {
+      const prefixCls = componentName;
+      return {
+        [prefixCls]: true
+      };
+    });
+
+    const onChange = (event: Event) => {
+      emit('on-change', event);
+    };
+
+    return {
+      onChange,
+      classes
+    };
+  }
+});
+</script>
+
+<style lang="scss">
+@import 'index.scss';
+</style>

+ 1 - 1
src/sites/doc/components/Header.vue

@@ -19,7 +19,7 @@
         <ul class="nav-list">
           <li class="nav-item nav-item-actie">指南</li>
           <li class="nav-item">组件</li>
-          <li class="nav-item">示例</li>
+          <li class="nav-item"><a href="http://localhost:8080/demo.html#/" style="color:#fff">示例</a></li>
           <li class="nav-item">资源</li>
           <li class="nav-item">
             <div

+ 6 - 0
src/sites/mobile/App.vue

@@ -68,9 +68,15 @@ export default defineComponent({
   .demo {
     height: 100%;
     background: #f7f8fa;
+    overflow-x: hidden;
     overflow-y: auto;
     padding: 0 25px;
     padding-top: 57px;
+
+    &.bg-w {
+      background: #fff;
+    }
+
     &::-webkit-scrollbar {
       width: 0;
       background: transparent;

+ 92 - 0
src/styles/font/demo_index.html

@@ -31,6 +31,30 @@
           <ul class="icon_lists dib-box">
           
             <li class="dib">
+              <span class="icon nutui-iconfont">&#xe69c;</span>
+                <div class="name">github</div>
+                <div class="code-name">&amp;#xe69c;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon nutui-iconfont">&#xe69b;</span>
+                <div class="name">uplode</div>
+                <div class="code-name">&amp;#xe69b;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon nutui-iconfont">&#xe69a;</span>
+                <div class="name">mask-close</div>
+                <div class="code-name">&amp;#xe69a;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon nutui-iconfont">&#xe699;</span>
+                <div class="name">circle-close</div>
+                <div class="code-name">&amp;#xe699;</div>
+              </li>
+          
+            <li class="dib">
               <span class="icon nutui-iconfont">&#xe698;</span>
                 <div class="name">right</div>
                 <div class="code-name">&amp;#xe698;</div>
@@ -441,6 +465,42 @@
         <ul class="icon_lists dib-box">
           
           <li class="dib">
+            <span class="icon nutui-iconfont nut-icon-github"></span>
+            <div class="name">
+              github
+            </div>
+            <div class="code-name">.nut-icon-github
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon nutui-iconfont nut-icon-uplode"></span>
+            <div class="name">
+              uplode
+            </div>
+            <div class="code-name">.nut-icon-uplode
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon nutui-iconfont nut-icon-mask-close"></span>
+            <div class="name">
+              mask-close
+            </div>
+            <div class="code-name">.nut-icon-mask-close
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon nutui-iconfont nut-icon-circle-close"></span>
+            <div class="name">
+              circle-close
+            </div>
+            <div class="code-name">.nut-icon-circle-close
+            </div>
+          </li>
+          
+          <li class="dib">
             <span class="icon nutui-iconfont nut-icon-right"></span>
             <div class="name">
               right
@@ -1011,6 +1071,38 @@
           
             <li class="dib">
                 <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#nut-icon-github"></use>
+                </svg>
+                <div class="name">github</div>
+                <div class="code-name">#nut-icon-github</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#nut-icon-uplode"></use>
+                </svg>
+                <div class="name">uplode</div>
+                <div class="code-name">#nut-icon-uplode</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#nut-icon-mask-close"></use>
+                </svg>
+                <div class="name">mask-close</div>
+                <div class="code-name">#nut-icon-mask-close</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#nut-icon-circle-close"></use>
+                </svg>
+                <div class="name">circle-close</div>
+                <div class="code-name">#nut-icon-circle-close</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
                   <use xlink:href="#nut-icon-right"></use>
                 </svg>
                 <div class="name">right</div>

File diff suppressed because it is too large
+ 22 - 6
src/styles/font/iconfont.css


BIN
src/styles/font/iconfont.eot


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


+ 28 - 0
src/styles/font/iconfont.json

@@ -6,6 +6,34 @@
   "description": "nutui 3.0字体管理",
   "glyphs": [
     {
+      "icon_id": "18193891",
+      "name": "github",
+      "font_class": "github",
+      "unicode": "e69c",
+      "unicode_decimal": 59036
+    },
+    {
+      "icon_id": "18193367",
+      "name": "uplode",
+      "font_class": "uplode",
+      "unicode": "e69b",
+      "unicode_decimal": 59035
+    },
+    {
+      "icon_id": "18193248",
+      "name": "mask-close",
+      "font_class": "mask-close",
+      "unicode": "e69a",
+      "unicode_decimal": 59034
+    },
+    {
+      "icon_id": "18193089",
+      "name": "circle-close",
+      "font_class": "circle-close",
+      "unicode": "e699",
+      "unicode_decimal": 59033
+    },
+    {
       "icon_id": "18117145",
       "name": "right",
       "font_class": "right",

File diff suppressed because it is too large
+ 12 - 0
src/styles/font/iconfont.svg


BIN
src/styles/font/iconfont.ttf


BIN
src/styles/font/iconfont.woff


BIN
src/styles/font/iconfont.woff2


+ 12 - 0
src/styles/variables.scss

@@ -86,3 +86,15 @@ $cell-title-font: $font-size-2;
 $cell-title-desc-font: $font-size-1;
 $cell-desc-font: $font-size-2;
 $cell-desc-color: $disable-color;
+
+// icon
+
+$icon-height: 20px;
+$icon-width: 20px;
+$icon-line-height: 20px;
+
+// uploader
+
+$uploader-width: 100px;
+$uploader-height: 100px;
+$uploader-babackground: #f7f8fa;

+ 1 - 1
tsconfig.json

@@ -32,7 +32,7 @@
     "src/**/*.tsx",
     "src/**/*.vue",
     "tests/**/*.ts",
-    "tests/**/*.tsx"
+    "tests/**/*.tsx", "src/config.js"
   ],
   "exclude": [
     "node_modules"