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

2
.gitignore vendored
View File

@@ -27,3 +27,5 @@ coverage
*.njsproj
*.sln
*.sw?
.assistant-rules.md

View File

@@ -1,13 +1,13 @@
<template>
<!-- <RouteCache /> -->
<router-view ></router-view>
<router-view></router-view>
<div id="loading" v-if="loading" v-loading="true"></div>
</template>
<script setup lang="ts">
import RouteCache from '@/components/RouteCache.vue'
import { useRouter } from 'vue-router'
import { computed } from 'vue'
import { computed, provide, nextTick, ref } from 'vue'
import { useGlobalStore } from '@/stores'
const router = useRouter()
const globalStore = useGlobalStore()
@@ -28,6 +28,9 @@
#app {
font-size: 1.6rem;
}
.el-message{
font-size: 1.6rem;
}
#loading {
position: fixed;
z-index: 999999999;

View File

@@ -12,68 +12,8 @@ export interface AgentParamsType {
export const fetchAgentReply = (data: AgentParamsType): Promise<AgentResponse> => {
return request({
url: '/api/ai-design/chat',
method: 'post',
method: 'get',
data,
meta: { responseAll: true }
})
}
// 流式对话
export const fetchAgentReplyStream = async (
data: AgentParamsType,
onMessage: (chunk: string) => void,
onEnd: () => void
) => {
try {
const params = new URLSearchParams({
message: data.message,
threadId: data.threadId,
token: data.token,
configParams: JSON.stringify(data.configParams)
})
const response = await fetch(`/api/ai-design/chat?${params.toString()}`, {
method: 'GET'
})
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
}
const reader = response.body?.getReader()
if (!reader) {
throw new Error('Response body is not readable')
}
const decoder = new TextDecoder()
let buffer = ''
while (true) {
const { done, value } = await reader.read()
if (done) break
buffer += decoder.decode(value, { stream: true })
const lines = buffer.split('\n')
buffer = lines.pop() || '' // 保留不完整的行
for (const line of lines) {
console.log('line---', line)
const trimmedLine = line.trim()
if (trimmedLine.startsWith('data: ')) {
const chunk = trimmedLine.slice(6)
if (chunk === '[DONE]') {
onEnd()
return
} else {
onMessage(chunk)
}
}
}
}
onEnd()
} catch (error) {
console.error('Stream error:', error)
onEnd()
}
}

View File

@@ -11,6 +11,7 @@ export const SendVerificationCode = (params) => {
return request({
url: '/api/user/send-verification-code',
method: 'post',
loading: true,
params
})
}
@@ -28,6 +29,7 @@ export const Register = (data) => {
return request({
url: '/api/user/register',
method: 'post',
loading: true,
data
})
}
@@ -43,6 +45,7 @@ export const Login = (data) => {
return request({
url: '/api/user/login',
method: 'post',
loading: true,
data
})
}

View File

@@ -0,0 +1,3 @@
<svg width="11" height="7" viewBox="0 0 11 7" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1 1L5.04979 5.04979L9.09958 1" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 223 B

View File

@@ -0,0 +1,5 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9.75 9C9.75 8.58579 9.41421 8.25 9 8.25C8.58579 8.25 8.25 8.58579 8.25 9V12C8.25 12.4142 8.58579 12.75 9 12.75C9.41421 12.75 9.75 12.4142 9.75 12V9Z" fill="#0D0D0D"/>
<path d="M9 1.5C4.85786 1.5 1.5 4.85786 1.5 9C1.5 13.1421 4.85786 16.5 9 16.5C13.1421 16.5 16.5 13.1421 16.5 9C16.5 4.85786 13.1421 1.5 9 1.5ZM3 9C3 5.68629 5.68629 3 9 3C12.3137 3 15 5.68629 15 9C15 12.3137 12.3137 15 9 15C5.68629 15 3 12.3137 3 9Z" fill="#0D0D0D"/>
<path d="M9 6.97504C9.47635 6.97504 9.8625 6.58888 9.8625 6.11254C9.8625 5.63619 9.47635 5.25004 9 5.25004C8.52365 5.25004 8.1375 5.63619 8.1375 6.11254C8.1375 6.58888 8.52365 6.97504 9 6.97504Z" fill="#0D0D0D"/>
</svg>

After

Width:  |  Height:  |  Size: 761 B

View File

@@ -0,0 +1,5 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6.6731 1.77832H3.95313C3.1247 1.77832 2.45312 2.44989 2.45312 3.27832V12.7214C2.45312 13.5499 3.1247 14.2214 3.95313 14.2214H6.6731" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
<path d="M13.2461 7.99854L7.18701 7.99853" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
<path d="M10.8088 5.26001L13.5488 8L10.8088 10.74" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 541 B

View File

@@ -0,0 +1,8 @@
<svg width="13" height="14" viewBox="0 0 13 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5.91235 5.97704C7.35576 5.97704 8.52587 4.80693 8.52587 3.36352C8.52587 1.92011 7.35576 0.75 5.91235 0.75C4.46894 0.75 3.29883 1.92011 3.29883 3.36352C3.29883 4.80693 4.46894 5.97704 5.91235 5.97704Z" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
<path d="M0.75 13.1317C0.75 13.1317 0.75 12.9684 0.75 12.903C0.75 10.5835 2.48146 8.65606 4.73562 8.3457" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
<path d="M8.39466 10.3384C9.01713 10.3384 9.52174 9.83378 9.52174 9.21131C9.52174 8.58884 9.01713 8.08423 8.39466 8.08423C7.77219 8.08423 7.26758 8.58884 7.26758 9.21131C7.26758 9.83378 7.77219 10.3384 8.39466 10.3384Z" fill="black"/>
<path d="M11.5138 10.3384C12.1363 10.3384 12.6409 9.83378 12.6409 9.21131C12.6409 8.58884 12.1363 8.08423 11.5138 8.08423C10.8913 8.08423 10.3867 8.58884 10.3867 9.21131C10.3867 9.83378 10.8913 10.3384 11.5138 10.3384Z" fill="black"/>
<path d="M8.39466 13.4583C9.01713 13.4583 9.52174 12.9537 9.52174 12.3312C9.52174 11.7087 9.01713 11.2041 8.39466 11.2041C7.77219 11.2041 7.26758 11.7087 7.26758 12.3312C7.26758 12.9537 7.77219 13.4583 8.39466 13.4583Z" fill="black"/>
<path d="M11.5138 13.4583C12.1363 13.4583 12.6409 12.9537 12.6409 12.3312C12.6409 11.7087 12.1363 11.2041 11.5138 11.2041C10.8913 11.2041 10.3867 11.7087 10.3867 12.3312C10.3867 12.9537 10.8913 13.4583 11.5138 13.4583Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -0,0 +1,4 @@
<svg width="19" height="19" viewBox="0 0 19 19" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.40886 2.76749C8.30078 1.44443 10.2483 1.44443 11.1402 2.76749L11.5451 3.3681C11.9325 3.94273 12.563 4.30679 13.2544 4.35495L13.977 4.4053C15.5687 4.51619 16.5425 6.20275 15.8426 7.63671L15.5249 8.28767C15.221 8.91046 15.221 9.63858 15.5249 10.2614L15.8426 10.9123C16.5425 12.3463 15.5687 14.0328 13.9769 14.1437L13.2544 14.1941C12.563 14.2423 11.9325 14.6063 11.5451 15.1809L11.1402 15.7816C10.2483 17.1046 8.30078 17.1046 7.40886 15.7816L7.00397 15.1809C6.61659 14.6063 5.98602 14.2423 5.29469 14.1941L4.57209 14.1437C2.98033 14.0328 2.00659 12.3463 2.70643 10.9123L3.02413 10.2614C3.32809 9.63858 3.32809 8.91046 3.02413 8.28767L2.70644 7.63671C2.00659 6.20276 2.98033 4.51619 4.57209 4.4053L5.29469 4.35495C5.98602 4.30679 6.61659 3.94273 7.00397 3.3681L7.40886 2.76749Z" stroke="black" stroke-width="1.6875"/>
<circle cx="9.27539" cy="9.27441" r="1.93359" stroke="black" stroke-width="1.6875"/>
</svg>

After

Width:  |  Height:  |  Size: 1014 B

View File

@@ -0,0 +1,34 @@
<svg width="18" height="13" viewBox="0 0 18 13" fill="none" xmlns="http://www.w3.org/2000/svg">
<g filter="url(#filter0_n_692_30137)">
<path d="M14.1922 7.86884C15.1626 6.8473 15.4839 5.58249 15.8809 4.28415C15.9364 4.1025 15.959 3.91214 15.945 3.72254L15.76 1.19999C15.727 0.749411 15.4047 0.372032 14.9647 0.26874L14.2145 0.0926218C14.1812 0.0848104 14.1471 0.0808921 14.113 0.0808993C11.5893 0.0814305 9.05569 -0.0851513 6.6376 0.0893913C5.05504 0.202548 3.62996 1.08793 3.0211 2.56473C2.95266 2.73066 2.74058 3.33271 2.74106 3.46776L2.75196 6.51746C2.75251 6.67298 2.67121 6.81713 2.53792 6.89694L0.958446 7.84263C0.915651 7.86826 0.889546 7.91454 0.889725 7.96447C0.890124 8.07621 1.01361 8.14388 1.10778 8.08398L2.16799 7.40964C2.38037 7.27455 2.66002 7.41962 2.67245 7.67132L2.76518 9.54984L2.77334 11.8342C2.77421 12.0781 2.97261 12.2755 3.21647 12.2753L5.29787 12.2729C5.55742 12.2726 5.75463 12.0403 5.71286 11.784L5.37608 9.71745C5.37352 9.70169 5.37243 9.68568 5.37671 9.67032C5.38393 9.64442 5.39803 9.60614 5.40506 9.56942C5.41219 9.53216 5.42374 9.49297 5.45097 9.46659C5.47163 9.44658 5.49928 9.43537 5.52808 9.43534L9.6298 9.43068C9.89861 9.43037 10.1059 9.66831 10.0687 9.93443L9.81285 11.7635C9.77563 12.0296 9.98292 12.2676 10.2517 12.2673L12.7476 12.2644C12.8771 12.2643 12.9816 12.1592 12.9812 12.0297L12.9706 9.07613C12.9702 8.94553 13.0271 8.82104 13.1296 8.74029C13.4912 8.45529 13.8743 8.20332 14.1922 7.86884Z" fill="#272727"/>
<path d="M16.9689 8.63445C18.729 1.99586 16.9384 0.0927843 14.123 0.0959828L15.0919 6.48998C15.2358 6.43615 15.317 6.46736 15.3396 6.48969C15.7923 7.11527 15.5346 8.53918 15.3492 9.17294C15.0405 11.0824 15.9378 11.9107 16.6669 12.1803C16.8842 12.2607 17.094 12.0939 17.1212 11.8641L17.1937 11.2521C17.2225 11.0093 17.0039 10.7773 16.8926 10.5592C16.6655 10.1145 16.8233 9.16971 16.9689 8.63445Z" fill="#272727"/>
<path d="M0.861304 8.40801C0.764736 8.51498 0.58841 8.47735 0.543626 8.34022L0.408584 7.92673C0.363724 7.78937 0.484462 7.65523 0.625813 7.6854L1.05205 7.77636C1.1934 7.80652 1.24923 7.97834 1.1525 8.08548L0.861304 8.40801Z" fill="#272727"/>
</g>
<path d="M17.043 5.93021C17.0438 5.93021 17.0446 5.93021 17.0777 5.93017C17.1108 5.93013 17.1762 5.93006 17.2733 5.93962" stroke="#E4E5D7" stroke-width="0.187554" stroke-linecap="round"/>
<path d="M17.043 6.30082C17.0438 6.30082 17.0446 6.30081 17.0777 6.30078C17.1108 6.30074 17.1762 6.30067 17.2733 6.31022" stroke="#E4E5D7" stroke-width="0.187554" stroke-linecap="round"/>
<path d="M12.393 1.62356C11.6861 0.960209 10.0414 0.0322616 8.74643 1.6277C8.19193 2.46239 7.80275 4.03743 10.0856 5.88913C10.5515 6.26701 11.4827 6.88377 12.1553 6.60499C12.3639 6.48891 12.6836 6.27077 12.6629 5.67769L12.6103 3.94013" stroke="#E4E5D7" stroke-width="0.281331" stroke-linecap="round"/>
<path d="M15.6617 2.4319C15.8624 2.43177 15.9885 2.62095 15.9892 2.8036C15.9898 2.98623 15.8651 3.17571 15.6643 3.17604C15.5537 3.17616 15.4649 3.10285 15.4109 3.03179C15.3545 2.95765 15.3137 2.8585 15.3132 2.75847C15.3129 2.65183 15.3586 2.56581 15.4295 2.50931C15.4974 2.45537 15.5827 2.43199 15.6617 2.4319Z" stroke="#E4E5D7" stroke-width="0.234443"/>
<path d="M14.4761 2.43312C14.6879 2.43298 14.8265 2.63062 14.8272 2.82823C14.8275 2.94411 14.7707 3.04327 14.6993 3.1106C14.6288 3.17686 14.5316 3.2231 14.4321 3.22321C14.3223 3.22324 14.2397 3.16682 14.1904 3.08579C14.1447 3.01068 14.1283 2.91761 14.1279 2.82902C14.1272 2.63136 14.2643 2.43336 14.4761 2.43312Z" stroke="#E4E5D7" stroke-width="0.234443"/>
<path d="M12.4615 11.4724C12.4612 11.3826 12.5337 11.3098 12.6235 11.3097C12.7133 11.3096 12.7864 11.3823 12.7867 11.4721L12.7888 12.0503C12.7888 12.0762 12.7679 12.0972 12.742 12.0972L12.5106 12.0975C12.4847 12.0975 12.4637 12.0766 12.4636 12.0507L12.4615 11.4724Z" fill="#E4E5D7"/>
<path d="M4.33481 11.6446C4.33436 11.5164 4.43797 11.4123 4.56625 11.4121C4.69452 11.412 4.79888 11.5158 4.79934 11.6441L4.80083 12.06C4.80092 12.0859 4.78 12.1069 4.7541 12.107L4.38336 12.1074C4.35746 12.1074 4.33639 12.0864 4.3363 12.0605L4.33481 11.6446Z" fill="#E4E5D7"/>
<path d="M4.99442 11.3379C4.98887 11.2355 5.06744 11.1481 5.16992 11.1428C5.27239 11.1376 5.35996 11.2164 5.36551 11.3188L5.40304 12.0119C5.40445 12.0378 5.38462 12.0598 5.35876 12.0612L5.08132 12.0755C5.05546 12.0768 5.03336 12.0569 5.03196 12.031L4.99442 11.3379Z" fill="#E4E5D7"/>
<path d="M12.0006 11.7974C12.0003 11.7076 12.0728 11.6347 12.1626 11.6346C12.2524 11.6345 12.3254 11.7072 12.3257 11.797L12.3267 12.0509C12.3267 12.0768 12.3058 12.0978 12.2799 12.0978L12.0485 12.0981C12.0226 12.0981 12.0016 12.0772 12.0015 12.0513L12.0006 11.7974Z" fill="#E4E5D7"/>
<defs>
<filter id="filter0_n_692_30137" x="0.398438" y="0.00955702" width="17.2695" height="12.2656" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feTurbulence type="fractalNoise" baseFrequency="21.327169418334961 21.327169418334961" stitchTiles="stitch" numOctaves="3" result="noise" seed="4152" />
<feColorMatrix in="noise" type="luminanceToAlpha" result="alphaNoise" />
<feComponentTransfer in="alphaNoise" result="coloredNoise1">
<feFuncA type="discrete" tableValues="1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 "/>
</feComponentTransfer>
<feComposite operator="in" in2="shape" in="coloredNoise1" result="noise1Clipped" />
<feFlood flood-color="#3B3B3B" result="color1Flood" />
<feComposite operator="in" in2="noise1Clipped" in="color1Flood" result="color1" />
<feMerge result="effect1_noise_692_30137">
<feMergeNode in="shape" />
<feMergeNode in="color1" />
</feMerge>
</filter>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 212 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 602 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 167 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

View File

@@ -0,0 +1,64 @@
<template>
<el-dropdown trigger="click" placement="bottom-end">
<div class="title">
<span class="label">{{ title }}</span>
<span class="icon"><svg-icon name="arrow-bottom" size="10" color="#000" /></span>
</div>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item
v-for="item in list"
:key="item.value"
:disabled="item.value === modelValue"
@click="onItemClick(item.value)"
style="--el-font-size-base: 1.4rem; --el-text-color-regular: #000"
>
<span>{{ item.label }}</span>
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</template>
<script setup lang="ts">
import { onMounted, ref, computed, watch, nextTick } from 'vue'
const props = defineProps({
modelValue: {
type: String,
default: ''
},
list: {
type: [Array, Object],
default: () => []
},
icon: {
type: String,
default: ''
}
})
const title = computed(() => {
return props.list.find((v) => v.value === props.modelValue)?.label || '---'
})
const emit = defineEmits(['update:modelValue', 'change'])
const onItemClick = (value: string) => {
emit('update:modelValue', value)
emit('change', value)
}
</script>
<style lang="less" scoped>
.title {
user-select: none;
cursor: pointer;
display: flex;
align-items: center;
font-size: 1.4rem;
color: #000;
> .label {
// min-width: 7rem;
}
> .icon {
margin-left: 1.6rem;
}
}
</style>

View File

@@ -4,7 +4,7 @@ export default {
login: 'Log in',
register: 'Register',
loginTo: 'Log on to <span>FiDA</span>',
LoginTitle: 'A multi-agent canvas for rapid, trend driven design iteration.',
loginTitle: 'A multi-agent canvas for rapid, trend driven design iteration.',
name: 'Name',
email: 'Email',
password: 'Password',

View File

@@ -27,7 +27,7 @@ const i18n = createI18n({
warnHtmlMessage: false,
legacy: false,
globalInjection:true, // 全局模式,可以直接使用 $t
locale: 'ENGLISH',
locale: localStorage.getItem('language') || 'ENGLISH',
messages: messages
})

View File

@@ -1,70 +1,46 @@
// 每一个存储的模块命名规则use开头store结尾
import router from '@/router'
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import { removeLocal, setLocal } from '@/utils/local'
import MyEvent from '@/utils/myEvent'
export const useUserInfoStore = defineStore('userInfo', () => {
const state = ref({
const state = ref({
userInfo: {},
token: '',
generateParams: {
stylist: '',
sex: '',
stylistImage: ''
}
})
// getters
const getUserInfo = computed(() => state.value.userInfo)
})
// actions
const setUserInfo = (data: any) => {
state.value.userInfo = data
}
// getters
const getUserInfo = computed(() => state.value.userInfo)
const setToken = (data: string) => {
state.value.token = data
// setLocal(data, 'token')
}
// actions
const setUserInfo = (data: any) => {
state.value.userInfo = data
}
const getGenerateParams = () => {
return state.value.generateParams
}
const setToken = (data: string) => {
state.value.token = data
// setLocal(data, 'token')
}
const setGenerateParams = (data: any) => {
state.value.generateParams = data
}
const logOut = async () => {
// 处理退出登录的一些逻辑
state.value.token = ''
state.value.userInfo = {}
// removeLocal('token')
// MyEvent.emit('clear-generate-state')
// MyEvent.emit('clear-client-state')
// MyEvent.emit('clearAllCache')
router.push({ name: 'login' })
return ""
}
const resetGenerateParams = () => {
state.value.generateParams = {
stylist: '',
sex: '',
stylistImage: ''
}
}
const logOut = () => {
// 处理退出登录的一些逻辑
return new Promise((resolve) => {
state.value.token = ''
state.value.userInfo = {}
removeLocal('token')
resetGenerateParams()
MyEvent.emit('clear-generate-state')
MyEvent.emit('clear-client-state')
MyEvent.emit('clearAllCache')
resolve('')
})
}
return {
state,
getUserInfo,
setToken,
setUserInfo,
setGenerateParams,
getGenerateParams,
resetGenerateParams,
logOut
}
return {
state,
getUserInfo,
setToken,
setUserInfo,
logOut
}
})

View File

@@ -1,6 +1,7 @@
import axios from 'axios'
import router from '@/router/index'
import { useGlobalStore, useUserInfoStore } from '@/stores'
import { ElMessage } from 'element-plus'
// 扩展 AxiosRequestConfig 接口
declare module 'axios' {
@@ -80,13 +81,7 @@ service.interceptors.response.use(
// 处理异常的情况
// console.log(res)
if (res.code != 200) {
// showToast({
// message: res.errMsg || res.message,
// // type: 'fail',
// duration: 5000,
// position: 'top',
// icon: 'none'
// })
ElMessage.error(res.message)
return Promise.reject(new Error(res.errMsg || res.message || 'error'))
} else {
// 默认只返回data不返回状态码和message
@@ -123,11 +118,7 @@ service.interceptors.response.use(
}
error.config && removePending(error.config)
console.log('err' + error) // for debug
// showToast({
// message: error.message,
// type: 'fail',
// duration: 5000
// })
ElMessage.error(error.message)
}
return Promise.reject(error)
}

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 {