Merge branch 'shiy-login' of http://git.7bm.co:1024/leke/docreview into shiy-login
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
|
||||
type Option = {
|
||||
value: string;
|
||||
label: string;
|
||||
children?: Option[];
|
||||
};
|
||||
|
||||
type CascaderProps = {
|
||||
options: Option[];
|
||||
defaultValue?: string[];
|
||||
onChange?: (value: string[]) => void;
|
||||
placeholder?: string;
|
||||
};
|
||||
|
||||
const Cascader: React.FC<CascaderProps> = ({ options, defaultValue = [], onChange, placeholder = 'Select' }) => {
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [selected, setSelected] = useState<string[]>(defaultValue);
|
||||
const [menus, setMenus] = useState<Option[][]>([options]);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (containerRef.current && !containerRef.current.contains(event.target as Node)) {
|
||||
setVisible(false);
|
||||
}
|
||||
};
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
return () => document.removeEventListener('mousedown', handleClickOutside);
|
||||
}, []);
|
||||
|
||||
const handleItemClick = (option: Option, level: number) => {
|
||||
const newSelected = selected.slice(0, level).concat(option.value);
|
||||
setSelected(newSelected);
|
||||
if (option.children) {
|
||||
setMenus(menus.slice(0, level + 1).concat([option.children]));
|
||||
} else {
|
||||
setMenus(menus.slice(0, level + 1));
|
||||
setVisible(false);
|
||||
onChange?.(newSelected);
|
||||
}
|
||||
};
|
||||
|
||||
const getDisplayText = () => {
|
||||
if (selected.length === 0) return placeholder;
|
||||
let currentOptions = options;
|
||||
return selected.map(val => {
|
||||
const opt = currentOptions.find(o => o.value === val);
|
||||
currentOptions = opt?.children || [];
|
||||
return opt?.label || '';
|
||||
}).join(' / ');
|
||||
};
|
||||
|
||||
return (
|
||||
<div ref={containerRef} className="relative inline-block">
|
||||
<div
|
||||
className="border border-gray-300 rounded px-3 py-1 cursor-pointer"
|
||||
onClick={() => setVisible(!visible)}
|
||||
>
|
||||
{getDisplayText()}
|
||||
</div>
|
||||
{visible && (
|
||||
<div className="absolute z-10 bg-white border border-gray-300 rounded shadow-lg mt-1 flex">
|
||||
{menus.map((menu, level) => (
|
||||
<ul key={level} className="list-none m-0 p-0 min-w-[150px] border-r border-gray-200 last:border-r-0">
|
||||
{menu.map(option => (
|
||||
<li
|
||||
key={option.value}
|
||||
className={`px-3 py-1 cursor-pointer hover:bg-gray-100 ${selected[level] === option.value ? 'bg-gray-200' : ''}`}
|
||||
onClick={() => handleItemClick(option, level)}
|
||||
>
|
||||
{option.label}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Cascader;
|
||||
@@ -16,6 +16,17 @@ export interface DateRangePickerProps {
|
||||
colorMode?: 'light' | 'dark' | 'auto';
|
||||
}
|
||||
|
||||
export interface SingleDatePickerProps {
|
||||
date: string;
|
||||
onDateChange: (value: string) => void;
|
||||
label?: string;
|
||||
className?: string;
|
||||
id?: string;
|
||||
format?: string;
|
||||
placeholder?: string;
|
||||
colorMode?: 'light' | 'dark' | 'auto';
|
||||
}
|
||||
|
||||
export function links() {
|
||||
return [
|
||||
{ rel: "stylesheet", href: dateRangePickerStyles }
|
||||
@@ -199,6 +210,117 @@ export function DateRangePicker({
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 单日期选择器组件
|
||||
* 用于选择单个日期
|
||||
*/
|
||||
export function SingleDatePicker({
|
||||
date,
|
||||
onDateChange,
|
||||
label = "日期",
|
||||
className = "",
|
||||
id = "date-single",
|
||||
format = "yyyy-MM-dd",
|
||||
placeholder = "请选择日期",
|
||||
colorMode = 'auto'
|
||||
}: SingleDatePickerProps) {
|
||||
// 使用ref获取input元素
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
// 添加状态跟踪日期输入框是否被聚焦
|
||||
const [isFocused, setIsFocused] = useState<boolean>(false);
|
||||
|
||||
// 格式化显示日期
|
||||
const formattedDate = date ? formatDate(date, format) : "";
|
||||
|
||||
// 保持原始日期输入框的显示格式
|
||||
useEffect(() => {
|
||||
if (inputRef.current) {
|
||||
inputRef.current.setAttribute('data-display-value', formattedDate || placeholder);
|
||||
}
|
||||
}, [formattedDate, placeholder]);
|
||||
|
||||
// 处理日期输入框全局点击
|
||||
const handleWrapperClick = () => {
|
||||
if (inputRef.current) {
|
||||
// 点击整个包装器区域时,触发输入框点击
|
||||
inputRef.current.focus();
|
||||
inputRef.current.click();
|
||||
|
||||
try {
|
||||
// 检查并使用showPicker API
|
||||
const hasShowPicker = typeof inputRef.current.showPicker === 'function';
|
||||
|
||||
// 尝试使用showPicker API
|
||||
if (hasShowPicker) {
|
||||
inputRef.current.showPicker();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to show date picker:', error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 处理键盘事件
|
||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
handleWrapperClick();
|
||||
}
|
||||
};
|
||||
|
||||
// 处理聚焦和失焦
|
||||
const handleFocus = () => {
|
||||
setIsFocused(true);
|
||||
};
|
||||
|
||||
const handleBlur = () => {
|
||||
setIsFocused(false);
|
||||
};
|
||||
|
||||
// 获取颜色模式类名
|
||||
const getColorModeClass = () => {
|
||||
if (colorMode === 'light') return 'color-mode-light';
|
||||
if (colorMode === 'dark') return 'color-mode-dark';
|
||||
return ''; // auto模式不添加额外类名
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`date-picker single-date-picker ${getColorModeClass()} ${className}`}>
|
||||
<div className="date-field">
|
||||
<label htmlFor={id} className="date-label">{label}</label>
|
||||
<div
|
||||
className={`date-input-wrapper ${isFocused ? 'focused' : ''}`}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleWrapperClick();
|
||||
}}
|
||||
onKeyDown={handleKeyDown}
|
||||
tabIndex={0}
|
||||
role="button"
|
||||
aria-label={`选择${label}`}
|
||||
>
|
||||
<input
|
||||
id={id}
|
||||
ref={inputRef}
|
||||
type="date"
|
||||
className="date-input"
|
||||
value={date}
|
||||
onChange={(e) => onDateChange(e.target.value)}
|
||||
onFocus={handleFocus}
|
||||
onBlur={handleBlur}
|
||||
/>
|
||||
<div
|
||||
className={`date-display ${!formattedDate ? 'placeholder' : ''}`}
|
||||
>
|
||||
{formattedDate || placeholder}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 简化版日期范围选择器,适用于紧凑布局,不显示标签
|
||||
export function SimpleDateRangePicker({
|
||||
startDate,
|
||||
@@ -344,4 +466,4 @@ export function SimpleDateRangePicker({
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
|
||||
type Option = {
|
||||
value: string;
|
||||
label: string;
|
||||
children?: Option[];
|
||||
};
|
||||
|
||||
type MultiCascaderProps = {
|
||||
options: Option[];
|
||||
defaultValue?: string[];
|
||||
value?: string[];
|
||||
onChange?: (value: string[]) => void;
|
||||
placeholder?: string;
|
||||
};
|
||||
|
||||
// 获取所有叶子节点的值
|
||||
const getAllLeafValues = (option: Option): string[] => {
|
||||
if (!option.children || option.children.length === 0) {
|
||||
return [option.value];
|
||||
}
|
||||
return option.children.flatMap(getAllLeafValues);
|
||||
};
|
||||
|
||||
// 检查节点是否所有子节点都被选中
|
||||
const isAllChildrenChecked = (option: Option, selected: string[]): boolean => {
|
||||
if (!option.children || option.children.length === 0) {
|
||||
return selected.includes(option.value);
|
||||
}
|
||||
// 对于有子节点的节点,检查其所有叶子节点是否都被选中
|
||||
const leafValues = getAllLeafValues(option);
|
||||
return leafValues.every(value => selected.includes(value));
|
||||
};
|
||||
|
||||
// 检查节点是否有部分子节点被选中
|
||||
const isSomeChildrenChecked = (option: Option, selected: string[]): boolean => {
|
||||
if (!option.children || option.children.length === 0) {
|
||||
return selected.includes(option.value);
|
||||
}
|
||||
// 对于有子节点的节点,检查其叶子节点是否有部分被选中
|
||||
const leafValues = getAllLeafValues(option);
|
||||
return leafValues.some(value => selected.includes(value));
|
||||
};
|
||||
|
||||
const MultiCascader: React.FC<MultiCascaderProps> = ({
|
||||
options,
|
||||
defaultValue = [],
|
||||
value,
|
||||
onChange,
|
||||
placeholder = '请选择'
|
||||
}) => {
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [selected, setSelected] = useState<string[]>(value ?? defaultValue);
|
||||
const [expandedKeys, setExpandedKeys] = useState<Set<string>>(new Set());
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// 当外部 value 变化时,同步内部状态
|
||||
useEffect(() => {
|
||||
if (value !== undefined) {
|
||||
setSelected(value);
|
||||
}
|
||||
}, [value]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (containerRef.current && !containerRef.current.contains(event.target as Node)) {
|
||||
setVisible(false);
|
||||
}
|
||||
};
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
return () => document.removeEventListener('mousedown', handleClickOutside);
|
||||
}, []);
|
||||
|
||||
const handleItemCheck = (option: Option, checked: boolean) => {
|
||||
const leafValues = getAllLeafValues(option);
|
||||
let newSelected: string[];
|
||||
|
||||
if (checked) {
|
||||
newSelected = Array.from(new Set([...selected, ...leafValues]));
|
||||
} else {
|
||||
newSelected = selected.filter(v => !leafValues.includes(v));
|
||||
}
|
||||
|
||||
setSelected(newSelected);
|
||||
onChange?.(newSelected);
|
||||
};
|
||||
|
||||
const getDisplayText = () => {
|
||||
if (selected.length === 0) return placeholder;
|
||||
return `已选择 ${selected.length} 项`;
|
||||
};
|
||||
|
||||
const toggleExpand = (key: string) => {
|
||||
setExpandedKeys(prev => {
|
||||
const newSet = new Set(prev);
|
||||
if (newSet.has(key)) {
|
||||
newSet.delete(key);
|
||||
} else {
|
||||
newSet.add(key);
|
||||
}
|
||||
return newSet;
|
||||
});
|
||||
};
|
||||
|
||||
const renderOption = (option: Option, level: number = 0) => {
|
||||
const allChecked = isAllChildrenChecked(option, selected);
|
||||
const someChecked = isSomeChildrenChecked(option, selected);
|
||||
const hasChildren = option.children && option.children.length > 0;
|
||||
const isExpanded = expandedKeys.has(option.value);
|
||||
|
||||
return (
|
||||
<div key={option.value} className={level > 0 ? "pl-4" : ""}>
|
||||
<div className="flex items-center py-1">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="form-checkbox h-4 w-4 text-[var(--color-primary,#00684a)] rounded border-gray-300 focus:ring-[var(--color-primary,#00684a)]"
|
||||
checked={allChecked}
|
||||
ref={el => { if (el) el.indeterminate = !allChecked && someChecked; }}
|
||||
onChange={e => handleItemCheck(option, e.target.checked)}
|
||||
id={`cascader-${option.value}`}
|
||||
/>
|
||||
<label htmlFor={`cascader-${option.value}`} className="ml-2 text-sm flex-1">
|
||||
{option.label}
|
||||
</label>
|
||||
{hasChildren && (
|
||||
<button
|
||||
type="button"
|
||||
className="ml-2 text-gray-400 hover:text-gray-600"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
toggleExpand(option.value);
|
||||
}}
|
||||
>
|
||||
<i className={isExpanded ? "ri-arrow-down-s-line" : "ri-arrow-right-s-line"}></i>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{hasChildren && isExpanded && (
|
||||
<div className="ml-4 border-l border-gray-200 pl-2">
|
||||
{option.children?.map(child => (
|
||||
<React.Fragment key={child.value}>
|
||||
{renderOption(child, level + 1)}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div ref={containerRef} className="relative inline-block w-full">
|
||||
<div
|
||||
className="border border-gray-300 rounded px-3 py-2 cursor-pointer bg-white flex justify-between items-center focus:outline-none focus:ring-2 focus:ring-[var(--color-primary,#00684a)] focus:border-[var(--color-primary,#00684a)]"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
setVisible(!visible);
|
||||
}
|
||||
}}
|
||||
onClick={() => setVisible(!visible)}
|
||||
>
|
||||
<span className={selected.length === 0 ? 'text-gray-400' : ''}>
|
||||
{getDisplayText()}
|
||||
</span>
|
||||
<span className="text-gray-400">
|
||||
<i className={visible ? "ri-arrow-up-s-line" : "ri-arrow-down-s-line"}></i>
|
||||
</span>
|
||||
</div>
|
||||
{visible && (
|
||||
<div className="absolute z-10 bg-white border border-gray-300 rounded shadow-lg mt-1 w-full max-h-100 overflow-y-auto">
|
||||
<div className="p-2">
|
||||
{options.map(option => renderOption(option))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MultiCascader;
|
||||
@@ -458,7 +458,7 @@ export default function CrossCheckingIndex() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Button type="primary" icon="ri-add-line" to="/cross-checking/new">
|
||||
<Button type="primary" icon="ri-add-line" to="/cross-checking/upload">
|
||||
创建任务
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { useState, useRef } from "react";
|
||||
import { type MetaFunction, type ActionFunctionArgs } from "@remix-run/node";
|
||||
import { Form, useNavigation } from "@remix-run/react";
|
||||
import { Form, useNavigation, useNavigate } from "@remix-run/react";
|
||||
import { UploadArea, type UploadAreaRef } from "~/components/ui/UploadArea";
|
||||
import { Button } from "~/components/ui/Button";
|
||||
import { messageService } from "~/components/ui/MessageModal";
|
||||
import { toastService } from "~/components/ui/Toast";
|
||||
import crossCheckingUploadStyles from "~/styles/pages/cross-checking-upload.css?url";
|
||||
import MultiCascader from "~/components/ui/MultiCascader";
|
||||
import { SingleDatePicker, links as dateRangePickerLinks } from "~/components/ui/DateRangePicker";
|
||||
import {
|
||||
CaseType,
|
||||
CASE_TYPE_TO_TYPE_ID,
|
||||
@@ -14,6 +16,7 @@ import {
|
||||
formatFileSize,
|
||||
batchUploadCrossCheckingFiles
|
||||
} from "~/api/cross-checking/cross-files-upload";
|
||||
import React from "react"; // Added for React.useState
|
||||
|
||||
export const meta: MetaFunction = () => {
|
||||
return [
|
||||
@@ -28,7 +31,8 @@ export const handle = {
|
||||
|
||||
export function links() {
|
||||
return [
|
||||
{ rel: "stylesheet", href: crossCheckingUploadStyles }
|
||||
{ rel: "stylesheet", href: crossCheckingUploadStyles },
|
||||
...dateRangePickerLinks()
|
||||
];
|
||||
}
|
||||
|
||||
@@ -39,6 +43,378 @@ const STEPS = [
|
||||
{ id: 3, label: "选择卷宗" }
|
||||
];
|
||||
|
||||
// 1. TreeNode类型和MOCK_TREE
|
||||
export interface TreeNode {
|
||||
label: string;
|
||||
value: string;
|
||||
children?: TreeNode[];
|
||||
}
|
||||
|
||||
// 无限层级组织架构数据结构
|
||||
const MOCK_TREE: TreeNode[] = [
|
||||
{
|
||||
label: "梅州市",
|
||||
value: "梅州市",
|
||||
children: [
|
||||
{
|
||||
label: "梅州市烟草局", // 市级局
|
||||
value: "梅州市烟草局",
|
||||
children: [
|
||||
{ label: "李局长", value: "梅州市-梅州市烟草局-李局长" },
|
||||
{ label: "王副局长", value: "梅州市-梅州市烟草局-王副局长" },
|
||||
{
|
||||
label: "市场监管科", // 市级局下的科室
|
||||
value: "梅州市烟草局-市场监管科",
|
||||
children: [
|
||||
{ label: "张科长", value: "梅州市-梅州市烟草局-市场监管科-张科长" },
|
||||
{ label: "陈主任", value: "梅州市-梅州市烟草局-市场监管科-陈主任" }
|
||||
]
|
||||
},
|
||||
{
|
||||
label: "法规科",
|
||||
value: "梅州市烟草局-法规科",
|
||||
children: [
|
||||
{ label: "刘科长", value: "梅州市-梅州市烟草局-法规科-刘科长" },
|
||||
{ label: "周专员", value: "梅州市-梅州市烟草局-法规科-周专员" }
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
label: "梅江区", // 区级
|
||||
value: "梅江区",
|
||||
children: [
|
||||
{
|
||||
label: "梅江区烟草分局", // 区级分局
|
||||
value: "梅江区烟草分局",
|
||||
children: [
|
||||
{ label: "张分局长", value: "梅州市-梅江区-梅江区烟草分局-张分局长" },
|
||||
{ label: "李副分局长", value: "梅州市-梅江区-梅江区烟草分局-李副分局长" },
|
||||
{
|
||||
label: "执法大队", // 分局下的大队
|
||||
value: "梅江区烟草分局-执法大队",
|
||||
children: [
|
||||
{ label: "王队长", value: "梅州市-梅江区-梅江区烟草分局-执法大队-王队长" },
|
||||
{ label: "陈副队长", value: "梅州市-梅江区-梅江区烟草分局-执法大队-陈副队长" },
|
||||
{
|
||||
label: "第一中队", // 大队下的中队
|
||||
value: "梅江区烟草分局-执法大队-第一中队",
|
||||
children: [
|
||||
{ label: "赵中队长", value: "梅州市-梅江区-梅江区烟草分局-执法大队-第一中队-赵中队长" },
|
||||
{ label: "孙执法员", value: "梅州市-梅江区-梅江区烟草分局-执法大队-第一中队-孙执法员" },
|
||||
{ label: "钱执法员", value: "梅州市-梅江区-梅江区烟草分局-执法大队-第一中队-钱执法员" }
|
||||
]
|
||||
},
|
||||
{
|
||||
label: "第二中队",
|
||||
value: "梅江区烟草分局-执法大队-第二中队",
|
||||
children: [
|
||||
{ label: "吴中队长", value: "梅州市-梅江区-梅江区烟草分局-执法大队-第二中队-吴中队长" },
|
||||
{ label: "郑执法员", value: "梅州市-梅江区-梅江区烟草分局-执法大队-第二中队-郑执法员" }
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
label: "办公室",
|
||||
value: "梅江区烟草分局-办公室",
|
||||
children: [
|
||||
{ label: "林主任", value: "梅州市-梅江区-梅江区烟草分局-办公室-林主任" },
|
||||
{ label: "黄秘书", value: "梅州市-梅江区-梅江区烟草分局-办公室-黄秘书" }
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
label: "梅江区市场监管局",
|
||||
value: "梅江区市场监管局",
|
||||
children: [
|
||||
{ label: "刘局长", value: "梅州市-梅江区-梅江区市场监管局-刘局长" },
|
||||
{ label: "周副局长", value: "梅州市-梅江区-梅江区市场监管局-周副局长" },
|
||||
{
|
||||
label: "执法监察科",
|
||||
value: "梅江区市场监管局-执法监察科",
|
||||
children: [
|
||||
{ label: "谢科长", value: "梅州市-梅江区-梅江区市场监管局-执法监察科-谢科长" },
|
||||
{ label: "何专员", value: "梅州市-梅江区-梅江区市场监管局-执法监察科-何专员" }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
label: "梅县区", // 另一个区
|
||||
value: "梅县区",
|
||||
children: [
|
||||
{
|
||||
label: "梅县区烟草分局",
|
||||
value: "梅县区烟草分局",
|
||||
children: [
|
||||
{ label: "黄分局长", value: "梅州市-梅县区-梅县区烟草分局-黄分局长" },
|
||||
{ label: "林副分局长", value: "梅州市-梅县区-梅县区烟草分局-林副分局长" },
|
||||
{
|
||||
label: "稽查队",
|
||||
value: "梅县区烟草分局-稽查队",
|
||||
children: [
|
||||
{ label: "吴队长", value: "梅州市-梅县区-梅县区烟草分局-稽查队-吴队长" },
|
||||
{ label: "郑稽查员", value: "梅州市-梅县区-梅县区烟草分局-稽查队-郑稽查员" },
|
||||
{ label: "谢稽查员", value: "梅州市-梅县区-梅县区烟草分局-稽查队-谢稽查员" }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
label: "丰顺县", // 县级
|
||||
value: "丰顺县",
|
||||
children: [
|
||||
{
|
||||
label: "丰顺县烟草分局",
|
||||
value: "丰顺县烟草分局",
|
||||
children: [
|
||||
{ label: "曾分局长", value: "梅州市-丰顺县-丰顺县烟草分局-曾分局长" },
|
||||
{
|
||||
label: "专卖管理所",
|
||||
value: "丰顺县烟草分局-专卖管理所",
|
||||
children: [
|
||||
{ label: "邓所长", value: "梅州市-丰顺县-丰顺县烟草分局-专卖管理所-邓所长" },
|
||||
{ label: "罗管理员", value: "梅州市-丰顺县-丰顺县烟草分局-专卖管理所-罗管理员" }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
label: "揭阳市",
|
||||
value: "揭阳市",
|
||||
children: [
|
||||
{
|
||||
label: "揭阳市烟草局", // 市级局
|
||||
value: "揭阳市烟草局",
|
||||
children: [
|
||||
{ label: "苏局长", value: "揭阳市-揭阳市烟草局-苏局长" },
|
||||
{ label: "叶副局长", value: "揭阳市-揭阳市烟草局-叶副局长" },
|
||||
{
|
||||
label: "专卖监督管理处",
|
||||
value: "揭阳市烟草局-专卖监督管理处",
|
||||
children: [
|
||||
{ label: "潘处长", value: "揭阳市-揭阳市烟草局-专卖监督管理处-潘处长" },
|
||||
{ label: "方副处长", value: "揭阳市-揭阳市烟草局-专卖监督管理处-方副处长" }
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
label: "榕城区",
|
||||
value: "榕城区",
|
||||
children: [
|
||||
{
|
||||
label: "榕城区烟草分局",
|
||||
value: "榕城区烟草分局",
|
||||
children: [
|
||||
{ label: "王分局长", value: "揭阳市-榕城区-榕城区烟草分局-王分局长" },
|
||||
{ label: "李明华", value: "揭阳市-榕城区-榕城区烟草分局-李明华" },
|
||||
{ label: "张丽萍", value: "揭阳市-榕城区-榕城区烟草分局-张丽萍" },
|
||||
{
|
||||
label: "市场检查组",
|
||||
value: "榕城区烟草分局-市场检查组",
|
||||
children: [
|
||||
{ label: "陈组长", value: "揭阳市-榕城区-榕城区烟草分局-市场检查组-陈组长" },
|
||||
{ label: "林检查员", value: "揭阳市-榕城区-榕城区烟草分局-市场检查组-林检查员" }
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
label: "榕城区质监局",
|
||||
value: "榕城区质监局",
|
||||
children: [
|
||||
{ label: "陈国强", value: "揭阳市-榕城区-榕城区质监局-陈国强" },
|
||||
{ label: "林小芳", value: "揭阳市-榕城区-榕城区质监局-林小芳" }
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
label: "揭东区",
|
||||
value: "揭东区",
|
||||
children: [
|
||||
{
|
||||
label: "揭东区烟草分局",
|
||||
value: "揭东区烟草分局",
|
||||
children: [
|
||||
{ label: "黄建军", value: "揭阳市-揭东区-揭东区烟草分局-黄建军" },
|
||||
{ label: "吴秀英", value: "揭阳市-揭东区-揭东区烟草分局-吴秀英" },
|
||||
{ label: "刘志华", value: "揭阳市-揭东区-揭东区烟草分局-刘志华" }
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
label: "惠来县", // 县级
|
||||
value: "惠来县",
|
||||
children: [
|
||||
{
|
||||
label: "惠来县烟草分局",
|
||||
value: "惠来县烟草分局",
|
||||
children: [
|
||||
{ label: "杨分局长", value: "揭阳市-惠来县-惠来县烟草分局-杨分局长" },
|
||||
{
|
||||
label: "案件审理室",
|
||||
value: "惠来县烟草分局-案件审理室",
|
||||
children: [
|
||||
{ label: "蔡主任", value: "揭阳市-惠来县-惠来县烟草分局-案件审理室-蔡主任" },
|
||||
{ label: "郭审理员", value: "揭阳市-惠来县-惠来县烟草分局-案件审理室-郭审理员" }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
label: "汕头市",
|
||||
value: "汕头市",
|
||||
children: [
|
||||
{
|
||||
label: "汕头市烟草局", // 市级局
|
||||
value: "汕头市烟草局",
|
||||
children: [
|
||||
{ label: "何局长", value: "汕头市-汕头市烟草局-何局长" },
|
||||
{ label: "许副局长", value: "汕头市-汕头市烟草局-许副局长" }
|
||||
]
|
||||
},
|
||||
{
|
||||
label: "龙湖区",
|
||||
value: "龙湖区",
|
||||
children: [
|
||||
{
|
||||
label: "龙湖区烟草分局",
|
||||
value: "龙湖区烟草分局",
|
||||
children: [
|
||||
{ label: "许志明", value: "汕头市-龙湖区-龙湖区烟草分局-许志明" },
|
||||
{ label: "蔡丽娜", value: "汕头市-龙湖区-龙湖区烟草分局-蔡丽娜" },
|
||||
{ label: "郭建华", value: "汕头市-龙湖区-龙湖区烟草分局-郭建华" },
|
||||
{ label: "何美霞", value: "汕头市-龙湖区-龙湖区烟草分局-何美霞" }
|
||||
]
|
||||
},
|
||||
{
|
||||
label: "龙湖区工商局",
|
||||
value: "龙湖区工商局",
|
||||
children: [
|
||||
{ label: "方国庆", value: "汕头市-龙湖区-龙湖区工商局-方国庆" },
|
||||
{ label: "杨小红", value: "汕头市-龙湖区-龙湖区工商局-杨小红" }
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
label: "金平区",
|
||||
value: "金平区",
|
||||
children: [
|
||||
{
|
||||
label: "金平区烟草分局",
|
||||
value: "金平区烟草分局",
|
||||
children: [
|
||||
{ label: "邓志强", value: "汕头市-金平区-金平区烟草分局-邓志强" },
|
||||
{ label: "罗美玲", value: "汕头市-金平区-金平区烟草分局-罗美玲" }
|
||||
]
|
||||
},
|
||||
{
|
||||
label: "金平区市场监管局",
|
||||
value: "金平区市场监管局",
|
||||
children: [
|
||||
{ label: "苏建国", value: "汕头市-金平区-金平区市场监管局-苏建国" },
|
||||
{ label: "叶丽华", value: "汕头市-金平区-金平区市场监管局-叶丽华" },
|
||||
{ label: "潘志明", value: "汕头市-金平区-金平区市场监管局-潘志明" }
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
label: "南澳县", // 县级
|
||||
value: "南澳县",
|
||||
children: [
|
||||
{
|
||||
label: "南澳县烟草分局",
|
||||
value: "南澳县烟草分局",
|
||||
children: [
|
||||
{ label: "陈分局长", value: "汕头市-南澳县-南澳县烟草分局-陈分局长" },
|
||||
{ label: "林管理员", value: "汕头市-南澳县-南澳县烟草分局-林管理员" }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
function isAllChildrenChecked(node: TreeNode, checked: string[]): boolean {
|
||||
if (!node.children || node.children.length === 0) return checked.includes(node.value);
|
||||
return node.children.every(child => isAllChildrenChecked(child, checked));
|
||||
}
|
||||
function isSomeChildrenChecked(node: TreeNode, checked: string[]): boolean {
|
||||
if (!node.children || node.children.length === 0) return checked.includes(node.value);
|
||||
return node.children.some(child => isSomeChildrenChecked(child, checked));
|
||||
}
|
||||
const TreeNodeCheckbox: React.FC<{
|
||||
node: TreeNode;
|
||||
checked: string[];
|
||||
onCheck: (node: TreeNode, checked: boolean) => void;
|
||||
level?: number;
|
||||
}> = ({ node, checked, onCheck, level = 0 }) => {
|
||||
const [expanded, setExpanded] = React.useState(true);
|
||||
const allChecked = isAllChildrenChecked(node, checked);
|
||||
const someChecked = isSomeChildrenChecked(node, checked);
|
||||
const isLeaf = !node.children || node.children.length === 0;
|
||||
return (
|
||||
<div style={{ marginLeft: level * 18 }}>
|
||||
<div className="flex items-center">
|
||||
{!isLeaf && (
|
||||
<span
|
||||
className="mr-1 cursor-pointer select-none"
|
||||
onClick={() => setExpanded(e => !e)}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onKeyDown={e => (e.key === 'Enter' || e.key === ' ') && setExpanded(e => !e)}
|
||||
style={{ width: 16, display: "inline-block", textAlign: "center" }}
|
||||
>
|
||||
<i className={expanded ? "ri-arrow-down-s-line" : "ri-arrow-right-s-line"}></i>
|
||||
</span>
|
||||
)}
|
||||
<input
|
||||
type="checkbox"
|
||||
className="form-checkbox"
|
||||
checked={allChecked}
|
||||
ref={el => { if (el) el.indeterminate = !allChecked && someChecked; }}
|
||||
onChange={e => onCheck(node, e.target.checked)}
|
||||
id={node.value}
|
||||
/>
|
||||
<label htmlFor={node.value} className="ml-2">{node.label}</label>
|
||||
</div>
|
||||
{expanded && node.children && (
|
||||
<div>
|
||||
{node.children.map(child => (
|
||||
<TreeNodeCheckbox
|
||||
key={child.value}
|
||||
node={child}
|
||||
checked={checked}
|
||||
onCheck={onCheck}
|
||||
level={level + 1}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export const action = async ({ request }: ActionFunctionArgs) => {
|
||||
const formData = await request.formData();
|
||||
const caseType = formData.get("caseType") as string;
|
||||
@@ -54,8 +430,16 @@ export const action = async ({ request }: ActionFunctionArgs) => {
|
||||
export default function CrossCheckingUpload() {
|
||||
// 基础状态
|
||||
const [caseType, setCaseType] = useState<CaseType>(CaseType.ADMINISTRATIVE_PENALTY);
|
||||
const [currentStep] = useState(1);
|
||||
const navigation = useNavigation();
|
||||
// 步骤状态
|
||||
const [currentStep, setCurrentStep] = useState(1);
|
||||
// 步骤1:任务信息
|
||||
const [taskInfo, setTaskInfo] = useState({
|
||||
name: '',
|
||||
date: '',
|
||||
type: '市局交叉评查',
|
||||
});
|
||||
// 步骤2状态
|
||||
const [groupChecked, setGroupChecked] = useState<string[]>([]);
|
||||
|
||||
// 上传配置状态 - 设置默认值
|
||||
const [priority] = useState<string>("normal");
|
||||
@@ -260,13 +644,11 @@ export default function CrossCheckingUpload() {
|
||||
if (failures.length === 0) {
|
||||
// 全部成功
|
||||
toastService.success(`成功上传 ${successes.length} 个文件`);
|
||||
// 立即清空文件列表
|
||||
clearAllFiles();
|
||||
messageService.success(`文件上传完成!成功上传 ${successes.length} 个文件,现在可以进行下一步操作。`, {
|
||||
title: '上传成功',
|
||||
confirmText: '确定',
|
||||
onConfirm: () => {
|
||||
// 清空文件列表
|
||||
clearAllFiles();
|
||||
}
|
||||
confirmText: '确定'
|
||||
});
|
||||
} else if (successes.length === 0) {
|
||||
// 全部失败
|
||||
@@ -299,221 +681,352 @@ export default function CrossCheckingUpload() {
|
||||
}
|
||||
};
|
||||
|
||||
// 步骤切换
|
||||
const handleNext = () => setCurrentStep((s) => Math.min(s + 1, 3));
|
||||
const handlePrev = () => setCurrentStep((s) => Math.max(s - 1, 1));
|
||||
|
||||
// 步骤1表单校验
|
||||
const canNextStep1 = taskInfo.name.trim() && taskInfo.date.trim() && taskInfo.type.trim();
|
||||
// 小组多选逻辑 - 默认不选择任何项
|
||||
|
||||
// 检查是否可以完成
|
||||
const canComplete = (singleFiles.length > 0 || multipleFiles.length > 0) && !isUploading;
|
||||
const navigation = useNavigation();
|
||||
const isSubmitting = navigation.state === "submitting";
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 py-8">
|
||||
<div className="max-w-6xl mx-auto px-4">
|
||||
{/* 步骤指示器 */}
|
||||
{/* 步骤指示器 */
|
||||
<div className="steps-indicator">
|
||||
{STEPS.map((step) => (
|
||||
<div
|
||||
key={step.id}
|
||||
<div
|
||||
key={step.id}
|
||||
className={`step-item ${step.id < currentStep ? 'completed' : ''}`}
|
||||
>
|
||||
<div className={`step-circle ${step.id <= currentStep ? 'active' : 'inactive'}`}>
|
||||
{step.id}
|
||||
</div>
|
||||
<div className={`step-circle ${step.id === currentStep ? 'active' : 'inactive'}`}>{step.id}</div>
|
||||
<div className="step-label">{step.label}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
}
|
||||
|
||||
{/* 案卷类型选择器 */}
|
||||
<div className="case-type-selector">
|
||||
<div className="case-type-options">
|
||||
<button
|
||||
type="button"
|
||||
className={`case-type-option ${caseType === CaseType.ADMINISTRATIVE_PENALTY ? 'active' : 'inactive'}`}
|
||||
onClick={() => handleCaseTypeChange(CaseType.ADMINISTRATIVE_PENALTY)}
|
||||
disabled={isUploading}
|
||||
>
|
||||
行政处罚
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`case-type-option ${caseType === CaseType.ADMINISTRATIVE_PERMIT ? 'active' : 'inactive'}`}
|
||||
onClick={() => handleCaseTypeChange(CaseType.ADMINISTRATIVE_PERMIT)}
|
||||
disabled={isUploading}
|
||||
>
|
||||
行政许可
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 文件上传区域 */}
|
||||
<Form method="post" encType="multipart/form-data">
|
||||
<input type="hidden" name="caseType" value={caseType} />
|
||||
<input type="hidden" name="uploadType" value={uploadType} />
|
||||
|
||||
<div className="upload-section">
|
||||
{/* 单案件导入 */}
|
||||
<div className="upload-item">
|
||||
<div className="upload-item-header">
|
||||
<i className="upload-item-icon ri-file-text-line"></i>
|
||||
<span>单案件导入</span>
|
||||
{uploadType === 'single' && singleFiles.length > 0 && (
|
||||
<Button
|
||||
type="default"
|
||||
size="small"
|
||||
icon="ri-delete-bin-line"
|
||||
onClick={() => handleClearFiles('single')}
|
||||
disabled={isUploading}
|
||||
>
|
||||
清空
|
||||
</Button>
|
||||
)}
|
||||
{/* 步骤1:创建任务 */}
|
||||
{currentStep === 1 && (
|
||||
<div className="flex justify-center">
|
||||
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6" style={{width: '1000px'}}>
|
||||
<div className="form-group">
|
||||
<label htmlFor="task-name">任务名称<span className="text-red-500">*</span></label>
|
||||
<input
|
||||
id="task-name"
|
||||
className="form-input"
|
||||
value={taskInfo.name}
|
||||
onChange={e => setTaskInfo({ ...taskInfo, name: e.target.value })}
|
||||
placeholder="请输入任务名称"
|
||||
/>
|
||||
</div>
|
||||
<UploadArea
|
||||
ref={singleUploadRef}
|
||||
onFilesSelected={handleSingleFilesSelected}
|
||||
className="custom-upload-area"
|
||||
accept=".pdf"
|
||||
multiple={true}
|
||||
icon="ri-file-upload-line"
|
||||
buttonText="选择文件"
|
||||
mainText="点击或拖拽文件到此区域上传"
|
||||
tipText={
|
||||
<div className="upload-tip-error">
|
||||
请上传案件相关PDF文件
|
||||
</div>
|
||||
}
|
||||
disabled={uploadType === 'multiple' || isUploading}
|
||||
/>
|
||||
|
||||
{/* 单案件文件列表 */}
|
||||
{singleFiles.length > 0 && (
|
||||
<div className="mt-4 space-y-2">
|
||||
<div className="text-sm font-medium text-gray-700">
|
||||
已选择 {singleFiles.length} 个文件:
|
||||
</div>
|
||||
<div className="max-h-32 overflow-y-auto space-y-1">
|
||||
{singleFiles.map((file) => (
|
||||
<div key={file.id} className="flex items-center justify-between bg-gray-50 p-2 rounded">
|
||||
<div className="flex items-center space-x-2 flex-1 min-w-0">
|
||||
<i className="ri-file-pdf-line text-red-500"></i>
|
||||
<span className="text-sm truncate">{file.name}</span>
|
||||
<span className="text-xs text-gray-500">{formatFileSize(file.size)}</span>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleRemoveFile(file.id, 'single')}
|
||||
className="text-red-500 hover:text-red-700 p-1"
|
||||
disabled={isUploading}
|
||||
>
|
||||
<i className="ri-close-line"></i>
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 多案件导入 */}
|
||||
<div className="upload-item">
|
||||
<div className="upload-item-header">
|
||||
<i className="upload-item-icon ri-file-list-line"></i>
|
||||
<span>多案件导入</span>
|
||||
{uploadType === 'multiple' && multipleFiles.length > 0 && (
|
||||
<Button
|
||||
type="default"
|
||||
size="small"
|
||||
icon="ri-delete-bin-line"
|
||||
onClick={() => handleClearFiles('multiple')}
|
||||
disabled={isUploading}
|
||||
>
|
||||
清空
|
||||
</Button>
|
||||
)}
|
||||
<div className="form-group">
|
||||
<label htmlFor="task-date" className="form-label required">评查时间</label>
|
||||
<SingleDatePicker
|
||||
date={taskInfo.date}
|
||||
onDateChange={(value) => setTaskInfo({ ...taskInfo, date: value })}
|
||||
className="w-full"
|
||||
id="task-date"
|
||||
placeholder="请选择日期"
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label htmlFor="task-type">任务类型</label>
|
||||
<input
|
||||
id="task-type"
|
||||
className="form-input"
|
||||
value={taskInfo.type}
|
||||
onChange={e => setTaskInfo({ ...taskInfo, type: e.target.value })}
|
||||
placeholder="请输入任务类型"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex justify-between items-center mt-6">
|
||||
<Button
|
||||
type="default"
|
||||
icon="ri-arrow-left-line"
|
||||
onClick={() => navigate('/cross-checking')}
|
||||
>
|
||||
返回列表
|
||||
</Button>
|
||||
<Button type="primary" disabled={!canNextStep1} onClick={handleNext}>下一步</Button>
|
||||
</div>
|
||||
<UploadArea
|
||||
ref={multipleUploadRef}
|
||||
onFilesSelected={handleMultipleFilesSelected}
|
||||
className="custom-upload-area"
|
||||
accept=".zip,.rar,.7z,.tar"
|
||||
multiple={false}
|
||||
icon="ri-folder-zip-line"
|
||||
buttonText="选择文件"
|
||||
mainText="点击或拖拽文件到此区域上传"
|
||||
tipText={
|
||||
<div className="upload-tip-error">
|
||||
请上传多个案件作为压缩包zip、rar、7z、tar文件
|
||||
</div>
|
||||
}
|
||||
disabled={uploadType === 'single' || isUploading}
|
||||
/>
|
||||
|
||||
{/* 多案件文件列表 */}
|
||||
{multipleFiles.length > 0 && (
|
||||
<div className="mt-4 space-y-2">
|
||||
<div className="text-sm font-medium text-gray-700">
|
||||
已选择 {multipleFiles.length} 个压缩包:
|
||||
</div>
|
||||
<div className="max-h-32 overflow-y-auto space-y-1">
|
||||
{multipleFiles.map((file) => (
|
||||
<div key={file.id} className="flex items-center justify-between bg-gray-50 p-2 rounded">
|
||||
<div className="flex items-center space-x-2 flex-1 min-w-0">
|
||||
<i className="ri-folder-zip-line text-orange-500"></i>
|
||||
<span className="text-sm truncate">{file.name}</span>
|
||||
<span className="text-xs text-gray-500">{formatFileSize(file.size)}</span>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleRemoveFile(file.id, 'multiple')}
|
||||
className="text-red-500 hover:text-red-700 p-1"
|
||||
disabled={isUploading}
|
||||
>
|
||||
<i className="ri-close-line"></i>
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 完成按钮 */}
|
||||
<div className="complete-button-container">
|
||||
<button
|
||||
type="button"
|
||||
className="complete-button"
|
||||
disabled={!canComplete}
|
||||
onClick={handleCompleteUpload}
|
||||
>
|
||||
{isUploading || isSubmitting ? (
|
||||
<>
|
||||
<i className="ri-loader-4-line animate-spin mr-2"></i>
|
||||
上传中...
|
||||
</>
|
||||
) : (
|
||||
"完成"
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</Form>
|
||||
|
||||
{/* 文件选择状态提示 */}
|
||||
{!canComplete && !isUploading && (
|
||||
<div className="text-center mt-4 text-gray-500 text-sm">
|
||||
请至少选择一种导入方式的文件
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 上传进度提示 */}
|
||||
{isUploading && (
|
||||
<div className="text-center mt-4">
|
||||
<div className="bg-blue-50 p-4 rounded-md border border-blue-100">
|
||||
<div className="flex items-center justify-center text-blue-800 mb-2">
|
||||
<i className="ri-loader-4-line animate-spin text-xl mr-2"></i>
|
||||
<span className="font-medium">正在上传文件...</span>
|
||||
{/* 步骤2:创建评查小组 */}
|
||||
{currentStep === 2 && (
|
||||
<>
|
||||
<div className="flex justify-center">
|
||||
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6" style={{width: '1000px'}}>
|
||||
<div className="flex flex-row justify-center gap-12">
|
||||
{/* 左侧树状多选 */}
|
||||
<div style={{ minWidth: 300, width: '40%' }}>
|
||||
<div className="form-group">
|
||||
<label htmlFor="review-group" className="form-label required">评查小组</label>
|
||||
<MultiCascader
|
||||
options={MOCK_TREE}
|
||||
placeholder="请选择评查小组"
|
||||
value={groupChecked}
|
||||
onChange={(values: string[]) => {
|
||||
setGroupChecked(values);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/* 右侧已选择成员显示区域 */}
|
||||
<div style={{ minWidth: 400, width: '50%', minHeight: 250, background: '#f9fafb', border: '1.5px solid #e5e7eb', borderRadius: 8, marginTop: '23px' }}>
|
||||
<div className="p-4">
|
||||
<h4 className="text-sm font-medium text-gray-700 mb-3">已选择的评查小组成员</h4>
|
||||
{groupChecked.length > 0 ? (
|
||||
<div className="space-y-2 max-h-64 overflow-y-auto">
|
||||
{groupChecked.map((member, index) => {
|
||||
const parts = member.split('-');
|
||||
const name = parts[parts.length - 1];
|
||||
const org = parts.slice(0, -1).join(' - ');
|
||||
return (
|
||||
<div key={index} className="bg-white p-2 rounded text-xs border">
|
||||
<div className="font-medium text-gray-800">{name}</div>
|
||||
<div className="text-gray-500 mt-1">{org}</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-gray-400 text-sm text-center mt-8">
|
||||
<i className="ri-user-line text-2xl mb-2 block"></i>
|
||||
暂未选择评查小组成员
|
||||
</div>
|
||||
)}
|
||||
<div className="mt-4 pt-3 border-t border-gray-200">
|
||||
<div className="text-xs text-gray-500">
|
||||
共选择 {groupChecked.length} 名成员
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 按钮区域移到卡片内部 */}
|
||||
<div className="flex justify-between items-center mt-6">
|
||||
<Button
|
||||
type="default"
|
||||
icon="ri-arrow-left-line"
|
||||
onClick={() => navigate('/cross-checking')}
|
||||
>
|
||||
返回列表
|
||||
</Button>
|
||||
<div className="flex space-x-4">
|
||||
<Button type="default" onClick={handlePrev}>上一步</Button>
|
||||
<Button type="primary" disabled={groupChecked.length === 0} onClick={handleNext}>下一步</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-sm text-blue-700">
|
||||
正在上传 {uploadType === 'single' ? singleFiles.length : multipleFiles.length} 个文件,请稍候
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* 步骤3:原有上传区域 */}
|
||||
{currentStep === 3 && (
|
||||
<div className="flex justify-center">
|
||||
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6" style={{width: '1000px'}}>
|
||||
{/* 案卷类型选择器 */}
|
||||
<div className="flex justify-center mb-6">
|
||||
<div>
|
||||
<div className="text-sm font-medium text-gray-700 mb-3 text-center">选择案卷类型</div>
|
||||
<div className="case-type-options">
|
||||
<button
|
||||
type="button"
|
||||
className={`case-type-option ${caseType === CaseType.ADMINISTRATIVE_PENALTY ? 'active' : 'inactive'}`}
|
||||
onClick={() => handleCaseTypeChange(CaseType.ADMINISTRATIVE_PENALTY)}
|
||||
disabled={isUploading}
|
||||
>
|
||||
行政处罚
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`case-type-option ${caseType === CaseType.ADMINISTRATIVE_PERMIT ? 'active' : 'inactive'}`}
|
||||
onClick={() => handleCaseTypeChange(CaseType.ADMINISTRATIVE_PERMIT)}
|
||||
disabled={isUploading}
|
||||
>
|
||||
行政许可
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 文件上传区域 */}
|
||||
<Form method="post" encType="multipart/form-data">
|
||||
<input type="hidden" name="caseType" value={caseType} />
|
||||
<input type="hidden" name="uploadType" value={uploadType} />
|
||||
|
||||
{/* 上传框区域 */}
|
||||
<div className="upload-section">
|
||||
{/* 单案件导入 */}
|
||||
<div className="upload-item">
|
||||
<div className="upload-item-header">
|
||||
<i className="upload-item-icon ri-file-text-line"></i>
|
||||
<span>单案件导入</span>
|
||||
</div>
|
||||
<UploadArea
|
||||
ref={singleUploadRef}
|
||||
onFilesSelected={handleSingleFilesSelected}
|
||||
className="custom-upload-area"
|
||||
accept=".pdf"
|
||||
multiple={true}
|
||||
icon="ri-file-upload-line"
|
||||
buttonText="选择文件"
|
||||
mainText="点击或拖拽文件到此区域上传"
|
||||
tipText={
|
||||
<div className="upload-tip-error">
|
||||
请上传案件相关PDF文件
|
||||
</div>
|
||||
}
|
||||
disabled={uploadType === 'multiple' || isUploading}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 多案件导入 */}
|
||||
<div className="upload-item">
|
||||
<div className="upload-item-header">
|
||||
<i className="upload-item-icon ri-file-list-line"></i>
|
||||
<span>多案件导入</span>
|
||||
</div>
|
||||
<UploadArea
|
||||
ref={multipleUploadRef}
|
||||
onFilesSelected={handleMultipleFilesSelected}
|
||||
className="custom-upload-area"
|
||||
accept=".zip,.rar,.7z,.tar"
|
||||
multiple={false}
|
||||
icon="ri-folder-zip-line"
|
||||
buttonText="选择文件"
|
||||
mainText="点击或拖拽文件到此区域上传"
|
||||
tipText={
|
||||
<div className="upload-tip-error">
|
||||
请上传多个案件作为压缩包zip、rar、7z、tar文件
|
||||
</div>
|
||||
}
|
||||
disabled={uploadType === 'single' || isUploading}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 文件预览区域 */}
|
||||
{(singleFiles.length > 0 || multipleFiles.length > 0) && (
|
||||
<div className="mt-6 bg-green-50 border border-green-200 rounded-lg p-4">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div className="text-sm font-medium text-gray-700">
|
||||
已选择 {uploadType === 'single' ? singleFiles.length : multipleFiles.length} 个文件
|
||||
</div>
|
||||
<Button
|
||||
type="default"
|
||||
size="small"
|
||||
icon="ri-delete-bin-line"
|
||||
onClick={() => handleClearFiles(uploadType === 'single' ? 'single' : 'multiple')}
|
||||
disabled={isUploading}
|
||||
>
|
||||
清空
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* 单案件文件列表 */}
|
||||
{uploadType === 'single' && singleFiles.length > 0 && (
|
||||
<div className="max-h-32 overflow-y-auto space-y-1">
|
||||
{singleFiles.map((file) => (
|
||||
<div key={file.id} className="flex items-center justify-between bg-white p-2 rounded border">
|
||||
<div className="flex items-center space-x-2 flex-1 min-w-0">
|
||||
<i className="ri-file-pdf-line text-red-500"></i>
|
||||
<span className="text-sm truncate">{file.name}</span>
|
||||
<span className="text-xs text-gray-500">{formatFileSize(file.size)}</span>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleRemoveFile(file.id, 'single')}
|
||||
className="text-red-500 hover:text-red-700 p-1"
|
||||
disabled={isUploading}
|
||||
>
|
||||
<i className="ri-close-line"></i>
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 多案件文件列表 */}
|
||||
{uploadType === 'multiple' && multipleFiles.length > 0 && (
|
||||
<div className="max-h-32 overflow-y-auto space-y-1">
|
||||
{multipleFiles.map((file) => (
|
||||
<div key={file.id} className="flex items-center justify-between bg-white p-2 rounded border">
|
||||
<div className="flex items-center space-x-2 flex-1 min-w-0">
|
||||
<i className="ri-folder-zip-line text-orange-500"></i>
|
||||
<span className="text-sm truncate">{file.name}</span>
|
||||
<span className="text-xs text-gray-500">{formatFileSize(file.size)}</span>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleRemoveFile(file.id, 'multiple')}
|
||||
className="text-red-500 hover:text-red-700 p-1"
|
||||
disabled={isUploading}
|
||||
>
|
||||
<i className="ri-close-line"></i>
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 完成按钮 */}
|
||||
<div className="flex justify-between items-center mt-8">
|
||||
<Button
|
||||
type="default"
|
||||
icon="ri-arrow-left-line"
|
||||
onClick={() => navigate('/cross-checking')}
|
||||
>
|
||||
返回列表
|
||||
</Button>
|
||||
<div className="flex space-x-4">
|
||||
<Button type="default" onClick={handlePrev}>上一步</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
disabled={!canComplete || isUploading}
|
||||
onClick={handleCompleteUpload}
|
||||
>
|
||||
{isUploading || isSubmitting ? "上传中..." : "开始创建任务"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Form>
|
||||
|
||||
{/* 文件选择状态提示 */}
|
||||
{!canComplete && !isUploading && (
|
||||
<div className="text-center mt-4 text-gray-500 text-sm">
|
||||
请至少选择一种导入方式的文件
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 上传进度提示 */}
|
||||
{isUploading && (
|
||||
<div className="text-center mt-4">
|
||||
<div className="bg-blue-50 p-4 rounded-md border border-blue-100">
|
||||
<div className="flex items-center justify-center text-blue-800 mb-2">
|
||||
<i className="ri-loader-4-line ri-spin animate-spin text-xl mr-2"></i>
|
||||
<span className="font-medium">正在上传文件...</span>
|
||||
</div>
|
||||
<p className="text-sm text-blue-700">
|
||||
正在上传 {uploadType === 'single' ? singleFiles.length : multipleFiles.length} 个文件,请稍候
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
+2
-2
@@ -45,7 +45,7 @@
|
||||
--color-primary: #00684a;
|
||||
--color-primary-hover: #005a3f;
|
||||
--color-primary-light: rgba(0, 104, 74, 0.1);
|
||||
|
||||
|
||||
/* 成功、警告、错误颜色 */
|
||||
--color-success: #52c41a;
|
||||
--color-warning: #faad14;
|
||||
@@ -296,4 +296,4 @@ i[class^="ri-"],
|
||||
i[class*=" ri-"] {
|
||||
font-family: 'remixicon' !important;
|
||||
font-style: normal !important;
|
||||
}
|
||||
}
|
||||
@@ -25,8 +25,8 @@
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 25px;
|
||||
left: 75px;
|
||||
width: calc(100% - 50px);
|
||||
left: 135px;
|
||||
width: calc(100% - 70px);
|
||||
height: 2px;
|
||||
background-color: #e5e7eb;
|
||||
z-index: 1;
|
||||
@@ -272,4 +272,72 @@
|
||||
left: 60px;
|
||||
width: calc(100% - 20px);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.form-input[type="date"]::-webkit-calendar-picker-indicator {
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
.form-input[type="date"] {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.group-multiselect {
|
||||
width: 100%;
|
||||
max-width: 350px;
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.04);
|
||||
padding: 0.5rem 1.5rem 1rem 1.5rem;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
.group-select-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
margin-bottom: 0.5rem;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
.group-select-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
.group-select-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 1rem;
|
||||
margin-left: 0.5rem;
|
||||
user-select: none;
|
||||
}
|
||||
.group-select-item input[type="checkbox"] {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
accent-color: #059669;
|
||||
border-radius: 4px;
|
||||
margin-right: 0.5rem;
|
||||
border: 1.5px solid #d1d5db;
|
||||
transition: border 0.2s;
|
||||
}
|
||||
.group-select-item input[type="checkbox"]:hover {
|
||||
border: 1.5px solid #059669;
|
||||
}
|
||||
.group-select-item label {
|
||||
font-size: 1rem;
|
||||
color: #374151;
|
||||
cursor: pointer;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
@media (max-width: 600px) {
|
||||
.group-multiselect {
|
||||
max-width: 100%;
|
||||
padding: 0.5rem 0.5rem 1rem 0.5rem;
|
||||
}
|
||||
.group-select-item label {
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user