Published on

螺丝是怎么拧成的

Authors
  • avatar
    作者
    Quelennuii

目录

vue2

路由跳转

 private jumpToTerminal (ip) {
    this.$router.push({ path: '/server/terminal', query: { ip } });
  }

  private ip = this.$route.query.ip

:star:调用函数中,有几句不执行,通过点击事件传参

<i class="el-icon-refresh-left" @click="reset(true)" />

public reset (clickFlag = false) {
    //不执行的
    if (!clickFlag) {
      this.activeForm = defaultForm; // 切回灰度图
    }
    //需要执行的
    this.isEdit = false;
    ……
}

websocket

挂载在 main.js

  // 建立链接
  if (!(store.getters['websocket/getExhibitWs'])) {
    store.dispatch('websocket/initExhibitSocket');
  }

监听输入框改变

上面的选择框决定下面的输入框内容:输入框@change 事件,不要用 watch 监听

传值双向绑定

@PropSync('formModel', { type: Object })
private formModelData!: MatchRelationModel

:visibility.sync = visible

祖孙传值

都是写在中间层

v-bind="$attrs"用于祖向孙传

v-on="$listeners"用于孙向祖传

网页终端 xterm

    "xterm": "^5.1.0",
    "xterm-addon-attach": "^0.8.0",
    "xterm-addon-fit": "^0.7.0"
<template>
  <div id="terminal" />
</template>

<script lang="ts">
import Vue from 'vue';
import { Component } from 'vue-property-decorator';
import { Terminal } from 'xterm';
import { FitAddon } from 'xterm-addon-fit';
import { AttachAddon } from 'xterm-addon-attach';
import 'xterm/css/xterm.css';

@Component
export default class WebTerminal extends Vue {
  private ip: string=this.$route.query.ip as string

  //不这么写会报错
  private term: Terminal|null=null
  private socket: WebSocket|null = null

  private mounted () {
    const term = new Terminal();
    const fitAddon = new FitAddon();
    const socket = new WebSocket(`ws://${window.location.host}${window.location.pathname}/api/ws?ip=${this.ip}`);
    const attachAddon = new AttachAddon(socket); // 建立websocket链接

    term.loadAddon(attachAddon);
    term.loadAddon(fitAddon);
    term.open(document.getElementById('terminal') as HTMLElement);
    fitAddon.fit();// 适应页面大小
    term.focus();
    socket.onopen = () => { socket.send('\n'); }; // 当连接建立时向终端发送一个换行符,不这么做的话最初终端是没有内容的,输入换行符可让终端显示当前用户的工作路径

    window.onresize = function () { // 窗口尺寸变化时,终端尺寸自适应
      fitAddon.fit();
    };

    this.term = term;
    this.socket = socket;
  }

  private beforeDestroy () {
    this.socket && this.socket.close();
    //不这么写会报错
    this.term && this.term.dispose();
  }
}

</script>

<style lang="scss" scoped>
  #terminal{
    height: 100%;
  }
</style>

动态绑定 class(多条件)

          :class="{ 'not-use': item.system_id === 0,'lock': item.lock }"

not-use 为类名,item.system_id === 0 为执行该类的条件

判断对象是否为空

 //方法一:for in 循环
function isEmptyObject(obj) {
//key表示前面的name  如果要获取value则 obj[key]
    for (key in obj) {
        return false
    }
    return true
}
//方法二:ES6 的 Object.keys()  -->获取对象身上的所有value
function isEmptyObject(obj) {
    var arr =Object.keys(obj)
    if(arr.length==0)return true
    return false
}

根据 id 查找对象并把找到的对象存入新数组

data 里存放所有数据,selectedIds 存放查找的 id

this.selectedIds.forEach((item) => {
      const selectItem = this.data.find((x) => x.id === item);
      if (selectItem) {
        this.selectList.push(selectItem);
      }
    });

