Appearance
实现 亲戚称谓计算器 工具
分类:general | 标签:亲戚、称谓、关系 按关系链推算亲戚称呼
2.1 功能说明
按关系链推算亲戚称呼
2.1.1 使用指南
功能说明
过年走亲戚不知道该叫什么?输入一串关系链,例如「爸爸的哥哥」「妈妈的妹妹的女儿」, 工具会沿着内置的亲属关系图逐步推导,算出你应该怎么称呼这个人,并给出书面称谓、辈分和南北方叫法差异。 也支持反过来查:输入「伯父」,告诉你它对应的关系链是「爸爸的哥哥」。全部规则内置,不联网。
怎么用
1. 先选好「我的性别」和「叫法习惯」,性别会影响「儿子的妈妈」这类需要判断的推导;
2. 在输入框写关系链,用「的」连接,如「爸爸的弟弟的儿子」,不写「的」也能识别;
3. 结果会显示称呼、书面称谓、辈分差和逐步推导过程;
4. 「称呼反查」页签输入称呼关键词,可以看到对应关系链;
5. 「称谓总表」页签列出全部内置称谓,可搜索、可导出 CSV。
推导规则说明
关系链先被切成一串原子关系(父、母、夫、妻、子、女、兄、弟、姐、妹), 再按等价规则化简,例如「爸爸的老婆」化简为「妈妈」、「哥哥的爸爸」化简为「爸爸」, 最后在称谓表中查出结果。因此「爸爸的哥哥的老婆」会被正确推出「伯母」。
中文亲属称谓严格区分父系与母系、长幼与嫡庶,本工具遵循以下惯例: 父之兄称伯父、父之弟称叔父;父之姐妹一律称姑母;母之兄弟一律称舅父、母之姐妹称姨母; 父系伯叔之子女为「堂」亲,姑母、舅父、姨母之子女为「表」亲。
覆盖范围
内置 {{ titleRows.length }} 条称谓,覆盖上下四代(曾祖辈到曾孙辈)的常见亲属,包括: 直系长辈与晚辈、亲兄弟姐妹及其配偶子女、父系伯叔姑及其配偶子女(堂亲)、 母系舅姨及其配偶子女(表亲)、祖辈旁支(伯祖父、姑婆、舅公等)、 夫妻双方的姻亲(公婆、岳父母、大伯子、小姨子、连襟、妯娌等)。 关系链词典收录 {{ aliasCount }} 个称呼写法,「爷爷」「姥爷」「阿嬷」等口语说法都能识别。
已知限制
以下情况本工具不支持或只能给出近似结果,请以当地习俗为准:
1. 长幼未知的情形:堂兄弟、表兄弟到底叫「哥」还是「弟」取决于实际年龄, 工具只能给出「堂兄 / 堂弟」这样的并列结果;
2. 超出四代的远亲:如高祖辈、玄孙辈,以及「爷爷的爷爷的兄弟」这类链路,会提示暂不支持;
3. 再婚、继养、过继关系:继父母、养子女、同父异母兄弟姐妹等未纳入, 推导默认按「同父同母的完整家庭」处理;
4. 地域差异极大的叫法:南北方一栏只给出流传较广的两种口语, 闽南语、客家话、吴语、粤语内部还有大量分支叫法,无法穷举;
5. 非亲属称谓:干爹干妈、师徒、老乡等社会关系不在范围内。
另外,「姑母」在部分地区会按长幼细分为姑妈(父之姐)与姑姐(父之妹), 「姨母」类似地分大姨、小姨,工具已在南北方叫法中体现,书面称谓仍统一为姑母、姨母。
2.2 代码实现
2.2.1 组件结构
本工具是一个基于 Vue 3 <script setup> 语法的单文件组件(SFC),统一包裹在 ToolPageShell 组件内,由它提供页面标题、工具 ID、分类与「使用指南」插槽等通用外壳;核心业务逻辑(响应式状态、计算属性、事件处理函数)全部写在 <script setup> 中,输入/输出通过 el-input、el-button 等 Element Plus 组件与用户交互,所有数据均在浏览器本地处理,不会上传服务器。
本工具目录 Kinship/ 下除 index.vue 外,还包含以下源文件:
data.js(256 行)
2.2.2 核心逻辑概览
<script setup> 中定义的主要函数/方法:
tokenize()matchAt()reducePath()readableOf()genText()resultText()copyResult()saveBlob()downloadResult()downloadTable()
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 { ATOM_ALIASES, ATOM_CN, COMPOUND_ALIASES, EXAMPLES, REDUCE_RULES, TITLES } from './data.js'
const REVERSE_EXAMPLES = ['伯父', '姨妈', '舅母', '堂兄', '外甥女', '连襟', '妯娌', '岳母', '曾祖父']
const myGender = ref('male')
const dialect = ref('standard')
const activeTab = ref('forward')
const chainInput = ref('爸爸的哥哥')
const titleQuery = ref('')
const tableQuery = ref('')
// ---------- 构建别名词典(长词优先) ----------
const ALIAS_LIST = []
for (const c of COMPOUND_ALIASES) {
for (const w of c.words) ALIAS_LIST.push({ word: w, atoms: c.atoms })
}
for (const a of ATOM_ALIASES) {
for (const w of a.words) ALIAS_LIST.push({ word: w, atoms: [a.atom] })
}
ALIAS_LIST.sort((x, y) => Array.from(y.word).length - Array.from(x.word).length)
const aliasCount = ALIAS_LIST.length
// ---------- 分词:中文关系链 → 原子关系数组 ----------
function tokenize (input) {
const clean = input.replace(/[的\s·、,,.。'"??!!]/g, '')
const chars = Array.from(clean)
const atoms = []
const words = []
let i = 0
while (i < chars.length) {
let hit = null
for (const a of ALIAS_LIST) {
const wLen = Array.from(a.word).length
if (i + wLen <= chars.length && chars.slice(i, i + wLen).join('') === a.word) {
hit = { alias: a, len: wLen }
break
}
}
if (!hit) {
return { error: true, badChar: chars[i], pos: i, parsed: words.join('的') }
}
atoms.push(...hit.alias.atoms)
words.push(hit.alias.word)
i += hit.len
}
return { error: false, atoms, words }
}
// ---------- 路径化简 ----------
function matchAt (path, from, idx) {
for (let k = 0; k < from.length; k++) {
if (path[idx + k] !== from[k]) return false
}
return true
}
function reducePath (path) {
let cur = path.slice()
let changed = true
let guard = 0
while (changed && guard < 50) {
changed = false
guard++
// 通用等价规则
for (const rule of REDUCE_RULES) {
for (let i = 0; i + rule.from.length <= cur.length; i++) {
if (matchAt(cur, rule.from, i)) {
cur = [...cur.slice(0, i), ...rule.to, ...cur.slice(i + rule.from.length)]
changed = true
break
}
}
if (changed) break
}
if (changed) continue
// 与「我」的性别相关的规则:子女的父 / 母
for (let i = 0; i + 2 <= cur.length; i++) {
const [a, b] = [cur[i], cur[i + 1]]
if ((a === 'S' || a === 'D') && (b === 'F' || b === 'M')) {
const isMe = (b === 'F' && myGender.value === 'male') || (b === 'M' && myGender.value === 'female')
const to = isMe ? [] : [myGender.value === 'male' ? 'W' : 'H']
cur = [...cur.slice(0, i), ...to, ...cur.slice(i + 2)]
changed = true
break
}
}
}
return cur
}
// ---------- 推导 ----------
const result = computed(() => {
const raw = chainInput.value.trim()
if (!raw) return {}
const tk = tokenize(raw)
if (tk.error) {
return {
error: '无法识别「' + tk.badChar + '」这个字',
hint: '请用「爸爸、妈妈、哥哥、弟弟、姐姐、妹妹、儿子、女儿、老公、老婆」等词,用「的」连接,例如「妈妈的哥哥的女儿」。'
}
}
const reduced = reducePath(tk.atoms)
const key = reduced.join('-')
const def = TITLES[key]
if (!def) {
return {
error: '暂不支持这条关系链的称呼推导',
hint: '识别到的关系为「' + readableOf(tk.atoms) + '」。本工具覆盖曾祖辈到曾孙辈的常见亲属,' +
'更远的旁支远亲、继养关系暂未收录,可在「称谓总表」查看全部支持范围。'
}
}
const steps = []
const walk = []
for (const atom of tk.atoms) {
walk.push(atom)
const stepDef = TITLES[reducePath(walk).join('-')]
steps.push({
atom: ATOM_CN[atom] || atom,
title: stepDef ? stepDef.name : '(中间关系)'
})
}
const north = def.north || def.name
const south = def.south || def.name
const display = dialect.value === 'north' ? north : dialect.value === 'south' ? south : def.name
const reducedChanged = reduced.join('-') !== tk.atoms.join('-')
return {
error: '',
key,
name: def.name,
north,
south,
display,
formal: def.formal || def.name,
gen: def.gen === undefined ? 0 : def.gen,
note: def.note || '',
readable: readableOf(tk.atoms),
reduced: reducedChanged,
reducedText: readableOf(reduced) + '(与原关系链等价)',
steps
}
})
function readableOf (atoms) {
if (!atoms.length) return '我自己'
return '我的' + atoms.map(a => ATOM_CN[a] || a).join('的')
}
function genText (g) {
if (g === 0) return '同辈'
if (g > 0) return '长 ' + g + ' 辈'
return '晚 ' + (-g) + ' 辈'
}
// ---------- 称谓总表 ----------
const titleRows = computed(() => {
return Object.keys(TITLES)
.filter(k => k !== '')
.map(k => {
const d = TITLES[k]
const atoms = k.split('-')
return {
key: k,
name: d.name,
north: d.north || d.name,
south: d.south || d.name,
formal: d.formal || d.name,
readable: readableOf(atoms),
gen: d.gen === undefined ? 0 : d.gen,
genText: genText(d.gen === undefined ? 0 : d.gen),
note: d.note || ''
}
})
.sort((a, b) => b.gen - a.gen || a.key.length - b.key.length)
})
const filteredTitleRows = computed(() => {
const q = tableQuery.value.trim()
if (!q) return titleRows.value
return titleRows.value.filter(r =>
r.name.includes(q) || r.readable.includes(q) ||
r.north.includes(q) || r.south.includes(q) || r.formal.includes(q))
})
const reverseResults = computed(() => {
const q = titleQuery.value.trim()
if (!q) return []
return titleRows.value
.filter(r => r.name.includes(q) || r.north.includes(q) || r.south.includes(q) || r.formal.includes(q))
.map(r => ({
...r,
dialectText: '北方:' + r.north + ';南方:' + r.south
}))
})
// ---------- 复制与下载 ----------
function resultText () {
const r = result.value
return [
'关系链:' + chainInput.value.trim(),
'解析为:' + r.readable,
'称呼:' + r.display,
'标准称谓:' + r.name,
'书面称谓:' + r.formal,
'北方常用:' + r.north,
'南方常用:' + r.south,
'辈分:' + genText(r.gen),
r.note ? '说明:' + r.note : ''
].filter(Boolean).join('\n')
}
async function copyResult () {
try {
await navigator.clipboard.writeText(resultText())
ElMessage.success('已复制推导结果')
} catch {
ElMessage.warning('复制失败,请手动选择复制')
}
}
function saveBlob (content, filename, type) {
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)
}
function downloadResult () {
saveBlob(resultText(), '亲戚称谓推导.txt', 'text/plain;charset=utf-8')
}
function downloadTable () {
const head = ['标准称谓', '关系链', '北方常用', '南方常用', '书面称谓', '辈分']
const rows = filteredTitleRows.value.map(r => [r.name, r.readable, r.north, r.south, r.formal, r.genText])
const csv = [head, ...rows]
.map(r => r.map(v => '"' + String(v).replace(/"/g, '""') + '"').join(','))
.join('\n')
saveBlob('\ufeff' + csv, '亲戚称谓总表.csv', 'text/csv;charset=utf-8')
}2.3 效果截图
