跳转至

15 定制运营拖拽组件:如何实现运营搭建页面的拖拽功能?

你好,我是杨文坚。

上节课,我们学习了单元测试,也收尾了基础组件库相关开发。从今天开始,我们会正式围绕着课程的最终实战项目——运营搭建平台,打造相关的Vue.js业务组件和业务功能。

运营搭建平台,核心是搭建功能,“核心业务”就是提供搭建页面的能力。那这节课,我们就围绕着这个搭建页面的“核心业务”来打造业务组件,搭建页面需要用到拖拽布局组件。

可能你会问,为什么要把拖拽组件划分成业务组件?拖拽功能也很基础,为什么不划分成基础组件呢?

这是因为,拖拽功能,虽然基础,但很难做到通用,不同场景下拖拽的需求效果是不一样的。比如,是实现布局弹性排序?还是让布局直接调整位置?还是把组件从一个布局容器拖到另外一个布局容器里?当然,不同的开发团队,组件规范定义有一定差异,不过,这些功能实现,跟业务需求特点息息相关,很难做到一个组件兼顾所有拖拽功能。

那如何搭建拖拽布局组件呢?我们开始今天的学习。

为什么需要拖拽布局组件?

搭建页面,原理就是通过配置数据,来驱动页面渲染对应内容,数据描述的是目标页面的布局情况,例如页面有多少个区块、每个区块里有多少个子区块,描述布局的数据也包含了每个区块里要渲染什么组件。

这就意味着,使用者只要能控制数据就行了。所以,实际上,即使没有拖拽布局组件,我们也是能实现运营搭建页面的基本操作功能。

但是,并不是所有项目的使用者都是前端开发程序员,也并不是所有项目的使用者都懂得搭建页面的数据格式、数据的规律和实现原理。

我们的运营搭建平台,目的就是要让非开发人员能低成本地搭建网页,不需要投入前端程序员的开发时间。假设搭建页面直接配置数据是一个“解答题”,那么拖拽配置页面就是一个“选择题”。“解答题”是留给专业人员来做,例如前端开发程序员,“选择题”是给非专业人员使用,例如运营同事和产品同事。

因此,开发拖拽布局组件,其实就是要降低配置页面的难度,让非前端开发的人员能直接上手,使用这个配置页面的功能,用技术来解放生产力。

既然拖拽布局业务组件对运营搭建平台这么重要,如何实现呢?我们还是按老规矩,先实现一个最简单的功能案例,分析清楚原理,再来抽象逻辑,封装成组件。

如何实现简单的拖拽布局功能?

我们先从一个简单的拖拽布局功能学起。我主要用原生的JavaScript API方式来实现功能,不使用任何框架,带你掌握拖拽实现的技术原理,希望能“授你以渔”。

对于拖拽布局功能,常见的功能要求是要在一个限定范围的“容器”内,让指定的“子模块”可以拖拽移动,在拖拽过程中还需要重新对模块进行排位。

具体实现可以分成五步:

  1. 定义父容器和子模块;
  2. 监听子模块的拖拽开始事件;
  3. 监听拖拽过程经过父容器里哪些子模块;
  4. 计算和重新渲染拖拽过程的临时布局;
  5. 监听拖拽结束事件,更新最终模块布局。

我们一步步分析相关的操作实现。

第一步定义父容器和子模块

这里“父容器”就是限定拖拽范围的DOM,“子模块”就是在父容器DOM里的指定排序的 children  DOM,也就是子节点。在定义子模块的DOM时,我们还要加上draggable属性,让它能够实现鼠标拖拽的视觉效果。

这里要注意一点,这个draggable属性,仅仅是让DOM在鼠标拖拽时有视觉上的移动,当鼠标释放DOM时,DOM会自动复原,不能直接实现拖拽功能。

看具体实现的HTML代码:

