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

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

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

View File

@@ -0,0 +1,39 @@
import type { ConfigEnv, PluginOption } from 'vite';
import path from 'node:path';
import vue from '@vitejs/plugin-vue';
import UnoCSS from 'unocss/vite';
import AutoImport from 'unplugin-auto-import/vite';
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers';
import Components from 'unplugin-vue-components/vite';
import envTyped from 'vite-plugin-env-typed';
import createSvgIcon from './svg-icon';
const root = path.resolve(__dirname, '../../');
function plugins({ mode, command }: ConfigEnv): PluginOption[] {
return [
UnoCSS(),
envTyped({
mode,
envDir: root,
envPrefix: 'VITE_',
filePath: path.join(root, 'types', 'import_meta.d.ts'),
}),
vue(),
AutoImport({
imports: ['vue'],
eslintrc: {
enabled: true,
},
resolvers: [ElementPlusResolver()],
dts: path.join(root, 'types', 'auto-imports.d.ts'),
}),
Components({
resolvers: [ElementPlusResolver()],
dts: path.join(root, 'types', 'components.d.ts'),
}),
createSvgIcon(command === 'build'),
];
}
export default plugins;

View File

@@ -0,0 +1,21 @@
import path from 'node:path';
import { createSvgIconsPlugin } from 'vite-plugin-svg-icons';
const root = path.resolve(__dirname, '../../');
export default function createSvgIcon(isBuild: boolean) {
return createSvgIconsPlugin({
iconDirs: [
path.join(root, 'src/assets/icons/svg'),
path.join(root, 'src/assets/icons/Buildings'),
path.join(root, 'src/assets/icons/Business'),
path.join(root, 'src/assets/icons/Device'),
path.join(root, 'src/assets/icons/Document'),
path.join(root, 'src/assets/icons/Others'),
path.join(root, 'src/assets/icons/System'),
path.join(root, 'src/assets/icons/User'),
],
symbolId: 'icon-[dir]-[name]',
svgoOptions: isBuild,
});
}

View File

@@ -0,0 +1,4 @@
# 开发环境配置
VITE_API_URL=
VITE_CLIENT_ID=e5cd7e4891bf95d1d19206ce24a7b32e
VITE_WEB_TITLE=Employee Portal 企业员工门户

View File

@@ -0,0 +1,78 @@
{
"globals": {
"Component": true,
"ComponentPublicInstance": true,
"ComputedRef": true,
"DirectiveBinding": true,
"EffectScope": true,
"ElMessage": true,
"ElMessageBox": true,
"ExtractDefaultPropTypes": true,
"ExtractPropTypes": true,
"ExtractPublicPropTypes": true,
"InjectionKey": true,
"MaybeRef": true,
"MaybeRefOrGetter": true,
"PropType": true,
"Ref": true,
"Slot": true,
"Slots": true,
"VNode": true,
"WritableComputedRef": true,
"computed": true,
"createApp": true,
"customRef": true,
"defineAsyncComponent": true,
"defineComponent": true,
"effectScope": true,
"getCurrentInstance": true,
"getCurrentScope": true,
"h": true,
"inject": true,
"isProxy": true,
"isReactive": true,
"isReadonly": true,
"isRef": true,
"markRaw": true,
"nextTick": true,
"onActivated": true,
"onBeforeMount": true,
"onBeforeUnmount": true,
"onBeforeUpdate": true,
"onDeactivated": true,
"onErrorCaptured": true,
"onMounted": true,
"onRenderTracked": true,
"onRenderTriggered": true,
"onScopeDispose": true,
"onServerPrefetch": true,
"onUnmounted": true,
"onUpdated": true,
"onWatcherCleanup": true,
"provide": true,
"reactive": true,
"readonly": true,
"ref": true,
"resolveComponent": true,
"shallowReactive": true,
"shallowReadonly": true,
"shallowRef": true,
"toRaw": true,
"toRef": true,
"toRefs": true,
"toValue": true,
"triggerRef": true,
"unref": true,
"useAttrs": true,
"useCssModule": true,
"useCssVars": true,
"useId": true,
"useModel": true,
"useSlots": true,
"useTemplateRef": true,
"watch": true,
"watchEffect": true,
"watchPostEffect": true,
"watchSyncEffect": true
}
}

View File

@@ -0,0 +1 @@
3422048

View File

@@ -0,0 +1,92 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
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
```bash
# Development
pnpm dev # Start dev server
# Build
pnpm build # Type-check and build for production
# Linting
pnpm lint # Run ESLint
pnpm lint:stylelint # Run Stylelint
pnpm fix # Auto-fix ESLint issues
# Preview production build
pnpm preview
```
## Architecture
### Tech Stack
- **Vue 3** with Composition API (`<script setup>`)
- **TypeScript** for type safety
- **Pinia** for state management with persistence (`pinia-plugin-persistedstate`)
- **Vue Router** for routing with navigation guards
- **Element Plus** for UI components
- **UnoCSS** for atomic CSS
- **hook-fetch** for HTTP requests with SSE support
### Directory Structure
```
src/
├── api/ # API modules (auth, chat, model, session) with types
├── assets/ # Static assets (SVG icons organized by category)
├── components/ # Reusable components (LoginDialog, ModelSelect, etc.)
├── config/ # App configuration constants
├── constants/ # Enum definitions
├── hooks/ # Custom Vue composables
├── layouts/ # Layout components (LayoutVertical, LayoutMobile)
├── pages/ # Page components (chat, error pages)
├── routers/ # Route definitions and guards
├── stores/ # Pinia stores (user, chat, session, model, design)
├── styles/ # Global SCSS styles and variables
├── utils/ # Utilities (request wrapper, markdown renderers)
└── main.ts # App entry point
```
### Key Patterns
**API Layer** (`src/api/`):
- Each module has `index.ts` for API calls and `types.ts` for TypeScript interfaces
- Uses `hook-fetch` with JWT plugin for authentication
- Base URL configured via `VITE_API_URL` environment variable
**State Management** (`src/stores/`):
- Stores use Composition API style with `defineStore`
- User store persists token and userInfo
- Session store handles chat sessions with pagination
- Chat store manages messages and deep thinking state
**Routing** (`src/routers/`):
- Static routes defined in `modules/staticRouter.ts`
- Route guard checks token and handles auth redirect
- White list routes bypass auth check
**HTTP Requests** (`src/utils/request.ts`):
- Auto-injects `Bearer` token and `ClientID` headers
- Handles 401 (logout) and 403 (redirect) responses
- Supports SSE streaming via `sseTextDecoderPlugin`
### Environment Variables
Configure in `.env.development`:
- `VITE_API_URL` - Backend API base URL
- `VITE_CLIENT_ID` - Client identifier for auth
- `VITE_WEB_TITLE` - Page title
### Code Style
- ESLint with `@antfu/eslint-config`
- Vue blocks order: script → template → style
- Single quotes, semicolons, 2-space indent
- Commit messages follow conventional commits with commitlint

View File

@@ -0,0 +1,47 @@
# 构建阶段
FROM node:22-alpine AS builder
# 接收构建参数
ARG VITE_API_URL
ARG VITE_APP_ENV=production
# 设置环境变量
ENV VITE_API_URL=${VITE_API_URL}
ENV VITE_APP_ENV=${VITE_APP_ENV}
# 设置工作目录
WORKDIR /app
# 复制 package 文件
COPY package.json pnpm-lock.yaml ./
# 安装 pnpm 并安装依赖
RUN npm install -g pnpm && \
pnpm install --frozen-lockfile
# 复制源代码
COPY . .
# 构建生产版本(使用 production 模式)
RUN pnpm build
# 生产阶段
FROM nginx:alpine
# 安装 envsubst
RUN apk add --no-cache gettext
# 复制构建产物
COPY --from=builder /app/dist /usr/share/nginx/html
# 设置后端地址环境变量(默认指向 hzhub-ai 服务)
ENV UPSTREAM_URL=http://hzhub-ai:6039
# 复制 Nginx 配置模板
COPY nginx.conf /etc/nginx/templates/default.conf.template
# 暴露端口
EXPOSE 5137
# 启动 Nginx使用 envsubst 替换环境变量)
CMD /bin/sh -c "envsubst '\$UPSTREAM_URL' < /etc/nginx/templates/default.conf.template > /etc/nginx/conf.d/default.conf && nginx -g 'daemon off;'"

View File

