diff --git a/jsconfig.json b/jsconfig.json
index df83de4..9eeca62 100644
--- a/jsconfig.json
+++ b/jsconfig.json
@@ -2,7 +2,12 @@
"compilerOptions": {
"baseUrl": ".",
"paths": {
- "@/*": ["src/*"]
+ "@/*": ["src/*"],
+ "@components/*": ["src/components/*"],
+ "@pages/*": ["src/pages/*"],
+ "@utils/*": ["src/utils/*"],
+ "@assets/*": ["src/assets/*"],
+ "@config/*": ["src/config/*"]
}
}
}
\ No newline at end of file
diff --git a/src/components/SectionList/index.jsx b/src/components/SectionList/index.jsx
index 38168af..d0f1ffe 100644
--- a/src/components/SectionList/index.jsx
+++ b/src/components/SectionList/index.jsx
@@ -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 { PlusOutlined, DeleteOutlined, EditOutlined, CheckOutlined, CloseOutlined } from '@ant-design/icons';
import { v4 as uuidv4 } from 'uuid';
@@ -247,12 +247,53 @@ const SectionList = ({
onClick={() => handleCreateCustom(add, fieldsLength)}
className="w-1/3 border-2"
>
- 自定义小节
+ 自定义模块
);
+ // 修改项目小计的计算,将其封装为 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 (
+
+
+ {formatCurrency(subtotal, currentCurrency)}
+
+
+ );
+ });
+
+ // 修改小节总计的计算,将其封装为 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 (
+
+
+ 小计总额:
+
+ {formatCurrency(total, currentCurrency)}
+
+
+
+ );
+ });
+
return (
<>
@@ -439,16 +480,11 @@ const SectionList = ({
>
-
-
- {formatCurrency(
- calculateItemAmount(
- formValues?.sections?.[sectionIndex]?.items?.[itemIndex]?.quantity,
- formValues?.sections?.[sectionIndex]?.items?.[itemIndex]?.price
- )
- )}
-
-
+
{!isView && itemFields.length > 1 && (
diff --git a/src/components/TemplateTypeModal/index.jsx b/src/components/TemplateTypeModal/index.jsx
new file mode 100644
index 0000000..b04dcc5
--- /dev/null
+++ b/src/components/TemplateTypeModal/index.jsx
@@ -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: ,
+ description: '创建标准化的报价单模板,包含服务项目、价格等信息'
+ },
+ {
+ key: 'project',
+ title: '专案模板',
+ icon: ,
+ description: '创建专案流程模板,包含项目阶段、时间线等信息'
+ },
+ {
+ key: 'task',
+ title: '任务模板',
+ icon: ,
+ description: '创建标准任务模板,包含检查项、执行步骤等信息'
+ }
+];
+
+const TemplateTypeModal = ({ open, onClose, onSelect }) => {
+ return (
+
+
+ {TEMPLATE_TYPES.map(type => (
+ onSelect(type.key)}
+ >
+
+ {type.icon}
+
{type.title}
+
{type.description}
+
+
+ ))}
+
+
+ );
+};
+
+export default TemplateTypeModal;
\ No newline at end of file
diff --git a/src/contexts/ThemeContext.jsx b/src/contexts/ThemeContext.jsx
index e1afa32..15d3cfa 100644
--- a/src/contexts/ThemeContext.jsx
+++ b/src/contexts/ThemeContext.jsx
@@ -5,7 +5,7 @@ const ThemeContext = createContext();
export const ThemeProvider = ({ children }) => {
const [isDarkMode, setIsDarkMode] = useState(() => {
const savedTheme = localStorage.getItem('theme');
- return savedTheme === 'dark' || (savedTheme === null && window.matchMedia('(prefers-color-scheme: dark)').matches);
+ return savedTheme === 'dark';
});
useEffect(() => {
diff --git a/src/hooks/supabaseService.js b/src/hooks/supabaseService.js
index a2c9fdc..60ffef9 100644
--- a/src/hooks/supabaseService.js
+++ b/src/hooks/supabaseService.js
@@ -64,20 +64,28 @@ class SupabaseService {
}
}
- // 通用 UPDATE 请求
+ // 优化 UPDATE 请求
async update(table, match, updates) {
try {
- const { data, error } = await supabase
+ let query = supabase
.from(table)
- .update(updates)
- .match(match)
- .select()
+ .update(updates);
- if (error) throw error
- return data
+ // 如果是对象,使用 match 方法
+ 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) {
- console.error(`Error updating ${table}:`, error.message)
- throw error
+ console.error(`Error updating ${table}:`, error.message);
+ throw error;
}
}
diff --git a/src/pages/company/customer/detail/index.jsx b/src/pages/company/customer/detail/index.jsx
index 44f4933..ea73f62 100644
--- a/src/pages/company/customer/detail/index.jsx
+++ b/src/pages/company/customer/detail/index.jsx
@@ -83,7 +83,7 @@ const CustomerForm = () => {
};
return (
-
+
{
};
return (
-
+
{
const [data, setData] = useState([]);
const [loading, setLoading] = useState(false);
const [editingKey, setEditingKey] = useState('');
const [drawerVisible, setDrawerVisible] = useState(false);
+ const [activeType, setActiveType] = useState('quotation'); // 当前选中的类型
const [form] = Form.useForm();
+ // 模板类型配置
+ const TEMPLATE_TYPES = [
+ {
+ value: 'quotation',
+ label: '报价单模板',
+ icon: ,
+ color: 'blue',
+ description: '用于创建标准化的报价单'
+ },
+ {
+ value: 'project',
+ label: '专案模板',
+ icon: ,
+ color: 'green',
+ description: '用于创建项目流程模板'
+ },
+ {
+ value: 'task',
+ label: '任务模板',
+ icon: ,
+ color: 'purple',
+ description: '用于创建标准任务模板'
+ }
+ ];
+
// 获取分类数据
- const fetchCategories = async () => {
+ const fetchCategories = async (type = activeType) => {
setLoading(true);
try {
- const { data: categories, error } = await supabase
- .from('resources')
- .select('*')
- .eq('type', 'categories')
- .order('created_at', { ascending: false });
+ const { data: categories } = await supabaseService.select('resources', {
+ filter: {
+ type: { eq: 'categories' },
+ 'attributes->>type': { eq: type }
+ },
+ order: {
+ column: 'created_at',
+ ascending: false
+ }
+ });
- if (error) throw error;
setData(categories || []);
} catch (error) {
message.error('获取分类数据失败');
@@ -34,13 +64,23 @@ const CategoryDrawer = () => {
if (drawerVisible) {
fetchCategories();
}
- }, [drawerVisible]);
+ }, [drawerVisible, activeType]);
+
+ // 切换模板类型
+ const handleTypeChange = (type) => {
+ setActiveType(type);
+ setEditingKey(''); // 清除编辑状态
+ form.resetFields(); // 重置表单
+ };
// 新增分类
- const handleAdd = () => {
+ const handleAdd = (type) => {
const newData = {
id: Date.now().toString(),
- attributes: { name: '' },
+ attributes: {
+ name: '',
+ type: type // 默认类型
+ },
isNew: true
};
setData([newData, ...data]);
@@ -53,31 +93,26 @@ const CategoryDrawer = () => {
try {
const values = await form.validateFields();
if (record.isNew) {
- // 新增
- const { error } = await supabase
- .from('resources')
- .insert([{
- type: 'categories',
- attributes: {
- name: values.name
- },
- schema_version: 1
- }]);
-
- if (error) throw error;
+ await supabaseService.insert('resources', {
+ type: 'categories',
+ attributes: {
+ name: values.name,
+ type: values.type
+ },
+ schema_version: 1
+ });
} else {
// 更新
- const { error } = await supabase
- .from('resources')
- .update({
+ await supabaseService.update('resources',
+ { id: record.id },
+ {
attributes: {
- name: values.name
+ name: values.name,
+ type: values.type
},
updated_at: new Date().toISOString()
- })
- .eq('id', record.id);
-
- if (error) throw error;
+ }
+ );
}
message.success('保存成功');
@@ -92,12 +127,7 @@ const CategoryDrawer = () => {
// 删除分类
const handleDelete = async (record) => {
try {
- const { error } = await supabase
- .from('resources')
- .delete()
- .eq('id', record.id);
-
- if (error) throw error;
+ await supabaseService.delete('resources', { id: record.id });
message.success('删除成功');
fetchCategories();
} catch (error) {
@@ -125,6 +155,29 @@ const CategoryDrawer = () => {
);
},
},
+ {
+ title: '模板类型',
+ dataIndex: ['attributes', 'type'],
+ render: (text, record) => {
+ const isEditing = record.id === editingKey;
+ return isEditing ? (
+
+
+
+ ) : (
+
+ {TEMPLATE_TYPES.find(t => t.value === text)?.label || text}
+
+ );
+ },
+ },
{
title: '操作',
render: (_, record) => {
@@ -161,13 +214,14 @@ const CategoryDrawer = () => {
setEditingKey(record.id);
form.setFieldsValue({
name: record.attributes.name,
+ type: record.attributes.type
});
}}
>
编辑
handleDelete(record)}
okText="确定"
@@ -209,14 +263,56 @@ const CategoryDrawer = () => {
className="category-drawer"
>
+ {/* 类型选择器 */}
+
+ handleTypeChange(e.target.value)}
+ className="flex space-x-4"
+ >
+ {TEMPLATE_TYPES.map(type => (
+
+
+ {type.icon}
+ {type.label}
+ item.attributes.type === type.value).length}
+ className={`bg-${type.color}-100 text-${type.color}-600`}
+ />
+
+
+ ))}
+
+
+
+ {/* 当前类型说明 */}
+
+
t.value === activeType)?.color}-500 text-lg font-medium mb-2 flex items-center`}>
+ {TEMPLATE_TYPES.find(t => t.value === activeType)?.icon}
+ {TEMPLATE_TYPES.find(t => t.value === activeType)?.label}
+
+
+ {TEMPLATE_TYPES.find(t => t.value === activeType)?.description}
+
+
+
+ {/* 新增按钮 */}
+
+ {/* 分类列表 */}