<html>
  <head>
    <meta charset="utf-8" />
  </head>
  <style>
    .drag-layout {
      display: block;
      width: 400px;
      margin: 0 auto;
      box-sizing: border-box;
      padding: 10px;
      border: 1px solid #999999;
    }
    .drag-item {
      height: 40px;
      border: 1px solid #cccccc;
      margin-bottom: 10px;
      font-size: 20px;
    }
    .drag-item.active {
      opacity: 0.4;
    }
  </style>
  <body>
    <div class="drag-layout">
      <div draggable="true" class="drag-item" style="background: #00bcd4;">
        模块1
      </div>
      <div draggable="true" class="drag-item" style="background: #8bc34a;">
        模块2
      </div>
      <div draggable="true" class="drag-item" style="background: #ffeb3b;">
        模块3
      </div>
      <div draggable="true" class="drag-item" style="background: #ff5722;">
        模块4
      </div>
    </div>
    <script type="module" src="./drag-demo.js"></script>
  </body>
</html>

效果图如下所示:

图片

第二步监听子模块的拖拽开始事件

具体的监听方式,是以父容器的DOM作为事件代理来监听。这里,就要用到DOM的原生事件监听API addEventListener,来监听dragstart事件。

拖拽子模块时,子模块的拖拽事件会通过“事件冒泡”的方式让父容器捕获,所以我们监听父容器,就能捕获到子模块的开始拖拽事件dragstart,并且监听这个事件。

这个时候,我们需要定义一个变量,来标记当前拖拽中的子模块原始位置,给后续拖拽布局重新排序时候计算用。我通过原生的DOM API写了一个方法,可以根据子模块DOM来计算定位到父容器DOM里的位置序号。

具体代码如下所示:

const dragLayout = document.querySelector('.drag-layout');
// 被拖拽时的子模块DOM所在序号
let activeIndex = -1;
// 拖拽到某个子模块上口的序号
let dragToIndex = -1;
// 上一次所有子模块顺序的DOM的列表
let prevItems = [];

// 根据DOM来获取在父容器里的序号
function getItemIndex(item) {
  let elem = item;
  let index = -1;
  if (!elem || !elem.parentElement) {
    return index;
  }
  index = 0;
  elem = elem.previousElementSibling;
  while (elem) {
    index++;
    elem = elem.previousElementSibling;
  }
  return index;
}

// 在父容器上监听 拖拽开始事件
dragLayout.addEventListener('dragstart', (e) => {
  const dom = e.target;
  const isItem = dom.classList.contains('drag-item');
  if (isItem) {
    const itemDOMs = document.querySelectorAll('.drag-item');
    prevItems = Array.from(itemDOMs);

    const itemIndex = getItemIndex(dom);
    activeIndex = itemIndex;
    dom.classList.add('active');
  }
});

第三步监听拖拽过程经过父容器里哪些子模块

这个需要监听父容器的dragover事件,基于拖拽事件,可以冒泡到父容器DOM来捕获,然后我们就可以通过dragover事件,来监听当前拖拽子组件的的鼠标位置,在哪个其它子模块的“上空”。

我们可以在事件里实时“捕获”鼠标拖拽到某个新位置下面的子模块DOM,然后通过上一步实现的查找子模块位置的方法,计算出拖拽过程中的新位置序号,这个序号也就是当前“被拖拽的子模块”可能被“释放”的新位置。

具体代码如下所示:

// 监听拖拽过程中的事件,
// 用来计算移动到某个子模块“上空”
dragLayout.addEventListener('dragover', (e) => {
  e.preventDefault();
  const dom = e.target;
  const isItem = dom.classList.contains('drag-item');
  if (isItem) {
    const overItemIndex = getItemIndex(dom);
    dragToIndex = overItemIndex;
    // 重新排序渲染子模块
    resetItems();
  }
});

第四步计算和重新渲染拖拽过程的临时布局

前面步骤中所有的子模块DOM,是在父容器DOM里并排显示的。当“被拖拽的子模块”经过“某个其他子模块”上空时,就可以做个临时重新排序渲染,“被拖拽的子模块”从原来位置抽出来,整个子模块布局就按顺序补位,把“被拖拽的子模块”插入经过的“某个其他子模块”的位置,达到重新排序的操作。

具体实现代码如下:

// 根据拖拽的数据重新排序
function resetItems() {
  if (!prevItems[activeIndex]) {
    return;
  }
  if (dragToIndex >= 0 && dragToIndex < prevItems.length) {
    const newList = prevItems.map((item) => item);
    const [activeItem] = newList.splice(activeIndex, 1);
    if (dragToIndex === 0) {
      newList.unshift(activeItem);
    } else {
      newList.splice(dragToIndex, 0, activeItem);
    }
    Array.from(dragLayout.children).forEach((child) => {
      dragLayout.removeChild(child);
    });
    newList.forEach((item) => {
      dragLayout.appendChild(item);
    });
  }
}

