四零语境后端代码仓库
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

460 lines
10 KiB

1 month ago
  1. <template>
  2. <div class="word-table-container">
  3. <div class="table-header">
  4. <h3>重点单词</h3>
  5. <div class="header-actions">
  6. <a-button type="primary" @click="handleAdd">
  7. <template #icon><PlusOutlined /></template>
  8. 添加单词
  9. </a-button>
  10. <a-button
  11. type="primary"
  12. danger
  13. :disabled="selectedRowKeys.length === 0"
  14. @click="handleBatchDelete"
  15. >
  16. <template #icon><DeleteOutlined /></template>
  17. 批量删除
  18. </a-button>
  19. </div>
  20. </div>
  21. <a-table
  22. :columns="columns"
  23. :data-source="dataSource"
  24. :loading="loading"
  25. :pagination="pagination"
  26. :row-selection="rowSelection"
  27. row-key="id"
  28. @change="handleTableChange"
  29. >
  30. <template #bodyCell="{ column, record }">
  31. <template v-if="column.key === 'action'">
  32. <a-space>
  33. <a-button type="link" size="small" @click="handleEdit(record)">
  34. <template #icon><EditOutlined /></template>
  35. 编辑
  36. </a-button>
  37. <a-popconfirm
  38. title="确定要删除这个单词吗?"
  39. @confirm="handleDelete(record.id)"
  40. >
  41. <a-button type="link" size="small" danger>
  42. <template #icon><DeleteOutlined /></template>
  43. 删除
  44. </a-button>
  45. </a-popconfirm>
  46. </a-space>
  47. </template>
  48. <template v-else-if="column.key === 'image'">
  49. <div class="image-cell">
  50. <img
  51. v-if="record.image"
  52. :src="record.image"
  53. :alt="record.word"
  54. class="word-image"
  55. @error="handleImageError"
  56. />
  57. <span v-else class="no-image">无图片</span>
  58. </div>
  59. </template>
  60. <template v-else-if="column.key === 'soundmark'">
  61. <span class="phonetic-text">{{ record.soundmark }}</span>
  62. </template>
  63. </template>
  64. </a-table>
  65. <!-- 添加/编辑单词弹窗 -->
  66. <a-modal
  67. v-model:open="modalVisible"
  68. :title="modalTitle"
  69. :confirm-loading="confirmLoading"
  70. @ok="handleModalOk"
  71. @cancel="handleModalCancel"
  72. width="800px"
  73. >
  74. <a-form
  75. ref="formRef"
  76. :model="formData"
  77. :rules="formRules"
  78. :label-col="{ span: 4 }"
  79. :wrapper-col="{ span: 18 }"
  80. >
  81. <a-form-item label="单词" name="word">
  82. <a-input v-model:value="formData.word" placeholder="请输入单词" />
  83. </a-form-item>
  84. <a-form-item label="图片" name="image">
  85. <JImageUpload
  86. v-model:value="formData.image"
  87. :fileMax="1"
  88. listType="picture-card"
  89. text="上传图片"
  90. bizPath="course"
  91. :accept="['image/*']"
  92. />
  93. </a-form-item>
  94. <a-form-item label="释义" name="paraphrase">
  95. <a-textarea
  96. v-model:value="formData.paraphrase"
  97. placeholder="请输入释义"
  98. :rows="3"
  99. />
  100. </a-form-item>
  101. <a-form-item label="音标" name="soundmark">
  102. <a-input v-model:value="formData.soundmark" placeholder="请输入音标,如:/ˈhæpɪ/" />
  103. </a-form-item>
  104. <a-form-item label="知识收获" name="knowledge">
  105. <a-textarea
  106. v-model:value="formData.knowledge"
  107. placeholder="请输入知识收获"
  108. :rows="4"
  109. />
  110. </a-form-item>
  111. </a-form>
  112. </a-modal>
  113. </div>
  114. </template>
  115. <script setup lang="ts">
  116. import { ref, reactive, onMounted, computed, watch, nextTick } from 'vue'
  117. import { message } from 'ant-design-vue'
  118. import { PlusOutlined, EditOutlined, DeleteOutlined } from '@ant-design/icons-vue'
  119. import { JImageUpload } from '/@/components/Form'
  120. import { list, saveOrUpdate, deleteOne, batchDelete } from './coursePageWord'
  121. interface WordRecord {
  122. id?: string
  123. word: string
  124. image?: string
  125. paraphrase: string
  126. soundmark: string
  127. knowledge: string
  128. pageId?: string
  129. }
  130. interface Props {
  131. coursePageId?: string
  132. }
  133. const props = withDefaults(defineProps<Props>(), {
  134. coursePageId: ''
  135. })
  136. // 表格列定义
  137. const columns = [
  138. {
  139. title: '单词',
  140. dataIndex: 'word',
  141. key: 'word',
  142. width: 150,
  143. },
  144. {
  145. title: '图片',
  146. dataIndex: 'image',
  147. key: 'image',
  148. width: 100,
  149. },
  150. {
  151. title: '释义',
  152. dataIndex: 'paraphrase',
  153. key: 'paraphrase',
  154. ellipsis: true,
  155. },
  156. {
  157. title: '音标',
  158. dataIndex: 'soundmark',
  159. key: 'soundmark',
  160. width: 150,
  161. },
  162. {
  163. title: '操作',
  164. key: 'action',
  165. width: 150,
  166. fixed: 'right',
  167. },
  168. ]
  169. // 响应式数据
  170. const loading = ref(false)
  171. const dataSource = ref<WordRecord[]>([])
  172. const selectedRowKeys = ref<string[]>([])
  173. const modalVisible = ref(false)
  174. const confirmLoading = ref(false)
  175. const formRef = ref()
  176. // 分页配置
  177. const pagination = reactive({
  178. current: 1,
  179. pageSize: 10,
  180. total: 0,
  181. showSizeChanger: true,
  182. showQuickJumper: true,
  183. showTotal: (total: number) => `${total} 条记录`,
  184. })
  185. // 表单数据
  186. const formData = reactive<WordRecord>({
  187. word: '',
  188. image: '',
  189. paraphrase: '',
  190. soundmark: '',
  191. knowledge: '',
  192. })
  193. // 表单验证规则
  194. const formRules = {
  195. word: [
  196. { required: true, message: '请输入单词', trigger: 'blur' },
  197. { max: 50, message: '单词长度不能超过50个字符', trigger: 'blur' },
  198. ],
  199. paraphrase: [
  200. { required: true, message: '请输入释义', trigger: 'blur' },
  201. { max: 500, message: '释义长度不能超过500个字符', trigger: 'blur' },
  202. ],
  203. soundmark: [
  204. { max: 100, message: '音标长度不能超过100个字符', trigger: 'blur' },
  205. ],
  206. knowledge: [
  207. { max: 1000, message: '知识收获长度不能超过1000个字符', trigger: 'blur' },
  208. ],
  209. }
  210. // 行选择配置
  211. const rowSelection = computed(() => ({
  212. selectedRowKeys: selectedRowKeys.value,
  213. onChange: (keys: string[]) => {
  214. selectedRowKeys.value = keys
  215. },
  216. }))
  217. // 弹窗标题
  218. const modalTitle = computed(() => {
  219. return formData.id ? '编辑单词' : '添加单词'
  220. })
  221. // 加载数据
  222. const loadData = async () => {
  223. try {
  224. loading.value = true
  225. const params = {
  226. pageNo: pagination.current,
  227. pageSize: pagination.pageSize,
  228. pageId: props.coursePageId,
  229. }
  230. const result = await list(params)
  231. dataSource.value = result.records || []
  232. pagination.total = result.total || 0
  233. } catch (error) {
  234. } finally {
  235. loading.value = false
  236. }
  237. }
  238. // 表格变化处理
  239. const handleTableChange = (pag: any) => {
  240. pagination.current = pag.current
  241. pagination.pageSize = pag.pageSize
  242. loadData()
  243. }
  244. // 添加单词
  245. const handleAdd = async () => {
  246. resetForm()
  247. modalVisible.value = true
  248. // 等待DOM更新后再次重置表单,确保表单完全清空
  249. await nextTick()
  250. formRef.value?.resetFields()
  251. }
  252. // 编辑单词
  253. const handleEdit = (record: WordRecord) => {
  254. Object.assign(formData, record)
  255. modalVisible.value = true
  256. }
  257. // 删除单词
  258. const handleDelete = async (id: string) => {
  259. try {
  260. await deleteOne({ id }, () => {
  261. message.success('删除成功')
  262. loadData()
  263. })
  264. } catch (error) {
  265. console.error('删除失败:', error)
  266. }
  267. }
  268. // 批量删除
  269. const handleBatchDelete = async () => {
  270. if (selectedRowKeys.value.length === 0) {
  271. message.warning('请选择要删除的记录')
  272. return
  273. }
  274. try {
  275. await batchDelete({ ids: selectedRowKeys.value.join(',') }, () => {
  276. message.success('批量删除成功')
  277. selectedRowKeys.value = []
  278. loadData()
  279. })
  280. } catch (error) {
  281. console.error('批量删除失败:', error)
  282. }
  283. }
  284. // 弹窗确认
  285. const handleModalOk = async () => {
  286. try {
  287. await formRef.value.validate()
  288. confirmLoading.value = true
  289. const params = {
  290. ...formData,
  291. pageId: props.coursePageId,
  292. }
  293. await saveOrUpdate(params, !!formData.id)
  294. message.success(formData.id ? '更新成功' : '添加成功')
  295. modalVisible.value = false
  296. loadData()
  297. } catch (error) {
  298. console.error('保存失败:', error)
  299. if (error.errorFields) {
  300. // 表单验证失败
  301. return
  302. }
  303. } finally {
  304. confirmLoading.value = false
  305. }
  306. }
  307. // 弹窗取消
  308. const handleModalCancel = () => {
  309. modalVisible.value = false
  310. resetForm()
  311. }
  312. // 重置表单
  313. const resetForm = () => {
  314. // 重置表单数据
  315. Object.assign(formData, {
  316. id: undefined,
  317. word: '',
  318. image: '',
  319. paraphrase: '',
  320. soundmark: '',
  321. knowledge: '',
  322. })
  323. // 重置表单验证状态
  324. if (formRef.value) {
  325. formRef.value.resetFields()
  326. formRef.value.clearValidate()
  327. }
  328. }
  329. // 组件挂载时加载数据
  330. onMounted(() => {
  331. if (props.coursePageId) {
  332. loadData()
  333. }
  334. })
  335. // 监听coursePageId变化,重新加载数据
  336. watch(() => props.coursePageId, (newId) => {
  337. if (newId) {
  338. loadData()
  339. } else {
  340. // 如果coursePageId为空,清空数据
  341. dataSource.value = []
  342. pagination.total = 0
  343. }
  344. }, { immediate: true })
  345. // 图片加载错误处理
  346. const handleImageError = (event: Event) => {
  347. const img = event.target as HTMLImageElement
  348. img.style.display = 'none'
  349. }
  350. // 暴露刷新方法给父组件
  351. defineExpose({
  352. refresh: loadData,
  353. })
  354. </script>
  355. <style scoped>
  356. .word-table-container {
  357. margin-top: 20px;
  358. background: #fff;
  359. border-radius: 6px;
  360. padding: 16px;
  361. box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
  362. }
  363. .table-header {
  364. display: flex;
  365. justify-content: space-between;
  366. align-items: center;
  367. margin-bottom: 16px;
  368. }
  369. .table-header h3 {
  370. margin: 0;
  371. color: #1890ff;
  372. font-size: 16px;
  373. font-weight: 600;
  374. }
  375. .header-actions {
  376. display: flex;
  377. gap: 8px;
  378. }
  379. .phonetic-text {
  380. font-family: 'Times New Roman', serif;
  381. color: #666;
  382. font-style: italic;
  383. }
  384. .image-cell {
  385. display: flex;
  386. align-items: center;
  387. justify-content: center;
  388. }
  389. .word-image {
  390. width: 60px;
  391. height: 60px;
  392. object-fit: cover;
  393. border-radius: 4px;
  394. border: 1px solid #d9d9d9;
  395. }
  396. .no-image {
  397. color: #999;
  398. font-size: 12px;
  399. }
  400. :deep(.ant-table-tbody > tr > td) {
  401. padding: 12px 16px;
  402. }
  403. :deep(.ant-table-thead > tr > th) {
  404. background: #fafafa;
  405. font-weight: 600;
  406. }
  407. :deep(.ant-modal-body) {
  408. padding: 24px;
  409. }
  410. :deep(.ant-form-item-label > label) {
  411. font-weight: 500;
  412. }
  413. </style>