生产管理系统前端 开发中心配置系统 所有页面

This commit is contained in:
2025-10-21 18:04:39 +08:00
parent 4a5d278d89
commit 9afc680833
185 changed files with 13677 additions and 4487 deletions

View File

@@ -0,0 +1,17 @@
import { Card } from '@/components/ui/card';
export function MessageInstructions() {
return (
<Card className="p-4 bg-blue-50 border-blue-200">
<h4 className="text-blue-900 mb-2"></h4>
<ul className="space-y-1 text-sm text-blue-800">
<li> </li>
<li> </li>
<li> </li>
<li> 使</li>
<li> </li>
<li> </li>
</ul>
</Card>
);
}

View File

@@ -0,0 +1,122 @@
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { Label } from '@/components/ui/label';
import { Card } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { format } from 'date-fns';
import { zhCN } from 'date-fns/locale';
import { MessageSendRecord } from '@/types/message';
interface MessagePreviewDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
record: MessageSendRecord | null;
getTypeIcon: (type: string) => JSX.Element;
getTypeLabel: (type: string) => string;
getTypeBadge: (type: string) => string;
getStatusBadge: (status: string) => JSX.Element;
}
export function MessagePreviewDialog({
open,
onOpenChange,
record,
getTypeIcon,
getTypeLabel,
getTypeBadge,
getStatusBadge
}: MessagePreviewDialogProps) {
if (!record) return null;
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-2xl">
<DialogHeader>
<DialogTitle></DialogTitle>
<DialogDescription className="sr-only">
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div>
<Label></Label>
<div className="field-value-inline">{record.templateName}</div>
</div>
<div>
<Label></Label>
<div className="mt-2">
<Badge className={getTypeBadge(record.type)}>
<span className="flex items-center gap-1">
{getTypeIcon(record.type)}
{getTypeLabel(record.type)}
</span>
</Badge>
</div>
</div>
<div>
<Label></Label>
<div className="field-value-inline">
{record.sendType === 'immediate' ? '实时发送' : '定时发送'}
</div>
</div>
<div>
<Label></Label>
<div className="mt-2">
{getStatusBadge(record.status)}
</div>
</div>
{record.scheduledTime && (
<div>
<Label></Label>
<div className="field-value-inline">
{format(new Date(record.scheduledTime), 'yyyy-MM-dd HH:mm', { locale: zhCN })}
</div>
</div>
)}
<div>
<Label></Label>
<div className="field-value-inline">
{format(new Date(record.createdAt), 'yyyy-MM-dd HH:mm', { locale: zhCN })}
</div>
</div>
</div>
{record.subject && (
<div>
<Label></Label>
<div className="field-value-inline">{record.subject}</div>
</div>
)}
<div>
<Label> {record.recipientCount} </Label>
<Card className="p-3 bg-gray-50 mt-2">
<div className="flex flex-wrap gap-2">
{record.recipients.map((recipient, index) => (
<Badge key={index} variant="outline">
{recipient}
</Badge>
))}
</div>
</Card>
</div>
<div>
<Label></Label>
<Card className="p-4 bg-blue-50 border-blue-200 mt-2">
<pre className="text-sm whitespace-pre-wrap">
{record.content}
</pre>
</Card>
</div>
</div>
<DialogFooter>
<Button onClick={() => onOpenChange(false)}>
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}

View File

