This commit is contained in:
xuqssq
2024-12-23 01:15:54 +08:00
parent df0aa520ca
commit feefbd7a5c
8 changed files with 592 additions and 120 deletions

View File

@@ -26,7 +26,8 @@
"react-infinite-scroll-component": "^6.1.0", "react-infinite-scroll-component": "^6.1.0",
"react-router-dom": "^6.18.0", "react-router-dom": "^6.18.0",
"recharts": "^2.9.0", "recharts": "^2.9.0",
"styled-components": "^6.1.0" "styled-components": "^6.1.0",
"uuid": "^11.0.3"
}, },
"devDependencies": { "devDependencies": {
"@types/react": "^18.2.15", "@types/react": "^18.2.15",
@@ -42,4 +43,4 @@
"tailwindcss": "^3.3.5", "tailwindcss": "^3.3.5",
"vite": "^4.4.5" "vite": "^4.4.5"
} }
} }

View File

@@ -1,122 +1,134 @@
import { lazy } from 'react'; import { lazy } from "react";
// Dashboard route // Dashboard route
const dashboardRoute = { const dashboardRoute = {
path: 'dashboard', path: "dashboard",
component: lazy(() => import('@/pages/Dashboard')), component: lazy(() => import("@/pages/Dashboard")),
name: '仪表盘', name: "仪表盘",
icon: 'dashboard', icon: "dashboard",
}; };
// Resource Management routes // Resource Management routes
const resourceRoutes = [ const resourceRoutes = [
{ {
path: 'team', path: "team",
component: lazy(() => import('@/pages/resource/team')), component: lazy(() => import("@/pages/resource/team")),
name: '团队管理', name: "团队管理",
icon: 'team', icon: "team",
}, },
{ {
path: 'bucket', path: "bucket",
component: lazy(() => import('@/pages/resource/bucket')), component: lazy(() => import("@/pages/resource/bucket")),
name: '对象存储', name: "对象存储",
icon: 'shop', 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 // Company routes
const companyRoutes = [ const companyRoutes = [
{ {
path: 'quotation', path: "quotation",
component: lazy(() => import('@/pages/company/quotation')), component: lazy(() => import("@/pages/company/quotation")),
name: '报价单', name: "报价单",
icon: 'file', icon: "file",
}, { },
path: 'quotaInfo/:id?', // 添加可选的 id 参数 {
path: "quotaInfo/:id?", // 添加可选的 id 参数
hidden: true, hidden: true,
component: lazy(() => import('@/pages/company/quotation/detail')), component: lazy(() => import("@/pages/company/quotation/detail")),
name: '报价单详情', name: "报价单详情",
icon: 'file', icon: "file",
}, },
{ {
path: 'serviceTeamplate', path: "serviceTeamplate",
component: lazy(() => import('@/pages/company/service')), component: lazy(() => import("@/pages/company/service")),
name: '服务管理', name: "服务管理",
icon: 'container', icon: "container",
}, },
{ {
path: 'serviceType', path: "serviceType",
hidden: true, hidden: true,
component: lazy(() => import('@/pages/company/service/serviceType')), component: lazy(() => import("@/pages/company/service/serviceType")),
name: '类型管理', name: "类型管理",
icon: 'container', icon: "container",
}, },
{ {
path: 'serviceTemplateInfo/:id?', path: "serviceTemplateInfo/:id?",
hidden: true, hidden: true,
component: lazy(() => import('@/pages/company/service/detail')), component: lazy(() => import("@/pages/company/service/detail")),
name: '服务模版详情', name: "服务模版详情",
icon: 'container', icon: "container",
}, },
{ {
path: 'quotaInfo/preview/:id?', // 添加可选的 id 参数 path: "quotaInfo/preview/:id?", // 添加可选的 id 参数
hidden: true, hidden: true,
component: lazy(() => import('@/pages/company/quotation/view')), component: lazy(() => import("@/pages/company/quotation/view")),
name: '报价单预览', name: "报价单预览",
icon: 'file', icon: "file",
}, },
{ {
path: 'customer', path: "customer",
component: lazy(() => import('@/pages/company/customer')), component: lazy(() => import("@/pages/company/customer")),
name: '客户管理', name: "客户管理",
icon: 'user', icon: "user",
}, },
{ {
path: 'customerInfo/:id?', path: "customerInfo/:id?",
hidden: true, hidden: true,
component: lazy(() => import('@/pages/company/customer/detail')), component: lazy(() => import("@/pages/company/customer/detail")),
name: '客户详情', name: "客户详情",
icon: 'user', icon: "user",
}, },
{ {
path: 'supplier', path: "supplier",
component: lazy(() => import('@/pages/company/supplier')), component: lazy(() => import("@/pages/company/supplier")),
name: '供应商管理', name: "供应商管理",
icon: 'branches', icon: "branches",
}, },
{ {
path: 'supplierInfo/:id?', path: "supplierInfo/:id?",
hidden: true, hidden: true,
component: lazy(() => import('@/pages/company/supplier/detail')), component: lazy(() => import("@/pages/company/supplier/detail")),
name: '供应商详情', name: "供应商详情",
icon: 'branches', icon: "branches",
}, },
]; ];
const marketingRoutes = [ const marketingRoutes = [];
];
export const routes = [ export const routes = [
dashboardRoute, dashboardRoute,
{ {
path: 'resource', path: "resource",
component: lazy(() => import('@/pages/resource')), component: lazy(() => import("@/pages/resource")),
name: '资源管理', name: "资源管理",
icon: 'appstore', icon: "appstore",
children: resourceRoutes, children: resourceRoutes,
}, },
{ {
path: 'company', path: "company",
component: lazy(() => import('@/pages/company')), component: lazy(() => import("@/pages/company")),
name: '公司管理', name: "公司管理",
icon: 'bank', icon: "bank",
children: companyRoutes, children: companyRoutes,
}, },
{ {
path: 'marketing', path: "marketing",
component: lazy(() => import('@/pages/marketing')), component: lazy(() => import("@/pages/marketing")),
name: '行销中心', name: "行销中心",
icon: 'shopping', icon: "shopping",
children: marketingRoutes, children: marketingRoutes,
}, },
]; ];

View File

@@ -1,58 +1,61 @@
import { useState, useCallback } from 'react'; import { useState, useCallback } from "react";
import { message } from 'antd'; import { message } from "antd";
import { resourceService } from '@/services/supabase/resource'; import { resourceService } from "@/services/supabase/resource";
export const useResources = (initialPagination, initialSorter,type) => { export const useResources = (initialPagination, initialSorter, type) => {
const [resources, setResources] = useState([]); const [resources, setResources] = useState([]);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [total, setTotal] = useState(0); const [total, setTotal] = useState(0);
const [currentPagination, setCurrentPagination] = useState(initialPagination); const [currentPagination, setCurrentPagination] = useState(initialPagination);
const [currentSorter, setCurrentSorter] = useState(initialSorter); const [currentSorter, setCurrentSorter] = useState(initialSorter);
const fetchResources = useCallback(async (params = {}) => { const fetchResources = useCallback(
try { async (params = {}) => {
setLoading(true); try {
const newPagination = { setLoading(true);
current: params.current || currentPagination.current, const newPagination = {
pageSize: params.pageSize || currentPagination.pageSize current: params.current || currentPagination.current,
}; pageSize: params.pageSize || currentPagination.pageSize,
const newSorter = { };
field: params.field || currentSorter.field, const newSorter = {
order: params.order || currentSorter.order field: params.field || currentSorter.field,
}; order: params.order || currentSorter.order,
};
setCurrentPagination(newPagination); setCurrentPagination(newPagination);
setCurrentSorter(newSorter); setCurrentSorter(newSorter);
const { data, total: newTotal } = await resourceService.getResources({ const { data, total: newTotal } = await resourceService.getResources({
page: newPagination.current, page: newPagination.current,
pageSize: newPagination.pageSize, pageSize: newPagination.pageSize,
orderBy: newSorter.field, orderBy: newSorter.field,
ascending: newSorter.order === 'ascend', ascending: newSorter.order === "ascend",
type: type, type: type,
...(params?.search !== '' ? { searchQuery: params.search } : {}) ...(params?.search !== "" ? { searchQuery: params.search } : {}),
}); });
setResources(data || []); setResources(data || []);
setTotal(newTotal || 0); setTotal(newTotal || 0);
return { data, total: newTotal }; return { data, total: newTotal };
} catch (error) { } catch (error) {
console.error('获取列表失败:', error); console.error("获取列表失败:", error);
message.error('获取列表失败'); message.error("获取列表失败");
} finally { } finally {
setLoading(false); setLoading(false);
} }
}, [currentPagination, currentSorter]); },
[currentPagination, currentSorter]
);
const createResource = async (values) => { const createResource = async (values) => {
try { try {
const newResource = await resourceService.createResource(values); const newResource = await resourceService.createResource(values);
await fetchResources({ current: 1 }); await fetchResources({ current: 1 });
message.success('创建成功'); message.success("创建成功");
return newResource; return newResource;
} catch (error) { } catch (error) {
message.error('创建失败'); message.error("创建失败");
throw error; throw error;
} }
}; };
@@ -61,24 +64,25 @@ export const useResources = (initialPagination, initialSorter,type) => {
try { try {
const updatedResource = await resourceService.updateResource(id, values); const updatedResource = await resourceService.updateResource(id, values);
await fetchResources({ current: currentPagination.current }); await fetchResources({ current: currentPagination.current });
message.success('更新成功'); message.success("更新成功");
return updatedResource; return updatedResource;
} catch (error) { } catch (error) {
message.error('更新失败'); message.error("更新失败");
throw error; throw error;
} }
}; };
const deleteResource = async (id) => { const deleteResource = async (id) => {
try { try {
await resourceService.deleteResource(id); await resourceService.deleteResource(id, type);
const newCurrent = resources.length === 1 && currentPagination.current > 1 const newCurrent =
? currentPagination.current - 1 resources.length === 1 && currentPagination.current > 1
: currentPagination.current; ? currentPagination.current - 1
: currentPagination.current;
await fetchResources({ current: newCurrent }); await fetchResources({ current: newCurrent });
message.success('删除成功'); message.success("删除成功");
} catch (error) { } catch (error) {
message.error('删除失败'); message.error("删除失败");
throw error; throw error;
} }
}; };
@@ -94,4 +98,4 @@ export const useResources = (initialPagination, initialSorter,type) => {
updateResource, updateResource,
deleteResource, deleteResource,
}; };
}; };

View File

@@ -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) => (
<Form.Item
name={["items", index, "name"]}
rules={[{ required: true, message: "请输入名称" }]}
style={{ margin: 0 }}
preserve={true}
>
<Input placeholder="请输入名称" maxLength={100} />
</Form.Item>
),
},
{
title: "操作",
width: 100,
render: (_, record, index) => (
<Button
type="link"
danger
disabled={dataSource.length === 1 || isView}
onClick={() => handleRemoveItem(record.id, index)}
>
删除
</Button>
),
},
];
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 (
<div className="bg-gradient-to-b from-gray-50 to-white min-h-screen p-6">
<Card
className="shadow-lg rounded-lg border-0"
title={
<div className="flex justify-between items-center py-2">
<div className="flex items-center space-x-3">
<Title level={4} className="mb-0 text-gray-800">
{id ? (isEdit ? "编辑任务" : "查看任务") : "新建任务"}
</Title>
<span className="text-gray-400 text-sm">
{id
? isEdit
? "请修改任务信息"
: "任务详情"
: "请填写任务信息"}
</span>
</div>
<Space size="middle">
<Button
icon={<ArrowLeftOutlined />}
onClick={() => navigate("/resource/task")}
>
返回
</Button>
{!isView && (
<Button
type="primary"
icon={<SaveOutlined />}
onClick={() => form.submit()}
loading={loading}
>
保存
</Button>
)}
</Space>
</div>
}
>
<Form
form={form}
onFinish={onFinish}
layout="vertical"
initialValues={{
items: [{ id: uuidv4() }],
}}
disabled={isView || loading}
>
<Card
className="mb-4 shadow-sm rounded-lg"
title={
<div className="flex items-center">
<div className="w-1 h-4 bg-blue-500 rounded-full mr-2" />
<span>基本信息</span>
</div>
}
bordered={false}
>
<Form.Item
name="taskName"
label="任务名称"
rules={[{ required: true, message: "请输入任务名称" }]}
>
<Input placeholder="请输入任务名称" maxLength={100} />
</Form.Item>
</Card>
<Card
className="shadow-sm rounded-lg"
title={
<div className="flex items-center">
<div className="w-1 h-4 bg-green-500 rounded-full mr-2" />
<span>任务明细</span>
</div>
}
extra={
!isView && (
<Button
type="dashed"
icon={<PlusOutlined />}
onClick={handleAddItem}
disabled={loading}
>
新增一栏
</Button>
)
}
bordered={false}
>
<Table
dataSource={dataSource}
columns={columns}
pagination={false}
rowKey="id"
loading={loading}
/>
</Card>
</Form>
</Card>
</div>
);
};
export default ResourceTaskForm;

