pc端
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

645 lines
20 KiB

<template>
<div class="p-4">
<!-- 标题 -->
<div class="mb-8 flex justify-between items-center">
<div class="text-2xl font-semibold">流水账</div>
<div class="flex items-center">
<a-button type="primary" v-if="!morning" @click="punch(0)"> 早打卡 </a-button>
<a-button type="primary" v-else>
{{ dayjs(+morning).format('HH:mm') }}
</a-button>
<a-button class="mx-5" type="primary" v-if="!night" @click="punch(1)"> 晚打卡 </a-button>
<a-button class="mx-5" type="primary" v-else>
{{ dayjs(+night).format('HH:mm') }}
</a-button>
<FullscreenExitOutlined v-if="isFullScreen" class="text-lg" style="color: #777" @click="changeIsFullScreen(false)" />
<FullscreenOutlined v-else class="text-lg" style="color: #777" @click="changeIsFullScreen(true)" />
</div>
</div>
<!-- 筛选 -->
<a-form class="flex flex-wrap" :model="formState">
<a-form-item name="timeRange" label="时间" style="width: 280px; margin-right: 20px; margin-bottom: 12px">
<a-range-picker v-model:value="formState.timeRange" />
</a-form-item>
<a-form-item name="staffRange" label="员工" style="width: 280px; margin-right: 20px; margin-bottom: 12px">
<a-select
v-model:value="formState.staffRange"
:options="memberList"
:field-names="{ label: 'empName', value: 'empName' }"
mode="multiple"
placeholder="请选择员工"
></a-select>
</a-form-item>
<a-form-item name="programName" label="项目" style="width: 280px; margin-right: 20px; margin-bottom: 12px">
<a-select
v-model:value="formState.programName"
:options="proList"
:field-names="{ label: 'projectName', value: 'projectName' }"
mode="multiple"
placeholder="请输入项目名称"
></a-select>
</a-form-item>
<div class="block w-full">
<a-button type="primary" html-type="submit" @click="handleSubmit">筛选</a-button>
<a-button class="mx-3" type="primary" @click="handleExport">导出</a-button>
<a-button @click="resetData">重置</a-button>
</div>
</a-form>
<!-- 表格 -->
<a-table class="mt-6" :columns="columns" :data-source="columnDatas" bordered :pagination="false" :scroll="{ x: 1400 }">
<template #bodyCell="{ column, text, record }">
<div
style="height: 100px"
:class="{ 'text-left': column.dataIndex === 'program' }"
class="overflow-y-auto task-today"
@dblclick="showModal(record, column.proId)"
>
<template v-if="column.dataIndex === 'program'">
<template v-for="item in text">
<template v-if="column.proId === item.proId">
<div class="mb-3" v-for="(task, key) in item.tasks" :key="key">
<PushpinOutlined class="mr-2" v-if="task.cooperation === 1" title="协作任务" />
<span>
我今日计划结果{{ key + 1 }}{{ task.taskName }}交付物是{{ task.deliverName }}截止时间{{
dayjs(+task.deadline).format('MM-DD')
}}时长{{ task.duration / 3600000 }}检查人{{ task.checker }}
</span>
</div>
</template>
</template>
</template>
<template v-else>
{{ text }}
</template>
</div>
</template>
</a-table>
<a-pagination
class="text-right"
v-model:current="current"
v-model:pageSize="pageSize"
:total="dataTotal"
@change="handlePage"
show-less-items
/>
</div>
<a-modal
v-model:visible="visible"
title="今日计划"
centered
:footer="null"
width="800px"
style="padding: 0; max-height: 800px; overflow-y: auto"
>
<div style="padding: 0 96px">
<div class="mb-4">当前操作项目{{ currProName }}当前操作成员{{ currEmpName }}</div>
<a-form :model="modalFormState">
<div v-for="(item, index) in modalFormState" :key="index">
<div class="flex justify-between items-center text-gray-400">
<div class="mb-5 text-right" style="width: 120px">计划结果{{ index + 1 }}</div>
<DeleteOutlined v-if="modalFormState.length > 1" @click="delFormItem(index)" />
</div>
<a-form-item name="taskName" style="margin-bottom: 20px">
<div class="flex">
<label class="flex-shrink-0 text-right" style="width: 120px; line-height: 32px">
<span style="color: #ff5353; margin-right: 5px">*</span>今日计划结果{{ index + 1 }}
</label>
<a-textarea v-model:value="item.taskName" :disabled="isDisabled" placeholder="请输入今日计划" />
</div>
</a-form-item>
<a-form-item name="deliverName" style="margin-bottom: 20px">
<div class="flex">
<label class="flex-shrink-0 text-right" style="width: 120px; line-height: 32px">
<span style="color: #ff5353; margin-right: 5px">*</span>交付物
</label>
<a-input v-model:value="item.deliverName" :disabled="isDisabled" placeholder="请输入交付物" />
</div>
</a-form-item>
<a-form-item name="showDeadLine" style="margin-bottom: 20px">
<div class="flex">
<label class="flex-shrink-0 text-right" style="width: 120px; line-height: 32px">
<span style="color: #ff5353; margin-right: 5px">*</span>截止时间
</label>
<a-date-picker class="w-full" v-model:value="item.showDeadLine" :disabled="isDisabled" />
</div>
</a-form-item>
<a-form-item name="showDuration" style="margin-bottom: 20px">
<div class="flex">
<label class="flex-shrink-0 text-right" style="width: 120px; line-height: 32px">
<span style="color: #ff5353; margin-right: 5px">*</span>时长
</label>
<a-input-group compact class="items-center" style="width: 100%; display: flex">
<a-auto-complete
style="width: calc(100% - 35px)"
v-model:value="item.showDuration"
:options="workDurations"
placeholder="请输入工作时长"
:disabled="isDisabled"
@change="handleDuration($event, index)"
/>
<div style="width: 35px; border: none" class="text-right">小时</div>
</a-input-group>
</div>
</a-form-item>
<a-form-item name="checker" style="margin-bottom: 20px">
<div class="flex">
<label class="flex-shrink-0 text-right" style="width: 120px; line-height: 32px">
<span style="color: #ff5353; margin-right: 5px">*</span>检查人
</label>
<a-select
v-model:value="item.checker"
:options="memberList"
:field-names="{ label: 'empName', value: 'empName' }"
show-search
placeholder="请选择检查人"
:disabled="isDisabled"
:filter-option="filterOption"
@change="handleInspector($event, index)"
></a-select>
</div>
</a-form-item>
<a-form-item name="cooperation" style="margin-bottom: 20px">
<div class="flex">
<label class="flex-shrink-0 text-right" style="width: 120px; line-height: 32px"> 是否协作 </label>
<a-radio-group v-model:value="item.cooperation" class="w-full items-center" style="display: flex">
<a-radio class="items-center" style="display: flex" :value="1" :disabled="isDisabled"></a-radio>
<a-radio class="items-center" style="display: flex" :value="0" :disabled="isDisabled"></a-radio>
</a-radio-group>
</div>
</a-form-item>
<a-form-item name="deliverLink" style="margin-bottom: 20px">
<div class="flex">
<label class="flex-shrink-0 text-right" style="width: 120px; line-height: 32px"> 交付物链接 </label>
<a-textarea v-model:value="item.deliverLink" :disabled="isDisabled" placeholder="请输入交付物链接" />
</div>
</a-form-item>
</div>
<a-form-item v-if="!isDisabled">
<div class="flex">
<label class="flex-shrink-0 text-right" style="width: 120px"></label>
<a-button style="color: #1890ff; border-color: #1890ff" type="dashed" @click="addFormItem">+ 添加</a-button>
</div>
</a-form-item>
<a-form-item v-if="!isDisabled">
<div class="flex items-center">
<label class="flex-shrink-0 text-right" style="width: 120px"></label>
<a-button class="mr-4" type="primary" @click="submitForm">提交</a-button>
<a-button @click="cancleModal">取消</a-button>
</div>
</a-form-item>
</a-form>
</div>
</a-modal>
</template>
<script setup>
import { useStore } from 'vuex';
import { reactive, ref, onMounted, computed, watch } from 'vue';
import dayjs from 'dayjs';
import { FullscreenExitOutlined, FullscreenOutlined, DeleteOutlined, PushpinOutlined } from '@ant-design/icons-vue';
import { getBasicInfo, queryTasks, submitTask, clockQuery, clockPunch, exportQuery } from 'apis';
import { message } from 'ant-design-vue';
const store = useStore();
const projectId = computed(() => store.getters['project/projectId']);
const sessionProjectId = sessionStorage.getItem('projectId');
const roleId = computed(() => store.state.role.roleId);
const members = computed(() => store.state.role.members);
const userId = computed(() => store.getters['user/userId']); // 用户id
const isFullScreen = computed(() => store.state.layout.isFullScreen); // 是否全屏
const visible = ref(false); // 是否显示弹框表单
const isDisabled = ref(false); // 是否允许编辑
const morning = ref(false); // 早打卡
const night = ref(false); // 晚打卡
const checkerId = ref(null); // 打卡审核人id
const recordId = ref(null); // 记录id
const memberId = ref(null); // 成员id
// 筛选表单
const formState = reactive({
timeRange: [dayjs(+new Date().getTime()), dayjs(+new Date().getTime()).add(1, 'day')], // 时间范围
staffRange: [], // 员工
programName: [], // 项目名称
});
const startTime = dayjs(+new Date().getTime());
const endTime = dayjs(+new Date().getTime()).add(1, 'day');
// 下拉选选项
const memberList = ref([]); // 成员列表
const proList = ref([]); // 项目列表
const emps = ref([]); // 选中的筛选成员
const proDatas = ref([]); // 选中的筛选项目
// 表格元素
const columns = ref();
const current = ref(1); // 当前页数
const pageSize = ref(10); // 每页条数
const dataTotal = ref(0); // 数据总量
// 表格数据
const columnDatas = ref([]);
// 今日计划表单
const modalFormState = ref([]);
// 工作时长
const workDurations = [...Array(8)].map((_, i) => ({ value: `${i + 1}` }));
const isSubmitDeliver = ref(false); // 是否提交交付物
const currEmpId = ref(null); // 当前点击的用户id
const currEmpName = ref(null); // 当前点击的用户名
const currProId = ref(null); // 当前点击的项目id
const currProName = ref(null); // 当前点击的项目名
onMounted(async () => {
getClockQuery();
await getInfo();
await getQueryTasks();
});
// 改变全屏状态
function changeIsFullScreen(data) {
store.commit('layout/setIsFullScreen', data);
}
// 获取基本信息(成员列表、项目列表)
async function getInfo() {
try {
const { url } = store.state.projects.project;
const data = await getBasicInfo(url);
memberList.value = data.emps;
pageSize.value = data.emps.length;
proList.value = data.pros;
} catch (error) {
message.info(error);
throw new Error(error);
}
}
// 获取任务列表
async function getQueryTasks() {
try {
const { url } = store.state.projects.project;
const params = {
param: {
startTime: startTime.value || formState.timeRange[0].startOf('day').valueOf(),
endTime: endTime.value || formState.timeRange[1].startOf('day').valueOf(),
emps: emps.value || [],
pros: proDatas.value || [],
},
};
// 相差天数
const days = dayjs(+formState.timeRange[1].startOf('day')).diff(+formState.timeRange[0].startOf('day'), 'day');
dataTotal.value = pageSize.value * days; // 分页数据总量
const data = await queryTasks(params, url);
columns.value = [
{
title: '时间',
width: 100,
dataIndex: 'time',
key: 'time',
fixed: 'left',
align: 'center',
},
{
title: '员工',
width: 100,
dataIndex: 'staff',
key: 'staff',
fixed: 'left',
align: 'center',
},
];
columnDatas.value = [];
data.pros.forEach((proId, index) => {
const proInfo = proList.value.find(item => item.id === proId);
columns.value.push({
title: proInfo.projectName,
dataIndex: 'program',
key: `${index}`,
proId,
width: 300,
align: 'center',
});
});
memberList.value.forEach((member, key) => {
const obj = {
empId: member.id,
key,
time: dayjs(+params.param.startTime).format('MM-DD'),
staff: `${member.empName}`,
program: [],
};
const currMember = data.recs.filter(item => member.id === item.empId);
if (currMember.length > 0) {
const { pros } = currMember[0];
obj.program = pros;
}
columnDatas.value.push(obj);
});
} catch (error) {
message.info(error);
throw new Error(error);
}
}
// 筛选
function handleSubmit() {
emps.value = [];
proDatas.value = [];
memberList.value.forEach(item => {
const index = formState.staffRange.findIndex(emp => emp === item.empName);
if (index > -1) emps.value.push(item.id);
});
proList.value.forEach(item => {
const index = formState.programName.findIndex(pro => pro === item.projectName);
if (index > -1) proDatas.value.push(item.id);
});
const end = dayjs(+formState.timeRange[0].startOf('day')).add(1, 'day');
endTime.value = dayjs(+end).valueOf();
getQueryTasks();
}
// 重置
function resetData() {
formState.timeRange = [dayjs(+new Date().getTime()), dayjs(+new Date().getTime()).add(1, 'day')];
formState.staffRange = [];
formState.programName = [];
emps.value = [];
proDatas.value = [];
}
// 导出
async function handleExport() {
try {
const start = formState.timeRange[0].startOf('day').valueOf();
const end = formState.timeRange[1].startOf('day').valueOf();
const params = {
param: {
projectId: projectId.value || sessionProjectId,
roleId: roleId.value,
memberIdList: [],
startTime: start,
endTime: end,
},
};
const { url } = store.state.projects.project;
const data = await exportQuery(params, url);
window.open(data, '_blank');
} catch (error) {
message.info(error);
throw new Error(error);
}
}
// 分页改变
function handlePage(e) {
const start = dayjs(+formState.timeRange[0].startOf('day')).add(e - 1, 'day');
const end = dayjs(+start).add(1, 'day');
startTime.value = dayjs(+start).valueOf();
endTime.value = dayjs(+end).valueOf();
getQueryTasks();
}
// 打开今日计划编辑框
function showModal(data, proId) {
currEmpId.value = data.empId;
currProId.value = proId;
const currEmpInfo = memberList.value.find(item => item.id === data.empId);
currEmpName.value = currEmpInfo.empName;
const currProInfo = proList.value.find(item => item.id === proId);
currProName.value = currProInfo.projectName;
isDisabled.value = dayjs(+new Date().getTime()).format('MM-DD') !== data.time;
visible.value = true;
modalFormState.value = [
{
taskName: '',
deliverName: '',
showDeadLine: dayjs(+new Date().getTime()),
deadline: '',
showDuration: '2',
duration: '',
checker: '',
cooperation: 0,
deliverLink: '',
sequence: 0,
},
];
data.program.forEach(item => {
if (proId === item.proId) {
modalFormState.value = [...item.tasks];
}
});
modalFormState.value.forEach((item, index) => {
item.showDeadLine = item.deadline ? dayjs(+item.deadline) : dayjs(+new Date().getTime());
item.showDuration = item.duration ? `${item.duration / 3600000}` : '2';
item.sequence = index;
});
}
// 检查人筛选
function filterOption(input, option) {
return option.empName.indexOf(input) >= 0;
}
// 选择工作时长
function handleDuration(e, index) {
modalFormState.value[index].duration = e;
}
// 选择检查人
function handleInspector(e, index) {
modalFormState.value[index].checker = e;
}
// 添加计划
function addFormItem() {
const len = modalFormState.value.length;
modalFormState.value.push({
taskName: '',
deliverName: '',
showDeadLine: dayjs(+new Date().getTime()),
deadline: '',
showDuration: '2',
duration: '',
checker: '',
cooperation: 0,
deliverLink: '',
sequence: len,
});
}
// 删除计划
const delFormItem = index => {
modalFormState.value.splice(index, 1);
};
// 取消任务弹框
function cancleModal() {
visible.value = false;
}
// 提交表单
async function submitForm() {
let flag = false;
for (let i = 0; i < modalFormState.value.length; i++) {
const data = modalFormState.value[i];
data.deadline = dayjs(+data.showDeadLine).valueOf();
data.duration = data.showDuration * 3600000;
if (!data.taskName) {
message.info(`请输入今日计划结果${i + 1}`);
return false;
}
if (!data.deliverName) {
message.info(`请输入交付物`);
return false;
}
if (!data.duration) {
message.info(`请输入时长`);
return false;
}
if (!data.checker) {
message.info(`请选择检查人`);
return false;
}
const reg = /[a-zA-z]+:\/\/[^\s]*/;
if (data.deliverLink && !reg.test(data.deliverLink)) {
message.info(`请输入正确的链接`);
return false;
}
flag = true;
}
if (flag) {
isSubmitDeliver.value = true;
}
const params = {
param: {
time: new Date().getTime(),
empId: currEmpId.value,
proId: currProId.value,
tasks: modalFormState.value,
},
};
try {
const { url } = store.state.projects.project;
const data = await submitTask(params, url);
visible.value = false;
// punch();
getQueryTasks();
} catch (error) {
message.info(error);
throw new Error(error);
}
}
// 获取当前打卡信息
async function getClockQuery() {
const start = dayjs(+new Date().getTime())
.startOf('day')
.valueOf();
const end = dayjs(+new Date().getTime())
.endOf('day')
.valueOf();
const params = {
param: {
projectId: projectId.value || sessionProjectId,
roleId: roleId.value,
memberIdList: [],
startTime: start,
endTime: end,
},
};
try {
const { url } = store.state.projects.project;
const data = await clockQuery(params, url);
// 审核人
if (data[0].recordList[0].lastCheckerId) {
checkerId.value = data[0].recordList[0].lastCheckerId;
} else if (data[0].recordList[0].checkerId) {
checkerId.value = data[0].recordList[0].checkerId;
} else {
checkerId.value = members.value[0].memberId;
}
// 成员id
if (data[0].recordList[0].isMine === 1) {
memberId.value = data[0].recordList[0].memberId;
}
recordId.value = data[0].recordList[0].id;
morning.value = data[0].recordList[0].morning; // 早打卡状态
night.value = data[0].recordList[0].night; // 晚打卡状态
} catch (error) {
message.info(error);
throw new Error(error);
}
}
async function punch(clockType) {
if ((clockType === 0 && morning.value) || (clockType === 1 && night.value)) return;
const dateTime = dayjs(+new Date().getTime()).valueOf(); // 打卡时间
const params = { param: { checkerId: checkerId.value, memberId: memberId.value, id: recordId.value, clockType, dateTime } };
try {
const { url } = store.state.projects.project;
const data = await clockPunch(params, url);
getClockQuery();
} catch (error) {
message.info(`打卡失败,${error}`);
throw new Error(error);
}
}
</script>
<style scoped>
.task-today::-webkit-scrollbar {
width: 0 !important;
}
</style>