從零開始實現一個線上三角形生成器

夕水發表於2021-10-25

線上三角形生成器

通過本文,你將學到如下知識:

  1. 快速入門vue 3.2的核心API知識
  2. 掌握最新瀏覽器實現的複製貼上的clipboard API
  3. 按需引入element plus
  4. vite 的一些入門配置
  5. 正規表示式以及typescript的型別
  6. less語法
  7. element plus 國際化

本示例的實現靈感來自於徐小夕大佬的線上三角形生成器--文章線上三角形生成器--示例,感謝大佬提供的靈感。

快速建立一個vite專案

參考文件官網vite。我們可以快速建立一個專案:

# npm 6.x
npm init vite@latest triangle --template vue

# npm 7+, extra double-dash is needed:
npm init vite@latest triangle -- --template vue

# yarn
yarn create vite triangle --template vue

接下來,我們需要再額外新增一些依賴。

yarn add unplugin-vue-components element-plus less

unplugin-vue-components是element plus提供的一個按需引入實現的外掛。然後修改vite.config.js的程式碼如下:

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import Components from "unplugin-vue-components/vite"
import { ElementPlusResolver  } from "unplugin-vue-components/resolvers"
// https://vitejs.dev/config/
export default defineConfig({
  plugins: [
    vue(),
    Components({
      resolvers:[ElementPlusResolver()]
    })
  ],
  base:"./"
})

全是照著element plus官方文件來一步一步操作的。

接下來,在main.js中引入element plus的樣式檔案:

import "./style/reset.less"
import 'element-plus/dist/index.css'

其中reset.less的程式碼如下:

