feat(新增加班申请功能): 新增申请功能,可在工作台进行审核。
fix(dict_data): 在字典数据新增、编辑时可以操作颜色类型字段(color_type)。
This commit is contained in:
401
src/views/personal-center/overtime-application/index.vue
Normal file
401
src/views/personal-center/overtime-application/index.vue
Normal file
@@ -0,0 +1,401 @@
|
||||
<script setup lang="tsx">
|
||||
import { computed, markRaw, reactive, ref } from 'vue';
|
||||
import { ElButton, ElMessageBox, ElTag } from 'element-plus';
|
||||
import dayjs from 'dayjs';
|
||||
import {
|
||||
fetchCancelOvertimeApplication,
|
||||
fetchDeleteOvertimeApplication,
|
||||
fetchExportOvertimeApplications,
|
||||
fetchGetOvertimeApplicationPage
|
||||
} from '@/service/api';
|
||||
import { useUIPaginatedTable } from '@/hooks/common/table';
|
||||
import BusinessTableActionCell, { type BusinessTableAction } from '@/components/custom/business-table-action-cell';
|
||||
import OvertimeApplicationActionDialog from './modules/overtime-application-action-dialog.vue';
|
||||
import OvertimeApplicationDetailDialog from './modules/overtime-application-detail-dialog.vue';
|
||||
import OvertimeApplicationOperateDialog from './modules/overtime-application-operate-dialog.vue';
|
||||
import OvertimeApplicationSearch from './modules/overtime-application-search.vue';
|
||||
import OvertimeApplicationStatusLogDialog from './modules/overtime-application-status-log-dialog.vue';
|
||||
import {
|
||||
downloadBlob,
|
||||
formatEmptyText,
|
||||
formatOvertimeDate,
|
||||
formatOvertimeDateTime,
|
||||
getOvertimeApplicationStatusLabel,
|
||||
resolveOvertimeApplicationStatusTagType
|
||||
} from './modules/overtime-application-shared';
|
||||
import IconMdiCloseCircleOutline from '~icons/mdi/close-circle-outline';
|
||||
import IconMdiDeleteOutline from '~icons/mdi/delete-outline';
|
||||
import IconMdiEyeOutline from '~icons/mdi/eye-outline';
|
||||
import IconMdiHistory from '~icons/mdi/history';
|
||||
import IconMdiPencilOutline from '~icons/mdi/pencil-outline';
|
||||
|
||||
defineOptions({ name: 'OvertimeApplication' });
|
||||
|
||||
type OvertimeApplicationPageResponse = Awaited<ReturnType<typeof fetchGetOvertimeApplicationPage>>;
|
||||
type ActionType = 'cancel';
|
||||
|
||||
function getInitSearchParams(): Api.OvertimeApplication.OvertimeApplicationSearchParams {
|
||||
return {
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
keyword: undefined,
|
||||
applicantName: undefined,
|
||||
approverId: undefined,
|
||||
approverName: undefined,
|
||||
statusCode: undefined,
|
||||
overtimeDate: undefined,
|
||||
createTime: undefined
|
||||
};
|
||||
}
|
||||
|
||||
function transformPageResult(response: OvertimeApplicationPageResponse, pageNo: number, pageSize: number) {
|
||||
if (!response.error && response.data) {
|
||||
return {
|
||||
data: response.data.list,
|
||||
pageNum: pageNo,
|
||||
pageSize,
|
||||
total: response.data.total
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
data: [],
|
||||
pageNum: 1,
|
||||
pageSize,
|
||||
total: 0
|
||||
};
|
||||
}
|
||||
|
||||
const searchParams = reactive(getInitSearchParams());
|
||||
const operateVisible = ref(false);
|
||||
const detailVisible = ref(false);
|
||||
const statusLogVisible = ref(false);
|
||||
const actionVisible = ref(false);
|
||||
const operateType = ref<'add' | 'edit'>('add');
|
||||
const currentRow = ref<Api.OvertimeApplication.OvertimeApplication | null>(null);
|
||||
const currentActionType = ref<ActionType>('cancel');
|
||||
const actionSubmitting = ref(false);
|
||||
const exporting = ref(false);
|
||||
|
||||
const ACTION_ICON_MAP = {
|
||||
detail: markRaw(IconMdiEyeOutline),
|
||||
statusLog: markRaw(IconMdiHistory),
|
||||
edit: markRaw(IconMdiPencilOutline),
|
||||
cancel: markRaw(IconMdiCloseCircleOutline),
|
||||
delete: markRaw(IconMdiDeleteOutline)
|
||||
};
|
||||
|
||||
const { columns, columnChecks, data, loading, getDataByPage, mobilePagination } = useUIPaginatedTable<
|
||||
OvertimeApplicationPageResponse,
|
||||
Api.OvertimeApplication.OvertimeApplication
|
||||
>({
|
||||
paginationProps: {
|
||||
currentPage: searchParams.pageNo,
|
||||
pageSize: searchParams.pageSize
|
||||
},
|
||||
api: () => fetchGetOvertimeApplicationPage(searchParams),
|
||||
transform: response => transformPageResult(response, searchParams.pageNo ?? 1, searchParams.pageSize ?? 10),
|
||||
onPaginationParamsChange: params => {
|
||||
searchParams.pageNo = params.currentPage ?? 1;
|
||||
searchParams.pageSize = params.pageSize ?? 10;
|
||||
},
|
||||
columns: () => [
|
||||
{ prop: 'index', type: 'index', label: '序号', width: 64 },
|
||||
{ prop: 'applicantName', label: '申请人', minWidth: 120, showOverflowTooltip: true },
|
||||
{
|
||||
prop: 'overtimeDate',
|
||||
label: '加班日期',
|
||||
width: 120,
|
||||
formatter: row => formatOvertimeDate(row.overtimeDate)
|
||||
},
|
||||
{ prop: 'overtimeDuration', label: '加班时长', width: 110, showOverflowTooltip: true },
|
||||
{
|
||||
prop: 'overtimeReason',
|
||||
label: '加班原因',
|
||||
minWidth: 180,
|
||||
showOverflowTooltip: true,
|
||||
formatter: row => formatEmptyText(row.overtimeReason)
|
||||
},
|
||||
{
|
||||
prop: 'overtimeContent',
|
||||
label: '加班内容',
|
||||
minWidth: 200,
|
||||
showOverflowTooltip: true,
|
||||
formatter: row => formatEmptyText(row.overtimeContent)
|
||||
},
|
||||
{
|
||||
prop: 'statusCode',
|
||||
label: '状态',
|
||||
width: 110,
|
||||
align: 'center',
|
||||
formatter: row => (
|
||||
<ElTag type={resolveOvertimeApplicationStatusTagType(row.statusCode)}>
|
||||
{getOvertimeApplicationStatusLabel(row.statusCode, row.statusName)}
|
||||
</ElTag>
|
||||
)
|
||||
},
|
||||
{ prop: 'approverName', label: '审核人', minWidth: 120, showOverflowTooltip: true },
|
||||
{
|
||||
prop: 'submitTime',
|
||||
label: '提交时间',
|
||||
minWidth: 170,
|
||||
formatter: row => formatOvertimeDateTime(row.submitTime)
|
||||
},
|
||||
{
|
||||
prop: 'approvalTime',
|
||||
label: '审核时间',
|
||||
minWidth: 170,
|
||||
formatter: row => formatOvertimeDateTime(row.approvalTime)
|
||||
},
|
||||
{
|
||||
prop: 'operate',
|
||||
label: '操作',
|
||||
width: 170,
|
||||
align: 'center',
|
||||
fixed: 'right',
|
||||
formatter: row => <BusinessTableActionCell actions={getRowActions(row)} variant="icon" />
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
const totalCount = computed(() => mobilePagination.value.total || data.value.length);
|
||||
|
||||
function getRowActions(row: Api.OvertimeApplication.OvertimeApplication): BusinessTableAction[] {
|
||||
const actions: BusinessTableAction[] = [
|
||||
{
|
||||
key: 'detail',
|
||||
label: '详情',
|
||||
buttonType: 'primary',
|
||||
icon: ACTION_ICON_MAP.detail,
|
||||
onClick: () => openDetail(row)
|
||||
}
|
||||
];
|
||||
|
||||
if (row.statusCode === 'rejected' && row.allowEdit) {
|
||||
actions.push({
|
||||
key: 'edit',
|
||||
label: '修改',
|
||||
buttonType: 'primary',
|
||||
icon: ACTION_ICON_MAP.edit,
|
||||
onClick: () => openEdit(row)
|
||||
});
|
||||
}
|
||||
|
||||
actions.push({
|
||||
key: 'status-log',
|
||||
label: '状态日志',
|
||||
buttonType: 'info',
|
||||
icon: ACTION_ICON_MAP.statusLog,
|
||||
onClick: () => openStatusLog(row)
|
||||
});
|
||||
|
||||
if (row.statusCode === 'pending' || row.statusCode === 'rejected') {
|
||||
actions.push({
|
||||
key: 'cancel',
|
||||
label: '撤销',
|
||||
buttonType: 'danger',
|
||||
icon: ACTION_ICON_MAP.cancel,
|
||||
onClick: () => openCancel(row)
|
||||
});
|
||||
}
|
||||
|
||||
if (row.statusCode === 'cancelled') {
|
||||
actions.push({
|
||||
key: 'delete',
|
||||
label: '删除',
|
||||
buttonType: 'danger',
|
||||
icon: ACTION_ICON_MAP.delete,
|
||||
onClick: () => handleDelete(row)
|
||||
});
|
||||
}
|
||||
|
||||
return actions;
|
||||
}
|
||||
|
||||
function openAdd() {
|
||||
operateType.value = 'add';
|
||||
currentRow.value = null;
|
||||
operateVisible.value = true;
|
||||
}
|
||||
|
||||
function openEdit(row: Api.OvertimeApplication.OvertimeApplication) {
|
||||
operateType.value = 'edit';
|
||||
currentRow.value = row;
|
||||
operateVisible.value = true;
|
||||
}
|
||||
|
||||
function openDetail(row: Api.OvertimeApplication.OvertimeApplication) {
|
||||
currentRow.value = row;
|
||||
detailVisible.value = true;
|
||||
}
|
||||
|
||||
function openStatusLog(row: Api.OvertimeApplication.OvertimeApplication) {
|
||||
currentRow.value = row;
|
||||
statusLogVisible.value = true;
|
||||
}
|
||||
|
||||
function openCancel(row: Api.OvertimeApplication.OvertimeApplication) {
|
||||
currentRow.value = row;
|
||||
currentActionType.value = 'cancel';
|
||||
actionVisible.value = true;
|
||||
}
|
||||
|
||||
async function reloadTable(page = searchParams.pageNo ?? 1) {
|
||||
await getDataByPage(page);
|
||||
}
|
||||
|
||||
function resetSearchParams() {
|
||||
const pageSize = searchParams.pageSize ?? 10;
|
||||
Object.assign(searchParams, getInitSearchParams(), { pageSize });
|
||||
reloadTable(1);
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
reloadTable(1);
|
||||
}
|
||||
|
||||
function handleSubmitted() {
|
||||
operateVisible.value = false;
|
||||
reloadTable(searchParams.pageNo ?? 1);
|
||||
}
|
||||
|
||||
async function handleActionSubmit(reason: string | null) {
|
||||
if (!currentRow.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
actionSubmitting.value = true;
|
||||
const { error } = await fetchCancelOvertimeApplication(currentRow.value.id, { reason });
|
||||
actionSubmitting.value = false;
|
||||
|
||||
if (error) {
|
||||
return;
|
||||
}
|
||||
|
||||
actionVisible.value = false;
|
||||
window.$message?.success('加班申请已撤销');
|
||||
await reloadTable(searchParams.pageNo ?? 1);
|
||||
}
|
||||
|
||||
async function handleDelete(row: Api.OvertimeApplication.OvertimeApplication) {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确定删除 ${row.applicantName} ${formatOvertimeDate(row.overtimeDate)} 的加班申请吗?`,
|
||||
'删除确认',
|
||||
{
|
||||
type: 'warning',
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消'
|
||||
}
|
||||
);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
const { error } = await fetchDeleteOvertimeApplication(row.id);
|
||||
|
||||
if (error) {
|
||||
return;
|
||||
}
|
||||
|
||||
window.$message?.success('加班申请已删除');
|
||||
await reloadTable(searchParams.pageNo ?? 1);
|
||||
}
|
||||
|
||||
async function handleExport() {
|
||||
exporting.value = true;
|
||||
const { error, data: blob } = await fetchExportOvertimeApplications(searchParams);
|
||||
exporting.value = false;
|
||||
|
||||
if (error || !blob) {
|
||||
return;
|
||||
}
|
||||
|
||||
downloadBlob(blob, `加班申请_${dayjs().format('YYYY-MM-DD')}.xls`);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex-col-stretch gap-16px overflow-hidden">
|
||||
<OvertimeApplicationSearch v-model:model="searchParams" @reset="resetSearchParams" @search="handleSearch" />
|
||||
|
||||
<ElCard class="flex-1-hidden card-wrapper" body-class="business-table-card-body">
|
||||
<template #header>
|
||||
<div class="flex flex-wrap items-center justify-between gap-12px">
|
||||
<div class="flex items-center gap-10px">
|
||||
<p class="text-16px font-600">加班申请</p>
|
||||
<ElTag effect="plain">{{ totalCount }}</ElTag>
|
||||
</div>
|
||||
<TableHeaderOperation v-model:columns="columnChecks" :loading="loading" @refresh="reloadTable">
|
||||
<template #default>
|
||||
<ElButton plain :loading="exporting" @click="handleExport">
|
||||
<template #icon>
|
||||
<icon-mdi-download class="text-icon" />
|
||||
</template>
|
||||
导出
|
||||
</ElButton>
|
||||
<ElButton plain type="primary" @click="openAdd">
|
||||
<template #icon>
|
||||
<icon-ic-round-plus class="text-icon" />
|
||||
</template>
|
||||
新增
|
||||
</ElButton>
|
||||
</template>
|
||||
</TableHeaderOperation>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="flex-1">
|
||||
<ElTable v-loading="loading" height="100%" border row-key="id" :data="data">
|
||||
<template v-for="col in columns" :key="String(col.prop)">
|
||||
<ElTableColumn v-bind="col" />
|
||||
</template>
|
||||
</ElTable>
|
||||
</div>
|
||||
|
||||
<div class="mt-20px flex justify-end">
|
||||
<ElPagination
|
||||
v-if="mobilePagination.total"
|
||||
layout="total,prev,pager,next,sizes"
|
||||
v-bind="mobilePagination"
|
||||
@current-change="mobilePagination['current-change']"
|
||||
@size-change="mobilePagination['size-change']"
|
||||
/>
|
||||
</div>
|
||||
</ElCard>
|
||||
|
||||
<OvertimeApplicationOperateDialog
|
||||
v-model:visible="operateVisible"
|
||||
:operate-type="operateType"
|
||||
:row-data="currentRow"
|
||||
@submitted="handleSubmitted"
|
||||
/>
|
||||
|
||||
<OvertimeApplicationDetailDialog v-model:visible="detailVisible" :row-data="currentRow" />
|
||||
|
||||
<OvertimeApplicationStatusLogDialog v-model:visible="statusLogVisible" :row-data="currentRow" />
|
||||
|
||||
<OvertimeApplicationActionDialog
|
||||
v-model:visible="actionVisible"
|
||||
:action-type="currentActionType"
|
||||
:loading="actionSubmitting"
|
||||
@submit="handleActionSubmit"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
:deep(.overtime-application__reason-link) {
|
||||
max-width: 100%;
|
||||
padding: 0;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
:deep(.overtime-application__reason-link > span) {
|
||||
display: inline-block;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,113 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, nextTick, reactive, watch } from 'vue';
|
||||
import { useForm, useFormRules } from '@/hooks/common/form';
|
||||
import BusinessFormDialog from '@/components/custom/business-form-dialog.vue';
|
||||
|
||||
defineOptions({ name: 'OvertimeApplicationActionDialog' });
|
||||
|
||||
type ActionType = 'approve' | 'reject' | 'cancel';
|
||||
|
||||
interface Props {
|
||||
actionType: ActionType;
|
||||
loading?: boolean;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
loading: false
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
submit: [reason: string | null];
|
||||
}>();
|
||||
|
||||
const visible = defineModel<boolean>('visible', {
|
||||
default: false
|
||||
});
|
||||
|
||||
const { formRef, validate } = useForm();
|
||||
const { createRequiredRule } = useFormRules();
|
||||
|
||||
const model = reactive({
|
||||
reason: ''
|
||||
});
|
||||
|
||||
const title = computed(() => {
|
||||
const map: Record<ActionType, string> = {
|
||||
approve: '通过加班申请',
|
||||
reject: '退回加班申请',
|
||||
cancel: '撤销加班申请'
|
||||
};
|
||||
|
||||
return map[props.actionType];
|
||||
});
|
||||
|
||||
const reasonLabel = computed(() => {
|
||||
const map: Record<ActionType, string> = {
|
||||
approve: '审核意见',
|
||||
reject: '退回原因',
|
||||
cancel: '撤销原因'
|
||||
};
|
||||
|
||||
return map[props.actionType];
|
||||
});
|
||||
|
||||
const reasonRequired = computed(() => props.actionType !== 'approve');
|
||||
|
||||
const rules = computed(() => ({
|
||||
reason: reasonRequired.value
|
||||
? [
|
||||
createRequiredRule(`请输入${reasonLabel.value}`),
|
||||
{
|
||||
validator: (_rule, value: string, callback) => {
|
||||
if (!value?.trim()) {
|
||||
callback(new Error(`请输入${reasonLabel.value}`));
|
||||
return;
|
||||
}
|
||||
|
||||
callback();
|
||||
},
|
||||
trigger: 'blur'
|
||||
}
|
||||
]
|
||||
: []
|
||||
}));
|
||||
|
||||
async function handleSubmit() {
|
||||
await validate();
|
||||
emit('submit', model.reason.trim() || null);
|
||||
}
|
||||
|
||||
watch(
|
||||
() => visible.value,
|
||||
async value => {
|
||||
if (value) {
|
||||
model.reason = '';
|
||||
await nextTick();
|
||||
formRef.value?.clearValidate();
|
||||
}
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BusinessFormDialog
|
||||
v-model="visible"
|
||||
:title="title"
|
||||
preset="sm"
|
||||
:confirm-loading="props.loading"
|
||||
@confirm="handleSubmit"
|
||||
>
|
||||
<ElForm ref="formRef" :model="model" :rules="rules" label-position="top" :validate-on-rule-change="false">
|
||||
<ElFormItem :label="reasonLabel" prop="reason">
|
||||
<ElInput
|
||||
v-model="model.reason"
|
||||
type="textarea"
|
||||
:rows="5"
|
||||
maxlength="1000"
|
||||
show-word-limit
|
||||
:placeholder="reasonRequired ? `请输入${reasonLabel}` : '可填写审核意见'"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
</BusinessFormDialog>
|
||||
</template>
|
||||
@@ -0,0 +1,97 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { fetchGetOvertimeApplicationDetail } from '@/service/api';
|
||||
import BusinessFormDialog from '@/components/custom/business-form-dialog.vue';
|
||||
import {
|
||||
formatEmptyText,
|
||||
formatOvertimeDate,
|
||||
formatOvertimeDateTime,
|
||||
getOvertimeApplicationStatusLabel,
|
||||
resolveOvertimeApplicationStatusTagType
|
||||
} from './overtime-application-shared';
|
||||
|
||||
defineOptions({ name: 'OvertimeApplicationDetailDialog' });
|
||||
|
||||
interface Props {
|
||||
rowData?: Api.OvertimeApplication.OvertimeApplication | null;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
|
||||
const visible = defineModel<boolean>('visible', {
|
||||
default: false
|
||||
});
|
||||
|
||||
const loading = ref(false);
|
||||
const detailData = ref<Api.OvertimeApplication.OvertimeApplication | null>(null);
|
||||
|
||||
const statusTagType = computed(() => resolveOvertimeApplicationStatusTagType(detailData.value?.statusCode));
|
||||
const statusLabel = computed(() =>
|
||||
getOvertimeApplicationStatusLabel(detailData.value?.statusCode, detailData.value?.statusName)
|
||||
);
|
||||
|
||||
async function loadDetail() {
|
||||
if (!props.rowData?.id) {
|
||||
detailData.value = null;
|
||||
return;
|
||||
}
|
||||
|
||||
loading.value = true;
|
||||
const { error, data } = await fetchGetOvertimeApplicationDetail(props.rowData.id);
|
||||
loading.value = false;
|
||||
|
||||
detailData.value = error || !data ? props.rowData : data;
|
||||
}
|
||||
|
||||
watch(
|
||||
() => visible.value,
|
||||
value => {
|
||||
if (value) {
|
||||
loadDetail();
|
||||
}
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BusinessFormDialog v-model="visible" title="加班申请详情" preset="md" :loading="loading" :show-footer="false">
|
||||
<ElDescriptions v-if="detailData" :column="2" border>
|
||||
<ElDescriptionsItem label="状态">
|
||||
<ElTag :type="statusTagType">{{ statusLabel }}</ElTag>
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="申请人">
|
||||
{{ detailData.applicantName }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="加班日期">{{ formatOvertimeDate(detailData.overtimeDate) }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="加班时长">{{ detailData.overtimeDuration }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="审核人">
|
||||
{{ detailData.approverName }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="提交时间">{{ formatOvertimeDateTime(detailData.submitTime) }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="审核时间">{{ formatOvertimeDateTime(detailData.approvalTime) }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="审核意见">{{ formatEmptyText(detailData.approvalComment) }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="加班原因" :span="2">{{ detailData.overtimeReason }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="加班内容" :span="2">{{ detailData.overtimeContent }}</ElDescriptionsItem>
|
||||
</ElDescriptions>
|
||||
<ElEmpty v-else description="未获取到加班申请详情" />
|
||||
</BusinessFormDialog>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
:deep(.overtime-application-detail-dialog__readonly-input .el-input__wrapper) {
|
||||
background: linear-gradient(180deg, rgb(241 245 249 / 98%), rgb(226 232 240 / 94%)), rgb(241 245 249);
|
||||
box-shadow: 0 0 0 1px rgb(203 213 225 / 96%) inset;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
:deep(.overtime-application-detail-dialog__readonly-input .el-input__wrapper:hover),
|
||||
:deep(.overtime-application-detail-dialog__readonly-input.is-focus .el-input__wrapper) {
|
||||
box-shadow: 0 0 0 1px rgb(203 213 225 / 96%) inset;
|
||||
}
|
||||
|
||||
:deep(.overtime-application-detail-dialog__readonly-input .el-input__inner) {
|
||||
color: rgb(51 65 85 / 96%);
|
||||
cursor: default;
|
||||
-webkit-text-fill-color: rgb(51 65 85 / 96%);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,273 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, nextTick, reactive, ref, watch } from 'vue';
|
||||
import { RDMS_OVERTIME_DURATION_DICT_CODE } from '@/constants/dict';
|
||||
import {
|
||||
fetchCreateOvertimeApplication,
|
||||
fetchGetLoginUserDirectManager,
|
||||
fetchGetOvertimeApplicationDetail,
|
||||
fetchUpdateRejectedOvertimeApplication
|
||||
} from '@/service/api';
|
||||
import { useAuthStore } from '@/store/modules/auth';
|
||||
import { useForm, useFormRules } from '@/hooks/common/form';
|
||||
import BusinessFormDialog from '@/components/custom/business-form-dialog.vue';
|
||||
import BusinessFormSection from '@/components/custom/business-form-section.vue';
|
||||
import DictSelect from '@/components/custom/dict-select.vue';
|
||||
|
||||
defineOptions({ name: 'OvertimeApplicationOperateDialog' });
|
||||
|
||||
type OperateType = 'add' | 'edit';
|
||||
|
||||
interface Props {
|
||||
operateType: OperateType;
|
||||
rowData?: Api.OvertimeApplication.OvertimeApplication | null;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
submitted: [];
|
||||
}>();
|
||||
|
||||
const visible = defineModel<boolean>('visible', {
|
||||
default: false
|
||||
});
|
||||
|
||||
const authStore = useAuthStore();
|
||||
const { formRef, validate } = useForm();
|
||||
const { createRequiredRule } = useFormRules();
|
||||
|
||||
const detailLoading = ref(false);
|
||||
const submitting = ref(false);
|
||||
const approverName = ref('');
|
||||
|
||||
const currentUserName = computed(() => authStore.userInfo.nickname || authStore.userInfo.userName || '--');
|
||||
const isEdit = computed(() => props.operateType === 'edit');
|
||||
const title = computed(() => (isEdit.value ? '修改并重新提交' : '新增加班申请'));
|
||||
|
||||
const model = reactive<Api.OvertimeApplication.SaveOvertimeApplicationParams>(createDefaultModel());
|
||||
|
||||
const rules = computed(
|
||||
() =>
|
||||
({
|
||||
overtimeDate: [createRequiredRule('请选择加班日期')],
|
||||
overtimeDuration: [createRequiredRule('请选择加班时长')],
|
||||
overtimeReason: [
|
||||
createRequiredRule('请输入加班原因'),
|
||||
{
|
||||
validator: (_rule, value: string, callback) => {
|
||||
if (!value?.trim()) {
|
||||
callback(new Error('请输入加班原因'));
|
||||
return;
|
||||
}
|
||||
|
||||
callback();
|
||||
},
|
||||
trigger: 'blur'
|
||||
}
|
||||
],
|
||||
overtimeContent: [
|
||||
createRequiredRule('请输入加班内容'),
|
||||
{
|
||||
validator: (_rule, value: string, callback) => {
|
||||
if (!value?.trim()) {
|
||||
callback(new Error('请输入加班内容'));
|
||||
return;
|
||||
}
|
||||
|
||||
callback();
|
||||
},
|
||||
trigger: 'blur'
|
||||
}
|
||||
],
|
||||
approverId: [createRequiredRule('请选择审核人')]
|
||||
}) satisfies Record<keyof Api.OvertimeApplication.SaveOvertimeApplicationParams, App.Global.FormRule[]>
|
||||
);
|
||||
|
||||
function createDefaultModel(): Api.OvertimeApplication.SaveOvertimeApplicationParams {
|
||||
return {
|
||||
overtimeDate: '',
|
||||
overtimeDuration: '',
|
||||
overtimeReason: '',
|
||||
overtimeContent: '',
|
||||
approverId: ''
|
||||
};
|
||||
}
|
||||
|
||||
async function loadDirectManagerAsDefaultApprover() {
|
||||
const { error, data } = await fetchGetLoginUserDirectManager();
|
||||
|
||||
if (!error && data?.id) {
|
||||
model.approverId = data.id;
|
||||
approverName.value = data.nickname;
|
||||
}
|
||||
}
|
||||
|
||||
async function initModel() {
|
||||
detailLoading.value = true;
|
||||
Object.assign(model, createDefaultModel());
|
||||
approverName.value = '';
|
||||
|
||||
if (isEdit.value && props.rowData) {
|
||||
const { error, data } = await fetchGetOvertimeApplicationDetail(props.rowData.id);
|
||||
const detail = error || !data ? props.rowData : data;
|
||||
|
||||
model.overtimeDate = detail.overtimeDate;
|
||||
model.overtimeDuration = detail.overtimeDuration;
|
||||
model.overtimeReason = detail.overtimeReason;
|
||||
model.overtimeContent = detail.overtimeContent;
|
||||
model.approverId = detail.approverId;
|
||||
approverName.value = detail.approverName;
|
||||
} else {
|
||||
await loadDirectManagerAsDefaultApprover();
|
||||
}
|
||||
|
||||
detailLoading.value = false;
|
||||
|
||||
await nextTick();
|
||||
formRef.value?.clearValidate();
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
await validate();
|
||||
|
||||
const payload: Api.OvertimeApplication.SaveOvertimeApplicationParams = {
|
||||
overtimeDate: model.overtimeDate,
|
||||
overtimeDuration: model.overtimeDuration,
|
||||
overtimeReason: model.overtimeReason.trim(),
|
||||
overtimeContent: model.overtimeContent.trim(),
|
||||
approverId: model.approverId
|
||||
};
|
||||
|
||||
submitting.value = true;
|
||||
|
||||
const result =
|
||||
isEdit.value && props.rowData
|
||||
? await fetchUpdateRejectedOvertimeApplication(props.rowData.id, payload)
|
||||
: await fetchCreateOvertimeApplication(payload);
|
||||
|
||||
submitting.value = false;
|
||||
|
||||
if (result.error) {
|
||||
return;
|
||||
}
|
||||
|
||||
window.$message?.success(isEdit.value ? '加班申请已重新提交' : '加班申请已提交');
|
||||
visible.value = false;
|
||||
emit('submitted');
|
||||
}
|
||||
|
||||
watch(
|
||||
() => visible.value,
|
||||
value => {
|
||||
if (value) {
|
||||
initModel();
|
||||
}
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BusinessFormDialog
|
||||
v-model="visible"
|
||||
:title="title"
|
||||
preset="md"
|
||||
:loading="detailLoading"
|
||||
:confirm-loading="submitting"
|
||||
@confirm="handleSubmit"
|
||||
>
|
||||
<ElForm ref="formRef" :model="model" :rules="rules" label-position="top" :validate-on-rule-change="false">
|
||||
<BusinessFormSection title="申请信息">
|
||||
<ElRow :gutter="16">
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="申请人">
|
||||
<ElInput
|
||||
class="overtime-application-operate-dialog__readonly-input"
|
||||
:model-value="currentUserName"
|
||||
readonly
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="审核人" prop="approverId">
|
||||
<ElInput
|
||||
class="overtime-application-operate-dialog__readonly-input"
|
||||
:model-value="approverName"
|
||||
readonly
|
||||
placeholder="暂无直属上级"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="加班日期" prop="overtimeDate" style="width: 100%">
|
||||
<ElDatePicker
|
||||
v-model="model.overtimeDate"
|
||||
class="w-full"
|
||||
type="date"
|
||||
value-format="YYYY-MM-DD"
|
||||
style="width: 100%"
|
||||
placeholder="请选择加班日期"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="加班时长" prop="overtimeDuration">
|
||||
<DictSelect
|
||||
v-model="model.overtimeDuration"
|
||||
:dict-code="RDMS_OVERTIME_DURATION_DICT_CODE"
|
||||
placeholder="请选择加班时长"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
</BusinessFormSection>
|
||||
|
||||
<BusinessFormSection title="加班说明">
|
||||
<ElRow :gutter="16">
|
||||
<ElCol :span="24">
|
||||
<ElFormItem label="加班原因" prop="overtimeReason">
|
||||
<ElInput
|
||||
v-model="model.overtimeReason"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
maxlength="500"
|
||||
show-word-limit
|
||||
placeholder="请输入加班原因"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="24">
|
||||
<ElFormItem label="加班内容" prop="overtimeContent">
|
||||
<ElInput
|
||||
v-model="model.overtimeContent"
|
||||
type="textarea"
|
||||
:rows="4"
|
||||
maxlength="1000"
|
||||
show-word-limit
|
||||
placeholder="请输入加班内容"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
</BusinessFormSection>
|
||||
</ElForm>
|
||||
</BusinessFormDialog>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
:deep(.overtime-application-operate-dialog__readonly-input .el-input__wrapper) {
|
||||
background: linear-gradient(180deg, rgb(241 245 249 / 98%), rgb(226 232 240 / 94%)), rgb(241 245 249);
|
||||
box-shadow: 0 0 0 1px rgb(203 213 225 / 96%) inset;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
:deep(.overtime-application-operate-dialog__readonly-input .el-input__wrapper:hover),
|
||||
:deep(.overtime-application-operate-dialog__readonly-input.is-focus .el-input__wrapper) {
|
||||
box-shadow: 0 0 0 1px rgb(203 213 225 / 96%) inset;
|
||||
}
|
||||
|
||||
:deep(.overtime-application-operate-dialog__readonly-input .el-input__inner) {
|
||||
color: rgb(51 65 85 / 96%);
|
||||
cursor: default;
|
||||
-webkit-text-fill-color: rgb(51 65 85 / 96%);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,95 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, reactive, watch } from 'vue';
|
||||
import { RDMS_OVERTIME_APPLICATION_STATUS_DICT_CODE } from '@/constants/dict';
|
||||
import TableSearchFields, { type SearchField } from '@/components/custom/table-search-fields.vue';
|
||||
|
||||
defineOptions({ name: 'OvertimeApplicationSearch' });
|
||||
|
||||
const emit = defineEmits<{
|
||||
reset: [];
|
||||
search: [];
|
||||
}>();
|
||||
|
||||
const model = defineModel<Api.OvertimeApplication.OvertimeApplicationSearchParams>('model', {
|
||||
required: true
|
||||
});
|
||||
|
||||
const searchModel = reactive<Record<string, any>>({
|
||||
applicantName: '',
|
||||
overtimeDate: undefined,
|
||||
statusCode: undefined,
|
||||
approverName: ''
|
||||
});
|
||||
|
||||
let syncingFromSource = false;
|
||||
|
||||
watch(
|
||||
() =>
|
||||
[model.value.applicantName, model.value.overtimeDate, model.value.statusCode, model.value.approverName] as const,
|
||||
([applicantName, overtimeDate, statusCode, approverName]) => {
|
||||
syncingFromSource = true;
|
||||
searchModel.applicantName = applicantName ?? '';
|
||||
searchModel.overtimeDate = overtimeDate;
|
||||
searchModel.statusCode = statusCode;
|
||||
searchModel.approverName = approverName ?? '';
|
||||
syncingFromSource = false;
|
||||
},
|
||||
{ immediate: true, flush: 'sync' }
|
||||
);
|
||||
|
||||
watch(
|
||||
() =>
|
||||
[searchModel.applicantName, searchModel.overtimeDate, searchModel.statusCode, searchModel.approverName] as const,
|
||||
([applicantName, overtimeDate, statusCode, approverName]) => {
|
||||
if (syncingFromSource) {
|
||||
return;
|
||||
}
|
||||
|
||||
model.value.applicantName = applicantName?.trim() || undefined;
|
||||
model.value.overtimeDate = overtimeDate;
|
||||
model.value.statusCode = statusCode;
|
||||
model.value.approverName = approverName?.trim() || undefined;
|
||||
},
|
||||
{ flush: 'sync' }
|
||||
);
|
||||
|
||||
const fields = computed<SearchField[]>(() => [
|
||||
{
|
||||
key: 'applicantName',
|
||||
label: '申请人',
|
||||
type: 'input',
|
||||
placeholder: '请输入申请人'
|
||||
},
|
||||
{
|
||||
key: 'overtimeDate',
|
||||
label: '加班日期',
|
||||
type: 'dateRange',
|
||||
placeholder: '请选择加班日期'
|
||||
},
|
||||
{
|
||||
key: 'statusCode',
|
||||
label: '状态',
|
||||
type: 'dict',
|
||||
dictCode: RDMS_OVERTIME_APPLICATION_STATUS_DICT_CODE,
|
||||
placeholder: '请选择状态'
|
||||
},
|
||||
{
|
||||
key: 'approverName',
|
||||
label: '审核人',
|
||||
type: 'input',
|
||||
placeholder: '请输入审核人'
|
||||
}
|
||||
]);
|
||||
|
||||
function handleReset() {
|
||||
emit('reset');
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
emit('search');
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<TableSearchFields v-model="searchModel" :fields="fields" :columns="4" @reset="handleReset" @search="handleSearch" />
|
||||
</template>
|
||||
@@ -0,0 +1,77 @@
|
||||
import dayjs from 'dayjs';
|
||||
import { getStatusTagType } from '@/constants/status-tag';
|
||||
|
||||
export const overtimeApplicationStatusOptions: Array<{
|
||||
label: string;
|
||||
value: Api.OvertimeApplication.OvertimeApplicationStatusCode;
|
||||
}> = [
|
||||
{ label: '待审批', value: 'pending' },
|
||||
{ label: '已通过', value: 'approved' },
|
||||
{ label: '已退回', value: 'rejected' },
|
||||
{ label: '已撤销', value: 'cancelled' }
|
||||
];
|
||||
|
||||
export const overtimeApplicationActionNameMap: Record<Api.OvertimeApplication.OvertimeApplicationActionType, string> = {
|
||||
submit: '提交',
|
||||
resubmit: '重新提交',
|
||||
approve: '通过',
|
||||
reject: '退回',
|
||||
cancel: '撤销'
|
||||
};
|
||||
|
||||
export function getOvertimeApplicationStatusLabel(statusCode?: string | null, statusName?: string | null) {
|
||||
if (statusName) {
|
||||
return statusName;
|
||||
}
|
||||
|
||||
return overtimeApplicationStatusOptions.find(item => item.value === statusCode)?.label || statusCode || '--';
|
||||
}
|
||||
|
||||
export function resolveOvertimeApplicationStatusTagType(statusCode?: string | null) {
|
||||
return getStatusTagType('overtimeApplication', statusCode);
|
||||
}
|
||||
|
||||
export function getOvertimeApplicationActionLabel(actionType?: string | null) {
|
||||
if (!actionType) {
|
||||
return '--';
|
||||
}
|
||||
|
||||
return (
|
||||
overtimeApplicationActionNameMap[actionType as Api.OvertimeApplication.OvertimeApplicationActionType] || actionType
|
||||
);
|
||||
}
|
||||
|
||||
export function formatOvertimeDate(value?: string | null) {
|
||||
if (!value) {
|
||||
return '--';
|
||||
}
|
||||
|
||||
const target = dayjs(value);
|
||||
|
||||
return target.isValid() ? target.format('YYYY-MM-DD') : value;
|
||||
}
|
||||
|
||||
export function formatOvertimeDateTime(value?: string | null) {
|
||||
if (!value) {
|
||||
return '--';
|
||||
}
|
||||
|
||||
const target = dayjs(value);
|
||||
|
||||
return target.isValid() ? target.format('YYYY-MM-DD HH:mm:ss') : value;
|
||||
}
|
||||
|
||||
export function formatEmptyText(value?: string | null) {
|
||||
return value?.trim() || '--';
|
||||
}
|
||||
|
||||
export function downloadBlob(blob: Blob, fileName: string) {
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
|
||||
link.href = url;
|
||||
link.download = fileName;
|
||||
link.click();
|
||||
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
<script setup lang="tsx">
|
||||
import { ref, watch } from 'vue';
|
||||
import { ElTag } from 'element-plus';
|
||||
import { fetchGetOvertimeApplicationStatusLogs } from '@/service/api';
|
||||
import BusinessFormDialog from '@/components/custom/business-form-dialog.vue';
|
||||
import {
|
||||
formatEmptyText,
|
||||
formatOvertimeDate,
|
||||
formatOvertimeDateTime,
|
||||
getOvertimeApplicationActionLabel,
|
||||
getOvertimeApplicationStatusLabel,
|
||||
resolveOvertimeApplicationStatusTagType
|
||||
} from './overtime-application-shared';
|
||||
|
||||
defineOptions({ name: 'OvertimeApplicationStatusLogDialog' });
|
||||
|
||||
interface Props {
|
||||
rowData?: Api.OvertimeApplication.OvertimeApplication | null;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
|
||||
const visible = defineModel<boolean>('visible', {
|
||||
default: false
|
||||
});
|
||||
|
||||
const loading = ref(false);
|
||||
const logs = ref<Api.OvertimeApplication.OvertimeApplicationStatusLog[]>([]);
|
||||
|
||||
async function loadLogs() {
|
||||
if (!props.rowData?.id) {
|
||||
logs.value = [];
|
||||
return;
|
||||
}
|
||||
|
||||
loading.value = true;
|
||||
const { error, data } = await fetchGetOvertimeApplicationStatusLogs(props.rowData.id);
|
||||
loading.value = false;
|
||||
|
||||
logs.value = error || !data ? [] : data;
|
||||
}
|
||||
|
||||
function renderStatus(code?: string | null) {
|
||||
if (!code) {
|
||||
return '--';
|
||||
}
|
||||
|
||||
return <ElTag type={resolveOvertimeApplicationStatusTagType(code)}>{getOvertimeApplicationStatusLabel(code)}</ElTag>;
|
||||
}
|
||||
|
||||
watch(
|
||||
() => visible.value,
|
||||
value => {
|
||||
if (value) {
|
||||
loadLogs();
|
||||
}
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BusinessFormDialog
|
||||
v-model="visible"
|
||||
title="状态日志"
|
||||
width="920px"
|
||||
:loading="loading"
|
||||
:show-footer="false"
|
||||
max-body-height="72vh"
|
||||
>
|
||||
<ElTable border :data="logs">
|
||||
<ElTableColumn prop="createTime" label="操作时间" width="170">
|
||||
<template #default="{ row }">{{ formatOvertimeDateTime(row.createTime) }}</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn prop="actionType" label="动作" width="110">
|
||||
<template #default="{ row }">{{ getOvertimeApplicationActionLabel(row.actionType) }}</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn prop="operatorName" label="操作人" width="120" show-overflow-tooltip />
|
||||
<ElTableColumn prop="fromStatus" label="原状态" width="110" :formatter="row => renderStatus(row.fromStatus)" />
|
||||
<ElTableColumn prop="toStatus" label="新状态" width="110" :formatter="row => renderStatus(row.toStatus)" />
|
||||
<ElTableColumn prop="reason" label="原因/意见" min-width="180" show-overflow-tooltip>
|
||||
<template #default="{ row }">{{ formatEmptyText(row.reason) }}</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn prop="overtimeDateSnapshot" label="加班日期" width="120">
|
||||
<template #default="{ row }">{{ formatOvertimeDate(row.overtimeDateSnapshot) }}</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn prop="overtimeDurationSnapshot" label="时长" width="90" show-overflow-tooltip />
|
||||
</ElTable>
|
||||
</BusinessFormDialog>
|
||||
</template>
|
||||
@@ -93,6 +93,10 @@ function getDictStatusTagType(status: Api.Dict.DictStatus): UI.ThemeColor {
|
||||
return status === 0 ? 'success' : 'info';
|
||||
}
|
||||
|
||||
function formatColorType(value?: string | null) {
|
||||
return value?.trim() || '--';
|
||||
}
|
||||
|
||||
const dictTypeSearchParams = reactive(getInitDictTypeSearchParams());
|
||||
const dictDataSearchParams = reactive(getInitDictDataSearchParams());
|
||||
|
||||
@@ -177,6 +181,13 @@ const {
|
||||
{ prop: 'index', type: 'index', label: $t('common.index'), width: 64 },
|
||||
{ prop: 'label', label: $t('page.system.dict.dictLabel'), minWidth: 160 },
|
||||
{ prop: 'value', label: $t('page.system.dict.dictValue'), minWidth: 180 },
|
||||
{
|
||||
prop: 'colorType',
|
||||
label: $t('page.system.dict.colorType'),
|
||||
width: 120,
|
||||
align: 'center',
|
||||
formatter: row => formatColorType(row.colorType)
|
||||
},
|
||||
{ prop: 'sort', label: $t('page.system.dict.sort'), width: 90, align: 'center' },
|
||||
{
|
||||
prop: 'status',
|
||||
|
||||
@@ -53,6 +53,7 @@ function createDefaultModel(): Model {
|
||||
label: '',
|
||||
value: '',
|
||||
dictType: '',
|
||||
colorType: '',
|
||||
sort: 0,
|
||||
status: 0,
|
||||
remark: ''
|
||||
@@ -81,6 +82,7 @@ function handleInitModel() {
|
||||
label: props.rowData.label,
|
||||
value: props.rowData.value,
|
||||
dictType: props.rowData.dictType,
|
||||
colorType: props.rowData.colorType ?? '',
|
||||
sort: props.rowData.sort,
|
||||
status: props.rowData.status,
|
||||
remark: props.rowData.remark ?? ''
|
||||
@@ -168,6 +170,11 @@ watch(visible, value => {
|
||||
<ElInput v-model="model.value" :placeholder="$t('page.system.dict.form.dictValue')" />
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="12">
|
||||
<ElFormItem :label="$t('page.system.dict.colorType')" prop="colorType">
|
||||
<ElInput v-model="model.colorType" clearable :placeholder="$t('page.system.dict.form.colorType')" />
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="12">
|
||||
<ElFormItem :label="$t('page.system.dict.sort')" prop="sort">
|
||||
<ElInputNumber
|
||||
|
||||
@@ -62,6 +62,10 @@ export interface WorkbenchTodoItemSource {
|
||||
overdue?: boolean;
|
||||
/** 跳转路由 key(可选,未配则不跳转) */
|
||||
routeKey?: string;
|
||||
/** 审批业务类型 */
|
||||
approvalBizType?: 'overtime_application' | string;
|
||||
/** 审批业务 ID */
|
||||
approvalBizId?: string;
|
||||
}
|
||||
|
||||
export interface WorkbenchTodoItem extends Omit<WorkbenchTodoItemSource, 'deadline'> {
|
||||
|
||||
@@ -1,8 +1,16 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { computed, markRaw, onMounted, ref, watch } from 'vue';
|
||||
import type { RouteKey } from '@elegant-router/types';
|
||||
import {
|
||||
fetchApproveOvertimeApplication,
|
||||
fetchGetOvertimeApplicationApprovalPage,
|
||||
fetchRejectOvertimeApplication
|
||||
} from '@/service/api';
|
||||
import { useRouterPush } from '@/hooks/common/router';
|
||||
import PersonalItemOperateDialog from '@/views/personal-center/my-item/modules/personal-item-operate-dialog.vue';
|
||||
import OvertimeApplicationActionDialog from '@/views/personal-center/overtime-application/modules/overtime-application-action-dialog.vue';
|
||||
import OvertimeApplicationDetailDialog from '@/views/personal-center/overtime-application/modules/overtime-application-detail-dialog.vue';
|
||||
import OvertimeApplicationStatusLogDialog from '@/views/personal-center/overtime-application/modules/overtime-application-status-log-dialog.vue';
|
||||
import {
|
||||
type WorkbenchTodoDeadlineFilter,
|
||||
type WorkbenchTodoItem,
|
||||
@@ -15,8 +23,14 @@ import {
|
||||
} from '../homepage';
|
||||
import { workbenchTodoMock } from '../mock';
|
||||
import WorkbenchModuleCard from './workbench-module-card.vue';
|
||||
import IconMdiCheckCircleOutline from '~icons/mdi/check-circle-outline';
|
||||
import IconMdiCloseCircleOutline from '~icons/mdi/close-circle-outline';
|
||||
import IconMdiEyeOutline from '~icons/mdi/eye-outline';
|
||||
import IconMdiHistory from '~icons/mdi/history';
|
||||
|
||||
type SortKey = 'created' | 'priority' | 'deadline';
|
||||
type OvertimeApprovalActionType = 'approve' | 'reject';
|
||||
type ApprovalBizType = 'overtime_application';
|
||||
|
||||
defineOptions({ name: 'WorkbenchTodoPanel' });
|
||||
|
||||
@@ -38,6 +52,7 @@ const PAGE_SIZE = 5;
|
||||
|
||||
const activeTab = ref<WorkbenchTodoMainTab>('all');
|
||||
const activeDeadlineFilter = ref<WorkbenchTodoDeadlineFilter>(null);
|
||||
const activeApprovalBizType = ref<ApprovalBizType>('overtime_application');
|
||||
const activeSort = ref<SortKey>('deadline');
|
||||
const currentPage = ref(1);
|
||||
|
||||
@@ -66,9 +81,33 @@ const deadlineFilters: Array<{ key: Exclude<WorkbenchTodoDeadlineFilter, null>;
|
||||
{ key: 'week', label: '本周到期' }
|
||||
];
|
||||
|
||||
const approvalBizTabs: Array<{ key: ApprovalBizType; label: string }> = [
|
||||
{ key: 'overtime_application', label: '加班申请' }
|
||||
];
|
||||
|
||||
const allItems = computed(() => buildWorkbenchTodoItems(workbenchTodoMock));
|
||||
const overtimeApprovalItems = ref<WorkbenchTodoItem[]>([]);
|
||||
const overtimeApprovalRows = ref<Api.OvertimeApplication.OvertimeApplication[]>([]);
|
||||
const mergedItems = computed(() => {
|
||||
const mockItems = allItems.value.filter(item => item.category !== 'approval');
|
||||
|
||||
return [...mockItems, ...overtimeApprovalItems.value];
|
||||
});
|
||||
|
||||
const addDialogVisible = ref(false);
|
||||
const overtimeDetailVisible = ref(false);
|
||||
const overtimeStatusLogVisible = ref(false);
|
||||
const overtimeActionVisible = ref(false);
|
||||
const overtimeActionSubmitting = ref(false);
|
||||
const currentOvertimeApplication = ref<Api.OvertimeApplication.OvertimeApplication | null>(null);
|
||||
const currentOvertimeActionType = ref<OvertimeApprovalActionType>('approve');
|
||||
|
||||
const OVERTIME_APPROVAL_ACTION_ICONS = {
|
||||
detail: markRaw(IconMdiEyeOutline),
|
||||
approve: markRaw(IconMdiCheckCircleOutline),
|
||||
reject: markRaw(IconMdiCloseCircleOutline),
|
||||
statusLog: markRaw(IconMdiHistory)
|
||||
};
|
||||
|
||||
function handleOpenAdd() {
|
||||
addDialogVisible.value = true;
|
||||
@@ -81,13 +120,13 @@ function handleAddSubmitted() {
|
||||
|
||||
const tabCounts = computed(() => {
|
||||
const counts: Record<WorkbenchTodoMainTab, number> = {
|
||||
all: allItems.value.length,
|
||||
all: mergedItems.value.length,
|
||||
task: 0,
|
||||
ticket: 0,
|
||||
personal: 0,
|
||||
approval: 0
|
||||
};
|
||||
allItems.value.forEach(item => {
|
||||
mergedItems.value.forEach(item => {
|
||||
counts[item.category] += 1;
|
||||
});
|
||||
return counts;
|
||||
@@ -101,7 +140,7 @@ const tabOverdueCount = computed(() => {
|
||||
personal: 0,
|
||||
approval: 0
|
||||
};
|
||||
allItems.value.forEach(item => {
|
||||
mergedItems.value.forEach(item => {
|
||||
if (!isWorkbenchTodoOverdue(item)) return;
|
||||
map.all += 1;
|
||||
map[item.category] += 1;
|
||||
@@ -109,9 +148,29 @@ const tabOverdueCount = computed(() => {
|
||||
return map;
|
||||
});
|
||||
|
||||
const itemsInTab = computed(() => filterWorkbenchTodoItemsByCategory(allItems.value, activeTab.value));
|
||||
const itemsInTab = computed(() => filterWorkbenchTodoItemsByCategory(mergedItems.value, activeTab.value));
|
||||
|
||||
const filteredItems = computed(() => filterWorkbenchTodoItemsByDeadline(itemsInTab.value, activeDeadlineFilter.value));
|
||||
const filteredItems = computed(() => {
|
||||
if (activeTab.value === 'approval') {
|
||||
return itemsInTab.value.filter(item => item.approvalBizType === activeApprovalBizType.value);
|
||||
}
|
||||
|
||||
return filterWorkbenchTodoItemsByDeadline(itemsInTab.value, activeDeadlineFilter.value);
|
||||
});
|
||||
|
||||
const approvalBizTabCounts = computed(() => {
|
||||
const counts: Record<ApprovalBizType, number> = {
|
||||
overtime_application: 0
|
||||
};
|
||||
|
||||
itemsInTab.value.forEach(item => {
|
||||
if (item.approvalBizType === 'overtime_application') {
|
||||
counts.overtime_application += 1;
|
||||
}
|
||||
});
|
||||
|
||||
return counts;
|
||||
});
|
||||
|
||||
const sortedItems = computed(() => {
|
||||
const base = filteredItems.value;
|
||||
@@ -155,20 +214,121 @@ function handleSelectDeadlineFilter(key: Exclude<WorkbenchTodoDeadlineFilter, nu
|
||||
activeDeadlineFilter.value = activeDeadlineFilter.value === key ? null : key;
|
||||
}
|
||||
|
||||
function handleSelectApprovalBizType(key: ApprovalBizType) {
|
||||
activeApprovalBizType.value = key;
|
||||
}
|
||||
|
||||
function handleSelectSort(key: SortKey) {
|
||||
activeSort.value = key;
|
||||
}
|
||||
|
||||
function handleClickItem(item: WorkbenchTodoItem) {
|
||||
if (item.approvalBizType === 'overtime_application') {
|
||||
openOvertimeDetail(item);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!item.routeKey) return;
|
||||
routerPushByKey(item.routeKey as RouteKey);
|
||||
}
|
||||
|
||||
function findOvertimeApprovalRow(item: WorkbenchTodoItem) {
|
||||
if (!item.approvalBizId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return overtimeApprovalRows.value.find(row => row.id === item.approvalBizId) || null;
|
||||
}
|
||||
|
||||
function openOvertimeDetail(item: WorkbenchTodoItem) {
|
||||
const row = findOvertimeApprovalRow(item);
|
||||
if (!row) return;
|
||||
|
||||
currentOvertimeApplication.value = row;
|
||||
overtimeDetailVisible.value = true;
|
||||
}
|
||||
|
||||
function openOvertimeStatusLog(item: WorkbenchTodoItem) {
|
||||
const row = findOvertimeApprovalRow(item);
|
||||
if (!row) return;
|
||||
|
||||
currentOvertimeApplication.value = row;
|
||||
overtimeStatusLogVisible.value = true;
|
||||
}
|
||||
|
||||
function openOvertimeAction(item: WorkbenchTodoItem, actionType: OvertimeApprovalActionType) {
|
||||
const row = findOvertimeApprovalRow(item);
|
||||
if (!row) return;
|
||||
|
||||
currentOvertimeApplication.value = row;
|
||||
currentOvertimeActionType.value = actionType;
|
||||
overtimeActionVisible.value = true;
|
||||
}
|
||||
|
||||
async function handleOvertimeActionSubmit(reason: string | null) {
|
||||
if (!currentOvertimeApplication.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
overtimeActionSubmitting.value = true;
|
||||
const result =
|
||||
currentOvertimeActionType.value === 'approve'
|
||||
? await fetchApproveOvertimeApplication(currentOvertimeApplication.value.id, { reason })
|
||||
: await fetchRejectOvertimeApplication(currentOvertimeApplication.value.id, { reason });
|
||||
overtimeActionSubmitting.value = false;
|
||||
|
||||
if (result.error) {
|
||||
return;
|
||||
}
|
||||
|
||||
overtimeActionVisible.value = false;
|
||||
overtimeDetailVisible.value = false;
|
||||
window.$message?.success(currentOvertimeActionType.value === 'approve' ? '加班申请已通过' : '加班申请已退回');
|
||||
await loadOvertimeApprovalItems();
|
||||
}
|
||||
|
||||
async function loadOvertimeApprovalItems() {
|
||||
const { error, data } = await fetchGetOvertimeApplicationApprovalPage({
|
||||
pageNo: 1,
|
||||
pageSize: 20,
|
||||
statusCode: 'pending',
|
||||
keyword: undefined,
|
||||
applicantName: undefined,
|
||||
approverId: undefined,
|
||||
approverName: undefined,
|
||||
overtimeDate: undefined,
|
||||
createTime: undefined
|
||||
});
|
||||
|
||||
if (error || !data) {
|
||||
overtimeApprovalRows.value = [];
|
||||
overtimeApprovalItems.value = [];
|
||||
return;
|
||||
}
|
||||
|
||||
overtimeApprovalRows.value = data.list;
|
||||
overtimeApprovalItems.value = buildWorkbenchTodoItems(
|
||||
data.list.map(item => ({
|
||||
id: `overtime-application-${item.id}`,
|
||||
category: 'approval',
|
||||
title: `${item.applicantName} · ${item.overtimeDate.slice(5, 7)} 月加班 ${item.overtimeDuration} 申请待审批`,
|
||||
createdTime: item.submitTime || item.createTime,
|
||||
deadline: item.submitTime || item.createTime,
|
||||
source: `加班申请 · ${item.applicantName}`,
|
||||
priority: 'mid',
|
||||
approvalBizType: 'overtime_application',
|
||||
approvalBizId: item.id
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
function getDeadlineToneClass(item: WorkbenchTodoItem) {
|
||||
if (isWorkbenchTodoOverdue(item)) return 'workbench-todo__deadline--rose';
|
||||
if (item.remainingDays === 0) return 'workbench-todo__deadline--amber';
|
||||
return 'workbench-todo__deadline--slate';
|
||||
}
|
||||
|
||||
onMounted(loadOvertimeApprovalItems);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -222,7 +382,19 @@ function getDeadlineToneClass(item: WorkbenchTodoItem) {
|
||||
{{ filter.label }}
|
||||
</button>
|
||||
</div>
|
||||
<div v-else></div>
|
||||
<div v-else class="workbench-todo__filters-left">
|
||||
<button
|
||||
v-for="tab in approvalBizTabs"
|
||||
:key="tab.key"
|
||||
type="button"
|
||||
class="workbench-todo__filter"
|
||||
:class="{ 'workbench-todo__filter--active': activeApprovalBizType === tab.key }"
|
||||
@click="handleSelectApprovalBizType(tab.key)"
|
||||
>
|
||||
{{ tab.label }}
|
||||
<span class="workbench-todo__filter-count">{{ approvalBizTabCounts[tab.key] }}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<ElDropdown trigger="click" placement="bottom-end" @command="handleSelectSort">
|
||||
<span class="workbench-todo__sort">
|
||||
@@ -250,7 +422,7 @@ function getDeadlineToneClass(item: WorkbenchTodoItem) {
|
||||
v-for="item in pagedItems"
|
||||
:key="item.id"
|
||||
class="workbench-todo__item"
|
||||
:class="{ 'workbench-todo__item--clickable': Boolean(item.routeKey) }"
|
||||
:class="{ 'workbench-todo__item--clickable': Boolean(item.routeKey || item.approvalBizType) }"
|
||||
@click="handleClickItem(item)"
|
||||
>
|
||||
<div class="workbench-todo__leading">
|
||||
@@ -268,6 +440,38 @@ function getDeadlineToneClass(item: WorkbenchTodoItem) {
|
||||
</div>
|
||||
|
||||
<div class="workbench-todo__trailing">
|
||||
<div v-if="item.approvalBizType === 'overtime_application'" class="workbench-todo__actions" @click.stop>
|
||||
<ElTooltip content="详情">
|
||||
<ElButton link type="primary" class="workbench-todo__action-btn" @click="openOvertimeDetail(item)">
|
||||
<component :is="OVERTIME_APPROVAL_ACTION_ICONS.detail" class="text-15px" />
|
||||
</ElButton>
|
||||
</ElTooltip>
|
||||
<ElTooltip content="通过">
|
||||
<ElButton
|
||||
link
|
||||
type="success"
|
||||
class="workbench-todo__action-btn"
|
||||
@click="openOvertimeAction(item, 'approve')"
|
||||
>
|
||||
<component :is="OVERTIME_APPROVAL_ACTION_ICONS.approve" class="text-15px" />
|
||||
</ElButton>
|
||||
</ElTooltip>
|
||||
<ElTooltip content="退回">
|
||||
<ElButton
|
||||
link
|
||||
type="danger"
|
||||
class="workbench-todo__action-btn"
|
||||
@click="openOvertimeAction(item, 'reject')"
|
||||
>
|
||||
<component :is="OVERTIME_APPROVAL_ACTION_ICONS.reject" class="text-15px" />
|
||||
</ElButton>
|
||||
</ElTooltip>
|
||||
<ElTooltip content="状态日志">
|
||||
<ElButton link type="info" class="workbench-todo__action-btn" @click="openOvertimeStatusLog(item)">
|
||||
<component :is="OVERTIME_APPROVAL_ACTION_ICONS.statusLog" class="text-15px" />
|
||||
</ElButton>
|
||||
</ElTooltip>
|
||||
</div>
|
||||
<span class="workbench-todo__deadline" :class="getDeadlineToneClass(item)">
|
||||
{{ item.deadlineLabel }}
|
||||
</span>
|
||||
@@ -295,6 +499,18 @@ function getDeadlineToneClass(item: WorkbenchTodoItem) {
|
||||
:row-data="null"
|
||||
@submitted="handleAddSubmitted"
|
||||
/>
|
||||
|
||||
<OvertimeApplicationDetailDialog v-model:visible="overtimeDetailVisible" :row-data="currentOvertimeApplication" />
|
||||
<OvertimeApplicationStatusLogDialog
|
||||
v-model:visible="overtimeStatusLogVisible"
|
||||
:row-data="currentOvertimeApplication"
|
||||
/>
|
||||
<OvertimeApplicationActionDialog
|
||||
v-model:visible="overtimeActionVisible"
|
||||
:action-type="currentOvertimeActionType"
|
||||
:loading="overtimeActionSubmitting"
|
||||
@submit="handleOvertimeActionSubmit"
|
||||
/>
|
||||
</WorkbenchModuleCard>
|
||||
</template>
|
||||
|
||||
@@ -468,6 +684,12 @@ function getDeadlineToneClass(item: WorkbenchTodoItem) {
|
||||
color: rgb(190 18 60 / 96%);
|
||||
}
|
||||
|
||||
.workbench-todo__filter-count {
|
||||
margin-left: 4px;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.workbench-todo__content {
|
||||
min-height: 400px;
|
||||
display: flex;
|
||||
@@ -596,6 +818,25 @@ function getDeadlineToneClass(item: WorkbenchTodoItem) {
|
||||
.workbench-todo__trailing {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.workbench-todo__actions {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.workbench-todo__actions :deep(.el-button + .el-button) {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
:deep(.workbench-todo__action-btn) {
|
||||
min-width: auto;
|
||||
height: auto;
|
||||
padding: 3px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.workbench-todo__deadline {
|
||||
|
||||
Reference in New Issue
Block a user