fix(加班申请): 去掉撤销相关的状态和动作。
feat(工作报告): 开发工作报告功能
This commit is contained in:
@@ -131,16 +131,23 @@ export function setupElegantRouter() {
|
||||
order: 1,
|
||||
keepAlive: true
|
||||
},
|
||||
'personal-center_my-weekly': {
|
||||
icon: 'mdi:calendar-week-outline',
|
||||
order: 2,
|
||||
keepAlive: true
|
||||
},
|
||||
'personal-center_my-monthly': {
|
||||
icon: 'mdi:calendar-month-outline',
|
||||
'personal-center_work-report': {
|
||||
icon: 'mdi:file-chart-outline',
|
||||
order: 3,
|
||||
keepAlive: true
|
||||
},
|
||||
'personal-center_work-report_weekly': {
|
||||
hideInMenu: true,
|
||||
activeMenu: 'personal-center_work-report'
|
||||
},
|
||||
'personal-center_work-report_monthly': {
|
||||
hideInMenu: true,
|
||||
activeMenu: 'personal-center_work-report'
|
||||
},
|
||||
'personal-center_work-report_project': {
|
||||
hideInMenu: true,
|
||||
activeMenu: 'personal-center_work-report'
|
||||
},
|
||||
'personal-center_my-performance': {
|
||||
icon: 'mdi:trophy-outline',
|
||||
order: 4,
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"pageComponentPattern": "view.<routeName>",
|
||||
"singlePageComponentPattern": "layout.<layoutName>$view.<routeName>"
|
||||
},
|
||||
"total": 23,
|
||||
"total": 22,
|
||||
"items": [
|
||||
{
|
||||
"name": "product_list",
|
||||
@@ -306,15 +306,15 @@
|
||||
"source": "generated"
|
||||
},
|
||||
{
|
||||
"name": "personal-center_my-weekly",
|
||||
"path": "/personal-center/my-weekly",
|
||||
"component": "view.personal-center_my-weekly",
|
||||
"title": "我的周报",
|
||||
"routeTitle": "personal-center_my-weekly",
|
||||
"i18nKey": "route.personal-center_my-weekly",
|
||||
"icon": "mdi:calendar-week-outline",
|
||||
"name": "personal-center_work-report",
|
||||
"path": "/personal-center/work-report",
|
||||
"component": "view.personal-center_work-report",
|
||||
"title": "工作报告",
|
||||
"routeTitle": "personal-center_work-report",
|
||||
"i18nKey": "route.personal-center_work-report",
|
||||
"icon": "mdi:file-chart-outline",
|
||||
"localIcon": null,
|
||||
"order": 1,
|
||||
"order": 3,
|
||||
"hideInMenu": false,
|
||||
"keepAlive": true,
|
||||
"activeMenu": null,
|
||||
@@ -323,44 +323,11 @@
|
||||
"redirect": null,
|
||||
"props": null,
|
||||
"meta": {
|
||||
"title": "我的周报",
|
||||
"i18nKey": "route.personal-center_my-weekly",
|
||||
"icon": "mdi:calendar-week-outline",
|
||||
"title": "工作报告",
|
||||
"i18nKey": "route.personal-center_work-report",
|
||||
"icon": "mdi:file-chart-outline",
|
||||
"localIcon": null,
|
||||
"order": 1,
|
||||
"keepAlive": true,
|
||||
"hideInMenu": false,
|
||||
"activeMenu": null,
|
||||
"multiTab": false,
|
||||
"fixedIndexInTab": null
|
||||
},
|
||||
"parentName": "personal-center",
|
||||
"pageType": "leaf",
|
||||
"source": "generated"
|
||||
},
|
||||
{
|
||||
"name": "personal-center_my-monthly",
|
||||
"path": "/personal-center/my-monthly",
|
||||
"component": "view.personal-center_my-monthly",
|
||||
"title": "我的月报",
|
||||
"routeTitle": "personal-center_my-monthly",
|
||||
"i18nKey": "route.personal-center_my-monthly",
|
||||
"icon": "mdi:calendar-month-outline",
|
||||
"localIcon": null,
|
||||
"order": 2,
|
||||
"hideInMenu": false,
|
||||
"keepAlive": true,
|
||||
"activeMenu": null,
|
||||
"multiTab": false,
|
||||
"fixedIndexInTab": null,
|
||||
"redirect": null,
|
||||
"props": null,
|
||||
"meta": {
|
||||
"title": "我的月报",
|
||||
"i18nKey": "route.personal-center_my-monthly",
|
||||
"icon": "mdi:calendar-month-outline",
|
||||
"localIcon": null,
|
||||
"order": 2,
|
||||
"order": 3,
|
||||
"keepAlive": true,
|
||||
"hideInMenu": false,
|
||||
"activeMenu": null,
|
||||
@@ -380,7 +347,7 @@
|
||||
"i18nKey": "route.personal-center_my-performance",
|
||||
"icon": "mdi:trophy-outline",
|
||||
"localIcon": null,
|
||||
"order": 3,
|
||||
"order": 4,
|
||||
"hideInMenu": false,
|
||||
"keepAlive": true,
|
||||
"activeMenu": null,
|
||||
@@ -393,7 +360,7 @@
|
||||
"i18nKey": "route.personal-center_my-performance",
|
||||
"icon": "mdi:trophy-outline",
|
||||
"localIcon": null,
|
||||
"order": 3,
|
||||
"order": 4,
|
||||
"keepAlive": true,
|
||||
"hideInMenu": false,
|
||||
"activeMenu": null,
|
||||
@@ -413,7 +380,7 @@
|
||||
"i18nKey": "route.personal-center_my-application",
|
||||
"icon": "mdi:file-document-outline",
|
||||
"localIcon": null,
|
||||
"order": 4,
|
||||
"order": 5,
|
||||
"hideInMenu": false,
|
||||
"keepAlive": true,
|
||||
"activeMenu": null,
|
||||
@@ -426,40 +393,7 @@
|
||||
"i18nKey": "route.personal-center_my-application",
|
||||
"icon": "mdi:file-document-outline",
|
||||
"localIcon": null,
|
||||
"order": 4,
|
||||
"keepAlive": true,
|
||||
"hideInMenu": false,
|
||||
"activeMenu": null,
|
||||
"multiTab": false,
|
||||
"fixedIndexInTab": null
|
||||
},
|
||||
"parentName": "personal-center",
|
||||
"pageType": "leaf",
|
||||
"source": "generated"
|
||||
},
|
||||
{
|
||||
"name": "personal-center_overtime-application",
|
||||
"path": "/personal-center/overtime-application",
|
||||
"component": "view.personal-center_overtime-application",
|
||||
"title": "加班申请",
|
||||
"routeTitle": "personal-center_overtime-application",
|
||||
"i18nKey": "route.personal-center_overtime-application",
|
||||
"icon": "mdi:clock-plus-outline",
|
||||
"localIcon": null,
|
||||
"order": 6,
|
||||
"hideInMenu": false,
|
||||
"keepAlive": true,
|
||||
"activeMenu": null,
|
||||
"multiTab": false,
|
||||
"fixedIndexInTab": null,
|
||||
"redirect": null,
|
||||
"props": null,
|
||||
"meta": {
|
||||
"title": "加班申请",
|
||||
"i18nKey": "route.personal-center_overtime-application",
|
||||
"icon": "mdi:clock-plus-outline",
|
||||
"localIcon": null,
|
||||
"order": 6,
|
||||
"order": 5,
|
||||
"keepAlive": true,
|
||||
"hideInMenu": false,
|
||||
"activeMenu": null,
|
||||
@@ -503,6 +437,39 @@
|
||||
"pageType": "leaf",
|
||||
"source": "generated"
|
||||
},
|
||||
{
|
||||
"name": "personal-center_overtime-application",
|
||||
"path": "/personal-center/overtime-application",
|
||||
"component": "view.personal-center_overtime-application",
|
||||
"title": "加班申请",
|
||||
"routeTitle": "personal-center_overtime-application",
|
||||
"i18nKey": "route.personal-center_overtime-application",
|
||||
"icon": "mdi:clock-plus-outline",
|
||||
"localIcon": null,
|
||||
"order": 6,
|
||||
"hideInMenu": false,
|
||||
"keepAlive": true,
|
||||
"activeMenu": null,
|
||||
"multiTab": false,
|
||||
"fixedIndexInTab": null,
|
||||
"redirect": null,
|
||||
"props": null,
|
||||
"meta": {
|
||||
"title": "加班申请",
|
||||
"i18nKey": "route.personal-center_overtime-application",
|
||||
"icon": "mdi:clock-plus-outline",
|
||||
"localIcon": null,
|
||||
"order": 6,
|
||||
"keepAlive": true,
|
||||
"hideInMenu": false,
|
||||
"activeMenu": null,
|
||||
"multiTab": false,
|
||||
"fixedIndexInTab": null
|
||||
},
|
||||
"parentName": "personal-center",
|
||||
"pageType": "leaf",
|
||||
"source": "generated"
|
||||
},
|
||||
{
|
||||
"name": "system_user",
|
||||
"path": "/system/user",
|
||||
|
||||
2431
pnpm-lock.yaml
generated
2431
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -18,6 +18,12 @@ export interface SearchField {
|
||||
label: string;
|
||||
/** 字段类型 */
|
||||
type: 'input' | 'select' | 'date' | 'dateRange' | 'dict';
|
||||
/** date 字段的日期粒度 */
|
||||
dateType?: 'date' | 'month';
|
||||
/** dateRange 字段的日期范围粒度 */
|
||||
dateRangeType?: 'daterange' | 'monthrange';
|
||||
/** 日期字段提交格式 */
|
||||
valueFormat?: string;
|
||||
/** 占位列数,默认 1 */
|
||||
span?: number;
|
||||
/** select 类型的选项 */
|
||||
@@ -156,23 +162,23 @@ function handleSearch() {
|
||||
<ElDatePicker
|
||||
v-else-if="field.type === 'date'"
|
||||
:model-value="props.modelValue[field.key]"
|
||||
type="date"
|
||||
:type="field.dateType || 'date'"
|
||||
:placeholder="field.placeholder"
|
||||
clearable
|
||||
:disabled="props.disabled"
|
||||
value-format="YYYY-MM-DD"
|
||||
:value-format="field.valueFormat || 'YYYY-MM-DD'"
|
||||
@update:model-value="val => (props.modelValue[field.key] = val)"
|
||||
/>
|
||||
<ElDatePicker
|
||||
v-else-if="field.type === 'dateRange'"
|
||||
:model-value="props.modelValue[field.key]"
|
||||
type="daterange"
|
||||
:type="field.dateRangeType || 'daterange'"
|
||||
:placeholder="field.placeholder"
|
||||
clearable
|
||||
:disabled="props.disabled"
|
||||
value-format="YYYY-MM-DD"
|
||||
start-placeholder="开始日期"
|
||||
end-placeholder="结束日期"
|
||||
:value-format="field.valueFormat || 'YYYY-MM-DD'"
|
||||
:start-placeholder="field.dateRangeType === 'monthrange' ? '开始月份' : '开始日期'"
|
||||
:end-placeholder="field.dateRangeType === 'monthrange' ? '结束月份' : '结束日期'"
|
||||
@update:model-value="val => (props.modelValue[field.key] = val)"
|
||||
/>
|
||||
<DictSelect
|
||||
@@ -253,23 +259,23 @@ function handleSearch() {
|
||||
<ElDatePicker
|
||||
v-else-if="field.type === 'date'"
|
||||
:model-value="props.modelValue[field.key]"
|
||||
type="date"
|
||||
:type="field.dateType || 'date'"
|
||||
:placeholder="field.placeholder"
|
||||
clearable
|
||||
:disabled="props.disabled"
|
||||
value-format="YYYY-MM-DD"
|
||||
:value-format="field.valueFormat || 'YYYY-MM-DD'"
|
||||
@update:model-value="val => (props.modelValue[field.key] = val)"
|
||||
/>
|
||||
<ElDatePicker
|
||||
v-else-if="field.type === 'dateRange'"
|
||||
:model-value="props.modelValue[field.key]"
|
||||
type="daterange"
|
||||
:type="field.dateRangeType || 'daterange'"
|
||||
:placeholder="field.placeholder"
|
||||
clearable
|
||||
:disabled="props.disabled"
|
||||
value-format="YYYY-MM-DD"
|
||||
start-placeholder="开始日期"
|
||||
end-placeholder="结束日期"
|
||||
:value-format="field.valueFormat || 'YYYY-MM-DD'"
|
||||
:start-placeholder="field.dateRangeType === 'monthrange' ? '开始月份' : '开始日期'"
|
||||
:end-placeholder="field.dateRangeType === 'monthrange' ? '结束月份' : '结束日期'"
|
||||
@update:model-value="val => (props.modelValue[field.key] = val)"
|
||||
/>
|
||||
<DictSelect
|
||||
|
||||
@@ -17,6 +17,7 @@ export type StatusDomain =
|
||||
| 'productRequirement'
|
||||
| 'projectRequirement'
|
||||
| 'workOrder'
|
||||
| 'workReport'
|
||||
| 'personalItem'
|
||||
| 'overtimeApplication';
|
||||
|
||||
@@ -80,6 +81,13 @@ const statusTagTypeRegistry: Record<StatusDomain, Record<string, StatusTagType>>
|
||||
},
|
||||
// 工单(待补全)
|
||||
workOrder: {},
|
||||
// 工作报告
|
||||
workReport: {
|
||||
draft: 'info',
|
||||
pending_approval: 'warning',
|
||||
approved: 'success',
|
||||
rejected: 'danger'
|
||||
},
|
||||
// 个人事项
|
||||
personalItem: {
|
||||
pending: 'info',
|
||||
@@ -91,8 +99,7 @@ const statusTagTypeRegistry: Record<StatusDomain, Record<string, StatusTagType>>
|
||||
overtimeApplication: {
|
||||
pending: 'warning',
|
||||
approved: 'success',
|
||||
rejected: 'danger',
|
||||
cancelled: 'info'
|
||||
rejected: 'danger'
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -169,8 +169,10 @@ const local: App.I18n.Schema = {
|
||||
'personal-center': 'Personal Center',
|
||||
'personal-center_my-profile': 'My Profile',
|
||||
'personal-center_my-item': 'My Items',
|
||||
'personal-center_my-weekly': 'My Weekly Report',
|
||||
'personal-center_my-monthly': 'My Monthly Report',
|
||||
'personal-center_work-report': 'Work Report',
|
||||
'personal-center_work-report_weekly': 'Weekly Report',
|
||||
'personal-center_work-report_monthly': 'Monthly Report',
|
||||
'personal-center_work-report_project': 'Project Fortnightly Report',
|
||||
'personal-center_my-performance': 'My Performance',
|
||||
'personal-center_my-application': 'My Application',
|
||||
'personal-center_overtime-application': 'Overtime Application',
|
||||
|
||||
@@ -169,8 +169,10 @@ const local: App.I18n.Schema = {
|
||||
'personal-center': '个人中心',
|
||||
'personal-center_my-profile': '个人信息',
|
||||
'personal-center_my-item': '我的事项',
|
||||
'personal-center_my-weekly': '我的周报',
|
||||
'personal-center_my-monthly': '我的月报',
|
||||
'personal-center_work-report': '工作报告',
|
||||
'personal-center_work-report_weekly': '个人周报',
|
||||
'personal-center_work-report_monthly': '个人月报',
|
||||
'personal-center_work-report_project': '项目半月报',
|
||||
'personal-center_my-performance': '我的绩效',
|
||||
'personal-center_my-application': '我的申请',
|
||||
'personal-center_overtime-application': '加班申请',
|
||||
|
||||
@@ -27,12 +27,14 @@ export const views: Record<LastLevelRouteKey, RouteComponent | (() => Promise<Ro
|
||||
metrics_worktime: () => import("@/views/metrics/worktime/index.vue"),
|
||||
"personal-center_my-application": () => import("@/views/personal-center/my-application/index.vue"),
|
||||
"personal-center_my-item": () => import("@/views/personal-center/my-item/index.vue"),
|
||||
"personal-center_my-monthly": () => import("@/views/personal-center/my-monthly/index.vue"),
|
||||
"personal-center_my-performance": () => import("@/views/personal-center/my-performance/index.vue"),
|
||||
"personal-center_my-profile": () => import("@/views/personal-center/my-profile/index.vue"),
|
||||
"personal-center_my-weekly": () => import("@/views/personal-center/my-weekly/index.vue"),
|
||||
"personal-center_overtime-application": () => import("@/views/personal-center/overtime-application/index.vue"),
|
||||
"personal-center_pending-approval": () => import("@/views/personal-center/pending-approval/index.vue"),
|
||||
"personal-center_work-report": () => import("@/views/personal-center/work-report/index.vue"),
|
||||
"personal-center_work-report_monthly": () => import("@/views/personal-center/work-report/monthly/index.vue"),
|
||||
"personal-center_work-report_project": () => import("@/views/personal-center/work-report/project/index.vue"),
|
||||
"personal-center_work-report_weekly": () => import("@/views/personal-center/work-report/weekly/index.vue"),
|
||||
product_dashboard: () => import("@/views/product/dashboard/index.vue"),
|
||||
product_list: () => import("@/views/product/list/index.vue"),
|
||||
product_requirement: () => import("@/views/product/requirement/index.vue"),
|
||||
|
||||
@@ -185,18 +185,6 @@ export const generatedRoutes: GeneratedRoute[] = [
|
||||
keepAlive: true
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'personal-center_my-monthly',
|
||||
path: '/personal-center/my-monthly',
|
||||
component: 'view.personal-center_my-monthly',
|
||||
meta: {
|
||||
title: 'personal-center_my-monthly',
|
||||
i18nKey: 'route.personal-center_my-monthly',
|
||||
icon: 'mdi:calendar-month-outline',
|
||||
order: 2,
|
||||
keepAlive: true
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'personal-center_my-performance',
|
||||
path: '/personal-center/my-performance',
|
||||
@@ -221,18 +209,6 @@ export const generatedRoutes: GeneratedRoute[] = [
|
||||
keepAlive: true
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'personal-center_my-weekly',
|
||||
path: '/personal-center/my-weekly',
|
||||
component: 'view.personal-center_my-weekly',
|
||||
meta: {
|
||||
title: 'personal-center_my-weekly',
|
||||
i18nKey: 'route.personal-center_my-weekly',
|
||||
icon: 'mdi:calendar-week-outline',
|
||||
order: 1,
|
||||
keepAlive: true
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'personal-center_overtime-application',
|
||||
path: '/personal-center/overtime-application',
|
||||
@@ -256,6 +232,53 @@ export const generatedRoutes: GeneratedRoute[] = [
|
||||
order: 7,
|
||||
keepAlive: true
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'personal-center_work-report',
|
||||
path: '/personal-center/work-report',
|
||||
component: 'view.personal-center_work-report',
|
||||
meta: {
|
||||
title: 'personal-center_work-report',
|
||||
i18nKey: 'route.personal-center_work-report',
|
||||
icon: 'mdi:file-chart-outline',
|
||||
order: 3,
|
||||
keepAlive: true
|
||||
},
|
||||
children: [
|
||||
{
|
||||
name: 'personal-center_work-report_monthly',
|
||||
path: '/personal-center/work-report/monthly',
|
||||
component: 'view.personal-center_work-report_monthly',
|
||||
meta: {
|
||||
title: 'personal-center_work-report_monthly',
|
||||
i18nKey: 'route.personal-center_work-report_monthly',
|
||||
hideInMenu: true,
|
||||
activeMenu: 'personal-center_work-report'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'personal-center_work-report_project',
|
||||
path: '/personal-center/work-report/project',
|
||||
component: 'view.personal-center_work-report_project',
|
||||
meta: {
|
||||
title: 'personal-center_work-report_project',
|
||||
i18nKey: 'route.personal-center_work-report_project',
|
||||
hideInMenu: true,
|
||||
activeMenu: 'personal-center_work-report'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'personal-center_work-report_weekly',
|
||||
path: '/personal-center/work-report/weekly',
|
||||
component: 'view.personal-center_work-report_weekly',
|
||||
meta: {
|
||||
title: 'personal-center_work-report_weekly',
|
||||
i18nKey: 'route.personal-center_work-report_weekly',
|
||||
hideInMenu: true,
|
||||
activeMenu: 'personal-center_work-report'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -182,12 +182,14 @@ const routeMap: RouteMap = {
|
||||
"personal-center": "/personal-center",
|
||||
"personal-center_my-application": "/personal-center/my-application",
|
||||
"personal-center_my-item": "/personal-center/my-item",
|
||||
"personal-center_my-monthly": "/personal-center/my-monthly",
|
||||
"personal-center_my-performance": "/personal-center/my-performance",
|
||||
"personal-center_my-profile": "/personal-center/my-profile",
|
||||
"personal-center_my-weekly": "/personal-center/my-weekly",
|
||||
"personal-center_overtime-application": "/personal-center/overtime-application",
|
||||
"personal-center_pending-approval": "/personal-center/pending-approval",
|
||||
"personal-center_work-report": "/personal-center/work-report",
|
||||
"personal-center_work-report_monthly": "/personal-center/work-report/monthly",
|
||||
"personal-center_work-report_project": "/personal-center/work-report/project",
|
||||
"personal-center_work-report_weekly": "/personal-center/work-report/weekly",
|
||||
"product": "/product",
|
||||
"product_dashboard": "/product/dashboard",
|
||||
"product_list": "/product/list",
|
||||
|
||||
@@ -10,3 +10,4 @@ export * from './project';
|
||||
export * from './project-shared';
|
||||
export * from './route';
|
||||
export * from './system-manage';
|
||||
export * from './work-report';
|
||||
|
||||
@@ -1,13 +1,7 @@
|
||||
import { WEB_SERVICE_PREFIX } from '@/constants/service';
|
||||
import { request } from '../request';
|
||||
import { type ProjectLocalDateValue, normalizeProjectLocalDate } from './project-shared';
|
||||
import {
|
||||
type ServiceRequestResult,
|
||||
mapServiceResult,
|
||||
normalizeNullableStringId,
|
||||
normalizeStringId,
|
||||
safeJsonRequestConfig
|
||||
} from './shared';
|
||||
import { type ServiceRequestResult, mapServiceResult, normalizeStringId, safeJsonRequestConfig } from './shared';
|
||||
|
||||
const OVERTIME_APPLICATION_PREFIX = `${WEB_SERVICE_PREFIX}/project/overtime-applications`;
|
||||
|
||||
@@ -30,14 +24,14 @@ type OvertimeApplicationPageResponse = Omit<Api.OvertimeApplication.OvertimeAppl
|
||||
list: OvertimeApplicationResponse[];
|
||||
};
|
||||
|
||||
type OvertimeApplicationStatusLogResponse = Omit<
|
||||
Api.OvertimeApplication.OvertimeApplicationStatusLog,
|
||||
'id' | 'applicationId' | 'operatorUserId' | 'overtimeDateSnapshot'
|
||||
type OvertimeApplicationApprovalRecordResponse = Omit<
|
||||
Api.OvertimeApplication.OvertimeApplicationApprovalRecord,
|
||||
'id' | 'overtimeApplicationId' | 'statusLogId' | 'auditorUserId'
|
||||
> & {
|
||||
id: StringIdResponse;
|
||||
applicationId: StringIdResponse;
|
||||
operatorUserId: StringIdResponse;
|
||||
overtimeDateSnapshot: ProjectLocalDateValue;
|
||||
overtimeApplicationId: StringIdResponse;
|
||||
statusLogId: StringIdResponse;
|
||||
auditorUserId: StringIdResponse;
|
||||
};
|
||||
|
||||
function normalizeBooleanFlag(value: boolean | number | string | null | undefined) {
|
||||
@@ -81,18 +75,16 @@ function normalizeOvertimeApplication(
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeStatusLog(
|
||||
response: OvertimeApplicationStatusLogResponse
|
||||
): Api.OvertimeApplication.OvertimeApplicationStatusLog {
|
||||
function normalizeApprovalRecord(
|
||||
response: OvertimeApplicationApprovalRecordResponse
|
||||
): Api.OvertimeApplication.OvertimeApplicationApprovalRecord {
|
||||
return {
|
||||
...response,
|
||||
id: normalizeStringId(response.id),
|
||||
applicationId: normalizeStringId(response.applicationId),
|
||||
operatorUserId: normalizeStringId(response.operatorUserId),
|
||||
overtimeDateSnapshot: normalizeProjectLocalDate(response.overtimeDateSnapshot) ?? '',
|
||||
fromStatus: normalizeNullableStringId(response.fromStatus),
|
||||
reason: response.reason ?? null,
|
||||
remark: response.remark ?? null
|
||||
overtimeApplicationId: normalizeStringId(response.overtimeApplicationId),
|
||||
statusLogId: normalizeStringId(response.statusLogId),
|
||||
auditorUserId: normalizeStringId(response.auditorUserId),
|
||||
opinion: response.opinion ?? null
|
||||
};
|
||||
}
|
||||
|
||||
@@ -240,15 +232,6 @@ export function fetchRejectOvertimeApplication(id: string, data: Api.OvertimeApp
|
||||
});
|
||||
}
|
||||
|
||||
export function fetchCancelOvertimeApplication(id: string, data: Api.OvertimeApplication.StatusActionParams) {
|
||||
return request<boolean>({
|
||||
...safeJsonRequestConfig,
|
||||
url: `${OVERTIME_APPLICATION_PREFIX}/${id}/cancel`,
|
||||
method: 'post',
|
||||
data: toStatusActionRequest(data)
|
||||
});
|
||||
}
|
||||
|
||||
export function fetchDeleteOvertimeApplication(id: string) {
|
||||
return request<boolean>({
|
||||
...safeJsonRequestConfig,
|
||||
@@ -257,15 +240,15 @@ export function fetchDeleteOvertimeApplication(id: string) {
|
||||
});
|
||||
}
|
||||
|
||||
export async function fetchGetOvertimeApplicationStatusLogs(id: string) {
|
||||
const result = await request<OvertimeApplicationStatusLogResponse[]>({
|
||||
export async function fetchGetOvertimeApplicationApprovalRecords(id: string) {
|
||||
const result = await request<OvertimeApplicationApprovalRecordResponse[]>({
|
||||
...safeJsonRequestConfig,
|
||||
url: `${OVERTIME_APPLICATION_PREFIX}/${id}/status-logs`,
|
||||
url: `${OVERTIME_APPLICATION_PREFIX}/${id}/approval-records`,
|
||||
method: 'get'
|
||||
});
|
||||
|
||||
return mapServiceResult(result as ServiceRequestResult<OvertimeApplicationStatusLogResponse[]>, data =>
|
||||
data.map(normalizeStatusLog)
|
||||
return mapServiceResult(result as ServiceRequestResult<OvertimeApplicationApprovalRecordResponse[]>, data =>
|
||||
data.map(normalizeApprovalRecord)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
866
src/service/api/work-report.ts
Normal file
866
src/service/api/work-report.ts
Normal file
@@ -0,0 +1,866 @@
|
||||
import { WEB_SERVICE_PREFIX } from '@/constants/service';
|
||||
import { request } from '../request';
|
||||
import {
|
||||
type ServiceRequestResult,
|
||||
mapServiceResult,
|
||||
normalizeNullableStringId,
|
||||
normalizeStringId,
|
||||
safeJsonRequestConfig
|
||||
} from './shared';
|
||||
|
||||
const WORK_REPORT_PREFIX = `${WEB_SERVICE_PREFIX}/project/work-reports`;
|
||||
const WEEKLY_PREFIX = `${WORK_REPORT_PREFIX}/weekly`;
|
||||
const MONTHLY_PREFIX = `${WORK_REPORT_PREFIX}/monthly`;
|
||||
const PROJECT_PREFIX = `${WORK_REPORT_PREFIX}/project`;
|
||||
|
||||
type StringIdResponse = string | number;
|
||||
type MaybeStringIdResponse = string | number | null | undefined;
|
||||
|
||||
type PageResponse<T> = {
|
||||
total: number | string;
|
||||
list: T[];
|
||||
};
|
||||
|
||||
type ReviewItemResponse = Omit<Api.WorkReport.Common.PersonalReportReviewItem, 'id'> & {
|
||||
id?: MaybeStringIdResponse;
|
||||
};
|
||||
|
||||
type PlanItemResponse = Omit<Api.WorkReport.Common.PersonalReportPlanItem, 'id'> & {
|
||||
id?: MaybeStringIdResponse;
|
||||
};
|
||||
|
||||
type WeeklyTravelSegmentResponse = Omit<Api.WorkReport.Weekly.WeeklyReportTravelSegment, 'id'> & {
|
||||
id?: MaybeStringIdResponse;
|
||||
};
|
||||
|
||||
type WeeklyReportResponse = Omit<
|
||||
Api.WorkReport.Weekly.WeeklyReport,
|
||||
'id' | 'reporterId' | 'supervisorUserId' | 'reviewItems' | 'planItems' | 'travelSegments'
|
||||
> & {
|
||||
id: StringIdResponse;
|
||||
reporterId: StringIdResponse;
|
||||
supervisorUserId: StringIdResponse;
|
||||
reviewItems?: ReviewItemResponse[] | null;
|
||||
planItems?: PlanItemResponse[] | null;
|
||||
travelSegments?: WeeklyTravelSegmentResponse[] | null;
|
||||
};
|
||||
|
||||
type MonthlyReportResponse = Omit<
|
||||
Api.WorkReport.Monthly.MonthlyReport,
|
||||
'id' | 'reporterId' | 'supervisorUserId' | 'reviewItems' | 'planItems'
|
||||
> & {
|
||||
id: StringIdResponse;
|
||||
reporterId: StringIdResponse;
|
||||
supervisorUserId: StringIdResponse;
|
||||
reviewItems?: ReviewItemResponse[] | null;
|
||||
planItems?: PlanItemResponse[] | null;
|
||||
};
|
||||
|
||||
type MemberSnapshotResponse = Omit<Api.WorkReport.Project.WorkReportMemberSnapshot, 'userId'> & {
|
||||
userId: StringIdResponse;
|
||||
};
|
||||
|
||||
type ProjectReportItemResponse = Omit<Api.WorkReport.Project.ProjectReportItem, 'id'> & {
|
||||
id?: MaybeStringIdResponse;
|
||||
};
|
||||
|
||||
type ProjectReportResponse = Omit<
|
||||
Api.WorkReport.Project.ProjectReport,
|
||||
'id' | 'projectId' | 'projectOwnerId' | 'projectMemberSnapshot' | 'supervisorUserId' | 'currentItems' | 'nextItems'
|
||||
> & {
|
||||
id: StringIdResponse;
|
||||
projectId: StringIdResponse;
|
||||
projectOwnerId: StringIdResponse;
|
||||
projectMemberSnapshot?: MemberSnapshotResponse[] | null;
|
||||
supervisorUserId: StringIdResponse;
|
||||
currentItems?: ProjectReportItemResponse[] | null;
|
||||
nextItems?: ProjectReportItemResponse[] | null;
|
||||
};
|
||||
|
||||
type ApprovalRecordResponse = Omit<
|
||||
Api.WorkReport.Common.WorkReportApprovalRecord,
|
||||
'id' | 'statusLogId' | 'auditorUserId'
|
||||
> & {
|
||||
id: StringIdResponse;
|
||||
statusLogId: StringIdResponse;
|
||||
auditorUserId: StringIdResponse;
|
||||
};
|
||||
|
||||
type MonthlyApprovalRecordResponse = Omit<
|
||||
Api.WorkReport.Monthly.MonthlyReportApprovalRecord,
|
||||
'id' | 'statusLogId' | 'auditorUserId'
|
||||
> & {
|
||||
id: StringIdResponse;
|
||||
statusLogId: StringIdResponse;
|
||||
auditorUserId: StringIdResponse;
|
||||
};
|
||||
|
||||
type ProjectOptionResponse = Omit<Api.WorkReport.Project.ProjectReportOwnerProjectOption, 'id'> & {
|
||||
id: StringIdResponse;
|
||||
};
|
||||
|
||||
function normalizeBooleanFlag(value: boolean | number | string | null | undefined) {
|
||||
if (typeof value === 'boolean') return value;
|
||||
if (typeof value === 'number') return value === 1;
|
||||
if (typeof value === 'string') {
|
||||
const normalized = value.trim().toLowerCase();
|
||||
return !['', '0', 'false', 'n', 'no'].includes(normalized);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function normalizeApprovalConclusion(value: unknown) {
|
||||
const conclusion = String(value || '')
|
||||
.trim()
|
||||
.toLowerCase();
|
||||
|
||||
if (conclusion === 'approve') return 'approved';
|
||||
if (conclusion === 'reject') return 'rejected';
|
||||
|
||||
return conclusion;
|
||||
}
|
||||
|
||||
function normalizeDateText(value: unknown) {
|
||||
if (value === null || value === undefined) return undefined;
|
||||
const text = String(value).trim();
|
||||
const commaDateMatch = text.match(/^(\d{4}),(\d{1,2}),(\d{1,2})$/);
|
||||
|
||||
if (commaDateMatch) {
|
||||
const [, year, month, day] = commaDateMatch;
|
||||
return `${year}-${month.padStart(2, '0')}-${day.padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
return text || undefined;
|
||||
}
|
||||
|
||||
function normalizeTotal(total: number | string) {
|
||||
const value = Number(total);
|
||||
return Number.isFinite(value) ? Math.max(0, value) : 0;
|
||||
}
|
||||
|
||||
function sumWorkHours(items: Array<{ workHours?: number | string | null }> = []) {
|
||||
return items.reduce((sum, item) => {
|
||||
const value = Number(item.workHours ?? 0);
|
||||
return Number.isFinite(value) ? sum + value : sum;
|
||||
}, 0);
|
||||
}
|
||||
|
||||
function normalizeReportTotalWorkHours(
|
||||
totalWorkHours: number | string | null | undefined,
|
||||
fallbackTotalWorkHours: number
|
||||
) {
|
||||
const normalizedTotal = Number(totalWorkHours ?? 0);
|
||||
|
||||
if (
|
||||
(totalWorkHours === null ||
|
||||
totalWorkHours === undefined ||
|
||||
totalWorkHours === '' ||
|
||||
(Number.isFinite(normalizedTotal) && normalizedTotal === 0)) &&
|
||||
fallbackTotalWorkHours > 0
|
||||
) {
|
||||
return fallbackTotalWorkHours;
|
||||
}
|
||||
|
||||
return totalWorkHours ?? 0;
|
||||
}
|
||||
|
||||
function appendValue(query: URLSearchParams, key: string, value: unknown) {
|
||||
if (value === null || value === undefined || value === '') return;
|
||||
query.append(key, String(value));
|
||||
}
|
||||
|
||||
function appendArray(query: URLSearchParams, key: string, values?: Array<string | null | undefined> | null) {
|
||||
values?.forEach(value => appendValue(query, key, value));
|
||||
}
|
||||
|
||||
function createBasePageQuery(params: Api.WorkReport.Common.WorkReportBaseSearchParams = {}) {
|
||||
const query = new URLSearchParams();
|
||||
|
||||
appendValue(query, 'pageNo', params.pageNo ?? 1);
|
||||
appendValue(query, 'pageSize', params.pageSize ?? 10);
|
||||
appendValue(query, 'keyword', params.keyword);
|
||||
appendValue(query, 'statusCode', params.statusCode);
|
||||
appendValue(query, 'supervisorName', params.supervisorName);
|
||||
appendArray(query, 'periodStartDate', params.periodStartDate);
|
||||
appendArray(query, 'submitTime', params.submitTime);
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
function createWeeklyPageQuery(params: Api.WorkReport.Weekly.WeeklyReportSearchParams = {}) {
|
||||
const query = createBasePageQuery(params);
|
||||
appendValue(query, 'isBusinessTrip', params.isBusinessTrip);
|
||||
return query.toString();
|
||||
}
|
||||
|
||||
function createMonthlyPageQuery(params: Api.WorkReport.Monthly.MonthlyReportSearchParams = {}) {
|
||||
return createBasePageQuery(params).toString();
|
||||
}
|
||||
|
||||
function createProjectPageQuery(params: Api.WorkReport.Project.ProjectReportSearchParams = {}) {
|
||||
const query = createBasePageQuery(params);
|
||||
appendValue(query, 'projectId', params.projectId);
|
||||
appendValue(query, 'flag', params.flag);
|
||||
return query.toString();
|
||||
}
|
||||
|
||||
function normalizeReviewItem(item: ReviewItemResponse): Api.WorkReport.Common.PersonalReportReviewItem {
|
||||
return {
|
||||
...item,
|
||||
id: normalizeNullableStringId(item.id) ?? undefined
|
||||
};
|
||||
}
|
||||
|
||||
function normalizePlanItem(item: PlanItemResponse): Api.WorkReport.Common.PersonalReportPlanItem {
|
||||
return {
|
||||
...item,
|
||||
id: normalizeNullableStringId(item.id) ?? undefined
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeWeeklyTravelSegment(
|
||||
item: WeeklyTravelSegmentResponse
|
||||
): Api.WorkReport.Weekly.WeeklyReportTravelSegment {
|
||||
return {
|
||||
...item,
|
||||
id: normalizeNullableStringId(item.id) ?? undefined,
|
||||
startDate: normalizeDateText(item.startDate),
|
||||
endDate: normalizeDateText(item.endDate)
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeWeeklyReport(response: WeeklyReportResponse): Api.WorkReport.Weekly.WeeklyReport {
|
||||
const fallbackTotalWorkHours = sumWorkHours(response.reviewItems ?? []);
|
||||
|
||||
return {
|
||||
...response,
|
||||
id: normalizeStringId(response.id),
|
||||
reporterId: normalizeStringId(response.reporterId),
|
||||
supervisorUserId: normalizeStringId(response.supervisorUserId),
|
||||
reporterDeptName: response.reporterDeptName ?? null,
|
||||
reporterPostName: response.reporterPostName ?? null,
|
||||
statusName: response.statusName || response.statusCode,
|
||||
allowEdit: normalizeBooleanFlag(response.allowEdit),
|
||||
terminal: normalizeBooleanFlag(response.terminal),
|
||||
isBusinessTrip: normalizeBooleanFlag(response.isBusinessTrip),
|
||||
totalWorkHours: normalizeReportTotalWorkHours(response.totalWorkHours, fallbackTotalWorkHours),
|
||||
submitTime: response.submitTime ?? null,
|
||||
reviewItems: response.reviewItems?.map(normalizeReviewItem) ?? [],
|
||||
planItems: response.planItems?.map(normalizePlanItem) ?? [],
|
||||
travelSegments: response.travelSegments?.map(normalizeWeeklyTravelSegment) ?? []
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeMonthlyReport(response: MonthlyReportResponse): Api.WorkReport.Monthly.MonthlyReport {
|
||||
const fallbackTotalWorkHours = sumWorkHours(response.reviewItems ?? []);
|
||||
|
||||
return {
|
||||
...response,
|
||||
id: normalizeStringId(response.id),
|
||||
reporterId: normalizeStringId(response.reporterId),
|
||||
supervisorUserId: normalizeStringId(response.supervisorUserId),
|
||||
reporterDeptName: response.reporterDeptName ?? null,
|
||||
reporterPostName: response.reporterPostName ?? null,
|
||||
statusName: response.statusName || response.statusCode,
|
||||
allowEdit: normalizeBooleanFlag(response.allowEdit),
|
||||
terminal: normalizeBooleanFlag(response.terminal),
|
||||
totalWorkHours: normalizeReportTotalWorkHours(response.totalWorkHours, fallbackTotalWorkHours),
|
||||
submitTime: response.submitTime ?? null,
|
||||
reviewItems: response.reviewItems?.map(normalizeReviewItem) ?? [],
|
||||
planItems: response.planItems?.map(normalizePlanItem) ?? []
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeMemberSnapshot(item: MemberSnapshotResponse): Api.WorkReport.Project.WorkReportMemberSnapshot {
|
||||
return {
|
||||
...item,
|
||||
userId: normalizeStringId(item.userId)
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeProjectReportItem(item: ProjectReportItemResponse): Api.WorkReport.Project.ProjectReportItem {
|
||||
return {
|
||||
...item,
|
||||
id: normalizeNullableStringId(item.id) ?? undefined
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeProjectReport(response: ProjectReportResponse): Api.WorkReport.Project.ProjectReport {
|
||||
const fallbackTotalWorkHours = sumWorkHours(response.currentItems ?? []);
|
||||
|
||||
return {
|
||||
...response,
|
||||
id: normalizeStringId(response.id),
|
||||
projectId: normalizeStringId(response.projectId),
|
||||
projectOwnerId: normalizeStringId(response.projectOwnerId),
|
||||
projectMemberSnapshot: response.projectMemberSnapshot?.map(normalizeMemberSnapshot) ?? [],
|
||||
supervisorUserId: normalizeStringId(response.supervisorUserId),
|
||||
statusName: response.statusName || response.statusCode,
|
||||
allowEdit: normalizeBooleanFlag(response.allowEdit),
|
||||
terminal: normalizeBooleanFlag(response.terminal),
|
||||
totalWorkHours: normalizeReportTotalWorkHours(response.totalWorkHours, fallbackTotalWorkHours),
|
||||
submitTime: response.submitTime ?? null,
|
||||
currentItems: response.currentItems?.map(normalizeProjectReportItem) ?? [],
|
||||
nextItems: response.nextItems?.map(normalizeProjectReportItem) ?? []
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeApprovalRecord(response: ApprovalRecordResponse): Api.WorkReport.Common.WorkReportApprovalRecord {
|
||||
return {
|
||||
...response,
|
||||
id: normalizeStringId(response.id),
|
||||
statusLogId: normalizeStringId(response.statusLogId),
|
||||
auditorUserId: normalizeStringId(response.auditorUserId),
|
||||
conclusion: normalizeApprovalConclusion(response.conclusion),
|
||||
opinion: response.opinion ?? null
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeMonthlyApprovalRecord(
|
||||
response: MonthlyApprovalRecordResponse
|
||||
): Api.WorkReport.Monthly.MonthlyReportApprovalRecord {
|
||||
return {
|
||||
...response,
|
||||
id: normalizeStringId(response.id),
|
||||
statusLogId: normalizeStringId(response.statusLogId),
|
||||
auditorUserId: normalizeStringId(response.auditorUserId),
|
||||
conclusion: normalizeApprovalConclusion(response.conclusion),
|
||||
opinion: response.opinion ?? null
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeProjectOption(
|
||||
response: ProjectOptionResponse
|
||||
): Api.WorkReport.Project.ProjectReportOwnerProjectOption {
|
||||
return {
|
||||
...response,
|
||||
id: normalizeStringId(response.id)
|
||||
};
|
||||
}
|
||||
|
||||
function mapPage<TInput, TOutput>(data: PageResponse<TInput>, mapper: (item: TInput) => TOutput) {
|
||||
return {
|
||||
total: normalizeTotal(data.total),
|
||||
list: data.list.map(mapper)
|
||||
};
|
||||
}
|
||||
|
||||
function toStatusActionRequest(data: Api.WorkReport.Common.StatusActionParams = {}) {
|
||||
return {
|
||||
reason: data.reason?.trim() || undefined
|
||||
};
|
||||
}
|
||||
|
||||
function toPersonalReviewItems(items: Api.WorkReport.Common.PersonalReportReviewItem[] = []) {
|
||||
return items.map((item, index) => ({
|
||||
itemNumber: item.itemNumber ?? index + 1,
|
||||
itemTitle: item.itemTitle?.trim() || '',
|
||||
workHours: item.workHours ?? 0,
|
||||
contentText: item.contentText?.trim() || '',
|
||||
contentJson: item.contentJson ?? null,
|
||||
reflectionText: item.reflectionText?.trim() || ''
|
||||
}));
|
||||
}
|
||||
|
||||
function toPersonalPlanItems(items: Api.WorkReport.Common.PersonalReportPlanItem[] = []) {
|
||||
return items.map((item, index) => ({
|
||||
itemNumber: item.itemNumber ?? index + 1,
|
||||
itemTitle: item.itemTitle?.trim() || '',
|
||||
targetText: item.targetText?.trim() || '',
|
||||
targetJson: item.targetJson ?? null,
|
||||
supportNeed: item.supportNeed?.trim() || ''
|
||||
}));
|
||||
}
|
||||
|
||||
function toWeeklySaveRequest(data: Api.WorkReport.Weekly.WeeklyReportSaveParams) {
|
||||
return {
|
||||
periodKey: data.periodKey,
|
||||
periodLabel: data.periodLabel,
|
||||
periodStartDate: data.periodStartDate,
|
||||
periodEndDate: data.periodEndDate,
|
||||
isBusinessTrip: data.isBusinessTrip,
|
||||
reviewItems: toPersonalReviewItems(data.reviewItems),
|
||||
planItems: toPersonalPlanItems(data.planItems),
|
||||
travelSegments: data.isBusinessTrip
|
||||
? data.travelSegments.map((item, index) => ({
|
||||
sort: item.sort ?? index + 1,
|
||||
startDate: item.startDate || undefined,
|
||||
endDate: item.endDate || undefined,
|
||||
travelDays: item.travelDays ?? 0,
|
||||
location: item.location?.trim() || ''
|
||||
}))
|
||||
: []
|
||||
};
|
||||
}
|
||||
|
||||
function toMonthlySaveRequest(data: Api.WorkReport.Monthly.MonthlyReportSaveParams) {
|
||||
return {
|
||||
periodKey: data.periodKey,
|
||||
periodLabel: data.periodLabel,
|
||||
periodStartDate: data.periodStartDate,
|
||||
periodEndDate: data.periodEndDate,
|
||||
reviewItems: toPersonalReviewItems(data.reviewItems),
|
||||
planItems: toPersonalPlanItems(data.planItems)
|
||||
};
|
||||
}
|
||||
|
||||
function toProjectItems(items: Api.WorkReport.Project.ProjectReportItem[] = []) {
|
||||
return items.map(item => ({
|
||||
itemTitle: item.itemTitle?.trim() || '',
|
||||
workHours: item.workHours ?? 0,
|
||||
priorityCode: item.priorityCode || undefined,
|
||||
progressRate: item.progressRate ?? 0
|
||||
}));
|
||||
}
|
||||
|
||||
function toProjectSaveRequest(data: Api.WorkReport.Project.ProjectReportSaveParams) {
|
||||
return {
|
||||
projectId: data.projectId,
|
||||
periodKey: data.periodKey,
|
||||
periodLabel: data.periodLabel,
|
||||
periodStartDate: data.periodStartDate,
|
||||
periodEndDate: data.periodEndDate,
|
||||
flag: data.flag,
|
||||
projectStatusDesc: data.projectStatusDesc?.trim() || '',
|
||||
projectProgressPlan: data.projectProgressPlan?.trim() || '',
|
||||
projectKeyPoints: data.projectKeyPoints?.trim() || '',
|
||||
projectProblems: data.projectProblems?.trim() || '',
|
||||
currentItems: toProjectItems(data.currentItems),
|
||||
nextItems: toProjectItems(data.nextItems)
|
||||
};
|
||||
}
|
||||
|
||||
export async function fetchGetWorkReportStatusDict() {
|
||||
const result = await request<Api.WorkReport.Common.WorkReportStatusDict[]>({
|
||||
...safeJsonRequestConfig,
|
||||
url: `${WORK_REPORT_PREFIX}/status/dict`,
|
||||
method: 'get'
|
||||
});
|
||||
|
||||
return mapServiceResult(result as ServiceRequestResult<Api.WorkReport.Common.WorkReportStatusDict[]>, data => data);
|
||||
}
|
||||
|
||||
export async function fetchGetWeeklyReportPage(params: Api.WorkReport.Weekly.WeeklyReportSearchParams = {}) {
|
||||
const query = createWeeklyPageQuery(params);
|
||||
const result = await request<PageResponse<WeeklyReportResponse>>({
|
||||
...safeJsonRequestConfig,
|
||||
url: query ? `${WEEKLY_PREFIX}/page?${query}` : `${WEEKLY_PREFIX}/page`,
|
||||
method: 'get'
|
||||
});
|
||||
|
||||
return mapServiceResult(result as ServiceRequestResult<PageResponse<WeeklyReportResponse>>, data =>
|
||||
mapPage(data, normalizeWeeklyReport)
|
||||
);
|
||||
}
|
||||
|
||||
export async function fetchGetWeeklyReportApprovalPage(params: Api.WorkReport.Weekly.WeeklyReportSearchParams = {}) {
|
||||
const query = createWeeklyPageQuery(params);
|
||||
const result = await request<PageResponse<WeeklyReportResponse>>({
|
||||
...safeJsonRequestConfig,
|
||||
url: query ? `${WEEKLY_PREFIX}/approval-page?${query}` : `${WEEKLY_PREFIX}/approval-page`,
|
||||
method: 'get'
|
||||
});
|
||||
|
||||
return mapServiceResult(result as ServiceRequestResult<PageResponse<WeeklyReportResponse>>, data =>
|
||||
mapPage(data, normalizeWeeklyReport)
|
||||
);
|
||||
}
|
||||
|
||||
export async function fetchGetWeeklyReportDetail(id: string) {
|
||||
const result = await request<WeeklyReportResponse>({
|
||||
...safeJsonRequestConfig,
|
||||
url: `${WEEKLY_PREFIX}/${id}`,
|
||||
method: 'get'
|
||||
});
|
||||
|
||||
return mapServiceResult(result as ServiceRequestResult<WeeklyReportResponse>, normalizeWeeklyReport);
|
||||
}
|
||||
|
||||
export async function fetchInitWeeklyReport() {
|
||||
const result = await request<WeeklyReportResponse>({
|
||||
...safeJsonRequestConfig,
|
||||
url: `${WEEKLY_PREFIX}/init`,
|
||||
method: 'get'
|
||||
});
|
||||
|
||||
return mapServiceResult(result as ServiceRequestResult<WeeklyReportResponse>, normalizeWeeklyReport);
|
||||
}
|
||||
|
||||
export async function fetchPreviewWeeklyReportDefaultDraft(
|
||||
params: Api.WorkReport.Weekly.WeeklyReportDefaultDraftParams
|
||||
) {
|
||||
const result = await request<WeeklyReportResponse>({
|
||||
...safeJsonRequestConfig,
|
||||
url: `${WEEKLY_PREFIX}/default-draft`,
|
||||
method: 'get',
|
||||
params
|
||||
});
|
||||
|
||||
return mapServiceResult(result as ServiceRequestResult<WeeklyReportResponse>, normalizeWeeklyReport);
|
||||
}
|
||||
|
||||
export async function fetchCreateWeeklyReport(data: Api.WorkReport.Weekly.WeeklyReportSaveParams) {
|
||||
const result = await request<StringIdResponse>({
|
||||
...safeJsonRequestConfig,
|
||||
url: WEEKLY_PREFIX,
|
||||
method: 'post',
|
||||
data: toWeeklySaveRequest(data)
|
||||
});
|
||||
|
||||
return mapServiceResult(result as ServiceRequestResult<StringIdResponse>, normalizeStringId);
|
||||
}
|
||||
|
||||
export function fetchUpdateWeeklyReport(id: string, data: Api.WorkReport.Weekly.WeeklyReportSaveParams) {
|
||||
return request<boolean>({
|
||||
...safeJsonRequestConfig,
|
||||
url: `${WEEKLY_PREFIX}/${id}`,
|
||||
method: 'put',
|
||||
data: toWeeklySaveRequest(data)
|
||||
});
|
||||
}
|
||||
|
||||
export function fetchSubmitWeeklyReport(id: string) {
|
||||
return request<boolean>({ ...safeJsonRequestConfig, url: `${WEEKLY_PREFIX}/${id}/submit`, method: 'post' });
|
||||
}
|
||||
|
||||
export function fetchApproveWeeklyReport(id: string, data: Api.WorkReport.Common.StatusActionParams = {}) {
|
||||
return request<boolean>({
|
||||
...safeJsonRequestConfig,
|
||||
url: `${WEEKLY_PREFIX}/${id}/approve`,
|
||||
method: 'post',
|
||||
data: toStatusActionRequest(data)
|
||||
});
|
||||
}
|
||||
|
||||
export function fetchRejectWeeklyReport(id: string, data: Api.WorkReport.Common.StatusActionParams) {
|
||||
return request<boolean>({
|
||||
...safeJsonRequestConfig,
|
||||
url: `${WEEKLY_PREFIX}/${id}/reject`,
|
||||
method: 'post',
|
||||
data: toStatusActionRequest(data)
|
||||
});
|
||||
}
|
||||
|
||||
export function fetchDeleteWeeklyReport(id: string) {
|
||||
return request<boolean>({ ...safeJsonRequestConfig, url: `${WEEKLY_PREFIX}/${id}`, method: 'delete' });
|
||||
}
|
||||
|
||||
export async function fetchGetWeeklyReportApprovalRecords(id: string) {
|
||||
const result = await request<ApprovalRecordResponse[]>({
|
||||
...safeJsonRequestConfig,
|
||||
url: `${WEEKLY_PREFIX}/${id}/approval-records`,
|
||||
method: 'get'
|
||||
});
|
||||
|
||||
return mapServiceResult(result as ServiceRequestResult<ApprovalRecordResponse[]>, data =>
|
||||
data.map(normalizeApprovalRecord)
|
||||
);
|
||||
}
|
||||
|
||||
export function fetchExportWeeklyReports(params: Api.WorkReport.Weekly.WeeklyReportSearchParams = {}) {
|
||||
const query = createWeeklyPageQuery(params);
|
||||
return request<Blob, 'blob'>({
|
||||
url: query ? `${WEEKLY_PREFIX}/export?${query}` : `${WEEKLY_PREFIX}/export`,
|
||||
method: 'get',
|
||||
responseType: 'blob'
|
||||
});
|
||||
}
|
||||
|
||||
export function fetchExportWeeklyReportContent(
|
||||
data: Api.WorkReport.Common.ContentExportParams<Api.WorkReport.Weekly.WeeklyReportSearchParams>
|
||||
) {
|
||||
return request<Blob, 'blob'>({
|
||||
url: `${WEEKLY_PREFIX}/content-export`,
|
||||
method: 'post',
|
||||
data,
|
||||
responseType: 'blob'
|
||||
});
|
||||
}
|
||||
|
||||
export async function fetchGetMonthlyReportPage(params: Api.WorkReport.Monthly.MonthlyReportSearchParams = {}) {
|
||||
const query = createMonthlyPageQuery(params);
|
||||
const result = await request<PageResponse<MonthlyReportResponse>>({
|
||||
...safeJsonRequestConfig,
|
||||
url: query ? `${MONTHLY_PREFIX}/page?${query}` : `${MONTHLY_PREFIX}/page`,
|
||||
method: 'get'
|
||||
});
|
||||
|
||||
return mapServiceResult(result as ServiceRequestResult<PageResponse<MonthlyReportResponse>>, data =>
|
||||
mapPage(data, normalizeMonthlyReport)
|
||||
);
|
||||
}
|
||||
|
||||
export async function fetchGetMonthlyReportApprovalPage(params: Api.WorkReport.Monthly.MonthlyReportSearchParams = {}) {
|
||||
const query = createMonthlyPageQuery(params);
|
||||
const result = await request<PageResponse<MonthlyReportResponse>>({
|
||||
...safeJsonRequestConfig,
|
||||
url: query ? `${MONTHLY_PREFIX}/approval-page?${query}` : `${MONTHLY_PREFIX}/approval-page`,
|
||||
method: 'get'
|
||||
});
|
||||
|
||||
return mapServiceResult(result as ServiceRequestResult<PageResponse<MonthlyReportResponse>>, data =>
|
||||
mapPage(data, normalizeMonthlyReport)
|
||||
);
|
||||
}
|
||||
|
||||
export async function fetchGetMonthlyReportDetail(id: string) {
|
||||
const result = await request<MonthlyReportResponse>({
|
||||
...safeJsonRequestConfig,
|
||||
url: `${MONTHLY_PREFIX}/${id}`,
|
||||
method: 'get'
|
||||
});
|
||||
|
||||
return mapServiceResult(result as ServiceRequestResult<MonthlyReportResponse>, normalizeMonthlyReport);
|
||||
}
|
||||
|
||||
export async function fetchInitMonthlyReport() {
|
||||
const result = await request<MonthlyReportResponse>({
|
||||
...safeJsonRequestConfig,
|
||||
url: `${MONTHLY_PREFIX}/init`,
|
||||
method: 'get'
|
||||
});
|
||||
|
||||
return mapServiceResult(result as ServiceRequestResult<MonthlyReportResponse>, normalizeMonthlyReport);
|
||||
}
|
||||
|
||||
export async function fetchPreviewMonthlyReportDefaultDraft(
|
||||
params: Api.WorkReport.Monthly.MonthlyReportDefaultDraftParams
|
||||
) {
|
||||
const result = await request<MonthlyReportResponse>({
|
||||
...safeJsonRequestConfig,
|
||||
url: `${MONTHLY_PREFIX}/default-draft`,
|
||||
method: 'get',
|
||||
params
|
||||
});
|
||||
|
||||
return mapServiceResult(result as ServiceRequestResult<MonthlyReportResponse>, normalizeMonthlyReport);
|
||||
}
|
||||
|
||||
export async function fetchCreateMonthlyReport(data: Api.WorkReport.Monthly.MonthlyReportSaveParams) {
|
||||
const result = await request<StringIdResponse>({
|
||||
...safeJsonRequestConfig,
|
||||
url: MONTHLY_PREFIX,
|
||||
method: 'post',
|
||||
data: toMonthlySaveRequest(data)
|
||||
});
|
||||
|
||||
return mapServiceResult(result as ServiceRequestResult<StringIdResponse>, normalizeStringId);
|
||||
}
|
||||
|
||||
export function fetchUpdateMonthlyReport(id: string, data: Api.WorkReport.Monthly.MonthlyReportSaveParams) {
|
||||
return request<boolean>({
|
||||
...safeJsonRequestConfig,
|
||||
url: `${MONTHLY_PREFIX}/${id}`,
|
||||
method: 'put',
|
||||
data: toMonthlySaveRequest(data)
|
||||
});
|
||||
}
|
||||
|
||||
export function fetchSubmitMonthlyReport(id: string) {
|
||||
return request<boolean>({ ...safeJsonRequestConfig, url: `${MONTHLY_PREFIX}/${id}/submit`, method: 'post' });
|
||||
}
|
||||
|
||||
export function fetchApproveMonthlyReport(id: string, data: Api.WorkReport.Monthly.MonthlyReportApproveParams) {
|
||||
return request<boolean>({
|
||||
...safeJsonRequestConfig,
|
||||
url: `${MONTHLY_PREFIX}/${id}/approve`,
|
||||
method: 'post',
|
||||
data
|
||||
});
|
||||
}
|
||||
|
||||
export function fetchRejectMonthlyReport(id: string, data: Api.WorkReport.Common.StatusActionParams) {
|
||||
return request<boolean>({
|
||||
...safeJsonRequestConfig,
|
||||
url: `${MONTHLY_PREFIX}/${id}/reject`,
|
||||
method: 'post',
|
||||
data: toStatusActionRequest(data)
|
||||
});
|
||||
}
|
||||
|
||||
export function fetchDeleteMonthlyReport(id: string) {
|
||||
return request<boolean>({ ...safeJsonRequestConfig, url: `${MONTHLY_PREFIX}/${id}`, method: 'delete' });
|
||||
}
|
||||
|
||||
export async function fetchGetMonthlyReportApprovalRecords(id: string) {
|
||||
const result = await request<MonthlyApprovalRecordResponse[]>({
|
||||
...safeJsonRequestConfig,
|
||||
url: `${MONTHLY_PREFIX}/${id}/approval-records`,
|
||||
method: 'get'
|
||||
});
|
||||
|
||||
return mapServiceResult(result as ServiceRequestResult<MonthlyApprovalRecordResponse[]>, data =>
|
||||
data.map(normalizeMonthlyApprovalRecord)
|
||||
);
|
||||
}
|
||||
|
||||
export function fetchExportMonthlyReports(params: Api.WorkReport.Monthly.MonthlyReportSearchParams = {}) {
|
||||
const query = createMonthlyPageQuery(params);
|
||||
return request<Blob, 'blob'>({
|
||||
url: query ? `${MONTHLY_PREFIX}/export?${query}` : `${MONTHLY_PREFIX}/export`,
|
||||
method: 'get',
|
||||
responseType: 'blob'
|
||||
});
|
||||
}
|
||||
|
||||
export function fetchExportMonthlyReportContent(
|
||||
data: Api.WorkReport.Common.ContentExportParams<Api.WorkReport.Monthly.MonthlyReportSearchParams>
|
||||
) {
|
||||
return request<Blob, 'blob'>({
|
||||
url: `${MONTHLY_PREFIX}/content-export`,
|
||||
method: 'post',
|
||||
data,
|
||||
responseType: 'blob'
|
||||
});
|
||||
}
|
||||
|
||||
export async function fetchGetProjectReportOwnerProjectOptions() {
|
||||
const result = await request<ProjectOptionResponse[]>({
|
||||
...safeJsonRequestConfig,
|
||||
url: `${PROJECT_PREFIX}/owner-project-options`,
|
||||
method: 'get'
|
||||
});
|
||||
|
||||
return mapServiceResult(result as ServiceRequestResult<ProjectOptionResponse[]>, data =>
|
||||
data.map(normalizeProjectOption)
|
||||
);
|
||||
}
|
||||
|
||||
export async function fetchGetProjectReportPage(params: Api.WorkReport.Project.ProjectReportSearchParams = {}) {
|
||||
const query = createProjectPageQuery(params);
|
||||
const result = await request<PageResponse<ProjectReportResponse>>({
|
||||
...safeJsonRequestConfig,
|
||||
url: query ? `${PROJECT_PREFIX}/page?${query}` : `${PROJECT_PREFIX}/page`,
|
||||
method: 'get'
|
||||
});
|
||||
|
||||
return mapServiceResult(result as ServiceRequestResult<PageResponse<ProjectReportResponse>>, data =>
|
||||
mapPage(data, normalizeProjectReport)
|
||||
);
|
||||
}
|
||||
|
||||
export async function fetchGetProjectReportApprovalPage(params: Api.WorkReport.Project.ProjectReportSearchParams = {}) {
|
||||
const query = createProjectPageQuery(params);
|
||||
const result = await request<PageResponse<ProjectReportResponse>>({
|
||||
...safeJsonRequestConfig,
|
||||
url: query ? `${PROJECT_PREFIX}/approval-page?${query}` : `${PROJECT_PREFIX}/approval-page`,
|
||||
method: 'get'
|
||||
});
|
||||
|
||||
return mapServiceResult(result as ServiceRequestResult<PageResponse<ProjectReportResponse>>, data =>
|
||||
mapPage(data, normalizeProjectReport)
|
||||
);
|
||||
}
|
||||
|
||||
export async function fetchGetProjectReportDetail(id: string) {
|
||||
const result = await request<ProjectReportResponse>({
|
||||
...safeJsonRequestConfig,
|
||||
url: `${PROJECT_PREFIX}/${id}`,
|
||||
method: 'get'
|
||||
});
|
||||
|
||||
return mapServiceResult(result as ServiceRequestResult<ProjectReportResponse>, normalizeProjectReport);
|
||||
}
|
||||
|
||||
export async function fetchInitProjectReport(projectId: string) {
|
||||
const result = await request<ProjectReportResponse>({
|
||||
...safeJsonRequestConfig,
|
||||
url: `${PROJECT_PREFIX}/init`,
|
||||
method: 'get',
|
||||
params: { projectId }
|
||||
});
|
||||
|
||||
return mapServiceResult(result as ServiceRequestResult<ProjectReportResponse>, normalizeProjectReport);
|
||||
}
|
||||
|
||||
export async function fetchPreviewProjectReportDefaultDraft(
|
||||
projectId: string,
|
||||
params: Api.WorkReport.Project.ProjectReportDefaultDraftParams
|
||||
) {
|
||||
const result = await request<ProjectReportResponse>({
|
||||
...safeJsonRequestConfig,
|
||||
url: `${PROJECT_PREFIX}/${projectId}/default-draft`,
|
||||
method: 'get',
|
||||
params
|
||||
});
|
||||
|
||||
return mapServiceResult(result as ServiceRequestResult<ProjectReportResponse>, normalizeProjectReport);
|
||||
}
|
||||
|
||||
export async function fetchCreateProjectReport(data: Api.WorkReport.Project.ProjectReportSaveParams) {
|
||||
const result = await request<StringIdResponse>({
|
||||
...safeJsonRequestConfig,
|
||||
url: PROJECT_PREFIX,
|
||||
method: 'post',
|
||||
data: toProjectSaveRequest(data)
|
||||
});
|
||||
|
||||
return mapServiceResult(result as ServiceRequestResult<StringIdResponse>, normalizeStringId);
|
||||
}
|
||||
|
||||
export function fetchUpdateProjectReport(id: string, data: Api.WorkReport.Project.ProjectReportSaveParams) {
|
||||
return request<boolean>({
|
||||
...safeJsonRequestConfig,
|
||||
url: `${PROJECT_PREFIX}/${id}`,
|
||||
method: 'put',
|
||||
data: toProjectSaveRequest(data)
|
||||
});
|
||||
}
|
||||
|
||||
export function fetchSubmitProjectReport(id: string) {
|
||||
return request<boolean>({ ...safeJsonRequestConfig, url: `${PROJECT_PREFIX}/${id}/submit`, method: 'post' });
|
||||
}
|
||||
|
||||
export function fetchApproveProjectReport(id: string, data: Api.WorkReport.Common.StatusActionParams = {}) {
|
||||
return request<boolean>({
|
||||
...safeJsonRequestConfig,
|
||||
url: `${PROJECT_PREFIX}/${id}/approve`,
|
||||
method: 'post',
|
||||
data: toStatusActionRequest(data)
|
||||
});
|
||||
}
|
||||
|
||||
export function fetchRejectProjectReport(id: string, data: Api.WorkReport.Common.StatusActionParams) {
|
||||
return request<boolean>({
|
||||
...safeJsonRequestConfig,
|
||||
url: `${PROJECT_PREFIX}/${id}/reject`,
|
||||
method: 'post',
|
||||
data: toStatusActionRequest(data)
|
||||
});
|
||||
}
|
||||
|
||||
export function fetchDeleteProjectReport(id: string) {
|
||||
return request<boolean>({ ...safeJsonRequestConfig, url: `${PROJECT_PREFIX}/${id}`, method: 'delete' });
|
||||
}
|
||||
|
||||
export async function fetchGetProjectReportApprovalRecords(id: string) {
|
||||
const result = await request<ApprovalRecordResponse[]>({
|
||||
...safeJsonRequestConfig,
|
||||
url: `${PROJECT_PREFIX}/${id}/approval-records`,
|
||||
method: 'get'
|
||||
});
|
||||
|
||||
return mapServiceResult(result as ServiceRequestResult<ApprovalRecordResponse[]>, data =>
|
||||
data.map(normalizeApprovalRecord)
|
||||
);
|
||||
}
|
||||
|
||||
export function fetchExportProjectReports(params: Api.WorkReport.Project.ProjectReportSearchParams = {}) {
|
||||
const query = createProjectPageQuery(params);
|
||||
return request<Blob, 'blob'>({
|
||||
url: query ? `${PROJECT_PREFIX}/export?${query}` : `${PROJECT_PREFIX}/export`,
|
||||
method: 'get',
|
||||
responseType: 'blob'
|
||||
});
|
||||
}
|
||||
|
||||
export function fetchExportProjectReportContent(
|
||||
data: Api.WorkReport.Common.ContentExportParams<Api.WorkReport.Project.ProjectReportSearchParams>
|
||||
) {
|
||||
return request<Blob, 'blob'>({
|
||||
url: `${PROJECT_PREFIX}/content-export`,
|
||||
method: 'post',
|
||||
data,
|
||||
responseType: 'blob'
|
||||
});
|
||||
}
|
||||
@@ -12,11 +12,13 @@ import type { RequestInstanceState } from './type';
|
||||
|
||||
const isHttpProxy = import.meta.env.DEV && import.meta.env.VITE_HTTP_PROXY === 'Y';
|
||||
const { baseURL, otherBaseURL } = getServiceBaseURL(import.meta.env, isHttpProxy);
|
||||
const REQUEST_TIMEOUT = 15 * 1000;
|
||||
|
||||
export const request = withDedupe(
|
||||
createFlatRequest(
|
||||
{
|
||||
baseURL,
|
||||
timeout: REQUEST_TIMEOUT,
|
||||
headers: {
|
||||
apifoxToken: 'XL299LiMEDZ0H5h3A29PxwQXdMJqWyY2'
|
||||
}
|
||||
@@ -126,6 +128,10 @@ export const request = withDedupe(
|
||||
let message = error.message;
|
||||
let backendErrorCode = '';
|
||||
|
||||
if (error.code === 'ECONNABORTED' && error.message.includes('timeout')) {
|
||||
message = '请求超时,请稍后重试';
|
||||
}
|
||||
|
||||
// 获取后端错误信息和错误码
|
||||
if (error.code === BACKEND_ERROR_CODE) {
|
||||
message = error.response?.data?.msg || message;
|
||||
|
||||
@@ -406,6 +406,7 @@ html .el-collapse {
|
||||
.business-table-action-cell {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
padding: 0 8px;
|
||||
|
||||
24
src/typings/api/overtime-application.d.ts
vendored
24
src/typings/api/overtime-application.d.ts
vendored
@@ -5,9 +5,9 @@ declare namespace Api {
|
||||
pageSize: number;
|
||||
}
|
||||
|
||||
type OvertimeApplicationStatusCode = 'pending' | 'approved' | 'rejected' | 'cancelled';
|
||||
type OvertimeApplicationStatusCode = 'pending' | 'approved' | 'rejected';
|
||||
|
||||
type OvertimeApplicationActionType = 'submit' | 'resubmit' | 'approve' | 'reject' | 'cancel';
|
||||
type OvertimeApplicationActionType = 'submit' | 'resubmit' | 'approve' | 'reject';
|
||||
|
||||
interface OvertimeApplication {
|
||||
id: string;
|
||||
@@ -59,19 +59,15 @@ declare namespace Api {
|
||||
reason?: string | null;
|
||||
}
|
||||
|
||||
interface OvertimeApplicationStatusLog {
|
||||
interface OvertimeApplicationApprovalRecord {
|
||||
id: string;
|
||||
applicationId: string;
|
||||
actionType: OvertimeApplicationActionType;
|
||||
fromStatus?: string | null;
|
||||
toStatus: string;
|
||||
reason?: string | null;
|
||||
operatorUserId: string;
|
||||
operatorName: string;
|
||||
applicantNameSnapshot: string;
|
||||
overtimeDateSnapshot: string;
|
||||
overtimeDurationSnapshot: string;
|
||||
remark?: string | null;
|
||||
overtimeApplicationId: string;
|
||||
statusLogId: string;
|
||||
approvalRound: number;
|
||||
conclusion: string;
|
||||
opinion?: string | null;
|
||||
auditorUserId: string;
|
||||
auditorName: string;
|
||||
createTime: string;
|
||||
}
|
||||
|
||||
|
||||
290
src/typings/api/work-report.d.ts
vendored
Normal file
290
src/typings/api/work-report.d.ts
vendored
Normal file
@@ -0,0 +1,290 @@
|
||||
declare namespace Api {
|
||||
namespace WorkReport {
|
||||
namespace Common {
|
||||
interface PageParams {
|
||||
pageNo: number;
|
||||
pageSize: number;
|
||||
}
|
||||
|
||||
type ReportType = 'weekly' | 'monthly' | 'project';
|
||||
type WorkReportStatusCode = 'draft' | 'pending_approval' | 'approved' | 'rejected';
|
||||
|
||||
interface WorkReportStatusDict {
|
||||
statusCode: WorkReportStatusCode | string;
|
||||
statusName: string;
|
||||
sort: number;
|
||||
initialFlag: boolean;
|
||||
terminalFlag: boolean;
|
||||
allowEdit: boolean;
|
||||
}
|
||||
|
||||
interface WorkReportApprovalRecord {
|
||||
id: string;
|
||||
statusLogId: string;
|
||||
approvalRound: number;
|
||||
conclusion: string;
|
||||
opinion?: string | null;
|
||||
auditorUserId: string;
|
||||
auditorName: string;
|
||||
createTime: string;
|
||||
}
|
||||
|
||||
interface PersonalReportReviewItem {
|
||||
id?: string;
|
||||
itemNumber?: number | null;
|
||||
itemTitle: string;
|
||||
workHours?: number | null;
|
||||
contentText?: string | null;
|
||||
contentJson?: unknown;
|
||||
reflectionText?: string | null;
|
||||
}
|
||||
|
||||
interface PersonalReportPlanItem {
|
||||
id?: string;
|
||||
itemNumber?: number | null;
|
||||
itemTitle: string;
|
||||
targetText?: string | null;
|
||||
targetJson?: unknown;
|
||||
supportNeed?: string | null;
|
||||
}
|
||||
|
||||
type WorkReportBaseSearchParams = CommonType.RecordNullable<
|
||||
Pick<PageParams, 'pageNo' | 'pageSize'> & {
|
||||
keyword: string;
|
||||
statusCode: WorkReportStatusCode | string;
|
||||
periodStartDate: string[];
|
||||
submitTime: string[];
|
||||
supervisorName: string;
|
||||
}
|
||||
>;
|
||||
|
||||
type ContentExportParams<TSearch> = Partial<TSearch> & {
|
||||
exportAll?: boolean;
|
||||
ids?: string[];
|
||||
};
|
||||
|
||||
interface StatusActionParams {
|
||||
reason?: string | null;
|
||||
}
|
||||
|
||||
interface PageResult<T> {
|
||||
total: number;
|
||||
list: T[];
|
||||
}
|
||||
}
|
||||
|
||||
namespace Weekly {
|
||||
interface WeeklyReportTravelSegment {
|
||||
id?: string;
|
||||
sort?: number | null;
|
||||
startDate?: string | null;
|
||||
endDate?: string | null;
|
||||
travelDays?: number | null;
|
||||
location?: string | null;
|
||||
}
|
||||
|
||||
interface WeeklyReport {
|
||||
id: string;
|
||||
reporterId: string;
|
||||
reporterName: string;
|
||||
reporterDeptName?: string | null;
|
||||
reporterPostName?: string | null;
|
||||
supervisorUserId: string;
|
||||
supervisorName: string;
|
||||
periodKey: string;
|
||||
periodLabel: string;
|
||||
periodStartDate: string;
|
||||
periodEndDate: string;
|
||||
statusCode: Common.WorkReportStatusCode | string;
|
||||
statusName: string;
|
||||
allowEdit: boolean;
|
||||
terminal: boolean;
|
||||
isBusinessTrip: boolean;
|
||||
totalTravelDays?: number | string | null;
|
||||
totalWorkHours?: number | string | null;
|
||||
approvalComment?: string | null;
|
||||
lastStatusReason?: string | null;
|
||||
submitTime?: string | null;
|
||||
approvalTime?: string | null;
|
||||
createTime?: string | null;
|
||||
updateTime?: string | null;
|
||||
reviewItems: Common.PersonalReportReviewItem[];
|
||||
planItems: Common.PersonalReportPlanItem[];
|
||||
travelSegments: WeeklyReportTravelSegment[];
|
||||
}
|
||||
|
||||
type WeeklyReportSearchParams = Common.WorkReportBaseSearchParams & {
|
||||
isBusinessTrip?: boolean | string | null;
|
||||
};
|
||||
|
||||
interface WeeklyReportSaveParams {
|
||||
periodKey: string;
|
||||
periodLabel: string;
|
||||
periodStartDate: string;
|
||||
periodEndDate: string;
|
||||
isBusinessTrip: boolean;
|
||||
reviewItems: Common.PersonalReportReviewItem[];
|
||||
planItems: Common.PersonalReportPlanItem[];
|
||||
travelSegments: WeeklyReportTravelSegment[];
|
||||
}
|
||||
|
||||
type WeeklyReportDefaultDraftParams = Pick<
|
||||
WeeklyReportSaveParams,
|
||||
'periodKey' | 'periodLabel' | 'periodStartDate' | 'periodEndDate'
|
||||
>;
|
||||
}
|
||||
|
||||
namespace Monthly {
|
||||
interface MonthlyReport {
|
||||
id: string;
|
||||
reporterId: string;
|
||||
reporterName: string;
|
||||
reporterDeptName?: string | null;
|
||||
reporterPostName?: string | null;
|
||||
supervisorUserId: string;
|
||||
supervisorName: string;
|
||||
periodKey: string;
|
||||
periodLabel: string;
|
||||
periodStartDate: string;
|
||||
periodEndDate: string;
|
||||
statusCode: Common.WorkReportStatusCode | string;
|
||||
statusName: string;
|
||||
allowEdit: boolean;
|
||||
terminal: boolean;
|
||||
totalWorkHours?: number | string | null;
|
||||
approvalComment?: string | null;
|
||||
lastStatusReason?: string | null;
|
||||
submitTime?: string | null;
|
||||
approvalTime?: string | null;
|
||||
createTime?: string | null;
|
||||
updateTime?: string | null;
|
||||
reviewItems: Common.PersonalReportReviewItem[];
|
||||
planItems: Common.PersonalReportPlanItem[];
|
||||
}
|
||||
|
||||
type MonthlyReportSearchParams = Common.WorkReportBaseSearchParams;
|
||||
|
||||
interface MonthlyReportSaveParams {
|
||||
periodKey: string;
|
||||
periodLabel: string;
|
||||
periodStartDate: string;
|
||||
periodEndDate: string;
|
||||
reviewItems: Common.PersonalReportReviewItem[];
|
||||
planItems: Common.PersonalReportPlanItem[];
|
||||
}
|
||||
|
||||
type MonthlyReportDefaultDraftParams = Pick<
|
||||
MonthlyReportSaveParams,
|
||||
'periodKey' | 'periodLabel' | 'periodStartDate' | 'periodEndDate'
|
||||
>;
|
||||
|
||||
interface MonthlyReportApproveParams extends Common.StatusActionParams {
|
||||
meetingDate?: string | null;
|
||||
strengthDesc?: string | null;
|
||||
strengthExample?: string | null;
|
||||
weaknessDesc?: string | null;
|
||||
weaknessExample?: string | null;
|
||||
improvementSuggestion?: string | null;
|
||||
performanceResult?: string | null;
|
||||
employeeSignName?: string | null;
|
||||
employeeSignedDate?: string | null;
|
||||
supervisorSignName?: string | null;
|
||||
supervisorSignedDate?: string | null;
|
||||
}
|
||||
|
||||
interface MonthlyReportApprovalRecord extends Common.WorkReportApprovalRecord {
|
||||
meetingDate?: string | null;
|
||||
strengthDesc?: string | null;
|
||||
strengthExample?: string | null;
|
||||
weaknessDesc?: string | null;
|
||||
weaknessExample?: string | null;
|
||||
improvementSuggestion?: string | null;
|
||||
performanceResult?: string | null;
|
||||
employeeSignName?: string | null;
|
||||
employeeSignedDate?: string | null;
|
||||
supervisorSignName?: string | null;
|
||||
supervisorSignedDate?: string | null;
|
||||
}
|
||||
}
|
||||
|
||||
namespace Project {
|
||||
interface WorkReportMemberSnapshot {
|
||||
userId: string;
|
||||
userName: string;
|
||||
}
|
||||
|
||||
interface ProjectReportItem {
|
||||
id?: string;
|
||||
itemTitle: string;
|
||||
workHours?: number | null;
|
||||
priorityCode?: string | null;
|
||||
progressRate?: number | null;
|
||||
}
|
||||
|
||||
interface ProjectReportOwnerProjectOption {
|
||||
id: string;
|
||||
projectCode: string;
|
||||
projectName: string;
|
||||
}
|
||||
|
||||
interface ProjectReport {
|
||||
id: string;
|
||||
projectId: string;
|
||||
projectName: string;
|
||||
projectOwnerId: string;
|
||||
projectOwnerName: string;
|
||||
technicalOwnerName?: string | null;
|
||||
projectMemberSnapshot: WorkReportMemberSnapshot[];
|
||||
supervisorUserId: string;
|
||||
supervisorName: string;
|
||||
periodKey: string;
|
||||
periodLabel: string;
|
||||
periodStartDate: string;
|
||||
periodEndDate: string;
|
||||
flag: number;
|
||||
statusCode: Common.WorkReportStatusCode | string;
|
||||
statusName: string;
|
||||
allowEdit: boolean;
|
||||
terminal: boolean;
|
||||
projectStatusDesc?: string | null;
|
||||
projectProgressPlan?: string | null;
|
||||
projectKeyPoints?: string | null;
|
||||
projectProblems?: string | null;
|
||||
totalWorkHours?: number | string | null;
|
||||
approvalComment?: string | null;
|
||||
lastStatusReason?: string | null;
|
||||
submitTime?: string | null;
|
||||
approvalTime?: string | null;
|
||||
createTime?: string | null;
|
||||
updateTime?: string | null;
|
||||
currentItems: ProjectReportItem[];
|
||||
nextItems: ProjectReportItem[];
|
||||
}
|
||||
|
||||
type ProjectReportSearchParams = Common.WorkReportBaseSearchParams & {
|
||||
projectId?: string | null;
|
||||
flag?: number | null;
|
||||
};
|
||||
|
||||
interface ProjectReportSaveParams {
|
||||
projectId: string;
|
||||
periodKey: string;
|
||||
periodLabel: string;
|
||||
periodStartDate: string;
|
||||
periodEndDate: string;
|
||||
flag: number;
|
||||
projectStatusDesc?: string | null;
|
||||
projectProgressPlan?: string | null;
|
||||
projectKeyPoints?: string | null;
|
||||
projectProblems?: string | null;
|
||||
currentItems: ProjectReportItem[];
|
||||
nextItems: ProjectReportItem[];
|
||||
}
|
||||
|
||||
type ProjectReportDefaultDraftParams = Pick<
|
||||
ProjectReportSaveParams,
|
||||
'periodKey' | 'periodLabel' | 'periodStartDate' | 'periodEndDate' | 'flag'
|
||||
>;
|
||||
}
|
||||
}
|
||||
}
|
||||
12
src/typings/elegant-router.d.ts
vendored
12
src/typings/elegant-router.d.ts
vendored
@@ -36,12 +36,14 @@ declare module "@elegant-router/types" {
|
||||
"personal-center": "/personal-center";
|
||||
"personal-center_my-application": "/personal-center/my-application";
|
||||
"personal-center_my-item": "/personal-center/my-item";
|
||||
"personal-center_my-monthly": "/personal-center/my-monthly";
|
||||
"personal-center_my-performance": "/personal-center/my-performance";
|
||||
"personal-center_my-profile": "/personal-center/my-profile";
|
||||
"personal-center_my-weekly": "/personal-center/my-weekly";
|
||||
"personal-center_overtime-application": "/personal-center/overtime-application";
|
||||
"personal-center_pending-approval": "/personal-center/pending-approval";
|
||||
"personal-center_work-report": "/personal-center/work-report";
|
||||
"personal-center_work-report_monthly": "/personal-center/work-report/monthly";
|
||||
"personal-center_work-report_project": "/personal-center/work-report/project";
|
||||
"personal-center_work-report_weekly": "/personal-center/work-report/weekly";
|
||||
"product": "/product";
|
||||
"product_dashboard": "/product/dashboard";
|
||||
"product_list": "/product/list";
|
||||
@@ -143,12 +145,14 @@ declare module "@elegant-router/types" {
|
||||
| "metrics_worktime"
|
||||
| "personal-center_my-application"
|
||||
| "personal-center_my-item"
|
||||
| "personal-center_my-monthly"
|
||||
| "personal-center_my-performance"
|
||||
| "personal-center_my-profile"
|
||||
| "personal-center_my-weekly"
|
||||
| "personal-center_overtime-application"
|
||||
| "personal-center_pending-approval"
|
||||
| "personal-center_work-report"
|
||||
| "personal-center_work-report_monthly"
|
||||
| "personal-center_work-report_project"
|
||||
| "personal-center_work-report_weekly"
|
||||
| "product_dashboard"
|
||||
| "product_list"
|
||||
| "product_requirement"
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
<template>
|
||||
<LookForward title="我的月报" subtitle="功能建设中,敬请期待" />
|
||||
</template>
|
||||
@@ -1,3 +0,0 @@
|
||||
<template>
|
||||
<LookForward title="我的周报" subtitle="功能建设中,敬请期待" />
|
||||
</template>
|
||||
@@ -1,20 +1,14 @@
|
||||
<script setup lang="tsx">
|
||||
import { computed, markRaw, reactive, ref } from 'vue';
|
||||
import { ElButton, ElMessageBox, ElTag } from 'element-plus';
|
||||
import { ElButton, ElTag } from 'element-plus';
|
||||
import dayjs from 'dayjs';
|
||||
import {
|
||||
fetchCancelOvertimeApplication,
|
||||
fetchDeleteOvertimeApplication,
|
||||
fetchExportOvertimeApplications,
|
||||
fetchGetOvertimeApplicationPage
|
||||
} from '@/service/api';
|
||||
import { fetchExportOvertimeApplications, fetchGetOvertimeApplicationPage } from '@/service/api';
|
||||
import { useUIPaginatedTable } from '@/hooks/common/table';
|
||||
import BusinessTableActionCell, { type BusinessTableAction } from '@/components/custom/business-table-action-cell';
|
||||
import OvertimeApplicationActionDialog from './modules/overtime-application-action-dialog.vue';
|
||||
import OvertimeApplicationApprovalRecordDialog from './modules/overtime-application-approval-record-dialog.vue';
|
||||
import OvertimeApplicationDetailDialog from './modules/overtime-application-detail-dialog.vue';
|
||||
import OvertimeApplicationOperateDialog from './modules/overtime-application-operate-dialog.vue';
|
||||
import OvertimeApplicationSearch from './modules/overtime-application-search.vue';
|
||||
import OvertimeApplicationStatusLogDialog from './modules/overtime-application-status-log-dialog.vue';
|
||||
import {
|
||||
downloadBlob,
|
||||
formatEmptyText,
|
||||
@@ -23,16 +17,13 @@ import {
|
||||
getOvertimeApplicationStatusLabel,
|
||||
resolveOvertimeApplicationStatusTagType
|
||||
} from './modules/overtime-application-shared';
|
||||
import IconMdiCloseCircleOutline from '~icons/mdi/close-circle-outline';
|
||||
import IconMdiDeleteOutline from '~icons/mdi/delete-outline';
|
||||
import IconMdiEyeOutline from '~icons/mdi/eye-outline';
|
||||
import IconMdiHistory from '~icons/mdi/history';
|
||||
import IconMdiFileDocumentCheckOutline from '~icons/mdi/file-document-check-outline';
|
||||
import IconMdiPencilOutline from '~icons/mdi/pencil-outline';
|
||||
|
||||
defineOptions({ name: 'OvertimeApplication' });
|
||||
|
||||
type OvertimeApplicationPageResponse = Awaited<ReturnType<typeof fetchGetOvertimeApplicationPage>>;
|
||||
type ActionType = 'cancel';
|
||||
|
||||
function getInitSearchParams(): Api.OvertimeApplication.OvertimeApplicationSearchParams {
|
||||
return {
|
||||
@@ -69,20 +60,15 @@ function transformPageResult(response: OvertimeApplicationPageResponse, pageNo:
|
||||
const searchParams = reactive(getInitSearchParams());
|
||||
const operateVisible = ref(false);
|
||||
const detailVisible = ref(false);
|
||||
const statusLogVisible = ref(false);
|
||||
const actionVisible = ref(false);
|
||||
const approvalRecordVisible = ref(false);
|
||||
const operateType = ref<'add' | 'edit'>('add');
|
||||
const currentRow = ref<Api.OvertimeApplication.OvertimeApplication | null>(null);
|
||||
const currentActionType = ref<ActionType>('cancel');
|
||||
const actionSubmitting = ref(false);
|
||||
const exporting = ref(false);
|
||||
|
||||
const ACTION_ICON_MAP = {
|
||||
detail: markRaw(IconMdiEyeOutline),
|
||||
statusLog: markRaw(IconMdiHistory),
|
||||
edit: markRaw(IconMdiPencilOutline),
|
||||
cancel: markRaw(IconMdiCloseCircleOutline),
|
||||
delete: markRaw(IconMdiDeleteOutline)
|
||||
approvalRecord: markRaw(IconMdiFileDocumentCheckOutline),
|
||||
edit: markRaw(IconMdiPencilOutline)
|
||||
};
|
||||
|
||||
const { columns, columnChecks, data, loading, getDataByPage, mobilePagination } = useUIPaginatedTable<
|
||||
@@ -113,14 +99,14 @@ const { columns, columnChecks, data, loading, getDataByPage, mobilePagination }
|
||||
prop: 'overtimeReason',
|
||||
label: '加班原因',
|
||||
minWidth: 180,
|
||||
showOverflowTooltip: true,
|
||||
className: 'overtime-application__cell-ellipsis',
|
||||
formatter: row => formatEmptyText(row.overtimeReason)
|
||||
},
|
||||
{
|
||||
prop: 'overtimeContent',
|
||||
label: '加班内容',
|
||||
minWidth: 200,
|
||||
showOverflowTooltip: true,
|
||||
className: 'overtime-application__cell-ellipsis',
|
||||
formatter: row => formatEmptyText(row.overtimeContent)
|
||||
},
|
||||
{
|
||||
@@ -134,17 +120,17 @@ const { columns, columnChecks, data, loading, getDataByPage, mobilePagination }
|
||||
</ElTag>
|
||||
)
|
||||
},
|
||||
{ prop: 'approverName', label: '审核人', minWidth: 120, showOverflowTooltip: true },
|
||||
{ prop: 'approverName', label: '审核人', minWidth: 80, showOverflowTooltip: true },
|
||||
{
|
||||
prop: 'submitTime',
|
||||
label: '提交时间',
|
||||
minWidth: 170,
|
||||
minWidth: 150,
|
||||
formatter: row => formatOvertimeDateTime(row.submitTime)
|
||||
},
|
||||
{
|
||||
prop: 'approvalTime',
|
||||
label: '审核时间',
|
||||
minWidth: 170,
|
||||
minWidth: 150,
|
||||
formatter: row => formatOvertimeDateTime(row.approvalTime)
|
||||
},
|
||||
{
|
||||
@@ -171,7 +157,7 @@ function getRowActions(row: Api.OvertimeApplication.OvertimeApplication): Busine
|
||||
}
|
||||
];
|
||||
|
||||
if ((row.statusCode === 'rejected' || row.statusCode === 'cancelled') && row.allowEdit) {
|
||||
if (row.statusCode === 'rejected' && row.allowEdit) {
|
||||
actions.push({
|
||||
key: 'edit',
|
||||
label: '修改',
|
||||
@@ -181,31 +167,13 @@ function getRowActions(row: Api.OvertimeApplication.OvertimeApplication): Busine
|
||||
});
|
||||
}
|
||||
|
||||
if (['approved', 'rejected'].includes(row.statusCode)) {
|
||||
actions.push({
|
||||
key: 'status-log',
|
||||
label: '状态日志',
|
||||
key: 'approval-record',
|
||||
label: '审批记录',
|
||||
buttonType: 'info',
|
||||
icon: ACTION_ICON_MAP.statusLog,
|
||||
onClick: () => openStatusLog(row)
|
||||
});
|
||||
|
||||
if (row.statusCode === 'pending') {
|
||||
actions.push({
|
||||
key: 'cancel',
|
||||
label: '撤销',
|
||||
buttonType: 'danger',
|
||||
icon: ACTION_ICON_MAP.cancel,
|
||||
onClick: () => openCancel(row)
|
||||
});
|
||||
}
|
||||
|
||||
if (row.statusCode === 'cancelled') {
|
||||
actions.push({
|
||||
key: 'delete',
|
||||
label: '删除',
|
||||
buttonType: 'danger',
|
||||
icon: ACTION_ICON_MAP.delete,
|
||||
onClick: () => handleDelete(row)
|
||||
icon: ACTION_ICON_MAP.approvalRecord,
|
||||
onClick: () => openApprovalRecord(row)
|
||||
});
|
||||
}
|
||||
|
||||
@@ -229,15 +197,9 @@ function openDetail(row: Api.OvertimeApplication.OvertimeApplication) {
|
||||
detailVisible.value = true;
|
||||
}
|
||||
|
||||
function openStatusLog(row: Api.OvertimeApplication.OvertimeApplication) {
|
||||
function openApprovalRecord(row: Api.OvertimeApplication.OvertimeApplication) {
|
||||
currentRow.value = row;
|
||||
statusLogVisible.value = true;
|
||||
}
|
||||
|
||||
function openCancel(row: Api.OvertimeApplication.OvertimeApplication) {
|
||||
currentRow.value = row;
|
||||
currentActionType.value = 'cancel';
|
||||
actionVisible.value = true;
|
||||
approvalRecordVisible.value = true;
|
||||
}
|
||||
|
||||
async function reloadTable(page = searchParams.pageNo ?? 1) {
|
||||
@@ -259,49 +221,6 @@ function handleSubmitted() {
|
||||
reloadTable(searchParams.pageNo ?? 1);
|
||||
}
|
||||
|
||||
async function handleActionSubmit(reason: string | null) {
|
||||
if (!currentRow.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
actionSubmitting.value = true;
|
||||
const { error } = await fetchCancelOvertimeApplication(currentRow.value.id, { reason });
|
||||
actionSubmitting.value = false;
|
||||
|
||||
if (error) {
|
||||
return;
|
||||
}
|
||||
|
||||
actionVisible.value = false;
|
||||
window.$message?.success('加班申请已撤销');
|
||||
await reloadTable(searchParams.pageNo ?? 1);
|
||||
}
|
||||
|
||||
async function handleDelete(row: Api.OvertimeApplication.OvertimeApplication) {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确定删除 ${row.applicantName} ${formatOvertimeDate(row.overtimeDate)} 的加班申请吗?`,
|
||||
'删除确认',
|
||||
{
|
||||
type: 'warning',
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消'
|
||||
}
|
||||
);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
const { error } = await fetchDeleteOvertimeApplication(row.id);
|
||||
|
||||
if (error) {
|
||||
return;
|
||||
}
|
||||
|
||||
window.$message?.success('加班申请已删除');
|
||||
await reloadTable(searchParams.pageNo ?? 1);
|
||||
}
|
||||
|
||||
async function handleExport() {
|
||||
exporting.value = true;
|
||||
const { error, data: blob } = await fetchExportOvertimeApplications(searchParams);
|
||||
@@ -373,14 +292,7 @@ async function handleExport() {
|
||||
|
||||
<OvertimeApplicationDetailDialog v-model:visible="detailVisible" :row-data="currentRow" />
|
||||
|
||||
<OvertimeApplicationStatusLogDialog v-model:visible="statusLogVisible" :row-data="currentRow" />
|
||||
|
||||
<OvertimeApplicationActionDialog
|
||||
v-model:visible="actionVisible"
|
||||
:action-type="currentActionType"
|
||||
:loading="actionSubmitting"
|
||||
@submit="handleActionSubmit"
|
||||
/>
|
||||
<OvertimeApplicationApprovalRecordDialog v-model:visible="approvalRecordVisible" :row-data="currentRow" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -398,4 +310,12 @@ async function handleExport() {
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* 加班原因/加班内容:单元格内容溢出时仅显示省略号,不弹出 tooltip */
|
||||
:deep(.overtime-application__cell-ellipsis .cell) {
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -5,7 +5,7 @@ import BusinessFormDialog from '@/components/custom/business-form-dialog.vue';
|
||||
|
||||
defineOptions({ name: 'OvertimeApplicationActionDialog' });
|
||||
|
||||
type ActionType = 'approve' | 'reject' | 'cancel';
|
||||
type ActionType = 'approve' | 'reject';
|
||||
|
||||
interface Props {
|
||||
actionType: ActionType;
|
||||
@@ -34,8 +34,7 @@ const model = reactive({
|
||||
const title = computed(() => {
|
||||
const map: Record<ActionType, string> = {
|
||||
approve: '通过加班申请',
|
||||
reject: '退回加班申请',
|
||||
cancel: '撤销加班申请'
|
||||
reject: '退回加班申请'
|
||||
};
|
||||
|
||||
return map[props.actionType];
|
||||
@@ -44,8 +43,7 @@ const title = computed(() => {
|
||||
const reasonLabel = computed(() => {
|
||||
const map: Record<ActionType, string> = {
|
||||
approve: '审核意见',
|
||||
reject: '退回原因',
|
||||
cancel: '撤销原因'
|
||||
reject: '退回原因'
|
||||
};
|
||||
|
||||
return map[props.actionType];
|
||||
@@ -58,7 +56,7 @@ const reasonPlaceholder = computed(() => {
|
||||
return `请输入${reasonLabel.value}`;
|
||||
}
|
||||
|
||||
return props.actionType === 'cancel' ? '可填写撤销原因' : '可填写审核意见';
|
||||
return '可填写审核意见';
|
||||
});
|
||||
|
||||
const rules = computed(() => ({
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue';
|
||||
import { fetchGetOvertimeApplicationApprovalRecords } from '@/service/api';
|
||||
import BusinessFormDialog from '@/components/custom/business-form-dialog.vue';
|
||||
import {
|
||||
formatEmptyText,
|
||||
formatOvertimeDateTime,
|
||||
getOvertimeApplicationStatusLabel
|
||||
} from './overtime-application-shared';
|
||||
|
||||
defineOptions({ name: 'OvertimeApplicationApprovalRecordDialog' });
|
||||
|
||||
interface Props {
|
||||
rowData?: Api.OvertimeApplication.OvertimeApplication | null;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
|
||||
const visible = defineModel<boolean>('visible', {
|
||||
default: false
|
||||
});
|
||||
|
||||
const loading = ref(false);
|
||||
const records = ref<Api.OvertimeApplication.OvertimeApplicationApprovalRecord[]>([]);
|
||||
|
||||
async function loadRecords() {
|
||||
if (!props.rowData?.id) {
|
||||
records.value = [];
|
||||
return;
|
||||
}
|
||||
|
||||
loading.value = true;
|
||||
const { error, data } = await fetchGetOvertimeApplicationApprovalRecords(props.rowData.id);
|
||||
loading.value = false;
|
||||
|
||||
records.value = error || !data ? [] : data;
|
||||
}
|
||||
|
||||
watch(
|
||||
() => visible.value,
|
||||
value => {
|
||||
if (value) {
|
||||
loadRecords();
|
||||
}
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BusinessFormDialog
|
||||
v-model="visible"
|
||||
title="加班申请审批记录"
|
||||
width="820px"
|
||||
:loading="loading"
|
||||
:show-footer="false"
|
||||
max-body-height="72vh"
|
||||
>
|
||||
<ElTable border :data="records">
|
||||
<ElTableColumn prop="approvalRound" label="轮次" width="80" />
|
||||
<ElTableColumn label="结论" width="110">
|
||||
<template #default="{ row }">{{ getOvertimeApplicationStatusLabel(row.conclusion) }}</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="审批意见" min-width="240" show-overflow-tooltip>
|
||||
<template #default="{ row }">{{ formatEmptyText(row.opinion) }}</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn prop="auditorName" label="审批人" width="130" show-overflow-tooltip />
|
||||
<ElTableColumn label="审批时间" width="170">
|
||||
<template #default="{ row }">{{ formatOvertimeDateTime(row.createTime) }}</template>
|
||||
</ElTableColumn>
|
||||
</ElTable>
|
||||
</BusinessFormDialog>
|
||||
</template>
|
||||
@@ -1,22 +1,28 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { ref, watch } from 'vue';
|
||||
import { fetchGetOvertimeApplicationDetail } from '@/service/api';
|
||||
import BusinessFormDialog from '@/components/custom/business-form-dialog.vue';
|
||||
import {
|
||||
formatEmptyText,
|
||||
formatOvertimeDate,
|
||||
formatOvertimeDateTime,
|
||||
getOvertimeApplicationStatusLabel,
|
||||
resolveOvertimeApplicationStatusTagType
|
||||
} from './overtime-application-shared';
|
||||
import { formatOvertimeDate, formatOvertimeDateTime } from './overtime-application-shared';
|
||||
import IconMdiCheckCircleOutline from '~icons/mdi/check-circle-outline';
|
||||
import IconMdiCloseCircleOutline from '~icons/mdi/close-circle-outline';
|
||||
|
||||
defineOptions({ name: 'OvertimeApplicationDetailDialog' });
|
||||
|
||||
interface Props {
|
||||
rowData?: Api.OvertimeApplication.OvertimeApplication | null;
|
||||
showApprovalActions?: boolean;
|
||||
actionLoading?: boolean;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
showApprovalActions: false,
|
||||
actionLoading: false
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
approve: [];
|
||||
reject: [];
|
||||
}>();
|
||||
|
||||
const visible = defineModel<boolean>('visible', {
|
||||
default: false
|
||||
@@ -25,11 +31,6 @@ const visible = defineModel<boolean>('visible', {
|
||||
const loading = ref(false);
|
||||
const detailData = ref<Api.OvertimeApplication.OvertimeApplication | null>(null);
|
||||
|
||||
const statusTagType = computed(() => resolveOvertimeApplicationStatusTagType(detailData.value?.statusCode));
|
||||
const statusLabel = computed(() =>
|
||||
getOvertimeApplicationStatusLabel(detailData.value?.statusCode, detailData.value?.statusName)
|
||||
);
|
||||
|
||||
async function loadDetail() {
|
||||
if (!props.rowData?.id) {
|
||||
detailData.value = null;
|
||||
@@ -54,30 +55,96 @@ watch(
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BusinessFormDialog v-model="visible" title="加班申请详情" preset="md" :loading="loading" :show-footer="false">
|
||||
<ElDescriptions v-if="detailData" :column="2" border>
|
||||
<ElDescriptionsItem label="状态">
|
||||
<ElTag :type="statusTagType">{{ statusLabel }}</ElTag>
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="申请人">
|
||||
<BusinessFormDialog
|
||||
v-model="visible"
|
||||
title="加班申请详情"
|
||||
preset="md"
|
||||
:loading="loading"
|
||||
:show-footer="props.showApprovalActions"
|
||||
>
|
||||
<ElDescriptions v-if="detailData" class="overtime-application-detail-dialog__descriptions" :column="2" border>
|
||||
<ElDescriptionsItem label="申请人" label-class-name="overtime-application-detail-dialog__label">
|
||||
{{ detailData.applicantName }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="加班日期">{{ formatOvertimeDate(detailData.overtimeDate) }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="加班时长">{{ detailData.overtimeDuration }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="审核人">
|
||||
{{ detailData.approverName }}
|
||||
<ElDescriptionsItem label="加班日期" label-class-name="overtime-application-detail-dialog__label--compact">
|
||||
{{ formatOvertimeDate(detailData.overtimeDate) }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="加班时长" label-class-name="overtime-application-detail-dialog__label">
|
||||
{{ detailData.overtimeDuration }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="提交时间" label-class-name="overtime-application-detail-dialog__label--compact">
|
||||
{{ formatOvertimeDateTime(detailData.submitTime) }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="加班原因" :span="2" label-class-name="overtime-application-detail-dialog__label">
|
||||
{{ detailData.overtimeReason }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="加班内容" :span="2" label-class-name="overtime-application-detail-dialog__label">
|
||||
{{ detailData.overtimeContent }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="提交时间">{{ formatOvertimeDateTime(detailData.submitTime) }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="审核时间">{{ formatOvertimeDateTime(detailData.approvalTime) }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="审核意见">{{ formatEmptyText(detailData.approvalComment) }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="加班原因" :span="2">{{ detailData.overtimeReason }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="加班内容" :span="2">{{ detailData.overtimeContent }}</ElDescriptionsItem>
|
||||
</ElDescriptions>
|
||||
<ElEmpty v-else description="未获取到加班申请详情" />
|
||||
|
||||
<template #footer>
|
||||
<div class="overtime-application-detail-dialog__footer">
|
||||
<ElButton
|
||||
class="overtime-application-detail-dialog__approve-btn"
|
||||
type="success"
|
||||
:loading="props.actionLoading"
|
||||
:disabled="props.actionLoading || !detailData"
|
||||
@click="emit('approve')"
|
||||
>
|
||||
<template #icon>
|
||||
<IconMdiCheckCircleOutline />
|
||||
</template>
|
||||
通过
|
||||
</ElButton>
|
||||
<ElButton type="danger" plain :disabled="props.actionLoading || !detailData" @click="emit('reject')">
|
||||
<template #icon>
|
||||
<IconMdiCloseCircleOutline />
|
||||
</template>
|
||||
退回
|
||||
</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
</BusinessFormDialog>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.overtime-application-detail-dialog__footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.overtime-application-detail-dialog__approve-btn {
|
||||
--el-button-bg-color: #0f766e;
|
||||
--el-button-border-color: #0f766e;
|
||||
--el-button-hover-bg-color: #115e59;
|
||||
--el-button-hover-border-color: #115e59;
|
||||
--el-button-active-bg-color: #134e4a;
|
||||
--el-button-active-border-color: #134e4a;
|
||||
}
|
||||
|
||||
:deep(.overtime-application-detail-dialog__descriptions .el-descriptions__cell) {
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
:deep(.overtime-application-detail-dialog__label),
|
||||
:deep(.overtime-application-detail-dialog__label--compact) {
|
||||
white-space: nowrap;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
:deep(.overtime-application-detail-dialog__label) {
|
||||
width: 96px;
|
||||
min-width: 96px;
|
||||
}
|
||||
|
||||
:deep(.overtime-application-detail-dialog__label--compact) {
|
||||
width: 86px;
|
||||
min-width: 86px;
|
||||
}
|
||||
|
||||
:deep(.overtime-application-detail-dialog__readonly-input .el-input__wrapper) {
|
||||
background: linear-gradient(180deg, rgb(241 245 249 / 98%), rgb(226 232 240 / 94%)), rgb(241 245 249);
|
||||
box-shadow: 0 0 0 1px rgb(203 213 225 / 96%) inset;
|
||||
|
||||
@@ -7,16 +7,14 @@ export const overtimeApplicationStatusOptions: Array<{
|
||||
}> = [
|
||||
{ label: '待审批', value: 'pending' },
|
||||
{ label: '已通过', value: 'approved' },
|
||||
{ label: '已退回', value: 'rejected' },
|
||||
{ label: '已撤销', value: 'cancelled' }
|
||||
{ label: '已退回', value: 'rejected' }
|
||||
];
|
||||
|
||||
export const overtimeApplicationActionNameMap: Record<Api.OvertimeApplication.OvertimeApplicationActionType, string> = {
|
||||
submit: '提交',
|
||||
resubmit: '重新提交',
|
||||
approve: '通过',
|
||||
reject: '退回',
|
||||
cancel: '撤销'
|
||||
reject: '退回'
|
||||
};
|
||||
|
||||
export function getOvertimeApplicationStatusLabel(statusCode?: string | null, statusName?: string | null) {
|
||||
|
||||
@@ -1,89 +0,0 @@
|
||||
<script setup lang="tsx">
|
||||
import { ref, watch } from 'vue';
|
||||
import { ElTag } from 'element-plus';
|
||||
import { fetchGetOvertimeApplicationStatusLogs } from '@/service/api';
|
||||
import BusinessFormDialog from '@/components/custom/business-form-dialog.vue';
|
||||
import {
|
||||
formatEmptyText,
|
||||
formatOvertimeDate,
|
||||
formatOvertimeDateTime,
|
||||
getOvertimeApplicationActionLabel,
|
||||
getOvertimeApplicationStatusLabel,
|
||||
resolveOvertimeApplicationStatusTagType
|
||||
} from './overtime-application-shared';
|
||||
|
||||
defineOptions({ name: 'OvertimeApplicationStatusLogDialog' });
|
||||
|
||||
interface Props {
|
||||
rowData?: Api.OvertimeApplication.OvertimeApplication | null;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
|
||||
const visible = defineModel<boolean>('visible', {
|
||||
default: false
|
||||
});
|
||||
|
||||
const loading = ref(false);
|
||||
const logs = ref<Api.OvertimeApplication.OvertimeApplicationStatusLog[]>([]);
|
||||
|
||||
async function loadLogs() {
|
||||
if (!props.rowData?.id) {
|
||||
logs.value = [];
|
||||
return;
|
||||
}
|
||||
|
||||
loading.value = true;
|
||||
const { error, data } = await fetchGetOvertimeApplicationStatusLogs(props.rowData.id);
|
||||
loading.value = false;
|
||||
|
||||
logs.value = error || !data ? [] : data;
|
||||
}
|
||||
|
||||
function renderStatus(code?: string | null) {
|
||||
if (!code) {
|
||||
return '--';
|
||||
}
|
||||
|
||||
return <ElTag type={resolveOvertimeApplicationStatusTagType(code)}>{getOvertimeApplicationStatusLabel(code)}</ElTag>;
|
||||
}
|
||||
|
||||
watch(
|
||||
() => visible.value,
|
||||
value => {
|
||||
if (value) {
|
||||
loadLogs();
|
||||
}
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BusinessFormDialog
|
||||
v-model="visible"
|
||||
title="状态日志"
|
||||
width="920px"
|
||||
:loading="loading"
|
||||
:show-footer="false"
|
||||
max-body-height="72vh"
|
||||
>
|
||||
<ElTable border :data="logs">
|
||||
<ElTableColumn prop="createTime" label="操作时间" width="170">
|
||||
<template #default="{ row }">{{ formatOvertimeDateTime(row.createTime) }}</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn prop="actionType" label="动作" width="110">
|
||||
<template #default="{ row }">{{ getOvertimeApplicationActionLabel(row.actionType) }}</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn prop="operatorName" label="操作人" width="120" show-overflow-tooltip />
|
||||
<ElTableColumn prop="fromStatus" label="原状态" width="110" :formatter="row => renderStatus(row.fromStatus)" />
|
||||
<ElTableColumn prop="toStatus" label="新状态" width="110" :formatter="row => renderStatus(row.toStatus)" />
|
||||
<ElTableColumn prop="reason" label="原因/意见" min-width="180" show-overflow-tooltip>
|
||||
<template #default="{ row }">{{ formatEmptyText(row.reason) }}</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn prop="overtimeDateSnapshot" label="加班日期" width="120">
|
||||
<template #default="{ row }">{{ formatOvertimeDate(row.overtimeDateSnapshot) }}</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn prop="overtimeDurationSnapshot" label="时长" width="90" show-overflow-tooltip />
|
||||
</ElTable>
|
||||
</BusinessFormDialog>
|
||||
</template>
|
||||
241
src/views/personal-center/work-report/index.vue
Normal file
241
src/views/personal-center/work-report/index.vue
Normal file
@@ -0,0 +1,241 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import { onBeforeRouteLeave } from 'vue-router';
|
||||
import { fetchGetProjectReportOwnerProjectOptions } from '@/service/api';
|
||||
import { useAuth } from '@/hooks/business/auth';
|
||||
import WorkReportCreateDialog from './shared/components/create-dialog.vue';
|
||||
import WorkReportPrototypePageDialog from './shared/components/prototype-page-dialog.vue';
|
||||
import WorkReportTabs from './shared/components/tabs.vue';
|
||||
import {
|
||||
WORK_REPORT_PROJECT_OWNER_PERMISSION,
|
||||
WORK_REPORT_TYPE_LABEL,
|
||||
type WorkReportRow,
|
||||
type WorkReportType
|
||||
} from './shared/types';
|
||||
import WeeklyReportIndex from './weekly/index.vue';
|
||||
import WeeklyReportApprovalRecordDialog from './weekly/modules/approval-record-dialog.vue';
|
||||
import MonthlyReportIndex from './monthly/index.vue';
|
||||
import MonthlyReportApprovalRecordDialog from './monthly/modules/approval-record-dialog.vue';
|
||||
import ProjectReportIndex from './project/index.vue';
|
||||
import ProjectReportApprovalRecordDialog from './project/modules/approval-record-dialog.vue';
|
||||
|
||||
defineOptions({ name: 'PersonalCenterWorkReport' });
|
||||
|
||||
type PageDialogMode = 'add' | 'edit' | 'detail';
|
||||
type ReportListExpose = {
|
||||
reload: (page?: number) => Promise<void>;
|
||||
};
|
||||
|
||||
const { hasAuth } = useAuth();
|
||||
|
||||
const activeTab = ref<WorkReportType>('weekly');
|
||||
const createVisible = ref(false);
|
||||
const pageDialogVisible = ref(false);
|
||||
const pageDialogMode = ref<PageDialogMode>('detail');
|
||||
const approvalRecordVisible = ref(false);
|
||||
const currentReportType = ref<WorkReportType>('weekly');
|
||||
const currentRow = ref<WorkReportRow | null>(null);
|
||||
const initialPeriod = ref<{
|
||||
periodKey: string;
|
||||
periodLabel: string;
|
||||
periodStartDate: string;
|
||||
periodEndDate: string;
|
||||
} | null>(null);
|
||||
const initialProjectId = ref('');
|
||||
const initialFlag = ref(1);
|
||||
const projectOptions = ref<Api.WorkReport.Project.ProjectReportOwnerProjectOption[]>([]);
|
||||
|
||||
const weeklyRef = ref<ReportListExpose | null>(null);
|
||||
const monthlyRef = ref<ReportListExpose | null>(null);
|
||||
const projectRef = ref<ReportListExpose | null>(null);
|
||||
|
||||
const canShowProjectTab = computed(() => hasAuth(WORK_REPORT_PROJECT_OWNER_PERMISSION));
|
||||
|
||||
/** 项目选项是否加载成功(用于项目半月报列表内部判断) */
|
||||
const projectOptionsLoaded = ref(false);
|
||||
|
||||
const visibleTabs = computed<Array<{ label: string; name: WorkReportType }>>(() => {
|
||||
const tabs: Array<{ label: string; name: WorkReportType }> = [
|
||||
{ label: WORK_REPORT_TYPE_LABEL.weekly, name: 'weekly' },
|
||||
{ label: WORK_REPORT_TYPE_LABEL.monthly, name: 'monthly' }
|
||||
];
|
||||
|
||||
if (canShowProjectTab.value) {
|
||||
tabs.push({ label: WORK_REPORT_TYPE_LABEL.project, name: 'project' });
|
||||
}
|
||||
|
||||
return tabs;
|
||||
});
|
||||
|
||||
const currentApprovalRecordDialogComponent = computed(() => {
|
||||
if (currentReportType.value === 'monthly') return MonthlyReportApprovalRecordDialog;
|
||||
if (currentReportType.value === 'project') return ProjectReportApprovalRecordDialog;
|
||||
return WeeklyReportApprovalRecordDialog;
|
||||
});
|
||||
|
||||
function getListRef(reportType: WorkReportType) {
|
||||
if (reportType === 'monthly') return monthlyRef.value;
|
||||
if (reportType === 'project') return projectRef.value;
|
||||
return weeklyRef.value;
|
||||
}
|
||||
|
||||
async function loadProjectOptions() {
|
||||
if (!canShowProjectTab.value) return;
|
||||
|
||||
const { error, data } = await fetchGetProjectReportOwnerProjectOptions();
|
||||
projectOptions.value = error || !data ? [] : data;
|
||||
projectOptionsLoaded.value = !error;
|
||||
}
|
||||
|
||||
function openCreate(reportType: WorkReportType) {
|
||||
currentReportType.value = reportType;
|
||||
createVisible.value = true;
|
||||
}
|
||||
|
||||
function handleCreateConfirm(
|
||||
payload:
|
||||
| { reportType: 'weekly' | 'monthly'; period: typeof initialPeriod.value extends infer T ? T : never }
|
||||
| {
|
||||
reportType: 'project';
|
||||
projectId: string;
|
||||
flag: number;
|
||||
period: typeof initialPeriod.value extends infer T ? T : never;
|
||||
}
|
||||
) {
|
||||
currentReportType.value = payload.reportType;
|
||||
pageDialogMode.value = 'add';
|
||||
currentRow.value = null;
|
||||
initialPeriod.value = payload.period as typeof initialPeriod.value;
|
||||
initialProjectId.value = 'projectId' in payload ? payload.projectId : '';
|
||||
initialFlag.value = 'flag' in payload ? payload.flag : 1;
|
||||
pageDialogVisible.value = true;
|
||||
}
|
||||
|
||||
function openEdit(reportType: WorkReportType, row: WorkReportRow) {
|
||||
currentReportType.value = reportType;
|
||||
pageDialogMode.value = 'edit';
|
||||
currentRow.value = row;
|
||||
initialPeriod.value = null;
|
||||
pageDialogVisible.value = true;
|
||||
}
|
||||
|
||||
function openDetail(reportType: WorkReportType, row: WorkReportRow) {
|
||||
currentReportType.value = reportType;
|
||||
pageDialogMode.value = 'detail';
|
||||
currentRow.value = row;
|
||||
initialPeriod.value = null;
|
||||
pageDialogVisible.value = true;
|
||||
}
|
||||
|
||||
function openApprovalRecord(reportType: WorkReportType, row: WorkReportRow) {
|
||||
currentReportType.value = reportType;
|
||||
currentRow.value = row;
|
||||
approvalRecordVisible.value = true;
|
||||
}
|
||||
|
||||
function handleTabChange(tab: WorkReportType) {
|
||||
activeTab.value = tab;
|
||||
getListRef(tab)?.reload(1);
|
||||
}
|
||||
|
||||
async function reloadReport(reportType = currentReportType.value) {
|
||||
await getListRef(reportType)?.reload();
|
||||
}
|
||||
|
||||
function handleSubmitted() {
|
||||
pageDialogVisible.value = false;
|
||||
reloadReport(currentReportType.value);
|
||||
}
|
||||
|
||||
function closeFloatingPanels() {
|
||||
createVisible.value = false;
|
||||
pageDialogVisible.value = false;
|
||||
approvalRecordVisible.value = false;
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await loadProjectOptions();
|
||||
});
|
||||
|
||||
onBeforeRouteLeave(() => {
|
||||
closeFloatingPanels();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="work-report-page-shell min-h-560px gap-16px overflow-hidden xl:grid xl:grid-cols-[240px_minmax(0,1fr)] lt-xl:flex lt-xl:flex-col lt-xl:overflow-auto"
|
||||
>
|
||||
<!-- 左侧:报告类型导航 -->
|
||||
<div class="flex-col-stretch gap-16px xl:min-h-0">
|
||||
<WorkReportTabs :active-tab="activeTab" :tabs="visibleTabs" @update:active-tab="handleTabChange" />
|
||||
</div>
|
||||
|
||||
<!-- 右侧:搜索区 + 列表区 -->
|
||||
<div class="flex-col-stretch gap-16px xl:min-h-0">
|
||||
<WeeklyReportIndex
|
||||
v-show="activeTab === 'weekly'"
|
||||
ref="weeklyRef"
|
||||
class="flex-1-hidden"
|
||||
@create="openCreate('weekly')"
|
||||
@edit="openEdit('weekly', $event)"
|
||||
@detail="openDetail('weekly', $event)"
|
||||
@approval-record="openApprovalRecord('weekly', $event)"
|
||||
/>
|
||||
|
||||
<MonthlyReportIndex
|
||||
v-show="activeTab === 'monthly'"
|
||||
ref="monthlyRef"
|
||||
class="flex-1-hidden"
|
||||
@create="openCreate('monthly')"
|
||||
@edit="openEdit('monthly', $event)"
|
||||
@detail="openDetail('monthly', $event)"
|
||||
@approval-record="openApprovalRecord('monthly', $event)"
|
||||
/>
|
||||
|
||||
<ProjectReportIndex
|
||||
v-if="canShowProjectTab"
|
||||
v-show="activeTab === 'project'"
|
||||
ref="projectRef"
|
||||
class="flex-1-hidden"
|
||||
:project-options="projectOptions"
|
||||
:project-options-loaded="projectOptionsLoaded"
|
||||
@create="openCreate('project')"
|
||||
@edit="openEdit('project', $event)"
|
||||
@detail="openDetail('project', $event)"
|
||||
@approval-record="openApprovalRecord('project', $event)"
|
||||
/>
|
||||
</div>
|
||||
<WorkReportCreateDialog
|
||||
v-model:visible="createVisible"
|
||||
:default-report-type="currentReportType"
|
||||
:project-visible="canShowProjectTab"
|
||||
:project-options="projectOptions"
|
||||
@confirm="handleCreateConfirm"
|
||||
/>
|
||||
|
||||
<WorkReportPrototypePageDialog
|
||||
v-model:visible="pageDialogVisible"
|
||||
:mode="pageDialogMode"
|
||||
scene="fill"
|
||||
:report-type="currentReportType"
|
||||
:row-data="currentRow"
|
||||
:initial-period="initialPeriod"
|
||||
:initial-project-id="initialProjectId"
|
||||
:initial-flag="initialFlag"
|
||||
@submitted="handleSubmitted"
|
||||
/>
|
||||
|
||||
<component
|
||||
:is="currentApprovalRecordDialogComponent"
|
||||
v-model:visible="approvalRecordVisible"
|
||||
:row-data="currentRow"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.work-report-page-shell {
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
345
src/views/personal-center/work-report/monthly/index.vue
Normal file
345
src/views/personal-center/work-report/monthly/index.vue
Normal file
@@ -0,0 +1,345 @@
|
||||
<script setup lang="tsx">
|
||||
/* eslint-disable no-void */
|
||||
import { markRaw, reactive, ref } from 'vue';
|
||||
import { ElMessageBox, ElTag } from 'element-plus';
|
||||
import {
|
||||
fetchDeleteMonthlyReport,
|
||||
fetchExportMonthlyReportContent,
|
||||
fetchGetMonthlyReportPage,
|
||||
fetchSubmitMonthlyReport
|
||||
} from '@/service/api';
|
||||
import { useAuth } from '@/hooks/business/auth';
|
||||
import { useUIPaginatedTable } from '@/hooks/common/table';
|
||||
import BusinessTableActionCell, { type BusinessTableAction } from '@/components/custom/business-table-action-cell';
|
||||
import {
|
||||
WORK_REPORT_TYPE_LABEL,
|
||||
type WorkReportRow,
|
||||
createMonthlySearchParams,
|
||||
createWorkReportContentExportFallbackName,
|
||||
downloadBlob,
|
||||
formatDateTime,
|
||||
formatEmptyText,
|
||||
formatPeriod,
|
||||
getWorkReportStatusLabel,
|
||||
resolveExportFilename,
|
||||
resolveWorkReportStatusTagType,
|
||||
transformWorkReportPage
|
||||
} from '../shared/types';
|
||||
import MonthlyReportSearch from './modules/search-panel.vue';
|
||||
import IconMdiDeleteOutline from '~icons/mdi/delete-outline';
|
||||
import IconMdiEyeOutline from '~icons/mdi/eye-outline';
|
||||
import IconMdiFileDocumentCheckOutline from '~icons/mdi/file-document-check-outline';
|
||||
import IconMdiPencilOutline from '~icons/mdi/pencil-outline';
|
||||
import IconMdiSendOutline from '~icons/mdi/send-outline';
|
||||
|
||||
defineOptions({ name: 'MonthlyWorkReportIndex' });
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'create'): void;
|
||||
(e: 'edit', row: WorkReportRow): void;
|
||||
(e: 'detail', row: WorkReportRow): void;
|
||||
(e: 'approvalRecord', row: WorkReportRow): void;
|
||||
}>();
|
||||
|
||||
const { hasAuth } = useAuth();
|
||||
const exporting = ref(false);
|
||||
const selectedRows = ref<Api.WorkReport.Monthly.MonthlyReport[]>([]);
|
||||
const searchParams = reactive(createMonthlySearchParams());
|
||||
|
||||
const ACTION_ICON_MAP = {
|
||||
detail: markRaw(IconMdiEyeOutline),
|
||||
edit: markRaw(IconMdiPencilOutline),
|
||||
submit: markRaw(IconMdiSendOutline),
|
||||
delete: markRaw(IconMdiDeleteOutline),
|
||||
approvalRecord: markRaw(IconMdiFileDocumentCheckOutline)
|
||||
};
|
||||
|
||||
const table = useUIPaginatedTable<
|
||||
Awaited<ReturnType<typeof fetchGetMonthlyReportPage>>,
|
||||
Api.WorkReport.Monthly.MonthlyReport
|
||||
>({
|
||||
paginationProps: { currentPage: searchParams.pageNo, pageSize: searchParams.pageSize },
|
||||
api: () => fetchGetMonthlyReportPage(searchParams),
|
||||
transform: response => transformWorkReportPage(response, searchParams.pageNo ?? 1, searchParams.pageSize ?? 10),
|
||||
onPaginationParamsChange: params => {
|
||||
searchParams.pageNo = params.currentPage ?? 1;
|
||||
searchParams.pageSize = params.pageSize ?? 10;
|
||||
},
|
||||
columns: () => [
|
||||
{ prop: 'index', type: 'index', label: '序号', width: 64 },
|
||||
{ prop: 'periodLabel', label: '月份', minWidth: 80, formatter: row => formatPeriod(row) },
|
||||
{
|
||||
prop: 'reporterDeptName',
|
||||
label: '部门/方向',
|
||||
minWidth: 80,
|
||||
showOverflowTooltip: true,
|
||||
formatter: row => row.reporterDeptName || '--'
|
||||
},
|
||||
{ prop: 'supervisorName', label: '直属上级', minWidth: 80 },
|
||||
{ prop: 'totalWorkHours', label: '总工时', minWidth: 80, formatter: row => formatEmptyText(row.totalWorkHours) },
|
||||
{
|
||||
prop: 'statusCode',
|
||||
label: '状态',
|
||||
minWidth: 80,
|
||||
align: 'center',
|
||||
formatter: row => (
|
||||
<ElTag type={resolveWorkReportStatusTagType(row.statusCode)}>
|
||||
{getWorkReportStatusLabel(row.statusCode, row.statusName)}
|
||||
</ElTag>
|
||||
)
|
||||
},
|
||||
{ prop: 'submitTime', label: '提交时间', minWidth: 100, formatter: row => formatDateTime(row.submitTime) },
|
||||
{ prop: 'approvalTime', label: '审批时间', minWidth: 100, formatter: row => formatDateTime(row.approvalTime) },
|
||||
{
|
||||
prop: 'operate',
|
||||
label: '操作',
|
||||
width: 180,
|
||||
align: 'center',
|
||||
fixed: 'right',
|
||||
formatter: row => <BusinessTableActionCell actions={getRowActions(row)} variant="icon" />
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
function getRowActions(row: Api.WorkReport.Monthly.MonthlyReport): BusinessTableAction[] {
|
||||
const actions: BusinessTableAction[] = [
|
||||
{
|
||||
key: 'detail',
|
||||
label: '详情',
|
||||
buttonType: 'primary',
|
||||
icon: ACTION_ICON_MAP.detail,
|
||||
onClick: () => emit('detail', row)
|
||||
}
|
||||
];
|
||||
|
||||
if (['draft', 'rejected'].includes(row.statusCode) && row.allowEdit && hasAuth('project:work-report:update')) {
|
||||
actions.push({
|
||||
key: 'edit',
|
||||
label: '编辑',
|
||||
buttonType: 'primary',
|
||||
icon: ACTION_ICON_MAP.edit,
|
||||
onClick: () => emit('edit', row)
|
||||
});
|
||||
actions.push({
|
||||
key: 'submit',
|
||||
label: row.statusCode === 'draft' ? '提交' : '重新提交',
|
||||
buttonType: 'success',
|
||||
icon: ACTION_ICON_MAP.submit,
|
||||
onClick: () => handleSubmitReport(row)
|
||||
});
|
||||
}
|
||||
|
||||
if (row.statusCode === 'draft' && hasAuth('project:work-report:delete')) {
|
||||
actions.push({
|
||||
key: 'delete',
|
||||
label: '删除',
|
||||
buttonType: 'danger',
|
||||
icon: ACTION_ICON_MAP.delete,
|
||||
onClick: () => handleDelete(row)
|
||||
});
|
||||
}
|
||||
|
||||
if (['approved', 'rejected'].includes(row.statusCode)) {
|
||||
actions.push({
|
||||
key: 'approval-record',
|
||||
label: '审批记录',
|
||||
buttonType: 'info',
|
||||
icon: ACTION_ICON_MAP.approvalRecord,
|
||||
onClick: () => emit('approvalRecord', row)
|
||||
});
|
||||
}
|
||||
|
||||
return actions;
|
||||
}
|
||||
|
||||
async function reload(page?: number) {
|
||||
await table.getDataByPage(page ?? searchParams.pageNo ?? 1);
|
||||
}
|
||||
|
||||
function resetSearchParams() {
|
||||
const pageSize = searchParams.pageSize ?? 10;
|
||||
Object.assign(searchParams, createMonthlySearchParams(), { pageSize });
|
||||
reload(1);
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
reload(1);
|
||||
}
|
||||
|
||||
async function handleSubmitReport(row: Api.WorkReport.Monthly.MonthlyReport) {
|
||||
try {
|
||||
await ElMessageBox.confirm('确认提交该报告吗?', '提交确认', {
|
||||
type: 'warning',
|
||||
confirmButtonText: row.statusCode === 'draft' ? '确认提交' : '确认重新提交',
|
||||
cancelButtonText: '取消'
|
||||
});
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await fetchSubmitMonthlyReport(row.id);
|
||||
|
||||
if (result.error) return;
|
||||
window.$message?.success('工作报告已提交');
|
||||
await reload();
|
||||
}
|
||||
|
||||
async function handleDelete(row: Api.WorkReport.Monthly.MonthlyReport) {
|
||||
try {
|
||||
await ElMessageBox.confirm(`确认删除 ${formatPeriod(row)} 吗?`, '删除确认', {
|
||||
type: 'warning',
|
||||
confirmButtonText: '确认',
|
||||
cancelButtonText: '取消'
|
||||
});
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await fetchDeleteMonthlyReport(row.id);
|
||||
|
||||
if (result.error) return;
|
||||
|
||||
window.$message?.success('工作报告已删除');
|
||||
await reload();
|
||||
}
|
||||
|
||||
function handleSelectionChange(rows: Api.WorkReport.Monthly.MonthlyReport[]) {
|
||||
selectedRows.value = rows;
|
||||
}
|
||||
|
||||
function createExportSearchParams() {
|
||||
const { pageNo: _pageNo, pageSize: _pageSize, ...params } = searchParams;
|
||||
return params;
|
||||
}
|
||||
|
||||
async function exportReportContent(
|
||||
params: Api.WorkReport.Common.ContentExportParams<Api.WorkReport.Monthly.MonthlyReportSearchParams>,
|
||||
reportCount: number
|
||||
) {
|
||||
exporting.value = true;
|
||||
const result = await fetchExportMonthlyReportContent(params);
|
||||
exporting.value = false;
|
||||
|
||||
if (result.error || !result.data) return;
|
||||
|
||||
const fallbackName = createWorkReportContentExportFallbackName('monthly', reportCount);
|
||||
downloadBlob(result.data, resolveExportFilename(result, fallbackName));
|
||||
}
|
||||
|
||||
async function handleExportSelected() {
|
||||
if (!selectedRows.value.length) {
|
||||
window.$message?.warning('请选择要导出的报告');
|
||||
return;
|
||||
}
|
||||
|
||||
await exportReportContent(
|
||||
{
|
||||
exportAll: false,
|
||||
ids: selectedRows.value.map(item => item.id)
|
||||
},
|
||||
selectedRows.value.length
|
||||
);
|
||||
}
|
||||
|
||||
async function handleExportAll() {
|
||||
const total = table.mobilePagination.value.total || 0;
|
||||
if (!total) {
|
||||
window.$message?.warning('暂无可导出的报告');
|
||||
return;
|
||||
}
|
||||
|
||||
await exportReportContent(
|
||||
{
|
||||
...createExportSearchParams(),
|
||||
exportAll: true,
|
||||
ids: []
|
||||
},
|
||||
total
|
||||
);
|
||||
}
|
||||
|
||||
async function handleExportCommand(command: 'selected' | 'all') {
|
||||
if (command === 'selected') {
|
||||
await handleExportSelected();
|
||||
return;
|
||||
}
|
||||
|
||||
await handleExportAll();
|
||||
}
|
||||
|
||||
defineExpose({ reload });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex-col-stretch gap-16px overflow-hidden">
|
||||
<MonthlyReportSearch v-model:model="searchParams" @reset="resetSearchParams" @search="handleSearch" />
|
||||
|
||||
<ElCard class="flex-1-hidden card-wrapper" body-class="business-table-card-body">
|
||||
<template #header>
|
||||
<div class="flex flex-wrap items-center justify-between gap-12px">
|
||||
<div class="flex items-center gap-10px">
|
||||
<p class="text-16px font-600">{{ WORK_REPORT_TYPE_LABEL.monthly }}</p>
|
||||
<ElTag effect="plain">{{ table.mobilePagination.value.total || 0 }}</ElTag>
|
||||
</div>
|
||||
|
||||
<TableHeaderOperation
|
||||
v-model:columns="table.columnChecks.value"
|
||||
:loading="table.loading.value"
|
||||
@refresh="reload()"
|
||||
>
|
||||
<template #default>
|
||||
<ElDropdown v-auth="'project:work-report:export'" trigger="click" @command="handleExportCommand">
|
||||
<ElButton plain :loading="exporting">
|
||||
<template #icon>
|
||||
<icon-mdi-download class="text-icon" />
|
||||
</template>
|
||||
导出
|
||||
</ElButton>
|
||||
<template #dropdown>
|
||||
<ElDropdownMenu>
|
||||
<ElDropdownItem command="selected" :disabled="exporting || !selectedRows.length">
|
||||
导出选中
|
||||
</ElDropdownItem>
|
||||
<ElDropdownItem command="all" :disabled="exporting">导出全部</ElDropdownItem>
|
||||
</ElDropdownMenu>
|
||||
</template>
|
||||
</ElDropdown>
|
||||
<ElButton v-auth="'project:work-report:create'" plain type="primary" @click="emit('create')">
|
||||
<template #icon>
|
||||
<icon-ic-round-plus class="text-icon" />
|
||||
</template>
|
||||
新增
|
||||
</ElButton>
|
||||
</template>
|
||||
</TableHeaderOperation>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="flex-1">
|
||||
<ElTable
|
||||
v-loading="table.loading.value"
|
||||
height="100%"
|
||||
border
|
||||
row-key="id"
|
||||
:data="table.data.value"
|
||||
@selection-change="handleSelectionChange"
|
||||
>
|
||||
<ElTableColumn type="selection" width="48" />
|
||||
<template v-for="col in table.columns.value" :key="String(col.prop)">
|
||||
<ElTableColumn v-bind="col" />
|
||||
</template>
|
||||
</ElTable>
|
||||
</div>
|
||||
|
||||
<div class="mt-20px flex justify-end">
|
||||
<ElPagination
|
||||
v-if="table.mobilePagination.value.total"
|
||||
layout="total,prev,pager,next,sizes"
|
||||
v-bind="table.mobilePagination.value"
|
||||
@current-change="table.mobilePagination.value['current-change']"
|
||||
@size-change="table.mobilePagination.value['size-change']"
|
||||
/>
|
||||
</div>
|
||||
</ElCard>
|
||||
</div>
|
||||
</template>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,16 @@
|
||||
<script setup lang="ts">
|
||||
import SharedWorkReportApprovalRecordDialog from '../../shared/components/approval-record-dialog.vue';
|
||||
import type { WorkReportRow } from '../../shared/types';
|
||||
|
||||
defineOptions({ name: 'MonthlyReportApprovalRecordDialog' });
|
||||
|
||||
const visible = defineModel<boolean>('visible', { default: false });
|
||||
|
||||
defineProps<{
|
||||
rowData?: WorkReportRow | null;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SharedWorkReportApprovalRecordDialog v-model:visible="visible" report-type="monthly" :row-data="rowData" />
|
||||
</template>
|
||||
@@ -0,0 +1,16 @@
|
||||
<script setup lang="ts">
|
||||
import SharedWorkReportDetailDialog from '../../shared/components/detail-dialog.vue';
|
||||
import type { WorkReportRow } from '../../shared/types';
|
||||
|
||||
defineOptions({ name: 'MonthlyReportDetailPage' });
|
||||
|
||||
const visible = defineModel<boolean>('visible', { default: false });
|
||||
|
||||
defineProps<{
|
||||
rowData?: WorkReportRow | null;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SharedWorkReportDetailDialog v-model:visible="visible" report-type="monthly" :row-data="rowData" />
|
||||
</template>
|
||||
2195
src/views/personal-center/work-report/monthly/modules/fill-page.vue
Normal file
2195
src/views/personal-center/work-report/monthly/modules/fill-page.vue
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,26 @@
|
||||
<script setup lang="ts">
|
||||
import SharedWorkReportSearch from '../../shared/components/search-panel.vue';
|
||||
|
||||
defineOptions({ name: 'MonthlyReportSearch' });
|
||||
|
||||
defineProps<{
|
||||
projectOptions?: Api.WorkReport.Project.ProjectReportOwnerProjectOption[];
|
||||
}>();
|
||||
|
||||
const model = defineModel<Api.WorkReport.Monthly.MonthlyReportSearchParams>('model', { required: true });
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'reset'): void;
|
||||
(e: 'search'): void;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SharedWorkReportSearch
|
||||
v-model:model="model"
|
||||
report-type="monthly"
|
||||
:project-options="projectOptions"
|
||||
@reset="emit('reset')"
|
||||
@search="emit('search')"
|
||||
/>
|
||||
</template>
|
||||
363
src/views/personal-center/work-report/project/index.vue
Normal file
363
src/views/personal-center/work-report/project/index.vue
Normal file
@@ -0,0 +1,363 @@
|
||||
<script setup lang="tsx">
|
||||
/* eslint-disable no-void */
|
||||
import { markRaw, reactive, ref } from 'vue';
|
||||
import { ElMessageBox, ElTag } from 'element-plus';
|
||||
import {
|
||||
fetchDeleteProjectReport,
|
||||
fetchExportProjectReportContent,
|
||||
fetchGetProjectReportPage,
|
||||
fetchSubmitProjectReport
|
||||
} from '@/service/api';
|
||||
import { useAuth } from '@/hooks/business/auth';
|
||||
import { useUIPaginatedTable } from '@/hooks/common/table';
|
||||
import BusinessTableActionCell, { type BusinessTableAction } from '@/components/custom/business-table-action-cell';
|
||||
import {
|
||||
WORK_REPORT_TYPE_LABEL,
|
||||
type WorkReportRow,
|
||||
createProjectSearchParams,
|
||||
createWorkReportContentExportFallbackName,
|
||||
downloadBlob,
|
||||
formatDateTime,
|
||||
formatEmptyText,
|
||||
formatPeriod,
|
||||
getProjectReportFlagLabel,
|
||||
getWorkReportStatusLabel,
|
||||
resolveExportFilename,
|
||||
resolveWorkReportStatusTagType,
|
||||
transformWorkReportPage
|
||||
} from '../shared/types';
|
||||
import ProjectReportSearch from './modules/search-panel.vue';
|
||||
import IconMdiDeleteOutline from '~icons/mdi/delete-outline';
|
||||
import IconMdiEyeOutline from '~icons/mdi/eye-outline';
|
||||
import IconMdiFileDocumentCheckOutline from '~icons/mdi/file-document-check-outline';
|
||||
import IconMdiPencilOutline from '~icons/mdi/pencil-outline';
|
||||
import IconMdiSendOutline from '~icons/mdi/send-outline';
|
||||
|
||||
defineOptions({ name: 'ProjectWorkReportIndex' });
|
||||
|
||||
const props = defineProps<{
|
||||
projectOptions: Api.WorkReport.Project.ProjectReportOwnerProjectOption[];
|
||||
projectOptionsLoaded: boolean;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'create'): void;
|
||||
(e: 'edit', row: WorkReportRow): void;
|
||||
(e: 'detail', row: WorkReportRow): void;
|
||||
(e: 'approvalRecord', row: WorkReportRow): void;
|
||||
}>();
|
||||
|
||||
const { hasAuth } = useAuth();
|
||||
const exporting = ref(false);
|
||||
const selectedRows = ref<Api.WorkReport.Project.ProjectReport[]>([]);
|
||||
const searchParams = reactive(createProjectSearchParams());
|
||||
|
||||
const ACTION_ICON_MAP = {
|
||||
detail: markRaw(IconMdiEyeOutline),
|
||||
edit: markRaw(IconMdiPencilOutline),
|
||||
submit: markRaw(IconMdiSendOutline),
|
||||
delete: markRaw(IconMdiDeleteOutline),
|
||||
approvalRecord: markRaw(IconMdiFileDocumentCheckOutline)
|
||||
};
|
||||
|
||||
const table = useUIPaginatedTable<
|
||||
Awaited<ReturnType<typeof fetchGetProjectReportPage>>,
|
||||
Api.WorkReport.Project.ProjectReport
|
||||
>({
|
||||
paginationProps: { currentPage: searchParams.pageNo, pageSize: searchParams.pageSize },
|
||||
api: () => fetchGetProjectReportPage(searchParams),
|
||||
transform: response => transformWorkReportPage(response, searchParams.pageNo ?? 1, searchParams.pageSize ?? 10),
|
||||
onPaginationParamsChange: params => {
|
||||
searchParams.pageNo = params.currentPage ?? 1;
|
||||
searchParams.pageSize = params.pageSize ?? 10;
|
||||
},
|
||||
columns: () => [
|
||||
{ prop: 'index', type: 'index', label: '序号', width: 64 },
|
||||
{ prop: 'projectName', label: '项目名称', minWidth: 200, showOverflowTooltip: true },
|
||||
{ prop: 'periodLabel', label: '半月周期', minWidth: 120, formatter: row => formatPeriod(row) },
|
||||
// { prop: 'flag', label: '半月', width: 90, formatter: row => getProjectReportFlagLabel(row.flag) },
|
||||
{ prop: 'projectOwnerName', label: '项目负责人', minWidth: 80 },
|
||||
{
|
||||
prop: 'technicalOwnerName',
|
||||
label: '技术负责人',
|
||||
minWidth: 80,
|
||||
formatter: row => row.technicalOwnerName || '--'
|
||||
},
|
||||
{ prop: 'supervisorName', label: '直属上级', minWidth: 80 },
|
||||
{ prop: 'totalWorkHours', label: '总工时', minWidth: 60, formatter: row => formatEmptyText(row.totalWorkHours) },
|
||||
{
|
||||
prop: 'statusCode',
|
||||
label: '状态',
|
||||
minWidth: 60,
|
||||
align: 'center',
|
||||
formatter: row => (
|
||||
<ElTag type={resolveWorkReportStatusTagType(row.statusCode)}>
|
||||
{getWorkReportStatusLabel(row.statusCode, row.statusName)}
|
||||
</ElTag>
|
||||
)
|
||||
},
|
||||
{ prop: 'submitTime', label: '提交时间', minWidth: 100, formatter: row => formatDateTime(row.submitTime) },
|
||||
{ prop: 'approvalTime', label: '审批时间', minWidth: 100, formatter: row => formatDateTime(row.approvalTime) },
|
||||
{
|
||||
prop: 'operate',
|
||||
label: '操作',
|
||||
width: 180,
|
||||
align: 'center',
|
||||
fixed: 'right',
|
||||
formatter: row => <BusinessTableActionCell actions={getRowActions(row)} variant="icon" />
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
function getRowActions(row: Api.WorkReport.Project.ProjectReport): BusinessTableAction[] {
|
||||
const actions: BusinessTableAction[] = [
|
||||
{
|
||||
key: 'detail',
|
||||
label: '详情',
|
||||
buttonType: 'primary',
|
||||
icon: ACTION_ICON_MAP.detail,
|
||||
onClick: () => emit('detail', row)
|
||||
}
|
||||
];
|
||||
|
||||
if (['draft', 'rejected'].includes(row.statusCode) && row.allowEdit && hasAuth('project:work-report:update')) {
|
||||
actions.push({
|
||||
key: 'edit',
|
||||
label: '编辑',
|
||||
buttonType: 'primary',
|
||||
icon: ACTION_ICON_MAP.edit,
|
||||
onClick: () => emit('edit', row)
|
||||
});
|
||||
actions.push({
|
||||
key: 'submit',
|
||||
label: row.statusCode === 'draft' ? '提交' : '重新提交',
|
||||
buttonType: 'success',
|
||||
icon: ACTION_ICON_MAP.submit,
|
||||
onClick: () => handleSubmitReport(row)
|
||||
});
|
||||
}
|
||||
|
||||
if (row.statusCode === 'draft' && hasAuth('project:work-report:delete')) {
|
||||
actions.push({
|
||||
key: 'delete',
|
||||
label: '删除',
|
||||
buttonType: 'danger',
|
||||
icon: ACTION_ICON_MAP.delete,
|
||||
onClick: () => handleDelete(row)
|
||||
});
|
||||
}
|
||||
|
||||
if (['approved', 'rejected'].includes(row.statusCode)) {
|
||||
actions.push({
|
||||
key: 'approval-record',
|
||||
label: '审批记录',
|
||||
buttonType: 'info',
|
||||
icon: ACTION_ICON_MAP.approvalRecord,
|
||||
onClick: () => emit('approvalRecord', row)
|
||||
});
|
||||
}
|
||||
|
||||
return actions;
|
||||
}
|
||||
|
||||
async function reload(page?: number) {
|
||||
await table.getDataByPage(page ?? searchParams.pageNo ?? 1);
|
||||
}
|
||||
|
||||
function resetSearchParams() {
|
||||
const pageSize = searchParams.pageSize ?? 10;
|
||||
Object.assign(searchParams, createProjectSearchParams(), { pageSize });
|
||||
reload(1);
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
reload(1);
|
||||
}
|
||||
|
||||
async function handleSubmitReport(row: Api.WorkReport.Project.ProjectReport) {
|
||||
try {
|
||||
await ElMessageBox.confirm('确认提交该报告吗?', '提交确认', {
|
||||
type: 'warning',
|
||||
confirmButtonText: row.statusCode === 'draft' ? '确认提交' : '确认重新提交',
|
||||
cancelButtonText: '取消'
|
||||
});
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await fetchSubmitProjectReport(row.id);
|
||||
|
||||
if (result.error) return;
|
||||
window.$message?.success('工作报告已提交');
|
||||
await reload();
|
||||
}
|
||||
|
||||
async function handleDelete(row: Api.WorkReport.Project.ProjectReport) {
|
||||
try {
|
||||
await ElMessageBox.confirm(`确认删除 ${formatPeriod(row)} 吗?`, '删除确认', {
|
||||
type: 'warning',
|
||||
confirmButtonText: '确认',
|
||||
cancelButtonText: '取消'
|
||||
});
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await fetchDeleteProjectReport(row.id);
|
||||
|
||||
if (result.error) return;
|
||||
|
||||
window.$message?.success('工作报告已删除');
|
||||
await reload();
|
||||
}
|
||||
|
||||
function handleSelectionChange(rows: Api.WorkReport.Project.ProjectReport[]) {
|
||||
selectedRows.value = rows;
|
||||
}
|
||||
|
||||
function createExportSearchParams() {
|
||||
const { pageNo: _pageNo, pageSize: _pageSize, ...params } = searchParams;
|
||||
return params;
|
||||
}
|
||||
|
||||
async function exportReportContent(
|
||||
params: Api.WorkReport.Common.ContentExportParams<Api.WorkReport.Project.ProjectReportSearchParams>,
|
||||
reportCount: number
|
||||
) {
|
||||
exporting.value = true;
|
||||
const result = await fetchExportProjectReportContent(params);
|
||||
exporting.value = false;
|
||||
|
||||
if (result.error || !result.data) return;
|
||||
|
||||
const fallbackName = createWorkReportContentExportFallbackName('project', reportCount);
|
||||
downloadBlob(result.data, resolveExportFilename(result, fallbackName));
|
||||
}
|
||||
|
||||
async function handleExportSelected() {
|
||||
if (!selectedRows.value.length) {
|
||||
window.$message?.warning('请选择要导出的报告');
|
||||
return;
|
||||
}
|
||||
|
||||
await exportReportContent(
|
||||
{
|
||||
exportAll: false,
|
||||
ids: selectedRows.value.map(item => item.id)
|
||||
},
|
||||
selectedRows.value.length
|
||||
);
|
||||
}
|
||||
|
||||
async function handleExportAll() {
|
||||
const total = table.mobilePagination.value.total || 0;
|
||||
if (!total) {
|
||||
window.$message?.warning('暂无可导出的报告');
|
||||
return;
|
||||
}
|
||||
|
||||
await exportReportContent(
|
||||
{
|
||||
...createExportSearchParams(),
|
||||
exportAll: true,
|
||||
ids: []
|
||||
},
|
||||
total
|
||||
);
|
||||
}
|
||||
|
||||
async function handleExportCommand(command: 'selected' | 'all') {
|
||||
if (command === 'selected') {
|
||||
await handleExportSelected();
|
||||
return;
|
||||
}
|
||||
|
||||
await handleExportAll();
|
||||
}
|
||||
|
||||
defineExpose({ reload });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex-col-stretch gap-16px overflow-hidden">
|
||||
<!-- 项目选项加载失败时的提示 -->
|
||||
<ElAlert v-if="!projectOptionsLoaded" type="warning" :closable="false" show-icon>
|
||||
项目数据加载失败,部分功能可能不可用,请刷新页面重试
|
||||
</ElAlert>
|
||||
|
||||
<ProjectReportSearch
|
||||
v-model:model="searchParams"
|
||||
:project-options="projectOptions"
|
||||
@reset="resetSearchParams"
|
||||
@search="handleSearch"
|
||||
/>
|
||||
|
||||
<ElCard class="flex-1-hidden card-wrapper" body-class="business-table-card-body">
|
||||
<template #header>
|
||||
<div class="flex flex-wrap items-center justify-between gap-12px">
|
||||
<div class="flex items-center gap-10px">
|
||||
<p class="text-16px font-600">{{ WORK_REPORT_TYPE_LABEL.project }}</p>
|
||||
<ElTag effect="plain">{{ table.mobilePagination.value.total || 0 }}</ElTag>
|
||||
</div>
|
||||
|
||||
<TableHeaderOperation
|
||||
v-model:columns="table.columnChecks.value"
|
||||
:loading="table.loading.value"
|
||||
@refresh="reload()"
|
||||
>
|
||||
<template #default>
|
||||
<ElDropdown v-auth="'project:work-report:export'" trigger="click" @command="handleExportCommand">
|
||||
<ElButton plain :loading="exporting">
|
||||
<template #icon>
|
||||
<icon-mdi-download class="text-icon" />
|
||||
</template>
|
||||
导出
|
||||
</ElButton>
|
||||
<template #dropdown>
|
||||
<ElDropdownMenu>
|
||||
<ElDropdownItem command="selected" :disabled="exporting || !selectedRows.length">
|
||||
导出选中
|
||||
</ElDropdownItem>
|
||||
<ElDropdownItem command="all" :disabled="exporting">导出全部</ElDropdownItem>
|
||||
</ElDropdownMenu>
|
||||
</template>
|
||||
</ElDropdown>
|
||||
<ElButton v-auth="'project:work-report:create'" plain type="primary" @click="emit('create')">
|
||||
<template #icon>
|
||||
<icon-ic-round-plus class="text-icon" />
|
||||
</template>
|
||||
新增
|
||||
</ElButton>
|
||||
</template>
|
||||
</TableHeaderOperation>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="flex-1">
|
||||
<ElTable
|
||||
v-loading="table.loading.value"
|
||||
height="100%"
|
||||
border
|
||||
row-key="id"
|
||||
:data="table.data.value"
|
||||
@selection-change="handleSelectionChange"
|
||||
>
|
||||
<ElTableColumn type="selection" width="48" />
|
||||
<template v-for="col in table.columns.value" :key="String(col.prop)">
|
||||
<ElTableColumn v-bind="col" />
|
||||
</template>
|
||||
</ElTable>
|
||||
</div>
|
||||
|
||||
<div class="mt-20px flex justify-end">
|
||||
<ElPagination
|
||||
v-if="table.mobilePagination.value.total"
|
||||
layout="total,prev,pager,next,sizes"
|
||||
v-bind="table.mobilePagination.value"
|
||||
@current-change="table.mobilePagination.value['current-change']"
|
||||
@size-change="table.mobilePagination.value['size-change']"
|
||||
/>
|
||||
</div>
|
||||
</ElCard>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,16 @@
|
||||
<script setup lang="ts">
|
||||
import SharedWorkReportApprovalRecordDialog from '../../shared/components/approval-record-dialog.vue';
|
||||
import type { WorkReportRow } from '../../shared/types';
|
||||
|
||||
defineOptions({ name: 'ProjectReportApprovalRecordDialog' });
|
||||
|
||||
const visible = defineModel<boolean>('visible', { default: false });
|
||||
|
||||
defineProps<{
|
||||
rowData?: WorkReportRow | null;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SharedWorkReportApprovalRecordDialog v-model:visible="visible" report-type="project" :row-data="rowData" />
|
||||
</template>
|
||||
@@ -0,0 +1,16 @@
|
||||
<script setup lang="ts">
|
||||
import SharedWorkReportDetailDialog from '../../shared/components/detail-dialog.vue';
|
||||
import type { WorkReportRow } from '../../shared/types';
|
||||
|
||||
defineOptions({ name: 'ProjectReportDetailPage' });
|
||||
|
||||
const visible = defineModel<boolean>('visible', { default: false });
|
||||
|
||||
defineProps<{
|
||||
rowData?: WorkReportRow | null;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SharedWorkReportDetailDialog v-model:visible="visible" report-type="project" :row-data="rowData" />
|
||||
</template>
|
||||
1057
src/views/personal-center/work-report/project/modules/fill-page.vue
Normal file
1057
src/views/personal-center/work-report/project/modules/fill-page.vue
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,26 @@
|
||||
<script setup lang="ts">
|
||||
import SharedWorkReportSearch from '../../shared/components/search-panel.vue';
|
||||
|
||||
defineOptions({ name: 'ProjectReportSearch' });
|
||||
|
||||
defineProps<{
|
||||
projectOptions?: Api.WorkReport.Project.ProjectReportOwnerProjectOption[];
|
||||
}>();
|
||||
|
||||
const model = defineModel<Api.WorkReport.Project.ProjectReportSearchParams>('model', { required: true });
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'reset'): void;
|
||||
(e: 'search'): void;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SharedWorkReportSearch
|
||||
v-model:model="model"
|
||||
report-type="project"
|
||||
:project-options="projectOptions"
|
||||
@reset="emit('reset')"
|
||||
@search="emit('search')"
|
||||
/>
|
||||
</template>
|
||||
@@ -0,0 +1,341 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, reactive, watch } from 'vue';
|
||||
import dayjs from 'dayjs';
|
||||
import BusinessFormDialog from '@/components/custom/business-form-dialog.vue';
|
||||
import BusinessFormSection from '@/components/custom/business-form-section.vue';
|
||||
import { WORK_REPORT_TYPE_LABEL, type WorkReportType } from '../types';
|
||||
|
||||
defineOptions({ name: 'WorkReportActionDialog' });
|
||||
|
||||
type ActionType = 'approve' | 'reject';
|
||||
type ApprovalConclusion = 'approve' | 'reject';
|
||||
|
||||
const visible = defineModel<boolean>('visible', { default: false });
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
reportType: WorkReportType;
|
||||
actionType: ActionType;
|
||||
initialMonthlyApproveData?: Partial<Api.WorkReport.Monthly.MonthlyReportApproveParams> | null;
|
||||
loading?: boolean;
|
||||
}>(),
|
||||
{
|
||||
initialMonthlyApproveData: null,
|
||||
loading: false
|
||||
}
|
||||
);
|
||||
|
||||
const emit = defineEmits<{
|
||||
(
|
||||
e: 'submit',
|
||||
payload: Api.WorkReport.Common.StatusActionParams | Api.WorkReport.Monthly.MonthlyReportApproveParams,
|
||||
actionType?: ActionType
|
||||
): void;
|
||||
}>();
|
||||
|
||||
const reasonModel = reactive<Api.WorkReport.Common.StatusActionParams>({
|
||||
reason: ''
|
||||
});
|
||||
const commonApprovalModel = reactive<{
|
||||
conclusion: ApprovalConclusion | '';
|
||||
opinion: string;
|
||||
}>({
|
||||
conclusion: 'approve',
|
||||
opinion: ''
|
||||
});
|
||||
|
||||
const monthlyModel = reactive<Api.WorkReport.Monthly.MonthlyReportApproveParams>({
|
||||
reason: '',
|
||||
meetingDate: '',
|
||||
strengthDesc: '',
|
||||
strengthExample: '',
|
||||
weaknessDesc: '',
|
||||
weaknessExample: '',
|
||||
improvementSuggestion: '',
|
||||
performanceResult: '',
|
||||
employeeSignName: '',
|
||||
employeeSignedDate: '',
|
||||
supervisorSignName: '',
|
||||
supervisorSignedDate: ''
|
||||
});
|
||||
|
||||
const isMonthlyApprove = computed(() => props.reportType === 'monthly' && props.actionType === 'approve');
|
||||
const isCommonApprove = computed(() => props.reportType !== 'monthly' && props.actionType === 'approve');
|
||||
const title = computed(() => {
|
||||
if (isCommonApprove.value) {
|
||||
return `审批${WORK_REPORT_TYPE_LABEL[props.reportType]}`;
|
||||
}
|
||||
|
||||
const actionLabel = props.actionType === 'approve' ? '审批通过' : '退回';
|
||||
|
||||
return `${actionLabel}${WORK_REPORT_TYPE_LABEL[props.reportType]}`;
|
||||
});
|
||||
|
||||
const preset = computed(() => (isMonthlyApprove.value ? 'lg' : 'sm'));
|
||||
const confirmText = computed(() => {
|
||||
if (isCommonApprove.value) return '确认提交';
|
||||
if (props.actionType === 'approve') return '通过';
|
||||
return '退回';
|
||||
});
|
||||
const confirmDisabled = computed(() => isCommonApprove.value && !commonApprovalModel.conclusion);
|
||||
|
||||
watch(visible, isVisible => {
|
||||
if (!isVisible) return;
|
||||
reasonModel.reason = '';
|
||||
Object.assign(commonApprovalModel, {
|
||||
conclusion: 'approve',
|
||||
opinion: ''
|
||||
});
|
||||
Object.assign(monthlyModel, {
|
||||
reason: '',
|
||||
meetingDate: dayjs().format('YYYY-MM-DD'),
|
||||
strengthDesc: '',
|
||||
strengthExample: '',
|
||||
weaknessDesc: '',
|
||||
weaknessExample: '',
|
||||
improvementSuggestion: '',
|
||||
performanceResult: '',
|
||||
employeeSignName: '',
|
||||
employeeSignedDate: dayjs().format('YYYY-MM-DD'),
|
||||
supervisorSignName: '',
|
||||
supervisorSignedDate: dayjs().format('YYYY-MM-DD')
|
||||
});
|
||||
|
||||
if (props.initialMonthlyApproveData) {
|
||||
Object.assign(monthlyModel, props.initialMonthlyApproveData);
|
||||
}
|
||||
});
|
||||
|
||||
function handleSubmit() {
|
||||
if (isCommonApprove.value) {
|
||||
if (!commonApprovalModel.conclusion) {
|
||||
window.$message?.warning('请选择审批结论');
|
||||
return;
|
||||
}
|
||||
emit(
|
||||
'submit',
|
||||
{
|
||||
reason: commonApprovalModel.opinion || (commonApprovalModel.conclusion === 'approve' ? '通过' : '不通过')
|
||||
},
|
||||
commonApprovalModel.conclusion
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
emit('submit', isMonthlyApprove.value ? { ...monthlyModel } : { ...reasonModel });
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BusinessFormDialog
|
||||
v-model="visible"
|
||||
:title="title"
|
||||
:preset="preset"
|
||||
:confirm-loading="loading"
|
||||
:confirm-disabled="confirmDisabled"
|
||||
:confirm-text="confirmText"
|
||||
max-body-height="76vh"
|
||||
@confirm="handleSubmit"
|
||||
>
|
||||
<template v-if="isCommonApprove">
|
||||
<div class="audit-form">
|
||||
<div class="audit-field">
|
||||
<label>审批结论</label>
|
||||
<div class="audit-conclusion">
|
||||
<button
|
||||
type="button"
|
||||
class="conclusion-btn"
|
||||
:class="{
|
||||
active: commonApprovalModel.conclusion === 'approve',
|
||||
pass: commonApprovalModel.conclusion === 'approve'
|
||||
}"
|
||||
@click="commonApprovalModel.conclusion = 'approve'"
|
||||
>
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
|
||||
<circle cx="8" cy="8" r="7" stroke="currentColor" stroke-width="1.5" />
|
||||
<path
|
||||
d="M5 8.5L7 10.5L11 6"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
通过
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="conclusion-btn"
|
||||
:class="{
|
||||
active: commonApprovalModel.conclusion === 'reject',
|
||||
reject: commonApprovalModel.conclusion === 'reject'
|
||||
}"
|
||||
@click="commonApprovalModel.conclusion = 'reject'"
|
||||
>
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
|
||||
<circle cx="8" cy="8" r="7" stroke="currentColor" stroke-width="1.5" />
|
||||
<path d="M6 6L10 10M10 6L6 10" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" />
|
||||
</svg>
|
||||
不通过
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="audit-field">
|
||||
<label>审批意见</label>
|
||||
<ElInput v-model="commonApprovalModel.opinion" type="textarea" :rows="3" placeholder="请输入审批意见" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-else-if="isMonthlyApprove">
|
||||
<BusinessFormSection title="当期工作反馈">
|
||||
<ElRow :gutter="16">
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="面谈时间">
|
||||
<ElDatePicker v-model="monthlyModel.meetingDate" class="w-full" type="date" value-format="YYYY-MM-DD" />
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="绩效考核结果">
|
||||
<ElInput v-model="monthlyModel.performanceResult" placeholder="请输入绩效结果" />
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="24">
|
||||
<ElFormItem label="审批意见">
|
||||
<ElInput v-model="monthlyModel.reason" type="textarea" :rows="3" />
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
</BusinessFormSection>
|
||||
|
||||
<BusinessFormSection title="优势与不足">
|
||||
<ElRow :gutter="16">
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="优势描述">
|
||||
<ElInput v-model="monthlyModel.strengthDesc" type="textarea" :rows="3" />
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="优势行为事例">
|
||||
<ElInput v-model="monthlyModel.strengthExample" type="textarea" :rows="3" />
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="劣势描述">
|
||||
<ElInput v-model="monthlyModel.weaknessDesc" type="textarea" :rows="3" />
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="劣势行为事例">
|
||||
<ElInput v-model="monthlyModel.weaknessExample" type="textarea" :rows="3" />
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="24">
|
||||
<ElFormItem label="改进建议">
|
||||
<ElInput v-model="monthlyModel.improvementSuggestion" type="textarea" :rows="3" />
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
</BusinessFormSection>
|
||||
|
||||
<BusinessFormSection title="签字区">
|
||||
<ElRow :gutter="16">
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="被考核人签名">
|
||||
<ElInput v-model="monthlyModel.employeeSignName" />
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="被考核人签字日期">
|
||||
<ElDatePicker
|
||||
v-model="monthlyModel.employeeSignedDate"
|
||||
class="w-full"
|
||||
type="date"
|
||||
value-format="YYYY-MM-DD"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="上级签名">
|
||||
<ElInput v-model="monthlyModel.supervisorSignName" />
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="上级签字日期">
|
||||
<ElDatePicker
|
||||
v-model="monthlyModel.supervisorSignedDate"
|
||||
class="w-full"
|
||||
type="date"
|
||||
value-format="YYYY-MM-DD"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
</BusinessFormSection>
|
||||
</template>
|
||||
|
||||
<ElForm v-else label-position="top">
|
||||
<ElFormItem :label="actionType === 'approve' ? '审批意见' : '原因'">
|
||||
<ElInput v-model="reasonModel.reason" type="textarea" :rows="5" placeholder="请输入原因或意见" />
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
</BusinessFormDialog>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.audit-form {
|
||||
display: grid;
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
.audit-field {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.audit-field label {
|
||||
color: #475467;
|
||||
font-size: 13px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.audit-conclusion {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.conclusion-btn {
|
||||
height: 44px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
border: 1px solid #d8e0e8;
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
color: #475467;
|
||||
font: inherit;
|
||||
font-size: 14px;
|
||||
font-weight: 800;
|
||||
cursor: pointer;
|
||||
transition: all 0.18s ease;
|
||||
}
|
||||
|
||||
.conclusion-btn:hover {
|
||||
border-color: #0f766e;
|
||||
color: #0f766e;
|
||||
}
|
||||
|
||||
.conclusion-btn.active.pass {
|
||||
border-color: #0f766e;
|
||||
background: #f0fdfa;
|
||||
color: #0f766e;
|
||||
}
|
||||
|
||||
.conclusion-btn.active.reject {
|
||||
border-color: #dc2626;
|
||||
background: #fef2f2;
|
||||
color: #dc2626;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,129 @@
|
||||
<script setup lang="ts">
|
||||
/* eslint-disable no-void */
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import {
|
||||
fetchGetMonthlyReportApprovalRecords,
|
||||
fetchGetProjectReportApprovalRecords,
|
||||
fetchGetWeeklyReportApprovalRecords
|
||||
} from '@/service/api';
|
||||
import BusinessFormDialog from '@/components/custom/business-form-dialog.vue';
|
||||
import {
|
||||
WORK_REPORT_TYPE_LABEL,
|
||||
type WorkReportRow,
|
||||
type WorkReportType,
|
||||
formatDate,
|
||||
formatDateTime,
|
||||
getWorkReportStatusLabel
|
||||
} from '../types';
|
||||
|
||||
/** 格式化文本,空值显示 -- */
|
||||
function formatTextOrDash(value?: string | number | null) {
|
||||
if (value === null || value === undefined || value === '') {
|
||||
return '--';
|
||||
}
|
||||
return String(value);
|
||||
}
|
||||
|
||||
defineOptions({ name: 'WorkReportApprovalRecordDialog' });
|
||||
|
||||
const visible = defineModel<boolean>('visible', { default: false });
|
||||
|
||||
const props = defineProps<{
|
||||
reportType: WorkReportType;
|
||||
rowData?: WorkReportRow | null;
|
||||
}>();
|
||||
|
||||
const loading = ref(false);
|
||||
const records = ref<
|
||||
Array<Api.WorkReport.Common.WorkReportApprovalRecord | Api.WorkReport.Monthly.MonthlyReportApprovalRecord>
|
||||
>([]);
|
||||
const title = computed(() => `${WORK_REPORT_TYPE_LABEL[props.reportType]}审批记录`);
|
||||
const monthlyRecords = computed(() => records.value as Api.WorkReport.Monthly.MonthlyReportApprovalRecord[]);
|
||||
|
||||
watch(
|
||||
[visible, () => props.rowData?.id, () => props.reportType],
|
||||
([isVisible, currentId]) => {
|
||||
if (!isVisible) return;
|
||||
// visible 为 true(首次打开、换行、换报告类型)时都重新加载记录
|
||||
if (currentId) {
|
||||
loadRecords();
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
async function loadRecords() {
|
||||
if (!props.rowData?.id) return;
|
||||
|
||||
loading.value = true;
|
||||
let result;
|
||||
|
||||
if (props.reportType === 'weekly') {
|
||||
result = await fetchGetWeeklyReportApprovalRecords(props.rowData.id);
|
||||
} else if (props.reportType === 'monthly') {
|
||||
result = await fetchGetMonthlyReportApprovalRecords(props.rowData.id);
|
||||
} else {
|
||||
result = await fetchGetProjectReportApprovalRecords(props.rowData.id);
|
||||
}
|
||||
loading.value = false;
|
||||
|
||||
records.value = !result.error && result.data ? result.data : [];
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BusinessFormDialog v-model="visible" :title="title" preset="lg" :loading="loading" :show-footer="false">
|
||||
<ElTable v-if="reportType !== 'monthly'" border :data="records">
|
||||
<ElTableColumn prop="approvalRound" label="轮次" width="80" />
|
||||
<ElTableColumn label="结论" width="100">
|
||||
<template #default="{ row }">
|
||||
{{ getWorkReportStatusLabel(row.conclusion) }}
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn prop="opinion" label="审批意见" min-width="220" show-overflow-tooltip />
|
||||
<ElTableColumn prop="auditorName" label="审批人" width="120" />
|
||||
<ElTableColumn label="审批时间" width="170">
|
||||
<template #default="{ row }">
|
||||
{{ formatDateTime(row.createTime) }}
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
</ElTable>
|
||||
|
||||
<div v-else class="work-report-approval-records">
|
||||
<ElCard v-for="item in monthlyRecords" :key="item.id">
|
||||
<template #header>
|
||||
<div class="flex items-center justify-between gap-12px">
|
||||
<span>第 {{ item.approvalRound }} 轮 · {{ getWorkReportStatusLabel(item.conclusion) }}</span>
|
||||
<span class="text-12px text-#64748b">{{ item.auditorName }} · {{ formatDateTime(item.createTime) }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<ElDescriptions :column="2" border size="small">
|
||||
<ElDescriptionsItem label="审批意见" :span="2">{{ formatTextOrDash(item.opinion) }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="面谈时间">{{ formatDate(item.meetingDate) }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="绩效结果">{{ formatTextOrDash(item.performanceResult) }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="优势描述">{{ formatTextOrDash(item.strengthDesc) }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="优势事例">{{ formatTextOrDash(item.strengthExample) }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="劣势描述">{{ formatTextOrDash(item.weaknessDesc) }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="劣势事例">{{ formatTextOrDash(item.weaknessExample) }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="改进建议" :span="2">
|
||||
{{ formatTextOrDash(item.improvementSuggestion) }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="被考核人签字">
|
||||
{{ formatTextOrDash(item.employeeSignName) }} / {{ formatDate(item.employeeSignedDate) }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="上级签字">
|
||||
{{ formatTextOrDash(item.supervisorSignName) }} / {{ formatDate(item.supervisorSignedDate) }}
|
||||
</ElDescriptionsItem>
|
||||
</ElDescriptions>
|
||||
</ElCard>
|
||||
</div>
|
||||
</BusinessFormDialog>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.work-report-approval-records {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,534 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { Calendar } from '@element-plus/icons-vue';
|
||||
import BusinessFormDialog from '@/components/custom/business-form-dialog.vue';
|
||||
import {
|
||||
type WorkReportPeriodOption,
|
||||
buildMonthlyPeriodFromMonth,
|
||||
buildProjectPeriodFromMonth,
|
||||
buildWeeklyPeriodFromDate,
|
||||
formatPeriodDisplayLabel,
|
||||
getReportTypePeriodOptions
|
||||
} from '../utils';
|
||||
import { WORK_REPORT_TYPE_LABEL, type WorkReportType } from '../types';
|
||||
|
||||
defineOptions({ name: 'WorkReportCreateDialog' });
|
||||
|
||||
interface Props {
|
||||
defaultReportType?: WorkReportType;
|
||||
projectVisible?: boolean;
|
||||
projectOptions?: Api.WorkReport.Project.ProjectReportOwnerProjectOption[];
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
defaultReportType: 'weekly',
|
||||
projectVisible: false,
|
||||
projectOptions: () => []
|
||||
});
|
||||
|
||||
const visible = defineModel<boolean>('visible', { default: false });
|
||||
|
||||
const emit = defineEmits<{
|
||||
(
|
||||
e: 'confirm',
|
||||
payload:
|
||||
| { reportType: 'weekly' | 'monthly'; period: WorkReportPeriodOption['period'] }
|
||||
| {
|
||||
reportType: 'project';
|
||||
projectId: string;
|
||||
flag: number;
|
||||
period: WorkReportPeriodOption['period'];
|
||||
}
|
||||
): void;
|
||||
}>();
|
||||
|
||||
const selectedPeriodKey = ref('');
|
||||
const selectedProjectId = ref('');
|
||||
const customWeekDate = ref('');
|
||||
const customMonth = ref('');
|
||||
const customProjectMonth = ref('');
|
||||
const customProjectFlag = ref(1);
|
||||
|
||||
const selectedReportType = computed<WorkReportType>(() => {
|
||||
if (props.defaultReportType === 'project' && !props.projectVisible) return 'weekly';
|
||||
return props.defaultReportType;
|
||||
});
|
||||
|
||||
const periodOptionMap = computed(() => getReportTypePeriodOptions());
|
||||
const activePeriodOptions = computed(() => periodOptionMap.value[selectedReportType.value]);
|
||||
const dialogTitle = computed(() => `新增${WORK_REPORT_TYPE_LABEL[selectedReportType.value]}`);
|
||||
const projectHalfOptions = [
|
||||
{ label: '上半月', value: 1 },
|
||||
{ label: '下半月', value: 2 }
|
||||
];
|
||||
|
||||
const defaultCustomMonth = computed(() => {
|
||||
const period = activePeriodOptions.value[0]?.period;
|
||||
return period?.periodStartDate.slice(0, 7) || '';
|
||||
});
|
||||
|
||||
const customPeriod = computed<WorkReportPeriodOption['period'] | null>(() => {
|
||||
if (selectedPeriodKey.value !== 'custom') return null;
|
||||
|
||||
if (selectedReportType.value === 'weekly') {
|
||||
if (!customWeekDate.value) return null;
|
||||
return buildWeeklyPeriodFromDate(customWeekDate.value);
|
||||
}
|
||||
|
||||
if (selectedReportType.value === 'monthly') {
|
||||
if (!customMonth.value) return null;
|
||||
return buildMonthlyPeriodFromMonth(customMonth.value);
|
||||
}
|
||||
|
||||
if (!customProjectMonth.value) return null;
|
||||
return buildProjectPeriodFromMonth(customProjectMonth.value, customProjectFlag.value);
|
||||
});
|
||||
|
||||
const selectedPeriod = computed(
|
||||
() => activePeriodOptions.value.find(item => item.key === selectedPeriodKey.value) ?? activePeriodOptions.value[0]
|
||||
);
|
||||
|
||||
const selectedPeriodValue = computed(() =>
|
||||
selectedPeriodKey.value === 'custom' ? customPeriod.value : selectedPeriod.value?.period
|
||||
);
|
||||
const customPeriodPreviewLabel = computed(() =>
|
||||
customPeriod.value ? formatPeriodDisplayLabel(customPeriod.value.periodLabel) : ''
|
||||
);
|
||||
|
||||
const confirmDisabled = computed(() => {
|
||||
if (!selectedPeriodValue.value) return true;
|
||||
if (selectedReportType.value === 'project' && !selectedProjectId.value) return true;
|
||||
return false;
|
||||
});
|
||||
|
||||
watch(
|
||||
selectedReportType,
|
||||
type => {
|
||||
selectedPeriodKey.value = periodOptionMap.value[type][0]?.key || '';
|
||||
|
||||
if (type === 'project' && !selectedProjectId.value) {
|
||||
selectedProjectId.value = props.projectOptions[0]?.id || '';
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
watch(visible, isVisible => {
|
||||
if (!isVisible) return;
|
||||
|
||||
selectedProjectId.value = props.projectOptions[0]?.id || '';
|
||||
selectedPeriodKey.value = periodOptionMap.value[selectedReportType.value][0]?.key || '';
|
||||
customWeekDate.value = activePeriodOptions.value[0]?.period.periodStartDate || '';
|
||||
customMonth.value = defaultCustomMonth.value;
|
||||
customProjectMonth.value = defaultCustomMonth.value;
|
||||
customProjectFlag.value = activePeriodOptions.value[0]?.flag || 1;
|
||||
});
|
||||
|
||||
function handleConfirm() {
|
||||
const period = selectedPeriodValue.value;
|
||||
if (!period) return;
|
||||
|
||||
if (selectedReportType.value === 'project') {
|
||||
emit('confirm', {
|
||||
reportType: 'project',
|
||||
projectId: selectedProjectId.value,
|
||||
flag: selectedPeriodKey.value === 'custom' ? customProjectFlag.value : selectedPeriod.value.flag || 1,
|
||||
period
|
||||
});
|
||||
} else {
|
||||
emit('confirm', {
|
||||
reportType: selectedReportType.value,
|
||||
period
|
||||
});
|
||||
}
|
||||
|
||||
visible.value = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BusinessFormDialog
|
||||
v-model="visible"
|
||||
:title="dialogTitle"
|
||||
class="work-report-create-dialog"
|
||||
preset="md"
|
||||
confirm-text="确认新增"
|
||||
append-to-body
|
||||
:close-on-click-modal="false"
|
||||
@confirm="handleConfirm"
|
||||
>
|
||||
<div v-if="selectedReportType === 'project'" class="work-report-create-dialog__project-select">
|
||||
<label class="work-report-create-dialog__label">项目</label>
|
||||
<ElSelect v-model="selectedProjectId" class="w-full" placeholder="请选择项目" filterable>
|
||||
<ElOption
|
||||
v-for="item in props.projectOptions"
|
||||
:key="item.id"
|
||||
:label="item.projectCode ? `${item.projectName}(${item.projectCode})` : item.projectName"
|
||||
:value="item.id"
|
||||
/>
|
||||
</ElSelect>
|
||||
</div>
|
||||
|
||||
<div class="work-report-create-dialog__section">
|
||||
<div class="work-report-create-dialog__grid is-period">
|
||||
<button
|
||||
v-for="item in activePeriodOptions"
|
||||
:key="item.key"
|
||||
type="button"
|
||||
class="work-report-create-dialog__choice"
|
||||
:class="{ 'is-active': selectedPeriodKey === item.key }"
|
||||
@click="selectedPeriodKey = item.key"
|
||||
>
|
||||
<div class="work-report-create-dialog__choice-title">{{ item.label }}</div>
|
||||
<div class="work-report-create-dialog__choice-desc">{{ item.description }}</div>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="work-report-create-dialog__choice"
|
||||
:class="{ 'is-active': selectedPeriodKey === 'custom' }"
|
||||
@click="selectedPeriodKey = 'custom'"
|
||||
>
|
||||
<div class="work-report-create-dialog__choice-title">自定义周期</div>
|
||||
<div class="work-report-create-dialog__choice-desc">
|
||||
{{
|
||||
selectedReportType === 'weekly'
|
||||
? '选择某一周作为周报周期。'
|
||||
: selectedReportType === 'monthly'
|
||||
? '选择某一月作为月报周期。'
|
||||
: '选择某个月的上半月或下半月。'
|
||||
}}
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="selectedPeriodKey === 'custom'" class="work-report-create-dialog__custom-period">
|
||||
<div v-if="selectedReportType === 'weekly'" class="work-report-create-dialog__custom-row">
|
||||
<div class="work-report-create-dialog__field work-report-create-dialog__field--inline">
|
||||
<label class="work-report-create-dialog__label">周报周期</label>
|
||||
<ElDatePicker
|
||||
v-model="customWeekDate"
|
||||
type="date"
|
||||
format="YYYY[年第]ww[周]"
|
||||
value-format="YYYY-MM-DD"
|
||||
popper-class="work-report-create-date-popper"
|
||||
placeholder="请选择周报周期"
|
||||
/>
|
||||
<div v-if="customPeriodPreviewLabel" class="work-report-create-dialog__period-preview">
|
||||
{{ customPeriodPreviewLabel }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else-if="selectedReportType === 'monthly'" class="work-report-create-dialog__custom-row">
|
||||
<div class="work-report-create-dialog__field work-report-create-dialog__field--inline">
|
||||
<label class="work-report-create-dialog__label">月报周期</label>
|
||||
<ElDatePicker
|
||||
v-model="customMonth"
|
||||
type="month"
|
||||
value-format="YYYY-MM"
|
||||
popper-class="work-report-create-date-popper"
|
||||
placeholder="请选择月份"
|
||||
/>
|
||||
<div v-if="customPeriodPreviewLabel" class="work-report-create-dialog__period-preview">
|
||||
{{ customPeriodPreviewLabel }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="work-report-create-dialog__custom-project">
|
||||
<div class="work-report-create-dialog__custom-project-grid">
|
||||
<div class="work-report-create-dialog__custom-project-item">
|
||||
<div class="work-report-create-dialog__custom-project-item-label">选择月份</div>
|
||||
<ElDatePicker
|
||||
v-model="customProjectMonth"
|
||||
class="w-full"
|
||||
type="month"
|
||||
value-format="YYYY-MM"
|
||||
popper-class="work-report-create-date-popper"
|
||||
placeholder="请选择月份"
|
||||
/>
|
||||
</div>
|
||||
<div class="work-report-create-dialog__custom-project-item">
|
||||
<div class="work-report-create-dialog__custom-project-item-label">选择半月</div>
|
||||
<ElSegmented
|
||||
v-model="customProjectFlag"
|
||||
:options="projectHalfOptions"
|
||||
class="work-report-create-dialog__half-segmented"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="customPeriodPreviewLabel" class="work-report-create-dialog__period-preview">
|
||||
<ElIcon class="work-report-create-dialog__period-preview-icon"><Calendar /></ElIcon>
|
||||
<span class="work-report-create-dialog__period-preview-text">已选周期:</span>
|
||||
<span class="work-report-create-dialog__period-preview-value">{{ customPeriodPreviewLabel }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #footer="{ close }">
|
||||
<div class="work-report-create-dialog__footer">
|
||||
<ElButton @click="close">取消</ElButton>
|
||||
<ElButton type="primary" :disabled="confirmDisabled" @click="handleConfirm">确认新增</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
</BusinessFormDialog>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.work-report-create-dialog__header {
|
||||
padding: 0 0 14px;
|
||||
}
|
||||
|
||||
.work-report-create-dialog__title {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.work-report-create-dialog__subtitle {
|
||||
margin-top: 5px;
|
||||
color: #667085;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.work-report-create-dialog__section + .work-report-create-dialog__section {
|
||||
margin-top: 18px;
|
||||
}
|
||||
|
||||
.work-report-create-dialog__grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.work-report-create-dialog__grid.is-period {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.work-report-create-dialog__choice {
|
||||
padding: 16px;
|
||||
border: 2px solid #e5edf1;
|
||||
border-radius: 16px;
|
||||
background: #fbfdfe;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
transition:
|
||||
border-color 0.16s ease,
|
||||
background 0.16s ease,
|
||||
box-shadow 0.16s ease;
|
||||
}
|
||||
|
||||
.work-report-create-dialog__choice:hover {
|
||||
border-color: rgba(15, 118, 110, 0.28);
|
||||
box-shadow: 0 8px 20px rgba(15, 23, 42, 0.06);
|
||||
}
|
||||
|
||||
.work-report-create-dialog__choice.is-active {
|
||||
border-color: #0f766e;
|
||||
background: #ecfdf5;
|
||||
}
|
||||
|
||||
.work-report-create-dialog__choice-title {
|
||||
font-weight: 900;
|
||||
color: #14213d;
|
||||
}
|
||||
|
||||
.work-report-create-dialog__choice-desc {
|
||||
margin-top: 7px;
|
||||
color: #667085;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.work-report-create-dialog__project-select {
|
||||
margin: 4px 0 18px;
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.work-report-create-dialog__field {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
/** 行内字段:label 和控件在同一行,绿色 label 紧贴日期选择器右边 */
|
||||
.work-report-create-dialog__field--inline {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.work-report-create-dialog__field--inline .work-report-create-dialog__label {
|
||||
flex-shrink: 0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.work-report-create-dialog__field--inline :deep(.el-date-editor) {
|
||||
width: auto;
|
||||
min-width: 160px;
|
||||
max-width: 240px;
|
||||
}
|
||||
|
||||
.work-report-create-dialog__label {
|
||||
color: #667085;
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.work-report-create-dialog__custom-period {
|
||||
margin-top: 14px;
|
||||
padding: 16px;
|
||||
border: 1px solid rgba(15, 118, 110, 0.18);
|
||||
border-radius: 14px;
|
||||
background: linear-gradient(180deg, #f8fffd 0%, #ffffff 100%);
|
||||
box-shadow: 0 8px 24px rgba(15, 23, 42, 0.06);
|
||||
}
|
||||
|
||||
.work-report-create-dialog__custom-row {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.work-report-create-dialog__custom-row > .work-report-create-dialog__field--inline {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.work-report-create-dialog__custom-project {
|
||||
display: grid;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.work-report-create-dialog__custom-project-grid {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1.4fr) minmax(0, 1fr);
|
||||
gap: 14px;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.work-report-create-dialog__custom-project-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
padding: 12px 14px;
|
||||
border: 1px solid #e5edf1;
|
||||
border-radius: 10px;
|
||||
background: #fff;
|
||||
transition: border-color 0.18s ease;
|
||||
}
|
||||
|
||||
.work-report-create-dialog__custom-project-item:hover {
|
||||
border-color: rgba(15, 118, 110, 0.4);
|
||||
}
|
||||
|
||||
.work-report-create-dialog__custom-project-item-label {
|
||||
color: #475467;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.2px;
|
||||
}
|
||||
|
||||
.work-report-create-dialog__custom-project-item :deep(.el-date-editor) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.work-report-create-dialog__half-segmented {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.work-report-create-dialog__half-segmented :deep(.el-segmented__group) {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
width: 100%;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.work-report-create-dialog__half-segmented :deep(.el-segmented__item) {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.work-report-create-dialog__period-preview {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
min-height: 32px;
|
||||
padding: 0 14px;
|
||||
border: 1px solid rgba(15, 118, 110, 0.18);
|
||||
border-radius: 999px;
|
||||
background: #ecfdf5;
|
||||
color: #0f766e;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
white-space: nowrap;
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.work-report-create-dialog__period-preview-icon {
|
||||
font-size: 14px;
|
||||
color: #0f766e;
|
||||
}
|
||||
|
||||
.work-report-create-dialog__period-preview-text {
|
||||
color: #475467;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.work-report-create-dialog__period-preview-value {
|
||||
color: #0f766e;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.work-report-create-dialog__footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
@media (width <= 900px) {
|
||||
.work-report-create-dialog__grid,
|
||||
.work-report-create-dialog__grid.is-period {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.work-report-create-dialog__custom-row,
|
||||
.work-report-create-dialog__custom-project-grid {
|
||||
flex-direction: column;
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.work-report-create-dialog__field--inline {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.work-report-create-dialog__field--inline :deep(.el-date-editor) {
|
||||
max-width: 100%;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.work-report-create-dialog__period-preview {
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
:global(.work-report-create-date-popper) {
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
:global(.work-report-create-date-popper .el-picker-panel__body-wrapper) {
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
:global(.work-report-create-date-popper .el-date-table td.current:not(.disabled) .el-date-table-cell__text),
|
||||
:global(.work-report-create-date-popper .el-month-table td.current:not(.disabled) .cell) {
|
||||
background-color: #0f766e;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,173 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { fetchGetMonthlyReportDetail, fetchGetProjectReportDetail, fetchGetWeeklyReportDetail } from '@/service/api';
|
||||
import BusinessFormDialog from '@/components/custom/business-form-dialog.vue';
|
||||
import BusinessFormSection from '@/components/custom/business-form-section.vue';
|
||||
import {
|
||||
WORK_REPORT_TYPE_LABEL,
|
||||
type WorkReportRow,
|
||||
type WorkReportType,
|
||||
formatDate,
|
||||
formatEmptyText,
|
||||
formatPeriod,
|
||||
getProjectReportFlagLabel,
|
||||
getWorkReportStatusLabel
|
||||
} from '../types';
|
||||
|
||||
defineOptions({ name: 'WorkReportDetailDialog' });
|
||||
|
||||
const visible = defineModel<boolean>('visible', { default: false });
|
||||
|
||||
const props = defineProps<{
|
||||
reportType: WorkReportType;
|
||||
rowData?: WorkReportRow | null;
|
||||
}>();
|
||||
|
||||
const loading = ref(false);
|
||||
const detail = ref<WorkReportRow | null>(null);
|
||||
const title = computed(() => `${WORK_REPORT_TYPE_LABEL[props.reportType]}详情`);
|
||||
const weeklyDetail = computed(() =>
|
||||
props.reportType === 'weekly' ? (detail.value as Api.WorkReport.Weekly.WeeklyReport | null) : null
|
||||
);
|
||||
|
||||
watch(visible, isVisible => {
|
||||
if (isVisible) loadDetail();
|
||||
});
|
||||
|
||||
async function loadDetail() {
|
||||
if (!props.rowData?.id) return;
|
||||
|
||||
loading.value = true;
|
||||
let result;
|
||||
|
||||
if (props.reportType === 'weekly') {
|
||||
result = await fetchGetWeeklyReportDetail(props.rowData.id);
|
||||
} else if (props.reportType === 'monthly') {
|
||||
result = await fetchGetMonthlyReportDetail(props.rowData.id);
|
||||
} else {
|
||||
result = await fetchGetProjectReportDetail(props.rowData.id);
|
||||
}
|
||||
loading.value = false;
|
||||
|
||||
if (!result.error && result.data) {
|
||||
detail.value = result.data;
|
||||
}
|
||||
}
|
||||
|
||||
function getProjectDetail() {
|
||||
return detail.value as Api.WorkReport.Project.ProjectReport | null;
|
||||
}
|
||||
|
||||
function getPersonalDetail() {
|
||||
return detail.value as Api.WorkReport.Weekly.WeeklyReport | Api.WorkReport.Monthly.MonthlyReport | null;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BusinessFormDialog v-model="visible" :title="title" preset="lg" :loading="loading" :show-footer="false">
|
||||
<div v-if="detail" class="work-report-detail">
|
||||
<BusinessFormSection title="基础信息">
|
||||
<ElDescriptions :column="3" border size="small">
|
||||
<ElDescriptionsItem label="报告周期">{{ formatPeriod(detail) }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="状态">
|
||||
{{ getWorkReportStatusLabel(detail.statusCode, detail.statusName) }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="直属上级">{{ detail.supervisorName }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="开始日期">{{ formatDate(detail.periodStartDate) }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="结束日期">{{ formatDate(detail.periodEndDate) }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="总工时">{{ formatEmptyText(detail.totalWorkHours) }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="提交时间">{{ formatEmptyText(detail.submitTime) }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="审批时间">{{ formatEmptyText(detail.approvalTime) }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="审批意见">{{ formatEmptyText(detail.approvalComment) }}</ElDescriptionsItem>
|
||||
</ElDescriptions>
|
||||
</BusinessFormSection>
|
||||
|
||||
<template v-if="reportType === 'project'">
|
||||
<BusinessFormSection title="项目信息">
|
||||
<ElDescriptions :column="2" border size="small">
|
||||
<ElDescriptionsItem label="项目名称">{{ getProjectDetail()?.projectName }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="半月周期">
|
||||
{{ getProjectReportFlagLabel(getProjectDetail()?.flag) }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="项目负责人">{{ getProjectDetail()?.projectOwnerName }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="技术负责人">
|
||||
{{ formatEmptyText(getProjectDetail()?.technicalOwnerName) }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="项目状态" :span="2">
|
||||
{{ formatEmptyText(getProjectDetail()?.projectStatusDesc) }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="整体计划进度" :span="2">
|
||||
{{ formatEmptyText(getProjectDetail()?.projectProgressPlan) }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="要点描述" :span="2">
|
||||
{{ formatEmptyText(getProjectDetail()?.projectKeyPoints) }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="项目问题" :span="2">
|
||||
{{ formatEmptyText(getProjectDetail()?.projectProblems) }}
|
||||
</ElDescriptionsItem>
|
||||
</ElDescriptions>
|
||||
</BusinessFormSection>
|
||||
|
||||
<BusinessFormSection title="本期工作内容">
|
||||
<ElTable border :data="getProjectDetail()?.currentItems || []">
|
||||
<ElTableColumn prop="itemTitle" label="工作内容" min-width="180" />
|
||||
<ElTableColumn prop="workHours" label="工时" width="100" />
|
||||
<ElTableColumn prop="priorityCode" label="优先级" width="120" />
|
||||
<ElTableColumn prop="progressRate" label="进度" width="100" />
|
||||
</ElTable>
|
||||
</BusinessFormSection>
|
||||
|
||||
<BusinessFormSection title="下期计划工作内容">
|
||||
<ElTable border :data="getProjectDetail()?.nextItems || []">
|
||||
<ElTableColumn prop="itemTitle" label="工作内容" min-width="180" />
|
||||
<ElTableColumn prop="workHours" label="工时" width="100" />
|
||||
<ElTableColumn prop="priorityCode" label="优先级" width="120" />
|
||||
<ElTableColumn prop="progressRate" label="进度" width="100" />
|
||||
</ElTable>
|
||||
</BusinessFormSection>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<BusinessFormSection title="当期重点工作回顾">
|
||||
<ElTable border :data="getPersonalDetail()?.reviewItems || []">
|
||||
<ElTableColumn prop="itemTitle" label="事项" min-width="160" />
|
||||
<ElTableColumn prop="workHours" label="工时" width="100" />
|
||||
<ElTableColumn prop="contentText" label="工作内容" min-width="220" show-overflow-tooltip />
|
||||
<ElTableColumn prop="reflectionText" label="复盘反思" min-width="220" show-overflow-tooltip />
|
||||
</ElTable>
|
||||
</BusinessFormSection>
|
||||
|
||||
<BusinessFormSection title="下周期重点工作计划">
|
||||
<ElTable border :data="getPersonalDetail()?.planItems || []">
|
||||
<ElTableColumn prop="itemTitle" label="事项" min-width="160" />
|
||||
<ElTableColumn prop="targetText" label="目标" min-width="220" show-overflow-tooltip />
|
||||
<ElTableColumn prop="supportNeed" label="支持需求" min-width="220" show-overflow-tooltip />
|
||||
</ElTable>
|
||||
</BusinessFormSection>
|
||||
|
||||
<BusinessFormSection v-if="reportType === 'weekly'" title="出差信息">
|
||||
<ElDescriptions :column="3" border size="small">
|
||||
<ElDescriptionsItem label="是否出差">
|
||||
{{ weeklyDetail?.isBusinessTrip ? '是' : '否' }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="出差天数">
|
||||
{{ formatEmptyText(weeklyDetail?.totalTravelDays) }}
|
||||
</ElDescriptionsItem>
|
||||
</ElDescriptions>
|
||||
<ElTable class="mt-12px" border :data="weeklyDetail?.travelSegments || []">
|
||||
<ElTableColumn prop="startDate" label="开始日期" width="120" />
|
||||
<ElTableColumn prop="endDate" label="结束日期" width="120" />
|
||||
<ElTableColumn prop="travelDays" label="天数" width="100" />
|
||||
<ElTableColumn prop="location" label="地点" min-width="160" />
|
||||
</ElTable>
|
||||
</BusinessFormSection>
|
||||
</template>
|
||||
</div>
|
||||
</BusinessFormDialog>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.work-report-detail {
|
||||
min-width: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,564 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, reactive, ref, watch } from 'vue';
|
||||
import { ElMessageBox } from 'element-plus';
|
||||
import {
|
||||
fetchCreateMonthlyReport,
|
||||
fetchCreateProjectReport,
|
||||
fetchCreateWeeklyReport,
|
||||
fetchGetMonthlyReportDetail,
|
||||
fetchGetProjectReportDetail,
|
||||
fetchGetWeeklyReportDetail,
|
||||
fetchInitMonthlyReport,
|
||||
fetchInitProjectReport,
|
||||
fetchInitWeeklyReport,
|
||||
fetchPreviewMonthlyReportDefaultDraft,
|
||||
fetchPreviewProjectReportDefaultDraft,
|
||||
fetchPreviewWeeklyReportDefaultDraft,
|
||||
fetchUpdateMonthlyReport,
|
||||
fetchUpdateProjectReport,
|
||||
fetchUpdateWeeklyReport
|
||||
} from '@/service/api';
|
||||
import BusinessFormDialog from '@/components/custom/business-form-dialog.vue';
|
||||
import BusinessFormSection from '@/components/custom/business-form-section.vue';
|
||||
import {
|
||||
WORK_REPORT_TYPE_LABEL,
|
||||
type WorkReportRow,
|
||||
type WorkReportType,
|
||||
createBlankPlanItem,
|
||||
createBlankProjectItem,
|
||||
createBlankReviewItem,
|
||||
createMonthlySaveParams,
|
||||
createProjectSaveParams,
|
||||
createWeeklySaveParams,
|
||||
normalizePlanItems,
|
||||
normalizeProjectItems,
|
||||
normalizeReviewItems
|
||||
} from '../types';
|
||||
|
||||
defineOptions({ name: 'WorkReportOperateDialog' });
|
||||
|
||||
interface PeriodPayload {
|
||||
periodKey: string;
|
||||
periodLabel: string;
|
||||
periodStartDate: string;
|
||||
periodEndDate: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
operateType: 'add' | 'edit';
|
||||
reportType: WorkReportType;
|
||||
rowData?: WorkReportRow | null;
|
||||
initialPeriod?: PeriodPayload | null;
|
||||
initialProjectId?: string;
|
||||
initialFlag?: number;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
rowData: null,
|
||||
initialPeriod: null,
|
||||
initialProjectId: '',
|
||||
initialFlag: 1
|
||||
});
|
||||
|
||||
const visible = defineModel<boolean>('visible', { default: false });
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'submitted'): void;
|
||||
}>();
|
||||
|
||||
const loading = ref(false);
|
||||
const submitting = ref(false);
|
||||
const baseInfo = ref<WorkReportRow | null>(null);
|
||||
|
||||
const weeklyModel = reactive<Api.WorkReport.Weekly.WeeklyReportSaveParams>(createWeeklySaveParams());
|
||||
const monthlyModel = reactive<Api.WorkReport.Monthly.MonthlyReportSaveParams>(createMonthlySaveParams());
|
||||
const projectModel = reactive<Api.WorkReport.Project.ProjectReportSaveParams>(createProjectSaveParams());
|
||||
|
||||
const title = computed(
|
||||
() => `${props.operateType === 'add' ? '新增' : '编辑'}${WORK_REPORT_TYPE_LABEL[props.reportType]}`
|
||||
);
|
||||
const dialogPreset = computed(() => (props.reportType === 'weekly' ? 'md' : 'lg'));
|
||||
const activeModel = computed(() => {
|
||||
if (props.reportType === 'monthly') return monthlyModel;
|
||||
if (props.reportType === 'project') return projectModel;
|
||||
return weeklyModel;
|
||||
});
|
||||
const baseReporterName = computed(() => {
|
||||
if (!baseInfo.value) return '--';
|
||||
if ('projectOwnerName' in baseInfo.value) return baseInfo.value.projectOwnerName || '--';
|
||||
return baseInfo.value.reporterName || '--';
|
||||
});
|
||||
const baseDeptName = computed(() => {
|
||||
if (!baseInfo.value || 'projectOwnerName' in baseInfo.value) return '--';
|
||||
return baseInfo.value.reporterDeptName || '--';
|
||||
});
|
||||
const basePostName = computed(() => {
|
||||
if (!baseInfo.value || 'projectOwnerName' in baseInfo.value) return '--';
|
||||
return baseInfo.value.reporterPostName || '--';
|
||||
});
|
||||
|
||||
function patchPeriod(target: {
|
||||
periodKey: string;
|
||||
periodLabel: string;
|
||||
periodStartDate: string;
|
||||
periodEndDate: string;
|
||||
}) {
|
||||
if (!props.initialPeriod) return;
|
||||
Object.assign(target, props.initialPeriod);
|
||||
}
|
||||
|
||||
function patchWeekly(report?: Partial<Api.WorkReport.Weekly.WeeklyReport>) {
|
||||
Object.assign(
|
||||
weeklyModel,
|
||||
createWeeklySaveParams({
|
||||
...report,
|
||||
reviewItems: report?.reviewItems,
|
||||
planItems: report?.planItems,
|
||||
travelSegments: report?.travelSegments
|
||||
})
|
||||
);
|
||||
patchPeriod(weeklyModel);
|
||||
}
|
||||
|
||||
function patchMonthly(report?: Partial<Api.WorkReport.Monthly.MonthlyReport>) {
|
||||
Object.assign(
|
||||
monthlyModel,
|
||||
createMonthlySaveParams({
|
||||
...report,
|
||||
reviewItems: report?.reviewItems,
|
||||
planItems: report?.planItems
|
||||
})
|
||||
);
|
||||
patchPeriod(monthlyModel);
|
||||
}
|
||||
|
||||
function patchProject(report?: Partial<Api.WorkReport.Project.ProjectReport>) {
|
||||
Object.assign(
|
||||
projectModel,
|
||||
createProjectSaveParams({
|
||||
...report,
|
||||
projectId: report?.projectId || props.initialProjectId,
|
||||
flag: report?.flag ?? props.initialFlag,
|
||||
currentItems: report?.currentItems,
|
||||
nextItems: report?.nextItems
|
||||
})
|
||||
);
|
||||
patchPeriod(projectModel);
|
||||
}
|
||||
|
||||
async function loadDetail() {
|
||||
if (!props.rowData?.id) return;
|
||||
|
||||
loading.value = true;
|
||||
let result;
|
||||
|
||||
if (props.reportType === 'weekly') {
|
||||
result = await fetchGetWeeklyReportDetail(props.rowData.id);
|
||||
} else if (props.reportType === 'monthly') {
|
||||
result = await fetchGetMonthlyReportDetail(props.rowData.id);
|
||||
} else {
|
||||
result = await fetchGetProjectReportDetail(props.rowData.id);
|
||||
}
|
||||
loading.value = false;
|
||||
|
||||
if (result.error || !result.data) return;
|
||||
|
||||
baseInfo.value = result.data;
|
||||
|
||||
if (props.reportType === 'weekly') patchWeekly(result.data as Api.WorkReport.Weekly.WeeklyReport);
|
||||
if (props.reportType === 'monthly') patchMonthly(result.data as Api.WorkReport.Monthly.MonthlyReport);
|
||||
if (props.reportType === 'project') patchProject(result.data as Api.WorkReport.Project.ProjectReport);
|
||||
}
|
||||
|
||||
async function loadInitAndDraft() {
|
||||
loading.value = true;
|
||||
let initResult;
|
||||
|
||||
if (props.reportType === 'weekly') {
|
||||
initResult = await fetchInitWeeklyReport();
|
||||
} else if (props.reportType === 'monthly') {
|
||||
initResult = await fetchInitMonthlyReport();
|
||||
} else {
|
||||
initResult = await fetchInitProjectReport(props.initialProjectId);
|
||||
}
|
||||
|
||||
if (!initResult.error && initResult.data) {
|
||||
baseInfo.value = initResult.data;
|
||||
if (props.reportType === 'weekly') patchWeekly(initResult.data as Api.WorkReport.Weekly.WeeklyReport);
|
||||
if (props.reportType === 'monthly') patchMonthly(initResult.data as Api.WorkReport.Monthly.MonthlyReport);
|
||||
if (props.reportType === 'project') patchProject(initResult.data as Api.WorkReport.Project.ProjectReport);
|
||||
}
|
||||
|
||||
await pullDefaultDraft(false);
|
||||
loading.value = false;
|
||||
}
|
||||
|
||||
async function pullDefaultDraft(confirmOverwrite = true) {
|
||||
if (confirmOverwrite) {
|
||||
try {
|
||||
await ElMessageBox.confirm('重新拉取默认稿会覆盖当前已编辑内容,是否继续?', '覆盖确认', {
|
||||
type: 'warning',
|
||||
confirmButtonText: '继续',
|
||||
cancelButtonText: '取消'
|
||||
});
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const period = {
|
||||
periodKey: activeModel.value.periodKey,
|
||||
periodLabel: activeModel.value.periodLabel,
|
||||
periodStartDate: activeModel.value.periodStartDate,
|
||||
periodEndDate: activeModel.value.periodEndDate
|
||||
};
|
||||
|
||||
let result;
|
||||
|
||||
if (props.reportType === 'weekly') {
|
||||
result = await fetchPreviewWeeklyReportDefaultDraft(period);
|
||||
} else if (props.reportType === 'monthly') {
|
||||
result = await fetchPreviewMonthlyReportDefaultDraft(period);
|
||||
} else {
|
||||
result = await fetchPreviewProjectReportDefaultDraft(projectModel.projectId, {
|
||||
...period,
|
||||
flag: projectModel.flag
|
||||
});
|
||||
}
|
||||
|
||||
if (result.error || !result.data) return;
|
||||
|
||||
if (props.reportType === 'weekly') {
|
||||
weeklyModel.reviewItems = normalizeReviewItems((result.data as Api.WorkReport.Weekly.WeeklyReport).reviewItems);
|
||||
weeklyModel.planItems = normalizePlanItems((result.data as Api.WorkReport.Weekly.WeeklyReport).planItems);
|
||||
}
|
||||
|
||||
if (props.reportType === 'monthly') {
|
||||
monthlyModel.reviewItems = normalizeReviewItems((result.data as Api.WorkReport.Monthly.MonthlyReport).reviewItems);
|
||||
monthlyModel.planItems = normalizePlanItems((result.data as Api.WorkReport.Monthly.MonthlyReport).planItems);
|
||||
}
|
||||
|
||||
if (props.reportType === 'project') {
|
||||
projectModel.currentItems = normalizeProjectItems(
|
||||
(result.data as Api.WorkReport.Project.ProjectReport).currentItems
|
||||
);
|
||||
projectModel.nextItems = normalizeProjectItems((result.data as Api.WorkReport.Project.ProjectReport).nextItems);
|
||||
}
|
||||
}
|
||||
|
||||
watch(visible, isVisible => {
|
||||
if (!isVisible) return;
|
||||
baseInfo.value = null;
|
||||
if (props.operateType === 'edit') {
|
||||
loadDetail();
|
||||
} else {
|
||||
loadInitAndDraft();
|
||||
}
|
||||
});
|
||||
|
||||
function addReviewItem(items: Api.WorkReport.Common.PersonalReportReviewItem[]) {
|
||||
items.push(createBlankReviewItem(items.length));
|
||||
}
|
||||
|
||||
function addPlanItem(items: Api.WorkReport.Common.PersonalReportPlanItem[]) {
|
||||
items.push(createBlankPlanItem(items.length));
|
||||
}
|
||||
|
||||
function addProjectItem(items: Api.WorkReport.Project.ProjectReportItem[]) {
|
||||
items.push(createBlankProjectItem());
|
||||
}
|
||||
|
||||
function removeItem<T>(items: T[], index: number) {
|
||||
if (items.length <= 1) return;
|
||||
items.splice(index, 1);
|
||||
}
|
||||
|
||||
function validateBase() {
|
||||
if (!activeModel.value.periodKey || !activeModel.value.periodStartDate || !activeModel.value.periodEndDate) {
|
||||
window.$message?.warning('请先选择报告周期');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (props.reportType === 'project' && !projectModel.projectId) {
|
||||
window.$message?.warning('请选择项目');
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
if (!validateBase()) return;
|
||||
|
||||
submitting.value = true;
|
||||
let result;
|
||||
|
||||
if (props.reportType === 'weekly') {
|
||||
result =
|
||||
props.operateType === 'add'
|
||||
? await fetchCreateWeeklyReport(weeklyModel)
|
||||
: await fetchUpdateWeeklyReport(props.rowData!.id, weeklyModel);
|
||||
} else if (props.reportType === 'monthly') {
|
||||
result =
|
||||
props.operateType === 'add'
|
||||
? await fetchCreateMonthlyReport(monthlyModel)
|
||||
: await fetchUpdateMonthlyReport(props.rowData!.id, monthlyModel);
|
||||
} else {
|
||||
result =
|
||||
props.operateType === 'add'
|
||||
? await fetchCreateProjectReport(projectModel)
|
||||
: await fetchUpdateProjectReport(props.rowData!.id, projectModel);
|
||||
}
|
||||
submitting.value = false;
|
||||
|
||||
if (result.error) return;
|
||||
|
||||
window.$message?.success(props.operateType === 'add' ? '工作报告已创建' : '工作报告已保存');
|
||||
visible.value = false;
|
||||
emit('submitted');
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BusinessFormDialog
|
||||
v-model="visible"
|
||||
:title="title"
|
||||
:preset="dialogPreset"
|
||||
:loading="loading"
|
||||
:confirm-loading="submitting"
|
||||
max-body-height="76vh"
|
||||
@confirm="handleSubmit"
|
||||
>
|
||||
<div class="work-report-operate">
|
||||
<BusinessFormSection title="基础信息">
|
||||
<ElDescriptions :column="3" border size="small">
|
||||
<ElDescriptionsItem label="填报人">
|
||||
{{ baseReporterName }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="部门/方向">{{ baseDeptName }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="岗位">{{ basePostName }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="直属上级">{{ baseInfo?.supervisorName || '--' }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="周期" :span="2">{{ activeModel.periodLabel || '--' }}</ElDescriptionsItem>
|
||||
</ElDescriptions>
|
||||
|
||||
<div class="mt-12px flex justify-end">
|
||||
<ElButton plain type="primary" @click="pullDefaultDraft(true)">
|
||||
<template #icon>
|
||||
<icon-mdi-refresh class="text-icon" />
|
||||
</template>
|
||||
重新拉取默认稿
|
||||
</ElButton>
|
||||
</div>
|
||||
</BusinessFormSection>
|
||||
|
||||
<template v-if="reportType === 'project'">
|
||||
<BusinessFormSection title="项目状况">
|
||||
<ElRow :gutter="16">
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="项目状态">
|
||||
<ElInput v-model="projectModel.projectStatusDesc" type="textarea" :rows="3" />
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="整体计划进度">
|
||||
<ElInput v-model="projectModel.projectProgressPlan" type="textarea" :rows="3" />
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="要点描述">
|
||||
<ElInput v-model="projectModel.projectKeyPoints" type="textarea" :rows="3" />
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="项目问题">
|
||||
<ElInput v-model="projectModel.projectProblems" type="textarea" :rows="3" />
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
</BusinessFormSection>
|
||||
|
||||
<BusinessFormSection title="本期工作内容">
|
||||
<div class="work-report-operate__items">
|
||||
<div v-for="(item, index) in projectModel.currentItems" :key="index" class="work-report-operate__item">
|
||||
<ElInput v-model="item.itemTitle" placeholder="工作内容" />
|
||||
<ElInputNumber v-model="item.workHours" :min="0" :precision="1" placeholder="工时" />
|
||||
<ElInput v-model="item.priorityCode" placeholder="优先级" />
|
||||
<ElInputNumber v-model="item.progressRate" :min="0" :max="100" :precision="0" placeholder="进度" />
|
||||
<ElButton link type="danger" @click="removeItem(projectModel.currentItems, index)">删除</ElButton>
|
||||
</div>
|
||||
<ElButton plain @click="addProjectItem(projectModel.currentItems)">新增本期工作</ElButton>
|
||||
</div>
|
||||
</BusinessFormSection>
|
||||
|
||||
<BusinessFormSection title="下期计划工作内容">
|
||||
<div class="work-report-operate__items">
|
||||
<div v-for="(item, index) in projectModel.nextItems" :key="index" class="work-report-operate__item">
|
||||
<ElInput v-model="item.itemTitle" placeholder="工作内容" />
|
||||
<ElInputNumber v-model="item.workHours" :min="0" :precision="1" placeholder="工时" />
|
||||
<ElInput v-model="item.priorityCode" placeholder="优先级" />
|
||||
<ElInputNumber v-model="item.progressRate" :min="0" :max="100" :precision="0" placeholder="进度" />
|
||||
<ElButton link type="danger" @click="removeItem(projectModel.nextItems, index)">删除</ElButton>
|
||||
</div>
|
||||
<ElButton plain @click="addProjectItem(projectModel.nextItems)">新增下期工作</ElButton>
|
||||
</div>
|
||||
</BusinessFormSection>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<BusinessFormSection title="当期重点工作回顾">
|
||||
<div class="work-report-operate__cards">
|
||||
<div
|
||||
v-for="(item, index) in reportType === 'weekly' ? weeklyModel.reviewItems : monthlyModel.reviewItems"
|
||||
:key="index"
|
||||
class="work-report-operate__card"
|
||||
>
|
||||
<ElRow :gutter="16">
|
||||
<ElCol :span="14">
|
||||
<ElFormItem label="事项标题">
|
||||
<ElInput v-model="item.itemTitle" />
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="6">
|
||||
<ElFormItem label="工时">
|
||||
<ElInputNumber v-model="item.workHours" class="w-full" :min="0" :precision="1" />
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="4" class="flex items-end justify-end pb-16px">
|
||||
<ElButton
|
||||
link
|
||||
type="danger"
|
||||
@click="
|
||||
removeItem(reportType === 'weekly' ? weeklyModel.reviewItems : monthlyModel.reviewItems, index)
|
||||
"
|
||||
>
|
||||
删除
|
||||
</ElButton>
|
||||
</ElCol>
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="工作内容">
|
||||
<ElInput v-model="item.contentText" type="textarea" :rows="3" />
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="复盘反思">
|
||||
<ElInput v-model="item.reflectionText" type="textarea" :rows="3" />
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
</div>
|
||||
<ElButton
|
||||
plain
|
||||
@click="addReviewItem(reportType === 'weekly' ? weeklyModel.reviewItems : monthlyModel.reviewItems)"
|
||||
>
|
||||
新增回顾项
|
||||
</ElButton>
|
||||
</div>
|
||||
</BusinessFormSection>
|
||||
|
||||
<BusinessFormSection title="下周期重点工作计划">
|
||||
<div class="work-report-operate__cards">
|
||||
<div
|
||||
v-for="(item, index) in reportType === 'weekly' ? weeklyModel.planItems : monthlyModel.planItems"
|
||||
:key="index"
|
||||
class="work-report-operate__card"
|
||||
>
|
||||
<ElRow :gutter="16">
|
||||
<ElCol :span="20">
|
||||
<ElFormItem label="计划标题">
|
||||
<ElInput v-model="item.itemTitle" />
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="4" class="flex items-end justify-end pb-16px">
|
||||
<ElButton
|
||||
link
|
||||
type="danger"
|
||||
@click="removeItem(reportType === 'weekly' ? weeklyModel.planItems : monthlyModel.planItems, index)"
|
||||
>
|
||||
删除
|
||||
</ElButton>
|
||||
</ElCol>
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="目标">
|
||||
<ElInput v-model="item.targetText" type="textarea" :rows="3" />
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="支持需求">
|
||||
<ElInput v-model="item.supportNeed" type="textarea" :rows="3" />
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
</div>
|
||||
<ElButton
|
||||
plain
|
||||
@click="addPlanItem(reportType === 'weekly' ? weeklyModel.planItems : monthlyModel.planItems)"
|
||||
>
|
||||
新增计划项
|
||||
</ElButton>
|
||||
</div>
|
||||
</BusinessFormSection>
|
||||
|
||||
<BusinessFormSection v-if="reportType === 'weekly'" title="出差信息">
|
||||
<ElFormItem label="是否出差">
|
||||
<ElSwitch v-model="weeklyModel.isBusinessTrip" />
|
||||
</ElFormItem>
|
||||
<div v-if="weeklyModel.isBusinessTrip" class="work-report-operate__items">
|
||||
<div v-for="(item, index) in weeklyModel.travelSegments" :key="index" class="work-report-operate__item">
|
||||
<ElDatePicker v-model="item.startDate" type="date" value-format="YYYY-MM-DD" placeholder="开始日期" />
|
||||
<ElDatePicker v-model="item.endDate" type="date" value-format="YYYY-MM-DD" placeholder="结束日期" />
|
||||
<ElInputNumber v-model="item.travelDays" :min="0" :precision="1" placeholder="天数" />
|
||||
<ElInput v-model="item.location" placeholder="地点" />
|
||||
<ElButton link type="danger" @click="removeItem(weeklyModel.travelSegments, index)">删除</ElButton>
|
||||
</div>
|
||||
<ElButton
|
||||
plain
|
||||
@click="
|
||||
weeklyModel.travelSegments.push({
|
||||
sort: weeklyModel.travelSegments.length + 1,
|
||||
travelDays: 0,
|
||||
location: ''
|
||||
})
|
||||
"
|
||||
>
|
||||
新增出差分段
|
||||
</ElButton>
|
||||
</div>
|
||||
</BusinessFormSection>
|
||||
</template>
|
||||
</div>
|
||||
</BusinessFormDialog>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.work-report-operate {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.work-report-operate__cards,
|
||||
.work-report-operate__items {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.work-report-operate__card,
|
||||
.work-report-operate__item {
|
||||
padding: 12px;
|
||||
border: 1px solid var(--el-border-color-lighter);
|
||||
border-radius: 6px;
|
||||
background-color: var(--el-fill-color-extra-light);
|
||||
}
|
||||
|
||||
.work-report-operate__item {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) 120px 120px 120px auto;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
@media (width <= 900px) {
|
||||
.work-report-operate__item {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,123 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
|
||||
defineOptions({
|
||||
name: 'WorkReportPageDialog',
|
||||
inheritAttrs: false
|
||||
});
|
||||
|
||||
interface Props {
|
||||
title?: string;
|
||||
loading?: boolean;
|
||||
showFooter?: boolean;
|
||||
approvalMode?: boolean;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
title: '',
|
||||
loading: false,
|
||||
showFooter: false,
|
||||
approvalMode: false
|
||||
});
|
||||
|
||||
const visible = defineModel<boolean>('visible', { default: false });
|
||||
const route = useRoute();
|
||||
const viewportWidth = ref(typeof window === 'undefined' ? 1920 : window.innerWidth);
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'close'): void;
|
||||
}>();
|
||||
|
||||
const drawerSize = computed(() => (viewportWidth.value >= 2560 ? '60%' : '75%'));
|
||||
|
||||
function handleClose() {
|
||||
visible.value = false;
|
||||
}
|
||||
|
||||
function syncViewportWidth() {
|
||||
viewportWidth.value = window.innerWidth;
|
||||
}
|
||||
|
||||
/** 抽屉关闭动画结束后触发 close 事件 */
|
||||
function onDrawerClosed() {
|
||||
emit('close');
|
||||
}
|
||||
|
||||
const drawerBodyClass = props.approvalMode
|
||||
? 'work-report-page-drawer__body work-report-page-drawer__body--approval'
|
||||
: 'work-report-page-drawer__body';
|
||||
|
||||
watch(
|
||||
() => route.fullPath,
|
||||
() => {
|
||||
if (visible.value) {
|
||||
visible.value = false;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
syncViewportWidth();
|
||||
window.addEventListener('resize', syncViewportWidth);
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('resize', syncViewportWidth);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ElDrawer
|
||||
v-model="visible"
|
||||
class="work-report-page-drawer"
|
||||
:class="{ 'work-report-page-drawer--approval': props.approvalMode }"
|
||||
:body-class="drawerBodyClass"
|
||||
:title="props.title"
|
||||
:size="drawerSize"
|
||||
:close-on-click-modal="false"
|
||||
@closed="onDrawerClosed"
|
||||
>
|
||||
<div v-loading="props.loading" class="work-report-page-drawer__content">
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
<div v-if="props.showFooter" class="work-report-page-drawer__footer">
|
||||
<slot name="footer" :close="handleClose" />
|
||||
</div>
|
||||
</ElDrawer>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
:global(.work-report-page-drawer__body) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
:global(.work-report-page-drawer__body--approval) {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.work-report-page-drawer__content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.work-report-page-drawer__content :deep(.form-page) {
|
||||
flex: 1 0 auto;
|
||||
min-height: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.work-report-page-drawer__footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
padding: 12px 20px;
|
||||
border-top: 1px solid var(--el-border-color-lighter);
|
||||
background: var(--el-bg-color);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,774 @@
|
||||
<script setup lang="ts">
|
||||
/* eslint-disable complexity, no-nested-ternary, no-void, vue/no-deprecated-filter */
|
||||
import { computed, reactive, ref, watch } from 'vue';
|
||||
import { ElMessageBox } from 'element-plus';
|
||||
import dayjs from 'dayjs';
|
||||
import {
|
||||
fetchApproveMonthlyReport,
|
||||
fetchApproveProjectReport,
|
||||
fetchApproveWeeklyReport,
|
||||
fetchCreateMonthlyReport,
|
||||
fetchCreateProjectReport,
|
||||
fetchCreateWeeklyReport,
|
||||
fetchGetMonthlyReportDetail,
|
||||
fetchGetProjectReportDetail,
|
||||
fetchGetWeeklyReportDetail,
|
||||
fetchInitMonthlyReport,
|
||||
fetchInitProjectReport,
|
||||
fetchInitWeeklyReport,
|
||||
fetchPreviewMonthlyReportDefaultDraft,
|
||||
fetchPreviewProjectReportDefaultDraft,
|
||||
fetchPreviewWeeklyReportDefaultDraft,
|
||||
fetchRejectMonthlyReport,
|
||||
fetchRejectProjectReport,
|
||||
fetchRejectWeeklyReport,
|
||||
fetchSubmitMonthlyReport,
|
||||
fetchSubmitProjectReport,
|
||||
fetchSubmitWeeklyReport,
|
||||
fetchUpdateMonthlyReport,
|
||||
fetchUpdateProjectReport,
|
||||
fetchUpdateWeeklyReport
|
||||
} from '@/service/api';
|
||||
import type { WorkReportRow, WorkReportType } from '../types';
|
||||
import {
|
||||
createMonthlySaveParams,
|
||||
createProjectSaveParams,
|
||||
createWeeklySaveParams,
|
||||
formatPeriodLabel,
|
||||
normalizePlanItems,
|
||||
normalizeProjectItems,
|
||||
normalizeReviewItems
|
||||
} from '../types';
|
||||
import WeeklyReportPage from '../../weekly/modules/fill-page.vue';
|
||||
import MonthlyReportPage from '../../monthly/modules/fill-page.vue';
|
||||
import ProjectReportPage from '../../project/modules/fill-page.vue';
|
||||
import MonthlyReportApprovalPage from '../../monthly/modules/approval-page.vue';
|
||||
import WorkReportActionDialog from './action-dialog.vue';
|
||||
import WorkReportPageDialog from './page-dialog.vue';
|
||||
|
||||
defineOptions({ name: 'WorkReportPrototypePageDialog' });
|
||||
|
||||
interface PeriodPayload {
|
||||
periodKey: string;
|
||||
periodLabel: string;
|
||||
periodStartDate: string;
|
||||
periodEndDate: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
reportType: WorkReportType;
|
||||
mode?: 'add' | 'edit' | 'detail';
|
||||
scene?: 'fill' | 'detail' | 'approval';
|
||||
rowData?: WorkReportRow | null;
|
||||
initialPeriod?: PeriodPayload | null;
|
||||
initialProjectId?: string;
|
||||
initialFlag?: number;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
mode: 'detail',
|
||||
scene: 'detail',
|
||||
rowData: null,
|
||||
initialPeriod: null,
|
||||
initialProjectId: '',
|
||||
initialFlag: 1
|
||||
});
|
||||
|
||||
const visible = defineModel<boolean>('visible', { default: false });
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'submitted'): void;
|
||||
}>();
|
||||
|
||||
const REPORT_TYPE_TEXT: Record<WorkReportType, string> = {
|
||||
weekly: '个人周报',
|
||||
monthly: '个人月报',
|
||||
project: '项目半月报'
|
||||
};
|
||||
|
||||
const loading = ref(false);
|
||||
const actionVisible = ref(false);
|
||||
const actionSubmitting = ref(false);
|
||||
const currentActionType = ref<'approve' | 'reject'>('approve');
|
||||
const currentStage = ref<'form' | 'approval'>('form');
|
||||
const currentReportId = ref('');
|
||||
const baseInfo = ref<WorkReportRow | null>(null);
|
||||
const monthlyApprovalDraft = reactive<Api.WorkReport.Monthly.MonthlyReportApproveParams>({
|
||||
reason: '',
|
||||
meetingDate: '',
|
||||
strengthDesc: '',
|
||||
strengthExample: '',
|
||||
weaknessDesc: '',
|
||||
weaknessExample: '',
|
||||
improvementSuggestion: '',
|
||||
performanceResult: '',
|
||||
employeeSignName: '',
|
||||
employeeSignedDate: '',
|
||||
supervisorSignName: '',
|
||||
supervisorSignedDate: ''
|
||||
});
|
||||
|
||||
const weeklyModel = reactive<Api.WorkReport.Weekly.WeeklyReportSaveParams>(createWeeklySaveParams());
|
||||
const monthlyModel = reactive<Api.WorkReport.Monthly.MonthlyReportSaveParams>(createMonthlySaveParams());
|
||||
const projectModel = reactive<Api.WorkReport.Project.ProjectReportSaveParams>(createProjectSaveParams());
|
||||
|
||||
const currentModel = computed(() => {
|
||||
if (props.reportType === 'monthly') return monthlyModel;
|
||||
if (props.reportType === 'project') return projectModel;
|
||||
return weeklyModel;
|
||||
});
|
||||
|
||||
const currentScene = computed(() => {
|
||||
if (props.reportType === 'monthly' && currentStage.value === 'approval') {
|
||||
return 'approval';
|
||||
}
|
||||
|
||||
return props.scene;
|
||||
});
|
||||
|
||||
const dialogTitle = computed(() => {
|
||||
if (props.reportType === 'monthly' && currentStage.value === 'approval') {
|
||||
return `${REPORT_TYPE_TEXT.monthly}审批页`;
|
||||
}
|
||||
|
||||
if (currentScene.value === 'approval') {
|
||||
return `${REPORT_TYPE_TEXT[props.reportType]}审批`;
|
||||
}
|
||||
|
||||
if (props.mode === 'add') return `${REPORT_TYPE_TEXT[props.reportType]}填报页`;
|
||||
if (props.mode === 'edit') return `${REPORT_TYPE_TEXT[props.reportType]}编辑页`;
|
||||
return `${REPORT_TYPE_TEXT[props.reportType]}查看页`;
|
||||
});
|
||||
|
||||
const periodText = computed(() => {
|
||||
const label = currentModel.value.periodLabel || props.rowData?.periodLabel || props.initialPeriod?.periodLabel;
|
||||
return formatPeriodLabel(label) || '当前周期';
|
||||
});
|
||||
|
||||
function resetModels() {
|
||||
Object.assign(weeklyModel, createWeeklySaveParams());
|
||||
Object.assign(monthlyModel, createMonthlySaveParams());
|
||||
Object.assign(projectModel, createProjectSaveParams());
|
||||
Object.assign(monthlyApprovalDraft, {
|
||||
reason: '',
|
||||
meetingDate: '',
|
||||
strengthDesc: '',
|
||||
strengthExample: '',
|
||||
weaknessDesc: '',
|
||||
weaknessExample: '',
|
||||
improvementSuggestion: '',
|
||||
performanceResult: '',
|
||||
employeeSignName: '',
|
||||
employeeSignedDate: '',
|
||||
supervisorSignName: '',
|
||||
supervisorSignedDate: ''
|
||||
});
|
||||
}
|
||||
|
||||
function patchMonthlyApprovalDefaults(report?: Partial<Api.WorkReport.Monthly.MonthlyReport> | null) {
|
||||
const today = dayjs().format('YYYY-MM-DD');
|
||||
|
||||
Object.assign(monthlyApprovalDraft, {
|
||||
employeeSignName: monthlyApprovalDraft.employeeSignName || report?.reporterName || '',
|
||||
employeeSignedDate: monthlyApprovalDraft.employeeSignedDate || today,
|
||||
supervisorSignName: monthlyApprovalDraft.supervisorSignName || report?.supervisorName || '',
|
||||
supervisorSignedDate: monthlyApprovalDraft.supervisorSignedDate || today
|
||||
});
|
||||
}
|
||||
|
||||
function patchPeriod(target: {
|
||||
periodKey: string;
|
||||
periodLabel: string;
|
||||
periodStartDate: string;
|
||||
periodEndDate: string;
|
||||
}) {
|
||||
if (!props.initialPeriod) return;
|
||||
target.periodKey = props.initialPeriod.periodKey;
|
||||
target.periodLabel = props.initialPeriod.periodLabel;
|
||||
target.periodStartDate = props.initialPeriod.periodStartDate;
|
||||
target.periodEndDate = props.initialPeriod.periodEndDate;
|
||||
}
|
||||
|
||||
function patchWeekly(report?: Partial<Api.WorkReport.Weekly.WeeklyReport>) {
|
||||
Object.assign(
|
||||
weeklyModel,
|
||||
createWeeklySaveParams({
|
||||
...report,
|
||||
reviewItems: report?.reviewItems,
|
||||
planItems: report?.planItems,
|
||||
travelSegments: report?.travelSegments
|
||||
})
|
||||
);
|
||||
|
||||
if (report && Array.isArray(report.reviewItems) && !report.reviewItems.length) weeklyModel.reviewItems = [];
|
||||
if (report && Array.isArray(report.planItems) && !report.planItems.length) weeklyModel.planItems = [];
|
||||
if (report && Array.isArray(report.travelSegments) && !report.travelSegments.length) weeklyModel.travelSegments = [];
|
||||
|
||||
if (props.mode === 'add') patchPeriod(weeklyModel);
|
||||
}
|
||||
|
||||
function patchMonthly(report?: Partial<Api.WorkReport.Monthly.MonthlyReport>) {
|
||||
Object.assign(
|
||||
monthlyModel,
|
||||
createMonthlySaveParams({
|
||||
...report,
|
||||
reviewItems: report?.reviewItems,
|
||||
planItems: report?.planItems
|
||||
})
|
||||
);
|
||||
|
||||
if (report && Array.isArray(report.reviewItems) && !report.reviewItems.length) monthlyModel.reviewItems = [];
|
||||
if (report && Array.isArray(report.planItems) && !report.planItems.length) monthlyModel.planItems = [];
|
||||
|
||||
if (props.mode === 'add') patchPeriod(monthlyModel);
|
||||
patchMonthlyApprovalDefaults(report);
|
||||
}
|
||||
|
||||
function patchProject(report?: Partial<Api.WorkReport.Project.ProjectReport>) {
|
||||
Object.assign(
|
||||
projectModel,
|
||||
createProjectSaveParams({
|
||||
...report,
|
||||
projectId: report?.projectId || props.initialProjectId,
|
||||
flag: report?.flag ?? props.initialFlag,
|
||||
currentItems: report?.currentItems,
|
||||
nextItems: report?.nextItems
|
||||
})
|
||||
);
|
||||
|
||||
if (report && Array.isArray(report.currentItems) && !report.currentItems.length) projectModel.currentItems = [];
|
||||
if (report && Array.isArray(report.nextItems) && !report.nextItems.length) projectModel.nextItems = [];
|
||||
|
||||
if (props.mode === 'add') patchPeriod(projectModel);
|
||||
}
|
||||
|
||||
function firstMeaningfulValue<T>(...values: Array<T | null | undefined | ''>) {
|
||||
return values.find(value => value !== null && value !== undefined && value !== '') as T | undefined;
|
||||
}
|
||||
|
||||
function firstPositiveWorkHours(...values: Array<string | number | null | undefined>) {
|
||||
const matchedValue = values.find(value => {
|
||||
const numberValue = Number(value ?? 0);
|
||||
return Number.isFinite(numberValue) && numberValue > 0;
|
||||
});
|
||||
|
||||
return matchedValue ?? firstMeaningfulValue(...values);
|
||||
}
|
||||
|
||||
function mergePersonalDetailBaseInfo<
|
||||
T extends Api.WorkReport.Weekly.WeeklyReport | Api.WorkReport.Monthly.MonthlyReport
|
||||
>(detail: T) {
|
||||
const rowData = props.rowData as Partial<T> | null;
|
||||
if (!rowData) return detail;
|
||||
|
||||
return {
|
||||
...rowData,
|
||||
...detail,
|
||||
reporterDeptName: firstMeaningfulValue(detail.reporterDeptName, rowData.reporterDeptName) ?? null,
|
||||
reporterPostName: firstMeaningfulValue(detail.reporterPostName, rowData.reporterPostName) ?? null,
|
||||
submitTime: firstMeaningfulValue(detail.submitTime, rowData.submitTime) ?? null,
|
||||
totalWorkHours: firstPositiveWorkHours(detail.totalWorkHours, rowData.totalWorkHours)
|
||||
} as T;
|
||||
}
|
||||
|
||||
function mergeProjectDetailBaseInfo(detail: Api.WorkReport.Project.ProjectReport) {
|
||||
const rowData = props.rowData as Partial<Api.WorkReport.Project.ProjectReport> | null;
|
||||
if (!rowData) return detail;
|
||||
|
||||
return {
|
||||
...rowData,
|
||||
...detail,
|
||||
submitTime: firstMeaningfulValue(detail.submitTime, rowData.submitTime) ?? null,
|
||||
totalWorkHours: firstPositiveWorkHours(detail.totalWorkHours, rowData.totalWorkHours)
|
||||
};
|
||||
}
|
||||
|
||||
async function loadDetail(id: string) {
|
||||
loading.value = true;
|
||||
let result;
|
||||
|
||||
if (props.reportType === 'weekly') {
|
||||
result = await fetchGetWeeklyReportDetail(id);
|
||||
} else if (props.reportType === 'monthly') {
|
||||
result = await fetchGetMonthlyReportDetail(id);
|
||||
} else {
|
||||
result = await fetchGetProjectReportDetail(id);
|
||||
}
|
||||
loading.value = false;
|
||||
|
||||
if (result.error || !result.data) return;
|
||||
|
||||
const detail =
|
||||
props.reportType === 'weekly'
|
||||
? mergePersonalDetailBaseInfo(result.data as Api.WorkReport.Weekly.WeeklyReport)
|
||||
: props.reportType === 'monthly'
|
||||
? mergePersonalDetailBaseInfo(result.data as Api.WorkReport.Monthly.MonthlyReport)
|
||||
: mergeProjectDetailBaseInfo(result.data as Api.WorkReport.Project.ProjectReport);
|
||||
|
||||
currentReportId.value = detail.id;
|
||||
baseInfo.value = detail;
|
||||
|
||||
if (props.reportType === 'weekly') patchWeekly(detail as Api.WorkReport.Weekly.WeeklyReport);
|
||||
if (props.reportType === 'monthly') patchMonthly(detail as Api.WorkReport.Monthly.MonthlyReport);
|
||||
if (props.reportType === 'project') patchProject(detail as Api.WorkReport.Project.ProjectReport);
|
||||
}
|
||||
|
||||
async function pullDefaultDraft(confirmOverwrite = false) {
|
||||
const period = {
|
||||
periodKey: currentModel.value.periodKey,
|
||||
periodLabel: currentModel.value.periodLabel,
|
||||
periodStartDate: currentModel.value.periodStartDate,
|
||||
periodEndDate: currentModel.value.periodEndDate
|
||||
};
|
||||
|
||||
loading.value = true;
|
||||
let result;
|
||||
|
||||
if (props.reportType === 'weekly') {
|
||||
result = await fetchPreviewWeeklyReportDefaultDraft(period);
|
||||
} else if (props.reportType === 'monthly') {
|
||||
result = await fetchPreviewMonthlyReportDefaultDraft(period);
|
||||
} else {
|
||||
result = await fetchPreviewProjectReportDefaultDraft(projectModel.projectId, {
|
||||
...period,
|
||||
flag: projectModel.flag
|
||||
});
|
||||
}
|
||||
loading.value = false;
|
||||
|
||||
if (result.error || !result.data) return;
|
||||
|
||||
baseInfo.value = {
|
||||
...(baseInfo.value || {}),
|
||||
...result.data
|
||||
} as WorkReportRow;
|
||||
|
||||
if (props.reportType === 'weekly') {
|
||||
const data = result.data as Api.WorkReport.Weekly.WeeklyReport;
|
||||
weeklyModel.reviewItems = data.reviewItems?.length ? normalizeReviewItems(data.reviewItems) : [];
|
||||
weeklyModel.planItems = data.planItems?.length ? normalizePlanItems(data.planItems) : [];
|
||||
if (confirmOverwrite) {
|
||||
weeklyModel.travelSegments = data.travelSegments || [];
|
||||
}
|
||||
}
|
||||
|
||||
if (props.reportType === 'monthly') {
|
||||
const data = result.data as Api.WorkReport.Monthly.MonthlyReport;
|
||||
monthlyModel.reviewItems = data.reviewItems?.length ? normalizeReviewItems(data.reviewItems) : [];
|
||||
monthlyModel.planItems = data.planItems?.length ? normalizePlanItems(data.planItems) : [];
|
||||
}
|
||||
|
||||
if (props.reportType === 'project') {
|
||||
const data = result.data as Api.WorkReport.Project.ProjectReport;
|
||||
projectModel.currentItems = data.currentItems?.length ? normalizeProjectItems(data.currentItems) : [];
|
||||
projectModel.nextItems = data.nextItems?.length ? normalizeProjectItems(data.nextItems) : [];
|
||||
}
|
||||
}
|
||||
|
||||
async function loadInitData() {
|
||||
loading.value = true;
|
||||
let result;
|
||||
|
||||
if (props.reportType === 'weekly') {
|
||||
result = await fetchInitWeeklyReport();
|
||||
} else if (props.reportType === 'monthly') {
|
||||
result = await fetchInitMonthlyReport();
|
||||
} else {
|
||||
result = await fetchInitProjectReport(props.initialProjectId);
|
||||
}
|
||||
|
||||
if (!result.error && result.data) {
|
||||
baseInfo.value = result.data;
|
||||
if (props.reportType === 'weekly') patchWeekly(result.data as Api.WorkReport.Weekly.WeeklyReport);
|
||||
if (props.reportType === 'monthly') patchMonthly(result.data as Api.WorkReport.Monthly.MonthlyReport);
|
||||
if (props.reportType === 'project') patchProject(result.data as Api.WorkReport.Project.ProjectReport);
|
||||
}
|
||||
|
||||
loading.value = false;
|
||||
await pullDefaultDraft();
|
||||
}
|
||||
|
||||
watch(visible, async isVisible => {
|
||||
if (!isVisible) return;
|
||||
|
||||
currentStage.value = props.reportType === 'monthly' && props.scene === 'approval' ? 'approval' : 'form';
|
||||
currentReportId.value = props.rowData?.id || '';
|
||||
baseInfo.value = null;
|
||||
resetModels();
|
||||
|
||||
if (props.mode === 'add') {
|
||||
if (props.reportType === 'project') {
|
||||
projectModel.projectId = props.initialProjectId;
|
||||
projectModel.flag = props.initialFlag;
|
||||
}
|
||||
patchPeriod(currentModel.value);
|
||||
await loadInitData();
|
||||
return;
|
||||
}
|
||||
|
||||
if (props.rowData?.id) {
|
||||
await loadDetail(props.rowData.id);
|
||||
}
|
||||
});
|
||||
|
||||
function hasTextValue(value: unknown) {
|
||||
const text = String(value ?? '')
|
||||
.replace(/<[^>]*>/g, '')
|
||||
.replace(/ /g, ' ')
|
||||
.trim();
|
||||
|
||||
return text.length > 0;
|
||||
}
|
||||
|
||||
function hasMeaningfulStructuredValue(value: unknown): boolean {
|
||||
if (value === null || value === undefined) return false;
|
||||
|
||||
if (typeof value === 'string') {
|
||||
const text = value.trim();
|
||||
if (!text || text === '{}' || text === '[]') return false;
|
||||
|
||||
try {
|
||||
return hasMeaningfulStructuredValue(JSON.parse(text));
|
||||
} catch {
|
||||
return hasTextValue(text);
|
||||
}
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
return value.some(item => hasMeaningfulStructuredValue(item));
|
||||
}
|
||||
|
||||
if (typeof value === 'object') {
|
||||
return Object.values(value as Record<string, unknown>).some(item => hasMeaningfulStructuredValue(item));
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function hasReviewContent(item: Api.WorkReport.Common.PersonalReportReviewItem) {
|
||||
return hasTextValue(item.contentText) || hasMeaningfulStructuredValue(item.contentJson);
|
||||
}
|
||||
|
||||
function hasPlanTarget(item: Api.WorkReport.Common.PersonalReportPlanItem) {
|
||||
return hasTextValue(item.targetText) || hasMeaningfulStructuredValue(item.targetJson);
|
||||
}
|
||||
|
||||
function isCompleteWeeklyTravelSegment(segment: Api.WorkReport.Weekly.WeeklyReportTravelSegment) {
|
||||
const travelDays = Number(segment.travelDays);
|
||||
|
||||
return Boolean(
|
||||
segment.startDate &&
|
||||
segment.endDate &&
|
||||
segment.location?.trim() &&
|
||||
Number.isFinite(travelDays) &&
|
||||
travelDays >= 0.5 &&
|
||||
Number.isInteger(travelDays * 2)
|
||||
);
|
||||
}
|
||||
|
||||
function hasCompleteWeeklyTravelSegment(items: Api.WorkReport.Weekly.WeeklyReportTravelSegment[]) {
|
||||
return items.some(isCompleteWeeklyTravelSegment);
|
||||
}
|
||||
|
||||
function hasCompletePersonalReviewItem(items: Api.WorkReport.Common.PersonalReportReviewItem[]) {
|
||||
return items.some(item => hasTextValue(item.itemTitle) && hasReviewContent(item));
|
||||
}
|
||||
|
||||
function hasCompletePersonalPlanItem(items: Api.WorkReport.Common.PersonalReportPlanItem[]) {
|
||||
return items.some(item => hasTextValue(item.itemTitle) && hasPlanTarget(item));
|
||||
}
|
||||
|
||||
function getPersonalReviewValidationMessage(label: string, items: Api.WorkReport.Common.PersonalReportReviewItem[]) {
|
||||
if (!items.length) return `至少要有一项${label}`;
|
||||
if (!hasCompletePersonalReviewItem(items)) {
|
||||
return `请完善${label},项目名或我的事项、具体工作内容及成果描述不能为空`;
|
||||
}
|
||||
|
||||
const hasIncompleteItem = items.some(item => !hasTextValue(item.itemTitle) || !hasReviewContent(item));
|
||||
return hasIncompleteItem ? `请完善${label},项目名或我的事项、具体工作内容及成果描述不能为空` : '';
|
||||
}
|
||||
|
||||
function getPersonalPlanValidationMessage(label: string, items: Api.WorkReport.Common.PersonalReportPlanItem[]) {
|
||||
if (!items.length) return `至少要有一项${label}`;
|
||||
if (!hasCompletePersonalPlanItem(items)) {
|
||||
return `请完善${label},项目名或我的事项、具体目标不能为空`;
|
||||
}
|
||||
|
||||
const hasIncompleteItem = items.some(item => !hasTextValue(item.itemTitle) || !hasPlanTarget(item));
|
||||
return hasIncompleteItem ? `请完善${label},项目名或我的事项、具体目标不能为空` : '';
|
||||
}
|
||||
|
||||
function hasProjectItem(items: Api.WorkReport.Project.ProjectReportItem[]) {
|
||||
return items.some(item => hasTextValue(item.itemTitle));
|
||||
}
|
||||
|
||||
function validateRequiredReportItems() {
|
||||
const messages: string[] = [];
|
||||
|
||||
if (props.reportType === 'weekly') {
|
||||
const hasTravelReview = weeklyModel.isBusinessTrip && hasCompleteWeeklyTravelSegment(weeklyModel.travelSegments);
|
||||
const reviewMessage = hasTravelReview
|
||||
? weeklyModel.reviewItems.length
|
||||
? getPersonalReviewValidationMessage('当期重点工作回顾', weeklyModel.reviewItems)
|
||||
: ''
|
||||
: getPersonalReviewValidationMessage('当期重点工作回顾', weeklyModel.reviewItems);
|
||||
const planMessage = getPersonalPlanValidationMessage('下周期重点工作计划', weeklyModel.planItems);
|
||||
if (weeklyModel.isBusinessTrip && !hasTravelReview) messages.push('请至少新增一条完整的出差分段');
|
||||
if (reviewMessage) messages.push(reviewMessage);
|
||||
if (planMessage) messages.push(planMessage);
|
||||
} else if (props.reportType === 'monthly') {
|
||||
const reviewMessage = getPersonalReviewValidationMessage('当期重点工作回顾', monthlyModel.reviewItems);
|
||||
const planMessage = getPersonalPlanValidationMessage('下周期重点工作计划', monthlyModel.planItems);
|
||||
if (reviewMessage) messages.push(reviewMessage);
|
||||
if (planMessage) messages.push(planMessage);
|
||||
} else {
|
||||
const missingLabels: string[] = [];
|
||||
|
||||
if (!hasProjectItem(projectModel.currentItems)) {
|
||||
missingLabels.push('本期工作内容');
|
||||
}
|
||||
if (!hasProjectItem(projectModel.nextItems)) {
|
||||
missingLabels.push('下期计划工作内容');
|
||||
}
|
||||
|
||||
if (missingLabels.length) messages.push(`至少要有一项${missingLabels.join('、')}`);
|
||||
}
|
||||
|
||||
if (!messages.length) return true;
|
||||
|
||||
window.$message?.warning(messages.join(';'));
|
||||
return false;
|
||||
}
|
||||
|
||||
async function persistReport(submitAfterSave: boolean) {
|
||||
if (!validateRequiredReportItems()) return false;
|
||||
|
||||
let result;
|
||||
|
||||
if (props.reportType === 'weekly') {
|
||||
if (currentReportId.value) {
|
||||
result = await fetchUpdateWeeklyReport(currentReportId.value, weeklyModel);
|
||||
} else {
|
||||
result = await fetchCreateWeeklyReport(weeklyModel);
|
||||
if (!result.error && result.data) currentReportId.value = result.data;
|
||||
}
|
||||
} else if (props.reportType === 'monthly') {
|
||||
if (currentReportId.value) {
|
||||
result = await fetchUpdateMonthlyReport(currentReportId.value, monthlyModel);
|
||||
} else {
|
||||
result = await fetchCreateMonthlyReport(monthlyModel);
|
||||
if (!result.error && result.data) currentReportId.value = result.data;
|
||||
}
|
||||
} else if (currentReportId.value) {
|
||||
result = await fetchUpdateProjectReport(currentReportId.value, projectModel);
|
||||
} else {
|
||||
result = await fetchCreateProjectReport(projectModel);
|
||||
if (!result.error && result.data) currentReportId.value = result.data;
|
||||
}
|
||||
|
||||
if (result.error) return false;
|
||||
|
||||
if (submitAfterSave) {
|
||||
if (props.reportType === 'weekly') {
|
||||
const submitResult = await fetchSubmitWeeklyReport(currentReportId.value);
|
||||
if (submitResult.error) return false;
|
||||
} else if (props.reportType === 'monthly') {
|
||||
const submitResult = await fetchSubmitMonthlyReport(currentReportId.value);
|
||||
if (submitResult.error) return false;
|
||||
} else {
|
||||
const submitResult = await fetchSubmitProjectReport(currentReportId.value);
|
||||
if (submitResult.error) return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
async function handleSaveDraft() {
|
||||
loading.value = true;
|
||||
const success = await persistReport(false);
|
||||
loading.value = false;
|
||||
|
||||
if (!success) return;
|
||||
|
||||
window.$message?.success('工作报告已保存');
|
||||
visible.value = false;
|
||||
emit('submitted');
|
||||
}
|
||||
|
||||
async function handleSubmitReport() {
|
||||
try {
|
||||
await ElMessageBox.confirm('确认提交当前工作报告吗?', '提交确认', {
|
||||
type: 'warning',
|
||||
confirmButtonText: '确认提交',
|
||||
cancelButtonText: '取消'
|
||||
});
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
loading.value = true;
|
||||
const success = await persistReport(true);
|
||||
loading.value = false;
|
||||
|
||||
if (!success) return;
|
||||
|
||||
window.$message?.success('工作报告已提交');
|
||||
visible.value = false;
|
||||
emit('submitted');
|
||||
}
|
||||
|
||||
function handleBack() {
|
||||
visible.value = false;
|
||||
}
|
||||
|
||||
function handleViewApproval() {
|
||||
currentStage.value = 'approval';
|
||||
}
|
||||
|
||||
function handleRequestApprove() {
|
||||
if (props.reportType === 'monthly') {
|
||||
handleActionSubmit({ ...monthlyApprovalDraft }, 'approve');
|
||||
return;
|
||||
}
|
||||
currentActionType.value = 'approve';
|
||||
actionVisible.value = true;
|
||||
}
|
||||
|
||||
function handleRequestReject() {
|
||||
if (props.reportType === 'monthly') {
|
||||
handleActionSubmit({ reason: monthlyApprovalDraft.reason }, 'reject');
|
||||
return;
|
||||
}
|
||||
currentActionType.value = 'reject';
|
||||
actionVisible.value = true;
|
||||
}
|
||||
|
||||
function handlePullDefaultDraft() {
|
||||
pullDefaultDraft(true);
|
||||
}
|
||||
|
||||
function handleMonthlyApprovalChange(payload: Api.WorkReport.Monthly.MonthlyReportApproveParams) {
|
||||
Object.assign(monthlyApprovalDraft, payload);
|
||||
}
|
||||
|
||||
async function handleActionSubmit(
|
||||
payload: Api.WorkReport.Common.StatusActionParams | Api.WorkReport.Monthly.MonthlyReportApproveParams,
|
||||
actionTypeOverride?: 'approve' | 'reject'
|
||||
) {
|
||||
if (!currentReportId.value) return;
|
||||
|
||||
const actionType = actionTypeOverride || currentActionType.value;
|
||||
actionSubmitting.value = true;
|
||||
let result;
|
||||
|
||||
if (props.reportType === 'weekly') {
|
||||
result =
|
||||
actionType === 'approve'
|
||||
? await fetchApproveWeeklyReport(currentReportId.value, payload)
|
||||
: await fetchRejectWeeklyReport(currentReportId.value, payload);
|
||||
} else if (props.reportType === 'monthly') {
|
||||
result =
|
||||
actionType === 'approve'
|
||||
? await fetchApproveMonthlyReport(
|
||||
currentReportId.value,
|
||||
payload as Api.WorkReport.Monthly.MonthlyReportApproveParams
|
||||
)
|
||||
: await fetchRejectMonthlyReport(currentReportId.value, payload);
|
||||
} else {
|
||||
result =
|
||||
actionType === 'approve'
|
||||
? await fetchApproveProjectReport(currentReportId.value, payload)
|
||||
: await fetchRejectProjectReport(currentReportId.value, payload);
|
||||
}
|
||||
actionSubmitting.value = false;
|
||||
|
||||
if (result.error) return;
|
||||
|
||||
actionVisible.value = false;
|
||||
window.$message?.success(actionType === 'approve' ? '审批已通过' : '工作报告已退回');
|
||||
visible.value = false;
|
||||
emit('submitted');
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<WorkReportPageDialog
|
||||
v-model:visible="visible"
|
||||
:title="dialogTitle"
|
||||
:loading="loading"
|
||||
:approval-mode="currentScene === 'approval'"
|
||||
@close="currentStage = 'form'"
|
||||
>
|
||||
<WeeklyReportPage
|
||||
v-if="reportType === 'weekly'"
|
||||
:report-type="REPORT_TYPE_TEXT[reportType]"
|
||||
:period="periodText"
|
||||
:mode="mode"
|
||||
:scene="currentScene"
|
||||
:base-info="baseInfo as Api.WorkReport.Weekly.WeeklyReport | null"
|
||||
:model="weeklyModel"
|
||||
@back="handleBack"
|
||||
@save="handleSaveDraft"
|
||||
@submit="handleSubmitReport"
|
||||
@request-approve="handleRequestApprove"
|
||||
@request-reject="handleRequestReject"
|
||||
@pull-default-draft="handlePullDefaultDraft"
|
||||
/>
|
||||
|
||||
<MonthlyReportApprovalPage
|
||||
v-else-if="reportType === 'monthly' && currentStage === 'approval'"
|
||||
:report-type="REPORT_TYPE_TEXT[reportType]"
|
||||
:period="periodText"
|
||||
:mode="mode"
|
||||
scene="approval"
|
||||
:base-info="baseInfo as Api.WorkReport.Monthly.MonthlyReport | null"
|
||||
:model="monthlyModel"
|
||||
:approval-model="monthlyApprovalDraft"
|
||||
@back="handleBack"
|
||||
@change-approval="handleMonthlyApprovalChange"
|
||||
@request-approve="handleRequestApprove"
|
||||
@request-reject="handleRequestReject"
|
||||
/>
|
||||
|
||||
<MonthlyReportPage
|
||||
v-else-if="reportType === 'monthly'"
|
||||
:report-type="REPORT_TYPE_TEXT[reportType]"
|
||||
:period="periodText"
|
||||
:mode="mode"
|
||||
:scene="currentScene"
|
||||
:base-info="baseInfo as Api.WorkReport.Monthly.MonthlyReport | null"
|
||||
:model="monthlyModel"
|
||||
@back="handleBack"
|
||||
@save="handleSaveDraft"
|
||||
@submit="handleSubmitReport"
|
||||
@view-approval="handleViewApproval"
|
||||
@pull-default-draft="handlePullDefaultDraft"
|
||||
/>
|
||||
|
||||
<ProjectReportPage
|
||||
v-else
|
||||
:report-type="REPORT_TYPE_TEXT[reportType]"
|
||||
:period="periodText"
|
||||
:mode="mode"
|
||||
:scene="currentScene"
|
||||
:base-info="baseInfo as Api.WorkReport.Project.ProjectReport | null"
|
||||
:model="projectModel"
|
||||
@back="handleBack"
|
||||
@save="handleSaveDraft"
|
||||
@submit="handleSubmitReport"
|
||||
@request-approve="handleRequestApprove"
|
||||
@request-reject="handleRequestReject"
|
||||
@pull-default-draft="handlePullDefaultDraft"
|
||||
/>
|
||||
</WorkReportPageDialog>
|
||||
|
||||
<WorkReportActionDialog
|
||||
v-model:visible="actionVisible"
|
||||
:report-type="reportType"
|
||||
:action-type="currentActionType"
|
||||
:initial-monthly-approve-data="reportType === 'monthly' ? monthlyApprovalDraft : null"
|
||||
:loading="actionSubmitting"
|
||||
@submit="handleActionSubmit"
|
||||
/>
|
||||
</template>
|
||||
@@ -0,0 +1,102 @@
|
||||
<script setup lang="ts">
|
||||
/* eslint-disable no-void */
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import { fetchGetWorkReportStatusDict } from '@/service/api';
|
||||
import type { SearchField } from '@/components/custom/table-search-fields.vue';
|
||||
import TableSearchFields from '@/components/custom/table-search-fields.vue';
|
||||
import { BOOLEAN_TRUE_FALSE_OPTIONS, type WorkReportSearchParams, type WorkReportType } from '../types';
|
||||
|
||||
defineOptions({ name: 'WorkReportSearch' });
|
||||
|
||||
interface Props {
|
||||
reportType: WorkReportType;
|
||||
projectOptions?: Api.WorkReport.Project.ProjectReportOwnerProjectOption[];
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
projectOptions: () => []
|
||||
});
|
||||
|
||||
const model = defineModel<WorkReportSearchParams>('model', { required: true });
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'reset'): void;
|
||||
(e: 'search'): void;
|
||||
}>();
|
||||
|
||||
const statusDict = ref<Api.WorkReport.Common.WorkReportStatusDict[]>([]);
|
||||
|
||||
const statusOptions = computed(() =>
|
||||
[...statusDict.value]
|
||||
.sort((a, b) => Number(a.sort || 0) - Number(b.sort || 0))
|
||||
.map(item => ({
|
||||
label: item.statusName || item.statusCode,
|
||||
value: item.statusCode
|
||||
}))
|
||||
);
|
||||
|
||||
const fields = computed<SearchField[]>(() => {
|
||||
const baseFields: SearchField[] = [
|
||||
{ key: 'statusCode', label: '状态', type: 'select', options: statusOptions.value, placeholder: '请选择状态' },
|
||||
{ key: 'periodStartDate', label: '周期', type: 'dateRange', placeholder: '请选择周期' }
|
||||
];
|
||||
|
||||
const monthPeriodField: SearchField = {
|
||||
key: 'periodStartDate',
|
||||
label: props.reportType === 'project' ? '月份' : '月份',
|
||||
type: 'dateRange',
|
||||
dateRangeType: 'monthrange',
|
||||
valueFormat: 'YYYY-MM-DD',
|
||||
placeholder: '请选择月份'
|
||||
};
|
||||
|
||||
if (props.reportType === 'weekly') {
|
||||
return [
|
||||
...baseFields,
|
||||
{
|
||||
key: 'isBusinessTrip',
|
||||
label: '是否出差',
|
||||
type: 'select',
|
||||
options: BOOLEAN_TRUE_FALSE_OPTIONS,
|
||||
placeholder: '请选择'
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
if (props.reportType === 'project') {
|
||||
return [
|
||||
baseFields[0],
|
||||
monthPeriodField,
|
||||
{
|
||||
key: 'projectId',
|
||||
label: '项目',
|
||||
type: 'select',
|
||||
options: props.projectOptions.map(item => ({
|
||||
label: item.projectCode ? `${item.projectName}(${item.projectCode})` : item.projectName,
|
||||
value: item.id
|
||||
})),
|
||||
placeholder: '请选择项目'
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
if (props.reportType === 'monthly') {
|
||||
return [baseFields[0], monthPeriodField];
|
||||
}
|
||||
|
||||
return baseFields;
|
||||
});
|
||||
|
||||
async function loadStatusDict() {
|
||||
const { error, data } = await fetchGetWorkReportStatusDict();
|
||||
statusDict.value = error || !data ? [] : data;
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadStatusDict();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<TableSearchFields v-model="model" :columns="4" :fields="fields" @reset="emit('reset')" @search="emit('search')" />
|
||||
</template>
|
||||
@@ -0,0 +1,92 @@
|
||||
<script setup lang="ts">
|
||||
import { WORK_REPORT_TYPE_LABEL, type WorkReportType } from '../types';
|
||||
|
||||
defineOptions({ name: 'WorkReportTabs' });
|
||||
|
||||
interface TabOption {
|
||||
label: string;
|
||||
name: WorkReportType;
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
tabs: TabOption[];
|
||||
}>();
|
||||
|
||||
const activeTab = defineModel<WorkReportType>('activeTab', { required: true });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ElCard class="work-report-sidebar" body-class="work-report-sidebar__body">
|
||||
<div class="work-report-sidebar__header">报告类型</div>
|
||||
<div class="work-report-sidebar__list">
|
||||
<div
|
||||
v-for="tab in tabs"
|
||||
:key="tab.name"
|
||||
class="work-report-sidebar__item"
|
||||
:class="{ 'work-report-sidebar__item--active': activeTab === tab.name }"
|
||||
@click="activeTab = tab.name"
|
||||
>
|
||||
<span class="work-report-sidebar__label">{{ tab.label || WORK_REPORT_TYPE_LABEL[tab.name] }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</ElCard>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.work-report-sidebar {
|
||||
height: 100%;
|
||||
border: 1px solid var(--el-border-color-light);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.work-report-sidebar :deep(.work-report-sidebar__body) {
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.work-report-sidebar__header {
|
||||
padding: 20px 20px 12px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-secondary);
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.work-report-sidebar__list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
padding: 0 12px;
|
||||
}
|
||||
|
||||
.work-report-sidebar__item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 12px 16px;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.work-report-sidebar__item:hover {
|
||||
background: var(--el-fill-color-light);
|
||||
}
|
||||
|
||||
.work-report-sidebar__item--active {
|
||||
background: var(--el-color-primary-light-9);
|
||||
}
|
||||
|
||||
.work-report-sidebar__item--active .work-report-sidebar__label {
|
||||
color: var(--el-color-primary);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.work-report-sidebar__label {
|
||||
font-size: 14px;
|
||||
color: var(--el-text-color-regular);
|
||||
line-height: 1;
|
||||
}
|
||||
</style>
|
||||
521
src/views/personal-center/work-report/shared/types.ts
Normal file
521
src/views/personal-center/work-report/shared/types.ts
Normal file
@@ -0,0 +1,521 @@
|
||||
import dayjs from 'dayjs';
|
||||
import type { PaginationData } from '@sa/hooks';
|
||||
import { getStatusTagType } from '@/constants/status-tag';
|
||||
|
||||
export type WorkReportType = Api.WorkReport.Common.ReportType;
|
||||
export type WorkReportRow =
|
||||
| Api.WorkReport.Weekly.WeeklyReport
|
||||
| Api.WorkReport.Monthly.MonthlyReport
|
||||
| Api.WorkReport.Project.ProjectReport;
|
||||
export type WorkReportSearchParams =
|
||||
| Api.WorkReport.Weekly.WeeklyReportSearchParams
|
||||
| Api.WorkReport.Monthly.MonthlyReportSearchParams
|
||||
| Api.WorkReport.Project.ProjectReportSearchParams;
|
||||
export type WorkReportSaveParams =
|
||||
| Api.WorkReport.Weekly.WeeklyReportSaveParams
|
||||
| Api.WorkReport.Monthly.MonthlyReportSaveParams
|
||||
| Api.WorkReport.Project.ProjectReportSaveParams;
|
||||
|
||||
export interface WorkReportStructuredTask {
|
||||
title: string;
|
||||
detail?: string;
|
||||
priority?: string | null;
|
||||
progress?: number | null;
|
||||
hours?: number | null;
|
||||
kind?: string | null;
|
||||
}
|
||||
|
||||
export interface WorkReportStructuredSection {
|
||||
category: string;
|
||||
tasks: WorkReportStructuredTask[];
|
||||
}
|
||||
|
||||
export const WORK_REPORT_PROJECT_OWNER_PERMISSION = 'project:work-report:project-owner';
|
||||
|
||||
export const WORK_REPORT_TYPE_LABEL: Record<WorkReportType, string> = {
|
||||
weekly: '个人周报',
|
||||
monthly: '个人月报',
|
||||
project: '项目半月报'
|
||||
};
|
||||
|
||||
export const WORK_REPORT_STATUS_LABEL: Record<string, string> = {
|
||||
draft: '待提交',
|
||||
pending_approval: '待审批',
|
||||
approved: '已通过',
|
||||
rejected: '已退回'
|
||||
};
|
||||
|
||||
export const PROJECT_REPORT_FLAG_OPTIONS = [
|
||||
{ label: '上半月', value: 1 },
|
||||
{ label: '下半月', value: 2 }
|
||||
];
|
||||
|
||||
export const BOOLEAN_TRUE_FALSE_OPTIONS = [
|
||||
{ label: '是', value: 'true' },
|
||||
{ label: '否', value: 'false' }
|
||||
];
|
||||
|
||||
export function getProjectReportFlagLabel(flag?: number | null) {
|
||||
return PROJECT_REPORT_FLAG_OPTIONS.find(item => item.value === flag)?.label || '--';
|
||||
}
|
||||
|
||||
export function getWorkReportStatusLabel(statusCode?: string | null, statusName?: string | null) {
|
||||
return statusName || WORK_REPORT_STATUS_LABEL[statusCode || ''] || statusCode || '--';
|
||||
}
|
||||
|
||||
export function resolveWorkReportStatusTagType(statusCode?: string | null) {
|
||||
return getStatusTagType('workReport', statusCode);
|
||||
}
|
||||
|
||||
export function formatEmptyText(value?: string | number | null) {
|
||||
if (value === null || value === undefined || value === '') {
|
||||
return '0';
|
||||
}
|
||||
|
||||
return String(value);
|
||||
}
|
||||
|
||||
export function formatDate(value?: string | null) {
|
||||
return value ? dayjs(value).format('YYYY-MM-DD') : '--';
|
||||
}
|
||||
|
||||
export function formatDateTime(value?: string | null) {
|
||||
return value ? dayjs(value).format('YYYY-MM-DD HH:mm') : '--';
|
||||
}
|
||||
|
||||
export function formatPeriodLabel(value?: string | null) {
|
||||
return String(value || '')
|
||||
.trim()
|
||||
.replace(/\s*(周报|月报|项目半月报|半月报)\s*$/u, '');
|
||||
}
|
||||
|
||||
export function formatPeriod(row: Pick<WorkReportRow, 'periodLabel' | 'periodStartDate' | 'periodEndDate'>) {
|
||||
return formatPeriodLabel(row.periodLabel) || `${formatDate(row.periodStartDate)} 至 ${formatDate(row.periodEndDate)}`;
|
||||
}
|
||||
|
||||
export function createInitBaseSearchParams() {
|
||||
return {
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
keyword: undefined,
|
||||
statusCode: undefined,
|
||||
periodStartDate: undefined,
|
||||
submitTime: undefined,
|
||||
supervisorName: undefined
|
||||
};
|
||||
}
|
||||
|
||||
export function createWeeklySearchParams(): Api.WorkReport.Weekly.WeeklyReportSearchParams {
|
||||
return {
|
||||
...createInitBaseSearchParams(),
|
||||
isBusinessTrip: undefined
|
||||
};
|
||||
}
|
||||
|
||||
export function createMonthlySearchParams(): Api.WorkReport.Monthly.MonthlyReportSearchParams {
|
||||
return createInitBaseSearchParams();
|
||||
}
|
||||
|
||||
export function createProjectSearchParams(): Api.WorkReport.Project.ProjectReportSearchParams {
|
||||
return {
|
||||
...createInitBaseSearchParams(),
|
||||
projectId: undefined,
|
||||
flag: undefined
|
||||
};
|
||||
}
|
||||
|
||||
export function transformWorkReportPage<T>(
|
||||
response: { data: Api.WorkReport.Common.PageResult<T> | null; error: unknown },
|
||||
pageNo: number,
|
||||
pageSize: number
|
||||
): PaginationData<T> {
|
||||
if (!response.error && response.data) {
|
||||
return {
|
||||
data: response.data.list,
|
||||
pageNum: pageNo,
|
||||
pageSize,
|
||||
total: response.data.total
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
data: [],
|
||||
pageNum: 1,
|
||||
pageSize,
|
||||
total: 0
|
||||
};
|
||||
}
|
||||
|
||||
export function createBlankReviewItem(index = 0): Api.WorkReport.Common.PersonalReportReviewItem {
|
||||
return {
|
||||
itemNumber: index + 1,
|
||||
itemTitle: '',
|
||||
workHours: 0,
|
||||
contentText: '',
|
||||
contentJson: null,
|
||||
reflectionText: ''
|
||||
};
|
||||
}
|
||||
|
||||
export function createBlankPlanItem(index = 0): Api.WorkReport.Common.PersonalReportPlanItem {
|
||||
return {
|
||||
itemNumber: index + 1,
|
||||
itemTitle: '',
|
||||
targetText: '',
|
||||
targetJson: null,
|
||||
supportNeed: ''
|
||||
};
|
||||
}
|
||||
|
||||
export function createBlankProjectItem(): Api.WorkReport.Project.ProjectReportItem {
|
||||
return {
|
||||
itemTitle: '',
|
||||
workHours: 0,
|
||||
priorityCode: undefined,
|
||||
progressRate: 0
|
||||
};
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
||||
}
|
||||
|
||||
function normalizeNumber(value: unknown): number | null {
|
||||
if (typeof value === 'number' && Number.isFinite(value)) return value;
|
||||
if (typeof value === 'string' && value.trim()) {
|
||||
const numberValue = Number(value);
|
||||
return Number.isFinite(numberValue) ? numberValue : null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function normalizeStructuredTask(value: unknown): WorkReportStructuredTask | null {
|
||||
if (!isRecord(value)) return null;
|
||||
|
||||
const title = String(value.title ?? value.name ?? '').trim();
|
||||
if (!title) return null;
|
||||
|
||||
return {
|
||||
title,
|
||||
detail: String(value.detail ?? value.content ?? '').trim(),
|
||||
priority: value.priority === null || value.priority === undefined ? null : String(value.priority),
|
||||
progress: normalizeNumber(value.progress),
|
||||
hours: normalizeNumber(value.hours),
|
||||
kind: value.kind === null || value.kind === undefined ? null : String(value.kind)
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeStructuredSection(value: unknown): WorkReportStructuredSection | null {
|
||||
if (!isRecord(value)) return null;
|
||||
|
||||
const category = String(value.category ?? value.title ?? value.name ?? '').trim();
|
||||
const rawTasks = Array.isArray(value.tasks) ? value.tasks : [];
|
||||
const tasks = rawTasks.map(normalizeStructuredTask).filter(Boolean) as WorkReportStructuredTask[];
|
||||
|
||||
if (!category && !tasks.length) return null;
|
||||
|
||||
return {
|
||||
category: category || '工作内容',
|
||||
tasks
|
||||
};
|
||||
}
|
||||
|
||||
function parseJsonLike(value: unknown): unknown {
|
||||
if (typeof value !== 'string') return value;
|
||||
|
||||
try {
|
||||
return JSON.parse(value);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function getStructuredTasks(value: unknown): WorkReportStructuredTask[] {
|
||||
const parsed = parseJsonLike(value);
|
||||
|
||||
if (Array.isArray(parsed)) {
|
||||
return parsed.map(normalizeStructuredTask).filter(Boolean) as WorkReportStructuredTask[];
|
||||
}
|
||||
|
||||
if (isRecord(parsed) && Array.isArray(parsed.tasks)) {
|
||||
return parsed.tasks.map(normalizeStructuredTask).filter(Boolean) as WorkReportStructuredTask[];
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
export function getStructuredSections(value: unknown): WorkReportStructuredSection[] {
|
||||
const parsed = parseJsonLike(value);
|
||||
|
||||
if (Array.isArray(parsed)) {
|
||||
return parsed.map(normalizeStructuredSection).filter(Boolean) as WorkReportStructuredSection[];
|
||||
}
|
||||
|
||||
if (isRecord(parsed) && Array.isArray(parsed.sections)) {
|
||||
return parsed.sections.map(normalizeStructuredSection).filter(Boolean) as WorkReportStructuredSection[];
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
function joinTextValues(...values: Array<string | null | undefined>) {
|
||||
return values
|
||||
.map(value => value?.trim())
|
||||
.filter(Boolean)
|
||||
.join('\n');
|
||||
}
|
||||
|
||||
function mergeStructuredSections(sections: WorkReportStructuredSection[]) {
|
||||
const sectionMap = new Map<string, WorkReportStructuredSection>();
|
||||
|
||||
sections.forEach(section => {
|
||||
const category = section.category || '工作内容';
|
||||
const existing = sectionMap.get(category);
|
||||
if (existing) {
|
||||
existing.tasks.push(...section.tasks);
|
||||
} else {
|
||||
sectionMap.set(category, { category, tasks: [...section.tasks] });
|
||||
}
|
||||
});
|
||||
|
||||
return Array.from(sectionMap.values());
|
||||
}
|
||||
|
||||
function mergeStructuredJson(current: unknown, next: unknown) {
|
||||
const currentSections = getStructuredSections(current);
|
||||
const nextSections = getStructuredSections(next);
|
||||
const sections = mergeStructuredSections([...currentSections, ...nextSections]);
|
||||
if (sections.length) return JSON.stringify({ sections });
|
||||
|
||||
const tasks = [...getStructuredTasks(current), ...getStructuredTasks(next)];
|
||||
if (tasks.length) return JSON.stringify({ tasks });
|
||||
|
||||
return current ?? next ?? null;
|
||||
}
|
||||
|
||||
function groupPersonalReportItemsByTitle<T extends { itemTitle: string }>(
|
||||
items: T[],
|
||||
merge: (target: T, source: T) => void
|
||||
) {
|
||||
const groupedItems: T[] = [];
|
||||
const itemMap = new Map<string, T>();
|
||||
|
||||
items.forEach((item, index) => {
|
||||
const title = item.itemTitle.trim();
|
||||
const key = title || `__blank_${index}`;
|
||||
const existing = itemMap.get(key);
|
||||
|
||||
if (existing) {
|
||||
merge(existing, item);
|
||||
return;
|
||||
}
|
||||
|
||||
itemMap.set(key, item);
|
||||
groupedItems.push(item);
|
||||
});
|
||||
|
||||
return groupedItems.map((item, index) => ({
|
||||
...item,
|
||||
itemNumber: index + 1
|
||||
}));
|
||||
}
|
||||
|
||||
export function normalizeReviewItems(items?: Api.WorkReport.Common.PersonalReportReviewItem[] | null) {
|
||||
const list = items?.length ? items : [createBlankReviewItem()];
|
||||
|
||||
const normalizedItems = list.map((item, index) => ({
|
||||
...item,
|
||||
itemNumber: item.itemNumber ?? index + 1,
|
||||
itemTitle: item.itemTitle || '',
|
||||
workHours: item.workHours ?? 0,
|
||||
contentText: item.contentText || '',
|
||||
contentJson: item.contentJson ?? null,
|
||||
reflectionText: item.reflectionText || ''
|
||||
}));
|
||||
|
||||
return groupPersonalReportItemsByTitle(normalizedItems, (target, source) => {
|
||||
target.workHours = Number(target.workHours || 0) + Number(source.workHours || 0);
|
||||
target.contentText = joinTextValues(target.contentText, source.contentText);
|
||||
target.contentJson = mergeStructuredJson(target.contentJson, source.contentJson);
|
||||
target.reflectionText = joinTextValues(target.reflectionText, source.reflectionText);
|
||||
});
|
||||
}
|
||||
|
||||
export function normalizePlanItems(items?: Api.WorkReport.Common.PersonalReportPlanItem[] | null) {
|
||||
const list = items?.length ? items : [createBlankPlanItem()];
|
||||
|
||||
const normalizedItems = list.map((item, index) => ({
|
||||
...item,
|
||||
itemNumber: item.itemNumber ?? index + 1,
|
||||
itemTitle: item.itemTitle || '',
|
||||
targetText: item.targetText || '',
|
||||
targetJson: item.targetJson ?? null,
|
||||
supportNeed: item.supportNeed || ''
|
||||
}));
|
||||
|
||||
return groupPersonalReportItemsByTitle(normalizedItems, (target, source) => {
|
||||
target.targetText = joinTextValues(target.targetText, source.targetText);
|
||||
target.targetJson = mergeStructuredJson(target.targetJson, source.targetJson);
|
||||
target.supportNeed = joinTextValues(target.supportNeed, source.supportNeed);
|
||||
});
|
||||
}
|
||||
|
||||
export function normalizeProjectItems(items?: Api.WorkReport.Project.ProjectReportItem[] | null) {
|
||||
const list = items?.length ? items : [createBlankProjectItem()];
|
||||
|
||||
return list.map(item => ({
|
||||
...item,
|
||||
itemTitle: item.itemTitle || '',
|
||||
workHours: item.workHours ?? 0,
|
||||
priorityCode: item.priorityCode || undefined,
|
||||
progressRate: item.progressRate ?? 0
|
||||
}));
|
||||
}
|
||||
|
||||
export function createWeeklySaveParams(
|
||||
base?: Partial<Api.WorkReport.Weekly.WeeklyReportSaveParams>
|
||||
): Api.WorkReport.Weekly.WeeklyReportSaveParams {
|
||||
return {
|
||||
periodKey: base?.periodKey || '',
|
||||
periodLabel: base?.periodLabel || '',
|
||||
periodStartDate: base?.periodStartDate || '',
|
||||
periodEndDate: base?.periodEndDate || '',
|
||||
isBusinessTrip: base?.isBusinessTrip ?? false,
|
||||
reviewItems: normalizeReviewItems(base?.reviewItems),
|
||||
planItems: normalizePlanItems(base?.planItems),
|
||||
travelSegments: base?.travelSegments?.length ? base.travelSegments : []
|
||||
};
|
||||
}
|
||||
|
||||
export function createMonthlySaveParams(
|
||||
base?: Partial<Api.WorkReport.Monthly.MonthlyReportSaveParams>
|
||||
): Api.WorkReport.Monthly.MonthlyReportSaveParams {
|
||||
return {
|
||||
periodKey: base?.periodKey || '',
|
||||
periodLabel: base?.periodLabel || '',
|
||||
periodStartDate: base?.periodStartDate || '',
|
||||
periodEndDate: base?.periodEndDate || '',
|
||||
reviewItems: normalizeReviewItems(base?.reviewItems),
|
||||
planItems: normalizePlanItems(base?.planItems)
|
||||
};
|
||||
}
|
||||
|
||||
export function createProjectSaveParams(
|
||||
base?: Partial<Api.WorkReport.Project.ProjectReportSaveParams>
|
||||
): Api.WorkReport.Project.ProjectReportSaveParams {
|
||||
const defaultParams: Api.WorkReport.Project.ProjectReportSaveParams = {
|
||||
projectId: '',
|
||||
periodKey: '',
|
||||
periodLabel: '',
|
||||
periodStartDate: '',
|
||||
periodEndDate: '',
|
||||
flag: 1,
|
||||
projectStatusDesc: '',
|
||||
projectProgressPlan: '',
|
||||
projectKeyPoints: '',
|
||||
projectProblems: '',
|
||||
currentItems: [createBlankProjectItem()],
|
||||
nextItems: [createBlankProjectItem()]
|
||||
};
|
||||
|
||||
return {
|
||||
...Object.assign(defaultParams, base),
|
||||
currentItems: normalizeProjectItems(base?.currentItems),
|
||||
nextItems: normalizeProjectItems(base?.nextItems)
|
||||
};
|
||||
}
|
||||
|
||||
type NavigatorWithLegacySave = Navigator & {
|
||||
msSaveOrOpenBlob?: (blob: Blob, defaultName?: string) => boolean;
|
||||
};
|
||||
|
||||
export function downloadBlob(blob: Blob, filename: string) {
|
||||
if (!(blob instanceof Blob)) {
|
||||
window.$message?.error(getBlobErrorMessage(blob) || '导出失败:接口未返回文件流');
|
||||
return;
|
||||
}
|
||||
|
||||
const downloadFile =
|
||||
blob instanceof File ? blob : new File([blob], filename, { type: blob.type || 'application/octet-stream' });
|
||||
const legacyNavigator = window.navigator as NavigatorWithLegacySave;
|
||||
|
||||
if (typeof legacyNavigator.msSaveOrOpenBlob === 'function') {
|
||||
legacyNavigator.msSaveOrOpenBlob(downloadFile, filename);
|
||||
window.$message?.success('导出文件已开始下载');
|
||||
return;
|
||||
}
|
||||
|
||||
const url = window.URL.createObjectURL(downloadFile);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = filename;
|
||||
link.style.display = 'none';
|
||||
link.rel = 'noopener';
|
||||
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
|
||||
window.setTimeout(() => {
|
||||
window.URL.revokeObjectURL(url);
|
||||
}, 1000);
|
||||
window.$message?.success('导出文件已开始下载');
|
||||
}
|
||||
|
||||
function getBlobErrorMessage(value: unknown) {
|
||||
if (!isRecord(value)) return '';
|
||||
|
||||
const message = value.msg || value.message || value.error;
|
||||
return typeof message === 'string' && message.trim() ? message.trim() : '';
|
||||
}
|
||||
|
||||
function safeDecodeFilename(value: string) {
|
||||
try {
|
||||
return decodeURIComponent(value);
|
||||
} catch {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
function getResponseHeader(headers: unknown, headerName: string) {
|
||||
if (!headers) return '';
|
||||
|
||||
if (typeof (headers as { get?: unknown }).get === 'function') {
|
||||
const value = (headers as { get: (name: string) => unknown }).get(headerName);
|
||||
return value === null || value === undefined ? '' : String(value);
|
||||
}
|
||||
|
||||
if (!isRecord(headers)) return '';
|
||||
|
||||
const matchedKey = Object.keys(headers).find(key => key.toLowerCase() === headerName.toLowerCase());
|
||||
if (!matchedKey) return '';
|
||||
|
||||
const value = headers[matchedKey];
|
||||
return value === null || value === undefined ? '' : String(value);
|
||||
}
|
||||
|
||||
export function getFilenameFromDisposition(disposition?: string | null) {
|
||||
if (!disposition) return '';
|
||||
|
||||
const filenameStarMatch = disposition.match(/filename\*=UTF-8''([^;]+)/i);
|
||||
if (filenameStarMatch?.[1]) {
|
||||
return safeDecodeFilename(filenameStarMatch[1].replace(/^"|"$/g, ''));
|
||||
}
|
||||
|
||||
const filenameMatch = disposition.match(/filename="?([^";]+)"?/i);
|
||||
if (filenameMatch?.[1]) {
|
||||
return safeDecodeFilename(filenameMatch[1]);
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
export function resolveExportFilename(result: { response?: { headers?: unknown } }, fallbackName: string) {
|
||||
const disposition = getResponseHeader(result.response?.headers, 'content-disposition');
|
||||
return getFilenameFromDisposition(disposition) || fallbackName;
|
||||
}
|
||||
|
||||
export function createWorkReportContentExportFallbackName(reportType: WorkReportType, reportCount: number) {
|
||||
const extension = reportCount === 1 ? 'docx' : 'zip';
|
||||
return `${WORK_REPORT_TYPE_LABEL[reportType]}_${dayjs().format('YYYY-MM-DD')}.${extension}`;
|
||||
}
|
||||
194
src/views/personal-center/work-report/shared/utils.ts
Normal file
194
src/views/personal-center/work-report/shared/utils.ts
Normal file
@@ -0,0 +1,194 @@
|
||||
import dayjs from 'dayjs';
|
||||
import isoWeek from 'dayjs/plugin/isoWeek';
|
||||
import weekday from 'dayjs/plugin/weekday';
|
||||
import type { WorkReportType } from './types';
|
||||
|
||||
dayjs.extend(isoWeek);
|
||||
dayjs.extend(weekday);
|
||||
|
||||
export interface WorkReportPeriodOption {
|
||||
key: string;
|
||||
label: string;
|
||||
description: string;
|
||||
reportType: WorkReportType;
|
||||
flag?: number;
|
||||
period: {
|
||||
periodKey: string;
|
||||
periodLabel: string;
|
||||
periodStartDate: string;
|
||||
periodEndDate: string;
|
||||
};
|
||||
}
|
||||
|
||||
function formatRangeLabel(start: dayjs.Dayjs, end: dayjs.Dayjs) {
|
||||
return `${start.format('YYYY-MM-DD')} 至 ${end.format('YYYY-MM-DD')}`;
|
||||
}
|
||||
|
||||
export function formatPeriodDisplayLabel(label?: string | null) {
|
||||
return String(label || '')
|
||||
.trim()
|
||||
.replace(/\s*(周报|月报|项目半月报|半月报)\s*$/u, '');
|
||||
}
|
||||
|
||||
export function getIsoWeekDisplay(date: string | dayjs.Dayjs) {
|
||||
const selectedDate = dayjs(date);
|
||||
if (!selectedDate.isValid()) return '';
|
||||
return `${selectedDate.format('YYYY')} 第${String(selectedDate.isoWeek()).padStart(2, '0')} 周`;
|
||||
}
|
||||
|
||||
/* eslint-disable-next-line max-params */
|
||||
function buildPeriod(reportType: WorkReportType, start: dayjs.Dayjs, end: dayjs.Dayjs, label: string, flag?: number) {
|
||||
const startText = start.format('YYYY-MM-DD');
|
||||
const endText = end.format('YYYY-MM-DD');
|
||||
|
||||
return {
|
||||
periodKey: flag ? `${reportType}-${startText}-${endText}-${flag}` : `${reportType}-${startText}-${endText}`,
|
||||
periodLabel: label,
|
||||
periodStartDate: startText,
|
||||
periodEndDate: endText
|
||||
};
|
||||
}
|
||||
|
||||
export function buildWeeklyPeriodFromDate(date: string | dayjs.Dayjs) {
|
||||
const selectedDate = dayjs(date);
|
||||
const start = selectedDate.startOf('isoWeek');
|
||||
const end = selectedDate.endOf('isoWeek');
|
||||
|
||||
return buildPeriod('weekly', start, end, `${formatRangeLabel(start, end)} 周报`);
|
||||
}
|
||||
|
||||
export function buildMonthlyPeriodFromMonth(month: string | dayjs.Dayjs) {
|
||||
const selectedMonth = dayjs(month);
|
||||
const start = selectedMonth.startOf('month');
|
||||
const end = selectedMonth.endOf('month');
|
||||
|
||||
return buildPeriod('monthly', start, end, `${selectedMonth.format('YYYY-MM')} 月报`);
|
||||
}
|
||||
|
||||
export function buildProjectPeriodFromMonth(month: string | dayjs.Dayjs, flag: number) {
|
||||
const selectedMonth = dayjs(month);
|
||||
const monthStart = selectedMonth.startOf('month');
|
||||
|
||||
if (flag === 2) {
|
||||
const start = monthStart.date(16);
|
||||
const end = selectedMonth.endOf('month');
|
||||
return buildPeriod('project', start, end, `${selectedMonth.format('YYYY-MM')} 下半月`, 2);
|
||||
}
|
||||
|
||||
const start = monthStart.startOf('month');
|
||||
const end = monthStart.date(15);
|
||||
return buildPeriod('project', start, end, `${selectedMonth.format('YYYY-MM')} 上半月`, 1);
|
||||
}
|
||||
|
||||
export function getWeeklyPeriodOptions(now = dayjs()): WorkReportPeriodOption[] {
|
||||
const thisWeekStart = now.startOf('isoWeek');
|
||||
const thisWeekEnd = now.endOf('isoWeek');
|
||||
const lastWeekStart = thisWeekStart.subtract(1, 'week');
|
||||
const lastWeekEnd = thisWeekEnd.subtract(1, 'week');
|
||||
|
||||
return [
|
||||
{
|
||||
key: 'current-week',
|
||||
label: '本周',
|
||||
description: formatRangeLabel(thisWeekStart, thisWeekEnd),
|
||||
reportType: 'weekly',
|
||||
period: buildPeriod('weekly', thisWeekStart, thisWeekEnd, `${formatRangeLabel(thisWeekStart, thisWeekEnd)} 周报`)
|
||||
},
|
||||
{
|
||||
key: 'last-week',
|
||||
label: '上周',
|
||||
description: formatRangeLabel(lastWeekStart, lastWeekEnd),
|
||||
reportType: 'weekly',
|
||||
period: buildPeriod('weekly', lastWeekStart, lastWeekEnd, `${formatRangeLabel(lastWeekStart, lastWeekEnd)} 周报`)
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
export function getMonthlyPeriodOptions(now = dayjs()): WorkReportPeriodOption[] {
|
||||
const thisMonthStart = now.startOf('month');
|
||||
const thisMonthEnd = now.endOf('month');
|
||||
const lastMonth = now.subtract(1, 'month');
|
||||
const lastMonthStart = lastMonth.startOf('month');
|
||||
const lastMonthEnd = lastMonth.endOf('month');
|
||||
|
||||
return [
|
||||
{
|
||||
key: 'current-month',
|
||||
label: '本月',
|
||||
description: thisMonthStart.format('YYYY-MM'),
|
||||
reportType: 'monthly',
|
||||
period: buildPeriod('monthly', thisMonthStart, thisMonthEnd, `${thisMonthStart.format('YYYY-MM')} 月报`)
|
||||
},
|
||||
{
|
||||
key: 'last-month',
|
||||
label: '上月',
|
||||
description: lastMonthStart.format('YYYY-MM'),
|
||||
reportType: 'monthly',
|
||||
period: buildPeriod('monthly', lastMonthStart, lastMonthEnd, `${lastMonthStart.format('YYYY-MM')} 月报`)
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
export function getProjectPeriodOptions(now = dayjs()): WorkReportPeriodOption[] {
|
||||
const currentMonthStart = now.startOf('month');
|
||||
const currentMonthEnd = now.endOf('month');
|
||||
const currentFirstHalfEnd = currentMonthStart.date(15);
|
||||
const currentSecondHalfStart = currentMonthStart.date(16);
|
||||
const previousMonth = now.subtract(1, 'month');
|
||||
const previousMonthStart = previousMonth.startOf('month');
|
||||
const previousMonthEnd = previousMonth.endOf('month');
|
||||
const previousSecondHalfStart = previousMonthStart.date(16);
|
||||
const isCurrentFirstHalf = now.date() <= 15;
|
||||
|
||||
const currentOption: WorkReportPeriodOption = isCurrentFirstHalf
|
||||
? {
|
||||
key: 'current-first-half',
|
||||
label: '上半月',
|
||||
description: `${now.format('YYYY-MM')} 上半月`,
|
||||
reportType: 'project',
|
||||
flag: 1,
|
||||
period: buildPeriod('project', currentMonthStart, currentFirstHalfEnd, `${now.format('YYYY-MM')} 上半月`, 1)
|
||||
}
|
||||
: {
|
||||
key: 'current-second-half',
|
||||
label: '下半月',
|
||||
description: `${now.format('YYYY-MM')} 下半月`,
|
||||
reportType: 'project',
|
||||
flag: 2,
|
||||
period: buildPeriod('project', currentSecondHalfStart, currentMonthEnd, `${now.format('YYYY-MM')} 下半月`, 2)
|
||||
};
|
||||
|
||||
const previousOption: WorkReportPeriodOption = isCurrentFirstHalf
|
||||
? {
|
||||
key: 'previous-second-half',
|
||||
label: '下半月',
|
||||
description: `${previousMonth.format('YYYY-MM')} 下半月`,
|
||||
reportType: 'project',
|
||||
flag: 2,
|
||||
period: buildPeriod(
|
||||
'project',
|
||||
previousSecondHalfStart,
|
||||
previousMonthEnd,
|
||||
`${previousMonth.format('YYYY-MM')} 下半月`,
|
||||
2
|
||||
)
|
||||
}
|
||||
: {
|
||||
key: 'previous-first-half',
|
||||
label: '上半月',
|
||||
description: `${now.format('YYYY-MM')} 上半月`,
|
||||
reportType: 'project',
|
||||
flag: 1,
|
||||
period: buildPeriod('project', currentMonthStart, currentFirstHalfEnd, `${now.format('YYYY-MM')} 上半月`, 1)
|
||||
};
|
||||
|
||||
return [currentOption, previousOption];
|
||||
}
|
||||
|
||||
export function getReportTypePeriodOptions(now = dayjs()) {
|
||||
return {
|
||||
weekly: getWeeklyPeriodOptions(now),
|
||||
monthly: getMonthlyPeriodOptions(now),
|
||||
project: getProjectPeriodOptions(now)
|
||||
} as const;
|
||||
}
|
||||
373
src/views/personal-center/work-report/weekly/index.vue
Normal file
373
src/views/personal-center/work-report/weekly/index.vue
Normal file
@@ -0,0 +1,373 @@
|
||||
<script setup lang="tsx">
|
||||
/* eslint-disable no-void */
|
||||
import { markRaw, reactive, ref } from 'vue';
|
||||
import { ElMessageBox, ElTag, ElTooltip } from 'element-plus';
|
||||
import {
|
||||
fetchDeleteWeeklyReport,
|
||||
fetchExportWeeklyReportContent,
|
||||
fetchGetWeeklyReportPage,
|
||||
fetchSubmitWeeklyReport
|
||||
} from '@/service/api';
|
||||
import { useAuth } from '@/hooks/business/auth';
|
||||
import { useUIPaginatedTable } from '@/hooks/common/table';
|
||||
import BusinessTableActionCell, { type BusinessTableAction } from '@/components/custom/business-table-action-cell';
|
||||
import {
|
||||
WORK_REPORT_TYPE_LABEL,
|
||||
type WorkReportRow,
|
||||
createWeeklySearchParams,
|
||||
createWorkReportContentExportFallbackName,
|
||||
downloadBlob,
|
||||
formatDateTime,
|
||||
formatEmptyText,
|
||||
formatPeriod,
|
||||
getWorkReportStatusLabel,
|
||||
resolveExportFilename,
|
||||
resolveWorkReportStatusTagType,
|
||||
transformWorkReportPage
|
||||
} from '../shared/types';
|
||||
import { getIsoWeekDisplay } from '../shared/utils';
|
||||
import WeeklyReportSearch from './modules/search-panel.vue';
|
||||
import IconMdiDeleteOutline from '~icons/mdi/delete-outline';
|
||||
import IconMdiEyeOutline from '~icons/mdi/eye-outline';
|
||||
import IconMdiFileDocumentCheckOutline from '~icons/mdi/file-document-check-outline';
|
||||
import IconMdiPencilOutline from '~icons/mdi/pencil-outline';
|
||||
import IconMdiSendOutline from '~icons/mdi/send-outline';
|
||||
|
||||
defineOptions({ name: 'WeeklyWorkReportIndex' });
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'create'): void;
|
||||
(e: 'edit', row: WorkReportRow): void;
|
||||
(e: 'detail', row: WorkReportRow): void;
|
||||
(e: 'approvalRecord', row: WorkReportRow): void;
|
||||
}>();
|
||||
|
||||
const { hasAuth } = useAuth();
|
||||
const exporting = ref(false);
|
||||
const selectedRows = ref<Api.WorkReport.Weekly.WeeklyReport[]>([]);
|
||||
const searchParams = reactive(createWeeklySearchParams());
|
||||
|
||||
const ACTION_ICON_MAP = {
|
||||
detail: markRaw(IconMdiEyeOutline),
|
||||
edit: markRaw(IconMdiPencilOutline),
|
||||
submit: markRaw(IconMdiSendOutline),
|
||||
delete: markRaw(IconMdiDeleteOutline),
|
||||
approvalRecord: markRaw(IconMdiFileDocumentCheckOutline)
|
||||
};
|
||||
|
||||
const table = useUIPaginatedTable<
|
||||
Awaited<ReturnType<typeof fetchGetWeeklyReportPage>>,
|
||||
Api.WorkReport.Weekly.WeeklyReport
|
||||
>({
|
||||
paginationProps: { currentPage: searchParams.pageNo, pageSize: searchParams.pageSize },
|
||||
api: () => fetchGetWeeklyReportPage(searchParams),
|
||||
transform: response => transformWorkReportPage(response, searchParams.pageNo ?? 1, searchParams.pageSize ?? 10),
|
||||
onPaginationParamsChange: params => {
|
||||
searchParams.pageNo = params.currentPage ?? 1;
|
||||
searchParams.pageSize = params.pageSize ?? 10;
|
||||
},
|
||||
columns: () => [
|
||||
{ prop: 'index', type: 'index', label: '序号', width: 64 },
|
||||
{
|
||||
prop: 'periodLabel',
|
||||
label: '周期',
|
||||
minWidth: 150,
|
||||
formatter: row => {
|
||||
const periodText = formatPeriod(row);
|
||||
const weekLabel = getIsoWeekDisplay(row.periodStartDate);
|
||||
if (!weekLabel) return periodText;
|
||||
return (
|
||||
<ElTooltip content={weekLabel} placement="top">
|
||||
<span>{periodText}</span>
|
||||
</ElTooltip>
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'reporterDeptName',
|
||||
label: '部门/方向',
|
||||
minWidth: 80,
|
||||
showOverflowTooltip: true,
|
||||
formatter: row => row.reporterDeptName || '--'
|
||||
},
|
||||
{ prop: 'supervisorName', label: '直属上级', minWidth: 80 },
|
||||
{ prop: 'totalWorkHours', label: '总工时', minWidth: 80, formatter: row => formatEmptyText(row.totalWorkHours) },
|
||||
{
|
||||
prop: 'isBusinessTrip',
|
||||
label: '出差',
|
||||
minWidth: 80,
|
||||
align: 'center',
|
||||
formatter: row => (row.isBusinessTrip ? '是' : '否')
|
||||
},
|
||||
{
|
||||
prop: 'totalTravelDays',
|
||||
label: '出差天数',
|
||||
minWidth: 90,
|
||||
formatter: row => formatEmptyText(row.totalTravelDays)
|
||||
},
|
||||
{
|
||||
prop: 'statusCode',
|
||||
label: '状态',
|
||||
minWidth: 80,
|
||||
align: 'center',
|
||||
formatter: row => (
|
||||
<ElTag type={resolveWorkReportStatusTagType(row.statusCode)}>
|
||||
{getWorkReportStatusLabel(row.statusCode, row.statusName)}
|
||||
</ElTag>
|
||||
)
|
||||
},
|
||||
{ prop: 'submitTime', label: '提交时间', minWidth: 100, formatter: row => formatDateTime(row.submitTime) },
|
||||
{ prop: 'approvalTime', label: '审批时间', minWidth: 100, formatter: row => formatDateTime(row.approvalTime) },
|
||||
{
|
||||
prop: 'operate',
|
||||
label: '操作',
|
||||
width: 180,
|
||||
align: 'center',
|
||||
fixed: 'right',
|
||||
formatter: row => <BusinessTableActionCell actions={getRowActions(row)} variant="icon" />
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
function getRowActions(row: Api.WorkReport.Weekly.WeeklyReport): BusinessTableAction[] {
|
||||
const actions: BusinessTableAction[] = [
|
||||
{
|
||||
key: 'detail',
|
||||
label: '详情',
|
||||
buttonType: 'primary',
|
||||
icon: ACTION_ICON_MAP.detail,
|
||||
onClick: () => emit('detail', row)
|
||||
}
|
||||
];
|
||||
|
||||
if (['draft', 'rejected'].includes(row.statusCode) && row.allowEdit && hasAuth('project:work-report:update')) {
|
||||
actions.push({
|
||||
key: 'edit',
|
||||
label: '编辑',
|
||||
buttonType: 'primary',
|
||||
icon: ACTION_ICON_MAP.edit,
|
||||
onClick: () => emit('edit', row)
|
||||
});
|
||||
actions.push({
|
||||
key: 'submit',
|
||||
label: row.statusCode === 'draft' ? '提交' : '重新提交',
|
||||
buttonType: 'success',
|
||||
icon: ACTION_ICON_MAP.submit,
|
||||
onClick: () => handleSubmitReport(row)
|
||||
});
|
||||
}
|
||||
|
||||
if (row.statusCode === 'draft' && hasAuth('project:work-report:delete')) {
|
||||
actions.push({
|
||||
key: 'delete',
|
||||
label: '删除',
|
||||
buttonType: 'danger',
|
||||
icon: ACTION_ICON_MAP.delete,
|
||||
onClick: () => handleDelete(row)
|
||||
});
|
||||
}
|
||||
|
||||
if (['approved', 'rejected'].includes(row.statusCode)) {
|
||||
actions.push({
|
||||
key: 'approval-record',
|
||||
label: '审批记录',
|
||||
buttonType: 'info',
|
||||
icon: ACTION_ICON_MAP.approvalRecord,
|
||||
onClick: () => emit('approvalRecord', row)
|
||||
});
|
||||
}
|
||||
|
||||
return actions;
|
||||
}
|
||||
|
||||
async function reload(page?: number) {
|
||||
await table.getDataByPage(page ?? searchParams.pageNo ?? 1);
|
||||
}
|
||||
|
||||
function resetSearchParams() {
|
||||
const pageSize = searchParams.pageSize ?? 10;
|
||||
Object.assign(searchParams, createWeeklySearchParams(), { pageSize });
|
||||
reload(1);
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
reload(1);
|
||||
}
|
||||
|
||||
async function handleSubmitReport(row: Api.WorkReport.Weekly.WeeklyReport) {
|
||||
try {
|
||||
await ElMessageBox.confirm('确认提交该报告吗?', '提交确认', {
|
||||
type: 'warning',
|
||||
confirmButtonText: row.statusCode === 'draft' ? '确认提交' : '确认重新提交',
|
||||
cancelButtonText: '取消'
|
||||
});
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await fetchSubmitWeeklyReport(row.id);
|
||||
|
||||
if (result.error) return;
|
||||
window.$message?.success('工作报告已提交');
|
||||
await reload();
|
||||
}
|
||||
|
||||
async function handleDelete(row: Api.WorkReport.Weekly.WeeklyReport) {
|
||||
try {
|
||||
await ElMessageBox.confirm(`确认删除 ${formatPeriod(row)} 吗?`, '删除确认', {
|
||||
type: 'warning',
|
||||
confirmButtonText: '确认',
|
||||
cancelButtonText: '取消'
|
||||
});
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await fetchDeleteWeeklyReport(row.id);
|
||||
|
||||
if (result.error) return;
|
||||
|
||||
window.$message?.success('工作报告已删除');
|
||||
await reload();
|
||||
}
|
||||
|
||||
function handleSelectionChange(rows: Api.WorkReport.Weekly.WeeklyReport[]) {
|
||||
selectedRows.value = rows;
|
||||
}
|
||||
|
||||
function createExportSearchParams() {
|
||||
const { pageNo: _pageNo, pageSize: _pageSize, ...params } = searchParams;
|
||||
return params;
|
||||
}
|
||||
|
||||
async function exportReportContent(
|
||||
params: Api.WorkReport.Common.ContentExportParams<Api.WorkReport.Weekly.WeeklyReportSearchParams>,
|
||||
reportCount: number
|
||||
) {
|
||||
exporting.value = true;
|
||||
const result = await fetchExportWeeklyReportContent(params);
|
||||
exporting.value = false;
|
||||
|
||||
if (result.error || !result.data) return;
|
||||
|
||||
const fallbackName = createWorkReportContentExportFallbackName('weekly', reportCount);
|
||||
downloadBlob(result.data, resolveExportFilename(result, fallbackName));
|
||||
}
|
||||
|
||||
async function handleExportSelected() {
|
||||
if (!selectedRows.value.length) {
|
||||
window.$message?.warning('请选择要导出的报告');
|
||||
return;
|
||||
}
|
||||
|
||||
await exportReportContent(
|
||||
{
|
||||
exportAll: false,
|
||||
ids: selectedRows.value.map(item => item.id)
|
||||
},
|
||||
selectedRows.value.length
|
||||
);
|
||||
}
|
||||
|
||||
async function handleExportAll() {
|
||||
const total = table.mobilePagination.value.total || 0;
|
||||
if (!total) {
|
||||
window.$message?.warning('暂无可导出的报告');
|
||||
return;
|
||||
}
|
||||
|
||||
await exportReportContent(
|
||||
{
|
||||
...createExportSearchParams(),
|
||||
exportAll: true,
|
||||
ids: []
|
||||
},
|
||||
total
|
||||
);
|
||||
}
|
||||
|
||||
async function handleExportCommand(command: 'selected' | 'all') {
|
||||
if (command === 'selected') {
|
||||
await handleExportSelected();
|
||||
return;
|
||||
}
|
||||
|
||||
await handleExportAll();
|
||||
}
|
||||
|
||||
defineExpose({ reload });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex-col-stretch gap-16px overflow-hidden">
|
||||
<WeeklyReportSearch v-model:model="searchParams" @reset="resetSearchParams" @search="handleSearch" />
|
||||
|
||||
<ElCard class="flex-1-hidden card-wrapper" body-class="business-table-card-body">
|
||||
<template #header>
|
||||
<div class="flex flex-wrap items-center justify-between gap-12px">
|
||||
<div class="flex items-center gap-10px">
|
||||
<p class="text-16px font-600">{{ WORK_REPORT_TYPE_LABEL.weekly }}</p>
|
||||
<ElTag effect="plain">{{ table.mobilePagination.value.total || 0 }}</ElTag>
|
||||
</div>
|
||||
|
||||
<TableHeaderOperation
|
||||
v-model:columns="table.columnChecks.value"
|
||||
:loading="table.loading.value"
|
||||
@refresh="reload()"
|
||||
>
|
||||
<template #default>
|
||||
<ElDropdown v-auth="'project:work-report:export'" trigger="click" @command="handleExportCommand">
|
||||
<ElButton plain :loading="exporting">
|
||||
<template #icon>
|
||||
<icon-mdi-download class="text-icon" />
|
||||
</template>
|
||||
导出
|
||||
</ElButton>
|
||||
<template #dropdown>
|
||||
<ElDropdownMenu>
|
||||
<ElDropdownItem command="selected" :disabled="exporting || !selectedRows.length">
|
||||
导出选中
|
||||
</ElDropdownItem>
|
||||
<ElDropdownItem command="all" :disabled="exporting">导出全部</ElDropdownItem>
|
||||
</ElDropdownMenu>
|
||||
</template>
|
||||
</ElDropdown>
|
||||
<ElButton v-auth="'project:work-report:create'" plain type="primary" @click="emit('create')">
|
||||
<template #icon>
|
||||
<icon-ic-round-plus class="text-icon" />
|
||||
</template>
|
||||
新增
|
||||
</ElButton>
|
||||
</template>
|
||||
</TableHeaderOperation>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="flex-1">
|
||||
<ElTable
|
||||
v-loading="table.loading.value"
|
||||
height="100%"
|
||||
border
|
||||
row-key="id"
|
||||
:data="table.data.value"
|
||||
@selection-change="handleSelectionChange"
|
||||
>
|
||||
<ElTableColumn type="selection" width="48" />
|
||||
<template v-for="col in table.columns.value" :key="String(col.prop)">
|
||||
<ElTableColumn v-bind="col" />
|
||||
</template>
|
||||
</ElTable>
|
||||
</div>
|
||||
|
||||
<div class="mt-20px flex justify-end">
|
||||
<ElPagination
|
||||
v-if="table.mobilePagination.value.total"
|
||||
layout="total,prev,pager,next,sizes"
|
||||
v-bind="table.mobilePagination.value"
|
||||
@current-change="table.mobilePagination.value['current-change']"
|
||||
@size-change="table.mobilePagination.value['size-change']"
|
||||
/>
|
||||
</div>
|
||||
</ElCard>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,16 @@
|
||||
<script setup lang="ts">
|
||||
import SharedWorkReportApprovalRecordDialog from '../../shared/components/approval-record-dialog.vue';
|
||||
import type { WorkReportRow } from '../../shared/types';
|
||||
|
||||
defineOptions({ name: 'WeeklyReportApprovalRecordDialog' });
|
||||
|
||||
const visible = defineModel<boolean>('visible', { default: false });
|
||||
|
||||
defineProps<{
|
||||
rowData?: WorkReportRow | null;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SharedWorkReportApprovalRecordDialog v-model:visible="visible" report-type="weekly" :row-data="rowData" />
|
||||
</template>
|
||||
@@ -0,0 +1,16 @@
|
||||
<script setup lang="ts">
|
||||
import SharedWorkReportDetailDialog from '../../shared/components/detail-dialog.vue';
|
||||
import type { WorkReportRow } from '../../shared/types';
|
||||
|
||||
defineOptions({ name: 'WeeklyReportDetailPage' });
|
||||
|
||||
const visible = defineModel<boolean>('visible', { default: false });
|
||||
|
||||
defineProps<{
|
||||
rowData?: WorkReportRow | null;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SharedWorkReportDetailDialog v-model:visible="visible" report-type="weekly" :row-data="rowData" />
|
||||
</template>
|
||||
2505
src/views/personal-center/work-report/weekly/modules/fill-page.vue
Normal file
2505
src/views/personal-center/work-report/weekly/modules/fill-page.vue
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,26 @@
|
||||
<script setup lang="ts">
|
||||
import SharedWorkReportSearch from '../../shared/components/search-panel.vue';
|
||||
|
||||
defineOptions({ name: 'WeeklyReportSearch' });
|
||||
|
||||
defineProps<{
|
||||
projectOptions?: Api.WorkReport.Project.ProjectReportOwnerProjectOption[];
|
||||
}>();
|
||||
|
||||
const model = defineModel<Api.WorkReport.Weekly.WeeklyReportSearchParams>('model', { required: true });
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'reset'): void;
|
||||
(e: 'search'): void;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SharedWorkReportSearch
|
||||
v-model:model="model"
|
||||
report-type="weekly"
|
||||
:project-options="projectOptions"
|
||||
@reset="emit('reset')"
|
||||
@search="emit('search')"
|
||||
/>
|
||||
</template>
|
||||
@@ -91,7 +91,7 @@ onBeforeRouteLeave(async (_to, _from, next) => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="workbench">
|
||||
<div class="workbench work-report-page-shell">
|
||||
<WorkbenchBanner />
|
||||
|
||||
<WorkbenchEditOverlay
|
||||
|
||||
@@ -3,14 +3,23 @@ import { computed, markRaw, onMounted, ref, watch } from 'vue';
|
||||
import type { RouteKey } from '@elegant-router/types';
|
||||
import {
|
||||
fetchApproveOvertimeApplication,
|
||||
fetchGetMonthlyReportApprovalPage,
|
||||
fetchGetOvertimeApplicationApprovalPage,
|
||||
fetchGetProjectReportApprovalPage,
|
||||
fetchGetWeeklyReportApprovalPage,
|
||||
fetchRejectOvertimeApplication
|
||||
} from '@/service/api';
|
||||
import { useRouterPush } from '@/hooks/common/router';
|
||||
import PersonalItemOperateDialog from '@/views/personal-center/my-item/modules/personal-item-operate-dialog.vue';
|
||||
import OvertimeApplicationActionDialog from '@/views/personal-center/overtime-application/modules/overtime-application-action-dialog.vue';
|
||||
import OvertimeApplicationDetailDialog from '@/views/personal-center/overtime-application/modules/overtime-application-detail-dialog.vue';
|
||||
import OvertimeApplicationStatusLogDialog from '@/views/personal-center/overtime-application/modules/overtime-application-status-log-dialog.vue';
|
||||
import WorkReportPrototypePageDialog from '@/views/personal-center/work-report/shared/components/prototype-page-dialog.vue';
|
||||
import {
|
||||
WORK_REPORT_TYPE_LABEL,
|
||||
type WorkReportRow,
|
||||
type WorkReportType,
|
||||
formatPeriod
|
||||
} from '@/views/personal-center/work-report/shared/types';
|
||||
import {
|
||||
type WorkbenchTodoDeadlineFilter,
|
||||
type WorkbenchTodoItem,
|
||||
@@ -24,14 +33,11 @@ import {
|
||||
import { workbenchTodoMock } from '../mock';
|
||||
import { useWorkbenchRefresh } from '../composables/use-workbench-refresh';
|
||||
import WorkbenchModuleCard from './workbench-module-card.vue';
|
||||
import IconMdiCheckCircleOutline from '~icons/mdi/check-circle-outline';
|
||||
import IconMdiCloseCircleOutline from '~icons/mdi/close-circle-outline';
|
||||
import IconMdiEyeOutline from '~icons/mdi/eye-outline';
|
||||
import IconMdiHistory from '~icons/mdi/history';
|
||||
|
||||
type SortKey = 'created' | 'priority' | 'deadline';
|
||||
type OvertimeApprovalActionType = 'approve' | 'reject';
|
||||
type ApprovalBizType = 'overtime_application';
|
||||
type ApprovalBizType = 'overtime_application' | WorkReportType;
|
||||
|
||||
defineOptions({ name: 'WorkbenchTodoPanel' });
|
||||
|
||||
@@ -53,7 +59,7 @@ const PAGE_SIZE = 5;
|
||||
|
||||
const activeTab = ref<WorkbenchTodoMainTab>('all');
|
||||
const activeDeadlineFilter = ref<WorkbenchTodoDeadlineFilter>(null);
|
||||
const activeApprovalBizType = ref<ApprovalBizType>('overtime_application');
|
||||
const activeApprovalBizType = ref<ApprovalBizType>('weekly');
|
||||
const activeSort = ref<SortKey>('deadline');
|
||||
const currentPage = ref(1);
|
||||
|
||||
@@ -83,31 +89,37 @@ const deadlineFilters: Array<{ key: Exclude<WorkbenchTodoDeadlineFilter, null>;
|
||||
];
|
||||
|
||||
const approvalBizTabs: Array<{ key: ApprovalBizType; label: string }> = [
|
||||
{ key: 'weekly', label: '周报' },
|
||||
{ key: 'monthly', label: '月报' },
|
||||
{ key: 'project', label: '项目半月报' },
|
||||
{ key: 'overtime_application', label: '加班申请' }
|
||||
];
|
||||
|
||||
const allItems = computed(() => buildWorkbenchTodoItems(workbenchTodoMock));
|
||||
const overtimeApprovalItems = ref<WorkbenchTodoItem[]>([]);
|
||||
const overtimeApprovalRows = ref<Api.OvertimeApplication.OvertimeApplication[]>([]);
|
||||
const workReportApprovalItems = ref<WorkbenchTodoItem[]>([]);
|
||||
const weeklyApprovalRows = ref<Api.WorkReport.Weekly.WeeklyReport[]>([]);
|
||||
const monthlyApprovalRows = ref<Api.WorkReport.Monthly.MonthlyReport[]>([]);
|
||||
const projectApprovalRows = ref<Api.WorkReport.Project.ProjectReport[]>([]);
|
||||
const mergedItems = computed(() => {
|
||||
const mockItems = allItems.value.filter(item => item.category !== 'approval');
|
||||
|
||||
return [...mockItems, ...overtimeApprovalItems.value];
|
||||
return [...mockItems, ...overtimeApprovalItems.value, ...workReportApprovalItems.value];
|
||||
});
|
||||
|
||||
const addDialogVisible = ref(false);
|
||||
const overtimeDetailVisible = ref(false);
|
||||
const overtimeStatusLogVisible = ref(false);
|
||||
const overtimeActionVisible = ref(false);
|
||||
const overtimeActionSubmitting = ref(false);
|
||||
const currentOvertimeApplication = ref<Api.OvertimeApplication.OvertimeApplication | null>(null);
|
||||
const currentOvertimeActionType = ref<OvertimeApprovalActionType>('approve');
|
||||
const workReportDetailVisible = ref(false);
|
||||
const currentWorkReport = ref<WorkReportRow | null>(null);
|
||||
const currentWorkReportType = ref<WorkReportType>('weekly');
|
||||
|
||||
const OVERTIME_APPROVAL_ACTION_ICONS = {
|
||||
detail: markRaw(IconMdiEyeOutline),
|
||||
approve: markRaw(IconMdiCheckCircleOutline),
|
||||
reject: markRaw(IconMdiCloseCircleOutline),
|
||||
statusLog: markRaw(IconMdiHistory)
|
||||
detail: markRaw(IconMdiEyeOutline)
|
||||
};
|
||||
|
||||
function handleOpenAdd() {
|
||||
@@ -161,13 +173,20 @@ const filteredItems = computed(() => {
|
||||
|
||||
const approvalBizTabCounts = computed(() => {
|
||||
const counts: Record<ApprovalBizType, number> = {
|
||||
overtime_application: 0
|
||||
overtime_application: 0,
|
||||
weekly: 0,
|
||||
monthly: 0,
|
||||
project: 0
|
||||
};
|
||||
|
||||
itemsInTab.value.forEach(item => {
|
||||
if (item.approvalBizType === 'overtime_application') {
|
||||
counts.overtime_application += 1;
|
||||
}
|
||||
|
||||
if (isWorkReportApprovalBizType(item.approvalBizType)) {
|
||||
counts[item.approvalBizType] += 1;
|
||||
}
|
||||
});
|
||||
|
||||
return counts;
|
||||
@@ -229,10 +248,19 @@ function handleClickItem(item: WorkbenchTodoItem) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isWorkReportApprovalBizType(item.approvalBizType)) {
|
||||
openWorkReportDetail(item);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!item.routeKey) return;
|
||||
routerPushByKey(item.routeKey as RouteKey);
|
||||
}
|
||||
|
||||
function isWorkReportApprovalBizType(value?: string): value is WorkReportType {
|
||||
return value === 'weekly' || value === 'monthly' || value === 'project';
|
||||
}
|
||||
|
||||
function findOvertimeApprovalRow(item: WorkbenchTodoItem) {
|
||||
if (!item.approvalBizId) {
|
||||
return null;
|
||||
@@ -249,23 +277,38 @@ function openOvertimeDetail(item: WorkbenchTodoItem) {
|
||||
overtimeDetailVisible.value = true;
|
||||
}
|
||||
|
||||
function openOvertimeStatusLog(item: WorkbenchTodoItem) {
|
||||
const row = findOvertimeApprovalRow(item);
|
||||
if (!row) return;
|
||||
function openCurrentOvertimeAction(actionType: OvertimeApprovalActionType) {
|
||||
if (!currentOvertimeApplication.value) return;
|
||||
|
||||
currentOvertimeApplication.value = row;
|
||||
overtimeStatusLogVisible.value = true;
|
||||
}
|
||||
|
||||
function openOvertimeAction(item: WorkbenchTodoItem, actionType: OvertimeApprovalActionType) {
|
||||
const row = findOvertimeApprovalRow(item);
|
||||
if (!row) return;
|
||||
|
||||
currentOvertimeApplication.value = row;
|
||||
currentOvertimeActionType.value = actionType;
|
||||
overtimeActionVisible.value = true;
|
||||
}
|
||||
|
||||
function findWorkReportApprovalRow(item: WorkbenchTodoItem) {
|
||||
if (!item.approvalBizId || !isWorkReportApprovalBizType(item.approvalBizType)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (item.approvalBizType === 'weekly') {
|
||||
return weeklyApprovalRows.value.find(row => row.id === item.approvalBizId) || null;
|
||||
}
|
||||
|
||||
if (item.approvalBizType === 'monthly') {
|
||||
return monthlyApprovalRows.value.find(row => row.id === item.approvalBizId) || null;
|
||||
}
|
||||
|
||||
return projectApprovalRows.value.find(row => row.id === item.approvalBizId) || null;
|
||||
}
|
||||
|
||||
function openWorkReportDetail(item: WorkbenchTodoItem) {
|
||||
const row = findWorkReportApprovalRow(item);
|
||||
if (!row || !isWorkReportApprovalBizType(item.approvalBizType)) return;
|
||||
|
||||
currentWorkReport.value = row;
|
||||
currentWorkReportType.value = item.approvalBizType;
|
||||
workReportDetailVisible.value = true;
|
||||
}
|
||||
|
||||
async function handleOvertimeActionSubmit(reason: string | null) {
|
||||
if (!currentOvertimeApplication.value) {
|
||||
return;
|
||||
@@ -288,6 +331,11 @@ async function handleOvertimeActionSubmit(reason: string | null) {
|
||||
await loadOvertimeApprovalItems();
|
||||
}
|
||||
|
||||
async function handleWorkReportSubmitted() {
|
||||
workReportDetailVisible.value = false;
|
||||
await loadWorkReportApprovalItems();
|
||||
}
|
||||
|
||||
async function loadOvertimeApprovalItems() {
|
||||
const { error, data } = await fetchGetOvertimeApplicationApprovalPage({
|
||||
pageNo: 1,
|
||||
@@ -323,13 +371,80 @@ async function loadOvertimeApprovalItems() {
|
||||
);
|
||||
}
|
||||
|
||||
function buildWorkReportApprovalItems<T extends WorkReportRow>(
|
||||
bizType: WorkReportType,
|
||||
rows: T[]
|
||||
): WorkbenchTodoItem[] {
|
||||
const reportTypeLabel = WORK_REPORT_TYPE_LABEL[bizType];
|
||||
|
||||
return buildWorkbenchTodoItems(
|
||||
rows.map(item => ({
|
||||
id: `${bizType}-${item.id}`,
|
||||
category: 'approval',
|
||||
title: `${reportTypeLabel} · ${formatPeriod(item)} 待审批`,
|
||||
createdTime: item.submitTime || item.createTime || '',
|
||||
deadline: item.submitTime || item.createTime || null,
|
||||
source: `${reportTypeLabel} · ${'projectName' in item ? item.projectName : item.reporterName}`,
|
||||
priority: 'mid',
|
||||
approvalBizType: bizType,
|
||||
approvalBizId: item.id
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
async function loadWorkReportApprovalItems() {
|
||||
const [weeklyResult, monthlyResult, projectResult] = await Promise.all([
|
||||
fetchGetWeeklyReportApprovalPage({
|
||||
pageNo: 1,
|
||||
pageSize: 20,
|
||||
statusCode: 'pending_approval',
|
||||
keyword: undefined,
|
||||
periodStartDate: undefined,
|
||||
submitTime: undefined,
|
||||
supervisorName: undefined,
|
||||
isBusinessTrip: undefined
|
||||
}),
|
||||
fetchGetMonthlyReportApprovalPage({
|
||||
pageNo: 1,
|
||||
pageSize: 20,
|
||||
statusCode: 'pending_approval',
|
||||
keyword: undefined,
|
||||
periodStartDate: undefined,
|
||||
submitTime: undefined,
|
||||
supervisorName: undefined
|
||||
}),
|
||||
fetchGetProjectReportApprovalPage({
|
||||
pageNo: 1,
|
||||
pageSize: 20,
|
||||
statusCode: 'pending_approval',
|
||||
keyword: undefined,
|
||||
periodStartDate: undefined,
|
||||
submitTime: undefined,
|
||||
supervisorName: undefined,
|
||||
projectId: undefined,
|
||||
flag: undefined
|
||||
})
|
||||
]);
|
||||
|
||||
weeklyApprovalRows.value = weeklyResult.error || !weeklyResult.data ? [] : weeklyResult.data.list;
|
||||
monthlyApprovalRows.value = monthlyResult.error || !monthlyResult.data ? [] : monthlyResult.data.list;
|
||||
projectApprovalRows.value = projectResult.error || !projectResult.data ? [] : projectResult.data.list;
|
||||
workReportApprovalItems.value = [
|
||||
...buildWorkReportApprovalItems('weekly', weeklyApprovalRows.value),
|
||||
...buildWorkReportApprovalItems('monthly', monthlyApprovalRows.value),
|
||||
...buildWorkReportApprovalItems('project', projectApprovalRows.value)
|
||||
];
|
||||
}
|
||||
|
||||
function getDeadlineToneClass(item: WorkbenchTodoItem) {
|
||||
if (isWorkbenchTodoOverdue(item)) return 'workbench-todo__deadline--rose';
|
||||
if (item.remainingDays === 0) return 'workbench-todo__deadline--amber';
|
||||
return 'workbench-todo__deadline--slate';
|
||||
}
|
||||
|
||||
onMounted(loadOvertimeApprovalItems);
|
||||
onMounted(async () => {
|
||||
await Promise.all([loadOvertimeApprovalItems(), loadWorkReportApprovalItems()]);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -447,29 +562,15 @@ onMounted(loadOvertimeApprovalItems);
|
||||
<component :is="OVERTIME_APPROVAL_ACTION_ICONS.detail" class="text-15px" />
|
||||
</ElButton>
|
||||
</ElTooltip>
|
||||
<ElTooltip content="通过">
|
||||
<ElButton
|
||||
link
|
||||
type="success"
|
||||
class="workbench-todo__action-btn"
|
||||
@click="openOvertimeAction(item, 'approve')"
|
||||
</div>
|
||||
<div
|
||||
v-else-if="isWorkReportApprovalBizType(item.approvalBizType)"
|
||||
class="workbench-todo__actions"
|
||||
@click.stop
|
||||
>
|
||||
<component :is="OVERTIME_APPROVAL_ACTION_ICONS.approve" class="text-15px" />
|
||||
</ElButton>
|
||||
</ElTooltip>
|
||||
<ElTooltip content="退回">
|
||||
<ElButton
|
||||
link
|
||||
type="danger"
|
||||
class="workbench-todo__action-btn"
|
||||
@click="openOvertimeAction(item, 'reject')"
|
||||
>
|
||||
<component :is="OVERTIME_APPROVAL_ACTION_ICONS.reject" class="text-15px" />
|
||||
</ElButton>
|
||||
</ElTooltip>
|
||||
<ElTooltip content="状态日志">
|
||||
<ElButton link type="info" class="workbench-todo__action-btn" @click="openOvertimeStatusLog(item)">
|
||||
<component :is="OVERTIME_APPROVAL_ACTION_ICONS.statusLog" class="text-15px" />
|
||||
<ElTooltip content="详情">
|
||||
<ElButton link type="primary" class="workbench-todo__action-btn" @click="openWorkReportDetail(item)">
|
||||
<component :is="OVERTIME_APPROVAL_ACTION_ICONS.detail" class="text-15px" />
|
||||
</ElButton>
|
||||
</ElTooltip>
|
||||
</div>
|
||||
@@ -503,10 +604,13 @@ onMounted(loadOvertimeApprovalItems);
|
||||
@submitted="handleAddSubmitted"
|
||||
/>
|
||||
|
||||
<OvertimeApplicationDetailDialog v-model:visible="overtimeDetailVisible" :row-data="currentOvertimeApplication" />
|
||||
<OvertimeApplicationStatusLogDialog
|
||||
v-model:visible="overtimeStatusLogVisible"
|
||||
<OvertimeApplicationDetailDialog
|
||||
v-model:visible="overtimeDetailVisible"
|
||||
:row-data="currentOvertimeApplication"
|
||||
show-approval-actions
|
||||
:action-loading="overtimeActionSubmitting"
|
||||
@approve="openCurrentOvertimeAction('approve')"
|
||||
@reject="openCurrentOvertimeAction('reject')"
|
||||
/>
|
||||
<OvertimeApplicationActionDialog
|
||||
v-model:visible="overtimeActionVisible"
|
||||
@@ -514,6 +618,15 @@ onMounted(loadOvertimeApprovalItems);
|
||||
:loading="overtimeActionSubmitting"
|
||||
@submit="handleOvertimeActionSubmit"
|
||||
/>
|
||||
|
||||
<WorkReportPrototypePageDialog
|
||||
v-model:visible="workReportDetailVisible"
|
||||
mode="detail"
|
||||
scene="approval"
|
||||
:report-type="currentWorkReportType"
|
||||
:row-data="currentWorkReport"
|
||||
@submitted="handleWorkReportSubmitted"
|
||||
/>
|
||||
</WorkbenchModuleCard>
|
||||
</template>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user