10
头图

Online triangle generator

Through this article, you will learn the following knowledge:

  1. Quick start vue 3.2 core API knowledge
  2. Master the copy and paste clipboard API implemented by the latest browsers
  3. Introduce element plus on demand
  4. Some entry configuration of vite
  5. Regular expressions and types of typescript
  6. less syntax
  7. element plus internationalization

Quickly create a vite project

Refer to the official website of the document vite . We can quickly create a project:

# 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

Next, we need to add some additional dependencies.

yarn add unplugin-vue-components element-plus less

unplugin-vue-components is an on-demand plug-in provided by element plus. Then modify the code of vite.config.js as follows:

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:"./"
})

All follow the element plus official document to operate step by step.

Next, introduce the element plus style file in main.js:

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

The code of reset.less is as follows:

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;
 }

In this way, the preparatory work is complete, and then, we will come to the realization step by step.

Implemented tool function

Create a utils directory under the src directory, and then create a new utils.ts file with the following code:

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];
}

This tool function is actually to switch the direction of the triangle.

Page analysis

Next, we look at the overall page. In fact, it contains five major parts, as shown in the following figure:

which is:

  1. Head component (including header component)
  2. Operation style form
  3. Preview module
  4. Code editor
  5. Bottom information

Head assembly

Let's look at it part by part. First, the realization of the head component. The head component only contains a heading component, so let's first look at the realization of the heading component. As follows:

template part:

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

js part:

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

With just such a little code, we need to understand the four knowledge points of vue3.x.

  1. Vue can add setup to the script tag, so that the entire code block is in the scope of the setup hook function. The setup hook function is equivalent to the combination of vue2.x's beforeCreated and created, and is also the entry function of the vue3.x composition API.
  2. Import defineProps to define the one-way data flow of vue props. Two fields are defined here, namely level and content . As the name suggests, level is used for dynamic components. We actually encapsulate a dynamic component. The label of the component is h1~h6 , and the default value of level is 1. Its type can be string or numeric value. And content is a string, which is used as the alternate content of the slot.
  3. Dynamic component component, the component name can be known by binding the is attribute.
  4. Slot.

It just so happens that our head uses this title component, and then we look at the implementation Header

template part:

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

js part:

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

style part:

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

You can see the head component. We use a flexible box layout to center the component vertically and horizontally. The font color is #535455 . In the js part, we directly imported the title component we encapsulated earlier. In the template part, we directly use the header element to wrap the header component. And added a slot element.

In this way, our head component part is completed, which is relatively simple.

Form part

Next we look at the form part.

template part:

<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 part:

<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 part:

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

Here, we analyze the part of the page, we know that we need to use radio button grouping components, radio button components, color selector components, form components, international configuration components (element plus new elConfigProvider, personal understanding of the design Borrowing from react's Provider component), slider component. The radio button component is used to modify the direction of the triangle, the slider component is used to configure the width, height and rotation angle of the triangle, and the color selector component is used to configure the background color of the triangle. So we define the following objects:

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

We use the reactive method of vue to define responsive data. Since the color picker is in English by default, I imported the Chinese package of element plus. which is:

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

Then in the color picker, add the el-config-provider component to wrap the color picker. In fact, I just set the Chinese package of the color picker separately here. This component should be wrapped in the root element component App . Then we used defineEmits send an event to the parent component. as follows:

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

We use the watch method to listen to the form data object and provide configuration options, which means that the component will execute this method immediately when it is created, and then send a on-change event form form data to the parent component for use.

Preview component

Next, let's look at the implementation of the preview component:

template part:

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

js part:

<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 part:

<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>

You can see that the preview component has two elements. The parent element is our outermost box element. The box element will have that effect by setting a gradient. Then, for our triangle element, some of its basic styles will not change, so we write in the style, and we define the changed style in the data. It can be seen that we accept the formData form data object passed through by the parent component, and then need to standardize the style object. This is the meaning of our monitoring function:

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 })

We also use the immediate option, which will make the component be called once when it is created, and then we get the props data from the parent component through object deconstruction, and modify the definition in the responsive data implemented by the reactive Then bind it to the style attribute of the element.

Root component

Next is the third part, the part of the code editor. We wrote it directly in the root component APP.vue . You can see that the part of the code editor contains three blocks.

  1. title
  2. Copy code button
  3. Text box showing code (disabled)

Next, let's take a look at the code of the root component.

template part:

<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 part:

<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 part:

<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>

Since it is compatible with the mobile layout, the style code looks a bit too much. But the whole is to use media queries to adjust. Here we use the ElRow,ElCol,ElInput,ElHeader,ElButton,ElFooter,ElLink,ElMessageBox component of element plus. The component elements of template should be well understood, and the five parts included are written in.

tips: I need to remind you that writing vue3.x syntax requires the installation of the volar plugin.

Next, let's look at the js part. The js part actually imports the components that need to be used, and then accepts the form data from the child components. And we got the css code of the triangle element, and then rendered it into the text box. Here we manipulate the css style to get the css code string of the element, and then use regular expressions to process it, and finally we can get the code that keeps the normalized indentation as we actually display.

We do a series of regular expression matching in order to keep the code display qualified indentation.

For example, add a \n , and add a blank before the css style attribute name. There is nothing to say about this. The more interesting implementation here is to copy the code:

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();
  }
}

On the WeChat browser side, I don’t know if it’s because of the implementation of the built-in view component that does not support the clipboard API. We get the clipboard property under the navigator, which is a clipboard object, and then we can call the writeText method to write content to the clipboard. It also realizes the realization of the user's mouse to copy the content that needs to be copied. This method returns a promise. In the then method, we pop up a prompt box to remind the user that the code content has been copied.

On the WeChat browser side, we still create an input element, assign the copied content to the input element, set the selected content, and call the document.execCommand("copy") event. Finally, a prompt pops up. In this way, our triangle generator is implemented like this.

finally

If you think this article can help you learn something, I hope you can give me a compliment. The detailed source code can be viewed at here . Finally, thank you all for reading.

tips: The implementation of this example is inspired by Xu 's 161777dd22bdf9 online triangle generator-article and online triangle generator-example , thank you for your inspiration.

夕水
5.2k 声望5.7k 粉丝

问之以是非而观其志,穷之以辞辩而观其变,资之以计谋而观其识,告知以祸难而观其勇,醉之以酒而观其性,临之以利而观其廉,期之以事而观其信。