代码中,我用了一个临时的子模块数组,缓存上一次未被修改的子模块排序数据,在拖拽dragover过程中,就根据上述算法来重新排序渲染。

我们已经完成拖拽布局的绝大部分功能,接下来就是收尾工作。

第五步监听拖拽结束事件,更新最终模块布局

这里,我们监听父容器DOM的dragend事件,捕获最终位置,然后通过第四步的位置布局方法重新渲染子模块DOM的顺序。具体代码实现如下:

// 渲染结束,更新数据
dragLayout.addEventListener('dragend', (e) => {
  e.preventDefault();
  prevItems.forEach((item) => {
    item.classList.remove('active');
  });
  dragToIndex = -1;
  activeIndex = -1;
});

最终的功能效果如动图所示:

图片

这五步就是一个完整的拖拽布局的原生JavaScript API实现过程。

我们还可以改一下HTML代码里的CSS样式,让这个纵向的拖拽布局变成横向的拖拽布局功能:

.drag-layout.horizontal {
  flex-direction: row;
  display: flex;
}
.drag-layout.horizontal .drag-item {
  width: 100px;
}

最终的功能效果如动图所示:

图片

现在,你应该知道原生JavaScript API如何实现一个简单的拖拽布局功能了吧,那么,把这个实现思路融入到Vue.js 3.x这个框架环境里,该怎么做呢?或者说,如果要用Vue.js 3.x来封装拖拽布局的业务组件,我们应该怎么做呢?

如何用Vue3封装拖拽布局的业务组件?

通过上面的简单实现过程,我们可以知道,拖拽布局操作核心就是在“父容器”里拖拽控制“子模块”,那么,在Vue.js中,我们就可以把“父容器”和“子模块”封装成两个独立的Vue.js组件来进行组合,实现拖拽功能。

基于上面的五步,通过Vue.js的组件封装思维,我们可以精简成三步来实现:

  • 第一步,封装父容器组件;
  • 第二步,封装子容器组件;
  • 第三步,组合父容器和子容器。

来看看每一步的具体实现细节。

第一步,封装父容器组件。这里主要的操作就是在父容器里直接监听dragstart、dragover和dragend事件。因为子容器会在父容器中使用,所以我们可以使用Vue3的API provide/inject,来实现父子组件里的轻量级数据通信。

具体实现,就是用provide在父容器定义好共享响应式数据,然后在子容器里用inject,来使用父容器定义的共享响应式数据。

父容器Vue.js 3.x组件代码的具体实现:

<template>
  <div
    :className="baseClassName"
    @dragstart="onDragStart"
    @dragover="onDragOver"
    @dragend="onDragEnd"
  >
    <slot></slot>
  </div>
</template>

<script setup lang="ts">
import { reactive, provide, toRaw } from 'vue';
import {
  DRAG_CONTEXT_KEY,
  getElementIndex,
  getDraggingElement
} from './common';
import { prefixName } from '../theme/index';
import type { DragContext } from './common';
const baseClassName = `${prefixName}-drag-layout`;

const emits = defineEmits<{
  (event: 'dragStart', e: { activeIndex: number; dragToIndex: number }): void;
  (event: 'dragOver', e: { activeIndex: number; dragToIndex: number }): void;
  (event: 'dragEnd', e: { activeIndex: number; dragToIndex: number }): void;
}>();

const dragContext = reactive<DragContext>({
  activeIndex: -1,
  dragToIndex: -1
});

provide(DRAG_CONTEXT_KEY, dragContext);

const onDragStart = () => {
  emits('dragStart', {
    activeIndex: toRaw(dragContext.activeIndex),
    dragToIndex: toRaw(dragContext.dragToIndex)
  });
};

const onDragOver = (e: DragEvent) => {
  e.preventDefault();
  const target: HTMLElement | null = getDraggingElement(
    e?.target as HTMLElement
  );
  const dragToIndex = getElementIndex(target);
  dragContext.dragToIndex = dragToIndex;
  emits('dragOver', {
    activeIndex: toRaw(dragContext.activeIndex),
    dragToIndex: toRaw(dragContext.dragToIndex)
  });
};

