给所有请求都加上jwt,隐藏生成jwt的secret(放到.env中),隐藏app-secret(放在pm2运行配置文件中,后续直接读取环境配置即可)
This commit is contained in:
@@ -51,8 +51,8 @@ export default function Index() {
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window !== 'undefined') {
|
||||
// setIsPort51708(window.location.port === '51708');
|
||||
setIsPort51708(window.location.port === '5178');
|
||||
setIsPort51708(window.location.port === '51708');
|
||||
// setIsPort51708(window.location.port === '5178');
|
||||
}
|
||||
}, []);
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { type ActionFunctionArgs, json } from "@remix-run/node";
|
||||
import { OAuthClient } from "~/api/login/oauth-client";
|
||||
import { OAUTH_CONFIG } from "~/config/api-config";
|
||||
import { getServerOAuthConfigRuntime } from "~/config/oauth-secret.server";
|
||||
|
||||
/**
|
||||
* 这个Action作为获取OAuth Access Token的服务器端代理。
|
||||
@@ -24,7 +24,8 @@ export async function action({ request }: ActionFunctionArgs) {
|
||||
console.log("🔧 [/api/oauth/token] 收到代理请求, code:", code ? `${code.substring(0, 10)}...` : null);
|
||||
|
||||
// 3. 在服务器端使用OAuthClient安全地获取访问令牌
|
||||
const oauthClient = new OAuthClient(OAUTH_CONFIG);
|
||||
// 🔒 安全:从 .server.ts 文件运行时读取配置,确保环境变量正确加载
|
||||
const oauthClient = new OAuthClient(getServerOAuthConfigRuntime());
|
||||
const tokenResponse = await oauthClient.getAccessToken(code);
|
||||
|
||||
// 4. 处理来自IDaaS服务器的响应
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { type ActionFunctionArgs, json } from "@remix-run/node";
|
||||
import { OAuthClient } from "~/api/login/oauth-client";
|
||||
import { OAUTH_CONFIG } from "~/config/api-config";
|
||||
import { getServerOAuthConfigRuntime } from "~/config/oauth-secret.server";
|
||||
|
||||
/**
|
||||
* 这个Action作为获取用户信息的服务器端代理。
|
||||
@@ -20,7 +20,8 @@ export async function action({ request }: ActionFunctionArgs) {
|
||||
|
||||
console.log("🔧 [/api/oauth/userinfo] 收到代理请求。");
|
||||
|
||||
const oauthClient = new OAuthClient(OAUTH_CONFIG);
|
||||
// 🔒 安全:从 .server.ts 文件运行时读取配置
|
||||
const oauthClient = new OAuthClient(getServerOAuthConfigRuntime());
|
||||
const userInfoResponse = await oauthClient.getUserInfo(accessToken);
|
||||
|
||||
if (!userInfoResponse || !userInfoResponse.success) {
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import { type LoaderFunctionArgs } from "@remix-run/node";
|
||||
import { getUserSession } from "~/api/login/auth.server";
|
||||
import { DOCUMENT_URL } from "~/api/axios-client";
|
||||
|
||||
/**
|
||||
* PDF 代理路由
|
||||
* 用于在服务器端添加 JWT 认证后获取 PDF 文件
|
||||
*/
|
||||
export async function loader({ request }: LoaderFunctionArgs) {
|
||||
try {
|
||||
// 获取 JWT token
|
||||
const { frontendJWT } = await getUserSession(request);
|
||||
|
||||
// 从查询参数获取文件路径
|
||||
const url = new URL(request.url);
|
||||
const filePath = url.searchParams.get("path");
|
||||
|
||||
if (!filePath) {
|
||||
return new Response("缺少文件路径参数", { status: 400 });
|
||||
}
|
||||
|
||||
// 构建完整的文件 URL
|
||||
const fileUrl = `${DOCUMENT_URL}${filePath}`;
|
||||
|
||||
// 使用 JWT 认证获取文件
|
||||
const response = await fetch(fileUrl, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${frontendJWT}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
return new Response(`获取文件失败: ${response.statusText}`, {
|
||||
status: response.status
|
||||
});
|
||||
}
|
||||
|
||||
// 获取文件内容
|
||||
const blob = await response.blob();
|
||||
|
||||
// 返回文件,保持原始的 Content-Type
|
||||
return new Response(blob, {
|
||||
headers: {
|
||||
'Content-Type': response.headers.get('Content-Type') || 'application/pdf',
|
||||
'Cache-Control': 'public, max-age=3600', // 缓存1小时
|
||||
},
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('PDF 代理错误:', error);
|
||||
return new Response(`服务器错误: ${error instanceof Error ? error.message : '未知错误'}`, {
|
||||
status: 500
|
||||
});
|
||||
}
|
||||
}
|
||||
+44
-15
@@ -1,7 +1,21 @@
|
||||
import { type LoaderFunctionArgs, redirect } from "@remix-run/node";
|
||||
import { createUserSession, saveUserInfo } from "~/api/login/auth.server";
|
||||
import { createUserSession, saveUserInfo, sessionStorage } from "~/api/login/auth.server";
|
||||
import { JWTUtils, type UserInfoForJWT } from "~/utils/jwt";
|
||||
|
||||
/**
|
||||
* 辅助函数:使用 session flash 重定向到登录页面并传递错误信息
|
||||
*/
|
||||
async function redirectToLoginWithError(request: Request, errorMessage: string) {
|
||||
const session = await sessionStorage.getSession(request.headers.get("Cookie"));
|
||||
session.flash("loginError", errorMessage);
|
||||
|
||||
return redirect("/login", {
|
||||
headers: {
|
||||
"Set-Cookie": await sessionStorage.commitSession(session)
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export async function loader({ request }: LoaderFunctionArgs) {
|
||||
const url = new URL(request.url);
|
||||
const origin = url.origin; // 获取请求的源 (e.g., "http://10.79.97.17:51703")
|
||||
@@ -21,19 +35,19 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
// 检查是否有错误
|
||||
if (error) {
|
||||
console.error("❌ OAuth2.0授权失败:", error, error_description);
|
||||
return redirect(`/login?error=${encodeURIComponent(error_description || error)}`);
|
||||
return redirectToLoginWithError(request, error_description || error || "授权失败");
|
||||
}
|
||||
|
||||
// 检查是否有授权码
|
||||
if (!code) {
|
||||
console.error("❌ OAuth2.0回调缺少授权码");
|
||||
return redirect("/login?error=missing_code");
|
||||
return redirectToLoginWithError(request, "登录过程中缺少授权码,请重新登录");
|
||||
}
|
||||
|
||||
// 验证状态值
|
||||
if (!state || !state.endsWith("_idp")) {
|
||||
console.error("❌ OAuth2.0状态值验证失败:", { state, expectedSuffix: "_idp" });
|
||||
return redirect("/login?error=invalid_state");
|
||||
return redirectToLoginWithError(request, "登录状态验证失败,请重新登录");
|
||||
}
|
||||
|
||||
console.log("✅ OAuth2.0回调参数验证通过");
|
||||
@@ -57,7 +71,7 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
|
||||
if (!proxyResponse.ok || !tokenResponse.success) {
|
||||
console.error("❌ [Callback] 通过内部代理获取访问令牌失败:", tokenResponse);
|
||||
return redirect("/login?error=token_proxy_error");
|
||||
return redirectToLoginWithError(request, "获取访问令牌失败,请重新登录");
|
||||
}
|
||||
|
||||
// --- 修改结束 ---
|
||||
@@ -78,7 +92,7 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
|
||||
if (!userInfoProxyResponse.ok || !userInfoResponse.success) {
|
||||
console.error("❌ [Callback] 通过内部代理获取用户信息失败:", userInfoResponse);
|
||||
return redirect("/login?error=userinfo_proxy_error");
|
||||
return redirectToLoginWithError(request, "获取用户信息失败,请重新登录");
|
||||
}
|
||||
|
||||
// 将代理返回的用户信息包装成与原有一致的结构
|
||||
@@ -95,19 +109,34 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
|
||||
// 获取重定向URL
|
||||
const redirectTo = url.searchParams.get("redirect") || "/";
|
||||
|
||||
|
||||
// 先生成一个临时 JWT
|
||||
const tempUserInfo = {
|
||||
sub: userInfo.data.sub,
|
||||
user_id: userInfo.data.user_id || "",
|
||||
username: userInfo.data.username,
|
||||
nick_name: userInfo.data.nick_name,
|
||||
email: userInfo.data.email,
|
||||
phone_number: userInfo.data.phone_number,
|
||||
ou_id: userInfo.data.ou_id,
|
||||
ou_name: userInfo.data.ou_name,
|
||||
is_leader: userInfo.data.is_leader,
|
||||
user_role: userRole as 'common' | 'developer'
|
||||
};
|
||||
const tempToken = JWTUtils.generateJWT(tempUserInfo, tokenResponse.expires_in);
|
||||
|
||||
// 成功获取用户信息之后通过auth.server.ts中的saveUserInfo方法去写入自己的数据库中,通过sub作为唯一值去添加数据
|
||||
const saveResult = await saveUserInfo(userInfo.data);
|
||||
const saveResult = await saveUserInfo(userInfo.data, tempToken);
|
||||
if (!saveResult.success) {
|
||||
console.error("保存用户信息到数据库失败:", saveResult.error);
|
||||
// 注意:即使保存到数据库失败,我们仍然继续登录流程,因为用户已经通过了身份验证
|
||||
return redirect("/login?error=save_user_error");
|
||||
return redirectToLoginWithError(request, "保存用户信息失败,请重新登录");
|
||||
}
|
||||
|
||||
|
||||
console.log("用户信息已成功保存到数据库");
|
||||
const savedUserData = saveResult.data!;
|
||||
|
||||
// 生成前端专用JWT
|
||||
|
||||
// 生成前端专用JWT(使用完整的用户信息,包括数据库 ID)
|
||||
const jwtUserInfo: UserInfoForJWT = {
|
||||
sub: userInfo.data.sub,
|
||||
user_id: savedUserData.id!,
|
||||
@@ -120,7 +149,7 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
is_leader: savedUserData.is_leader,
|
||||
user_role: userRole as 'common' | 'developer'
|
||||
};
|
||||
|
||||
|
||||
const frontendJWT = JWTUtils.generateJWT(jwtUserInfo, tokenResponse.expires_in);
|
||||
console.log("前端JWT已生成");
|
||||
|
||||
@@ -143,7 +172,7 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
// 使用统一的session创建函数
|
||||
return createUserSession({
|
||||
isAuthenticated: true,
|
||||
userRole: userRole as 'common' | 'developer',
|
||||
userRole: userRole,
|
||||
redirectTo,
|
||||
accessToken: tokenResponse.access_token,
|
||||
refreshToken: tokenResponse.refresh_token,
|
||||
@@ -154,7 +183,7 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
|
||||
} catch (error) {
|
||||
console.error("OAuth2.0回调处理失败:", error);
|
||||
return redirect("/login?error=callback_error");
|
||||
return redirectToLoginWithError(request, "登录回调处理失败,请重新登录");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import { getConfigLists, getConfigOptions, updateConfigStatus, type ConfigItem }
|
||||
import configListsStyles from "~/styles/pages/config-lists_index.css?url";
|
||||
import { toastService } from "~/components/ui/Toast";
|
||||
import { messageService } from "~/components/ui/MessageModal";
|
||||
import { getUserSession } from "~/api/login/auth.server";
|
||||
|
||||
export const links = () => [
|
||||
{ rel: "stylesheet", href: configListsStyles }
|
||||
@@ -72,7 +73,10 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
const is_active = url.searchParams.get("is_active") ? url.searchParams.get("is_active") === "true" : undefined;
|
||||
const currentPage = parseInt(url.searchParams.get("page") || "1", 10);
|
||||
const pageSize = parseInt(url.searchParams.get("pageSize") || "10", 10);
|
||||
|
||||
|
||||
// 获取JWT token
|
||||
const { frontendJWT } = await getUserSession(request);
|
||||
|
||||
try {
|
||||
// 获取配置列表
|
||||
const configsResponse = await getConfigLists({
|
||||
@@ -82,14 +86,14 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
is_active,
|
||||
page: currentPage,
|
||||
pageSize
|
||||
});
|
||||
}, frontendJWT);
|
||||
|
||||
if (configsResponse.error || !configsResponse.data) {
|
||||
throw new Error(configsResponse.error || "获取配置列表失败");
|
||||
}
|
||||
|
||||
// 获取配置选项
|
||||
const optionsResponse = await getConfigOptions();
|
||||
const optionsResponse = await getConfigOptions(frontendJWT);
|
||||
|
||||
if (optionsResponse.error || !optionsResponse.data) {
|
||||
throw new Error(optionsResponse.error || "获取配置选项失败");
|
||||
@@ -121,17 +125,20 @@ export async function action({ request }: ActionFunctionArgs) {
|
||||
const formData = await request.formData();
|
||||
const _action = formData.get('_action');
|
||||
const configId = formData.get('configId');
|
||||
|
||||
|
||||
if (!configId) {
|
||||
return Response.json({ result: false, message: "缺少配置ID" }, { status: 400 });
|
||||
}
|
||||
|
||||
// 获取JWT token
|
||||
const { frontendJWT } = await getUserSession(request);
|
||||
|
||||
// 进行更新启用和禁用的状态
|
||||
try {
|
||||
if (_action === 'toggleStatus') {
|
||||
const is_active = formData.get('is_active') === 'true';
|
||||
|
||||
const response = await updateConfigStatus(parseInt(configId as string), is_active);
|
||||
|
||||
const response = await updateConfigStatus(parseInt(configId as string), is_active, frontendJWT);
|
||||
|
||||
if (response.error) {
|
||||
return Response.json({ result: false, message: response.error }, { status: 500 });
|
||||
|
||||
@@ -7,6 +7,7 @@ import { ENVIRONMENT_LABELS } from "./config-lists._index";
|
||||
import { getConfigOptions, getConfigDetail, createConfig, updateConfig } from "~/api/system_setting/config-lists";
|
||||
import configNewStyles from "~/styles/pages/config-lists_new.css?url";
|
||||
import { toastService } from "~/components/ui/Toast";
|
||||
import { getUserSession } from "~/api/login/auth.server";
|
||||
|
||||
export const links = () => [
|
||||
{ rel: "stylesheet", href: configNewStyles }
|
||||
@@ -113,17 +114,20 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
const url = new URL(request.url);
|
||||
const id = url.searchParams.get("id");
|
||||
let config: ConfigData | undefined = undefined;
|
||||
|
||||
|
||||
// 获取JWT token
|
||||
const { frontendJWT } = await getUserSession(request);
|
||||
|
||||
try {
|
||||
// 获取配置选项
|
||||
const optionsResponse = await getConfigOptions();
|
||||
const optionsResponse = await getConfigOptions(frontendJWT);
|
||||
if (optionsResponse.error) {
|
||||
throw new Error(optionsResponse.error);
|
||||
}
|
||||
|
||||
|
||||
if (id) {
|
||||
// 获取配置详情
|
||||
const detailResponse = await getConfigDetail(id);
|
||||
const detailResponse = await getConfigDetail(id, frontendJWT);
|
||||
if (detailResponse.error) {
|
||||
throw new Error(detailResponse.error);
|
||||
}
|
||||
@@ -159,7 +163,10 @@ export async function action({ request }: ActionFunctionArgs) {
|
||||
const config = formData.get("config") as string;
|
||||
const is_active = formData.get("is_active") === "true";
|
||||
const remark = formData.get("remark") as string;
|
||||
|
||||
|
||||
// 获取JWT token
|
||||
const { frontendJWT } = await getUserSession(request);
|
||||
|
||||
const errors: ActionData["errors"] = {};
|
||||
|
||||
// 表单验证
|
||||
@@ -206,13 +213,13 @@ export async function action({ request }: ActionFunctionArgs) {
|
||||
|
||||
if (id) {
|
||||
// 更新配置
|
||||
const response = await updateConfig(id, configData);
|
||||
const response = await updateConfig(id, configData, frontendJWT);
|
||||
if (response.error) {
|
||||
throw new Error(response.error);
|
||||
}
|
||||
} else {
|
||||
// 创建配置
|
||||
const response = await createConfig(configData);
|
||||
const response = await createConfig(configData, frontendJWT);
|
||||
if (response.error) {
|
||||
throw new Error(response.error);
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { getContractTemplate } from '~/api/contract-template/templates';
|
||||
import type { ContractTemplate } from '~/api/contract-template/templates';
|
||||
import styles from '~/styles/pages/contract-template.css?url';
|
||||
import filePreviewStyles from '~/styles/components/file-preview-isolation.css?url';
|
||||
import { getUserSession } from '~/api/login/auth.server';
|
||||
|
||||
// 导入FilePreview组件
|
||||
import { FilePreview } from '~/components/reviews';
|
||||
@@ -30,20 +31,24 @@ export const handle = {
|
||||
}
|
||||
};
|
||||
|
||||
export async function loader({ params }: LoaderFunctionArgs) {
|
||||
export async function loader({ params, request }: LoaderFunctionArgs) {
|
||||
const templateId = params.id!;
|
||||
|
||||
// 获取 JWT
|
||||
const { frontendJWT } = await getUserSession(request);
|
||||
const jwt = frontendJWT || undefined;
|
||||
|
||||
try {
|
||||
const response = await getContractTemplate(templateId);
|
||||
|
||||
if (response.error) {
|
||||
throw new Response(response.error, { status: response.status || 404 });
|
||||
}
|
||||
|
||||
if (!response.data) {
|
||||
throw new Response('模板未找到', { status: 404 });
|
||||
}
|
||||
|
||||
const response = await getContractTemplate(templateId, jwt);
|
||||
|
||||
if (response.error) {
|
||||
throw new Response(response.error, { status: response.status || 404 });
|
||||
}
|
||||
|
||||
if (!response.data) {
|
||||
throw new Response('模板未找到', { status: 404 });
|
||||
}
|
||||
|
||||
// 添加调试信息
|
||||
// console.log('模板详情数据:', response.data);
|
||||
// console.log('分类信息:', response.data.category);
|
||||
|
||||
@@ -7,6 +7,7 @@ import { Pagination } from '~/components/ui/Pagination';
|
||||
import { getContractTemplates, getContractCategoriesWithCount } from '~/api/contract-template/templates';
|
||||
import type { ContractTemplate, TemplateSearchParams, ContractCategoryWithCount } from '~/api/contract-template/templates';
|
||||
import styles from '~/styles/pages/contract-template.css?url';
|
||||
import { getUserSession } from '~/api/login/auth.server';
|
||||
|
||||
export const links = () => [
|
||||
{ rel: 'stylesheet', href: styles }
|
||||
@@ -47,91 +48,95 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
const page = parseInt(url.searchParams.get('page') || '1');
|
||||
const pageSize = 12
|
||||
|
||||
// 获取 JWT
|
||||
const { frontendJWT } = await getUserSession(request);
|
||||
const jwt = frontendJWT || undefined;
|
||||
|
||||
try {
|
||||
// 根据sortBy值设置数据库排序参数
|
||||
let dbSortBy = 'id';
|
||||
let dbSortOrder: 'asc' | 'desc' = 'asc';
|
||||
|
||||
switch (sortBy) {
|
||||
case 'relevance':
|
||||
dbSortBy = 'id';
|
||||
dbSortOrder = 'asc';
|
||||
break;
|
||||
case 'newest':
|
||||
dbSortBy = 'updated_at';
|
||||
dbSortOrder = 'desc';
|
||||
break;
|
||||
/* case 'popular':
|
||||
// 暂时按创建时间排序,后续可以加入使用频率字段
|
||||
dbSortBy = 'created_at';
|
||||
dbSortOrder = 'desc';
|
||||
break;
|
||||
case 'rating':
|
||||
// 暂时按特色推荐排序,后续可以加入评分字段
|
||||
dbSortBy = 'is_featured';
|
||||
dbSortOrder = 'desc';
|
||||
break; */
|
||||
default:
|
||||
dbSortBy = 'id';
|
||||
dbSortOrder = 'asc';
|
||||
}
|
||||
|
||||
switch (sortBy) {
|
||||
case 'relevance':
|
||||
dbSortBy = 'id';
|
||||
dbSortOrder = 'asc';
|
||||
break;
|
||||
case 'newest':
|
||||
dbSortBy = 'updated_at';
|
||||
dbSortOrder = 'desc';
|
||||
break;
|
||||
/* case 'popular':
|
||||
// 暂时按创建时间排序,后续可以加入使用频率字段
|
||||
dbSortBy = 'created_at';
|
||||
dbSortOrder = 'desc';
|
||||
break;
|
||||
case 'rating':
|
||||
// 暂时按特色推荐排序,后续可以加入评分字段
|
||||
dbSortBy = 'is_featured';
|
||||
dbSortOrder = 'desc';
|
||||
break; */
|
||||
default:
|
||||
dbSortBy = 'id';
|
||||
dbSortOrder = 'asc';
|
||||
}
|
||||
|
||||
// 构建搜索参数
|
||||
const searchParams: TemplateSearchParams = {
|
||||
page,
|
||||
pageSize,
|
||||
sortBy: dbSortBy,
|
||||
sortOrder: dbSortOrder
|
||||
};
|
||||
// 构建搜索参数
|
||||
const searchParams: TemplateSearchParams = {
|
||||
page,
|
||||
pageSize,
|
||||
sortBy: dbSortBy,
|
||||
sortOrder: dbSortOrder
|
||||
};
|
||||
|
||||
// 优先使用category_id,其次使用category名称
|
||||
if (category_id) {
|
||||
searchParams.category_id = parseInt(category_id);
|
||||
} else if (category) {
|
||||
searchParams.category = category;
|
||||
}
|
||||
// 优先使用category_id,其次使用category名称
|
||||
if (category_id) {
|
||||
searchParams.category_id = parseInt(category_id);
|
||||
} else if (category) {
|
||||
searchParams.category = category;
|
||||
}
|
||||
|
||||
// 并行获取模板数据和分类数据
|
||||
const [templatesResponse, categoriesResponse] = await Promise.all([
|
||||
getContractTemplates(searchParams),
|
||||
getContractCategoriesWithCount()
|
||||
getContractTemplates({ ...searchParams, token: jwt }),
|
||||
getContractCategoriesWithCount(jwt)
|
||||
]);
|
||||
|
||||
// 处理模板数据
|
||||
if (templatesResponse.error) {
|
||||
console.error('获取模板列表失败:', templatesResponse.error);
|
||||
return {
|
||||
templates: [],
|
||||
total: 0,
|
||||
page,
|
||||
pageSize,
|
||||
category,
|
||||
category_id,
|
||||
type,
|
||||
sortBy,
|
||||
categories: []
|
||||
};
|
||||
}
|
||||
// 处理模板数据
|
||||
if (templatesResponse.error) {
|
||||
console.error('获取模板列表失败:', templatesResponse.error);
|
||||
return {
|
||||
templates: [],
|
||||
total: 0,
|
||||
page,
|
||||
pageSize,
|
||||
category,
|
||||
category_id,
|
||||
type,
|
||||
sortBy,
|
||||
categories: []
|
||||
};
|
||||
}
|
||||
|
||||
// 处理分类数据
|
||||
const categories: ContractCategoryWithCount[] = categoriesResponse.error ? [] : categoriesResponse.data || [];
|
||||
// 处理分类数据
|
||||
const categories: ContractCategoryWithCount[] = categoriesResponse.error ? [] : categoriesResponse.data || [];
|
||||
|
||||
// 转换模板数据格式
|
||||
const transformedTemplates = templatesResponse.data?.templates.map(transformTemplate) || [];
|
||||
// 转换模板数据格式
|
||||
const transformedTemplates = templatesResponse.data?.templates.map(transformTemplate) || [];
|
||||
|
||||
// 注释掉类型筛选,因为数据库中没有type字段且已隐藏该功能
|
||||
/* if (type) {
|
||||
transformedTemplates = transformedTemplates.filter(t => t.type === type);
|
||||
} */
|
||||
// 注释掉类型筛选,因为数据库中没有type字段且已隐藏该功能
|
||||
/* if (type) {
|
||||
transformedTemplates = transformedTemplates.filter(t => t.type === type);
|
||||
} */
|
||||
|
||||
// 获取当前分类信息(用于显示)
|
||||
let currentCategory = '全部';
|
||||
if (category_id) {
|
||||
const cat = categories.find(c => c.id === parseInt(category_id));
|
||||
currentCategory = cat?.name || '全部';
|
||||
} else if (category) {
|
||||
currentCategory = category;
|
||||
}
|
||||
// 获取当前分类信息(用于显示)
|
||||
let currentCategory = '全部';
|
||||
if (category_id) {
|
||||
const cat = categories.find(c => c.id === parseInt(category_id));
|
||||
currentCategory = cat?.name || '全部';
|
||||
} else if (category) {
|
||||
currentCategory = category;
|
||||
}
|
||||
|
||||
return {
|
||||
templates: transformedTemplates,
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import type { MetaFunction } from '@remix-run/node';
|
||||
import type { MetaFunction, LoaderFunctionArgs } from '@remix-run/node';
|
||||
import { useNavigate, useLoaderData } from '@remix-run/react';
|
||||
import { ContractSearchHero } from '~/components/contract-template/ContractSearchHero';
|
||||
import { getContractCategoriesWithCount } from '~/api/contract-template/templates';
|
||||
import type { ContractCategoryWithCount } from '~/api/contract-template/templates';
|
||||
import styles from '~/styles/pages/contract-template.css?url';
|
||||
import { getUserSession } from '~/api/login/auth.server';
|
||||
|
||||
export const links = () => [
|
||||
{ rel: 'stylesheet', href: styles }
|
||||
@@ -42,18 +43,22 @@ function transformCategory(category: ContractCategoryWithCount) {
|
||||
* 加载分类数据
|
||||
* @returns 分类数据
|
||||
*/
|
||||
export async function loader() {
|
||||
export async function loader({ request }: LoaderFunctionArgs) {
|
||||
// 获取 JWT
|
||||
const { frontendJWT } = await getUserSession(request);
|
||||
const jwt = frontendJWT || undefined;
|
||||
|
||||
try {
|
||||
// 使用聚合查询获取分类及其模板数量
|
||||
const categoriesResponse = await getContractCategoriesWithCount();
|
||||
const categoriesResponse = await getContractCategoriesWithCount(jwt);
|
||||
|
||||
// 处理分类数据
|
||||
if (categoriesResponse.error) {
|
||||
console.error('获取分类失败:', categoriesResponse.error);
|
||||
return { categories: [] };
|
||||
}
|
||||
// 处理分类数据
|
||||
if (categoriesResponse.error) {
|
||||
console.error('获取分类失败:', categoriesResponse.error);
|
||||
return { categories: [] };
|
||||
}
|
||||
|
||||
const categories = categoriesResponse.data || [];
|
||||
const categories = categoriesResponse.data || [];
|
||||
|
||||
// 转换分类数据格式
|
||||
const categoriesWithCount = categories.map(transformCategory);
|
||||
|
||||
@@ -9,6 +9,7 @@ import { Pagination } from '~/components/ui/Pagination';
|
||||
import { searchContractTemplates, getContractCategories } from '~/api/contract-template/templates';
|
||||
import type { ContractTemplate, ContractCategory } from '~/api/contract-template/templates';
|
||||
import styles from '~/styles/pages/contract-template.css?url';
|
||||
import { getUserSession } from '~/api/login/auth.server';
|
||||
|
||||
export const links = () => [
|
||||
{ rel: 'stylesheet', href: styles }
|
||||
@@ -105,6 +106,10 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
dbSortOrder = 'asc';
|
||||
}
|
||||
|
||||
// 获取 JWT
|
||||
const { frontendJWT } = await getUserSession(request);
|
||||
const jwt = frontendJWT || undefined;
|
||||
|
||||
try {
|
||||
// 并行获取搜索结果和分类数据
|
||||
const [searchResponse, categoriesResponse] = await Promise.all([
|
||||
@@ -113,67 +118,69 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
page,
|
||||
pageSize,
|
||||
sortBy: dbSortBy,
|
||||
sortOrder: dbSortOrder
|
||||
sortOrder: dbSortOrder,
|
||||
token: jwt
|
||||
}),
|
||||
getContractCategories()
|
||||
getContractCategories(jwt)
|
||||
]);
|
||||
|
||||
// 处理搜索结果
|
||||
if (searchResponse.error) {
|
||||
console.error('搜索合同模板失败:', searchResponse.error);
|
||||
return {
|
||||
results: [],
|
||||
query,
|
||||
category,
|
||||
total: 0,
|
||||
page,
|
||||
pageSize,
|
||||
sortBy,
|
||||
searchTime: '搜索失败',
|
||||
categories: []
|
||||
};
|
||||
}
|
||||
// 处理搜索结果
|
||||
if (searchResponse.error) {
|
||||
console.error('搜索合同模板失败:', searchResponse.error);
|
||||
return {
|
||||
results: [],
|
||||
query,
|
||||
category,
|
||||
total: 0,
|
||||
page,
|
||||
pageSize,
|
||||
sortBy,
|
||||
searchTime: '搜索失败',
|
||||
categories: []
|
||||
};
|
||||
}
|
||||
|
||||
// 处理分类数据
|
||||
const categories = categoriesResponse.error ? [] : categoriesResponse.data || [];
|
||||
// 处理分类数据
|
||||
const categories = categoriesResponse.error ? [] : categoriesResponse.data || [];
|
||||
|
||||
// 转换模板数据格式
|
||||
const transformedResults = searchResponse.data?.templates.map(transformTemplate) || [];
|
||||
// 转换模板数据格式
|
||||
const transformedResults = searchResponse.data?.templates.map(transformTemplate) || [];
|
||||
|
||||
// 为每个分类获取搜索结果统计
|
||||
let categoriesWithSearchCount: CategoryWithSearchCount[] = [];
|
||||
if (query && query.trim()) {
|
||||
// 并行为每个分类获取搜索结果数量
|
||||
const categorySearchPromises = categories.map(async (cat): Promise<CategoryWithSearchCount> => {
|
||||
try {
|
||||
const categorySearchResponse = await searchContractTemplates(query, {
|
||||
category: cat.name,
|
||||
page: 1,
|
||||
pageSize: 1000 // 设置较大的pageSize来获取总数
|
||||
});
|
||||
|
||||
const count = categorySearchResponse.error ? 0 : (categorySearchResponse.data?.total || 0);
|
||||
return {
|
||||
...cat,
|
||||
searchCount: count
|
||||
};
|
||||
} catch (error) {
|
||||
console.error(`获取分类${cat.name}的搜索统计失败:`, error);
|
||||
return {
|
||||
...cat,
|
||||
searchCount: 0
|
||||
};
|
||||
}
|
||||
});
|
||||
// 为每个分类获取搜索结果统计
|
||||
let categoriesWithSearchCount: CategoryWithSearchCount[] = [];
|
||||
if (query && query.trim()) {
|
||||
// 并行为每个分类获取搜索结果数量
|
||||
const categorySearchPromises = categories.map(async (cat): Promise<CategoryWithSearchCount> => {
|
||||
try {
|
||||
const categorySearchResponse = await searchContractTemplates(query, {
|
||||
category: cat.name,
|
||||
page: 1,
|
||||
pageSize: 1000, // 设置较大的pageSize来获取总数
|
||||
token: jwt
|
||||
});
|
||||
|
||||
const count = categorySearchResponse.error ? 0 : (categorySearchResponse.data?.total || 0);
|
||||
return {
|
||||
...cat,
|
||||
searchCount: count
|
||||
};
|
||||
} catch (error) {
|
||||
console.error(`获取分类${cat.name}的搜索统计失败:`, error);
|
||||
return {
|
||||
...cat,
|
||||
searchCount: 0
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
categoriesWithSearchCount = await Promise.all(categorySearchPromises);
|
||||
} else {
|
||||
// 如果没有搜索关键词,searchCount设为0
|
||||
categoriesWithSearchCount = categories.map(cat => ({
|
||||
...cat,
|
||||
searchCount: 0
|
||||
}));
|
||||
}
|
||||
categoriesWithSearchCount = await Promise.all(categorySearchPromises);
|
||||
} else {
|
||||
// 如果没有搜索关键词,searchCount设为0
|
||||
categoriesWithSearchCount = categories.map(cat => ({
|
||||
...cat,
|
||||
searchCount: 0
|
||||
}));
|
||||
}
|
||||
|
||||
// 计算搜索耗时
|
||||
const endTime = Date.now();
|
||||
|
||||
@@ -50,6 +50,7 @@ export type LoaderData = {
|
||||
completedTasks: number;
|
||||
};
|
||||
initialLoad?: boolean;
|
||||
frontendJWT?: string; // 新增JWT
|
||||
};
|
||||
|
||||
export async function loader({ request }: LoaderFunctionArgs) {
|
||||
@@ -100,7 +101,8 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
currentPage: tasksResponse.data?.currentPage || params.page,
|
||||
pageSize: tasksResponse.data?.pageSize || params.pageSize,
|
||||
totalPages: tasksResponse.data?.totalPages || 0,
|
||||
stats: statsResponse.data || { totalTasks: 0, pendingTasks: 0, inProgressTasks: 0, completedTasks: 0 }
|
||||
stats: statsResponse.data || { totalTasks: 0, pendingTasks: 0, inProgressTasks: 0, completedTasks: 0 },
|
||||
frontendJWT // 新增:返回JWT给客户端
|
||||
}, {
|
||||
headers: {
|
||||
"Cache-Control": "max-age=60, s-maxage=180"
|
||||
@@ -210,7 +212,7 @@ const docTypeConfig = {
|
||||
|
||||
export default function CrossCheckingIndex() {
|
||||
const loaderData = useLoaderData<typeof loader>();
|
||||
const { tasks, totalCount, currentPage, pageSize, stats } = loaderData;
|
||||
const { tasks, totalCount, currentPage, pageSize, stats, frontendJWT } = loaderData;
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const dateFrom = searchParams.get('dateFrom') || '';
|
||||
const dateTo = searchParams.get('dateTo') || '';
|
||||
@@ -750,6 +752,7 @@ export default function CrossCheckingIndex() {
|
||||
total={modalState.total}
|
||||
onPageChange={handleModalPageChange}
|
||||
onPageSizeChange={handleModalPageSizeChange}
|
||||
frontendJWT={frontendJWT}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -202,7 +202,7 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
const reviewData = await getReviewPoints(id, request);
|
||||
|
||||
// 获取当前登录用户是否是发起人
|
||||
const isProposer = await findIsProposer(taskId, userInfo?.user_id);
|
||||
const isProposer = await findIsProposer(taskId, userInfo?.user_id, frontendJWT);
|
||||
|
||||
// console.log("documentData-------",JSON.stringify(documentData.data,null,2));
|
||||
// console.log("reviewData-------",JSON.stringify('data' in reviewData ? reviewData.data : '',null,2));
|
||||
@@ -560,9 +560,9 @@ export default function CrossCheckingResult() {
|
||||
cancelText: '取消',
|
||||
onConfirm: async () => {
|
||||
setIsLoading(true);
|
||||
const res = await confirmReviewResults(document.id);
|
||||
const res = await confirmReviewResults(document.id, jwtToken);
|
||||
setIsLoading(false);
|
||||
|
||||
|
||||
if (res.error) {
|
||||
toastService.error(res.error);
|
||||
return;
|
||||
|
||||
@@ -42,11 +42,16 @@ interface LoaderData {
|
||||
error?: string;
|
||||
groups: DocumentTypeGroup[];
|
||||
ruleTypes: RuleType[];
|
||||
frontendJWT?: string | null;
|
||||
}
|
||||
|
||||
// 加载函数 - 获取文档类型列表
|
||||
export async function loader({ request }: LoaderFunctionArgs) {
|
||||
try {
|
||||
// 获取用户会话信息
|
||||
const { getUserSession } = await import("~/api/login/auth.server");
|
||||
const { frontendJWT } = await getUserSession(request);
|
||||
|
||||
const url = new URL(request.url);
|
||||
const name = url.searchParams.get('name') || undefined;
|
||||
const ruleType = url.searchParams.get('ruleType') || undefined;
|
||||
@@ -64,13 +69,13 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
};
|
||||
|
||||
// 并行获取文档类型数据和父级评查点分组
|
||||
const ruleTypesResponse = await getRuleTypes();
|
||||
const ruleTypesResponse = await getRuleTypes(undefined, frontendJWT);
|
||||
if(ruleTypesResponse.error){
|
||||
console.error("获取父级评查点分组失败:", ruleTypesResponse.error);
|
||||
}
|
||||
const ruleTypes = ruleTypesResponse.error ? [] : ruleTypesResponse.data;
|
||||
|
||||
const typesResponse = await getDocumentTypes(searchParams);
|
||||
const typesResponse = await getDocumentTypes(searchParams, frontendJWT);
|
||||
if(typesResponse.error){
|
||||
console.error("获取文档类型失败:", typesResponse.error);
|
||||
throw new Error(typesResponse.error);
|
||||
@@ -85,7 +90,8 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
total: typesResponse.data?.total || typesResult.length,
|
||||
pageSize,
|
||||
currentPage: page,
|
||||
ruleTypes
|
||||
ruleTypes,
|
||||
frontendJWT
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("加载文档类型列表失败:", error);
|
||||
@@ -104,10 +110,12 @@ export async function action({ request }: ActionFunctionArgs) {
|
||||
const formData = await request.formData();
|
||||
const id = formData.get("id") as string;
|
||||
const intent = formData.get("intent") as string;
|
||||
const { getUserSession } = await import("~/api/login/auth.server");
|
||||
const { frontendJWT } = await getUserSession(request);
|
||||
|
||||
if (intent === "delete" && id) {
|
||||
try {
|
||||
const result = await deleteDocumentType(id);
|
||||
const result = await deleteDocumentType(id, frontendJWT || undefined);
|
||||
|
||||
if (result.error) {
|
||||
return Response.json({ success: false, error: result.error }, { status: result.status || 500 });
|
||||
@@ -132,7 +140,7 @@ export default function DocumentTypesList() {
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
|
||||
// 获取加载器数据
|
||||
const { types, total, error, ruleTypes } = useLoaderData<LoaderData>();
|
||||
const { types, total, error, ruleTypes, frontendJWT } = useLoaderData<LoaderData>();
|
||||
|
||||
// 状态管理
|
||||
const [ruleGroups, setRuleGroups] = useState<RuleGroup[]>([]);
|
||||
@@ -160,7 +168,7 @@ export default function DocumentTypesList() {
|
||||
const loadRuleGroups = async () => {
|
||||
setLoadingGroups(true);
|
||||
try {
|
||||
const response = await getRuleGroupsByType(ruleTypeParam);
|
||||
const response = await getRuleGroupsByType(ruleTypeParam, frontendJWT || undefined);
|
||||
if (response.data) {
|
||||
setRuleGroups(response.data);
|
||||
} else if (response.error) {
|
||||
|
||||
@@ -62,12 +62,16 @@ interface ActionData {
|
||||
// 获取数据
|
||||
export async function loader({ request }: LoaderFunctionArgs) {
|
||||
try {
|
||||
// 获取用户会话信息
|
||||
const { getUserSession } = await import("~/api/login/auth.server");
|
||||
const { frontendJWT } = await getUserSession(request);
|
||||
|
||||
const url = new URL(request.url);
|
||||
const id = url.searchParams.get("id");
|
||||
const isEdit = id ? true : false;
|
||||
|
||||
// 1. 获取评查点分组 - 使用getAllRuleGroups获取所有分组
|
||||
const ruleGroupsResponse = await getAllRuleGroups();
|
||||
const ruleGroupsResponse = await getAllRuleGroups(frontendJWT);
|
||||
if (ruleGroupsResponse.error) {
|
||||
console.error("获取评查点分组失败:", ruleGroupsResponse.error);
|
||||
// throw new Error(ruleGroupsResponse.error);
|
||||
@@ -80,16 +84,16 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
// 2. 获取各类型的提示词模板
|
||||
const [llmExtractionTemplatesResponse, vlmExtractionTemplatesResponse, evaluationTemplatesResponse, summaryTemplatesResponse] =
|
||||
await Promise.all([
|
||||
getPromptTemplates({ type: TEMPLATE_TYPES.LLM_EXTRACTION }),
|
||||
getPromptTemplates({ type: TEMPLATE_TYPES.VLM_EXTRACTION }),
|
||||
getPromptTemplates({ type: TEMPLATE_TYPES.EVALUATION }),
|
||||
getPromptTemplates({ type: TEMPLATE_TYPES.SUMMARY })
|
||||
getPromptTemplates({ type: TEMPLATE_TYPES.LLM_EXTRACTION }, frontendJWT),
|
||||
getPromptTemplates({ type: TEMPLATE_TYPES.VLM_EXTRACTION }, frontendJWT),
|
||||
getPromptTemplates({ type: TEMPLATE_TYPES.EVALUATION }, frontendJWT),
|
||||
getPromptTemplates({ type: TEMPLATE_TYPES.SUMMARY }, frontendJWT)
|
||||
]);
|
||||
|
||||
// 3. 如果是编辑模式,获取文档类型详情
|
||||
let documentType = undefined;
|
||||
if (id) {
|
||||
const typeResponse = await getDocumentType(id);
|
||||
const typeResponse = await getDocumentType(id, frontendJWT);
|
||||
if (typeResponse.data) {
|
||||
documentType = typeResponse.data;
|
||||
}
|
||||
@@ -121,6 +125,9 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
|
||||
// 处理表单提交
|
||||
export async function action({ request }: ActionFunctionArgs) {
|
||||
const { getUserSession } = await import("~/api/login/auth.server");
|
||||
const { frontendJWT } = await getUserSession(request);
|
||||
|
||||
const formData = await request.formData();
|
||||
const id = formData.get("id") as string | null;
|
||||
const name = formData.get("name") as string;
|
||||
@@ -185,11 +192,11 @@ export async function action({ request }: ActionFunctionArgs) {
|
||||
// 更新文档类型
|
||||
response = await updateDocumentType(id, {
|
||||
...documentTypeData,
|
||||
id: parseInt(id)
|
||||
});
|
||||
id: parseInt(id),
|
||||
}, frontendJWT);
|
||||
} else {
|
||||
// 创建新文档类型
|
||||
response = await createDocumentType(documentTypeData);
|
||||
response = await createDocumentType(documentTypeData, frontendJWT);
|
||||
}
|
||||
|
||||
if (response.error) {
|
||||
|
||||
@@ -46,7 +46,7 @@ export const loader = async ({ request }: LoaderFunctionArgs) => {
|
||||
const pageSize = parseInt(url.searchParams.get("pageSize") || "10", 10);
|
||||
|
||||
// 获取文档类型列表,用于筛选条件
|
||||
const typesResponse = await getDocumentTypes({ pageSize: 500 });
|
||||
const typesResponse = await getDocumentTypes({ pageSize: 500 }, frontendJWT);
|
||||
const documentTypes = typesResponse.data?.types || [];
|
||||
const documentTypeOptions = documentTypes.map(type => ({
|
||||
value: type.id,
|
||||
@@ -126,7 +126,7 @@ export const action = async ({ request }: ActionFunctionArgs) => {
|
||||
try {
|
||||
// 获取用户会话信息
|
||||
const { getUserSession } = await import("~/api/login/auth.server");
|
||||
const { userInfo } = await getUserSession(request);
|
||||
const { userInfo, frontendJWT } = await getUserSession(request);
|
||||
|
||||
if (!userInfo?.user_id) {
|
||||
return Response.json({ result: false, message: "用户身份验证失败" }, { status: 401 });
|
||||
@@ -138,7 +138,7 @@ export const action = async ({ request }: ActionFunctionArgs) => {
|
||||
|
||||
if (action === "delete") {
|
||||
const id = formData.get("id") as string;
|
||||
const response = await deleteDocument(id, userId);
|
||||
const response = await deleteDocument(id, userId, frontendJWT);
|
||||
|
||||
if (response.error) {
|
||||
return Response.json({ result: false, message: response.error }, { status: response.status || 500 });
|
||||
@@ -150,7 +150,7 @@ export const action = async ({ request }: ActionFunctionArgs) => {
|
||||
const ids = formData.getAll("ids") as string[];
|
||||
|
||||
// 批量删除处理
|
||||
const results = await Promise.all(ids.map(id => deleteDocument(id, userId)));
|
||||
const results = await Promise.all(ids.map(id => deleteDocument(id, userId, frontendJWT)));
|
||||
const failures = results.filter(r => r.error);
|
||||
|
||||
if (failures.length > 0) {
|
||||
@@ -257,7 +257,8 @@ export default function DocumentsIndex() {
|
||||
reviewType: storedReviewType || undefined,
|
||||
userId: userId, // 添加用户ID筛选
|
||||
page: currentPage,
|
||||
pageSize
|
||||
pageSize,
|
||||
token: loaderData.frontendJWT || undefined // 传递 JWT token
|
||||
};
|
||||
|
||||
// 获取文档列表
|
||||
@@ -270,7 +271,7 @@ export default function DocumentsIndex() {
|
||||
const filteredTypesResponse = await getDocumentTypes({
|
||||
pageSize: 500,
|
||||
reviewType: storedReviewType || undefined
|
||||
});
|
||||
}, loaderData.frontendJWT || undefined);
|
||||
const filteredDocumentTypes = filteredTypesResponse.data?.types || [];
|
||||
const filteredOptions = filteredDocumentTypes.map(type => ({
|
||||
value: type.id,
|
||||
@@ -492,20 +493,21 @@ export default function DocumentsIndex() {
|
||||
// 下载文档
|
||||
const handleDownload = async (path: string) => {
|
||||
try {
|
||||
const downloadUrl = `${DOCUMENT_URL}${path}`;
|
||||
|
||||
// 使用 PDF 代理路由获取文件,自动添加 JWT 认证
|
||||
const downloadUrl = `/api/pdf-proxy?path=${encodeURIComponent(path)}`;
|
||||
|
||||
// 使用fetch获取文件内容
|
||||
const response = await fetch(downloadUrl);
|
||||
if (!response.ok) {
|
||||
throw new Error(`下载失败: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
|
||||
|
||||
// 将响应转换为Blob
|
||||
const blob = await response.blob();
|
||||
|
||||
|
||||
// 创建Blob URL
|
||||
const blobUrl = URL.createObjectURL(blob);
|
||||
|
||||
|
||||
// 创建一个隐藏的a标签并点击它
|
||||
const a = document.createElement('a');
|
||||
a.style.display = 'none';
|
||||
@@ -515,7 +517,7 @@ export default function DocumentsIndex() {
|
||||
a.download = decodeURIComponent(fileName);
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
|
||||
|
||||
// 清理
|
||||
setTimeout(() => {
|
||||
document.body.removeChild(a);
|
||||
@@ -631,24 +633,25 @@ export default function DocumentsIndex() {
|
||||
console.warn(`文档 ${doc.name} 没有有效的路径`);
|
||||
return;
|
||||
}
|
||||
|
||||
const downloadUrl = `${DOCUMENT_URL}${doc.path}`;
|
||||
|
||||
|
||||
// 使用 PDF 代理路由获取文件,自动添加 JWT 认证
|
||||
const downloadUrl = `/api/pdf-proxy?path=${encodeURIComponent(doc.path)}`;
|
||||
|
||||
// 获取文件内容
|
||||
const response = await fetch(downloadUrl);
|
||||
if (!response.ok) {
|
||||
throw new Error(`下载失败: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
|
||||
|
||||
// 将响应转换为Blob
|
||||
const blob = await response.blob();
|
||||
|
||||
|
||||
// 从路径中获取文件名
|
||||
const fileName = doc.path.split('/').pop() || doc.name;
|
||||
|
||||
|
||||
// 添加到ZIP文件
|
||||
zip.file(decodeURIComponent(fileName), blob);
|
||||
|
||||
|
||||
return { success: true, name: fileName };
|
||||
} catch (error) {
|
||||
console.error(`下载文件 ${doc.name} 失败:`, error);
|
||||
@@ -714,7 +717,7 @@ export default function DocumentsIndex() {
|
||||
}
|
||||
|
||||
// console.log('开始审核',fileId,auditStatus)
|
||||
const response = await updateDocumentAuditStatus(fileId.toString(), 2, userId);
|
||||
const response = await updateDocumentAuditStatus(fileId.toString(), 2, userId, loaderData.frontendJWT as string | undefined);
|
||||
if (response.error) {
|
||||
console.error('更新文件审核状态失败:', response.error);
|
||||
toastService.error('更新文件审核状态失败:' + (response.error || '未知错误'));
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { LoaderFunctionArgs } from "@remix-run/node";
|
||||
import { postgrestGet } from "~/api/postgrest-client";
|
||||
import { getUserSession } from "~/api/login/auth.server";
|
||||
|
||||
/**
|
||||
* 文档下载路由 - 处理文档下载请求
|
||||
@@ -7,6 +8,7 @@ import { postgrestGet } from "~/api/postgrest-client";
|
||||
*/
|
||||
export async function loader({ request }: LoaderFunctionArgs) {
|
||||
try {
|
||||
const { frontendJWT } = await getUserSession(request);
|
||||
// 获取文件路径参数
|
||||
const url = new URL(request.url);
|
||||
const filePath = url.searchParams.get("path");
|
||||
@@ -23,7 +25,8 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
filter: {
|
||||
'object_path': `eq.${filePath}`,
|
||||
'expires_in': 'eq.300' // 5分钟有效期
|
||||
}
|
||||
},
|
||||
token: frontendJWT
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@@ -82,7 +82,7 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
try {
|
||||
// 获取用户会话信息
|
||||
const { getUserSession } = await import("~/api/login/auth.server");
|
||||
const { userInfo } = await getUserSession(request);
|
||||
const { userInfo, frontendJWT } = await getUserSession(request);
|
||||
|
||||
if (!userInfo?.user_id) {
|
||||
throw new Response("用户身份验证失败", { status: 401 });
|
||||
@@ -100,8 +100,8 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
|
||||
// 并行获取文档详情和文档类型列表
|
||||
const [documentResponse, documentTypesResponse] = await Promise.all([
|
||||
getDocument(id, userId),
|
||||
getDocumentTypes({ pageSize: 500 })
|
||||
getDocument(id, userId, frontendJWT),
|
||||
getDocumentTypes({ pageSize: 500 }, frontendJWT)
|
||||
]);
|
||||
|
||||
if (documentResponse.error) {
|
||||
@@ -114,7 +114,8 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
|
||||
return Response.json({
|
||||
document: documentResponse.data,
|
||||
documentTypes: documentTypesResponse.data?.types || []
|
||||
documentTypes: documentTypesResponse.data?.types || [],
|
||||
frontendJWT: frontendJWT
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("加载文档数据失败:", error);
|
||||
@@ -126,7 +127,7 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
export async function action({ request }: ActionFunctionArgs) {
|
||||
// 获取用户会话信息
|
||||
const { getUserSession } = await import("~/api/login/auth.server");
|
||||
const { userInfo } = await getUserSession(request);
|
||||
const { userInfo, frontendJWT } = await getUserSession(request);
|
||||
|
||||
if (!userInfo?.user_id) {
|
||||
return Response.json({ error: "用户身份验证失败" }, { status: 401 });
|
||||
@@ -173,7 +174,7 @@ export async function action({ request }: ActionFunctionArgs) {
|
||||
auditStatus,
|
||||
isTest,
|
||||
remark
|
||||
}, userId);
|
||||
}, userId, frontendJWT);
|
||||
|
||||
if (updateResponse.error) {
|
||||
console.error('更新文档失败:', updateResponse.error);
|
||||
@@ -323,7 +324,7 @@ export default function DocumentEdit() {
|
||||
return (
|
||||
<div className="preview-content relative overflow-y-auto max-h-[1000px]">
|
||||
<Document
|
||||
file={DOCUMENT_URL + documentData.path}
|
||||
file={`/api/pdf-proxy?path=${encodeURIComponent(documentData.path)}`}
|
||||
onLoadSuccess={onDocumentLoadSuccess}
|
||||
onLoadError={(error) => {
|
||||
console.error("PDF加载错误:", error);
|
||||
@@ -416,20 +417,21 @@ export default function DocumentEdit() {
|
||||
// 下载文档
|
||||
const downloadDocument = async () => {
|
||||
try {
|
||||
const downloadUrl = `${DOCUMENT_URL}${documentData.path}`;
|
||||
|
||||
// 使用 PDF 代理路由获取文件,自动添加 JWT 认证
|
||||
const downloadUrl = `/api/pdf-proxy?path=${encodeURIComponent(documentData.path)}`;
|
||||
|
||||
// 使用fetch获取文件内容
|
||||
const response = await fetch(downloadUrl);
|
||||
if (!response.ok) {
|
||||
throw new Error(`下载失败: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
|
||||
|
||||
// 将响应转换为Blob
|
||||
const blob = await response.blob();
|
||||
|
||||
|
||||
// 创建Blob URL
|
||||
const blobUrl = URL.createObjectURL(blob);
|
||||
|
||||
|
||||
// 创建一个隐藏的a标签并点击它
|
||||
const a = document.createElement('a');
|
||||
a.style.display = 'none';
|
||||
@@ -439,23 +441,24 @@ export default function DocumentEdit() {
|
||||
a.download = decodeURIComponent(fileName);
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
|
||||
|
||||
// 清理
|
||||
setTimeout(() => {
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(blobUrl);
|
||||
}, 100);
|
||||
|
||||
|
||||
toastService.success('文件下载成功');
|
||||
} catch (error) {
|
||||
console.error('下载文件失败:', error);
|
||||
toastService.error(`下载文件失败: ${error instanceof Error ? error.message : '未知错误'}`);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// 在新窗口打开文档预览
|
||||
const openPreview = () => {
|
||||
const previewUrl = `${DOCUMENT_URL}${documentData.path}`;
|
||||
// 使用 PDF 代理路由,自动添加 JWT 认证
|
||||
const previewUrl = `/api/pdf-proxy?path=${encodeURIComponent(documentData.path)}`;
|
||||
window.open(previewUrl, '_blank');
|
||||
};
|
||||
|
||||
|
||||
@@ -245,8 +245,8 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
// 我们不能在服务器端访问 sessionStorage,所以在客户端组件中处理 reviewType 过滤
|
||||
// 并行加载文档和文档类型
|
||||
const [documentsResponse, typesResponse] = await Promise.all([
|
||||
getTodayDocuments(userInfo),
|
||||
getDocumentTypes()
|
||||
getTodayDocuments(userInfo, undefined, frontendJWT),
|
||||
getDocumentTypes(undefined, frontendJWT)
|
||||
]);
|
||||
|
||||
// console.log('loader: 文档加载结果:', documentsResponse);
|
||||
@@ -410,7 +410,7 @@ export default function FilesUpload() {
|
||||
|
||||
try {
|
||||
// 使用 reviewType 获取过滤后的文档列表
|
||||
const response = await getTodayDocuments(loaderData.userInfo || undefined, reviewType);
|
||||
const response = await getTodayDocuments(loaderData.userInfo || undefined, reviewType, loaderData.frontendJWT || undefined);
|
||||
|
||||
if (response.error) {
|
||||
console.error('过滤文档列表失败:', response.error);
|
||||
@@ -575,16 +575,16 @@ export default function FilesUpload() {
|
||||
}
|
||||
});
|
||||
|
||||
console.log('合同主文件ID:', mainDocumentIds);
|
||||
console.log('合同附件ID:', attachmentIds);
|
||||
// console.log('合同主文件ID:', mainDocumentIds);
|
||||
// console.log('合同附件ID:', attachmentIds);
|
||||
|
||||
// 分别查询状态
|
||||
statusResponse = await getDocumentsStatus(mainDocumentIds, attachmentIds);
|
||||
statusResponse = await getDocumentsStatus(mainDocumentIds, attachmentIds, loaderData.frontendJWT || undefined);
|
||||
} else {
|
||||
// 非合同类型,使用原有逻辑
|
||||
const incompleteIds = incompleteFiles.map(file => file.id);
|
||||
// console.log('未完成的文档ID:', incompleteIds);
|
||||
statusResponse = await getDocumentsStatus(incompleteIds);
|
||||
statusResponse = await getDocumentsStatus(incompleteIds, undefined, loaderData.frontendJWT || undefined);
|
||||
}
|
||||
|
||||
// console.log('状态检查响应:', statusResponse);
|
||||
@@ -1666,7 +1666,7 @@ export default function FilesUpload() {
|
||||
|
||||
// 获取文件状态
|
||||
// console.log('【调试-checkProcessingStatus】发送请求获取文件状态');
|
||||
const response = await getDocumentsStatus(fileIds);
|
||||
const response = await getDocumentsStatus(fileIds, undefined, loaderData.frontendJWT || undefined);
|
||||
|
||||
if (response.error) {
|
||||
console.error('【调试-checkProcessingStatus】获取文件状态出错:', response.error);
|
||||
|
||||
+7
-6
@@ -92,7 +92,7 @@ export async function action({ request }: ActionFunctionArgs) {
|
||||
|
||||
export default function Home() {
|
||||
const navigate = useNavigate();
|
||||
const { homeData: initialHomeData, recentFiles: initialRecentFiles, userRole: serverUserRole, userInfo } = useLoaderData<typeof loader>();
|
||||
const { homeData: initialHomeData, recentFiles: initialRecentFiles, userRole: serverUserRole, userInfo, frontendJWT } = useLoaderData<typeof loader>();
|
||||
const [recentFiles, setRecentFiles] = useState<DocumentUI[]>(initialRecentFiles || []);
|
||||
const [homeData, setHomeData] = useState(initialHomeData);
|
||||
const [currentDateTime, setCurrentDateTime] = useState({
|
||||
@@ -160,9 +160,9 @@ export default function Home() {
|
||||
setIsLoading(true);
|
||||
// 从 sessionStorage 获取 reviewType
|
||||
const reviewType = sessionStorage.getItem('reviewType');
|
||||
|
||||
|
||||
// 加载主页数据
|
||||
const newHomeData = await getHomeData(reviewType || undefined,userInfo.user_id);
|
||||
const newHomeData = await getHomeData(reviewType || undefined,userInfo.user_id, frontendJWT);
|
||||
setHomeData(newHomeData);
|
||||
|
||||
// 加载文档数据
|
||||
@@ -185,7 +185,8 @@ export default function Home() {
|
||||
const documentSearchParams: DocumentSearchParams = {
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
userId: userInfo.user_id
|
||||
userId: userInfo.user_id,
|
||||
token: frontendJWT || undefined
|
||||
};
|
||||
|
||||
// 根据 reviewType 添加过滤条件
|
||||
@@ -244,9 +245,9 @@ export default function Home() {
|
||||
// 如果 reviewType 发生变化
|
||||
if (currentReviewType !== previousReviewType) {
|
||||
setIsLoading(true);
|
||||
|
||||
|
||||
// 更新主页数据
|
||||
const newHomeData = await getHomeData(currentReviewType || undefined,userInfo.user_id);
|
||||
const newHomeData = await getHomeData(currentReviewType || undefined,userInfo.user_id, frontendJWT);
|
||||
setHomeData(newHomeData);
|
||||
|
||||
// 更新文档数据
|
||||
|
||||
+98
-43
@@ -1,5 +1,5 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useSearchParams, Form } from "@remix-run/react";
|
||||
import { useActionData, useLoaderData, Form } from "@remix-run/react";
|
||||
import { type MetaFunction, type LoaderFunctionArgs, type ActionFunctionArgs, redirect } from "@remix-run/node";
|
||||
import { OAuthClient } from "~/api/login/oauth-client";
|
||||
import { OAUTH_CONFIG } from "~/config/api-config";
|
||||
@@ -32,11 +32,30 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
const redirectTo = url.searchParams.get("redirect") || "/";
|
||||
|
||||
const session = await getSession(request);
|
||||
|
||||
// 读取 flash 消息(来自 callback 的错误)
|
||||
const loginError = session.get("loginError");
|
||||
|
||||
session.set("redirectTo", redirectTo);
|
||||
|
||||
// 提交 session 以清除 flash 消息
|
||||
if (loginError) {
|
||||
const { sessionStorage } = await import("~/api/login/auth.server");
|
||||
return Response.json({
|
||||
isAuthenticated: false,
|
||||
redirectTo,
|
||||
flashError: loginError
|
||||
}, {
|
||||
headers: {
|
||||
"Set-Cookie": await sessionStorage.commitSession(session)
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return Response.json({
|
||||
isAuthenticated: false,
|
||||
redirectTo
|
||||
redirectTo,
|
||||
flashError: null
|
||||
});
|
||||
}
|
||||
|
||||
@@ -60,10 +79,17 @@ export async function action({ request }: ActionFunctionArgs) {
|
||||
// 登录成功,直接返回重定向响应
|
||||
return response;
|
||||
} else {
|
||||
// 登录失败,解析错误信息并重定向到登录页面
|
||||
// 登录失败,返回错误信息(不再使用URL参数)
|
||||
const errorData = await response.json();
|
||||
const errorMsg = errorData.error || "登录失败";
|
||||
return redirect(`/login?error=${encodeURIComponent(errorMsg)}`);
|
||||
return Response.json({
|
||||
success: false,
|
||||
error: errorData.error || "登录失败",
|
||||
retryCount: errorData.retryCount || 0,
|
||||
isLocked: errorData.isLocked || false,
|
||||
remainingAttempts: errorData.remainingAttempts || 5
|
||||
}, {
|
||||
status: response.status
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,40 +97,19 @@ export async function action({ request }: ActionFunctionArgs) {
|
||||
}
|
||||
|
||||
export default function Login() {
|
||||
const [searchParams] = useSearchParams();
|
||||
const error = searchParams.get("error");
|
||||
const actionData = useActionData<typeof action>();
|
||||
const loaderData = useLoaderData<typeof loader>();
|
||||
const [isFlipped, setIsFlipped] = useState(false);
|
||||
const [username, setUsername] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
|
||||
// 获取错误消息的友好描述
|
||||
const getErrorMessage = (error: string | null) => {
|
||||
if (!error) return null;
|
||||
|
||||
switch (error) {
|
||||
case "missing_code":
|
||||
return "登录过程中缺少授权码,请重新登录";
|
||||
case "invalid_state":
|
||||
return "登录状态验证失败,请重新登录";
|
||||
case "token_error":
|
||||
return "获取访问令牌失败,请重新登录";
|
||||
case "userinfo_error":
|
||||
return "获取用户信息失败,请重新登录";
|
||||
case "callback_error":
|
||||
return "登录回调处理失败,请重新登录";
|
||||
case "用户名和密码不能为空":
|
||||
case "用户名和密码不能为空,请重新输入":
|
||||
return "用户名和密码不能为空,请重新输入";
|
||||
case "登录失败,请检查用户名和密码":
|
||||
case "用户名或密码错误,请重新输入":
|
||||
return "用户名或密码错误,请重新输入";
|
||||
case "登录请求失败,请稍后重试":
|
||||
case "网络连接失败,请稍后重试":
|
||||
return "网络连接失败,请稍后重试";
|
||||
default:
|
||||
return decodeURIComponent(error);
|
||||
}
|
||||
};
|
||||
// 从 actionData 或 loaderData 中获取错误信息
|
||||
// actionData 的错误优先(来自密码登录)
|
||||
// loaderData.flashError 次之(来自 OAuth 回调)
|
||||
const error = actionData?.error || loaderData?.flashError;
|
||||
const isLocked = actionData?.isLocked || false;
|
||||
const retryCount = actionData?.retryCount || 0;
|
||||
const remainingAttempts = actionData?.remainingAttempts || 5;
|
||||
|
||||
// 处理OAuth2.0登录
|
||||
const handleOAuthLogin = () => {
|
||||
@@ -143,6 +148,13 @@ export default function Login() {
|
||||
|
||||
// 处理账号密码登录表单提交
|
||||
const handlePasswordLoginSubmit = (e: React.FormEvent) => {
|
||||
// 检查账户是否被锁定
|
||||
if (isLocked) {
|
||||
e.preventDefault();
|
||||
toastService.error("账户已被锁定,请联系管理员");
|
||||
return;
|
||||
}
|
||||
|
||||
// 客户端验证
|
||||
if (!username.trim()) {
|
||||
e.preventDefault();
|
||||
@@ -180,9 +192,22 @@ export default function Login() {
|
||||
<h2 className="login-subtitle">统一身份认证登录</h2>
|
||||
|
||||
{error && (
|
||||
<div className="error-message-container">
|
||||
<div className="error-icon"><i className="ri-error-warning-line"></i></div>
|
||||
<div className="error-text">{getErrorMessage(error)}</div>
|
||||
<div className={`error-message-container ${isLocked ? 'locked' : ''}`}>
|
||||
<div className="error-icon">
|
||||
{isLocked ? (
|
||||
<i className="ri-lock-line"></i>
|
||||
) : (
|
||||
<i className="ri-error-warning-line"></i>
|
||||
)}
|
||||
</div>
|
||||
<div className="error-text">
|
||||
{error}
|
||||
{!isLocked && retryCount > 0 && (
|
||||
<div className="retry-info" style={{ fontSize: '0.9em', marginTop: '4px', opacity: 0.9 }}>
|
||||
剩余尝试次数:{remainingAttempts} 次
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -235,9 +260,22 @@ export default function Login() {
|
||||
<h2 className="login-subtitle">管理员登录</h2>
|
||||
|
||||
{error && (
|
||||
<div className="error-message-container">
|
||||
<div className="error-icon"><i className="ri-error-warning-line"></i></div>
|
||||
<div className="error-text">{getErrorMessage(error)}</div>
|
||||
<div className={`error-message-container ${isLocked ? 'locked' : ''}`}>
|
||||
<div className="error-icon">
|
||||
{isLocked ? (
|
||||
<i className="ri-lock-line"></i>
|
||||
) : (
|
||||
<i className="ri-error-warning-line"></i>
|
||||
)}
|
||||
</div>
|
||||
<div className="error-text">
|
||||
{error}
|
||||
{!isLocked && retryCount > 0 && (
|
||||
<div className="retry-info" style={{ fontSize: '0.9em', marginTop: '4px', opacity: 0.9 }}>
|
||||
剩余尝试次数:{remainingAttempts} 次
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -275,10 +313,27 @@ export default function Login() {
|
||||
<button
|
||||
type="submit"
|
||||
className="admin-login-button"
|
||||
disabled={isLocked}
|
||||
style={isLocked ? { opacity: 0.5, cursor: 'not-allowed' } : {}}
|
||||
>
|
||||
<i className="ri-login-box-line"></i>
|
||||
登录
|
||||
<i className={isLocked ? "ri-lock-line" : "ri-login-box-line"}></i>
|
||||
{isLocked ? "账户已锁定" : "登录"}
|
||||
</button>
|
||||
|
||||
{isLocked && (
|
||||
<div style={{
|
||||
marginTop: '12px',
|
||||
padding: '8px',
|
||||
background: '#fff3cd',
|
||||
border: '1px solid #ffc107',
|
||||
borderRadius: '4px',
|
||||
fontSize: '0.9em',
|
||||
textAlign: 'center',
|
||||
color: '#856404'
|
||||
}}>
|
||||
<i className="ri-information-line"></i> 账户已被锁定,请联系管理员解锁
|
||||
</div>
|
||||
)}
|
||||
</Form>
|
||||
|
||||
<div className="back-to-oauth">
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { MetaFunction, json, type LoaderFunctionArgs } from "@remix-run/node";
|
||||
import { useSearchParams, useNavigate, useLoaderData } from "@remix-run/react";
|
||||
import { useState } from "react";
|
||||
import { MetaFunction, type LoaderFunctionArgs, type ActionFunctionArgs } from "@remix-run/node";
|
||||
import { useSearchParams, useNavigate, useLoaderData, useFetcher } from "@remix-run/react";
|
||||
import { useState, useEffect } from "react";
|
||||
import indexStyles from "~/styles/pages/prompts_index.css?url";
|
||||
import { Card } from "~/components/ui/Card";
|
||||
import { Button } from "~/components/ui/Button";
|
||||
@@ -9,19 +9,6 @@ import { Table } from "~/components/ui/Table";
|
||||
import { Pagination } from "~/components/ui/Pagination";
|
||||
import { getPromptTemplates, deletePromptTemplate, type PromptTemplateUI } from "~/api/prompts/prompts";
|
||||
|
||||
// 定义提示词模板类型
|
||||
export interface PromptTemplate {
|
||||
id: string;
|
||||
template_name: string;
|
||||
template_type: 'Common' | 'Extraction' | 'Evaluation' | 'Summary';
|
||||
description: string;
|
||||
version: string;
|
||||
status: 'active' | 'inactive' | 'system';
|
||||
created_by: string;
|
||||
template_content: string;
|
||||
variables: string; // JSON字符串
|
||||
}
|
||||
|
||||
// 样式链接
|
||||
export function links() {
|
||||
return [{ rel: "stylesheet", href: indexStyles }];
|
||||
@@ -44,18 +31,28 @@ interface LoaderData {
|
||||
error?: string;
|
||||
}
|
||||
|
||||
// 定义 Action 返回数据类型
|
||||
interface ActionData {
|
||||
success: boolean;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
// 数据加载器
|
||||
export async function loader({ request }: LoaderFunctionArgs) {
|
||||
try {
|
||||
// 获取用户会话信息
|
||||
const { getUserSession } = await import("~/api/login/auth.server");
|
||||
const { frontendJWT } = await getUserSession(request);
|
||||
|
||||
const url = new URL(request.url);
|
||||
const name = url.searchParams.get('name') || undefined;
|
||||
const type = url.searchParams.get('type') || undefined;
|
||||
const status = url.searchParams.get('status') || undefined;
|
||||
const page = parseInt(url.searchParams.get('page') || '1', 10);
|
||||
const pageSize = parseInt(url.searchParams.get('pageSize') || '10', 10);
|
||||
|
||||
|
||||
// console.log('加载提示词模板参数:', { name, type, status, page, pageSize });
|
||||
|
||||
|
||||
// 从 API 获取数据
|
||||
const result = await getPromptTemplates({
|
||||
name,
|
||||
@@ -63,7 +60,7 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
status,
|
||||
page,
|
||||
pageSize
|
||||
});
|
||||
}, frontendJWT);
|
||||
|
||||
if (result.error) {
|
||||
console.error('获取提示词模板失败:', result.error);
|
||||
@@ -102,12 +99,43 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
}
|
||||
}
|
||||
|
||||
// Action函数 - 处理删除请求
|
||||
export async function action({ request }: ActionFunctionArgs) {
|
||||
const formData = await request.formData();
|
||||
const id = formData.get("id") as string;
|
||||
const intent = formData.get("intent") as string;
|
||||
|
||||
// 获取用户会话信息
|
||||
const { getUserSession } = await import("~/api/login/auth.server");
|
||||
const { frontendJWT } = await getUserSession(request);
|
||||
|
||||
if (intent === "delete" && id) {
|
||||
try {
|
||||
const result = await deletePromptTemplate(id, frontendJWT);
|
||||
|
||||
if (result.error) {
|
||||
return Response.json({ success: false, error: result.error }, { status: result.status || 500 });
|
||||
}
|
||||
|
||||
return Response.json({ success: true });
|
||||
} catch (error) {
|
||||
return Response.json(
|
||||
{ success: false, error: error instanceof Error ? error.message : "删除提示词模板失败" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return Response.json({ success: false, error: "无效的操作" }, { status: 400 });
|
||||
}
|
||||
|
||||
// 页面组件
|
||||
export default function PromptsIndex() {
|
||||
const navigate = useNavigate();
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const { templates, total, currentPage, pageSize, error } = useLoaderData<typeof loader>();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const fetcher = useFetcher<ActionData>();
|
||||
|
||||
// 处理搜索名称
|
||||
const handleNameSearch = (value: string) => {
|
||||
@@ -176,26 +204,28 @@ export default function PromptsIndex() {
|
||||
};
|
||||
|
||||
// 删除模板
|
||||
const handleDeleteTemplate = async (id: string) => {
|
||||
const handleDeleteTemplate = (id: string) => {
|
||||
if (confirm('确定要删除该模板吗?删除后无法恢复。')) {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const result = await deletePromptTemplate(id);
|
||||
if (result.error) {
|
||||
alert(`删除失败: ${result.error}`);
|
||||
} else {
|
||||
alert('删除成功!');
|
||||
// 刷新页面
|
||||
window.location.reload();
|
||||
}
|
||||
} catch (error) {
|
||||
alert(`删除失败: ${error instanceof Error ? error.message : '未知错误'}`);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
const formData = new FormData();
|
||||
formData.append('id', id);
|
||||
formData.append('intent', 'delete');
|
||||
|
||||
fetcher.submit(formData, { method: 'post' });
|
||||
}
|
||||
};
|
||||
|
||||
// 监听 fetcher 状态变化
|
||||
useEffect(() => {
|
||||
if (fetcher.state === 'idle' && fetcher.data) {
|
||||
if (fetcher.data.success) {
|
||||
alert('删除成功!');
|
||||
window.location.reload();
|
||||
} else if (fetcher.data.error) {
|
||||
alert(`删除失败: ${fetcher.data.error}`);
|
||||
}
|
||||
}
|
||||
}, [fetcher.state, fetcher.data]);
|
||||
|
||||
// 处理分页
|
||||
const handlePageChange = (page: number) => {
|
||||
const newParams = new URLSearchParams(searchParams);
|
||||
|
||||
@@ -64,28 +64,32 @@ interface ActionData {
|
||||
// 加载函数
|
||||
export async function loader({ request }: LoaderFunctionArgs) {
|
||||
try {
|
||||
// 获取用户会话信息
|
||||
const { getUserSession } = await import("~/api/login/auth.server");
|
||||
const { frontendJWT } = await getUserSession(request);
|
||||
|
||||
const url = new URL(request.url);
|
||||
const id = url.searchParams.get("id");
|
||||
const mode = url.searchParams.get("mode") || "create";
|
||||
|
||||
|
||||
// 模板数据,如果是新建则为空
|
||||
let template = null;
|
||||
|
||||
|
||||
if (id) {
|
||||
// 从API获取数据
|
||||
const result = await getPromptTemplate(id);
|
||||
|
||||
const result = await getPromptTemplate(id, frontendJWT);
|
||||
|
||||
if (result.error) {
|
||||
console.error('获取提示词模板失败:', result.error);
|
||||
throw new Error(result.error);
|
||||
}
|
||||
|
||||
|
||||
template = result.data || null;
|
||||
if (!template) {
|
||||
throw new Error(`未找到ID为${id}的模板`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return Response.json({
|
||||
template,
|
||||
mode
|
||||
@@ -104,6 +108,9 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
|
||||
// Action函数 - 处理表单提交
|
||||
export async function action({ request }: ActionFunctionArgs) {
|
||||
const { getUserSession } = await import("~/api/login/auth.server");
|
||||
const { frontendJWT } = await getUserSession(request);
|
||||
|
||||
const formData = await request.formData();
|
||||
const id = formData.get("id") as string;
|
||||
const template_name = formData.get("template_name") as string;
|
||||
@@ -158,10 +165,10 @@ export async function action({ request }: ActionFunctionArgs) {
|
||||
let result;
|
||||
if (id) {
|
||||
// 更新模板
|
||||
result = await updatePromptTemplate(id, apiTemplate);
|
||||
result = await updatePromptTemplate(id, apiTemplate, frontendJWT);
|
||||
} else {
|
||||
// 创建模板
|
||||
result = await createPromptTemplate(apiTemplate);
|
||||
result = await createPromptTemplate(apiTemplate, frontendJWT);
|
||||
}
|
||||
|
||||
if (result.error) {
|
||||
|
||||
@@ -238,13 +238,13 @@ export async function action({ request }: ActionFunctionArgs) {
|
||||
|
||||
try {
|
||||
const response = await updateReviewResult(reviewPointResultId, editAuditStatusId, result, message, request);
|
||||
|
||||
|
||||
if (response.error) {
|
||||
console.error('updateReviewResult返回错误:', response.error);
|
||||
return Response.json({ success: false, error: response.error }, { status: response.status || 500 });
|
||||
}
|
||||
|
||||
return Response.json({ success: true, data: response.data, intent: "confirmReviewResults" });
|
||||
|
||||
return Response.json({ success: true, data: response.data, intent: "updateReviewResult" });
|
||||
} catch (updateError) {
|
||||
console.error('调用updateReviewResult时发生异常:', updateError);
|
||||
return Response.json({
|
||||
|
||||
@@ -21,13 +21,17 @@ export const meta: MetaFunction = () => {
|
||||
];
|
||||
};
|
||||
|
||||
export async function loader() {
|
||||
export async function loader({ request }: { request: Request }) {
|
||||
try {
|
||||
const response = await getRuleGroups();
|
||||
// 获取用户会话信息
|
||||
const { getUserSession } = await import("~/api/login/auth.server");
|
||||
const { frontendJWT } = await getUserSession(request);
|
||||
|
||||
const response = await getRuleGroups(frontendJWT);
|
||||
if (response.error) {
|
||||
throw new Error(response.error);
|
||||
}
|
||||
return Response.json({ groups: response.data });
|
||||
return Response.json({ groups: response.data, frontendJWT });
|
||||
} catch (error) {
|
||||
console.error('加载评查点分组失败:', error);
|
||||
return Response.json({ groups: [] });
|
||||
@@ -35,7 +39,7 @@ export async function loader() {
|
||||
}
|
||||
|
||||
export default function RuleGroupsIndex() {
|
||||
const { groups: initialGroups } = useLoaderData<typeof loader>();
|
||||
const { groups: initialGroups, frontendJWT } = useLoaderData<typeof loader>();
|
||||
const rootData = useRouteLoaderData("root") as { userRole: string };
|
||||
const navigate = useNavigate();
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
@@ -65,7 +69,7 @@ export default function RuleGroupsIndex() {
|
||||
// 并行加载所有父分组的子分组
|
||||
const promises = initialGroups.map(async (group: RuleGroup) => {
|
||||
try {
|
||||
const response = await getChildGroups(group.id);
|
||||
const response = await getChildGroups(group.id, frontendJWT);
|
||||
if (response.error) {
|
||||
console.error(`加载分组 ${group.id} 的子分组失败:`, response.error);
|
||||
return { parentId: group.id, children: [] };
|
||||
@@ -139,7 +143,7 @@ export default function RuleGroupsIndex() {
|
||||
}
|
||||
|
||||
// 否则加载子分组
|
||||
const response = await getChildGroups(groupId);
|
||||
const response = await getChildGroups(groupId, frontendJWT);
|
||||
if (response.error) {
|
||||
throw new Error(response.error);
|
||||
}
|
||||
@@ -187,7 +191,7 @@ export default function RuleGroupsIndex() {
|
||||
const handleDeleteGroup = async (groupId: string) => {
|
||||
if (confirm("确定要删除该分组吗?此操作将同时删除该分组下的所有评查点,且不可恢复。")) {
|
||||
try {
|
||||
const result = await deleteRuleGroup(groupId);
|
||||
const result = await deleteRuleGroup(groupId, frontendJWT);
|
||||
if (result.success) {
|
||||
// 从本地状态中移除被删除的分组
|
||||
setGroups(prev => {
|
||||
|
||||
@@ -91,12 +91,16 @@ function mapApiToFrontend(apiGroup: ApiRuleGroup): RuleGroup {
|
||||
export async function loader({ request }: LoaderFunctionArgs) {
|
||||
// console.log("rule-groups.new loader被调用,URL:", request.url);
|
||||
try {
|
||||
// 获取用户会话信息
|
||||
const { getUserSession } = await import("~/api/login/auth.server");
|
||||
const { frontendJWT } = await getUserSession(request);
|
||||
|
||||
const url = new URL(request.url);
|
||||
const id = url.searchParams.get("id");
|
||||
// console.log("获取到的ID参数:", id);
|
||||
|
||||
// 获取一级分组列表 (用于选择父级分组)
|
||||
const parentGroupsResponse = await getRuleGroups();
|
||||
const parentGroupsResponse = await getRuleGroups(frontendJWT);
|
||||
if (parentGroupsResponse.error) {
|
||||
console.error("获取父分组列表失败:", parentGroupsResponse.error);
|
||||
throw new Error(parentGroupsResponse.error);
|
||||
@@ -112,7 +116,7 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
|
||||
// 如果有ID,获取分组详情
|
||||
if (id) {
|
||||
const groupResponse = await getRuleGroup(id);
|
||||
const groupResponse = await getRuleGroup(id, frontendJWT);
|
||||
if (groupResponse.error) {
|
||||
console.error("获取分组详情失败:", groupResponse.error);
|
||||
throw new Error(groupResponse.error);
|
||||
@@ -146,6 +150,10 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
export async function action({ request }: ActionFunctionArgs) {
|
||||
const formData = await request.formData();
|
||||
|
||||
// 获取用户会话信息
|
||||
const { getUserSession } = await import("~/api/login/auth.server");
|
||||
const { frontendJWT } = await getUserSession(request);
|
||||
|
||||
// 提取表单数据
|
||||
const id = formData.get("id") as string | null;
|
||||
const name = formData.get("name") as string;
|
||||
@@ -193,9 +201,9 @@ export async function action({ request }: ActionFunctionArgs) {
|
||||
// 根据是否有ID决定是创建还是更新
|
||||
let response;
|
||||
if (id) {
|
||||
response = await updateRuleGroup(id, saveData);
|
||||
response = await updateRuleGroup(id, saveData, frontendJWT);
|
||||
} else {
|
||||
response = await createRuleGroup(saveData);
|
||||
response = await createRuleGroup(saveData, frontendJWT);
|
||||
}
|
||||
|
||||
// 处理API响应
|
||||
|
||||
@@ -71,7 +71,7 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
|
||||
try {
|
||||
// 获取文档类型列表
|
||||
const typesResponse = await getDocumentTypes({pageSize:500});
|
||||
const typesResponse = await getDocumentTypes({pageSize:500}, frontendJWT);
|
||||
const documentTypes = typesResponse.data?.types || [];
|
||||
|
||||
// 返回初始空数据,客户端将根据 sessionStorage 中的 reviewType 加载实际数据
|
||||
@@ -175,7 +175,7 @@ export default function RulesFiles() {
|
||||
const userId = userInfo?.user_id?.toString();
|
||||
|
||||
// 获取文件列表
|
||||
const filesResponse = await getReviewFiles(searchParams, null, userId);
|
||||
const filesResponse = await getReviewFiles({...searchParams, token: frontendJWT}, null, userId);
|
||||
if (filesResponse.error) {
|
||||
throw new Error(filesResponse.error);
|
||||
}
|
||||
@@ -240,14 +240,17 @@ export default function RulesFiles() {
|
||||
|
||||
// 从loader data中获取用户ID
|
||||
const userId = userInfo?.user_id?.toString();
|
||||
|
||||
|
||||
// 添加 token 参数到 apiSearchParams
|
||||
apiSearchParams.token = frontendJWT;
|
||||
|
||||
// 获取文件列表
|
||||
getReviewFiles(apiSearchParams, null, userId)
|
||||
.then(filesResponse => {
|
||||
if (filesResponse.error) {
|
||||
throw new Error(filesResponse.error);
|
||||
}
|
||||
|
||||
|
||||
setFiles(filesResponse.data?.files || []);
|
||||
setTotalCount(filesResponse.data?.total || 0);
|
||||
})
|
||||
@@ -335,7 +338,7 @@ export default function RulesFiles() {
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await updateDocumentAuditStatus(fileId, 2, userId);
|
||||
const response = await updateDocumentAuditStatus(fileId, 2, userId, frontendJWT);
|
||||
if (response.error) {
|
||||
throw new Error(response.error);
|
||||
}
|
||||
|
||||
@@ -153,9 +153,10 @@ export default function RuleNew() {
|
||||
const [isEditMode, setIsEditMode] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [instanceKey, setInstanceKey] = useState<string>('new');
|
||||
// 从root路由获取用户角色,而不是从sessionStorage
|
||||
const rootData = useRouteLoaderData("root") as { userRole: UserRole };
|
||||
// 从root路由获取用户角色和JWT token
|
||||
const rootData = useRouteLoaderData("root") as { userRole: UserRole; frontendJWT?: string };
|
||||
const userRole = rootData?.userRole || 'common';
|
||||
const frontendJWT = rootData?.frontendJWT;
|
||||
|
||||
const [formData, setFormData] = useState<EvaluationPoint>({});
|
||||
const [evaluationPointGroups, setEvaluationPointGroups] = useState<EvaluationPointGroup[]>([]);
|
||||
@@ -284,7 +285,8 @@ export default function RuleNew() {
|
||||
const postgrestParams = {
|
||||
filter: {
|
||||
'id': `eq.${id}`
|
||||
}
|
||||
},
|
||||
token: frontendJWT
|
||||
};
|
||||
const response = await postgrestGet('evaluation_points', postgrestParams);
|
||||
|
||||
@@ -332,7 +334,7 @@ export default function RuleNew() {
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [navigate, extractFieldsFromFormData, resetFormData]);
|
||||
}, [navigate, extractFieldsFromFormData, resetFormData, frontendJWT]);
|
||||
|
||||
/**
|
||||
* 获取评查点组数据
|
||||
@@ -341,7 +343,7 @@ export default function RuleNew() {
|
||||
const fetchEvaluationPointGroups = useCallback(async () => {
|
||||
try {
|
||||
// console.log("获取评查点组数据");
|
||||
const response = await postgrestGet('evaluation_point_groups');
|
||||
const response = await postgrestGet('evaluation_point_groups', { token: frontendJWT });
|
||||
|
||||
if (response.data && Array.isArray(response.data) && response.data.length > 0) {
|
||||
setEvaluationPointGroups(response.data);
|
||||
@@ -351,7 +353,7 @@ export default function RuleNew() {
|
||||
// 显示错误提示但不影响应用继续使用
|
||||
toastService.error(`获取评查点组数据失败: ${error instanceof Error ? error.message : '未知错误'}\n将使用默认数据`);
|
||||
}
|
||||
}, []);
|
||||
}, [frontendJWT]);
|
||||
|
||||
const handleSave = async () => {
|
||||
// console.log("保存评查点", formData);
|
||||
@@ -582,9 +584,9 @@ export default function RuleNew() {
|
||||
|
||||
let response;
|
||||
if (isEditMode) {
|
||||
response = await postgrestPut('evaluation_points', finalData, {id: formData.id!});
|
||||
response = await postgrestPut('evaluation_points', finalData, {id: formData.id!}, frontendJWT);
|
||||
} else {
|
||||
response = await postgrestPost('evaluation_points', finalData);
|
||||
response = await postgrestPost('evaluation_points', finalData, frontendJWT);
|
||||
}
|
||||
|
||||
if (response.error) {
|
||||
|
||||
@@ -97,8 +97,12 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
};
|
||||
|
||||
try {
|
||||
// 获取用户会话信息
|
||||
const { getUserSession } = await import("~/api/login/auth.server");
|
||||
const { frontendJWT } = await getUserSession(request);
|
||||
|
||||
// 获取评查点类型列表,供前端筛选使用
|
||||
const typeResponse = await getRuleTypes();
|
||||
const typeResponse = await getRuleTypes(undefined, frontendJWT);
|
||||
|
||||
if (typeResponse.error) {
|
||||
console.error('获取评查点类型失败:', typeResponse.error);
|
||||
@@ -113,7 +117,8 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
currentPage: params.page,
|
||||
pageSize: params.pageSize,
|
||||
ruleTypes,
|
||||
initialLoad: true
|
||||
initialLoad: true,
|
||||
frontendJWT
|
||||
}, {
|
||||
headers: {
|
||||
"Cache-Control": "max-age=60, s-maxage=180"
|
||||
@@ -139,11 +144,15 @@ export async function action({ request }: LoaderFunctionArgs) {
|
||||
}
|
||||
|
||||
try {
|
||||
// 获取用户会话信息
|
||||
const { getUserSession } = await import("~/api/login/auth.server");
|
||||
const { frontendJWT } = await getUserSession(request);
|
||||
|
||||
if (_action === 'delete') {
|
||||
// 调用API删除评查点
|
||||
// console.log(`删除评查点 ${ruleId}`);
|
||||
|
||||
const deleteResponse = await deleteRule(ruleId as string);
|
||||
const deleteResponse = await deleteRule(ruleId as string, frontendJWT);
|
||||
|
||||
if (deleteResponse.error) {
|
||||
return Response.json({ result: false, message: deleteResponse.error }, { status: deleteResponse.status || 500 });
|
||||
@@ -257,7 +266,7 @@ export default function RulesIndex() {
|
||||
|
||||
// 获取评查点类型
|
||||
try {
|
||||
const typeResponse = await getRuleTypes(typeToUse);
|
||||
const typeResponse = await getRuleTypes(typeToUse, loaderData.frontendJWT);
|
||||
if (typeResponse.data) {
|
||||
setRuleTypes(typeResponse.data);
|
||||
}
|
||||
@@ -273,7 +282,8 @@ export default function RulesIndex() {
|
||||
keyword: searchParams.get('keyword') || undefined,
|
||||
page: currentPage,
|
||||
pageSize,
|
||||
reviewType: typeToUse
|
||||
reviewType: typeToUse,
|
||||
token: loaderData.frontendJWT
|
||||
};
|
||||
|
||||
// 调用 API 获取数据
|
||||
@@ -307,7 +317,7 @@ export default function RulesIndex() {
|
||||
const loadRuleGroups = async () => {
|
||||
setLoadingGroups(true);
|
||||
try {
|
||||
const response = await getRuleGroupsByType(ruleTypeParam);
|
||||
const response = await getRuleGroupsByType(ruleTypeParam, loaderData.frontendJWT);
|
||||
if (response.data) {
|
||||
setRuleGroups(response.data);
|
||||
} else if (response.error) {
|
||||
|
||||
Reference in New Issue
Block a user