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"
+ />
+ }
+ onClick={handleSectionNameSave}
+ className="text-green-500"
+ />
+ }
+ onClick={() => {
+ setEditingSectionIndex(null);
+ setEditingSectionName("");
+ }}
+ className="text-red-500"
+ />
+
+ ) : (
+
+
+
+ {form.getFieldValue([
+ "sections",
+ sectionIndex,
+ "sectionName",
+ ]) || `资源分组 ${sectionIndex + 1}`}
+
+ {!isView && (
+ }
+ onClick={() =>
+ handleSectionNameEdit(
+ sectionIndex,
+ form.getFieldValue([
+ "sections",
+ sectionIndex,
+ "sectionName",
+ ])
+ )
+ }
+ className="text-gray-400 hover:text-blue-500"
+ />
+ )}
+
+ )}
+
+ {!isView && (
+
+ }
+ onClick={() => addSectionFn(field)}
+ className="ml-2"
+ >
+ 添加到模版
+
+ }
+ onClick={() => remove(field.name)}
+ />
+
+ )}
+
+ }
+ >
+
+ {(itemFields, { add: addItem, remove: removeItem }) => (
+ <>
+
+
资源链接
+
资源标题
+
资源类型
+
资源描述
+
操作
+
+
+ {itemFields.map((itemField) => {
+ const { key, ...restItemField } = itemField;
+ return (
+
+ );
+ })}
+
+ {!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
+ ? "请修改专案信息"
+ : "专案详情"
+ : "请填写专案信息"}
+
+
+
+ }
+ onClick={() => navigate("/company/project")}
+ >
+ 返回
+
+ {!isView && (
+ }
+ onClick={() => form.submit()}
+ loading={loading}
+ >
+ 保存
+
+ )}
+
+
+ }
+ >
+
+
+
+
+ );
}
diff --git a/src/pages/company/project/index.jsx b/src/pages/company/project/index.jsx
index 8669832..074719a 100644
--- a/src/pages/company/project/index.jsx
+++ b/src/pages/company/project/index.jsx
@@ -1,41 +1,331 @@
-import React from 'react';
-import { Card, Table, Button } from 'antd';
-import { PlusOutlined } from '@ant-design/icons';
+import React, { useEffect, useState } from 'react';
+import { Card, Table, Button, message, Popconfirm, Tag, Space, Select } from 'antd';
+import { PlusOutlined, EditOutlined, DeleteOutlined, EyeOutlined } from '@ant-design/icons';
+import { useResources } from "@/hooks/resource/useResource";
+import { useNavigate } from "react-router-dom";
+import { supabase } from "@/config/supabase";
+import dayjs from 'dayjs';
const ProjectPage = () => {
+ const navigate = useNavigate();
+ const [pagination, setPagination] = useState({ current: 1, pageSize: 10 });
+ const [sorter, setSorter] = useState({
+ field: "created_at",
+ order: "descend",
+ });
+ const [customers, setCustomers] = useState([]);
+ const [loadingCustomers, setLoadingCustomers] = useState(false);
+ const [selectedCustomer, setSelectedCustomer] = useState(null);
+ const [tasks, setTasks] = useState([]);
+ const [loadingTasks, setLoadingTasks] = useState(false);
+ const [selectedTask, setSelectedTask] = useState(null);
+
+ const {
+ resources: projects,
+ loading: loadingProjects,
+ total,
+ fetchResources: fetchProjects,
+ deleteResource: deleteProject,
+ } = useResources(pagination, sorter, "project");
+
+ useEffect(() => {
+ fetchProjects();
+ fetchCustomers();
+ fetchTasks();
+ }, []);
+
+ const fetchCustomers = async () => {
+ setLoadingCustomers(true);
+ try {
+ const { data, error } = await supabase
+ .from('resources')
+ .select('*')
+ .eq('type', 'customer')
+ .order('created_at', { ascending: false });
+
+ if (error) throw error;
+ setCustomers(data || []);
+ } catch (error) {
+ console.error('获取客户列表失败:', error);
+ message.error('获取客户列表失败');
+ } finally {
+ setLoadingCustomers(false);
+ }
+ };
+
+ 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 columns = [
{
- title: '项目名称',
- dataIndex: 'name',
- key: 'name',
+ title: "专案名称",
+ dataIndex: ["attributes", "projectName"],
+ key: "projectName",
+ ellipsis: true,
+ className: "dark:text-gray-200",
},
{
- title: '负责人',
- dataIndex: 'manager',
- key: 'manager',
+ title: "客户",
+ dataIndex: ["attributes", "customers"],
+ key: "customers",
+ render: (customers) => (
+
+ {customers?.map(customer => (
+
+ {customer.name}
+
+ )) || '-'}
+
+ ),
},
{
- title: '开始日期',
- dataIndex: 'startDate',
- key: 'startDate',
+ title: "相关任务",
+ dataIndex: ["attributes", "relatedTasks"],
+ key: "relatedTasks",
+ render: (tasks) => (
+
+ {tasks?.map(task => (
+
+ {task.name}
+
+ )) || '-'}
+
+ ),
},
{
- title: '状态',
- dataIndex: 'status',
- key: 'status',
+ title: "项目周期",
+ key: "projectPeriod",
+ render: (_, record) => {
+ const startDate = record.attributes.startDate;
+ const endDate = record.attributes.endDate;
+ if (!startDate || !endDate) return '-';
+
+ const start = dayjs(startDate);
+ const end = dayjs(endDate);
+ const days = end.diff(start, 'day');
+ const months = end.diff(start, 'month', true).toFixed(1);
+
+ return (
+
+ {days > 60
+ ? `约 ${months} 个月`
+ : `${days} 天`}
+
+ );
+ },
+ },
+ {
+ title: "开始日期",
+ dataIndex: ["attributes", "startDate"],
+ key: "startDate",
+ render: (date) => (
+
+ {date || '-'}
+
+ ),
+ },
+
+ {
+ title: "创建时间",
+ dataIndex: "created_at",
+ key: "created_at",
+ sorter: true,
+ render: (text) => (
+
+ {dayjs(text).format('YYYY-MM-DD HH:mm')}
+
+ ),
+ },
+ {
+ title: "操作",
+ key: "action",
+ fixed: "right",
+ render: (_, record) => (
+
+ }
+ onClick={() => navigate(`/company/projectView/${record.id}`)}
+ className="dark:text-gray-300 dark:hover:text-blue-400"
+ >
+ 查看
+
+ }
+ onClick={() => navigate(`/company/projectInfo/${record.id}?edit=true`)}
+ className="dark:text-gray-300 dark:hover:text-blue-400"
+ >
+ 编辑
+
+ handleDelete(record.id)}
+ okText="确定"
+ cancelText="取消"
+ okButtonProps={{ danger: true }}
+ >
+ }
+ className="dark:hover:text-red-400"
+ >
+ 删除
+
+
+
+ ),
},
];
+ const handleDelete = async (id) => {
+ try {
+ await deleteProject(id);
+ fetchProjects();
+ message.success('删除成功');
+ } catch (error) {
+ message.error("删除失败:" + error.message);
+ }
+ };
+
+ const handleTableChange = (pagination, filters, sorter) => {
+ setPagination(pagination);
+ setSorter({
+ field: sorter.field || "created_at",
+ order: sorter.order || "descend",
+ });
+ fetchProjects({
+ current: pagination.current,
+ pageSize: pagination.pageSize,
+ field: sorter.field,
+ order: sorter.order,
+ search: JSON.stringify({
+ ...(selectedCustomer && { customerId: selectedCustomer }),
+ ...(selectedTask && { taskId: selectedTask })
+ })
+ });
+ };
+
return (
+ 专案管理
+
+ {total} 个专案
+
+
+ }
+ className="h-full w-full overflow-auto "
extra={
- }>
- 新增专案
-
+
+
}
>
-
+ `共 ${total} 条记录`,
+ }}
+ />
);
};
diff --git a/src/pages/company/project/info.jsx b/src/pages/company/project/info.jsx
new file mode 100644
index 0000000..1b7f18e
--- /dev/null
+++ b/src/pages/company/project/info.jsx
@@ -0,0 +1,758 @@
+import React, { useState, useEffect } from 'react';
+import { useParams } from 'react-router-dom';
+import { supabase } from "@/config/supabase";
+import { Spin, Tag, Empty, Button, Modal, Form, Input, Collapse, message, Upload,Tabs, Select, Divider } from 'antd';
+import { PlusOutlined, FolderOpenOutlined, ClockCircleOutlined, InboxOutlined } from '@ant-design/icons';
+import dayjs from 'dayjs';
+import {supabaseService}from '@/hooks/supabaseService'
+const type="project"
+export default function ProjectInfo() {
+ const { id } = useParams();
+ const [loading, setLoading] = useState(true);
+ const [projectData, setProjectData] = useState(null);
+ const [form] = Form.useForm();
+ const [isModalVisible, setIsModalVisible] = useState(false);
+ const [units, setUnits] = useState([]);
+ const [loadingUnits, setLoadingUnits] = useState(false);
+ const [currentUnit, setCurrentUnit] = useState('');
+ const [relatedTasks, setRelatedTasks] = useState([]);
+ const [loadingTasks, setLoadingTasks] = useState(false);
+ const [uploadFileList, setUploadFileList] = useState([]);
+
+ const fetchProjectDetail = async () => {
+ try {
+ setLoading(true);
+ const { data, error } = await supabase
+ .from("resources")
+ .select("*")
+ .eq("id", id)
+ .single();
+
+ if (error) throw error;
+ setProjectData(data);
+ } catch (error) {
+ console.error("获取项目详情失败:", error);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ useEffect(() => {
+ if (id) {
+ fetchProjectDetail();
+ }
+
+ }, [id]);
+
+ const handleAddItem = async (values) => {
+ try {
+ const newItem = {
+ url: values.url,
+ unit: currentUnit,
+ title: values.title,
+ description: values.description,
+ };
+
+ const updatedSections = projectData.attributes.sections.map(section => {
+ if (section.sectionName === currentUnit) {
+ return {
+ ...section,
+ items: [...(section.items || []), newItem]
+ };
+ }
+ return section;
+ });
+
+ const { error } = await supabase
+ .from("resources")
+ .update({
+ attributes: {
+ ...projectData.attributes,
+ sections: updatedSections
+ }
+ })
+ .eq("id", id);
+
+ if (error) throw error;
+
+ setProjectData({
+ ...projectData,
+ attributes: {
+ ...projectData.attributes,
+ sections: updatedSections
+ }
+ });
+
+ message.success('添加成功');
+ setIsModalVisible(false);
+ form.resetFields();
+ } catch (error) {
+ console.error("添加资源项失败:", error);
+ message.error('添加失败');
+ }
+ };
+
+ 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);
+ }
+ };
+
+ const handleAddUnit = async (unitName) => {
+ try {
+ const { error } = await supabase.from("resources").insert([
+ {
+ type: "units",
+ attributes: {
+ name: unitName,
+ template_type: "project",
+ },
+ schema_version: 1,
+ },
+ ]);
+
+ if (error) throw error;
+ message.success("新增资源类型成功");
+ fetchUnits();
+ return true;
+ } catch (error) {
+ message.error("新增资源类型失败");
+ console.error(error);
+ return false;
+ }
+ };
+
+ useEffect(() => {
+ fetchUnits();
+ }, []);
+
+ const getDomainFromUrl = (url) => {
+ try {
+ const domain = new URL(url).hostname;
+ return domain.replace('www.', '');
+ } catch (error) {
+ return url;
+ }
+ };
+
+ // 处理资源分组
+ const getResourcesByUnit = () => {
+ const resourcesByUnit = {};
+
+ // 确保 sections 存在且是数组
+ if (projectData?.attributes?.sections && Array.isArray(projectData.attributes.sections)) {
+ projectData.attributes.sections.forEach(section => {
+ // 确保 items 存在且是数组
+ if (section.items && Array.isArray(section.items)) {
+ // 使用 sectionName 作为 unit
+ const unit = section.sectionName;
+ if (!resourcesByUnit[unit]) {
+ resourcesByUnit[unit] = [];
+ }
+ // 将该 section 的所有 items 添加到对应的 unit 中
+ resourcesByUnit[unit].push(...section.items);
+ }
+ });
+ }
+
+ return resourcesByUnit;
+ };
+
+ // 在渲染之前获取处理后的资源
+ const resourcesByUnit = getResourcesByUnit();
+
+ // 添加一个图标映射函数
+ const getResourceIcon = (url) => {
+ const domain = getDomainFromUrl(url).toLowerCase();
+ if (domain.includes('figma')) return '🎨';
+ if (domain.includes('github')) return '💻';
+ if (domain.includes('notion')) return '📝';
+ if (domain.includes('docs.google')) return '📄';
+ if (domain.includes('sheets.google')) return '📊';
+ if (domain.includes('youtube')) return '🎥';
+ if (domain.includes('drive.google')) return '📁';
+ return '🔗';
+ };
+
+ // 获取关联任务的详细信息
+ const fetchRelatedTasks = async () => {
+ if (!projectData?.attributes?.relatedTasks?.length) return;
+
+ setLoadingTasks(true);
+ try {
+ const taskIds = projectData.attributes.relatedTasks.map(task => task.id);
+ const { data: tasks, error } = await supabase
+ .from("resources")
+ .select("*")
+ .eq("type", "task")
+ .in("id", taskIds);
+
+ if (error) throw error;
+ setRelatedTasks(tasks || []);
+ } catch (error) {
+ console.error("获取关联任务失败:", error);
+ message.error("获取关联任务失败");
+ } finally {
+ setLoadingTasks(false);
+ }
+ };
+
+ useEffect(() => {
+ if (projectData) {
+ fetchRelatedTasks();
+ }
+ }, [projectData]);
+
+ // 渲染任务详情
+ const renderTaskDetails = () => {
+ if (loadingTasks) {
+ return ;
+ }
+
+ if (!relatedTasks.length) {
+ return null;
+ }
+
+ return (
+
+
+
+
+
+ 关联任务
+
+
+
+ {relatedTasks.map((task) => (
+
+ {/* 任务标题和状态 */}
+
+
+ {task.attributes.taskName}
+
+
+ {task.attributes.status}
+
+
+
+ {/* 任务时间和客户 */}
+
+
+
+ {task.attributes.timeRange ?
+ `${dayjs(task.attributes.timeRange[0]).format('YYYY-MM-DD')} 至 ${dayjs(task.attributes.timeRange[1]).format('YYYY-MM-DD')}` :
+ '未设置时间'
+ }
+
+ {/*
+
客户:
+
+ {task.attributes.customers?.map((customer) => (
+
+ {customer.name}
+
+ ))}
+
+
*/}
+
+
+ {/* 任务流程 */}
+
+
+
+ {task.attributes.sections?.map((section, sIndex) => (
+
+
+ {section.sectionName}
+
+
+ {section.items?.map((item, iIndex) => (
+
+
+
+ {item.name}
+
+ {item.description && (
+
+ {item.description}
+
+ )}
+
+
+ {item.unit}
+
+
+ ))}
+
+
+ ))}
+
+
+
+
+ ))}
+
+
+
+
+ );
+ };
+
+ // 修改文件上传处理函数
+ const handleFileUpload = async (file) => {
+ 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 = '其他文件';
+ }
+
+ // 自动填充表单
+ form.setFieldsValue({
+ url: publicUrl,
+ title: `${fileType}-${file.name}`,
+ unit: fileType,
+ description: ''
+ });
+
+ return true;
+ } catch (error) {
+ console.error('文件上传失败:', error);
+ return false;
+ }
+ };
+
+ // 修改模态框内容
+ const renderModal = () => (
+ {
+ setIsModalVisible(false);
+ form.resetFields();
+ }}
+ destroyOnClose={true}
+ footer={null}
+ >
+
+
+
+
+
+
+
+
+
+ ({
+ label: unit.attributes.name,
+ value: unit.attributes.name,
+ }))}
+ dropdownRender={(menu) => (
+ <>
+ {menu}
+
+
+ }
+ onSearch={async (value) => {
+ if (!value.trim()) return;
+ if (await handleAddUnit(value.trim())) {
+ form.setFieldValue('unit', value.trim());
+ }
+ }}
+ />
+
+ >
+ )}
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+
+ if (loading) {
+ return (
+
+
+
+ );
+ }
+
+ if (!projectData) {
+ return (
+
+
+
+ );
+ }
+
+ const { attributes } = projectData;
+
+ // 打开所有资源的函数
+ const openAllResources = (items) => {
+ items.forEach(item => {
+ if (item.url) {
+ window.open(item.url, '_blank');
+ }
+ });
+ };
+
+ return (
+
+
+
+ {renderTaskDetails()}
+
+
+
+
+ {attributes.projectName}
+
+
+
+
+
+ 项目描述
+
+
+ {attributes.description || '暂无描述'}
+
+
+
+
+
+ 项目周期
+
+
+ {attributes.startDate && attributes.endDate ? (
+
+ {dayjs(attributes.startDate).format('YYYY-MM-DD')} 至 {dayjs(attributes.endDate).format('YYYY-MM-DD')}
+
+ ) : '未设置'}
+
+
+
+
+
+ 相关客户
+
+
+ {attributes.customers?.map((customer) => (
+
+ {customer.name}
+
+ )) || '暂无客户'}
+
+
+
+
+
+ 关联任务
+
+
+ {attributes.relatedTasks?.map((task) => (
+
+ {task.name}
+
+ )) || '暂无任务'}
+
+
+
+
+
+
+
+
+
+ {Object.entries(resourcesByUnit).map(([unit, items]) => (
+
+
+
+
+
+ {unit}
+
+
+ {items.length > 0 && (
+ }
+ onClick={() => openAllResources(items)}
+ className="flex items-center hover:text-blue-600 hover:border-blue-600 transition-colors
+ dark:hover:text-blue-400 dark:hover:border-blue-400"
+ >
+ 打开全部
+
+ )}
+ }
+ onClick={() => {
+ setCurrentUnit(unit);
+ setIsModalVisible(true);
+ }}
+ className="flex items-center bg-blue-500 hover:bg-blue-600 border-none shadow-lg
+ shadow-blue-500/30 transition-all hover:shadow-blue-500/50"
+ >
+ 添加资源
+
+
+
+
+ {items.length > 0 ? (
+
+ {items.map((item, itemIndex) => (
+
+
+ {/* 资源图标 */}
+
+ {item.url && getResourceIcon(item.url)}
+
+
+ {/* 资源内容 */}
+
+
+
+ {/* 资源描述 */}
+ {item.description && (
+
+
+ {item.description}
+
+
+
+ )}
+
+
+
+
+
+
+
+ ))}
+
+ ) : (
+
+
+ 📂
+
+
+ 暂无资源
+
+
+ 点击"添加资源"按钮开始添加资源
+
+
+ )}
+
+
+ ))}
+
+
+
+
+
+ {renderModal()}
+
+ );
+}
diff --git a/src/pages/company/quotation/index.jsx b/src/pages/company/quotation/index.jsx
index 664ff7d..d75a358 100644
--- a/src/pages/company/quotation/index.jsx
+++ b/src/pages/company/quotation/index.jsx
@@ -353,6 +353,7 @@ useEffect(()=>{
{
switch (type) {
@@ -10,7 +10,7 @@ export default function SectionComponent({ activeType }) {
case "task":
return ;
default:
- return ;
+ return ;
}
};
return <>{renderFn(activeType)}>;
diff --git a/src/pages/company/service/itemsManange/sections/project.jsx b/src/pages/company/service/itemsManange/sections/project.jsx
new file mode 100644
index 0000000..7133884
--- /dev/null
+++ b/src/pages/company/service/itemsManange/sections/project.jsx
@@ -0,0 +1,544 @@
+import React, { useState, useEffect } from 'react';
+import {
+ Table,
+ Button,
+ Form,
+ Input,
+ Space,
+ message,
+ Popconfirm,
+ Select,
+ Divider,
+ Card,
+ Typography,
+ Upload,
+ Modal
+} from 'antd';
+import {
+ PlusOutlined,
+ DeleteOutlined,
+ InboxOutlined,
+ UploadOutlined
+} from '@ant-design/icons';
+import { supabaseService } from '@/hooks/supabaseService';
+import { v4 as uuidv4 } from 'uuid';
+import { supabase } from "@/config/supabase";
+
+const { Text } = Typography;
+const TYPE = 'project';
+
+const ProjectSections = () => {
+ const [data, setData] = useState([]);
+ const [loading, setLoading] = useState(false);
+ const [editingKey, setEditingKey] = useState('');
+ const [form] = Form.useForm();
+ const [loadingUnits, setLoadingUnits] = useState(false);
+ const [units, setUnits] = useState([]);
+ const [formValues, setFormValues] = useState({});
+ const [uploadModalVisible, setUploadModalVisible] = useState(false);
+ const [currentAddItem, setCurrentAddItem] = useState(null);
+
+ const fetchSections = async () => {
+ setLoading(true);
+ try {
+ const { data: sections } = await supabaseService.select('resources', {
+ filter: {
+ 'type': { eq: 'sections' },
+ 'attributes->>template_type': { eq: TYPE }
+ },
+ order: {
+ column: 'created_at',
+ ascending: false
+ }
+ });
+ setData(sections || []);
+ } catch (error) {
+ message.error('获取模块数据失败');
+ console.error(error);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ 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(() => {
+ fetchSections();
+ fetchUnits();
+ }, []);
+
+ const handleAdd = () => {
+ const newData = {
+ id: Date.now().toString(),
+ attributes: {
+ name: '',
+ template_type: TYPE,
+ items: [{
+ key: uuidv4(),
+ url: '',
+ title: '',
+ unit: '',
+ description: ''
+ }]
+ },
+ isNew: true
+ };
+ setData([newData, ...data]);
+ setEditingKey(newData.id);
+ form.setFieldsValue(newData.attributes);
+ };
+
+ const handleSave = async (record) => {
+ try {
+ const values = await form.validateFields();
+ const items = form.getFieldValue(['items']) || [];
+
+ if (!items.length || !items.some(item => item.title)) {
+ message.error('请至少添加一个有效的资源项目');
+ return;
+ }
+
+ const formattedItems = items.filter(item => item.title).map(item => ({
+ url: item.url,
+ title: item.title,
+ unit: item.unit,
+ description: item.description
+ }));
+
+ if (record.isNew) {
+ await supabaseService.insert('resources', {
+ type: 'sections',
+ attributes: {
+ name: values.name,
+ template_type: TYPE,
+ items: formattedItems,
+ },
+ schema_version: 1
+ });
+ } else {
+ await supabaseService.update('resources',
+ { id: record.id },
+ {
+ attributes: {
+ name: values.name,
+ template_type: TYPE,
+ items: formattedItems,
+ },
+ updated_at: new Date().toISOString()
+ }
+ );
+ }
+
+ message.success('保存成功');
+ setEditingKey('');
+ fetchSections();
+ } catch (error) {
+ message.error('保存失败');
+ console.error(error);
+ }
+ };
+
+ const handleDelete = async (record) => {
+ try {
+ await supabaseService.delete('resources', { id: record.id });
+ message.success('删除成功');
+ fetchSections();
+ } catch (error) {
+ message.error('删除失败');
+ console.error(error);
+ }
+ };
+
+ const handleAddUnit = async (unitName) => {
+ try {
+ await supabaseService.insert("resources", {
+ type: "units",
+ attributes: {
+ name: unitName,
+ template_type: TYPE,
+ },
+ schema_version: 1,
+ });
+
+ message.success("新增资源类型成功");
+ fetchUnits();
+ return true;
+ } catch (error) {
+ message.error("新增资源类型失败");
+ console.error(error);
+ return false;
+ }
+ };
+
+ const handleValuesChange = (changedValues, allValues) => {
+ setFormValues(allValues);
+ };
+
+ const handleFileUpload = async (file, addItem) => {
+ try {
+ const fileExt = file.name.split('.').pop();
+ const fileName = `${Date.now()}.${fileExt}`;
+
+ const { data, error } = await supabase.storage
+ .from('file')
+ .upload(filePath, file);
+
+ if (error) throw error;
+
+ const { data: { publicUrl } } = supabase.storage
+ .from('file')
+ .getPublicUrl(fileName);
+
+ 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 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 columns = [
+ {
+ title: '模块名称',
+ dataIndex: ['attributes', 'name'],
+ width: 200,
+ render: (text, record) => {
+ const isEditing = record.id === editingKey;
+ return isEditing ? (
+
+
+
+ ) : (
+ {text}
+ );
+ },
+ },
+ {
+ title: '资源项目',
+ dataIndex: ['attributes', 'items'],
+ render: (items, record) => {
+ const isEditing = record.id === editingKey;
+ if (isEditing) {
+ return (
+
+ {(fields, { add, remove }) => (
+
+ {fields.map((field, index) => (
+
+
+
+ ))}
+
+
+
+
+
+
+ )}
+
+ );
+ }
+
+ return (
+
+ {(items || []).map((item, index) => (
+
+
+ {item.title}
+
+
{item.unit || '未设置类型'}
+ {item.description && (
+
({item.description})
+ )}
+
+ ))}
+
+ );
+ },
+ },
+ {
+ title: '操作',
+ width: 160,
+ fixed: 'right',
+ render: (_, record) => {
+ const isEditing = record.id === editingKey;
+ return isEditing ? (
+
+
+
+
+ ) : (
+
+
+ handleDelete(record)}
+ okText="确定"
+ cancelText="取消"
+ okButtonProps={{
+ className: "bg-red-500 hover:bg-red-600 border-red-500"
+ }}
+ >
+
+
+
+ );
+ },
+ },
+ ];
+
+ return (
+
+
+
+
+ }
+ className="bg-blue-600 hover:bg-blue-700 border-0 shadow-sm h-10"
+ >
+ 新增模块
+
+
+
+
+
+
+