body,h1,img,h2,h3,h4,h5,h6,p {
    margin: 0;
    padding: 0;
}
.app {
    width: 100vw;
    height: 100vh;
    background: linear-gradient(135deg,#e0e0e0 10%,#f7f7f7 90%);
    font-family: Avenir, Helvetica, Arial, sans-serif;
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
    overflow-y: auto;
    overflow-x: hidden;
}
::-webkit-scrollbar {
    width: 0;
    height: 0;
 }

如此一來,準備工作算是完成了,接下來,我們就來一步一步的實現。

實現的工具函式

在src目錄下建立一個utils目錄,然後新建一個utils.ts檔案,裡面寫上如下程式碼:

export const getTriangleStyle = (direction:string,w:number,h:number,color:string) => {
     const style = {
         "top":{
             "borderColor":`transparent transparent ${color} transparent`,
             "borderWidth":`0 ${w / 2}px ${h}px ${w / 2}px`
         },
         "bottom":{
            "borderColor":`${color} transparent transparent transparent`,
            "borderWidth":`${h}px ${w / 2}px 0 ${w / 2}px`
         },
         "left":{
            "borderColor":`transparent ${color} transparent transparent`,
            "borderWidth":`${h / 2}px ${w}px ${h / 2}px 0`
         },
         "right":{
            "borderColor":`transparent transparent transparent ${color}`,
            "borderWidth":`${h / 2}px 0 ${h / 2}px  ${w}px`
         }
     }
     return style[direction];
}

這個工具函式其實也就是實現三角形的方向切換問題。

頁面分析

接下來,我們來看頁面的整體。其實包含了五大部分,如下圖所示:

即:

  1. 頭部元件(包含標題元件)
  2. 操作樣式的表單
  3. 預覽模組
  4. 程式碼編輯器
  5. 底部資訊

頭部元件

我們一部分一部分的來看,首先是頭部元件的實現,頭部元件只是包含一個標題元件,所以我們先來看標題元件的實現。如下所示:

template部分:

<component :is="'h' + level">
    <slot>{{ content }}</slot>
</component>

js部分:

<script setup>
   import { defineProps } from '@vue/runtime-core';
   const props = defineProps({
       level:{
           type:[String,Number],
           default:1
       },
       content:String
   });
</script>

就這麼一點程式碼,我們就需要了解vue3.x的四個知識點。

  1. vue可以為script標籤新增setup,從而使得整個程式碼塊都在setup鉤子函式作用域中,setup鉤子函式相當於vue2.x的beforeCreated和created的合併,也是vue3.x composition API 的入口函式。
  2. 匯入defineProps就可以定義vue的props單向資料流。這裡定義了2個欄位,即levelcontent。顧名思義,level就是用於動態元件的,我們實際上就是封裝一個動態元件,元件的標籤是h1~h6,level的預設值是1。它的型別可以使字串或者數值。而content就是字串,被用作插槽的備用內容。
  3. 動態元件component,通過繫結is屬性可以知道元件名。
  4. 插槽slot。

正好,我們的頭部就用到了這個標題元件,接下來我們來看頭部元件即Header元件的實現。

template部分:

<header class="tri-header">
    <Title level="2" class="tri-title">
        趣談前端|線上三角形生成器
    </Title>
    <slot></slot>
</header>

js部分:

<script setup>
    import Title from "./Title.vue";
</script>

style部分:

<style lang="less">
    .tri-header {
        width: 100%;
        height: 60px;
        display: flex;
        justify-content: center;
        align-items: center;
        color: #535455;
    }
</style>

可以看到頭部元件,我們使用彈性盒子佈局,讓元件垂直水平居中,字型顏色為#535455。在js部分,我們直接匯入了前面我們封裝的標題元件。在模板部分,我們直接使用了header元素包裹這個標題元件。並且新增了一個插槽元素。

這樣一來,我們的頭部元件部分就完成了,比較簡單。

表單部分

接下來我們來看錶單部分。

template部分:

<el-form class="tri-form">
    <el-form-item label="方向:">
        <el-radio-group v-model="state.form.direction">
            <el-radio v-for="(item,index) in state.radioList" :key="item.value + index":label="item.value" class="tri-radio">{{ item.label }}</el-radio>
        </el-radio-group>
    </el-form-item>
    <el-form-item label="寬度:">
        <el-slider v-model="state.form.width" :min="0":max="200"></el-slider>
    </el-form-item>
    <el-form-item label="高度:">
        <el-slider v-model="state.form.height" :min="0":max="200"></el-slider>
    </el-form-item>
    <el-form-item label="旋轉角度:">
        <el-slider v-model="state.form.rotate" :min="0":max="360"></el-slider>
    </el-form-item>
    <el-form-item label="背景色:">
        <el-config-provider :locale="state.locale">
            <el-color-picker v-model="state.form.color"><el-color-picker>
        </el-config-provider>
    </el-form-item>
</el-form>

js部分:

<script setup lang="ts">
import zhCn from 'element-plus/lib/locale/lang/zh-cn'
import { ElForm,ElSlider,ElRadio,ElRadioGroup,ElFormItem,ElColorPicker,ElConfigProvider } from 'element-plus';
import { reactive,defineEmits, watch } from 'vue-demi';
const state = reactive({
    form:{
        direction:"top",
        width:60,
        height:60,
        color:"#2396ef",
        rotate:0
    },
    radioList:[
        { label:"上",value:"top"},
        { label:"下",value:"bottom"},
        { label:"左",value:"left"},
        { label:"右",value:"right"}
    ],
    locale:zhCn
});
const emit = defineEmits(["on-change"]);
watch(() => state.form,(val) => {
    emit("on-change",val);
},{ deep:true,immediate:true })
</script>

style部分:

<style lang="less" scoped>
      @media (max-width: 1000px) {
          .tri-radio {
              margin-right: 10px;
          }
      }
</style>

在這裡,我們分析頁面的部分,我們知道,我們需要用到單選框分組元件,單選框元件,顏色選擇器元件,表單元件,國際化配置元件(element plus新增的elConfigProvider,個人理解設計借鑑了react的Provider元件),滑塊元件。單選框元件用於修改三角形的方向,滑塊元件用於配置三角形的寬高以及旋轉角度,而顏色選擇器元件用於配置三角形的背景顏色。所以我們定義瞭如下物件:

form:{
    direction:"top",//方向
    width:60,//寬度
    height:60,//高度
    color:"#2396ef",//背景色
    rotate:0 //旋轉角度
},

我們使用vue的reactive方法來定義響應式資料。由於顏色選擇器預設是英文,所以我匯入了element plus的中文包。即:

import zhCn from 'element-plus/lib/locale/lang/zh-cn'

然後再顏色選擇器中,新增el-config-provider元件包裹顏色選擇器。實際上我這裡只是單獨設定顏色選擇器的中文包,這個元件應該是包裹在根元素元件App的。然後我們使用了defineEmits發出一個事件給父元件使用。如下:

const emit = defineEmits(["on-change"]);
watch(() => state.form,(val) => {
    emit("on-change",val);
},{ deep:true,immediate:true })

我們使用watch方法監聽表單資料物件,並且提供了配置選項,也就是說該元件在建立的時候就會立即執行一次該方法,然後發出一個on-change事件,將form表單資料給傳給父元件使用。

預覽元件

接下來,我們來看預覽元件的實現:

template部分:

<div class="tri-code-effect">
    <div class="tri-element" :style="state.style"></div>
</div>

js部分:

<script lang="ts" setup>
    import { getTriangleStyle } from "../utils/util";
    import { defineProps,reactive,watch } from '@vue/runtime-core';
    const props = defineProps({
        formData:{
            type:Object,
            default:() => ({
                width:60,
                height:60,
                direction:"top",
                color:"#2396ef",
                rotate:0
            })
        }
    });
    
    const state = reactive({
        style:{}
    });
    watch(() => props.formData,(val) => {
        const { direction,color,width,height,rotate } = val;
        state.style = { ...getTriangleStyle(direction,width,height,color),transform:`rotate(${rotate}deg)`};
    },{ deep:true,immediate:true })
</script>

style部分:

<style lang="less" scoped>
    .tri-code-effect{
        min-width: 300px;
        height: 350px;
        display: flex;
        justify-content: center;
        align-items: center;
        background: linear-gradient(45deg,rgba(0,0,0,.2) 25%,transparent 0,transparent 75%,rgba(0,0,0,.2) 0),
                    linear-gradient(45deg,rgba(0,0,0,.2) 25%,transparent 0,transparent 75%,rgba(0,0,0,.2) 0);
        background-size: 30px 30px;
        background-position: 0 0,15px 15px;
        .tri-element {
            display: inline-block;
            border-style: solid;
            width: 0;
            height: 0;
            transition: all .3s;
        }
    }
</style>

可以看到預覽元件就2個元素,其中父元素就是我們最外層的盒子元素,盒子元素通過設定漸變,才會出現那樣的效果。然後我們的三角形元素,它的基本樣式還是有一些不會變動的,所以我們寫在樣式當中,變動的樣式我們才定義在資料中。可以看到,我們通過接受父元件穿過來的formData表單資料物件,然後需要進行樣式物件的規範化處理,這才是我們監聽函式的意義所在:

watch(() => props.formData,(val) => {
    const { direction,color,width,height,rotate } = val;
    state.style = { ...getTriangleStyle(direction,width,height,color),transform:`rotate(${rotate}deg)`};
},{ deep:true,immediate:true })

我們也是使用了immediate選項,會讓元件在建立的時候就立即呼叫一次,然後我們通過物件解構拿到父元件出來的props資料,並且修改定義在reactive方法實現的響應式資料中。然後再繫結到元素的style屬性上。

根元件

接下來是第三部分,程式碼編輯器的部分,我們是直接寫在根元件APP.vue中的,可以看到程式碼編輯器的部分包含三塊。

  1. 標題
  2. 複製程式碼按鈕
  3. 顯示程式碼的文字框(禁用)

接下來,我們就來看一下根元件的程式碼吧。

template部分:

<Header></Header>
<main class="tri-main">
    <el-row class="tri-row">
        <el-col :span="12" class="tri-left tri-col">
            <Form @on-change="changeForm"></Form>
        </el-col>
        <el-col :span="12" class="tri-right tri-col">
            <code-effect :formData="state.formData"></code-effect>
        </el-col>
    </el-row>
    <el-row class="tri-row tri-code-row">
        <el-col :span="12" class="tri-left tri-col">
            <el-header class="tri-header tri-fcs">
                  <Title level="2">CSS程式碼</Title>
                  <el-button @click="onCopyCodeHandler">複製程式碼</el-button>
            </el-header>
            <el-input :autosize="{ minRows: 8, maxRows: 10 }" type="textarea" v-model="state.code" disabled></el-input>
        </el-col>
    </el-row>
    <el-footer class="tri-footer">
        inspired by <el-link href="http://49.234.61.19/tool/cssTriangle" target="_blank" type="primary" class="tri-link">線上三角形樣式生成器</el-link>
        更多示例盡在<el-link href="https://eveningwater.com/my-web-projects/home/" target="_blank" type="primary" class="tri-link">我的個人專案集合</el-link>中。
    </el-footer>
</main>

js部分:

<script setup>
import Header from './components/Header.vue';
import Title from "./components/Title.vue";
import CodeEffect from './components/CodeEffect.vue';
import { ElRow,ElCol,ElInput,ElHeader,ElButton,ElFooter,ElLink,ElMessageBox } from 'element-plus';
import { nextTick, reactive } from 'vue-demi';
const state = reactive({ formData:{},code:"" })
const changeForm = (form) => {
   state.formData = {...form};
   nextTick(() => {
      const codeEffect = document.querySelector(".tri-element");
      const style = codeEffect.style.cssText;
      const templateStyle = `.tri-element {display: inline-block;border-style: solid;width: 0;height: 0;${style.replace(/\;(.*?)\:/g,w => w.slice(0,1) + w.slice(1).trim())}}`;
      state.code = templateStyle.replace(/\;/g,";\n").replace(/\{|\}/,w => w + "\n").replace(/((.*?)\:)/g,w => "     "+ w);
   })
}
const confirm = () => {
  ElMessageBox.alert(`CSS程式碼已複製,請貼上檢視!`,"溫馨提示",{
     confirmButtonText:"確定",
     callback:() => {}
  })
}
const onCopyCodeHandler = () => {
  // `navigator.clipboard.writeText` not working in wechat browser.
  if(navigator.userAgent.toLowerCase().indexOf('micromessenger') === -1){
    navigator.clipboard.writeText(state.code).then(() => confirm())
  }else{
      const input = document.createElement("input");
      input.value = state.code;
      document.body.appendChild(input);
      input.select();
      document.execCommand("copy");
      input.remove();
      confirm();
  }
}
</script>

style樣式部分:

<style lang="less" scoped>
  .tri-main {
     display:flex;
     min-height: 600px;
     flex-direction: column;
     justify-content: center;
     width: 100%;
     min-width: 750px;
     max-width: 980px;
     margin: auto;
     .tri-left {
       background-color: #fff;
       box-shadow: 0 4px 12px rgba(255,255,255,.4);
       height: 350px;
       padding: 10px;
       border-radius: 2px;
     }
     .tri-header.tri-fcs {
       justify-content: space-between;
       padding: 0;
     }
     .tri-code-row {
       margin-top: 15px;
     }
     .tri-row {
       margin-left: -12.5px;
       margin-right: -12.5px;
       .tri-col {
          padding-left: 12.5px;
          padding-right: 12.5px;
       }
     }
     .tri-footer {
       align-items: center;
       display: flex;
       color: #535455;
       font-size: 14px;
       justify-content: center;
       flex-wrap: wrap;
       .tri-link {
         margin: 0 1%;
       }
     }
  }
  @media (max-width: 1000px) {
        .tri-left,.tri-right {
           width: 100%;
           flex:0 0 100%;
           max-width: 100%;
           overflow-x: hidden;
           .tri-code-effect {
             margin-top: 15px;
           }
        }
        .tri-main {
          padding: 10px 0;
          min-width: 300px;
          max-width: calc(100% - 20px);
          margin: 0 10px;
          overflow-x: hidden;
          .tri-row {
            min-width: 100%;
            max-width: 100%;
            margin: 0;
            &.tri-code-row {
              margin-top: 15px;
            }
            .tri-col:not(.tri-right) {
              padding: 10px;
            }
            .tri-col.tri-right {
              padding: 0;
            }
          }
        }
    }
</style>

由於相容了移動端佈局,所以樣式程式碼看起來有點多。但整體就是利用媒體查詢來調整一下。這裡我們用到了element plus的ElRow,ElCol,ElInput,ElHeader,ElButton,ElFooter,ElLink,ElMessageBox元件。template的元件元素應該是很好理解的,包含的五個部分都寫進去了。

tips:這裡需要提醒一下,寫vue3.x的語法需要安裝volar外掛。

接下來我們來看js部分,js部分其實就是匯入了需要使用的元件,然後接受了子元件傳來的表單資料。並且我們獲取到了三角形元素的css程式碼,然後渲染到文字框中去。這裡我們操作css樣式獲取到了元素的css程式碼字串,然後利用正規表示式處理了一下,最後才能得到我們實際顯示的那樣保持規範化的縮排而顯示的程式碼。

我們做的一系列正規表示式的匹配,就是為了讓程式碼顯示保持合格的縮排。

比如新增\n換行符,css樣式屬性名的前面新增一段空白。這些都沒什麼好說的。這裡比較有意思的實現,就是複製程式碼的實現:

const onCopyCodeHandler = () => {
  // `navigator.clipboard.writeText` not working in wechat browser.
  if(navigator.userAgent.toLowerCase().indexOf('micromessenger') === -1){
    navigator.clipboard.writeText(state.code).then(() => confirm())
  }else{
      const input = document.createElement("input");
      input.value = state.code;
      document.body.appendChild(input);
      input.select();
      document.execCommand("copy");
      input.remove();
      confirm();
  }
}

在微信瀏覽器端不知道是不是因為內建view元件的實現原因,並不支援clipboardAPI。我們通過獲取到navigator下的clipboard屬性,它就是一個剪貼簿物件,然後我們可以呼叫writeText方法,就可以往剪貼簿中寫入內容。也就實現了使用者滑鼠複製需要複製的內容的實現,該方法返回一個promise,在then方法中,我們彈出一個提示框,用於提示使用者程式碼內容已經複製了。

而在微信瀏覽器端,我們還是通過建立一個input元素,將複製的內容賦給input元素,然後設定選中,呼叫document.execCommand("copy")事件。最後彈出提示。如此一來,我們的三角形生成器就這樣實現了。

最後

如果覺得本文能讓你學到東西,望不吝嗇點個贊。詳細原始碼可以檢視這裡。最後感謝大家的閱讀。

相關文章