胖蔡说技术
随便扯扯

在SVG中创建UI组件

我完全相信SVG开启了整个网络构建界面的世界。刚开始学习SVG可能会让人望而生畏,但您有一个专门用于创建形状的规范,但仍然有一些元素,如文本、链接和咏叹调标签。您可以在CSS中实现一些相同的效果,但更特别的是要正确定位,尤其是在视口之间和响应式开发中。

SVG的特殊之处在于,所有的定位都是基于坐标系的,有点像游戏《战舰》。这意味着决定一切走向何方、如何绘制,以及它们之间的相对关系,可以非常简单地进行推理。CSS定位是用于布局的,这很好,因为在文档流方面,您有彼此对应的东西。如果你正在制作一个非常特别的组件,其中有重叠和精确放置的元素,那么这种积极的特性就更难处理了。

的确,一旦你学会了SVG,你就可以画任何东西,并在任何设备上缩放。

在这篇文章中,我们不会涵盖关于SVG的所有内容(您可以在这里、这里、这里和这里学习一些基本知识),但为了说明SVGUI组件开发开辟的可能性,让我们讨论一个特定的用例,并分解我们将如何考虑构建自定义的东西。

时间线任务列表组件

那么我们该怎么做呢?我将在VueReact中展示一个例子,这样您就可以看到它在这两个框架中是如何工作的。

Vue

我们决定在Next.js中制作该平台用于dogfooding目的(即在Netlify构建插件上尝试我们自己的Next),但我对Vue更流利,所以我用Vue编写了最初的原型,并将其移植到React。

<template>
  <div id="app">
    <div>
      <svg :viewBox="`0 0 30 ${tasks.length * 50}`"
           xmlns="http://www.w3.org/2000/svg" 
           width="30" 
           stroke="currentColor" 
           fill="white"
           aria-labelledby="timeline"
           role="presentation">
        <title id="timeline">timeline element</title>
        <line x1="10" x2="10" :y1="num2" :y2="tasks.length * num1 - num2" />
        <circle 
          @click="selectThis(i)" 
          v-for="(task, i) in tasks"
          :key="task.name"
          cx="10"
          :cy="i * num1 + num2" r="4"
          :fill="task.done ? 'currentColor' : 'white'"
          class="select"/>
      </svg>
    </div>
    
    <div>
      <div 
        @click="selectThis(i)"
        v-for="(task, i) in tasks"
        :key="task.name"
        class="select">
        {{ task.name }}
      </div>
    </div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      num1: 32,
      num2: 15,
      tasks: [
        {
          name: 'thing',
          done: false
        },
        {
          name: 'other thing',
          done: false
        },
        {
          name: 'tacos',
          done: false
        },
        {
          name: 'yaya',
          done: false
        },
         {
          name: 'more things',
          done: false
        },
        {
          name: 'tada',
          done: false
        },
      ]
    };
  },
  methods: {
    selectThis(index) {
      this.tasks[index].done = !this.tasks[index].done
    }
  }
};
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  color: #2c3e50;
  margin: 60px 120px;
  display: grid;
  grid-template-columns: 30px 1fr;
  grid-template-rows: 1fr;
  height: 400px;
  width: 400px;
  line-height: 2;
}
  
svg {
  margin-top: 3px;
}
  
.select {
  cursor: pointer;
}
</style>

让我们来浏览一下这段代码。首先,这是一个单一文件组件(SFC),因此模板HTML、反应脚本和作用域样式都封装在这个文件中。

我们将在数据中存储一些伪任务,包括每个任务是否完成。我们还将制作一个方法,可以对click指令进行调用,以便切换状态是否完成。

<script>
export default {
  data() {
    return {
      tasks: [
        {
          name: 'thing',
          done: false
        },
        // ...
      ]
    };
  },
  methods: {
    selectThis(index) {
      this.tasks[index].done = !this.tasks[index].done
    }
  }
};
</script>

现在,我们要做的是创建一个SVG,它根据元素的数量具有灵活的viewBox。我们还想告诉屏幕读者,这是一个表现元素,我们将提供一个具有唯一时间线id的标题。(获取有关创建可访问SVG的更多信息。)

<template>
  <div id="app">
    <div>
      <svg :viewBox="`0 0 30 ${tasks.length * 50}`"
           xmlns="http://www.w3.org/2000/svg" 
           width="30" 
           stroke="currentColor" 
           fill="white"
           aria-labelledby="timeline"
           role="presentation">
           <title id="timeline">timeline element</title>
        <!-- ... -->
      </svg>
    </div>
  </div>
</template>

笔划被设置为currentColor以允许一些灵活性——如果我们想在多个位置重用组件,它将继承封装div上使用的任何颜色。

