阅读视图

发现新文章,点击刷新页面。

阿里邮件下载器使用说明

邮件下载器使用说明 📋 项目简介 这是一个基于Node.js的邮件自动下载和分类工具,能够通过IMAP协议连接到邮箱服务器,自动下载所有邮件并按照联系人进行分类保存。 🗂️ 目录结构 程序运行后会自动

鸿蒙应用开发之@Builder自定义构建函数:值传递与引用传递与UI更新

@Builder装饰器:自定义构建函数

@Builder装饰的函数称为自定义构建函数,它是一种轻量化 UI 复用机制,它允许开发者将重复使用的 UI 元素抽象为函数,这些函数可以在 build 函数中被调用以实现 UI 复用。

自定义构建函数根据定义的位置不同分为私有自定义函数全局自定义函数

私有自定义构建函数

私有自定义构建函数定义在@Component组件内,属于该组件私有的,只能在该组件内build函数或者其他自定义构建函数中调用。

@Component
struct MyComponent {
  build() {
    Column() {
      this.myBuilder()
    }
  }

  @Builder
  myBuilder() {
    Text("私有@Builder")
  }
}

@Entry
@Component
struct Index {
  build() {
    Column({ space: 10 }) {
      MyComponent()
    }
    .width("100%")
    .height("100%")
  }
}

全局自定义构建函数

全局自定义函数定义在@Component组件外,可以在其他自定义函数或者其他自定义组件中调用。

@Builder
function myBuilder() {
  Text("全局@Builder")
}

@Entry
@Component
struct Index {
  build() {
    Column({ space: 10 }) {
      myBuilder()
    }
    .width("100%")
    .height("100%")
  }
}

@Builder与@Component的区别

经过上面的描述可能会发现@Component和@Builder都可以用于UI复用,他们的区别如下:

  • @Builder:纯UI逻辑复用,无独立状态管理与生命周期,必须通过参数传递的方式与调用方完成数据交互。
  • @Component:完整组件封装,拥有独立状态管理与生命周期。

自定义构建函数参数传递规则

自定义构建函数的参数传递有按值传递按引用传递两种,均需遵守以下规则:

  • 参数的类型必须与参数声明的类型一致,不允许undefined、null和返回undefined、null的表达式。
  • @Builder装饰的函数内部,不允许改变参数值。
  • 只有当传入一个参数且该参数直接传入对象字面量时,才会按引用传递,其他传递方式均为按值传递。

按值传递参数

定义一个全局自定义构建函数textBuilder(text: string),参数只有一个且不是字面量对象,所以按照值传递。

@Builder
function textBuilder(text: string) {
  Text("Hello,"+text).fontSize(30)
}

调用@Builder装饰的函数默认按值传递。当传递的参数为状态变量时,状态变量的改变不会引起@Builder函数内的UI刷新


@Entry
@Component
struct Index {
  @State text: string = 'World'

  build() {
    Column() {
      textBuilder(this.text)
      Button("修改text").onClick(() => {
        //修改状态变量,不会引用@Builder函数中UI的更新
        this.text = "ArkUI"
      })
    }
  }
}

如下图,经测试状态变量的改变不会引起@Builder函数内UI刷新

在这里插入图片描述

按引用传递参数

定义一个@Builder自定义构建函数,参数为自定义对象类型。

class Params {
  text: string = ''
}

@Builder
function textBuilder(params: Params) {
  Text(`Hello, ${params.text}`).fontSize(30)
}

调用按引用传递参数时,传递的参数可为状态变量,且状态变量的改变会引起@Builder函数内的UI刷新。

@Entry
@Component
struct Index {
  @State text: string = 'World'

  build() {
    Column() {
      textBuilder({
        text: this.text
      })
      Button("修改text").onClick(() => {
        //修改状态变量,不会引用@Builder函数中UI的更新
        this.text = "ArkUI"
      })
    }
  }
}

预览效果如下图所示:

在这里插入图片描述

限制条件

  • @Builder自定义函数内部不允许修改参数值,否则框架会抛出运行时异常。
  • @Builder自定义构建函数存在两个或两个以上的参数时,即使通过对象字面量形式传递,值的改变也不会触发UI刷新。

示例1:在@Builder中修改参数值

class Params {
  text: string = ''
}

