publishDynamic.vue 31 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030
  1. <template>
  2. <div class="publish-dynamic-container">
  3. <!-- 头部导航 -->
  4. <div class="header-section">
  5. <el-button @click="handleGoBack" class="back-btn"> 返回 </el-button>
  6. <div class="header-title">动态发布</div>
  7. </div>
  8. <!-- 表单内容 -->
  9. <div class="form-container">
  10. <el-form ref="formRef" :model="formData" :rules="rules" label-position="top">
  11. <!-- 图片/视频上传 -->
  12. <el-form-item label="图片/视频" prop="images">
  13. <div class="upload-section">
  14. <el-upload
  15. v-model:file-list="fileList"
  16. list-type="picture-card"
  17. :limit="hasVideoInList() ? 1 : 20"
  18. :on-preview="handlePicturePreview"
  19. :on-remove="handleRemoveImage"
  20. :on-change="handleFileChange"
  21. :before-upload="beforeImageUpload"
  22. :http-request="handleImageUpload"
  23. accept="image/*,video/mp4,video/*"
  24. multiple
  25. class="dynamic-upload"
  26. >
  27. <template #file="{ file }">
  28. <div class="upload-file-preview">
  29. <!-- 视频缩略图 -->
  30. <video v-if="isVideoFile(file)" :src="file.url" class="upload-video-thumb" />
  31. <!-- 图片缩略图 -->
  32. <img v-else :src="file.url" class="upload-image-thumb" />
  33. <!-- 操作按钮 -->
  34. <div class="upload-actions">
  35. <span class="upload-action-item" @click.stop="handlePicturePreview(file)">
  36. <el-icon :size="20"><ZoomIn /></el-icon>
  37. </span>
  38. <span class="upload-action-item" @click.stop="handleRemoveFile(file)">
  39. <el-icon :size="20"><Delete /></el-icon>
  40. </span>
  41. </div>
  42. </div>
  43. </template>
  44. <div class="upload-trigger">
  45. <el-icon :size="32" color="#999">
  46. <Plus />
  47. </el-icon>
  48. <div class="upload-count">({{ fileList.length }}/{{ hasVideoInList() ? 1 : 20 }})</div>
  49. </div>
  50. </el-upload>
  51. </div>
  52. </el-form-item>
  53. <!-- 标题 -->
  54. <el-form-item label="标题" prop="title">
  55. <el-input v-model="formData.title" placeholder="请输入标题" maxlength="50" show-word-limit size="large" />
  56. </el-form-item>
  57. <!-- 正文 -->
  58. <el-form-item label="正文" prop="content">
  59. <el-input
  60. v-model="formData.content"
  61. type="textarea"
  62. placeholder="请输入正文"
  63. :rows="8"
  64. maxlength="500"
  65. show-word-limit
  66. />
  67. </el-form-item>
  68. <!-- 位置 -->
  69. <el-form-item label="位置" prop="location">
  70. <el-select
  71. v-model="formData.storePosition"
  72. filterable
  73. placeholder="请输入地址进行查询"
  74. remote
  75. reserve-keyword
  76. :remote-method="getLonAndLat"
  77. @change="selectAddress"
  78. >
  79. <el-option v-for="item in addressList" :key="item.id" :label="item.name" :value="item.location">
  80. <span style="float: left">{{ item.name }}</span>
  81. <span style="float: right; font-size: 13px; color: var(--el-text-color-secondary)">{{ item.district }}</span>
  82. </el-option>
  83. </el-select>
  84. </el-form-item>
  85. </el-form>
  86. <div class="footer-actions">
  87. <el-button size="large" class="draft-btn" @click="handleSaveDraft"> 保存草稿 </el-button>
  88. <el-button type="primary" size="large" class="publish-btn" :loading="publishing" @click="handlePublish"> 发布 </el-button>
  89. </div>
  90. </div>
  91. <!-- 图片/视频预览对话框 -->
  92. <el-dialog v-model="previewDialogVisible" width="800px" append-to-body>
  93. <!-- 视频预览 -->
  94. <video v-if="previewIsVideo" :src="previewImageUrl" controls style="width: 100%; max-height: 70vh" />
  95. <!-- 图片预览 -->
  96. <img v-else :src="previewImageUrl" alt="预览图片" style="width: 100%" />
  97. </el-dialog>
  98. <!-- 位置选择对话框 -->
  99. <el-dialog v-model="locationDialogVisible" title="选择位置" width="500px" append-to-body>
  100. <el-input v-model="locationSearch" placeholder="搜索位置" clearable>
  101. <template #prefix>
  102. <el-icon>
  103. <Search />
  104. </el-icon>
  105. </template>
  106. </el-input>
  107. <div class="location-list">
  108. <div v-for="location in locationList" :key="location.id" class="location-item" @click="handleChooseLocation(location)">
  109. <el-icon>
  110. <Location />
  111. </el-icon>
  112. <span>{{ location.name }}</span>
  113. </div>
  114. <el-empty v-if="locationList.length === 0" description="暂无位置信息" />
  115. </div>
  116. </el-dialog>
  117. </div>
  118. </template>
  119. <script setup lang="ts" name="publishDynamic">
  120. import { ref, reactive, onMounted } from "vue";
  121. import { useRouter, useRoute } from "vue-router";
  122. import { ElMessage, ElMessageBox } from "element-plus";
  123. import { ArrowLeft, Plus, Location, Search, ZoomIn, Delete } from "@element-plus/icons-vue";
  124. import type { FormInstance, FormRules, UploadUserFile, UploadFile } from "element-plus";
  125. // import { publishDynamic, saveDraft, uploadDynamicImage } from "@/api/modules/dynamicManagement";
  126. // import { addOrUpdateDynamic } from "@/api/modules/dynamicManagement";
  127. import { uploadImg, addOrUpdateDynamic } from "@/api/modules/newLoginApi";
  128. import { useUserStore } from "@/stores/modules/user";
  129. import { getUserDraftDynamics, getInputPrompt } from "@/api/modules/newLoginApi";
  130. const router = useRouter();
  131. const route = useRoute();
  132. const userStore = useUserStore();
  133. // 接口定义
  134. interface FormData {
  135. title: string;
  136. content: string;
  137. images: string[];
  138. location: string;
  139. locationId?: string;
  140. address?: string; // 经纬度
  141. addressProvince?: string; // 省市
  142. storePosition: string;
  143. queryAddress: string;
  144. storePositionLongitude: string;
  145. storePositionLatitude: string;
  146. }
  147. interface LocationItem {
  148. id: string;
  149. name: string;
  150. address: string;
  151. latitude?: string; // 纬度
  152. longitude?: string; // 经度
  153. province?: string; // 省市
  154. }
  155. // 响应式数据
  156. const formRef = ref<FormInstance>();
  157. const fileList = ref<UploadUserFile[]>([]);
  158. const previewDialogVisible = ref(false);
  159. const previewImageUrl = ref("");
  160. const previewIsVideo = ref(false); // 预览的是否为视频
  161. const locationDialogVisible = ref(false);
  162. const locationSearch = ref("");
  163. const locationList = ref<LocationItem[]>([]);
  164. const publishing = ref(false);
  165. const pendingUploadFiles = ref<UploadFile[]>([]);
  166. const uploading = ref(false);
  167. const currentDraftId = ref<number | string | null>(null); // 当前编辑的草稿ID
  168. const isEditMode = ref(false); // 是否为编辑模式
  169. // 表单数据
  170. const formData = reactive<FormData>({
  171. title: "",
  172. content: "",
  173. images: [],
  174. location: "",
  175. address: "", // 经纬度
  176. addressProvince: "", // 省市
  177. storePosition: "",
  178. queryAddress: "",
  179. storePositionLongitude: "",
  180. storePositionLatitude: ""
  181. });
  182. // 经纬度查询
  183. const addressMap = new Map();
  184. const addressList = ref<any[]>([]);
  185. const getLonAndLat = async (keyword: string) => {
  186. if (keyword) {
  187. console.log("地址查询", keyword);
  188. let param = {
  189. addressName: keyword
  190. };
  191. let res: any = await getInputPrompt(param as any);
  192. if (res.code == "200") {
  193. res.data.tips.forEach((item: any) => {
  194. addressMap.set(item.location, item.name);
  195. });
  196. addressList.value = res?.data?.tips || [];
  197. console.log("res", res);
  198. } else {
  199. ElMessage.error("新增失败!");
  200. }
  201. } else {
  202. addressList.value = [];
  203. }
  204. };
  205. const selectAddress = async (param: any) => {
  206. if (!formData.storePosition || typeof formData.storePosition !== "string") {
  207. ElMessage.warning("地址格式不正确,请重新选择");
  208. return;
  209. }
  210. if (!formData.storePosition.includes(",")) {
  211. ElMessage.warning("地址格式不正确,缺少经纬度信息");
  212. return;
  213. }
  214. // 安全地分割地址字符串
  215. let locationList = formData.storePosition.split(",");
  216. // 查找对应的地址名称
  217. addressList.value.forEach((item: any) => {
  218. console.log(item);
  219. if (item.location == formData.storePosition) {
  220. formData.queryAddress = item.name;
  221. formData.addressProvince = item.district;
  222. }
  223. });
  224. formData.storePositionLongitude = locationList[0]?.trim();
  225. formData.storePositionLatitude = locationList[1]?.trim();
  226. };
  227. // 表单验证规则
  228. const rules = reactive<FormRules<FormData>>({
  229. title: [{ required: true, message: "请输入标题", trigger: "blur" }],
  230. content: [{ required: true, message: "请输入正文", trigger: "blur" }]
  231. });
  232. // 返回上一页
  233. const handleGoBack = async () => {
  234. // 检查是否有未保存的内容
  235. if (formData.title || formData.content || fileList.value.length > 0) {
  236. try {
  237. await ElMessageBox.confirm("当前有未保存的内容,确定要离开吗?", "提示", {
  238. confirmButtonText: "确定",
  239. cancelButtonText: "取消",
  240. type: "warning"
  241. });
  242. router.back();
  243. } catch {
  244. // 用户取消
  245. }
  246. } else {
  247. router.back();
  248. }
  249. };
  250. // 文件改变
  251. const handleFileChange = (uploadFile: UploadFile, uploadFiles: UploadFile[]) => {
  252. // 验证文件
  253. if (uploadFile.raw) {
  254. const isImage = uploadFile.raw.type.startsWith("image/");
  255. const isVideo = uploadFile.raw.type.startsWith("video/");
  256. const isValidType = isImage || isVideo;
  257. if (!isValidType) {
  258. const index = uploadFiles.findIndex(f => f.uid === uploadFile.uid);
  259. if (index > -1) {
  260. uploadFiles.splice(index, 1);
  261. }
  262. ElMessage.warning("只能上传图片或视频文件");
  263. return;
  264. }
  265. // 权限:已上传视频则不能再传;已上传图片则只能继续传图片(检查时排除当前文件,用已存在的列表)
  266. const otherFiles = fileList.value.filter(f => f.uid !== uploadFile.uid);
  267. const alreadyHasVideo = otherFiles.some(f => isVideoFile(f));
  268. const alreadyHasImage = otherFiles.some(f => !isVideoFile(f));
  269. if (alreadyHasVideo) {
  270. const index = uploadFiles.findIndex(f => f.uid === uploadFile.uid);
  271. if (index > -1) uploadFiles.splice(index, 1);
  272. ElMessage.warning("已上传视频,只能上传一个视频,不可再上传");
  273. return;
  274. }
  275. if (alreadyHasImage && isVideo) {
  276. const index = uploadFiles.findIndex(f => f.uid === uploadFile.uid);
  277. if (index > -1) uploadFiles.splice(index, 1);
  278. // ElMessage.warning("已上传图片,后续只能上传图片,不能上传视频");
  279. return;
  280. }
  281. // 根据文件类型设置不同的大小限制
  282. const maxSize = isVideo ? 100 : 20;
  283. const isLtMaxSize = uploadFile.raw.size / 1024 / 1024 < maxSize;
  284. if (!isLtMaxSize) {
  285. const index = uploadFiles.findIndex(f => f.uid === uploadFile.uid);
  286. if (index > -1) {
  287. uploadFiles.splice(index, 1);
  288. }
  289. ElMessage.warning(`${isVideo ? "视频" : "图片"}大小不能超过 ${maxSize}MB`);
  290. return;
  291. }
  292. }
  293. // 为视频文件创建临时预览URL
  294. if (uploadFile.raw && uploadFile.raw.type.startsWith("video/") && !uploadFile.url) {
  295. uploadFile.url = URL.createObjectURL(uploadFile.raw);
  296. console.log("为视频创建临时URL:", uploadFile.url);
  297. }
  298. // 添加到待上传队列
  299. const existingIndex = fileList.value.findIndex(f => f.uid === uploadFile.uid);
  300. if (existingIndex === -1 && uploadFile.status) {
  301. fileList.value.push(uploadFile as UploadUserFile);
  302. }
  303. const readyFiles = fileList.value.filter(file => file.status === "ready");
  304. if (readyFiles.length) {
  305. readyFiles.forEach(file => {
  306. if (!pendingUploadFiles.value.some(item => item.uid === file.uid)) {
  307. pendingUploadFiles.value.push(file as UploadFile);
  308. }
  309. });
  310. }
  311. processUploadQueue();
  312. };
  313. // 处理上传队列
  314. const processUploadQueue = async () => {
  315. if (uploading.value || pendingUploadFiles.value.length === 0) {
  316. return;
  317. }
  318. const file = pendingUploadFiles.value.shift();
  319. if (file) {
  320. await uploadSingleFile(file);
  321. processUploadQueue();
  322. }
  323. };
  324. // 单个文件上传
  325. const uploadSingleFile = async (file: UploadFile) => {
  326. if (!file.raw) {
  327. return;
  328. }
  329. const rawFile = file.raw as File;
  330. const uploadFormData = new FormData();
  331. uploadFormData.append("file", rawFile);
  332. file.status = "uploading";
  333. file.percentage = 0;
  334. uploading.value = true;
  335. try {
  336. console.log("开始上传文件:", rawFile.name, "类型:", rawFile.type);
  337. // 图片和视频都使用同一个上传接口
  338. const result: any = await uploadImg(uploadFormData);
  339. console.log("上传接口返回:", result);
  340. if (result?.code === 200 && result.data) {
  341. let fileUrl = "";
  342. // 处理不同的返回格式
  343. if (Array.isArray(result.data) && result.data.length > 0) {
  344. fileUrl = result.data[0]; // 数组格式
  345. } else if (typeof result.data === "string") {
  346. fileUrl = result.data; // 字符串格式
  347. } else if (result.data.url) {
  348. fileUrl = result.data.url; // 对象格式
  349. }
  350. if (fileUrl) {
  351. file.status = "success";
  352. file.percentage = 100;
  353. file.url = fileUrl;
  354. file.response = { url: fileUrl };
  355. console.log("上传成功,文件URL:", fileUrl);
  356. // 添加到 formData.images
  357. if (!formData.images.includes(fileUrl)) {
  358. formData.images.push(fileUrl);
  359. console.log("添加到 images 数组,当前长度:", formData.images.length);
  360. }
  361. } else {
  362. throw new Error("上传接口返回数据格式错误");
  363. }
  364. } else {
  365. throw new Error(result?.msg || "文件上传失败");
  366. }
  367. } catch (error: any) {
  368. console.error("上传失败:", error);
  369. file.status = "fail";
  370. if (file.url && file.url.startsWith("blob:")) {
  371. URL.revokeObjectURL(file.url);
  372. }
  373. const index = fileList.value.findIndex(f => f.uid === file.uid);
  374. if (index > -1) {
  375. fileList.value.splice(index, 1);
  376. }
  377. ElMessage.error(error?.message || "文件上传失败");
  378. } finally {
  379. uploading.value = false;
  380. fileList.value = [...fileList.value];
  381. }
  382. };
  383. // 图片上传前验证
  384. const beforeImageUpload = (file: File) => {
  385. const isImage = file.type.startsWith("image/");
  386. const isVideo = file.type.startsWith("video/");
  387. const isValidType = isImage || isVideo;
  388. if (!isValidType) {
  389. ElMessage.error("只能上传图片或视频文件!");
  390. return false;
  391. }
  392. // 权限:已上传视频则不能再传;已上传图片则只能继续传图片(排除当前文件,避免清除图片后选视频被误判)
  393. const permission = checkUploadPermission(file, file);
  394. if (!permission.allowed) {
  395. ElMessage.warning(permission.message);
  396. return false;
  397. }
  398. // 图片和视频使用不同的大小限制
  399. const maxSize = isVideo ? 100 : 20; // 视频100MB,图片20MB
  400. const isLtMaxSize = file.size / 1024 / 1024 < maxSize;
  401. if (!isLtMaxSize) {
  402. ElMessage.error(`${isVideo ? "视频" : "图片"}大小不能超过 ${maxSize}MB!`);
  403. return false;
  404. }
  405. return true;
  406. };
  407. // 自定义上传
  408. const handleImageUpload = async (options: any) => {
  409. // 上传逻辑已在 uploadSingleFile 中处理
  410. return;
  411. };
  412. // 判断文件是否为视频
  413. const isVideoFile = (file: any) => {
  414. const fileName = file.name || file.url || "";
  415. return fileName.toLowerCase().endsWith(".mp4") || file.raw?.type?.startsWith("video/") || false;
  416. };
  417. // 上传类型权限:第一个是图片则只能继续传图片;第一个是视频则只能传一个视频
  418. const hasVideoInList = () => fileList.value.some(f => isVideoFile(f));
  419. const hasImageInList = () => fileList.value.some(f => !isVideoFile(f));
  420. /** 校验上传权限,excludeRawFile 为当前正在校验的文件,校验时排除它(避免刚选中的文件已被加入列表导致误判) */
  421. const checkUploadPermission = (file: File, excludeRawFile?: File): { allowed: boolean; message?: string } => {
  422. const isVideo = file.type.startsWith("video/");
  423. const existingFiles = excludeRawFile ? fileList.value.filter(f => f.raw !== excludeRawFile) : fileList.value;
  424. const existingHasVideo = existingFiles.some(f => isVideoFile(f));
  425. const existingHasImage = existingFiles.some(f => !isVideoFile(f));
  426. if (existingHasVideo) {
  427. return { allowed: false, message: "已上传视频,只能上传一个视频,不可再上传" };
  428. }
  429. if (existingHasImage && isVideo) {
  430. return { allowed: false, message: "已上传图片,后续只能上传图片,不能上传视频" };
  431. }
  432. return { allowed: true };
  433. };
  434. // 图片/视频预览
  435. const handlePicturePreview = (uploadFile: UploadUserFile) => {
  436. previewImageUrl.value = uploadFile.url!;
  437. // 判断是否为视频文件
  438. previewIsVideo.value = isVideoFile(uploadFile);
  439. previewDialogVisible.value = true;
  440. };
  441. // 移除图片/视频(组件回调)
  442. const handleRemoveImage = (uploadFile: UploadUserFile, uploadFiles: UploadUserFile[]) => {
  443. console.log("组件回调删除文件:", uploadFile);
  444. // 从 formData.images 中移除对应的 URL
  445. const fileUrl = uploadFile.url || (uploadFile.response as any)?.url;
  446. if (fileUrl) {
  447. // 清理 blob URL
  448. if (fileUrl.startsWith("blob:")) {
  449. URL.revokeObjectURL(fileUrl);
  450. }
  451. const index = formData.images.findIndex(url => url === fileUrl);
  452. if (index > -1) {
  453. formData.images.splice(index, 1);
  454. }
  455. }
  456. };
  457. // 手动移除文件(点击自定义删除按钮)
  458. const handleRemoveFile = (file: UploadUserFile) => {
  459. console.log("删除文件:", file);
  460. // 从 formData.images 中移除对应的 URL
  461. const fileUrl = file.url || (file.response as any)?.url;
  462. console.log("文件URL:", fileUrl);
  463. if (fileUrl) {
  464. // 清理 blob URL
  465. if (fileUrl.startsWith("blob:")) {
  466. URL.revokeObjectURL(fileUrl);
  467. console.log("清理临时URL");
  468. }
  469. const index = formData.images.findIndex(url => url === fileUrl);
  470. console.log("在 images 数组中的索引:", index);
  471. if (index > -1) {
  472. formData.images.splice(index, 1);
  473. }
  474. }
  475. // 从 fileList 中移除
  476. const fileIndex = fileList.value.findIndex(f => f.uid === file.uid);
  477. console.log("在 fileList 中的索引:", fileIndex);
  478. if (fileIndex > -1) {
  479. fileList.value.splice(fileIndex, 1);
  480. }
  481. console.log("删除后 fileList 长度:", fileList.value.length);
  482. console.log("删除后 images 长度:", formData.images.length);
  483. };
  484. // 选择位置
  485. const handleChooseLocation = (location: LocationItem) => {
  486. formData.location = location.name;
  487. formData.locationId = location.id;
  488. // 保存经纬度和省市信息(如果有)
  489. if (location.longitude && location.latitude) {
  490. formData.address = `${location.longitude},${location.latitude}`;
  491. }
  492. if (location.province) {
  493. formData.addressProvince = location.province;
  494. }
  495. console.log("选择位置:", {
  496. name: formData.location,
  497. address: formData.address,
  498. province: formData.addressProvince
  499. });
  500. locationDialogVisible.value = false;
  501. };
  502. // 保存草稿
  503. const handleSaveDraft = async () => {
  504. if (!formData.title && !formData.content && fileList.value.length === 0) {
  505. ElMessage.warning("请至少填写标题或正文");
  506. return;
  507. }
  508. // 检查是否有图片/视频正在上传
  509. if (uploading.value || pendingUploadFiles.value.length > 0) {
  510. ElMessage.warning("图片/视频正在上传中,请稍候...");
  511. return;
  512. }
  513. try {
  514. // 获取当前用户信息
  515. const phone = userStore.userInfo?.phone || "";
  516. const currentUserPhoneId = phone.startsWith("store_") ? phone : `store_${phone}`;
  517. const createId = userStore.userInfo?.id || userStore.userInfo?.userId || 0;
  518. // 调用保存草稿接口
  519. const params: any = {
  520. title: formData.title || "未命名",
  521. context: formData.content || "", // 正文
  522. imagePath: formData.images.join(","), // 多个图片/视频用逗号分隔
  523. phoneId: currentUserPhoneId, // 店铺ID
  524. createId: createId, // 创建者ID
  525. type: "2", // 动态类型
  526. draft: 1, // ✅ 1表示草稿
  527. addressName: formData.queryAddress, // 地址名称
  528. address: formData.storePositionLongitude + "," + formData.storePositionLatitude, // 经纬度
  529. addressProvince: formData.addressProvince || "" // 省市
  530. };
  531. // 如果是编辑模式,传递ID
  532. if (isEditMode.value && currentDraftId.value) {
  533. params.id = currentDraftId.value;
  534. }
  535. await addOrUpdateDynamic(params);
  536. ElMessage.success("草稿保存成功");
  537. router.back();
  538. } catch (error) {
  539. console.error("保存草稿失败:", error);
  540. ElMessage.error("保存草稿失败");
  541. }
  542. };
  543. // 发布动态
  544. const handlePublish = async () => {
  545. if (!formRef.value) return;
  546. await formRef.value.validate(async (valid: boolean) => {
  547. if (valid) {
  548. // 检查是否有图片正在上传
  549. if (uploading.value || pendingUploadFiles.value.length > 0) {
  550. ElMessage.warning("图片/视频正在上传中,请稍候...");
  551. return;
  552. }
  553. // 检查是否有上传的文件
  554. if (formData.images.length === 0) {
  555. ElMessage.warning("请至少上传一张图片或一个视频");
  556. return;
  557. }
  558. publishing.value = true;
  559. try {
  560. // 获取当前用户信息
  561. const phone = userStore.userInfo?.phone || "";
  562. const currentUserPhoneId = phone.startsWith("store_") ? phone : `store_${phone}`;
  563. const createId = userStore.userInfo?.id || userStore.userInfo?.userId || 0;
  564. // 调用发布动态接口
  565. const params: any = {
  566. title: formData.title,
  567. context: formData.content, // 正文
  568. imagePath: formData.images.join(","), // 多个图片/视频用逗号分隔
  569. phoneId: currentUserPhoneId, // 店铺ID
  570. createId: createId, // 创建者ID
  571. type: "2", // 动态类型
  572. draft: 0, // 0表示发布
  573. addressName: formData.queryAddress, // 地址名称
  574. address: formData.storePositionLongitude + "," + formData.storePositionLatitude, // 经纬度
  575. addressProvince: formData.addressProvince || "" // 省市
  576. };
  577. // 如果是编辑模式,传递ID
  578. if (isEditMode.value && currentDraftId.value) {
  579. params.id = currentDraftId.value;
  580. }
  581. await addOrUpdateDynamic(params);
  582. ElMessage.success("发布成功");
  583. router.back();
  584. } catch (error) {
  585. console.error("发布失败:", error);
  586. ElMessage.error("发布失败");
  587. } finally {
  588. publishing.value = false;
  589. }
  590. }
  591. });
  592. };
  593. // 加载草稿数据
  594. const loadDraftData = async (draftId: string | number) => {
  595. try {
  596. console.log("加载草稿数据,ID:", draftId);
  597. // 获取当前用户的手机号
  598. const phone = userStore.userInfo?.phone || "";
  599. const phoneId = phone.startsWith("store_") ? phone : `store_${phone}`;
  600. // 调用接口获取草稿列表
  601. const res: any = await getUserDraftDynamics({
  602. phoneId: phoneId,
  603. page: 1,
  604. size: 1000
  605. });
  606. if (res.code === 200) {
  607. const dataList = res.data?.records || res.data?.list || [];
  608. // 查找指定的草稿
  609. const draft = dataList.find((item: any) => item.id == draftId);
  610. if (draft) {
  611. console.log("找到草稿数据:", draft);
  612. // 回显标题
  613. formData.title = draft.title || "";
  614. // 回显正文
  615. formData.content = draft.context || "";
  616. // 回显位置
  617. formData.location = draft.addressName || "";
  618. formData.address = draft.address || "";
  619. formData.addressProvince = draft.addressProvince || "";
  620. formData.storePosition = draft.address || ""; // 回显到下拉框(经纬度格式)
  621. formData.queryAddress = draft.addressName || ""; // 回显地址名称
  622. formData.storePositionLongitude = draft.address ? draft.address.split(",")[0]?.trim() : "";
  623. formData.storePositionLatitude = draft.address ? draft.address.split(",")[1]?.trim() : "";
  624. // 如果有地址,需要将其添加到 addressList 中以便下拉框显示
  625. if (draft.address && draft.addressName) {
  626. addressList.value = [
  627. {
  628. id: draft.id || Date.now(),
  629. name: draft.addressName,
  630. location: draft.address,
  631. district: draft.addressProvince || ""
  632. }
  633. ];
  634. }
  635. // 回显图片/视频
  636. if (draft.imagePath) {
  637. const imageUrls = draft.imagePath.split(",").filter((url: string) => url.trim());
  638. formData.images = imageUrls;
  639. // 构建文件列表用于显示
  640. fileList.value = imageUrls.map((url: string, index: number) => {
  641. const isVideo = url.toLowerCase().endsWith(".mp4");
  642. return {
  643. uid: Date.now() + index,
  644. name: isVideo ? `video_${index + 1}.mp4` : `image_${index + 1}.jpg`,
  645. status: "success" as const,
  646. url: url,
  647. response: { url: url }
  648. };
  649. });
  650. console.log("回显图片/视频列表:", fileList.value);
  651. }
  652. ElMessage.success("草稿数据加载成功");
  653. } else {
  654. ElMessage.warning("未找到指定的草稿");
  655. }
  656. }
  657. } catch (error) {
  658. console.error("加载草稿数据失败:", error);
  659. ElMessage.error("加载草稿数据失败");
  660. }
  661. };
  662. // 初始化
  663. onMounted(() => {
  664. // 检查是否为编辑草稿模式
  665. const draftId = route.query.draftId;
  666. const mode = route.query.mode;
  667. if (draftId && mode === "edit") {
  668. console.log("进入编辑草稿模式, draftId:", draftId);
  669. isEditMode.value = true;
  670. currentDraftId.value = draftId as string | number;
  671. loadDraftData(draftId as string | number);
  672. }
  673. });
  674. </script>
  675. <style scoped lang="scss">
  676. .header-section {
  677. display: flex;
  678. align-items: center;
  679. justify-content: space-between;
  680. padding: 16px 0;
  681. border-bottom: 1px solid #e4e7ed;
  682. .back-btn {
  683. display: flex;
  684. gap: 4px;
  685. align-items: center;
  686. padding: 8px 16px;
  687. margin-left: 20px;
  688. font-size: 15px;
  689. color: #606266;
  690. transition: all 0.3s;
  691. &:hover {
  692. color: #409eff;
  693. background: #ecf5ff;
  694. }
  695. }
  696. .header-title {
  697. flex: 1;
  698. font-size: 20px;
  699. font-weight: 600;
  700. color: #303133;
  701. text-align: center;
  702. }
  703. .header-right {
  704. width: 100px;
  705. }
  706. }
  707. .publish-dynamic-container {
  708. display: flex;
  709. flex-direction: column;
  710. background: #ffffff;
  711. // 头部导航
  712. .header-bar {
  713. position: sticky;
  714. top: 0;
  715. z-index: 100;
  716. display: flex;
  717. align-items: center;
  718. justify-content: space-between;
  719. height: 60px;
  720. .header-left {
  721. flex: 1;
  722. .return-btn {
  723. background: #ffffff;
  724. border: 1px solid #dcdfe6;
  725. }
  726. :deep(.el-button) {
  727. padding: 8px 0;
  728. font-size: 16px;
  729. color: #606266;
  730. &:hover {
  731. color: #409eff;
  732. }
  733. .el-icon {
  734. margin-right: 4px;
  735. }
  736. }
  737. }
  738. .header-title {
  739. flex: 1;
  740. font-size: 18px;
  741. font-weight: 600;
  742. color: #303133;
  743. text-align: center;
  744. }
  745. .header-right {
  746. flex: 1;
  747. }
  748. }
  749. // 表单容器
  750. .form-container {
  751. flex: 1;
  752. width: 100%;
  753. max-width: 800px;
  754. padding: 30px 20px;
  755. margin: 0 auto;
  756. :deep(.el-form) {
  757. .el-form-item {
  758. margin-bottom: 15px;
  759. .el-form-item__label {
  760. padding-bottom: 5px;
  761. font-size: 16px;
  762. font-weight: 600;
  763. color: #303133;
  764. }
  765. }
  766. }
  767. // 图片上传区域
  768. .upload-section {
  769. :deep(.el-upload-list) {
  770. display: flex;
  771. flex-wrap: wrap;
  772. gap: 8px;
  773. }
  774. :deep(.el-upload--picture-card) {
  775. width: 138px;
  776. height: 138px;
  777. border: 1px dashed #dcdfe6;
  778. border-radius: 8px;
  779. transition: all 0.3s;
  780. &:hover {
  781. border-color: #409eff;
  782. }
  783. .upload-trigger {
  784. display: flex;
  785. flex-direction: column;
  786. align-items: center;
  787. justify-content: center;
  788. height: 100%;
  789. .upload-count {
  790. margin-top: 8px;
  791. font-size: 14px;
  792. color: #909399;
  793. }
  794. }
  795. }
  796. :deep(.el-upload-list--picture-card .el-upload-list__item) {
  797. width: 138px;
  798. height: 138px;
  799. margin: 0;
  800. border-radius: 8px;
  801. }
  802. // 自定义文件预览
  803. .upload-file-preview {
  804. position: relative;
  805. width: 100%;
  806. height: 100%;
  807. overflow: hidden;
  808. border-radius: 8px;
  809. .upload-video-thumb,
  810. .upload-image-thumb {
  811. width: 100%;
  812. height: 100%;
  813. object-fit: cover;
  814. }
  815. .upload-actions {
  816. position: absolute;
  817. top: 0;
  818. left: 0;
  819. z-index: 10;
  820. display: flex;
  821. gap: 8px;
  822. align-items: center;
  823. justify-content: center;
  824. width: 100%;
  825. height: 100%;
  826. background: rgb(0 0 0 / 50%);
  827. opacity: 0;
  828. transition: opacity 0.3s;
  829. .upload-action-item {
  830. display: flex;
  831. align-items: center;
  832. justify-content: center;
  833. width: 36px;
  834. height: 36px;
  835. color: #ffffff;
  836. cursor: pointer;
  837. background: rgb(0 0 0 / 50%);
  838. border-radius: 50%;
  839. transition: transform 0.2s;
  840. &:hover {
  841. transform: scale(1.1);
  842. }
  843. }
  844. }
  845. &:hover .upload-actions {
  846. opacity: 1;
  847. }
  848. }
  849. }
  850. // 输入框样式
  851. :deep(.el-input__inner),
  852. :deep(.el-textarea__inner) {
  853. border-radius: 8px;
  854. }
  855. :deep(.el-textarea__inner) {
  856. padding: 12px 15px;
  857. font-size: 15px;
  858. line-height: 1.6;
  859. }
  860. }
  861. // 底部操作按钮
  862. .footer-actions {
  863. display: flex;
  864. justify-content: center;
  865. .el-button {
  866. min-width: 150px;
  867. height: 45px;
  868. margin-top: 20px;
  869. font-size: 16px;
  870. border-radius: 8px;
  871. }
  872. .draft-btn {
  873. color: #606266;
  874. background: #f5f7fa;
  875. border-color: #dcdfe6;
  876. &:hover {
  877. color: #409eff;
  878. background: #ecf5ff;
  879. border-color: #409eff;
  880. }
  881. }
  882. .publish-btn {
  883. background: #409eff;
  884. border-color: #409eff;
  885. }
  886. }
  887. // 位置选择对话框
  888. :deep(.el-dialog) {
  889. border-radius: 12px;
  890. .location-list {
  891. max-height: 400px;
  892. margin-top: 16px;
  893. overflow-y: auto;
  894. .location-item {
  895. display: flex;
  896. gap: 12px;
  897. align-items: center;
  898. padding: 12px 16px;
  899. cursor: pointer;
  900. border-radius: 8px;
  901. transition: all 0.3s;
  902. &:hover {
  903. background: #f5f7fa;
  904. }
  905. .el-icon {
  906. font-size: 18px;
  907. color: #909399;
  908. }
  909. span {
  910. font-size: 15px;
  911. color: #303133;
  912. }
  913. }
  914. }
  915. }
  916. }
  917. // 响应式适配
  918. @media (width <= 768px) {
  919. .publish-dynamic-container {
  920. .form-container {
  921. padding: 20px 16px 100px;
  922. }
  923. .footer-actions {
  924. padding: 12px 16px;
  925. .el-button {
  926. min-width: 140px;
  927. height: 44px;
  928. font-size: 15px;
  929. }
  930. }
  931. }
  932. }
  933. </style>