newActivity.vue 50 KB

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