feat(erp/timesheet): 新增工时填报管理模块

dev
Yangk 1 week ago
parent d4aa14cc21
commit ad81d2028e

@ -0,0 +1,76 @@
import request from '@/utils/request';
import { AxiosPromise } from 'axios';
import { TimesheetDeptVO, TimesheetDeptForm, TimesheetDeptQuery } from '@/api/oa/erp/timesheetDept/types';
/**
*
* @param query
* @returns {*}
*/
export const listTimesheetDept = (query?: TimesheetDeptQuery): AxiosPromise<TimesheetDeptVO[]> => {
return request({
url: '/oa/erp/timesheetDept/list',
method: 'get',
params: query
});
};
/**
*
* @param timesheetDeptId
*/
export const getTimesheetDept = (timesheetDeptId: string | number): AxiosPromise<TimesheetDeptVO> => {
return request({
url: '/oa/erp/timesheetDept/' + timesheetDeptId,
method: 'get'
});
};
/**
*
* @param data
*/
export const addTimesheetDept = (data: TimesheetDeptForm) => {
return request({
url: '/oa/erp/timesheetDept',
method: 'post',
data: data
});
};
/**
*
* @param data
*/
export const updateTimesheetDept = (data: TimesheetDeptForm) => {
return request({
url: '/oa/erp/timesheetDept',
method: 'put',
data: data
});
};
/**
*
* @param timesheetDeptId
*/
export const delTimesheetDept = (timesheetDeptId: string | number | Array<string | number>) => {
return request({
url: '/oa/erp/timesheetDept/' + timesheetDeptId,
method: 'delete'
});
};
/**
*
* @param query
* @returns {*}
*/
export function getErpTimesheetDeptList (query) {
return request({
url: '/oa/erp/timesheetDept/getErpTimesheetDeptList',
method: 'get',
params: query
});
};

@ -0,0 +1,116 @@
export interface TimesheetDeptVO {
/**
* ID
*/
timesheetDeptId: string | number;
/**
* ID
*/
timesheetId: string | number;
/**
*
*/
sortOrder: number;
/**
*
*/
workDescription: string;
/**
* ID
*/
deptId: string | number;
/**
* 线ID
*/
deptManagerId: string | number;
/**
*
*/
hours: number;
}
export interface TimesheetDeptForm extends BaseEntity {
/**
* ID
*/
timesheetDeptId?: string | number;
/**
* ID
*/
timesheetId?: string | number;
/**
*
*/
sortOrder?: number;
/**
*
*/
workDescription?: string;
/**
* ID
*/
deptId?: string | number;
/**
* 线ID
*/
deptManagerId?: string | number;
/**
*
*/
hours?: number;
}
export interface TimesheetDeptQuery extends PageQuery {
/**
* ID
*/
timesheetId?: string | number;
/**
*
*/
sortOrder?: number;
/**
*
*/
workDescription?: string;
/**
* ID
*/
deptId?: string | number;
/**
* 线ID
*/
deptManagerId?: string | number;
/**
*
*/
hours?: number;
/**
*
*/
params?: any;
}

@ -0,0 +1,88 @@
import request from '@/utils/request';
import { AxiosPromise } from 'axios';
import { TimesheetInfoVO, TimesheetInfoForm, TimesheetInfoQuery } from '@/api/oa/erp/timesheetInfo/types';
/**
*
* @param query
* @returns {*}
*/
export const listTimesheetInfo = (query?: TimesheetInfoQuery): AxiosPromise<TimesheetInfoVO[]> => {
return request({
url: '/oa/erp/timesheetInfo/list',
method: 'get',
params: query
});
};
/**
*
* @param timesheetId
*/
export const getTimesheetInfo = (timesheetId: string | number): AxiosPromise<TimesheetInfoVO> => {
return request({
url: '/oa/erp/timesheetInfo/' + timesheetId,
method: 'get'
});
};
/**
*
* @param data
*/
export const addTimesheetInfo = (data: TimesheetInfoForm) => {
return request({
url: '/oa/erp/timesheetInfo',
method: 'post',
data: data
});
};
/**
*
* @param data
*/
export const updateTimesheetInfo = (data: TimesheetInfoForm) => {
return request({
url: '/oa/erp/timesheetInfo',
method: 'put',
data: data
});
};
/**
*
* @param timesheetId
*/
export const delTimesheetInfo = (timesheetId: string | number | Array<string | number>) => {
return request({
url: '/oa/erp/timesheetInfo/' + timesheetId,
method: 'delete'
});
};
/**
*
* @param query
* @returns {*}
*/
export function getErpTimesheetInfoList (query) {
return request({
url: '/oa/erp/timesheetInfo/getErpTimesheetInfoList',
method: 'get',
params: query
});
};
/**
*
* @param data
*/
export const submitTimesheetAndFlowStart = (data: any) => {
return request({
url: '/oa/erp/timesheet/submitAndFlowStart',
method: 'post',
data: data
});
};

