feat(工作报告、加班申请团队视角): 工作报告、加班申请现在可以查看团队视角了(查看下属)。
fix(工作报告): 修复周报在新增/编辑时,不能展示工作日志。
This commit is contained in:
108
src/components/custom/subordinate-selector.vue
Normal file
108
src/components/custom/subordinate-selector.vue
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
defineOptions({ name: 'SubordinateSelector' });
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
loading?: boolean;
|
||||||
|
data?: Api.SystemManage.MySubordinateTreeNode | null;
|
||||||
|
emptyText?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
loading: false,
|
||||||
|
data: null,
|
||||||
|
emptyText: '暂无下属数据'
|
||||||
|
});
|
||||||
|
|
||||||
|
const selectedUserId = defineModel<string | null>('selectedUserId', {
|
||||||
|
default: null
|
||||||
|
});
|
||||||
|
|
||||||
|
function handleNodeClick(node: Api.SystemManage.MySubordinateTreeNode) {
|
||||||
|
selectedUserId.value = node.userId;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderNodeLabel(node: Api.SystemManage.MySubordinateTreeNode) {
|
||||||
|
const label = node.isRoot ? '全部下属' : node.userNickname;
|
||||||
|
return `${label}${node.subordinateCount ? `(${node.subordinateCount})` : ''}`;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<ElCard class="subordinate-selector" body-class="subordinate-selector__body">
|
||||||
|
<template #header>
|
||||||
|
<div class="flex items-center justify-between gap-12px">
|
||||||
|
<span class="text-14px font-600">团队成员</span>
|
||||||
|
<ElTag v-if="props.data" effect="plain">{{ props.data.subordinateCount }}</ElTag>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div v-loading="props.loading" class="subordinate-selector__content">
|
||||||
|
<ElEmpty v-if="!props.data" :image-size="88" :description="props.emptyText" />
|
||||||
|
<ElTree
|
||||||
|
v-else
|
||||||
|
:data="[props.data]"
|
||||||
|
node-key="userId"
|
||||||
|
:current-node-key="selectedUserId || undefined"
|
||||||
|
:props="{ label: 'userNickname', children: 'children' }"
|
||||||
|
highlight-current
|
||||||
|
default-expand-all
|
||||||
|
expand-on-click-node
|
||||||
|
class="subordinate-selector__tree"
|
||||||
|
@node-click="handleNodeClick"
|
||||||
|
>
|
||||||
|
<template #default="{ data: node }">
|
||||||
|
<span class="subordinate-selector__node-label">{{ renderNodeLabel(node) }}</span>
|
||||||
|
</template>
|
||||||
|
</ElTree>
|
||||||
|
</div>
|
||||||
|
</ElCard>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.subordinate-selector {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
border: 1px solid var(--el-border-color-light);
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.subordinate-selector__body) {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 0;
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subordinate-selector__content {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 240px;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subordinate-selector__tree {
|
||||||
|
height: 100%;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subordinate-selector__node-label {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
min-width: 0;
|
||||||
|
color: var(--el-text-color-regular);
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.subordinate-selector__tree .el-tree-node__content) {
|
||||||
|
height: 36px;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.subordinate-selector__tree .el-tree-node__content:hover) {
|
||||||
|
background: var(--el-fill-color-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.subordinate-selector__tree .el-tree-node.is-current > .el-tree-node__content) {
|
||||||
|
background: var(--el-color-primary-light-9);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
163
src/components/custom/team-context-panel.vue
Normal file
163
src/components/custom/team-context-panel.vue
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import type { TeamViewMode } from '@/views/personal-center/shared/team-dashboard';
|
||||||
|
|
||||||
|
defineOptions({ name: 'TeamContextPanel' });
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
loading?: boolean;
|
||||||
|
selectedLabel?: string;
|
||||||
|
subordinateCount?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
loading: false,
|
||||||
|
selectedLabel: '',
|
||||||
|
subordinateCount: 0
|
||||||
|
});
|
||||||
|
|
||||||
|
const mode = defineModel<TeamViewMode>('mode', {
|
||||||
|
required: true
|
||||||
|
});
|
||||||
|
|
||||||
|
const scopeOptions = computed(() => [
|
||||||
|
{ label: '个人视角', value: 'self' satisfies TeamViewMode },
|
||||||
|
{ label: '团队视角', value: 'team' satisfies TeamViewMode }
|
||||||
|
]);
|
||||||
|
|
||||||
|
const contextText = computed(() => {
|
||||||
|
if (mode.value === 'self') {
|
||||||
|
return '当前查看我自己的数据。';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.selectedLabel) {
|
||||||
|
return `当前范围:${props.selectedLabel}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return '当前查看团队数据。';
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<ElCard class="team-context-panel" body-class="team-context-panel__body">
|
||||||
|
<div v-loading="props.loading" class="team-context-panel__layout">
|
||||||
|
<div class="team-context-panel__controls">
|
||||||
|
<ElSegmented v-model="mode" :options="scopeOptions" class="team-context-panel__segmented" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="team-context-panel__info">
|
||||||
|
<div class="team-context-panel__info-main">
|
||||||
|
<div class="team-context-panel__info-item">
|
||||||
|
<span class="team-context-panel__info-label">当前范围</span>
|
||||||
|
<strong class="team-context-panel__info-value">
|
||||||
|
{{ props.selectedLabel || (mode === 'self' ? '我自己' : '--') }}
|
||||||
|
</strong>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="mode === 'team'" class="team-context-panel__info-item">
|
||||||
|
<span class="team-context-panel__info-label">下属人数</span>
|
||||||
|
<strong class="team-context-panel__info-value">{{ props.subordinateCount }}</strong>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="team-context-panel__info-desc">{{ contextText }}</p>
|
||||||
|
<div v-if="$slots.default" class="team-context-panel__summary">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ElCard>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.team-context-panel {
|
||||||
|
border: 1px solid var(--el-border-color-light);
|
||||||
|
background: var(--el-fill-color-blank);
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.team-context-panel__body) {
|
||||||
|
padding: 16px 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.team-context-panel__layout {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.team-context-panel__controls {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.team-context-panel__segmented) {
|
||||||
|
padding: 6px;
|
||||||
|
background: var(--el-fill-color-light);
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.team-context-panel__segmented .el-segmented__item) {
|
||||||
|
min-width: 96px;
|
||||||
|
min-height: 40px;
|
||||||
|
padding: 0 22px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.team-context-panel__info {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
padding-left: 20px;
|
||||||
|
border-left: 1px solid var(--el-border-color-lighter);
|
||||||
|
}
|
||||||
|
|
||||||
|
.team-context-panel__info-main {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.team-context-panel__info-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 10px;
|
||||||
|
min-width: 180px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.team-context-panel__info-label {
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.team-context-panel__info-value {
|
||||||
|
color: var(--el-text-color-primary);
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.team-context-panel__info-desc {
|
||||||
|
margin: 10px 0 0;
|
||||||
|
color: var(--el-text-color-regular);
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.team-context-panel__summary {
|
||||||
|
margin-top: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (width <= 1200px) {
|
||||||
|
.team-context-panel__layout {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.team-context-panel__info {
|
||||||
|
padding-left: 0;
|
||||||
|
padding-top: 14px;
|
||||||
|
border-left: none;
|
||||||
|
border-top: 1px solid var(--el-border-color-lighter);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -34,6 +34,8 @@ type OvertimeApplicationApprovalRecordResponse = Omit<
|
|||||||
auditorUserId: StringIdResponse;
|
auditorUserId: StringIdResponse;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type TeamOvertimeSummaryResponse = Api.OvertimeApplication.TeamOvertimeSummary;
|
||||||
|
|
||||||
function normalizeBooleanFlag(value: boolean | number | string | null | undefined) {
|
function normalizeBooleanFlag(value: boolean | number | string | null | undefined) {
|
||||||
if (typeof value === 'boolean') {
|
if (typeof value === 'boolean') {
|
||||||
return value;
|
return value;
|
||||||
@@ -94,6 +96,18 @@ function createPageQuery(params: Api.OvertimeApplication.OvertimeApplicationSear
|
|||||||
query.append('pageNo', String(params.pageNo ?? 1));
|
query.append('pageNo', String(params.pageNo ?? 1));
|
||||||
query.append('pageSize', String(params.pageSize ?? 10));
|
query.append('pageSize', String(params.pageSize ?? 10));
|
||||||
|
|
||||||
|
if (params.applicantIds !== null && params.applicantIds !== undefined) {
|
||||||
|
if (params.applicantIds.length) {
|
||||||
|
params.applicantIds.forEach(item => {
|
||||||
|
if (item) {
|
||||||
|
query.append('applicantIds', item);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
query.append('applicantIds', '');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (params.keyword) {
|
if (params.keyword) {
|
||||||
query.append('keyword', params.keyword);
|
query.append('keyword', params.keyword);
|
||||||
}
|
}
|
||||||
@@ -287,6 +301,17 @@ export async function fetchGetOvertimeApplicationStatusDict() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function fetchGetTeamOvertimeSummary(params: Api.OvertimeApplication.TeamOvertimeSummaryParams = {}) {
|
||||||
|
const result = await request<TeamOvertimeSummaryResponse>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${OVERTIME_APPLICATION_PREFIX}/team/summary`,
|
||||||
|
method: 'get',
|
||||||
|
params
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapServiceResult(result as ServiceRequestResult<TeamOvertimeSummaryResponse>, data => data);
|
||||||
|
}
|
||||||
|
|
||||||
export function fetchExportOvertimeApplications(params: Api.OvertimeApplication.OvertimeApplicationSearchParams = {}) {
|
export function fetchExportOvertimeApplications(params: Api.OvertimeApplication.OvertimeApplicationSearchParams = {}) {
|
||||||
const query = createPageQuery(params);
|
const query = createPageQuery(params);
|
||||||
|
|
||||||
|
|||||||
@@ -118,6 +118,11 @@ type UserManagementRelationTreeResponse = Omit<
|
|||||||
children?: UserManagementRelationTreeResponse[] | null;
|
children?: UserManagementRelationTreeResponse[] | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type MySubordinateTreeNodeResponse = Omit<Api.SystemManage.MySubordinateTreeNode, 'userId' | 'children'> & {
|
||||||
|
userId: string | number;
|
||||||
|
children?: MySubordinateTreeNodeResponse[] | null;
|
||||||
|
};
|
||||||
|
|
||||||
function normalizeUserSimple(user: UserSimpleResponse): Api.SystemManage.UserSimple {
|
function normalizeUserSimple(user: UserSimpleResponse): Api.SystemManage.UserSimple {
|
||||||
return {
|
return {
|
||||||
...user,
|
...user,
|
||||||
@@ -181,6 +186,14 @@ function normalizeUserManagementRelationTree(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeMySubordinateTreeNode(node: MySubordinateTreeNodeResponse): Api.SystemManage.MySubordinateTreeNode {
|
||||||
|
return {
|
||||||
|
...node,
|
||||||
|
userId: normalizeStringId(node.userId),
|
||||||
|
children: node.children?.map(normalizeMySubordinateTreeNode) ?? null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/** 获取角色分页 */
|
/** 获取角色分页 */
|
||||||
export async function fetchGetRolePage(params?: Api.SystemManage.RoleSearchParams) {
|
export async function fetchGetRolePage(params?: Api.SystemManage.RoleSearchParams) {
|
||||||
const query = createRolePageQuery(params);
|
const query = createRolePageQuery(params);
|
||||||
@@ -712,6 +725,17 @@ export async function fetchGetUserManagementRelationQuery(query: UserManagementR
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 获取当前登录用户下属树 */
|
||||||
|
export async function fetchGetMySubordinateTree() {
|
||||||
|
return request<MySubordinateTreeNodeResponse>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${USER_MANAGEMENT_RELATION_PREFIX}/my-subordinate-tree`,
|
||||||
|
method: 'get'
|
||||||
|
}).then(result =>
|
||||||
|
mapServiceResult(result as ServiceRequestResult<MySubordinateTreeNodeResponse>, normalizeMySubordinateTreeNode)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取用户管理链路详情
|
* 获取用户管理链路详情
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -99,6 +99,14 @@ type ProjectOptionResponse = Omit<Api.WorkReport.Project.ProjectReportOwnerProje
|
|||||||
id: StringIdResponse;
|
id: StringIdResponse;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type TeamReportPendingUserResponse = Omit<Api.WorkReport.Common.TeamReportPendingUser, 'userId'> & {
|
||||||
|
userId: StringIdResponse;
|
||||||
|
};
|
||||||
|
|
||||||
|
type TeamReportSummaryResponse = Omit<Api.WorkReport.Common.TeamReportSummary, 'unsubmittedUsers'> & {
|
||||||
|
unsubmittedUsers?: TeamReportPendingUserResponse[] | null;
|
||||||
|
};
|
||||||
|
|
||||||
function normalizeBooleanFlag(value: boolean | number | string | null | undefined) {
|
function normalizeBooleanFlag(value: boolean | number | string | null | undefined) {
|
||||||
if (typeof value === 'boolean') return value;
|
if (typeof value === 'boolean') return value;
|
||||||
if (typeof value === 'number') return value === 1;
|
if (typeof value === 'number') return value === 1;
|
||||||
@@ -173,6 +181,21 @@ function appendArray(query: URLSearchParams, key: string, values?: Array<string
|
|||||||
values?.forEach(value => appendValue(query, key, value));
|
values?.forEach(value => appendValue(query, key, value));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function appendNullableArrayFlag(
|
||||||
|
query: URLSearchParams,
|
||||||
|
key: string,
|
||||||
|
values?: Array<string | null | undefined> | null
|
||||||
|
) {
|
||||||
|
if (values === null || values === undefined) return;
|
||||||
|
|
||||||
|
if (!values.length) {
|
||||||
|
query.append(key, '');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
appendArray(query, key, values);
|
||||||
|
}
|
||||||
|
|
||||||
function createBasePageQuery(params: Api.WorkReport.Common.WorkReportBaseSearchParams = {}) {
|
function createBasePageQuery(params: Api.WorkReport.Common.WorkReportBaseSearchParams = {}) {
|
||||||
const query = new URLSearchParams();
|
const query = new URLSearchParams();
|
||||||
|
|
||||||
@@ -189,16 +212,20 @@ function createBasePageQuery(params: Api.WorkReport.Common.WorkReportBaseSearchP
|
|||||||
|
|
||||||
function createWeeklyPageQuery(params: Api.WorkReport.Weekly.WeeklyReportSearchParams = {}) {
|
function createWeeklyPageQuery(params: Api.WorkReport.Weekly.WeeklyReportSearchParams = {}) {
|
||||||
const query = createBasePageQuery(params);
|
const query = createBasePageQuery(params);
|
||||||
|
appendNullableArrayFlag(query, 'reporterIds', params.reporterIds);
|
||||||
appendValue(query, 'isBusinessTrip', params.isBusinessTrip);
|
appendValue(query, 'isBusinessTrip', params.isBusinessTrip);
|
||||||
return query.toString();
|
return query.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
function createMonthlyPageQuery(params: Api.WorkReport.Monthly.MonthlyReportSearchParams = {}) {
|
function createMonthlyPageQuery(params: Api.WorkReport.Monthly.MonthlyReportSearchParams = {}) {
|
||||||
return createBasePageQuery(params).toString();
|
const query = createBasePageQuery(params);
|
||||||
|
appendNullableArrayFlag(query, 'reporterIds', params.reporterIds);
|
||||||
|
return query.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
function createProjectPageQuery(params: Api.WorkReport.Project.ProjectReportSearchParams = {}) {
|
function createProjectPageQuery(params: Api.WorkReport.Project.ProjectReportSearchParams = {}) {
|
||||||
const query = createBasePageQuery(params);
|
const query = createBasePageQuery(params);
|
||||||
|
appendNullableArrayFlag(query, 'projectOwnerIds', params.projectOwnerIds);
|
||||||
appendValue(query, 'projectId', params.projectId);
|
appendValue(query, 'projectId', params.projectId);
|
||||||
appendValue(query, 'flag', params.flag);
|
appendValue(query, 'flag', params.flag);
|
||||||
return query.toString();
|
return query.toString();
|
||||||
@@ -338,6 +365,17 @@ function normalizeProjectOption(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeTeamReportSummary(response: TeamReportSummaryResponse): Api.WorkReport.Common.TeamReportSummary {
|
||||||
|
return {
|
||||||
|
...response,
|
||||||
|
unsubmittedUsers:
|
||||||
|
response.unsubmittedUsers?.map(item => ({
|
||||||
|
...item,
|
||||||
|
userId: normalizeStringId(item.userId)
|
||||||
|
})) ?? []
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function mapPage<TInput, TOutput>(data: PageResponse<TInput>, mapper: (item: TInput) => TOutput) {
|
function mapPage<TInput, TOutput>(data: PageResponse<TInput>, mapper: (item: TInput) => TOutput) {
|
||||||
return {
|
return {
|
||||||
total: normalizeTotal(data.total),
|
total: normalizeTotal(data.total),
|
||||||
@@ -440,6 +478,34 @@ export async function fetchGetWorkReportStatusDict() {
|
|||||||
return mapServiceResult(result as ServiceRequestResult<Api.WorkReport.Common.WorkReportStatusDict[]>, data => data);
|
return mapServiceResult(result as ServiceRequestResult<Api.WorkReport.Common.WorkReportStatusDict[]>, data => data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function fetchGetTeamReportSummary(params: Api.WorkReport.Common.TeamReportSummaryParams) {
|
||||||
|
const result = await request<TeamReportSummaryResponse>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${WORK_REPORT_PREFIX}/team/summary`,
|
||||||
|
method: 'get',
|
||||||
|
params
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapServiceResult(result as ServiceRequestResult<TeamReportSummaryResponse>, normalizeTeamReportSummary);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchRemindTeamReport(data: Api.WorkReport.Common.TeamReportRemindParams) {
|
||||||
|
const result = await request<Api.WorkReport.Common.TeamReportRemindResult>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${WORK_REPORT_PREFIX}/team/remind`,
|
||||||
|
method: 'post',
|
||||||
|
data: {
|
||||||
|
...data,
|
||||||
|
userIds: data.userIds && data.userIds.length ? data.userIds : undefined
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapServiceResult(
|
||||||
|
result as ServiceRequestResult<Api.WorkReport.Common.TeamReportRemindResult>,
|
||||||
|
payload => payload
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export async function fetchGetWeeklyReportPage(params: Api.WorkReport.Weekly.WeeklyReportSearchParams = {}) {
|
export async function fetchGetWeeklyReportPage(params: Api.WorkReport.Weekly.WeeklyReportSearchParams = {}) {
|
||||||
const query = createWeeklyPageQuery(params);
|
const query = createWeeklyPageQuery(params);
|
||||||
const result = await request<PageResponse<WeeklyReportResponse>>({
|
const result = await request<PageResponse<WeeklyReportResponse>>({
|
||||||
|
|||||||
13
src/typings/api/overtime-application.d.ts
vendored
13
src/typings/api/overtime-application.d.ts
vendored
@@ -32,6 +32,7 @@ declare namespace Api {
|
|||||||
|
|
||||||
type OvertimeApplicationSearchParams = CommonType.RecordNullable<
|
type OvertimeApplicationSearchParams = CommonType.RecordNullable<
|
||||||
Pick<PageParams, 'pageNo' | 'pageSize'> & {
|
Pick<PageParams, 'pageNo' | 'pageSize'> & {
|
||||||
|
applicantIds: string[] | null;
|
||||||
keyword: string;
|
keyword: string;
|
||||||
applicantName: string;
|
applicantName: string;
|
||||||
approverId: string;
|
approverId: string;
|
||||||
@@ -95,5 +96,17 @@ declare namespace Api {
|
|||||||
terminalFlag: boolean;
|
terminalFlag: boolean;
|
||||||
allowEdit: boolean;
|
allowEdit: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface TeamOvertimeSummaryParams {
|
||||||
|
month?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TeamOvertimeSummary {
|
||||||
|
month: string;
|
||||||
|
totalApplicationCount: number;
|
||||||
|
pendingCount: number;
|
||||||
|
approvedCount: number;
|
||||||
|
rejectedCount: number;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
18
src/typings/api/system-manage.d.ts
vendored
18
src/typings/api/system-manage.d.ts
vendored
@@ -386,6 +386,24 @@ declare namespace Api {
|
|||||||
children?: UserManagementRelationTreeRespVO[] | null;
|
children?: UserManagementRelationTreeRespVO[] | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 当前登录用户的下属树
|
||||||
|
*
|
||||||
|
* 用于团队视角选择器;根节点代表“全部下属范围”
|
||||||
|
*/
|
||||||
|
interface MySubordinateTreeNode {
|
||||||
|
/** 用户 ID */
|
||||||
|
userId: string;
|
||||||
|
/** 用户昵称 */
|
||||||
|
userNickname: string;
|
||||||
|
/** 是否为当前登录用户根节点 */
|
||||||
|
isRoot: boolean;
|
||||||
|
/** 全链路下属人数 */
|
||||||
|
subordinateCount: number;
|
||||||
|
/** 下级用户列表 */
|
||||||
|
children?: MySubordinateTreeNode[] | null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 用户管理链路保存参数
|
* 用户管理链路保存参数
|
||||||
*
|
*
|
||||||
|
|||||||
34
src/typings/api/work-report.d.ts
vendored
34
src/typings/api/work-report.d.ts
vendored
@@ -71,6 +71,34 @@ declare namespace Api {
|
|||||||
total: number;
|
total: number;
|
||||||
list: T[];
|
list: T[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface TeamReportPendingUser {
|
||||||
|
userId: string;
|
||||||
|
userNickname: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TeamReportSummary {
|
||||||
|
totalShouldSubmit: number;
|
||||||
|
submittedCount: number;
|
||||||
|
unsubmittedCount: number;
|
||||||
|
pendingApprovalCount: number;
|
||||||
|
unsubmittedUsers: TeamReportPendingUser[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TeamReportSummaryParams {
|
||||||
|
reportType: ReportType;
|
||||||
|
periodKey: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TeamReportRemindParams {
|
||||||
|
reportType: ReportType;
|
||||||
|
periodKey: string;
|
||||||
|
userIds?: string[] | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TeamReportRemindResult {
|
||||||
|
remindedCount: number;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
namespace Weekly {
|
namespace Weekly {
|
||||||
@@ -114,6 +142,7 @@ declare namespace Api {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type WeeklyReportSearchParams = Common.WorkReportBaseSearchParams & {
|
type WeeklyReportSearchParams = Common.WorkReportBaseSearchParams & {
|
||||||
|
reporterIds?: string[] | null;
|
||||||
isBusinessTrip?: boolean | string | null;
|
isBusinessTrip?: boolean | string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -164,7 +193,9 @@ declare namespace Api {
|
|||||||
planItems: Common.PersonalReportPlanItem[];
|
planItems: Common.PersonalReportPlanItem[];
|
||||||
}
|
}
|
||||||
|
|
||||||
type MonthlyReportSearchParams = Common.WorkReportBaseSearchParams;
|
type MonthlyReportSearchParams = Common.WorkReportBaseSearchParams & {
|
||||||
|
reporterIds?: string[] | null;
|
||||||
|
};
|
||||||
|
|
||||||
interface MonthlyReportSaveParams {
|
interface MonthlyReportSaveParams {
|
||||||
periodKey: string;
|
periodKey: string;
|
||||||
@@ -266,6 +297,7 @@ declare namespace Api {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ProjectReportSearchParams = Common.WorkReportBaseSearchParams & {
|
type ProjectReportSearchParams = Common.WorkReportBaseSearchParams & {
|
||||||
|
projectOwnerIds?: string[] | null;
|
||||||
projectId?: string | null;
|
projectId?: string | null;
|
||||||
flag?: number | null;
|
flag?: number | null;
|
||||||
};
|
};
|
||||||
|
|||||||
2
src/typings/components.d.ts
vendored
2
src/typings/components.d.ts
vendored
@@ -178,12 +178,14 @@ declare module 'vue' {
|
|||||||
RouterLink: typeof import('vue-router')['RouterLink']
|
RouterLink: typeof import('vue-router')['RouterLink']
|
||||||
RouterView: typeof import('vue-router')['RouterView']
|
RouterView: typeof import('vue-router')['RouterView']
|
||||||
SoybeanAvatar: typeof import('./../components/custom/soybean-avatar.vue')['default']
|
SoybeanAvatar: typeof import('./../components/custom/soybean-avatar.vue')['default']
|
||||||
|
SubordinateSelector: typeof import('./../components/custom/subordinate-selector.vue')['default']
|
||||||
SvgIcon: typeof import('./../components/custom/svg-icon.vue')['default']
|
SvgIcon: typeof import('./../components/custom/svg-icon.vue')['default']
|
||||||
SystemLogo: typeof import('./../components/common/system-logo.vue')['default']
|
SystemLogo: typeof import('./../components/common/system-logo.vue')['default']
|
||||||
TableColumnSetting: typeof import('./../components/advanced/table-column-setting.vue')['default']
|
TableColumnSetting: typeof import('./../components/advanced/table-column-setting.vue')['default']
|
||||||
TableHeaderOperation: typeof import('./../components/advanced/table-header-operation.vue')['default']
|
TableHeaderOperation: typeof import('./../components/advanced/table-header-operation.vue')['default']
|
||||||
TableSearchFields: typeof import('./../components/custom/table-search-fields.vue')['default']
|
TableSearchFields: typeof import('./../components/custom/table-search-fields.vue')['default']
|
||||||
TableSearchPanel: typeof import('./../components/custom/table-search-panel.vue')['default']
|
TableSearchPanel: typeof import('./../components/custom/table-search-panel.vue')['default']
|
||||||
|
TeamContextPanel: typeof import('./../components/custom/team-context-panel.vue')['default']
|
||||||
ThemeSchemaSwitch: typeof import('./../components/common/theme-schema-switch.vue')['default']
|
ThemeSchemaSwitch: typeof import('./../components/common/theme-schema-switch.vue')['default']
|
||||||
UserPickerTrigger: typeof import('./../components/custom/business-user-picker/components/user-picker-trigger.vue')['default']
|
UserPickerTrigger: typeof import('./../components/custom/business-user-picker/components/user-picker-trigger.vue')['default']
|
||||||
WaveBg: typeof import('./../components/custom/wave-bg.vue')['default']
|
WaveBg: typeof import('./../components/custom/wave-bg.vue')['default']
|
||||||
|
|||||||
@@ -1,10 +1,24 @@
|
|||||||
<script setup lang="tsx">
|
<script setup lang="tsx">
|
||||||
import { computed, markRaw, reactive, ref } from 'vue';
|
import { computed, markRaw, reactive, ref, watch } from 'vue';
|
||||||
import { ElButton, ElTag } from 'element-plus';
|
import { ElButton, ElTag } from 'element-plus';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import { fetchExportOvertimeApplications, fetchGetOvertimeApplicationPage } from '@/service/api';
|
import {
|
||||||
|
fetchExportOvertimeApplications,
|
||||||
|
fetchGetMySubordinateTree,
|
||||||
|
fetchGetOvertimeApplicationPage,
|
||||||
|
fetchGetTeamOvertimeSummary
|
||||||
|
} from '@/service/api';
|
||||||
import { useUIPaginatedTable } from '@/hooks/common/table';
|
import { useUIPaginatedTable } from '@/hooks/common/table';
|
||||||
|
import { useAuth } from '@/hooks/business/auth';
|
||||||
|
import SubordinateSelector from '@/components/custom/subordinate-selector.vue';
|
||||||
|
import TeamContextPanel from '@/components/custom/team-context-panel.vue';
|
||||||
import BusinessTableActionCell, { type BusinessTableAction } from '@/components/custom/business-table-action-cell';
|
import BusinessTableActionCell, { type BusinessTableAction } from '@/components/custom/business-table-action-cell';
|
||||||
|
import {
|
||||||
|
type TeamViewContext,
|
||||||
|
type TeamViewMode,
|
||||||
|
collectSubordinateUserIds,
|
||||||
|
findSubordinateNode
|
||||||
|
} from '../shared/team-dashboard';
|
||||||
import OvertimeApplicationApprovalRecordDialog from './modules/overtime-application-approval-record-dialog.vue';
|
import OvertimeApplicationApprovalRecordDialog from './modules/overtime-application-approval-record-dialog.vue';
|
||||||
import OvertimeApplicationDetailDialog from './modules/overtime-application-detail-dialog.vue';
|
import OvertimeApplicationDetailDialog from './modules/overtime-application-detail-dialog.vue';
|
||||||
import OvertimeApplicationOperateDialog from './modules/overtime-application-operate-dialog.vue';
|
import OvertimeApplicationOperateDialog from './modules/overtime-application-operate-dialog.vue';
|
||||||
@@ -20,6 +34,7 @@ import {
|
|||||||
import IconMdiEyeOutline from '~icons/mdi/eye-outline';
|
import IconMdiEyeOutline from '~icons/mdi/eye-outline';
|
||||||
import IconMdiFileDocumentCheckOutline from '~icons/mdi/file-document-check-outline';
|
import IconMdiFileDocumentCheckOutline from '~icons/mdi/file-document-check-outline';
|
||||||
import IconMdiPencilOutline from '~icons/mdi/pencil-outline';
|
import IconMdiPencilOutline from '~icons/mdi/pencil-outline';
|
||||||
|
import IconMdiDownloadOutline from '~icons/mdi/download-outline';
|
||||||
|
|
||||||
defineOptions({ name: 'OvertimeApplication' });
|
defineOptions({ name: 'OvertimeApplication' });
|
||||||
|
|
||||||
@@ -58,6 +73,13 @@ function transformPageResult(response: OvertimeApplicationPageResponse, pageNo:
|
|||||||
}
|
}
|
||||||
|
|
||||||
const searchParams = reactive(getInitSearchParams());
|
const searchParams = reactive(getInitSearchParams());
|
||||||
|
const { hasAuth } = useAuth();
|
||||||
|
const teamViewMode = ref<TeamViewMode>('self');
|
||||||
|
const subordinateTreeLoading = ref(false);
|
||||||
|
const subordinateTree = ref<Api.SystemManage.MySubordinateTreeNode | null>(null);
|
||||||
|
const selectedSubordinateUserId = ref<string | null>(null);
|
||||||
|
const teamSummaryLoading = ref(false);
|
||||||
|
const teamSummary = ref<Api.OvertimeApplication.TeamOvertimeSummary | null>(null);
|
||||||
const operateVisible = ref(false);
|
const operateVisible = ref(false);
|
||||||
const detailVisible = ref(false);
|
const detailVisible = ref(false);
|
||||||
const approvalRecordVisible = ref(false);
|
const approvalRecordVisible = ref(false);
|
||||||
@@ -71,6 +93,39 @@ const ACTION_ICON_MAP = {
|
|||||||
edit: markRaw(IconMdiPencilOutline)
|
edit: markRaw(IconMdiPencilOutline)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const canUseTeamDashboard = computed(() => hasAuth('project:work-report:team-dashboard'));
|
||||||
|
const allSubordinateUserIds = computed(() => collectSubordinateUserIds(subordinateTree.value));
|
||||||
|
const selectedSubordinateNode = computed(() =>
|
||||||
|
findSubordinateNode(subordinateTree.value, selectedSubordinateUserId.value)
|
||||||
|
);
|
||||||
|
const isTeamMode = computed(() => teamViewMode.value === 'team');
|
||||||
|
const isRootSelected = computed(() => Boolean(isTeamMode.value && selectedSubordinateNode.value?.isRoot));
|
||||||
|
const selectedTeamLabel = computed(() => {
|
||||||
|
if (!isTeamMode.value) return '我自己';
|
||||||
|
if (!selectedSubordinateNode.value) return '--';
|
||||||
|
return selectedSubordinateNode.value.isRoot ? '全部下属' : selectedSubordinateNode.value.userNickname;
|
||||||
|
});
|
||||||
|
const teamContext = computed<TeamViewContext | null>(() => {
|
||||||
|
if (!canUseTeamDashboard.value) return null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
mode: teamViewMode.value,
|
||||||
|
selectedUserId: selectedSubordinateUserId.value,
|
||||||
|
selectedUserIds:
|
||||||
|
isTeamMode.value && selectedSubordinateUserId.value && !isRootSelected.value
|
||||||
|
? [selectedSubordinateUserId.value]
|
||||||
|
: [],
|
||||||
|
isRootSelected: isRootSelected.value,
|
||||||
|
allSubordinateUserIds: allSubordinateUserIds.value,
|
||||||
|
selectedLabel: selectedTeamLabel.value
|
||||||
|
};
|
||||||
|
});
|
||||||
|
const currentApplicantIds = computed(() => {
|
||||||
|
if (!isTeamMode.value) return null;
|
||||||
|
if (isRootSelected.value) return [];
|
||||||
|
return teamContext.value?.selectedUserIds ?? [];
|
||||||
|
});
|
||||||
|
|
||||||
const { columns, columnChecks, data, loading, getDataByPage, mobilePagination } = useUIPaginatedTable<
|
const { columns, columnChecks, data, loading, getDataByPage, mobilePagination } = useUIPaginatedTable<
|
||||||
OvertimeApplicationPageResponse,
|
OvertimeApplicationPageResponse,
|
||||||
Api.OvertimeApplication.OvertimeApplication
|
Api.OvertimeApplication.OvertimeApplication
|
||||||
@@ -79,7 +134,11 @@ const { columns, columnChecks, data, loading, getDataByPage, mobilePagination }
|
|||||||
currentPage: searchParams.pageNo,
|
currentPage: searchParams.pageNo,
|
||||||
pageSize: searchParams.pageSize
|
pageSize: searchParams.pageSize
|
||||||
},
|
},
|
||||||
api: () => fetchGetOvertimeApplicationPage(searchParams),
|
api: () =>
|
||||||
|
fetchGetOvertimeApplicationPage({
|
||||||
|
...searchParams,
|
||||||
|
applicantIds: currentApplicantIds.value
|
||||||
|
}),
|
||||||
transform: response => transformPageResult(response, searchParams.pageNo ?? 1, searchParams.pageSize ?? 10),
|
transform: response => transformPageResult(response, searchParams.pageNo ?? 1, searchParams.pageSize ?? 10),
|
||||||
onPaginationParamsChange: params => {
|
onPaginationParamsChange: params => {
|
||||||
searchParams.pageNo = params.currentPage ?? 1;
|
searchParams.pageNo = params.currentPage ?? 1;
|
||||||
@@ -87,7 +146,7 @@ const { columns, columnChecks, data, loading, getDataByPage, mobilePagination }
|
|||||||
},
|
},
|
||||||
columns: () => [
|
columns: () => [
|
||||||
{ prop: 'index', type: 'index', label: '序号', width: 64 },
|
{ prop: 'index', type: 'index', label: '序号', width: 64 },
|
||||||
{ prop: 'applicantName', label: '申请人', minWidth: 120, showOverflowTooltip: true },
|
...(isTeamMode.value ? [{ prop: 'applicantName', label: '申请人', minWidth: 120, showOverflowTooltip: true }] : []),
|
||||||
{
|
{
|
||||||
prop: 'overtimeDate',
|
prop: 'overtimeDate',
|
||||||
label: '加班日期',
|
label: '加班日期',
|
||||||
@@ -136,7 +195,7 @@ const { columns, columnChecks, data, loading, getDataByPage, mobilePagination }
|
|||||||
{
|
{
|
||||||
prop: 'operate',
|
prop: 'operate',
|
||||||
label: '操作',
|
label: '操作',
|
||||||
width: 170,
|
width: isTeamMode.value ? 140 : 170,
|
||||||
align: 'center',
|
align: 'center',
|
||||||
fixed: 'right',
|
fixed: 'right',
|
||||||
formatter: row => <BusinessTableActionCell actions={getRowActions(row)} variant="icon" />
|
formatter: row => <BusinessTableActionCell actions={getRowActions(row)} variant="icon" />
|
||||||
@@ -150,13 +209,27 @@ function getRowActions(row: Api.OvertimeApplication.OvertimeApplication): Busine
|
|||||||
const actions: BusinessTableAction[] = [
|
const actions: BusinessTableAction[] = [
|
||||||
{
|
{
|
||||||
key: 'detail',
|
key: 'detail',
|
||||||
label: '详情',
|
label: '查看',
|
||||||
buttonType: 'primary',
|
buttonType: 'primary',
|
||||||
icon: ACTION_ICON_MAP.detail,
|
icon: ACTION_ICON_MAP.detail,
|
||||||
onClick: () => openDetail(row)
|
onClick: () => openDetail(row)
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
if (isTeamMode.value) {
|
||||||
|
if (['approved', 'rejected'].includes(row.statusCode)) {
|
||||||
|
actions.push({
|
||||||
|
key: 'approval-record',
|
||||||
|
label: '审批记录',
|
||||||
|
buttonType: 'info',
|
||||||
|
icon: ACTION_ICON_MAP.approvalRecord,
|
||||||
|
onClick: () => openApprovalRecord(row)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return actions;
|
||||||
|
}
|
||||||
|
|
||||||
if (row.statusCode === 'rejected' && row.allowEdit) {
|
if (row.statusCode === 'rejected' && row.allowEdit) {
|
||||||
actions.push({
|
actions.push({
|
||||||
key: 'edit',
|
key: 'edit',
|
||||||
@@ -221,9 +294,20 @@ function handleSubmitted() {
|
|||||||
reloadTable(searchParams.pageNo ?? 1);
|
reloadTable(searchParams.pageNo ?? 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function createExportParams() {
|
||||||
|
const { pageNo: _pageNo, pageSize: _pageSize, ...params } = searchParams;
|
||||||
|
return {
|
||||||
|
...params,
|
||||||
|
applicantIds: currentApplicantIds.value
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
async function handleExport() {
|
async function handleExport() {
|
||||||
exporting.value = true;
|
exporting.value = true;
|
||||||
const { error, data: blob } = await fetchExportOvertimeApplications(searchParams);
|
const { error, data: blob } = await fetchExportOvertimeApplications({
|
||||||
|
...createExportParams(),
|
||||||
|
applicantIds: currentApplicantIds.value
|
||||||
|
});
|
||||||
exporting.value = false;
|
exporting.value = false;
|
||||||
|
|
||||||
if (error || !blob) {
|
if (error || !blob) {
|
||||||
@@ -232,56 +316,150 @@ async function handleExport() {
|
|||||||
|
|
||||||
downloadBlob(blob, `加班申请_${dayjs().format('YYYY-MM-DD')}.xls`);
|
downloadBlob(blob, `加班申请_${dayjs().format('YYYY-MM-DD')}.xls`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function loadSubordinateTree() {
|
||||||
|
if (!canUseTeamDashboard.value) return;
|
||||||
|
|
||||||
|
subordinateTreeLoading.value = true;
|
||||||
|
const { error, data: treeData } = await fetchGetMySubordinateTree();
|
||||||
|
subordinateTreeLoading.value = false;
|
||||||
|
|
||||||
|
subordinateTree.value = error || !treeData ? null : treeData;
|
||||||
|
selectedSubordinateUserId.value = treeData?.userId || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadTeamSummary() {
|
||||||
|
if (!isRootSelected.value) {
|
||||||
|
teamSummary.value = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
teamSummaryLoading.value = true;
|
||||||
|
const { error, data: summaryData } = await fetchGetTeamOvertimeSummary();
|
||||||
|
teamSummaryLoading.value = false;
|
||||||
|
|
||||||
|
teamSummary.value = error || !summaryData ? null : summaryData;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleTeamViewModeChange(mode: TeamViewMode) {
|
||||||
|
teamViewMode.value = mode;
|
||||||
|
|
||||||
|
if (mode === 'team') {
|
||||||
|
if (!subordinateTree.value) {
|
||||||
|
await loadSubordinateTree();
|
||||||
|
}
|
||||||
|
if (!selectedSubordinateUserId.value) {
|
||||||
|
selectedSubordinateUserId.value = subordinateTree.value?.userId || null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await reloadTable(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => [teamViewMode.value, selectedSubordinateUserId.value],
|
||||||
|
async () => {
|
||||||
|
if (!isTeamMode.value) return;
|
||||||
|
await reloadTable(1);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => isRootSelected.value,
|
||||||
|
() => {
|
||||||
|
loadTeamSummary();
|
||||||
|
}
|
||||||
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="flex-col-stretch gap-16px overflow-hidden">
|
<div class="overtime-application-page">
|
||||||
<OvertimeApplicationSearch v-model:model="searchParams" @reset="resetSearchParams" @search="handleSearch" />
|
<TeamContextPanel
|
||||||
|
v-if="canUseTeamDashboard"
|
||||||
<ElCard class="flex-1-hidden card-wrapper" body-class="business-table-card-body">
|
v-model:mode="teamViewMode"
|
||||||
<template #header>
|
:loading="subordinateTreeLoading"
|
||||||
<div class="flex flex-wrap items-center justify-between gap-12px">
|
:selected-label="selectedTeamLabel"
|
||||||
<div class="flex items-center gap-10px">
|
:subordinate-count="subordinateTree?.subordinateCount || 0"
|
||||||
<p class="text-16px font-600">加班申请</p>
|
@update:mode="handleTeamViewModeChange"
|
||||||
<ElTag effect="plain">{{ totalCount }}</ElTag>
|
>
|
||||||
</div>
|
<div v-if="isRootSelected" v-loading="teamSummaryLoading" class="team-overtime-summary">
|
||||||
<TableHeaderOperation v-model:columns="columnChecks" :loading="loading" @refresh="reloadTable">
|
<div class="team-overtime-summary__item">
|
||||||
<template #default>
|
<span class="team-overtime-summary__label">本月申请单数</span>
|
||||||
<ElButton plain :loading="exporting" @click="handleExport">
|
<strong class="team-overtime-summary__value">{{ teamSummary?.totalApplicationCount ?? 0 }}</strong>
|
||||||
<template #icon>
|
</div>
|
||||||
<icon-mdi-download class="text-icon" />
|
<div class="team-overtime-summary__item">
|
||||||
</template>
|
<span class="team-overtime-summary__label">本月待审批</span>
|
||||||
导出
|
<strong class="team-overtime-summary__value">{{ teamSummary?.pendingCount ?? 0 }}</strong>
|
||||||
</ElButton>
|
</div>
|
||||||
<ElButton plain type="primary" @click="openAdd">
|
<div class="team-overtime-summary__item">
|
||||||
<template #icon>
|
<span class="team-overtime-summary__label">本月已通过</span>
|
||||||
<icon-ic-round-plus class="text-icon" />
|
<strong class="team-overtime-summary__value">{{ teamSummary?.approvedCount ?? 0 }}</strong>
|
||||||
</template>
|
</div>
|
||||||
新增
|
<div class="team-overtime-summary__item">
|
||||||
</ElButton>
|
<span class="team-overtime-summary__label">本月已退回</span>
|
||||||
</template>
|
<strong class="team-overtime-summary__value">{{ teamSummary?.rejectedCount ?? 0 }}</strong>
|
||||||
</TableHeaderOperation>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
|
||||||
|
|
||||||
<div class="flex-1">
|
|
||||||
<ElTable v-loading="loading" height="100%" border row-key="id" :data="data">
|
|
||||||
<template v-for="col in columns" :key="String(col.prop)">
|
|
||||||
<ElTableColumn v-bind="col" />
|
|
||||||
</template>
|
|
||||||
</ElTable>
|
|
||||||
</div>
|
</div>
|
||||||
|
</TeamContextPanel>
|
||||||
|
|
||||||
<div class="mt-20px flex justify-end">
|
<div class="overtime-application-page__content" :class="{ 'overtime-application-page__content--team': isTeamMode }">
|
||||||
<ElPagination
|
<div v-if="canUseTeamDashboard && isTeamMode" class="overtime-application-page__sidebar">
|
||||||
v-if="mobilePagination.total"
|
<SubordinateSelector
|
||||||
layout="total,prev,pager,next,sizes"
|
v-model:selected-user-id="selectedSubordinateUserId"
|
||||||
v-bind="mobilePagination"
|
:loading="subordinateTreeLoading"
|
||||||
@current-change="mobilePagination['current-change']"
|
:data="subordinateTree"
|
||||||
@size-change="mobilePagination['size-change']"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</ElCard>
|
|
||||||
|
<div class="overtime-application-page__main">
|
||||||
|
<OvertimeApplicationSearch 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">加班申请</p>
|
||||||
|
<ElTag effect="plain">{{ totalCount }}</ElTag>
|
||||||
|
</div>
|
||||||
|
<TableHeaderOperation v-model:columns="columnChecks" :loading="loading" @refresh="reloadTable">
|
||||||
|
<template #default>
|
||||||
|
<ElButton plain :loading="exporting" @click="handleExport">
|
||||||
|
<template #icon>
|
||||||
|
<icon-mdi-download class="text-icon" />
|
||||||
|
</template>
|
||||||
|
导出
|
||||||
|
</ElButton>
|
||||||
|
<ElButton v-if="!isTeamMode" plain type="primary" @click="openAdd">
|
||||||
|
<template #icon>
|
||||||
|
<icon-ic-round-plus class="text-icon" />
|
||||||
|
</template>
|
||||||
|
新增
|
||||||
|
</ElButton>
|
||||||
|
</template>
|
||||||
|
</TableHeaderOperation>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div class="flex-1">
|
||||||
|
<ElTable v-loading="loading" height="100%" border row-key="id" :data="data">
|
||||||
|
<template v-for="col in columns" :key="String(col.prop)">
|
||||||
|
<ElTableColumn v-bind="col" />
|
||||||
|
</template>
|
||||||
|
</ElTable>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-20px flex justify-end">
|
||||||
|
<ElPagination
|
||||||
|
v-if="mobilePagination.total"
|
||||||
|
layout="total,prev,pager,next,sizes"
|
||||||
|
v-bind="mobilePagination"
|
||||||
|
@current-change="mobilePagination['current-change']"
|
||||||
|
@size-change="mobilePagination['size-change']"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</ElCard>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<OvertimeApplicationOperateDialog
|
<OvertimeApplicationOperateDialog
|
||||||
v-model:visible="operateVisible"
|
v-model:visible="operateVisible"
|
||||||
@@ -297,6 +475,42 @@ async function handleExport() {
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
.overtime-application-page {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overtime-application-page__content {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overtime-application-page__main {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
min-width: 0;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1280px) {
|
||||||
|
.overtime-application-page__content--team {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 240px minmax(0, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
.overtime-application-page__sidebar {
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
:deep(.overtime-application__reason-link) {
|
:deep(.overtime-application__reason-link) {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
@@ -318,4 +532,31 @@ async function handleExport() {
|
|||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.team-overtime-summary {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.team-overtime-summary__item {
|
||||||
|
display: grid;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 14px 16px;
|
||||||
|
border: 1px solid var(--el-border-color-light);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: var(--el-fill-color-blank);
|
||||||
|
}
|
||||||
|
|
||||||
|
.team-overtime-summary__label {
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.team-overtime-summary__value {
|
||||||
|
color: var(--el-text-color-primary);
|
||||||
|
font-size: 22px;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
70
src/views/personal-center/shared/team-dashboard.ts
Normal file
70
src/views/personal-center/shared/team-dashboard.ts
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
export type TeamViewMode = 'self' | 'team';
|
||||||
|
|
||||||
|
export interface TeamSelectableUser {
|
||||||
|
userId: string;
|
||||||
|
userNickname: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TeamSelectionState {
|
||||||
|
mode: TeamViewMode;
|
||||||
|
selectedUserId: string | null;
|
||||||
|
selectedUserIds: string[] | null;
|
||||||
|
isRootSelected: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TeamViewContext extends TeamSelectionState {
|
||||||
|
allSubordinateUserIds: string[];
|
||||||
|
selectedLabel: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveTeamQueryUserIds(context: TeamViewContext | null | undefined): string[] | null {
|
||||||
|
if (!context || context.mode !== 'team') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (context.isRootSelected) {
|
||||||
|
return [...context.allSubordinateUserIds];
|
||||||
|
}
|
||||||
|
|
||||||
|
return context.selectedUserIds ? [...context.selectedUserIds] : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function collectSubordinateUserIds(root: Api.SystemManage.MySubordinateTreeNode | null | undefined): string[] {
|
||||||
|
if (!root) return [];
|
||||||
|
|
||||||
|
const ids: string[] = [];
|
||||||
|
|
||||||
|
const walk = (nodes?: Api.SystemManage.MySubordinateTreeNode[] | null) => {
|
||||||
|
nodes?.forEach(node => {
|
||||||
|
ids.push(node.userId);
|
||||||
|
walk(node.children ?? null);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
walk(root.children ?? null);
|
||||||
|
return ids;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function findSubordinateNode(
|
||||||
|
root: Api.SystemManage.MySubordinateTreeNode | null | undefined,
|
||||||
|
userId: string | null
|
||||||
|
): Api.SystemManage.MySubordinateTreeNode | null {
|
||||||
|
if (!root || !userId) return null;
|
||||||
|
|
||||||
|
if (root.userId === userId) {
|
||||||
|
return root;
|
||||||
|
}
|
||||||
|
|
||||||
|
const stack = [...(root.children ?? [])];
|
||||||
|
while (stack.length) {
|
||||||
|
const current = stack.shift()!;
|
||||||
|
if (current.userId === userId) {
|
||||||
|
return current;
|
||||||
|
}
|
||||||
|
if (current.children?.length) {
|
||||||
|
stack.push(...current.children);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
@@ -1,16 +1,24 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onMounted, ref } from 'vue';
|
import { computed, nextTick, onMounted, ref, watch } from 'vue';
|
||||||
import { onBeforeRouteLeave } from 'vue-router';
|
import { onBeforeRouteLeave } from 'vue-router';
|
||||||
import { fetchGetProjectReportOwnerProjectOptions } from '@/service/api';
|
import { fetchGetMySubordinateTree, fetchGetProjectReportOwnerProjectOptions } from '@/service/api';
|
||||||
import { useAuth } from '@/hooks/business/auth';
|
import { useAuth } from '@/hooks/business/auth';
|
||||||
|
import SubordinateSelector from '@/components/custom/subordinate-selector.vue';
|
||||||
|
import TeamContextPanel from '@/components/custom/team-context-panel.vue';
|
||||||
|
import {
|
||||||
|
type TeamViewContext,
|
||||||
|
type TeamViewMode,
|
||||||
|
collectSubordinateUserIds,
|
||||||
|
findSubordinateNode
|
||||||
|
} from '../shared/team-dashboard';
|
||||||
import WorkReportCreateDialog from './shared/components/create-dialog.vue';
|
import WorkReportCreateDialog from './shared/components/create-dialog.vue';
|
||||||
import WorkReportPrototypePageDialog from './shared/components/prototype-page-dialog.vue';
|
import WorkReportPrototypePageDialog from './shared/components/prototype-page-dialog.vue';
|
||||||
import WorkReportTabs from './shared/components/tabs.vue';
|
import WorkReportTabs from './shared/components/tabs.vue';
|
||||||
import {
|
import {
|
||||||
WORK_REPORT_PROJECT_OWNER_PERMISSION,
|
WORK_REPORT_PROJECT_OWNER_PERMISSION,
|
||||||
WORK_REPORT_TYPE_LABEL,
|
|
||||||
type WorkReportRow,
|
type WorkReportRow,
|
||||||
type WorkReportType
|
type WorkReportType,
|
||||||
|
getWorkReportTypeDisplayLabel
|
||||||
} from './shared/types';
|
} from './shared/types';
|
||||||
import WeeklyReportIndex from './weekly/index.vue';
|
import WeeklyReportIndex from './weekly/index.vue';
|
||||||
import WeeklyReportApprovalRecordDialog from './weekly/modules/approval-record-dialog.vue';
|
import WeeklyReportApprovalRecordDialog from './weekly/modules/approval-record-dialog.vue';
|
||||||
@@ -29,6 +37,10 @@ type ReportListExpose = {
|
|||||||
const { hasAuth } = useAuth();
|
const { hasAuth } = useAuth();
|
||||||
|
|
||||||
const activeTab = ref<WorkReportType>('weekly');
|
const activeTab = ref<WorkReportType>('weekly');
|
||||||
|
const teamViewMode = ref<TeamViewMode>('self');
|
||||||
|
const subordinateTreeLoading = ref(false);
|
||||||
|
const subordinateTree = ref<Api.SystemManage.MySubordinateTreeNode | null>(null);
|
||||||
|
const selectedSubordinateUserId = ref<string | null>(null);
|
||||||
const createVisible = ref(false);
|
const createVisible = ref(false);
|
||||||
const pageDialogVisible = ref(false);
|
const pageDialogVisible = ref(false);
|
||||||
const pageDialogMode = ref<PageDialogMode>('detail');
|
const pageDialogMode = ref<PageDialogMode>('detail');
|
||||||
@@ -50,18 +62,45 @@ const monthlyRef = ref<ReportListExpose | null>(null);
|
|||||||
const projectRef = ref<ReportListExpose | null>(null);
|
const projectRef = ref<ReportListExpose | null>(null);
|
||||||
|
|
||||||
const canShowProjectTab = computed(() => hasAuth(WORK_REPORT_PROJECT_OWNER_PERMISSION));
|
const canShowProjectTab = computed(() => hasAuth(WORK_REPORT_PROJECT_OWNER_PERMISSION));
|
||||||
|
const canUseTeamDashboard = computed(() => hasAuth('project:work-report:team-dashboard'));
|
||||||
|
const allSubordinateUserIds = computed(() => collectSubordinateUserIds(subordinateTree.value));
|
||||||
|
const selectedSubordinateNode = computed(() =>
|
||||||
|
findSubordinateNode(subordinateTree.value, selectedSubordinateUserId.value)
|
||||||
|
);
|
||||||
|
const isRootSelected = computed(() => Boolean(selectedSubordinateNode.value?.isRoot));
|
||||||
|
const selectedTeamLabel = computed(() => {
|
||||||
|
if (teamViewMode.value === 'self') return '我自己';
|
||||||
|
if (!selectedSubordinateNode.value) return '--';
|
||||||
|
return selectedSubordinateNode.value.isRoot ? '全部下属' : selectedSubordinateNode.value.userNickname;
|
||||||
|
});
|
||||||
|
const teamContext = computed<TeamViewContext | null>(() => {
|
||||||
|
if (!canUseTeamDashboard.value) return null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
mode: teamViewMode.value,
|
||||||
|
selectedUserId: selectedSubordinateUserId.value,
|
||||||
|
selectedUserIds:
|
||||||
|
teamViewMode.value === 'team' && selectedSubordinateUserId.value && !isRootSelected.value
|
||||||
|
? [selectedSubordinateUserId.value]
|
||||||
|
: [],
|
||||||
|
isRootSelected: teamViewMode.value === 'team' && isRootSelected.value,
|
||||||
|
allSubordinateUserIds: allSubordinateUserIds.value,
|
||||||
|
selectedLabel: selectedTeamLabel.value
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
/** 项目选项是否加载成功(用于项目半月报列表内部判断) */
|
/** 项目选项是否加载成功(用于项目半月报列表内部判断) */
|
||||||
const projectOptionsLoaded = ref(false);
|
const projectOptionsLoaded = ref(false);
|
||||||
|
|
||||||
const visibleTabs = computed<Array<{ label: string; name: WorkReportType }>>(() => {
|
const visibleTabs = computed<Array<{ label: string; name: WorkReportType }>>(() => {
|
||||||
|
const isTeamReportMode = teamViewMode.value === 'team';
|
||||||
const tabs: Array<{ label: string; name: WorkReportType }> = [
|
const tabs: Array<{ label: string; name: WorkReportType }> = [
|
||||||
{ label: WORK_REPORT_TYPE_LABEL.weekly, name: 'weekly' },
|
{ label: getWorkReportTypeDisplayLabel('weekly', isTeamReportMode), name: 'weekly' },
|
||||||
{ label: WORK_REPORT_TYPE_LABEL.monthly, name: 'monthly' }
|
{ label: getWorkReportTypeDisplayLabel('monthly', isTeamReportMode), name: 'monthly' }
|
||||||
];
|
];
|
||||||
|
|
||||||
if (canShowProjectTab.value) {
|
if (canShowProjectTab.value) {
|
||||||
tabs.push({ label: WORK_REPORT_TYPE_LABEL.project, name: 'project' });
|
tabs.push({ label: getWorkReportTypeDisplayLabel('project', isTeamReportMode), name: 'project' });
|
||||||
}
|
}
|
||||||
|
|
||||||
return tabs;
|
return tabs;
|
||||||
@@ -87,6 +126,17 @@ async function loadProjectOptions() {
|
|||||||
projectOptionsLoaded.value = !error;
|
projectOptionsLoaded.value = !error;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function loadSubordinateTree() {
|
||||||
|
if (!canUseTeamDashboard.value) return;
|
||||||
|
|
||||||
|
subordinateTreeLoading.value = true;
|
||||||
|
const { error, data } = await fetchGetMySubordinateTree();
|
||||||
|
subordinateTreeLoading.value = false;
|
||||||
|
|
||||||
|
subordinateTree.value = error || !data ? null : data;
|
||||||
|
selectedSubordinateUserId.value = data?.userId || null;
|
||||||
|
}
|
||||||
|
|
||||||
function openCreate(reportType: WorkReportType) {
|
function openCreate(reportType: WorkReportType) {
|
||||||
currentReportType.value = reportType;
|
currentReportType.value = reportType;
|
||||||
createVisible.value = true;
|
createVisible.value = true;
|
||||||
@@ -133,9 +183,10 @@ function openApprovalRecord(reportType: WorkReportType, row: WorkReportRow) {
|
|||||||
approvalRecordVisible.value = true;
|
approvalRecordVisible.value = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleTabChange(tab: WorkReportType) {
|
async function handleTabChange(tab: WorkReportType) {
|
||||||
activeTab.value = tab;
|
activeTab.value = tab;
|
||||||
getListRef(tab)?.reload(1);
|
await nextTick();
|
||||||
|
await getListRef(tab)?.reload(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function reloadReport(reportType = currentReportType.value) {
|
async function reloadReport(reportType = currentReportType.value) {
|
||||||
@@ -153,8 +204,35 @@ function closeFloatingPanels() {
|
|||||||
approvalRecordVisible.value = false;
|
approvalRecordVisible.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handleTeamViewModeChange(mode: TeamViewMode) {
|
||||||
|
teamViewMode.value = mode;
|
||||||
|
|
||||||
|
if (mode === 'team') {
|
||||||
|
if (!subordinateTree.value) {
|
||||||
|
await loadSubordinateTree();
|
||||||
|
}
|
||||||
|
if (!selectedSubordinateUserId.value) {
|
||||||
|
selectedSubordinateUserId.value = subordinateTree.value?.userId || null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await nextTick();
|
||||||
|
await getListRef(activeTab.value)?.reload(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(selectedSubordinateUserId, async (currentUserId, previousUserId) => {
|
||||||
|
if (!canUseTeamDashboard.value || teamViewMode.value !== 'team') return;
|
||||||
|
if (!currentUserId || currentUserId === previousUserId) return;
|
||||||
|
|
||||||
|
await nextTick();
|
||||||
|
await getListRef(activeTab.value)?.reload(1);
|
||||||
|
});
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await loadProjectOptions();
|
await loadProjectOptions();
|
||||||
|
if (canUseTeamDashboard.value) {
|
||||||
|
await loadSubordinateTree();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
onBeforeRouteLeave(() => {
|
onBeforeRouteLeave(() => {
|
||||||
@@ -167,16 +245,41 @@ onBeforeRouteLeave(() => {
|
|||||||
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"
|
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">
|
<div
|
||||||
<WorkReportTabs :active-tab="activeTab" :tabs="visibleTabs" @update:active-tab="handleTabChange" />
|
class="work-report-page-shell__sidebar"
|
||||||
|
:class="{ 'work-report-page-shell__sidebar--team': canUseTeamDashboard && teamViewMode === 'team' }"
|
||||||
|
>
|
||||||
|
<WorkReportTabs
|
||||||
|
class="work-report-page-shell__sidebar-card"
|
||||||
|
:active-tab="activeTab"
|
||||||
|
:tabs="visibleTabs"
|
||||||
|
@update:active-tab="handleTabChange"
|
||||||
|
/>
|
||||||
|
<SubordinateSelector
|
||||||
|
v-if="canUseTeamDashboard && teamViewMode === 'team'"
|
||||||
|
v-model:selected-user-id="selectedSubordinateUserId"
|
||||||
|
class="work-report-page-shell__sidebar-card"
|
||||||
|
:loading="subordinateTreeLoading"
|
||||||
|
:data="subordinateTree"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 右侧:搜索区 + 列表区 -->
|
<!-- 右侧:搜索区 + 列表区 -->
|
||||||
<div class="flex-col-stretch gap-16px xl:min-h-0">
|
<div class="flex-col-stretch gap-16px xl:min-h-0">
|
||||||
|
<TeamContextPanel
|
||||||
|
v-if="canUseTeamDashboard"
|
||||||
|
v-model:mode="teamViewMode"
|
||||||
|
:loading="subordinateTreeLoading"
|
||||||
|
:selected-label="selectedTeamLabel"
|
||||||
|
:subordinate-count="subordinateTree?.subordinateCount || 0"
|
||||||
|
@update:mode="handleTeamViewModeChange"
|
||||||
|
/>
|
||||||
|
|
||||||
<WeeklyReportIndex
|
<WeeklyReportIndex
|
||||||
v-show="activeTab === 'weekly'"
|
v-show="activeTab === 'weekly'"
|
||||||
ref="weeklyRef"
|
ref="weeklyRef"
|
||||||
class="flex-1-hidden"
|
class="flex-1-hidden"
|
||||||
|
:team-context="teamContext"
|
||||||
@create="openCreate('weekly')"
|
@create="openCreate('weekly')"
|
||||||
@edit="openEdit('weekly', $event)"
|
@edit="openEdit('weekly', $event)"
|
||||||
@detail="openDetail('weekly', $event)"
|
@detail="openDetail('weekly', $event)"
|
||||||
@@ -187,6 +290,7 @@ onBeforeRouteLeave(() => {
|
|||||||
v-show="activeTab === 'monthly'"
|
v-show="activeTab === 'monthly'"
|
||||||
ref="monthlyRef"
|
ref="monthlyRef"
|
||||||
class="flex-1-hidden"
|
class="flex-1-hidden"
|
||||||
|
:team-context="teamContext"
|
||||||
@create="openCreate('monthly')"
|
@create="openCreate('monthly')"
|
||||||
@edit="openEdit('monthly', $event)"
|
@edit="openEdit('monthly', $event)"
|
||||||
@detail="openDetail('monthly', $event)"
|
@detail="openDetail('monthly', $event)"
|
||||||
@@ -198,6 +302,7 @@ onBeforeRouteLeave(() => {
|
|||||||
v-show="activeTab === 'project'"
|
v-show="activeTab === 'project'"
|
||||||
ref="projectRef"
|
ref="projectRef"
|
||||||
class="flex-1-hidden"
|
class="flex-1-hidden"
|
||||||
|
:team-context="teamContext"
|
||||||
:project-options="projectOptions"
|
:project-options="projectOptions"
|
||||||
:project-options-loaded="projectOptionsLoaded"
|
:project-options-loaded="projectOptionsLoaded"
|
||||||
@create="openCreate('project')"
|
@create="openCreate('project')"
|
||||||
@@ -238,4 +343,25 @@ onBeforeRouteLeave(() => {
|
|||||||
.work-report-page-shell {
|
.work-report-page-shell {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.work-report-page-shell__sidebar {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1280px) {
|
||||||
|
.work-report-page-shell__sidebar {
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.work-report-page-shell__sidebar--team {
|
||||||
|
display: grid;
|
||||||
|
grid-template-rows: minmax(0, 1fr) minmax(0, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
.work-report-page-shell__sidebar-card {
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,18 +1,19 @@
|
|||||||
<script setup lang="tsx">
|
<script setup lang="tsx">
|
||||||
/* eslint-disable no-void */
|
/* eslint-disable no-void */
|
||||||
import { markRaw, reactive, ref } from 'vue';
|
import { computed, markRaw, reactive, ref } from 'vue';
|
||||||
import { ElMessageBox, ElTag } from 'element-plus';
|
import { ElMessageBox, ElTag } from 'element-plus';
|
||||||
import {
|
import {
|
||||||
fetchDeleteMonthlyReport,
|
fetchDeleteMonthlyReport,
|
||||||
fetchExportMonthlyReportContent,
|
fetchExportMonthlyReportContent,
|
||||||
fetchGetMonthlyReportPage,
|
fetchGetMonthlyReportPage,
|
||||||
|
fetchGetTeamReportSummary,
|
||||||
fetchSubmitMonthlyReport
|
fetchSubmitMonthlyReport
|
||||||
} from '@/service/api';
|
} from '@/service/api';
|
||||||
import { useAuth } from '@/hooks/business/auth';
|
import { useAuth } from '@/hooks/business/auth';
|
||||||
import { useUIPaginatedTable } from '@/hooks/common/table';
|
import { useUIPaginatedTable } from '@/hooks/common/table';
|
||||||
import BusinessTableActionCell, { type BusinessTableAction } from '@/components/custom/business-table-action-cell';
|
import BusinessTableActionCell, { type BusinessTableAction } from '@/components/custom/business-table-action-cell';
|
||||||
|
import { type TeamViewContext, resolveTeamQueryUserIds } from '@/views/personal-center/shared/team-dashboard';
|
||||||
import {
|
import {
|
||||||
WORK_REPORT_TYPE_LABEL,
|
|
||||||
type WorkReportRow,
|
type WorkReportRow,
|
||||||
createMonthlySearchParams,
|
createMonthlySearchParams,
|
||||||
createWorkReportContentExportFallbackName,
|
createWorkReportContentExportFallbackName,
|
||||||
@@ -21,19 +22,27 @@ import {
|
|||||||
formatEmptyText,
|
formatEmptyText,
|
||||||
formatPeriod,
|
formatPeriod,
|
||||||
getWorkReportStatusLabel,
|
getWorkReportStatusLabel,
|
||||||
|
getWorkReportTypeDisplayLabel,
|
||||||
resolveExportFilename,
|
resolveExportFilename,
|
||||||
resolveWorkReportStatusTagType,
|
resolveWorkReportStatusTagType,
|
||||||
transformWorkReportPage
|
transformWorkReportPage
|
||||||
} from '../shared/types';
|
} from '../shared/types';
|
||||||
|
import { resolveWorkReportSummaryPeriod } from '../shared/utils';
|
||||||
|
import TeamReportSummary from '../shared/components/team-report-summary.vue';
|
||||||
import MonthlyReportSearch from './modules/search-panel.vue';
|
import MonthlyReportSearch from './modules/search-panel.vue';
|
||||||
import IconMdiDeleteOutline from '~icons/mdi/delete-outline';
|
import IconMdiDeleteOutline from '~icons/mdi/delete-outline';
|
||||||
import IconMdiEyeOutline from '~icons/mdi/eye-outline';
|
import IconMdiEyeOutline from '~icons/mdi/eye-outline';
|
||||||
import IconMdiFileDocumentCheckOutline from '~icons/mdi/file-document-check-outline';
|
import IconMdiFileDocumentCheckOutline from '~icons/mdi/file-document-check-outline';
|
||||||
import IconMdiPencilOutline from '~icons/mdi/pencil-outline';
|
import IconMdiPencilOutline from '~icons/mdi/pencil-outline';
|
||||||
import IconMdiSendOutline from '~icons/mdi/send-outline';
|
import IconMdiSendOutline from '~icons/mdi/send-outline';
|
||||||
|
import IconMdiDownloadOutline from '~icons/mdi/download-outline';
|
||||||
|
|
||||||
defineOptions({ name: 'MonthlyWorkReportIndex' });
|
defineOptions({ name: 'MonthlyWorkReportIndex' });
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
teamContext?: TeamViewContext | null;
|
||||||
|
}>();
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: 'create'): void;
|
(e: 'create'): void;
|
||||||
(e: 'edit', row: WorkReportRow): void;
|
(e: 'edit', row: WorkReportRow): void;
|
||||||
@@ -45,21 +54,33 @@ const { hasAuth } = useAuth();
|
|||||||
const exporting = ref(false);
|
const exporting = ref(false);
|
||||||
const selectedRows = ref<Api.WorkReport.Monthly.MonthlyReport[]>([]);
|
const selectedRows = ref<Api.WorkReport.Monthly.MonthlyReport[]>([]);
|
||||||
const searchParams = reactive(createMonthlySearchParams());
|
const searchParams = reactive(createMonthlySearchParams());
|
||||||
|
const teamSummaryLoading = ref(false);
|
||||||
|
const teamSummary = ref<Api.WorkReport.Common.TeamReportSummary | null>(null);
|
||||||
|
|
||||||
const ACTION_ICON_MAP = {
|
const ACTION_ICON_MAP = {
|
||||||
detail: markRaw(IconMdiEyeOutline),
|
detail: markRaw(IconMdiEyeOutline),
|
||||||
edit: markRaw(IconMdiPencilOutline),
|
edit: markRaw(IconMdiPencilOutline),
|
||||||
submit: markRaw(IconMdiSendOutline),
|
submit: markRaw(IconMdiSendOutline),
|
||||||
delete: markRaw(IconMdiDeleteOutline),
|
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('monthly', isTeamMode.value));
|
||||||
|
|
||||||
const table = useUIPaginatedTable<
|
const table = useUIPaginatedTable<
|
||||||
Awaited<ReturnType<typeof fetchGetMonthlyReportPage>>,
|
Awaited<ReturnType<typeof fetchGetMonthlyReportPage>>,
|
||||||
Api.WorkReport.Monthly.MonthlyReport
|
Api.WorkReport.Monthly.MonthlyReport
|
||||||
>({
|
>({
|
||||||
paginationProps: { currentPage: searchParams.pageNo, pageSize: searchParams.pageSize },
|
paginationProps: { currentPage: searchParams.pageNo, pageSize: searchParams.pageSize },
|
||||||
api: () => fetchGetMonthlyReportPage(searchParams),
|
api: () =>
|
||||||
|
fetchGetMonthlyReportPage({
|
||||||
|
...searchParams,
|
||||||
|
reporterIds: currentTeamReporterIds.value
|
||||||
|
}),
|
||||||
transform: response => transformWorkReportPage(response, searchParams.pageNo ?? 1, searchParams.pageSize ?? 10),
|
transform: response => transformWorkReportPage(response, searchParams.pageNo ?? 1, searchParams.pageSize ?? 10),
|
||||||
onPaginationParamsChange: params => {
|
onPaginationParamsChange: params => {
|
||||||
searchParams.pageNo = params.currentPage ?? 1;
|
searchParams.pageNo = params.currentPage ?? 1;
|
||||||
@@ -67,6 +88,7 @@ const table = useUIPaginatedTable<
|
|||||||
},
|
},
|
||||||
columns: () => [
|
columns: () => [
|
||||||
{ prop: 'index', type: 'index', label: '序号', width: 64 },
|
{ prop: 'index', type: 'index', label: '序号', width: 64 },
|
||||||
|
...(isTeamMode.value ? [{ prop: 'reporterName', label: '提交人', minWidth: 100, showOverflowTooltip: true }] : []),
|
||||||
{ prop: 'periodLabel', label: '月份', minWidth: 80, formatter: row => formatPeriod(row) },
|
{ prop: 'periodLabel', label: '月份', minWidth: 80, formatter: row => formatPeriod(row) },
|
||||||
{
|
{
|
||||||
prop: 'reporterDeptName',
|
prop: 'reporterDeptName',
|
||||||
@@ -93,7 +115,7 @@ const table = useUIPaginatedTable<
|
|||||||
{
|
{
|
||||||
prop: 'operate',
|
prop: 'operate',
|
||||||
label: '操作',
|
label: '操作',
|
||||||
width: 180,
|
width: isTeamMode.value ? 140 : 180,
|
||||||
align: 'center',
|
align: 'center',
|
||||||
fixed: 'right',
|
fixed: 'right',
|
||||||
formatter: row => <BusinessTableActionCell actions={getRowActions(row)} variant="icon" />
|
formatter: row => <BusinessTableActionCell actions={getRowActions(row)} variant="icon" />
|
||||||
@@ -101,17 +123,53 @@ const table = useUIPaginatedTable<
|
|||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const summaryPeriod = computed(() =>
|
||||||
|
resolveWorkReportSummaryPeriod('monthly', {
|
||||||
|
currentRow: table.data.value[0],
|
||||||
|
periodRange: searchParams.periodStartDate
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
function getRowActions(row: Api.WorkReport.Monthly.MonthlyReport): BusinessTableAction[] {
|
function getRowActions(row: Api.WorkReport.Monthly.MonthlyReport): BusinessTableAction[] {
|
||||||
const actions: BusinessTableAction[] = [
|
const actions: BusinessTableAction[] = [
|
||||||
{
|
{
|
||||||
key: 'detail',
|
key: 'detail',
|
||||||
label: '详情',
|
label: '查看',
|
||||||
buttonType: 'primary',
|
buttonType: 'primary',
|
||||||
icon: ACTION_ICON_MAP.detail,
|
icon: ACTION_ICON_MAP.detail,
|
||||||
onClick: () => emit('detail', row)
|
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')) {
|
if (['draft', 'rejected'].includes(row.statusCode) && row.allowEdit && hasAuth('project:work-report:update')) {
|
||||||
actions.push({
|
actions.push({
|
||||||
key: 'edit',
|
key: 'edit',
|
||||||
@@ -154,6 +212,7 @@ function getRowActions(row: Api.WorkReport.Monthly.MonthlyReport): BusinessTable
|
|||||||
|
|
||||||
async function reload(page?: number) {
|
async function reload(page?: number) {
|
||||||
await table.getDataByPage(page ?? searchParams.pageNo ?? 1);
|
await table.getDataByPage(page ?? searchParams.pageNo ?? 1);
|
||||||
|
await loadTeamSummary();
|
||||||
}
|
}
|
||||||
|
|
||||||
function resetSearchParams() {
|
function resetSearchParams() {
|
||||||
@@ -209,7 +268,10 @@ function handleSelectionChange(rows: Api.WorkReport.Monthly.MonthlyReport[]) {
|
|||||||
|
|
||||||
function createExportSearchParams() {
|
function createExportSearchParams() {
|
||||||
const { pageNo: _pageNo, pageSize: _pageSize, ...params } = searchParams;
|
const { pageNo: _pageNo, pageSize: _pageSize, ...params } = searchParams;
|
||||||
return params;
|
return {
|
||||||
|
...params,
|
||||||
|
reporterIds: currentTeamReporterIds.value
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function exportReportContent(
|
async function exportReportContent(
|
||||||
@@ -267,6 +329,23 @@ async function handleExportCommand(command: 'selected' | 'all') {
|
|||||||
await handleExportAll();
|
await handleExportAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function loadTeamSummary() {
|
||||||
|
if (!isTeamRootSelected.value) {
|
||||||
|
teamSummaryLoading.value = false;
|
||||||
|
teamSummary.value = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
teamSummaryLoading.value = true;
|
||||||
|
const { error, data } = await fetchGetTeamReportSummary({
|
||||||
|
reportType: 'monthly',
|
||||||
|
periodKey: summaryPeriod.value.periodKey
|
||||||
|
});
|
||||||
|
teamSummaryLoading.value = false;
|
||||||
|
|
||||||
|
teamSummary.value = error || !data ? null : data;
|
||||||
|
}
|
||||||
|
|
||||||
defineExpose({ reload });
|
defineExpose({ reload });
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -274,11 +353,21 @@ defineExpose({ reload });
|
|||||||
<div class="flex-col-stretch gap-16px overflow-hidden">
|
<div class="flex-col-stretch gap-16px overflow-hidden">
|
||||||
<MonthlyReportSearch v-model:model="searchParams" @reset="resetSearchParams" @search="handleSearch" />
|
<MonthlyReportSearch v-model:model="searchParams" @reset="resetSearchParams" @search="handleSearch" />
|
||||||
|
|
||||||
|
<TeamReportSummary
|
||||||
|
v-if="isTeamRootSelected"
|
||||||
|
report-type="monthly"
|
||||||
|
:period-key="summaryPeriod.periodKey"
|
||||||
|
:period-label="formatPeriod(summaryPeriod)"
|
||||||
|
:loading="teamSummaryLoading"
|
||||||
|
:summary="teamSummary"
|
||||||
|
@reminded="loadTeamSummary"
|
||||||
|
/>
|
||||||
|
|
||||||
<ElCard class="flex-1-hidden card-wrapper" body-class="business-table-card-body">
|
<ElCard class="flex-1-hidden card-wrapper" body-class="business-table-card-body">
|
||||||
<template #header>
|
<template #header>
|
||||||
<div class="flex flex-wrap items-center justify-between gap-12px">
|
<div class="flex flex-wrap items-center justify-between gap-12px">
|
||||||
<div class="flex items-center gap-10px">
|
<div class="flex items-center gap-10px">
|
||||||
<p class="text-16px font-600">{{ WORK_REPORT_TYPE_LABEL.monthly }}</p>
|
<p class="text-16px font-600">{{ reportTitle }}</p>
|
||||||
<ElTag effect="plain">{{ table.mobilePagination.value.total || 0 }}</ElTag>
|
<ElTag effect="plain">{{ table.mobilePagination.value.total || 0 }}</ElTag>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -304,7 +393,13 @@ defineExpose({ reload });
|
|||||||
</ElDropdownMenu>
|
</ElDropdownMenu>
|
||||||
</template>
|
</template>
|
||||||
</ElDropdown>
|
</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>
|
<template #icon>
|
||||||
<icon-ic-round-plus class="text-icon" />
|
<icon-ic-round-plus class="text-icon" />
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,18 +1,19 @@
|
|||||||
<script setup lang="tsx">
|
<script setup lang="tsx">
|
||||||
/* eslint-disable no-void */
|
/* eslint-disable no-void */
|
||||||
import { markRaw, reactive, ref } from 'vue';
|
import { computed, markRaw, reactive, ref } from 'vue';
|
||||||
import { ElMessageBox, ElTag } from 'element-plus';
|
import { ElMessageBox, ElTag } from 'element-plus';
|
||||||
import {
|
import {
|
||||||
fetchDeleteProjectReport,
|
fetchDeleteProjectReport,
|
||||||
fetchExportProjectReportContent,
|
fetchExportProjectReportContent,
|
||||||
fetchGetProjectReportPage,
|
fetchGetProjectReportPage,
|
||||||
|
fetchGetTeamReportSummary,
|
||||||
fetchSubmitProjectReport
|
fetchSubmitProjectReport
|
||||||
} from '@/service/api';
|
} from '@/service/api';
|
||||||
import { useAuth } from '@/hooks/business/auth';
|
import { useAuth } from '@/hooks/business/auth';
|
||||||
import { useUIPaginatedTable } from '@/hooks/common/table';
|
import { useUIPaginatedTable } from '@/hooks/common/table';
|
||||||
import BusinessTableActionCell, { type BusinessTableAction } from '@/components/custom/business-table-action-cell';
|
import BusinessTableActionCell, { type BusinessTableAction } from '@/components/custom/business-table-action-cell';
|
||||||
|
import { type TeamViewContext, resolveTeamQueryUserIds } from '@/views/personal-center/shared/team-dashboard';
|
||||||
import {
|
import {
|
||||||
WORK_REPORT_TYPE_LABEL,
|
|
||||||
type WorkReportRow,
|
type WorkReportRow,
|
||||||
createProjectSearchParams,
|
createProjectSearchParams,
|
||||||
createWorkReportContentExportFallbackName,
|
createWorkReportContentExportFallbackName,
|
||||||
@@ -20,22 +21,26 @@ import {
|
|||||||
formatDateTime,
|
formatDateTime,
|
||||||
formatEmptyText,
|
formatEmptyText,
|
||||||
formatPeriod,
|
formatPeriod,
|
||||||
getProjectReportFlagLabel,
|
|
||||||
getWorkReportStatusLabel,
|
getWorkReportStatusLabel,
|
||||||
|
getWorkReportTypeDisplayLabel,
|
||||||
resolveExportFilename,
|
resolveExportFilename,
|
||||||
resolveWorkReportStatusTagType,
|
resolveWorkReportStatusTagType,
|
||||||
transformWorkReportPage
|
transformWorkReportPage
|
||||||
} from '../shared/types';
|
} from '../shared/types';
|
||||||
|
import { resolveWorkReportSummaryPeriod } from '../shared/utils';
|
||||||
|
import TeamReportSummary from '../shared/components/team-report-summary.vue';
|
||||||
import ProjectReportSearch from './modules/search-panel.vue';
|
import ProjectReportSearch from './modules/search-panel.vue';
|
||||||
import IconMdiDeleteOutline from '~icons/mdi/delete-outline';
|
import IconMdiDeleteOutline from '~icons/mdi/delete-outline';
|
||||||
import IconMdiEyeOutline from '~icons/mdi/eye-outline';
|
import IconMdiEyeOutline from '~icons/mdi/eye-outline';
|
||||||
import IconMdiFileDocumentCheckOutline from '~icons/mdi/file-document-check-outline';
|
import IconMdiFileDocumentCheckOutline from '~icons/mdi/file-document-check-outline';
|
||||||
import IconMdiPencilOutline from '~icons/mdi/pencil-outline';
|
import IconMdiPencilOutline from '~icons/mdi/pencil-outline';
|
||||||
import IconMdiSendOutline from '~icons/mdi/send-outline';
|
import IconMdiSendOutline from '~icons/mdi/send-outline';
|
||||||
|
import IconMdiDownloadOutline from '~icons/mdi/download-outline';
|
||||||
|
|
||||||
defineOptions({ name: 'ProjectWorkReportIndex' });
|
defineOptions({ name: 'ProjectWorkReportIndex' });
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
|
teamContext?: TeamViewContext | null;
|
||||||
projectOptions: Api.WorkReport.Project.ProjectReportOwnerProjectOption[];
|
projectOptions: Api.WorkReport.Project.ProjectReportOwnerProjectOption[];
|
||||||
projectOptionsLoaded: boolean;
|
projectOptionsLoaded: boolean;
|
||||||
}>();
|
}>();
|
||||||
@@ -51,21 +56,33 @@ const { hasAuth } = useAuth();
|
|||||||
const exporting = ref(false);
|
const exporting = ref(false);
|
||||||
const selectedRows = ref<Api.WorkReport.Project.ProjectReport[]>([]);
|
const selectedRows = ref<Api.WorkReport.Project.ProjectReport[]>([]);
|
||||||
const searchParams = reactive(createProjectSearchParams());
|
const searchParams = reactive(createProjectSearchParams());
|
||||||
|
const teamSummaryLoading = ref(false);
|
||||||
|
const teamSummary = ref<Api.WorkReport.Common.TeamReportSummary | null>(null);
|
||||||
|
|
||||||
const ACTION_ICON_MAP = {
|
const ACTION_ICON_MAP = {
|
||||||
detail: markRaw(IconMdiEyeOutline),
|
detail: markRaw(IconMdiEyeOutline),
|
||||||
edit: markRaw(IconMdiPencilOutline),
|
edit: markRaw(IconMdiPencilOutline),
|
||||||
submit: markRaw(IconMdiSendOutline),
|
submit: markRaw(IconMdiSendOutline),
|
||||||
delete: markRaw(IconMdiDeleteOutline),
|
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 currentProjectOwnerIds = computed(() => resolveTeamQueryUserIds(props.teamContext));
|
||||||
|
const reportTitle = computed(() => getWorkReportTypeDisplayLabel('project', isTeamMode.value));
|
||||||
|
|
||||||
const table = useUIPaginatedTable<
|
const table = useUIPaginatedTable<
|
||||||
Awaited<ReturnType<typeof fetchGetProjectReportPage>>,
|
Awaited<ReturnType<typeof fetchGetProjectReportPage>>,
|
||||||
Api.WorkReport.Project.ProjectReport
|
Api.WorkReport.Project.ProjectReport
|
||||||
>({
|
>({
|
||||||
paginationProps: { currentPage: searchParams.pageNo, pageSize: searchParams.pageSize },
|
paginationProps: { currentPage: searchParams.pageNo, pageSize: searchParams.pageSize },
|
||||||
api: () => fetchGetProjectReportPage(searchParams),
|
api: () =>
|
||||||
|
fetchGetProjectReportPage({
|
||||||
|
...searchParams,
|
||||||
|
projectOwnerIds: currentProjectOwnerIds.value
|
||||||
|
}),
|
||||||
transform: response => transformWorkReportPage(response, searchParams.pageNo ?? 1, searchParams.pageSize ?? 10),
|
transform: response => transformWorkReportPage(response, searchParams.pageNo ?? 1, searchParams.pageSize ?? 10),
|
||||||
onPaginationParamsChange: params => {
|
onPaginationParamsChange: params => {
|
||||||
searchParams.pageNo = params.currentPage ?? 1;
|
searchParams.pageNo = params.currentPage ?? 1;
|
||||||
@@ -73,9 +90,11 @@ const table = useUIPaginatedTable<
|
|||||||
},
|
},
|
||||||
columns: () => [
|
columns: () => [
|
||||||
{ prop: 'index', type: 'index', label: '序号', width: 64 },
|
{ prop: 'index', type: 'index', label: '序号', width: 64 },
|
||||||
|
...(isTeamMode.value
|
||||||
|
? [{ prop: 'projectOwnerName', label: '提交人', minWidth: 100, showOverflowTooltip: true }]
|
||||||
|
: []),
|
||||||
{ prop: 'projectName', label: '项目名称', minWidth: 200, showOverflowTooltip: true },
|
{ prop: 'projectName', label: '项目名称', minWidth: 200, showOverflowTooltip: true },
|
||||||
{ prop: 'periodLabel', label: '半月周期', minWidth: 120, formatter: row => formatPeriod(row) },
|
{ 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: 'projectOwnerName', label: '项目负责人', minWidth: 80 },
|
||||||
{
|
{
|
||||||
prop: 'technicalOwnerName',
|
prop: 'technicalOwnerName',
|
||||||
@@ -101,7 +120,7 @@ const table = useUIPaginatedTable<
|
|||||||
{
|
{
|
||||||
prop: 'operate',
|
prop: 'operate',
|
||||||
label: '操作',
|
label: '操作',
|
||||||
width: 180,
|
width: isTeamMode.value ? 140 : 180,
|
||||||
align: 'center',
|
align: 'center',
|
||||||
fixed: 'right',
|
fixed: 'right',
|
||||||
formatter: row => <BusinessTableActionCell actions={getRowActions(row)} variant="icon" />
|
formatter: row => <BusinessTableActionCell actions={getRowActions(row)} variant="icon" />
|
||||||
@@ -109,17 +128,54 @@ const table = useUIPaginatedTable<
|
|||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const summaryPeriod = computed(() =>
|
||||||
|
resolveWorkReportSummaryPeriod('project', {
|
||||||
|
currentRow: table.data.value[0],
|
||||||
|
periodRange: searchParams.periodStartDate,
|
||||||
|
flag: searchParams.flag
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
function getRowActions(row: Api.WorkReport.Project.ProjectReport): BusinessTableAction[] {
|
function getRowActions(row: Api.WorkReport.Project.ProjectReport): BusinessTableAction[] {
|
||||||
const actions: BusinessTableAction[] = [
|
const actions: BusinessTableAction[] = [
|
||||||
{
|
{
|
||||||
key: 'detail',
|
key: 'detail',
|
||||||
label: '详情',
|
label: '查看',
|
||||||
buttonType: 'primary',
|
buttonType: 'primary',
|
||||||
icon: ACTION_ICON_MAP.detail,
|
icon: ACTION_ICON_MAP.detail,
|
||||||
onClick: () => emit('detail', row)
|
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')) {
|
if (['draft', 'rejected'].includes(row.statusCode) && row.allowEdit && hasAuth('project:work-report:update')) {
|
||||||
actions.push({
|
actions.push({
|
||||||
key: 'edit',
|
key: 'edit',
|
||||||
@@ -162,6 +218,7 @@ function getRowActions(row: Api.WorkReport.Project.ProjectReport): BusinessTable
|
|||||||
|
|
||||||
async function reload(page?: number) {
|
async function reload(page?: number) {
|
||||||
await table.getDataByPage(page ?? searchParams.pageNo ?? 1);
|
await table.getDataByPage(page ?? searchParams.pageNo ?? 1);
|
||||||
|
await loadTeamSummary();
|
||||||
}
|
}
|
||||||
|
|
||||||
function resetSearchParams() {
|
function resetSearchParams() {
|
||||||
@@ -217,7 +274,10 @@ function handleSelectionChange(rows: Api.WorkReport.Project.ProjectReport[]) {
|
|||||||
|
|
||||||
function createExportSearchParams() {
|
function createExportSearchParams() {
|
||||||
const { pageNo: _pageNo, pageSize: _pageSize, ...params } = searchParams;
|
const { pageNo: _pageNo, pageSize: _pageSize, ...params } = searchParams;
|
||||||
return params;
|
return {
|
||||||
|
...params,
|
||||||
|
projectOwnerIds: currentProjectOwnerIds.value
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function exportReportContent(
|
async function exportReportContent(
|
||||||
@@ -275,6 +335,23 @@ async function handleExportCommand(command: 'selected' | 'all') {
|
|||||||
await handleExportAll();
|
await handleExportAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function loadTeamSummary() {
|
||||||
|
if (!isTeamRootSelected.value) {
|
||||||
|
teamSummaryLoading.value = false;
|
||||||
|
teamSummary.value = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
teamSummaryLoading.value = true;
|
||||||
|
const { error, data } = await fetchGetTeamReportSummary({
|
||||||
|
reportType: 'project',
|
||||||
|
periodKey: summaryPeriod.value.periodKey
|
||||||
|
});
|
||||||
|
teamSummaryLoading.value = false;
|
||||||
|
|
||||||
|
teamSummary.value = error || !data ? null : data;
|
||||||
|
}
|
||||||
|
|
||||||
defineExpose({ reload });
|
defineExpose({ reload });
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -292,11 +369,21 @@ defineExpose({ reload });
|
|||||||
@search="handleSearch"
|
@search="handleSearch"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<TeamReportSummary
|
||||||
|
v-if="isTeamRootSelected"
|
||||||
|
report-type="project"
|
||||||
|
:period-key="summaryPeriod.periodKey"
|
||||||
|
:period-label="formatPeriod(summaryPeriod)"
|
||||||
|
:loading="teamSummaryLoading"
|
||||||
|
:summary="teamSummary"
|
||||||
|
@reminded="loadTeamSummary"
|
||||||
|
/>
|
||||||
|
|
||||||
<ElCard class="flex-1-hidden card-wrapper" body-class="business-table-card-body">
|
<ElCard class="flex-1-hidden card-wrapper" body-class="business-table-card-body">
|
||||||
<template #header>
|
<template #header>
|
||||||
<div class="flex flex-wrap items-center justify-between gap-12px">
|
<div class="flex flex-wrap items-center justify-between gap-12px">
|
||||||
<div class="flex items-center gap-10px">
|
<div class="flex items-center gap-10px">
|
||||||
<p class="text-16px font-600">{{ WORK_REPORT_TYPE_LABEL.project }}</p>
|
<p class="text-16px font-600">{{ reportTitle }}</p>
|
||||||
<ElTag effect="plain">{{ table.mobilePagination.value.total || 0 }}</ElTag>
|
<ElTag effect="plain">{{ table.mobilePagination.value.total || 0 }}</ElTag>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -322,7 +409,13 @@ defineExpose({ reload });
|
|||||||
</ElDropdownMenu>
|
</ElDropdownMenu>
|
||||||
</template>
|
</template>
|
||||||
</ElDropdown>
|
</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>
|
<template #icon>
|
||||||
<icon-ic-round-plus class="text-icon" />
|
<icon-ic-round-plus class="text-icon" />
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -0,0 +1,217 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
import { fetchRemindTeamReport } from '@/service/api';
|
||||||
|
|
||||||
|
defineOptions({ name: 'TeamReportSummary' });
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
reportType: Api.WorkReport.Common.ReportType;
|
||||||
|
periodKey: string;
|
||||||
|
periodLabel?: string;
|
||||||
|
loading?: boolean;
|
||||||
|
summary?: Api.WorkReport.Common.TeamReportSummary | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
periodLabel: '',
|
||||||
|
loading: false,
|
||||||
|
summary: null
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
reminded: [];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const remindingAll = ref(false);
|
||||||
|
const remindingUserId = ref('');
|
||||||
|
|
||||||
|
const cards = computed(() => [
|
||||||
|
{ label: '应填人数', value: props.summary?.totalShouldSubmit ?? 0 },
|
||||||
|
{ label: '已提交', value: props.summary?.submittedCount ?? 0 },
|
||||||
|
{ label: '待提交', value: props.summary?.unsubmittedCount ?? 0, key: 'unsubmitted' as const },
|
||||||
|
{ label: '待审批', value: props.summary?.pendingApprovalCount ?? 0 }
|
||||||
|
]);
|
||||||
|
|
||||||
|
async function handleRemind(userIds?: string[]) {
|
||||||
|
const targetUserId = userIds?.length === 1 ? userIds[0] : '';
|
||||||
|
|
||||||
|
if (targetUserId) {
|
||||||
|
remindingUserId.value = targetUserId;
|
||||||
|
} else {
|
||||||
|
remindingAll.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { error, data } = await fetchRemindTeamReport({
|
||||||
|
reportType: props.reportType,
|
||||||
|
periodKey: props.periodKey,
|
||||||
|
userIds
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!targetUserId) {
|
||||||
|
remindingAll.value = false;
|
||||||
|
}
|
||||||
|
remindingUserId.value = '';
|
||||||
|
|
||||||
|
if (error) return;
|
||||||
|
|
||||||
|
window.$message?.success(`已催办 ${data?.remindedCount ?? 0} 人`);
|
||||||
|
emit('reminded');
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div v-loading="props.loading" class="team-report-summary">
|
||||||
|
<div v-if="props.periodLabel" class="team-report-summary__period">{{ props.periodLabel }}</div>
|
||||||
|
|
||||||
|
<div class="team-report-summary__grid">
|
||||||
|
<div v-for="card in cards" :key="card.label" class="team-report-summary__item">
|
||||||
|
<div class="team-report-summary__label">{{ card.label }}</div>
|
||||||
|
<div class="team-report-summary__value">
|
||||||
|
<template v-if="card.key === 'unsubmitted'">
|
||||||
|
<ElPopover placement="bottom" :width="300" trigger="hover">
|
||||||
|
<template #reference>
|
||||||
|
<button type="button" class="team-report-summary__link-button">{{ card.value }}</button>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div class="team-report-summary__popover">
|
||||||
|
<div class="team-report-summary__popover-title">未提交人员</div>
|
||||||
|
<div v-if="props.summary?.unsubmittedUsers?.length" class="team-report-summary__user-list">
|
||||||
|
<div
|
||||||
|
v-for="user in props.summary.unsubmittedUsers"
|
||||||
|
:key="user.userId"
|
||||||
|
class="team-report-summary__user-item"
|
||||||
|
>
|
||||||
|
<span class="team-report-summary__user-name">{{ user.userNickname }}</span>
|
||||||
|
<ElButton
|
||||||
|
link
|
||||||
|
type="primary"
|
||||||
|
:loading="remindingUserId === user.userId"
|
||||||
|
@click="handleRemind([user.userId])"
|
||||||
|
>
|
||||||
|
催办
|
||||||
|
</ElButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ElEmpty v-else :image-size="60" description="暂无待提交人员" />
|
||||||
|
|
||||||
|
<div class="team-report-summary__popover-footer">
|
||||||
|
<ElButton
|
||||||
|
size="small"
|
||||||
|
type="primary"
|
||||||
|
plain
|
||||||
|
:loading="remindingAll"
|
||||||
|
:disabled="!props.summary?.unsubmittedUsers?.length"
|
||||||
|
@click="handleRemind()"
|
||||||
|
>
|
||||||
|
一键催办全部
|
||||||
|
</ElButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ElPopover>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
{{ card.value }}
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.team-report-summary {
|
||||||
|
display: grid;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.team-report-summary__period {
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.team-report-summary__grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.team-report-summary__item {
|
||||||
|
display: grid;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 14px 16px;
|
||||||
|
border: 1px solid var(--el-border-color-light);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: var(--el-fill-color-blank);
|
||||||
|
}
|
||||||
|
|
||||||
|
.team-report-summary__label {
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.team-report-summary__value {
|
||||||
|
color: var(--el-text-color-primary);
|
||||||
|
font-size: 22px;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.team-report-summary__link-button {
|
||||||
|
padding: 0;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--el-color-primary);
|
||||||
|
font: inherit;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.team-report-summary__popover {
|
||||||
|
display: grid;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.team-report-summary__popover-title {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--el-text-color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.team-report-summary__user-list {
|
||||||
|
display: grid;
|
||||||
|
gap: 8px;
|
||||||
|
max-height: 240px;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.team-report-summary__user-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 8px 10px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: var(--el-fill-color-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.team-report-summary__user-name {
|
||||||
|
min-width: 0;
|
||||||
|
color: var(--el-text-color-regular);
|
||||||
|
}
|
||||||
|
|
||||||
|
.team-report-summary__popover-footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (width <= 1200px) {
|
||||||
|
.team-report-summary__grid {
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (width <= 768px) {
|
||||||
|
.team-report-summary__grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -41,6 +41,16 @@ export const WORK_REPORT_TYPE_LABEL: Record<WorkReportType, string> = {
|
|||||||
project: '项目半月报'
|
project: '项目半月报'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const TEAM_WORK_REPORT_TYPE_LABEL: Record<WorkReportType, string> = {
|
||||||
|
weekly: '团队周报',
|
||||||
|
monthly: '团队月报',
|
||||||
|
project: '团队项目半月报'
|
||||||
|
};
|
||||||
|
|
||||||
|
export function getWorkReportTypeDisplayLabel(reportType: WorkReportType, isTeamMode = false) {
|
||||||
|
return isTeamMode ? TEAM_WORK_REPORT_TYPE_LABEL[reportType] : WORK_REPORT_TYPE_LABEL[reportType];
|
||||||
|
}
|
||||||
|
|
||||||
export const WORK_REPORT_STATUS_LABEL: Record<string, string> = {
|
export const WORK_REPORT_STATUS_LABEL: Record<string, string> = {
|
||||||
draft: '待提交',
|
draft: '待提交',
|
||||||
pending_approval: '待审批',
|
pending_approval: '待审批',
|
||||||
|
|||||||
@@ -20,6 +20,14 @@ export interface WorkReportPeriodOption {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface WorkReportResolvedPeriod {
|
||||||
|
periodKey: string;
|
||||||
|
periodLabel: string;
|
||||||
|
periodStartDate: string;
|
||||||
|
periodEndDate: string;
|
||||||
|
flag?: number;
|
||||||
|
}
|
||||||
|
|
||||||
function formatRangeLabel(start: dayjs.Dayjs, end: dayjs.Dayjs) {
|
function formatRangeLabel(start: dayjs.Dayjs, end: dayjs.Dayjs) {
|
||||||
return `${start.format('YYYY-MM-DD')} 至 ${end.format('YYYY-MM-DD')}`;
|
return `${start.format('YYYY-MM-DD')} 至 ${end.format('YYYY-MM-DD')}`;
|
||||||
}
|
}
|
||||||
@@ -192,3 +200,72 @@ export function getReportTypePeriodOptions(now = dayjs()) {
|
|||||||
project: getProjectPeriodOptions(now)
|
project: getProjectPeriodOptions(now)
|
||||||
} as const;
|
} as const;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type PeriodRange = string[] | null | undefined;
|
||||||
|
|
||||||
|
interface ResolveWorkReportSummaryPeriodOptions {
|
||||||
|
currentRow?: Partial<WorkReportResolvedPeriod> | null;
|
||||||
|
periodRange?: PeriodRange;
|
||||||
|
flag?: number | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRangeReferenceDate(periodRange?: PeriodRange) {
|
||||||
|
const [, endDate] = periodRange || [];
|
||||||
|
return endDate || periodRange?.[0] || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function inferProjectSummaryFlag(referenceDate: string, explicitFlag?: number | null) {
|
||||||
|
if (explicitFlag === 1 || explicitFlag === 2) {
|
||||||
|
return explicitFlag;
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectedDate = dayjs(referenceDate);
|
||||||
|
if (selectedDate.isValid()) {
|
||||||
|
return selectedDate.date() > 15 ? 2 : 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return getProjectPeriodOptions()[0]?.flag || 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveWorkReportSummaryPeriod(
|
||||||
|
reportType: WorkReportType,
|
||||||
|
options: ResolveWorkReportSummaryPeriodOptions = {}
|
||||||
|
): WorkReportResolvedPeriod {
|
||||||
|
const { currentRow, periodRange, flag } = options;
|
||||||
|
|
||||||
|
if (currentRow?.periodKey && currentRow.periodStartDate && currentRow.periodEndDate) {
|
||||||
|
return {
|
||||||
|
periodKey: currentRow.periodKey,
|
||||||
|
periodLabel: currentRow.periodLabel || '',
|
||||||
|
periodStartDate: currentRow.periodStartDate,
|
||||||
|
periodEndDate: currentRow.periodEndDate,
|
||||||
|
flag: currentRow.flag
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const referenceDate = getRangeReferenceDate(periodRange);
|
||||||
|
const fallbackNow = dayjs();
|
||||||
|
|
||||||
|
if (reportType === 'weekly') {
|
||||||
|
return referenceDate ? buildWeeklyPeriodFromDate(referenceDate) : buildWeeklyPeriodFromDate(fallbackNow);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (reportType === 'monthly') {
|
||||||
|
return referenceDate ? buildMonthlyPeriodFromMonth(referenceDate) : buildMonthlyPeriodFromMonth(fallbackNow);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (referenceDate) {
|
||||||
|
const resolvedFlag = inferProjectSummaryFlag(referenceDate, flag);
|
||||||
|
return {
|
||||||
|
...buildProjectPeriodFromMonth(referenceDate, resolvedFlag),
|
||||||
|
flag: resolvedFlag
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const fallbackOption = getProjectPeriodOptions(fallbackNow)[0];
|
||||||
|
|
||||||
|
return {
|
||||||
|
...fallbackOption.period,
|
||||||
|
flag: fallbackOption.flag
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,18 +1,19 @@
|
|||||||
<script setup lang="tsx">
|
<script setup lang="tsx">
|
||||||
/* eslint-disable no-void */
|
/* 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 { ElMessageBox, ElTag, ElTooltip } from 'element-plus';
|
||||||
import {
|
import {
|
||||||
fetchDeleteWeeklyReport,
|
fetchDeleteWeeklyReport,
|
||||||
fetchExportWeeklyReportContent,
|
fetchExportWeeklyReportContent,
|
||||||
|
fetchGetTeamReportSummary,
|
||||||
fetchGetWeeklyReportPage,
|
fetchGetWeeklyReportPage,
|
||||||
fetchSubmitWeeklyReport
|
fetchSubmitWeeklyReport
|
||||||
} from '@/service/api';
|
} from '@/service/api';
|
||||||
import { useAuth } from '@/hooks/business/auth';
|
import { useAuth } from '@/hooks/business/auth';
|
||||||
import { useUIPaginatedTable } from '@/hooks/common/table';
|
import { useUIPaginatedTable } from '@/hooks/common/table';
|
||||||
import BusinessTableActionCell, { type BusinessTableAction } from '@/components/custom/business-table-action-cell';
|
import BusinessTableActionCell, { type BusinessTableAction } from '@/components/custom/business-table-action-cell';
|
||||||
|
import { type TeamViewContext, resolveTeamQueryUserIds } from '@/views/personal-center/shared/team-dashboard';
|
||||||
import {
|
import {
|
||||||
WORK_REPORT_TYPE_LABEL,
|
|
||||||
type WorkReportRow,
|
type WorkReportRow,
|
||||||
createWeeklySearchParams,
|
createWeeklySearchParams,
|
||||||
createWorkReportContentExportFallbackName,
|
createWorkReportContentExportFallbackName,
|
||||||
@@ -23,19 +24,27 @@ import {
|
|||||||
formatPeriodDateRange,
|
formatPeriodDateRange,
|
||||||
formatWeeklyPeriodLabel,
|
formatWeeklyPeriodLabel,
|
||||||
getWorkReportStatusLabel,
|
getWorkReportStatusLabel,
|
||||||
|
getWorkReportTypeDisplayLabel,
|
||||||
resolveExportFilename,
|
resolveExportFilename,
|
||||||
resolveWorkReportStatusTagType,
|
resolveWorkReportStatusTagType,
|
||||||
transformWorkReportPage
|
transformWorkReportPage
|
||||||
} from '../shared/types';
|
} 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 WeeklyReportSearch from './modules/search-panel.vue';
|
||||||
import IconMdiDeleteOutline from '~icons/mdi/delete-outline';
|
import IconMdiDeleteOutline from '~icons/mdi/delete-outline';
|
||||||
import IconMdiEyeOutline from '~icons/mdi/eye-outline';
|
import IconMdiEyeOutline from '~icons/mdi/eye-outline';
|
||||||
import IconMdiFileDocumentCheckOutline from '~icons/mdi/file-document-check-outline';
|
import IconMdiFileDocumentCheckOutline from '~icons/mdi/file-document-check-outline';
|
||||||
import IconMdiPencilOutline from '~icons/mdi/pencil-outline';
|
import IconMdiPencilOutline from '~icons/mdi/pencil-outline';
|
||||||
import IconMdiSendOutline from '~icons/mdi/send-outline';
|
import IconMdiSendOutline from '~icons/mdi/send-outline';
|
||||||
|
import IconMdiDownloadOutline from '~icons/mdi/download-outline';
|
||||||
|
|
||||||
defineOptions({ name: 'WeeklyWorkReportIndex' });
|
defineOptions({ name: 'WeeklyWorkReportIndex' });
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
teamContext?: TeamViewContext | null;
|
||||||
|
}>();
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: 'create'): void;
|
(e: 'create'): void;
|
||||||
(e: 'edit', row: WorkReportRow): void;
|
(e: 'edit', row: WorkReportRow): void;
|
||||||
@@ -47,21 +56,33 @@ const { hasAuth } = useAuth();
|
|||||||
const exporting = ref(false);
|
const exporting = ref(false);
|
||||||
const selectedRows = ref<Api.WorkReport.Weekly.WeeklyReport[]>([]);
|
const selectedRows = ref<Api.WorkReport.Weekly.WeeklyReport[]>([]);
|
||||||
const searchParams = reactive(createWeeklySearchParams());
|
const searchParams = reactive(createWeeklySearchParams());
|
||||||
|
const teamSummaryLoading = ref(false);
|
||||||
|
const teamSummary = ref<Api.WorkReport.Common.TeamReportSummary | null>(null);
|
||||||
|
|
||||||
const ACTION_ICON_MAP = {
|
const ACTION_ICON_MAP = {
|
||||||
detail: markRaw(IconMdiEyeOutline),
|
detail: markRaw(IconMdiEyeOutline),
|
||||||
edit: markRaw(IconMdiPencilOutline),
|
edit: markRaw(IconMdiPencilOutline),
|
||||||
submit: markRaw(IconMdiSendOutline),
|
submit: markRaw(IconMdiSendOutline),
|
||||||
delete: markRaw(IconMdiDeleteOutline),
|
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<
|
const table = useUIPaginatedTable<
|
||||||
Awaited<ReturnType<typeof fetchGetWeeklyReportPage>>,
|
Awaited<ReturnType<typeof fetchGetWeeklyReportPage>>,
|
||||||
Api.WorkReport.Weekly.WeeklyReport
|
Api.WorkReport.Weekly.WeeklyReport
|
||||||
>({
|
>({
|
||||||
paginationProps: { currentPage: searchParams.pageNo, pageSize: searchParams.pageSize },
|
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),
|
transform: response => transformWorkReportPage(response, searchParams.pageNo ?? 1, searchParams.pageSize ?? 10),
|
||||||
onPaginationParamsChange: params => {
|
onPaginationParamsChange: params => {
|
||||||
searchParams.pageNo = params.currentPage ?? 1;
|
searchParams.pageNo = params.currentPage ?? 1;
|
||||||
@@ -69,6 +90,7 @@ const table = useUIPaginatedTable<
|
|||||||
},
|
},
|
||||||
columns: () => [
|
columns: () => [
|
||||||
{ prop: 'index', type: 'index', label: '序号', width: 64 },
|
{ prop: 'index', type: 'index', label: '序号', width: 64 },
|
||||||
|
...(isTeamMode.value ? [{ prop: 'reporterName', label: '提交人', minWidth: 100, showOverflowTooltip: true }] : []),
|
||||||
{
|
{
|
||||||
prop: 'periodLabel',
|
prop: 'periodLabel',
|
||||||
label: '周期',
|
label: '周期',
|
||||||
@@ -122,7 +144,7 @@ const table = useUIPaginatedTable<
|
|||||||
{
|
{
|
||||||
prop: 'operate',
|
prop: 'operate',
|
||||||
label: '操作',
|
label: '操作',
|
||||||
width: 180,
|
width: isTeamMode.value ? 140 : 180,
|
||||||
align: 'center',
|
align: 'center',
|
||||||
fixed: 'right',
|
fixed: 'right',
|
||||||
formatter: row => <BusinessTableActionCell actions={getRowActions(row)} variant="icon" />
|
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[] {
|
function getRowActions(row: Api.WorkReport.Weekly.WeeklyReport): BusinessTableAction[] {
|
||||||
const actions: BusinessTableAction[] = [
|
const actions: BusinessTableAction[] = [
|
||||||
{
|
{
|
||||||
key: 'detail',
|
key: 'detail',
|
||||||
label: '详情',
|
label: '查看',
|
||||||
buttonType: 'primary',
|
buttonType: 'primary',
|
||||||
icon: ACTION_ICON_MAP.detail,
|
icon: ACTION_ICON_MAP.detail,
|
||||||
onClick: () => emit('detail', row)
|
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')) {
|
if (['draft', 'rejected'].includes(row.statusCode) && row.allowEdit && hasAuth('project:work-report:update')) {
|
||||||
actions.push({
|
actions.push({
|
||||||
key: 'edit',
|
key: 'edit',
|
||||||
@@ -183,6 +241,7 @@ function getRowActions(row: Api.WorkReport.Weekly.WeeklyReport): BusinessTableAc
|
|||||||
|
|
||||||
async function reload(page?: number) {
|
async function reload(page?: number) {
|
||||||
await table.getDataByPage(page ?? searchParams.pageNo ?? 1);
|
await table.getDataByPage(page ?? searchParams.pageNo ?? 1);
|
||||||
|
await loadTeamSummary();
|
||||||
}
|
}
|
||||||
|
|
||||||
function resetSearchParams() {
|
function resetSearchParams() {
|
||||||
@@ -238,7 +297,10 @@ function handleSelectionChange(rows: Api.WorkReport.Weekly.WeeklyReport[]) {
|
|||||||
|
|
||||||
function createExportSearchParams() {
|
function createExportSearchParams() {
|
||||||
const { pageNo: _pageNo, pageSize: _pageSize, ...params } = searchParams;
|
const { pageNo: _pageNo, pageSize: _pageSize, ...params } = searchParams;
|
||||||
return params;
|
return {
|
||||||
|
...params,
|
||||||
|
reporterIds: currentTeamReporterIds.value
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function exportReportContent(
|
async function exportReportContent(
|
||||||
@@ -296,6 +358,23 @@ async function handleExportCommand(command: 'selected' | 'all') {
|
|||||||
await handleExportAll();
|
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 });
|
defineExpose({ reload });
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -303,11 +382,21 @@ defineExpose({ reload });
|
|||||||
<div class="flex-col-stretch gap-16px overflow-hidden">
|
<div class="flex-col-stretch gap-16px overflow-hidden">
|
||||||
<WeeklyReportSearch v-model:model="searchParams" @reset="resetSearchParams" @search="handleSearch" />
|
<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">
|
<ElCard class="flex-1-hidden card-wrapper" body-class="business-table-card-body">
|
||||||
<template #header>
|
<template #header>
|
||||||
<div class="flex flex-wrap items-center justify-between gap-12px">
|
<div class="flex flex-wrap items-center justify-between gap-12px">
|
||||||
<div class="flex items-center gap-10px">
|
<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>
|
<ElTag effect="plain">{{ table.mobilePagination.value.total || 0 }}</ElTag>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -333,7 +422,13 @@ defineExpose({ reload });
|
|||||||
</ElDropdownMenu>
|
</ElDropdownMenu>
|
||||||
</template>
|
</template>
|
||||||
</ElDropdown>
|
</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>
|
<template #icon>
|
||||||
<icon-ic-round-plus class="text-icon" />
|
<icon-ic-round-plus class="text-icon" />
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
/* eslint-disable vue/no-mutating-props, unicorn/prefer-dom-node-text-content, no-useless-escape, no-nested-ternary */
|
/* 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 { Plus } from '@element-plus/icons-vue';
|
||||||
import { RDMS_REQ_PRIORITY_DICT_CODE, RDMS_TASK_ITEM_TYPE_DICT_CODE } from '@/constants/dict';
|
import { RDMS_REQ_PRIORITY_DICT_CODE, RDMS_TASK_ITEM_TYPE_DICT_CODE } from '@/constants/dict';
|
||||||
import { fetchGetMyParticipatedProjectPage } from '@/service/api';
|
import { fetchGetMyParticipatedProjectPage } from '@/service/api';
|
||||||
@@ -1065,6 +1065,34 @@ function blurEditField(key: string) {
|
|||||||
if (activeEditField.value === key) activeEditField.value = '';
|
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) {
|
function syncRichContent(item: ReviewItem, event: Event) {
|
||||||
const target = event.currentTarget as HTMLElement;
|
const target = event.currentTarget as HTMLElement;
|
||||||
if (!item.source) return;
|
if (!item.source) return;
|
||||||
@@ -1183,7 +1211,12 @@ function syncRichSupport(item: PlanItem, event: Event) {
|
|||||||
<div class="review-editor-grid">
|
<div class="review-editor-grid">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label>具体工作内容及成果描述</label>
|
<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
|
<div
|
||||||
v-for="(section, sectionIndex) in item.contentSections"
|
v-for="(section, sectionIndex) in item.contentSections"
|
||||||
:key="`${index}-${sectionIndex}`"
|
:key="`${index}-${sectionIndex}`"
|
||||||
@@ -1214,6 +1247,7 @@ function syncRichSupport(item: PlanItem, event: Event) {
|
|||||||
class="rich-editor"
|
class="rich-editor"
|
||||||
:contenteditable="!isReadonly"
|
:contenteditable="!isReadonly"
|
||||||
spellcheck="false"
|
spellcheck="false"
|
||||||
|
:data-field-key="`content-${index}`"
|
||||||
:data-placeholder="isReadonly ? undefined : '请输入具体工作内容及成果描述'"
|
:data-placeholder="isReadonly ? undefined : '请输入具体工作内容及成果描述'"
|
||||||
@focus="focusEditField(`content-${index}`)"
|
@focus="focusEditField(`content-${index}`)"
|
||||||
@blur="
|
@blur="
|
||||||
@@ -1282,7 +1316,12 @@ function syncRichSupport(item: PlanItem, event: Event) {
|
|||||||
<div class="plan-editor-grid">
|
<div class="plan-editor-grid">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label>具体目标</label>
|
<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
|
<div
|
||||||
v-for="(section, sectionIndex) in item.targetSections"
|
v-for="(section, sectionIndex) in item.targetSections"
|
||||||
:key="`${index}-${sectionIndex}`"
|
:key="`${index}-${sectionIndex}`"
|
||||||
@@ -1313,6 +1352,7 @@ function syncRichSupport(item: PlanItem, event: Event) {
|
|||||||
class="rich-editor"
|
class="rich-editor"
|
||||||
:contenteditable="!isReadonly"
|
:contenteditable="!isReadonly"
|
||||||
spellcheck="false"
|
spellcheck="false"
|
||||||
|
:data-field-key="`target-${index}`"
|
||||||
:data-placeholder="isReadonly ? undefined : '请输入具体目标'"
|
:data-placeholder="isReadonly ? undefined : '请输入具体目标'"
|
||||||
@focus="focusEditField(`target-${index}`)"
|
@focus="focusEditField(`target-${index}`)"
|
||||||
@blur="
|
@blur="
|
||||||
@@ -2107,6 +2147,15 @@ function syncRichSupport(item: PlanItem, event: Event) {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 编辑态下结构化预览区域:点击可切换到编辑模式 */
|
||||||
|
.rich-editor--preview {
|
||||||
|
cursor: text;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rich-editor--preview:hover {
|
||||||
|
border-color: #0f766e;
|
||||||
|
}
|
||||||
|
|
||||||
.structured-preview__popover {
|
.structured-preview__popover {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
color: #334155;
|
color: #334155;
|
||||||
|
|||||||
Reference in New Issue
Block a user