使用vue指令解決文本的溢出提示的問(wèn)題
在我們項(xiàng)目開(kāi)發(fā)中,經(jīng)常會(huì)有 超長(zhǎng)文本溢出提示 ,未溢出則不提示的場(chǎng)景。
筆者就遇到了比較復(fù)雜的場(chǎng)景,在一個(gè)表格中,總計(jì)約有 5000 個(gè)單元格,需要對(duì)每個(gè)單元格進(jìn)行這個(gè)需求的校驗(yàn),剛開(kāi)始開(kāi)發(fā)的時(shí)候也是用 v-if v-else el-tooltip 一把梭。導(dǎo)致運(yùn)行時(shí)存在大量的 <el-tooltip> 實(shí)例。這樣的操作,代碼重復(fù)性極高,而且不利于后期維護(hù),關(guān)鍵我們發(fā)現(xiàn)性能極差,一拉伸就卡頓。和組長(zhǎng)溝通后,開(kāi)發(fā)出了這個(gè)指令。
接下來(lái) 讓我們一步步用 vue指令 實(shí)現(xiàn)這個(gè)需求
本文涉及到的技術(shù)棧
- vue2
- element-ui
在線體驗(yàn)
彥祖?zhèn)?這個(gè) codesandbox 好像用不了了,clone 到本地跑一下吧~
報(bào)錯(cuò) Error in render: "TypeError: (0 , _vue.resolveComponent) is not a function" , 如果有知道怎么解決的彥祖,請(qǐng)?jiān)u論區(qū) 留言。
常規(guī)開(kāi)發(fā)
如下圖所示, 兩個(gè)寬度為 100px 的 div, 第一個(gè)不需要提示,第二個(gè)則需要提示

<template>
<div class="parent">
<div>
天翼云
</div>
<el-tooltip content="天翼云,海量數(shù)據(jù)">
<div class="tooltip">
天翼云,海量數(shù)據(jù)
</div>
</el-tooltip>
</div>
</template>
<style lang="scss" scoped>
.parent{
margin:100px;
>div{
border:1px solid lightblue;
width:100px;
margin-bottom:20px;
&.tooltip{
overflow: hidden; //超出的文本隱藏
text-overflow: ellipsis; //溢出用省略號(hào)顯示
white-space: nowrap; // 默認(rèn)不換行;
}
}
}
</style>這樣的代碼比較復(fù)雜,而且復(fù)用性極低。如果在其他頁(yè)面也有類似的場(chǎng)景,我們就不得不做一個(gè) cv 戰(zhàn)士 了
指令開(kāi)發(fā)
如何判斷是否溢出?
這也算是一個(gè)知識(shí)點(diǎn),首先我們需要判斷文本是否溢出了節(jié)點(diǎn)。后來(lái)在 element-ui/packages/table/src/table-body.js 上找到了這段代碼
const range = document.createRange();
range.setStart(cellChild, 0);
range.setEnd(cellChild, cellChild.childNodes.length);
const rangeWidth = range.getBoundingClientRect().width;
const padding = (parseInt(getStyle(cellChild, 'paddingLeft'), 10) || 0) +
(parseInt(getStyle(cellChild, 'paddingRight'), 10) || 0);
if ((rangeWidth + padding > cellChild.offsetWidth || cellChild.scrollWidth > cellChild.offsetWidth) && this.$refs.tooltip) {
// 省略了不重要的邏輯
}那我們就改造一下 直接拿來(lái)用吧
實(shí)現(xiàn)溢出指令
接下來(lái),讓我們用這段代碼 實(shí)現(xiàn)一個(gè)判斷溢出的指令,非常簡(jiǎn)單,直接上代碼
function getElStyleAttr (element, attr) {
const styles = window.getComputedStyle(element)
return styles[attr]
}
const isOverflow = (target) => {
const scrollWidth = target.scrollWidth
const offsetWidth = target.offsetWidth
const range = document.createRange()
range.setStart(target, 0)
range.setEnd(target, target.childNodes.length)
const rangeWidth = range.getBoundingClientRect().width
const padding = (parseInt(getElStyleAttr(target, 'paddingLeft'), 10) || 0) + (parseInt(getElStyleAttr(target, 'paddingRight'), 10) || 0)
return (rangeWidth + padding > target.offsetWidth) || scrollWidth > offsetWidth
}
export const ellipsisTooltip = {
bind (el, binding, vnode, oldVnode) {
// 避免用戶遺漏樣式,我們必須強(qiáng)制加上超出...樣式
el.style.overflow = 'hidden'
el.style.textOverflow = 'ellipsis'
el.style.whiteSpace = 'nowrap'
const onMouseEnter = (e) => {
if (isOverflow(el)) {
console.log('溢出了')
} else {
console.log('未溢出')
}
}
el.addEventListener('mouseenter', onMouseEnter)
}
}來(lái)看下效果吧

