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:
136
hzhub-portal-employee/src/utils/markdownRenderers.ts
Normal file
136
hzhub-portal-employee/src/utils/markdownRenderers.ts
Normal 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;
|
||||
114
hzhub-portal-employee/src/utils/request.ts
Normal file
114
hzhub-portal-employee/src/utils/request.ts
Normal 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;
|
||||
Reference in New Issue
Block a user