四零语境后端代码仓库
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.
 
 
 
 
 
 

461 lines
10 KiB

<template>
<div class="word-table-container">
<div class="table-header">
<h3>重点单词</h3>
<div class="header-actions">
<a-button type="primary" @click="handleAdd">
<template #icon><PlusOutlined /></template>
添加单词
</a-button>
<a-button
type="primary"
danger
:disabled="selectedRowKeys.length === 0"
@click="handleBatchDelete"
>
<template #icon><DeleteOutlined /></template>
批量删除
</a-button>
</div>
</div>
<a-table
:columns="columns"
:data-source="dataSource"
:loading="loading"
:pagination="pagination"
:row-selection="rowSelection"
row-key="id"
@change="handleTableChange"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'action'">
<a-space>
<a-button type="link" size="small" @click="handleEdit(record)">
<template #icon><EditOutlined /></template>
编辑
</a-button>
<a-popconfirm
title="确定要删除这个单词吗?"
@confirm="handleDelete(record.id)"
>
<a-button type="link" size="small" danger>
<template #icon><DeleteOutlined /></template>
删除
</a-button>
</a-popconfirm>
</a-space>
</template>
<template v-else-if="column.key === 'image'">
<div class="image-cell">
<img
v-if="record.image"
:src="record.image"
:alt="record.word"
class="word-image"
@error="handleImageError"
/>
<span v-else class="no-image">无图片</span>
</div>
</template>
<template v-else-if="column.key === 'soundmark'">
<span class="phonetic-text">{{ record.soundmark }}</span>
</template>
</template>
</a-table>
<!-- 添加/编辑单词弹窗 -->
<a-modal
v-model:open="modalVisible"
:title="modalTitle"
:confirm-loading="confirmLoading"
@ok="handleModalOk"
@cancel="handleModalCancel"
width="800px"
>
<a-form
ref="formRef"
:model="formData"
:rules="formRules"
:label-col="{ span: 4 }"
:wrapper-col="{ span: 18 }"
>
<a-form-item label="单词" name="word">
<a-input v-model:value="formData.word" placeholder="请输入单词" />
</a-form-item>
<a-form-item label="图片" name="image">
<JImageUpload
v-model:value="formData.image"
:fileMax="1"
listType="picture-card"
text="上传图片"
bizPath="course"
:accept="['image/*']"
/>
</a-form-item>
<a-form-item label="释义" name="paraphrase">
<a-textarea
v-model:value="formData.paraphrase"
placeholder="请输入释义"
:rows="3"
/>
</a-form-item>
<a-form-item label="音标" name="soundmark">
<a-input v-model:value="formData.soundmark" placeholder="请输入音标,如:/ˈhæpɪ/" />
</a-form-item>
<a-form-item label="知识收获" name="knowledge">
<a-textarea
v-model:value="formData.knowledge"
placeholder="请输入知识收获"
:rows="4"
/>
</a-form-item>
</a-form>
</a-modal>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted, computed, watch, nextTick } from 'vue'
import { message } from 'ant-design-vue'
import { PlusOutlined, EditOutlined, DeleteOutlined } from '@ant-design/icons-vue'
import { JImageUpload } from '/@/components/Form'
import { list, saveOrUpdate, deleteOne, batchDelete } from './coursePageWord'
interface WordRecord {
id?: string
word: string
image?: string
paraphrase: string
soundmark: string
knowledge: string
pageId?: string
}
interface Props {
coursePageId?: string
}
const props = withDefaults(defineProps<Props>(), {
coursePageId: ''
})
// 表格列定义
const columns = [
{
title: '单词',
dataIndex: 'word',
key: 'word',
width: 150,
},
{
title: '图片',
dataIndex: 'image',
key: 'image',
width: 100,
},
{
title: '释义',
dataIndex: 'paraphrase',
key: 'paraphrase',
ellipsis: true,
},
{
title: '音标',
dataIndex: 'soundmark',
key: 'soundmark',
width: 150,
},
{
title: '操作',
key: 'action',
width: 150,
fixed: 'right',
},
]
// 响应式数据
const loading = ref(false)
const dataSource = ref<WordRecord[]>([])
const selectedRowKeys = ref<string[]>([])
const modalVisible = ref(false)
const confirmLoading = ref(false)
const formRef = ref()
// 分页配置
const pagination = reactive({
current: 1,
pageSize: 10,
total: 0,
showSizeChanger: true,
showQuickJumper: true,
showTotal: (total: number) => `${total} 条记录`,
})
// 表单数据
const formData = reactive<WordRecord>({
word: '',
image: '',
paraphrase: '',
soundmark: '',
knowledge: '',
})
// 表单验证规则
const formRules = {
word: [
{ required: true, message: '请输入单词', trigger: 'blur' },
{ max: 50, message: '单词长度不能超过50个字符', trigger: 'blur' },
],
paraphrase: [
{ required: true, message: '请输入释义', trigger: 'blur' },
{ max: 500, message: '释义长度不能超过500个字符', trigger: 'blur' },
],
soundmark: [
{ max: 100, message: '音标长度不能超过100个字符', trigger: 'blur' },
],
knowledge: [
{ max: 1000, message: '知识收获长度不能超过1000个字符', trigger: 'blur' },
],
}
// 行选择配置
const rowSelection = computed(() => ({
selectedRowKeys: selectedRowKeys.value,
onChange: (keys: string[]) => {
selectedRowKeys.value = keys
},
}))
// 弹窗标题
const modalTitle = computed(() => {
return formData.id ? '编辑单词' : '添加单词'
})
// 加载数据
const loadData = async () => {
try {
loading.value = true
const params = {
pageNo: pagination.current,
pageSize: pagination.pageSize,
pageId: props.coursePageId,
}
const result = await list(params)
dataSource.value = result.records || []
pagination.total = result.total || 0
} catch (error) {
} finally {
loading.value = false
}
}
// 表格变化处理
const handleTableChange = (pag: any) => {
pagination.current = pag.current
pagination.pageSize = pag.pageSize
loadData()
}
// 添加单词
const handleAdd = async () => {
resetForm()
modalVisible.value = true
// 等待DOM更新后再次重置表单,确保表单完全清空
await nextTick()
formRef.value?.resetFields()
}
// 编辑单词
const handleEdit = (record: WordRecord) => {
Object.assign(formData, record)
modalVisible.value = true
}
// 删除单词
const handleDelete = async (id: string) => {
try {
await deleteOne({ id }, () => {
message.success('删除成功')
loadData()
})
} catch (error) {
console.error('删除失败:', error)
}
}
// 批量删除
const handleBatchDelete = async () => {
if (selectedRowKeys.value.length === 0) {
message.warning('请选择要删除的记录')
return
}
try {
await batchDelete({ ids: selectedRowKeys.value.join(',') }, () => {
message.success('批量删除成功')
selectedRowKeys.value = []
loadData()
})
} catch (error) {
console.error('批量删除失败:', error)
}
}
// 弹窗确认
const handleModalOk = async () => {
try {
await formRef.value.validate()
confirmLoading.value = true
const params = {
...formData,
pageId: props.coursePageId,
}
await saveOrUpdate(params, !!formData.id)
message.success(formData.id ? '更新成功' : '添加成功')
modalVisible.value = false
loadData()
} catch (error) {
console.error('保存失败:', error)
if (error.errorFields) {
// 表单验证失败
return
}
} finally {
confirmLoading.value = false
}
}
// 弹窗取消
const handleModalCancel = () => {
modalVisible.value = false
resetForm()
}
// 重置表单
const resetForm = () => {
// 重置表单数据
Object.assign(formData, {
id: undefined,
word: '',
image: '',
paraphrase: '',
soundmark: '',
knowledge: '',
})
// 重置表单验证状态
if (formRef.value) {
formRef.value.resetFields()
formRef.value.clearValidate()
}
}
// 组件挂载时加载数据
onMounted(() => {
if (props.coursePageId) {
loadData()
}
})
// 监听coursePageId变化,重新加载数据
watch(() => props.coursePageId, (newId) => {
if (newId) {
loadData()
} else {
// 如果coursePageId为空,清空数据
dataSource.value = []
pagination.total = 0
}
}, { immediate: true })
// 图片加载错误处理
const handleImageError = (event: Event) => {
const img = event.target as HTMLImageElement
img.style.display = 'none'
}
// 暴露刷新方法给父组件
defineExpose({
refresh: loadData,
})
</script>
<style scoped>
.word-table-container {
margin-top: 20px;
background: #fff;
border-radius: 6px;
padding: 16px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.table-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
}
.table-header h3 {
margin: 0;
color: #1890ff;
font-size: 16px;
font-weight: 600;
}
.header-actions {
display: flex;
gap: 8px;
}
.phonetic-text {
font-family: 'Times New Roman', serif;
color: #666;
font-style: italic;
}
.image-cell {
display: flex;
align-items: center;
justify-content: center;
}
.word-image {
width: 60px;
height: 60px;
object-fit: cover;
border-radius: 4px;
border: 1px solid #d9d9d9;
}
.no-image {
color: #999;
font-size: 12px;
}
:deep(.ant-table-tbody > tr > td) {
padding: 12px 16px;
}
:deep(.ant-table-thead > tr > th) {
background: #fafafa;
font-weight: 600;
}
:deep(.ant-modal-body) {
padding: 24px;
}
:deep(.ant-form-item-label > label) {
font-weight: 500;
}
</style>