17 changed files with 542 additions and 33 deletions
@ -0,0 +1,59 @@ |
|||
<template> |
|||
<div class="px-2 py-1 border rounded-sm" @click="collapsed = !collapsed"> |
|||
<div class="flex justify-between items-center"> |
|||
<div>审核人</div> |
|||
<div class="flex items-center justify-end flex-1 text-sm"> |
|||
<div class="mx-1" v-for="(item, index) in showCheckers" :key="index">{{ item.name }}</div> |
|||
<div class="mx-1" v-show="checkedCheckers.length > 3">...</div> |
|||
<DownOutlined v-if="!collapsed" /> |
|||
<UpOutlined v-else /> |
|||
</div> |
|||
</div> |
|||
|
|||
<!-- 隐藏的审核人选项 --> |
|||
<div v-show="collapsed" class="foot mt-2 flex flex-wrap"> |
|||
<a-button |
|||
v-for="(item, index) in checkers" |
|||
:key="index" |
|||
:type="checkedCheckers.find(checker => checker.memberId === item.memberId) ? 'primary' : 'default'" |
|||
size="small" |
|||
class="my-1 mx-2" |
|||
@click.stop="handleSelectChecker(item)" |
|||
> |
|||
{{ item.name }} |
|||
</a-button> |
|||
</div> |
|||
</div> |
|||
</template> |
|||
|
|||
<script setup> |
|||
import { ref, computed } from 'vue'; |
|||
import { useStore } from 'vuex'; |
|||
import { DownOutlined, UpOutlined } from '@ant-design/icons-vue'; |
|||
|
|||
const store = useStore(); |
|||
// 是否显示全部审核人 |
|||
const collapsed = ref(false); |
|||
|
|||
// 审核人员列表 从store获取真实的项目成员列表 |
|||
// 项目加载之初 就已经获取了 |
|||
const checkers = computed(() => store.state.role.members); |
|||
// 所有选中的审核人 |
|||
const checkedCheckers = ref([]); |
|||
// 折叠状态 显示的选中的检查人(超过3个用...显示) |
|||
const showCheckers = computed(() => (checkedCheckers.value.length > 3 ? checkedCheckers.value.slice(0, 3) : checkedCheckers.value)); |
|||
|
|||
/** |
|||
* 点击成员 切换检查人的选中状态 |
|||
* @param {object} member 成员对象 |
|||
*/ |
|||
function handleSelectChecker(member) { |
|||
const target = checkedCheckers.value.find(item => item.memberId === member.memberId); |
|||
if (target) { |
|||
// 已经选中 将此项移除选中的数组中 |
|||
checkedCheckers.value = checkedCheckers.value.filter(item => item.memberId !== member.memberId); |
|||
} else { |
|||
checkedCheckers.value.push(member); |
|||
} |
|||
} |
|||
</script> |
|||
@ -1,18 +0,0 @@ |
|||
<template> |
|||
<!-- 任务名插件 --> |
|||
<div class="u-font-14"> |
|||
{{ deliver.deliverName }} |
|||
</div> |
|||
</template> |
|||
|
|||
<script setup> |
|||
// import { useStore } from 'vuex'; |
|||
import { ref, inject } from 'vue'; |
|||
|
|||
// const store = useStore(); |
|||
// const task = inject('task'); |
|||
const pluginInfo = inject('pluginInfo'); |
|||
const deliver = ref(null); // 服务端返回的交付物信息 |
|||
|
|||
deliver.value = pluginInfo && pluginInfo.data ? JSON.parse(pluginInfo.data) : null; |
|||
</script> |
|||
@ -0,0 +1,127 @@ |
|||
<template> |
|||
<a-modal v-model:visible="visible" centered :footer="null" :closable="false" @cancel="handleHide"> |
|||
<div> |
|||
<!-- 审核标题 --> |
|||
<div class="modal-content-head">{{ data.mode === 'RESOLVE' ? '审核通过' : '审核驳回' }}</div> |
|||
|
|||
<div class="modal-content-body mt-5"> |
|||
<!-- 评分 --> |
|||
<div class="flex justify-between items-center mb-4" v-show="data.mode === 'RESOLVE'"> |
|||
<a-input-number v-model:value="score" :max="100" :min="0" :step="1"> </a-input-number> |
|||
<div class="w-64"> |
|||
<a-progress :percent="score" /> |
|||
</div> |
|||
</div> |
|||
|
|||
<a-textarea v-model:value="commit" /> |
|||
|
|||
<div class="common-list"> |
|||
<div v-for="(item, index) in words" :key="index" class="leading-12" @click="commit = item"> |
|||
{{ item }} |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
<div class="modal-content-foot mt-3 flex justify-center items-center"> |
|||
<a-button class="mr-4" @click="handleHide">取消</a-button> |
|||
<a-button class="" type="primary" @click="handleSubmit(data.mode)">确定</a-button> |
|||
</div> |
|||
</div> |
|||
</a-modal> |
|||
</template> |
|||
|
|||
<script setup> |
|||
import { ref, computed, watch, inject, defineProps, defineEmits } from 'vue'; |
|||
import { useStore } from 'vuex'; |
|||
import { message } from 'ant-design-vue'; |
|||
import { checkDeliver } from 'apis'; |
|||
import { quickWords } from '@/utils/deliver'; |
|||
|
|||
const props = defineProps({ |
|||
data: { type: Object, default: () => {} }, |
|||
msgId: { default: '', type: String }, |
|||
}); |
|||
|
|||
const emits = defineEmits(['hide', 'submit-end']); |
|||
|
|||
const store = useStore(); |
|||
const task = inject('task'); |
|||
const projectId = computed(() => store.getters['project/projectId']); |
|||
const roleId = computed(() => store.state.role.roleId); |
|||
const words = computed(() => quickWords[props.data.mode]); // 快捷用语 |
|||
|
|||
const visible = ref(false); |
|||
|
|||
const commit = ref(''); // 提交的信息 |
|||
const score = ref(100); // 评分 |
|||
|
|||
watch(props, () => { |
|||
visible.value = props.data.mode !== 'HIDE'; |
|||
}); |
|||
|
|||
/** |
|||
* 提交评审信息 |
|||
* 提交成功后隐藏modal 重置表单控件 |
|||
* 给父组件信息 更新值 |
|||
* @param {string} mode 'RESOLVE'|'REJECT' |
|||
*/ |
|||
async function handleSubmit(mode) { |
|||
try { |
|||
const { url } = store.state.projects.project; |
|||
const deliverRecordId = props.data.deliverRecordId(); |
|||
const param = { |
|||
projectId: projectId.value, |
|||
roleId: roleId.value, |
|||
deliverRecordId, |
|||
type: mode === 'RESOLVE' ? 1 : 2, |
|||
remark: commit.value, |
|||
score: mode === 'RESOLVE' ? score.value : '', |
|||
msgId: task.msgId, |
|||
}; |
|||
|
|||
await checkDeliver(param, url); |
|||
handleHide(); // 隐藏 + 重置 |
|||
message.info('审核信息提交成功'); |
|||
// 通知父组件评审成功 更新信息 |
|||
emits('submit-end', param); |
|||
} catch (error) { |
|||
console.error('error: ', error); |
|||
message.info('审核信息提交失败, 请稍后重试'); |
|||
} |
|||
} |
|||
|
|||
// 隐藏及 重置 |
|||
function handleHide() { |
|||
emits('hide'); |
|||
// 重置相关数据 |
|||
score.value = 100; |
|||
commit.value = ''; |
|||
} |
|||
</script> |
|||
|
|||
<style scoped> |
|||
.modal-content-head { |
|||
text-align: center; |
|||
margin-top: 40rpx; |
|||
margin-bottom: 20rpx; |
|||
font-size: 16px; |
|||
font-weight: 600; |
|||
} |
|||
|
|||
.common-list { |
|||
height: 12.5rem; |
|||
overflow-y: scroll; |
|||
} |
|||
|
|||
.common-list::-webkit-scrollbar { |
|||
display: none; |
|||
} |
|||
|
|||
.common-list div { |
|||
border-bottom: 1px solid #e5e7eb; |
|||
} |
|||
|
|||
.common-list div:last-child { |
|||
border-bottom: none; |
|||
} |
|||
</style> |
|||
@ -0,0 +1,104 @@ |
|||
<template> |
|||
<div> |
|||
<!-- 审核标题 --> |
|||
<div class="flex justify-between items-center" @click="collapsed = !collapsed"> |
|||
<div>{{ deliverData ? deliverData.deliverName : '' }}审核状态</div> |
|||
<DownOutlined v-if="!collapsed" /> |
|||
<UpOutlined v-else /> |
|||
</div> |
|||
|
|||
<!-- 审核结果 --> |
|||
<div v-show="collapsed"> |
|||
<!-- 提交人和时间 --> |
|||
<div class="my-2 flex justify-between items-center"> |
|||
<div class="text-gray-400 text-xs"> |
|||
<span class="mr-4" v-if="deliverData.submitMemberName">{{ deliverData.submitMemberName }}</span> |
|||
<span v-if="deliverData.submitTime"> {{ dayjs(+deliverData.submitTime).format('MM-DD HH:mm') }}</span> |
|||
</div> |
|||
|
|||
<span class="text-blue-400 text-xs text-right" @click="openDeliverHistory">历史交付物</span> |
|||
</div> |
|||
|
|||
<div @click="openLink" class="break-all text-blue-400 text-xs my-1" v-if="deliverData.details && deliverData.details[0]"> |
|||
{{ deliverData.details[0] }} |
|||
</div> |
|||
|
|||
<!-- 审核人 标题 --> |
|||
<div class="text-gray-400 flex justify-between mt-3"> |
|||
<span>审核</span> |
|||
<span class="text-blue-400 text-xs" @click="openMoreRecords">更多审核记录</span> |
|||
</div> |
|||
|
|||
<!-- 审核人 列表 --> |
|||
<div v-if="deliverData.checkerList"> |
|||
<div class="mt-2 text-sm flex justify-between" v-for="(item, index) in deliverData.checkerList" :key="index"> |
|||
<div> |
|||
<div class="font-semibold">{{ item.checkerName }}</div> |
|||
<div class="text-xs text-gray-400">{{ item.remark }}</div> |
|||
<div class="text-xs text-gray-400" v-if="+item.checkTime > 0">{{ dayjs(+item.checkTime).format('MM-DD HH:mm') }}</div> |
|||
</div> |
|||
|
|||
<!-- 不是自己 显示审核状态 --> |
|||
<div v-show="item.isMine !== 1" class="text-xs"> |
|||
<span v-if="item.status === 1" class="text-green-600"> 已通过 </span> |
|||
<span v-else-if="item.status === 2" class="text-red-600"> 已驳回 </span> |
|||
<span v-else class="text-gray-400"> 待审核 </span> |
|||
</div> |
|||
|
|||
<!-- 自己是当前审核人 且未审核状态 --> |
|||
<div v-show="item.isMine === 1 && (item.status === null || item.status === 0)"> |
|||
<a-button size="small" shape="round" class="mr-4 h-1-4 leading-1-4" type="primary" @click="checkModal.mode = 'RESOLVE'"> |
|||
通过 |
|||
</a-button> |
|||
<a-button size="small" shape="round" class="h-1-4 leading-1-4" type="primary" danger @click="checkModal.mode = 'REJECT'"> |
|||
驳回 |
|||
</a-button> |
|||
</div> |
|||
|
|||
<!-- 自己是审核人 且审核过 当前审核人的审核状态并展示得分情况 --> |
|||
<div v-show="item.isMine === 1 && item.status > 0" class="text-xs"> |
|||
<div class="mb-1"> |
|||
<span v-if="item.status === 1" class="text-green-600"> 已通过 </span> |
|||
<span v-else-if="item.status === 2" class="text-red-600"> 已驳回 </span> |
|||
</div> |
|||
<span class="text-yellow-500 font-medium">{{ item.score }}</span> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
<checkFormModal :data="checkModal" @hide="checkModal.mode = 'HIDE'" @submit-end="$emit('check-success')" /> |
|||
</div> |
|||
</template> |
|||
|
|||
<script setup> |
|||
import { ref, reactive, inject, defineEmits } from 'vue'; |
|||
import { useStore } from 'vuex'; |
|||
import dayjs from 'dayjs'; |
|||
import { DownOutlined, UpOutlined } from '@ant-design/icons-vue'; |
|||
import checkFormModal from './check-form-modal.vue'; |
|||
|
|||
const store = useStore(); |
|||
const deliverData = inject('deliver'); |
|||
const collapsed = ref(false); // 展开/关闭 |
|||
defineEmits(['check-success']); |
|||
|
|||
// 历史交付物 |
|||
function openDeliverHistory() {} |
|||
|
|||
// 交付物链接 |
|||
function openLink() { |
|||
store.commit('task/setTaskDetailUrl', deliverData.details[0]); |
|||
sessionStorage.setItem('targetUrl', deliverData.details[0]); |
|||
store.commit('task/setTaskDetailShow', ''); |
|||
sessionStorage.setItem('taskDetailShow', ''); |
|||
} |
|||
|
|||
const checkModal = reactive({ |
|||
mode: 'HIDE', // HIDE->隐藏 RESOLVE->通过 REJECT->驳回 |
|||
deliverRecordId: () => (deliverData.value ? deliverData.value.deliverRecordId : ''), // 交付物记录id |
|||
}); |
|||
|
|||
// 审核记录 |
|||
function openMoreRecords() {} |
|||
</script> |
|||
@ -0,0 +1,135 @@ |
|||
<template> |
|||
<div @longpress.prevent="showMask = true"> |
|||
<!-- 插件名称和提交按钮 --> |
|||
<div class="flex item-center justify-between"> |
|||
<div class="flex-1"> |
|||
<div v-if="deliver.deliverName" class="relative inline-block"> |
|||
{{ deliver.deliverName }} |
|||
</div> |
|||
</div> |
|||
|
|||
<!-- 提交 --> |
|||
<a-button type="primary" size="small" @click="submit" :disabled="submitState" :loading="submitBtnLoading"> 提交 </a-button> |
|||
</div> |
|||
|
|||
<!-- 插件上传方式 --> |
|||
<div class="mt-3"> |
|||
<div class="link-box"> |
|||
<a-input v-model:value="linkValue" placeholder="请输入交付物地址/链接" /> |
|||
</div> |
|||
<div class="mt-3"> |
|||
<!-- <a-button type="primary" size="small" class="mr-3" @click="paste">粘贴</a-button> --> |
|||
|
|||
<div class="inline-block"> |
|||
<a-upload name="file" :action="action" :headers="headers" :max-count="1" @change="handleChange"> |
|||
<a-button type="primary" size="small" class="mr-3">文件</a-button> |
|||
</a-upload> |
|||
</div> |
|||
|
|||
<!-- <a-button type="primary" size="small" class="mr-3" @click="uploadPhoto">拍照</a-button> --> |
|||
</div> |
|||
</div> |
|||
|
|||
<!-- 审核人 --> |
|||
<Reviewer class="mt-3" ref="reviewerRef" /> |
|||
</div> |
|||
</template> |
|||
|
|||
<script setup> |
|||
import { useStore } from 'vuex'; |
|||
import { ref, inject, computed, defineEmits } from 'vue'; |
|||
import { message } from 'ant-design-vue'; |
|||
import { submitDeliverInfo, uploadImg } from 'apis'; |
|||
import Reviewer from '@/components/tall/Reviewer/Reviewer.vue'; |
|||
|
|||
const store = useStore(); |
|||
const projectId = computed(() => store.getters['project/projectId']); |
|||
const roleId = computed(() => store.state.role.roleId); |
|||
|
|||
const deliver = inject('deliver'); // 交付物初始值 |
|||
const task = inject('task'); // 任务信息 |
|||
|
|||
const linkValue = ref(''); // 链接的值 |
|||
|
|||
const action = uploadImg; |
|||
const token = computed(() => store.state.user.token); |
|||
const headers = { Authorization: `Bearer ${token.value}` }; |
|||
|
|||
// 判断提交按钮的状态 |
|||
const submitState = computed(() => !linkValue.value); // 按钮状态 |
|||
const submitBtnLoading = ref(false); // 是否加载中 |
|||
|
|||
const reviewerRef = ref(null); // 审核人组件 |
|||
|
|||
const showMask = ref(false); // 编辑和删除页面是否显示 |
|||
|
|||
const emits = defineEmits(['upload-success']); |
|||
|
|||
function handleChange(info) { |
|||
console.log('info', info); |
|||
} |
|||
|
|||
// 提交交付物 |
|||
async function submit() { |
|||
const { checkedCheckers } = reviewerRef.value; // 拿到选择检查人组件中选中的检查人 |
|||
// 提交前的验证 |
|||
if (!validateDeliverForm(checkedCheckers)) return; |
|||
|
|||
submitBtnLoading.value = true; // 按钮loading |
|||
|
|||
try { |
|||
const checkerList = []; |
|||
checkedCheckers.forEach(item => { |
|||
checkerList.push(item.memberId); |
|||
}); |
|||
|
|||
const { url } = store.state.projects.project; |
|||
|
|||
const param = { |
|||
projectId: projectId.value, |
|||
roleId: roleId.value, |
|||
deliverId: deliver.value.deliverId, |
|||
fileList: [linkValue.value], |
|||
checkerList, |
|||
msgId: task.msgId, |
|||
}; |
|||
|
|||
const data = await submitDeliverInfo(param, url); |
|||
message.info('提交交付物信息成功'); |
|||
resetControlState(); // 重置控件的初始状态 |
|||
emits('upload-success'); |
|||
} catch (error) { |
|||
message.info('提交交付物信息失败'); |
|||
throw new Error(error); |
|||
} |
|||
} |
|||
|
|||
// 粘贴 |
|||
// function paste() {} |
|||
|
|||
// 拍照 |
|||
// function uploadPhoto() {} |
|||
|
|||
// 验证提交的交付物信息格式 |
|||
function validateDeliverForm(checkedCheckers) { |
|||
const reg = /[a-zA-z]+:\/\/[^\s]*/; |
|||
if (!reg.test(linkValue.value)) { |
|||
// 显示toast信息 |
|||
message.info('请输入正确的链接'); |
|||
return false; |
|||
} |
|||
// 没有检查人 提示选择检查人 |
|||
if (!checkedCheckers || !checkedCheckers.length) { |
|||
message.info('请选择检查人'); |
|||
return false; |
|||
} |
|||
return true; |
|||
} |
|||
|
|||
// 重置控件的初始状态 |
|||
function resetControlState() { |
|||
submitBtnLoading.value = false; // 隐藏loading |
|||
linkValue.value = ''; // 清空输入框内容 |
|||
reviewerRef.value.collapsed = true; // 折叠检查人面板 |
|||
} |
|||
</script> |
|||
@ -0,0 +1,37 @@ |
|||
<template> |
|||
<!-- 任务名插件 --> |
|||
<div> |
|||
<p-deliver-upload v-if="deliver" @upload-success="getDeliverData"></p-deliver-upload> |
|||
|
|||
<p-deliver-check class="mt-3" v-if="deliver && deliver.details && deliver.details.length"></p-deliver-check> |
|||
</div> |
|||
</template> |
|||
|
|||
<script setup> |
|||
// import { useStore } from 'vuex'; |
|||
import { ref, inject, provide } from 'vue'; |
|||
import { getDeliverByTaskId } from 'apis'; |
|||
import pDeliverUpload from '@/plugins/p-deliver/p-deliver-upload.vue'; |
|||
import pDeliverCheck from '@/plugins/p-deliver/p-deliver-check.vue'; |
|||
|
|||
// const store = useStore(); |
|||
const task = inject('task'); |
|||
const pluginInfo = inject('pluginInfo'); |
|||
const deliver = ref(null); // 服务端返回的交付物信息 |
|||
|
|||
deliver.value = pluginInfo && pluginInfo.data ? JSON.parse(pluginInfo.data) : null; |
|||
provide('deliver', deliver); |
|||
|
|||
// 根据任务id获取交付物信息 |
|||
async function getDeliverData() { |
|||
try { |
|||
const { id: taskId } = task; |
|||
if (!taskId) return; |
|||
const param = { taskId }; |
|||
const data = await getDeliverByTaskId(param); |
|||
deliver.value = data; |
|||
} catch (error) { |
|||
console.log('error: ', error); |
|||
} |
|||
} |
|||
</script> |
|||
@ -0,0 +1,9 @@ |
|||
// 上传文件的扩展名
|
|||
export const UPLOAD_EXTENSION = ['.xls', '.xlsx', '.zip', '.exe', '.pdf', '.doc', '.docx', '.ppt', '.pptx']; |
|||
|
|||
// 审核的快捷用语
|
|||
export const quickWords = { |
|||
// RESOLVE: ['加油,再接再厉!', '很棒!', '不错,很详细!', '不详细', '还有需要改进的地方', '驳回审批']
|
|||
RESOLVE: ['加油,再接再厉!', '很棒!', '不错,很详细!', '加油,再接再厉'], // 审核通过常用的审批语
|
|||
REJECT: ['不详细', '还有需要改进的地方', '驳回审批1', '驳回审批2'], // 审核驳回常用的审批语
|
|||
}; |
|||
Loading…
Reference in new issue