newActivity.vue 51 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523
  1. <template>
  2. <!-- 运营活动管理 - 新增/编辑页面 -->
  3. <div class="table-box" style="width: 100%; min-height: 100%; background-color: white">
  4. <div class="header">
  5. <el-button @click="goBack"> 返回 </el-button>
  6. <h2 class="title">{{ type == "add" ? "新建" : "编辑" }}活动</h2>
  7. </div>
  8. <div class="form-wrapper">
  9. <div class="form-wrapper-main">
  10. <el-form ref="ruleFormRef" :model="activityModel" :rules="rules" class="formBox" label-width="140px">
  11. <div class="form-content">
  12. <!-- 活动类型 -->
  13. <el-form-item label="活动类型" prop="activityType">
  14. <el-select v-model="activityModel.activityType" class="form-input" clearable placeholder="请选择">
  15. <el-option label="评论有礼" :value="2" />
  16. <el-option label="营销活动" :value="1" />
  17. </el-select>
  18. </el-form-item>
  19. <!-- 活动名称 -->
  20. <el-form-item label="活动名称" prop="activityName">
  21. <el-input v-model="activityModel.activityName" class="form-input" clearable maxlength="50" placeholder="请输入" />
  22. </el-form-item>
  23. <!-- 活动时间 -->
  24. <el-form-item class="activity-time-item" label="活动时间" prop="activityTimeRange">
  25. <el-date-picker
  26. v-model="activityModel.activityTimeRange"
  27. :disabled-date="disabledDate"
  28. class="form-input"
  29. end-placeholder="结束日期"
  30. format="YYYY/MM/DD"
  31. range-separator="-"
  32. start-placeholder="开始日期"
  33. type="daterange"
  34. value-format="YYYY-MM-DD"
  35. />
  36. </el-form-item>
  37. <!-- 评论有礼相关字段 -->
  38. <template v-if="activityModel.activityType === 2">
  39. <!-- 用户可参与次数 -->
  40. <el-form-item label="用户可参与次数" prop="participationLimit">
  41. <el-input v-model="activityModel.participationLimit" placeholder="请输入" maxlength="4" />
  42. </el-form-item>
  43. <!-- 活动规则 -->
  44. <el-form-item label="活动规则" prop="activityRule">
  45. <el-cascader
  46. v-model="activityModel.activityRule"
  47. :options="ruleCascaderOptions"
  48. :props="cascaderProps"
  49. class="form-input"
  50. clearable
  51. placeholder="请选择"
  52. style="width: 100%"
  53. />
  54. </el-form-item>
  55. <!-- 优惠券 -->
  56. <el-form-item label="优惠券" prop="couponId">
  57. <el-select v-model="activityModel.couponId" class="form-input" clearable filterable placeholder="请选择">
  58. <el-option v-for="item in couponList" :key="item.id" :label="item.name" :value="item.id" />
  59. </el-select>
  60. </el-form-item>
  61. <!-- 优惠券发放数量 -->
  62. <el-form-item label="优惠券发放数量" prop="couponQuantity">
  63. <el-input v-model="activityModel.couponQuantity" placeholder="请输入" maxlength="5" />
  64. </el-form-item>
  65. </template>
  66. <!-- 营销活动相关字段 -->
  67. <template v-if="activityModel.activityType === 1">
  68. <!-- 报名时间 -->
  69. <el-form-item class="activity-time-item" label="报名时间" prop="signupTimeRange">
  70. <el-date-picker
  71. v-model="activityModel.signupTimeRange"
  72. :disabled-date="disabledDate"
  73. class="form-input"
  74. end-placeholder="结束日期"
  75. format="YYYY/MM/DD"
  76. range-separator="-"
  77. start-placeholder="开始日期"
  78. type="daterange"
  79. value-format="YYYY-MM-DD"
  80. />
  81. </el-form-item>
  82. <!-- 活动限制人数 -->
  83. <el-form-item label="活动限制人数">
  84. <el-input
  85. style="width: 95%"
  86. v-model="activityModel.activityLimitPeople"
  87. placeholder="请输入"
  88. maxlength="20"
  89. @input="handlePositiveIntegerInput('activityLimitPeople', $event)"
  90. />
  91. <div style="width: 5%; text-align: right">人</div>
  92. </el-form-item>
  93. <!-- 活动详情 -->
  94. <el-form-item label="活动详情" prop="activityDetails">
  95. <el-input
  96. v-model="activityModel.activityDetails"
  97. type="textarea"
  98. :rows="6"
  99. placeholder="请输入活动详情"
  100. maxlength="1000"
  101. show-word-limit
  102. />
  103. </el-form-item>
  104. </template>
  105. <!-- 上传图片方式 -->
  106. <el-form-item label="活动图片类型" prop="uploadImgType">
  107. <el-radio-group v-model="activityModel.uploadImgType">
  108. <el-radio :label="1"> 本地上传 </el-radio>
  109. <el-radio :label="2"> AI生成 </el-radio>
  110. </el-radio-group>
  111. </el-form-item>
  112. <!-- 图片描述(当选择使用描述时显示) -->
  113. <el-form-item v-if="activityModel.uploadImgType === 2" label="图片描述" prop="imgDescribe">
  114. <el-input
  115. v-model="activityModel.imgDescribe"
  116. type="textarea"
  117. :rows="4"
  118. placeholder="请输入图片描述"
  119. maxlength="500"
  120. show-word-limit
  121. />
  122. </el-form-item>
  123. <!-- 活动标题图 -->
  124. <el-form-item v-if="activityModel.uploadImgType === 1" label="活动标题图" prop="activityTitleImage">
  125. <div class="upload-item-wrapper">
  126. <div class="upload-area upload-area-horizontal-21-9" :class="{ 'upload-full': titleFileList.length >= 1 }">
  127. <el-upload
  128. v-model:file-list="titleFileList"
  129. :accept="'.jpg,.jpeg,.png'"
  130. :auto-upload="false"
  131. :before-remove="handleBeforeRemove"
  132. :disabled="hasUnuploadedImages"
  133. :limit="1"
  134. :on-change="handleTitleUploadChange"
  135. :on-exceed="handleUploadExceed"
  136. :on-preview="handlePictureCardPreview"
  137. :on-remove="handleTitleRemove"
  138. :show-file-list="true"
  139. list-type="picture-card"
  140. >
  141. <template #trigger>
  142. <div v-if="titleFileList.length < 1" class="upload-trigger-card el-upload--picture-card">
  143. <el-icon>
  144. <Plus />
  145. </el-icon>
  146. </div>
  147. </template>
  148. </el-upload>
  149. </div>
  150. <div class="upload-hint">请上传21:9尺寸图片效果更佳,支持jpg、jpeg、png格式,上传图片不得超过5M</div>
  151. </div>
  152. </el-form-item>
  153. <!-- 活动详情图 -->
  154. <el-form-item v-if="activityModel.uploadImgType === 1" label="活动详情图" prop="activityDetailImage">
  155. <div class="upload-item-wrapper">
  156. <div class="upload-area upload-area-vertical" :class="{ 'upload-full': detailFileList.length >= 9 }">
  157. <el-upload
  158. v-model:file-list="detailFileList"
  159. :accept="'.jpg,.jpeg,.png'"
  160. :auto-upload="false"
  161. :before-remove="handleBeforeRemove"
  162. :disabled="hasUnuploadedImages"
  163. :limit="9"
  164. :on-change="handleDetailUploadChange"
  165. :on-exceed="handleDetailUploadExceed"
  166. :on-preview="handlePictureCardPreview"
  167. :on-remove="handleDetailRemove"
  168. :show-file-list="true"
  169. list-type="picture-card"
  170. >
  171. <template #trigger>
  172. <div v-if="detailFileList.length < 9" class="upload-trigger-card el-upload--picture-card">
  173. <el-icon>
  174. <Plus />
  175. </el-icon>
  176. </div>
  177. </template>
  178. </el-upload>
  179. </div>
  180. <div class="upload-hint">请上传竖版图片,支持jpg、jpeg、png格式,最多上传9张,单张图片不得超过5M</div>
  181. </div>
  182. </el-form-item>
  183. </div>
  184. </el-form>
  185. </div>
  186. <!-- 底部按钮区域 -->
  187. <div class="button-container">
  188. <el-button @click="goBack"> 取消 </el-button>
  189. <el-button type="primary" @click="handleSubmit()"> 提交审核 </el-button>
  190. </div>
  191. </div>
  192. <!-- 图片预览 -->
  193. <el-image-viewer
  194. v-if="imageViewerVisible"
  195. :initial-index="imageViewerInitialIndex"
  196. :url-list="imageViewerUrlList"
  197. @close="imageViewerVisible = false"
  198. />
  199. </div>
  200. </template>
  201. <script lang="tsx" name="newActivity" setup>
  202. /**
  203. * 运营活动管理 - 新增/编辑页面
  204. * 功能:支持运营活动的新增和编辑操作
  205. */
  206. import { computed, nextTick, onMounted, reactive, ref, watch } from "vue";
  207. import type { FormInstance, UploadFile, UploadProps } from "element-plus";
  208. import { ElMessage, ElMessageBox } from "element-plus";
  209. import { Plus } from "@element-plus/icons-vue";
  210. import { useRoute, useRouter } from "vue-router";
  211. import {
  212. addActivity,
  213. getActivityDetail,
  214. getActivityRuleOptions,
  215. getCouponList,
  216. updateActivity
  217. } from "@/api/modules/operationManagement";
  218. import { uploadContractImage } from "@/api/modules/licenseManagement";
  219. import { localGet } from "@/utils";
  220. // ==================== 响应式数据定义 ====================
  221. // 路由相关
  222. const router = useRouter();
  223. const route = useRoute();
  224. // 页面状态
  225. const type = ref<string>(""); // 页面类型:add-新增, edit-编辑
  226. const id = ref<string>(""); // 页面ID参数
  227. // 表单引用
  228. const ruleFormRef = ref<FormInstance>();
  229. // 优惠券列表
  230. const couponList = ref<any[]>([]);
  231. // 文件上传相关
  232. const titleFileList = ref<UploadFile[]>([]);
  233. const detailFileList = ref<UploadFile[]>([]);
  234. const titleImageUrl = ref<string>("");
  235. const detailImageUrl = ref<string>("");
  236. const pendingUploadFiles = ref<UploadFile[]>([]);
  237. const uploading = ref(false);
  238. const imageViewerVisible = ref(false);
  239. const imageViewerUrlList = ref<string[]>([]);
  240. const imageViewerInitialIndex = ref(0);
  241. // 活动规则级联选择器选项
  242. const ruleCascaderOptions = ref<any[]>([]);
  243. // 级联选择器配置
  244. const cascaderProps = {
  245. expandTrigger: "hover" as const,
  246. emitPath: true,
  247. disabled: (data: any) => {
  248. // 除了 "当用户 > 核销并评论 > 优惠券" 这个路径,其余选项不可选择
  249. if (data.disabled !== undefined) {
  250. return data.disabled;
  251. }
  252. return false;
  253. }
  254. };
  255. // 是否有未上传的图片
  256. const hasUnuploadedImages = computed(() => {
  257. return (
  258. titleFileList.value.some(file => file.status === "ready" || file.status === "uploading") ||
  259. detailFileList.value.some(file => file.status === "ready" || file.status === "uploading")
  260. );
  261. });
  262. // ==================== 表单验证规则 ====================
  263. const rules = reactive({
  264. activityType: [{ required: true, message: "请选择活动类型", trigger: "change" }],
  265. activityName: [{ required: true, message: "请输入活动名称", trigger: "blur" }],
  266. activityTimeRange: [
  267. { required: true, message: "请选择活动时间", trigger: "change" },
  268. {
  269. validator: (rule: any, value: any, callback: any) => {
  270. if (!value || !Array.isArray(value) || value.length !== 2) {
  271. callback(new Error("请选择活动时间"));
  272. return;
  273. }
  274. const [startTime, endTime] = value;
  275. if (!startTime || !endTime) {
  276. callback(new Error("请选择完整的活动时间"));
  277. return;
  278. }
  279. const start = new Date(startTime);
  280. const end = new Date(endTime);
  281. const today = new Date();
  282. today.setHours(0, 0, 0, 0);
  283. if (start < today) {
  284. callback(new Error("活动开始时间不能早于当前时间"));
  285. return;
  286. }
  287. if (start >= end) {
  288. callback(new Error("活动开始时间必须早于活动结束时间"));
  289. return;
  290. }
  291. callback();
  292. // 活动时间验证通过后,重新验证报名时间(如果已设置)
  293. if (
  294. activityModel.value.activityType === 1 &&
  295. activityModel.value.signupTimeRange &&
  296. Array.isArray(activityModel.value.signupTimeRange) &&
  297. activityModel.value.signupTimeRange.length === 2
  298. ) {
  299. nextTick(() => {
  300. ruleFormRef.value?.validateField("signupTimeRange");
  301. });
  302. }
  303. },
  304. trigger: "change"
  305. }
  306. ],
  307. participationLimit: [
  308. { required: true, message: "请输入用户可参与次数", trigger: "blur" },
  309. {
  310. validator: (rule: any, value: any, callback: any) => {
  311. if (activityModel.value.activityType === 2) {
  312. if (!value) {
  313. callback(new Error("请输入用户可参与次数"));
  314. return;
  315. }
  316. const numValue = Number(value);
  317. if (isNaN(numValue) || !Number.isInteger(numValue) || numValue <= 0) {
  318. callback(new Error("用户可参与次数必须为正整数"));
  319. return;
  320. }
  321. if (numValue > 9999) {
  322. callback(new Error("用户可参与次数必须小于9999"));
  323. return;
  324. }
  325. }
  326. callback();
  327. },
  328. trigger: "blur"
  329. }
  330. ],
  331. activityRule: [
  332. { required: true, message: "请选择活动规则", trigger: "change" },
  333. {
  334. validator: (rule: any, value: any, callback: any) => {
  335. if (activityModel.value.activityType === 2) {
  336. if (!value || !Array.isArray(value) || value.length < 2) {
  337. callback(new Error("请选择完整的活动规则(至少选择角色和行为)"));
  338. return;
  339. }
  340. }
  341. callback();
  342. },
  343. trigger: "change"
  344. }
  345. ],
  346. couponId: [
  347. { required: true, message: "请选择优惠券", trigger: "change" },
  348. {
  349. validator: (rule: any, value: any, callback: any) => {
  350. if (activityModel.value.activityType === 2) {
  351. if (!value) {
  352. callback(new Error("请选择优惠券"));
  353. return;
  354. }
  355. }
  356. callback();
  357. },
  358. trigger: "change"
  359. }
  360. ],
  361. couponQuantity: [
  362. { required: true, message: "请输入优惠券发放数量", trigger: "blur" },
  363. {
  364. validator: (rule: any, value: any, callback: any) => {
  365. if (activityModel.value.activityType === 2) {
  366. if (!value) {
  367. callback(new Error("请输入优惠券发放数量"));
  368. return;
  369. }
  370. const numValue = Number(value);
  371. if (isNaN(numValue) || !Number.isInteger(numValue) || numValue <= 0) {
  372. callback(new Error("优惠券发放数量必须为正整数"));
  373. return;
  374. }
  375. if (numValue > 99999) {
  376. callback(new Error("优惠券发放数量必须小于99999"));
  377. return;
  378. }
  379. }
  380. callback();
  381. },
  382. trigger: "blur"
  383. }
  384. ],
  385. signupTimeRange: [
  386. { required: true, message: "请选择报名时间", trigger: "change" },
  387. {
  388. validator: (rule: any, value: any, callback: any) => {
  389. if (activityModel.value.activityType === 1) {
  390. if (!value || !Array.isArray(value) || value.length !== 2) {
  391. callback(new Error("请选择报名时间"));
  392. return;
  393. }
  394. const [startTime, endTime] = value;
  395. if (!startTime || !endTime) {
  396. callback(new Error("请选择完整的报名时间"));
  397. return;
  398. }
  399. const start = new Date(startTime);
  400. const end = new Date(endTime);
  401. if (start >= end) {
  402. callback(new Error("报名开始时间必须早于报名结束时间"));
  403. return;
  404. }
  405. // 检查报名结束时间是否在活动开始时间之前
  406. if (
  407. activityModel.value.activityTimeRange &&
  408. Array.isArray(activityModel.value.activityTimeRange) &&
  409. activityModel.value.activityTimeRange.length === 2
  410. ) {
  411. const [activityStartTime] = activityModel.value.activityTimeRange;
  412. if (activityStartTime) {
  413. const activityStart = new Date(activityStartTime);
  414. if (end >= activityStart) {
  415. callback(new Error("报名结束时间必须在活动开始时间之前"));
  416. return;
  417. }
  418. }
  419. }
  420. }
  421. callback();
  422. },
  423. trigger: "change"
  424. }
  425. ],
  426. activityDetails: [
  427. { required: true, message: "请输入活动详情", trigger: ["blur", "change"] },
  428. {
  429. validator: (rule: any, value: any, callback: any) => {
  430. if (activityModel.value.activityType === 1) {
  431. if (!value || value.trim() === "") {
  432. callback(new Error("请输入活动详情"));
  433. return;
  434. }
  435. }
  436. callback();
  437. },
  438. trigger: ["blur", "change"]
  439. }
  440. ],
  441. uploadImgType: [{ required: true, message: "请选择上传图片方式", trigger: "change" }],
  442. imgDescribe: [
  443. {
  444. required: true,
  445. validator: (rule: any, value: any, callback: any) => {
  446. if (activityModel.value.uploadImgType === 2) {
  447. if (!value || value.trim() === "") {
  448. callback(new Error("请输入图片描述"));
  449. return;
  450. }
  451. }
  452. callback();
  453. },
  454. trigger: ["blur", "change"]
  455. }
  456. ],
  457. activityTitleImage: [
  458. {
  459. required: true,
  460. validator: (rule: any, value: any, callback: any) => {
  461. if (activityModel.value.uploadImgType === 1) {
  462. if (!titleImageUrl.value) {
  463. callback(new Error("请上传活动标题图"));
  464. return;
  465. }
  466. }
  467. callback();
  468. },
  469. trigger: ["change", "blur"]
  470. }
  471. ],
  472. activityDetailImage: [
  473. {
  474. required: true,
  475. validator: (rule: any, value: any, callback: any) => {
  476. if (activityModel.value.uploadImgType === 1) {
  477. // 检查是否有成功上传的图片
  478. const successFiles = detailFileList.value.filter((f: any) => f.status === "success" && f.url);
  479. if (successFiles.length === 0) {
  480. callback(new Error("请上传活动详情图"));
  481. return;
  482. }
  483. }
  484. callback();
  485. },
  486. trigger: ["change", "blur"]
  487. }
  488. ]
  489. });
  490. // ==================== 活动信息数据模型 ====================
  491. const activityModel = ref<any>({
  492. // 活动类型:1-营销活动,2-评论有礼
  493. activityType: 1,
  494. // 活动宣传图(包含标题和详情)
  495. promotionImages: null,
  496. // 活动标题图片
  497. activityTitleImg: null,
  498. // 活动详情图片
  499. activityDetailImg: null,
  500. // 活动标题图(用于表单验证)
  501. activityTitleImage: null,
  502. // 活动详情图(用于表单验证)
  503. activityDetailImage: null,
  504. // 活动名称
  505. activityName: "",
  506. // 活动时间范围
  507. activityTimeRange: [],
  508. // 用户可参与次数(评论有礼)
  509. participationLimit: "",
  510. // 活动规则(级联选择器的值数组)(评论有礼)
  511. activityRule: [],
  512. // 优惠券ID(评论有礼)
  513. couponId: "",
  514. // 优惠券发放数量(评论有礼)
  515. couponQuantity: "",
  516. // 报名时间范围(营销活动)
  517. signupTimeRange: [],
  518. // 活动限制人数(营销活动)
  519. activityLimitPeople: "",
  520. // 活动详情(营销活动)
  521. activityDetails: "",
  522. // 上传图片方式:1-正常用户,2-使用描述
  523. uploadImgType: 1,
  524. // 图片描述(当uploadImgType为2时使用)
  525. imgDescribe: ""
  526. });
  527. // ==================== 日期选择器禁用规则 ====================
  528. // 禁用日期(不能早于今天)
  529. const disabledDate = (time: Date) => {
  530. const today = new Date();
  531. today.setHours(0, 0, 0, 0);
  532. return time.getTime() < today.getTime();
  533. };
  534. // ==================== 图片参数转换函数 ====================
  535. /**
  536. * 图片URL转换为upload组件的参数
  537. * @param imageUrl 图片URL
  538. * @returns UploadFile对象
  539. */
  540. const handleImageParam = (imageUrl: string): UploadFile => {
  541. // 使用split方法以'/'为分隔符将URL拆分成数组
  542. const parts = imageUrl.split("/");
  543. // 取数组的最后一项,即图片名称
  544. const imageName = parts[parts.length - 1];
  545. return {
  546. uid: Date.now() + Math.random(),
  547. name: imageName,
  548. status: "success",
  549. percentage: 100,
  550. url: imageUrl
  551. } as unknown as UploadFile;
  552. };
  553. // ==================== 文件上传相关函数 ====================
  554. /**
  555. * 检查文件是否在排队中(未上传)
  556. */
  557. const isFilePending = (file: any): boolean => {
  558. if (file.status === "ready") {
  559. return true;
  560. }
  561. if (pendingUploadFiles.value.some(item => item.uid === file.uid)) {
  562. return true;
  563. }
  564. return false;
  565. };
  566. /**
  567. * 图片上传 - 删除前确认
  568. */
  569. const handleBeforeRemove = async (uploadFile: any, uploadFiles: any[]): Promise<boolean> => {
  570. if (isFilePending(uploadFile)) {
  571. ElMessage.warning("图片尚未上传,请等待上传完成后再删除");
  572. return false;
  573. }
  574. try {
  575. await ElMessageBox.confirm("确定要删除这张图片吗?", "提示", {
  576. confirmButtonText: "确定",
  577. cancelButtonText: "取消",
  578. type: "warning"
  579. });
  580. return true;
  581. } catch {
  582. return false;
  583. }
  584. };
  585. /**
  586. * 活动标题图片上传 - 移除图片回调
  587. */
  588. const handleTitleRemove: UploadProps["onRemove"] = (uploadFile, uploadFiles) => {
  589. const file = uploadFile as any;
  590. const imageUrl = file.url;
  591. if (imageUrl) {
  592. titleImageUrl.value = "";
  593. activityModel.value.activityTitleImg = null;
  594. activityModel.value.activityTitleImage = null;
  595. // 触发表单验证
  596. nextTick(() => {
  597. ruleFormRef.value?.validateField("activityTitleImage");
  598. });
  599. }
  600. if (file.url && file.url.startsWith("blob:")) {
  601. URL.revokeObjectURL(file.url);
  602. }
  603. titleFileList.value = [...uploadFiles];
  604. ElMessage.success("图片已删除");
  605. };
  606. /**
  607. * 活动详情图片上传 - 移除图片回调
  608. */
  609. const handleDetailRemove: UploadProps["onRemove"] = (uploadFile, uploadFiles) => {
  610. const file = uploadFile as any;
  611. const imageUrl = file.url;
  612. // 更新图片URL列表(移除已删除的图片)
  613. const successFiles = uploadFiles.filter((f: any) => f.status === "success" && f.url);
  614. const imageUrls = successFiles.map((f: any) => f.url);
  615. detailImageUrl.value = imageUrls.length > 0 ? imageUrls.join(",") : "";
  616. activityModel.value.activityDetailImg = imageUrls.length > 0 ? { url: imageUrls.join(",") } : null;
  617. activityModel.value.activityDetailImage = imageUrls.length > 0 ? imageUrls.join(",") : "";
  618. // 触发表单验证
  619. nextTick(() => {
  620. ruleFormRef.value?.validateField("activityDetailImage");
  621. });
  622. if (file.url && file.url.startsWith("blob:")) {
  623. URL.revokeObjectURL(file.url);
  624. }
  625. detailFileList.value = [...uploadFiles];
  626. ElMessage.success("图片已删除");
  627. };
  628. /**
  629. * 上传文件超出限制提示(标题图)
  630. */
  631. const handleUploadExceed: UploadProps["onExceed"] = () => {
  632. ElMessage.warning("最多只能上传1张图片");
  633. };
  634. /**
  635. * 活动详情图上传超出限制提示
  636. */
  637. const handleDetailUploadExceed: UploadProps["onExceed"] = () => {
  638. ElMessage.warning("最多只能上传9张图片");
  639. };
  640. /**
  641. * 活动标题图片上传 - 文件变更
  642. */
  643. const handleTitleUploadChange: UploadProps["onChange"] = async (uploadFile, uploadFiles) => {
  644. if (uploadFile.raw) {
  645. const fileType = uploadFile.raw.type.toLowerCase();
  646. const fileName = uploadFile.name.toLowerCase();
  647. const validTypes = ["image/jpeg", "image/jpg", "image/png"];
  648. const validExtensions = [".jpg", ".jpeg", ".png"];
  649. const isValidType = validTypes.includes(fileType) || validExtensions.some(ext => fileName.endsWith(ext));
  650. if (!isValidType) {
  651. // 从文件列表中移除不符合类型的文件
  652. const index = titleFileList.value.findIndex((f: any) => f.uid === uploadFile.uid);
  653. if (index > -1) {
  654. titleFileList.value.splice(index, 1);
  655. }
  656. // 从 uploadFiles 中移除
  657. const uploadIndex = uploadFiles.findIndex((f: any) => f.uid === uploadFile.uid);
  658. if (uploadIndex > -1) {
  659. uploadFiles.splice(uploadIndex, 1);
  660. }
  661. // 如果文件有 blob URL,释放它
  662. if (uploadFile.url && uploadFile.url.startsWith("blob:")) {
  663. URL.revokeObjectURL(uploadFile.url);
  664. }
  665. ElMessage.warning("只支持上传 JPG、JPEG 和 PNG 格式的图片");
  666. return;
  667. }
  668. // 检查文件大小,不得超过5M
  669. const maxSize = 5 * 1024 * 1024; // 5MB
  670. if (uploadFile.raw.size > maxSize) {
  671. // 从文件列表中移除超过大小的文件
  672. const index = titleFileList.value.findIndex((f: any) => f.uid === uploadFile.uid);
  673. if (index > -1) {
  674. titleFileList.value.splice(index, 1);
  675. }
  676. // 从 uploadFiles 中移除
  677. const uploadIndex = uploadFiles.findIndex((f: any) => f.uid === uploadFile.uid);
  678. if (uploadIndex > -1) {
  679. uploadFiles.splice(uploadIndex, 1);
  680. }
  681. // 如果文件有 blob URL,释放它
  682. if (uploadFile.url && uploadFile.url.startsWith("blob:")) {
  683. URL.revokeObjectURL(uploadFile.url);
  684. }
  685. ElMessage.warning("上传图片不得超过5M");
  686. return;
  687. }
  688. }
  689. const existingIndex = titleFileList.value.findIndex((f: any) => f.uid === uploadFile.uid);
  690. if (existingIndex === -1) {
  691. titleFileList.value.push(uploadFile);
  692. }
  693. const readyFiles = titleFileList.value.filter(file => file.status === "ready");
  694. if (readyFiles.length) {
  695. readyFiles.forEach(file => {
  696. if (!pendingUploadFiles.value.some(item => item.uid === file.uid)) {
  697. pendingUploadFiles.value.push(file);
  698. }
  699. });
  700. }
  701. processUploadQueue("title");
  702. };
  703. /**
  704. * 活动详情图片上传 - 文件变更
  705. */
  706. const handleDetailUploadChange: UploadProps["onChange"] = async (uploadFile, uploadFiles) => {
  707. if (uploadFile.raw) {
  708. const fileType = uploadFile.raw.type.toLowerCase();
  709. const fileName = uploadFile.name.toLowerCase();
  710. const validTypes = ["image/jpeg", "image/jpg", "image/png"];
  711. const validExtensions = [".jpg", ".jpeg", ".png"];
  712. const isValidType = validTypes.includes(fileType) || validExtensions.some(ext => fileName.endsWith(ext));
  713. if (!isValidType) {
  714. // 从文件列表中移除不符合类型的文件
  715. const index = detailFileList.value.findIndex((f: any) => f.uid === uploadFile.uid);
  716. if (index > -1) {
  717. detailFileList.value.splice(index, 1);
  718. }
  719. // 从 uploadFiles 中移除
  720. const uploadIndex = uploadFiles.findIndex((f: any) => f.uid === uploadFile.uid);
  721. if (uploadIndex > -1) {
  722. uploadFiles.splice(uploadIndex, 1);
  723. }
  724. // 如果文件有 blob URL,释放它
  725. if (uploadFile.url && uploadFile.url.startsWith("blob:")) {
  726. URL.revokeObjectURL(uploadFile.url);
  727. }
  728. ElMessage.warning("只支持上传 JPG、JPEG 和 PNG 格式的图片");
  729. return;
  730. }
  731. // 检查文件大小,不得超过5M
  732. const maxSize = 5 * 1024 * 1024; // 5MB
  733. if (uploadFile.raw.size > maxSize) {
  734. // 从文件列表中移除超过大小的文件
  735. const index = detailFileList.value.findIndex((f: any) => f.uid === uploadFile.uid);
  736. if (index > -1) {
  737. detailFileList.value.splice(index, 1);
  738. }
  739. // 从 uploadFiles 中移除
  740. const uploadIndex = uploadFiles.findIndex((f: any) => f.uid === uploadFile.uid);
  741. if (uploadIndex > -1) {
  742. uploadFiles.splice(uploadIndex, 1);
  743. }
  744. // 如果文件有 blob URL,释放它
  745. if (uploadFile.url && uploadFile.url.startsWith("blob:")) {
  746. URL.revokeObjectURL(uploadFile.url);
  747. }
  748. ElMessage.warning("上传图片不得超过5M");
  749. return;
  750. }
  751. }
  752. const existingIndex = detailFileList.value.findIndex((f: any) => f.uid === uploadFile.uid);
  753. if (existingIndex === -1) {
  754. detailFileList.value.push(uploadFile);
  755. }
  756. const readyFiles = detailFileList.value.filter(file => file.status === "ready");
  757. if (readyFiles.length) {
  758. readyFiles.forEach(file => {
  759. if (!pendingUploadFiles.value.some(item => item.uid === file.uid)) {
  760. pendingUploadFiles.value.push(file);
  761. }
  762. });
  763. }
  764. processUploadQueue("detail");
  765. };
  766. /**
  767. * 处理上传队列 - 逐个上传文件
  768. */
  769. const processUploadQueue = async (type: string) => {
  770. if (uploading.value || pendingUploadFiles.value.length === 0) {
  771. return;
  772. }
  773. const file = pendingUploadFiles.value.shift();
  774. if (file) {
  775. await uploadSingleFile(file, type);
  776. processUploadQueue(type);
  777. }
  778. };
  779. /**
  780. * 单文件上传图片
  781. */
  782. const uploadSingleFile = async (file: UploadFile, uploadType: string) => {
  783. if (!file.raw) {
  784. return;
  785. }
  786. const rawFile = file.raw as File;
  787. const formData = new FormData();
  788. formData.append("file", rawFile);
  789. formData.append("user", "text");
  790. file.status = "uploading";
  791. file.percentage = 0;
  792. uploading.value = true;
  793. try {
  794. const result: any = await uploadContractImage(formData);
  795. if (result?.code === 200 && result.data) {
  796. let imageUrl = result.data[0];
  797. if (!imageUrl) {
  798. throw new Error("上传成功但未获取到图片URL");
  799. }
  800. file.status = "success";
  801. file.percentage = 100;
  802. file.url = imageUrl;
  803. file.response = { url: imageUrl };
  804. if (uploadType === "title") {
  805. titleImageUrl.value = imageUrl;
  806. activityModel.value.activityTitleImg = { url: imageUrl };
  807. activityModel.value.activityTitleImage = imageUrl;
  808. // 触发表单验证
  809. nextTick(() => {
  810. ruleFormRef.value?.validateField("activityTitleImage");
  811. });
  812. } else if (uploadType === "detail") {
  813. // 支持多张图片,将所有成功上传的图片URL组合
  814. const successFiles = detailFileList.value.filter((f: any) => f.status === "success" && f.url);
  815. const imageUrls = successFiles.map((f: any) => f.url);
  816. // 如果有多张图片,用逗号连接;如果只有一张,保持字符串格式
  817. detailImageUrl.value = imageUrls.length > 0 ? imageUrls.join(",") : "";
  818. activityModel.value.activityDetailImg = imageUrls.length > 0 ? { url: imageUrls.join(",") } : null;
  819. activityModel.value.activityDetailImage = imageUrls.length > 0 ? imageUrls.join(",") : "";
  820. // 触发表单验证
  821. nextTick(() => {
  822. ruleFormRef.value?.validateField("activityDetailImage");
  823. });
  824. }
  825. } else {
  826. throw new Error(result?.msg || "图片上传失败");
  827. }
  828. } catch (error: any) {
  829. // 上传失败时保持进度条为 0
  830. file.percentage = 0;
  831. // 不要设置 file.status = "fail",直接移除文件,避免显示错误占位图
  832. if (file.url && file.url.startsWith("blob:")) {
  833. URL.revokeObjectURL(file.url);
  834. }
  835. // 从文件列表中移除失败的文件
  836. const index =
  837. uploadType === "title"
  838. ? titleFileList.value.findIndex((f: any) => f.uid === file.uid)
  839. : detailFileList.value.findIndex((f: any) => f.uid === file.uid);
  840. if (index > -1) {
  841. if (uploadType === "title") {
  842. titleFileList.value.splice(index, 1);
  843. } else {
  844. detailFileList.value.splice(index, 1);
  845. }
  846. }
  847. // Error message handled by global upload method, except for specific business logic errors
  848. if (error?.message && error.message.includes("未获取到图片URL")) {
  849. ElMessage.error(error.message);
  850. }
  851. } finally {
  852. uploading.value = false;
  853. // 触发视图更新
  854. if (uploadType === "title") {
  855. titleFileList.value = [...titleFileList.value];
  856. } else {
  857. detailFileList.value = [...detailFileList.value];
  858. }
  859. }
  860. };
  861. /**
  862. * 图片预览
  863. */
  864. const handlePictureCardPreview = (file: any) => {
  865. if (isFilePending(file)) {
  866. ElMessage.warning("图片尚未上传,请等待上传完成后再预览");
  867. return;
  868. }
  869. if (file.status === "uploading" && file.url) {
  870. imageViewerUrlList.value = [file.url];
  871. imageViewerInitialIndex.value = 0;
  872. imageViewerVisible.value = true;
  873. return;
  874. }
  875. const allFiles = [...titleFileList.value, ...detailFileList.value];
  876. const urlList = allFiles
  877. .filter((item: any) => item.status === "success" && (item.url || item.response?.data))
  878. .map((item: any) => item.url || item.response?.data);
  879. const currentIndex = urlList.findIndex((url: string) => url === (file.url || file.response?.data));
  880. if (currentIndex < 0) {
  881. ElMessage.warning("图片尚未上传完成,无法预览");
  882. return;
  883. }
  884. imageViewerUrlList.value = urlList;
  885. imageViewerInitialIndex.value = currentIndex;
  886. imageViewerVisible.value = true;
  887. };
  888. // ==================== 监听器 ====================
  889. /**
  890. * 监听活动类型变化,切换时清除相关数据并重新验证
  891. */
  892. watch(
  893. () => activityModel.value.activityType,
  894. (newVal, oldVal) => {
  895. // 如果 oldVal 为空或 undefined,说明是初始化,不处理
  896. if (oldVal === undefined || oldVal === null || oldVal === "") return;
  897. // 如果新旧值相同,不处理
  898. if (newVal === oldVal) return;
  899. nextTick(() => {
  900. if (newVal === 1) {
  901. // 切换到营销活动:清除评论有礼相关字段
  902. activityModel.value.participationLimit = "";
  903. activityModel.value.activityRule = [];
  904. activityModel.value.couponId = "";
  905. activityModel.value.couponQuantity = "";
  906. // 清除评论有礼字段的验证状态
  907. ruleFormRef.value?.clearValidate(["participationLimit", "activityRule", "couponId", "couponQuantity"]);
  908. } else if (newVal === 2) {
  909. // 切换到评论有礼:清除营销活动相关字段
  910. activityModel.value.signupTimeRange = [];
  911. activityModel.value.activityLimitPeople = "";
  912. activityModel.value.activityDetails = "";
  913. // 清除营销活动字段的验证状态
  914. ruleFormRef.value?.clearValidate(["signupTimeRange", "activityLimitPeople", "activityDetails"]);
  915. }
  916. });
  917. },
  918. { immediate: false }
  919. );
  920. /**
  921. * 监听上传图片方式变化,切换时清除相关数据并重新验证
  922. */
  923. watch(
  924. () => activityModel.value.uploadImgType,
  925. (newVal, oldVal) => {
  926. if (oldVal === undefined) return; // 初始化时不处理
  927. nextTick(() => {
  928. if (newVal === 2) {
  929. // 切换到使用描述:清除图片数据
  930. titleFileList.value = [];
  931. detailFileList.value = [];
  932. titleImageUrl.value = "";
  933. detailImageUrl.value = "";
  934. activityModel.value.activityTitleImg = null;
  935. activityModel.value.activityDetailImg = null;
  936. activityModel.value.activityTitleImage = null;
  937. activityModel.value.activityDetailImage = null;
  938. // 清除图片字段的验证
  939. ruleFormRef.value?.clearValidate(["activityTitleImage", "activityDetailImage"]);
  940. // 验证描述字段
  941. ruleFormRef.value?.validateField("imgDescribe");
  942. } else if (newVal === 1) {
  943. // 切换到正常用户:清除描述数据
  944. activityModel.value.imgDescribe = "";
  945. // 清除描述字段的验证
  946. ruleFormRef.value?.clearValidate("imgDescribe");
  947. // 验证图片字段
  948. ruleFormRef.value?.validateField(["activityTitleImage", "activityDetailImage"]);
  949. }
  950. });
  951. }
  952. );
  953. /**
  954. * 监听活动时间变化,重新验证报名时间
  955. */
  956. watch(
  957. () => activityModel.value.activityTimeRange,
  958. () => {
  959. // 当活动时间改变时,如果已设置报名时间,重新验证报名时间
  960. if (
  961. activityModel.value.activityType === 1 &&
  962. activityModel.value.signupTimeRange &&
  963. Array.isArray(activityModel.value.signupTimeRange) &&
  964. activityModel.value.signupTimeRange.length === 2
  965. ) {
  966. nextTick(() => {
  967. ruleFormRef.value?.validateField("signupTimeRange");
  968. });
  969. }
  970. }
  971. );
  972. // ==================== 生命周期钩子 ====================
  973. /**
  974. * 组件挂载时初始化
  975. */
  976. onMounted(async () => {
  977. id.value = (route.query.id as string) || "";
  978. type.value = (route.query.type as string) || "";
  979. // 加载优惠券列表
  980. try {
  981. const params = {
  982. storeId: localGet("createdId"),
  983. groupType: localGet("businessSection"),
  984. couponType: "2",
  985. couponStatus: "1",
  986. couponsFromType: 1,
  987. pageNum: 1,
  988. pageSize: 99999
  989. };
  990. const res: any = await getCouponList(params);
  991. if (res && res.code == 200) {
  992. couponList.value = res.data?.discountList?.records || [];
  993. }
  994. } catch (error) {
  995. console.error("加载优惠券列表失败:", error);
  996. }
  997. // 加载活动规则级联选择器选项
  998. try {
  999. const res: any = await getActivityRuleOptions({ page: 1, size: 99999 });
  1000. console.log("ruleCascaderOptions:", res.data);
  1001. if (res && res.code == 200) {
  1002. ruleCascaderOptions.value = res.data || [];
  1003. }
  1004. } catch (error) {
  1005. console.error("加载活动规则选项失败:", error);
  1006. }
  1007. // 编辑模式下加载数据
  1008. if (type.value != "add" && id.value) {
  1009. try {
  1010. const res: any = await getActivityDetail({ id: id.value });
  1011. if (res && res.code == 200) {
  1012. // 先设置 activityType 为数字类型,确保下拉框正确显示
  1013. if (res.data.activityType !== undefined) {
  1014. activityModel.value.activityType = Number(res.data.activityType);
  1015. }
  1016. // 合并其他数据
  1017. activityModel.value.activityName = res.data.activityName || "";
  1018. // 处理活动时间范围
  1019. if (res.data.startTime && res.data.endTime) {
  1020. // 只取日期部分(去掉时分秒)
  1021. const startDate = res.data.startTime.split(" ")[0];
  1022. const endDate = res.data.endTime.split(" ")[0];
  1023. activityModel.value.activityTimeRange = [startDate, endDate];
  1024. }
  1025. // 根据活动类型加载对应字段
  1026. if (activityModel.value.activityType === 1) {
  1027. // 营销活动:加载报名时间、活动限制人数、活动详情
  1028. if (res.data.signupStartTime && res.data.signupEndTime) {
  1029. // 只取日期部分(去掉时分秒)
  1030. const signupStartDate = res.data.signupStartTime.split(" ")[0];
  1031. const signupEndDate = res.data.signupEndTime.split(" ")[0];
  1032. activityModel.value.signupTimeRange = [signupStartDate, signupEndDate];
  1033. } else {
  1034. activityModel.value.signupTimeRange = [];
  1035. }
  1036. // 加载活动限制人数
  1037. if (res.data.activityLimitPeople !== undefined && res.data.activityLimitPeople !== null) {
  1038. activityModel.value.activityLimitPeople = String(res.data.activityLimitPeople);
  1039. } else {
  1040. activityModel.value.activityLimitPeople = "";
  1041. }
  1042. // 加载活动详情
  1043. activityModel.value.activityDetails = res.data.activityDetails || "";
  1044. } else if (activityModel.value.activityType === 2) {
  1045. // 评论有礼:加载活动规则、优惠券、优惠券发放数量、用户可参与次数
  1046. // 加载活动规则
  1047. if (res.data.activityRule) {
  1048. activityModel.value.activityRule = res.data.activityRule.split(",");
  1049. } else {
  1050. activityModel.value.activityRule = [];
  1051. }
  1052. // 加载优惠券ID
  1053. activityModel.value.couponId = res.data.couponId || "";
  1054. // 加载优惠券发放数量
  1055. if (res.data.couponQuantity !== undefined && res.data.couponQuantity !== null) {
  1056. activityModel.value.couponQuantity = String(res.data.couponQuantity);
  1057. } else {
  1058. activityModel.value.couponQuantity = "";
  1059. }
  1060. // 加载用户可参与次数
  1061. if (res.data.participationLimit !== undefined && res.data.participationLimit !== null) {
  1062. activityModel.value.participationLimit = String(res.data.participationLimit);
  1063. } else {
  1064. activityModel.value.participationLimit = "";
  1065. }
  1066. }
  1067. // 加载上传图片方式
  1068. if (res.data.uploadImgType !== undefined) {
  1069. activityModel.value.uploadImgType = res.data.uploadImgType;
  1070. } else {
  1071. activityModel.value.uploadImgType = 1; // 默认为正常用户
  1072. }
  1073. // 加载图片描述
  1074. if (res.data.imgDescribe) {
  1075. activityModel.value.imgDescribe = res.data.imgDescribe;
  1076. } else {
  1077. activityModel.value.imgDescribe = "";
  1078. }
  1079. // 根据上传图片方式决定是否加载图片数据
  1080. if (activityModel.value.uploadImgType === 1) {
  1081. // 如果有标题图片,添加到文件列表
  1082. if (res.data.activityTitleImgUrl) {
  1083. const titleImgUrl = res.data.activityTitleImgUrl;
  1084. if (titleImgUrl) {
  1085. titleImageUrl.value = titleImgUrl;
  1086. activityModel.value.activityTitleImg = res.data.activityTitleImgUrl;
  1087. activityModel.value.activityTitleImage = titleImgUrl;
  1088. const titleFile = handleImageParam(titleImgUrl);
  1089. titleFileList.value = [titleFile];
  1090. }
  1091. }
  1092. // 如果有详情图片,添加到文件列表(支持多张图片,可能是字符串或数组)
  1093. if (res.data.activityDetailImgUrl) {
  1094. let detailImgUrls: string[] = [];
  1095. // 如果是字符串,可能是逗号分隔的多张图片
  1096. if (typeof res.data.activityDetailImgUrl === "string") {
  1097. detailImgUrls = res.data.activityDetailImgUrl.split(",").filter((url: string) => url.trim());
  1098. } else if (Array.isArray(res.data.activityDetailImgUrl)) {
  1099. detailImgUrls = res.data.activityDetailImgUrl.filter((url: any) => url);
  1100. }
  1101. if (detailImgUrls.length > 0) {
  1102. detailImageUrl.value = detailImgUrls.join(",");
  1103. activityModel.value.activityDetailImg = res.data.activityDetailImgUrl;
  1104. activityModel.value.activityDetailImage = detailImgUrls.join(",");
  1105. // 将多张图片添加到文件列表
  1106. detailFileList.value = detailImgUrls.map((url: string) => handleImageParam(url));
  1107. }
  1108. }
  1109. }
  1110. }
  1111. } catch (error) {
  1112. console.error("加载活动详情失败:", error);
  1113. ElMessage.error("加载活动详情失败");
  1114. }
  1115. }
  1116. await nextTick();
  1117. ruleFormRef.value?.clearValidate();
  1118. });
  1119. // ==================== 事件处理函数 ====================
  1120. /**
  1121. * 处理正整数输入(只允许输入正整数)
  1122. */
  1123. const handlePositiveIntegerInput = (field: string, value: string) => {
  1124. // 移除所有非数字字符
  1125. let filteredValue = value.replace(/[^\d]/g, "");
  1126. // 限制最大长度为20
  1127. if (filteredValue.length > 20) {
  1128. filteredValue = filteredValue.substring(0, 20);
  1129. }
  1130. // 更新对应字段的值
  1131. if (field === "activityLimitPeople") {
  1132. activityModel.value.activityLimitPeople = filteredValue;
  1133. }
  1134. };
  1135. /**
  1136. * 返回上一页
  1137. */
  1138. const goBack = () => {
  1139. router.go(-1);
  1140. };
  1141. /**
  1142. * 提交表单
  1143. */
  1144. const handleSubmit = async () => {
  1145. if (!ruleFormRef.value) return;
  1146. // 如果选择正常用户方式且有未上传的图片,阻止提交
  1147. if (activityModel.value.uploadImgType === 1 && hasUnuploadedImages.value) {
  1148. ElMessage.warning("请等待图片上传完成后再提交");
  1149. return;
  1150. }
  1151. await ruleFormRef.value.validate(async valid => {
  1152. if (valid) {
  1153. const [startTime, endTime] = activityModel.value.activityTimeRange || [];
  1154. const auditParam = {
  1155. text: `${activityModel.value.activityName}, ${activityModel.value.imgDescribe || ""}`,
  1156. image_urls: [titleImageUrl.value, detailImageUrl.value]
  1157. };
  1158. const params: any = {
  1159. activityType: activityModel.value.activityType,
  1160. activityName: activityModel.value.activityName,
  1161. startTime: startTime,
  1162. endTime: endTime,
  1163. uploadImgType: activityModel.value.uploadImgType,
  1164. storeId: localGet("createdId"),
  1165. groupType: localGet("businessSection"),
  1166. status: 1, // 1-待审核
  1167. auditParam: JSON.stringify(auditParam)
  1168. };
  1169. // 根据活动类型添加不同的字段,确保只提交对应类型的字段
  1170. if (activityModel.value.activityType === 1) {
  1171. // 营销活动:只添加营销活动相关字段
  1172. const [signupStartTime, signupEndTime] = activityModel.value.signupTimeRange || [];
  1173. params.signupStartTime = signupStartTime;
  1174. params.signupEndTime = signupEndTime;
  1175. params.activityLimitPeople = activityModel.value.activityLimitPeople;
  1176. params.activityDetails = activityModel.value.activityDetails;
  1177. // 确保不包含评论有礼的字段
  1178. delete params.participationLimit;
  1179. delete params.activityRule;
  1180. delete params.couponId;
  1181. delete params.couponQuantity;
  1182. } else if (activityModel.value.activityType === 2) {
  1183. // 评论有礼:只添加评论有礼相关字段
  1184. params.participationLimit = activityModel.value.participationLimit;
  1185. params.activityRule = activityModel.value.activityRule.join(",");
  1186. params.couponId = activityModel.value.couponId;
  1187. params.couponQuantity = activityModel.value.couponQuantity;
  1188. // 确保不包含营销活动的字段
  1189. delete params.signupStartTime;
  1190. delete params.signupEndTime;
  1191. delete params.activityLimitPeople;
  1192. delete params.activityDetails;
  1193. }
  1194. // 根据上传图片方式设置不同的参数
  1195. if (activityModel.value.uploadImgType === 1) {
  1196. // 正常用户:上传图片
  1197. params.activityTitleImg = {
  1198. imgUrl: titleImageUrl.value,
  1199. imgSort: 0,
  1200. storeId: localGet("createdId")
  1201. };
  1202. params.activityDetailImg = {
  1203. imgUrl: detailImageUrl.value,
  1204. imgSort: 0,
  1205. storeId: localGet("createdId")
  1206. };
  1207. } else if (activityModel.value.uploadImgType === 2) {
  1208. // 使用描述:提交描述文本,但依然保留图片字段,imgUrl为空
  1209. params.imgDescribe = activityModel.value.imgDescribe;
  1210. params.activityTitleImg = {
  1211. imgUrl: "",
  1212. imgSort: 0,
  1213. storeId: localGet("createdId")
  1214. };
  1215. params.activityDetailImg = {
  1216. imgUrl: "",
  1217. imgSort: 0,
  1218. storeId: localGet("createdId")
  1219. };
  1220. }
  1221. try {
  1222. let res: any;
  1223. if (type.value == "add") {
  1224. res = await addActivity(params);
  1225. } else {
  1226. params.id = id.value;
  1227. res = await updateActivity(params);
  1228. }
  1229. if (res && res.code == 200) {
  1230. ElMessage.success(type.value == "add" ? "新增成功" : "编辑成功");
  1231. goBack();
  1232. } else {
  1233. ElMessage.error(res?.msg || "操作失败");
  1234. }
  1235. } catch (error) {
  1236. console.error("提交失败:", error);
  1237. ElMessage.error("操作失败");
  1238. }
  1239. }
  1240. });
  1241. };
  1242. </script>
  1243. <style lang="scss" scoped>
  1244. /* 页面容器 */
  1245. .table-box {
  1246. display: flex;
  1247. flex-direction: column;
  1248. height: auto !important;
  1249. min-height: 100%;
  1250. }
  1251. /* 头部区域 */
  1252. .header {
  1253. display: flex;
  1254. align-items: center;
  1255. justify-content: center;
  1256. padding: 20px 24px;
  1257. background-color: #ffffff;
  1258. border-bottom: 1px solid #e4e7ed;
  1259. box-shadow: 0 2px 4px rgb(0 0 0 / 2%);
  1260. }
  1261. .title {
  1262. flex: 1;
  1263. margin: 0;
  1264. font-size: 18px;
  1265. font-weight: 600;
  1266. color: #303133;
  1267. text-align: center;
  1268. }
  1269. /* 表单内容区域 */
  1270. .form-content {
  1271. padding: 24px;
  1272. background-color: #ffffff;
  1273. }
  1274. /* 表单包装器 */
  1275. .form-wrapper {
  1276. display: flex;
  1277. flex-direction: column;
  1278. align-items: center;
  1279. background-color: #ffffff;
  1280. .form-wrapper-main {
  1281. width: 100%;
  1282. max-width: 800px;
  1283. }
  1284. }
  1285. /* 上传项容器 */
  1286. .upload-item-wrapper {
  1287. display: flex;
  1288. flex-direction: column;
  1289. gap: 12px;
  1290. align-items: flex-start;
  1291. width: 100%;
  1292. }
  1293. .upload-hint {
  1294. margin-top: 4px;
  1295. font-size: 12px;
  1296. line-height: 1.5;
  1297. color: #909399;
  1298. }
  1299. /* 规则表格容器 */
  1300. .rule-table-container {
  1301. margin-top: 20px;
  1302. }
  1303. /* 底部按钮区域 */
  1304. .button-container {
  1305. display: flex;
  1306. gap: 20px;
  1307. justify-content: center;
  1308. padding-bottom: 15px;
  1309. background-color: #ffffff;
  1310. }
  1311. .upload-area {
  1312. width: 100%;
  1313. :deep(.el-upload--picture-card) {
  1314. width: 100%;
  1315. height: 180px;
  1316. }
  1317. :deep(.el-upload-list--picture-card) {
  1318. width: 100%;
  1319. .el-upload-list__item {
  1320. width: 100%;
  1321. height: 180px;
  1322. margin: 0;
  1323. }
  1324. }
  1325. :deep(.el-upload-list--picture-card .el-upload-list__item:hover .el-upload-list__item-status-label) {
  1326. display: inline-flex !important;
  1327. opacity: 1 !important;
  1328. }
  1329. :deep(.el-upload-list__item.is-success:focus .el-upload-list__item-status-label) {
  1330. display: inline-flex !important;
  1331. opacity: 1 !important;
  1332. }
  1333. :deep(.el-upload-list--picture-card .el-icon--close-tip) {
  1334. display: none !important;
  1335. }
  1336. &.upload-full {
  1337. :deep(.el-upload--picture-card) {
  1338. display: none !important;
  1339. }
  1340. }
  1341. // 21:9横向图片样式
  1342. &.upload-area-horizontal-21-9 {
  1343. :deep(.el-upload--picture-card) {
  1344. width: 100%;
  1345. height: auto;
  1346. aspect-ratio: 21 / 9;
  1347. }
  1348. :deep(.el-upload-list--picture-card) {
  1349. width: 100%;
  1350. .el-upload-list__item {
  1351. width: 100%;
  1352. height: auto;
  1353. aspect-ratio: 21 / 9;
  1354. margin: 0;
  1355. }
  1356. }
  1357. }
  1358. // 竖版图片样式
  1359. &.upload-area-vertical {
  1360. :deep(.el-upload--picture-card) {
  1361. width: 150px;
  1362. height: 150px;
  1363. }
  1364. :deep(.el-upload-list--picture-card) {
  1365. .el-upload-list__item {
  1366. width: 150px;
  1367. height: 150px;
  1368. margin: 0;
  1369. }
  1370. }
  1371. }
  1372. }
  1373. .upload-trigger-card {
  1374. display: flex;
  1375. flex-direction: column;
  1376. align-items: center;
  1377. justify-content: center;
  1378. width: 100%;
  1379. height: 100%;
  1380. font-size: 28px;
  1381. color: #8c939d;
  1382. }
  1383. /* el-upload 图片预览铺满容器 */
  1384. :deep(.el-upload-list--picture-card) {
  1385. .el-upload-list__item {
  1386. margin: 0;
  1387. overflow: hidden;
  1388. .el-upload-list__item-thumbnail {
  1389. width: 100%;
  1390. height: 100%;
  1391. //object-fit: fill;
  1392. }
  1393. }
  1394. .el-upload-list__item[data-status="ready"],
  1395. .el-upload-list__item.is-ready {
  1396. position: relative;
  1397. pointer-events: none;
  1398. cursor: not-allowed;
  1399. opacity: 0.6;
  1400. &::after {
  1401. position: absolute;
  1402. inset: 0;
  1403. z-index: 1;
  1404. content: "";
  1405. background-color: rgb(0 0 0 / 30%);
  1406. }
  1407. .el-upload-list__item-actions {
  1408. pointer-events: none;
  1409. opacity: 0.5;
  1410. }
  1411. }
  1412. .el-upload-list__item:hover .el-upload-list__item-status-label {
  1413. display: inline-flex !important;
  1414. opacity: 1 !important;
  1415. }
  1416. .el-upload-list__item.is-success:focus .el-upload-list__item-status-label {
  1417. display: inline-flex !important;
  1418. opacity: 1 !important;
  1419. }
  1420. .el-icon--close-tip {
  1421. display: none !important;
  1422. }
  1423. }
  1424. </style>