feat(order): 实现订单总结内容的实时流式渲染

- 替换订单总结接口响应类型为object,支持事件流格式
- 采用EventSource实现订单总结的SSE连接,支持服务器推送数据
- 实现打字机效果,实时显示总结文本的追加内容
- 使用marked解析Markdown格式摘要内容,支持GFM和换行
- 使用DOMPurify清理生成HTML,保障内容安全,仅允许部分标签
- 增加实时生成中状态提示和闪烁的输入光标动画
- 添加超时机制和连接错误处理,保证用户提示友好
- 组件卸载时自动关闭SSE连接,防止资源泄漏
- 更新依赖,新增dompurify和marked及相关类型定义
- 调整ESLint和构建配置支持新增脚本模式
This commit is contained in:
danial
2025-11-29 12:48:37 +08:00
parent c9c6ac82aa
commit e9d1fbd218
8 changed files with 281 additions and 54 deletions

View File

@@ -11,7 +11,8 @@
"Bash(pnpm eslint:*)",
"Bash(mkdir:*)",
"Bash(pnpm build)",
"Bash(pnpm eslint:fix)"
"Bash(pnpm eslint:fix)",
"Bash(pnpm eslint:fix:*)"
],
"deny": [],
"ask": []

View File

@@ -61,8 +61,10 @@
"codemirror": "^6.0.2",
"crypto-js": "^4.2.0",
"dayjs": "^1.11.19",
"dompurify": "^3.3.0",
"echarts": "^5.6.0",
"lodash": "^4.17.21",
"marked": "^17.0.1",
"mitt": "^3.0.1",
"nprogress": "^0.2.0",
"pinia": "^2.3.1",
@@ -83,7 +85,9 @@
"@openapi-codegen/cli": "^3.1.0",
"@openapi-codegen/typescript": "^11.0.1",
"@types/crypto-js": "^4.2.2",
"@types/dompurify": "^3.2.0",
"@types/lodash": "^4.17.20",
"@types/marked": "^6.0.0",
"@types/mockjs": "^1.0.10",
"@types/node": "^22.14.1",
"@types/nprogress": "^0.2.3",

77
pnpm-lock.yaml generated
View File

