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>
38
CLAUDE.md
@@ -32,10 +32,18 @@ docker-compose down
|
||||
### Backend Development (Spring Boot)
|
||||
|
||||
```bash
|
||||
# Run AI service locally
|
||||
cd hzhub-ai/hzhub-admin
|
||||
# Run AI service locally (foreground)
|
||||
cd hzhub-ai
|
||||
mvn spring-boot:run -Dspring-boot.run.profiles=dev
|
||||
|
||||
# Run AI service locally (background)
|
||||
cd hzhub-ai
|
||||
./start.sh # Start service in background
|
||||
./status.sh # Check service status
|
||||
./logs.sh # View logs
|
||||
./stop.sh # Stop service
|
||||
./restart.sh # Restart service
|
||||
|
||||
# Build all modules
|
||||
cd hzhub-ai
|
||||
mvn clean package
|
||||
@@ -48,20 +56,28 @@ mvn clean package
|
||||
mvn test
|
||||
```
|
||||
|
||||
**💡 Tip:** For background service management, see [SERVICE_MANAGEMENT.md](../SERVICE_MANAGEMENT.md)
|
||||
|
||||
### Frontend Development (Vue 3 + Vben Admin)
|
||||
|
||||
```bash
|
||||
# Admin portal development
|
||||
cd hzhub-admin
|
||||
pnpm install # Install dependencies
|
||||
pnpm dev # Start dev server
|
||||
pnpm dev # Start dev server (foreground)
|
||||
./start.sh # Start dev server (background)
|
||||
./status.sh # Check service status
|
||||
./logs.sh # View logs
|
||||
pnpm build # Build all packages
|
||||
pnpm --filter=@vben/web-antd build:prod # Build admin frontend
|
||||
|
||||
# Company portal development
|
||||
cd hzhub-portal-company
|
||||
# Employee portal development
|
||||
cd hzhub-portal-employee
|
||||
pnpm install
|
||||
pnpm dev
|
||||
pnpm dev # Start dev server (foreground)
|
||||
./start.sh # Start dev server (background)
|
||||
./status.sh # Check service status
|
||||
./logs.sh # View logs
|
||||
|
||||
# Dealer portal development
|
||||
cd hzhub-portal-dealer
|
||||
@@ -69,6 +85,8 @@ pnpm install
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
**💡 Tip:** For background service management (start/stop/restart/status/logs), see [SERVICE_MANAGEMENT.md](../SERVICE_MANAGEMENT.md)
|
||||
|
||||
## Architecture
|
||||
|
||||
### Multi-Service Structure
|
||||
@@ -76,7 +94,7 @@ pnpm dev
|
||||
```
|
||||
┌─────────────────────────────────────────┐
|
||||
│ Frontend Layer │
|
||||
│ hzhub-admin | hzhub-portal-company │
|
||||
│ hzhub-admin | hzhub-portal-employee │
|
||||
│ | hzhub-portal-dealer │
|
||||
└────────────┬────────────────────────────┘
|
||||
│
|
||||
@@ -125,7 +143,7 @@ hzhub-admin/
|
||||
└── package.json # Root monorepo config
|
||||
```
|
||||
|
||||
**Portal applications** (hzhub-portal-company, hzhub-portal-dealer) are Vue 3 apps with:
|
||||
**Portal applications** (hzhub-portal-employee, hzhub-portal-dealer) are Vue 3 apps with:
|
||||
- Composition API (`<script setup>`)
|
||||
- Pinia state management with persistence
|
||||
- Element Plus UI components
|
||||
@@ -282,7 +300,7 @@ Frontend Dockerfiles in each portal directory:
|
||||
| Service | Port | Access |
|
||||
|---------|------|--------|
|
||||
| hzhub-admin (frontend) | 5666 | http://localhost:5666 |
|
||||
| hzhub-portal-company | 5137 | http://localhost:5137 |
|
||||
| hzhub-portal-employee | 5137 | http://localhost:5137 |
|
||||
| hzhub-portal-dealer | 5138 | http://localhost:5138 |
|
||||
| hzhub-ai (backend API) | 6039 | http://localhost:6039 |
|
||||
| MySQL | 3306 | localhost:3306 |
|
||||
@@ -314,7 +332,7 @@ For admin portal:
|
||||
3. Add route in `apps/web-antd/src/router/`
|
||||
4. Add menu configuration
|
||||
|
||||
For portals (company/dealer):
|
||||
For portals (employee/dealer):
|
||||
1. Add API module in `src/api/` with `index.ts` and `types.ts`
|
||||
2. Create page in `src/pages/`
|
||||
3. Define route in `src/routers/modules/`
|
||||
|
||||
46
README.md
@@ -12,9 +12,9 @@ HZHub(汇智中台)是面向企业级市场的业务中台系统,集成 AI
|
||||
┌─────────────────────────────────────────────────────────────────────────┐
|
||||
│ 前端接入层 │
|
||||
├─────────────────┬─────────────────┬─────────────────────────────────────┤
|
||||
│ 管理后台 │ 公司门户 │ 经销商门户 │
|
||||
│ 管理后台 │ 员工门户 │ 经销商门户 │
|
||||
│ (hzhub-admin) │ (hzhub-portal- │ (hzhub-portal-dealer) │
|
||||
│ · 模型管理 │ company) │ · 企业微信H5 │
|
||||
│ · 模型管理 │ employee) │ · 企业微信H5 │
|
||||
│ · 知识库配置 │ · 企业微信H5 │ · 自助开单 │
|
||||
│ · 智能体编排 │ · 审批流程 │ · 自助对账/发货 │
|
||||
│ · 系统管理 │ · 经销商管理 │ · 进销存 │
|
||||
@@ -60,7 +60,7 @@ hzhub/
|
||||
├── hzhub-erp/ # ERP服务(新建)
|
||||
├── hzhub-gateway/ # API网关(新建)
|
||||
├── hzhub-admin/ # 管理后台(复用hzhub-admin)
|
||||
├── hzhub-portal-company/ # 公司门户(复用hzhub-portal)
|
||||
├── hzhub-portal-employee/ # 员工门户(复用hzhub-portal)
|
||||
├── hzhub-portal-dealer/ # 经销商门户(复用hzhub-portal)
|
||||
├── hzhub-deploy/ # 部署配置
|
||||
│ └── docker-compose.yml
|
||||
@@ -101,7 +101,7 @@ hzhub/
|
||||
- ✅ 前端管理后台 Docker 化 (hzhub-admin)
|
||||
- ⏳ ERP服务 (开发中)
|
||||
- ⏳ API网关 (开发中)
|
||||
- ⏳ 公司门户 (待配置)
|
||||
- ⏳ 员工门户 (待配置)
|
||||
- ⏳ 经销商门户 (待配置)
|
||||
|
||||
## 快速开始
|
||||
@@ -123,9 +123,47 @@ docker-compose up -d
|
||||
|
||||
启动完成后访问:
|
||||
- 🌐 管理后台: http://localhost:5666
|
||||
- 🚀 员工门户: http://localhost:5137
|
||||
- 🔧 AI服务API: http://localhost:6039
|
||||
- 🔄 n8n工作流: http://localhost:5678
|
||||
|
||||
### 本地开发启动(后台运行)
|
||||
|
||||
项目提供了便捷的后台运行管理脚本:
|
||||
|
||||
```bash
|
||||
# 一键启动所有服务
|
||||
cd /data/hzhub
|
||||
./start-all.sh
|
||||
|
||||
# 查看所有服务状态
|
||||
./status-all.sh
|
||||
|
||||
# 一键停止所有服务
|
||||
./stop-all.sh
|
||||
```
|
||||
|
||||
或分别启动各服务:
|
||||
|
||||
```bash
|
||||
# 启动后端服务
|
||||
cd hzhub-ai
|
||||
./start.sh # 启动
|
||||
./status.sh # 查看状态
|
||||
./logs.sh # 查看日志
|
||||
./stop.sh # 停止
|
||||
|
||||
# 启动管理后台
|
||||
cd hzhub-admin
|
||||
./start.sh
|
||||
|
||||
# 启动员工门户
|
||||
cd hzhub-portal-employee
|
||||
./start.sh
|
||||
```
|
||||
|
||||
📖 详细说明请查看 [服务管理文档](./SERVICE_MANAGEMENT.md)
|
||||
|
||||
### 服务清单
|
||||
|
||||
| 服务名 | 容器名 | 端口 | 说明 |
|
||||
|
||||
286
SERVICE_MANAGEMENT.md
Executable file
@@ -0,0 +1,286 @@
|
||||
# 服务管理脚本使用说明
|
||||
|
||||
本项目为 `hzhub-admin`、`hzhub-ai` 和 `hzhub-portal-employee` 三个项目提供了后台运行管理脚本,方便开发调试。
|
||||
|
||||
## 📋 脚本列表
|
||||
|
||||
每个项目根目录下都包含以下脚本:
|
||||
|
||||
| 脚本 | 功能 | 说明 |
|
||||
|------|------|------|
|
||||
| `start.sh` | 启动服务 | 后台启动开发服务器 |
|
||||
| `stop.sh` | 停止服务 | 停止后台运行的服务 |
|
||||
| `restart.sh` | 重启服务 | 先停止后启动 |
|
||||
| `status.sh` | 查看状态 | 查看服务运行状态和最新日志 |
|
||||
| `logs.sh` | 查看日志 | 实时查看服务日志 |
|
||||
|
||||
## 🚀 快速开始
|
||||
|
||||
### 1. 启动后端服务 (hzhub-ai)
|
||||
|
||||
```bash
|
||||
cd /data/hzhub/hzhub-ai
|
||||
./start.sh
|
||||
```
|
||||
|
||||
**输出示例:**
|
||||
```
|
||||
=========================================
|
||||
启动 hzhub-ai 后端服务
|
||||
=========================================
|
||||
🚀 启动 Spring Boot 服务...
|
||||
⏳ 等待服务启动中...
|
||||
✅ 服务启动成功
|
||||
PID: 12345
|
||||
日志: /data/hzhub/hzhub-ai/logs/backend.log
|
||||
API: http://localhost:6039
|
||||
```
|
||||
|
||||
**访问地址:** http://localhost:6039
|
||||
|
||||
### 2. 启动管理后台 (hzhub-admin)
|
||||
|
||||
```bash
|
||||
cd /data/hzhub/hzhub-admin
|
||||
./start.sh
|
||||
```
|
||||
|
||||
**访问地址:** http://localhost:5666
|
||||
|
||||
### 3. 启动员工门户 (hzhub-portal-employee)
|
||||
|
||||
```bash
|
||||
cd /data/hzhub/hzhub-portal-employee
|
||||
./start.sh
|
||||
```
|
||||
|
||||
**访问地址:** http://localhost:5137
|
||||
|
||||
## 📊 查看服务状态
|
||||
|
||||
```bash
|
||||
# 查看后端服务状态
|
||||
cd /data/hzhub/hzhub-ai
|
||||
./status.sh
|
||||
|
||||
# 查看管理后台状态
|
||||
cd /data/hzhub/hzhub-admin
|
||||
./status.sh
|
||||
|
||||
# 查看员工门户状态
|
||||
cd /data/hzhub/hzhub-portal-employee
|
||||
./status.sh
|
||||
```
|
||||
|
||||
**输出示例:**
|
||||
```
|
||||
=========================================
|
||||
hzhub-ai 服务状态
|
||||
=========================================
|
||||
状态: 🟢 运行中
|
||||
PID: 12345
|
||||
PID PPID CMD ELAPSED
|
||||
12345 1 mvn spring-boot:run -Dspring- 00:15:32
|
||||
|
||||
日志文件: /data/hzhub/hzhub-ai/logs/backend.log
|
||||
API地址: http://localhost:6039
|
||||
|
||||
端口状态: ✅ 6039 端口正在监听
|
||||
```
|
||||
|
||||
## 📝 查看实时日志
|
||||
|
||||
```bash
|
||||
# 查看后端日志
|
||||
cd /data/hzhub/hzhub-ai
|
||||
./logs.sh
|
||||
|
||||
# 或直接使用tail命令
|
||||
tail -f /data/hzhub/hzhub-ai/logs/backend.log
|
||||
```
|
||||
|
||||
按 `Ctrl+C` 退出日志查看。
|
||||
|
||||
## 🔄 重启服务
|
||||
|
||||
```bash
|
||||
# 重启后端服务
|
||||
cd /data/hzhub/hzhub-ai
|
||||
./restart.sh
|
||||
|
||||
# 重启管理后台
|
||||
cd /data/hzhub/hzhub-admin
|
||||
./restart.sh
|
||||
|
||||
# 重启员工门户
|
||||
cd /data/hzhub/hzhub-portal-employee
|
||||
./restart.sh
|
||||
```
|
||||
|
||||
## 🛑 停止服务
|
||||
|
||||
```bash
|
||||
# 停止后端服务
|
||||
cd /data/hzhub/hzhub-ai
|
||||
./stop.sh
|
||||
|
||||
# 停止管理后台
|
||||
cd /data/hzhub/hzhub-admin
|
||||
./stop.sh
|
||||
|
||||
# 停止员工门户
|
||||
cd /data/hzhub/hzhub-portal-employee
|
||||
./stop.sh
|
||||
```
|
||||
|
||||
## 💡 使用技巧
|
||||
|
||||
### 启动顺序建议
|
||||
|
||||
1. **先启动后端服务** (hzhub-ai)
|
||||
```bash
|
||||
cd /data/hzhub/hzhub-ai && ./start.sh
|
||||
```
|
||||
|
||||
2. **等待后端完全启动** (约30-60秒)
|
||||
```bash
|
||||
./status.sh # 查看端口6039是否监听
|
||||
```
|
||||
|
||||
3. **启动前端应用**
|
||||
```bash
|
||||
cd /data/hzhub/hzhub-admin && ./start.sh
|
||||
cd /data/hzhub/hzhub-portal-employee && ./start.sh
|
||||
```
|
||||
|
||||
### 一键启动所有服务
|
||||
|
||||
创建一个启动脚本 `/data/hzhub/start-all.sh`:
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
echo "启动所有 HZHub 服务..."
|
||||
|
||||
cd /data/hzhub/hzhub-ai
|
||||
./start.sh
|
||||
sleep 10
|
||||
|
||||
cd /data/hzhub/hzhub-admin
|
||||
./start.sh
|
||||
|
||||
cd /data/hzhub/hzhub-portal-employee
|
||||
./start.sh
|
||||
|
||||
echo "所有服务启动完成!"
|
||||
```
|
||||
|
||||
### 一键停止所有服务
|
||||
|
||||
创建一个停止脚本 `/data/hzhub/stop-all.sh`:
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
echo "停止所有 HZHub 服务..."
|
||||
|
||||
cd /data/hzhub/hzhub-portal-employee
|
||||
./stop.sh
|
||||
|
||||
cd /data/hzhub/hzhub-admin
|
||||
./stop.sh
|
||||
|
||||
cd /data/hzhub/hzhub-ai
|
||||
./stop.sh
|
||||
|
||||
echo "所有服务已停止!"
|
||||
```
|
||||
|
||||
## ⚙️ 服务信息
|
||||
|
||||
| 项目 | 端口 | 日志文件 | PID文件 |
|
||||
|------|------|----------|---------|
|
||||
| hzhub-ai | 6039 | logs/backend.log | .pid |
|
||||
| hzhub-admin | 5666 | logs/dev.log | .pid |
|
||||
| hzhub-portal-employee | 5137 | logs/dev.log | .pid |
|
||||
|
||||
## 🔍 常见问题
|
||||
|
||||
### Q: 服务启动失败怎么办?
|
||||
|
||||
**A:** 检查以下几点:
|
||||
1. 查看日志文件:`./logs.sh` 或 `tail -f logs/*.log`
|
||||
2. 检查端口是否被占用:`netstat -tuln | grep 端口号`
|
||||
3. 确认依赖是否安装:前端项目执行 `pnpm install`
|
||||
4. 后端项目检查 Maven 是否可用:`mvn -v`
|
||||
|
||||
### Q: 如何确认服务是否完全启动?
|
||||
|
||||
**A:**
|
||||
- **后端服务**:执行 `./status.sh` 查看6039端口是否监听
|
||||
- **前端服务**:访问对应端口查看页面是否正常显示
|
||||
|
||||
### Q: PID文件损坏怎么办?
|
||||
|
||||
**A:** 手动删除PID文件:
|
||||
```bash
|
||||
rm .pid
|
||||
./start.sh
|
||||
```
|
||||
|
||||
### Q: 进程僵死无法停止?
|
||||
|
||||
**A:** 查找并强制终止进程:
|
||||
```bash
|
||||
# 查找进程
|
||||
ps aux | grep "spring-boot\|vite\|pnpm"
|
||||
|
||||
# 强制终止
|
||||
kill -9 <PID>
|
||||
|
||||
# 清理PID文件
|
||||
rm .pid
|
||||
```
|
||||
|
||||
## 📁 日志管理
|
||||
|
||||
### 日志位置
|
||||
|
||||
所有日志都保存在各项目的 `logs/` 目录下:
|
||||
|
||||
```
|
||||
hzhub-ai/
|
||||
├── logs/
|
||||
│ └── backend.log
|
||||
|
||||
hzhub-admin/
|
||||
├── logs/
|
||||
│ └── dev.log
|
||||
|
||||
hzhub-portal-employee/
|
||||
├── logs/
|
||||
│ └── dev.log
|
||||
```
|
||||
|
||||
### 日志轮转
|
||||
|
||||
建议定期清理日志文件:
|
||||
|
||||
```bash
|
||||
# 清空日志文件
|
||||
> logs/backend.log
|
||||
|
||||
# 或删除旧日志
|
||||
rm logs/*.log
|
||||
```
|
||||
|
||||
## ⚠️ 注意事项
|
||||
|
||||
1. **端口冲突**:确保端口 6039、5666、5137 未被其他服务占用
|
||||
2. **资源占用**:后端服务启动较慢(30-60秒),请耐心等待
|
||||
3. **开发模式**:这些脚本仅用于开发调试,生产环境请使用 Docker 部署
|
||||
4. **数据持久化**:前端开发服务器会自动热重载,后端服务需要手动重启
|
||||
|
||||
## 🔗 相关文档
|
||||
|
||||
- [项目主文档](../CLAUDE.md)
|
||||
- [部署文档](../hzhub-deploy/README.md)
|
||||
- [开发环境配置](../docs/project/plan/phase-1.md)
|
||||
@@ -60,7 +60,7 @@ foshanhuiya/hzhub/
|
||||
├── hzhub-erp/ # ERP服务(Spring Boot 4.0)
|
||||
├── hzhub-gateway/ # API网关(Spring Boot 4.0)
|
||||
├── hzhub-admin/ # 管理后台前端
|
||||
├── hzhub-portal-company/ # 公司门户前端
|
||||
├── hzhub-portal-employee/ # 公司门户前端
|
||||
├── hzhub-portal-dealer/ # 经销商门户前端
|
||||
└── hzhub-deploy/ # 部署脚本与配置
|
||||
```
|
||||
|
||||
@@ -31,7 +31,7 @@ HZHub(汇智中台)是基于 HZHub-AI 构建的企业级业务中台系统
|
||||
| hzhub-ai | AI 核心能力 | Spring Boot 4.0 + Spring AI 2.0 |
|
||||
| hzhub-erp | ERP 数据适配 | Spring Boot 4.0 + JDBC |
|
||||
| hzhub-admin | 管理后台前端 | Vue 3 |
|
||||
| hzhub-portal-company | 公司门户前端 | Vue 3 |
|
||||
| hzhub-portal-employee | 公司门户前端 | Vue 3 |
|
||||
| hzhub-portal-dealer | 经销商门户前端 | Vue 3 |
|
||||
|
||||
### 2.3 数据存储
|
||||
|
||||
@@ -18,7 +18,7 @@ digraph HZHubArchitectureCN {
|
||||
labeljust=left;
|
||||
|
||||
admin [label="hzhub-admin\n管理后台\n• 模型管理\n• 知识库配置\n• 智能体编排", fillcolor="#BBDEFB", color="#1976D2"];
|
||||
company [label="hzhub-portal-company\n公司门户\n• 企业微信H5\n• 审批流程\n• 销售CRM\n• BI报表", fillcolor="#BBDEFB", color="#1976D2"];
|
||||
company [label="hzhub-portal-employee\n员工门户\n• 企业微信H5\n• 审批流程\n• 销售CRM\n• BI报表", fillcolor="#BBDEFB", color="#1976D2"];
|
||||
dealer [label="hzhub-portal-dealer\n经销商门户\n• 企业微信H5\n• 自助开单\n• 进销存\n• AI素材生成", fillcolor="#BBDEFB", color="#1976D2"];
|
||||
}
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ digraph HZHubArchitecture {
|
||||
labeljust=left;
|
||||
|
||||
admin [label="hzhub-admin\nManagement Portal\n• Model Management\n• Knowledge Base Config\n• Agent Orchestration", fillcolor="#BBDEFB", color="#1976D2"];
|
||||
company [label="hzhub-portal-company\nCompany Portal\n• WeChat Work H5\n• Approval Workflow\n• Sales CRM\n• BI Reports", fillcolor="#BBDEFB", color="#1976D2"];
|
||||
company [label="hzhub-portal-employee\nEmployee Portal\n• WeChat Work H5\n• Approval Workflow\n• Sales CRM\n• BI Reports", fillcolor="#BBDEFB", color="#1976D2"];
|
||||
dealer [label="hzhub-portal-dealer\nDealer Portal\n• WeChat Work H5\n• Self-service Order\n• Inventory Mgmt\n• AI Material Gen", fillcolor="#BBDEFB", color="#1976D2"];
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
graph TB
|
||||
subgraph 前端层["🖥️ 前端接入层"]
|
||||
A1["hzhub-admin<br/>管理后台"]
|
||||
A2["hzhub-portal-company<br/>公司门户"]
|
||||
A2["hzhub-portal-employee<br/>员工门户"]
|
||||
A3["hzhub-portal-dealer<br/>经销商门户"]
|
||||
end
|
||||
|
||||
|
||||
@@ -289,7 +289,7 @@ async function runWorkflow() {
|
||||
### 6.3 hzhub-portal推荐结构
|
||||
|
||||
```
|
||||
hzhub-portal-company/ # 公司门户
|
||||
hzhub-portal-employee/ # 公司门户
|
||||
├── src/
|
||||
│ ├── components/
|
||||
│ │ ├── ErpQueryBuilder/ # ERP查询构建器
|
||||
|
||||
@@ -152,7 +152,7 @@ HZHub 技术栈决策:
|
||||
├── pnpm workspace + Turbo
|
||||
└── 复用hzhub-admin的packages
|
||||
|
||||
门户前端 (hzhub-portal-company / hzhub-portal-dealer)
|
||||
门户前端 (hzhub-portal-employee / hzhub-portal-dealer)
|
||||
├── Vue 3 + TypeScript
|
||||
├── Element Plus X(复用hzhub-portal组件)
|
||||
├── UnoCSS
|
||||
@@ -167,7 +167,7 @@ HZHub 技术栈决策:
|
||||
| P0 | hzhub-ai | 复用ruoyi-chat | 1周 |
|
||||
| P0 | hzhub-admin | 复用hzhub-admin | 1周 |
|
||||
| P1 | hzhub-erp | 自研SQL Server适配 | 2-3周 |
|
||||
| P1 | hzhub-portal-company | 复用+改造 | 2周 |
|
||||
| P1 | hzhub-portal-employee | 复用+改造 | 2周 |
|
||||
| P2 | hzhub-portal-dealer | 复用+改造 | 2周 |
|
||||
| P2 | 流程编排ERP节点 | 扩展aiflow | 1周 |
|
||||
|
||||
@@ -271,7 +271,7 @@ HZHub 技术栈决策:
|
||||
**目标**:门户开发
|
||||
|
||||
**任务**:
|
||||
1. 开发hzhub-portal-company
|
||||
1. 开发hzhub-portal-employee
|
||||
2. 开发hzhub-portal-dealer
|
||||
3. 集成AI组件到门户
|
||||
4. 企业微信对接
|
||||
|
||||
@@ -63,7 +63,7 @@
|
||||
- [x] 启动基础设施(MySQL、Redis、Weaviate、n8n、MinIO)
|
||||
- [x] 验证AI服务运行
|
||||
- [x] 验证管理后台运行
|
||||
- [x] 配置hzhub-portal-company
|
||||
- [x] 配置hzhub-portal-employee
|
||||
- [x] 配置hzhub-portal-dealer
|
||||
- [x] RuoYi→HZHub重命名
|
||||
- [ ] 测试前端登录功能
|
||||
@@ -108,7 +108,7 @@
|
||||
**目标**:完成公司门户和经销商门户
|
||||
|
||||
**任务清单**:
|
||||
- [ ] 初始化hzhub-portal-company
|
||||
- [ ] 初始化hzhub-portal-employee
|
||||
- [ ] 初始化hzhub-portal-dealer
|
||||
- [ ] 开发公司门户页面
|
||||
- [ ] 开发经销商门户页面
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
搭建可运行的基础框架,包括:
|
||||
- [x] AI服务可用(hzhub-ai)
|
||||
- [x] 管理后台可用(hzhub-admin)
|
||||
- [x] 公司门户可用(hzhub-portal-company)
|
||||
- [x] 公司门户可用(hzhub-portal-employee)
|
||||
- [x] 经销商门户可用(hzhub-portal-dealer)
|
||||
- [x] 基础设施就绪(MySQL、Redis、Weaviate、n8n、MinIO)
|
||||
- [ ] 开发环境配置完成
|
||||
@@ -58,7 +58,7 @@
|
||||
| 任务 | 负责人 | 状态 | 备注 |
|
||||
|------|--------|------|------|
|
||||
| 配置hzhub-admin | 大壮 | ✅ 已完成 | 端口5666,Nginx |
|
||||
| 配置hzhub-portal-company | 大壮 | ✅ 已完成 | 端口5137,Nginx |
|
||||
| 配置hzhub-portal-employee | 大壮 | ✅ 已完成 | 端口5137,Nginx |
|
||||
| 配置hzhub-portal-dealer | 大壮 | ✅ 已完成 | 端口5138,Nginx |
|
||||
| 验证前端服务 | 大壮 | ✅ 已完成 | 全部可访问 |
|
||||
|
||||
@@ -187,7 +187,7 @@ services:
|
||||
services:
|
||||
hzhub-ai: # Spring Boot - 端口6039
|
||||
hzhub-admin: # Nginx + Vue - 端口5666
|
||||
hzhub-portal-company: # Nginx + Vue - 端口5137
|
||||
hzhub-portal-employee: # Nginx + Vue - 端口5137
|
||||
hzhub-portal-dealer: # Nginx + Vue - 端口5138
|
||||
```
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
- hzhub-admin(复用hzhub-admin)
|
||||
- hzhub-erp(新建)
|
||||
- hzhub-gateway(新建)
|
||||
- hzhub-portal-company/dealer(复用hzhub-portal)
|
||||
- hzhub-portal-employee/dealer(复用hzhub-portal)
|
||||
|
||||
4. **建立项目管理文档**
|
||||
- 总体计划(8周,4个阶段)
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
| 配置hzhub-ai数据库 | 3.30 | 3.27 | ✅ 已完成 |
|
||||
| 验证AI服务运行 | 4.1 | 3.27 | ✅ 已完成 |
|
||||
| 配置hzhub-admin | 4.2 | 3.27 | ✅ 已完成 |
|
||||
| 配置hzhub-portal-company | - | 3.27 | ✅ 已完成 |
|
||||
| 配置hzhub-portal-employee | - | 3.27 | ✅ 已完成 |
|
||||
| 配置hzhub-portal-dealer | - | 3.27 | ✅ 已完成 |
|
||||
| RuoYi→HZHub重命名 | - | 3.27 | ✅ 已完成 |
|
||||
| 修复租户管理日期格式问题 | - | 4.02 | ✅ 已完成 |
|
||||
@@ -44,7 +44,7 @@
|
||||
|
||||
4. **前端服务全部Docker化**
|
||||
- hzhub-admin (管理后台) - 端口5666
|
||||
- hzhub-portal-company (公司门户) - 端口5137
|
||||
- hzhub-portal-employee (公司门户) - 端口5137
|
||||
- hzhub-portal-dealer (经销商门户) - 端口5138
|
||||
|
||||
5. **RuoYi到HZHub全面重命名**
|
||||
@@ -76,7 +76,7 @@
|
||||
services:
|
||||
hzhub-ai: 端口6039
|
||||
hzhub-admin: 端口5666
|
||||
hzhub-portal-company: 端口5137
|
||||
hzhub-portal-employee: 端口5137
|
||||
hzhub-portal-dealer: 端口5138
|
||||
hzhub-mysql: 端口3306
|
||||
hzhub-redis: 端口6379
|
||||
|
||||
1
hzhub-admin/.pid
Normal file
@@ -0,0 +1 @@
|
||||
3357568
|
||||
16
hzhub-admin/logs.sh
Executable file
@@ -0,0 +1,16 @@
|
||||
#!/bin/bash
|
||||
# HZHub Admin 前端项目日志查看脚本
|
||||
# 功能:查看服务运行日志
|
||||
|
||||
PROJECT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
LOG_FILE="$PROJECT_DIR/logs/dev.log"
|
||||
|
||||
if [ ! -f "$LOG_FILE" ]; then
|
||||
echo "⚠️ 日志文件不存在: $LOG_FILE"
|
||||
echo "服务可能未运行或未产生日志"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "查看 hzhub-admin 日志 (Ctrl+C 退出)"
|
||||
echo "========================================="
|
||||
tail -f "$LOG_FILE"
|
||||
18
hzhub-admin/restart.sh
Executable file
@@ -0,0 +1,18 @@
|
||||
#!/bin/bash
|
||||
# HZHub Admin 前端项目重启脚本
|
||||
# 功能:重启后台运行的 hzhub-admin 前端开发服务器
|
||||
|
||||
PROJECT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
|
||||
echo "========================================="
|
||||
echo "重启 hzhub-admin 开发服务器"
|
||||
echo "========================================="
|
||||
|
||||
# 停止服务
|
||||
"$PROJECT_DIR/stop.sh"
|
||||
|
||||
# 等待一秒
|
||||
sleep 1
|
||||
|
||||
# 启动服务
|
||||
"$PROJECT_DIR/start.sh"
|
||||
53
hzhub-admin/start.sh
Executable file
@@ -0,0 +1,53 @@
|
||||
#!/bin/bash
|
||||
# HZHub Admin 前端项目启动脚本
|
||||
# 功能:后台启动 hzhub-admin 前端开发服务器
|
||||
|
||||
PROJECT_NAME="hzhub-admin"
|
||||
PROJECT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
PID_FILE="$PROJECT_DIR/.pid"
|
||||
LOG_FILE="$PROJECT_DIR/logs/dev.log"
|
||||
|
||||
# 创建日志目录
|
||||
mkdir -p "$PROJECT_DIR/logs"
|
||||
|
||||
echo "========================================="
|
||||
echo "启动 $PROJECT_NAME 开发服务器"
|
||||
echo "========================================="
|
||||
|
||||
# 检查是否已经在运行
|
||||
if [ -f "$PID_FILE" ]; then
|
||||
PID=$(cat "$PID_FILE")
|
||||
if ps -p "$PID" > /dev/null 2>&1; then
|
||||
echo "⚠️ 服务已在运行中 (PID: $PID)"
|
||||
echo "如需重启,请先执行 ./stop.sh"
|
||||
exit 1
|
||||
else
|
||||
echo "清理无效的PID文件"
|
||||
rm -f "$PID_FILE"
|
||||
fi
|
||||
fi
|
||||
|
||||
# 启动服务
|
||||
echo "🚀 启动开发服务器..."
|
||||
cd "$PROJECT_DIR"
|
||||
|
||||
# 使用nohup后台运行pnpm dev
|
||||
nohup pnpm dev > "$LOG_FILE" 2>&1 &
|
||||
PID=$!
|
||||
|
||||
# 等待一秒检查进程是否成功启动
|
||||
sleep 2
|
||||
if ps -p "$PID" > /dev/null 2>&1; then
|
||||
echo "$PID" > "$PID_FILE"
|
||||
echo "✅ 服务启动成功"
|
||||
echo " PID: $PID"
|
||||
echo " 日志: $LOG_FILE"
|
||||
echo " 访问: http://localhost:5666"
|
||||
echo ""
|
||||
echo "查看日志: tail -f $LOG_FILE"
|
||||
echo "查看状态: ./status.sh"
|
||||
else
|
||||
echo "❌ 服务启动失败"
|
||||
echo "请查看日志: $LOG_FILE"
|
||||
exit 1
|
||||
fi
|
||||
40
hzhub-admin/status.sh
Executable file
@@ -0,0 +1,40 @@
|
||||
#!/bin/bash
|
||||
# HZHub Admin 前端项目状态检查脚本
|
||||
# 功能:查看服务运行状态
|
||||
|
||||
PROJECT_NAME="hzhub-admin"
|
||||
PROJECT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
PID_FILE="$PROJECT_DIR/.pid"
|
||||
LOG_FILE="$PROJECT_DIR/logs/dev.log"
|
||||
|
||||
echo "========================================="
|
||||
echo " $PROJECT_NAME 服务状态"
|
||||
echo "========================================="
|
||||
|
||||
# 检查PID文件
|
||||
if [ ! -f "$PID_FILE" ]; then
|
||||
echo "状态: ⚪ 未运行"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
PID=$(cat "$PID_FILE")
|
||||
|
||||
# 检查进程是否存在
|
||||
if ps -p "$PID" > /dev/null 2>&1; then
|
||||
echo "状态: 🟢 运行中"
|
||||
echo "PID: $PID"
|
||||
|
||||
# 显示进程详细信息
|
||||
ps -p "$PID" -o pid,ppid,cmd,etime
|
||||
|
||||
echo ""
|
||||
echo "日志文件: $LOG_FILE"
|
||||
echo "访问地址: http://localhost:5666"
|
||||
echo ""
|
||||
echo "最近10行日志:"
|
||||
echo "---"
|
||||
tail -n 10 "$LOG_FILE" 2>/dev/null || echo "暂无日志"
|
||||
else
|
||||
echo "状态: 🔴 已停止 (PID文件存在但进程不存在)"
|
||||
rm -f "$PID_FILE"
|
||||
fi
|
||||
44
hzhub-admin/stop.sh
Executable file
@@ -0,0 +1,44 @@
|
||||
#!/bin/bash
|
||||
# HZHub Admin 前端项目停止脚本
|
||||
# 功能:停止后台运行的 hzhub-admin 前端开发服务器
|
||||
|
||||
PROJECT_NAME="hzhub-admin"
|
||||
PROJECT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
PID_FILE="$PROJECT_DIR/.pid"
|
||||
|
||||
echo "========================================="
|
||||
echo "停止 $PROJECT_NAME 开发服务器"
|
||||
echo "========================================="
|
||||
|
||||
# 检查PID文件是否存在
|
||||
if [ ! -f "$PID_FILE" ]; then
|
||||
echo "⚠️ 未找到PID文件,服务可能未运行"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
PID=$(cat "$PID_FILE")
|
||||
|
||||
# 检查进程是否存在
|
||||
if ! ps -p "$PID" > /dev/null 2>&1; then
|
||||
echo "⚠️ 进程不存在 (PID: $PID)"
|
||||
rm -f "$PID_FILE"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# 停止进程
|
||||
echo "🛑 正在停止服务 (PID: $PID)..."
|
||||
kill "$PID"
|
||||
|
||||
# 等待进程结束
|
||||
sleep 2
|
||||
|
||||
# 检查进程是否已停止
|
||||
if ps -p "$PID" > /dev/null 2>&1; then
|
||||
echo "⚠️ 进程未响应,强制终止..."
|
||||
kill -9 "$PID"
|
||||
sleep 1
|
||||
fi
|
||||
|
||||
# 清理PID文件
|
||||
rm -f "$PID_FILE"
|
||||
echo "✅ 服务已停止"
|
||||
1
hzhub-ai/.pid
Normal file
@@ -0,0 +1 @@
|
||||
3999223
|
||||
@@ -58,7 +58,7 @@ spring:
|
||||
driverClassName: com.mysql.cj.jdbc.Driver
|
||||
# jdbc 所有参数配置参考 https://lionli.blog.csdn.net/article/details/122018562
|
||||
# rewriteBatchedStatements=true 批处理优化 大幅提升批量插入更新删除性能(对数据库有性能损耗 使用批量操作应考虑性能问题)
|
||||
url: jdbc:mysql://127.0.0.1:3306/ruoyi_ai?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&autoReconnect=true&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true
|
||||
url: jdbc:mysql://127.0.0.1:3306/hzhub?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&autoReconnect=true&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true
|
||||
username: root
|
||||
password: hzhub123
|
||||
# agent:
|
||||
|
||||
@@ -105,7 +105,7 @@ public class LoginUser implements Serializable {
|
||||
/**
|
||||
* 用户昵称
|
||||
*/
|
||||
private String nickname;
|
||||
private String nickName;
|
||||
|
||||
/**
|
||||
* 角色对象
|
||||
|
||||
@@ -40,7 +40,6 @@ public class KnowledgeInfoController extends BaseController {
|
||||
/**
|
||||
* 查询知识库列表
|
||||
*/
|
||||
@SaCheckPermission("system:info:list")
|
||||
@GetMapping("/list")
|
||||
public TableDataInfo<KnowledgeInfoVo> list(KnowledgeInfoBo bo, PageQuery pageQuery) {
|
||||
return knowledgeInfoService.queryPageList(bo, pageQuery);
|
||||
|
||||
@@ -2,6 +2,7 @@ package org.hzhub.system.domain.vo;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.Data;
|
||||
import org.hzhub.common.core.domain.model.LoginUser;
|
||||
|
||||
/**
|
||||
* 登录验证信息
|
||||
@@ -51,4 +52,10 @@ public class LoginVo {
|
||||
*/
|
||||
private String openid;
|
||||
|
||||
/**
|
||||
* 用户信息(用于前端展示昵称等)
|
||||
*/
|
||||
@JsonProperty("userInfo")
|
||||
private LoginUser userInfo;
|
||||
|
||||
}
|
||||
|
||||
@@ -154,7 +154,7 @@ public class SysLoginService {
|
||||
loginUser.setUserId(userId);
|
||||
loginUser.setDeptId(user.getDeptId());
|
||||
loginUser.setUsername(user.getUserName());
|
||||
loginUser.setNickname(user.getNickName());
|
||||
loginUser.setNickName(user.getNickName());
|
||||
loginUser.setUserType(user.getUserType());
|
||||
loginUser.setMenuPermission(permissionService.getMenuPermission(userId));
|
||||
loginUser.setRolePermission(permissionService.getRolePermission(userId));
|
||||
|
||||
@@ -84,6 +84,7 @@ public class PasswordAuthStrategy implements IAuthStrategy {
|
||||
loginVo.setAccessToken(StpUtil.getTokenValue());
|
||||
loginVo.setExpireIn(StpUtil.getTokenTimeout());
|
||||
loginVo.setClientId(client.getClientId());
|
||||
loginVo.setUserInfo(loginUser);
|
||||
return loginVo;
|
||||
}
|
||||
|
||||
|
||||
16
hzhub-ai/logs.sh
Executable file
@@ -0,0 +1,16 @@
|
||||
#!/bin/bash
|
||||
# HZHub AI 后端服务日志查看脚本
|
||||
# 功能:查看服务运行日志
|
||||
|
||||
PROJECT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
LOG_FILE="$PROJECT_DIR/logs/backend.log"
|
||||
|
||||
if [ ! -f "$LOG_FILE" ]; then
|
||||
echo "⚠️ 日志文件不存在: $LOG_FILE"
|
||||
echo "服务可能未运行或未产生日志"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "查看 hzhub-ai 日志 (Ctrl+C 退出)"
|
||||
echo "========================================="
|
||||
tail -f "$LOG_FILE"
|
||||
18
hzhub-ai/restart.sh
Executable file
@@ -0,0 +1,18 @@
|
||||
#!/bin/bash
|
||||
# HZHub AI 后端服务重启脚本
|
||||
# 功能:重启后台运行的 hzhub-ai Spring Boot 服务
|
||||
|
||||
PROJECT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
|
||||
echo "========================================="
|
||||
echo "重启 hzhub-ai 后端服务"
|
||||
echo "========================================="
|
||||
|
||||
# 停止服务
|
||||
"$PROJECT_DIR/stop.sh"
|
||||
|
||||
# 等待两秒确保完全停止
|
||||
sleep 2
|
||||
|
||||
# 启动服务
|
||||
"$PROJECT_DIR/start.sh"
|
||||
59
hzhub-ai/start.sh
Executable file
@@ -0,0 +1,59 @@
|
||||
#!/bin/bash
|
||||
# HZHub AI 后端服务启动脚本
|
||||
# 功能:后台启动 hzhub-ai Spring Boot 服务
|
||||
# 注意:实际启动的是 hzhub-admin 子模块
|
||||
|
||||
PROJECT_NAME="hzhub-ai"
|
||||
PROJECT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
PID_FILE="$PROJECT_DIR/.pid"
|
||||
LOG_FILE="$PROJECT_DIR/logs/backend.log"
|
||||
|
||||
# 创建日志目录
|
||||
mkdir -p "$PROJECT_DIR/logs"
|
||||
|
||||
echo "========================================="
|
||||
echo "启动 $PROJECT_NAME 后端服务"
|
||||
echo "========================================="
|
||||
|
||||
# 检查是否已经在运行
|
||||
if [ -f "$PID_FILE" ]; then
|
||||
PID=$(cat "$PID_FILE")
|
||||
if ps -p "$PID" > /dev/null 2>&1; then
|
||||
echo "⚠️ 服务已在运行中 (PID: $PID)"
|
||||
echo "如需重启,请先执行 ./stop.sh"
|
||||
exit 1
|
||||
else
|
||||
echo "清理无效的PID文件"
|
||||
rm -f "$PID_FILE"
|
||||
fi
|
||||
fi
|
||||
|
||||
# 启动服务
|
||||
echo "🚀 启动 Spring Boot 服务..."
|
||||
cd "$PROJECT_DIR/hzhub-admin"
|
||||
|
||||
# 使用nohup后台运行Maven Spring Boot
|
||||
nohup mvn spring-boot:run -Dspring-boot.run.profiles=dev > "$LOG_FILE" 2>&1 &
|
||||
PID=$!
|
||||
|
||||
# 等待更长时间检查进程是否成功启动(Spring Boot启动较慢)
|
||||
echo "⏳ 等待服务启动中..."
|
||||
sleep 5
|
||||
|
||||
if ps -p "$PID" > /dev/null 2>&1; then
|
||||
echo "$PID" > "$PID_FILE"
|
||||
echo "✅ 服务启动成功"
|
||||
echo " PID: $PID"
|
||||
echo " 日志: $LOG_FILE"
|
||||
echo " API: http://localhost:6039"
|
||||
echo ""
|
||||
echo "查看日志: tail -f $LOG_FILE"
|
||||
echo "查看状态: ./status.sh"
|
||||
echo ""
|
||||
echo "💡 提示: Spring Boot 完整启动需要30-60秒"
|
||||
echo " 请执行 ./logs.sh 或 tail -f $LOG_FILE 查看启动进度"
|
||||
else
|
||||
echo "❌ 服务启动失败"
|
||||
echo "请查看日志: $LOG_FILE"
|
||||
exit 1
|
||||
fi
|
||||
49
hzhub-ai/status.sh
Executable file
@@ -0,0 +1,49 @@
|
||||
#!/bin/bash
|
||||
# HZHub AI 后端服务状态检查脚本
|
||||
# 功能:查看服务运行状态
|
||||
|
||||
PROJECT_NAME="hzhub-ai"
|
||||
PROJECT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
PID_FILE="$PROJECT_DIR/.pid"
|
||||
LOG_FILE="$PROJECT_DIR/logs/backend.log"
|
||||
|
||||
echo "========================================="
|
||||
echo " $PROJECT_NAME 服务状态"
|
||||
echo "========================================="
|
||||
|
||||
# 检查PID文件
|
||||
if [ ! -f "$PID_FILE" ]; then
|
||||
echo "状态: ⚪ 未运行"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
PID=$(cat "$PID_FILE")
|
||||
|
||||
# 检查进程是否存在
|
||||
if ps -p "$PID" > /dev/null 2>&1; then
|
||||
echo "状态: 🟢 运行中"
|
||||
echo "PID: $PID"
|
||||
|
||||
# 显示进程详细信息
|
||||
ps -p "$PID" -o pid,ppid,cmd,etime
|
||||
|
||||
echo ""
|
||||
echo "日志文件: $LOG_FILE"
|
||||
echo "API地址: http://localhost:6039"
|
||||
echo ""
|
||||
|
||||
# 检查端口是否在监听
|
||||
if netstat -tuln 2>/dev/null | grep -q ":6039 "; then
|
||||
echo "端口状态: ✅ 6039 端口正在监听"
|
||||
else
|
||||
echo "端口状态: ⏳ 6039 端口未监听 (可能还在启动中)"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "最近20行日志:"
|
||||
echo "---"
|
||||
tail -n 20 "$LOG_FILE" 2>/dev/null || echo "暂无日志"
|
||||
else
|
||||
echo "状态: 🔴 已停止 (PID文件存在但进程不存在)"
|
||||
rm -f "$PID_FILE"
|
||||
fi
|
||||
44
hzhub-ai/stop.sh
Executable file
@@ -0,0 +1,44 @@
|
||||
#!/bin/bash
|
||||
# HZHub AI 后端服务停止脚本
|
||||
# 功能:停止后台运行的 hzhub-ai Spring Boot 服务
|
||||
|
||||
PROJECT_NAME="hzhub-ai"
|
||||
PROJECT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
PID_FILE="$PROJECT_DIR/.pid"
|
||||
|
||||
echo "========================================="
|
||||
echo "停止 $PROJECT_NAME 后端服务"
|
||||
echo "========================================="
|
||||
|
||||
# 检查PID文件是否存在
|
||||
if [ ! -f "$PID_FILE" ]; then
|
||||
echo "⚠️ 未找到PID文件,服务可能未运行"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
PID=$(cat "$PID_FILE")
|
||||
|
||||
# 检查进程是否存在
|
||||
if ! ps -p "$PID" > /dev/null 2>&1; then
|
||||
echo "⚠️ 进程不存在 (PID: $PID)"
|
||||
rm -f "$PID_FILE"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# 停止进程
|
||||
echo "🛑 正在停止服务 (PID: $PID)..."
|
||||
kill "$PID"
|
||||
|
||||
# 等待进程结束
|
||||
sleep 3
|
||||
|
||||
# 检查进程是否已停止
|
||||
if ps -p "$PID" > /dev/null 2>&1; then
|
||||
echo "⚠️ 进程未响应,强制终止..."
|
||||
kill -9 "$PID"
|
||||
sleep 2
|
||||
fi
|
||||
|
||||
# 清理PID文件
|
||||
rm -f "$PID_FILE"
|
||||
echo "✅ 服务已停止"
|
||||
@@ -84,7 +84,7 @@ services:
|
||||
- "6039:6039"
|
||||
environment:
|
||||
SPRING_PROFILES_ACTIVE: dev
|
||||
SPRING_DATASOURCE_DYNAMIC_DATASOURCE_MASTER_URL: jdbc:mysql://hzhub-mysql:3306/ruoyi_ai?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&autoReconnect=true&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true
|
||||
SPRING_DATASOURCE_DYNAMIC_DATASOURCE_MASTER_URL: jdbc:mysql://hzhub-mysql:3306/hzhub?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&autoReconnect=true&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true
|
||||
SPRING_DATASOURCE_DYNAMIC_DATASOURCE_MASTER_USERNAME: root
|
||||
SPRING_DATASOURCE_DYNAMIC_DATASOURCE_MASTER_PASSWORD: hzhub123
|
||||
SPRING_DATA_REDIS_HOST: hzhub-redis
|
||||
@@ -127,12 +127,12 @@ services:
|
||||
networks:
|
||||
- hzhub-network
|
||||
|
||||
# hzhub-portal-company (公司门户)
|
||||
hzhub-portal-company:
|
||||
# hzhub-portal-employee (员工门户)
|
||||
hzhub-portal-employee:
|
||||
build:
|
||||
context: ../hzhub-portal-company
|
||||
context: ../hzhub-portal-employee
|
||||
dockerfile: Dockerfile
|
||||
container_name: hzhub-portal-company
|
||||
container_name: hzhub-portal-employee
|
||||
ports:
|
||||
- "5137:5137"
|
||||
environment:
|
||||
|
||||
@@ -1,150 +0,0 @@
|
||||
# HZHub-AI 用户端
|
||||
|
||||
<div align="center">
|
||||
|
||||
<img src="https://github.com/ageerle/hzhub-ai/raw/main/docs/image/logo.png" alt="HZHub AI Logo" width="120" height="120">
|
||||
|
||||
### 企业级AI助手平台 - 用户前端
|
||||
|
||||
*HZHub-AI 的用户前端,提供 AI 对话、智能体交互、知识库问答等功能*
|
||||
|
||||
**[在线体验](https://web.pandarobot.chat)** | **[后端服务](https://github.com/ageerle/hzhub-ai)** | **[管理后台](https://github.com/ageerle/hzhub-admin)**
|
||||
|
||||
</div>
|
||||
|
||||
## 技术栈
|
||||
|
||||
- **框架**: Vue 3 + TypeScript
|
||||
- **UI组件**: Ant Design Vue
|
||||
- **状态管理**: Pinia
|
||||
- **构建工具**: Vite
|
||||
|
||||
## Docker 部署
|
||||
|
||||
本用户端支持两种 Docker 部署方式:
|
||||
|
||||
### 方式一:一键启动所有服务(推荐)
|
||||
|
||||
使用 `docker-compose-all.yaml` 可以一键启动所有服务(包括后端、管理端、用户端及依赖服务):
|
||||
|
||||
```bash
|
||||
# 克隆后端仓库
|
||||
git clone https://github.com/ageerle/hzhub-ai.git
|
||||
cd hzhub-ai
|
||||
|
||||
# 启动所有服务(从镜像仓库拉取预构建镜像)
|
||||
docker-compose -f docker-compose-all.yaml up -d
|
||||
|
||||
# 访问用户端
|
||||
# 地址: http://localhost:25137
|
||||
# 账号: admin / admin123
|
||||
```
|
||||
|
||||
### 方式二:分步部署(源码编译)
|
||||
|
||||
如果您需要从源码构建,请按照以下步骤操作:
|
||||
|
||||
#### 第一步:部署后端服务
|
||||
|
||||
```bash
|
||||
# 进入后端项目目录
|
||||
cd hzhub-ai
|
||||
|
||||
# 启动后端服务(源码编译构建)
|
||||
docker-compose up -d --build
|
||||
|
||||
# 等待后端服务启动完成
|
||||
docker-compose logs -f backend
|
||||
```
|
||||
|
||||
#### 第二步:部署用户端
|
||||
|
||||
```bash
|
||||
# 进入用户端项目目录
|
||||
cd hzhub-portal
|
||||
|
||||
# 构建并启动用户端
|
||||
docker-compose up -d --build
|
||||
|
||||
# 访问用户端
|
||||
# 地址: http://localhost:5137
|
||||
```
|
||||
|
||||
#### 第三步:部署管理端(可选)
|
||||
|
||||
```bash
|
||||
# 进入管理端项目目录
|
||||
cd hzhub-admin
|
||||
|
||||
# 构建并启动管理端
|
||||
docker-compose up -d --build
|
||||
|
||||
# 访问管理端
|
||||
# 地址: http://localhost:5666
|
||||
```
|
||||
|
||||
### 服务端口说明
|
||||
|
||||
| 服务 | 端口 | 说明 |
|
||||
|------|------|------|
|
||||
| 用户端 | 5137 | 用户前端访问地址 |
|
||||
| 管理端 | 5666 | 管理后台访问地址 |
|
||||
| 后端服务 | 6039 | 后端 API 服务 |
|
||||
| MySQL | 23306 | 数据库服务 |
|
||||
| Redis | 6379 | 缓存服务 |
|
||||
| Weaviate | 28080 | 向量数据库 |
|
||||
| MinIO | 9000/9090 | 对象存储 |
|
||||
|
||||
### 镜像仓库
|
||||
|
||||
所有镜像托管在阿里云容器镜像服务:
|
||||
|
||||
```
|
||||
crpi-31mraxd99y2gqdgr.cn-beijing.personal.cr.aliyuncs.com/ruoyi_ai
|
||||
```
|
||||
|
||||
可用镜像:
|
||||
- `mysql:v3` - MySQL 数据库(包含初始化 SQL)
|
||||
- `redis:6.2` - Redis 缓存
|
||||
- `weaviate:1.30.0` - 向量数据库
|
||||
- `minio:latest` - 对象存储
|
||||
- `hzhub-ai-backend:latest` - 后端服务
|
||||
- `hzhub-ai-admin:latest` - 管理端前端
|
||||
- `hzhub-ai-web:latest` - 用户端前端
|
||||
|
||||
## 本地开发
|
||||
|
||||
```bash
|
||||
# 安装依赖
|
||||
pnpm install
|
||||
|
||||
# 启动开发服务器
|
||||
pnpm dev
|
||||
|
||||
# 构建生产版本
|
||||
pnpm build
|
||||
```
|
||||
|
||||
## 常见问题
|
||||
|
||||
**Q: 用户端无法连接后端服务?**
|
||||
|
||||
A: 请确保后端服务已启动,并检查环境变量 `UPSTREAM_URL` 配置是否正确。
|
||||
|
||||
**Q: 一键启动和分步部署有什么区别?**
|
||||
|
||||
A: 一键启动使用预构建的镜像,部署速度快;分步部署从源码编译,适合需要自定义修改的场景。
|
||||
|
||||
## 开源协议
|
||||
|
||||
本项目采用 **MIT 开源协议**,详情请查看 [license](license) 文件。
|
||||
|
||||
---
|
||||
|
||||
<div align="center">
|
||||
|
||||
**[⭐ 点个Star支持一下](https://github.com/ageerle/hzhub-portal)** • **[Fork 开始贡献](https://github.com/ageerle/hzhub-portal/fork)**
|
||||
|
||||
*用 ❤️ 打造,由 HZHub AI 开源社区维护*
|
||||
|
||||
</div>
|
||||
@@ -1,15 +0,0 @@
|
||||
// 全局默认配置项
|
||||
// 首页地址[默认]
|
||||
export const HOME_URL: string = '/chat';
|
||||
|
||||
// 默认主题颜色
|
||||
export const DEFAULT_THEME_COLOR: string = '#2992FF';
|
||||
|
||||
// 折叠阈值
|
||||
export const COLLAPSE_THRESHOLD: number = 600;
|
||||
|
||||
// 左侧菜单宽度
|
||||
export const SIDE_BAR_WIDTH: number = 280;
|
||||
|
||||
// 路由白名单地址[本地存在的路由 staticRouter.ts 中]
|
||||
export const ROUTER_WHITE_LIST: string[] = ['/chat', '/chat/not_login', '/403', '/404'];
|
||||
@@ -1,70 +0,0 @@
|
||||
<!-- 纵向布局作为基础布局 -->
|
||||
<script setup lang="ts">
|
||||
import { useSafeArea } from '@/hooks/useSafeArea';
|
||||
import { useWindowWidthObserver } from '@/hooks/useWindowWidthObserver';
|
||||
import Aside from '@/layouts/components/Aside/index.vue';
|
||||
import Header from '@/layouts/components/Header/index.vue';
|
||||
import Main from '@/layouts/components/Main/index.vue';
|
||||
import { useDesignStore } from '@/stores';
|
||||
|
||||
const designStore = useDesignStore();
|
||||
|
||||
const isCollapse = computed(() => designStore.isCollapse);
|
||||
|
||||
/* 是否移入了安全区 */
|
||||
useSafeArea({
|
||||
direction: 'left',
|
||||
size: 50,
|
||||
onChange(isInSafeArea) {
|
||||
// 设置悬停为 true
|
||||
designStore.isSafeAreaHover = isInSafeArea;
|
||||
},
|
||||
enabled: isCollapse, // 折叠才开启监听
|
||||
});
|
||||
|
||||
/** 监听窗口大小变化,折叠侧边栏 */
|
||||
useWindowWidthObserver();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-container class="layout-container">
|
||||
<el-header class="layout-header">
|
||||
<Header />
|
||||
</el-header>
|
||||
<el-container class="layout-container-main">
|
||||
<Aside />
|
||||
<el-main class="layout-main">
|
||||
<!-- 路由页面 -->
|
||||
<Main />
|
||||
</el-main>
|
||||
</el-container>
|
||||
</el-container>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.layout-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
.layout-header {
|
||||
padding: 0;
|
||||
}
|
||||
.layout-main {
|
||||
height: 100%;
|
||||
padding: 0;
|
||||
}
|
||||
.layout-container-main {
|
||||
margin-left: var(--sidebar-left-container-default-width, 0);
|
||||
transition: margin-left 0.3s ease;
|
||||
}
|
||||
}
|
||||
|
||||
/** 去除菜单右侧边框 */
|
||||
.el-menu {
|
||||
border-right: none;
|
||||
}
|
||||
.layout-scrollbar {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -1,643 +0,0 @@
|
||||
<!-- Aside 侧边栏 -->
|
||||
<script setup lang="ts">
|
||||
import type { ConversationItem } from 'vue-element-plus-x/types/Conversations';
|
||||
import type { ChatSessionVo } from '@/api/session/types';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { get_session } from '@/api';
|
||||
import logo from '@/assets/images/logo.png';
|
||||
import Collapse from '@/layouts/components/Header/components/Collapse.vue';
|
||||
import { useDesignStore } from '@/stores';
|
||||
import { useSessionStore } from '@/stores/modules/session';
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const designStore = useDesignStore();
|
||||
const sessionStore = useSessionStore();
|
||||
|
||||
const sessionId = computed(() => route.params?.id);
|
||||
const conversationsList = computed(() => sessionStore.sessionList);
|
||||
const loadMoreLoading = computed(() => sessionStore.isLoadingMore);
|
||||
const active = ref<string | undefined>();
|
||||
|
||||
// 固定应用列表
|
||||
const appList = ref([
|
||||
{
|
||||
id: 'ai-chat',
|
||||
name: 'AI 对话',
|
||||
icon: 'ChatLineRound',
|
||||
route: '/chat',
|
||||
},
|
||||
{
|
||||
id: 'ai-image',
|
||||
name: 'AI 画图',
|
||||
icon: 'Picture',
|
||||
route: '/ai-image',
|
||||
},
|
||||
{
|
||||
id: 'ai-video',
|
||||
name: 'AI 视频',
|
||||
icon: 'VideoCamera',
|
||||
route: '/ai-video',
|
||||
},
|
||||
{
|
||||
id: 'ai-ppt',
|
||||
name: 'AI PPT',
|
||||
icon: 'Document',
|
||||
route: '/ai-ppt',
|
||||
},
|
||||
]);
|
||||
|
||||
const activeApp = ref('ai-chat');
|
||||
const searchKeyword = ref('');
|
||||
const activeFooterBtn = ref<'agent' | 'knowledge' | null>(null);
|
||||
|
||||
// 切换应用
|
||||
function handleAppClick(app: typeof appList.value[0]) {
|
||||
activeApp.value = app.id;
|
||||
// 这里可以添加路由跳转逻辑
|
||||
// router.push(app.route);
|
||||
}
|
||||
|
||||
// 智能体中心
|
||||
function handleAgentCenter() {
|
||||
activeFooterBtn.value = activeFooterBtn.value === 'agent' ? null : 'agent';
|
||||
console.log('打开智能体中心');
|
||||
// router.push('/agent-center');
|
||||
}
|
||||
|
||||
// 知识库管理
|
||||
function handleKnowledgeBase() {
|
||||
activeFooterBtn.value = activeFooterBtn.value === 'knowledge' ? null : 'knowledge';
|
||||
console.log('打开知识库管理');
|
||||
// router.push('/knowledge-base');
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
// 默认选中 AI 对话应用
|
||||
activeApp.value = 'ai-chat';
|
||||
|
||||
// 获取会话列表
|
||||
console.log('[Aside.onMounted] 开始获取会话列表');
|
||||
await sessionStore.requestSessionList();
|
||||
console.log('[Aside.onMounted] 获取会话列表完成,conversationsList.length:', conversationsList.value.length);
|
||||
console.log('[Aside.onMounted] conversationsList:', conversationsList.value);
|
||||
|
||||
// 高亮最新会话
|
||||
if (conversationsList.value.length > 0 && sessionId.value) {
|
||||
console.log('[Aside.onMounted] 获取当前选中会话,sessionId:', sessionId.value);
|
||||
const currentSessionRes = await get_session(`${sessionId.value}`);
|
||||
// 通过 ID 查询详情,设置当前会话 (因为有分页)
|
||||
sessionStore.setCurrentSession(currentSessionRes.data);
|
||||
}
|
||||
});
|
||||
|
||||
watch(
|
||||
() => sessionStore.currentSession,
|
||||
(newValue) => {
|
||||
active.value = newValue ? `${newValue.id}` : undefined;
|
||||
},
|
||||
);
|
||||
|
||||
// 创建会话
|
||||
function handleCreatChat() {
|
||||
// 创建会话, 跳转到默认聊天
|
||||
sessionStore.createSessionBtn();
|
||||
}
|
||||
|
||||
// 切换会话
|
||||
function handleChange(item: ConversationItem<ChatSessionVo>) {
|
||||
sessionStore.setCurrentSession(item);
|
||||
router.replace({
|
||||
name: 'chatWithId',
|
||||
params: {
|
||||
id: item.id,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// 处理组件触发的加载更多事件
|
||||
async function handleLoadMore() {
|
||||
if (!sessionStore.hasMore)
|
||||
return; // 无更多数据时不加载
|
||||
await sessionStore.loadMoreSessions();
|
||||
}
|
||||
|
||||
// 右键菜单
|
||||
function handleMenuCommand(command: string, item: ConversationItem<ChatSessionVo>) {
|
||||
switch (command) {
|
||||
case 'delete':
|
||||
ElMessageBox.confirm('删除后,聊天记录将不可恢复。', '确定删除对话?', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
confirmButtonClass: 'el-button--danger',
|
||||
cancelButtonClass: 'el-button--info',
|
||||
roundButton: true,
|
||||
autofocus: false,
|
||||
})
|
||||
.then(async () => {
|
||||
// 删除会话
|
||||
await sessionStore.deleteSessions([item.id!]);
|
||||
|
||||
// 检查删除的是否为当前选中会话,若是则返回默认页
|
||||
nextTick(() => {
|
||||
if (item.id === active.value) {
|
||||
// 如果删除当前会话,返回到默认页面
|
||||
sessionStore.createSessionBtn();
|
||||
}
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
// 取消删除
|
||||
});
|
||||
break;
|
||||
case 'rename':
|
||||
ElMessageBox.prompt('', '编辑对话名称', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
inputErrorMessage: '请输入对话名称',
|
||||
confirmButtonClass: 'el-button--primary',
|
||||
cancelButtonClass: 'el-button--info',
|
||||
roundButton: true,
|
||||
inputValue: item.sessionTitle, // 设置默认值
|
||||
autofocus: false,
|
||||
inputValidator: (value) => {
|
||||
if (!value) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
}).then(({ value }) => {
|
||||
sessionStore
|
||||
.updateSession({
|
||||
id: item.id!,
|
||||
sessionTitle: value,
|
||||
sessionContent: item.sessionContent,
|
||||
})
|
||||
.then(() => {
|
||||
ElMessage({
|
||||
type: 'success',
|
||||
message: '修改成功',
|
||||
});
|
||||
nextTick(() => {
|
||||
// 如果是当前会话,则更新当前选中会话信息
|
||||
if (sessionStore.currentSession?.id === item.id) {
|
||||
sessionStore.setCurrentSession({
|
||||
...item,
|
||||
sessionTitle: value,
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="aside-container"
|
||||
:class="{
|
||||
'aside-container-suspended': designStore.isSafeAreaHover,
|
||||
'aside-container-collapse': designStore.isCollapse,
|
||||
// 折叠且未激活悬停时添加 no-delay 类
|
||||
'no-delay': designStore.isCollapse && !designStore.hasActivatedHover,
|
||||
}"
|
||||
>
|
||||
<div class="aside-wrapper">
|
||||
<div v-if="!designStore.isCollapse" class="aside-header">
|
||||
<div class="flex items-center gap-8px hover:cursor-pointer" @click="handleCreatChat">
|
||||
<el-image :src="logo" alt="logo" fit="cover" class="logo-img" />
|
||||
<span class="logo-text max-w-150px text-overflow">HZHub-AI</span>
|
||||
</div>
|
||||
<Collapse class="ml-auto" />
|
||||
</div>
|
||||
|
||||
<div class="aside-body">
|
||||
<!-- 搜索框 -->
|
||||
<div class="search-wrapper">
|
||||
<el-input
|
||||
v-model="searchKeyword"
|
||||
placeholder="搜索对话"
|
||||
clearable
|
||||
class="search-input"
|
||||
>
|
||||
<template #prefix>
|
||||
<el-icon>
|
||||
<Search />
|
||||
</el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
</div>
|
||||
|
||||
<!-- 应用入口区域 -->
|
||||
<div class="app-list-wrapper">
|
||||
<div class="app-list-title">应用</div>
|
||||
<div class="app-list">
|
||||
<div
|
||||
v-for="app in appList"
|
||||
:key="app.id"
|
||||
class="app-item"
|
||||
:class="{ 'app-item-active': activeApp === app.id }"
|
||||
@click="handleAppClick(app)"
|
||||
>
|
||||
<el-icon class="app-icon">
|
||||
<component :is="app.icon" />
|
||||
</el-icon>
|
||||
<span class="app-name">{{ app.name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 分割线 -->
|
||||
<div class="divider" />
|
||||
|
||||
<div class="aside-content">
|
||||
<div v-if="conversationsList.length > 0" class="conversations-wrap overflow-hidden">
|
||||
<Conversations
|
||||
v-model:active="active"
|
||||
:items="conversationsList"
|
||||
:label-max-width="200"
|
||||
:show-tooltip="true"
|
||||
:tooltip-offset="60"
|
||||
show-built-in-menu
|
||||
groupable
|
||||
row-key="id"
|
||||
label-key="sessionTitle"
|
||||
tooltip-placement="right"
|
||||
:load-more="handleLoadMore"
|
||||
:load-more-loading="loadMoreLoading"
|
||||
:items-style="{
|
||||
marginLeft: '8px',
|
||||
userSelect: 'none',
|
||||
borderRadius: '10px',
|
||||
padding: '8px 12px',
|
||||
}"
|
||||
:items-active-style="{
|
||||
backgroundColor: '#fff',
|
||||
boxShadow: '0 1px 2px rgba(0, 0, 0, 0.05)',
|
||||
color: 'rgba(0, 0, 0, 0.85)',
|
||||
}"
|
||||
:items-hover-style="{
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.04)',
|
||||
}"
|
||||
@menu-command="handleMenuCommand"
|
||||
@change="handleChange"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<el-empty v-else class="h-full flex-center" description="暂无对话记录" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 底部悬浮按钮 -->
|
||||
<div class="aside-footer">
|
||||
<div class="footer-btn" :class="{ active: activeFooterBtn === 'agent' }" @click="handleAgentCenter">
|
||||
<el-icon class="footer-btn-icon">
|
||||
<Avatar />
|
||||
</el-icon>
|
||||
<span class="footer-btn-text">智能体中心</span>
|
||||
</div>
|
||||
<div class="footer-divider" />
|
||||
<div class="footer-btn" :class="{ active: activeFooterBtn === 'knowledge' }" @click="handleKnowledgeBase">
|
||||
<el-icon class="footer-btn-icon">
|
||||
<FolderOpened />
|
||||
</el-icon>
|
||||
<span class="footer-btn-text">知识库管理</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
// 基础样式
|
||||
.aside-container {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 11;
|
||||
width: var(--sidebar-default-width);
|
||||
height: 100%;
|
||||
pointer-events: auto;
|
||||
background-color: var(--sidebar-background-color);
|
||||
border-right: 0.5px solid var(--s-color-border-tertiary, rgb(0 0 0 / 8%));
|
||||
.aside-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
|
||||
// 侧边栏头部样式
|
||||
.aside-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 36px;
|
||||
margin: 10px 12px 0;
|
||||
.logo-img {
|
||||
box-sizing: border-box;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
padding: 4px;
|
||||
overflow: hidden;
|
||||
background-color: #ffffff;
|
||||
border-radius: 50%;
|
||||
img {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
.logo-text {
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
color: rgb(0 0 0 / 85%);
|
||||
transform: skewX(-2deg);
|
||||
}
|
||||
}
|
||||
|
||||
// 侧边栏内容样式
|
||||
.aside-body {
|
||||
// 搜索框样式
|
||||
.search-wrapper {
|
||||
padding: 16px 12px 12px;
|
||||
.search-input {
|
||||
:deep(.el-input__wrapper) {
|
||||
padding: 8px 12px;
|
||||
background-color: rgb(0 0 0 / 4%);
|
||||
border-radius: 10px;
|
||||
box-shadow: none;
|
||||
transition: all 0.2s ease;
|
||||
&:hover {
|
||||
background-color: rgb(0 0 0 / 6%);
|
||||
}
|
||||
&.is-focus {
|
||||
background-color: #ffffff;
|
||||
box-shadow: 0 0 0 1px rgb(0 87 255 / 20%);
|
||||
}
|
||||
}
|
||||
:deep(.el-input__inner) {
|
||||
font-size: 14px;
|
||||
color: rgb(0 0 0 / 85%);
|
||||
&::placeholder {
|
||||
color: rgb(0 0 0 / 45%);
|
||||
}
|
||||
}
|
||||
:deep(.el-input__prefix) {
|
||||
color: rgb(0 0 0 / 45%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 应用列表区域
|
||||
.app-list-wrapper {
|
||||
padding: 16px 12px 0;
|
||||
.app-list-title {
|
||||
padding-left: 6px;
|
||||
margin-bottom: 8px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: rgb(0 0 0 / 45%);
|
||||
}
|
||||
.app-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
.app-item {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
padding: 10px 12px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
border-radius: 10px;
|
||||
transition: all 0.2s ease;
|
||||
&:hover {
|
||||
background-color: rgb(0 0 0 / 4%);
|
||||
}
|
||||
&.app-item-active {
|
||||
color: rgb(0 0 0 / 85%);
|
||||
background-color: #ffffff;
|
||||
box-shadow: 0 1px 2px rgb(0 0 0 / 5%);
|
||||
.app-icon {
|
||||
color: #0057ff;
|
||||
}
|
||||
}
|
||||
.app-icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
font-size: 20px;
|
||||
color: rgb(0 0 0 / 45%);
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
.app-name {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: rgb(0 0 0 / 85%);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 分割线
|
||||
.divider {
|
||||
height: 1px;
|
||||
margin: 12px;
|
||||
background-color: rgb(0 0 0 / 6%);
|
||||
}
|
||||
.creat-chat-btn-wrapper {
|
||||
padding: 0 12px;
|
||||
.creat-chat-btn {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
padding: 8px 6px;
|
||||
margin-top: 16px;
|
||||
margin-bottom: 6px;
|
||||
color: #0057ff;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
background-color: rgb(0 87 255 / 6%);
|
||||
border: 1px solid rgb(0 102 255 / 15%);
|
||||
border-radius: 12px;
|
||||
&:hover {
|
||||
background-color: rgb(0 87 255 / 12%);
|
||||
}
|
||||
.creat-chat-text {
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
line-height: 22px;
|
||||
}
|
||||
.add-icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
font-size: 16px;
|
||||
}
|
||||
.svg-icon {
|
||||
height: 24px;
|
||||
margin-left: auto;
|
||||
color: rgb(0 87 255 / 30%);
|
||||
}
|
||||
}
|
||||
}
|
||||
.aside-content {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
|
||||
// 会话列表高度-基础样式(调整高度以适应应用区域和底部按钮)
|
||||
.conversations-wrap {
|
||||
height: calc(100vh - 400px);
|
||||
.label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 底部悬浮按钮样式
|
||||
.aside-footer {
|
||||
position: absolute;
|
||||
right: 12px;
|
||||
bottom: 20px;
|
||||
left: 12px;
|
||||
z-index: 20;
|
||||
display: flex;
|
||||
gap: 0;
|
||||
align-items: center;
|
||||
padding: 8px;
|
||||
background-color: #f3f4f6;
|
||||
border-radius: 10px;
|
||||
.footer-btn {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 8px 4px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
background-color: transparent;
|
||||
border-radius: 8px;
|
||||
transition: all 0.2s ease;
|
||||
&:hover {
|
||||
background-color: rgb(0 0 0 / 4%);
|
||||
}
|
||||
&.active {
|
||||
background-color: rgb(0 87 255 / 8%);
|
||||
.footer-btn-icon {
|
||||
color: #0057ff;
|
||||
}
|
||||
.footer-btn-text {
|
||||
color: #0057ff;
|
||||
}
|
||||
}
|
||||
.footer-btn-icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
font-size: 20px;
|
||||
color: rgb(0 0 0 / 65%);
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
.footer-btn-text {
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
line-height: 1.2;
|
||||
color: rgb(0 0 0 / 65%);
|
||||
text-align: center;
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
}
|
||||
.footer-divider {
|
||||
flex-shrink: 0;
|
||||
width: 1px;
|
||||
height: 40px;
|
||||
background-color: rgb(0 0 0 / 10%);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 折叠样式
|
||||
.aside-container-collapse {
|
||||
position: absolute;
|
||||
top: 54px;
|
||||
z-index: 22;
|
||||
height: auto;
|
||||
max-height: calc(100% - 110px);
|
||||
padding-bottom: 12px;
|
||||
overflow: hidden;
|
||||
|
||||
/* 禁用悬停事件 */
|
||||
pointer-events: none;
|
||||
border: 1px solid rgb(0 0 0 / 8%);
|
||||
border-radius: 15px;
|
||||
box-shadow:
|
||||
0 10px 20px 0 rgb(0 0 0 / 10%),
|
||||
0 0 1px 0 rgb(0 0 0 / 15%);
|
||||
opacity: 0;
|
||||
|
||||
// 指定样式过渡
|
||||
|
||||
// 向左偏移一个宽度
|
||||
transform: translateX(-100%);
|
||||
transition: opacity 0.3s ease 0.3s, transform 0.3s ease 0.3s;
|
||||
|
||||
/* 新增:未激活悬停时覆盖延迟 */
|
||||
&.no-delay {
|
||||
transition-delay: 0s, 0s;
|
||||
}
|
||||
}
|
||||
|
||||
// 悬停样式
|
||||
.aside-container-collapse:hover,
|
||||
.aside-container-collapse.aside-container-suspended {
|
||||
height: auto;
|
||||
max-height: calc(100% - 110px);
|
||||
padding-bottom: 12px;
|
||||
overflow: hidden;
|
||||
pointer-events: auto;
|
||||
border: 1px solid rgb(0 0 0 / 8%);
|
||||
border-radius: 15px;
|
||||
box-shadow:
|
||||
0 10px 20px 0 rgb(0 0 0 / 10%),
|
||||
0 0 1px 0 rgb(0 0 0 / 15%);
|
||||
|
||||
// 直接在这里写悬停时的样式(与 aside-container-suspended 一致)
|
||||
opacity: 1;
|
||||
|
||||
// 过渡动画沿用原有设置
|
||||
transform: translateX(15px);
|
||||
transition: opacity 0.3s ease 0s, transform 0.3s ease 0s;
|
||||
|
||||
// 会话列表高度-悬停样式
|
||||
.conversations-wrap {
|
||||
height: calc(100vh - 155px) !important;
|
||||
}
|
||||
}
|
||||
|
||||
// 样式穿透
|
||||
:deep() {
|
||||
// 会话列表背景色
|
||||
.conversations-list {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
// 群组标题样式 和 侧边栏菜单背景色一致
|
||||
.conversation-group-title {
|
||||
padding-left: 12px !important;
|
||||
font-weight: 700 !important;
|
||||
color: rgb(0 0 0 / 85%) !important;
|
||||
background-color: var(--sidebar-background-color) !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,100 +0,0 @@
|
||||
<!-- Header 头部 -->
|
||||
<script setup lang="ts">
|
||||
import { onKeyStroke } from '@vueuse/core';
|
||||
import { SIDE_BAR_WIDTH } from '@/config/index';
|
||||
import { useDesignStore, useUserStore } from '@/stores';
|
||||
import { useSessionStore } from '@/stores/modules/session';
|
||||
import Avatar from './components/Avatar.vue';
|
||||
import Collapse from './components/Collapse.vue';
|
||||
import CreateChat from './components/CreateChat.vue';
|
||||
import LoginBtn from './components/LoginBtn.vue';
|
||||
import TitleEditing from './components/TitleEditing.vue';
|
||||
|
||||
const userStore = useUserStore();
|
||||
const designStore = useDesignStore();
|
||||
const sessionStore = useSessionStore();
|
||||
|
||||
const currentSession = computed(() => sessionStore.currentSession);
|
||||
|
||||
onMounted(() => {
|
||||
// 全局设置侧边栏默认宽度 (这个是不变的,一开始就设置)
|
||||
document.documentElement.style.setProperty(`--sidebar-default-width`, `${SIDE_BAR_WIDTH}px`);
|
||||
if (designStore.isCollapse) {
|
||||
document.documentElement.style.setProperty(`--sidebar-left-container-default-width`, ``);
|
||||
}
|
||||
else {
|
||||
document.documentElement.style.setProperty(
|
||||
`--sidebar-left-container-default-width`,
|
||||
`${SIDE_BAR_WIDTH}px`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// 定义 Ctrl+K 的处理函数
|
||||
function handleCtrlK(event: KeyboardEvent) {
|
||||
event.preventDefault(); // 防止默认行为
|
||||
sessionStore.createSessionBtn();
|
||||
}
|
||||
|
||||
// 设置全局的键盘按键监听
|
||||
onKeyStroke(event => event.ctrlKey && event.key.toLowerCase() === 'k', handleCtrlK, {
|
||||
passive: false,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="header-container">
|
||||
<div class="header-box relative z-10 top-0 left-0 right-0">
|
||||
<div class="absolute left-0 right-0 top-0 bottom-0 flex items-center flex-row">
|
||||
<div
|
||||
class="overflow-hidden flex h-full items-center flex-row flex-1 w-fit flex-shrink-0 min-w-0"
|
||||
>
|
||||
<div class="w-full flex items-center flex-row">
|
||||
<!-- 左边 -->
|
||||
<div
|
||||
v-if="designStore.isCollapse"
|
||||
class="left-box flex h-full items-center pl-20px gap-12px flex-shrink-0 flex-row"
|
||||
>
|
||||
<Collapse />
|
||||
<CreateChat />
|
||||
<div v-if="currentSession" class="w-0.5px h-30px bg-[rgba(217,217,217)]" />
|
||||
</div>
|
||||
|
||||
<!-- 中间 -->
|
||||
<div class="middle-box flex-1 min-w-0 ml-12px">
|
||||
<TitleEditing />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右边 -->
|
||||
<div class="right-box flex h-full items-center pr-20px flex-shrink-0 mr-auto flex-row">
|
||||
<Avatar v-show="userStore.token" />
|
||||
<LoginBtn v-show="!userStore.token" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.header-container {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
height: fit-content;
|
||||
.header-box {
|
||||
width: 100%;
|
||||
width: calc(
|
||||
100% - var(--sidebar-left-container-default-width, 0px) - var(
|
||||
--sidebar-right-container-default-width,
|
||||
0px
|
||||
)
|
||||
);
|
||||
height: var(--header-container-default-heigth);
|
||||
margin: 0 var(--sidebar-right-container-default-width, 0) 0
|
||||
var(--sidebar-left-container-default-width, 0);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,78 +0,0 @@
|
||||
import type { RouteRecordRaw } from 'vue-router';
|
||||
import { HOME_URL } from '@/config';
|
||||
|
||||
// LayoutRouter[布局路由]
|
||||
export const layoutRouter: RouteRecordRaw[] = [
|
||||
{
|
||||
path: '/',
|
||||
redirect: HOME_URL,
|
||||
component: () => import('@/layouts/index.vue'),
|
||||
children: [
|
||||
{
|
||||
path: HOME_URL,
|
||||
name: 'chat',
|
||||
component: () => import('@/pages/chat/index.vue'),
|
||||
meta: {
|
||||
// title: '通用聊天页面',
|
||||
isDefaultChat: true,
|
||||
icon: 'HomeFilled',
|
||||
// isHide: '1', // 是否在菜单中隐藏[0是,1否] 预留
|
||||
// isKeepAlive: '0', // 是否缓存路由数据[0是,1否] 预留
|
||||
// isFull: '1', // 是否全屏[0是,1否] 预留
|
||||
// enName: "Master Station", // 英文名称 预留
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/chat/:id',
|
||||
name: 'chatWithId',
|
||||
component: () => import('@/pages/chat/index.vue'),
|
||||
meta: {
|
||||
// title: '带 ID 的聊天页面',
|
||||
isDefaultChat: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
// staticRouter[静态路由] 预留
|
||||
export const staticRouter: RouteRecordRaw[] = [];
|
||||
|
||||
// errorRouter (错误页面路由)
|
||||
export const errorRouter = [
|
||||
{
|
||||
path: '/403',
|
||||
name: '403',
|
||||
component: () => import('@/pages/error/403.vue'),
|
||||
meta: {
|
||||
title: '403页面',
|
||||
enName: '403 Page', // 英文名称
|
||||
icon: 'QuestionFilled', // 菜单图标
|
||||
isHide: '1', // 代表路由在菜单中是否隐藏,是否隐藏[0隐藏,1显示]
|
||||
isLink: '1', // 是否外链[有值则是外链]
|
||||
isKeepAlive: '0', // 是否缓存路由数据[0是,1否]
|
||||
isFull: '1', // 是否缓存全屏[0是,1否]
|
||||
isAffix: '1', // 是否缓存固定路由[0是,1否]
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/404',
|
||||
name: '404',
|
||||
component: () => import('@/pages/error/404.vue'),
|
||||
meta: {
|
||||
title: '404页面',
|
||||
enName: '404 Page', // 英文名称
|
||||
icon: 'CircleCloseFilled', // 菜单图标
|
||||
isHide: '1', // 代表路由在菜单中是否隐藏,是否隐藏[0隐藏,1显示]
|
||||
isLink: '1', // 是否外链[有值则是外链]
|
||||
isKeepAlive: '0', // 是否缓存路由数据[0是,1否]
|
||||
isFull: '1', // 是否缓存全屏[0是,1否]
|
||||
isAffix: '1', // 是否缓存固定路由[0是,1否]
|
||||
},
|
||||
},
|
||||
// 找不到path将跳转404页面
|
||||
{
|
||||
path: '/:pathMatch(.*)*',
|
||||
component: () => import('@/pages/error/404.vue'),
|
||||
},
|
||||
];
|
||||
@@ -1,9 +0,0 @@
|
||||
@use './media';
|
||||
@use './btn-style';
|
||||
@use 'reset-css';
|
||||
@use './element-plus';
|
||||
@use './elx';
|
||||
@use './markdown';
|
||||
body{
|
||||
overflow: hidden;
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
:root {
|
||||
/* 头部高度 */
|
||||
--header-container-default-heigth: 56px;
|
||||
|
||||
/* 左侧侧边栏背景色 */
|
||||
--sidebar-background-color: #f3f4f6;
|
||||
|
||||
|
||||
/* 登录弹框变量 */
|
||||
--login-dialog-width: 738px;
|
||||
--login-dialog-height: 416px;
|
||||
--login-dialog-padding: 0px;
|
||||
--login-dialog-section-padding: 0px;
|
||||
--login-dialog-border-radius: 24px;
|
||||
--login-dialog-mode-toggle-color: #409eff;
|
||||
--login-dialog-logo-background: #ffffff;
|
||||
--login-dialog-logo-text-color: #191919;
|
||||
|
||||
|
||||
|
||||
/* 覆盖 element-plus 样式 */
|
||||
--el-border-radius-base: 12px !important;
|
||||
--el-messagebox-border-radius: 16px !important;
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
import type { HookFetchPlugin } from 'hook-fetch';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import hookFetch from 'hook-fetch';
|
||||
import { sseTextDecoderPlugin } from 'hook-fetch/plugins';
|
||||
import router from '@/routers';
|
||||
import { useUserStore } from '@/stores';
|
||||
|
||||
interface BaseResponse {
|
||||
code: number;
|
||||
data: never;
|
||||
msg: string;
|
||||
rows: never;
|
||||
}
|
||||
|
||||
export const request = hookFetch.create<BaseResponse, 'data' | 'rows'>({
|
||||
baseURL: import.meta.env.VITE_API_URL,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
plugins: [sseTextDecoderPlugin({ json: true, prefix: 'data:' })],
|
||||
});
|
||||
|
||||
function jwtPlugin(): HookFetchPlugin<BaseResponse> {
|
||||
const userStore = useUserStore();
|
||||
return {
|
||||
name: 'jwt',
|
||||
beforeRequest: async (config) => {
|
||||
config.headers = new Headers(config.headers);
|
||||
config.headers.set('authorization', `Bearer ${userStore.token}`);
|
||||
config.headers.set('ClientID', import.meta.env.VITE_CLIENT_ID);
|
||||
return config;
|
||||
},
|
||||
afterResponse: async (response) => {
|
||||
// console.log(response);
|
||||
if (response.result?.code === 200) {
|
||||
return response;
|
||||
}
|
||||
// 处理403逻辑
|
||||
if (response.result?.code === 403) {
|
||||
// 跳转到403页面(确保路由已配置)
|
||||
router.replace({
|
||||
name: '403',
|
||||
});
|
||||
ElMessage.error(response.result?.msg);
|
||||
return Promise.reject(response);
|
||||
}
|
||||
// 处理401逻辑
|
||||
if (response.result?.code === 401) {
|
||||
// 如果没有权限,退出,且弹框提示登录
|
||||
userStore.logout();
|
||||
userStore.openLoginDialog();
|
||||
}
|
||||
ElMessage.error(response.result?.msg);
|
||||
return Promise.reject(response);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
request.use(jwtPlugin());
|
||||
|
||||
export const post = request.post;
|
||||
|
||||
export const get = request.get;
|
||||
|
||||
export const put = request.put;
|
||||
|
||||
export const del = request.delete;
|
||||
|
||||
export default request;
|
||||
4
hzhub-portal-employee/.env.development
Normal file
@@ -0,0 +1,4 @@
|
||||
# 开发环境配置
|
||||
VITE_API_URL=
|
||||
VITE_CLIENT_ID=e5cd7e4891bf95d1d19206ce24a7b32e
|
||||
VITE_WEB_TITLE=Employee Portal 企业员工门户
|
||||
1
hzhub-portal-employee/.pid
Normal file
@@ -0,0 +1 @@
|
||||
3422048
|
||||
@@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
||||
|
||||
## Project Overview
|
||||
|
||||
This is **hzhub-portal**, a Vue 3 AI chat application built with TypeScript, Vite, and Element Plus. It provides a chat interface for AI interactions with support for sessions, models, and various authentication methods.
|
||||
This is **hzhub-portal-employee**, a Vue 3 Employee Portal application built with TypeScript, Vite, and Element Plus. It provides enterprise employee portal features including workflow approvals, CRM management, BI reports, and AI application integration.
|
||||
|
||||
## Commands
|
||||
|
||||
149
hzhub-portal-employee/README.md
Normal file
@@ -0,0 +1,149 @@
|
||||
# HZHub 员工门户
|
||||
|
||||
<div align="center">
|
||||
|
||||
### 企业员工门户系统
|
||||
|
||||
*HZHub 企业员工门户,提供审批流程、销售CRM、BI报表、AI应用集成等功能*
|
||||
|
||||
**[后端服务](https://github.com/hzhub/hzhub-ai)** | **[管理后台](https://github.com/hzhub/hzhub-admin)** | **[经销商门户](https://github.com/hzhub/hzhub-portal-dealer)**
|
||||
|
||||
</div>
|
||||
|
||||
## 功能特性
|
||||
|
||||
- 🔄 **审批流程**: 支持多级审批、流程管理
|
||||
- 📊 **BI报表**: 实时数据分析、可视化报表
|
||||
- 🤝 **CRM管理**: 客户管理、销售跟踪
|
||||
- 🤖 **AI应用**: AI助手、智能问答
|
||||
- 📱 **企业微信**: 支持企业微信H5集成
|
||||
|
||||
## 技术栈
|
||||
|
||||
- **框架**: Vue 3 + TypeScript
|
||||
- **UI组件**: Element Plus
|
||||
- **状态管理**: Pinia
|
||||
- **构建工具**: Vite
|
||||
- **HTTP请求**: hook-fetch (支持SSE流式传输)
|
||||
|
||||
## Docker 部署
|
||||
|
||||
### 一键启动(推荐)
|
||||
|
||||
```bash
|
||||
cd hzhub-deploy
|
||||
docker-compose up -d
|
||||
|
||||
# 访问员工门户
|
||||
# 地址: http://localhost:5137
|
||||
```
|
||||
|
||||
### 单独部署
|
||||
|
||||
```bash
|
||||
# 进入项目目录
|
||||
cd hzhub-portal-employee
|
||||
|
||||
# 构建并启动
|
||||
docker-compose up -d --build
|
||||
```
|
||||
|
||||
### 服务端口
|
||||
|
||||
| 服务 | 端口 | 说明 |
|
||||
|------|------|------|
|
||||
| 员工门户 | 5137 | 员工前端访问地址 |
|
||||
| 经销商门户 | 5138 | 经销商前端访问地址 |
|
||||
| 管理后台 | 5666 | 管理后台访问地址 |
|
||||
| 后端服务 | 6039 | 后端 API 服务 |
|
||||
|
||||
## 本地开发
|
||||
|
||||
```bash
|
||||
# 安装依赖
|
||||
pnpm install
|
||||
|
||||
# 启动开发服务器
|
||||
pnpm dev
|
||||
|
||||
# 类型检查
|
||||
pnpm build
|
||||
|
||||
# 代码检查
|
||||
pnpm lint
|
||||
```
|
||||
|
||||
## 环境变量配置
|
||||
|
||||
项目根目录 `.env.development` 文件:
|
||||
|
||||
```bash
|
||||
VITE_API_URL= # 后端API地址(默认通过Vite代理)
|
||||
VITE_CLIENT_ID=e5cd7e4891bf95d1d19206ce24a7b32e
|
||||
VITE_WEB_TITLE=Employee Portal 企业员工门户
|
||||
```
|
||||
|
||||
## 登录超时配置
|
||||
|
||||
员工门户默认登录超时时间为30分钟,可通过修改数据库配置调整为1小时或其他时长。
|
||||
|
||||
**详细配置说明**: [TIMEOUT_CONFIG.md](./TIMEOUT_CONFIG.md)
|
||||
|
||||
**快速修改**:
|
||||
```bash
|
||||
# 执行SQL更新超时时间为1小时
|
||||
mysql -u root -phzhub123 hzhub < /tmp/update_timeout_1hour.sql
|
||||
```
|
||||
|
||||
**注意**: 修改后需要重新登录才能生效。
|
||||
|
||||
## 多租户支持
|
||||
|
||||
系统支持多租户架构,不同公司实体(集团总部、汇亚公司、恒福公司、玛缇公司)通过登录时选择不同租户进行数据隔离。
|
||||
|
||||
### 登录方式
|
||||
|
||||
1. **URL参数方式**: `http://localhost:5137/login?tenant=000002`
|
||||
2. **手动选择方式**: 在登录界面选择公司
|
||||
|
||||
## 项目结构
|
||||
|
||||
```
|
||||
hzhub-portal-employee/
|
||||
├── src/
|
||||
│ ├── api/ # API模块
|
||||
│ ├── components/ # 可复用组件
|
||||
│ ├── layouts/ # 布局组件
|
||||
│ ├── pages/ # 页面组件
|
||||
│ ├── routers/ # 路由配置
|
||||
│ ├── stores/ # Pinia状态管理
|
||||
│ ├── styles/ # 样式文件
|
||||
│ └── utils/ # 工具函数
|
||||
├── public/ # 静态资源
|
||||
├── .env.development # 开发环境配置
|
||||
├── Dockerfile # Docker构建文件
|
||||
├── nginx.conf # Nginx配置
|
||||
└── package.json # 项目配置
|
||||
```
|
||||
|
||||
## 常见问题
|
||||
|
||||
**Q: 如何切换不同公司登录?**
|
||||
|
||||
A: 通过URL参数 `?tenant=租户ID` 或在登录页面手动选择公司。
|
||||
|
||||
**Q: 如何连接后端服务?**
|
||||
|
||||
A: 开发环境通过Vite代理,生产环境通过Nginx代理配置 `UPSTREAM_URL`。
|
||||
|
||||
## 开源协议
|
||||
|
||||
本项目采用 **MIT 开源协议**。
|
||||
|
||||
---
|
||||
|
||||
<div align="center">
|
||||
|
||||
*用 ❤️ 打造,由 HZHub 团队维护*
|
||||
|
||||
</div>
|
||||
150
hzhub-portal-employee/TIMEOUT_CONFIG.md
Normal file
@@ -0,0 +1,150 @@
|
||||
# 员工门户登录超时配置说明
|
||||
|
||||
## 问题需求
|
||||
|
||||
修改员工门户(hzhub-portal-employee)的登录超时时间为1小时,超时后自动退出。
|
||||
|
||||
## 配置原理
|
||||
|
||||
### Token超时机制
|
||||
|
||||
HZHub使用Sa-Token进行认证管理,token超时时间由 `sys_client` 表的 `active_timeout` 字段控制:
|
||||
|
||||
| 字段 | 说明 | 单位 | 默认值 |
|
||||
|------|------|------|--------|
|
||||
| `active_timeout` | token活跃超时时间 | 秒 | 1800 (30分钟) |
|
||||
| `timeout` | token固定超时时间 | 秒 | 604800 (7天) |
|
||||
|
||||
**区别说明:**
|
||||
- **active_timeout**: 用户活跃超时,用户在指定时间内无操作则token失效
|
||||
- **timeout**: token绝对超时,从登录开始计时,无论是否活跃都将在该时间后失效
|
||||
|
||||
### 客户端配置
|
||||
|
||||
员工门户使用的客户端ID: `e5cd7e4891bf95d1d19206ce24a7b32e`(来自 .env.development)
|
||||
|
||||
## 修改方案
|
||||
|
||||
### 方法1:执行SQL更新(推荐)
|
||||
|
||||
执行以下SQL脚本将员工门户的超时时间修改为1小时:
|
||||
|
||||
```bash
|
||||
# 连接数据库
|
||||
mysql -u root -phzhub123
|
||||
|
||||
# 执行更新SQL
|
||||
source /tmp/update_employee_timeout.sql
|
||||
```
|
||||
|
||||
或手动执行SQL:
|
||||
|
||||
```sql
|
||||
-- 查询当前配置
|
||||
SELECT id, client_id, client_key, active_timeout, timeout, status
|
||||
FROM sys_client
|
||||
WHERE client_id = 'e5cd7e4891bf95d1d19206ce24a7b32e';
|
||||
|
||||
-- 更新为1小时超时
|
||||
UPDATE sys_client
|
||||
SET active_timeout = 3600
|
||||
WHERE client_id = 'e5cd7e4891bf95d1d19206ce24a7b32e';
|
||||
|
||||
-- 验证结果
|
||||
SELECT id, client_id, client_key, active_timeout, timeout, status
|
||||
FROM sys_client
|
||||
WHERE client_id = 'e5cd7e4891bf95d1d19206ce24a7b32e';
|
||||
```
|
||||
|
||||
### 方法2:通过管理后台修改
|
||||
|
||||
1. 登录管理后台: http://localhost:5666
|
||||
2. 进入 "系统管理" -> "客户端管理"
|
||||
3. 找到 `client_id = e5cd7e4891bf95d1d19206ce24a7b32e` 的记录
|
||||
4. 编辑该记录,将 `活跃超时时间` 修改为 `3600`
|
||||
5. 保存
|
||||
|
||||
## 超时时间建议
|
||||
|
||||
根据不同场景,建议的超时时间配置:
|
||||
|
||||
| 场景 | active_timeout | timeout | 说明 |
|
||||
|------|----------------|---------|------|
|
||||
| 内部员工 | 3600 (1小时) | 604800 (7天) | 平衡安全性和便利性 |
|
||||
| 移动端APP | 7200 (2小时) | 604800 (7天) | 移动端使用频率高 |
|
||||
| 公共终端 | 1800 (30分钟) | 86400 (1天) | 安全性优先 |
|
||||
| 超级管理员 | 1800 (30分钟) | 86400 (1天) | 高安全性要求 |
|
||||
|
||||
## 验证配置生效
|
||||
|
||||
修改后需要**重新登录**才能生效:
|
||||
|
||||
1. 清除浏览器缓存或使用隐身模式
|
||||
2. 访问员工门户登录页面
|
||||
3. 登录后等待1小时不操作
|
||||
4. 再次访问应自动跳转到登录页面
|
||||
|
||||
## 技术实现
|
||||
|
||||
### 后端实现
|
||||
|
||||
登录时通过 `SaLoginParameter.setActiveTimeout()` 设置:
|
||||
|
||||
```java
|
||||
// PasswordAuthStrategy.java
|
||||
SaLoginParameter model = new SaLoginParameter();
|
||||
model.setActiveTimeout(client.getActiveTimeout()); // 从sys_client表读取
|
||||
LoginHelper.login(loginUser, model);
|
||||
```
|
||||
|
||||
### 前端响应
|
||||
|
||||
前端通过以下方式检测token过期:
|
||||
|
||||
1. **API响应401**: 拦截器检测到401状态码,自动跳转登录页
|
||||
2. **路由守卫**: 访问页面时检查token是否有效
|
||||
3. **本地存储**: Pinia持久化存储token,过期后自动清除
|
||||
|
||||
## 相关配置文件
|
||||
|
||||
- **后端配置**: `/data/hzhub/hzhub-ai/hzhub-common/hzhub-common-satoken/src/main/resources/common-satoken.yml`
|
||||
- `dynamic-active-timeout: true` - 允许动态设置token有效期
|
||||
|
||||
- **前端配置**: `/data/hzhub/hzhub-portal-employee/.env.development`
|
||||
- `VITE_CLIENT_ID=e5cd7e4891bf95d1d19206ce24a7b32e` - 客户端ID
|
||||
|
||||
- **数据库表**: `sys_client`
|
||||
- `active_timeout` - 活跃超时时间(秒)
|
||||
- `timeout` - 固定超时时间(秒)
|
||||
|
||||
## 常见问题
|
||||
|
||||
### Q: 修改后没有生效?
|
||||
|
||||
A: 需要**重新登录**。已登录的token不会立即失效,只有新登录的token才会使用新的超时时间。
|
||||
|
||||
### Q: 如何测试超时是否正确?
|
||||
|
||||
A:
|
||||
1. 登录系统
|
||||
2. 等待设定的超时时间(如1小时)
|
||||
3. 不进行任何操作
|
||||
4. 刷新页面或访问API,应自动跳转到登录页
|
||||
|
||||
### Q: 可以设置为永不过期吗?
|
||||
|
||||
A: 不建议。为了安全考虑,应该设置合理的超时时间。如需延长,可以设置为更大的值:
|
||||
|
||||
```sql
|
||||
-- 设置为8小时
|
||||
UPDATE sys_client SET active_timeout = 28800 WHERE client_id = 'e5cd7e4891bf95d1d19206ce24a7b32e';
|
||||
|
||||
-- 设置为12小时
|
||||
UPDATE sys_client SET active_timeout = 43200 WHERE client_id = 'e5cd7e4891bf95d1d19206ce24a7b32e';
|
||||
```
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [Sa-Token官方文档](https://sa-token.cc/)
|
||||
- [HZHub认证流程](../docs/architecture/README.md)
|
||||
- [服务管理脚本](./SERVICE_MANAGEMENT.md)
|
||||
169
hzhub-portal-employee/docs/COMPANY_NAME_CONFIG.md
Normal file
@@ -0,0 +1,169 @@
|
||||
# 动态公司名称配置说明
|
||||
|
||||
## 修改内容
|
||||
|
||||
已将员工门户的左上角标题从固定的"HZHub 企业员工门户"改为动态显示"登录用户对应公司名称 + 企业员工门户"。
|
||||
|
||||
## 修改文件
|
||||
|
||||
### 1. Aside侧边栏组件
|
||||
|
||||
**文件**: `src/layouts/components/Aside/index.vue`
|
||||
|
||||
**修改内容**:
|
||||
- 引入 `useUserStore` 获取用户信息
|
||||
- 创建 `tenantNameMap` 映射租户ID到公司名称
|
||||
- 使用 `computed` 计算属性动态获取公司名称
|
||||
- 将固定的 "HZHub" 改为动态的 `{{ companyName }}`
|
||||
|
||||
**关键代码**:
|
||||
```typescript
|
||||
import { useUserStore } from '@/stores';
|
||||
import { computed } from 'vue';
|
||||
|
||||
const userStore = useUserStore();
|
||||
|
||||
// 租户ID到公司名称的映射
|
||||
const tenantNameMap: Record<string, string> = {
|
||||
'000001': '集团总部',
|
||||
'000002': '汇亚公司',
|
||||
'000003': '恒福公司',
|
||||
'000004': '玛缇公司',
|
||||
};
|
||||
|
||||
// 计算当前用户的公司名称
|
||||
const companyName = computed(() => {
|
||||
const tenantId = userStore.userInfo?.tenantId;
|
||||
return tenantId ? tenantNameMap[tenantId] || 'HZHub' : 'HZHub';
|
||||
});
|
||||
```
|
||||
|
||||
### 2. 登录页面
|
||||
|
||||
**文件**: `src/pages/login/index.vue`
|
||||
|
||||
**修改内容**:
|
||||
- 从URL参数获取 `tenant` 参数
|
||||
- 使用相同的 `tenantNameMap` 映射
|
||||
- 根据URL参数动态显示公司名称
|
||||
|
||||
**关键代码**:
|
||||
```typescript
|
||||
import { useRoute } from 'vue-router';
|
||||
import { computed } from 'vue';
|
||||
|
||||
const route = useRoute();
|
||||
|
||||
// 从URL参数获取租户ID,并映射到公司名称
|
||||
const companyName = computed(() => {
|
||||
const tenantId = route.query.tenant as string;
|
||||
return tenantId ? tenantNameMap[tenantId] || 'HZHub' : 'HZHub';
|
||||
});
|
||||
```
|
||||
|
||||
## 公司名称映射关系
|
||||
|
||||
| 租户ID | 公司名称 | 登录URL示例 |
|
||||
|--------|----------|-------------|
|
||||
| 000001 | 集团总部 | http://localhost:5137/login?tenant=000001 |
|
||||
| 000002 | 汇亚公司 | http://localhost:5137/login?tenant=000002 |
|
||||
| 000003 | 恒福公司 | http://localhost:5137/login?tenant=000003 |
|
||||
| 000004 | 玛缇公司 | http://localhost:5137/login?tenant=000004 |
|
||||
|
||||
## 显示效果
|
||||
|
||||
### 登录页面
|
||||
- **集团总部用户**: 左上角显示 "集团总部 企业员工门户"
|
||||
- **汇亚公司用户**: 左上角显示 "汇亚公司 企业员工门户"
|
||||
- **恒福公司用户**: 左上角显示 "恒福公司 企业员工门户"
|
||||
- **玛缇公司用户**: 左上角显示 "玛缇公司 企业员工门户"
|
||||
|
||||
### 主界面侧边栏
|
||||
登录后,侧边栏顶部会显示对应公司的名称:
|
||||
```
|
||||
[公司名称]
|
||||
企业员工门户
|
||||
```
|
||||
|
||||
## 扩展性
|
||||
|
||||
### 添加新公司
|
||||
|
||||
如果需要添加新的公司,只需在两个组件的 `tenantNameMap` 中添加映射:
|
||||
|
||||
```typescript
|
||||
const tenantNameMap: Record<string, string> = {
|
||||
'000001': '集团总部',
|
||||
'000002': '汇亚公司',
|
||||
'000003': '恒福公司',
|
||||
'000004': '玛缇公司',
|
||||
'000005': '新公司名称', // 添加新公司
|
||||
};
|
||||
```
|
||||
|
||||
### 从后端获取公司名称
|
||||
|
||||
如果需要从后端动态获取公司名称,可以:
|
||||
|
||||
1. **修改 LoginUser 类型**,添加 `companyName` 字段:
|
||||
```typescript
|
||||
export interface LoginUser {
|
||||
// ... 现有字段
|
||||
companyName?: string; // 新增公司名称字段
|
||||
}
|
||||
```
|
||||
|
||||
2. **修改 Aside 组件**,直接使用 `companyName`:
|
||||
```typescript
|
||||
const companyName = computed(() => {
|
||||
return userStore.userInfo?.companyName || 'HZHub';
|
||||
});
|
||||
```
|
||||
|
||||
3. **后端接口**:在登录接口返回用户信息时,包含 `companyName` 字段
|
||||
|
||||
## 测试验证
|
||||
|
||||
### 测试步骤
|
||||
|
||||
1. **登录页面测试**:
|
||||
- 访问 `http://localhost:5137/login?tenant=000002`
|
||||
- 检查登录页面左上角是否显示 "汇亚公司 企业员工门户"
|
||||
|
||||
2. **主界面测试**:
|
||||
- 使用不同公司的账号登录
|
||||
- 检查侧边栏顶部是否显示对应公司名称
|
||||
|
||||
3. **切换租户测试**:
|
||||
- 登录汇亚公司账号
|
||||
- 退出登录
|
||||
- 用恒福公司账号登录
|
||||
- 检查标题是否正确切换
|
||||
|
||||
### 预期结果
|
||||
|
||||
- ✅ 登录页面根据URL参数显示对应公司名称
|
||||
- ✅ 主界面侧边栏显示登录用户的公司名称
|
||||
- ✅ 切换不同公司登录时,标题正确更新
|
||||
- ✅ 未识别的租户ID显示为默认值 "HZHub"
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **租户ID一致性**:确保 `tenantId` 与数据库 `sys_tenant` 表中的 `tenant_id` 字段一致
|
||||
2. **映射完整性**:所有可能的租户ID都应该在 `tenantNameMap` 中有对应映射
|
||||
3. **默认值**:未识别的租户ID会显示默认值 "HZHub"
|
||||
4. **性能优化**:使用 `computed` 计算属性,避免不必要的重复计算
|
||||
|
||||
## 相关文件
|
||||
|
||||
- `src/layouts/components/Aside/index.vue` - 侧边栏组件
|
||||
- `src/pages/login/index.vue` - 登录页面
|
||||
- `src/stores/modules/user.ts` - 用户状态管理
|
||||
- `src/api/auth/types.ts` - 用户类型定义
|
||||
|
||||
## 后续优化建议
|
||||
|
||||
1. **统一管理映射关系**:将 `tenantNameMap` 提取到统一的配置文件
|
||||
2. **支持多语言**:添加国际化支持,显示不同语言的公司名称
|
||||
3. **动态获取**:从后端API动态获取公司名称,避免硬编码
|
||||
4. **Logo定制**:不同公司使用不同的Logo图标
|
||||
198
hzhub-portal-employee/docs/PROFILE_FEATURE.md
Normal file
@@ -0,0 +1,198 @@
|
||||
# 个人中心功能说明
|
||||
|
||||
## 功能概述
|
||||
|
||||
已将 `hzhub-admin` 中的个人中心功能移植到 `hzhub-portal-employee`,并进行了以下修改:
|
||||
|
||||
1. **移除收藏夹菜单项**
|
||||
2. **将"设置"改为"个人中心"**
|
||||
3. **移植个人中心页面和功能**
|
||||
|
||||
## 主要功能
|
||||
|
||||
### 1. 个人信息展示
|
||||
|
||||
- 用户头像(支持上传更换)
|
||||
- 用户昵称、账号
|
||||
- 手机号码、邮箱
|
||||
- 所属部门和岗位
|
||||
- 角色信息
|
||||
- 上次登录时间
|
||||
|
||||
### 2. 基本设置
|
||||
|
||||
- 修改昵称
|
||||
- 修改邮箱
|
||||
- 修改性别
|
||||
- 修改手机号
|
||||
|
||||
### 3. 安全设置
|
||||
|
||||
- 修改密码
|
||||
- 密码强度验证
|
||||
- 确认密码校验
|
||||
|
||||
## 技术实现
|
||||
|
||||
### UI框架适配
|
||||
|
||||
由于 `hzhub-admin` 使用 **Ant Design Vue**,而 `hzhub-portal-employee` 使用 **Element Plus**,进行了以下适配:
|
||||
|
||||
| Ant Design Vue | Element Plus | 说明 |
|
||||
|----------------|--------------|------|
|
||||
| `<a-card>` | `<el-card>` | 卡片容器 |
|
||||
| `<a-descriptions>` | `<el-descriptions>` | 描述列表 |
|
||||
| `<a-tabs>` | `<el-tabs>` | 标签页 |
|
||||
| `<a-form>` | `<el-form>` | 表单 |
|
||||
| `<a-upload>` | `<el-upload>` | 文件上传 |
|
||||
|
||||
### 文件结构
|
||||
|
||||
```
|
||||
src/
|
||||
├── api/
|
||||
│ └── profile/
|
||||
│ ├── index.ts # API接口
|
||||
│ └── types.ts # 类型定义
|
||||
├── pages/
|
||||
│ └── profile/
|
||||
│ ├── index.vue # 个人中心主页面
|
||||
│ └── components/
|
||||
│ ├── BasicSetting.vue # 基本设置组件
|
||||
│ └── SecuritySetting.vue # 安全设置组件
|
||||
└── layouts/
|
||||
└── components/
|
||||
└── Header/
|
||||
└── UserDropdown.vue # 用户下拉菜单(已修改)
|
||||
```
|
||||
|
||||
## 使用说明
|
||||
|
||||
### 访问个人中心
|
||||
|
||||
1. **通过下拉菜单**:点击右上角用户头像 → 选择"个人中心"
|
||||
2. **直接访问**:访问 `/profile` 路由
|
||||
|
||||
### 修改个人信息
|
||||
|
||||
1. 进入个人中心页面
|
||||
2. 在"基本设置"标签页修改信息
|
||||
3. 点击"保存修改"按钮
|
||||
|
||||
### 修改密码
|
||||
|
||||
1. 进入个人中心页面
|
||||
2. 切换到"安全设置"标签页
|
||||
3. 输入当前密码和新密码
|
||||
4. 点击"修改密码"按钮
|
||||
5. 修改成功后需要重新登录
|
||||
|
||||
### 更换头像
|
||||
|
||||
1. 点击头像区域的"更换头像"
|
||||
2. 选择图片文件(支持 JPG、PNG 格式,最大 2MB)
|
||||
3. 上传成功后自动更新显示
|
||||
|
||||
## API接口
|
||||
|
||||
### 获取用户信息
|
||||
```typescript
|
||||
GET /system/user/profile
|
||||
Response: UserProfile
|
||||
```
|
||||
|
||||
### 更新用户信息
|
||||
```typescript
|
||||
PUT /system/user/profile
|
||||
Request: {
|
||||
userId: number
|
||||
nickName: string
|
||||
email: string
|
||||
phonenumber: string
|
||||
sex: string
|
||||
}
|
||||
```
|
||||
|
||||
### 修改密码
|
||||
```typescript
|
||||
PUT /system/user/profile/updatePwd
|
||||
Request: {
|
||||
oldPassword: string
|
||||
newPassword: string
|
||||
}
|
||||
```
|
||||
|
||||
### 上传头像
|
||||
```typescript
|
||||
POST /system/user/profile/avatar
|
||||
Request: FormData (avatarfile: File)
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **头像上传限制**:
|
||||
- 仅支持图片格式(JPG、PNG等)
|
||||
- 文件大小不超过 2MB
|
||||
|
||||
2. **密码修改**:
|
||||
- 新密码长度 6-20 个字符
|
||||
- 需要输入确认密码
|
||||
- 修改成功后需要重新登录
|
||||
|
||||
3. **表单验证**:
|
||||
- 邮箱格式验证
|
||||
- 手机号格式验证(中国大陆手机号)
|
||||
- 必填项验证
|
||||
|
||||
4. **实时更新**:
|
||||
- 修改个人信息后,右上角用户信息会同步更新
|
||||
- 头像更新后会立即刷新显示
|
||||
|
||||
## 与 hzhub-admin 的差异
|
||||
|
||||
### 保留的功能
|
||||
|
||||
- ✅ 个人信息展示
|
||||
- ✅ 基本设置(修改昵称、邮箱、性别、手机号)
|
||||
- ✅ 安全设置(修改密码)
|
||||
- ✅ 头像上传
|
||||
|
||||
### 简化的功能
|
||||
|
||||
- ❌ 移除"账号绑定"功能(暂不需要)
|
||||
- ❌ 移除"在线设备"功能(暂不需要)
|
||||
- ⚠️ 简化了表单验证(使用Element Plus原生验证)
|
||||
|
||||
## 后续优化建议
|
||||
|
||||
1. **头像裁剪**:添加图片裁剪功能
|
||||
2. **更多安全选项**:
|
||||
- 两步验证
|
||||
- 登录日志查看
|
||||
3. **个性化设置**:
|
||||
- 主题色切换
|
||||
- 字体大小调整
|
||||
4. **国际化**:支持多语言显示
|
||||
|
||||
## 测试建议
|
||||
|
||||
### 功能测试
|
||||
|
||||
1. **个人信息修改**:
|
||||
- 修改昵称并保存
|
||||
- 验证右上角显示是否更新
|
||||
|
||||
2. **头像上传**:
|
||||
- 上传符合要求的图片
|
||||
- 上传超大文件(应失败)
|
||||
- 上传非图片文件(应失败)
|
||||
|
||||
3. **密码修改**:
|
||||
- 输入错误旧密码(应失败)
|
||||
- 两次新密码不一致(应失败)
|
||||
- 正确修改密码并验证重新登录
|
||||
|
||||
### 兼容性测试
|
||||
|
||||
- Chrome、Firefox、Safari 浏览器测试
|
||||
- 响应式布局测试(桌面端、平板、移动端)
|
||||
358
hzhub-portal-employee/docs/TABBAR_FEATURE.md
Normal file
@@ -0,0 +1,358 @@
|
||||
# 标签页功能说明
|
||||
|
||||
## 功能概述
|
||||
|
||||
已将 `hzhub-admin` 中的标签页导航功能移植到 `hzhub-portal-employee`,使用 Element Plus 组件重新实现。
|
||||
|
||||
## 主要功能
|
||||
|
||||
### 1. 标签页管理
|
||||
|
||||
- 自动添加标签页:路由切换时自动创建对应标签页
|
||||
- 关闭标签页:点击关闭按钮关闭当前标签页
|
||||
- 固定标签页:固定后的标签页不可关闭,以不同颜色标识
|
||||
- 标签页持久化:标签页状态自动保存到本地存储
|
||||
|
||||
### 2. 工具栏功能
|
||||
|
||||
- 更多操作菜单:
|
||||
- 关闭其他标签页
|
||||
- 关闭右侧标签页
|
||||
- 关闭所有标签页
|
||||
- 全屏切换:切换浏览器全屏模式
|
||||
|
||||
### 3. 视觉效果
|
||||
|
||||
- 当前激活标签页高亮显示(主题色)
|
||||
- 固定标签页以警告色标识
|
||||
- 悬停显示关闭按钮
|
||||
- 平滑过渡动画
|
||||
- 标签页图标支持(可选)
|
||||
|
||||
## 技术实现
|
||||
|
||||
### UI 框架适配
|
||||
|
||||
从 `hzhub-admin` (Ant Design Vue) 到 `hzhub-portal-employee` (Element Plus) 的适配:
|
||||
|
||||
| 功能 | hzhub-admin | hzhub-portal-employee |
|
||||
|------|-------------|----------------------|
|
||||
| UI 框架 | Ant Design Vue + Vben | Element Plus |
|
||||
| 图标组件 | `<VbenIcon>` | `<el-icon>` |
|
||||
| 下拉菜单 | `<VbenDropdownMenu>` | `<el-dropdown>` |
|
||||
| CSS 系统 | TailwindCSS | SCSS + Element Plus 变量 |
|
||||
| 图标库 | Vben Icons | Element Plus Icons |
|
||||
|
||||
### 文件结构
|
||||
|
||||
```
|
||||
src/
|
||||
├── stores/
|
||||
│ ├── modules/
|
||||
│ │ └── tabbar.ts # 标签页状态管理
|
||||
│ └── index.ts # 导出 tabbar store
|
||||
├── hooks/
|
||||
│ └── useTabbar.ts # 标签页操作 Hook
|
||||
├── layouts/
|
||||
│ ├── components/
|
||||
│ │ └── TabsView/
|
||||
│ │ └── index.vue # 标签页视图组件
|
||||
│ └── LayoutVertical/
|
||||
│ └── index.vue # 集成标签页的布局
|
||||
└── routers/
|
||||
└ modules/
|
||||
└── staticRouter.ts # 路由配置
|
||||
```
|
||||
|
||||
### 核心代码
|
||||
|
||||
#### Tabbar Store
|
||||
|
||||
```typescript
|
||||
export interface TabItem {
|
||||
path: string; // 路由路径
|
||||
name: string; // 路由名称
|
||||
title: string; // 标签页标题
|
||||
icon?: string; // 图标
|
||||
affix?: boolean; // 是否固定
|
||||
query?: any; // 路由参数
|
||||
}
|
||||
|
||||
export const useTabbarStore = defineStore('tabbar', {
|
||||
state: (): TabbarState => ({
|
||||
tabs: [], // 标签页列表
|
||||
activeTab: '/', // 当前激活的标签页
|
||||
cachedTabs: new Set(), // 缓存的组件名称
|
||||
}),
|
||||
|
||||
actions: {
|
||||
addTab(route), // 添加标签页
|
||||
closeTab(path), // 关闭标签页
|
||||
closeOtherTabs(path), // 关闭其他标签页
|
||||
closeAllTabs(), // 关闭所有标签页
|
||||
closeRightTabs(path), // 关闭右侧标签页
|
||||
toggleAffixTab(path), // 固定/取消固定
|
||||
initAffixTabs(routes), // 初始化固定标签页
|
||||
},
|
||||
|
||||
persist: {
|
||||
key: 'hzhub-employee-tabs',
|
||||
paths: ['tabs', 'activeTab'],
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
#### useTabbar Hook
|
||||
|
||||
```typescript
|
||||
export function useTabbar() {
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
const tabbarStore = useTabbarStore();
|
||||
|
||||
// 监听路由变化,自动添加标签页
|
||||
watch(() => route.path, () => {
|
||||
if (route.name) {
|
||||
tabbarStore.addTab(route);
|
||||
}
|
||||
}, { immediate: true });
|
||||
|
||||
// 初始化固定标签页
|
||||
watch(() => router.getRoutes(), () => {
|
||||
tabbarStore.initAffixTabs(router.getRoutes());
|
||||
}, { immediate: true });
|
||||
|
||||
return {
|
||||
currentActive, // 当前激活的标签页
|
||||
currentTabs, // 标签页列表
|
||||
handleClick, // 点击标签页
|
||||
handleClose, // 关闭标签页
|
||||
handleUnpin, // 固定/取消固定
|
||||
closeOtherTabs, // 关闭其他标签页
|
||||
closeAllTabs, // 关闭所有标签页
|
||||
closeRightTabs, // 关闭右侧标签页
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
#### TabsView 组件
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { useTabbarStore } from '@/stores';
|
||||
|
||||
const tabbarStore = useTabbarStore();
|
||||
const activeTabPath = computed(() => tabbarStore.activeTab);
|
||||
const tabs = computed(() => tabbarStore.tabs);
|
||||
const isFullscreen = ref(false);
|
||||
|
||||
// 点击标签页
|
||||
const handleTabClick = (tab: TabItem) => {
|
||||
tabbarStore.setActiveTab(tab.path);
|
||||
router.push(tab.path);
|
||||
};
|
||||
|
||||
// 关闭标签页
|
||||
const handleTabClose = (path: string) => {
|
||||
tabbarStore.closeTab(path);
|
||||
};
|
||||
|
||||
// 切换全屏
|
||||
const toggleFullscreen = () => {
|
||||
isFullscreen.value = !isFullscreen.value;
|
||||
if (isFullscreen.value) {
|
||||
document.documentElement.requestFullscreen();
|
||||
} else {
|
||||
document.exitFullscreen();
|
||||
}
|
||||
};
|
||||
</script>
|
||||
```
|
||||
|
||||
#### 布局集成
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
import TabsView from '@/layouts/components/TabsView/index.vue';
|
||||
import { useTabbar } from '@/hooks/useTabbar';
|
||||
|
||||
const { handleClose, handleUnpin } = useTabbar();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="layout">
|
||||
<Aside />
|
||||
<div class="main-area">
|
||||
<Header />
|
||||
<TabsView
|
||||
:show-icon="true"
|
||||
@close="handleClose"
|
||||
@unpin="handleUnpin"
|
||||
/>
|
||||
<main class="page-content">
|
||||
<router-view />
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
## 使用说明
|
||||
|
||||
### 固定标签页
|
||||
|
||||
在路由配置中添加 `meta.affix` 属性:
|
||||
|
||||
```typescript
|
||||
{
|
||||
path: '/dashboard',
|
||||
name: 'dashboard',
|
||||
meta: {
|
||||
title: '工作台',
|
||||
icon: 'Odometer',
|
||||
affix: true, // 固定标签页
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
### 隐藏标签页
|
||||
|
||||
在路由配置中添加 `meta.isHide` 属性:
|
||||
|
||||
```typescript
|
||||
{
|
||||
path: '/profile',
|
||||
name: 'profile',
|
||||
meta: {
|
||||
title: '个人中心',
|
||||
isHide: '1', // 不显示在标签页中
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
### 禁用缓存
|
||||
|
||||
在路由配置中添加 `meta.isKeepAlive` 属性:
|
||||
|
||||
```typescript
|
||||
{
|
||||
path: '/example',
|
||||
name: 'example',
|
||||
meta: {
|
||||
title: '示例页面',
|
||||
isKeepAlive: '0', // 不缓存组件
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
## 样式定制
|
||||
|
||||
标签页样式使用 Element Plus CSS 变量系统:
|
||||
|
||||
- **激活状态**: `--el-color-primary` (主题色)
|
||||
- **固定状态**: `--el-color-warning` (警告色)
|
||||
- **悬停状态**: `--el-color-primary-light-9`
|
||||
- **背景色**: `--el-bg-color-page`
|
||||
- **边框色**: `--el-border-color-light`
|
||||
|
||||
可在 `src/styles/var.scss` 中自定义这些变量值。
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **标签页限制**:
|
||||
- 固定的标签页不能关闭
|
||||
- 至少保留一个标签页
|
||||
- 标签页标题过长会自动截断
|
||||
|
||||
2. **路由要求**:
|
||||
- 路由必须设置 `name` 属性
|
||||
- 路由必须设置 `meta.title` 属性
|
||||
- 隐藏路由(`isHide='1'`)不会添加到标签页
|
||||
|
||||
3. **持久化配置**:
|
||||
- 使用 `pinia-plugin-persistedstate` 自动保存
|
||||
- 保存路径:`['tabs', 'activeTab']`
|
||||
- 不保存 `cachedTabs` (组件缓存)
|
||||
|
||||
4. **性能优化**:
|
||||
- 使用 `keep-alive` 缓存组件
|
||||
- 标签页列表使用 `Set` 优化查找
|
||||
- 懒加载图标组件
|
||||
|
||||
## 与 hzhub-admin 的差异
|
||||
|
||||
### 保留的功能
|
||||
|
||||
- ✅ 标签页自动添加/关闭
|
||||
- ✅ 标签页固定/取消固定
|
||||
- ✅ 批量关闭操作
|
||||
- ✅ 标签页持久化
|
||||
- ✅ 全屏切换
|
||||
- ✅ 标签页图标显示
|
||||
|
||||
### 简化的功能
|
||||
|
||||
- ❌ 拖拽排序(暂未实现)
|
||||
- ❌ 滚轮滚动(暂未实现)
|
||||
- ❌ 中键关闭(暂未实现)
|
||||
- ❌ 右键菜单(暂未实现)
|
||||
- ❌ 多种标签页样式(plain/brisk/card)
|
||||
- ❌ 国际化标题切换
|
||||
|
||||
### 新增的功能
|
||||
|
||||
- ✅ 简化的工具栏设计
|
||||
- ✅ Element Plus 原生样式
|
||||
- ✅ 更简洁的代码结构
|
||||
|
||||
## 后续优化建议
|
||||
|
||||
1. **交互增强**:
|
||||
- 添加右键菜单功能
|
||||
- 支持拖拽排序标签页
|
||||
- 支持鼠标滚轮横向滚动
|
||||
- 支持中键点击关闭
|
||||
|
||||
2. **样式定制**:
|
||||
- 提供多种标签页样式(chrome/card/plain)
|
||||
- 支持自定义标签页主题色
|
||||
- 标签页宽度自适应
|
||||
|
||||
3. **功能扩展**:
|
||||
- 标签页分组功能
|
||||
- 标签页搜索功能
|
||||
- 最近访问的标签页历史
|
||||
- 标签页快捷键操作
|
||||
|
||||
4. **性能优化**:
|
||||
- 虚拟滚动优化大量标签页
|
||||
- 标签页懒加载优化
|
||||
- 组件缓存策略优化
|
||||
|
||||
## 测试建议
|
||||
|
||||
### 功能测试
|
||||
|
||||
1. **标签页管理**:
|
||||
- 切换路由自动添加标签页
|
||||
- 关闭标签页后自动切换到相邻标签页
|
||||
- 固定标签页不可关闭
|
||||
- 刷新后标签页状态保持
|
||||
|
||||
2. **批量操作**:
|
||||
- 关闭其他标签页
|
||||
- 关闭右侧标签页
|
||||
- 关闭所有标签页
|
||||
|
||||
3. **全屏功能**:
|
||||
- 点击全屏按钮进入全屏模式
|
||||
- 再次点击退出全屏模式
|
||||
|
||||
### 兼容性测试
|
||||
|
||||
- Chrome、Firefox、Safari 浏览器测试
|
||||
- 响应式布局测试(桌面端、平板)
|
||||
- 标签页滚动测试(超过 10 个标签页)
|
||||
- 固定标签页与普通标签页混排测试
|
||||
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 ruoyi-ai
|
||||
Copyright (c) 2026 hzhub
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
16
hzhub-portal-employee/logs.sh
Executable file
@@ -0,0 +1,16 @@
|
||||
#!/bin/bash
|
||||
# HZHub Portal Employee 前端项目日志查看脚本
|
||||
# 功能:查看服务运行日志
|
||||
|
||||
PROJECT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
LOG_FILE="$PROJECT_DIR/logs/dev.log"
|
||||
|
||||
if [ ! -f "$LOG_FILE" ]; then
|
||||
echo "⚠️ 日志文件不存在: $LOG_FILE"
|
||||
echo "服务可能未运行或未产生日志"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "查看 hzhub-portal-employee 日志 (Ctrl+C 退出)"
|
||||
echo "========================================="
|
||||
tail -f "$LOG_FILE"
|
||||
@@ -1,22 +1,22 @@
|
||||
{
|
||||
"name": "hzhub-portal",
|
||||
"name": "hzhub-portal-employee",
|
||||
"type": "module",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"description": "hzhub-portal open-source PC AI template",
|
||||
"description": "HZHub Employee Portal - 企业员工门户系统",
|
||||
"author": {
|
||||
"name": "HeJiaYue520",
|
||||
"email": "2834007710@qq.com",
|
||||
"url": "https://github.com/HeJiaYue520/HeJiaYue520"
|
||||
"name": "HZHub Team",
|
||||
"email": "hzhub@example.com",
|
||||
"url": "https://github.com/hzhub"
|
||||
},
|
||||
"license": "MIT",
|
||||
"homepage": "https://github.com/element-plus-x/ruoyi-element-ai",
|
||||
"homepage": "https://github.com/hzhub/hzhub-portal-employee",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git@github.com:element-plus-x/ruoyi-element-ai.git"
|
||||
"url": "git@github.com:hzhub/hzhub-portal-employee.git"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/element-plus-x/ruoyi-element-ai/issues"
|
||||
"url": "https://github.com/hzhub/hzhub-portal-employee/issues"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 2.8 KiB |
18
hzhub-portal-employee/restart.sh
Executable file
@@ -0,0 +1,18 @@
|
||||
#!/bin/bash
|
||||
# HZHub Portal Employee 前端项目重启脚本
|
||||
# 功能:重启后台运行的员工门户前端开发服务器
|
||||
|
||||
PROJECT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
|
||||
echo "========================================="
|
||||
echo "重启 hzhub-portal-employee 开发服务器"
|
||||
echo "========================================="
|
||||
|
||||
# 停止服务
|
||||
"$PROJECT_DIR/stop.sh"
|
||||
|
||||
# 等待一秒
|
||||
sleep 1
|
||||
|
||||
# 启动服务
|
||||
"$PROJECT_DIR/start.sh"
|
||||
40
hzhub-portal-employee/src/api/profile/index.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import type { UserProfile, UpdatePasswordParam } from './types';
|
||||
|
||||
import { get, put, post } from '@/utils/request';
|
||||
|
||||
/**
|
||||
* 用户个人主页信息
|
||||
* @returns userInformation
|
||||
*/
|
||||
export function userProfile() {
|
||||
return get<UserProfile>('/system/user/profile').json();
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新用户个人主页信息
|
||||
* @param data
|
||||
* @returns void
|
||||
*/
|
||||
export function userProfileUpdate(data: any) {
|
||||
return put<void>('/system/user/profile', { body: data }).json();
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户修改密码
|
||||
* @param data
|
||||
* @returns void
|
||||
*/
|
||||
export function userUpdatePassword(data: UpdatePasswordParam) {
|
||||
return put<void>('/system/user/profile/updatePwd', { body: data }).json();
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户更新个人头像
|
||||
* @param file 文件
|
||||
* @returns void
|
||||
*/
|
||||
export function userUpdateAvatar(file: File) {
|
||||
const formData = new FormData();
|
||||
formData.append('avatarfile', file);
|
||||
return post<void>('/system/user/profile/avatar', { body: formData }).json();
|
||||
}
|
||||
69
hzhub-portal-employee/src/api/profile/types.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
export interface Dept {
|
||||
deptId: number;
|
||||
parentId: number;
|
||||
parentName?: any;
|
||||
ancestors: string;
|
||||
deptName: string;
|
||||
orderNum: number;
|
||||
leader: string;
|
||||
phone?: any;
|
||||
email: string;
|
||||
status: string;
|
||||
createTime?: any;
|
||||
}
|
||||
|
||||
export interface Role {
|
||||
roleId: number;
|
||||
roleName: string;
|
||||
roleKey: string;
|
||||
roleSort: number;
|
||||
dataScope: string;
|
||||
menuCheckStrictly?: any;
|
||||
deptCheckStrictly?: any;
|
||||
status: string;
|
||||
remark: string;
|
||||
createTime?: any;
|
||||
flag: boolean;
|
||||
superAdmin: boolean;
|
||||
}
|
||||
|
||||
export interface User {
|
||||
userId: number;
|
||||
tenantId: string;
|
||||
deptId: number;
|
||||
userName: string;
|
||||
nickName: string;
|
||||
userType: string;
|
||||
email: string;
|
||||
phonenumber: string;
|
||||
sex: string;
|
||||
avatar: string;
|
||||
status: string;
|
||||
loginIp: string;
|
||||
loginDate: string;
|
||||
remark: string;
|
||||
createTime: string;
|
||||
dept: Dept;
|
||||
roles: Role[];
|
||||
roleIds?: string[];
|
||||
postIds?: string[];
|
||||
roleId: number;
|
||||
deptName: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 用户个人主页信息
|
||||
* @param user 用户信息
|
||||
* @param roleGroup 角色名称
|
||||
* @param postGroup 岗位名称
|
||||
*/
|
||||
export interface UserProfile {
|
||||
user: User;
|
||||
roleGroup: string;
|
||||
postGroup: string;
|
||||
}
|
||||
|
||||
export interface UpdatePasswordParam {
|
||||
oldPassword: string;
|
||||
newPassword: string;
|
||||
}
|
||||
|
Before Width: | Height: | Size: 448 B After Width: | Height: | Size: 448 B |
|
Before Width: | Height: | Size: 860 B After Width: | Height: | Size: 860 B |
|
Before Width: | Height: | Size: 332 B After Width: | Height: | Size: 332 B |
|
Before Width: | Height: | Size: 483 B After Width: | Height: | Size: 483 B |
|
Before Width: | Height: | Size: 281 B After Width: | Height: | Size: 281 B |
|
Before Width: | Height: | Size: 310 B After Width: | Height: | Size: 310 B |
|
Before Width: | Height: | Size: 265 B After Width: | Height: | Size: 265 B |
|
Before Width: | Height: | Size: 314 B After Width: | Height: | Size: 314 B |
|
Before Width: | Height: | Size: 177 B After Width: | Height: | Size: 177 B |
|
Before Width: | Height: | Size: 228 B After Width: | Height: | Size: 228 B |
|
Before Width: | Height: | Size: 292 B After Width: | Height: | Size: 292 B |
|
Before Width: | Height: | Size: 308 B After Width: | Height: | Size: 308 B |
|
Before Width: | Height: | Size: 256 B After Width: | Height: | Size: 256 B |
|
Before Width: | Height: | Size: 275 B After Width: | Height: | Size: 275 B |
|
Before Width: | Height: | Size: 364 B After Width: | Height: | Size: 364 B |
|
Before Width: | Height: | Size: 434 B After Width: | Height: | Size: 434 B |
|
Before Width: | Height: | Size: 283 B After Width: | Height: | Size: 283 B |
|
Before Width: | Height: | Size: 277 B After Width: | Height: | Size: 277 B |
|
Before Width: | Height: | Size: 236 B After Width: | Height: | Size: 236 B |
|
Before Width: | Height: | Size: 271 B After Width: | Height: | Size: 271 B |