将 value 相同的对象合并

   // 合并camera名称相同的对象
    const dataInfo = {};
    this.selectList.forEach((item) => {
      const { camera } = item;
      if (!dataInfo[camera]) {
        dataInfo[camera] = {
          ...defaultForm,
          camera,
          point_name: []
        };
      }
      dataInfo[camera].point_name.push(item.point_name);
      const sortPointName = dataInfo[camera].point_name.map(Number).sort((a, b) => { return a - b; });
      // 字符串转换为数字后,001会变成1,要补零
      const sortList = sortPointName.map(String).map((item) => item.padStart('3', 0));

v-for 循环导致表单验证失效

数据结构

1683712928886 这里的 model 必须绑定一个对象,所以给数组外面套了一个

    <el-form
      ref="form"
      :model="formModel"
    >
        <el-form-item
          label="阈值"
          :prop="`pointList.${index}.threshold`"
          :rules="{ required: true, pattern: /^(?:(?:[1-9](?:\.[0-9])?)|0\.[1-9]|10|10.0)$/, message: '输入范围为0.1-10' }"
        >
          <div class="threshold-input">
            <basic-input
              v-model="item.threshold"
            />
          </div>
        </el-form-item>

prop 传参有延迟

直接点击事件传参,定义变量接受 子组件 list 触发

  private toEdit (id: number) {
    this.$emit('setParameter', id);
  }

父组件

  public setParameter (id: number) {
    if (typeof id === 'number') {
      this.$refs.parameterDialog.editParameter(id);
      return;
    }
    if (!this.selectedIds.length) {
      this.$message.error('请选择要配置的数据');
      return;
    }
    this.$refs.parameterDialog.batchEdit();
  }

一行有两个 input,不用单独写样式

注意得写到一个 div 里

.input-item{
  display: flex;
  justify-content: space-between;
  ::v-deep .el-input {
    &:first-child {
      width: 20%;
    }
    &:nth-child(2) {
      width: 75%
    }
  }
}

路由带参数传递,参数为 number 格式

需要改成 string

  private toDetail (id: number) {
    this.$router.push({ path: '/update/detail', query: { id: id.toString() } });
  }

单独调用分页组件,发现挂载时会额外发送一次请求

系统将初始化判定为页码改变

解决:

      <pagination
        v-if="total"
        :page-params="pageParams"
        :total="total"
        page-layout="prev, pager, next"
        class="pagination"
        @current-change="handlePageChange"
      />

序号从 1 开始

      <el-table-column
        min-width="15%"
        label="调整后序号"
      >
        <template slot-scope="{$index}">
          {{ $index+1 }}
        </template>

sortable.js 拖拽

row-key="id"必须写

   <basic-table
      border
      :data="dataList"
      row-key="id"
    >
      <el-table-column
        prop="id"
        min-width="15%"
        label="调整前序号"
      />
      <el-table-column
        min-width="15%"
        label="调整后序号"
      >
        <template slot-scope="{$index}">
          {{ $index+1 }}
        </template>
      </el-table-column>
      <el-table-column
        prop="name"
        min-width="57%"
        label="数据组名称"
      >
        <template slot-scope="{row}">
          <text-popover :content="row.name" />
        </template>
      </el-table-column>
      <el-table-column
        min-width="13%"
        class-name="drag"
      >
        <template>
          <i
            class="iconfont icon-menu"
          />
        </template>
      </el-table-column>
    </basic-table>

引入

import Sortable from 'sortablejs';

注意 nexttick

  // open方法
  public async open () {
    this.visible = true;
    // 防止初次打开时获取不到dom元素
    this.$nextTick(() => {
      this.setSort();
    });
  }
  private setSort () {
    const tbody = document.querySelector('.el-table__body-wrapper tbody');
    const _this = this;
    Sortable.create(tbody, {
      // 仅class-name=“drag”那一列可拖拽
      handle: '.drag',
      animation: 180,
      delay: 0,
      onEnd ({ newIndex, oldIndex }) {
        const currRow = _this.dataList.splice(oldIndex, 1)[0];
        _this.dataList.splice(newIndex, 0, currRow);
        // _this.dataList.forEach((item, index) => {
        //   this.newList[index] = {
        //     id: index + 1,
        //     name: item.name
        //   };
        // });
        this.newList = _this.dataList;
      }
    });
  }

修改表格样式(child 伪元素)

某一列

::v-deep .el-table thead tr th:nth-child(2) .cell, ::v-deep .el-table .el-table__row td:nth-child(2) .cell {
   display: flex;
   justify-content: center;
 }
 ::v-deep .el-table thead tr th:last-child .cell, ::v-deep .el-table .el-table__row td:last-child .cell {
   display: flex;
   justify-content: flex-end;
   .iconfont {
     margin-right: 0;
   }
 }

多列修改

::v-deep .el-table thead tr, ::v-deep .el-table .el-table__row {
  td:first-child{
    color: $color-input
  }
  th:nth-child(2) .cell,td:nth-child(2) .cell{
    display: flex;
    justify-content: center;
  }
  td:last-child {
    .cell {
    display: flex;
    justify-content: flex-end;
      .iconfont {
        margin-right: 0;
      }
    }
  }
  td:nth-child(3),th:nth-child(3){
      border-right: 0 !important;
  }
}

选项循环

需求:这样写太麻烦

  private statusOptions: Option[] = [
    { value: 1, label: 1 },
    { value: 2, label: 2 },
    { value: 3, label: 3 }
  ];

this.carriageList = new Array(6).fill(null).map((i, index) => {
      return {
        label: `${index + 1}车`,
        value: (index + 1)
      };
    });

路由监听

  @Watch('$route')
  private offlineChanged (to, from) {
    const offlineDetailPath = '/offline/taskRecord';
    const taskDetailPath = '/tasks/taskRecord';
    const taskPath = '/tasks';
    const offlinePath = '/offline';
    // 不是从任务详情跳转回任务记录、离线详情跳转回离线记录,刷新列表
    if ((from.path === taskDetailPath && to.path === taskPath) || (from.path === offlineDetailPath && to.path === offlinePath) || to.name === '任务详情') {
      return;
    }
    this.taskListData = [];
    this.searchForm.reset();
    // this.getTaskData();
  }

表格跳转到第一行

表格记得写 ref

  public backToTop () {
    (this.$refs.table as any).bodyWrapper.scrollTop = 0;
    (this.$refs.table as any).bodyWrapper.scrollLeft = 0;
  }

vue3

可能为 undefined 的时候八成是又忘了写.value 了

图片缩放

以宽度为标准缩放图片

动态绑定 width

          <div
            v-for="(cameraInfo, index) in cameraInfoList"
            :key="index"
            :class="`imgs-content-item ${activeCamera.length ? 'one-camera' : ''}`"
            :style="`width:${imgWidth}`"
          >

设定宽度

const photoContentWidth = ref<number>(0); // photo-content的宽

计算图片尺寸

// 一个相机图片的高
const imgHeight = computed(() => (activeCamera.value.length ? photoContentHeight.value : photoContentHeight.value / 2));
const imgWidth = computed(() => (activeCamera.value.length ? photoContentWidth.value : photoContentWidth.value / 2));

加载时计算缩放比例

const imageLoaded = (e: any) => {
  imageRate.value = e.target.naturalWidth / e.target.naturalHeight;
  imageRate.value = e.target.naturalHeight / e.target.naturalWidth;
  naturalWidth.value = e.target.naturalWidth;
  naturalHeight.value = e.target.naturalHeight;
  resetActivePoint(); // 图片有时加载缓慢,需要重新选中
      activeCameraOptions.value.unshift({ value: '', label: '', icon: 'icon-menu' });
    }
    // 避免有些时候没有高度
    !photoContentHeight.value && updateContentStyle();
    !photoContentWidth.value && updateContentStyle();
  },
  { deep: true }
);

