|
|
@@ -59,6 +59,29 @@
|
|
|
<el-input v-model="activityModel.couponQuantity" placeholder="请输入" maxlength="5" />
|
|
|
</el-form-item>
|
|
|
|
|
|
+ <!-- AI生成活动宣传图 -->
|
|
|
+ <el-form-item label="活动宣传图描述">
|
|
|
+ <div class="ai-generate-wrapper">
|
|
|
+ <el-input
|
|
|
+ v-model="aiPromptText"
|
|
|
+ :rows="4"
|
|
|
+ maxlength="200"
|
|
|
+ placeholder="请描述您想举办的活动图,AI助手会帮您自动生成海报图~"
|
|
|
+ show-word-limit
|
|
|
+ type="textarea"
|
|
|
+ />
|
|
|
+ <el-button
|
|
|
+ :disabled="!aiPromptText.trim() || aiGenerating"
|
|
|
+ :loading="aiGenerating"
|
|
|
+ class="generate-btn"
|
|
|
+ type="primary"
|
|
|
+ @click="handleAIGenerate"
|
|
|
+ >
|
|
|
+ {{ aiGenerating ? "生成中..." : "生成图片" }}
|
|
|
+ </el-button>
|
|
|
+ </div>
|
|
|
+ </el-form-item>
|
|
|
+
|
|
|
<!-- 活动标题图 -->
|
|
|
<el-form-item label="活动标题图" prop="activityTitleImage">
|
|
|
<div class="upload-item-wrapper">
|
|
|
@@ -75,10 +98,11 @@
|
|
|
:on-preview="handlePictureCardPreview"
|
|
|
:on-remove="handleTitleRemove"
|
|
|
:show-file-list="true"
|
|
|
+ :class="{ 'upload-hidden': aiGenerated }"
|
|
|
list-type="picture-card"
|
|
|
>
|
|
|
<template #trigger>
|
|
|
- <div v-if="titleFileList.length < 1" class="upload-trigger-card el-upload--picture-card">
|
|
|
+ <div v-if="titleFileList.length < 1 && !aiGenerated" class="upload-trigger-card el-upload--picture-card">
|
|
|
<el-icon>
|
|
|
<Plus />
|
|
|
</el-icon>
|
|
|
@@ -86,7 +110,9 @@
|
|
|
</template>
|
|
|
</el-upload>
|
|
|
</div>
|
|
|
- <div class="upload-hint">请上传21:9尺寸图片效果更佳,支持jpg、jpeg、png格式,上传图片不得超过20M</div>
|
|
|
+ <div v-if="!aiGenerated" class="upload-hint">
|
|
|
+ 请上传21:9尺寸图片效果更佳,支持jpg、jpeg、png格式,上传图片不得超过20M
|
|
|
+ </div>
|
|
|
</div>
|
|
|
</el-form-item>
|
|
|
|
|
|
@@ -106,10 +132,11 @@
|
|
|
:on-preview="handlePictureCardPreview"
|
|
|
:on-remove="handleDetailRemove"
|
|
|
:show-file-list="true"
|
|
|
+ :class="{ 'upload-hidden': aiGenerated }"
|
|
|
list-type="picture-card"
|
|
|
>
|
|
|
<template #trigger>
|
|
|
- <div v-if="detailFileList.length < 1" class="upload-trigger-card el-upload--picture-card">
|
|
|
+ <div v-if="detailFileList.length < 1 && !aiGenerated" class="upload-trigger-card el-upload--picture-card">
|
|
|
<el-icon>
|
|
|
<Plus />
|
|
|
</el-icon>
|
|
|
@@ -117,7 +144,7 @@
|
|
|
</template>
|
|
|
</el-upload>
|
|
|
</div>
|
|
|
- <div class="upload-hint">请上传竖版图片,支持jpg、jpeg、png格式,上传图片不得超过20M</div>
|
|
|
+ <div v-if="!aiGenerated" class="upload-hint">请上传竖版图片,支持jpg、jpeg、png格式,上传图片不得超过20M</div>
|
|
|
</div>
|
|
|
</el-form-item>
|
|
|
</div>
|
|
|
@@ -156,7 +183,8 @@ import {
|
|
|
getActivityDetail,
|
|
|
getActivityRuleOptions,
|
|
|
getCouponList,
|
|
|
- updateActivity
|
|
|
+ updateActivity,
|
|
|
+ generatePromotionImage
|
|
|
} from "@/api/modules/operationManagement";
|
|
|
import { uploadContractImage } from "@/api/modules/licenseManagement";
|
|
|
import { localGet } from "@/utils";
|
|
|
@@ -177,6 +205,11 @@ const ruleFormRef = ref<FormInstance>();
|
|
|
// 优惠券列表
|
|
|
const couponList = ref<any[]>([]);
|
|
|
|
|
|
+// AI生成相关
|
|
|
+const aiPromptText = ref<string>("");
|
|
|
+const aiGenerating = ref<boolean>(false);
|
|
|
+const aiGenerated = ref<boolean>(false);
|
|
|
+
|
|
|
// 文件上传相关
|
|
|
const titleFileList = ref<UploadFile[]>([]);
|
|
|
const detailFileList = ref<UploadFile[]>([]);
|
|
|
@@ -435,6 +468,12 @@ const handleTitleRemove: UploadProps["onRemove"] = (uploadFile, uploadFiles) =>
|
|
|
URL.revokeObjectURL(file.url);
|
|
|
}
|
|
|
titleFileList.value = [...uploadFiles];
|
|
|
+
|
|
|
+ // 如果删除的是AI生成的图片,重置AI生成状态
|
|
|
+ if (aiGenerated.value && uploadFiles.length === 0) {
|
|
|
+ aiGenerated.value = false;
|
|
|
+ }
|
|
|
+
|
|
|
ElMessage.success("图片已删除");
|
|
|
};
|
|
|
|
|
|
@@ -459,6 +498,12 @@ const handleDetailRemove: UploadProps["onRemove"] = (uploadFile, uploadFiles) =>
|
|
|
URL.revokeObjectURL(file.url);
|
|
|
}
|
|
|
detailFileList.value = [...uploadFiles];
|
|
|
+
|
|
|
+ // 如果删除的是AI生成的图片,重置AI生成状态
|
|
|
+ if (aiGenerated.value && uploadFiles.length === 0) {
|
|
|
+ aiGenerated.value = false;
|
|
|
+ }
|
|
|
+
|
|
|
ElMessage.success("图片已删除");
|
|
|
};
|
|
|
|
|
|
@@ -828,6 +873,101 @@ const goBack = () => {
|
|
|
};
|
|
|
|
|
|
/**
|
|
|
+ * AI生成活动宣传图
|
|
|
+ */
|
|
|
+const handleAIGenerate = async () => {
|
|
|
+ if (!aiPromptText.value.trim()) {
|
|
|
+ ElMessage.warning("请输入活动宣传图描述");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ aiGenerating.value = true;
|
|
|
+
|
|
|
+ try {
|
|
|
+ const res: any = await generatePromotionImage({
|
|
|
+ text: aiPromptText.value.trim()
|
|
|
+ });
|
|
|
+
|
|
|
+ if (res && res.code === 200 && res.data) {
|
|
|
+ const { banner_image, vertical_image } = res.data;
|
|
|
+
|
|
|
+ // 处理横版图片(活动标题图)
|
|
|
+ if (banner_image && banner_image.image_base64) {
|
|
|
+ const bannerFile = await base64ToFile(banner_image.image_base64, `banner_${Date.now()}.png`, "image/png");
|
|
|
+ const bannerUploadFile: UploadFile = {
|
|
|
+ uid: Date.now(),
|
|
|
+ name: bannerFile.name,
|
|
|
+ status: "success",
|
|
|
+ percentage: 100,
|
|
|
+ url: `data:image/png;base64,${banner_image.image_base64}`,
|
|
|
+ raw: bannerFile
|
|
|
+ } as UploadFile;
|
|
|
+
|
|
|
+ titleFileList.value = [bannerUploadFile];
|
|
|
+ titleImageUrl.value = `data:image/png;base64,${banner_image.image_base64}`;
|
|
|
+ activityModel.value.activityTitleImg = { url: titleImageUrl.value };
|
|
|
+ activityModel.value.activityTitleImage = titleImageUrl.value;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 处理竖版图片(活动详情图)
|
|
|
+ if (vertical_image && vertical_image.image_base64) {
|
|
|
+ const verticalFile = await base64ToFile(vertical_image.image_base64, `vertical_${Date.now()}.png`, "image/png");
|
|
|
+ const verticalUploadFile: UploadFile = {
|
|
|
+ uid: Date.now() + 1,
|
|
|
+ name: verticalFile.name,
|
|
|
+ status: "success",
|
|
|
+ percentage: 100,
|
|
|
+ url: `data:image/png;base64,${vertical_image.image_base64}`,
|
|
|
+ raw: verticalFile
|
|
|
+ } as UploadFile;
|
|
|
+
|
|
|
+ detailFileList.value = [verticalUploadFile];
|
|
|
+ detailImageUrl.value = `data:image/png;base64,${vertical_image.image_base64}`;
|
|
|
+ activityModel.value.activityDetailImg = { url: detailImageUrl.value };
|
|
|
+ activityModel.value.activityDetailImage = detailImageUrl.value;
|
|
|
+ }
|
|
|
+
|
|
|
+ aiGenerated.value = true;
|
|
|
+
|
|
|
+ // 触发表单验证
|
|
|
+ nextTick(() => {
|
|
|
+ ruleFormRef.value?.validateField("activityTitleImage");
|
|
|
+ ruleFormRef.value?.validateField("activityDetailImage");
|
|
|
+ });
|
|
|
+
|
|
|
+ ElMessage.success("图片生成成功");
|
|
|
+ } else {
|
|
|
+ ElMessage.error(res?.message || "生成失败");
|
|
|
+ }
|
|
|
+ } catch (error: any) {
|
|
|
+ console.error("AI生成失败:", error);
|
|
|
+ ElMessage.error(error?.message || "生成失败,请稍后重试");
|
|
|
+ } finally {
|
|
|
+ aiGenerating.value = false;
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+/**
|
|
|
+ * Base64转File对象
|
|
|
+ */
|
|
|
+const base64ToFile = async (base64: string, filename: string, mimeType: string): Promise<File> => {
|
|
|
+ // 如果base64包含data URL前缀,去除它
|
|
|
+ const base64Data = base64.includes(",") ? base64.split(",")[1] : base64;
|
|
|
+
|
|
|
+ // 将base64转换为二进制数据
|
|
|
+ const byteCharacters = atob(base64Data);
|
|
|
+ const byteNumbers = new Array(byteCharacters.length);
|
|
|
+ for (let i = 0; i < byteCharacters.length; i++) {
|
|
|
+ byteNumbers[i] = byteCharacters.charCodeAt(i);
|
|
|
+ }
|
|
|
+ const byteArray = new Uint8Array(byteNumbers);
|
|
|
+ const blob = new Blob([byteArray], { type: mimeType });
|
|
|
+
|
|
|
+ // 创建File对象
|
|
|
+ return new File([blob], filename, { type: mimeType });
|
|
|
+};
|
|
|
+
|
|
|
+/**
|
|
|
* 提交表单
|
|
|
*/
|
|
|
const handleSubmit = async () => {
|
|
|
@@ -841,6 +981,37 @@ const handleSubmit = async () => {
|
|
|
|
|
|
await ruleFormRef.value.validate(async valid => {
|
|
|
if (valid) {
|
|
|
+ // 如果是AI生成的图片,需要先上传
|
|
|
+ if (aiGenerated.value) {
|
|
|
+ try {
|
|
|
+ // 上传标题图
|
|
|
+ if (titleFileList.value.length > 0 && titleFileList.value[0].raw) {
|
|
|
+ const titleFormData = new FormData();
|
|
|
+ titleFormData.append("file", titleFileList.value[0].raw);
|
|
|
+ titleFormData.append("user", "text");
|
|
|
+ const titleResult: any = await uploadContractImage(titleFormData);
|
|
|
+ if (titleResult?.code === 200 && titleResult.data) {
|
|
|
+ titleImageUrl.value = titleResult.data[0];
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 上传详情图
|
|
|
+ if (detailFileList.value.length > 0 && detailFileList.value[0].raw) {
|
|
|
+ const detailFormData = new FormData();
|
|
|
+ detailFormData.append("file", detailFileList.value[0].raw);
|
|
|
+ detailFormData.append("user", "text");
|
|
|
+ const detailResult: any = await uploadContractImage(detailFormData);
|
|
|
+ if (detailResult?.code === 200 && detailResult.data) {
|
|
|
+ detailImageUrl.value = detailResult.data[0];
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error("上传AI生成图片失败:", error);
|
|
|
+ ElMessage.error("上传图片失败");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
const [startTime, endTime] = activityModel.value.activityTimeRange || [];
|
|
|
const params: any = {
|
|
|
activityName: activityModel.value.activityName,
|
|
|
@@ -935,6 +1106,22 @@ const handleSubmit = async () => {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+/* AI生成包装器 */
|
|
|
+.ai-generate-wrapper {
|
|
|
+ display: flex;
|
|
|
+ gap: 12px;
|
|
|
+ align-items: flex-start;
|
|
|
+ width: 100%;
|
|
|
+ :deep(.el-textarea) {
|
|
|
+ flex: 1;
|
|
|
+ }
|
|
|
+ .generate-btn {
|
|
|
+ flex-shrink: 0;
|
|
|
+ align-self: flex-start;
|
|
|
+ margin-top: 2px;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
/* 上传项容器 */
|
|
|
.upload-item-wrapper {
|
|
|
display: flex;
|
|
|
@@ -950,6 +1137,13 @@ const handleSubmit = async () => {
|
|
|
color: #909399;
|
|
|
}
|
|
|
|
|
|
+/* 隐藏上传触发器 */
|
|
|
+:deep(.upload-hidden) {
|
|
|
+ .el-upload--picture-card {
|
|
|
+ display: none !important;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
/* 规则表格容器 */
|
|
|
.rule-table-container {
|
|
|
margin-top: 20px;
|