浏览代码

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

ailululu 5 年之前
父节点
当前提交
bea93ea9cd

+ 38 - 1
src/config.ts

@@ -71,12 +71,40 @@ export const nav = [
         show: true,
         desc: '按钮用于触发一个操作,如提交表单。',
         author: 'richard1015'
+      },
+      {
+        name: 'collapse',
+        sort: 2,
+        cName: '折叠面板',
+        type: 'component',
+        show: true,
+        desc: '折叠面板',
+        author: 'Ymm0008'
+      },
+      {
+        name: 'collapse',
+        sort: 3,
+        cName: '折叠面板-item',
+        type: 'component',
+        show: false,
+        desc: '折叠面板-item',
+        author: 'Ymm0008'
       }
     ]
   },
   {
     name: '操作反馈',
-    packages: []
+    packages: [
+      {
+        name: 'BackTop',
+        sort: '1',
+        cName: '回到顶部',
+        type: 'component',
+        show: true,
+        desc: '较长页面快捷回到顶部',
+        author: 'liqiong43'
+      }
+    ]
   },
   {
     name: '基础组件',
@@ -125,6 +153,15 @@ export const nav = [
         show: true,
         desc: '价格组件',
         author: 'ailululu'
+      },
+      {
+        name: 'Checkbox',
+        sort: 5,
+        cName: '复选按钮',
+        type: 'component',
+        show: true,
+        desc: '复选按钮',
+        author: 'Ymm0008'
       }
     ]
   },

+ 95 - 0
src/packages/collapse/demo.vue

@@ -0,0 +1,95 @@
+<template>
+  <div class="demo-list">
+    <h4>基本用法</h4>
+    <div class="show-demo">
+      <nut-collapse v-model:active="active1">
+        <nut-collapse-item :title="title1" :name="1">
+          京东“厂直优品计划”首推“政府优品馆” 3年覆盖80%镇级政府
+        </nut-collapse-item>
+        <nut-collapse-item :title="title2" :name="2">
+          京东到家:教师节期间 创意花束销量增长53倍
+        </nut-collapse-item>
+        <nut-collapse-item :title="title3" :name="3" disabled> </nut-collapse-item>
+      </nut-collapse>
+    </div>
+    <div class="show-demo">
+      <h4>手风琴</h4>
+      <nut-collapse v-model:active="active2" :accordion="true">
+        <nut-collapse-item :title="title1" :name="1">
+          华为终端操作系统EMUI 11发布,9月11日正式开启
+        </nut-collapse-item>
+        <nut-collapse-item :title="title2" :name="2" :subTitle="subTitle">
+          中国服务机器人市场已占全球市场超1/4
+        </nut-collapse-item>
+        <nut-collapse-item :title="title3" :name="3">
+          QuestMobile:90后互联网用户规模超越80后达3.62亿
+        </nut-collapse-item>
+      </nut-collapse>
+    </div>
+    <div class="show-demo">
+      <h4>图标展示</h4>
+      <nut-collapse
+        v-model:active="active3"
+        :accordion="true"
+        :expandIconPosition="expandIconPosition"
+        :icon="icon"
+        :iconWidth="iconWidth"
+        :iconHeight="iconHeight"
+        :rotate="rotate"
+      >
+        <nut-collapse-item :title="title1" :name="1">
+          京东数科IPO将引入“绿鞋机制”
+        </nut-collapse-item>
+        <nut-collapse-item :title="title2" :name="2">
+          世界制造业大会开幕,阿里巴巴与安徽合作再升级
+        </nut-collapse-item>
+      </nut-collapse>
+    </div>
+  </div>
+</template>
+<script lang="ts">
+import { reactive, toRefs } from 'vue';
+import { createComponent } from '@/utils/create';
+const { createDemo } = createComponent('collapse');
+export default createDemo({
+  setup(props, context) {
+    const data = reactive({
+      active1: [1, '2'],
+      active2: 1,
+      active3: 1,
+      expandIconPosition: 'left',
+      title1: '标题1',
+      title2: '标题2',
+      title3: '标题3',
+      subTitle: '副标题',
+      icon: 'https://img11.360buyimg.com/imagetools/jfs/t1/132849/8/9709/550/5f5f0d8aE802abee7/68bd02b3a52c3988.png',
+      iconWidth: '20px',
+      iconHeight: '20px',
+      rotate: 90
+    });
+    const change = (name: string) => {
+      console.log(`点击了name是${name}的面板`);
+    };
+    return {
+      change,
+      ...toRefs(data)
+    };
+  }
+});
+</script>
+
+<style>
+#app {
+  background: #f7f8fa;
+}
+</style>
+<style lang="scss" scoped>
+.demo-list {
+  margin: 60px 0;
+  h4 {
+    margin: 20px 0 10px 25px;
+    font-size: 14px;
+    color: #909ca4;
+  }
+}
+</style>

