fix(加班申请): 去掉撤销相关的状态和动作。
feat(工作报告): 开发工作报告功能
This commit is contained in:
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>
|
||||
Reference in New Issue
Block a user