监听

watch(
  () => drawerStyleWidth.value,
  () => drawerStyleHeight.value,
  async (newValue: number) => {
    const xRate = newValue / naturalWidth.value;
    const yRate = imgHeight.value / naturalHeight.value;
    await imageDrawersOperation('updateCanvasStyle', newValue, imgHeight.value, xRate, yRate);
    const xRate = imgWidth.value / naturalWidth.value;
    const yRate = newValue / naturalHeight.value;
    await imageDrawersOperation('updateCanvasStyle', imgWidth.value, newValue, xRate, yRate);
    // 再次选中
    resetActivePoint();
  }
);

图片缩放,依据高度计算宽度横竖颠倒后缩放比例不对:因为 rate 计算没有修改

计算属性

带参数的计算属性

const imageDrawerSrc = computed(() => (cameraInfo: any) => {
  const leftPhotoSrc =
    activeForm.value === ActivePhotoForm.灰度图 ? cameraInfo.texture_image_url : cameraInfo.rgb_image_url;
  return leftPhotoSrc;
});

不带参数的计算属性

const isGreyOrColor = computed(
  () => activeForm.value === ActivePhotoForm.灰度图 || activeForm.value === ActivePhotoForm.彩色图
);

左右按钮切换的值以及事件传递给父级

    <i
      :class="`iconfont icon-expandleft ${activeSide === 'left' ? 'active-side' : ''}`"
      @click="setActiveSide('left')"
    />
    <i
      :class="`iconfont icon-expandright ${activeSide === 'right' ? 'active-side' : ''}`"
      @click="setActiveSide('right')"
    />