View File

@@ -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) => (
<span>
{new Date(text).toLocaleString("zh-CN", {
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
})}
</span>
),
},
{
title: "操作",
width: 250,
key: "action",
render: (_, record) => (
<Space size={0}>
<Button
size="small"
type="link"
icon={<EyeOutlined />}
onClick={() => navigate(`/resource/task/edit/${record.id}`)}
>
查看
</Button>
<Button
size="small"
type="link"
icon={<EditOutlined />}
onClick={() =>
navigate(`/resource/task/edit/${record.id}?edit=true`)
}
>
编辑
</Button>
<Popconfirm
title="确定要删除这个任务吗?"
description="删除后将无法恢复!"
onConfirm={() => handleDelete(record.id)}
okText="确定"
cancelText="取消"
okButtonProps={{ danger: true }}
>
<Button size="small" type="link" danger icon={<DeleteOutlined />}>
删除
</Button>
</Popconfirm>
</Space>
),
},
];
return (
<Card
title={
<Space>
<span>任务管理</span>
<Tag color="blue">{total} 个任务</Tag>
</Space>
}
className="h-full w-full overflow-auto"
extra={
<Button
type="primary"
icon={<PlusOutlined />}
onClick={() => navigate("/resource/task/edit")}
>
新增任务
</Button>
}
>
<Table
columns={columns}
dataSource={quotations}
rowKey="id"
loading={loading}
onChange={handleTableChange}
pagination={{
...pagination,
total,
showSizeChanger: true,
showQuickJumper: true,
showTotal: (total) => `${total} 条记录`,
}}
/>
</Card>
);
};
export default ResourceTask;

View File

@@ -69,13 +69,13 @@ export const resourceService = {
} }
}, },
async deleteResource(id) { async deleteResource(id,type) {
try { try {
const { error } = await supabase const { error } = await supabase
.from('resources') .from('resources')
.delete() .delete()
.eq('id', id) .eq('id', id)
.eq('type', 'quota') .eq('type', type)
.select() .select()
if (error) throw error; if (error) throw error;

7
vercel.json Normal file
View File

@@ -0,0 +1,7 @@
{
"rewrites": [
{
"source": "/(.*)"
}
]
}

View File

@@ -4143,6 +4143,11 @@ util-deprecate@^1.0.2:
resolved "https://registry.npmmirror.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" resolved "https://registry.npmmirror.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== 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: victory-vendor@^36.6.8:
version "36.9.2" version "36.9.2"
resolved "https://registry.npmmirror.com/victory-vendor/-/victory-vendor-36.9.2.tgz#668b02a448fa4ea0f788dbf4228b7e64669ff801" resolved "https://registry.npmmirror.com/victory-vendor/-/victory-vendor-36.9.2.tgz#668b02a448fa4ea0f788dbf4228b7e64669ff801"