feat(performance): 优化绩效模板管理功能
- 新增绩效模板上传范围接口,限制组织负责人权限 - 实现单人上传和批量导入两种模板上传方式 - 添加模板年份、员工筛选等搜索条件 - 集成文件上传验证和批量导入结果展示 - 重构模板对话框UI布局,提升用户体验 - 更新API类型定义和数据转换逻辑
This commit is contained in:
@@ -19,6 +19,8 @@ type TemplateResponse = Omit<Api.Performance.Template.Template, 'id' | 'fileId'
|
|||||||
id: StringIdResponse;
|
id: StringIdResponse;
|
||||||
fileId: StringIdResponse;
|
fileId: StringIdResponse;
|
||||||
uploadUserId: StringIdResponse;
|
uploadUserId: StringIdResponse;
|
||||||
|
employeeId?: StringIdResponse | null;
|
||||||
|
employeeDeptId?: StringIdResponse | null;
|
||||||
activeFlag?: boolean | number | string | null;
|
activeFlag?: boolean | number | string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -27,6 +29,34 @@ type TemplatePageResponse = {
|
|||||||
list: TemplateResponse[];
|
list: TemplateResponse[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type TemplateUploadScopeResponse = {
|
||||||
|
orgLeader?: boolean | number | string | null;
|
||||||
|
employeeCount?: number | string | null;
|
||||||
|
employees?: Array<{
|
||||||
|
employeeId: StringIdResponse;
|
||||||
|
employeeName: string;
|
||||||
|
employeeDeptId?: StringIdResponse | null;
|
||||||
|
employeeDeptName?: string | null;
|
||||||
|
}> | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
type TemplateBatchImportResponse = {
|
||||||
|
successCount?: number | string | null;
|
||||||
|
failureCount?: number | string | null;
|
||||||
|
successes?: Array<{
|
||||||
|
employeeId: StringIdResponse;
|
||||||
|
employeeName: string;
|
||||||
|
templateId: StringIdResponse;
|
||||||
|
sourceFileName: string;
|
||||||
|
sheetName?: string | null;
|
||||||
|
}> | null;
|
||||||
|
failures?: Array<{
|
||||||
|
sourceFileName: string;
|
||||||
|
sheetName?: string | null;
|
||||||
|
reason: string;
|
||||||
|
}> | null;
|
||||||
|
};
|
||||||
|
|
||||||
type SheetResponse = Omit<
|
type SheetResponse = Omit<
|
||||||
Api.Performance.Sheet.Sheet,
|
Api.Performance.Sheet.Sheet,
|
||||||
'id' | 'employeeId' | 'employeeDeptId' | 'managerId' | 'templateId' | 'fileId'
|
'id' | 'employeeId' | 'employeeDeptId' | 'managerId' | 'templateId' | 'fileId'
|
||||||
@@ -135,6 +165,8 @@ function normalizeTemplate(response: TemplateResponse): Api.Performance.Template
|
|||||||
...response,
|
...response,
|
||||||
id: normalizeStringId(response.id),
|
id: normalizeStringId(response.id),
|
||||||
fileId: normalizeStringId(response.fileId),
|
fileId: normalizeStringId(response.fileId),
|
||||||
|
employeeId: normalizeNullableStringId(response.employeeId),
|
||||||
|
employeeDeptId: normalizeNullableStringId(response.employeeDeptId),
|
||||||
uploadUserId: normalizeStringId(response.uploadUserId),
|
uploadUserId: normalizeStringId(response.uploadUserId),
|
||||||
activeFlag: normalizeBooleanFlag(response.activeFlag),
|
activeFlag: normalizeBooleanFlag(response.activeFlag),
|
||||||
remark: response.remark ?? null,
|
remark: response.remark ?? null,
|
||||||
@@ -142,6 +174,40 @@ function normalizeTemplate(response: TemplateResponse): Api.Performance.Template
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeTemplateUploadScope(response: TemplateUploadScopeResponse): Api.Performance.Template.UploadScope {
|
||||||
|
return {
|
||||||
|
orgLeader: normalizeBooleanFlag(response.orgLeader),
|
||||||
|
employeeCount: normalizeTotal(response.employeeCount),
|
||||||
|
employees: (response.employees || []).map(item => ({
|
||||||
|
employeeId: normalizeStringId(item.employeeId),
|
||||||
|
employeeName: item.employeeName,
|
||||||
|
employeeDeptId: normalizeNullableStringId(item.employeeDeptId),
|
||||||
|
employeeDeptName: item.employeeDeptName ?? null
|
||||||
|
}))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeTemplateBatchImportResult(
|
||||||
|
response: TemplateBatchImportResponse
|
||||||
|
): Api.Performance.Template.BatchImportResult {
|
||||||
|
return {
|
||||||
|
successCount: normalizeTotal(response.successCount),
|
||||||
|
failureCount: normalizeTotal(response.failureCount),
|
||||||
|
successes: (response.successes || []).map(item => ({
|
||||||
|
employeeId: normalizeStringId(item.employeeId),
|
||||||
|
employeeName: item.employeeName,
|
||||||
|
templateId: normalizeStringId(item.templateId),
|
||||||
|
sourceFileName: item.sourceFileName,
|
||||||
|
sheetName: item.sheetName ?? null
|
||||||
|
})),
|
||||||
|
failures: (response.failures || []).map(item => ({
|
||||||
|
sourceFileName: item.sourceFileName,
|
||||||
|
sheetName: item.sheetName ?? null,
|
||||||
|
reason: item.reason
|
||||||
|
}))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function normalizeSheet(response: SheetResponse): Api.Performance.Sheet.Sheet {
|
function normalizeSheet(response: SheetResponse): Api.Performance.Sheet.Sheet {
|
||||||
return {
|
return {
|
||||||
...response,
|
...response,
|
||||||
@@ -304,15 +370,21 @@ function createTemplateQuery(params: Api.Performance.Template.SearchParams = {})
|
|||||||
query.append('pageNo', String(params.pageNo ?? 1));
|
query.append('pageNo', String(params.pageNo ?? 1));
|
||||||
query.append('pageSize', String(params.pageSize ?? 10));
|
query.append('pageSize', String(params.pageSize ?? 10));
|
||||||
appendValue(query, 'templateName', params.templateName);
|
appendValue(query, 'templateName', params.templateName);
|
||||||
|
appendValue(query, 'employeeId', params.employeeId);
|
||||||
|
appendValue(query, 'employeeName', params.employeeName);
|
||||||
|
appendValue(query, 'templateYear', params.templateYear);
|
||||||
|
appendValue(query, 'uploadUserName', params.uploadUserName);
|
||||||
appendValue(query, 'activeFlag', params.activeFlag);
|
appendValue(query, 'activeFlag', params.activeFlag);
|
||||||
|
|
||||||
return query.toString();
|
return query.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchPerformanceTemplateCurrent() {
|
export async function fetchPerformanceTemplateCurrent(employeeId?: string | null) {
|
||||||
|
const query = new URLSearchParams();
|
||||||
|
appendValue(query, 'employeeId', employeeId);
|
||||||
const result = await request<TemplateResponse | null>({
|
const result = await request<TemplateResponse | null>({
|
||||||
...safeJsonRequestConfig,
|
...safeJsonRequestConfig,
|
||||||
url: `${TEMPLATE_PREFIX}/current`,
|
url: query.toString() ? `${TEMPLATE_PREFIX}/current?${query.toString()}` : `${TEMPLATE_PREFIX}/current`,
|
||||||
method: 'get'
|
method: 'get'
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -335,10 +407,20 @@ export async function fetchPerformanceTemplatePage(params: Api.Performance.Templ
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function uploadPerformanceTemplate(data: Api.Performance.Template.UploadParams) {
|
export async function fetchPerformanceTemplateUploadScope() {
|
||||||
|
const result = await request<TemplateUploadScopeResponse>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${TEMPLATE_PREFIX}/upload-scope`,
|
||||||
|
method: 'get'
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapServiceResult(result as ServiceRequestResult<TemplateUploadScopeResponse>, normalizeTemplateUploadScope);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function uploadSinglePerformanceTemplate(data: Api.Performance.Template.SingleUploadParams) {
|
||||||
const result = await request<StringIdResponse>({
|
const result = await request<StringIdResponse>({
|
||||||
...safeJsonRequestConfig,
|
...safeJsonRequestConfig,
|
||||||
url: `${TEMPLATE_PREFIX}/upload`,
|
url: `${TEMPLATE_PREFIX}/upload-single`,
|
||||||
method: 'post',
|
method: 'post',
|
||||||
data
|
data
|
||||||
});
|
});
|
||||||
@@ -346,6 +428,20 @@ export async function uploadPerformanceTemplate(data: Api.Performance.Template.U
|
|||||||
return mapServiceResult(result as ServiceRequestResult<StringIdResponse>, normalizeStringId);
|
return mapServiceResult(result as ServiceRequestResult<StringIdResponse>, normalizeStringId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function batchImportPerformanceTemplates(data: Api.Performance.Template.BatchImportParams) {
|
||||||
|
const result = await request<TemplateBatchImportResponse>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${TEMPLATE_PREFIX}/batch-import`,
|
||||||
|
method: 'post',
|
||||||
|
data
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapServiceResult(
|
||||||
|
result as ServiceRequestResult<TemplateBatchImportResponse>,
|
||||||
|
normalizeTemplateBatchImportResult
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function activatePerformanceTemplate(id: string) {
|
export function activatePerformanceTemplate(id: string) {
|
||||||
return request<boolean>({
|
return request<boolean>({
|
||||||
...safeJsonRequestConfig,
|
...safeJsonRequestConfig,
|
||||||
|
|||||||
63
src/typings/api/performance.d.ts
vendored
63
src/typings/api/performance.d.ts
vendored
@@ -32,6 +32,11 @@ declare namespace Api {
|
|||||||
templateName: string;
|
templateName: string;
|
||||||
fileId: string;
|
fileId: string;
|
||||||
fileName: string;
|
fileName: string;
|
||||||
|
employeeId?: string | null;
|
||||||
|
employeeName?: string | null;
|
||||||
|
employeeDeptId?: string | null;
|
||||||
|
employeeDeptName?: string | null;
|
||||||
|
templateYear?: number | null;
|
||||||
versionNo: number;
|
versionNo: number;
|
||||||
activeFlag: boolean;
|
activeFlag: boolean;
|
||||||
uploadUserId: string;
|
uploadUserId: string;
|
||||||
@@ -44,17 +49,67 @@ declare namespace Api {
|
|||||||
type SearchParams = CommonType.RecordNullable<
|
type SearchParams = CommonType.RecordNullable<
|
||||||
Common.PageParams & {
|
Common.PageParams & {
|
||||||
templateName: string;
|
templateName: string;
|
||||||
|
employeeId: string;
|
||||||
|
employeeName: string;
|
||||||
|
templateYear: number;
|
||||||
|
uploadUserName: string;
|
||||||
activeFlag: boolean;
|
activeFlag: boolean;
|
||||||
}
|
}
|
||||||
>;
|
>;
|
||||||
|
|
||||||
interface UploadParams {
|
interface ScopeEmployee {
|
||||||
|
employeeId: string;
|
||||||
|
employeeName: string;
|
||||||
|
employeeDeptId?: string | null;
|
||||||
|
employeeDeptName?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UploadScope {
|
||||||
|
orgLeader: boolean;
|
||||||
|
employeeCount: number;
|
||||||
|
employees: ScopeEmployee[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SingleUploadParams {
|
||||||
|
employeeId: string;
|
||||||
templateName: string;
|
templateName: string;
|
||||||
fileId: string;
|
templateYear?: number | null;
|
||||||
fileName: string;
|
sourceFileId: string;
|
||||||
activeFlag?: boolean | null;
|
sourceFileName: string;
|
||||||
remark?: string | null;
|
remark?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface BatchImportFileItem {
|
||||||
|
sourceFileId: string;
|
||||||
|
sourceFileName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BatchImportParams {
|
||||||
|
templateYear?: number | null;
|
||||||
|
remark?: string | null;
|
||||||
|
files: BatchImportFileItem[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BatchImportSuccessItem {
|
||||||
|
employeeId: string;
|
||||||
|
employeeName: string;
|
||||||
|
templateId: string;
|
||||||
|
sourceFileName: string;
|
||||||
|
sheetName?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BatchImportFailureItem {
|
||||||
|
sourceFileName: string;
|
||||||
|
sheetName?: string | null;
|
||||||
|
reason: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BatchImportResult {
|
||||||
|
successCount: number;
|
||||||
|
failureCount: number;
|
||||||
|
successes: BatchImportSuccessItem[];
|
||||||
|
failures: BatchImportFailureItem[];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
namespace Sheet {
|
namespace Sheet {
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
fetchGetMySubordinateTree,
|
fetchGetMySubordinateTree,
|
||||||
fetchPerformanceSheetPage,
|
fetchPerformanceSheetPage,
|
||||||
fetchPerformanceSheetStatusDict,
|
fetchPerformanceSheetStatusDict,
|
||||||
|
fetchPerformanceTemplateUploadScope,
|
||||||
fetchTeamPerformanceSummary,
|
fetchTeamPerformanceSummary,
|
||||||
formatToYYYYMM,
|
formatToYYYYMM,
|
||||||
resendPerformanceSheet,
|
resendPerformanceSheet,
|
||||||
@@ -111,6 +112,8 @@ const deptOptions = ref<Array<{ label: string; value: string }>>([]);
|
|||||||
const directSubordinateOptions = ref<Array<{ label: string; value: string }>>([]);
|
const directSubordinateOptions = ref<Array<{ label: string; value: string }>>([]);
|
||||||
const statusOptions = ref<Array<{ label: string; value: string }>>([]);
|
const statusOptions = ref<Array<{ label: string; value: string }>>([]);
|
||||||
const currentDeptId = ref<string>('');
|
const currentDeptId = ref<string>('');
|
||||||
|
const templateUploadScope = ref<Api.Performance.Template.UploadScope | null>(null);
|
||||||
|
const templateUploadScopeLoading = ref(false);
|
||||||
|
|
||||||
const templateVisible = ref(false);
|
const templateVisible = ref(false);
|
||||||
const excelVisible = ref(false);
|
const excelVisible = ref(false);
|
||||||
@@ -144,9 +147,10 @@ const canSignOff = computed(
|
|||||||
() => hasAuth(PerformancePermission.SheetImport) && hasAuth(PerformancePermission.SheetSignOff)
|
() => hasAuth(PerformancePermission.SheetImport) && hasAuth(PerformancePermission.SheetSignOff)
|
||||||
);
|
);
|
||||||
const canExport = computed(() => hasAuth(PerformancePermission.SheetExport));
|
const canExport = computed(() => hasAuth(PerformancePermission.SheetExport));
|
||||||
const canManageTemplate = computed(
|
const hasTemplatePermission = computed(
|
||||||
() => hasAuth(PerformancePermission.TemplateQuery) || hasAuth(PerformancePermission.TemplateUpdate)
|
() => hasAuth(PerformancePermission.TemplateQuery) || hasAuth(PerformancePermission.TemplateUpdate)
|
||||||
);
|
);
|
||||||
|
const canManageTemplate = computed(() => hasTemplatePermission.value && Boolean(templateUploadScope.value?.orgLeader));
|
||||||
const isTeamMode = computed(() => teamViewMode.value === 'team');
|
const isTeamMode = computed(() => teamViewMode.value === 'team');
|
||||||
const allSubordinateUserIds = computed(() => collectSubordinateUserIds(subordinateTree.value));
|
const allSubordinateUserIds = computed(() => collectSubordinateUserIds(subordinateTree.value));
|
||||||
const subordinateOptions = computed(() => {
|
const subordinateOptions = computed(() => {
|
||||||
@@ -656,6 +660,19 @@ async function loadStatusOptions() {
|
|||||||
statusOptions.value = getPerformanceStatusOptions(statusDict);
|
statusOptions.value = getPerformanceStatusOptions(statusDict);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function loadTemplateUploadScope() {
|
||||||
|
if (!hasTemplatePermission.value) {
|
||||||
|
templateUploadScope.value = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
templateUploadScopeLoading.value = true;
|
||||||
|
const { error, data: scopeData } = await fetchPerformanceTemplateUploadScope();
|
||||||
|
templateUploadScopeLoading.value = false;
|
||||||
|
|
||||||
|
templateUploadScope.value = error || !scopeData ? null : scopeData;
|
||||||
|
}
|
||||||
|
|
||||||
async function handleTeamViewModeChange(mode: TeamViewMode) {
|
async function handleTeamViewModeChange(mode: TeamViewMode) {
|
||||||
teamViewMode.value = mode;
|
teamViewMode.value = mode;
|
||||||
|
|
||||||
@@ -694,7 +711,12 @@ watch(excelVisible, isVisible => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await Promise.all([loadDeptOptions(), loadDirectSubordinateOptions(), loadStatusOptions()]);
|
await Promise.all([
|
||||||
|
loadDeptOptions(),
|
||||||
|
loadDirectSubordinateOptions(),
|
||||||
|
loadStatusOptions(),
|
||||||
|
loadTemplateUploadScope()
|
||||||
|
]);
|
||||||
|
|
||||||
if (canUseTeamDashboard.value) {
|
if (canUseTeamDashboard.value) {
|
||||||
await loadSubordinateTree();
|
await loadSubordinateTree();
|
||||||
@@ -773,7 +795,12 @@ onMounted(async () => {
|
|||||||
</template>
|
</template>
|
||||||
下签
|
下签
|
||||||
</ElButton>
|
</ElButton>
|
||||||
<ElButton v-if="isTeamMode && canManageTemplate" plain @click="templateVisible = true">
|
<ElButton
|
||||||
|
v-if="isTeamMode && canManageTemplate"
|
||||||
|
plain
|
||||||
|
:loading="templateUploadScopeLoading"
|
||||||
|
@click="templateVisible = true"
|
||||||
|
>
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<icon-mdi-file-cog-outline class="text-icon" />
|
<icon-mdi-file-cog-outline class="text-icon" />
|
||||||
</template>
|
</template>
|
||||||
@@ -821,6 +848,7 @@ onMounted(async () => {
|
|||||||
<PerformanceTemplateDialog
|
<PerformanceTemplateDialog
|
||||||
v-if="templateVisible"
|
v-if="templateVisible"
|
||||||
v-model:visible="templateVisible"
|
v-model:visible="templateVisible"
|
||||||
|
:scope="templateUploadScope"
|
||||||
@updated="reloadAfterMutation"
|
@updated="reloadAfterMutation"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -520,7 +520,12 @@ async function loadWorkbook() {
|
|||||||
let sourceFileId = '';
|
let sourceFileId = '';
|
||||||
|
|
||||||
if (isCreateMode.value) {
|
if (isCreateMode.value) {
|
||||||
const templateResult = await fetchPerformanceTemplateCurrent();
|
if (!createForm.employeeId) {
|
||||||
|
errorMessage.value = '请选择下属后再加载绩效模板';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const templateResult = await fetchPerformanceTemplateCurrent(createForm.employeeId);
|
||||||
if (templateResult.error || !templateResult.data) {
|
if (templateResult.error || !templateResult.data) {
|
||||||
errorMessage.value = '当前没有可用的绩效模板';
|
errorMessage.value = '当前没有可用的绩效模板';
|
||||||
return;
|
return;
|
||||||
@@ -533,7 +538,7 @@ async function loadWorkbook() {
|
|||||||
|
|
||||||
const [sheetResult, templateResult] = await Promise.all([
|
const [sheetResult, templateResult] = await Promise.all([
|
||||||
fetchPerformanceSheet(props.rowData.id),
|
fetchPerformanceSheet(props.rowData.id),
|
||||||
fetchPerformanceTemplateCurrent()
|
fetchPerformanceTemplateCurrent(props.rowData.employeeId)
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (sheetResult.error || !sheetResult.data) {
|
if (sheetResult.error || !sheetResult.data) {
|
||||||
|
|||||||
@@ -1,181 +1,254 @@
|
|||||||
<script setup lang="tsx">
|
<script setup lang="ts">
|
||||||
import { computed, reactive, ref, watch } from 'vue';
|
import { computed, reactive, ref, watch } from 'vue';
|
||||||
import type { UploadFile, UploadFiles } from 'element-plus';
|
import { ElMessageBox } from 'element-plus';
|
||||||
import { ElButton, ElTag } from 'element-plus';
|
import type { UploadFile, UploadFiles, UploadRawFile } from 'element-plus';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
import {
|
import {
|
||||||
activatePerformanceTemplate,
|
activatePerformanceTemplate,
|
||||||
|
batchImportPerformanceTemplates,
|
||||||
fetchPerformanceTemplatePage,
|
fetchPerformanceTemplatePage,
|
||||||
uploadFile,
|
uploadFile,
|
||||||
uploadPerformanceTemplate
|
uploadSinglePerformanceTemplate
|
||||||
} from '@/service/api';
|
} from '@/service/api';
|
||||||
import { useUIPaginatedTable } from '@/hooks/common/table';
|
|
||||||
import { useAuth } from '@/hooks/business/auth';
|
import { useAuth } from '@/hooks/business/auth';
|
||||||
import BusinessFormDialog from '@/components/custom/business-form-dialog.vue';
|
import BusinessFormDialog from '@/components/custom/business-form-dialog.vue';
|
||||||
import BusinessTableActionCell, { type BusinessTableAction } from '@/components/custom/business-table-action-cell';
|
|
||||||
import { PerformancePermission, formatDateTime } from './performance-shared';
|
import { PerformancePermission, formatDateTime } from './performance-shared';
|
||||||
|
|
||||||
defineOptions({ name: 'PerformanceTemplateDialog' });
|
defineOptions({ name: 'PerformanceTemplateDialog' });
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
scope?: Api.Performance.Template.UploadScope | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
scope: null
|
||||||
|
});
|
||||||
|
|
||||||
const visible = defineModel<boolean>('visible', { default: false });
|
const visible = defineModel<boolean>('visible', { default: false });
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
updated: [];
|
updated: [];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
type TemplatePageResponse = Awaited<ReturnType<typeof fetchPerformanceTemplatePage>>;
|
|
||||||
const { hasAuth } = useAuth();
|
const { hasAuth } = useAuth();
|
||||||
const canQueryTemplate = computed(() => hasAuth(PerformancePermission.TemplateQuery));
|
const canQueryTemplate = computed(() => hasAuth(PerformancePermission.TemplateQuery));
|
||||||
const canUpdateTemplate = computed(() => hasAuth(PerformancePermission.TemplateUpdate));
|
const canUpdateTemplate = computed(() => hasAuth(PerformancePermission.TemplateUpdate));
|
||||||
|
const employeeOptions = computed(() => props.scope?.employees || []);
|
||||||
|
const isOrgLeader = computed(() => Boolean(props.scope?.orgLeader));
|
||||||
|
const currentYear = dayjs().year();
|
||||||
|
|
||||||
const searchParams = reactive<Api.Performance.Template.SearchParams>({
|
const searchParams = reactive<Api.Performance.Template.SearchParams>({
|
||||||
pageNo: 1,
|
pageNo: 1,
|
||||||
pageSize: 10,
|
pageSize: 10,
|
||||||
templateName: undefined,
|
templateName: undefined,
|
||||||
|
employeeId: undefined,
|
||||||
|
employeeName: undefined,
|
||||||
|
templateYear: undefined,
|
||||||
|
uploadUserName: undefined,
|
||||||
activeFlag: undefined
|
activeFlag: undefined
|
||||||
});
|
});
|
||||||
|
|
||||||
const uploadForm = reactive({
|
const singleForm = reactive({
|
||||||
|
employeeId: '',
|
||||||
templateName: '',
|
templateName: '',
|
||||||
|
templateYear: currentYear,
|
||||||
remark: '',
|
remark: '',
|
||||||
activeFlag: true,
|
file: null as UploadRawFile | null
|
||||||
file: null as File | null
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const uploading = ref(false);
|
const batchForm = reactive({
|
||||||
|
templateYear: currentYear,
|
||||||
|
remark: '',
|
||||||
|
files: [] as UploadRawFile[]
|
||||||
|
});
|
||||||
|
|
||||||
|
const tableData = ref<Api.Performance.Template.Template[]>([]);
|
||||||
|
const total = ref(0);
|
||||||
|
const loading = ref(false);
|
||||||
|
const singleUploading = ref(false);
|
||||||
|
const batchUploading = ref(false);
|
||||||
const activatingId = ref('');
|
const activatingId = ref('');
|
||||||
|
|
||||||
const { columns, columnChecks, data, loading, getDataByPage, mobilePagination } = useUIPaginatedTable<
|
const singleFileName = computed(() => singleForm.file?.name || '');
|
||||||
TemplatePageResponse,
|
const batchFileNames = computed(() => batchForm.files.map(file => file.name));
|
||||||
Api.Performance.Template.Template
|
|
||||||
>({
|
|
||||||
paginationProps: {
|
|
||||||
currentPage: searchParams.pageNo,
|
|
||||||
pageSize: searchParams.pageSize
|
|
||||||
},
|
|
||||||
api: () => fetchPerformanceTemplatePage(searchParams),
|
|
||||||
transform: response => {
|
|
||||||
if (!response.error && response.data) {
|
|
||||||
return {
|
|
||||||
data: response.data.list,
|
|
||||||
pageNum: searchParams.pageNo ?? 1,
|
|
||||||
pageSize: searchParams.pageSize ?? 10,
|
|
||||||
total: response.data.total
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
function resetSingleForm() {
|
||||||
data: [],
|
Object.assign(singleForm, {
|
||||||
pageNum: 1,
|
employeeId: '',
|
||||||
pageSize: 10,
|
templateName: '',
|
||||||
total: 0
|
templateYear: currentYear,
|
||||||
};
|
remark: '',
|
||||||
},
|
file: null
|
||||||
onPaginationParamsChange: params => {
|
});
|
||||||
searchParams.pageNo = params.currentPage ?? 1;
|
}
|
||||||
searchParams.pageSize = params.pageSize ?? 10;
|
|
||||||
},
|
|
||||||
columns: () => [
|
|
||||||
{ prop: 'index', type: 'index', label: '序号', width: 64 },
|
|
||||||
{ prop: 'templateName', label: '模板名称', minWidth: 170, showOverflowTooltip: true },
|
|
||||||
// { prop: 'fileName', label: '文件名', minWidth: 200, showOverflowTooltip: true },
|
|
||||||
// { prop: 'versionNo', label: '版本', width: 80 },
|
|
||||||
{
|
|
||||||
prop: 'activeFlag',
|
|
||||||
label: '状态',
|
|
||||||
width: 100,
|
|
||||||
formatter: row => <ElTag type={row.activeFlag ? 'success' : 'info'}>{row.activeFlag ? '已启用' : '已禁用'}</ElTag>
|
|
||||||
},
|
|
||||||
{ prop: 'uploadUserName', label: '上传人', width: 110 },
|
|
||||||
{
|
|
||||||
prop: 'uploadTime',
|
|
||||||
label: '上传时间',
|
|
||||||
width: 180,
|
|
||||||
formatter: row => formatDateTime(row.uploadTime)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
prop: 'operate',
|
|
||||||
label: '操作',
|
|
||||||
width: 110,
|
|
||||||
align: 'center',
|
|
||||||
fixed: 'right',
|
|
||||||
formatter: row => <BusinessTableActionCell actions={getTemplateActions(row)} />
|
|
||||||
}
|
|
||||||
]
|
|
||||||
});
|
|
||||||
|
|
||||||
const selectedFileName = computed(() => uploadForm.file?.name || '');
|
function resetBatchForm() {
|
||||||
|
Object.assign(batchForm, {
|
||||||
|
templateYear: currentYear,
|
||||||
|
remark: '',
|
||||||
|
files: []
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async function loadTemplatePage(page = 1) {
|
async function loadTemplatePage(page = searchParams.pageNo ?? 1) {
|
||||||
if (!canQueryTemplate.value) return;
|
if (!canQueryTemplate.value) return;
|
||||||
await getDataByPage(page);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getTemplateActions(row: Api.Performance.Template.Template): BusinessTableAction[] {
|
searchParams.pageNo = page;
|
||||||
if (!canUpdateTemplate.value) return [];
|
loading.value = true;
|
||||||
|
const { error, data } = await fetchPerformanceTemplatePage(searchParams);
|
||||||
|
loading.value = false;
|
||||||
|
|
||||||
return [
|
if (error || !data) {
|
||||||
{
|
tableData.value = [];
|
||||||
key: 'activate',
|
total.value = 0;
|
||||||
label: row.activeFlag ? '已启用' : '启用',
|
|
||||||
buttonType: 'primary',
|
|
||||||
disabled: row.activeFlag || Boolean(activatingId.value),
|
|
||||||
onClick: () => handleActivate(row)
|
|
||||||
}
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleFileChange(file: UploadFile, _files: UploadFiles) {
|
|
||||||
const rawFile = file.raw;
|
|
||||||
|
|
||||||
if (!rawFile) return;
|
|
||||||
|
|
||||||
uploadForm.file = rawFile;
|
|
||||||
if (!uploadForm.templateName) {
|
|
||||||
uploadForm.templateName = rawFile.name.replace(/\.[^.]+$/u, '');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleUploadTemplate() {
|
|
||||||
if (!canUpdateTemplate.value) return;
|
|
||||||
|
|
||||||
if (!uploadForm.file) {
|
|
||||||
window.$message?.warning('请选择 Excel 模板文件');
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!uploadForm.templateName.trim()) {
|
|
||||||
|
tableData.value = data.list;
|
||||||
|
total.value = data.total;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSearch() {
|
||||||
|
loadTemplatePage(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleReset() {
|
||||||
|
Object.assign(searchParams, {
|
||||||
|
pageNo: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
templateName: undefined,
|
||||||
|
employeeId: undefined,
|
||||||
|
employeeName: undefined,
|
||||||
|
templateYear: undefined,
|
||||||
|
uploadUserName: undefined,
|
||||||
|
activeFlag: undefined
|
||||||
|
});
|
||||||
|
loadTemplatePage(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSingleFileChange(file: UploadFile) {
|
||||||
|
const rawFile = file.raw;
|
||||||
|
if (!rawFile) return;
|
||||||
|
|
||||||
|
singleForm.file = rawFile;
|
||||||
|
if (!singleForm.templateName) {
|
||||||
|
singleForm.templateName = rawFile.name.replace(/\.[^.]+$/u, '');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleBatchFileChange(_file: UploadFile, files: UploadFiles) {
|
||||||
|
batchForm.files = files.reduce<UploadRawFile[]>((list, item) => {
|
||||||
|
if (item.raw) {
|
||||||
|
list.push(item.raw);
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
}, []);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function uploadSourceFile(file: File) {
|
||||||
|
return uploadFile(file, 'performance/template-sources');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSingleUpload() {
|
||||||
|
if (!canUpdateTemplate.value || !isOrgLeader.value) return;
|
||||||
|
|
||||||
|
if (!singleForm.employeeId) {
|
||||||
|
window.$message?.warning('请选择员工');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!singleForm.templateName.trim()) {
|
||||||
window.$message?.warning('请输入模板名称');
|
window.$message?.warning('请输入模板名称');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (!singleForm.file) {
|
||||||
uploading.value = true;
|
window.$message?.warning('请选择 Excel 文件');
|
||||||
const fileResult = await uploadFile(uploadForm.file, 'performance/templates');
|
|
||||||
if (fileResult.error || !fileResult.data) {
|
|
||||||
uploading.value = false;
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await uploadPerformanceTemplate({
|
singleUploading.value = true;
|
||||||
templateName: uploadForm.templateName.trim(),
|
const fileResult = await uploadSourceFile(singleForm.file);
|
||||||
fileId: fileResult.data.id,
|
if (fileResult.error || !fileResult.data) {
|
||||||
fileName: uploadForm.file.name,
|
singleUploading.value = false;
|
||||||
activeFlag: uploadForm.activeFlag,
|
return;
|
||||||
remark: uploadForm.remark.trim() || undefined
|
}
|
||||||
|
|
||||||
|
const result = await uploadSinglePerformanceTemplate({
|
||||||
|
employeeId: singleForm.employeeId,
|
||||||
|
templateName: singleForm.templateName.trim(),
|
||||||
|
templateYear: singleForm.templateYear || undefined,
|
||||||
|
sourceFileId: fileResult.data.id,
|
||||||
|
sourceFileName: singleForm.file.name,
|
||||||
|
remark: singleForm.remark.trim() || undefined
|
||||||
});
|
});
|
||||||
uploading.value = false;
|
singleUploading.value = false;
|
||||||
|
|
||||||
if (result.error) return;
|
if (result.error) return;
|
||||||
|
|
||||||
window.$message?.success('绩效模板已上传');
|
window.$message?.success('绩效模板上传成功');
|
||||||
Object.assign(uploadForm, {
|
resetSingleForm();
|
||||||
templateName: '',
|
|
||||||
remark: '',
|
|
||||||
activeFlag: true,
|
|
||||||
file: null
|
|
||||||
});
|
|
||||||
await loadTemplatePage(1);
|
await loadTemplatePage(1);
|
||||||
emit('updated');
|
emit('updated');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handleBatchImport() {
|
||||||
|
if (!canUpdateTemplate.value || !isOrgLeader.value) return;
|
||||||
|
|
||||||
|
if (!batchForm.files.length) {
|
||||||
|
window.$message?.warning('请选择要导入的 Excel 文件');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
batchUploading.value = true;
|
||||||
|
const uploadResults = await Promise.all(
|
||||||
|
batchForm.files.map(async file => {
|
||||||
|
const fileResult = await uploadSourceFile(file);
|
||||||
|
return { file, fileResult };
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const failedUpload = uploadResults.find(item => item.fileResult.error || !item.fileResult.data);
|
||||||
|
if (failedUpload) {
|
||||||
|
batchUploading.value = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const files: Api.Performance.Template.BatchImportFileItem[] = uploadResults.map(item => ({
|
||||||
|
sourceFileId: item.fileResult.data!.id,
|
||||||
|
sourceFileName: item.file.name
|
||||||
|
}));
|
||||||
|
|
||||||
|
const result = await batchImportPerformanceTemplates({
|
||||||
|
templateYear: batchForm.templateYear || undefined,
|
||||||
|
remark: batchForm.remark.trim() || undefined,
|
||||||
|
files
|
||||||
|
});
|
||||||
|
batchUploading.value = false;
|
||||||
|
|
||||||
|
if (result.error || !result.data) return;
|
||||||
|
|
||||||
|
await showBatchImportResult(result.data);
|
||||||
|
resetBatchForm();
|
||||||
|
await loadTemplatePage(1);
|
||||||
|
emit('updated');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function showBatchImportResult(result: Api.Performance.Template.BatchImportResult) {
|
||||||
|
const lines = [`成功 ${result.successCount} 条`, `失败 ${result.failureCount} 条`];
|
||||||
|
|
||||||
|
result.failures.slice(0, 20).forEach(item => {
|
||||||
|
const sheetName = item.sheetName ? ` / ${item.sheetName}` : '';
|
||||||
|
lines.push(`${item.sourceFileName}${sheetName}:${item.reason}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.failures.length > 20) {
|
||||||
|
lines.push(`其余 ${result.failures.length - 20} 条失败明细请查看后端日志或再次筛查源文件`);
|
||||||
|
}
|
||||||
|
|
||||||
|
await ElMessageBox.alert(lines.join('\n'), '批量导入结果', {
|
||||||
|
confirmButtonText: '知道了'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async function handleActivate(row: Api.Performance.Template.Template) {
|
async function handleActivate(row: Api.Performance.Template.Template) {
|
||||||
if (!canUpdateTemplate.value) return;
|
if (!canUpdateTemplate.value) return;
|
||||||
|
|
||||||
@@ -191,7 +264,13 @@ async function handleActivate(row: Api.Performance.Template.Template) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
watch(visible, isVisible => {
|
watch(visible, isVisible => {
|
||||||
if (isVisible && canQueryTemplate.value) {
|
if (!isVisible) return;
|
||||||
|
|
||||||
|
if (!singleForm.employeeId && employeeOptions.value.length === 1) {
|
||||||
|
singleForm.employeeId = employeeOptions.value[0].employeeId;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (canQueryTemplate.value) {
|
||||||
loadTemplatePage(1);
|
loadTemplatePage(1);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -204,125 +283,217 @@ watch(visible, isVisible => {
|
|||||||
preset="lg"
|
preset="lg"
|
||||||
append-to-body
|
append-to-body
|
||||||
:show-footer="false"
|
:show-footer="false"
|
||||||
max-body-height="76vh"
|
max-body-height="78vh"
|
||||||
>
|
>
|
||||||
<div class="performance-template-dialog">
|
<div class="performance-template-dialog">
|
||||||
<ElCard v-if="canUpdateTemplate" shadow="never">
|
<ElAlert
|
||||||
<ElForm :model="uploadForm" label-position="top" class="performance-template-dialog__upload-form">
|
v-if="canUpdateTemplate && !isOrgLeader"
|
||||||
<div class="performance-template-dialog__upload-grid">
|
type="warning"
|
||||||
<ElFormItem label="模板名称" class="performance-template-dialog__field">
|
:closable="false"
|
||||||
<ElInput v-model="uploadForm.templateName" placeholder="请输入模板名称" />
|
show-icon
|
||||||
</ElFormItem>
|
title="当前账号不是组织负责人,不能上传绩效模板"
|
||||||
|
/>
|
||||||
|
|
||||||
<ElFormItem class="performance-template-dialog__field">
|
<div v-if="canUpdateTemplate && isOrgLeader" class="performance-template-dialog__upload-sections">
|
||||||
<template #label>
|
<ElCard shadow="never">
|
||||||
<div class="performance-template-dialog__label">
|
<template #header>
|
||||||
<span>Excel 文件</span>
|
<div class="performance-template-dialog__section-title">单人上传</div>
|
||||||
</div>
|
</template>
|
||||||
</template>
|
|
||||||
<div class="performance-template-dialog__file-row">
|
<ElForm :model="singleForm" label-position="top" class="performance-template-dialog__form">
|
||||||
<ElUpload
|
<div class="performance-template-dialog__grid">
|
||||||
class="performance-template-dialog__upload-trigger"
|
<ElFormItem label="员工">
|
||||||
:auto-upload="false"
|
<ElSelect v-model="singleForm.employeeId" filterable placeholder="请选择员工">
|
||||||
:show-file-list="false"
|
<ElOption
|
||||||
accept=".xlsx,.xls"
|
v-for="item in employeeOptions"
|
||||||
:limit="1"
|
:key="item.employeeId"
|
||||||
:on-change="handleFileChange"
|
:label="
|
||||||
>
|
item.employeeDeptName ? `${item.employeeName}(${item.employeeDeptName})` : item.employeeName
|
||||||
<ElButton plain class="performance-template-dialog__upload-button">
|
"
|
||||||
<template #icon>
|
:value="item.employeeId"
|
||||||
<icon-mdi-upload class="text-icon" />
|
/>
|
||||||
</template>
|
</ElSelect>
|
||||||
选择文件
|
</ElFormItem>
|
||||||
</ElButton>
|
|
||||||
</ElUpload>
|
<ElFormItem label="模板名称">
|
||||||
<div class="performance-template-dialog__file-name-wrapper">
|
<ElInput v-model="singleForm.templateName" placeholder="请输入模板名称" />
|
||||||
<ElTooltip
|
</ElFormItem>
|
||||||
:disabled="!selectedFileName"
|
|
||||||
:content="selectedFileName"
|
<ElFormItem label="模板年份">
|
||||||
placement="top"
|
<ElInputNumber v-model="singleForm.templateYear" :min="2000" :max="2100" controls-position="right" />
|
||||||
effect="light"
|
</ElFormItem>
|
||||||
popper-class="performance-template-dialog__file-tooltip"
|
|
||||||
|
<ElFormItem label="Excel 文件" class="performance-template-dialog__full">
|
||||||
|
<div class="performance-template-dialog__file-line">
|
||||||
|
<ElUpload
|
||||||
|
:auto-upload="false"
|
||||||
|
:show-file-list="false"
|
||||||
|
:limit="1"
|
||||||
|
accept=".xlsx"
|
||||||
|
@change="handleSingleFileChange"
|
||||||
>
|
>
|
||||||
<div
|
<ElButton>
|
||||||
class="performance-template-dialog__file-name"
|
<template #icon>
|
||||||
:class="{ 'performance-template-dialog__file-name--placeholder': !selectedFileName }"
|
<icon-mdi-upload class="text-icon" />
|
||||||
>
|
</template>
|
||||||
<span class="performance-template-dialog__file-name-text">
|
选择文件
|
||||||
{{ selectedFileName || '未选择文件' }}
|
</ElButton>
|
||||||
</span>
|
</ElUpload>
|
||||||
</div>
|
<span class="performance-template-dialog__file-text">
|
||||||
</ElTooltip>
|
{{ singleFileName || '未选择文件,仅支持 .xlsx' }}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<ElTooltip placement="top" effect="light">
|
</ElFormItem>
|
||||||
<template #content>仅支持 .xlsx格式,选择后会在这里显示文件名</template>
|
|
||||||
<button type="button" class="performance-template-dialog__hint-button" aria-label="Excel 文件说明">
|
|
||||||
<icon-mdi-information-outline />
|
|
||||||
</button>
|
|
||||||
</ElTooltip>
|
|
||||||
</div>
|
|
||||||
</ElFormItem>
|
|
||||||
|
|
||||||
<ElFormItem
|
<ElFormItem label="备注" class="performance-template-dialog__full">
|
||||||
label="上传后启用"
|
<ElInput v-model="singleForm.remark" type="textarea" :rows="2" maxlength="500" show-word-limit />
|
||||||
class="performance-template-dialog__field performance-template-dialog__switch-field"
|
</ElFormItem>
|
||||||
>
|
</div>
|
||||||
<div class="performance-template-dialog__switch-box">
|
|
||||||
<span>上传后立即切换为当前模板</span>
|
|
||||||
<ElSwitch v-model="uploadForm.activeFlag" />
|
|
||||||
</div>
|
|
||||||
</ElFormItem>
|
|
||||||
|
|
||||||
<!-- <ElFormItem-->
|
<div class="performance-template-dialog__actions">
|
||||||
<!-- label="备注"-->
|
<ElButton type="primary" :loading="singleUploading" @click="handleSingleUpload">上传模板</ElButton>
|
||||||
<!-- class="performance-template-dialog__field performance-template-dialog__field--full"-->
|
</div>
|
||||||
<!-- >-->
|
</ElForm>
|
||||||
<!-- <ElInput v-model="uploadForm.remark" type="textarea" :rows="3" maxlength="500" show-word-limit />-->
|
</ElCard>
|
||||||
<!-- </ElFormItem>-->
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="performance-template-dialog__actions">
|
<ElCard shadow="never">
|
||||||
<ElButton type="primary" :loading="uploading" @click="handleUploadTemplate">上传模板</ElButton>
|
<template #header>
|
||||||
</div>
|
<div class="performance-template-dialog__section-title">批量导入</div>
|
||||||
</ElForm>
|
</template>
|
||||||
</ElCard>
|
|
||||||
|
|
||||||
<ElCard v-if="canQueryTemplate" shadow="never" body-class="business-table-card-body">
|
<ElForm :model="batchForm" label-position="top" class="performance-template-dialog__form">
|
||||||
|
<div class="performance-template-dialog__grid">
|
||||||
|
<ElFormItem label="模板年份">
|
||||||
|
<ElInputNumber v-model="batchForm.templateYear" :min="2000" :max="2100" controls-position="right" />
|
||||||
|
</ElFormItem>
|
||||||
|
|
||||||
|
<ElFormItem label="Excel 文件" class="performance-template-dialog__full">
|
||||||
|
<div class="performance-template-dialog__file-line">
|
||||||
|
<ElUpload
|
||||||
|
:auto-upload="false"
|
||||||
|
:show-file-list="false"
|
||||||
|
multiple
|
||||||
|
accept=".xlsx"
|
||||||
|
@change="handleBatchFileChange"
|
||||||
|
>
|
||||||
|
<ElButton>
|
||||||
|
<template #icon>
|
||||||
|
<icon-mdi-upload class="text-icon" />
|
||||||
|
</template>
|
||||||
|
选择多个文件
|
||||||
|
</ElButton>
|
||||||
|
</ElUpload>
|
||||||
|
<div class="performance-template-dialog__batch-files">
|
||||||
|
<span v-if="!batchFileNames.length" class="performance-template-dialog__file-text">
|
||||||
|
未选择文件,仅支持 .xlsx
|
||||||
|
</span>
|
||||||
|
<ElTag v-for="name in batchFileNames" :key="name" effect="plain">{{ name }}</ElTag>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ElFormItem>
|
||||||
|
|
||||||
|
<ElFormItem label="备注" class="performance-template-dialog__full">
|
||||||
|
<ElInput v-model="batchForm.remark" type="textarea" :rows="2" maxlength="500" show-word-limit />
|
||||||
|
</ElFormItem>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="performance-template-dialog__actions">
|
||||||
|
<ElButton type="primary" :loading="batchUploading" @click="handleBatchImport">批量导入</ElButton>
|
||||||
|
</div>
|
||||||
|
</ElForm>
|
||||||
|
</ElCard>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ElCard v-if="canQueryTemplate" shadow="never">
|
||||||
<template #header>
|
<template #header>
|
||||||
<div class="flex items-center justify-between gap-12px">
|
<div class="performance-template-dialog__section-title">模板列表</div>
|
||||||
<p class="text-16px font-600">模板列表</p>
|
|
||||||
<ElSpace wrap alignment="center">
|
|
||||||
<ElButton @click="loadTemplatePage(searchParams.pageNo ?? 1)">
|
|
||||||
<template #icon>
|
|
||||||
<icon-mdi-refresh class="text-icon" :class="{ 'animate-spin': loading }" />
|
|
||||||
</template>
|
|
||||||
刷新
|
|
||||||
</ElButton>
|
|
||||||
<TableColumnSetting v-model:columns="columnChecks" />
|
|
||||||
</ElSpace>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<div class="performance-template-dialog__table">
|
<ElForm :model="searchParams" inline label-position="top" class="performance-template-dialog__search">
|
||||||
<ElTable v-loading="loading" height="100%" border :data="data">
|
<ElFormItem label="模板名称">
|
||||||
<template v-for="col in columns" :key="String(col.prop)">
|
<ElInput v-model="searchParams.templateName" clearable placeholder="请输入模板名称" />
|
||||||
<ElTableColumn v-bind="col" />
|
</ElFormItem>
|
||||||
</template>
|
<ElFormItem label="员工姓名">
|
||||||
</ElTable>
|
<ElInput v-model="searchParams.employeeName" clearable placeholder="请输入员工姓名" />
|
||||||
</div>
|
</ElFormItem>
|
||||||
|
<ElFormItem label="模板年份">
|
||||||
|
<ElInputNumber
|
||||||
|
v-model="searchParams.templateYear"
|
||||||
|
:min="2000"
|
||||||
|
:max="2100"
|
||||||
|
controls-position="right"
|
||||||
|
placeholder="年份"
|
||||||
|
/>
|
||||||
|
</ElFormItem>
|
||||||
|
<ElFormItem label="上传人">
|
||||||
|
<ElInput v-model="searchParams.uploadUserName" clearable placeholder="请输入上传人" />
|
||||||
|
</ElFormItem>
|
||||||
|
<ElFormItem label="状态">
|
||||||
|
<ElSelect v-model="searchParams.activeFlag" clearable placeholder="全部">
|
||||||
|
<ElOption :value="true" label="已启用" />
|
||||||
|
<ElOption :value="false" label="未启用" />
|
||||||
|
</ElSelect>
|
||||||
|
</ElFormItem>
|
||||||
|
<ElFormItem class="performance-template-dialog__search-actions">
|
||||||
|
<ElButton type="primary" @click="handleSearch">查询</ElButton>
|
||||||
|
<ElButton @click="handleReset">重置</ElButton>
|
||||||
|
</ElFormItem>
|
||||||
|
</ElForm>
|
||||||
|
|
||||||
<div class="mt-16px flex justify-end">
|
<ElTable v-loading="loading" :data="tableData" border>
|
||||||
|
<ElTableColumn type="index" label="序号" width="64" />
|
||||||
|
<ElTableColumn prop="templateName" label="模板名称" min-width="180" show-overflow-tooltip />
|
||||||
|
<ElTableColumn prop="employeeName" label="员工" min-width="110" show-overflow-tooltip />
|
||||||
|
<ElTableColumn prop="employeeDeptName" label="部门" min-width="120" show-overflow-tooltip />
|
||||||
|
<ElTableColumn prop="templateYear" label="年份" width="90" />
|
||||||
|
<ElTableColumn label="状态" width="100" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<ElTag :type="row.activeFlag ? 'success' : 'info'">{{ row.activeFlag ? '已启用' : '未启用' }}</ElTag>
|
||||||
|
</template>
|
||||||
|
</ElTableColumn>
|
||||||
|
<ElTableColumn prop="uploadUserName" label="上传人" width="110" />
|
||||||
|
<ElTableColumn label="上传时间" width="180">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ formatDateTime(row.uploadTime) }}
|
||||||
|
</template>
|
||||||
|
</ElTableColumn>
|
||||||
|
<ElTableColumn label="操作" width="100" fixed="right" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<ElButton
|
||||||
|
v-if="canUpdateTemplate"
|
||||||
|
link
|
||||||
|
type="primary"
|
||||||
|
:disabled="row.activeFlag || activatingId === row.id"
|
||||||
|
@click="handleActivate(row)"
|
||||||
|
>
|
||||||
|
启用
|
||||||
|
</ElButton>
|
||||||
|
</template>
|
||||||
|
</ElTableColumn>
|
||||||
|
</ElTable>
|
||||||
|
|
||||||
|
<div class="performance-template-dialog__pagination">
|
||||||
<ElPagination
|
<ElPagination
|
||||||
v-if="mobilePagination.total"
|
:current-page="searchParams.pageNo || 1"
|
||||||
layout="total,prev,pager,next,sizes"
|
:page-size="searchParams.pageSize || 10"
|
||||||
v-bind="mobilePagination"
|
:total="total"
|
||||||
@current-change="mobilePagination['current-change']"
|
layout="total, prev, pager, next, sizes"
|
||||||
@size-change="mobilePagination['size-change']"
|
@current-change="loadTemplatePage"
|
||||||
|
@size-change="
|
||||||
|
size => {
|
||||||
|
searchParams.pageSize = size;
|
||||||
|
loadTemplatePage(1);
|
||||||
|
}
|
||||||
|
"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</ElCard>
|
</ElCard>
|
||||||
|
|
||||||
<ElEmpty v-if="!canQueryTemplate && !canUpdateTemplate" :image-size="80" description="当前账号没有绩效模板权限" />
|
<ElEmpty
|
||||||
|
v-if="!canQueryTemplate && !canUpdateTemplate"
|
||||||
|
:image-size="80"
|
||||||
|
description="当前账号没有绩效模板相关权限"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</BusinessFormDialog>
|
</BusinessFormDialog>
|
||||||
</template>
|
</template>
|
||||||
@@ -333,148 +504,47 @@ watch(visible, isVisible => {
|
|||||||
gap: 16px;
|
gap: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.performance-template-dialog__upload-form {
|
.performance-template-dialog__upload-sections {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.performance-template-dialog__upload-grid {
|
.performance-template-dialog__section-title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.performance-template-dialog__form {
|
||||||
|
display: grid;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.performance-template-dialog__grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.performance-template-dialog__field {
|
.performance-template-dialog__full {
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.performance-template-dialog__field :deep(.el-form-item__label) {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
min-height: 22px;
|
|
||||||
padding-bottom: 6px;
|
|
||||||
line-height: 22px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.performance-template-dialog__field :deep(.el-form-item__content) {
|
|
||||||
min-height: 36px;
|
|
||||||
align-items: stretch;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.performance-template-dialog__field--full {
|
|
||||||
grid-column: 1 / -1;
|
grid-column: 1 / -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.performance-template-dialog__label {
|
.performance-template-dialog__file-line {
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.performance-template-dialog__file-row {
|
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 112px minmax(0, 1fr) 24px;
|
grid-template-columns: auto 1fr;
|
||||||
align-items: stretch;
|
|
||||||
gap: 6px;
|
|
||||||
min-height: 36px;
|
|
||||||
width: 100% !important;
|
|
||||||
min-width: 0 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.performance-template-dialog__upload-trigger {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.performance-template-dialog__upload-trigger :deep(.el-upload) {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.performance-template-dialog__upload-button {
|
|
||||||
width: 100%;
|
|
||||||
height: 36px;
|
|
||||||
padding: 0 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.performance-template-dialog__file-name-wrapper {
|
|
||||||
display: block;
|
|
||||||
width: 100%;
|
|
||||||
min-width: 0;
|
|
||||||
flex: 1 1 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.performance-template-dialog__file-name-wrapper :deep(.el-tooltip__trigger) {
|
|
||||||
display: block;
|
|
||||||
width: 100% !important;
|
|
||||||
min-width: 0 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.performance-template-dialog__file-name {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
min-width: 0;
|
|
||||||
width: 100% !important;
|
|
||||||
height: 36px;
|
|
||||||
padding: 0 12px;
|
|
||||||
overflow: hidden;
|
|
||||||
border: 1px solid var(--el-border-color);
|
|
||||||
border-radius: 8px;
|
|
||||||
background: var(--el-fill-color-blank);
|
|
||||||
color: var(--el-text-color-regular);
|
|
||||||
font-size: 13px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.performance-template-dialog__file-name-text {
|
|
||||||
display: block;
|
|
||||||
width: 100%;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.performance-template-dialog__file-name--placeholder {
|
|
||||||
color: var(--el-text-color-placeholder);
|
|
||||||
}
|
|
||||||
|
|
||||||
.performance-template-dialog__hint-button {
|
|
||||||
width: 24px;
|
|
||||||
height: 36px;
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
padding: 0;
|
|
||||||
border: none;
|
|
||||||
border-radius: 6px;
|
|
||||||
background: transparent;
|
|
||||||
color: var(--el-text-color-secondary);
|
|
||||||
cursor: pointer;
|
|
||||||
transition:
|
|
||||||
background-color 0.2s ease,
|
|
||||||
color 0.2s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.performance-template-dialog__hint-button:hover {
|
|
||||||
background: var(--el-fill-color-light);
|
|
||||||
color: var(--el-color-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.performance-template-dialog__switch-field {
|
|
||||||
align-self: stretch;
|
|
||||||
}
|
|
||||||
|
|
||||||
.performance-template-dialog__switch-box {
|
|
||||||
height: 36px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
padding: 0 14px;
|
align-items: center;
|
||||||
border: 1px solid var(--el-border-color);
|
}
|
||||||
border-radius: 8px;
|
|
||||||
background: var(--el-fill-color-blank);
|
.performance-template-dialog__file-text {
|
||||||
color: var(--el-text-color-regular);
|
color: var(--el-text-color-secondary);
|
||||||
font-size: 13px;
|
word-break: break-all;
|
||||||
line-height: 1;
|
}
|
||||||
|
|
||||||
|
.performance-template-dialog__batch-files {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.performance-template-dialog__actions {
|
.performance-template-dialog__actions {
|
||||||
@@ -482,32 +552,31 @@ watch(visible, isVisible => {
|
|||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
}
|
}
|
||||||
|
|
||||||
.performance-template-dialog__table {
|
.performance-template-dialog__search {
|
||||||
height: 360px;
|
margin-bottom: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 1080px) {
|
.performance-template-dialog__search-actions {
|
||||||
.performance-template-dialog__upload-grid {
|
align-self: end;
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
||||||
}
|
|
||||||
|
|
||||||
.performance-template-dialog__switch-field {
|
|
||||||
grid-column: 1 / -1;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
.performance-template-dialog__pagination {
|
||||||
.performance-template-dialog__upload-grid {
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 960px) {
|
||||||
|
.performance-template-dialog__grid {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
.performance-template-dialog__field--full,
|
.performance-template-dialog__full {
|
||||||
.performance-template-dialog__switch-field {
|
|
||||||
grid-column: auto;
|
grid-column: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.performance-template-dialog__switch-box {
|
.performance-template-dialog__file-line {
|
||||||
height: 36px;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user