+ 116 - 0
src/packages/collapse/doc.md

@@ -0,0 +1,116 @@
+# Collapse 折叠面板
+
+## 基本用法
+
+通过`v-model`控制展开的面板列表,`activeNames`为数组格式
+
+```html
+<nut-collapse v-model="activeNames">
+    <nut-collapse-item title="标题1" :name="1">
+        京东“厂直优品计划”首推“政府优品馆” 3年覆盖80%镇级政府 
+    </nut-collapse-item>
+    <nut-collapse-item title="标题2" :name="2">
+        京东到家:教师节期间 创意花束销量增长53倍 
+    </nut-collapse-item>
+    <nut-collapse-item title="标题3" :name="3" disabled>
+    </nut-collapse-item>
+</nut-collapse>
+```
+
+``` javascript
+export default {
+  data() {
+    return {
+        activeNames: [1, 2]
+    };
+  }
+};
+```
+
+
+### 手风琴
+
+通过`accordion`可以设置为手风琴模式,最多展开一个面板,此时`activeName`为字符串格式;`subTitle`可以设置副标题的内容
+
+```html
+<nut-collapse v-model="activeName" :accordion="true">
+    <nut-collapse-item :title="title1" :name="1">
+        华为终端操作系统EMUI 11发布,9月11日正式开启 
+    </nut-collapse-item>
+    <nut-collapse-item :title="title2" :name="2" :sub-title="subTitle">
+        中国服务机器人市场已占全球市场超1/4 
+    </nut-collapse-item>
+    <nut-collapse-item :title="title3" :name="3">
+        QuestMobile:90后互联网用户规模超越80后达3.62亿 
+    </nut-collapse-item>
+</nut-collapse>
+```
+
+``` javascript
+export default {
+  data() {
+    return {
+      activeName: 1,
+      subTitle: '副标题'
+    };
+  }
+};
+```
+
+
+### 图标展示
+
+通过`expandIconPosition`可以设置图标的位置,icon设置自定义图标,rotate设置图标旋转的角度
+
+```html
+<nut-collapse v-model="activeName" :accordion="true" :expand-icon-position="expandIconPosition" :icon="icon" :rotate="rotate" :icon-width="iconWidth"
+        :icon-height="iconHeight">
+    <nut-collapse-item :title="title1" :name="1">
+        京东数科IPO将引入“绿鞋机制” 
+    </nut-collapse-item>
+    <nut-collapse-item :title="title2" :name="2">
+        世界制造业大会开幕,阿里巴巴与安徽合作再升级
+    </nut-collapse-item>
+</nut-collapse>
+```
+
+``` javascript
+export default {
+  data() {
+    return {
+      activeName: 1,
+      expandIconPosition: 'left',
+      icon: 'https://img11.360buyimg.com/imagetools/jfs/t1/132849/8/9709/550/5f5f0d8aE802abee7/68bd02b3a52c3988.png'
+      rotate: 180,
+      iconWidth: '20px',
+      iconHeight: '20px',
+    };
+  }
+};
+```
+
+## Collapse Prop
+
+| 字段 | 说明 | 类型 | 默认值
+|----- | ----- | ----- | ----- 
+| v-model | 当前展开面板的 name | 手风琴模式:string \| number<br>非手风琴模式:(string \| number)[] | - |
+| accordion | 是否开启手风琴模式 | boolean | false |
+| border | 是否显示外边框 | boolean | true |
+
+### Events
+
+| 事件名 | 说明 | 回调参数 |
+|------|------|------|
+| change | 切换面板时触发 | 类型与 v-model 绑定的值一致 |
+
+### CollapseItem Props
+| 参数 | 说明 | 类型 | 默认值 | 
+|------|------|------|------|
+| title | 标题栏左侧内容 | string | - |
+| name | 唯一标识符,必填 | string \ number | -1 |
+| expand-icon-position | 标题图标的位置 | string | right |
+| sub-title | 标题栏副标题 | string | - |
+| icon | 标题栏自定义图标链接 | string | - |
+| icon-width | 标题栏自定义图标宽度 | string | 24px |
+| icon-height | 标题栏自定义图标高度 | string | 12px |
+| rotate | 点击折叠和展开的旋转角度,在自定义图标模式下生效 | string \ number | 180 |

