19
头图

Introduction

In front-end development, sometimes you will encounter the need to scroll the wheel to achieve zooming of images, canvas, and DOM elements based on the current position of the mouse as the origin. Some students may find it a bit difficult, but in fact, with the help of matrix operations in linear algebra, this function can be realized very easily. More importantly, mathematics as a subject is universal and has nothing to do with specific programming languages and environments. A good grasp of the principle can achieve generality.

鼠标滚轮缩放元素和图片

The nature of scaling

The essence of scaling is a matrix transformation.

When we want to scale a Div element, we can generally think of it as scaling a rectangle. For ease of understanding, here we take the scaling of the simplest rectangle as an example. As shown in the figure below, we assume that there is a rectangle with a side length of 4. We take its center as the origin to establish a two-dimensional XY coordinate axis, and we can get the following figure:

01.png

When we enlarge the rectangle by 2 times, we will get a rectangle with side lengths of 8. Continue to use the center as the origin to establish a two-dimensional XY coordinate axis, and we can get the following figure:

02.png

If we mathematically abstract the graphic coordinate points of these two graphs, we can get the following two matrices:

Matrix A:

$$ \left[ \begin{matrix} -2 & 2 \\ 2 & 2 \\ 2 & -2 \\ -2 & -2 \\ \end{matrix} \right] $$

Matrix B:

$$ \left[ \begin{matrix} -4 & 4 \\ 4 & 4 \\ 4 & -4 \\ -4 & -4 \\ \end{matrix} \right] $$

That is to say, the matter of magnifying the rectangle by 2 times is actually just the transformation of matrix A into matrix B, so that we can skillfully convert the problem of rectangle scaling into the problem of conversion between matrices, which can be abstracted with the help of matrix mathematical formulas, Next, let's understand the basics of matrix transformation: matrix multiplication.

matrix multiplication

Provided A is matrix B as matrix, then said matrix C matrix A and B product, denoted , where the element at row and column in matrix C can be expressed as:

As follows:

There is another principle that needs special attention: A and B can be multiplied only when the number of columns (column) of of matrix A is equal to the number of rows (rows) of of matrix B , otherwise, matrix multiplication cannot be performed. Remember! Because of this principle and convenient calculation later, we will convert the 4x2 matrix to the 4x4 matrix.

For ease of understanding, here is the introduction of 3x3 matrix multiplication in the book "3D Mathematical Fundamentals: Graphics and Game Development" to help you understand and recall the specific details of matrix multiplication.

矩阵乘法.jpg

matrix transformation

When discussing transformations, functions (also called mappings) are generally used in mathematics, that is, accept input and produce output. We can denote the F function/map from a to b as F(a)=b . To use mathematical tools to solve the transformation between matrices (scaling is a kind of transformation, others include translation, rotation, shear, etc.), the easiest way is to find the mapping expressed by the matrix and its operation rules.

In elementary school, we all learned the four arithmetic operations. For example, there is a number a . If we want to a , we will use:

$$ a' = a * 2 $$

If we want to scale matrices, then we also need to find a similar multiplication rule, that is, what matrix is multiplied by a matrix to get its multiple. Remember the math we learned from kindergarten? In addition to the special number 0, the world we know of this number starts from 1 , and other numbers are obtained by adding and subtracting 1 , such as 2 we need above, which can be obtained by $$ 1 + 1 $$, then What is the 1 in the matrix becomes an important thing.

The one in the matrix 1 - the identity matrix

In the multiplication of matrix , there is a matrix that plays a special role, like 1 in the multiplication of numbers, this matrix is called the identity matrix. It is a square matrix , with elements on the diagonal (called the main diagonal ) from the upper left to the lower right that are all 1s. Everything else is 0.

The identity matrix of 2x2 $$ \left[ \begin{matrix} 1 & 0 \\ 0 & 1 \end{matrix} \right]$$, the identity matrix of 3x3 $$ \left[ \begin{matrix} 1 & 0 & 0 \\ 0 & 1 & 0 \\ 0 & 0 & 1 \end{matrix} \right]$$, the identity matrix of 4x4 $$ \left[ \begin{matrix} 1 & 0 & 0 & 0 \\ 0 & 1 & 0 & 0 \\ 0 & 0 & 1 & 0 \\ 0 & 0 & 0 & 1\\ \end{matrix} \right]$$