如何把溢出節(jié)點(diǎn)掛載到 el-tooltip 上?
本文的難點(diǎn)也是核心代碼,我們?cè)撊绾伟堰@個(gè)節(jié)點(diǎn)掛載到 <el-tooltip> 上呢。筆者也在這個(gè)問(wèn)題上卡了非常久,嘗試過(guò)復(fù)制一個(gè)新節(jié)點(diǎn), 包裹一層 <el-tooltip> 去代替老的節(jié)點(diǎn),也嘗試過(guò)用 template 常規(guī)渲染調(diào)用,直接生成。但是發(fā)現(xiàn)都比較麻煩,最終不得去看了它的源碼。發(fā)現(xiàn)了以下代碼
mounted() {
this.referenceElm = this.$el;
if (this.$el.nodeType === 1) {
this.$el.setAttribute('aria-describedby', this.tooltipId);
this.$el.setAttribute('tabindex', this.tabindex);
on(this.referenceElm, 'mouseenter', this.show);
on(this.referenceElm, 'mouseleave', this.hide);
on(this.referenceElm, 'focus', () => {
if (!this.$slots.default || !this.$slots.default.length) {
this.handleFocus();
return;
}
const instance = this.$slots.default[0].componentInstance;
if (instance && instance.focus) {
instance.focus();
} else {
this.handleFocus();
}
});
on(this.referenceElm, 'blur', this.handleBlur);
on(this.referenceElm, 'click', this.removeFocusing);
}
// fix issue https://github.com/ElemeFE/element/issues/14424
if (this.value && this.popperVM) {
this.popperVM.$nextTick(() => {
if (this.value) {
this.updatePopper();
}
});
}
}其實(shí)我們發(fā)現(xiàn) 核心點(diǎn) 就是這個(gè) this.$el , 它在 mounted 時(shí)對(duì) this.$el 進(jìn)行了一系列初始化操作
那么接下來(lái)就簡(jiǎn)單了, 我們?cè)囍ヌ鎿Q這個(gè) this.$el ,然后再執(zhí)行 mounted 邏輯不就行了?
但是我們最好不要去改變這個(gè)屬性
vue 官方解釋

改造 el-tooltip 源碼
替換 el-tooltip 的 $el
我們只需要把源碼的 this.$el 部分改成 this.target
也就是在源碼內(nèi)部,我們新增一個(gè)
setEl(el){
this.target = el
}替換 mounted 邏輯
同樣非常簡(jiǎn)單,我們把 mounted 的邏輯重新封裝一個(gè) init 方法
init () {
this.referenceElm = this.target
if (this.target.nodeType === 1) {
this.target.setAttribute('aria-describedby', this.tooltipId)
this.target.setAttribute('tabindex', this.tabindex)
on(this.referenceElm, 'mouseenter', this.show)
on(this.referenceElm, 'mouseleave', this.hide)
on(this.referenceElm, 'focus', () => {
if (!this.$slots.default || !this.$slots.default.length) {
this.handleFocus()
return
}
const instance = this.$slots.default[0].componentInstance
if (instance && instance.focus) {
instance.focus()
} else {
this.handleFocus()
}
})
on(this.referenceElm, 'blur', this.handleBlur)
on(this.referenceElm, 'click', this.removeFocusing)
}
// fix issue https://github.com/ElemeFE/element/issues/14424
if (this.value && this.popperVM) {
this.popperVM.$nextTick(() => {
if (this.value) {
this.updatePopper()
}
})
}
}引入改造后的 el-tooltip
我們看下此時(shí)的目錄結(jié)構(gòu)

