feat(我的绩效): 开发我的绩效功能。

fix(加班申请、工作报告): 重构加班申请在审批时的样式,工作报告在新增时的对话框、报告详情页的样式。
This commit is contained in:
dk
2026-06-21 18:22:44 +08:00
parent cd64cf42cc
commit 9a5845708d
35 changed files with 4211 additions and 924 deletions

View File

@@ -123,12 +123,8 @@ const table = useUIPaginatedTable<
]
});
const summaryPeriod = computed(() =>
resolveWorkReportSummaryPeriod('monthly', {
currentRow: table.data.value[0],
periodRange: searchParams.periodStartDate
})
);
// 团队统计始终使用当前周期(本月),不跟随列表第一条数据的周期
const summaryPeriod = computed(() => resolveWorkReportSummaryPeriod('monthly'));
function getRowActions(row: Api.WorkReport.Monthly.MonthlyReport): BusinessTableAction[] {
const actions: BusinessTableAction[] = [

View File

@@ -801,7 +801,7 @@ watch(
<div class="form-actions approval-form-actions">
<ElButton @click="emit('back')">退出审批</ElButton>
<ElButton type="primary" class="btn-submit" @click="openAuditDialog">开始审批</ElButton>
<ElButton type="primary" @click="openAuditDialog">开始审批</ElButton>
</div>
<BusinessFormDialog
@@ -915,8 +915,8 @@ watch(
place-items: center;
flex-shrink: 0;
border-radius: 16px;
background: #ccfbf1;
color: #0f766e;
background: var(--el-color-primary-light-8);
color: var(--el-color-primary);
font-weight: 900;
}
@@ -1009,12 +1009,12 @@ watch(
}
.radio-group-full :deep(.el-radio.is-checked) {
border-color: #0f766e;
background: #f0fdfa;
border-color: var(--el-color-primary);
background: var(--el-color-primary-light-9);
}
.radio-group-full :deep(.el-radio__input.is-checked + .el-radio__label) {
color: #0f766e;
color: var(--el-color-primary);
}
.review-grid,
@@ -1123,7 +1123,7 @@ watch(
display: grid;
place-items: center;
border-radius: 999px;
background: #0f766e;
background: var(--el-color-primary);
color: #fff;
font-size: 13px;
font-weight: 900;
@@ -1292,7 +1292,7 @@ watch(
position: relative;
min-width: 0;
padding-left: 14px;
color: #0f766e;
color: var(--el-color-primary);
font-size: 13px;
font-weight: 800;
white-space: normal;
@@ -1308,10 +1308,10 @@ watch(
width: 4px;
height: 16px;
border-radius: 999px;
background: #0f766e;
background: var(--el-color-primary);
}
.rich-editor :deep(.rich-section-tasks) {
.rich-editor :deep(.rich-section-task) {
display: grid;
gap: 6px;
padding-left: 14px;
@@ -1324,7 +1324,7 @@ watch(
}
.rich-editor :deep(.rich-category-line) {
color: #0f766e;
color: var(--el-color-primary);
font-size: 13px;
font-weight: 700;
line-height: 1.6;
@@ -1454,7 +1454,7 @@ watch(
.structured-section-title {
position: relative;
padding-left: 14px;
color: #0f766e;
color: var(--el-color-primary);
font-size: 13px;
font-weight: 800;
}
@@ -1467,7 +1467,7 @@ watch(
width: 4px;
height: 16px;
border-radius: 999px;
background: #0f766e;
background: var(--el-color-primary);
}
.structured-section-tasks {
@@ -1551,14 +1551,14 @@ watch(
}
.conclusion-btn:hover {
border-color: #0f766e;
color: #0f766e;
border-color: var(--el-color-primary);
color: var(--el-color-primary);
}
.conclusion-btn.active.pass {
border-color: #0f766e;
background: #f0fdfa;
color: #0f766e;
border-color: var(--el-color-primary);
background: var(--el-color-primary-light-9);
color: var(--el-color-primary);
}
.conclusion-btn.active.reject {
@@ -1689,9 +1689,9 @@ watch(
}
.feedback-tag.success {
background: #ecfdf5;
color: #059669;
border: 1px solid #d1fae5;
background: var(--el-color-primary-light-9);
color: var(--el-color-primary);
border: 1px solid var(--el-color-primary-light-7);
}
.feedback-tag.warning {
@@ -1838,16 +1838,6 @@ watch(
box-shadow: 0 -8px 18px rgba(15, 23, 42, 0.06);
}
.btn-submit {
background: #0f766e !important;
border-color: #0f766e !important;
}
.btn-submit:hover {
background: #0d9488 !important;
border-color: #0d9488 !important;
}
@media (max-width: 1180px) {
.form-head,
.compose-grid,

View File

@@ -1077,7 +1077,6 @@ function syncRichSupport(item: PlanItem, event: Event) {
<ElButton
size="small"
type="primary"
class="btn-submit"
:disabled="
!planForm.workItem.trim() ||
!planForm.sections.some(section => section.category.trim() && section.tasks.length)
@@ -1094,7 +1093,7 @@ function syncRichSupport(item: PlanItem, event: Event) {
<div v-if="!isReadonly" class="form-actions">
<!-- <ElButton>重置表单</ElButton>-->
<ElButton @click="emit('save')">保存草稿</ElButton>
<ElButton type="primary" class="btn-submit" @click="emit('submit')">提交审批</ElButton>
<ElButton type="primary" @click="emit('submit')">提交审批</ElButton>
</div>
<BusinessFormDialog
@@ -1327,8 +1326,8 @@ function syncRichSupport(item: PlanItem, event: Event) {
place-items: center;
flex-shrink: 0;
border-radius: 16px;
background: #ccfbf1;
color: #0f766e;
background: var(--el-color-primary-light-8);
color: var(--el-color-primary);
font-weight: 900;
}
@@ -1421,12 +1420,12 @@ function syncRichSupport(item: PlanItem, event: Event) {
}
.radio-group-full :deep(.el-radio.is-checked) {
border-color: #0f766e;
background: #f0fdfa;
border-color: var(--el-color-primary);
background: var(--el-color-primary-light-9);
}
.radio-group-full :deep(.el-radio__input.is-checked + .el-radio__label) {
color: #0f766e;
color: var(--el-color-primary);
}
.review-grid,
@@ -1546,7 +1545,7 @@ function syncRichSupport(item: PlanItem, event: Event) {
display: grid;
place-items: center;
border-radius: 999px;
background: #0f766e;
background: var(--el-color-primary);
color: #fff;
font-size: 13px;
font-weight: 900;
@@ -1744,8 +1743,8 @@ function syncRichSupport(item: PlanItem, event: Event) {
}
.rich-editor:focus {
border-color: #0f766e;
box-shadow: 0 0 0 2px rgba(15, 118, 110, 0.1);
border-color: var(--el-color-primary);
box-shadow: 0 0 0 2px var(--el-color-primary-light-8);
}
.rich-editor:empty::before {
@@ -1769,7 +1768,7 @@ function syncRichSupport(item: PlanItem, event: Event) {
position: relative;
min-width: 0;
padding-left: 14px;
color: #0f766e;
color: var(--el-color-primary);
font-size: 13px;
font-weight: 800;
white-space: normal;
@@ -1785,10 +1784,10 @@ function syncRichSupport(item: PlanItem, event: Event) {
width: 4px;
height: 16px;
border-radius: 999px;
background: #0f766e;
background: var(--el-color-primary);
}
.rich-editor :deep(.rich-section-tasks) {
.rich-editor :deep(.rich-section-task) {
display: grid;
gap: 6px;
padding-left: 14px;
@@ -1828,7 +1827,7 @@ function syncRichSupport(item: PlanItem, event: Event) {
}
.rich-editor :deep(.rich-category-line) {
color: #0f766e;
color: var(--el-color-primary);
font-size: 13px;
font-weight: 700;
line-height: 1.6;
@@ -1899,7 +1898,7 @@ function syncRichSupport(item: PlanItem, event: Event) {
.structured-section-title {
position: relative;
padding-left: 14px;
color: #0f766e;
color: var(--el-color-primary);
font-size: 13px;
font-weight: 800;
}
@@ -1912,7 +1911,7 @@ function syncRichSupport(item: PlanItem, event: Event) {
width: 4px;
height: 16px;
border-radius: 999px;
background: #0f766e;
background: var(--el-color-primary);
}
.structured-task {
@@ -1928,7 +1927,7 @@ function syncRichSupport(item: PlanItem, event: Event) {
.structured-task-category {
position: relative;
padding-left: 14px;
color: #0f766e;
color: var(--el-color-primary);
font-size: 13px;
font-weight: 800;
}
@@ -1941,7 +1940,7 @@ function syncRichSupport(item: PlanItem, event: Event) {
width: 4px;
height: 16px;
border-radius: 999px;
background: #0f766e;
background: var(--el-color-primary);
}
.structured-task-title {
@@ -1988,9 +1987,9 @@ function syncRichSupport(item: PlanItem, event: Event) {
}
.plan-section.active {
border-color: #cfe3e0;
background: #f7fbfa;
box-shadow: inset 0 0 0 1px rgba(15, 118, 110, 0.06);
border-color: var(--el-color-primary-light-7);
background: var(--el-color-primary-light-9);
box-shadow: inset 0 0 0 1px var(--el-color-primary-light-9);
}
.plan-section-head {
@@ -2124,7 +2123,7 @@ function syncRichSupport(item: PlanItem, event: Event) {
}
.inline-plan-card {
border-color: rgba(15, 118, 110, 0.42);
border-color: var(--el-color-primary-light-5);
background: #f8fbfc;
}
@@ -2171,14 +2170,14 @@ function syncRichSupport(item: PlanItem, event: Event) {
.inline-plan-card :deep(.el-input__wrapper.is-focus),
.inline-plan-card :deep(.el-select__wrapper.is-focused) {
box-shadow:
0 0 0 1px #0f766e inset,
0 0 0 2px rgba(15, 118, 110, 0.1);
0 0 0 1px var(--el-color-primary) inset,
0 0 0 2px var(--el-color-primary-light-8);
}
.inline-plan-card :deep(.el-textarea__inner:focus) {
box-shadow:
0 0 0 1px #0f766e inset,
0 0 0 2px rgba(15, 118, 110, 0.1);
0 0 0 1px var(--el-color-primary) inset,
0 0 0 2px var(--el-color-primary-light-8);
}
.inline-plan-card .inline-task-row :deep(.el-select__selected-item),
@@ -2216,8 +2215,8 @@ function syncRichSupport(item: PlanItem, event: Event) {
.inline-plan-card .inline-task-row :deep(.el-input-number:focus-within .el-input__wrapper) {
overflow: hidden;
box-shadow:
0 0 0 1px #0f766e inset,
0 0 0 2px rgba(15, 118, 110, 0.1) !important;
0 0 0 1px var(--el-color-primary) inset,
0 0 0 2px var(--el-color-primary-light-8) !important;
}
.form-actions {
@@ -2237,16 +2236,6 @@ function syncRichSupport(item: PlanItem, event: Event) {
box-shadow: 0 -8px 18px rgba(15, 23, 42, 0.06);
}
.btn-submit {
background: #0f766e !important;
border-color: #0f766e !important;
}
.btn-submit:hover {
background: #0d9488 !important;
border-color: #0d9488 !important;
}
@media (max-width: 1180px) {
.form-head,
.compose-grid,

View File

@@ -128,13 +128,8 @@ const table = useUIPaginatedTable<
]
});
const summaryPeriod = computed(() =>
resolveWorkReportSummaryPeriod('project', {
currentRow: table.data.value[0],
periodRange: searchParams.periodStartDate,
flag: searchParams.flag
})
);
// 团队统计始终使用当前周期(当前半月),不跟随列表第一条数据的周期
const summaryPeriod = computed(() => resolveWorkReportSummaryPeriod('project'));
function getRowActions(row: Api.WorkReport.Project.ProjectReport): BusinessTableAction[] {
const actions: BusinessTableAction[] = [

View File

@@ -474,7 +474,6 @@ function notifyTitleSaved(item: WorkItem) {
<ElButton
size="small"
type="primary"
class="btn-submit"
:disabled="!planForm.title.trim() || isDuplicatePlanTitle"
@click="submitInlinePlan"
>
@@ -520,11 +519,11 @@ function notifyTitleSaved(item: WorkItem) {
<div v-if="!isReadonly" class="form-actions">
<!-- <ElButton>重置表单</ElButton>-->
<ElButton @click="emit('save')">保存草稿</ElButton>
<ElButton type="primary" class="btn-submit" @click="emit('submit')">提交审批</ElButton>
<ElButton type="primary" @click="emit('submit')">提交审批</ElButton>
</div>
<div v-else-if="scene === 'approval'" class="form-actions approval-form-actions">
<ElButton @click="emit('back')">退出审批</ElButton>
<ElButton type="primary" class="btn-submit" @click="emit('requestApprove')">开始审批</ElButton>
<ElButton type="primary" @click="emit('requestApprove')">开始审批</ElButton>
</div>
</div>
</template>
@@ -567,8 +566,8 @@ function notifyTitleSaved(item: WorkItem) {
place-items: center;
flex-shrink: 0;
border-radius: 16px;
background: #ccfbf1;
color: #0f766e;
background: var(--el-color-primary-light-8);
color: var(--el-color-primary);
font-weight: 900;
}
@@ -671,8 +670,8 @@ function notifyTitleSaved(item: WorkItem) {
}
.member-chip.more {
background: #f0fdfa;
color: #0f766e;
background: var(--el-color-primary-light-9);
color: var(--el-color-primary);
cursor: default;
}
@@ -715,7 +714,7 @@ function notifyTitleSaved(item: WorkItem) {
}
.compact-work-card:hover {
border-color: rgba(15, 118, 110, 0.45);
border-color: var(--el-color-primary-light-5);
box-shadow: 0 8px 22px rgba(15, 23, 42, 0.08);
transform: translateY(-1px);
}
@@ -742,7 +741,7 @@ function notifyTitleSaved(item: WorkItem) {
display: grid;
place-items: center;
border-radius: 999px;
background: #0f766e;
background: var(--el-color-primary);
color: #fff;
font-size: 13px;
font-weight: 900;
@@ -806,7 +805,7 @@ function notifyTitleSaved(item: WorkItem) {
}
.work-title-input:focus {
border-bottom-color: #0f766e;
border-bottom-color: var(--el-color-primary);
}
.work-title-line span {
@@ -908,8 +907,8 @@ function notifyTitleSaved(item: WorkItem) {
.rich-editor:focus {
box-shadow:
0 0 0 1px #0f766e inset,
0 0 0 2px rgba(15, 118, 110, 0.1);
0 0 0 1px var(--el-color-primary) inset,
0 0 0 2px var(--el-color-primary-light-8);
}
.rich-editor:empty::before {
@@ -938,7 +937,7 @@ function notifyTitleSaved(item: WorkItem) {
}
.inline-plan-card {
border-color: rgba(15, 118, 110, 0.42);
border-color: var(--el-color-primary-light-5);
background: #f8fbfc;
}
@@ -1001,8 +1000,8 @@ function notifyTitleSaved(item: WorkItem) {
.inline-plan-card :deep(.el-input__wrapper.is-focus),
.inline-plan-card :deep(.el-select__wrapper.is-focused) {
box-shadow:
0 0 0 1px #0f766e inset,
0 0 0 2px rgba(15, 118, 110, 0.1);
0 0 0 1px var(--el-color-primary) inset,
0 0 0 2px var(--el-color-primary-light-8);
}
.form-actions {
@@ -1034,16 +1033,6 @@ function notifyTitleSaved(item: WorkItem) {
box-shadow: 0 -8px 18px rgba(15, 23, 42, 0.06);
}
.btn-submit {
background: #0f766e !important;
border-color: #0f766e !important;
}
.btn-submit:hover {
background: #0d9488 !important;
border-color: #0d9488 !important;
}
@media (max-width: 1180px) {
.compose-grid,
.review-grid,

View File

@@ -385,14 +385,14 @@ async function handleSubmit() {
}
.conclusion-btn:hover {
border-color: #0f766e;
color: #0f766e;
border-color: var(--el-color-primary);
color: var(--el-color-primary);
}
.conclusion-btn.active.pass {
border-color: #0f766e;
background: #f0fdfa;
color: #0f766e;
border-color: var(--el-color-primary);
background: var(--el-color-primary-light-9);
color: var(--el-color-primary);
}
.conclusion-btn.active.reject {

View File

@@ -1,6 +1,8 @@
<script setup lang="ts">
import { computed, ref, watch } from 'vue';
import dayjs from 'dayjs';
import { Calendar } from '@element-plus/icons-vue';
import isoWeek from 'dayjs/plugin/isoWeek';
import BusinessFormDialog from '@/components/custom/business-form-dialog.vue';
import {
type WorkReportPeriodOption,
@@ -12,6 +14,8 @@ import {
} from '../utils';
import { WORK_REPORT_TYPE_LABEL, type WorkReportType } from '../types';
dayjs.extend(isoWeek);
defineOptions({ name: 'WorkReportCreateDialog' });
interface Props {
@@ -44,11 +48,24 @@ const emit = defineEmits<{
const selectedPeriodKey = ref('');
const selectedProjectId = ref('');
const customWeekDate = ref('');
const rawCustomWeekDate = ref('');
const customMonth = ref('');
const customProjectMonth = ref('');
const customProjectFlag = ref(1);
// 自定义周报周期:无论用户点哪一天,都归一到该 ISO 周的周一,
// 这样 ElDatePicker 使用内置 dayjs 的 wwlocale week也能正确显示 ISO 周数。
const customWeekDate = computed<string>({
get: () => rawCustomWeekDate.value,
set: val => {
if (!val) {
rawCustomWeekDate.value = val;
return;
}
rawCustomWeekDate.value = dayjs(val).startOf('isoWeek').format('YYYY-MM-DD');
}
});
const selectedReportType = computed<WorkReportType>(() => {
if (props.defaultReportType === 'project' && !props.projectVisible) return 'weekly';
return props.defaultReportType;
@@ -157,110 +174,113 @@ function handleConfirm() {
:close-on-click-modal="false"
@confirm="handleConfirm"
>
<div v-if="selectedReportType === 'project'" class="work-report-create-dialog__project-select">
<label class="work-report-create-dialog__label">项目</label>
<ElSelect v-model="selectedProjectId" class="w-full" placeholder="请选择项目" filterable>
<ElOption
v-for="item in props.projectOptions"
:key="item.id"
:label="item.projectCode ? `${item.projectName}${item.projectCode}` : item.projectName"
:value="item.id"
/>
</ElSelect>
</div>
<div class="work-report-create-dialog__section">
<div class="work-report-create-dialog__grid is-period">
<button
v-for="item in activePeriodOptions"
:key="item.key"
type="button"
class="work-report-create-dialog__choice"
:class="{ 'is-active': selectedPeriodKey === item.key }"
@click="selectedPeriodKey = item.key"
>
<div class="work-report-create-dialog__choice-title">{{ item.label }}</div>
<div class="work-report-create-dialog__choice-desc">{{ item.description }}</div>
</button>
<button
type="button"
class="work-report-create-dialog__choice"
:class="{ 'is-active': selectedPeriodKey === 'custom' }"
@click="selectedPeriodKey = 'custom'"
>
<div class="work-report-create-dialog__choice-title">自定义周期</div>
<div class="work-report-create-dialog__choice-desc">
{{
selectedReportType === 'weekly'
? '选择某一周作为周报周期。'
: selectedReportType === 'monthly'
? '选择某一月作为月报周期。'
: '选择某个月的上半月或下半月。'
}}
</div>
</button>
<!-- 必须用单根节点包裹ElScrollbar 默认 slot 接收多根 Fragment destroy-on-close patch 会触发 null __vnode 错误 -->
<div class="work-report-create-dialog__body">
<div v-if="selectedReportType === 'project'" class="work-report-create-dialog__project-select">
<label class="work-report-create-dialog__label">项目</label>
<ElSelect v-model="selectedProjectId" class="w-full" placeholder="请选择项目" filterable>
<ElOption
v-for="item in props.projectOptions"
:key="item.id"
:label="item.projectCode ? `${item.projectName}${item.projectCode}` : item.projectName"
:value="item.id"
/>
</ElSelect>
</div>
<div v-if="selectedPeriodKey === 'custom'" class="work-report-create-dialog__custom-period">
<div v-if="selectedReportType === 'weekly'" class="work-report-create-dialog__custom-row">
<div class="work-report-create-dialog__field work-report-create-dialog__field--inline">
<label class="work-report-create-dialog__label">周报周期</label>
<ElDatePicker
v-model="customWeekDate"
type="date"
format="YYYY[年第]ww[周]"
value-format="YYYY-MM-DD"
popper-class="work-report-create-date-popper"
placeholder="请选择周报周期"
/>
<div v-if="customPeriodPreviewLabel" class="work-report-create-dialog__period-preview">
{{ customPeriodPreviewLabel }}
<div class="work-report-create-dialog__section">
<div class="work-report-create-dialog__grid is-period">
<button
v-for="item in activePeriodOptions"
:key="item.key"
type="button"
class="work-report-create-dialog__choice"
:class="{ 'is-active': selectedPeriodKey === item.key }"
@click="selectedPeriodKey = item.key"
>
<div class="work-report-create-dialog__choice-title">{{ item.label }}</div>
<div class="work-report-create-dialog__choice-desc">{{ item.description }}</div>
</button>
<button
type="button"
class="work-report-create-dialog__choice"
:class="{ 'is-active': selectedPeriodKey === 'custom' }"
@click="selectedPeriodKey = 'custom'"
>
<div class="work-report-create-dialog__choice-title">自定义周期</div>
<div class="work-report-create-dialog__choice-desc">
{{
selectedReportType === 'weekly'
? '选择某一周作为周报周期。'
: selectedReportType === 'monthly'
? '选择某一月作为月报周期。'
: '选择某个月的上半月或下半月。'
}}
</div>
</div>
</button>
</div>
<div v-else-if="selectedReportType === 'monthly'" class="work-report-create-dialog__custom-row">
<div class="work-report-create-dialog__field work-report-create-dialog__field--inline">
<label class="work-report-create-dialog__label">月报周期</label>
<ElDatePicker
v-model="customMonth"
type="month"
value-format="YYYY-MM"
popper-class="work-report-create-date-popper"
placeholder="请选择月份"
/>
<div v-if="customPeriodPreviewLabel" class="work-report-create-dialog__period-preview">
{{ customPeriodPreviewLabel }}
</div>
</div>
</div>
<div v-else class="work-report-create-dialog__custom-project">
<div class="work-report-create-dialog__custom-project-grid">
<div class="work-report-create-dialog__custom-project-item">
<div class="work-report-create-dialog__custom-project-item-label">选择月份</div>
<div v-if="selectedPeriodKey === 'custom'" class="work-report-create-dialog__custom-period">
<div v-if="selectedReportType === 'weekly'" class="work-report-create-dialog__custom-row">
<div class="work-report-create-dialog__field work-report-create-dialog__field--inline">
<label class="work-report-create-dialog__label">周报周期</label>
<ElDatePicker
v-model="customProjectMonth"
class="w-full"
v-model="customWeekDate"
type="date"
format="YYYY[年第]ww[周]"
value-format="YYYY-MM-DD"
popper-class="work-report-create-date-popper"
placeholder="请选择周报周期"
/>
<div v-if="customPeriodPreviewLabel" class="work-report-create-dialog__period-preview">
{{ customPeriodPreviewLabel }}
</div>
</div>
</div>
<div v-else-if="selectedReportType === 'monthly'" class="work-report-create-dialog__custom-row">
<div class="work-report-create-dialog__field work-report-create-dialog__field--inline">
<label class="work-report-create-dialog__label">月报周期</label>
<ElDatePicker
v-model="customMonth"
type="month"
value-format="YYYY-MM"
popper-class="work-report-create-date-popper"
placeholder="请选择月份"
/>
</div>
<div class="work-report-create-dialog__custom-project-item">
<div class="work-report-create-dialog__custom-project-item-label">选择半月</div>
<ElSegmented
v-model="customProjectFlag"
:options="projectHalfOptions"
class="work-report-create-dialog__half-segmented"
/>
<div v-if="customPeriodPreviewLabel" class="work-report-create-dialog__period-preview">
{{ customPeriodPreviewLabel }}
</div>
</div>
</div>
<div v-if="customPeriodPreviewLabel" class="work-report-create-dialog__period-preview">
<ElIcon class="work-report-create-dialog__period-preview-icon"><Calendar /></ElIcon>
<span class="work-report-create-dialog__period-preview-text">已选周期</span>
<span class="work-report-create-dialog__period-preview-value">{{ customPeriodPreviewLabel }}</span>
<div v-else class="work-report-create-dialog__custom-project">
<div class="work-report-create-dialog__custom-project-grid">
<div class="work-report-create-dialog__custom-project-item">
<div class="work-report-create-dialog__custom-project-item-label">选择月份</div>
<ElDatePicker
v-model="customProjectMonth"
class="w-full"
type="month"
value-format="YYYY-MM"
popper-class="work-report-create-date-popper"
placeholder="请选择月份"
/>
</div>
<div class="work-report-create-dialog__custom-project-item">
<div class="work-report-create-dialog__custom-project-item-label">选择半月</div>
<ElSegmented
v-model="customProjectFlag"
:options="projectHalfOptions"
class="work-report-create-dialog__half-segmented"
/>
</div>
</div>
<div v-if="customPeriodPreviewLabel" class="work-report-create-dialog__period-preview">
<ElIcon class="work-report-create-dialog__period-preview-icon"><Calendar /></ElIcon>
<span class="work-report-create-dialog__period-preview-text">已选周期</span>
<span class="work-report-create-dialog__period-preview-value">{{ customPeriodPreviewLabel }}</span>
</div>
</div>
</div>
</div>
@@ -320,13 +340,13 @@ function handleConfirm() {
}
.work-report-create-dialog__choice:hover {
border-color: rgba(15, 118, 110, 0.28);
border-color: var(--el-color-primary-light-3);
box-shadow: 0 8px 20px rgba(15, 23, 42, 0.06);
}
.work-report-create-dialog__choice.is-active {
border-color: #0f766e;
background: #ecfdf5;
border-color: var(--el-color-primary);
background: var(--el-color-primary-light-9);
}
.work-report-create-dialog__choice-title {
@@ -381,9 +401,9 @@ function handleConfirm() {
.work-report-create-dialog__custom-period {
margin-top: 14px;
padding: 16px;
border: 1px solid rgba(15, 118, 110, 0.18);
border: 1px solid var(--el-color-primary-light-7);
border-radius: 14px;
background: linear-gradient(180deg, #f8fffd 0%, #ffffff 100%);
background: linear-gradient(180deg, var(--el-color-primary-light-9) 0%, #ffffff 100%);
box-shadow: 0 8px 24px rgba(15, 23, 42, 0.06);
}
@@ -422,7 +442,7 @@ function handleConfirm() {
}
.work-report-create-dialog__custom-project-item:hover {
border-color: rgba(15, 118, 110, 0.4);
border-color: var(--el-color-primary-light-5);
}
.work-report-create-dialog__custom-project-item-label {
@@ -460,10 +480,10 @@ function handleConfirm() {
gap: 6px;
min-height: 32px;
padding: 0 14px;
border: 1px solid rgba(15, 118, 110, 0.18);
border: 1px solid var(--el-color-primary-light-7);
border-radius: 999px;
background: #ecfdf5;
color: #0f766e;
background: var(--el-color-primary-light-9);
color: var(--el-color-primary);
font-size: 13px;
font-weight: 700;
white-space: nowrap;
@@ -472,7 +492,7 @@ function handleConfirm() {
.work-report-create-dialog__period-preview-icon {
font-size: 14px;
color: #0f766e;
color: var(--el-color-primary);
}
.work-report-create-dialog__period-preview-text {
@@ -481,7 +501,7 @@ function handleConfirm() {
}
.work-report-create-dialog__period-preview-value {
color: #0f766e;
color: var(--el-color-primary);
font-weight: 800;
}
@@ -529,6 +549,6 @@ function handleConfirm() {
:global(.work-report-create-date-popper .el-date-table td.current:not(.disabled) .el-date-table-cell__text),
:global(.work-report-create-date-popper .el-month-table td.current:not(.disabled) .cell) {
background-color: #0f766e;
background-color: var(--el-color-primary);
}
</style>

View File

@@ -16,6 +16,7 @@ import {
fetchInitMonthlyReport,
fetchInitProjectReport,
fetchInitWeeklyReport,
fetchPerformanceMonthlyResult,
fetchPreviewMonthlyReportDefaultDraft,
fetchPreviewProjectReportDefaultDraft,
fetchPreviewWeeklyReportDefaultDraft,
@@ -111,6 +112,7 @@ const monthlyApprovalDraft = reactive<Api.WorkReport.Monthly.MonthlyReportApprov
supervisorSignName: '',
supervisorSignedDate: ''
});
const monthlyPerformanceAutoFilled = ref(false);
const weeklyModel = reactive<Api.WorkReport.Weekly.WeeklyReportSaveParams>(createWeeklySaveParams());
const monthlyModel = reactive<Api.WorkReport.Monthly.MonthlyReportSaveParams>(createMonthlySaveParams());
@@ -167,6 +169,7 @@ function resetModels() {
supervisorSignName: '',
supervisorSignedDate: ''
});
monthlyPerformanceAutoFilled.value = false;
}
function patchMonthlyApprovalDefaults(report?: Partial<Api.WorkReport.Monthly.MonthlyReport> | null) {
@@ -471,9 +474,19 @@ watch(visible, async isVisible => {
if (props.rowData?.id) {
await loadDetail(props.rowData.id);
await fillMonthlyPerformanceResult();
}
});
watch(
() => currentStage.value,
stage => {
if (stage === 'approval') {
fillMonthlyPerformanceResult();
}
}
);
function hasTextValue(value: unknown) {
const text = String(value ?? '')
.replace(/<[^>]*>/g, '')
@@ -706,6 +719,45 @@ function handleBack() {
function handleViewApproval() {
currentStage.value = 'approval';
fillMonthlyPerformanceResult();
}
function resolveMonthlyPeriodMonth() {
const startDate = monthlyModel.periodStartDate || baseInfo.value?.periodStartDate;
const fromStartDate = dayjs(startDate);
if (fromStartDate.isValid()) {
return fromStartDate.format('YYYY-MM');
}
const periodKey = monthlyModel.periodKey || baseInfo.value?.periodKey;
const periodKeyMatch = String(periodKey || '').match(/(\d{4})[-/年]?(\d{1,2})/u);
if (periodKeyMatch) {
return `${periodKeyMatch[1]}-${periodKeyMatch[2].padStart(2, '0')}`;
}
const periodLabel = monthlyModel.periodLabel || baseInfo.value?.periodLabel;
const periodLabelMatch = String(periodLabel || '').match(/(\d{4})[-/年]?(\d{1,2})/u);
if (periodLabelMatch) {
return `${periodLabelMatch[1]}-${periodLabelMatch[2].padStart(2, '0')}`;
}
return '';
}
async function fillMonthlyPerformanceResult() {
if (props.reportType !== 'monthly' || monthlyPerformanceAutoFilled.value) return;
if (monthlyApprovalDraft.performanceResult?.trim()) return;
const employeeId = (baseInfo.value as Api.WorkReport.Monthly.MonthlyReport | null)?.reporterId;
const periodMonth = resolveMonthlyPeriodMonth();
if (!employeeId || !periodMonth) return;
monthlyPerformanceAutoFilled.value = true;
const { error, data } = await fetchPerformanceMonthlyResult(employeeId, periodMonth);
if (error || !data?.actualScoreTotal) return;
if (monthlyApprovalDraft.performanceResult?.trim()) return;
monthlyApprovalDraft.performanceResult = String(data.actualScoreTotal);
}
function handleRequestApprove() {

View File

@@ -38,10 +38,12 @@ export function formatPeriodDisplayLabel(label?: string | null) {
.replace(/\s*(|||)\s*$/u, '');
}
// 使用 ISO 周数,确保与 buildWeeklyPeriodFromDate 的计算一致
export function getIsoWeekDisplay(date: string | dayjs.Dayjs) {
const selectedDate = dayjs(date);
if (!selectedDate.isValid()) return '';
return `${selectedDate.format('YYYY')}${String(selectedDate.isoWeek()).padStart(2, '0')}`;
// isoWeek() 返回 ISO 8601 周数(周一为一周起始)
return `${selectedDate.format('GGGG')}${String(selectedDate.isoWeek()).padStart(2, '0')}`;
}
/* eslint-disable-next-line max-params */

View File

@@ -152,12 +152,8 @@ const table = useUIPaginatedTable<
]
});
const summaryPeriod = computed(() =>
resolveWorkReportSummaryPeriod('weekly', {
currentRow: table.data.value[0],
periodRange: searchParams.periodStartDate
})
);
// 团队统计始终使用当前周期(本周),不跟随列表第一条数据的周期
const summaryPeriod = computed(() => resolveWorkReportSummaryPeriod('weekly'));
function getRowActions(row: Api.WorkReport.Weekly.WeeklyReport): BusinessTableAction[] {
const actions: BusinessTableAction[] = [

View File

@@ -1386,11 +1386,11 @@ function syncRichSupport(item: PlanItem, event: Event) {
<div v-if="!isReadonly" class="form-actions">
<!-- <ElButton>重置表单</ElButton>-->
<ElButton @click="emit('save')">保存草稿</ElButton>
<ElButton type="primary" class="btn-submit" @click="emit('submit')">提交审批</ElButton>
<ElButton type="primary" @click="emit('submit')">提交审批</ElButton>
</div>
<div v-else-if="scene === 'approval'" class="form-actions approval-form-actions">
<ElButton @click="emit('back')">退出审批</ElButton>
<ElButton type="primary" class="btn-submit" @click="emit('requestApprove')">开始审批</ElButton>
<ElButton type="primary" @click="emit('requestApprove')">开始审批</ElButton>
</div>
<BusinessFormDialog
@@ -1672,8 +1672,8 @@ function syncRichSupport(item: PlanItem, event: Event) {
place-items: center;
flex-shrink: 0;
border-radius: 16px;
background: #ccfbf1;
color: #0f766e;
background: var(--el-color-primary-light-8);
color: var(--el-color-primary);
font-weight: 900;
}
@@ -1766,12 +1766,12 @@ function syncRichSupport(item: PlanItem, event: Event) {
}
.radio-group-full :deep(.el-radio.is-checked) {
border-color: #0f766e;
background: #f0fdfa;
border-color: var(--el-color-primary);
background: var(--el-color-primary-light-9);
}
.radio-group-full :deep(.el-radio__input.is-checked + .el-radio__label) {
color: #0f766e;
color: var(--el-color-primary);
}
.review-grid,
@@ -1891,7 +1891,7 @@ function syncRichSupport(item: PlanItem, event: Event) {
display: grid;
place-items: center;
border-radius: 999px;
background: #0f766e;
background: var(--el-color-primary);
color: #fff;
font-size: 13px;
font-weight: 900;
@@ -2091,8 +2091,8 @@ function syncRichSupport(item: PlanItem, event: Event) {
}
.rich-editor:focus {
border-color: #0f766e;
box-shadow: 0 0 0 2px rgba(15, 118, 110, 0.1);
border-color: var(--el-color-primary);
box-shadow: 0 0 0 2px var(--el-color-primary-light-8);
}
.rich-editor:empty::before {
@@ -2113,7 +2113,7 @@ function syncRichSupport(item: PlanItem, event: Event) {
}
.rich-editor :deep(.rich-category-line) {
color: #0f766e;
color: var(--el-color-primary);
font-size: 13px;
font-weight: 700;
line-height: 1.6;
@@ -2153,7 +2153,7 @@ function syncRichSupport(item: PlanItem, event: Event) {
}
.rich-editor--preview:hover {
border-color: #0f766e;
border-color: var(--el-color-primary);
}
.structured-preview__popover {
@@ -2193,7 +2193,7 @@ function syncRichSupport(item: PlanItem, event: Event) {
min-width: 0;
flex: 1;
padding-left: 14px;
color: #0f766e;
color: var(--el-color-primary);
font-size: 13px;
font-weight: 800;
white-space: normal;
@@ -2239,7 +2239,7 @@ function syncRichSupport(item: PlanItem, event: Event) {
width: 4px;
height: 16px;
border-radius: 999px;
background: #0f766e;
background: var(--el-color-primary);
}
.rich-editor :deep(.rich-task-detail) {
@@ -2300,7 +2300,7 @@ function syncRichSupport(item: PlanItem, event: Event) {
.structured-task-title {
position: relative;
padding-left: 14px;
color: #0f766e;
color: var(--el-color-primary);
font-size: 13px;
font-weight: 800;
}
@@ -2313,7 +2313,7 @@ function syncRichSupport(item: PlanItem, event: Event) {
width: 4px;
height: 16px;
border-radius: 999px;
background: #0f766e;
background: var(--el-color-primary);
}
.structured-task-detail {
@@ -2330,8 +2330,8 @@ function syncRichSupport(item: PlanItem, event: Event) {
align-items: center;
padding: 12px 14px;
border-radius: 10px;
background: #f0fdfa;
color: #0f766e;
background: var(--el-color-primary-light-9);
color: var(--el-color-primary);
font-size: 13px;
font-weight: 900;
}
@@ -2401,9 +2401,9 @@ function syncRichSupport(item: PlanItem, event: Event) {
}
.plan-section.active {
border-color: #cfe3e0;
background: #f7fbfa;
box-shadow: inset 0 0 0 1px rgba(15, 118, 110, 0.06);
border-color: var(--el-color-primary-light-7);
background: var(--el-color-primary-light-9);
box-shadow: inset 0 0 0 1px var(--el-color-primary-light-9);
}
.plan-section-head {
@@ -2627,16 +2627,6 @@ function syncRichSupport(item: PlanItem, event: Event) {
box-shadow: 0 -8px 18px rgba(15, 23, 42, 0.06);
}
.btn-submit {
background: #0f766e !important;
border-color: #0f766e !important;
}
.btn-submit:hover {
background: #0d9488 !important;
border-color: #0d9488 !important;
}
@media (max-width: 1180px) {
.form-head,
.compose-grid,