opt: 附件上传/删除

This commit is contained in:
2025-11-21 17:02:48 +08:00
parent 5af4d27532
commit 738deb04df
15 changed files with 376 additions and 36 deletions

View File

@@ -28,5 +28,9 @@ export default {
// 获取项目附件表详情
bindProjectFile(data) {
return request('bindProjectFile', data, 'post')
},
// 获取项目附件表详情
deleteFile(data) {
return request('delete-file', data, 'post')
}
}

View File

@@ -24,5 +24,9 @@ export default {
// 获取项目阶段详情
projectStageDetail(data) {
return request('detail', data, 'get')
},
// 获取项目阶段详情
batchEdit(data) {
return request('batch-edit', data, 'post')
}
}

View File

@@ -0,0 +1,153 @@
<template>
<xn-form-container title="编辑项目阶段" :width="700" v-model:open="open" :destroy-on-close="true" @close="onClose">
<div class="stage-form">
<div class="stage-form__header">
<div>阶段名称</div>
<div>阶段序号</div>
<div>操作</div>
</div>
<div v-if="!visibleStages.length" class="stage-form__empty">
<a-empty description="暂无阶段,请新增一行" />
</div>
<div v-for="(stage, index) in visibleStages" :key="stage._key ?? index" class="stage-form__row">
<a-input v-model:value="stage.stageName" placeholder="请输入阶段名称" allow-clear :maxlength="50" />
<a-input-number v-model:value="stage.stageSeq" placeholder="请输入阶段序号" :min="1" style="width: 100%" />
<a-space>
<a-tooltip title="删除当前行以及需求">
<a-button type="link" danger @click="removeRow(stage)">删除</a-button>
</a-tooltip>
</a-space>
</div>
<a-button type="dashed" block @click="addRow"> 新增一行 </a-button>
</div>
<template #footer>
<a-button style="margin-right: 8px" @click="onClose">关闭</a-button>
<a-button type="primary" @click="onSubmit" :loading="submitLoading">保存</a-button>
</template>
</xn-form-container>
</template>
<script setup name="projectStageForm">
import projectStageApi from '@/api/pmm/projectStageApi'
import { createVNode } from 'vue'
import { Modal } from 'ant-design-vue'
import { ExclamationCircleOutlined } from '@ant-design/icons-vue'
import { cloneDeep } from 'lodash-es'
// 抽屉状态
const open = ref(false)
const emit = defineEmits({ successful: null })
// 表单数据
const formData = ref([])
const projectId = ref('')
const visibleStages = computed(() => formData.value.filter((item) => !item.isDeleted))
const submitLoading = ref(false)
const createEmptyStage = () => ({
stageId: '',
stageName: '',
projectId: projectId,
stageSeq: visibleStages.value.length + 1,
isDeleted: false,
_key: `${Date.now()}-${Math.random()}`
})
// 打开抽屉
const onOpen = (record) => {
if (record.length > 0) {
projectId.value = record[0].projectId
}
open.value = true
const list = Array.isArray(record) ? record : []
formData.value = cloneDeep(list).map((item, index) => ({
stageId: item.stageId ?? '',
stageName: item.stageName ?? '',
projectId: item.projectId ?? '',
stageSeq: item.stageSeq ?? index + 1,
isDeleted: item.isDeleted ?? false,
_key: `${Date.now()}-${index}`
}))
if (!formData.value.length) {
formData.value.push(createEmptyStage())
}
}
// 关闭抽屉
const onClose = () => {
formData.value = []
open.value = false
}
const addRow = () => {
formData.value.push(createEmptyStage())
}
const removeRow = (stage) => {
stage.isDeleted = true
if (!visibleStages.value.length) {
formData.value.push(createEmptyStage())
}
}
const submitStages = () => {
submitLoading.value = true
const submitList = formData.value.map(({ _key, ...rest }) => rest)
projectStageApi
.batchEdit(submitList)
.then(() => {
onClose()
emit('successful')
})
.finally(() => {
submitLoading.value = false
})
}
// 验证并提交数据
const onSubmit = () => {
const deletedStageCount = formData.value.filter((item) => item.stageId && item.isDeleted).length
const addedStageCount = formData.value.filter((item) => !item.stageId && !item.isDeleted).length
const content = `将删除${deletedStageCount}个阶段以及该阶段的需求,新增${addedStageCount}个阶段,是否继续?`
Modal.confirm({
title: '提示',
content,
icon: createVNode(ExclamationCircleOutlined),
okText: '确定',
cancelText: '取消',
onOk: () => {
submitStages()
}
})
}
// 抛出函数
defineExpose({
onOpen
})
</script>
<style scoped>
.stage-form {
display: flex;
flex-direction: column;
gap: 12px;
}
.stage-form__header,
.stage-form__row {
display: grid;
grid-template-columns: 1fr 1fr 120px;
gap: 12px;
align-items: center;
}
.stage-form__header {
font-weight: 600;
color: var(--text-color);
}
.stage-form__empty {
padding: 24px 0;
border: 1px dashed var(--border-color-split);
border-radius: 4px;
text-align: center;
}
.stage-form__row {
padding: 12px;
border: 1px solid var(--border-color-split);
border-radius: 4px;
}
</style>

