普通视图

发现新文章,点击刷新页面。
今天 — 2025年6月8日首页

🚀🚀🚀Prisma 发布无 Rust 引擎预览版,安装和使用更轻量;支持任何 ORM 连接引擎;支持自动备份...

2025年6月7日 22:26

前言

之前我已经陆陆续续介绍了两篇 Prisma 的文章,最近发布了新的预览版本,我就马上来更新这次的更新内容了~

往期精彩推荐

正文

Prisma 作为一款强大的 ORM 工具,近期发布了多项更新。这些更新不仅提升了开发体验,还为本地开发和数据管理带来了更多便利!

下面是详细内容!

1. Prisma ORM v6.9.0 无 Rust 引擎预览

Prisma ORM v6.9.0 推出了无 Rust 引擎的预览版,减少了对 Rust 的依赖,这意味着安装和部署流程更加轻量,当前支持 PostgreSQLSQLite!

需要在 schema.prisma 中启用预览功能:

generator client {
  provider = "prisma-client-js"
  previewFeatures = ["queryCompiler", "driverAdapters"]
}

2. 通过任何 ORM 连接 Prisma Postgres

Prisma Postgres 现支持通过任何 ORM(如 DrizzleKyselyTypeORM)使用常规 PostgreSQL TCP 连接字符串进行连接。这大大增强了 Prisma Postgres 的灵活性,开发者可以自由选择喜欢的 ORM 工具。无服务器驱动目前仍处于早期访问阶段。

任何 ORM 连接

在环境变量中配置 PostgreSQL 连接字符串:

DATABASE_URL=postgres://user:password@host:port/database?schema=public

在代码中(如使用 Kysely):

import { Kysely } from "kysely";
import { PostgresDialect } from "kysely";
import { Pool } from "pg";

const db = new Kysely({
  dialect: new PostgresDialect({
    pool: new Pool({
      connectionString: process.env.DATABASE_URL,
    }),
  }),
});

开发者可以无缝将 Prisma Postgres 集成到现有项目中,无需局限于 Prisma Client。灵活性提升明显,特别适合混合技术栈团队!

3. Prisma Postgres 自动备份与恢复

Prisma Postgres 新增了自动备份与恢复功能,通过 Prisma Console UI 提供一键操作,开发者可轻松管理数据库备份。这项功能显著提高了数据安全性,适合需要频繁备份的生产环境!

地址:Prisma Console

Prisma Console UI 中,切换到“Backups”选项卡,点击“Create Backup”生成备份,或选择现有备份进行恢复!

Backups

确保配置好环境变量:

PRISMA_POSTGRES_URL=postgres://user:password@host:port/database

自动备份功能简化了数据管理流程,减少手动操作的时间成本。恢复过程通过 UI 直观完成,适合快速回滚或灾难恢复场景。相比传统手动备份,操作效率提升约 50%,且 UI 界面降低了误操作风险。

4. Prisma VS Code 扩展 UI 改进

PrismaVS Code 扩展新增了数据库管理 UI,支持认证、实例管理、数据编辑和模式可视化。这让开发者能在 VS Code 中直接管理 Prisma Postgres 实例,提升生产力。

安装扩展后,在 VS Code 侧边栏打开 Prisma 面板,输入 Prisma Postgres 连接字符串进行认证:

DATABASE_URL=postgres://user:password@host:port/database

随后可通过 UI 创建/删除实例、编辑数据或可视化数据库模式。

UI 创建/删除实例

新 UI 提供了一站式数据库管理体验,开发者无需切换到其他工具即可完成认证、数据编辑等操作。模式可视化功能直观展示表关系,调试效率提升约 30%。对于频繁操作数据库的开发者,这是一个显著的生产力提升。

5. 本地 Prisma Postgres 开发增强

本地 Prisma Postgres 现支持持久化数据库和多实例运行,prisma init 默认使用本地开发环境。

运行以下命令初始化本地开发环境:

npx prisma init --datasource-provider postgres

schema.prisma 中配置本地数据库:

datasource db {
  provider = "postgresql"
  url      = "postgres://localhost:5432/mydb"
}

本地开发环境支持持久化数据和多实例运行,开发者可模拟生产环境进行测试,减少云端成本。prisma init 默认本地化设置简化了配置流程,适合快速原型开发,测试效率提升约 40% !

最后

以上是 Prisma 最新更新的详细解析!这是具体的更新日志:Prisma | Changelog

今天的分享就这些了,感谢大家的阅读,如果文章中存在错误的地方欢迎指正!

往期精彩推荐

记录uniapp开发安卓使用webRTC实现语音推送

作者 张老爷子
2025年6月7日 16:56

最近遇到一个需求,需要使用webRTC向监控摄像头发送语音。这个功能很常见,市面上的监控摄像头(比如我家的小米监控)都有这个互相通话的功能。

但是对于要开发这个功能可以说是毫无头绪,网上找了好多也基本上是使用第三方实时通话方案。但是对于监控来说它不适用啊。于是自己慢慢踩坑记录一下从摸索到实现的过程。

首先说结论,如果你是编译APP,是无法传递音频流的。只能获取一段音频文件然后提交给后端。

app向设备发送音频

const recorderManager = uni.getRecorderManager()
// 监听录音开始
recorderManager.onStart(() => {
  console.log('recorder start');
});
recorderManager.onStop((res) => {
  const { tempFilePath } = res;
  console.log('recorder stop', tempFilePath);
  // 处理录音文件 传给后端
});
recorderManager.start({
  format: 'mp3' // 音频格式,有效值 aac/mp3/wav/PCM。App默认值为mp3,小程序默认值aac
});
// 实际需要绑定停止事件,这里模拟结束
setTimeout(() => {
  recorderManager.stop()
}, 1000)

web应用发送音频

如果是web应用则可通过webRTC直接向rtc地址推送一段流实现实时对讲:

  1. 获取用户音频流:
async function start() {
  const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
}

这一步用于获取音频流,获取到的stream可以直接用于audio标签播放。

  1. 创建RTCPeerConnection:
const pc = new RTCPeerConnection();
pc.addTrack(stream.getAudioTracks()[0], stream);

创建RTCPeerConnection实例并添加音频流。

  1. 创建Offer并发送到服务端:
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);

// 发送Offer到服务器
const response = await axios.post(rtcUrl, offer.sdp, {
  timeout: 10000, // 设置合理的超时时间
})

//传参形式看实际场景
//const response = await axios.post(rtcUrl, {sdp:offer.sdp}, {
//  timeout: 10000, // 设置合理的超时时间
//})

此处rtcUrl就是的音频推送地址,这里由于后端定是这样定义的,所以直接将获取到的offer.sdp作为参数传递给过去(别忘了引入axios)。

4.处理返回结果

// 处理服务器的Answer
const answer = await response.data;
await pc.setRemoteDescription(new RTCSessionDescription(answer));

//上一步的返回结果
{
  code: 0
  id: "CgAAjB9BH0E=_54"
  sdp:'xxx'
  type: "answer"
}

第三步成功以后接口会返回这样的结果,获取到type:answer和具体的sdp以后音频就成功推送到服务器了。

vxe-table 在项目中的实践!【附源码】

2025年6月6日 16:29

大家好,我是 前端架构师 - 大卫

更多优质内容请关注微信公众号 @程序员大卫

初心为助前端人🚀,进阶路上共星辰✨,

您的点赞👍与关注❤️,是我笔耕不辍的灯💡。

背景

vxe-table 是一个基于 Vue 的 PC 端表格组件,功能非常丰富,并且支持 Vue 2.6+Vue 3.x 最新版本。从它的 issues 可以看出,作者解决了很多问题,但目前看起来是由一个人维护。随着版本功能不断增加,有 issues 反馈性能相较于旧版本有所下降。

下面是支持 Vue2 和 Vue3 的源码地址:

支持 Vue 2.6+vxe-table 的源码地址:github.com/x-extends/v…

支持 Vue 3.xvxe-table 的源码地址:github.com/x-extends/v…

说说 Vue2 的问题

我之前遇到的一些老项目使用的是 Vue2,因为这些项目可能还需要兼容 IE11 浏览器,所以 Vue3 不太合适。而 Vue2 可以说是后端开发者写前端代码的便利框架,有以下几点原因,同时也是 Vue2 的缺点:

  • Vue2 不强制使用 Typescript,因此写代码的门槛较低,但这也导致后期的重构和维护成本较高。
  • Vue2 可以往 Vue.configVue.prototype 添加任意全局配置属性和实例方法,导致全局污染严重;而 Vue3 已经不再支持这种做法。
Vue.config.productionTip = false
Vue.prototype.$Alert = Alert
  • Vue2 对子组件的控制权限过大,只要给子组件加上 ref,就可以获取其所有的 data 和 methods,这对项目的维护和重构造成很大困扰,不敢随意改动子组件中的数据和方法。而 Vue3 需要使用 defineExpose 显式暴露属性和方法,React 则是使用 useImperativeHandle
<template>
  <div id="app">
    <ChildCom ref="childRef" />
  </div>
</template>

<script>
import ChildCom from "./components/ChildCom.vue";

export default {
  name: "App",
  components: {
    ChildCom,
  },
  mounted() {
    console.log(this.$refs.childRef.num);
    console.log(this.$refs.childRef.increace);
  },
};
</script>

vxe-table 里的结构

vxe-table 一共包含 5 个组件,下面简单介绍它们的作用:

  • vxe-table:基础表格组件。虽然 v3.9+ 版本已将表格与 UI 组件分离,但其 dependencies 中仍然依赖 vxe-pc-ui,因为 vxe-table 的某些功能仍使用了 vxe-pc-ui
  • vxe-pc-ui:一个 PC 端组件库,你可以把它理解成类似 element ui 的 UI 库,它的 dependencies 中依赖 @vxe-ui/corevxe-table 的部分功能依赖它。例如在 vxe-table 3.15.34 中,如果 <vxe-table show-overflow> 组件使用了 show-overflow 属性,就会调用 vxe-pc-ui 中的 vxe-tooltip 组件。
  • @vxe-ui/core:封装了和表格相关的一些公共方法,比如 i18npermissionthemeVxeGlobalConfig 等。它依赖于 dom-zindexxe-utils
  • dom-zindex:一个用来简单控制和设置 zIndex 的工具包。
  • xe-utils:封装了一些常用工具方法,你可以将其类比为 lodash

⚠️ 注意:vxe-table v3.9+ 已将纯表格和 UI 组件分离,而之前是未分离的。具体可参考:vxetable.cn/v3.8/#/tabl…

vxe-table 里的 5 个组件

最常用的组合是 vxe-tablevxe-column,其他几个组件相对使用频率较低。

1. vxe-table

基础表格功能。

<template>
  <div>
    <vxe-table
      :data="tableData">
      <vxe-column type="seq" width="70"></vxe-column>
      <vxe-column field="name" title="Name"></vxe-column>
      <vxe-column field="sex" title="Sex"></vxe-column>
      <vxe-column field="age" title="Age"></vxe-column>
    </vxe-table>
  </div>
</template>

<script>
export default {
  data () {
    const tableData = [
      { id: 10001, name: 'Test1', role: 'Develop', sex: 'Man', age: 28, address: 'test abc' },
      { id: 10002, name: 'Test2', role: 'Test', sex: 'Women', age: 22, address: 'Guangzhou' },
      { id: 10003, name: 'Test3', role: 'PM', sex: 'Man', age: 32, address: 'Shanghai' },
      { id: 10004, name: 'Test4', role: 'Designer', sex: 'Women', age: 24, address: 'Shanghai' }
    ]
    return {
      tableData
    }
  }
}
</script>

2. vxe-column

表格列的标识,可参考上面 vxe-table 示例。

3. vxe-colgroup

分组表头,用于多级表头展示。

<template>
  <div>
    <vxe-table
      border
      height="400"
      :data="tableData">
      <vxe-colgroup title="基本信息">
        <vxe-column type="seq" width="70"></vxe-column>
        <vxe-column field="name" title="Name"></vxe-column>
      </vxe-colgroup>
      <!-- other code... -->
    </vxe-table>
  </div>
</template>

具体可参考:vxetable.cn/#/component…

4. vxe-grid

配置式表格:以 JSON 方式调用,适合用于低代码平台,纯 JSON 生成表格。

具体可参考: vxetable.cn/#/component…

5. vxe-toolbar

工具栏。

具体可参考: vxetable.cn/#/global/co…

如何让 vxe-table 3.5.9 和 3.15.34 共存

由于公司老项目使用的是 vxe-table@3.5.9,它不支持虚拟滚动下的自适应行高,而部分页面组件需要使用该功能。

如果将 vxe-table3.5.9 一次性升级到 3.15.34,风险较大。下面我们将采用工程化的思路,实现这两个版本在项目中共存。

1. 下载 vxe-table 3.15.34 的代码

访问 vxe-table 官方地址:github.com/x-extends/v…,从 master 分支切换到 v3 分支,下载并解压代码,目录命名为 vxe-table-v3

执行以下命令:

npm install --legacy-peer-deps // 安装依赖
npm run lib // 打包

最终会生成三个目录:eshelperlib。其中,es 模块的入口文件为 es/index.esm.js,通常用于按需引入或 ES Module 规范的引用场景。

⚠️ 注意:建议使用 Node.js 版本 18.20.8(对应的 npm 版本是 10.8.2),不要使用 pnpm 安装依赖,该项目较老,使用 pnpm 会安装失败。

2. 修改 vxe-table 3.15.34 代码

为了避免和 3.5.9 版本的代码冲突,需要修改以下几点。

1. 修改 packages/table/index.ts

主要修改点在于:将组件注册到 app 时添加版本后缀 3,以避免未来全局注册与原版本发生冲突。

export const VxeTable = Object.assign({}, VxeTableComponent, {
  install (app: VueConstructor) {
    // ...
    app.component(VxeTableComponent.name as string + '3', VxeTableComponent)
  }
})

然后删除一些冗余的代码,这些代码是为了调试用途而注入到 Vue.prototype 上的:

if (!Vue.prototype.$vxe) {
  Vue.prototype.$vxe = { t: VxeUI.t, _t: VxeUI._t }
} else {
  Vue.prototype.$vxe.t = VxeUI.t
  Vue.prototype.$vxe._t = VxeUI._t
}

2. 修改 packages/column/index.ts

与上面类似,添加版本标识 3

export const VxeColumn = Object.assign({}, VxeColumnComponent, {
  install (app: VueConstructor) {
    app.component(VxeColumnComponent.name as string + '3', VxeColumnComponent)
  }
})

3. 修改 packages/colgroup/index.ts

同样添加版本标识 3

export const VxeColgroup = Object.assign({}, VxeColgroupComponent, {
  install (app: VueConstructor) {
    app.component(VxeColgroupComponent.name as string + '3', VxeColgroupComponent)
  }
});

4. 修改 packages/grid/index.ts

继续添加版本标识 3

export const VxeGrid = Object.assign({}, VxeGridComponent, {
  install (app: VueConstructor) {
    app.component(VxeGridComponent.name as string + '3', VxeGridComponent)
  }
});

5. 修改 packages/toolbar/index.ts

同理,添加版本标识 3

export const VxeToolbar = Object.assign({}, VxeToolbarComponent, {
  install (app: VueConstructor) {
    app.component(VxeToolbarComponent.name as string + '3', VxeToolbarComponent)
  }
});

3. 初始化打包库的代码

创建文件夹 vxe-table-lib,在其上级目录执行以下 Vite 脚手架命令:

pnpm create vite vxe-table-lib --template vue-ts
cd vxe-table-lib
pnpm install

安装 postcss-prefix-selector,用于给所有样式类添加前缀,避免与 vxe-table 3.5.9 样式冲突:

pnpm i postcss-prefix-selector @types/postcss-prefix-selector -D

4. 修改打包库的代码

创建文件 lib/main.ts,内容如下:

import "../../vxe-table-v3/lib/style.css";

export {
  VxeColumn as VxeColumn3,
  VxeColgroup as VxeColgroup3,
  VxeGrid as VxeGrid3,
  VxeTable as VxeTable3,
  VxeToolbar as VxeToolbar3,
  VxeUI as VxeUI3,
} from "../../vxe-table-v3";

修改 vite.config.ts 如下:

import vue from "@vitejs/plugin-vue";
import postcssPrefixSelector from "postcss-prefix-selector";
import { defineConfig } from "vite";

// https://vite.dev/config/
export default defineConfig({
  plugins: [vue()],
  build: {
    lib: {
      entry: "./lib/main.ts",
      formats: ["es"],
    },
    commonjsOptions: {
      include: [/vxe-table-v3/],
    },
    rollupOptions: {
      external: ["vue", "vue/jsx-runtime"],
      output: {
        assetFileNames: "style.css",
        entryFileNames: "[name].js",
      },
    },
    copyPublicDir: false,
  },
  css: {
    postcss: {
      plugins: [
        postcssPrefixSelector({
          prefix: `.vxe-table-wrapper-v3`,
          transform(_prefix, selector, prefixedSelector) {
            // 只对 .xxx class 类起作用,而对于 :root, @keyframe, [data-xxx] 不起作用。
            if (selector.startsWith(".") || selector.startsWith("#")) {
              return prefixedSelector;
            }
            return selector;
          },
        }),
      ],
    },
  },
});

postcssPrefixSelector 插件的作用是给所有样式添加一个类名前缀 .vxe-table-wrapper-v3

注意其中传递了一个 commonjsOptions 属性,它是专门用于处理 CommonJS 模块(CJS)的配置。Vite 会借助该插件将 CJS 模块转换为 ES 模块,从而在构建过程中正常处理。如果不加这个配置,打包会失败,因为 vxe-table-3 中的部分代码导出方式不符合 ES 规范,具体会报如下错误:

✗ Build failed in 176ms
error during build:
lib/main.ts (4:2): "VxeColumn" is not exported by "../vxe-table-3/es/index.esm.js", imported by "lib/main.ts".
file: /Users/zhengming/git/wechat-oa/examples/vxe-table-3-build/vxe-table-build/lib/main.ts:4:2

