专案完成
This commit is contained in:
544
src/pages/company/service/itemsManange/sections/project.jsx
Normal file
544
src/pages/company/service/itemsManange/sections/project.jsx
Normal file
@@ -0,0 +1,544 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Table,
|
||||
Button,
|
||||
Form,
|
||||
Input,
|
||||
Space,
|
||||
message,
|
||||
Popconfirm,
|
||||
Select,
|
||||
Divider,
|
||||
Card,
|
||||
Typography,
|
||||
Upload,
|
||||
Modal
|
||||
} from 'antd';
|
||||
import {
|
||||
PlusOutlined,
|
||||
DeleteOutlined,
|
||||
InboxOutlined,
|
||||
UploadOutlined
|
||||
} from '@ant-design/icons';
|
||||
import { supabaseService } from '@/hooks/supabaseService';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { supabase } from "@/config/supabase";
|
||||
|
||||
const { Text } = Typography;
|
||||
const TYPE = 'project';
|
||||
|
||||
const ProjectSections = () => {
|
||||
const [data, setData] = useState([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [editingKey, setEditingKey] = useState('');
|
||||
const [form] = Form.useForm();
|
||||
const [loadingUnits, setLoadingUnits] = useState(false);
|
||||
const [units, setUnits] = useState([]);
|
||||
const [formValues, setFormValues] = useState({});
|
||||
const [uploadModalVisible, setUploadModalVisible] = useState(false);
|
||||
const [currentAddItem, setCurrentAddItem] = useState(null);
|
||||
|
||||
const fetchSections = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const { data: sections } = await supabaseService.select('resources', {
|
||||
filter: {
|
||||
'type': { eq: 'sections' },
|
||||
'attributes->>template_type': { eq: TYPE }
|
||||
},
|
||||
order: {
|
||||
column: 'created_at',
|
||||
ascending: false
|
||||
}
|
||||
});
|
||||
setData(sections || []);
|
||||
} catch (error) {
|
||||
message.error('获取模块数据失败');
|
||||
console.error(error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchUnits = async () => {
|
||||
setLoadingUnits(true);
|
||||
try {
|
||||
const { data: units } = await supabaseService.select("resources", {
|
||||
filter: {
|
||||
type: { eq: "units" },
|
||||
"attributes->>template_type": { in: `(${TYPE})` },
|
||||
},
|
||||
order: {
|
||||
column: "created_at",
|
||||
ascending: false,
|
||||
},
|
||||
});
|
||||
setUnits(units || []);
|
||||
} catch (error) {
|
||||
message.error("获取资源类型列表失败");
|
||||
console.error(error);
|
||||
} finally {
|
||||
setLoadingUnits(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchSections();
|
||||
fetchUnits();
|
||||
}, []);
|
||||
|
||||
const handleAdd = () => {
|
||||
const newData = {
|
||||
id: Date.now().toString(),
|
||||
attributes: {
|
||||
name: '',
|
||||
template_type: TYPE,
|
||||
items: [{
|
||||
key: uuidv4(),
|
||||
url: '',
|
||||
title: '',
|
||||
unit: '',
|
||||
description: ''
|
||||
}]
|
||||
},
|
||||
isNew: true
|
||||
};
|
||||
setData([newData, ...data]);
|
||||
setEditingKey(newData.id);
|
||||
form.setFieldsValue(newData.attributes);
|
||||
};
|
||||
|
||||
const handleSave = async (record) => {
|
||||
try {
|
||||
const values = await form.validateFields();
|
||||
const items = form.getFieldValue(['items']) || [];
|
||||
|
||||
if (!items.length || !items.some(item => item.title)) {
|
||||
message.error('请至少添加一个有效的资源项目');
|
||||
return;
|
||||
}
|
||||
|
||||
const formattedItems = items.filter(item => item.title).map(item => ({
|
||||
url: item.url,
|
||||
title: item.title,
|
||||
unit: item.unit,
|
||||
description: item.description
|
||||
}));
|
||||
|
||||
if (record.isNew) {
|
||||
await supabaseService.insert('resources', {
|
||||
type: 'sections',
|
||||
attributes: {
|
||||
name: values.name,
|
||||
template_type: TYPE,
|
||||
items: formattedItems,
|
||||
},
|
||||
schema_version: 1
|
||||
});
|
||||
} else {
|
||||
await supabaseService.update('resources',
|
||||
{ id: record.id },
|
||||
{
|
||||
attributes: {
|
||||
name: values.name,
|
||||
template_type: TYPE,
|
||||
items: formattedItems,
|
||||
},
|
||||
updated_at: new Date().toISOString()
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
message.success('保存成功');
|
||||
setEditingKey('');
|
||||
fetchSections();
|
||||
} catch (error) {
|
||||
message.error('保存失败');
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async (record) => {
|
||||
try {
|
||||
await supabaseService.delete('resources', { id: record.id });
|
||||
message.success('删除成功');
|
||||
fetchSections();
|
||||
} catch (error) {
|
||||
message.error('删除失败');
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddUnit = async (unitName) => {
|
||||
try {
|
||||
await supabaseService.insert("resources", {
|
||||
type: "units",
|
||||
attributes: {
|
||||
name: unitName,
|
||||
template_type: TYPE,
|
||||
},
|
||||
schema_version: 1,
|
||||
});
|
||||
|
||||
message.success("新增资源类型成功");
|
||||
fetchUnits();
|
||||
return true;
|
||||
} catch (error) {
|
||||
message.error("新增资源类型失败");
|
||||
console.error(error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleValuesChange = (changedValues, allValues) => {
|
||||
setFormValues(allValues);
|
||||
};
|
||||
|
||||
const handleFileUpload = async (file, addItem) => {
|
||||
try {
|
||||
const fileExt = file.name.split('.').pop();
|
||||
const fileName = `${Date.now()}.${fileExt}`;
|
||||
|
||||
const { data, error } = await supabase.storage
|
||||
.from('file')
|
||||
.upload(filePath, file);
|
||||
|
||||
if (error) throw error;
|
||||
|
||||
const { data: { publicUrl } } = supabase.storage
|
||||
.from('file')
|
||||
.getPublicUrl(fileName);
|
||||
|
||||
let fileType = '';
|
||||
if (file.type.startsWith('image/')) {
|
||||
fileType = '图片';
|
||||
} else if (file.type.startsWith('video/')) {
|
||||
fileType = '视频';
|
||||
} else if (file.type.includes('pdf')) {
|
||||
fileType = 'PDF文档';
|
||||
} else {
|
||||
fileType = '其他文件';
|
||||
}
|
||||
|
||||
addItem({
|
||||
key: uuidv4(),
|
||||
url: publicUrl,
|
||||
title: `${fileType}-${file.name}`,
|
||||
unit: fileType,
|
||||
description: '',
|
||||
});
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
message.error('文件上传失败');
|
||||
console.error(error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const renderUploadModal = () => (
|
||||
<Modal
|
||||
title="上传资源文件"
|
||||
open={uploadModalVisible}
|
||||
onCancel={() => setUploadModalVisible(false)}
|
||||
footer={null}
|
||||
>
|
||||
<Upload.Dragger
|
||||
multiple
|
||||
customRequest={async ({ file, onSuccess, onError }) => {
|
||||
try {
|
||||
const result = await handleFileUpload(file, currentAddItem);
|
||||
if (result) {
|
||||
onSuccess();
|
||||
} else {
|
||||
onError();
|
||||
}
|
||||
} catch (err) {
|
||||
onError();
|
||||
}
|
||||
}}
|
||||
onChange={(info) => {
|
||||
if (info.file.status === 'done') {
|
||||
message.success(`${info.file.name} 上传成功`);
|
||||
} else if (info.file.status === 'error') {
|
||||
message.error(`${info.file.name} 上传失败`);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<p className="ant-upload-drag-icon">
|
||||
<InboxOutlined />
|
||||
</p>
|
||||
<p className="ant-upload-text">点击或拖拽文件到此区域上传</p>
|
||||
<p className="ant-upload-hint">支持单个或批量上传</p>
|
||||
</Upload.Dragger>
|
||||
</Modal>
|
||||
);
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: '模块名称',
|
||||
dataIndex: ['attributes', 'name'],
|
||||
width: 200,
|
||||
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="font-medium">{text}</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '资源项目',
|
||||
dataIndex: ['attributes', 'items'],
|
||||
render: (items, record) => {
|
||||
const isEditing = record.id === editingKey;
|
||||
if (isEditing) {
|
||||
return (
|
||||
<Form.List name="items">
|
||||
{(fields, { add, remove }) => (
|
||||
<div className="space-y-4">
|
||||
{fields.map((field, index) => (
|
||||
<Card key={field.key} size="small" className="bg-gray-50">
|
||||
<div className="grid grid-cols-5 gap-4">
|
||||
<Form.Item
|
||||
{...field}
|
||||
name={[field.name, 'url']}
|
||||
rules={[{ required: true, message: '请输入资源链接' }]}
|
||||
className="mb-0"
|
||||
>
|
||||
<Input placeholder="资源链接" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
{...field}
|
||||
name={[field.name, 'title']}
|
||||
rules={[{ required: true, message: '请输入资源标题' }]}
|
||||
className="mb-0"
|
||||
>
|
||||
<Input placeholder="资源标题" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
{...field}
|
||||
name={[field.name, 'unit']}
|
||||
className="mb-0"
|
||||
>
|
||||
<Select
|
||||
placeholder="资源类型"
|
||||
loading={loadingUnits}
|
||||
showSearch
|
||||
allowClear
|
||||
options={units.map((unit) => ({
|
||||
label: unit.attributes.name,
|
||||
value: unit.attributes.name,
|
||||
}))}
|
||||
dropdownRender={(menu) => (
|
||||
<>
|
||||
{menu}
|
||||
<Divider style={{ margin: '8px 0' }} />
|
||||
<div style={{ padding: '4px 8px' }}>
|
||||
<Input.Search
|
||||
placeholder="输入新类型"
|
||||
enterButton={<PlusOutlined />}
|
||||
onSearch={async (value) => {
|
||||
if (!value.trim()) return;
|
||||
if (await handleAddUnit(value.trim())) {
|
||||
const currentItems = form.getFieldValue('items');
|
||||
currentItems[index].unit = value.trim();
|
||||
form.setFieldValue('items', currentItems);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
{...field}
|
||||
name={[field.name, 'description']}
|
||||
className="mb-0"
|
||||
>
|
||||
<Input placeholder="资源描述" />
|
||||
</Form.Item>
|
||||
|
||||
<Button
|
||||
type="text"
|
||||
danger
|
||||
icon={<DeleteOutlined />}
|
||||
onClick={() => remove(field.name)}
|
||||
disabled={fields.length === 1}
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
type="dashed"
|
||||
onClick={() => add({
|
||||
key: uuidv4(),
|
||||
url: '',
|
||||
title: '',
|
||||
unit: '',
|
||||
description: ''
|
||||
})}
|
||||
icon={<PlusOutlined />}
|
||||
className="w-full hover:border-blue-400 hover:text-blue-500"
|
||||
>
|
||||
手动添加资源
|
||||
</Button>
|
||||
<Button
|
||||
type="dashed"
|
||||
onClick={() => {
|
||||
setCurrentAddItem(() => add);
|
||||
setUploadModalVisible(true);
|
||||
}}
|
||||
icon={<UploadOutlined />}
|
||||
className="w-full hover:border-blue-400 hover:text-blue-500"
|
||||
>
|
||||
上传文件
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Form.List>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
{(items || []).map((item, index) => (
|
||||
<div key={index} className="flex items-center gap-4 text-sm">
|
||||
<a href={item.url} target="_blank" rel="noopener noreferrer" className="text-blue-600">
|
||||
{item.title}
|
||||
</a>
|
||||
<span className="text-gray-500">{item.unit || '未设置类型'}</span>
|
||||
{item.description && (
|
||||
<span className="text-gray-500">({item.description})</span>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
width: 160,
|
||||
fixed: 'right',
|
||||
render: (_, record) => {
|
||||
const isEditing = record.id === editingKey;
|
||||
return isEditing ? (
|
||||
<Space>
|
||||
<Button
|
||||
type="link"
|
||||
className="text-green-600 hover:text-green-500 font-medium"
|
||||
onClick={() => handleSave(record)}
|
||||
>
|
||||
保存
|
||||
</Button>
|
||||
<Button
|
||||
type="link"
|
||||
className="text-gray-600 hover:text-gray-500 font-medium"
|
||||
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 font-medium"
|
||||
disabled={editingKey !== ''}
|
||||
onClick={() => {
|
||||
setEditingKey(record.id);
|
||||
form.setFieldsValue({
|
||||
name: record.attributes.name,
|
||||
items: record.attributes.items
|
||||
});
|
||||
}}
|
||||
>
|
||||
编辑
|
||||
</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 font-medium"
|
||||
disabled={editingKey !== ''}
|
||||
>
|
||||
删除
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="p-6">
|
||||
<div className="rounded-lg shadow-sm mb-6 p-4">
|
||||
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={handleAdd}
|
||||
icon={<PlusOutlined />}
|
||||
className="bg-blue-600 hover:bg-blue-700 border-0 shadow-sm h-10"
|
||||
>
|
||||
新增模块
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded-lg shadow-sm">
|
||||
<Form
|
||||
form={form}
|
||||
onValuesChange={handleValuesChange}
|
||||
>
|
||||
<Table
|
||||
scroll={{ x: 1200 }}
|
||||
columns={columns}
|
||||
dataSource={data}
|
||||
rowKey="id"
|
||||
loading={loading}
|
||||
pagination={{
|
||||
pageSize: 10,
|
||||
showTotal: (total) => `共 ${total} 条`,
|
||||
className: "px-4"
|
||||
}}
|
||||
className="rounded-lg"
|
||||
/>
|
||||
</Form>
|
||||
</div>
|
||||
{renderUploadModal()}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProjectSections;
|
||||
Reference in New Issue
Block a user