@@ -0,0 +1,44 @@
# 构建阶段
FROM node:22-alpine AS builder
# 接收构建参数
ARG VITE_API_URL
ARG VITE_APP_ENV=production
# 设置环境变量
ENV VITE_API_URL=${VITE_API_URL}
ENV VITE_APP_ENV=${VITE_APP_ENV}
# 设置工作目录
WORKDIR /app
# 复制 package 文件
COPY package.json pnpm-lock.yaml ./
# 安装 pnpm 并安装依赖
RUN npm install -g pnpm && \
pnpm install --frozen-lockfile
# 复制源代码
COPY . .
# 构建生产版本(使用 production 模式)
RUN pnpm build
# 生产阶段
FROM nginx:alpine
# 复制构建产物
COPY --from=builder /app/dist /usr/share/nginx/html
# 设置后端地址环境变量(默认指向本机 6039 端口)
ENV UPSTREAM_URL=http://127.0.0.1:6039
# 复制 Nginx 配置并使用 envsubst 替换环境变量
COPY nginx.conf /etc/nginx/templates/default.conf.template
# 暴露端口
EXPOSE 5137
# 启动 Nginxdocker-compose 中可通过环境变量 UPSTREAM_URL 覆盖后端地址)
CMD ["nginx", "-g", "daemon off;"]

View 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>

View 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)

View File

@@ -0,0 +1,181 @@
# HZHub-AI 一键启动全部服务
# 使用方式: docker-compose up -d
#
# 包含服务:
# - MySQL 8.0 (数据库包含初始化SQL)
# - Redis 6.2 (缓存)
# - Weaviate (向量数据库)
# - MinIO (对象存储)
# - HZHub-Backend (后端服务)
# - HZHub-Admin (管理端前端)
# - HZHub-Web (用户端前端)
#
# 镜像仓库地址: crpi-31mraxd99y2gqdgr.cn-beijing.personal.cr.aliyuncs.com/ruoyi_ai
version: '3.8'
services:
# ==================== MySQL 数据库 ====================
mysql:
# 阿里云镜像地址包含初始化SQL
image: crpi-31mraxd99y2gqdgr.cn-beijing.personal.cr.aliyuncs.com/ruoyi_ai/mysql:v3
container_name: hzhub-ai-mysql
restart: always
ports:
- "23306:3306"
environment:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: hzhub-ai-agent
TZ: Asia/Shanghai
volumes:
- mysql-data:/var/lib/mysql
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-proot"]
interval: 15s
timeout: 10s
retries: 10
start_period: 60s
networks:
- ruoyi-net
# ==================== Redis 缓存 ====================
redis:
image: crpi-31mraxd99y2gqdgr.cn-beijing.personal.cr.aliyuncs.com/ruoyi_ai/redis:6.2
container_name: hzhub-ai-redis
restart: always
ports:
- "26379:6379"
volumes:
- redis-data:/data
command: redis-server --appendonly yes
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 5
networks:
- ruoyi-net
# ==================== Weaviate 向量数据库 ====================
weaviate:
image: crpi-31mraxd99y2gqdgr.cn-beijing.personal.cr.aliyuncs.com/ruoyi_ai/weaviate:1.30.0
container_name: hzhub-ai-weaviate
restart: always
ports:
- "28080:8080"
environment:
QUERY_DEFAULTS_LIMIT: 25
AUTHENTICATION_ANONYMOUS_ACCESS_ENABLED: true
PERSISTENCE_DATA_PATH: /var/lib/weaviate
DEFAULT_VECTORIZER_MODULE: none
ENABLE_MODULES: text2vec-cohere,text2vec-huggingface,text2vec-palm,text2vec-openai,generative-openai,generative-cohere,generative-palm,ref2vec-centroid,reranker-cohere,qna-openai
CLUSTER_HOSTNAME: node1
volumes:
- weaviate-data:/var/lib/weaviate
networks:
- ruoyi-net
# ==================== MinIO 对象存储 ====================
minio:
image: crpi-31mraxd99y2gqdgr.cn-beijing.personal.cr.aliyuncs.com/ruoyi_ai/minio:latest
container_name: hzhub-ai-minio
restart: always
ports:
- "29000:9000"
- "29090:9090"
environment:
MINIO_ROOT_USER: ruoyi
MINIO_ROOT_PASSWORD: ruoyi123
volumes:
- minio-data:/data
command: server /data --console-address ":9090"
networks:
- ruoyi-net
# ==================== HZHub-AI 后端服务 ====================
backend:
image: crpi-31mraxd99y2gqdgr.cn-beijing.personal.cr.aliyuncs.com/ruoyi_ai/hzhub-ai-backend:latest
container_name: hzhub-ai-backend
restart: always
ports:
- "26039:6039"
environment:
TZ: Asia/Shanghai
# MySQL 配置
SPRING_DATASOURCE_DYNAMIC_PRIMARY: master
SPRING_DATASOURCE_DYNAMIC_DATASOURCE_MASTER_DRIVERCLASSNAME: com.mysql.cj.jdbc.Driver
SPRING_DATASOURCE_DYNAMIC_DATASOURCE_MASTER_URL: jdbc:mysql://mysql:3306/hzhub-ai-agent?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&serverTimezone=GMT%2B8&autoReconnect=true&rewriteBatchedStatements=true
SPRING_DATASOURCE_DYNAMIC_DATASOURCE_MASTER_USERNAME: root
SPRING_DATASOURCE_DYNAMIC_DATASOURCE_MASTER_PASSWORD: root
# Redis 配置
SPRING_DATA_REDIS_HOST: redis
SPRING_DATA_REDIS_PORT: 6379
SPRING_DATA_REDIS_DATABASE: 0
# 日志配置
LOGGING_LEVEL_ORG_RUOYI: info
LOGGING_LEVEL_ORG_SPRINGFRAMEWORK: warn
SYS_UPLOAD_PATH: /ruoyi/upload
volumes:
- logs-data:/ruoyi/server/logs
- upload-data:/ruoyi/upload
depends_on:
mysql:
condition: service_healthy
redis:
condition: service_started
networks:
- ruoyi-net
# ==================== HZHub-AI 管理端前端 ====================
admin-frontend:
image: crpi-31mraxd99y2gqdgr.cn-beijing.personal.cr.aliyuncs.com/ruoyi_ai/hzhub-ai-admin:latest
container_name: hzhub-ai-admin
restart: always
ports:
- "25666:5666"
environment:
# 后端 API 地址 - 运行时动态配置(无需重新构建镜像)
# 在完整 docker-compose 中应设置为: http://backend:6039
# 独立运行时可设置为实际后端地址,如: 192.168.1.100:6039
UPSTREAM_HOST: backend:6039
# 资源限制 - 防止 CPU 和内存耗尽
deploy:
resources:
limits:
cpus: '2'
memory: 3G
reservations:
cpus: '1'
memory: 1G
depends_on:
- backend
networks:
- ruoyi-net
# ==================== HZHub-AI 用户端前端 ====================
web-frontend:
image: crpi-31mraxd99y2gqdgr.cn-beijing.personal.cr.aliyuncs.com/ruoyi_ai/hzhub-ai-web:latest
container_name: hzhub-ai-web
restart: always
ports:
- "25137:5137"
environment:
UPSTREAM_URL: http://backend:6039
depends_on:
- backend
networks:
- ruoyi-net
# ==================== 网络配置 ====================
networks:
ruoyi-net:
driver: bridge
# ==================== 数据卷配置 ====================
volumes:
mysql-data:
redis-data:
weaviate-data:
minio-data:
logs-data:
upload-data:

View File

@@ -0,0 +1,24 @@
version: '3.8'
services:
# ==================== HZHub-AI 前端服务 ====================
frontend:
build:
context: .
dockerfile: Dockerfile.frontend
container_name: hzhub-ai-web
restart: always
ports:
- "5137:5137"
environment:
# 后端 API 地址 - 运行时通过 nginx 代理
# 在完整 docker-compose 中应设置为: http://backend:6039
# 独立运行时可设置为实际后端地址
UPSTREAM_URL: http://host.docker.internal:26039
networks:
- ruoyi-net
# ==================== 网络配置 ====================
networks:
ruoyi-net:
driver: bridge

View 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图标

View 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 浏览器测试
- 响应式布局测试(桌面端、平板、移动端)

View 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 个标签页)
- 固定标签页与普通标签页混排测试

View File

