Browse Source

Merge branch 'next' of https://github.com/jdf2e/nutui into next

guoxiaoxiao8 5 years ago
parent
commit
d44513b981

+ 0 - 1
.prettierrc

@@ -3,7 +3,6 @@
   "semi": true,
   "bracketSpacing": true,
   "tabWidth": 2,
-  "printWidth": 300,
   "useTabs": false,
   "htmlWhitespaceSensitivity": "strict",
   "trailingComma": "none"

+ 14 - 0
src/config.ts

@@ -207,6 +207,20 @@ export const nav = [
     ]
   },
   {
+    name: '数据录入',
+    packages: [
+      {
+        name: 'InputNumber',
+        sort: 1,
+        cName: '数字输入框',
+        type: 'component',
+        show: true,
+        desc: '数字输入框组件',
+        author: 'szg2008'
+      }
+    ]
+  },
+  {
     name: '业务组件',
     packages: []
   }

+ 18 - 9
src/packages/avatar/demo.vue

@@ -1,19 +1,19 @@
 <template>
-  <div class="demo">
+  <div class="demo avatar-demo">
     <h2>默认用法</h2>
     <!-- <p>内置"small","normal","large"三种尺寸规格</p> -->
     <nut-cell>
-      <nut-avatar size="80"></nut-avatar>
-      <nut-avatar size="large"></nut-avatar>
-      <nut-avatar size="normal"></nut-avatar>
-      <nut-avatar size="small"></nut-avatar>
+      <nut-avatar size="80" bg-icon bg-image="https://img12.360buyimg.com/imagetools/jfs/t1/143702/31/16654/116794/5fc6f541Edebf8a57/4138097748889987.png"></nut-avatar>
+      <nut-avatar size="large" bg-icon bg-image="https://img12.360buyimg.com/imagetools/jfs/t1/143702/31/16654/116794/5fc6f541Edebf8a57/4138097748889987.png"></nut-avatar>
+      <nut-avatar size="normal" bg-icon bg-image="https://img12.360buyimg.com/imagetools/jfs/t1/143702/31/16654/116794/5fc6f541Edebf8a57/4138097748889987.png"></nut-avatar>
+      <nut-avatar size="small" bg-icon bg-image="https://img12.360buyimg.com/imagetools/jfs/t1/143702/31/16654/116794/5fc6f541Edebf8a57/4138097748889987.png"></nut-avatar>
     </nut-cell>
-    <h2>修改形状类型</h2>
+    <!-- <h2>修改形状类型</h2>
     <nut-cell>
       <nut-avatar size="large" shape="square"></nut-avatar>
       <nut-avatar size="normal" shape="square"></nut-avatar>
       <nut-avatar size="small" shape="square"></nut-avatar>
-    </nut-cell>
+    </nut-cell> -->
     <h2>修改背景色</h2>
     <nut-cell>
       <nut-avatar bg-color="#FA2C19"></nut-avatar>
