Skip to content

实现 亲戚称谓计算器 工具

分类: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-inputel-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 效果截图

亲戚称谓计算器 效果截图

工具访问地址:https://www.i91tools.com/tools/kinship