publishDynamic.vue 31 KB

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