diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fdd5663..713ccee 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -92,6 +92,9 @@ importers: uuid: specifier: ^11.0.3 version: 11.0.3 + zod: + specifier: ^3.24.1 + version: 3.24.1 devDependencies: '@types/react': specifier: ^18.2.15 diff --git a/src/components/ProjectList/index.jsx b/src/components/ProjectList/index.jsx new file mode 100644 index 0000000..0d07330 --- /dev/null +++ b/src/components/ProjectList/index.jsx @@ -0,0 +1,615 @@ +import React, { useState, useEffect } from 'react'; +import { + Form, + Input, + Button, + Card, + Typography, + Modal, + message, + Select, + Divider, + Upload, +} from 'antd'; +import { + PlusOutlined, + DeleteOutlined, + EditOutlined, + CheckOutlined, + CloseOutlined, + InboxOutlined, + UploadOutlined, +} from '@ant-design/icons'; +import { v4 as uuidv4 } from 'uuid'; +import { supabase } from "@/config/supabase"; +import { supabaseService } from "@/hooks/supabaseService"; + +const { Text } = Typography; + +const ProjectResourceList = ({ form, isView, formValues, type = "project" }) => { + const [editingSectionIndex, setEditingSectionIndex] = useState(null); + const [editingSectionName, setEditingSectionName] = useState(""); + const [templateModalVisible, setTemplateModalVisible] = useState(false); + const [availableSections, setAvailableSections] = useState([]); + const [loading, setLoading] = useState(false); + const [resourceTypes, setResourceTypes] = useState([]); + const [loadingResourceTypes, setLoadingResourceTypes] = useState(false); + const [units, setUnits] = useState([]); + const [loadingUnits, setLoadingUnits] = useState(false); + const [uploadModalVisible, setUploadModalVisible] = useState(false); + const [currentAddItem, setCurrentAddItem] = useState(null); + + const fetchAvailableSections = async () => { + try { + setLoading(true); + const { data: sections, error } = await supabase + .from("resources") + .select("*") + .eq("type", "sections") + .eq("attributes->>template_type", type) + .order("created_at", { ascending: false }); + + if (error) throw error; + setAvailableSections(sections || []); + } catch (error) { + message.error("获取资源模板失败"); + console.error(error); + } finally { + setLoading(false); + } + }; + + const fetchResourceTypes = async () => { + setLoadingResourceTypes(true); + try { + const { data: types } = await supabaseService.select("resources", { + filter: { + type: { eq: "resource_types" }, + "attributes->>template_type": { in: `(${type})` }, + }, + order: { + column: "created_at", + ascending: false, + }, + }); + setResourceTypes(types || []); + } catch (error) { + console.error('获取资源类型失败:', error); + } finally { + setLoadingResourceTypes(false); + } + }; + + const handleSectionNameEdit = (sectionIndex, initialValue) => { + setEditingSectionIndex(sectionIndex); + setEditingSectionName(initialValue || ""); + }; + + const handleSectionNameSave = () => { + if (!editingSectionName.trim()) { + message.error("请输入名称"); + return; + } + + const sections = form.getFieldValue("sections"); + const newSections = [...sections]; + newSections[editingSectionIndex] = { + ...newSections[editingSectionIndex], + sectionName: editingSectionName.trim(), + }; + form.setFieldValue("sections", newSections); + setEditingSectionIndex(null); + setEditingSectionName(""); + }; + const handleAddUnit = async (unitName) => { + try { + const { error } = await supabase.from("resources").insert([ + { + type: "units", + attributes: { + name: unitName, + template_type: type, + }, + schema_version: 1, + }, + ]); + + if (error) throw error; + message.success("新增资源类型成功"); + fetchUnits(); + return true; + } catch (error) { + message.error("新增资源类型失败"); + console.error(error); + return false; + } + }; + + const handleUseTemplate = (template, add) => { + const newSection = { + key: uuidv4(), + sectionName: template.attributes.name, + items: (template.attributes.items || []).map((item) => ({ + key: uuidv4(), + title: item.title || "", + unit: item.unit || "", + url: item.url || "", + description: item.description || "", + })), + }; + add(newSection); + setTemplateModalVisible(false); + message.success("套用模版成功"); + }; + + const handleCreateCustom = (add, fieldsLength) => { + add({ + key: uuidv4(), + sectionName: `资源分组 ${fieldsLength + 1}`, + items: [ + { + key: uuidv4(), + title: "", + unit: "", + url: "", + description: "", + }, + ], + }); + setTemplateModalVisible(false); + }; + + const handleAddItem = (addItem) => { + addItem({ + key: uuidv4(), + title: "", + unit: "", + url: "", + description: "", + }); + }; + + const handleFileUpload = async (file, addItem) => { + try { + const fileExt = file.name.split('.').pop(); + const fileName = `${Date.now()}.${fileExt}`; + const filePath = `${type}/${fileName}`; + + const { data, error } = await supabase.storage + .from('file') + .upload(filePath, file); + + if (error) throw error; + + const { data: { publicUrl } } = supabase.storage + .from('file') + .getPublicUrl(filePath); + + let fileType = ''; + if (file.type.startsWith('image/')) { + fileType = '图片'; + } else if (file.type.startsWith('video/')) { + fileType = '视频'; + } else if (file.type.includes('pdf')) { + fileType = 'PDF文档'; + } else { + fileType = '其他文件'; + } + + addItem({ + key: uuidv4(), + url: publicUrl, + title: `${fileType}-${file.name}`, + unit: fileType, + description: '', + }); + + return true; + } catch (error) { + message.error('文件上传失败'); + console.error(error); + return false; + } + }; + + const handleButtonClick = (addItem) => { + setCurrentAddItem(addItem); + setUploadModalVisible(true); + }; + + const renderUploadModal = () => ( + setUploadModalVisible(false)} + footer={null} + > + { + try { + const result = await handleFileUpload(file, currentAddItem); + if (result) { + onSuccess(); + } else { + onError(); + } + } catch (err) { + onError(); + } + }} + onChange={(info) => { + if (info.file.status === 'done') { + message.success(`${info.file.name} 上传成功`); + } else if (info.file.status === 'error') { + message.error(`${info.file.name} 上传失败`); + } + }} + > +

