Merge branch 'main' of ssh://18.167.251.121:10002/aidlab/FiDA_Front

This commit is contained in:
X1627315083@163.com
2026-02-24 13:20:08 +08:00
30 changed files with 724 additions and 214 deletions

View File

@@ -77,8 +77,8 @@
})
messageList.value.push(aiMessage)
const threadId = '' //
console.log('token---', params.token, '参数---', params)
// const threadId = '' //
// console.log('token---', params.token, '参数---', params)
try {
const urlParams = new URLSearchParams<AgentParamsType>({
@@ -100,7 +100,10 @@
// 非流式错误响应,使用 text() 读取错误信息
const errorText = await response.text()
console.error('请求错误:', errorText)
throw new Error(`发起对话错误--- ${response.status}: ${errorText}`)
aiMessage.text = '发送失败,请重试'
aiMessage.streaming = false
aiMessage.loading = false
return
}
// 不是流式响应,使用 text()读取错误信息
@@ -112,6 +115,7 @@
} catch (e) {
console.error('非流式响应文本:', text)
}
aiMessage.text = '发送失败,请重试'
aiMessage.streaming = false
aiMessage.loading = false
return
@@ -195,6 +199,7 @@
}
} catch (error) {
console.error('流式传输错误:', error)
aiMessage.text = '发送失败,请重试'
aiMessage.streaming = false
aiMessage.loading = false
} finally {
@@ -202,6 +207,7 @@
}
} catch (error) {
console.error('fetch请求失败:', error)
aiMessage.text = '发送失败,请重试'
aiMessage.streaming = false
aiMessage.loading = false
}

View File