设置样式

  .active-side {
    color: $color-primary;
  }

初始化并暴露

const activeSide = ref<string>('');

type EmitType = {
  (e: 'setActiveSide', activeSide: string): void;
};
const emit = defineEmits<EmitType>();

const setActiveSide = (side: string) => {
  activeSide.value = side;
};

watch(displayShapes, () => {
  emit('setDisplayShapes', displayShapes.value);
});

defineExpose({
  activeSide
});

父组件接受

          <drawer-operation
            ref="drawerOperationRef"
            @setActiveSide="setActiveSide"
          />

调用

// 左右拓展
const setActiveSide = (activeSide: string) => {
  imageDrawersOperation('setActiveSide', activeSide);
};

读取子组件中的数据:drawerOperationRef.value.activeSide = '';

对象 key 不定的时候读取变量

对象:

list = [
 {
  id:1,
  left_extend_image_url:qwer
 },
 {
  id:3,
  right_extend_image_url:asdf
 },
  {
  id:2,
  left_extend_image_url:zxcv
 },
]

读取 id 用 list.id,但是_extend_image_url 是个变量,不能用. 来读,用

    if (respItem.length) {
      item[`${changeKey}_extend_image_url`] = respItem[0][`${changeKey}_extend_image_url`];
    }

按钮切换状态

需求,两个按钮只能激活一个,并且点一下激活点两下重置

1679557068820

    <i
      :class="`iconfont icon-expandleft ${activeSide === 'left' ? 'active-side' : ''}`"
      @click="setActiveSide('left')"
    />
    <i
      :class="`iconfont icon-expandright ${activeSide === 'right' ? 'active-side' : ''}`"
      @click="setActiveSide('right')"
    />
const setActiveSide = (side: string) => {
 activeSide.value = activeSide.value === side ? '' : side;
};

不可以赋先在判断,因为可能点完左再点右

defineExpose 父调用子方法和变量

在 setup 函数中,存在 defineExpose 语法糖,用于完成子向父传参数,不调用方法

子组件暴露数据:

defineExpose({
  activeSide
})

父组件定义 ref 用于调用

          <drawer-operation
            ref="drawerOperationRef"
          />
const drawerOperationRef = ref<InstanceType<typeof DrawerOperation>>();

调用直接drawerOperationRef.value.activeIndex,调用方法的话在后面加上括号

defineEmit 子调用父方法和变量

父组件定义

const handleDeletePhoto = async (id: number, stop_frame_id: number) => {
  const stopTableDetailRef = stopTableDetailRefs.value[stop_frame_id];
  if (stopTableDetailRef) {
    await stopTableDetailRef?.batchDelete(id);
  }
};

父组件传递

  <image-detail-dialog
    ref="imageDetailDialogRef"
    @handle-delete-photo="handleDeletePhoto"
  />

子组件定义

type EmitType = {
  (e: 'handleDeleteItem', id: number): void;
};
const emit = defineEmits<EmitType>();

子组件调用

const handleDelete = () => {
  emit('handleDeleteItem', photoItem.value?.id as number);
};

祖孙组件通信

provide inject(记得注册)

爷组件:

