解析一段vue模板字符串

102 阅读6分钟

首先贴一下生成代码方法, 这里其实踩了很多坑, 最开始使用了自己解析模板, 但是出现各种兼容问题, 最后还是使用了vue的模板编译的功能实现的, 自己写的话太坑了

import * as Vue from 'vue'
import {compile} from '@vue/compiler-dom'
const handleVueTemplate = (internalValue, cb) => {
  // 提取模板字符串中的各部分
  const templateMatch = internalValue.match(/<template>([\s\S]*?)<\/template>/i);
  const scriptMatch = internalValue.match(/<script>([\s\S]*?)<\/script>/i) ||
    internalValue.match(/<script setup>([\s\S]*?)<\/script>/i);
  const styleMatch = internalValue.match(/<style[^>]*>([\s\S]*?)<\/style>/i);

  let template = templateMatch ? templateMatch[1] : '';
  let script = scriptMatch ? scriptMatch[1] : '';
  let style = styleMatch ? styleMatch[1] : '';

  // 创建预览HTML
  let previewContent = '';

  // 添加样式
  if (style) {
    previewContent += `<style>${style}</style>`;
  }

  // 如果有脚本,提取数据并应用到预览中
  let dataObj = {};
  if (script) {
    try {
      // 提取data部分 - 支持两种格式:data() { return {...} } 和 data: {...}
      let dataMatch = script.match(/data\s*\(\)\s*{\s*return\s*({[\s\S]*?});?\s*}/i);
      if (!dataMatch) {
        dataMatch = script.match(/data\s*:\s*({[\s\S]*?})[,\n]/i);
      }

      if (dataMatch && dataMatch[1]) {
        // 处理单引号和双引号的问题
        let dataStr = dataMatch[1];

        // 处理特殊情况:将单引号替换为双引号,但要避免替换已经转义的单引号
        dataStr = dataStr.replace(/(?<!\\)'/g, '"');

        // 处理数组中的逗号后面的空格
        dataStr = dataStr.replace(/,\s*([\]}])/g, ',$1');

        if (cb) {
          dataStr = cb(dataStr);
        }
        try {
          // 尝试解析data对象
          dataObj = new Function(`return ${dataStr}`)();
        } catch (e) {
          console.error('解析data对象失败:', e);
          console.error('尝试解析的数据字符串:', dataStr);
        }
      }
    } catch (e) {
      console.error('处理脚本失败:', e);
    }
  }

  // 处理模板内容
  if (template) {
    // 使用 @vue/compiler-dom 编译模板为渲染函数
    const { code } = compile(template, { mode: 'function', filename: 'user-template.vue' });
    const renderFn = new Function('Vue', `${code}; return render`)(Vue);
    const app = Vue.createApp({
      data() {
        return dataObj;
      },
      render: renderFn
    });
    const container = document.createElement('div');
    app.mount(container);
    previewContent += container.innerHTML;
  }

  // 设置预览内容
  return previewContent;
}

下面是测试这个方法

