index.vue 69 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314
  1. <template>
  2. <div class="dynamic-management-container">
  3. <!-- 头部:Tabs和发布按钮 -->
  4. <div class="header-section">
  5. <el-tabs v-model="activeTab" @tab-click="handleTabClick">
  6. <el-tab-pane label="推荐" name="recommend" />
  7. <el-tab-pane label="关注" name="follow" />
  8. </el-tabs>
  9. <div class="action-buttons">
  10. <el-button type="primary" @click="handlePublish"> 发布动态 </el-button>
  11. </div>
  12. </div>
  13. <!-- 动态列表(推荐和关注共用) -->
  14. <div v-if="dynamicList.length > 0" class="content-section">
  15. <div class="dynamic-grid">
  16. <div v-for="item in paginatedList" :key="item.id" class="dynamic-card" @click="handleCardClick(item)">
  17. <!-- 图片/视频区域 -->
  18. <div class="dynamic-image-wrapper">
  19. <!-- 视频 -->
  20. <video v-if="item.isVideo && item.imageUrl" :src="item.imageUrl" class="dynamic-image" controls preload="metadata" />
  21. <!-- 图片 -->
  22. <img v-else-if="item.imageUrl" :src="item.imageUrl" :alt="item.title" class="dynamic-image" />
  23. <!-- 占位符 -->
  24. <div v-else class="image-placeholder">
  25. <el-icon :size="48" color="#999">
  26. <Picture />
  27. </el-icon>
  28. </div>
  29. </div>
  30. <!-- 动态内容 -->
  31. <div class="dynamic-content">
  32. <div class="dynamic-text">
  33. {{ item.title }}
  34. </div>
  35. <!-- 用户信息和点赞 -->
  36. <div class="dynamic-footer">
  37. <div class="user-info">
  38. <div class="user-avatar">
  39. <img v-if="item.userAvatar" :src="item.userAvatar" :alt="item.userName" />
  40. <el-icon v-else :size="24">
  41. <Avatar />
  42. </el-icon>
  43. </div>
  44. <span class="user-name">{{ item.userName }}</span>
  45. </div>
  46. <div class="like-section" @click.stop="handleLike(item)">
  47. <el-icon :size="18" :color="item.isLike == '1' ? '#f56c6c' : '#999'" class="like-icon">
  48. <Star />
  49. </el-icon>
  50. <span class="like-count">{{ item.dianzanCount }}</span>
  51. </div>
  52. </div>
  53. </div>
  54. </div>
  55. </div>
  56. </div>
  57. <!-- 空状态 -->
  58. <div v-else class="empty-section">
  59. <el-empty :description="activeTab === 'follow' ? '暂无关注的动态' : '暂无动态数据'" />
  60. </div>
  61. <!-- 分页 -->
  62. <div v-if="dynamicList.length > 0" class="pagination-section">
  63. <el-pagination
  64. v-model:current-page="pagination.page"
  65. v-model:page-size="pagination.pageSize"
  66. :page-sizes="[10, 20, 30, 50]"
  67. :total="pagination.total"
  68. layout="total, sizes, prev, pager, next, jumper"
  69. @size-change="handleSizeChange"
  70. @current-change="handleCurrentChange"
  71. />
  72. </div>
  73. <!-- 动态详情 Drawer -->
  74. <el-drawer
  75. v-model="detailDrawerVisible"
  76. direction="rtl"
  77. size="90%"
  78. :show-close="false"
  79. destroy-on-close
  80. class="detail-drawer"
  81. >
  82. <template #header>
  83. <div class="drawer-header">
  84. <el-button class="close-btn" text @click="handleCloseDetail">
  85. <el-icon :size="24">
  86. <Close />
  87. </el-icon>
  88. </el-button>
  89. </div>
  90. </template>
  91. <div v-if="currentDetail" class="detail-content">
  92. <!-- 主内容区域 -->
  93. <div class="detail-main">
  94. <!-- 图片/视频轮播展示 -->
  95. <div class="media-container">
  96. <!-- 多媒体轮播 -->
  97. <el-carousel
  98. v-if="currentDetail.mediaList && currentDetail.mediaList.length > 0"
  99. :autoplay="false"
  100. :loop="false"
  101. indicator-position="outside"
  102. arrow="always"
  103. height="100%"
  104. class="media-carousel"
  105. @change="handleCarouselChange"
  106. >
  107. <el-carousel-item v-for="(media, index) in currentDetail.mediaList" :key="index">
  108. <!-- 视频 -->
  109. <video
  110. v-if="media.type === 'video'"
  111. :ref="el => setVideoRef(el, index)"
  112. :src="media.url"
  113. class="detail-media detail-video"
  114. controls
  115. preload="metadata"
  116. @play="handleVideoPlay(index)"
  117. />
  118. <!-- 图片 -->
  119. <img v-else :src="media.url" :alt="currentDetail.title" class="detail-media detail-image" />
  120. </el-carousel-item>
  121. </el-carousel>
  122. <!-- 占位符 -->
  123. <div v-else class="media-placeholder">
  124. <el-icon :size="80" color="#dcdfe6">
  125. <Picture />
  126. </el-icon>
  127. </div>
  128. <!-- 媒体计数指示器 -->
  129. <div v-if="currentDetail.mediaList && currentDetail.mediaList.length > 1" class="media-counter">
  130. {{ currentCarouselIndex + 1 }} / {{ currentDetail.mediaList.length }}
  131. </div>
  132. </div>
  133. <!-- 底部信息 -->
  134. <div class="detail-info">
  135. <div class="author-info">
  136. <div class="author-avatar">
  137. <img
  138. v-if="currentDetail.author?.avatar || currentDetail.userAvatar"
  139. :src="currentDetail.author?.avatar || currentDetail.userAvatar"
  140. :alt="currentDetail.author?.name || currentDetail.userName"
  141. />
  142. <el-icon v-else :size="32">
  143. <Avatar />
  144. </el-icon>
  145. </div>
  146. <div class="author-details">
  147. <div class="author-name">@{{ currentDetail.author?.name || currentDetail.userName }}</div>
  148. <div class="publish-time">
  149. {{ currentDetail.createdTime }}
  150. </div>
  151. </div>
  152. </div>
  153. <div class="detail-description">
  154. <p>{{ currentDetail.context }}</p>
  155. </div>
  156. </div>
  157. </div>
  158. <!-- 右侧操作栏 -->
  159. <div class="action-bar">
  160. <!-- 作者头像 -->
  161. <div class="action-item author-action">
  162. <div class="action-avatar" @click="handleViewUserProfile">
  163. <img
  164. v-if="currentDetail.author?.avatar || currentDetail.userAvatar"
  165. :src="currentDetail.author?.avatar || currentDetail.userAvatar"
  166. :alt="currentDetail.author?.name || currentDetail.userName"
  167. />
  168. <el-icon v-else :size="40" color="#fff">
  169. <Avatar />
  170. </el-icon>
  171. <!-- 关注按钮 (定位在头像右下角) -->
  172. <div v-if="currentDetail.isFollowThis == 0 && !isMyDynamic" class="follow-badge" @click.stop="handleFollowInDetail">
  173. <el-icon :size="16" color="#fff">
  174. <Plus />
  175. </el-icon>
  176. </div>
  177. </div>
  178. </div>
  179. <!-- 点赞 -->
  180. <div class="action-item" @click="handleDetailLike">
  181. <div class="action-icon">
  182. <el-icon :size="28" :color="currentDetail.isLike == '1' ? '#f56c6c' : '#fff'">
  183. <StarFilled v-if="currentDetail.isLike" />
  184. <Star v-else />
  185. </el-icon>
  186. </div>
  187. <div class="action-count">
  188. {{ currentDetail.dianzanCount }}
  189. </div>
  190. </div>
  191. <!-- 评论 -->
  192. <div class="action-item" @click="handleShowComments">
  193. <div class="action-icon">
  194. <el-icon :size="28" color="#fff">
  195. <ChatDotRound />
  196. </el-icon>
  197. </div>
  198. <div class="action-count">
  199. {{ currentDetail.commentCount }}
  200. </div>
  201. </div>
  202. <!-- 分享 -->
  203. <div class="action-item" @click="handleShare">
  204. <div class="action-icon">
  205. <el-icon :size="28" color="#fff">
  206. <Share />
  207. </el-icon>
  208. </div>
  209. <div class="action-count">分享</div>
  210. </div>
  211. <!-- 更多 -->
  212. <el-popover placement="left" :width="120" trigger="click" popper-class="more-actions-popover">
  213. <template #reference>
  214. <div class="action-item">
  215. <div class="action-icon">
  216. <el-icon :size="28" color="#fff">
  217. <MoreFilled />
  218. </el-icon>
  219. </div>
  220. </div>
  221. </template>
  222. <div class="more-actions-menu">
  223. <!-- 如果是当前用户的动态,显示编辑和删除 -->
  224. <template v-if="isMyDynamic">
  225. <div class="menu-item" style="display: flex; align-items: center; cursor: pointer" @click="handleDeleteDynamic">
  226. <el-icon :size="18">
  227. <Delete /> </el-icon
  228. >&nbsp;&nbsp;
  229. <span>删除</span>
  230. </div>
  231. </template>
  232. <!-- 如果不是当前用户的动态,显示举报和拉黑 -->
  233. <template v-else>
  234. <div
  235. class="menu-item"
  236. style="display: flex; align-items: center; padding-bottom: 10px; cursor: pointer"
  237. @click="handleReportDynamic"
  238. >
  239. <el-icon :size="18">
  240. <Warning /> </el-icon
  241. >&nbsp;&nbsp;
  242. <span>举报</span>
  243. </div>
  244. <div class="menu-item" style="display: flex; align-items: center; cursor: pointer" @click="handleBlockUserClick">
  245. <el-icon :size="18">
  246. <CircleClose /> </el-icon
  247. >&nbsp;&nbsp;
  248. <span>拉黑</span>
  249. </div>
  250. </template>
  251. </div>
  252. </el-popover>
  253. </div>
  254. </div>
  255. </el-drawer>
  256. <!-- 评论侧边栏 -->
  257. <el-drawer v-model="commentDrawerVisible" title="评论" direction="rtl" size="400px" destroy-on-close>
  258. <!-- 评论列表 -->
  259. <div class="comment-list-container">
  260. <div v-if="commentListData.length > 0" class="comment-list">
  261. <div v-for="comment in commentListData" :key="comment.id" class="comment-item">
  262. <div class="comment-avatar">
  263. <img v-if="comment.userAvatar" :src="comment.userAvatar" :alt="comment.userName" />
  264. <el-icon v-else :size="32">
  265. <Avatar />
  266. </el-icon>
  267. </div>
  268. <div class="comment-content-wrapper">
  269. <div class="comment-header">
  270. <span class="comment-user-name">{{ comment.userName }}</span>
  271. </div>
  272. <div class="comment-text">
  273. {{ comment.commentContent }}
  274. </div>
  275. <div class="comment-actions">
  276. <span class="comment-action-item" @click="handleLikeComment(comment)">
  277. <el-icon :size="16" :color="comment.isLiked ? '#f56c6c' : '#999'">
  278. <Star />
  279. </el-icon>
  280. <span>{{ comment.likeCount || 0 }}</span>
  281. </span>
  282. <span class="comment-action-item" @click="handleReplyComment(comment)">
  283. <el-icon :size="16">
  284. <ChatDotRound />
  285. </el-icon>
  286. <span>回复</span>
  287. </span>
  288. </div>
  289. <!-- 商家回复 -->
  290. <div v-for="item in comment.storeComment" :key="item.id" class="store-comment-wrapper">
  291. <div class="store-comment-item">
  292. <div class="store-comment-avatar">
  293. <img v-if="item.userImage" :src="item.userImage" :alt="item.userName" />
  294. <el-icon v-else :size="24">
  295. <Avatar />
  296. </el-icon>
  297. </div>
  298. <div class="store-comment-content">
  299. <div class="store-comment-header">
  300. <span class="store-comment-user-name">{{ item.userName || "商家" }}</span>
  301. <span class="store-comment-time">{{ item.createdTime || item.createDate }}</span>
  302. </div>
  303. <div class="store-comment-text">
  304. {{ item.commentContent }}
  305. <span
  306. class="comment-action-item"
  307. @click="handleLikeComment(item)"
  308. style="display: flex; align-items: center"
  309. >
  310. <el-icon :size="16" :color="item.isLiked ? '#f56c6c' : '#999'"> <Star /> </el-icon>&nbsp;
  311. <span>{{ item.likeCount || 0 }}</span>
  312. </span>
  313. </div>
  314. </div>
  315. </div>
  316. </div>
  317. </div>
  318. </div>
  319. </div>
  320. <el-empty v-else description="暂无评论" />
  321. </div>
  322. <!-- 评论输入框 -->
  323. <div class="comment-input-wrapper">
  324. <!-- 回复提示 -->
  325. <div v-if="replyingComment" class="reply-hint">
  326. <span class="reply-text">回复 @{{ replyingComment.userName }}</span>
  327. <el-icon class="cancel-reply" @click="handleCancelReply">
  328. <Close />
  329. </el-icon>
  330. </div>
  331. <el-input
  332. v-model="commentInput"
  333. type="textarea"
  334. :rows="3"
  335. :placeholder="replyingComment ? '输入回复内容...' : '你要评论点什么呢~'"
  336. maxlength="500"
  337. show-word-limit
  338. />
  339. <el-button type="primary" :loading="commentSubmitting" @click="handleSubmitComment">
  340. {{ replyingComment ? "回复" : "发送" }}
  341. </el-button>
  342. </div>
  343. </el-drawer>
  344. <!-- 举报对话框 -->
  345. <el-dialog v-model="reportDialogVisible" title="举报理由" width="500px" destroy-on-close @close="handleCloseReportDialog">
  346. <div class="report-dialog-content">
  347. <div class="report-tip">请选择最符合的原因,以便于我们进行的处理</div>
  348. <!-- 举报原因选项 -->
  349. <div class="report-reasons">
  350. <el-radio-group v-model="reportForm.reason">
  351. <el-radio label="用户头像"> 用户头像 </el-radio>
  352. <el-radio label="名称/昵称"> 名称/昵称 </el-radio>
  353. <el-radio label="违法违规"> 违法违规 </el-radio>
  354. <el-radio label="低俗色情、暴力恐怖、政治谣言"> 低俗色情、暴力恐怖、政治谣言 </el-radio>
  355. <el-radio label="涉嫌诈骗"> 涉嫌诈骗 </el-radio>
  356. <el-radio label="人身攻击"> 人身攻击 </el-radio>
  357. <el-radio label="侵犯版权"> 侵犯版权 </el-radio>
  358. <el-radio label="恶意骚扰"> 恶意骚扰 </el-radio>
  359. <el-radio label="虚假/过度宣传"> 虚假/过度宣传 </el-radio>
  360. <el-radio label="诱导点赞分享"> 诱导点赞分享 </el-radio>
  361. <el-radio label="传播人身安全"> 传播人身安全 </el-radio>
  362. <el-radio label="侵权举报"> 侵权举报 </el-radio>
  363. <el-radio label="其他举报"> 其他举报 </el-radio>
  364. </el-radio-group>
  365. </div>
  366. <!-- 详细描述(仅"其他举报"时显示) -->
  367. <div v-if="reportForm.reason === '其他举报'" class="report-description">
  368. <el-input
  369. v-model="reportForm.description"
  370. type="textarea"
  371. :rows="4"
  372. placeholder="请描述任何涉及举报内容的其体情况,我们会综合一判断合举政采!(必填)"
  373. maxlength="300"
  374. show-word-limit
  375. />
  376. </div>
  377. <!-- 上传凭证 -->
  378. <div class="report-upload">
  379. <div class="upload-title">上传凭证</div>
  380. <el-upload
  381. v-model:file-list="reportForm.fileList"
  382. list-type="picture-card"
  383. :limit="9"
  384. :on-preview="handleReportPreview"
  385. :on-remove="handleReportRemove"
  386. :before-upload="beforeReportUpload"
  387. :http-request="handleReportUpload"
  388. accept="image/*"
  389. multiple
  390. >
  391. <el-icon :size="24">
  392. <Plus />
  393. </el-icon>
  394. </el-upload>
  395. </div>
  396. <!-- 同意协议 -->
  397. <div class="report-agreement">
  398. <el-checkbox v-model="reportForm.agreed"> 同时拉黑该用户 </el-checkbox>
  399. </div>
  400. </div>
  401. <template #footer>
  402. <div class="dialog-footer">
  403. <el-button @click="reportDialogVisible = false"> 取消 </el-button>
  404. <el-button type="primary" :loading="reportSubmitting" @click="handleSubmitReport"> 提交 </el-button>
  405. </div>
  406. </template>
  407. </el-dialog>
  408. <!-- 分享对话框 -->
  409. <el-dialog v-model="shareDialogVisible" title="分享给好友" width="500px" destroy-on-close @close="handleCloseShareDialog">
  410. <div class="share-dialog-content">
  411. <!-- 好友列表 -->
  412. <div class="share-friend-list">
  413. <div v-if="filteredShareFriendList.length > 0">
  414. <div
  415. v-for="friend in filteredShareFriendList"
  416. :key="friend.id"
  417. class="share-friend-item"
  418. @click="handleSelectFriend(friend)"
  419. >
  420. <div class="friend-info">
  421. <div class="friend-avatar">
  422. <img v-if="friend.avatar" :src="friend.avatar" :alt="friend.name" />
  423. <el-icon v-else :size="40">
  424. <Avatar />
  425. </el-icon>
  426. </div>
  427. <div class="friend-name">
  428. {{ friend.name }}
  429. </div>
  430. </div>
  431. <el-icon v-if="selectedFriends.includes(friend.id)" :size="20" color="#409eff">
  432. <CircleCheck />
  433. </el-icon>
  434. </div>
  435. </div>
  436. <el-empty v-else description="暂无好友" />
  437. </div>
  438. <!-- 已选择的好友 -->
  439. <div v-if="selectedFriends.length > 0" class="selected-friends">
  440. <div class="selected-title">已选择 {{ selectedFriends.length }} 位好友</div>
  441. <div class="selected-list">
  442. <el-tag v-for="friendId in selectedFriends" :key="friendId" closable @close="handleRemoveFriend(friendId)">
  443. {{ shareFriendList.find(f => f.id === friendId)?.name }}
  444. </el-tag>
  445. </div>
  446. </div>
  447. </div>
  448. <template #footer>
  449. <div class="dialog-footer">
  450. <el-button @click="shareDialogVisible = false"> 取消 </el-button>
  451. <el-button
  452. type="primary"
  453. :loading="shareSubmitting"
  454. :disabled="selectedFriends.length === 0"
  455. @click="handleConfirmShare"
  456. >
  457. 确认分享
  458. </el-button>
  459. </div>
  460. </template>
  461. </el-dialog>
  462. </div>
  463. </template>
  464. <script setup lang="ts" name="dynamicManagementIndex">
  465. import { ref, reactive, computed, onMounted, watch } from "vue";
  466. import { useRouter } from "vue-router";
  467. import { ElMessage, ElMessageBox } from "element-plus";
  468. import {
  469. Picture,
  470. Avatar,
  471. Star,
  472. StarFilled,
  473. Close,
  474. ChatDotRound,
  475. Share,
  476. MoreFilled,
  477. Edit,
  478. Delete,
  479. Warning,
  480. CircleClose,
  481. Plus,
  482. Search,
  483. CircleCheck
  484. } from "@element-plus/icons-vue";
  485. import {
  486. getUserDynamics,
  487. likeDynamic,
  488. unlikeDynamic,
  489. reportUserViolation,
  490. blockUser,
  491. getUserByPhone,
  492. toggleFollowUser,
  493. cancelFollowed,
  494. likeDynamicNew,
  495. unlikeDynamicNew,
  496. likeDynamicList,
  497. likeComment,
  498. unlikeComment
  499. } from "@/api/modules/dynamicManagement";
  500. import { saveComment, commentList, getMutualAttention, addTransferCount } from "@/api/modules/newLoginApi";
  501. import { uploadImg } from "@/api/modules/upload";
  502. import { useUserStore } from "@/stores/modules/user";
  503. const router = useRouter();
  504. const userStore = useUserStore();
  505. // 举报原因到违规类型的映射
  506. const violationTypeMap: Record<string, number> = {
  507. 用户头像: 1,
  508. "名称/昵称": 2,
  509. 违法违规: 3,
  510. "低俗色情、暴力恐怖、政治谣言": 4,
  511. 涉嫌诈骗: 5,
  512. 人身攻击: 6,
  513. 侵犯版权: 7,
  514. 恶意骚扰: 8,
  515. "虚假/过度宣传": 9,
  516. 诱导点赞分享: 10,
  517. 传播人身安全: 11,
  518. 侵权举报: 12,
  519. 其他举报: 13
  520. };
  521. // 接口定义
  522. // 媒体项类型
  523. interface MediaItem {
  524. url: string;
  525. type: "image" | "video";
  526. }
  527. interface DynamicItem {
  528. isLike: any;
  529. dianzanCount: any;
  530. createdTime: any;
  531. context: string;
  532. id: number;
  533. title: string;
  534. content: string;
  535. imageUrl: string;
  536. userName: string;
  537. userAvatar: string;
  538. likeCount: number;
  539. isLiked: boolean;
  540. createTime: string;
  541. userId?: string | number; // 发布者ID
  542. phoneId?: string; // 发布者店铺ID
  543. storeUserId?: string | number; // 小店用户ID(用于举报)
  544. userType?: number; // 发布者用户类型:1商家,2用户
  545. phone?: string; // 发布者手机号
  546. isFollowed?: number; // 是否已关注:0未关注,1已关注
  547. isFollowThis?: number; // 是否已关注:0未关注,1已关注(用于判断关注按钮显示)
  548. isVideo?: boolean; // 是否为视频
  549. mediaType?: string; // 媒体类型:image 或 video
  550. mediaList?: MediaItem[]; // 媒体列表(支持多张图片和视频)
  551. }
  552. interface DetailItem extends DynamicItem {
  553. author?: {
  554. id: number;
  555. name: string;
  556. avatar: string;
  557. };
  558. context: string;
  559. commentCount: number;
  560. isFollowThis?: number; // 是否已关注:0未关注,1已关注(用于判断关注按钮显示)
  561. }
  562. // 响应式数据
  563. const activeTab = ref("recommend");
  564. const dynamicList = ref<DynamicItem[]>([]);
  565. const isfollowed = ref(0); // 0: 推荐, 1: 关注
  566. // 详情 Drawer 相关
  567. const detailDrawerVisible = ref(false);
  568. const currentDetail = ref<DetailItem | null>(null);
  569. // 轮播相关
  570. const currentCarouselIndex = ref(0);
  571. const videoRefs = ref<Map<number, HTMLVideoElement>>(new Map());
  572. // 设置视频引用
  573. const setVideoRef = (el: any, index: number) => {
  574. if (el) {
  575. videoRefs.value.set(index, el as HTMLVideoElement);
  576. }
  577. };
  578. // 轮播切换时暂停所有视频
  579. const handleCarouselChange = (newIndex: number) => {
  580. // 暂停所有视频
  581. videoRefs.value.forEach(video => {
  582. if (video && !video.paused) {
  583. video.pause();
  584. }
  585. });
  586. currentCarouselIndex.value = newIndex;
  587. };
  588. // 视频播放时暂停其他视频
  589. const handleVideoPlay = (currentIndex: number) => {
  590. videoRefs.value.forEach((video, index) => {
  591. if (index !== currentIndex && video && !video.paused) {
  592. video.pause();
  593. }
  594. });
  595. };
  596. // 举报对话框相关
  597. const reportDialogVisible = ref(false);
  598. const reportSubmitting = ref(false);
  599. const reportForm = reactive({
  600. reason: "用户头像", // 默认选择第一个选项
  601. description: "",
  602. fileList: [] as any[],
  603. agreed: false
  604. });
  605. // 分享对话框相关
  606. interface ShareFriend {
  607. id: number;
  608. name: string;
  609. avatar: string;
  610. phoneId?: string;
  611. }
  612. const shareDialogVisible = ref(false);
  613. const shareSubmitting = ref(false);
  614. const shareSearch = ref("");
  615. const shareFriendList = ref<ShareFriend[]>([]);
  616. const selectedFriends = ref<number[]>([]);
  617. // 过滤后的好友列表
  618. const filteredShareFriendList = computed(() => {
  619. if (!shareSearch.value) {
  620. return shareFriendList.value;
  621. }
  622. const keyword = shareSearch.value.toLowerCase();
  623. return shareFriendList.value.filter(friend => friend.name.toLowerCase().includes(keyword));
  624. });
  625. // 分页
  626. const pagination = reactive({
  627. page: 1,
  628. pageSize: 10,
  629. total: 0
  630. });
  631. // 直接使用动态列表(后端已完成分页)
  632. const paginatedList = computed(() => {
  633. return dynamicList.value;
  634. });
  635. // 判断当前详情是否是当前用户的动态
  636. const isMyDynamic = computed(() => {
  637. const currentUserStoreId = userStore.userInfo?.storeId;
  638. const dynamicStoreUserId = currentDetail.value?.storeUserId; // ✅ 添加可选链操作符
  639. // 通过 storeId 和 storeUserId 判断是否是当前用户的动态
  640. const result = currentUserStoreId == dynamicStoreUserId;
  641. console.log("是否是自己发布的作品:", result);
  642. return result;
  643. });
  644. // 标签切换
  645. const handleTabClick = (tab: any) => {
  646. // 根据切换的 tab 更新 isfollowed 的值
  647. // 使用传入的 tab.props.name 获取当前点击的 tab,而不是 activeTab.value
  648. const tabName = tab?.props?.name || tab?.paneName || activeTab.value;
  649. if (tabName === "recommend") {
  650. isfollowed.value = 0; // 推荐
  651. } else if (tabName === "follow") {
  652. isfollowed.value = 1; // 关注
  653. }
  654. pagination.page = 1;
  655. loadDynamicList();
  656. };
  657. // 发布动态
  658. const handlePublish = () => {
  659. // 校验是否已入驻店铺
  660. if (!userStore.userInfo?.storeId) {
  661. ElMessage.warning("请先入驻店铺");
  662. return;
  663. }
  664. router.push("/dynamicManagement/publishDynamic");
  665. };
  666. // 分页大小改变
  667. const handleSizeChange = (val: number) => {
  668. pagination.pageSize = val;
  669. pagination.page = 1;
  670. loadDynamicList();
  671. };
  672. // 当前页改变
  673. const handleCurrentChange = (val: number) => {
  674. pagination.page = val;
  675. loadDynamicList();
  676. };
  677. // 根据 phoneId 判断用户类型
  678. const getUserTypeFromPhoneId = (phoneId: string | undefined): number => {
  679. if (!phoneId) return 1; // 默认商家
  680. const prefix = phoneId.split("_")[0]; // 截取 "_" 之前的文字
  681. return prefix === "store" ? 1 : 2; // store = 商家(1), 其他 = 用户(2)
  682. };
  683. // 加载动态列表
  684. const loadDynamicList = async () => {
  685. try {
  686. // 获取店铺ID(从 userStore 中获取,如果没有则使用默认值)
  687. const phoneId = userStore.userInfo?.phoneId;
  688. const res = await getUserDynamics({
  689. type: 2, // 固定值,表示动态类型
  690. isfollowed: isfollowed.value, // 0 推荐, 1 关注(使用全局变量)
  691. myself: 0, // 0 表示他人的动态
  692. page: pagination.page,
  693. size: pagination.pageSize,
  694. phoneId: `store_${userStore.userInfo?.phone}`
  695. });
  696. // 处理返回的数据
  697. if (res.data) {
  698. // 根据实际返回的数据结构进行映射
  699. const responseData = res.data as any;
  700. const list = responseData.records;
  701. dynamicList.value = list.map((item: any) => {
  702. const phoneId = item.phoneId || item.storeId;
  703. const userType = getUserTypeFromPhoneId(phoneId); // 根据 phoneId 判断用户类型
  704. // 从 phoneId 中提取手机号("_" 之后的部分)
  705. let phone = item.phone || item.userPhone || item.mobile || "";
  706. if (!phone && phoneId && phoneId.includes("_")) {
  707. phone = phoneId.split("_")[1]; // 截取 "_" 之后的文字作为手机号
  708. }
  709. // 输出关注状态(仅第一条)
  710. if (item.id === list[0].id) {
  711. console.log("接口返回的isFollowThis:", item.isFollowThis, "(0=未关注, 1=已关注)");
  712. }
  713. // 解析媒体列表(支持多张图片和视频)
  714. const mediaUrl = item.imagePath || "";
  715. const mediaUrls = mediaUrl
  716. .split(",")
  717. .map((url: string) => url.trim())
  718. .filter((url: string) => url);
  719. const mediaList: MediaItem[] = mediaUrls.map((url: string) => ({
  720. url,
  721. type: url.toLowerCase().endsWith(".mp4") ? ("video" as const) : ("image" as const)
  722. }));
  723. const firstUrl = mediaUrls[0] || "";
  724. const isVideo = firstUrl.toLowerCase().endsWith(".mp4");
  725. const mediaType = isVideo ? "video" : "image";
  726. // 获取用户头像(优先使用 userImage 字段)
  727. const userAvatar = item.userImage || item.userAvatar || item.avatar || item.headImg || "";
  728. return {
  729. // 保留接口返回的所有原始字段
  730. ...item,
  731. // 额外添加的处理字段
  732. id: item.id || item.dynamicId,
  733. title: item.title || item.content || item.dynamicContent || "这家店超好吃....",
  734. content: item.content || item.dynamicContent || "",
  735. imageUrl: firstUrl, // 使用第一个URL(兼容旧逻辑)
  736. mediaList, // 完整媒体列表
  737. userName: item.userName || item.nickname || item.storeName || "用户",
  738. userAvatar: userAvatar,
  739. likeCount: item.likeCount || item.praiseCount || 0,
  740. isLiked: item.isLiked || item.isPraise || false,
  741. createTime: item.createTime || item.createDate || new Date().toISOString(),
  742. userId: item.userId || item.createUserId,
  743. phoneId: phoneId,
  744. storeUserId: item.storeUserId || item.userId || item.createUserId, // 小店用户ID
  745. userType: userType, // 用户类型:1商家,2用户
  746. phone: phone, // 手机号(从 phoneId 或其他字段获取)
  747. isFollowed: item.isFollowThis, // 使用isFollowThis字段:0未关注(显示按钮),1已关注(隐藏按钮)
  748. isFollowThis: item.isFollowThis, // 是否已关注:0未关注,1已关注(用于判断关注按钮显示)
  749. isVideo: isVideo, // 是否为视频
  750. mediaType: mediaType // 媒体类型
  751. };
  752. });
  753. pagination.total = responseData.total || responseData.totalCount || list.length;
  754. }
  755. } catch (error) {
  756. console.error("加载动态列表失败:", error);
  757. ElMessage.error("加载动态列表失败");
  758. // 失败时清空列表
  759. dynamicList.value = [];
  760. pagination.total = 0;
  761. }
  762. };
  763. // 点击卡片 - 查看详情(直接使用列表数据)
  764. const handleCardClick = async (item: DynamicItem) => {
  765. console.log("点击动态:", item);
  766. console.log("isFollowThis值:", item.isFollowThis, "(0=未关注显示按钮, 1=已关注隐藏按钮)");
  767. console.log("isMyDynamic:", isMyDynamic.value);
  768. // 直接使用列表数据构建详情
  769. currentDetail.value = {
  770. ...item,
  771. author: {
  772. id: 0,
  773. name: item.userName,
  774. avatar: item.userAvatar
  775. },
  776. context: item.context,
  777. createdTime: item.createdTime,
  778. dianzanCount: item.dianzanCount,
  779. isLike: item.isLike,
  780. commentCount: 0,
  781. isFollowThis: item.isFollowThis // 使用item.isFollowThis而不是item.isFollowed
  782. };
  783. detailDrawerVisible.value = true;
  784. // 打开抽屉时加载评论列表
  785. await loadCommentList();
  786. };
  787. // 列表点赞/取消点赞
  788. const handleLike = async (item: DynamicItem) => {
  789. try {
  790. // 获取当前用户的手机号,并在前面拼接 "store_"
  791. const phone = userStore.userInfo?.phone || "";
  792. const currentUserPhoneId = phone.startsWith("store_") ? phone : `store_${phone}`;
  793. const params = {
  794. userId: currentUserPhoneId, // 当前用户phoneId
  795. huifuId: item.id, // 动态ID
  796. type: 2 // 2表示点赞
  797. };
  798. // 根据当前点赞状态调用不同的接口
  799. if (item.isLike == "1") {
  800. // 已点赞,调用取消点赞接口
  801. await unlikeDynamicNew(params);
  802. } else {
  803. // 未点赞,调用点赞接口
  804. await likeDynamicNew(params);
  805. }
  806. await loadDynamicList();
  807. ElMessage.success(item.isLike != "1" ? "点赞成功" : "取消点赞");
  808. } catch (error) {
  809. console.error("列表点赞操作失败:", error);
  810. ElMessage.error("操作失败");
  811. }
  812. };
  813. // 关闭详情
  814. const handleCloseDetail = () => {
  815. detailDrawerVisible.value = false;
  816. // 暂停所有视频
  817. videoRefs.value.forEach(video => {
  818. if (video && !video.paused) {
  819. video.pause();
  820. }
  821. });
  822. setTimeout(() => {
  823. currentDetail.value = null;
  824. currentCarouselIndex.value = 0;
  825. videoRefs.value.clear();
  826. }, 300);
  827. };
  828. // 详情页点赞(表单方式提交)
  829. const handleDetailLike = async () => {
  830. if (!currentDetail.value) return;
  831. try {
  832. // 获取当前用户的手机号,并在前面拼接 "store_"
  833. const phone = userStore.userInfo?.phone || "";
  834. const currentUserPhoneId = phone.startsWith("store_") ? phone : `store_${phone}`;
  835. const params = {
  836. userId: currentUserPhoneId, // 当前用户phoneId
  837. huifuId: currentDetail.value.id, // 动态ID
  838. type: 2 // 2表示点赞
  839. };
  840. // 根据当前点赞状态调用不同的接口
  841. if (currentDetail.value.isLike == "1") {
  842. // 已点赞,调用取消点赞接口
  843. await unlikeDynamicNew(params);
  844. } else {
  845. // 未点赞,调用点赞接口
  846. await likeDynamicNew(params);
  847. }
  848. // 切换点赞状态
  849. currentDetail.value.isLike = !currentDetail.value.isLike;
  850. currentDetail.value.likeCount += currentDetail.value.isLike ? 1 : -1;
  851. // 同步更新列表中的数据
  852. const listItem = dynamicList.value.find(item => item.id === currentDetail.value?.id);
  853. if (listItem) {
  854. listItem.isLike = currentDetail.value.isLike;
  855. listItem.likeCount = currentDetail.value.likeCount;
  856. }
  857. ElMessage.success(currentDetail.value.isLike == "1" ? "点赞成功" : "取消点赞");
  858. } catch (error) {
  859. console.error("点赞操作失败:", error);
  860. ElMessage.error("操作失败");
  861. }
  862. };
  863. // 显示评论
  864. // 评论相关
  865. const commentDrawerVisible = ref(false);
  866. const commentListData = ref<any[]>([]);
  867. const commentInput = ref("");
  868. const commentSubmitting = ref(false);
  869. const currentCommentDynamicId = ref<number | string>("");
  870. const replyingComment = ref<any>(null); // 当前正在回复的评论
  871. const handleShowComments = () => {
  872. if (!currentDetail.value) return;
  873. commentDrawerVisible.value = true;
  874. currentCommentDynamicId.value = currentDetail.value.id;
  875. };
  876. // 加载评论列表
  877. const loadCommentList = async () => {
  878. if (!currentDetail.value) return;
  879. try {
  880. let params = {
  881. businessId: String(currentDetail.value.id),
  882. businessType: "2",
  883. replyStatus: 0,
  884. pageNum: 1,
  885. pageSize: 10,
  886. commentType: 0,
  887. days: "",
  888. phoneId: `store_${userStore.userInfo?.phone}`
  889. };
  890. const res: any = await commentList(params);
  891. if (res.code === 200) {
  892. commentListData.value = res.data.records || [];
  893. // 更新评论总数
  894. if (currentDetail.value) {
  895. currentDetail.value.commentCount = res.data.total || 0;
  896. }
  897. console.log("评论列表:", commentListData.value);
  898. console.log("评论总数:", res.data.total);
  899. }
  900. } catch (error) {
  901. console.error("加载评论列表失败:", error);
  902. }
  903. };
  904. // 提交评论
  905. const handleSubmitComment = async () => {
  906. if (!commentInput.value.trim()) {
  907. ElMessage.warning("请输入评论内容");
  908. return;
  909. }
  910. if (!currentDetail.value) {
  911. ElMessage.error("动态信息不存在");
  912. return;
  913. }
  914. try {
  915. commentSubmitting.value = true;
  916. // 判断是回复评论还是评论动态
  917. const isReply = !!replyingComment.value;
  918. const params: any = {
  919. replyId: isReply ? replyingComment.value.id : "", // 回复评论时传评论ID,否则为空
  920. commentContent: commentInput.value,
  921. businessType: "2",
  922. businessId: String(currentDetail.value.id),
  923. storeId: userStore.userInfo?.storeId || userStore.userInfo?.createdId,
  924. commentStar: "",
  925. phoneId: `store_${userStore.userInfo?.phone}`
  926. };
  927. const res: any = await saveComment(params);
  928. if (res.code === 200) {
  929. ElMessage.success(isReply ? "回复成功" : "评论成功");
  930. commentInput.value = "";
  931. replyingComment.value = null; // 清空回复状态
  932. await loadCommentList();
  933. } else {
  934. ElMessage.error(res.message || (isReply ? "回复失败" : "评论失败"));
  935. }
  936. } catch (error) {
  937. console.error("提交评论失败:", error);
  938. ElMessage.error(replyingComment.value ? "回复失败" : "评论失败");
  939. } finally {
  940. commentSubmitting.value = false;
  941. }
  942. };
  943. // 点赞评论
  944. const handleLikeComment = async (comment: any) => {
  945. console.log(comment);
  946. try {
  947. // 获取当前用户的手机号,并在前面拼接 "store_"
  948. const phone = userStore.userInfo?.phone || "";
  949. const currentUserPhoneId = phone.startsWith("store_") ? phone : `store_${phone}`;
  950. const params = {
  951. userId: currentUserPhoneId, // 当前用户phoneId
  952. huifuId: comment.id, // 动态ID
  953. type: 1 // 2表示点赞
  954. };
  955. // 根据当前点赞状态调用不同的接口
  956. if (comment.isLiked) {
  957. // 已点赞,调用取消点赞接口
  958. await unlikeDynamicNew(params);
  959. } else {
  960. // 未点赞,调用点赞接口
  961. await likeDynamicNew(params);
  962. }
  963. // 切换点赞状态
  964. comment.isLiked = !comment.isLiked;
  965. comment.likeCount += comment.isLiked ? 1 : -1;
  966. ElMessage.success(comment.isLiked ? "点赞成功" : "取消点赞");
  967. } catch (error) {
  968. console.error("列表点赞操作失败:", error);
  969. ElMessage.error("操作失败");
  970. }
  971. };
  972. // 回复评论
  973. const handleReplyComment = (comment: any) => {
  974. replyingComment.value = comment;
  975. commentInput.value = ``;
  976. // 聚焦到输入框
  977. setTimeout(() => {
  978. const textarea = document.querySelector(".comment-input-wrapper textarea") as HTMLTextAreaElement;
  979. if (textarea) {
  980. textarea.focus();
  981. }
  982. }, 100);
  983. };
  984. // 取消回复
  985. const handleCancelReply = () => {
  986. replyingComment.value = null;
  987. commentInput.value = "";
  988. };
  989. // 分享
  990. const handleShare = async () => {
  991. shareDialogVisible.value = true;
  992. await loadShareFriendList();
  993. };
  994. // 加载好友列表
  995. const loadShareFriendList = async () => {
  996. try {
  997. // 获取当前用户的手机号,并在前面拼接 "store_"
  998. const phone = userStore.userInfo?.phone || "";
  999. const fansId = phone.startsWith("store_") ? phone : `store_${phone}`;
  1000. const res: any = await getMutualAttention({
  1001. page: 1,
  1002. size: 1000,
  1003. fansId: fansId,
  1004. name: ""
  1005. });
  1006. if (res.code === 200) {
  1007. const dataList = res.data?.records || res.data?.list || res.data || [];
  1008. shareFriendList.value = dataList.map((item: any) => ({
  1009. id: item.id || item.userId,
  1010. name: item.userName || item.nickname || item.name || "用户",
  1011. avatar: item.userImage || item.avatar || item.headImg || "",
  1012. phoneId: item.phoneId || item.fansId || ""
  1013. }));
  1014. console.log("加载好友列表成功:", shareFriendList.value);
  1015. }
  1016. } catch (error) {
  1017. console.error("加载好友列表失败:", error);
  1018. ElMessage.error("加载好友列表失败");
  1019. shareFriendList.value = [];
  1020. }
  1021. };
  1022. // 搜索好友
  1023. const handleShareSearch = () => {
  1024. // 搜索由计算属性自动处理
  1025. };
  1026. // 选择好友
  1027. const handleSelectFriend = (friend: ShareFriend) => {
  1028. const index = selectedFriends.value.indexOf(friend.id);
  1029. if (index > -1) {
  1030. // 已选择,取消选择
  1031. selectedFriends.value.splice(index, 1);
  1032. } else {
  1033. // 未选择,添加选择
  1034. selectedFriends.value.push(friend.id);
  1035. }
  1036. };
  1037. // 移除已选择的好友
  1038. const handleRemoveFriend = (friendId: number) => {
  1039. const index = selectedFriends.value.indexOf(friendId);
  1040. if (index > -1) {
  1041. selectedFriends.value.splice(index, 1);
  1042. }
  1043. };
  1044. // 确认分享
  1045. const handleConfirmShare = async () => {
  1046. if (selectedFriends.value.length === 0) {
  1047. ElMessage.warning("请选择要分享的好友");
  1048. return;
  1049. }
  1050. if (!currentDetail.value) {
  1051. ElMessage.error("动态信息不存在");
  1052. return;
  1053. }
  1054. try {
  1055. shareSubmitting.value = true;
  1056. // 调用 addTransferCount 接口,传递动态 id
  1057. const res: any = await addTransferCount({
  1058. id: currentDetail.value.id
  1059. });
  1060. if (res.code === 200) {
  1061. ElMessage.success(`已分享给 ${selectedFriends.value.length} 位好友`);
  1062. shareDialogVisible.value = false;
  1063. // 可以在这里更新动态的分享数(如果需要的话)
  1064. console.log("分享成功,动态ID:", currentDetail.value.id);
  1065. } else {
  1066. ElMessage.error(res.message || "分享失败");
  1067. }
  1068. } catch (error) {
  1069. console.error("分享失败:", error);
  1070. ElMessage.error("分享失败");
  1071. } finally {
  1072. shareSubmitting.value = false;
  1073. }
  1074. };
  1075. // 关闭分享对话框
  1076. const handleCloseShareDialog = () => {
  1077. shareSearch.value = "";
  1078. selectedFriends.value = [];
  1079. shareFriendList.value = [];
  1080. };
  1081. // 查看用户主页
  1082. const handleViewUserProfile = () => {
  1083. if (!currentDetail.value) return;
  1084. // 跳转到他人动态主页,传递用户信息
  1085. router.push({
  1086. path: "/dynamicManagement/userDynamic",
  1087. query: {
  1088. userId: currentDetail.value.storeUserId || currentDetail.value.userId || "",
  1089. phoneId: currentDetail.value.phoneId || "",
  1090. userName: currentDetail.value.userName || "",
  1091. userAvatar: currentDetail.value.userAvatar || "",
  1092. phone: currentDetail.value.phone || ""
  1093. }
  1094. });
  1095. };
  1096. // 详情页关注(右侧操作栏)
  1097. const handleFollowInDetail = async () => {
  1098. if (!currentDetail.value) return;
  1099. try {
  1100. const phone = userStore.userInfo?.phone || "";
  1101. const currentUserPhoneId = phone.startsWith("store_") ? phone : `store_${phone}`;
  1102. await toggleFollowUser({
  1103. followedId: currentDetail.value.phoneId || "",
  1104. fansId: currentUserPhoneId,
  1105. fansType: 2
  1106. });
  1107. // 更新关注状态
  1108. if (currentDetail.value) {
  1109. currentDetail.value.isFollowed = 1;
  1110. currentDetail.value.isFollowThis = 1; // 同时更新isFollowThis字段
  1111. }
  1112. // 同步更新列表中的状态
  1113. const listItem = dynamicList.value.find(item => item.id === currentDetail.value?.id);
  1114. if (listItem) {
  1115. listItem.isFollowed = 1;
  1116. listItem.isFollowThis = 1; // 同时更新列表项的isFollowThis状态
  1117. }
  1118. ElMessage.success("关注成功");
  1119. } catch (error) {
  1120. console.error("关注操作失败:", error);
  1121. ElMessage.error("操作失败");
  1122. }
  1123. };
  1124. // 编辑动态
  1125. const handleEditDynamic = () => {
  1126. if (!currentDetail.value) return;
  1127. detailDrawerVisible.value = false;
  1128. router.push({
  1129. path: "/dynamicManagement/publishDynamic",
  1130. query: { id: currentDetail.value.id }
  1131. });
  1132. };
  1133. // 删除动态
  1134. const handleDeleteDynamic = async () => {
  1135. if (!currentDetail.value) return;
  1136. try {
  1137. await ElMessageBox.confirm("确定要删除这条动态吗?删除后将无法恢复。", "删除确认", {
  1138. confirmButtonText: "确定删除",
  1139. cancelButtonText: "取消",
  1140. type: "warning"
  1141. });
  1142. // TODO: 集成真实接口
  1143. // await deleteDynamic({ id: currentDetail.value.id });
  1144. ElMessage.success("删除成功");
  1145. detailDrawerVisible.value = false;
  1146. // 从列表中移除
  1147. const index = dynamicList.value.findIndex(item => item.id === currentDetail.value?.id);
  1148. if (index > -1) {
  1149. dynamicList.value.splice(index, 1);
  1150. pagination.total--;
  1151. }
  1152. } catch {
  1153. // 用户取消删除
  1154. }
  1155. };
  1156. // 举报动态
  1157. const handleReportDynamic = () => {
  1158. reportDialogVisible.value = true;
  1159. };
  1160. // 举报图片预览
  1161. const handleReportPreview = (uploadFile: any) => {
  1162. console.log("预览图片", uploadFile);
  1163. };
  1164. // 移除举报图片
  1165. const handleReportRemove = (uploadFile: any, uploadFiles: any[]) => {
  1166. // 已由 v-model:file-list 自动处理
  1167. };
  1168. // 举报图片上传前验证
  1169. const beforeReportUpload = (file: File) => {
  1170. const isImage = file.type.startsWith("image/");
  1171. const isLt5M = file.size / 1024 / 1024 < 5;
  1172. if (!isImage) {
  1173. ElMessage.error("只能上传图片文件!");
  1174. return false;
  1175. }
  1176. if (!isLt5M) {
  1177. ElMessage.error("图片大小不能超过 5MB!");
  1178. return false;
  1179. }
  1180. return true;
  1181. };
  1182. // 自定义举报图片上传
  1183. const handleReportUpload = async (options: any) => {
  1184. const { file, onSuccess, onError } = options;
  1185. try {
  1186. const uploadFormData = new FormData();
  1187. uploadFormData.append("file", file);
  1188. const response: any = await uploadImg(uploadFormData);
  1189. // 处理返回格式:{ code, success, data: string[], msg }
  1190. if (response && response.code === 200 && response.data && Array.isArray(response.data) && response.data.length > 0) {
  1191. // 上传成功,返回图片URL(取数组第一个元素)
  1192. onSuccess({
  1193. url: response.data[0]
  1194. });
  1195. } else {
  1196. ElMessage.error(response?.msg || "图片上传失败");
  1197. onError(new Error(response?.msg || "图片上传失败"));
  1198. }
  1199. } catch (error) {
  1200. console.error("图片上传失败:", error);
  1201. ElMessage.error("图片上传失败");
  1202. onError(error);
  1203. }
  1204. };
  1205. // 提交举报
  1206. const handleSubmitReport = async () => {
  1207. // 验证表单
  1208. if (!reportForm.reason) {
  1209. ElMessage.warning("请选择举报原因");
  1210. return;
  1211. }
  1212. // 只有选择"其他举报"时才验证详细描述
  1213. if (reportForm.reason === "其他举报" && !reportForm.description.trim()) {
  1214. ElMessage.warning("请填写详细描述");
  1215. return;
  1216. }
  1217. if (!currentDetail.value) {
  1218. ElMessage.warning("动态信息异常");
  1219. return;
  1220. }
  1221. reportSubmitting.value = true;
  1222. try {
  1223. // 获取违规类型编号
  1224. const violationType = violationTypeMap[reportForm.reason];
  1225. // 获取举报凭证图片(如果有多张,用逗号分隔)
  1226. const reportEvidenceImg = reportForm.fileList
  1227. .map((f: any) => f.url || f.response?.url)
  1228. .filter(Boolean)
  1229. .join(",");
  1230. // 获取当前用户信息
  1231. const currentUserId = userStore.userInfo?.id || userStore.userInfo?.userId || "";
  1232. const currentUserType = userStore.userInfo?.userType || userStore.userInfo?.type || 1; // 用户类型:1商家,2用户,默认1
  1233. // 获取被举报用户类型(从 phoneId 解析)
  1234. const reportedUserType = currentDetail.value.userType || 1;
  1235. // 根据手机号获取被举报人ID
  1236. let reportedUserId = currentDetail.value.storeUserId || currentDetail.value.userId || "";
  1237. if (currentDetail.value.phone) {
  1238. try {
  1239. const userRes = await getUserByPhone({ phone: currentDetail.value.phone });
  1240. const userData = userRes.data as any;
  1241. if (userData && userData.id) {
  1242. reportedUserId = userData.id;
  1243. console.log("通过手机号获取到被举报人ID:", reportedUserId);
  1244. }
  1245. } catch (error) {
  1246. console.error("获取被举报人ID失败:", error);
  1247. // 如果获取失败,使用原有的 storeUserId
  1248. }
  1249. }
  1250. console.log("当前用户类型:", userStore.userInfo);
  1251. console.log("被举报用户类型:", reportedUserType);
  1252. console.log("被举报人ID:", reportedUserId);
  1253. console.log("动态详情:", currentDetail.value);
  1254. // 调用举报接口
  1255. await reportUserViolation({
  1256. dynamicsId: currentDetail.value.id, // 动态ID
  1257. reportContextType: "2", // 举报上下文类型:2表示动态
  1258. violationType: violationType, // 违规类型
  1259. otherReasonContent: reportForm.reason === "其他举报" ? reportForm.description : "", // 只有选择"其他举报"时才传详细描述
  1260. reportEvidenceImg: reportEvidenceImg, // 举报凭证图片
  1261. reportedUserId: reportedUserId, // 被举报用户ID(通过手机号获取)
  1262. reportedUserType: reportedUserType, // 被举报用户类型(从 phoneId 解析)
  1263. reportingUserId: currentUserId, // 举报人ID
  1264. reportingUserType: currentUserType // 举报人类型(当前登录用户类型)
  1265. });
  1266. // 如果同时拉黑该用户
  1267. if (reportForm.agreed) {
  1268. try {
  1269. // 调用拉黑接口(跳过确认对话框)
  1270. await handleBlockUser(true);
  1271. ElMessage.success("举报提交成功,已拉黑该用户");
  1272. } catch (blockError) {
  1273. console.error("拉黑失败:", blockError);
  1274. ElMessage.warning("举报提交成功,但拉黑失败");
  1275. }
  1276. } else {
  1277. ElMessage.success("举报提交成功,我们会尽快处理");
  1278. }
  1279. reportDialogVisible.value = false;
  1280. } catch (error) {
  1281. console.error("举报提交失败:", error);
  1282. ElMessage.error("举报提交失败");
  1283. } finally {
  1284. reportSubmitting.value = false;
  1285. }
  1286. };
  1287. // 关闭举报对话框
  1288. const handleCloseReportDialog = () => {
  1289. reportForm.reason = "用户头像"; // 重置为默认选项
  1290. reportForm.description = "";
  1291. reportForm.fileList = [];
  1292. reportForm.agreed = false;
  1293. };
  1294. // 监听举报原因变化,如果不是"其他举报"则清空详细描述
  1295. watch(
  1296. () => reportForm.reason,
  1297. newReason => {
  1298. if (newReason !== "其他举报") {
  1299. reportForm.description = "";
  1300. }
  1301. }
  1302. );
  1303. // 拉黑用户(点击菜单项)
  1304. const handleBlockUserClick = () => {
  1305. handleBlockUser(false);
  1306. };
  1307. // 拉黑用户
  1308. const handleBlockUser = async (skipConfirm: boolean = false) => {
  1309. if (!currentDetail.value) return;
  1310. try {
  1311. // 如果不跳过确认,显示确认对话框
  1312. if (!skipConfirm) {
  1313. await ElMessageBox.confirm("拉黑后将不再看到该用户的动态,确定要拉黑吗?", "拉黑确认", {
  1314. confirmButtonText: "确定拉黑",
  1315. cancelButtonText: "取消",
  1316. type: "warning"
  1317. });
  1318. }
  1319. // 获取当前用户信息
  1320. const currentUserId = userStore.userInfo?.id || userStore.userInfo?.userId || "";
  1321. const currentUserType = userStore.userInfo?.userType || userStore.userInfo?.type || 1; // 用户类型:1商家,2用户,默认1
  1322. // 获取被拉黑用户类型(从 phoneId 解析)
  1323. const blockedUserType = currentDetail.value.userType || 1;
  1324. console.log("当前用户信息:", userStore.userInfo);
  1325. console.log("当前用户类型:", currentUserType);
  1326. console.log("被拉黑用户类型:", blockedUserType);
  1327. console.log("动态详情:", currentDetail.value);
  1328. // 调用拉黑接口
  1329. await blockUser({
  1330. blockerType: currentUserType, // 拉黑者类型(当前登录用户类型)
  1331. blockedType: blockedUserType, // 被拉黑者类型(从 phoneId 解析)
  1332. blockerId: currentUserId, // 拉黑者ID(当前登录用户)
  1333. blockedId: currentDetail.value.storeUserId || currentDetail.value.userId || "" // 被拉黑者ID
  1334. });
  1335. if (!skipConfirm) {
  1336. ElMessage.success("已拉黑该用户");
  1337. }
  1338. detailDrawerVisible.value = false;
  1339. // 从列表中移除该用户的动态
  1340. const index = dynamicList.value.findIndex(item => item.id === currentDetail.value?.id);
  1341. if (index > -1) {
  1342. dynamicList.value.splice(index, 1);
  1343. pagination.total--;
  1344. }
  1345. } catch (error) {
  1346. if (!skipConfirm) {
  1347. // 单独拉黑时,用户取消操作或接口失败
  1348. console.error("拉黑失败:", error);
  1349. if (error !== "cancel") {
  1350. ElMessage.error("拉黑失败");
  1351. }
  1352. } else {
  1353. // 举报后自动拉黑失败,抛出错误
  1354. throw error;
  1355. }
  1356. }
  1357. };
  1358. // 初始化
  1359. onMounted(() => {
  1360. loadDynamicList();
  1361. });
  1362. </script>
  1363. <style scoped lang="scss">
  1364. .dynamic-management-container {
  1365. min-height: calc(100vh - 120px);
  1366. padding: 20px;
  1367. background: #ffffff;
  1368. // 头部区域
  1369. .header-section {
  1370. display: flex;
  1371. align-items: center;
  1372. justify-content: space-between;
  1373. margin-bottom: 20px;
  1374. border-bottom: 1px solid #e4e7ed;
  1375. :deep(.el-tabs) {
  1376. flex: 1;
  1377. margin-bottom: -1px;
  1378. .el-tabs__header {
  1379. margin-bottom: 0;
  1380. }
  1381. .el-tabs__nav-wrap::after {
  1382. display: none;
  1383. }
  1384. }
  1385. .action-buttons {
  1386. padding-bottom: 10px;
  1387. margin-left: 20px;
  1388. }
  1389. }
  1390. // 内容区域
  1391. .content-section {
  1392. margin-top: 20px;
  1393. }
  1394. // 动态网格布局
  1395. .dynamic-grid {
  1396. display: grid;
  1397. grid-template-columns: repeat(3, 1fr);
  1398. gap: 20px;
  1399. margin-bottom: 20px;
  1400. @media (width <= 1400px) {
  1401. grid-template-columns: repeat(2, 1fr);
  1402. }
  1403. @media (width <= 768px) {
  1404. grid-template-columns: repeat(1, 1fr);
  1405. }
  1406. }
  1407. // 动态卡片
  1408. .dynamic-card {
  1409. overflow: hidden;
  1410. cursor: pointer;
  1411. background: #ffffff;
  1412. border: 1px solid #e4e7ed;
  1413. border-radius: 8px;
  1414. transition: all 0.3s;
  1415. &:hover {
  1416. box-shadow: 0 2px 12px rgb(0 0 0 / 10%);
  1417. transform: translateY(-2px);
  1418. }
  1419. // 图片区域
  1420. .dynamic-image-wrapper {
  1421. display: flex;
  1422. align-items: center;
  1423. justify-content: center;
  1424. width: 100%;
  1425. height: 220px;
  1426. overflow: hidden;
  1427. background: #f5f7fa;
  1428. .dynamic-image {
  1429. width: 100%;
  1430. height: 100%;
  1431. object-fit: cover;
  1432. }
  1433. .image-placeholder {
  1434. display: flex;
  1435. align-items: center;
  1436. justify-content: center;
  1437. width: 100%;
  1438. height: 100%;
  1439. background: #f5f7fa;
  1440. }
  1441. }
  1442. // 动态内容
  1443. .dynamic-content {
  1444. padding: 12px 16px;
  1445. .dynamic-text {
  1446. margin-bottom: 12px;
  1447. overflow: hidden;
  1448. font-size: 14px;
  1449. color: #333333;
  1450. text-overflow: ellipsis;
  1451. white-space: nowrap;
  1452. }
  1453. // 底部信息
  1454. .dynamic-footer {
  1455. display: flex;
  1456. align-items: center;
  1457. justify-content: space-between;
  1458. .user-info {
  1459. display: flex;
  1460. gap: 8px;
  1461. align-items: center;
  1462. .user-avatar {
  1463. display: flex;
  1464. align-items: center;
  1465. justify-content: center;
  1466. width: 24px;
  1467. height: 24px;
  1468. overflow: hidden;
  1469. background: #e4e7ed;
  1470. border-radius: 50%;
  1471. img {
  1472. width: 100%;
  1473. height: 100%;
  1474. object-fit: cover;
  1475. }
  1476. }
  1477. .user-name {
  1478. font-size: 13px;
  1479. color: #666666;
  1480. }
  1481. }
  1482. .like-section {
  1483. display: flex;
  1484. gap: 4px;
  1485. align-items: center;
  1486. .like-icon {
  1487. cursor: pointer;
  1488. transition: color 0.3s;
  1489. &:hover {
  1490. color: #f56c6c;
  1491. }
  1492. }
  1493. .like-count {
  1494. font-size: 13px;
  1495. color: #666666;
  1496. }
  1497. }
  1498. }
  1499. }
  1500. }
  1501. // 空状态
  1502. .empty-section {
  1503. padding: 80px 0;
  1504. text-align: center;
  1505. }
  1506. // 分页
  1507. .pagination-section {
  1508. display: flex;
  1509. justify-content: center;
  1510. padding: 20px 0;
  1511. margin-top: 30px;
  1512. }
  1513. // 详情 Drawer
  1514. :deep(.detail-drawer) {
  1515. .el-drawer__header {
  1516. position: absolute;
  1517. top: 0;
  1518. right: 0;
  1519. left: 0;
  1520. z-index: 10;
  1521. padding: 0;
  1522. margin: 0;
  1523. background: transparent;
  1524. }
  1525. .el-drawer__body {
  1526. padding: 0;
  1527. background: #000000;
  1528. }
  1529. .drawer-header {
  1530. padding: 20px;
  1531. .close-btn {
  1532. padding: 8px;
  1533. font-size: 24px;
  1534. color: #ffffff;
  1535. background: #ffffff;
  1536. }
  1537. }
  1538. .detail-content {
  1539. position: relative;
  1540. display: flex;
  1541. width: 100%;
  1542. height: 100%;
  1543. // 主内容区域
  1544. .detail-main {
  1545. display: flex;
  1546. flex: 1;
  1547. flex-direction: column;
  1548. align-items: center;
  1549. justify-content: center;
  1550. padding: 80px 120px 40px 40px;
  1551. .media-container {
  1552. position: relative;
  1553. display: flex;
  1554. flex: 1;
  1555. align-items: center;
  1556. justify-content: center;
  1557. width: 100%;
  1558. max-width: 800px;
  1559. min-height: 400px;
  1560. .media-carousel {
  1561. width: 100%;
  1562. height: 100%;
  1563. min-height: 400px;
  1564. max-height: 70vh;
  1565. :deep(.el-carousel__container) {
  1566. height: 100% !important;
  1567. min-height: 400px;
  1568. }
  1569. :deep(.el-carousel__item) {
  1570. display: flex;
  1571. align-items: center;
  1572. justify-content: center;
  1573. background: transparent;
  1574. }
  1575. :deep(.el-carousel__arrow) {
  1576. width: 48px;
  1577. height: 48px;
  1578. font-size: 24px;
  1579. color: #ffffff;
  1580. background-color: rgb(0 0 0 / 40%);
  1581. border: none;
  1582. &:hover {
  1583. background-color: rgb(0 0 0 / 60%);
  1584. }
  1585. }
  1586. :deep(.el-carousel__indicators) {
  1587. bottom: -30px;
  1588. .el-carousel__indicator {
  1589. .el-carousel__button {
  1590. width: 8px;
  1591. height: 8px;
  1592. background-color: rgb(255 255 255 / 40%);
  1593. border-radius: 50%;
  1594. }
  1595. &.is-active .el-carousel__button {
  1596. background-color: #409eff;
  1597. }
  1598. }
  1599. }
  1600. }
  1601. .detail-media {
  1602. max-width: 100%;
  1603. max-height: 65vh;
  1604. object-fit: contain;
  1605. border-radius: 8px;
  1606. }
  1607. .detail-image {
  1608. max-width: 100%;
  1609. max-height: 65vh;
  1610. object-fit: contain;
  1611. border-radius: 8px;
  1612. }
  1613. .detail-video {
  1614. width: 100%;
  1615. max-height: 65vh;
  1616. background: #000000;
  1617. border-radius: 8px;
  1618. }
  1619. .media-placeholder {
  1620. display: flex;
  1621. align-items: center;
  1622. justify-content: center;
  1623. width: 400px;
  1624. height: 400px;
  1625. background: rgb(255 255 255 / 5%);
  1626. border-radius: 8px;
  1627. }
  1628. .media-counter {
  1629. position: absolute;
  1630. right: 16px;
  1631. bottom: -24px;
  1632. padding: 4px 12px;
  1633. font-size: 14px;
  1634. color: #ffffff;
  1635. background: rgb(0 0 0 / 50%);
  1636. border-radius: 12px;
  1637. }
  1638. }
  1639. .detail-info {
  1640. width: 100%;
  1641. max-width: 800px;
  1642. padding: 20px 0;
  1643. .author-info {
  1644. display: flex;
  1645. gap: 12px;
  1646. align-items: center;
  1647. margin-bottom: 16px;
  1648. .author-avatar {
  1649. display: flex;
  1650. align-items: center;
  1651. justify-content: center;
  1652. width: 40px;
  1653. height: 40px;
  1654. overflow: hidden;
  1655. background: rgb(255 255 255 / 10%);
  1656. border-radius: 50%;
  1657. img {
  1658. width: 100%;
  1659. height: 100%;
  1660. object-fit: cover;
  1661. }
  1662. }
  1663. .author-details {
  1664. flex: 1;
  1665. .author-name {
  1666. margin-bottom: 4px;
  1667. font-size: 16px;
  1668. font-weight: 500;
  1669. color: #ffffff;
  1670. }
  1671. .publish-time {
  1672. font-size: 13px;
  1673. color: rgb(255 255 255 / 60%);
  1674. }
  1675. }
  1676. }
  1677. .detail-description {
  1678. font-size: 15px;
  1679. line-height: 1.6;
  1680. color: #ffffff;
  1681. p {
  1682. margin: 0;
  1683. }
  1684. }
  1685. }
  1686. }
  1687. // 右侧操作栏
  1688. .action-bar {
  1689. position: fixed;
  1690. right: 40px;
  1691. bottom: 100px;
  1692. z-index: 10;
  1693. display: flex;
  1694. flex-direction: column;
  1695. gap: 24px;
  1696. .action-item {
  1697. display: flex;
  1698. flex-direction: column;
  1699. gap: 6px;
  1700. align-items: center;
  1701. cursor: pointer;
  1702. transition: transform 0.3s;
  1703. &:hover {
  1704. transform: scale(1.1);
  1705. }
  1706. &.author-action {
  1707. cursor: pointer;
  1708. &:hover {
  1709. transform: scale(1.1);
  1710. }
  1711. }
  1712. .action-avatar {
  1713. position: relative;
  1714. display: flex;
  1715. align-items: center;
  1716. justify-content: center;
  1717. width: 48px;
  1718. height: 48px;
  1719. overflow: visible;
  1720. cursor: pointer;
  1721. background: rgb(255 255 255 / 20%);
  1722. border: 2px solid #ffffff;
  1723. border-radius: 50%;
  1724. img {
  1725. width: 100%;
  1726. height: 100%;
  1727. object-fit: cover;
  1728. border-radius: 50%;
  1729. }
  1730. .follow-badge {
  1731. position: absolute;
  1732. right: -4px;
  1733. bottom: -4px;
  1734. z-index: 2;
  1735. display: flex;
  1736. align-items: center;
  1737. justify-content: center;
  1738. width: 24px;
  1739. height: 24px;
  1740. cursor: pointer;
  1741. background: #409eff;
  1742. border: 2px solid #ffffff;
  1743. border-radius: 50%;
  1744. transition: transform 0.2s;
  1745. &:hover {
  1746. transform: scale(1.15);
  1747. }
  1748. }
  1749. }
  1750. .action-icon {
  1751. display: flex;
  1752. align-items: center;
  1753. justify-content: center;
  1754. width: 48px;
  1755. height: 48px;
  1756. background: rgb(0 0 0 / 50%);
  1757. backdrop-filter: blur(10px);
  1758. border-radius: 50%;
  1759. &.follow-icon {
  1760. background: #409eff;
  1761. }
  1762. }
  1763. .action-count {
  1764. font-size: 13px;
  1765. color: #ffffff;
  1766. text-align: center;
  1767. text-shadow: 0 1px 3px rgb(0 0 0 / 50%);
  1768. }
  1769. }
  1770. }
  1771. }
  1772. }
  1773. // 更多操作 Popover
  1774. :deep(.more-actions-popover) {
  1775. min-width: 120px;
  1776. padding: 8px 0;
  1777. background: rgb(0 0 0 / 90%);
  1778. backdrop-filter: blur(10px);
  1779. border: 1px solid rgb(255 255 255 / 10%);
  1780. .el-popper__arrow::before {
  1781. background: rgb(0 0 0 / 90%);
  1782. border: 1px solid rgb(255 255 255 / 10%);
  1783. }
  1784. .more-actions-menu {
  1785. .menu-item {
  1786. display: flex;
  1787. gap: 10px;
  1788. align-items: center;
  1789. padding: 10px 16px;
  1790. font-size: 14px;
  1791. line-height: 20px;
  1792. color: #ffffff;
  1793. cursor: pointer;
  1794. transition: all 0.3s;
  1795. &:hover {
  1796. background: rgb(255 255 255 / 10%);
  1797. }
  1798. .el-icon {
  1799. display: inline-flex;
  1800. flex-shrink: 0;
  1801. align-items: center;
  1802. justify-content: center;
  1803. width: 18px;
  1804. height: 18px;
  1805. color: #ffffff;
  1806. vertical-align: middle;
  1807. }
  1808. span {
  1809. display: inline-block;
  1810. line-height: 20px;
  1811. vertical-align: middle;
  1812. }
  1813. }
  1814. }
  1815. }
  1816. // 举报对话框
  1817. :deep(.el-dialog) {
  1818. .report-dialog-content {
  1819. .report-tip {
  1820. margin-bottom: 20px;
  1821. font-size: 14px;
  1822. line-height: 1.6;
  1823. color: #606266;
  1824. }
  1825. .report-reasons {
  1826. margin-bottom: 20px;
  1827. .el-radio-group {
  1828. display: flex;
  1829. flex-wrap: wrap;
  1830. gap: 12px 16px;
  1831. .el-radio {
  1832. height: auto;
  1833. margin-right: 0;
  1834. white-space: nowrap;
  1835. .el-radio__label {
  1836. font-size: 14px;
  1837. color: #303133;
  1838. }
  1839. }
  1840. }
  1841. }
  1842. .report-description {
  1843. margin-bottom: 20px;
  1844. :deep(.el-textarea__inner) {
  1845. font-size: 14px;
  1846. }
  1847. }
  1848. .report-upload {
  1849. margin-bottom: 20px;
  1850. .upload-title {
  1851. margin-bottom: 12px;
  1852. font-size: 14px;
  1853. font-weight: 500;
  1854. color: #303133;
  1855. }
  1856. :deep(.el-upload-list) {
  1857. display: flex;
  1858. flex-wrap: wrap;
  1859. gap: 8px;
  1860. }
  1861. :deep(.el-upload--picture-card) {
  1862. width: 100px;
  1863. height: 100px;
  1864. border-radius: 4px;
  1865. }
  1866. :deep(.el-upload-list--picture-card .el-upload-list__item) {
  1867. width: 100px;
  1868. height: 100px;
  1869. margin: 0;
  1870. border-radius: 4px;
  1871. }
  1872. }
  1873. .report-agreement {
  1874. .el-checkbox {
  1875. .el-checkbox__label {
  1876. font-size: 14px;
  1877. color: #606266;
  1878. }
  1879. }
  1880. }
  1881. }
  1882. .dialog-footer {
  1883. display: flex;
  1884. gap: 12px;
  1885. justify-content: flex-end;
  1886. }
  1887. }
  1888. // 评论侧边栏样式
  1889. .comment-list-container {
  1890. flex: 1;
  1891. height: calc(100vh - 200px);
  1892. padding: 0 20px;
  1893. overflow-y: auto;
  1894. .comment-list {
  1895. .comment-item {
  1896. display: flex;
  1897. gap: 12px;
  1898. padding: 16px 0;
  1899. border-bottom: 1px solid #f0f0f0;
  1900. &:last-child {
  1901. border-bottom: none;
  1902. }
  1903. .comment-avatar {
  1904. display: flex;
  1905. flex-shrink: 0;
  1906. align-items: center;
  1907. justify-content: center;
  1908. width: 40px;
  1909. height: 40px;
  1910. overflow: hidden;
  1911. background: #f5f5f5;
  1912. border-radius: 50%;
  1913. img {
  1914. width: 100%;
  1915. height: 100%;
  1916. object-fit: cover;
  1917. }
  1918. }
  1919. .comment-content-wrapper {
  1920. flex: 1;
  1921. min-width: 0;
  1922. .comment-header,
  1923. .store-comment-header {
  1924. display: flex;
  1925. align-items: center;
  1926. justify-content: space-between;
  1927. margin-bottom: 8px;
  1928. .comment-user-name {
  1929. font-size: 14px;
  1930. font-weight: 500;
  1931. color: #303133;
  1932. }
  1933. .comment-time {
  1934. font-size: 12px;
  1935. color: #909399;
  1936. }
  1937. }
  1938. .comment-text {
  1939. margin-bottom: 8px;
  1940. font-size: 14px;
  1941. line-height: 1.6;
  1942. color: #606266;
  1943. word-break: break-all;
  1944. }
  1945. // 商家回复样式
  1946. .store-comment-wrapper {
  1947. padding: 12px;
  1948. margin-top: 12px;
  1949. background: #f5f7fa;
  1950. border-radius: 8px;
  1951. .store-comment-item {
  1952. display: flex;
  1953. gap: 10px;
  1954. .store-comment-avatar {
  1955. display: flex;
  1956. flex-shrink: 0;
  1957. align-items: center;
  1958. justify-content: center;
  1959. width: 32px;
  1960. height: 32px;
  1961. overflow: hidden;
  1962. background: #ffffff;
  1963. border-radius: 50%;
  1964. img {
  1965. width: 100%;
  1966. height: 100%;
  1967. object-fit: cover;
  1968. }
  1969. }
  1970. .store-comment-content {
  1971. flex: 1;
  1972. min-width: 0;
  1973. .store-comment-header {
  1974. display: flex;
  1975. align-items: center;
  1976. justify-content: space-between;
  1977. margin-bottom: 6px;
  1978. .store-comment-user-name {
  1979. font-size: 13px;
  1980. font-weight: 500;
  1981. color: #409eff;
  1982. }
  1983. .store-comment-time {
  1984. font-size: 11px;
  1985. color: #909399;
  1986. }
  1987. }
  1988. .store-comment-text {
  1989. display: flex;
  1990. align-items: center;
  1991. justify-content: space-between;
  1992. font-size: 13px;
  1993. line-height: 1.5;
  1994. color: #606266;
  1995. word-break: break-all;
  1996. }
  1997. }
  1998. }
  1999. }
  2000. .comment-actions {
  2001. display: flex;
  2002. gap: 20px;
  2003. .comment-action-item {
  2004. display: flex;
  2005. gap: 4px;
  2006. align-items: center;
  2007. font-size: 13px;
  2008. color: #909399;
  2009. cursor: pointer;
  2010. transition: color 0.3s;
  2011. &:hover {
  2012. color: #409eff;
  2013. }
  2014. span {
  2015. font-size: 13px;
  2016. }
  2017. }
  2018. }
  2019. }
  2020. }
  2021. }
  2022. }
  2023. .comment-input-wrapper {
  2024. position: absolute;
  2025. right: 0;
  2026. bottom: 0;
  2027. left: 0;
  2028. padding: 16px 20px;
  2029. background: #ffffff;
  2030. border-top: 1px solid #e4e7ed;
  2031. box-shadow: 0 -2px 8px rgb(0 0 0 / 5%);
  2032. .reply-hint {
  2033. display: flex;
  2034. align-items: center;
  2035. justify-content: space-between;
  2036. padding: 8px 12px;
  2037. margin-bottom: 8px;
  2038. background: #f5f7fa;
  2039. border-radius: 4px;
  2040. .reply-text {
  2041. font-size: 13px;
  2042. color: #409eff;
  2043. }
  2044. .cancel-reply {
  2045. font-size: 16px;
  2046. color: #909399;
  2047. cursor: pointer;
  2048. transition: color 0.3s;
  2049. &:hover {
  2050. color: #f56c6c;
  2051. }
  2052. }
  2053. }
  2054. :deep(.el-textarea) {
  2055. margin-bottom: 12px;
  2056. }
  2057. .el-button {
  2058. width: 100%;
  2059. }
  2060. }
  2061. // 分享对话框
  2062. .share-dialog-content {
  2063. .search-box {
  2064. margin-bottom: 16px;
  2065. }
  2066. .share-friend-list {
  2067. max-height: 400px;
  2068. margin-bottom: 16px;
  2069. overflow-y: auto;
  2070. .share-friend-item {
  2071. display: flex;
  2072. align-items: center;
  2073. justify-content: space-between;
  2074. padding: 12px;
  2075. cursor: pointer;
  2076. border-radius: 8px;
  2077. transition: background-color 0.3s;
  2078. &:hover {
  2079. background-color: #f5f7fa;
  2080. }
  2081. .friend-info {
  2082. display: flex;
  2083. gap: 12px;
  2084. align-items: center;
  2085. .friend-avatar {
  2086. display: flex;
  2087. flex-shrink: 0;
  2088. align-items: center;
  2089. justify-content: center;
  2090. width: 40px;
  2091. height: 40px;
  2092. overflow: hidden;
  2093. background: #f5f5f5;
  2094. border-radius: 50%;
  2095. img {
  2096. width: 100%;
  2097. height: 100%;
  2098. object-fit: cover;
  2099. }
  2100. }
  2101. .friend-name {
  2102. font-size: 14px;
  2103. font-weight: 500;
  2104. color: #303133;
  2105. }
  2106. }
  2107. }
  2108. }
  2109. .selected-friends {
  2110. padding: 12px;
  2111. background: #f5f7fa;
  2112. border-radius: 8px;
  2113. .selected-title {
  2114. margin-bottom: 8px;
  2115. font-size: 13px;
  2116. color: #606266;
  2117. }
  2118. .selected-list {
  2119. display: flex;
  2120. flex-wrap: wrap;
  2121. gap: 8px;
  2122. .el-tag {
  2123. max-width: 120px;
  2124. overflow: hidden;
  2125. text-overflow: ellipsis;
  2126. white-space: nowrap;
  2127. }
  2128. }
  2129. }
  2130. }
  2131. }
  2132. </style>