@@ -23,24 +23,7 @@
@input="handleEditorInput"
@paste="handleEditorPaste"
@keypress="handleKeyPress"
>
<!-- <Tag v-if="showReportTag" /> -->
<div
class="editor-tag report-btn flex-center"
v-if="showReportTag"
contenteditable="false"
>
<SvgIcon class="light-icon" name="light" size="16" />
<span>{{ $t('Input.trendingReport') }}</span>
<SvgIcon
class="close-icon"
name="closeTransparent"
size="24"
color="#e6e6e6"
@click="showReportTag = false"
/>
</div>
</div>
></div>
</div>
<div class="operate flex align-center space-between">
<div class="left flex align-center">
@@ -178,6 +161,8 @@
import { computed, ref, watch, nextTick, onMounted } from 'vue'
import { areaList } from '@/utils/area'
import { useI18n } from 'vue-i18n'
import lightIcon from '@/assets/images/light-icon.png'
import closeIcon from '@/assets/images/close-icon.png'
// import Tag from './Tag.vue'
const props = withDefaults(
@@ -202,8 +187,6 @@
fileInputRef.value?.click()
}
// BUG 标签被删除后无法重新出现
// 处理文件选择
const handleFileChange = (event: Event) => {
const input = event.target as HTMLInputElement
@@ -243,21 +226,101 @@
'NordicNoir'
]
// 标签相关固定标签v-show 控制显示)
const showReportTag = ref(false)
const editorRef = ref<HTMLDivElement | null>(null)
const inputValue = ref<string>('')
const toogltReportTag = () => {
console.log(showReportTag.value)
const reportTags = ref([])
const addReportTag = () => {
// create container matching static structure: <div class="editor-tag report-btn flex-center" contenteditable="false">...
const tag = document.createElement('div')
tag.className = 'editor-tag report-btn flex-center'
tag.contentEditable = 'false'
showReportTag.value = !showReportTag.value
const imgLeft = document.createElement('img')
imgLeft.className = 'light-icon'
imgLeft.src = lightIcon as unknown as string
const textSpan = document.createElement('span')
textSpan.innerText = t('Input.trendingReport')
const imgClose = document.createElement('img')
imgClose.className = 'close-icon'
imgClose.src = closeIcon as unknown as string
imgClose.addEventListener('click', (ev) => {
ev.stopPropagation()
// remove tag when close clicked
tag.remove()
const idx = reportTags.value.indexOf(tag)
if (idx > -1) reportTags.value.splice(idx, 1)
})
// assemble
tag.appendChild(imgLeft)
tag.appendChild(textSpan)
tag.appendChild(imgClose)
// Insert tag at the current cursor position
const selection = window.getSelection()
if (selection && selection.rangeCount > 0) {
const range = selection.getRangeAt(0)
range.insertNode(tag)
// Insert a zero-width space text node after the tag so the caret can be placed there
const zwsp = document.createTextNode('\u200B')
if (tag.parentNode) tag.parentNode.insertBefore(zwsp, tag.nextSibling)
// Create a new collapsed range positioned inside the zwsp (after the tag)
const newRange = document.createRange()
newRange.setStart(zwsp, 1)
newRange.collapse(true)
selection.removeAllRanges()
selection.addRange(newRange)
// ensure editor has focus
editorRef.value && (editorRef.value as HTMLElement).focus()
} else if (editorRef.value) {
// If no selection, append directly to editor and place caret after
editorRef.value.appendChild(tag)
const zwsp = document.createTextNode('\u200B')
editorRef.value.appendChild(zwsp)
const sel = window.getSelection()
if (sel) {
const r = document.createRange()
r.setStart(zwsp, 1)
r.collapse(true)
sel.removeAllRanges()
sel.addRange(r)
}
editorRef.value && (editorRef.value as HTMLElement).focus()
}
reportTags.value.push(tag)
}
const toogltReportTag = () => {
// 清理掉已被删除的标签引用(从 DOM 中移除的元素)
reportTags.value = reportTags.value.filter((tag) => tag.parentNode !== null)
if (reportTags.value.length > 0) {
// 移除所有标签及其关联的零宽空格
reportTags.value.forEach((tag) => {
if (tag.nextSibling && tag.nextSibling.nodeType === Node.TEXT_NODE && tag.nextSibling.textContent === '\u200B') {
tag.nextSibling.remove()
}
tag.remove()
})
reportTags.value = []
} else {
// 添加标签
addReportTag()
}
}
const handleEditorInput = () => {
if (!editorRef.value) return
// 提取纯文本(排除标签)
// 提取纯文本(排除插入的report标签)
let text = ''
const walker = document.createTreeWalker(editorRef.value, NodeFilter.SHOW_TEXT, null)
@@ -266,7 +329,7 @@
text += node.textContent
}
// 移除末尾的空格(如果有的话)
// 移除末尾的空格
text = text.replace(/\s+$/, '')
inputValue.value = text
@@ -288,19 +351,58 @@
// editor.style.overflowY = 'auto'
return
} else {
editor.style.height = 'auto'
const maxHeight =
20 * parseFloat(getComputedStyle(document.documentElement).fontSize || '16')
editor.style.height = Math.min(editor.scrollHeight, maxHeight) + 'px'
// editor.style.height = 'auto'
// const maxHeight =
// 20 * parseFloat(getComputedStyle(document.documentElement).fontSize || '16')
// editor.style.height = Math.min(editor.scrollHeight, maxHeight) + 'px'
}
}
}
const handleKeyPress = (e) => {
// 检测回车
if (e.key === 'Enter' && !e.shiftKey) {
if (e.key === 'Enter') {
e.preventDefault()
handleSendAgent()
return
}
if (e.key === 'Backspace') {
const selection = window.getSelection()
if (selection.rangeCount > 0) {
const range = selection.getRangeAt(0)
if (range.collapsed) {
let nodeToDelete = null
const startContainer = range.startContainer
const startOffset = range.startOffset
if (startContainer.nodeType === Node.TEXT_NODE) {
// Cursor at the end of a text node, check next sibling
if (startOffset === startContainer.length) {
nodeToDelete = startContainer.nextSibling
}
} else if (startContainer.nodeType === Node.ELEMENT_NODE) {
// Cursor positioned between child nodes
nodeToDelete = startContainer.childNodes[startOffset]
}
if (
nodeToDelete &&
nodeToDelete.nodeType === Node.ELEMENT_NODE &&
(nodeToDelete as Element).classList &&
((nodeToDelete as Element).classList.contains('editor-tag') ||
(nodeToDelete as Element).classList.contains('report-tag'))
) {
e.preventDefault()
;(nodeToDelete as Element).remove()
// Optional: remove from reportTags if tracking
const index = reportTags.value.indexOf(nodeToDelete)
if (index > -1) {
reportTags.value.splice(index, 1)
}
return
}
}
}
}
}
@@ -400,7 +502,7 @@
border-radius: 2.2rem;
width: 20rem;
background-color: #fff;
border: 0.11rem solid #f6f4ef1a;
border: 1px solid #F6F4EF;
column-gap: 1.2rem;
cursor: pointer;
.c-svg {
@@ -410,14 +512,15 @@
}
.scroll-content {
overflow-y: visible;
display: flex;
flex: 1;
overflow-y: auto;
padding: 3.4rem 1.7rem 1.7rem;
}
.editor {
width: 100%;
min-height: 5rem;
max-height: 20rem;
flex: 1;
border: none;
outline: none;
padding: 0 1.4rem 1.4rem;
@@ -437,41 +540,6 @@
color: #999;
pointer-events: none;
}
// 标签样式
.editor-tag {
width: 21.8rem;
height: 4.4rem;
display: inline-flex;
position: initial;
bottom: initial;
border: 0.11rem solid #0000001a;
font-family: 'GeneralMedium';
font-weight: 500;
font-size: 1.8rem;
column-gap: 0;
span {
margin: 0 0.7rem 0 1.2rem;
}
.c-svg.close-icon {
width: 2.4rem;
height: 2.4rem;
cursor: pointer;
}
}
}
// 标签容器(已废弃,保留兼容性)
.tags-container {
display: inline-flex;
flex-wrap: wrap;
gap: 0.5rem;
padding: 0 1.4rem 0.5rem;
.tag-item {
display: inline-flex;
align-items: center;
}
}
// 图片预览区域样式
@@ -804,4 +872,36 @@
display: none;
}
}
/* 动态添加的编辑器标签样式 */
.assist-input-wrapper .editor .editor-tag {
width: 21.8rem;
height: 4.4rem;
display: inline-flex;
border: 0.11rem solid #0000001a;
font-family: 'GeneralMedium';
font-weight: 500;
font-size: 1.8rem;
column-gap: 0;
margin: 0 0.5rem;
vertical-align: middle;
border-radius: 2.2rem;
span {
margin: 0 0.7rem 0 1.2rem;
}
.light-icon {
width: 1.5rem;
height: 1.9rem;
flex-shrink: 0;
}
.close-icon {
width: 1rem;
height: 1rem;
cursor: pointer;
flex-shrink: 0;
}
}
</style>