View File

@@ -60,6 +60,7 @@
<span class="hours-label">计划工时</span>
<span class="hours-value">{{ plannedHours }}</span>
<a-button type="primary" class="create-stage-btn" @click="formRef.onOpen(projectInfo.projectId)">创建项目阶段</a-button>
<a-button type="primary" class="create-stage-btn" @click="form2Ref.onOpen(stageList)">编辑项目阶段</a-button>
</div>
</a-card>
@@ -156,6 +157,14 @@
class="attachment-item"
@click="handlePreview(file)"
>
<a-button
type="text"
size="small"
class="attachment-delete-btn"
@click.stop="handleAttachmentDelete(file)"
>
<CloseOutlined />
</a-button>
<div class="attachment-thumb">
<img :src="getAttachmentThumbnail(file)" :alt="file.name" />
</div>
@@ -173,6 +182,7 @@
<preview v-else ref="previewRef" @goBack="handlePreviewClose" />
<stage-add-dialog ref="formRef" @successful="fetchDetail"/>
<fj-upload-form ref="uploadFormRef" @successful="fetchAttachments"/>
<batch-stage-add-dialog ref="form2Ref" @successful="fetchDetail"/>
</div>
</template>
@@ -180,10 +190,13 @@
import { computed, nextTick, ref, watch } from 'vue'
import { useRoute } from 'vue-router'
import projectApi from '@/api/pmm/projectApi'
import { message } from 'ant-design-vue'
import projectFileApi from '@/api/pmm/projectFileApi'
import { Modal, message } from 'ant-design-vue'
import StageAddDialog from "@/views/pmm/project/request/stageAddDialog.vue";
import Preview from '@/views/dev/file/preview.vue'
import FjUploadForm from "@/views/pmm/project/request/fjUploadForm.vue";
import BatchStageAddDialog from "@/views/pmm/project/request/batchStageAddDialog.vue";
import { CloseOutlined } from '@ant-design/icons-vue'
interface AttachmentItem {
id: number | string
@@ -199,6 +212,7 @@
const stageList = ref<any[]>([])
const plannedHours = ref(0)
const formRef = ref()
const form2Ref = ref()
const previewRef = ref()
const uploadFormRef = ref()
const previewVisible = ref(false)
@@ -324,7 +338,7 @@ const attachmentList = ref<AttachmentItem[]>([])
return total
}
// 计算计划工时(所有阶段的总天数 * 24
// 计算计划工时(所有阶段的总天数 * 8
const calculatePlannedHours = (stages: any[]) => {
let total = 0
stages.forEach((stage) => {
@@ -332,7 +346,7 @@ const attachmentList = ref<AttachmentItem[]>([])
total += getStageTotalDays(stage.descriptions)
}
})
return total * 24
return total * 8
}
// 获取项目需求明细
@@ -512,6 +526,29 @@ watch(projectId, () => {
const handlePreviewClose = () => {
previewVisible.value = false
}
const handleAttachmentDelete = (file: AttachmentItem) => {
const param = {
id: file.id
}
Modal.confirm({
title: '确认删除该附件?',
content: `附件:${file.name}`,
okText: '确认',
cancelText: '取消',
onOk: () => {
projectFileApi
.deleteFile(param)
.then(() => {
message.success(`已确认删除:${file.name}`)
fetchAttachments()
})
},
onCancel: () => {
console.log('取消删除附件:', file)
}
})
}
</script>
<style scoped>
@@ -669,6 +706,23 @@ watch(projectId, () => {
position: relative;
}
.attachment-delete-btn {
position: absolute;
right: 4px;
top: 4px;
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
padding: 0;
color: #999;
}
.attachment-delete-btn:hover {
color: #ff4d4f;
}
.attachment-item:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
transform: translateY(-2px);

View File

@@ -49,7 +49,6 @@
const onOpen = (record) => {
open.value = true
if (record) {
console.log(record)
formData.value.projectId = record
}
}