const onDragEnd = () => {
  dragContext.activeIndex = -1;
  dragContext.dragToIndex = -1;
  emits('dragEnd', {
    activeIndex: toRaw(dragContext.activeIndex),
    dragToIndex: toRaw(dragContext.dragToIndex)
  });
};
</script>

完整代码在源码文件 packages/business/src/drag/drag-layout.vue 里。

第二步,封装子容器组件。

我们直接注册监听拖拽事件,同时,触发事件时来跟父容器进行数据通信。也就是说,当子容器触发dragstart事件时,通过inject拿到共享的响应式数据,来传递拖拽选中的组件序号位置。具体代码实现如下所示:

<template>
  <div
    ref="domItem"
    :className="baseClassName"
    draggable="true"
    data-drag-item="yes"
    @dragstart="onDragItem"
  >
    <slot />
  </div>
</template>

<script setup lang="ts">
import { ref, inject } from 'vue';
import { DRAG_CONTEXT_KEY, getElementIndex } from './common';
import { prefixName } from '../theme/index';
import type { DragContext } from './common';
const baseClassName = `${prefixName}-drag-item`;

const domItem = ref<HTMLDivElement>();
const dragContext: DragContext | undefined =
  inject<DragContext>(DRAG_CONTEXT_KEY);

const onDragItem = (e: DragEvent) => {
  e.stopPropagation();
  const index = getElementIndex(domItem?.value || null);
  if (dragContext && dragContext?.activeIndex >= -1) {
    dragContext.activeIndex = index;
  }
};
</script>

完整代码在源码文件 packages/business/src/drag/drag-item.vue 里。

第三步,组合父容器和子容器。

这是最重要的,我们要将子容器和父容器组件进行组合,同时要处理子容器里可以用插槽来实现自定义拖拽的内容。所以,这里需要一个Map,来注册有哪些自定义组件要进行拖拽,然后再加一个数组,来描述拖拽的初始位置。

最后我用了Vue.js的 <component> 组件来实现动态组件渲染,并实现了自定义渲染拖拽内容。具体代码如下所示:

<template>
  <div :class="baseClassName">
    <DragLayout @dragOver="onDragOver" @dragEnd="onDragEnd">
      <DragItem v-for="(item, index) in viewData.list" :key="index">
        <div>{{ item.name }}</div>
        <component
          v-if="item?.componentName"
          :is="componentMap?.[item.componentName]"
        ></component>
      </DragItem>
    </DragLayout>
  </div>
</template>

<script setup lang="ts">
import { reactive, toRaw } from 'vue';
import DragLayout from './drag-layout.vue';
import DragItem from './drag-item.vue';
import { prefixName } from '../theme/index';
import type { Component } from 'vue';
import type { DragContext } from './common';
const baseClassName = `${prefixName}-drag`;

const props = defineProps<{
  componentMap: { [name: string]: Component };
  layoutList: { name: string; componentName: string }[];
}>();

const emits = defineEmits<{
  (
    event: 'change',
    e: { layoutList: { name: string; componentName: string }[] }
  ): void;
}>();

const componentMap = toRaw(props.componentMap);
const prevContext: DragContext = {
  activeIndex: -1,
  dragToIndex: -1
};

// TODO
const clone = (data: unknown) => {
  return JSON.parse(JSON.stringify(data));
};

let layoutList: { name: string; componentName: string }[] = toRaw(
  props.layoutList
);

const viewData = reactive<{
  list: { name: string; componentName: string }[];
}>({
  list: clone(layoutList)
});

function resetLayoutList(context: DragContext) {
  const { activeIndex, dragToIndex } = context;
  const tempList: { name: string; componentName: string }[] = clone(layoutList);
  if (
    prevContext.activeIndex === activeIndex &&
    prevContext.dragToIndex === dragToIndex
  ) {
    return;
  }

  if (activeIndex >= 0 && dragToIndex >= 0) {
    const [target] = tempList.splice(activeIndex, 1);
    if (dragToIndex === 0) {
      tempList.unshift(target);
    } else if (dragToIndex >= tempList.length) {
      tempList.push(target);
    } else {
      tempList.splice(dragToIndex, 0, target);
    }
    viewData.list = tempList;
  }

  prevContext.activeIndex = activeIndex;
  prevContext.dragToIndex = dragToIndex;
}