View File

@@ -15,12 +15,14 @@
<div class="bottom-view"><router-view></router-view></div>
</div>
</div>
<setting />
</template>
<script setup lang="ts">
import { computed } from 'vue'
import LeftNav from './left-nav.vue'
import TopNav from './top-nav.vue'
import setting from './setting/index.vue'
import { useGlobalStore } from '@/stores'
const globalStore = useGlobalStore()
const loading = computed(() => globalStore.state.loading)

View File

@@ -0,0 +1,56 @@
<template>
<div>
<div class="label">User Name</div>
<div class="value">张三</div>
</div>
<div>
<div class="label">Email</div>
<div class="value">zhangsan@example.com</div>
</div>
<div>
<div class="label">Language</div>
<dropdown-menu v-model="locale" :list="langs" @change="changeLang" />
</div>
<div>
<div class="label">Log out on this device</div>
<button class="logout-btn" @click="logout">Log out</button>
</div>
</template>
<script setup lang="ts">
import { computed, ref, nextTick, inject } from 'vue'
import { useRouter } from 'vue-router'
import dropdownMenu from '@/components/dropdown-menu.vue'
import { useUserInfoStore } from '@/stores'
import { useI18n } from 'vue-i18n'
const router = useRouter()
const { locale } = useI18n()
const userInfoStore = useUserInfoStore()
const langs = ref([
{ label: 'English', value: 'ENGLISH' },
{ label: '中文', value: 'CHINESE_SIMPLIFIED' }
])
const changeLang = (value: string) => {
locale.value = value
localStorage.setItem('language', value)
nextTick(() => {
router.go(0)
})
}
const logout = () => {
userInfoStore.logOut()
}
</script>
<style lang="less" scoped>
.logout-btn {
cursor: pointer;
height: 3.7rem;
width: 9.6rem;
border-radius: 3.7rem;
border: none;
background-color: #ff7a51;
color: #fff;
font-size: 1.4rem;
}
</style>

View File

@@ -0,0 +1,33 @@
<template>
<div>
<div class="label">User Agreement</div>
<button @click="onClickUserAgreement">View</button>
</div>
<div>
<div class="label">Privacy Policy</div>
<button @click="onClickPrivacy">View</button>
</div>
</template>
<script setup lang="ts">
import { computed, ref, onBeforeUnmount, inject } from 'vue'
const onClickUserAgreement = () => {
console.log('onClickUserAgreement')
}
const onClickPrivacy = () => {
console.log('onClickPrivacy')
}
</script>
<style lang="less" scoped>
button {
width: 10rem;
height: 3.73rem;
background-color: transparent;
border: 0.01rem solid #b5b5b5;
color: #000;
font-size: 1.4rem;
border-radius: 3rem;
}
</style>

View File

