重构历史项目导入弹窗

This commit is contained in:
wkc
2026-03-29 09:58:55 +08:00
parent a019abb950
commit f2cc9e2700
4 changed files with 168 additions and 219 deletions

View File

@@ -64,3 +64,10 @@
- 调整 `ruoyi-ui/src/views/ccdiProject/index.vue``handleSubmitImport`,成功后提示“历史项目导入任务已开始”、关闭弹窗、刷新列表并跳转到新项目详情页 - 调整 `ruoyi-ui/src/views/ccdiProject/index.vue``handleSubmitImport`,成功后提示“历史项目导入任务已开始”、关闭弹窗、刷新列表并跳转到新项目详情页
- 新增 `ruoyi-ui/tests/unit/project-import-history-submit-flow.test.js`,锁定真实接口地址、成功提示文案与详情页跳转路径 - 新增 `ruoyi-ui/tests/unit/project-import-history-submit-flow.test.js`,锁定真实接口地址、成功提示文案与详情页跳转路径
- 验证命令:`cd ruoyi-ui && node tests/unit/project-import-history-submit-flow.test.js` - 验证命令:`cd ruoyi-ui && node tests/unit/project-import-history-submit-flow.test.js`
### 2026-03-29 Task 2 前端导入弹窗重构
-`ImportHistoryDialog.vue` 从单选 + Mock 数据改为“上方新项目配置、下方历史项目多选列表”的纵向结构
- 接入 `listHistoryProjects``importFromHistory` 真实接口,提交时按计划组装 `projectName/description/sourceProjectIds/startDate/endDate`
- 新增 `project-import-history-dialog-layout.test.js``project-import-history-dialog-behavior.test.js`,锁定备注字段、日期范围、多选语义、真实接口调用与 `response.data` 回传
- 验证命令:`cd ruoyi-ui && node tests/unit/project-import-history-dialog-layout.test.js && node tests/unit/project-import-history-dialog-behavior.test.js`

View File

