v-t.js 3.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145
  1. // directives/vT.js
  2. import { watch } from 'vue'
  3. import { lang } from '@/composables/config'
  4. const TEXT_NODE_CLASS = 'v-t-node'
  5. /**
  6. * 解析绑定值
  7. * 支持:'key' 或 ['key', param]
  8. */
  9. const parseBinding = (val) => {
  10. if (Array.isArray(val)) {
  11. const [key, ...rest] = val
  12. if (rest.length === 0) return { key, param: undefined }
  13. if (rest.length === 1) return { key, param: rest[0] }
  14. return { key, param: rest }
  15. }
  16. return { key: val, param: undefined }
  17. }
  18. const isFormField = (el) => {
  19. const tag = el?.tagName?.toUpperCase?.() || ''
  20. return tag === 'INPUT' || tag === 'TEXTAREA'
  21. }
  22. /**
  23. * 在宿主内创建独立文本节点,避免直接改 button/view 的 textContent
  24. * 导致 Vue patch 时清空子树、click 等事件失效
  25. */
  26. const ensureTextNode = (el) => {
  27. if (isFormField(el)) return el
  28. if (el._vTTextEl?.parentNode === el) {
  29. return el._vTTextEl
  30. }
  31. if (typeof document === 'undefined') {
  32. return el
  33. }
  34. const node = document.createElement('span')
  35. node.className = TEXT_NODE_CLASS
  36. node.setAttribute('data-v-t', '')
  37. node.style.pointerEvents = 'none'
  38. el.appendChild(node)
  39. el._vTTextEl = node
  40. return node
  41. }
  42. const updateElementText = (el, text) => {
  43. if (!el) return
  44. const target = ensureTextNode(el)
  45. if (isFormField(target)) {
  46. target.value = text
  47. return
  48. }
  49. if ('textContent' in target) {
  50. target.textContent = text
  51. } else if ('innerText' in target) {
  52. target.innerText = text
  53. }
  54. }
  55. const getI18n = () => {
  56. return typeof globalThis !== 'undefined' && globalThis.__i18n ? globalThis.__i18n : null
  57. }
  58. const bindingSignature = (val) => {
  59. if (Array.isArray(val)) return JSON.stringify(val)
  60. return String(val ?? '')
  61. }
  62. export default {
  63. mounted(el, binding) {
  64. const setState = (val) => {
  65. const { key, param } = parseBinding(val)
  66. el._vTKey = key
  67. el._vTParam = param
  68. el._vTBindingSig = bindingSignature(val)
  69. }
  70. const update = () => {
  71. const i18n = getI18n()
  72. const key = el._vTKey
  73. const param = el._vTParam
  74. if (!key) {
  75. updateElementText(el, '')
  76. return
  77. }
  78. let text = ''
  79. if (i18n) {
  80. const translated = i18n.global.t(key, param)
  81. text = translated === key ? '' : translated
  82. }
  83. updateElementText(el, text)
  84. }
  85. setState(binding.value)
  86. update()
  87. el._vTSetState = setState
  88. el._vTUpdate = update
  89. el._vTStopWatch = watch(
  90. () => lang.value,
  91. () => {
  92. const i18n = getI18n()
  93. if (i18n?.global?.locale?.value !== undefined) {
  94. i18n.global.locale.value = lang.value
  95. }
  96. update()
  97. },
  98. { immediate: false, flush: 'sync' }
  99. )
  100. },
  101. updated(el, binding) {
  102. const sig = bindingSignature(binding.value)
  103. if (sig === el._vTBindingSig) return
  104. if (el._vTSetState) el._vTSetState(binding.value)
  105. if (el._vTUpdate) el._vTUpdate()
  106. },
  107. unmounted(el) {
  108. if (el._vTStopWatch) {
  109. el._vTStopWatch()
  110. delete el._vTStopWatch
  111. }
  112. if (el._vTTextEl?.parentNode === el) {
  113. el.removeChild(el._vTTextEl)
  114. }
  115. delete el._vTTextEl
  116. delete el._vTUpdate
  117. delete el._vTSetState
  118. delete el._vTKey
  119. delete el._vTParam
  120. delete el._vTBindingSig
  121. },
  122. }