@@ -0,0 +1,75 @@
<template>
<div>
<div class="label">Region</div>
<dropdown-menu v-model="region" :list="regions" @change="changeRegion" />
</div>
<div>
<div class="label">Role</div>
<dropdown-menu v-model="role" :list="roles" @change="changeRole" />
</div>
<div>
<div class="label">Current Agent Profile</div>
<div class="group">
<span class="icon"><svg-icon name="xiang" size="20" color="#000" /></span>
<dropdown-menu v-model="agent" :list="agents" @change="changeAgent" />
</div>
</div>
<div>
<div class="label">Current Notification Frequency</div>
<div class="value">36 times per hour</div>
</div>
</template>
<script setup lang="ts">
import { computed, ref, onBeforeUnmount, inject } from 'vue'
import dropdownMenu from '@/components/dropdown-menu.vue'
import { useUserInfoStore } from '@/stores'
import { useI18n } from 'vue-i18n'
const { locale } = useI18n()
const region = ref('China')
const regions = ref([
{ label: 'United States', value: 'United States' },
{ label: 'Singapore', value: 'Singapore' },
{ label: 'Australia', value: 'Australia' },
{ label: 'South Korea', value: 'South Korea' },
{ label: 'China', value: 'China' },
{ label: 'Italy', value: 'Italy' },
{ label: 'France', value: 'France' },
{ label: 'Japan', value: 'Japan' },
{ label: 'Canada', value: 'Canada' },
{ label: 'Germany', value: 'Germany' }
])
const changeRegion = (value: string) => {
console.log(value)
}
const role = ref('Designer')
const roles = ref([
{ label: 'Designer', value: 'Designer' },
{ label: 'Student', value: 'Student' },
{ label: 'Teacher', value: 'Teacher' },
{ label: 'Parent', value: 'Parent' }
])
const changeRole = (value: string) => {
console.log(value)
}
const agent = ref('Partner')
const agents = ref([
{ label: 'Partner', value: 'Partner' },
{ label: 'Observer', value: 'Observer' },
{ label: 'Mentor', value: 'Mentor' }
])
const changeAgent = (value: string) => {
console.log(value)
}
</script>
<style lang="less" scoped>
.group {
display: flex;
align-items: center;
justify-content: center;
gap: 1rem;
}
</style>

View File

@@ -0,0 +1,126 @@
<template>
<el-dialog
class="setting"
v-model="showDialog"
align-center
:show-close="false"
width="70rem"
style="border-radius: 2rem; padding: 3.8rem; --el-dialog-padding-primary: 1rem"
>
<template #header="{ close }">
<div class="setting-header">
<div class="title">Setting</div>
<span class="close" @click="close">
<svg-icon name="close" size="10" color="#000" />
</span>
</div>
</template>
<div class="setting-box">
<div class="left">
<div
v-for="v in navs"
:key="v.icon"
:class="{ active: nav === v.icon }"
@click="nav = v.icon"
>
<span class="icon"><svg-icon :name="v.icon" size="18" color="#000" /></span>
<span class="label">{{ v.label }}</span>
</div>
</div>
<div class="view">
<component :is="navs.find((v) => v.icon === nav).component" />
</div>
</div>
</el-dialog>
</template>
<script setup lang="ts">
import General from './General.vue'
import Profile from './Profile.vue'
import LearnMore from './LearnMore.vue'
import MyEvent from '@/utils/myEvent'
import { computed, ref, onBeforeUnmount, shallowRef } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useI18n } from 'vue-i18n'
const { t: $t } = useI18n()
const route = useRoute()
const router = useRouter()
const showDialog = ref(false)
const openSetting = () => {
showDialog.value = true
}
MyEvent.add('openSettingDialog', openSetting)
onBeforeUnmount(() => {
MyEvent.remove('openSettingDialog', openSetting)
})
const nav = ref('setting')
const navs = shallowRef([
{ icon: 'setting', label: 'General', component: General },
{ icon: 'profile', label: 'Profile', component: Profile },
{ icon: 'learn-more', label: 'Learn more', component: LearnMore }
])
</script>
<style lang="less" scoped>
.setting-header {
margin-top: 0.2rem;
display: flex;
align-items: center;
justify-content: space-between;
padding-bottom: 3.3rem;
border-bottom: 0.1rem solid rgba(0, 0, 0, 0.1);
> .title {
font-family: Semibold;
font-size: 1.6rem;
color: #000;
}
}
.setting-box {
width: 100%;
height: 50rem;
display: flex;
> .left {
margin-top: 1.8rem;
> div {
width: 16.4rem;
height: 3.4rem;
display: flex;
align-items: center;
// justify-content: center;
cursor: pointer;
&.active {
background: rgba(0, 0, 0, 0.04);
border-radius: 1rem;
}
> .icon {
margin: 0 1rem;
}
> .label {
color: #000;
font-size: 1.4rem;
}
}
}
> .view {
margin-left: 3.5rem;
flex: 1;
overflow-y: auto;
&::v-deep(> div) {
display: flex;
align-items: center;
justify-content: space-between;
height: 6rem;
border-bottom: 0.1rem solid rgba(0, 0, 0, 0.1);
&:last-child {
border-bottom: none;
}
> .value,
> .label {
font-size: 1.4rem;
color: #000;
}
}
}
}
</style>