@@ -6,76 +6,7 @@
:close-on-click-modal="false" :close-on-click-modal="false"
@close="handleClose" @close="handleClose"
> >
<!-- 搜索区 -->
<div class="search-section">
<el-input
v-model="searchKeyword"
placeholder="搜索历史项目名称"
prefix-icon="el-icon-search"
clearable
size="small"
style="width: 300px"
>
<el-button slot="append" icon="el-icon-search" @click="handleSearch" />
</el-input>
<el-date-picker
v-model="dateRange"
type="daterange"
range-separator=""
start-placeholder="开始日期"
end-placeholder="结束日期"
size="small"
style="width: 240px; margin-left: 12px"
/>
</div>
<!-- 历史项目列表 -->
<div class="project-list-section">
<div class="section-title">历史项目列表</div>
<el-radio-group v-model="selectedProjectId" class="project-radio-group">
<div
v-for="item in filteredProjects"
:key="item.projectId"
class="project-item-wrapper"
>
<el-radio :label="item.projectId" class="project-radio">
<div class="project-item">
<div class="project-header">
<span class="name">{{ item.projectName }}</span>
<el-tag
:type="getStatusType(item.projectStatus)"
size="mini"
>{{ getStatusLabel(item.projectStatus) }}</el-tag>
</div>
<div class="project-info">
<span class="info-item">
<i class="el-icon-time"></i>
创建时间: {{ item.createTime }}
</span>
<span class="info-item">
<i class="el-icon-user"></i>
目标人数: {{ item.targetCount }}
</span>
<span class="info-item warning">
<i class="el-icon-warning"></i>
预警人数: {{ item.warningCount }}
</span>
</div>
</div>
</el-radio>
</div>
</el-radio-group>
<el-empty
v-if="filteredProjects.length === 0"
description="暂无历史项目"
:image-size="100"
/>
</div>
<!-- 新项目配置 -->
<div class="new-project-config"> <div class="new-project-config">
<el-divider />
<el-form <el-form
ref="configForm" ref="configForm"
:model="configForm" :model="configForm"
@@ -86,45 +17,92 @@
<el-form-item label="新项目名称" prop="newProjectName"> <el-form-item label="新项目名称" prop="newProjectName">
<el-input <el-input
v-model="configForm.newProjectName" v-model="configForm.newProjectName"
placeholder="新项目名称(可自动生成)" placeholder="请输入新项目名称"
maxlength="50" maxlength="200"
/>
</el-form-item>
<el-form-item label="备注" prop="description">
<el-input
v-model="configForm.description"
type="textarea"
:rows="3"
maxlength="500"
show-word-limit
placeholder="请输入备注"
/>
</el-form-item>
<el-form-item label="流水时间">
<el-date-picker
v-model="configForm.dateRange"
type="daterange"
value-format="yyyy-MM-dd"
range-separator=""
start-placeholder="开始日期"
end-placeholder="结束日期"
style="width: 100%"
/> />
</el-form-item> </el-form-item>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="开始日期" prop="startDate">
<el-date-picker
v-model="configForm.startDate"
type="date"
placeholder="选择开始日期"
value-format="yyyy-MM-dd"
style="width: 100%"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="结束日期" prop="endDate">
<el-date-picker
v-model="configForm.endDate"
type="date"
placeholder="选择结束日期"
value-format="yyyy-MM-dd"
:picker-options="endDatePickerOptions"
style="width: 100%"
/>
</el-form-item>
</el-col>
</el-row>
</el-form> </el-form>
</div> </div>
<el-divider />
<div class="project-list-section">
<div class="section-header">
<div class="section-title">历史项目列表</div>
<el-input
v-model="searchKeyword"
placeholder="搜索历史项目名称"
prefix-icon="el-icon-search"
clearable
size="small"
class="search-input"
@clear="loadHistoryProjects"
@keyup.enter.native="handleSearch"
>
<el-button slot="append" icon="el-icon-search" @click="handleSearch" />
</el-input>
</div>
<el-checkbox-group v-model="selectedProjectIds" class="project-checkbox-group">
<label
v-for="item in historyProjects"
:key="item.projectId"
class="project-item-wrapper"
>
<el-checkbox :label="item.projectId" class="project-checkbox">
<div class="project-item">
<div class="project-header">
<span class="name">{{ item.projectName }}</span>
<el-tag :type="getStatusType(item.status)" size="mini">{{ getStatusLabel(item.status) }}</el-tag>
</div>
<div class="project-info">
<span class="info-item">
<i class="el-icon-time"></i>
创建时间: {{ item.createTime || '-' }}
</span>
<span class="info-item">
<i class="el-icon-document"></i>
备注: {{ item.description || '-' }}
</span>
</div>
</div>
</el-checkbox>
</label>
</el-checkbox-group>
<el-empty
v-if="historyProjects.length === 0"
description="暂无历史项目"
:image-size="100"
/>
</div>
<div slot="footer" class="dialog-footer"> <div slot="footer" class="dialog-footer">
<el-button @click="handleClose"> </el-button> <el-button @click="handleClose"> </el-button>
<el-button <el-button
type="primary" type="primary"
:loading="submitting" :loading="submitting"
:disabled="!selectedProjectId"
@click="handleSubmit" @click="handleSubmit"
> >
<i v-if="!submitting" class="el-icon-download"></i> <i v-if="!submitting" class="el-icon-download"></i>
@@ -135,7 +113,7 @@
</template> </template>
<script> <script>
import {getMockHistoryProjects} from '@/api/ccdiProject' import { importFromHistory, listHistoryProjects } from '@/api/ccdiProject'
export default { export default {
name: 'ImportHistoryDialog', name: 'ImportHistoryDialog',
@@ -146,45 +124,23 @@ export default {
} }
}, },
data() { data() {
// 结束日期验证规则
const validateEndDate = (rule, value, callback) => {
if (value && this.configForm.startDate && value < this.configForm.startDate) {
callback(new Error('结束日期不能早于开始日期'))
} else {
callback()
}
}
return { return {
submitting: false, submitting: false,
searchKeyword: '', searchKeyword: '',
dateRange: null, selectedProjectIds: [],
selectedProjectId: null,
historyProjects: [], historyProjects: [],
configForm: { configForm: {
newProjectName: '', newProjectName: '',
startDate: '', description: '',
endDate: '' dateRange: []
}, },
configRules: { configRules: {
newProjectName: [ newProjectName: [
{ required: true, message: '请输入新项目名称', trigger: 'blur' } { required: true, message: '请输入新项目名称', trigger: 'blur' }
], ],
startDate: [ description: [
{ required: true, message: '请选择开始日期', trigger: 'change' } { max: 500, message: '备注长度不能超过500个字符', trigger: 'blur' }
],
endDate: [
{ required: true, message: '请选择结束日期', trigger: 'change' },
{ validator: validateEndDate, trigger: 'change' }
] ]
},
endDatePickerOptions: {
disabledDate: (time) => {
if (this.configForm.startDate) {
return time.getTime() < new Date(this.configForm.startDate).getTime()
}
return false
}
} }
} }
}, },
@@ -198,22 +154,6 @@ export default {
this.handleClose() this.handleClose()
} }
} }
},
filteredProjects() {
let result = [...this.historyProjects]
if (this.searchKeyword) {
result = result.filter(item =>
item.projectName.toLowerCase().includes(this.searchKeyword.toLowerCase())
)
}
if (this.dateRange && this.dateRange.length === 2) {
const [start, end] = this.dateRange
result = result.filter(item => {
const createTime = new Date(item.createTime)
return createTime >= new Date(start) && createTime <= new Date(end)
})
}
return result
} }
}, },
watch: { watch: {
@@ -221,81 +161,70 @@ export default {
if (val) { if (val) {
this.loadHistoryProjects() this.loadHistoryProjects()
} }
},
selectedProjectId(val) {
if (val) {
const project = this.historyProjects.find(p => p.projectId === val)
if (project) {
// 自动生成新项目名称
const now = new Date()
const year = now.getFullYear()
const month = String(now.getMonth() + 1).padStart(2, '0')
const day = String(now.getDate()).padStart(2, '0')
this.configForm.newProjectName = `${project.projectName}${year}-${month}-${day}复制)`
}
}
} }
}, },
methods: { methods: {
/** 加载历史项目 */
loadHistoryProjects() { loadHistoryProjects() {
getMockHistoryProjects().then(response => { return listHistoryProjects({
this.historyProjects = response.data projectName: this.searchKeyword || undefined
}).then(response => {
this.historyProjects = response.data || []
}) })
}, },
/** 获取状态类型 */
getStatusType(status) { getStatusType(status) {
const statusMap = { const statusMap = {
'0': 'primary', '0': 'primary',
'1': 'success', '1': 'success',
'2': 'info' '2': 'info',
'3': 'warning'
} }
return statusMap[status] || 'info' return statusMap[status] || 'info'
}, },
/** 获取状态标签 */
getStatusLabel(status) { getStatusLabel(status) {
const statusMap = { const statusMap = {
'0': '进行中', '0': '进行中',
'1': '已完成', '1': '已完成',
'2': '已归档' '2': '已归档',
'3': '打标中'
} }
return statusMap[status] || '未知' return statusMap[status] || '未知'
}, },
/** 搜索 */
handleSearch() { handleSearch() {
// 搜索逻辑在 computed 中处理 this.loadHistoryProjects()
}, },
/** 提交导入 */
handleSubmit() { handleSubmit() {
if (!this.selectedProjectId) { if (this.selectedProjectIds.length === 0) {
this.$message.warning('请选择要导入的历史项目') this.$message.warning('请选择历史项目')
return return
} }
this.$refs.configForm.validate(valid => { this.$refs.configForm.validate(async valid => {
if (valid) { if (!valid) {
this.submitting = true return
const selectedProject = this.historyProjects.find(p => p.projectId === this.selectedProjectId) }
setTimeout(() => { this.submitting = true
this.submitting = false try {
this.$emit('submit', { const response = await importFromHistory({
sourceProjectId: this.selectedProjectId, projectName: this.configForm.newProjectName,
sourceProject: selectedProject, description: this.configForm.description,
...this.configForm sourceProjectIds: this.selectedProjectIds,
}) startDate: this.configForm.dateRange[0] || '',
}, 500) endDate: this.configForm.dateRange[1] || ''
})
this.$emit('submit', response.data)
} finally {
this.submitting = false
} }
}) })
}, },
/** 关闭对话框 */
handleClose() { handleClose() {
this.$emit('close') this.$emit('close')
this.selectedProjectId = null this.selectedProjectIds = []
this.searchKeyword = '' this.searchKeyword = ''
this.dateRange = null this.historyProjects = []
this.configForm = { this.configForm = {
newProjectName: '', newProjectName: '',
startDate: '', description: '',
endDate: '' dateRange: []
} }
this.$refs.configForm?.resetFields() this.$refs.configForm?.resetFields()
} }
@@ -304,26 +233,35 @@ export default {
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.search-section { .new-project-config {
display: flex; margin-bottom: 8px;
align-items: center;
margin-bottom: 16px;
} }
.project-list-section { .project-list-section {
margin-bottom: 16px; margin-bottom: 16px;
.section-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
margin-bottom: 12px;
}
.section-title { .section-title {
font-size: 14px; font-size: 14px;
font-weight: 600; font-weight: 600;
color: #303133; color: #303133;
margin-bottom: 12px; }
.search-input {
width: 280px;
} }
} }
.project-radio-group { .project-checkbox-group {
width: 100%; width: 100%;
max-height: 300px; max-height: 320px;
overflow-y: auto; overflow-y: auto;
&::-webkit-scrollbar { &::-webkit-scrollbar {
@@ -333,14 +271,11 @@ export default {
&::-webkit-scrollbar-thumb { &::-webkit-scrollbar-thumb {
background-color: #dcdfe6; background-color: #dcdfe6;
border-radius: 3px; border-radius: 3px;
&:hover {
background-color: #c0c4cc;
}
} }
} }
.project-item-wrapper { .project-item-wrapper {
display: block;
margin-bottom: 12px; margin-bottom: 12px;
&:last-child { &:last-child {
@@ -348,16 +283,16 @@ export default {
} }
} }
.project-radio { .project-checkbox {
display: block; display: block;
width: 100%; width: 100%;
:deep(.el-radio__label) { :deep(.el-checkbox__label) {
width: calc(100% - 24px); width: calc(100% - 24px);
padding-left: 8px; padding-left: 8px;
} }
:deep(.el-radio__input) { :deep(.el-checkbox__input) {
margin-top: 20px; margin-top: 20px;
} }
} }
@@ -366,11 +301,6 @@ export default {
padding: 12px; padding: 12px;
background-color: #f5f7fa; background-color: #f5f7fa;
border-radius: 8px; border-radius: 8px;
transition: all 0.3s;
&:hover {
background-color: #ecf5ff;
}
.project-header { .project-header {
display: flex; display: flex;
@@ -391,29 +321,15 @@ export default {
gap: 16px; gap: 16px;
.info-item { .info-item {
font-size: 13px;
color: #909399;
display: flex; display: flex;
align-items: center; align-items: center;
gap: 4px; gap: 4px;
font-size: 13px;
i { color: #909399;
font-size: 14px;
}
&.warning {
color: #E6A23C;
}
} }
} }
} }
.new-project-config {
:deep(.el-divider) {
margin: 16px 0;
}
}
.dialog-footer { .dialog-footer {
text-align: right; text-align: right;

View File

@@ -0,0 +1,12 @@
const assert = require("assert");
const fs = require("fs");
const path = require("path");
const dialogPath = path.resolve(__dirname, "../../src/views/ccdiProject/components/ImportHistoryDialog.vue");
const source = fs.readFileSync(dialogPath, "utf8");
assert(source.includes("listHistoryProjects("), "应通过真实历史项目接口加载数据");
assert(source.includes("importFromHistory("), "应通过真实导入接口提交");
assert(source.includes("this.$emit('submit', response.data)"), "提交成功后应向父组件返回新项目数据");
console.log("project-import-history-dialog-behavior test passed");

View File

@@ -0,0 +1,14 @@
const assert = require("assert");
const fs = require("fs");
const path = require("path");
const dialogPath = path.resolve(__dirname, "../../src/views/ccdiProject/components/ImportHistoryDialog.vue");
const source = fs.readFileSync(dialogPath, "utf8");
assert(source.includes('label="新项目名称"'), "应保留新项目名称字段");
assert(source.includes('label="备注"'), "应新增备注字段");
assert(source.includes('type="daterange"'), "应使用日期范围选择器");
assert(source.includes("data-multiselect") === false, "不应继续保留单选语义");
assert(source.includes("selectedProjectIds"), "应改为多选项目ID数组");
console.log("project-import-history-dialog-layout test passed");