@ -0,0 +1,186 @@
export interface TimesheetInfoVO {
/**
* ID
*/
timesheetId: string | number;
/**
*
*/
timesheetCode: string;
/**
* ID
*/
userId: string | number;
/**
* ID
*/
deptId: string | number;
/**
*
*/
startTime: string;
/**
*
*/
endTime: string;
/**
*
*/
totalHours: number;
/**
*
*/
deptHours: number;
/**
*
*/
projectHours: number;
/**
* 1 2 3 4
*/
timesheetStatus: string;
/**
*
*/
flowStatus: string;
/**
*
*/
remark: string;
}
export interface TimesheetInfoForm extends BaseEntity {
/**
* ID
*/
timesheetId?: string | number;
/**
*
*/
timesheetCode?: string;
/**
* ID
*/
userId?: string | number;
/**
* ID
*/
deptId?: string | number;
/**
*
*/
startTime?: string;
/**
*
*/
endTime?: string;
/**
*
*/
totalHours?: number;
/**
*
*/
deptHours?: number;
/**
*
*/
projectHours?: number;
/**
* 1 2 3 4
*/
timesheetStatus?: string;
/**
*
*/
flowStatus?: string;
/**
*
*/
remark?: string;
}
export interface TimesheetInfoQuery extends PageQuery {
/**
*
*/
timesheetCode?: string;
/**
* ID
*/
userId?: string | number;
/**
* ID
*/
deptId?: string | number;
/**
*
*/
startTime?: string;
/**
*
*/
endTime?: string;
/**
*
*/
totalHours?: number;
/**
*
*/
deptHours?: number;
/**
*
*/
projectHours?: number;
/**
* 1 2 3 4
*/
timesheetStatus?: string;
/**
*
*/
flowStatus?: string;
/**
*
*/
params?: any;
}

@ -0,0 +1,76 @@
import request from '@/utils/request';
import { AxiosPromise } from 'axios';
import { TimesheetProjectVO, TimesheetProjectForm, TimesheetProjectQuery } from '@/api/oa/erp/timesheetProject/types';
/**
*
* @param query
* @returns {*}
*/
export const listTimesheetProject = (query?: TimesheetProjectQuery): AxiosPromise<TimesheetProjectVO[]> => {
return request({
url: '/oa/erp/timesheetProject/list',
method: 'get',
params: query
});
};
/**
*
* @param timesheetProjectId
*/
export const getTimesheetProject = (timesheetProjectId: string | number): AxiosPromise<TimesheetProjectVO> => {
return request({
url: '/oa/erp/timesheetProject/' + timesheetProjectId,
method: 'get'
});
};
/**
*
* @param data
*/
export const addTimesheetProject = (data: TimesheetProjectForm) => {
return request({
url: '/oa/erp/timesheetProject',
method: 'post',
data: data
});
};
/**
*
* @param data
*/
export const updateTimesheetProject = (data: TimesheetProjectForm) => {
return request({
url: '/oa/erp/timesheetProject',
method: 'put',
data: data
});
};
/**
*
* @param timesheetProjectId
*/
export const delTimesheetProject = (timesheetProjectId: string | number | Array<string | number>) => {
return request({
url: '/oa/erp/timesheetProject/' + timesheetProjectId,
method: 'delete'
});
};
/**
*
* @param query
* @returns {*}
*/
export function getErpTimesheetProjectList (query) {
return request({
url: '/oa/erp/timesheetProject/getErpTimesheetProjectList',
method: 'get',
params: query
});
};

@ -0,0 +1,146 @@
export interface TimesheetProjectVO {
/**
* ID
*/
timesheetProjectId: string | number;
/**
* ID
*/
timesheetId: string | number;
/**
*
*/
sortOrder: number;
/**
* ID
*/
projectId: string | number;
/**
*
*/
projectCode: string;
/**
*
*/
projectName: string;
/**
* ID
*/
projectManagerId: string | number;
/**
* ID
*/
deptId: string | number;
/**
*
*/
hours: number;
}
export interface TimesheetProjectForm extends BaseEntity {
/**
* ID
*/
timesheetProjectId?: string | number;
/**
* ID
*/
timesheetId?: string | number;
/**
*
*/
sortOrder?: number;
/**
* ID
*/
projectId?: string | number;
/**
*
*/
projectCode?: string;
/**
*
*/
projectName?: string;
/**
* ID
*/
projectManagerId?: string | number;
/**
* ID
*/
deptId?: string | number;
/**
*
*/
hours?: number;
}
export interface TimesheetProjectQuery extends PageQuery {
/**
* ID
*/
timesheetId?: string | number;
/**
*
*/
sortOrder?: number;
/**
* ID
*/
projectId?: string | number;
/**
*
*/
projectCode?: string;
/**
*
*/
projectName?: string;
/**
* ID
*/
projectManagerId?: string | number;
/**
* ID
*/
deptId?: string | number;
/**
*
*/
hours?: number;
/**
*
*/
params?: any;
}

@ -22,6 +22,11 @@ export enum CodeRuleEnum {
*/
AFTER_SALES = '1012',
/**
*
*/
TIMESHEET = '1015',
PROJECT_REPORT = '1013'
}
@ -104,5 +109,9 @@ export enum FlowCodeEnum {
/**
* KEY
*/
AFTER_SALES_KEY = 'OAAS'
AFTER_SALES_KEY = 'OAAS',
/**
* KEY
*/
TIMESHEET_KEY = 'OATS'
}

