Reply.vue 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380
  1. <template>
  2. <div class="reply-container">
  3. <!-- 搜索表单 -->
  4. <div class="mb10">
  5. <SearchForm :searchFields="searchItems" :maxFirstRowItems="3" @search="handleSearch" @reset="handleReset" />
  6. </div>
  7. <!-- 操作按钮 -->
  8. <div class="mb10">
  9. <div class="table-operations">
  10. <a-button type="primary" danger :disabled="!hasSelected" @click="handleBatchDelete">
  11. 批量删除
  12. </a-button>
  13. <span style="margin-left: 8px" v-if="hasSelected">
  14. 已选择 {{ selectedRowKeys.length }} 项
  15. </span>
  16. </div>
  17. </div>
  18. <!-- 数据列表 -->
  19. <div class="card-wrap">
  20. <DataList ref="dataListRef" :api="(params) => commentApi.getListByPage(params)" :deleteApi="commentApi.deleteComment"
  21. :columns="columns" :rowSelection="rowSelection" @action="onAction">
  22. <template #content="{ record }">
  23. <div class="content-cell">
  24. {{ record.content.length > 50 ? record.content.substring(0, 50) + '...' : record.content }}
  25. </div>
  26. </template>
  27. <template #user="{ record }">
  28. <div v-if="record.user" class="flex items-center space-x-2" style="display: flex;align-items: center;">
  29. <a-avatar :size="32" :src="record.user.avatar ? getImageUrl(record.user.avatar) : undefined">
  30. {{ record.user.nickname ? record.user.nickname.charAt(0) : (record.user.username ?
  31. record.user.username.charAt(0) : 'U') }}
  32. </a-avatar>
  33. <div style="margin-left: 5px;">{{ record.user.nickname || record.user.username }}</div>
  34. </div>
  35. <div v-else class="text-gray-400">用户已删除</div>
  36. </template>
  37. <template #post="{ record }">
  38. <div v-if="record.post">
  39. <div class="max-w-xs truncate" :title="record.post.content">{{ record.post.content || '无内容' }}
  40. </div>
  41. </div>
  42. <div v-else class="text-gray-400">帖子已删除</div>
  43. </template>
  44. <template #status="{ record }">
  45. <a-tag :color="record.status === 1 ? 'green' : 'red'">
  46. {{ record.status === 1 ? '正常' : '禁用' }}
  47. </a-tag>
  48. </template>
  49. <template #created_at="{ record }">
  50. {{ formatDate(record.created_at * 1000) }}
  51. </template>
  52. </DataList>
  53. </div>
  54. <!-- 评论详情弹窗 -->
  55. <a-modal v-model:open="detailModalVisible" title="评论详情" :footer="null" width="600px">
  56. <div v-if="currentComment">
  57. <a-descriptions :column="1" bordered>
  58. <a-descriptions-item label="评论内容">
  59. <div class="whitespace-pre-wrap">{{ currentComment.content }}</div>
  60. </a-descriptions-item>
  61. <a-descriptions-item label="评论用户">
  62. <div v-if="currentComment.user">
  63. {{ currentComment.user.nickname || currentComment.user.username }}
  64. <span class="text-gray-500">(ID: {{ currentComment.user.id }})</span>
  65. </div>
  66. <span v-else class="text-gray-400">用户已删除</span>
  67. </a-descriptions-item>
  68. <a-descriptions-item label="所属帖子">
  69. <div v-if="currentComment.post">
  70. <div class="whitespace-pre-wrap">{{ currentComment.post.content || '无内容' }}</div>
  71. <span class="text-gray-500">(ID: {{ currentComment.post.id }})</span>
  72. </div>
  73. <span v-else class="text-gray-400">帖子已删除</span>
  74. </a-descriptions-item>
  75. <a-descriptions-item label="回复用户" v-if="currentComment.reply_user">
  76. <div v-if="currentComment.replyUser">
  77. {{ currentComment.replyUser.nickname || currentComment.replyUser.username }}
  78. <span class="text-gray-500">(ID: {{ currentComment.replyUser.id }})</span>
  79. </div>
  80. <span v-else class="text-gray-400">用户已删除</span>
  81. </a-descriptions-item>
  82. <a-descriptions-item label="类型">
  83. <a-tag :color="currentComment.type === 1 ? 'blue' : 'orange'">
  84. {{ currentComment.type === 1 ? '评论' : '回复' }}
  85. </a-tag>
  86. </a-descriptions-item>
  87. <a-descriptions-item label="状态">
  88. <a-tag :color="currentComment.status === 1 ? 'green' : 'red'">
  89. {{ currentComment.status === 1 ? '正常' : '禁用' }}
  90. </a-tag>
  91. </a-descriptions-item>
  92. <a-descriptions-item label="点赞数">{{ currentComment.thumb_num || 0 }}</a-descriptions-item>
  93. <a-descriptions-item label="创建时间">{{ currentComment.created_at }}</a-descriptions-item>
  94. <a-descriptions-item label="更新时间">{{ currentComment.updated_at }}</a-descriptions-item>
  95. </a-descriptions>
  96. </div>
  97. </a-modal>
  98. <!-- 编辑评论弹窗 -->
  99. <a-modal v-model:open="editModalVisible" title="编辑评论" @ok="handleSave"
  100. @cancel="handleCancel" width="600px">
  101. <a-form :model="editForm" :label-col="{ span: 4 }" :wrapper-col="{ span: 20 }">
  102. <a-form-item label="评论内容" :rules="[{ required: true, message: '请输入评论内容' }]">
  103. <a-textarea v-model:value="editForm.content" :rows="4" placeholder="请输入评论内容" />
  104. </a-form-item>
  105. <a-form-item label="状态">
  106. <a-select v-model:value="editForm.status" placeholder="请选择状态">
  107. <a-select-option :value="1">正常</a-select-option>
  108. <a-select-option :value="0">禁用</a-select-option>
  109. </a-select>
  110. </a-form-item>
  111. </a-form>
  112. </a-modal>
  113. </div>
  114. </template>
  115. <script setup>
  116. import { notifyError } from '@/utils/request.js'
  117. import { ref, reactive, computed } from 'vue'
  118. import { message, Modal } from 'ant-design-vue'
  119. import DataList from '@/components/DataList.vue'
  120. import SearchForm from '@/components/SearchForm.vue'
  121. import * as commentApi from '@/api/comment'
  122. import { getImageUrl } from '@/utils/utils'
  123. import { formatDate } from '@/utils/date'
  124. // 响应式数据
  125. const dataListRef = ref()
  126. const selectedRowKeys = ref([])
  127. const detailModalVisible = ref(false)
  128. const editModalVisible = ref(false)
  129. const currentComment = ref(null)
  130. // 搜索表单配置
  131. const searchItems = ref([
  132. {
  133. label: '评论内容',
  134. prop: 'content',
  135. type: 'input',
  136. placeholder: '请输入评论内容'
  137. },
  138. {
  139. label: '状态',
  140. prop: 'status',
  141. type: 'select',
  142. placeholder: '请选择状态',
  143. options: [
  144. { label: '正常', value: 1 },
  145. { label: '禁用', value: 0 }
  146. ]
  147. },
  148. {
  149. label: '创建时间',
  150. prop: 'date_range',
  151. type: 'daterange',
  152. placeholder: ['开始日期', '结束日期']
  153. }
  154. ])
  155. // 编辑表单
  156. const editForm = reactive({
  157. id: null,
  158. content: '',
  159. status: 1
  160. })
  161. // 计算属性
  162. const hasSelected = computed(() => selectedRowKeys.value.length > 0)
  163. // 行选择配置
  164. const rowSelection = {
  165. selectedRowKeys: selectedRowKeys,
  166. onChange: (keys) => {
  167. selectedRowKeys.value = keys
  168. }
  169. }
  170. // 表格列配置
  171. const columns = [
  172. {
  173. title: '评论内容',
  174. dataIndex: 'content',
  175. width: 200,
  176. slot: true
  177. },
  178. {
  179. title: '用户',
  180. dataIndex: 'user',
  181. width: 150,
  182. slot: true
  183. },
  184. {
  185. title: '帖子',
  186. dataIndex: 'post',
  187. width: 200,
  188. slot: true
  189. },
  190. {
  191. title: '状态',
  192. dataIndex: 'status',
  193. width: 100,
  194. slot: true
  195. },
  196. {
  197. title: '点赞数',
  198. dataIndex: 'thumb_num',
  199. width: 80
  200. },
  201. {
  202. title: '创建时间',
  203. dataIndex: 'created_at',
  204. width: 180,
  205. slot: true,
  206. sorter: true
  207. },
  208. {
  209. title: '操作',
  210. key: 'action',
  211. width: 200,
  212. type: 'action',
  213. options: [
  214. { title: '查看', key: 'view' },
  215. { title: '编辑', key: 'edit' },
  216. { title: '删除', key: 'delete', type: 'delete' }
  217. ]
  218. }
  219. ]
  220. // 搜索处理
  221. const handleSearch = (searchParams) => {
  222. dataListRef.value.search(searchParams)
  223. }
  224. // 重置处理
  225. const handleReset = () => {
  226. dataListRef.value.reset()
  227. }
  228. // DataList操作处理
  229. const onAction = (action, record) => {
  230. if (action.key === 'view') {
  231. showDetailModal(record)
  232. } else if (action.key === 'edit') {
  233. showEditModal(record)
  234. }
  235. }
  236. // 显示详情弹窗
  237. const showDetailModal = (record) => {
  238. currentComment.value = record
  239. detailModalVisible.value = true
  240. }
  241. // 显示编辑弹窗
  242. const showEditModal = (record) => {
  243. Object.assign(editForm, {
  244. id: record.id,
  245. content: record.content,
  246. status: record.status
  247. })
  248. editModalVisible.value = true
  249. }
  250. // 保存
  251. const handleSave = async () => {
  252. try {
  253. await commentApi.updateComment(editForm)
  254. message.success('更新评论成功')
  255. editModalVisible.value = false
  256. dataListRef.value.refreshData()
  257. } catch (error) {
  258. notifyError(error, '更新评论失败')
  259. }
  260. }
  261. // 取消
  262. const handleCancel = () => {
  263. editModalVisible.value = false
  264. }
  265. // 切换状态
  266. const toggleStatus = async (record) => {
  267. try {
  268. const newStatus = record.status === 1 ? 0 : 1
  269. await commentApi.updateCommentStatus({ id: record.id, status: newStatus })
  270. message.success(newStatus === 1 ? '启用成功' : '禁用成功')
  271. dataListRef.value.refreshData()
  272. } catch (error) {
  273. notifyError(error, '状态更新失败')
  274. }
  275. }
  276. // 删除单个
  277. const handleDelete = (record) => {
  278. Modal.confirm({
  279. title: '确认删除',
  280. content: '确定要删除这条评论吗?',
  281. onOk: async () => {
  282. try {
  283. await commentApi.deleteComment({ id: record.id })
  284. message.success('删除成功')
  285. dataListRef.value.refreshData()
  286. } catch (error) {
  287. notifyError(error, '删除失败')
  288. }
  289. }
  290. })
  291. }
  292. // 批量删除
  293. const handleBatchDelete = () => {
  294. Modal.confirm({
  295. title: '确认批量删除',
  296. content: `确定要删除选中的 ${selectedRowKeys.value.length} 条评论吗?`,
  297. onOk: async () => {
  298. try {
  299. await commentApi.batchDeleteComments({ ids: selectedRowKeys.value })
  300. message.success('批量删除成功')
  301. selectedRowKeys.value = []
  302. dataListRef.value.refreshData()
  303. } catch (error) {
  304. notifyError(error, '批量删除失败')
  305. }
  306. }
  307. })
  308. }
  309. </script>
  310. <style scoped>
  311. .reply-container {
  312. padding: 20px;
  313. }
  314. .search-form {
  315. background: #fff;
  316. padding: 20px;
  317. margin-bottom: 20px;
  318. border-radius: 6px;
  319. box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
  320. }
  321. .operation-buttons {
  322. margin-bottom: 20px;
  323. }
  324. .operation-buttons .ant-btn {
  325. margin-right: 8px;
  326. }
  327. .content-cell {
  328. max-width: 200px;
  329. word-break: break-all;
  330. }
  331. .text-gray-500 {
  332. color: #6b7280;
  333. }
  334. .text-gray-400 {
  335. color: #9ca3af;
  336. }
  337. .text-xs {
  338. font-size: 0.75rem;
  339. }
  340. .whitespace-pre-wrap {
  341. white-space: pre-wrap;
  342. }
  343. </style>