const onDragOver = (ctx: DragContext) => {
  resetLayoutList(ctx);
};

const onDragEnd = () => {
  layoutList = toRaw(viewData.list);
  emits('change', { layoutList });
};
</script>

完整代码在源码文件 packages/business/src/drag/drag.vue 里。

当我们基于Vue3实现了拖拽功能组件代码后,可以这么使用:

<template>
  <div>
    <Drag
      :componentMap="componentMap"
      :layoutList="layoutList"
      @change="onChange"
    ></Drag>
  </div>
</template>

<script setup lang="ts">
import { Drag } from '../src/index';
import Mod1 from './modules/mod-1.vue';
import Mod2 from './modules/mod-2.vue';
import Mod3 from './modules/mod-3.vue';

const componentMap = {
  Mod1: Mod1,
  Mod2: Mod2,
  Mod3: Mod3
};

const layoutList = [
  {
    name: '组件001',
    componentName: 'Mod1'
  },
  {
    name: '组件002',
    componentName: 'Mod2'
  },
  {
    name: '组件003',
    componentName: 'Mod3'
  }
];

const onChange = (e: unknown) => {
  console.log('onChange ===', e);
};
</script>

具体效果如图所示:

图片

现在,我们基于Vue.js 3.x正式实现了拖拽布局组件。

如何优雅扩展拖拽布局组件的业务能力?

在实际的业务项目中,业务方可能会随时随地修改功能的业务需求,所以,做这类拖拽组件,我们可能会有随时快速定制的要求,例如需要拖拽布局组件可以自定义横向和纵向的布局操作等。那如何优雅扩展拖拽布局组件的业务能力呢?

我们可以基于上述封装Vue3组件,对组件的Props进行调整和修改。

我们可以添加一个horizontal的Prop,来控制整个拖拽功能的布局方向CSS样式,具体就是根据这个horizontal变量来判断是否要加上对应的className。而且,还要改造一下父容器布局的CSS样式,也就是 drag-layout.vue的代码。

修改如下所示:

<template>
  <div
    :class="{ [baseClassName]: true, ['is-horizontal']: props.horizontal }"
    @dragstart="onDragStart"
    @dragover="onDragOver"
    @dragend="onDragEnd"
  >
    <slot></slot>
  </div>
</template>

<script setup lang="ts">
// 省略原来其他代码 ...

// 修改代码
const props = defineProps<{ horizontal?: boolean }>();

// 省略原来其他代码 ...
</script>

添加布局样式:

.@{prefix-name}-drag-layout {
  // 省略原来其他代码 ...

  &.is-horizontal {
    flex-direction: row;
  }
}

实现效果如动图所示:

图片

总之,如果后续业务方要对业务组件的功能进行修改或者定制,你可以封装成对应的Vue3组件的Props API进行控制,尽可能用最小的代码修改量,对组件的功能进行调整。

总结

现在,你应该对拖拽布局组件的实现有清晰的认识了。拖拽布局组件的功能很难界定,业务需求也变化多端,所以我们把它定位成业务组件。

基于原生JavaScript API来实现拖拽布局功能,核心就是要定义“父容器”来承载和限制拖拽的范围,定义“子容器”来控制拖拽操作。父、子容器拖拽交互和子容器重新排序,主要是要记录每次“被拖拽”和“拖拽到”两个容器位置的序号数据,再通过这两个数据,换算出重新排序的位置,最后达到排序重新渲染。

基于原生JavaScript API实现的步骤,我们再通过Vue.js 3.x的API重新实现了一遍。如果你真的充分理解了原生JavaScript API和Vue.js 3.x实现步骤,即使换成React.js,你也完全可以实现出类似的拖拽功能,这也是我前面说的“授人以渔”。

最后,业务需求都是千变万化,在实现业务组件时候,你要时刻做好用优雅的改造方式对原有组件做功能调整和改造的准备。

思考题

我们实现了拖拽布局组件,只是通过拖拽调整了布局,那么,如果要从一个布局拖拽到另外一个布局里,应该怎么实现这个“拖放”布局组件呢?

期待你的留言,和我和其他同学一起讨论。下节课见。

完整的代码在这里

