diff --git a/package.json b/package.json index 71e98c1..61a9c8c 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,8 @@ "react-infinite-scroll-component": "^6.1.0", "react-router-dom": "^6.18.0", "recharts": "^2.9.0", - "styled-components": "^6.1.0" + "styled-components": "^6.1.0", + "uuid": "^11.0.3" }, "devDependencies": { "@types/react": "^18.2.15", @@ -42,4 +43,4 @@ "tailwindcss": "^3.3.5", "vite": "^4.4.5" } -} \ No newline at end of file +} diff --git a/src/config/routes.js b/src/config/routes.js index 1de2bd4..50e1909 100644 --- a/src/config/routes.js +++ b/src/config/routes.js @@ -1,122 +1,134 @@ -import { lazy } from 'react'; +import { lazy } from "react"; // Dashboard route const dashboardRoute = { - path: 'dashboard', - component: lazy(() => import('@/pages/Dashboard')), - name: '仪表盘', - icon: 'dashboard', + path: "dashboard", + component: lazy(() => import("@/pages/Dashboard")), + name: "仪表盘", + icon: "dashboard", }; // Resource Management routes const resourceRoutes = [ { - path: 'team', - component: lazy(() => import('@/pages/resource/team')), - name: '团队管理', - icon: 'team', + path: "team", + component: lazy(() => import("@/pages/resource/team")), + name: "团队管理", + icon: "team", }, { - path: 'bucket', - component: lazy(() => import('@/pages/resource/bucket')), - name: '对象存储', - icon: 'shop', + path: "bucket", + component: lazy(() => import("@/pages/resource/bucket")), + name: "对象存储", + icon: "shop", + }, + { + path: "task", + component: lazy(() => import("@/pages/resource/resourceTask")), + name: "任务管理", + icon: "appstore", + }, + { + path: "task/edit/:id?", + component: lazy(() => import("@/pages/resource/resourceTask/edit")), + hidden: true, + name: "新增/编辑任务", }, ]; // Company routes const companyRoutes = [ { - path: 'quotation', - component: lazy(() => import('@/pages/company/quotation')), - name: '报价单', - icon: 'file', - }, { - path: 'quotaInfo/:id?', // 添加可选的 id 参数 + path: "quotation", + component: lazy(() => import("@/pages/company/quotation")), + name: "报价单", + icon: "file", + }, + { + path: "quotaInfo/:id?", // 添加可选的 id 参数 hidden: true, - component: lazy(() => import('@/pages/company/quotation/detail')), - name: '报价单详情', - icon: 'file', + component: lazy(() => import("@/pages/company/quotation/detail")), + name: "报价单详情", + icon: "file", }, { - path: 'serviceTeamplate', - component: lazy(() => import('@/pages/company/service')), - name: '服务管理', - icon: 'container', + path: "serviceTeamplate", + component: lazy(() => import("@/pages/company/service")), + name: "服务管理", + icon: "container", }, { - path: 'serviceType', + path: "serviceType", hidden: true, - component: lazy(() => import('@/pages/company/service/serviceType')), - name: '类型管理', - icon: 'container', + component: lazy(() => import("@/pages/company/service/serviceType")), + name: "类型管理", + icon: "container", }, { - path: 'serviceTemplateInfo/:id?', + path: "serviceTemplateInfo/:id?", hidden: true, - component: lazy(() => import('@/pages/company/service/detail')), - name: '服务模版详情', - icon: 'container', + component: lazy(() => import("@/pages/company/service/detail")), + name: "服务模版详情", + icon: "container", }, { - path: 'quotaInfo/preview/:id?', // 添加可选的 id 参数 + path: "quotaInfo/preview/:id?", // 添加可选的 id 参数 hidden: true, - component: lazy(() => import('@/pages/company/quotation/view')), - name: '报价单预览', - icon: 'file', + component: lazy(() => import("@/pages/company/quotation/view")), + name: "报价单预览", + icon: "file", }, { - path: 'customer', - component: lazy(() => import('@/pages/company/customer')), - name: '客户管理', - icon: 'user', + path: "customer", + component: lazy(() => import("@/pages/company/customer")), + name: "客户管理", + icon: "user", }, { - path: 'customerInfo/:id?', + path: "customerInfo/:id?", hidden: true, - component: lazy(() => import('@/pages/company/customer/detail')), - name: '客户详情', - icon: 'user', + component: lazy(() => import("@/pages/company/customer/detail")), + name: "客户详情", + icon: "user", }, { - path: 'supplier', - component: lazy(() => import('@/pages/company/supplier')), - name: '供应商管理', - icon: 'branches', + path: "supplier", + component: lazy(() => import("@/pages/company/supplier")), + name: "供应商管理", + icon: "branches", }, { - path: 'supplierInfo/:id?', + path: "supplierInfo/:id?", hidden: true, - component: lazy(() => import('@/pages/company/supplier/detail')), - name: '供应商详情', - icon: 'branches', + component: lazy(() => import("@/pages/company/supplier/detail")), + name: "供应商详情", + icon: "branches", }, ]; -const marketingRoutes = [ -]; +const marketingRoutes = []; export const routes = [ dashboardRoute, { - path: 'resource', - component: lazy(() => import('@/pages/resource')), - name: '资源管理', - icon: 'appstore', + path: "resource", + component: lazy(() => import("@/pages/resource")), + name: "资源管理", + icon: "appstore", children: resourceRoutes, }, { - path: 'company', - component: lazy(() => import('@/pages/company')), - name: '公司管理', - icon: 'bank', + path: "company", + component: lazy(() => import("@/pages/company")), + name: "公司管理", + icon: "bank", children: companyRoutes, }, { - path: 'marketing', - component: lazy(() => import('@/pages/marketing')), - name: '行销中心', - icon: 'shopping', + path: "marketing", + component: lazy(() => import("@/pages/marketing")), + name: "行销中心", + icon: "shopping", children: marketingRoutes, }, -]; \ No newline at end of file +]; diff --git a/src/hooks/resource/useResource.js b/src/hooks/resource/useResource.js index 46cbaf7..6716d7d 100644 --- a/src/hooks/resource/useResource.js +++ b/src/hooks/resource/useResource.js @@ -1,58 +1,61 @@ -import { useState, useCallback } from 'react'; -import { message } from 'antd'; -import { resourceService } from '@/services/supabase/resource'; +import { useState, useCallback } from "react"; +import { message } from "antd"; +import { resourceService } from "@/services/supabase/resource"; -export const useResources = (initialPagination, initialSorter,type) => { +export const useResources = (initialPagination, initialSorter, type) => { const [resources, setResources] = useState([]); const [loading, setLoading] = useState(false); const [total, setTotal] = useState(0); const [currentPagination, setCurrentPagination] = useState(initialPagination); const [currentSorter, setCurrentSorter] = useState(initialSorter); - const fetchResources = useCallback(async (params = {}) => { - try { - setLoading(true); - const newPagination = { - current: params.current || currentPagination.current, - pageSize: params.pageSize || currentPagination.pageSize - }; - const newSorter = { - field: params.field || currentSorter.field, - order: params.order || currentSorter.order - }; + const fetchResources = useCallback( + async (params = {}) => { + try { + setLoading(true); + const newPagination = { + current: params.current || currentPagination.current, + pageSize: params.pageSize || currentPagination.pageSize, + }; + const newSorter = { + field: params.field || currentSorter.field, + order: params.order || currentSorter.order, + }; - setCurrentPagination(newPagination); - setCurrentSorter(newSorter); + setCurrentPagination(newPagination); + setCurrentSorter(newSorter); - 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 } : {}) - }); + 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 } : {}), + }); - setResources(data || []); - setTotal(newTotal || 0); - - return { data, total: newTotal }; - } catch (error) { - console.error('获取列表失败:', error); - message.error('获取列表失败'); - } finally { - setLoading(false); - } - }, [currentPagination, currentSorter]); + setResources(data || []); + setTotal(newTotal || 0); + + return { data, total: newTotal }; + } catch (error) { + console.error("获取列表失败:", error); + message.error("获取列表失败"); + } finally { + setLoading(false); + } + }, + [currentPagination, currentSorter] + ); const createResource = async (values) => { try { const newResource = await resourceService.createResource(values); await fetchResources({ current: 1 }); - message.success('创建成功'); + message.success("创建成功"); return newResource; } catch (error) { - message.error('创建失败'); + message.error("创建失败"); throw error; } }; @@ -61,24 +64,25 @@ export const useResources = (initialPagination, initialSorter,type) => { try { const updatedResource = await resourceService.updateResource(id, values); await fetchResources({ current: currentPagination.current }); - message.success('更新成功'); + message.success("更新成功"); return updatedResource; } catch (error) { - message.error('更新失败'); + message.error("更新失败"); throw error; } }; const deleteResource = async (id) => { try { - await resourceService.deleteResource(id); - const newCurrent = resources.length === 1 && currentPagination.current > 1 - ? currentPagination.current - 1 - : currentPagination.current; + await resourceService.deleteResource(id, type); + const newCurrent = + resources.length === 1 && currentPagination.current > 1 + ? currentPagination.current - 1 + : currentPagination.current; await fetchResources({ current: newCurrent }); - message.success('删除成功'); + message.success("删除成功"); } catch (error) { - message.error('删除失败'); + message.error("删除失败"); throw error; } }; @@ -94,4 +98,4 @@ export const useResources = (initialPagination, initialSorter,type) => { updateResource, deleteResource, }; -}; \ No newline at end of file +}; diff --git a/src/pages/resource/resourceTask/edit/index.jsx b/src/pages/resource/resourceTask/edit/index.jsx new file mode 100644 index 0000000..a1189b9 --- /dev/null +++ b/src/pages/resource/resourceTask/edit/index.jsx @@ -0,0 +1,277 @@ +import React, { useState, useEffect } from "react"; +import { + Form, + Input, + Button, + Space, + Card, + Table, + Typography, + message, +} from "antd"; +import { + PlusOutlined, + ArrowLeftOutlined, + SaveOutlined, +} from "@ant-design/icons"; +import { supabase } from "@/config/supabase"; +import { useNavigate, useParams, useSearchParams } from "react-router-dom"; +import { v4 as uuidv4 } from "uuid"; + +const { Title } = Typography; + +const ResourceTaskForm = () => { + const { id } = useParams(); + const [searchParams] = useSearchParams(); + const isEdit = searchParams.get("edit") === "true"; + const isView = id && !isEdit; + const [form] = Form.useForm(); + const navigate = useNavigate(); + const [loading, setLoading] = useState(false); + const [dataSource, setDataSource] = useState([{ id: uuidv4(), name: '' }]); + + const columns = [ + { + title: "名称", + dataIndex: "name", + render: (_, record, index) => ( + + + + ), + }, + { + title: "操作", + width: 100, + render: (_, record, index) => ( + + ), + }, + ]; + + const handleRemoveItem = (itemId, index) => { + // 获取当前所有表单项的值 + const allFormValues = form.getFieldsValue(); + const currentItems = allFormValues.items || []; + + // 保留要删除项之外的所有项 + const newItems = currentItems.filter((_, idx) => idx !== index); + + // 更新 dataSource,确保保留其他项的数据 + const newDataSource = dataSource.filter((_, idx) => idx !== index); + + // 同步更新 form 和 dataSource + setDataSource(newDataSource); + form.setFieldsValue({ items: newItems }); + }; + + const handleAddItem = () => { + const newItem = { id: uuidv4(), name: '' }; + // 获取当前表单值 + const currentItems = form.getFieldValue('items') || []; + + // 同步更新表单和 dataSource + setDataSource(prev => [...prev, newItem]); + form.setFieldsValue({ + items: [...currentItems, newItem] + }); + }; + + const fetchResourceTaskDetail = async () => { + try { + setLoading(true); + const { data, error } = await supabase + .from("resources") + .select("*") + .eq("id", id) + .single(); + + if (error) throw error; + + if (data?.attributes) { + const items = data.attributes.items?.map(item => ({ + ...item, + id: item.id || uuidv4(), + name: item.name || '' + })) || [{ id: uuidv4(), name: '' }]; + + // 同步设置表单值和 dataSource + form.setFieldsValue({ + taskName: data.attributes.taskName, + items, + }); + setDataSource(items); + } + } catch (error) { + message.error("获取任务详情失败,请稍后重试"); + console.error("获取任务详情失败:", error); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + if (id) { + fetchResourceTaskDetail(); + } + }, [id]); + + const onFinish = async (values) => { + try { + setLoading(true); + + // 验证任务项 + const hasEmptyItems = values.items.some((item) => !item.name?.trim()); + if (hasEmptyItems) { + message.error("请填写所有任务项的名称"); + return; + } + + const resourceTaskData = { + type: "task", + attributes: { + taskName: values.taskName.trim(), + items: values.items.map((item, index) => ({ + id: dataSource[index].id, + name: item.name?.trim(), + })), + }, + }; + + if (id) { + resourceTaskData.id = id; + } + + const { error } = await supabase + .from("resources") + .upsert(resourceTaskData) + .select(); + + if (error) throw error; + + message.success("保存成功"); + navigate("/resource/task"); + } catch (error) { + message.error("保存失败,请稍后重试"); + console.error("保存失败:", error); + } finally { + setLoading(false); + } + }; + + return ( +
+ +
+ + {id ? (isEdit ? "编辑任务" : "查看任务") : "新建任务"} + + + {id + ? isEdit + ? "请修改任务信息" + : "任务详情" + : "请填写任务信息"} + +
+ + + {!isView && ( + + )} + +
+ } + > +
+ +
+ 基本信息 +
+ } + bordered={false} + > + + + +
+ + +
+ 任务明细 +
+ } + extra={ + !isView && ( + + ) + } + bordered={false} + > + + + + + + ); +}; + +export default ResourceTaskForm; diff --git a/src/pages/resource/resourceTask/index.jsx b/src/pages/resource/resourceTask/index.jsx new file mode 100644 index 0000000..0f1e579 --- /dev/null +++ b/src/pages/resource/resourceTask/index.jsx @@ -0,0 +1,166 @@ +import React, { useEffect, useState } from "react"; +import { + Card, + Table, + Button, + message, + Popconfirm, + Tag, + Space, + Tooltip, +} from "antd"; +import { + PlusOutlined, + EditOutlined, + DeleteOutlined, + EyeOutlined, + CopyOutlined, +} from "@ant-design/icons"; +import { useResources } from "@/hooks/resource/useResource"; +import { useNavigate } from "react-router-dom"; + +const ResourceTask = () => { + const navigate = useNavigate(); + const [pagination, setPagination] = useState({ current: 1, pageSize: 10 }); + const [sorter, setSorter] = useState({ + field: "created_at", + order: "descend", + }); + + const { + resources: quotations, + loading, + total, + fetchResources: fetchQuotations, + deleteResource: deleteQuotation, + } = useResources(pagination, sorter, "task"); + + useEffect(() => { + fetchQuotations(); + }, []); + + const handleTableChange = (pagination, filters, sorter) => { + setPagination(pagination); + setSorter(sorter); + fetchQuotations({ + current: pagination.current, + pageSize: pagination.pageSize, + field: sorter.field, + order: sorter.order, + }); + }; + + const handleDelete = async (id) => { + try { + await deleteQuotation(id); + message.success("删除成功"); + fetchQuotations(); + } catch (error) { + message.error("删除失败:" + error.message); + } + }; + + const columns = [ + { + title: "任务名称", + dataIndex: ["attributes", "taskName"], + key: "taskName", + ellipsis: true, + }, + { + title: "创建日期", + dataIndex: "created_at", + key: "created_at", + sorter: true, + render: (text) => ( + + {new Date(text).toLocaleString("zh-CN", { + year: "numeric", + month: "2-digit", + day: "2-digit", + hour: "2-digit", + minute: "2-digit", + })} + + ), + }, + { + title: "操作", + width: 250, + key: "action", + render: (_, record) => ( + + + + + handleDelete(record.id)} + okText="确定" + cancelText="取消" + okButtonProps={{ danger: true }} + > + + + + ), + }, + ]; + + return ( + + 任务管理 + {total} 个任务 + + } + className="h-full w-full overflow-auto" + extra={ + + } + > +
`共 ${total} 条记录`, + }} + /> + + ); +}; + +export default ResourceTask; diff --git a/src/services/supabase/resource.js b/src/services/supabase/resource.js index 8e6873e..4236a08 100644 --- a/src/services/supabase/resource.js +++ b/src/services/supabase/resource.js @@ -69,13 +69,13 @@ export const resourceService = { } }, - async deleteResource(id) { + async deleteResource(id,type) { try { const { error } = await supabase .from('resources') .delete() .eq('id', id) - .eq('type', 'quota') + .eq('type', type) .select() if (error) throw error; diff --git a/vercel.json b/vercel.json new file mode 100644 index 0000000..741c3ff --- /dev/null +++ b/vercel.json @@ -0,0 +1,7 @@ +{ + "rewrites": [ + { + "source": "/(.*)" + } + ] +} diff --git a/yarn.lock b/yarn.lock index 848b3eb..6001831 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4143,6 +4143,11 @@ util-deprecate@^1.0.2: resolved "https://registry.npmmirror.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== +uuid@^11.0.3: + version "11.0.3" + resolved "https://registry.npmmirror.com/uuid/-/uuid-11.0.3.tgz#248451cac9d1a4a4128033e765d137e2b2c49a3d" + integrity sha512-d0z310fCWv5dJwnX1Y/MncBAqGMKEzlBb1AOf7z9K8ALnd0utBX/msg/fA0+sbyN1ihbMsLhrBlnl1ak7Wa0rg== + victory-vendor@^36.6.8: version "36.9.2" resolved "https://registry.npmmirror.com/victory-vendor/-/victory-vendor-36.9.2.tgz#668b02a448fa4ea0f788dbf4228b7e64669ff801"