Appearance
实现 银行卡车牌邮编查询 工具
分类:developer | 标签:银行卡、车牌、邮编 银行卡BIN车牌归属地邮编区号查询
2.1 功能说明
银行卡BIN车牌归属地邮编区号查询
2.1.1 使用指南
功能说明
三个常用查表工具合在一起:银行卡 BIN 识别发卡行、车牌号识别归属地、城市邮编与电话区号互查。 所有数据都内置在页面里,输入内容不会离开你的浏览器,不发起任何网络请求。
银行卡 BIN 查询
BIN(Bank Identification Number,发卡行识别码)是银行卡号的前 6–8 位, 由卡组织统一分配,用来标识发卡机构。输入卡号前 6 位以上即可查出发卡行、卡种和卡组织。 工具同时会做Luhn 校验:如果你输入了完整卡号,会验证校验位是否合法, 这是识别输入错误的常用方法(银联、VISA、MasterCard 均遵循此算法)。
内置 {{ BANK_BINS.length }} 条 BIN 记录,覆盖六大国有银行、主要股份制银行和部分城商行。 卡种分为借记卡(储蓄卡,先存后用)、信用卡(贷记卡,先用后还)和准贷记卡(需存备用金、可小额透支,现已少见)。
车牌归属地查询
中国大陆民用车牌格式为「省份简称 + 发牌机关代号 + 序号」,例如「粤B·12345」表示广东省深圳市。 输入完整车牌或仅输入省份简称都可以查询。工具收录全部 31 个省级行政区简称, 并对 {{ Object.keys(PLATE_CITIES).length }} 个省份提供了地市字母代号的细分对照。 新能源车牌为 8 位,规则相同,只是序号多一位。
邮编与区号查询
支持城市名、邮编、区号三种输入方式互查,内置 {{ CITIES.length }} 个主要城市。 中国邮政编码为 6 位数字:前两位表示省级区域,第三位表示邮区,第四位表示县市,后两位表示投递局。 长途电话区号为 3–4 位,直辖市和部分特大城市为 3 位(如北京 010、上海 021)。
数据覆盖度与限制
BIN 数据:银行 BIN 总量有数千条且会随新卡种发行而变化,本工具收录的是最常见的 {{ BANK_BINS.length }} 条。查不到不代表卡号有问题,只说明该 BIN 不在收录范围内。 另外同一家银行不同分行、不同卡种的 BIN 可能不同,结果仅供参考, 请勿用于任何风控、支付验证等生产用途,正式场景应调用银联或发卡行的官方接口。
车牌数据:地市字母代号只覆盖了主要省份,未收录省份只显示到省级。 个别城市因扩容启用了额外字母(如粤A、粤S 之外的新增号段),以及军车、警车、使领馆车牌等特殊号牌, 规则与民用牌不同,表中仅作简要说明。
邮编数据:只收录到地级市层面,使用的是各市的通用邮编(多为「XX0000」形式), 实际投递需精确到区县甚至街道的邮编。部分城市多个区号并存或经历过区号合并,以当地实际使用为准。
以上数据整理自公开资料,可能存在滞后,重要用途请以官方渠道核实。
2.2 代码实现
2.2.1 组件结构
本工具是一个基于 Vue 3 <script setup> 语法的单文件组件(SFC),统一包裹在 ToolPageShell 组件内,由它提供页面标题、工具 ID、分类与「使用指南」插槽等通用外壳;核心业务逻辑(响应式状态、计算属性、事件处理函数)全部写在 <script setup> 中,输入/输出通过 el-input、el-button 等 Element Plus 组件与用户交互,所有数据均在浏览器本地处理,不会上传服务器。
本工具目录 RegionLookup/ 下除 index.vue 外,还包含以下源文件:
data.js(286 行)
2.2.2 核心逻辑概览
<script setup> 中定义的主要函数/方法:
luhnValid()copyRow()downloadZipCsv()copyText()download()
2.2.3 关键实现代码
以下是该工具的完整 <script setup> 实现(核心代码)。模板(<template>)与样式(<style>)已省略。
vue
import { computed, ref } from 'vue'
import { ElMessage } from 'element-plus'
import ToolPageShell from '@/components/ToolPageShell.vue'
import { BANK_BINS, CITIES, PLATE_CITIES, PLATE_PROVINCES, PLATE_SPECIAL } from './data.js'
const activeTab = ref('bin')
const cardInput = ref('')
const binTableQuery = ref('')
const plateInput = ref('')
const zipInput = ref('')
// ---------- 银行卡 BIN ----------
const cardDigits = computed(() => cardInput.value.replace(/\D/g, ''))
// 取最长匹配的 BIN 前缀
const binResult = computed(() => {
const d = cardDigits.value
if (d.length < 6) return null
let best = null
for (const item of BANK_BINS) {
if (d.startsWith(item.bin) && (!best || item.bin.length > best.bin.length)) best = item
}
return best
})
function luhnValid (digits) {
let sum = 0
let double = false
for (let i = digits.length - 1; i >= 0; i--) {
let n = Number(digits.charAt(i))
if (double) {
n *= 2
if (n > 9) n -= 9
}
sum += n
double = !double
}
return sum % 10 === 0
}
const luhnState = computed(() => {
const d = cardDigits.value
if (d.length < 12) return { type: 'info', text: '卡号位数不足,暂不校验' }
return luhnValid(d)
? { type: 'success', text: '校验通过' }
: { type: 'danger', text: '校验不通过,请检查卡号是否输错' }
})
const maskedCard = computed(() => {
const d = cardDigits.value
if (d.length < 10) return d
return d.slice(0, 6) + '*'.repeat(d.length - 10) + d.slice(-4)
})
const binText = computed(() => {
const r = binResult.value
if (!r) return ''
return [
'【银行卡 BIN 查询结果】',
'发卡行:' + r.bank,
'卡种:' + r.type,
'卡组织:' + r.brand,
'BIN 前缀:' + r.bin,
'常见卡号位数:' + r.len,
'输入位数:' + cardDigits.value.length,
'Luhn 校验:' + luhnState.value.text,
'脱敏卡号:' + maskedCard.value,
'',
'提示:结果基于内置静态 BIN 表,仅供参考,请勿用于支付风控等生产用途。'
].join('\n')
})
const binRows = computed(() => {
const q = binTableQuery.value.trim()
if (!q) return BANK_BINS
return BANK_BINS.filter(b =>
b.bank.includes(q) || b.bin.includes(q) || b.type.includes(q) || b.brand.toLowerCase().includes(q.toLowerCase()))
})
// ---------- 车牌 ----------
const plateResult = computed(() => {
const raw = plateInput.value.trim().replace(/[·.\s-]/g, '').toUpperCase()
if (!raw) return null
const chars = Array.from(raw)
const abbr = chars[0]
const prov = PLATE_PROVINCES[abbr]
if (!prov) return null
const letter = chars[1] && /[A-Z]/.test(chars[1]) ? chars[1] : ''
const cityMap = PLATE_CITIES[abbr]
const city = letter && cityMap ? cityMap[letter] || '' : ''
const body = chars.slice(2).join('')
let plateType = '未填写序号'
if (body.length >= 5) {
plateType = body.length >= 6 ? '新能源汽车号牌(8 位)' : '普通汽车号牌(7 位)'
if (raw.endsWith('学')) plateType = '教练车号牌'
else if (raw.endsWith('警')) plateType = '警用车号牌'
else if (raw.endsWith('港') || raw.endsWith('澳')) plateType = '粤港澳出入境车号牌'
} else if (body.length > 0) {
plateType = '序号不完整'
}
return {
abbr,
province: prov.province,
region: prov.region,
capital: prov.capital,
letter,
city,
plateType
}
})
const plateText = computed(() => {
const r = plateResult.value
if (!r) return ''
return [
'【车牌归属地查询结果】',
'车牌:' + plateInput.value.trim(),
'省份简称:' + r.abbr,
'省级行政区:' + r.province,
'省会 / 首府:' + r.capital,
'所属地区:' + r.region,
'发牌机关代号:' + (r.letter || '未输入') + (r.city ? '(' + r.city + ')' : ''),
'车牌类型:' + r.plateType
].join('\n')
})
// ---------- 邮编区号 ----------
const zipRows = computed(() => {
const q = zipInput.value.trim()
if (!q) return CITIES
return CITIES.filter(c =>
c.city.includes(q) || c.province.includes(q) || c.zip.includes(q) || c.area.includes(q))
})
function copyRow (row) {
copyText(row.city + '(' + row.province + ')邮编 ' + row.zip + ',电话区号 ' + row.area)
}
function downloadZipCsv () {
const head = ['城市', '省份', '邮政编码', '电话区号']
const rows = zipRows.value.map(c => [c.city, c.province, c.zip, c.area])
const csv = [head, ...rows]
.map(r => r.map(v => '"' + String(v).replace(/"/g, '""') + '"').join(','))
.join('\n')
download('\ufeff' + csv, '城市邮编区号表.csv', 'text/csv;charset=utf-8')
}
// ---------- 通用 ----------
async function copyText (text) {
if (!text) return
try {
await navigator.clipboard.writeText(text)
ElMessage.success('已复制')
} catch {
ElMessage.warning('复制失败,请手动选择复制')
}
}
function download (content, filename, type = 'text/plain;charset=utf-8') {
if (!content) return
const blob = new Blob([content], { type })
const a = document.createElement('a')
a.href = URL.createObjectURL(blob)
a.download = filename
a.click()
URL.revokeObjectURL(a.href)
}2.3 效果截图