const str = "<template>
  <div class="financial-form">
    <h2 class="financial-title">海通证券财富管理中⼼固定收益证券现券交易申请表</h2>
    <table class="form-table">
      <!-- 申请信息 -->
      <tr>
        <td class="label form-item">申请人</td>
        <td class="data form-item">{{ requestUser }}</td>
        <td class="label form-item">联系电话</td>
        <td class="data form-item" colspan="3">--</td>
        <td class="label form-item">申请⽇期</td>
        <td class="data form-item">{{ dataList[0].requestDate }}</td>
      </tr>
      <tr>
        <td class="label form-item" colspan="2">产品名称</td>
        <td class="data form-item" colspan="6">{{ dataList[0].agencyAccountName }}</td>
      </tr>
      <!-- 交易内容 -->
      <tr>
        <td class="label">序号</td>
        <td class="label">代码</td>
        <td class="label">简称</td>
        <td class="label">交易方向</td>
        <td class="label">交易量(万)</td>
        <td class="label">交易净价/收益率</td>
        <td class="label">清算速度</td>
        <td class="label">对手方</td>
      </tr>
      <tr v-for="(item, index) in dataList" :key="item.bizInstNo">
        <td class="data">{{index + 1}}</td>
        <td class="data">{{ item.underlyingAssetDetails[0].firstAssetKey }}</td>
        <td class="data">{{ item.underlyingAssetDetails[0].firstAssetName }}</td>
        <td class="data">{{ item.underlyingAssetDetails[0].dealSide === '1' ? "买入" : "卖出" }}</td>
        <td class="data">{{ item.underlyingAssetDetails[0].firstTradeAmt }}</td>
        <td class="data">
          <div>{{
              item.underlyingAssetDetails?.[0].calMode === 0
                ? "净价"
                : item.underlyingAssetDetails[0].calMode === 1
                  ? "到期收益率"
                  : "行权收益率"
          }}</div>
          <div>{{
              item.underlyingAssetDetails[0].calMode === 0
              ? item.underlyingAssetDetails[0].firstCleanPrice
              : item.underlyingAssetDetails[0].calMode === 1
              ? item.underlyingAssetDetails[0].firstMatYield
              : item.underlyingAssetDetails[0].firstOptionYield
            }}</div>
        </td>
        <td class="data">{{
            item.underlyingAssetDetails[0].settleType === 1 ? "T+0"
             : item.underlyingAssetDetails[0].settleType === 2 ? "T+1"
             : item.underlyingAssetDetails[0].settleType === 3 ? "T+2"
             : item.underlyingAssetDetails[0].settleType === 4 ? "T+3"
             : item.underlyingAssetDetails[0].settleType === 5? "T+4"
             : "T+5"
          }}</td>
        <td class="data">{{ item.counterPartyName }}</td>
      </tr>

      <!-- 审核区域 -->
      <tr v-for="(approve, index) in approvedList" :key="index">
        <td colspan="2">{{approve.nodeName}}</td>
        <td colspan="6">{{approve.userNames}}</td>
      </tr>
    </table>
  </div>
</template>