@Builder
function textBuilder(params: Params) {
  Text(`Hello, ${params.text}`).fontSize(30)
    .onClick(() => {
      params.text = "哈哈哈"//【错误】,禁止改参数的值
    })
}

如下图所示,出现预览错误

在这里插入图片描述

示例2:在@Builder中接收两个参数,不会触发UI更新。

import { promptAction } from "@kit.ArkUI"

class Params {
  text: string = ''
}

@Builder
function textBuilder(params: Params,num:number) {
  Column(){
    Text(`Hello, ${params.text}`).fontSize(30)
      .onClick(() => {
        params.text = "哈哈哈"
      })

    Text(`${num}`)
  }

}

@Entry
@Component
struct Index {
  @State text: string = 'World'

  build() {
    Column() {
      textBuilder({
        text: this.text
      },100)
      Button("修改text").onClick(() => {
        //修改状态变量,不会引用@Builder函数中UI的更新
        this.text = "ArkUI"
      })
    }
  }
}

如下图,当@Builder接收2个参数时,UI更新不生效。 在这里插入图片描述 对鸿蒙感兴趣的同学,免费考取鸿蒙开发者认证

HarmonyOS应用开发之滚动容器Scroll

滚动容器(Scroll)

Scroll表示可滚动的容器组件,当子组件的布局尺寸超过父组件的尺寸时,内容可以滚动。

[!warning]

注意两个要点:

  1. Scroll子组件的布局尺寸超过父组件尺寸时,才可以滚动
  2. Scroll只能有一个子组件

基本使用

如图所示,在Column中包含10Text文本,每个Text文本的高度为100,间距为10

Scroll内子组件的总高度大于Scroll父组件的高度,所以可以滚动。

在这里插入图片描述

代码如下

@Entry
@Component
struct Index {
  @State array: number[] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
  build() {
    Column(){
      Scroll(){
        Column({ space: 20 }) {
          ForEach(this.array, (item: number) => {
            Text(`${item}`)
              .width("100%")
              .textAlign(TextAlign.Center)
              .width("100%")
              .height(100)
              .borderRadius(15)
              .backgroundColor(Color.White)
          })
        }
        .padding(10)
      }
    }.width("100%")
    .height("100%")
    .backgroundColor("#dedede")
  }
}

关闭滚动条

通过.scrollBar(barState: BarState)设置滚动条的显示模式

  • BarState.Off 关闭滚动条
  • BarState.On 显示滚动条
  • BarState.Auto 滑动时显示滚动条,3秒后消失
Scroll(){
    ...
}.scrollBar(BarState.Off)   //关闭滚动条

在这里插入图片描述

滚动方向

通过.scrollable(value: ScrollDirection)属性用来设置Scroll的滚动方向。

参数

  • ScrollDirection.Vertical 垂直滚动
  • ScrollDirection.Horizontal 水平滚动

示例:在水平方向显示10个Text,沿水平方向滚动。

在这里插入图片描述

代码如下

@Entry
@Component
struct Index {
  @State array: number[] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
  build() {
    Column(){
      Scroll(){
        Row({ space: 20 }) {
          ForEach(this.array, (item: number) => {
            Text(`${item}`)
              .width("100%")
              .textAlign(TextAlign.Center)
              .width(100)
              .height(100)
              .borderRadius(15)
              .backgroundColor(Color.White)
          })
        }.padding(10)
      }
      .scrollBar(BarState.Off)//关闭滚动条
      .scrollable(ScrollDirection.Horizontal) //水平滚动方向
    }.width("100%")
    .height(120)
    .backgroundColor("#dedede")
  }
}

滚动条样式

通过scrollBarWidth(20)和scrollBarColor(Color.Red)设置滚动条的宽度与颜色。

在这里插入图片描述

Scroll(){
       ...
}
.scrollBar(BarState.Auto)
.scrollable(ScrollDirection.Horizontal) //滚动方向
.scrollBarWidth(10)//滚动条宽度
.scrollBarColor(Color.Red) //滚动条颜色

滚动控制器

通过给Scroll配置Scroller滚动控制器,来控制Scroll的滚动。

