vue中使用el-table的@row-click如何配置.stop属性

时间: 2025-06-22 22:39:59 AIGC 浏览: 25
### 配置 `@row-click` 事件并防止子元素点击触发 为了确保在 Vue 中使用 Element UI 的 `<el-table>` 组件时,点击子元素不会触发父级的 `@row-click` 事件,可以在子元素上添加 `.stop` 修饰符来阻止事件冒泡。 当定义表格列内的可交互元素(如按钮或其他链接)时,在其点击事件处理程序中加入 `.stop` 修饰符能够有效避免不必要的行点击行为被激活。具体实现方式如下所示: ```html <template> <div> <!-- 定义 table 并设置 row-click --> <el-table :data="tableData" @row-click="handleRowClick"> <el-table-column prop="name" label="Name"></el-table-column> <el-table-column prop="action" label="Action"> <template slot-scope="scope"> <!-- 子元素上的 click 事件带有 .stop 修饰符 --> <button type="text" size="small" @click.stop="handleChildClick(scope.row)">操作</button> </template> </el-table-column> </el-table> </div> </template> <script> export default { data() { return { tableData: [ { name: 'Tom' }, { name: 'Jerry' } ] }; }, methods: { handleRowClick(row) { console.log('Row clicked:', row); }, handleChildClick(row) { console.log('Child button clicked, not triggering parent event:', row); } } }; </script> ``` 通过上述代码片段可以看出,对于希望独立响应而不引起整个行选中的单元格内部控件来说,只需简单地在其对应的事件监听器后面追加`.stop`即可达到目的[^2]。 此外,还可以考虑在 `@row-click` 处理逻辑里增加条件判断,比如检测是否有文本被选中等情况下的特殊处理。
阅读全文

相关推荐

<template> <el-container> <el-breadcrumb separator="/"> <el-breadcrumb-item>合同信息:</el-breadcrumb-item> </el-breadcrumb> <el-form> <el-row :gutter="24"> <el-col :span="8"> <el-form-item label="合同编号:" :title="ContractNo"> <el-input v-model="ContractNo" /> </el-form-item> </el-col> <el-col :span="2"> <el-button @click="getHetongList()">搜索</el-button> </el-col> <el-col :span="3"> <el-button type="primary" plain style="margin-left: 10px" icon="Import" @click="importData">导入合同清单</el-button> </el-col> </el-row> </el-form> <VolBox v-model="model1" title="合同导入" :width="600" :padding="15" :onModelClose="onModelClose"> <UploadExcel :url="url1" :template="template"></UploadExcel> <template #footer> <el-button @click="closeModel">关闭</el-button> </template> </VolBox> <el-table ref="conTableDataRef" :data="conTableData" border style="width: 100%;height: 85%;" :header-cell-style="{ 'text-align': 'center', }" :cell-style="{ 'text-align': 'center' }" @selection-change="handleSelectionChange" :row-key="(row) => row.htId" :row-class-name="tableRowClassName"> <el-table-column type="selection" width="55" :selectable="(row) => !row.isSelected" reserve-selection /> <el-table-column prop="con_Name" label="XS名称" show-overflow-tooltip /> <el-table-column prop="con_XH" label="XS型号" show-overflow-tooltip /> <el-table-column prop="con_Unit" label="XS单位" show-overflow-tooltip /> <el-table-column prop="con_SL" label="XS税率" show-overflow-tooltip /> <el-table-column prop="con_QTY" label="XS数量" show-overflow-tooltip /> <el-table-column prop="con_UnitPrice" label="XS单价" show-overflow-tooltip /> <el-table-column prop="con_SumamOut" label="XS合计" show-overflow-tooltip /> </el-table> <el-pagination @size-change="handleSizeChangeHt" @current-change="handleCurrentChangeHt" :current-page="queryInfoHt.page" :page-sizes="[5, 10, 15]" :page-size="queryInfoHt.rows" layout="->,total, sizes, prev, pager, next, jumper" :total="htTotal"> </el-pagination> <el-breadcrumb separator="/"> <el-breadcrumb-item>物料信息:</el-breadcrumb-item> </el-breadcrumb> <el-form> <el-row :gutter="24"> <el-col :span="5"> <el-form-item label="所属组织:"> <el-select v-model="orgValue" class="m-2" placeholder="选择所属组织" clearable> <el-option v-for="item in orgOptions" :key="item.value" :label="item.label" :value="item.value" /> </el-select> </el-form-item> </el-col> <el-col :span="8"> <el-form-item label="物料名称:"> <el-input v-model="matName" clearable /> </el-form-item> </el-col> <el-col :span="6"> <el-form-item label="物料编码:"> <el-input v-model="matId" clearable /> </el-form-item> </el-col> <el-col :span="5"> <el-button @click="getMatLiistSearch()">搜索</el-button> </el-col> </el-row> </el-form> <el-table ref="MatDataRef" :data="MatData" border style="width: 100%;height: 85%;" :header-cell-style="{ 'text-align': 'center', }" :cell-style="{ 'text-align': 'center' }" @selection-change="handleSelectionChangeMat" :row-key="(row) => row.FNumber" :row-class-name="tableRowClassName"> <el-table-column type="selection" width="55" :selectable="(row) => !row.isSelected && this.htSelectFlag" reserve-selection /> <el-table-column prop="FName" label="物料名称" show-overflow-tooltip /> <el-table-column prop="FNumber" label="物料编码" show-overflow-tooltip /> <el-table-column prop="FSpecification" label="型号" show-overflow-tooltip /> <el-table-column prop="FBaseUnit" label="单位" show-overflow-tooltip /> <el-table-column prop="FMATERIALGROUP" label="分组" show-overflow-tooltip /> <el-table-column prop="FMnemonicCode" label="版本说明" show-overflow-tooltip /> <el-table-column prop="FDescription" label="备注" show-overflow-tooltip /> </el-table> <el-pagination @size-change="handleSizeChange" @current-change="handleCurrentChange" :current-page="queryInfo.page" :page-sizes="[5, 10, 15]" :page-size="queryInfo.rows" layout="->,total, sizes, prev, pager, next, jumper" :total="matTotal"> </el-pagination> <el-breadcrumb separator="/"> <el-breadcrumb-item>详情</el-breadcrumb-item> </el-breadcrumb> <el-table ref="spResultDataRef" :data="spResultData" border style="width: 100%;" :header-cell-style="{ 'text-align': 'center', }" :cell-style="{ 'text-align': 'center' }"> <el-table-column prop="con_Name" label="XS名称" width="180px" show-overflow-tooltip></el-table-column> <el-table-column prop="con_XH" label="XS型号" width="120px" show-overflow-tooltip></el-table-column> <el-table-column prop="con_Unit" label="XS单位" width="80px" show-overflow-tooltip></el-table-column> <el-table-column prop="con_QTY" label="XS数量" width="80px" show-overflow-tooltip></el-table-column> <el-table-column prop="con_SL" label="XS税率" width="80px" show-overflow-tooltip></el-table-column> <el-table-column prop="con_UnitPrice" label="XS单价" width="80px" show-overflow-tooltip></el-table-column> <el-table-column prop="con_SumamOut" label="XS合计" width="80px" show-overflow-tooltip></el-table-column> <el-table-column label="物料数据" height="250"> <template #default="{ row, $index: contractIndex }"> <el-table :data="row.children" border :row-class-name="draggingRowClass" @row-dragstart="handleDragStart(contractIndex)" @row-dragover="handleDragOver" @row-drop="handleDrop(contractIndex)"> <el-table-column prop="FNumber" label="物料编码" width="90px" show-overflow-tooltip> <template #default="{ row: material }"> {{ material.FNumber }} </template> </el-table-column> <el-table-column prop="FName" label="名称" width="100px" show-overflow-tooltip></el-table-column> <el-table-column prop="FSpecification" label="型号" width="100px" show-overflow-tooltip></el-table-column> <el-table-column prop="FBaseUnit" label="单位" width="60px" show-overflow-tooltip></el-table-column> <el-table-column prop="wtNumber" label="数量" width="68px" show-overflow-tooltip></el-table-column> <el-table-column prop="FMATERIALGROUP" label="分组" width="90p" show-overflow-tooltip></el-table-column> <el-table-column prop="FMnemonicCode" label="版本说明" width="100px" show-overflow-tooltip></el-table-column> <el-table-column prop="FDescription" label="备注" width="100px" show-overflow-tooltip></el-table-column> <el-table-column label="操作"> <template #default="{ row, $index: materialIndex }"> <el-button type="primary" @click="handleEdit(row)" circle><el-icon> <Edit /> </el-icon></el-button> <el-button type="danger" @click="() => { deleteMaterialRow(row, contractIndex, materialIndex) }" circle><el-icon> <Delete /> </el-icon></el-button> </template> </el-table-column> </el-table> </template> </el-table-column> <el-table-column label="操作" width="140px"> <template v-slot:header="scope"> 操作 <el-tooltip content="切换物料信息添加状态" placement="bottom" effect="light"> <el-switch v-model="addFlag" @change="handleSwitchChange" :disabled="!(conTableData.length > 0 && MatData.length > 0)" active-color="#67C23A" inactive-color="#F56C6C"></el-switch> </el-tooltip> </template> <template #default="{ row, $index: contractIndex }"> <el-button type="danger" @click="() => deleteContract(row)" circle><el-icon> <CloseBold /> </el-icon></el-button> <el-button type="success" @click="() => addSelectedMaterials(contractIndex)" circle :disabled="addFlagAble"> <el-icon> </el-icon> </el-button> </template> </el-table-column> </el-table> <el-dialog v-model="dialogVisibleFlag" width="35%" style="padding: 10px;"> <template #header> <el-icon> <EditPen /> </el-icon> 物料信息编辑: </template> <el-form ref="editFormRef" :model="editForm" :rules="editRules" label-width="80px"> <el-row :gutter="24"> <el-col :span="12"> <el-form-item label="物料编码:" prop="FNumber" class="textStyle"> <el-input v-model="editForm.FNumber" disabled></el-input> </el-form-item> </el-col> <el-col :span="12"> <el-form-item label="名称:" prop="FName" class="textStyle" :title="editForm.FName"> <el-input v-model="editForm.FName" disabled></el-input> </el-form-item> </el-col> </el-row> <el-row :gutter="24"> <el-col :span="12"> <el-form-item label="数量:" prop="wtNumber"> <el-input-number v-model="editForm.wtNumber"></el-input-number> </el-form-item> </el-col> <el-col :span="12"> <el-form-item label="版本说明:" prop="FMnemonicCode"> <el-input v-model="editForm.FMnemonicCode"></el-input> </el-form-item> </el-col> </el-row> <el-row :gutter="24"> <el-col :span="12"> <el-form-item label="备注:" prop="FDescription"> <el-input v-model="editForm.FDescription"></el-input> </el-form-item> </el-col> </el-row> </el-form> <template #footer> <el-button @click="dialogVisibleFlag = false">取消</el-button> <el-button type="primary" @click="submitForm">保存</el-button> </template> </el-dialog> <el-button @click="saveData" style="margin-top: 10px;">保存数据</el-button> <el-button @click="loadData" style="margin-top: 10px">加载数据</el-button> </el-container> </template> <script> import VolBox from '@/components/basic/VolBox.vue' import VolTable from '@/components/basic/VolTable.vue' import UploadExcel from '@/components/basic/UploadExcel.vue' import { ElMessage } from 'element-plus' export default { components: { VolBox: VolBox, VolTable: VolTable, UploadExcel: UploadExcel }, data() { return { dialogVisibleFlag: false, editForm: { FNumber: '', FName: '', wtNumber: '', FMnemonicCode: '', FDescription: '', }, url1: 'api/MaterialHetong/import1', template: { url: this.http.ipAddress + 'api/MaterialHetong/DownLoadTemplate', fileName: '合同清单' }, model1: false, matTotal: 0, htTotal: 0, queryInfo: { page: 1, rows: 10, sort: "id", order: "desc", wheres: [] }, queryInfoHt: { page: 1, rows: 10, sort: "id", order: "desc", wheres: [] }, orgOptions: [ { label: '华洋', value: '1', }, { label: '华讯', value: '100153', } ], orgValue: '', selectedCategory: null, conName: '', ContractNo: '', matName: '', matId: '', selectedProduct: null, conTableData: [], MatData: [], spResultData: [], selectMatValueList: [], htSelectFlag: false, addFlag: false, currentFNumber: [], currentCXH: [], disabledRows: [], childBeforeStorage: {}, isAddFlag: false, editRules: { wtNumber: [ { required: true, message: '请输入数量', trigger: 'blur' }, { pattern: /^-?\d+$/, message: '请输入整数', trigger: 'blur' } ], }, addFlagAble: true, box3: false, draggingMaterial: null, sourceContractIndex: null, draggingRow: null } }, computed: { filteredProducts() { if (!this.selectedCategory) { return this.products } return this.products.filter(product => product.categoryId === this.selectedCategory) } }, created() { localStorage.removeItem('currentCXH') localStorage.removeItem('spResultData') localStorage.removeItem('currentFNumber') this.loadData() }, watch: { orgValue(newValue, oldValue) { if (oldValue === '1' && newValue === '100153' || oldValue === '100153' && newValue === '1') { this.spResultData = [] this.currentCXH = [] this.currentFNumber = [] if (this.$refs.MatDataRef) { this.$refs.MatDataRef.clearSelection() } if (this.$refs.conTableDataRef) { this.$refs.conTableDataRef.clearSelection() } // this.saveData() this.getHetongList() this.getMatLiist() } } }, methods: { open3(temp, charactorTemp) { this.orgValue = '1' this.ContractNo = temp this.box3 = true if (charactorTemp == 2) { this.orgValue = '100153' } this.getHetongList() // this.getMatLiist() }, handleSizeChange(newSize) { this.queryInfo.rows = newSize this.getMatLiist() }, handleCurrentChange(newPage) { // this.saveData() this.queryInfo.page = newPage this.getMatLiist() }, handleSizeChangeHt(newSize) { this.queryInfoHt.rows = newSize this.getHetongList() }, handleCurrentChangeHt(newPage) { // this.saveData() this.queryInfoHt.page = newPage this.getHetongList() setTimeout(() => { this.getHetongList() }, 100) }, tableRowClassName({ row, rowIndex }) { return row.isSelected ? 'warning-row' : '' }, startDrag(contractIndex, material, event) { this.draggingMaterial = material this.sourceContractIndex = contractIndex event.dataTransfer.effectAllowed = 'move' this.draggingRow = event.target.closest('tr') // this.draggingRow.style.opacity = '0.5' }, handleDragOver(event) { event.preventDefault() event.dataTransfer.dropEffect = 'move' }, handleMaterialDrop(targetContractIndex, targetMaterial, event) { event.preventDefault() if (!this.draggingMaterial) return // 移除原合同的物料 const sourceMaterials = this.spResultData[this.sourceContractIndex].children const sourceIndex = sourceMaterials.findIndex(m => m.FNumber === this.draggingMaterial.FNumber) if (sourceIndex > -1) { sourceMaterials.splice(sourceIndex, 1) } // 添加到新合同 const targetMaterials = this.spResultData[targetContractIndex].children const existsIndex = targetMaterials.findIndex(m => m.FNumber === this.draggingMaterial.FNumber) if (existsIndex === -1) { targetMaterials.push(this.draggingMaterial) } this.draggingRow.style.opacity = '1' this.draggingMaterial = null this.sourceContractIndex = null this.draggingRow = null this.saveData() }, draggingRowClass({ row }) { return { 'dragging-row': this.draggingMaterial?.FNumber === row.FNumber } }, async getHetongList() { this.saveData() this.queryInfoHt.wheres = JSON.stringify([ { name: 'Contract_No', value: this.ContractNo, displayType: 'like' } ]) let htUrl = 'api/MaterialHetong/getPageData' this.loadData() await this.http.post(htUrl, this.queryInfoHt, true).then((data) => { this.conTableData = data.rows this.htTotal = data.total this.loadData() if (this.spResultData.length > 0) { // this.sortSpResultDataByCreateDate() this.updateTableSelection(this.conTableData, 'htId', this.currentCXH, this.$refs.conTableDataRef) this.updateTableSelection(this.MatData, 'FNumber', this.currentFNumber, this.$refs.MatDataRef) } }) }, getMatLiistSearch() { this.queryInfo.page = 1 // this.saveData() this.getMatLiist() }, async getMatLiist() { this.saveData() if (!this.orgValue) { return ElMessage.info('请选择所属组织') } this.queryInfo.wheres = JSON.stringify([ { name: 'fuseOrgId', value: this.orgValue, displayType: '=' }, { name: 'FName', value: this.matName, displayType: 'like' }, { name: 'FNumber', value: this.matId, displayType: 'like' } ]) let url = 'api/Material/getPageData' this.loadData() await this.http.post(url, this.queryInfo, true).then((res) => { this.MatData = res.rows this.matTotal = res.total this.loadData() if (this.spResultData.length > 0) { this.updateTableSelection(this.MatData, 'FNumber', this.currentFNumber, this.$refs.MatDataRef) } }) }, importData() { this.model1 = true }, closeModel() { this.model1 = false }, onModelClose() { }, handleSelectionChange(val) { this.htSelectFlag = val.length === 0 ? false : true this.addFlag = false this.childBeforeStorage = {} if (this.spResultData.length > 0) { this.spResultData.forEach((item, index) => { if (item.children.length > 0) { this.childBeforeStorage[index] = item.children } if (item) { const index = this.conTableData.findIndex(m => m.htId === item.htId) if (index > -1) { this.$refs.conTableDataRef.toggleRowSelection(this.conTableData[index], false) this.conTableData[index].isSelected = true } } }) this.updateTableSelection(this.conTableData, 'htId', this.currentCXH, this.$refs.conTableDataRef) this.updateTableSelection(this.MatData, 'FNumber', this.currentFNumber, this.$refs.MatDataRef) } val.forEach(item => { const existingIndex = this.spResultData.findIndex(sp => sp.htId === item.htId) if (existingIndex === -1) { this.spResultData.push({ ...item, children: [] }) } }) let index = 0 for (let item of this.spResultData) { if (this.childBeforeStorage[index]) { item.children = this.childBeforeStorage[index] } index++ } if (this.$refs.MatDataRef) { this.$refs.MatDataRef.clearSelection() } this.matName = '' this.matId = '' this.currentCXH = [...new Set(this.spResultData.filter(obj => obj && obj.htId).map(obj => obj.htId))] // this.sortSpResultDataByConTableData() }, sortSpResultDataByConTableData() { const conTableIds = this.conTableData.map(item => item.htId); this.spResultData.sort((a, b) => { const indexA = conTableIds.indexOf(a.htId); const indexB = conTableIds.indexOf(b.htId); return indexA - indexB; }); }, sortSpResultDataByCreateDate() { this.spResultData.sort((a, b) => { if (!a.CreateDate || !b.CreateDate) return 0; const dateA = new Date(a.CreateDate); const dateB = new Date(b.CreateDate); return dateB - dateA; }); }, handleSelectionChangeMat(val) { if (this.htSelectFlag === false) { this.$refs.MatDataRef.clearSelection() return // return ElMessage.info('请先选择合同信息') } if (this.addFlag) { this.selectMatValueList = val this.spResultData.forEach(obj => { if (obj.children) { const index = this.MatData.findIndex(m => m.FNumber === obj.children.FNumber) if (index > -1) { this.MatData[index].isSelected = true } } }) this.currentFNumber = [...new Set( this.spResultData.filter(obj => obj && obj.children) .flatMap(obj => obj.children.filter(child => child && child.FNumber).map(child => child.FNumber)) )] } else { this.selectMatValueList = val this.spResultData.forEach(obj => { if (obj.children) { const index = this.MatData.findIndex(m => m.FNumber === obj.children.FNumber) if (index > -1) { // this.$refs.MatDataRef.toggleRowSelection(this.MatData[index], false) this.MatData[index].isSelected = true } } }) if (this.spResultData.length > 0) { const lastOrder = this.spResultData[this.spResultData.length - 1] lastOrder.children = val } this.currentFNumber = [...new Set( this.spResultData.filter(obj => obj && obj.children) .flatMap(obj => obj.children.filter(child => child && child.FNumber).map(child => child.FNumber)) )] } }, deleteMaterialRow(row, contractIndex, materialIndex) { let curr = row.FNumber if (this.spResultData.length > contractIndex) { this.spResultData[contractIndex].children.splice(materialIndex, 1) } const index = this.MatData.findIndex(m => m.FNumber === curr) if (index > -1) { this.MatData[index].isSelected = false this.$refs.MatDataRef.toggleRowSelection(this.MatData[index], false) } }, handleEdit(row) { // this.editForm = row this.editForm = { ...row } this.dialogVisibleFlag = true }, submitForm() { this.$refs.editFormRef.validate(async (valid) => { if (!valid) return this.dialogVisibleFlag = false for (let contract of this.spResultData) { for (let material of contract.children) { if (material.FNumber === this.editForm.FNumber) { Object.assign(material, this.editForm) break } } } this.saveData() }) }, addFlagFun() { this.$refs.conTableDataRef.clearSelection() this.updateTableSelection(this.conTableData, 'htId', this.currentCXH, this.$refs.conTableDataRef) this.htSelectFlag = true this.addFlag = true this.addFlagAble = false }, exitFlagFun() { this.htSelectFlag = false this.addFlag = false this.addFlagAble = true }, handleSwitchChange(value) { if (value) { this.$refs.conTableDataRef.clearSelection() this.updateTableSelection(this.conTableData, 'htId', this.currentCXH, this.$refs.conTableDataRef) this.htSelectFlag = true this.addFlag = true this.addFlagAble = false } else { this.htSelectFlag = false this.addFlag = false this.addFlagAble = true } }, addSelectedMaterials(contractIndex) { if (this.spResultData.length > contractIndex) { const contract = this.spResultData[contractIndex] this.selectMatValueList.forEach(newMaterial => { const existingIndex = contract.children.findIndex(child => child.FNumber === newMaterial.FNumber) if (existingIndex === -1) { contract.children.push(newMaterial) } }) } this.selectMatValueList = [] this.$refs.MatDataRef.clearSelection() this.updateTableSelection(this.MatData, 'FNumber', this.currentFNumber, this.$refs.MatDataRef) }, saveData() { localStorage.setItem('spResultData', JSON.stringify(this.spResultData)) localStorage.setItem('currentCXH', this.currentCXH) localStorage.setItem('currentFNumber', JSON.stringify(this.currentFNumber)) }, loadData() { const savedData = localStorage.getItem('spResultData') if (savedData) { try { this.spResultData = JSON.parse(savedData) this.currentCXH = [...new Set(this.spResultData.filter(obj => obj && obj.htId).map(obj => obj.htId))] this.currentFNumber = [...new Set( this.spResultData.filter(obj => obj && obj.children) .flatMap(obj => obj.children.filter(child => child && child.FNumber).map(child => child.FNumber)) )] } catch (error) { console.log('数据解析错误:', error) } } else { console.log('未找到保存的数据') } }, deleteContract(row) { const spIndex = this.spResultData.findIndex(item => item.htId === row.htId) if (spIndex > -1) { const materials = this.spResultData[spIndex].children materials.forEach(material => { const matIndex = this.MatData.findIndex(m => m.FNumber === material.FNumber) if (matIndex > -1) { this.MatData[matIndex].isSelected = false if (this.$refs.MatDataRef) { this.$refs.MatDataRef.toggleRowSelection(material, false) } } }) this.spResultData.splice(spIndex, 1) } this.currentCXH = this.currentCXH.filter(id => id !== row.htId) const index = this.conTableData.findIndex(m => m.htId === row.htId) if (index > -1) { this.conTableData[index].isSelected = false if (this.$refs.conTableDataRef) { this.$refs.conTableDataRef.toggleRowSelection(this.conTableData[index], false) } } this.saveData() }, updateTableSelection(tableData, key, selectedIds, tableRef) { if (key === 'FNumber') { tableData.forEach(row => { row.isSelected = false if (row[key] == '10999999' || row[key] == '10999998') { row.isSelected = false } else { selectedIds.forEach(item => { if (row[key] === item) { tableRef.toggleRowSelection(item, false) row.isSelected = true } }) } }) } else { tableData.forEach(row => { row.isSelected = false selectedIds.forEach(item => { if (row[key] === item) { tableRef.toggleRowSelection(item, false) row.isSelected = true } }) }) } } } } </script> <style scoped> .cont { width: 100%; height: 100%; /* position: absolute; left: 0; top: 0; display: flex; flex-direction: column; */ overflow-y: auto; overflow-x: hidden; } .top { width: 100%; /* height: 50%; */ height: 500px; display: flex; } .topL { width: 49%; height: 97%; padding: 10px; margin: 10px; border: 1px solid transparent; box-shadow: 0 0 10px #a6afafb3; } .topR { width: 49%; height: 97%; padding: 10px; margin: 10px 10px 10px 0px; border: 1px solid transparent; box-shadow: 0 0 10px #a6afafb3; } .formStyle { width: 100%; height: 10%; } .tableStyle { width: 100%; height: 90%; } .bot { width: 99%; height: 50%; margin: 10px; padding: 10px; border: 1px solid transparent; box-shadow: 0 0 10px #a6afafb3; } /* :deep(.el-select--large .el-select__wrapper) { min-height: 28px; } */ .el-pagination { margin-top: 2px; } :deep(.el-breadcrumb__item:last-child .el-breadcrumb__inner, .el-breadcrumb__item:last-child .el-breadcrumb__inner a, .el-breadcrumb__item:last-child .el-breadcrumb__inner a:hover, .el-breadcrumb__item:last-child .el-breadcrumb__inner:hover) { color: #2d8cf0; font-weight: 600; margin-bottom: 5px; } :deep(.el-dialog) { width: 30%; min-width: 500px; } .el-form { margin-top: 20px; } :deep(.el-table .warning-row) { background: oldlace; } .dialog-footer { justify-content: end; display: flexdraggingRowClass } .dialog-title { color: #2d8cf0; font-size: 16px; } .draggable-cell { cursor: move; user-select: none; } .dragging-row { opacity: 0.5; background: #f5f7fa; } .el-table tr.drop-over td { background-color: #ebeefa !important; } .textStyle { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } </style>当合同信息被加入spResultData数组时,根据合同列表中的顺序排列spResultData的顺序,合同列表数组是根据sortCode排序的