@@ -0,0 +1,38 @@
import antfu from "@antfu/eslint-config";
// 使用 antfu 配置
export default antfu({
vue: {
"vue/block-order": [
"error",
{
// 块顺序
order: ["script", "template", "style"],
},
],
},
typescript: true,
stylistic: {
indent: 2, // 缩进
semi: true, // 语句分号
quotes: "single", // 单引号
},
rules: {
"new-cap": ["off", { newIsCap: true, capIsNew: false }],
"no-console": "off", // 忽略console
},
ignores: [
"**/dist/**",
"**/node_modules/**",
"**/build/**",
"**/lib/**",
"**/es/**",
"**/types/**",
"**/public/**",
"**/vite.config.ts",
"**/eslint.config.js",
"./*.cjs",
"./*.js",
"./package.json",
],
});

View File

@@ -0,0 +1,16 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>%VITE_WEB_TITLE%</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

View File

@@ -0,0 +1,21 @@
MIT License
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
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

16
hzhub-portal-employee/logs.sh Executable file
View 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"

View File

@@ -0,0 +1,75 @@
server {
listen 5137;
server_name localhost;
# gzip 压缩
gzip on;
gzip_vary on;
gzip_min_length 1k;
gzip_comp_level 6;
gzip_types text/plain text/css text/xml text/javascript application/json application/javascript application/xml+rss application/rss+xml font/truetype font/opentype application/vnd.ms-fontobject image/svg+xml;
gzip_disable "MSIE [1-6]\.";
# 静态文件目录
root /usr/share/nginx/html;
index index.html;
# 访问日志
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log;
# 前端路由 fallback
location / {
try_files $uri $uri/ /index.html;
}
# API 代理 - 代理所有后端 API 请求
# 匹配常见的后端 API 路径前缀
location ~ ^/(system|chat|auth|resource|agent|knowledge|workflow)/ {
proxy_pass ${UPSTREAM_URL};
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# 超时设置
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
# 支持 WebSocket
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
# 兼容 /api 前缀的请求
location /api {
proxy_pass ${UPSTREAM_URL};
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# 超时设置
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
# 支持 WebSocket
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
# 静态资源缓存
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
expires 30d;
add_header Cache-Control "public, immutable";
}
# 禁止访问隐藏文件
location ~ /\. {
deny all;
}
}

17297
hzhub-portal-employee/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,88 @@
{
"name": "hzhub-portal-employee",
"type": "module",
"version": "0.0.0",
"private": true,
"description": "HZHub Employee Portal - 企业员工门户系统",
"author": {
"name": "HZHub Team",
"email": "hzhub@example.com",
"url": "https://github.com/hzhub"
},
"license": "MIT",
"homepage": "https://github.com/hzhub/hzhub-portal-employee",
"repository": {
"type": "git",
"url": "git@github.com:hzhub/hzhub-portal-employee.git"
},
"bugs": {
"url": "https://github.com/hzhub/hzhub-portal-employee/issues"
},
"scripts": {
"dev": "vite",
"build": "vue-tsc -b && vite build",
"preview": "vite preview",
"prepare": "husky",
"lint": "eslint .",
"lint:stylelint": "stylelint --cache --fix \"**/*.{vue,less,postcss,css,scss}\" --cache --cache-location node_modules/.cache/stylelint/",
"fix": "eslint . --fix"
},
"dependencies": {
"@element-plus/icons-vue": "^2.3.1",
"@floating-ui/core": "^1.7.2",
"@floating-ui/dom": "^1.7.2",
"@floating-ui/vue": "^1.1.7",
"@jsonlee_12138/enum": "^1.0.4",
"@vueuse/core": "^13.5.0",
"@vueuse/integrations": "^13.5.0",
"echarts": "^6.0.0",
"element-plus": "^2.10.4",
"hook-fetch": "2.0.4-beta.1",
"nprogress": "^0.2.0",
"pinia": "^3.0.3",
"pinia-plugin-persistedstate": "^4.4.1",
"qrcode": "^1.5.4",
"radash": "^12.1.1",
"reset-css": "^5.0.2",
"vue": "^3.5.17",
"vue-element-plus-x": "1.3.0",
"vue-router": "4"
},
"devDependencies": {
"@antfu/eslint-config": "^4.16.2",
"@changesets/cli": "^2.29.5",
"@commitlint/config-conventional": "^19.8.1",
"@vitejs/plugin-vue": "^6.0.0",
"@vue/tsconfig": "^0.7.0",
"commitlint": "^19.8.1",
"cz-git": "^1.12.0",
"eslint": "^9.31.0",
"husky": "^9.1.7",
"lint-staged": "^16.1.2",
"postcss": "8.4.31",
"postcss-html": "1.5.0",
"prettier": "^3.6.2",
"sass-embedded": "^1.89.2",
"stylelint": "^16.21.1",
"stylelint-config-html": "^1.1.0",
"stylelint-config-recess-order": "^7.1.0",
"stylelint-config-recommended-scss": "^15.0.1",
"stylelint-config-recommended-vue": "^1.6.1",
"stylelint-config-standard": "^38.0.0",
"stylelint-config-standard-scss": "^15.0.1",
"typescript": "~5.8.3",
"typescript-api-pro": "^0.0.7",
"unocss": "66.3.3",
"unplugin-auto-import": "^19.3.0",
"unplugin-vue-components": "^28.8.0",
"vite": "6.0.0",
"vite-plugin-env-typed": "^0.0.2",
"vite-plugin-svg-icons": "^2.0.1",
"vue-tsc": "^3.0.1"
},
"config": {
"commitizen": {
"path": "node_modules/cz-git"
}
}
}

9935
hzhub-portal-employee/pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@@ -0,0 +1,35 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="32px" height="32px" viewBox="0 0 32 32" enable-background="new 0 0 32 32" xml:space="preserve"> <image id="image0" width="32" height="32" x="0" y="0"
xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAIGNIUk0AAHomAACAhAAA+gAAAIDo
AAB1MAAA6mAAADqYAAAXcJy6UTwAAAAGYktHRAD/AP8A/6C9p5MAAAW1SURBVFjD7ZZdjF1VGYbf
b6299t7n/8ycOZ1OO+m0FSkFKVjpBSogTY20EocwiT+ICojaqMC0kaBoMI0RApW2JNUE00TShGoI
NcGCFrCmMaMQbYq2QinD37Sd/86cnjPn7N+11ue1sad2vOFmntu1kvfJu76sfMACC3zA0IUOpXTx
9I+O4Ivb1uDlR6fgCBdWWTS5iYIq4Z8TRzCjInSvuhE6mAFlizCSEMgEe5/9FgY2bMPjm9chTcL/
T+Dgw6dQ13VcmrsMk8nZjO9kSkZxPDp35Fyl+zqObBOzrgCrPMYmXqTysuvLRgovRFiPuqphNHkC
5OexvX9Z2wznQgJj6Rl8quNaTMSNG8tO+QEjsFQIhD2dn3gjsno/rPkTMwDW67uXbhhIiC63knzj
qnGOgsc6Fq8+VBt9DUQSzOb8LbcL33z7z7B+yWcwl4bLPOnvZYHxFGanFvaoFlhuHHl36nqfto77
eSPEF1jgfQ3z61Ty7xNXrDau/HKkWy+IfKWu2MX7xw7PrwFrLTzhwcKuBUTBUPygA2c4EYzTyZnn
ym7vrpTE3Y3mNITn7ZGuvU/45SBRjBDRSZD/knGctUTylEnT+T+BAIGIQEwZMCfamlYiDZo26VyU
6X3YCOcma8ItJIVSufJgKq3WHDzI7NQE25YGYkOUISIYMNrntGFlXx+MsLCCR1hQTgqnr8XNUkbm
dzsQm4yJ7kHg7zp9zmzXNr7XEN1s3dxuN4xKLFUfiHIMPsWwuHLtx9oKtJ2BidEYm9b1w8I0Jan+
hHWVhL9RSrUhQfLNxaXi8zPxOFTBotDXfWJurjVsPPXdxJUrEpiPsFIFpmQnsY2e+ckP0Jp5e34N
HH/vdxhunUROlGqazW+0tXfNBbWbwXpwSab4h+EzJ6BbLYQmxtg7w9DV0vMp0q2pFLdEc/U706Cx
z5wNaq3xf2Hq5MH5P8GOwRewprgOb82+iTONUyNBGijPzb472Rr/40h9Er7XgeNvD+H144eRFkto
1iZRb4y9NHdu4l2ZL3qWzYjJSODKa3HH06/OX+DD3atRS2fQUahUFxWXDqacDCsvt6pY7Nk6lYxK
60hcc00/Vn2yH2AGTZ6WfkfvoMwWVljot2S5vFXkVVXVGhA9vfMXmNCnUXUqyIjsVxyhPlTMl+80
0uyA49xX7Vh1z2z9DREKhjKEeOSocHtX3wsptzh+bhf58q5EYmXq+7dzqYjM1PT8hvD+b+zBxxet
R2Siqiv9xy3xy9cvzj/5j3D6CEm3yzjOFlHsYZMRf0+CuuMtveJ77LgPGTK/itPaTzNXL3pvrpGu
SJX8XKyb+9POzqCMIkaOHvqvrPP+A77nwyMPTPYqYiyzMN9/ZTyEyGbDIKr9kPKVxAh6oDY7fYkq
V6HJHWCT/pKjuR9n/Hw4PhoBsAeslLdZqdZYIQ6pTOa8DZxXoFKoQAgJA14CQBpOa4mQcLQAZavN
uq4/EsR6uaNyX9daI2hMP4esfMR0drYaMgIZAy24ZgmSSfRACJRXLL94gaDZhKkYWPAYCVLS8b7d
4pAZglknH/WcwgB5Yp1A+ueJ1hhUd88Nadbdp23yLMi+5sQWJud9hwW5sHacLWDj5OIFUp0ioghs
zSueEHtJyK95yN3Kkq0VAAkcZ5j7dVz/baHShUTJAUt0BztqOykFVhAQUDD6KUTBq0JImDYCbfeB
v+4MoMmgxc2cJ/PXpSQuiyXCWJjXI4qP2XxXoxlNo5kjcLEL59LpYpL11ySevCJRlEkde0InrSGb
zbZYAPuuKlx8AwBwbOpvuLTnakjjtATJg0qog5oYBAZA+OptwC+eOItZ30FpYxfoADWIxBBJd4gc
ARYxWEoQA3R4qF3MhTciAHjmsZMwVmNl3+WYDUNs+/kt2HTDZjy049b/uPelR/fjnT1P4rN/eRFO
FRg78CZIKezeeMn/ilhggQU+WP4Ne/S05BHkAUAAAAAldEVYdGRhdGU6Y3JlYXRlADIwMjYtMDMt
MTlUMDk6MDA6NDArMDA6MDCEOXtUAAAAJXRFWHRkYXRlOm1vZGlmeQAyMDI2LTAzLTE5VDA5OjAw
OjQwKzAwOjAw9WTD6AAAACh0RVh0ZGF0ZTp0aW1lc3RhbXAAMjAyNi0wMy0xOVQwOTowMDo0MCsw
MDowMKJx4jcAAAAASUVORK5CYII=" />
</svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

View 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"

View File

@@ -0,0 +1,7 @@
<script setup lang="ts"></script>
<template>
<router-view />
</template>
<style scoped lang="scss"></style>

View File

@@ -0,0 +1,10 @@
import type { EmailCodeDTO, LoginDTO, LoginVO, RegisterDTO } from './types';
import { post } from '@/utils/request';
export const login = (data: LoginDTO) => post<LoginVO>('/auth/login', data).json();
// 邮箱验证码
export const emailCode = (data: EmailCodeDTO) => post('/resource/email/code', data).json();
// 注册账号
export const register = (data: RegisterDTO) => post('/auth/register', data).json();

View File

@@ -0,0 +1,150 @@
export interface LoginDTO {
username: string;
password: string;
code?: string;
clientId?: string;
grantType?: string;
tenantId?: string;
uuid?: string;
// 二次确认密码
confirmPassword?: string;
}
export interface LoginVO {
access_token?: string;
token?: string;
userInfo?: LoginUser;
}
/**
* LoginUser登录用户身份权限
*/
export interface LoginUser {
/**
* 微信头像
*/
avatar?: string;
/**
* 浏览器类型
*/
browser?: string;
/**
* 部门ID
*/
deptId?: number;
/**
* 部门名
*/
deptName?: string;
/**
* 过期时间
*/
expireTime?: number;
/**
* 登录IP地址
*/
ipaddr?: string;
/**
* 获取登录id
*/
loginId?: string;
/**
* 登录地点
*/
loginLocation?: string;
/**
* 登录时间
*/
loginTime?: number;
/**
* 菜单权限
*/
menuPermission?: string[];
/**
* 用户名
*/
nickName?: string;
/**
* 操作系统
*/
os?: string;
/**
* 数据权限 当前角色ID
*/
roleId?: number;
/**
* 角色权限
*/
rolePermission?: string[];
/**
* 角色对象
*/
roles?: RoleDTO[];
/**
* 租户ID
*/
tenantId?: string;
/**
* 用户唯一标识
*/
token?: string;
/**
* 用户ID
*/
userId?: number;
/**
* 用户名
*/
username?: string;
/**
* 用户类型
*/
userType?: string;
}
/**
* RoleDTO角色
*/
export interface RoleDTO {
/**
* 数据范围1所有数据权限2自定义数据权限3本部门数据权限4本部门及以下数据权限5仅本人数据权限
*/
dataScope?: string;
/**
* 角色ID
*/
roleId?: number;
/**
* 角色权限
*/
roleKey?: string;
/**
* 角色名称
*/
roleName?: string;
}
// 邮箱验证码
export interface EmailCodeDTO {
username?: string;
}
// 邮箱注册
export interface RegisterDTO {
/**
* 邮箱
*/
username: string;
/**
* 密码
*/
password: string;
/**
* 验证码
*/
code: string;
/**
* 确认密码
*/
confirmPassword?: string;
}

View File

@@ -0,0 +1,30 @@
import type { ChatMessageVo, GetChatListParams, SendDTO, workflowVo } from './types';
import { get, post } from '@/utils/request';
// 发送消息
export const send = (data: SendDTO) => post('/chat/send', data);
// 新增对应会话聊天记录
export function addChat(data: ChatMessageVo) {
return post('/system/message', data).json();
}
// 获取当前会话的聊天记录
export function getChatList(params: GetChatListParams) {
return get<ChatMessageVo[]>('/system/message/list', params).json();
}
// 获取知识库列表
export function getKnowledgeList() {
return get('/system/info/list').json();
}
// 获取工作流列表
export function getWorkflowList(params: workflowVo) {
// 将参数转换为查询字符串
const queryString = new URLSearchParams(params as Record<string, string>).toString();
// 拼接 URL
const url = `/admin/workflow/search?${queryString}`;
// 发送 POST 请求
return post<workflowVo[]>(url, {}).json();
}

View File

@@ -0,0 +1,283 @@
/**
* 工作流请求体
*/
export interface WorkFlowRunner {
workflowId?: string;
inputs?: {
name: string;
type: string;
content: {
value: string;
type: string;
};
}[];
}
/**
* 人机交互信息体
*/
export interface ReSumeRunner {
runtimeUuid?: string;
feedbackContent?: string;
}
/**
* SSE 事件类型
*/
export type SseEventType = 'content' | 'reasoning' | 'done' | 'error' | 'message';
/**
* SSE 事件数据
*/
export interface SseEventData {
/**
* 事件类型
*/
event: SseEventType;
/**
* 消息内容
*/
content?: string;
/**
* 推理内容(深度思考模式)
*/
reasoningContent?: string;
/**
* 错误信息
*/
error?: string;
/**
* 是否完成
*/
done?: boolean;
}
/**
* ChatRequest描述对话请求对象
*/
export interface SendDTO {
/**
* 传入的模型(必填)
*/
model: string;
/**
* 对话消息(必填)
*/
content: string;
/**
* 工作流请求体
*/
workFlowRunner?: WorkFlowRunner;
/**
* 人机交互信息体
*/
reSumeRunner?: ReSumeRunner;
/**
* 是否为人机交互用户继续输入
*/
isResume?: boolean;
/**
* 是否启用工作流
*/
enableWorkFlow?: boolean;
/**
* 会话id
*/
sessionId?: string;
/**
* 知识库id
*/
knowledgeId?: string;
/**
* 应用ID
*/
appId?: string;
/**
* 对话id(每个聊天窗口都不一样)
*/
uuid?: number;
/**
* 是否启用深度思考
*/
enableThinking?: boolean;
/**
* 是否支持联网
*/
enableInternet?: boolean;
}
/**
* Message描述
*/
export interface Message {
content?: string;
name?: string;
reasoning_content?: string;
/**
* 目前支持四个中角色参考官网,进行情景输入:
* https://platform.openai.com/docs/guides/chat/introduction
*/
role?: 'system' | 'user' | 'assistant' | 'function' | 'tool';
tool_call_id?: string;
/**
* The tool calls generated by the model, such as function calls.
*/
tool_calls?: ToolCalls[];
}
/**
* ToolCallsThe tool calls generated by the model, such as function calls.
*/
export interface ToolCalls {
function?: ToolCallFunction;
/**
* The ID of the tool call.
*/
id?: string;
/**
* The type of the tool. Currently, only function is supported.
*/
type?: string;
}
/**
* ToolCallFunctionToolCall 的 Function参数
* The function that the model called.
*/
export interface ToolCallFunction {
/**
* 方法参数
*/
arguments?: string;
/**
* 方法名
*/
name?: string;
}
export interface GetChatListParams {
/**
* 消息内容
*/
content?: string;
/**
* 创建者
*/
createBy?: number;
/**
* 创建部门
*/
createDept?: number;
/**
* 创建时间
*/
createTime?: Date;
/**
* 扣除金额
*/
deductCost?: number;
/**
* 主键
*/
id?: number;
/**
* 排序的方向desc或者asc
*/
isAsc?: string;
/**
* 模型名称
*/
modelName?: string;
/**
* 排序列
*/
orderByColumn?: string;
/**
* 当前页数
*/
pageNum?: number;
/**
* 分页大小
*/
pageSize?: number;
/**
* 请求参数
*/
params?: { [key: string]: { [key: string]: any } };
/**
* 备注
*/
remark?: string;
/**
* 对话角色
*/
role?: string;
/**
* 会话id
*/
sessionId?: string;
/**
* 累计 Tokens
*/
totalTokens?: number;
/**
* 更新者
*/
updateBy?: number;
/**
* 更新时间
*/
updateTime?: Date;
/**
* 用户id
*/
userId?: number;
}
/**
* ChatMessageVo聊天消息视图对象 chat_message
*/
export interface ChatMessageVo {
/**
* 消息内容
*/
content?: string;
/**
* 扣除金额
*/
deductCost?: number;
/**
* 主键
*/
id?: number;
/**
* 模型名称
*/
modelName?: string;
/**
* 备注
*/
remark?: string;
/**
* 对话角色
*/
role?: string;
/**
* 会话id
*/
sessionId?: string;
/**
* 累计 Tokens
*/
totalTokens?: number;
/**
* 用户id
*/
userId?: number;
}
export interface workflowVo {
currentPage?: number;
pageSize?: number;
}

View File

@@ -0,0 +1,4 @@
export * from './auth';
export * from './chat';
export * from './model';
export * from './session';

View File

@@ -0,0 +1,7 @@
import type { GetSessionListVO } from './types';
import { get } from '@/utils/request';
// 获取当前用户的模型列表
export function getModelList() {
return get<GetSessionListVO[]>('/system/model/modelList').json();
}

View File

@@ -0,0 +1,14 @@
// 查询用户模型列表返回的数据结构
export interface GetSessionListVO {
id?: number;
category?: string;
modelName?: string;
modelDescribe?: string;
modelPrice?: number;
modelType?: string;
modelShow?: string;
systemPrompt?: string;
apiHost?: string;
apiKey?: string;
remark?: string;
}

View 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();
}