Scroller有如下方法控制Scroll滚动

  • scrollEdge(value: Edge, options?: ScrollEdgeOptions | undefined) 滚动到容器边缘
  • scrollTo(options: ScrollOptions) 滚动到指定位置
    • xOffset: 水平偏移量(相对于容器开始位置)
    • yOffset: 垂直偏移量(相对于容器开始位置)
  • scrollBy(dx: Length, dy: Length) 滚动指定距离。
    • dx: 水平滚动距离(相对于当前位置)
    • dy: 垂直滚动距离(相对于当前位置)

如下图所示,点击按钮时控制Scroll滚动到底部、顶部、中间位置,此时就需要用到Scroller滚动控制器。

在这里插入图片描述

代码如下

@Entry
@Component
struct Index {
  @State array: number[] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
  private scorller: Scroller = new Scroller()

  build() {
    Column() {
      Scroll(this.scorller) {
        Column({ space: 20 }) {
          ForEach(this.array, (item: number) => {
            Text(`${item}`)
              .width("100%")
              .textAlign(TextAlign.Center)
              .width("100%")
              .height(100)
              .borderRadius(15)
              .backgroundColor(Color.White)
          })
        }.padding(10)
      }
      .layoutWeight(1)
      .scrollBar(BarState.Auto)
      .scrollable(ScrollDirection.Vertical) //滚动方向
      .scrollBarWidth(10)
      .scrollBarColor(Color.Red)

      Row() {
        Button("到底部").onClick(() => {
          this.scorller.scrollEdge(Edge.Bottom)
        })

        Button("到顶部").onClick(() => {
          this.scorller.scrollEdge(Edge.Top)
        })

        Button("到中间").onClick(() => {
          animateToImmediately({
            duration: 300,
            playMode: PlayMode.Normal,
            curve: Curve.Ease
          }, () => {
            //滑动到指定位置
            this.scorller.scrollTo({
              xOffset: 0,
              yOffset: 300
            })
          })
        })

        Button("滚动一段").onClick(() => {
          //滑动到指定位置
          animateToImmediately({
            duration: 300,
            playMode: PlayMode.Normal,
            curve: Curve.Ease
          }, () => {
            //滑动到指定位置
            this.scorller.scrollBy(0, 100)
          })
        })
      }.width("100%")
      .height(200)
      .alignItems(VerticalAlign.Center)
      .justifyContent(FlexAlign.SpaceEvenly)

    }.width("100%")
    .height("100%")
    .backgroundColor("#dedede")
  }
}

滚动监听

有时候需要监听当前Scroll滚动的状态,根据状态做出相应的处理,此时需要用到滚动监听。

  • onDidScroll(handler: ScrollOnScrollCallback) 滚动事件回调,Scroll滚动时触发。返回当前帧滚动的偏移量和当前滚动状态。
    • xOffset: 每一帧的水平偏移量
    • yOffset: 每一帧的垂直偏移量

触发该事件的条件:

  1. 滚动组件触发滚动时触发,支持键鼠操作等其他触发滚动的输入设置。
  2. 通过滚动控制器API接口调用。
  3. 越界回弹。

示例代码: 监听Scroll滚动的距离,当向下滚动200像素时,弹出提示,效果如下图所示

在这里插入图片描述

代码如下

import { promptAction } from '@kit.ArkUI'

@Entry
@Component
struct Index {
  @State array: number[] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
  private scorller: Scroller = new Scroller()
  //记录相对于Scroll开始位置的偏移量
  private yOffset_total: number = 0

  build() {
    Column() {
      Scroll(this.scorller) {
        Column({ space: 20 }) {
          ForEach(this.array, (item: number) => {
            Text(`${item}`)
              .width("100%")
              .textAlign(TextAlign.Center)
              .width("100%")
              .height(100)
              .borderRadius(15)
              .backgroundColor(Color.Pink)
          })
        }.padding(10)
      }
      .layoutWeight(1)
      .scrollBar(BarState.Auto)
      .scrollable(ScrollDirection.Vertical) 
      .scrollBarWidth(10)
      .scrollBarColor(Color.Red)
      .backgroundColor("#dedede")
       //监听Scorll滚动,计算总偏移量
      .onDidScroll((xOffset: number, yOffset) => {
        this.yOffset_total += yOffset
        if (this.yOffset_total >= 200 && yOffset > 0) {
          promptAction.openToast({ message: "滚动到200了" })
        }
        console.log("偏移量:" + this.yOffset_total)
      })
    }.width("100%")
    .height("100%")
  }
}

在滚动时,监听滚动偏移量并打印,如下图所示,总偏移量yOffset_total是依次递增的。