According to the characteristics of the identity matrix, any matrix multiplied by the identity matrix is equal to itself.

Now that you know what "2" is, what is "1" ? In fact, it is not difficult to guess, for example, the "2" of the 2x2 matrix is $$ \left[ \begin{matrix} 2 & 0 \\ 0 & 2 \end{matrix} \right]$$, that is, if there is a 2x2 matrix $$ A = \left[ \begin{matrix} 1 & 0 \\ 0 & 1 \end{matrix} \right]$$, then if $$ B = A * \left[ \begin{matrix} 2 & 0 \\ 0 & 2 \end{matrix} \right] $$, according to the calculation rules of matrix multiplication mentioned above, we can get $$ B = \left[ \begin{matrix} 2 & 0 \\ 0 & 2 \end{matrix} \right] $$, then we can think that the B matrix is twice the magnification of the A matrix.

scaling along the axes

The above-mentioned statement of magnifying the matrix by 2 times is for the convenience of understanding. In fact, it is more accurate to zoom in along the coordinate axis, because in addition to along the coordinate axis, can also be zoomed in any direction, for example Scale toward 45 degrees in the first quadrant of the axis. Since the mouse wheel zooming in this article does not involve the in any direction, I will write an article to explain this later.

2D scaling matrix along the axes

If there is a matrix $$ M= \left[\begin{matrix} p & 0 \\ 0 & q \end{matrix}\right]$$, we regard it as a 2D coordinate axis parallel to X The vector p of the axis and the vector q parallel to the Y axis are the two basis vectors. Assuming that there are 2 scaling factors: \( k_{x} \) and \( k_{y} \), then there are:

