朋友圈表格导出
This commit is contained in:
@@ -1,4 +1,6 @@
|
||||
import request from "@/api/request";
|
||||
import axios from "axios";
|
||||
import { useUserStore } from "@/store/module/user";
|
||||
|
||||
// 获取微信号详情
|
||||
export function getWechatAccountDetail(id: string) {
|
||||
@@ -50,3 +52,68 @@ export function transferWechatFriends(params: {
|
||||
}) {
|
||||
return request("/v1/wechats/transfer-friends", params, "POST");
|
||||
}
|
||||
|
||||
// 导出朋友圈接口(直接下载文件)
|
||||
export async function exportWechatMoments(params: {
|
||||
wechatId: string;
|
||||
keyword?: string;
|
||||
type?: number;
|
||||
startTime?: string;
|
||||
endTime?: string;
|
||||
}): Promise<void> {
|
||||
const { token } = useUserStore.getState();
|
||||
const baseURL =
|
||||
(import.meta as any).env?.VITE_API_BASE_URL || "/api";
|
||||
|
||||
// 构建查询参数
|
||||
const queryParams = new URLSearchParams();
|
||||
queryParams.append("wechatId", params.wechatId);
|
||||
if (params.keyword) {
|
||||
queryParams.append("keyword", params.keyword);
|
||||
}
|
||||
if (params.type !== undefined) {
|
||||
queryParams.append("type", params.type.toString());
|
||||
}
|
||||
if (params.startTime) {
|
||||
queryParams.append("startTime", params.startTime);
|
||||
}
|
||||
if (params.endTime) {
|
||||
queryParams.append("endTime", params.endTime);
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await axios.get(
|
||||
`${baseURL}/v1/wechats/moments/export?${queryParams.toString()}`,
|
||||
{
|
||||
responseType: "blob",
|
||||
headers: {
|
||||
Authorization: token ? `Bearer ${token}` : undefined,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
// 创建下载链接
|
||||
const blob = new Blob([response.data]);
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const link = document.createElement("a");
|
||||
link.href = url;
|
||||
|
||||
// 从响应头获取文件名,如果没有则使用默认文件名
|
||||
const contentDisposition = response.headers["content-disposition"];
|
||||
let fileName = "朋友圈导出.xlsx";
|
||||
if (contentDisposition) {
|
||||
const fileNameMatch = contentDisposition.match(/filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/);
|
||||
if (fileNameMatch && fileNameMatch[1]) {
|
||||
fileName = decodeURIComponent(fileNameMatch[1].replace(/['"]/g, ""));
|
||||
}
|
||||
}
|
||||
|
||||
link.download = fileName;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
window.URL.revokeObjectURL(url);
|
||||
} catch (error: any) {
|
||||
throw new Error(error.response?.data?.message || error.message || "导出失败");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -845,6 +845,56 @@
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.popup-footer {
|
||||
margin-top: 24px;
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.export-form {
|
||||
margin-top: 20px;
|
||||
|
||||
.form-item {
|
||||
margin-bottom: 20px;
|
||||
|
||||
label {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.type-selector {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.type-option {
|
||||
padding: 8px 16px;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
background: white;
|
||||
|
||||
&:hover {
|
||||
border-color: #1677ff;
|
||||
color: #1677ff;
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: #1677ff;
|
||||
border-color: #1677ff;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.restrictions-detail {
|
||||
.restriction-detail-item {
|
||||
display: flex;
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
Avatar,
|
||||
Tag,
|
||||
Switch,
|
||||
DatePicker,
|
||||
} from "antd-mobile";
|
||||
import { Input, Pagination } from "antd";
|
||||
import NavCommon from "@/components/NavCommon";
|
||||
@@ -27,6 +28,7 @@ import {
|
||||
transferWechatFriends,
|
||||
getWechatAccountOverview,
|
||||
getWechatMoments,
|
||||
exportWechatMoments,
|
||||
} from "./api";
|
||||
import DeviceSelection from "@/components/DeviceSelection";
|
||||
import { DeviceSelectionItem } from "@/components/DeviceSelection/data";
|
||||
@@ -64,6 +66,16 @@ const WechatAccountDetail: React.FC = () => {
|
||||
const [momentsError, setMomentsError] = useState<string | null>(null);
|
||||
const MOMENTS_LIMIT = 10;
|
||||
|
||||
// 导出相关状态
|
||||
const [showExportPopup, setShowExportPopup] = useState(false);
|
||||
const [exportKeyword, setExportKeyword] = useState("");
|
||||
const [exportType, setExportType] = useState<number | undefined>(undefined);
|
||||
const [exportStartTime, setExportStartTime] = useState<Date | null>(null);
|
||||
const [exportEndTime, setExportEndTime] = useState<Date | null>(null);
|
||||
const [showStartTimePicker, setShowStartTimePicker] = useState(false);
|
||||
const [showEndTimePicker, setShowEndTimePicker] = useState(false);
|
||||
const [exportLoading, setExportLoading] = useState(false);
|
||||
|
||||
// 获取基础信息
|
||||
const fetchAccountInfo = useCallback(async () => {
|
||||
if (!id) return;
|
||||
@@ -370,6 +382,50 @@ const WechatAccountDetail: React.FC = () => {
|
||||
fetchMomentsList(momentsPage + 1, true);
|
||||
};
|
||||
|
||||
// 处理朋友圈导出
|
||||
const handleExportMoments = useCallback(async () => {
|
||||
if (!id) {
|
||||
Toast.show({ content: "微信ID不存在", position: "top" });
|
||||
return;
|
||||
}
|
||||
|
||||
setExportLoading(true);
|
||||
try {
|
||||
// 格式化时间
|
||||
const formatDate = (date: Date | null): string | undefined => {
|
||||
if (!date) return undefined;
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, "0");
|
||||
const day = String(date.getDate()).padStart(2, "0");
|
||||
return `${year}-${month}-${day}`;
|
||||
};
|
||||
|
||||
await exportWechatMoments({
|
||||
wechatId: id,
|
||||
keyword: exportKeyword || undefined,
|
||||
type: exportType,
|
||||
startTime: formatDate(exportStartTime),
|
||||
endTime: formatDate(exportEndTime),
|
||||
});
|
||||
|
||||
Toast.show({ content: "导出成功", position: "top" });
|
||||
setShowExportPopup(false);
|
||||
// 重置筛选条件
|
||||
setExportKeyword("");
|
||||
setExportType(undefined);
|
||||
setExportStartTime(null);
|
||||
setExportEndTime(null);
|
||||
} catch (error: any) {
|
||||
console.error("导出失败:", error);
|
||||
Toast.show({
|
||||
content: error.message || "导出失败,请重试",
|
||||
position: "top",
|
||||
});
|
||||
} finally {
|
||||
setExportLoading(false);
|
||||
}
|
||||
}, [id, exportKeyword, exportType, exportStartTime, exportEndTime]);
|
||||
|
||||
const formatMomentDateParts = (dateString: string) => {
|
||||
const date = new Date(dateString);
|
||||
if (Number.isNaN(date.getTime())) {
|
||||
@@ -810,7 +866,10 @@ const WechatAccountDetail: React.FC = () => {
|
||||
<span className={style["action-icon-video"]}></span>
|
||||
<span className={style["action-text"]}>视频</span>
|
||||
</div>
|
||||
<div className={style["action-button-dark"]}>
|
||||
<div
|
||||
className={style["action-button-dark"]}
|
||||
onClick={() => setShowExportPopup(true)}
|
||||
>
|
||||
<span className={style["action-icon-export"]}></span>
|
||||
<span className={style["action-text-light"]}>导出</span>
|
||||
</div>
|
||||
@@ -1023,6 +1082,153 @@ const WechatAccountDetail: React.FC = () => {
|
||||
</div>
|
||||
</Popup>
|
||||
|
||||
{/* 朋友圈导出弹窗 */}
|
||||
<Popup
|
||||
visible={showExportPopup}
|
||||
onMaskClick={() => setShowExportPopup(false)}
|
||||
bodyStyle={{ borderRadius: "16px 16px 0 0" }}
|
||||
>
|
||||
<div className={style["popup-content"]}>
|
||||
<div className={style["popup-header"]}>
|
||||
<h3>导出朋友圈</h3>
|
||||
<Button
|
||||
size="small"
|
||||
fill="outline"
|
||||
onClick={() => setShowExportPopup(false)}
|
||||
>
|
||||
关闭
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className={style["export-form"]}>
|
||||
{/* 关键词搜索 */}
|
||||
<div className={style["form-item"]}>
|
||||
<label>关键词搜索</label>
|
||||
<Input
|
||||
placeholder="请输入关键词"
|
||||
value={exportKeyword}
|
||||
onChange={e => setExportKeyword(e.target.value)}
|
||||
allowClear
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 类型筛选 */}
|
||||
<div className={style["form-item"]}>
|
||||
<label>类型筛选</label>
|
||||
<div className={style["type-selector"]}>
|
||||
<div
|
||||
className={`${style["type-option"]} ${
|
||||
exportType === undefined ? style["active"] : ""
|
||||
}`}
|
||||
onClick={() => setExportType(undefined)}
|
||||
>
|
||||
全部
|
||||
</div>
|
||||
<div
|
||||
className={`${style["type-option"]} ${
|
||||
exportType === 4 ? style["active"] : ""
|
||||
}`}
|
||||
onClick={() => setExportType(4)}
|
||||
>
|
||||
文本
|
||||
</div>
|
||||
<div
|
||||
className={`${style["type-option"]} ${
|
||||
exportType === 1 ? style["active"] : ""
|
||||
}`}
|
||||
onClick={() => setExportType(1)}
|
||||
>
|
||||
图片
|
||||
</div>
|
||||
<div
|
||||
className={`${style["type-option"]} ${
|
||||
exportType === 3 ? style["active"] : ""
|
||||
}`}
|
||||
onClick={() => setExportType(3)}
|
||||
>
|
||||
视频
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 开始时间 */}
|
||||
<div className={style["form-item"]}>
|
||||
<label>开始时间</label>
|
||||
<Input
|
||||
readOnly
|
||||
placeholder="请选择开始时间"
|
||||
value={
|
||||
exportStartTime
|
||||
? exportStartTime.toLocaleDateString("zh-CN")
|
||||
: ""
|
||||
}
|
||||
onClick={() => setShowStartTimePicker(true)}
|
||||
/>
|
||||
<DatePicker
|
||||
visible={showStartTimePicker}
|
||||
title="开始时间"
|
||||
value={exportStartTime}
|
||||
onClose={() => setShowStartTimePicker(false)}
|
||||
onConfirm={val => {
|
||||
setExportStartTime(val);
|
||||
setShowStartTimePicker(false);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 结束时间 */}
|
||||
<div className={style["form-item"]}>
|
||||
<label>结束时间</label>
|
||||
<Input
|
||||
readOnly
|
||||
placeholder="请选择结束时间"
|
||||
value={
|
||||
exportEndTime ? exportEndTime.toLocaleDateString("zh-CN") : ""
|
||||
}
|
||||
onClick={() => setShowEndTimePicker(true)}
|
||||
/>
|
||||
<DatePicker
|
||||
visible={showEndTimePicker}
|
||||
title="结束时间"
|
||||
value={exportEndTime}
|
||||
onClose={() => setShowEndTimePicker(false)}
|
||||
onConfirm={val => {
|
||||
setExportEndTime(val);
|
||||
setShowEndTimePicker(false);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={style["popup-footer"]}>
|
||||
<Button
|
||||
block
|
||||
color="primary"
|
||||
onClick={handleExportMoments}
|
||||
loading={exportLoading}
|
||||
disabled={exportLoading}
|
||||
>
|
||||
{exportLoading ? "导出中..." : "确认导出"}
|
||||
</Button>
|
||||
<Button
|
||||
block
|
||||
color="danger"
|
||||
fill="outline"
|
||||
onClick={() => {
|
||||
setShowExportPopup(false);
|
||||
setExportKeyword("");
|
||||
setExportType(undefined);
|
||||
setExportStartTime(null);
|
||||
setExportEndTime(null);
|
||||
}}
|
||||
style={{ marginTop: 12 }}
|
||||
>
|
||||
取消
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Popup>
|
||||
|
||||
{/* 好友详情弹窗 */}
|
||||
{/* Removed */}
|
||||
</Layout>
|
||||
|
||||
@@ -26,6 +26,11 @@ class ExportController extends Controller
|
||||
* @param array $rows 数据行,需与 $headers 的 key 对应
|
||||
* @param array $imageColumns 需要渲染为图片的列 key 列表
|
||||
* @param string $sheetName 工作表名称
|
||||
* @param array $options 额外选项:
|
||||
* - imageWidth(图片宽度,默认100)
|
||||
* - imageHeight(图片高度,默认100)
|
||||
* - imageColumnWidth(图片列宽,默认15)
|
||||
* - titleRow(标题行内容,支持多行文本数组)
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
@@ -34,7 +39,8 @@ class ExportController extends Controller
|
||||
array $headers,
|
||||
array $rows,
|
||||
array $imageColumns = [],
|
||||
$sheetName = 'Sheet1'
|
||||
$sheetName = 'Sheet1',
|
||||
array $options = []
|
||||
) {
|
||||
if (empty($headers)) {
|
||||
throw new Exception('导出列定义不能为空');
|
||||
@@ -43,22 +49,114 @@ class ExportController extends Controller
|
||||
throw new Exception('导出数据不能为空');
|
||||
}
|
||||
|
||||
// 默认选项
|
||||
$imageWidth = isset($options['imageWidth']) ? (int)$options['imageWidth'] : 100;
|
||||
$imageHeight = isset($options['imageHeight']) ? (int)$options['imageHeight'] : 100;
|
||||
$imageColumnWidth = isset($options['imageColumnWidth']) ? (float)$options['imageColumnWidth'] : 15;
|
||||
$rowHeight = isset($options['rowHeight']) ? (int)$options['rowHeight'] : ($imageHeight + 10);
|
||||
|
||||
$excel = new PHPExcel();
|
||||
$sheet = $excel->getActiveSheet();
|
||||
$sheet->setTitle($sheetName);
|
||||
|
||||
$columnKeys = array_keys($headers);
|
||||
$totalColumns = count($columnKeys);
|
||||
$lastColumnLetter = self::columnLetter($totalColumns - 1);
|
||||
|
||||
// 写入表头
|
||||
// 定义特定列的固定宽度(如果未指定则使用默认值)
|
||||
$columnWidths = isset($options['columnWidths']) ? $options['columnWidths'] : [];
|
||||
|
||||
// 检查是否有标题行
|
||||
$titleRow = isset($options['titleRow']) ? $options['titleRow'] : null;
|
||||
$dataStartRow = 1; // 数据开始行(表头行)
|
||||
|
||||
// 如果有标题行,先写入标题行
|
||||
if ($titleRow && is_array($titleRow) && !empty($titleRow)) {
|
||||
$dataStartRow = 2; // 数据从第2行开始(第1行是标题,第2行是表头)
|
||||
|
||||
// 合并标题行单元格(从第一列到最后一列)
|
||||
$titleRange = 'A1:' . $lastColumnLetter . '1';
|
||||
$sheet->mergeCells($titleRange);
|
||||
|
||||
// 构建标题内容(支持多行)
|
||||
$titleContent = '';
|
||||
if (is_array($titleRow)) {
|
||||
$titleContent = implode("\n", $titleRow);
|
||||
} else {
|
||||
$titleContent = (string)$titleRow;
|
||||
}
|
||||
|
||||
// 写入标题
|
||||
$sheet->setCellValue('A1', $titleContent);
|
||||
|
||||
// 设置标题行样式
|
||||
$sheet->getStyle('A1')->applyFromArray([
|
||||
'font' => ['bold' => true, 'size' => 16],
|
||||
'alignment' => [
|
||||
'horizontal' => \PHPExcel_Style_Alignment::HORIZONTAL_CENTER,
|
||||
'vertical' => \PHPExcel_Style_Alignment::VERTICAL_CENTER,
|
||||
'wrap' => true
|
||||
],
|
||||
'fill' => [
|
||||
'type' => \PHPExcel_Style_Fill::FILL_SOLID,
|
||||
'color' => ['rgb' => 'FFF8DC'] // 浅黄色背景
|
||||
],
|
||||
'borders' => [
|
||||
'allborders' => [
|
||||
'style' => \PHPExcel_Style_Border::BORDER_THIN,
|
||||
'color' => ['rgb' => '000000']
|
||||
]
|
||||
]
|
||||
]);
|
||||
$sheet->getRowDimension(1)->setRowHeight(80); // 标题行高度
|
||||
}
|
||||
|
||||
// 写入表头并设置列宽
|
||||
$headerRow = $dataStartRow;
|
||||
foreach ($columnKeys as $index => $key) {
|
||||
$columnLetter = self::columnLetter($index);
|
||||
$sheet->setCellValue($columnLetter . '1', $headers[$key]);
|
||||
$sheet->getColumnDimension($columnLetter)->setAutoSize(true);
|
||||
$sheet->setCellValue($columnLetter . $headerRow, $headers[$key]);
|
||||
|
||||
// 如果是图片列,设置固定列宽
|
||||
if (in_array($key, $imageColumns, true)) {
|
||||
$sheet->getColumnDimension($columnLetter)->setWidth($imageColumnWidth);
|
||||
} elseif (isset($columnWidths[$key])) {
|
||||
// 如果指定了该列的宽度,使用指定宽度
|
||||
$sheet->getColumnDimension($columnLetter)->setWidth($columnWidths[$key]);
|
||||
} else {
|
||||
// 否则自动调整
|
||||
$sheet->getColumnDimension($columnLetter)->setAutoSize(true);
|
||||
}
|
||||
}
|
||||
|
||||
// 设置表头样式
|
||||
$headerRange = 'A' . $headerRow . ':' . $lastColumnLetter . $headerRow;
|
||||
$sheet->getStyle($headerRange)->applyFromArray([
|
||||
'font' => ['bold' => true, 'size' => 11],
|
||||
'alignment' => [
|
||||
'horizontal' => \PHPExcel_Style_Alignment::HORIZONTAL_CENTER,
|
||||
'vertical' => \PHPExcel_Style_Alignment::VERTICAL_CENTER,
|
||||
'wrap' => true
|
||||
],
|
||||
'fill' => [
|
||||
'type' => \PHPExcel_Style_Fill::FILL_SOLID,
|
||||
'color' => ['rgb' => 'FFF8DC']
|
||||
],
|
||||
'borders' => [
|
||||
'allborders' => [
|
||||
'style' => \PHPExcel_Style_Border::BORDER_THIN,
|
||||
'color' => ['rgb' => '000000']
|
||||
]
|
||||
]
|
||||
]);
|
||||
$sheet->getRowDimension($headerRow)->setRowHeight(30); // 增加表头行高以确保文本完整显示
|
||||
|
||||
// 写入数据与图片
|
||||
$dataRowStart = $dataStartRow + 1; // 数据从表头行下一行开始
|
||||
foreach ($rows as $rowIndex => $rowData) {
|
||||
$excelRow = $rowIndex + 2; // 数据从第 2 行开始
|
||||
$excelRow = $dataRowStart + $rowIndex; // 数据行
|
||||
$maxRowHeight = $rowHeight; // 记录当前行的最大高度
|
||||
|
||||
foreach ($columnKeys as $colIndex => $key) {
|
||||
$columnLetter = self::columnLetter($colIndex);
|
||||
$cell = $columnLetter . $excelRow;
|
||||
@@ -67,21 +165,79 @@ class ExportController extends Controller
|
||||
if (in_array($key, $imageColumns, true) && !empty($value)) {
|
||||
$imagePath = self::resolveImagePath($value);
|
||||
if ($imagePath) {
|
||||
$drawing = new PHPExcel_Worksheet_Drawing();
|
||||
$drawing->setPath($imagePath);
|
||||
$drawing->setCoordinates($cell);
|
||||
$drawing->setOffsetX(5);
|
||||
$drawing->setOffsetY(5);
|
||||
$drawing->setHeight(60);
|
||||
$drawing->setWorksheet($sheet);
|
||||
$sheet->getRowDimension($excelRow)->setRowHeight(60);
|
||||
// 获取图片实际尺寸并等比例缩放
|
||||
$imageSize = @getimagesize($imagePath);
|
||||
if ($imageSize) {
|
||||
$originalWidth = $imageSize[0];
|
||||
$originalHeight = $imageSize[1];
|
||||
|
||||
// 计算等比例缩放后的尺寸
|
||||
$ratio = min($imageWidth / $originalWidth, $imageHeight / $originalHeight);
|
||||
$scaledWidth = $originalWidth * $ratio;
|
||||
$scaledHeight = $originalHeight * $ratio;
|
||||
|
||||
// 确保不超过最大尺寸
|
||||
if ($scaledWidth > $imageWidth) {
|
||||
$scaledWidth = $imageWidth;
|
||||
$scaledHeight = $originalHeight * ($imageWidth / $originalWidth);
|
||||
}
|
||||
if ($scaledHeight > $imageHeight) {
|
||||
$scaledHeight = $imageHeight;
|
||||
$scaledWidth = $originalWidth * ($imageHeight / $originalHeight);
|
||||
}
|
||||
|
||||
$drawing = new PHPExcel_Worksheet_Drawing();
|
||||
$drawing->setPath($imagePath);
|
||||
$drawing->setCoordinates($cell);
|
||||
|
||||
// 居中显示图片(Excel列宽1单位≈7像素,行高1单位≈0.75像素)
|
||||
$cellWidthPx = $imageColumnWidth * 7;
|
||||
$cellHeightPx = $maxRowHeight * 0.75;
|
||||
$offsetX = max(2, ($cellWidthPx - $scaledWidth) / 2);
|
||||
$offsetY = max(2, ($cellHeightPx - $scaledHeight) / 2);
|
||||
|
||||
$drawing->setOffsetX((int)$offsetX);
|
||||
$drawing->setOffsetY((int)$offsetY);
|
||||
$drawing->setWidth((int)$scaledWidth);
|
||||
$drawing->setHeight((int)$scaledHeight);
|
||||
$drawing->setWorksheet($sheet);
|
||||
|
||||
// 更新行高以适应图片(留出一些边距)
|
||||
$neededHeight = (int)($scaledHeight / 0.75) + 10;
|
||||
if ($neededHeight > $maxRowHeight) {
|
||||
$maxRowHeight = $neededHeight;
|
||||
}
|
||||
} else {
|
||||
// 如果无法获取图片尺寸,使用默认尺寸
|
||||
$drawing = new PHPExcel_Worksheet_Drawing();
|
||||
$drawing->setPath($imagePath);
|
||||
$drawing->setCoordinates($cell);
|
||||
$drawing->setOffsetX(5);
|
||||
$drawing->setOffsetY(5);
|
||||
$drawing->setWidth($imageWidth);
|
||||
$drawing->setHeight($imageHeight);
|
||||
$drawing->setWorksheet($sheet);
|
||||
}
|
||||
} else {
|
||||
$sheet->setCellValue($cell, $value);
|
||||
$sheet->setCellValue($cell, '');
|
||||
}
|
||||
} else {
|
||||
$sheet->setCellValue($cell, $value);
|
||||
// 设置文本对齐和换行
|
||||
$style = $sheet->getStyle($cell);
|
||||
$style->getAlignment()->setVertical(\PHPExcel_Style_Alignment::VERTICAL_CENTER);
|
||||
$style->getAlignment()->setWrapText(true);
|
||||
// 根据列类型设置水平对齐
|
||||
if (in_array($key, ['date', 'postTime'])) {
|
||||
$style->getAlignment()->setHorizontal(\PHPExcel_Style_Alignment::HORIZONTAL_CENTER);
|
||||
} else {
|
||||
$style->getAlignment()->setHorizontal(\PHPExcel_Style_Alignment::HORIZONTAL_LEFT);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 设置行高
|
||||
$sheet->getRowDimension($excelRow)->setRowHeight($maxRowHeight);
|
||||
}
|
||||
|
||||
$safeName = preg_replace('/[^\w\-]/', '_', $fileName ?: 'export_' . date('Ymd_His'));
|
||||
|
||||
@@ -38,6 +38,7 @@ Route::group('v1/', function () {
|
||||
Route::get('getWechatInfo', 'app\cunkebao\controller\wechat\GetWechatController@getWechatInfo');
|
||||
Route::get('overview', 'app\cunkebao\controller\wechat\GetWechatOverviewV1Controller@index'); // 获取微信账号概览数据
|
||||
Route::get('moments', 'app\cunkebao\controller\wechat\GetWechatMomentsV1Controller@index'); // 获取微信朋友圈
|
||||
Route::get('moments/export', 'app\cunkebao\controller\wechat\GetWechatMomentsV1Controller@export'); // 导出微信朋友圈
|
||||
Route::get('count', 'app\cunkebao\controller\DeviceWechat@count');
|
||||
Route::get('device-count', 'app\cunkebao\controller\DeviceWechat@deviceCount'); // 获取有登录微信的设备数量
|
||||
Route::put('refresh', 'app\cunkebao\controller\DeviceWechat@refresh'); // 刷新设备微信状态
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace app\cunkebao\controller\wechat;
|
||||
|
||||
use app\common\controller\ExportController;
|
||||
use app\common\model\Device as DeviceModel;
|
||||
use app\common\model\DeviceUser as DeviceUserModel;
|
||||
use app\common\model\DeviceWechatLogin as DeviceWechatLoginModel;
|
||||
@@ -145,6 +146,164 @@ class GetWechatMomentsV1Controller extends BaseController
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出朋友圈数据到Excel
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function export()
|
||||
{
|
||||
try {
|
||||
$wechatId = $this->request->param('wechatId/s', '');
|
||||
if (empty($wechatId)) {
|
||||
return ResponseHelper::error('wechatId不能为空');
|
||||
}
|
||||
|
||||
// 权限校验:只能查看当前账号可访问的微信
|
||||
$accessibleWechatIds = $this->getAccessibleWechatIds();
|
||||
if (!in_array($wechatId, $accessibleWechatIds, true)) {
|
||||
return ResponseHelper::error('无权查看该微信的朋友圈', 403);
|
||||
}
|
||||
|
||||
// 获取对应的微信账号ID
|
||||
$accountId = Db::table('s2_wechat_account')
|
||||
->where('wechatId', $wechatId)
|
||||
->value('id');
|
||||
|
||||
if (empty($accountId)) {
|
||||
return ResponseHelper::error('微信账号不存在或尚未同步', 404);
|
||||
}
|
||||
|
||||
$query = Db::table('s2_wechat_moments')
|
||||
->where('wechatAccountId', $accountId);
|
||||
|
||||
// 关键词搜索
|
||||
if ($keyword = trim((string)$this->request->param('keyword', ''))) {
|
||||
$query->whereLike('content', '%' . $keyword . '%');
|
||||
}
|
||||
|
||||
// 类型筛选
|
||||
$type = $this->request->param('type', '');
|
||||
if ($type !== '' && $type !== null) {
|
||||
$query->where('type', (int)$type);
|
||||
}
|
||||
|
||||
// 时间筛选
|
||||
$startTime = $this->request->param('startTime', '');
|
||||
$endTime = $this->request->param('endTime', '');
|
||||
if ($startTime || $endTime) {
|
||||
$start = $startTime ? strtotime($startTime) : 0;
|
||||
$end = $endTime ? strtotime($endTime) : time();
|
||||
if ($start && $end && $end < $start) {
|
||||
return ResponseHelper::error('结束时间不能早于开始时间');
|
||||
}
|
||||
$query->whereBetween('createTime', [$start ?: 0, $end ?: time()]);
|
||||
}
|
||||
|
||||
// 获取所有数据(不分页)
|
||||
$moments = $query->order('createTime', 'desc')->select();
|
||||
|
||||
if (empty($moments)) {
|
||||
return ResponseHelper::error('暂无数据可导出');
|
||||
}
|
||||
|
||||
// 定义表头
|
||||
$headers = [
|
||||
'date' => '日期',
|
||||
'postTime' => '投放时间',
|
||||
'functionCategory' => '作用分类',
|
||||
'content' => '朋友圈文案',
|
||||
'selfReply' => '自回评内容',
|
||||
'displayForm' => '朋友圈展示形式',
|
||||
'image1' => '配图1',
|
||||
'image2' => '配图2',
|
||||
'image3' => '配图3',
|
||||
'image4' => '配图4',
|
||||
'image5' => '配图5',
|
||||
'image6' => '配图6',
|
||||
'image7' => '配图7',
|
||||
'image8' => '配图8',
|
||||
'image9' => '配图9',
|
||||
];
|
||||
|
||||
// 格式化数据
|
||||
$rows = [];
|
||||
foreach ($moments as $moment) {
|
||||
$resUrls = $this->decodeJson($moment['resUrls'] ?? null);
|
||||
$imageUrls = is_array($resUrls) ? $resUrls : [];
|
||||
|
||||
// 格式化日期和时间
|
||||
$createTime = !empty($moment['createTime'])
|
||||
? (is_numeric($moment['createTime']) ? $moment['createTime'] : strtotime($moment['createTime']))
|
||||
: 0;
|
||||
$date = $createTime ? date('Y年m月d日', $createTime) : '';
|
||||
$postTime = $createTime ? date('H:i', $createTime) : '';
|
||||
|
||||
// 判断展示形式
|
||||
$displayForm = '';
|
||||
if (!empty($moment['content']) && !empty($imageUrls)) {
|
||||
$displayForm = '文字+图片';
|
||||
} elseif (!empty($moment['content'])) {
|
||||
$displayForm = '文字';
|
||||
} elseif (!empty($imageUrls)) {
|
||||
$displayForm = '图片';
|
||||
}
|
||||
|
||||
$row = [
|
||||
'date' => $date,
|
||||
'postTime' => $postTime,
|
||||
'functionCategory' => '', // 暂时放空
|
||||
'content' => $moment['content'] ?? '',
|
||||
'selfReply' => '', // 暂时放空
|
||||
'displayForm' => $displayForm,
|
||||
];
|
||||
|
||||
// 分配图片到配图1-9列
|
||||
for ($i = 1; $i <= 9; $i++) {
|
||||
$imageKey = 'image' . $i;
|
||||
$row[$imageKey] = isset($imageUrls[$i - 1]) ? $imageUrls[$i - 1] : '';
|
||||
}
|
||||
|
||||
$rows[] = $row;
|
||||
}
|
||||
|
||||
// 定义图片列(配图1-9)
|
||||
$imageColumns = ['image1', 'image2', 'image3', 'image4', 'image5', 'image6', 'image7', 'image8', 'image9'];
|
||||
|
||||
// 生成文件名
|
||||
$fileName = '朋友圈投放_' . date('Ymd_His');
|
||||
|
||||
// 调用导出方法,优化图片显示效果
|
||||
ExportController::exportExcelWithImages(
|
||||
$fileName,
|
||||
$headers,
|
||||
$rows,
|
||||
$imageColumns,
|
||||
'朋友圈投放',
|
||||
[
|
||||
'imageWidth' => 120, // 图片宽度(像素)
|
||||
'imageHeight' => 120, // 图片高度(像素)
|
||||
'imageColumnWidth' => 18, // 图片列宽(Excel单位)
|
||||
'rowHeight' => 130, // 行高(像素)
|
||||
'columnWidths' => [ // 特定列的固定宽度
|
||||
'date' => 15, // 日期列宽
|
||||
'postTime' => 12, // 投放时间列宽
|
||||
'functionCategory' => 15, // 作用分类列宽
|
||||
'content' => 40, // 朋友圈文案列宽(自动调整可能不够)
|
||||
'selfReply' => 30, // 自回评内容列宽
|
||||
'displayForm' => 18, // 朋友圈展示形式列宽
|
||||
],
|
||||
'titleRow' => [ // 标题行内容(第一行)
|
||||
'朋友圈投放',
|
||||
'我能提供什么价值? (40%) 有谁正在和我合作 (20%) 如何和我合作? (20%) 你找我合作需要付多少钱? (20%)'
|
||||
]
|
||||
]
|
||||
);
|
||||
} catch (\Exception $e) {
|
||||
return ResponseHelper::error('导出失败:' . $e->getMessage(), 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化朋友圈数据
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user