在这里插入图片描述

到此,Scroll的使用以及Scroller滚动控制器就介绍完了。Scroller滚动控制器在其他可滚动组件也同样适用。

对鸿蒙感兴趣的同学,免费考取鸿蒙开发者认证

🔥 纯 JS 实现 SQL 字段智能解析工具类,前端也能玩转 SQL 解析

🔥 纯 JS 实现 SQL 字段智能解析工具类,前端也能玩转 SQL 解析

在前端开发中,我们偶尔会遇到需要解析 SQL 语句的场景 —— 比如可视化 SQL 编辑器、数据大屏字段映射、低代码平台的 SQL 配置解析等。如果每次都靠后端返回字段信息,不仅增加联调成本,还会降低前端交互的灵活性。

今天给大家分享一个我封装的纯 JavaScript SQL 字段解析工具类,无需依赖任何第三方库,就能快速提取 SQL 中的查询字段、表名、字段别名等核心信息,兼容绝大多数常见的 SELECT 语法场景。

image.png

🎯 工具类核心能力

这个工具类专为前端场景设计,主打轻量、易用、兼容性强,核心功能包括:

  • 📝 SQL 预处理:自动清理注释、多余空格,统一语法格式
  • 🔍 字段提取:支持提取原始字段、纯字段名(剔除表别名 / 函数包裹)
  • 📌 别名解析:兼容AS 别名和直接字段 别名两种写法,生成别名映射表
  • 📊 表名识别:从 FROM 子句中提取表名,自动跳过表别名和嵌套查询
  • 🚨 嵌套查询标记:快速识别 SQL 中是否包含子查询,便于特殊处理
  • ✨ 零依赖:纯原生 JS 实现,无需引入 SQL 解析库

🛠 核心代码实现

完整工具类代码

javascript

运行

/**
 * SQL解析工具类
 * 特性:
 * 1. data_field 优先使用别名,无别名时用实际字段(物理字段去前缀/字符串常量原始值)
 * 2. 物理字段(如"ERP_Provider"."No")无别名时提取纯字段名(No)
 * 3. 字符串常量字段(如'入库单')无别名时保留原始值作为字段名
 * 4. 支持 PostgreSQL 特有语法(DISTINCT ON)和嵌套子查询
 * 5. 完全按SQL原始内容解析,不做自定义生成
 */
class SqlParser {
    /**
     * 从SQL语句中提取字段信息
     * @param {String} sql - 待解析的SQL语句
     * @returns {Array} 格式化后的字段列表
     */
    static extractFormattedFields(sql) {
        // 防御:处理空SQL或非字符串输入
        if (!sql || typeof sql !== 'string') return [];

        // 清理SQL:移除注释、多余空格/换行,保留核心结构
        const cleanedSql = this.cleanSQL(sql);

        // 提取目标SQL(优先解析子查询,若外层是 select * from (...))
        const targetSql = this.extractTargetSql(cleanedSql);

        // 验证SQL有效性(必须含SELECT/FROM,且SELECT在FROM前)
        if (!this.isValidSql(targetSql)) {
            console.warn('无效SQL:缺少SELECT/FROM或顺序错误');
            return [];
        }

        // 提取SELECT和FROM之间的字段部分(兼容DISTINCT ON)
        const upperSql = targetSql.toUpperCase();
        const selectStart = upperSql.indexOf('SELECT') + 6;
        // 处理DISTINCT ON:跳过ON后的括号内容
        const distinctOnEnd = this.findDistinctOnEnd(targetSql, selectStart);
        const fromStart = upperSql.indexOf('FROM');
        const fieldsPart = targetSql.substring(distinctOnEnd, fromStart).trim();

        // 分割字段并解析(处理括号内逗号)
        return this.splitFields(fieldsPart)
            .map(token => token.trim())
            .filter(token => token)
            .map(token => this.parseToFormattedField(token))
            .filter(field => field); // 过滤解析失败的字段
    }

