Appearance
实现 PX↔REM/VW 换算 工具
分类:general | 标签:px、rem、vw 按根字号批量转换 CSS 中的 px/rem/vw
2.1 功能说明
按根字号批量转换 CSS 中的 px/rem/vw
2.1.1 使用指南
功能说明
在移动端和响应式适配中,把 CSS 里的固定像素批量换算成相对单位(rem 或 vw),或者反向还原成像素,便于核对设计稿。整段 CSS 粘进来即可,注释、选择器和其它属性都会原样保留,只替换带单位的数值。
两个基准参数
- 根字号:html 元素的 font-size,rem 换算的基准。默认 16 是浏览器默认值;如果项目用了 flexible 方案把根字号设成 37.5 或 75,请改成对应数值。
- 基准视口宽度:设计稿宽度,vw 换算的基准。默认 375 对应主流移动端设计稿,750 的二倍图设计稿请填 750。
换算公式
- px 转 rem:数值 除以 根字号
- rem 转 px:数值 乘以 根字号
- px 转 vw:数值 除以 视口宽度 再乘 100
- vw 转 px:数值 除以 100 再乘 视口宽度
选项说明
- 保留小数位:结果四舍五入到指定位数,并自动去掉末尾多余的 0,例如 1.500 会输出 1.5。
- 保留 1px 不转换:常用于 1px 边框和分割线,转成相对单位后在部分设备上会被抹掉或变粗,建议保持勾选。
- 保留 0 值不转换:0px 转换后没有意义,勾选后保持原样。
匹配规则与注意事项
使用正则匹配数值加单位的组合,支持负号和小数(例如 -0.5px、.75rem),单位大小写不敏感。需要注意:工具不解析 CSS 语法树,因此字符串内容、注释里的数值、以及 url() 中恰好形如数值加单位的文本也会被一并替换。转换后建议快速浏览一遍结果,或先在小片段上试转。
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> 中定义的主要函数/方法:
convertValue()formatValue()loadSample()applyToInput()clearAll()copyOutput()downloadOutput()
2.2.3 关键实现代码
以下是该工具的完整 <script setup> 实现(核心代码)。模板(<template>)与样式(<style>)已省略。
vue
import { ref, computed } from 'vue'
import { ElMessage } from 'element-plus'
import ToolPageShell from '@/components/ToolPageShell.vue'
const rootFontSize = ref(16)
const viewportWidth = ref(375)
const precision = ref(6)
const keepOnePx = ref(true)
const keepZero = ref(true)
const mode = ref('px2rem')
const cssInput = ref('')
const singleValue = ref(16)
const formulas = {
px2rem: () => `当前公式:rem 值 = px 值 / ${rootFontSize.value}`,
rem2px: () => `当前公式:px 值 = rem 值 × ${rootFontSize.value}`,
px2vw: () => `当前公式:vw 值 = px 值 / ${viewportWidth.value} × 100`,
vw2px: () => `当前公式:px 值 = vw 值 / 100 × ${viewportWidth.value}`
}
const formulaText = computed(() => formulas[mode.value]())
const sourceUnit = computed(() => mode.value.slice(0, mode.value.indexOf('2')))
const targetUnit = computed(() => mode.value.slice(mode.value.indexOf('2') + 1))
const singleUnitLabel = computed(() => sourceUnit.value)
function convertValue(n) {
if (mode.value === 'px2rem') return n / rootFontSize.value
if (mode.value === 'rem2px') return n * rootFontSize.value
if (mode.value === 'px2vw') return (n / viewportWidth.value) * 100
return (n / 100) * viewportWidth.value
}
function formatValue(n) {
if (!Number.isFinite(n)) return '0'
const fixed = n.toFixed(precision.value)
// 去掉末尾多余的零,例如 1.500000 输出 1.5
return fixed.indexOf('.') === -1 ? fixed : fixed.replace(/0+$/, '').replace(/\.$/, '')
}
const conversion = computed(() => {
const source = cssInput.value
if (!source) return { text: '', count: 0 }
const unit = sourceUnit.value
const pattern = new RegExp('(-?(?:\\d+\\.?\\d*|\\.\\d+))' + unit + '\\b', 'gi')
let count = 0
const text = source.replace(pattern, (match, numText) => {
const n = parseFloat(numText)
if (!Number.isFinite(n)) return match
if (keepZero.value && n === 0) return match
if (keepOnePx.value && unit === 'px' && Math.abs(n) === 1) return match
count++
return formatValue(convertValue(n)) + targetUnit.value
})
return { text, count }
})
const cssOutput = computed(() => conversion.value.text)
const convertedCount = computed(() => conversion.value.count)
const quickResult = computed(() => {
const n = Number(singleValue.value) || 0
const converted = formatValue(convertValue(n))
return {
labelA: `${sourceUnit.value} 值`,
valueA: `${n}${sourceUnit.value}`,
labelB: `${targetUnit.value} 值`,
valueB: `${converted}${targetUnit.value}`
}
})
function loadSample() {
cssInput.value = [
'.card {',
' width: 350px;',
' padding: 12px 16px;',
' margin: -8px auto 24px;',
' border: 1px solid #e4e7ed;',
' border-radius: 8px;',
' font-size: 14px;',
' line-height: 22px;',
' letter-spacing: 0.5px;',
'}',
'',
'.card__title {',
' font-size: 18px;',
' margin-bottom: 0px;',
'}'
].join('\n')
}
function applyToInput() {
cssInput.value = cssOutput.value
ElMessage.success('已回填,可继续做下一步换算')
}
function clearAll() {
cssInput.value = ''
}
async function copyOutput() {
if (!cssOutput.value) return
try {
await navigator.clipboard.writeText(cssOutput.value)
ElMessage.success('已复制到剪贴板')
} catch {
ElMessage.warning('复制失败,请手动选择文本复制')
}
}
function downloadOutput() {
if (!cssOutput.value) return
const blob = new Blob([cssOutput.value], { type: 'text/css;charset=utf-8' })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = `converted-${mode.value}.css`
document.body.appendChild(a)
a.click()
document.body.removeChild(a)
URL.revokeObjectURL(url)
ElMessage.success('已开始下载')
}2.3 效果截图