@@ -0,0 +1,154 @@
import { Card } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
import {
Send,
Clock,
Users,
Eye,
Trash2,
CheckCircle2,
XCircle,
Timer
} from 'lucide-react';
import { format } from 'date-fns';
import { zhCN } from 'date-fns/locale';
import { MessageSendRecord } from '@/types/message';
interface MessageSendTableProps {
sendRecords: MessageSendRecord[];
onPreview: (record: MessageSendRecord) => void;
onCancel: (id: string) => void;
onDelete: (id: string) => void;
getTypeIcon: (type: string) => JSX.Element;
getTypeLabel: (type: string) => string;
getTypeBadge: (type: string) => string;
getStatusBadge: (status: string) => JSX.Element;
}
export function MessageSendTable({
sendRecords,
onPreview,
onCancel,
onDelete,
getTypeIcon,
getTypeLabel,
getTypeBadge,
getStatusBadge
}: MessageSendTableProps) {
return (
<Card>
<Table>
<TableHeader>
<TableRow>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{sendRecords.length === 0 ? (
<TableRow>
<TableCell colSpan={7} className="text-center text-muted-foreground py-8">
</TableCell>
</TableRow>
) : (
sendRecords.map((record) => (
<TableRow key={record.id}>
<TableCell>
<div>{record.templateName}</div>
{record.subject && (
<p className="text-xs text-muted-foreground">{record.subject}</p>
)}
</TableCell>
<TableCell>
<Badge className={getTypeBadge(record.type)}>
<span className="flex items-center gap-1">
{getTypeIcon(record.type)}
{getTypeLabel(record.type)}
</span>
</Badge>
</TableCell>
<TableCell>
<div className="flex items-center gap-1">
<Users className="w-4 h-4 text-muted-foreground" />
<span>{record.recipientCount}</span>
</div>
</TableCell>
<TableCell>
{record.sendType === 'immediate' ? (
<Badge variant="outline">
<Send className="w-3 h-3 mr-1" />
</Badge>
) : (
<div>
<Badge variant="outline">
<Clock className="w-3 h-3 mr-1" />
</Badge>
{record.scheduledTime && (
<p className="text-xs text-muted-foreground mt-1">
{format(new Date(record.scheduledTime), 'MM-dd HH:mm', { locale: zhCN })}
</p>
)}
</div>
)}
</TableCell>
<TableCell>
{getStatusBadge(record.status)}
{record.status === 'sent' && (
<p className="text-xs text-muted-foreground mt-1">
{record.sentCount}/{record.recipientCount}
</p>
)}
</TableCell>
<TableCell className="text-sm text-muted-foreground">
{format(new Date(record.createdAt), 'MM-dd HH:mm', { locale: zhCN })}
</TableCell>
<TableCell>
<div className="flex gap-1">
<Button
variant="ghost"
size="sm"
onClick={() => onPreview(record)}
title="查看详情"
>
<Eye className="w-4 h-4" />
</Button>
{record.status === 'pending' && (
<Button
variant="ghost"
size="sm"
onClick={() => onCancel(record.id)}
title="取消发送"
>
<XCircle className="w-4 h-4 text-orange-600" />
</Button>
)}
{(record.status === 'sent' || record.status === 'cancelled') && (
<Button
variant="ghost"
size="sm"
onClick={() => onDelete(record.id)}
title="删除记录"
>
<Trash2 className="w-4 h-4 text-destructive" />
</Button>
)}
</div>
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
</Card>
);
}

View File

@@ -0,0 +1,42 @@
import { Card } from '@/components/ui/card';
import { MessageSendRecord } from '@/types/message';
interface MessageStatsCardsProps {
sendRecords: MessageSendRecord[];
}
export function MessageStatsCards({ sendRecords }: MessageStatsCardsProps) {
const stats = [
{
label: '总发送数',
value: sendRecords.length,
color: 'text-blue-600',
},
{
label: '已发送',
value: sendRecords.filter(r => r.status === 'sent').length,
color: 'text-green-600',
},
{
label: '待发送',
value: sendRecords.filter(r => r.status === 'pending').length,
color: 'text-yellow-600',
},
{
label: '已取消',
value: sendRecords.filter(r => r.status === 'cancelled').length,
color: 'text-gray-600',
},
];
return (
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
{stats.map((stat, index) => (
<Card key={index} className="p-4">
<div className="text-sm text-muted-foreground">{stat.label}</div>
<div className={`mt-2 text-2xl font-bold ${stat.color}`}>{stat.value}</div>
</Card>
))}
</div>
);
}

View File

@@ -0,0 +1,258 @@
import { useState } from 'react';
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Textarea } from '@/components/ui/textarea';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Card } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { Calendar } from '@/components/ui/calendar';
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
import { Send, Clock, CalendarIcon } from 'lucide-react';
import { format } from 'date-fns';
import { zhCN } from 'date-fns/locale';
import { MessageTemplate } from '@/types/message';
import { MessageSendFormData } from '../types';
interface SendMessageDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
templates: MessageTemplate[];
formData: MessageSendFormData;
onFormDataChange: (data: MessageSendFormData) => void;
onSend: () => void;
getTypeIcon: (type: string) => JSX.Element;
getTypeLabel: (type: string) => string;
}
export function SendMessageDialog({
open,
onOpenChange,
templates,
formData,
onFormDataChange,
onSend,
getTypeIcon,
getTypeLabel
}: SendMessageDialogProps) {
const replaceVariables = (content: string, variables: Record<string, string>): string => {
let result = content;
Object.entries(variables).forEach(([key, value]) => {
result = result.replace(new RegExp(`\\{\\{${key}\\}\\}`, 'g'), value || `{{${key}}}`);
});
return result;
};
const handleTemplateChange = (templateId: string) => {
const template = templates.find(t => t.id === templateId);
if (template) {
// 初始化变量
const vars: Record<string, string> = {};
template.variables.forEach(v => {
vars[v] = '';
});
onFormDataChange({
...formData,
templateId,
type: template.type,
subject: template.subject || '',
content: template.content,
variables: vars,
});
}
};
const selectedTemplate = templates.find(t => t.id === formData.templateId);
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-3xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>
<div className="flex items-center gap-2">
<Send className="w-5 h-5 text-green-600" />
</div>
</DialogTitle>
<DialogDescription className="sr-only">
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
{/* 选择模版 */}
<div>
<Label> *</Label>
<Select value={formData.templateId} onValueChange={handleTemplateChange}>
<SelectTrigger>
<SelectValue placeholder="请选择消息模版" />
</SelectTrigger>
<SelectContent>
{templates.filter(t => t.isActive).map(template => (
<SelectItem key={template.id} value={template.id}>
<div className="flex items-center gap-2">
{getTypeIcon(template.type)}
<span>{template.name}</span>
<Badge variant="outline" className="text-xs">
{getTypeLabel(template.type)}
</Badge>
</div>
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* 发送方式 */}
<div>
<Label> *</Label>
<Select
value={formData.sendType}
onValueChange={(value: 'immediate' | 'scheduled') => onFormDataChange({ ...formData, sendType: value })}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="immediate">
<div className="flex items-center gap-2">
<Send className="w-4 h-4" />
</div>
</SelectItem>
<SelectItem value="scheduled">
<div className="flex items-center gap-2">
<Clock className="w-4 h-4" />
</div>
</SelectItem>
</SelectContent>
</Select>
</div>
{/* 定时发送设置 */}
{formData.sendType === 'scheduled' && (
<div className="grid grid-cols-2 gap-4">
<div>
<Label> *</Label>
<Popover>
<PopoverTrigger asChild>
<Button variant="outline" className="w-full justify-start">
<CalendarIcon className="w-4 h-4 mr-2" />
{formData.scheduledDate ? (
format(formData.scheduledDate, 'yyyy年MM月dd日', { locale: zhCN })
) : (
'选择日期'
)}
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-0" align="start">
<Calendar
mode="single"
selected={formData.scheduledDate}
onSelect={(date) => onFormDataChange({ ...formData, scheduledDate: date })}
locale={zhCN}
disabled={(date) => date < new Date(new Date().setHours(0, 0, 0, 0))}
/>
</PopoverContent>
</Popover>
</div>
<div>
<Label> *</Label>
<Input
type="time"
value={formData.scheduledTime}
onChange={(e) => onFormDataChange({ ...formData, scheduledTime: e.target.value })}
/>
</div>
</div>
)}
{/* 接收人 */}
<div>
<Label> *</Label>
<Textarea
value={formData.recipients}
onChange={(e) => onFormDataChange({ ...formData, recipients: e.target.value })}
placeholder={
formData.type === 'sms' ? '输入手机号,多个用逗号或换行分隔' :
formData.type === 'email' ? '输入邮箱地址,多个用逗号或换行分隔' :
formData.type === 'push' ? '输入设备ID或用户ID多个用逗号或换行分隔' :
'输入用户名,多个用逗号或换行分隔'
}
rows={3}
/>
<p className="text-xs text-muted-foreground mt-1">
使
</p>
</div>
{/* 消息主题(邮件和推送) */}
{(formData.type === 'email' || formData.type === 'push') && (
<div>
<Label></Label>
<Input
value={formData.subject}
onChange={(e) => onFormDataChange({ ...formData, subject: e.target.value })}
placeholder="输入消息主题"
/>
</div>
)}
{/* 变量填写 */}
{selectedTemplate && selectedTemplate.variables.length > 0 && (
<div>
<Label> *</Label>
<Card className="p-4 bg-gray-50">
<div className="grid grid-cols-2 gap-4">
{selectedTemplate.variables.map(variable => (
<div key={variable}>
<Label htmlFor={`var-${variable}`} className="text-xs">
{variable}
</Label>
<Input
id={`var-${variable}`}
value={formData.variables[variable] || ''}
onChange={(e) => onFormDataChange({
...formData,
variables: {
...formData.variables,
[variable]: e.target.value,
},
})}
placeholder={`输入 ${variable}`}
className="mt-1"
/>
</div>
))}
</div>
</Card>
</div>
)}
{/* 消息内容预览 */}
{formData.content && (
<div>
<Label></Label>
<Card className="p-4 bg-blue-50 border-blue-200">
<pre className="text-sm whitespace-pre-wrap">
{replaceVariables(formData.content, formData.variables)}
</pre>
</Card>
</div>
)}
</div>
<DialogFooter>
<Button variant="outline" onClick={() => onOpenChange(false)}>
</Button>
<Button onClick={onSend} className="bg-green-600 hover:bg-green-700">
<Send className="w-4 h-4 mr-2" />
{formData.sendType === 'immediate' ? '立即发送' : '创建定时任务'}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}

View File

@@ -0,0 +1,5 @@
export { MessageStatsCards } from './MessageStatsCards';
export { MessageSendTable } from './MessageSendTable';
export { SendMessageDialog } from './SendMessageDialog';
export { MessagePreviewDialog } from './MessagePreviewDialog';
export { MessageInstructions } from './MessageInstructions';