View File

@@ -12,13 +12,38 @@
<span class="link"></span>
<span class="icon" @click="onShop"><svg-icon name="shop" size="21" /></span>
</div>
<img class="pic" src="@/assets/images/pic.jpg" />
<el-popover
placement="bottom-end"
popper-style="width:auto; min-width: 22rem; padding: 0.8rem; border-radius: 1.4rem;"
>
<template #reference>
<img class="pic" src="@/assets/images/pic.jpg" />
</template>
<div class="menu-box">
<div>
<span class="label">{{ email }}</span>
</div>
<p></p>
<div class="btn" @click="onSetting">
<span class="icon"><svg-icon name="setting" size="18" /></span>
<span class="label">Settings</span>
</div>
<div class="btn" @click="onLogout">
<span class="icon"><svg-icon name="logout" size="18" /></span>
<span class="label">Log out</span>
</div>
</div>
</el-popover>
</div>
</template>
<script setup lang="ts">
import MyEvent from '@/utils/myEvent'
import { computed, ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useUserInfoStore } from '@/stores'
const userInfoStore = useUserInfoStore()
const email = computed(() => userInfoStore.state.userInfo.email || '------')
const route = useRoute()
const topNavStyle = computed(() => route.meta.topNavStyle)
const router = useRouter()
@@ -37,6 +62,12 @@
loading.value = false
}, 1500)
}
const onSetting = () => {
MyEvent.emit('openSettingDialog')
}
const onLogout = () => {
userInfoStore.logOut()
}
</script>
<style lang="less" scoped>
@@ -68,7 +99,7 @@
height: 4.3rem;
margin-right: 1rem;
background-color: rgba(255, 252, 244, 1);
border: 1px solid #FFCF90;
border: 1px solid #ffcf90;
border-radius: 0.8rem;
> .credits {
flex: 1;
@@ -78,7 +109,7 @@
> .link {
height: 100%;
width: 0;
border-right: 1px solid #FFCF90;
border-right: 1px solid #ffcf90;
}
> .icon {
cursor: pointer;
@@ -88,10 +119,46 @@
animation: loading 0.6s linear infinite;
}
}
> .pic {
.pic {
width: 4.65rem;
height: 4.65rem;
border-radius: 50%;
}
}
.menu-box {
user-select: none;
> * {
margin-bottom: 0.4rem;
&:last-child {
margin-bottom: 0;
}
}
> div {
height: 3.7rem;
display: flex;
align-items: center;
// justify-content: center;
&.btn {
cursor: pointer;
}
&.btn:hover {
background-color: rgba(0, 0, 0, 0.06);
}
> .label {
font-size: 1.4rem;
color: #000;
margin-left: 0.8rem;
}
> .icon {
margin-left: 1rem;
--svg-icon-color: #000;
}
}
> p {
width: 100%;
height: 0;
border-bottom: 0.1rem solid #e5e5e5;
}
}
</style>

View File

@@ -102,13 +102,17 @@
verificationCode: code
})
.then((res) => {
console.log(res)
if (res) {
userInfoStore.setToken(res)
userInfoStore.setUserInfo({
email: formData.email
})
router.push({ name: 'mainInput' })
}
})
.catch(() => {
console.warn('error verify code!')
.catch((error) => {
console.warn(error)
})
}
</script>

View File

@@ -111,6 +111,9 @@
.then((res) => {
if (res) {
userInfoStore.setToken(res)
userInfoStore.setUserInfo({
email: formData.email
})
router.push({ name: 'nuic' })
}
})

View File

@@ -25,7 +25,7 @@
<div class="bg-2"></div>
<div class="bg-1"></div>
</div>
<img class="loading-img" :class="{ loading }" src="@/assets/images/nuic/xiang.png" />
<img class="loading-img" :class="{ loading }" src="@/assets/images/nuic/xiang.gif" />
<div class="loading-tip" :class="{ loading }">{{ $t('Nuic.loadingTip') }}</div>
</div>
</template>
@@ -167,7 +167,7 @@
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 14.4rem;
width: 60rem;
height: auto;
opacity: 0;
&.loading {