Flex中如何利用Array.filter()事件从一个数组中移除重复项目的例子

 

 

<!-- Google Adsense Begin <div style="margin: 3px; float: right;"> </div> Google Adsense End --><!--content with more link-->

和前面的Flex中如何通过filterFunction属性对XMLListCollection进行筛选的例子类似的,接下来的例子演示了Flex中如何利用Array.filter()事件,从一个数组中移除重复项目。

 

<?xml version="1.0" encoding="utf-8"?> 
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" 
        layout="vertical" 
        verticalAlign="middle" 
        backgroundColor="white" 
        creationComplete="init()"> 
  
    <mx:Script> 
        <![CDATA[ 
            private var keys:Object = {}; 
  
            /** 
             * Called by the Application container's creationComplete 
             * event handler. This method creates a new Array object 
             * which will be used as a data provider as well as a 
             * filtered view of that array which does not contain 
             * duplicated items. 
             */ 
            private function init():void { 
                /* Create a dummy data source with some semi-random 
                    data. */ 
                var arr:Array = []; 
                arr.push({data:1, label:"one"}); 
                arr.push({data:1, label:"one"}); 
                arr.push({data:1, label:"one"}); 
                arr.push({data:1, label:"one"}); 
                arr.push({data:2, label:"two"}); 
                arr.push({data:2, label:"two"}); 
                arr.push({data:2, label:"two"}); 
                arr.push({data:1, label:"one"}); 
                arr.push({data:3, label:"three"}); 
                arr.push({data:3, label:"three"}); 
  
                /* Filter the original array and call the 
                    removeDuplicates() function on each item 
                    in the array. */ 
                var filteredArr:Array = arr.filter(removedDuplicates); 
  
                arrColl.source = arr; 
                dedupedArrColl.source = filteredArr; 
            } 
  
            /** 
             * This method is used to filter an array so that no 
             * duplicate items are created. It works by first 
             * checking to see if a keys object already contains 
             * a key equal to the current value of the item.data 
             * value. If the key already exists, the  current item 
             * will not be readded to the data provider. If the key 
             * does not already exist, add the key to the keys 
             * object and add this item to the data provider. 
             */ 
            private function removedDuplicates(item:Object, idx:uint, arr:Array):Boolean { 
                if (keys.hasOwnProperty(item.data)) { 
                    /* If the keys Object already has this property, 
                        return false and discard this item. */ 
                    return false; 
                } else { 
                    /* Else the keys Object does *NOT* already have 
                        this key, so add this item to the new data 
                        provider. */ 
                    keys[item.data] = item; 
                    return true; 
                } 
            } 
        ]]> 
    </mx:Script> 
  
    <mx:ArrayCollection id="arrColl" /> 
    <mx:ArrayCollection id="dedupedArrColl" /> 
  
    <mx:HBox> 
        <mx:VBox> 
            <mx:Label text="Original ({arrColl.length} items):" /> 
            <mx:List dataProvider="{arrColl}" /> 
        </mx:VBox> 
        <mx:VBox> 
            <mx:Label text="Filtered ({dedupedArrColl.length} items):" /> 
            <mx:List dataProvider="{dedupedArrColl}" /> 
        </mx:VBox> 
    </mx:HBox> 
  
</mx:Application> 

 

