1.添加移动端用户的检测工具类,移动端用户只能访问对话页面。
2.评查点列表添加文档属性类型字段。 3.优化dify的对话侧边栏的显示效果。 4.评查点规则添加使用文档属性类型的输入框。添加多实体开关的操作开关。
This commit is contained in:
@@ -311,7 +311,8 @@ export async function getRulesList(params: RulesQueryParams): Promise<{data: Rul
|
|||||||
isActive: point.is_enabled,
|
isActive: point.is_enabled,
|
||||||
createdAt: formatDate(point.created_at || ''),
|
createdAt: formatDate(point.created_at || ''),
|
||||||
updatedAt: formatDate(point.updated_at || ''),
|
updatedAt: formatDate(point.updated_at || ''),
|
||||||
area: point.area || ''
|
area: point.area || '',
|
||||||
|
documentAttributeType: point.document_attribute_type || ''
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
// console.log('✅ [getRulesList] 成功映射评查点列表数据', response.data.data[0]);
|
// console.log('✅ [getRulesList] 成功映射评查点列表数据', response.data.data[0]);
|
||||||
@@ -1045,6 +1046,7 @@ export interface EvaluationPointData {
|
|||||||
ruleType?: string; // 评查点类型(一级分组名称)
|
ruleType?: string; // 评查点类型(一级分组名称)
|
||||||
groupName?: string; // 所属规则组(二级分组名称)
|
groupName?: string; // 所属规则组(二级分组名称)
|
||||||
groupId?: string; // 规则组ID(二级分组ID的字符串形式)
|
groupId?: string; // 规则组ID(二级分组ID的字符串形式)
|
||||||
|
document_attribute_type?: string; // 文档属性类型
|
||||||
references_laws: {
|
references_laws: {
|
||||||
name: string;
|
name: string;
|
||||||
content: string;
|
content: string;
|
||||||
@@ -1218,3 +1220,60 @@ export async function getEvaluationPoint(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 适用属性类型选项
|
||||||
|
*/
|
||||||
|
export interface AttributeTypeOption {
|
||||||
|
code: string;
|
||||||
|
label: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取适用属性类型列表
|
||||||
|
* 从后端获取当前评查点表中所有已使用的 document_attribute_type 去重列表
|
||||||
|
* @param token JWT token (可选)
|
||||||
|
* @returns 适用属性类型列表
|
||||||
|
*/
|
||||||
|
export async function getAttributeTypes(
|
||||||
|
token?: string
|
||||||
|
): Promise<{data: AttributeTypeOption[]; error?: never} | {data?: never; error: string; status?: number}> {
|
||||||
|
try {
|
||||||
|
// 调用后端 FastAPI 接口: GET /api/v3/evaluation-points/attribute-types
|
||||||
|
const response = await apiRequest<{
|
||||||
|
types: AttributeTypeOption[];
|
||||||
|
}>(
|
||||||
|
'/api/v3/evaluation-points/attribute-types',
|
||||||
|
{
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
...(token ? { 'Authorization': `Bearer ${token}` } : {})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.error) {
|
||||||
|
return { error: response.error, status: response.status };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!response.data || !Array.isArray(response.data.types)) {
|
||||||
|
// 返回默认的属性类型列表
|
||||||
|
return {
|
||||||
|
data: [
|
||||||
|
{ code: 'ALL', label: '通用' }
|
||||||
|
]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('✅ getAttributeTypes 成功:', response.data.types);
|
||||||
|
return { data: response.data.types };
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ 获取适用属性类型列表出错:', error);
|
||||||
|
// 出错时返回默认列表,不阻塞用户操作
|
||||||
|
return {
|
||||||
|
data: [
|
||||||
|
{ code: 'ALL', label: '通用' }
|
||||||
|
]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -665,13 +665,11 @@ export default function Chat() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout style={{ height: '100%', display: 'flex', flexDirection: 'row', position: 'relative' }}>
|
<Layout style={{ height: '100%', display: 'flex', flexDirection: 'row', position: 'relative' }}>
|
||||||
{/* 移动端遮罩层 */}
|
{/* 移动端遮罩层 - 点击可收起侧边栏 */}
|
||||||
{!sidebarCollapsed && isMobile && (
|
<div
|
||||||
<div
|
className={`chat-sidebar-overlay ${!sidebarCollapsed && isMobile ? 'visible' : ''}`}
|
||||||
className="fixed inset-0 bg-black bg-opacity-50 z-[999]"
|
onClick={handleSidebarToggle}
|
||||||
onClick={handleSidebarToggle}
|
/>
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* ChatSidebar 隐藏时显示的展开按钮 */}
|
{/* ChatSidebar 隐藏时显示的展开按钮 */}
|
||||||
{sidebarCollapsed && (
|
{sidebarCollapsed && (
|
||||||
|
|||||||
@@ -82,10 +82,11 @@ export const SourcesPanel = React.memo(({ resources }: { resources: RetrieverRes
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
placement="topLeft"
|
placement="top"
|
||||||
autoAdjustOverflow={false}
|
autoAdjustOverflow={true}
|
||||||
color="rgba(0, 104, 74, 0.92)"
|
color="rgba(0, 104, 74, 0.92)"
|
||||||
classNames={{ root: 'source-tooltip-overlay' }}
|
classNames={{ root: 'source-tooltip-overlay' }}
|
||||||
|
overlayStyle={{ maxWidth: 'calc(100vw - 32px)' }}
|
||||||
>
|
>
|
||||||
<div className="source-item">
|
<div className="source-item">
|
||||||
<span className="source-item-number">{resource.position}</span>
|
<span className="source-item-number">{resource.position}</span>
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ interface LayoutProps {
|
|||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
userRole?: UserRole;
|
userRole?: UserRole;
|
||||||
frontendJWT?: string;
|
frontendJWT?: string;
|
||||||
|
isMobile?: boolean; // 是否为移动端设备(服务端通过 User-Agent 检测)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 添加一个接口表示路由handle可能包含的属性
|
// 添加一个接口表示路由handle可能包含的属性
|
||||||
@@ -23,7 +24,7 @@ interface Match {
|
|||||||
data: unknown;
|
data: unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Layout({ children, userRole = 'developer' as UserRole, frontendJWT = '' }: LayoutProps) {
|
export function Layout({ children, userRole = 'developer' as UserRole, frontendJWT = '', isMobile = false }: LayoutProps) {
|
||||||
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
|
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
|
||||||
const [effectiveUserRole, setEffectiveUserRole] = useState<UserRole>(userRole);
|
const [effectiveUserRole, setEffectiveUserRole] = useState<UserRole>(userRole);
|
||||||
const [effectiveFrontendJWT, setEffectiveFrontendJWT] = useState<string>(frontendJWT);
|
const [effectiveFrontendJWT, setEffectiveFrontendJWT] = useState<string>(frontendJWT);
|
||||||
@@ -32,10 +33,12 @@ export function Layout({ children, userRole = 'developer' as UserRole, frontendJ
|
|||||||
|
|
||||||
// 检查当前路径是否应该隐藏侧边栏
|
// 检查当前路径是否应该隐藏侧边栏
|
||||||
const noLayoutPaths = ['/login', '/'];
|
const noLayoutPaths = ['/login', '/'];
|
||||||
const shouldHideSidebar = noLayoutPaths.includes(location.pathname);
|
// 移动端设备强制隐藏侧边栏
|
||||||
|
const shouldHideSidebar = isMobile || noLayoutPaths.includes(location.pathname);
|
||||||
|
|
||||||
// 检查当前路由是否应该隐藏默认面包屑
|
// 检查当前路由是否应该隐藏默认面包屑
|
||||||
const shouldHideBreadcrumb = shouldHideSidebar || matches.some(match =>
|
// 移动端设备强制隐藏面包屑(避免显示首页链接)
|
||||||
|
const shouldHideBreadcrumb = isMobile || shouldHideSidebar || matches.some(match =>
|
||||||
match.handle && match.handle.hideBreadcrumb === true
|
match.handle && match.handle.hideBreadcrumb === true
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import type { EvaluationPoint } from '~/models/evaluation_points';
|
import type { EvaluationPoint } from '~/models/evaluation_points';
|
||||||
import type { EvaluationPointGroup } from '~/models/evaluation_point_groups';
|
import type { EvaluationPointGroup } from '~/models/evaluation_point_groups';
|
||||||
import { getRulesList, getRuleTypes, type RuleType } from '~/api/evaluation_points/rules';
|
import { getRulesList, getRuleTypes, getAttributeTypes, type RuleType, type AttributeTypeOption } from '~/api/evaluation_points/rules';
|
||||||
|
|
||||||
interface BasicInfoProps {
|
interface BasicInfoProps {
|
||||||
onChange?: (data: Record<string, unknown>) => void;
|
onChange?: (data: Record<string, unknown>) => void;
|
||||||
@@ -101,6 +101,11 @@ export function BasicInfo({
|
|||||||
const [filteredRuleTypes, setFilteredRuleTypes] = useState<RuleType[]>([]);
|
const [filteredRuleTypes, setFilteredRuleTypes] = useState<RuleType[]>([]);
|
||||||
const [ruleTypesLoading, setRuleTypesLoading] = useState(false);
|
const [ruleTypesLoading, setRuleTypesLoading] = useState(false);
|
||||||
|
|
||||||
|
// 适用文档属性类型相关状态
|
||||||
|
const [attributeTypeOptions, setAttributeTypeOptions] = useState<AttributeTypeOption[]>([]);
|
||||||
|
const [attributeTypesLoading, setAttributeTypesLoading] = useState(false);
|
||||||
|
const [isCustomAttributeType, setIsCustomAttributeType] = useState(false); // 是否为自定义输入模式
|
||||||
|
|
||||||
// 从 Session Storage 获取 documentTypeIds 并调用 API 获取评查点类型
|
// 从 Session Storage 获取 documentTypeIds 并调用 API 获取评查点类型
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchRuleTypes = async () => {
|
const fetchRuleTypes = async () => {
|
||||||
@@ -140,6 +145,43 @@ export function BasicInfo({
|
|||||||
fetchRuleTypes();
|
fetchRuleTypes();
|
||||||
}, [frontendJWT]);
|
}, [frontendJWT]);
|
||||||
|
|
||||||
|
// 获取适用文档属性类型选项
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchAttributeTypes = async () => {
|
||||||
|
try {
|
||||||
|
setAttributeTypesLoading(true);
|
||||||
|
const response = await getAttributeTypes(frontendJWT);
|
||||||
|
|
||||||
|
if (response.data) {
|
||||||
|
setAttributeTypeOptions(response.data);
|
||||||
|
} else {
|
||||||
|
// 使用默认选项
|
||||||
|
setAttributeTypeOptions([{ code: 'ALL', label: '通用' }]);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取适用属性类型失败:', error);
|
||||||
|
setAttributeTypeOptions([{ code: 'ALL', label: '通用' }]);
|
||||||
|
} finally {
|
||||||
|
setAttributeTypesLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchAttributeTypes();
|
||||||
|
}, [frontendJWT]);
|
||||||
|
|
||||||
|
// 检查初始数据中的 document_attribute_type 是否为自定义值
|
||||||
|
useEffect(() => {
|
||||||
|
if (formData.document_attribute_type) {
|
||||||
|
const isInOptions = attributeTypeOptions.some(
|
||||||
|
opt => opt.code === formData.document_attribute_type
|
||||||
|
);
|
||||||
|
// 如果当前值不在选项列表中,则切换到自定义模式
|
||||||
|
if (!isInOptions && attributeTypeOptions.length > 0) {
|
||||||
|
setIsCustomAttributeType(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [formData.document_attribute_type, attributeTypeOptions]);
|
||||||
|
|
||||||
// 根据选择的评查点类型筛选可用的规则组
|
// 根据选择的评查点类型筛选可用的规则组
|
||||||
const filteredRuleGroups = evaluationPointGroups.filter(group =>
|
const filteredRuleGroups = evaluationPointGroups.filter(group =>
|
||||||
formData.evaluation_point_groups_pid &&
|
formData.evaluation_point_groups_pid &&
|
||||||
@@ -268,6 +310,14 @@ export function BasicInfo({
|
|||||||
newData.evaluation_point_groups_id = null; // 清空规则组选择
|
newData.evaluation_point_groups_id = null; // 清空规则组选择
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case 'document-attribute-type':
|
||||||
|
// 处理适用文档属性类型选择(下拉框模式)
|
||||||
|
newData.document_attribute_type = value || 'ALL';
|
||||||
|
break;
|
||||||
|
case 'document-attribute-type-custom':
|
||||||
|
// 处理适用文档属性类型输入(自定义模式)
|
||||||
|
newData.document_attribute_type = value;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
setFormData(newData);
|
setFormData(newData);
|
||||||
@@ -320,18 +370,19 @@ export function BasicInfo({
|
|||||||
}
|
}
|
||||||
}, [formData.references_laws?.articles]);
|
}, [formData.references_laws?.articles]);
|
||||||
|
|
||||||
// 检查是否需要自动展开描述区域
|
// 检查是否需要自动展开描述区域(仅在初始数据加载时执行一次)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// 如果描述或法律依据相关字段有值,则自动展开
|
// 如果初始数据中描述或法律依据相关字段有值,则自动展开
|
||||||
if (
|
if (
|
||||||
formData.description ||
|
initialData?.description ||
|
||||||
formData.references_laws?.name ||
|
initialData?.references_laws?.name ||
|
||||||
(formData.references_laws?.articles && formData.references_laws.articles.length > 0) ||
|
(initialData?.references_laws?.articles && initialData.references_laws.articles.length > 0) ||
|
||||||
formData.references_laws?.content
|
initialData?.references_laws?.content
|
||||||
) {
|
) {
|
||||||
setIsDescExpanded(true);
|
setIsDescExpanded(true);
|
||||||
}
|
}
|
||||||
}, [formData]);
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, []);
|
||||||
|
|
||||||
// 注释掉自动选择规则组的逻辑,避免无限循环
|
// 注释掉自动选择规则组的逻辑,避免无限循环
|
||||||
// 原因:此 useEffect 依赖 onChange 和 filteredRuleGroups,每次渲染都可能触发
|
// 原因:此 useEffect 依赖 onChange 和 filteredRuleGroups,每次渲染都可能触发
|
||||||
@@ -472,11 +523,83 @@ export function BasicInfo({
|
|||||||
</select>
|
</select>
|
||||||
<div className="form-tip">创建后是否立即启用此评查点</div>
|
<div className="form-tip">创建后是否立即启用此评查点</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="form-label" htmlFor="document-attribute-type">
|
||||||
|
适用文档属性类型
|
||||||
|
</label>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{isCustomAttributeType ? (
|
||||||
|
// 自定义输入模式
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="document-attribute-type-custom"
|
||||||
|
className="form-input flex-1"
|
||||||
|
placeholder="请输入自定义属性类型"
|
||||||
|
value={formData.document_attribute_type || ''}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
// 下拉选择模式
|
||||||
|
<select
|
||||||
|
id="document-attribute-type"
|
||||||
|
className="form-select flex-1"
|
||||||
|
value={formData.document_attribute_type || 'ALL'}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
disabled={attributeTypesLoading}
|
||||||
|
>
|
||||||
|
{attributeTypesLoading ? (
|
||||||
|
<option value="">加载中...</option>
|
||||||
|
) : (
|
||||||
|
attributeTypeOptions.map(option => (
|
||||||
|
<option key={option.code} value={option.code}>
|
||||||
|
{option.label}
|
||||||
|
</option>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</select>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`px-3 py-2 text-sm rounded border transition-colors ${
|
||||||
|
isCustomAttributeType
|
||||||
|
? 'bg-[#00684a] text-white border-[#00684a] hover:bg-[#005a3f]'
|
||||||
|
: 'bg-white text-gray-600 border-gray-300 hover:border-[#00684a] hover:text-[#00684a]'
|
||||||
|
}`}
|
||||||
|
onClick={() => {
|
||||||
|
setIsCustomAttributeType(!isCustomAttributeType);
|
||||||
|
// 切换到自定义模式时,如果当前值是预设值,清空它让用户输入
|
||||||
|
// 切换回下拉模式时,如果当前值不在选项中,设置为默认值 ALL
|
||||||
|
if (!isCustomAttributeType) {
|
||||||
|
// 切换到自定义模式
|
||||||
|
} else {
|
||||||
|
// 切换回下拉模式
|
||||||
|
const currentValue = formData.document_attribute_type;
|
||||||
|
const isInOptions = attributeTypeOptions.some(opt => opt.code === currentValue);
|
||||||
|
if (!isInOptions) {
|
||||||
|
const newData = { ...formData, document_attribute_type: 'ALL' };
|
||||||
|
setFormData(newData);
|
||||||
|
if (onChange) {
|
||||||
|
onChange(newData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
title={isCustomAttributeType ? '切换到下拉选择' : '切换到自定义输入'}
|
||||||
|
>
|
||||||
|
<i className={`ri-${isCustomAttributeType ? 'list-check' : 'edit-line'}`}></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="form-tip">
|
||||||
|
{isCustomAttributeType
|
||||||
|
? '输入自定义的文档属性类型,点击右侧按钮可切换回下拉选择'
|
||||||
|
: '选择评查点适用的文档属性类型,点击右侧按钮可自定义输入'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-8">
|
<div className="mt-8">
|
||||||
<div
|
<div
|
||||||
className={`flex justify-between items-center cursor-pointer ${isDescExpanded ? 'expanded' : ''}`}
|
className={`flex items-center cursor-pointer ${isDescExpanded ? 'expanded' : ''}`}
|
||||||
onClick={handleToggleDescription}
|
onClick={handleToggleDescription}
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
if (e.key === 'Enter' || e.key === ' ') {
|
if (e.key === 'Enter' || e.key === ' ') {
|
||||||
@@ -487,7 +610,7 @@ export function BasicInfo({
|
|||||||
role="button"
|
role="button"
|
||||||
>
|
>
|
||||||
<label className="form-label mb-0" htmlFor="description-section">评查点描述与法律依据</label>
|
<label className="form-label mb-0" htmlFor="description-section">评查点描述与法律依据</label>
|
||||||
<i className={`ri-arrow-${isDescExpanded ? 'up' : 'down'}-s-line text-lg expand-icon`}></i>
|
<i className={`${isDescExpanded ? 'ri-arrow-drop-up-line' : 'ri-arrow-drop-down-line'} text-lg expand-icon ml-2`}></i>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={`mt-2 ${isDescExpanded ? '' : 'hidden'}`} id="description-section">
|
<div className={`mt-2 ${isDescExpanded ? '' : 'hidden'}`} id="description-section">
|
||||||
|
|||||||
@@ -48,10 +48,19 @@ export function ExtractionSettings({
|
|||||||
vlmFieldTypeOptions = EVALUATION_OPTIONS.vlmFieldTypeOptions,
|
vlmFieldTypeOptions = EVALUATION_OPTIONS.vlmFieldTypeOptions,
|
||||||
}: ExtractionSettingsProps) {
|
}: ExtractionSettingsProps) {
|
||||||
|
|
||||||
|
// 多实体抽取开关状态
|
||||||
|
const [multiEntityEnabled, setMultiEntityEnabled] = useState<boolean>(
|
||||||
|
initialData?.extraction_config?.multi_entity?.enabled ?? false
|
||||||
|
);
|
||||||
|
|
||||||
// 核心数据状态
|
// 核心数据状态
|
||||||
const [formData, setFormData] = useState<EvaluationPoint>({
|
const [formData, setFormData] = useState<EvaluationPoint>({
|
||||||
// 字段配置
|
// 字段配置
|
||||||
extraction_config: {
|
extraction_config: {
|
||||||
|
multi_entity: initialData?.extraction_config?.multi_entity ?? {
|
||||||
|
enabled: false,
|
||||||
|
expand_mode: 'awareness',
|
||||||
|
},
|
||||||
llm: initialData?.extraction_config?.llm ?? {
|
llm: initialData?.extraction_config?.llm ?? {
|
||||||
fields: [],
|
fields: [],
|
||||||
prompt_setting: {
|
prompt_setting: {
|
||||||
@@ -488,6 +497,10 @@ export function ExtractionSettings({
|
|||||||
const updatedFormData = {
|
const updatedFormData = {
|
||||||
...formData,
|
...formData,
|
||||||
extraction_config: {
|
extraction_config: {
|
||||||
|
multi_entity: {
|
||||||
|
enabled: multiEntityEnabled,
|
||||||
|
expand_mode: 'awareness' as const
|
||||||
|
},
|
||||||
llm: {
|
llm: {
|
||||||
fields: fields.llm,
|
fields: fields.llm,
|
||||||
prompt_setting: {
|
prompt_setting: {
|
||||||
@@ -562,12 +575,46 @@ export function ExtractionSettings({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 处理多实体抽取开关变化
|
||||||
|
const handleMultiEntityToggle = () => {
|
||||||
|
const newValue = !multiEntityEnabled;
|
||||||
|
setMultiEntityEnabled(newValue);
|
||||||
|
setHasPendingChanges(true);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="ant-card">
|
<div className="ant-card">
|
||||||
<div className="ant-card-header">
|
<div className="ant-card-header">
|
||||||
<h3>抽取设置</h3>
|
<h3>抽取设置</h3>
|
||||||
</div>
|
</div>
|
||||||
<div className="ant-card-body">
|
<div className="ant-card-body">
|
||||||
|
{/* 多实体抽取开关 */}
|
||||||
|
<div className="mb-6 p-3 bg-gray-50 rounded-md border border-gray-200 w-[40%]">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<i className="ri-group-line text-lg mr-2 text-gray-600"></i>
|
||||||
|
<div>
|
||||||
|
<span className="font-medium text-gray-800">多实体抽取</span>
|
||||||
|
<span className="text-xs text-gray-500 ml-3">启用后,系统将按实体展开字段进行抽取(AI感知模式)</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<label className="relative inline-flex items-center cursor-pointer ml-5">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
className="sr-only peer"
|
||||||
|
checked={multiEntityEnabled}
|
||||||
|
onChange={handleMultiEntityToggle}
|
||||||
|
/>
|
||||||
|
<div className="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-2 peer-focus:ring-green-700/20 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-green-700"></div>
|
||||||
|
<span className="ml-2 text-sm font-medium text-gray-700">
|
||||||
|
{multiEntityEnabled ? '已启用' : '已禁用'}
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
<div className="tab-nav mb-4" id="extraction-method-tabs">
|
<div className="tab-nav mb-4" id="extraction-method-tabs">
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { Button } from '~/components/ui/Button';
|
||||||
|
|
||||||
interface PageHeaderProps {
|
interface PageHeaderProps {
|
||||||
title: string;
|
title: string;
|
||||||
onSave?: () => void;
|
onSave?: () => void;
|
||||||
@@ -18,13 +20,14 @@ export function PageHeader({
|
|||||||
<h1 className="text-xl font-medium text-gray-800">{title}</h1>
|
<h1 className="text-xl font-medium text-gray-800">{title}</h1>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{showBackButton && (
|
{showBackButton && (
|
||||||
<button
|
<Button
|
||||||
type="button"
|
type="default"
|
||||||
className="ant-btn ant-btn-default"
|
className=" focus:!ring-gray-300"
|
||||||
onClick={onBack}
|
onClick={onBack}
|
||||||
|
icon="ri-arrow-left-line"
|
||||||
>
|
>
|
||||||
<i className="ri-arrow-left-line mr-1"></i> 返回
|
返回
|
||||||
</button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{showSaveButton && (
|
{showSaveButton && (
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ interface EvaluationPoint {
|
|||||||
};
|
};
|
||||||
evaluation_point_groups_pid?: number | null; // 评查点类型组ID
|
evaluation_point_groups_pid?: number | null; // 评查点类型组ID
|
||||||
evaluation_point_groups_id?: number | null; // 所属评查组ID
|
evaluation_point_groups_id?: number | null; // 所属评查组ID
|
||||||
|
document_attribute_type?: string; // 适用文档属性类型
|
||||||
extraction_config?: ExtactionConfigType; // 抽取配置
|
extraction_config?: ExtactionConfigType; // 抽取配置
|
||||||
evaluation_config?: EvaluationConfigType; // 评查配置
|
evaluation_config?: EvaluationConfigType; // 评查配置
|
||||||
pass_message?: string; // 通过消息
|
pass_message?: string; // 通过消息
|
||||||
@@ -41,10 +42,24 @@ type SuggestionMessageType = 'info' | 'warning' | 'error';
|
|||||||
*/
|
*/
|
||||||
type PostActionType = 'none' | 'manual' | 'replace';
|
type PostActionType = 'none' | 'manual' | 'replace';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 多实体抽取配置类型定义
|
||||||
|
*/
|
||||||
|
interface MultiEntityConfig {
|
||||||
|
enabled: boolean; // 是否启用多实体抽取
|
||||||
|
expand_mode: MultiEntityExpandMode; // 展开模式
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 多实体展开模式类型定义
|
||||||
|
*/
|
||||||
|
type MultiEntityExpandMode = 'awareness'; // AI感知模式
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 抽取配置类型定义
|
* 抽取配置类型定义
|
||||||
*/
|
*/
|
||||||
interface ExtactionConfigType {
|
interface ExtactionConfigType {
|
||||||
|
multi_entity?: MultiEntityConfig; // 多实体抽取配置(控制抽取阶段是否按实体展开字段)
|
||||||
llm: {
|
llm: {
|
||||||
fields: string[];
|
fields: string[];
|
||||||
prompt_setting: LLMPromptSetting;
|
prompt_setting: LLMPromptSetting;
|
||||||
@@ -312,6 +327,8 @@ export type {
|
|||||||
SuggestionMessageType,
|
SuggestionMessageType,
|
||||||
PostActionType,
|
PostActionType,
|
||||||
ExtactionConfigType,
|
ExtactionConfigType,
|
||||||
|
MultiEntityConfig,
|
||||||
|
MultiEntityExpandMode,
|
||||||
VLMPromptSetting,
|
VLMPromptSetting,
|
||||||
VLMFieldType,
|
VLMFieldType,
|
||||||
LLMPromptSetting,
|
LLMPromptSetting,
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ export interface Rule {
|
|||||||
prompt: string;
|
prompt: string;
|
||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
area?: string; // 地区
|
area?: string; // 地区
|
||||||
|
documentAttributeType?: string; // 文档属性类型
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
}
|
}
|
||||||
|
|||||||
+20
-2
@@ -47,6 +47,13 @@ import {
|
|||||||
getCurrentPort
|
getCurrentPort
|
||||||
} from "~/config/api-config";
|
} from "~/config/api-config";
|
||||||
|
|
||||||
|
// 导入移动端检测工具
|
||||||
|
import {
|
||||||
|
isMobileDevice,
|
||||||
|
isMobileAllowedPath,
|
||||||
|
MOBILE_CHAT_PATH
|
||||||
|
} from "~/utils/mobile-detect.server";
|
||||||
|
|
||||||
// 定义需要高级权限的路径
|
// 定义需要高级权限的路径
|
||||||
// export const developerOnlyPaths = [
|
// export const developerOnlyPaths = [
|
||||||
// '/settings',
|
// '/settings',
|
||||||
@@ -296,6 +303,16 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
|||||||
// 如果执行到这里,说明已通过认证或是公共路径
|
// 如果执行到这里,说明已通过认证或是公共路径
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 🔒 移动端路由守卫
|
||||||
|
// 移动端用户只能访问对话页面,尝试访问其他页面时重定向
|
||||||
|
const isMobile = isMobileDevice(request);
|
||||||
|
if (isMobile && !isPublicPath) {
|
||||||
|
if (!isMobileAllowedPath(pathname)) {
|
||||||
|
console.log(`📱 [Root Loader] 移动端用户尝试访问 ${pathname},重定向到对话页面`);
|
||||||
|
return redirect(MOBILE_CHAT_PATH);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 检查交叉评查专属模式访问控制
|
// 检查交叉评查专属模式访问控制
|
||||||
// 当 CROSS_CHECKING_ONLY_MODE=true 且端口为指定端口时,只允许访问 /cross-checking 相关路由
|
// 当 CROSS_CHECKING_ONLY_MODE=true 且端口为指定端口时,只允许访问 /cross-checking 相关路由
|
||||||
const currentPort = getCurrentPort();
|
const currentPort = getCurrentPort();
|
||||||
@@ -330,6 +347,7 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
|||||||
pathname,
|
pathname,
|
||||||
frontendJWT,
|
frontendJWT,
|
||||||
isPublicPath, // 传递给客户端,用于判断是否需要认证
|
isPublicPath, // 传递给客户端,用于判断是否需要认证
|
||||||
|
isMobile, // 🔒 传递移动端标识
|
||||||
permissionMap, // ✅ 传递权限映射表
|
permissionMap, // ✅ 传递权限映射表
|
||||||
ENV: {
|
ENV: {
|
||||||
// 客户端不再需要直接调用 Dify API
|
// 客户端不再需要直接调用 Dify API
|
||||||
@@ -366,7 +384,7 @@ export function links() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
const { userRole, ENV, frontendJWT, isPublicPath } = useLoaderData<typeof loader>();
|
const { userRole, ENV, frontendJWT, isPublicPath, isMobile } = useLoaderData<typeof loader>();
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -406,7 +424,7 @@ export default function App() {
|
|||||||
{isPublicPath ? (
|
{isPublicPath ? (
|
||||||
<Outlet />
|
<Outlet />
|
||||||
) : (
|
) : (
|
||||||
<Layout userRole={userRole} frontendJWT={frontendJWT}>
|
<Layout userRole={userRole} frontendJWT={frontendJWT} isMobile={isMobile}>
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</Layout>
|
</Layout>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { createUserSession, sessionStorage } from "~/api/login/auth.server";
|
|||||||
import { OAuthClient } from "~/api/login/oauth-client";
|
import { OAuthClient } from "~/api/login/oauth-client";
|
||||||
import { getServerOAuthConfigRuntime } from "~/config/oauth-secret.server";
|
import { getServerOAuthConfigRuntime } from "~/config/oauth-secret.server";
|
||||||
import { loginWithOAuth, type LoginRequest } from "~/api/login/login-client";
|
import { loginWithOAuth, type LoginRequest } from "~/api/login/login-client";
|
||||||
|
import { isMobileDevice, MOBILE_CHAT_PATH } from "~/utils/mobile-detect.server";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 端口号到地区的映射关系
|
* 端口号到地区的映射关系
|
||||||
@@ -145,9 +146,12 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
|||||||
}
|
}
|
||||||
console.log("✅ [Callback] 用户信息获取成功");
|
console.log("✅ [Callback] 用户信息获取成功");
|
||||||
|
|
||||||
// 🔑 强制重定向到首页,确保用户选择入口模块并初始化 sessionStorage
|
// 🔑 检测移动端设备,决定重定向目标
|
||||||
// 忽略 redirect 参数,总是跳转到首页让用户选择模块
|
const isMobile = isMobileDevice(request);
|
||||||
const redirectTo = "/";
|
console.log(`📱 [Callback] 设备类型检测: ${isMobile ? '移动端' : '桌面端'}`);
|
||||||
|
|
||||||
|
// 移动端用户直接跳转到对话页面,桌面端用户跳转到首页选择模块
|
||||||
|
const redirectTo = isMobile ? MOBILE_CHAT_PATH : "/";
|
||||||
|
|
||||||
// 调用后端登录接口,传递 OAuth 用户信息,获取 JWT token
|
// 调用后端登录接口,传递 OAuth 用户信息,获取 JWT token
|
||||||
const loginRequest: LoginRequest = {
|
const loginRequest: LoginRequest = {
|
||||||
@@ -272,8 +276,8 @@ export default function Callback() {
|
|||||||
// 从 URL 参数中获取 token(如果有)
|
// 从 URL 参数中获取 token(如果有)
|
||||||
const token = searchParams.get("token");
|
const token = searchParams.get("token");
|
||||||
const userInfo = searchParams.get("userInfo");
|
const userInfo = searchParams.get("userInfo");
|
||||||
// 🔑 强制重定向到首页,确保用户选择入口模块并初始化 sessionStorage
|
// 从 URL 参数中获取重定向目标(服务端已根据设备类型设置)
|
||||||
const redirectTo = "/";
|
const redirectTo = searchParams.get("redirectTo") || "/";
|
||||||
|
|
||||||
if (token && typeof window !== 'undefined') {
|
if (token && typeof window !== 'undefined') {
|
||||||
console.log('🔑 [Callback] 开始保存 token 到 localStorage');
|
console.log('🔑 [Callback] 开始保存 token 到 localStorage');
|
||||||
|
|||||||
@@ -288,7 +288,7 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
|||||||
typesPromise
|
typesPromise
|
||||||
]);
|
]);
|
||||||
// console.log(`[loader 耗时] 并行API调用总耗时: ${Date.now() - apiStart}ms`);
|
// console.log(`[loader 耗时] 并行API调用总耗时: ${Date.now() - apiStart}ms`);
|
||||||
console.log(`[loader 耗时] loader总耗时: ${(Date.now() - loaderStart)/1000}s`);
|
// console.log(`[loader 耗时] loader总耗时: ${(Date.now() - loaderStart)/1000}s`);
|
||||||
|
|
||||||
// console.log('loader: 文档加载结果:', documentsResponse);
|
// console.log('loader: 文档加载结果:', documentsResponse);
|
||||||
// console.log('loader: 文档类型加载结果:', typesResponse);
|
// console.log('loader: 文档类型加载结果:', typesResponse);
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ interface ApiRule {
|
|||||||
description: string;
|
description: string;
|
||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
area?: string; // 地区
|
area?: string; // 地区
|
||||||
|
documentAttributeType?: string; // 文档属性类型
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
}
|
}
|
||||||
@@ -98,6 +99,7 @@ function mapApiRuleToModel(apiRule: ApiRule): Rule {
|
|||||||
prompt: apiRule.description, // 使用描述作为默认prompt
|
prompt: apiRule.description, // 使用描述作为默认prompt
|
||||||
isActive: apiRule.isActive,
|
isActive: apiRule.isActive,
|
||||||
area: apiRule.area || '', // 地区
|
area: apiRule.area || '', // 地区
|
||||||
|
documentAttributeType: apiRule.documentAttributeType || '', // 文档属性类型
|
||||||
createdAt: apiRule.createdAt,
|
createdAt: apiRule.createdAt,
|
||||||
updatedAt: apiRule.updatedAt
|
updatedAt: apiRule.updatedAt
|
||||||
};
|
};
|
||||||
@@ -667,7 +669,7 @@ export default function RulesIndex() {
|
|||||||
),
|
),
|
||||||
key: "selection",
|
key: "selection",
|
||||||
align: "center" as const,
|
align: "center" as const,
|
||||||
width: "50px",
|
width: "3%",
|
||||||
render: (_: unknown, record: Rule) => (
|
render: (_: unknown, record: Rule) => (
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
@@ -682,7 +684,7 @@ export default function RulesIndex() {
|
|||||||
dataIndex: "code" as keyof Rule,
|
dataIndex: "code" as keyof Rule,
|
||||||
key: "code",
|
key: "code",
|
||||||
align: "left" as const,
|
align: "left" as const,
|
||||||
width: "15%",
|
width: "9%",
|
||||||
className: "whitespace-normal break-all",
|
className: "whitespace-normal break-all",
|
||||||
render: (value: string) => (
|
render: (value: string) => (
|
||||||
<div className="whitespace-normal break-all overflow-visible">{value}</div>
|
<div className="whitespace-normal break-all overflow-visible">{value}</div>
|
||||||
@@ -693,13 +695,13 @@ export default function RulesIndex() {
|
|||||||
dataIndex: "name" as keyof Rule,
|
dataIndex: "name" as keyof Rule,
|
||||||
key: "name",
|
key: "name",
|
||||||
align: "left" as const,
|
align: "left" as const,
|
||||||
width: "15%"
|
width: "12%"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "评查点类型",
|
title: "评查点类型",
|
||||||
key: "ruleType",
|
key: "ruleType",
|
||||||
align: "left" as const,
|
align: "left" as const,
|
||||||
width: "10%",
|
width: "8%",
|
||||||
render: (_: unknown, record: Rule) => {
|
render: (_: unknown, record: Rule) => {
|
||||||
const typeColor = RULE_TYPE_COLORS[record.ruleType] as TagColor;
|
const typeColor = RULE_TYPE_COLORS[record.ruleType] as TagColor;
|
||||||
return (
|
return (
|
||||||
@@ -714,13 +716,21 @@ export default function RulesIndex() {
|
|||||||
dataIndex: "groupName" as keyof Rule,
|
dataIndex: "groupName" as keyof Rule,
|
||||||
key: "groupName",
|
key: "groupName",
|
||||||
align: "left" as const,
|
align: "left" as const,
|
||||||
width: "10%"
|
width: "8%"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "地区",
|
title: "地区",
|
||||||
dataIndex: "area" as keyof Rule,
|
dataIndex: "area" as keyof Rule,
|
||||||
key: "area",
|
key: "area",
|
||||||
align: "left" as const,
|
align: "left" as const,
|
||||||
|
width: "5%",
|
||||||
|
render: (value: string) => value || '-'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "属性类型",
|
||||||
|
dataIndex: "documentAttributeType" as keyof Rule,
|
||||||
|
key: "documentAttributeType",
|
||||||
|
align: "left" as const,
|
||||||
width: "6%",
|
width: "6%",
|
||||||
render: (value: string) => value || '-'
|
render: (value: string) => value || '-'
|
||||||
},
|
},
|
||||||
@@ -728,7 +738,7 @@ export default function RulesIndex() {
|
|||||||
title: "优先级",
|
title: "优先级",
|
||||||
key: "priority",
|
key: "priority",
|
||||||
align: "left" as const,
|
align: "left" as const,
|
||||||
width: "6%",
|
width: "5%",
|
||||||
render: (_: unknown, record: Rule) => {
|
render: (_: unknown, record: Rule) => {
|
||||||
const priorityColor = RULE_PRIORITY_COLORS[record.priority] as TagColor;
|
const priorityColor = RULE_PRIORITY_COLORS[record.priority] as TagColor;
|
||||||
return (
|
return (
|
||||||
@@ -742,7 +752,7 @@ export default function RulesIndex() {
|
|||||||
title: "状态",
|
title: "状态",
|
||||||
key: "isActive",
|
key: "isActive",
|
||||||
align: "left" as const,
|
align: "left" as const,
|
||||||
width: "6%",
|
width: "5%",
|
||||||
render: (_: unknown, record: Rule) => (
|
render: (_: unknown, record: Rule) => (
|
||||||
<StatusDot status={record.isActive} text={record.isActive ? "启用" : "禁用"} />
|
<StatusDot status={record.isActive} text={record.isActive ? "启用" : "禁用"} />
|
||||||
)
|
)
|
||||||
@@ -752,13 +762,13 @@ export default function RulesIndex() {
|
|||||||
dataIndex: "createdAt" as keyof Rule,
|
dataIndex: "createdAt" as keyof Rule,
|
||||||
key: "createdAt",
|
key: "createdAt",
|
||||||
align: "left" as const,
|
align: "left" as const,
|
||||||
width: "10%"
|
width: "9%"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "操作",
|
title: "操作",
|
||||||
key: "operation",
|
key: "operation",
|
||||||
align: "left" as const,
|
align: "left" as const,
|
||||||
width: "150px",
|
width: "14%",
|
||||||
render: (_: unknown, record: Rule) => (
|
render: (_: unknown, record: Rule) => (
|
||||||
<div className="operations-cell">
|
<div className="operations-cell">
|
||||||
{/* ✅ 查看/编辑和复制按钮 - 需要查看权限 */}
|
{/* ✅ 查看/编辑和复制按钮 - 需要查看权限 */}
|
||||||
|
|||||||
@@ -238,7 +238,12 @@ export default function RuleNew() {
|
|||||||
references_laws: { name: '', articles: [], content: '' },
|
references_laws: { name: '', articles: [], content: '' },
|
||||||
evaluation_point_groups_pid: null,
|
evaluation_point_groups_pid: null,
|
||||||
evaluation_point_groups_id: null,
|
evaluation_point_groups_id: null,
|
||||||
|
document_attribute_type: '通用',
|
||||||
extraction_config: {
|
extraction_config: {
|
||||||
|
multi_entity: {
|
||||||
|
enabled: false,
|
||||||
|
expand_mode: 'awareness'
|
||||||
|
},
|
||||||
llm: {
|
llm: {
|
||||||
fields: [],
|
fields: [],
|
||||||
prompt_setting: {
|
prompt_setting: {
|
||||||
@@ -633,7 +638,12 @@ export default function RuleNew() {
|
|||||||
references_laws: formData.references_laws || null,
|
references_laws: formData.references_laws || null,
|
||||||
evaluation_point_groups_pid: formData.evaluation_point_groups_pid || null,
|
evaluation_point_groups_pid: formData.evaluation_point_groups_pid || null,
|
||||||
evaluation_point_groups_id: formData.evaluation_point_groups_id || null,
|
evaluation_point_groups_id: formData.evaluation_point_groups_id || null,
|
||||||
|
document_attribute_type: formData.document_attribute_type || '',
|
||||||
extraction_config: {
|
extraction_config: {
|
||||||
|
multi_entity: {
|
||||||
|
enabled: formData.extraction_config?.multi_entity?.enabled ?? false,
|
||||||
|
expand_mode: formData.extraction_config?.multi_entity?.expand_mode || 'awareness'
|
||||||
|
},
|
||||||
llm: {
|
llm: {
|
||||||
fields: Array.isArray(formData.extraction_config?.llm?.fields) ?
|
fields: Array.isArray(formData.extraction_config?.llm?.fields) ?
|
||||||
[...formData.extraction_config.llm.fields] : [],
|
[...formData.extraction_config.llm.fields] : [],
|
||||||
@@ -838,7 +848,7 @@ export default function RuleNew() {
|
|||||||
if (isEditMode) {
|
if (isEditMode) {
|
||||||
// 使用新的 updateEvaluationPoint API
|
// 使用新的 updateEvaluationPoint API
|
||||||
response = await updateEvaluationPoint(String(formData.id!), finalData, frontendJWT);
|
response = await updateEvaluationPoint(String(formData.id!), finalData, frontendJWT);
|
||||||
// console.log("最终提交的数据", finalData)
|
console.log("最终提交的数据", finalData)
|
||||||
} else {
|
} else {
|
||||||
// 使用新的 createEvaluationPoint API
|
// 使用新的 createEvaluationPoint API
|
||||||
response = await createEvaluationPoint(finalData as Omit<EvaluationPointData, 'id' | 'created_at' | 'updated_at'>, frontendJWT);
|
response = await createEvaluationPoint(finalData as Omit<EvaluationPointData, 'id' | 'created_at' | 'updated_at'>, frontendJWT);
|
||||||
|
|||||||
@@ -495,4 +495,56 @@
|
|||||||
.source-item {
|
.source-item {
|
||||||
max-width: 180px;
|
max-width: 180px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 移动端 Tooltip 弹出框适配 */
|
||||||
|
.source-tooltip-overlay {
|
||||||
|
max-width: calc(100vw - 32px) !important;
|
||||||
|
left: 16px !important;
|
||||||
|
right: 16px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.source-tooltip-overlay .ant-tooltip-inner {
|
||||||
|
max-width: 100%;
|
||||||
|
word-wrap: break-word;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.source-tooltip {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.source-tooltip-header {
|
||||||
|
word-wrap: break-word;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.source-tooltip-content {
|
||||||
|
max-height: 40vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
word-wrap: break-word;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 小屏手机进一步优化 */
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.source-tooltip-overlay {
|
||||||
|
max-width: calc(100vw - 24px) !important;
|
||||||
|
left: 12px !important;
|
||||||
|
right: 12px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.source-tooltip-content {
|
||||||
|
max-height: 35vh;
|
||||||
|
font-size: 11px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.source-tooltip-header {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.source-tooltip-meta {
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -144,9 +144,40 @@
|
|||||||
/* 小手机 - 480px以下 */
|
/* 小手机 - 480px以下 */
|
||||||
@media (max-width: 479px) {
|
@media (max-width: 479px) {
|
||||||
.ant-layout-sider {
|
.ant-layout-sider {
|
||||||
width: 100vw !important;
|
width: 70% !important;
|
||||||
min-width: 100vw !important;
|
min-width: 70% !important;
|
||||||
max-width: 100vw !important;
|
max-width: 70% !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========== 移动端遮罩层样式 ========== */
|
||||||
|
|
||||||
|
/* 侧边栏遮罩层 - 作用域:移动端侧边栏展开时的半透明背景 */
|
||||||
|
.chat-sidebar-overlay {
|
||||||
|
display: none;
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background-color: rgba(0, 0, 0, 0.45);
|
||||||
|
z-index: 999;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 遮罩层显示状态 */
|
||||||
|
.chat-sidebar-overlay.visible {
|
||||||
|
display: block;
|
||||||
|
opacity: 1;
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 移动端显示遮罩层 */
|
||||||
|
@media (max-width: 991px) {
|
||||||
|
.chat-sidebar-overlay.visible {
|
||||||
|
display: block;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -67,6 +67,14 @@
|
|||||||
.rules-page .ant-table {
|
.rules-page .ant-table {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
|
table-layout: fixed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 表格单元格自适应 */
|
||||||
|
.rules-page .ant-table td,
|
||||||
|
.rules-page .ant-table th {
|
||||||
|
word-wrap: break-word;
|
||||||
|
overflow-wrap: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -85,9 +93,13 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* 表格操作列样式 */
|
/* 表格操作列样式 - 自适应换行 */
|
||||||
.rules-page .operations-cell {
|
.rules-page .operations-cell {
|
||||||
white-space: nowrap;
|
white-space: normal;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 4px 8px;
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 操作按钮样式 - 改为文本按钮样式 */
|
/* 操作按钮样式 - 改为文本按钮样式 */
|
||||||
@@ -105,7 +117,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.rules-page .operation-btn {
|
.rules-page .operation-btn {
|
||||||
margin-right: 8px;
|
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -114,10 +125,12 @@
|
|||||||
color: #00684a;
|
color: #00684a;
|
||||||
border: none;
|
border: none;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
padding: 4px 8px;
|
padding: 4px 6px;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
white-space: nowrap;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.rules-page .operation-btn i {
|
.rules-page .operation-btn i {
|
||||||
|
|||||||
@@ -0,0 +1,106 @@
|
|||||||
|
/**
|
||||||
|
* 移动端检测工具函数(服务端)
|
||||||
|
* 通过 User-Agent 判断设备类型
|
||||||
|
*/
|
||||||
|
|
||||||
|
// 移动端 User-Agent 关键词
|
||||||
|
const MOBILE_UA_KEYWORDS = [
|
||||||
|
'Mobile',
|
||||||
|
'Android',
|
||||||
|
'iPhone',
|
||||||
|
'iPad',
|
||||||
|
'iPod',
|
||||||
|
'BlackBerry',
|
||||||
|
'Windows Phone',
|
||||||
|
'Opera Mini',
|
||||||
|
'IEMobile',
|
||||||
|
'webOS',
|
||||||
|
'Kindle',
|
||||||
|
'Silk',
|
||||||
|
'UCBrowser',
|
||||||
|
'MiuiBrowser',
|
||||||
|
'HuaweiBrowser',
|
||||||
|
'VivoBrowser',
|
||||||
|
'OppoBrowser'
|
||||||
|
];
|
||||||
|
|
||||||
|
// 平板设备关键词(可能需要区分对待)
|
||||||
|
const TABLET_UA_KEYWORDS = [
|
||||||
|
'iPad',
|
||||||
|
'Tablet',
|
||||||
|
'PlayBook',
|
||||||
|
'Silk'
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检测请求是否来自移动端设备
|
||||||
|
* @param request - HTTP 请求对象
|
||||||
|
* @returns true 表示移动端设备(包括平板)
|
||||||
|
*/
|
||||||
|
export function isMobileDevice(request: Request): boolean {
|
||||||
|
const userAgent = request.headers.get('User-Agent') || '';
|
||||||
|
|
||||||
|
// 检查是否包含移动端关键词
|
||||||
|
return MOBILE_UA_KEYWORDS.some(keyword =>
|
||||||
|
userAgent.toLowerCase().includes(keyword.toLowerCase())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检测请求是否来自平板设备
|
||||||
|
* @param request - HTTP 请求对象
|
||||||
|
* @returns true 表示平板设备
|
||||||
|
*/
|
||||||
|
export function isTabletDevice(request: Request): boolean {
|
||||||
|
const userAgent = request.headers.get('User-Agent') || '';
|
||||||
|
|
||||||
|
return TABLET_UA_KEYWORDS.some(keyword =>
|
||||||
|
userAgent.toLowerCase().includes(keyword.toLowerCase())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检测请求是否来自手机设备(排除平板)
|
||||||
|
* @param request - HTTP 请求对象
|
||||||
|
* @returns true 表示手机设备
|
||||||
|
*/
|
||||||
|
export function isPhoneDevice(request: Request): boolean {
|
||||||
|
return isMobileDevice(request) && !isTabletDevice(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取设备类型
|
||||||
|
* @param request - HTTP 请求对象
|
||||||
|
* @returns 'mobile' | 'tablet' | 'desktop'
|
||||||
|
*/
|
||||||
|
export function getDeviceType(request: Request): 'mobile' | 'tablet' | 'desktop' {
|
||||||
|
if (isTabletDevice(request)) {
|
||||||
|
return 'tablet';
|
||||||
|
}
|
||||||
|
if (isMobileDevice(request)) {
|
||||||
|
return 'mobile';
|
||||||
|
}
|
||||||
|
return 'desktop';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 移动端用户专属的聊天页面路径
|
||||||
|
export const MOBILE_CHAT_PATH = '/chat-with-llm/chat';
|
||||||
|
|
||||||
|
// 移动端允许访问的路径(白名单)
|
||||||
|
export const MOBILE_ALLOWED_PATHS = [
|
||||||
|
'/chat-with-llm/chat',
|
||||||
|
'/login',
|
||||||
|
'/callback',
|
||||||
|
'/favicon.ico'
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查路径是否为移动端允许访问的路径
|
||||||
|
* @param pathname - 当前访问的路径
|
||||||
|
* @returns true 表示允许访问
|
||||||
|
*/
|
||||||
|
export function isMobileAllowedPath(pathname: string): boolean {
|
||||||
|
return MOBILE_ALLOWED_PATHS.some(path =>
|
||||||
|
pathname === path || pathname.startsWith(path + '/')
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user