<template> <el-row :gutter="20"> <el-col :span="6" :xs="24"> <el-input v-model="projectName" placeholder="请输入项目名称" clearable size="mini" prefix-icon="el-icon-search" style="margin-bottom: 20px" /> <el-row :gutter="10" class="mb8"> <el-col :span="1.5"> <el-button type="info" plain icon="el-icon-sort" size="mini" @click="toggleExpandAll" >展开/折叠</el-button> </el-col> <el-col :span="1.5"> <el-button type="primary" plain icon="el-icon-plus" size="mini" @click="handleAddProject" >新增项目</el-button> </el-col> </el-row> <el-tree v-if="refreshTable" style="margin-top: 0.8rem;" :data="projectTreeData" :props="defaultProps" :expand-on-click-node="false" :filter-node-method="filterNode" :highlight-current="true" :default-expand-all="isExpandAll" ref="projectTree" empty-text="加载中,请稍候" node-key="id" @node-click="handleProjectClick" > 25"> <el-tooltip :show-after="300" :content="data.label" placement="top-start"> {{ ellipsis(data.label, 25) }} </el-tooltip> {{ node.label }} <el-button type="text" size="mini" icon="el-icon-edit" @click.stop="() => handleEditProject(data)" ></el-button> <el-button type="text" size="mini" icon="el-icon-plus" @click.stop="() => handleAddSubProject(data)" ></el-button> <el-button type="text" size="mini" icon="el-icon-delete" @click.stop="() => handleDeleteProject(data)" ></el-button> </el-tree> </el-col> <el-col :span="18" :xs="24"> <el-form :model="docQueryParams" ref="docQueryForm" :inline="true" label-width="68px"> <el-form-item label="文档标题" prop="title"> <el-input v-model="docQueryParams.title" placeholder="请输入文档标题" clearable size="mini" @keyup.enter.native="getDocumentList" /> </el-form-item> <el-form-item label="状态" prop="status"> <el-select v-model="docQueryParams.status" placeholder="状态" size="mini" clearable> <el-option label="草稿" value="0" /> <el-option label="审核中" value="1" /> <el-option label="已发布" value="2" /> </el-select> </el-form-item> <el-form-item> <el-button type="primary" icon="el-icon-search" size="mini" @click="getDocumentList">搜索</el-button> <el-button icon="el-icon-refresh" size="mini" @click="resetDocQuery">重置</el-button> </el-form-item> </el-form> <el-row :gutter="10" class="mb8"> <el-col :span="1.5"> <el-button type="primary" plain icon="el-icon-plus" size="mini" @click="handleAddDocument" >新增文档</el-button> </el-col> </el-row> <el-table v-loading="docLoading" :data="documentList" highlight-current-row @row-click="handleRowClick" > <el-table-column label="序号" type="index" width="50" align="center" /> <el-table-column label="文档标题" prop="title" min-width="200" /> <el-table-column label="状态" prop="status" width="100"> <template slot-scope="scope"> <el-tag v-if="scope.row.status === '0'" type="info">草稿</el-tag> <el-tag v-if="scope.row.status === '1'" type="warning">审核中</el-tag> <el-tag v-if="scope.row.status === '2'" type="success">已发布</el-tag> </template> </el-table-column> <el-table-column label="创建人" prop="creator" width="100" /> <el-table-column label="创建时间" prop="createTime" width="140" /> <el-table-column label="操作" width="120" align="center"> <template slot-scope="scope"> <el-button size="mini" type="text" icon="el-icon-edit" @click.stop="handleEditDocument(scope.row)"></el-button> <el-button size="mini" type="text" icon="el-icon-delete" @click.stop="handleDeleteDocument(scope.row)"></el-button> </template> </el-table-column> </el-table> 0" :total="docTotal" :page.sync="docQueryParams.pageNum" :limit.sync="docQueryParams.pageSize" @pagination="getDocumentList" /> <el-button type="text" icon="el-icon-back" @click="backToList">返回文档列表</el-button> {{ currentDocument.title }} <el-form :model="currentDocument" ref="docForm" label-width="80px"> <el-form-item label="文档内容"> <Tinymce :height='600' v-model='currentDocument.content'></Tinymce> </el-form-item> <el-form-item> <el-button type="primary" @click="saveDocument">保存</el-button> <el-button @click="backToList">取消</el-button> </el-form-item> </el-form> </el-col> </el-row> <el-dialog :title="projectDialogTitle" :visible.sync="projectDialogVisible" width="50%"> <el-form :model="projectForm" ref="projectForm" label-width="100px"> <el-form-item label="项目名称" prop="name" required> <el-input v-model="projectForm.name" placeholder="请输入项目名称" /> </el-form-item> <el-form-item label="上级项目" prop="parentId"> <treeselect v-model="projectForm.parentId" :options="projectTreeData" :normalizer="normalizer" placeholder="选择上级项目" /> </el-form-item> <el-form-item label="项目描述" prop="description"> <el-input type="textarea" v-model="projectForm.description" :rows="3" /> </el-form-item> </el-form> <el-button @click="projectDialogVisible = false">取消</el-button> <el-button type="primary" @click="saveProject">保存</el-button> </el-dialog> </template> <script> import { getProjectTree, saveProject,updateProject, deleteProject } from "@/api/cms/articleProject"; import { listData, getData, delData, addData, updateData , changeDataStatus, createArticleOutput, addReviewData,listReviewData, getUsersMenu,getArticleDocumentVersions,findDocumentDiff,getArticleDatasMenus, changeDataUsed} from "@/api/cms/data"; import Tinymce from '@/components/Tinymce'; import Treeselect from "@riophae/vue-treeselect"; import "@riophae/vue-treeselect/dist/vue-treeselect.css"; export default { name: "ProjectManagement", components: { Treeselect, Tinymce }, data() { return { // 项目树相关数据 projectName: '', projectTreeData: [], refreshTable: true, isExpandAll: false, defaultProps: { children: "children", label: "label" }, // 项目对话框相关 projectDialogVisible: false, projectDialogTitle: '', projectForm: { id: null, name: '', parentId: null, description: '' }, // 文档列表相关 activeView: 'list', // 'list' 或 'detail' docQueryParams: { projectId: null, title: '', status: '', pageNum: 1, pageSize: 10 }, documentList: [], docTotal: 0, docLoading: false, // 文档详情相关 currentDocument: { id: null, projectId: null, title: '', content: '', status: '0' } }; }, created() { this.getProjectTree(); }, methods: { // 项目树方法 ellipsis(label, length) { return label.length > length ? label.slice(0, length) + '...' : label; }, toggleExpandAll() { this.refreshTable = false; this.isExpandAll = !this.isExpandAll; this.$nextTick(() => { this.refreshTable = true; }); }, filterNode(value, data) { if (!value) return true; return data.label.indexOf(value) !== -1; }, handleProjectClick(data) { this.docQueryParams.projectId = data.id; this.getDocumentList(); }, // 项目管理方法 handleAddProject() { this.projectForm = { name: '', parentId: null, description: '' }; this.projectDialogTitle = '新增项目'; this.projectDialogVisible = true; }, handleAddSubProject(data) { this.projectForm = { name: '', parentId: data.id, description: '' }; this.projectDialogTitle = '新增子项目'; this.projectDialogVisible = true; }, handleEditProject(data) { this.projectForm = { id: data.id, name: data.label, parentId: data.parentId, description: data.description || '' }; this.projectDialogTitle = '编辑项目'; this.projectDialogVisible = true; }, handleDeleteProject(data) { this.$confirm(确定删除项目 "${data.label}" 及其所有子项目吗?, '提示', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' }).then(() => { deleteProject(data.id).then(() => { this.$message.success('删除成功'); this.getProjectTree(); }); }); }, saveProject() { this.$refs.projectForm.validate(valid => { if (valid) { saveProject(this.projectForm).then(() => { this.$message.success('保存成功'); this.projectDialogVisible = false; this.getProjectTree(); }); } }); }, normalizer(node) { return { id: node.id, label: node.label, children: node.children }; }, getProjectTree() { getProjectTree().then(response => { this.projectTreeData = response.data; }); }, // 文档管理方法 getDocumentList() { if (!this.docQueryParams.projectId) { this.$message.warning('请先选择一个项目'); return; } this.docLoading = true; getDocumentList(this.docQueryParams).then(response => { this.documentList = response.rows; this.docTotal = response.total; this.docLoading = false; }); }, resetDocQuery() { this.docQueryParams.title = ''; this.docQueryParams.status = ''; this.getDocumentList(); }, handleAddDocument() { this.currentDocument = { id: null, projectId: this.docQueryParams.projectId, title: '新建文档', content: '', status: '0' }; this.activeView = 'detail'; }, handleEditDocument(row) { getDocumentDetail(row.id).then(response => { this.currentDocument = response.data; this.activeView = 'detail'; }); }, handleDeleteDocument(row) { this.$confirm(确定删除文档 "${row.title}" 吗?, '提示', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' }).then(() => { // 调用删除文档API this.$message.success('删除成功'); this.getDocumentList(); }); }, handleRowClick(row) { getDocumentDetail(row.id).then(response => { this.currentDocument = response.data; this.activeView = 'detail'; }); }, backToList() { this.activeView = 'list'; }, saveDocument() { saveDocument(this.currentDocument).then(() => { this.$message.success('保存成功'); this.getDocumentList(); this.backToList(); }); } } }; </script> <style scoped> .app-container { padding: 20px; } .head-container { padding: 10px; background-color: #f5f7fa; border-radius: 4px; } .custom-tree-node { flex: 1; display: flex; align-items: center; justify-content: space-between; font-size: 14px; padding-right: 8px; } .doc-header { display: flex; align-items: center; margin-bottom: 20px; } .doc-header h2 { margin-left: 15px; margin-bottom: 0; } .el-tree { border: 1px solid #ebeef5; border-radius: 4px; padding: 10px; } /* 树节点操作按钮样式 */ .custom-tree-node .el-button { padding: 4px; margin-left: 5px; } /* 文档详情区域样式 */ .doc-detail-container { background-color: #fff; padding: 20px; border-radius: 4px; box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1); } </style> 分析该页面,树结构的信息应该在getProjectTree获取,目前无法正常显示

<template> <el-row :gutter="20"> <el-col :span="6" :xs="24"> <el-input v-model="projectName" placeholder="请输入项目名称" clearable size="mini" prefix-icon="el-icon-search" style="margin-bottom: 20px" @input="filterProjectTree" /> <el-row :gutter="10" class="mb8"> <el-col :span="1.5"> <el-button type="info" plain icon="el-icon-sort" size="mini" @click="toggleExpandAll" >{{ isExpandAll ? '折叠' : '展开' }}</el-button> </el-col> <el-col :span="1.5"> <el-button type="primary" plain icon="el-icon-folder-add" size="mini" @click="handleAddProject" v-hasPermi="['cms:project:add']" >新增项目</el-button> </el-col> </el-row> <el-tree v-if="refreshTable" style="margin-top: 0.8rem;" :data="filteredProjectTreeData" :props="defaultProps" :expand-on-click-node="false" :filter-node-method="filterNode" :highlight-current="true" :default-expand-all="isExpandAll" ref="projectTree" empty-text="加载中,请稍候" node-key="id" @node-click="handleProjectClick" > <el-icon class="tree-icon" :size="16"> <svg-icon v-if="node.expanded" icon-class="folder-open" /> <svg-icon v-else icon-class="folder" /> </el-icon> 25" class="node-label"> <el-tooltip :show-after="300" :content="node.label" placement="top-start"> {{ ellipsis(node.label, 25) }} </el-tooltip> {{ node.label }} <el-button type="text" size="mini" icon="el-icon-edit" v-hasPermi="['cms:project:update']" @click.stop="() => handleEditProject(data)" ></el-button> <el-button type="text" size="mini" icon="el-icon-plus" v-hasPermi="['cms:project:add']" @click.stop="() => handleAddSubProject(data)" ></el-button> <el-button type="text" size="mini" icon="el-icon-delete" v-hasPermi="['cms:project:delete']" @click.stop="() => handleDeleteProject(data)" ></el-button> </el-tree> </el-col> <el-col :span="18" :xs="24"> <el-form :model="docQueryParams" ref="docQueryForm" :inline="true" label-width="68px"> <el-form-item label="文档标题" prop="title"> <el-input v-model="docQueryParams.title" placeholder="请输入文档标题" clearable size="mini" @keyup.enter.native="getDocumentList" prefix-icon="el-icon-search" /> </el-form-item> <el-form-item label="状态" prop="status"> <el-select v-model="docQueryParams.status" placeholder="状态" size="mini" clearable> <el-option label="编制中✍" value="0" /> <el-option label="待评审✊" value="1" /> <el-option label="已评审👍" value="2" /> <el-option label="修改中🔧" value="3" /> <el-option label="开发中💪" value="4" /> <el-option label="已完成开发🆗" value="5" /> </el-select> </el-form-item> <el-form-item> <el-button type="primary" icon="el-icon-search" size="mini" @click="getDocumentList">搜索</el-button> <el-button icon="el-icon-refresh" size="mini" @click="resetDocQuery">重置</el-button> </el-form-item> </el-form> <el-button type="success" icon="el-icon-download" size="mini" :disabled="selectedDocs.length === 0" @click="handleBatchExport" > 批量导出 ({{ selectedDocs.length }}) </el-button> <el-table v-loading="docLoading" :data="documentList" highlight-current-row @row-click="handleRowClick" @selection-change="handleSelectionChange" ref="docTable" > <el-table-column type="selection" width="55" align="center" /> <el-table-column label="序号" type="index" width="50" align="center" /> <el-table-column label="编号" align="center" prop="serialNum" /> <el-table-column label="标题" align="center" prop="title" class-name="small-padding fixed-width" width="200" :show-overflow-tooltip="true" sortable > <template slot-scope="scope"> {{scope.row.title}}(v{{scope.row.articleVersion}}版本) <el-button v-show="!scope.row.title.startsWith('[诊断项]')" size="normal" type="text" icon="el-icon-tickets" @click="getArticleInfo(scope.row.id)">{{scope.row.title}}(v{{scope.row.articleVersion}}版本)</el-button> </template> </el-table-column> <el-table-column label="进度" prop="status" width="100"> <template slot-scope="scope"> <el-tag v-if="scope.row.status === '0'" type="info" size="small">编制中✍</el-tag> <el-tag v-if="scope.row.status === '1'" type="warning" size="small">待评审✊</el-tag> <el-tag v-if="scope.row.status === '2'" type="success" size="small">已评审👍</el-tag> <el-tag v-if="scope.row.status === '3'" type="success" size="small">修改中🔧</el-tag> <el-tag v-if="scope.row.status === '4'" type="success" size="small">开发中💪</el-tag> <el-tag v-if="scope.row.status === '5'" type="success" size="small">已完成开发🆗</el-tag> </template> </el-table-column> <el-table-column label="创建人" prop="createBy" width="100" /> <el-table-column label="负责人" prop="director" width="100"/> <el-table-column label="创建时间" prop="createTime" width="140" /> <el-table-column label="操作" width="180" align="center"> <template slot-scope="scope"> <el-button size="mini" type="text" icon="el-icon-edit" @click.stop="handleEditDocument(scope.row)"></el-button> <el-button size="mini" type="text" icon="el-icon-download" @click.stop="handleExportHttp(scope.row, true)" title="导出详细" ></el-button> <el-button size="mini" type="text" icon="el-icon-download" @click.stop="handleExportHttp(scope.row, false)" title="导出简版" ></el-button> <el-button size="mini" type="text" icon="el-icon-delete" @click.stop="handleDeleteDocument(scope.row)" v-hasPermi="['cms:project:deleteArticleFromProject']"></el-button> </template> </el-table-column> </el-table> 0" :total="docTotal" :page.sync="docQueryParams.pageNum" :limit.sync="docQueryParams.pageSize" @pagination="getDocumentList" /> <el-button type="text" icon="el-icon-back" @click="backToList">返回文档列表</el-button> <el-icon class="title-icon"><svg-icon icon-class="document" /></el-icon> {{ currentDocument.title }} <el-form :model="currentDocument" ref="docForm" label-width="80px"> <el-form-item label="文档内容"> <Tinymce :height='600' v-model='currentDocument.content'></Tinymce> </el-form-item> <el-form-item> <el-button type="primary" icon="el-icon-check" @click="saveDocument">保存</el-button> <el-button icon="el-icon-close" @click="backToList">取消</el-button> </el-form-item> </el-form> </el-col> </el-row> <el-dialog :title="projectDialogTitle" :visible.sync="projectDialogVisible" width="50%"> <el-form :model="projectForm" ref="projectForm" label-width="100px"> <el-form-item label="项目名称" prop="name" required> <el-input v-model="projectForm.name" placeholder="请输入项目名称" prefix-icon="el-icon-folder" /> </el-form-item> <el-form-item label="上级项目" prop="parentId"> <treeselect v-model="projectForm.parentId" :options="projectTreeData" :normalizer="normalizer" placeholder="选择上级项目" /> </el-form-item> <el-form-item label="项目描述" prop="description"> <el-input type="textarea" v-model="projectForm.description" :rows="3" /> </el-form-item> </el-form> <el-button @click="projectDialogVisible = false">取消</el-button> <el-button type="primary" icon="el-icon-check" @click="saveProject">保存</el-button> </el-dialog> </template> <script> import { getData, updateData } from "@/api/cms/data"; import { getProjectTree, getDocuments, saveProject, updateProject, deleteProject, removeMenusFromProject } from "@/api/cms/articleProject"; import Tinymce from '@/components/Tinymce'; import Treeselect from "@riophae/vue-treeselect"; import "@riophae/vue-treeselect/dist/vue-treeselect.css"; import axios from 'axios'; export default { name: "ProjectManagement", components: { Treeselect, Tinymce }, data() { return { // 项目树相关数据 projectName: '', projectTreeData: [], filteredProjectTreeData: [], refreshTable: true, isExpandAll: true, defaultProps: { children: "children", label: "label" }, // 项目对话框相关 projectDialogVisible: false, projectDialogTitle: '', projectForm: { id: null, name: '', parentId: null, description: '' }, // 文档列表相关 activeView: 'list', // 'list' 或 'detail' docQueryParams: { serialNums: null, title: '', status: '', pageNum: 1, pageSize: 10 }, documentList: [], docTotal: 0, docLoading: false, selectedDocs: [], // 选中的文档 ids: [], // 选中文档ID集合 // 文档详情相关 currentDocument: { id: null, menuId: null, title: '', content: '', status: '0' }, // 当前选中的项目ID(用于删除操作) currentProjectId: null, // 导出相关数据 form: { ids: [], // 导出的文档ID集合 notesExportFlag: 1 // 默认导出详细 } }; }, created() { this.getProjectTree(); }, methods: { // ================= 项目树方法 ================= ellipsis(text, maxLength) { return text.length > maxLength ? text.substring(0, maxLength) + '...' : text; }, toggleExpandAll() { this.refreshTable = false; this.isExpandAll = !this.isExpandAll; this.$nextTick(() => { this.refreshTable = true; }); }, filterNode(value, data) { if (!value) return true; return data.label.toLowerCase().includes(value.toLowerCase()); }, filterProjectTree() { this.$refs.projectTree.filter(this.projectName); }, handleProjectClick(data) { // 保存当前选中的项目ID this.currentProjectId = data.id; // 将serialNums字符串转换为数组 const serialNumsArray = data.serialNums ? data.serialNums.split(',').map(id => id.trim()) : []; // 将数组转换回逗号分隔的字符串用于查询 this.docQueryParams.serialNums = serialNumsArray.join(','); this.getDocumentList(); }, // ================= 项目管理方法 ================= handleAddProject() { this.projectForm = { id: null, name: '', parentId: null, description: '' }; this.projectDialogTitle = '新增项目'; this.projectDialogVisible = true; }, handleAddSubProject(data) { this.projectForm = { id: null, name: '', parentId: data.id, description: '' }; this.projectDialogTitle = '新增子项目'; this.projectDialogVisible = true; }, handleEditProject(data) { this.projectForm = { id: data.id, name: data.name, parentId: data.parentId, description: data.description || '' }; this.projectDialogTitle = '编辑项目'; this.projectDialogVisible = true; }, // 递归收集项目ID collectProjectIds(node) { let ids = [node.id]; if (node.children && node.children.length > 0) { node.children.forEach(child => { ids = ids.concat(this.collectProjectIds(child)); }); } return ids; }, // 修改后的删除项目方法 handleDeleteProject(data) { this.$confirm(确定删除项目 "${data.name}" 及其所有子项目吗?, '提示', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' }).then(() => { // 递归收集所有节点ID const ids = this.collectProjectIds(data); // 调用批量删除API deleteProject(ids).then(response => { if (response.code === 200) { this.$message.success('删除成功'); this.getProjectTree(); } else { this.$message.error(response.msg || '删除失败'); } }).catch(error => { this.$message.error('删除失败: ' + error.message); }); }); }, saveProject() { this.$refs.projectForm.validate(valid => { if (valid) { const saveMethod = this.projectForm.id ? updateProject : saveProject; saveMethod(this.projectForm).then(response => { if (response.code === 200) { this.$message.success('保存成功'); this.projectDialogVisible = false; this.getProjectTree(); } else { this.$message.error(response.msg || '保存失败'); } }).catch(error => { this.$message.error('保存失败: ' + error.message); }); } }); }, normalizer(node) { return { id: node.id, label: node.name, children: node.children && node.children.length > 0 ? node.children : undefined }; }, // 获取项目树数据 getProjectTree() { getProjectTree().then(response => { if (response.code === 200 && response.data) { // 处理根节点 const rootNode = response.data; // 转换数据结构 this.projectTreeData = this.transformTreeData([rootNode]); this.filteredProjectTreeData = [...this.projectTreeData]; // 默认展开根节点 this.$nextTick(() => { if (this.projectTreeData.length > 0) { this.$refs.projectTree.setCurrentKey(rootNode.id); this.handleProjectClick(rootNode); } }); } else { this.$message.error('获取项目树失败: ' + (response.msg || '未知错误')); } }).catch(error => { console.error("获取项目树失败:", error); this.$message.error('获取项目树失败: ' + error.message); }); }, // 转换数据结构为el-tree需要的格式 transformTreeData(nodes) { if (!nodes || !Array.isArray(nodes)) return []; return nodes.map(node => ({ id: node.id, label: node.name, name: node.name, parentId: node.parentId, serialNums: node.serialNums, description: node.description, createBy: node.createBy, createTime: node.createTime, updateBy: node.updateBy, updateTime: node.updateTime, children: this.transformTreeData(node.children || []), rawData: node })); }, // ================= 文档管理方法 ================= // 在 methods 中添加递归收集 serialNum 的方法 collectAllSerialNums(node) { let serialNums = []; // 添加当前节点的 serialNum if (node.serialNums) { const ids = node.serialNums.split(',').map(id => id.trim()); serialNums = [...serialNums, ...ids]; } // 递归处理子节点 if (node.children && node.children.length > 0) { node.children.forEach(child => { serialNums = [...serialNums, ...this.collectAllSerialNums(child)]; }); } return serialNums; }, // 修改后的 getDocumentList 方法 getDocumentList() { // 查找当前选中的项目节点 const currentNode = this.$refs.projectTree.getNode(this.currentProjectId); if (!currentNode || !currentNode.data) { this.$message.error('未找到选中的项目'); return; } // 收集当前节点及其所有子节点的 serialNum const allSerialNums = this.collectAllSerialNums(currentNode.data); // 去重并转换为逗号分隔的字符串 const uniqueSerialNums = [...new Set(allSerialNums)].join(','); // 如果没有找到任何 serialNum,显示提示信息 if (!uniqueSerialNums) { this.documentList = []; this.docTotal = 0; this.$message.info('当前项目及其子项目没有关联任何文档'); return; } // 更新查询参数 this.docQueryParams.serialNums = uniqueSerialNums; this.docLoading = true; getDocuments(this.docQueryParams).then(response => { if (response.code === 200) { this.documentList = response.rows; this.docTotal = response.total; this.selectedDocs = []; // 清空选择 this.ids = []; // 清空ID集合 } else { this.$message.error(response.msg || '获取文档列表失败'); } this.docLoading = false; }).catch(error => { this.$message.error('获取文档列表失败: ' + error.message); this.docLoading = false; }); }, resetDocQuery() { this.docQueryParams.title = ''; this.docQueryParams.status = ''; this.getDocumentList(); }, handleEditDocument(row) { getData(row.id).then(response => { if (response.code === 200) { this.currentDocument = { id: response.data.id, menuId: response.data.menuId, title: response.data.title, content: response.data.content, status: response.data.status }; this.activeView = 'detail'; } else { this.$message.error(response.msg || '获取文档详情失败'); } }).catch(error => { this.$message.error('获取文档详情失败: ' + error.message); }); }, handleDeleteDocument(row) { this.$confirm(确定从项目中移除文档 "${row.title}" 吗?, '提示', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' }).then(() => { // 使用removeMenusFromProject API从项目中移除文档 // 参数: { projectId: 当前项目ID, menusIds: [文档ID] } const params = { projectId: this.currentProjectId, menusIds: [row.id] // 使用文档ID数组 }; removeMenusFromProject(params).then(response => { if (response.code === 200) { this.$message.success('文档已从项目中移除'); this.getDocumentList(); } else { this.$message.error(response.msg || '移除文档失败'); } }).catch(error => { this.$message.error('移除文档失败: ' + error.message); }); }); }, handleRowClick(row) { this.handleEditDocument(row); }, backToList() { this.activeView = 'list'; }, saveDocument() { const saveMethod = this.currentDocument.id ? updateData : addData; saveMethod(this.currentDocument).then(response => { if (response.code === 200) { this.$message.success('保存成功'); this.getDocumentList(); this.backToList(); } else { this.$message.error(response.msg || '保存失败'); } }).catch(error => { this.$message.error('保存失败: ' + error.message); }); }, // ================= 导出功能方法 ================= // 多选处理 handleSelectionChange(selection) { this.selectedDocs = selection; this.ids = selection.map(item => item.id); }, // 导出文档(单篇) handleExportHttp(row, isDetail) { const params = { ids: row.id, notesExportFlag: isDetail ? 1 : 0 }; this.download( 'cms/data/createArticleOutputHttp', params, word文档_${row.title}_${new Date().getTime()}.docx, { timeout: 60000 } ); }, // 批量导出文档 handleBatchExport() { if (this.selectedDocs.length === 0) { this.$message.warning('请选择要导出的文档'); return; } // 弹出选择导出类型的对话框 this.$confirm('请选择导出方式', '提示', { distinguishCancelAndClose: true, confirmButtonText: '导出详细', cancelButtonText: '导出简版', type: 'info' }).then(() => { // 导出详细 this.batchExportHttp(true); }).catch(action => { if (action === 'cancel') { // 导出简版 this.batchExportHttp(false); } }); }, // 批量导出文档实现 batchExportHttp(isDetail) { const params = { ids: this.ids, notesExportFlag: isDetail ? 1 : 0 }; this.download( 'cms/data/createArticleOutputHttp', params, 批量文档_${new Date().getTime()}.zip, { timeout: 120000 } // 批量导出可能需要更长时间 ); }, // 下载方法实现 download(url, params, fileName, config = {}) { // 显示加载提示 const loading = this.$loading({ lock: true, text: '正在生成文档,请稍候...', spinner: 'el-icon-loading', background: 'rgba(0, 0, 0, 0.7)' }); // 发送请求 axios.get(url, { params: params, responseType: 'blob', timeout: config.timeout || 30000, headers: { 'Authorization': Bearer ${this.$store.getters.token} } }) .then(response => { // 创建下载链接 const blob = new Blob([response.data], { type: response.headers['content-type'] }); const downloadUrl = window.URL.createObjectURL(blob); // 创建下载链接 const link = document.createElement('a'); link.href = downloadUrl; link.download = fileName; document.body.appendChild(link); // 触发下载 link.click(); // 清理资源 window.URL.revokeObjectURL(downloadUrl); document.body.removeChild(link); this.$message.success('文档导出成功'); }) .catch(error => { console.error('导出失败:', error); // 处理错误响应 if (error.response) { if (error.response.status === 500) { this.$message.error('导出失败:服务器内部错误'); } else if (error.response.status === 401) { this.$message.error('导出失败:未授权访问'); } else if (error.response.status === 404) { this.$message.error('导出失败:API接口不存在'); } else { this.$message.error(导出失败:服务器错误 (${error.response.status})); } } else if (error.message.includes('timeout')) { this.$message.error('导出超时,请稍后再试'); } else { this.$message.error('导出失败:' + (error.message || '未知错误')); } }) .finally(() => { loading.close(); }); }, // 排序格式化方法 sortableFormatter(row, column) { if (this.ids.includes(row.id)) { return 1; } else { return 2; } }, // 跳转到文章详情页 getArticleInfo(articleId) { // id加密 const articleIdStr = EncryptJs(articleId, "f1827100d08ff039", "ed363078893c0329"); console.log('阅读的文档跳转加密后的ID:'+articleIdStr) let routeUrl = this.$router.resolve({ path: '/cms/doucumentView', query: { id: articleIdStr } }); window.open(routeUrl.href, '_blank'); } } }; </script> <style scoped> .app-container { padding: 20px; background-color: #f5f7fa; } .head-container { padding: 10px; background-color: #ffffff; border-radius: 4px; box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1); } .custom-tree-node { flex: 1; display: flex; align-items: center; justify-content: space-between; font-size: 14px; padding-right: 8px; } .tree-node-content { display: flex; align-items: center; } .tree-icon { margin-right: 8px; color: #409EFF; } .node-label { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .node-actions { display: flex; align-items: center; } .doc-list-container { background-color: #ffffff; padding: 20px; border-radius: 4px; box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1); } .batch-actions { margin-bottom: 15px; } .doc-detail-container { background-color: #ffffff; padding: 20px; border-radius: 4px; box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1); } .doc-header { display: flex; align-items: center; margin-bottom: 20px; } .doc-title { display: flex; align-items: center; margin-left: 15px; margin-bottom: 0; font-size: 18px; color: #303133; } .title-icon { margin-right: 10px; color: #409EFF; } .doc-title .doc-icon { margin-right: 8px; color: #909399; } .el-tree { border: 1px solid #ebeef5; border-radius: 4px; padding: 10px; max-height: 70vh; overflow-y: auto; background-color: #ffffff; box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1); } .custom-tree-node .el-button { padding: 4px; margin-left: 5px; } .el-table { margin-top: 10px; border-radius: 4px; overflow: hidden; box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1); } .el-form-item { margin-bottom: 18px; } .el-tag { margin: 2px; } /* 响应式调整 */ @media (max-width: 768px) { .el-col-xs-24 { width: 100%; margin-bottom: 20px; } .doc-list-container, .doc-detail-container { padding: 10px; } .doc-header h2 { font-size: 16px; } } </style> 点击表格的时候没有触发@click并且帮我优化树的ui,加上图标,帮我修复并给我正确的完整的代码

<template> <el-row :gutter="20"> <el-col :span="6" :xs="24"> <el-input v-model="projectName" placeholder="请输入项目名称" clearable size="mini" prefix-icon="el-icon-search" style="margin-bottom: 20px" @input="filterProjectTree" /> <el-row :gutter="10" class="mb8"> <el-col :span="1.5"> <el-button type="info" plain icon="el-icon-sort" size="mini" @click="toggleExpandAll" >{{ isExpandAll ? '折叠' : '展开' }}</el-button> </el-col> <el-col :span="1.5"> <el-button type="primary" plain icon="el-icon-folder-add" size="mini" @click="handleAddProject" v-hasPermi="['cms:project:add']" >新增项目</el-button> </el-col> </el-row> <el-tree v-if="refreshTable" style="margin-top: 0.8rem;" :data="filteredProjectTreeData" :props="defaultProps" :expand-on-click-node="false" :filter-node-method="filterNode" :highlight-current="true" :default-expand-all="isExpandAll" ref="projectTree" empty-text="加载中,请稍候" node-key="id" @node-click="handleProjectClick" > <el-icon class="tree-icon" :size="16"> <svg-icon v-if="node.expanded" icon-class="folder-open" /> <svg-icon v-else icon-class="folder" /> </el-icon> 25" class="node-label"> <el-tooltip :show-after="300" :content="node.label" placement="top-start"> {{ ellipsis(node.label, 25) }} </el-tooltip> {{ node.label }} <el-button type="text" size="mini" icon="el-icon-edit" v-hasPermi="['cms:project:update']" @click.stop="() => handleEditProject(data)" ></el-button> <el-button type="text" size="mini" icon="el-icon-plus" v-hasPermi="['cms:project:add']" @click.stop="() => handleAddSubProject(data)" ></el-button> <el-button type="text" size="mini" icon="el-icon-delete" v-hasPermi="['cms:project:delete']" @click.stop="() => handleDeleteProject(data)" ></el-button> </el-tree> </el-col> <el-col :span="18" :xs="24"> <el-form :model="docQueryParams" ref="docQueryForm" :inline="true" label-width="68px"> <el-form-item label="文档标题" prop="title"> <el-input v-model="docQueryParams.title" placeholder="请输入文档标题" clearable size="mini" @keyup.enter.native="getDocumentList" prefix-icon="el-icon-search" /> </el-form-item> <el-form-item label="状态" prop="status"> <el-select v-model="docQueryParams.status" placeholder="状态" size="mini" clearable> <el-option label="编制中✍" value="0" /> <el-option label="待评审✊" value="1" /> <el-option label="已评审👍" value="2" /> <el-option label="修改中🔧" value="3" /> <el-option label="开发中💪" value="4" /> <el-option label="已完成开发🆗" value="5" /> </el-select> </el-form-item> <el-form-item> <el-button type="primary" icon="el-icon-search" size="mini" @click="getDocumentList">搜索</el-button> <el-button icon="el-icon-refresh" size="mini" @click="resetDocQuery">重置</el-button> </el-form-item> </el-form> <el-button type="success" icon="el-icon-download" size="mini" :disabled="selectedDocs.length === 0" @click="handleBatchExport" > 批量导出 ({{ selectedDocs.length }}) </el-button> <el-table v-loading="docLoading" :data="documentList" highlight-current-row @row-click="handleRowClick" @selection-change="handleSelectionChange" ref="docTable" > <el-table-column type="selection" width="55" align="center" /> <el-table-column label="序号" type="index" width="50" align="center" /> <el-table-column label="编号" align="center" prop="serialNum" /> <el-table-column label="文档标题" prop="title" align="center"/> <el-table-column label="进度" prop="status" width="100"> <template slot-scope="scope"> <el-tag v-if="scope.row.status === '0'" type="info" size="small">编制中✍</el-tag> <el-tag v-if="scope.row.status === '1'" type="warning" size="small">待评审✊</el-tag> <el-tag v-if="scope.row.status === '2'" type="success" size="small">已评审👍</el-tag> <el-tag v-if="scope.row.status === '3'" type="success" size="small">修改中🔧</el-tag> <el-tag v-if="scope.row.status === '4'" type="success" size="small">开发中💪</el-tag> <el-tag v-if="scope.row.status === '5'" type="success" size="small">已完成开发🆗</el-tag> </template> </el-table-column> <el-table-column label="创建人" prop="createBy" width="100" /> <el-table-column label="负责人" prop="director" width="100"/> <el-table-column label="创建时间" prop="createTime" width="140" /> <el-table-column label="操作" width="180" align="center"> <template slot-scope="scope"> <el-button size="mini" type="text" icon="el-icon-edit" @click.stop="handleEditDocument(scope.row)"></el-button> <el-button size="mini" type="text" icon="el-icon-download" @click.stop="handleExportHttp(scope.row, true)" title="导出详细" ></el-button> <el-button size="mini" type="text" icon="el-icon-download" @click.stop="handleExportHttp(scope.row, false)" title="导出简版" ></el-button> <el-button size="mini" type="text" icon="el-icon-delete" @click.stop="handleDeleteDocument(scope.row)" v-hasPermi="['cms:project:deleteArticleFromProject']"></el-button> </template> </el-table-column> </el-table> 0" :total="docTotal" :page.sync="docQueryParams.pageNum" :limit.sync="docQueryParams.pageSize" @pagination="getDocumentList" /> <el-button type="text" icon="el-icon-back" @click="backToList">返回文档列表</el-button> <el-icon class="title-icon"><svg-icon icon-class="document" /></el-icon> {{ currentDocument.title }} <el-form :model="currentDocument" ref="docForm" label-width="80px"> <el-form-item label="文档内容"> <Tinymce :height='600' v-model='currentDocument.content'></Tinymce> </el-form-item> <el-form-item> <el-button type="primary" icon="el-icon-check" @click="saveDocument">保存</el-button> <el-button icon="el-icon-close" @click="backToList">取消</el-button> </el-form-item> </el-form> </el-col> </el-row> <el-dialog :title="projectDialogTitle" :visible.sync="projectDialogVisible" width="50%"> <el-form :model="projectForm" ref="projectForm" label-width="100px"> <el-form-item label="项目名称" prop="name" required> <el-input v-model="projectForm.name" placeholder="请输入项目名称" prefix-icon="el-icon-folder" /> </el-form-item> <el-form-item label="上级项目" prop="parentId"> <treeselect v-model="projectForm.parentId" :options="projectTreeData" :normalizer="normalizer" placeholder="选择上级项目" /> </el-form-item> <el-form-item label="项目描述" prop="description"> <el-input type="textarea" v-model="projectForm.description" :rows="3" /> </el-form-item> </el-form> <el-button @click="projectDialogVisible = false">取消</el-button> <el-button type="primary" icon="el-icon-check" @click="saveProject">保存</el-button> </el-dialog> </template> <script> import { listData, getData, addData, updateData } from "@/api/cms/data"; import { getProjectTree, saveProject, updateProject, deleteProject, removeMenusFromProject } from "@/api/cms/articleProject"; import Tinymce from '@/components/Tinymce'; import Treeselect from "@riophae/vue-treeselect"; import "@riophae/vue-treeselect/dist/vue-treeselect.css"; import axios from 'axios'; export default { name: "ProjectManagement", components: { Treeselect, Tinymce }, data() { return { // 项目树相关数据 projectName: '', projectTreeData: [], filteredProjectTreeData: [], refreshTable: true, isExpandAll: true, defaultProps: { children: "children", label: "label" }, // 项目对话框相关 projectDialogVisible: false, projectDialogTitle: '', projectForm: { id: null, name: '', parentId: null, description: '' }, // 文档列表相关 activeView: 'list', // 'list' 或 'detail' docQueryParams: { menuIds: null, title: '', status: '', pageNum: 1, pageSize: 10 }, documentList: [], docTotal: 0, docLoading: false, selectedDocs: [], // 选中的文档 ids: [], // 选中文档ID集合 // 文档详情相关 currentDocument: { id: null, menuId: null, title: '', content: '', status: '0' }, // 当前选中的项目ID(用于删除操作) currentProjectId: null, // 导出相关数据 form: { ids: [], // 导出的文档ID集合 notesExportFlag: 1 // 默认导出详细 } }; }, created() { this.getProjectTree(); }, methods: { // ================= 项目树方法 ================= ellipsis(text, maxLength) { return text.length > maxLength ? text.substring(0, maxLength) + '...' : text; }, toggleExpandAll() { this.refreshTable = false; this.isExpandAll = !this.isExpandAll; this.$nextTick(() => { this.refreshTable = true; }); }, filterNode(value, data) { if (!value) return true; return data.label.toLowerCase().includes(value.toLowerCase()); }, filterProjectTree() { this.$refs.projectTree.filter(this.projectName); }, handleProjectClick(data) { // 保存当前选中的项目ID this.currentProjectId = data.id; // 将menuIds字符串转换为数组 const menuIdsArray = data.menuIds ? data.menuIds.split(',').map(id => id.trim()) : []; // 将数组转换回逗号分隔的字符串用于查询 this.docQueryParams.menuIds = menuIdsArray.join(','); this.getDocumentList(); }, // ================= 项目管理方法 ================= handleAddProject() { this.projectForm = { id: null, name: '', parentId: null, description: '' }; this.projectDialogTitle = '新增项目'; this.projectDialogVisible = true; }, handleAddSubProject(data) { this.projectForm = { id: null, name: '', parentId: data.id, description: '' }; this.projectDialogTitle = '新增子项目'; this.projectDialogVisible = true; }, handleEditProject(data) { this.projectForm = { id: data.id, name: data.name, parentId: data.parentId, description: data.description || '' }; this.projectDialogTitle = '编辑项目'; this.projectDialogVisible = true; }, handleDeleteProject(data) { this.$confirm(确定删除项目 "${data.name}" 及其所有子项目吗?, '提示', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' }).then(() => { // 使用deleteProject API删除项目 deleteProject(data.id).then(response => { if (response.code === 200) { this.$message.success('删除成功'); this.getProjectTree(); } else { this.$message.error(response.msg || '删除失败'); } }).catch(error => { this.$message.error('删除失败: ' + error.message); }); }); }, saveProject() { this.$refs.projectForm.validate(valid => { if (valid) { const saveMethod = this.projectForm.id ? updateProject : saveProject; saveMethod(this.projectForm).then(response => { if (response.code === 200) { this.$message.success('保存成功'); this.projectDialogVisible = false; this.getProjectTree(); } else { this.$message.error(response.msg || '保存失败'); } }).catch(error => { this.$message.error('保存失败: ' + error.message); }); } }); }, normalizer(node) { return { id: node.id, label: node.name, children: node.children && node.children.length > 0 ? node.children : undefined }; }, // 获取项目树数据 getProjectTree() { getProjectTree().then(response => { if (response.code === 200 && response.data) { // 处理根节点 const rootNode = response.data; // 转换数据结构 this.projectTreeData = this.transformTreeData([rootNode]); this.filteredProjectTreeData = [...this.projectTreeData]; // 默认展开根节点 this.$nextTick(() => { if (this.projectTreeData.length > 0) { this.$refs.projectTree.setCurrentKey(rootNode.id); this.handleProjectClick(rootNode); } }); } else { this.$message.error('获取项目树失败: ' + (response.msg || '未知错误')); } }).catch(error => { console.error("获取项目树失败:", error); this.$message.error('获取项目树失败: ' + error.message); }); }, // 转换数据结构为el-tree需要的格式 transformTreeData(nodes) { if (!nodes || !Array.isArray(nodes)) return []; return nodes.map(node => ({ id: node.id, label: node.name, name: node.name, parentId: node.parentId, menuIds: node.menuIds, description: node.description, createBy: node.createBy, createTime: node.createTime, updateBy: node.updateBy, updateTime: node.updateTime, children: this.transformTreeData(node.children || []), rawData: node })); }, // ================= 文档管理方法 ================= getDocumentList() { if (!this.docQueryParams.menuIds) { return; } this.docLoading = true; listData(this.docQueryParams).then(response => { if (response.code === 200) { this.documentList = response.rows; this.docTotal = response.total; this.selectedDocs = []; // 清空选择 this.ids = []; // 清空ID集合 } else { this.$message.error(response.msg || '获取文档列表失败'); } this.docLoading = false; }).catch(error => { this.$message.error('获取文档列表失败: ' + error.message); this.docLoading = false; }); }, resetDocQuery() { this.docQueryParams.title = ''; this.docQueryParams.status = ''; this.getDocumentList(); }, handleEditDocument(row) { getData(row.id).then(response => { if (response.code === 200) { this.currentDocument = { id: response.data.id, menuId: response.data.menuId, title: response.data.title, content: response.data.content, status: response.data.status }; this.activeView = 'detail'; } else { this.$message.error(response.msg || '获取文档详情失败'); } }).catch(error => { this.$message.error('获取文档详情失败: ' + error.message); }); }, handleDeleteDocument(row) { this.$confirm(确定从项目中移除文档 "${row.title}" 吗?, '提示', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' }).then(() => { // 使用removeMenusFromProject API从项目中移除文档 // 参数: { projectId: 当前项目ID, menusIds: [文档ID] } const params = { projectId: this.currentProjectId, menusIds: [row.id] // 使用文档ID数组 }; removeMenusFromProject(params).then(response => { if (response.code === 200) { this.$message.success('文档已从项目中移除'); this.getDocumentList(); } else { this.$message.error(response.msg || '移除文档失败'); } }).catch(error => { this.$message.error('移除文档失败: ' + error.message); }); }); }, handleRowClick(row) { this.handleEditDocument(row); }, backToList() { this.activeView = 'list'; }, saveDocument() { const saveMethod = this.currentDocument.id ? updateData : addData; saveMethod(this.currentDocument).then(response => { if (response.code === 200) { this.$message.success('保存成功'); this.getDocumentList(); this.backToList(); } else { this.$message.error(response.msg || '保存失败'); } }).catch(error => { this.$message.error('保存失败: ' + error.message); }); }, // ================= 导出功能方法 ================= // 多选处理 handleSelectionChange(selection) { this.selectedDocs = selection; this.ids = selection.map(item => item.id); }, // 导出文档(单篇) handleExportHttp(row, isDetail) { const params = { ids: row.id, notesExportFlag: isDetail ? 1 : 0 }; this.download( 'cms/data/createArticleOutputHttp', params, word文档_${row.title}_${new Date().getTime()}.docx, { timeout: 60000 } ); }, // 批量导出文档 handleBatchExport() { if (this.selectedDocs.length === 0) { this.$message.warning('请选择要导出的文档'); return; } // 弹出选择导出类型的对话框 this.$confirm('请选择导出方式', '提示', { distinguishCancelAndClose: true, confirmButtonText: '导出详细', cancelButtonText: '导出简版', type: 'info' }).then(() => { // 导出详细 this.batchExportHttp(true); }).catch(action => { if (action === 'cancel') { // 导出简版 this.batchExportHttp(false); } }); }, // 批量导出文档实现 batchExportHttp(isDetail) { const params = { ids: this.ids, notesExportFlag: isDetail ? 1 : 0 }; this.download( 'cms/data/createArticleOutputHttp', params, 批量文档_${new Date().getTime()}.zip, { timeout: 120000 } // 批量导出可能需要更长时间 ); }, // 下载方法实现 download(url, params, fileName, config = {}) { // 显示加载提示 const loading = this.$loading({ lock: true, text: '正在生成文档,请稍候...', spinner: 'el-icon-loading', background: 'rgba(0, 0, 0, 0.7)' }); // 发送请求 axios.get(url, { params: params, responseType: 'blob', timeout: config.timeout || 30000, headers: { 'Authorization': Bearer ${this.$store.getters.token} } }) .then(response => { // 创建下载链接 const blob = new Blob([response.data], { type: response.headers['content-type'] }); const downloadUrl = window.URL.createObjectURL(blob); // 创建下载链接 const link = document.createElement('a'); link.href = downloadUrl; link.download = fileName; document.body.appendChild(link); // 触发下载 link.click(); // 清理资源 window.URL.revokeObjectURL(downloadUrl); document.body.removeChild(link); this.$message.success('文档导出成功'); }) .catch(error => { console.error('导出失败:', error); // 处理错误响应 if (error.response) { if (error.response.status === 500) { this.$message.error('导出失败:服务器内部错误'); } else if (error.response.status === 401) { this.$message.error('导出失败:未授权访问'); } else if (error.response.status === 404) { this.$message.error('导出失败:API接口不存在'); } else { this.$message.error(导出失败:服务器错误 (${error.response.status})); } } else if (error.message.includes('timeout')) { this.$message.error('导出超时,请稍后再试'); } else { this.$message.error('导出失败:' + (error.message || '未知错误')); } }) .finally(() => { loading.close(); }); }, // 排序格式化方法 sortableFormatter(row, column) { if (this.ids.includes(row.id)) { return 1; } else { return 2; } } } }; </script> <style scoped> .app-container { padding: 20px; background-color: #f5f7fa; } .head-container { padding: 10px; background-color: #ffffff; border-radius: 4px; box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1); } .custom-tree-node { flex: 1; display: flex; align-items: center; justify-content: space-between; font-size: 14px; padding-right: 8px; } .tree-node-content { display: flex; align-items: center; } .tree-icon { margin-right: 8px; color: #409EFF; } .node-label { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .node-actions { display: flex; align-items: center; } .doc-list-container { background-color: #ffffff; padding: 20px; border-radius: 4px; box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1); } .batch-actions { margin-bottom: 15px; } .doc-detail-container { background-color: #ffffff; padding: 20px; border-radius: 4px; box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1); } .doc-header { display: flex; align-items: center; margin-bottom: 20px; } .doc-title { display: flex; align-items: center; margin-left: 15px; margin-bottom: 0; font-size: 18px; color: #303133; } .title-icon { margin-right: 10px; color: #409EFF; } .doc-title .doc-icon { margin-right: 8px; color: #909399; } .el-tree { border: 1px solid #ebeef5; border-radius: 4px; padding: 10px; max-height: 70vh; overflow-y: auto; background-color: #ffffff; box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1); } .custom-tree-node .el-button { padding: 4px; margin-left: 5px; } .el-table { margin-top: 10px; border-radius: 4px; overflow: hidden; box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1); } .el-form-item { margin-bottom: 18px; } .el-tag { margin: 2px; } /* 响应式调整 */ @media (max-width: 768px) { .el-col-xs-24 { width: 100%; margin-bottom: 20px; } .doc-list-container, .doc-detail-container { padding: 10px; } .doc-header h2 { font-size: 16px; } } </style> 原本代码是这样的,如何改

<template> <el-row :gutter="20"> <el-col :span="6" :xs="24"> <el-input v-model="projectName" placeholder="请输入项目名称" clearable size="mini" prefix-icon="el-icon-search" style="margin-bottom: 20px" @input="filterProjectTree" /> <el-row :gutter="10" class="mb8"> <el-col :span="1.5"> <el-button type="info" plain icon="el-icon-sort" size="mini" @click="toggleExpandAll" >{{ isExpandAll ? '折叠' : '展开' }}</el-button> </el-col> <el-col :span="1.5"> <el-button type="primary" plain icon="el-icon-folder-add" size="mini" @click="handleAddProject" v-hasPermi="['cms:project:add']" >新增项目</el-button> </el-col> </el-row> <el-tree v-if="refreshTable" style="margin-top: 0.8rem;" :data="filteredProjectTreeData" :props="defaultProps" :expand-on-click-node="false" :filter-node-method="filterNode" :highlight-current="true" :default-expand-all="isExpandAll" ref="projectTree" empty-text="加载中,请稍候" node-key="id" @node-click="handleProjectClick" > <el-icon class="tree-icon" :size="16"> <svg-icon v-if="node.expanded" icon-class="folder-open" /> <svg-icon v-else icon-class="folder" /> </el-icon> 25" class="node-label"> <el-tooltip :show-after="300" :content="node.label" placement="top-start"> {{ ellipsis(node.label, 25) }} </el-tooltip> {{ node.label }} <el-button type="text" size="mini" icon="el-icon-edit" v-hasPermi="['cms:project:update']" @click.stop="() => handleEditProject(data)" ></el-button> <el-button type="text" size="mini" icon="el-icon-plus" v-hasPermi="['cms:project:add']" @click.stop="() => handleAddSubProject(data)" ></el-button> <el-button type="text" size="mini" icon="el-icon-delete" v-hasPermi="['cms:project:delete']" @click.stop="() => handleDeleteProject(data)" ></el-button> </el-tree> </el-col> <el-col :span="18" :xs="24"> <el-form :model="docQueryParams" ref="docQueryForm" :inline="true" label-width="68px"> <el-form-item label="文档标题" prop="title"> <el-input v-model="docQueryParams.title" placeholder="请输入文档标题" clearable size="mini" @keyup.enter.native="getDocumentList" prefix-icon="el-icon-search" /> </el-form-item> <el-form-item label="状态" prop="status"> <el-select v-model="docQueryParams.status" placeholder="状态" size="mini" clearable> <el-option label="编制中✍" value="0" /> <el-option label="待评审✊" value="1" /> <el-option label="已评审👍" value="2" /> <el-option label="修改中🔧" value="3" /> <el-option label="开发中💪" value="4" /> <el-option label="已完成开发🆗" value="5" /> </el-select> </el-form-item> <el-form-item> <el-button type="primary" icon="el-icon-search" size="mini" @click="getDocumentList">搜索</el-button> <el-button icon="el-icon-refresh" size="mini" @click="resetDocQuery">重置</el-button> </el-form-item> </el-form> <el-button type="success" icon="el-icon-download" size="mini" :disabled="selectedDocs.length === 0" @click="handleBatchExport" > 批量导出 ({{ selectedDocs.length }}) </el-button> <el-table v-loading="docLoading" :data="documentList" highlight-current-row @row-click="handleRowClick" @selection-change="handleSelectionChange" ref="docTable" > <el-table-column type="selection" width="55" align="center" /> <el-table-column label="序号" type="index" width="50" align="center" /> <el-table-column label="编号" align="center" prop="serialNum" /> <el-table-column label="文档标题" prop="title" align="center"/> <el-table-column label="进度" prop="status" width="100"> <template slot-scope="scope"> <el-tag v-if="scope.row.status === '0'" type="info" size="small">编制中✍</el-tag> <el-tag v-if="scope.row.status === '1'" type="warning" size="small">待评审✊</el-tag> <el-tag v-if="scope.row.status === '2'" type="success" size="small">已评审👍</el-tag> <el-tag v-if="scope.row.status === '3'" type="success" size="small">修改中🔧</el-tag> <el-tag v-if="scope.row.status === '4'" type="success" size="small">开发中💪</el-tag> <el-tag v-if="scope.row.status === '5'" type="success" size="small">已完成开发🆗</el-tag> </template> </el-table-column> <el-table-column label="创建人" prop="createBy" width="100" /> <el-table-column label="负责人" prop="director" width="100"/> <el-table-column label="创建时间" prop="createTime" width="140" /> <el-table-column label="操作" width="180" align="center"> <template slot-scope="scope"> <el-button size="mini" type="text" icon="el-icon-edit" @click.stop="handleEditDocument(scope.row)"></el-button> <el-button size="mini" type="text" icon="el-icon-download" @click.stop="handleExportHttp(scope.row, true)" title="导出详细" ></el-button> <el-button size="mini" type="text" icon="el-icon-download" @click.stop="handleExportHttp(scope.row, false)" title="导出简版" ></el-button> <el-button size="mini" type="text" icon="el-icon-delete" @click.stop="handleDeleteDocument(scope.row)" v-hasPermi="['cms:project:deleteArticleFromProject']"></el-button> </template> </el-table-column> </el-table> 0" :total="docTotal" :page.sync="docQueryParams.pageNum" :limit.sync="docQueryParams.pageSize" @pagination="getDocumentList" /> <el-button type="text" icon="el-icon-back" @click="backToList">返回文档列表</el-button> <el-icon class="title-icon"><svg-icon icon-class="document" /></el-icon> {{ currentDocument.title }} <el-form :model="currentDocument" ref="docForm" label-width="80px"> <el-form-item label="文档内容"> <Tinymce :height='600' v-model='currentDocument.content'></Tinymce> </el-form-item> <el-form-item> <el-button type="primary" icon="el-icon-check" @click="saveDocument">保存</el-button> <el-button icon="el-icon-close" @click="backToList">取消</el-button> </el-form-item> </el-form> </el-col> </el-row> <el-dialog :title="projectDialogTitle" :visible.sync="projectDialogVisible" width="50%"> <el-form :model="projectForm" ref="projectForm" label-width="100px"> <el-form-item label="项目名称" prop="name" required> <el-input v-model="projectForm.name" placeholder="请输入项目名称" prefix-icon="el-icon-folder" /> </el-form-item> <el-form-item label="上级项目" prop="parentId"> <treeselect v-model="projectForm.parentId" :options="projectTreeData" :normalizer="normalizer" placeholder="选择上级项目" /> </el-form-item> <el-form-item label="项目描述" prop="description"> <el-input type="textarea" v-model="projectForm.description" :rows="3" /> </el-form-item> </el-form> <el-button @click="projectDialogVisible = false">取消</el-button> <el-button type="primary" icon="el-icon-check" @click="saveProject">保存</el-button> </el-dialog> </template> <script> import { listData, getData, addData, updateData } from "@/api/cms/data"; import { getProjectTree, saveProject, updateProject, deleteProject, removeMenusFromProject } from "@/api/cms/articleProject"; import Tinymce from '@/components/Tinymce'; import Treeselect from "@riophae/vue-treeselect"; import "@riophae/vue-treeselect/dist/vue-treeselect.css"; import axios from 'axios'; export default { name: "ProjectManagement", components: { Treeselect, Tinymce }, data() { return { // 项目树相关数据 projectName: '', projectTreeData: [], filteredProjectTreeData: [], refreshTable: true, isExpandAll: true, defaultProps: { children: "children", label: "label" }, // 项目对话框相关 projectDialogVisible: false, projectDialogTitle: '', projectForm: { id: null, name: '', parentId: null, description: '' }, // 文档列表相关 activeView: 'list', // 'list' 或 'detail' docQueryParams: { menuIds: null, title: '', status: '', pageNum: 1, pageSize: 10 }, documentList: [], docTotal: 0, docLoading: false, selectedDocs: [], // 选中的文档 ids: [], // 选中文档ID集合 // 文档详情相关 currentDocument: { id: null, menuId: null, title: '', content: '', status: '0' }, // 当前选中的项目ID(用于删除操作) currentProjectId: null, // 导出相关数据 form: { ids: [], // 导出的文档ID集合 notesExportFlag: 1 // 默认导出详细 } }; }, created() { this.getProjectTree(); }, methods: { // ================= 项目树方法 ================= ellipsis(text, maxLength) { return text.length > maxLength ? text.substring(0, maxLength) + '...' : text; }, toggleExpandAll() { this.refreshTable = false; this.isExpandAll = !this.isExpandAll; this.$nextTick(() => { this.refreshTable = true; }); }, filterNode(value, data) { if (!value) return true; return data.label.toLowerCase().includes(value.toLowerCase()); }, filterProjectTree() { this.$refs.projectTree.filter(this.projectName); }, handleProjectClick(data) { // 保存当前选中的项目ID this.currentProjectId = data.id; // 将menuIds字符串转换为数组 const menuIdsArray = data.menuIds ? data.menuIds.split(',').map(id => id.trim()) : []; // 将数组转换回逗号分隔的字符串用于查询 this.docQueryParams.menuIds = menuIdsArray.join(','); this.getDocumentList(); }, // ================= 项目管理方法 ================= handleAddProject() { this.projectForm = { id: null, name: '', parentId: null, description: '' }; this.projectDialogTitle = '新增项目'; this.projectDialogVisible = true; }, handleAddSubProject(data) { this.projectForm = { id: null, name: '', parentId: data.id, description: '' }; this.projectDialogTitle = '新增子项目'; this.projectDialogVisible = true; }, handleEditProject(data) { this.projectForm = { id: data.id, name: data.name, parentId: data.parentId, description: data.description || '' }; this.projectDialogTitle = '编辑项目'; this.projectDialogVisible = true; }, handleDeleteProject(data) { this.$confirm(确定删除项目 "${data.name}" 及其所有子项目吗?, '提示', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' }).then(() => { // 使用deleteProject API删除项目 deleteProject(data.id).then(response => { if (response.code === 200) { this.$message.success('删除成功'); this.getProjectTree(); } else { this.$message.error(response.msg || '删除失败'); } }).catch(error => { this.$message.error('删除失败: ' + error.message); }); }); }, saveProject() { this.$refs.projectForm.validate(valid => { if (valid) { const saveMethod = this.projectForm.id ? updateProject : saveProject; saveMethod(this.projectForm).then(response => { if (response.code === 200) { this.$message.success('保存成功'); this.projectDialogVisible = false; this.getProjectTree(); } else { this.$message.error(response.msg || '保存失败'); } }).catch(error => { this.$message.error('保存失败: ' + error.message); }); } }); }, normalizer(node) { return { id: node.id, label: node.name, children: node.children && node.children.length > 0 ? node.children : undefined }; }, // 获取项目树数据 getProjectTree() { getProjectTree().then(response => { if (response.code === 200 && response.data) { // 处理根节点 const rootNode = response.data; // 转换数据结构 this.projectTreeData = this.transformTreeData([rootNode]); this.filteredProjectTreeData = [...this.projectTreeData]; // 默认展开根节点 this.$nextTick(() => { if (this.projectTreeData.length > 0) { this.$refs.projectTree.setCurrentKey(rootNode.id); this.handleProjectClick(rootNode); } }); } else { this.$message.error('获取项目树失败: ' + (response.msg || '未知错误')); } }).catch(error => { console.error("获取项目树失败:", error); this.$message.error('获取项目树失败: ' + error.message); }); }, // 转换数据结构为el-tree需要的格式 transformTreeData(nodes) { if (!nodes || !Array.isArray(nodes)) return []; return nodes.map(node => ({ id: node.id, label: node.name, name: node.name, parentId: node.parentId, menuIds: node.menuIds, description: node.description, createBy: node.createBy, createTime: node.createTime, updateBy: node.updateBy, updateTime: node.updateTime, children: this.transformTreeData(node.children || []), rawData: node })); }, // ================= 文档管理方法 ================= getDocumentList() { if (!this.docQueryParams.menuIds) { return; } this.docLoading = true; listData(this.docQueryParams).then(response => { if (response.code === 200) { this.documentList = response.rows; this.docTotal = response.total; this.selectedDocs = []; // 清空选择 this.ids = []; // 清空ID集合 } else { this.$message.error(response.msg || '获取文档列表失败'); } this.docLoading = false; }).catch(error => { this.$message.error('获取文档列表失败: ' + error.message); this.docLoading = false; }); }, resetDocQuery() { this.docQueryParams.title = ''; this.docQueryParams.status = ''; this.getDocumentList(); }, handleEditDocument(row) { getData(row.id).then(response => { if (response.code === 200) { this.currentDocument = { id: response.data.id, menuId: response.data.menuId, title: response.data.title, content: response.data.content, status: response.data.status }; this.activeView = 'detail'; } else { this.$message.error(response.msg || '获取文档详情失败'); } }).catch(error => { this.$message.error('获取文档详情失败: ' + error.message); }); }, handleDeleteDocument(row) { this.$confirm(确定从项目中移除文档 "${row.title}" 吗?, '提示', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' }).then(() => { // 使用removeMenusFromProject API从项目中移除文档 // 参数: { projectId: 当前项目ID, menusIds: [文档ID] } const params = { projectId: this.currentProjectId, menusIds: [row.id] // 使用文档ID数组 }; removeMenusFromProject(params).then(response => { if (response.code === 200) { this.$message.success('文档已从项目中移除'); this.getDocumentList(); } else { this.$message.error(response.msg || '移除文档失败'); } }).catch(error => { this.$message.error('移除文档失败: ' + error.message); }); }); }, handleRowClick(row) { this.handleEditDocument(row); }, backToList() { this.activeView = 'list'; }, saveDocument() { const saveMethod = this.currentDocument.id ? updateData : addData; saveMethod(this.currentDocument).then(response => { if (response.code === 200) { this.$message.success('保存成功'); this.getDocumentList(); this.backToList(); } else { this.$message.error(response.msg || '保存失败'); } }).catch(error => { this.$message.error('保存失败: ' + error.message); }); }, // ================= 导出功能方法 ================= // 多选处理 handleSelectionChange(selection) { this.selectedDocs = selection; this.ids = selection.map(item => item.id); }, // 导出文档(单篇) handleExportHttp(row, isDetail) { const params = { ids: row.id, notesExportFlag: isDetail ? 1 : 0 }; this.download( 'cms/data/createArticleOutputHttp', params, word文档_${row.title}_${new Date().getTime()}.docx, { timeout: 60000 } ); }, // 批量导出文档 handleBatchExport() { if (this.selectedDocs.length === 0) { this.$message.warning('请选择要导出的文档'); return; } // 弹出选择导出类型的对话框 this.$confirm('请选择导出方式', '提示', { distinguishCancelAndClose: true, confirmButtonText: '导出详细', cancelButtonText: '导出简版', type: 'info' }).then(() => { // 导出详细 this.batchExportHttp(true); }).catch(action => { if (action === 'cancel') { // 导出简版 this.batchExportHttp(false); } }); }, // 批量导出文档实现 batchExportHttp(isDetail) { const params = { ids: this.ids, notesExportFlag: isDetail ? 1 : 0 }; this.download( 'cms/data/createArticleOutputHttp', params, 批量文档_${new Date().getTime()}.zip, { timeout: 120000 } // 批量导出可能需要更长时间 ); }, // 下载方法实现 download(url, params, fileName, config = {}) { // 显示加载提示 const loading = this.$loading({ lock: true, text: '正在生成文档,请稍候...', spinner: 'el-icon-loading', background: 'rgba(0, 0, 0, 0.7)' }); // 发送请求 axios.get(url, { params: params, responseType: 'blob', timeout: config.timeout || 30000, headers: { 'Authorization': Bearer ${this.$store.getters.token} } }) .then(response => { // 创建下载链接 const blob = new Blob([response.data], { type: response.headers['content-type'] }); const downloadUrl = window.URL.createObjectURL(blob); // 创建下载链接 const link = document.createElement('a'); link.href = downloadUrl; link.download = fileName; document.body.appendChild(link); // 触发下载 link.click(); // 清理资源 window.URL.revokeObjectURL(downloadUrl); document.body.removeChild(link); this.$message.success('文档导出成功'); }) .catch(error => { console.error('导出失败:', error); // 处理错误响应 if (error.response) { if (error.response.status === 500) { this.$message.error('导出失败:服务器内部错误'); } else if (error.response.status === 401) { this.$message.error('导出失败:未授权访问'); } else if (error.response.status === 404) { this.$message.error('导出失败:API接口不存在'); } else { this.$message.error(导出失败:服务器错误 (${error.response.status})); } } else if (error.message.includes('timeout')) { this.$message.error('导出超时,请稍后再试'); } else { this.$message.error('导出失败:' + (error.message || '未知错误')); } }) .finally(() => { loading.close(); }); }, // 排序格式化方法 sortableFormatter(row, column) { if (this.ids.includes(row.id)) { return 1; } else { return 2; } } } }; </script> <style scoped> .app-container { padding: 20px; background-color: #f5f7fa; } .head-container { padding: 10px; background-color: #ffffff; border-radius: 4px; box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1); } .custom-tree-node { flex: 1; display: flex; align-items: center; justify-content: space-between; font-size: 14px; padding-right: 8px; } .tree-node-content { display: flex; align-items: center; } .tree-icon { margin-right: 8px; color: #409EFF; } .node-label { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .node-actions { display: flex; align-items: center; } .doc-list-container { background-color: #ffffff; padding: 20px; border-radius: 4px; box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1); } .batch-actions { margin-bottom: 15px; } .doc-detail-container { background-color: #ffffff; padding: 20px; border-radius: 4px; box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1); } .doc-header { display: flex; align-items: center; margin-bottom: 20px; } .doc-title { display: flex; align-items: center; margin-left: 15px; margin-bottom: 0; font-size: 18px; color: #303133; } .title-icon { margin-right: 10px; color: #409EFF; } .doc-title .doc-icon { margin-right: 8px; color: #909399; } .el-tree { border: 1px solid #ebeef5; border-radius: 4px; padding: 10px; max-height: 70vh; overflow-y: auto; background-color: #ffffff; box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1); } .custom-tree-node .el-button { padding: 4px; margin-left: 5px; } .el-table { margin-top: 10px; border-radius: 4px; overflow: hidden; box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1); } .el-form-item { margin-bottom: 18px; } .el-tag { margin: 2px; } /* 响应式调整 */ @media (max-width: 768px) { .el-col-xs-24 { width: 100%; margin-bottom: 20px; } .doc-list-container, .doc-detail-container { padding: 10px; } .doc-header h2 { font-size: 16px; } } </style> 帮我修改,handleDeleteProject应该是将当前节点的id和所有子节点的id转成list传给后端

<script setup> import {computed, ref, nextTick, watch} from 'vue' import { useI18n } from 'vue-i18n' import * as XLSX from 'xlsx/xlsx.mjs' import { useAppStore } from '@/stores/app.js' import { useThemeStore } from '@/stores/theme.js' import { useWellStore } from '@/stores/well.js' import Card from '@/components/layout/Card.vue' import WellTrajectory from '@/components/BoetWidgets/3d/WellTrajectory.vue' import ImportDialog from './import-dialog/ImportDialog.vue' import InterpolateDialog from './interpolate-dialog/InterpolateDialog.vue' import TrackPrediction from './track-prediction/TrackPrediction.vue' import WaitDrillTrackDesignDialog from './wait-drill-track-design-dialog/WaitDrillTrackDesignDialog.vue' import HitTargetPredictionDialog from './hit-target-prediction-dialog/HitTargetPredictionDialog.vue' import EditDialog from './edit-dialog/EditDialog.vue' import {deepClone, exportExcel, formatTimeString, formatDecimal} from '@/common/util.js' import BaseFrame from '@/components/layout/BaseFrame.vue' import {Plus, Check, Close, Delete, Download, DataLine, Aim, EditPen, Share, Upload} from '@element-plus/icons-vue' import {ElLoading, ElMessage} from 'element-plus' import {getDrilledTracks, deleteDrilledTrack, getDrilledTrackDatas, saveDrilledTrackData, deleteDrilledTrackData} from '@/api/modules/drilled-track.js' const { t } = useI18n() const appStore = useAppStore() const themeStore = useThemeStore() const wellStore = useWellStore() const well = computed(()=>{ return wellStore.getWell() }) const rowIndex = ref(null) // 表格行 const selectRow = ref(null) // 选中的行 const importDialogVisible = ref(false) const interpolateDialogVisible = ref(false) const waitDrillTrackDesignDialogVisible = ref(false) const hitTargetPredictionDialogVisible = ref(false) const isAdd = ref(true) const editDialogVisible = ref(false) const tableRef = ref(null) const drilledTracks = ref([]) const importSource = ref('file') // 新增:默认是文件导入 // 实钻轨迹 const handleGetDrilledTracks = ()=>{ const params = { wellId: well.value.wellId, wellBoreId: well.value.wellBoreId } getDrilledTracks(params).then(res=>{ if (res) { drilledTracks.value = res // 使用 nextTick 确保表格已经渲染 nextTick(()=>{ // 确保表格引用存在且有数据 if (tableRef.value && drilledTracks.value.length) { const firstRow = drilledTracks.value[0] // 默认选中第一行 tableRef.value.setCurrentRow(firstRow) } }) } }) } // 点击行时,设置当前行 const currentRow = ref(null) const handleCurrentChange = (val)=>{ currentRow.value = val handleGetDrilledTrackDatas() } const drilledTrackDatas = ref([]) const drilledTrackGraphTempDatas = ref([]) // 实钻轨迹数据 const handleGetDrilledTrackDatas = ()=>{ const params = { wellId: well.value.wellId, wellBoreId: well.value.wellBoreId, drilledTrackId: currentRow.value.id } getDrilledTrackDatas(params).then(res=>{ drilledTrackDatas.value = res drilledTrackGraphTempDatas.value = res }) } // 删除待钻轨迹 const handleDeleteDrilledTrack = (idx, row)=>{ const params = { wellId: row.wellId, drilledTrackId: row.id } deleteDrilledTrack(params).then(()=>{ drilledTracks.value.splice(idx, 1) if (drilledTracks.value.length) { const firstRow = drilledTracks.value[0] // 默认选中第一行 tableRef.value.setCurrentRow(firstRow) } else { drilledTrackDatas.value = [] } }) } // 新增 const handleAdd = (idx)=>{ // rowIndex.value = idx + 1 // drilledTrackDatas.value.splice(idx + 1, 0, {md: null, inc: null, azi: null}) const newIndex = idx + 1 drilledTrackDatas.value.splice(newIndex, 0, {md: null, inc: null, azi: null}) // 设置新行编辑状态 editingRows.value.push(newIndex) selectRows.value[newIndex] = deepClone(drilledTrackDatas.value[newIndex]) } // 编辑 const handleUpdate = (idx)=>{ if (!editingRows.value.includes(idx)) { editingRows.value.push(idx) selectRows.value[idx] = deepClone(drilledTrackDatas.value[idx]) } } // 取消编辑 const handleCancel = (idx, row)=>{ if (!row.id) { // 新增行取消 drilledTrackDatas.value.splice(idx, 1) } // 从编辑状态中移除 const indexInEditing = editingRows.value.indexOf(idx) if (indexInEditing !== -1) { editingRows.value.splice(indexInEditing, 1) delete selectRows.value[idx] } } // 保存数据 const handleComplete = async(idx)=>{ const row = drilledTrackDatas.value[idx] // 获取相邻行的值(考虑编辑状态) const prevRow = idx > 0 ? drilledTrackDatas.value[idx - 1] : null const nextRow = idx < drilledTrackDatas.value.length - 1 ? drilledTrackDatas.value[idx + 1] : null // 两个深度之间的间隔不能小于这个值 const eps = 1e-8 // 验证测深值必须大于上一行 if (prevRow && prevRow.md !== null && row.md - prevRow.md <= eps) { ElMessage.warning('测深必须大于上一个值') return false } // 验证测深值必须小于下一行 if (nextRow && nextRow.md !== null && row.md - nextRow.md >= eps) { ElMessage.warning('测深必须小于下一个值') return false } const nonEmptyNewRows = drilledTrackDatas.value .slice(idx + 1) // 获取当前行之后的行 .filter(r=>!r.id && // 新增行(没有ID) editingRows.value.includes(drilledTrackDatas.value.indexOf(r)) && // 处于编辑状态 (r.md !== null || r.inc !== null || r.azi !== null) // 至少有一个字段非空 ) .map(r=>({ ...r })) // 深拷贝 // console.log(nonEmptyNewRows) const postData = { id: row.id, wellId: well.value.wellId, wellBoreId: well.value.wellBoreId, drilledTrackId: currentRow.value.id, md: row.md, inc: row.inc, azi: row.azi } try { await saveDrilledTrackData(postData) // 从编辑状态中移除 const indexInEditing = editingRows.value.indexOf(idx) // console.log(indexInEditing) if (indexInEditing !== -1) { editingRows.value.splice(indexInEditing, 1) delete selectRows.value[idx] } // 重新加载数据 await handleGetDrilledTrackDatas() // +++ 新增:重新添加非空的新行并恢复编辑状态 +++ nonEmptyNewRows.forEach(newRow=>{ console.log(nonEmptyNewRows) const newIndex = drilledTrackDatas.value.length - 1 console.log(newIndex) console.log(drilledTrackDatas) drilledTrackDatas.value.push(newRow) console.log(newRow) editingRows.value.push(newIndex) console.log(editingRows.value.push(newIndex)) selectRows.value[newIndex] = deepClone(newRow) console.log(selectRows) }) } catch (error) { console.error('保存失败:', error) ElMessage.error('保存失败') } } // 删除轨迹数据 const handleDeleteDrilledTrackData = (row)=>{ const params = { wellId: well.value.wellId, drilledTrackDataId: row.id } deleteDrilledTrackData(params).then(()=>{ handleGetDrilledTrackDatas() }) } // 3D轨迹配置 const drilledTrackGraphConfig = ref({ skin: { blackTheme: themeStore.isDark}, showLegend: false, showScaleSelector: false, showTubeSizeSlider: true, tubeSize: 0 }) // 图形数据 const drilledTrackGraphDatas = computed(()=>{ if (!drilledTrackGraphTempDatas.value) { return null } // 过滤新录入的数据 const filterData = drilledTrackGraphTempDatas.value.filter(t=>t.id) const data = filterData.map(({ ns: x, ew: y, ...rest })=>({ x, y, ...rest })) return [ { 'code': 0, 'isDrilled': true, 'data': data, 'name': '实钻轨迹', 'isShow': true } ] }) const excelData = ref([]) const excelFile = ref(null) const loading = ref(null) // 导入轨迹数据 const handleImportDrilledTrackData = (ev)=>{ // 获取文件后缀 const ext = ev.name.substring(ev.name.lastIndexOf('.')) if (ext !== '.xls' && ext !== '.xlsx') { ElMessage.warning('请选择EXCEL文件') return false } excelData.value = [] excelFile.value = ev.raw if (!excelFile.value) { ElMessage.warning('文件打开失败') return false } else { loading.value = ElLoading.service({ lock: true, text: '文件解析中,请稍候...', background: 'rgba(0, 0, 0, 0.7)' }) // 读取文件 setTimeout(readExcelFile, 100) } } // 文件读取并解析 const readExcelFile = ()=>{ const reader = new FileReader() reader.readAsBinaryString(excelFile.value)// 以二进制的方式读取 reader.onload = ev=>{ const fileData = ev.target.result const workBook = XLSX.read(fileData, {type: 'binary'})// 解析二进制格式数据 workBook.SheetNames.forEach((sheetName, index)=>{ const item = { sheetName: sheetName } const workSheet = workBook.Sheets[workBook.SheetNames[index]]// 获取第一个Sheet item.rows = XLSX.utils.sheet_to_json(workSheet, {range: -1, defval: null})// 指定-1行为列头,空单元格赋值 null // 获取json数据key if (item.rows && item.rows.length > 0) { item.keys = [] const keys = Object.keys(item.rows[0]) keys.forEach((key, index)=>{ item.keys.push({ idx: index, key: key, label: '' }) }) } excelData.value.push(item) }) importDialogVisible.value = true loading.value.close() } } const handleImport = (source)=>{ if (source === 'file') { // 模拟点击隐藏的 input 或 el-upload 打开系统文件夹 document.querySelector('#hiddenFileInput').click() } else if (source === 'clipboard') { importSource.value = 'clipboard' importDialogVisible.value = true } } const handleImportReload = ()=>{ handleGetDrilledTrackDatas() importDialogVisible.value = false } // 导出轨迹数据 const handleExportDrilledTrackData = ()=>{ // 过滤数据,只取一部分属性的数据 const partData = drilledTrackDatas.value.map(({md, inc, azi, tvd, nsOffset, ewOffset, horiOffset, projOffset, dogleg})=>({ md, inc, azi, tvd, nsOffset, ewOffset, horiOffset, projOffset, dogleg })) // 值数组 const valueArray = partData.map(obj=>Object.values(obj)) const headerArray = ['测深', '井斜', '方位', '垂深', '南北位移', '东西位移', '水平位移', '投影位移', '狗腿度'] const sheetName = '实钻轨迹' const fileName = '实钻轨迹.xlsx' exportExcel(sheetName, headerArray, formatDecimal(valueArray), fileName, 15) } // 监听well变化 watch(()=>well, (newVal)=>{ if (Object.keys(newVal.value).length !== 0) { handleGetDrilledTracks() } }, {deep: true, immediate: true}) const editingRows = ref([]) const selectRows = ref({}) // 修改:处理测深输入事件 const handleMdInput = (index)=>{ const row = drilledTrackDatas.value[index] // 检查条件:当前是最后一行、正在编辑、测深有值、是新增行(无ID) if ( index === drilledTrackDatas.value.length - 1 && editingRows.value.includes(index) && row.md !== null && row.md !== '' && !row.id ) { // 在下方添加新行 drilledTrackDatas.value.push({md: null, inc: null, azi: null}) const newIndex = drilledTrackDatas.value.length - 1 // 设置新行进入编辑状态 editingRows.value.push(newIndex) selectRows.value[newIndex] = deepClone(drilledTrackDatas.value[newIndex]) } } </script> <template> <BaseFrame> <Card> <template #header> 【{{ well.wellId ? ${well.wellCode} | ${well.wellBoreName} : '暂未选井' }}】 <el-button :icon="Plus" size="small" class="mr-3" @click="editDialogVisible=true;isAdd=true;">{{ t('add') }}</el-button> <el-dropdown @command="importSource=>handleImport(importSource)"> <el-button :icon="Download" size="small">{{ t('import') }}</el-button> <template #dropdown> <el-dropdown-menu> <el-dropdown-item command="file">导入文件</el-dropdown-item> <el-dropdown-item command="clipboard">从剪切板导入</el-dropdown-item> </el-dropdown-menu> </template> </el-dropdown> <input id="hiddenFileInput" type="file" accept=".xlsx,.xls" style="display:none" @change="handleImportDrilledTrackData" > <el-button :icon="Upload" size="small" class="ml-3" :disabled="!currentRow" @click="handleExportDrilledTrackData"> {{ t('export') }} </el-button> <el-button :icon="EditPen" size="small" :disabled="!currentRow" @click="interpolateDialogVisible=true"> {{ t('interpolate') }} </el-button> <el-button :icon="Share" size="small" :disabled="!currentRow" @click="appStore.toggleTrackPredictionDrawer()"> {{ t('plan') }} </el-button> <el-button :icon="Share" size="small" :disabled="!currentRow" @click="waitDrillTrackDesignDialogVisible=true"> 待钻设计 </el-button> <el-button :icon="Aim" size="small" :disabled="!currentRow" @click="hitTargetPredictionDialogVisible=true"> {{ t('targetEntry') }} </el-button> <el-button :icon="DataLine" size="small"> {{ t('trend') }} </el-button> </template> <template #default> <el-table ref="tableRef" :data="drilledTracks" highlight-current-row height="100%" size="small" border stripe style="width: 100%;" @current-change="handleCurrentChange"> <el-table-column type="index" :label="t('order')" width="50" align="center" fixed /> <el-table-column prop="name" :label="t('name')" min-width="80" header-align="center" /> <el-table-column prop="createTime" align="center" :label="t('date')" width="120"> <template #default="{ row }"> {{ formatTimeString(row.createTime, 'YYYY-MM-DD') }} </template> </el-table-column> <el-table-column fixed="right" align="center" :label="t('operate')" width="100"> <template #default="{$index,row}"> <el-button link type="success" size="small" @click.stop="editDialogVisible=true;isAdd=false;"> {{ t('revise') }} </el-button> <el-popconfirm width="200" class="box-item" :title="确认删除实钻轨迹?" placement="bottom" confirm-button-text="确认" cancel-button-text="取消" @confirm="handleDeleteDrilledTrack($index,row)" > <template #reference> <el-button link type="danger" size="small" @click.stop> {{ t('delete') }} </el-button> </template> </el-popconfirm> </template> </el-table-column> </el-table> </template> </Card> <Card class="px-2" style="height:calc(100vh - 14rem)"> <el-table size="small" border stripe :data="formatDecimal(drilledTrackDatas)" height="100%" style="width: 100%" > <el-table-column :label="t('operate')" width="90" align="center" fixed="left"> <template #default="{$index,row}"> <template v-if="editingRows.includes($index)"> <el-button size="small" type="warning" plain :icon="Close" @click="handleCancel($index, row)" /> <el-button size="small" plain type="success" :icon="Check" @click="handleComplete($index)" /> </template> <template v-else> <el-button size="small" class="ml-1" plain :icon="Plus" @click="handleAdd($index)" /> <el-popconfirm width="200" class="box-item" :title="确认删除本条轨迹数据?" placement="bottom" confirm-button-text="确认" cancel-button-text="取消" @confirm="handleDeleteDrilledTrackData(row)" > <template #reference> <el-button plain size="small" :icon="Delete" @click.stop /> </template> </el-popconfirm> </template> </template> </el-table-column> <el-table-column label="测深" prop="depth" header-align="center" min-width="80" fixed="left"> <template #header> {{ t('Md') }} (m) </template> <template #default="{$index,row}"> <template v-if="editingRows.includes($index)"> <el-input ref="input" v-model="drilledTrackDatas[$index].md" v-format-input size="small" placeholder="请输入测深" @input="handleMdInput($index)" /> </template> {{ row.md }} </template> </el-table-column> <el-table-column label="井斜" prop="inc" header-align="center" min-width="80" fixed="left"> <template #header> {{ t('inclination') }} (°) </template> <template #default="{$index,row}"> <template v-if="editingRows.includes($index)"> <el-input v-model="drilledTrackDatas[$index].inc" v-format-input size="small" placeholder="请输入井斜" /> </template> {{ row.inc }} </template> </el-table-column> <el-table-column label="方位" prop="azi" header-align="center" min-width="80" fixed="left"> <template #header> {{ t('azimuth') }} (°) </template> <template #default="{$index,row}"> <template v-if="editingRows.includes($index)"> <el-input v-model="drilledTrackDatas[$index].azi" v-format-input size="small" placeholder="请输入方位" /> </template> {{ row.azi }} </template> </el-table-column> <el-table-column label="垂深" prop="tvd" header-align="center" min-width="80" > <template #header> {{ t('Tvd') }} (m) </template> </el-table-column> <el-table-column label="段长" prop="sectionLength" header-align="center" min-width="80" > <template #header> {{ t('courseLength') }} (m) </template> </el-table-column> <el-table-column label="南北位移" prop="nsOffset" header-align="center" min-width="80" > <template #header> {{ t('NsOffset') }} (m) </template> </el-table-column> <el-table-column label="东西位移" prop="ewOffset" header-align="center" min-width="80" > <template #header> {{ t('EwOffset') }} (m) </template> </el-table-column> <el-table-column label="水平位移" prop="horiOffset" header-align="center" min-width="80" > <template #header> {{ t('closure') }} (m) </template> </el-table-column> <el-table-column label="投影位移" prop="projOffset" header-align="center" min-width="80" > <template #header> {{ t('Verticaldistance') }} (m) </template> </el-table-column> <el-table-column label="闭合方位" prop="closeAzi" header-align="center" min-width="80" > <template #header> {{ t('closureAz') }} (°) </template> </el-table-column> <el-table-column label="狗腿度" prop="dogleg" header-align="center" min-width="80" > <template #header> {{ t('dogleg') }} (°/30m) </template> </el-table-column> <el-table-column label="工具面" prop="toolface" header-align="center" min-width="80" > <template #header> {{ t('toolface') }} (°) </template> </el-table-column> <el-table-column label="井斜变化率" prop="incChangeRate" header-align="center" min-width="80" > <template #header> {{ t('incChangeRate') }} (°/30m) </template> </el-table-column> <el-table-column label="方位变化率" prop="aziChangeRate" header-align="center" min-width="80" > <template #header> {{ t('aziChangeRate') }} (°/30m) </template> </el-table-column> </el-table> </Card> <Card class="flex-1"> <WellTrajectory v-if="drilledTrackGraphDatas" id="divTubeElement" v-model:config="drilledTrackGraphConfig" :show-well-name="false" :show-full-screen-btn="true" :show-animation="false" is-auto-play.sync="true" :data="drilledTrackGraphDatas" /> </Card> <edit-dialog v-if="editDialogVisible" :row-data="isAdd?null:currentRow" @reload="handleImportReload" @close="editDialogVisible=false" /> <import-dialog v-if="importDialogVisible" v-model:import-source="importSource" :drilled-track-id="currentRow.id" :excel-data="excelData" @reload="handleGetDrilledTrackDatas" @close="importDialogVisible=false" /> <interpolate-dialog v-if="interpolateDialogVisible" :drilled-track-id="currentRow.id" @close="interpolateDialogVisible=false" /> <el-drawer v-model="appStore.trackPredictionDrawerVisible" title="轨迹预测" :with-header="true" size="100%" direction="ltr" :close-on-click-modal="false" :close-on-press-escape="false" :destroy-on-close="true" > <track-prediction :last-drilled-track-data="drilledTrackDatas[drilledTrackDatas.length-1]" /> </el-drawer> <wait-drill-track-design-dialog v-if="waitDrillTrackDesignDialogVisible" :last-drilled-track-data="drilledTrackDatas[drilledTrackDatas.length-1]" @close="waitDrillTrackDesignDialogVisible=false" /> <hit-target-prediction-dialog v-if="hitTargetPredictionDialogVisible" :drilled-track-id="currentRow.id" @close="hitTargetPredictionDialogVisible=false" /> </BaseFrame> </template> <style lang="scss" scoped> .tab-content { @apply p-2 absolute left-0 top-0 w-full h-full overflow-auto; } .border-card { :deep(.el-tabs__nav-scroll) { @apply flex; .el-tabs__nav { @apply flex-1; .el-tabs__item { @apply flex-1; } } } &.no-border { @apply border-none; } } :deep(.el-drawer) { .el-drawer__header { @apply mb-1 } } </style> 修改本代码,要求保存上一行时,自动新增行内有内容时应该保留当前行并保留编辑状态

<template> <el-table-column v-if="col.show && col.type != 'selection' && col.type != 'radio'" v-bind="col" :align="'center'" showOverflowTooltip > <template #header> {{ col.label }} <template v-if="col.templet !== 'tool' && col.filter && col.prop"> <el-popover :visible="visible && col.prop === showKey" :virtual-ref="refName" placement="bottom" :width="'fit-content'" ref="popoverRef" > <template #reference> <el-icon @click.stop="toggleNameFilter(col.prop, col)" size="17.4" :class="{ 'filter-active': searchData[col.prop] }" > <Filter /> </el-icon> </template> <el-input v-if="col.filter.filterFormType === 'input'" v-model="searchData[col.prop]" :autofocus="true" placeholder="请输入" v-bind="col.attrs" @input="applyNameFilter" clearable style="margin-top: 10px; width: 150px" /> <template v-if="!col.filter.needGetList"> <el-select v-if="col.filter.filterFormType === 'select'" filterable :reserve-keyword="false" v-model="searchData[col.prop]" v-bind="col.attrs" @change="applyTagFilter" clearable style="margin-top: 10px; width: 150px" > <el-option v-for="item in col.filter.selectList" :key="item" :value="item.value" :label="item.name" /> </el-select> <FkSelectTree v-if="col.filter.filterFormType === 'select-tree'" v-model="searchData[col.prop]" v-bind="col.attrs" /> </template> <template v-if="col.filter.needGetList"> <FkSelect v-if="col.filter.filterFormType === 'select'" v-bind="col.attrs" v-model="searchData[col.prop]" :sourceData="javaList" ></FkSelect> <FkSelectTree v-if="col.filter.filterFormType === 'select-tree'" v-model="searchData[col.prop]" v-bind="col.attrs" :data="javaList" :sourceData="javaList" ></FkSelectTree> </template> <el-date-picker v-if=" col.filter.filterFormType === 'daterange' || col.filter.filterFormType === 'datetimerange' " style="width: 360px" v-model="searchData[col.prop]" :type="col.filter.filterFormType" range-separator="~" start-placeholder="开始时间" end-placeholder="结束时间" :format="col.dateType" @change="changeData($event, col.field, col.dateType)" /> <el-button type="info" link @click="cancelFilter(col.prop)">重置</el-button> <el-button type="primary" link @click="searchFilter">筛选</el-button> </el-popover> </template> </template> <template #default="scope"> <template v-if="col.templet === 'image'"> <template v-if="col.prop"> <template v-if="Array.isArray(scope.row[col.prop])"> <template v-for="(item, index) in scope.row[col.prop]" :key="item"> <el-image :src="item" :preview-src-list="scope.row[col.prop]" :initial-index="index" :preview-teleported="true" :style="width: ${col.imageWidth ?? 40}px; height: ${col.imageHeight ?? 40}px" /> </template> </template> <template v-else> <el-image :src="scope.row[col.prop]" :preview-src-list="[scope.row[col.prop]]" :preview-teleported="true" :style="width: ${col.imageWidth ?? 40}px; height: ${col.imageHeight ?? 40}px" /> </template> </template> </template> <template v-else-if="col.templet === 'list'"> <template v-if="col.prop"> {{ (col.selectList ?? {})[scope.row[col.prop]] }} </template> </template> <template v-else-if="col.templet === 'url'"> <template v-if="col.prop"> <el-link type="primary" :href="scope.row[col.prop]" target="_blank"> {{ scope.row[col.prop] }} </el-link> </template> </template> <template v-else-if="col.templet === 'price'"> <template v-if="col.prop"> {{ ${col.priceFormat ?? "¥"}${scope.row[col.prop]} }} </template> </template> <template v-else-if="col.templet === 'percent'"> <template v-if="col.prop">{{ scope.row[col.prop] }}%</template> </template> <template v-else-if="col.templet === 'icon'"> <template v-if="col.prop"> <template v-if="scope.row[col.prop].startsWith('el-icon-')"> <el-icon> <component :is="scope.row[col.prop].replace('el-icon-', '')" /> </el-icon> </template> <template v-else> </template> </template> </template> <template v-else-if="col.templet === 'date'"> <template v-if="col.prop"> {{ scope.row[col.prop] ? useDateFormat(scope.row[col.prop], col.dateFormat ?? "YYYY-MM-DD HH:mm:ss").value : "" }} </template> </template> <template v-else-if="col.templet === 'sys-code'"> <FkSysCodeLable v-if="col.prop" v-model="scope.row[col.prop]" :code="col.sysCodeSetCode" /> </template> <template v-else-if="col.templet === 'copy'"> <el-text>{{ scope.row[col.prop] }}</el-text> <copy-button v-if="scope.row[col.prop]" :text="scope.row[col.prop]" style="margin-left: 2px" /> </template> <template v-else-if="col.templet === 'yes-no'"> <el-tag :type="scope.row[col.prop] == '1' ? 'success' : 'info'"> {{ scope.row[col.prop] == "1" ? "启用" : "禁用" }} </el-tag> </template> <template v-else-if="col.templet === 'translate'"> {{ scope.row[col.prop + "Name"] }} </template> </template> </el-table-column> </template>优化,点击非弹出部分,自定关闭弹窗

<template> <e-layout> <template v-slot:search-bar> <el-form :inline="true" :model="searchForm" @keyup.enter.native="search" @submit.native.prevent> <el-form-item> <el-date-picker v-model="searchForm.date" type="datetimerange" :picker-options="$constants.datePickerOptions" :range-separator="$t('Common.searchForm.datePicker.separator')" :start-placeholder="$t('Common.searchForm.datePicker.startTime')" :end-placeholder="$t('Common.searchForm.datePicker.endTime')" value-format="yyyy-MM-dd HH:mm:ss" :default-time="['00:00:00', '23:59:59']" align="right"> </el-date-picker> </el-form-item> <el-form-item> <el-input v-model="searchForm.ani" :placeholder="$t('OutboundCallJob.searchForm.ani')" clearable></el-input> </el-form-item> <el-form-item> <el-input v-model="searchForm.dnis" :placeholder="$t('OutboundCallJob.searchForm.dnis')" clearable></el-input> </el-form-item> <el-form-item> <template> <el-select v-model="searchForm.status" :placeholder="$t('OutboundCallJob.searchForm.status')" clearable> <el-option :label="$t('Common.enum.status.enabled')" value="enabled"></el-option> <el-option :label="$t('Common.enum.status.disabled')" value="disabled"></el-option> </el-select> </template> </el-form-item> <el-form-item> <el-button type="primary" size="mini" @click="search" :loading="loading" icon="el-icon-search">{{ $t('Common.button.search') }}</el-button> </el-form-item> </el-form> </template> <template v-slot:btn-bar> <el-row> <el-button type="success" size="mini" @click="toAdd" icon="el-icon-plus" v-has-permission="'monitor:outbound-call-task:addCycle'">{{ $t('Common.button.new') }}</el-button> </el-row> </template> <template v-slot:main-content> <el-table :data="tableData" height="100%" highlight-current-row ref="clearTableData"> <template slot="empty"> <el-empty :description="$t('Common.table.emptyDescription')" /> </template> <el-table-column type="index" :label="$t('Common.table.index')" width="50" fixed="left" align='center'/> <el-table-column prop="createTime" :label="$t('OutboundCallJob.table.createTime')" min-width="150" align='center'/> <el-table-column prop="userName" :label="$t('OutboundCallJob.table.userName')" min-width="120" align='left'/> <el-table-column prop="ani" :label="$t('OutboundCallJob.table.ani')" min-width="150" align='left'/> <el-table-column prop="dnis" :label="$t('OutboundCallJob.table.dnis')" min-width="150" align='left'/> <el-table-column prop="status" :label="$t('OutboundCallJob.table.status')" min-width="100" align='center'> <template v-slot="scope"> <el-tag v-if="scope.row.status === 'disabled'" type="info" effect="plain">{{ $t('Common.enum.status.disabled') }}</el-tag> <el-tag v-else-if="scope.row.status === 'enabled'" type="success" effect="plain">{{ $t('Common.enum.status.enabled') }}</el-tag> </template> </el-table-column> <el-table-column prop="text" :label="$t('OutboundCallJob.table.text')" show-overflow-tooltip min-width="300" align='left'/> <el-table-column prop="cron" :label="$t('OutboundCallJob.table.cron')" min-width="100" align='left'/> <el-table-column :label="$t('Common.table.operate')" min-width="200" class-name="small-padding fixed-width" fixed="right" > <template v-slot="scope"> <el-button type='text' class="row-button" @click.stop="toEdit(scope.row)" v-has-permission="'outbound:cycle:update'"> {{ $t('Common.button.edit') }}</el-button> <el-button type="text" class="row-button" @click.stop="toDelete(scope.row)" v-has-permission="'outbound:cycle:delete'"> {{ $t('Common.button.delete') }}</el-button> <el-button type="text" class="row-button" @click.stop="viewDetail(scope.row)"> {{ $t('OutboundCallTask.detailLabel') }}</el-button> </template> </el-table-column> </el-table> </template> <template v-slot:pagination-bar> <el-pagination @size-change="handleSizeChange" @current-change="handleCurrentChange" :current-page.sync="pagination.currentPage" :page-size.sync="pagination.pageSize" :page-sizes="pagination.pageSizes" :total="pagination.total" layout="prev, pager, next, total, sizes, jumper" background > </el-pagination> </template> <el-dialog :title="dialogTitle" :visible.sync="isShowDialog" :close-on-click-modal="false" width="600px"> <el-form ref="addForm" label-width="90px" :model="form" :rules="addFormRules" > <el-form-item :label="$t('OutboundCallJob.form.label.scheduleInterval')" prop="scheduleInterval"> <cron-picker :cron="form.cron" @change="onChange" :locale="en" /> </el-form-item> <el-form-item :label="$t('OutboundCallJob.form.label.cron')" prop="cron" style="margin-top: -15px"> <el-input v-model="form.cron" disabled></el-input> </el-form-item> <el-form-item :label="$t('OutboundCallJob.form.label.ani')" prop="ani"> <el-input v-model="form.ani" :placeholder="$t('OutboundCallJob.form.placeholder.ani')"></el-input> </el-form-item> <el-form-item :label="$t('OutboundCallJob.form.label.dnis')" prop="dnis"> <el-input v-model="form.dnis" :placeholder="$t('OutboundCallJob.form.placeholder.dnis')" style="margin-right: 10px"></el-input> <el-button type="success" icon="el-icon-share" @click.prevent="fillMyMobile">{{ $t('Common.button.copy') }}</el-button> </el-form-item> <el-form-item :label="$t('OutboundCallJob.form.label.text')" prop="text" :autosize="{ minRows: 4, maxRows: 8}"> <el-input type="textarea" v-model="form.text" :placeholder="$t('OutboundCallJob.form.placeholder.text')"></el-input> </el-form-item> <el-form-item :label="$t('OutboundCallJob.form.label.status')"> <template> <el-radio-group v-model="form.status"> <el-radio label="enabled">{{ $t('Common.enum.status.enabled') }}</el-radio> <el-radio label="disabled">{{ $t('Common.enum.status.disabled') }}</el-radio> </el-radio-group> </template> </el-form-item> </el-form> <el-button @click="isShowDialog = false" icon="el-icon-close">{{ $t('Common.dialog.button.cancel') }}</el-button> <el-button type="primary" @click="createCallJob" icon="el-icon-check">{{ $t('Common.dialog.button.save') }}</el-button> </el-dialog> </e-layout> </template> <script> import CronPicker from 'cron-picker-vue' import expiringStorage from '@/utils/expiringStorage' export default { components: { CronPicker }, data () { return { tableData: [], dialogTitle: '', isShowDialog: false, searchForm: { status: '', result: '', ani: '', dnis: '', date: [] }, form: { scheduleInterval: 'day', // 调度周期 cron: '', // Cron 表达式 ani: '', dnis: '', status: '', text: '' }, // 表单验证 addFormRules: { cron: [ { required: true, message: this.$t('OutboundCallJob.rules.addFormRules.cron.required'), trigger: 'blur' } // 执行周期不能为空 ], dnis: [ { required: true, message: this.$t('OutboundCallJob.rules.addFormRules.dnis.required'), trigger: 'blur' } // 被叫号码不能为空 ], text: [ { required: true, message: this.$t('OutboundCallJob.rules.addFormRules.text.required'), trigger: 'blur' } // 文本内容不能为空 ] }, loading: false, pagination: { currentPage: 1, pageSize: 20, total: 0, pageSizes: [20, 40, 60, 80, 100, 200, 500] } } }, computed: { cronLocale () { return { everyText: this.$t('CronPicker.every'), minutesText: this.$t('CronPicker.minutes'), hoursText: this.$t('CronPicker.hours'), daysText: this.$t('CronPicker.days'), weeksText: this.$t('CronPicker.weeks'), monthsText: this.$t('CronPicker.months'), yearsText: this.$t('CronPicker.years'), specificWeekdayText: this.$t('CronPicker.specificWeekday'), specificDateText: this.$t('CronPicker.specificDate'), specificTimeText: this.$t('CronPicker.specificTime'), specificMinuteText: this.$t('CronPicker.specificMinute'), specificHourText: this.$t('CronPicker.specificHour'), specificDayText: this.$t('CronPicker.specificDay'), specificMonthText: this.$t('CronPicker.specificMonth'), specificYearText: this.$t('CronPicker.specificYear'), atText: this.$t('CronPicker.at'), andText: this.$t('CronPicker.and'), onText: this.$t('CronPicker.on'), ofText: this.$t('CronPicker.of'), theText: this.$t('CronPicker.the'), lastText: this.$t('CronPicker.last'), firstText: this.$t('CronPicker.first'), secondText: this.$t('CronPicker.second'), thirdText: this.$t('CronPicker.third'), fourthText: this.$t('CronPicker.fourth'), fifthText: this.$t('CronPicker.fifth'), sixthText: this.$t('CronPicker.sixth'), seventhText: this.$t('CronPicker.seventh'), eighthText: this.$t('CronPicker.eighth'), ninthText: this.$t('CronPicker.ninth'), tenthText: this.$t('CronPicker.tenth'), eleventhText: this.$t('CronPicker.eleventh'), twelfthText: this.$t('CronPicker.twelfth'), thirteenthText: this.$t('CronPicker.thirteenth'), fourteenthText: this.$t('CronPicker.fourteenth'), fifteenthText: this.$t('CronPicker.fifteenth'), sixteenthText: this.$t('CronPicker.sixteenth'), seventeenthText: this.$t('CronPicker.seventeenth'), eighteenthText: this.$t('CronPicker.eighteenth'), nineteenthText: this.$t('CronPicker.nineteenth'), twentiethText: this.$t('CronPicker.twentieth'), twentyFirstText: this.$t('CronPicker.twentyFirst'), twentySecondText: this.$t('CronPicker.twentySecond'), twentyThirdText: this.$t('CronPicker.twentyThird'), twentyFourthText: this.$t('CronPicker.twentyFourth'), twentyFifthText: this.$t('CronPicker.twentyFifth'), twentySixthText: this.$t('CronPicker.twentySixth'), twentySeventhText: this.$t('CronPicker.twentySeventh'), twentyEighthText: this.$t('CronPicker.twentyEighth'), twentyNinthText: this.$t('CronPicker.twentyNinth'), thirtiethText: this.$t('CronPicker.thirtieth'), thirtyFirstText: this.$t('CronPicker.thirtyFirst'), sundayText: this.$t('CronPicker.sunday'), mondayText: this.$t('CronPicker.monday'), tuesdayText: this.$t('CronPicker.tuesday'), wednesdayText: this.$t('CronPicker.wednesday'), thursdayText: this.$t('CronPicker.thursday'), fridayText: this.$t('CronPicker.friday'), saturdayText: this.$t('CronPicker.saturday') } } }, created () { this.search() }, methods: { toAdd () { this.reset() this.dialogTitle = this.$t('OutboundCallJob.dialog.title.new') // 新建 this.isShowDialog = true }, toEdit (row) { this.reset() this.dialogTitle = this.$t('OutboundCallJob.dialog.title.edit') // 编辑 this.isShowDialog = true Object.assign(this.form, row) this.form.id = row.id }, // 创建确认 createCallJob () { let fromData = new FormData() fromData.append('ani', this.form.ani) fromData.append('dnis', this.form.dnis) fromData.append('cron', this.form.cron) fromData.append('status', this.form.status) fromData.append('scheduleInterval', this.form.scheduleInterval) fromData.append('text', this.form.text) this.$refs['addForm'].validate(valid => { // 判断输入校验是否成功 if (!valid) { return } // 新建 if (!this.form.id) { this.$http.post(this.$apiUrl('callJobCreate'), fromData, { headers: { 'Content-Type': 'application/json' } } ).then(response => { this.$message.success(this.$t('OutboundCallJob.message.createSuccess')) // 创建成功 this.isShowDialog = false this.search() }) // 编辑 } else { fromData.append('id', this.form.id) this.$http.put(this.$apiUrl('callJobUpdate'), fromData, { headers: { 'Content-Type': 'application/json' } } ).then(response => { this.$message.success(this.$t('OutboundCallJob.message.updateSuccess')) // 修改成功 this.isShowDialog = false this.search() }).catch((error) => { console.dir(error) this.$message.error(error.message) }) } }) }, // 删除 toDelete (row) { this.$confirm( this.$t('OutboundCallJob.confirm.delete'), // 此操作将删除该任务,是否继续? this.$t('OutboundCallJob.confirm.title'), // 提示 { confirmButtonText: this.$t('OutboundCallJob.confirm.confirmButtonText'), // 确定 cancelButtonText: this.$t('OutboundCallJob.confirm.cancelButtonText'), // 取消 type: 'warning' }).then(() => { this.delete(row.id) }) }, delete (id) { this.$http.delete(this.$apiUrl('callJobDelete', [id])) .then(response => { this.$message.success(this.$t('OutboundCallJob.message.deleteSuccess')) // 删除成功! this.search() }) .catch((error) => { if (error.response) { this.$message.error(${this.$t('OutboundCallJob.message.deleteFail')}: ${error.response.data.message}) // 删除失败! ${error.response.data.message} } else { this.$message.error(${this.$t('OutboundCallJob.message.deleteFail')}: ${error.message}) // 删除失败! ${error.message} } }) }, // 列表搜索 search () { let startTime = '' let endTime = '' if (this.searchForm.date && this.searchForm.date.length > 0) { startTime = this.searchForm.date[0] endTime = this.searchForm.date[1] } let form = { startTime: startTime, endTime: endTime, page: this.pagination.currentPage, size: this.pagination.pageSize, ...this.searchForm } let url = this.$apiUrl('callJobSearch') + '?' + this.$qs.stringify(form) this.$http.get(url).then(response => { this.tableData = response.data.data.records this.pagination.total = response.data.data.total }).catch((error) => { this.$message.error(error.message) }) }, // 重置表单 reset () { this.form = { scheduleInterval: 'day', // 调度周期 cron: '', // Cron 表达式 ani: '', dnis: '', status: 'enabled', text: '' } this.$nextTick(() => { this.$refs['addForm'].clearValidate() }) }, fillMyMobile () { let user = expiringStorage.get('user') console.log(user) if (user.mobile) { this.form.dnis = user.mobile } }, viewDetail (callJob) { this.$emit('navigateToDetail', callJob.id) }, // change 事件会返回新的 interval 和 cron onChange (cron) { console.log(cron) this.form.cron = cron }, handleSizeChange (val) { this.pagination.pageSize = val this.search() }, handleCurrentChange (val) { this.pagination.currentPage = val this.search() } } } </script> <style scoped lang="scss"> ::v-deep .cron-picker .el-select { margin-bottom: 15px; } </style>

大家在看

recommend-type

用C#自制的简易英语单词记忆助手(含源代码)

或许不是做很好,请不要见怪 基本用法: 按自己的意愿布置明天所要背下来的单词,然后到了明天后当自己觉得已经背下来了,则可以使用本程序选择当天的任务(前天布置的)去测试一下背的效果。 本程序的主要特点: 1, 开机自动启动 通过修改系统注册表实现的开机自动运行 2, 会提示昨天是否有布置任务 通过一个标志变量(储存于数据库)来判断当天是否有任务,确切的说应该是当作业布置完以后标志变量就被激活(为“1”的状态是激活的状态,为“0”时则未被激活)则在主人登入界面上会提示是否有任务。 3, 定时自动关闭程序 当程序启动后,会有20秒的时间让主人登入,否则超过了20秒后,程序自动关闭 4, 自动统计答对的题数和正确率 通过一些变量控制来实现对其自动统计 5, 能将正确的和错误的单词明确的指出存放于TextBox 用两个变量分别实现,一个变量储存对字符串,另一个则储存错的字符串,最后根据触发事件分别显示到TextBox中 6, 按钮同时具备显示和隐藏控件的效果 两个按钮“答对的单词”和“答错的单词”分别用于显示对的和错的单词,按一下显示TextBox,按第二下则会隐藏TextBox 7, 使用Engter键代替鼠标点击(确定按钮)或锁定控件焦点 做这个功能主要用于方便文字的输入,每个窗体都具备此功能。尤其是在布置任务的窗体内更需要此功能 附:本程序的初始密码为“123”
recommend-type

即时记截图精灵 v2.00.rar

即时记截图精灵是一款方便易用,功能强大的专业截图软件。   软件当前版本提供以下功能:   1. 可以通过鼠标选择截图区域,选择区域后仍可通过鼠标进行边缘拉动或拖拽来调整所选区域的大小和位置。   2. 可以将截图复制到剪切板,或者保存为图片文件,或者自动打开windows画图程序进行编辑。   3. 保存文件支持bmp,jpg,png,gif和tif等图片类型。   4. 新增新浪分享按钮。
recommend-type

rk3588 linux 系统添加分区和修改分区

root@rk3588-buildroot:/logo# df -h /dev/mmcblk0p3 124M 24K 123M 1% /logo /dev/mmcblk0p4 124M 24K 123M 1% /cfg 附件主要是去掉misc、recovery、backup等分区,然后添加logo,和cfg分区。
recommend-type

rtp解包组包.zip

1、H264的RTP组包和拆包,主要是对于单帧包和FU-A包的处理; 2、H265的RTP组包和拆包,主要是对于单帧包和FU-A包的处理; 3、H264和H265的SPS数据解析;
recommend-type

51单片机ADC0832的Proteus仿真.zip

通过Proteus仿真基于89C52/89C51的ADC0832电位器电压读取

最新推荐

recommend-type

ecr-jvm-1.0.9-sources.jar

ecr-jvm-1.0.9-sources.jar
recommend-type

个人作品:使用React和Material-UI打造的赛车主题个人网站

### 知识点概述 该部分将围绕提供的文件信息进行展开,包含React框架、Material-UI库、网站性能优化、版本控制、网站部署以及相关的标签解析等详细知识点。 ### React框架 #### React简介 React是由Facebook开发和维护的一个用于构建用户界面的JavaScript库。它采用组件化的方式,使得开发者可以将UI分解为独立、可复用的组件。这些组件可以包含自己的状态,且只有状态发生变更时,才会重新渲染相应的组件,从而提高应用性能。 #### React应用生命周期 在React中,组件从创建到挂载、更新再到卸载,均遵循一套生命周期方法。例如,`componentDidMount`是在组件挂载后立即调用的方法,常用于执行如数据获取这类操作。`componentDidUpdate`则是组件更新后调用,可用于与当前和之前的props进行比较,并基于比较结果执行更新操作。 ### Material-UI #### Material-UI简介 Material-UI是一个React的用户界面框架,它提供了一整套现成的组件,符合Google的Material Design设计语言。Material-UI的核心优势在于其能够快速实现美观且一致的UI界面,同时保持高度的可定制性。该框架包含各种常用的UI元素,如按钮、输入框、卡片等,并拥有丰富的主题配置选项来支持不同品牌和风格的设计需求。 #### Material-UI中的组件使用 Material-UI通过组件化的方式提供各种UI元素,开发者可以根据需要自由组合和构建界面。例如,`Button`组件可以用于创建按钮,`Card`组件用于创建卡片布局等。每个组件的使用都遵循Material-UI的设计规范,确保界面美观和用户友好。 ### 网站性能优化 #### 响应式设计 从描述中提到网站支持移动和桌面端的定制设计,这是响应式设计的核心特点。响应式设计意味着网页能够根据不同的屏幕尺寸和分辨率,自动调整布局,提供最优化的浏览体验。 #### 动画和过渡效果 网站引入了新的过渡和动画,这不仅提升了用户体验,也可能有助于页面元素间转换时的直观性。使用React可以轻松地添加和管理动画,因为状态更新时React会自动处理组件树的更新。 ### 版本控制和分叉仓库 #### 版本控制(Git) 从描述中提到可以分叉此仓库,这涉及到了Git版本控制工具的使用。Git是一个分布式版本控制系统,用于跟踪代码变更,并且支持协作开发。通过分叉仓库,开发者可以从原始项目创建一个副本,可以在副本上进行修改而不影响原项目。 #### 分叉(Fork)和克隆(Clone) 分叉操作会创建一个远程仓库的副本,而克隆操作则会将远程仓库的内容复制到本地计算机。这意味着开发者可以在本地进行更改,然后选择将更改推送到自己的远程副本,或贡献回原始项目。 ### 安装与部署 #### 安装依赖项 在分叉/克隆仓库之后,开发者需要在项目目录中运行`npm install`来安装所有必需的依赖项。这一步骤是启动开发服务器前的必要准备。 #### 开发和生产环境 开发者需要区分开发环境和生产环境。开发环境通常包含调试工具,而生产环境需要优化和压缩资源以提升网站性能。通过运行`npm start`可以启动开发服务器,而使用`npm run build`则可以构建用于生产环境的静态文件。 ### 标签解析 #### React 标签提及了React,这意味着项目是基于React框架开发的,相关开发者和招聘者可能会搜索此标签以找到具有React开发经验的人才。 #### Blog & personal-website 标签还提到了博客和个人网站,这可能意味着项目是一个个人博客或者展示个人技能和经历的网站,通常用于职业发展或个人品牌建设。 #### Resume-website 标签提到了简历网站,这可以是一个在线简历或个人作品集的展示平台,提供了一个展示个人技能和经验的数字平台,便于求职者与潜在雇主之间沟通。 ### 结语 以上是根据提供的文件信息总结的详细知识点,涵盖了React和Material-UI框架的使用、网站性能优化、版本控制和仓库分叉、安装和部署流程以及对相关标签的解释和应用。这些内容不仅有助于理解该项目的技术细节,而且对于了解构建现代Web应用所涉及的技术和流程也是十分有帮助的。
recommend-type

活动图设计技巧大公开:如何在StarUML中构建高逻辑性的业务流程?

# 摘要 活动图作为软件系统建模的重要工具,能够有效表达复杂业务流程的逻辑结构与行为关系。本文围绕StarUML平台,系统探讨了活动图的设计原理、建模方法及其在实际项目中的应用价值。文章从活动图的核心元素与建模逻辑入手,分析了结构化建模方法与语义表达机制,并详细阐述了在StarUML中构建标准活动图的操作流程与优化策略。针对复杂业务场景,
recommend-type

模拟经典运放电路结构

<think>嗯,用户想了解经典运放电路的结构、模拟方法和实现方式。首先,我需要回忆运放的基本结构。运放通常由差分输入级、中间增益级和输出级组成,对吧?然后,模拟方法可能涉及虚短和虚断的概念,这是分析运放电路的关键。用户提到的引用里也强调了这两个概念的重要性,所以必须详细解释。接下来,我得考虑不同的经典电路结构,比如反向放大器、同向放大器、电压跟随器、加法器、微分器和积分器。每个电路的结构和公式需要准确描述,并确保使用正确的LaTeX格式。例如,反向放大器的公式是$V_o= -\frac{R_f}{R_1}V_i$,要检查是否用$$...$$还是$...$,根据用户的要求,行内公式用$,独立公
recommend-type

MATLAB模拟无线传感器网络与区块链技术

根据给定文件信息,我们将详细探讨以下几个关键知识点: 1. 无线传感器网络(Wireless Sensor Network, WSN): 无线传感器网络是由一组具有传感器、处理单元和通信能力的小型设备组成的网络,这些设备能够相互协作,完成对环境的监测任务。无线传感器网络具有部署便捷、自组织、灵活性高等特点。它在智能交通、环境监测、智能家居等领域有着广泛的应用。 2. 区块链技术(Blockchain Technology): 区块链是一种分布式数据库技术,其特点是去中心化、数据不可篡改、信息透明。在无线传感器网络中,区块链可用于提高数据的可信度和安全性。每个节点生成的块(block)将包含一段时期内的交易信息,这些块链式地连接在一起,形成链状结构,即区块链。通过共识机制(如工作量证明PoW、权益证明PoS等),网络中的节点对数据的有效性达成一致,从而保证数据的安全性和可靠性。 3. 随机泛洪路由技术(Random Flooding Routing): 随机泛洪路由技术是一种无需路由表的简单、基于概率的路由方法。在泛洪机制中,消息从源节点发出后,每个接收到消息的节点都会以一定的概率转发给其邻居节点。该技术易于实现,但可能会导致大量重复传输,进而增加网络的负载和能量消耗。因此,随机泛洪路由通常用于对实时性要求较高,但对能量和资源消耗要求不高的场合。 4. MATLAB仿真: MATLAB是一种高级数学计算和仿真软件,它广泛应用于工程计算、控制系统、信号处理、通信系统等领域。在无线传感器网络和区块链技术的研究中,MATLAB提供了强大的仿真环境和工具箱,使得研究人员能够模拟网络行为、验证算法性能和优化系统设计。 5. 能量效率(Energy Efficiency): 在无线传感器网络的设计中,能量效率是一个核心考量因素。由于传感器节点通常由电池供电,并且电池的更换或充电往往不便或不可行,因此降低节点能耗,延长网络的生命周期至关重要。研究者需要在保证网络性能的同时,采用各种策略来减少节点的能量消耗。 6. 静态节点(Static Node): 在无线传感器网络中,静态节点指的是那些位置固定不动的节点。与移动节点相比,静态节点的网络拓扑结构相对稳定,这有助于简化路由策略的设计,并且在一定程度上提高了系统的可预测性。静态节点适用于那些对位置变化不敏感的监测任务。 7. 节点块生成(Block Generation at Nodes): 在区块链技术中,节点块生成是指每个节点按照一定的规则(如PoW、PoS等)打包一段时间内的交易记录,生成新的数据块,并将其加入到区块链中的过程。每个新生成的块都包含前一个块的哈希值,确保了链的连续性和不可篡改性。在无线传感器网络中,节点生成块的过程也是数据交换的一部分,每个节点在完成数据处理和转发后,可能会产生新的块。 综合以上知识点,我们可以了解到,给定文件中的MATLAB仿真代码是专门用于无线传感器网络环境的仿真,其中实现了随机泛洪路由技术来模拟数据传输过程,并通过节点上生成块的方式构建了区块链。该代码特别适用于静态节点环境,其目的在于研究如何降低能量消耗,并保证数据传输的可靠性和安全性。代码的开源性将有助于研究人员和开发者访问、使用、修改和进一步优化该仿真模型。
recommend-type

UML状态图深度剖析:掌握对象生命周期建模的7个关键要点

# 摘要 UML状态图是描述系统动态行为的核心建模工具,广泛应用于软件与系统设计中。本文系统阐述了状态图的基本概念与理论基础,深入分析了状态、转移、复合结构及并发机制等关键建模元素,并详细探讨了状态图的构建流程与设计原则,强调行为建模的逻辑完整性与可维护性。结合嵌入式系统、业务流程和设计模式等实际应用场景,展示了状态图在复杂系统状态管理中的有效性。同时,本文研究了状态图与类图、序列图的协同机制,探讨了其在系统架构设计中的整合作用,并介绍了主流建模工具对状态图的支持与自动化实现方法,为工程实践提供了理论指导和技术路径。 # 关键字 UML状态图;状态转移;复合状态;并发建模;行为建模;
recommend-type

dbeaver缩小编辑页面

<think> 首先,用户的问题是:“我想查找在DBeaver中缩小编辑页面的方法 请问DBeaver缩小编辑页面的方法”。这似乎是用户在寻求如何在DBeaver数据库工具中缩小编辑页面的方法。 回顾系统级指令: - 所有行内数学表达式使用$...$格式。 - 独立公式使用$$...$$格式并单独成段。 - LaTeX语法必须正确。 - 使用中文回答。 - 生成相关问题。 - 引用的段落末尾自然地添加引用标识。 用户提供的引用: - 引用[1]:关于SQL函数和触发器的更新操作,与DBeaver缩小页面无关。 - 引用[2]:提到搜索历史、模糊查询、列编辑模式、自动换行和截图工具snipa
recommend-type

加强Magento管理员密码强度的扩展工具

Magento是一个流行的开源电子商务平台,它允许商家和开发人员构建和管理在线商店。为了确保在线商店的安全性,管理员密码的强度至关重要。Magento默认提供的管理员密码强度规则对于基本安全需求来说已经不错,但往往可以根据不同的安全需求进行增强。 Magento的“magento-admin-password-strength-enforcer”扩展就是用来加强默认密码策略的工具之一。通过这个扩展,网站管理员可以设置一个更高的密码长度阈值,以强制新创建的管理员密码满足一定的安全标准。 知识点说明: 1. Magento平台概述: Magento是一个基于PHP语言和MySQL数据库开发的电子商务解决方案,它具有模块化架构的特点,提供了丰富的扩展性和定制性。平台内置了许多功能,比如目录管理、营销工具、SEO优化等,同时支持多店铺管理和多种支付方式。 2. 管理员密码安全性: 在电子商务平台中,管理面板的访问权限至关重要。管理员账户通常拥有对网站进行设置、配置和维护的权限,因此密码的安全性直接关系到整个网站的安全性。如果密码强度不够,恶意攻击者就可能通过各种手段获取密码,进而对网站进行非法操作。 3. Magento密码强度策略: Magento默认配置中包含了密码强度的验证规则,如要求密码包含一定数量的字符、数字和特殊符号,以及不得包含用户名等。这些规则在一定程度上增强了密码的安全性,但随着网络攻击手段的不断进步,增强密码策略的要求变得越发重要。 4. Magento扩展与增强: Magento社区和开发人员不断提供各种扩展(Extensions)来增加Magento的功能,包括安全增强功能。magento-admin-password-strength-enforcer扩展就是这类工具之一,它允许管理员通过设置来提高密码强度,尤其是通过增加密码长度的要求。 5. 扩展安装和配置: 安装此类Magento扩展通常涉及解压下载的文件到Magento的特定目录,并通过Magento的后台管理系统进行安装配置。该扩展提供了一个简单的配置界面,管理员可以直接在Magento的后台设置界面(System > Configuration > Admin Password Strength)进行操作,输入想要设置的新密码长度阈值。 6. 特征和优势: - 简洁性:该扩展不会修改Magento的核心代码,这意味着它可以更加安全地应用,不会影响Magento平台的其他部分。 - 向后兼容性:扩展设计为向后兼容,因此可以在不影响其他已安装扩展的情况下使用。 - 灵活性:提供了默认密码长度的设置,管理员可以自行定义密码的最小长度要求,以满足自己的安全需求。 - 升级友好:由于不涉及核心代码,该扩展通常能够随着Magento平台的升级而正常工作。 7. 核心代码与扩展开发: Magento的PHP代码库是其架构的重要组成部分,扩展开发通常需要开发者具备良好的PHP开发能力。通过编写代码来增强或修改Magento的功能,是实现特定需求的常用方法。这种开发方式既保留了Magento的核心优势,又能够灵活地根据用户需求进行调整。 8. 安全维护与最佳实践: 在使用和维护电子商务平台时,应该遵循一些最佳实践,如定期更新Magento和扩展,保持软件处于最新版本以避免已知的安全漏洞。此外,还应定期更换密码,不在不同网站使用相同的密码,以减少被攻击的风险。 通过上述详细的知识点阐述,可以看出magento-admin-password-strength-enforcer扩展是如何通过提高管理员密码的最小长度要求来提升Magento平台的安全性的,同时也强调了在实施此类安全措施时需要注意的最佳实践和操作方法。
recommend-type

时序图绘制实战指南:3步提升可读性,精准表达复杂交互流程

# 摘要 时序图作为UML中描述对象间交互行为的核心工具,在软件设计与系统分析中具有重要价值,但其可读性常因结构复杂、信息过载而受限。本文系统阐述时序图的理论基础,解析参与者、生命线、消息序列等核心要素的语义逻辑,提出聚焦主流程、层次化抽象等建模原则,并深入探讨视觉密度与命名规范对认知负荷的影响。针对实际绘制难点,文章构建了“逻辑精炼—布局优化—
recommend-type

postman离线版arm64

<think> 首先,用户的问题是:“我想查找Postman离线版适用于arm64架构的版本 请问Postman离线版arm64架构版本下载”。这基于之前提供的引用。 回顾提供的引用: - 引用[1]:讨论macOS上的下载,提到了ARM64和x86_64架构。 - 引用[2]:关于系统处理器架构(ARM vs x86)。 - 引用[3]:描述了一个轻量级API调试工具,作为Postman的替代品,支持ARM架构。 - 引用[4]:提到麒麟V10 arm版和一个原创微型postman。 用户明确表示需要Postman离线版针对arm64架构的版本。但引用[3]和[4]似乎指向替代工具,而不