<template> <div> <!-- 编辑模态框 --> <ElDialog v-model="showEditModal" title="编辑测试用例" width="70%" :close-on-click-modal="false"> <ElForm ref="editFormRef" :model="editForm" :rules="editFormRules" label-width="120px"> <!-- 使用过滤后的字段 --> <ElFormItem v-for="(field, key) in filteredTestCaseFields" :key="key" :label="field.label" :prop="key"> <template v-if="field.type === 'select'"> <ElSelect v-model="editForm[key]" :placeholder="`请选择${field.label}`" class="w-full" @focus="loadOptionsIfNeeded(key)" :loading="optionsloading[key]" > <ElOption v-for="option in field.options" :key="option.value" :label="option.label" :value="option.value" /> </ElSelect> </template> <template v-else-if="key === 'test_steps'"> <TestStepAdd :test-steps="editForm.test_steps" :expected-results="editForm.expected_result" fieldname="测试步骤" @update:test-steps="editForm.test_steps = $event" @update:expected-results="editForm.expected_result = $event" /> </template> <template v-else-if="field.type === 'select-multi'"> <ElSelect v-model="editForm[key]" :placeholder="`请选择${field.label}`" class="w-full" multiple @focus="loadOptionsIfNeeded(key)" @blur="handleSort(editForm, key)" :loading="optionsloading[key]" > <ElOption v-for="option in field.options" :key="option.value" :label="option.label" :value="option.value" /> <template #footer> <el-button v-if="!isAdding[key]" text bg size="small" @click="() => onAddOption(key)"> 增加选项 </el-button> <template v-else> <el-input v-model="optionName[key]" class="option-input" placeholder="输入选项" size="small" /> <el-button type="primary" size="small" @click="() => onConfirm(key)"> 确认 </el-button> <el-button size="small" @click="() => clear(key)"> 取消 </el-button> </template> </template> </ElSelect> </template> <template v-else-if="field.type === 'image'"> <el-upload class="upload-demo" :auto-upload="false" :on-change="(file) => handleImageChange(file, key, 'edit')" :on-remove="(file) => handleImageRemove(key, 'edit', file)" :on-preview="(file) => handlePreview(file)" :limit="3" :file-list="getFileList(key, 'edit')" list-type="picture-card" accept="image/*" multiple > <el-icon><Plus /></el-icon> <!-- <el-button type="primary">选择图片</el-button> --> <template #tip> <div class="el-upload__tip">最多3张,支持jpg、png格式</div> </template> </el-upload> </template> <template v-else> <ElInput v-model="editForm[key]" :placeholder="`请输入${field.label}`" :type="field.type" :rows="field.type === 'textarea' ? 3 : undefined" :disabled=" key === 'test_case_id' || key === 'test_task' || key === 'modification_date' || key === 'modification_user' || key === 'scenario' " /> </template> </ElFormItem> </ElForm> <template #footer> <span class="dialog-footer"> <ElButton @click="showEditModal = false">取消</ElButton> <ElButton type="primary" @click="handleEditSubmit">确定</ElButton> </span> </template> </ElDialog> <!-- 新建用例模态框 --> <ElDialog v-model="showNewCaseModal" title="新建测试用例" width="70%" :close-on-click-modal="false"> <ElForm ref="newFormRef" :model="newForm" :rules="editFormRules" label-width="120px"> <!-- 使用过滤后的字段 --> <ElFormItem v-for="(field, key) in filteredTestCaseFields" :key="key" :label="field.label" :prop="key"> <template v-if="field.type === 'select'"> <ElSelect v-model="newForm[key]" :placeholder="`请选择${field.label}`" class="w-full" @focus="loadOptionsIfNeeded(key)" :loading="optionsloading[key]" > <ElOption v-for="option in field.options" :key="option.value" :label="option.label" :value="option.value" /> </ElSelect> </template> <template v-else-if="key === 'test_steps'"> <TestStepAdd :test-steps="newForm.test_steps" :expected-results="newForm.expected_result" fieldname="测试步骤" @update:test-steps="newForm.test_steps = $event" @update:expected-results="newForm.expected_result = $event" /> </template> <template v-else-if="field.type === 'select-multi'"> <ElSelect v-model="newForm[key]" :placeholder="`请选择${field.label}`" class="w-full" multiple @focus="loadOptionsIfNeeded(key)" @blur="handleSort(newForm, key)" :loading="optionsloading[key]" > <ElOption v-for="option in field.options" :key="option.value" :label="option.label" :value="option.value" /> <template #footer> <el-button v-if="!isAdding[key]" text bg size="small" @click="() => onAddOption(key)"> 增加选项 </el-button> <template v-else> <el-input v-model="optionName[key]" class="option-input" placeholder="输入选项" size="small" /> <el-button type="primary" size="small" @click="() => onConfirm(key)"> 确认 </el-button> <el-button size="small" @click="() => clear(key)"> 取消 </el-button> </template> </template> </ElSelect> </template> <template v-else-if="field.type === 'image'"> <el-upload class="upload-demo" :auto-upload="false" :on-change="(file) => handleImageChange(file, key, 'new')" :on-remove="(file) => handleImageRemove(key, 'new', file)" :on-preview="(file) => handlePreview(file)" :limit="3" :file-list="getFileList(key, 'new')" list-type="picture-card" accept="image/*" multiple > <el-icon><Plus /></el-icon> <template #tip> <div class="el-upload__tip">最多3张,支持jpg、png格式</div> </template> </el-upload> </template> <template v-else> <ElInput v-model="newForm[key]" :placeholder="key === 'test_case_id' ? '由功能名称决定' : `请输入${field.label}`" :type="field.type" :rows="field.type === 'textarea' ? 3 : undefined" @input="key === 'test_case_id' ? checkIdDuplicate() : null" :class="{ 'is-error': key === 'test_case_id' && isIdDuplicate }" :disabled=" key === 'test_task' || key === 'modification_date' || key === 'test_case_id' || key === 'modification_user' " /> </template> <div v-if="key === 'test_case_id' && isIdDuplicate" class="el-form-item__error"> 用例ID已存在,请使用其他ID </div> </ElFormItem> </ElForm> <template #footer> <span class="dialog-footer"> <ElButton @click="showNewCaseModal = false">取消</ElButton> <ElButton type="primary" @click="handleNewSubmit">确定</ElButton> </span> </template> </ElDialog> <!-- 预览对话框 --> <el-dialog v-model="previewDialogVisible" title="图片预览" width="50%" :close-on-click-modal="true"> <div class="flex justify-center"> <img :src="previewImageUrl" class="max-w-full max-h-[70vh] object-contain" alt="预览图片" /> </div> </el-dialog> </div> </template> <script setup> import { ref, watch, computed } from 'vue' import { ElMessage, ElMessageBox } from 'element-plus' import { testcaseApi } from '@/api/modules/api.testcases' import { testCaseFields, getRequiredFields, getDefaultFormData } from '@/views/testcases/fields' import TestStepAdd from './TestStepAdd.vue' const props = defineProps({ module: { type: String, required: true }, tableData: { type: Array, default: () => [] } }) const emit = defineEmits(['refresh']) // 状态变量 const showEditModal = ref(false) const showNewCaseModal = ref(false) const editFormRef = ref(null) const newFormRef = ref(null) const isIdDuplicate = ref(false) const uploadedFiles = ref({}) const previewDialogVisible = ref(false) const previewImageUrl = ref('') const isAdding = reactive({}) const optionName = reactive({}) // 选项加载状态 const optionsloading = reactive({}) // 选项是否已加载 const optionsLoaded = reactive({}) // 表单数据 const editForm = ref(getDefaultFormData()) const newForm = ref(getDefaultFormData()) // 历史用例类型,功能,新用例id const editscenario = ref('') const edittestcaseid = ref('') // 使用配置的必填字段生成验证规则 const editFormRules = {} getRequiredFields().forEach((field) => { editFormRules[field] = [ { required: true, message: `请输入${testCaseFields[field].label}`, trigger: 'blur' } ] }) // 获取字段选项 const loadOptionsIfNeeded = async (key) => { // 如果选项已加载或正在加载,不重复请求 if (optionsLoaded[key] || optionsloading[key]) { return } optionsloading[key] = true try { // 调用API获取选项 const response = await testcaseApi.getFieldOptions(key) // 更新字段配置中的选项 testCaseFields[key].options = response.data.option // 标记选项已加载 optionsLoaded[key] = true } catch (error) { console.error(`加载${testCaseFields[key].label}选项失败`, error) // 显示错误提示 ElMessage.error(`加载${testCaseFields[key].label}选项失败: ${error.message}`) } finally { optionsloading[key] = false } } // 获取北京时间的函数 const getBeijingTime = () => { const now = new Date() const utc = now.getTime() + now.getTimezoneOffset() * 60000 const beijingTime = new Date(utc + 3600000 * 8) return beijingTime } // 格式化时间为 YYYY-MM-DD HH:mm:ss const formatDateTime = (date) => { const year = date.getFullYear() const month = String(date.getMonth() + 1).padStart(2, '0') const day = String(date.getDate()).padStart(2, '0') const hours = String(date.getHours()).padStart(2, '0') const minutes = String(date.getMinutes()).padStart(2, '0') const seconds = String(date.getSeconds()).padStart(2, '0') return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}` } // 生成唯一ID const generateUniqueId = () => { return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { const r = (Math.random() * 16) | 0 const v = c === 'x' ? r : (r & 0x3) | 0x8 return v.toString(16) }) } // 获取文件列表 const getFileList = (field, formType = 'edit') => { if (!uploadedFiles.value[formType]) { uploadedFiles.value[formType] = {} } if (!uploadedFiles.value[formType][field]) { uploadedFiles.value[formType][field] = [] } return uploadedFiles.value[formType][field] } // 处理图片变更 - 支持多文件 const handleImageChange = (file, field, formType = 'edit') => { if (file.raw) { // 生成唯一ID作为图片名称 const uniqueId = generateUniqueId() // 获取文件扩展名 const fileExtension = file.raw.name.split('.').pop() // 创建新的文件名 const newFileName = `${uniqueId}.${fileExtension}` // 创建新的 File 对象,使用新的文件名 const newFile = new File([file.raw], newFileName, { type: file.raw.type, lastModified: file.raw.lastModified }) // 初始化表单数据存储结构 if (!uploadedFiles.value[formType]) { uploadedFiles.value[formType] = {} } if (!uploadedFiles.value[formType][field]) { uploadedFiles.value[formType][field] = [] } // 限制数量(保留最新上传的3个文件) const fileList = [ ...uploadedFiles.value[formType][field], { name: file.name, raw: newFile, originalName: file.name, uniqueId: uniqueId, // 新增唯一标识 url: URL.createObjectURL(newFile) } ] if (fileList.length > 3) { fileList.shift() // 移除最早的文件 } // 更新文件列表 uploadedFiles.value[formType][field] = fileList // 更新表单数据,如果是文件存文件,不是文件存URL if (formType === 'edit') { editForm.value[field] = fileList.map((f) => (f.raw ? f.raw : f.url)) } else { newForm.value[field] = fileList.map((f) => (f.raw ? f.raw : f.url)) } } } // 处理图片移除 - 支持多文件 const handleImageRemove = (field, formType = 'edit', fileToRemove) => { if (!uploadedFiles.value[formType] || !uploadedFiles.value[formType][field]) return // 过滤掉要移除的文件(兼容编辑模式的不同结构) const updatedFiles = uploadedFiles.value[formType][field].filter((file) => { if (formType === 'edit' && file.url && !file.uniqueId) { return file.url !== fileToRemove.url // 通过 URL 匹配移除 } return file.uniqueId !== fileToRemove.uniqueId }) // 更新文件列表 uploadedFiles.value[formType][field] = updatedFiles // 更新表单数据 if (formType === 'edit') { // 新增:编辑模式下直接使用 URL 数组 editForm.value[field] = updatedFiles.map((f) => f.url || f.raw) } else { newForm.value[field] = updatedFiles.map((f) => f.url || f.raw) } // 如果预览的是被移除的图片,则关闭预览 if ( fileToRemove.url === previewImageUrl.value || (fileToRemove.raw && URL.createObjectURL(fileToRemove.raw) === previewImageUrl.value) ) { previewDialogVisible.value = false } } // 处理图片预览 const handlePreview = (file) => { if (file.url) { previewImageUrl.value = file.url } else if (file.raw) { previewImageUrl.value = URL.createObjectURL(file.raw) } previewDialogVisible.value = true } // 拆分 URL 字符串 const splitImageUrls = (url) => { if (!url) return const imageUrls = url .split(',') .map((url) => url.trim()) .map((path) => (path.startsWith('http') ? path : `${import.meta.env.VITE_APP_BASE_URL}${path}`)) .map((path) => path.replace(':81', ':80')) // 去除可能的空格 return imageUrls } // 检查ID是否重复 const checkIdDuplicate = () => { if (!newForm.value.test_case_id) { isIdDuplicate.value = false return } isIdDuplicate.value = props.tableData.some((item) => item.test_case_id === newForm.value.test_case_id) } //增加选项 const onAddOption = (key) => { isAdding[key] = true } const onConfirm = async (key) => { const newLabel = optionName[key].trim() const newValue = optionName[key].trim() if (!newLabel) { ElMessage.warning('选项名称不能为空') return } // 检查前端选项列表中是否已存在 const exists = testCaseFields[key].options.some( (option) => option.value === newValue || option.label === newLabel ) if (exists) { ElMessage.warning('选项已存在') clear(key) return } if (optionName[key]) { const response = await testcaseApi.createFieldOptions(key, optionName[key], optionName[key]) if (response.status) { // 添加新选项到 field.options testCaseFields[key].options.push({ label: optionName[key], value: optionName[key] }) } // 清空输入并隐藏添加表单 clear(key) } } const clear = (key) => { optionName[key] = '' isAdding[key] = false } const fieldToLabel = (fieldName, fieldsConfig = testCaseFields) => { // 从配置中查找字段的标签 const fieldConfig = fieldsConfig[fieldName] return fieldConfig.label } // 提交编辑 const handleEditSubmit = async () => { if (!editFormRef.value) return try { await editFormRef.value.validate() // 冲突验证 // 获取后端最新的版本,检查最后修改时间 const lastestTestCase = await testcaseApi.getTestcase(props.module, editForm.value.test_case_id) // 如果最后修改时间与当前编辑的用例一致,则说明没有冲突,反之。 if ( editForm.value.modification_date !== lastestTestCase.data.modification_date && lastestTestCase.data.test_case_history[0].changes && Object.keys(lastestTestCase.data.test_case_history[0].changes).length > 0 ) { // 构建修改内容摘要列表 let changesHtml = '' if (lastestTestCase.data.test_case_history[0].changes) { const changes = lastestTestCase.data.test_case_history[0].changes changesHtml = Object.entries(changes) .filter( ([field]) => field !== 'modification_date' && field !== 'modification_status' && field !== 'modification_user' ) .map( ([field, change]) => ` <li class="mb-2"> <div> <span class="text-sm font-medium text-gray-700"> ${fieldToLabel(field)} </span> <p class="text-sm text-gray-500 mt-1"> 旧值:${change.old || '无'}<br /> 新值:${change.new || '无'} </p> </div> </li> ` ) .join('') } const message = ` <div style="line-height: 1.6; color: #666;"> <p>检测到冲突:该用例已被<strong>${lastestTestCase.data.modification_user}</strong>修改</p> <p>最后修改时间:<strong>${lastestTestCase.data.modification_date}</strong></p> <p>修改内容摘要:</p> <ul class="space-y-3" style="margin-left: 20px; list-style-type: disc;"> ${changesHtml} </ul> </div> ` ElMessageBox.confirm(message, '编辑冲突', { confirmButtonText: '加载最新', cancelButtonText: '继续编辑', type: 'warning', dangerouslyUseHTMLString: true }) .then(() => { // 用户选择加载最新版本 openEditModal(lastestTestCase.data) }) .catch(() => { // 用户选择继续编辑 editForm.value.modification_date = lastestTestCase.data.modification_date }) return } const formData = new FormData() Object.entries(editForm.value).forEach(([key, value]) => { if ( value === null || value === undefined || value === '' || (Array.isArray(value) && value.length === 0) ) { return } else if (key === 'ui' || key === 'case_source_screenshot') { // 确保value是数组(兼容单文件场景) const files = Array.isArray(value) ? value : [value] files.forEach((file) => { if (file instanceof File) { formData.append(key, file) // 逐个添加File对象 } else { formData.append(key, String(file)) } }) } else if (key === 'test_steps' || key === 'expected_result') { // 在每个步骤前加上序号 const numberedSteps = value.map((step, index) => `${index + 1}. ${step}`) formData.append(key, numberedSteps.join('\n')) } else { formData.append(key, String(value)) } }) formData.append('modification_date', formatDateTime(getBeijingTime())) formData.append( 'modification_status', editForm.value.modification_status === '新建' ? '修改' : editForm.value.modification_status ) await testcaseApi.postTestcases(props.module, formData) ElMessage.success('更新成功') showEditModal.value = false emit('refresh') } catch (error) { if (error.response) { ElMessage.error(error.response.data.message || '更新失败') } else { ElMessage.error('更新失败') } } } // 提交新建 const handleNewSubmit = async () => { if (!newFormRef.value) return try { if (isIdDuplicate.value) { ElMessage.error('用例ID已存在,请使用其他ID') return } await newFormRef.value.validate() const formData = new FormData() Object.entries(newForm.value).forEach(([key, value]) => { if (key === 'ui' || key === 'case_source_screenshot') { // 确保value是数组(兼容单文件场景) const files = Array.isArray(value) ? value : [value] files.forEach((file) => { if (file instanceof File) { formData.append(key, file) // 逐个添加File对象 } else { formData.append(key, String(file)) } }) } else if (key === 'test_steps' || key === 'expected_result') { // 在每个步骤前加上序号 const numberedSteps = value.map((step, index) => `${index + 1}. ${step}`) formData.append(key, numberedSteps.join('\n')) } else { formData.append(key, String(value)) } }) formData.append('modification_date', formatDateTime(getBeijingTime())) formData.append('modification_status', '新建') await testcaseApi.postTestcases(props.module, formData) ElMessage.success('创建成功') showNewCaseModal.value = false newForm.value = getDefaultFormData() uploadedFiles.value = {} isIdDuplicate.value = false emit('refresh') } catch (error) { if (error.response) { ElMessage.error(error.response.data.message || '创建失败') } else { ElMessage.error('创建失败') } } } // 打开编辑模态框 const openEditModal = (row) => { editForm.value = { ...row } editscenario.value = row.scenario edittestcaseid.value = row.test_case_id // 将字符串转换为数组,并移除序号 if (typeof row.test_steps === 'string') { // 处理测试步骤 if (row.test_steps) { const steps = row.test_steps.split('\n') editForm.value.test_steps = steps .map((step) => { // 移除开头的序号(例如:"1. 步骤内容" -> "步骤内容") return step.replace(/^\d+\.\s*/, '').trim() }) .filter((step) => step !== '') // 过滤掉空步骤 } else { editForm.value.test_steps = [''] } } else if (!Array.isArray(editForm.value.test_steps)) { editForm.value.test_steps = [''] } // 处理预期结果 if (typeof row.expected_result === 'string') { if (row.expected_result) { const results = row.expected_result.split('\n') editForm.value.expected_result = results .map((result) => { // 移除开头的序号 return result.replace(/^\d+\.\s*/, '').trim() }) .filter((result) => result !== '') // 过滤掉空结果 } else { editForm.value.expected_result = [''] } } else if (!Array.isArray(editForm.value.expected_result)) { editForm.value.expected_result = [''] } // 确保测试步骤和预期结果数量一致 const maxLength = Math.max(editForm.value.test_steps.length, editForm.value.expected_result.length) while (editForm.value.test_steps.length < maxLength) { editForm.value.test_steps.push('') } while (editForm.value.expected_result.length < maxLength) { editForm.value.expected_result.push('') } // 初始化图片字段的文件列表 Object.keys(testCaseFields).forEach((key) => { if (testCaseFields[key].type === 'image' && row[key]) { const imageUrls = splitImageUrls(row[key]) if (!uploadedFiles.value.edit) { uploadedFiles.value.edit = {} } // 将每个URL单独添加到uploadedFiles中 uploadedFiles.value.edit[key] = imageUrls.map((url, index) => ({ url: url })) editForm.value[key] = imageUrls } else if (testCaseFields[key].type === 'select-multi') { if (typeof row[key] === 'string') { const allValues = row[key].split(/[、,]/) const validValues = allValues.filter((value) => value.trim() !== '') editForm.value[key] = validValues } } }) showEditModal.value = true } // 打开新建模态框 const openNewModal = (row) => { if (row instanceof PointerEvent) { newForm.value = getDefaultFormData() uploadedFiles.value = {} isIdDuplicate.value = false } else { newForm.value = { ...row } newForm.value.test_case_id = '' delete newForm.value.test_case_history editscenario.value = row.scenario edittestcaseid.value = row.test_case_id // 将字符串转换为数组,并移除序号 if (typeof row.test_steps === 'string') { // 处理测试步骤 if (row.test_steps) { const steps = row.test_steps.split('\n') newForm.value.test_steps = steps .map((step) => { // 移除开头的序号(例如:"1. 步骤内容" -> "步骤内容") return step.replace(/^\d+\.\s*/, '').trim() }) .filter((step) => step !== '') // 过滤掉空步骤 } else { newForm.value.test_steps = [''] } } else if (!Array.isArray(newForm.value.test_steps)) { newForm.value.test_steps = [''] } // 处理预期结果 if (typeof row.expected_result === 'string') { if (row.expected_result) { const results = row.expected_result.split('\n') newForm.value.expected_result = results .map((result) => { // 移除开头的序号 return result.replace(/^\d+\.\s*/, '').trim() }) .filter((result) => result !== '') // 过滤掉空结果 } else { newForm.value.expected_result = [''] } } else if (!Array.isArray(newForm.value.expected_result)) { newForm.value.expected_result = [''] } // 确保测试步骤和预期结果数量一致 const maxLength = Math.max(newForm.value.test_steps.length, newForm.value.expected_result.length) while (newForm.value.test_steps.length < maxLength) { newForm.value.test_steps.push('') } while (newForm.value.expected_result.length < maxLength) { newForm.value.expected_result.push('') } // 初始化图片字段的文件列表 Object.keys(testCaseFields).forEach((key) => { if (testCaseFields[key].type === 'image' && row[key]) { const imageUrls = splitImageUrls(row[key]) if (!uploadedFiles.value.new) { uploadedFiles.value.new = {} } // 将每个URL单独添加到uploadedFiles中 uploadedFiles.value.new[key] = imageUrls.map((url, index) => ({ url: url })) newForm.value[key] = imageUrls } else if (testCaseFields[key].type === 'select-multi') { if (typeof row[key] === 'string') { const allValues = row[key].split(/[、,]/) const validValues = allValues.filter((value) => value.trim() !== '') newForm.value[key] = validValues } } }) } showNewCaseModal.value = true } // 处理排序逻辑 const handleSort = (formRef, key) => { // const formValue = formRef.value; // 获取当前选中的值 let selectedValues = formRef[key] // 对值进行排序 selectedValues.sort((a, b) => { // 如果值是对象,假设对象有一个label属性作为显示文本 if (typeof a === 'object' && typeof b === 'object') { return a.label.localeCompare(b.label) } // 如果值是基本类型(字符串或数字) return a.toString().localeCompare(b.toString()) }) // 更新绑定的数据 formRef[key] = selectedValues } // 监听预览对话框关闭 watch(previewDialogVisible, (newVal) => { if (!newVal) { if (previewImageUrl.value.startsWith('blob:')) { URL.revokeObjectURL(previewImageUrl.value) } previewImageUrl.value = '' } }) // 监听编辑模态框关闭 watch(showEditModal, (newVal) => { if (!newVal) { uploadedFiles.value = {} // 重置所有字段的添加选项状态 Object.keys(isAdding).forEach((key) => { isAdding[key] = false optionName[key] = '' }) } }) // 监听新建模态框关闭 watch(showNewCaseModal, (newVal) => { if (!newVal) { newForm.value = getDefaultFormData() uploadedFiles.value = {} isIdDuplicate.value = false // 重置所有字段的添加选项状态 Object.keys(isAdding).forEach((key) => { isAdding[key] = false optionName[key] = '' }) } }) // 添加计算属性来过滤字段 const filteredTestCaseFields = computed(() => { const fields = { ...testCaseFields } if (showNewCaseModal.value || showEditModal.value) { delete fields.expected_result } if (showNewCaseModal.value) { delete fields.test_case_id } return fields }) // 暴露方法给父组件 defineExpose({ openEditModal, openNewModal }) </script> <style scoped> .dialog-footer { display: flex; justify-content: flex-end; gap: 10px; } .option-input { width: 100%; margin-bottom: 8px; } </style> 将新建和编辑逻辑统一,不改变任何功能
07-30
<template> <div class="app-container"> <el-row :gutter="20"> <!-- 项目树菜单 --> <el-col :span="6" :xs="24"> <div class="head-container"> <el-input v-model="projectName" placeholder="请输入项目名称" clearable size="mini" prefix-icon="el-icon-search" style="margin-bottom: 20px" @input="filterProjectTree" /> </div> <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" 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" > <span class="custom-tree-node" slot-scope="{ node, data }"> <div v-if="node.label && node.label.length > 25"> <el-tooltip :show-after="300" :content="node.label" placement="top-start"> <span>{{ ellipsis(node.label, 25) }}</span> </el-tooltip> </div> <div v-else> <span>{{ node.label }}</span> </div> <span> <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> </span> </span> </el-tree> </el-col> <!-- 文档区域 --> <el-col :span="18" :xs="24"> <!-- 文档列表视图 --> <div v-if="activeView === 'list'"> <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-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)" v-hasPermi="['cms:project:deleteArticleFromProject']"></el-button> </template> </el-table-column> </el-table> <pagination v-show="docTotal>0" :total="docTotal" :page.sync="docQueryParams.pageNum" :limit.sync="docQueryParams.pageSize" @pagination="getDocumentList" /> </div> <!-- 文档详情视图 --> <div v-else-if="activeView === 'detail'"> <div class="doc-header"> <el-button type="text" icon="el-icon-back" @click="backToList">返回文档列表</el-button> <h2>{{ currentDocument.title }}</h2> </div> <!-- 富文本编辑器 --> <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> </div> </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> <div slot="footer"> <el-button @click="projectDialogVisible = false">取消</el-button> <el-button type="primary" @click="saveProject">保存</el-button> </div> </el-dialog> </div> </template> <script> import { listData, getData, addData, updateData } from "@/api/cms/data"; import { getProjectTree, saveProject, updateProject, deleteProject, removeMenusFromProject // 导入新的API方法 } from "@/api/cms/articleProject"; 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: [], 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, // 文档详情相关 currentDocument: { id: null, menuId: null, title: '', content: '', status: '0' }, // 当前选中的项目ID(用于删除操作) currentProjectId: null }; }, 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; } 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); }); } } }; </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; max-height: 70vh; overflow-y: auto; } .custom-tree-node .el-button { padding: 4px; margin-left: 5px; } </style> 该页面搜索栏没生效,表格要有多选功能,改变原有的功能下优化该页面的ui和交互,图标也要加上,添加一个批量导出按钮
最新发布
08-02
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值