feat: 添加员工门户项目及相关后端改造

- 新增 hzhub-portal-employee 员工门户前端项目(基于 Vue3 + Element Plus)
- 后端登录接口增加返回 nickName 字段
- 移除 KnowledgeInfoController 的 @SaCheckPermission 注解
- 删除 hzhub-portal-company 旧门户项目
- 更新项目文档和架构说明
- 添加后台运行管理脚本(start-all.sh / status-all.sh / stop-all.sh)
- 更新 docker-compose 配置

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
大壮
2026-04-13 03:47:33 +00:00
parent 4e82f8e1e2
commit 278e507e8a
1310 changed files with 7243 additions and 1248 deletions

View File

@@ -0,0 +1,250 @@
<!-- ChatWidget AI对话组件 -->
<script setup lang="ts">
import type { FilesCardProps } from 'vue-element-plus-x/types/FilesCard';
import { Sender } from 'vue-element-plus-x';
import { getKnowledgeList } from '@/api/chat';
import ModelSelect from '@/components/ModelSelect/index.vue';
import { useUserStore } from '@/stores';
import { useChatStore } from '@/stores/modules/chat';
import { useFilesStore } from '@/stores/modules/files';
import { useSessionStore } from '@/stores/modules/session';
const userStore = useUserStore();
const sessionStore = useSessionStore();
const filesStore = useFilesStore();
const chatStore = useChatStore();
const senderValue = ref('');
const senderRef = ref<InstanceType<typeof Sender> | null>(null);
const isWidgetExpanded = ref(true);
// 知识库列表
const knowledgeList = ref<any[]>([]);
const selectedKnowledgeId = ref<string>('');
const selectedKnowledgeName = ref<string>('知识库');
// 推理开关
const isReasoningEnabled = ref(false);
// 当前会话ID使用 computed 避免 v-model 绑定可选链)
const currentSessionId = computed({
get: () => sessionStore.currentSession?.id || '',
set: (value: string) => {
const session = sessionStore.sessionList.find(s => s.id === value);
if (session) {
sessionStore.setCurrentSession(session);
}
}
});
// 加载知识库列表
async function loadKnowledgeList() {
try {
const response = await getKnowledgeList();
if (response?.rows && Array.isArray(response.rows)) {
knowledgeList.value = response.rows.map((item: any) => ({
id: item.id,
name: item.name,
icon: 'Document',
}));
}
}
catch {
// 静默失败,知识库列表为空即可,不影响其他功能
}
}
// 选择知识库
function insertKnowledgeTag(knowledgeId: string) {
const knowledge = knowledgeList.value.find(k => k.id === knowledgeId);
if (knowledge) {
selectedKnowledgeId.value = knowledgeId;
selectedKnowledgeName.value = knowledge.name;
chatStore.setKnowledgeId(knowledgeId);
}
}
// 发送消息
async function handleSend() {
const messageContent = senderValue.value;
senderValue.value = '';
await sessionStore.createSessionList({
userId: userStore.userInfo?.userId as number,
sessionContent: messageContent,
sessionTitle: messageContent.slice(0, 10),
remark: messageContent.slice(0, 10),
});
}
// 切换展开状态
function toggleWidget() {
isWidgetExpanded.value = !isWidgetExpanded.value;
}
onMounted(() => {
loadKnowledgeList();
});
</script>
<template>
<div class="chat-widget-card" :class="{ 'widget-collapsed': !isWidgetExpanded }">
<div class="card-header">
<div class="header-left">
<el-icon class="header-icon" color="#1d5af3">
<ChatLineRound />
</el-icon>
<h3 class="card-title">AI 对话助手</h3>
</div>
<el-button
class="toggle-btn"
:icon="isWidgetExpanded ? 'ArrowDown' : 'ArrowUp'"
size="small"
circle
@click="toggleWidget"
/>
</div>
<div v-if="isWidgetExpanded" class="card-body">
<!-- 会话选择器 -->
<div class="session-selector">
<el-select
v-model="currentSessionId"
placeholder="选择会话"
size="small"
style="width: 100%"
>
<el-option
v-for="session in sessionStore.sessionList"
:key="session.id"
:label="session.sessionTitle"
:value="session.id"
/>
</el-select>
</div>
<!-- 消息输入区 -->
<div class="message-input-wrapper">
<Sender
ref="senderRef"
v-model="senderValue"
class="chat-widget-sender"
:auto-size="{ maxRows: 4, minRows: 2 }"
variant="updown"
clearable
allow-speech
@submit="handleSend"
>
<template #header>
<div class="sender-header">
<ModelSelect />
<el-dropdown trigger="click" @command="insertKnowledgeTag">
<el-button size="small" type="primary" plain>
<el-icon><FolderOpened /></el-icon>
{{ selectedKnowledgeName }}
</el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item
v-for="item in knowledgeList"
:key="item.id"
:command="item.id"
>
{{ item.name }}
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</template>
</Sender>
</div>
<!-- 提示信息 -->
<div class="widget-footer">
<el-button size="small" text @click="$router.push('/chat')">
<el-icon><FullScreen /></el-icon>
打开完整对话界面
</el-button>
</div>
</div>
</div>
</template>
<style scoped lang="scss">
.chat-widget-card {
background-color: #ffffff;
border-radius: var(--radius-lg);
padding: 20px;
border: 1px solid var(--color-border);
transition: all var(--transition);
.card-header {
display: flex;
gap: 12px;
align-items: center;
justify-content: space-between;
margin-bottom: 16px;
.header-left {
display: flex;
gap: 12px;
align-items: center;
.header-icon {
width: 24px;
height: 24px;
font-size: 24px;
}
.card-title {
font-size: 16px;
font-weight: 600;
color: var(--color-text-primary);
margin: 0;
}
}
.toggle-btn {
flex-shrink: 0;
}
}
.card-body {
display: flex;
flex-direction: column;
gap: 12px;
.session-selector {
margin-bottom: 8px;
}
.message-input-wrapper {
.chat-widget-sender {
:deep(.el-textarea__inner) {
font-size: 14px;
}
}
.sender-header {
display: flex;
gap: 8px;
align-items: center;
padding: 8px;
}
}
.widget-footer {
display: flex;
justify-content: center;
padding-top: 8px;
border-top: 1px solid var(--color-border-light);
}
}
&.widget-collapsed {
.card-body {
display: none;
}
}
}
</style>