专案优化

This commit is contained in:
‘Liammcl’
2025-01-04 21:47:24 +08:00
parent a3860a1dd0
commit a9746b8ceb
3 changed files with 257 additions and 214 deletions

View File

@@ -11,8 +11,7 @@ import {
message, message,
Select, Select,
DatePicker, DatePicker,
Typography, Typography
Spin
} from 'antd'; } from 'antd';
import { import {
ArrowLeftOutlined, ArrowLeftOutlined,
@@ -259,179 +258,177 @@ export default function ProjectDetail() {
return ( return (
<div className="bg-gradient-to-b from-gray-50 to-white dark:from-gray-800 dark:to-gray-900/90 min-h-screen p-2"> <div className="bg-gradient-to-b from-gray-50 to-white dark:from-gray-800 dark:to-gray-900/90 min-h-screen p-2">
<Spin spinning={loading}> <Card
<Card className="shadow-lg rounded-lg border-0"
className="shadow-lg rounded-lg border-0" title={
title={ <div className="flex justify-between items-center py-2">
<div className="flex justify-between items-center py-2"> <div className="flex items-center space-x-3">
<div className="flex items-center space-x-3"> <Title level={4} className="mb-0 text-gray-800">
<Title level={4} className="mb-0 text-gray-800"> {id ? (isEdit ? "编辑专案" : "查看专案") : "新建专案"}
{id ? (isEdit ? "编辑专案" : "查看专案") : "新建专案"} </Title>
</Title> <span className="text-gray-400 text-sm">
<span className="text-gray-400 text-sm"> {id
{id ? isEdit
? isEdit ? "请修改专案信息"
? "请修改专案信息" : "专案详情"
: "专案详情" : "请填写专案信息"}
: "请填写专案信息"} </span>
</span>
</div>
<Space size="middle">
<Button
icon={<ArrowLeftOutlined />}
onClick={() => navigate("/company/project")}
>
返回
</Button>
{!isView && (
<Button
type="primary"
icon={<SaveOutlined />}
onClick={() => form.submit()}
loading={loading}
>
保存
</Button>
)}
</Space>
</div> </div>
} <Space size="middle">
> <Button
<Form icon={<ArrowLeftOutlined />}
form={form} onClick={() => navigate("/company/project")}
onFinish={onFinish}
onValuesChange={handleValuesChange}
layout="vertical"
disabled={isView}
initialValues={initialValues}
>
<Card
className="shadow-sm rounded-lg"
type="inner"
title={
<span className="flex items-center space-x-2 text-gray-700">
<span className="w-1 h-4 bg-blue-500 rounded-full" />
<span>基本信息</span>
</span>
}
bordered={false}
>
<div className="grid grid-cols-2 gap-8">
<Form.Item
name="projectName"
label={<span className="text-gray-700 font-medium">专案名称</span>}
rules={[{ required: true, message: "请输入专案名称" }]}
>
<Input
placeholder="请输入专案名称"
className="hover:border-blue-400 focus:border-blue-500"
/>
</Form.Item>
<Form.Item
name="customers"
label={<span className="text-gray-700 font-medium">客户名称</span>}
>
<Select
mode="multiple"
placeholder="请选择客户"
className="hover:border-blue-400 focus:border-blue-500"
showSearch
optionFilterProp="children"
filterOption={(input, option) =>
(option?.label ?? "").toLowerCase().includes(input.toLowerCase())
}
options={customers.map((customer) => ({
value: customer.id,
label: customer.attributes.name,
}))}
/>
</Form.Item>
<Form.Item
name="timeRange"
label={<span className="text-gray-700 font-medium">时间范围</span>}
>
<DatePicker.RangePicker
className="w-full hover:border-blue-400 focus:border-blue-500"
format="YYYY-MM-DD"
onChange={(dates) => {
if (dates) {
form.setFieldValue('timeRange', [
dayjs(dates[0]),
dayjs(dates[1])
]);
} else {
form.setFieldValue('timeRange', null);
}
}}
/>
</Form.Item>
<Form.Item
name="relatedTasks"
label={<span className="text-gray-700 font-medium">关联任务</span>}
>
<Select
mode="multiple"
placeholder="请选择关联任务"
className="hover:border-blue-400 focus:border-blue-500"
loading={loadingTasks}
showSearch
optionFilterProp="children"
filterOption={(input, option) =>
(option?.label ?? "").toLowerCase().includes(input.toLowerCase())
}
options={tasks.map((task) => ({
value: task.id,
label: task.attributes.taskName,
}))}
/>
</Form.Item>
</div>
<Form.Item
name="description"
label={<span className="text-gray-700 font-medium">专案描述</span>}
> >
<TextArea 返回
rows={4} </Button>
placeholder="请输入专案描述" {!isView && (
<Button
type="primary"
icon={<SaveOutlined />}
onClick={() => form.submit()}
loading={loading}
>
保存
</Button>
)}
</Space>
</div>
}
>
<Form
form={form}
onFinish={onFinish}
onValuesChange={handleValuesChange}
layout="vertical"
disabled={isView}
initialValues={initialValues}
>
<Card
className="shadow-sm rounded-lg"
type="inner"
title={
<span className="flex items-center space-x-2 text-gray-700">
<span className="w-1 h-4 bg-blue-500 rounded-full" />
<span>基本信息</span>
</span>
}
bordered={false}
>
<div className="grid grid-cols-2 gap-8">
<Form.Item
name="projectName"
label={<span className="text-gray-700 font-medium">专案名称</span>}
rules={[{ required: true, message: "请输入专案名称" }]}
>
<Input
placeholder="请输入专案名称"
className="hover:border-blue-400 focus:border-blue-500" className="hover:border-blue-400 focus:border-blue-500"
/> />
</Form.Item> </Form.Item>
</Card>
<Card <Form.Item
className="shadow-sm rounded-lg mt-6" name="customers"
type="inner" label={<span className="text-gray-700 font-medium">客户名称</span>}
title={ >
<span className="flex items-center space-x-2 text-gray-700"> <Select
<span className="w-1 h-4 bg-blue-500 rounded-full" /> mode="multiple"
<span>专案资源</span> placeholder="请选择客户"
</span> className="hover:border-blue-400 focus:border-blue-500"
showSearch
optionFilterProp="children"
filterOption={(input, option) =>
(option?.label ?? "").toLowerCase().includes(input.toLowerCase())
}
options={customers.map((customer) => ({
value: customer.id,
label: customer.attributes.name,
}))}
/>
</Form.Item>
<Form.Item
name="timeRange"
label={<span className="text-gray-700 font-medium">时间范围</span>}
>
<DatePicker.RangePicker
className="w-full hover:border-blue-400 focus:border-blue-500"
format="YYYY-MM-DD"
onChange={(dates) => {
if (dates) {
form.setFieldValue('timeRange', [
dayjs(dates[0]),
dayjs(dates[1])
]);
} else {
form.setFieldValue('timeRange', null);
}
}}
/>
</Form.Item>
<Form.Item
name="relatedTasks"
label={<span className="text-gray-700 font-medium">关联任务</span>}
>
<Select
mode="multiple"
placeholder="请选择关联任务"
className="hover:border-blue-400 focus:border-blue-500"
loading={loadingTasks}
showSearch
optionFilterProp="children"
filterOption={(input, option) =>
(option?.label ?? "").toLowerCase().includes(input.toLowerCase())
} }
bordered={false} options={tasks.map((task) => ({
value: task.id,
label: task.attributes.taskName,
}))}
/>
</Form.Item>
</div>
<Form.Item
name="description"
label={<span className="text-gray-700 font-medium">专案描述</span>}
> >
<ProjectResourceList <TextArea
type={TYPE} rows={4}
form={form} placeholder="请输入专案描述"
isView={isView} className="hover:border-blue-400 focus:border-blue-500"
formValues={formValues}
onValuesChange={handleValuesChange}
/> />
{/* <TaskList type={TYPE} </Form.Item>
form={form} </Card>
isView={isView}
formValues={formValues} <Card
onValuesChange={handleValuesChange} /> */} className="shadow-sm rounded-lg mt-6"
</Card> type="inner"
title={
<span className="flex items-center space-x-2 text-gray-700">
<span className="w-1 h-4 bg-blue-500 rounded-full" />
<span>专案资源</span>
</span>
}
bordered={false}
>
<ProjectResourceList
type={TYPE}
form={form}
isView={isView}
formValues={formValues}
onValuesChange={handleValuesChange}
/>
{/* <TaskList type={TYPE}
form={form}
isView={isView}
formValues={formValues}
onValuesChange={handleValuesChange} /> */}
</Card>
</Form> </Form>
</Card> </Card>
</Spin>
</div> </div>
); );
} }

View File

@@ -1,10 +1,11 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { Card, Table, Button, message, Popconfirm, Tag, Space, Select } from 'antd'; import { Card, Table, Button, message, Popconfirm, Tag, Space, Select, Tooltip } from 'antd';
import { PlusOutlined, EditOutlined, DeleteOutlined, EyeOutlined } from '@ant-design/icons'; import { PlusOutlined, EditOutlined, DeleteOutlined, EyeOutlined, ShareAltOutlined } 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 { supabase } from "@/config/supabase"; import { supabase } from "@/config/supabase";
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { useCopyToClipboard } from 'react-use';
const ProjectPage = () => { const ProjectPage = () => {
const navigate = useNavigate(); const navigate = useNavigate();
@@ -19,6 +20,7 @@ const ProjectPage = () => {
const [tasks, setTasks] = useState([]); const [tasks, setTasks] = useState([]);
const [loadingTasks, setLoadingTasks] = useState(false); const [loadingTasks, setLoadingTasks] = useState(false);
const [selectedTask, setSelectedTask] = useState(null); const [selectedTask, setSelectedTask] = useState(null);
const [state, copyToClipboard] = useCopyToClipboard();
const { const {
resources: projects, resources: projects,
@@ -164,46 +166,67 @@ const ProjectPage = () => {
title: "操作", title: "操作",
key: "action", key: "action",
fixed: "right", fixed: "right",
render: (_, record) => ( render: (_, record) => {
<Space size={0} className="dark:text-gray-300"> return (
<Button <Space size={0} className="dark:text-gray-300">
size="small"
type="link"
icon={<EyeOutlined />}
onClick={() => navigate(`/company/projectView/${record.id}`)}
className="dark:text-gray-300 dark:hover:text-blue-400"
>
查看
</Button>
<Button
size="small"
type="link"
icon={<EditOutlined />}
onClick={() => navigate(`/company/projectInfo/${record.id}?edit=true`)}
className="dark:text-gray-300 dark:hover:text-blue-400"
>
编辑
</Button>
<Popconfirm
title="确定要删除这个专案吗?"
description="删除后将无法恢复!"
onConfirm={() => handleDelete(record.id)}
okText="确定"
cancelText="取消"
okButtonProps={{ danger: true }}
>
<Button <Button
size="small" size="small"
type="link" type="link"
danger icon={<EyeOutlined />}
icon={<DeleteOutlined />} onClick={() => navigate(`/company/projectView/${record.id}`)}
className="dark:hover:text-red-400" className="dark:text-gray-300 dark:hover:text-blue-400"
> >
删除 查看
</Button> </Button>
</Popconfirm> <Tooltip title="复制查看链接">
</Space> <Button
), size="small"
type="link"
icon={<ShareAltOutlined />}
onClick={() => {
const viewUrl = `${window.location.origin}/company/projectView/${record.id}`;
copyToClipboard(viewUrl);
if (!state.error) {
message.success('链接已复制到剪贴板');
} else {
message.error('复制失败,请手动复制');
}
}}
className="dark:text-gray-300 dark:hover:text-blue-400"
>
分享
</Button>
</Tooltip>
<Button
size="small"
type="link"
icon={<EditOutlined />}
onClick={() => navigate(`/company/projectInfo/${record.id}?edit=true`)}
className="dark:text-gray-300 dark:hover:text-blue-400"
>
编辑
</Button>
<Popconfirm
title="确定要删除这个专案吗?"
description="删除后将无法恢复!"
onConfirm={() => handleDelete(record.id)}
okText="确定"
cancelText="取消"
okButtonProps={{ danger: true }}
>
<Button
size="small"
type="link"
danger
icon={<DeleteOutlined />}
className="dark:hover:text-red-400"
>
删除
</Button>
</Popconfirm>
</Space>
);
},
}, },
]; ];

View File

@@ -1,5 +1,5 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { useParams } from 'react-router-dom'; import { useParams, useNavigate } from 'react-router-dom';
import { supabase } from "@/config/supabase"; import { supabase } from "@/config/supabase";
import { Spin, Tag, Empty, Button, Modal, Form, Input, Collapse, message, Upload,Tabs, Select, Divider } from 'antd'; import { Spin, Tag, Empty, Button, Modal, Form, Input, Collapse, message, Upload,Tabs, Select, Divider } from 'antd';
import { PlusOutlined, FolderOpenOutlined, ClockCircleOutlined, InboxOutlined } from '@ant-design/icons'; import { PlusOutlined, FolderOpenOutlined, ClockCircleOutlined, InboxOutlined } from '@ant-design/icons';
@@ -8,6 +8,7 @@ import {supabaseService}from '@/hooks/supabaseService'
const type="project" const type="project"
export default function ProjectInfo() { export default function ProjectInfo() {
const { id } = useParams(); const { id } = useParams();
const navigate = useNavigate();
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [projectData, setProjectData] = useState(null); const [projectData, setProjectData] = useState(null);
const [form] = Form.useForm(); const [form] = Form.useForm();
@@ -496,7 +497,10 @@ export default function ProjectInfo() {
<Input.TextArea rows={4} placeholder="请输入资源描述" /> <Input.TextArea rows={4} placeholder="请输入资源描述" />
</Form.Item> </Form.Item>
<Form.Item className="mb-0 flex justify-end gap-3"> <Form.Item >
<div className="mb-0 flex justify-end w-full gap-3">
<Button <Button
onClick={() => { onClick={() => {
setIsModalVisible(false); setIsModalVisible(false);
@@ -508,23 +512,42 @@ export default function ProjectInfo() {
<Button type="primary" htmlType="submit"> <Button type="primary" htmlType="submit">
确定 确定
</Button> </Button>
</div>
</Form.Item> </Form.Item>
</Form> </Form>
</Modal> </Modal>
); );
// 加载状态展示
if (loading) { if (loading) {
return ( return (
<div className="flex items-center justify-center min-h-screen"> <div className="min-h-screen bg-gray-50/40 flex items-center justify-center">
<Spin tip="加载中..." /> <div className="text-center">
<Spin size="large" />
<div className="mt-4 text-gray-500">加载项目信息中...</div>
</div>
</div> </div>
); );
} }
if (!projectData) { // 无项目数据状态(包括无 ID 和未找到项目)
if (!projectData || !id) {
return ( return (
<div className="flex items-center justify-center min-h-screen"> <div className="min-h-screen bg-gray-50/40 flex items-center justify-center">
<Empty description="未找到项目信息" /> <Empty
description={
<div className="text-gray-500">
<p className="mb-4">未找到项目信息</p>
<Button
onClick={() => navigate(-1)}
className="bg-blue-50 text-blue-600 hover:bg-blue-100 border-none
transition-colors duration-200"
>
返回上一页
</Button>
</div>
}
/>
</div> </div>
); );
} }