通用产品标签打印 (为制衣厂 打印纸箱错印或不足 补打修改纸箱通用程序)html版

复制代码

🏷️ 通用产品标签打印

一个基于 Vue 3 的纯前端产品标签打印工具,支持灵活的字段配置、批量打印、模板管理和数据导入导出。

✨ 功能特性

产品管理

  • 录入产品 --- 支持 12 个可自定义字段,可通过模板快速填充

  • 编辑产品 --- 修改字段值,支持单张卡片独立覆盖字段名称

  • 删除产品 --- 单条删除

  • 搜索过滤 --- 实时搜索所有字段,匹配内容高亮显示

  • 热门标签 --- 自动从已有数据中提取高频值作为快捷搜索入口

字段配置

  • 全局字段设置 --- 自定义每个字段的默认标签名、提示文字

  • 卡片显示控制 --- 勾选决定哪些字段展示在产品卡片上

  • 单卡片覆盖 --- 每张产品卡片可单独修改字段名称(如将 FILE 1 改为 SKU

模板系统

  • 从卡片创建模板 --- 一键将现有产品存为模板

  • 录入时选择模板 --- 快速填充常用字段值

  • 模板管理 --- 重命名、删除、查看模板填充项数

打印功能

  • 字段勾选 --- 按需选择打印哪些字段行

  • 字段合并 --- 支持 FILE 7+8FILE 9+10FILE 11+12 两两合并为一行

  • 水印存根 --- 可选右侧水印,支持自定义字段和透明度(1%--60%)

  • 打印数量 --- 设置打印份数

  • 每产品独立设置 --- 每个产品的打印布局配置可单独保存

  • 自适应排版 --- 字体大小自动缩放,超长内容横向压缩适配 A4 横版

  • 实时预览 --- 打印前预览标签效果

数据管理

  • 导入 --- 支持 JSON 文件导入(兼容旧版数据格式自动迁移)

  • 导出 --- 导出包含字段配置、产品数据和模板的完整 JSON

  • 清除 --- 一键清空所有产品数据

  • 本地持久化 --- 所有数据存储在浏览器 localStorage 中,刷新不丢失


🚀 快速使用

直接打开

本项目是单文件 HTML 应用,无需构建:

复制代码
# 用浏览器直接打开
open index.html
​
# 或使用任意本地服务器
npx serve .
python -m http.server 8080

依赖

依赖 来源 说明
Vue 3 unpkg.com/vue@3/dist/vue.global.js CDN 引入,需联网

无需 npm install,无需构建工具。


📖 使用指南

1. 配置字段

点击顶部 ⚙️ 字段 按钮,进入全局字段配置:

  • 修改默认标签名(如将 FILE 1SKUFILE 2类别

  • 设置输入提示文字

  • 通过复选框控制是否在卡片上显示

  • 随时点击「恢复默认」重置

2. 录入产品

点击 + 录入

  • 可从下拉菜单选择已有模板自动填充

  • 填写各字段值

  • 点击「添加」或「添加并继续」(连续录入模式)

  • 可随时点击 ⭐ 存为模板 保存当前表单为模板

3. 管理卡片

将鼠标悬停在产品卡片上,出现操作按钮:

按钮 功能
✏️ 编辑字段值和自定义标签名
将该产品存为模板
🖨️ 打开打印配置
🗑️ 删除该产品

4. 打印标签

点击卡片上的 🖨️ 进入打印面板:

  • 左侧 --- 实时预览标签效果

  • 右侧 --- 勾选需要打印的字段行

  • 启用「合并」开关将相邻字段合并为一行

  • 启用「水印存根」添加右侧垂直水印

  • 设置打印数量后点击 🖨️ 打印

  • 产品级打印设置会自动保存(标签显示「已保存设置」)

5. 导入导出

操作 说明
📥 导入 选择 JSON 文件,追加到现有数据
📤 导出 下载包含字段、产品、模板的完整 JSON
🗑️ 清除 清空所有产品数据(需确认)

📁 数据格式

导出的 JSON 文件结构:

复制代码
{
  "version": 3,
  "fields": [
    { "key": "file1", "label": "SKU", "ph": "输入SKU", "show": true }
  ],
  "products": [
    {
      "id": 1716000000000,
      "file1": "ABC-001",
      "file2": "服装",
      "labels": { "file1": "自定义名" },
      "ps": { "vis": {}, "comb78": false, "useWm": false, "wmOp": 12, "prtCnt": 1 }
    }
  ],
  "templates": [
    { "id": 1716000000001, "name": "常用模板", "vals": {}, "labels": {}, "ps": {} }
  ]
}

兼容性

导入时自动识别并迁移旧版数据格式(label_products_v3label_app_v2label_app),旧字段名映射:

旧字段 新字段
sku file1
category file2
style file3
color file4
size file5
qty file6
boxNo file7
boxTotal file8

🖨️ 打印规格

项目
纸张 A4 横版(297mm × 210mm)
默认字号 80px
字体 Arial Bold
自适应 字段 >6 行时自动缩小字号;超长内容自动横向缩放
水印 右侧垂直排列,支持透明度 1%--60%

🛠️ 技术栈

  • Vue 3 --- Composition API(setup

  • 纯 CSS --- 无外部 UI 框架

  • localStorage --- 本地数据持久化

  • 零构建 --- 单 HTML 文件,CDN 引入 Vue


📌 注意事项

  • 数据存储在浏览器本地,清除浏览器缓存会丢失数据,建议定期导出备份

  • 打印功能依赖浏览器的 window.print(),建议使用 Chrome / Edge 获得最佳效果

  • 需联网加载 Vue 3 CDN,离线使用需将 vue.global.js 下载到本地并替换引用路径

​代码:

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>通用产品标签打印</title>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<style>
*{margin:0;padding:0;box-sizing:border-box}
body{font-family:Arial,sans-serif;background:#f8f6f3;color:#1a1a1a;padding:24px}
.tb{max-width:1200px;margin:0 auto;background:#fff;border-radius:12px;padding:14px 20px;box-shadow:0 1px 4px rgba(0,0,0,.04);display:flex;align-items:center;gap:12px;flex-wrap:wrap}
.tw{flex:1;position:relative;min-width:0}
.ti{width:100%;border:2px solid #e5e2dc;border-radius:8px;padding:10px 40px 10px 14px;font-size:14px;transition:.2s}
.ti:focus{outline:none;border-color:#c41e3a;box-shadow:0 0 0 3px rgba(196,30,58,.1)}
.cl{position:absolute;right:8px;top:50%;transform:translateY(-50%);width:28px;height:28px;border:none;background:#eee;border-radius:50%;cursor:pointer;font-size:16px;color:#555}
.tc{font-size:13px;color:#555;white-space:nowrap}.tc b{color:#c41e3a}
.tg{display:flex;gap:6px;flex-wrap:wrap;max-width:1200px;margin:10px auto 0}
.tg span{padding:4px 12px;background:#fff;border:1px solid #e5e2dc;border-radius:16px;font-size:12px;cursor:pointer;transition:.15s}
.tg span:hover{background:#c41e3a;color:#fff;border-color:#c41e3a}
.head{display:flex;justify-content:space-between;align-items:center;max-width:1200px;margin:20px auto 16px;flex-wrap:wrap;gap:10px}
.head h1{font-size:24px;font-weight:700}
.ha{display:flex;gap:8px;flex-wrap:wrap}
.g{display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:16px;max-width:1200px;margin:0 auto}
.cd{background:#fff;border:1.5px solid #1a1a1a;border-radius:2px;overflow:hidden;position:relative;transition:.2s}
.cd:hover{box-shadow:0 8px 24px rgba(0,0,0,.12)}
.cd .bd{padding:10px 12px}
.cd .rw{display:flex;font-size:10px;line-height:1.6;border-bottom:1px dotted #eee}
.cd .rw:last-child{border-bottom:none}
.cd .lb{font-weight:700;width:80px;flex-shrink:0}
.cd .vl{color:#555;text-transform:uppercase;white-space:pre-wrap;word-break:break-all}
.hl{background:#fef08a;padding:0 2px;border-radius:2px}
.cd .ac{position:absolute;top:8px;right:8px;opacity:0;transition:.2s;display:flex;gap:3px}
.cd:hover .ac{opacity:1}
.ab{width:28px;height:28px;border:none;border-radius:6px;cursor:pointer;font-size:12px}
.ab-prt{background:#dcfce7;color:#16a34a}.ab-prt.has-ps{background:#fef3c7;color:#d97706}
.ab-edt{background:#dbeafe;color:#2563eb}
.ab-del{background:#fee2e2;color:#dc2626}
.ab-tpl{background:#fef3c7;color:#d97706}
.mo{position:fixed;inset:0;background:rgba(0,0,0,.6);display:flex;align-items:center;justify-content:center;z-index:1000;backdrop-filter:blur(4px)}
.pm{background:#fff;border-radius:16px;max-width:560px;width:100%;overflow:hidden;display:flex;flex-direction:column;max-height:92vh}
.pm.wd{max-width:700px}
.mh{padding:20px 24px;border-bottom:1px solid #e5e2dc;display:flex;justify-content:space-between;align-items:center;flex-shrink:0}
.mt{font-size:20px;font-weight:700;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:400px}
.xb{width:36px;height:36px;border:none;background:#f8f6f3;border-radius:8px;cursor:pointer;font-size:20px;flex-shrink:0}
.prv{padding:24px;display:flex;flex-direction:column;align-items:center;gap:20px;background:#f0f0f0}
.lpv{width:400px;background:#fff;padding:20px;font-family:Arial,sans-serif;font-weight:700;position:relative;overflow:hidden;min-height:260px}
.lr{font-weight:700;text-transform:uppercase;white-space:pre;letter-spacing:1px;margin-bottom:8px;font-size:24px;position:relative;z-index:2;overflow:hidden;text-overflow:ellipsis}
.lrc{display:flex;gap:20px}.lrc span{white-space:pre}
.wm{position:absolute;right:0;top:0;bottom:0;width:40px;z-index:1;display:flex;align-items:center;justify-content:center;pointer-events:none;border-left:1px dashed rgba(0,0,0,.12)}
.wm span{writing-mode:vertical-rl;transform:rotate(180deg);font-weight:800;white-space:pre;text-transform:uppercase;letter-spacing:4px}
.ck{display:flex;align-items:center;gap:8px;padding:10px 24px;border-bottom:1px solid #eee;cursor:pointer}
.ck input[type="checkbox"]{width:18px;height:18px;accent-color:#c41e3a;cursor:pointer}
.opt{display:flex;align-items:center;gap:10px;padding:12px 24px;border-bottom:1px solid #eee}
.opt label{font-size:13px;font-weight:500;white-space:nowrap}
.opt input[type="range"]{flex:1;accent-color:#c41e3a;cursor:pointer}
.opt input[type="number"]{width:56px;padding:4px 6px;border:1.5px solid #e5e2dc;border-radius:6px;font-size:13px;text-align:center}
.opt .ut{font-size:12px;color:#999}
.opt select{padding:6px 10px;border:1.5px solid #e5e2dc;border-radius:6px;font-size:13px;background:#fff;cursor:pointer}
.qty{display:flex;align-items:center;justify-content:space-between;padding:12px 24px;border-bottom:1px solid #eee}
.ql{font-size:13px;font-weight:500}
.qn{width:80px;padding:6px 10px;border:1.5px solid #e5e2dc;border-radius:6px;font-size:14px;text-align:center}
.ft{padding:16px 24px;border-top:1px solid #e5e2dc;display:flex;justify-content:center;gap:10px;background:#fafafa;flex-shrink:0;flex-wrap:wrap}
.btn{padding:10px 16px;border:none;border-radius:8px;font-size:13px;font-weight:500;cursor:pointer;display:inline-flex;align-items:center;gap:6px}
.btn-o{background:#fff;border:1.5px solid #e5e2dc;color:#1a1a1a}.btn-o:hover{border-color:#c41e3a;color:#c41e3a}
.btn-d{background:#1a1a1a;color:#fff}.btn-d:hover{background:#333}
.btn-p{background:#c41e3a;color:#fff}.btn-p:hover{background:#a01830}
.btn-w{background:#fff;border:1.5px solid #fbbf24;color:#d97706}.btn-w:hover{background:#fef3c7}
.btn-s{background:#fff;border:1.5px solid #a78bfa;color:#7c3aed}.btn-s:hover{background:#f5f3ff}
.em{text-align:center;padding:60px;color:#999;grid-column:1/-1}
.fg{display:grid;grid-template-columns:1fr 1fr;gap:12px 16px;padding:20px 24px;overflow-y:auto}
.ff{display:flex;flex-direction:column;gap:4px}
.fl{font-size:11px;font-weight:700;color:#888;text-transform:uppercase;letter-spacing:.5px}
.fv{padding:10px 12px;border:1.5px solid #e5e2dc;border-radius:8px;font-size:14px;transition:.2s}
.fv:focus{outline:none;border-color:#c41e3a;box-shadow:0 0 0 3px rgba(196,30,58,.08)}
.rc{display:flex;align-items:center;gap:8px;padding:10px 24px;border-bottom:1px solid #eee;font-size:13px}
.rc input[type="checkbox"]{width:16px;height:16px;accent-color:#c41e3a;cursor:pointer}
.rc .rn{font-weight:600;flex:1}.rc .rv{color:#555;text-transform:uppercase;white-space:pre}.rc.off{opacity:.45}
.sep{height:1px;background:#e5e2dc;margin:0 24px}
.mg{display:flex;align-items:center;gap:8px;padding:8px 24px;background:#fef9c3;border-bottom:1px solid #eee;font-size:12px;font-weight:600}
.mg label{display:flex;align-items:center;gap:6px;cursor:pointer}
.mg input[type="checkbox"]{width:15px;height:15px;accent-color:#d97706;cursor:pointer}
.sf{display:flex;align-items:center;gap:12px;padding:10px 0;border-bottom:1px solid #eee}
.sk{font-size:11px;color:#999;width:58px;flex-shrink:0;font-weight:700}
.si{flex:1;padding:7px 10px;border:1.5px solid #e5e2dc;border-radius:6px;font-size:13px;transition:.2s;min-width:0}
.si:focus{outline:none;border-color:#c41e3a;box-shadow:0 0 0 3px rgba(196,30,58,.08)}
.scb{width:18px;height:18px;accent-color:#c41e3a;cursor:pointer;flex-shrink:0}
.sh{display:flex;align-items:center;gap:12px;padding:6px 0 10px;font-size:11px;font-weight:700;color:#aaa;text-transform:uppercase;letter-spacing:.5px}
.ef{display:flex;gap:8px;padding:8px 0;border-bottom:1px solid #eee;align-items:center}
.ek{font-size:10px;color:#999;width:42px;flex-shrink:0;font-weight:700}
.el{width:130px;padding:7px 10px;border:1.5px solid #d97706;border-radius:6px;font-size:13px;background:#fffbeb}
.el:focus{outline:none;border-color:#d97706;box-shadow:0 0 0 3px rgba(217,119,6,.12)}
.ev{flex:1;padding:7px 10px;border:1.5px solid #e5e2dc;border-radius:6px;font-size:13px;transition:.2s}
.ev:focus{outline:none;border-color:#c41e3a;box-shadow:0 0 0 3px rgba(196,30,58,.08)}
.eh{display:flex;gap:8px;padding:4px 0 8px;font-size:11px;font-weight:700;color:#aaa;text-transform:uppercase;letter-spacing:.5px}
.lbl-custom{color:#d97706;font-style:italic}
.ps-tag{font-size:9px;padding:1px 6px;border-radius:8px;background:#fef3c7;color:#d97706;margin-left:6px;font-weight:700}
.tpl-bar{padding:10px 24px;display:flex;gap:8px;align-items:center;border-bottom:1px solid #eee;background:#faf5ff}
.tpl-bar label{font-size:12px;font-weight:700;color:#7c3aed;white-space:nowrap}
.tpl-bar select{flex:1;padding:7px 10px;border:1.5px solid #a78bfa;border-radius:6px;font-size:13px;background:#fff;cursor:pointer}
.tpl-bar select:focus{outline:none;border-color:#7c3aed}
.tpl-item{display:flex;align-items:center;gap:10px;padding:12px 0;border-bottom:1px solid #eee}
.tpl-nm{flex:1;font-weight:600;font-size:14px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.tpl-info{font-size:11px;color:#999;white-space:nowrap}
.tpl-info b{color:#7c3aed}
</style>
</head>
<body>
<div id="app">

<div class="tb">
  <div class="tw">
    <input class="ti" v-model="q" placeholder="🔍  搜索任意字段 ...">
    <button class="cl" v-if="q" @click="q=''">×</button>
  </div>
  <span class="tc" v-if="q">找到 <b>{{filtered.length}}</b> / {{products.length}}</span>
</div>
<div class="tg" v-if="hotTags.length">
  <span v-for="t in hotTags" :key="t" @click="q=t">{{t}}</span>
</div>

<div class="head">
  <h1>🏷️ 通用标签打印</h1>
  <div class="ha">
    <button class="btn btn-p" @click="openAdd()">+ 录入</button>
    <button class="btn btn-s" @click="dlgTmpl=true">📋 模板</button>
    <button class="btn btn-o" @click="openCfg()">⚙️ 字段</button>
    <button class="btn btn-o" @click="doImportBtn()">📥</button>
    <button class="btn btn-o" @click="doExport()">📤</button>
    <button class="btn btn-o" @click="doClear()" style="color:#dc2626">🗑️</button>
  </div>
</div>

<div class="g">
  <div class="cd" v-for="p in filtered" :key="p.id">
    <div class="ac">
      <button class="ab ab-edt" @click="openEdit(p)" title="编辑">✏️</button>
      <button class="ab ab-tpl" @click="saveAsTmpl(p)" title="存为模板">⭐</button>
      <button class="ab ab-prt" :class="{'has-ps':p.ps}" @click="openPrt(p)" title="打印">🖨️</button>
      <button class="ab ab-del" @click="delP(p)" title="删除">🗑️</button>
    </div>
    <div class="bd">
      <div class="rw" v-for="f in cardFlds" :key="f.key">
        <span class="lb" :class="{'lbl-custom':hasCustom(p,f.key)}">{{getLabel(p,f.key)}}</span>
        <span class="vl" v-html="hl(p[f.key])"></span>
      </div>
      <div class="rw" v-if="!cardFlds.length" style="color:#aaa;font-size:11px">⚙️ 配置字段</div>
    </div>
  </div>
  <div class="em" v-if="!filtered.length && q">🔍 未找到</div>
  <div class="em" v-if="!products.length">暂无数据 --- 点击「+ 录入」</div>
</div>

<!-- 录入 -->
<div class="mo" v-if="dlgAdd" @click.self="dlgAdd=false">
  <div class="pm">
    <div class="mh"><span class="mt">📝 录入产品</span><button class="xb" @click="dlgAdd=false">×</button></div>
    <div class="tpl-bar" v-if="tmpls.length">
      <label>📋 模板</label>
      <select v-model="selTmpl" @change="onTmplSel()">
        <option value="">不使用模板</option>
        <option v-for="t in tmpls" :key="t.id" :value="String(t.id)">{{t.name}}</option>
      </select>
    </div>
    <div class="fg">
      <div class="ff" v-for="f in flds" :key="f.key">
        <span class="fl">{{f.label}}</span>
        <input class="fv" v-model="form[f.key]" :placeholder="f.ph||('输入 '+f.label)">
      </div>
    </div>
    <div class="ft">
      <button class="btn btn-o" @click="dlgAdd=false">取消</button>
      <button class="btn btn-w" @click="saveFormAsTmpl()">⭐ 存为模板</button>
      <button class="btn btn-p" @click="addP(false)">添加</button>
      <button class="btn btn-d" @click="addP(true)">添加并继续</button>
    </div>
  </div>
</div>

<!-- 编辑 -->
<div class="mo" v-if="dlgEdit" @click.self="dlgEdit=false">
  <div class="pm wd">
    <div class="mh"><span class="mt">✏️ 编辑产品</span><button class="xb" @click="dlgEdit=false">×</button></div>
    <div style="overflow-y:auto;flex:1;padding:12px 24px">
      <div class="eh"><span style="width:42px">KEY</span><span style="width:130px">字段名(可改)</span><span style="flex:1">值</span></div>
      <div class="ef" v-for="f in flds" :key="f.key">
        <span class="ek">{{f.key}}</span>
        <input class="el" v-model="editLabels[f.key]" :placeholder="f.label">
        <input class="ev" v-model="editVals[f.key]" :placeholder="editLabels[f.key]||f.label">
      </div>
    </div>
    <div class="ft">
      <button class="btn btn-w" @click="rstLabels()">↺ 名称恢复默认</button>
      <button class="btn btn-o" @click="dlgEdit=false">取消</button>
      <button class="btn btn-p" @click="saveEdit()">✓ 保存</button>
    </div>
  </div>
</div>

<!-- 打印 -->
<div class="mo" v-if="dlgPrt" @click.self="dlgPrt=false">
  <div class="pm">
    <div class="mh">
      <span class="mt">🖨️ {{printTitle}} <span class="ps-tag" v-if="hasCustomPS">已保存设置</span></span>
      <button class="xb" @click="dlgPrt=false">×</button>
    </div>
    <div style="overflow-y:auto;flex:1">
      <div class="prv">
        <div class="lpv">
          <div class="wm" v-if="useWm"><span :style="{fontSize:'16px',color:'rgba(0,0,0,'+wmOp/100+')'}">{{pi[wmFld]||''}}</span></div>
          <template v-for="r in pvRows" :key="r.id">
            <div v-if="r.t==='s'" class="lr">{{r.lbl}}: {{pi[r.f.key]||''}}</div>
            <div v-else class="lr lrc">
              <span v-if="r.s1">{{r.lbl1}}: {{pi[r.f1.key]||''}}</span>
              <span v-if="r.s2">{{r.lbl2}}: {{pi[r.f2.key]||''}}</span>
            </div>
          </template>
          <div class="em" v-if="!pvRows.length" style="padding:30px;font-size:12px">请勾选字段 ↓</div>
        </div>
      </div>
      <div style="background:#fafafa;border-top:1px solid #e5e2dc">
        <div style="padding:10px 24px 4px;font-size:12px;font-weight:700;color:#888;text-transform:uppercase;letter-spacing:.5px">勾选打印行</div>
        <div class="rc" v-for="i in 6" :key="'c'+i" :class="{off:!pv['file'+i]}">
          <input type="checkbox" v-model="pv['file'+i]">
          <span class="rn" :class="{'lbl-custom':hasCustom(prtProd,'file'+i)}">{{prtLabel('file'+i)}}</span>
          <span class="rv">{{displayVal(pi['file'+i])}}</span>
        </div>
        <div class="sep"></div>
        <div class="mg"><label><input type="checkbox" v-model="cm78"> {{prtLabel('file7')}} + {{prtLabel('file8')}} 合并</label></div>
        <div class="rc" :class="{off:!pv.file7}"><input type="checkbox" v-model="pv.file7"><span class="rn" :class="{'lbl-custom':hasCustom(prtProd,'file7')}">{{prtLabel('file7')}}</span><span class="rv">{{displayVal(pi.file7)}}</span></div>
        <div class="rc" :class="{off:!pv.file8}"><input type="checkbox" v-model="pv.file8"><span class="rn" :class="{'lbl-custom':hasCustom(prtProd,'file8')}">{{prtLabel('file8')}}</span><span class="rv">{{displayVal(pi.file8)}}</span></div>
        <div class="sep"></div>
        <div class="mg"><label><input type="checkbox" v-model="cm910"> {{prtLabel('file9')}} + {{prtLabel('file10')}} 合并</label></div>
        <div class="rc" :class="{off:!pv.file9}"><input type="checkbox" v-model="pv.file9"><span class="rn" :class="{'lbl-custom':hasCustom(prtProd,'file9')}">{{prtLabel('file9')}}</span><span class="rv">{{displayVal(pi.file9)}}</span></div>
        <div class="rc" :class="{off:!pv.file10}"><input type="checkbox" v-model="pv.file10"><span class="rn" :class="{'lbl-custom':hasCustom(prtProd,'file10')}">{{prtLabel('file10')}}</span><span class="rv">{{displayVal(pi.file10)}}</span></div>
        <div class="sep"></div>
        <div class="mg"><label><input type="checkbox" v-model="cm1112"> {{prtLabel('file11')}} + {{prtLabel('file12')}} 合并</label></div>
        <div class="rc" :class="{off:!pv.file11}"><input type="checkbox" v-model="pv.file11"><span class="rn" :class="{'lbl-custom':hasCustom(prtProd,'file11')}">{{prtLabel('file11')}}</span><span class="rv">{{displayVal(pi.file11)}}</span></div>
        <div class="rc" :class="{off:!pv.file12}"><input type="checkbox" v-model="pv.file12"><span class="rn" :class="{'lbl-custom':hasCustom(prtProd,'file12')}">{{prtLabel('file12')}}</span><span class="rv">{{displayVal(pi.file12)}}</span></div>
        <div class="sep"></div>
        <label class="ck"><input type="checkbox" v-model="useWm">水印存根</label>
        <div class="opt" v-if="useWm"><label>水印字段</label><select v-model="wmFld"><option v-for="f in flds" :key="f.key" :value="f.key">{{prtLabel(f.key)}}</option></select></div>
        <div class="opt" v-if="useWm"><label>透明度</label><input type="range" min="1" max="60" v-model.number="wmOp"><input type="number" min="1" max="60" v-model.number="wmOp"><span class="ut">%</span></div>
      </div>
      <div class="qty"><span class="ql">打印数量</span><input class="qn" type="number" v-model.number="prtCnt" min="1"></div>
    </div>
    <div class="ft">
      <button class="btn btn-o" @click="dlgPrt=false">取消</button>
      <button class="btn btn-w" @click="rstPS()">↺ 恢复默认</button>
      <button class="btn btn-d" @click="doPrt()">🖨️ 打印</button>
    </div>
  </div>
</div>

<!-- 模板管理 -->
<div class="mo" v-if="dlgTmpl" @click.self="dlgTmpl=false">
  <div class="pm">
    <div class="mh"><span class="mt">📋 模板管理</span><button class="xb" @click="dlgTmpl=false">×</button></div>
    <div style="overflow-y:auto;flex:1;padding:12px 24px">
      <div v-if="!tmpls.length" style="text-align:center;padding:40px;color:#999;font-size:13px">
        暂无模板<br><span style="font-size:12px">在卡片上点击 ⭐ 或录入时点击「存为模板」</span>
      </div>
      <div class="tpl-item" v-for="t in tmpls" :key="t.id">
        <span class="tpl-nm">{{t.name}}</span>
        <span class="tpl-info"><b>{{countFilled(t)}}</b> 项有值</span>
        <button class="ab ab-edt" @click="renameTmpl(t)" title="重命名">✏️</button>
        <button class="ab ab-del" @click="delTmpl(t)" title="删除">🗑️</button>
      </div>
    </div>
    <div class="ft"><button class="btn btn-o" @click="dlgTmpl=false">关闭</button></div>
  </div>
</div>

<!-- 全局字段配置 -->
<div class="mo" v-if="dlgCfg" @click.self="dlgCfg=false">
  <div class="pm wd">
    <div class="mh"><span class="mt">⚙️ 全局字段配置</span><button class="xb" @click="dlgCfg=false">×</button></div>
    <div style="overflow-y:auto;flex:1;padding:12px 24px">
      <div style="font-size:12px;color:#888;margin-bottom:12px">此处修改全局默认。单张卡片可在 ✏️ 编辑中单独覆盖字段名。</div>
      <div class="sh"><span style="width:58px">KEY</span><span style="width:18px">卡片</span><span style="flex:1">默认标签名</span><span style="flex:1">默认提示</span></div>
      <div class="sf" v-for="(f,idx) in flds" :key="f.key">
        <span class="sk">{{f.key}}</span>
        <input class="scb" type="checkbox" v-model="f.show">
        <input class="si" v-model="f.label" :placeholder="'FILE '+(idx+1)">
        <input class="si" v-model="f.ph" placeholder="提示文字">
      </div>
    </div>
    <div class="ft">
      <button class="btn btn-o" @click="rstCfg()">恢复默认</button>
      <button class="btn btn-p" @click="savCfg()">✓ 保存</button>
    </div>
  </div>
</div>

<input type="file" ref="fiRef" style="display:none" accept=".json" @change="onFile">
</div>

<script>
var V = Vue;
V.createApp({
  setup: function() {

    function mkDefs() {
      var a = [];
      for (var i = 1; i <= 12; i++) a.push({ key: 'file' + i, label: 'FILE ' + i, ph: '', show: true });
      return a;
    }
    function mkPS() {
      var vis = {};
      for (var i = 1; i <= 12; i++) vis['file' + i] = true;
      return { vis: vis, comb78: false, comb910: false, comb1112: false, useWm: false, wmOp: 12, wmFld: 'file1', prtCnt: 1 };
    }
    function isDefaultPS(ps) {
      if (!ps) return true;
      if (ps.comb78 || ps.comb910 || ps.comb1112 || ps.useWm) return false;
      if ((ps.prtCnt || 1) !== 1) return false;
      for (var i = 1; i <= 12; i++) if ((ps.vis['file' + i] || false) !== true) return false;
      return true;
    }
    function cloneDeep(o) { return JSON.parse(JSON.stringify(o)); }

    var flds = V.ref(mkDefs());
    var products = V.ref([]);
    var tmpls = V.ref([]);
    var dlgAdd = V.ref(false), dlgEdit = V.ref(false), dlgPrt = V.ref(false), dlgCfg = V.ref(false), dlgTmpl = V.ref(false);
    var prtId = V.ref(0), editId = V.ref(0);
    var prtCnt = V.ref(1), useWm = V.ref(false), wmOp = V.ref(12), wmFld = V.ref('file1');
    var cm78 = V.ref(false), cm910 = V.ref(false), cm1112 = V.ref(false);
    var q = V.ref(''), selTmpl = V.ref('');
    var pi = V.reactive({}), form = V.reactive({}), pv = V.reactive({});
    var editLabels = V.reactive({}), editVals = V.reactive({});
    var fiRef = V.ref(null);

    var keys = [];
    for (var i = 1; i <= 12; i++) { var k = 'file' + i; keys.push(k); form[k] = ''; pv[k] = true; }

    /* ═══ 空格处理 ═══ */
    function escH(s) {
      return String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/ /g, '&nbsp;');
    }
    function escPrint(s) {
      return String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/ /g, '\u00A0');
    }
    function displayVal(v) { return v || '---'; }

    function getLabel(p, key) {
      if (p && p.labels && p.labels[key]) return p.labels[key];
      for (var i = 0; i < flds.value.length; i++) if (flds.value[i].key === key) return flds.value[i].label;
      return key;
    }
    function hasCustom(p, key) { return p && p.labels && p.labels[key] && p.labels[key].trim(); }
    function findProd(id) { for (var i = 0; i < products.value.length; i++) if (products.value[i].id === id) return products.value[i]; return null; }
    function findTmpl(id) { var n = Number(id); for (var i = 0; i < tmpls.value.length; i++) if (tmpls.value[i].id === n) return tmpls.value[i]; return null; }
    function countFilled(t) { var c = 0; for (var i = 0; i < keys.length; i++) if (t.vals[keys[i]]) c++; return c; }

    /* ═══ 计算 ═══ */
    var prtProd = V.computed(function() { return findProd(prtId.value); });
    var prtLabel = function(key) { return getLabel(prtProd.value, key); };
    var cardFlds = V.computed(function() { return flds.value.filter(function(f) { return f.show; }); });
    var hotTags = V.computed(function() { var s = {}; products.value.forEach(function(p) { flds.value.forEach(function(f) { if (p[f.key]) s[p[f.key]] = 1; }); }); return Object.keys(s).slice(0, 16); });
    var filtered = V.computed(function() {
      if (!q.value.trim()) return products.value;
      var t = q.value.trim().toLowerCase();
      return products.value.filter(function(p) { return flds.value.some(function(f) { return (p[f.key] || '').toLowerCase().indexOf(t) > -1; }); });
    });
    var printTitle = V.computed(function() {
      var p = prtProd.value; if (!p) return '打印标签';
      for (var i = 0; i < keys.length; i++) { if (p[keys[i]]) return getLabel(p, keys[i]) + ': ' + p[keys[i]]; }
      return '打印标签';
    });
    var hasCustomPS = V.computed(function() { var p = prtProd.value; return p && p.ps && !isDefaultPS(p.ps); });
    var pvRows = V.computed(function() {
      var rows = [], rid = 0, F = flds.value, pp = prtProd.value || {};
      for (var i = 0; i < 6; i++) { if (pv[F[i].key]) rows.push({ id: rid++, t: 's', f: F[i], lbl: getLabel(pp, F[i].key) }); }
      if (cm78.value) { if (pv.file7 || pv.file8) rows.push({ id: rid++, t: 'c', f1: F[6], f2: F[7], s1: pv.file7, s2: pv.file8, lbl1: getLabel(pp, 'file7'), lbl2: getLabel(pp, 'file8') }); }
      else { if (pv.file7) rows.push({ id: rid++, t: 's', f: F[6], lbl: getLabel(pp, 'file7') }); if (pv.file8) rows.push({ id: rid++, t: 's', f: F[7], lbl: getLabel(pp, 'file8') }); }
      if (cm910.value) { if (pv.file9 || pv.file10) rows.push({ id: rid++, t: 'c', f1: F[8], f2: F[9], s1: pv.file9, s2: pv.file10, lbl1: getLabel(pp, 'file9'), lbl2: getLabel(pp, 'file10') }); }
      else { if (pv.file9) rows.push({ id: rid++, t: 's', f: F[8], lbl: getLabel(pp, 'file9') }); if (pv.file10) rows.push({ id: rid++, t: 's', f: F[9], lbl: getLabel(pp, 'file10') }); }
      if (cm1112.value) { if (pv.file11 || pv.file12) rows.push({ id: rid++, t: 'c', f1: F[10], f2: F[11], s1: pv.file11, s2: pv.file12, lbl1: getLabel(pp, 'file11'), lbl2: getLabel(pp, 'file12') }); }
      else { if (pv.file11) rows.push({ id: rid++, t: 's', f: F[10], lbl: getLabel(pp, 'file11') }); if (pv.file12) rows.push({ id: rid++, t: 's', f: F[11], lbl: getLabel(pp, 'file12') }); }
      return rows;
    });

    function hl(text) {
      if (!q.value.trim() || !text) return escH(text || '---');
      var esc = q.value.trim().replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
      var safe = escH(text);
      var safeQ = escH(q.value.trim()).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
      return safe.replace(new RegExp('(' + safeQ + ')', 'gi'), '<span class="hl">$1</span>');
    }

    /* ═══ 模板 ═══ */
    function saveAsTmpl(p) {
      var suggest = '';
      for (var i = 0; i < keys.length; i++) { if (p[keys[i]]) { suggest = p[keys[i]]; break; } }
      var name = prompt('输入模板名称:', suggest || '');
      if (!name || !name.trim()) return;
      var vals = {};
      for (var i = 0; i < keys.length; i++) vals[keys[i]] = p[keys[i]] || '';
      tmpls.value.push({ id: Date.now(), name: name.trim(), vals: vals, labels: p.labels ? cloneDeep(p.labels) : {}, ps: p.ps ? cloneDeep(p.ps) : mkPS() });
      saveTmpls();
    }
    function saveFormAsTmpl() {
      var name = prompt('输入模板名称:');
      if (!name || !name.trim()) return;
      var vals = {};
      for (var i = 0; i < keys.length; i++) vals[keys[i]] = form[keys[i]] || '';
      tmpls.value.push({ id: Date.now(), name: name.trim(), vals: vals, labels: {}, ps: mkPS() });
      saveTmpls();
    }
    function onTmplSel() {
      if (!selTmpl.value) return;
      var t = findTmpl(selTmpl.value);
      if (!t) return;
      for (var i = 0; i < keys.length; i++) form[keys[i]] = t.vals[keys[i]] || '';
    }
    function renameTmpl(t) {
      var name = prompt('重命名模板:', t.name);
      if (!name || !name.trim()) return;
      t.name = name.trim(); saveTmpls();
    }
    function delTmpl(t) {
      if (!confirm('删除模板「' + t.name + '」?')) return;
      for (var i = 0; i < tmpls.value.length; i++) { if (tmpls.value[i].id === t.id) { tmpls.value.splice(i, 1); break; } }
      saveTmpls();
    }

    /* ═══ 录入 ═══ */
    function openAdd() { for (var i = 0; i < keys.length; i++) form[keys[i]] = ''; selTmpl.value = ''; dlgAdd.value = true; }
    function addP(cont) {
      var has = false;
      for (var i = 0; i < keys.length; i++) { if (form[keys[i]] && form[keys[i]].trim()) { has = true; break; } }
      if (!has) return;
      var row = { id: Date.now(), ps: mkPS(), labels: {} };
      for (var i = 0; i < keys.length; i++) row[keys[i]] = form[keys[i]];
      if (selTmpl.value) {
        var t = findTmpl(selTmpl.value);
        if (t) { if (t.labels) row.labels = cloneDeep(t.labels); if (t.ps) row.ps = cloneDeep(t.ps); }
      }
      products.value.push(row); saveProd();
      for (var i = 0; i < keys.length; i++) form[keys[i]] = '';
      selTmpl.value = '';
      if (!cont) dlgAdd.value = false;
    }
    function delP(p) {
      for (var i = 0; i < products.value.length; i++) { if (products.value[i].id === p.id) { products.value.splice(i, 1); saveProd(); return; } }
    }

    /* ═══ 编辑 ═══ */
    function openEdit(p) {
      editId.value = p.id;
      for (var i = 0; i < keys.length; i++) { editLabels[keys[i]] = (p.labels && p.labels[keys[i]]) || ''; editVals[keys[i]] = p[keys[i]] || ''; }
      dlgEdit.value = true;
    }
    function saveEdit() {
      var p = findProd(editId.value); if (!p) return;
      if (!p.labels) p.labels = {};
      for (var i = 0; i < keys.length; i++) {
        p[keys[i]] = editVals[keys[i]];
        var lb = editLabels[keys[i]];
        if (lb && lb.trim()) p.labels[keys[i]] = lb.trim(); else delete p.labels[keys[i]];
      }
      saveProd(); dlgEdit.value = false;
    }
    function rstLabels() { for (var i = 0; i < keys.length; i++) editLabels[keys[i]] = ''; }

    /* ═══ 打印 ═══ */
    function loadPS(ps) {
      var s = ps || mkPS();
      for (var i = 0; i < keys.length; i++) pv[keys[i]] = s.vis[keys[i]] !== undefined ? !!s.vis[keys[i]] : true;
      cm78.value = !!s.comb78; cm910.value = !!s.comb910; cm1112.value = !!s.comb1112;
      useWm.value = !!s.useWm; wmOp.value = s.wmOp || 12; wmFld.value = s.wmFld || 'file1'; prtCnt.value = s.prtCnt || 1;
    }
    function savePS() {
      var p = findProd(prtId.value); if (!p) return;
      var vis = {}; for (var i = 0; i < keys.length; i++) vis[keys[i]] = !!pv[keys[i]];
      p.ps = { vis: vis, comb78: cm78.value, comb910: cm910.value, comb1112: cm1112.value, useWm: useWm.value, wmOp: wmOp.value, wmFld: wmFld.value, prtCnt: prtCnt.value };
      saveProd();
    }
    function openPrt(p) {
      prtId.value = p.id;
      for (var i = 0; i < keys.length; i++) pi[keys[i]] = p[keys[i]] || '';
      loadPS(p.ps); dlgPrt.value = true;
    }
    function rstPS() { loadPS(null); }

    function doPrt() {
      savePS();
      var rows = pvRows.value;
      if (!rows.length) { alert('请至少勾选一个字段'); return; }
      var alpha = wmOp.value / 100, wmText = escPrint(pi[wmFld.value] || '');
      var pagesHtml = '';
      for (var n = 0; n < prtCnt.value; n++) {
        var wmDiv = useWm.value ? '<div class="wm"><span style="font-size:28px;color:rgba(0,0,0,' + alpha + ')">' + wmText + '</span></div>' : '';
        var linesHtml = '';
        for (var ri = 0; ri < rows.length; ri++) {
          var r = rows[ri];
          if (r.t === 's') {
            linesHtml += '<div class="lr">' + escPrint(r.lbl) + ': ' + escPrint(pi[r.f.key] || '') + '</div>';
          } else {
            var p1 = r.s1 ? escPrint(r.lbl1) + ': ' + escPrint(pi[r.f1.key] || '') : '';
            var p2 = r.s2 ? escPrint(r.lbl2) + ': ' + escPrint(pi[r.f2.key] || '') : '';
            linesHtml += '<div class="lr">' + p1 + (p1 && p2 ? '   ' : '') + p2 + '</div>';
          }
        }
        pagesHtml += '<div class="page">' + wmDiv + linesHtml + '</div>';
      }
      var css = '*{margin:0;padding:0;box-sizing:border-box}body{font-family:Arial,sans-serif;font-weight:700}.page{width:297mm;height:210mm;background:#fff;padding:15mm 25mm 15mm 15mm;page-break-after:always;display:flex;flex-direction:column;justify-content:center;position:relative;overflow:hidden}.wm{position:absolute;right:0;top:0;bottom:0;width:18mm;display:flex;align-items:center;justify-content:center;pointer-events:none;border-left:1px dashed rgba(0,0,0,.15)}.wm span{writing-mode:vertical-rl;transform:rotate(180deg);font-weight:800;white-space:pre;text-transform:uppercase;letter-spacing:4px}.lr{font-weight:700;text-transform:uppercase;white-space:pre;letter-spacing:1px;margin-bottom:3mm;font-size:80px;line-height:1.2;transform-origin:left center;position:relative;z-index:2}@page{size:A4 landscape;margin:0}';
      var ifm = document.createElement('iframe');
      ifm.style.cssText = 'position:absolute!important;top:-9999px!important;left:-9999px!important;width:800px!important;height:600px!important;border:none!important;';
      document.body.appendChild(ifm);
      var doc = ifm.contentDocument || ifm.contentWindow.document;
      doc.open(); doc.write('<!DOCTYPE html><html><head><meta charset="UTF-8"><style>' + css + '</style></head><body>' + pagesHtml + '</body></html>'); doc.close();
      setTimeout(function() {
        try {
          var elems = doc.querySelectorAll('.lr'), cnt = elems.length;
          if (cnt > 6) { var fs = Math.max(24, Math.round(500 / cnt)); for (var i = 0; i < cnt; i++) elems[i].style.fontSize = fs + 'px'; }
          for (var i = 0; i < cnt; i++) { var maxW = 237 * 3.78; if (elems[i].scrollWidth > maxW) elems[i].style.transform = 'scaleX(' + (maxW / elems[i].scrollWidth) + ')'; }
          ifm.contentWindow.focus(); ifm.contentWindow.print();
        } catch (err) { alert('打印出错: ' + err.message); }
        setTimeout(function() { if (ifm.parentNode) ifm.parentNode.removeChild(ifm); }, 2000);
      }, 500);
    }

    /* ═══ 全局配置 ═══ */
    function openCfg() { dlgCfg.value = true; }
    function savCfg() { for (var i = 0; i < keys.length; i++) if (!(keys[i] in form)) form[keys[i]] = ''; try { localStorage.setItem('lbl_flds', JSON.stringify(flds.value)); } catch(e) {} dlgCfg.value = false; }
    function rstCfg() { flds.value = mkDefs(); }

    /* ═══ 持久化 ═══ */
    function saveProd() { try { localStorage.setItem('lbl_prods', JSON.stringify(products.value)); } catch(e) {} }
    function saveTmpls() { try { localStorage.setItem('lbl_tmpls', JSON.stringify(tmpls.value)); } catch(e) {} }

    function loadAll() {
      try { var s = localStorage.getItem('lbl_flds'); if (s) { var saved = JSON.parse(s); if (Array.isArray(saved) && saved.length) { flds.value = mkDefs().map(function(d) { var f = null; for (var i = 0; i < saved.length; i++) if (saved[i].key === d.key) { f = saved[i]; break; } return f ? { key: d.key, label: f.label || d.label, ph: f.ph || '', show: f.show !== undefined ? f.show : true } : d; }); }}} catch(e) {}
      for (var i = 0; i < flds.value.length; i++) if (!(flds.value[i].key in form)) form[flds.value[i].key] = '';
      try { var st = localStorage.getItem('lbl_tmpls'); if (st) tmpls.value = JSON.parse(st); } catch(e) {}
      try {
        var s2 = localStorage.getItem('lbl_prods');
        if (s2) { products.value = JSON.parse(s2); for (var i = 0; i < products.value.length; i++) { if (!products.value[i].ps) products.value[i].ps = mkPS(); if (!products.value[i].labels) products.value[i].labels = {}; } return; }
        var oks = ['label_products_v3', 'label_app_v2', 'label_app'];
        for (var oi = 0; oi < oks.length; oi++) { var raw = localStorage.getItem(oks[oi]); if (raw) { var data = JSON.parse(raw); if (Array.isArray(data)) {
          var km = { sku:'file1',category:'file2',style:'file3',color:'file4',size:'file5',qty:'file6',boxNo:'file7',boxTotal:'file8' }, kmK = Object.keys(km);
          products.value = data.map(function(item, idx) { var n = { id: item.id || Date.now() + idx, ps: mkPS(), labels: {} }; for (var j = 1; j <= 12; j++) n['file' + j] = item['file' + j] || ''; for (var j = 0; j < kmK.length; j++) if (item[kmK[j]] !== undefined && !n[km[kmK[j]]]) n[km[kmK[j]]] = item[kmK[j]]; return n; });
          saveProd(); return;
        }}}
      } catch(e) {}
    }
    loadAll();

    /* ═══ 导入导出 ═══ */
    function doExport() {
      if (!products.value.length) return;
      var blob = new Blob([JSON.stringify({ version: 3, fields: flds.value, products: products.value, templates: tmpls.value }, null, 2)], { type: 'application/json' });
      var a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = 'products_' + new Date().toISOString().slice(0, 10) + '.json';
      document.body.appendChild(a); a.click(); document.body.removeChild(a);
    }
    function doImportBtn() { fiRef.value.click(); }
    function onFile(e) {
      var file = e.target.files[0]; if (!file) return;
      var reader = new FileReader(); reader.onload = function(ev) {
        try {
          var d = JSON.parse(ev.target.result);
          if (d.fields && d.products && Array.isArray(d.products)) {
            flds.value = mkDefs().map(function(df) { var f = null; for (var i = 0; i < d.fields.length; i++) if (d.fields[i].key === df.key) { f = d.fields[i]; break; } return f ? { key: df.key, label: f.label || df.label, ph: f.ph || '', show: f.show !== undefined ? f.show : true } : df; });
            try { localStorage.setItem('lbl_flds', JSON.stringify(flds.value)); } catch(ex) {}
            if (d.templates && Array.isArray(d.templates)) { for (var i = 0; i < d.templates.length; i++) tmpls.value.push(d.templates[i]); saveTmpls(); }
            var mx = 0; for (var i = 0; i < products.value.length; i++) if (products.value[i].id > mx) mx = products.value[i].id;
            for (var i = 0; i < d.products.length; i++) { d.products[i].id = mx + i + 1; if (!d.products[i].ps) d.products[i].ps = mkPS(); if (!d.products[i].labels) d.products[i].labels = {}; }
            products.value = products.value.concat(d.products);
          } else if (Array.isArray(d)) {
            var mx2 = 0; for (var i = 0; i < products.value.length; i++) if (products.value[i].id > mx2) mx2 = products.value[i].id;
            for (var i = 0; i < d.length; i++) { d[i].id = mx2 + i + 1; if (!d[i].ps) d[i].ps = mkPS(); if (!d[i].labels) d[i].labels = {}; }
            products.value = products.value.concat(d);
          } else throw 0;
          saveProd();
        } catch(er) { alert('JSON 格式错误'); }
      };
      reader.readAsText(file); e.target.value = '';
    }
    function doClear() { if (confirm('确定清除所有产品数据?')) { products.value = []; saveProd(); } }

    return {
      flds, products, tmpls, filtered, hotTags, cardFlds, pvRows, printTitle, hasCustomPS, prtProd,
      dlgAdd, dlgEdit, dlgPrt, dlgCfg, dlgTmpl,
      prtCnt, useWm, wmOp, wmFld, cm78, cm910, cm1112, selTmpl,
      q, pi, form, pv, editLabels, editVals, fiRef,
      getLabel, hasCustom, prtLabel, hl, displayVal, countFilled,
      openAdd, addP, delP, openEdit, saveEdit, rstLabels,
      openPrt, doPrt, rstPS,
      openCfg, savCfg, rstCfg,
      saveAsTmpl, saveFormAsTmpl, onTmplSel, renameTmpl, delTmpl,
      doExport, doImportBtn, onFile, doClear
    };
  }
}).mount('#app');
</script>
</body>
</html>
相关推荐
专注API从业者11 小时前
用 Open Claw + 淘宝商品接口,快速实现电商商品监控与智能选品(附完整代码)
大数据·前端·数据结构·数据库
muddjsv11 小时前
前端开发语言使用流行度排行与分析
前端·javascript·typescript
心.c12 小时前
CommonJS和ES Module
javascript·后端·node.js
步十人12 小时前
【JWT】验证令牌的使用
前端·bootstrap·html
吃好睡好便好12 小时前
用if…elseif…end语句输出成绩等级
开发语言·前端·javascript·数据库·学习·matlab·信息可视化
弹简特12 小时前
【Vue3速成】03-vue基本语法的使用
前端·javascript·vue.js
AI产品实战12 小时前
95coder一句话生成MOM系统,AI用时6分50秒,Token只消耗25107
vue.js·spring boot·ai编程·ruoyi
humcomm12 小时前
FinClip vs React Native:两大跨平台方案的深度对比
javascript·react native·react.js