diff --git a/packages/common b/packages/common index 21e056075..d73d63dcb 160000 --- a/packages/common +++ b/packages/common @@ -1 +1 @@ -Subproject commit 21e0560751ba680188fc0f97aa8bf79835990084 +Subproject commit d73d63dcbf9202b83798ca84f09c89672e660b74 diff --git a/packages/pro-components/chat/chat-content/chat-content.json b/packages/pro-components/chat/chat-content/chat-content.json index e4071fdd8..c7ef469b2 100644 --- a/packages/pro-components/chat/chat-content/chat-content.json +++ b/packages/pro-components/chat/chat-content/chat-content.json @@ -1,7 +1,12 @@ { "component": true, "styleIsolation": "apply-shared", + "componentGenerics": { + "tail-component": { + "default": "tdesign-miniprogram/chat-markdown/chat-markdown-tail/chat-markdown-tail" + } + }, "usingComponents": { "t-chat-markdown": "tdesign-miniprogram/chat-markdown/chat-markdown" } -} +} \ No newline at end of file diff --git a/packages/pro-components/chat/chat-content/chat-content.wxml b/packages/pro-components/chat/chat-content/chat-content.wxml index a2463c9d1..18ddf2917 100644 --- a/packages/pro-components/chat/chat-content/chat-content.wxml +++ b/packages/pro-components/chat/chat-content/chat-content.wxml @@ -10,7 +10,13 @@ - + diff --git a/packages/pro-components/chat/chat-list/_example/docs/custom-tail/custom-tail.js b/packages/pro-components/chat/chat-list/_example/docs/custom-tail/custom-tail.js new file mode 100644 index 000000000..b89373255 --- /dev/null +++ b/packages/pro-components/chat/chat-list/_example/docs/custom-tail/custom-tail.js @@ -0,0 +1,3 @@ +Component({ + properties: {}, +}); diff --git a/packages/pro-components/chat/chat-list/_example/docs/custom-tail/custom-tail.json b/packages/pro-components/chat/chat-list/_example/docs/custom-tail/custom-tail.json new file mode 100644 index 000000000..66165257f --- /dev/null +++ b/packages/pro-components/chat/chat-list/_example/docs/custom-tail/custom-tail.json @@ -0,0 +1,4 @@ +{ + "component": true, + "styleIsolation": "apply-shared" +} \ No newline at end of file diff --git a/packages/pro-components/chat/chat-list/_example/docs/custom-tail/custom-tail.wxml b/packages/pro-components/chat/chat-list/_example/docs/custom-tail/custom-tail.wxml new file mode 100644 index 000000000..48c6e1257 --- /dev/null +++ b/packages/pro-components/chat/chat-list/_example/docs/custom-tail/custom-tail.wxml @@ -0,0 +1,2 @@ + + diff --git a/packages/pro-components/chat/chat-list/_example/docs/custom-tail/custom-tail.wxss b/packages/pro-components/chat/chat-list/_example/docs/custom-tail/custom-tail.wxss new file mode 100644 index 000000000..7552813db --- /dev/null +++ b/packages/pro-components/chat/chat-list/_example/docs/custom-tail/custom-tail.wxss @@ -0,0 +1,12 @@ +/* 自定义光标样式:蓝色渐变闪烁 */ +.custom-tail { + display: inline-block; + color: #0052d9; + font-weight: bold; + animation: custom-tail-blink 0.8s ease-in-out infinite; +} + +@keyframes custom-tail-blink { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.2; } +} diff --git a/packages/pro-components/chat/chat-list/_example/docs/index.js b/packages/pro-components/chat/chat-list/_example/docs/index.js index 179cbcad6..ef39efe05 100644 --- a/packages/pro-components/chat/chat-list/_example/docs/index.js +++ b/packages/pro-components/chat/chat-list/_example/docs/index.js @@ -189,6 +189,7 @@ Component({ }, ], }, + markdownProps: { streaming: { hasNextChunk: true, tail: true } }, chatId: getUniqueKey(), }; @@ -208,6 +209,7 @@ Component({ complete() { that.setData({ 'chatList[0].message.status': 'complete', + 'chatList[0].markdownProps': {}, loading: false, }); }, diff --git a/packages/pro-components/chat/chat-list/_example/docs/index.json b/packages/pro-components/chat/chat-list/_example/docs/index.json index a56fa5337..7c409dc19 100644 --- a/packages/pro-components/chat/chat-list/_example/docs/index.json +++ b/packages/pro-components/chat/chat-list/_example/docs/index.json @@ -1,7 +1,13 @@ { "component": true, "styleIsolation": "shared", + "componentGenerics": { + "tail-component": { + "default": "./custom-tail/custom-tail" + } + }, "usingComponents": { + "custom-tail": "./custom-tail/custom-tail", "t-chat-message": "tdesign-miniprogram/chat-message/chat-message", "t-chat-content": "tdesign-miniprogram/chat-content/chat-content", "t-chat": "tdesign-miniprogram/chat-list/chat-list", @@ -9,4 +15,4 @@ "t-chat-actionbar": "tdesign-miniprogram/chat-actionbar/chat-actionbar", "t-toast": "tdesign-miniprogram/toast/toast" } -} +} \ No newline at end of file diff --git a/packages/pro-components/chat/chat-list/_example/docs/index.wxml b/packages/pro-components/chat/chat-list/_example/docs/index.wxml index 3a22faae8..04bcc526b 100644 --- a/packages/pro-components/chat/chat-list/_example/docs/index.wxml +++ b/packages/pro-components/chat/chat-list/_example/docs/index.wxml @@ -6,12 +6,26 @@ avatar="{{item.avatar || ''}}" name="{{item.name || ''}}" datetime="{{item.datetime || ''}}" - content="{{item.message.content}}" role="{{item.message.role}}" - chatContentProps="{{chatContentProps}}" placement="{{item.message.role === 'user' ? 'right' : 'left'}}" bind:message-longpress="showPopover" > + + + + + + + + diff --git a/packages/pro-components/chat/chat-markdown/_example/tail/custom-tail/custom-tail.js b/packages/pro-components/chat/chat-markdown/_example/tail/custom-tail/custom-tail.js new file mode 100644 index 000000000..b89373255 --- /dev/null +++ b/packages/pro-components/chat/chat-markdown/_example/tail/custom-tail/custom-tail.js @@ -0,0 +1,3 @@ +Component({ + properties: {}, +}); diff --git a/packages/pro-components/chat/chat-markdown/_example/tail/custom-tail/custom-tail.json b/packages/pro-components/chat/chat-markdown/_example/tail/custom-tail/custom-tail.json new file mode 100644 index 000000000..66165257f --- /dev/null +++ b/packages/pro-components/chat/chat-markdown/_example/tail/custom-tail/custom-tail.json @@ -0,0 +1,4 @@ +{ + "component": true, + "styleIsolation": "apply-shared" +} \ No newline at end of file diff --git a/packages/pro-components/chat/chat-markdown/_example/tail/custom-tail/custom-tail.wxml b/packages/pro-components/chat/chat-markdown/_example/tail/custom-tail/custom-tail.wxml new file mode 100644 index 000000000..48c6e1257 --- /dev/null +++ b/packages/pro-components/chat/chat-markdown/_example/tail/custom-tail/custom-tail.wxml @@ -0,0 +1,2 @@ + + diff --git a/packages/pro-components/chat/chat-markdown/_example/tail/custom-tail/custom-tail.wxss b/packages/pro-components/chat/chat-markdown/_example/tail/custom-tail/custom-tail.wxss new file mode 100644 index 000000000..7552813db --- /dev/null +++ b/packages/pro-components/chat/chat-markdown/_example/tail/custom-tail/custom-tail.wxss @@ -0,0 +1,12 @@ +/* 自定义光标样式:蓝色渐变闪烁 */ +.custom-tail { + display: inline-block; + color: #0052d9; + font-weight: bold; + animation: custom-tail-blink 0.8s ease-in-out infinite; +} + +@keyframes custom-tail-blink { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.2; } +} diff --git a/packages/pro-components/chat/chat-markdown/_example/tail/index.js b/packages/pro-components/chat/chat-markdown/_example/tail/index.js new file mode 100644 index 000000000..81d738c84 --- /dev/null +++ b/packages/pro-components/chat/chat-markdown/_example/tail/index.js @@ -0,0 +1,45 @@ +import markdownData from '../base/mock2.js'; + +const CHUNK_SIZE = 5; +const INTERVAL_MS = 80; + +Page({ + data: { + content: '', + streaming: { hasNextChunk: false, tail: true }, + }, + + onLoad() { + this.startStreaming(); + }, + + startStreaming() { + let index = 0; + + this.setData({ + content: '', + streaming: { hasNextChunk: true, tail: true }, + }); + + const timer = setInterval(() => { + index += CHUNK_SIZE; + const isDone = index >= markdownData.length; + this.setData({ + content: markdownData.slice(0, index), + streaming: { hasNextChunk: !isDone, tail: true }, + }); + if (isDone) clearInterval(timer); + }, INTERVAL_MS); + }, + + handleReplay() { + this.startStreaming(); + }, + + handleNodeTap(e) { + const { node } = e.detail; + if (node && node.type === 'image') { + wx.previewImage({ urls: [node.href], current: node.href }); + } + }, +}); diff --git a/packages/pro-components/chat/chat-markdown/_example/tail/index.json b/packages/pro-components/chat/chat-markdown/_example/tail/index.json new file mode 100644 index 000000000..1f8a5342b --- /dev/null +++ b/packages/pro-components/chat/chat-markdown/_example/tail/index.json @@ -0,0 +1,6 @@ +{ + "usingComponents": { + "t-chat-markdown": "tdesign-miniprogram/chat-markdown/chat-markdown", + "custom-tail": "./custom-tail/custom-tail" + } +} \ No newline at end of file diff --git a/packages/pro-components/chat/chat-markdown/_example/tail/index.wxml b/packages/pro-components/chat/chat-markdown/_example/tail/index.wxml new file mode 100644 index 000000000..46e45bde3 --- /dev/null +++ b/packages/pro-components/chat/chat-markdown/_example/tail/index.wxml @@ -0,0 +1,18 @@ + + + + 默认光标 + + + + + 自定义光标组件 + + + + diff --git a/packages/pro-components/chat/chat-markdown/_example/tail/index.wxss b/packages/pro-components/chat/chat-markdown/_example/tail/index.wxss new file mode 100644 index 000000000..700cf8b15 --- /dev/null +++ b/packages/pro-components/chat/chat-markdown/_example/tail/index.wxss @@ -0,0 +1,4 @@ +.chat-example-block { + background-color: var(--td-bg-color-container); + padding: 32rpx; +} diff --git a/packages/pro-components/chat/chat-markdown/chat-markdown-node/chat-markdown-node.json b/packages/pro-components/chat/chat-markdown/chat-markdown-node/chat-markdown-node.json index 984cca0aa..b83e0193d 100644 --- a/packages/pro-components/chat/chat-markdown/chat-markdown-node/chat-markdown-node.json +++ b/packages/pro-components/chat/chat-markdown/chat-markdown-node/chat-markdown-node.json @@ -1,9 +1,14 @@ { "component": true, "styleIsolation": "apply-shared", + "componentGenerics": { + "tail-component": { + "default": "../chat-markdown-tail/chat-markdown-tail" + } + }, "usingComponents": { "chat-markdown-table": "../chat-markdown-table/chat-markdown-table", "chat-markdown-code": "../chat-markdown-code/chat-markdown-code", "chat-markdown-node": "./chat-markdown-node" } -} +} \ No newline at end of file diff --git a/packages/pro-components/chat/chat-markdown/chat-markdown-node/chat-markdown-node.wxml b/packages/pro-components/chat/chat-markdown/chat-markdown-node/chat-markdown-node.wxml index f3ce92f2f..023aefda0 100644 --- a/packages/pro-components/chat/chat-markdown/chat-markdown-node/chat-markdown-node.wxml +++ b/packages/pro-components/chat/chat-markdown/chat-markdown-node/chat-markdown-node.wxml @@ -2,7 +2,7 @@ - + @@ -15,7 +15,7 @@ - + {{''+li.text+''}} @@ -24,7 +24,7 @@ - + @@ -37,7 +37,7 @@ + > @@ -53,15 +53,18 @@ bindtap="nodeClick" > - + + + + {{''+item.raw+''}} + - {{''+item.raw+''}} - + {{''+item.text+''}} @@ -69,7 +72,7 @@ - + {{''+item.text+''}} @@ -77,7 +80,7 @@ - + {{''+item.text+''}} @@ -85,7 +88,7 @@ - + diff --git a/packages/pro-components/chat/chat-markdown/chat-markdown-tail/chat-markdown-tail.json b/packages/pro-components/chat/chat-markdown/chat-markdown-tail/chat-markdown-tail.json new file mode 100644 index 000000000..66165257f --- /dev/null +++ b/packages/pro-components/chat/chat-markdown/chat-markdown-tail/chat-markdown-tail.json @@ -0,0 +1,4 @@ +{ + "component": true, + "styleIsolation": "apply-shared" +} \ No newline at end of file diff --git a/packages/pro-components/chat/chat-markdown/chat-markdown-tail/chat-markdown-tail.ts b/packages/pro-components/chat/chat-markdown/chat-markdown-tail/chat-markdown-tail.ts new file mode 100644 index 000000000..ea48b23a8 --- /dev/null +++ b/packages/pro-components/chat/chat-markdown/chat-markdown-tail/chat-markdown-tail.ts @@ -0,0 +1,22 @@ +import { SuperComponent, wxComponent, ComponentsOptionsType } from '../../../../components/common/src/index'; +import config from '../../../../components/common/config'; + +const { prefix } = config; +const name = `${prefix}-chat-markdown`; + +@wxComponent() +export default class ChatMarkdownTail extends SuperComponent { + options: ComponentsOptionsType = {}; + + properties = { + /** 光标字符内容,由 chat-markdown 内部注入,默认 ▋ */ + content: { + type: String, + value: '▋', + }, + }; + + data = { + classPrefix: name, + }; +} diff --git a/packages/pro-components/chat/chat-markdown/chat-markdown-tail/chat-markdown-tail.wxml b/packages/pro-components/chat/chat-markdown/chat-markdown-tail/chat-markdown-tail.wxml new file mode 100644 index 000000000..f06047840 --- /dev/null +++ b/packages/pro-components/chat/chat-markdown/chat-markdown-tail/chat-markdown-tail.wxml @@ -0,0 +1 @@ +{{content}} diff --git a/packages/pro-components/chat/chat-markdown/chat-markdown.json b/packages/pro-components/chat/chat-markdown/chat-markdown.json index 640df53ef..5c6a3fa3f 100644 --- a/packages/pro-components/chat/chat-markdown/chat-markdown.json +++ b/packages/pro-components/chat/chat-markdown/chat-markdown.json @@ -1,7 +1,12 @@ { "component": true, "styleIsolation": "shared", + "componentGenerics": { + "tail-component": { + "default": "./chat-markdown-tail/chat-markdown-tail" + } + }, "usingComponents": { "chat-markdown-node": "./chat-markdown-node/chat-markdown-node" } -} +} \ No newline at end of file diff --git a/packages/pro-components/chat/chat-markdown/chat-markdown.less b/packages/pro-components/chat/chat-markdown/chat-markdown.less index 8c8e8ec3f..7d2769427 100644 --- a/packages/pro-components/chat/chat-markdown/chat-markdown.less +++ b/packages/pro-components/chat/chat-markdown/chat-markdown.less @@ -189,4 +189,20 @@ border: 1rpx solid @component-border; } } + + // 流式输出尾部光标 + &-tail { + display: inline-block; + animation: chat-markdown-tail-blink 1s step-start infinite; + } +} + +@keyframes chat-markdown-tail-blink { + 0%, + 100% { + opacity: 1; + } + 50% { + opacity: 0; + } } diff --git a/packages/pro-components/chat/chat-markdown/chat-markdown.ts b/packages/pro-components/chat/chat-markdown/chat-markdown.ts index 3fce2f5c3..8b4bf3b62 100644 --- a/packages/pro-components/chat/chat-markdown/chat-markdown.ts +++ b/packages/pro-components/chat/chat-markdown/chat-markdown.ts @@ -7,6 +7,55 @@ import { TdChatMarkdownProps } from './type'; const { prefix } = config; const name = `${prefix}-chat-markdown`; +const DEFAULT_TAIL_CONTENT = '▋'; + +/** 解析 tail 参数,返回光标字符;不需要显示时返回 null */ +function resolveTailContent(tail?: boolean | { content?: string }): string | null { + if (!tail) return null; + if (typeof tail === 'boolean') return DEFAULT_TAIL_CONTENT; + return tail.content || DEFAULT_TAIL_CONTENT; +} + +/** + * 将列表项的子 tokens 展平,供 injectTailToTokens 递归使用。 + * marked 的 list token 结构:list.items[].tokens(而非 list.tokens) + */ +function flatListItems(items: any[]): any[] { + return items.reduce((result: any[], item: any) => { + if (item.tokens?.length) result.push(...item.tokens); + return result; + }, []); +} + +/** + * 从后往前遍历 token 树,找到最后一个非空 text 叶子节点,打上 isTail 标记。 + * - 有子节点(tokens / items)时优先递归 + * - 末尾是 code / table / image 等非 text 节点时静默跳过,不注入 + * @returns 是否成功注入 + */ +function injectTailToTokens(tokens: any[], tailChar: string): boolean { + for (let i = tokens.length - 1; i >= 0; i -= 1) { + const token = tokens[i]; + // 优先递归子节点 + let children: any[] | null = null; + if (token.tokens?.length) { + children = token.tokens; + } else if (token.items?.length) { + children = flatListItems(token.items); + } + if (children?.length) { + if (injectTailToTokens(children, tailChar)) return true; + } + // 叶子文本节点且内容非空 + if (token.type === 'text' && (token.text || token.raw)?.trim()) { + token.isTail = true; + token.tailContent = tailChar; + return true; + } + } + return false; +} + export interface ChatMarkdownProps extends TdChatMarkdownProps {} @wxComponent() @@ -28,6 +77,10 @@ export default class ChatMarkdown extends SuperComponent { content: function (markdown: string) { this.parseMarkdown(markdown); }, + // streaming 变化时重新解析(如 hasNextChunk 从 true 变 false,光标消失) + streaming: function () { + this.parseMarkdown(this.data.content); + }, }; methods = { @@ -37,6 +90,13 @@ export default class ChatMarkdown extends SuperComponent { const lexer = new Lexer(this.data.options); const tokens = lexer.lex(markdown); + // 尾部光标注入 + const { streaming } = this.data; + const tailChar = resolveTailContent(streaming?.tail); + if (streaming?.hasNextChunk && tailChar) { + injectTailToTokens(tokens, tailChar); + } + this.setData({ nodes: tokens }); } catch (error) { console.error('Markdown parsing error:', error); diff --git a/packages/pro-components/chat/chat-markdown/chat-markdown.wxml b/packages/pro-components/chat/chat-markdown/chat-markdown.wxml index 4f84d8603..3fa72f3e3 100644 --- a/packages/pro-components/chat/chat-markdown/chat-markdown.wxml +++ b/packages/pro-components/chat/chat-markdown/chat-markdown.wxml @@ -1,5 +1,5 @@ - + diff --git a/packages/pro-components/chat/chat-markdown/props.ts b/packages/pro-components/chat/chat-markdown/props.ts index 5bcc8967f..e83ec446e 100644 --- a/packages/pro-components/chat/chat-markdown/props.ts +++ b/packages/pro-components/chat/chat-markdown/props.ts @@ -17,6 +17,11 @@ const props: TdChatMarkdownProps = { type: Object, value: { gfm: true, pedantic: false, breaks: true }, }, + /** 流式输出配置 */ + streaming: { + type: Object, + value: null, + }, }; export default props; diff --git a/packages/pro-components/chat/chat-markdown/type.ts b/packages/pro-components/chat/chat-markdown/type.ts index adf560d5a..5e59a07d0 100644 --- a/packages/pro-components/chat/chat-markdown/type.ts +++ b/packages/pro-components/chat/chat-markdown/type.ts @@ -22,6 +22,13 @@ export interface TdChatMarkdownProps { type: ObjectConstructor; value?: TdChatContentMDOptions; }; + /** + * 流式输出配置,控制尾部光标的显示与隐藏 + */ + streaming?: { + type: ObjectConstructor; + value?: TdChatMarkdownStreamingOption; + }; } export interface TdChatContentMDOptions { @@ -30,3 +37,15 @@ export interface TdChatContentMDOptions { smartLists?: boolean; breaks?: boolean; } + +export interface TdChatMarkdownTailOption { + /** 自定义光标字符,默认 '▋' */ + content?: string; +} + +export interface TdChatMarkdownStreamingOption { + /** 是否还有后续内容块,false 时光标消失 */ + hasNextChunk: boolean; + /** 尾部光标配置,true 使用默认光标 ▋,false/不传则不显示 */ + tail?: boolean | TdChatMarkdownTailOption; +} diff --git a/packages/uniapp-pro-components/chat/chat-content/chat-content.vue b/packages/uniapp-pro-components/chat/chat-content/chat-content.vue index fb4d79e09..89545afb7 100644 --- a/packages/uniapp-pro-components/chat/chat-content/chat-content.vue +++ b/packages/uniapp-pro-components/chat/chat-content/chat-content.vue @@ -16,6 +16,7 @@ diff --git a/packages/uniapp-pro-components/chat/chat-list/_example/docs/index.vue b/packages/uniapp-pro-components/chat/chat-list/_example/docs/index.vue index 8c4204f6c..e1b391ac7 100644 --- a/packages/uniapp-pro-components/chat/chat-list/_example/docs/index.vue +++ b/packages/uniapp-pro-components/chat/chat-list/_example/docs/index.vue @@ -14,12 +14,28 @@ :avatar="item.avatar || ''" :name="item.name || ''" :datetime="item.datetime || ''" - :content="item.message.content" :role="item.message.role" - :chat-content-props="chatContentProps" :placement="item.message.role === 'user' ? 'right' : 'left'" @message-longpress="showPopover" > + @@ -45,6 +48,7 @@ import CodeDemo from './code/index.vue'; import SheetDemo from './sheet/index.vue'; import UrlDemo from './url/index.vue'; import ReferDemo from './refer/index.vue'; +import TailDemo from './tail/index.vue'; export default { @@ -55,6 +59,7 @@ export default { SheetDemo, UrlDemo, ReferDemo, + TailDemo, }, data() { return {}; diff --git a/packages/uniapp-pro-components/chat/chat-markdown/_example/tail/index.vue b/packages/uniapp-pro-components/chat/chat-markdown/_example/tail/index.vue new file mode 100644 index 000000000..c45d9efd0 --- /dev/null +++ b/packages/uniapp-pro-components/chat/chat-markdown/_example/tail/index.vue @@ -0,0 +1,68 @@ + + + + diff --git a/packages/uniapp-pro-components/chat/chat-markdown/chat-markdown.less b/packages/uniapp-pro-components/chat/chat-markdown/chat-markdown.less index 09defedb2..e02148a87 100644 --- a/packages/uniapp-pro-components/chat/chat-markdown/chat-markdown.less +++ b/packages/uniapp-pro-components/chat/chat-markdown/chat-markdown.less @@ -150,6 +150,12 @@ border: 1rpx solid @component-border; } + // 流式输出尾部光标 + &-tail { + display: inline-block; + animation: chat-markdown-tail-blink 1s step-start infinite; + } + // 表格样式 .@{chat-markdown-table}__container { display: table; @@ -190,3 +196,14 @@ } } } + +@keyframes chat-markdown-tail-blink { + 0%, + 100% { + opacity: 1; + } + + 50% { + opacity: 0; + } +} diff --git a/packages/uniapp-pro-components/chat/chat-markdown/chat-markdown.vue b/packages/uniapp-pro-components/chat/chat-markdown/chat-markdown.vue index 7831cff1f..487cc8444 100644 --- a/packages/uniapp-pro-components/chat/chat-markdown/chat-markdown.vue +++ b/packages/uniapp-pro-components/chat/chat-markdown/chat-markdown.vue @@ -22,9 +22,44 @@ import props from './props'; import tools from '@tdesign/uniapp/common/utils.wxs'; import { uniComponent } from '@tdesign/uniapp/common/src/index'; - const name = `${prefix}-chat-markdown`; +const DEFAULT_TAIL_CONTENT = '▋'; + +function resolveTailContent(tail) { + if (!tail) return null; + if (typeof tail === 'boolean') return DEFAULT_TAIL_CONTENT; + return tail.content || DEFAULT_TAIL_CONTENT; +} + +function flatListItems(items) { + return items.reduce((result, item) => { + if (item.tokens?.length) result.push(...item.tokens); + return result; + }, []); +} + +function injectTailToTokens(tokens, tailChar) { + for (let i = tokens.length - 1; i >= 0; i -= 1) { + const token = tokens[i]; + let children = null; + if (token.tokens?.length) { + children = token.tokens; + } else if (token.items?.length) { + children = flatListItems(token.items); + } + if (children?.length) { + if (injectTailToTokens(children, tailChar)) return true; + } + if (token.type === 'text' && (token.text || token.raw)?.trim()) { + token.isTail = true; + token.tailContent = tailChar; + return true; + } + } + return false; +} + export default { components: { chatMarkdownNode, @@ -56,6 +91,12 @@ export default { immediate: true, deep: true, }, + streaming: { + handler() { + this.parseMarkdown(this.content); + }, + deep: true, + }, }, methods: { @@ -72,6 +113,12 @@ export default { const tokens = lexer.lex(markdown); + // 尾部光标注入 + const tailChar = resolveTailContent(this.streaming?.tail); + if (this.streaming?.hasNextChunk && tailChar) { + injectTailToTokens(tokens, tailChar); + } + this.nodes = tokens; } catch (error) { // 解析失败时,将原始文本作为普通文本显示 diff --git a/packages/uniapp-pro-components/chat/chat-markdown/props.ts b/packages/uniapp-pro-components/chat/chat-markdown/props.ts index 4eaccdcdd..3723a0d1e 100644 --- a/packages/uniapp-pro-components/chat/chat-markdown/props.ts +++ b/packages/uniapp-pro-components/chat/chat-markdown/props.ts @@ -16,6 +16,11 @@ export default { type: Object, default: () => ({ gfm: true, pedantic: false, breaks: true }), }, + /** 流式输出配置 */ + streaming: { + type: Object, + default: () => null, + }, /** 点击链接时触发 */ onClick: { type: Function, diff --git a/packages/uniapp-pro-components/chat/chat-markdown/type.ts b/packages/uniapp-pro-components/chat/chat-markdown/type.ts index da3890d2b..d099eab6e 100644 --- a/packages/uniapp-pro-components/chat/chat-markdown/type.ts +++ b/packages/uniapp-pro-components/chat/chat-markdown/type.ts @@ -15,12 +15,23 @@ export interface TdChatMarkdownProps { * @default { gfm: true, pedantic: false, breaks: true } */ options?: TdChatContentMDOptions; + /** + * 流式输出配置 + */ + streaming?: TdChatStreamingConfig; /** * 点击链接时触发 */ onClick?: (context: TdMarkdownClickContext) => void; } +export interface TdChatStreamingConfig { + /** 是否还有下一个数据块,控制光标显隐 */ + hasNextChunk?: boolean; + /** 尾部光标配置,true 使用默认光标 ▋,传对象可自定义光标字符 */ + tail?: boolean | { content?: string }; +} + export interface TdChatContentMDOptions { gfm?: boolean; pedantic?: boolean;