<a-table :row-selection="rowSelection" :columns="computedColumns" :customRow="handleCheck" :data-source="list" :pagination="pagination" @change="tableChange" >找不到key是在哪里设置的
时间: 2025-08-01 15:33:48 AIGC 浏览: 22
<think>我们正在讨论的是在Vue或React中使用AntDesign的Table组件(a-table)时如何正确设置key属性。关键点:1.在AntDesign中,表格的每一行需要一个唯一的key来标识。2.在React版本的AntDesignTable中,我们通常使用`rowKey`属性来指定数据源中作为唯一标识的字段名,或者一个函数来生成唯一key。3.在Vue版本的AntDesignTable中,同样使用`rowKey`属性(在Vue中可能是`:rowKey`)来指定。根据引用[1]中提到的ali-react-table(一个高性能的React表格组件)也强调了性能,其中虚拟滚动依赖于正确的key设置。引用[2]展示了Vue中a-table的使用,其中提到了column.key,但这里我们需要的是行的key,不是列的key。引用[3]展示了React中AntDesignTable的使用,其中在展开行时使用了`defaultExpandedRowKeys`,它需要一个key的数组,这个key就是行的key。在示例中,他们使用`pageList.map(r=>r.key)`,说明数据源中有一个key字段。引用[4]则展示了Vue中columns的定义,其中每列通过dataIndex来指定数据字段,但没有直接涉及行key。因此,我们重点讨论如何设置行的key。在ReactAntDesignTable中:-通过`rowKey`属性设置,可以是字符串(指定数据中的字段名)或函数(如:`rowKey={record=>record.id}`)。-如果没有设置`rowKey`,Table会默认使用数据源的索引(index)作为key,但这在数据排序、过滤、分页等操作时可能会导致问题,因为索引会变。在VueAntDesignTable中:-同样通过`rowKey`属性设置,可以是字符串或函数。在Vue中,我们通常使用绑定属性:`:rowKey="(record)=>record.id"`或`rowKey="id"`。注意事项:-key必须是唯一的,否则会导致渲染错误。-如果数据中没有唯一标识,可以考虑使用函数生成一个唯一值(例如,使用数据中的多个字段组合,或者使用第三方库生成唯一ID),但通常建议后端提供唯一标识。下面分别给出React和Vue的示例:###ReactAntDesignTable设置rowKey```jsximport{Table}from'antd';constdataSource=[{id:'1',//唯一标识name:'JohnBrown',age:32,},{id:'2',name:'JimGreen',age:42,},];constcolumns=[{title:'Name',dataIndex:'name',key:'name',},{title:'Age',dataIndex:'age',key:'age',},];//使用字符串指定key字段<TabledataSource={dataSource}columns={columns}rowKey="id"/>//或者使用函数<TabledataSource={dataSource}columns={columns}rowKey={record=>record.id}/>```###VueAntDesignTable设置rowKey```vue<template><a-table:data-source="dataSource":columns="columns":rowKey="record=>record.id"></a-table><!--或者--><a-table:data-source="dataSource":columns="columns"rowKey="id"></a-table></template><script>exportdefault{data(){return{dataSource:[{id:'1',name:'JohnBrown',age:32,},{id:'2',name:'JimGreen',age:42,},],columns:[{title:'Name',dataIndex:'name',key:'name',},{title:'Age',dataIndex:'age',key:'age',},],};},};</script>```###常见问题及解决1.**为什么设置了rowKey,但是选择行、展开行等功能仍然有问题?**-确保rowKey指定的字段在每条数据中都是唯一的,且不会变化。如果数据中有重复的id,或者id在数据更新时发生变化,会导致问题。-在动态数据(比如从后端获取)中,确保数据加载完成后再渲染表格(如引用[3]中,他们通过判断数据长度来条件渲染表格)。2.**数据中没有唯一字段怎么办?**-如果数据中确实没有唯一字段,可以考虑使用函数生成一个唯一key,例如:```jsx//React<TablerowKey={record=>`${record.field1}-${record.field2}`}/>``````vue<!--Vue--><a-table:rowKey="record=>`${record.field1}-${record.field2}`"/>```-或者使用索引(不推荐,因为索引在数据变动时会变):```jsx<TablerowKey={(record,index)=>index}/>//注意:当数据会变化(排序、过滤等)时,这样会导致问题```3.**在Vue中,使用rowKey属性时,如果直接写字符串(如`rowKey="id"`)和写函数(`:rowKey="record=>record.id"`)有什么区别?**-在Vue中,`rowKey="id"`是字符串属性,表格内部会将其解释为从记录中获取id字段作为key。-`:rowKey="record=>record.id"`是传递一个函数,效果相同,但更灵活(比如可以组合多个字段)。4.**使用虚拟滚动时,key的重要性**-如引用[1]所述,虚拟滚动需要正确的key来跟踪行位置,如果key不稳定,会导致滚动时出现错乱。总结:设置key是使用AntDesignTable组件的重要步骤,确保每一行有一个稳定且唯一的标识,这样表格的各种功能(选择、展开、排序等)才能正常工作。相关问题:1.在AntDesignTable中,如何实现行的单选模式?2.如何动态更新AntDesignTable的数据源并保持选中行的状态?3.AntDesignTable中如何根据行数据禁用某些行的选择?4.在AntDesignTable中同时使用行选择和行展开时需要注意什么?5.如何解决AntDesignTable在动态加载数据时展开行不生效的问题?</think>### Ant Design Table 中 Key 属性设置指南
在 Vue 和 React 中使用 Ant Design Table 组件时,正确设置 `key` 属性至关重要,它直接关系到表格性能、行选择、展开等功能的正确性。
#### 一、核心概念
1. **`rowKey` 的作用**:
- 唯一标识表格中的每一行数据
- 用于行选择(`rowSelection`)、行展开(`expandable`)等功能的关联
- 优化虚拟滚动性能(引用[1]中提到的万级数据处理依赖此机制)
2. **与普通 `key` 的区别**:
- 普通 `key`:React/Vue 列表渲染的通用优化属性
- `rowKey`:Ant Design Table 特有的行标识配置
#### 二、Vue 版本设置方式
```vue
<template>
<a-table
:data-source="data"
:rowKey="rowKeyResolver" // ✨ 关键设置
:row-selection="{ selectedRowKeys }"
:expandable="{ defaultExpandedRowKeys }"
>
<!-- 列定义 -->
</a-table>
</template>
<script>
export default {
data() {
return {
// 数据源必须包含唯一标识字段
data: [
{ id: 'user1', name: '张三', age: 28 },
{ id: 'user2', name: '李四', age: 32 }
],
// 行选择绑定值
selectedRowKeys: [],
// 行展开绑定值
defaultExpandedRowKeys: ['user1']
}
},
methods: {
// 推荐方式:函数指定 key
rowKeyResolver(record) {
return record.id; // 使用唯一字段
},
// 备选方式:字符串指定 key 字段名
// rowKey: 'id'
}
}
</script>
```
#### 三、React 版本设置方式
```jsx
import { Table } from 'antd';
function App() {
const data = [
{ userId: 'U001', name: 'John' },
{ userId: 'U002', name: 'Jane' }
];
return (
<Table
dataSource={data}
rowKey={record => record.userId} // ✨ 关键设置
rowSelection={{
selectedRowKeys,
onChange: setSelectedRowKeys
}}
expandable={{
defaultExpandedRowKeys: ['U001']
}}
/>
);
}
```
#### 四、配置规则与最佳实践
| **场景** | **推荐写法** | **注意事项** |
|----------|--------------|--------------|
| 数据有唯一ID | `rowKey="id"` | 确保字段值唯一 |
| 复合主键 | `:rowKey="r => `${r.id}-${r.dept}`"` | 避免冲突 |
| 无唯一字段 | `rowKey={(r,i) => `row-${i}`}` | 最后手段(性能较差) |
| 动态数据 | 数据更新后重置选择状态 | 引用[3]的解决方案 |
| 服务端分页 | 使用稳定ID(非索引) | 跨页选择必需 |
#### 五、常见问题解决
1. **行选择绑定值异常**:
```jsx
// React 错误:绑定到索引值
// ❌ 错误:selectedRowKeys 会变成 [0, 1] 而不是 ['U001','U002']
<Table rowKey={(record, index) => index} ... />
// ✅ 正确:绑定到稳定ID
<Table rowKey="userId" ... />
```
2. **行展开不生效**(引用[3]问题):
```jsx
// 确保数据加载完成后再设置展开(引用[3]方案)
{data.length > 0 && (
<Table
expandable={{
defaultExpandedRowKeys: data.map(item => item.key)
}}
/>
)}
```
3. **虚拟滚动性能优化**(引用[1]):
- 必须设置 `rowKey` 才能启用虚拟滚动
- 确保 key 值是稳定不变的(避免使用随机数)
#### 六、调试技巧
1. 检查 key 值唯一性:
```javascript
// Vue
mounted() {
const keys = this.data.map(item => this.rowKeyResolver(item));
const uniqueKeys = new Set(keys);
if(keys.length !== uniqueKeys.size) {
console.error('存在重复的rowKey值');
}
}
```
2. 监控选择状态变化:
```jsx
// React
const onSelectChange = (selectedKeys, selectedRows) => {
console.log('Selected Keys:', selectedKeys);
setSelectedRowKeys(selectedKeys);
};
```
> **关键总结**:`rowKey` 是 Ant Design Table 的核心配置,必须确保:
> 1. 使用数据中的**稳定唯一标识**
> 2. 避免使用数组索引(分页/排序时会失效)
> 3. 在 Vue 和 React 中的配置方式类似(函数或字符串)
> 4. 正确设置可保证行选择/展开/虚拟滚动等功能正常工作[^1][^3]
---
### 相关问题
1. Ant Design Table 如何实现跨页保持选中状态?
2. 虚拟滚动开启条件是什么?如何优化万级数据表格性能?
3. 动态更新数据源时如何避免行展开状态丢失?
4. 如何在自定义列渲染中获取当前行的 key 值?
5. 复合主键场景下如何确保 rowKey 的唯一性?
阅读全文
相关推荐