精选留言(6)
  • x 👍(0) 💬(1)

    老师您好,我想弄个简单的东西,让运营自己整一些简单的页面。比如我定义一个p标签的对象,pobj={tag:'p',attrs:{}}这种格式。然后运营拖特定组件到固定地方后在一个全局json中添加这个对象,并且在这个对象中添加一个全局唯一的id。然后运营在页面中点击我通过一个深度递归函数生成的这个p,就可以通过id找到这个对象从而给这个元素修改宽高等属性。请老师指教一下。。我感觉我现在这样写走歪了。。

    2023-01-25

  • 定宇 👍(0) 💬(3)

    老师您好 想问一下后续会有讲到打包Vue组件到npm上+index.d.ts的内容吗 最近刚好遇到类似的需求 但研究了两天没有什么太好的解法 尤其简单打包然后npm install后没办法有ts提示

    2022-12-29

  • ifelse 👍(0) 💬(0)

    学习打卡

    2024-09-16

  • Geek_12e8fd 👍(0) 💬(0)

    DropTarget.vue(放置目标): vue <template> <div class="drop-target" @dragover.prevent @drop="dropItem" > <!-- 容器内容 --> </div> </template> <script> export default { methods: { dropItem(event) { // 阻止默认行为 event.preventDefault(); // 从拖拽数据中获取被拖拽项的 id const itemId = event.dataTransfer.getData('text/plain'); // 调用父组件或状态管理逻辑来移动项 this.$emit('itemDropped', itemId); } }, // ... 其他选项 ... }; </script> 在父组件或状态管理逻辑中,你需要监听 itemDropped 事件,并根据需要更新数据模型或执行其他逻辑来移动项。这通常涉及从源容器数组中移除项,并将其添加到目标容器数组中。然后,你可以使用 Vue 的响应性系统来自动更新 UI。 请注意,这只是一个简化的示例,实际应用中可能需要处理更复杂的逻辑和边界情况。此外,还可以考虑使用第三方库(如 Vue.Draggable)来简化拖拽和放置功能的实现。

    2024-06-14

  • Geek_12e8fd 👍(0) 💬(0)

    实现从一个布局拖拽到另外一个布局里的“拖放”布局组件,需要处理跨容器的拖拽逻辑。这通常涉及几个关键步骤: 监听拖拽事件: 在被拖拽的元素上监听 dragstart 事件,设置拖拽的数据(通常使用 setData 方法)。 在目标容器上监听 dragover 事件,并阻止其默认行为(使用 event.preventDefault()),以允许放置。 在目标容器上监听 drop 事件,处理放置逻辑。 设置允许放置: 为了使元素可以被放置到目标容器内,目标容器需要设置 dropzone 属性或者通过 CSS 设置 user-select: none; 和合适的指针样式。 处理放置逻辑: 在 drop 事件处理函数中,从拖拽数据中获取被拖拽元素的信息。 根据需要从源容器移除被拖拽的元素,并添加到目标容器。 更新任何相关的数据模型或状态,以反映新的布局。 处理边界情况: 确保拖拽操作不会超出浏览器窗口或容器的边界。 处理被拖拽元素与容器内其他元素的碰撞检测(如果需要)。 反馈和动画: 提供视觉反馈,如高亮显示目标容器或预览放置位置。 使用 CSS 动画或过渡来平滑地移动被拖拽的元素。 错误处理和兼容性: 处理不同浏览器对 HTML5 拖拽 API 的支持差异。 添加错误处理逻辑,以应对拖拽过程中可能出现的任何问题。 以下是一个简化的代码示例,展示了如何在 Vue.js 组件中实现跨容器的拖拽放置功能: DraggableItem.vue(可拖拽的项): vue <template> <div class="draggable-item" draggable="true" @dragstart="dragStart" > <!-- 内容 --> </div> </template> <script> export default { methods: { dragStart(event) { // 设置拖拽数据 event.dataTransfer.setData('text/plain', this.itemId); // 假设每个项都有一个唯一的 itemId } }, // ... 其他选项 ... }; </script>

    2024-06-14

  • escray 👍(0) 💬(0)

    提一个小白问题,在执行第 15 课的源码的时候,提示如下错误: The following dependencies are imported but could not be resolved: @my/components/css/index.css (imported by ../vue3-course/chapter/15/packages/business/main.ts) Are they installed? 应该是我引用的那个 css 文件没有正确的加载,是少了什么命令操作么?

    2024-01-09