fix(产品需求、项目需求、工作报告、我的绩效、加班申请): 1、修复搜索区域的下拉框,无法搜索的问题。2、修复工作报告内容溢出的问题。3、修复我的待办 - 待审批,里面的二级tabs没有加权限的问题。4、优化周报里工作日志展示时的分隔符。5、优化项目需求池、我的绩效请求重复发送、影响效率的问题。
feat(我的绩效): 1、增加我的绩效在工作台可以直接处理的功能。2、增加我的绩效全部导出时,合并多个excel的sheet为一个excel的功能。
This commit is contained in:
@@ -40,6 +40,8 @@ export interface SearchField {
|
|||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
/** select 类型的自定义选项渲染函数 */
|
/** select 类型的自定义选项渲染函数 */
|
||||||
renderOption?: (option: Option) => VNode | VNode[] | string;
|
renderOption?: (option: Option) => VNode | VNode[] | string;
|
||||||
|
/** select/dict 类型是否支持搜索 */
|
||||||
|
filterable?: boolean;
|
||||||
/** 值写回模型前的转换函数 */
|
/** 值写回模型前的转换函数 */
|
||||||
transformValue?: (value: any) => any;
|
transformValue?: (value: any) => any;
|
||||||
/** 从模型值解析展示值 */
|
/** 从模型值解析展示值 */
|
||||||
@@ -168,6 +170,7 @@ function getFieldValue(field: SearchField) {
|
|||||||
:model-value="getFieldValue(field)"
|
:model-value="getFieldValue(field)"
|
||||||
:placeholder="field.placeholder"
|
:placeholder="field.placeholder"
|
||||||
clearable
|
clearable
|
||||||
|
:filterable="field.filterable"
|
||||||
:disabled="props.disabled"
|
:disabled="props.disabled"
|
||||||
@update:model-value="val => updateFieldValue(field, val)"
|
@update:model-value="val => updateFieldValue(field, val)"
|
||||||
>
|
>
|
||||||
@@ -207,6 +210,7 @@ function getFieldValue(field: SearchField) {
|
|||||||
:model-value="getFieldValue(field)"
|
:model-value="getFieldValue(field)"
|
||||||
:dict-code="field.dictCode!"
|
:dict-code="field.dictCode!"
|
||||||
:placeholder="field.placeholder"
|
:placeholder="field.placeholder"
|
||||||
|
:filterable="field.filterable"
|
||||||
:disabled="props.disabled"
|
:disabled="props.disabled"
|
||||||
:show-remark="field.showRemark"
|
:show-remark="field.showRemark"
|
||||||
@update:model-value="val => updateFieldValue(field, val)"
|
@update:model-value="val => updateFieldValue(field, val)"
|
||||||
@@ -268,6 +272,7 @@ function getFieldValue(field: SearchField) {
|
|||||||
:model-value="getFieldValue(field)"
|
:model-value="getFieldValue(field)"
|
||||||
:placeholder="field.placeholder"
|
:placeholder="field.placeholder"
|
||||||
clearable
|
clearable
|
||||||
|
:filterable="field.filterable"
|
||||||
:disabled="props.disabled"
|
:disabled="props.disabled"
|
||||||
@update:model-value="val => updateFieldValue(field, val)"
|
@update:model-value="val => updateFieldValue(field, val)"
|
||||||
>
|
>
|
||||||
@@ -307,6 +312,7 @@ function getFieldValue(field: SearchField) {
|
|||||||
:model-value="getFieldValue(field)"
|
:model-value="getFieldValue(field)"
|
||||||
:dict-code="field.dictCode!"
|
:dict-code="field.dictCode!"
|
||||||
:placeholder="field.placeholder"
|
:placeholder="field.placeholder"
|
||||||
|
:filterable="field.filterable"
|
||||||
:disabled="props.disabled"
|
:disabled="props.disabled"
|
||||||
:show-remark="field.showRemark"
|
:show-remark="field.showRemark"
|
||||||
@update:model-value="val => updateFieldValue(field, val)"
|
@update:model-value="val => updateFieldValue(field, val)"
|
||||||
|
|||||||
@@ -118,6 +118,7 @@ const actionType = ref<'confirm' | 'reject'>('confirm');
|
|||||||
const recordVisible = ref(false);
|
const recordVisible = ref(false);
|
||||||
|
|
||||||
const exporting = ref(false);
|
const exporting = ref(false);
|
||||||
|
const currentUserId = computed(() => authStore.userInfo.userId || '');
|
||||||
const rowActionLoadingKey = ref('');
|
const rowActionLoadingKey = ref('');
|
||||||
|
|
||||||
const ACTION_ICON_MAP = {
|
const ACTION_ICON_MAP = {
|
||||||
@@ -326,7 +327,9 @@ function getRowActions(row: Api.Performance.Sheet.Sheet): BusinessTableAction[]
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isTeamMode.value) {
|
if (isTeamMode.value) {
|
||||||
if (canUpdate.value && ['draft', 'rejected'].includes(row.statusCode)) {
|
const isDirectManager = Boolean(currentUserId.value) && row.managerId === currentUserId.value;
|
||||||
|
|
||||||
|
if (isDirectManager && canUpdate.value && ['draft', 'rejected'].includes(row.statusCode)) {
|
||||||
actions.push({
|
actions.push({
|
||||||
key: 'edit',
|
key: 'edit',
|
||||||
label: '编辑',
|
label: '编辑',
|
||||||
@@ -344,7 +347,7 @@ function getRowActions(row: Api.Performance.Sheet.Sheet): BusinessTableAction[]
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (canDelete.value && row.statusCode === 'draft') {
|
if (isDirectManager && canDelete.value && row.statusCode === 'draft') {
|
||||||
actions.push({
|
actions.push({
|
||||||
key: 'delete',
|
key: 'delete',
|
||||||
label: '删除',
|
label: '删除',
|
||||||
@@ -599,13 +602,13 @@ async function loadDeptOptions() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function loadDirectSubordinateOptions() {
|
async function loadDirectSubordinateOptions() {
|
||||||
const currentUserId = authStore.userInfo.userId;
|
const loginUserId = authStore.userInfo.userId;
|
||||||
if (!currentUserId) {
|
if (!loginUserId) {
|
||||||
directSubordinateOptions.value = [];
|
directSubordinateOptions.value = [];
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { error, data: userList } = await fetchGetDirectSubordinates(currentUserId);
|
const { error, data: userList } = await fetchGetDirectSubordinates(loginUserId);
|
||||||
|
|
||||||
if (error || !userList) {
|
if (error || !userList) {
|
||||||
directSubordinateOptions.value = [];
|
directSubordinateOptions.value = [];
|
||||||
@@ -643,8 +646,12 @@ async function handleTeamViewModeChange(mode: TeamViewMode) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => [teamViewMode.value, selectedSubordinateUserId.value],
|
() => [teamViewMode.value, selectedSubordinateUserId.value] as const,
|
||||||
async () => {
|
async ([mode], [previousMode]) => {
|
||||||
|
if (mode === 'self' && previousMode === 'self') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
await refreshPageData(1);
|
await refreshPageData(1);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -7,13 +7,18 @@ import BusinessFormDialog from '@/components/custom/business-form-dialog.vue';
|
|||||||
|
|
||||||
defineOptions({ name: 'PerformanceActionDialog' });
|
defineOptions({ name: 'PerformanceActionDialog' });
|
||||||
|
|
||||||
|
type ActionType = 'confirm' | 'reject';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
rowData?: Api.Performance.Sheet.Sheet | null;
|
rowData?: Api.Performance.Sheet.Sheet | null;
|
||||||
actionType: 'confirm' | 'reject';
|
actionType?: ActionType;
|
||||||
|
selectableActionType?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
rowData: null
|
rowData: null,
|
||||||
|
actionType: 'confirm',
|
||||||
|
selectableActionType: false
|
||||||
});
|
});
|
||||||
|
|
||||||
const visible = defineModel<boolean>('visible', { default: false });
|
const visible = defineModel<boolean>('visible', { default: false });
|
||||||
@@ -26,13 +31,25 @@ const { formRef, validate } = useForm();
|
|||||||
const { createRequiredRule } = useFormRules();
|
const { createRequiredRule } = useFormRules();
|
||||||
|
|
||||||
const submitting = ref(false);
|
const submitting = ref(false);
|
||||||
const form = reactive({
|
const form = reactive<{
|
||||||
|
actionType: ActionType;
|
||||||
|
reason: string;
|
||||||
|
}>({
|
||||||
|
actionType: 'confirm',
|
||||||
reason: ''
|
reason: ''
|
||||||
});
|
});
|
||||||
|
|
||||||
const isReject = computed(() => props.actionType === 'reject');
|
const isReject = computed(() => form.actionType === 'reject');
|
||||||
const title = computed(() => (isReject.value ? '退回绩效表' : '确认绩效表'));
|
const title = computed(() => {
|
||||||
const confirmText = computed(() => (isReject.value ? '确认退回' : '确认'));
|
if (props.selectableActionType) return '绩效审批';
|
||||||
|
return isReject.value ? '退回绩效表' : '确认绩效表';
|
||||||
|
});
|
||||||
|
const confirmText = computed(() => {
|
||||||
|
if (props.selectableActionType) return '确认提交';
|
||||||
|
return isReject.value ? '确认退回' : '确认';
|
||||||
|
});
|
||||||
|
const opinionLabel = computed(() => (isReject.value ? '退回原因' : '审批意见'));
|
||||||
|
const opinionPlaceholder = computed(() => (isReject.value ? `请输入${opinionLabel.value}` : '可填写审批意见'));
|
||||||
const rules = computed<FormRules>(() => ({
|
const rules = computed<FormRules>(() => ({
|
||||||
reason: isReject.value ? [createRequiredRule('请输入退回原因')] : []
|
reason: isReject.value ? [createRequiredRule('请输入退回原因')] : []
|
||||||
}));
|
}));
|
||||||
@@ -63,6 +80,7 @@ async function handleSubmit() {
|
|||||||
|
|
||||||
watch(visible, isVisible => {
|
watch(visible, isVisible => {
|
||||||
if (!isVisible) return;
|
if (!isVisible) return;
|
||||||
|
form.actionType = props.actionType;
|
||||||
form.reason = '';
|
form.reason = '';
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
@@ -84,16 +102,119 @@ watch(visible, isVisible => {
|
|||||||
<ElDescriptionsItem label="实际得分">{{ props.rowData?.actualScoreTotal ?? '--' }}</ElDescriptionsItem>
|
<ElDescriptionsItem label="实际得分">{{ props.rowData?.actualScoreTotal ?? '--' }}</ElDescriptionsItem>
|
||||||
</ElDescriptions>
|
</ElDescriptions>
|
||||||
|
|
||||||
<ElFormItem class="mt-16px" :label="isReject ? '退回原因' : '确认意见'" prop="reason">
|
<div v-if="props.selectableActionType" class="performance-action-dialog__approval-form">
|
||||||
|
<div class="audit-field">
|
||||||
|
<label>审批结论</label>
|
||||||
|
<div class="audit-conclusion">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="conclusion-btn"
|
||||||
|
:class="{
|
||||||
|
active: form.actionType === 'confirm',
|
||||||
|
pass: form.actionType === 'confirm'
|
||||||
|
}"
|
||||||
|
@click="form.actionType = 'confirm'"
|
||||||
|
>
|
||||||
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
|
||||||
|
<circle cx="8" cy="8" r="7" stroke="currentColor" stroke-width="1.5" />
|
||||||
|
<path
|
||||||
|
d="M5 8.5L7 10.5L11 6"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="1.5"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
确认
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="conclusion-btn"
|
||||||
|
:class="{
|
||||||
|
active: form.actionType === 'reject',
|
||||||
|
reject: form.actionType === 'reject'
|
||||||
|
}"
|
||||||
|
@click="form.actionType = 'reject'"
|
||||||
|
>
|
||||||
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
|
||||||
|
<circle cx="8" cy="8" r="7" stroke="currentColor" stroke-width="1.5" />
|
||||||
|
<path d="M6 6L10 10M10 6L6 10" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" />
|
||||||
|
</svg>
|
||||||
|
退回
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ElFormItem class="mt-16px" :label="opinionLabel" prop="reason">
|
||||||
<ElInput
|
<ElInput
|
||||||
v-model="form.reason"
|
v-model="form.reason"
|
||||||
type="textarea"
|
type="textarea"
|
||||||
:rows="4"
|
:rows="4"
|
||||||
maxlength="1000"
|
maxlength="1000"
|
||||||
show-word-limit
|
show-word-limit
|
||||||
:placeholder="isReject ? '请输入退回原因' : '可填写确认意见'"
|
:placeholder="opinionPlaceholder"
|
||||||
/>
|
/>
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
</ElForm>
|
</ElForm>
|
||||||
</BusinessFormDialog>
|
</BusinessFormDialog>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.performance-action-dialog__approval-form {
|
||||||
|
display: grid;
|
||||||
|
gap: 18px;
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.audit-field {
|
||||||
|
display: grid;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.audit-field label {
|
||||||
|
color: #475467;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.audit-conclusion {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.conclusion-btn {
|
||||||
|
height: 44px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
|
border: 1px solid #d8e0e8;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #fff;
|
||||||
|
color: #475467;
|
||||||
|
font: inherit;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 800;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.18s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.conclusion-btn:hover {
|
||||||
|
border-color: var(--el-color-primary);
|
||||||
|
color: var(--el-color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.conclusion-btn.active.pass {
|
||||||
|
border-color: var(--el-color-primary);
|
||||||
|
background: var(--el-color-primary-light-9);
|
||||||
|
color: var(--el-color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.conclusion-btn.active.reject {
|
||||||
|
border-color: #dc2626;
|
||||||
|
background: #fef2f2;
|
||||||
|
color: #dc2626;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -26,11 +26,13 @@ interface Props {
|
|||||||
rowData?: Api.Performance.Sheet.Sheet | null;
|
rowData?: Api.Performance.Sheet.Sheet | null;
|
||||||
mode: 'view' | 'edit' | 'create';
|
mode: 'view' | 'edit' | 'create';
|
||||||
subordinateOptions?: SubordinateOption[];
|
subordinateOptions?: SubordinateOption[];
|
||||||
|
showApprovalFooter?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
rowData: null,
|
rowData: null,
|
||||||
subordinateOptions: () => []
|
subordinateOptions: () => [],
|
||||||
|
showApprovalFooter: false
|
||||||
});
|
});
|
||||||
|
|
||||||
const visible = defineModel<boolean>('visible', { default: false });
|
const visible = defineModel<boolean>('visible', { default: false });
|
||||||
@@ -38,6 +40,7 @@ const visible = defineModel<boolean>('visible', { default: false });
|
|||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
saved: [];
|
saved: [];
|
||||||
savedAndSent: [];
|
savedAndSent: [];
|
||||||
|
startApproval: [];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const { formRef, validate } = useForm();
|
const { formRef, validate } = useForm();
|
||||||
@@ -83,6 +86,7 @@ const drawerTitle = computed(() => {
|
|||||||
return `${action}绩效 Excel${name ? ` - ${name}` : ''}`;
|
return `${action}绩效 Excel${name ? ` - ${name}` : ''}`;
|
||||||
});
|
});
|
||||||
const canSave = computed(() => props.mode !== 'view');
|
const canSave = computed(() => props.mode !== 'view');
|
||||||
|
const showApprovalFooter = computed(() => props.mode === 'view' && props.showApprovalFooter);
|
||||||
const drawerSize = computed(() => (viewportWidth.value >= 2560 ? '60%' : '88%'));
|
const drawerSize = computed(() => (viewportWidth.value >= 2560 ? '60%' : '88%'));
|
||||||
|
|
||||||
function syncViewportWidth() {
|
function syncViewportWidth() {
|
||||||
@@ -93,6 +97,10 @@ function handleClose() {
|
|||||||
visible.value = false;
|
visible.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleStartApproval() {
|
||||||
|
emit('startApproval');
|
||||||
|
}
|
||||||
|
|
||||||
function disposeUniver() {
|
function disposeUniver() {
|
||||||
try {
|
try {
|
||||||
univerInstance?.dispose?.();
|
univerInstance?.dispose?.();
|
||||||
@@ -687,10 +695,16 @@ onMounted(() => {
|
|||||||
<div ref="containerRef" class="performance-excel-editor__container" />
|
<div ref="containerRef" class="performance-excel-editor__container" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<template v-if="canSave" #footer>
|
<template v-if="canSave || showApprovalFooter" #footer>
|
||||||
<div class="performance-excel-editor__footer">
|
<div class="performance-excel-editor__footer">
|
||||||
|
<template v-if="canSave">
|
||||||
<ElButton :loading="saving" :disabled="sending" @click="handleSaveDraft">保存草稿</ElButton>
|
<ElButton :loading="saving" :disabled="sending" @click="handleSaveDraft">保存草稿</ElButton>
|
||||||
<ElButton type="primary" :loading="sending" :disabled="saving" @click="handleSaveAndSend">发送绩效</ElButton>
|
<ElButton type="primary" :loading="sending" :disabled="saving" @click="handleSaveAndSend">发送绩效</ElButton>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="showApprovalFooter">
|
||||||
|
<ElButton @click="handleClose">退出审批</ElButton>
|
||||||
|
<ElButton type="primary" @click="handleStartApproval">开始审批</ElButton>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</ElDrawer>
|
</ElDrawer>
|
||||||
|
|||||||
@@ -45,7 +45,14 @@ const baseFields = computed<SearchField[]>(() => [
|
|||||||
|
|
||||||
const teamFields = computed<SearchField[]>(() => [
|
const teamFields = computed<SearchField[]>(() => [
|
||||||
baseFields.value[0],
|
baseFields.value[0],
|
||||||
{ key: 'employeeId', label: '下属', type: 'select', placeholder: '请选择下属', options: props.subordinateOptions },
|
{
|
||||||
|
key: 'employeeId',
|
||||||
|
label: '下属',
|
||||||
|
type: 'select',
|
||||||
|
placeholder: '请选择下属',
|
||||||
|
options: props.subordinateOptions,
|
||||||
|
filterable: true
|
||||||
|
},
|
||||||
{ key: 'employeeDeptId', label: '部门', type: 'select', placeholder: '请选择部门', options: props.deptOptions },
|
{ key: 'employeeDeptId', label: '部门', type: 'select', placeholder: '请选择部门', options: props.deptOptions },
|
||||||
{ key: 'managerName', label: '直属上级', type: 'input', placeholder: '请输入直属上级' },
|
{ key: 'managerName', label: '直属上级', type: 'input', placeholder: '请输入直属上级' },
|
||||||
baseFields.value[1]
|
baseFields.value[1]
|
||||||
|
|||||||
@@ -255,7 +255,7 @@ watch(visible, isVisible => {
|
|||||||
</ElTooltip>
|
</ElTooltip>
|
||||||
</div>
|
</div>
|
||||||
<ElTooltip placement="top" effect="light">
|
<ElTooltip placement="top" effect="light">
|
||||||
<template #content>支持 .xlsx、.xls,选择后会在这里显示文件名</template>
|
<template #content>仅支持 .xlsx格式,选择后会在这里显示文件名</template>
|
||||||
<button type="button" class="performance-template-dialog__hint-button" aria-label="Excel 文件说明">
|
<button type="button" class="performance-template-dialog__hint-button" aria-label="Excel 文件说明">
|
||||||
<icon-mdi-information-outline />
|
<icon-mdi-information-outline />
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -97,6 +97,7 @@ const fields = computed<SearchField[]>(() => {
|
|||||||
type: 'select' as const,
|
type: 'select' as const,
|
||||||
options: props.subordinateOptions,
|
options: props.subordinateOptions,
|
||||||
placeholder: '请选择申请人',
|
placeholder: '请选择申请人',
|
||||||
|
filterable: true,
|
||||||
transformValue: (value: string | number | null | undefined) => (value ? [value] : undefined),
|
transformValue: (value: string | number | null | undefined) => (value ? [value] : undefined),
|
||||||
resolveValue: (value: unknown) => (Array.isArray(value) ? value[0] : value)
|
resolveValue: (value: unknown) => (Array.isArray(value) ? value[0] : value)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -342,6 +342,52 @@ function createStructuredSectionsFromTextV2(text: string, defaultCategory: strin
|
|||||||
return sections.filter(section => section.tasks.length);
|
return sections.filter(section => section.tasks.length);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isStructuredMetricsText(text: string) {
|
||||||
|
return /(?:\bP\d+\b|进度\s*\d+(?:\.\d+)?%|\d+(?:\.\d+)?%|\d+(?:\.\d+)?h)/iu.test(text.trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractStructuredTaskParts(normalizedText: string) {
|
||||||
|
const colonIndex = normalizedText.search(/[::]/u);
|
||||||
|
const mainText = colonIndex >= 0 ? normalizedText.slice(0, colonIndex).trim() : normalizedText.trim();
|
||||||
|
const detailText = colonIndex >= 0 ? normalizedText.slice(colonIndex + 1).trim() : '';
|
||||||
|
|
||||||
|
const bracketMatches = Array.from(mainText.matchAll(/[((]([^()()]*)[))]/gu));
|
||||||
|
const completeMetricsMatch = [...bracketMatches].reverse().find(match => isStructuredMetricsText(match[1] || ''));
|
||||||
|
|
||||||
|
if (completeMetricsMatch && completeMetricsMatch.index !== undefined) {
|
||||||
|
const fullMatch = completeMetricsMatch[0] || '';
|
||||||
|
const metricsText = completeMetricsMatch[1] || '';
|
||||||
|
const startIndex = completeMetricsMatch.index;
|
||||||
|
const endIndex = startIndex + fullMatch.length;
|
||||||
|
|
||||||
|
return {
|
||||||
|
rawTitle: `${mainText.slice(0, startIndex)}${mainText.slice(endIndex)}`.trim(),
|
||||||
|
metricsText,
|
||||||
|
detail: detailText
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const lastOpenIndex = Math.max(mainText.lastIndexOf('('), mainText.lastIndexOf('('));
|
||||||
|
const lastCloseIndex = Math.max(mainText.lastIndexOf(')'), mainText.lastIndexOf(')'));
|
||||||
|
|
||||||
|
if (lastOpenIndex > lastCloseIndex) {
|
||||||
|
const metricsText = mainText.slice(lastOpenIndex + 1).trim();
|
||||||
|
if (isStructuredMetricsText(metricsText)) {
|
||||||
|
return {
|
||||||
|
rawTitle: mainText.slice(0, lastOpenIndex).trim(),
|
||||||
|
metricsText,
|
||||||
|
detail: detailText
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
rawTitle: mainText,
|
||||||
|
metricsText: '',
|
||||||
|
detail: detailText
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function parseStructuredSectionTaskText(
|
function parseStructuredSectionTaskText(
|
||||||
text: string,
|
text: string,
|
||||||
fallback?: Partial<StructuredTask>,
|
fallback?: Partial<StructuredTask>,
|
||||||
@@ -350,13 +396,7 @@ function parseStructuredSectionTaskText(
|
|||||||
const normalizedText = stripStructuredTaskPrefixV2(text);
|
const normalizedText = stripStructuredTaskPrefixV2(text);
|
||||||
if (!normalizedText) return null;
|
if (!normalizedText) return null;
|
||||||
|
|
||||||
const structuredMatch =
|
const { rawTitle, metricsText, detail } = extractStructuredTaskParts(normalizedText);
|
||||||
normalizedText.match(/^(.+?)(?:[((]([^()()]*)[))])?(?:\s*[::]\s*(.*))?$/u) ||
|
|
||||||
normalizedText.match(/^(.+?)(?:\(([^()]*)\))?(?::\s*(.*))?$/u);
|
|
||||||
|
|
||||||
if (!structuredMatch) return null;
|
|
||||||
|
|
||||||
const [, rawTitle, metricsText = '', detail = ''] = structuredMatch;
|
|
||||||
const title = stripStructuredTaskSuffixV2(rawTitle);
|
const title = stripStructuredTaskSuffixV2(rawTitle);
|
||||||
if (!title) return null;
|
if (!title) return null;
|
||||||
|
|
||||||
@@ -412,7 +452,7 @@ function parseStructuredSectionsFromEditorV2(
|
|||||||
return {
|
return {
|
||||||
...task,
|
...task,
|
||||||
detail: fallbackTask?.detail || task.detail || '',
|
detail: fallbackTask?.detail || task.detail || '',
|
||||||
hours: task.hours ?? fallbackTask?.hours
|
hours: task.hours
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -178,7 +178,12 @@ function handleConfirm() {
|
|||||||
<div class="work-report-create-dialog__body">
|
<div class="work-report-create-dialog__body">
|
||||||
<div v-if="selectedReportType === 'project'" class="work-report-create-dialog__project-select">
|
<div v-if="selectedReportType === 'project'" class="work-report-create-dialog__project-select">
|
||||||
<label class="work-report-create-dialog__label">项目</label>
|
<label class="work-report-create-dialog__label">项目</label>
|
||||||
<ElSelect v-model="selectedProjectId" class="w-full" placeholder="请选择项目" filterable>
|
<ElSelect
|
||||||
|
v-model="selectedProjectId"
|
||||||
|
class="work-report-create-dialog__project-select-control w-full"
|
||||||
|
placeholder="请选择项目"
|
||||||
|
filterable
|
||||||
|
>
|
||||||
<ElOption
|
<ElOption
|
||||||
v-for="item in props.projectOptions"
|
v-for="item in props.projectOptions"
|
||||||
:key="item.id"
|
:key="item.id"
|
||||||
@@ -367,6 +372,16 @@ function handleConfirm() {
|
|||||||
gap: 6px;
|
gap: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.work-report-create-dialog__project-select-control :deep(.el-select__wrapper) {
|
||||||
|
min-height: 46px;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding-inline: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.work-report-create-dialog__project-select-control :deep(.el-select__selected-item) {
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
.work-report-create-dialog__field {
|
.work-report-create-dialog__field {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
|
|||||||
@@ -83,6 +83,7 @@ const fields = computed<SearchField[]>(() => {
|
|||||||
type: 'select',
|
type: 'select',
|
||||||
options: props.subordinateOptions,
|
options: props.subordinateOptions,
|
||||||
placeholder: '请选择提交人',
|
placeholder: '请选择提交人',
|
||||||
|
filterable: true,
|
||||||
transformValue: value => (value ? [value] : undefined),
|
transformValue: value => (value ? [value] : undefined),
|
||||||
resolveValue: value => (Array.isArray(value) ? value[0] : value)
|
resolveValue: value => (Array.isArray(value) ? value[0] : value)
|
||||||
}
|
}
|
||||||
@@ -115,7 +116,8 @@ const fields = computed<SearchField[]>(() => {
|
|||||||
label: item.projectCode ? `${item.projectName}(${item.projectCode})` : item.projectName,
|
label: item.projectCode ? `${item.projectName}(${item.projectCode})` : item.projectName,
|
||||||
value: item.id
|
value: item.id
|
||||||
})),
|
})),
|
||||||
placeholder: '请选择项目'
|
placeholder: '请选择项目',
|
||||||
|
filterable: true
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@@ -129,6 +131,7 @@ const fields = computed<SearchField[]>(() => {
|
|||||||
type: 'select',
|
type: 'select',
|
||||||
options: props.subordinateOptions,
|
options: props.subordinateOptions,
|
||||||
placeholder: '请选择提交人',
|
placeholder: '请选择提交人',
|
||||||
|
filterable: true,
|
||||||
transformValue: value => (value ? [value] : undefined),
|
transformValue: value => (value ? [value] : undefined),
|
||||||
resolveValue: value => (Array.isArray(value) ? value[0] : value)
|
resolveValue: value => (Array.isArray(value) ? value[0] : value)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -358,8 +358,10 @@ function resolveTaskItemTypeLabel(value?: string | null) {
|
|||||||
return getTaskItemTypeLabel(value, { fallback: value || '工作内容' });
|
return getTaskItemTypeLabel(value, { fallback: value || '工作内容' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const STRUCTURED_TASK_PREFIX_RE = /^(?:(?:\d+[..、])|(?:\d+\s+)|(?:[一二三四五六七八九十百千万]+[、..]))\s*/u;
|
||||||
|
|
||||||
function stripStructuredTaskPrefixV2(value: string) {
|
function stripStructuredTaskPrefixV2(value: string) {
|
||||||
return value.trim().replace(/^\d+[..、]\s*/u, '');
|
return value.trim().replace(STRUCTURED_TASK_PREFIX_RE, '');
|
||||||
}
|
}
|
||||||
|
|
||||||
function stripStructuredTaskSuffixV2(value: string) {
|
function stripStructuredTaskSuffixV2(value: string) {
|
||||||
@@ -381,16 +383,13 @@ function formatStructuredTaskDisplayLine(task: StructuredTask, index: number, sh
|
|||||||
return `${index + 1}、${formatStructuredTaskLineV2(task, showHours)}`;
|
return `${index + 1}、${formatStructuredTaskLineV2(task, showHours)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 周报工作日志弹层展示:后端用中文分号 ";" 拼接多条工作日志,
|
function getWorkLogEntries(detail: string): string[] {
|
||||||
// 在 popover 中按行展示,每条工作日志仍保留末尾的分号。
|
if (!detail) return [];
|
||||||
function formatWorkLogDetail(detail: string): string {
|
// 仅按中文分号切分,避免误伤文本中的其他标点;每段作为单独一天的工作日志展示。
|
||||||
if (!detail) return '';
|
|
||||||
// 仅按中文分号切分,避免误伤文本中的其他标点;每段保持原样展示。
|
|
||||||
return detail
|
return detail
|
||||||
.split(';')
|
.split(';')
|
||||||
.map(item => item.trim())
|
.map(item => item.trim())
|
||||||
.filter(Boolean)
|
.filter(Boolean);
|
||||||
.join(';\n');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function createStructuredTextV2(sections: StructuredSection[], showHours = false) {
|
function createStructuredTextV2(sections: StructuredSection[], showHours = false) {
|
||||||
@@ -493,7 +492,7 @@ function createStructuredTasksFromText(text: string, defaultTask?: Partial<Struc
|
|||||||
}
|
}
|
||||||
|
|
||||||
function stripStructuredTaskPrefix(value: string) {
|
function stripStructuredTaskPrefix(value: string) {
|
||||||
return value.trim().replace(/^\d+[..、]\s*/u, '');
|
return value.trim().replace(STRUCTURED_TASK_PREFIX_RE, '');
|
||||||
}
|
}
|
||||||
|
|
||||||
function stripStructuredTaskSuffix(value: string) {
|
function stripStructuredTaskSuffix(value: string) {
|
||||||
@@ -524,6 +523,52 @@ function resolveTaskMetrics(metricsText: string, fallback?: Partial<StructuredTa
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isStructuredMetricsText(text: string) {
|
||||||
|
return /(?:\bP\d+\b|进度\s*\d+(?:\.\d+)?%|\d+(?:\.\d+)?%|\d+(?:\.\d+)?h)/iu.test(text.trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractStructuredTaskParts(normalizedText: string) {
|
||||||
|
const colonIndex = normalizedText.search(/[::]/u);
|
||||||
|
const mainText = colonIndex >= 0 ? normalizedText.slice(0, colonIndex).trim() : normalizedText.trim();
|
||||||
|
const detailText = colonIndex >= 0 ? normalizedText.slice(colonIndex + 1).trim() : '';
|
||||||
|
|
||||||
|
const bracketMatches = Array.from(mainText.matchAll(/[((]([^()()]*)[))]/gu));
|
||||||
|
const completeMetricsMatch = [...bracketMatches].reverse().find(match => isStructuredMetricsText(match[1] || ''));
|
||||||
|
|
||||||
|
if (completeMetricsMatch && completeMetricsMatch.index !== undefined) {
|
||||||
|
const fullMatch = completeMetricsMatch[0] || '';
|
||||||
|
const metricsText = completeMetricsMatch[1] || '';
|
||||||
|
const startIndex = completeMetricsMatch.index;
|
||||||
|
const endIndex = startIndex + fullMatch.length;
|
||||||
|
|
||||||
|
return {
|
||||||
|
rawTitle: `${mainText.slice(0, startIndex)}${mainText.slice(endIndex)}`.trim(),
|
||||||
|
metricsText,
|
||||||
|
detail: detailText
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const lastOpenIndex = Math.max(mainText.lastIndexOf('('), mainText.lastIndexOf('('));
|
||||||
|
const lastCloseIndex = Math.max(mainText.lastIndexOf(')'), mainText.lastIndexOf(')'));
|
||||||
|
|
||||||
|
if (lastOpenIndex > lastCloseIndex) {
|
||||||
|
const metricsText = mainText.slice(lastOpenIndex + 1).trim();
|
||||||
|
if (isStructuredMetricsText(metricsText)) {
|
||||||
|
return {
|
||||||
|
rawTitle: mainText.slice(0, lastOpenIndex).trim(),
|
||||||
|
metricsText,
|
||||||
|
detail: detailText
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
rawTitle: mainText,
|
||||||
|
metricsText: '',
|
||||||
|
detail: detailText
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function parseStructuredSectionTaskText(
|
function parseStructuredSectionTaskText(
|
||||||
text: string,
|
text: string,
|
||||||
fallback?: Partial<StructuredTask>,
|
fallback?: Partial<StructuredTask>,
|
||||||
@@ -532,13 +577,7 @@ function parseStructuredSectionTaskText(
|
|||||||
const normalizedText = stripStructuredTaskPrefix(text);
|
const normalizedText = stripStructuredTaskPrefix(text);
|
||||||
if (!normalizedText) return null;
|
if (!normalizedText) return null;
|
||||||
|
|
||||||
const structuredMatch =
|
const { rawTitle, metricsText, detail } = extractStructuredTaskParts(normalizedText);
|
||||||
normalizedText.match(/^(.+?)(?:[((]([^()()]*)[))])?(?:\s*[::]\s*(.*))?$/u) ||
|
|
||||||
normalizedText.match(/^(.+?)(?:\(([^()]*)\))?(?::\s*(.*))?$/u);
|
|
||||||
|
|
||||||
if (!structuredMatch) return null;
|
|
||||||
|
|
||||||
const [, rawTitle, metricsText = '', detail = ''] = structuredMatch;
|
|
||||||
const title = stripStructuredTaskSuffix(rawTitle);
|
const title = stripStructuredTaskSuffix(rawTitle);
|
||||||
if (!title) return null;
|
if (!title) return null;
|
||||||
|
|
||||||
@@ -569,6 +608,16 @@ function createStructuredSectionsFromTextV2(
|
|||||||
};
|
};
|
||||||
|
|
||||||
let currentCategory = '';
|
let currentCategory = '';
|
||||||
|
let previousTask: StructuredTask | null = null;
|
||||||
|
|
||||||
|
const shouldAppendToPreviousTaskDetail = (line: string) => {
|
||||||
|
if (!previousTask?.detail) return false;
|
||||||
|
if (line.startsWith('#')) return false;
|
||||||
|
if (line.includes('(') || line.includes('(') || line.includes(':') || line.includes(':')) return false;
|
||||||
|
return !/^(?!(?:(?:\d+[..、])|(?:\d+\s+)|(?:[一二三四五六七八九十百千万]+[、..]))\s*)(.+?)\s*[--]\s*(.+)$/u.test(
|
||||||
|
line
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
lines.forEach(line => {
|
lines.forEach(line => {
|
||||||
const trimmedLine = line.trim();
|
const trimmedLine = line.trim();
|
||||||
@@ -577,12 +626,15 @@ function createStructuredSectionsFromTextV2(
|
|||||||
if (trimmedLine.startsWith('#')) {
|
if (trimmedLine.startsWith('#')) {
|
||||||
currentCategory = trimmedLine.replace(/^#\s*/u, '').trim();
|
currentCategory = trimmedLine.replace(/^#\s*/u, '').trim();
|
||||||
if (currentCategory) ensureSection(currentCategory);
|
if (currentCategory) ensureSection(currentCategory);
|
||||||
|
previousTask = null;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 仅当行首不是结构化任务前缀(如 "3、")时,才按旧式 "<分类> - <事项>" 解析;
|
// 仅当行首不是结构化任务前缀(如 "3、")时,才按旧式 "<分类> - <事项>" 解析;
|
||||||
// 否则会把 "2026-06-12 - 2026-06-19" 这种含 " - " 的出差行误判为分类。
|
// 否则会把 "2026-06-12 - 2026-06-19" 这种含 " - " 的出差行误判为分类。
|
||||||
const legacyMatch = trimmedLine.match(/^(?!\d+[、..]\s*)(.+?)\s*[--]\s*(.+)$/u);
|
const legacyMatch = trimmedLine.match(
|
||||||
|
/^(?!(?:(?:\d+[..、])|(?:\d+\s+)|(?:[一二三四五六七八九十百千万]+[、..]))\s*)(.+?)\s*[--]\s*(.+)$/u
|
||||||
|
);
|
||||||
if (legacyMatch) {
|
if (legacyMatch) {
|
||||||
const [, rawCategory, rawTaskText] = legacyMatch;
|
const [, rawCategory, rawTaskText] = legacyMatch;
|
||||||
const category = rawCategory.trim();
|
const category = rawCategory.trim();
|
||||||
@@ -590,6 +642,14 @@ function createStructuredSectionsFromTextV2(
|
|||||||
if (!category || !task) return;
|
if (!category || !task) return;
|
||||||
ensureSection(category).tasks.push(task);
|
ensureSection(category).tasks.push(task);
|
||||||
currentCategory = category;
|
currentCategory = category;
|
||||||
|
previousTask = task;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldAppendToPreviousTaskDetail(trimmedLine)) {
|
||||||
|
const lastTask = previousTask;
|
||||||
|
if (!lastTask) return;
|
||||||
|
lastTask.detail = lastTask.detail ? `${lastTask.detail}\n${trimmedLine}` : trimmedLine;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -597,7 +657,7 @@ function createStructuredSectionsFromTextV2(
|
|||||||
if (!task) return;
|
if (!task) return;
|
||||||
|
|
||||||
const hasStructuredHint =
|
const hasStructuredHint =
|
||||||
/^(\d+[..、]\s*)/u.test(trimmedLine) ||
|
STRUCTURED_TASK_PREFIX_RE.test(trimmedLine) ||
|
||||||
trimmedLine.includes('(') ||
|
trimmedLine.includes('(') ||
|
||||||
trimmedLine.includes('(') ||
|
trimmedLine.includes('(') ||
|
||||||
trimmedLine.includes(':') ||
|
trimmedLine.includes(':') ||
|
||||||
@@ -606,6 +666,7 @@ function createStructuredSectionsFromTextV2(
|
|||||||
if (!hasStructuredHint && !currentCategory) return;
|
if (!hasStructuredHint && !currentCategory) return;
|
||||||
|
|
||||||
ensureSection(currentCategory || defaultCategory).tasks.push(task);
|
ensureSection(currentCategory || defaultCategory).tasks.push(task);
|
||||||
|
previousTask = task;
|
||||||
});
|
});
|
||||||
|
|
||||||
return mergeSectionsByCategory(sections);
|
return mergeSectionsByCategory(sections);
|
||||||
@@ -657,7 +718,7 @@ function parseStructuredSectionsFromEditorV2(
|
|||||||
...task,
|
...task,
|
||||||
detail: fallbackTask?.detail || '',
|
detail: fallbackTask?.detail || '',
|
||||||
kind: fallbackTask?.kind,
|
kind: fallbackTask?.kind,
|
||||||
hours: task.hours ?? fallbackTask?.hours
|
hours: task.hours
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
@@ -1065,6 +1126,11 @@ function blurEditField(key: string) {
|
|||||||
if (activeEditField.value === key) activeEditField.value = '';
|
if (activeEditField.value === key) activeEditField.value = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isStructuredEditorUnchanged(text: string, sections: StructuredSection[] | undefined, showHours = false) {
|
||||||
|
if (!sections?.length) return false;
|
||||||
|
return normalizeEditorText(text) === createStructuredTextV2(sections, showHours);
|
||||||
|
}
|
||||||
|
|
||||||
/** 编辑态下是否显示"具体工作内容"的结构化预览(含 ElPopover 工作日志) */
|
/** 编辑态下是否显示"具体工作内容"的结构化预览(含 ElPopover 工作日志) */
|
||||||
function showContentStructuredView(index: number) {
|
function showContentStructuredView(index: number) {
|
||||||
const item = reviewItems.value[index];
|
const item = reviewItems.value[index];
|
||||||
@@ -1096,6 +1162,11 @@ function handleStructuredViewClick(fieldKey: string) {
|
|||||||
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;
|
||||||
|
if (isStructuredEditorUnchanged(target.innerText, item.contentSections, true)) {
|
||||||
|
item.source.contentJson = createSectionsJson(item.contentSections || []);
|
||||||
|
item.source.contentText = createStructuredTextV2(item.contentSections || [], true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
const sections = parseStructuredSectionsFromEditorV2(
|
const sections = parseStructuredSectionsFromEditorV2(
|
||||||
target,
|
target,
|
||||||
item.contentSections || [],
|
item.contentSections || [],
|
||||||
@@ -1108,6 +1179,11 @@ function syncRichContent(item: ReviewItem, event: Event) {
|
|||||||
function syncRichTarget(item: PlanItem, event: Event) {
|
function syncRichTarget(item: PlanItem, event: Event) {
|
||||||
const target = event.currentTarget as HTMLElement;
|
const target = event.currentTarget as HTMLElement;
|
||||||
if (!item.source) return;
|
if (!item.source) return;
|
||||||
|
if (isStructuredEditorUnchanged(target.innerText, item.targetSections)) {
|
||||||
|
item.source.targetJson = createSectionsJson(item.targetSections || []);
|
||||||
|
item.source.targetText = createStructuredTextV2(item.targetSections || []);
|
||||||
|
return;
|
||||||
|
}
|
||||||
const sections = parseStructuredSectionsFromEditorV2(
|
const sections = parseStructuredSectionsFromEditorV2(
|
||||||
target,
|
target,
|
||||||
item.targetSections || [],
|
item.targetSections || [],
|
||||||
@@ -1232,7 +1308,20 @@ function syncRichSupport(item: PlanItem, event: Event) {
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<div class="structured-preview__popover">
|
<div class="structured-preview__popover">
|
||||||
{{ formatWorkLogDetail(task.detail) || '暂无内容' }}
|
<template v-if="getWorkLogEntries(task.detail).length">
|
||||||
|
<div
|
||||||
|
v-for="(entry, entryIndex) in getWorkLogEntries(task.detail)"
|
||||||
|
:key="`${index}-${sectionIndex}-${taskIndex}-${entryIndex}`"
|
||||||
|
class="structured-preview__log-entry"
|
||||||
|
>
|
||||||
|
<div class="structured-preview__log-text">{{ entry }}</div>
|
||||||
|
<div
|
||||||
|
v-if="entryIndex < getWorkLogEntries(task.detail).length - 1"
|
||||||
|
class="structured-preview__log-divider"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-else>暂无内容</template>
|
||||||
</div>
|
</div>
|
||||||
</ElPopover>
|
</ElPopover>
|
||||||
<div v-else class="rich-task-line">
|
<div v-else class="rich-task-line">
|
||||||
@@ -1337,7 +1426,20 @@ function syncRichSupport(item: PlanItem, event: Event) {
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<div class="structured-preview__popover">
|
<div class="structured-preview__popover">
|
||||||
{{ formatWorkLogDetail(task.detail) || '暂无内容' }}
|
<template v-if="getWorkLogEntries(task.detail).length">
|
||||||
|
<div
|
||||||
|
v-for="(entry, entryIndex) in getWorkLogEntries(task.detail)"
|
||||||
|
:key="`${index}-${sectionIndex}-${taskIndex}-${entryIndex}`"
|
||||||
|
class="structured-preview__log-entry"
|
||||||
|
>
|
||||||
|
<div class="structured-preview__log-text">{{ entry }}</div>
|
||||||
|
<div
|
||||||
|
v-if="entryIndex < getWorkLogEntries(task.detail).length - 1"
|
||||||
|
class="structured-preview__log-divider"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-else>暂无内容</template>
|
||||||
</div>
|
</div>
|
||||||
</ElPopover>
|
</ElPopover>
|
||||||
<div v-else class="rich-task-line">
|
<div v-else class="rich-task-line">
|
||||||
@@ -2161,11 +2263,30 @@ function syncRichSupport(item: PlanItem, event: Event) {
|
|||||||
color: #334155;
|
color: #334155;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
|
display: grid;
|
||||||
|
gap: 0;
|
||||||
|
white-space: normal;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.structured-preview__log-entry {
|
||||||
|
display: grid;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.structured-preview__log-text {
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
overflow-wrap: anywhere;
|
overflow-wrap: anywhere;
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.structured-preview__log-divider {
|
||||||
|
width: 100%;
|
||||||
|
height: 1px;
|
||||||
|
background: #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
.rich-editor :deep(.rich-task) {
|
.rich-editor :deep(.rich-task) {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ const PAGE_SIZE = 10;
|
|||||||
/** 产品行多列数(名称/编码/经理/我的角色/状态/原因/更新),非产品行整行合并用 */
|
/** 产品行多列数(名称/编码/经理/我的角色/状态/原因/更新),非产品行整行合并用 */
|
||||||
const COLUMN_COUNT = 7;
|
const COLUMN_COUNT = 7;
|
||||||
/** 产品描述副行长度阈值:超过时展示「详情」入口 */
|
/** 产品描述副行长度阈值:超过时展示「详情」入口 */
|
||||||
const PRODUCT_DESC_MAX_LEN = 48;
|
const PRODUCT_DESC_MAX_LEN = 40;
|
||||||
|
|
||||||
interface DirectionGroup {
|
interface DirectionGroup {
|
||||||
directionCode: string;
|
directionCode: string;
|
||||||
|
|||||||
@@ -122,6 +122,7 @@ const fields = computed(() => [
|
|||||||
type: 'select' as const,
|
type: 'select' as const,
|
||||||
placeholder: '筛选负责人',
|
placeholder: '筛选负责人',
|
||||||
options: memberSelectOptions.value,
|
options: memberSelectOptions.value,
|
||||||
|
filterable: true,
|
||||||
renderOption: renderMemberOption
|
renderOption: renderMemberOption
|
||||||
}
|
}
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -639,7 +639,7 @@ async function reloadTable() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await loadTreeData();
|
await loadTreeData();
|
||||||
await Promise.all([loadAllowedTransitionsForAll(), loadRequirementDisplayTotal()]);
|
await loadAllowedTransitionsForAll();
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
}
|
}
|
||||||
@@ -813,11 +813,13 @@ async function handleDelete(row: Api.Project.ProjectRequirement) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
window.$message?.success('需求删除成功');
|
window.$message?.success('需求删除成功');
|
||||||
|
await loadRequirementDisplayTotal();
|
||||||
await reloadTable();
|
await reloadTable();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleCreateSubmitted() {
|
async function handleCreateSubmitted() {
|
||||||
createVisible.value = false;
|
createVisible.value = false;
|
||||||
|
await loadRequirementDisplayTotal();
|
||||||
await reloadTable();
|
await reloadTable();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -828,6 +830,7 @@ async function handleDetailSubmitted() {
|
|||||||
|
|
||||||
async function handleSplitSubmitted() {
|
async function handleSplitSubmitted() {
|
||||||
splitVisible.value = false;
|
splitVisible.value = false;
|
||||||
|
await loadRequirementDisplayTotal();
|
||||||
await reloadTable();
|
await reloadTable();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -855,8 +858,10 @@ watch(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await Promise.all([loadMembers(), loadTreeData()]);
|
treeData.value = [];
|
||||||
await Promise.all([loadAllowedTransitionsForAll(), loadRequirementDisplayTotal()]);
|
allowedTransitionsMap.value = new Map();
|
||||||
|
pagination.total = 0;
|
||||||
|
await Promise.all([loadMembers(), loadRequirementDisplayTotal()]);
|
||||||
},
|
},
|
||||||
{ immediate: true }
|
{ immediate: true }
|
||||||
);
|
);
|
||||||
@@ -876,6 +881,7 @@ Promise.all([loadStatusOptions()]);
|
|||||||
:member-options="memberUserOptions"
|
:member-options="memberUserOptions"
|
||||||
:category-dict-code="RDMS_REQ_CATEGORY_DICT_CODE"
|
:category-dict-code="RDMS_REQ_CATEGORY_DICT_CODE"
|
||||||
:priority-dict-code="RDMS_REQ_PRIORITY_DICT_CODE"
|
:priority-dict-code="RDMS_REQ_PRIORITY_DICT_CODE"
|
||||||
|
:status-options="statusOptions"
|
||||||
@reset="handleResetSearch"
|
@reset="handleResetSearch"
|
||||||
@search="handleSearch"
|
@search="handleSearch"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, h, onMounted, ref } from 'vue';
|
import { computed, h } from 'vue';
|
||||||
// import { RDMS_REQ_SOURCE_TYPE_DICT_CODE } from '@/constants/dict';
|
|
||||||
import { fetchGetProjectRequirementStatusDict } from '@/service/api';
|
|
||||||
// import { useDict } from '@/hooks/business/dict';
|
// import { useDict } from '@/hooks/business/dict';
|
||||||
import TableSearchFields from '@/components/custom/table-search-fields.vue';
|
import TableSearchFields from '@/components/custom/table-search-fields.vue';
|
||||||
import MemberSelectOption from './member-select-option.vue';
|
import MemberSelectOption from './member-select-option.vue';
|
||||||
@@ -18,6 +16,7 @@ interface Props {
|
|||||||
memberOptions: MemberUserOption[];
|
memberOptions: MemberUserOption[];
|
||||||
categoryDictCode: string;
|
categoryDictCode: string;
|
||||||
priorityDictCode: string;
|
priorityDictCode: string;
|
||||||
|
statusOptions: Array<{ label: string; value: string }>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = defineProps<Props>();
|
const props = defineProps<Props>();
|
||||||
@@ -31,7 +30,6 @@ const emit = defineEmits<Emits>();
|
|||||||
|
|
||||||
const model = defineModel<Api.Project.ProjectRequirementSearchParams>('model', { required: true });
|
const model = defineModel<Api.Project.ProjectRequirementSearchParams>('model', { required: true });
|
||||||
|
|
||||||
const requirementStatusOptions = ref<Array<{ label: string; value: string }>>([]);
|
|
||||||
// const { enabledDictData: sourceTypeDictData } = useDict(RDMS_REQ_SOURCE_TYPE_DICT_CODE);
|
// const { enabledDictData: sourceTypeDictData } = useDict(RDMS_REQ_SOURCE_TYPE_DICT_CODE);
|
||||||
// const sourceTypeOptions = computed(() => {
|
// const sourceTypeOptions = computed(() => {
|
||||||
// return sourceTypeDictData.value.map(item => ({
|
// return sourceTypeDictData.value.map(item => ({
|
||||||
@@ -55,24 +53,6 @@ function renderMemberOption(option: { label: string; value: string | number; rol
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadStatusOptions() {
|
|
||||||
const { error, data } = await fetchGetProjectRequirementStatusDict();
|
|
||||||
|
|
||||||
if (error || !data) {
|
|
||||||
requirementStatusOptions.value = [];
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
requirementStatusOptions.value = data.map(item => ({
|
|
||||||
label: item.statusName,
|
|
||||||
value: item.statusCode
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
await loadStatusOptions();
|
|
||||||
});
|
|
||||||
|
|
||||||
const fields = computed(() => [
|
const fields = computed(() => [
|
||||||
{
|
{
|
||||||
key: 'title',
|
key: 'title',
|
||||||
@@ -92,7 +72,7 @@ const fields = computed(() => [
|
|||||||
label: '状态',
|
label: '状态',
|
||||||
type: 'select' as const,
|
type: 'select' as const,
|
||||||
placeholder: '筛选状态',
|
placeholder: '筛选状态',
|
||||||
options: requirementStatusOptions.value
|
options: props.statusOptions
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'category',
|
key: 'category',
|
||||||
@@ -120,6 +100,7 @@ const fields = computed(() => [
|
|||||||
type: 'select' as const,
|
type: 'select' as const,
|
||||||
placeholder: '筛选负责人',
|
placeholder: '筛选负责人',
|
||||||
options: memberSelectOptions.value,
|
options: memberSelectOptions.value,
|
||||||
|
filterable: true,
|
||||||
renderOption: renderMemberOption
|
renderOption: renderMemberOption
|
||||||
}
|
}
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import {
|
|||||||
fetchGetProjectReportApprovalPage,
|
fetchGetProjectReportApprovalPage,
|
||||||
fetchGetProjectTask,
|
fetchGetProjectTask,
|
||||||
fetchGetWeeklyReportApprovalPage,
|
fetchGetWeeklyReportApprovalPage,
|
||||||
|
fetchPerformanceSheetPage,
|
||||||
fetchRejectOvertimeApplication
|
fetchRejectOvertimeApplication
|
||||||
} from '@/service/api';
|
} from '@/service/api';
|
||||||
import { useAuthStore } from '@/store/modules/auth';
|
import { useAuthStore } from '@/store/modules/auth';
|
||||||
@@ -29,6 +30,9 @@ import TaskWorklogDialog from '@/views/project/project/execution/modules/task-wo
|
|||||||
import PersonalItemDetailDialog from '@/views/personal-center/my-item/modules/personal-item-detail-dialog.vue';
|
import PersonalItemDetailDialog from '@/views/personal-center/my-item/modules/personal-item-detail-dialog.vue';
|
||||||
import PersonalItemOperateDialog from '@/views/personal-center/my-item/modules/personal-item-operate-dialog.vue';
|
import PersonalItemOperateDialog from '@/views/personal-center/my-item/modules/personal-item-operate-dialog.vue';
|
||||||
import PersonalItemStatusActionDialog from '@/views/personal-center/my-item/modules/personal-item-status-action-dialog.vue';
|
import PersonalItemStatusActionDialog from '@/views/personal-center/my-item/modules/personal-item-status-action-dialog.vue';
|
||||||
|
import PerformanceActionDialog from '@/views/personal-center/my-performance/modules/performance-action-dialog.vue';
|
||||||
|
import PerformanceExcelEditorDrawer from '@/views/personal-center/my-performance/modules/performance-excel-editor-drawer.vue';
|
||||||
|
import { PerformancePermission } from '@/views/personal-center/my-performance/modules/performance-shared';
|
||||||
import OvertimeApplicationBatchDetailDialog from '@/views/personal-center/overtime-application/modules/overtime-application-batch-detail-dialog.vue';
|
import OvertimeApplicationBatchDetailDialog from '@/views/personal-center/overtime-application/modules/overtime-application-batch-detail-dialog.vue';
|
||||||
import OvertimeApplicationDetailDialog from '@/views/personal-center/overtime-application/modules/overtime-application-detail-dialog.vue';
|
import OvertimeApplicationDetailDialog from '@/views/personal-center/overtime-application/modules/overtime-application-detail-dialog.vue';
|
||||||
import WorkReportPrototypePageDialog from '@/views/personal-center/work-report/shared/components/prototype-page-dialog.vue';
|
import WorkReportPrototypePageDialog from '@/views/personal-center/work-report/shared/components/prototype-page-dialog.vue';
|
||||||
@@ -61,7 +65,8 @@ import IconMdiPlayCircleOutline from '~icons/mdi/play-circle-outline';
|
|||||||
|
|
||||||
type SortKey = 'created' | 'priority' | 'deadline';
|
type SortKey = 'created' | 'priority' | 'deadline';
|
||||||
type OvertimeApprovalActionType = 'approve' | 'reject';
|
type OvertimeApprovalActionType = 'approve' | 'reject';
|
||||||
type ApprovalBizType = 'overtime_application' | WorkReportType;
|
type PerformanceApprovalActionType = 'confirm' | 'reject';
|
||||||
|
type ApprovalBizType = 'overtime_application' | 'performance' | WorkReportType;
|
||||||
|
|
||||||
defineOptions({ name: 'WorkbenchTodoPanel' });
|
defineOptions({ name: 'WorkbenchTodoPanel' });
|
||||||
|
|
||||||
@@ -80,6 +85,11 @@ const { routerPushByKey } = useRouterPush();
|
|||||||
|
|
||||||
const authStore = useAuthStore();
|
const authStore = useAuthStore();
|
||||||
const currentUserId = computed(() => authStore.userInfo.userId || '');
|
const currentUserId = computed(() => authStore.userInfo.userId || '');
|
||||||
|
const buttonPermissions = computed(() => new Set(authStore.userInfo.buttons || []));
|
||||||
|
|
||||||
|
function hasButtonPermission(permission: string) {
|
||||||
|
return buttonPermissions.value.has(permission);
|
||||||
|
}
|
||||||
|
|
||||||
// 工时填报在工作台内弹层完成(不切路由),需广播给「我的工时」widget 重拉
|
// 工时填报在工作台内弹层完成(不切路由),需广播给「我的工时」widget 重拉
|
||||||
const { notify: notifyWorklogChanged } = useWorkbenchWorklogSignal();
|
const { notify: notifyWorklogChanged } = useWorkbenchWorklogSignal();
|
||||||
@@ -89,6 +99,7 @@ const { loading, refresh } = useWorkbenchRefresh(async () => {
|
|||||||
loadMyTaskItems(),
|
loadMyTaskItems(),
|
||||||
loadPersonalTodoItems(),
|
loadPersonalTodoItems(),
|
||||||
loadOvertimeApprovalItems(),
|
loadOvertimeApprovalItems(),
|
||||||
|
loadPerformanceApprovalItems(),
|
||||||
loadWorkReportApprovalItems()
|
loadWorkReportApprovalItems()
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
@@ -130,9 +141,30 @@ const approvalBizTabs: Array<{ key: ApprovalBizType; label: string }> = [
|
|||||||
{ key: 'weekly', label: '周报' },
|
{ key: 'weekly', label: '周报' },
|
||||||
{ key: 'monthly', label: '月报' },
|
{ key: 'monthly', label: '月报' },
|
||||||
{ key: 'project', label: '项目半月报' },
|
{ key: 'project', label: '项目半月报' },
|
||||||
|
{ key: 'performance', label: '我的绩效' },
|
||||||
{ key: 'overtime_application', label: '加班申请' }
|
{ key: 'overtime_application', label: '加班申请' }
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const hasWorkReportApprovePermission = computed(() => hasButtonPermission('project:work-report:approve'));
|
||||||
|
const hasOvertimeApprovePermission = computed(() => hasButtonPermission('project:overtime-application:approve'));
|
||||||
|
const hasPerformanceApprovePermission = computed(
|
||||||
|
() =>
|
||||||
|
hasButtonPermission(PerformancePermission.SheetConfirm) && hasButtonPermission(PerformancePermission.SheetReject)
|
||||||
|
);
|
||||||
|
const visibleApprovalBizTabs = computed(() =>
|
||||||
|
approvalBizTabs.filter(tab => {
|
||||||
|
if (tab.key === 'performance') {
|
||||||
|
return hasPerformanceApprovePermission.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tab.key === 'overtime_application') {
|
||||||
|
return hasOvertimeApprovePermission.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return hasWorkReportApprovePermission.value;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
const myTaskItems = ref<WorkbenchTodoItem[]>([]);
|
const myTaskItems = ref<WorkbenchTodoItem[]>([]);
|
||||||
// 保留任务原始行,供操作图标按 availableActions 渲染并取 projectId / executionId 调状态变更接口
|
// 保留任务原始行,供操作图标按 availableActions 渲染并取 projectId / executionId 调状态变更接口
|
||||||
const myTaskRows = ref<Api.Project.MyTaskItem[]>([]);
|
const myTaskRows = ref<Api.Project.MyTaskItem[]>([]);
|
||||||
@@ -141,6 +173,8 @@ const personalTodoItems = ref<WorkbenchTodoItem[]>([]);
|
|||||||
const personalItemRows = ref<Api.PersonalItem.PersonalItem[]>([]);
|
const personalItemRows = ref<Api.PersonalItem.PersonalItem[]>([]);
|
||||||
const overtimeApprovalItems = ref<WorkbenchTodoItem[]>([]);
|
const overtimeApprovalItems = ref<WorkbenchTodoItem[]>([]);
|
||||||
const overtimeApprovalRows = ref<Api.OvertimeApplication.OvertimeApplication[]>([]);
|
const overtimeApprovalRows = ref<Api.OvertimeApplication.OvertimeApplication[]>([]);
|
||||||
|
const performanceApprovalItems = ref<WorkbenchTodoItem[]>([]);
|
||||||
|
const performanceApprovalRows = ref<Api.Performance.Sheet.Sheet[]>([]);
|
||||||
const workReportApprovalItems = ref<WorkbenchTodoItem[]>([]);
|
const workReportApprovalItems = ref<WorkbenchTodoItem[]>([]);
|
||||||
const weeklyApprovalRows = ref<Api.WorkReport.Weekly.WeeklyReport[]>([]);
|
const weeklyApprovalRows = ref<Api.WorkReport.Weekly.WeeklyReport[]>([]);
|
||||||
const monthlyApprovalRows = ref<Api.WorkReport.Monthly.MonthlyReport[]>([]);
|
const monthlyApprovalRows = ref<Api.WorkReport.Monthly.MonthlyReport[]>([]);
|
||||||
@@ -150,6 +184,7 @@ const mergedItems = computed(() => [
|
|||||||
...myTaskItems.value,
|
...myTaskItems.value,
|
||||||
...personalTodoItems.value,
|
...personalTodoItems.value,
|
||||||
...overtimeApprovalItems.value,
|
...overtimeApprovalItems.value,
|
||||||
|
...performanceApprovalItems.value,
|
||||||
...workReportApprovalItems.value
|
...workReportApprovalItems.value
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -179,6 +214,10 @@ const batchSubmitting = ref(false);
|
|||||||
const workReportDetailVisible = ref(false);
|
const workReportDetailVisible = ref(false);
|
||||||
const currentWorkReport = ref<WorkReportRow | null>(null);
|
const currentWorkReport = ref<WorkReportRow | null>(null);
|
||||||
const currentWorkReportType = ref<WorkReportType>('weekly');
|
const currentWorkReportType = ref<WorkReportType>('weekly');
|
||||||
|
const performanceDetailVisible = ref(false);
|
||||||
|
const performanceActionVisible = ref(false);
|
||||||
|
const currentPerformanceSheet = ref<Api.Performance.Sheet.Sheet | null>(null);
|
||||||
|
const currentPerformanceActionType = ref<PerformanceApprovalActionType>('confirm');
|
||||||
|
|
||||||
// 批量审批选中状态(存原始加班申请 id,避免映射转换)
|
// 批量审批选中状态(存原始加班申请 id,避免映射转换)
|
||||||
const selectedOvertimeIds = ref<Set<string>>(new Set());
|
const selectedOvertimeIds = ref<Set<string>>(new Set());
|
||||||
@@ -191,6 +230,7 @@ function getApprovalCategoryLabel(bizType: ApprovalBizType) {
|
|||||||
if (bizType === 'weekly') return '周报';
|
if (bizType === 'weekly') return '周报';
|
||||||
if (bizType === 'monthly') return '月报';
|
if (bizType === 'monthly') return '月报';
|
||||||
if (bizType === 'project') return '项目半月报';
|
if (bizType === 'project') return '项目半月报';
|
||||||
|
if (bizType === 'performance') return '我的绩效';
|
||||||
if (bizType === 'overtime_application') return '加班申请';
|
if (bizType === 'overtime_application') return '加班申请';
|
||||||
return '待审批';
|
return '待审批';
|
||||||
}
|
}
|
||||||
@@ -464,6 +504,7 @@ const filteredItems = computed(() => {
|
|||||||
const approvalBizTabCounts = computed(() => {
|
const approvalBizTabCounts = computed(() => {
|
||||||
const counts: Record<ApprovalBizType, number> = {
|
const counts: Record<ApprovalBizType, number> = {
|
||||||
overtime_application: 0,
|
overtime_application: 0,
|
||||||
|
performance: 0,
|
||||||
weekly: 0,
|
weekly: 0,
|
||||||
monthly: 0,
|
monthly: 0,
|
||||||
project: 0
|
project: 0
|
||||||
@@ -472,6 +513,12 @@ const approvalBizTabCounts = computed(() => {
|
|||||||
itemsInTab.value.forEach(item => {
|
itemsInTab.value.forEach(item => {
|
||||||
if (item.approvalBizType === 'overtime_application') {
|
if (item.approvalBizType === 'overtime_application') {
|
||||||
counts.overtime_application += 1;
|
counts.overtime_application += 1;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.approvalBizType === 'performance') {
|
||||||
|
counts.performance += 1;
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isWorkReportApprovalBizType(item.approvalBizType)) {
|
if (isWorkReportApprovalBizType(item.approvalBizType)) {
|
||||||
@@ -510,6 +557,17 @@ watch([activeTab, activeDeadlineFilter, activeSort], () => {
|
|||||||
currentPage.value = 1;
|
currentPage.value = 1;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
watch(
|
||||||
|
visibleApprovalBizTabs,
|
||||||
|
tabs => {
|
||||||
|
if (!tabs.length) return;
|
||||||
|
if (!tabs.some(tab => tab.key === activeApprovalBizType.value)) {
|
||||||
|
activeApprovalBizType.value = tabs[0].key;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
);
|
||||||
|
|
||||||
function handleSelectTab(key: WorkbenchTodoMainTab) {
|
function handleSelectTab(key: WorkbenchTodoMainTab) {
|
||||||
if (activeTab.value === key) return;
|
if (activeTab.value === key) return;
|
||||||
activeTab.value = key;
|
activeTab.value = key;
|
||||||
@@ -538,6 +596,11 @@ function handleClickItem(item: WorkbenchTodoItem) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (item.approvalBizType === 'performance') {
|
||||||
|
openPerformanceDetail(item);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (isWorkReportApprovalBizType(item.approvalBizType)) {
|
if (isWorkReportApprovalBizType(item.approvalBizType)) {
|
||||||
openWorkReportDetail(item);
|
openWorkReportDetail(item);
|
||||||
return;
|
return;
|
||||||
@@ -568,6 +631,14 @@ function findOvertimeApprovalRow(item: WorkbenchTodoItem) {
|
|||||||
return overtimeApprovalRows.value.find(row => row.id === item.approvalBizId) || null;
|
return overtimeApprovalRows.value.find(row => row.id === item.approvalBizId) || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function findPerformanceApprovalRow(item: WorkbenchTodoItem) {
|
||||||
|
if (!item.approvalBizId) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return performanceApprovalRows.value.find(row => row.id === item.approvalBizId) || null;
|
||||||
|
}
|
||||||
|
|
||||||
function openOvertimeDetail(item: WorkbenchTodoItem) {
|
function openOvertimeDetail(item: WorkbenchTodoItem) {
|
||||||
const row = findOvertimeApprovalRow(item);
|
const row = findOvertimeApprovalRow(item);
|
||||||
if (!row) return;
|
if (!row) return;
|
||||||
@@ -576,6 +647,21 @@ function openOvertimeDetail(item: WorkbenchTodoItem) {
|
|||||||
overtimeDetailVisible.value = true;
|
overtimeDetailVisible.value = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function openPerformanceDetail(item: WorkbenchTodoItem) {
|
||||||
|
const row = findPerformanceApprovalRow(item);
|
||||||
|
if (!row) return;
|
||||||
|
|
||||||
|
currentPerformanceSheet.value = row;
|
||||||
|
performanceDetailVisible.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function openCurrentPerformanceAction(actionType: PerformanceApprovalActionType) {
|
||||||
|
if (!currentPerformanceSheet.value) return;
|
||||||
|
|
||||||
|
currentPerformanceActionType.value = actionType;
|
||||||
|
performanceActionVisible.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
function findWorkReportApprovalRow(item: WorkbenchTodoItem) {
|
function findWorkReportApprovalRow(item: WorkbenchTodoItem) {
|
||||||
if (!item.approvalBizId || !isWorkReportApprovalBizType(item.approvalBizType)) {
|
if (!item.approvalBizId || !isWorkReportApprovalBizType(item.approvalBizType)) {
|
||||||
return null;
|
return null;
|
||||||
@@ -627,6 +713,12 @@ async function handleWorkReportSubmitted() {
|
|||||||
await loadWorkReportApprovalItems();
|
await loadWorkReportApprovalItems();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handlePerformanceActionSubmitted() {
|
||||||
|
performanceActionVisible.value = false;
|
||||||
|
performanceDetailVisible.value = false;
|
||||||
|
await loadPerformanceApprovalItems();
|
||||||
|
}
|
||||||
|
|
||||||
// 优先级角标用字典 label 原样回显(rdms_req_priority:P0~P3),不翻译成高/中/低
|
// 优先级角标用字典 label 原样回显(rdms_req_priority:P0~P3),不翻译成高/中/低
|
||||||
const { getLabel: getPriorityLabel } = useDict(RDMS_REQ_PRIORITY_DICT_CODE);
|
const { getLabel: getPriorityLabel } = useDict(RDMS_REQ_PRIORITY_DICT_CODE);
|
||||||
|
|
||||||
@@ -774,6 +866,12 @@ async function handleBatchActionSubmit(payload: { actionType: OvertimeApprovalAc
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function loadOvertimeApprovalItems() {
|
async function loadOvertimeApprovalItems() {
|
||||||
|
if (!hasOvertimeApprovePermission.value) {
|
||||||
|
overtimeApprovalRows.value = [];
|
||||||
|
overtimeApprovalItems.value = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const { error, data } = await fetchGetOvertimeApplicationApprovalPage({
|
const { error, data } = await fetchGetOvertimeApplicationApprovalPage({
|
||||||
pageNo: 1,
|
pageNo: 1,
|
||||||
pageSize: 20,
|
pageSize: 20,
|
||||||
@@ -809,6 +907,42 @@ async function loadOvertimeApprovalItems() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function loadPerformanceApprovalItems() {
|
||||||
|
if (!hasPerformanceApprovePermission.value) {
|
||||||
|
performanceApprovalRows.value = [];
|
||||||
|
performanceApprovalItems.value = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { error, data } = await fetchPerformanceSheetPage({
|
||||||
|
pageNo: 1,
|
||||||
|
pageSize: 20,
|
||||||
|
statusCode: 'sent'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error || !data) {
|
||||||
|
performanceApprovalRows.value = [];
|
||||||
|
performanceApprovalItems.value = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
performanceApprovalRows.value = data.list;
|
||||||
|
performanceApprovalItems.value = buildWorkbenchTodoItems(
|
||||||
|
data.list.map(item => ({
|
||||||
|
id: `performance-${item.id}`,
|
||||||
|
category: 'approval',
|
||||||
|
categoryLabel: '我的绩效',
|
||||||
|
title: `${item.periodMonth} 绩效待确认`,
|
||||||
|
createdTime: item.sentTime || item.createTime || item.updateTime || '',
|
||||||
|
deadline: item.sentTime || item.createTime || item.updateTime || null,
|
||||||
|
source: `我的绩效 · ${item.managerName || '--'}`,
|
||||||
|
priority: 'mid',
|
||||||
|
approvalBizType: 'performance',
|
||||||
|
approvalBizId: item.id
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function buildWorkReportApprovalItems<T extends WorkReportRow>(
|
function buildWorkReportApprovalItems<T extends WorkReportRow>(
|
||||||
bizType: WorkReportType,
|
bizType: WorkReportType,
|
||||||
rows: T[]
|
rows: T[]
|
||||||
@@ -832,6 +966,14 @@ function buildWorkReportApprovalItems<T extends WorkReportRow>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function loadWorkReportApprovalItems() {
|
async function loadWorkReportApprovalItems() {
|
||||||
|
if (!hasWorkReportApprovePermission.value) {
|
||||||
|
weeklyApprovalRows.value = [];
|
||||||
|
monthlyApprovalRows.value = [];
|
||||||
|
projectApprovalRows.value = [];
|
||||||
|
workReportApprovalItems.value = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const [weeklyResult, monthlyResult, projectResult] = await Promise.all([
|
const [weeklyResult, monthlyResult, projectResult] = await Promise.all([
|
||||||
fetchGetWeeklyReportApprovalPage({
|
fetchGetWeeklyReportApprovalPage({
|
||||||
pageNo: 1,
|
pageNo: 1,
|
||||||
@@ -938,7 +1080,7 @@ onActivated(refresh);
|
|||||||
</div>
|
</div>
|
||||||
<div v-else class="workbench-todo__filters-left">
|
<div v-else class="workbench-todo__filters-left">
|
||||||
<button
|
<button
|
||||||
v-for="tab in approvalBizTabs"
|
v-for="tab in visibleApprovalBizTabs"
|
||||||
:key="tab.key"
|
:key="tab.key"
|
||||||
type="button"
|
type="button"
|
||||||
class="workbench-todo__filter"
|
class="workbench-todo__filter"
|
||||||
@@ -1076,6 +1218,13 @@ onActivated(refresh);
|
|||||||
</ElButton>
|
</ElButton>
|
||||||
</ElTooltip>
|
</ElTooltip>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-else-if="item.approvalBizType === 'performance'" class="workbench-todo__actions" @click.stop>
|
||||||
|
<ElTooltip content="查看绩效">
|
||||||
|
<ElButton link type="primary" class="workbench-todo__action-btn" @click="openPerformanceDetail(item)">
|
||||||
|
<IconMdiEyeOutline class="text-15px" />
|
||||||
|
</ElButton>
|
||||||
|
</ElTooltip>
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
v-else-if="isWorkReportApprovalBizType(item.approvalBizType)"
|
v-else-if="isWorkReportApprovalBizType(item.approvalBizType)"
|
||||||
class="workbench-todo__actions"
|
class="workbench-todo__actions"
|
||||||
@@ -1212,6 +1361,22 @@ onActivated(refresh);
|
|||||||
:row-data="currentWorkReport"
|
:row-data="currentWorkReport"
|
||||||
@submitted="handleWorkReportSubmitted"
|
@submitted="handleWorkReportSubmitted"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<PerformanceExcelEditorDrawer
|
||||||
|
v-model:visible="performanceDetailVisible"
|
||||||
|
:row-data="currentPerformanceSheet"
|
||||||
|
mode="view"
|
||||||
|
show-approval-footer
|
||||||
|
@start-approval="openCurrentPerformanceAction('confirm')"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<PerformanceActionDialog
|
||||||
|
v-model:visible="performanceActionVisible"
|
||||||
|
:row-data="currentPerformanceSheet"
|
||||||
|
:action-type="currentPerformanceActionType"
|
||||||
|
selectable-action-type
|
||||||
|
@submitted="handlePerformanceActionSubmitted"
|
||||||
|
/>
|
||||||
</WorkbenchModuleCard>
|
</WorkbenchModuleCard>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user