+ 0 - 0
src/packages/collapse/index.scss


+ 91 - 0
src/packages/collapse/index.vue

@@ -0,0 +1,91 @@
+<template>
+  <view @changeEvt="changeEvt">
+    <slot></slot>
+  </view>
+</template>
+<script lang="ts">
+import { toRefs } from 'vue';
+import { createComponent } from '@/utils/create';
+import { useChildren } from '@/utils/useRelation/useChildren';
+export const COLLAPSE_KEY = 'nutCollapse';
+const { create } = createComponent('collapse');
+
+export default create({
+  props: {
+    active: {
+      type: [String, Number, Array]
+    },
+    accordion: {
+      type: Boolean
+    },
+    expandIconPosition: {
+      type: String,
+      default: 'right'
+    },
+    icon: {
+      type: String,
+      default: ''
+    },
+    iconWidth: {
+      type: String,
+      default: ''
+    },
+    iconHeight: {
+      type: String,
+      default: ''
+    },
+    rotate: {
+      type: [String, Number],
+      default: 180
+    }
+  },
+  setup(props, { emit }) {
+    const { active } = toRefs(props);
+    // 多个 item 展开
+    const changeValAry = (name: any) => {
+      const activeItem: any = active?.value instanceof Object ? Object.values(active.value) : active?.value;
+      let index = -1;
+      activeItem.forEach((item: string | number, idx: number) => {
+        if (String(item) == String(name)) {
+          index = idx;
+        }
+      });
+      const v = JSON.parse(JSON.stringify(activeItem));
+      index > -1 ? v.splice(index, 1) : v.push(name);
+      emit('update:active', v);
+    };
+
+    // 更新v-modal的值
+    const changeVal = (val: string | number | Array<string | number>, expanded: boolean) => {
+      emit('update:active', val);
+    };
+
+    const isExpanded = (name: string | number | Array<string | number>) => {
+      const { accordion, active } = props;
+      if (accordion) {
+        if (typeof active == 'number' || typeof active == 'string') {
+          return active == name;
+        } else {
+          return false;
+        }
+      }
+    };
+
+    const { linkChildren } = useChildren(COLLAPSE_KEY);
+    linkChildren({
+      value: props.active,
+      accordion: props.accordion,
+      expandIconPosition: props.expandIconPosition,
+      icon: props.icon,
+      rotate: props.rotate,
+      changeValAry,
+      changeVal,
+      isExpanded
+    });
+  }
+});
+</script>
+
+<style lang="scss">
+@import 'index.scss';
+</style>

+ 4 - 0
src/packages/collapseitem/demo.vue

@@ -0,0 +1,4 @@
+<template>
+  <div class="demo-list"> </div>
+</template>
+<script lang="ts"></script>

+ 2 - 0
src/packages/collapseitem/doc.md

@@ -0,0 +1,2 @@
+# CollapseItem 折叠面板
+

+ 94 - 0
src/packages/collapseitem/index.scss

