wangfumin 6 дней назад
Родитель
Сommit
0ad9c854ed

+ 7 - 1
src/components/bill-ui/components/input/index.vue

@@ -26,6 +26,7 @@ import search from './search.vue'
 import richtext from './richtext.vue'
 import color from './color.vue'
 import multiple from './multiple.vue'
+import richinput from './richInput.vue'
 import {
     inputPropsDesc,
     textPropsDesc,
@@ -42,7 +43,8 @@ import {
     colorPropsDesc,
     inputEmitDesc,
     textEmitsDesc,
-    multiplePropsDesc
+    multiplePropsDesc,
+    richinputPropsDesc
 } from './state'
 
 const types = {
@@ -97,6 +99,10 @@ const types = {
     multiple: {
         component: multiple,
         propsDesc: multiplePropsDesc
+    },
+    richinput: {
+        component: richinput,
+        propsDesc: richinputPropsDesc,
     }
 }
 

+ 765 - 0
src/components/bill-ui/components/input/richInput.vue

@@ -0,0 +1,765 @@
+<template>
+    <div
+        class="input textarea"
+        :class="{ suffix: showBar, disabled, right, 'has-toolbar': enableToolbar }"
+        ref="textRef"
+    >
+        <div
+            :contenteditable="canEdit"
+            class="ui-text input-div"
+            @input="inputHandler"
+            :placeholder="props.placeholder"
+            @click="emit('click')"
+            @focus="focusHandler"
+            @blur="blurHandler"
+            @paste="pasteHandler"
+            @compositionstart="compositionstartHandler"
+            @compositionend="compositionendHandler"
+            @mouseup="selectionUpdate"
+            @keyup="selectionUpdate"
+            ref="inputRef"
+            v-bind="other"
+        />
+        <span class="replace"></span>
+        <span v-if="showBar" class="retouch">
+            <div v-if="enableToolbar" class="toolbar" @mousedown.prevent>
+                <div class="toolbar-item dropdown font-size" :class="{ open: showFontSizeMenu }" @mousedown.prevent="toggleFontSizeMenu">
+                    <span class="dropdown-value">{{ currentFontSize }}</span>
+                    <span class="dropdown-arrow">▾</span>
+                    <div v-if="showFontSizeMenu" class="dropdown-menu" @mousedown.prevent>
+                        <div
+                            v-for="size in fontSizeOptions"
+                            :key="size"
+                            class="dropdown-option"
+                            :class="{ active: size === currentFontSize }"
+                            @mousedown.prevent="applyFontSize(size)"
+                        >
+                            {{ size }}
+                        </div>
+                    </div>
+                </div>
+
+                <button
+                    class="toolbar-item btn"
+                    type="button"
+                    :class="{ active: isBold }"
+                    :disabled="!canEdit"
+                    @mousedown.prevent="toggleBold"
+                >
+                    B
+                </button>
+
+                <button
+                    class="toolbar-item btn"
+                    type="button"
+                    :disabled="!canEdit"
+                    @mousedown.prevent="triggerTextColor"
+                >
+                    <span class="a-letter" :style="{ '--a-color': textColor }">A</span>
+                </button>
+                <input ref="textColorInputRef" class="color-input" type="color" :value="textColor" @input="applyTextColor" />
+
+                <button
+                    class="toolbar-item btn"
+                    type="button"
+                    :disabled="!canEdit"
+                    @mousedown.prevent="triggerHighlightColor"
+                >
+                    <span class="hl-icon" :style="{ '--hl-color': highlightColor }"></span>
+                </button>
+                <input ref="highlightColorInputRef" class="color-input" type="color" :value="highlightColor" @input="applyHighlightColor" />
+
+                <div class="toolbar-item dropdown align-menu" :class="{ open: showAlignMenu }" @mousedown.prevent="toggleAlignMenu">
+                    <span class="align-icon">{{ alignLabel }}</span>
+                    <span class="dropdown-arrow">▾</span>
+                    <div v-if="showAlignMenu" class="dropdown-menu" @mousedown.prevent>
+                        <div class="dropdown-option" :class="{ active: align === 'left' }" @mousedown.prevent="applyAlign('left')">左对齐</div>
+                        <div class="dropdown-option" :class="{ active: align === 'center' }" @mousedown.prevent="applyAlign('center')">居中</div>
+                        <div class="dropdown-option" :class="{ active: align === 'right' }" @mousedown.prevent="applyAlign('right')">右对齐</div>
+                    </div>
+                </div>
+
+                <div class="toolbar-item link">
+                    <button class="btn link-btn-trigger" type="button" :disabled="!canEdit" @mousedown.prevent="openLinkInput">
+                        链接
+                    </button>
+                    <div v-if="showLinkInput" class="link-pop" @mousedown.prevent>
+                        <input class="link-input" v-model="linkValue" placeholder="请输入链接" />
+                        <button class="link-btn" type="button" @mousedown.prevent="confirmLink">确认</button>
+                    </div>
+                </div>
+            </div>
+            <slot v-else name="icon" />
+            <span v-if="props.maxlength" class="len">
+                <span>{{ length }}</span> / {{ maxlength }}
+            </span>
+        </span>
+    </div>
+</template>
+
+<script setup>
+import { richtextPropsDesc } from './state'
+import { computed, nextTick, onMounted, onUnmounted, ref, useSlots, watchEffect } from 'vue'
+const props = defineProps({
+    ...richtextPropsDesc,
+})
+
+const emit = defineEmits(['update:modelValue', 'focus', 'blur', 'click', 'updatePos'])
+const slots = useSlots()
+const textRef = ref(null)
+const inputRef = ref(null)
+const length = ref(0)
+const textColor = ref('#ffffff')
+const highlightColor = ref('#ffe58f')
+const textColorInputRef = ref(null)
+const highlightColorInputRef = ref(null)
+const showFontSizeMenu = ref(false)
+const showAlignMenu = ref(false)
+const showLinkInput = ref(false)
+const linkValue = ref('')
+const savedRange = ref(null)
+const isBold = ref(false)
+const align = ref('left')
+const currentFontSize = ref(12)
+
+const enableToolbar = computed(() => !!props.rich)
+const showBar = computed(() => enableToolbar.value || !!props.maxlength || !!slots.icon)
+const canEdit = computed(() => !props.disabled && !props.readonly)
+
+const fontSizeOptions = computed(() => Array.from({ length: 37 }, (_, idx) => idx + 12))
+const alignLabel = computed(() => (align.value === 'center' ? '居中' : align.value === 'right' ? '右' : '左'))
+
+const getTextLen = (text = '') => text.replace(/[\u200B\uFEFF]/g, '').length
+const normalizeUrl = raw => {
+    const val = String(raw || '').trim()
+    if (!val) return ''
+    const low = val.toLowerCase()
+    if (low.startsWith('javascript:') || low.startsWith('data:')) return ''
+    if (/^(https?:\/\/|mailto:|tel:)/i.test(val)) return val
+    return 'https://' + val
+}
+
+const syncFromDom = () => {
+    if (!inputRef.value) return
+    length.value = getTextLen(inputRef.value.textContent || '')
+    emit('update:modelValue', inputRef.value.innerHTML)
+}
+
+const saveSelection = () => {
+    const sel = window.getSelection?.()
+    if (!sel || sel.rangeCount === 0) return
+    const range = sel.getRangeAt(0)
+    if (!inputRef.value || !inputRef.value.contains(range.commonAncestorContainer)) return
+    savedRange.value = range.cloneRange()
+}
+
+const restoreSelection = () => {
+    if (!savedRange.value || !window.getSelection || !inputRef.value) return false
+    const range = savedRange.value
+    if (range?.startContainer?.isConnected === false || range?.endContainer?.isConnected === false) {
+        savedRange.value = null
+        return false
+    }
+    if (!inputRef.value.contains(range.commonAncestorContainer)) return false
+    const sel = window.getSelection()
+    if (!sel) return false
+    try {
+        sel.removeAllRanges()
+        sel.addRange(range)
+        return true
+    } catch (e) {
+        savedRange.value = null
+        return false
+    }
+}
+
+const getAlignFromRange = range => {
+    if (!inputRef.value || !window.getComputedStyle) return null
+    let node = range?.startContainer
+    if (!node) return null
+    if (node.nodeType === Node.TEXT_NODE) node = node.parentElement
+    if (!(node instanceof Element)) return null
+
+    let cur = node
+    while (cur && cur !== inputRef.value) {
+        const style = window.getComputedStyle(cur)
+        const display = style.display
+        if (display === 'block' || display === 'list-item') {
+            const v = style.textAlign
+            if (v === 'center') return 'center'
+            if (v === 'right' || v === 'end') return 'right'
+            if (v === 'left' || v === 'start' || v === 'justify') return 'left'
+        }
+        cur = cur.parentElement
+    }
+
+    const v = window.getComputedStyle(inputRef.value).textAlign
+    if (v === 'center') return 'center'
+    if (v === 'right' || v === 'end') return 'right'
+    if (v === 'left' || v === 'start' || v === 'justify') return 'left'
+    return null
+}
+
+const selectionUpdate = () => {
+    const sel = window.getSelection?.()
+    if (!sel || sel.rangeCount === 0) return
+    const range = sel.getRangeAt(0)
+    if (!inputRef.value || !inputRef.value.contains(range.commonAncestorContainer)) return
+    savedRange.value = range.cloneRange()
+    if (!document.queryCommandState) return
+    try {
+        isBold.value = document.queryCommandState('bold')
+        if (document.queryCommandState('justifyCenter')) align.value = 'center'
+        else if (document.queryCommandState('justifyRight')) align.value = 'right'
+        else if (document.queryCommandState('justifyLeft')) align.value = 'left'
+    } catch (e) {}
+    const nextAlign = getAlignFromRange(range)
+    if (nextAlign) align.value = nextAlign
+}
+
+const focusEditor = () => {
+    if (!inputRef.value) return
+    inputRef.value.focus()
+}
+
+const ensureEditorSelection = () => {
+    if (!inputRef.value) return false
+    focusEditor()
+    const sel = window.getSelection?.()
+    if (!sel) return false
+    if (sel.rangeCount > 0) {
+        const range = sel.getRangeAt(0)
+        if (inputRef.value.contains(range.commonAncestorContainer)) {
+            savedRange.value = range.cloneRange()
+            return true
+        }
+    }
+    if (restoreSelection()) return true
+    const range = document.createRange()
+    range.selectNodeContents(inputRef.value)
+    range.collapse(false)
+    try {
+        sel.removeAllRanges()
+        sel.addRange(range)
+        savedRange.value = range.cloneRange()
+        return true
+    } catch (e) {
+        return false
+    }
+}
+
+const wrapSelectionWithStyle = style => {
+    if (!canEdit.value) return
+    ensureEditorSelection()
+    const sel = window.getSelection?.()
+    if (!sel || sel.rangeCount === 0) return
+    const range = sel.getRangeAt(0)
+    if (!inputRef.value || !inputRef.value.contains(range.commonAncestorContainer)) return
+
+    const span = document.createElement('span')
+    Object.keys(style).forEach(key => {
+        span.style[key] = style[key]
+    })
+
+    if (range.collapsed) {
+        span.appendChild(document.createTextNode('\u200B'))
+        range.insertNode(span)
+        const nextRange = document.createRange()
+        nextRange.setStart(span.firstChild, 1)
+        nextRange.collapse(true)
+        sel.removeAllRanges()
+        sel.addRange(nextRange)
+        savedRange.value = nextRange.cloneRange()
+    } else {
+        const content = range.extractContents()
+        span.appendChild(content)
+        range.insertNode(span)
+        const nextRange = document.createRange()
+        nextRange.selectNodeContents(span)
+        nextRange.collapse(false)
+        sel.removeAllRanges()
+        sel.addRange(nextRange)
+        savedRange.value = nextRange.cloneRange()
+    }
+
+    syncFromDom()
+    selectionUpdate()
+}
+
+const toggleFontSizeMenu = () => {
+    if (!enableToolbar.value) return
+    saveSelection()
+    showFontSizeMenu.value = !showFontSizeMenu.value
+    showAlignMenu.value = false
+    showLinkInput.value = false
+}
+const applyFontSize = size => {
+    currentFontSize.value = Number(size)
+    showFontSizeMenu.value = false
+    wrapSelectionWithStyle({ fontSize: currentFontSize.value + 'px' })
+}
+
+const toggleBold = () => {
+    if (!canEdit.value) return
+    ensureEditorSelection()
+    document.execCommand?.('bold')
+    nextTick(() => {
+        syncFromDom()
+        selectionUpdate()
+    })
+}
+
+const triggerTextColor = () => {
+    if (!canEdit.value) return
+    saveSelection()
+    textColorInputRef.value?.click?.()
+}
+const applyTextColor = ev => {
+    if (!canEdit.value) return
+    const val = ev?.target?.value
+    if (!val) return
+    textColor.value = val
+    ensureEditorSelection()
+    document.execCommand?.('styleWithCSS', false, true)
+    document.execCommand?.('foreColor', false, val)
+    nextTick(() => {
+        syncFromDom()
+        selectionUpdate()
+    })
+}
+
+const triggerHighlightColor = () => {
+    if (!canEdit.value) return
+    saveSelection()
+    highlightColorInputRef.value?.click?.()
+}
+const applyHighlightColor = ev => {
+    if (!canEdit.value) return
+    const val = ev?.target?.value
+    if (!val) return
+    highlightColor.value = val
+    ensureEditorSelection()
+    document.execCommand?.('styleWithCSS', false, true)
+    document.execCommand?.('hiliteColor', false, val) || document.execCommand?.('backColor', false, val)
+    nextTick(() => {
+        syncFromDom()
+        selectionUpdate()
+    })
+}
+
+const toggleAlignMenu = () => {
+    if (!enableToolbar.value) return
+    saveSelection()
+    showAlignMenu.value = !showAlignMenu.value
+    showFontSizeMenu.value = false
+    showLinkInput.value = false
+}
+const applyAlign = mode => {
+    if (!canEdit.value) return
+    align.value = mode
+    showAlignMenu.value = false
+    ensureEditorSelection()
+    const cmd = mode === 'center' ? 'justifyCenter' : mode === 'right' ? 'justifyRight' : 'justifyLeft'
+    document.execCommand?.(cmd)
+    nextTick(() => {
+        syncFromDom()
+        selectionUpdate()
+    })
+}
+
+const openLinkInput = () => {
+    if (!canEdit.value) return
+    saveSelection()
+    showLinkInput.value = true
+    showFontSizeMenu.value = false
+    showAlignMenu.value = false
+    nextTick(() => {
+        const el = textRef.value?.querySelector?.('.link-input')
+        el?.focus?.()
+    })
+}
+const confirmLink = () => {
+    if (!canEdit.value) return
+    const url = normalizeUrl(linkValue.value)
+    showLinkInput.value = false
+    linkValue.value = ''
+    if (!url) return
+    ensureEditorSelection()
+    const sel = window.getSelection?.()
+    if (!sel || sel.rangeCount === 0) return
+    const range = sel.getRangeAt(0)
+    if (!inputRef.value || !inputRef.value.contains(range.commonAncestorContainer)) return
+
+    if (range.collapsed) {
+        const a = document.createElement('a')
+        a.href = url
+        a.target = '_blank'
+        a.rel = 'noopener noreferrer'
+        a.textContent = url
+        range.insertNode(a)
+        const nextRange = document.createRange()
+        nextRange.setStartAfter(a)
+        nextRange.collapse(true)
+        sel.removeAllRanges()
+        sel.addRange(nextRange)
+        savedRange.value = nextRange.cloneRange()
+        syncFromDom()
+        selectionUpdate()
+        return
+    }
+
+    try {
+        document.execCommand?.('createLink', false, url)
+        inputRef.value.querySelectorAll('a').forEach(a => {
+            a.target = '_blank'
+            a.rel = 'noopener noreferrer'
+        })
+        nextTick(() => {
+            syncFromDom()
+            selectionUpdate()
+        })
+    } catch (e) {}
+}
+
+const updateContent = html => {
+    if (!inputRef.value) return
+    inputRef.value.innerHTML = html || ''
+    length.value = getTextLen(inputRef.value.textContent || '')
+}
+
+watchEffect(() => {
+    if (inputRef.value && props.modelValue !== inputRef.value.innerHTML) {
+        updateContent(props.modelValue)
+    }
+})
+
+let inComposition = false
+const compositionstartHandler = () => {
+    inComposition = true
+}
+const compositionendHandler = ev => {
+    inComposition = false
+    inputHandler(ev)
+}
+
+const inputHandler = ev => {
+    if (inComposition) return
+    const nextLen = getTextLen(ev.target.textContent || '')
+    if (!props.maxlength || nextLen <= Number(props.maxlength)) {
+        length.value = nextLen
+        emit('update:modelValue', ev.target.innerHTML || '')
+    } else {
+        nextTick(() => {
+            if (ev.target.innerHTML !== props.modelValue.toString()) {
+                updateContent(props.modelValue.toString())
+                inputFocus()
+            }
+        })
+    }
+    nextTick(selectionUpdate)
+}
+//获取当前光标位置
+const getCursortPosition = function (element = inputRef.value) {
+    var caretOffset = 0
+    var doc = element.ownerDocument || element.document
+    var win = doc.defaultView || doc.parentWindow
+    var sel
+    if (typeof win.getSelection != 'undefined') {
+        //谷歌、火狐
+        sel = win.getSelection()
+        if (sel.rangeCount > 0) {
+            //选中的区域
+            var range = win.getSelection().getRangeAt(0)
+            var preCaretRange = range.cloneRange() //克隆一个选中区域
+            preCaretRange.selectNodeContents(element) //设置选中区域的节点内容为当前节点
+            preCaretRange.setEnd(range.endContainer, range.endOffset) //重置选中区域的结束位置
+            caretOffset = preCaretRange.toString().length
+        }
+    } else if ((sel = doc.selection) && sel.type != 'Control') {
+        //IE
+        var textRange = sel.createRange()
+        var preCaretTextRange = doc.body.createTextRange()
+        preCaretTextRange.moveToElementText(element)
+        preCaretTextRange.setEndPoint('EndToEnd', textRange)
+        caretOffset = preCaretTextRange.text.length
+    }
+    return caretOffset
+}
+
+let interval
+const focusHandler = ev => {
+    clearInterval(interval)
+    interval = setInterval(() => {
+        emit('updatePos', getCursortPosition())
+    }, 100)
+    emit('focus')
+    nextTick(selectionUpdate)
+}
+const blurHandler = () => {
+    clearInterval(interval)
+    emit('blur')
+}
+
+const inputFocus = () => {
+    inputRef.value.focus()
+    const range = window.getSelection()
+    range.selectAllChildren(inputRef.value)
+    range.collapseToEnd()
+}
+
+const getPasteText = text => {
+    if (!props.maxlength) {
+        return text
+    }
+
+    const $el = document.createElement('div')
+    $el.innerHTML = text
+    const allowLen = Number(props.maxlength) - length.value
+    const rawText = $el.textContent || ''
+    if (getTextLen(rawText) > allowLen) {
+        return rawText.replace(/[\u200B\uFEFF]/g, '').substring(0, allowLen)
+    } else {
+        return text
+    }
+}
+
+const pasteHandler = event => {
+    event.preventDefault()
+    var text
+    var clp = (event.originalEvent || event).clipboardData
+    // 兼容针对于opera ie等浏览器
+    if (clp === undefined || clp === null) {
+        text = window.clipboardData.getData('text') || ''
+        if (text !== '') {
+            if (window.getSelection) {
+                // 针对于ie11 10 9 safari
+                var newNode = document.createElement('span')
+                newNode.innerHTML = getPasteText(text)
+                window.getSelection().getRangeAt(0).insertNode(newNode)
+            } else {
+                document.selection.createRange().pasteHTML(text)
+            }
+        }
+    } else {
+        // 兼容chorme或hotfire
+        text = clp.getData('text/plain') || ''
+        if (text !== '') {
+            document.execCommand('insertText', false, getPasteText(text))
+        }
+    }
+}
+
+const onDocMousedown = ev => {
+    const root = textRef.value
+    if (!root) return
+    const target = ev.target
+    if (showFontSizeMenu.value) {
+        const el = root.querySelector('.font-size')
+        if (!el || !el.contains(target)) showFontSizeMenu.value = false
+    }
+    if (showAlignMenu.value) {
+        const el = root.querySelector('.align-menu')
+        if (!el || !el.contains(target)) showAlignMenu.value = false
+    }
+    if (showLinkInput.value) {
+        const pop = root.querySelector('.link-pop')
+        const btn = root.querySelector('.link-btn-trigger')
+        if ((!pop || !pop.contains(target)) && (!btn || !btn.contains(target))) showLinkInput.value = false
+    }
+
+    if (!root.contains(target)) {
+        savedRange.value = null
+        window.getSelection?.()?.removeAllRanges?.()
+    }
+}
+
+onMounted(() => {
+    document.addEventListener('mousedown', onDocMousedown)
+})
+onUnmounted(() => {
+    document.removeEventListener('mousedown', onDocMousedown)
+})
+
+defineExpose({
+    root: textRef,
+    input: inputRef,
+    getCursortPosition: getCursortPosition,
+})
+</script>
+
+<style scoped>
+.toolbar {
+    display: flex;
+    align-items: center;
+    gap: 8px;
+    min-width: 0;
+}
+
+.btn {
+    border: none;
+    background: transparent;
+    color: rgba(255, 255, 255, 0.8);
+    padding: 0 6px;
+    height: 22px;
+    line-height: 22px;
+    border-radius: 4px;
+    cursor: pointer;
+    font-weight: 600;
+    display: inline-flex;
+    align-items: center;
+    justify-content: center;
+    user-select: none;
+}
+
+.btn:disabled {
+    opacity: 0.4;
+    cursor: not-allowed;
+}
+
+.btn.active {
+    color: var(--colors-primary-base);
+    background-color: rgba(var(--colors-primary-fill), 0.18);
+}
+
+.dropdown {
+    position: relative;
+    display: inline-flex;
+    align-items: center;
+    gap: 6px;
+    padding: 0 6px;
+    height: 22px;
+    border-radius: 4px;
+    cursor: pointer;
+    color: rgba(255, 255, 255, 0.8);
+    background: rgba(255, 255, 255, 0.06);
+    border: 1px solid rgba(255, 255, 255, 0.08);
+    user-select: none;
+}
+
+.dropdown-value {
+    font-size: 12px;
+    width: 24px;
+    text-align: center;
+}
+
+.dropdown-arrow {
+    font-size: 10px;
+    opacity: 0.7;
+}
+
+.dropdown-menu {
+    position: absolute;
+    bottom: calc(100% + 6px);
+    left: 0;
+    min-width: 92px;
+    max-height: 220px;
+    overflow: auto;
+    background: rgba(27, 27, 28, 0.95);
+    border: 1px solid rgba(255, 255, 255, 0.1);
+    border-radius: 6px;
+    padding: 6px 0;
+    z-index: 2;
+    backdrop-filter: blur(6px);
+}
+
+.dropdown-option {
+    height: 28px;
+    line-height: 28px;
+    padding: 0 10px;
+    font-size: 12px;
+    color: rgba(255, 255, 255, 0.85);
+    cursor: pointer;
+}
+
+.dropdown-option:hover {
+    background: rgba(255, 255, 255, 0.06);
+}
+
+.dropdown-option.active {
+    color: var(--colors-primary-base);
+}
+
+.a-letter {
+    position: relative;
+    display: inline-flex;
+    align-items: center;
+    justify-content: center;
+    width: 14px;
+    height: 14px;
+    color: rgba(255, 255, 255, 0.95);
+}
+.a-letter::after {
+    content: '';
+    position: absolute;
+    left: 1px;
+    right: 1px;
+    bottom: -3px;
+    height: 2px;
+    border-radius: 2px;
+    background: var(--a-color, #ffffff);
+}
+
+.hl-icon {
+    width: 14px;
+    height: 14px;
+    border-radius: 3px;
+    background: var(--hl-color, #ffe58f);
+    box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.18) inset;
+}
+
+.color-input {
+    position: absolute;
+    width: 1px;
+    height: 1px;
+    opacity: 0;
+    pointer-events: none;
+}
+
+.link {
+    position: relative;
+    display: inline-flex;
+    align-items: center;
+}
+
+.link-pop {
+    position: absolute;
+    bottom: calc(100% + 6px);
+    left: 0;
+    padding: 10px;
+    width: 220px;
+    display: flex;
+    gap: 8px;
+    background: rgba(27, 27, 28, 0.95);
+    border: 1px solid rgba(255, 255, 255, 0.1);
+    border-radius: 6px;
+    z-index: 3;
+    backdrop-filter: blur(6px);
+}
+
+.link-input {
+    flex: 1;
+    height: 28px;
+    border-radius: 4px;
+    border: 1px solid rgba(255, 255, 255, 0.12);
+    background: rgba(255, 255, 255, 0.06);
+    outline: none;
+    color: rgba(255, 255, 255, 0.9);
+    padding: 0 8px;
+    font-size: 12px;
+}
+
+.link-btn {
+    height: 28px;
+    padding: 0 10px;
+    border-radius: 4px;
+    border: 1px solid rgba(255, 255, 255, 0.12);
+    background: rgba(var(--colors-primary-fill), 0.2);
+    color: rgba(255, 255, 255, 0.9);
+    cursor: pointer;
+    font-size: 12px;
+}
+
+.has-toolbar > .retouch {
+    justify-content: space-between !important;
+}
+</style>

+ 8 - 0
src/components/bill-ui/components/input/state.js

@@ -123,6 +123,13 @@ export const richtextPropsDesc = {
     onUpdatePos: Function,
 }
 
+export const richinputPropsDesc = {
+    ...textareaPropsDesc,
+    onUpdatePos: Function,
+}
+
+
+
 export const selectPropsDesc = {
     ...textPropsDesc,
     isTransform: {
@@ -212,6 +219,7 @@ const summary = {
     ...filePropsDesc,
     ...searchPropsDesc,
     ...richtextPropsDesc,
+    ...richinputPropsDesc,
     ...colorPropsDesc,
 }
 for (let key in summary) {

+ 2 - 1
src/views/tagging/hot/edit.vue

@@ -46,7 +46,8 @@
         width="100%"
         height="158px"
         :placeholder="defStyleType.id === type ? '描述:' : '特征描述:'"
-        type="richtext"
+        type="richinput"
+        :rich="true"
         v-model="tagging.desc"
         :maxlength="200"
       />