Skip to content
Toggle navigation
P
Projects
G
Groups
S
Snippets
Help
shun peng
/
lufa-ai
This project
Loading...
Sign in
Toggle navigation
Go to a project
Project
Repository
Issues
0
Merge Requests
0
Pipelines
Wiki
Snippets
Members
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Commit
710c5e1c
authored
Jul 29, 2026
by
Shucai Chen
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
修复
parent
25c49386
Hide whitespace changes
Inline
Side-by-side
Showing
2 changed files
with
1027 additions
and
142 deletions
+1027
-142
ruoyi-vue-pro-master-jdk17/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/chat/AiChatAnalysisServiceImpl.java
+271
-11
yudao-ui-admin-vue3/src/views/ai/chat_analysis/summary/index.vue
+756
-131
No files found.
ruoyi-vue-pro-master-jdk17/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/chat/AiChatAnalysisServiceImpl.java
View file @
710c5e1c
...
@@ -7,24 +7,25 @@ import cn.iocoder.yudao.module.ai.dal.dataobject.chat.AiChatSummaryDO;
...
@@ -7,24 +7,25 @@ import cn.iocoder.yudao.module.ai.dal.dataobject.chat.AiChatSummaryDO;
import
cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeDocumentDO
;
import
cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeDocumentDO
;
import
cn.iocoder.yudao.module.ai.dal.mysql.chat.AiChatAnalysisMapper
;
import
cn.iocoder.yudao.module.ai.dal.mysql.chat.AiChatAnalysisMapper
;
import
cn.iocoder.yudao.module.ai.dal.mysql.chat.AiChatSummaryMapper
;
import
cn.iocoder.yudao.module.ai.dal.mysql.chat.AiChatSummaryMapper
;
import
cn.iocoder.yudao.module.system.api.user.AdminUserApi
;
import
cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO
;
import
com.baomidou.mybatisplus.core.metadata.IPage
;
import
com.baomidou.mybatisplus.core.metadata.IPage
;
import
com.baomidou.mybatisplus.extension.plugins.pagination.Page
;
import
com.baomidou.mybatisplus.extension.plugins.pagination.Page
;
import
jakarta.annotation.Resource
;
import
jakarta.annotation.Resource
;
import
lombok.extern.slf4j.Slf4j
;
import
lombok.extern.slf4j.Slf4j
;
import
org.springframework.http.*
;
import
org.springframework.stereotype.Service
;
import
org.springframework.stereotype.Service
;
import
org.springframework.validation.annotation.Validated
;
import
org.springframework.validation.annotation.Validated
;
import
org.springframework.web.client.RestTemplate
;
import
java.time.DayOfWeek
;
import
java.time.DayOfWeek
;
import
java.time.LocalDateTime
;
import
java.time.LocalDateTime
;
import
java.time.temporal.ChronoUnit
;
import
java.time.temporal.ChronoUnit
;
import
java.util.ArrayList
;
import
java.util.*
;
import
java.util.HashMap
;
import
java.util.concurrent.ConcurrentHashMap
;
import
java.util.List
;
import
java.util.Map
;
import
java.util.stream.Collectors
;
import
static
cn
.
iocoder
.
yudao
.
framework
.
common
.
exception
.
util
.
ServiceExceptionUtil
.
exception
;
import
static
cn
.
iocoder
.
yudao
.
framework
.
common
.
exception
.
util
.
ServiceExceptionUtil
.
exception
;
import
static
cn
.
iocoder
.
yudao
.
framework
.
common
.
util
.
collection
.
CollectionUtils
.
convertList
;
import
static
cn
.
iocoder
.
yudao
.
framework
.
common
.
util
.
collection
.
CollectionUtils
.
convertList
;
import
static
cn
.
iocoder
.
yudao
.
framework
.
security
.
core
.
util
.
SecurityFrameworkUtils
.
getLoginUserId
;
import
static
cn
.
iocoder
.
yudao
.
module
.
ai
.
enums
.
ErrorCodeConstants
.*;
import
static
cn
.
iocoder
.
yudao
.
module
.
ai
.
enums
.
ErrorCodeConstants
.*;
/**
/**
...
@@ -43,6 +44,43 @@ public class AiChatAnalysisServiceImpl implements AiChatAnalysisService {
...
@@ -43,6 +44,43 @@ public class AiChatAnalysisServiceImpl implements AiChatAnalysisService {
@Resource
@Resource
private
AiChatSummaryMapper
chatSummaryMapper
;
private
AiChatSummaryMapper
chatSummaryMapper
;
@Resource
private
RestTemplate
restTemplate
;
@Resource
private
AdminUserApi
adminUserApi
;
// ==================== FastGPT 知识库配置 ====================
private
static
final
String
FASTGPT_BASE_URL
=
"http://192.168.252.71:18089"
;
private
static
final
String
FASTGPT_OPENAPI_BASE_URL
=
"http://192.168.252.71:18089/api/v1"
;
/** 测试智能体 key(测试按钮使用) */
private
static
final
String
TEST_AGENT_KEY
=
"fastgpt-cVpF0oIC2aqU0Xk9kzENXE6JsuykGt6c63pLitJLHHAETWqKLzQavD4QG3WX"
;
/** 测试知识库 ID(上传时先上传到这里) */
private
static
final
Map
<
String
,
String
>
TEST_KNOWLEDGE_BASE_MAP
=
Map
.
of
(
"公租房"
,
"6a471965df2c00b51ed97c4e"
,
"长租房"
,
"6a471957df2c00b51ed97b29"
);
/** 正式知识库 ID(测试通过后上传到这里) */
private
static
final
Map
<
String
,
String
>
PRODUCTION_KNOWLEDGE_BASE_MAP
=
Map
.
of
(
"公租房"
,
"6a4afb9bdf2c00b51eff4ca1"
,
"长租房"
,
"6a4afba5df2c00b51eff4dba"
);
/** 上传使用的 API key(测试/正式知识库共用) */
private
static
final
Map
<
String
,
String
>
FASTGPT_API_KEY_MAP
=
Map
.
of
(
"公租房"
,
"fastgpt-dU1AvlAtczVI95eEzmN7Dkx9O3hXArmUtUFYrbIbwUDpFHFPIPIqKw4m3S1jt2uB"
,
"长租房"
,
"fastgpt-t5H5QVcn3jb1Jj9mW169VbfZcphuFQrsOeiAIfdQbXD7Bld789QJle2ERt7ex"
);
/** 测试知识库集合 ID 缓存 */
private
static
final
Map
<
String
,
String
>
TEST_COLLECTION_ID_CACHE
=
new
ConcurrentHashMap
<>();
/** 正式知识库集合 ID 缓存 */
private
static
final
Map
<
String
,
String
>
PRODUCTION_COLLECTION_ID_CACHE
=
new
ConcurrentHashMap
<>();
private
static
final
String
COLLECTION_NAME
=
"对话知识库分析"
;
public
AiChatAnalysisDO
validateChatAnalysisExists
(
Long
id
)
{
public
AiChatAnalysisDO
validateChatAnalysisExists
(
Long
id
)
{
AiChatAnalysisDO
conversation
=
chatAnalysisMapper
.
selectById
(
id
);
AiChatAnalysisDO
conversation
=
chatAnalysisMapper
.
selectById
(
id
);
...
@@ -185,9 +223,6 @@ public class AiChatAnalysisServiceImpl implements AiChatAnalysisService {
...
@@ -185,9 +223,6 @@ public class AiChatAnalysisServiceImpl implements AiChatAnalysisService {
throw
exception
(
CHAT_ANALYSIS_NOT_EXISTS
);
throw
exception
(
CHAT_ANALYSIS_NOT_EXISTS
);
}
}
LocalDateTime
summaryTime
=
createReqVO
.
getSummaryTime
();
LocalDateTime
summaryTime
=
createReqVO
.
getSummaryTime
();
if
(
summaryTime
!=
null
)
{
summaryTime
=
summaryTime
.
withHour
(
0
).
withMinute
(
0
).
withSecond
(
0
).
withNano
(
0
);
}
// 2. 创建 AiChatConversationDO 聊天对话
// 2. 创建 AiChatConversationDO 聊天对话
AiChatSummaryDO
conversation
=
new
AiChatSummaryDO
().
setChatId
(
createReqVO
.
getChatId
())
AiChatSummaryDO
conversation
=
new
AiChatSummaryDO
().
setChatId
(
createReqVO
.
getChatId
())
.
setSummary
(
createReqVO
.
getSummary
())
.
setSummary
(
createReqVO
.
getSummary
())
...
@@ -211,9 +246,234 @@ public class AiChatAnalysisServiceImpl implements AiChatAnalysisService {
...
@@ -211,9 +246,234 @@ public class AiChatAnalysisServiceImpl implements AiChatAnalysisService {
throw
exception
(
CHAT_ANALYSIS_NOT_EXISTS
);
throw
exception
(
CHAT_ANALYSIS_NOT_EXISTS
);
}
}
// 2. 更新状态
// 2. 获取当前用户昵称
String
modifierName
=
null
;
try
{
Long
userId
=
getLoginUserId
();
if
(
userId
!=
null
)
{
// 优先从安全上下文获取昵称
modifierName
=
cn
.
iocoder
.
yudao
.
framework
.
security
.
core
.
util
.
SecurityFrameworkUtils
.
getLoginUserNickname
();
// 如果昵称为空,从 AdminUserApi 获取
if
(
cn
.
hutool
.
core
.
util
.
StrUtil
.
isBlank
(
modifierName
))
{
AdminUserRespDTO
user
=
adminUserApi
.
getUser
(
userId
);
if
(
user
!=
null
)
{
modifierName
=
user
.
getNickname
();
}
}
// 如果仍然为空,使用"管理员"作为默认显示名
if
(
cn
.
hutool
.
core
.
util
.
StrUtil
.
isBlank
(
modifierName
))
{
modifierName
=
"管理员"
;
}
}
}
catch
(
Exception
e
)
{
log
.
warn
(
"[updateSummaryStatus] 获取当前用户信息失败"
,
e
);
}
// 3. 更新状态 + 处理人昵称
chatSummaryMapper
.
updateById
(
new
AiChatSummaryDO
()
chatSummaryMapper
.
updateById
(
new
AiChatSummaryDO
()
.
setId
(
reqVO
.
getId
()).
setStatus
(
reqVO
.
getStatus
()));
.
setId
(
reqVO
.
getId
())
.
setStatus
(
reqVO
.
getStatus
())
.
setModifierName
(
modifierName
));
}
@Override
public
void
uploadToFastGpt
(
AiChatSummaryUploadReqVO
reqVO
)
{
uploadToKnowledgeBase
(
reqVO
,
TEST_KNOWLEDGE_BASE_MAP
,
TEST_COLLECTION_ID_CACHE
,
"测试"
);
}
@Override
public
void
uploadToProductionFastGpt
(
AiChatSummaryUploadReqVO
reqVO
)
{
uploadToKnowledgeBase
(
reqVO
,
PRODUCTION_KNOWLEDGE_BASE_MAP
,
PRODUCTION_COLLECTION_ID_CACHE
,
"正式"
);
}
private
void
uploadToKnowledgeBase
(
AiChatSummaryUploadReqVO
reqVO
,
Map
<
String
,
String
>
knowledgeBaseMap
,
Map
<
String
,
String
>
collectionIdCache
,
String
envName
)
{
String
datasetId
=
knowledgeBaseMap
.
get
(
reqVO
.
getCategory
());
if
(
datasetId
==
null
)
{
throw
exception
(
FASTGPT_CONFIG_ERROR
);
}
try
{
String
token
=
getFastGptToken
(
reqVO
.
getCategory
());
String
collectionId
=
collectionIdCache
.
get
(
datasetId
);
if
(
collectionId
==
null
)
{
synchronized
(
collectionIdCache
)
{
collectionId
=
collectionIdCache
.
get
(
datasetId
);
if
(
collectionId
==
null
)
{
collectionId
=
createTextCollection
(
token
,
datasetId
);
collectionIdCache
.
put
(
datasetId
,
collectionId
);
log
.
info
(
"[uploadTo{}KnowledgeBase] 首次创建集合, collectionId={}"
,
envName
,
collectionId
);
}
}
}
else
{
log
.
info
(
"[uploadTo{}KnowledgeBase] 复用已有集合, collectionId={}"
,
envName
,
collectionId
);
}
pushQaData
(
token
,
collectionId
,
reqVO
.
getKbTitle
(),
reqVO
.
getKbContent
());
log
.
info
(
"[uploadTo{}KnowledgeBase] QA 数据推送成功"
,
envName
);
}
catch
(
Exception
e
)
{
log
.
error
(
"[uploadTo{}KnowledgeBase] 上传至 FastGPT 失败"
,
envName
,
e
);
throw
exception
(
FASTGPT_UPLOAD_FAILED
);
}
}
// ==================== FastGPT API 调用 ====================
/**
* 获取 FastGPT 鉴权 token(按知识库区分)
*/
private
String
getFastGptToken
(
String
category
)
{
String
apiKey
=
FASTGPT_API_KEY_MAP
.
get
(
category
);
if
(
apiKey
==
null
)
{
throw
new
RuntimeException
(
"未找到分类 "
+
category
+
" 对应的 FastGPT API Key"
);
}
return
apiKey
;
}
/**
* 创建文本集合,返回 collectionId
* 接口:POST /api/core/dataset/collection/create/text
*/
private
String
createTextCollection
(
String
token
,
String
datasetId
)
{
HttpHeaders
headers
=
new
HttpHeaders
();
headers
.
setContentType
(
MediaType
.
APPLICATION_JSON
);
headers
.
setBearerAuth
(
token
);
Map
<
String
,
Object
>
body
=
new
HashMap
<>();
body
.
put
(
"datasetId"
,
datasetId
);
body
.
put
(
"name"
,
COLLECTION_NAME
);
body
.
put
(
"text"
,
""
);
// 空文本,避免自动生成多余 chunk
body
.
put
(
"trainingType"
,
"chunk"
);
body
.
put
(
"chunkSize"
,
500
);
HttpEntity
<
Map
<
String
,
Object
>>
request
=
new
HttpEntity
<>(
body
,
headers
);
ResponseEntity
<
Map
>
response
=
restTemplate
.
postForEntity
(
FASTGPT_BASE_URL
+
"/api/core/dataset/collection/create/text"
,
request
,
Map
.
class
);
if
(
response
.
getStatusCode
()
!=
HttpStatus
.
OK
||
response
.
getBody
()
==
null
)
{
throw
new
RuntimeException
(
"创建文本集合失败, status="
+
response
.
getStatusCode
());
}
Map
<
String
,
Object
>
responseBody
=
response
.
getBody
();
Integer
code
=
(
Integer
)
responseBody
.
get
(
"code"
);
if
(
code
==
null
||
code
!=
200
)
{
throw
new
RuntimeException
(
"创建文本集合失败, response="
+
responseBody
);
}
Map
<
String
,
Object
>
data
=
(
Map
<
String
,
Object
>)
responseBody
.
get
(
"data"
);
if
(
data
==
null
||
!
data
.
containsKey
(
"collectionId"
))
{
throw
new
RuntimeException
(
"创建文本集合响应中未找到 collectionId"
);
}
return
(
String
)
data
.
get
(
"collectionId"
);
}
/**
* 以 QA 模式推送数据到集合(q=用户问题, a=AI回答)
* 接口:POST /api/core/dataset/data/pushData
*/
private
void
pushQaData
(
String
token
,
String
collectionId
,
String
q
,
String
a
)
{
// 创建集合后需要短暂等待,确保集合已就绪
try
{
Thread
.
sleep
(
2000
);
}
catch
(
InterruptedException
ignored
)
{
}
HttpHeaders
headers
=
new
HttpHeaders
();
headers
.
setContentType
(
MediaType
.
APPLICATION_JSON
);
headers
.
setBearerAuth
(
token
);
// 构建单个 QA 数据项
Map
<
String
,
Object
>
qaItem
=
new
HashMap
<>();
qaItem
.
put
(
"q"
,
q
);
qaItem
.
put
(
"a"
,
a
);
Map
<
String
,
Object
>
body
=
new
HashMap
<>();
body
.
put
(
"collectionId"
,
collectionId
);
body
.
put
(
"trainingMode"
,
"chunk"
);
body
.
put
(
"data"
,
Collections
.
singletonList
(
qaItem
));
HttpEntity
<
Map
<
String
,
Object
>>
request
=
new
HttpEntity
<>(
body
,
headers
);
ResponseEntity
<
Map
>
response
=
restTemplate
.
postForEntity
(
FASTGPT_BASE_URL
+
"/api/core/dataset/data/pushData"
,
request
,
Map
.
class
);
if
(
response
.
getStatusCode
()
!=
HttpStatus
.
OK
||
response
.
getBody
()
==
null
)
{
throw
new
RuntimeException
(
"推送 QA 数据失败, status="
+
response
.
getStatusCode
());
}
Map
<
String
,
Object
>
responseBody
=
response
.
getBody
();
Integer
code
=
(
Integer
)
responseBody
.
get
(
"code"
);
if
(
code
==
null
||
code
!=
200
)
{
throw
new
RuntimeException
(
"推送 QA 数据失败, response="
+
responseBody
);
}
// insertLen 在 responseBody.data 内部
Map
<
String
,
Object
>
data
=
(
Map
<
String
,
Object
>)
responseBody
.
get
(
"data"
);
Integer
insertLen
=
data
!=
null
?
(
Integer
)
data
.
get
(
"insertLen"
)
:
0
;
if
(
insertLen
==
null
||
insertLen
==
0
)
{
throw
new
RuntimeException
(
"推送 QA 数据成功但 insertLen=0,数据可能未被索引"
);
}
}
@Override
public
String
testFastGpt
(
String
question
,
String
category
)
{
// 使用测试智能体 key,通过 house_type 变量区分公租房/长租房
String
token
=
TEST_AGENT_KEY
;
HttpHeaders
headers
=
new
HttpHeaders
();
headers
.
setContentType
(
MediaType
.
APPLICATION_JSON
);
headers
.
setBearerAuth
(
token
);
Map
<
String
,
Object
>
requestBody
=
new
HashMap
<>();
requestBody
.
put
(
"messages"
,
Collections
.
singletonList
(
Map
.
of
(
"role"
,
"user"
,
"content"
,
question
)));
requestBody
.
put
(
"stream"
,
false
);
requestBody
.
put
(
"variables"
,
Map
.
of
(
"house_type"
,
category
));
HttpEntity
<
Map
<
String
,
Object
>>
request
=
new
HttpEntity
<>(
requestBody
,
headers
);
ResponseEntity
<
Map
>
response
=
restTemplate
.
postForEntity
(
FASTGPT_OPENAPI_BASE_URL
+
"/chat/completions"
,
request
,
Map
.
class
);
if
(
response
.
getStatusCode
()
!=
HttpStatus
.
OK
||
response
.
getBody
()
==
null
)
{
throw
new
RuntimeException
(
"FastGPT 测试连接失败, status="
+
response
.
getStatusCode
());
}
Map
<
String
,
Object
>
responseBody
=
response
.
getBody
();
Integer
code
=
(
Integer
)
responseBody
.
get
(
"code"
);
if
(
code
!=
null
&&
code
!=
200
)
{
throw
new
RuntimeException
(
"FastGPT 测试失败, response="
+
responseBody
);
}
// 兼容两种响应格式:OpenAI 兼容格式 或 FastGPT 内部格式
// OpenAI: { choices: [{ message: { content: "..." } }] }
// FastGPT: { data: { choices: [{ message: { content: "..." } }] } }
Map
<
String
,
Object
>
data
=
(
Map
<
String
,
Object
>)
responseBody
.
get
(
"data"
);
List
<
Map
<
String
,
Object
>>
choices
;
if
(
data
!=
null
&&
data
.
containsKey
(
"choices"
))
{
choices
=
(
List
<
Map
<
String
,
Object
>>)
data
.
get
(
"choices"
);
}
else
if
(
responseBody
.
containsKey
(
"choices"
))
{
choices
=
(
List
<
Map
<
String
,
Object
>>)
responseBody
.
get
(
"choices"
);
}
else
{
// 如果 responseBody 本身就有 message/content 字段(部分 FastGPT 版本)
Object
directContent
=
responseBody
.
get
(
"content"
);
if
(
directContent
!=
null
)
{
return
directContent
.
toString
();
}
throw
new
RuntimeException
(
"FastGPT 响应格式异常,无法解析回复"
);
}
if
(
choices
==
null
||
choices
.
isEmpty
())
{
throw
new
RuntimeException
(
"FastGPT 返回空结果"
);
}
Map
<
String
,
Object
>
message
=
(
Map
<
String
,
Object
>)
choices
.
get
(
0
).
get
(
"message"
);
if
(
message
==
null
||
!
message
.
containsKey
(
"content"
))
{
throw
new
RuntimeException
(
"FastGPT 响应中未找到回复内容"
);
}
return
(
String
)
message
.
get
(
"content"
);
}
}
private
Double
calculatePercent
(
Long
current
,
Long
previous
)
{
private
Double
calculatePercent
(
Long
current
,
Long
previous
)
{
...
...
yudao-ui-admin-vue3/src/views/ai/chat_analysis/summary/index.vue
View file @
710c5e1c
<
template
>
<
template
>
<ContentWrap>
<!-- ==================== 视图1: 表格页 ==================== -->
<!-- 搜索工作栏 -->
<div
v-show=
"currentView === 'table'"
>
<el-form
<ContentWrap>
class=
"-mb-15px"
<div
class=
"filter-card"
>
:model=
"queryParams"
<div
class=
"filter-item"
>
ref=
"queryFormRef"
<span
class=
"filter-label"
>
分类
</span>
:inline=
"true"
<el-select
v-model=
"queryParams.type"
placeholder=
"请选择分类"
clearable
style=
"width:220px"
>
label-width=
"68px"
<el-option
label=
"公租房"
value=
"公租房"
/>
>
<el-option
label=
"长租房"
value=
"长租房"
/>
<el-form-item
label=
"分类"
prop=
"type"
>
</el-select>
<el-select
</div>
v-model=
"queryParams.type"
<div
class=
"filter-item"
>
placeholder=
"请选择分类"
<span
class=
"filter-label"
>
状态
</span>
clearable
<el-select
v-model=
"queryParams.status"
placeholder=
"请选择状态"
clearable
style=
"width:220px"
>
class=
"!w-240px"
<el-option
label=
"待处理"
:value=
"0"
/>
>
<el-option
label=
"已处理"
:value=
"1"
/>
<el-option
label=
"公租房"
value=
"公租房"
/>
</el-select>
<el-option
label=
"长租房"
value=
"长租房"
/>
</div>
</el-select>
<button
class=
"btn"
@
click=
"handleQuery"
>
🔍 搜索
</button>
</el-form-item>
<button
class=
"btn"
@
click=
"resetQuery"
>
↻ 重置
</button>
<el-form-item
label=
"状态"
prop=
"status"
>
</div>
<el-select
</ContentWrap>
v-model=
"queryParams.status"
placeholder=
"请选择状态"
<ContentWrap>
clearable
<div
class=
"table-card"
>
class=
"!w-240px"
<table>
>
<thead>
<el-option
label=
"待处理"
:value=
"0"
/>
<tr>
<el-option
label=
"已处理"
:value=
"1"
/>
<th
class=
"col-index"
>
序号
</th>
</el-select>
<th
class=
"col-category"
>
分类
</th>
</el-form-item>
<th>
建议内容
</th>
<el-form-item>
<th
class=
"col-time"
>
分析时间
</th>
<el-button
@
click=
"handleQuery"
>
<th
class=
"col-status"
>
状态
</th>
<Icon
icon=
"ep:search"
class=
"mr-5px"
/>
搜索
<th
class=
"col-action"
>
操作
</th>
</el-button>
</tr>
<el-button
@
click=
"resetQuery"
>
</thead>
<Icon
icon=
"ep:refresh"
class=
"mr-5px"
/>
重置
<tbody
id=
"tableBody"
>
</el-button>
<tr
</el-form-item>
v-for=
"(row, idx) in list"
:key=
"row.id"
</el-form>
:data-id=
"row.id"
</ContentWrap>
:data-category=
"row.type"
:data-original-question=
"row.originalQuestion || ''"
>
<!-- 列表 -->
<td
class=
"col-index"
>
{{
(
queryParams
.
pageNo
-
1
)
*
queryParams
.
pageSize
+
idx
+
1
}}
</td>
<ContentWrap>
<td
class=
"col-category"
>
{{
row
.
type
}}
</td>
<el-table
<td
class=
"col-content"
>
{{
row
.
summary
||
'-'
}}
</td>
v-loading=
"loading"
<td
class=
"col-time"
>
{{
formatTime
(
row
.
summaryTime
||
row
.
createTime
)
}}
</td>
:data=
"list"
<td
class=
"col-status"
>
:stripe=
"true"
<span
:class=
"['status-tag', row.status === 1 ? 'done' : '']"
>
:show-overflow-tooltip=
"true"
{{
row
.
status
===
1
?
'已完成'
:
'待处理'
}}
>
</span>
<!--
<el-table-column
label=
"编号"
align=
"center"
prop=
"id"
width=
"100px"
/>
-->
<div
v-if=
"row.status === 1"
class=
"completed-info"
>
<el-table-column
label=
"序号"
align=
"center"
type=
"index"
width=
"100px"
/>
<div
v-if=
"row.modifierName"
>
处理人:
{{
row
.
modifierName
}}
</div>
<el-table-column
label=
"分类"
align=
"center"
prop=
"type"
min-width=
"120px"
/>
<div
v-if=
"row.updateTime"
>
{{
formatTime
(
row
.
updateTime
)
}}
</div>
<el-table-column
label=
"建议内容"
align=
"left"
prop=
"summary"
min-width=
"300px"
>
</div>
<template
#
default=
"scope"
>
</td>
<div
class=
"summary-content"
>
{{
scope
.
row
.
summary
||
'-'
}}
</div>
<td
class=
"col-action"
>
</
template
>
<button
class=
"btn-handle"
@
click=
"goToEdit(row)"
>
处理
</button>
</el-table-column>
</td>
<!-- 新增:状态标签列 -->
</tr>
<el-table-column
label=
"状态"
align=
"center"
width=
"100px"
>
</tbody>
<
template
#
default=
"scope"
>
</table>
<el-tag
:type=
"scope.row.status === 1 ? 'success' : 'warning'"
>
</div>
{{
scope
.
row
.
status
===
1
?
'已处理'
:
'待处理'
}}
<Pagination
</el-tag>
:total=
"total"
</
template
>
v-model:page=
"queryParams.pageNo"
</el-table-column>
v-model:limit=
"queryParams.pageSize"
<el-table-column
label=
"是否已修改"
align=
"center"
prop=
"status"
width=
"100px"
>
@
pagination=
"getList"
<
template
#
default=
"scope"
>
/>
<el-checkbox
</ContentWrap>
v-model=
"scope.row.status"
</div>
:true-value=
"1"
:false-value=
"0"
<!-- ==================== 视图2: 编辑页 ==================== -->
@
change=
"handleStatusChange(scope.row, $event)"
<div
v-show=
"currentView === 'edit'"
>
/>
<ContentWrap>
</
template
>
<div
class=
"steps"
>
</el-table-column>
<div
class=
"step active"
><span
class=
"step-num"
>
1
</span><span>
编辑知识库
</span></div>
</el-table>
<span
class=
"step-line"
></span>
<div
class=
"step"
><span
class=
"step-num"
>
2
</span><span>
测试验证
</span></div>
<!-- 分页 -->
<span
class=
"step-line"
></span>
<Pagination
<div
class=
"step"
><span
class=
"step-num"
>
3
</span><span>
完成处理
</span></div>
:total=
"total"
</div>
v-model:page=
"queryParams.pageNo"
v-model:limit=
"queryParams.pageSize"
<div
class=
"page-header"
>
@
pagination=
"getList"
<div
class=
"page-title"
>
📝 知识库编辑
</div>
/>
</div>
</ContentWrap>
<!-- 原对话 -->
<div
class=
"dialog-card"
>
<div
class=
"ref-label"
>
💬 原对话
</div>
<div
class=
"dialog-bubble user"
>
<span
class=
"bubble-label"
>
用户
</span>
<span
class=
"bubble-text"
id=
"dlgUserQ"
>
{{
editForm
.
question
}}
</span>
</div>
<div
class=
"dialog-bubble ai"
>
<span
class=
"bubble-label"
>
AI
</span>
<span
class=
"bubble-text"
id=
"dlgAiA"
>
{{
editForm
.
answer
}}
</span>
</div>
</div>
<!-- 分析建议 -->
<div
class=
"ref-card"
>
<div
class=
"ref-label"
>
{{
'📌 对话分析建议(分类:'
+
editForm
.
category
+
')'
}}
</div>
<div
class=
"ref-content"
>
{{
editForm
.
suggestionContent
}}
</div>
</div>
<!-- 编辑表单 -->
<div
class=
"form-card"
>
<div
class=
"form-group"
>
<div
class=
"form-label"
>
<span
class=
"required"
>
*
</span>
问答标题
<span
class=
"label-tip"
>
(可修改)
</span>
</div>
<input
class=
"form-input"
v-model=
"editForm.kbTitle"
type=
"text"
placeholder=
"请输入知识库条目标题"
/>
</div>
<div
class=
"form-group"
>
<div
class=
"form-label"
><span
class=
"required"
>
*
</span>
正文内容
</div>
<textarea
class=
"form-textarea"
v-model=
"editForm.kbContent"
placeholder=
"请输入知识库条目正文内容 提示:请根据原始建议完善知识内容,确保信息准确、完整、易于检索回答。"
></textarea>
<div
class=
"char-count"
><span>
{{
editForm
.
kbContent
.
length
}}
</span>
字
</div>
<div
class=
"tip-text"
>
💡 编辑完成后点击"保存并测试",将进入测试验证环节确认知识库条目是否正确回答问题。
</div>
</div>
</div>
<div
class=
"btn-row"
>
<button
class=
"btn"
@
click=
"switchView('table')"
>
← 返回列表
</button>
<button
class=
"btn btn-primary"
:disabled=
"saving"
@
click=
"saveAndTest"
>
💾 保存并测试
</button>
</div>
</ContentWrap>
</div>
<!-- ==================== 视图3: 测试页 ==================== -->
<div
v-show=
"currentView === 'test'"
>
<ContentWrap>
<div
class=
"steps"
>
<div
class=
"step done"
><span
class=
"step-num"
>
1
</span><span>
编辑知识库
</span></div>
<span
class=
"step-line"
></span>
<div
class=
"step active"
><span
class=
"step-num"
>
2
</span><span>
测试验证
</span></div>
<span
class=
"step-line"
></span>
<div
class=
"step"
><span
class=
"step-num"
>
3
</span><span>
完成处理
</span></div>
</div>
<div
class=
"page-header"
>
<div
class=
"page-title"
>
🧪 知识测试验证
</div>
</div>
<div
class=
"info-card"
>
<div
class=
"info-row"
><span
class=
"info-label"
>
原始问题
</span><span
class=
"info-value"
>
{{
testInfo
.
originalQuestion
}}
</span></div>
<div
class=
"info-row"
><span
class=
"info-label"
>
建议分类
</span><span
class=
"info-value"
><span
class=
"cat-tag"
>
{{
testInfo
.
category
}}
</span></span></div>
<div
class=
"info-row"
><span
class=
"info-label"
>
知识标题
</span><span
class=
"info-value"
>
{{
testInfo
.
kbTitle
}}
</span></div>
</div>
<div
class=
"test-list"
>
<div
:class=
"['test-item', testResultPassed ? 'pass-border' : 'fail-border']"
>
<div
class=
"test-header"
>
<div
class=
"test-question"
><span
class=
"q-label original"
>
原始问题
</span><span>
{{
testInfo
.
originalQuestion
}}
</span></div>
</div>
<div
class=
"test-body open"
>
<table
class=
"compare-table"
>
<tr>
<th
class=
"col-label"
>
AI 回答
</th>
<td
class=
"ai-answer"
>
{{
testInfo
.
aiAnswer
||
'等待测试...'
}}
</td>
</tr>
<tr>
<th
class=
"col-label"
>
期望回答
</th>
<td
class=
"expected-answer"
>
{{
testInfo
.
kbContent
||
'-'
}}
</td>
</tr>
</table>
</div>
</div>
</div>
<!-- 操作按钮 -->
<div
class=
"btn-row"
>
<button
class=
"btn"
@
click=
"switchView('edit')"
>
← 返回编辑
</button>
<button
class=
"btn btn-success"
id=
"btnWrite"
:disabled=
"!testDone"
@
click=
"writeToKB"
>
测试通过
</button>
</div>
</ContentWrap>
</div>
</
template
>
</
template
>
<
script
setup
lang=
"ts"
>
<
script
setup
lang=
"ts"
>
import
{
ref
,
reactive
,
onMounted
}
from
'vue'
import
{
ref
,
reactive
,
onMounted
}
from
'vue'
import
{
ElMessage
,
ElMessageBox
}
from
'element-plus'
import
{
ElMessage
}
from
'element-plus'
import
{
ChatAnalysisApi
,
ChatAnalysisVO
}
from
'@/api/ai/chat/analysis'
import
{
ChatAnalysisApi
}
from
'@/api/ai/chat/analysis'
/** AI 对话总结列表 */
defineOptions
({
name
:
'AiChatSummary'
})
defineOptions
({
name
:
'AiChatSummary'
})
const
message
=
ElMessage
const
message
=
ElMessage
// ==================== 视图切换 ====================
const
currentView
=
ref
(
'table'
)
function
switchView
(
view
:
string
)
{
currentView
.
value
=
view
}
// ==================== 表格数据 ====================
const
loading
=
ref
(
true
)
const
loading
=
ref
(
true
)
const
list
=
ref
<
ChatAnalysisVO
[]
>
([])
const
list
=
ref
<
any
[]
>
([])
const
total
=
ref
(
0
)
const
total
=
ref
(
0
)
const
queryFormRef
=
ref
()
// 查询参数
const
queryParams
=
reactive
({
const
queryParams
=
reactive
({
pageNo
:
1
,
pageNo
:
1
,
pageSize
:
10
,
pageSize
:
10
,
...
@@ -109,8 +205,7 @@ const queryParams = reactive({
...
@@ -109,8 +205,7 @@ const queryParams = reactive({
status
:
undefined
as
number
|
undefined
status
:
undefined
as
number
|
undefined
})
})
/** 查询列表 */
async
function
getList
()
{
const
getList
=
async
()
=>
{
loading
.
value
=
true
loading
.
value
=
true
try
{
try
{
const
data
=
await
ChatAnalysisApi
.
getChatSummaryPage
(
queryParams
)
const
data
=
await
ChatAnalysisApi
.
getChatSummaryPage
(
queryParams
)
...
@@ -121,59 +216,590 @@ const getList = async () => {
...
@@ -121,59 +216,590 @@ const getList = async () => {
}
}
}
}
/** 搜索按钮操作 */
function
handleQuery
()
{
const
handleQuery
=
()
=>
{
queryParams
.
pageNo
=
1
queryParams
.
pageNo
=
1
getList
()
getList
()
}
}
/** 重置按钮操作 */
function
resetQuery
()
{
const
resetQuery
=
()
=>
{
queryParams
.
type
=
undefined
query
FormRef
.
value
?.
resetFields
()
query
Params
.
status
=
undefined
handleQuery
()
handleQuery
()
}
}
/** 修改状态操作(使用 switch 切换时触发) */
const
formatTime
=
(
time
:
string
|
number
|
undefined
|
null
)
=>
{
const
handleStatusChange
=
async
(
row
:
ChatAnalysisVO
,
newStatus
:
number
)
=>
{
if
(
time
===
undefined
||
time
===
null
||
time
===
''
)
return
'-'
const
oldStatus
=
row
.
status
if
(
typeof
time
===
'number'
)
{
const
d
=
new
Date
(
time
)
const
pad
=
(
n
:
number
)
=>
String
(
n
).
padStart
(
2
,
'0'
)
return
`
${
d
.
getFullYear
()}
-
${
pad
(
d
.
getMonth
()
+
1
)}
-
${
pad
(
d
.
getDate
())}
${
pad
(
d
.
getHours
())}
:
${
pad
(
d
.
getMinutes
())}
:
${
pad
(
d
.
getSeconds
())}
`
}
const
t
=
time
.
replace
(
'T'
,
' '
)
return
t
.
length
>
16
?
t
.
substring
(
0
,
19
)
:
t
}
// ==================== 编辑页数据 ====================
const
editForm
=
reactive
({
id
:
0
,
chatId
:
''
,
category
:
''
,
question
:
''
,
answer
:
''
,
suggestionContent
:
''
,
kbTitle
:
''
,
kbContent
:
''
})
/** 从列表点击"处理"进入编辑 */
async
function
goToEdit
(
row
:
any
)
{
editForm
.
id
=
row
.
id
editForm
.
chatId
=
row
.
chatId
||
''
editForm
.
category
=
row
.
type
||
''
editForm
.
suggestionContent
=
row
.
summary
||
''
editForm
.
kbTitle
=
''
editForm
.
kbContent
=
''
// 加载对话消息
if
(
editForm
.
chatId
)
{
try
{
const
msgs
=
await
ChatAnalysisApi
.
getChatList
(
editForm
.
chatId
)
if
(
msgs
&&
msgs
.
length
>
0
)
{
// 【修复】优先根据 summaryTime 匹配生成该总结的那条消息,而不是取第一条
// 因为同一个对话(chatId)下有多条 Q&A,总结是从某一条特定问答生成的
const
matchedMsg
=
row
.
summaryTime
?
msgs
.
find
((
m
:
any
)
=>
{
// 比较时间,处理可能的格式差异(ISO字符串 / 时间戳 / 数组)
const
msgTime
=
m
.
chatTime
return
msgTime
&&
String
(
msgTime
)
===
String
(
row
.
summaryTime
)
})
:
null
const
targetMsg
=
matchedMsg
||
msgs
.
find
((
m
:
any
)
=>
m
.
question
)
editForm
.
question
=
targetMsg
?.
question
||
'-'
editForm
.
answer
=
targetMsg
?.
answer
||
'-'
// 自动预填标题(用匹配到的消息的question)
if
(
targetMsg
?.
question
)
{
editForm
.
kbTitle
=
targetMsg
.
question
}
}
}
catch
{
editForm
.
question
=
'-'
editForm
.
answer
=
'-'
message
.
warning
(
'加载对话消息失败'
)
}
}
// 重置测试状态
saving
.
value
=
false
testResultPassed
.
value
=
false
testDone
.
value
=
false
testInfo
.
originalQuestion
=
editForm
.
question
testInfo
.
aiAnswer
=
''
testInfo
.
kbContent
=
''
testInfo
.
category
=
editForm
.
category
testInfo
.
kbTitle
=
editForm
.
kbTitle
switchView
(
'edit'
)
}
// ==================== 测试页数据 ====================
const
testResultPassed
=
ref
(
false
)
const
testDone
=
ref
(
false
)
const
uploading
=
ref
(
false
)
const
testInfo
=
reactive
({
originalQuestion
:
''
,
category
:
''
,
kbTitle
:
''
,
kbContent
:
''
,
aiAnswer
:
''
})
const
saving
=
ref
(
false
)
/** 保存并测试 - 点击后立即跳转测试页,后台慢慢保存 */
async
function
saveAndTest
()
{
if
(
saving
.
value
)
return
if
(
!
editForm
.
kbTitle
.
trim
())
{
message
.
warning
(
'请输入知识标题'
)
return
}
if
(
!
editForm
.
kbContent
.
trim
())
{
message
.
warning
(
'请输入正文内容'
)
return
}
// 立即禁用按钮并跳转测试页
saving
.
value
=
true
testInfo
.
originalQuestion
=
editForm
.
question
testInfo
.
category
=
editForm
.
category
testInfo
.
kbTitle
=
editForm
.
kbTitle
testInfo
.
kbContent
=
editForm
.
kbContent
testInfo
.
aiAnswer
=
'正在保存并测试,请稍候...'
testResultPassed
.
value
=
false
testDone
.
value
=
false
switchView
(
'test'
)
// 后台异步执行上传 + 测试
executeSaveAndTest
()
}
async
function
executeSaveAndTest
()
{
// 先上传至测试知识库
uploading
.
value
=
true
try
{
try
{
// 二次确认
await
ChatAnalysisApi
.
uploadToFastGpt
({
// await ElMessageBox.confirm(
id
:
editForm
.
id
,
// `确认要将该记录状态改为 ${newStatus === 1 ? '已处理' : '未处理'} 吗?`,
kbTitle
:
editForm
.
kbTitle
.
trim
(),
// '提示',
kbContent
:
editForm
.
kbContent
.
trim
(),
// {
category
:
editForm
.
category
// confirmButtonText: '确定',
// cancelButtonText: '取消',
// type: 'warning'
// }
//)
// 调用更新接口
await
ChatAnalysisApi
.
updateSummaryStatus
({
id
:
row
.
id
,
status
:
newStatus
})
})
message
.
success
(
'状态更新成功'
)
}
catch
(
e
:
any
)
{
// 刷新列表(保持当前页码)
message
.
error
(
'上传至测试知识库失败:'
+
(
e
?.
message
||
e
?.
userMsg
||
'连接异常'
))
await
getList
()
uploading
.
value
=
false
}
catch
(
error
)
{
return
// 用户取消或接口失败时,恢复原来的状态
}
row
.
status
=
oldStatus
uploading
.
value
=
false
if
(
error
!==
'cancel'
)
{
message
.
error
(
'状态更新失败,请稍后重试'
)
// 自动调用 FastGPT 测试
await
runTest
()
}
/** 执行测试 - 调用 FastGPT */
async
function
runTest
()
{
if
(
!
testInfo
.
originalQuestion
||
testInfo
.
originalQuestion
===
'-'
)
{
message
.
warning
(
'无原始问题可用于测试'
)
return
}
testDone
.
value
=
false
testResultPassed
.
value
=
false
try
{
const
result
=
await
ChatAnalysisApi
.
testFastGpt
({
question
:
testInfo
.
originalQuestion
,
category
:
testInfo
.
category
})
testInfo
.
aiAnswer
=
result
testDone
.
value
=
true
// 简单的通过判断:不包含"抱歉""暂无"即为通过
testResultPassed
.
value
=
result
.
indexOf
(
'抱歉'
)
===
-
1
&&
result
.
indexOf
(
'暂无'
)
===
-
1
if
(
!
testResultPassed
.
value
)
{
message
.
warning
(
'AI 回答未能命中知识库,请检查知识内容'
)
}
}
}
catch
(
e
:
any
)
{
testInfo
.
aiAnswer
=
'测试失败:'
+
(
e
?.
message
||
e
?.
userMsg
||
'连接异常'
)
testDone
.
value
=
true
testResultPassed
.
value
=
false
}
}
}
}
/** 初始化加载数据 */
/** 测试通过 - 自动上传正式知识库 → 标记已完成 → 返回列表 */
async
function
writeToKB
()
{
// 1. 先上传至正式知识库
uploading
.
value
=
true
try
{
await
ChatAnalysisApi
.
uploadToProductionFastGpt
({
id
:
editForm
.
id
,
kbTitle
:
editForm
.
kbTitle
.
trim
(),
kbContent
:
editForm
.
kbContent
.
trim
(),
category
:
editForm
.
category
})
}
catch
(
e
:
any
)
{
message
.
error
(
'上传正式知识库失败:'
+
(
e
?.
message
||
e
?.
userMsg
||
'连接异常'
))
uploading
.
value
=
false
return
}
uploading
.
value
=
false
// 2. 标记为已完成
message
.
success
(
'知识库写入成功!正在返回列表页...'
)
setTimeout
(
async
()
=>
{
try
{
await
ChatAnalysisApi
.
updateSummaryStatus
({
id
:
editForm
.
id
,
status
:
1
})
}
catch
{
// 静默处理
}
await
getList
()
switchView
(
'table'
)
},
1500
)
}
onMounted
(()
=>
{
onMounted
(()
=>
{
getList
()
getList
()
})
})
</
script
>
</
script
>
<
style
scoped
lang=
"scss"
>
<
style
scoped
lang=
"scss"
>
.summary-content
{
/* ====== 筛选卡片 ====== */
white-space
:
pre-wrap
;
.filter-card
{
word-break
:
break-all
;
background
:
#fff
;
line-height
:
1.5
;
border-radius
:
4px
;
padding
:
16px
20px
;
margin-bottom
:
12px
;
display
:
flex
;
align-items
:
center
;
gap
:
16px
;
}
.filter-item
{
display
:
flex
;
align-items
:
center
;
gap
:
8px
;
}
.filter-label
{
font-size
:
13px
;
color
:
#333
;
white-space
:
nowrap
;
}
/* ====== 通用按钮 ====== */
.btn
{
height
:
32px
;
padding
:
0
16px
;
border-radius
:
4px
;
border
:
1px
solid
#d9d9d9
;
background
:
#fff
;
cursor
:
pointer
;
font-size
:
13px
;
display
:
inline-flex
;
align-items
:
center
;
gap
:
4px
;
color
:
#333
;
&:hover
{
border-color
:
#1890ff
;
color
:
#1890ff
;
}
}
.btn-primary
{
background
:
#1890ff
;
color
:
#fff
;
border-color
:
#1890ff
;
&:hover
{
background
:
#40a9ff
;
border-color
:
#40a9ff
;
color
:
#fff
;
}
&
:disabled
{
background
:
#91caff
;
border-color
:
#91caff
;
cursor
:
not-allowed
;
}
}
.btn-success
{
background
:
#52c41a
;
color
:
#fff
;
border-color
:
#52c41a
;
&:hover
{
background
:
#73d13d
;
border-color
:
#73d13d
;
color
:
#fff
;
}
&
:disabled
{
background
:
#b7eb8f
;
border-color
:
#b7eb8f
;
cursor
:
not-allowed
;
}
}
/* ====== 表格 ====== */
.table-card
{
background
:
#fff
;
border-radius
:
4px
;
overflow
:
hidden
;
}
table
{
width
:
100%
;
border-collapse
:
collapse
;
}
th
{
background
:
#fafafa
;
padding
:
12px
16px
;
text-align
:
left
;
font-size
:
13px
;
font-weight
:
500
;
color
:
#333
;
border-bottom
:
1px
solid
#e8e8e8
;
}
td
{
padding
:
14px
16px
;
font-size
:
13px
;
color
:
#333
;
border-bottom
:
1px
solid
#f0f0f0
;
vertical-align
:
top
;
word-wrap
:
break-word
;
}
tr
:hover
td
{
background
:
#fafafa
;
}
tr
:nth-child
(
even
)
td
{
background
:
#fafbfc
;
}
tr
:nth-child
(
even
)
:hover
td
{
background
:
#f0f5ff
;
}
.col-index
{
width
:
60px
;
text-align
:
center
;
}
.col-category
{
width
:
100px
;
text-align
:
center
;
}
.col-status
{
width
:
180px
;
text-align
:
center
;
}
.col-time
{
width
:
150px
;
text-align
:
center
;
white-space
:
nowrap
;
}
.col-action
{
width
:
80px
;
text-align
:
center
;
}
.col-content
{
line-height
:
1.6
;
}
.status-tag
{
display
:
inline-block
;
padding
:
2px
10px
;
background
:
#fff7e6
;
color
:
#fa8c16
;
border
:
1px
solid
#ffd591
;
border-radius
:
2px
;
font-size
:
12px
;
&.done
{
background
:
#f6ffed
;
color
:
#52c41a
;
border-color
:
#b7eb8f
;
}
}
.completed-info
{
font-size
:
11px
;
color
:
#999
;
margin-top
:
4px
;
line-height
:
1.5
;
}
.btn-handle
{
height
:
28px
;
padding
:
0
12px
;
background
:
#1890ff
;
color
:
#fff
;
border
:
none
;
border-radius
:
4px
;
cursor
:
pointer
;
font-size
:
12px
;
white-space
:
nowrap
;
&:hover
{
background
:
#40a9ff
;
}
}
/* ====== 步骤条 ====== */
.steps
{
display
:
flex
;
align-items
:
center
;
gap
:
8px
;
margin-bottom
:
20px
;
}
.step
{
display
:
flex
;
align-items
:
center
;
gap
:
6px
;
font-size
:
13px
;
color
:
#999
;
&.active
{
color
:
#1890ff
;
font-weight
:
500
;
}
&
.done
{
color
:
#52c41a
;
}
}
.step-num
{
width
:
22px
;
height
:
22px
;
border-radius
:
50%
;
background
:
#d9d9d9
;
color
:
#fff
;
display
:
flex
;
align-items
:
center
;
justify-content
:
center
;
font-size
:
12px
;
.step.active
&
{
background
:
#1890ff
;
}
.step.done
&
{
background
:
#52c41a
;
}
}
.step-line
{
width
:
40px
;
height
:
1px
;
background
:
#d9d9d9
;
.step.done
+
&
{
background
:
#52c41a
;
}
}
/* ====== 页面标题 ====== */
.page-header
{
margin-bottom
:
20px
;
}
.page-title
{
font-size
:
18px
;
font-weight
:
500
;
color
:
#333
;
margin-bottom
:
8px
;
}
/* ====== 参考卡片 ====== */
.ref-card
{
background
:
#fff
;
border-radius
:
4px
;
padding
:
16px
20px
;
margin-bottom
:
16px
;
border-left
:
3px
solid
#1890ff
;
}
.ref-label
{
font-size
:
12px
;
color
:
#999
;
margin-bottom
:
6px
;
display
:
inline-block
;
}
.ref-content
{
font-size
:
13px
;
color
:
#555
;
line-height
:
1.6
;
background
:
#fafafa
;
padding
:
12px
;
border-radius
:
4px
;
}
/* ====== 对话卡片 ====== */
.dialog-card
{
background
:
#fff
;
border-radius
:
4px
;
padding
:
16px
20px
;
margin-bottom
:
16px
;
border-left
:
3px
solid
#52c41a
;
}
.dialog-bubble
{
display
:
flex
;
align-items
:
flex-start
;
margin-bottom
:
10px
;
line-height
:
1.6
;
&:last-child
{
margin-bottom
:
0
;
}
.bubble-label
{
display
:
inline-block
;
width
:
36px
;
height
:
22px
;
line-height
:
22px
;
text-align
:
center
;
border-radius
:
3px
;
font-size
:
11px
;
color
:
#fff
;
flex-shrink
:
0
;
margin-right
:
10px
;
margin-top
:
2px
;
}
&
.user
.bubble-label
{
background
:
#1890ff
;
}
&
.ai
.bubble-label
{
background
:
#52c41a
;
}
.bubble-text
{
font-size
:
13px
;
color
:
#333
;
background
:
#fafafa
;
padding
:
8px
12px
;
border-radius
:
4px
;
flex
:
1
;
}
}
/* ====== 表单卡片 ====== */
.form-card
{
background
:
#fff
;
border-radius
:
4px
;
padding
:
24px
20px
;
margin-bottom
:
16px
;
}
.form-group
{
margin-bottom
:
20px
;
&:last-child
{
margin-bottom
:
0
;
}
}
.form-label
{
font-size
:
13px
;
font-weight
:
500
;
color
:
#333
;
margin-bottom
:
8px
;
display
:
flex
;
align-items
:
center
;
.required
{
color
:
#ff4d4f
;
margin-right
:
4px
;
}
.label-tip
{
color
:
#999
;
font-weight
:
400
;
margin-left
:
4px
;
font-size
:
12px
;
}
}
.form-input
{
width
:
100%
;
height
:
36px
;
border
:
1px
solid
#d9d9d9
;
border-radius
:
4px
;
padding
:
0
12px
;
font-size
:
13px
;
color
:
#333
;
outline
:
none
;
transition
:
border-color
0.2s
;
&:focus
{
border-color
:
#1890ff
;
box-shadow
:
0
0
0
2px
rgba
(
24
,
144
,
255
,
0.1
);
}
}
.form-textarea
{
width
:
100%
;
min-height
:
260px
;
border
:
1px
solid
#d9d9d9
;
border-radius
:
4px
;
padding
:
12px
;
font-size
:
13px
;
color
:
#333
;
outline
:
none
;
resize
:
vertical
;
line-height
:
1.8
;
font-family
:
inherit
;
transition
:
border-color
0.2s
;
&:focus
{
border-color
:
#1890ff
;
box-shadow
:
0
0
0
2px
rgba
(
24
,
144
,
255
,
0.1
);
}
}
.char-count
{
text-align
:
right
;
font-size
:
12px
;
color
:
#999
;
margin-top
:
4px
;
}
.tip-text
{
font-size
:
12px
;
color
:
#999
;
margin-top
:
8px
;
}
/* ====== 按钮行 ====== */
.btn-row
{
display
:
flex
;
gap
:
12px
;
justify-content
:
flex-end
;
background
:
#fff
;
border-radius
:
4px
;
padding
:
16px
20px
;
margin-top
:
16px
;
}
/* ====== 测试页 ====== */
.info-card
{
background
:
#fff
;
border-radius
:
4px
;
padding
:
16px
20px
;
margin-bottom
:
16px
;
}
.info-row
{
display
:
flex
;
margin-bottom
:
10px
;
font-size
:
13px
;
&:last-child
{
margin-bottom
:
0
;
}
}
.info-label
{
color
:
#999
;
width
:
80px
;
flex-shrink
:
0
;
}
.info-value
{
color
:
#333
;
line-height
:
1.5
;
}
.cat-tag
{
display
:
inline-block
;
padding
:
2px
10px
;
background
:
#e6f7ff
;
color
:
#1890ff
;
border-radius
:
2px
;
font-size
:
12px
;
}
.test-list
{
margin-bottom
:
20px
;
}
.test-item
{
background
:
#fff
;
border-radius
:
4px
;
margin-bottom
:
12px
;
overflow
:
hidden
;
border
:
1px
solid
#e8e8e8
;
&.pass-border
{
border-left
:
3px
solid
#52c41a
;
}
&
.fail-border
{
border-left
:
3px
solid
#ff4d4f
;
}
}
.test-header
{
padding
:
12px
16px
;
display
:
flex
;
align-items
:
center
;
justify-content
:
space-between
;
border-bottom
:
1px
solid
#f0f0f0
;
}
.test-question
{
font-size
:
13px
;
font-weight
:
500
;
color
:
#333
;
display
:
flex
;
align-items
:
center
;
gap
:
8px
;
}
.q-label
{
display
:
inline-block
;
padding
:
2px
8px
;
border-radius
:
2px
;
font-size
:
11px
;
font-weight
:
500
;
&.original
{
background
:
#e6f7ff
;
color
:
#1890ff
;
}
}
.test-body
{
display
:
block
;
}
.compare-table
{
width
:
100%
;
border-collapse
:
collapse
;
font-size
:
13px
;
th
{
background
:
#fafafa
;
padding
:
10px
12px
;
text-align
:
left
;
font-weight
:
500
;
color
:
#555
;
border
:
1px
solid
#e8e8e8
;
}
td
{
padding
:
10px
12px
;
border
:
1px
solid
#e8e8e8
;
vertical-align
:
top
;
line-height
:
1.6
;
color
:
#333
;
}
.col-label
{
width
:
90px
;
color
:
#999
;
}
}
}
</
style
>
.ai-answer
{
background
:
#f6ffed
;
color
:
#333
;
}
\ No newline at end of file
.expected-answer
{
background
:
#e6f7ff
;
color
:
#333
;
}
</
style
>
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment