feat(config): 添加配置文件支持动态标题和提示词

- 新增 public/config.json 配置文件,包含网站标题、副标题及任务提示词
- 在 Header 组件中读取并应用配置中的标题信息-为 Task 组件的搜索建议框引入配置中的提示词列表
- 创建 useConfigStore 管理全局配置状态,并在应用初始化时加载配置
- 更新 main.ts 在应用启动时设置文档标题
- 移除了 Task.vue 中硬编码的提示词数组,改由配置驱动-修复了 agents store 中版本标识监听逻辑,实现存储清理功能
- 添加 MultiLineTooltip 组件用于文本溢出时显示完整内容
-重构 TaskSyllabus 页面布局与样式,提升视觉效果与交互体验
- 引入 Bg
This commit is contained in:
zhaoweijie 2025-11-04 15:26:52 +08:00
parent 00ef22505e
commit 041986f5cd
13 changed files with 363 additions and 154 deletions

1
components.d.ts vendored
View File

@ -18,6 +18,7 @@ declare module 'vue' {
ElCollapseItem: typeof import('element-plus/es')['ElCollapseItem']
ElPopover: typeof import('element-plus/es')['ElPopover']
ElTooltip: typeof import('element-plus/es')['ElTooltip']
MultiLineTooltip: typeof import('./src/components/MultiLineTooltip/index.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
SvgIcon: typeof import('./src/components/SvgIcon/index.vue')['default']

19
public/config.json Normal file
View File

@ -0,0 +1,19 @@
{
"title": "数联网",
"subTitle": "众创智能体",
"centerTitle": "多智能体协同平台",
"taskPromptWords": [
"如何快速筛选慢性肾脏病药物潜在受试者?",
"如何补充“丹芍活血胶囊”不良反应数据?",
"如何快速研发用于战场失血性休克的药物?",
"二维材料的光电性质受哪些关键因素影响?",
"如何通过AI模拟的方法分析材料的微观结构?",
"如何分析获取液态金属热力学参数?",
"如何解决固态电池的成本和寿命难题?",
"如何解决船舶制造中的材料腐蚀难题?",
"如何解决船舶制造中流体模拟和建模优化难题?"
],
"agentRepository": {
"storageVersionIdentifier": "1"
}
}

View File

@ -0,0 +1,93 @@
<template>
<el-tooltip :disabled="!isOverflow" effect="light" placement="top" :content="text">
<div
ref="containerRef"
class="multi-line-ellipsis"
:style="containerStyle"
@mouseenter="handleMouseEnter"
@mouseleave="handleMouseLeave"
>
<slot>
{{ text }}
</slot>
</div>
</el-tooltip>
</template>
<script lang="ts" setup>
import { computed, type HTMLAttributes, nextTick, onMounted, ref } from 'vue'
import { ElTooltip } from 'element-plus'
// props
interface Props {
text?: string
lines?: number
maxWidth?: string | number
}
const props = withDefaults(defineProps<Props>(), {
text: '',
lines: 3,
maxWidth: '100%',
})
const isOverflow = ref(false)
const containerRef = ref<HTMLElement | null>(null)
//
const containerStyle = computed(
() =>
({
maxWidth: props.maxWidth,
display: '-webkit-box',
WebkitBoxOrient: 'vertical',
WebkitLineClamp: props.lines,
overflow: 'hidden',
textOverflow: 'ellipsis',
lineHeight: '1.5',
wordBreak: 'break-all',
}) as HTMLAttributes['style'],
)
//
const checkOverflow = (element: HTMLElement): boolean => {
// 使
if (props.lines === 1) {
return element.scrollWidth > element.clientWidth
}
// 使
else {
return element.scrollHeight > element.clientHeight
}
}
//
const handleMouseEnter = (event: MouseEvent) => {
const element = event.target as HTMLElement
console.log(checkOverflow(element))
isOverflow.value = checkOverflow(element)
}
//
const handleMouseLeave = () => {
isOverflow.value = false
}
//
onMounted(() => {
nextTick(() => {
if (containerRef.value) {
isOverflow.value = checkOverflow(containerRef.value)
}
})
})
</script>
<style scoped>
.multi-line-ellipsis {
cursor: default;
overflow: hidden;
text-overflow: ellipsis;
white-space: normal;
}
</style>

View File

@ -1,15 +1,21 @@
<script setup lang="ts">
import { useConfigStore } from '@/stores'
defineOptions({
name: 'AppHeader'
})
const configStore = useConfigStore()
</script>
<template>
<div class="bg-[var(--color-bg-secondary)] h-[60px] relative pl-[27px] font-[900] text-[24px]">
<div class="absolute left-0 h-full flex items-center">
<img class="w-[36.8px] h-[36.8px] rounded-full mr-[12px]" src="/logo.jpg" alt="logo">
<span class="text-[#9E0000]">数联网</span>众创智能体
<img class="w-[36.8px] h-[36.8px] rounded-full mr-[12px]" src="/logo.jpg" alt="logo" />
<span class="text-[#9E0000]">{{ configStore.config.title }}</span>{{ configStore.config.subTitle }}
</div>
<div class="text-center h-full w-full tracking-[8.5px] flex items-center justify-center">
{{ configStore.config.centerTitle }}
</div>
<div class="text-center h-full w-full tracking-[8.5px] flex items-center justify-center">多智能体协同平台</div>
</div>
</template>

View File

@ -2,7 +2,7 @@
import { ref } from 'vue'
import SvgIcon from '@/components/SvgIcon/index.vue'
import { useAgentsStore } from '@/stores'
import { useAgentsStore, useConfigStore } from '@/stores'
import api from '@/api'
import { changeBriefs } from '@/utils/collaboration_Brief_FrontEnd.ts'
import { ElMessage } from 'element-plus'
@ -13,6 +13,7 @@ const emit = defineEmits<{
}>()
const agentsStore = useAgentsStore()
const configStore = useConfigStore()
const searchValue = ref('')
const triggerOnFocus = ref(true)
@ -41,22 +42,10 @@ async function handleSearch() {
}
}
const restaurants = ref<string[]>([
'如何快速筛选慢性肾脏病药物潜在受试者?',
'如何补充“丹芍活血胶囊”不良反应数据?',
'如何快速研发用于战场失血性休克的药物?',
'二维材料的光电性质受哪些关键因素影响?',
'如何通过AI模拟的方法分析材料的微观结构?',
'如何分析获取液态金属热力学参数?',
'如何解决固态电池的成本和寿命难题?',
'如何解决船舶制造中的材料腐蚀难题?',
'如何解决船舶制造中流体模拟和建模优化难题?',
])
const querySearch = (queryString: string, cb: (v: { value: string }[]) => void) => {
console.log(queryString)
const results = queryString
? restaurants.value.filter(createFilter(queryString))
: restaurants.value
? configStore.config.taskPromptWords.filter(createFilter(queryString))
: configStore.config.taskPromptWords
// call callback function to return suggestions
cb(results.map((item) => ({ value: item })))
}

View File

@ -15,14 +15,19 @@ const md = new MarkdownIt({
html: true,
linkify: true,
typographer: true,
breaks: true,
})
function sanitize(str?: string) {
if (!str) {
return ''
}
const html = md.render(str)
return DOMPurify.sanitize(html)
const cleanStr = str
.replace(/\\n/g, '\n')
.replace(/\n\s*\d+\./g, '\n$&')
const html = md.render(cleanStr)
return html
// return DOMPurify.sanitize(html)
}
interface Data {

View File

@ -0,0 +1,31 @@
<template>
<div class="absolute inset-0 flex items-start gap-[14%]">
<!-- 左侧元素 -->
<div class="flex-1 relative h-full flex justify-center">
<!-- 背景那一根线 -->
<div
class="h-full bg-[var(--color-bg-tertiary)] w-[5px]"
>
<!-- 线底部的小圆球 -->
<div
class="absolute bottom-0 left-1/2 transform -translate-x-1/2 bg-[var(--color-bg-tertiary)] w-[15px] h-[15px] rounded-full"
></div>
</div>
</div>
<!-- 右侧元素 -->
<div class="flex-1 relative h-full flex justify-center">
<!-- 背景那一根线 -->
<div
class="h-full bg-[var(--color-bg-tertiary)] w-[5px]"
>
<!-- 线底部的小圆球 -->
<div
class="absolute bottom-0 left-1/2 transform -translate-x-1/2 bg-[var(--color-bg-tertiary)] w-[15px] h-[15px] rounded-full"
></div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
</script>

View File

@ -1,14 +1,13 @@
<script setup lang="ts">
import SvgIcon from '@/components/SvgIcon/index.vue'
import { getAgentMapIcon } from '@/layout/components/config.ts'
import {
type ConnectArg,
Jsplumb,
type JsplumbConfig,
} from '@/layout/components/Main/TaskTemplate/utils.ts'
import { type ConnectArg, Jsplumb } from '@/layout/components/Main/TaskTemplate/utils.ts'
import { type IRawStepTask, useAgentsStore } from '@/stores'
import { computed } from 'vue'
import { AnchorLocations } from '@jsplumb/browser-ui'
import MultiLineTooltip from '@/components/MultiLineTooltip/index.vue'
import Bg from './Bg.vue'
const emit = defineEmits<{
(el: 'resetAgentRepoLine'): void
@ -51,7 +50,7 @@ function handleCurrentTask(task: IRawStepTask, transparent: boolean): ConnectArg
config: {
type: 'output',
transparent,
}
},
})
}
})
@ -90,124 +89,116 @@ defineExpose({
class="flex-1 w-full overflow-y-auto relative"
@scroll="handleScroll"
>
<div class="flex items-center gap-[14%] w-full px-5 relative" id="task-syllabus">
<!-- 流程 -->
<div
v-if="agentsStore.agentRawPlan.data"
class="w-[43%] min-h-full relative flex justify-center"
>
<!-- 流程内容 -->
<div class="relative min-h-full z-10 flex flex-col items-center card-box pb-[100px]">
<!-- 背景那一根线 -->
<div
class="absolute h-full left-1/2 transform -translate-x-1/2 bg-[var(--color-bg-tertiary)] w-[5px] z-[1]"
>
<!-- 线底部的小圆球 -->
<div
class="absolute bottom-0 left-1/2 transform -translate-x-1/2 bg-[var(--color-bg-tertiary)] w-[15px] h-[15px] rounded-full"
></div>
</div>
<!-- 固定的标题 -->
<div v-show="collaborationProcess.length > 0" class="w-full relative min-h-full" id="task-syllabus">
<Bg />
<div class="w-full flex items-center gap-[14%] mb-[35px]">
<div class="flex-1 flex justify-center">
<div
class="card-item w-[45%] h-[41px] flex justify-center relative z-99 items-center rounded-[20px] bg-[var(--color-bg-tertiary)]"
>
流程
</div>
<el-card
v-for="item in collaborationProcess"
:key="item.Id"
class="card-item w-full h-[158px] overflow-y-auto relative z-99"
:class="agentsStore.currentTask?.StepName === item.StepName ? 'active-card' : ''"
shadow="hover"
:id="`task-syllabus-flow-${item.Id}`"
@click="changeTask(item, true)"
>
<div class="text-[18px] font-bold text-center">{{ item.StepName }}</div>
<div class="h-[1px] w-full bg-[#494B51] my-[8px]"></div>
<div class="text-[14px] line-clamp-3 text-[var(--color-text-secondary)]">
{{ item.TaskContent }}
</div>
<div class="h-[1px] w-full bg-[#494B51] my-[8px]"></div>
<div class="flex items-center gap-2 flex-wrap relative">
<!-- 连接到智能体库的连接点 -->
<div
class="absolute left-[-10px] top-1/2 transform -translate-y-1/2"
:id="`task-syllabus-flow-agents-${item.Id}`"
></div>
<el-tooltip
v-for="agentSelection in item.AgentSelection"
:key="agentSelection"
effect="light"
placement="right"
>
<template #content>
<div class="w-[150px]">
<div class="text-[18px] font-bold">{{ agentSelection }}</div>
<div class="h-[1px] w-full bg-[#494B51] my-[8px]"></div>
<div>
{{
item.TaskProcess.find((i) => i.AgentName === agentSelection)?.Description
}}
</div>
</div>
</template>
<div
class="w-[31px] h-[31px] rounded-full flex items-center justify-center"
:style="{ background: getAgentMapIcon(agentSelection).color }"
>
<svg-icon
:icon-class="getAgentMapIcon(agentSelection).icon"
color="var(--color-text)"
size="24px"
/>
</div>
</el-tooltip>
</div>
</el-card>
</div>
</div>
<!-- 产出 -->
<div
v-if="agentsStore.agentRawPlan.data"
class="w-[43%] h-full relative flex justify-center"
>
<div class="min-h-full w-full relative">
<!-- 产出内容 -->
<div class="min-h-full relative z-10 flex flex-col items-center card-box pb-[100px]">
<!-- 背景那一根线 -->
<div
class="absolute h-full left-1/2 transform -translate-x-1/2 bg-[var(--color-bg-tertiary)] w-[5px] z-[1]"
>
<!-- 线底部的小圆球 -->
<div
class="absolute bottom-0 left-1/2 transform -translate-x-1/2 bg-[var(--color-bg-tertiary)] w-[15px] h-[15px] rounded-full"
></div>
</div>
<!-- 固定的标题 -->
<div
class="card-item w-[45%] h-[41px] flex justify-center items-center rounded-[20px] bg-[var(--color-bg-tertiary)] relative z-99"
>
产出
</div>
<div
v-for="item in collaborationProcess"
:key="item.Id"
class="h-[158px] overflow-y-auto flex items-center w-full card-item relative z-99"
>
<el-card
class="card-item w-full relative"
shadow="hover"
:class="agentsStore.currentTask?.StepName === item.StepName ? 'active-card' : ''"
:id="`task-syllabus-output-object-${item.Id}`"
>
<div class="text-[18px] font-bold text-center">{{ item.OutputObject }}</div>
</el-card>
</div>
<div class="flex-1 flex justify-center">
<div
class="card-item w-[45%] h-[41px] flex justify-center relative z-99 items-center rounded-[20px] bg-[var(--color-bg-tertiary)]"
>
产物
</div>
</div>
</div>
<div
v-for="item in collaborationProcess"
:key="item.Id"
class="card-item w-full flex items-center gap-[14%]"
>
<!-- 流程卡片 -->
<el-card
class="w-[43%] overflow-y-auto relative z-99 task-syllabus-flow-card"
:class="agentsStore.currentTask?.StepName === item.StepName ? 'active-card' : ''"
shadow="hover"
:id="`task-syllabus-flow-${item.Id}`"
@click="changeTask(item, true)"
>
<MultiLineTooltip placement="right" :text="item.StepName" :lines="2">
<div class="text-[18px] font-bold text-center">
{{ item.StepName }}
</div>
</MultiLineTooltip>
<div class="h-[1px] w-full bg-[#494B51] my-[8px]"></div>
<MultiLineTooltip placement="right" :text="item.StepName" :lines="3">
<div
class="text-[14px] text-[var(--color-text-secondary)]"
:title="item.TaskContent"
>
{{ item.TaskContent }}
</div>
</MultiLineTooltip>
<div class="h-[1px] w-full bg-[#494B51] my-[8px]"></div>
<div
class="flex items-center gap-2 overflow-y-auto flex-wrap relative w-full max-h-[72px]"
>
<!-- 连接到智能体库的连接点 -->
<div
class="absolute left-[-10px] top-1/2 transform -translate-y-1/2"
:id="`task-syllabus-flow-agents-${item.Id}`"
></div>
<el-tooltip
v-for="agentSelection in item.AgentSelection"
:key="agentSelection"
effect="light"
placement="right"
>
<template #content>
<div class="w-[150px]">
<div class="text-[18px] font-bold">{{ agentSelection }}</div>
<div class="h-[1px] w-full bg-[#494B51] my-[8px]"></div>
<div>
{{
item.TaskProcess.find((i) => i.AgentName === agentSelection)?.Description
}}
</div>
</div>
</template>
<div
class="w-[31px] h-[31px] rounded-full flex items-center justify-center"
:style="{ background: getAgentMapIcon(agentSelection).color }"
>
<svg-icon
:icon-class="getAgentMapIcon(agentSelection).icon"
color="var(--color-text)"
size="24px"
/>
</div>
</el-tooltip>
</div>
</el-card>
<!-- 产物卡片 -->
<el-card
class="w-[43%] relative"
shadow="hover"
:class="agentsStore.currentTask?.StepName === item.StepName ? 'active-card' : ''"
:id="`task-syllabus-output-object-${item.Id}`"
>
<div class="text-[18px] font-bold text-center">{{ item.OutputObject }}</div>
</el-card>
</div>
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
.task-syllabus-flow-card {
:deep(.el-card__body) {
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
overflow: auto;
}
}
</style>

View File

@ -7,12 +7,17 @@ import './styles/tailwindcss.css'
import 'element-plus/theme-chalk/dark/css-vars.css'
import 'virtual:svg-icons-register'
import { initService } from '@/utils/request.ts'
import { setupStore } from '@/stores'
import { setupStore, useConfigStore } from '@/stores'
const app = createApp(App)
async function init() {
const app = createApp(App)
setupStore(app)
initService()
const configStore = useConfigStore()
await configStore.initConfig()
document.title = configStore.config.centerTitle
app.use(router)
app.mount('#app')
}
initService()
setupStore(app)
app.use(router)
app.mount('#app')
void init()

View File

@ -9,3 +9,4 @@ export function setupStore(app: App<Element>) {
}
export * from './modules/agents.ts'
export * from './modules/config.ts'

View File

@ -5,7 +5,7 @@ import { v4 as uuidv4 } from 'uuid'
import { store } from '../index'
import { useStorage } from '@vueuse/core'
import type { IExecuteRawResponse } from '@/api'
import { useConfigStore } from '@/stores/modules/config.ts'
export interface Agent {
Name: string
@ -17,12 +17,15 @@ export interface Agent {
type HslColorVector = [number, number, number]
export interface IRichText {
template: string;
data: Record<string, {
text: string;
style?: Record<string, string>
color?: HslColorVector
}>;
template: string
data: Record<
string,
{
text: string
style?: Record<string, string>
color?: HslColorVector
}
>
}
export interface TaskProcess {
@ -50,18 +53,45 @@ export interface IRawPlanResponse {
'Collaboration Process'?: IRawStepTask[]
}
export const useAgentsStore = defineStore('counter', () => {
const agents = useStorage<Agent[]>('agents-v1', [])
const storageKey = '$agents' as const
// 清除所有以 storageKey 开头的 localStorage
function clearStorageByVersion() {
Object.keys(localStorage)
.filter((key) => key.startsWith(storageKey))
.forEach((key) => localStorage.removeItem(key))
}
export const useAgentsStore = defineStore('agents', () => {
const configStore = useConfigStore()
const agents = useStorage<Agent[]>(`${storageKey}-repository`, [])
function setAgents(agent: Agent[]) {
agents.value = agent
}
// 任务搜索的内容
const searchValue = useStorage<string>('agent-search-value', '')
const searchValue = useStorage<string>(`${storageKey}-search-value`, '')
function setSearchValue(value: string) {
searchValue.value = value
}
const storageVersionIdentifier = useStorage<string>(`${storageKey}-storage-version-identifier`, '')
// 监听 configStore.config.agentRepository.storageVersionIdentifier 改变
watch(
() => configStore.config.agentRepository.storageVersionIdentifier,
(value) => {
// value与storageVersionIdentifier不一致清除所有storageKey开头的localStorage
if (value !== storageVersionIdentifier.value) {
clearStorageByVersion()
storageVersionIdentifier.value = value
}
},
{
immediate: true,
}
)
// 当前的展示的任务流程
const currentTask = ref<IRawStepTask>()
function setCurrentTask(task: IRawStepTask) {
@ -72,7 +102,7 @@ export const useAgentsStore = defineStore('counter', () => {
function setAgentRawPlan(plan: { data?: IRawPlanResponse; loading?: boolean }) {
if (plan.data) {
plan.data['Collaboration Process'] = plan.data['Collaboration Process']?.map(item => ({
plan.data['Collaboration Process'] = plan.data['Collaboration Process']?.map((item) => ({
...item,
Id: uuidv4(),
}))
@ -97,7 +127,6 @@ export const useAgentsStore = defineStore('counter', () => {
executePlan.value = []
}
return {
agents,
setAgents,

View File

@ -0,0 +1,39 @@
import { defineStore } from 'pinia'
import { readConfig } from '@/utils/readJson.ts'
import { store } from '@/stores'
export interface Config {
title: string
subTitle: string
centerTitle: string
taskPromptWords: string[]
agentRepository: {
storageVersionIdentifier: string
}
}
export const useConfigStore = defineStore('config', () => {
const config = ref<Config>({} as Config)
// 异步调用readConfig
async function initConfig() {
config.value = await readConfig<Config>('config.json')
}
return {
config,
initConfig
}
})
/**
* Pinia Store 使 Pinia store
* 使 Pinia Store
* https://pinia.vuejs.org/core-concepts/outside-component-usage.html#using-a-store-outside-of-a-component
*/
export function useConfigStoreHook() {
return useConfigStore(store)
}

View File

@ -3,7 +3,7 @@ import axios from 'axios'
import qs from 'qs'
import type { Ref } from 'vue'
import { ElNotification } from 'element-plus'
import { ref, onMounted } from 'vue'
import { ref } from 'vue'
// 创建 axios 实例
let service: AxiosInstance