feat(order): 实现订单总结内容的实时流式渲染
- 替换订单总结接口响应类型为object,支持事件流格式 - 采用EventSource实现订单总结的SSE连接,支持服务器推送数据 - 实现打字机效果,实时显示总结文本的追加内容 - 使用marked解析Markdown格式摘要内容,支持GFM和换行 - 使用DOMPurify清理生成HTML,保障内容安全,仅允许部分标签 - 增加实时生成中状态提示和闪烁的输入光标动画 - 添加超时机制和连接错误处理,保证用户提示友好 - 组件卸载时自动关闭SSE连接,防止资源泄漏 - 更新依赖,新增dompurify和marked及相关类型定义 - 调整ESLint和构建配置支持新增脚本模式
This commit is contained in:
@@ -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": []
|
||||
|
||||
@@ -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
77
pnpm-lock.yaml
generated
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>;
|
||||
|
||||
/**
|
||||
*
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user