接下来,在SVG中,我们想要创建一条垂直线,它是任务列表的长度。线条相当简单。我们有x1和x2值(其中线绘制在x轴上),类似地,还有y1和y2。

<line x1="10" x2="10" :y1="num2" :y2="tasks.length * num1 - num2" />

x轴始终保持在10,因为我们向下画一条线,而不是从左到右。我们将在数据中存储两个数字:我们希望间距为num1的量,以及我们希望边距为num2的量。

data() {
  return {
    num1: 32,
    num2: 15,
    // ...
  }
}

y轴从num2开始,从末尾减去num2和边距。tasks.length乘以间距,即num1。

现在,我们需要位于直线上的圆。每个圆圈都是任务是否已完成的指示器。我们每个任务需要一个圆圈,所以我们将使用v-for和一个唯一的键,即索引(在这里使用是安全的,因为它们永远不会重新排序)。我们将把click指令与我们的方法连接起来,并将索引作为参数传入。

SVG中的循环由三个属性组成。圆的中间画在cx和cy处,然后我们用r画一个半径。就像这条线一样,cx从10开始。半径是4,因为在这个比例下是可读的。cy的间距将像线一样:index乘以间距(num1),再加上边距(num2)。

最后,我们将使用三元来设置填充。如果任务完成,它将填充currentColor。如果没有,它将填充白色(或任何背景)。这可以用一个道具填充,这个道具可以在背景中通过,例如,在你有亮圈和黑圈的地方。

<circle 
  @click="selectThis(i)" 
  v-for="(task, i) in tasks"
  :key="task.name"
  cx="10"
  r="4"
  :cy="i * num1 + num2"
  :fill="task.done ? 'currentColor' : 'white'"
  class="select"/>

最后,我们使用CSS网格将div与任务名称对齐。这是以大致相同的方式布置的,即我们在任务中循环,并且还与相同的单击事件绑定以切换完成状态。

<template>
  <div>
    <div 
      @click="selectThis(i)"
      v-for="(task, i) in tasks"
      :key="task.name"
      class="select">
      {{ task.name }}
    </div>
  </div>
</template>

React版本

这就是我们最终推出React版本的地方。我们正在致力于开源,这样你就可以看到完整的代码及其历史。以下是一些修改:

  • 我们在Vue中使用CSS模块而不是SCF
  • 我们正在导入Next.js链接,这样我们就不会切换“完成”状态,而是将用户带到Next.js中的动态页面
  • 我们使用的任务实际上是课程的各个阶段——或者我们称之为“任务”——它们在这里传递,而不是由组件持有。

大多数其他功能都是相同的:

import styles from './MissionTracker.module.css';
import React, { useState } from 'react';
import Link from 'next/link';

function MissionTracker({ currentMission, currentStage, stages }) {
 const [tasks, setTasks] = useState([...stages]);
 const num1 = 32;
 const num2 = 15;

 const updateDoneTasks = (index) => () => {
   let tasksCopy = [...tasks];
   tasksCopy[index].done = !tasksCopy[index].done;
   setTasks(tasksCopy);
 };

 const taskTextStyles = (task) => {
   const baseStyles = `${styles['tracker-select']} ${styles['task-label']}`;

   if (currentStage === task.slug.current) {
     return baseStyles + ` ${styles['is-current-task']}`;
   } else {
     return baseStyles;
   }
 };

 return (
   <div className={styles.container}>
     <section>
       {tasks.map((task, index) => (
         <div
           key={`mt-${task.slug}-${index}`}
           className={taskTextStyles(task)}
         >
           <Link href={`/learn/${currentMission}/${task.slug.current}`}>
             {task.title}
           </Link>
         </div>
       ))}
     </section>

     <section>
       <svg
         viewBox={`0 0 30 ${tasks.length * 50}`}
         className={styles['tracker-svg']}
         xmlns="http://www.w3.org/2000/svg"
         width="30"
         stroke="currentColor"
         fill="white"
         aria-labelledby="timeline"
         role="presentation"
       >
         <title id="timeline">timeline element</title>

         <line x1="10" x2="10" y1={num2} y2={tasks.length * num1 - num2} />
         {tasks.map((task, index) => (
           <circle
             key={`mt-circle-${task.name}-${index}`}
             onClick={updateDoneTasks(index)}
             cx="10"
             r="4"
             cy={index * +num1 + +num2}
             fill={
               task.slug.current === currentStage ? 'currentColor' : 'black'
             }
             className={styles['tracker-select']}
           />
         ))}
       </svg>
     </section>
   </div>
 );
}

export default MissionTracker;
赞(0) 打赏
转载请附上原文出处链接:胖蔡说技术 » 在SVG中创建UI组件
分享到: 更多 (0)

评论 抢沙发

评论前必须登录!

 

请小编喝杯咖啡~

支付宝扫一扫打赏

微信扫一扫打赏