$$ p^{'}=k_{x}p=k_{x}\left[\begin{matrix} 1 & 0 \end{matrix}\right]=\left[\begin{matrix} k_{x} & 0 \end{matrix}\right] $$

$$ q^{'}=k_{y}p=k_{y}\left[\begin{matrix} 0 & 1 \end{matrix}\right]=\left[\begin{matrix} k_{y} & 0 \end{matrix}\right] $$

Using the basis vectors to construct the matrix, the 2D scaling matrix along the coordinate axis is as follows:

$$ S(k_{x},k_{y})=\left[ \begin{matrix} p^{'} \\ q^{'} \\ \end{matrix} \right]=\left[ \begin{matrix} k_{x} & 0 \\ 0 & k_{y} \end{matrix} \right] $$

For example, a matrix \(M\) representing a 2D plane needs to be enlarged by 2 times on the \(X\) axis and reduced by 3 times on the \(Y\) axis, then you can do this to get the converted matrix\(M^{ '}\):

$$ M^{'}=M*\left[ \begin{matrix} 2 & 0 \\ 0 & \frac{1}{3} \end{matrix} \right] $$

3D scaling matrix along the axes

For 3D, adding a third scaling factor \(k_{z}\), the 3D scaling matrix along the coordinate axis is as follows:

$$ S(k_{x},k_{y},k_{z})=\left[ \begin{matrix} k_{x} & 0 & 0 \\ 0 & k_{y} & 0 \\ 0 & 0 & k_{z} \end{matrix} \right] $$

4D scaling matrix along the axes

For 4D, adding a fourth scaling factor \(k_{W}\), the 4D scaling matrix along the coordinate axis is as follows:

$$ S(k_{x},k_{y},k_{z},k_{w})=\left[ \begin{matrix} k_{x} & 0 & 0 & 0 \\ 0 & k_{y} & 0 & 0 \\ 0 & 0 & k_{z} & 0 \\ 0 & 0 & 0 & k_{w} \end{matrix} \right] $$

How to represent a 2D matrix with a 3D matrix?

Compared with the 2D matrix, the 3D matrix has more expressions about the \(Z\) axis. Since the two-dimensional plane can be regarded as "flattened object" in the three-dimensional coordinate system, we need to give it a \( Z\) axis value, but cannot be 0 , at this time the value of \(Z\) axis is 1 .

For example, the 2D matrix A mentioned above: $$\left[ \begin{matrix} -2 & 2 \\ 2 & 2 \\ 2 & -2 \\ -2 & -2 \\ \end{matrix} \ right]$$, converted to a 3D matrix: $$\left[ \begin{matrix} -2 & 2 & 1 \\ 2 & 2 & 1 \\ 2 & -2 & 1 \\ -2 & -2 & 1 \\ \end{matrix} \right]$$

How to represent a 2D matrix with a 4D matrix?

Compared with the 2D matrix, the 4D matrix has more expressions about the \(Z\) axis and the \(W\) axis.

For example, the 2D matrix A mentioned above: $$\left[ \begin{matrix} -2 & 2 \\ 2 & 2 \\ 2 & -2 \\ -2 & -2 \\ \end{matrix} \ right]$$, which is converted into a 4D matrix: $$\left[ \begin{matrix} -2 & 2 & 1 & 1\\ 2 & 2 & 1 &1 \\ 2 & -2 & 1 & 1 \\ -2 & -2 & 1 & 1 \\ \end{matrix} \right]$$

Matrix calculation library gl-matrix

gl-matrix is an open source matrix computation library written in JavaScript language. We can use the operation function between matrices provided by this library to simplify and speed up our development. In order to avoid reducing the complexity, the following uses the native ES6 syntax, uses the <script> tag to directly reference JS library, and does not introduce any front-end compilation toolchain.

Scale the element with the current mouse position as the origin

In the previous article, we have simplified the scaling of elements to the scaling of rectangles. Next, we continue to abstract and simplify the scaling of rectangles to the scaling of coordinate points in the coordinate axis, so as to see the surface.

Suppose there are two coordinate points in \(XY coordinate axis\)\(\left( -3,0 \right)\) and \(\left( 3,0 \right)\), the distance between them is 6 , as shown below:

(3,0)和(-3,0).png

Extend the two coordinate points \(\left( -3,0 \right)\) and \(\left( 3,0 \right)\) with the origin as the center and extend along the \(X axis\) by 2 times , you can get new coordinate points \(\left( -6,0 \right)\) and \(\left( 6,0 \right)\), the distance between them is 12 , as shown below:

(6,0)和(-6,0).png

If you want to keep the distance between the two coordinate points at 12 units after zooming in, and the position of the coordinate point in the positive direction of \(X axis\) remains unchanged, then we need to zoom in and move the two coordinate points along \ (X axis\) Shift 3 units to the left, that is, -3 , as shown below:

(3,0)和(-9,0).png

Observe that:

$$ -3=3-3*2 = 3*(1-2) \\ 即: 缩放后在X/Y轴上偏移量=X/Y坐标值*(1-缩放倍数) $$

In fact, the above process is an abstraction of the process of the graphics with the current mouse point as the origin, that is, zooming the graphics first, and then moving the original zoom point back to the previous position.

4x4 translation matrix

Since the 3x3 transformation matrix represents a linear transformation, it does not include translation, but in 4D, the translation can still be expressed by matrix multiplication of the 4x4 matrix:

$$ \left[\begin{matrix}x &y &z &1 \end{matrix}\right]\left[\begin{matrix}1 &0 &0 &0\\ 0&1&0&0\\0&0&1&0\\\Delta x &\Delta y &\Delta z&1 \end{matrix}\right]=\left[\begin{matrix}x+\Delta x &y+\Delta y &z+\Delta z &1 \end{matrix}\right] $$

Matrix calculation expression zooms and then translates

Assuming the existing matrix \(v\), it scales and then translates, the scale matrix is $$R=\left[ \begin{matrix} k_{x} & 0 & 0 & 0 \\ 0 & k_{y} & 0 & 0 \\ 0 & 0 & k_{z} & 0 \\ 0 & 0 & 0 & k_{w} \end{matrix} \right]$$, the translation matrix is $$T=\left[\begin {matrix}1 &0 &0 &0\\ 0&1&0&0\\0&0&1&0\\\Delta x &\Delta y &\Delta z&1 \end{matrix}\right]$$, then:

$$v^{'}=v*R*T$$

The matrix realizes that the Div element is scaled with the mouse as the origin

Assume that the page now has a ID element whose div is app , located in the middle of the page, and the code is as follows:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>矩阵缩放Div</title>
    <style>
        *,
        *::before,
        *::after {
            box-sizing: border-box;
        }

        body {
            position: relative;
            background-color: #eee;
            min-height: 1000px;
            margin: 0;
            padding: 0;
        }

        #app {
            position: absolute;
            left: 50%;
            top: 50%;
            transform: translate(-50%, -50%);
            width: 200px;
            height: 200px;
            border: 1px dashed black;
        }
    </style>
</head>

<body>
    <div id="app"></div>
    <script src="./gl-matrix-min.js"></script>
    <script src="./index.js"></script>
</body>

</html>

The layout effect is as follows:

布局效果.png

First, we need to obtain the position information and width and height information of Div elements, and use them to form a matrix. This can be done with the help of the api # Element.getBoundingClientRect() .

Then listen div#app mouse scroll event. When scrolling, judge whether to zoom in or zoom out according to the value of deltaY of the event object. Here, in order to be consistent with the native zoom direction of the Windows system, choose to zoom down when the scroll wheel scrolls down, and zoom in when the scroll wheel scrolls up, that is Zoom in when the value of deltaY is less than 0 , and zoom out when it is less than 0 .

Matrix transformation and multiplication, since we are using 4x4 matrix, we can use the api glMatrix.mat4.multiply , so the code is as follows:

document.addEventListener("DOMContentLoaded", () => {
    const $app = document.querySelector(`#app`);

    $app.addEventListener("wheel", (e) => {
        const {clientX, clientY, deltaY } = e;
        let scale = 1 + (deltaY < 0 ? 0.1 : -0.1);
        scale = Math.max(scale > 0 ? scale : 1, 0.1);
        const {top, right, bottom, left}   = $app.getBoundingClientRect();
        const o = new Float32Array([
            left, top, 1, 1,
            right, top, 1, 1,
            right, bottom, 1, 1,
            left, bottom, 1, 1
        ]);
        const x = clientX * (1 - scale);
        const y = clientY * (1 - scale);
        const t = new Float32Array([
            scale, 0, 0, 0,
            0, scale, 0, 0,
            0, 0, 1, 0,
            0, 0, 0, 1
        ]);
        const m = new Float32Array([
            1, 0, 0, 0,
            0, 1, 0, 0,
            0, 0, 1, 0,
            x, y, 0, 1
        ]);
        // 在XY轴上进行缩放
        let res1 = glMatrix.mat4.multiply(new Float32Array([
            0, 0, 0, 0,
            0, 0, 0, 0,
            0, 0, 0, 0,
            0, 0, 0, 0
        ]), t, o);
        // 在XY轴上进行平移
        const res2 = glMatrix.mat4.multiply(new Float32Array([
            0, 0, 0, 0,
            0, 0, 0, 0,
            0, 0, 0, 0,
            0, 0, 0, 0
        ]), m, res1);
        $app.setAttribute("style", `left: ${res2[0]}px; top: ${res2[1]}px;width: ${res2[4] - res2[0]}px;height: ${res2[9] - res2[1]}px;transform: none;`);
    });
});

The effect is as follows:

鼠标原点缩放.gif

Matrix implements Div element drag and drop

The use of matrix to achieve Div element drag and drop is similar to the code we usually use to achieve drag and drop, but the absolute positioning information data is formed into a translation matrix. The specific code is as follows:

document.addEventListener("DOMContentLoaded", () => {
    const $app = document.querySelector(`#app`);
    const width = $app.offsetWidth;
    const height = $app.offsetHeight;
    let isDrag = false;
    let x; // 鼠标拖拽时鼠标的横坐标值
    let y; // 鼠标拖拽时鼠标的纵坐标值
    let left; // 元素距离页面左上角顶点的横坐标偏移值
    let top; // 元素距离页面左上角顶点的纵坐标偏移值
    
    $app.addEventListener("mousedown", (e) => {
        const bcr = $app.getBoundingClientRect();
        isDrag = true;
        x = e.clientX;
        y = e.clientY;
        left = bcr.left + window.scrollX;
        top = bcr.top + window.scrollY;
    });
    document.addEventListener("mousemove", (e) => {
        if (!isDrag) {
            return;
        }
        const {clientX, clientY} = e;
        const movementX = clientX - (x - left); // 计算出X轴的偏移量
        const movementY = clientY - (y - top); // 计算出Y轴的偏移量
        // 平移矩阵
        const t = new Float32Array([
            movementX, movementY
        ]);
        // 计算出相对于页面左上角的绝对定位的矩阵
        const res = glMatrix.mat2.add(new Float32Array([0, 0]),  t, new Float32Array([0, 0]));
        $app.setAttribute("style", `left: ${res[0]}px;top:${res[1]}px;width:${width}px;height:${height}px;transform: none;`);
    })
    document.addEventListener("mouseup", () => {
        isDrag = false;
    });
});

Matrix implements Div element dragging and scaling at the same time

Since matrix multiplication conforms to the associative law, assuming the existing matrix \(v\), it is first scaled and then translated, and the scaled matrix is $$R=\left[ \begin{matrix} k_{x} & 0 & 0 & 0 \\ 0 & k_{y} & 0 & 0 \\ 0 & 0 & k_{z} & 0 \\ 0 & 0 & 0 & k_{w} \end{matrix} \right]$$, the translation matrix is $$ T=\left[\begin{matrix}1 &0 &0 &0\\ 0&1&0&0\\0&0&1&0\\\Delta x &\Delta y &\Delta z&1 \end{matrix}\right]$$, thus:

$$v^{'}=v*R*T=v*(\left[ \begin{matrix} k_{x} & 0 & 0 & 0 \\ 0 & k_{y} & 0 & 0 \\ 0 & 0 & k_{z} & 0 \\ 0 & 0 & 0 & k_{w} \end{matrix} \right]\left[\begin{matrix}1 &0 &0 &0\\ 0&1&0&0\\0&0&1&0\\\Delta x &\Delta y &\Delta z&1 \end{matrix}\right])=v*\left[ \begin{matrix} k_{x} & 0 & 0 & 0 \\ 0 & k_{y} & 0 & 0 \\ 0 & 0 & k_{z} & 0 \\ \Delta x &\Delta y &\Delta z & k_{w} \end{matrix} \right]$$
Here is the code that implements both dragging and zooming of Div elements:

document.addEventListener("DOMContentLoaded", () => {
    const $app = document.querySelector(`#app`);
    let isDrag = false;
    let x; // 鼠标拖拽时鼠标的横坐标值
    let y; // 鼠标拖拽时鼠标的纵坐标值
    let left; // 元素距离页面左上角顶点的横坐标偏移值
    let top; // 元素距离页面左上角顶点的纵坐标偏移值


    function reDraw(el, t, move=false) {
        const bcr = el.getBoundingClientRect();
        const {width, height} = bcr;
        const o = new Float32Array([
            bcr.left, bcr.top, 1, 1,
            bcr.right, bcr.top, 1, 1,
            bcr.right, bcr.bottom, 1, 1,
            bcr.left, bcr.bottom, 1, 1,
        ]);
        const out = new Float32Array([
            0, 0, 0, 0, 
            0, 0, 0, 0, 
            0, 0, 0, 0, 
            0, 0, 0, 0,
        ]);
        const res = glMatrix.mat4.multiply(out,  t, o);
        const left = parseInt(res[0]);
        const top = parseInt(res[1]);
        // 如果是移动,那么不需要调整宽高
        const w = move ?  width : res[4] - left;
        const h = move ? height : res[9] - top;
        el.setAttribute("style", `left: ${left}px;top:${top}px;width:${w}px;height:${h}px;transform: none;`);
    }

    $app.addEventListener("mousedown", (e) => {
        const bcr = $app.getBoundingClientRect();
        isDrag = true;
        x = e.clientX;
        y = e.clientY;
        left = bcr.left + window.scrollX;
        top = bcr.top + window.scrollY;
    });
    document.addEventListener("mousemove", (e) => {
        if (!isDrag) {
            return;
        }
        const {clientX, clientY} = e;
        const movementX = clientX - (x - left); // 计算出X轴的偏移量
        const movementY = clientY - (y - top); // 计算出Y轴的偏移量
        // 4x4平移矩阵
        const t = new Float32Array([
            0, 0, 0, 0,
            0, 0, 0, 0,
            0, 0, 0, 0,
            movementX, movementY, 0, 1
        ]);
        reDraw($app, t, true);
    })
    document.addEventListener("mouseup", () => {
        isDrag = false;
    });
    $app.addEventListener("wheel", (e) => {
        const {clientX, clientY, deltaY } = e;
        const currSacle = 1 + (deltaY < 0 ? 0.1 : -0.1);
        const zoom = Math.max(currSacle > 0 ? currSacle : 1, 0.1);
        const x = (clientX + window.scrollX) * (1 - zoom);
        const y = (clientY + window.scrollY) * (1 - zoom);
        const t = new Float32Array([
            zoom, 0, 0, 0,
            0, zoom, 0, 0,
            0, 0, 1, 0,
            x, y, 0, 1,
        ]);
        reDraw($app, t);
    });
});

Matrix implements Canvas image dragging and zooming at the same time

The logic of dragging and zooming Canvas images is basically the same as the logic of dragging and zooming ordinary Divs. The difference is that what we need to modify is the current transformation matrix rendered by Canvas, which is initially the unit matrix. For the corresponding matrix transformation, set a new transformation matrix and give it to the underlying rendering of Canvas. The specific code is as follows:
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Canvas缩放和拖拽</title>
    <style>
        body {
            position: relative;
            background-color: black;
            min-height: 1000px;
            margin: 0;
            padding: 0;
        }

        #app {
            border:1px solid white;
        }
    </style>
</head>
<body>
    <canvas id="app" width="640" height="340"></canvas>
    <script src="./gl-matrix-min.js"></script>
    <script src="./index.js"></script>
</body>
</html>
// index.js
document.addEventListener("DOMContentLoaded", () => {
    const $app = document.querySelector(`#app`);
    const {width, height} = $app.getBoundingClientRect();
    const ctx = $app.getContext("2d");
    const $img = document.createElement("img");
    $img.onload = () => {
        ctx.drawImage($img, 0, 0);
    };
    $img.src = "./01.png";
    let isDrag = false;
    let ov = new Float32Array([
            1, 0, 0, 0,
            0, 1, 0, 0,
            0, 0, 1, 0,
            0, 0, 0, 1,
    ]);

    function reDraw(ctx, o, t) {
        const out = new Float32Array([
            0, 0, 0, 0, 
            0, 0, 0, 0, 
            0, 0, 0, 0, 
            0, 0, 0, 0,
        ]);
        const nv = glMatrix.mat4.multiply(out,  t, o);
        ctx.save();
        ctx.clearRect(0, 0, width, height);
        ctx.transform(nv[0], nv[4], nv[1], nv[5], nv[12], nv[13]);
        ctx.drawImage($img, 0, 0);
        ctx.restore();
        return nv;
    }

    $app.addEventListener("mousedown", (e) => {
        isDrag = true;
    });

    document.addEventListener("mousemove", (e) => {
        if (!isDrag) {
            return;
        }
        const {movementX, movementY} = e;
        const t = new Float32Array([
            1, 0, 0, 0,
            0, 1, 0, 0,
            0, 0, 1, 0,
            movementX, movementY, 0, 1,
        ]);
        ov = reDraw(ctx, ov, t);
    });

    document.addEventListener("mouseup", (e) => {
        isDrag = false;
    });

    $app.addEventListener("wheel", (e) => {
        const {clientX, clientY, deltaY } = e;
        const currSacle = 1 + (deltaY < 0 ? 0.1 : -0.1);
        const zoom = Math.max(currSacle > 0 ? currSacle : 1, 0.1);
        const x = clientX * (1 - zoom);
        const y = clientY * (1 - zoom);
        const t = new Float32Array([
            zoom, 0, 0, 0,
            0, zoom, 0, 0,
            0, 0, 1, 0,
            x, y, 0, 1,
        ]);
        ov = reDraw(ctx, ov, t);
    });
});

canvas鼠标原点缩放.gif

concluding remarks

This is a series of articles about the use of linear algebra in the front end, and then I will share more practical articles on linear algebra.

Due to my average level of mathematics, there are inevitably mistakes in the writing. The significance of writing this article is to organize knowledge and facilitate future review. If it can arouse your interest in the application of mathematics in the front-end, it will be even better. Now, especially for the back-end management system form front-end engineer like me, looking for other fun outside the form.

If you want to get the complete source code of the sample, you can search for the front-end train conductor of on WeChat, follow and reply to 20220222 , you can get the source code link, see you next time!


而井
851 声望1.8k 粉丝