@@ -66,12 +66,18 @@ importers:
dayjs:
specifier: ^1.11.19
version: 1.11.19
dompurify:
specifier: ^3.3.0
version: 3.3.0
echarts:
specifier: ^5.6.0
version: 5.6.0
lodash:
specifier: ^4.17.21
version: 4.17.21
marked:
specifier: ^17.0.1
version: 17.0.1
mitt:
specifier: ^3.0.1
version: 3.0.1
@@ -127,9 +133,15 @@ importers:
'@types/crypto-js':
specifier: ^4.2.2
version: 4.2.2
'@types/dompurify':
specifier: ^3.2.0
version: 3.2.0
'@types/lodash':
specifier: ^4.17.20
version: 4.17.20
'@types/marked':
specifier: ^6.0.0
version: 6.0.0
'@types/mockjs':
specifier: ^1.0.10
version: 1.0.10
@@ -156,7 +168,7 @@ importers:
version: 4.1.2(vite@5.4.19(@types/node@22.15.18)(less@4.3.0))(vue@3.5.22(typescript@5.8.3))
autoprefixer:
specifier: ^10.4.21
version: 10.4.21(postcss@8.5.3)
version: 10.4.21(postcss@8.5.6)
consola:
specifier: ^3.4.2
version: 3.4.2
@@ -192,7 +204,7 @@ importers:
version: 1.8.0
postcss-less:
specifier: ^6.0.0
version: 6.0.0(postcss@8.5.3)
version: 6.0.0(postcss@8.5.6)
prettier:
specifier: ^3.5.3
version: 3.5.3
@@ -213,13 +225,13 @@ importers:
version: 37.0.0(stylelint@16.19.1(typescript@5.8.3))
stylelint-config-standard-less:
specifier: ^3.0.1
version: 3.0.1(postcss@8.5.3)(stylelint@16.19.1(typescript@5.8.3))
version: 3.0.1(postcss@8.5.6)(stylelint@16.19.1(typescript@5.8.3))
stylelint-config-standard-vue:
specifier: ^1.0.0
version: 1.0.0(postcss-html@1.8.0)(stylelint@16.19.1(typescript@5.8.3))
stylelint-less:
specifier: ^3.0.1
version: 3.0.1(postcss@8.5.3)(stylelint@16.19.1(typescript@5.8.3))
version: 3.0.1(postcss@8.5.6)(stylelint@16.19.1(typescript@5.8.3))
stylelint-order:
specifier: ^6.0.4
version: 6.0.4(stylelint@16.19.1(typescript@5.8.3))
@@ -1491,6 +1503,10 @@ packages:
'@types/debug@4.1.12':
resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==}
'@types/dompurify@3.2.0':
resolution: {integrity: sha512-Fgg31wv9QbLDA0SpTOXO3MaxySc4DKGLi8sna4/Utjo4r3ZRPdCt4UQee8BWr+Q5z21yifghREPJGYaEOEIACg==}
deprecated: This is a stub types definition. dompurify provides its own type definitions, so you do not need this installed.
'@types/estree@1.0.7':
resolution: {integrity: sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==}
@@ -1542,6 +1558,10 @@ packages:
'@types/lodash@4.17.20':
resolution: {integrity: sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA==}
'@types/marked@6.0.0':
resolution: {integrity: sha512-jmjpa4BwUsmhxcfsgUit/7A9KbrC48Q0q8KvnY107ogcjGgTFDlIL3RpihNpx2Mu1hM4mdFQjoVc4O6JoGKHsA==}
deprecated: This is a stub types definition. marked provides its own type definitions, so you do not need this installed.
'@types/mdast@4.0.4':
resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==}
@@ -2428,8 +2448,8 @@ packages:
resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==}
engines: {node: '>= 4'}
dompurify@3.2.5:
resolution: {integrity: sha512-mLPd29uoRe9HpvwP2TxClGQBzGXeEC/we/q+bFlmPPmj2p2Ugl3r6ATu/UU1v77DXNcehiBg9zsr1dREyA/dJQ==}
dompurify@3.3.0:
resolution: {integrity: sha512-r+f6MYR1gGN1eJv0TVQbhA7if/U7P87cdPl3HN5rikqaBSBxLiCb/b9O+2eG0cxz0ghyU+mU1QkbsOwERMYlWQ==}
domutils@2.8.0:
resolution: {integrity: sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==}
@@ -3772,6 +3792,11 @@ packages:
markdown-table@3.0.4:
resolution: {integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==}
marked@17.0.1:
resolution: {integrity: sha512-boeBdiS0ghpWcSwoNm/jJBwdpFaMnZWRzjA6SkUMYb40SVaN1x7mmfGKp0jvexGcx+7y2La5zRZsYFZI6Qpypg==}
engines: {node: '>= 20'}
hasBin: true
math-intrinsics@1.1.0:
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
engines: {node: '>= 0.4'}
@@ -6684,7 +6709,7 @@ snapshots:
'@types/lodash.debounce': 4.0.9
'@types/lodash.throttle': 4.1.9
clsx: 2.1.1
dompurify: 3.2.5
dompurify: 3.3.0
lodash.debounce: 4.0.8
lodash.throttle: 4.1.1
nanoid: 5.1.5
@@ -7314,6 +7339,10 @@ snapshots:
dependencies:
'@types/ms': 2.1.0
'@types/dompurify@3.2.0':
dependencies:
dompurify: 3.3.0
'@types/estree@1.0.7': {}
'@types/glob@7.2.0':
@@ -7374,6 +7403,10 @@ snapshots:
'@types/lodash@4.17.20': {}
'@types/marked@6.0.0':
dependencies:
marked: 17.0.1
'@types/mdast@4.0.4':
dependencies:
'@types/unist': 3.0.3
@@ -7758,14 +7791,14 @@ snapshots:
asynckit@0.4.0: {}
autoprefixer@10.4.21(postcss@8.5.3):
autoprefixer@10.4.21(postcss@8.5.6):
dependencies:
browserslist: 4.24.5
caniuse-lite: 1.0.30001718
fraction.js: 4.3.7
normalize-range: 0.1.2
picocolors: 1.1.1
postcss: 8.5.3
postcss: 8.5.6
postcss-value-parser: 4.2.0
axios@1.12.2:
@@ -8404,7 +8437,7 @@ snapshots:
dependencies:
domelementtype: 2.3.0
dompurify@3.2.5:
dompurify@3.3.0:
optionalDependencies:
'@types/trusted-types': 2.0.7
@@ -9872,6 +9905,8 @@ snapshots:
markdown-table@3.0.4: {}
marked@17.0.1: {}
math-intrinsics@1.1.0: {}
mathml-tag-names@2.1.3: {}
@@ -10678,9 +10713,9 @@ snapshots:
postcss: 8.5.3
postcss-safe-parser: 6.0.0(postcss@8.5.3)
postcss-less@6.0.0(postcss@8.5.3):
postcss-less@6.0.0(postcss@8.5.6):
dependencies:
postcss: 8.5.3
postcss: 8.5.6
postcss-resolve-nested-selector@0.1.6: {}
@@ -11389,14 +11424,14 @@ snapshots:
stylelint: 16.19.1(typescript@5.8.3)
stylelint-order: 6.0.4(stylelint@16.19.1(typescript@5.8.3))
stylelint-config-recommended-less@3.0.1(postcss@8.5.3)(stylelint@16.19.1(typescript@5.8.3)):
stylelint-config-recommended-less@3.0.1(postcss@8.5.6)(stylelint@16.19.1(typescript@5.8.3)):
dependencies:
postcss-less: 6.0.0(postcss@8.5.3)
postcss-less: 6.0.0(postcss@8.5.6)
stylelint: 16.19.1(typescript@5.8.3)
stylelint-config-recommended: 14.0.1(stylelint@16.19.1(typescript@5.8.3))
stylelint-less: 3.0.1(postcss@8.5.3)(stylelint@16.19.1(typescript@5.8.3))
stylelint-less: 3.0.1(postcss@8.5.6)(stylelint@16.19.1(typescript@5.8.3))
optionalDependencies:
postcss: 8.5.3
postcss: 8.5.6
stylelint-config-recommended-vue@1.6.0(postcss-html@1.8.0)(stylelint@16.19.1(typescript@5.8.3)):
dependencies:
@@ -11418,13 +11453,13 @@ snapshots:
dependencies:
stylelint: 16.19.1(typescript@5.8.3)
stylelint-config-standard-less@3.0.1(postcss@8.5.3)(stylelint@16.19.1(typescript@5.8.3)):
stylelint-config-standard-less@3.0.1(postcss@8.5.6)(stylelint@16.19.1(typescript@5.8.3)):
dependencies:
stylelint: 16.19.1(typescript@5.8.3)
stylelint-config-recommended-less: 3.0.1(postcss@8.5.3)(stylelint@16.19.1(typescript@5.8.3))
stylelint-config-recommended-less: 3.0.1(postcss@8.5.6)(stylelint@16.19.1(typescript@5.8.3))
stylelint-config-standard: 35.0.0(stylelint@16.19.1(typescript@5.8.3))
optionalDependencies:
postcss: 8.5.3
postcss: 8.5.6
stylelint-config-standard-vue@1.0.0(postcss-html@1.8.0)(stylelint@16.19.1(typescript@5.8.3)):
dependencies:
@@ -11444,9 +11479,9 @@ snapshots:
stylelint: 16.19.1(typescript@5.8.3)
stylelint-config-recommended: 15.0.0(stylelint@16.19.1(typescript@5.8.3))
stylelint-less@3.0.1(postcss@8.5.3)(stylelint@16.19.1(typescript@5.8.3)):
stylelint-less@3.0.1(postcss@8.5.6)(stylelint@16.19.1(typescript@5.8.3)):
dependencies:
postcss: 8.5.3
postcss: 8.5.6
postcss-resolve-nested-selector: 0.1.6
postcss-value-parser: 4.2.0
stylelint: 16.19.1(typescript@5.8.3)