@ -17,7 +17,6 @@
<!-- 主表信息卡片 -->
<el-card shadow="never">
<template #header>
<!-- 标题根据状态动态显示或者固定为添加项目售后 -->
<span class="card-title">{{ form.afterSalesId ? '修改项目售后' : '添加项目售后' }}</span>
</template>
@ -190,7 +189,7 @@
</el-row>
</el-card>
<!-- 子表 A人员费用 -->
<!-- 子表 人员费用 -->
<el-card shadow="never" class="mt-2">
<template #header>
<el-row :gutter="10">
@ -207,7 +206,7 @@
</template>
<el-table :data="form.laborCostsList" border show-summary :summary-method="getLaborSummaries" @selection-change="handleLaborSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="#" type="index" width="55" align="center" />
<el-table-column label="序号" type="index" width="55" align="center" />
<el-table-column label="人员姓名" prop="laborName">
<template #default="scope">
<el-select
@ -276,7 +275,6 @@
<el-table-column label="产品名称" prop="productName" min-width="150" show-overflow-tooltip />
<el-table-column label="规格型号" prop="specificationModel" width="120" show-overflow-tooltip />
<!-- 单位调用函数翻译 -->
<el-table-column label="单位" prop="unitId" width="80" align="center">
<template #default="scope">
{{ getUnitName(scope.row.unitId) }}
@ -286,7 +284,6 @@
<el-table-column label="数量" prop="quantity" width="100" align="center" />
<el-table-column label="税率%" prop="taxRate" width="80" align="center" />
<!-- 金额类格式化显示 -->
<el-table-column label="含税单价" prop="taxInclusivePrice" width="120" align="right">
<template #default="scope"> {{ Number(scope.row.taxInclusivePrice).toFixed(2) }}</template>
</el-table-column>
@ -359,7 +356,6 @@
</el-form-item>
</el-col>
<el-col :span="8">
<!-- 需求赠品 (数据来源 gift_flag 字典) -->
<el-form-item label="赠品" prop="giftFlag">
<el-select v-model="materialForm.giftFlag" placeholder="是否赠品" style="width: 100%">
<el-option v-for="dict in gift_flag" :key="dict.value" :label="dict.label" :value="dict.value" />

@ -106,7 +106,7 @@
<el-tag v-if="budgetTotal > 0" type="info" size="small" class="count-tag"> {{ budgetTotal }} </el-tag>
</div>
<div class="header-actions">
<el-button size="default" type="primary" plain icon="Link" @click="openBudgetPage"></el-button>
<el-button size="default" type="primary" plain icon="Link" @click="openBudgetPage"></el-button>
<el-button size="default" icon="Refresh" @click="loadBudgetList"></el-button>
</div>
</div>
@ -162,7 +162,7 @@
<el-tag v-if="planTotal > 0" type="info" size="small" class="count-tag"> {{ planTotal }} </el-tag>
</div>
<div class="header-actions">
<el-button size="default" type="primary" plain icon="Link" @click="openPlanPage"></el-button>
<el-button size="default" type="primary" plain icon="Link" @click="openPlanPage"></el-button>
<el-button size="default" icon="Refresh" @click="loadPlanList"></el-button>
</div>
</div>
@ -212,7 +212,7 @@
<el-tag v-if="changeTotal > 0" type="info" size="small" class="count-tag"> {{ changeTotal }} </el-tag>
</div>
<div class="header-actions">
<el-button size="default" type="primary" plain icon="Link" @click="openChangePage"></el-button>
<el-button size="default" type="primary" plain icon="Link" @click="openChangePage"></el-button>
<el-button size="default" icon="Refresh" @click="loadChangeList"></el-button>
</div>
</div>

