js 常用的开发技巧

字符串常用技巧

时间对比:时间个位数形式需补0

const time1 = "2019-03-31 21:00:00";
const time2 = "2019-05-01 09:00:00";
const overtime = time1 > time2;
// overtime => false

替换图片的class类正则

let reg = /(<\s*img[^>]*)class[=\s\"\']+[^\"\']*[\"\']?([^>]*>)/gi;

格式化金钱

const ThousandNum = num => num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
const money = ThousandNum(19941112);
// money => "19,941,112"

生成随机ID

const RandomId = len => Math.random().toString(36).substr(3, len);
const id = RandomId(10);
// id => "jg7zpgiqva"

操作URL查询参数

const params = new URLSearchParams(location.search.replace(/\?/ig, "")); // location.search = "?name=yajun&sex=female"
params.has("yajun"); // true
params.get("sex"); // "female"

Number常用技巧

取整:代替正数的Math.floor(),代替负数的Math.ceil()

const num1 = ~~ 1.69;
const num2 = 1.69 | 0;
const num3 = 1.69 >> 0;
// num1 num2 num3 => 1 1 1

补零

const FillZero = (num, len) => num.toString().padStart(len, "0");
const num = FillZero(169, 5);
// num => "00169"

转数值::只对null、""、false、数值字符串有效

const num1 = +null;
const num2 = +"";
const num3 = +false;
const num4 = +"169";
// num1 num2 num3 num4 => 0 0 0 169

时间戳

const timestamp = +new Date("2019-03-31");
// timestamp => 1553990400000

精确小数

const RoundNum = (num, decimal) => Math.round(num * 10 ** decimal) / 10 ** decimal;
const num = RoundNum(1.69, 1);
// num => 1.7

判断奇偶

const OddEven = num => !!(num & 1) ? "odd" : "even";
const num = OddEven(2);
// num => "even"

取最小最大值

const arr = [0, 1, 2];
const min = Math.min(...arr);
const max = Math.max(...arr);
// min max => 0 2

生成范围随机数

const RandomNum = (min, max) => Math.floor(Math.random() * (max - min + 1)) + min;
const num = RandomNum(1, 10);

Boolean常用技巧

短路运算符

const a = d && 1; // 满足条件赋值:取假运算,从左到右依次判断,遇到假值返回假值,后面不再执行,否则返回最后一个真值
const b = d || 1; // 默认赋值:取真运算,从左到右依次判断,遇到真值返回真值,后面不再执行,否则返回最后一个假值
const c = !d; // 取假赋值:单个表达式转换为true则返回false,否则返回true

判断数据类型:undefined、null、string、number、boolean、array、object、symbol、date、regexp、function、asyncfunction、arguments、set、map、weakset、weakmap

function DataType(tgt, type) {
    const dataType = Object.prototype.toString.call(tgt).replace(/\[object /g, "").replace(/\]/g, "").toLowerCase();
    return type ? dataType === type : dataType;
}
DataType("yajun"); // "string"
DataType(19941112); // "number"
DataType(true); // "boolean"
DataType([], "array"); // true
DataType({}, "array"); // false

是否为空数组

const arr = [];
const flag = Array.isArray(arr) && !arr.length;
// flag => true

是否为空对象

const obj = {};
const flag = DataType(obj, "object") && !Object.keys(obj).length;
// flag => true

Array常用技巧

克隆数组

const _arr = [0, 1, 2];
const arr = [..._arr];
// arr => [0, 1, 2]

合并数组

const arr1 = [0, 1, 2];
const arr2 = [3, 4, 5];
const arr = [...arr1, ...arr2];
// arr => [0, 1, 2, 3, 4, 5];

去重数组

const arr = [...new Set([0, 1, 1, null, null])];
// arr => [0, 1, null]

混淆数组

const arr = [0, 1, 2, 3, 4, 5].slice().sort(() => Math.random() - .5);
// arr => [3, 4, 0, 5, 1, 2]

清空数组

const arr = [0, 1, 2];
arr.length = 0;
// arr => []

截断数组

const arr = [0, 1, 2];
arr.length = 2;
// arr => [0, 1]

交换赋值

let a = 0;
let b = 1;
[a, b] = [b, a];
// a b => 1 0

过滤空值:undefined、null、""、0、false、NaN

const arr = [undefined, null, "", 0, false, NaN, 1, 2].filter(Boolean);
// arr => [1, 2]

解构数组成员嵌套

const arr = [0, 1, [2, 3, [4, 5]]];
const [a, b, [c, d, [e, f]]] = arr;
// a b c d e f => 0 1 2 3 4 5

解构数组成员别名

const arr = [0, 1, 2];
const { 0: a, 1: b, 2: c } = arr;
// a b c => 0 1 2

Object常用技巧

克隆对象

const _obj = { a: 0, b: 1, c: 2 }; // 以下方法任选一种
const obj = { ..._obj };
const obj = JSON.parse(JSON.stringify(_obj));
// obj => { a: 0, b: 1, c: 2 }

合并对象

const obj1 = { a: 0, b: 1, c: 2 };
const obj2 = { c: 3, d: 4, e: 5 };
const obj = { ...obj1, ...obj2 };
// obj => { a: 0, b: 1, c: 3, d: 4, e: 5 }

对象变量属性

const flag = false;
const obj = {
    a: 0,
    b: 1,
    [flag ? "c" : "d"]: 2
};
// obj => { a: 0, b: 1, d: 2 }

删除对象无用属性

const obj = { a: 0, b: 1, c: 2 }; // 只想拿b和c
const { a, ...rest } = obj;
// rest => { b: 1, c: 2 }

解构对象属性嵌套

const obj = { a: 0, b: 1, c: { d: 2, e: 3 } };
const { c: { d, e } } = obj;
// d e => 2 3

解构对象属性默认值

const obj = { a: 0, b: 1, c: 2 };
const { a, b = 2, d = 3 } = obj;
// a b d => 0 1 3

js数组去重

function unique1(array){             
    var n=[]; //一个新的临时数组             
    //遍历当前数组             
    for(var i=0;i<array.length;i++){              
    //如果当前数组的第i已经保存进了临时数组,那么跳过, 否则把当前项push到临时数组里面                     
        if(n.indexOf(array[i])==-1) 
        n.push(array[i]);             
    }             
    return n;            
}

数组偏平化处理

var arr = [ [1, 2, 2], [3, 4, 5, 5], [6, 7, 8, 9, [11, 12, [12, 13, [14] ] ] ], 10];
  var b = arr.toString().split(",").sort((a,b) => { return a - b}).map(Number)

ES6 flat

  function flat5 (arr=[]) {
      // flat() 方法会移除数组中的空项
      return arr.flat(Infinity)
  }
 flat5(arr) // [1, 2, 2, 3, 4, 5, 5, 6, 7, 8, 9, 11, 12, 12, 13, 14, 10]

js书写的几条基本规范

  • 不要在同一行声明多个变量。
  • 请使用 ===/!==来比较true/false或者数值
  • 不要使用全局函数。
  • Switch语句必须带有default分支
  • 函数不应该有时候有返回值,有时候没有返回值。
  • For循环必须使用大括号
  • If语句必须使用大括号
  • for-in循环中的变量 应该使用var关键字明确限定作用域,从而避免作用域污染。

JS基本类型和引用类型的区别?

基本类型: undefined,boolean,number,string,null,symbol(ES6)

引用类型:object,arrary,date,RegExp(正则),Function

基本数据类型是简单的数据段。

引用类型是由多个值构成的对象,其实都是Object的实例。

基本类型可以直接访问,而引用类型的访问是按照对象在内存中的地址,再按照地址去获取对象的值,叫做引用访问。

当从一个变量向另一个变量赋值引用类型的值时,同样也会将存储在变量中的对象的值复制一份放到为新变量分配的空间中。前面讲引用类型的时候提到,

保存在变量中的是对象在堆内存中的地址,所以,与简单赋值不同,这个值的副本实际上是一个指针,而这个指针指向存储在堆内存的一个对象。那么赋值操作后,

两个变量都保存了同一个对象地址,则这两个变量指向了同一个对象。因此,改变其中任何一个变量,都会相互影响,从而引发了对象的深拷贝和浅拷贝的问题。

获取两个日期中间的所有日期

function getAllDateCN(startTime, endTime) {
  let start = new Date(startTime);
  let end = new Date(endTime);
  let date_all = [];
  let i = 0;
  while ((end.getTime() - start.getTime()) >= 0) {
    let year = start.getFullYear();
    let month = start.getMonth() + 1;
    let day = start.getDate();
    date_all[i] = year + '-' + month + '-' + day;
    start.setDate(start.getDate() + 1);
    i += 1;
  }
  return date_all
}

柯理化函数

在数学和计算机科学中,柯里化是一种将使用多个参数的一个函数转换成一系列使用一个参数的函数的技术。所谓柯里化就是把具有较多参数的函数转换成具有较少参数的函数的过程。
举个例子

//普通函数
function fn(a, b, c, d, e) {
  console.log(a, b, c, d, e)
}
//生成的柯里化函数
let _fn = curry(fn)

_fn(1, 2, 3, 4, 5) // print: 1,2,3,4,5
_fn(1)(2)(3, 4, 5) // print: 1,2,3,4,5
_fn(1, 2)(3, 4)(5) // print: 1,2,3,4,5
_fn(1)(2)(3)(4)(5) // print: 1,2,3,4,5

柯理化函数的实现

function add () {
    let x=0;
    for(let i=0;i<arguments.length;i++){
    x+=arguments[i];
    }
    return x;
}

// 对求和函数做curry化
let f1 = curry(add, 1, 2, 3)
console.log('复杂版', f1()) // 6

// 对求和函数做curry化
let f2 = curry(add, 1, 2)
console.log('复杂版', f2(3)) // 6

// 对求和函数做curry化
let f3 = curry(add)
console.log('复杂版', f3(1, 2, 3)) // 6

// 复杂版curry函数可以多次调用,如下:
console.log('复杂版', f3(1)(2)(3)) // 6
console.log('复杂版', f3(1, 2)(3)) // 6
console.log('复杂版', f3(1)(2, 3)) // 6

// 复杂版(每次可传入不定数量的参数,当所传参数总数不少于函数的形参总数时,才会执行)
function curry(fn) {
  // 闭包
  // 缓存除函数fn之外的所有参数
  let args = Array.prototype.slice.call(arguments, 1)
  return function() {
    // 连接已缓存的老的参数和新传入的参数(即把每次传入的参数全部先保存下来,但是并不执行)
    let newArgs = args.concat(Array.from(arguments))
    if (newArgs.length < fn.length) {
      // 累积的参数总数少于fn形参总数
      // 递归传入fn和已累积的参数
      return curry.call(this, fn, ...newArgs)
    } else {
      // 调用
      return fn.apply(this, newArgs)
    }
  }
}

柯里化的用途

function checkByRegExp(regExp, string) {
  return regExp.test(string)
}

checkByRegExp(/^1\d{10}$/, '18642838455') // 校验电话号码
checkByRegExp(/^(\w)+(\.\w+)*@(\w)+((\.\w+)+)$/, 'test@163.com') // 校验邮箱

我们每次进行校验的时候都需要输入一串正则,再校验同一类型的数据时,相同的正则我们需要写多次, 这就导致我们在使用的时候效率低下,并且由于 checkByRegExp 函数本身是一个工具函数并没有任何意义。此时,我们可以借助柯里化对 checkByRegExp 函数进行封装,以简化代码书写,提高代码可读性。

//进行柯里化
let _check = curry(checkByRegExp)
//生成工具函数,验证电话号码
let checkCellPhone = _check(/^1\d{10}$/)
//生成工具函数,验证邮箱
let checkEmail = _check(/^(\w)+(\.\w+)*@(\w)+((\.\w+)+)$/)

checkCellPhone('18642838455') // 校验电话号码
checkCellPhone('13109840560') // 校验电话号码
checkCellPhone('13204061212') // 校验电话号码

checkEmail('test@163.com') // 校验邮箱
checkEmail('test@qq.com') // 校验邮箱
checkEmail('test@gmail.com') // 校验邮箱

compose 函数

compose 就是组合函数,将子函数串联起来执行,一个函数的输出结果是另一个函数的输入参数,一旦第一个函数开始执行,会像多米诺骨牌一样推导执行后续函数。

const greeting = name => `Hello ${name}`
const toUpper = str => str.toUpperCase()

toUpper(greeting('Onion')) // HELLO ONION

compose 函数的特点

  • compose 接受函数作为参数,从右向左执行,返回类型函数
  • fn()全部参数传给最右边的函数,得到结果后传给倒数第二个,依次传递

var compose = function(...args) {
  var len = args.length // args函数的个数
  var count = len - 1
  var result
  return function func(...args1) {
    // func函数的args1参数枚举
    result = args[count].call(this, args1)
    if (count > 0) {
      count--
      return func.call(null, result) // result 上一个函数的返回结果
    } else {
      //回复count初始状态
      count = len - 1
      return result
    }
  }
}

举个例子

var greeting = (name) =>  `Hello ${name}`
var toUpper = str => str.toUpperCase()
var fn = compose(toUpper, greeting)
console.log(fn('jack'))

大家熟悉的 webpack 里面的 loader 执行顺序是从右到左,是因为webpack 选择的是 compose 方式,从右到左依次执行 loader,每个 loader 是一个函数。

rules: [
  { test: /\.css$/, use: ['style-loader', 'css-loader'] }
]

如上,webpack 使用了 style-loader 和 css-loader,它是先用 css-loader 加载.css 文件,然后 style-loader 将内部样式注入到我们的 html 页面。
webpack 里面的 compose 代码如下:

const compose = (...fns) => {
  return fns.reduce(
    (prevFn, nextFn) => {
      return value =>prevFn(nextFn(value)) 
    },
    value => value
  )
}

尾调用和尾递归

尾调用(Tail Call)是函数式编程的一个重要概念,本身非常简单,一句话就能说清楚。就是指某个函数的最后一步是调用另一个函数。

function g(x) {
  console.log(x)
}
function f(x) {
  return g(x)
}
console.log(f(1))
//上面代码中,函数f的最后一步是调用函数g,这就是尾调用。

上面代码中,函数 f 的最后一步是调用函数 g,这就是尾调用。尾调用不一定出现在函数尾部,只要是最后一步操作即可。
函数调用自身,称为递归。如果尾调用自身,就称为尾递归。递归非常耗费内存,因为需要同时保存成千上百个调用帧,很容易发生栈溢出错误。但是队伍尾递归来说,由于只存在一个调用帧,所以永远不会发生栈溢出错误。

function factorial(n) {
  if (n === 1) {
    return 1
  }
  return n * factorial(n - 1)
}

上面代码是一个阶乘函数,计算 n 的阶乘,最多需要保存 n 个调用数据,复杂度为 function(n),如果改写成尾调用,只保留一个调用记录,复杂度为 function(1)。简单来说 每次的返回结果 n * factorial(n - 1) 都会被缓存在内存中,以便于下次递归时候使用,而缓存函数,浏览器就会自动开辟内存空间。而下面这种写法,函数执行后的结果,函数执行过后则会自动销毁。

function factor(n, total = 1) {
  if (n === 1) {
    return total
  }
  return factor(n - 1, n * total)
}

最经典的例子则是斐波拉切数列的尾调用。

function Fibonacci(n) {
  if (n <= 1) {
    return 1
  }
  return Fibonacci(n - 1) + Fibonacci(n - 2)
}
//尾递归
function Fibona(n, ac1 = 1, ac2 = 1) {
  if (n <= 1) {
    return ac2
  }
  return Fibona(n - 1, ac2, ac1 + ac2)
}

函数防抖和节流

1. 函数防抖

  • 非立即执行版 (非立即执行版的意思是触发事件后函数不会立即执行,而是在 n 秒后执行,如果在 n 秒内又触发了事件,则会重新计算函数执行时间。)

function debounce(func, wait) {
    let timeout;
    return function () {
        let context = this;
        let args = arguments;
        if (timeout) clearTimeout(timeout);      
        timeout = setTimeout(() => {
            func.apply(context, args)
        }, wait);
    }
}
  • 立即执行版(立即执行版的意思是触发事件后函数会立即执行,然后 n 秒内不触发事件才能继续执行函数的效果)

function debounce(func,wait) {
    let timeout;
    return function () {
        let context = this;
        let args = arguments;
        if (timeout) clearTimeout(timeout);
        let callNow = !timeout;
        timeout = setTimeout(() => {
            timeout = null;
        }, wait)
        if (callNow) func.apply(context, args)
    }
}

2.函数节流 (使得一定时间内只触发一次函数。原理是通过判断是否到达一定时间来触发函数。)

  • 时间戳版
function throttle(func, wait) {
    let previous = 0;
    return function() {
        let now = Date.now();
        let context = this;
        let args = arguments;
        if (now - previous > wait) {
            func.apply(context, args);
            previous = now;
        }
    }
}
  • 定时器版

function throttle(func, wait) {
    let timeout;
    return function() {
        let context = this;
        let args = arguments;
        if (!timeout) {
            timeout = setTimeout(() => {
                timeout = null;
                func.apply(context, args)
            }, wait)
        }
    }
}

常见算法

1.全排列

function permutate(str) {
    var array = str.split('');
    function loop(array, pre = []) {
        if (array.length == 1) {
            return [pre.concat(array).join('')];
        }
        let res = [];
        for (let index = 0; index < array.length; index++) {
            var first = array.pop();
            res = res.concat(loop(array, [...pre, first]));
            array.unshift(first);
        }
        return res;
    }
    return Array.from(new Set(loop(array)))
}

2.二分搜索

function BinarySearch1 (arr, target) {
    return search(arr, target, 0, arr.length - 1)
    function search (arr, target, from, to) {
        if (from > to) {
            return -1
        }
        const mid = Math.floor((from + to)/2)
        if (arr[mid] > target) {
            return search(arr, target, from, mid - 1)
        } else if (arr[mid] < target) {
            return search(arr, target, mid + 1, to)
        } else {
            return mid
        }
    }
}
function BinarySearch2 (arr, target) {
    let from = 0
    let to = arr.length - 1
    let mid = Math.floor((from + to)/2)
    while (from <= to) {
        mid = Math.floor((from + to)/2)
        if (arr[mid] > target) {
            to = mid - 1
        } else if (arr[mid] < target) {
            from = mid + 1
        } else {
            return mid
        }
    }

    return -1
}
console.log(BinarySearch1([1,2,3,4,5,6,7,8,9],5)) // 4
console.log(BinarySearch2([1,2,3,4,5,6,7,8,9],3)) // 2

3.冒泡排序

function BubbleSort (arr) {
    const length = arr.length
    for (let i = 0; i < length; i++) {
        for (let j = 1; j < length-i; j++) {
            if (arr[j] < arr[j - 1]) {
                const temp = arr[j]
                arr[j] = arr[j - 1]
                arr[j - 1] = temp
            }
        }
    }

    return arr
}

4.选择排序

function SelectionSort (arr) {
    const length = arr.length
    for (let i = 0; i < length; i++ ) {
        let minIndex= i
        for (let j = i + 1; j < length; j++) {
            minIndex = arr[minIndex] <= arr[j] ? minIndex : j
        }
        if (minIndex !== i) {
            const temp = arr[i]
            arr[i] = arr[minIndex]
            arr[minIndex] = temp

        }
    }
    return arr
}

5.插入排序

function InsertionSort (arr) {
    const length = arr.length
    for (let i = 1; i < length; i++) {
        const temp = arr[i]
        let j
        for (j = i - 1; j >= 0 && temp < arr[j]; j--) {
            arr[j+1] = arr[j]
        }
        arr[j+1] = temp
    }
    return arr
}

6.希尔排序


function ShellSort (arr) {
    const length = arr.length
    let gap = Math.floor(length)
    while (gap) {
        for (let i = gap; i < length; i++) {
            const temp = arr[i]
            let j
            for (j = i - gap; j >= 0 && temp < arr[j]; j = j - gap) {
                arr[j + gap] = arr[j]
            }
            arr[j + gap] = temp
        }
        gap = Math.floor(gap / 2)
    }
    return arr
}

7.归并排序

function MergeSort (arr, low, high) {
    const length = arr.length
    if (low === high) {
        return arr[low]
    }
    const mid = Math.floor((low + high)/2)
    MergeSort(arr, low, mid)
    MergeSort(arr, mid + 1, high)
    merge(arr, low, high)
    return arr

}

function merge (arr, low, high) {
    const mid = Math.floor((low + high)/2)
    let left = low
    let right = mid + 1
    const result = []
    while (left <= mid && right <= high) {
        if (arr[left] <= arr[right]) {
            result.push(arr[left++])
        } else {
            result.push(arr[right++])
        }
    }
    while (left <= mid) {
        result.push(arr[left++])
    }
    while (right <= high) {
        result.push(arr[right++])
    }

    arr.splice(low, high-low+1, ...result)
}

const test = [2, 34, 452,3,5, 785, 32, 345, 567, 322,5]
console.log(MergeSort(test, 0, test.length - 1))

8.堆排序

function HeapSort (arr) {
    const length = arr.length

    // 调整初始堆,调整完其实也确定了最大值
    // 但此时最大值是在 arr[0] 中
    for (let i = Math.floor(length/2) - 1; i >= 0; i--) {
        adjustHeap(arr, i, length)
    }

    // 把 arr[0](最大值)换到后面
    for (let i = length - 1; i >=0; i--) {
        const temp = arr[0]
        arr[0] = arr[i]
        arr[i] = temp
        adjustHeap(arr, 0, i)
    }

    return arr
}

// size 是还需要调整的堆的大小
// 随着一个个最大值的确定,size 会越来越小
function adjustHeap (arr, position, size) {
    const left = position * 2 + 1
    const right = left + 1
    let maxIndex = position
    if (left < size && arr[left] > arr[maxIndex]) {
        maxIndex = left
    }
    if (right < size && arr[right] > arr[maxIndex]) {
        maxIndex = right
    }
    if (maxIndex !== position) {
        const temp = arr[position]
        arr[position] = arr[maxIndex]
        arr[maxIndex] = temp
        adjustHeap(arr, maxIndex, size)
    }
    return arr
}

数组去重es5的高阶写法

var array = [{value: 1}, {value: 1}, {value: 2}];

function unique(array) {
    var obj = {};
    return array.filter(function(item, index, array){
        console.log(typeof item + JSON.stringify(item))
        return obj.hasOwnProperty(typeof item + JSON.stringify(item)) ? false : (obj[typeof item + JSON.stringify(item)] = true)
    })
}

console.log(unique(array)); // [{value: 1}, {value: 2}]

数组去重es6的几种写法

function unique(array) {
   return Array.from(new Set(array));
}


function unique(array) {
    return [...new Set(array)];
}


function unique (arr) {
    const seen = new Map()
    return arr.filter((a) => !seen.has(a) && seen.set(a, 1))
}


 

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
在Vue页面设计开发中,有几个技巧是可以提高效率和代码质量的。首先,在样式中使用scoped属性可以确保样式只作用于当前组件,不会影响其他组件的样式。这可以通过使用deep选择器实现,例如在<style scoped>标签中使用 .my-content >>> .child-component { font-size: 16px; } 的方式。 其次,Vue提供了模板语法来搭建HTML,这在大多数情况下是推荐的做法。然而,在某些特定场景下,我们可能需要使用JavaScript的完全编程能力,这时候可以使用渲染函数来实现。渲染函数可以帮助我们更灵活地构建组件,同时也提供了与JSX相似的语法。可以根据具体需求来选择使用模板语法或渲染函数,以获得更好的开发体验。 对于Vue项目的报表设计器和积木报表模块,可以参考ruoyi-vue-pro yudao项目中的相关SQL脚本和启用方式。这个项目可能提供了一些关于报表设计和模块开启的经验和教程,可以作为参考资料。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* [超实用的Vue 小技巧开发常用整理)](https://blog.csdn.net/weixin_55953988/article/details/122621453)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 33.333333333333336%"] - *2* [分享 9个 关于 vue3 相关的开发技巧](https://blog.csdn.net/Ed7zgeE9X/article/details/124486530)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 33.333333333333336%"] - *3* [ruoyi-vue-pro yudao 项目报表设计器 积木报表模块启用及相关SQL脚本](https://download.csdn.net/download/zengwenbo225566/88234865)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 33.333333333333336%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

搬砖的小马

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值