- directive.js //指令代碼
- main.js //改造過(guò)的 el-tooltip 代碼
這里還有個(gè)很重要的知識(shí)點(diǎn), 就是創(chuàng)建一個(gè) vue 實(shí)例
我們?cè)谌粘i_(kāi)發(fā)中, 一般只會(huì)在 main.js 進(jìn)行一個(gè) new Vue 的操作。
在閱讀了 element-ui 源碼后,我們會(huì)發(fā)現(xiàn) el-message el-date-picker 中也用到了這個(gè)實(shí)例化的操作,更有 Vue.extend 等高階操作。
我們看下此時(shí)的 directive.js 代碼
import Vue from 'vue'
import Tooltip from './main'
function getElStyleAttr (element, attr) {
const styles = window.getComputedStyle(element)
return styles[attr]
}
const isOverflow = (target) => {
const scrollWidth = target.scrollWidth
const offsetWidth = target.offsetWidth
const range = document.createRange()
range.setStart(target, 0)
range.setEnd(target, target.childNodes.length)
const rangeWidth = range.getBoundingClientRect().width
const padding = (parseInt(getElStyleAttr(target, 'paddingLeft'), 10) || 0) + (parseInt(getElStyleAttr(target, 'paddingRight'), 10) || 0)
return (rangeWidth + padding > target.offsetWidth) || scrollWidth > offsetWidth
}
export const ellipsisTooltip = {
bind (el, binding, vnode, oldVnode) {
// 加上超出...樣式
el.style.overflow = 'hidden'
el.style.textOverflow = 'ellipsis'
el.style.whiteSpace = 'nowrap'
const onMouseEnter = (e) => {
// 需要展示
if (isOverflow(el)) {
// 參考 https://v2.cn.vuejs.org/v2/api/#vm-mount
const vm = new Vue(Tooltip).$mount()
vm.setEl(el)
vm.init()
}
}
el.addEventListener('mouseenter', onMouseEnter)
}
}看下此時(shí)的效果

已經(jīng)初步成效了,但是沒(méi)有任何顯示內(nèi)容?
填充顯示內(nèi)容
彥祖?zhèn)?這個(gè)可更簡(jiǎn)單了 直接一個(gè) setContent 搞定了
- main.js
setContent(content){
this.content = content
}- directive.js
if (isOverflow(el)) {
// 參考 https://v2.cn.vuejs.org/v2/api/#vm-mount
const vm = new Vue(Tooltip).$mount()
vm.setEl(el)
vm.init()
vm.setContent('天翼云,海量數(shù)據(jù)')
}
此時(shí),可能有彥祖會(huì)說(shuō),這個(gè) setContent 也太復(fù)雜了,難道我每次都需要手動(dòng)傳入數(shù)據(jù)嗎?
當(dāng)然不必, 我們 默認(rèn) vm.setContent(content || el.innerText) 就好了
此處只做演示說(shuō)明
此時(shí)我們基本已經(jīng)實(shí)現(xiàn)了一個(gè) 簡(jiǎn)單的 ellipsis-tooltip
開(kāi)始進(jìn)階
以下優(yōu)化代碼,只展示核心代碼,已去掉冗余代碼
防止重復(fù)實(shí)例化
如果我們不進(jìn)行 實(shí)例化的檢測(cè),那么我們可能會(huì)存在大量的 vue 實(shí)例,用戶操作久了,就可能導(dǎo)致頁(yè)面卡頓
const vmMap = new WeakMap()
if (isOverflow(el)) {
if(vmMap.get(el)) return
const vm = new Vue(Tooltip).$mount()
// ...
vmMap.set(el,vm)
}兼容 el-tooltip 屬性
此時(shí),還有很多 el-tooltip 的源生屬性待支持的。比如 placement effect ...
其實(shí)我們只需要在代碼上加上
const vm = new Vue(Tooltip).$mount() vm.placement = placement || 'top-start' vm.effect = effect || 'dark' // ...其他屬性 不做贅述,自行拓展
兼容 文本 寬度改變的場(chǎng)景