<script>
export default {
  data() {
    return {
      "requestUser": "Admin",
      "approvedList": [
          {
            "nodeName": "审批节点1",
            "userNames": "王朔铖",
            "approveTime": "2025-05-19 14:04:47",
            "nodeId": null
          },
          {
            "nodeName": "审批节点2",
            "userNames": "王朔铖",
            "approveTime": "2025-05-19 14:04:56",
            "nodeId": null
          },
          {
            "nodeName": "审批节点2",
            "userNames": "王朔铖",
            "approveTime": "2025-05-19 14:04:56",
            "nodeId": null
          }
      ],
      "dataList": [
        {
          "marginSecurityDetails": [

          ],
          "settleState": null,
          "bizInstDesc": null,
          "pledgeMortgageArrangement": null,
          "cashAssignDetails": null,
          "bizInstState": 12,
          "bondRepoFlag": false,
          "expectTermDay": null,
          "realCounterPartyName": "",
          "outRepoType": 1,
          "bizInstSummary": "买入,21西藏债专项(八期)IB(Z21120852.IB),净价:100.0000元,交易面额:1,000.00万元(固收自营组合[银行间交易性]/1,000.00万元),到期收益率:0.0000%,T+012年第6期私银混合精选,结算金额:10,000,000.00",
          "counterTraderName": "",
          "marketPriceFlag": false,
          "requestUserId": "Admin",
          "agencyAccountNo": "Account-20250403-00002",
          "executeDate": null,
          "dataVal2": null,
          "requestDate": "2025-05-19",
          "selfTraderName": "",
          "dataVal1": null,
          "dataVal0": null,
          "originalExecId": null,
          "deliveryDate": "2025-05-19",
          "dataVal4": null,
          "dataVal3": null,
          "numVal4": null,
          "numVal3": null,
          "tradeRateType": null,
          "numVal6": null,
          "exposureAtDefault": null,
          "numVal5": null,
          "numVal0": null,
          "securityCode": null,
          "numVal2": null,
          "counterPartyName": "12年第6期私银混合精选",
          "numVal1": 1,
          "instFeeDetails": [
            {
              "channelType": "null",
              "channelCode": null,
              "feeType": "",
              "feeRate": 0,
              "feeAmt": 0,
              "dataVal0": null,
              "numVal0": null,
              "numVal1": null,
              "strVal0": null,
              "strVal1": null
            }
          ],
          "sedSettleAmt": null,
          "sedDeliveryType": null,
          "settleType": 1,
          "operateNo": null,
          "marginAmt": null,
          "bizInstNo": "BIZ-20250519-00201",
          "agencyAccountName": null,
          "disputeSolution": null,
          "dealSide": 1,
          "channelType": null,
          "fieldValueMap": null,
          "realCounterPartyCode": "",
          "sedDealType": null,
          "relatedExecId": null,
          "sedDeliveryDate": null,
          "tradePlatform": null,
          "interestAmt": 0,
          "openCloseFlag": null,
          "operateType": null,
          "apiContactDetail": {
            "quoteMode": "0",
            "quoteNo": "",
            "quoteValidTime": null,
            "initiator": "",
            "sendParty": "1",
            "investNo": "",
            "saleChannel": "",
            "tailDiffFlag": null,
            "redeemMode": "",
            "allRedeemFlag": null,
            "largeRedeemMode": "",
            "settledFlag": null,
            "swapType": null,
            "counterPartyType": "",
            "counterAccountNo": "",
            "counterSeatNo": "",
            "counterCompanyCode": "",
            "counterMemberCode": "",
            "dealerCode": "",
            "tradeCompanyCode": "",
            "promiseNo": "",
            "bonsMode": "",
            "dataVal0": null,
            "numVal0": null,
            "numVal1": null,
            "strVal0": null,
            "strVal1": null,
            "strVal2": null,
            "strVal3": null
          },
          "dealType": 223,
          "cashGap": null,
          "timeVal2": null,
          "timeVal0": null,
          "timeVal1": null,
          "tradeRate": null,
          "endTradeDate": "2025-05-19",
          "fstSettleAmt": 10000000,
          "performReward": null,
          "cashAccountNo": "900610000012001",
          "securityName": null,
          "penaltyInterest": null,
          "strVal5": null,
          "strVal4": null,
          "requestUserName": null,
          "settleDate": "2025-05-19",
          "strVal6": null,
          "relatedTradeIndexes": null,
          "strVal1": null,
          "counterPartyCode": "402401",
          "strVal0": null,
          "strVal3": null,
          "strVal2": null,
          "counterTraderCode": "",
          "profitPercent": null,
          "marketCode": "IB",
          "benchMark": null,
          "groupNo": null,
          "categorize": 0,
          "channelCode": null,
          "underlyingAssetDetails": [
            {
              "uniqueNo": "BIZ-20250519-00201",
              "firstAssignAmt": 10000000,
              "firstAssetKey": "Z21120852.IB",
              "firstOuterKey": null,
              "firstAssetName": "21西藏债专项(八期)IB",
              "firstAssetType": "BOND",
              "firstCleanPrice": 100,
              "firstAccruedInterest": 0,
              "firstDirtyPrice": 100,
              "firstMatYield": 0.00000000000007289964042575258,
              "firstOptionYield": 0,
              "firstTradeAmt": 10000000,
              "firstInterestAmt": 0,
              "firstSettleAmt": 10000000,
              "firstIssueSize": null,
              "calMode": 0,
              "changeField": null,
              "firstCustodyAgency": null,
              "firstCustAccNo": "665544",
              "firstAssignDetails": [
                {
                  "assignAmt": 10000000,
                  "occupyAssignAmt": null,
                  "portfolioNo": "P-20250308-00001",
                  "operateType": null,
                  "portfolioName": "固收自营组合[银行间交易性]",
                  "assetKey": "Z21120852.IB",
                  "outerKey": null,
                  "assetName": "21西藏债专项(八期)IB",
                  "assetType": "BOND",
                  "custodyAccountNo": "665544",
                  "useType": 1,
                  "convertRate": 1,
                  "principalAmt": null,
                  "interestAmt": null,
                  "feeAmt": null,
                  "dataVal0": null,
                  "numVal0": null,
                  "numVal1": null,
                  "strVal0": null,
                  "strVal1": null,
                  "underlyingSecurity": true,
                  "marginSecurity": false,
                  "remainAssignAmt": 10000000
                }
              ],
              "secondAssignAmt": null,
              "secondAssetKey": null,
              "secondOuterKey": null,
              "secondAssetName": null,
              "secondAssetType": null,
              "secondCleanPrice": null,
              "secondAccruedInterest": null,
              "secondDirtyPrice": null,
              "secondMatYield": null,
              "secondOptionYield": null,
              "secondTradeAmt": null,
              "secondInterestAmt": null,
              "secondSettleAmt": null,
              "secondCustAccNo": null,
              "secondAssignDetails": null,
              "innerCreditRate": null,
              "convertRate": 1,
              "rateInvalidDate": null,
              "currencyType": "CNY",
              "currencyRate": null,
              "bidRate": null,
              "dataVal1": null,
              "dataVal2": null,
              "dataVal3": null,
              "numVal1": null,
              "numVal2": null,
              "numVal3": null,
              "strVal1": null,
              "strVal2": null,
              "strVal3": null,
              "strVal4": null,
              "firstOccupyAssignAmt": 0,
              "secondOccupyAssignAmt": 0,
              "remainFirstAssignAmt": 10000000,
              "remainSecondAssignAmt": 0
            }
          ],
          "requestType": 223,
          "sedSettleDate": null,
          "deliveryType": 0,
          "occupyTrustFlag": null,
          "thirdPartyRenewalFlag": null,
          "tradeDate": "2025-05-19",
          "requestNo": "REQ-20250519-00301",
          "needCalFstSettleAmt": null,
          "selfTraderCode": "",
          "rateSpread": null,
          "actualTermDay": null,
          "clearingMode": 13,
          "totalAssignAmt": 10000000,
          "requestState": 2,
          "warehouseState": null,
          "firstOccupySettleAmt": 0,
          "class": "com.bocloud.ficc.pms.provider.api.core.dto.BizInstUsualIndexDTO",
          "remainFirstSettleAmt": 0,
          "transactionState": 0
        }
      ]
    };
  }
};
</script>