2: 
3: export {
4:   VxeColumn as VxeColumn3,
     ^
5:   VxeColgroup as VxeColgroup3,
6:   VxeGrid as VxeGrid3,
// ...
error during build:
../vxe-table-3/node_modules/@vxe-ui/core/es/src/config.js (1:7): "default" is not exported by "../vxe-table-3/node_modules/xe-utils/index.js", imported by "../vxe-table-3/node_modules/@vxe-ui/core/es/src/config.js".
file: /Users/zhengming/git/wechat-oa/examples/vxe-table-3-build/vxe-table-3/node_modules/@vxe-ui/core/es/src/config.js:1:7

1: import XEUtils from 'xe-utils';
          ^
2: import DomZIndex from 'dom-zindex';
3: import { VxeCore } from './core';

// ...

5. 完成打包,生成 dist 文件

执行以下命令,生成文件 dist/main.jsdist/style.css

pnpm build

6. 验证两个版本的共存

1. 初始化项目代码

创建文件夹 vxe-table-test,并初始化一个 Vue2 项目:

npm create vue@legacy // 根据提示一步步创建
cd vxe-table-test
pnpm install

安装 vxe-table@3.5.9

pnpm i vxe-table@3.5.9

2. 编写基于 vxe-table 3.5.9 的组件

1.在 main.js 中全局注册 vxe-table 3.5.9

import VxeUITable from "vxe-table";
import "vxe-table/lib/style.css";

Vue.use(VxeUITable);

2.在 src/components 目录下新建 VxeTable_3_5_9.vue 文件,并编写如下代码:

<template>
  <div>
    <vxe-table
      border
      show-overflow
      height="300"
      :column-config="{ resizable: true }"
      :virtual-y-config="{ enabled: true, gt: 0 }"
      :data="tableData"
    >
      <vxe-column type="seq" width="70"></vxe-column>
      <vxe-column field="name" title="Name"></vxe-column>
      <vxe-column field="role" title="Role"></vxe-column>
      <vxe-column field="sex" title="Sex"></vxe-column>
    </vxe-table>
  </div>
</template>

<script>
export default {
  data() {
    return {
      tableData: [],
    };
  },
  methods: {
    // 模拟生成表格数据
    loadList(size = 300) {
      const dataList = [];
      for (let i = 0; i < size; i++) {
        dataList.push({
          id: 10000 + i,
          name: "Test" + i,
          role: "Developer",
          sex: i === 0 ? "男".repeat(100) : "男",
        });
      }
      this.tableData = dataList;
    },
  },
  created() {
    this.loadList();
  },
};
</script>

<style>
body .vxe-table .vxe-body--column.col--ellipsis:not(.col--actived) > .vxe-cell {
  white-space: normal;
  max-height: fit-content;
}
</style>

3. 编写基于 vxe-table 3.15.34 的组件

src/components 目录下创建 VxeTable_3_15_34.vue 文件,并引入之前打包生成的 vxe-table-lib/dist 中的组件,并编写如下代码:

<template>
  <div class="vxe-table-wrapper-v3">
    <vxe-table3
      border
      height="300"
      :column-config="{ resizable: true }"
      :virtual-y-config="{ enabled: true, gt: 0 }"
      :data="tableData"
    >
      <vxe-column3 type="seq"></vxe-column3>
      <vxe-column3 field="name" title="Name"></vxe-column3>
      <vxe-column3 field="role" title="Role"></vxe-column3>
      <vxe-column3 field="sex" title="Sex"></vxe-column3>
    </vxe-table3>
  </div>
</template>

<script>
import { VxeTable3, VxeColumn3 } from "../../../vxe-table-lib/dist/main.js";
import "../../../vxe-table-lib/dist/style.css";

export default {
  components: {
    VxeTable3,
    VxeColumn3,
  },
  data() {
    return {
      tableData: [],
    };
  },
  methods: {
    // 模拟行数据
    loadList(size) {
      setTimeout(() => {
        const dataList = [];
        for (let i = 0; i < size; i++) {
          dataList.push({
            id: i,
            name: "Test" + i,
            role: "Developer",
            sex:
              i === 0
                ? "男".repeat(100)
                : "男",
          });
        }
        this.tableData = dataList;
      }, 100);
    },
  },
  mounted() {
    this.loadList(300);
  },
};
</script>

4. 在 App.vue 中引入两个版本的组件

<script setup>
import VxeTable_3_15_34 from "./components/VxeTable_3_15_34.vue";
import VxeTable_3_5_9 from "./components/VxeTable_3_5_9.vue";
</script>

<template>
  <div id="app">
    <VxeTable_3_15_34 />
    <div style="height: 40px"></div>
    <VxeTable_3_5_9 />
  </div>
</template>

5. 启动项目

执行以下命令以本地启动项目:

pnpm dev

6. 最终效果验证

启动成功后,页面上可以同时看到两个版本的 vxe-table 渲染结果:

  • vxe-table@3.15.34 支持不定高的虚拟滚动
  • vxe-table@3.5.9 则不支持该特性。

另外如果给 vxe-table@3.15.34 添加 show-overflow 属性时,控制台会提示如下警告:

main.js:3649 [vxe table v3.15.33] 缺少 "vxe-tooltip" 组件,请检查是否正确安装。

这是因为 vxe-tooltipvxe-pc-ui 包中的组件,当前项目中未安装该依赖。因此,在未安装 vxe-pc-ui 的情况下,应避免使用 show-overflow 属性。

源码地址

github.com/zm8/wechat-…

昨天 — 2025年6月7日首页

都25年啦,还有谁分不清双向绑定原理,响应式原理、v-model实现原理

2025年6月6日 21:56

一、Vue 的响应式原理

(一)Vue 2 中的响应式实现

Vue 2 使用Object.defineProperty来实现数据的响应式。当我们将一个普通的 JavaScript 对象传给 Vue 实例的data选项时,Vue 会遍历此对象所有的属性,并使用Object.defineProperty把这些属性全部转换为getter/setter。例如:

let data = {
  message: 'Hello, Vue!'
};
Object.keys(data).forEach(key => {
  let value = data[key];
  Object.defineProperty(data, key, {
    enumerable: true,
    configurable: true,
    get() {
      console.log(`Getting ${key}: ${value}`);
      return value;
    },
    set(newValue) {
      if (newValue!== value) {
        value = newValue;
        console.log(`Setting ${key} to: ${newValue}`);
        // 这里触发视图更新的相关逻辑
      }
    }
  });
});

在上述代码中,通过Object.defineProperty为data对象的message属性添加了getter和setter。当获取message属性时,会触发getter,打印相关信息;当设置message属性时,会触发setter,在setter中判断新值与旧值是否不同,若不同则更新值并可以触发视图更新的逻辑(实际 Vue 中更为复杂,这里仅作示意)。

(二)Vue 3 中的响应式实现

Vue 3 改用Proxy来实现响应式。Proxy可以直接监听对象上属性的添加和删除,弥补了Object.defineProperty的一些不足。示例如下:

let data = {
  message: 'Hello, Vue 3!'
};
let handler = {
  get(target, key) {
    console.log(`Getting ${key}: ${target[key]}`);
    return target[key];
  },
  set(target, key, value) {
    if (target[key]!== value) {
      target[key] = value;
      console.log(`Setting ${key} to: ${value}`);
      // 这里触发视图更新的相关逻辑
      return true;
    }
    return false;
  }
};
let proxy = new Proxy(data, handler);

在这个例子中,通过Proxy创建了一个代理对象proxy,对data对象的属性访问和设置进行了拦截。当获取或设置proxy对象的属性时,会执行handler中的get和set方法,同样可以在set方法中触发视图更新逻辑。

(三)依赖收集与更新

无论是 Vue 2 还是 Vue 3,响应式系统的核心都包括依赖收集和更新。当数据被访问时,会进行依赖收集,将依赖该数据的 “观察者”(Watcher)添加到一个依赖容器(如 Vue 2 中的Dep)中。当数据发生变化时,依赖容器会通知所有相关的Watcher进行更新,从而重新渲染视图。例如在 Vue 2 中,每个组件实例都对应一个Watcher实例,它会在组件渲染的过程中把 “接触” 过的数据属性记录为依赖。之后当依赖项的setter触发时,会通知Watcher,使它关联的组件重新渲染。

二、Vue 的双向绑定原理

(一)双向绑定的概念

双向绑定的核心是让数据的变化自动反映到视图上,而视图的变化也能自动同步回数据。它主要应用于表单元素(如、等)。Vue 的双向绑定原理建立在响应式系统和事件监听机制上。

(二)实现流程

以为例,双向绑定的实现流程如下:

  1. 初始化绑定:当 Vue 实例被创建时,会对data选项中的message数据进行响应式处理,为其设置getter和setter。v-model指令会将message的初始值绑定到元素中,使视图显示message的当前值。
  1. 数据变化更新视图:当message的值发生改变时,Vue 的响应式系统会触发message的setter。setter通知依赖于message的Watcher去更新视图,Watcher会重新渲染依赖message的 DOM 元素,使显示的新值与数据同步。
  1. 视图变化更新数据:在使用v-model时,Vue 会为元素添加一个input事件监听器。当用户在输入框中输入内容时,会触发input事件,Vue 捕获到这个事件后,将输入框的值赋给message,触发message的setter,数据会被更新为用户输入的内容。由于数据被更新,Vue 会再次触发响应式更新过程,如果有其他依赖于message的 DOM 元素或计算属性,它们也会同步更新。

三、v-model 的实现原理

(一)v-model 是语法糖

v-model本质上是一个语法糖。它负责监听用户的输入事件以更新数据,并对一些极端场景进行特殊处理。它会忽略所有表单元素的value、checked、selected attribute 的初始值,而总是将 Vue 实例的数据作为数据来源。我们应该通过 JavaScript 在组件的data选项中声明初始值。

(二)v-model 在不同表单元素上的实现

  1. text 和 textarea 元素:使用value property 和input事件。例如,等价于<input type="text" :value="message" @input="message = event.target.value">:value="message"实现了数据到视图的单向绑定,将message的值绑定到输入框的value属性上;@input="message=event.target.value">。:value="message"实现了数据到视图的单向绑定,将message的值绑定到输入框的value属性上;@input="message = event.target.value"监听输入事件,当用户输入内容时,将新的值赋给message,实现视图到数据的同步。
  1. radio 和 checkbox 元素:使用checked property 和change事件。对于多个复选框,需要手动设置value值,以便在事件处理函数中拿到新数据来更新数据。而单个复选框绑定的是布尔值,在其事件处理函数中可以直接拿到新数据进行更新。例如:
<input type="checkbox" v-model="isChecked">
<input type="checkbox" v-model="checkboxValues" value="option1">
<input type="checkbox" v-model="checkboxValues" value="option2">
  1. select 字段:将value作为 prop 并将change作为事件。例如:
<select v-model="selectedOption">
  <option value="option1">Option 1</option>
  <option value="option2">Option 2</option>
</select>

等价于:

<select :value="selectedOption" @change="selectedOption = $event.target.value">
  <option value="option1">Option 1</option>
  <option value="option2">Option 2</option>
</select>

(三)v-model 在组件中的应用

在组件中,一个组件上的v-model默认会利用名为value的属性和名为input的事件(Vue 2),在 Vue 3 中v-model默认使用modelValue属性和update:modelValue事件来实现双向数据绑定。实现双向绑定的核心步骤如下:

  1. 父传子:数据通过父组件的props传递给子组件,子组件内将v-model拆解绑定数据。
  1. 子传父:通过自定义事件,子组件将新值传递给父组件修改。例如在 Vue 2 中:
<!-- 父组件 -->
<template>
  <MyComponent v-model="parentValue"></MyComponent>
</template>
<script>
import MyComponent from './MyComponent.vue';
export default {
  components: {
    MyComponent
  },
  data() {
    return {
      parentValue: ''
    };
  }
};
</script>
<!-- 子组件MyComponent.vue -->
<template>
  <input :value="value" @input="$emit('input', $event.target.value)">
</template>
<script>
export default {
  props: ['value']
};
</script>

在 Vue 3 中:

<!-- 父组件 -->
<template>
  <MyComponent v-model="parentValue"></MyComponent>
</template>
<script setup>
import MyComponent from './MyComponent.vue';
let parentValue = ref('');
</script>
<!-- 子组件MyComponent.vue -->
<template>
  <input :modelValue="modelValue" @update:modelValue="(newValue) => $emit('update:modelValue', newValue)">
</template>
<script setup>
defineProps(['modelValue']);
defineEmits(['update:modelValue']);
</script>

总结:

 以下是对 Vue 响应式原理、双向绑定原理及 `v-model` 实现原理的总结,帮助快速梳理核心要点:

一、响应式原理

1. Vue 2(Object.defineProperty

  • 核心机制:通过 Object.defineProperty 为数据对象的属性添加 getter/setter,拦截属性的读取和修改。
  • 依赖收集:当属性被读取时(触发 getter),收集当前组件的 Watcher 作为依赖。
  • 更新触发:当属性被修改时(触发 setter),通知所有依赖的 Watcher 重新渲染视图。
  • 局限性:无法监听数组索引和对象新增属性的变化,需通过 Vue.set 等方法手动处理。

2. Vue 3(Proxy

  • 核心机制:使用 Proxy 代理原始数据对象,拦截所有属性操作(读取、修改、删除等)。
  • 依赖收集:通过 Proxy 的 get 拦截器收集依赖(Watcher)。
  • 更新触发:通过 set 拦截器触发依赖更新,自动处理数组和对象新增属性的响应式。
  • 优势:更高效、更全面,原生支持数组和对象的所有操作。

二、双向绑定原理

核心逻辑

  • 数据 → 视图:基于响应式系统,数据变化时通过 Watcher 自动更新视图。
  • 视图 → 数据:通过监听表单元素的事件(如 inputchange),将用户输入的值同步回数据。
  • 典型场景:表单元素(如 <input><textarea><select>)的值与 Vue 实例的数据同步。

实现流程

  1. 初始化绑定:将数据初始值渲染到视图(如 :value="data")。
  2. 数据更新视图:数据变化时,响应式系统触发视图重新渲染。
  3. 视图更新数据:表单元素触发事件(如 input)时,将新值赋值给数据(如 data = $event.target.value)。

三、v-model 实现原理

1. 本质:语法糖

  • 简化表单元素的双向绑定操作,等价于 :value(或 :checked 等)与事件监听(如 @input@change)的组合。

2. 不同元素的实现

元素类型 绑定属性 监听事件 等价代码示例
input/textarea :value @input <input :value="data" @input="data=$event.target.value">
checkbox :checked @change <input :checked="data" @change="data=$event.target.checked">
radio :checked @change <input :checked="data" @change="data=$event.target.value">
select :value @change <select :value="data" @change="data=$event.target.value">

3. 组件中的 v-model

  • Vue 2:通过 props 接收父组件数据(默认属性 value),通过 $emit('input', 新值) 通知父组件更新。
  • Vue 3:通过 props 接收 modelValue,通过 $emit('update:modelValue', 新值) 实现双向绑定。

四、常见误区与注意事项

  1. 响应式边界

    • Vue 2 中,直接修改数组索引或对象新增属性不会触发更新,需用 Vue.set 或数组变异方法(如 pushsplice)。
    • Vue 3 中,Proxy 原生支持数组和对象的所有操作,无需额外处理。
  2. v-model 与单向绑定的区别

    • v-model 是双向绑定,同时包含数据到视图(:)和视图到数据(@)的逻辑。
    • 单向绑定(如 :value)仅实现数据到视图的更新,需手动添加事件监听才能反向更新数据。
  3. 组件开发注意点

    • 子组件使用 v-model 时,需显式声明 props 和触发对应事件(input 或 update:modelValue)。
    • 避免在子组件中直接修改 props 传递的数据,应通过事件通知父组件修改。

总结对比

特性 Vue 2(Object.defineProperty Vue 3(Proxy
响应式实现 getter/setter 拦截属性 Proxy 代理对象所有操作
数组 / 对象新增属性 需手动处理(Vue.set 自动支持
v-model 本质 语法糖(:value + @input 等) 同上,但组件中使用 modelValue 和 update:modelValue

🚀 前端开发福音:用 json-server 快速搭建本地 Mock 数据服务

作者 zhEng
2025年6月6日 16:22

假如你是一名前端开发者,曾为了“等后端接口”而发愁,那么你一定会爱上这个工具——json-server

🧠 为什么要用 json-server?

在开发中,接口数据未准备好是家常便饭。前端同学常常陷入以下困境:

  • 后端接口还没写好,我就不能调试页面了?
  • 想调试一个边界条件,却苦等后端造数据?
  • 接口文档与实际返回格式不一致,怎么办?

json-server 的出现就是为了解决这些痛点!

你只需要一个 JSON 文件,就能在几秒钟内启动一个完整的 REST API 服务,让前端不再“接口等于被动”。

🔧 一、快速开始

1. 安装

在你的前端项目中,使用 npm 或 yarn 安装:

npm install -g json-server
# 或者项目局部安装
npm install json-server --save-dev

2. 创建数据文件

创建一个 db.json 文件,作为模拟数据库。示例内容如下:

{
  "users": [
    { "id": 1, "name": "小明", "age": 18, "hobby": "唱歌" },
    { "id": 2, "name": "小红", "age": 20, "hobby":"跳舞" }
  ],
  "articles": [
    { "id": 1, "title": "前端工程化", "author": "小明" },
    { "id": 2, "title": "Vue3实战", "author": "小红" }
  ]
}

3. 启动服务

package.json文件配置scripts执行脚本

  "scripts": {
    "mock:server": "json-server --watch db.json"
  },

运行以下命令:

npm run mock:server

默认会监听 http://localhost:3000,你将获得一个完整的 REST API 服务。

📚 二、支持的 RESTful 接口

你可以直接使用以下方式访问数据:

  • 获取所有用户:GET /users
  • 获取指定用户:GET /users/1
  • 添加用户:POST /users
  • 修改用户:PUT /users/1
  • 删除用户:DELETE /users/1

是不是很像真实接口?是的,因为它本来就是模拟真实 REST 接口的。

🧪 三、进阶用法

1. 自定义端口和根路径

你可以通过参数修改默认行为:

"scripts": {
   "mock:server": "json-server --watch db.json --port 4000 --routes routes.json"
}

2. 配置 routes.json 自定义路由

如果你想将 /users 映射为 /api/user-list,可以创建 routes.json

// routes.json
{
  "/api/user-list":"/users",
  "/api/get-user-info/:id": "/users/:id",
  "/api/add-user": "/users",
  "/api/update-user-info/:id":"/users/:id",
  "/api/delete-user/:id":"/users/:id",
  "/api/article-list":"/articles"
}

然后用命令启动:

npm run mock:server

访问 http://localhost:4000/api/user-list/users 即可返回用户列表。

3. 使用中间件

可以使用自定义中间件来扩展json-server的功能。例如,添加日志记录中间件:

// middlewares.js
module.exports = (req,res,next)=> {
  console.log(req,'req')
  console.log(res,'res')
  console.log('这是第一个中间件')
  next();
}

启动服务时指定中间件:

// package.json
{
"scripts": {
   "mock:server": "json-server --watch db.json --middlewares ./middlewares.js"
}

4. 自定义配置文件

json-server允许我们把所有的配置放到一个配置文件中,默认为json-server.json。配置文件的主要内容如下:

// json-server.json
{
  // 自定义服务监听端口
  "port": 4000,  
  // 服务监听
  "watch": true, 
  "host": "127.0.0.1",// 指定域
  // 静态文件目录,可以将项目的HTML,JS,IMG等资源放在这里
  "static": "./public", 
   // 是否只允许get请求
  "read-only": false,
   // 是否允许跨域访问
  "no-cors":false,
   // 是否可压缩
  "no-gzip": false, 
   // 自定义路由,这个配置可以暂时省略,后续会有所涉及
  "routes": "routes.json"
}

配置运行脚本

// package.json
{
  "scripts": {
    "mock:server": "json-server -w db.json"
  }
}

可以通过--config或简写-c,指定配置文件名称

// package.json
{
  "scripts": {
    "mock:server": "json-server -w db.json --config ./json-server-config.json"
  }
}

5.其他相关配置参数

参数 简写 默认值 说明
--watch -w false 监听文件变动自动重启服务,常用于开发时实时响应数据更新
--port -p 3000 设置服务运行端口
--host -H localhost 设置服务器绑定的主机名
--routes -r 指定 routes.json 文件路径,用于配置自定义路由映射
--middlewares 指定中间件文件路径(支持一个或多个 .js 文件)
--static -s 设置静态文件目录(如配合前端页面开发)
--read-only false 启用只读模式,禁止对数据进行 POST/PUT/PATCH/DELETE 操作
--no-cors false 禁用跨域请求头(CORS)
--no-gzip false 禁用 Gzip 压缩
--delay 0 模拟网络延迟(单位为毫秒)
--id id 设置资源主键字段(默认是 id
--foreignKeySuffix Id 设置外键字段后缀(例如 postIduserId
--quiet -q false 启用静默模式,控制台不输出请求日志
--help -h - 查看帮助信息
--version -v - 查看版本信息

🧱 四、通过接口获取数据

1.获取users列表数据

  • http://localhost:4000/api/user-list
const { data } = axios.get('http://localhost:4000/api/user-list');
// 返回
[
    {
        "id": 1,
        "name": "小明",
        "age": 18,
        "hobby": "唱歌"
    },
    {
        "id": 2,
        "name": "小红",
        "age": 20,
        "hobby": "跳舞"
    }
]

2.根据id获取用户数据

  • http://localhost:4000/api/get-user-info/1
const { data } = axios.get(`http://localhost:4000/api/get-user-info/1`);
// 返回
{
    "id": 1,
    "name": "小明",
    "age": 18,
    "hobby": "唱歌"
}

3.添加用户数据

  • http://localhost:4000/api/add-user
const form =  {
  name: "小刘",
  age: 16,
  hobby: "羽毛球"
}
const { data } = axios.post(`http://localhost:4000/api/add-user`,form);
   // 返回新增成功的数据
{
  "id": 3
  "name": "小刘",
  "age": 16,
  "hobby": "羽毛球",
}

4.修改用户数据

  • http://localhost:4000/api/update-user-info/3
const form = {
    id:3,
    name:'小刘',
    age:18,
    hobby: '踢足球'
}
const { data } = axios.put(`http://localhost:4000/api/update-user-info/3`,form);
// 返回修改成功后的数据
{
    "id": 3,
    "name": "小刘",
    "age": 18,
    "hobby": "踢足球"
}

5.根据用户id删除用户数据

  • http://localhost:4000/api/delete-user/3
// 删除id为3的用户数据
const { data } = axios.delete(`http://localhost:4000/api/delete-user/3`);

⚠️ 五、常见问题

Q1:为什么 POST 后的数据没有 id?

json-server 会自动为你添加自增 id,除非你手动指定。

Q2:能否写一些默认响应字段?

可以在 POST 时发送完整对象,也可以配合前端自动补字段。

Q3:如何模拟嵌套关系(如用户下的文章)?

你可以在 db.json 中设计嵌套结构,或者用 _expand_embed 功能:

GET /articles?_expand=user
GET /users?_embed=articles

🎁 六、推荐使用场景

  • 🎯 前后端分离开发阶段
  • 📱 移动端接口调试
  • 🧪 测试场景的数据造假
  • 🎨 UI 组件对数据有依赖时的联调阶段

🧩 附加资源

昨天以前首页

(VUE3集成CAD)在线CAD实现焊接符号自定义

2025年6月6日 11:39

前言

在工程制图和制造领域,焊接符号(Welding Symbols)是用于表示焊缝类型、尺寸、位置以及工艺要求的标准化图形语言。广泛应用于机械设计、钢结构、船舶制造、压力容器等行业中,帮助技术人员理解焊接意图。

本文将介绍焊接符号的基本结构、符号、含义,并根据焊接符号的特性通过网页CAD实现焊接符号类,实现绘制焊接符号的功能。

image-20250530143231089.png

焊接符号特性分析

一、焊接符号的基本构成

焊接符号由以下几个基本部分组成:

1.参考线:焊接符号的核心部分,通常是一条水平线,所有其他符号都依附于这条线。参考线分为两部分,箭头上面就是上方区域,箭头下面就是下方区域,如下图:

9d2077076.png

2.箭头:箭头指向要焊接的位置,连接参考线和被焊件的具体部位。

3.基本焊接符号:表示焊缝的类型,如角焊缝、对接焊缝等,绘制在参考线的上方或下方。

4.尾部:可选部分,用于标注焊接方法、工艺编号或其他说明信息(如“GTAW”、“SMAW”等)。

5.补充符号:包括现场焊缝符号、周围焊缝符号、熔透深度符号等。

6.尺寸标注:表示焊缝的具体尺寸,如焊脚高度、坡口角度、根部间隙等。

二、焊接符号的附加元素

1.现场焊缝符号:一个小旗子标志,表示该焊缝需在现场施工时完成,而非在工厂内完成。

2.周围焊缝符号:一个圆圈,表示焊缝应围绕整个接合处进行。 

3.熔透符号:表示焊接过程中需要完全熔透母材。

4.打底焊道符号:表示打底焊或衬垫焊。

三、焊接尺寸标注方法

焊接符号中常常包含尺寸信息,以指导焊工操作。以下是常见尺寸标注方式:

3.1.角焊缝尺寸标注

  • 焊脚尺寸(Leg Size):用数字标注在焊缝符号左侧。
  • 长度:标注在符号右侧。
  • 间距(Pitch):标注在长度之后,斜杠后加数字。例如:6/25 表示每25mm间距有一个6mm的角焊缝。

3.2.对接焊缝尺寸标注

  • 坡口角度:标注在符号旁边。
  • 根部间隙:标注在角度下方。
  • 钝边厚度:标注在角度另一侧

实现 McDbTestWeldingSymbol 焊接符号自定义实体

1.定义符号相关的枚举或接口  

 // 符号位置
   enum symbolPos {
       top = 1,
       down
   }
   // 符号名
   enum symbolName {
       // 带垫板符号
       WithPadSymbol,
       // 周围焊接符
       SurroundingWeldSeamSymbol,
       // 现场符号
       OnSiteSymbol,
       // 三面焊缝符号
       ThreeSidedWeldSeamSymbol,
       // 尾部符号
       TailSymbol,
       // 交错断续焊接符号
       ZSymbol,
       // 卷边焊缝
       RolledEdge,
       // I型焊缝
       IWeldSeam,
       // V型焊缝
       VWeldSeam,
       // 单边V型焊缝
       SingleVWeldSeam,
       // 单边陡侧V型坡口堆焊
       SingleSteepVWeldSeam,
       // 倾斜焊缝
       InclinedWeldSeam,
       // 可移除衬垫
       RemovablePadding1,
       RemovablePadding2,
       // 持久衬垫
       DurableLiner,
       // 带顿边V型焊缝
       VFlangedEdgeSeam,
       // 带顿边U型焊缝
       UFlangedEdgeSeam,
       // 带顿边单边V型焊缝
       SingleVFlangedEdgeSeam,
       // 带顿边单边U型焊缝
       SingleUFlangedEdgeSeam,
       // 端接焊缝
       EndWeldSeam,
       // 堆焊接头
       SurfacingJoint,
       // 封底焊缝
       BottomSeamWeld,
       // 角焊缝
       FilletWeld,
       // 塞焊缝或槽焊缝
       GrooveWeldSeam,
       // 点焊缝
       SpotWeldSeam,
       // 折叠接口
       FoldingInterface,
       // 倾斜接口
       InclinedInterface,
       // 点焊缝(偏移中心)
       SpotWeldSeamOffset,
       // 缝焊缝
       SeamWeld,
       // 缝焊缝(偏离中心)
       SeamWeldOffset,
       // 陡侧V型坡口堆焊
       SteepVWeldSeam,
       // 喇叭形焊缝
       BellShapedWeldSeam,
       // 单边喇叭形焊缝
       SingleBellShapedWeldSeam,
       // 堆焊缝
       HeapWeldSeam,
       // 锁边焊缝
       SeamSeam,
       // 锁边坡口
       LockTheSlopeOpening,
       // 第一种平面符号
       FirstPlaneSymbol,
       // 第二种凹面符号
       SecondConcaveSymbol,
       // 第二种凸面符号
       SecondConvexeSymbol,
       // 第二种平面符号
       SecondPlaneSymbol,
       // 第三种平面符号
       ThirdPlaneSymbol,
       // 第一种凸面符号
       FirstConvexeSymbol,
       // 第一种凹面符号
       FirstConcaveSymbol,
       // 削平焊缝
       FlattenWeldSeam,
       // 趾部平滑过渡
       ToeAareaTranSmoothly,
       // 无
       none,
   }
   // 符号类型设置
   interface symbolType {
       typeName: symbolName,
       position?: symbolPos,
       rotation?: number,
       info?: string
   }
   // 符号标注设置
   interface symbolDim {
       position: symbolPos,
       content: string
   }

2.基本结构设置

  // 焊接符号自定义实体
   export class McDbTestWeldingSymbol extends McDbCustomEntity {
       // 定义McDbTestWeldingSymbol内部的点对象
       // 标记定位点
       private position: McGePoint3d;
       // 标记转折点
       private turningPt: McGePoint3d;
       // 标记定点
       private fixedPoint: McGePoint3d;
       // 箭头长度
       private size: number = 4;
       // 基本符号
       private baseSymbok: symbolType[] = [];
       // 辅助符号
       private auxiliarySymbol: symbolType[] = [];
       // 特殊符号
       private specialSymbol: symbolType[] = [];
       // 补充符号
       private suppleSymbol: symbolType[] = [];
       // 左尺寸
       private leftDim: symbolDim[] = [];
       // 上尺寸
       private topDim: symbolDim[] = [];
       // 右尺寸
       private rightDim: symbolDim[] = [];
       // 虚线位置:0:无虚线, 1:下虚线,2:上虚线
       private dottedPos: number = 0;
       // 标注内字高
       private height: number = this.size * (7 / 8);
       // 焊接说明
       private weldingInfo: string = '';
       // 交错焊缝
       private interlacedWeldSeam: symbolDim[] = [];
   
       /** 包围盒最大点 包围盒最小点 */
       private minPt: McGePoint3d = new McGePoint3d();
       private maxPt: McGePoint3d = new McGePoint3d();
   }

3.构造函数和创建方法

       // 构造函数
       constructor(imp?: any) {
           super(imp);
       }
       // 创建函数
       public create(imp: any) {
           return new McDbTestWeldingSymbol(imp)
       }
       // 获取类名
       public getTypeName(): string {
           return "McDbTestWeldingSymbol";
       }

4.数据持久化

 // 读取自定义实体数据pt1、pt2
       public dwgInFields(filter: IMcDbDwgFiler): boolean {
           this.position = filter.readPoint("position").val;
           this.turningPt = filter.readPoint("turningPt").val;
           this.fixedPoint = filter.readPoint("fixedPoint").val;
           this.size = filter.readDouble("size").val;
           this.dottedPos = filter.readLong("dottedPos").val;
           this.minPt = filter.readPoint("minPt").val;
           this.maxPt = filter.readPoint("maxPt").val;
           const _baseSymbok = filter.readString('baseSymbok').val;
           this.baseSymbok = JSON.parse(_baseSymbok)
           const _AuxiliarySymbol = filter.readString('AuxiliarySymbol').val;
           this.auxiliarySymbol = JSON.parse(_AuxiliarySymbol)
           const _specialSymbol = filter.readString('specialSymbol').val;
           this.specialSymbol = JSON.parse(_specialSymbol)
           const _suppleSymbol = filter.readString('suppleSymbol').val;
           this.suppleSymbol = JSON.parse(_suppleSymbol)
           const _leftDim = filter.readString('leftDim').val;
           this.leftDim = JSON.parse(_leftDim);
           const _topDim = filter.readString('topDim').val;
           this.topDim = JSON.parse(_topDim);
           const _rightDim = filter.readString('rightDim').val;
           this.rightDim = JSON.parse(_rightDim);
           const _interlacedWeldSeam = filter.readString('interlacedWeldSeam').val;
           this.interlacedWeldSeam = JSON.parse(_interlacedWeldSeam);
           this.weldingInfo = filter.readString('weldingInfo').val;
           return true;
       }
       // 写入自定义实体数据pt1、pt2
       public dwgOutFields(filter: IMcDbDwgFiler): boolean {
           filter.writePoint("turningPt", this.turningPt);
           filter.writePoint("position", this.position);
           filter.writePoint("fixedPoint", this.fixedPoint);
           filter.writeDouble("size", this.size);
           filter.writeLong("dottedPos", this.dottedPos);
           filter.writePoint("minPt", this.minPt);
           filter.writePoint("maxPt", this.maxPt);
           filter.writeString('baseSymbok', JSON.stringify(this.baseSymbok));
           filter.writeString('AuxiliarySymbol', JSON.stringify(this.auxiliarySymbol));
           filter.writeString('specialSymbol', JSON.stringify(this.specialSymbol));
           filter.writeString('suppleSymbol', JSON.stringify(this.suppleSymbol));
           filter.writeString('leftDim', JSON.stringify(this.leftDim));
           filter.writeString('topDim', JSON.stringify(this.topDim));
           filter.writeString('rightDim', JSON.stringify(this.rightDim));
           filter.writeString('interlacedWeldSeam', JSON.stringify(this.interlacedWeldSeam));
           filter.writeString('weldingInfo', this.weldingInfo);
           return true;
       }

5.设置标注夹点及夹点移动规则

  // 移动自定义对象的夹点
       public moveGripPointsAt(iIndex: number, dXOffset: number, dYOffset: number, dZOffset: number) {
           this.assertWrite();
           if (iIndex === 0) {
               this.position.x += dXOffset;
               this.position.y += dYOffset;
               this.position.z += dZOffset;
           } else if (iIndex === 1) {
               this.turningPt.x += dXOffset;
               this.turningPt.y += dYOffset;
               this.turningPt.z += dZOffset;
           } else if (iIndex === 2) {
               this.fixedPoint.x += dXOffset;
               this.fixedPoint.y += dYOffset;
               this.fixedPoint.z += dZOffset;
           }
       };
       // 获取自定义对象的夹点
       public getGripPoints(): McGePoint3dArray {
           let ret = new McGePoint3dArray()
           ret.append(this.position);
           ret.append(this.turningPt);
           ret.append(this.fixedPoint);
           return ret;
       };

6.绘制标注实体

// 绘制实体
       public worldDraw(draw: MxCADWorldDraw): void {
           const allEntityArr = this.getAllEntity();
           allEntityArr.forEach((ent, index) => {
               if(index === allEntityArr.length-1) draw.setupForEntity(ent);
               draw.drawEntity(ent)
           });
       }
       private getAllEntity(): McDbEntity[] {
           const allEntityArr: McDbEntity[] = [];
           const noChangeEntityArr: McDbEntity[] = [];
           // 是否反向
           let isReverse = this.fixedPoint.x < this.position.x ? true : false;
           // 引线结束点
           let lastPt = this.fixedPoint.clone();
           lastPt.addvec(McGeVector3d.kXAxis.clone().mult(this.size * (1 / 8)));
           // 绘制标注线
           const pl = new McDbPolyline();
           pl.addVertexAt(this.position);
           pl.addVertexAt(this.turningPt);
           pl.addVertexAt(this.fixedPoint);
           noChangeEntityArr.push(pl);
           // 绘制标注箭头
           const v = this.turningPoint.sub(this.position).normalize();
           const midPt = this.position.clone().addvec(v.mult(this.size));
           const _v = v.clone().perpVector().mult(this.size * (1 / 8));
           const pt1 = midPt.clone().addvec(_v);
           const pt2 = midPt.clone().addvec(_v.clone().negate());
           const hatch = new McDbHatch();
           hatch.appendLoop(new McGePoint3dArray([pt1, pt2, this.position]));
           noChangeEntityArr.push(hatch);
           // 绘制补充符号
           if (this.suppleSymbol.length) {
               const { entityArr, noChangeArr, v } = this.drawSuppleSymbol(isReverse);
               lastPt.addvec(v);
               allEntityArr.push(...entityArr);
               noChangeEntityArr.push(...noChangeArr);
           }
           // 绘制左尺寸
           if (this.leftDim.length) {
               const { entityArr, v } = this.drawLeftDim(lastPt);
               lastPt.addvec(v);
               allEntityArr.push(...entityArr)
           }
           // 绘制特殊符号
           if (this.specialSymbol.length) {
               const { entityArr } = this.drawSpecialSymbol(lastPt);
               allEntityArr.push(...entityArr);
                  lastPt.addvec(McGeVector3d.kXAxis.clone().mult(this.size / 2))
           }
           if (this.suppleSymbol.length && this.suppleSymbol.filter(item => item.typeName === symbolName.WithPadSymbol).length) {
               const entityArr = this.drawWidth(lastPt);
               allEntityArr.push(...entityArr);
           }
           // 绘制基本符号
           if (this.baseSymbok.length) {
               const { entityArr } = this.drawBasicSymbol(lastPt);
               allEntityArr.push(...entityArr);
           }
           // 绘制辅助符号
           if (this.auxiliarySymbol.length) {
               const { entityArr } = this.drawAuxiliarySymbol(lastPt);
               allEntityArr.push(...entityArr);
           }
           // 绘制上尺寸
           if (this.topDim.length) {
               const { entityArr } = this.drawTopDim(lastPt);
               allEntityArr.push(...entityArr)
           }
           lastPt.addvec(McGeVector3d.kXAxis.clone().mult(this.size * (7 / 4)));
           // 绘制右尺寸
           if (this.rightDim.length) {
               const { entityArr, v } = this.drawRightDim(lastPt);
               lastPt.addvec(v);
               allEntityArr.push(...entityArr)
           }
           lastPt.addvec(McGeVector3d.kXAxis.clone().mult(this.size * (4 / 8)));
           // 绘制交错断续焊接符号
           if (this.suppleSymbol.length && this.suppleSymbol.filter(item => item.typeName === symbolName.ZSymbol).length) {
               if (this.baseSymbok.length || this.suppleSymbol.filter(item => item.typeName === symbolName.WithPadSymbol)) lastPt.addvec(McGeVector3d.kXAxis.clone().mult(this.size / 2));
               const distanceVec = McGeVector3d.kXAxis.clone().mult(this.size * (1 / 16));
               const v_y = McGeVector3d.kYAxis.clone().mult(this.size * (17 / 16));
               const v_x = McGeVector3d.kXAxis.clone().mult(this.height).negate();
               const pt2 = lastPt.clone().addvec(v_y);
               const pt4 = lastPt.clone().addvec(v_y.clone().negate());
               const pt1 = pt2.clone().addvec(v_x);
               const pt3 = pt4.clone().addvec(v_x);
               const pl = new McDbPolyline();
               pl.addVertexAt(pt1);
               pl.addVertexAt(pt2);
               pl.addVertexAt(pt3);
               pl.addVertexAt(pt4);
               allEntityArr.push(pl);
               if (this.interlacedWeldSeam.length) {
                   let maxPt_x = null;
                   this.interlacedWeldSeam.forEach(item => {
                       const text = new McDbMText();
                       text.contents = item.content;
                       text.textHeight = this.height;
                       text.attachment = McDb.AttachmentPoint.kTopLeft;
                       if (item.position === symbolPos.down) {
                           text.location = lastPt.clone().addvec(distanceVec)
                       } else if (item.position === symbolPos.top) {
                           text.location = lastPt.clone().addvec(McGeVector3d.kYAxis.clone().mult(this.size)).addvec(distanceVec)
                       }
                       // 将ent的文字样式设置为当前文字样式
                       const textStyleId = MxCpp.getCurrentMxCAD().getDatabase().getCurrentlyTextStyleId();
                       text.textStyleId = textStyleId;
                       text.reCompute();
                       const { maxPt } = MxCADUtility.getTextEntityBox(text, false);
                       if (!maxPt_x || maxPt_x < maxPt.x) {
                           maxPt_x = maxPt.x;
                       }
                       allEntityArr.push(text);
                   });
                   lastPt.x = maxPt_x;
               }
           }
           // 绘制尾部符号(焊接说明)
           if (this.suppleSymbol.length && this.suppleSymbol.filter(item => item.typeName === symbolName.TailSymbol).length) {
               const vec = this.fixedPoint.sub(lastPt).mult(2);
               const v_y = McGeVector3d.kYAxis.clone().mult(this.height);
               const v_x = isReverse ? McGeVector3d.kXAxis.clone().negate().mult(this.height) : McGeVector3d.kXAxis.clone().mult(this.height);
               const pt1 = lastPt.clone().addvec(v_y).addvec(v_x);
               const pt2 = lastPt.clone().addvec(v_y.clone().negate()).addvec(v_x);
               const pl = new McDbPolyline();
               pl.addVertexAt(pt1)
               pl.addVertexAt(lastPt);
               pl.addVertexAt(pt2);
               if (isReverse) pl.move(lastPt, lastPt.clone().addvec(vec));
               noChangeEntityArr.push(pl)
   
               if (this.weldingInfo) {
                   const text = new McDbMText();
                   text.contents = this.weldingInfo;
                   const v = isReverse ? McGeVector3d.kXAxis.clone().mult(this.size * (5 / 4)).negate() : McGeVector3d.kXAxis.clone().mult(this.size * (5 / 4))
                   text.location = lastPt.clone().addvec(v);
                   text.attachment = isReverse ? McDb.AttachmentPoint.kMiddleRight : McDb.AttachmentPoint.kMiddleLeft;
                   text.textHeight = this.height;
                   if (isReverse) text.move(lastPt, lastPt.clone().addvec(vec));
                   noChangeEntityArr.push(text);
               }
           }
           // 绘制虚线
           const line = new McDbLine(lastPt, this.fixedPoint);
           if (isReverse) {
               line.move(lastPt, this.fixedPoint);
           }
           noChangeEntityArr.push(line);
           if (this.dottedPos && !this.suppleSymbol.filter(item => item.typeName === symbolName.ZSymbol).length) {
               const vec = McGeVector3d.kYAxis.clone().mult(this.size * (1 / 4))
               if (this.dottedPos === 2) vec.negate();
               const movePt = this.fixedPoint.clone().addvec(vec);
               const dottedLine = line.clone() as McDbLine;
               dottedLine.move(this.fixedPoint, movePt);
               const mxcad = MxCpp.getCurrentMxCAD();
               let dashline = this.dimSize / 8;
               let databse = mxcad.database;
               let lcale = mxcad.getSysVarDouble("LTSCALE");
               dashline = dashline / lcale;
               let lineTable = databse.getLinetypeTable();
               let lineTypeName = "weldingDottedLineType";
               let lineRecId = lineTable.get(lineTypeName);
               if (lineRecId.isNull()) {
                   let lineTypeRecord = new McDbLinetypeTableRecord();
                   lineTypeRecord.numDashes = 2;
                   lineTypeRecord.setDashLengthAt(0, dashline);
                   lineTypeRecord.setDashLengthAt(1, -dashline);
                   lineTypeRecord.name = lineTypeName;
                   lineRecId = lineTable.add(lineTypeRecord);
               }
               dottedLine.linetypeId = lineRecId;
               noChangeEntityArr.push(dottedLine);
           }
   
           // 是否反转
           if(isReverse){
               let endPt = this.fixedPoint.clone();
               if(this.suppleSymbol.filter(item=>item.typeName === symbolName.OnSiteSymbol).length){
                   endPt = this.fixedPoint.clone().addvec(McGeVector3d.kXAxis.clone().mult(this.size * (5 / 4)).negate())
               }
               allEntityArr.forEach(ent=>{
                   ent.move(lastPt, endPt);
               })
           }
   
           allEntityArr.push(...noChangeEntityArr);
   
           return allEntityArr;
       }
       // 绘制补充符号
       private drawSuppleSymbol(isReverse: boolean): { entityArr: McDbEntity[], noChangeArr: McDbEntity[], v: McGeVector3d } {
           const entityArr: McDbEntity[] = [];
           const noChangeArr: McDbEntity[] = [];
           let basePt = this.fixedPoint.clone().addvec(McGeVector3d.kXAxis.clone().mult(this.size * (1 / 8)));
           let lastPt = this.fixedPoint.clone();
           // 虚线间距
           let dottedPosVec = McGeVector3d.kYAxis.clone().mult(this.size * (1 / 4));
           if (this.suppleSymbol.filter(item => item.typeName === symbolName.SurroundingWeldSeamSymbol).length) {
               // 周围焊接符号
               const circle = new McDbCircle(this.fixedPoint.x, this.fixedPoint.y, 0, this.size * (3 / 8));
               // entityArr.push(circle);
               noChangeArr.push(circle);
   
               basePt.addvec(McGeVector3d.kXAxis.clone().mult(this.size * (3 / 8)));
               lastPt = new McGePoint3d(basePt.x, this.fixedPoint.y);
           }
           if (this.suppleSymbol.filter(item => item.typeName === symbolName.OnSiteSymbol).length) {
               // 现场符号
               const v = McGeVector3d.kYAxis.clone().mult(this.size * (3 / 8));
               const midPt = this.fixedPoint.clone().addvec(v);
   
               const topPt = midPt.clone().addvec(McGeVector3d.kYAxis.clone().mult(this.size * (7 / 8)));
               const vec = McGeVector3d.kXAxis.clone().mult(this.size * (5 / 4))
               const endPt = midPt.clone().addvec(vec);
               const line = new McDbLine(this.fixedPoint, midPt);
               let hatch = new McDbHatch();
               hatch.appendLoop(new McGePoint3dArray([midPt, topPt, endPt]));
               const { maxPt } = hatch.getBoundingBox();
               lastPt = new McGePoint3d(maxPt.x, this.fixedPoint.y);
               if(isReverse){
                   hatch = new McDbHatch();
                   const endPt = midPt.clone().addvec(vec.clone().negate());
                   hatch.appendLoop(new McGePoint3dArray([midPt, topPt, endPt]));
               }
               noChangeArr.push(hatch);
               noChangeArr.push(line);
   
               basePt.addvec(McGeVector3d.kXAxis.clone().mult(this.size * (7 / 8)));
           }
           if (this.suppleSymbol.filter(item => item.typeName === symbolName.ThreeSidedWeldSeamSymbol).length) {
               const item = this.suppleSymbol.filter(item => item.typeName === symbolName.ThreeSidedWeldSeamSymbol)[0];
               // 三面焊缝符号
               const point = this.dottedPos === 1 ? basePt.clone().addvec(dottedPosVec) : basePt.clone();
               const v = McGeVector3d.kYAxis.clone();
               const pt2 = point.clone().addvec(v.clone().mult(this.size * (1 / 8)));
               const pt1 = pt2.clone().addvec(McGeVector3d.kXAxis.clone().mult(this.size));
               const pt3 = pt2.clone().addvec(McGeVector3d.kYAxis.clone().mult(this.size * (3 / 4)));
               const pt4 = pt1.clone().addvec(McGeVector3d.kYAxis.clone().mult(this.size * (3 / 4)));
               const pl = new McDbPolyline();
               pl.addVertexAt(pt1);
               pl.addVertexAt(pt2);
               pl.addVertexAt(pt3);
               pl.addVertexAt(pt4);
               const { minPt, maxPt } = pl.getBoundingBox();
               let last_x: number = maxPt.x;
               const centerPt = minPt.clone().addvec(maxPt.sub(minPt).mult(1 / 2))
               // 根据旋转角度旋转
               if (item.rotation) {
                   pl.rotate(centerPt, item.rotation);
                   if (item.rotation != Math.PI) {
                       pl.move(point, pt2);
                   }
                   const { maxPt } = pl.getBoundingBox();
                   last_x = maxPt.x;
               }
               lastPt = new McGePoint3d(last_x, this.fixedPoint.y);
               entityArr.push(pl);
           }
           const v = lastPt.sub(this.fixedPoint);
           return { entityArr, noChangeArr, v }
       }
       // 绘制带垫板符号
       private drawWidth(lastPt: McGePoint3d): McDbEntity[] {
           const entityArr = [];
           // 虚线间距
           let dottedPosVec = McGeVector3d.kYAxis.clone().mult(this.size * (1 / 4));
           const v1 = McGeVector3d.kXAxis.clone().mult(this.size / 2);
           const v2 = McGeVector3d.kYAxis.clone().mult(this.size * (7 / 16)).negate();
           const pt = lastPt.clone().addvec(McGeVector3d.kXAxis.clone().mult(this.size));
           if (this.dottedLinePos === 2) pt.addvec(dottedPosVec.clone().negate());
           const pt1 = pt.clone().addvec(v1);
           const pt2 = pt1.clone().addvec(v2);
           const pt4 = pt.clone().addvec(v1.clone().negate());
           const pt3 = pt4.clone().addvec(v2);
           const ptArr = [pt1, pt2, pt3, pt4];
           const pl = new McDbPolyline();
           ptArr.forEach(pt => { pl.addVertexAt(pt) });
           entityArr.push(pl);
           return entityArr
       }
       // 绘制左标注
       private drawLeftDim(lastPt: McGePoint3d): { entityArr: McDbEntity[], v: McGeVector3d } {
           const entityArr = [];
           // 实体最大x
           let maxPt_x = null;
           // 字体实体间距
           const distanceVec = McGeVector3d.kXAxis.clone().mult(this.size * (1 / 16));
           // 虚线间距
           const dottedPosVec = McGeVector3d.kYAxis.clone().mult(this.size * (1 / 4));
           this.leftDim.forEach(item => {
               const text = new McDbMText();
               text.contents = item.content;
               text.textHeight = this.height;
               text.attachment = McDb.AttachmentPoint.kTopLeft;
               if (item.position === symbolPos.down) {
                   const pt = this.dottedPos === 2 && !this.suppleSymbol.filter(item => item.typeName === symbolName.ZSymbol).length ? lastPt.clone().addvec(dottedPosVec.negate()) : lastPt.clone()
                   text.location = pt.clone().addvec(distanceVec);
               } else if (item.position === symbolPos.top) {
                   const pt = this.dottedPos === 1 && this.suppleSymbol.filter(item => item.typeName === symbolName.ZSymbol).length ? lastPt.clone().addvec(dottedPosVec) : lastPt.clone()
                   text.location = pt.clone().addvec(McGeVector3d.kYAxis.clone().mult(this.size)).addvec(distanceVec);
               }
               // 将ent的文字样式设置为当前文字样式
               const textStyleId = MxCpp.getCurrentMxCAD().getDatabase().getCurrentlyTextStyleId();
               text.textStyleId = textStyleId;
               text.reCompute();
               const { maxPt } = MxCADUtility.getTextEntityBox(text, false);
               if (!maxPt_x || maxPt_x < maxPt.x) {
                   maxPt_x = maxPt.x;
               }
               // 是否做反向处理
               // if (isReverse) {
               //     text.attachment = McDb.AttachmentPoint.kTopRight;
               //     const pt = new McGePoint3d(this.fixedPoint.x, text.location.y, 0)
               //     const v = pt.sub(text.location);
               //     text.location = pt.clone().addvec(v);
               // };
               if (this.dottedLinePos && item.position === this.dottedLinePos) {
                   const v = this.dottedLinePos === 1 ? McGeVector3d.kYAxis.clone().mult(this.size / 4) : McGeVector3d.kYAxis.clone().mult(this.size / 4).negate();
                   text.location = text.location.addvec(v);
               }
               entityArr.push(text)
           })  
           const v = (new McGePoint3d(maxPt_x, lastPt.y)).sub(lastPt);
           return { entityArr, v }
       }
       // 绘制上标注
       private drawTopDim(lastPt: McGePoint3d): { entityArr: McDbEntity[] } {
           const entityArr = [];
   
           const basePt = lastPt.clone().addvec(McGeVector3d.kXAxis.clone().mult(this.size)).addvec(McGeVector3d.kYAxis.clone().mult(this.size * (7 / 4)));
           this.topDim.forEach(item => {
               const text = new McDbMText;
               text.contents = item.content;
               text.textHeight = this.height;
               text.attachment = McDb.AttachmentPoint.kBaseCenter;
               text.location = basePt.clone();
               text.location = basePt.clone().addvec(McGeVector3d.kYAxis.clone().mult(this.height));
               if (item.position === symbolPos.down) {
                   text.mirror(this.fixedPoint, lastPt);
                   text.attachment = McDb.AttachmentPoint.kBaseCenter;
                   text.location = text.location.clone().addvec(McGeVector3d.kYAxis.clone().mult(this.height));
               };
   
               // 是否有虚线
               if (this.dottedLinePos && item.position === this.dottedLinePos) {
                   const v = this.dottedLinePos === 1 ? McGeVector3d.kYAxis.clone().mult(this.size / 4) : McGeVector3d.kYAxis.clone().mult(this.size / 4).negate();
                   text.location = text.location.addvec(v);
               }
               entityArr.push(text)
           })
   
           return { entityArr }
       }
   
       // 绘制右标注
       private drawRightDim(lastPt: McGePoint3d): { entityArr: McDbEntity[], v: McGeVector3d } {
           const entityArr = [];
           const v = McGeVector3d.kXAxis.clone();
           let maxPt_x = null;
           this.rightDim.forEach(item => {
               const text = new McDbMText();
               text.contents = item.content;
               text.textHeight = this.height;
               text.attachment = McDb.AttachmentPoint.kTopLeft;
               if (item.position === symbolPos.top) {
                   text.location = lastPt.clone().addvec(McGeVector3d.kYAxis.clone().mult(this.height + this.size / 8));
               } else if (item.position === symbolPos.down) {
                   text.location = lastPt.clone();
               };
               const textStyleId = MxCpp.getCurrentMxCAD().getDatabase().getCurrentlyTextStyleId();
               text.textStyleId = textStyleId;
               text.reCompute();
               const { maxPt } = MxCADUtility.getTextEntityBox(text, false);
               if (!maxPt_x || maxPt_x < maxPt.x) {
                   maxPt_x = maxPt.x;
               }
               // 是否有虚线
               if (this.dottedLinePos && item.position === this.dottedLinePos) {
                   const v = this.dottedLinePos === 1 ? McGeVector3d.kYAxis.clone().mult(this.size / 4) : McGeVector3d.kYAxis.clone().mult(this.size / 4).negate();
                   text.location = text.location.addvec(v);
               }
               entityArr.push(text);
           })
           v.mult(Math.abs(maxPt_x - lastPt.x));
           return { entityArr, v }
       }
       // 绘制基本符号
       private drawBasicSymbol(lastPt: McGePoint3d): { entityArr: McDbEntity[] } {
           const entityArr = [];
           const allEntityArr = [];
           // 设置基本符号内的文字
           const setInfoText = (item: symbolType, pl: McDbPolyline): McGePoint3d[] => {
               let plPoints = [];
               const { maxPt: plMaxPt, minPt: plMinPt } = pl.getBoundingBox();
               const mindPt = plMinPt.clone().addvec(plMaxPt.sub(plMinPt).mult(1 / 2));
               const point = new McGePoint3d(mindPt.x, lastPt.y);
               const text = new McDbMText();
               text.contents = item.info;
               text.attachment = McDb.AttachmentPoint.kTopCenter;
               text.textHeight = this.size * (1 / 3);
               text.location = point.clone().addvec(McGeVector3d.kYAxis.clone().mult(this.size * (11 / 12)));
   
               // 将ent的文字样式设置为当前文字样式
               const textStyleId = MxCpp.getCurrentMxCAD().getDatabase().getCurrentlyTextStyleId();
               text.textStyleId = textStyleId;
               text.reCompute();
               const { minPt, maxPt } = MxCADUtility.getTextEntityBox(text, false);
   
               // 位置下
               if (item.position === symbolPos.down) {
                   const text_clone = text.clone() as McDbMText;
                   text_clone.mirror(this.fixedPoint, lastPt);
                   text.attachment = McDb.AttachmentPoint.kTopCenter;
                   text.location = text_clone.location.addvec(McGeVector3d.kYAxis.clone().mult(text.textHeight * (7 / 6)));
               }
               entityArr.push(text);
   
               const l = new McDbLine(minPt, new McGePoint3d(maxPt.x, minPt.y));
               const arr = l.IntersectWith(pl, McDb.Intersect.kOnBothOperands);
               if (arr.length() === 2) {
                   plPoints = [arr.at(0), pl.getPointAt(1).val, arr.at(1)]
               } else if (arr.length() === 1) {
                   plPoints = [pl.getPointAt(0).val, pl.getPointAt(1).val, arr.at(0)]
               }
   
               return plPoints
           }
           // 根据点数组设置多段线
           const getPl = (points: McGePoint3d[]): McDbPolyline => {
               const pl = new McDbPolyline();
               points.forEach(pt => {
                   pl.addVertexAt(pt)
               })
               return pl
           }
           // 设置文字基本符号
           const setBaseSymbolText = (content: string, item: symbolType) => {
               const pt1 = lastPt.clone().addvec(McGeVector3d.kXAxis.clone().mult(this.size * (3 / 8)));
               const pt2 = pt1.clone().addvec(McGeVector3d.kYAxis.clone().mult(this.size * (5 / 8)));
               const pt3 = pt2.clone().addvec(McGeVector3d.kXAxis.clone().mult(this.size * (5 / 4)));
               const pt4 = pt1.clone().addvec(McGeVector3d.kXAxis.clone().mult(this.size * (5 / 4)));
               const pl = getPl([pt1, pt2, pt3, pt4]);
               const { minPt, maxPt } = pl.getBoundingBox();
               const midPt = minPt.clone().addvec(maxPt.sub(minPt).mult(1 / 2));
   
               const text = new McDbMText();
               text.contents = content;
               text.attachment = McDb.AttachmentPoint.kBaseFit;
               text.textHeight = this.size * (5 / 8);
               text.location = midPt;
               // 根据中心点和location 之间的距离大小去将text文本设置到矩形框中心位置
               // 将ent的文字样式设置为当前文字样式
               text.reCompute();
               const textStyleId = MxCpp.getCurrentMxCAD().getDatabase().getCurrentlyTextStyleId();
               text.textStyleId = textStyleId;
               text.reCompute();
               const { minPt: textMinPt, maxPt: textMaxPt } = MxCADUtility.getTextEntityBox(text, false);
               const textMidPt = textMinPt.clone().addvec(textMaxPt.sub(textMinPt).mult(0.5));
               const vec = midPt.sub(textMidPt);
               text.location = midPt.clone().addvec(vec).addvec(McGeVector3d.kXAxis.clone().mult(this.size / 15)).addvec(McGeVector3d.kYAxis.clone().mult(this.size / 17));
   
               if (item.position === symbolPos.down) {
                   const text_clone = text.clone() as McDbMText;
                   text_clone.mirror(this.fixedPoint, lastPt);
                   text.location = text_clone.location.addvec(McGeVector3d.kYAxis.clone().mult(text.textHeight * (7 / 6)));
               };
   
               entityArr.push(pl, text);
           }
           this.baseSymbok.forEach(item => {
               entityArr.length = 0;
   
               if (item.typeName === symbolName.RolledEdge) {
                   // 卷边焊缝
                   const radius = this.size * (3 / 4);
                   const arc = new McDbArc();
                   arc.center = lastPt.clone().addvec(McGeVector3d.kYAxis.clone().mult(radius)).addvec(McGeVector3d.kXAxis.clone().mult(this.size / 8));
                   arc.radius = radius;
                   arc.startAngle = Math.PI * (3 / 2);
                   arc.endAngle = 0;
   
                   const endPt = arc.center.clone().addvec(McGeVector3d.kXAxis.clone().mult(radius));
                   const topPt = endPt.clone().addvec(McGeVector3d.kYAxis.clone().mult(this.size * (1 / 8)));
                   const line = new McDbLine(topPt, endPt);
   
                   const originPt = endPt.clone().addvec(McGeVector3d.kXAxis.clone().mult(this.size * (1 / 8)));
                   const _arc = arc.clone() as McDbEntity;
                   _arc.mirror(originPt, new McGePoint3d(originPt.x, lastPt.y));
                   const _line = line.clone() as McDbEntity;
                   _line.mirror(originPt, new McGePoint3d(originPt.x, lastPt.y));
                   entityArr.push(...[arc, _arc, _line, line]);
   
               } else if (item.typeName === symbolName.IWeldSeam) {
                   // I型焊缝
                   const pt = lastPt.clone().addvec(McGeVector3d.kXAxis.clone().mult(this.size * (11 / 16)));
                   const endPt = pt.clone().addvec(McGeVector3d.kYAxis.clone().mult(this.height));
                   const line = new McDbLine(pt, endPt);
                   const _line = line.clone() as McDbLine;
                   const toPt = pt.clone().addvec(McGeVector3d.kXAxis.clone().mult(this.size * (5 / 8)));
                   _line.move(pt, toPt);
                   entityArr.push(...[line, _line]);
               } else if (item.typeName === symbolName.VWeldSeam) {
                   // V型焊缝
                   const pt = lastPt.clone().addvec(McGeVector3d.kXAxis.clone().mult(this.size * (1 / 2)));
                   const pt1 = pt.clone().addvec(McGeVector3d.kYAxis.clone().mult(this.size * (7 / 8)));
                   const pt2 = pt.clone().addvec(McGeVector3d.kXAxis.clone().mult(this.size * (1 / 2)));
                   const pt3 = pt1.clone().addvec(McGeVector3d.kXAxis.clone().mult(this.size));
                   let plPoints = [pt1, pt2, pt3];
                   const pl = getPl(plPoints);
   
                   if (item.info) {
                       const pointArr = setInfoText(item, pl);
                       if (pointArr.length) {
                           plPoints = pointArr;
                       }
                   }
                   const polyline = getPl(plPoints);
                   entityArr.push(polyline);
   
               } else if (item.typeName === symbolName.SingleVWeldSeam) {
                   // 单边V型焊缝
                   const pt2 = lastPt.clone().addvec(McGeVector3d.kXAxis.clone().mult(this.size * (5 / 8)));
                   const pt1 = pt2.clone().addvec(McGeVector3d.kYAxis.clone().mult(this.height));
                   const pt3 = pt1.clone().addvec(McGeVector3d.kXAxis.clone().mult(this.height));
                   let plPoints = [pt1, pt2, pt3];
                   const pl = getPl(plPoints);
                   if (item.info) {
                       const pointArr = setInfoText(item, pl);
                       if (pointArr.length) {
                           plPoints = pointArr;
                       }
                   }
                   const polyline = getPl(plPoints);
                   entityArr.push(polyline);
               } else if (item.typeName === symbolName.SingleSteepVWeldSeam) {
                   // 单边陡侧V型坡口堆焊
                   const pt2 = lastPt.clone().addvec(McGeVector3d.kXAxis.clone().mult(this.size * (5 / 8)));
                   pt2.addvec(McGeVector3d.kYAxis.clone().mult(this.size * (1 / 8)));
                   const pt1 = pt2.clone().addvec(McGeVector3d.kYAxis.clone().mult(this.height));
                   const pt3 = pt2.clone().addvec(McGeVector3d.kXAxis.clone().mult(this.size / 2));
   
                   const linePt1 = pt3.clone().addvec(pt2.sub(pt3).mult(1 / 4));
                   const linePt2 = linePt1.clone().addvec(McGeVector3d.kYAxis.clone().mult(this.height)).addvec(McGeVector3d.kXAxis.clone().mult(this.size * (5 / 16)));
                   const line = new McDbLine(linePt1, linePt2);
   
                   let plPoints = [pt1, pt2, pt3];
   
                   const polyline = getPl(plPoints);
                   entityArr.push(polyline, line);
               } else if (item.typeName === symbolName.InclinedWeldSeam) {
                   // 倾斜焊缝
                   const pt1 = lastPt.clone().addvec(McGeVector3d.kXAxis.clone().mult(this.size / 2));
                   const _pt1 = pt1.clone().addvec(McGeVector3d.kYAxis.clone().mult(this.height / 2));
                   const pt2 = _pt1.clone().addvec(McGeVector3d.kXAxis.clone().mult(this.size * (5 / 8)));
                   const _pt2 = pt2.clone().addvec(McGeVector3d.kYAxis.clone().mult(this.height / 2));
   
                   const _line = new McDbLine(_pt1, _pt2);
                   const line = new McDbLine(pt1, pt2);
                   entityArr.push(_line, line);
               } else if (item.typeName === symbolName.RemovablePadding1) {
                   // 可移除衬垫-MR
                   setBaseSymbolText('MR', item);
               } else if (item.typeName === symbolName.RemovablePadding2) {
                   // 可移除衬垫-MR
                   setBaseSymbolText('R', item);
               } else if (item.typeName === symbolName.DurableLiner) {
                   // 持久衬垫-M
                   setBaseSymbolText('M', item);
               } else if (item.typeName === symbolName.VFlangedEdgeSeam) {
                   // 带顿边V型焊缝
                   const pt1 = lastPt.clone().addvec(McGeVector3d.kXAxis.clone().mult(this.size));
                   const pt2 = pt1.clone().addvec(McGeVector3d.kYAxis.clone().mult(this.size * (3 / 8)));
                   const pt3 = pt2.clone().addvec(McGeVector3d.kXAxis.clone().mult(this.size * (5 / 16))).addvec(McGeVector3d.kYAxis.clone().mult(this.size / 2));
                   const pt4 = pt3.clone().addvec(McGeVector3d.kXAxis.clone().negate().mult(this.size * (5 / 8)));
                   let plPoints = [pt3, pt2, pt4];
   
                   const line = new McDbLine(pt1, pt2);
                   const pl = getPl(plPoints);
                   if (item.info) {
                       const pointArr = setInfoText(item, pl);
                       if (pointArr.length) {
                           plPoints = pointArr;
                       }
                   }
                   const polyline = getPl(plPoints);
                   entityArr.push(polyline, line);
               } else if (item.typeName === symbolName.SingleVFlangedEdgeSeam) {
                   // 带单边顿边V型焊缝
                   const pt1 = lastPt.clone().addvec(McGeVector3d.kXAxis.clone().mult(this.size * (11 / 16)));
                   const pt2 = pt1.clone().addvec(McGeVector3d.kYAxis.clone().mult(this.size * (3 / 8)));
                   const pt3 = pt2.clone().addvec(McGeVector3d.kYAxis.clone().mult(this.size / 2));
                   const pt4 = pt3.clone().addvec(McGeVector3d.kXAxis.clone().mult(this.size * (5 / 8)));
                   let plPoints = [pt3, pt2, pt4];
   
                   const line = new McDbLine(pt1, pt2);
                   const pl = getPl(plPoints);
                   if (item.info) {
                       const pointArr = setInfoText(item, pl);
                       if (pointArr.length) {
                           plPoints = pointArr;
                       }
                   }
                   const polyline = getPl(plPoints);
                   entityArr.push(polyline, line);
               } else if (item.typeName === symbolName.UFlangedEdgeSeam) {
                   // 带顿边U型焊缝
                   const pt1 = lastPt.clone().addvec(McGeVector3d.kXAxis.clone().mult(this.size));
                   const pt2 = pt1.clone().addvec(McGeVector3d.kYAxis.clone().mult(this.size * (1 / 4)));
                   const line = new McDbLine(pt1, pt2);
   
                   const radius = this.size * (3 / 8);
                   const arc = new McDbArc();
                   arc.center = pt2.clone().addvec(McGeVector3d.kYAxis.clone().mult(radius));
                   arc.radius = radius;
                   arc.startAngle = Math.PI;
                   arc.endAngle = Math.PI * 2;
   
                   const startPt = arc.getStartPoint().val;
                   const point1 = startPt.clone().addvec(McGeVector3d.kYAxis.clone().mult(this.size / 4));
   
                   const endPt = arc.getEndPoint().val;
                   const point2 = endPt.clone().addvec(McGeVector3d.kYAxis.clone().mult(this.size / 4));
   
                   let plPoints = [point1, pt1, point2];
                   const pl = getPl(plPoints);
                   if (item.info) {
                       const pointArr = setInfoText(item, pl);
                       if (pointArr.length) {
                           plPoints = pointArr;
                       }
                       const line = new McDbLine(plPoints[0], plPoints[2]);
                       const pts = line.IntersectWith(arc, McDb.Intersect.kOnBothOperands);
                       if (pts.length()) {
                           const _arc = new McDbArc()
                           _arc.computeArc(pts.at(0).x, pts.at(0).y, pt2.x, pt2.y, pts.at(1).x, pts.at(1).y);
                           entityArr.push(_arc);
                       }else{
                           entityArr.push(arc);
                       }
                   } else {
                       const line1 = new McDbLine(startPt, point1);
                       const line2 = new McDbLine(endPt, point2);
                       entityArr.push(line1, line2, arc);
                   }
                   entityArr.push(line);
               } else if (item.typeName === symbolName.SingleUFlangedEdgeSeam) {
                   // 单边带顿边U型焊缝
                   const radius = this.size * (3 / 8);
                   const pt1 = lastPt.clone().addvec(McGeVector3d.kXAxis.clone().mult(this.size - radius / 2));
                   const pt2 = pt1.clone().addvec(McGeVector3d.kYAxis.clone().mult(this.height));
                   const line = new McDbLine(pt1, pt2);
   
                   const pt3 = pt1.clone().addvec(McGeVector3d.kYAxis.clone().mult(this.size * (1 / 4)));
                   const arc = new McDbArc();
                   arc.center = pt3.clone().addvec(McGeVector3d.kYAxis.clone().mult(radius));
                   arc.radius = radius;
                   arc.startAngle = Math.PI * (3 / 2);
                   arc.endAngle = Math.PI * 2;
                   const endPt = arc.getEndPoint().val;
                   const point1 = endPt.clone().addvec(McGeVector3d.kYAxis.clone().mult(this.size / 4));
                   const line2 = new McDbLine(endPt, point1);
                   entityArr.push(line2, arc, line);
               } else if (item.typeName === symbolName.EndWeldSeam) {
                   // 端接焊缝
                   const pt1 = lastPt.clone().addvec(McGeVector3d.kXAxis.clone().mult(this.size));
                   const pt2 = pt1.clone().addvec(McGeVector3d.kYAxis.clone().mult(this.height));
                   const line = new McDbLine(pt1, pt2);
                   const v = McGeVector3d.kXAxis.clone().mult(this.size * (3 / 16));
                   const line1 = line.clone() as McDbLine;
                   line1.move(pt1, pt1.clone().addvec(v));
                   const line2 = line.clone() as McDbLine;
                   line2.move(pt1, pt1.clone().addvec(v.clone().negate()));
                   entityArr.push(line, line1, line2)
               } else if (item.typeName === symbolName.SurfacingJoint) {
                   // 堆焊接头
                   const basePt = lastPt.clone().addvec(McGeVector3d.kXAxis.clone().mult(this.size)).addvec(McGeVector3d.kYAxis.clone().mult(this.size * (11 / 16)));
                   const v = McGeVector3d.kXAxis.clone().mult(this.size * (3 / 8))
                   const pt1 = basePt.clone().addvec(v);
                   const pt2 = basePt.clone().addvec(v.clone().negate());
                   const line = new McDbLine(pt1, pt2);
                   const line1 = line.clone() as McDbLine;
                   line1.mirror(this.fixedPoint, lastPt);
                   entityArr.push(line, line1)
               } else if (item.typeName === symbolName.BottomSeamWeld) {
                   // 封底焊缝
                   const basePt = lastPt.clone().addvec(McGeVector3d.kXAxis.clone().mult(this.size));
                   const center = basePt.clone().addvec(McGeVector3d.kYAxis.clone().mult(this.size * (1 / 4)));
                   const circle = new McDbCircle();
                   circle.center = center;
                   circle.radius = this.size * (11 / 16);
                   const line = new McDbLine(this.fixedPoint, lastPt);
                   const ptsArr = circle.IntersectWith(line, McDb.Intersect.kExtendBoth);
                   if (ptsArr.length() === 2) {
                       const arc = new McDbArc();
                       const midPt = center.clone().addvec(McGeVector3d.kYAxis.clone().negate().mult(circle.radius));
                       const pt1 = ptsArr.at(0);
                       const pt2 = ptsArr.at(1);
                       arc.computeArc(pt1.x, pt1.y, midPt.x, midPt.y, pt2.x, pt2.y);
                       arc.mirror(this.fixedPoint, lastPt);
                       const line = new McDbLine(pt1, pt2);
                       entityArr.push(arc, line);
                   }
               } else if (item.typeName === symbolName.FilletWeld) {
                   // 角焊缝
                   const basePt = lastPt.clone().addvec(McGeVector3d.kXAxis.clone().mult(this.size));
                   const v = McGeVector3d.kXAxis.clone().mult(this.height / 2);
                   const pt1 = basePt.clone().addvec(v);
                   const pt2 = basePt.clone().addvec(v.clone().negate());
                   const pt3 = pt2.clone().addvec(McGeVector3d.kYAxis.clone().mult(this.height));
                   const pl = getPl([pt1, pt2, pt3]);
                   pl.isClosed = true;
                   entityArr.push(pl)
               } else if (item.typeName === symbolName.GrooveWeldSeam) {
                   // 槽焊缝
                   const basePt = lastPt.clone().addvec(McGeVector3d.kXAxis.clone().mult(this.size));
                   const v = McGeVector3d.kXAxis.clone().mult(this.size / 2);
                   const _v = McGeVector3d.kYAxis.clone().mult(this.size * (5 / 8));
                   const pt1 = basePt.clone().addvec(v);
                   const pt2 = pt1.clone().addvec(_v);
                   const pt3 = pt2.clone().addvec(v.negate().mult(2));
                   const pt4 = pt3.clone().addvec(_v.clone().negate());
                   const pl = getPl([pt1, pt2, pt3, pt4]);
                   entityArr.push(pl);
               } else if (item.typeName === symbolName.SpotWeldSeam) {
                   // 点焊缝
                   const circle = new McDbCircle();
                   circle.center = lastPt.clone().addvec(McGeVector3d.kXAxis.clone().mult(this.size));
                   circle.radius = this.size * (3 / 8);
                   entityArr.push(circle)
               } else if (item.typeName === symbolName.FoldingInterface) {
                   // 折叠接口
                   const basePt = lastPt.clone().addvec(McGeVector3d.kXAxis.clone().mult(this.size));
                   const lineMidPt = basePt.clone().addvec(McGeVector3d.kYAxis.clone().mult(this.height * (2 / 3)));
                   const v = McGeVector3d.kXAxis.clone().mult(this.size * (1 / 8))
                   const pt1 = lineMidPt.clone().addvec(v);
                   const pt2 = lineMidPt.clone().addvec(v.clone().negate());
                   const line1 = new McDbLine(pt1, pt2);
                   const vec = McGeVector3d.kYAxis.clone().mult(this.height / 3);
                   const line2 = line1.clone() as McDbLine;
                   line2.move(pt1, pt1.clone().addvec(vec));
                   line2.endPoint = line2.endPoint.clone().addvec(McGeVector3d.kXAxis.clone().mult(this.height / 3).negate())
                   const line3 = line1.clone() as McDbLine;
                   line3.move(pt1, pt1.clone().addvec(vec.clone().negate()));
                   const arc1 = new McDbArc();
                   arc1.center = pt2.clone().addvec(vec.clone().negate());
                   arc1.radius = this.height / 3;
                   arc1.startAngle = Math.PI / 2;
                   arc1.endAngle = Math.PI * (3 / 2);
                   const arc2 = new McDbArc();
                   arc2.center = pt1;
                   arc2.radius = this.height / 3;
                   arc2.startAngle = Math.PI * (3 / 2);
                   arc2.endAngle = Math.PI / 2;
                   entityArr.push(line1, line2, line3, arc1, arc2)
               } else if (item.typeName === symbolName.InclinedInterface) {
                   // 倾斜接口
                   const pt1 = lastPt.clone().addvec(McGeVector3d.kXAxis.clone().mult(this.size - this.height / 2));
                   const pt2 = pt1.clone().addvec(McGeVector3d.kYAxis.clone().mult(this.height)).addvec(McGeVector3d.kXAxis.clone().mult(this.height / 2));
                   const line = new McDbLine(pt1, pt2);
                   const _line = line.clone() as McDbLine;
                   _line.move(pt1, pt1.clone().addvec(McGeVector3d.kXAxis.clone().mult(this.height / 2)));
                   entityArr.push(line, _line)
               } else if (item.typeName === symbolName.SpotWeldSeamOffset) {
                   // 点焊缝(偏移中心)
                   const circle = new McDbCircle();
                   circle.center = lastPt.clone().addvec(McGeVector3d.kXAxis.clone().mult(this.size));
                   circle.radius = this.height / 2;
                   entityArr.push(circle);
               } else if (item.typeName === symbolName.SeamWeld) {
                   // 缝焊缝
                   const basePt = lastPt.clone().addvec(McGeVector3d.kXAxis.clone().mult(this.size));
                   const circle = new McDbCircle();
                   circle.center = basePt;
                   circle.radius = this.size * (11 / 16);
                   const v = McGeVector3d.kXAxis.clone().mult(this.height);
                   const pt1 = basePt.clone().addvec(v);
                   const pt2 = basePt.clone().addvec(v.clone().negate());
                   const line = new McDbLine(pt1, pt2);
                   const vec = McGeVector3d.kYAxis.clone().mult(this.height / 2);
                   const line1 = line.clone() as McDbLine;
                   line1.move(basePt, basePt.clone().addvec(vec));
                   const line2 = line.clone() as McDbLine;
                   line2.move(basePt, basePt.clone().addvec(vec.clone().negate()));
                   entityArr.push(circle, line1, line2);
               } else if (item.typeName === symbolName.SeamWeldOffset) {
                   // 缝焊缝(偏离中心)
                   const basePt = lastPt.clone().addvec(McGeVector3d.kXAxis.clone().mult(this.size));
                   // 2.275
                   const v = McGeVector3d.kYAxis.clone().mult(this.size * (9 / 16));
                   const circle = new McDbCircle();
                   circle.center = basePt.clone().addvec(v);
                   circle.radius = this.size * (9 / 16);
                   const vec = McGeVector3d.kXAxis.clone().mult(this.height)
                   const pt1 = basePt.clone().addvec(vec);
                   const pt2 = basePt.clone().addvec(vec.clone().negate());
                   const line = new McDbLine(pt1, pt2);
                   const movePt1 = circle.center.clone().addvec(McGeVector3d.kYAxis.clone().mult(this.size * (3 / 16)));
                   const movePt2 = circle.center.clone().addvec(McGeVector3d.kYAxis.clone().mult(this.size / 4).negate());
                   const line1 = line.clone() as McDbLine;
                   line1.move(basePt, movePt1);
                   const line2 = line.clone() as McDbLine;
                   line2.move(basePt, movePt2);
                   entityArr.push(circle, line1, line2)
               } else if (item.typeName === symbolName.SteepVWeldSeam) {
                   // 陡侧V型坡口堆焊
                   const _lastPt = lastPt.clone().addvec(McGeVector3d.kXAxis.clone().mult(this.size));
                   const basePt = _lastPt.addvec(McGeVector3d.kYAxis.clone().mult(this.size / 4));
                   const v = McGeVector3d.kXAxis.clone().mult(this.size * (5 / 16))
                   const pt1 = basePt.clone().addvec(v);
                   const pt2 = basePt.clone().addvec(v.clone().negate());
                   const line = new McDbLine(pt1, pt2);
                   const point1 = basePt.clone().addvec(McGeVector3d.kXAxis.clone().mult(this.size / 8));
                   const point2 = point1.clone().addvec(v).addvec(McGeVector3d.kYAxis.clone().mult(this.height));
                   const line1 = new McDbLine(point1, point2);
                   const line2 = line1.clone() as McDbLine;
                   line2.mirror(_lastPt, basePt.clone().addvec(McGeVector3d.kYAxis.clone().mult(this.size)));
                   entityArr.push(line, line1, line2);
               }
               if (item.position === symbolPos.down) {
                   entityArr.forEach(ent => {
                       if (!(ent instanceof McDbMText)) {
                           (ent as McDbEntity).mirror(this.fixedPoint, lastPt);
                       }
                   })
               };
               if (this.dottedLinePos && item.position === this.dottedLinePos) {
                   const v = this.dottedLinePos === 1 ? McGeVector3d.kYAxis.clone().mult(this.size / 4) : McGeVector3d.kYAxis.clone().mult(this.size / 4).negate();
                   entityArr.forEach(ent => {
                       ent.move(lastPt, lastPt.clone().addvec(v))
                   })
               }
               allEntityArr.push(...entityArr);
           });
           return { entityArr: allEntityArr }
       }
       // 绘制特殊符号
       private drawSpecialSymbol(lastPt: McGePoint3d): { entityArr: McDbEntity[] } {
           const entityArr: McDbEntity[] = [];
           const allEntityArr = [];
           this.specialSymbol.forEach(item => {
               entityArr.length = 0;
               if (item.typeName === symbolName.BellShapedWeldSeam) {
                   // 喇叭形焊缝
                   const basePt = lastPt.clone().addvec(McGeVector3d.kXAxis.clone().mult(this.size * (5 / 4)));
                   const v = McGeVector3d.kXAxis.clone().mult(this.size / 8);
                   const pt1 = basePt.clone().addvec(v);
                   const pt2 = pt1.clone().addvec(McGeVector3d.kYAxis.clone().mult(this.size / 8));
                   const line = new McDbLine(pt1, pt2);
                   const arc = new McDbArc();
                   const r = this.size * (3 / 4);
                   arc.center = pt2.clone().addvec(McGeVector3d.kXAxis.clone().mult(r));
                   arc.radius = r;
                   arc.startAngle = Math.PI / 2;
                   arc.endAngle = Math.PI;
                   const _pt = basePt.clone().addvec(McGeVector3d.kYAxis.clone().mult(this.size));
                   const _line = line.clone() as McDbLine;
                   _line.mirror(basePt, _pt);
                   const _arc = arc.clone() as McDbArc;
                   _arc.mirror(basePt, _pt);
                   entityArr.push(line, arc, _line, _arc)
               } else if (item.typeName === symbolName.SingleBellShapedWeldSeam) {
                   // 单边喇叭形焊缝
                   const pt1 = lastPt.clone().addvec(McGeVector3d.kXAxis.clone().mult(this.size * (3 / 8)));
                   const pt2 = pt1.clone().addvec(McGeVector3d.kYAxis.clone().mult(this.height));
                   const line = new McDbLine(pt1, pt2)
                   const point1 = pt1.clone().addvec(McGeVector3d.kXAxis.clone().mult(this.size / 4));
                   const point2 = point1.clone().addvec(McGeVector3d.kYAxis.clone().mult(this.size / 8));
                   const line1 = new McDbLine(point1, point2);
                   const arc = new McDbArc();
                   arc.center = point2.clone().addvec(McGeVector3d.kXAxis.clone().mult(this.size * (3 / 4)));
                   arc.radius = this.size * (3 / 4);
                   arc.startAngle = Math.PI / 2;
                   arc.endAngle = Math.PI;
                   entityArr.push(line, line1, arc)
               } else if (item.typeName === symbolName.HeapWeldSeam) {
                   // 堆焊缝
                   const basePt = lastPt.clone().addvec(McGeVector3d.kXAxis.clone().mult(this.size * (3 / 8)));
                   const radius = this.size * (9 / 16);
                   const arc = new McDbArc();
                   arc.center = basePt.clone().addvec(McGeVector3d.kXAxis.clone().mult(radius));
                   arc.radius = radius;
                   arc.startAngle = 0;
                   arc.endAngle = Math.PI;
                   const _arc = arc.clone() as McDbArc;
                   const midPt = basePt.clone().addvec(McGeVector3d.kXAxis.clone().mult(radius * 2))
                   _arc.mirror(midPt, midPt.clone().addvec(McGeVector3d.kYAxis.clone().mult(this.size)));
                   entityArr.push(arc, _arc);
               } else if (item.typeName === symbolName.SeamSeam) {
                   // 锁边焊缝
                   const basePt = lastPt.clone().addvec(McGeVector3d.kXAxis.clone().mult(this.size * (3 / 8)));
                   const _v = McGeVector3d.kYAxis.clone().mult(this.size / 4).negate();
                   const pt2 = basePt.clone().addvec(_v);
                   const v = McGeVector3d.kXAxis.clone().mult(this.height);
                   const pt3 = pt2.clone().addvec(v);
                   const pt4 = pt3.clone().addvec(_v.clone().mult(7 / 4));
                   const pt5 = pt4.clone().addvec(v.clone().negate());
                   const pl = new McDbPolyline();
                   const pts = [basePt, pt2, pt3, pt4, pt5];
                   pts.forEach(pt => pl.addVertexAt(pt));
                   const point1 = basePt.clone().addvec(McGeVector3d.kYAxis.clone().mult(this.height));
                   const point3 = point1.clone().addvec(v);
                   const _pl = new McDbPolyline();
                   const _pts = [point1, basePt, point3];
                   _pts.forEach(pt => _pl.addVertexAt(pt));
                   entityArr.push(pl, _pl)
               } else if (item.typeName === symbolName.LockTheSlopeOpening) {
                   // 锁边坡口
                   const basePt = lastPt.clone().addvec(McGeVector3d.kXAxis.clone().mult(this.size)).addvec(McGeVector3d.kYAxis.clone().negate().mult(this.size / 4));
                   const vec = McGeVector3d.kXAxis.clone().mult(this.size * (7 / 16))
                   const pt2 = basePt.clone().addvec(vec);
                   const pt3 = pt2.clone().addvec(McGeVector3d.kYAxis.clone().mult(this.size * (7 / 16)).negate());
                   const pt4 = pt3.clone().addvec(vec.clone().negate());
                   const pl = new McDbPolyline();
                   const pts = [basePt, pt2, pt3, pt4];
                   pts.forEach(pt => pl.addVertexAt(pt));
                   const point = basePt.clone().addvec(McGeVector3d.kYAxis.clone().mult(this.size * (5 / 8)));
                   const v = McGeVector3d.kXAxis.clone().mult(this.size * (9 / 16));
                   const point1 = point.clone().addvec(v);
                   const point2 = point.clone().addvec(v.clone().negate());
                   const _pl = new McDbPolyline();
                   const _pts = [point1, basePt, point2];
                   _pts.forEach(pt => _pl.addVertexAt(pt));
                   entityArr.push(pl, _pl)
               }
               if (item.position === symbolPos.down) {
                   entityArr.forEach(ent => {
                       (ent as McDbEntity).mirror(this.fixedPoint, this.fixedPoint.clone().addvec(McGeVector3d.kXAxis.clone().mult(this.size * 10)));
                   })
               };
               if (this.dottedLinePos && item.position === this.dottedLinePos) {
                   const v = this.dottedLinePos === 1 ? McGeVector3d.kYAxis.clone().mult(this.size / 4) : McGeVector3d.kYAxis.clone().mult(this.size / 4).negate();
                   entityArr.forEach(ent => {
                       ent.move(lastPt, lastPt.clone().addvec(v))
                   })
               }
               allEntityArr.push(...entityArr);
           })
           return { entityArr: allEntityArr }
       }
       // 绘制辅助符号
       private drawAuxiliarySymbol(lastPt: McGePoint3d): { entityArr: McDbEntity[] } {
           const entityArr = [];
           const allEntityArr = [];
           this.auxiliarySymbol.forEach(item => {
               entityArr.length = 0;
               const basePt = lastPt.clone().addvec(McGeVector3d.kXAxis.clone().mult(this.size)).addvec(McGeVector3d.kYAxis.clone().mult(this.size * (21 / 16)));
               const v = McGeVector3d.kXAxis.clone().mult(this.size * (5 / 8));
               const pt1 = basePt.clone().addvec(v);
               const pt2 = basePt.clone().addvec(v.clone().negate());
               if (item.typeName === symbolName.FirstPlaneSymbol) {
                   // 第一种平面符号
                   const line = new McDbLine(pt1, pt2);
                   entityArr.push(line)
               } else if (item.typeName === symbolName.SecondConcaveSymbol) {
                   // 第二种凹面符号
                   const midPt = basePt.clone().addvec(McGeVector3d.kYAxis.clone().negate().mult(this.size * (3 / 8)));
                   const arc = new McDbArc();
                   arc.computeArc(pt1.x, pt1.y, midPt.x, midPt.y, pt2.x, pt2.y);
                      entityArr.push(arc)
               } else if (item.typeName === symbolName.SecondConvexeSymbol) {
                   // 第二种凸面符号
                   const midPt = basePt.clone().addvec(McGeVector3d.kYAxis.clone().mult(this.size * (3 / 8)));
                   const arc = new McDbArc();
                   arc.computeArc(pt1.x, pt1.y, midPt.x, midPt.y, pt2.x, pt2.y);
                   entityArr.push(arc)
               } else if (item.typeName === symbolName.SecondPlaneSymbol) {
                   // 第二种平面符号
                   const basePt = lastPt.clone().addvec(McGeVector3d.kXAxis.clone().mult(this.size)).addvec(McGeVector3d.kYAxis.clone().mult(this.size / 4));
                   const pt1 = basePt.clone().addvec(McGeVector3d.kXAxis.clone().mult(this.size * (11 / 16)));
                   const pt2 = basePt.clone().addvec(McGeVector3d.kXAxis.clone().negate().mult(this.size / 4)).addvec(McGeVector3d.kYAxis.clone().mult(this.size * (15 / 16)));
                   const line = new McDbLine(pt1, pt2);
                    entityArr.push(line)
               } else if (item.typeName === symbolName.ThirdPlaneSymbol) {
                   // 第三种平面符号
                   const basePt = lastPt.clone().addvec(McGeVector3d.kXAxis.clone().mult(this.size)).addvec(McGeVector3d.kYAxis.clone().mult(this.size / 4));
                   const pt1 = basePt.clone().addvec(McGeVector3d.kXAxis.clone().negate().mult(this.size * (11 / 16)));
                   const pt2 = basePt.clone().addvec(McGeVector3d.kXAxis.clone().mult(this.size / 4)).addvec(McGeVector3d.kYAxis.clone().mult(this.size * (15 / 16)));
                   const line = new McDbLine(pt1, pt2);
                   entityArr.push(line)
               } else if (item.typeName === symbolName.FirstConvexeSymbol) {
                   // 第一种凸面符号
                   const basePt = lastPt.clone().addvec(McGeVector3d.kXAxis.clone().mult(this.size)).addvec(McGeVector3d.kYAxis.clone().mult(this.size * (5 / 8)));
                   const arc = new McDbArc();
                   arc.center = basePt;
                   arc.startAngle = 0;
                   arc.endAngle = Math.PI / 2;
                   arc.radius = this.size * (5 / 8);
                   entityArr.push(arc)
               } else if (item.typeName === symbolName.FirstConcaveSymbol) {
                   // 第一种凹面符号
                   const basePt = lastPt.clone().addvec(McGeVector3d.kXAxis.clone().mult(this.size)).addvec(McGeVector3d.kYAxis.clone().mult(this.size * (5 / 8)));
                   const radius = this.size * (5 / 8);
                   const center = basePt.clone().addvec(McGeVector3d.kXAxis.clone().mult(radius)).addvec(McGeVector3d.kYAxis.clone().mult(radius));
                   const arc = new McDbArc();
                   arc.center = center;
                   arc.radius = radius;
                   arc.startAngle = Math.PI;
                   arc.endAngle = Math.PI * (3 / 2);
                   entityArr.push(arc)
               } else if (item.typeName === symbolName.FlattenWeldSeam) {
                   // 削平符号
                   const basePt = lastPt.clone().addvec(McGeVector3d.kXAxis.clone().mult(this.size)).addvec(McGeVector3d.kYAxis.clone().mult(this.size));
                   const v = McGeVector3d.kXAxis.clone().mult(this.size * (7 / 16));
                   const pt1 = basePt.clone().addvec(v);
                   const pt2 = basePt.clone().addvec(v.clone().negate());
                   const line = new McDbLine(pt1, pt2);
                   const point3 = basePt.clone().addvec(McGeVector3d.kYAxis.clone().mult(this.size / 8));
                   const vec = McGeVector3d.kXAxis.clone().mult(this.size * (7 / 32));
                   const point2 = point3.clone().addvec(McGeVector3d.kYAxis.clone().mult(this.size * (7 / 16))).addvec(vec.clone().negate());
                   const point1 = point2.clone().addvec(vec.clone().mult(2));
                   const _vec = point1.sub(point3);
                   const point4 = point1.clone().addvec(_vec);
                   const ptsArr = [point1, point2, point3, point4];
                   const pl = new McDbPolyline();
                   ptsArr.forEach(pt => pl.addVertexAt(pt));
                   entityArr.push(pl, line)
               } else if (item.typeName === symbolName.ToeAareaTranSmoothly) {
                   // 趾部平滑过渡
                   const basePt = lastPt.clone().addvec(McGeVector3d.kXAxis.clone().mult(this.size));
                   const pt1 = basePt.clone().addvec(McGeVector3d.kYAxis.clone().mult(this.size * (5 / 4)));
                   const pt2 = pt1.clone().addvec(McGeVector3d.kYAxis.clone().mult(this.size * (3 / 8)));
                   const line = new McDbLine(pt1, pt2);
   
                   const radius = this.size * (7 / 32);
                   const center = pt1.clone().addvec(McGeVector3d.kXAxis.clone().mult(radius));
                   const arc = new McDbArc();
                   arc.center = center;
                   arc.radius = radius;
                   arc.startAngle = Math.PI;
                   arc.endAngle = 0;
   
                   const _arc = arc.clone() as McDbArc;
                   _arc.mirror(basePt, pt1);
   
                   entityArr.push(line, arc, _arc)
               }   
               if (item.position === symbolPos.down) {
                   entityArr.forEach(item => {
                       item.mirror(this.fixedPoint, lastPt)
                   })
               }
               if (this.dottedLinePos && item.position === this.dottedLinePos) {
                   const v = this.dottedLinePos === 1 ? McGeVector3d.kYAxis.clone().mult(this.size / 4) : McGeVector3d.kYAxis.clone().mult(this.size / 4).negate();
                   entityArr.forEach(ent => {
                       ent.move(lastPt, lastPt.clone().addvec(v))
                   })
               }
               allEntityArr.push(...entityArr)
           })
           return { entityArr: allEntityArr }
       }

7.暴露获取、设置实体内部数据的属性或方法

       //设置或获取标记定位点
       public set weldingPosition(val: McGePoint3d) {
           this.position = this.fixedPoint = this.turningPoint = val.clone();
       }
       public get weldingPosition(): McGePoint3d {
           return this.position;
       }
       //设置或获取标记转折点
       public set turningPoint(val: McGePoint3d) {
           this.turningPt = this.fixedPoint = val.clone();
       }
       public get turningPoint(): McGePoint3d {
           return this.turningPt;
       }
       //设置或获取标记定点
       public set fixedPos(val: McGePoint3d) {
           this.fixedPoint = val.clone();
       }
       public get fixedPos(): McGePoint3d {
           return this.fixedPoint;
       }
       //设置或获取标记箭头尺寸
       public set dimSize(val: number) {
           this.size = val;
           this.height = this.size * (7 / 8)
       }
       public get dimSize(): number {
           return this.size;
       }
       //设置或获取虚线位置
       public set dottedLinePos(val: number) {
           this.dottedPos = val;
       }
       public get dottedLinePos(): number {
           return this.dottedPos;
       }
       //设置或获取焊接说明
       public set weldingSymbolInfo(val: string) {
           this.weldingInfo = val;
       }
       public get weldingSymbolInfo(): string {
           return this.weldingInfo;
       }
       private getBox(entityArr: McDbEntity[]) {
           let _minPt, _maxPt = null;
           entityArr.forEach(entity => {
               if (entity instanceof McDbMText) {
                   const textStyleId = MxCpp.getCurrentMxCAD().getDatabase().getCurrentlyTextStyleId();
                   entity.textStyleId = textStyleId;
                   entity.reCompute();
               }
               const { minPt, maxPt } = entity instanceof McDbMText ? MxCADUtility.getTextEntityBox(entity, false) : entity.getBoundingBox();
               if (!_minPt) _minPt = minPt.clone();
               if (!_maxPt) _maxPt = maxPt.clone();
               if (minPt.x < _minPt.x) _minPt.x = minPt.x;
               if (minPt.y < _minPt.y) _minPt.y = minPt.y;
               if (maxPt.x > _maxPt.x) _maxPt.x = maxPt.x;
               if (maxPt.y > _maxPt.y) _maxPt.y = maxPt.y;
           });
           if (_minPt && _maxPt) {
               this.assertWrite();
               this.maxPt = _maxPt;
               this.minPt = _minPt;
           }
       }
       // 获取包围盒
       public getBoundingBox(): { minPt: McGePoint3d; maxPt: McGePoint3d; ret: boolean; } {
           const allEntityArr = this.getAllEntity();
           this.getBox(allEntityArr);
           return { minPt: this.minPt, maxPt: this.maxPt, ret: true }
       }
       //获取或设置基本符号.
       public getBaseSymbok(): symbolType[] {
           return this.baseSymbok
       }
       public setBaseSymbok(val: symbolType[]) {
           const res = val.filter(item => item.typeName !== symbolName.none);
           this.baseSymbok = res;
       }
       //获取或设置辅助符号
       public getAuxiliarySymbol(): symbolType[] {
           return this.auxiliarySymbol
       }
       public setAuxiliarySymbol(val: symbolType[]) {
           const res = val.filter(item => item.typeName !== symbolName.none);
           this.auxiliarySymbol = res;
       }
       //获取或设置特殊符号
       public getSpecialSymbol(): symbolType[] {
           return this.specialSymbol
       }
       public setSpecialSymbol(val: symbolType[]) {
           const res = val.filter(item => item.typeName !== symbolName.none);
           this.specialSymbol = res;
       }
       //获取或设置补充符号
       public getSuppleSymbol(): symbolType[] {
           return this.suppleSymbol
       }
       public setSuppleSymbol(val: symbolType[]) {
           const res = val.filter(item => item.typeName !== symbolName.none);
           this.suppleSymbol = res;
       }
       //获取或设置左尺寸
       public getLeftDim(): symbolDim[] {
           return this.leftDim;
       }
       public setLeftDim(val: symbolDim[]) {
           const res = val.filter(item => item.content !== '');
           this.leftDim = res;
       }
       //获取或设置上尺寸
       public getTopDim(): symbolDim[] {
           return this.topDim;
       }
       public setTopDim(val: symbolDim[]) {
           const res = val.filter(item => item.content !== '');
           this.topDim = res;
       }
       //获取或设置右尺寸
       public getRightDim(): symbolDim[] {
           return this.rightDim;
       }
       public setRightDim(val: symbolDim[]) {
           const res = val.filter(item => item.content !== '');
           this.rightDim = res;
       }
       //获取或设置交错焊缝
       public getIntWeldSeam(): symbolDim[] {
           return this.interlacedWeldSeam
       }
       public setIntWeldSeam(val: symbolDim[]) {
           const res = val.filter(item => item.content !== '');
           this.interlacedWeldSeam = res;
       }

使用焊接符号标注

根据焊接符号标注于直线、圆弧或圆时标注将垂直于曲线的切线上的位置特点,我们可以通过识别标注点所在的实体类型获取该实体在标注点的切线方向和位置,并以此来确定标注的旋转角度和方向。下面为焊接符号标注的基础的用法,用户可在此基础上为焊接符号设置更多属性值,如补充符号、特殊符号、上下标注等:

let isArc = false;
// 设置标记位置点
const getPos = new MxCADUiPrPoint();
getPos.setMessage('请设置定位点或直线或圆弧');
const pos = await getPos.go();
if (!pos) return;
weldingSymbol.weldingPosition = pos;
// 定义过滤器
const filter = new MxCADResbuf()
// 添加对象类型,选择集只选择文字类型的对象
filter.AddMcDbEntityTypes("ARC,CIRCLE")
let objId = MxCADUtility.findEntAtPoint(pos.x, pos.y,pos.z,-1,filter);
if(objId.isValid()) isArc = true;
// 设置标记转折点
const getTurnPt = new MxCADUiPrPoint();
getTurnPt.setMessage('请设置引线转折点');
getTurnPt.setUserDraw((pt, pw) => {
    if(isArc){
        const arc = objId.getMcDbEntity() as McDbArc;
        const point = arc.getClosestPointTo(pt, true).val;
        weldingSymbol.weldingPosition = point;
    };
    weldingSymbol.turningPoint = pt;
    pw.drawMcDbEntity(weldingSymbol);
});
const turningPt = await getTurnPt.go();
if (!turningPt) return;
weldingSymbol.turningPoint = turningPt;
 
// 设置标记定点
const getFixedPt = new MxCADUiPrPoint();
getFixedPt.setMessage('拖动确定定位点');
getFixedPt.setUserDraw((pt, pw) => {
    const clone = weldingSymbol.clone() as McDbTestWeldingSymbol;
    clone.fixedPos = pt;
    pw.drawMcDbEntity(clone)
});
const fixedPt = await getFixedPt.go();
if (fixedPt) weldingSymbol.fixedPos = fixedPt;
const mxcad = MxCpp.getCurrentMxCAD();
mxcad.drawEntity(weldingSymbol);

效果演示

在上述介绍中,我们已经实现了焊接符号的自定义实体,通过该实体与我们的mxcad项目结合,我们就能够实现更完善的焊接符号标注功能,demo查看地址demo2.mxdraw3d.com:3000/mxcad/

基础效果演示: image-20250530141023471.png

根据上述内容可做扩展开发,根据焊接符号特性设置对应的弹框,其示例效果如下:

image-20250530141303952.png

基于transform(scale)的大屏自适应缩放:两种实用解决方案

2025年6月5日 23:11

描述

在现代数据可视化和大屏展示项目中,设计师通常以1920×1080(或类似比例)作为标准设计稿,而实际展示环境却可能涵盖从4K大屏到移动笔记本等各种终端设备。这种差异给实现完美自适应带来了巨大挑战——开发者既需要精确还原设计原貌,又要确保内容在不同尺寸下都能合理呈现。

传统响应式方案虽然可以通过媒体查询和百分比布局实现基本适配,但在大屏项目中往往捉襟见肘:布局容易失真、开发效率低下、性能表现不佳等问题频发。而基于CSS transform的创新方案则展现出独特优势:

  • 仅需对外层容器设置缩放比例,内部元素自动等比适配

  • 通过精确的数学计算保持设计稿的原始比例关系

  • 借助GPU硬件加速实现丝滑流畅的渲染性能

  • 代码量减少60%以上,开发效率显著提升

  • 智能适配从超宽屏到竖屏等各类极端显示比例

这种方案在保证设计还原度的同时,大幅降低了开发和维护成本。

简易效果展示

方案一:安全缩放(尽可能铺满但不溢出)

核心思想

这种方案确保内容始终完整显示在容器内,不会因为屏幕比例不同而被裁剪。

技术要点

  1. 使用Math.min选择宽度和高度的最小比例

  2. 确保内容在任何方向上都不会超出容器

  3. 适合展示重要信息,保证内容完整性

方案二:强制填充(必须铺满可以溢出)

核心思想

这种方案确保内容始终填满整个容器,可能会在某个方向上溢出,但能保证没有空白区域。

技术要点

  1. 使用Math.max选择宽度和高度的最大比例

  2. 内容可能会在某个方向上超出容器范围

  3. 适合背景图或非关键信息展示

实现代码

/**
 * 设置元素等比缩放
 * @param {number} containerWidth - 容器实际宽度 
 * @param {number} containerHeight - 容器实际高度
 * @param {HTMLElement} element - 需要缩放的元素
 * @param {'min'|'max'} scaleMode - 缩放模式:min(不溢出)/max(完全填充)
 */
function setElementScale(containerWidth, containerHeight, element, scaleMode) {
  const widthRatio = containerWidth / 1920  // 计算宽度比例
  const heightRatio = containerHeight / 1080 // 计算高度比例
  
  // 根据模式选择缩放比例
  const scale = scaleMode === 'min' 
    ? Math.min(widthRatio, heightRatio)  // 安全缩放
    : Math.max(widthRatio, heightRatio)  // 强制填充
  
  // 应用缩放(居中+缩放)
  element.style.transform = `translate(-50%, -50%) scale(${scale})`
}

关键CSS技巧

.element{
  width: 1920px;  /* 设计稿宽度 */
  height: 1080px; /* 设计稿高度 */
  transform-origin: 0 0;
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translateX(-50%) translateY(-50%);
}

  1. 固定尺寸:使用设计稿原始尺寸

  2. 居中定位:通过translate实现完美居中

  3. 变换原点:确保缩放从中心开始

性能优化建议

防抖处理:为resize事件添加防抖

window.addEventListener('resize', debounce(setElementScale, 100))

GPU加速:强制使用GPU渲染

.element{
  will-change: transform;
  backface-visibility: hidden;
}

总结对比

特性

安全缩放模式

强制填充模式

内容完整性

完全保证

可能部分溢出

空白区域

可能出现

完全填充

适用场景

关键数据展示

背景/装饰元素

实现方式

Math.min

Math.max

用户体验

内容始终可见

视觉冲击力强

这两种方案各有优劣,开发者应根据实际项目需求选择合适的方案,甚至可以组合使用,在不同区域采用不同的缩放策略,以达到最佳的展示效果。

完整代码

<template>
  <div class="container-group">
    <div class="container-group__row">
      <div class="display-container" ref="safeContainer1">
        <div class="scalable-content" ref="safeContent1">方案一:安全缩放(尽可能铺满但不溢出)</div>
      </div>
      <div class="display-container" ref="safeContainer2">
        <div class="scalable-content" ref="safeContent2">方案一:安全缩放(尽可能铺满但不溢出)</div>
      </div>
    </div>
    <div class="container-group__row">
      <div class="display-container" ref="fillContainer1">
        <div class="scalable-content" ref="fillContent1">方案二:强制填充(必须铺满可以溢出)</div>
      </div>
      <div class="display-container" ref="fillContainer2">
        <div class="scalable-content" ref="fillContent2">方案二:强制填充(必须铺满可以溢出)</div>
      </div>
    </div>
  </div>
</template>

<script setup>
import { onMounted, ref } from 'vue'

// 容器和内容引用
const safeContainer1 = ref(null)
const safeContainer2 = ref(null)
const fillContainer1 = ref(null)
const fillContainer2 = ref(null)
const safeContent1 = ref(null)
const safeContent2 = ref(null)
const fillContent1 = ref(null)
const fillContent2 = ref(null)

// 设计稿基准尺寸
const DESIGN_WIDTH = 1920
const DESIGN_HEIGHT = 1080

/**
 * 设置内容元素的自适应缩放
 * @param {number} containerWidth - 容器实际宽度
 * @param {number} containerHeight - 容器实际高度 
 * @param {HTMLElement} contentElement - 需要缩放的内容元素
 * @param {'safe'|'fill'} scaleStrategy - 缩放策略: safe(安全)/fill(填充)
 */
function scaleContentToFit(containerWidth, containerHeight, contentElement, scaleStrategy) {
  const widthRatio = containerWidth / DESIGN_WIDTH
  const heightRatio = containerHeight / DESIGN_HEIGHT
  
  // 根据策略选择缩放比例
  const scale = scaleStrategy === 'safe' 
    ? Math.min(widthRatio, heightRatio)  // 安全模式取最小值
    : Math.max(widthRatio, heightRatio)  // 填充模式取最大值
  
  // 应用缩放变换(保持居中)
  contentElement.style.transform = `translate(-50%, -50%) scale(${scale})`
}

onMounted(() => {
  // 安全缩放模式示例
  if (safeContainer1.value && safeContent1.value) {
    scaleContentToFit(
      safeContainer1.value.clientWidth,
      safeContainer1.value.clientHeight,
      safeContent1.value,
      'safe'
    )
  }

  if (safeContainer2.value && safeContent2.value) {
    scaleContentToFit(
      safeContainer2.value.clientWidth,
      safeContainer2.value.clientHeight,
      safeContent2.value,
      'safe'
    )
  }

  // 强制填充模式示例
  if (fillContainer1.value && fillContent1.value) {
    scaleContentToFit(
      fillContainer1.value.clientWidth,
      fillContainer1.value.clientHeight,
      fillContent1.value,
      'fill'
    )
  }

  if (fillContainer2.value && fillContent2.value) {
    scaleContentToFit(
      fillContainer2.value.clientWidth,
      fillContainer2.value.clientHeight,
      fillContent2.value,
      'fill'
    )
  }
})
</script>

<style scoped>
/* 容器组样式 */
.container-group {
  width: 100%;
  height: 100vh;
  display: flex;
  flex-direction: column;
  overflow: hidden;
}

/* 行样式 */
.container-group__row {
  width: 100vw;
  height: 50vh;
  display: flex;
  justify-content: space-evenly;
  align-items: center;
}

/* 显示容器基础样式 */
.display-container {
  background-color: rgba(0, 0, 0, 1);
  position: relative;
}

/* 具体容器尺寸 */
.display-container:nth-child(1) {
  width: 534px;
  height: 266px;
}

.display-container:nth-child(2) {
  width: 434px;
  height: 316px;
}

/* 可缩放内容样式 */
.scalable-content {
  width: 1920px;
  height: 1080px;
  background-color: rgba(252, 86, 49, 0.53);
  display: flex;
  justify-content: center;
  align-items: center;
  color: #fff;
  font-size: 80px;
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
}
</style>

教室选座项目分析

2025年6月5日 16:44

前言

该项目是使用Vue3+Node.js+mongodb数据库实现的,主要实现了用户登录验证、教室选择以及座位选择功能

项目实现效果

选座流程:登录 → 选座 → 选座成功

登录页面,输入学号和姓名,点击登录按钮进行登录,这里的登录的信息是存储在数据库中的,如果输入的学号和姓名信息与数据库中存储的信息不一致,则会有一个弹窗,提示学号必须是23503开头的9位或9-12位数字字符串,只有当输入的学号和姓名信息和数据库中的信息一致时,才能登录成功,并跳转到选座页面,进行选座!

屏幕截图 2025-06-05 095641.png 选座页面,最上面有一个退出按钮,用户点击退出,则会退出登录,返回到登录页面,然后下面是一个选座时间段和教室号的提示,当然每个教室的座位信息也是不同的,它们是实时更新的,在不同的时间段登录,它显示的就是不一样的。用户可以点击不同的教室进行选座,点击座位后,可以点击确认锁定座位,同时会向后端发送选座请求,并对你的token值进行验证,之后会跳转到选择成功的页面,这样选座就成功了。

屏幕截图 2025-06-05 095859.png

在选座成功的页面中,你也可以清楚的看到,具体的教室、时间段、以及日期和星期,你选择的座位在哪里,点击搜索可以进行刷新,如果别人也选座成功了,你也可以看到他的选座情况。点击右侧的学生名单,可以看到在这个时间段上课的学生名单,再次点击就可以恢复原样。

屏幕截图 2025-06-05 100238.png

前端逻辑代码

接下来,我们来看看是如何实现选座的,它的逻辑是怎样的吧!

  1. 退出按钮功能:用户点击退出按钮,就会触发 logout 方法,实现用户退出登录
  2. 信息展示:使用 <pre> 标签展示课程开设时间和可选座时段信息
  3. 时段选择:遍历 ampmArr 数组,渲染选座时段选项
  4. 教室选择:遍历 vmRoomArr 数组渲染教室选项,点击触发 selectRoom 方法加载对应教室数据。
  5. 座位渲染:通过两层 v-for 循环渲染教室座位布局,根据 seat 的值判断是座位还是过道(1表示座位,0表示过道),点击座位触发 handleSeatClick 方法。
<template>
    <div>
        <button @click="logout">退出</button>
        <!-- 添加导航链接 -->
<pre>
    Exprsss开设时间:周一、周五全天 。Python周四 上午
    当天可选座时段:
        上午 08:00-12:00  
        下午 13:00-17:00  
        晚上 19:00-23:00  
        其他时间 记录为 非选座时段
</pre>
        <!-- 上午、下午、晚上 -->
        <section>
            <ul class="ampm">
                <li v-for="(item, index) in ampmArr
                    :class="{ 'pick': ampm === item }"
                    :key="index"
                >
                    {{ item  }}
                </li>
            </ul>
        </section>
        
        <section>
            <ul class="room">
                <li v-for="roomID in vmRoomArr"
                    :class="{ 'pick': vmRoom === roomID }"
                    :key="roomID"
                    @click="selectRoom(roomID)"
                >
                    {{ roomID  }}
                </li>
            </ul>
        </section>
        <!-- 讲台 -->
        <div class="blackboard">讲台</div>
        <div class="classroom">
            <!-- 遍历每一排 -->
            <div v-for="(row, rowIndex) in 教室数据.arr" :key="rowIndex" class="row">

                <!-- 遍历每一个座位或过道 -->
                <div v-for="(seat, colIndex) in row" :key="`${rowIndex}-${colIndex}`"
                    :class="{ 'seat': seat === 1, 'aisle': seat === 0, 'selected': getSeatById(教室数据.layout[rowIndex][colIndex])?.isSelected }"
                    @click="seat === 1 && handleSeatClick(教室数据.layout[rowIndex][colIndex])">
                    <span v-if="seat === 1">{{ 教室数据.layout[rowIndex][colIndex] }}</span>
                </div>
            </div>
        </div>
    </div>
</template>

登录验证

调用 验证token 方法,检查 localStorage 中是否存在 token,若不存在则跳转到登录页

// 判断是否有 token
const hasToken = ref(false);
const 验证token = () => {
    const token = localStorage.getItem('token');
    console.log('token=', token);
    hasToken.value = !!token;
    console.log(hasToken.value);
    if (!hasToken.value) {
        console.log('跳转登录页');
        router.push('/login');
    }
};

教室数据获取

用户选择教室时,通过使用async异步获取数据,调用 fetchClassroomData 方法,发送 GET 请求,获取对应教室的座位布局数据。

// 获取教室数据
const fetchClassroomData = async () => {
    try {
        // 获取本地存储的 token(身份验证凭证
        const token = localStorage.getItem('token');
        let URL = `${BASE_URL}/api/room/want?roomID=${vmRoom.value}`
        // 发送 GET 请求(携带 token 进行身份验证)
        const response = await fetch(URL, {
            method: 'GET',
            headers: {
                'Content-Type': 'application/json',
                'Authorization': `Bearer ${token}`
            }
        });
        // 检查响应状态
        if (!response.ok) {
            throw new Error(`HTTP 错误!状态:${response.status}`);
        }
        const data = await response.json();
        教室数据.value = data;
    } catch (error) {
        console.error('获取教室数据出错:', error);
    }
};

onMounted(() => {
    console.log('组件已挂载,检查 token...');
    验证token();
    // 组件挂载后获取教室数据
    fetchClassroomData();
});

选座功能

用户点击座位就会触发 handleSeatClick 方法,先更新本地座位选中状态,弹出确认框,确认后发送 POST 请求提交选座信息

选中状态管理

  • 先将所有座位的 isSelected 设为 false,再将当前点击的座位设为 true
  • 这确保同一时间只有一个座位被选中

座位锁定流程

  • 弹出确认对话框,用户确认后发送 POST 请求,锁定座位。
  • 根据服务器返回的状态码(code)处理结果:
  • 成功(code === 10000):显示提示并跳转到查看页面。
  • 失败:显示错误信息,锁定座位出错
// 根据 seat_id 获取座位信息
const getSeatById = (seatId) => {
    return 教室数据.value.data.find(seat => seat.seat_id === seatId);
};

const handleSeatClick = async (seatId) => {
    // 先将所有座位的选中状态置为 false
    教室数据.value.data.forEach(seat => {
        seat.isSelected = false;
    });

    // 将当前点击的座位选中状态置为 true
    const currentSeat = getSeatById(seatId);
    if (currentSeat) {
        currentSeat.isSelected = true;
    }

    console.log(`你点击了座位 ID 为 ${seatId} 的座位`);
    if (confirm(`确认锁定座位 ID 为 ${seatId} 的座位吗?`)) {
        let 请求参数 = {
            seat_id: seatId,
            roomID: 教室数据.value.roomID,
            token: localStorage.getItem('token'),
        }
        console.log(请求参数)
        try {
            const 响应 = await fetch(`${BASE_URL}/api/seat/pick`, {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify(请求参数)
            });
            if (!响应.ok) {
                throw new Error(`HTTP 错误!状态:${响应.status}`);
            }
            
            // 处理响应数据
            const { code, text } = await 响应.json();   //数据

            if (code === 10000) {
                alert(text);
                let 参数 = {
                    name:'look',
                    query:{
                        roomID:vmRoom.value,
                        ampm:ampm.value,
                    }
                }
                
                router.push( 参数 );
            } else {
                alert(text);
            }
            
            // 更新本地座位状态
            if (currentSeat) {
                currentSeat.isLocked = true;
            }
        } catch (error) {
            console.error('锁定座位出错:', error);
        }
    } else {
        // 如果取消锁定,将当前座位的选中状态置为 false
        if (currentSeat) {
            currentSeat.isSelected = false;
        }
    }
};

连接MongoDB数据库

需要用到的数据都存储在MongoDB数据库中,为了方便调用 屏幕截图 2025-06-05 154103.png

// db.js
const { MongoClient } = require('mongodb');
// 数据库连接配置
class DB {
    constructor(数据库名="云上教室") {
        this.uri = "mongodb://localhost:27017/云上教室";
        this.uri = "mongodb://’这里放你的IPV4地址‘:27017/云上教室";
        //this.uri = "mongodb://127.0.0.1:27017/云上教室";    //上线设置为本机IP 速度更快

        this.client = new MongoClient(this.uri);
        this.db = this.client.db(数据库名);
        return this.db;
    }

    // 封装连接数据库的函数
    async connect() {
        try {
            await this.client.connect();
            console.log('已连接到 MongoDB');
            return this.db;
        } catch (error) {
            console.error('连接到 MongoDB 时出错:', error);
            throw error;
        }
    }

    // 封装关闭数据库连接的函数
    async close() {
        try {
            await this.client.close();
            console.log('已断开与 MongoDB 的连接');
        } catch (error) {
            console.error('断开与 MongoDB 的连接时出错:', error);
            throw error;
        }
    }
}
module.exports = new DB();

后端逻辑代码

类文件

用于获取当前的时间信息(上午/下午、星期和日期)

class 工具 {
    constructor() {
    }
    获取上午下午() {
        const now = new Date();
        const hours = now.getHours();
        if (hours >= 8 && hours < 12) {
            return '上午';
        } else if (hours >= 13 && hours < 17) {
            return '下午';
        } else if (hours >= 19 && hours < 23) {
            return '晚上';
        } else {
            return '非选座时段';
        }
    }
    获取星期() {
        const daysOfWeek = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六'];
        const now = new Date();
        const dayIndex = now.getDay();
        return daysOfWeek[dayIndex];
    }

    //获取当前日期,格式为 YYYY-MM-DD
    获取当前日期() {
        const now = new Date();
        const year = now.getFullYear();
        const month = String(now.getMonth() + 1).padStart(2, '0');
        const day = String(now.getDate()).padStart(2, '0');
        return `${year}-${month}-${day}`;
    }
}// 定义 工具 类

// 导出 工具 类
module.exports = 工具;

路由处理函数

/api/room/list,用于连接数据库,获取所有教室的数据

屏幕截图 2025-06-05 162303.png/api/room/want,用于获取指定教室在特定日期和上下午时段的座位信息

路由.get('/api/room/want', async (req, res ) => {
    //一些参数 动态生成 Data
    let { roomID = 506 } = req.query;
    //发起请求时必须在有效时段
    //服务器设置时间
    let ampm = tool.获取上午下午();
    let week = tool.获取星期();
    let date = tool.获取当前日期();
    // 数据校验  注意严格 区分数据类型 、大小写
    // RoomID  roomID ='501'
    // 转换数字类型 在 Express 里,通过 req.query 获取的查询字符串参数默认都是字符串类型,这是因为 HTTP 请求中的查询字符串是以文本形式传输的,Express 不会自动对这些参数进行类型转换
    roomID = parseInt(roomID);
    //查看教室座位 数量
    const 教室 =await  DB.collection("教室");
    let room = await 教室.findOne(
        { roomID },                // 查询条件
        { projection: { _id: 0 } } // 投影参数
    );

    // 关联数据 根据的应该是 date 决定,不是由Week决定的
    const 座位记录 =await DB.collection("座位记录");
    let 参数 = {
        roomID,
        date,
        ampm
    }
    let 投影参数 = {
        projection: { _id: 0,roomID:0,date:0,ampm:0 }
    }
    const seatArr = await 座位记录.find(参数,投影参数).sort({ time: 1 }).toArray();
    console.table(seatArr)

    // 确保 room 不为 null 再进行操作
    const 合并数据 =room ?  room.data?.map(座位 => {
        const 记录 = seatArr.find(记录 => 记录.seat_id === 座位.seat_id);
        return {
            ...座位,
            ...记录, // 保留原始记录数据
            is_free: 记录 ? false : 座位.is_free // 动态设置状态
        };
    }) : [];
    console.log( `--------------------------------------------------` )
    console.log( 合并数据 )
    res.json({
        ...(room || {}),
        // 过滤完全无记录的座位,先检查元素是否为对象,再检查属性数量
        data: 合并数据?.filter(item => typeof item === 'object' && item!== null && Object.keys(item).length > 3)
    });

/api/room/look,用于查看指定教室在特定日期和上下午时段的座位详情

//查看教室座位 详情
路由.get('/api/room/look', async (req, res ) => {
    let { roomID: rawRoomID, ampm: rawAmpm,date:dateFE  } = req.query;

    // 参数有效性校验
    roomID = parseInt(rawRoomID);
    if (isNaN(roomID)) roomID = 506; // 无效值时使用默认值
    ampm = ['上午', '下午'].includes(rawAmpm) ? rawAmpm : tool.获取上午下午();
    date =dateFE ?? tool.获取当前日期();
    // 数据校验  注意严格 区分数据类型 、大小写
    // RoomID  roomID ='501'
    // 转换数字类型 在 Express 里,通过 req.query 获取的查询字符串参数默认都是字符串类型,这是因为 HTTP 请求中的查询字符串是以文本形式传输的,Express 不会自动对这些参数进行类型转换
    roomID = parseInt(roomID);
    const 集合 =await  DB.collection("教室");
    let room = await 集合.findOne(
        { roomID },                // 查询条件
        { projection: { _id: 0 } } // 投影参数
    );
    // 新增字段过滤
    // const { _id, ...教室数据 } = room ? room : {};  // 处理空值情况
    console.log('room',room);  // 现在输出已移除 _id 的数据
    // 关联数据 根据的应该是 date 决定,不是由Week决定的
    const 集合2 =await DB.collection("座位记录");
    let 参数 = {
        roomID,
        date,
        ampm
    }
    let 投影参数 = {
        projection: { _id: 0,roomID:0,date:0,ampm:0 }
    }
    const seatArr = await 集合2.find(参数,投影参数).sort({ time: 1 }).toArray();
    console.table(seatArr)
    // 确保 room 不为 null 再进行操作
    const 合并数据 =room ?  room.data?.map(座位 => {
        const 记录 = seatArr.find(记录 => 记录.seat_id === 座位.seat_id);
        return {
            ...座位,
            ...记录, // 保留原始记录数据
            is_free: 记录 ? false : 座位.is_free // 动态设置状态
        };
    }) : [];
    console.log( `--------------------------------------------------` )
    console.log( 合并数据 )
    res.json({
        ...(room || {}),
        // 过滤完全无记录的座位,先检查元素是否为对象,再检查属性数量
        data: 合并数据?.filter(item => typeof item === 'object' && item!== null && Object.keys(item).length > 3)
    });
});

获取座位列表和选择座位

用于处理时间信息和座位状态的更新

  1. 定义了一个输入验证函数 validateInput,用于验证 tokenseat_id 是否有效
  2. 使用 validateSeat 函数验证座位号是否在有效范围内(1-154)。
  3. 使用 validateToken 函数验证 token 的格式是否正确。
// 封装输入验证逻辑
const validateInput = (token, seat_id, ip) => {
    if (!token || !seat_id) {
        const msg = { text: `🐮,🐯,🐰,🐱,🐲,🐳,🐴,🐵 。 token和seat_id都是必需的!👀 🐶,🐷,🐸,🐹,🐺,🐻,🐼,🐽,🐾,🐿️, ` };
        report(ip, msg);
        return msg;
    }

    if (!validateSeat(seat_id)) {
        const msg = { text: `🐱座位号应该在 154(含)以内!请核对真实座位,使用数字 ` };
        report(ip, msg);
        return msg;
    }

    if (!validateToken(token)) {
        const msg = { text: `🐲🐲🐲🐲门票(token)应通过node 1获得,格式为 XXXX-XXXX-XXXX` };
        report(ip, msg);
        return msg;
    }
    return null;
};

总结

以上就是云上课堂选座的内容,这个项目提供了一个线上可视化选座的平台,操作步骤简单,不仅为学生提供了自由选座,也让老师能够清楚地看出教室的选座情况。

vue-05(自定义事件)

作者 Smile_frank
2025年6月5日 09:19

自定义事件:在父组件中发出和处理事件

自定义事件是 Vue.js 中组件通信的基石,它允许子组件向其父组件发出事件信号。此机制使您能够通过解耦组件逻辑和提高可重用性来构建更具交互性和动态性的用户界面。了解如何正确发出和处理自定义事件对于创建复杂的 Vue.js 应用程序至关重要。

发出自定义事件

$emit 方法是组件触发自定义事件的主要方式。这个方法在每个 Vue 组件实例上都可用,并接受两个参数:事件名称(字符串)和可选的有效负载(你想传递给父组件的任何数据)。

基本事件

事件发出的最简单形式涉及使用事件名称调用 $emit

<template>
  <button @click="handleClick">Click me</button>
</template>

<script>
export default {
  methods: {
    handleClick() {
      this.$emit('my-custom-event');
    }
  }
};
</script>

在此示例中,单击按钮时,将执行 handleClick 方法,该方法将发出 my-custom-event 事件。然后,父组件可以侦听此事件。

使用 Payload 发出事件

通常,您需要将数据与事件一起发送。这就是可选的 payload 参数的用武之地。

<template>
  <button @click="handleClick">Add Item</button>
</template>

<script>
export default {
  data() {
    return {
      newItem: 'Example Item'
    };
  },
  methods: {
    handleClick() {
      this.$emit('add-item', this.newItem);
    }
  }
};
</script>

在这里,当单击按钮时,将发出 add-item 事件,并将 this.newItem 的值作为有效负载传递。父组件可以在其事件处理程序中访问此有效负载。

事件命名约定

虽然从技术上讲,你可以使用任何字符串作为事件名称,但最好使用 kebab-case(例如,my-custom-eventadd-item)。此约定与 HTML 属性命名一致,并提高了可读性。

本机事件与自定义事件

区分本机 DOM 事件(如 clickmouseoversubmit)和自定义事件非常重要。原生事件由浏览器触发,而自定义事件由 Vue 组件使用 $emit 触发。虽然您可以侦听组件上的本机事件,但自定义事件为组件提供了一种更加结构化和可控的通信方式。

处理父组件中的自定义事件

父组件可以使用 v-on 指令(或其简写 @)监听其子组件发出的自定义事件。

侦听事件

要监听事件,你可以在父组件模板中的子组件标签上使用 v-on(或 @)。

<template>
  <div>
    <child-component @my-custom-event="handleCustomEvent"></child-component>
  </div>
</template>

<script>
import ChildComponent from './ChildComponent.vue';

export default {
  components: {
    ChildComponent
  },
  methods: {
    handleCustomEvent() {
      console.log('Custom event received!');
    }
  }
};
</script>

在此示例中,父组件正在侦听 ChildComponent 发出的 my-custom-event 事件。当事件被触发时,将执行父组件中的 handleCustomEvent 方法。

访问 Payload

如果子组件发出带有有效负载的事件,则父组件可以将有效负载作为事件处理程序的参数进行访问。

<template>
  <div>
    <child-component @add-item="addItem"></child-component>
    <ul>
      <li v-for="item in items" :key="item">{{ item }}</li>
    </ul>
  </div>
</template>

<script>
import ChildComponent from './ChildComponent.vue';

export default {
  components: {
    ChildComponent
  },
  data() {
    return {
      items: []
    };
  },
  methods: {
    addItem(newItem) {
      this.items.push(newItem);
    }
  }
};
</script>

在这种情况下,addItem 方法接收 ChildComponent 发出的 newItem 值,并将其添加到 items 数组中。

内联事件处理程序

您还可以直接在模板中使用内联事件处理程序。

<template>
  <div>
    <child-component @my-custom-event="message = 'Event received!'"></child-component>
    <p>{{ message }}</p>
  </div>
</template>

<script>
import ChildComponent from './ChildComponent.vue';

export default {
  components: {
    ChildComponent
  },
  data() {
    return {
      message: ''
    };
  }
};
</script>

这对于简单的事件处理逻辑很有用,但对于更复杂的逻辑,通常最好使用专用方法。

事件修饰符

Vue 提供了事件修饰符,可以与 v-on 一起使用来修改事件的处理方式。虽然某些修饰符特定于本机 DOM 事件(例如,.prevent.stop),但其他修饰符也可用于自定义事件。我们将在下一课中更详细地探讨事件修饰符。

实际示例和演示

让我们考虑这样一个场景:您有一个 ProductCard 组件,该组件显示产品信息并允许用户将产品添加到他们的购物车。

ProductCard.vue (子组件):

<template>
  <div class="product-card">
    <h3>{{ product.name }}</h3>
    <p>{{ product.description }}</p>
    <button @click="addToCart">Add to Cart</button>
  </div>
</template>

<script>
export default {
  props: {
    product: {
      type: Object,
      required: true
    }
  },
  methods: {
    addToCart() {
      this.$emit('add-to-cart', this.product);
    }
  }
};
</script>

<style scoped>
.product-card {
  border: 1px solid #ccc;
  padding: 10px;
  margin-bottom: 10px;
}
</style>

ProductList.vue (父组件):

<template>
  <div>
    <h2>Product List</h2>
    <product-card
      v-for="product in products"
      :key="product.id"
      :product="product"
      @add-to-cart="handleAddToCart"
    ></product-card>
    <h3>Cart</h3>
    <ul>
      <li v-for="item in cart" :key="item.id">{{ item.name }}</li>
    </ul>
  </div>
</template>

<script>
import ProductCard from './ProductCard.vue';

export default {
  components: {
    ProductCard
  },
  data() {
    return {
      products: [
        { id: 1, name: 'Product A', description: 'Description of Product A' },
        { id: 2, name: 'Product B', description: 'Description of Product B' }
      ],
      cart: []
    };
  },
  methods: {
    handleAddToCart(product) {
      this.cart.push(product);
    }
  }
};
</script>

在此示例中,ProductCard 组件在单击“Add to Cart”按钮时发出 add-to-cart 事件,并将 product 对象作为有效负载传递。ProductList 组件侦听此事件并将产品添加到 cart 数组中。

vue中内置指令v-model的作用和常见使用方法介绍以及在自定义组件上支持

作者 邹荣乐
2025年6月5日 09:11

@[TOC]

一、v-model是什么

v-model是Vue框架的一种内置的API指令,本质是一种语法糖写法,它负责监听用户的输入事件以更新数据,并对一些极端场景进行一些特殊处理。在Vue中,v-model是用于在表单元素和组件之间创建双向数据绑定的指令。它可以简化表单元素的绑定,使得在用户输入时能够自动更新数据。

v-model是value+input的语法糖,是v-band和v-on的简洁写法。v-model就实现了双向数据绑定,实际上它就是通过Vue提供的事件机制。即在子组件通过$emit()触发一个事件,在父组件使用v-model即可。

二、什么是语法糖

在计算机科学中,语法糖(syntactic sugar)是指编程语言中可以更容易地表达一个操作的语法。它可以使程序员更加容易地使用这门语言,使操作变得更加清晰、方便,或者更加符合程序员的编程习惯。

具体来说,语法糖是语言中的一个构件,当去掉该构件后并不影响语言的功能和表达能力。例如,C语言中的标记a[i]就是*(a+i)的语法糖。

语言的处理器,包括编译器,静态分析器等,经常会在处理之前把语法糖构件转换成更加基础的构件,这个过程通常被称为"desugaring"。

简而言之,语法糖就是一种便捷写法。例如:input.map(a => a-8); 去掉语法糖就是:input.map(function (a) { return a - 8; }); 通过例子你可以看出来,语法糖的使用其实就是让我们的写的代码更简单,看起来也更容易理解。

三、v-model常见的用法

单向数据绑定: 在Vue中,我们可以使用v-bind实现单项的数据绑定,也就是通过父组件向子组件传入数据 ,但是反过来,子组件不可以修改父组件传递过来的数据 ,这也就是所谓的单向数据绑定。

双向数据绑定 v-bind和v-on实现了双向绑定实现了双向数据绑定。

1、对于输入框(input):
<input type="text" v-bind:value="value" v-on:input="value = $event.target.value" />

<input type="text" :value="value" @input="value = $event.target.value" />

v-model是v-bind和v-on的语法糖,即,v-model算是v-band和v-on的简洁写法。

<input type="text" v-model="value" />

在这个例子中,v-model将输入框的值与数据对象中的value属性进行了绑定。当用户输入时,value的值会自动更新。

2、对于复选框(checkbox):
<input v-model="checked" type="checkbox">

在这个例子中,v-model将复选框的选中状态与数据对象中的checked属性进行了绑定。当用户选中或取消选中复选框时,checked的值会自动更新。

3、对于选择框(select):
<select v-model="selected">  
  <option value="option1">Option 1</option>  
  <option value="option2">Option 2</option>  
</select>

在这个例子中,v-model将选择框的值与数据对象中的selected属性进行了绑定。当用户选择一个选项时,selected的值会自动更新为所选选项的value值。

4、对于组件(component):

父组件

<template>
    <div>
        <child-component v-model="message"></child-component>
        <p>Message from parent component: {{ message }}</p>
    </div>
</template>  
    
<script>
import ChildComponent from './childComponent.vue';

export default {
    data() {
        return {
            message: 'hello world'
        };
    },
    components: {
        ChildComponent
    }
};
</script>

子组件

<template>
    <div>
        <p>Message from parent component: {{ value }}</p>
        <button type="button" @click="updateValue">更新</button>
    </div>
</template>  
    
<script>
export default {
    props: {
        value: {
            type: String,
            default: ""
        }
    },
    methods: {
        updateValue() {
            this.$emit("input", 'update message');
        }
    }
};
</script>

在这个例子中,父组件将message属性绑定到子组件的value属性上,并使用v-model指令来实现双向数据绑定。子组件内部点击按钮更新message,并使用$emit()触发一个事件,从而更新父组件的message属性。


除了以上的例子,v-model还可以用于其他表单元素和组件,如文本域(textarea)、开关(switch)等。它的工作原理是监听表单元素的输入事件,将输入值同步到绑定的数据属性上,同时当数据属性的值发生变化时,也会自动更新表单元素的值。

需要注意的是,v-model使用的数据属性通常应该是响应式对象或数组,这样才能够实现数据的双向绑定。如果使用非响应式对象或数组,v-model可能无法正常工作。

四、v-model修饰符

v-model有一些常用的修饰符,它们可以用来控制v-model的行为。使用这些修饰符可以让我们更方便地控制v-model的行为,提高开发效率。

以下是一些常用的v-model修饰符:

<input v-model.lazy="message">  
<input v-model.number="message">  
<input v-model.trim="message">
  • .lazy:用于实现懒加载,只有当输入框获取焦点时才会更新绑定的数据。
  • .number:我们的输入将自动将输入转为字符串—即使我们将输入是数字。确保将值作为数字处理的一种方法是使用. number修饰符。根据Vue文档,如果输入发生变化,并且parseFloat()无法解析新值,那么将返回输入的最后一个有效值。
  • .trim:与大多数编程语言中的trim方法类似,.trim修饰符在返回值之前删除开头或结尾的空格。

五、v-model 仅仅是语法糖吗?

v-model不仅仅是语法糖,它还具有创建响应式数据的功能。v-model将组件的value属性和input事件进行绑定,实现数据的双向绑定。同时,v-model还可以创建响应式数据,例如在表单元素上绑定一个不存在的属性,v-model会自动创建该属性,并且该属性是响应式的。因此,v-model的作用不仅仅是语法糖,还包括创建响应式数据和实现数据的双向绑定。

举个例子:

<template>  
  <div>  
    <input type="text" v-model="user.age">  
  </div>  
</template>  
  
<script>  
export default {  
  data() {  
    return {  
      user:{
        name:"张三"
      }
    };  
  }  
};  
</script>

在这个例子中,响应式数据user中没有定义 user.age 属性,但是 template 里却用 v-model 绑定了 user.age,v-model会在user 上新增 age 属性,并且 age 这个属性还是响应式的。

六、v-model 是单向数据流吗?

虽然官方没有明确表示这点,但我们可以捋一捋两者的关系。

  • 什么是单项数据流? 子组件不能改变父组件传递给它的 prop 属性,推荐的做法是它抛出事件,通知父组件自行改变绑定的值。

  • v-model 的做法是怎样的? v-model 做法完全符合单项数据流。甚至于,它给出了一种在命名和事件定义上的规范。

在这里插入图片描述

单向数据流』总结起来其实也就8个字:『数据向下,事件向上』。

七、如何让自定义组件支持 v-model?

一个组件上的 v-model 默认会利用名为 value 的 prop 和名为 input 的事件,但是像单选框、复选框等类型的输入控件可能会将 value 属性 用于不同的目的。model 选项可以用来避免这样的冲突。

在定义 vue 组件时,你可以提供一个 model 属性,用来定义该组件以何种方式支持 v-model。

model 属性本身是有默认值的,如下:

// 默认的 model 属性
export default {
  model: {
    prop: 'value',
    event: 'input'
  }
}

也就是说,如果你不定义 model 属性,或者你按照上面方法定义属性,当其他人使用你的自定义组件时,v-model="foo" 就完全等价于 :value="foo" 加上 @input="foo = $event"。

让我们看个例子。

先定义一个自定义组件:

<template>
    <div>
        当前数量是{{ count }}
        <el-button @click="changeCount(1)"></el-button>
        <el-button @click="changeCount(-1)"></el-button>
    </div>
</template>
<script>
export default {
    props: {
        count: {
            type: Number,
            default: 1
        },
    },
    // // 自定义v-model的格式
    model: {
        prop: 'count',// 代表 v-model 绑定的prop名
        event: 'input'// 代码 v-model 通知父组件更新属性的事件名
    },
    methods: {
        changeCount(step) {
            const newCount = this.count + step
            this.$emit('input', newCount)
        },
    }
}
</script>

然后我们在父组件中使用该组件:

<template>
    <div>
        <child-component v-model="count"></child-component>
    </div>
</template>
<script>
import ChildComponent from './childComponent.vue';
export default {
    data() {
        return {
            count: 6
        };
    },
    components: {
        ChildComponent
    }
};
</script>

在这个例子中,这里的 count 的值将会传入这个名为 count 的 prop。同时当 触发一个 changeCount 事件并附带一个新的值的时候,这个 count 的 property 将会被更新。

注意你仍然需要在组件的 props 选项里声明 count 这个 prop。

Vue事件总线(EventBus)使用指南:详细解析与实战应用

作者 邹荣乐
2025年6月5日 09:10

@[TOC]

前言

vue组件非常常见的有父子组件通信,兄弟组件通信。而父子组件通信就很简单,父组件会通过 props 向下传数据给子组件,当子组件有事情要告诉父组件时会通过 $emit 事件告诉父组件。今天就来说说如果两个页面没有任何引入和被引入关系,该如何通信了?

如果咱们的应用程序不需要类似Vuex这样的库来处理组件之间的数据通信,就可以考虑Vue中的 事件总线 ,即 EventBus来通信。

EventBus的简介

EventBus 又称为事件总线。在Vue中可以使用 EventBus 来作为沟通桥梁的概念,就像是所有组件共用相同的事件中心,可以向该中心注册发送事件或接收事件,所以组件都可以上下平行地通知其他组件,但也就是太方便所以若使用不慎,就会造成难以维护的“灾难”,因此才需要更完善的Vuex作为状态管理中心,将通知的概念上升到共享状态层次。

EventBus是 基于 订阅/发布 模式实现的 基于事件的异步分发处理系统。 好处就是能够解耦 订阅者 和 发布者,简化代码。

在这里插入图片描述 发布者通过EventBus发布事件,订阅者通过EventBus订阅事件,当发布者发送事件时,订阅该事件的订阅者的事件处理方法将被调用。从图中看出,发布者发送一个事件时,则该事件将会同时传递给一个或多个该事件的订阅者。

订阅者(Subscriber)把自己想订阅的事件(Event)注册(register)到调度中心(EventBus),当发布者(Publisher)发布该事件到调度中心时,也就是该事件触发时,由调度中心统一调度订阅者注册到调度中心的处理代码(onEvent())。

EventBus的优点

  • 简化了组件间间的通信;
  • 对事件通信双方进行解耦 ;
  • 避免了复杂且容易出错的依赖性和生命周期问题;
  • 速度快,性能好,代码简单优雅;
  • 分离了事件的发送者和接受者;

如何使用EventBus

一、初始化

首先需要创建事件总线并将其导出,以便其它模块可以使用或者监听它。我们可以通过两种方式来处理。先来看第一种,新创建一个 .js 文件,比如 event-bus.js

// event-bus.js
import Vue from 'vue'
export const EventBus = new Vue()

实质上EventBus是一个不具备 DOM 的组件,它具有的仅仅只是它实例方法而已,因此它非常的轻便。

另外一种方式,可以直接在项目中的 main.js 初始化 EventBus :

// main.js
Vue.prototype.$EventBus = new Vue()

注意,这种方式初始化的EventBus是一个全局的事件总线。稍后再来聊一聊全局的事件总线。

现在我们已经创建了 EventBus ,接下来你需要做到的就是在你的组件中加载它,并且调用同一个方法,就如你在父子组件中互相传递消息一样。

二、发送事件

假设你有两个Vue页面需要通信: A 和 B ,A页面 在按钮上面绑定了点击事件,发送一则消息,想=通知 B页面。

<!-- A.vue -->
<template>
    <button @click="sendMsg()">-</button>
</template>

<script> 
import { EventBus } from "../event-bus.js";
export default {
  methods: {
    sendMsg() {
      EventBus.$emit("aMsg", '来自A页面的消息');
    }
  }
}; 
</script>

接下来,我们需要在 B页面 中接收这则消息。

三、接收事件

<!-- IncrementCount.vue -->
<template>
  <p>{{msg}}</p>
</template>

<script> 
import { 
  EventBus 
} from "../event-bus.js";
export default {
  data(){
    return {
      msg: ''
    }
  },
  mounted() {
    EventBus.$on("aMsg", (msg) => {
      // A发送来的消息
      this.msg = msg;
    });
  }
};
</script>

同理我们也可以在 B页面 向 A页面 发送消息。这里主要用到的两个方法:

// 发送消息
EventBus.$emit(channel: string, callback(payload1,…))

// 监听接收消息
EventBus.$on(channel: string, callback(payload1,…))

前面提到过,如果使用不善,EventBus会是一种灾难,到底是什么样的“灾难”了?大家都知道vue是单页应用,如果你在某一个页面刷新了之后,与之相关的EventBus会被移除,这样就导致业务走不下去。还要就是如果业务有反复操作的页面,EventBus在监听的时候就会触发很多次,也是一个非常大的隐患。这时候我们就需要好好处理EventBus在项目中的关系。通常会用到,在vue页面销毁时,同时移除EventBus事件监听。

四、移除事件监听者

如果想移除事件的监听,可以像下面这样操作:

import { 
  eventBus 
} from './event-bus.js'
EventBus.$off('aMsg', {})

你也可以使用 EventBus.$off('aMsg') 来移除应用内所有对此某个事件的监听。或者直接调用 EventBus.$off() 来移除所有事件频道,不需要添加任何参数 。

上面就是 EventBus 的使用方法,是不是很简单。上面的示例中我们也看到了,每次使用 EventBus 时都需要在各组件中引入 event-bus.js 。事实上,我们还可以通过别的方式,让事情变得简单一些。那就是创建一个全局的 EventBus 。接下来的示例向大家演示如何在Vue项目中创建一个全局的 EventBus 。

五、创建全局EventBus

var EventBus = new Vue();
Object.defineProperties(Vue.prototype, {
  $bus: {
    get: function () {
      return EventBus
    }
  }
})

在这个特定的总线中使用两个方法$on和$emit。一个用于创建发出的事件,它就是$emit;另一个用于订阅$on:

var EventBus = new Vue();

this.$bus.$emit('nameOfEvent', { ... pass some event data ...});

this.$bus.$on('nameOfEvent',($event) => {
  // ...
})

然后我们可以在某个Vue页面使用this.$bus.$emit("sendMsg", '我是web');,另一个Vue页面使用

this.$bus.$on('sendMsg', function(value) {
  console.log(value); 
})

同时也可以使用this.$bus.$off('sendMsg')来移除事件监听。

总结

本文主要通过简单的实例学习了Vue中有关于 EventBus 相关的知识点。主要涉及了 EventBus 如何实例化,又是如何通过 emit发送频道信号,又是如何通过emit 发送频道信号,又是如何通过 on 来接收频道信号。最后简单介绍了如何创建全局的 EventBus 。从实例中我们可以了解到, EventBus 可以较好的实现兄弟组件之间的数据通讯。

Vue2 与 Vuex 状态管理实战指南

2025年6月5日 08:21

Vue2 与 Vuex 状态管理实战指南

一、初识 Vuex

1. 为什么需要 Vuex?

在多组件共享状态的场景中(如用户登录信息、购物车数据),传统父子组件通信方式存在以下问题:

  • ** prop drilling(属性钻孔)**:多层组件间传递数据需逐层传递
  • 事件总线复杂性:事件订阅/发布模式难以维护
  • 全局变量风险:使用全局变量会导致状态修改不可控

2. 核心概念

graph TD
    A[Vue 实例] --> B(Vuex Store)
    B --> C[State]
    B --> D[Getters]
    B --> E[Mutations]
    B --> F[Actions]
    B --> G[Modules]

二、快速上手

1. 安装与配置

# 安装指定版本(Vue2 必须使用 Vuex 3)
npm install vuex@3.6.2
// store/index.js
import Vue from 'vue';
import Vuex from 'vuex';

Vue.use(Vuex);

export default new Vuex.Store({
  state: {
    count: 0,
    user: null
  },
  mutations: {
    INCREASE(state) {
      state.count++;
    },
    SET_USER(state, payload) {
      state.user = payload;
    }
  },
  actions: {
    asyncIncrease({ commit }) {
      setTimeout(() => {
        commit('INCREASE');
      }, 1000);
    }
  }
});
// main.js
import Vue from 'vue';
import App from './App.vue';
import store from './store';

new Vue({
  el: '#app',
  store, // 注入到 Vue 实例
  render: h => h(App)
});

2. 第一个示例:计数器

<!-- Counter.vue -->
<template>
  <div>
    <p>{{ count }}</p >
    <button @click="increment">+1</button>
    <button @click="asyncIncrement">异步+1</button>
  </div>
</template>

<script>
import { mapState, mapActions } from 'vuex';

export default {
  computed: {
    ...mapState(['count']) // 映射为计算属性
  },
  methods: {
    ...mapActions(['asyncIncrease']),
    increment() {
      this.$store.commit('INCREASE'); // 直接提交 mutation
    }
  }
};
</script>

三、核心功能详解

1. State 存储体系

// store/modules/cart.js
const state = () => ({
  items: [], // 购物车商品列表
  total: 0   // 总金额
});

2. Mutations 同步修改

// store/modules/cart.js
mutations: {
  ADD_ITEM(state, item) {
    state.items.push(item);
    state.total += item.price;
  },
  REMOVE_ITEM(state, itemId) {
    const index = state.items.findIndex(i => i.id === itemId);
    if (index !== -1) {
      state.total -= state.items[index].price;
      state.items.splice(index, 1);
    }
  }
}

3. Actions 异步处理

// store/modules/cart.js
actions: {
  fetchItems({ commit }) {
    // 模拟 API 请求
    setTimeout(() => {
      const items = [
        { id: 1, name: 'iPhone', price: 5000 },
        { id: 2, name: 'MacBook', price: 15000 }
      ];
      items.forEach(item => commit('ADD_ITEM', item));
    }, 500);
  }
}

4. Getters 数据加工

// store/modules/cart.js
getters: {
  itemCount(state) {
    return state.items.length;
  },
  discountTotal(state) {
    return state.total * 0.9; // 模拟九折优惠
  }
}

四、模块化架构实践

场景:电商系统状态管理

// store/index.js
import Vue from 'vue';
import Vuex from 'vuex';
import cart from './modules/cart';
import user from './modules/user';

Vue.use(Vuex);

export default new Vuex.Store({
  modules: {
    cart, // 购物车模块
    user  // 用户模块
  }
});
// store/modules/user.js
const state = () => ({
  isLoggedIn: false,
  info: null
});

const mutations = {
  LOGIN(state, userInfo) {
    state.isLoggedIn = true;
    state.info = userInfo;
  }
};

const actions = {
  login({ commit }, username) {
    // 模拟登录验证
    setTimeout(() => {
      const user = { name: username, age: 20 };
      commit('LOGIN', user);
    }, 1000);
  }
};

export default {
  state,
  mutations,
  actions
};

五、最佳实践

1. 严格模式开发

// store/index.js
export default new Vuex.Store({
  strict: true, // 禁止直接修改 state
  modules: {
    cart,
    user
  }
});

2. 辅助函数完全指南

<!-- 使用 map 系列辅助函数 -->
<script>
import { mapState, mapGetters, mapActions, mapMutations } from 'vuex';

export default {
  computed: {
    ...mapState({
      cartItems: state => state.cart.items
    }),
    ...mapGetters({
      totalPrice: 'cart/discountTotal'
    })
  },
  methods: {
    ...mapActions('cart', ['fetchItems']),
    ...mapMutations('user', ['LOGIN'])
  }
};
</script>

3. 命名空间管理

// 访问带命名空间的模块
this.$store.dispatch('cart/fetchItems');
this.$store.commit('user/LOGIN', userData);

六、完整购物车示例

目录结构

src/
├── store/
│   ├── index.js
│   ├── modules/
│   │   ├── cart.js
│   │   └── user.js
├── components/
│   ├── CartList.vue
│   ├── UserProfile.vue
│   └── Main.vue

CartList.vue

<template>
  <div>
    <h2>购物车</h2>
    <p>总价:{{ totalPrice | currency }}</p >
    <ul>
      <li v-for="item in cartItems" :key="item.id">
        {{ item.name }} - ¥{{ item.price }}
      </li>
    </ul>
    <button @click="removeItem(1)">删除 iPhone</button>
  </div>
</template>

<script>
import { mapState, mapGetters, mapMutations } from 'vuex';

export default {
  computed: {
    ...mapState('cart', ['items']),
    ...mapGetters('cart', ['discountTotal'])
  },
  methods: {
    ...mapMutations('cart', ['REMOVE_ITEM']),
    removeItem(id) {
      this.REMOVE_ITEM(id);
    }
  }
};
</script>

七、总结与建议

1. 适用场景

  • 多组件共享状态(如用户认证、主题设置)
  • 复杂数据流管理(如表单向导、多步流程)
  • 需要状态持久化的场景

2. 注意事项

  • 避免过度集中:简单状态可保留在组件内
  • 严格模式警告:防止意外修改状态
  • 性能优化:使用模块化减少watcher数量

3. 扩展学习

  • 结合 Vue Devtools 调试状态变化
  • 学习 Vuex Persistedstate 插件实现状态持久化
  • 对比 Redux 理解不同状态管理方案差异

通过本文的购物车案例,你已经掌握从基础状态管理到模块化架构的完整流程。在实际项目中,建议:

  1. 按业务领域划分模块
  2. 使用命名空间避免命名冲突
  3. 封装常用逻辑到 Vuex 插件
  4. 配合 TypeScript 增强类型安全(Vue2 需使用 Vuex 3 + TS)

🏖️ TanStack Router:搜索参数即状态!🚀🚀🚀

2025年6月4日 20:30

前言

之前介绍过 TanStack,最近发现 TanStack Router 更新了一篇博客,介绍了搜索参数解析的新理念!今天分享给大家~,下面是翻译之后的原文!

往期精彩推荐

正文

搜索参数即状态

搜索参数历来被视为二等状态。它们是全局的、可序列化的、可共享的——但在大多数应用程序中,它们仍然通过字符串解析、松散的约定和脆弱的实用工具拼凑而成。

即使是像验证 sort 参数这样简单的事情,也很快变得冗长:

const schema = z.object({
  sort: z.enum(['asc', 'desc']),
})

const raw = Object.fromEntries(new URLSearchParams(location.href))
const result = schema.safeParse(raw)

if (!result.success) {
  // 回退、重定向或显示错误
}

这种方法有效,但它是手动的且重复的。没有类型推断、与路由本身没有关联,而且一旦你想添加更多类型、默认值、转换或结构,它就会崩溃。

更糟糕的是,URLSearchParams 只支持字符串。它不支持嵌套 JSON、数组(除了简单的逗号分割外)或类型强制转换。因此,除非你的状态是扁平且简单的,否则你很快就会遇到瓶颈。

这就是为什么我们开始看到工具和提案的兴起——比如 Nuqs、Next.js RFCs 和用户自定义模式——旨在使搜索参数更具类型安全性和人体工程学。这些大多专注于改进从 URL 的 读取

但几乎没有哪一个解决了更深层次、更困难的问题:写入 搜索参数,以安全和原子化的方式,充分了解路由上下文。

写入搜索参数是问题所在

从 URL 读取是一回事。从代码中构建一个有效且有意的 URL 是另一回事。

当你尝试这样做时:

<Link to="/dashboards/overview" search={{ sort: 'asc' }} />

你会意识到你根本不知道这个路由支持哪些搜索参数,或者你是否正确地格式化了它们。即使有助手来将它们字符串化,也没有任何机制来强制调用者和路由之间的契约。没有类型推断、没有验证、没有护栏。

这就是 约束成为特性 的地方。

如果不在路由本身中明确声明搜索参数模式,你就只能猜测。你可能在一个地方进行了验证,但没有什么能阻止另一个组件使用无效、部分或冲突的状态进行导航。

约束是协调成为可能的关键。它使 非本地调用者 能够安全参与。

本地抽象有所帮助 —— 但它们无法协调

Nuqs 这样的工具是本地抽象如何改善搜索参数处理 人体工程学 的绝佳例子。你可以获得基于 Zod 的解析、类型推断,甚至是可写的 API——所有这些都限定在特定组件或钩子中。

它们使在 隔离 中读写搜索参数变得更容易——这很有价值。

但它们无法解决更广泛的 协调 问题。你仍然会遇到重复的模式、分散的期望,以及无法在路由或组件之间强制一致性的问题。默认值可能冲突。类型可能漂移。当路由演变时,没有什么能保证所有调用者都会随之更新。

这才是真正的碎片化问题——解决它需要将搜索参数模式引入路由层本身。

TanStack Router 如何解决这个问题

TanStack Router 提供了整体解决方案。

你无需在应用程序中分散模式逻辑,而是 在路由本身中定义它

export const Route = createFileRoute('/dashboards/overview')({
  validateSearch: z.object({
    sort: z.enum(['asc', 'desc']),
    filter: z.string().optional(),
  }),
})

这个模式成为唯一的真相来源。你在任何地方都能获得完整的推断、验证和自动补全:

<Link
  to="/dashboards/overview"
  search={{ sort: 'asc' }} // 完全类型化,完全验证
/>

想只更新部分搜索状态?没问题:

navigate({
  search: (prev) => ({ ...prev, page: prev.page + 1 }),
})

它是 reducer 风格的、事务性的,并直接与路由器的响应性模型集成。组件仅在它们使用的特定搜索参数发生变化时才会重新渲染——而不是每次 URL 发生变化时。

TanStack Router 如何防止模式碎片化

当你的搜索参数逻辑存在于用户空间——分散在钩子、实用工具和助手函数中——不可避免地会出现 冲突的模式

也许一个组件期望 sort: 'asc' | 'desc'。另一个添加了 filter。第三个假定 sort: 'desc' 为默认值。它们之间没有共享的真相来源。

这会导致:

  • 不一致的默认值
  • 冲突的格式
  • 设置了其他组件无法解析的值的导航
  • 损坏的深度链接和无法追踪的错误

TanStack Router 通过将模式直接绑定到路由定义——以 层级方式 防止这种情况。

父路由可以定义共享的搜索参数验证。子路由继承该上下文,以类型安全的方式添加或扩展它。这使得在应用程序的不同部分意外创建重叠、不兼容的模式变得 不可能

示例:安全的分层搜索参数验证

以下是实际操作方式:

// routes/dashboard.tsx
export const Route = createFileRoute('/dashboard')({
  validateSearch: z.object({
    sort: z.enum(['asc', 'desc']).default('asc'),
  }),
})

然后,子路由可以安全地扩展该模式:

// routes/dashboard/$dashboardId.tsx
export const Route = createFileRoute('/dashboard/$dashboardId')({
  validateSearch: z.object({
    filter: z.string().optional(),
    // ✅ `sort` 从父路由自动继承
  }),
})

当你匹配 /dashboard/123?sort=desc&filter=active 时,父路由验证 sort,子路由验证 filter,一切无缝协作。

尝试在子路由中将所需的父参数重新定义为完全不同的内容?会触发类型错误。

validateSearch: z.object({
  // ❌ 类型错误:布尔值无法扩展父路由的 'asc' | 'desc'
  sort: z.boolean(),
  filter: z.string().optional(),
})

这种强制执行使嵌套路由既可组合又安全——这是一种罕见的组合。

内置纪律

这里的魔法在于,你无需教导团队遵循约定。路由 拥有 模式。每个人只需使用它。没有重复。没有漂移。没有无声的错误。没有猜测。

当你将验证、类型化和所有权引入路由器本身时,你就不再将 URL 当作字符串,而是开始将其视为真正的状态——因为它们就是状态。

搜索参数即状态

大多数路由系统将搜索参数视为事后补充。你 可以 读取它们,也许可以解析,也许可以字符串化,但很少有你真正可以 信任 的东西。

TanStack Router 颠倒了这一观念。它使搜索参数成为路由契约的核心部分——经过验证、可推断、可写且具有响应性。

因为如果你不将搜索参数视为状态,你就会不断泄露它、破坏它并绕过它。

最好从一开始就正确对待它。

如果你对将搜索参数作为一等状态的可能性感到好奇,我们邀请你尝试 TanStack Router。体验在路由逻辑中验证、可推断和响应性搜索参数的力量。

最后

搜索参数很可能导致类型安全和协调等问题。TanStack Router 提供了一种解决方案,将搜索参数模式整合到路由定义中,实现验证、推断和响应性,从而防止模式碎片化并确保安全的分层验证!

原文链接:tanstack.com/blog/search…

往期精彩推荐

从原生 JS 到 Vue 和 React:前端开发的进化之路

作者 红衣信
2025年6月4日 18:19

在前端开发的发展历程中,技术的迭代日新月异。从最初的原生 JS 到如今流行的 Vue 和 React 框架,前端开发变得更加高效和便捷。

一、原生 JS 时代

1.1 语义化标签

在原生 JS 开发中,语义化标签是构建页面结构的基础。比如在展示图表和表格时,通常会使用 <table> 标签,其结构包含 <thead><tr><th><tbody><tr><td> 等。这些标签不仅能让页面结构更加清晰,还便于老板等非技术人员查看数据。示例代码如下:

<table>
    <thead>
        <tr>
            <th>姓名</th>
            <th>家乡</th>
        </tr>
    </thead>
    <tbody>
        <tr>
            <td>张三</td>
            <td>北京</td>
        </tr>
    </tbody>
</table>

1.2 DOM 编程

原生 JS 中,DOM 编程是核心部分。通过操作 DOM 节点,我们可以动态更新页面内容。例如,当需要根据用户输入或数据变化更新表格内容时,就需要使用 DOM 操作。但这种方式代码量较大,且维护起来比较困难。

1.3 样式与用户体验

在原生 JS 开发中,样式编写往往需要关注很多细节,容易出现重复代码。为了提升用户体验,我们可以引入第三方库,如 Bootstrap。它是一个 PC 端的 CSS 框架,提供了栅格系统等功能,能让我们更专注于业务逻辑。

二、从原生 JS 到框架的转变

2.1 jQuery 的兴衰

在早期的前端开发中,jQuery 发挥了重要作用,它简化了 DOM 操作和事件处理。然而,随着前端技术的发展,jQuery 逐渐退出了历史舞台。现代前端框架提供了更强大的功能和更高效的开发方式。

2.2 Vue:聚焦业务开发

Vue 是一款轻量级的前端框架,它让开发者能够聚焦于业务逻辑。以展示 friends 数据为例,在原生 JS 中需要手动操作 DOM 来循环输出 <tr> 元素,而在 Vue 中可以使用 v-for 指令轻松实现。示例代码如下:

<div id="app">
    <table>
        <thead>
            <tr>
                <th>姓名</th>
                <th>家乡</th>
            </tr>
        </thead>
        <tbody>
            <tr v-for="friend in friends" :key="friend.id">
                <td>{{ friend.name }}</td>
                <td>{{ friend.hometown }}</td>
            </tr>
        </tbody>
    </table>
</div>

<script>
    const app = Vue.createApp({
        data() {
            return {
                friends: [
                    { id: 1, name: '王同学', hometown: '九江' },
                    { id: 2, name: '刘同学', hometown: '赣州' }
                ]
            };
        }
    });

    app.mount('#app');
</script>

在这个例子中,我们使用 v-for 指令遍历 friends 数组,并动态生成 <tr> 元素。这样就无需手动操作 DOM,大大提高了开发效率。

2.3 React:适合大型应用的框架

React 是 Facebook 开发的一款前端框架,适合构建大型应用。创建 React 应用可以使用 npm init vite 命令。React 采用组件化开发模式,将页面拆分成多个独立的组件,每个组件有自己的状态和逻辑,便于维护和复用。

三、现代前端开发框架的优势

3.1 离开原生 JS 的“刀耕火种”

现代前端框架让开发者摆脱了原生 JS 中繁琐的 DOM 操作和复杂的代码逻辑,提供了更高效、更简洁的开发方式。

3.2 聚焦业务

现代前端框架引入了 App 的概念,将开发者从“切图崽”的角色转变为 App 应用开发工程师。例如在 Vue 中,可以使用 Vue.createApp(App).mount('#app') 来接管页面,使用高级的框架特性完成业务需求,而无需使用低级的 DOM API。

3.3 循环输出数据的便捷性

在 Vue 和 React 中,循环输出数据变得非常简单。Vue 可以使用 v-for 指令,React 可以使用 map 方法。这种方式让代码更加简洁易读,也便于维护。

四、总结

从原生 JS 到 Vue 和 React,前端开发经历了巨大的变革。现代前端框架让开发者能够更专注于业务逻辑,提高开发效率和代码质量。无论是小型项目还是大型应用,都可以根据需求选择合适的框架。希望本文能帮助大家更好地理解前端开发的进化之路,在实际开发中做出更合适的选择。

❌
❌