View 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;
}

View File

@@ -0,0 +1,27 @@
import type {
ChatSessionVo,
CreateSessionDTO,
// CreateSessionVO,
GetSessionListParams,
} from './types';
import { del, get, post, put } from '@/utils/request';
export function get_session_list(params: GetSessionListParams) {
return get<ChatSessionVo[]>('/system/session/list', params).json();
}
export function create_session(data: CreateSessionDTO) {
return post('/system/session', data).json();
}
export function update_session(data: ChatSessionVo) {
return put('/system/session', data).json();
}
export function get_session(id: string) {
return get<ChatSessionVo>(`/system/session/${id}`).json();
}
export function delete_session(ids: string[]) {
return del(`/system/session/${ids}`).json();
}

View File

@@ -0,0 +1,154 @@
import type { Component } from 'vue';
export interface GetSessionListParams {
/**
* 创建者
*/
createBy?: number;
/**
* 创建部门
*/
createDept?: number;
/**
* 创建时间
*/
createTime?: Date;
/**
* 主键
*/
id?: number;
/**
* 排序的方向desc或者asc
*/
isAsc?: string;
/**
* 排序列
*/
orderByColumn?: string;
/**
* 当前页数
*/
pageNum?: number;
/**
* 分页大小
*/
pageSize?: number;
/**
* 请求参数
*/
params?: { [key: string]: { [key: string]: any } };
/**
* 备注
*/
remark?: string;
/**
* 会话内容
*/
sessionContent?: string;
/**
* 会话标题
*/
sessionTitle?: string;
/**
* 更新者
*/
updateBy?: number;
/**
* 更新时间
*/
updateTime?: Date;
/**
* 用户id
*/
userId: number;
}
/**
* ChatSessionVo会话管理视图对象 chat_session
*/
export interface ChatSessionVo {
/**
* 主键
*/
// id?: number
id?: string;
/**
* 备注
*/
remark?: string;
/**
* 会话内容
*/
sessionContent?: string;
/**
* 会话标题
*/
sessionTitle?: string;
/**
* 用户id
*/
userId?: number;
/**
* 创建时间
*/
createTime?: Date;
/**
* 自定义的消息前缀图标字段
*/
prefixIcon?: Component;
}
/**
* ChatSessionBo会话管理业务对象 chat_session
*/
export interface CreateSessionDTO {
/**
* 创建者
*/
createBy?: number;
/**
* 创建部门
*/
createDept?: number;
/**
* 创建时间
*/
createTime?: Date;
/**
* 主键
*/
// id?: number;
id?: string;
/**
* 请求参数
*/
params?: { [key: string]: { [key: string]: any } };
/**
* 备注
*/
remark?: string;
/**
* 会话内容
*/
sessionContent?: string;
/**
* 会话标题
*/
sessionTitle: string;
/**
* 更新者
*/
updateBy?: number;
/**
* 更新时间
*/
updateTime?: Date;
/**
* 用户id
*/
userId: number;
}
// export interface CreateSessionVO {
// id: number;
// }

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<g>
<path fill="none" d="M0 0h24v24H0z"/>
<path d="M18.901 10a2.999 2.999 0 0 0 4.075 1.113 3.5 3.5 0 0 1-1.975 3.55L21 21h-6v-2a3 3 0 0 0-5.995-.176L9 19v2H3v-6.336a3.5 3.5 0 0 1-1.979-3.553A2.999 2.999 0 0 0 5.098 10h13.803zm-1.865-7a3.5 3.5 0 0 0 4.446 2.86 3.5 3.5 0 0 1-3.29 3.135L18 9H6a3.5 3.5 0 0 1-3.482-3.14A3.5 3.5 0 0 0 6.964 3h10.072z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 448 B

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<g>
<path fill="none" d="M0 0h24v24H0z"/>
<path fill-rule="nonzero" d="M18.901 10a2.999 2.999 0 0 0 4.075 1.113 3.5 3.5 0 0 1-1.975 3.55L21 21h-7v-2a2 2 0 0 0-1.85-1.995L12 17a2 2 0 0 0-1.995 1.85L10 19v2H3v-6.336a3.5 3.5 0 0 1-1.979-3.553A2.999 2.999 0 0 0 5.098 10h13.803zm-.971 2H6.069l-.076.079c-.431.42-.935.76-1.486 1.002l-.096.039.589.28-.001 5.6 3.002-.001v-.072l.01-.223c.149-2.016 1.78-3.599 3.854-3.698l.208-.005.223.01a4 4 0 0 1 3.699 3.787l.004.201L19 19l.001-5.6.587-.28-.095-.04a5.002 5.002 0 0 1-1.486-1.001L17.93 12zm-.894-9a3.5 3.5 0 0 0 4.446 2.86 3.5 3.5 0 0 1-3.29 3.135L18 9H6a3.5 3.5 0 0 1-3.482-3.14A3.5 3.5 0 0 0 6.964 3h10.072zM15.6 5H8.399a5.507 5.507 0 0 1-1.49 1.816L6.661 7h10.677l-.012-.008a5.518 5.518 0 0 1-1.579-1.722L15.6 5z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 860 B

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<g>
<path fill="none" d="M0 0h24v24H0z"/>
<path d="M12.513 2.001a9.004 9.004 0 0 0 9.97 5.877A4.501 4.501 0 0 1 19 11.888V19l2 .001v2H3v-2h2v-7.113a4.503 4.503 0 0 1-3.484-4.01 9.004 9.004 0 0 0 9.972-5.876h1.025zM17 12H7V19h10v-7z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 332 B

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<g>
<path fill="none" d="M0 0h24v24H0z"/>
<path fill-rule="nonzero" d="M12.513 2.001a9.004 9.004 0 0 0 9.97 5.877A4.501 4.501 0 0 1 19 11.888V19l2 .001v2H3v-2h2v-7.113a4.503 4.503 0 0 1-3.484-4.01 9.004 9.004 0 0 0 9.972-5.876h1.025zM17 12H7V19h10v-7zm-5-6.673l-.11.155A11.012 11.012 0 0 1 5.4 9.736l-.358.073.673.19h12.573l.668-.19-.011-.002a11.01 11.01 0 0 1-6.836-4.326L12 5.326z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 483 B

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<g>
<path fill="none" d="M0 0h24v24H0z"/>
<path d="M2 20h20v2H2v-2zm2-8h2v7H4v-7zm5 0h2v7H9v-7zm4 0h2v7h-2v-7zm5 0h2v7h-2v-7zM2 7l10-5 10 5v4H2V7zm10 1a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 281 B

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<g>
<path fill="none" d="M0 0h24v24H0z"/>
<path d="M2 20h20v2H2v-2zm2-8h2v7H4v-7zm5 0h2v7H9v-7zm4 0h2v7h-2v-7zm5 0h2v7h-2v-7zM2 7l10-5 10 5v4H2V7zm2 1.236V9h16v-.764l-8-4-8 4zM12 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 310 B

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<g>
<path fill="none" d="M0 0h24v24H0z"/>
<path d="M12 19h2V6l6.394 2.74a1 1 0 0 1 .606.92V19h2v2H1v-2h2V5.65a1 1 0 0 1 .594-.914l7.703-3.424A.5.5 0 0 1 12 1.77V19z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 265 B

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<g>
<path fill="none" d="M0 0h24v24H0z"/>
<path d="M3 19V5.7a1 1 0 0 1 .658-.94l9.671-3.516a.5.5 0 0 1 .671.47v4.953l6.316 2.105a1 1 0 0 1 .684.949V19h2v2H1v-2h2zm2 0h7V3.855L5 6.401V19zm14 0v-8.558l-5-1.667V19h5z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 314 B

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<g>
<path fill="none" d="M0 0h24v24H0z"/>
<path d="M10 10.111V1l11 6v14H3V7z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 177 B

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<g>
<path fill="none" d="M0 0h24v24H0z"/>
<path d="M10 10.111V1l11 6v14H3V7l7 3.111zm2-5.742v8.82l-7-3.111V19h14V8.187L12 4.37z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 228 B

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<g>
<path fill="none" d="M0 0h24v24H0z"/>
<path d="M21 20h2v2H1v-2h2V3a1 1 0 0 1 1-1h16a1 1 0 0 1 1 1v17zM8 11v2h3v-2H8zm0-4v2h3V7H8zm0 8v2h3v-2H8zm5 0v2h3v-2h-3zm0-4v2h3v-2h-3zm0-4v2h3V7h-3z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 292 B

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<g>
<path fill="none" d="M0 0h24v24H0z"/>
<path d="M21 20h2v2H1v-2h2V3a1 1 0 0 1 1-1h16a1 1 0 0 1 1 1v17zm-2 0V4H5v16h14zM8 11h3v2H8v-2zm0-4h3v2H8V7zm0 8h3v2H8v-2zm5 0h3v2h-3v-2zm0-4h3v2h-3v-2zm0-4h3v2h-3V7z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 308 B

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<g>
<path fill="none" d="M0 0h24v24H0z"/>
<path d="M21 19h2v2H1v-2h2V4a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v15h2V9h3a1 1 0 0 1 1 1v9zM7 11v2h4v-2H7zm0-4v2h4V7H7z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 256 B

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<g>
<path fill="none" d="M0 0h24v24H0z"/>
<path d="M21 19h2v2H1v-2h2V4a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v15h4v-8h-2V9h3a1 1 0 0 1 1 1v9zM5 5v14h8V5H5zm2 6h4v2H7v-2zm0-4h4v2H7V7z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 275 B

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<g>
<path fill="none" d="M0 0h24v24H0z"/>
<path d="M9 19h3v-6.058L8 9.454l-4 3.488V19h3v-4h2v4zm12 2H3a1 1 0 0 1-1-1v-7.513a1 1 0 0 1 .343-.754L6 8.544V4a1 1 0 0 1 1-1h14a1 1 0 0 1 1 1v16a1 1 0 0 1-1 1zm-5-10v2h2v-2h-2zm0 4v2h2v-2h-2zm0-8v2h2V7h-2zm-4 0v2h2V7h-2z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 364 B

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<g>
<path fill="none" d="M0 0h24v24H0z"/>
<path d="M21 21H3a1 1 0 0 1-1-1v-7.513a1 1 0 0 1 .343-.754L6 8.544V4a1 1 0 0 1 1-1h14a1 1 0 0 1 1 1v16a1 1 0 0 1-1 1zM9 19h3v-6.058L8 9.454l-4 3.488V19h3v-4h2v4zm5 0h6V5H8v2.127c.234 0 .469.082.657.247l5 4.359a1 1 0 0 1 .343.754V19zm2-8h2v2h-2v-2zm0 4h2v2h-2v-2zm0-8h2v2h-2V7zm-4 0h2v2h-2V7z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 434 B

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<g>
<path fill="none" d="M0 0h24v24H0z"/>
<path d="M2 19V8H1V6h3V4a1 1 0 0 1 1-1h14a1 1 0 0 1 1 1v2h3v2h-1v11h1v2H1v-2h1zm11 0v-7h-2v7h2zm-5 0v-7H6v7h2zm10 0v-7h-2v7h2zM6 5v1h12V5H6z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 283 B

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<g>
<path fill="none" d="M0 0h24v24H0z"/>
<path d="M20 6h3v2h-1v11h1v2H1v-2h1V8H1V6h3V4a1 1 0 0 1 1-1h14a1 1 0 0 1 1 1v2zm0 2H4v11h3v-7h2v7h2v-7h2v7h2v-7h2v7h3V8zM6 5v1h12V5H6z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 277 B

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<g>
<path fill="none" d="M0 0h24v24H0z"/>
<path d="M20 20a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1v-9H1l10.327-9.388a1 1 0 0 1 1.346 0L23 11h-3v9z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 236 B

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<g>
<path fill="none" d="M0 0h24v24H0z"/>
<path d="M19 21H5a1 1 0 0 1-1-1v-9H1l10.327-9.388a1 1 0 0 1 1.346 0L23 11h-3v9a1 1 0 0 1-1 1zM6 19h12V9.157l-6-5.454-6 5.454V19z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 271 B

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<g>
<path fill="none" d="M0 0h24v24H0z"/>
<path d="M20 20a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1v-9H1l10.327-9.388a1 1 0 0 1 1.346 0L23 11h-3v9zM8 15v2h8v-2H8z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 251 B

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<g>
<path fill="none" d="M0 0h24v24H0z"/>
<path d="M19 21H5a1 1 0 0 1-1-1v-9H1l10.327-9.388a1 1 0 0 1 1.346 0L23 11h-3v9a1 1 0 0 1-1 1zM6 19h12V9.157l-6-5.454-6 5.454V19zm2-4h8v2H8v-2z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 285 B

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<g>
<path fill="none" d="M0 0h24v24H0z"/>
<path d="M20 20a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1v-9H1l10.327-9.388a1 1 0 0 1 1.346 0L23 11h-3v9zm-9-7v6h2v-6h-2z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 252 B

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<g>
<path fill="none" d="M0 0h24v24H0z"/>
<path d="M19 21H5a1 1 0 0 1-1-1v-9H1l10.327-9.388a1 1 0 0 1 1.346 0L23 11h-3v9a1 1 0 0 1-1 1zm-6-2h5V9.157l-6-5.454-6 5.454V19h5v-6h2v6z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 279 B

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<g>
<path fill="none" d="M0 0h24v24H0z"/>
<path d="M21 20a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V9.49a1 1 0 0 1 .386-.79l8-6.222a1 1 0 0 1 1.228 0l8 6.222a1 1 0 0 1 .386.79V20zm-10-7v6h2v-6h-2z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 285 B

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<g>
<path fill="none" d="M0 0h24v24H0z"/>
<path d="M13 19h6V9.978l-7-5.444-7 5.444V19h6v-6h2v6zm8 1a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V9.49a1 1 0 0 1 .386-.79l8-6.222a1 1 0 0 1 1.228 0l8 6.222a1 1 0 0 1 .386.79V20z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 310 B

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<g>
<path fill="none" d="M0 0h24v24H0z"/>
<path d="M21 20a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V9.49a1 1 0 0 1 .386-.79l8-6.222a1 1 0 0 1 1.228 0l8 6.222a1 1 0 0 1 .386.79V20zM7 15v2h10v-2H7z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 284 B

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<g>
<path fill="none" d="M0 0h24v24H0z"/>
<path d="M21 20a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V9.49a1 1 0 0 1 .386-.79l8-6.222a1 1 0 0 1 1.228 0l8 6.222a1 1 0 0 1 .386.79V20zm-2-1V9.978l-7-5.444-7 5.444V19h14zM7 15h10v2H7v-2z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 319 B

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<g>
<path fill="none" d="M0 0h24v24H0z"/>
<path d="M19 20a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1v-9H0l10.327-9.388a1 1 0 0 1 1.346 0L22 11h-3v9zm-8-5a2.5 2.5 0 1 0 0-5 2.5 2.5 0 0 0 0 5z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 278 B

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<g>
<path fill="none" d="M0 0h24v24H0z"/>
<path d="M19 21H5a1 1 0 0 1-1-1v-9H1l10.327-9.388a1 1 0 0 1 1.346 0L23 11h-3v9a1 1 0 0 1-1 1zM6 19h12V9.157l-6-5.454-6 5.454V19zm6-4a2.5 2.5 0 1 1 0-5 2.5 2.5 0 0 1 0 5z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 312 B

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<g>
<path fill="none" d="M0 0h24v24H0z"/>
<path d="M20 20a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1v-9H1l10.327-9.388a1 1 0 0 1 1.346 0L23 11h-3v9zM9 10v6h6v-6H9zm2 2h2v2h-2v-2z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 266 B

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<g>
<path fill="none" d="M0 0h24v24H0z"/>
<path d="M19 21H5a1 1 0 0 1-1-1v-9H1l10.327-9.388a1 1 0 0 1 1.346 0L23 11h-3v9a1 1 0 0 1-1 1zM6 19h12V9.157l-6-5.454-6 5.454V19zm3-9h6v6H9v-6zm2 2v2h2v-2h-2z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 300 B

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<g>
<path fill="none" d="M0 0h24v24H0z"/>
<path d="M21 20a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V9.49a1 1 0 0 1 .386-.79l8-6.222a1 1 0 0 1 1.228 0l8 6.222a1 1 0 0 1 .386.79V20z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 268 B

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<g>
<path fill="none" d="M0 0h24v24H0z"/>
<path d="M20 20a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1v-9H1l10.327-9.388a1 1 0 0 1 1.346 0L23 11h-3v9zM8.592 13.808l-.991.572 1 1.733.993-.573a3.5 3.5 0 0 0 1.405.811v1.145h2.002V16.35a3.5 3.5 0 0 0 1.405-.81l.992.572L16.4 14.38l-.991-.572a3.504 3.504 0 0 0 0-1.62l.991-.573-1-1.733-.993.573A3.5 3.5 0 0 0 13 9.645V8.5h-2.002v1.144a3.5 3.5 0 0 0-1.405.811l-.992-.573L7.6 11.616l.991.572a3.504 3.504 0 0 0 0 1.62zm3.408.69a1.5 1.5 0 1 1-.002-3.001 1.5 1.5 0 0 1 .002 3z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 602 B

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<g>
<path fill="none" d="M0 0h24v24H0z"/>
<path d="M19 21H5a1 1 0 0 1-1-1v-9H1l10.327-9.388a1 1 0 0 1 1.346 0L23 11h-3v9a1 1 0 0 1-1 1zM6 19h12V9.157l-6-5.454-6 5.454V19zm2.591-5.191a3.508 3.508 0 0 1 0-1.622l-.991-.572 1-1.732.991.573a3.495 3.495 0 0 1 1.404-.812V8.5h2v1.144c.532.159 1.01.44 1.404.812l.991-.573 1 1.731-.991.573a3.508 3.508 0 0 1 0 1.622l.991.572-1 1.731-.991-.572a3.495 3.495 0 0 1-1.404.811v1.145h-2V16.35a3.495 3.495 0 0 1-1.404-.811l-.991.572-1-1.73.991-.573zm3.404.688a1.5 1.5 0 1 0 0-2.998 1.5 1.5 0 0 0 0 2.998z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 638 B

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<g>
<path fill="none" d="M0 0h24v24H0z"/>
<path d="M20 20a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1v-9H1l10.327-9.388a1 1 0 0 1 1.346 0L23 11h-3v9zm-8-3l3.359-3.359a2.25 2.25 0 1 0-3.182-3.182l-.177.177-.177-.177a2.25 2.25 0 1 0-3.182 3.182L12 17z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 336 B

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<g>
<path fill="none" d="M0 0h24v24H0z"/>
<path fill-rule="nonzero" d="M20 20a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1v-9H1l10.327-9.388a1 1 0 0 1 1.346 0L23 11h-3v9zm-2-1V9.157l-6-5.454-6 5.454V19h12zm-6-2l-3.359-3.359a2.25 2.25 0 1 1 3.182-3.182l.177.177.177-.177a2.25 2.25 0 1 1 3.182 3.182L12 17z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 390 B

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<g>
<path fill="none" d="M0 0h24v24H0z"/>
<path d="M21 20a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V9.49a1 1 0 0 1 .386-.79l8-6.222a1 1 0 0 1 1.228 0l8 6.222a1 1 0 0 1 .386.79V20zm-2-1V9.978l-7-5.444-7 5.444V19h14z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 303 B

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<g>
<path fill="none" d="M0 0h24v24H0z"/>
<path d="M21 20a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V9.314a1 1 0 0 1 .38-.785l8-6.311a1 1 0 0 1 1.24 0l8 6.31a1 1 0 0 1 .38.786V20zM7 12a5 5 0 0 0 10 0h-2a3 3 0 0 1-6 0H7z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 307 B

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<g>
<path fill="none" d="M0 0h24v24H0z"/>
<path d="M19 19V9.799l-7-5.522-7 5.522V19h14zm2 1a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V9.314a1 1 0 0 1 .38-.785l8-6.311a1 1 0 0 1 1.24 0l8 6.31a1 1 0 0 1 .38.786V20zM7 12h2a3 3 0 0 0 6 0h2a5 5 0 0 1-10 0z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 340 B

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<g>
<path fill="none" d="M0 0h24v24H0z"/>
<path d="M20 20a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1v-9H1l10.327-9.388a1 1 0 0 1 1.346 0L23 11h-3v9zM7.5 13a4.5 4.5 0 1 0 9 0h-2a2.5 2.5 0 1 1-5 0h-2z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 286 B

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<g>
<path fill="none" d="M0 0h24v24H0z"/>
<path d="M6 19h12V9.157l-6-5.454-6 5.454V19zm13 2H5a1 1 0 0 1-1-1v-9H1l10.327-9.388a1 1 0 0 1 1.346 0L23 11h-3v9a1 1 0 0 1-1 1zM7.5 13h2a2.5 2.5 0 1 0 5 0h2a4.5 4.5 0 1 1-9 0z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 318 B

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<g>
<path fill="none" d="M0 0h24v24H0z"/>
<path d="M20 20a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1v-9H1l10.327-9.388a1 1 0 0 1 1.346 0L23 11h-3v9zM7 11v2a5 5 0 0 1 5 5h2a7 7 0 0 0-7-7zm0 4v3h3a3 3 0 0 0-3-3z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 297 B

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<g>
<path fill="none" d="M0 0h24v24H0z"/>
<path d="M6 19h12V9.157l-6-5.454-6 5.454V19zm13 2H5a1 1 0 0 1-1-1v-9H1l10.327-9.388a1 1 0 0 1 1.346 0L23 11h-3v9a1 1 0 0 1-1 1zM8 10a7 7 0 0 1 7 7h-2a5 5 0 0 0-5-5v-2zm0 4a3 3 0 0 1 3 3H8v-3z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 334 B

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<g>
<path fill="none" d="M0 0h24v24H0z"/>
<path d="M21 20h2v2H1v-2h2V3a1 1 0 0 1 1-1h16a1 1 0 0 1 1 1v17zM11 8H9v2h2v2h2v-2h2V8h-2V6h-2v2zm3 12h2v-6H8v6h2v-4h4v4z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 263 B

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<g>
<path fill="none" d="M0 0h24v24H0z"/>
<path fill-rule="nonzero" d="M8 20v-6h8v6h3V4H5v16h3zm2 0h4v-4h-4v4zm11 0h2v2H1v-2h2V3a1 1 0 0 1 1-1h16a1 1 0 0 1 1 1v17zM11 8V6h2v2h2v2h-2v2h-2v-2H9V8h2z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 297 B

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<g>
<path fill="none" d="M0 0h24v24H0z"/>
<path d="M17 19h2v-8h-6v8h2v-6h2v6zM3 19V4a1 1 0 0 1 1-1h14a1 1 0 0 1 1 1v5h2v10h1v2H2v-2h1zm4-8v2h2v-2H7zm0 4v2h2v-2H7zm0-8v2h2V7H7z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 276 B

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<g>
<path fill="none" d="M0 0h24v24H0z"/>
<path d="M22 21H2v-2h1V4a1 1 0 0 1 1-1h14a1 1 0 0 1 1 1v5h2v10h1v2zm-5-2h2v-8h-6v8h2v-6h2v6zm0-10V5H5v14h6V9h6zM7 11h2v2H7v-2zm0 4h2v2H7v-2zm0-8h2v2H7V7z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 296 B

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<g>
<path fill="none" d="M0 0h24v24H0z"/>
<path d="M22 20v2H2v-2h1v-6.758A4.496 4.496 0 0 1 1 9.5c0-.827.224-1.624.633-2.303L4.345 2.5a1 1 0 0 1 .866-.5H18.79a1 1 0 0 1 .866.5l2.702 4.682A4.496 4.496 0 0 1 21 13.242V20h1zM5.789 4L3.356 8.213a2.5 2.5 0 0 0 4.466 2.216c.335-.837 1.52-.837 1.856 0a2.5 2.5 0 0 0 4.644 0c.335-.837 1.52-.837 1.856 0a2.5 2.5 0 1 0 4.457-2.232L18.21 4H5.79z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 486 B

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<g>
<path fill="none" d="M0 0h24v24H0z"/>
<path d="M21 13.242V20h1v2H2v-2h1v-6.758A4.496 4.496 0 0 1 1 9.5c0-.827.224-1.624.633-2.303L4.345 2.5a1 1 0 0 1 .866-.5H18.79a1 1 0 0 1 .866.5l2.702 4.682A4.496 4.496 0 0 1 21 13.242zm-2 .73a4.496 4.496 0 0 1-3.75-1.36A4.496 4.496 0 0 1 12 14.001a4.496 4.496 0 0 1-3.25-1.387A4.496 4.496 0 0 1 5 13.973V20h14v-6.027zM5.789 4L3.356 8.213a2.5 2.5 0 0 0 4.466 2.216c.335-.837 1.52-.837 1.856 0a2.5 2.5 0 0 0 4.644 0c.335-.837 1.52-.837 1.856 0a2.5 2.5 0 1 0 4.457-2.232L18.21 4H5.79z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 623 B

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<g>
<path fill="none" d="M0 0h24v24H0z"/>
<path d="M21 13v7a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1v-7H2v-2l1-5h18l1 5v2h-1zM5 13v6h14v-6H5zm1 1h8v3H6v-3zM3 3h18v2H3V3z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 259 B

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<g>
<path fill="none" d="M0 0h24v24H0z"/>
<path d="M21 13v7a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1v-7H2v-2l1-5h18l1 5v2h-1zM5 13v6h14v-6H5zm-.96-2h15.92l-.6-3H4.64l-.6 3zM6 14h8v3H6v-3zM3 3h18v2H3V3z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 291 B

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<g>
<path fill="none" d="M0 0h24v24H0z"/>
<path d="M21 11.646V21a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1v-9.354A3.985 3.985 0 0 1 2 9V3a1 1 0 0 1 1-1h18a1 1 0 0 1 1 1v6c0 1.014-.378 1.94-1 2.646zM14 9a1 1 0 0 1 2 0 2 2 0 1 0 4 0V4H4v5a2 2 0 1 0 4 0 1 1 0 1 1 2 0 2 2 0 1 0 4 0z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 368 B

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<g>
<path fill="none" d="M0 0h24v24H0z"/>
<path d="M21 11.646V21a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1v-9.354A3.985 3.985 0 0 1 2 9V3a1 1 0 0 1 1-1h18a1 1 0 0 1 1 1v6c0 1.014-.378 1.94-1 2.646zm-2 1.228a4.007 4.007 0 0 1-4-1.228A3.99 3.99 0 0 1 12 13a3.99 3.99 0 0 1-3-1.354 3.99 3.99 0 0 1-4 1.228V20h14v-7.126zM14 9a1 1 0 0 1 2 0 2 2 0 1 0 4 0V4H4v5a2 2 0 1 0 4 0 1 1 0 1 1 2 0 2 2 0 1 0 4 0z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 487 B

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<g>
<path fill="none" d="M0 0h24v24H0z"/>
<path d="M21 3a1 1 0 0 1 1 1v16a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h18zM9.399 8h-2l-3.2 8h2.154l.4-1h3.29l.4 1h2.155L9.399 8zM19 8h-2v2h-1a3 3 0 0 0-.176 5.995L16 16h3V8zm-2 4v2h-1l-.117-.007a1 1 0 0 1 0-1.986L16 12h1zm-8.601-1.115L9.244 13H7.552l.847-2.115z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 411 B

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<g>
<path fill="none" d="M0 0h24v24H0z"/>
<path fill-rule="nonzero" d="M21 3a1 1 0 0 1 1 1v16a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h18zm-1 2H4v14h16V5zM9.399 8l3.199 8h-2.155l-.4-1h-3.29l-.4 1H4.199l3.2-8h2zM19 8v8h-3a3 3 0 0 1 0-6h.999L17 8h2zm-2 4h-1a1 1 0 0 0-.117 1.993L16 14h1v-2zm-8.601-1.115L7.552 13h1.692l-.845-2.115z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 435 B

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<g>
<path fill="none" d="M0 0h24v24H0z"/>
<path d="M3 13h18v8.002c0 .551-.445.998-.993.998H3.993A.995.995 0 0 1 3 21.002V13zM3 2.998C3 2.447 3.445 2 3.993 2h16.014c.548 0 .993.446.993.998V11H3V2.998zM9 5v2h6V5H9zm0 11v2h6v-2H9z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 328 B

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<g>
<path fill="none" d="M0 0h24v24H0z"/>
<path d="M3 2.992C3 2.444 3.445 2 3.993 2h16.014a1 1 0 0 1 .993.992v18.016a.993.993 0 0 1-.993.992H3.993A1 1 0 0 1 3 21.008V2.992zM19 11V4H5v7h14zm0 2H5v7h14v-7zM9 6h6v2H9V6zm0 9h6v2H9v-2z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 331 B

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<g>
<path fill="none" d="M0 0h24v24H0z"/>
<path d="M3 10h18v10.004c0 .55-.445.996-.993.996H3.993A.994.994 0 0 1 3 20.004V10zm6 2v2h6v-2H9zM2 4c0-.552.455-1 .992-1h18.016c.548 0 .992.444.992 1v4H2V4z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 299 B

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<g>
<path fill="none" d="M0 0h24v24H0z"/>
<path d="M3 10H2V4.003C2 3.449 2.455 3 2.992 3h18.016A.99.99 0 0 1 22 4.003V10h-1v10.001a.996.996 0 0 1-.993.999H3.993A.996.996 0 0 1 3 20.001V10zm16 0H5v9h14v-9zM4 5v3h16V5H4zm5 7h6v2H9v-2z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 333 B

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<g>
<path fill="none" d="M0 0h24v24H0z"/>
<path d="M12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10-4.477 10-10 10zm8-10a8 8 0 1 0-3.968 6.911l-1.008-1.727A6 6 0 1 1 18 12v1a1 1 0 0 1-2 0V9h-1.354a4 4 0 1 0 .066 5.94A3 3 0 0 0 20 13v-1zm-8-2a2 2 0 1 1 0 4 2 2 0 0 1 0-4z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 380 B

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<g>
<path fill="none" d="M0 0h24v24H0z"/>
<path d="M20 12a8 8 0 1 0-3.562 6.657l1.11 1.664A9.953 9.953 0 0 1 12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10v1.5a3.5 3.5 0 0 1-6.396 1.966A5 5 0 1 1 15 8H17v5.5a1.5 1.5 0 0 0 3 0V12zm-8-3a3 3 0 1 0 0 6 3 3 0 0 0 0-6z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 374 B

Some files were not shown because too many files have changed in this diff Show More