备份:PWA配置前的完整版本

包含所有最新开发的功能和修复,作为PWA正确配置前的备份点。
This commit is contained in:
a273825743
2026-06-13 12:44:48 +08:00
parent 5fd19822a2
commit 706dcc24eb
83 changed files with 26590 additions and 13379 deletions
+72 -61
View File
@@ -4,6 +4,7 @@ import { Card, Typography, Button, Space, Table, Tag, message, Spin, Modal, Inpu
import { PlusOutlined, DeleteOutlined } from '@ant-design/icons';
import apiClient from '../../utils/request';
import { useAuthStore } from '../../store/authStore';
import { useLanguageStore } from '../../store/languageStore';
import dayjs from 'dayjs';
const { Title, Paragraph } = Typography;
@@ -28,6 +29,7 @@ interface Project {
const ProjectsPage: React.FC = () => {
const navigate = useNavigate();
const { t, currentLanguage } = useLanguageStore();
const [isMobile, setIsMobile] = useState(false);
const [projects, setProjects] = useState<Project[]>([]);
const [loading, setLoading] = useState(true);
@@ -41,6 +43,7 @@ const ProjectsPage: React.FC = () => {
const [form] = Form.useForm();
const [customers, setCustomers] = useState<any[]>([]);
const [users, setUsers] = useState<any[]>([]);
const [templates, setTemplates] = useState<any[]>([]);
const { user: currentUser } = useAuthStore();
const isAdmin = currentUser?.role === 'admin' || false;
@@ -62,13 +65,13 @@ const ProjectsPage: React.FC = () => {
...p,
key: p.id.toString(),
progress: p.status === 'completed' ? 100 : (p.phase_progress || 0),
manager_name: p.manager_name || '未分配'
manager_name: p.manager_name || t('project.unassigned')
})));
} else {
message.error(`获取项目列表失败: ${response.data.message}`);
message.error(`${t('project.getListFailed')}: ${response.data.message}`);
}
} catch (error) {
message.error('获取项目列表失败');
message.error(t('project.getListFailed'));
} finally {
setLoading(false);
}
@@ -76,12 +79,14 @@ const ProjectsPage: React.FC = () => {
const fetchCreateOptions = async () => {
try {
const [custRes, userRes] = await Promise.all([
const [custRes, userRes, tplRes] = await Promise.all([
apiClient.get('/customers'),
apiClient.get('/users'),
apiClient.get('/process-templates'),
]);
if (custRes.data.success) setCustomers(custRes.data.data || []);
if (userRes.data.success) setUsers(userRes.data.data || []);
if (tplRes.data.success) setTemplates(tplRes.data.data || []);
} catch (e) { /* ignore */ }
};
@@ -99,17 +104,18 @@ const ProjectsPage: React.FC = () => {
const payload = {
...values,
manager_id: values.project_manager_id,
type_template_id: values.type_template_id || undefined,
start_date: values.start_date?.format('YYYY-MM-DD'),
end_date: values.end_date?.format('YYYY-MM-DD') || null,
contract_amount: values.contract_amount || 0,
};
const res = await apiClient.post('/projects', payload);
if (res.data.success) {
message.success('项目创建成功');
message.success(t('project.createSuccess'));
setCreateModalVisible(false);
fetchProjects();
} else {
message.error(res.data.message || '创建失败');
message.error(res.data.message || t('project.createFailed'));
}
} catch (e: any) {
if (e.response?.data?.message) message.error(e.response.data.message);
@@ -126,7 +132,7 @@ const ProjectsPage: React.FC = () => {
const handleDeleteConfirm = async () => {
if (!deleteProjectId) return;
if (deletePassword !== 'X123c321@') {
message.error('密码错误');
message.error(t('project.passwordError'));
return;
}
setDeleteLoading(true);
@@ -135,14 +141,14 @@ const ProjectsPage: React.FC = () => {
headers: { 'x-user-role': 'admin' }
});
if (response.data.success) {
message.success('项目删除成功');
message.success(t('project.deleteSuccess'));
setDeleteModalVisible(false);
fetchProjects();
} else {
message.error(response.data.message || '删除失败');
message.error(response.data.message || t('common.deleteFailed'));
}
} catch (error) {
message.error('删除项目失败');
message.error(t('common.operationFailed'));
} finally {
setDeleteLoading(false);
}
@@ -150,21 +156,21 @@ const ProjectsPage: React.FC = () => {
const desktopColumns = [
{
title: '项目名称', dataIndex: 'name', key: 'name', width: 250, ellipsis: true,
title: t('project.projectName'), dataIndex: 'name', key: 'name', width: 250, ellipsis: true,
render: (text: string, record: Project) => (
<a onClick={() => navigate(`/projects/${record.id}`)}>{text}</a>
),
},
{ title: '项目经理', dataIndex: 'manager_name', key: 'manager_name', width: 100 },
{ title: t('project.projectManager'), dataIndex: 'manager_name', key: 'manager_name', width: 100 },
{
title: '预算', dataIndex: 'budget', key: 'budget', width: 120,
title: t('project.budget'), dataIndex: 'budget', key: 'budget', width: 120,
render: (amount: string) => {
const val = parseFloat(amount || '0');
return val > 0 ? `¥${(val / 10000).toFixed(1)}` : '-';
return val > 0 ? `¥${(val / 10000).toFixed(1)}${t('common.tenThousand')}` : '-';
},
},
{
title: '进度', dataIndex: 'progress', key: 'progress', width: 120,
title: t('project.progress'), dataIndex: 'progress', key: 'progress', width: 120,
render: (progress: number) => (
<div style={{ width: 100 }}>
<div style={{ background: '#f0f0f0', borderRadius: 10, height: 8 }}>
@@ -178,23 +184,23 @@ const ProjectsPage: React.FC = () => {
),
},
{
title: '状态', dataIndex: 'status', key: 'status', width: 100,
title: t('project.status'), dataIndex: 'status', key: 'status', width: 100,
render: (status: string) => {
const statusMap: Record<string, { color: string; text: string }> = {
planning: { color: 'blue', text: '规划中' }, in_progress: { color: 'processing', text: '进行中' },
completed: { color: 'success', text: '已完成' }, suspended: { color: 'warning', text: '已暂停' },
active: { color: 'processing', text: '进行中' },
planning: { color: 'blue', text: t('project.planning') }, in_progress: { color: 'processing', text: t('project.inProgress') },
completed: { color: 'success', text: t('project.completed') }, suspended: { color: 'warning', text: t('project.paused') },
active: { color: 'processing', text: t('project.inProgress') },
};
const config = statusMap[status] || { color: 'default', text: status };
return <Tag color={config.color}>{config.text}</Tag>;
},
},
{
title: '操作', key: 'action', width: 140,
title: t('common.action'), key: 'action', width: 140,
render: (_: unknown, record: Project) => (
<Space>
<Button size="small" onClick={() => navigate(`/projects/${record.id}`)}></Button>
{isAdmin && <Button size="small" danger icon={<DeleteOutlined />} onClick={() => handleDeleteProject(record.id)}></Button>}
<Button size="small" onClick={() => navigate(`/projects/${record.id}`)}>{t('common.view')}</Button>
{isAdmin && <Button size="small" danger icon={<DeleteOutlined />} onClick={() => handleDeleteProject(record.id)}>{t('common.delete')}</Button>}
</Space>
),
},
@@ -202,13 +208,13 @@ const ProjectsPage: React.FC = () => {
const mobileColumns = [
{
title: '项目', dataIndex: 'name', key: 'name', ellipsis: true,
title: t('dashboard.project'), dataIndex: 'name', key: 'name', ellipsis: true,
render: (text: string, record: Project) => (
<a onClick={() => navigate(`/projects/${record.id}`)}>{text}</a>
),
},
{
title: '进度', dataIndex: 'progress', key: 'progress', width: 80,
title: t('project.progress'), dataIndex: 'progress', key: 'progress', width: 80,
render: (progress: number) => (
<div style={{ width: 60 }}>
<div style={{ background: '#f0f0f0', borderRadius: 4, height: 6 }}>
@@ -222,21 +228,21 @@ const ProjectsPage: React.FC = () => {
),
},
{
title: '状态', dataIndex: 'status', key: 'status', width: 70,
title: t('project.status'), dataIndex: 'status', key: 'status', width: 70,
render: (status: string) => {
const statusMap: Record<string, { color: string; text: string }> = {
planning: { color: 'blue', text: '规划' }, in_progress: { color: 'processing', text: '进行中' },
completed: { color: 'success', text: '完成' }, suspended: { color: 'warning', text: '暂停' },
active: { color: 'processing', text: '进行中' },
planning: { color: 'blue', text: t('project.plan') }, in_progress: { color: 'processing', text: t('project.inProgress') },
completed: { color: 'success', text: t('project.complete') }, suspended: { color: 'warning', text: t('project.pause') },
active: { color: 'processing', text: t('project.inProgress') },
};
const config = statusMap[status] || { color: 'default', text: status };
return <Tag color={config.color} style={{ fontSize: 10 }}>{config.text}</Tag>;
},
},
{
title: '操作', key: 'action', width: 60,
title: t('common.action'), key: 'action', width: 60,
render: (_: unknown, record: Project) => (
<Button size="small" onClick={() => navigate(`/projects/${record.id}`)}></Button>
<Button size="small" onClick={() => navigate(`/projects/${record.id}`)}>{t('common.view')}</Button>
),
},
];
@@ -245,15 +251,15 @@ const ProjectsPage: React.FC = () => {
<div style={{ padding: isMobile ? 8 : 24 }}>
<div style={{ marginBottom: isMobile ? 12 : 24, display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexWrap: 'wrap', gap: 8 }}>
<div>
<Title level={isMobile ? 3 : 2} style={{ marginBottom: 8 }}></Title>
<Paragraph type="secondary" style={{ marginBottom: 0 }}></Paragraph>
<Title level={isMobile ? 3 : 2} style={{ marginBottom: 8 }}>{t('project.title')}</Title>
<Paragraph type="secondary" style={{ marginBottom: 0 }}>{t('project.description')}</Paragraph>
</div>
<Button type="primary" icon={<PlusOutlined />} onClick={handleCreateClick}>
{isMobile ? '新建' : '新建项目'}
{isMobile ? t('common.create') : t('project.newProject')}
</Button>
</div>
<Card title="项目列表" size="small" styles={{ body: { padding: isMobile ? 8 : 24 } }}>
<Card title={t('project.list')} size="small" styles={{ body: { padding: isMobile ? 8 : 24 } }}>
{loading ? (
<div style={{ textAlign: 'center', padding: 40 }}><Spin /></div>
) : (
@@ -268,32 +274,37 @@ const ProjectsPage: React.FC = () => {
</Card>
<Modal
title="快速新建项目"
title={t('project.quickCreate')}
open={createModalVisible}
onOk={handleCreateSubmit}
onCancel={() => setCreateModalVisible(false)}
confirmLoading={createLoading}
okText="创建"
cancelText="取消"
okText={t('common.create')}
cancelText={t('common.cancel')}
width={isMobile ? '95%' : 560}
style={{ top: isMobile ? 10 : 40 }}
destroyOnClose
>
<Form form={form} layout="vertical" size="middle">
<Form.Item name="name" label="项目名称" rules={[{ required: true, message: '请输入项目名称' }]}>
<Input placeholder="如:万象省赛塔尼县22kV线路工程" />
<Form.Item name="name" label={t('project.projectName')} rules={[{ required: true, message: t('common.inputPlaceholder') + t('project.projectName') }]}>
<Input placeholder={t('project.projectNamePlaceholder')} />
</Form.Item>
<Form.Item name="type_template_id" label={t('project.projectTemplate')}>
<Select allowClear placeholder={t('project.selectTemplate')}
options={templates.map((tpl: any) => ({ value: tpl.id, label: tpl.name }))} />
</Form.Item>
<Row gutter={8}>
<Col span={12}>
<Form.Item name="customer_id" label="客户">
<Select showSearch optionFilterProp="label" allowClear placeholder="选择客户"
<Form.Item name="customer_id" label={t('project.customer')}>
<Select showSearch optionFilterProp="label" allowClear placeholder={t('project.selectCustomer')}
options={customers.map((c: any) => ({ value: c.id, label: c.name }))} />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item name="project_manager_id" label="项目经理">
<Select showSearch optionFilterProp="label" allowClear placeholder="选择项目经理"
<Form.Item name="project_manager_id" label={t('project.projectManager')}>
<Select showSearch optionFilterProp="label" allowClear placeholder={t('project.selectManager')}
options={users.map((u: any) => ({ value: u.id, label: u.name || u.username }))} />
</Form.Item>
</Col>
@@ -301,16 +312,16 @@ const ProjectsPage: React.FC = () => {
<Row gutter={8}>
<Col span={12}>
<Form.Item name="contract_amount" label="合同金额">
<Form.Item name="contract_amount" label={t('project.contractAmount')}>
<InputNumber style={{ width: '100%' }} min={0} placeholder="0" />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item name="status" label="项目状态">
<Form.Item name="status" label={t('project.projectStatus')}>
<Select options={[
{ value: 'planning', label: '规划中' },
{ value: 'in_progress', label: '进行中' },
{ value: 'completed', label: '已完成(补录历史项目)' },
{ value: 'planning', label: t('project.planning') },
{ value: 'in_progress', label: t('project.inProgress') },
{ value: 'completed', label: t('project.completedHistory') },
]} />
</Form.Item>
</Col>
@@ -318,44 +329,44 @@ const ProjectsPage: React.FC = () => {
<Row gutter={8}>
<Col span={12}>
<Form.Item name="start_date" label="开始日期">
<Form.Item name="start_date" label={t('project.startDate')}>
<DatePicker style={{ width: '100%' }} />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item name="end_date" label="结束日期">
<Form.Item name="end_date" label={t('project.endDate')}>
<DatePicker style={{ width: '100%' }} />
</Form.Item>
</Col>
</Row>
<Form.Item name="location" label="项目地点">
<Input placeholder="如:老挝万象省" />
<Form.Item name="location" label={t('project.location')}>
<Input placeholder={t('project.locationPlaceholder')} />
</Form.Item>
<Form.Item name="description" label="项目描述">
<Input.TextArea rows={2} placeholder="简要描述项目内容" />
<Form.Item name="description" label={t('project.description')}>
<Input.TextArea rows={2} placeholder={t('project.descriptionPlaceholder')} />
</Form.Item>
</Form>
</Modal>
<Modal
title="删除确认"
title={t('project.deleteConfirm')}
open={deleteModalVisible}
onOk={handleDeleteConfirm}
onCancel={() => setDeleteModalVisible(false)}
confirmLoading={deleteLoading}
okText="确认删除"
cancelText="取消"
okText={t('common.deleteConfirm')}
cancelText={t('common.cancel')}
>
<div style={{ marginBottom: 16 }}>
<p></p>
<p></p>
<p>{t('project.deleteConfirmMsg')}</p>
<p>{t('project.deletePassMsg')}</p>
</div>
<Input.Password placeholder="请输入管理员密码" value={deletePassword} onChange={(e) => setDeletePassword(e.target.value)} size="large" />
<Input.Password placeholder={t('common.inputPassword')} value={deletePassword} onChange={(e) => setDeletePassword(e.target.value)} size="large" />
</Modal>
</div>
);
};
export default ProjectsPage;
export default ProjectsPage;