業(yè)務(wù)中的文本寬度可能不是固定的
溢出的變成非溢出,非溢出也能變成溢出
所以我們需要在 mouseenter 的時(shí)候進(jìn)行對(duì)應(yīng)判斷
const vm = vmMap.get(el)
if (isOverflow(el)) {
if(vm) return vm.disabled = false
} else {
vm.disabled = true // 沒(méi)有溢出,則應(yīng)該禁用 tooltip
}
}移除大量實(shí)例化時(shí)候的節(jié)點(diǎn)
在開(kāi)發(fā)中,筆者發(fā)現(xiàn), el-tooltip 在 render 的時(shí)候, 對(duì)應(yīng)的 dom 并不會(huì)從 文檔中移除,這個(gè)在表格或者樹(shù)這種大量節(jié)點(diǎn)的場(chǎng)景中, 性能開(kāi)銷是我們不能接受的

我們開(kāi)放一個(gè) destroyOnLeave 配置,用于設(shè)置 移出時(shí) 是否銷毀對(duì)應(yīng) 提示節(jié)點(diǎn)
const onMouseLeave = () => {
const elVm = vmMap.get(el)
if (!elVm) return
elVm.disabled = true
elVm.$nextTick(elVm.unMount) //卸載 tooltip,在 main.js 做了注釋
vmMap.set(el, null) // 銷毀內(nèi)存
}
if (destroyOnLeave) el.addEventListener('mouseleave', onMouseLeave)其他優(yōu)化項(xiàng)
其他一些常規(guī)的 eventListener 的移除操作,自我優(yōu)化,就不浪費(fèi)彥祖?zhèn)兊那啻毫?/p>
完整代碼
- main.js
import Popper from 'element-ui/src/utils/vue-popper'
import debounce from 'throttle-debounce/debounce'
import { addClass, removeClass, on, off } from 'element-ui/src/utils/dom'
import { generateId } from 'element-ui/src/utils/util'
import Vue from 'vue'
export default {
name: 'TooltipWrapper',
mixins: [Popper],
props: {
openDelay: {
type: Number,
default: 0
},
disabled: Boolean,
manual: Boolean,
effect: {
type: String,
default: 'dark'
},
arrowOffset: {
type: Number,
default: 0
},
popperClass: String,
content: String,
visibleArrow: {
default: true
},
transition: {
type: String,
default: 'el-fade-in-linear'
},
popperOptions: {
default () {
return {
boundariesPadding: 10,
gpuAcceleration: false
}
}
},
enterable: {
type: Boolean,
default: true
},
hideAfter: {
type: Number,
default: 0
},
tabindex: {
type: Number,
default: 0
}
},
data () {
return {
tooltipId: `el-tooltip-${generateId()}`,
timeoutPending: null,
focusing: false
}
},
beforeCreate () {
if (this.$isServer) return
this.popperVM = new Vue({
data: { node: '' },
render (h) {
return this.node
}
}).$mount()
this.debounceClose = debounce(200, () => this.handleClosePopper())
},
render (h) {
if (this.popperVM) {
this.popperVM.node = (
<transition
name={ this.transition }
onAfterLeave={ this.doDestroy }>
<div
onMouseleave={ () => { this.setExpectedState(false); this.debounceClose() } }
onMouseenter= { () => { this.setExpectedState(true) } }
ref="popper"
role="tooltip"
id={this.tooltipId}
aria-hidden={ (this.disabled || !this.showPopper) ? 'true' : 'false' }
v-show={!this.disabled && this.showPopper}
class={
['el-tooltip__popper', 'is-' + this.effect, this.popperClass]
}>
{ this.$slots.content || this.content }
</div>
</transition>)
}
const firstElement = this.getFirstElement()
if (!firstElement) return null
const data = firstElement.data = firstElement.data || {}
data.staticClass = this.addTooltipClass(data.staticClass)
return firstElement
},
watch: {
focusing (val) {
if (val) {
addClass(this.referenceElm, 'focusing')
} else {
removeClass(this.referenceElm, 'focusing')
}
}
},
methods: {
// 掛載目標(biāo)節(jié)點(diǎn)
setEl (el) {
this.target = el
},
setContent (content) {
this.content = content
},
init () {
this.referenceElm = this.target
if (this.target.nodeType === 1) {
this.target.setAttribute('aria-describedby', this.tooltipId)
this.target.setAttribute('tabindex', this.tabindex)
on(this.referenceElm, 'mouseenter', this.show)
on(this.referenceElm, 'mouseleave', this.hide)
on(this.referenceElm, 'focus', () => {
if (!this.$slots.default || !this.$slots.default.length) {
this.handleFocus()
return
}
const instance = this.$slots.default[0].componentInstance
if (instance && instance.focus) {
instance.focus()
} else {
this.handleFocus()
}
})
on(this.referenceElm, 'blur', this.handleBlur)
on(this.referenceElm, 'click', this.removeFocusing)
}
// fix issue https://github.com/ElemeFE/element/issues/14424
if (this.value && this.popperVM) {
this.popperVM.$nextTick(() => {
if (this.value) {
this.updatePopper()
}
})
}
},
show () {
this.setExpectedState(true)
this.handleShowPopper()
},
hide () {
this.setExpectedState(false)
this.debounceClose()
},
handleFocus () {
this.focusing = true
this.show()
},
handleBlur () {
this.focusing = false
this.hide()
},
removeFocusing () {
this.focusing = false
},
addTooltipClass (prev) {
if (!prev) {
return 'el-tooltip'
} else {
return 'el-tooltip ' + prev.replace('el-tooltip', '')
}
},
handleShowPopper () {
if (!this.expectedState || this.manual) return
clearTimeout(this.timeout)
this.timeout = setTimeout(() => {
this.showPopper = true
}, this.openDelay)
if (this.hideAfter > 0) {
this.timeoutPending = setTimeout(() => {
this.showPopper = false
}, this.hideAfter)
}
},
handleClosePopper () {
if (this.enterable && this.expectedState || this.manual) return
clearTimeout(this.timeout)
if (this.timeoutPending) {
clearTimeout(this.timeoutPending)
}
this.showPopper = false
if (this.disabled) {
this.doDestroy()
}
},
setExpectedState (expectedState) {
if (expectedState === false) {
clearTimeout(this.timeoutPending)
}
this.expectedState = expectedState
},
getFirstElement () {
if (this.slotEl) return this.slotEl
const slots = this.$slots.default
if (!Array.isArray(slots)) return null
let element = null
for (let index = 0; index < slots.length; index++) {
if (slots[index] && slots[index].tag) {
element = slots[index]
}
}
return element
},
unMount () {
if (this.popperVM) {
this.popperVM.$destroy() // 銷毀 popperVM 實(shí)例
this.popperVM.node && this.popperVM.node.elm.remove() // 移除對(duì)應(yīng)的 tooltip節(jié)點(diǎn)
}
const reference = this.referenceElm
// 解綁事件
if (reference.nodeType === 1) {
off(reference, 'mouseenter', this.show)
off(reference, 'mouseleave', this.hide)
off(reference, 'focus', this.handleFocus)
off(reference, 'blur', this.handleBlur)
off(reference, 'click', this.removeFocusing)
}
this.$nextTick(() => {
this.doDestroy() // 調(diào)用 mixins的 Popper.doDestroy 銷毀 popper
})
}
},
beforeDestroy () {
this.popperVM && this.popperVM.$destroy()
},
destroyed () {
const reference = this.referenceElm
if (reference.nodeType === 1) {
off(reference, 'mouseenter', this.show)
off(reference, 'mouseleave', this.hide)
off(reference, 'focus', this.handleFocus)
off(reference, 'blur', this.handleBlur)
off(reference, 'click', this.removeFocusing)
}
}
}- directive.js
import Vue from 'vue'
import Tooltip from './main'
const vmMap = new WeakMap()
const listenerMap = new WeakMap()
function getElStyleAttr (element, attr) {
const styles = window.getComputedStyle(element)
return styles[attr]
}
const isOverflow = (target) => {
const scrollWidth = target.scrollWidth
const offsetWidth = target.offsetWidth
const range = document.createRange()
range.setStart(target, 0)
range.setEnd(target, target.childNodes.length)
const rangeWidth = range.getBoundingClientRect().width
const padding = (parseInt(getElStyleAttr(target, 'paddingLeft'), 10) || 0) + (parseInt(getElStyleAttr(target, 'paddingRight'), 10) || 0)
return (rangeWidth + padding > target.offsetWidth) || scrollWidth > offsetWidth
}
export const ellipsisTooltip = {
bind (el, binding, vnode, oldVnode) {
const { value: { placement, disabled, content, effect, destroyOnLeave } = {} } = binding
if (disabled) return
// 加上超出...樣式
el.style.overflow = 'hidden'
el.style.textOverflow = 'ellipsis'
el.style.whiteSpace = 'nowrap'
const onMouseLeave = () => {
const elVm = vmMap.get(el)
if (!elVm) return
elVm.disabled = true
elVm.$nextTick(elVm.unMount)
vmMap.set(el, null)
}
const onMouseEnter = (e) => {
const elVm = vmMap.get(el)
// 需要展示
if (isOverflow(el)) {
if (elVm) {
elVm.disabled = false
return
}
const vm = new Vue(Tooltip).$mount()
vm.placement = placement || 'top-start'
vm.effect = effect || 'dark'
vm.setEl(el)
vm.setContent(content || el.innerText)
vm.init()
vm.show()
vmMap.set(el, vm)
if (destroyOnLeave) el.addEventListener('mouseleave', onMouseLeave)
} else {
if (elVm) elVm.disabled = true
}
}
listenerMap.set(el, [
['mouseenter', onMouseEnter]
]) // 用于拓展后續(xù)的監(jiān)聽(tīng)
el.addEventListener('mouseenter', onMouseEnter)
},
unbind (el) {
const events = listenerMap.get(el)
if (events?.length) {
events.forEach(([name, event]) => el.removeEventListener(name, event))
}
}
}以上就是使用vue指令解決文本的溢出提示的問(wèn)題的詳細(xì)內(nèi)容,更多關(guān)于vue指令解決文本溢出提示的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
vue+elemet實(shí)現(xiàn)表格手動(dòng)合并行列
這篇文章主要為大家詳細(xì)介紹了vue+elemet實(shí)現(xiàn)表格手動(dòng)合并行列,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-08-08
vue 實(shí)現(xiàn)基礎(chǔ)組件的自動(dòng)化全局注冊(cè)
這篇文章主要介紹了vue 實(shí)現(xiàn)基礎(chǔ)組件的自動(dòng)化全局注冊(cè)的方法,幫助大家更好的理解和使用vue框架,感興趣的朋友可以了解下2020-12-12
解讀Vue3中keep-alive和動(dòng)態(tài)組件的實(shí)現(xiàn)邏輯
這篇文章主要介紹了Vue3中keep-alive和動(dòng)態(tài)組件的實(shí)現(xiàn)邏輯,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-05-05
vue項(xiàng)目運(yùn)行或打包時(shí),頻繁內(nèi)存溢出情況問(wèn)題
這篇文章主要介紹了vue項(xiàng)目運(yùn)行或打包時(shí),頻繁內(nèi)存溢出情況的解決方案,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-04-04
Vue實(shí)現(xiàn)導(dǎo)出Excel表格文件提示“文件已損壞無(wú)法打開(kāi)”的解決方法
xlsx用于讀取解析和寫入Excel文件的JavaScript庫(kù),它提供了一系列的API處理Excel文件,使用該庫(kù),可以將數(shù)據(jù)轉(zhuǎn)換Excel文件并下載到本地,適用于在前端直接生成Excel文件,這篇文章主要介紹了Vue實(shí)現(xiàn)導(dǎo)出Excel表格,提示文件已損壞,無(wú)法打開(kāi)的解決方法,需要的朋友可以參考下2024-01-01
Vue.set() this.$set()引發(fā)的視圖更新思考及注意事項(xiàng)
this.$set()和Vue.set()本質(zhì)方法一樣,前者可以用在methods中使用。這篇文章主要介紹了Vue.set() this.$set()引發(fā)的視圖更新思考及注意事項(xiàng),需要的朋友可以參考下2018-08-08
vue.js 實(shí)現(xiàn)點(diǎn)擊div標(biāo)簽時(shí)改變樣式
這篇文章主要介紹了vue.js 實(shí)現(xiàn)點(diǎn)擊div標(biāo)簽時(shí)改變樣式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-08-08