    /**
     * 清理SQL:移除注释和多余空格
     */
    static cleanSQL(sql) {
        return sql
            .replace(/--.*$/gm, "") // 移除单行注释
            .replace(/\/\*[\s\S]*?\*\//g, "") // 移除多行注释
            .replace(/\s+/g, ' ') // 压缩空格
            .trim();
    }

    /**
     * 提取目标SQL:若外层是 select * from (子查询),则解析子查询
     */
    static extractTargetSql(cleanedSql) {
        const subqueryRegex = /SELECT\s+\*\s+FROM\s+\(([\s\S]*)\)\s+AS\s+\w+/i;
        const subqueryMatch = cleanedSql.match(subqueryRegex);
        if (subqueryMatch && subqueryMatch[1]) {
            return subqueryMatch[1].trim(); // 子查询作为目标
        }
        return cleanedSql; // 否则用原始SQL
    }

    /**
     * 定位DISTINCT ON的结束位置(跳过括号内容)
     */
    static findDistinctOnEnd(sql, selectStart) {
        const distinctOnRegex = /DISTINCT\s+ON\s*\(/i;
        const match = sql.substring(selectStart).match(distinctOnRegex);
        if (!match) return selectStart; // 无DISTINCT ON,直接返回SELECT起始位置

        // 计算DISTINCT ON(...)的结束索引(跳过括号内内容)
        const onStart = selectStart + match.index + match[0].length;
        let bracketCount = 1; // 已进入一个括号
        let currentIndex = onStart;

        while (currentIndex < sql.length && bracketCount > 0) {
            const char = sql[currentIndex];
            if (char === '(') bracketCount++;
            if (char === ')') bracketCount--;
            currentIndex++;
        }

        return currentIndex; // 返回DISTINCT ON(...)后的位置
    }

    /**
     * 智能分割字段(处理括号内/字符串内的逗号,避免误分割)
     */
    static splitFields(fieldsPart) {
        const fields = [];
        let currentField = '';
        let bracketCount = 0;
        let inQuote = false; // 标记是否在字符串引号内

        for (const char of fieldsPart) {
            // 处理单/双引号切换(字符串内逗号不分割)
            if (char === '"' || char === "'") {
                inQuote = !inQuote;
                currentField += char;
                continue;
            }

            // 仅当:不在引号内 + 括号计数为0 → 用逗号分割字段
            if (char === ',' && !inQuote && bracketCount === 0) {
                fields.push(currentField);
                currentField = '';
                continue;
            }

            // 更新括号计数(仅当不在引号内)
            if (!inQuote) {
                if (char === '(') bracketCount++;
                if (char === ')') bracketCount = Math.max(0, bracketCount - 1);
            }

            currentField += char;
        }

        // 添加最后一个未分割的字段
        if (currentField.trim()) fields.push(currentField.trim());
        return fields;
    }

    /**
     * 解析单个字段令牌(核心:data_field优先用别名)
     */
static parseToFormattedField(token) {
    try {
        // 移除引号(不改变原始逻辑,仅清理格式)
        const cleanToken = token
            .replace(/"([^"]+)"/g, '$1')  // 移除字段名双引号(如 "No" → No)
            .replace(/'([^']+)'/g, '$1')  // 移除字符串单引号(如 '入库单' → 入库单)
            .trim();

        // 1. 区分物理字段和字符串常量字段
        const isStringConst = !cleanToken.includes('.') && !cleanToken.includes('(') && !cleanToken.includes(')') && isNaN(cleanToken);

        // 2. 解析别名(支持 AS 别名、空格别名)
        let fieldExpr, alias;
        const asRegex = /\s+as\s+/i;

        if (asRegex.test(cleanToken)) {
            // 场景1:带AS的别名(如 No AS 单号、"ERP_Provider"."No" AS 供应商编号)
            [fieldExpr, alias] = cleanToken.split(asRegex).map(item => item.trim());
        } else {
            // 场景2:空格分隔的别名(如 No 单号、SUM(Amount) 总金额)
            const lastSpaceIndex = cleanToken.lastIndexOf(' ');
            if (lastSpaceIndex > -1) {
                const potentialAlias = cleanToken.substring(lastSpaceIndex + 1).trim();
                // 排除别名含括号/运算符的情况(避免误判函数内空格)
                if (!potentialAlias.includes('(') && !potentialAlias.includes(')') && !/[+\-*/=<>]/.test(potentialAlias)) {
                    fieldExpr = cleanToken.substring(0, lastSpaceIndex).trim();
                    alias = potentialAlias;
                } else {
                    // 无有效别名:字段表达式=完整令牌,别名为空
                    fieldExpr = cleanToken;
                    alias = '';
                }
            } else {
                // 场景3:无空格(无别名,如 No、'入库单'、SUM(Amount))
                fieldExpr = cleanToken;
                alias = '';
            }
        }

        // 变量:tableWithRealField → 专门存储“表名+该字段真实名称”(有别名时有效,无别名时为null)
        let tableWithRealField = null;
        // 仅当“有别名”且“是物理字段”时,才提取表名和真实字段名
        if (alias && !isStringConst) {
            // 从 fieldExpr 中提取表名和真实字段名(处理函数包裹场景,如 SUM(ERP_Order.Amount) → ERP_Order.Amount)
            let realFieldExpr = fieldExpr;
            const funcMatch = fieldExpr.match(/\(([^)]+)\)/);
            if (funcMatch && funcMatch[1]) {
                realFieldExpr = funcMatch[1].trim(); // 去除函数包裹(如 SUM(...) → ...)
            }

            // 分割表名和真实字段名(如 ERP_Provider.No → 表名=ERP_Provider,真实字段名=No)
            const exprParts = realFieldExpr.split('.').filter(part => part.trim());
            if (exprParts.length >= 2) {
                const tableName = exprParts[0].trim(); // 表名
                const realFieldName = exprParts.slice(1).join('.').trim(); // 真实字段名(兼容字段名含“.”的极端场景)
                tableWithRealField = `${tableName}.${realFieldName}`; // 格式:表名.真实字段名
            }
        }

        // 3. 核心逻辑:data_field 优先用别名,无别名用实际字段
        let dataField;
        if (alias) {
            // 有别名 → data_field = 别名
            dataField = alias;
        } else {
            // 无别名 → 按字段类型取实际值
            dataField = isStringConst
                ? fieldExpr  // 字符串常量→原始值(如 '入库单' → 入库单)
                : this.getPureFieldName(fieldExpr); // 物理字段→去前缀(如 ERP_Provider.No → No)
        }

        // 4. data_title 保持原逻辑(显示名称,优先用别名)
        const dataTitle = alias || this.extractSimpleName(fieldExpr);

        // 5. 推断数据类型和枚举选项
        const dataType = this.inferDataType(dataField, dataTitle, token);
        const enumOptions = dataType === 'enum' ? this.extractEnumOptions(token) : [];

        return {
            data_title: dataTitle,    // 显示名称(原逻辑不变)
            data_field: dataField,    // 字段标识(别名优先!)
            FormType: this.getFormTypeByDataType(dataType),
            enumOptions: enumOptions,
            search_data_field: fieldExpr,
            tableWithRealField: tableWithRealField // 专门存储“表名+该字段真实名称”(有别名时有效)
        };
    } catch (error) {
        console.warn(`字段解析失败,跳过:${token}`, error);
        return null;
    }
}

    /**
     * 物理字段提取纯字段名(无别名时用:去除表名前缀、函数包裹)
     */
    static getPureFieldName(fieldExpr) {
        // 处理函数包裹(如 SUM(ERP_StorageIn.Amount) → ERP_StorageIn.Amount)
        const funcMatch = fieldExpr.match(/\(([^)]+)\)/);
        if (funcMatch && funcMatch[1]) {
            fieldExpr = funcMatch[1].trim();
            // 递归处理嵌套函数(如 SUM(COALESCE(Amount, 0)) → Amount)
            if (/\w+\(/.test(fieldExpr)) {
                return this.getPureFieldName(fieldExpr);
            }
        }

        // 去除表名前缀(如 ERP_Provider.No → no、"ERP_StorageIn"."Amount" → Amount)
        const dotIndex = fieldExpr.lastIndexOf('.');
        return dotIndex > -1
            ? fieldExpr.substring(dotIndex + 1).trim()
            : fieldExpr.trim();
    }

    /**
     * 提取简单名称(无别名时用于 data_title)
     */
    static extractSimpleName(fieldExpr) {
        // 处理函数(如 SUM(Amount) → Amount)
        const funcMatch = fieldExpr.match(/\(([^)]+)\)/);
        if (funcMatch && funcMatch[1]) {
            fieldExpr = funcMatch[1].trim();
        }

        // 处理表名前缀(如 ERP_Provider.No → No)
        const dotIndex = fieldExpr.lastIndexOf('.');
        if (dotIndex > -1) {
            return fieldExpr.substring(dotIndex + 1).trim();
        }

        return fieldExpr.trim();
    }

    /**
     * 从CASE语句提取枚举选项
     */
    static extractEnumOptions(token) {
        const options = [];
        const upperToken = token.toUpperCase();

        if (upperToken.includes('CASE') && upperToken.includes('WHEN') && upperToken.includes('THEN')) {
            const caseStart = upperToken.indexOf('CASE');
            const caseEnd = upperToken.indexOf('END');
            if (caseStart !== -1 && caseEnd !== -1) {
                const caseContent = token.substring(caseStart + 4, caseEnd).trim();
                const whenParts = caseContent.split(/WHEN/i).filter(part => part.trim());

                for (const part of whenParts) {
                    const thenIndex = part.toUpperCase().indexOf('THEN');
                    if (thenIndex !== -1) {
                        const optionText = part.substring(thenIndex + 4).trim()
                            .replace(/['"]/g, '')
                            .replace(/[,;]/g, '')
                            .split(/\s+/)[0];

                        if (optionText) options.push({ label: optionText, value: optionText });
                    }
                }
            }
        }

        return options;
    }

    /**
     * 推断数据类型
     */
    static inferDataType(fieldName, alias, originalToken) {
        const upperField = fieldName.toUpperCase();
        const upperAlias = alias.toUpperCase();
        const upperToken = originalToken.toUpperCase();

        // CASE语句→枚举
        if (upperToken.includes('CASE') && upperToken.includes('WHEN') && upperToken.includes('THEN')) {
            return 'select';
        }

        // 日期类型
        const dateKeys = ['DATE', 'TIME', 'DAY', 'MONTH', 'YEAR', '日期', '时间', '天'];
        const dateFuncs = ['DATE_PART', 'CURRENT_DATE', 'DATE_TRUNC', 'CREATETIME'];
        if (dateFuncs.some(f => upperToken.includes(f)) || dateKeys.some(k => upperField.includes(k) || upperAlias.includes(k))) {
            return 'date';
        }

        // 数字类型
        const numKeys = ['NUM', '数量', '金额', '天数', 'INT','DOUBLE','单价','分数','总数','余额','价格','成本','重量','指数','率','额'];
        const numFuncs = ['SUM', 'COUNT', 'AVG', 'COALESCE'];
        if (numFuncs.some(f => upperToken.includes(f)) || /[+\-*/]/.test(upperToken) || numKeys.some(k => upperField.includes(k) || upperAlias.includes(k))) {
            return 'double';
        }

        // 枚举类型
        const enumKeys = ['STATE', 'TYPE', '状态', '类型','enum','等级','种类','类别','级别','等级','性别','分类'];
        if (enumKeys.some(k => upperField.includes(k) || upperAlias.includes(k))) {
            return 'select';
        }

        // 默认字符串
        return 'string';
    }

    /**
     * 获取表单组件类型
     */
    static getFormTypeByDataType(dataType) {
        const typeMap = {
            'date': 'date',
            'number': 'double',
            'double': 'double',
            'string': 'string',
            'enum': 'select',
            'select': 'select'
        };
        return typeMap[dataType] || 'string';
    }

    /**
     * 验证SQL有效性
     */
    static isValidSql(sql) {
        const upperSql = sql.toUpperCase();
        const selectIndex = upperSql.indexOf('SELECT');
        const fromIndex = upperSql.indexOf('FROM');
        // 排除SELECT * 无具体字段的场景
        const hasFields = selectIndex + 6 < fromIndex && sql.substring(selectIndex + 6, fromIndex).trim() !== '*';
        return selectIndex !== -1 && fromIndex !== -1 && selectIndex < fromIndex && hasFields;
    }
}

export default SqlParser;

🚀 快速使用示例

基础使用

javascript

运行

// 测试SQL(包含别名、函数、表别名)
const testSql = `
  SELECT 
    t.id AS user_id, 
    t.name, 
    t.age, 
    MAX(t.score) AS max_score,
    t.address
  FROM 
    user_info t
  WHERE 
    t.age > 18
  GROUP BY 
    t.id, t.name
`;

// 一键解析
const result = SqlFieldParser.quickParse(testSql);

// 输出结果
console.log('原始字段列表:', result.fields); // ["t.id", "t.name", "t.age", "t.score", "t.address"]
console.log('纯字段名:', result.pureFields); // ["id", "name", "age", "score", "address"]
console.log('字段别名映射:', result.fieldAliasMap); // { user_id: "t.id", max_score: "t.score" }
console.log('涉及表名:', result.tables); // ["user_info"]
console.log('是否有嵌套查询:', result.hasNestedQuery); // false

输出结果说明

字段 类型 说明
fields Array 原始字段列表(含表别名 / 函数包裹前的字段)
pureFields Array 纯字段名(剔除表别名、函数,仅保留字段本身)
fieldAliasMap Object 别名映射表(别名 → 原始字段)
tables Array 去重后的表名列表
hasNestedQuery Boolean 是否包含嵌套查询
reverseAliasMap Object 反向别名映射(原始字段 → 别名)

🎨 适用场景

  1. 可视化 SQL 编辑器:解析用户输入的 SQL,自动提取字段用于表单 / 表格渲染
  2. 低代码平台:解析配置的 SQL 语句,实现字段映射、数据预览
  3. 数据大屏 / 报表工具:自动识别 SQL 中的维度 / 指标字段,简化配置
  4. 前端数据校验:校验 SQL 中是否包含指定字段,避免非法查询
  5. SQL 格式化工具:辅助提取核心信息,优化格式化效果

📈 扩展方向

这个工具类是基础版,满足大部分前端场景需求,你可以根据业务扩展:

  1. 支持 JOIN 表解析:扩展parseTables方法,解析 JOIN 子句中的关联表
  2. 递归解析嵌套查询:对(SELECT ...)形式的子查询做递归解析
  3. 支持 INSERT/UPDATE 语法:新增parseInsertFields/parseUpdateFields方法
  4. 语法错误提示:增加 SQL 语法合法性校验,返回错误位置
  5. 结合专业解析库:如需更精准的语法分析,可集成sql-parser等库增强能力

💡 核心设计思路

  1. 预处理优先:先清理注释、统一格式,避免因 SQL 写法不规范导致解析失败
  2. 正则精准匹配:针对 SELECT/FROM 核心子句设计专属正则,兼顾兼容性和性能
  3. 分层解析:先提取核心片段,再拆分字段 / 表名,降低解析复杂度
  4. 轻量优先:前端场景下,避免引入重量级解析库,用原生 JS 实现核心能力

🎯 兼容性说明

✅ 支持的 SQL 语法:

  • 基础 SELECT 查询(含 DISTINCT/TOP 关键字)
  • 字段别名(AS 别名 / 直接别名)
  • 表别名(如 user_info t
  • 函数包裹字段(MAX/COUNT/CONCAT 等)
  • 多表查询(FROM 后多表逗号分隔)
  • 含 WHERE/GROUP BY/ORDER BY 的复杂查询

❌ 暂不支持(可扩展):

  • 复杂嵌套子查询的深度解析
  • INSERT/UPDATE/DELETE 语句解析
  • 非常规 SQL 语法(如存储过程、自定义函数)

📝 总结

这个工具类的核心价值在于前端自主解析 SQL,摆脱对后端的依赖,提升交互体验。代码结构清晰,易于扩展,适合作为前端 SQL 解析的基础组件。

如果你有类似的业务场景,直接复制代码就能用,也可以根据自己的需求扩展功能。如果觉得有用,欢迎点赞收藏,也欢迎在评论区交流更多扩展思路~

完整代码已整理好,可直接复制到项目中使用,建议根据实际业务场景做个性化调整。

脚手架步骤流程

搭建脚手架步骤流程: 在自己的目录新建一个文件夹,命名为test-hp-cli 进入文件夹,执行命令npm init -y 根目录新建bin文件夹,在目录下新建index.js文件 在index.js

在Angular中实现基于nz-calendar的日历甘特图

前言 最近有一个日历相关的功能需求,用于记录主站的各类促销活动。 其中比较棘手的需求是日历需要拥有甘特图那样的功能,持续一段时间的活动需要在日历中以长条形态显示,而不同活动的持续时间不同、排序顺序也不

Next项目中静态资源的压缩、优化

前言 几个月前从0到1搭建落地了现在Next框架的项目,做了很多项目处理,现在想整理一下整个过程,这一次主要是对Next项目中静态资源(图片、打包的静态css、js)的压缩、优化,做个记录 实现了什么
❌