|
|
@ -30,7 +30,7 @@ |
|
|
</div> |
|
|
</div> |
|
|
<!-- 回答 --> |
|
|
<!-- 回答 --> |
|
|
<div v-if="item.type === 'answer'" class="item-content mb-16 lighter" style="font-weight: 400"> |
|
|
<div v-if="item.type === 'answer'" class="item-content mb-16 lighter" style="font-weight: 400"> |
|
|
<el-popconfirm popper-style="z-index:99999" title="确定要回到这一步吗?" @confirm="confirmReturn(item,index)"> |
|
|
<el-popconfirm popper-style="z-index:2000" title="确定要回到这一步吗?" @confirm="confirmReturn(item,index)"> |
|
|
<template #reference> |
|
|
<template #reference> |
|
|
<div class="returnToHere" style="margin-top: 5px;margin-bottom: 5px"> |
|
|
<div class="returnToHere" style="margin-top: 5px;margin-bottom: 5px"> |
|
|
<i class="ri-bookmark-2-fill returnToHereIcon"></i> |
|
|
<i class="ri-bookmark-2-fill returnToHereIcon"></i> |
|
|
@ -46,27 +46,44 @@ |
|
|
<!-- </div>--> |
|
|
<!-- </div>--> |
|
|
<div class="content"> |
|
|
<div class="content"> |
|
|
<MdRenderer :is_large="is_large" :chatIndex="index" :source="item.content" @fullscreenG6="fullscreen"></MdRenderer> |
|
|
<MdRenderer :is_large="is_large" :chatIndex="index" :source="item.content" @fullscreenG6="fullscreen"></MdRenderer> |
|
|
<Interrupt :isLastChat="index === (chatList.length - 1)" :chatId="item.chatId" :source="item.interrupt" :checkpointer="item.checkpointer" @processAuth="processAuth"></Interrupt> |
|
|
<Interrupt :isLastChat="index === (chatList.length - 1)" :chatId="item.chatId" :action="item.action" :source="item.interrupt" :checkpointer="item.checkpointer" @processAuth="processAuth"></Interrupt> |
|
|
<div class="flex-between mt-8" style="display: flex; justify-content: space-between; align-items: center; margin-top: 8px"> |
|
|
|
|
|
<div> |
|
|
|
|
|
<el-button |
|
|
|
|
|
type="primary" |
|
|
|
|
|
v-if="item.isStop && !item.isEnd" |
|
|
|
|
|
@click="startChat(index)" |
|
|
|
|
|
link |
|
|
|
|
|
>重新生成 |
|
|
|
|
|
</el-button> |
|
|
|
|
|
<el-button type="primary" v-else-if="!item.isEnd" @click="stopChat(index)" link |
|
|
|
|
|
>停止回答 |
|
|
|
|
|
</el-button> |
|
|
|
|
|
</div> |
|
|
|
|
|
</div> |
|
|
|
|
|
<div v-if="item.isEnd" class="flex-between" style="display: flex; justify-content: space-between; align-items: center;"> |
|
|
<div v-if="item.isEnd" class="flex-between" style="display: flex; justify-content: space-between; align-items: center;"> |
|
|
<OperationButton :data="item" :index="index" @regeneration="regenerationChart(index)" @changeThumb="changeThumb" /> |
|
|
<OperationButton :data="item" :index="index" @changeThumb="changeThumb" /> |
|
|
|
|
|
<!-- <OperationButton :data="item" :index="index" @regeneration="regenerationChart(index)" @changeThumb="changeThumb" />--> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</template> |
|
|
</template> |
|
|
|
|
|
<div class="flex-between mt-8" style="display: flex; justify-content: space-between; align-items: center; margin-top: 8px"> |
|
|
|
|
|
<div> |
|
|
|
|
|
<div v-if="currentJobId !== ''"> |
|
|
|
|
|
<i class="ri-loader-2-line loading-icon"></i> |
|
|
|
|
|
<span style="font-size: 14px;color: #409EFF">回复中...</span> |
|
|
|
|
|
</div> |
|
|
|
|
|
<!-- <el-button--> |
|
|
|
|
|
<!-- type="primary"--> |
|
|
|
|
|
<!-- v-if="chatList.length>0 && chatList[chatList.length-1].isStop && !chatList[chatList.length-1].isEnd"--> |
|
|
|
|
|
<!-- @click="startChat(chatList.length-1)"--> |
|
|
|
|
|
<!-- link--> |
|
|
|
|
|
<!-- >重新生成--> |
|
|
|
|
|
<!-- </el-button>--> |
|
|
|
|
|
<!-- <el-button type="primary" v-else-if="chatList.length>0 && !chatList[chatList.length-1].isEnd" @click="stopChat(chatList.length-1)" link--> |
|
|
|
|
|
<!-- >停止回答--> |
|
|
|
|
|
<!-- </el-button>--> |
|
|
|
|
|
<el-button type="primary" v-if="currentJobId !== ''" link @click="stopChat(chatList.length-1)" |
|
|
|
|
|
>停止回答 |
|
|
|
|
|
</el-button> |
|
|
|
|
|
</div> |
|
|
|
|
|
</div> |
|
|
|
|
|
<div v-if="currentError !== ''"> |
|
|
|
|
|
<div class="returnToHere" style="margin-top: 5px;margin-bottom: 5px"> |
|
|
|
|
|
<i class="ri-bookmark-2-fill returnToHereIcon"></i> |
|
|
|
|
|
<el-divider class="returnToHereDivider"> |
|
|
|
|
|
</el-divider> |
|
|
|
|
|
</div> |
|
|
|
|
|
<span style="font-size: 14px">{{currentError}}</span> |
|
|
|
|
|
<el-link :underline="false" icon="Refresh" type="primary" @click="refreshModal">重试</el-link> |
|
|
|
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</el-scrollbar> |
|
|
</el-scrollbar> |
|
|
<div class="ai-chat__operate p-24" style="padding: 24px"> |
|
|
<div class="ai-chat__operate p-24" style="padding: 24px"> |
|
|
@ -91,7 +108,7 @@ |
|
|
:visible="popoverVisible" |
|
|
:visible="popoverVisible" |
|
|
placement="top" |
|
|
placement="top" |
|
|
:width="is_large?'400px':'860px'" |
|
|
:width="is_large?'400px':'860px'" |
|
|
popper-style="max-width:860px;z-index:99999;margin-left:35px" |
|
|
popper-style="max-width:860px;z-index:2000;margin-left:35px" |
|
|
:show-arrow=false |
|
|
:show-arrow=false |
|
|
:offset=1 |
|
|
:offset=1 |
|
|
> |
|
|
> |
|
|
@ -161,7 +178,7 @@ |
|
|
<img v-show="!isDisabledChart && !loading" src="@/assets/aichat/icon_send_colorful.svg" alt="" /> |
|
|
<img v-show="!isDisabledChart && !loading" src="@/assets/aichat/icon_send_colorful.svg" alt="" /> |
|
|
</el-button> |
|
|
</el-button> |
|
|
<el-popover |
|
|
<el-popover |
|
|
popper-style="z-index:99999;width:300px" |
|
|
popper-style="z-index:2000;width:300px" |
|
|
title="自动审批配置" |
|
|
title="自动审批配置" |
|
|
placement="left-end" |
|
|
placement="left-end" |
|
|
trigger="click" |
|
|
trigger="click" |
|
|
@ -199,7 +216,7 @@ |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
<!-- 文件导入对话框 --> |
|
|
<!-- 文件导入对话框 --> |
|
|
<el-dialog :z-index="99999" title="文件导入" v-model="upload.open" width="400px" append-to-body> |
|
|
<el-dialog :z-index="2000" title="文件导入" v-model="upload.open" width="400px" append-to-body> |
|
|
<el-upload v-if="upload.open" |
|
|
<el-upload v-if="upload.open" |
|
|
ref="uploadRef" |
|
|
ref="uploadRef" |
|
|
:headers="upload.headers" |
|
|
:headers="upload.headers" |
|
|
@ -233,7 +250,7 @@ import OperationButton from './OperationButton.vue' |
|
|
import MdRenderer from '@/views/aichat/MdRenderer.vue' |
|
|
import MdRenderer from '@/views/aichat/MdRenderer.vue' |
|
|
import fullscreenG6 from '@/views/aichat/fullscreenG6.vue' |
|
|
import fullscreenG6 from '@/views/aichat/fullscreenG6.vue' |
|
|
import {getToken} from "@/utils/auth.js"; |
|
|
import {getToken} from "@/utils/auth.js"; |
|
|
import {addChat, DeleteChatList, postChatMessage, updateChatProcessData} from "@/api/aichat/aichat.js" |
|
|
import {addChat, cancelJob, DeleteChatList, postChatMessage, updateChatProcessData} from "@/api/aichat/aichat.js" |
|
|
import cache from "@/plugins/cache.js"; |
|
|
import cache from "@/plugins/cache.js"; |
|
|
import Cookies from "js-cookie"; |
|
|
import Cookies from "js-cookie"; |
|
|
import Interrupt from "@/views/aichat/Interrupt.vue"; |
|
|
import Interrupt from "@/views/aichat/Interrupt.vue"; |
|
|
@ -279,8 +296,8 @@ const inputValue = ref('') |
|
|
const currentQuestion = ref({}) |
|
|
const currentQuestion = ref({}) |
|
|
const chartOpenId = ref('') |
|
|
const chartOpenId = ref('') |
|
|
const chatList = ref([]) |
|
|
const chatList = ref([]) |
|
|
const answerList = ref([]) |
|
|
|
|
|
const controller = ref(null) |
|
|
const controller = ref(null) |
|
|
|
|
|
const currentChatData = ref({}) |
|
|
const autoProcess = ref({ |
|
|
const autoProcess = ref({ |
|
|
checkAll: false, |
|
|
checkAll: false, |
|
|
isIndeterminate: false, |
|
|
isIndeterminate: false, |
|
|
@ -290,6 +307,9 @@ const autoProcess = ref({ |
|
|
const popoverVisible = ref(false) |
|
|
const popoverVisible = ref(false) |
|
|
const currentMachine = ref([]) |
|
|
const currentMachine = ref([]) |
|
|
const currentFiles = ref([]) |
|
|
const currentFiles = ref([]) |
|
|
|
|
|
const currentError = ref('') |
|
|
|
|
|
const currentJobId = ref('') |
|
|
|
|
|
const lastQuestion = ref({}) |
|
|
const upload = reactive({ |
|
|
const upload = reactive({ |
|
|
// 是否显示弹出层(用户导入) |
|
|
// 是否显示弹出层(用户导入) |
|
|
open: false, |
|
|
open: false, |
|
|
@ -415,24 +435,30 @@ watch( |
|
|
|
|
|
|
|
|
watch(() => props.cookieSessionId, value => upload.data = {sessionId:value}) |
|
|
watch(() => props.cookieSessionId, value => upload.data = {sessionId:value}) |
|
|
|
|
|
|
|
|
function regenerationChart(index){ |
|
|
// function regenerationChart(index){ |
|
|
let question = chatList.value[index - 1] |
|
|
// |
|
|
let chat = { |
|
|
// if (currentQuestion.value.query){ |
|
|
"chatId":uuidv4(), |
|
|
// sendChatMessage(currentQuestion.value) |
|
|
"type":"question", |
|
|
// }else{ |
|
|
"content":question.content, |
|
|
// // let question = chatList.value[index - 1] |
|
|
"time": formatDate(new Date()), |
|
|
// // let chat = { |
|
|
"file": question.file} |
|
|
// // "chatId":uuidv4(), |
|
|
chatList.value.push(chat) |
|
|
// // "type":"question", |
|
|
let data = { |
|
|
// // "content":question.content, |
|
|
"query": chat.content, |
|
|
// // "time": formatDate(new Date()), |
|
|
"user_id": cache.local.get("username"), |
|
|
// // "file": question.file} |
|
|
"robot": currentMachine.value.length>0?currentMachine.value[0]:"", |
|
|
// // chatList.value.push(chat) |
|
|
"session_id": Cookies.get("chatSessionId"), |
|
|
// // let data = { |
|
|
"doc": chat.file, |
|
|
// // "query": chat.content, |
|
|
} |
|
|
// // "user_id": cache.local.get("username"), |
|
|
sendChatMessage(data) |
|
|
// // "robot": currentMachine.value.length>0?currentMachine.value[0]:"", |
|
|
} |
|
|
// // "session_id": Cookies.get("chatSessionId"), |
|
|
|
|
|
// // "doc": chat.file, |
|
|
|
|
|
// // } |
|
|
|
|
|
// sendChatMessage(data) |
|
|
|
|
|
// } |
|
|
|
|
|
// |
|
|
|
|
|
// } |
|
|
|
|
|
|
|
|
function changeThumb(index,chat){ |
|
|
function changeThumb(index,chat){ |
|
|
chatList.value[index].operate = chat.operate |
|
|
chatList.value[index].operate = chat.operate |
|
|
@ -503,6 +529,13 @@ function downloadFile(file,bucket,sessionId){ |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
function processAuth(data){ |
|
|
function processAuth(data){ |
|
|
|
|
|
for (let i = 0; i < data.interrupt.block.length; i++) { |
|
|
|
|
|
for (let key in data.formData){ |
|
|
|
|
|
if (key && key === data.interrupt.block[i].name){ |
|
|
|
|
|
data.interrupt.block[i].default_value = data.formData[key] |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
let updateData = { |
|
|
let updateData = { |
|
|
chatId: data.chatId, |
|
|
chatId: data.chatId, |
|
|
action: data.action, |
|
|
action: data.action, |
|
|
@ -517,10 +550,15 @@ function processAuth(data){ |
|
|
"resume": true, |
|
|
"resume": true, |
|
|
"block": data.interrupt.block |
|
|
"block": data.interrupt.block |
|
|
} |
|
|
} |
|
|
|
|
|
currentChatData.value = reqData |
|
|
sendChatMessage(reqData) |
|
|
sendChatMessage(reqData) |
|
|
}) |
|
|
}) |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
function refreshModal(){ |
|
|
|
|
|
sendChatMessage(currentChatData.value) |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
async function sendChatHandle(event) { |
|
|
async function sendChatHandle(event) { |
|
|
if (!event.ctrlKey) { |
|
|
if (!event.ctrlKey) { |
|
|
// 如果没有按下组合键ctrl,则会阻止默认事件 |
|
|
// 如果没有按下组合键ctrl,则会阻止默认事件 |
|
|
@ -570,8 +608,11 @@ function sendChatMessage(data){ |
|
|
controller.value = new AbortController() |
|
|
controller.value = new AbortController() |
|
|
postChatMessage(data,{signal:controller.value.signal}).then(res=>{ |
|
|
postChatMessage(data,{signal:controller.value.signal}).then(res=>{ |
|
|
if (res.status !== 200){ |
|
|
if (res.status !== 200){ |
|
|
chatList.value.push({"chatId":uuidv4(),"type":"answer","content":[{"type":"text","content":"服务异常,错误码:"+res.status}],"isEnd":true,"isStop":false,"sessionId":chatList.value[0].sessionId,"sessionName":chatList.value[0].sessionName,"operate":'',"thumbDownReason":''}) |
|
|
// chatList.value.push({"chatId":uuidv4(),"type":"answer","content":[{"type":"text","content":"服务异常,错误码:"+res.status}],"isEnd":true,"isStop":false,"sessionId":chatList.value[0].sessionId,"sessionName":chatList.value[0].sessionName,"operate":'',"thumbDownReason":''}) |
|
|
|
|
|
currentError.value = "服务异常,错误码:"+res.status +",请联系管理员!" |
|
|
}else { |
|
|
}else { |
|
|
|
|
|
currentError.value = '' |
|
|
|
|
|
currentChatData.value = {} |
|
|
currentFiles.value = [] |
|
|
currentFiles.value = [] |
|
|
chatList.value.push({"chatId":uuidv4(),"type":"answer","content":[],"isEnd":false,"isStop":false,"sessionId":chatList.value[0].sessionId,"sessionName":chatList.value[0].sessionName, "operate":'',"thumbDownReason":''}) |
|
|
chatList.value.push({"chatId":uuidv4(),"type":"answer","content":[],"isEnd":false,"isStop":false,"sessionId":chatList.value[0].sessionId,"sessionName":chatList.value[0].sessionName, "operate":'',"thumbDownReason":''}) |
|
|
const reader = res.body.getReader() |
|
|
const reader = res.body.getReader() |
|
|
@ -579,7 +620,7 @@ function sendChatMessage(data){ |
|
|
reader.read().then(write).then(()=> { |
|
|
reader.read().then(write).then(()=> { |
|
|
let answer = JSON.parse(JSON.stringify(chatList.value[chatList.value.length - 1])) |
|
|
let answer = JSON.parse(JSON.stringify(chatList.value[chatList.value.length - 1])) |
|
|
answer.content = JSON.stringify(answer.content) |
|
|
answer.content = JSON.stringify(answer.content) |
|
|
answer.interrupt = JSON.stringify(answer.interrupt)? answer.interrupt: null |
|
|
answer.interrupt = answer.interrupt ? JSON.stringify(answer.interrupt): null |
|
|
answer.checkpointer = JSON.stringify(answer.checkpointer) |
|
|
answer.checkpointer = JSON.stringify(answer.checkpointer) |
|
|
addChat(answer) |
|
|
addChat(answer) |
|
|
}).then(()=>{ |
|
|
}).then(()=>{ |
|
|
@ -631,7 +672,7 @@ function sendChatMessage(data){ |
|
|
}) |
|
|
}) |
|
|
} |
|
|
} |
|
|
}).catch((e) => { |
|
|
}).catch((e) => { |
|
|
chatList.value.push({"chatId":uuidv4(),"type":"answer","content":[{"type":"text","content":"服务异常"}],"isEnd":true,"isStop":false,"sessionId":chatList.value[0].sessionId,"sessionName":chatList.value[0].sessionName,"operate":"","thumbDownReason":""}) |
|
|
currentError.value = "服务异常,请联系系统管理员!" |
|
|
}) |
|
|
}) |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
@ -653,13 +694,11 @@ watch( |
|
|
const getWrite = (reader) => { |
|
|
const getWrite = (reader) => { |
|
|
// 修复点1:将tempResult改为局部变量,避免递归状态污染 |
|
|
// 修复点1:将tempResult改为局部变量,避免递归状态污染 |
|
|
let tempResult = ''; |
|
|
let tempResult = ''; |
|
|
|
|
|
|
|
|
const write_stream = ({ done, value }) => { |
|
|
const write_stream = ({ done, value }) => { |
|
|
if (done) return; |
|
|
if (done) return; |
|
|
const decoder = new TextDecoder('utf-8'); |
|
|
const decoder = new TextDecoder('utf-8'); |
|
|
let str = decoder.decode(value, { stream: true }); |
|
|
let str = decoder.decode(value, { stream: true }); |
|
|
tempResult += str; |
|
|
tempResult += str; |
|
|
|
|
|
|
|
|
// 修复点2:使用非贪婪匹配确保精确分块 |
|
|
// 修复点2:使用非贪婪匹配确保精确分块 |
|
|
const split = tempResult.match(/data:.*?}\r\n/g); |
|
|
const split = tempResult.match(/data:.*?}\r\n/g); |
|
|
|
|
|
|
|
|
@ -684,6 +723,7 @@ const getWrite = (reader) => { |
|
|
}; |
|
|
}; |
|
|
|
|
|
|
|
|
const processChunk = (chunk) => { |
|
|
const processChunk = (chunk) => { |
|
|
|
|
|
currentJobId.value = chunk.job_id; |
|
|
const lastMsg = chatList.value[chatList.value.length - 1]; |
|
|
const lastMsg = chatList.value[chatList.value.length - 1]; |
|
|
// 修复点5:统一处理所有类型的数据块 |
|
|
// 修复点5:统一处理所有类型的数据块 |
|
|
if (chunk.docs?.length) { |
|
|
if (chunk.docs?.length) { |
|
|
@ -700,12 +740,13 @@ const getWrite = (reader) => { |
|
|
// 修复点6:纯文本处理增加防重复校验 |
|
|
// 修复点6:纯文本处理增加防重复校验 |
|
|
// const text = chunk.choices[0].delta.content.replace(/\n/g, "\n\n"); |
|
|
// const text = chunk.choices[0].delta.content.replace(/\n/g, "\n\n"); |
|
|
// 智能换行处理 |
|
|
// 智能换行处理 |
|
|
const content = chunk.choices[0].delta.content; |
|
|
const text = chunk.choices[0].delta.content; |
|
|
const isNewParagraph = content.startsWith('\n\n'); |
|
|
// const isNewParagraph = content.startsWith('\n\n'); |
|
|
// 仅当需要时添加换行 |
|
|
// 仅当需要时添加换行 |
|
|
const text = isNewParagraph |
|
|
// const text = content; |
|
|
? content.replace(/\n{2,}/g, '\n\n') |
|
|
// const text = isNewParagraph |
|
|
: content.replace(/\n/g, ' '); |
|
|
// ? content.replace(/\n{2,}/g, '\n\n') |
|
|
|
|
|
// : content.replace(/\n/g, '\n\n'); |
|
|
const lastContent = lastMsg.content[lastMsg.content.length - 1]; |
|
|
const lastContent = lastMsg.content[lastMsg.content.length - 1]; |
|
|
if (lastContent?.type === "text") { |
|
|
if (lastContent?.type === "text") { |
|
|
lastContent.content += text; |
|
|
lastContent.content += text; |
|
|
@ -719,6 +760,7 @@ const getWrite = (reader) => { |
|
|
// 修复点7:统一处理结束标志 |
|
|
// 修复点7:统一处理结束标志 |
|
|
if (chunk.checkpointer) { |
|
|
if (chunk.checkpointer) { |
|
|
lastMsg.checkpointer = chunk.checkpointer |
|
|
lastMsg.checkpointer = chunk.checkpointer |
|
|
|
|
|
currentJobId.value = '' |
|
|
lastMsg.isEnd = true; |
|
|
lastMsg.isEnd = true; |
|
|
lastMsg.time = formatDate(new Date()); |
|
|
lastMsg.time = formatDate(new Date()); |
|
|
} |
|
|
} |
|
|
@ -729,6 +771,9 @@ const getWrite = (reader) => { |
|
|
}; |
|
|
}; |
|
|
|
|
|
|
|
|
const stopChat = (index) => { |
|
|
const stopChat = (index) => { |
|
|
|
|
|
if (currentJobId.value !== ''){ |
|
|
|
|
|
cancelJob({job_id:currentJobId.value}) |
|
|
|
|
|
} |
|
|
chatList.value[index].isStop = true |
|
|
chatList.value[index].isStop = true |
|
|
if (controller.value !== null){ |
|
|
if (controller.value !== null){ |
|
|
controller.value.abort() |
|
|
controller.value.abort() |
|
|
@ -736,11 +781,12 @@ const stopChat = (index) => { |
|
|
let answer = JSON.parse(JSON.stringify(chatList.value[index])) |
|
|
let answer = JSON.parse(JSON.stringify(chatList.value[index])) |
|
|
answer.content = JSON.stringify(answer.content) |
|
|
answer.content = JSON.stringify(answer.content) |
|
|
addChat(answer) |
|
|
addChat(answer) |
|
|
|
|
|
currentJobId.value = '' |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
const startChat = (index) => { |
|
|
// const startChat = (index) => { |
|
|
regenerationChart(index) |
|
|
// regenerationChart(index) |
|
|
} |
|
|
// } |
|
|
|
|
|
|
|
|
defineExpose({ |
|
|
defineExpose({ |
|
|
setScrollBottom |
|
|
setScrollBottom |
|
|
@ -945,5 +991,33 @@ defineExpose({ |
|
|
:deep(.el-divider__line) { |
|
|
:deep(.el-divider__line) { |
|
|
border-top-color: var(--divider-border-color) !important; |
|
|
border-top-color: var(--divider-border-color) !important; |
|
|
} |
|
|
} |
|
|
|
|
|
/* 定义旋转动画 */ |
|
|
|
|
|
@keyframes rotate { |
|
|
|
|
|
0% { |
|
|
|
|
|
transform: rotate(0deg); |
|
|
|
|
|
} |
|
|
|
|
|
100% { |
|
|
|
|
|
transform: rotate(360deg); |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
/* 应用动画到图标 */ |
|
|
|
|
|
//.ri-loader-2-line { |
|
|
|
|
|
// display: inline-block; /* 确保 transform 生效 */ |
|
|
|
|
|
// animation: rotate 1s linear infinite; /* 1秒一圈,匀速旋转,无限循环 */ |
|
|
|
|
|
// transform-origin: 50% 50%; |
|
|
|
|
|
//} |
|
|
|
|
|
/* 可选:添加渐隐效果 */ |
|
|
|
|
|
//.ri-loader-2-line.fade { |
|
|
|
|
|
// opacity: 0.7; |
|
|
|
|
|
// transition: opacity 0.3s; |
|
|
|
|
|
//} |
|
|
|
|
|
.loading-icon { |
|
|
|
|
|
display: inline-block; /* 确保 transform 生效 */ |
|
|
|
|
|
animation: rotate 1s linear infinite; /* 1秒一圈,匀速旋转,无限循环 */ |
|
|
|
|
|
transform-origin: 50% 50%; |
|
|
|
|
|
color: #409EFF; /* 修改颜色 */ |
|
|
|
|
|
//animation: rotate 1s cubic-bezier(0.68, -0.55, 0.27, 1.55) infinite; |
|
|
|
|
|
/* 使用非匀速旋转增加动感 */ |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
</style> |
|
|
</style> |
|
|
|