<style scoped>
.financial-form {
  font-family: Arial, sans-serif;
  max-width: 800px;
  margin: 20px auto;
}

.financial-title {
  font-size: 20px;
  font-weight: bold;
  text-align: center;
  margin-bottom: 6px;
}

.form-table {
  width: 100%;
  border-collapse: collapse;
  border: 2px solid #e1e1e1;
}

td {
  padding: 12px;
  border: 1px solid #e1e1e1;
}

td.form-item {
  padding: 6px 12px;
}

.label {
  width: 12.5%;
  text-align: center;
}

.data {
  width: 12.5%;
  text-align: center;
}

.section-header {
  font-size: 18px;
  font-weight: bold;
  text-align: center;
  padding: 15px;
}

.approval-section {
  min-height: 60px;
  padding: 10px;
}
</style>
"
const html = handleVueTemplate(str)
console.log(html)

生成的html代码

<style>
.financial-form {
  font-family: Arial, sans-serif;
  max-width: 800px;
  margin: 20px auto;
}

.financial-title {
  font-size: 20px;
  font-weight: bold;
  text-align: center;
  margin-bottom: 6px;
}

.form-table {
  width: 100%;
  border-collapse: collapse;
  border: 2px solid #e1e1e1;
}

td {
  padding: 12px;
  border: 1px solid #e1e1e1;
}

td.form-item {
  padding: 6px 12px;
}

.label {
  width: 12.5%;
  text-align: center;
}

.data {
  width: 12.5%;
  text-align: center;
}

.section-header {
  font-size: 18px;
  font-weight: bold;
  text-align: center;
  padding: 15px;
}

.approval-section {
  min-height: 60px;
  padding: 10px;
}
</style><div class="financial-form"><h2 class="financial-title">海通证券财富管理中⼼固定收益证券现券交易申请表</h2><table class="form-table"><!-- 申请信息 --><tr><td class="label form-item">申请人</td><td class="data form-item">Admin</td><td class="label form-item">联系电话</td><td class="data form-item" colspan="3">--</td><td class="label form-item">申请⽇期</td><td class="data form-item">2025-05-19</td></tr><tr><td class="label form-item" colspan="2">产品名称</td><td class="data form-item" colspan="6"></td></tr><!-- 交易内容 --><tr><td class="label">序号</td><td class="label">代码</td><td class="label">简称</td><td class="label">交易方向</td><td class="label">交易量(万)</td><td class="label">交易净价/收益率</td><td class="label">清算速度</td><td class="label">对手方</td></tr><tr><td class="data">1</td><td class="data">Z21120852.IB</td><td class="data">21西藏债专项(八期)IB</td><td class="data">卖出</td><td class="data">10000000</td><td class="data"><div>净价</div><div>100</div></td><td class="data">T+5</td><td class="data">12年第6期私银混合精选</td></tr><!-- 审核区域 --><tr><td colspan="2">审批节点1</td><td colspan="6">王朔铖</td></tr><tr><td colspan="2">审批节点2</td><td colspan="6">王朔铖</td></tr><tr><td colspan="2">审批节点2</td><td colspan="6">王朔铖</td></tr></table></div>

放到html文件运行没问题, 证明方法没问题

这里说明下: 产品有个需求是用户要在页面添加一个编辑器功能, 自己手写vue代码, 然后生成新的页面的需求