@@ -0,0 +1,94 @@
+.nut-collapse-item {
+  position: relative;
+  .collapse-item {
+    &::after {
+      position: absolute;
+      box-sizing: border-box;
+      content: ' ';
+      pointer-events: none;
+      right: 16px;
+      bottom: 0;
+      left: 16px;
+      border-bottom: 1px solid #ebedf0;
+      -webkit-transform: scaleY(0.5);
+      transform: scaleY(0.5);
+    }
+  }
+  .collapse-item {
+    position: relative;
+    display: block;
+    width: 100%;
+    overflow: hidden;
+    padding: 13px 26px;
+    color: #666666;
+    font-size: 14px;
+    line-height: 24px;
+    background-color: #fff;
+    box-sizing: border-box;
+    .collapse-icon {
+      display: block;
+      position: absolute;
+      top: 50%;
+      margin-top: -6px;
+      right: 16px;
+      width: 24px;
+      height: 12px;
+      line-height: 24px;
+      background-image: url(https://img13.360buyimg.com/imagetools/jfs/t1/153504/23/6125/464/5fb240abE562ed1c5/382909c698fa3c1f.png);
+      background-repeat: no-repeat;
+      background-size: 100% 100%;
+      transition: transform 0.3s;
+    }
+    .col-expanded {
+      transform: rotate(-180deg);
+    }
+    .subTitle {
+      position: absolute;
+      top: 50%;
+      right: 60px;
+      margin-top: -12px;
+      color: #969799;
+    }
+  }
+  .collapse-wrapper {
+    display: block;
+    position: relative;
+    height: 0;
+    overflow: hidden;
+    transition: height 0.3s ease-in-out;
+    .collapse-content {
+      display: block;
+      padding: 12px 16px;
+      color: #969799;
+      font-size: 14px;
+      line-height: 1.5;
+      background-color: #fff;
+    }
+  }
+  .nut-collapse-item-disabled {
+    color: #c8c9cc;
+    cursor: not-allowed;
+    pointer-events: none;
+    .collapse-icon-disabled {
+      background-image: url(https://img12.360buyimg.com/imagetools/jfs/t1/150103/9/14710/474/5fb2419aE63e79ef3/8db2699c11139027.png);
+      background-repeat: no-repeat;
+      background-size: 100% 100%;
+    }
+  }
+  .nut-collapse-item-left {
+    .collapse-item {
+      padding: 10px 16px 10px 50px;
+      .collapse-icon {
+        left: 20px;
+      }
+      .subTitle {
+        right: 16px;
+      }
+    }
+  }
+  // .nut-collapse-item.nut-collapse-item-icon {
+  //     .collapse-icon {
+  //         transform: rotate(0deg);
+  //     }
+  // }
+}

+ 190 - 0
src/packages/collapseitem/index.vue

@@ -0,0 +1,190 @@
+<template>
+  <view :class="['nut-collapse-item', { 'nut-collapse-item-left': classDirection == 'left' }, { 'nut-collapse-item-icon': icon }]">
+    <view :class="['collapse-item', { 'item-expanded': openExpanded }, { 'nut-collapse-item-disabled': disabled }]" @click="toggleOpen">
+      <view class="collapse-title">
+        <view v-html="title"></view>
+      </view>
+      <view v-if="subTitle" v-html="subTitle" class="subTitle"></view>
+      <i v-if="icon" :class="['collapse-icon', { 'col-expanded': openExpanded }, { 'collapse-icon-disabled': disabled }]" :style="iconStyle"></i>
+      <i v-else :class="['collapse-icon', { 'col-expanded': openExpanded }, { 'collapse-icon-disabled': disabled }]"></i>
+    </view>
+    <view class="collapse-wrapper" ref="wrapperRef">
+      <view class="collapse-content" ref="contentRef">
+        <slot></slot>
+      </view>
+    </view>
+  </view>
+</template>
+<script lang="ts">
+import { reactive, toRefs, onMounted, ref, nextTick, computed, watch } from 'vue';
+import { createComponent } from '@/utils/create';
+import { useParent } from '@/utils/useRelation/useParent';
+import { COLLAPSE_KEY } from './../collapse/index.vue';
+const { create } = createComponent('collapse-item');
+
+export default create({
+  props: {
+    title: {
+      type: String,
+      default: ''
+    },
+    subTitle: {
+      type: String,
+      default: ''
+    },
+    disabled: {
+      type: Boolean,
+      default: false
+    },
+    name: {
+      type: [Number, String],
+      default: -1,
+      required: true
+    },
+    collapseRef: {
+      type: Object
+    }
+  },
+  setup(props) {
+    const collapse = useParent(COLLAPSE_KEY);
+    const parent: any = reactive(collapse.parent as any);
+    const index: any = reactive(collapse.index as any);
+    const proxyData = reactive({
+      openExpanded: false,
+      classDirection: 'right',
+      iconStyle: {
+        width: '20px',
+        height: '20px',
+        'background-image': 'url(https://img10.360buyimg.com/imagetools/jfs/t1/111306/10/17422/341/5f58aa0eEe9218dd6/28d76a42db334e31.png)',
+        'background-repeat': 'no-repeat',
+        'background-size': '100% 100%',
+        transform: 'rotate(0deg)'
+      }
+    });
+
+    // 获取 Dom 元素
+    const wrapperRef: any = ref(null);
+    const contentRef: any = ref(null);
+
+    // 清除 willChange 减少性能浪费
+    const onTransitionEnd = () => {
+      const wrapperRefEle: any = document.getElementsByClassName('collapse-wrapper')[0];
+      wrapperRefEle.style.willChange = 'auto';
+    };
+
+    // 手风琴模式
+    const animation = () => {
+      const wrapperRefEle: any = wrapperRef.value;
+      const contentRefEle: any = contentRef.value;
+      if (!wrapperRefEle || !contentRefEle) {
+        return;
+      }
+      const offsetHeight = contentRefEle.offsetHeight;
+      if (offsetHeight) {
+        const contentHeight = `${offsetHeight}px`;
+        wrapperRefEle.style.willChange = 'height';
+        wrapperRefEle.style.height = !proxyData.openExpanded ? 0 : contentHeight;
+        if (parent.icon && !proxyData.openExpanded) {
+          proxyData.iconStyle['transform'] = 'rotate(0deg)';
+        } else {
+          proxyData.iconStyle['transform'] = 'rotate(' + parent.rotate + 'deg)';
+        }
+      }
+      if (!proxyData.openExpanded) {
+        onTransitionEnd();
+      }
+    };
+
+    const open = () => {
+      proxyData.openExpanded = !proxyData.openExpanded;
+      animation();
+    };
+
+    const defaultOpen = () => {
+      open();
+      if (parent.icon) {
+        proxyData['iconStyle']['transform'] = 'rotate(' + parent.rotate + 'deg)';
+      }
+    };
+
+    const currentName = computed(() => props.name ?? index.value);
+    const toggleOpen = () => {
+      if (parent.accordion) {
+        parent.children.forEach((item: any, index: number) => {
+          if (currentName.value == item.name) {
+            item.changeOpen(!item.openExpanded);
+          } else {
+            item.changeOpen(false);
+            item.animation();
+          }
+        });
+        nextTick(() => {
+          parent.changeVal(currentName.value, !proxyData.openExpanded);
+          animation();
+        });
+      } else {
+        parent.changeValAry(props.name);
+        open();
+      }
+    };
+    // 更改子组件展示
+    const changeOpen = (bol: boolean) => {
+      proxyData.openExpanded = bol;
+    };
+
+    const expanded = computed(() => {
+      if (parent) {
+        return parent.isExpanded(props.name);
+      }
+      return null;
+    });
+
+    watch(expanded, (value, oldValue) => {
+      if (value) {
+        proxyData.openExpanded = true;
+      }
+    });
+
+    onMounted(() => {
+      const { name } = props;
+      const active = parent && parent.value;
+
+      if (typeof active == 'number' || typeof active == 'string') {
+        if (name == active) {
+          defaultOpen();
+        }
+      } else if (Object.values(active) instanceof Array) {
+        const f = Object.values(active).filter(item => item == name);
+        if (f.length > 0) {
+          defaultOpen();
+        }
+      }
+      proxyData.classDirection = parent.expandIconPosition;
+      if (parent.icon) {
+        proxyData.iconStyle['background-image'] = 'url(' + parent.icon + ')';
+      }
+      if (parent.iconWidth) {
+        proxyData.iconStyle['width'] = parent.conWidth;
+      }
+      if (parent.iconHeght) {
+        proxyData.iconStyle['height'] = parent.iconHeight;
+      }
+    });
+
+    return {
+      ...toRefs(proxyData),
+      ...toRefs(parent),
+      wrapperRef,
+      contentRef,
+      open,
+      toggleOpen,
+      changeOpen,
+      animation
+    };
+  }
+});
+</script>
+
+<style lang="scss" scoped>
+@import './index.scss';
+</style>

+ 90 - 0
src/utils/useRelation/useChildren.ts

@@ -0,0 +1,90 @@
+import {
+  VNode,
+  isVNode,
+  provide,
+  reactive,
+  getCurrentInstance,
+  VNodeNormalizedChildren,
+  ComponentPublicInstance,
+  ComponentInternalInstance
+} from 'vue';
+
+export function flattenVNodes(children: VNodeNormalizedChildren) {
+  const result: VNode[] = [];
+
+  const traverse = (children: VNodeNormalizedChildren) => {
+    if (Array.isArray(children)) {
+      children.forEach(child => {
+        if (isVNode(child)) {
+          result.push(child);
+
+          if (child.component?.subTree) {
+            traverse(child.component.subTree.children);
+          }
+
+          if (child.children) {
+            traverse(child.children);
+          }
+        }
+      });
+    }
+  };
+
+  traverse(children);
+
+  return result;
+}
+
+// sort children instances by vnodes order
+export function sortChildren(
+  parent: ComponentInternalInstance,
+  publicChildren: ComponentPublicInstance[],
+  internalChildren: ComponentInternalInstance[]
+) {
+  const vnodes = flattenVNodes(parent.subTree.children);
+
+  internalChildren.sort((a, b) => vnodes.indexOf(a.vnode) - vnodes.indexOf(b.vnode));
+
+  const orderedPublicChildren = internalChildren.map(item => item.proxy!);
+
+  publicChildren.sort((a, b) => {
+    const indexA = orderedPublicChildren.indexOf(a);
+    const indexB = orderedPublicChildren.indexOf(b);
+    return indexA - indexB;
+  });
+}
+
+export function useChildren(key: string | symbol) {
+  const publicChildren: ComponentPublicInstance[] = reactive([]);
+  const internalChildren: ComponentInternalInstance[] = reactive([]);
+  const parent = getCurrentInstance()!;
+
+  const linkChildren = (value?: any) => {
+    const link = (child: ComponentInternalInstance) => {
+      if (child.proxy) {
+        internalChildren.push(child);
+        publicChildren.push(child.proxy);
+        sortChildren(parent, publicChildren, internalChildren);
+      }
+    };
+
+    const unlink = (child: ComponentInternalInstance) => {
+      const index = internalChildren.indexOf(child);
+      publicChildren.splice(index, 1);
+      internalChildren.splice(index, 1);
+    };
+
+    provide(key, {
+      link,
+      unlink,
+      children: publicChildren,
+      internalChildren,
+      ...value
+    });
+  };
+
+  return {
+    children: publicChildren,
+    linkChildren
+  };
+}

+ 35 - 0
src/utils/useRelation/useParent.ts

@@ -0,0 +1,35 @@
+import { inject, computed, onUnmounted, getCurrentInstance, ComponentPublicInstance, ComponentInternalInstance } from 'vue';
+
+type ParentProvide<T> = T & {
+  link(child: ComponentInternalInstance): void;
+  unlink(child: ComponentInternalInstance): void;
+  children: ComponentPublicInstance[];
+  internalChildren: ComponentInternalInstance[];
+};
+
+export function useParent<T>(key: string | symbol) {
+  const parent = inject<ParentProvide<T> | null>(key, null);
+
+  if (parent) {
+    const instance = getCurrentInstance();
+
+    if (instance) {
+      const { link, unlink, internalChildren, ...rest } = parent;
+
+      link(instance);
+
+      onUnmounted(() => {
+        unlink(instance);
+      });
+
+      const index = computed(() => internalChildren.indexOf(instance));
+
+      return {
+        parent: rest,
+        index
+      };
+    }
+  }
+
+  return {};
+}