init: Initialize the basic project.
This commit is contained in:
167
nl-vue/src/views/pmm/project/form.vue
Normal file
167
nl-vue/src/views/pmm/project/form.vue
Normal file
@@ -0,0 +1,167 @@
|
||||
<template>
|
||||
<xn-form-container
|
||||
:title="formData.projectId ? '编辑项目信息' : '增加项目信息'"
|
||||
:width="700"
|
||||
v-model:open="open"
|
||||
:destroy-on-close="true"
|
||||
@close="onClose"
|
||||
>
|
||||
<a-form ref="formRef" :model="formData" :rules="formRules" layout="horizontal">
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="项目编码:" name="projectCode">
|
||||
<a-input v-model:value="formData.projectCode" placeholder="请输入项目编码" allow-clear />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="项目名称:" name="projectName">
|
||||
<a-input v-model:value="formData.projectName" placeholder="请输入项目名称" allow-clear />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="项目经理:" name="projectManage">
|
||||
<a-select
|
||||
v-model:value="formData.projectManage"
|
||||
style="width: 100%"
|
||||
placeholder="请选择项目经理"
|
||||
:options="pmDatas"
|
||||
></a-select>
|
||||
<!-- <a-input v-model:value="formData.projectManage" placeholder="请输入项目经理" allow-clear />-->
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="产品需求:" name="requireManage">
|
||||
<a-select
|
||||
v-model:value="formData.requireManage"
|
||||
style="width: 100%"
|
||||
placeholder="请选择产品需求"
|
||||
:options="prDatas"
|
||||
></a-select>
|
||||
<!-- <a-input v-model:value="formData.requireManage" placeholder="请输入产品需求" allow-clear />-->
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="开 发 组:" name="devGroups">
|
||||
<a-select
|
||||
v-model:value="formData.devGroups"
|
||||
mode="multiple"
|
||||
style="width: 100%"
|
||||
placeholder="请选择开发组成员"
|
||||
:options="devDatas"
|
||||
></a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="开始时间:" name="startTime">
|
||||
<a-date-picker
|
||||
v-model:value="formData.startTime"
|
||||
:format="dateFormat"
|
||||
value-format="YYYY年MM月DD日"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="结束时间:" name="endTime">
|
||||
<a-date-picker
|
||||
v-model:value="formData.endTime"
|
||||
:format="dateFormat"
|
||||
value-format="YYYY年MM月DD日"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-form>
|
||||
<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="projectForm">
|
||||
import { cloneDeep } from 'lodash-es'
|
||||
import { required } from '@/utils/formRules'
|
||||
import projectApi from '@/api/pmm/projectApi'
|
||||
// 抽屉状态
|
||||
const open = ref(false)
|
||||
const emit = defineEmits({ successful: null })
|
||||
const formRef = ref()
|
||||
// 表单数据
|
||||
const formData = ref({})
|
||||
const submitLoading = ref(false)
|
||||
const devDatas = ref([])
|
||||
const pmDatas = ref([])
|
||||
const prDatas = ref([])
|
||||
const dateFormat = 'YYYY年MM月DD日'
|
||||
|
||||
// 打开抽屉
|
||||
const onOpen = (record) => {
|
||||
open.value = true
|
||||
if (record) {
|
||||
let recordData = cloneDeep(record)
|
||||
formData.value = Object.assign({}, recordData)
|
||||
}
|
||||
}
|
||||
// 关闭抽屉
|
||||
const onClose = () => {
|
||||
formRef.value.resetFields()
|
||||
formData.value = {}
|
||||
open.value = false
|
||||
}
|
||||
// 默认要校验的
|
||||
const formRules = {
|
||||
}
|
||||
// 验证并提交数据
|
||||
const onSubmit = () => {
|
||||
formRef.value
|
||||
.validate()
|
||||
.then(() => {
|
||||
submitLoading.value = true
|
||||
const formDataParam = cloneDeep(formData.value)
|
||||
console.log(formDataParam)
|
||||
projectApi
|
||||
.projectSubmitForm(formDataParam, formDataParam.projectId)
|
||||
.then(() => {
|
||||
onClose()
|
||||
emit('successful')
|
||||
})
|
||||
.finally(() => {
|
||||
submitLoading.value = false
|
||||
})
|
||||
})
|
||||
.catch(() => {})
|
||||
}
|
||||
const loadDevUserData = () => {
|
||||
const param = {
|
||||
groupId: '1988222962091225089'
|
||||
}
|
||||
projectApi.devUsers(param).then((data) => {
|
||||
devDatas.value = data
|
||||
})
|
||||
}
|
||||
const loadPmUserData = () => {
|
||||
const param = {
|
||||
groupId: '1988526425308659714'
|
||||
}
|
||||
projectApi.devUsers(param).then((data) => {
|
||||
pmDatas.value = data
|
||||
})
|
||||
}
|
||||
const loadPrUserData = () => {
|
||||
const param = {
|
||||
groupId: '1988526867551879170'
|
||||
}
|
||||
projectApi.devUsers(param).then((data) => {
|
||||
prDatas.value = data
|
||||
})
|
||||
}
|
||||
loadDevUserData()
|
||||
loadPmUserData()
|
||||
loadPrUserData()
|
||||
// 抛出函数
|
||||
defineExpose({
|
||||
onOpen
|
||||
})
|
||||
</script>
|
||||
133
nl-vue/src/views/pmm/project/index.vue
Normal file
133
nl-vue/src/views/pmm/project/index.vue
Normal file
@@ -0,0 +1,133 @@
|
||||
<template>
|
||||
<a-card :bordered="false">
|
||||
<s-table
|
||||
ref="tableRef"
|
||||
:columns="columns"
|
||||
:data="loadData"
|
||||
:alert="options.alert.show"
|
||||
bordered
|
||||
:row-key="(record) => record.projectId"
|
||||
:tool-config="toolConfig"
|
||||
:row-selection="options.rowSelection"
|
||||
>
|
||||
<template #operator class="table-operator">
|
||||
<a-space>
|
||||
<a-button type="primary" @click="formRef.onOpen()" v-if="hasPerm('projectAdd')">
|
||||
<template #icon><plus-outlined /></template>
|
||||
新增
|
||||
</a-button>
|
||||
<xn-batch-button
|
||||
v-if="hasPerm('projectBatchDelete')"
|
||||
buttonName="批量删除"
|
||||
icon="DeleteOutlined"
|
||||
:selectedRowKeys="selectedRowKeys"
|
||||
@batchCallBack="deleteBatchProject"
|
||||
/>
|
||||
</a-space>
|
||||
</template>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.dataIndex === 'action'">
|
||||
<a-space>
|
||||
<a @click="formRef.onOpen(record)" v-if="hasPerm('projectEdit')">编辑</a>
|
||||
<a-divider type="vertical" v-if="hasPerm(['projectEdit', 'projectDelete'], 'and')" />
|
||||
<a-popconfirm title="确定要删除吗?" @confirm="deleteProject(record)">
|
||||
<a-button type="link" danger size="small" v-if="hasPerm('projectDelete')">删除</a-button>
|
||||
</a-popconfirm>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
</s-table>
|
||||
</a-card>
|
||||
<Form ref="formRef" @successful="tableRef.refresh()" />
|
||||
</template>
|
||||
|
||||
<script setup name="project">
|
||||
import { cloneDeep } from 'lodash-es'
|
||||
import Form from './form.vue'
|
||||
import projectApi from '@/api/pmm/projectApi'
|
||||
const tableRef = ref()
|
||||
const formRef = ref()
|
||||
const toolConfig = { refresh: true, height: true, columnSetting: true, striped: false }
|
||||
const columns = [
|
||||
{
|
||||
title: '项目编码',
|
||||
dataIndex: 'projectCode'
|
||||
},
|
||||
{
|
||||
title: '项目名称',
|
||||
dataIndex: 'projectName'
|
||||
},
|
||||
{
|
||||
title: '项目经理',
|
||||
dataIndex: 'projectManage'
|
||||
},
|
||||
{
|
||||
title: '产品需求',
|
||||
dataIndex: 'requireManage'
|
||||
},
|
||||
{
|
||||
title: '开发组',
|
||||
dataIndex: 'devGroup'
|
||||
},
|
||||
{
|
||||
title: '开始时间',
|
||||
dataIndex: 'startTime'
|
||||
},
|
||||
{
|
||||
title: '结束时间',
|
||||
dataIndex: 'endTime'
|
||||
},
|
||||
]
|
||||
// 操作栏通过权限判断是否显示
|
||||
if (hasPerm(['projectEdit', 'projectDelete'])) {
|
||||
columns.push({
|
||||
title: '操作',
|
||||
dataIndex: 'action',
|
||||
align: 'center',
|
||||
width: 150
|
||||
})
|
||||
}
|
||||
const selectedRowKeys = ref([])
|
||||
// 列表选择配置
|
||||
const options = {
|
||||
// columns数字类型字段加入 needTotal: true 可以勾选自动算账
|
||||
alert: {
|
||||
show: true,
|
||||
clear: () => {
|
||||
selectedRowKeys.value = ref([])
|
||||
}
|
||||
},
|
||||
rowSelection: {
|
||||
onChange: (selectedRowKey, selectedRows) => {
|
||||
selectedRowKeys.value = selectedRowKey
|
||||
}
|
||||
}
|
||||
}
|
||||
const loadData = (parameter) => {
|
||||
return projectApi.projectPage(parameter).then((data) => {
|
||||
return data
|
||||
})
|
||||
}
|
||||
// 重置
|
||||
const reset = () => {
|
||||
searchFormRef.value.resetFields()
|
||||
tableRef.value.refresh(true)
|
||||
}
|
||||
// 删除
|
||||
const deleteProject = (record) => {
|
||||
let params = [
|
||||
{
|
||||
projectId: record.projectId
|
||||
}
|
||||
]
|
||||
projectApi.projectDelete(params).then(() => {
|
||||
tableRef.value.refresh(true)
|
||||
})
|
||||
}
|
||||
// 批量删除
|
||||
const deleteBatchProject = (params) => {
|
||||
projectApi.projectDelete(params).then(() => {
|
||||
tableRef.value.clearRefreshSelected()
|
||||
})
|
||||
}
|
||||
</script>
|
||||
153
nl-vue/src/views/pmm/project/request/batchStageAddDialog.vue
Normal file
153
nl-vue/src/views/pmm/project/request/batchStageAddDialog.vue
Normal 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>
|
||||
755
nl-vue/src/views/pmm/project/request/description.vue
Normal file
755
nl-vue/src/views/pmm/project/request/description.vue
Normal file
@@ -0,0 +1,755 @@
|
||||
<template>
|
||||
<div class="description-wrapper">
|
||||
<div v-if="!previewVisible">
|
||||
<a-card :bordered="false">
|
||||
<a-spin :spinning="loading">
|
||||
<!-- 项目名称标题 -->
|
||||
<div class="project-title">
|
||||
<h2>{{ parsedQuery?.projectName || '项目名称' }}</h2>
|
||||
</div>
|
||||
|
||||
<!-- 项目信息部分 -->
|
||||
<a-card :bordered="false" class="project-info-card">
|
||||
<a-form :model="projectInfo" layout="horizontal" :label-col="{ span: 7 }" :wrapper-col="{ span: 17 }" class="compact-form">
|
||||
<!-- 第一行:项目名称、项目编号 -->
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="8">
|
||||
<a-form-item label="项目名称:">
|
||||
<span>{{ projectInfo.projectName || '-' }}</span>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
<a-form-item label="项目编号:">
|
||||
<span>{{ projectInfo.projectCode || '-' }}</span>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<!-- 第二行:项目经理、产品需求、开发组 -->
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="8">
|
||||
<a-form-item label="项目经理:">
|
||||
<span>{{ projectInfo.projectManage || '-' }}</span>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
<a-form-item label="产品需求:">
|
||||
<span>{{ projectInfo.requireManage || '-' }}</span>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
<a-form-item label="开发组:">
|
||||
<span>{{ projectInfo.devGroup || '-' }}</span>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<!-- 第三行:开始时间、完成时间 -->
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="8">
|
||||
<a-form-item label="开始时间:">
|
||||
<span>{{ projectInfo.startTime || '-' }}</span>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
<a-form-item label="完成时间:">
|
||||
<span>{{ projectInfo.endTime || '-' }}</span>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-form>
|
||||
<div class="planned-hours">
|
||||
<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>
|
||||
|
||||
<!-- 表格信息部分 -->
|
||||
<div v-if="stageList.length > 0" class="stage-tables">
|
||||
<a-card
|
||||
v-for="(stage, stageIndex) in stageList"
|
||||
:key="stage.stageId"
|
||||
:bordered="false"
|
||||
class="stage-card"
|
||||
>
|
||||
<template #title>
|
||||
<div class="stage-title-wrapper">
|
||||
<span>第{{ getStageNumber(stage.stageSeq) }}阶段 {{ stage.stageName }}</span>
|
||||
<a-button type="primary" class="create-requirement-btn">创建需求</a-button>
|
||||
</div>
|
||||
</template>
|
||||
<a-table
|
||||
:columns="tableColumns"
|
||||
:data-source="stage.descriptions"
|
||||
:pagination="false"
|
||||
:row-key="(record) => record.detailId"
|
||||
:defaultExpandAllRows="true"
|
||||
bordered
|
||||
size="middle"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.dataIndex === 'seq'">
|
||||
{{ record.seq }}
|
||||
</template>
|
||||
<template v-if="column.dataIndex === 'modelName'">
|
||||
{{ record.modelName }}
|
||||
</template>
|
||||
<template v-if="column.dataIndex === 'modelDetail'">
|
||||
{{ record.modelDetail }}
|
||||
</template>
|
||||
<template v-if="column.dataIndex === 'modelDescription'">
|
||||
{{ record.modelDescription }}
|
||||
</template>
|
||||
<template v-if="column.dataIndex === 'priority'">
|
||||
{{ record.priority }}
|
||||
</template>
|
||||
<template v-if="column.dataIndex === 'remark'">
|
||||
{{ record.remark || '无' }}
|
||||
</template>
|
||||
<template v-if="column.dataIndex === 'startTime'">
|
||||
{{ formatDate(record.startTime) }}
|
||||
</template>
|
||||
<template v-if="column.dataIndex === 'endTime'">
|
||||
{{ formatDate(record.endTime) }}
|
||||
</template>
|
||||
<template v-if="column.dataIndex === 'days'">
|
||||
{{ record.days }}
|
||||
</template>
|
||||
<template v-if="column.dataIndex === 'action'">
|
||||
<a-button type="link" size="small" class="action-btn">
|
||||
新增同级
|
||||
</a-button>
|
||||
<a-button type="link" size="small" class="action-btn">
|
||||
新增子级
|
||||
</a-button>
|
||||
<a-button type="link" size="small" class="action-btn">
|
||||
修改需求
|
||||
</a-button>
|
||||
<a-button type="link" size="small" class="action-btn">
|
||||
删除需求
|
||||
</a-button>
|
||||
</template>
|
||||
</template>
|
||||
<template #summary>
|
||||
<a-table-summary fixed>
|
||||
<a-table-summary-row>
|
||||
<a-table-summary-cell :index="0" :col-span="10">
|
||||
<span style="font-weight: bold">总计天数:{{ getStageTotalDays(stage.descriptions) }}</span>
|
||||
</a-table-summary-cell>
|
||||
</a-table-summary-row>
|
||||
</a-table-summary>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
</div>
|
||||
<a-empty v-else description="暂无数据" />
|
||||
|
||||
<!-- 附件区域 -->
|
||||
<div class="attachments-section">
|
||||
<div class="attachments-header">
|
||||
<div class="attachments-title">附件</div>
|
||||
<a-button type="primary" @click="uploadFormRef.openUpload(projectInfo.projectId)">上传附件</a-button>
|
||||
</div>
|
||||
<div v-if="attachmentList.length" class="attachment-list">
|
||||
<div
|
||||
v-for="file in attachmentList"
|
||||
:key="file.id"
|
||||
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>
|
||||
<div class="attachment-name" :title="file.name">
|
||||
{{ file.name }}
|
||||
</div>
|
||||
<a-tag v-if="!canPreview(file)" color="default" class="attachment-tag">暂不支持预览</a-tag>
|
||||
</div>
|
||||
</div>
|
||||
<a-empty v-else description="暂无附件" />
|
||||
</div>
|
||||
</a-spin>
|
||||
</a-card>
|
||||
</div>
|
||||
<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>
|
||||
|
||||
<script setup lang="ts" name="Description">
|
||||
import { computed, nextTick, ref, watch } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import projectApi from '@/api/pmm/projectApi'
|
||||
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
|
||||
name: string
|
||||
suffix: string
|
||||
downloadPath: string
|
||||
thumbnail?: string
|
||||
}
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
const loading = ref(false)
|
||||
const stageList = ref<any[]>([])
|
||||
const plannedHours = ref(0)
|
||||
const formRef = ref()
|
||||
const form2Ref = ref()
|
||||
const previewRef = ref()
|
||||
const uploadFormRef = ref()
|
||||
const previewVisible = ref(false)
|
||||
|
||||
const attachmentList = ref<AttachmentItem[]>([])
|
||||
|
||||
const parsedQuery = computed(() => {
|
||||
const payload = route.query.item as string | undefined
|
||||
if (!payload) return null
|
||||
try {
|
||||
return JSON.parse(decodeURIComponent(payload))
|
||||
} catch (error) {
|
||||
console.warn('解析项目详情失败', error)
|
||||
return null
|
||||
}
|
||||
})
|
||||
|
||||
const projectInfo = computed(() => {
|
||||
return parsedQuery.value || {}
|
||||
})
|
||||
|
||||
const projectId = computed(() => parsedQuery.value?.projectId)
|
||||
|
||||
// 表格列配置
|
||||
const tableColumns = [
|
||||
{
|
||||
title: '序号',
|
||||
dataIndex: 'seq',
|
||||
key: 'seq',
|
||||
width: 80,
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '功能模块',
|
||||
dataIndex: 'modelName',
|
||||
key: 'modelName',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
title: '功能明细',
|
||||
dataIndex: 'modelDetail',
|
||||
key: 'modelDetail',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
title: '描述',
|
||||
dataIndex: 'modelDescription',
|
||||
key: 'modelDescription',
|
||||
width: 150
|
||||
},
|
||||
{
|
||||
title: '优先级',
|
||||
dataIndex: 'priority',
|
||||
key: 'priority',
|
||||
width: 80,
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '备注',
|
||||
dataIndex: 'remark',
|
||||
key: 'remark',
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
title: '开始日期',
|
||||
dataIndex: 'startTime',
|
||||
key: 'startTime',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
title: '结束日期',
|
||||
dataIndex: 'endTime',
|
||||
key: 'endTime',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
title: '计划天数',
|
||||
dataIndex: 'days',
|
||||
key: 'days',
|
||||
width: 100,
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
dataIndex: 'action',
|
||||
key: 'action',
|
||||
width: 300,
|
||||
align: 'center'
|
||||
}
|
||||
]
|
||||
|
||||
// 格式化日期
|
||||
const formatDate = (dateStr: string) => {
|
||||
if (!dateStr) return '-'
|
||||
const date = new Date(dateStr)
|
||||
const year = date.getFullYear()
|
||||
const month = date.getMonth() + 1
|
||||
const day = date.getDate()
|
||||
const monthStr = month < 10 ? '0' + month : String(month)
|
||||
const dayStr = day < 10 ? '0' + day : String(day)
|
||||
return `${year}-${monthStr}-${dayStr}`
|
||||
}
|
||||
|
||||
// 获取阶段序号中文
|
||||
const getStageNumber = (seq: string) => {
|
||||
const num = parseInt(seq)
|
||||
const numbers = ['一', '二', '三', '四', '五', '六', '七', '八', '九', '十']
|
||||
if (num <= 10) {
|
||||
return numbers[num - 1] || seq
|
||||
}
|
||||
return seq
|
||||
}
|
||||
|
||||
// 计算阶段总天数(只统计一级,不包含子项)
|
||||
const getStageTotalDays = (descriptions: any[]) => {
|
||||
if (!descriptions || descriptions.length === 0) return 0
|
||||
let total = 0
|
||||
descriptions.forEach((item) => {
|
||||
if (item.days) {
|
||||
total += item.days
|
||||
}
|
||||
})
|
||||
return total
|
||||
}
|
||||
|
||||
// 计算计划工时(所有阶段的总天数 * 8)
|
||||
const calculatePlannedHours = (stages: any[]) => {
|
||||
let total = 0
|
||||
stages.forEach((stage) => {
|
||||
if (stage.descriptions) {
|
||||
total += getStageTotalDays(stage.descriptions)
|
||||
}
|
||||
})
|
||||
return total * 8
|
||||
}
|
||||
|
||||
// 获取项目需求明细
|
||||
const fetchDetail = () => {
|
||||
if (!projectId.value) {
|
||||
stageList.value = []
|
||||
return
|
||||
}
|
||||
loading.value = true
|
||||
projectApi
|
||||
.requestDetail({ projectId: projectId.value })
|
||||
.then((data) => {
|
||||
if (Array.isArray(data)) {
|
||||
stageList.value = data
|
||||
plannedHours.value = calculatePlannedHours(data)
|
||||
} else {
|
||||
stageList.value = []
|
||||
message.warning('数据格式错误')
|
||||
}
|
||||
loading.value = false
|
||||
})
|
||||
.catch((error) => {
|
||||
console.warn('获取项目需求明细失败', error)
|
||||
stageList.value = []
|
||||
message.error('获取项目需求明细失败')
|
||||
loading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
const resolveAttachmentSource = (payload: unknown) => {
|
||||
if (Array.isArray(payload)) {
|
||||
return payload
|
||||
}
|
||||
if (payload && typeof payload === 'object') {
|
||||
const obj = payload as Record<string, any>
|
||||
const candidateKeys = ['data', 'records', 'rows', 'list']
|
||||
for (const key of candidateKeys) {
|
||||
if (Array.isArray(obj[key])) {
|
||||
return obj[key]
|
||||
}
|
||||
}
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
const getSuffixFromName = (name?: string) => {
|
||||
if (!name || typeof name !== 'string') {
|
||||
return ''
|
||||
}
|
||||
const lastDotIndex = name.lastIndexOf('.')
|
||||
if (lastDotIndex === -1) {
|
||||
return ''
|
||||
}
|
||||
return name.slice(lastDotIndex + 1)
|
||||
}
|
||||
|
||||
const normalizeAttachmentRecord = (raw: Record<string, any>): AttachmentItem | null => {
|
||||
if (!raw) {
|
||||
return null
|
||||
}
|
||||
const name =
|
||||
raw.name ||
|
||||
raw.fileName ||
|
||||
raw.originName ||
|
||||
raw.originalName ||
|
||||
raw.fileOriginalName ||
|
||||
raw.attachmentName ||
|
||||
raw.title
|
||||
const downloadPath =
|
||||
raw.downloadPath ||
|
||||
raw.fileUrl ||
|
||||
raw.url ||
|
||||
raw.downloadUrl ||
|
||||
raw.path ||
|
||||
raw.filePath ||
|
||||
raw.storagePath
|
||||
if (!downloadPath) {
|
||||
return null
|
||||
}
|
||||
const suffixSource =
|
||||
raw.suffix ||
|
||||
raw.fileSuffix ||
|
||||
raw.fileType ||
|
||||
getSuffixFromName(name) ||
|
||||
getSuffixFromName(downloadPath)
|
||||
const suffix = typeof suffixSource === 'string' ? suffixSource.toLowerCase() : ''
|
||||
const thumbnail = raw.thumbnail || raw.thumbUrl || raw.previewUrl || raw.cover
|
||||
const id = raw.id || raw.fileId || raw.attachmentId || raw.uid || downloadPath || name || Date.now()
|
||||
return {
|
||||
id,
|
||||
name: name || '未命名文件',
|
||||
suffix,
|
||||
downloadPath,
|
||||
thumbnail
|
||||
}
|
||||
}
|
||||
|
||||
const fetchAttachments = () => {
|
||||
if (!projectId.value) {
|
||||
attachmentList.value = []
|
||||
return
|
||||
}
|
||||
projectApi
|
||||
.getAttachment({ projectId: projectId.value })
|
||||
.then((response) => {
|
||||
const source = resolveAttachmentSource(response)
|
||||
if (!source.length) {
|
||||
attachmentList.value = []
|
||||
return
|
||||
}
|
||||
const parsed = source
|
||||
.map((item) => normalizeAttachmentRecord(item))
|
||||
.filter((item): item is AttachmentItem => Boolean(item))
|
||||
if (!parsed.length) {
|
||||
message.warning('附件数据格式错误')
|
||||
}
|
||||
attachmentList.value = parsed
|
||||
})
|
||||
.catch((error) => {
|
||||
console.warn('获取项目附件失败', error)
|
||||
message.error('获取项目附件失败')
|
||||
attachmentList.value = []
|
||||
})
|
||||
}
|
||||
|
||||
watch(projectId, () => {
|
||||
fetchDetail()
|
||||
fetchAttachments()
|
||||
}, { immediate: true })
|
||||
|
||||
const previewableSuffix = ['doc', 'docx', 'xls', 'xlsx', 'pdf', 'jpg', 'png', 'gif', 'svg', 'ico', 'tmp', 'jpeg']
|
||||
|
||||
const getFileIcon = (suffix: string) => {
|
||||
const lower = suffix?.toLowerCase()
|
||||
const iconMap: Record<string, string> = {
|
||||
doc: '/src/assets/images/fileImg/docx.png',
|
||||
docx: '/src/assets/images/fileImg/docx.png',
|
||||
xls: '/src/assets/images/fileImg/xlsx.png',
|
||||
xlsx: '/src/assets/images/fileImg/xlsx.png',
|
||||
ppt: '/src/assets/images/fileImg/ppt.png',
|
||||
pptx: '/src/assets/images/fileImg/ppt.png',
|
||||
pdf: '/src/assets/images/fileImg/pdf.png',
|
||||
txt: '/src/assets/images/fileImg/txt.png',
|
||||
zip: '/src/assets/images/fileImg/zip.png',
|
||||
rar: '/src/assets/images/fileImg/rar.png',
|
||||
html: '/src/assets/images/fileImg/html.png'
|
||||
}
|
||||
return iconMap[lower] || '/src/assets/images/fileImg/file.png'
|
||||
}
|
||||
|
||||
const getAttachmentThumbnail = (file: AttachmentItem) => {
|
||||
if (file.thumbnail) {
|
||||
return file.thumbnail
|
||||
}
|
||||
const imgSuffix = ['png', 'jpg', 'jpeg', 'gif', 'bmp']
|
||||
if (imgSuffix.indexOf(file.suffix?.toLowerCase()) > -1) {
|
||||
return file.downloadPath
|
||||
}
|
||||
return getFileIcon(file.suffix)
|
||||
}
|
||||
|
||||
const canPreview = (file: AttachmentItem) => {
|
||||
return previewableSuffix.indexOf(file.suffix?.toLowerCase()) > -1
|
||||
}
|
||||
|
||||
const handlePreview = (file: AttachmentItem) => {
|
||||
if (!canPreview(file)) {
|
||||
message.warning('当前文件暂不支持在线预览')
|
||||
return
|
||||
}
|
||||
previewVisible.value = true
|
||||
nextTick(() => {
|
||||
previewRef.value?.onOpen(file)
|
||||
})
|
||||
}
|
||||
|
||||
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>
|
||||
.project-title {
|
||||
text-align: center;
|
||||
margin-bottom: 10px;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.project-title h2 {
|
||||
margin: 0;
|
||||
color: #1890ff;
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.project-info-card {
|
||||
margin-bottom: 24px;
|
||||
padding: 12px 16px;
|
||||
}
|
||||
|
||||
.project-info-card :deep(.ant-card-body) {
|
||||
padding: 12px 16px;
|
||||
}
|
||||
|
||||
.compact-form :deep(.ant-form-item) {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.compact-form :deep(.ant-row) {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.compact-form :deep(.ant-row:last-child) {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.compact-form :deep(.ant-form-item-label) {
|
||||
padding-bottom: 0;
|
||||
width: 90px;
|
||||
flex: 0 0 90px;
|
||||
}
|
||||
|
||||
.compact-form :deep(.ant-form-item-label > label) {
|
||||
height: 28px;
|
||||
line-height: 28px;
|
||||
font-size: 14px;
|
||||
width: 90px;
|
||||
text-align: right;
|
||||
padding-right: 8px;
|
||||
}
|
||||
|
||||
.compact-form :deep(.ant-form-item-control) {
|
||||
line-height: 28px;
|
||||
font-size: 14px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.compact-form :deep(.ant-form-item-control-input) {
|
||||
min-height: 28px;
|
||||
}
|
||||
|
||||
.compact-form :deep(.ant-form-item-control-input-content) {
|
||||
line-height: 28px;
|
||||
}
|
||||
|
||||
.planned-hours {
|
||||
margin-top: 12px;
|
||||
text-align: right;
|
||||
padding-right: 24px;
|
||||
}
|
||||
|
||||
.hours-label {
|
||||
margin-right: 8px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.hours-value {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
color: #1890ff;
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
.create-stage-btn {
|
||||
margin-left: 16px;
|
||||
}
|
||||
|
||||
.stage-tables {
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.stage-card {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.stage-card :deep(.ant-card-head-title) {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stage-title-wrapper {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.stage-title-wrapper > span {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.create-requirement-btn {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
padding: 0 8px;
|
||||
}
|
||||
|
||||
.attachments-section {
|
||||
margin-top: 32px;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
padding-top: 24px;
|
||||
}
|
||||
|
||||
.attachments-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.attachments-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.attachment-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.attachment-item {
|
||||
width: 120px;
|
||||
text-align: center;
|
||||
border: 1px solid #f0f0f0;
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
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);
|
||||
}
|
||||
|
||||
.attachment-thumb {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
margin: 0 auto 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.attachment-thumb img {
|
||||
max-width: 80px;
|
||||
max-height: 80px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.attachment-name {
|
||||
font-size: 13px;
|
||||
color: #333;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.attachment-tag {
|
||||
margin-top: 8px;
|
||||
}
|
||||
</style>
|
||||
156
nl-vue/src/views/pmm/project/request/fjUploadForm.vue
Normal file
156
nl-vue/src/views/pmm/project/request/fjUploadForm.vue
Normal file
@@ -0,0 +1,156 @@
|
||||
<template>
|
||||
<xn-form-container
|
||||
title="文件上传"
|
||||
:width="550"
|
||||
:visible="visible"
|
||||
:destroy-on-close="true"
|
||||
:bodyStyle="{ 'padding-top': '0px' }"
|
||||
@close="onClose"
|
||||
>
|
||||
<a-tabs v-model:activeKey="activeKey">
|
||||
<a-tab-pane key="Local" tab="本地">
|
||||
<a-spin :spinning="uploadLoading">
|
||||
<a-upload-dragger :show-upload-list="false" :custom-request="customRequestLocal">
|
||||
<p class="ant-upload-drag-icon">
|
||||
<inbox-outlined></inbox-outlined>
|
||||
</p>
|
||||
<p class="ant-upload-text">单击或拖动文件到此区域进行上传</p>
|
||||
<p class="ant-upload-hint">支持单个上传</p>
|
||||
</a-upload-dragger>
|
||||
</a-spin>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="Aliyun" tab="阿里云">
|
||||
<a-spin :spinning="uploadLoading">
|
||||
<a-upload-dragger :custom-request="customRequestAliyun" :show-upload-list="false">
|
||||
<p class="ant-upload-drag-icon">
|
||||
<inbox-outlined></inbox-outlined>
|
||||
</p>
|
||||
<p class="ant-upload-text">单击或拖动文件到此区域进行上传</p>
|
||||
<p class="ant-upload-hint">支持单个上传</p>
|
||||
</a-upload-dragger>
|
||||
</a-spin>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="Tencent" tab="腾讯云">
|
||||
<a-spin :spinning="uploadLoading">
|
||||
<a-upload-dragger :custom-request="customRequestTencent" :show-upload-list="false">
|
||||
<p class="ant-upload-drag-icon">
|
||||
<inbox-outlined></inbox-outlined>
|
||||
</p>
|
||||
<p class="ant-upload-text">单击或拖动文件到此区域进行上传</p>
|
||||
<p class="ant-upload-hint">支持单个上传</p>
|
||||
</a-upload-dragger>
|
||||
</a-spin>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="Minio" tab="MINIO">
|
||||
<a-spin :spinning="uploadLoading">
|
||||
<a-upload-dragger :custom-request="customRequestMinio" :show-upload-list="false">
|
||||
<p class="ant-upload-drag-icon">
|
||||
<inbox-outlined></inbox-outlined>
|
||||
</p>
|
||||
<p class="ant-upload-text">单击或拖动文件到此区域进行上传</p>
|
||||
<p class="ant-upload-hint">支持单个上传</p>
|
||||
</a-upload-dragger>
|
||||
</a-spin>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</xn-form-container>
|
||||
</template>
|
||||
|
||||
<script setup name="uploadForm">
|
||||
import fileApi from '@/api/dev/fileApi'
|
||||
import projectFileApi from '@/api/pmm/projectFileApi'
|
||||
// 定义emit事件
|
||||
const emit = defineEmits({ successful: null })
|
||||
// 默认是关闭状态
|
||||
const visible = ref(false)
|
||||
const activeKey = ref('Local')
|
||||
const uploadLoading = ref(false)
|
||||
const projectId = ref('')
|
||||
// 打开抽屉
|
||||
const openUpload = (record) => {
|
||||
projectId.value = record
|
||||
visible.value = true
|
||||
}
|
||||
// 关闭抽屉
|
||||
const onClose = () => {
|
||||
visible.value = false
|
||||
projectId.value = ''
|
||||
emit('successful')
|
||||
}
|
||||
// 上传本地文件
|
||||
const customRequestLocal = (data) => {
|
||||
uploadLoading.value = true
|
||||
const fileData = new FormData()
|
||||
fileData.append('file', data.file)
|
||||
fileApi
|
||||
.fileUploadLocalReturnUrl(fileData)
|
||||
.then((res) => {
|
||||
bindProjectFile(res).then(() => {
|
||||
emit('successful')
|
||||
})
|
||||
})
|
||||
.finally(() => {
|
||||
uploadLoading.value = false
|
||||
})
|
||||
}
|
||||
// 上传阿里云文件
|
||||
const customRequestAliyun = (data) => {
|
||||
uploadLoading.value = true
|
||||
const fileData = new FormData()
|
||||
fileData.append('file', data.file)
|
||||
fileApi
|
||||
.fileUploadAliyunReturnUrl(fileData)
|
||||
.then((res) => {
|
||||
bindProjectFile(res).then(() => {
|
||||
emit('successful')
|
||||
})
|
||||
})
|
||||
.finally(() => {
|
||||
uploadLoading.value = false
|
||||
})
|
||||
}
|
||||
// 上传腾讯文件
|
||||
const customRequestTencent = (data) => {
|
||||
uploadLoading.value = true
|
||||
const fileData = new FormData()
|
||||
fileData.append('file', data.file)
|
||||
fileApi
|
||||
.fileUploadTencentReturnUrl(fileData)
|
||||
.then((res) => {
|
||||
bindProjectFile(res).then(() => {
|
||||
emit('successful')
|
||||
})
|
||||
})
|
||||
.finally(() => {
|
||||
uploadLoading.value = false
|
||||
})
|
||||
}
|
||||
// 上传Minio文件
|
||||
const customRequestMinio = (data) => {
|
||||
uploadLoading.value = true
|
||||
const fileData = new FormData()
|
||||
fileData.append('file', data.file)
|
||||
fileApi
|
||||
.fileUploadMinioReturnUrl(fileData)
|
||||
.then((res) => {
|
||||
bindProjectFile(res).then(() => {
|
||||
emit('successful')
|
||||
})
|
||||
})
|
||||
.finally(() => {
|
||||
uploadLoading.value = false
|
||||
})
|
||||
}
|
||||
// 绑定项目与文件
|
||||
const bindProjectFile = (fileUrl) => {
|
||||
const param = {
|
||||
projectId: projectId.value,
|
||||
fileAddress: fileUrl
|
||||
}
|
||||
return projectFileApi.bindProjectFile(param)
|
||||
}
|
||||
// 调用这个函数将子组件的一些数据和方法暴露出去
|
||||
defineExpose({
|
||||
openUpload
|
||||
})
|
||||
</script>
|
||||
102
nl-vue/src/views/pmm/project/request/index.vue
Normal file
102
nl-vue/src/views/pmm/project/request/index.vue
Normal file
@@ -0,0 +1,102 @@
|
||||
<template>
|
||||
<a-card :bordered="false">
|
||||
<s-table
|
||||
ref="tableRef"
|
||||
:columns="columns"
|
||||
:data="loadData"
|
||||
:alert="options.alert.show"
|
||||
bordered
|
||||
:row-key="(record) => record.projectId"
|
||||
:tool-config="toolConfig"
|
||||
:row-selection="options.rowSelection"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.dataIndex === 'projectName'">
|
||||
<a @click="toDescriptionWeb(record)">{{ record.projectName }}</a>
|
||||
</template>
|
||||
</template>
|
||||
</s-table>
|
||||
</a-card>
|
||||
</template>
|
||||
|
||||
<script setup name="projectRequest">
|
||||
import { useRouter } from 'vue-router'
|
||||
import projectApi from '@/api/pmm/projectApi'
|
||||
const router = useRouter()
|
||||
const tableRef = ref()
|
||||
const formRef = ref()
|
||||
const toolConfig = { refresh: true, height: true, columnSetting: true, striped: false }
|
||||
const columns = [
|
||||
{
|
||||
title: '项目编码',
|
||||
dataIndex: 'projectCode'
|
||||
},
|
||||
{
|
||||
title: '项目名称',
|
||||
dataIndex: 'projectName'
|
||||
},
|
||||
{
|
||||
title: '项目经理',
|
||||
dataIndex: 'projectManage'
|
||||
},
|
||||
{
|
||||
title: '产品需求',
|
||||
dataIndex: 'requireManage'
|
||||
},
|
||||
{
|
||||
title: '开发组',
|
||||
dataIndex: 'devGroup'
|
||||
},
|
||||
{
|
||||
title: '开始时间',
|
||||
dataIndex: 'startTime'
|
||||
},
|
||||
{
|
||||
title: '结束时间',
|
||||
dataIndex: 'endTime'
|
||||
},
|
||||
]
|
||||
// 操作栏通过权限判断是否显示
|
||||
// if (hasPerm(['projectEdit', 'projectDelete'])) {
|
||||
// columns.push({
|
||||
// title: '操作',
|
||||
// dataIndex: 'action',
|
||||
// align: 'center',
|
||||
// width: 150
|
||||
// })
|
||||
// }
|
||||
const selectedRowKeys = ref([])
|
||||
// 列表选择配置
|
||||
const options = {
|
||||
// columns数字类型字段加入 needTotal: true 可以勾选自动算账
|
||||
alert: {
|
||||
show: false,
|
||||
clear: () => {
|
||||
selectedRowKeys.value = ref([])
|
||||
}
|
||||
},
|
||||
rowSelection: {
|
||||
onChange: (selectedRowKey, selectedRows) => {
|
||||
selectedRowKeys.value = selectedRowKey
|
||||
}
|
||||
}
|
||||
}
|
||||
const loadData = (parameter) => {
|
||||
return projectApi.projectPage(parameter).then((data) => {
|
||||
return data
|
||||
})
|
||||
}
|
||||
// 重置
|
||||
const reset = () => {
|
||||
searchFormRef.value.resetFields()
|
||||
tableRef.value.refresh(true)
|
||||
}
|
||||
const toDescriptionWeb = (item) => {
|
||||
router.push({
|
||||
path: '/pmm/project/request/description',
|
||||
query: {
|
||||
item: encodeURIComponent(JSON.stringify(item))
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
86
nl-vue/src/views/pmm/project/request/stageAddDialog.vue
Normal file
86
nl-vue/src/views/pmm/project/request/stageAddDialog.vue
Normal file
@@ -0,0 +1,86 @@
|
||||
<template>
|
||||
<xn-form-container
|
||||
:title="formData.stageId ? '编辑项目阶段' : '增加项目阶段'"
|
||||
:width="700"
|
||||
v-model:open="open"
|
||||
:destroy-on-close="true"
|
||||
@close="onClose"
|
||||
>
|
||||
<a-form ref="formRef" :model="formData" :rules="formRules" layout="horizontal">
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="阶段名称:" name="stageName">
|
||||
<a-input v-model:value="formData.stageName" placeholder="请输入阶段名称" allow-clear />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="阶段序号:" name="stageSeq">
|
||||
<a-input-number
|
||||
v-model:value="formData.stageSeq"
|
||||
placeholder="请输入阶段序号"
|
||||
style="width: 100%"
|
||||
allow-clear
|
||||
:min="1"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-form>
|
||||
<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 { cloneDeep } from 'lodash-es'
|
||||
import { required } from '@/utils/formRules'
|
||||
import projectStageApi from '@/api/pmm/projectStageApi'
|
||||
// 抽屉状态
|
||||
const open = ref(false)
|
||||
const emit = defineEmits({ successful: null })
|
||||
const formRef = ref()
|
||||
// 表单数据
|
||||
const formData = ref({})
|
||||
const submitLoading = ref(false)
|
||||
|
||||
// 打开抽屉
|
||||
const onOpen = (record) => {
|
||||
open.value = true
|
||||
if (record) {
|
||||
formData.value.projectId = record
|
||||
}
|
||||
}
|
||||
// 关闭抽屉
|
||||
const onClose = () => {
|
||||
formRef.value.resetFields()
|
||||
formData.value = {}
|
||||
open.value = false
|
||||
}
|
||||
// 默认要校验的
|
||||
const formRules = {}
|
||||
// 验证并提交数据
|
||||
const onSubmit = () => {
|
||||
formRef.value
|
||||
.validate()
|
||||
.then(() => {
|
||||
submitLoading.value = true
|
||||
const formDataParam = cloneDeep(formData.value)
|
||||
projectStageApi
|
||||
.projectStageSubmitForm(formDataParam, formDataParam.stageId)
|
||||
.then(() => {
|
||||
onClose()
|
||||
emit('successful')
|
||||
})
|
||||
.finally(() => {
|
||||
submitLoading.value = false
|
||||
})
|
||||
})
|
||||
.catch(() => {})
|
||||
}
|
||||
// 抛出函数
|
||||
defineExpose({
|
||||
onOpen
|
||||
})
|
||||
</script>
|
||||
Reference in New Issue
Block a user