View File

@@ -338,7 +338,6 @@ docs/KamiApiMerchantV1OrderQueryRecord.md
docs/KamiApiMerchantV1OrderQueryReq.md
docs/KamiApiMerchantV1OrderQueryRes.md
docs/KamiApiMerchantV1OrderQuerySummaryReq.md
docs/KamiApiMerchantV1OrderQuerySummaryRes.md
docs/KamiApiMerchantV1PlatformRateRecord.md
docs/KamiApiMerchantV1SampleAllListRes.md
docs/KamiApiMerchantV1StealCreateReq.md
@@ -770,7 +769,6 @@ models/kami-api-merchant-v1-order-query-record.ts
models/kami-api-merchant-v1-order-query-req.ts
models/kami-api-merchant-v1-order-query-res.ts
models/kami-api-merchant-v1-order-query-summary-req.ts
models/kami-api-merchant-v1-order-query-summary-res.ts
models/kami-api-merchant-v1-platform-rate-record.ts
models/kami-api-merchant-v1-sample-all-list-res.ts
models/kami-api-merchant-v1-steal-create-req.ts

View File

@@ -307,8 +307,6 @@ import type { KamiApiMerchantV1MerchantSampleAllListRes } from '../models';
// @ts-ignore
import type { KamiApiMerchantV1OrderQueryRes } from '../models';
// @ts-ignore
import type { KamiApiMerchantV1OrderQuerySummaryRes } from '../models';
// @ts-ignore
import type { KamiApiMerchantV1StealCreateReq } from '../models';
// @ts-ignore
import type { KamiApiMerchantV1StealListRes } from '../models';
@@ -15860,10 +15858,7 @@ export const DefaultApiFp = function (configuration?: Configuration) {
merchantOrderNo?: string,
options?: RawAxiosRequestConfig
): Promise<
(
axios?: AxiosInstance,
basePath?: string
) => AxiosPromise<KamiApiMerchantV1OrderQuerySummaryRes>
(axios?: AxiosInstance, basePath?: string) => AxiosPromise<object>
> {
const localVarAxiosArgs =
await localVarAxiosParamCreator.apiMerchantOrderSummaryGet(
@@ -20459,7 +20454,7 @@ export const DefaultApiFactory = function (
apiMerchantOrderSummaryGet(
requestParameters: DefaultApiApiMerchantOrderSummaryGetRequest = {},
options?: RawAxiosRequestConfig
): AxiosPromise<KamiApiMerchantV1OrderQuerySummaryRes> {
): AxiosPromise<object> {
return localVarFp
.apiMerchantOrderSummaryGet(requestParameters.merchantOrderNo, options)
.then(request => request(axios, basePath));
@@ -23159,7 +23154,7 @@ export interface DefaultApiInterface {
apiMerchantOrderSummaryGet(
requestParameters?: DefaultApiApiMerchantOrderSummaryGetRequest,
options?: RawAxiosRequestConfig
): AxiosPromise<KamiApiMerchantV1OrderQuerySummaryRes>;
): AxiosPromise<object>;
/**
*

View File

@@ -7150,7 +7150,7 @@ No authorization required
# **apiMerchantOrderSummaryGet**
> KamiApiMerchantV1OrderQuerySummaryRes apiMerchantOrderSummaryGet()
> object apiMerchantOrderSummaryGet()
### Example
@@ -7174,7 +7174,7 @@ const { status, data } =
### Return type
**KamiApiMerchantV1OrderQuerySummaryRes**
**object**
### Authorization
@@ -7183,7 +7183,7 @@ No authorization required
### HTTP request headers
- **Content-Type**: Not defined
- **Accept**: application/json
- **Accept**: text/event-stream
### HTTP response details

View File

@@ -317,7 +317,6 @@ export * from './kami-api-merchant-v1-order-query-record';
export * from './kami-api-merchant-v1-order-query-req';
export * from './kami-api-merchant-v1-order-query-res';
export * from './kami-api-merchant-v1-order-query-summary-req';
export * from './kami-api-merchant-v1-order-query-summary-res';
export * from './kami-api-merchant-v1-platform-rate-record';
export * from './kami-api-merchant-v1-sample-all-list-res';
export * from './kami-api-merchant-v1-steal-create-req';

View File

@@ -1,3 +1,4 @@
<!-- eslint-disable vue/no-v-html -->
<template>
<div class="container">
<a-typography-title style="text-align: center">
@@ -91,12 +92,23 @@
:bordered="false"
style="background-color: var(--color-fill-2)"
>
<a-typography-paragraph
style="margin: 0; line-height: 1.6"
:deep="false"
<!-- 实时生成状态提示 -->
<div
v-if="isTyping"
style="text-align: right; margin-bottom: 8px"
>
{{ summaryData.message }}
</a-typography-paragraph>
<a-tag color="green" size="small">
<icon-sync />
实时生成中
</a-tag>
</div>
<div
class="summary-content"
style="margin: 0; line-height: 1.6"
v-html="formattedSummaryHtml"
></div>
<span v-if="isTyping" class="typing-cursor">|</span>
</a-card>
</a-space>
</div>
@@ -138,16 +150,20 @@
<script lang="ts" setup>
import { apiClient } from '@/api';
import {
KamiApiMerchantV1OrderQueryRes,
KamiApiMerchantV1OrderQuerySummaryRes
} from '@/api/generated';
import { KamiApiMerchantV1OrderQueryRes } from '@/api/generated';
// 定义总结数据的类型
interface OrderSummaryRes {
message?: string;
}
import useLoading from '@/hooks/loading';
import { Message } from '@arco-design/web-vue';
import { reactive, ref, computed } from 'vue';
import DOMPurify from 'dompurify';
import { marked } from 'marked';
import { onUnmounted, reactive, ref, computed } from 'vue';
const renderData = ref<KamiApiMerchantV1OrderQueryRes>({});
const summaryData = ref<KamiApiMerchantV1OrderQuerySummaryRes | null>(null);
const summaryData = ref<OrderSummaryRes | null>(null);
const { loading, setLoading } = useLoading(false);
const { loading: summaryLoading, setLoading: setSummaryLoading } =
useLoading(false);
@@ -161,24 +177,164 @@ const isMobile = computed(() => {
return window.innerWidth <= 768;
});
// 存储 SSE 连接实例,用于清理
const currentEventSource = ref<EventSource | null>(null);
// 打字机状态管理
const isTyping = ref(false); // 是否正在流式接收数据
const hasStartedTyping = ref(false); // 是否已经开始打字机效果
// 配置 marked 选项
marked.setOptions({
breaks: true, // 支持换行
gfm: true // 启用 GitHub Flavored Markdown
});
// 同步版本的格式化函数(用于流式显示)
const formatSummaryTextSync = (text: string): string => {
if (!text) return '';
try {
// 使用同步版本的 marked
const html = marked.parse(text) as string;
// 使用 DOMPurify 清理 HTML只允许安全的标签
return DOMPurify.sanitize(html, {
ALLOWED_TAGS: ['p', 'br', 'strong', 'em', 'b', 'i', 'ul', 'ol', 'li'],
ALLOWED_ATTR: []
});
} catch (error) {
console.error('Markdown 解析错误:', error);
// 降级处理:返回纯文本,只处理基本格式并清理
const basicHtml = text
.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>') // 粗体
.replace(/\*(.*?)\*/g, '<em>$1</em>') // 斜体
.replace(/\n/g, '<br>'); // 换行
return DOMPurify.sanitize(basicHtml, {
ALLOWED_TAGS: ['p', 'br', 'strong', 'em', 'b', 'i'],
ALLOWED_ATTR: []
});
}
};
// 计算属性:格式化后的总结内容
// 使用 DOMPurify 清理后的 HTML确保安全
const formattedSummaryHtml = computed(() => {
return summaryData.value?.message
? formatSummaryTextSync(summaryData.value.message)
: '';
});
const fetchSummary = async (merchantOrderNo: string) => {
if (!merchantOrderNo) return;
// 清理之前的连接
if (currentEventSource.value) {
currentEventSource.value.close();
currentEventSource.value = null;
}
try {
setSummaryLoading(true);
summaryData.value = null;
const summaryResponse = await apiClient.apiMerchantOrderSummaryGet({
merchantOrderNo
});
summaryData.value = summaryResponse.data;
isTyping.value = false;
hasStartedTyping.value = false;
// 创建 SSE 连接
const baseUrl =
import.meta.env.VITE_API_BASE_URL || 'http://127.0.0.1:12401';
const sseUrl = `${baseUrl}/api/merchant/order/summary?merchantOrderNo=${encodeURIComponent(merchantOrderNo)}`;
const eventSource = new EventSource(sseUrl);
currentEventSource.value = eventSource;
eventSource.onopen = () => {
console.log('SSE 连接已建立');
};
eventSource.onmessage = event => {
try {
console.log('收到 SSE 数据:', event.data);
// EventSource 会自动处理 "data: " 前缀event.data 已经是解析后的内容
// 直接解析 JSON
const data = JSON.parse(event.data);
if (data.content) {
// 第一次接收到数据,开始打字机效果
if (!hasStartedTyping.value) {
hasStartedTyping.value = true;
isTyping.value = true;
setSummaryLoading(false); // 结束 loading 状态
summaryData.value = { message: data.content };
} else {
// 累积流式数据
summaryData.value.message += data.content;
}
}
// 检查是否完成 - 根据新的格式可能需要调整
if (data.complete || data.finished || data.done) {
eventSource.close();
currentEventSource.value = null;
isTyping.value = false; // 停止打字机效果
}
} catch (parseError) {
console.error(
'解析 SSE 数据失败:',
parseError,
'原始数据:',
event.data
);
}
};
eventSource.onerror = error => {
console.error('SSE 连接错误:', error);
eventSource.close();
currentEventSource.value = null;
setSummaryLoading(false);
isTyping.value = false;
hasStartedTyping.value = false;
// 如果没有收到任何数据,显示错误提示
if (!summaryData.value?.message) {
Message.warning('订单总结获取失败,请稍后重试');
}
};
// 设置超时
setTimeout(() => {
if (eventSource.readyState !== EventSource.CLOSED) {
eventSource.close();
currentEventSource.value = null;
setSummaryLoading(false);
isTyping.value = false;
hasStartedTyping.value = false;
console.log('SSE 连接超时');
if (!summaryData.value?.message) {
Message.warning('订单总结获取超时,请稍后重试');
}
}
}, 30000); // 30秒超时
} catch (error) {
console.error('获取订单总结失败:', error);
Message.warning('订单总结获取失败,请稍后重试');
} finally {
console.error('建立 SSE 连接失败:', error);
setSummaryLoading(false);
currentEventSource.value = null;
isTyping.value = false;
hasStartedTyping.value = false;
Message.warning('订单总结获取失败,请稍后重试');
}
};
// 组件卸载时清理 SSE 连接
onUnmounted(() => {
if (currentEventSource.value) {
currentEventSource.value.close();
currentEventSource.value = null;
}
});
const search = async (value: string) => {
try {
state.value = value;
@@ -208,6 +364,18 @@ const search = async (value: string) => {
<style lang="less" scoped>
.container {
@keyframes blink {
0%,
50% {
opacity: 1;
}
51%,
100% {
opacity: 0;
}
}
max-width: 800px;
padding: 200px 20px;
margin: 0 auto;
@@ -357,6 +525,33 @@ const search = async (value: string) => {
}
}
// 流式输入光标动画
.typing-cursor {
display: inline-block;
width: 2px;
height: 1.2em;
background-color: var(--color-primary);
margin-left: 2px;
animation: blink 1s infinite;
vertical-align: text-bottom;
}
// 格式化总结内容样式
.summary-content {
p {
margin: 0 0 8px;
&:last-child {
margin-bottom: 0;
}
}
strong {
font-weight: 600;
color: var(--color-text-1);
}
}
:deep(.arco-input-wrapper) {
background-color: var(--color-white);