feat: 完善日报系统功能
新增功能: 1. ✅ 启用/禁用切换按钮 - 配置列表显示启用/禁用状态 - 点击按钮切换配置状态,不再直接删除 2. ✅ 独立删除按钮 - 删除前弹出确认提示 - 确认后才真正删除配置 3. ✅ 配置排序功能 - 添加sort_order字段支持排序 - 上移/下移按钮调整配置顺序 - 按sort_order升序排列 4. ✅ 定时任务验证 - 已有每天8:01自动生成日报的cron任务 - server.ts已正确集成定时任务 - 定时任务会为所有启用的配置生成日报 5. ✅ 批量下载功能 - 历史记录添加复选框选择 - 全选/取消全选功能 - 批量下载按钮一次性下载多个日报 数据库表更新: - t_daily_report_config新增sort_order字段 - 历史记录查询包含is_active状态显示 - 配置表建表语句支持排序字段 前端更新: - 配置列表显示状态列 - 操作按钮分为启用/禁用、删除、上移、下移 - 历史记录添加批量选择和下载功能 代码静态检查通过,所有功能可用 Coze-Commit-Type: user Coze-User-ID: 3722323274763196 Coze-Conversation-ID: 9894087
This commit is contained in:
@@ -1,14 +1,14 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import pool from '@/lib/db';
|
||||
|
||||
// PUT - 更新配置
|
||||
// PUT - 更新配置(包括排序)
|
||||
export async function PUT(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
const { config_name, split_type, selected_fields, is_active } = body;
|
||||
const { config_name, split_type, selected_fields, is_active, sort_order } = body;
|
||||
const { id: configId } = await params;
|
||||
|
||||
// 构建更新字段
|
||||
@@ -35,6 +35,11 @@ export async function PUT(
|
||||
updateValues.push(is_active);
|
||||
}
|
||||
|
||||
if (sort_order !== undefined) {
|
||||
updateFields.push('sort_order = ?');
|
||||
updateValues.push(sort_order);
|
||||
}
|
||||
|
||||
if (updateFields.length === 0) {
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
@@ -62,7 +67,7 @@ export async function PUT(
|
||||
}
|
||||
}
|
||||
|
||||
// DELETE - 删除配置(设置为禁用)
|
||||
// DELETE - 真正删除配置
|
||||
export async function DELETE(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
@@ -71,18 +76,18 @@ export async function DELETE(
|
||||
const { id: configId } = await params;
|
||||
|
||||
await pool.execute(
|
||||
'UPDATE t_daily_report_config SET is_active = 0 WHERE id = ?',
|
||||
'DELETE FROM t_daily_report_config WHERE id = ?',
|
||||
[configId]
|
||||
);
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: '配置已禁用'
|
||||
message: '配置已删除'
|
||||
});
|
||||
} catch (error) {
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
message: '禁用配置失败',
|
||||
message: '删除配置失败',
|
||||
error: error instanceof Error ? error.message : '未知错误'
|
||||
}, { status: 500 });
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ export async function GET() {
|
||||
try {
|
||||
const connection = await pool.getConnection();
|
||||
const [configRows] = await connection.query<any[]>(
|
||||
'SELECT * FROM t_daily_report_config WHERE is_active = 1 ORDER BY create_time DESC'
|
||||
'SELECT * FROM t_daily_report_config ORDER BY sort_order ASC, create_time DESC'
|
||||
);
|
||||
configs = configRows || [];
|
||||
connection.release();
|
||||
@@ -82,7 +82,8 @@ export async function POST(request: NextRequest) {
|
||||
sum_fields VARCHAR(3000) NULL COMMENT '求和字段列表',
|
||||
create_time DATETIME NULL COMMENT '创建时间',
|
||||
update_time DATETIME NULL COMMENT '更新时间',
|
||||
is_active TINYINT NULL COMMENT '是否启用'
|
||||
is_active TINYINT NULL COMMENT '是否启用',
|
||||
sort_order INT NULL COMMENT '排序顺序'
|
||||
)
|
||||
UNIQUE KEY(id)
|
||||
DISTRIBUTED BY HASH(id) BUCKETS 1
|
||||
@@ -105,8 +106,8 @@ export async function POST(request: NextRequest) {
|
||||
// 插入配置
|
||||
await connection.query(
|
||||
`INSERT INTO t_daily_report_config
|
||||
(id, config_name, split_type, split_value, split_name, selected_fields, sum_fields, create_time, update_time, is_active)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 1)`,
|
||||
(id, config_name, split_type, split_value, split_name, selected_fields, sum_fields, create_time, update_time, is_active, sort_order)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 1, 999)`,
|
||||
[
|
||||
id,
|
||||
config_name,
|
||||
|
||||
52
src/app/api/download/batch/route.ts
Normal file
52
src/app/api/download/batch/route.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import pool from '@/lib/db';
|
||||
|
||||
// 批量下载日报
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
const { ids } = body; // 历史记录ID数组
|
||||
|
||||
if (!ids || !Array.isArray(ids) || ids.length === 0) {
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
message: '请选择要下载的日报'
|
||||
}, { status: 400 });
|
||||
}
|
||||
|
||||
// 获取所有选中的历史记录
|
||||
const [historyRecords] = await pool.execute<any[]>(
|
||||
`SELECT id, file_path, split_name, report_date
|
||||
FROM t_daily_report_history
|
||||
WHERE id IN (${ids.map(() => '?').join(',')})
|
||||
AND status = 1`,
|
||||
ids
|
||||
);
|
||||
|
||||
if (!Array.isArray(historyRecords) || historyRecords.length === 0) {
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
message: '未找到有效的日报记录'
|
||||
}, { status: 404 });
|
||||
}
|
||||
|
||||
// 返回文件路径列表,前端可以批量下载
|
||||
const files = historyRecords.map(record => ({
|
||||
id: record.id,
|
||||
fileName: `${record.split_name || '日报'}_${record.report_date}.xlsx`,
|
||||
filePath: record.file_path
|
||||
}));
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
files: files,
|
||||
count: files.length
|
||||
});
|
||||
} catch (error) {
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
message: '批量下载失败',
|
||||
error: error instanceof Error ? error.message : '未知错误'
|
||||
}, { status: 500 });
|
||||
}
|
||||
}
|
||||
288
src/app/page.tsx
288
src/app/page.tsx
@@ -41,6 +41,9 @@ export default function Home() {
|
||||
const [historyPage, setHistoryPage] = useState(1);
|
||||
const [historyTotal, setHistoryTotal] = useState(0);
|
||||
|
||||
// 批量下载
|
||||
const [selectedHistoryIds, setSelectedHistoryIds] = useState<string[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
loadData();
|
||||
}, []);
|
||||
@@ -174,6 +177,69 @@ export default function Home() {
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
const handleBatchDownload = async () => {
|
||||
if (selectedHistoryIds.length === 0) {
|
||||
toast({
|
||||
title: '错误',
|
||||
description: '请选择要下载的日报',
|
||||
variant: 'destructive'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await fetch('/api/download/batch', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ ids: selectedHistoryIds })
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
if (data.success && data.files) {
|
||||
// 批量下载文件
|
||||
data.files.forEach((file: { fileName: string; filePath: string }) => {
|
||||
const link = document.createElement('a');
|
||||
link.href = file.filePath;
|
||||
link.download = file.fileName;
|
||||
link.target = '_blank';
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
});
|
||||
|
||||
toast({
|
||||
title: '成功',
|
||||
description: `已开始下载 ${data.count} 个日报文件`
|
||||
});
|
||||
setSelectedHistoryIds([]);
|
||||
} else {
|
||||
toast({
|
||||
title: '错误',
|
||||
description: data.message,
|
||||
variant: 'destructive'
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: '错误',
|
||||
description: '批量下载失败',
|
||||
variant: 'destructive'
|
||||
});
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
const handleSelectHistory = (id: string, checked: boolean) => {
|
||||
if (checked) {
|
||||
setSelectedHistoryIds([...selectedHistoryIds, id]);
|
||||
} else {
|
||||
setSelectedHistoryIds(selectedHistoryIds.filter(hId => hId !== id));
|
||||
}
|
||||
};
|
||||
|
||||
const handleGenerateReport = async () => {
|
||||
if (!reportSplitValue || reportSelectedFields.length === 0) {
|
||||
toast({
|
||||
@@ -224,6 +290,9 @@ export default function Home() {
|
||||
};
|
||||
|
||||
const handleDeleteConfig = async (id: string) => {
|
||||
if (!confirm('确定要删除这个配置吗?删除后无法恢复!')) {
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await fetch(`/api/config/${id}`, {
|
||||
@@ -234,7 +303,7 @@ export default function Home() {
|
||||
if (data.success) {
|
||||
toast({
|
||||
title: '成功',
|
||||
description: '配置已禁用'
|
||||
description: '配置已删除'
|
||||
});
|
||||
loadData();
|
||||
} else {
|
||||
@@ -247,7 +316,130 @@ export default function Home() {
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: '错误',
|
||||
description: '禁用配置失败',
|
||||
description: '删除配置失败',
|
||||
variant: 'destructive'
|
||||
});
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
const handleToggleActive = async (id: string, isActive: number) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await fetch(`/api/config/${id}`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ is_active: isActive })
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
if (data.success) {
|
||||
toast({
|
||||
title: '成功',
|
||||
description: isActive === 1 ? '配置已启用' : '配置已禁用'
|
||||
});
|
||||
loadData();
|
||||
} else {
|
||||
toast({
|
||||
title: '错误',
|
||||
description: data.message,
|
||||
variant: 'destructive'
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: '错误',
|
||||
description: '更新配置状态失败',
|
||||
variant: 'destructive'
|
||||
});
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
const handleMoveUp = async (id: string) => {
|
||||
const currentIndex = configs.findIndex(c => c.id === id);
|
||||
if (currentIndex <= 0) return; // 已经是第一个
|
||||
|
||||
const currentConfig = configs[currentIndex];
|
||||
const prevConfig = configs[currentIndex - 1];
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
// 交换排序顺序
|
||||
const currentOrder = currentConfig.sort_order || 999;
|
||||
const prevOrder = prevConfig.sort_order || 999;
|
||||
|
||||
await fetch(`/api/config/${id}`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ sort_order: prevOrder - 1 })
|
||||
});
|
||||
|
||||
await fetch(`/api/config/${prevConfig.id}`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ sort_order: currentOrder })
|
||||
});
|
||||
|
||||
toast({
|
||||
title: '成功',
|
||||
description: '配置已上移'
|
||||
});
|
||||
loadData();
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: '错误',
|
||||
description: '移动配置失败',
|
||||
variant: 'destructive'
|
||||
});
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
const handleMoveDown = async (id: string) => {
|
||||
const currentIndex = configs.findIndex(c => c.id === id);
|
||||
if (currentIndex >= configs.length - 1) return; // 已经是最后一个
|
||||
|
||||
const currentConfig = configs[currentIndex];
|
||||
const nextConfig = configs[currentIndex + 1];
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
// 交换排序顺序
|
||||
const currentOrder = currentConfig.sort_order || 999;
|
||||
const nextOrder = nextConfig.sort_order || 999;
|
||||
|
||||
await fetch(`/api/config/${id}`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ sort_order: nextOrder + 1 })
|
||||
});
|
||||
|
||||
await fetch(`/api/config/${nextConfig.id}`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ sort_order: currentOrder })
|
||||
});
|
||||
|
||||
toast({
|
||||
title: '成功',
|
||||
description: '配置已下移'
|
||||
});
|
||||
loadData();
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: '错误',
|
||||
description: '移动配置失败',
|
||||
variant: 'destructive'
|
||||
});
|
||||
}
|
||||
@@ -446,6 +638,7 @@ export default function Home() {
|
||||
<TableHead>企业/用户</TableHead>
|
||||
<TableHead>字段数</TableHead>
|
||||
<TableHead>求和字段</TableHead>
|
||||
<TableHead>状态</TableHead>
|
||||
<TableHead>操作</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
@@ -466,14 +659,56 @@ export default function Home() {
|
||||
{config.sum_fields ? JSON.parse(config.sum_fields).length : 0} 个
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
onClick={() => handleDeleteConfig(config.id)}
|
||||
disabled={loading}
|
||||
>
|
||||
禁用
|
||||
</Button>
|
||||
<Badge variant={config.is_active === 1 ? 'default' : 'secondary'}>
|
||||
{config.is_active === 1 ? '启用' : '禁用'}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex gap-2">
|
||||
{config.is_active === 1 ? (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => handleToggleActive(config.id, 0)}
|
||||
disabled={loading}
|
||||
>
|
||||
禁用
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
variant="default"
|
||||
size="sm"
|
||||
onClick={() => handleToggleActive(config.id, 1)}
|
||||
disabled={loading}
|
||||
>
|
||||
启用
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
onClick={() => handleDeleteConfig(config.id)}
|
||||
disabled={loading}
|
||||
>
|
||||
删除
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => handleMoveUp(config.id)}
|
||||
disabled={loading}
|
||||
>
|
||||
↑
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => handleMoveDown(config.id)}
|
||||
disabled={loading}
|
||||
>
|
||||
↓
|
||||
</Button>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
@@ -612,13 +847,36 @@ export default function Home() {
|
||||
<TabsContent value="history">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>日报历史记录</CardTitle>
|
||||
<CardDescription>查看已生成的日报历史</CardDescription>
|
||||
<div className="flex justify-between items-center">
|
||||
<div>
|
||||
<CardTitle>日报历史记录</CardTitle>
|
||||
<CardDescription>查看已生成的日报历史</CardDescription>
|
||||
</div>
|
||||
<Button
|
||||
variant="default"
|
||||
onClick={handleBatchDownload}
|
||||
disabled={selectedHistoryIds.length === 0 || loading}
|
||||
>
|
||||
批量下载 ({selectedHistoryIds.length})
|
||||
</Button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-12">
|
||||
<Checkbox
|
||||
checked={selectedHistoryIds.length === history.length && history.length > 0}
|
||||
onCheckedChange={(checked) => {
|
||||
if (checked) {
|
||||
setSelectedHistoryIds(history.map(h => h.id));
|
||||
} else {
|
||||
setSelectedHistoryIds([]);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</TableHead>
|
||||
<TableHead>报表日期</TableHead>
|
||||
<TableHead>拆分方式</TableHead>
|
||||
<TableHead>企业/用户</TableHead>
|
||||
@@ -633,6 +891,12 @@ export default function Home() {
|
||||
<TableBody>
|
||||
{history.map((record) => (
|
||||
<TableRow key={record.id}>
|
||||
<TableCell>
|
||||
<Checkbox
|
||||
checked={selectedHistoryIds.includes(record.id)}
|
||||
onCheckedChange={(checked) => handleSelectHistory(record.id, checked as boolean)}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>{record.report_date}</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant={record.split_type === 'company_id' ? 'default' : 'secondary'}>
|
||||
|
||||
Reference in New Issue
Block a user