export default defineComponent({
  // ...
  setup () {
    // provide一个ref
    const msg = ref<string>('Hello World!');
    provide('msg', msg);
    // provide一个reactive
    const userInfo: Member = reactive({
      id: 1,
      name: 'Petter'
    });
    provide('userInfo', userInfo);
  }

孙组件:

export default defineComponent({
  setup () {
    // 获取数据
    const msg = inject('msg');
    const userInfo = inject('userInfo');
    // 打印刚刚拿到的数据
    console.log(msg);
    console.log(userInfo);
  }
})

给数组添加 value 属性

headFrameOptions 是个数组,但是选项需要 label 和 value 两个属性

const open = () => {
  headFrameOptions.value = props.headFrameList?.map((item, index) => {
    return {
      value: index,
      label: item
    };
  });
  dialogFormRef.value?.open();
};

表格里拼接字符串

    <el-table-column
      prop="carriage_name"
      label="关联车厢"
    >
      <template #default="{ row }">{{ row.carriage_name }} 车厢</template>
    </el-table-column>

动态 ref

解决展开后的 table 绑顶的是相同数据

          <stop-table
            :ref="el => setStopDetailRef(el, item.id)"
            :region-id="item.id"
          />
const stopTableRefs = ref<any>({});
const setStopDetailRef = (el: any, id: number) => {
  if (el) {
    stopTableRefs.value[id] = el;
  }
};
// 遍历取出各个ref中所有的id并拼接
const allStopIds = computed<number[]>(() => {
  const ids: number[] = [];
  activeNames.value.map((regionId: number) => {
    if (stopTableRefs.value[regionId]?.allDataIds.length) {
      ids.push(...stopTableRefs.value[regionId].allDataIds);
    }
  });
  return ids;
});

折叠后选择和 allid 都在

调用 reset 方法无效,因为用的的引入的 allIds 和 selectedIds,本质上是一样的,所以这里不能引入,要单独定义

最里层折叠有问题

在 get 请求的时候,需要赋值一个变量 disexpand,不然表格无法判断是展开还是折叠

子组件传给父组件没问题,父组件转发时 undefined

get 请求先后顺序,父组件转发时子组件还没赋值。

解决: watch 看变量的变化,变了就赋值给新变量然后传递

第一行展开获取不到数据,unmounted 里没法读到 prop 的数据,传过来的数组显示无定义

因为子组件加载比父组件早,所以传值时父组件还没有值

解决:定一个新变量=prop.regionlist,虽然会显示不是 ref 但是后面都调用这个就都没问题了

不是 ref 可以通过 toRef 解决

阻止点击事件冒泡

@click.stop写在子级阻止冒泡

图片详情弹窗层级混乱

详情列表包括图片列表和图片弹框,渲染时每一行都会拥有自己的图片弹窗,这时父级修改样式导致样式穿透

解决:弹窗写在表格层,而不是图片列表层

没有数据的时候展开折叠会频繁掉接口

在 get 请求里写一个 flag,默认为 false,调用后变为 true,只有为 false 时才调用接口

删除后弹窗刷新数据,并改变 activeindex

第一个后移,中间的前移,没有直接关闭

// 删除时候需要监听 用于刷新数据
watch(
  () => photoList.value.length,
  () => {
    activeIndex.value = !activeIndex.value ? activeIndex.value : activeIndex.value - 1;
    imageSrc.value = photoItem.value?.texture_image_url as string;
    dialogCancel();
    !photoList.value.length && cancel();
  }
);

带有默认值的 props

const props = withDefaults(defineProps<{ photoList: PhotoListModel[] }>(), {
  photoList: () => []
});

显示 undefined

提前判断 存在才走

if(){
return
}

v01.2.1.zip 提取名字(v01.2.1)

不能用点分割,不然会变成 v01

  const lastPointIndex = file.name.lastIndexOf('.');
  // 上传文件的格式
  const splitFormat = file.name.substring(lastPointIndex + 1, file.name.length);
  // 上传文件的名字
  const splitName = file.name.substring(0, lastPointIndex);

用 key 强制刷新组件

          <cy-image-collapse
            v-if="!index && standardImageList.length"
            :key="key"
            :min-height="159"
            :max-height="520"
          >
            <div class="image-list">
              <image-item
                v-for="(item, index) in standardImageList"
                :key="index"
                :data="item"
                @click="itemClick(index)"
              />
            </div>
          </cy-image-collapse>
const searchImage = async () => {
  await getStandardImageList();
  // 获取数据后刷新组件
  key.value++;
};

枚举

enum LineStatus {
  进行中,
  已通过,
  未通过,
  忽略测试结果
}

因为枚举默认从 0 开始,并且累加,写成这样也可以

enum LineStatus {
  进行中=0,
  已通过=1,
  未通过=2,
  忽略测试结果=3
}

应用:

        <span
          :class="
            row.test_result === LineStatus.已通过 ? 'pass' : row.test_result === LineStatus.未通过 ? 'failed' : ''
          "
        >

变量可以用

{{ LineStatus[row.test_result] }}

读取

文字垂直

      writing-mode: vertical-lr;
      text-orientation: upright;
      letter-spacing: 4px;

draggable.js

import draggable from 'vuedraggable';

  <draggable
            :list="item.component"
            :animation="200"
            item-key="name"  //必须写
            class="component"
          >
          //插槽必须写 并且只能含一个子标签,哪怕是注释也不行
            <template #item="{ element }">
              <div class="component-item">
                <div class="top" />
                <div class="name">{{ element.name }}</div>
              </div>
            </template>
          </draggable>

表格跳转到最后一行

表格记得写 ref

const jumpToBottom = () => {
  setTimeout(() => {
    const ScrollTop = 55 * props.data.length;
    //1、50为每一行的高度;2、tableData.data.length为数据数量
    table.value?.setScrollTop(ScrollTop);
    table.value?.setScrollLeft(0);
  }, 100);
};

TS 语法

非必传参数用?:,有时候读不到参数就

组件传值不用private改成public

判断字符串是否为空,不用drawerOperationRef.value?.activeSide===''drawerOperationRef.value?.activeSide.length

在父组件里设置子组件的ref=drawerOperationRef,可以直接读取子组件中的数据:drawerOperationRef.value.activeSide ;

代码优化

length 判断不用if (ids.length>1) {},用if (ids.length) {}

用三目运算,不用 if else

可以提前 return 就 return 出去

有的时候用 some 或者 every 可以少写一层 if

git

合并远程 master 上的代码

git pull origin master

撤回已经 push 的代码

git log查看需要撤回的版本的上一个版本的版本号

git reset 版本号 --soft

git push origin xxx --force

正则表达式

0.1-10:/^(?:(?:[1-9](?:\.[0-9])?)|0\.[1-9]|10|10.0)$/

正整数:/^[+]{0,1}(\d+)$/

1-99 整数:/^(([1-9][0-9])|[1-9])$/

正整数:/^[0-9]*[1-9][0-9]*$/

大小写,数字,(_ -.),10 个字符以内: /^[A-Za-z0-9-_.]{1,10}$/

React

发请求在 useEffect 里,[]就是挂载时发送

useState 太多了就用 useReducer,相当于一个 switch

严格模式下 useEffect 就是执行两次,不用管

set 的时候直接 push 数组没用,得...拼接

特别神奇,then 和 await 不一样,useEffect 不用 then 会出现数据加载不全的情况(感觉像请求还没完事但那边已经渲染完了)

没事别用 useContext

useColums 的时候不传弹窗组件,用父子事件的方式触发弹窗

less 的样式穿透是/deep/

tailwind 的 class 是有顺序的,找不到复制搜(为什么会有这么恶心的东西)

Next.js 实现 SSR 是通过 getServerSideProps 和 getStaticProps,getServerSideProps 类似于 getStaticProps,但两者的区别在于 getServerSideProps 在每次页面请求时都会运行,在构建时不运行,两者执行时机相反

Next.js 不允许在除 _app.js 文件以外的任何文件中导入全局 CSS,可以把 css 文件改名,.css 改为 .module.css 就能正常使用了,CSS Modules 允许你在组件级别导入 CSS,而不会影响全局样式,但是实际引入的时候那确实是不知道什么原因,反正引入不了,还是放在全局样式了,起了个不会被穿透的名字

npm 包

在 utils 里调用函数是可以被执行的,都得判断项目环境再搞

搞个 story 调试很方便

chrome 插件安装好才能调试,必须判断当前页面是不是 active

如果想要在页面中插入元素需要用原生 DOM 去写,啥 tsx react 的都不行

cssText 可以用字符串的形式定义样式,按钮得用 pointer 老忘

localstorage 读数据会跨域,searchURLParams 可以获取 url 里?后面的东西

keytoValue 不用映射到 i18n 给的那个存储对象上,直接 window[xx] = keytoValue

判断是否具备该功能是 url 和所处环境两个条件

ide 插件

调试的时候必须切换到子项目!!!!!cd 没用!!!!!!!!卡了四天啊!!!!!!!!!!我不是弱智谁是弱智!!!!!!!!!!!!!!

读文件用 node 的 fs 读,写也是,字符串正则匹配两次 replace

搜了一下还可以用 AST 搞,直接在打包的时候改不影响开发代码(听起来怎么比我们这个好)

如果文字回车了,不仅会/n,还会(很多空格)/n(很多空格),正则不好写