@ -0,0 +1,577 @@
<template>
<div class="p-2">
<!-- 1. 顶部操作栏 -->
<el-card shadow="never" style="margin-top: 0" class="mb-2">
<approvalButton
@submitForm="submitForm"
@approvalVerifyOpen="approvalVerifyOpen"
@handleApprovalRecord="handleApprovalRecord"
:buttonLoading="buttonLoading"
:id="form.timesheetId"
:status="form.flowStatus"
:pageType="routeParams.type"
:mode="false"
/>
</el-card>
<!-- 2. 主表单 -->
<el-form
ref="timesheetFormRef"
:model="form"
:rules="rules"
label-width="120px"
:disabled="routeParams.type === 'view' || routeParams.type === 'approval'"
>
<el-card shadow="never" class="mb-2">
<template #header>
<span class="card-title">{{ form.timesheetId ? '修改工时填报' : '添加工时填报' }}</span>
</template>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="工时单号" prop="timesheetCode">
<el-input v-model="form.timesheetCode" placeholder="自动生成工时单号" disabled>
<template #append>
<el-button
type="primary"
@click="generateCode"
:disabled="isCodeGenerated || routeParams.type === 'view' || routeParams.type === 'approval'"
>
生成编号
</el-button>
</template>
</el-input>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="人员" prop="userId">
<!-- 显示当前登录人名字 -->
<el-input :model-value="userStore.nickname" disabled />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="起始日期" prop="startTime">
<el-date-picker
v-model="form.startTime"
type="date"
value-format="YYYY-MM-DD"
placeholder="本周一"
style="width: 100%"
@change="handleStartTimeChange"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="结束日期" prop="endTime">
<el-date-picker v-model="form.endTime" type="date" value-format="YYYY-MM-DD" placeholder="本周日" style="width: 100%" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="部门工时" prop="deptHours">
<el-input-number v-model="form.deptHours" :min="0" :precision="1" disabled style="width: 100%" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="项目工时" prop="projectHours">
<el-input-number v-model="form.projectHours" :min="0" :precision="1" disabled style="width: 100%" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="总工时" prop="totalHours">
<el-input-number v-model="form.totalHours" :min="0" :precision="1" disabled style="width: 100%" />
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="备注" prop="remark">
<el-input v-model="form.remark" type="textarea" :rows="2" placeholder="请输入备注" />
</el-form-item>
</el-col>
</el-row>
</el-card>
</el-form>
<!-- 3. 子表 A部门工作 -->
<el-card shadow="never" class="mt-2">
<template #header>
<el-row :gutter="10">
<el-col :span="1.5"><span class="card-title">部门工作</span></el-col>
<el-col :span="1.5" v-if="!isReadOnly">
<el-button type="primary" plain icon="Plus" size="small" @click="handleAddDeptRow"></el-button>
</el-col>
<el-col :span="1.5" v-if="!isReadOnly">
<el-button type="danger" plain icon="Delete" size="small" @click="handleDeleteDeptRow"></el-button>
</el-col>
</el-row>
</template>
<el-table
:data="form.timesheetDeptList"
border
show-summary
:summary-method="getSummary"
@selection-change="(selection) => (checkedDept = selection)"
>
<el-table-column type="selection" width="50" align="center" />
<el-table-column label="序号" type="index" width="55" align="center" />
<el-table-column label="部门工作描述" prop="workDescription" align="center">
<template #default="scope">
<el-input v-model="scope.row.workDescription" placeholder="请输入工作内容" :disabled="isReadOnly" />
</template>
</el-table-column>
<el-table-column label="部门" prop="deptId" width="200" align="center">
<template #default="scope">
<el-select
v-model="scope.row.deptId"
placeholder="请选择部门"
filterable
clearable
style="width: 100%"
:disabled="isReadOnly"
@change="(val) => handleDeptChange(val, scope.row)"
>
<el-option v-for="item in deptOptions" :key="item.deptId" :label="item.deptName" :value="item.deptId" />
</el-select>
</template>
</el-table-column>
<el-table-column label="部门直线经理" prop="deptManagerId" width="150" align="center">
<template #default="scope">
<el-input :model-value="getUserName(scope.row.deptManagerId)" placeholder="自动关联" disabled />
</template>
</el-table-column>
<el-table-column label="工时(H)" prop="hours" width="150" align="center">
<template #default="scope">
<el-input-number v-model="scope.row.hours" :min="0" :precision="1" :step="0.5" style="width: 100%" :disabled="isReadOnly" />
</template>
</el-table-column>
</el-table>
</el-card>
<!-- 4. 子表 B项目工作 -->
<el-card shadow="never" class="mt-2">
<template #header>
<el-row :gutter="10">
<el-col :span="1.5"><span class="card-title">项目工作</span></el-col>
<el-col :span="1.5" v-if="!isReadOnly">
<el-button type="primary" plain icon="Plus" size="small" @click="handleAddProjectRow"></el-button>
</el-col>
<el-col :span="1.5" v-if="!isReadOnly">
<el-button type="danger" plain icon="Delete" size="small" @click="handleDeleteProjectRow"></el-button>
</el-col>
</el-row>
</template>
<el-table
:data="form.timesheetProjectList"
border
show-summary
:summary-method="getSummary"
@selection-change="(selection) => (checkedProject = selection)"
>
<el-table-column type="selection" width="50" align="center" />
<el-table-column label="序号" type="index" width="55" align="center" />
<!-- 项目名称 -->
<el-table-column label="项目名称" prop="projectId" min-width="250" align="center">
<template #default="scope">
<el-select
v-model="scope.row.projectId"
placeholder="请选择项目"
filterable
clearable
style="width: 100%"
:disabled="isReadOnly"
@change="(val) => handleProjectWorkChange(val, scope.row)"
>
<el-option v-for="item in projectOptions" :key="item.projectId" :label="item.projectName" :value="item.projectId" />
</el-select>
</template>
</el-table-column>
<el-table-column label="项目号" prop="projectCode" width="150" align="center">
<template #default="scope">
<el-input v-model="scope.row.projectCode" disabled placeholder="自动关联" />
</template>
</el-table-column>
<el-table-column label="部门" prop="deptId" width="150" align="center">
<template #default="scope">
<el-input :model-value="getDeptName(scope.row.deptId)" disabled placeholder="自动关联" />
</template>
</el-table-column>
<el-table-column label="项目经理" prop="projectManagerId" width="120" align="center">
<template #default="scope">
<el-input :model-value="getUserName(scope.row.projectManagerId)" disabled placeholder="自动关联" />
</template>
</el-table-column>
<el-table-column label="工时(H)" prop="hours" width="150" align="center">
<template #default="scope">
<el-input-number v-model="scope.row.hours" :min="0" :precision="1" :step="0.5" style="width: 100%" :disabled="isReadOnly" />
</template>
</el-table-column>
</el-table>
</el-card>
<submitVerify ref="submitVerifyRef" :task-variables="taskVariables" @submit-callback="submitCallback" />
<approvalRecord ref="approvalRecordRef" />
</div>
</template>
<script setup name="TimesheetEdit" lang="ts">
import { ref, reactive, toRefs, onMounted, getCurrentInstance, watch, computed, nextTick } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { ElMessage } from 'element-plus';
// API
import { getTimesheetInfo, addTimesheetInfo, updateTimesheetInfo, submitTimesheetAndFlowStart } from '@/api/oa/erp/timesheetInfo';
import { listProjectInfo } from '@/api/oa/erp/projectInfo';
import { getRuleGenerateCode } from '@/api/system/codeRule';
// Components & Store & Enums
import SubmitVerify from '@/components/Process/submitVerify.vue';
import ApprovalRecord from '@/components/Process/approvalRecord.vue';
import ApprovalButton from '@/components/Process/approvalButton.vue';
import { useUserStore } from '@/store/modules/user';
import { CodeRuleEnum, FlowCodeEnum } from '@/enums/OAEnum';
import { getInfo } from '@/api/login';
import { listDept } from '@/api/system/dept';
import { listUser } from '@/api/system/user';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const route = useRoute();
const router = useRouter();
const userStore = useUserStore();
// Vars
const routeParams = ref<any>({});
const buttonLoading = ref(false);
const timesheetFormRef = ref<ElFormInstance>();
const isCodeGenerated = ref(false);
const submitVerifyRef = ref<InstanceType<typeof SubmitVerify>>();
const approvalRecordRef = ref<InstanceType<typeof ApprovalRecord>>();
const taskVariables = ref<Record<string, any>>({});
// Data Source
const projectOptions = ref<any[]>([]);
const userList = ref<any[]>([]);
const checkedDept = ref<any[]>([]);
const checkedProject = ref<any[]>([]);
const deptOptions = ref<any[]>([]);
const data = reactive({
form: {
timesheetId: undefined,
timesheetCode: undefined,
userId: undefined,
deptId: undefined,
chargeId: undefined,
startTime: undefined,
endTime: undefined,
totalHours: 0,
deptHours: 0,
projectHours: 0,
timesheetStatus: '0',
flowStatus: 'draft',
remark: undefined,
//
timesheetDeptList: [] as any[],
timesheetProjectList: [] as any[]
},
rules: {
timesheetCode: [{ required: true, message: '编号不能为空', trigger: 'blur' }],
startTime: [{ required: true, message: '请选择起始日期', trigger: 'change' }],
endTime: [{ required: true, message: '请选择结束日期', trigger: 'change' }]
}
});
const { form, rules } = toRefs(data);
const isReadOnly = computed(() => routeParams.value.type === 'view' || routeParams.value.type === 'approval');
/** 初始化 */
onMounted(async () => {
nextTick(async () => {
routeParams.value = route.query;
const id = routeParams.value.id;
//
loadBaseOptions();
if (id && ['update', 'view', 'approval'].includes(routeParams.value.type)) {
const res: any = await getTimesheetInfo(id);
form.value = res.data;
if (!form.value.timesheetDeptList) form.value.timesheetDeptList = [];
if (!form.value.timesheetProjectList) form.value.timesheetProjectList = [];
if (form.value.timesheetCode) isCodeGenerated.value = true;
} else {
setWeekDates();
form.value.timesheetDeptList = [];
form.value.timesheetProjectList = [];
//
try {
const infoRes = await getInfo();
const user = infoRes.data.user; //
if (user) {
form.value.userId = user.userId;
form.value.deptId = user.deptId;
form.value.chargeId = user.chargeId;
}
} catch (error) {
console.error('获取用户信息失败', error);
}
}
});
});
/** 辅助:设置本周日期 */
const setWeekDates = () => {
const now = new Date();
const day = now.getDay() || 7; // 07
const monday = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1 - day);
const sunday = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 7 - day);
form.value.startTime = proxy.parseTime(monday, '{y}-{m}-{d}');
form.value.endTime = proxy.parseTime(sunday, '{y}-{m}-{d}');
};
/**
* 起始日期改变联动
*/
const handleStartTimeChange = (val: any) => {
if (!val) {
form.value.endTime = undefined;
return;
}
const startDate = new Date(val.replace(/-/g, '/'));
const dayOfWeek = startDate.getDay() || 7;
const daysUntilSunday = 7 - dayOfWeek;
const endDate = new Date(startDate);
endDate.setDate(startDate.getDate() + daysUntilSunday);
form.value.endTime = proxy.parseTime(endDate, '{y}-{m}-{d}');
};
//
function loadBaseOptions() {
listProjectInfo({ pageNum: 1, pageSize: 1000 }).then((res: any) => {
projectOptions.value = res.rows;
});
listUser({ pageNum: 1, pageSize: 1000 }).then((res: any) => {
userList.value = res.rows;
});
listDept({ pageNum: 1, pageSize: 1000 }).then((res: any) => {
deptOptions.value = res.data;
});
}
/**
* 部门选择联动
*/
const handleDeptChange = (val: any, row: any) => {
if (!val) {
row.deptManagerId = undefined;
return;
}
const dept = deptOptions.value.find((d: any) => d.deptId === val);
if (dept && dept.leader) {
row.deptManagerId = dept.leader;
} else {
row.deptManagerId = undefined;
}
};
/**
* 项目选择联动
*/
const handleProjectWorkChange = (val: any, row: any) => {
if (!val) {
row.projectCode = '';
row.deptId = undefined;
row.projectManagerId = undefined;
return;
}
//
const project = projectOptions.value.find((p: any) => p.projectId === val);
if (project) {
//
row.projectName = project.projectName;
row.projectCode = project.projectCode;
row.deptId = project.deptId;
row.projectManagerId = project.managerId;
}
};
/**
* 根据 UserID 获取用户昵称
*/
const getUserName = (userId: any) => {
if (!userId) return '';
const user = userList.value.find((u: any) => u.userId == userId);
return user ? user.nickName : '';
};
/**
* 根据 DeptID 获取部门名称
*/
const getDeptName = (deptId: any) => {
if (!deptId) return '';
const dept = deptOptions.value.find((d: any) => d.deptId == deptId);
return dept ? dept.deptName : '';
};
/** 生成编号 */
const generateCode = async () => {
if (isCodeGenerated.value) return;
try {
const res = await getRuleGenerateCode({ codeRuleCode: CodeRuleEnum.TIMESHEET } as any);
if (res.code === 200) {
form.value.timesheetCode = res.msg;
isCodeGenerated.value = true;
proxy?.$modal.msgSuccess('生成成功');
}
} catch (error) {
console.error(error);
}
};
/** 自动计算工时 */
const calculateHours = () => {
const deptList = form.value.timesheetDeptList || [];
const projectList = form.value.timesheetProjectList || [];
const deptTotal = deptList.reduce((sum: number, item: any) => sum + (Number(item.hours) || 0), 0);
const projectTotal = projectList.reduce((sum: number, item: any) => sum + (Number(item.hours) || 0), 0);
form.value.deptHours = deptTotal;
form.value.projectHours = projectTotal;
form.value.totalHours = deptTotal + projectTotal;
};
watch([() => form.value.timesheetDeptList, () => form.value.timesheetProjectList], () => calculateHours(), { deep: true });
/** 子表逻辑 */
const handleAddDeptRow = () => {
form.value.timesheetDeptList.push({ workDescription: '', hours: 0 });
};
const handleDeleteDeptRow = () => {
if (!checkedDept.value.length) return proxy.$modal.msgError('请选择数据');
form.value.timesheetDeptList = form.value.timesheetDeptList.filter((item: any) => !checkedDept.value.includes(item));
};
const handleAddProjectRow = () => {
form.value.timesheetProjectList.push({ projectId: undefined, hours: 0 });
};
const handleDeleteProjectRow = () => {
if (!checkedProject.value.length) return proxy.$modal.msgError('请选择数据');
form.value.timesheetProjectList = form.value.timesheetProjectList.filter((item: any) => !checkedProject.value.includes(item));
};
//
const handleProjectChange = (val: any, row: any) => {
const proj = projectOptions.value.find((p: any) => p.projectId === val);
if (proj) {
row.projectName = proj.projectName;
row.projectCode = proj.projectCode;
}
};
//
const getSummary = (param: any) => {
const { columns, data } = param;
const sums: string[] = [];
columns.forEach((column: any, index: number) => {
if (index === 0) return;
if (index === 1) {
sums[index] = '合计';
return;
}
if (column.property === 'hours') {
const values = data.map((item: any) => Number(item[column.property]));
const total = values.reduce((prev: any, curr: any) => (!Number.isNaN(curr) ? prev + curr : prev), 0);
sums[index] = total.toFixed(1) + ' H';
}
});
return sums;
};
/** 提交按钮 */
const submitForm = (status: string) => {
timesheetFormRef.value?.validate(async (valid: boolean) => {
if (valid) {
buttonLoading.value = true;
try {
const submitData: any = { ...form.value };
if (status !== 'draft') {
//
submitData.flowCode = FlowCodeEnum.TIMESHEET_KEY;
submitData.variables = {
startUserId: userStore.userId,
totalHours: submitData.totalHours
};
submitData.bizExt = {
businessTitle: `工时填报:${userStore.nickname} (${submitData.startTime})`,
businessCode: submitData.timesheetCode
};
submitData.timesheetStatus = '2'; //
submitData.flowStatus = 'waiting';
const res = await submitTimesheetAndFlowStart(submitData);
if (res?.data) form.value = res.data;
proxy.$modal.msgSuccess('提交成功');
} else {
//
submitData.timesheetStatus = '1'; // 稿
submitData.flowStatus = 'draft';
if (submitData.timesheetId) await updateTimesheetInfo(submitData);
else await addTimesheetInfo(submitData);
proxy.$modal.msgSuccess('暂存成功');
}
proxy.$tab.closePage(route);
router.go(-1);
} catch (e) {
console.error(e);
} finally {
buttonLoading.value = false;
}
}
});
};
const handleApprovalRecord = () => {
if (form.value.timesheetId) approvalRecordRef.value?.init(form.value.timesheetId);
};
const approvalVerifyOpen = async () => {
if (submitVerifyRef.value) await submitVerifyRef.value.openDialog(routeParams.value.taskId);
};
const submitCallback = async () => {
await proxy.$tab.closePage(route);
router.go(-1);
};
</script>
<style scoped lang="scss">
.card-title {
font-weight: bold;
font-size: 16px;
color: #303133;
}
.mt-2 {
margin-top: 10px;
}
:deep(.el-table__footer-wrapper tbody td.el-table__cell) {
background-color: #ffffff !important;
font-weight: 700;
}
</style>

