feat(工作报告、加班申请团队视角): 工作报告、加班申请现在可以查看团队视角了(查看下属)。
fix(工作报告): 修复周报在新增/编辑时,不能展示工作日志。
This commit is contained in:
@@ -1,18 +1,19 @@
|
||||
<script setup lang="tsx">
|
||||
/* eslint-disable no-void */
|
||||
import { markRaw, reactive, ref } from 'vue';
|
||||
import { computed, markRaw, reactive, ref } from 'vue';
|
||||
import { ElMessageBox, ElTag, ElTooltip } from 'element-plus';
|
||||
import {
|
||||
fetchDeleteWeeklyReport,
|
||||
fetchExportWeeklyReportContent,
|
||||
fetchGetTeamReportSummary,
|
||||
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 { type TeamViewContext, resolveTeamQueryUserIds } from '@/views/personal-center/shared/team-dashboard';
|
||||
import {
|
||||
WORK_REPORT_TYPE_LABEL,
|
||||
type WorkReportRow,
|
||||
createWeeklySearchParams,
|
||||
createWorkReportContentExportFallbackName,
|
||||
@@ -23,19 +24,27 @@ import {
|
||||
formatPeriodDateRange,
|
||||
formatWeeklyPeriodLabel,
|
||||
getWorkReportStatusLabel,
|
||||
getWorkReportTypeDisplayLabel,
|
||||
resolveExportFilename,
|
||||
resolveWorkReportStatusTagType,
|
||||
transformWorkReportPage
|
||||
} from '../shared/types';
|
||||
import { resolveWorkReportSummaryPeriod } from '../shared/utils';
|
||||
import TeamReportSummary from '../shared/components/team-report-summary.vue';
|
||||
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';
|
||||
import IconMdiDownloadOutline from '~icons/mdi/download-outline';
|
||||
|
||||
defineOptions({ name: 'WeeklyWorkReportIndex' });
|
||||
|
||||
const props = defineProps<{
|
||||
teamContext?: TeamViewContext | null;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'create'): void;
|
||||
(e: 'edit', row: WorkReportRow): void;
|
||||
@@ -47,21 +56,33 @@ const { hasAuth } = useAuth();
|
||||
const exporting = ref(false);
|
||||
const selectedRows = ref<Api.WorkReport.Weekly.WeeklyReport[]>([]);
|
||||
const searchParams = reactive(createWeeklySearchParams());
|
||||
const teamSummaryLoading = ref(false);
|
||||
const teamSummary = ref<Api.WorkReport.Common.TeamReportSummary | null>(null);
|
||||
|
||||
const ACTION_ICON_MAP = {
|
||||
detail: markRaw(IconMdiEyeOutline),
|
||||
edit: markRaw(IconMdiPencilOutline),
|
||||
submit: markRaw(IconMdiSendOutline),
|
||||
delete: markRaw(IconMdiDeleteOutline),
|
||||
approvalRecord: markRaw(IconMdiFileDocumentCheckOutline)
|
||||
approvalRecord: markRaw(IconMdiFileDocumentCheckOutline),
|
||||
export: markRaw(IconMdiDownloadOutline)
|
||||
};
|
||||
|
||||
const isTeamMode = computed(() => props.teamContext?.mode === 'team');
|
||||
const isTeamRootSelected = computed(() => Boolean(isTeamMode.value && props.teamContext?.isRootSelected));
|
||||
const currentTeamReporterIds = computed(() => resolveTeamQueryUserIds(props.teamContext));
|
||||
const reportTitle = computed(() => getWorkReportTypeDisplayLabel('weekly', isTeamMode.value));
|
||||
|
||||
const table = useUIPaginatedTable<
|
||||
Awaited<ReturnType<typeof fetchGetWeeklyReportPage>>,
|
||||
Api.WorkReport.Weekly.WeeklyReport
|
||||
>({
|
||||
paginationProps: { currentPage: searchParams.pageNo, pageSize: searchParams.pageSize },
|
||||
api: () => fetchGetWeeklyReportPage(searchParams),
|
||||
api: () =>
|
||||
fetchGetWeeklyReportPage({
|
||||
...searchParams,
|
||||
reporterIds: currentTeamReporterIds.value
|
||||
}),
|
||||
transform: response => transformWorkReportPage(response, searchParams.pageNo ?? 1, searchParams.pageSize ?? 10),
|
||||
onPaginationParamsChange: params => {
|
||||
searchParams.pageNo = params.currentPage ?? 1;
|
||||
@@ -69,6 +90,7 @@ const table = useUIPaginatedTable<
|
||||
},
|
||||
columns: () => [
|
||||
{ prop: 'index', type: 'index', label: '序号', width: 64 },
|
||||
...(isTeamMode.value ? [{ prop: 'reporterName', label: '提交人', minWidth: 100, showOverflowTooltip: true }] : []),
|
||||
{
|
||||
prop: 'periodLabel',
|
||||
label: '周期',
|
||||
@@ -122,7 +144,7 @@ const table = useUIPaginatedTable<
|
||||
{
|
||||
prop: 'operate',
|
||||
label: '操作',
|
||||
width: 180,
|
||||
width: isTeamMode.value ? 140 : 180,
|
||||
align: 'center',
|
||||
fixed: 'right',
|
||||
formatter: row => <BusinessTableActionCell actions={getRowActions(row)} variant="icon" />
|
||||
@@ -130,17 +152,53 @@ const table = useUIPaginatedTable<
|
||||
]
|
||||
});
|
||||
|
||||
const summaryPeriod = computed(() =>
|
||||
resolveWorkReportSummaryPeriod('weekly', {
|
||||
currentRow: table.data.value[0],
|
||||
periodRange: searchParams.periodStartDate
|
||||
})
|
||||
);
|
||||
|
||||
function getRowActions(row: Api.WorkReport.Weekly.WeeklyReport): BusinessTableAction[] {
|
||||
const actions: BusinessTableAction[] = [
|
||||
{
|
||||
key: 'detail',
|
||||
label: '详情',
|
||||
label: '查看',
|
||||
buttonType: 'primary',
|
||||
icon: ACTION_ICON_MAP.detail,
|
||||
onClick: () => emit('detail', row)
|
||||
}
|
||||
];
|
||||
|
||||
if (isTeamMode.value) {
|
||||
actions.push({
|
||||
key: 'export',
|
||||
label: '导出',
|
||||
buttonType: 'success',
|
||||
icon: ACTION_ICON_MAP.export,
|
||||
onClick: () =>
|
||||
exportReportContent(
|
||||
{
|
||||
exportAll: false,
|
||||
ids: [row.id]
|
||||
},
|
||||
1
|
||||
)
|
||||
});
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
if (['draft', 'rejected'].includes(row.statusCode) && row.allowEdit && hasAuth('project:work-report:update')) {
|
||||
actions.push({
|
||||
key: 'edit',
|
||||
@@ -183,6 +241,7 @@ function getRowActions(row: Api.WorkReport.Weekly.WeeklyReport): BusinessTableAc
|
||||
|
||||
async function reload(page?: number) {
|
||||
await table.getDataByPage(page ?? searchParams.pageNo ?? 1);
|
||||
await loadTeamSummary();
|
||||
}
|
||||
|
||||
function resetSearchParams() {
|
||||
@@ -238,7 +297,10 @@ function handleSelectionChange(rows: Api.WorkReport.Weekly.WeeklyReport[]) {
|
||||
|
||||
function createExportSearchParams() {
|
||||
const { pageNo: _pageNo, pageSize: _pageSize, ...params } = searchParams;
|
||||
return params;
|
||||
return {
|
||||
...params,
|
||||
reporterIds: currentTeamReporterIds.value
|
||||
};
|
||||
}
|
||||
|
||||
async function exportReportContent(
|
||||
@@ -296,6 +358,23 @@ async function handleExportCommand(command: 'selected' | 'all') {
|
||||
await handleExportAll();
|
||||
}
|
||||
|
||||
async function loadTeamSummary() {
|
||||
if (!isTeamRootSelected.value) {
|
||||
teamSummaryLoading.value = false;
|
||||
teamSummary.value = null;
|
||||
return;
|
||||
}
|
||||
|
||||
teamSummaryLoading.value = true;
|
||||
const { error, data } = await fetchGetTeamReportSummary({
|
||||
reportType: 'weekly',
|
||||
periodKey: summaryPeriod.value.periodKey
|
||||
});
|
||||
teamSummaryLoading.value = false;
|
||||
|
||||
teamSummary.value = error || !data ? null : data;
|
||||
}
|
||||
|
||||
defineExpose({ reload });
|
||||
</script>
|
||||
|
||||
@@ -303,11 +382,21 @@ defineExpose({ reload });
|
||||
<div class="flex-col-stretch gap-16px overflow-hidden">
|
||||
<WeeklyReportSearch v-model:model="searchParams" @reset="resetSearchParams" @search="handleSearch" />
|
||||
|
||||
<TeamReportSummary
|
||||
v-if="isTeamRootSelected"
|
||||
report-type="weekly"
|
||||
:period-key="summaryPeriod.periodKey"
|
||||
:period-label="formatWeeklyPeriodLabel(summaryPeriod)"
|
||||
:loading="teamSummaryLoading"
|
||||
:summary="teamSummary"
|
||||
@reminded="loadTeamSummary"
|
||||
/>
|
||||
|
||||
<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>
|
||||
<p class="text-16px font-600">{{ reportTitle }}</p>
|
||||
<ElTag effect="plain">{{ table.mobilePagination.value.total || 0 }}</ElTag>
|
||||
</div>
|
||||
|
||||
@@ -333,7 +422,13 @@ defineExpose({ reload });
|
||||
</ElDropdownMenu>
|
||||
</template>
|
||||
</ElDropdown>
|
||||
<ElButton v-auth="'project:work-report:create'" plain type="primary" @click="emit('create')">
|
||||
<ElButton
|
||||
v-if="!isTeamMode"
|
||||
v-auth="'project:work-report:create'"
|
||||
plain
|
||||
type="primary"
|
||||
@click="emit('create')"
|
||||
>
|
||||
<template #icon>
|
||||
<icon-ic-round-plus class="text-icon" />
|
||||
</template>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
/* eslint-disable vue/no-mutating-props, unicorn/prefer-dom-node-text-content, no-useless-escape, no-nested-ternary */
|
||||
import { computed, onMounted, reactive, ref, watch } from 'vue';
|
||||
import { computed, nextTick, onMounted, reactive, ref, watch } from 'vue';
|
||||
import { Plus } from '@element-plus/icons-vue';
|
||||
import { RDMS_REQ_PRIORITY_DICT_CODE, RDMS_TASK_ITEM_TYPE_DICT_CODE } from '@/constants/dict';
|
||||
import { fetchGetMyParticipatedProjectPage } from '@/service/api';
|
||||
@@ -1065,6 +1065,34 @@ function blurEditField(key: string) {
|
||||
if (activeEditField.value === key) activeEditField.value = '';
|
||||
}
|
||||
|
||||
/** 编辑态下是否显示"具体工作内容"的结构化预览(含 ElPopover 工作日志) */
|
||||
function showContentStructuredView(index: number) {
|
||||
const item = reviewItems.value[index];
|
||||
if (!item?.contentSections?.length) return false;
|
||||
if (isReadonly.value) return true;
|
||||
// 编辑/新增模式下,仅在该字段未聚焦时显示结构化预览
|
||||
return activeEditField.value !== `content-${index}`;
|
||||
}
|
||||
|
||||
/** 编辑态下是否显示"具体目标"的结构化预览(含 ElPopover 工作日志) */
|
||||
function showTargetStructuredView(index: number) {
|
||||
const item = nextPlans.value[index];
|
||||
if (!item?.targetSections?.length) return false;
|
||||
if (isReadonly.value) return true;
|
||||
// 编辑/新增模式下,仅在该字段未聚焦时显示结构化预览
|
||||
return activeEditField.value !== `target-${index}`;
|
||||
}
|
||||
|
||||
/** 点击结构化预览区域时切换到编辑态并聚焦 */
|
||||
function handleStructuredViewClick(fieldKey: string) {
|
||||
if (isReadonly.value) return;
|
||||
activeEditField.value = fieldKey;
|
||||
nextTick(() => {
|
||||
const editor = document.querySelector(`[data-field-key="${fieldKey}"]`) as HTMLElement;
|
||||
editor?.focus();
|
||||
});
|
||||
}
|
||||
|
||||
function syncRichContent(item: ReviewItem, event: Event) {
|
||||
const target = event.currentTarget as HTMLElement;
|
||||
if (!item.source) return;
|
||||
@@ -1183,7 +1211,12 @@ function syncRichSupport(item: PlanItem, event: Event) {
|
||||
<div class="review-editor-grid">
|
||||
<div class="field">
|
||||
<label>具体工作内容及成果描述</label>
|
||||
<div v-if="isReadonly && item.contentSections?.length" class="rich-editor">
|
||||
<div
|
||||
v-if="showContentStructuredView(index)"
|
||||
class="rich-editor"
|
||||
:class="{ 'rich-editor--preview': !isReadonly }"
|
||||
@click="handleStructuredViewClick(`content-${index}`)"
|
||||
>
|
||||
<div
|
||||
v-for="(section, sectionIndex) in item.contentSections"
|
||||
:key="`${index}-${sectionIndex}`"
|
||||
@@ -1214,6 +1247,7 @@ function syncRichSupport(item: PlanItem, event: Event) {
|
||||
class="rich-editor"
|
||||
:contenteditable="!isReadonly"
|
||||
spellcheck="false"
|
||||
:data-field-key="`content-${index}`"
|
||||
:data-placeholder="isReadonly ? undefined : '请输入具体工作内容及成果描述'"
|
||||
@focus="focusEditField(`content-${index}`)"
|
||||
@blur="
|
||||
@@ -1282,7 +1316,12 @@ function syncRichSupport(item: PlanItem, event: Event) {
|
||||
<div class="plan-editor-grid">
|
||||
<div class="field">
|
||||
<label>具体目标</label>
|
||||
<div v-if="isReadonly && item.targetSections?.length" class="rich-editor">
|
||||
<div
|
||||
v-if="showTargetStructuredView(index)"
|
||||
class="rich-editor"
|
||||
:class="{ 'rich-editor--preview': !isReadonly }"
|
||||
@click="handleStructuredViewClick(`target-${index}`)"
|
||||
>
|
||||
<div
|
||||
v-for="(section, sectionIndex) in item.targetSections"
|
||||
:key="`${index}-${sectionIndex}`"
|
||||
@@ -1313,6 +1352,7 @@ function syncRichSupport(item: PlanItem, event: Event) {
|
||||
class="rich-editor"
|
||||
:contenteditable="!isReadonly"
|
||||
spellcheck="false"
|
||||
:data-field-key="`target-${index}`"
|
||||
:data-placeholder="isReadonly ? undefined : '请输入具体目标'"
|
||||
@focus="focusEditField(`target-${index}`)"
|
||||
@blur="
|
||||
@@ -2107,6 +2147,15 @@ function syncRichSupport(item: PlanItem, event: Event) {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* 编辑态下结构化预览区域:点击可切换到编辑模式 */
|
||||
.rich-editor--preview {
|
||||
cursor: text;
|
||||
}
|
||||
|
||||
.rich-editor--preview:hover {
|
||||
border-color: #0f766e;
|
||||
}
|
||||
|
||||
.structured-preview__popover {
|
||||
max-width: 100%;
|
||||
color: #334155;
|
||||
|
||||
Reference in New Issue
Block a user