Appearance
实现 UTM 链接生成器 工具
分类:developer | 标签:UTM、追踪、营销 生成带 utm 参数的追踪链接,支持批量渠道
2.1 功能说明
生成带 utm 参数的追踪链接,支持批量渠道
核心能力
- 参数名
- 原始值
- 编码后
2.1.1 使用指南
功能说明
UTM 参数是投放链接上的一组标准标记,用来告诉统计工具「这个访客是从哪来的」。本工具帮你按规范拼接这些参数并正确做 URL 编码,避免手写时漏掉问号、写错连接符或让中文参数变成乱码。所有拼接都在浏览器本地完成,不会把链接上传到任何服务器。
五个参数怎么填
- utm_source(必填):具体的流量来源站点或渠道,例如 wechat、baidu、newsletter。
- utm_medium:媒介类型,描述这是哪一类流量,例如 cpc(付费点击)、social(社交)、email(邮件)、banner(横幅广告)。
- utm_campaign:营销活动名称,用于把同一次活动的所有链接归到一起,例如 spring_sale_2026。
- utm_term:付费搜索场景下的投放关键词。
- utm_content:区分同一活动内的不同素材或位置,例如 top_banner 与 footer_link,便于做 A/B 对比。
拼接规则
- 只有填写了内容的参数才会出现在链接里,留空的参数会被跳过。
- 会自动识别链接中已有的查询参数,用
&追加而不是重复加问号;链接末尾的锚点(井号后面的部分)会被保留并始终放在最后。 - 参数值统一做 URL 编码,中文、空格和特殊符号都能安全传输。
- 勾选「覆盖链接中已有的 utm 参数」后,原链接里旧的 utm 参数会先被移除再写入新值;不勾选则保留原有参数,可能出现同名参数重复。
- 勾选「参数值统一转小写」有助于避免统计工具把 WeChat 和 wechat 当成两个来源。
批量生成
在渠道列表里每行填一个来源名,工具会用相同的 medium、campaign 等参数为每个渠道各生成一条链接,一行一条,可整体复制或下载。CSV 导出包含「渠道」与「链接」两列,带 UTF-8 BOM,可直接用 Excel 打开分发给各渠道负责人。
命名建议
建议团队内部统一使用小写字母加下划线的写法,并保持一份命名对照表。参数名本身(utm_source 等)是统计工具约定的固定写法,不能改动。
2.2 代码实现
2.2.1 组件结构
本工具是一个基于 Vue 3 <script setup> 语法的单文件组件(SFC),统一包裹在 ToolPageShell 组件内,由它提供页面标题、工具 ID、分类与「使用指南」插槽等通用外壳;核心业务逻辑(响应式状态、计算属性、事件处理函数)全部写在 <script setup> 中,输入/输出通过 el-input、el-button 等 Element Plus 组件与用户交互,所有数据均在浏览器本地处理,不会上传服务器。
2.2.2 核心逻辑概览
<script setup> 中定义的主要函数/方法:
normalizeValue()buildUrl()loadSample()clearAll()copyText()saveBlob()downloadText()downloadBatchCsv()
2.2.3 关键实现代码
以下是该工具的完整 <script setup> 实现(核心代码)。模板(<template>)与样式(<style>)已省略。
vue
import { ref, reactive, computed } from 'vue'
import { ElMessage } from 'element-plus'
import ToolPageShell from '@/components/ToolPageShell.vue'
const baseUrl = ref('')
const overwriteExisting = ref(true)
const lowerCaseValues = ref(false)
const batchSources = ref('')
const params = reactive({
source: '',
medium: '',
campaign: '',
term: '',
content: ''
})
const paramKeys = [
['source', 'utm_source'],
['medium', 'utm_medium'],
['campaign', 'utm_campaign'],
['term', 'utm_term'],
['content', 'utm_content']
]
function normalizeValue(v) {
const trimmed = String(v || '').trim()
return lowerCaseValues.value ? trimmed.toLowerCase() : trimmed
}
const paramRows = computed(() =>
paramKeys
.map(([field, key]) => {
const raw = normalizeValue(params[field])
return raw ? { key, raw, encoded: encodeURIComponent(raw) } : null
})
.filter(Boolean)
)
const urlWarning = computed(() => {
const url = baseUrl.value.trim()
if (!url) return ''
if (!/^https?:\/\//i.test(url)) return '落地页地址建议以 http:// 或 https:// 开头,否则部分平台可能无法正确识别'
if (/\s/.test(url)) return '落地页地址中包含空格,请检查是否粘贴出错'
return ''
})
// 手工拆解链接,避免相对地址在 URL 构造函数中抛错
function buildUrl(sourceValue) {
const url = baseUrl.value.trim()
if (!url) return ''
let rest = url
let hash = ''
const hashIndex = rest.indexOf('#')
if (hashIndex !== -1) {
hash = rest.slice(hashIndex)
rest = rest.slice(0, hashIndex)
}
let path = rest
let query = ''
const qIndex = rest.indexOf('?')
if (qIndex !== -1) {
path = rest.slice(0, qIndex)
query = rest.slice(qIndex + 1)
}
const utmNames = paramKeys.map(([, key]) => key)
const existing = query
.split('&')
.filter(Boolean)
.filter(item => {
if (!overwriteExisting.value) return true
const name = item.split('=')[0]
return !utmNames.includes(name)
})
const added = []
paramKeys.forEach(([field, key]) => {
const raw = key === 'utm_source' && sourceValue !== undefined ? normalizeValue(sourceValue) : normalizeValue(params[field])
if (raw) added.push(`${key}=${encodeURIComponent(raw)}`)
})
const all = [...existing, ...added]
return path + (all.length ? '?' + all.join('&') : '') + hash
}
const singleUrl = computed(() => {
if (!baseUrl.value.trim() || !normalizeValue(params.source)) return ''
return buildUrl()
})
const batchList = computed(() =>
batchSources.value
.split(/\r?\n/)
.map(s => s.trim())
.filter(Boolean)
)
const batchResult = computed(() => {
if (!baseUrl.value.trim() || !batchList.value.length) return ''
return batchList.value.map(s => buildUrl(s)).join('\n')
})
function loadSample() {
baseUrl.value = 'https://example.com/landing?ref=home'
params.source = 'wechat'
params.medium = 'social'
params.campaign = 'spring_sale_2026'
params.term = ''
params.content = 'top_banner'
batchSources.value = ['wechat', 'weibo', 'zhihu', 'douyin'].join('\n')
}
function clearAll() {
baseUrl.value = ''
paramKeys.forEach(([field]) => {
params[field] = ''
})
batchSources.value = ''
}
async function copyText(text) {
if (!text) return
try {
await navigator.clipboard.writeText(text)
ElMessage.success('已复制到剪贴板')
} catch {
ElMessage.warning('复制失败,请手动选择文本复制')
}
}
function saveBlob(content, filename, mime) {
const blob = new Blob([content], { type: mime })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = filename
document.body.appendChild(a)
a.click()
document.body.removeChild(a)
URL.revokeObjectURL(url)
ElMessage.success('已开始下载')
}
function downloadText(content, filename) {
if (!content) return
saveBlob(content, filename, 'text/plain;charset=utf-8')
}
function downloadBatchCsv() {
if (!batchResult.value) return
const escape = v => {
const s = String(v)
return /[",\n\r]/.test(s) ? '"' + s.replace(/"/g, '""') + '"' : s
}
const lines = ['渠道,链接']
batchList.value.forEach(s => lines.push([escape(s), escape(buildUrl(s))].join(',')))
saveBlob('\ufeff' + lines.join('\r\n'), 'utm-links.csv', 'text/csv;charset=utf-8')
}2.3 效果截图