+ +

+

点击或拖拽文件到此区域上传

+

支持单个或批量上传

+
+
+ ); + + const fetchUnits = async () => { + setLoadingUnits(true); + try { + const { data: units } = await supabaseService.select("resources", { + filter: { + type: { eq: "units" }, + "attributes->>template_type": { in: `(${type})` }, + }, + order: { + column: "created_at", + ascending: false, + }, + }); + setUnits(units || []); + } catch (error) { + message.error("获取状态列表失败"); + console.error(error); + } finally { + setLoadingUnits(false); + } + }; +useEffect(()=>{ + fetchUnits() +},[]) + +const addSectionFn = async (field) => { + try { + const sections = form.getFieldValue("sections"); + const currentSection = sections[field.name]; + + const { error } = await supabase.from("resources").insert([ + { + type: "sections", + attributes: { + name: currentSection.sectionName, + template_type: type, + items: currentSection.items.map(item => ({ + title: item.title, + unit: item.unit, + url: item.url, + description: item.description + })) + }, + schema_version: 1 + } + ]); + + if (error) throw error; + message.success("保存模板成功"); + // 刷新可用模板列表 + fetchAvailableSections(); + } catch (error) { + message.error("保存模板失败"); + console.error(error); + } +}; + + return ( + <> + + {(fields, { add, remove }) => ( + <> +
+ {fields.map((field, sectionIndex) => ( + +
+ {editingSectionIndex === sectionIndex ? ( +
+ setEditingSectionName(e.target.value)} + onPressEnter={handleSectionNameSave} + autoFocus + className="w-48" + /> +
+ ) : ( +
+ + + {form.getFieldValue([ + "sections", + sectionIndex, + "sectionName", + ]) || `资源分组 ${sectionIndex + 1}`} + + {!isView && ( +
+ )} +
+ {!isView && ( +
+ +
+ )} +
+ } + > + + {(itemFields, { add: addItem, remove: removeItem }) => ( + <> +
+
资源链接
+
资源标题
+
资源类型
+
资源描述
+
操作
+
+ + {itemFields.map((itemField) => { + const { key, ...restItemField } = itemField; + return ( +
+ + + + + + + + {/* 资源类型 */} + + + + + {!isView && itemFields.length > 1 && ( +
+ ); + })} + + {!isView && ( +
+ + +
+ )} + + )} +
+ + ))} + + + {/* 添加资源分组按钮 */} +
+ {!isView && ( +
+ +
+ )} +
+ + {/* 模板选择弹窗 */} + 选择模版} + open={templateModalVisible} + onCancel={() => setTemplateModalVisible(false)} + footer={null} + width={800} + closeIcon={} + > +
+ {availableSections.map((section) => ( + handleUseTemplate(section, add)} + className="cursor-pointer" + > + + + ))} + handleCreateCustom(add, fields.length)} + className="cursor-pointer" + > +
+ + 创建自定义分组 +
+
+
+
+ + {renderUploadModal()} + + )} +
+ + ); +}; + +export default ProjectResourceList; \ No newline at end of file diff --git a/src/hooks/resource/useResource.js b/src/hooks/resource/useResource.js index 6716d7d..5ba1c5e 100644 --- a/src/hooks/resource/useResource.js +++ b/src/hooks/resource/useResource.js @@ -24,14 +24,16 @@ export const useResources = (initialPagination, initialSorter, type) => { setCurrentPagination(newPagination); setCurrentSorter(newSorter); - + console.log(params.searchQuery,'params.searchQuery'); + const { data, total: newTotal } = await resourceService.getResources({ page: newPagination.current, pageSize: newPagination.pageSize, orderBy: newSorter.field, ascending: newSorter.order === "ascend", type: type, - ...(params?.search !== "" ? { searchQuery: params.search } : {}), + ...(params?.search ? { searchQuery: params.search } : {}), + ...(params?.searchQuery ? {searchQuery:params.searchQuery} : {}), }); setResources(data || []); diff --git a/src/pages/company/project/detail/index.jsx b/src/pages/company/project/detail/index.jsx index e629f6a..ce35943 100644 --- a/src/pages/company/project/detail/index.jsx +++ b/src/pages/company/project/detail/index.jsx @@ -1,7 +1,437 @@ -import React from 'react' +import React, { useState, useEffect } from 'react'; +import { useParams, useSearchParams, useNavigate } from 'react-router-dom'; +import { supabase } from "@/config/supabase"; +import { supabaseService } from "@/hooks/supabaseService"; +import { + Card, + Form, + Input, + Button, + Space, + message, + Select, + DatePicker, + Typography, + Spin +} from 'antd'; +import { + ArrowLeftOutlined, + SaveOutlined +} from '@ant-design/icons'; +import dayjs from 'dayjs'; +import { v4 as uuidv4 } from 'uuid'; +import ProjectResourceList from '@/components/ProjectList'; +const { Title } = Typography; +const { TextArea } = Input; +const TYPE = 'project'; export default function ProjectDetail() { + const { id } = useParams(); + const [searchParams] = useSearchParams(); + const isEdit = searchParams.get("edit") === "true"; + const templateId = searchParams.get("templateId"); + const isView = id && !isEdit; + const [form] = Form.useForm(); + const navigate = useNavigate(); + const [loading, setLoading] = useState(false); + const [customers, setCustomers] = useState([]); + const [formValues, setFormValues] = useState({}); + + const [tasks, setTasks] = useState([]); + const [loadingTasks, setLoadingTasks] = useState(false); + + const initialValues = { + sections: [ + { + uniqueId: uuidv4(), + sectionName: "资源分组 1", + items: [ + { + uniqueId: uuidv4(), + title: "", + unit: "", + url: "", + description: "", + }, + ], + }, + ], + timeRange: null, + relatedTasks: [], + }; + + + const fetchProjectDetail = async () => { + try { + setLoading(true); + const { data, error } = await supabase + .from("resources") + .select("*") + .eq("id", id) + .single(); + + if (error) throw error; + + if (data?.attributes) { + const formData = { + projectName: data.attributes.projectName, + customers: data.attributes.customers.map((customer) => customer.id) || [], + description: data.attributes.description, + sections: data.attributes.sections.map((section) => ({ + uniqueId: uuidv4(), + sectionName: section.sectionName, + items: section.items.map((item) => ({ + uniqueId: uuidv4(), + title: item.title, + unit: item.unit, + url: item.url, + description: item.description || "", + })), + })), + timeRange: data.attributes.startDate && data.attributes.endDate + ? [dayjs(data.attributes.startDate), dayjs(data.attributes.endDate)] + : null, + relatedTasks: data.attributes.relatedTasks?.map(task => task.id) || [], + }; + + form.setFieldsValue(formData); + setFormValues(formData); + } + } catch (error) { + console.error("获取项目详情失败:", error); + message.error("获取项目详情失败"); + } finally { + setLoading(false); + } + }; + + const fetchTemplateData = async () => { + try { + setLoading(true); + const { data: template, error } = await supabase + .from("resources") + .select("*") + .eq("type", "projectTemplate") + .eq("id", templateId) + .single(); + + if (error) throw error; + + if (template?.attributes) { + const projectData = { + projectName: template.attributes.templateName, + description: template.attributes.description, + sections: template.attributes.sections.map((section) => ({ + uniqueId: uuidv4(), + sectionName: section.sectionName, + items: section.items.map((item) => ({ + uniqueId: uuidv4(), + title: item.title, + unit: item.unit, + url: item.url, + description: item.description, + })), + })), + }; + form.setFieldsValue(projectData); + setFormValues(projectData); + } + } catch (error) { + console.error("获取模板数据失败:", error); + message.error("获取模板数据失败"); + } finally { + setLoading(false); + } + }; + + const fetchCustomers = async () => { + try { + const { data, error } = await supabase + .from("resources") + .select("*") + .eq("type", "customer"); + + if (error) throw error; + setCustomers(data || []); + } catch (error) { + console.error("获取客户列表失败:", error); + } + }; + + const fetchTasks = async () => { + setLoadingTasks(true); + try { + const { data, error } = await supabase + .from("resources") + .select("*") + .eq("type", "task") + .order("created_at", { ascending: false }); + + if (error) throw error; + setTasks(data || []); + } catch (error) { + console.error("获取任务列表失败:", error); + message.error("获取任务列表失败"); + } finally { + setLoadingTasks(false); + } + }; + + const handleValuesChange = (changedValues, allValues) => { + setFormValues(allValues); + }; + + const onFinish = async (values) => { + try { + setLoading(true); + const projectData = { + type: "project", + attributes: { + projectName: values.projectName, + customers: customers + .filter(customer => values.customers.includes(customer.id)) + .map(customer => ({ + id: customer.id, + name: customer.attributes.name + })), + description: values.description, + sections: values.sections.map((section) => ({ + sectionName: section.sectionName, + items: section.items.map((item) => ({ + title: item.title, + unit: item.unit, + url: item.url, + description: item.description, + })), + })), + startDate: values.timeRange ? values.timeRange[0].format('YYYY-MM-DD') : null, + endDate: values.timeRange ? values.timeRange[1].format('YYYY-MM-DD') : null, + relatedTasks: tasks + .filter(task => values.relatedTasks?.includes(task.id)) + .map(task => ({ + id: task.id, + name: task.attributes.taskName + })), + }, + }; + + let result; + if (id) { + result = await supabase + .from("resources") + .update(projectData) + .eq("id", id) + .select(); + } else { + result = await supabase + .from("resources") + .insert([projectData]) + .select(); + } + + if (result.error) throw result.error; + + message.success("保存成功"); + navigate("/company/project"); + } catch (error) { + console.error("保存失败:", error); + message.error("保存失败"); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + fetchCustomers(); + fetchTasks(); + }, []); + + useEffect(() => { + if (id) { + fetchProjectDetail(); + } else if (templateId) { + fetchTemplateData(); + } else { + form.setFieldsValue(initialValues); + setFormValues(initialValues); + } + }, [id, templateId]); + return ( -
ProjectDetail
- ) +
+ + +
+ + {id ? (isEdit ? "编辑专案" : "查看专案") : "新建专案"} + + + {id + ? isEdit + ? "请修改专案信息" + : "专案详情" + : "请填写专案信息"} + +
+ + + {!isView && ( + + )} + +
+ } + > +
+ + + 基本信息 + + } + bordered={false} + > +
+ 专案名称} + rules={[{ required: true, message: "请输入专案名称" }]} + > + + + + 客户名称} + > + + (option?.label ?? "").toLowerCase().includes(input.toLowerCase()) + } + options={tasks.map((task) => ({ + value: task.id, + label: task.attributes.taskName, + }))} + /> + +
+ + 专案描述} + > +