feat:服务模版 抽离

This commit is contained in:
‘Liammcl’
2024-12-27 01:10:16 +08:00
parent bde0a8fd65
commit b9ea7218e3
16 changed files with 1078 additions and 883 deletions

View File

@@ -2,7 +2,12 @@
"compilerOptions": { "compilerOptions": {
"baseUrl": ".", "baseUrl": ".",
"paths": { "paths": {
"@/*": ["src/*"] "@/*": ["src/*"],
"@components/*": ["src/components/*"],
"@pages/*": ["src/pages/*"],
"@utils/*": ["src/utils/*"],
"@assets/*": ["src/assets/*"],
"@config/*": ["src/config/*"]
} }
} }
} }

View File

@@ -1,4 +1,4 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect, useMemo } from 'react';
import { Form, Input, InputNumber, Button, Card, Typography, Modal, message, Divider, Select } from 'antd'; import { Form, Input, InputNumber, Button, Card, Typography, Modal, message, Divider, Select } from 'antd';
import { PlusOutlined, DeleteOutlined, EditOutlined, CheckOutlined, CloseOutlined } from '@ant-design/icons'; import { PlusOutlined, DeleteOutlined, EditOutlined, CheckOutlined, CloseOutlined } from '@ant-design/icons';
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
@@ -247,12 +247,53 @@ const SectionList = ({
onClick={() => handleCreateCustom(add, fieldsLength)} onClick={() => handleCreateCustom(add, fieldsLength)}
className="w-1/3 border-2" className="w-1/3 border-2"
> >
自定义小节 自定义模块
</Button> </Button>
</div> </div>
</div> </div>
); );
// 修改项目小计的计算,将其封装为 memo 组件
const ItemSubtotal = React.memo(({ quantity, price, currentCurrency }) => {
const subtotal = useMemo(() => {
const safeQuantity = Number(quantity) || 0;
const safePrice = Number(price) || 0;
return safeQuantity * safePrice;
}, [quantity, price]);
return (
<div className="text-right">
<span className="text-gray-500">
{formatCurrency(subtotal, currentCurrency)}
</span>
</div>
);
});
// 修改小节总计的计算,将其封装为 memo 组件
const SectionTotal = React.memo(({ items, currentCurrency }) => {
const total = useMemo(() => {
if (!Array.isArray(items)) return 0;
return items.reduce((sum, item) => {
if (!item) return sum;
const safeQuantity = Number(item.quantity) || 0;
const safePrice = Number(item.price) || 0;
return sum + (safeQuantity * safePrice);
}, 0);
}, [items]);
return (
<div className="flex justify-end border-t pt-4">
<span className="text-gray-500">
小计总额
<span className="text-blue-500 font-medium ml-2">
{formatCurrency(total, currentCurrency)}
</span>
</span>
</div>
);
});
return ( return (
<> <>
<Form.List name="sections"> <Form.List name="sections">
@@ -439,16 +480,11 @@ const SectionList = ({
> >
<InputNumber placeholder="单价" min={0} className="w-full" /> <InputNumber placeholder="单价" min={0} className="w-full" />
</Form.Item> </Form.Item>
<div className="text-right"> <ItemSubtotal
<span className="text-gray-500"> quantity={formValues?.sections?.[sectionIndex]?.items?.[itemIndex]?.quantity}
{formatCurrency( price={formValues?.sections?.[sectionIndex]?.items?.[itemIndex]?.price}
calculateItemAmount( currentCurrency={currentCurrency}
formValues?.sections?.[sectionIndex]?.items?.[itemIndex]?.quantity, />
formValues?.sections?.[sectionIndex]?.items?.[itemIndex]?.price
)
)}
</span>
</div>
{!isView && itemFields.length > 1 && ( {!isView && itemFields.length > 1 && (
<Button <Button
type="text" type="text"
@@ -472,18 +508,10 @@ const SectionList = ({
</Button> </Button>
)} )}
<div className="flex justify-end border-t pt-4"> <SectionTotal
<span className="text-gray-500"> items={formValues?.sections?.[sectionIndex]?.items}
小计总额 currentCurrency={currentCurrency}
<span className="text-blue-500 font-medium ml-2"> />
{formatCurrency(
calculateSectionTotal(
formValues?.sections?.[sectionIndex]?.items
)
)}
</span>
</span>
</div>
</> </>
)} )}
</Form.List> </Form.List>

View File

@@ -0,0 +1,55 @@
import React from 'react'
import { Modal, Card, Space } from 'antd';
import { FileTextOutlined, ProjectOutlined, CheckSquareOutlined } from '@ant-design/icons';
const TEMPLATE_TYPES = [
{
key: 'quotation',
title: '报价单模板',
icon: <FileTextOutlined className="text-2xl text-blue-500" />,
description: '创建标准化的报价单模板,包含服务项目、价格等信息'
},
{
key: 'project',
title: '专案模板',
icon: <ProjectOutlined className="text-2xl text-green-500" />,
description: '创建专案流程模板,包含项目阶段、时间线等信息'
},
{
key: 'task',
title: '任务模板',
icon: <CheckSquareOutlined className="text-2xl text-purple-500" />,
description: '创建标准任务模板,包含检查项、执行步骤等信息'
}
];
const TemplateTypeModal = ({ open, onClose, onSelect }) => {
return (
<Modal
title="选择模板类型"
open={open}
onCancel={onClose}
footer={null}
width={800}
>
<Space className="w-full" size="large">
{TEMPLATE_TYPES.map(type => (
<Card
key={type.key}
hoverable
className="flex-1 cursor-pointer transition-all hover:shadow-lg"
onClick={() => onSelect(type.key)}
>
<div className="text-center space-y-4">
{type.icon}
<h3 className="text-lg font-medium">{type.title}</h3>
<p className="text-gray-500 text-sm">{type.description}</p>
</div>
</Card>
))}
</Space>
</Modal>
);
};
export default TemplateTypeModal;

View File

@@ -5,7 +5,7 @@ const ThemeContext = createContext();
export const ThemeProvider = ({ children }) => { export const ThemeProvider = ({ children }) => {
const [isDarkMode, setIsDarkMode] = useState(() => { const [isDarkMode, setIsDarkMode] = useState(() => {
const savedTheme = localStorage.getItem('theme'); const savedTheme = localStorage.getItem('theme');
return savedTheme === 'dark' || (savedTheme === null && window.matchMedia('(prefers-color-scheme: dark)').matches); return savedTheme === 'dark';
}); });
useEffect(() => { useEffect(() => {

View File

@@ -64,20 +64,28 @@ class SupabaseService {
} }
} }
// 通用 UPDATE 请求 // 优化 UPDATE 请求
async update(table, match, updates) { async update(table, match, updates) {
try { try {
const { data, error } = await supabase let query = supabase
.from(table) .from(table)
.update(updates) .update(updates);
.match(match)
.select()
if (error) throw error // 如果是对象,使用 match 方法
return data if (typeof match === 'object') {
query = query.match(match);
} else {
// 如果是单个 id使用 eq 方法
query = query.eq('id', match);
}
const { data, error } = await query.select();
if (error) throw error;
return data;
} catch (error) { } catch (error) {
console.error(`Error updating ${table}:`, error.message) console.error(`Error updating ${table}:`, error.message);
throw error throw error;
} }
} }

View File

@@ -83,7 +83,7 @@ const CustomerForm = () => {
}; };
return ( return (
<div className="bg-gradient-to-b from-gray-50 to-white min-h-screen p-2"> <div className="bg-gradient-to-b from-gray-50 to-white min-h-screen p-2">
<Card <Card
className="shadow-lg rounded-lg border-0" className="shadow-lg rounded-lg border-0"
title={ title={

View File

@@ -629,7 +629,7 @@ const QuotationForm = () => {
}; };
return ( return (
<div className="bg-gradient-to-b from-gray-50 to-white min-h-screen p-2"> <div className="bg-gradient-to-b from-gray-50 to-white dark:from-gray-800 dark:to-gray-900/90 min-h-screen p-2">
<Card <Card
className="shadow-lg rounded-lg border-0" className="shadow-lg rounded-lg border-0"
title={ title={

View File

@@ -1,26 +1,56 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { Table, Button, Form, Input, Space, message, Popconfirm, Drawer } from 'antd'; import { Table, Button, Form, Input, Space, message, Popconfirm, Drawer, Select, Tabs, Badge, Radio } from 'antd';
import { PlusOutlined } from '@ant-design/icons'; import { PlusOutlined, FileTextOutlined, ProjectOutlined, CheckSquareOutlined } from '@ant-design/icons';
import { supabase } from '@/config/supabase'; import { supabaseService } from '@/hooks/supabaseService';
const CategoryDrawer = () => { const CategoryDrawer = () => {
const [data, setData] = useState([]); const [data, setData] = useState([]);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [editingKey, setEditingKey] = useState(''); const [editingKey, setEditingKey] = useState('');
const [drawerVisible, setDrawerVisible] = useState(false); const [drawerVisible, setDrawerVisible] = useState(false);
const [activeType, setActiveType] = useState('quotation'); // 当前选中的类型
const [form] = Form.useForm(); const [form] = Form.useForm();
// 模板类型配置
const TEMPLATE_TYPES = [
{
value: 'quotation',
label: '报价单模板',
icon: <FileTextOutlined className="text-blue-500" />,
color: 'blue',
description: '用于创建标准化的报价单'
},
{
value: 'project',
label: '专案模板',
icon: <ProjectOutlined className="text-green-500" />,
color: 'green',
description: '用于创建项目流程模板'
},
{
value: 'task',
label: '任务模板',
icon: <CheckSquareOutlined className="text-purple-500" />,
color: 'purple',
description: '用于创建标准任务模板'
}
];
// 获取分类数据 // 获取分类数据
const fetchCategories = async () => { const fetchCategories = async (type = activeType) => {
setLoading(true); setLoading(true);
try { try {
const { data: categories, error } = await supabase const { data: categories } = await supabaseService.select('resources', {
.from('resources') filter: {
.select('*') type: { eq: 'categories' },
.eq('type', 'categories') 'attributes->>type': { eq: type }
.order('created_at', { ascending: false }); },
order: {
column: 'created_at',
ascending: false
}
});
if (error) throw error;
setData(categories || []); setData(categories || []);
} catch (error) { } catch (error) {
message.error('获取分类数据失败'); message.error('获取分类数据失败');
@@ -34,13 +64,23 @@ const CategoryDrawer = () => {
if (drawerVisible) { if (drawerVisible) {
fetchCategories(); fetchCategories();
} }
}, [drawerVisible]); }, [drawerVisible, activeType]);
// 切换模板类型
const handleTypeChange = (type) => {
setActiveType(type);
setEditingKey(''); // 清除编辑状态
form.resetFields(); // 重置表单
};
// 新增分类 // 新增分类
const handleAdd = () => { const handleAdd = (type) => {
const newData = { const newData = {
id: Date.now().toString(), id: Date.now().toString(),
attributes: { name: '' }, attributes: {
name: '',
type: type // 默认类型
},
isNew: true isNew: true
}; };
setData([newData, ...data]); setData([newData, ...data]);
@@ -53,31 +93,26 @@ const CategoryDrawer = () => {
try { try {
const values = await form.validateFields(); const values = await form.validateFields();
if (record.isNew) { if (record.isNew) {
// 新增 await supabaseService.insert('resources', {
const { error } = await supabase type: 'categories',
.from('resources') attributes: {
.insert([{ name: values.name,
type: 'categories', type: values.type
attributes: { },
name: values.name schema_version: 1
}, });
schema_version: 1
}]);
if (error) throw error;
} else { } else {
// 更新 // 更新
const { error } = await supabase await supabaseService.update('resources',
.from('resources') { id: record.id },
.update({ {
attributes: { attributes: {
name: values.name name: values.name,
type: values.type
}, },
updated_at: new Date().toISOString() updated_at: new Date().toISOString()
}) }
.eq('id', record.id); );
if (error) throw error;
} }
message.success('保存成功'); message.success('保存成功');
@@ -92,12 +127,7 @@ const CategoryDrawer = () => {
// 删除分类 // 删除分类
const handleDelete = async (record) => { const handleDelete = async (record) => {
try { try {
const { error } = await supabase await supabaseService.delete('resources', { id: record.id });
.from('resources')
.delete()
.eq('id', record.id);
if (error) throw error;
message.success('删除成功'); message.success('删除成功');
fetchCategories(); fetchCategories();
} catch (error) { } catch (error) {
@@ -125,6 +155,29 @@ const CategoryDrawer = () => {
); );
}, },
}, },
{
title: '模板类型',
dataIndex: ['attributes', 'type'],
render: (text, record) => {
const isEditing = record.id === editingKey;
return isEditing ? (
<Form.Item
name="type"
style={{ margin: 0 }}
rules={[{ required: true, message: '请选择模板类型!' }]}
>
<Select
options={TEMPLATE_TYPES}
placeholder="请选择模板类型"
/>
</Form.Item>
) : (
<span className="text-gray-600">
{TEMPLATE_TYPES.find(t => t.value === text)?.label || text}
</span>
);
},
},
{ {
title: '操作', title: '操作',
render: (_, record) => { render: (_, record) => {
@@ -161,13 +214,14 @@ const CategoryDrawer = () => {
setEditingKey(record.id); setEditingKey(record.id);
form.setFieldsValue({ form.setFieldsValue({
name: record.attributes.name, name: record.attributes.name,
type: record.attributes.type
}); });
}} }}
> >
编辑 编辑
</Button> </Button>
<Popconfirm <Popconfirm
title="确认除" title="确认<EFBFBD><EFBFBD><EFBFBD>除"
description="确定要删除这个分类吗?" description="确定要删除这个分类吗?"
onConfirm={() => handleDelete(record)} onConfirm={() => handleDelete(record)}
okText="确定" okText="确定"
@@ -209,14 +263,56 @@ const CategoryDrawer = () => {
className="category-drawer" className="category-drawer"
> >
<div className="p-6"> <div className="p-6">
{/* 类型选择器 */}
<div className="mb-6">
<Radio.Group
value={activeType}
onChange={(e) => handleTypeChange(e.target.value)}
className="flex space-x-4"
>
{TEMPLATE_TYPES.map(type => (
<Radio.Button
key={type.value}
value={type.value}
className={`flex items-center px-4 py-2 ${
activeType === type.value ? `bg-${type.color}-50 border-${type.color}-500` : ''
}`}
>
<span className="flex items-center space-x-2">
{type.icon}
<span>{type.label}</span>
<Badge
count={data.filter(item => item.attributes.type === type.value).length}
className={`bg-${type.color}-100 text-${type.color}-600`}
/>
</span>
</Radio.Button>
))}
</Radio.Group>
</div>
{/* 当前类型说明 */}
<div className="mb-4 p-4 bg-gray-50 dark:bg-gray-800 rounded-lg">
<h3 className={`text-${TEMPLATE_TYPES.find(t => t.value === activeType)?.color}-500 text-lg font-medium mb-2 flex items-center`}>
{TEMPLATE_TYPES.find(t => t.value === activeType)?.icon}
<span className="ml-2">{TEMPLATE_TYPES.find(t => t.value === activeType)?.label}</span>
</h3>
<p className="text-gray-500 dark:text-gray-400">
{TEMPLATE_TYPES.find(t => t.value === activeType)?.description}
</p>
</div>
{/* 新增按钮 */}
<Button <Button
type="primary" type="primary"
onClick={handleAdd} onClick={() => handleAdd(activeType)}
className="mb-4" className="mb-4"
icon={<PlusOutlined />} icon={<PlusOutlined />}
> >
新增分类 新增分类
</Button> </Button>
{/* 分类列表 */}
<Form form={form}> <Form form={form}>
<Table <Table
scroll={{ x: true }} scroll={{ x: true }}
@@ -228,7 +324,7 @@ const CategoryDrawer = () => {
pageSize: 10, pageSize: 10,
showTotal: (total) => `${total}`, showTotal: (total) => `${total}`,
}} }}
className="bg-white rounded-lg shadow" className="bg-white dark:bg-gray-800 rounded-lg shadow"
/> />
</Form> </Form>
</div> </div>

View File

@@ -0,0 +1,19 @@
import React from 'react';
import { Form, Card } from 'antd';
const ProjectTemplate = ({ form, id, isView }) => {
return (
<Form
form={form}
layout="vertical"
disabled={isView}
>
{/* 专案模板特有的字段和组件 */}
<Card>
{/* 项目阶段、时间线等内容 */}
</Card>
</Form>
);
};
export default ProjectTemplate;

View File

@@ -0,0 +1,222 @@
import React, { useState, useEffect } from 'react';
import { Form, Card, Input, Select, message,Button } from 'antd';
import { supabaseService } from '@/hooks/supabaseService';
import {ArrowLeftOutlined}from '@ant-design/icons'
import SectionList from '@/components/SectionList';
const QuotationTemplate = ({ id, isView, onCancel,isEdit }) => {
const [form] = Form.useForm();
const [loading, setLoading] = useState(false);
const [formValues, setFormValues] = useState({
sections: [{ items: [{}] }],
currency: "CNY"
});
const [categories, setCategories] = useState([]);
useEffect(() => {
console.log(id,'id');
if (id) {
fetchServiceTemplate();
}
fetchCategories();
}, [id]);
const fetchServiceTemplate = async () => {
try {
setLoading(true);
const { data } = await supabaseService.select('resources', {
filter: {
id: { eq: id },
type: { eq: 'serviceTemplate' },
'attributes->>type': { eq: 'quotation' }
}
});
console.log(data,'data');
if (data?.[0]) {
const formData = {
templateName: data[0].attributes.templateName,
description: data[0].attributes.description,
category: data[0].attributes.category?.map(v => v.id) || [],
sections: data[0].attributes.sections || [{ items: [{}] }],
currency: data[0].attributes.currency || "CNY",
};
form.setFieldsValue(formData);
setFormValues(formData);
}
} catch (error) {
console.error("获取服务模版失败:", error);
message.error("获取服务模版失败");
} finally {
setLoading(false);
}
};
const fetchCategories = async () => {
try {
const { data } = await supabaseService.select('resources', {
filter: {
type: { eq: 'categories' },
'attributes->>type': { eq: 'quotation' }
},
order: {
column: 'created_at',
ascending: false
}
});
const formattedCategories = (data || []).map(category => ({
value: category.id,
label: category.attributes.name
}));
setCategories(formattedCategories);
} catch (error) {
message.error('获取分类数据失败');
console.error(error);
}
};
const handleValuesChange = (changedValues, allValues) => {
setFormValues(allValues);
};
const onFinish = async (values) => {
try {
setLoading(true);
const categoryData = values.category.map(categoryId => {
const category = categories.find(c => c.value === categoryId);
return {
id: categoryId,
name: category.label
};
});
const serviceData = {
type: "serviceTemplate",
attributes: {
type: "quotation",
templateName: values.templateName,
description: values.description,
sections: values.sections,
category: categoryData,
},
};
if (id) {
// 更新
await supabaseService.update('resources',
{ id },
serviceData
);
} else {
// 新增
await supabaseService.insert('resources', serviceData);
}
message.success("保存成功");
onCancel();
} catch (error) {
console.error("保存失败:", error);
message.error("保存失败");
} finally {
setLoading(false);
}
};
return (
<>
<Form
form={form}
onFinish={onFinish}
onValuesChange={handleValuesChange}
layout="vertical"
variant="filled"
disabled={isView}
>
<Card
className="shadow-sm rounded-lg mb-6"
type="inner"
title={
<span className="flex items-center space-x-2 text-gray-700">
<span className="w-1 h-4 bg-blue-500 rounded-full" />
<span>基本信息</span>
</span>
}
bordered={false}
>
<div className="grid grid-cols-2 gap-6">
<Form.Item
label="模版名称"
name="templateName"
rules={[{ required: true, message: "请输入模版名称" }]}
>
<Input placeholder="请输入模版名称" />
</Form.Item>
<Form.Item
label="模版分类"
name="category"
rules={[{ required: true, message: "请选择或输入分类" }]}
>
<Select
placeholder="请选择或输入分类"
showSearch
allowClear
mode="tags"
options={categories}
loading={loading}
/>
</Form.Item>
<Form.Item
label="模版描述"
name="description"
className="col-span-2"
>
<Input.TextArea rows={4} placeholder="请输入模版描述" />
</Form.Item>
</div>
</Card>
<Card
className="shadow-sm rounded-lg"
type="inner"
title={
<span className="flex items-center space-x-2 text-gray-700">
<span className="w-1 h-4 bg-blue-500 rounded-full" />
<span>服务明细</span>
</span>
}
bordered={false}
>
<SectionList
form={form}
isView={isView}
formValues={formValues}
onValuesChange={handleValuesChange}
/>
</Card>
<div className="flex justify-end space-x-4">
<Button
icon={<ArrowLeftOutlined />}
onClick={onCancel}
>
返回
</Button>
{!isView && (
<Button
type="primary"
onClick={() => form.submit()}
loading={loading}
>
保存
</Button>
)}
</div>
</Form>
</>
);
};
export default QuotationTemplate;

View File

@@ -0,0 +1,19 @@
import React from 'react';
import { Form, Card } from 'antd';
const TaskTemplate = ({ form, id, isView }) => {
return (
<Form
form={form}
layout="vertical"
disabled={isView}
>
{/* 任务模板特有的字段和组件 */}
<Card>
{/* 任务步骤、检查项等内容 */}
</Card>
</Form>
);
};
export default TaskTemplate;

View File

@@ -1,251 +1,71 @@
import React, { useState, useEffect } from "react"; import React from 'react';
import { import { Card, Typography } from 'antd';
Card, import { useNavigate, useSearchParams,useParams } from 'react-router-dom';
Form, import QuotationTemplate from './components/QuotationTemplate';
Input, import ProjectTemplate from './components/ProjectTemplate';
Select, import TaskTemplate from './components/TaskTemplate';
Button,
Space,
Typography,
message,
} from "antd";
import {
ArrowLeftOutlined,
} from "@ant-design/icons";
import { useNavigate, useParams,useSearchParams, useLocation } from "react-router-dom";
import { supabase } from "@/config/supabase";
import SectionList from '@/components/SectionList';
const { Title } = Typography; const { Title } = Typography;
// 模板类型配置
const TEMPLATE_CONFIG = {
quotation: {
title: '报价单模板',
component: QuotationTemplate,
},
project: {
title: '专案模板',
component: ProjectTemplate,
},
task: {
title: '任务模板',
component: TaskTemplate,
}
};
const ServiceForm = () => { const ServiceForm = () => {
const [form] = Form.useForm();
const navigate = useNavigate(); const navigate = useNavigate();
const { id} = useParams();
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
const isView = searchParams.get("isView") === "true"; const type = searchParams.get('type') || 'quotation';
const [loading, setLoading] = useState(false); const { id } = useParams();
const location = useLocation(); const isView = searchParams.get('isView') === 'true';
const isEdit = location.search.includes("edit=true");
const [formValues, setFormValues] = useState({
sections: [{ items: [{}] }],
currency: "CNY"
});
const [categories, setCategories] = useState([]);
useEffect(() => { const currentTemplate = TEMPLATE_CONFIG[type];
if (id) { const TemplateComponent = currentTemplate?.component;
fetchServiceTemplate();
}
fetchCategories();
}, [id]);
const fetchServiceTemplate = async () => { if (!currentTemplate) {
try { return <div>无效的模板类型</div>;
setLoading(true); }
const { data, error } = await supabase
.from("resources")
.select("*")
.eq("id", id)
.single();
if (error) throw error;
const formData = {
templateName: data.attributes.templateName,
description: data.attributes.description,
category: data.attributes.category.map(v=>v.id),
sections: data.attributes.sections,
};
form.setFieldsValue(formData);
setFormValues(formData);
} catch (error) {
console.error("获取服务模版失败:", error);
message.error("获取服务模版失败");
} finally {
setLoading(false);
}
};
const fetchCategories = async () => {
try {
const { data: categoriesData, error } = await supabase
.from('resources')
.select('*')
.eq('type', 'categories')
.order('created_at', { ascending: false });
if (error) throw error;
const formattedCategories = (categoriesData || []).map(category => ({
value: category.id,
label: category.attributes.name
}));
setCategories(formattedCategories);
} catch (error) {
message.error('获取分类数据失败');
console.error(error);
}
};
const onFinish = async (values) => {
try {
setLoading(true);
const categoryData = values.category.map(categoryId => {
const category = categories.find(c => c.value === categoryId);
return {
id: categoryId,
name: category.label
};
});
const serviceData = {
type: "serviceTemplate",
attributes: {
templateName: values.templateName,
description: values.description,
sections: values.sections,
category: categoryData,
},
};
let result;
if (id) {
result = await supabase
.from("resources")
.update(serviceData)
.eq("id", id)
.select();
} else {
result = await supabase
.from("resources")
.insert([serviceData])
.select();
}
if (result.error) throw result.error;
message.success("保存成功");
navigate("/company/serviceTeamplate");
} catch (error) {
console.error("保存失败:", error);
message.error("保存失败");
} finally {
setLoading(false);
}
};
const handleValuesChange = (changedValues, allValues) => {
setFormValues(allValues);
};
return ( return (
<div className="bg-gradient-to-b from-gray-50 to-white min-h-screen p-2"> <div className="bg-gradient-to-b from-gray-50 to-white dark:from-gray-800 dark:to-gray-900/90 min-h-screen p-2">
<Card <Card
className="shadow-lg rounded-lg border-0" className="shadow-lg rounded-lg border-0"
title={ title={
<div className="flex justify-between items-center py-2"> <div className="flex justify-between items-center py-2">
<div className="flex items-center space-x-3"> <div className="flex items-center space-x-3">
<Title level={4} className="mb-0 text-gray-800"> <Title level={4} className="mb-0 text-gray-800 dark:text-gray-200">
{id ? (isEdit ? "编辑服务模版" : "查看服务模版") : "新建服务模版"} {id ? (isView ? "查看" : "编辑") : "新建"}{currentTemplate.title}
</Title> </Title>
<span className="text-gray-400 text-sm"> <span className="text-gray-400 text-sm">
{id {id
? isEdit ? isView
? "请修改服务模版信息" ? `${currentTemplate.title}详情`
: "服务模版详情" : `请修改${currentTemplate.title}信息`
: "请填写服务模版信息"} : `请填写${currentTemplate.title}信息`}
</span> </span>
</div> </div>
<Space size="middle">
<Button
icon={<ArrowLeftOutlined />}
onClick={() => navigate("/company/serviceTeamplate")}
>
返回
</Button>
{!isView&& <Button
type="primary"
onClick={() => form.submit()}
loading={loading}
>
保存
</Button>}
</Space>
</div> </div>
} }
> >
<Form <TemplateComponent
form={form} id={id}
onFinish={onFinish} isView={isView}
onValuesChange={handleValuesChange} onCancel={() => navigate("/company/serviceTeamplate")}
layout="vertical" />
variant="filled"
disabled={isView}
>
<Card
className="shadow-sm rounded-lg mb-6"
type="inner"
title={
<span className="flex items-center space-x-2 text-gray-700">
<span className="w-1 h-4 bg-blue-500 rounded-full" />
<span>基本信息</span>
</span>
}
bordered={false}
>
<div className="grid grid-cols-2 gap-6">
<Form.Item
label="模版名称"
name="templateName"
rules={[{ required: true, message: "请输入模版名称" }]}
>
<Input placeholder="请输入模版名称" />
</Form.Item>
<Form.Item
label="模版分类"
name="category"
rules={[{ required: true, message: "请选择或输入分类" }]}
>
<Select
placeholder="请选择或输入分类"
showSearch
allowClear
mode="tags"
options={categories}
loading={loading}
/>
</Form.Item>
<Form.Item
label="模版描述"
name="description"
className="col-span-2"
>
<Input.TextArea rows={4} placeholder="请输入模版描述" />
</Form.Item>
</div>
</Card>
<Card
className="shadow-sm rounded-lg"
type="inner"
title={
<span className="flex items-center space-x-2 text-gray-700">
<span className="w-1 h-4 bg-blue-500 rounded-full" />
<span>服务明细</span>
</span>
}
bordered={false}
>
<SectionList
form={form}
isView={!isEdit && id}
formValues={formValues}
onValuesChange={handleValuesChange}
/>
</Card>
</Form>
</Card> </Card>
</div> </div>
); );
}; };
export default ServiceForm; export default ServiceForm;

File diff suppressed because it is too large Load Diff

View File

@@ -83,7 +83,7 @@ const SupplierForm = () => {
}; };
return ( return (
<div className="bg-gradient-to-b from-gray-50 to-white min-h-screen p-2"> <div className="bg-gradient-to-b from-gray-50 to-white dark:from-gray-800 dark:to-gray-900/90 min-h-screen p-2">
<Card <Card
className="shadow-lg rounded-lg border-0" className="shadow-lg rounded-lg border-0"
title={ title={

View File

@@ -172,7 +172,7 @@ const ResourceTaskForm = () => {
}; };
return ( return (
<div className="bg-gradient-to-b from-gray-50 to-white min-h-screen p-2"> <div className="bg-gradient-to-b from-gray-50 to-white dark:from-gray-800 dark:to-gray-900/90 min-h-screen p-2">
<Card <Card
className="shadow-lg rounded-lg border-0" className="shadow-lg rounded-lg border-0"
title={ title={

View File

@@ -147,4 +147,40 @@ body {
.stat-value { .stat-value {
@apply text-2xl font-semibold mt-2; @apply text-2xl font-semibold mt-2;
} }
} }
/* 滚动条基础样式 */
::-webkit-scrollbar {
width: 8px; /* 垂直滚动条宽度 */
height: 8px; /* 水平滚动条高度 */
}
/* 亮色模式滚动条样式 */
::-webkit-scrollbar-track {
@apply bg-gray-100; /* 轨道背景色 */
}
::-webkit-scrollbar-thumb {
@apply bg-gray-300 rounded-full hover:bg-gray-400 transition-colors; /* 滑块样式 */
}
/* 暗色模式滚动条样式 */
.dark {
::-webkit-scrollbar-track {
@apply bg-gray-800; /* 暗色模式轨道背景 */
}
::-webkit-scrollbar-thumb {
@apply bg-gray-600 hover:bg-gray-500; /* 暗色模式滑块样式 */
}
}
/* Firefox 滚动条样式 */
* {
scrollbar-width: thin;
scrollbar-color: theme('colors.gray.300') theme('colors.gray.100');
}
.dark * {
scrollbar-color: theme('colors.gray.600') theme('colors.gray.800');
}