@ -0,0 +1,356 @@
<template>
<div class="p-2">
<transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
<div v-show="showSearch" class="mb-[10px]">
<el-card shadow="hover">
<el-form ref="queryFormRef" :model="queryParams" :inline="true" label-width="100px">
<el-form-item label="工时填报编号" prop="timesheetCode">
<el-input v-model="queryParams.timesheetCode" placeholder="请输入工时填报编号" clearable @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="起始时间" prop="startTime">
<el-date-picker clearable v-model="queryParams.startTime" type="date" value-format="YYYY-MM-DD" placeholder="请选择起始时间" />
</el-form-item>
<el-form-item label="结束时间" prop="endTime">
<el-date-picker clearable v-model="queryParams.endTime" type="date" value-format="YYYY-MM-DD" placeholder="请选择结束时间" />
</el-form-item>
<el-form-item label="工时填报状态" prop="timesheetStatus">
<el-select v-model="queryParams.timesheetStatus" placeholder="请选择工时填报状态" clearable>
<el-option v-for="dict in timesheet_status" :key="dict.value" :label="dict.label" :value="dict.value" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery"></el-button>
<el-button icon="Refresh" @click="resetQuery"></el-button>
</el-form-item>
</el-form>
</el-card>
</div>
</transition>
<el-card shadow="never">
<template #header>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button type="primary" plain icon="Plus" @click="handleAdd" v-hasPermi="['oa/erp:timesheetInfo:add']"></el-button>
</el-col>
<el-col :span="1.5">
<el-button type="success" plain icon="Edit" :disabled="single" @click="handleUpdate()" v-hasPermi="['oa/erp:timesheetInfo:edit']"
>修改</el-button
>
</el-col>
<el-col :span="1.5">
<el-button type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete()" v-hasPermi="['oa/erp:timesheetInfo:remove']"
>删除</el-button
>
</el-col>
<el-col :span="1.5">
<el-button type="warning" plain icon="Download" @click="handleExport" v-hasPermi="['oa/erp:timesheetInfo:export']"></el-button>
</el-col>
<right-toolbar v-model:showSearch="showSearch" :columns="columns" :search="true" @queryTable="getList"></right-toolbar>
</el-row>
</template>
<el-table v-loading="loading" border :data="timesheetInfoList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="工时填报ID" align="center" prop="timesheetId" v-if="columns[0].visible" />
<el-table-column label="工时填报编号" align="center" prop="timesheetCode" v-if="columns[2].visible" />
<el-table-column label="人员ID" align="center" prop="userId" v-if="columns[3].visible" />
<el-table-column label="部门ID" align="center" prop="deptId" v-if="columns[4].visible" />
<el-table-column label="起始时间" align="center" prop="startTime" width="180" v-if="columns[5].visible">
<template #default="scope">
<span>{{ parseTime(scope.row.startTime, '{y}-{m}-{d}') }}</span>
</template>
</el-table-column>
<el-table-column label="结束时间" align="center" prop="endTime" width="180" v-if="columns[6].visible">
<template #default="scope">
<span>{{ parseTime(scope.row.endTime, '{y}-{m}-{d}') }}</span>
</template>
</el-table-column>
<el-table-column label="总工时" align="center" prop="totalHours" v-if="columns[7].visible" />
<el-table-column label="部门工时" align="center" prop="deptHours" v-if="columns[8].visible" />
<el-table-column label="项目工时" align="center" prop="projectHours" v-if="columns[9].visible" />
<el-table-column label="工时填报状态" align="center" prop="timesheetStatus" v-if="columns[10].visible">
<template #default="scope">
<dict-tag :options="timesheet_status" :value="scope.row.timesheetStatus" />
</template>
</el-table-column>
<el-table-column label="流程状态" align="center" prop="flowStatus" v-if="columns[11].visible">
<template #default="scope">
<dict-tag :options="flow_status" :value="scope.row.flowStatus" />
</template>
</el-table-column>
<el-table-column label="备注" align="center" prop="remark" v-if="columns[12].visible" />
<el-table-column label="操作" align="center" fixed="right" class-name="small-padding fixed-width">
<template #default="scope">
<el-tooltip content="修改" placement="top">
<el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['oa/erp:timesheetInfo:edit']"></el-button>
</el-tooltip>
<el-tooltip content="删除" placement="top">
<el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['oa/erp:timesheetInfo:remove']"></el-button>
</el-tooltip>
</template>
</el-table-column>
</el-table>
<pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
</el-card>
<!-- 添加或修改工时填报对话框 -->
<el-dialog :title="dialog.title" v-model="dialog.visible" width="500px" append-to-body>
<el-form ref="timesheetInfoFormRef" :model="form" :rules="rules" label-width="120px">
<el-form-item label="工时填报编号" prop="timesheetCode">
<el-input v-model="form.timesheetCode" placeholder="请输入工时填报编号" />
</el-form-item>
<el-form-item label="人员ID" prop="userId">
<el-input v-model="form.userId" placeholder="请输入人员ID" />
</el-form-item>
<el-form-item label="部门ID" prop="deptId">
<el-input v-model="form.deptId" placeholder="请输入部门ID" />
</el-form-item>
<el-form-item label="起始时间" prop="startTime">
<el-date-picker clearable v-model="form.startTime" type="datetime" value-format="YYYY-MM-DD HH:mm:ss" placeholder="请选择起始时间">
</el-date-picker>
</el-form-item>
<el-form-item label="结束时间" prop="endTime">
<el-date-picker clearable v-model="form.endTime" type="datetime" value-format="YYYY-MM-DD HH:mm:ss" placeholder="请选择结束时间">
</el-date-picker>
</el-form-item>
<el-form-item label="总工时" prop="totalHours">
<el-input v-model="form.totalHours" placeholder="请输入总工时" />
</el-form-item>
<el-form-item label="部门工时" prop="deptHours">
<el-input v-model="form.deptHours" placeholder="请输入部门工时" />
</el-form-item>
<el-form-item label="项目工时" prop="projectHours">
<el-input v-model="form.projectHours" placeholder="请输入项目工时" />
</el-form-item>
<el-form-item label="工时填报状态" prop="timesheetStatus">
<el-radio-group v-model="form.timesheetStatus">
<el-radio v-for="dict in timesheet_status" :key="dict.value" :value="dict.value">{{ dict.label }}</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="流程状态" prop="flowStatus">
<el-radio-group v-model="form.flowStatus">
<el-radio v-for="dict in flow_status" :key="dict.value" :value="dict.value">{{ dict.label }}</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="form.remark" type="textarea" placeholder="请输入内容" />
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button :loading="buttonLoading" type="primary" @click="submitForm"> </el-button>
<el-button @click="cancel"> </el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup name="TimesheetInfo" lang="ts">
import { listTimesheetInfo, getTimesheetInfo, delTimesheetInfo, addTimesheetInfo, updateTimesheetInfo } from '@/api/oa/erp/timesheetInfo';
import { TimesheetInfoVO, TimesheetInfoQuery, TimesheetInfoForm } from '@/api/oa/erp/timesheetInfo/types';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const route = useRoute();
const router = useRouter();
const { flow_status, timesheet_status } = toRefs<any>(proxy?.useDict('flow_status', 'timesheet_status'));
const timesheetInfoList = ref<TimesheetInfoVO[]>([]);
const buttonLoading = ref(false);
const loading = ref(true);
const showSearch = ref(true);
const ids = ref<Array<string | number>>([]);
const single = ref(true);
const multiple = ref(true);
const total = ref(0);
const queryFormRef = ref<ElFormInstance>();
const timesheetInfoFormRef = ref<ElFormInstance>();
const dialog = reactive<DialogOption>({
visible: false,
title: ''
});
//
const columns = ref<FieldOption[]>([
{ key: 0, label: `工时填报ID`, visible: false },
{ key: 1, label: `租户编号`, visible: false },
{ key: 2, label: `工时填报编号`, visible: true },
{ key: 3, label: `人员ID`, visible: false },
{ key: 4, label: `部门ID`, visible: false },
{ key: 5, label: `起始时间`, visible: true },
{ key: 6, label: `结束时间`, visible: true },
{ key: 7, label: `总工时`, visible: true },
{ key: 8, label: `部门工时`, visible: true },
{ key: 9, label: `项目工时`, visible: true },
{ key: 10, label: `工时填报状态`, visible: true },
{ key: 11, label: `流程状态`, visible: true },
{ key: 12, label: `备注`, visible: true },
{ key: 13, label: `删除标志`, visible: true },
{ key: 14, label: `创建部门`, visible: true },
{ key: 15, label: `创建者`, visible: true },
{ key: 16, label: `创建时间`, visible: true },
{ key: 17, label: `更新者`, visible: true },
{ key: 18, label: `更新时间`, visible: true }
]);
const initFormData: TimesheetInfoForm = {
timesheetId: undefined,
timesheetCode: undefined,
userId: undefined,
deptId: undefined,
startTime: undefined,
endTime: undefined,
totalHours: undefined,
deptHours: undefined,
projectHours: undefined,
timesheetStatus: undefined,
flowStatus: undefined,
remark: undefined
};
const data = reactive<PageData<TimesheetInfoForm, TimesheetInfoQuery>>({
form: { ...initFormData },
queryParams: {
pageNum: 1,
pageSize: 10,
timesheetCode: undefined,
userId: undefined,
deptId: undefined,
startTime: undefined,
endTime: undefined,
totalHours: undefined,
deptHours: undefined,
projectHours: undefined,
timesheetStatus: undefined,
flowStatus: undefined,
params: {}
},
rules: {
timesheetId: [{ required: true, message: '工时填报ID不能为空', trigger: 'blur' }],
timesheetCode: [{ required: true, message: '工时填报编号不能为空', trigger: 'blur' }],
userId: [{ required: true, message: '人员ID不能为空', trigger: 'blur' }],
startTime: [{ required: true, message: '起始时间不能为空', trigger: 'blur' }],
endTime: [{ required: true, message: '结束时间不能为空', trigger: 'blur' }]
}
});
const { queryParams, form, rules } = toRefs(data);
/** 查询工时填报列表 */
const getList = async () => {
loading.value = true;
const res = await listTimesheetInfo(queryParams.value);
timesheetInfoList.value = res.rows;
total.value = res.total;
loading.value = false;
};
/** 取消按钮 */
const cancel = () => {
reset();
dialog.visible = false;
};
/** 表单重置 */
const reset = () => {
form.value = { ...initFormData };
timesheetInfoFormRef.value?.resetFields();
};
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.value.pageNum = 1;
getList();
};
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value?.resetFields();
handleQuery();
};
/** 多选框选中数据 */
const handleSelectionChange = (selection: TimesheetInfoVO[]) => {
ids.value = selection.map((item) => item.timesheetId);
single.value = selection.length != 1;
multiple.value = !selection.length;
};
/** 新增按钮操作 */
const handleAdd = () => {
proxy.$tab.closePage(route);
router.push({
path: '/timesheet/timesheetInfo/edit',
query: { type: 'add' }
});
};
/** 修改按钮操作 */
const handleUpdate = async (row?: TimesheetInfoVO) => {
const _timesheetId = row?.timesheetId || ids.value[0];
proxy.$tab.closePage(route);
router.push({
path: '/timesheet/timesheetInfo/edit',
query: {
id: _timesheetId,
type: 'update'
}
});
};
/** 查看按钮操作 */
const handleView = async (row?: TimesheetInfoVO) => {
const _timesheetId = row?.timesheetId;
proxy.$tab.closePage(route);
router.push({
path: '/timesheet/timesheetInfo/edit',
query: {
id: _timesheetId,
type: 'view'
}
});
};
/** 提交按钮 */
const submitForm = () => {
timesheetInfoFormRef.value?.validate(async (valid: boolean) => {
if (valid) {
buttonLoading.value = true;
if (form.value.timesheetId) {
await updateTimesheetInfo(form.value).finally(() => (buttonLoading.value = false));
} else {
await addTimesheetInfo(form.value).finally(() => (buttonLoading.value = false));
}
proxy?.$modal.msgSuccess('操作成功');
dialog.visible = false;
await getList();
}
});
};
/** 删除按钮操作 */
const handleDelete = async (row?: TimesheetInfoVO) => {
const _timesheetIds = row?.timesheetId || ids.value;
await proxy?.$modal.confirm('是否确认删除工时填报编号为"' + _timesheetIds + '"的数据项?').finally(() => (loading.value = false));
await delTimesheetInfo(_timesheetIds);
proxy?.$modal.msgSuccess('删除成功');
await getList();
};
/** 导出按钮操作 */
const handleExport = () => {
proxy?.download(
'oa/erp/timesheetInfo/export',
{
...queryParams.value
},
`timesheetInfo_${new Date().getTime()}.xlsx`
);
};
onMounted(() => {
getList();
});
</script>
Loading…
Cancel
Save