Merge branch 'main' of github.com:xuqssq/uppmkt-admin
This commit is contained in:
9
pnpm-lock.yaml
generated
9
pnpm-lock.yaml
generated
@@ -59,6 +59,9 @@ importers:
|
|||||||
styled-components:
|
styled-components:
|
||||||
specifier: ^6.1.0
|
specifier: ^6.1.0
|
||||||
version: 6.1.13(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
version: 6.1.13(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||||
|
uuid:
|
||||||
|
specifier: ^11.0.3
|
||||||
|
version: 11.0.3
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@types/react':
|
'@types/react':
|
||||||
specifier: ^18.2.15
|
specifier: ^18.2.15
|
||||||
@@ -2420,6 +2423,10 @@ packages:
|
|||||||
util-deprecate@1.0.2:
|
util-deprecate@1.0.2:
|
||||||
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
|
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
|
||||||
|
|
||||||
|
uuid@11.0.3:
|
||||||
|
resolution: {integrity: sha512-d0z310fCWv5dJwnX1Y/MncBAqGMKEzlBb1AOf7z9K8ALnd0utBX/msg/fA0+sbyN1ihbMsLhrBlnl1ak7Wa0rg==}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
victory-vendor@36.9.2:
|
victory-vendor@36.9.2:
|
||||||
resolution: {integrity: sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==}
|
resolution: {integrity: sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==}
|
||||||
|
|
||||||
@@ -5318,6 +5325,8 @@ snapshots:
|
|||||||
|
|
||||||
util-deprecate@1.0.2: {}
|
util-deprecate@1.0.2: {}
|
||||||
|
|
||||||
|
uuid@11.0.3: {}
|
||||||
|
|
||||||
victory-vendor@36.9.2:
|
victory-vendor@36.9.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/d3-array': 3.2.1
|
'@types/d3-array': 3.2.1
|
||||||
|
|||||||
27
src/App.jsx
27
src/App.jsx
@@ -26,6 +26,33 @@ const ThemedApp = () => {
|
|||||||
? "rgba(255, 255, 255, 0.45)"
|
? "rgba(255, 255, 255, 0.45)"
|
||||||
: "rgba(0, 0, 0, 0.45)",
|
: "rgba(0, 0, 0, 0.45)",
|
||||||
},
|
},
|
||||||
|
components: {
|
||||||
|
// 为所有支持 variant 的组件设置 filled 模式
|
||||||
|
Form: {
|
||||||
|
variant: 'filled',
|
||||||
|
},
|
||||||
|
Input: {
|
||||||
|
variant: 'filled',
|
||||||
|
},
|
||||||
|
Select: {
|
||||||
|
variant: 'filled',
|
||||||
|
},
|
||||||
|
TreeSelect: {
|
||||||
|
variant: 'filled',
|
||||||
|
},
|
||||||
|
DatePicker: {
|
||||||
|
variant: 'filled',
|
||||||
|
},
|
||||||
|
TimePicker: {
|
||||||
|
variant: 'filled',
|
||||||
|
},
|
||||||
|
Cascader: {
|
||||||
|
variant: 'filled',
|
||||||
|
},
|
||||||
|
AutoComplete: {
|
||||||
|
variant: 'filled',
|
||||||
|
},
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className={isDarkMode ? "dark" : ""}>
|
<div className={isDarkMode ? "dark" : ""}>
|
||||||
|
|||||||
@@ -103,7 +103,6 @@ export const AuthProvider = ({ children }) => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(data, error, "data");
|
|
||||||
if (error) {
|
if (error) {
|
||||||
message.error(error.message || "Google 登录失败,请稍后重试");
|
message.error(error.message || "Google 登录失败,请稍后重试");
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -1,24 +0,0 @@
|
|||||||
import { Card, Avatar, Typography, Descriptions } from 'antd';
|
|
||||||
import { UserOutlined } from '@ant-design/icons';
|
|
||||||
|
|
||||||
const { Title } = Typography;
|
|
||||||
|
|
||||||
const Profile = () => {
|
|
||||||
return (
|
|
||||||
<Card>
|
|
||||||
<div style={{ textAlign: 'center', marginBottom: '24px' }}>
|
|
||||||
<Avatar size={64} icon={<UserOutlined />} />
|
|
||||||
<Title level={3} style={{ marginTop: '16px' }}>John Doe</Title>
|
|
||||||
</div>
|
|
||||||
<Descriptions bordered>
|
|
||||||
<Descriptions.Item label="Username">johndoe</Descriptions.Item>
|
|
||||||
<Descriptions.Item label="Email">john@example.com</Descriptions.Item>
|
|
||||||
<Descriptions.Item label="Role">Administrator</Descriptions.Item>
|
|
||||||
<Descriptions.Item label="Status">Active</Descriptions.Item>
|
|
||||||
<Descriptions.Item label="Join Date">2023-10-25</Descriptions.Item>
|
|
||||||
</Descriptions>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Profile;
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,18 +1,28 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { Card, Table, Button, message, Popconfirm, Tag, Space, Tooltip } from 'antd';
|
import { Card, Table, Button, message, Popconfirm, Tag, Space, Spin, Modal, Empty, Select, Typography, Statistic, Divider } from 'antd';
|
||||||
import { PlusOutlined, EditOutlined, DeleteOutlined, EyeOutlined,CopyOutlined } from '@ant-design/icons';
|
import { PlusOutlined, EditOutlined, DeleteOutlined, EyeOutlined,CopyOutlined, FileAddOutlined, AppstoreOutlined } from '@ant-design/icons';
|
||||||
import { useResources } from '@/hooks/resource/useResource';
|
import { useResources } from '@/hooks/resource/useResource';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { formatCurrency } from '@/utils/format'; // 假设你有这个工具函数,如果没有我会提供
|
import {supabase}from '@/config/supabase'
|
||||||
|
const CURRENCY_SYMBOLS = {
|
||||||
|
CNY: "¥",
|
||||||
|
TWD: "NT$",
|
||||||
|
USD: "$",
|
||||||
|
};
|
||||||
const QuotationPage = () => {
|
const QuotationPage = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [pagination, setPagination] = useState({ current: 1, pageSize: 10 });
|
const [pagination, setPagination] = useState({ current: 1, pageSize: 10 });
|
||||||
const [sorter, setSorter] = useState({ field: 'created_at', order: 'descend' });
|
const [sorter, setSorter] = useState({ field: 'created_at', order: 'descend' });
|
||||||
|
const [isModalVisible, setIsModalVisible] = useState(false);
|
||||||
|
const [selectedTemplateId, setSelectedTemplateId] = useState(null);
|
||||||
|
const [templates, setTemplates] = useState([]);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [selectedCategory, setSelectedCategory] = useState('all');
|
||||||
|
const [categories, setCategories] = useState([]);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
resources: quotations,
|
resources: quotations,
|
||||||
loading,
|
loading: loadingQuotations,
|
||||||
total,
|
total,
|
||||||
fetchResources: fetchQuotations,
|
fetchResources: fetchQuotations,
|
||||||
deleteResource: deleteQuotation
|
deleteResource: deleteQuotation
|
||||||
@@ -43,6 +53,68 @@ const QuotationPage = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const fetchTemplates = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
const { data: services, error } = await supabase
|
||||||
|
.from("resources")
|
||||||
|
.select("*")
|
||||||
|
.eq("type", "serviceTemplate")
|
||||||
|
.order("created_at", { ascending: false });
|
||||||
|
|
||||||
|
if (error) throw error;
|
||||||
|
setTemplates(services);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("获取服务模板失败:", error);
|
||||||
|
message.error("获取服务模板失败");
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isModalVisible) {
|
||||||
|
fetchTemplates();
|
||||||
|
}
|
||||||
|
}, [isModalVisible]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (templates.length > 0) {
|
||||||
|
setCategories(getAllCategories(templates));
|
||||||
|
}
|
||||||
|
}, [templates]);
|
||||||
|
|
||||||
|
const handleTemplateSelect = (templateId) => {
|
||||||
|
setSelectedTemplateId(templateId);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleConfirm = () => {
|
||||||
|
if (selectedTemplateId) {
|
||||||
|
navigate(`/company/quotaInfo?templateId=${selectedTemplateId}`);
|
||||||
|
} else {
|
||||||
|
navigate('/company/quotaInfo');
|
||||||
|
}
|
||||||
|
setIsModalVisible(false);
|
||||||
|
setSelectedTemplateId(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getAllCategories = (templates) => {
|
||||||
|
const categorySet = new Set();
|
||||||
|
templates.forEach(template => {
|
||||||
|
template.attributes.category?.forEach(cat => {
|
||||||
|
categorySet.add(JSON.stringify(cat));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return Array.from(categorySet).map(cat => JSON.parse(cat));
|
||||||
|
};
|
||||||
|
|
||||||
|
const getFilteredTemplates = () => {
|
||||||
|
if (selectedCategory === 'all') return templates;
|
||||||
|
return templates.filter(template =>
|
||||||
|
template.attributes.category?.some(cat => cat.id === selectedCategory)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const columns = [
|
const columns = [
|
||||||
{
|
{
|
||||||
title: '报价单名称',
|
title: '报价单名称',
|
||||||
@@ -52,24 +124,59 @@ const QuotationPage = () => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '客户信息',
|
title: '客户信息',
|
||||||
dataIndex: ['attributes', 'customerName'],
|
dataIndex: ['attributes', 'customers'],
|
||||||
key: 'customerName',
|
key: 'customers',
|
||||||
render: (customerName,record) => (
|
render: (customers,record) => (
|
||||||
<Tag color="blue" className='cursor-pointer' onClick={() => {
|
<Space>
|
||||||
navigate(`/company/customerInfo/${record.attributes.customerId}`)
|
{customers?.map(customer => (
|
||||||
}}>{customerName}</Tag>
|
<Tag
|
||||||
|
key={customer.id}
|
||||||
|
color="blue"
|
||||||
|
className='cursor-pointer'
|
||||||
|
onClick={() => {
|
||||||
|
navigate(`/company/customerInfo/${customer.id}`)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{customer.name}
|
||||||
|
</Tag>
|
||||||
|
))}
|
||||||
|
</Space>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '报价总额',
|
title: '报价总额',
|
||||||
dataIndex: ['attributes', 'totalAmount'],
|
dataIndex: ['attributes'],
|
||||||
key: 'totalAmount',
|
key: 'totalAmount',
|
||||||
align: 'right',
|
align: 'right',
|
||||||
render: (amount) => (
|
width: 200,
|
||||||
<span style={{ color: '#f50', fontWeight: 'bold' }}>
|
render: (attributes) => {
|
||||||
¥{amount?.toLocaleString()}
|
// 获取货币符号
|
||||||
</span>
|
const currencySymbol = CURRENCY_SYMBOLS[attributes.currency] || '¥';
|
||||||
),
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-1">
|
||||||
|
<div className="flex justify-between items-center text-sm">
|
||||||
|
<Typography.Text type="secondary" style={{ fontSize: '12px' }}>
|
||||||
|
税前:{currencySymbol}{attributes.beforeTaxAmount?.toLocaleString()}
|
||||||
|
</Typography.Text>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<Statistic
|
||||||
|
value={attributes.afterTaxAmount}
|
||||||
|
prefix={currencySymbol}
|
||||||
|
precision={2}
|
||||||
|
valueStyle={{
|
||||||
|
fontSize: '16px',
|
||||||
|
fontWeight: 600,
|
||||||
|
color: '#1890ff'
|
||||||
|
}}
|
||||||
|
className="!mb-0"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '创建日期',
|
title: '创建日期',
|
||||||
@@ -135,40 +242,164 @@ const QuotationPage = () => {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const getTemplatesByCategory = () => {
|
||||||
|
const groups = new Map();
|
||||||
|
|
||||||
|
// 添加未分类组
|
||||||
|
groups.set('uncategorized', {
|
||||||
|
name: '未分类',
|
||||||
|
templates: templates.filter(t => !t.attributes.category || t.attributes.category.length === 0)
|
||||||
|
});
|
||||||
|
|
||||||
|
// 按分类分组
|
||||||
|
templates.forEach(template => {
|
||||||
|
if (template.attributes.category) {
|
||||||
|
template.attributes.category.forEach(cat => {
|
||||||
|
if (!groups.has(cat.id)) {
|
||||||
|
groups.set(cat.id, {
|
||||||
|
name: cat.name,
|
||||||
|
templates: []
|
||||||
|
});
|
||||||
|
}
|
||||||
|
groups.get(cat.id).templates.push(template);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return Array.from(groups.values()).filter(group => group.templates.length > 0);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card
|
<>
|
||||||
title={
|
<Card
|
||||||
<Space>
|
title={
|
||||||
<span>报价单管理</span>
|
<Space>
|
||||||
<Tag color="blue">{total} 个报价单</Tag>
|
<span>报价单管理</span>
|
||||||
</Space>
|
<Tag color="blue">{total} 个报价单</Tag>
|
||||||
}
|
</Space>
|
||||||
className='h-full w-full overflow-auto'
|
}
|
||||||
extra={
|
className='h-full w-full overflow-auto'
|
||||||
<Button
|
extra={
|
||||||
type="primary"
|
<Button
|
||||||
icon={<PlusOutlined />}
|
type="primary"
|
||||||
onClick={() => navigate('/company/quotaInfo')}
|
icon={<PlusOutlined />}
|
||||||
>
|
onClick={() => setIsModalVisible(true)}
|
||||||
新增报价单
|
>
|
||||||
</Button>
|
新增报价单
|
||||||
}
|
</Button>
|
||||||
>
|
}
|
||||||
<Table
|
>
|
||||||
columns={columns}
|
<Table
|
||||||
dataSource={quotations}
|
columns={columns}
|
||||||
rowKey="id"
|
dataSource={quotations}
|
||||||
loading={loading}
|
rowKey="id"
|
||||||
onChange={handleTableChange}
|
loading={loadingQuotations}
|
||||||
pagination={{
|
onChange={handleTableChange}
|
||||||
...pagination,
|
pagination={{
|
||||||
total,
|
...pagination,
|
||||||
showSizeChanger: true,
|
total,
|
||||||
showQuickJumper: true,
|
showSizeChanger: true,
|
||||||
showTotal: (total) => `共 ${total} 条记录`,
|
showQuickJumper: true,
|
||||||
|
showTotal: (total) => `共 ${total} 条记录`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
title="选择报价单模板"
|
||||||
|
open={isModalVisible}
|
||||||
|
onCancel={() => {
|
||||||
|
setIsModalVisible(false);
|
||||||
|
setSelectedTemplateId(null);
|
||||||
}}
|
}}
|
||||||
/>
|
footer={[
|
||||||
</Card>
|
<Button key="custom" onClick={() => handleConfirm()}>
|
||||||
|
<FileAddOutlined /> 自定义创建
|
||||||
|
</Button>,
|
||||||
|
<Button
|
||||||
|
key="submit"
|
||||||
|
type="primary"
|
||||||
|
disabled={!selectedTemplateId}
|
||||||
|
onClick={handleConfirm}
|
||||||
|
>
|
||||||
|
<AppstoreOutlined /> 使用选中模板
|
||||||
|
</Button>,
|
||||||
|
]}
|
||||||
|
width={800}
|
||||||
|
>
|
||||||
|
{loading ? (
|
||||||
|
<div className="flex justify-center items-center h-[400px]">
|
||||||
|
<Spin size="large" />
|
||||||
|
</div>
|
||||||
|
) : templates.length === 0 ? (
|
||||||
|
<Empty description="暂无可用模板" />
|
||||||
|
) : (
|
||||||
|
<div className="max-h-[600px] overflow-y-auto px-1">
|
||||||
|
{getTemplatesByCategory().map((group, groupIndex) => (
|
||||||
|
<div key={groupIndex} className="mb-6 last:mb-2">
|
||||||
|
<div className="flex items-center gap-2 mb-3">
|
||||||
|
<div className="h-6 w-1 bg-blue-500 rounded-full"></div>
|
||||||
|
<h3 className="text-base font-medium text-gray-700">
|
||||||
|
{group.name}
|
||||||
|
<span className="ml-2 text-sm text-gray-400 font-normal">
|
||||||
|
({group.templates.length})
|
||||||
|
</span>
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-3 gap-3">
|
||||||
|
{group.templates.map(template => (
|
||||||
|
<div
|
||||||
|
key={template.id}
|
||||||
|
className={`
|
||||||
|
p-3 border rounded-lg cursor-pointer transition-all
|
||||||
|
${selectedTemplateId === template.id
|
||||||
|
? 'border-blue-500 bg-blue-50/50'
|
||||||
|
: 'border-gray-200 hover:border-blue-300 hover:bg-gray-50/50'
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
onClick={() => handleTemplateSelect(template.id)}
|
||||||
|
>
|
||||||
|
<div className="flex justify-between items-start gap-2 mb-2">
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<h4 className="font-medium text-gray-800 truncate">
|
||||||
|
{template.attributes.templateName}
|
||||||
|
</h4>
|
||||||
|
<p className="text-xs text-gray-500 mt-1 line-clamp-1">
|
||||||
|
{template.attributes.description || '暂无描述'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<span className="text-red-500 font-medium whitespace-nowrap text-sm">
|
||||||
|
¥{template.attributes.totalAmount?.toLocaleString()}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-1">
|
||||||
|
{template.attributes.sections.map((section, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className="bg-white/80 px-2 py-1 rounded text-xs border border-gray-100"
|
||||||
|
>
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<span className="font-medium text-blue-600 truncate flex-1">
|
||||||
|
{section.sectionName}
|
||||||
|
</span>
|
||||||
|
<span className="text-gray-400 ml-1">
|
||||||
|
{section.items.length}项
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Modal>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
241
src/pages/company/service/classify/index.jsx
Normal file
241
src/pages/company/service/classify/index.jsx
Normal file
@@ -0,0 +1,241 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { Table, Button, Form, Input, Space, message, Popconfirm, Drawer } from 'antd';
|
||||||
|
import { PlusOutlined } from '@ant-design/icons';
|
||||||
|
import { supabase } from '@/config/supabase';
|
||||||
|
|
||||||
|
const CategoryDrawer = () => {
|
||||||
|
const [data, setData] = useState([]);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [editingKey, setEditingKey] = useState('');
|
||||||
|
const [drawerVisible, setDrawerVisible] = useState(false);
|
||||||
|
const [form] = Form.useForm();
|
||||||
|
|
||||||
|
// 获取分类数据
|
||||||
|
const fetchCategories = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const { data: categories, error } = await supabase
|
||||||
|
.from('resources')
|
||||||
|
.select('*')
|
||||||
|
.eq('type', 'categories')
|
||||||
|
.order('created_at', { ascending: false });
|
||||||
|
|
||||||
|
if (error) throw error;
|
||||||
|
setData(categories || []);
|
||||||
|
} catch (error) {
|
||||||
|
message.error('获取分类数据失败');
|
||||||
|
console.error(error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (drawerVisible) {
|
||||||
|
fetchCategories();
|
||||||
|
}
|
||||||
|
}, [drawerVisible]);
|
||||||
|
|
||||||
|
// 新增分类
|
||||||
|
const handleAdd = () => {
|
||||||
|
const newData = {
|
||||||
|
id: Date.now().toString(),
|
||||||
|
attributes: { name: '' },
|
||||||
|
isNew: true
|
||||||
|
};
|
||||||
|
setData([newData, ...data]);
|
||||||
|
setEditingKey(newData.id);
|
||||||
|
form.setFieldsValue(newData.attributes);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 保存分类数据
|
||||||
|
const handleSave = async (record) => {
|
||||||
|
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;
|
||||||
|
} else {
|
||||||
|
// 更新
|
||||||
|
const { error } = await supabase
|
||||||
|
.from('resources')
|
||||||
|
.update({
|
||||||
|
attributes: {
|
||||||
|
name: values.name
|
||||||
|
},
|
||||||
|
updated_at: new Date().toISOString()
|
||||||
|
})
|
||||||
|
.eq('id', record.id);
|
||||||
|
|
||||||
|
if (error) throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
message.success('保存成功');
|
||||||
|
setEditingKey('');
|
||||||
|
fetchCategories();
|
||||||
|
} catch (error) {
|
||||||
|
message.error('保存失败');
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 删除分类
|
||||||
|
const handleDelete = async (record) => {
|
||||||
|
try {
|
||||||
|
const { error } = await supabase
|
||||||
|
.from('resources')
|
||||||
|
.delete()
|
||||||
|
.eq('id', record.id);
|
||||||
|
|
||||||
|
if (error) throw error;
|
||||||
|
message.success('删除成功');
|
||||||
|
fetchCategories();
|
||||||
|
} catch (error) {
|
||||||
|
message.error('删除失败');
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{
|
||||||
|
title: '分类名称',
|
||||||
|
dataIndex: ['attributes', 'name'],
|
||||||
|
width: '60%',
|
||||||
|
render: (text, record) => {
|
||||||
|
const isEditing = record.id === editingKey;
|
||||||
|
return isEditing ? (
|
||||||
|
<Form.Item
|
||||||
|
name="name"
|
||||||
|
style={{ margin: 0 }}
|
||||||
|
rules={[{ required: true, message: '请输入分类名称!' }]}
|
||||||
|
>
|
||||||
|
<Input placeholder="请输入分类名称" />
|
||||||
|
</Form.Item>
|
||||||
|
) : (
|
||||||
|
<span className="text-gray-600">{text}</span>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '操作',
|
||||||
|
width: '40%',
|
||||||
|
render: (_, record) => {
|
||||||
|
const isEditing = record.id === editingKey;
|
||||||
|
return isEditing ? (
|
||||||
|
<Space>
|
||||||
|
<Button
|
||||||
|
type="link"
|
||||||
|
className="text-green-600 hover:text-green-500"
|
||||||
|
onClick={() => handleSave(record)}
|
||||||
|
>
|
||||||
|
保存
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="link"
|
||||||
|
className="text-gray-600 hover:text-gray-500"
|
||||||
|
onClick={() => {
|
||||||
|
setEditingKey('');
|
||||||
|
if (record.isNew) {
|
||||||
|
setData(data.filter(item => item.id !== record.id));
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
取消
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
) : (
|
||||||
|
<Space>
|
||||||
|
<Button
|
||||||
|
type="link"
|
||||||
|
className="text-blue-600 hover:text-blue-500"
|
||||||
|
disabled={editingKey !== ''}
|
||||||
|
onClick={() => {
|
||||||
|
setEditingKey(record.id);
|
||||||
|
form.setFieldsValue({
|
||||||
|
name: record.attributes.name,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
编辑
|
||||||
|
</Button>
|
||||||
|
<Popconfirm
|
||||||
|
title="确认删除"
|
||||||
|
description="确定要删除这个分类吗?"
|
||||||
|
onConfirm={() => handleDelete(record)}
|
||||||
|
okText="确定"
|
||||||
|
cancelText="取消"
|
||||||
|
okButtonProps={{
|
||||||
|
className: "bg-red-500 hover:bg-red-600 border-red-500"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
type="link"
|
||||||
|
className="text-red-600 hover:text-red-500"
|
||||||
|
disabled={editingKey !== ''}
|
||||||
|
>
|
||||||
|
删除
|
||||||
|
</Button>
|
||||||
|
</Popconfirm>
|
||||||
|
</Space>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
onClick={() => setDrawerVisible(true)}
|
||||||
|
icon={<PlusOutlined />}
|
||||||
|
>
|
||||||
|
分类管理
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Drawer
|
||||||
|
title="分类管理"
|
||||||
|
placement="right"
|
||||||
|
width={800}
|
||||||
|
onClose={() => setDrawerVisible(false)}
|
||||||
|
open={drawerVisible}
|
||||||
|
className="category-drawer"
|
||||||
|
>
|
||||||
|
<div className="p-6">
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
onClick={handleAdd}
|
||||||
|
className="mb-4"
|
||||||
|
icon={<PlusOutlined />}
|
||||||
|
>
|
||||||
|
新增分类
|
||||||
|
</Button>
|
||||||
|
<Form form={form}>
|
||||||
|
<Table
|
||||||
|
columns={columns}
|
||||||
|
dataSource={data}
|
||||||
|
rowKey="id"
|
||||||
|
loading={loading}
|
||||||
|
pagination={{
|
||||||
|
pageSize: 10,
|
||||||
|
showTotal: (total) => `共 ${total} 条`,
|
||||||
|
}}
|
||||||
|
className="bg-white rounded-lg shadow"
|
||||||
|
/>
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
|
</Drawer>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CategoryDrawer;
|
||||||
@@ -9,16 +9,20 @@ import {
|
|||||||
Typography,
|
Typography,
|
||||||
message,
|
message,
|
||||||
Select,
|
Select,
|
||||||
|
Modal,
|
||||||
|
Divider,
|
||||||
|
Popconfirm,
|
||||||
} from "antd";
|
} from "antd";
|
||||||
import {
|
import {
|
||||||
PlusOutlined,
|
PlusOutlined,
|
||||||
DeleteOutlined,
|
DeleteOutlined,
|
||||||
ArrowLeftOutlined,
|
ArrowLeftOutlined,
|
||||||
EditOutlined,
|
EditOutlined,
|
||||||
|
CloseOutlined,
|
||||||
|
CheckOutlined,
|
||||||
} from "@ant-design/icons";
|
} from "@ant-design/icons";
|
||||||
import { useNavigate, useParams, useLocation } from "react-router-dom";
|
import { useNavigate, useParams, useLocation } from "react-router-dom";
|
||||||
import { supabase } from "@/config/supabase";
|
import { supabase } from "@/config/supabase";
|
||||||
|
|
||||||
const { Title, Text } = Typography;
|
const { Title, Text } = Typography;
|
||||||
|
|
||||||
const ServiceForm = () => {
|
const ServiceForm = () => {
|
||||||
@@ -34,11 +38,17 @@ const ServiceForm = () => {
|
|||||||
sections: [{ items: [{}] }],
|
sections: [{ items: [{}] }],
|
||||||
currency: "CNY"
|
currency: "CNY"
|
||||||
});
|
});
|
||||||
|
const [templateModalVisible, setTemplateModalVisible] = useState(false);
|
||||||
|
const [categories, setCategories] = useState([]);
|
||||||
|
const [units, setUnits] = useState([]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (id) {
|
if (id) {
|
||||||
fetchServiceTemplate();
|
fetchServiceTemplate();
|
||||||
}
|
}
|
||||||
fetchAvailableSections();
|
fetchAvailableSections();
|
||||||
|
fetchCategories();
|
||||||
|
fetchUnits();
|
||||||
}, [id]);
|
}, [id]);
|
||||||
|
|
||||||
const fetchServiceTemplate = async () => {
|
const fetchServiceTemplate = async () => {
|
||||||
@@ -54,6 +64,7 @@ const ServiceForm = () => {
|
|||||||
const formData = {
|
const formData = {
|
||||||
templateName: data.attributes.templateName,
|
templateName: data.attributes.templateName,
|
||||||
description: data.attributes.description,
|
description: data.attributes.description,
|
||||||
|
category:data.attributes.category.map(v=>v.id),
|
||||||
sections: data.attributes.sections,
|
sections: data.attributes.sections,
|
||||||
};
|
};
|
||||||
form.setFieldsValue(formData);
|
form.setFieldsValue(formData);
|
||||||
@@ -69,15 +80,60 @@ const ServiceForm = () => {
|
|||||||
const fetchAvailableSections = async () => {
|
const fetchAvailableSections = async () => {
|
||||||
try {
|
try {
|
||||||
const { data: sections, error } = await supabase
|
const { data: sections, error } = await supabase
|
||||||
.from("resources")
|
.from('resources')
|
||||||
.select("*")
|
.select('*')
|
||||||
.eq("type", "serviceSection");
|
.eq('type', 'sections')
|
||||||
|
.order('created_at', { ascending: false });
|
||||||
|
|
||||||
if (error) throw error;
|
if (error) throw error;
|
||||||
setAvailableSections(sections);
|
setAvailableSections(sections || []);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("获取服务小节失败:", error);
|
message.error('获取小节模版失败');
|
||||||
message.error("获取服务小节失败");
|
console.error(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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 fetchUnits = async () => {
|
||||||
|
try {
|
||||||
|
const { data: unitsData, error } = await supabase
|
||||||
|
.from('resources')
|
||||||
|
.select('*')
|
||||||
|
.eq('type', 'units')
|
||||||
|
.order('created_at', { ascending: false });
|
||||||
|
|
||||||
|
if (error) throw error;
|
||||||
|
|
||||||
|
const formattedUnits = (unitsData || []).map(unit => ({
|
||||||
|
value: unit.attributes.name,
|
||||||
|
label: unit.attributes.name
|
||||||
|
}));
|
||||||
|
|
||||||
|
setUnits(formattedUnits);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取单位数据失败:', error);
|
||||||
|
message.error('获取单位数据失败');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -92,14 +148,20 @@ const ServiceForm = () => {
|
|||||||
}, 0)
|
}, 0)
|
||||||
);
|
);
|
||||||
}, 0);
|
}, 0);
|
||||||
|
const categoryData = values.category.map(categoryId => {
|
||||||
|
const category = categories.find(c => c.value === categoryId);
|
||||||
|
return {
|
||||||
|
id: categoryId,
|
||||||
|
name: category.label
|
||||||
|
};
|
||||||
|
});
|
||||||
const serviceData = {
|
const serviceData = {
|
||||||
type: "serviceTemplate",
|
type: "serviceTemplate",
|
||||||
attributes: {
|
attributes: {
|
||||||
templateName: values.templateName,
|
templateName: values.templateName,
|
||||||
description: values.description,
|
description: values.description,
|
||||||
sections: values.sections,
|
sections: values.sections,
|
||||||
category: values.category || [], // 添加 category 字段
|
category: categoryData,
|
||||||
totalAmount,
|
totalAmount,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -241,6 +303,243 @@ const ServiceForm = () => {
|
|||||||
setEditingSectionName('');
|
setEditingSectionName('');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 使用选中的模版
|
||||||
|
const handleUseTemplate = (template) => {
|
||||||
|
const currentSections = form.getFieldValue('sections') || [];
|
||||||
|
|
||||||
|
// 确保所有必要的字段都存在
|
||||||
|
const newSection = {
|
||||||
|
sectionName: template.attributes.name,
|
||||||
|
items: (template.attributes.items || []).map(item => ({
|
||||||
|
name: item.name || '',
|
||||||
|
description: item.description || '',
|
||||||
|
price: item.price || 0,
|
||||||
|
quantity: item.quantity || 1,
|
||||||
|
unit: item.unit || '',
|
||||||
|
}))
|
||||||
|
};
|
||||||
|
|
||||||
|
const newSections = [...currentSections, newSection];
|
||||||
|
|
||||||
|
// 更新表单值
|
||||||
|
form.setFieldValue('sections', newSections);
|
||||||
|
|
||||||
|
// 更新 formValues 以触发金额计算
|
||||||
|
setFormValues(prev => ({
|
||||||
|
...prev,
|
||||||
|
sections: newSections
|
||||||
|
}));
|
||||||
|
|
||||||
|
setTemplateModalVisible(false);
|
||||||
|
message.success('套用模版成功');
|
||||||
|
};
|
||||||
|
|
||||||
|
// 创建自定义小节
|
||||||
|
const handleCreateCustom = () => {
|
||||||
|
const currentSections = form.getFieldValue('sections') || [];
|
||||||
|
const newSection = {
|
||||||
|
sectionName: `服务类型 ${currentSections.length + 1}`,
|
||||||
|
items: [{ name: '', description: '', price: 0, quantity: 1, unit: '' }]
|
||||||
|
};
|
||||||
|
const newSections = [...currentSections, newSection];
|
||||||
|
|
||||||
|
// 更新表单值
|
||||||
|
form.setFieldValue('sections', newSections);
|
||||||
|
|
||||||
|
// 更新 formValues 以触发金额计算
|
||||||
|
setFormValues(prev => ({
|
||||||
|
...prev,
|
||||||
|
sections: newSections
|
||||||
|
}));
|
||||||
|
|
||||||
|
setTemplateModalVisible(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 修改新增小节按钮的点击事件处理
|
||||||
|
const handleAddSection = () => {
|
||||||
|
setTemplateModalVisible(true);
|
||||||
|
fetchAvailableSections();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 修改移除小节的处理方法
|
||||||
|
const handleRemoveSection = (sectionIndex) => {
|
||||||
|
const currentSections = form.getFieldValue('sections') || [];
|
||||||
|
const newSections = currentSections.filter((_, index) => index !== sectionIndex);
|
||||||
|
|
||||||
|
// 更新表单值
|
||||||
|
form.setFieldValue('sections', newSections);
|
||||||
|
|
||||||
|
// 更新 formValues 以触发金额计算
|
||||||
|
setFormValues(prev => ({
|
||||||
|
...prev,
|
||||||
|
sections: newSections
|
||||||
|
}));
|
||||||
|
|
||||||
|
message.success('删除成功');
|
||||||
|
};
|
||||||
|
|
||||||
|
// 优化模版选择弹窗内容
|
||||||
|
const renderTemplateModalContent = () => (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
|
{availableSections.map(section => (
|
||||||
|
<div
|
||||||
|
key={section.id}
|
||||||
|
className="group relative bg-white dark:bg-gray-800 rounded-lg shadow-sm
|
||||||
|
border border-gray-200 dark:border-gray-700 hover:shadow-lg
|
||||||
|
transition-all duration-300 cursor-pointer"
|
||||||
|
onClick={() => handleUseTemplate(section)}
|
||||||
|
>
|
||||||
|
<div className="p-6">
|
||||||
|
{/* 标题和项目数量 */}
|
||||||
|
<div className="text-center mb-4">
|
||||||
|
<h3 className="text-lg font-medium text-gray-900 dark:text-gray-100
|
||||||
|
group-hover:text-blue-500 transition-colors">
|
||||||
|
{section.attributes.name}
|
||||||
|
</h3>
|
||||||
|
<div className="text-sm text-gray-500 dark:text-gray-400 mt-1">
|
||||||
|
{section.attributes.items?.length || 0} 个项目
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 项目列表预览 */}
|
||||||
|
<div className="space-y-2 mt-4 border-t dark:border-gray-700 pt-4">
|
||||||
|
{(section.attributes.items || []).slice(0, 3).map((item, index) => (
|
||||||
|
<div key={index} className="flex justify-between items-center">
|
||||||
|
<span className="text-sm text-gray-600 dark:text-gray-300 truncate flex-1">
|
||||||
|
{item.name}
|
||||||
|
</span>
|
||||||
|
<span className="text-sm text-gray-500 dark:text-gray-400 ml-2">
|
||||||
|
{formatCurrency(item.price)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{(section.attributes.items || []).length > 3 && (
|
||||||
|
<div className="text-sm text-gray-500 dark:text-gray-400 text-center">
|
||||||
|
还有 {section.attributes.items.length - 3} 个项目...
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 小节总金额 */}
|
||||||
|
<div className="mt-4 pt-4 border-t dark:border-gray-700 flex justify-between items-center">
|
||||||
|
<span className="text-sm text-gray-600 dark:text-gray-300">总金额</span>
|
||||||
|
<span className="text-base font-medium text-blue-500">
|
||||||
|
{formatCurrency(
|
||||||
|
(section.attributes.items || []).reduce(
|
||||||
|
(sum, item) => sum + (item.price * (item.quantity || 1) || 0),
|
||||||
|
0
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 悬边框效果 */}
|
||||||
|
<div className="absolute inset-0 border-2 border-transparent
|
||||||
|
group-hover:border-blue-500 rounded-lg transition-colors duration-300"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* 选择指示器 */}
|
||||||
|
<div className="absolute top-3 right-3 opacity-0 group-hover:opacity-100
|
||||||
|
transition-opacity duration-300">
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
size="small"
|
||||||
|
className="flex items-center gap-1"
|
||||||
|
icon={<PlusOutlined />}
|
||||||
|
>
|
||||||
|
套用
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{availableSections.length === 0 && (
|
||||||
|
<div className="text-center py-8">
|
||||||
|
<div className="text-gray-500 dark:text-gray-400">
|
||||||
|
暂无可用模版
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Divider className="dark:border-gray-700" />
|
||||||
|
|
||||||
|
<div className="flex justify-center">
|
||||||
|
<Button
|
||||||
|
type="dashed"
|
||||||
|
icon={<PlusOutlined />}
|
||||||
|
onClick={handleCreateCustom}
|
||||||
|
className="w-1/3 border-2 hover:border-blue-400 hover:text-blue-500
|
||||||
|
dark:border-gray-600 dark:text-gray-400 dark:hover:text-blue-400
|
||||||
|
transition-all duration-300"
|
||||||
|
>
|
||||||
|
自定义小节
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderUnitSelect = (itemField, sectionIndex, itemIndex) => {
|
||||||
|
const addUnit = async (unitName) => {
|
||||||
|
try {
|
||||||
|
const { error } = await supabase
|
||||||
|
.from('resources')
|
||||||
|
.insert([{
|
||||||
|
type: 'units',
|
||||||
|
attributes: {
|
||||||
|
name: unitName
|
||||||
|
},
|
||||||
|
schema_version: 1
|
||||||
|
}]);
|
||||||
|
|
||||||
|
if (error) throw error;
|
||||||
|
|
||||||
|
message.success('新增单位成功');
|
||||||
|
await fetchUnits();
|
||||||
|
|
||||||
|
// 自动选中新添加的单位
|
||||||
|
form.setFieldValue(['sections', sectionIndex, 'items', itemIndex, 'unit'], unitName);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
message.error('新增单位失败');
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form.Item
|
||||||
|
{...itemField}
|
||||||
|
name={[itemField.name, "unit"]}
|
||||||
|
className="!mb-0"
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
placeholder="选择或输入单位"
|
||||||
|
options={units}
|
||||||
|
showSearch
|
||||||
|
allowClear
|
||||||
|
onSearch={(value) => {
|
||||||
|
// 当输入的值不在现有选项中时,显示添加选项
|
||||||
|
if (value && !units.find(unit => unit.value === value)) {
|
||||||
|
const newOption = { value, label: `添加 "${value}"` };
|
||||||
|
setUnits([...units, newOption]);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onSelect={(value, option) => {
|
||||||
|
// 如果选择的是新添加的选项
|
||||||
|
if (option.label.startsWith('添加 "')) {
|
||||||
|
addUnit(value);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
filterOption={(input, option) =>
|
||||||
|
(option?.label ?? '').toLowerCase().includes(input.toLowerCase())
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
title={
|
title={
|
||||||
@@ -313,12 +612,8 @@ const ServiceForm = () => {
|
|||||||
showSearch
|
showSearch
|
||||||
allowClear
|
allowClear
|
||||||
mode="tags"
|
mode="tags"
|
||||||
options={[
|
options={categories}
|
||||||
{ value: "VI设计", label: "VI设计" },
|
loading={loading}
|
||||||
{ value: "平面设计", label: "平面设计" },
|
|
||||||
{ value: "网站建设", label: "网站建设" },
|
|
||||||
{ value: "营销推广", label: "营销推广" },
|
|
||||||
]}
|
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
@@ -340,48 +635,42 @@ const ServiceForm = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Form.List name="sections">
|
<Form.List name="sections">
|
||||||
{(fields, { add: addSection, remove: removeSection }) => (
|
{(fields, { add, remove }) => (
|
||||||
<>
|
<>
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{fields.map((field, sectionIndex) => (
|
{fields.map((field, sectionIndex) => (
|
||||||
<Card
|
<Card
|
||||||
key={field.key}
|
key={field.key}
|
||||||
className="!border-gray-200 dark:!border-gray-700 dark:bg-gray-800 hover:shadow-md transition-shadow duration-300"
|
className="!border-gray-200 dark:!border-gray-700
|
||||||
|
dark:bg-gray-800 hover:shadow-md transition-shadow duration-300"
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-between mb-4">
|
<div className="flex items-center justify-between mb-4">
|
||||||
<div className="flex items-center gap-2 ">
|
<div className="flex items-center gap-2">
|
||||||
{editingSectionIndex === sectionIndex ? (
|
{editingSectionIndex === sectionIndex ? (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Input
|
<Input
|
||||||
placeholder="请输入小节名称"
|
value={editingSectionName}
|
||||||
className="!w-[200px]"
|
onChange={(e) => setEditingSectionName(e.target.value)}
|
||||||
value={editingSectionName}
|
onPressEnter={handleSectionNameSave}
|
||||||
onChange={e => setEditingSectionName(e.target.value)}
|
autoFocus
|
||||||
onPressEnter={handleSectionNameSave}
|
className="w-48"
|
||||||
autoFocus
|
/>
|
||||||
/>
|
|
||||||
<Space className="flex items-center gap-2">
|
|
||||||
<Button
|
<Button
|
||||||
type="primary"
|
type="link"
|
||||||
size="small"
|
icon={<CheckOutlined />}
|
||||||
onClick={handleSectionNameSave}
|
onClick={handleSectionNameSave}
|
||||||
>
|
className="text-green-500 hover:text-green-600"
|
||||||
保存
|
/>
|
||||||
</Button>
|
|
||||||
<Button
|
<Button
|
||||||
size="small"
|
type="link"
|
||||||
|
icon={<CloseOutlined />}
|
||||||
onClick={handleSectionNameCancel}
|
onClick={handleSectionNameCancel}
|
||||||
>
|
className="text-red-500 hover:text-red-600"
|
||||||
取消
|
/>
|
||||||
</Button>
|
</div>
|
||||||
</Space>
|
|
||||||
</div>
|
|
||||||
) : (
|
) : (
|
||||||
<>
|
<div className="flex items-center gap-2">
|
||||||
<Text
|
<Text strong className="text-lg dark:text-gray-200">
|
||||||
strong
|
|
||||||
className="text-lg dark:text-gray-200"
|
|
||||||
>
|
|
||||||
{form.getFieldValue([
|
{form.getFieldValue([
|
||||||
"sections",
|
"sections",
|
||||||
sectionIndex,
|
sectionIndex,
|
||||||
@@ -390,7 +679,7 @@ const ServiceForm = () => {
|
|||||||
</Text>
|
</Text>
|
||||||
{(!id || isEdit) && (
|
{(!id || isEdit) && (
|
||||||
<Button
|
<Button
|
||||||
type="text"
|
type="link"
|
||||||
icon={<EditOutlined />}
|
icon={<EditOutlined />}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
handleSectionNameEdit(
|
handleSectionNameEdit(
|
||||||
@@ -399,23 +688,37 @@ const ServiceForm = () => {
|
|||||||
"sections",
|
"sections",
|
||||||
sectionIndex,
|
sectionIndex,
|
||||||
"sectionName",
|
"sectionName",
|
||||||
])
|
]) || `服务类型 ${sectionIndex + 1}`
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
size="small"
|
className="text-gray-400 hover:text-blue-500"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<Text className="text-gray-500 dark:text-gray-400">
|
<div className="flex items-center gap-4">
|
||||||
合计:{" "}
|
|
||||||
{formatCurrency(
|
{(!id || isEdit) && (
|
||||||
calculateSectionTotal(
|
<Popconfirm
|
||||||
formValues?.sections?.[sectionIndex]?.items
|
title="确认删除"
|
||||||
)
|
description="确定要删除这个小节吗?"
|
||||||
|
onConfirm={() => handleRemoveSection(sectionIndex)}
|
||||||
|
okText="确定"
|
||||||
|
cancelText="取消"
|
||||||
|
okButtonProps={{
|
||||||
|
className: "bg-red-500 hover:bg-red-600 border-red-500"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
danger
|
||||||
|
icon={<DeleteOutlined />}
|
||||||
|
className="hover:text-red-500 transition-colors"
|
||||||
|
/>
|
||||||
|
</Popconfirm>
|
||||||
)}
|
)}
|
||||||
</Text>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 表头 */}
|
{/* 表头 */}
|
||||||
@@ -463,13 +766,7 @@ const ServiceForm = () => {
|
|||||||
className="w-full"
|
className="w-full"
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
{renderUnitSelect(itemField, sectionIndex, itemIndex)}
|
||||||
{...itemField}
|
|
||||||
name={[itemField.name, "unit"]}
|
|
||||||
className="!mb-0"
|
|
||||||
>
|
|
||||||
<Input placeholder="单位" />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
<Form.Item
|
||||||
{...itemField}
|
{...itemField}
|
||||||
name={[itemField.name, "price"]}
|
name={[itemField.name, "price"]}
|
||||||
@@ -538,12 +835,14 @@ const ServiceForm = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{(!id || isEdit) && (
|
{(!id || isEdit) && (
|
||||||
<div className="mt-6 flex gap-4 justify-center">
|
<div className="mt-6 flex justify-center">
|
||||||
<Button
|
<Button
|
||||||
type="dashed"
|
type="dashed"
|
||||||
onClick={() => addSection({ items: [{}] })}
|
onClick={handleAddSection}
|
||||||
icon={<PlusOutlined />}
|
icon={<PlusOutlined />}
|
||||||
className="w-1/3 hover:border-blue-400 hover:text-blue-500 dark:border-gray-600 dark:text-gray-400 dark:hover:text-blue-400"
|
className="w-1/3 border-2 hover:border-blue-400 hover:text-blue-500
|
||||||
|
dark:border-gray-600 dark:text-gray-400 dark:hover:text-blue-400
|
||||||
|
transition-all duration-300"
|
||||||
>
|
>
|
||||||
新建小节
|
新建小节
|
||||||
</Button>
|
</Button>
|
||||||
@@ -589,6 +888,25 @@ const ServiceForm = () => {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* 模版选择弹窗 */}
|
||||||
|
<Modal
|
||||||
|
title={
|
||||||
|
<h3 className="text-lg font-medium text-gray-900 dark:text-gray-100">
|
||||||
|
选择小节模版
|
||||||
|
</h3>
|
||||||
|
}
|
||||||
|
open={templateModalVisible}
|
||||||
|
onCancel={() => setTemplateModalVisible(false)}
|
||||||
|
footer={null}
|
||||||
|
width={800}
|
||||||
|
className="dark:bg-gray-800"
|
||||||
|
closeIcon={
|
||||||
|
<CloseOutlined className="text-gray-500 dark:text-gray-400" />
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{renderTemplateModalContent()}
|
||||||
|
</Modal>
|
||||||
</Form>
|
</Form>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,16 +1,35 @@
|
|||||||
import { Card, Table, Button, Space, Input, message, Popconfirm, Form, Tag, Typography } from "antd";
|
import {
|
||||||
import { DownOutlined, UpOutlined,EyeOutlined,EditOutlined } from "@ant-design/icons";
|
Card,
|
||||||
|
Table,
|
||||||
|
Button,
|
||||||
|
Space,
|
||||||
|
Input,
|
||||||
|
message,
|
||||||
|
Popconfirm,
|
||||||
|
Form,
|
||||||
|
Tag,
|
||||||
|
Typography,
|
||||||
|
} from "antd";
|
||||||
|
import {
|
||||||
|
DownOutlined,
|
||||||
|
UpOutlined,
|
||||||
|
EyeOutlined,
|
||||||
|
EditOutlined,
|
||||||
|
} from "@ant-design/icons";
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { supabase } from "@/config/supabase";
|
import { supabase } from "@/config/supabase";
|
||||||
|
import UnitManagement from "@/pages/company/service/unit";
|
||||||
|
import Sections from "@/pages/company/service/sections";
|
||||||
|
import CategoryDrawer from "@/pages/company/service/classify";
|
||||||
const { Paragraph } = Typography;
|
const { Paragraph } = Typography;
|
||||||
|
|
||||||
const ServicePage = () => {
|
const ServicePage = () => {
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [data, setData] = useState([]);
|
const [data, setData] = useState([]);
|
||||||
|
const [categories, setCategories] = useState([]);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [editingKey, setEditingKey] = useState('');
|
const [editingKey, setEditingKey] = useState("");
|
||||||
|
|
||||||
// 获取服务模板列表
|
// 获取服务模板列表
|
||||||
const fetchServices = async () => {
|
const fetchServices = async () => {
|
||||||
@@ -32,19 +51,35 @@ const ServicePage = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 添加获取分类数据的方法
|
||||||
|
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;
|
||||||
|
setCategories(categoriesData || []);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取分类数据失败:', error);
|
||||||
|
message.error('获取分类数据失败');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// 修改保存逻辑
|
// 修改保存逻辑
|
||||||
const handleSaveItem = async (record) => {
|
const handleSaveItem = async (record) => {
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const { serviceId, sectionIndex, itemIndex } = record;
|
const { serviceId, sectionIndex, itemIndex } = record;
|
||||||
|
|
||||||
// 找到当前服务模板
|
// 找到当前服务模板
|
||||||
const currentService = data.find(item => item.id === serviceId);
|
const currentService = data.find((item) => item.id === serviceId);
|
||||||
if (!currentService) {
|
if (!currentService) {
|
||||||
throw new Error('Service not found');
|
throw new Error("Service not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 创建新的 attributes 对象,避免直接修改状态
|
|
||||||
const newAttributes = {
|
const newAttributes = {
|
||||||
...currentService.attributes,
|
...currentService.attributes,
|
||||||
templateName: currentService.attributes.templateName,
|
templateName: currentService.attributes.templateName,
|
||||||
@@ -60,47 +95,57 @@ const ServicePage = () => {
|
|||||||
name: record.name,
|
name: record.name,
|
||||||
price: Number(record.price) || 0,
|
price: Number(record.price) || 0,
|
||||||
quantity: Number(record.quantity) || 0,
|
quantity: Number(record.quantity) || 0,
|
||||||
description: record.description || '',
|
description: record.description || "",
|
||||||
unit: record.unit || ''
|
unit: record.unit || "",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return item;
|
return item;
|
||||||
})
|
}),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return section;
|
return section;
|
||||||
})
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
// 重新计算总金额
|
// 重新计算总金额
|
||||||
newAttributes.totalAmount = newAttributes.sections.reduce((total, section) => {
|
newAttributes.totalAmount = newAttributes.sections.reduce(
|
||||||
return total + section.items.reduce((sectionTotal, item) =>
|
(total, section) => {
|
||||||
sectionTotal + ((Number(item.price) || 0) * (Number(item.quantity) || 0)), 0);
|
return (
|
||||||
}, 0);
|
total +
|
||||||
|
section.items.reduce(
|
||||||
|
(sectionTotal, item) =>
|
||||||
|
sectionTotal +
|
||||||
|
(Number(item.price) || 0) * (Number(item.quantity) || 0),
|
||||||
|
0
|
||||||
|
)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
0
|
||||||
|
);
|
||||||
|
|
||||||
// 调用 supabase 更新数据
|
// 调用 supabase 更新数据
|
||||||
const { error } = await supabase
|
const { error } = await supabase
|
||||||
.from('resources')
|
.from("resources")
|
||||||
.update({
|
.update({
|
||||||
type: "serviceTemplate",
|
type: "serviceTemplate",
|
||||||
attributes: newAttributes
|
attributes: newAttributes,
|
||||||
})
|
})
|
||||||
.eq('id', serviceId);
|
.eq("id", serviceId);
|
||||||
|
|
||||||
if (error) throw error;
|
if (error) throw error;
|
||||||
|
|
||||||
// 更新本地状态
|
// 更新本地状态
|
||||||
setData(prevData => prevData.map(item =>
|
setData((prevData) =>
|
||||||
item.id === serviceId
|
prevData.map((item) =>
|
||||||
? { ...item, attributes: newAttributes }
|
item.id === serviceId ? { ...item, attributes: newAttributes } : item
|
||||||
: item
|
)
|
||||||
));
|
);
|
||||||
|
|
||||||
setEditingKey('');
|
setEditingKey("");
|
||||||
message.success('保存成功');
|
message.success("保存成功");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('保存失败:', error);
|
console.error("保存失败:", error);
|
||||||
message.error('保存失败');
|
message.error("保存失败");
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@@ -111,11 +156,11 @@ const ServicePage = () => {
|
|||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const { serviceId, sectionIndex, itemIndex } = record;
|
const { serviceId, sectionIndex, itemIndex } = record;
|
||||||
|
|
||||||
// 找到目标服务
|
// 找到目标服务
|
||||||
const targetService = data.find(item => item.id === serviceId);
|
const targetService = data.find((item) => item.id === serviceId);
|
||||||
if (!targetService) throw new Error('Service not found');
|
if (!targetService) throw new Error("Service not found");
|
||||||
|
|
||||||
// 创建新的 attributes 对象,避免直接修改状态
|
// 创建新的 attributes 对象,避免直接修改状态
|
||||||
const newAttributes = {
|
const newAttributes = {
|
||||||
...targetService.attributes,
|
...targetService.attributes,
|
||||||
@@ -123,40 +168,51 @@ const ServicePage = () => {
|
|||||||
if (secIdx === sectionIndex) {
|
if (secIdx === sectionIndex) {
|
||||||
return {
|
return {
|
||||||
...section,
|
...section,
|
||||||
items: section.items.filter((_, idx) => idx !== itemIndex)
|
items: section.items.filter((_, idx) => idx !== itemIndex),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return section;
|
return section;
|
||||||
})
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
// 重新计算总金额
|
// 重新计算总金额
|
||||||
newAttributes.totalAmount = newAttributes.sections.reduce((total, section) => {
|
newAttributes.totalAmount = newAttributes.sections.reduce(
|
||||||
return total + section.items.reduce((sectionTotal, item) =>
|
(total, section) => {
|
||||||
sectionTotal + ((Number(item.price) || 0) * (Number(item.quantity) || 0)), 0);
|
return (
|
||||||
}, 0);
|
total +
|
||||||
|
section.items.reduce(
|
||||||
|
(sectionTotal, item) =>
|
||||||
|
sectionTotal +
|
||||||
|
(Number(item.price) || 0) * (Number(item.quantity) || 0),
|
||||||
|
0
|
||||||
|
)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
0
|
||||||
|
);
|
||||||
|
|
||||||
// 调用 supabase 更新数据
|
// 调用 supabase 更新数据
|
||||||
const { error } = await supabase
|
const { error } = await supabase
|
||||||
.from('resources')
|
.from("resources")
|
||||||
.update({
|
|
||||||
attributes: newAttributes
|
.update({
|
||||||
|
attributes: newAttributes,
|
||||||
})
|
})
|
||||||
.eq('id', serviceId);
|
.eq("id", serviceId);
|
||||||
|
|
||||||
if (error) throw error;
|
if (error) throw error;
|
||||||
|
|
||||||
// 更新本地状态
|
// 更新本地状态
|
||||||
setData(prevData => prevData.map(item =>
|
setData((prevData) =>
|
||||||
item.id === serviceId
|
prevData.map((item) =>
|
||||||
? { ...item, attributes: newAttributes }
|
item.id === serviceId ? { ...item, attributes: newAttributes } : item
|
||||||
: item
|
)
|
||||||
));
|
);
|
||||||
|
|
||||||
message.success('删除成功');
|
message.success("删除成功");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('删除失败:', error);
|
console.error("删除失败:", error);
|
||||||
message.error('删<><E588A0><EFBFBD>失败');
|
message.error("删除失败");
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@@ -166,21 +222,21 @@ const ServicePage = () => {
|
|||||||
const handleDeleteService = async (serviceId) => {
|
const handleDeleteService = async (serviceId) => {
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
// 调用 supabase 删除数据
|
// 调用 supabase 删除数据
|
||||||
const { error } = await supabase
|
const { error } = await supabase
|
||||||
.from('resources')
|
.from("resources")
|
||||||
.delete()
|
.delete()
|
||||||
.eq('id', serviceId);
|
.eq("id", serviceId);
|
||||||
|
|
||||||
if (error) throw error;
|
if (error) throw error;
|
||||||
|
|
||||||
// 更新本地状态
|
// 更新本地状态
|
||||||
setData(prevData => prevData.filter(item => item.id !== serviceId));
|
setData((prevData) => prevData.filter((item) => item.id !== serviceId));
|
||||||
message.success('删除成功');
|
message.success("删除成功");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('删除失败:', error);
|
console.error("删除失败:", error);
|
||||||
message.error('删除失败');
|
message.error("删除失败");
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@@ -198,38 +254,39 @@ const ServicePage = () => {
|
|||||||
title: "分类",
|
title: "分类",
|
||||||
dataIndex: ["attributes", "category"],
|
dataIndex: ["attributes", "category"],
|
||||||
key: "category",
|
key: "category",
|
||||||
render: (category) => {
|
render: (categories) => {
|
||||||
if (!category || !Array.isArray(category)) return null;
|
if (!categories || !Array.isArray(categories)) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Paragraph
|
<Paragraph
|
||||||
ellipsis={{
|
ellipsis={{
|
||||||
rows: 1,
|
rows: 1,
|
||||||
tooltip: (
|
tooltip: (
|
||||||
<Space size={[0, 8]} wrap>
|
<Space size={[0, 8]} wrap>
|
||||||
{category.map((cat, index) => (
|
{categories.map((category) => (
|
||||||
<Tag
|
<Tag
|
||||||
size="small"
|
size="small"
|
||||||
key={`${cat}-${index}`}
|
key={category.id}
|
||||||
color="blue"
|
color="blue"
|
||||||
className="px-2 py-1 rounded-md"
|
className="px-2 py-1 rounded-md"
|
||||||
>
|
>
|
||||||
{cat}
|
{category.name}
|
||||||
</Tag>
|
</Tag>
|
||||||
))}
|
))}
|
||||||
</Space>
|
</Space>
|
||||||
)
|
),
|
||||||
}}
|
}}
|
||||||
className="mb-0"
|
className="mb-0"
|
||||||
>
|
>
|
||||||
<Space size={[0, 8]} nowrap>
|
<Space size={[0, 8]} nowrap>
|
||||||
{category.map((cat, index) => (
|
{categories.map((category) => (
|
||||||
<Tag
|
<Tag
|
||||||
size="small"
|
size="small"
|
||||||
key={`${cat}-${index}`}
|
key={category.id}
|
||||||
color="blue"
|
color="blue"
|
||||||
className="px-2 py-1 rounded-md"
|
className="px-2 py-1 rounded-md"
|
||||||
>
|
>
|
||||||
{cat}
|
{category.name}
|
||||||
</Tag>
|
</Tag>
|
||||||
))}
|
))}
|
||||||
</Space>
|
</Space>
|
||||||
@@ -241,18 +298,18 @@ const ServicePage = () => {
|
|||||||
title: "描述",
|
title: "描述",
|
||||||
dataIndex: ["attributes", "description"],
|
dataIndex: ["attributes", "description"],
|
||||||
key: "description",
|
key: "description",
|
||||||
width: 300,
|
className: "min-w-[100px] ",
|
||||||
render: (text) => (
|
render: (text) => (
|
||||||
<Paragraph
|
<Paragraph
|
||||||
ellipsis={{
|
ellipsis={{
|
||||||
rows: 2,
|
rows: 2,
|
||||||
tooltip: text
|
tooltip: text,
|
||||||
}}
|
}}
|
||||||
className="mb-0"
|
className="mb-0"
|
||||||
>
|
>
|
||||||
{text}
|
{text}
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
)
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "总金额",
|
title: "总金额",
|
||||||
@@ -260,7 +317,7 @@ const ServicePage = () => {
|
|||||||
key: "totalAmount",
|
key: "totalAmount",
|
||||||
className: "min-w-[150px] text-right",
|
className: "min-w-[150px] text-right",
|
||||||
render: (amount) => (
|
render: (amount) => (
|
||||||
<span style={{ color: '#f50', fontWeight: 'bold' }}>
|
<span style={{ color: "#f50", fontWeight: "bold" }}>
|
||||||
¥
|
¥
|
||||||
{amount?.toLocaleString("zh-CN", {
|
{amount?.toLocaleString("zh-CN", {
|
||||||
minimumFractionDigits: 2,
|
minimumFractionDigits: 2,
|
||||||
@@ -272,21 +329,25 @@ const ServicePage = () => {
|
|||||||
{
|
{
|
||||||
title: "操作",
|
title: "操作",
|
||||||
key: "action",
|
key: "action",
|
||||||
fixed: 'right',
|
fixed: "right",
|
||||||
width: 220,
|
width: 220,
|
||||||
render: (_, record) => (
|
render: (_, record) => (
|
||||||
<Space onClick={e => e.stopPropagation()}>
|
<Space onClick={(e) => e.stopPropagation()}>
|
||||||
<Button
|
<Button
|
||||||
type="link"
|
type="link"
|
||||||
icon={<EyeOutlined />}
|
icon={<EyeOutlined />}
|
||||||
onClick={() => navigate(`/company/serviceTemplateInfo/${record.id}`)}
|
onClick={() =>
|
||||||
|
navigate(`/company/serviceTemplateInfo/${record.id}`)
|
||||||
|
}
|
||||||
>
|
>
|
||||||
查看
|
查看
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
type="link"
|
type="link"
|
||||||
icon={<EditOutlined />}
|
icon={<EditOutlined />}
|
||||||
onClick={() => navigate(`/company/serviceTemplateInfo/${record.id}?edit=true`)}
|
onClick={() =>
|
||||||
|
navigate(`/company/serviceTemplateInfo/${record.id}?edit=true`)
|
||||||
|
}
|
||||||
>
|
>
|
||||||
编辑
|
编辑
|
||||||
</Button>
|
</Button>
|
||||||
@@ -298,11 +359,7 @@ const ServicePage = () => {
|
|||||||
okButtonProps={{ danger: true }}
|
okButtonProps={{ danger: true }}
|
||||||
onConfirm={() => handleDeleteService(record.id)}
|
onConfirm={() => handleDeleteService(record.id)}
|
||||||
>
|
>
|
||||||
<Button
|
<Button type="link" danger className="text-red-600">
|
||||||
type="link"
|
|
||||||
danger
|
|
||||||
className="text-red-600"
|
|
||||||
>
|
|
||||||
删除
|
删除
|
||||||
</Button>
|
</Button>
|
||||||
</Popconfirm>
|
</Popconfirm>
|
||||||
@@ -326,19 +383,26 @@ const ServicePage = () => {
|
|||||||
name={[record.sectionIndex, "items", record.itemIndex, "name"]}
|
name={[record.sectionIndex, "items", record.itemIndex, "name"]}
|
||||||
className="!mb-0"
|
className="!mb-0"
|
||||||
>
|
>
|
||||||
<Input
|
<Input
|
||||||
value={text}
|
value={text}
|
||||||
onChange={e => {
|
onChange={(e) => {
|
||||||
const newData = [...data];
|
const newData = [...data];
|
||||||
const targetService = newData.find(item => item.id === record.serviceId);
|
const targetService = newData.find(
|
||||||
const targetItem = targetService.attributes.sections[record.sectionIndex].items[record.itemIndex];
|
(item) => item.id === record.serviceId
|
||||||
|
);
|
||||||
|
const targetItem =
|
||||||
|
targetService.attributes.sections[record.sectionIndex].items[
|
||||||
|
record.itemIndex
|
||||||
|
];
|
||||||
targetItem.name = e.target.value;
|
targetItem.name = e.target.value;
|
||||||
setData(newData);
|
setData(newData);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
) : text;
|
) : (
|
||||||
}
|
text
|
||||||
|
);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "描述",
|
title: "描述",
|
||||||
@@ -349,16 +413,25 @@ const ServicePage = () => {
|
|||||||
const isEditing = record.key === editingKey;
|
const isEditing = record.key === editingKey;
|
||||||
return isEditing ? (
|
return isEditing ? (
|
||||||
<Form.Item
|
<Form.Item
|
||||||
name={[record.sectionIndex, "items", record.itemIndex, "description"]}
|
name={[
|
||||||
|
record.sectionIndex,
|
||||||
|
"items",
|
||||||
|
record.itemIndex,
|
||||||
|
"description",
|
||||||
|
]}
|
||||||
className="!mb-0"
|
className="!mb-0"
|
||||||
>
|
>
|
||||||
<Input.TextArea
|
<Input
|
||||||
rows={1}
|
value={text}
|
||||||
value={text}
|
onChange={(e) => {
|
||||||
onChange={e => {
|
|
||||||
const newData = [...data];
|
const newData = [...data];
|
||||||
const targetService = newData.find(item => item.id === record.serviceId);
|
const targetService = newData.find(
|
||||||
const targetItem = targetService.attributes.sections[record.sectionIndex].items[record.itemIndex];
|
(item) => item.id === record.serviceId
|
||||||
|
);
|
||||||
|
const targetItem =
|
||||||
|
targetService.attributes.sections[record.sectionIndex].items[
|
||||||
|
record.itemIndex
|
||||||
|
];
|
||||||
targetItem.description = e.target.value;
|
targetItem.description = e.target.value;
|
||||||
setData(newData);
|
setData(newData);
|
||||||
}}
|
}}
|
||||||
@@ -368,14 +441,14 @@ const ServicePage = () => {
|
|||||||
<Paragraph
|
<Paragraph
|
||||||
ellipsis={{
|
ellipsis={{
|
||||||
rows: 2,
|
rows: 2,
|
||||||
tooltip: text
|
tooltip: text,
|
||||||
}}
|
}}
|
||||||
className="mb-0"
|
className="mb-0"
|
||||||
>
|
>
|
||||||
{text}
|
{text}
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "单价",
|
title: "单价",
|
||||||
@@ -385,26 +458,32 @@ const ServicePage = () => {
|
|||||||
render: (price, record) => {
|
render: (price, record) => {
|
||||||
const isEditing = record.key === editingKey;
|
const isEditing = record.key === editingKey;
|
||||||
return isEditing ? (
|
return isEditing ? (
|
||||||
<Input
|
<Input
|
||||||
type="number"
|
type="number"
|
||||||
value={price}
|
value={price}
|
||||||
onChange={e => {
|
onChange={(e) => {
|
||||||
const newData = [...data];
|
const newData = [...data];
|
||||||
const targetService = newData.find(item => item.id === record.serviceId);
|
const targetService = newData.find(
|
||||||
const targetItem = targetService.attributes.sections[record.sectionIndex].items[record.itemIndex];
|
(item) => item.id === record.serviceId
|
||||||
|
);
|
||||||
|
const targetItem =
|
||||||
|
targetService.attributes.sections[record.sectionIndex].items[
|
||||||
|
record.itemIndex
|
||||||
|
];
|
||||||
targetItem.price = Number(e.target.value);
|
targetItem.price = Number(e.target.value);
|
||||||
setData(newData);
|
setData(newData);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<span className="text-gray-600">
|
<span className="text-gray-600">
|
||||||
¥{price?.toLocaleString("zh-CN", {
|
¥
|
||||||
|
{price?.toLocaleString("zh-CN", {
|
||||||
minimumFractionDigits: 2,
|
minimumFractionDigits: 2,
|
||||||
maximumFractionDigits: 2,
|
maximumFractionDigits: 2,
|
||||||
})}
|
})}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "数量",
|
title: "数量",
|
||||||
@@ -414,19 +493,26 @@ const ServicePage = () => {
|
|||||||
render: (quantity, record) => {
|
render: (quantity, record) => {
|
||||||
const isEditing = record.key === editingKey;
|
const isEditing = record.key === editingKey;
|
||||||
return isEditing ? (
|
return isEditing ? (
|
||||||
<Input
|
<Input
|
||||||
type="number"
|
type="number"
|
||||||
value={quantity}
|
value={quantity}
|
||||||
onChange={e => {
|
onChange={(e) => {
|
||||||
const newData = [...data];
|
const newData = [...data];
|
||||||
const targetService = newData.find(item => item.id === record.serviceId);
|
const targetService = newData.find(
|
||||||
const targetItem = targetService.attributes.sections[record.sectionIndex].items[record.itemIndex];
|
(item) => item.id === record.serviceId
|
||||||
|
);
|
||||||
|
const targetItem =
|
||||||
|
targetService.attributes.sections[record.sectionIndex].items[
|
||||||
|
record.itemIndex
|
||||||
|
];
|
||||||
targetItem.quantity = Number(e.target.value);
|
targetItem.quantity = Number(e.target.value);
|
||||||
setData(newData);
|
setData(newData);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
) : <span className="text-gray-600">{quantity}</span>;
|
) : (
|
||||||
}
|
<span className="text-gray-600">{quantity}</span>
|
||||||
|
);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "小计",
|
title: "小计",
|
||||||
@@ -446,37 +532,39 @@ const ServicePage = () => {
|
|||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '操作',
|
title: "操作",
|
||||||
key: 'action',
|
key: "action",
|
||||||
width: "150px",
|
width: "150px",
|
||||||
render: (_, record) => {
|
render: (_, record) => {
|
||||||
const isEditing = record.key === editingKey;
|
const isEditing = record.key === editingKey;
|
||||||
return isEditing ? (
|
return isEditing ? (
|
||||||
<Space>
|
<Space>
|
||||||
<Button
|
<Button
|
||||||
type="link"
|
type="link"
|
||||||
size="small"
|
size="small"
|
||||||
className="text-green-600"
|
className="text-green-600"
|
||||||
onClick={() => handleSaveItem(record, record.sectionIndex, record.itemIndex)}
|
onClick={() =>
|
||||||
|
handleSaveItem(record, record.sectionIndex, record.itemIndex)
|
||||||
|
}
|
||||||
>
|
>
|
||||||
保存
|
保存
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
type="link"
|
type="link"
|
||||||
size="small"
|
size="small"
|
||||||
className="text-gray-600"
|
className="text-gray-600"
|
||||||
onClick={() => setEditingKey('')}
|
onClick={() => setEditingKey("")}
|
||||||
>
|
>
|
||||||
取消
|
取消
|
||||||
</Button>
|
</Button>
|
||||||
</Space>
|
</Space>
|
||||||
) : (
|
) : (
|
||||||
<Space>
|
<Space>
|
||||||
<Button
|
<Button
|
||||||
type="link"
|
type="link"
|
||||||
size="small"
|
size="small"
|
||||||
className="text-blue-600"
|
className="text-blue-600"
|
||||||
disabled={editingKey !== ''}
|
disabled={editingKey !== ""}
|
||||||
onClick={() => setEditingKey(record.key)}
|
onClick={() => setEditingKey(record.key)}
|
||||||
>
|
>
|
||||||
编辑
|
编辑
|
||||||
@@ -489,19 +577,19 @@ const ServicePage = () => {
|
|||||||
okButtonProps={{ danger: true }}
|
okButtonProps={{ danger: true }}
|
||||||
onConfirm={() => handleDeleteItem(record)}
|
onConfirm={() => handleDeleteItem(record)}
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
type="link"
|
type="link"
|
||||||
size="small"
|
size="small"
|
||||||
className="text-red-600"
|
className="text-red-600"
|
||||||
disabled={editingKey !== ''}
|
disabled={editingKey !== ""}
|
||||||
>
|
>
|
||||||
删除
|
删除
|
||||||
</Button>
|
</Button>
|
||||||
</Popconfirm>
|
</Popconfirm>
|
||||||
</Space>
|
</Space>
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
// 修改 expandedRowRender 函数,确保 key 的唯一性
|
// 修改 expandedRowRender 函数,确保 key 的唯一性
|
||||||
@@ -520,8 +608,9 @@ const ServicePage = () => {
|
|||||||
总计: ¥
|
总计: ¥
|
||||||
{section.items
|
{section.items
|
||||||
.reduce(
|
.reduce(
|
||||||
(total, item) =>
|
(total, item) =>
|
||||||
total + (Number(item.price) || 0) * (Number(item.quantity) || 0),
|
total +
|
||||||
|
(Number(item.price) || 0) * (Number(item.quantity) || 0),
|
||||||
0
|
0
|
||||||
)
|
)
|
||||||
.toLocaleString("zh-CN", {
|
.toLocaleString("zh-CN", {
|
||||||
@@ -532,11 +621,11 @@ const ServicePage = () => {
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
headStyle={{
|
headStyle={{
|
||||||
background: 'rgba(59, 130, 246, 0.05)',
|
background: "rgba(59, 130, 246, 0.05)",
|
||||||
borderBottom: '1px solid rgba(59, 130, 246, 0.1)'
|
borderBottom: "1px solid rgba(59, 130, 246, 0.1)",
|
||||||
}}
|
}}
|
||||||
bodyStyle={{
|
bodyStyle={{
|
||||||
background: 'rgba(59, 130, 246, 0.02)'
|
background: "rgba(59, 130, 246, 0.02)",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Table
|
<Table
|
||||||
@@ -546,7 +635,7 @@ const ServicePage = () => {
|
|||||||
key: `${record.id}-${sectionIndex}-${itemIndex}`,
|
key: `${record.id}-${sectionIndex}-${itemIndex}`,
|
||||||
serviceId: record.id,
|
serviceId: record.id,
|
||||||
sectionIndex,
|
sectionIndex,
|
||||||
itemIndex
|
itemIndex,
|
||||||
}))}
|
}))}
|
||||||
pagination={false}
|
pagination={false}
|
||||||
className="rounded-lg overflow-hidden"
|
className="rounded-lg overflow-hidden"
|
||||||
@@ -559,6 +648,7 @@ const ServicePage = () => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchServices();
|
fetchServices();
|
||||||
|
fetchCategories();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -567,12 +657,18 @@ const ServicePage = () => {
|
|||||||
title={<span className="text-xl font-medium">服务模版管理</span>}
|
title={<span className="text-xl font-medium">服务模版管理</span>}
|
||||||
className="h-full w-full overflow-auto"
|
className="h-full w-full overflow-auto"
|
||||||
extra={
|
extra={
|
||||||
<Button
|
<Space>
|
||||||
type="primary"
|
<Sections />
|
||||||
onClick={() => navigate("/company/serviceTemplateInfo")}
|
<UnitManagement />
|
||||||
>
|
<CategoryDrawer />
|
||||||
新建模版
|
|
||||||
</Button>
|
<Button
|
||||||
|
type="primary"
|
||||||
|
onClick={() => navigate("/company/serviceTemplateInfo")}
|
||||||
|
>
|
||||||
|
新建模版
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Table
|
<Table
|
||||||
@@ -587,29 +683,34 @@ const ServicePage = () => {
|
|||||||
expandIcon: ({ expanded, onExpand, record }) => (
|
expandIcon: ({ expanded, onExpand, record }) => (
|
||||||
<Button
|
<Button
|
||||||
type="link"
|
type="link"
|
||||||
onClick={e => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
onExpand(record, e);
|
onExpand(record, e);
|
||||||
}}
|
}}
|
||||||
icon={expanded ? <UpOutlined /> : <DownOutlined />}
|
icon={expanded ? <UpOutlined /> : <DownOutlined />}
|
||||||
className={`
|
className={`
|
||||||
transition-all duration-300
|
transition-all duration-300
|
||||||
${expanded
|
${
|
||||||
? 'text-blue-600'
|
expanded
|
||||||
: 'text-gray-400 hover:text-blue-600'
|
? "text-blue-600"
|
||||||
|
: "text-gray-400 hover:text-blue-600"
|
||||||
}
|
}
|
||||||
`}
|
`}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
rowExpandable: record => record.attributes.sections?.length > 0,
|
rowExpandable: (record) => record.attributes.sections?.length > 0,
|
||||||
}}
|
}}
|
||||||
className="rounded-lg overflow-hidden"
|
className="rounded-lg overflow-hidden"
|
||||||
rowClassName={(record, index) => `
|
rowClassName={(record, index) => `
|
||||||
cursor-pointer transition-colors
|
cursor-pointer transition-colors
|
||||||
${index % 2 === 0 ? 'bg-white dark:bg-gray-800' : 'bg-gray-50 dark:bg-gray-750'}
|
${
|
||||||
|
index % 2 === 0
|
||||||
|
? "bg-white dark:bg-gray-800"
|
||||||
|
: "bg-gray-50 dark:bg-gray-750"
|
||||||
|
}
|
||||||
hover:bg-blue-50 dark:hover:bg-blue-900/20
|
hover:bg-blue-50 dark:hover:bg-blue-900/20
|
||||||
`}
|
`}
|
||||||
expandedRowClassName={() => 'bg-blue-50/50 dark:bg-blue-900/10'}
|
expandedRowClassName={() => "bg-blue-50/50 dark:bg-blue-900/10"}
|
||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
446
src/pages/company/service/sections/index.jsx
Normal file
446
src/pages/company/service/sections/index.jsx
Normal file
@@ -0,0 +1,446 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { Table, Button, Drawer, Modal, Form, Input, InputNumber, Space, message, Popconfirm, Select } from 'antd';
|
||||||
|
import { PlusOutlined, DeleteOutlined, EditOutlined } from '@ant-design/icons';
|
||||||
|
import { supabase } from '@/config/supabase';
|
||||||
|
|
||||||
|
const SectionManagement = () => {
|
||||||
|
const [data, setData] = useState([]);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [drawerVisible, setDrawerVisible] = useState(false);
|
||||||
|
const [modalVisible, setModalVisible] = useState(false);
|
||||||
|
const [editingRecord, setEditingRecord] = useState(null);
|
||||||
|
const [form] = Form.useForm();
|
||||||
|
const [units, setUnits] = useState([]);
|
||||||
|
|
||||||
|
// 获取子模块数据
|
||||||
|
const fetchSections = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const { data: sections, error } = await supabase
|
||||||
|
.from('resources')
|
||||||
|
.select('*')
|
||||||
|
.eq('type', 'sections');
|
||||||
|
|
||||||
|
if (error) throw error;
|
||||||
|
setData(sections || []);
|
||||||
|
} catch (error) {
|
||||||
|
message.error('获取子模块数据失败');
|
||||||
|
console.error(error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取单位数据
|
||||||
|
const fetchUnits = async () => {
|
||||||
|
try {
|
||||||
|
const { data: unitsData, error } = await supabase
|
||||||
|
.from('resources')
|
||||||
|
.select('*')
|
||||||
|
.eq('type', 'units')
|
||||||
|
.order('created_at', { ascending: false });
|
||||||
|
|
||||||
|
if (error) throw error;
|
||||||
|
setUnits(unitsData || []);
|
||||||
|
} catch (error) {
|
||||||
|
message.error('获取单位数据失败');
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (drawerVisible) {
|
||||||
|
fetchSections();
|
||||||
|
}
|
||||||
|
fetchUnits();
|
||||||
|
}, [drawerVisible]);
|
||||||
|
|
||||||
|
// 打开新增/编辑模态框
|
||||||
|
const showModal = async (record = null) => {
|
||||||
|
setModalVisible(true);
|
||||||
|
setEditingRecord(record);
|
||||||
|
|
||||||
|
if (record) {
|
||||||
|
try {
|
||||||
|
const { data: section, error } = await supabase
|
||||||
|
.from('resources')
|
||||||
|
.select('*')
|
||||||
|
.eq('id', record.id)
|
||||||
|
.single();
|
||||||
|
|
||||||
|
if (error) throw error;
|
||||||
|
|
||||||
|
form.setFieldsValue({
|
||||||
|
name: section.attributes.name,
|
||||||
|
items: section.attributes.items
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
message.error('获取子模块详情失败');
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
form.setFieldsValue({
|
||||||
|
name: '',
|
||||||
|
items: [{ name: '', description: '', price: 0, quantity: 1, unit: '' }]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 保存子模块数据
|
||||||
|
const handleSave = async (values) => {
|
||||||
|
try {
|
||||||
|
if (editingRecord) {
|
||||||
|
// 更新
|
||||||
|
const { error } = await supabase
|
||||||
|
.from('resources')
|
||||||
|
.update({
|
||||||
|
attributes: {
|
||||||
|
name: values.name,
|
||||||
|
items: values.items
|
||||||
|
},
|
||||||
|
updated_at: new Date().toISOString()
|
||||||
|
})
|
||||||
|
.eq('id', editingRecord.id);
|
||||||
|
|
||||||
|
if (error) throw error;
|
||||||
|
} else {
|
||||||
|
// 新增
|
||||||
|
const { error } = await supabase
|
||||||
|
.from('resources')
|
||||||
|
.insert([{
|
||||||
|
type: 'sections',
|
||||||
|
attributes: {
|
||||||
|
name: values.name,
|
||||||
|
items: values.items
|
||||||
|
},
|
||||||
|
schema_version: 1
|
||||||
|
}]);
|
||||||
|
|
||||||
|
if (error) throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
message.success('保存成功');
|
||||||
|
setModalVisible(false);
|
||||||
|
form.resetFields();
|
||||||
|
fetchSections();
|
||||||
|
} catch (error) {
|
||||||
|
message.error('保存失败');
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 删除子模块
|
||||||
|
const handleDelete = async (id) => {
|
||||||
|
try {
|
||||||
|
const { error } = await supabase
|
||||||
|
.from('resources')
|
||||||
|
.delete()
|
||||||
|
.eq('id', id);
|
||||||
|
|
||||||
|
if (error) throw error;
|
||||||
|
message.success('删除成功');
|
||||||
|
fetchSections();
|
||||||
|
} catch (error) {
|
||||||
|
message.error('删除失败');
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Drawer 中表格的列定义
|
||||||
|
const drawerColumns = [
|
||||||
|
{
|
||||||
|
title: '项目名称',
|
||||||
|
dataIndex: 'name',
|
||||||
|
width: '20%',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '描述',
|
||||||
|
dataIndex: 'description',
|
||||||
|
width: '25%',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '单价',
|
||||||
|
dataIndex: 'price',
|
||||||
|
width: '15%',
|
||||||
|
render: (price) => `¥${price}`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '数量',
|
||||||
|
dataIndex: 'quantity',
|
||||||
|
width: '15%',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '单位',
|
||||||
|
dataIndex: 'unit',
|
||||||
|
width: '15%',
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
// 添加模态框内表格列定义
|
||||||
|
const modalColumns = [
|
||||||
|
{
|
||||||
|
title: '项目名称',
|
||||||
|
dataIndex: 'name',
|
||||||
|
width: '20%',
|
||||||
|
render: (_, __, index) => (
|
||||||
|
<Form.Item
|
||||||
|
name={[index, 'name']}
|
||||||
|
style={{ margin: 0 }}
|
||||||
|
>
|
||||||
|
<Input placeholder="项目名称" />
|
||||||
|
</Form.Item>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '描述',
|
||||||
|
dataIndex: 'description',
|
||||||
|
width: '25%',
|
||||||
|
render: (_, __, index) => (
|
||||||
|
<Form.Item
|
||||||
|
name={[index, 'description']}
|
||||||
|
style={{ margin: 0 }}
|
||||||
|
>
|
||||||
|
<Input placeholder="描述" />
|
||||||
|
</Form.Item>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '单价',
|
||||||
|
dataIndex: 'price',
|
||||||
|
width: '15%',
|
||||||
|
render: (_, __, index) => (
|
||||||
|
<Form.Item
|
||||||
|
name={[index, 'price']}
|
||||||
|
rules={[{ required: true, message: '请输入单价!' }]}
|
||||||
|
style={{ margin: 0 }}
|
||||||
|
>
|
||||||
|
<InputNumber
|
||||||
|
min={0}
|
||||||
|
placeholder="单价"
|
||||||
|
className="w-full"
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '数量',
|
||||||
|
dataIndex: 'quantity',
|
||||||
|
width: '15%',
|
||||||
|
render: (_, __, index) => (
|
||||||
|
<Form.Item
|
||||||
|
name={[index, 'quantity']}
|
||||||
|
rules={[{ required: true, message: '请输入数量!' }]}
|
||||||
|
style={{ margin: 0 }}
|
||||||
|
>
|
||||||
|
<InputNumber
|
||||||
|
min={1}
|
||||||
|
placeholder="数量"
|
||||||
|
className="w-full"
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '单位',
|
||||||
|
dataIndex: 'unit',
|
||||||
|
width: '15%',
|
||||||
|
render: (_, __, index) => (
|
||||||
|
<Form.Item
|
||||||
|
name={[index, 'unit']}
|
||||||
|
rules={[{ required: true, message: '请选择或输入单位!' }]}
|
||||||
|
style={{ margin: 0 }}
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
placeholder="请选择或输入单位"
|
||||||
|
className="w-full"
|
||||||
|
showSearch
|
||||||
|
allowClear
|
||||||
|
options={units.map(unit => ({
|
||||||
|
label: unit.attributes.name,
|
||||||
|
value: unit.attributes.name
|
||||||
|
}))}
|
||||||
|
filterOption={(input, option) =>
|
||||||
|
(option?.label ?? '').toLowerCase().includes(input.toLowerCase())
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '操作',
|
||||||
|
width: '10%',
|
||||||
|
render: (_, __, index, { remove }) => (
|
||||||
|
<Button
|
||||||
|
type="link"
|
||||||
|
danger
|
||||||
|
icon={<DeleteOutlined />}
|
||||||
|
onClick={() => remove(index)}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
icon={<PlusOutlined />}
|
||||||
|
onClick={() => setDrawerVisible(true)}
|
||||||
|
className="flex items-center"
|
||||||
|
>
|
||||||
|
子模块管理
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Drawer
|
||||||
|
title={
|
||||||
|
<span className="text-lg font-medium text-gray-800 dark:text-gray-200">
|
||||||
|
子模块管理
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
placement="right"
|
||||||
|
width={1000}
|
||||||
|
onClose={() => setDrawerVisible(false)}
|
||||||
|
open={drawerVisible}
|
||||||
|
className="dark:bg-gray-800"
|
||||||
|
>
|
||||||
|
<div className="flex flex-col h-full">
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
icon={<PlusOutlined />}
|
||||||
|
onClick={() => showModal()}
|
||||||
|
className="mb-4 w-32 flex items-center justify-center"
|
||||||
|
>
|
||||||
|
新增子模块
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<div className="space-y-6">
|
||||||
|
{data.map((section) => (
|
||||||
|
<div
|
||||||
|
key={section.id}
|
||||||
|
className="bg-white rounded-lg shadow-sm dark:bg-gray-800 border border-gray-200 dark:border-gray-700"
|
||||||
|
>
|
||||||
|
<div className="p-4 border-b border-gray-200 dark:border-gray-700 flex justify-between items-center">
|
||||||
|
<h3 className="text-lg font-medium text-gray-800 dark:text-gray-200">
|
||||||
|
{section.attributes.name}
|
||||||
|
</h3>
|
||||||
|
<Space>
|
||||||
|
<Button
|
||||||
|
type="link"
|
||||||
|
icon={<EditOutlined />}
|
||||||
|
onClick={() => showModal(section)}
|
||||||
|
className="text-blue-600 hover:text-blue-500"
|
||||||
|
>
|
||||||
|
编辑
|
||||||
|
</Button>
|
||||||
|
<Popconfirm
|
||||||
|
title="确定要删除吗?"
|
||||||
|
onConfirm={() => handleDelete(section.id)}
|
||||||
|
okButtonProps={{
|
||||||
|
className: "bg-red-500 hover:bg-red-600 border-red-500"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
type="link"
|
||||||
|
danger
|
||||||
|
icon={<DeleteOutlined />}
|
||||||
|
className="text-red-600 hover:text-red-500"
|
||||||
|
/>
|
||||||
|
</Popconfirm>
|
||||||
|
</Space>
|
||||||
|
</div>
|
||||||
|
<div className="p-4">
|
||||||
|
<Table
|
||||||
|
dataSource={section.attributes.items}
|
||||||
|
columns={drawerColumns}
|
||||||
|
pagination={false}
|
||||||
|
rowKey={(record, index) => `${section.id}-${index}`}
|
||||||
|
className="border dark:border-gray-700 rounded-lg"
|
||||||
|
rowClassName="hover:bg-gray-50 dark:hover:bg-gray-700/50"
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Drawer>
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
title={`${editingRecord ? '编辑' : '新增'}子模块`}
|
||||||
|
open={modalVisible}
|
||||||
|
onCancel={() => {
|
||||||
|
setModalVisible(false);
|
||||||
|
setEditingRecord(null);
|
||||||
|
form.resetFields();
|
||||||
|
}}
|
||||||
|
footer={null}
|
||||||
|
width={1200}
|
||||||
|
destroyOnClose={true}
|
||||||
|
>
|
||||||
|
<Form
|
||||||
|
form={form}
|
||||||
|
onFinish={handleSave}
|
||||||
|
layout="vertical"
|
||||||
|
className="mt-4"
|
||||||
|
>
|
||||||
|
<Form.Item
|
||||||
|
name="name"
|
||||||
|
label="子模块名称"
|
||||||
|
rules={[{ required: true, message: '请输入子模块名称!' }]}
|
||||||
|
>
|
||||||
|
<Input placeholder="请输入子模块名称" />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.List name="items">
|
||||||
|
{(fields, { add, remove }) => (
|
||||||
|
<div className="bg-white rounded-lg border dark:bg-gray-800 dark:border-gray-700">
|
||||||
|
<Table
|
||||||
|
dataSource={fields}
|
||||||
|
columns={modalColumns.map(col => ({
|
||||||
|
...col,
|
||||||
|
render: (...args) => col.render(...args, { remove })
|
||||||
|
}))}
|
||||||
|
pagination={false}
|
||||||
|
rowKey="key"
|
||||||
|
className="mb-4"
|
||||||
|
/>
|
||||||
|
<div className="p-4 border-t dark:border-gray-700">
|
||||||
|
<Button
|
||||||
|
type="dashed"
|
||||||
|
onClick={() => add({
|
||||||
|
name: '',
|
||||||
|
description: '',
|
||||||
|
price: 0,
|
||||||
|
quantity: 1,
|
||||||
|
unit: ''
|
||||||
|
})}
|
||||||
|
icon={<PlusOutlined />}
|
||||||
|
className="w-full hover:border-blue-400 hover:text-blue-500
|
||||||
|
dark:border-gray-600 dark:text-gray-400 dark:hover:text-blue-400"
|
||||||
|
>
|
||||||
|
添加项目
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Form.List>
|
||||||
|
|
||||||
|
<div className="flex justify-end gap-4 mt-6">
|
||||||
|
<Button onClick={() => {
|
||||||
|
setModalVisible(false);
|
||||||
|
setEditingRecord(null);
|
||||||
|
form.resetFields();
|
||||||
|
}}>
|
||||||
|
取消
|
||||||
|
</Button>
|
||||||
|
<Button type="primary" htmlType="submit">
|
||||||
|
保存
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Form>
|
||||||
|
</Modal>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SectionManagement;
|
||||||
262
src/pages/company/service/unit/index.jsx
Normal file
262
src/pages/company/service/unit/index.jsx
Normal file
@@ -0,0 +1,262 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { Table, Button, Drawer, Form, Input, Space, message, Popconfirm } from 'antd';
|
||||||
|
import { PlusOutlined, DeleteOutlined, SaveOutlined, CloseOutlined } from '@ant-design/icons';
|
||||||
|
import { supabase } from '@/config/supabase';
|
||||||
|
|
||||||
|
const UnitManagement = () => {
|
||||||
|
const [data, setData] = useState([]);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [drawerVisible, setDrawerVisible] = useState(false);
|
||||||
|
const [editingKey, setEditingKey] = useState('');
|
||||||
|
const [form] = Form.useForm();
|
||||||
|
|
||||||
|
// 获取单位数据
|
||||||
|
const fetchUnits = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const { data: units, error } = await supabase
|
||||||
|
.from('resources')
|
||||||
|
.select('*')
|
||||||
|
.eq('type', 'units')
|
||||||
|
.order('created_at', { ascending: false });
|
||||||
|
|
||||||
|
if (error) throw error;
|
||||||
|
setData(units || []);
|
||||||
|
} catch (error) {
|
||||||
|
message.error('获取单位数据失败');
|
||||||
|
console.error(error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (drawerVisible) {
|
||||||
|
fetchUnits();
|
||||||
|
}
|
||||||
|
}, [drawerVisible]);
|
||||||
|
|
||||||
|
// 新增<E696B0><E5A29E><EFBFBD>位
|
||||||
|
const handleAdd = () => {
|
||||||
|
const newData = {
|
||||||
|
id: Date.now().toString(),
|
||||||
|
attributes: { name: '' },
|
||||||
|
isNew: true
|
||||||
|
};
|
||||||
|
setData([newData, ...data]);
|
||||||
|
setEditingKey(newData.id);
|
||||||
|
form.setFieldsValue(newData.attributes);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 保存单位数据
|
||||||
|
const handleSave = async (record) => {
|
||||||
|
try {
|
||||||
|
const values = await form.validateFields();
|
||||||
|
if (record.isNew) {
|
||||||
|
// 新增
|
||||||
|
const { error } = await supabase
|
||||||
|
.from('resources')
|
||||||
|
.insert([{
|
||||||
|
type: 'units',
|
||||||
|
attributes: {
|
||||||
|
name: values.name
|
||||||
|
},
|
||||||
|
schema_version: 1
|
||||||
|
}]);
|
||||||
|
|
||||||
|
if (error) throw error;
|
||||||
|
} else {
|
||||||
|
// 更新
|
||||||
|
const { error } = await supabase
|
||||||
|
.from('resources')
|
||||||
|
.update({
|
||||||
|
attributes: {
|
||||||
|
name: values.name
|
||||||
|
},
|
||||||
|
updated_at: new Date().toISOString()
|
||||||
|
})
|
||||||
|
.eq('id', record.id);
|
||||||
|
|
||||||
|
if (error) throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
message.success('保存成功');
|
||||||
|
setEditingKey('');
|
||||||
|
fetchUnits();
|
||||||
|
} catch (error) {
|
||||||
|
message.error('保存失败');
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 删除单位
|
||||||
|
const handleDelete = async (id) => {
|
||||||
|
try {
|
||||||
|
const { error } = await supabase
|
||||||
|
.from('resources')
|
||||||
|
.delete()
|
||||||
|
.eq('id', id);
|
||||||
|
|
||||||
|
if (error) throw error;
|
||||||
|
message.success('删除成功');
|
||||||
|
fetchUnits();
|
||||||
|
} catch (error) {
|
||||||
|
message.error('删除失败');
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 修改取消编辑的处理方法
|
||||||
|
const handleCancel = (record) => {
|
||||||
|
setEditingKey('');
|
||||||
|
if (record.isNew) {
|
||||||
|
// 如果是新增的记录,直接从数据中移除
|
||||||
|
setData(data.filter(item => item.id !== record.id));
|
||||||
|
}
|
||||||
|
form.resetFields();
|
||||||
|
};
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{
|
||||||
|
title: '单位名称',
|
||||||
|
dataIndex: ['attributes', 'name'],
|
||||||
|
width: '60%',
|
||||||
|
render: (text, record) => {
|
||||||
|
const isEditing = record.id === editingKey;
|
||||||
|
return isEditing ? (
|
||||||
|
<Form.Item
|
||||||
|
name="name"
|
||||||
|
rules={[{ required: true, message: '请输入单位名称!' }]}
|
||||||
|
style={{ margin: 0 }}
|
||||||
|
initialValue={text}
|
||||||
|
>
|
||||||
|
<Input placeholder="请输入单位名称" />
|
||||||
|
</Form.Item>
|
||||||
|
) : (
|
||||||
|
text
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '操作',
|
||||||
|
width: '40%',
|
||||||
|
render: (_, record) => {
|
||||||
|
const isEditing = record.id === editingKey;
|
||||||
|
return isEditing ? (
|
||||||
|
<Space>
|
||||||
|
<Button
|
||||||
|
type="link"
|
||||||
|
icon={<SaveOutlined />}
|
||||||
|
onClick={() => handleSave(record)}
|
||||||
|
className="text-green-600 hover:text-green-500"
|
||||||
|
>
|
||||||
|
保存
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="link"
|
||||||
|
icon={<CloseOutlined />}
|
||||||
|
onClick={() => handleCancel(record)}
|
||||||
|
className="text-gray-600 hover:text-gray-500"
|
||||||
|
>
|
||||||
|
取消
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
) : (
|
||||||
|
<Space>
|
||||||
|
<Button
|
||||||
|
type="link"
|
||||||
|
disabled={editingKey !== ''}
|
||||||
|
onClick={() => {
|
||||||
|
setEditingKey(record.id);
|
||||||
|
form.setFieldsValue(record.attributes);
|
||||||
|
}}
|
||||||
|
className="text-blue-600 hover:text-blue-500 disabled:text-gray-400"
|
||||||
|
>
|
||||||
|
编辑
|
||||||
|
</Button>
|
||||||
|
<Popconfirm
|
||||||
|
title="确定要删除吗?"
|
||||||
|
onConfirm={() => handleDelete(record.id)}
|
||||||
|
okButtonProps={{
|
||||||
|
className: "bg-red-500 hover:bg-red-600 border-red-500"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
type="link"
|
||||||
|
danger
|
||||||
|
disabled={editingKey !== ''}
|
||||||
|
icon={<DeleteOutlined />}
|
||||||
|
className="text-red-600 hover:text-red-500 disabled:text-gray-400"
|
||||||
|
/>
|
||||||
|
</Popconfirm>
|
||||||
|
</Space>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
icon={<PlusOutlined />}
|
||||||
|
onClick={() => setDrawerVisible(true)}
|
||||||
|
className="flex items-center"
|
||||||
|
>
|
||||||
|
单位管理
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Drawer
|
||||||
|
title={
|
||||||
|
<span className="text-lg font-medium text-gray-800 dark:text-gray-200">
|
||||||
|
单位管理
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
placement="right"
|
||||||
|
width={600}
|
||||||
|
onClose={() => {
|
||||||
|
setDrawerVisible(false);
|
||||||
|
setEditingKey('');
|
||||||
|
form.resetFields();
|
||||||
|
}}
|
||||||
|
open={drawerVisible}
|
||||||
|
className="dark:bg-gray-800"
|
||||||
|
>
|
||||||
|
<div className="flex flex-col h-full">
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
icon={<PlusOutlined />}
|
||||||
|
onClick={handleAdd}
|
||||||
|
disabled={editingKey !== ''}
|
||||||
|
className="mb-4 w-32 flex items-center justify-center"
|
||||||
|
>
|
||||||
|
新增单位
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Form form={form} className="flex-1">
|
||||||
|
<Table
|
||||||
|
dataSource={data}
|
||||||
|
columns={columns}
|
||||||
|
rowKey="id"
|
||||||
|
loading={loading}
|
||||||
|
pagination={{
|
||||||
|
pageSize: 10,
|
||||||
|
className: "dark:text-gray-300"
|
||||||
|
}}
|
||||||
|
className="bg-white dark:bg-gray-800 rounded-lg shadow-sm"
|
||||||
|
rowClassName={(record) =>
|
||||||
|
`transition-colors ${
|
||||||
|
record.id === editingKey
|
||||||
|
? 'bg-blue-50 dark:bg-blue-900/20'
|
||||||
|
: 'hover:bg-gray-50 dark:hover:bg-gray-700/50'
|
||||||
|
}`
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
|
</Drawer>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default UnitManagement;
|
||||||
@@ -1,5 +1,15 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "es2015"
|
"target": "es2015",
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["src/*"],
|
||||||
|
"@components/*": ["src/components/*"],
|
||||||
|
"@pages/*": ["src/pages/*"],
|
||||||
|
"@utils/*": ["src/utils/*"],
|
||||||
|
"@config/*": ["src/config/*"],
|
||||||
|
"@contexts/*": ["src/contexts/*"],
|
||||||
|
"@assets/*": ["src/assets/*"]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2,18 +2,30 @@ import { defineConfig } from 'vite';
|
|||||||
import react from '@vitejs/plugin-react';
|
import react from '@vitejs/plugin-react';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
|
||||||
|
// 使用 __dirname 需要添加 import.meta.url 的支持
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = path.dirname(__filename);
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [react()],
|
plugins: [react()],
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
'@': path.resolve(__dirname, './src'),
|
'@': path.resolve(__dirname, 'src'),
|
||||||
|
'@components': path.resolve(__dirname, 'src/components'),
|
||||||
|
'@pages': path.resolve(__dirname, 'src/pages'),
|
||||||
|
'@utils': path.resolve(__dirname, 'src/utils'),
|
||||||
|
'@config': path.resolve(__dirname, 'src/config'),
|
||||||
|
'@contexts': path.resolve(__dirname, 'src/contexts'),
|
||||||
|
'@assets': path.resolve(__dirname, 'src/assets'),
|
||||||
},
|
},
|
||||||
|
extensions: ['.js', '.jsx', '.ts', '.tsx', '.json'] // 添加文件扩展名自动解析
|
||||||
},
|
},
|
||||||
server: {
|
server: {
|
||||||
port: 3000, // 设置开发服务器端口为 3000
|
port: 3000,
|
||||||
open: true, // 自动打开浏览器
|
open: true,
|
||||||
host: true, // 监听所有地址,包括局域网和公网地址
|
host: true,
|
||||||
strictPort: true, // 如果端口被占用,直接退出
|
strictPort: true,
|
||||||
},
|
},
|
||||||
build: {
|
build: {
|
||||||
outDir: 'dist',
|
outDir: 'dist',
|
||||||
|
|||||||
Reference in New Issue
Block a user