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,136 @@
import { h } from 'vue';
import EchartsRenderer from '@/components/EchartsRenderer/index.vue';
/**
* 将 JavaScript 对象字面量转换为有效的 JSON
*/
function convertJsObjectToJson(jsCode: string): string {
let cleaned = jsCode.trim();
// 如果已经是标准 JSON直接返回
try {
JSON.parse(cleaned);
return cleaned;
}
catch {
// 继续处理
}
// 为无引号的键添加引号key: value -> "key": value
cleaned = cleaned.replace(/([{,]\s*)([a-zA-Z_$][a-zA-Z0-9_$]*)\s*:/g, '$1"$2":');
// 处理单引号字符串,转换为双引号
cleaned = cleaned.replace(/'([^']*)'/g, '"$1"');
// 移除末尾的逗号
cleaned = cleaned.replace(/,(\s*[}\]])/g, '$1');
return cleaned;
}
/**
* 检查代码是否是 ECharts 配置
*/
function isEchartsConfig(code: string): boolean {
try {
const trimmed = code.trim();
// 检查是否以 { 开头
if (!trimmed.startsWith('{')) {
return false;
}
// 尝试转换为 JSON 并解析
const jsonStr = convertJsObjectToJson(code);
const obj = JSON.parse(jsonStr);
// 检查是否包含 ECharts 的关键配置字段
return !!(
typeof obj === 'object' &&
obj !== null &&
(obj.xAxis || obj.yAxis || obj.series || obj.title || obj.legend || obj.gauge || obj.pie || obj.bar)
);
}
catch {
return false;
}
}
/**
* 渲染 ECharts 图表
*/
function renderEcharts(code: string) {
console.log('[codeXRender] 渲染 echarts代码长度:', code?.length || 0);
return h(EchartsRenderer, {
selfProps: {
code: code,
width: '100%',
height: '600px', // 增加高度
theme: 'dark',
},
style: {
width: '100%',
maxWidth: '100%'
}
});
}
/**
* 渲染普通代码块
*/
function renderCodeBlock(code: string, language: string) {
console.log('[codeXRender] 渲染代码块,语言:', language, '长度:', code?.length || 0);
return h('pre', [
h('code', {
class: `language-${language}`,
}, code),
]);
}
/**
* XMarkdown code block renderer
* API 格式: { [language]: (props: { raw: { content: string } }) => VNode }
*/
export const codeXRender = {
echarts: (props: { raw: any }) => {
const code = props.raw?.content || '';
console.log('[codeXRender.echarts] 收到代码,长度:', code.length);
return renderEcharts(code);
},
json: (props: { raw: any }) => {
const code = props.raw?.content || '';
console.log('[codeXRender.json] 收到代码,长度:', code.length);
if (isEchartsConfig(code)) {
return renderEcharts(code);
}
return renderCodeBlock(code, 'json');
},
javascript: (props: { raw: any }) => {
const code = props.raw?.content || '';
console.log('[codeXRender.javascript] 收到代码,长度:', code.length);
if (isEchartsConfig(code)) {
return renderEcharts(code);
}
return renderCodeBlock(code, 'javascript');
},
text: (props: { raw: any }) => {
const code = props.raw?.content || '';
if (isEchartsConfig(code)) {
return renderEcharts(code);
}
return renderCodeBlock(code, 'text');
},
'': (props: { raw: any }) => {
const code = props.raw?.content || '';
if (isEchartsConfig(code)) {
return renderEcharts(code);
}
return renderCodeBlock(code, 'plaintext');
},
};
export default codeXRender;

View File

@@ -0,0 +1,114 @@
import type { HookFetchPlugin } from 'hook-fetch';
import { ElMessage } from 'element-plus';
import hookFetch from 'hook-fetch';
import { sseTextDecoderPlugin } from 'hook-fetch/plugins';
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:' })],
});
// 错误信息翻译映射
const errorMessageMap: Record<string, string> = {
'Password input error': '密码输入错误',
'User does not exist': '用户不存在',
'User is not exist': '用户不存在',
'Invalid username or password': '用户名或密码错误',
'Account has been locked': '账号已被锁定',
'Account has been disabled': '账号已被禁用',
'Login expired': '登录已过期',
'no basic auth': '未登录',
};
// 翻译错误信息
function translateError(msg: string): string {
if (!msg)
return '请求失败';
// 精确匹配
if (errorMessageMap[msg]) {
return errorMessageMap[msg];
}
// 部分匹配(处理类似 "Password input error 1 times" 的情况)
for (const [key, value] of Object.entries(errorMessageMap)) {
if (msg.includes(key)) {
// 提取数字(如错误次数)
const match = msg.match(/\d+/);
if (match) {
return `${value} ${match[0]}`;
}
return value;
}
}
// 没有匹配的翻译,返回原始消息
return msg;
}
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);
// 添加租户ID到请求头
if (userStore.userInfo?.tenantId) {
config.headers.set('tenant-id', userStore.userInfo.tenantId);
}
return config;
},
afterResponse: async (response) => {
// console.log(response);
// 成功响应
if (response.result?.code === 200) {
return response;
}
const errorMsg = translateError(response.result?.msg);
// 处理403静默拒绝由业务代码自行处理
if (response.result?.code === 403) {
return Promise.reject(response);
}
// 处理401逻辑
if (response.result?.code === 401) {
userStore.logout();
ElMessage.error(errorMsg || '登录已过期,请重新登录');
return Promise.reject(response);
}
// 其他错误:显示错误信息
ElMessage.error(errorMsg);
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;