初版编写交叉评查页面,增加非范围日期选择器

This commit is contained in:
2025-07-16 19:01:13 +08:00
parent 328f326db3
commit 277c54f34d
4 changed files with 687 additions and 174 deletions
+83
View File
@@ -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;
+123 -1
View File
@@ -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>
);
}
}
+413 -173
View File
@@ -1,4 +1,4 @@
import { useState, useRef } from "react";
import { useState, useRef, useEffect } from "react";
import { type MetaFunction, type ActionFunctionArgs } from "@remix-run/node";
import { Form, useNavigation } from "@remix-run/react";
import { UploadArea, type UploadAreaRef } from "~/components/ui/UploadArea";
@@ -6,6 +6,8 @@ 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,152 @@ 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: "梅江区烟草局",
children: [
{ label: "张三", value: "梅州市-梅江区-梅江区烟草局-张三" },
{ label: "李四", value: "梅州市-梅江区-梅江区烟草局-李四" }
]
}
]
}
]
},
{
label: "揭阳市",
value: "揭阳市",
children: [
{
label: "榕城区",
value: "榕城区",
children: [
{
label: "榕城区烟草局",
value: "榕城区烟草局",
children: [
{ label: "王五", value: "揭阳市-榕城区-榕城区烟草局-王五" }
]
}
]
}
]
}
];
// 2. TreeMultiSelect递归组件
function getAllLeafValues(node: TreeNode): string[] {
if (!node.children || node.children.length === 0) return [node.value];
return node.children.flatMap(getAllLeafValues);
}
function getAllLeafValuesFromTree(tree: TreeNode[]): string[] {
return tree.flatMap(getAllLeafValues);
}
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" }}
>
{expanded ? "▼" : "▶"}
</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>
);
};
const TreeMultiSelect: React.FC<{
treeData: TreeNode[];
value: string[];
onChange: (v: string[]) => void;
}> = ({ treeData, value, onChange }) => {
// 递归处理选中/取消
const handleCheck = (node: TreeNode, checked: boolean) => {
const leafValues = getAllLeafValues(node);
let newValue: string[];
if (checked) {
newValue = Array.from(new Set([...value, ...leafValues]));
} else {
newValue = value.filter(v => !leafValues.includes(v));
}
onChange(newValue);
};
return (
<div>
{treeData.map(node => (
<TreeNodeCheckbox
key={node.value}
node={node}
checked={value}
onCheck={handleCheck}
/>
))}
</div>
);
};
export const action = async ({ request }: ActionFunctionArgs) => {
const formData = await request.formData();
const caseType = formData.get("caseType") as string;
@@ -54,8 +204,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");
@@ -299,8 +457,20 @@ 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();
// 小组多选逻辑
useEffect(() => {
setGroupChecked(getAllLeafValuesFromTree(MOCK_TREE));
}, []);
// 检查是否可以完成
const canComplete = (singleFiles.length > 0 || multipleFiles.length > 0) && !isUploading;
const navigation = useNavigation();
const isSubmitting = navigation.state === "submitting";
return (
@@ -309,13 +479,11 @@ export default function CrossCheckingUpload() {
{/* 步骤指示器 */}
<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>
))}
@@ -343,179 +511,251 @@ export default function CrossCheckingUpload() {
</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>
)}
</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}
{/* 步骤1:创建任务 */}
{currentStep === 1 && (
<div className="step-form-container">
<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="请输入任务名称"
/>
{/* 单案件文件列表 */}
{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>
<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">
ziprar7ztar文件
</div>
}
disabled={uploadType === 'single' || isUploading}
<div className="form-group">
<label className="form-label required"></label>
<SingleDatePicker
date={taskInfo.date}
onDateChange={(value) => setTaskInfo({ ...taskInfo, date: value })}
className="w-full"
id="task-date"
placeholder="请选择日期"
/>
{/* 多案件文件列表 */}
{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 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-center mt-8">
<Button type="primary" disabled={!canNextStep1} onClick={handleNext}></Button>
</div>
</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="step-form-container flex flex-row justify-center items-start gap-12">
{/* 左侧树状多选 */}
<div style={{ minWidth: 260 }}>
<div className="form-group">
<label className="form-label required"></label>
<MultiCascader
options={MOCK_TREE}
placeholder="请选择评查小组"
defaultValue={groupChecked}
onChange={(values: string[]) => {
setGroupChecked(values);
}}
/>
</div>
</div>
<p className="text-sm text-blue-700">
{uploadType === 'single' ? singleFiles.length : multipleFiles.length}
</p>
{/* 右侧预留区域 */}
<div style={{ minWidth: 320, minHeight: 200, background: '#fff', border: '1.5px solid #e5e7eb', borderRadius: 8 }}></div>
</div>
</div>
<div className="flex justify-center mt-8 space-x-4">
<Button type="default" onClick={handlePrev}></Button>
<Button type="primary" disabled={groupChecked.length === 0} onClick={handleNext}></Button>
</div>
</>
)}
{/* 步骤3:原有上传区域 */}
{currentStep === 3 && (
<>
{/* 文件上传区域 */}
<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>
)}
</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>
<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">
ziprar7ztar文件
</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 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>
@@ -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;
}
}