@@ -37,12 +37,10 @@ import { createComponent } from '@/utils/create';
 const { createDemo } = createComponent('avatar');
 export default createDemo({
   props: {},
-
   setup() {
     const activeAvatar = (event: Event) => {
       console.log('点击了头像', event);
     };
-
     return {
       activeAvatar
     };
@@ -50,7 +48,18 @@ export default createDemo({
 });
 </script>
 <style lang="scss" scoped>
+#app {
+  .avatar-demo {
+    padding-left: 0;
+    padding-right: 0;
+    & > h2 {
+      padding: 0 25px;
+    }
+  }
+}
+
 .nut-cell {
   align-items: flex-end;
+  border-radius: 0;
 }
 </style>

+ 2 - 2
src/packages/avatar/doc.md

@@ -8,10 +8,10 @@
 
 ``` javascript
 import { createApp } from 'vue';
-import { Price } from '@nutui/nutui';
+import { Avatar } from '@nutui/nutui';
 
 const app = createApp();
-app.use(Price);
+app.use(Avatar);
 
 ```
 

+ 1 - 0
src/packages/avatar/index.scss

@@ -5,6 +5,7 @@
   display: inline-block;
   position: relative;
   margin-right: 24px;
+  flex: 0 0 auto; // 防止被压缩
   .icon {
     width: 50%;
     height: 50%;

File diff suppressed because it is too large
+ 1 - 5
src/packages/avatar/index.vue


+ 106 - 0
src/packages/inputnumber/demo.vue

@@ -0,0 +1,106 @@
+<template>
+  <div class="demo">
+    <h2>基本用法</h2>
+    <nut-cell>
+      <nut-inputnumber
+        v-model:modelValue="state.val1"
+        @change="change"
+        @blur="blur"
+        @focus="focus"
+      />
+    </nut-cell>
+    <h2>步长设置</h2>
+    <nut-cell>
+      <nut-inputnumber
+        v-model:modelValue="state.val2"
+        :step="state.step"
+        :decimal-places="1"
+      />
+    </nut-cell>
+    <h2>限制输入范围</h2>
+    <nut-cell>
+      <nut-inputnumber
+        v-model:modelValue="state.val3"
+        :min="state.min"
+        :max="state.max"
+        @reduce-no-allow="reduceNoAllow"
+        @add-no-allow="addNoAllow"
+      />
+    </nut-cell>
+    <h2>禁用操作&输入框</h2>
+    <nut-cell>
+      <nut-inputnumber
+        :readonly="true"
+        v-model:modelValue="state.val4"
+        min="0"
+        max="0"
+        @focus="focus"
+        @blur="blur"
+      />
+    </nut-cell>
+    <h2>支持异步修改(点击+/-,手动改成了3)</h2>
+    <nut-cell>
+      <nut-inputnumber
+        :async="state.async"
+        @change="handleChangeAsync"
+        v-model:modelValue="state.val5"
+      />
+    </nut-cell>
+  </div>
+</template>
+
+<script lang="ts">
+import { reactive, onMounted } from 'vue';
+import { createComponent } from '@/utils/create';
+const { createDemo } = createComponent('inputnumber');
+export default createDemo({
+  props: {},
+  setup() {
+    const state = reactive({
+      val1: 2,
+      val2: 1.1,
+      val3: 3,
+      val4: 0,
+      val5: 1,
+      step: 1.1,
+      min: 3,
+      max: 100,
+      async: true
+    });
+    onMounted(() => {
+      state.max = 5;
+    });
+    const change = (num: string | number) => {
+      console.log('change: ', num);
+    };
+    const blur = (e: Event, num: string | number) => {
+      console.log('blur: ', num);
+    };
+    const focus = (e: Event, num: string | number) => {
+      console.log('focus: ', e, num);
+    };
+    const addNoAllow = () => {
+      alert('超出最大限制数');
+    };
+    const reduceNoAllow = () => {
+      alert('超出最小限制数');
+    };
+    const handleChangeAsync = (num: number) => {
+      setTimeout(() => {
+        state.val5 = 3;
+      }, 1000);
+    };
+    return {
+      state,
+      change,
+      blur,
+      focus,
+      reduceNoAllow,
+      addNoAllow,
+      handleChangeAsync
+    };
+  }
+});
+</script>
+
+<style lang="scss" scoped></style>

+ 94 - 0
src/packages/inputnumber/doc.md

@@ -0,0 +1,94 @@
+# InputNumber 数字输入框组件
+
+### 介绍
+
+基于
+
+### 安装
+
+``` javascript
+import { createApp } from 'vue';
+import { inputnumber } from '@nutui/nutui';
+
+const app = createApp();
+app.use(inputnumber);
+
+```
+
+## 代码演示
+
+### 基础用法1
+
+初始化一个默认值
+
+```html
+<nut-inputnumber v-model:modelValue="1" />
+```
+
+### 基础用法2
+
+设置步长`step` 和 保留的小数位`decimalPlaces`
+
+```html
+<nut-inputnumber v-model:modelValue="1" :step="1.1" :decimalPlaces="1" />
+```
+
+### 基础用法3
+
+`min` 和 `max` 属性分别表示最小值和最大值
+
+```html
+<nut-inputnumber v-model:modelValue="1" :min="3" :max="5" />
+```
+
+### 基础用法4
+
+`readonly`设置只读
+
+```html
+<nut-inputnumber v-model:modelValue="1" :readonly="true" />
+```
+
+### 基础用法5
+
+`size`设置操作符的大小
+
+```html
+<nut-inputnumber v-model:modelValue="1" :size="20px" />
+```
+
+### 高级用法
+
+`async`支持异步修改数量,设置了此属性为true,必须同时在`change`事件中手动设置input值才能生效
+
+```html
+<nut-inputnumber v-model:modelValue="1" :async="true" @change="change"/>
+```
+
+
+## API
+
+### Props
+
+| 参数         | 说明                             | 类型   | 默认值           |
+|--------------|----------------------------------|--------|------------------|
+| size         | 操作符+、-尺寸               | String          | 20px                |
+| color        | 操作符+、-颜色               | String          | #1a1a1a             |
+| dis-color     | 操作符+、-禁用时颜色          | String          | #ccc                |
+| min          | 最小值                      | String、Number | 1                   |
+| max          | 最大值                      | String、Number | Infinity             |
+| step         | 步长                        | String、Number |     1                |
+| readonly     | 只读                   | Boolean | false              |
+| modelValue   | 初始值                   | String、Number | ''              |
+| decimal-places| 设置保留的小数位                   | String、Number | 1              |
+| async        | 支持异步                   | Boolean | false              |
+
+### Events
+
+| 事件名 | 说明           | 回调参数     |
+|--------|----------------|--------------|
+| change  | 值改变时触发 | num: string | number |
+| focus  | 输入框获取焦点时触发 | event: Event, num: string | number |
+| blur  | 输入框失去焦点时触发 | event: Event, num: string | number |
+| add-no-allow  | 超出最大事件回调 | - |
+| reduce-no-allow  | 超出最小事件回调 | - |

+ 21 - 0
src/packages/inputnumber/index.scss

@@ -0,0 +1,21 @@
+.nut-inputnumber {
+  display: flex;
+  align-items: center;
+  input {
+    width: $inputnumber-input-width;
+    height: $inputnumber-input-height;
+    text-align: center;
+    outline: none;
+    border: 0;
+    font-family: initial;
+    background-color: $inputnumber-input-background-color;
+    border-radius: $inputnumber-input-border-radius;
+    &:read-only {
+      color: $disable-color;
+    }
+  }
+  input::-webkit-outer-spin-button,
+  input::-webkit-inner-spin-button {
+    appearance: none;
+  }
+}

+ 322 - 0
src/packages/inputnumber/index.vue

@@ -0,0 +1,322 @@
+<template>
+  <view :class="classes">
+    <nut-icon
+      name="minus"
+      :size="size"
+      :color="getIconColor('minus')"
+      @click="reduce"
+    ></nut-icon>
+    <input
+      type="number"
+      :min="state.minVal"
+      :max="max"
+      :readonly="readonly"
+      :value="state.num"
+      @input="numChange"
+      @blur="blur"
+      @focus="focus"
+    />
+    <nut-icon
+      name="plus"
+      :size="size"
+      :color="getIconColor('plus')"
+      @click="add"
+    ></nut-icon>
+  </view>
+</template>
+<script lang="ts">
+import { computed, reactive, watch, toRefs } from 'vue';
+import { createComponent } from '@/utils/create';
+const { componentName, create } = createComponent('inputnumber');
+
+export default create({
+  props: {
+    size: {
+      type: [String],
+      default: '20px'
+    },
+    color: {
+      type: String,
+      default: '#1a1a1a'
+    },
+    disColor: {
+      type: String,
+      default: '#ccc'
+    },
+    min: {
+      type: [Number, String],
+      default: 0
+    },
+    max: {
+      type: [Number, String],
+      default: Infinity
+    },
+    step: {
+      type: [Number, String],
+      default: 1
+    },
+    readonly: {
+      type: Boolean,
+      default: false
+    },
+    modelValue: {
+      type: [String, Number],
+      default: ''
+    },
+    decimalPlaces: {
+      type: [String, Number],
+      default: 0
+    },
+    async: {
+      type: Boolean,
+      default: false
+    }
+  },
+  emits: [
+    'update:modelValue',
+    'change',
+    'blur',
+    'focus',
+    'reduce',
+    'reduce-no-allow',
+    'add',
+    'add-no-allow'
+  ],
+
+  setup(props, { emit }) {
+    interface Events {
+      eventName:
+        | 'update:modelValue'
+        | 'change'
+        | 'focus'
+        | 'blur'
+        | 'add-no-allow'
+        | 'reduce-no-allow';
+      params: (string | number | Event)[];
+    }
+    const { modelValue, min, max, step } = toRefs(props);
+    const state = reactive({
+      num: !modelValue.value ? min.value : modelValue.value,
+      minVal: min.value,
+      tempVal: '',
+      focusing: false
+    });
+    const format = (v: string | number): string | number => {
+      if (v > max.value) {
+        v = max.value;
+      }
+      if (v < state.minVal) {
+        v = state.minVal;
+      }
+
+      return v;
+    };
+
+    const fixedDecimalPlaces = (v: string | number): string => {
+      return Number(v).toFixed(Number(props.decimalPlaces));
+    };
+
+    const getIconColor = (type: 'minus' | 'plus') => {
+      if (type === 'minus') {
+        return (state.focusing ? Number(state.tempVal) : Number(state.num)) -
+          Number(step.value) <
+          min.value
+          ? props.disColor
+          : props.color;
+      } else if (type === 'plus') {
+        return Number(state.num) > Number(max.value) - Number(step.value)
+          ? props.disColor
+          : props.color;
+      } else {
+        throw new Error('type is not be supported~');
+      }
+    };
+
+    const emitChange = (envs: Events[]) => {
+      envs.forEach(item => {
+        emit(item.eventName, ...item.params);
+      });
+    };
+
+    const classes = computed(() => {
+      const prefixCls = componentName;
+      return {
+        [prefixCls]: true
+      };
+    });
+
+    const numChange = (e: Event) => {
+      const input = e.target as HTMLInputElement;
+      let val = input.value;
+      val = String(format(val));
+      input.value = val;
+      state.num = val;
+      emitChange([
+        {
+          eventName: 'update:modelValue',
+          params: [state.num]
+        },
+        {
+          eventName: 'change',
+          params: [state.num]
+        }
+      ]);
+    };
+
+    const focus = (e: Event) => {
+      if (props.readonly) return;
+      const val = String(state.num);
+      state.tempVal = val;
+      state.minVal = '';
+      state.focusing = true;
+      emitChange([
+        {
+          eventName: 'focus',
+          params: [e, state.num]
+        }
+      ]);
+    };
+
+    const blur = (e: Event) => {
+      if (props.async) {
+        emitChange([
+          {
+            eventName: 'change',
+            params: ['']
+          }
+        ]);
+        return;
+      }
+      const val = (e.target as HTMLInputElement).value;
+      state.minVal = String(min.value);
+      state.num = val ? format(val) : state.tempVal;
+      state.focusing = false;
+      emitChange([
+        {
+          eventName: 'update:modelValue',
+          params: [state.num]
+        },
+        {
+          eventName: 'blur',
+          params: [e, state.num]
+        }
+      ]);
+    };
+
+    const reduce = () => {
+      if (props.async) {
+        emitChange([
+          {
+            eventName: 'change',
+            params: [state.num]
+          }
+        ]);
+        return;
+      }
+      if (getIconColor('minus') === props.color) {
+        const [n1, n2] = fixedDecimalPlaces(
+          Number(state.num) - Number(props.step)
+        ).split('.');
+        const fixedLen = n2 ? n2.length : 0;
+        state.num = parseFloat(n1 + (n2 ? `.${n2}` : '')).toFixed(fixedLen);
+        emitChange([
+          {
+            eventName: 'update:modelValue',
+            params: [state.num]
+          },
+          {
+            eventName: 'change',
+            params: [state.num]
+          }
+        ]);
+      } else {
+        emitChange([
+          {
+            eventName: 'reduce-no-allow',
+            params: []
+          }
+        ]);
+      }
+    };
+
+    const add = () => {
+      if (props.async) {
+        emitChange([
+          {
+            eventName: 'change',
+            params: [state.num]
+          }
+        ]);
+        return;
+      }
+      if (getIconColor('plus') === props.color) {
+        const [n1, n2] = fixedDecimalPlaces(
+          Number(state.num) + Number(props.step)
+        ).split('.');
+        const fixedLen = n2 ? n2.length : 0;
+        state.num = parseFloat(n1 + (n2 ? '.' + n2 : '')).toFixed(fixedLen);
+        emitChange([
+          {
+            eventName: 'update:modelValue',
+            params: [state.num]
+          },
+          {
+            eventName: 'change',
+            params: [state.num]
+          }
+        ]);
+      } else {
+        emitChange([
+          {
+            eventName: 'add-no-allow',
+            params: []
+          }
+        ]);
+      }
+    };
+
+    watch(
+      () => min.value,
+      newValues => {
+        if (newValues < Number(max.value)) {
+          state.minVal = newValues;
+          const val = format(state.num);
+          state.num = val > 0 ? fixedDecimalPlaces(val) : val;
+        }
+      }
+    );
+
+    watch(
+      () => modelValue.value,
+      newValues => {
+        const val = format(newValues);
+        state.num = val > 0 ? fixedDecimalPlaces(val) : val;
+        emitChange([
+          {
+            eventName: 'change',
+            params: [state.num]
+          }
+        ]);
+      }
+    );
+
+    return {
+      state,
+      classes,
+      format,
+      getIconColor,
+      fixedDecimalPlaces,
+      emitChange,
+      numChange,
+      blur,
+      focus,
+      reduce,
+      add
+    };
+  }
+});
+</script>
+
+<style lang="scss">
+@import 'index.scss';
+</style>

+ 16 - 4
src/packages/navbar/index.vue

@@ -6,15 +6,27 @@
         <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
+        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
+        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>

+ 51 - 28
src/packages/tab/index.vue

@@ -20,16 +20,20 @@
   </div>
 </template>
 <script lang="ts">
-// @ts-nocheck
-import { PropType, h, toRefs, reactive, computed, ref, onMounted, nextTick, watch, watchEffect } from 'vue';
+import { PropType, reactive, ref, onMounted, watch, VNode } from 'vue';
 import { createComponent } from '@/utils/create';
 const { create } = createComponent('tab');
 import TabTitle from './tabTitle';
 import Swiper from 'swiper';
-// import 'swiper/swiper-bundle.css';
 import 'swiper/dist/css/swiper.min.css';
+import { extend } from '@vue/shared';
 type TabDirection = 'horizontal' | 'vertical';
 
+interface DataTitle {
+  title?: string;
+  content?: VNode[];
+}
+
 export default create({
   props: {
     defaultIndex: {
@@ -53,31 +57,40 @@ export default create({
     TabTitle
   },
   setup(props, ctx) {
-    const titles: any = reactive([]);
-    let mySwiper: any = reactive({});
+    const titles: Array<DataTitle> = reactive([]);
+    let mySwiper = reactive({});
     const isLock = ref(false);
     const activeIndex = ref(props.defaultIndex);
-    const navlist: any = ref(null);
+    const navlist = ref<null | HTMLElement>(null);
     // 生成随机的id
     function createHash() {
-      return Array.from(Array(10), () => Math.floor(Math.random() * 36).toString(36)).join('');
+      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);
+      if (navlist.value) {
+        const currEle = navlist.value.querySelectorAll('.tab-title-box')[
+          index
+        ] as HTMLElement;
+        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);
+        }
       }
     }
 
@@ -85,7 +98,7 @@ export default create({
     function switchTitle(index: number) {
       activeIndex.value = index;
       centerTitle(index);
-      mySwiper.slideToLoop(index, props.animatedTime, false);
+      (mySwiper as Swiper).slideToLoop(index, props.animatedTime, false);
     }
     function initSwiper(currIndex: number) {
       mySwiper = new Swiper('.' + swiperClassName.value, {
@@ -100,11 +113,11 @@ export default create({
           touchStart: function() {
             isLock.value = true;
           },
-          transitionEnd: function(swiper: Swiper): void {
-            ctx.emit('switchTab', swiper.realIndex, swiper);
+          transitionEnd: function(): void {
+            ctx.emit('switchTab', (mySwiper as Swiper).realIndex, mySwiper);
             if (isLock.value) {
-              activeIndex.value = swiper.realIndex;
-              centerTitle(swiper.realIndex);
+              activeIndex.value = (mySwiper as Swiper).realIndex;
+              centerTitle((mySwiper as Swiper).realIndex);
             }
           }
         }
@@ -113,12 +126,22 @@ export default create({
     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();
+        const slots: VNode[] =
+          ctx.slots.default().length === 1
+            ? (ctx.slots.default()[0].children as VNode[])
+            : 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
+              title:
+                item.props && item.props['tab-title']
+                  ? item.props['tab-title']
+                  : '',
+              // @ts-ignore:已经做了header是否存在的判断
+              content:
+                item && item?.children?.header
+                  ? (item.children as VNode[]).header()
+                  : null
             });
           });
       }
@@ -130,7 +153,7 @@ export default create({
       initTitle();
     });
     watch(
-      () => ctx.slots.default(),
+      () => (ctx.slots.default ? ctx.slots.default() : ''),
       () => {
         initTitle();
       }

+ 46 - 2
src/packages/uploader/demo.vue

@@ -1,15 +1,59 @@
 <template>
   <div class="demo bg-w">
     <h2>基础用法</h2>
-    <nut-uploader></nut-uploader>
+    <nut-uploader :url="uploadUrl"></nut-uploader>
+    <h2>上传状态</h2>
+    <nut-uploader
+      :url="uploadUrl"
+      multiple
+      @on-delete="onDelete"
+    ></nut-uploader>
+    <h2>限制上传数量5个</h2>
+    <nut-uploader :url="uploadUrl" multiple max-count="5"></nut-uploader>
+    <h2>限制上传大小(每个文件最大不超过 50kb)</h2>
+    <nut-uploader
+      :url="uploadUrl"
+      multiple
+      :max-size="1024 * 50"
+      @oversize="onOversize"
+    ></nut-uploader>
+    <h2>自定义数据 FormData 、 headers </h2>
+    <nut-uploader
+      :url="uploadUrl"
+      :form-data="formData"
+      :headers="formData"
+      :with-Credentials="true"
+    ></nut-uploader>
+    <h2>禁用状态</h2>
+    <nut-uploader disabled></nut-uploader>
   </div>
 </template>
 
 <script lang="ts">
 import { createComponent } from '@/utils/create';
+import { FileItem } from './index.vue';
 const { createDemo } = createComponent('uploader');
 export default createDemo({
-  props: {}
+  setup() {
+    const uploadUrl =
+      'https://my-json-server.typicode.com/linrufeng/demo/posts';
+
+    const formData = {
+      custom: 'test'
+    };
+    const onOversize = (files: File[]) => {
+      console.log('oversize 触发 文件大小不能超过 50kb', files);
+    };
+    const onDelete = (file: FileItem, fileList: FileItem[]) => {
+      console.log('on-delete 事件触发', file, fileList);
+    };
+    return {
+      onOversize,
+      onDelete,
+      uploadUrl,
+      formData
+    };
+  }
 });
 </script>
 

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

@@ -19,44 +19,102 @@ app.use(Uploader);
 
 ### 基本用法
 
+``` html
+<nut-uploader url="http://服务器地址"></nut-uploader>
+```
+### 限制上传数量5个
+
+``` html
+<nut-uploader url="http://服务器地址" multiple max-count="5"></nut-uploader>
+```
+### 限制上传大小(每个文件最大不超过 50kb)
+
+``` html
+<nut-uploader url="http://服务器地址" multiple :max-size="1024 * 50" @oversize="onOversize"></nut-uploader>
+```
+
+``` javascript
+setup() {
+    const formData = {
+      custom: 'test'
+    };
+    const onOversize = (files: File[]) => {
+      console.log('oversize 触发 文件大小不能超过 50kb', files);
+    };
+   
+    return {
+      onOversize,
+      formData
+    };
+}
+```
+
+### 自定义 FormData headers
+
+``` html
+<nut-uploader url="http://服务器地址" :form-data="formData" :headers="formData" :with-Credentials="true"></nut-uploader>
+```
+
+``` javascript
+setup() {
+    const formData = {
+      custom: 'test'
+    };
+    const onOversize = (files: File[]) => {
+      console.log('oversize 触发 文件大小不能超过 50kb', files);
+    };
+   
+    return {
+      onOversize,
+      formData
+    };
+}
+```
+
+### 禁用状态
+
+``` html
+<nut-uploader disabled></nut-uploader>
+```
+
 ## API
 
 ### Prop
 
-| 字段 | 说明 | 类型 | 默认值
-|----- | ----- | ----- | ----- 
-| name | `input` 标签 `name` 的名称,发到后台的文件参数名 | String | "file"
-| url | 上传服务器的接口地址 | String | -
-| 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
-| withCredentials | 支持发送 cookie 凭证信息 | Boolean | fasle
-| 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  | -
+| 字段              | 说明                                                                                                                                                                                   | 类型                              | 默认值      |
+|-------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------|-------------|
+| name              | `input` 标签 `name` 的名称,发到后台的文件参数名                                                                                                                                       | String                            | "file"      |
+| url               | 上传服务器的接口地址                                                                                                                                                                   | String                            | -           |
+| 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                    | -           |
+| 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  |
+| xhr-state         | 接口响应的成功状态(status)值                                                                                                                                                         | Number                            | 200         |
+| with-credentials  | 支持发送 cookie 凭证信息                                                                                                                                                               | Boolean                           | fasle       |
+| multiple          | 是否支持文件多选                                                                                                                                                                       | Boolean                           | fasle       |
+| disabled          | 是否禁用文件上传                                                                                                                                                                       | Boolean                           | fasle       |
+| before-upload     | 上传前的函数需要返回一个对象                                                                                                                                                           | Function                          | input files |
+| before-delete     | 除文件时的回调,返回值为 false 时不移除。支持返回一个 Promise 对象,Promise 对象 resolve(false) 或 reject 时不移除                                                                     | Function(file): boolean 丨Promise | -           |
+| on-change         | 上传文件改变时的状态,详见                                                                                                                                                             | Function(fileList) 丨 Promise     | -           |
+| custom-request    | 通过覆盖默认的上传行为,可以自定义自己的上传实现                                                                                                                                       | Function                          | -           |
 
 ### Event
 
-| 名称 | 说明 | 回调参数 
-|----- | ----- | ----- 
-| start | 文件上传开始 | -
-| progress | 文件上传的进度 | 上传文件、已上传数据量、总数据量
-| oversize | 	文件大小超过限制时触发 | fileItem
-| success | 上传成功 | fileList
-| failure | 上传失败 | fileList
+| 名称     | 说明                   | 回调参数                         |
+|----------|------------------------|----------------------------------|
+| start    | 文件上传开始           | -                                |
+| progress | 文件上传的进度         | 上传文件、已上传数据量、总数据量 |
+| oversize | 文件大小超过限制时触发 | files                            |
+| success  | 上传成功               | fileList                         |
+| failure  | 上传失败               | fileList                         |
 

+ 60 - 16
src/packages/uploader/index.scss

@@ -1,20 +1,64 @@
 .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;
+  display: flex;
+  flex-wrap: wrap;
+
+  .upload {
+    position: relative;
+    background: $uploader-babackground;
+    width: $uploader-width;
+    height: $uploader-height;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    input {
+      position: absolute;
+      top: 0;
+      left: 0;
+      width: 100%;
+      height: 100%;
+      overflow: hidden;
+      cursor: pointer;
+      opacity: 0;
+      &:disabled {
+        cursor: not-allowed;
+      }
+    }
+  }
+  .preview {
+    width: $uploader-width;
+    height: $uploader-height;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    margin-right: 10px;
+    margin-bottom: 10px;
+    &-img {
+      position: relative;
+      width: 100%;
+      height: 100%;
+      .close {
+        position: absolute;
+        right: 0;
+        top: 0;
+        transform: translate(50%, -50%);
+      }
+      .tips {
+        position: absolute;
+        bottom: 0;
+        left: 0;
+        right: 0;
+        font-size: 12px;
+        color: $white;
+        height: 30px;
+        line-height: 30px;
+        text-align: c;
+        background: rgba(0, 0, 0, 0.54);
+      }
+    }
+    img {
+      height: 100%;
+      width: 100%;
+    }
   }
 }

+ 201 - 14
src/packages/uploader/index.vue

@@ -1,30 +1,69 @@
 <template>
   <view :class="classes">
-    <nut-icon color="#808080" :name="uploadIcon"></nut-icon>
-    <input type="file" :name="name" @change="onChange" />
+    <view class="preview" v-for="item in fileList" :key="item.uid">
+      <view class="preview-img">
+        <nut-icon
+          v-if="isDeletable"
+          color="rgba(0,0,0,0.6)"
+          @click="onDelete(item, index)"
+          class="close"
+          name="mask-close"
+        ></nut-icon>
+        <img v-if="item.type.includes('image') && item.url" :src="item.url" />
+        <view class="tips" v-if="item.status != 'success'">{{
+          item.status
+        }}</view>
+      </view>
+    </view>
+    <view class="upload" v-if="maxCount - fileList.length">
+      <nut-icon color="#808080" :name="uploadIcon"></nut-icon>
+      <input
+        type="file"
+        :capture="capture"
+        :accept="acceptType"
+        :multiple="multiple"
+        :name="name"
+        :disabled="disabled"
+        @change="onChange"
+      />
+    </view>
   </view>
 </template>
 
 <script lang="ts">
-import { computed } from 'vue';
+import { computed, reactive } from 'vue';
 import { createComponent } from '@/utils/create';
 import Icon from '@/packages/icon/index.vue';
+import { Uploader, UploadOptions } from './uploader';
 const { componentName, create } = createComponent('uploader');
-
+export type FileItemStatus =
+  | 'ready'
+  | 'uploading'
+  | 'success'
+  | 'error'
+  | 'removed';
+export class FileItem {
+  status: FileItemStatus = 'ready';
+  uid: string = new Date().getTime().toString();
+  name?: string;
+  url?: string;
+  type?: string;
+  formData: FormData = new FormData();
+}
 export default create({
   props: {
     name: { type: String, default: 'file' },
     url: { type: String, default: '' },
-    defaultFileList: { type: Array, default: [] },
-    fileList: { type: Array, default: [] },
+    defaultFileList: { type: Array, default: () => new Array<FileItem>() },
+    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 },
+    maxSize: { type: [Number, String], default: Number.MAX_VALUE },
     maxCount: { type: [Number, String], default: 1 },
     clearInput: { type: Boolean, default: false },
-    acceptType: { type: String, default: '' },
+    acceptType: { type: String, default: '*' },
     headers: { type: Object, default: {} },
     formData: { type: Object, default: {} },
     uploadIcon: { type: String, default: 'photograph' },
@@ -32,17 +71,35 @@ export default create({
     withCredentials: { type: Boolean, default: false },
     multiple: { type: Boolean, default: false },
     disabled: { type: Boolean, default: false },
-    beforeUpload: { type: Function },
-    beforeDelete: { type: Function },
+    beforeUpload: {
+      type: Function,
+      default: (files: FileList) => {
+        return files;
+      }
+    },
+    beforeDelete: {
+      type: Function,
+      default: (file: FileItem, files: FileItem[]) => {
+        return true;
+      }
+    },
     onChange: { type: Function },
     customRequest: { type: Function }
   },
   components: {
     [Icon.name]: Icon
   },
-  emits: ['start', 'progress', 'oversize', 'success', 'failure', 'on-change'],
+  emits: [
+    'start',
+    'progress',
+    'oversize',
+    'success',
+    'failure',
+    'on-change',
+    'on-delete'
+  ],
   setup(props, { emit }) {
-    console.log(props);
+    const fileList = reactive(props.fileList) as Array<FileItem>;
     const classes = computed(() => {
       const prefixCls = componentName;
       return {
@@ -50,12 +107,142 @@ export default create({
       };
     });
 
-    const onChange = (event: Event) => {
-      emit('on-change', event);
+    const clearInput = (el: HTMLInputElement) => {
+      el.value = '';
+    };
+
+    const executeUpload = (fileItem: FileItem) => {
+      const uploadOption = new UploadOptions();
+      uploadOption.url = props.url;
+      for (const [key, value] of Object.entries(props.formData)) {
+        fileItem.formData.append(key, value);
+      }
+      uploadOption.formData = fileItem.formData;
+      uploadOption.method = props.method;
+      uploadOption.xhrState = props.xhrState as number;
+      uploadOption.headers = props.headers;
+      uploadOption.withCredentials = props.withCredentials;
+      uploadOption.onStart = (option: UploadOptions) => {
+        fileItem.status = 'ready';
+        emit('start', option);
+      };
+      uploadOption.onProgress = (
+        e: ProgressEvent<XMLHttpRequestEventTarget>,
+        option: UploadOptions
+      ) => {
+        fileItem.status = 'uploading';
+        emit('progress', { e, option });
+      };
+
+      uploadOption.onSuccess = (
+        responseText: XMLHttpRequest['responseText'],
+        option: UploadOptions
+      ) => {
+        fileItem.status = 'success';
+        emit('success', {
+          responseText,
+          option
+        });
+      };
+      uploadOption.onFailure = (
+        responseText: XMLHttpRequest['responseText'],
+        option: UploadOptions
+      ) => {
+        fileItem.status = 'error';
+        emit('failure', {
+          responseText,
+          option
+        });
+      };
+      new Uploader(uploadOption).upload();
+    };
+
+    const readFile = (files: File[]) => {
+      files.forEach((file: File) => {
+        const formData = new FormData();
+        formData.append(props.name, file);
+
+        const fileItem = new FileItem();
+        fileItem.name = file.name;
+        fileItem.status = 'uploading';
+        fileItem.type = file.type;
+        fileItem.formData = formData;
+        executeUpload(fileItem);
+
+        if (props.isPreview && file.type.includes('image')) {
+          const reader = new FileReader();
+          reader.onload = (event: ProgressEvent<FileReader>) => {
+            fileItem.url = (event.target as FileReader).result as string;
+            fileList.push(fileItem);
+          };
+          reader.readAsDataURL(file);
+        } else {
+          fileList.push(fileItem);
+        }
+      });
+    };
+
+    const filterFiles = (files: File[]) => {
+      const maxCount = (props.maxCount as number) * 1;
+      const maxSize = (props.maxSize as number) * 1;
+      const oversizes = new Array<File>();
+      files = files.filter((file: File) => {
+        if (file.size > maxSize) {
+          oversizes.push(file);
+          return false;
+        } else {
+          return true;
+        }
+      });
+      if (oversizes.length) {
+        emit('oversize', oversizes);
+      }
+      if (files.length > maxCount) {
+        files.splice(maxCount - 1, files.length - maxCount);
+      }
+      return files;
+    };
+    const onDelete = (file: FileItem, index: number) => {
+      if (props.beforeDelete(file, fileList)) {
+        fileList.splice(index, 1);
+        emit('on-delete', {
+          file,
+          fileList
+        });
+      } else {
+        console.log('用户阻止了删除!');
+      }
+    };
+
+    const onChange = (event: InputEvent) => {
+      if (props.disabled) {
+        return;
+      }
+      const $el = event.target as HTMLInputElement;
+      let { files } = $el;
+
+      if (props.beforeUpload) {
+        files = props.beforeUpload(files);
+      }
+
+      const _files: File[] = filterFiles(new Array<File>().slice.call(files));
+
+      readFile(_files);
+
+      if (props.clearInput) {
+        clearInput($el);
+      }
+
+      emit('on-change', {
+        fileList,
+        event
+      });
     };
 
     return {
       onChange,
+      onDelete,
+      fileList,
       classes
     };
   }

+ 50 - 0
src/packages/uploader/uploader.ts

@@ -0,0 +1,50 @@
+export class UploadOptions {
+  url = '';
+  formData: FormData = new FormData();
+  method = 'post';
+  xhrState = 200;
+  headers = {};
+  withCredentials = false;
+  onStart?: Function;
+  onProgress?: Function;
+  onSuccess?: Function;
+  onFailure?: Function;
+}
+export class Uploader {
+  options: UploadOptions;
+  constructor(options: UploadOptions) {
+    this.options = options;
+  }
+  upload() {
+    const options = this.options;
+    const xhr = new XMLHttpRequest();
+    if (xhr.upload) {
+      xhr.upload.addEventListener(
+        'progress',
+        (e: ProgressEvent<XMLHttpRequestEventTarget>) => {
+          options.onProgress?.(e, options);
+        },
+        false
+      );
+      xhr.onreadystatechange = () => {
+        if (xhr.readyState === 4) {
+          if (xhr.status === options.xhrState) {
+            options.onSuccess?.(xhr.responseText, options);
+          } else {
+            options.onFailure?.(xhr.responseText, options);
+          }
+        }
+      };
+      xhr.withCredentials = options.withCredentials;
+      xhr.open(options.method, options.url, true);
+      // headers
+      for (const [key, value] of Object.entries(options.headers)) {
+        xhr.setRequestHeader(key, value as string);
+      }
+      options.onStart?.(options);
+      xhr.send(options.formData);
+    } else {
+      console.warn('浏览器不支持 XMLHttpRequest');
+    }
+  }
+}

+ 2 - 0
src/sites/assets/util/ref.ts

@@ -1,3 +1,5 @@
 import { ref } from 'vue';
 
 export const currentRoute = ref('/');
+
+export const themeColor = ref('red');

+ 1 - 1
src/sites/config/env.ts

@@ -32,7 +32,7 @@ switch (process.env.NODE_ENV) {
      * 线上环境 => npm run build
      */
     config.isPrd = true;
-    config.baseUrl = 'https://nutui-server.jd.com';
+    config.baseUrl = 'http://nutui-server.jd.com';
     break;
 }
 export default config;

+ 10 - 6
src/sites/doc/components/Footer.vue

@@ -5,6 +5,8 @@
         <h4 class="doc-footer-title">相关资源</h4>
         <div class="doc-footer-item">JDW智能构建平台</div>
         <div class="doc-footer-item">JDW智能构建平台</div>
+        <div class="doc-footer-item">JDW智能构建平台</div>
+        <div class="doc-footer-item">JDW智能构建平台</div>
       </div>
       <div class="doc-footer-list">
         <h4 class="doc-footer-title">相关资源</h4>
@@ -28,7 +30,7 @@
             class="doc-footer-select-item"
             v-for="(item, index) in data.themeList"
             :key="index"
-            @click.stop="checkTheme(index)"
+            @click.stop="checkTheme(item.color, index)"
             :class="{ active: data.activeIndex === index }"
           >
             <i :class="`circle-${item.color}`"></i>{{ item.name }}
@@ -41,6 +43,7 @@
 </template>
 <script lang="ts">
 import { defineComponent, reactive } from 'vue';
+import { themeColor } from '@/sites/assets/util/ref';
 export default defineComponent({
   name: 'doc-footer',
   setup() {
@@ -69,17 +72,17 @@ export default defineComponent({
         'click',
         e => {
           console.log('e.target', e.target);
-          // if (!this.$el.contains(e.target)){
-          //   data.isShowSelect = false;
-          // }
         },
         false
       );
     };
-    const checkTheme = (index: number) => {
+    const checkTheme = (color: string, index: number) => {
       data.isShowSelect = false;
       data.activeIndex = index;
-      console.log('data.isShowSelect', data.isShowSelect);
+      data.theme = color;
+      themeColor.value = color;
+      console.log('themeColor1', themeColor);
+      // bus.emit('select-theme', color)
     };
     return {
       data,
@@ -98,6 +101,7 @@ export default defineComponent({
     &-content {
       display: flex;
       justify-content: space-around;
+      align-items: flex-start;
       margin-right: 50px;
     }
     &-list {

+ 60 - 30
src/sites/doc/components/Header.vue

@@ -1,14 +1,8 @@
 <template>
-  <div class="doc-header" :style="{ background: themeColor === 'red' ? headerBg : themeColor }" :class="`doc-header-${data.theme}`">
+  <!-- <div class="doc-header" :style="{ background: themeColor === 'red' ? headerBg : themeColor }" :class="`doc-header-${data.theme}`"> -->
+  <div class="doc-header" :class="themeName()">
     <div class="header-logo">
-      <a class="logo-link" href="#">
-        <template v-if="data.theme === 'red'">
-          <img src="../../assets/images/logo-header-white.png" />
-        </template>
-        <template v-else>
-          <img src="../../assets/images/logo-header-red.png" />
-        </template>
-      </a>
+      <a class="logo-link" href="#"></a>
       <span class="logo-border"></span>
     </div>
     <div class="header-nav">
@@ -17,16 +11,23 @@
       </div>
       <div class="nav-box">
         <ul class="nav-list">
-          <li class="nav-item nav-item-actie">{{ header[0].cName }}</li>
-          <li class="nav-item">
+          <li class="nav-item" :class="{ active: isActive(header[0].name) }">
+            <router-link :to="header[0].name">{{ header[0].cName }}</router-link>
+          </li>
+          <li class="nav-item" :class="{ active: isActive(header[1].name) }">
             <router-link :to="header[1].name">{{ header[1].cName }}</router-link>
           </li>
-          <li class="nav-item"
+          <li class="nav-item" :class="{ active: isActive(header[2].name) }"
             ><a href="http://localhost:8080/demo.html#/" style="color:#fff">{{ header[2].cName }}</a></li
           >
-          <li class="nav-item">
+          <li class="nav-item" :class="{ active: isActive(header[3].name) }">
             <router-link :to="header[3].name">{{ header[3].cName }}</router-link>
           </li>
+          <!-- <li :class="{ active: isActive(_package.name) }" v-for="_package in docs.packages" :key="_package">
+            <router-link v-if="!_package.isLink" :to="_package.name.toLowerCase()">{{ _package.cName }}</router-link>
+            <a v-else :href="_package.name" target="_blank">{{ _package.cName }}</a>
+          </li> -->
+
           <li class="nav-item">
             <div class="header-select-box" @click.stop="data.isShowSelect = !data.isShowSelect" :class="[data.isShowSelect == true ? 'select-up' : 'select-down']">
               <div class="header-select-hd">{{ data.verson }}<i class=""></i></div>
@@ -46,15 +47,15 @@
   </div>
 </template>
 <script lang="ts">
-import { defineComponent, reactive } from 'vue';
+import { defineComponent, reactive, computed } from 'vue';
 import { header } from '@/config';
+import { currentRoute, themeColor } from '@/sites/assets/util/ref';
 export default defineComponent({
   name: 'doc-header',
   setup() {
     const data = reactive({
       theme: 'black',
-      themeColor: 'black',
-      headerBg: 'url(' + require('../../assets/images/header-bg.png') + ')',
+      // headerBg: 'url(' + require('../../assets/images/header-bg.png') + ')',
       versonList: [
         {
           name: '1.x'
@@ -67,18 +68,32 @@ export default defineComponent({
         }
       ],
       verson: '3.x',
+      navIndex: 0,
       activeIndex: 0,
       isShowSelect: false
     });
-    const checkTheme = (verson: string, index: number) => {
+    const isActive = computed(() => {
+      return function(name: string) {
+        // console.log('name1', currentRoute.value == name.toLowerCase());
+        return currentRoute.value == name.toLowerCase();
+      };
+    });
+    const themeName = computed(() => {
+      return function() {
+        return `doc-header-${themeColor.value}`;
+      };
+    });
+    const checkTheme = (item: string, index: number) => {
       data.isShowSelect = false;
       data.activeIndex = index;
-      data.verson = verson;
+      data.verson = item;
     };
     return {
       header,
       data,
-      checkTheme
+      isActive,
+      checkTheme,
+      themeName
     };
   }
 });
@@ -92,6 +107,7 @@ export default defineComponent({
     top: 0;
     left: 0;
     right: 0;
+    min-width: 1300px;
     background-size: cover;
     background-position: center;
     height: $doc-header-height;
@@ -108,13 +124,13 @@ export default defineComponent({
     width: 240px;
     height: 64px;
     .logo-link {
-      display: flex;
-      align-items: center;
-      height: 64px;
+      display: inline-block;
+      width: 120px;
+      height: 46px;
       vertical-align: middle;
-      img {
-        height: 46px;
-      }
+      position: absolute;
+      top: 50%;
+      margin-top: -23px;
     }
     .logo-border {
       display: inline-block;
@@ -132,6 +148,7 @@ export default defineComponent({
     align-items: center;
     float: right;
     width: calc(100% - 240px);
+    min-width: 900px;
     padding: 0 40px;
     .search-box {
       font-size: 0;
@@ -155,13 +172,17 @@ export default defineComponent({
         position: relative;
         margin-right: 30px;
         font-size: 14px;
-        padding: 0 10px;
         height: 63px;
         line-height: 63px;
         text-align: center;
         cursor: pointer;
+        a {
+          display: inline-block;
+          padding: 0 10px;
+          line-height: 64px;
+        }
         // overflow: hidden;
-        &.nav-item-actie {
+        &.active {
           font-weight: bold;
           &:after {
             content: '';
@@ -236,6 +257,9 @@ export default defineComponent({
     color: $theme-red-word;
     .header {
       &-logo {
+        .logo-link {
+          background: url('../../assets/images/logo-header-white.png') no-repeat center/100%;
+        }
         .logo-border {
           background: $theme-red-border;
         }
@@ -256,7 +280,7 @@ export default defineComponent({
             a {
               color: $theme-red-word;
             }
-            &.nav-item-actie {
+            &.active {
               color: $theme-red-actice;
               &:after {
                 background-position: 0 0;
@@ -308,6 +332,9 @@ export default defineComponent({
     border-bottom: 1px solid $theme-white-box-border;
     .header {
       &-logo {
+        .logo-link {
+          background: url('../../assets/images/logo-header-red.png') no-repeat center/100%;
+        }
         .logo-border {
           background: $theme-white-border;
         }
@@ -328,7 +355,7 @@ export default defineComponent({
             a {
               color: $theme-white-word;
             }
-            &.nav-item-actie {
+            &.active {
               color: $theme-white-actice;
               &:after {
                 background-position: 0 -13px;
@@ -380,6 +407,9 @@ export default defineComponent({
     border-bottom: 1px solid $theme-black-box-border;
     .header {
       &-logo {
+        .logo-link {
+          background: url('../../assets/images/logo-header-red.png') no-repeat center/100%;
+        }
         .logo-border {
           background: $theme-black-border;
         }
@@ -400,7 +430,7 @@ export default defineComponent({
             a {
               color: $theme-black-word;
             }
-            &.nav-item-actie {
+            &.active {
               color: $theme-black-actice;
               &:after {
                 background-position: 0 -13px;

+ 2 - 3
src/sites/doc/router.ts

@@ -30,10 +30,9 @@ const routes: Array<RouteRecordRaw> = [
     name: '/',
     component: Index,
     children: pagesRouter
-  },
-  {
+  }, {
     path: '/resource',
-    name: '/resource',
+    name: 'resource',
     component: Resource
   }
 ];

+ 13 - 4
src/sites/doc/views/Resource.vue

@@ -63,8 +63,10 @@
 </template>
 <script lang="ts">
 import { defineComponent, onMounted, reactive } from 'vue';
+import { onBeforeRouteUpdate, RouteLocationNormalized, useRoute } from 'vue-router';
 import Header from '@/sites/doc/components/Header.vue';
 import Footer from '@/sites/doc/components/Footer.vue';
+import { currentRoute } from '@/sites/assets/util/ref';
 import { ArticleApiService } from '@/sites/service/ArticleApiService';
 export default defineComponent({
   name: 'doc',
@@ -95,8 +97,15 @@ export default defineComponent({
       ],
       activeIndex: 0
     });
+    const watchDemoUrl = (router: RouteLocationNormalized) => {
+      currentRoute.value = router.name as string;
+    };
     onMounted(() => {
-      console.log('mounted');
+      // 路由
+      const route = useRoute();
+      watchDemoUrl(route);
+
+      // 文章列表接口
       const articleApiService = new ArticleApiService();
       articleApiService.getArticle().then(res => {
         if (res?.state == 0) {
@@ -104,11 +113,11 @@ export default defineComponent({
         }
       });
     });
-
+    onBeforeRouteUpdate(to => {
+      watchDemoUrl(to);
+    });
     const clickTab = (index: number) => {
-      console.log('22', index);
       data.activeIndex = index;
-      console.log('33', data.activeIndex);
     };
     return {
       data,

+ 12 - 1
src/styles/variables.scss

@@ -53,7 +53,11 @@ $button-primary-background-color: linear-gradient(
 );
 $button-info-color: $white;
 $button-info-border-color: rgba(73, 106, 242, 1);
-$button-info-background-color: linear-gradient(315deg, rgba(73, 143, 242, 1) 0%, rgba(73, 101, 242, 1) 100%);
+$button-info-background-color: linear-gradient(
+  315deg,
+  rgba(73, 143, 242, 1) 0%,
+  rgba(73, 101, 242, 1) 100%
+);
 $button-success-color: $white;
 $button-success-border-color: rgba(38, 191, 38, 1);
 $button-success-background-color: linear-gradient(
@@ -98,3 +102,10 @@ $icon-line-height: 20px;
 $uploader-width: 100px;
 $uploader-height: 100px;
 $uploader-babackground: #f7f8fa;
+
+// inputnumber
+
+$inputnumber-input-background-color: $help-color;
+$inputnumber-input-border-radius: 8px;
+$inputnumber-input-width: 40px;
+$inputnumber-input-height: 20px;

+ 1 - 1
vue.config.js

@@ -10,7 +10,7 @@ module.exports = {
     open: true,
     proxy: {
       '/devServer': {
-        target: 'https://nutui-server.jd.com',
+        target: 'http://nutui-server.jd.com',
         changeOrigin: true,
         pathRewrite: {
           '^/devServer': ''