<template> <a-card :bordered="false" style="margin-bottom: 10px;"> </a-card> <a-card :bordered="false" > <bt-inventory-add-form v-if="showAddModal" ref="btInventoryAddForm" :contractId="contractId" @ok="getList" @close="showAddModal = false" /> <bt-inventory-edit-form v-if="showEditModal" ref="btInventoryEditForm" :contractId="contractId" @ok="getList" @close="showEditModal = false" /> <a-tooltip title="刷新"> <a-icon @click="getList" class="action" :type="loading ? 'loading' : 'reload'" /> </a-tooltip> <a-table :loading="loading" rowKey="id" size="middle" @change="handleTableChange" @refresh="getList" :columns="columns" :data-source="btInventoryList" :row-selection="{ selectedRowKeys: selectedRowKeys, onChange: onSelectChange }" :pagination="false" > </a-table> </a-card> 暂无相关库存数据 <a-tag v-if="isOutbound" color="green" style="margin-left:8px;">已出库</a-tag> </template> <script> import { listBtInventory, delBtInventory, exportBtInventory } from '@/api/tt/Inventory' import AdvanceTable from '@/components/pt/table/AdvanceTable' import BtInventoryAddForm from '@/views/bt/btoutbound/modules/BtInventoryAddForm' import BtInventoryEditForm from '@/views/bt/btoutbound/modules/BtInventoryEditForm' export default { name: 'BtInventory', props: { isOutbound: { type: Boolean, default: false, // 默认不显示“已出库” }, contractId: { type: String, required: true }, title: { type: String, default: '子表' } },watch: { hasData: { immediate: true, handler(newVal) { if (!newVal && !this.isOutbound) { this.$emit('outbound-status', true); this.isOutbound = true; } } } }, components: { AdvanceTable, BtInventoryAddForm, BtInventoryEditForm }, data () { return { hasData:false, showAddModal: false, showEditModal: false, // 遮罩层 loading: true, // 选中数组 ids: [], // 选中的主键集合 selectedRowKeys: [], // 选中的数据集合 selectedRows: [], // 高级搜索 展开/关闭 advanced: false, // 非单个禁用 single: true, // 非多个禁用 multiple: true, // 总条数 total: 0, // label的百分比 labelCol: { span: 6 }, // 内容区域的百分比 wrapperCol: { span: 18 }, // 合同管理表格数据 btInventoryList: [], // 查询参数 queryParam: { pageNum: 1, pageSize: 1000, itemName: undefined, description: undefined, quantity: undefined, beginIndate: undefined }, columns: [ { title: '物资名称', dataIndex: 'itemName', ellipsis: true, align: 'left', width: '12.8%' }, { title: '规格型号', dataIndex: 'description', ellipsis: true, align: 'left', width: '12.8%' }, { title: '总库存', dataIndex: 'fulfilledQuantity', align: 'right', width: '12.8%' }, { title: '当前库存数量', dataIndex: 'quantity', align: 'right', width: '12.8%' }, { title: '入库日期', dataIndex: 'indate', align: 'center', width: '12.8%' }, // { // title: '操作', // dataIndex: 'operation', // align: 'center', // width: '10%', // scopedSlots: { customRender: 'operation' } // } ] } }, created () { this.getList() }, methods: { /** 查询合同管理列表 */ getList () { this.loading = true this.queryParam.contractId = this.contractId listBtInventory(this.queryParam).then(response => { this.btInventoryList = response.data.list this.total = response.data.total this.loading = false if(this.btInventoryList.length > 0){ this.hasData = true }else{ this.hasData = false } }) }, /** 搜索按钮操作 */ handleQuery () { this.queryParam.pageNum = 1 this.getList() }, /** 重置按钮操作 */ resetQuery () { this.queryParam = { pageNum: 1, pageSize: 1000, itemName: undefined, description: undefined, quantity: undefined, beginIndate: undefined } this.handleQuery() }, /** 翻页操作 */ onShowSizeChange (current, pageSize) { this.queryParam.pageSize = pageSize this.getList() }, /** 翻页操作 */ onSizeChange (current, size) { this.queryParam.pageNum = 1 this.queryParam.pageSize = size this.getList() }, /** 翻页操作 */ changeSize (current, pageSize) { this.queryParam.pageNum = current this.queryParam.pageSize = pageSize this.getList() }, /** 翻页操作 */ onSelectChange (selectedRowKeys, selectedRows) { this.selectedRowKeys = selectedRowKeys this.selectedRows = selectedRows this.ids = this.selectedRows.map(item => item.id) this.single = selectedRowKeys.length !== 1 this.multiple = !selectedRowKeys.length }, /** 查询折叠和展开操作 */ toggleAdvanced () { this.advanced = !this.advanced }, handleAdd () { this.showAddModal = true this.$nextTick(() => ( this.$refs.btInventoryAddForm.handleAdd() )) }, handleUpdate (record, ids) { this.showEditModal = true this.$nextTick(() => ( this.$refs.btInventoryEditForm.handleUpdate(record, ids) )) }, /** 删除按钮操作 */ handleDelete (row) { var that = this const btInventoryIds = row.id || this.ids this.$confirm({ title: '确认删除所选中数据?', onOk () { return delBtInventory(btInventoryIds) .then(() => { that.onSelectChange([], []) that.getList() that.$message.success( '删除成功', 3 ) }) }, onCancel () {} }) }, /** 导出按钮操作 */ handleExport () { var that = this this.$confirm({ title: '是否确认导出?', content: '此操作将导出当前条件下所有数据而非选中数据', onOk () { return exportBtInventory(that.queryParam) .then(response => { that.download(response.msg) that.$message.success( '导出成功', 3 ) }) }, onCancel () {} }) }, handleTableChange (pagination, filters, sorter) { if (sorter.field !== undefined && sorter.field !== null && sorter.field !== '') { this.queryParam.orderByColumn = 'a.' + sorter.field this.queryParam.isAsc = sorter.order } this.getList() } } } </script> <style scoped> .empty-container { padding: 20px; text-align: center; background: #fff; border-radius: 4px; } .action { font-size: 16px; cursor: pointer; margin-left: 8px; } .action:hover { color: #1890ff; } </style><template> <a-card :bordered="false" style="margin-bottom: 10px;"> <a-form :labelCol="labelCol" :wrapperCol="wrapperCol" ref="queryForm"> <a-row :gutter="32"> <a-col :span="6" > <a-form-item label="合同名称"> <a-input v-model="queryParam.contractName" placeholder="请输入合同名称" allow-clear @keyup.enter.native="handleQuery"/> </a-form-item> </a-col> <a-col :span="6" > <a-form-item label="合同正式编号"> <a-input v-model="queryParam.contractId" placeholder="请输入合同正式编号" allow-clear @keyup.enter.native="handleQuery"/> </a-form-item> </a-col> <a-col :span="6" > <a-form-item label="对方签约单位"> <a-input v-model="queryParam.counterpartContractUnit" placeholder="请输入对方签约单位" allow-clear @keyup.enter.native="handleQuery"/> </a-form-item> </a-col> <a-col> <a-button type="primary" @click="handleQuery"><a-icon type="search" />查询</a-button> <a-button style="margin-left: 8px" @click="resetQuery"><a-icon type="redo" />重置</a-button> {{ advanced ? '收起' : '展开' }} <a-icon :type="advanced ? 'up' : 'down'"/> </a-col> </a-row> </a-form> </a-card> <a-card :bordered="false" class="table-card"> <bt-contracts-detailed-add-form v-if="showAddModal" ref="contractmanageAddForm" :firstContractTypeOptions="firstContractTypeOptions" :secondContractTypeOptions="secondContractTypeOptions" :sealTypeOptions="sealTypeOptions" :signTypeOptions="signTypeOptions" @ok="getList" @close="showAddModal = false" /> <bt-contracts-detailed-edit-form v-if="showEditModal" ref="contractmanageEditForm" :firstContractTypeOptions="firstContractTypeOptions" :secondContractTypeOptions="secondContractTypeOptions" :sealTypeOptions="sealTypeOptions" :signTypeOptions="signTypeOptions" @ok="getList" @close="showEditModal = false" /> <BtLiveEidt v-if="showLiveModal" ref="liveEidt" :firstContractTypeOptions="firstContractTypeOptions" :secondContractTypeOptions="secondContractTypeOptions" :sealTypeOptions="sealTypeOptions" :signTypeOptions="signTypeOptions" @ok="getList" @close="showLiveModal = false" /> <outBound v-if="visible" ref="outBoundref" @close="onClose" /> <advance-table title="出库管理" :pagination="{ current: queryParam.pageNum, pageSize: queryParam.pageSize, total: total, showSizeChanger: true, showLessItems: true, showQuickJumper: true, showTotal: (total, range) => 第 ${range[0]}-${range[1]} 条,总计 ${total} 条, onChange: changeSize, onShowSizeChange: onShowSizeChange }" tableKey="base-contractmanage-index-table" @change="handleTableChange" rowKey="id" size="middle" @refresh="getList" :columns="columns" :data-source="contractmanageList" :loading="loading" :format-conditions="true" :row-selection="{ selectedRowKeys: selectedRowKeys, onChange: onSelectChange }" > <bt-inventory-index ref="BtInventoryIndex" title="子表" :contractId="record.id" @outbound-status="(status) => handleOutboundStatus(record, status)" /> {{ firstContractTypeFormat(record) }} {{ secondContractTypeFormat(record) }} {{ sealTypeFormat(record) }} {{ signTypeFormat(record) }} <a-radio-group default-value="a" size="small"> <a-button type="primary" @click="handleUpdate1(record)" v-hasPermi="['bt:btOutbound:edit']" :size="size" ghost> <a-icon type="upload" />生活出库 </a-button> | <a-button type="primary" @click="handleUpdate(record)" v-hasPermi="['bt:btOutbound:edit']" :size="size" > <a-icon type="upload" />生产出库 </a-button> | <a-button type="dashed" @click="showDrawer(record)" v-hasPermi="['bt:btOutbound:list']" :size="size"> <a-icon type="table" />出库记录 </a-button> <a-button v-if="record.hasOutbound" type="danger" @click="resetOutboundStatus(record)" :size="size" > <a-icon type="undo" />重置为未出库 </a-button> </a-radio-group> <a-tag v-if="record.hasOutbound" color="green">已出库</a-tag> <a-tag v-else color="orange">未出库</a-tag> </advance-table> </a-card> </template> <script> import { listBtContractsDetailed, delBtContractsDetailed, exportBtContractsDetailed, getInitData } from '@/api/tt/ContractsDetailed' import AdvanceTable from '@/components/pt/table/AdvanceTable' import BtContractsDetailedAddForm from '@/views/bt/btoutbound/modules/BtContractsDetailedAddForm' import BtContractsDetailedEditForm from '@/views/bt/btoutbound/modules/BtContractsDetailedEditForm' import BtInventoryIndex from '@/views/bt/btoutbound/BtInventoryIndex' import outBound from '@/views/bt/btoutbound/outbound/index.vue' import BtLiveEidt from './modules/BtLiveEidt.vue' export default { name: 'BtoutBound', components: { AdvanceTable, BtInventoryIndex, outBound, BtContractsDetailedAddForm, BtContractsDetailedEditForm, BtLiveEidt }, data () { return { size:'small', visible: false, showAddModal: false, showEditModal: false, showLiveModal: false, // 遮罩层 loading: true, // 选中数组 ids: [], // 选中的主键集合 selectedRowKeys: [], // 选中的数据集合 selectedRows: [], // 高级搜索 展开/关闭 advanced: false, // 非单个禁用 single: true, // 非多个禁用 multiple: true, // 总条数 total: 0, // label的百分比 labelCol: { span: 6 }, // 内容区域的百分比 wrapperCol: { span: 18 }, // 合同管理表格数据 contractmanageList: [], // 一级合同类型字典 firstContractTypeOptions: [], // 二级合同类型字典 secondContractTypeOptions: [], // 用章类别字典 sealTypeOptions: [], // 签署类型字典 signTypeOptions: [], outBoundId: 0, // 查询参数 queryParam: { pageNum: 1, pageSize: 10, contractName: undefined, contractId: undefined, organizer: undefined, counterpartContractUnit: undefined }, columns: [ { title: '合同分类', dataIndex: 'contractClassification', ellipsis: true, align: 'left', width: '12.8%' }, { title: '合同名称', dataIndex: 'contractName', ellipsis: true, align: 'left', width: '12.8%' }, { title: '合同正式编号', dataIndex: 'contractId', ellipsis: true, align: 'left', width: '12.8%' }, { title: '采购方式', dataIndex: 'procurementMethod', ellipsis: true, align: 'left', width: '12.8%' }, { title: '对方签约单位', dataIndex: 'counterpartContractUnit', ellipsis: true, align: 'left', width: '12.8%' }, { title: '我方签订日期', dataIndex: 'ourSignDate', ellipsis: true, align: 'left', width: '12.8%' }, { title: '合同总金额', dataIndex: 'totalContractAmount', ellipsis: true, align: 'left', width: '12.8%' }, { title: '删除作废', dataIndex: 'deletedOrVoided', ellipsis: true, align: 'left', width: '12.8%' }, { title: '出库状态', dataIndex: 'hasOutbound', align: 'center', width: '10%', customRender: (text, record) => { return record.hasOutbound ? <a-tag color="green">已出库</a-tag> : <a-tag color="orange">未出库</a-tag>; } }, { title: '操作', dataIndex: 'operation', align: 'center', width: '60%', scopedSlots: { customRender: 'operation' } } ] } }, created () { this.getList() getInitData('contract_type_level1,contract_type_level2,sys_oper_type,sys_oper_type').then(response => { this.firstContractTypeOptions = response.data.contract_type_level1 this.secondContractTypeOptions = response.data.contract_type_level2 this.sealTypeOptions = response.data.sys_oper_type this.signTypeOptions = response.data.sys_oper_type }) }, methods: { resetOutboundStatus(record) { const index = this.contractmanageList.findIndex(item => item.id === record.id); if (index !== -1) { this.$set(this.contractmanageList[index], 'hasOutbound', false); // 更新前端状态 localStorage.removeItem(outbound_${record.id}); // 清除 localStorage 存储 this.$message.success("已重置为未出库"); } }, handleOutboundStatus(record, status) { const index = this.contractmanageList.findIndex(item => item.id === record.id); if (index !== -1 && status && !this.contractmanageList[index].hasOutbound) { this.$set(this.contractmanageList[index], 'hasOutbound', true); localStorage.setItem(outbound_${record.id}, 'true'); // 存储到 localStorage } }, onClose () { this.visible = false this.$emit('close') }, showDrawer(record) { this.visible = true const that = this that.$nextTick(() => ( that.$refs.outBoundref.handleList(record) )) }, // onClose() { // this.visible = false; // }, /** 查询合同管理列表 */ getList () { this.loading = true; listBtContractsDetailed(this.queryParam).then(response => { this.contractmanageList = response.data.list.map(item => { // 从 localStorage 读取是否已出库 const storedOutbound = localStorage.getItem(outbound_${item.id}); return { ...item, hasOutbound: storedOutbound === 'true' || false, }; }); this.total = response.data.total; this.loading = false; }); }, // 一级合同类型字典翻译 firstContractTypeFormat (row) { if (row.firstContractType) { return this.selectDictLabel(this.firstContractTypeOptions, row.firstContractType) } else { return '' } }, // 二级合同类型字典翻译 secondContractTypeFormat (row) { if (row.secondContractType) { return this.selectDictLabel(this.secondContractTypeOptions, row.secondContractType) } else { return '' } }, // 用章类别字典翻译 sealTypeFormat (row) { if (row.sealType) { return this.selectDictLabel(this.sealTypeOptions, row.sealType) } else { return '' } }, // 签署类型字典翻译 signTypeFormat (row) { if (row.signType) { return this.selectDictLabel(this.signTypeOptions, row.signType) } else { return '' } }, /** 搜索按钮操作 */ handleQuery () { this.queryParam.pageNum = 1 this.getList() }, /** 重置按钮操作 */ resetQuery () { this.queryParam = { pageNum: 1, pageSize: 10, contractName: undefined, contractId: undefined, organizer: undefined, counterpartContractUnit: undefined } this.handleQuery() }, /** 翻页操作 */ onShowSizeChange (current, pageSize) { this.queryParam.pageSize = pageSize this.getList() }, /** 翻页操作 */ onSizeChange (current, size) { this.queryParam.pageNum = 1 this.queryParam.pageSize = size this.getList() }, /** 翻页操作 */ changeSize (current, pageSize) { this.queryParam.pageNum = current this.queryParam.pageSize = pageSize this.getList() }, /** 翻页操作 */ onSelectChange (selectedRowKeys, selectedRows) { this.selectedRowKeys = selectedRowKeys this.selectedRows = selectedRows this.ids = this.selectedRows.map(item => item.id) this.single = selectedRowKeys.length !== 1 this.multiple = !selectedRowKeys.length }, /** 查询折叠和展开操作 */ toggleAdvanced () { this.advanced = !this.advanced }, handleAdd () { this.showAddModal = true this.$nextTick(() => ( this.$refs.contractmanageAddForm.handleAdd() )) }, //生活导出 handleUpdate1 (record, ids) { this.showLiveModal = true this.$nextTick(() => ( this.$refs.liveEidt.handleUpdate(record, ids) )) }, handleUpdate (record, ids) { this.showEditModal = true this.$nextTick(() => ( this.$refs.contractmanageEditForm.handleUpdate(record, ids) )) }, /** 删除按钮操作 */ handleDelete (row) { var that = this const contractmanageIds = row.id || this.ids this.$confirm({ title: '确认删除所选中数据?', onOk () { return delBtContractsDetailed(contractmanageIds) .then(() => { that.onSelectChange([], []) that.getList() that.$message.success( '删除成功', 3 ) }) }, onCancel () {} }) }, /** 导出按钮操作 */ handleExport () { var that = this this.$confirm({ title: '是否确认导出?', content: '此操作将导出当前条件下所有数据而非选中数据', onOk () { return exportBtContractsDetailed(that.queryParam) .then(response => { that.download(response.msg) that.$message.success( '导出成功', 3 ) }) }, onCancel () {} }) }, handleTableChange (pagination, filters, sorter) { if (sorter.field !== undefined && sorter.field !== null && sorter.field !== '') { this.queryParam.orderByColumn = 'a.' + sorter.field this.queryParam.isAsc = sorter.order } this.getList() } } } </script> 进行修改,我现在的未出库和已出库是存储在客户端,但现在要改成存储到数据库里,/** * 修改合同管理 */ @PreAuthorize("@ss.hasPermi('bt:btOutbound:edit')") @Log(title = "合同管理", businessType = BusinessType.UPDATE) @PutMapping public R edit(@RequestBody @Validated ContractsDetailed btContractsDetailed) { return R.status(btContractsDetailedService.save(btContractsDetailed)); }存储时调用这个接口里的 ecc_transfer varchar(50) DEFAULT NULL COMMENT '传输ECC', 这个字段,直接存储汉字

<template>
<a-card :bordered="false">
<a-form layout="inline" @keyup.enter.native="searchQuery">
<a-row :gutter="24">
</a-row>
</a-form>
<a-button @click="handleAdd" type="primary" icon="plus">新增</a-button>
<a-button type="primary" icon="download" @click="handleExportXls('流程实例关联表单')">导出</a-button>
<a-upload name="file" :showUploadList="false" :multiple="false" :headers="tokenHeader" :action="importExcelUrl" @change="handleImportExcel">
<a-button type="primary" icon="import">导入</a-button>
</a-upload>
<j-super-query :fieldList="superFieldList" ref="superQueryModal" @handleSuperQuery="handleSuperQuery"></j-super-query>
<a-dropdown v-if="selectedRowKeys.length > 0">
<a-menu slot="overlay">
<a-menu-item key="1" @click="batchDel"><a-icon type="delete"/>删除</a-menu-item>
</a-menu>
<a-button style="margin-left: 8px"> 批量操作 <a-icon type="down" /></a-button>
</a-dropdown>
已选择 {{ selectedRowKeys.length }}项
清空
<a-table
ref="table"
size="middle"
:scroll="{x:true}"
bordered
rowKey="id"
:columns="columns"
:dataSource="dataSource"
:pagination="ipagination"
:loading="loading"
:rowSelection="{selectedRowKeys: selectedRowKeys, onChange: onSelectChange}"
class="j-table-force-nowrap"
@change="handleTableChange">
<template slot="htmlSlot" slot-scope="text">
</template>
<template slot="imgSlot" slot-scope="text">
无图片
</template>
<template slot="fileSlot" slot-scope="text">
无文件
<a-button
v-else
:ghost="true"
type="primary"
icon="download"
size="small"
@click="downloadFile(text)">
下载
</a-button>
</template>
编辑
<a-divider type="vertical" />
<a-dropdown>
更多 <a-icon type="down" />
<a-menu slot="overlay">
<a-menu-item>
详情
</a-menu-item>
<a-menu-item>
<a-popconfirm title="确定删除吗?" @confirm="() => handleDelete(record.id)">
删除
</a-popconfirm>
</a-menu-item>
</a-menu>
</a-dropdown>
</a-table>
<sys-deploy-form-modal ref="modalForm" @ok="modalFormOk"></sys-deploy-form-modal>
</a-card>
</template>
<script>
import '@/assets/less/TableExpand.less'
import { mixinDevice } from '@/utils/mixin'
import { JeecgListMixin } from '@/mixins/JeecgListMixin'
import SysDeployFormModal from './modules/SysDeployFormModal'
export default {
name: 'SysDeployFormList',
mixins:[JeecgListMixin, mixinDevice],
components: {
SysDeployFormModal
},
data () {
return {
description: '流程实例关联表单管理页面',
// 表头
columns: [
{
title: '#',
dataIndex: '',
key:'rowIndex',
width:60,
align:"center",
customRender:function (t,r,index) {
return parseInt(index)+1;
}
},
{
title:'表单主键',
align:"center",
dataIndex: 'formId'
},
{
title:'流程实例主键',
align:"center",
dataIndex: 'deployId'
},
{
title:'创建人',
align:"center",
dataIndex: 'createBy'
},
{
title:'创建日期',
align:"center",
dataIndex: 'createTime'
},
{
title: '操作',
dataIndex: 'action',
align:"center",
fixed:"right",
width:147,
scopedSlots: { customRender: 'action' }
}
],
url: {
list: "/flowable/sysDeployForm/list",
delete: "/flowable/sysDeployForm/delete",
deleteBatch: "/flowable/sysDeployForm/deleteBatch",
exportXlsUrl: "/flowable/sysDeployForm/exportXls",
importExcelUrl: "flowable/sysDeployForm/importExcel",
},
dictOptions:{},
superFieldList:[],
}
},
created() {
this.getSuperFieldList();
},
computed: {
importExcelUrl: function(){
return ${window._CONFIG['domianURL']}/${this.url.importExcelUrl};
},
},
methods: {
/** 挂载流程定义 */
submitDefinitionForm(row) {
this.customformParam.formId = row.id;
updateCustomForm(this.customformParam).then(res => {
this.$message.success(res.message);
this.definitionOpen = false;
this.getList();
})
},
handleCurrentChange(data) {
if (data) {
this.currentRow = JSON.parse(data.formContent);
}
},
getCategoryName(category) {
let find = this.categorys.find(o => o.id == category);
if (find) {
return find.name
}
return ''
},
initDictConfig(){
},
getSuperFieldList(){
let fieldList=[];
fieldList.push({type:'string',value:'formId',text:'表单主键'})
fieldList.push({type:'string',value:'deployId',text:'流程实例主键'})
fieldList.push({type:'string',value:'createBy',text:'创建人'})
fieldList.push({type:'datetime',value:'createTime',text:'创建日期'})
this.superFieldList = fieldList
}
}
}
</script>
<style scoped>
@import '~@assets/less/common.less';
</style>上传文件为vue文件,vue版本为vue2版本,脚本为JavaScript编写;现在将该vue文件进行转化:转化vue版本为vue3版本,脚本为TypeScript编写

<template> 查询条件 <a-form-model :model="searchForm" layout="inline" ref="searchForm" class="searchForm"> <a-form-model-item label="题库名称" prop="title"> <a-input v-model="searchForm.title" placeholder="请输入题库名称" /> </a-form-model-item> <a-form-model-item class="searchButton"> <a-button type="primary" v-if="QX.read" @click="getSearch">查询</a-button> <a-button type="default" v-if="QX.read" @click="restSearch('searchForm')">重置</a-button> </a-form-model-item> </a-form-model> <a-button type="primary" @click="addBank" icon="plus" v-if="QX.add">新增</a-button> <a-table :columns="columns" :data-source="dataList" :pagination="false" :loading="loading" rowKey="questionBankId" :scroll="{ y: this.$getViewportSize().height - 300 }" > <a-icon class="iconBtn" type="edit" />编辑 <a-divider v-if="QX.edit && QX.delete" type="vertical" /> <a-popconfirm title="确认是否删除?" ok-text="是" cancel-text="否" @confirm="removeBank(record)" > <a-icon class="iconBtn" type="delete" />删除 </a-popconfirm> </a-table> <template slot="action" slot-scope="text, record, index"> 删除 </template> <a-pagination show-size-changer :total="totalPage" :current="pageIndex" :pageSize="pageSize" @showSizeChange="onShowSizeChange" @change="onChangePage" style="float: right; margin-top: 15px" /> <a-drawer :closable="true" :title="title" width="auto" :visible="visible" @close="visible = !visible" > <a-spin class="submitLoading" :spinning="submitLoading"> <a-form-model :model="form" :label-col="labelCol" :wrapper-col="wrapperCol" :rules="rules" ref="form" class="lay-drawer-form"> <a-row> <a-col :span="12"> <a-form-model-item label="题库名称" prop="title"> <a-input v-model="form.title" :maxLength="25" placeholder="请输入题库名称" style="width: 380px" /> </a-form-model-item> </a-col> <a-col :span="12"> <a-form-model-item label="关联岗位" prop="positionId"> <a-select v-model="form.positionId" style="width: 380px" placeholder="请选择岗位"> <a-select-option v-for="(label, value) in positionDict" :key="value" :value="value"> {{ label }} </a-select-option> </a-select> </a-form-model-item> </a-col> <a-col :span="12"> <a-form-model-item label="参与PK" prop="participateInPk"> <a-switch v-model="form.participateInPk" :checkedValue="1" :unCheckedValue="0" /> </a-form-model-item> </a-col> </a-row> </a-form-model> <a-button type="primary" @click="downloadTemplate">下载模板</a-button> <a-upload name="file" :showUploadList="false" :beforeUpload="beforeUpload" accept=".xlsx,.xls" :disabled="!importEnabled" > <a-button :disabled="!importEnabled"><a-icon type="upload" /> 导入题目</a-button> </a-upload> <a-button type="dashed" @click="addTopicVisible = true" :disabled="!addEnabled" > <a-icon type="plus" /> 添加题目 </a-button> {{ index + 1 }}. {{ topic.content }} <label v-for="option in getOptions(topic)" :key="option.key"> <input type="radio" :name="'topic' + topic.topicId" :value="option.key" :checked="topic.correctAnswer === option.key" /> {{ option.key }}. {{ option.value }} </label> <label> <input type="radio" :name="'topic' + topic.topicId" value="正确" :checked="topic.correctAnswer === '正确'" /> 正确 </label> <label> <input type="radio" :name="'topic' + topic.topicId" value="错误" :checked="topic.correctAnswer === '错误'" /> 错误 </label> <a-icon type="delete" /> 删除 <a-pagination v-model="topicPageNum" :pageSize="topicPageSize" :total="totalTopicCount" @change="handleTopicPageChange" style="text-align: right;" /> <a-button type="default" @click="visible = !visible" > 取消 </a-button> <a-button type="primary" @click="submitForm" > 确认 </a-button> </a-spin> </a-drawer> <a-drawer title="新增题目" :visible="addTopicVisible" @close="addTopicVisible = false" width="500" > <a-form-model :model="addTopicForm" layout="vertical" :rules="rulesForAddTopic" ref="addTopicFormRef"> <a-form-model-item label="题目类型" prop="topicType"> <a-select v-model="addTopicForm.topicType" style="width: 100%"> <a-select-option :value="1">选择题</a-select-option> <a-select-option :value="2">判断题</a-select-option> </a-select> </a-form-model-item> <a-form-model-item label="题目内容" prop="content"> <a-input v-model="addTopicForm.content" placeholder="请输入题目内容" /> </a-form-model-item> <a-form-model-item label="选项A" prop="optionA"> <a-input v-model="addTopicForm.optionA" placeholder="请输入选项A内容" /> </a-form-model-item> <a-form-model-item label="选项B" prop="optionB"> <a-input v-model="addTopicForm.optionB" placeholder="请输入选项B内容" /> </a-form-model-item> <a-form-model-item label="选项C" prop="optionC"> <a-input v-model="addTopicForm.optionC" placeholder="请输入选项C内容" /> </a-form-model-item> <a-form-model-item label="选项D" prop="optionD"> <a-input v-model="addTopicForm.optionD" placeholder="请输入选项D内容" /> </a-form-model-item> <a-form-model-item label="正确答案" prop="correctAnswer"> <a-select v-model="addTopicForm.correctAnswer" style="width: 100%"> <a-select-option value="A">A</a-select-option> <a-select-option value="B">B</a-select-option> <a-select-option value="C">C</a-select-option> <a-select-option value="D">D</a-select-option> </a-select> </a-form-model-item> <a-form-model-item label="正确答案" prop="correctAnswer"> <a-select v-model="addTopicForm.correctAnswer" style="width: 100%"> <a-select-option value="正确">正确</a-select-option> <a-select-option value="错误">错误</a-select-option> </a-select> </a-form-model-item> <a-button @click="addTopicVisible = false">取消</a-button> <a-button type="primary" @click="saveNewTopic">保存</a-button> </a-form-model> </a-drawer> </template> <script> import { req, fileDownload } from '../../../api/axiosFun'; import preventBack from 'vue-prevent-browser-back'; export default { name: 'Bank', mixins: [preventBack], data() { return { QX: {}, topicQX: {}, topicList: [], totalTopicCount: 0, // 题目总数 topicPageNum: 1, // 当前页码 topicPageSize: 10, // 每页数量 addTopicVisible: false, addTopicForm: { content: '', // 题目内容 topicType: 1, // 题目类型:1=选择题,2=判断题 optionA: '', optionB: '', optionC: '', optionD: '', correctAnswer: '', }, disabled: false, checkedKeys: [], selectAuth: [], treeData: [], positionDict: {}, title: '', labelCol: { span: 4 }, wrapperCol: { span: 20 }, tableHeight: 0, expanded: false, // 筛选条件是否展开 form: { questionBankId: 0, bankCode: '', title: '', positionId: '', participateInPk: true, }, isEdit: false, // 是否是编辑状态 isAdd: false, // 是否是新增状态 importEnabled: false, // 导入题目按钮是否可用 - 默认为不可用 addEnabled: false, // 添加题目按钮是否可用 - 默认为不可用 rules: { positionId: [ { required: true, message: '请选择岗位', trigger: 'blur' }, ], title: [ { required: true, message: '请输入题库名称', trigger: 'blur' }, ], }, rulesForAddTopic: { content: [ { required: true, message: '请输入题目内容', trigger: ['blur', 'change'] }, ], topicType: [ { required: true, message: '请选择题目类型', trigger: 'change' }, ], optionA: [ { required: (rule, value) => this.addTopicForm.topicType === 1, message: '选择题必须输入选项A', trigger: ['blur', 'change'], }, ], optionB: [ { required: (rule, value) => this.addTopicForm.topicType === 1, message: '选择题必须输入选项B', trigger: ['blur', 'change'], }, ], optionC: [ { required: (rule, value) => this.addTopicForm.topicType === 1, message: '选择题必须输入选项C', trigger: ['blur', 'change'], }, ], optionD: [ { required: (rule, value) => this.addTopicForm.topicType === 1, message: '选择题必须输入选项D', trigger: ['blur', 'change'], }, ], correctAnswer: [ { required: true, message: '请选择正确答案', trigger: 'change' }, ], }, searchForm: { title: '', }, visible: false, dataList: [], columns, loading: false, submitLoading: false, pageIndex: 1, pageSize: 10, totalPage: 0, ops: { vuescroll: {}, scrollPanel: {}, rail: { keepShow: true, }, bar: { hoverStyle: true, onlyShowBarOnScroll: false, // 是否只有滚动的时候才显示滚动条 background: '#F5F5F5', // 滚动条颜色 opacity: 1, // 滚动条透明度 'overflow-x': 'hidden', }, }, }; }, watch: { 'addTopicForm.topicType': function (newVal) { // 当题目类型变化时,触发相关字段的验证 this.$nextTick(() => { if (this.$refs.addTopicFormRef) { // 验证选项字段 if (newVal === 1) { this.$refs.addTopicFormRef.validateFields(['optionA', 'optionB', 'optionC', 'optionD']); } else { // 清除非必填字段的验证状态 this.$refs.addTopicFormRef.clearValidate(['optionA', 'optionB', 'optionC', 'optionD']); } } }); }, visible(newVal, oldVal) { if (!newVal) { this.restForm('form'); this.form.questionBankId = 0; this.checkedKeys = []; this.selectAuth = []; this.treeData = []; // 重置状态标志 this.isEdit = false; this.isAdd = false; } else { // 当抽屉打开时强制更新按钮状态 this.$nextTick(() => { this.$forceUpdate(); }); } }, }, mounted() { this.actionTitle = '操作'; this.getDict('position').then(res => { const dictMap = {}; res.data.forEach(item => { dictMap[item.dicValue] = item.dicDisplayName; }); this.positionDict = dictMap; }); this.getBankList(); }, methods: { // 获取字典数据方法 getDict(type) { return req('get', /dict/getDictItemByNo, { dicNo: type }) .then((res) => { if (res.result === 'success') { return res; } throw new Error(res.message || '获取字典数据失败'); }) .catch((error) => { console.error(获取${type}字典失败:, error); throw error; }); }, /* 新增题库 */ addBank() { this.visible = true; this.disabled = false; this.title = '新增题库'; // 获取功能权限树 req('get', '/bank/getTree', {}).then((res) => { this.treeData = res.data; }); // 初始化题目列表为空 this.topicList = []; this.totalTopicCount = 0; this.topicPageNum = 1; this.isEdit = false; // 不是编辑状态 this.isAdd = true; // 设置为新增状态 // 在新增状态下禁用导入和添加功能 this.importEnabled = false; this.addEnabled = false; // 强制更新按钮状态 this.$nextTick(() => { this.$forceUpdate(); }); }, // 查询 getSearch() { this.pageIndex = 1; this.getBankList(); }, /* 重置查询 */ restSearch(form) { this.restForm(form); this.pageIndex = 1; this.getBankList(); }, /* 删除题库 */ removeBank(record) { this.loading = true; req('post', '/bank/removeBQuestionBank', { questionBankId: record.questionBankId, }).then((res) => { this.loading = false; if (res.result === 'success') { this.$message.success(res.message); this.getBankList(); } }); }, downloadTemplate() { fileDownload('get', '/topic/downloadTemplate', null).then((res) => { const blob = new Blob([res], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', }); const link = document.createElement('a'); link.href = window.URL.createObjectURL(blob); link.download = '题库模板.xlsx'; link.click(); window.URL.revokeObjectURL(link.href); }).catch(() => { this.$message.error('下载失败'); }); }, beforeUpload(file) { // 显示加载状态 this.loading = true; const formData = new FormData(); formData.append('file', file); formData.append('questionBankId', this.form.questionBankId); // 添加 questionBankId 字段 req('post', '/topic/import', formData, { headers: { 'Content-Type': 'multipart/form-data', }, }) .then((res) => { // 隐藏加载状态 this.loading = false; if (res.result === 'success') { // 确保在DOM更新后再刷新题目列表 this.$nextTick(() => { // 刷新题目列表 this.getTopicList(this.form.questionBankId, this.topicPageNum, this.topicPageSize) .then(() => { this.$message.success('导入成功'); }) .catch(() => { this.$message.error('刷新题目列表失败'); }); }); } else { this.$message.error(res.message || '导入失败'); } }) .catch((error) => { // 隐藏加载状态 this.loading = false; this.$message.error('导入失败,请重试'); console.error('导入题目出错:', error); }); return false; // 阻止默认上传行为 }, // 获取题库下的题目列表(返回Promise以便链式调用) getTopicList(questionBankId, pageNum = 1, pageSize = 10) { return new Promise((resolve, reject) => { req('post', '/topic/list', { questionBankId, page: pageNum, rows: pageSize, }).then((res) => { if (res.result === 'success') { this.topicQX = res.QX; if (!this.topicQX.edit && !this.topicQX.delete) { this.hideAction(); } else { this.topicList = res.data.map((item, index) => ({ index: (pageNum - 1) * pageSize + index + 1, content: item.content, optionA: item.optionA, optionB: item.optionB, optionC: item.optionC, optionD: item.optionD, topicId: item.topicId, correctAnswer: item.correctAnswer, topicType: item.topicType, })); } // 更新总条数用于分页组件 this.totalTopicCount = res.page.totalResult; resolve(res); } else { this.$message.error(res.message || '获取题目列表失败'); reject(res); } }).catch((error) => { this.$message.error('获取题目列表失败'); reject(error); }); }); }, handleTopicPageChange(page) { this.topicPageNum = page; this.getTopicList(this.form.questionBankId, page, this.topicPageSize); }, // 获取选择题的选项 getOptions(topic) { if (topic.topicType === 1) { // 选择题:返回 A/B/C/D 选项 const options = []; if (topic.optionA) options.push({ key: 'A', value: topic.optionA }); if (topic.optionB) options.push({ key: 'B', value: topic.optionB }); if (topic.optionC) options.push({ key: 'C', value: topic.optionC }); if (topic.optionD) options.push({ key: 'D', value: topic.optionD }); return options; } else if (topic.topicType === 2) { // 判断题:直接返回 正确/错误 return [ { key: '正确', value: '正确' }, { key: '错误', value: '错误' }, ]; } return []; }, checkAnswer(topic, selectedAnswer) { // 仅记录用户选择的答案,不进行是否正确的判断 this.$set(topic, 'userAnswer', selectedAnswer); }, resetAddTopicForm() { this.addTopicForm = { content: '', topicType: 1, optionA: '', optionB: '', optionC: '', optionD: '', correctAnswer: '', }; }, saveNewTopic() { this.$refs.addTopicFormRef.validate((valid, fields) => { if (!valid) { console.log('表单验证失败:', fields); // 找出第一个错误字段并聚焦 const firstErrorField = Object.keys(fields).find(key => fields[key]); if (firstErrorField && this.$refs.addTopicFormRef) { const formItem = this.$refs.addTopicFormRef.$children.find( child => child.prop === firstErrorField, ); if (formItem && formItem.$el) { const input = formItem.$el.querySelector('input, select, textarea'); if (input) input.focus(); } } return; } // 验证通过,处理保存逻辑 const newTopic = { ...this.addTopicForm }; // 发送请求保存题目 req('post', '/topic/add', { questionBankId: this.form.questionBankId, optionA: newTopic.optionA, optionB: newTopic.optionB, optionC: newTopic.optionC, optionD: newTopic.optionD, correctAnswer: newTopic.correctAnswer, content: newTopic.content, topicType: newTopic.topicType, }).then((res) => { if (res.result === 'success') { this.$message.success('题目添加成功'); this.addTopicVisible = false; this.getTopicList(this.form.questionBankId); // 刷新题目列表 } else { this.$message.error(res.message || '保存失败'); } this.resetAddTopicForm(); }).catch((err) => { this.$message.error('网络异常,请重试'); }); }); }, /* 删除题库下的题目 */ removeQuestion(topicId) { this.$confirm({ title: '确认删除该题目?', content: '删除后将无法恢复', okText: '是', cancelText: '否', onOk: () => { req('post', '/topic/removeBTopic', { topicId, questionBankId: this.form.questionBankId, }).then((res) => { if (res.result === 'success') { this.$message.success(res.message); // 刷新题目列表 this.getTopicList(this.form.questionBankId, this.topicPageNum, this.topicPageSize); } else { this.$message.error(res.message || '删除失败'); } }); }, }); }, editAuth(record) { this.loading = true; req('post', '/bank/getBQuestionBank', { questionBankId: record.questionBankId, }).then((res) => { this.loading = false; if (res.result === 'success') { this.visible = true; this.disabled = true; this.title = '修改题库'; this.isEdit = true; // 设置为编辑状态 this.isAdd = false; // 不是新增状态 // 在编辑状态下启用导入和添加功能 this.importEnabled = true; this.addEnabled = true; // 强制更新按钮状态 this.$nextTick(() => { this.$forceUpdate(); }); const bank = res.data; this.$nextTick(() => { this.form.questionBankId = bank.questionBankId; this.form.title = bank.title; this.form.participateInPk = Boolean(bank.participateInPk); this.form.positionId = bank.positionId; this.treeData = bank.treeData; this.checkedKeys = bank.auths; // 获取题目列表 this.topicPageNum = 1; this.getTopicList(bank.questionBankId, this.topicPageNum, this.topicPageSize); }); } }); }, /* 保存or修改题库信息 */ submitForm() { this.$refs.form.validate((valid) => { if (valid) { this.form.participateInPk = this.form.participateInPk ? 1 : 0; const url = this.form.questionBankId ? 'edit' : 'add'; const selectAuth = this.selectAuth; this.form.selectAuth = JSON.stringify(selectAuth); this.submitLoading = true; req('post', /bank/${url}, this.form).then((res) => { if (res.result === 'success') { this.visible = false; this.getBankList(); // 如果是新增题库且成功,获取题目列表 if (!this.form.questionBankId && res.data && res.data.questionBankId) { this.form.questionBankId = res.data.questionBankId; this.topicPageNum = 1; this.getTopicList(res.data.questionBankId, this.topicPageNum, this.topicPageSize); // 新增成功后启用导入和添加功能 this.importEnabled = true; this.addEnabled = true; } this.$message.success(res.message); // 重置新增/编辑状态 this.isEdit = false; this.isAdd = false; } this.submitLoading = false; }); } }); }, /* 重置表单 */ restForm(form) { this.$refs[form].resetFields(); }, /* 改变页数事件 */ onChangePage(page, pageSize) { this.pageIndex = page; this.getBankList(); }, /* 改变每页显示条数 */ onShowSizeChange(current, pageSize) { this.pageIndex = 1; this.pageSize = pageSize; this.getBankList(); }, /* 题库信息列表 */ getBankList() { this.loading = true; this.searchForm.page = this.pageIndex; this.searchForm.rows = this.pageSize; req('post', '/bank/list', this.searchForm) .then((res) => { if (res.result === 'success') { this.dataList = res.data; this.QX = res.QX; // 无权限隐藏操作列 if (!this.QX.edit && !this.QX.delete) { this.hideAction(); } else if (columns[columns.length - 1].title != '操作') { columns.push(actionShow); } this.totalPage = res.page.totalResult; } this.loading = false; }).catch((error) => { this.loading = false; }); }, /* 无所有行操作权限时,隐藏操作栏 */ hideAction() { if (columns[columns.length - 1].title == '操作') { columns.splice(columns.length - 1, 1); } }, /* 校验代号类型 */ validCode(value) { if (value.length > 20) { value = value.slice(0, 20); } for (let i = value.length - 1; i >= 0; i--) { const unicode = value.charCodeAt(i); if (unicode > 65280 && unicode < 65375) { value = value.substr(0, i); } } this.value = value; }, }, }; const actionShow = { title: '操作', width: '200px', hide: true, dataIndex: 'action', key: 'action', align: 'center', scopedSlots: { customRender: 'action' }, }; const columns = [ { title: '序号', width: '50px', align: 'center', customRender: (text, row, index) => index + 1, }, { title: '题库名称', align: 'center', dataIndex: 'title', key: 'title', width: '120px', }, { title: '涉及岗位', align: 'center', dataIndex: 'positionName', key: 'positionName', width: '110px', }, { title: '题目数量', dataIndex: 'topicCount', key: 'topicCount', align: 'center', width: '180px', }, { title: '操作', width: '150px', hide: true, dataIndex: 'action', key: 'action', align: 'center', scopedSlots: { customRender: 'action' }, }, ]; </script> <style scoped> .topic-list-container { max-height: calc(100vh - 400px); /* 根据实际布局调整最大高度 */ overflow-y: auto; } .pagination-wrapper { position: sticky; bottom: 0; left: 0; right: 0; background: #fff; padding: 10px; z-index: 1; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); margin-top: 15px; } .topic-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 20px; } .topic-item { border: 1px solid #e8e8e8; padding: 15px; border-radius: 4px; position: relative; /* 为绝对定位的子元素提供定位上下文 */ } .topic-content { font-size: 16px; margin-bottom: 10px; } .options label { display: block; margin: 5px 0; font-size: 14px; } .options input[type="radio"] { margin-right: 5px; } /* 按钮禁用状态样式优化 */ .ant-upload.ant-upload-disabled .ant-upload-list, .ant-btn[disabled] { cursor: not-allowed; opacity: 0.5; } .topic-delete { position: absolute; top: 10px; right: 10px; } </style> <style scoped> @import "../../../assets/css/maincss.css"; </style> 在编辑页面中选择某个题目的选项后,再次打开该题时,默认显示的是上次选择的选项而不是正确答案。修改此问题




<script setup lang="tsx"> import { DataTableSortState, NButton, NTag, SelectOption } from "naive-ui"; import { fetchGetChangeLedgerList, fetchBatchDeleteChange, fetchDeleteChange, } from "@/service/api"; import { generateZtUri } from "@/plugins"; import { $t } from "@/locales"; import { useAppStore } from "@/store/modules/app"; import { useTable, useTableOperate } from "@/hooks/common/table"; import { useAuth } from "@/hooks/business/auth"; import LedgerSearch from "./modules/ledger-search.vue"; import { useBoolean } from "@sa/hooks"; import { canUpgradeRecord } from "@/constants/business"; import { ref, onMounted, watch, h } from "vue"; // import {useDepartment} from "@/hooks/common/useDepartment" // 导入嵌入模态框 import { getFullDeptOptions } from "@/service/api/"; // 导入完整部门选项服务 import { renderTooltip2, smartTextSplit } from "@/utils/other"; // 存储完整部门选项 const fullDeptOptions = ref<SelectOption[]>([]); // 加载完整部门列表 onMounted(async () => { fullDeptOptions.value = await getFullDeptOptions(); }); const appStore = useAppStore(); const { columns, columnChecks, data, getData, loading, mobilePagination, searchParams, resetSearchParams, } = useTable({ apiFn: fetchGetChangeLedgerList, showTotal: true, apiParams: { current: 1, size: 10, // if you want to use the searchParams in Form, you need to define the following properties, and the value is null // the value can not be undefined, otherwise the property in Form will not be reactive source: null, pecoName: null, peco: null, softwareChange: null, projectno: null, dept: null, canUpgrade: null, softResponsiblePerson: null, elecResponsiblePerson: null, changeResponsiblePerson: null, customerDemandTime: null, softCompletionTime: null, hardCompletionTime: null, fmtCustomerDemandTime: null, fmtSoftCompletionTime: null, fmtHardCompletionTime: null, orderList: [], }, columns: () => [ { type: "selection", align: "center", width: 48, }, { key: "index", title: $t("common.index"), align: "center", width: 64, sorter: false, }, { key: "hwswcoId", title: $t("page.change.ledger.hwswcoId"), align: "left", minWidth: 50, width: 80, maxWidth: 200, sorter: true, fixed: "left", resizable: true, // render: (row) => ( // // // {row.hwswcoId} // // // ), render: (row) => { //const href = ${row?.test_url}${row.testProcessId}?zentaosid=${appStore.zenTaoSid}; const contents: any[] = []; contents.push( 未填写${$t("page.change.ledger.hwswcoId")}, 无法跳转禅道, ); if (row.hwswcoId !== null) { return h( NButton, { strong: true, secondary: true, size: "small", type: "info", onClick: () => { const href = generateZtUri(${row?.hwswco_url}); window.open(href, "_blank"); }, }, { default: () => row.hwswcoId, }, ); } else { return <NTag>{row.hwswcoId}</NTag>; } }, }, { key: "source", title: $t("page.change.ledger.source"), align: "center", width: 80, sorter: true, resizable: true, }, { key: "pecoName", title: $t("page.change.ledger.pecoName"), align: "center", minWidth: 50, width: 100, maxWidth: 200, sorter: true, resizable: true, render: (row) => { const contents: any[] = smartTextSplit(row.pecoName, 50); return row.pecoName ? renderTooltip2(row.pecoName.slice(0, 10), contents) : null; }, }, { key: "peco", title: $t("page.change.ledger.peco"), align: "center", minWidth: 50, width: 100, maxWidth: 200, sorter: true, resizable: true, }, { key: "softwareChange", title: $t("page.change.ledger.softwareChange"), align: "center", minWidth: 50, width: 100, maxWidth: 200, sorter: true, resizable: true, }, { key: "projectno", title: $t("page.change.ledger.projectno"), align: "center", minWidth: 40, width: 80, maxWidth: 100, sorter: true, resizable: true, }, { key: "dept", title: $t("page.change.ledger.dept"), align: "center", width: 130, sorter: true, render: (row) => { // 使用完整部门选项映射部门名称 if (row.dept && fullDeptOptions.value.length > 0) { const dept = fullDeptOptions.value.find( (option) => option.value === row.dept, ); if (dept) { return <spn>{dept.label}</spn>; } } return null; }, }, { key: "softResponsiblePersonName", title: $t("page.change.ledger.softResponsiblePersonName"), align: "center", minWidth: 50, width: 110, maxWidth: 200, sorter: true, resizable: true, }, { key: "elecResponsiblePersonName", title: $t("page.change.ledger.elecResponsiblePersonName"), align: "center", minWidth: 50, width: 110, maxWidth: 200, sorter: true, resizable: true, }, // { // key: "changeResponsiblePerson", // title: $t("page.change.ledger.changeResponsiblePerson"), // align: "center", // minWidth: 50, // }, { key: "canUpgrade", title: $t("page.change.ledger.canUpgrade"), align: "center", minWidth: 50, width: 80, maxWidth: 100, sorter: true, resizable: true, typeRecord: canUpgradeRecord, render: (row) => { console.log(row.canUpgrade); if (row.canUpgrade) { // console.log( row.canUpgrade, canUpgradeRecord[ // row.canUpgrade as Api.Change.canUpgrade // ]) const label = $t( canUpgradeRecord[row.canUpgrade as Api.Change.canUpgrade], ); return <NTag type="default">{label}</NTag>; } return null; }, }, { key: "fmtCustomerDemandTime", title: $t("page.change.ledger.customerDemandTime"), align: "center", width: 100, sorter: true, resizable: true, render: (row) => row.fmtCustomerDemandTime === "1970-01-01" ? null : row.fmtCustomerDemandTime, }, { key: "fmtSoftCompletionTime", title: $t("page.change.ledger.softCompletionTime"), align: "center", width: 100, sorter: true, resizable: true, render: (row) => row.fmtSoftCompletionTime === "1970-01-01" ? null : row.fmtSoftCompletionTime, }, { key: "fmtHardCompletionTime", title: $t("page.change.ledger.hardCompletionTime"), align: "center", width: 100, sorter: true, resizable: true, render: (row) => row.fmtHardCompletionTime === "1970-01-01" ? null : row.fmtHardCompletionTime, }, { key: "operate", title: $t("common.operate"), align: "center", width: 130, fixed: "right", render: (row) => ( {hasAuth("B_ChangeLedger_Upgrade") ? ( <NButton type="primary" ghost size="small" onClick={() => { const href = generateZtUri(${row?.hwswco_upgrade_url}); window.open(href, "_blank"); }} > {$t("common.upgrade")} </NButton> ) : null} {hasAuth("B_ChangeLedger_Assign") ? ( <NButton type="primary" ghost size="small" onClick={() => { const href = generateZtUri(${row?.hwswco_url}); window.open(href, "_blank"); }} > {$t("common.assign")} </NButton> ) : null} ), }, ], }); const { drawerVisible, operateType, editingData, handleAdd, handleEdit, handleClone, checkedRowKeys, onBatchDeleted, onDeleted, // closeDrawer } = useTableOperate(data, getData); // 定义排序器 // const sorter = ref<DataTableSortState | null>(null); // // 使用自定义钩子 // const { sortTable } = useTableSort(data, sorter); // 处理排序器变化 const handleSorterChange = (newSorter: DataTableSortState) => { let columnKey = String(newSorter.columnKey); // 移除fmt前缀(不区分大小写) if (/^fmt/i.test(columnKey)) { columnKey = columnKey.substring(3); } // 小驼峰转下划线命名(优化连续大写字母处理) columnKey = columnKey .replace(/([a-z])([A-Z])/g, "$1_$2") .replace(/([A-Z])([A-Z][a-z])/g, "$1_$2") .toLowerCase(); // 处理排序状态 if (newSorter.order === "ascend") { searchParams.orderList = [columnKey]; } else if (newSorter.order === "descend") { searchParams.orderList = ["-" + columnKey]; } else { searchParams.orderList = []; } getData(); }; const { hasAuth } = useAuth(); const { bool: uploadVisible, setTrue: openModal } = useBoolean(); async function handleBatchDelete() { // request const { error } = await fetchBatchDeleteChange({ ids: checkedRowKeys.value, }); if (!error) { onBatchDeleted(); } } async function handleDelete(id: number) { // request const { error } = await fetchDeleteChange({ id }); if (!error) { onDeleted(); } } function edit(id: number) { handleEdit(id); } function clone(id: number) { handleClone(id); } interface Emits { (e: "update-total", payload: number | undefined): void; } const emit = defineEmits<Emits>(); watch( () => mobilePagination.value.itemCount, (newVal, oldVal) => { emit("update-total", newVal); }, { deep: true }, ); // async function handleDownload() { // // 请求cosnt {data, error } = await fetchGetProblemList({size:xxx,current:1}) // if (checkedRowKeys.value.length === 0) { // window.$message?.warning('请选择需要导出的数据!'); // return; // } // const { data:responseData, error } = await fetchGetChangeLedgerList({ // size: checkedRowKeys.value.length, // current: 1, // ids: checkedRowKeys.value, // }); // if (error) { // console.error("获取数据时出错:", error); // alert("获取数据时发生错误,请稍后再试。"); // return; // } // // 关键修改:将部门ID转换为部门名称 // const processedRecords = responseData.records.map(record => { // // 查找匹配的部门选项 // const deptOption = fullDeptOptions.value.find( // option => option.value === record.dept // ); // // 转换canUpgrade数值为文本标签 // const upgradeLabel = canUpgradeRecord[record.canUpgrade as keyof typeof canUpgradeRecord] // ? $t(canUpgradeRecord[record.canUpgrade as keyof typeof canUpgradeRecord]) // : record.canUpgrade; // 未匹配时保留原值 // // 返回新对象(保留原数据,替换dept和canUpgrade字段) // return { // ...record, // dept: deptOption ? deptOption.label : record.dept, // canUpgrade: upgradeLabel // 替换为转换后的文本 // }; // }); // // 翻译表头 // const titleMap = { // hwswcoId: $t("page.change.ledger.hwswcoId"), // source: $t("page.change.ledger.source"), // pecoName: $t("page.change.ledger.pecoName"), // peco: $t("page.change.ledger.peco"), // softwareChange:$t("page.change.ledger.softwareChange"), // projectno: $t("page.change.ledger.projectno"), // dept: $t("page.change.ledger.dept"), // softResponsiblePersonName: $t("page.change.ledger.softResponsiblePersonName"), // elecResponsiblePersonName: $t("page.change.ledger.elecResponsiblePersonName"), // canUpgrade: $t("page.change.ledger.canUpgrade"), // // customerDemandTime: $t("page.change.ledger.customerDemandTime"), // // softCompletionTime: $t("page.change.ledger.softCompletionTime"), // // hardCompletionTime: $t("page.change.ledger.hardCompletionTime"), // fmtCustomerDemandTime: $t("page.change.ledger.fmtCustomerDemandTime"), // fmtSoftCompletionTime: $t("page.change.ledger.fmtSoftCompletionTime"), // fmtHardCompletionTime: $t("page.change.ledger.fmtHardCompletionTime"), // }; // const headers = Object.keys(titleMap).map((key) => titleMap[key]); // const sheetData = [headers]; // processedRecords.forEach(record => { // const row = Object.keys(titleMap).map(key => record[key]); // sheetData.push(row); // }); // const sheetName = $t("page.change.ledger.title"); // const sheets: SheetData[] = [ // { // sheetName: sheetName, // data: sheetData, // }, // ]; // exportXlsx(sheets, ${sheetName}); // } // 在页面加载时获取最新数据 getData(); </script> <template> <LedgerSearch v-model:model="searchParams" @reset="resetSearchParams" @search="getData" /> <NCard :bordered="false" size="small" class="sm:flex-1-hidden card-wrapper"> <template #header >{{ $t("page.change.ledger.title") }} ({{ $t("datatable.itemCount", { total: mobilePagination?.itemCount, }) }}) </template> <template #header-extra> <template #default> </template> </template> <NDataTable v-model:checked-row-keys="checkedRowKeys" :columns="columns" :data="data" size="small" :flex-height="!appStore.isMobile" :scroll-x="2400" :loading="loading" remote :row-key="(row) => row.id" :pagination="mobilePagination" class="sm:h-full" @update:sorter="handleSorterChange" /> </NCard> </template> <style scoped></style>


