feat: 添加请假申请功能

This commit is contained in:
AN 2025-05-29 10:34:00 +08:00
parent b265f590e4
commit d7e0516cfb
15 changed files with 531 additions and 4 deletions

View File

@ -20,8 +20,13 @@ const attrs: TreeSelectProps = useAttrs();
const { loading, startLoading, endLoading } = useLoading();
/** 转换为strid可能是number类型或者String类型导致回显失败 */
const strValue = computed(() => {
return isNull(rawValue.value) ? null : rawValue.value?.toString();
const strValue = computed({
get() {
return isNull(rawValue.value) ? null : rawValue.value?.toString();
},
set(val) {
rawValue.value = val;
}
});
async function getCategoryList() {

View File

@ -1,5 +1,15 @@
import { transformRecordToOption } from '@/utils/common';
/** leave type */
export const leaveTypeRecord: Record<Api.Workflow.LeaveType, string> = {
'1': '事假',
'2': '调休',
'3': '病假',
'4': '婚假'
};
export const leaveTypeOptions = transformRecordToOption(leaveTypeRecord);
/** workflow publish status */
export const workflowPublishStatusRecord: Record<Api.Workflow.WorkflowPublishStatus, string> = {
'0': '未发布',

View File

@ -219,7 +219,8 @@ const local: App.I18n.Schema = {
exception_404: '404',
exception_500: '500',
'workflow_process-definition': 'Process Definition',
'workflow_process-instance': 'Process Instance'
'workflow_process-instance': 'Process Instance',
workflow_leave: 'Leave Apply'
},
page: {
login: {

View File

@ -219,7 +219,8 @@ const local: App.I18n.Schema = {
exception_404: '404',
exception_500: '500',
'workflow_process-definition': '流程定义',
'workflow_process-instance': '流程实例'
'workflow_process-instance': '流程实例',
workflow_leave: '请假申请'
},
page: {
login: {

View File

@ -44,6 +44,7 @@ export const views: Record<LastLevelRouteKey, RouteComponent | (() => Promise<Ro
system_user: () => import("@/views/system/user/index.vue"),
tool_gen: () => import("@/views/tool/gen/index.vue"),
workflow_category: () => import("@/views/workflow/category/index.vue"),
workflow_leave: () => import("@/views/workflow/leave/index.vue"),
"workflow_process-definition": () => import("@/views/workflow/process-definition/index.vue"),
"workflow_process-instance": () => import("@/views/workflow/process-instance/index.vue"),
};

View File

@ -350,6 +350,15 @@ export const generatedRoutes: GeneratedRoute[] = [
i18nKey: 'route.workflow_category'
}
},
{
name: 'workflow_leave',
path: '/workflow/leave',
component: 'view.workflow_leave',
meta: {
title: 'workflow_leave',
i18nKey: 'route.workflow_leave'
}
},
{
name: 'workflow_process-definition',
path: '/workflow/process-definition',

View File

@ -201,6 +201,7 @@ const routeMap: RouteMap = {
"user-center": "/user-center",
"workflow": "/workflow",
"workflow_category": "/workflow/category",
"workflow_leave": "/workflow/leave",
"workflow_process-definition": "/workflow/process-definition",
"workflow_process-instance": "/workflow/process-instance"
};

View File

@ -1,2 +1,4 @@
export * from './category';
export * from './leave';
export * from './instance';
export * from './definition';

View File

@ -0,0 +1,36 @@
import { request } from '@/service/request';
/** 获取请假申请列表 */
export function fetchGetLeaveList(params?: Api.Workflow.LeaveSearchParams) {
return request<Api.Workflow.LeaveList>({
url: '/workflow/leave/list',
method: 'get',
params
});
}
/** 新增请假申请 */
export function fetchCreateLeave(data: Api.Workflow.LeaveOperateParams) {
return request<boolean>({
url: '/workflow/leave',
method: 'post',
data
});
}
/** 修改请假申请 */
export function fetchUpdateLeave(data: Api.Workflow.LeaveOperateParams) {
return request<boolean>({
url: '/workflow/leave',
method: 'put',
data
});
}
/** 批量删除请假申请 */
export function fetchBatchDeleteLeave(ids: CommonType.IdType[]) {
return request<boolean>({
url: `/workflow/leave/${ids.join(',')}`,
method: 'delete'
});
}

View File

@ -10,6 +10,40 @@ declare namespace Api {
* backend api module: "Workflow"
*/
namespace Workflow {
/** 请假状态 */
type LeaveType = '1' | '2' | '3' | '4';
/** leave */
type Leave = Common.CommonRecord<{
/** id */
id: CommonType.IdType;
/** 租户编号 */
tenantId: CommonType.IdType;
/** 请假类型 */
leaveType: LeaveType;
/** 开始时间 */
startDate: string;
/** 结束时间 */
endDate: string;
/** 请假天数 */
leaveDays: number;
/** 请假原因 */
remark: string;
/** 状态 */
status: string;
}>;
/** leave search params */
type LeaveSearchParams = CommonType.RecordNullable<
Pick<Api.Workflow.Leave, 'leaveDays'> & Api.Common.CommonSearchParams
>;
/** leave operate params */
type LeaveOperateParams = CommonType.RecordNullable<
Pick<Api.Workflow.Leave, 'id' | 'leaveType' | 'startDate' | 'endDate' | 'leaveDays' | 'remark'>
>;
/** leave list */
type LeaveList = Api.Common.PaginatingQueryRecord<Leave>;
/** 工作流分类 */
type WorkflowCategory = Common.CommonRecord<{
/** 主键 */

View File

@ -92,6 +92,7 @@ declare module 'vue' {
NInput: typeof import('naive-ui')['NInput']
NInputGroup: typeof import('naive-ui')['NInputGroup']
NInputGroupLabel: typeof import('naive-ui')['NInputGroupLabel']
NInputNubmer: typeof import('naive-ui')['NInputNubmer']
NInputNumber: typeof import('naive-ui')['NInputNumber']
NLayout: typeof import('naive-ui')['NLayout']
NLayoutContent: typeof import('naive-ui')['NLayoutContent']

View File

@ -55,6 +55,7 @@ declare module "@elegant-router/types" {
"user-center": "/user-center";
"workflow": "/workflow";
"workflow_category": "/workflow/category";
"workflow_leave": "/workflow/leave";
"workflow_process-definition": "/workflow/process-definition";
"workflow_process-instance": "/workflow/process-instance";
};
@ -151,6 +152,7 @@ declare module "@elegant-router/types" {
| "system_user"
| "tool_gen"
| "workflow_category"
| "workflow_leave"
| "workflow_process-definition"
| "workflow_process-instance"
>;

View File

@ -0,0 +1,221 @@
<script setup lang="tsx">
import { NDivider, NTag } from 'naive-ui';
import { leaveTypeRecord } from '@/constants/workflow';
import { fetchBatchDeleteLeave, fetchGetLeaveList } from '@/service/api/workflow';
import { useAppStore } from '@/store/modules/app';
import { useAuth } from '@/hooks/business/auth';
import { useDownload } from '@/hooks/business/download';
import { useTable, useTableOperate } from '@/hooks/common/table';
import { useDict } from '@/hooks/business/dict';
import { $t } from '@/locales';
import DictTag from '@/components/custom/dict-tag.vue';
import ButtonIcon from '@/components/custom/button-icon.vue';
import LeaveOperateDrawer from './modules/leave-operate-drawer.vue';
import LeaveSearch from './modules/leave-search.vue';
defineOptions({
name: 'LeaveList'
});
const appStore = useAppStore();
const { download } = useDownload();
const { hasAuth } = useAuth();
useDict('wf_business_status');
const {
columns,
columnChecks,
data,
getData,
getDataByPage,
loading,
mobilePagination,
searchParams,
resetSearchParams
} = useTable({
apiFn: fetchGetLeaveList,
apiParams: {
pageNum: 1,
pageSize: 10,
// if you want to use the searchParams in Form, you need to define the following properties, and the value is null
// the value can not be undefined, otherwise the property in Form will not be reactive
leaveDays: null,
params: {}
},
columns: () => [
{
type: 'selection',
align: 'center',
width: 48
},
{
key: 'leaveType',
title: '请假类型',
align: 'center',
minWidth: 100,
render: row => {
return (
<NTag size="small" type="info">
{leaveTypeRecord[row.leaveType]}
</NTag>
);
}
},
{
key: 'startDate',
title: '开始时间',
align: 'center',
minWidth: 100
},
{
key: 'endDate',
title: '结束时间',
align: 'center',
minWidth: 100
},
{
key: 'leaveDays',
title: '请假天数',
align: 'center',
minWidth: 100
},
{
key: 'remark',
title: '请假原因',
align: 'center',
minWidth: 100
},
{
key: 'status',
title: '状态',
align: 'center',
minWidth: 100,
render: row => {
return <DictTag size="small" value={row.status} dictCode="wf_business_status" />;
}
},
{
key: 'operate',
title: $t('common.operate'),
align: 'center',
width: 130,
render: row => {
const divider = () => {
if (!hasAuth('workflow:leave:edit') || !hasAuth('workflow:leave:remove')) {
return null;
}
return <NDivider vertical />;
};
const editBtn = () => {
if (!hasAuth('workflow:leave:edit')) {
return null;
}
return (
<ButtonIcon
text
type="primary"
icon="material-symbols:drive-file-rename-outline-outline"
tooltipContent={$t('common.edit')}
onClick={() => edit(row.id!)}
/>
);
};
const deleteBtn = () => {
if (!hasAuth('workflow:leave:remove')) {
return null;
}
return (
<ButtonIcon
text
type="error"
icon="material-symbols:delete-outline"
tooltipContent={$t('common.delete')}
popconfirmContent={$t('common.confirmDelete')}
onPositiveClick={() => handleDelete(row.id!)}
/>
);
};
return (
<div class="flex-center gap-8px">
{editBtn()}
{divider()}
{deleteBtn()}
</div>
);
}
}
]
});
const { drawerVisible, operateType, editingData, handleAdd, handleEdit, checkedRowKeys, onBatchDeleted, onDeleted } =
useTableOperate(data, getData);
async function handleBatchDelete() {
// request
const { error } = await fetchBatchDeleteLeave(checkedRowKeys.value);
if (error) return;
onBatchDeleted();
}
async function handleDelete(id: CommonType.IdType) {
// request
const { error } = await fetchBatchDeleteLeave([id]);
if (error) return;
onDeleted();
}
function edit(id: CommonType.IdType) {
handleEdit('id', id);
}
function handleExport() {
download('/workflow/leave/export', searchParams, `请假申请_${new Date().getTime()}.xlsx`);
}
</script>
<template>
<div class="min-h-500px flex-col-stretch gap-16px overflow-hidden lt-sm:overflow-auto">
<LeaveSearch v-model:model="searchParams" @reset="resetSearchParams" @search="getDataByPage" />
<NCard title="请假申请列表" :bordered="false" size="small" class="sm:flex-1-hidden card-wrapper">
<template #header-extra>
<TableHeaderOperation
v-model:columns="columnChecks"
:disabled-delete="checkedRowKeys.length === 0"
:loading="loading"
:show-add="hasAuth('workflow:leave:add')"
:show-delete="hasAuth('workflow:leave:remove')"
:show-export="hasAuth('workflow:leave:export')"
@add="handleAdd"
@delete="handleBatchDelete"
@export="handleExport"
@refresh="getData"
/>
</template>
<NDataTable
v-model:checked-row-keys="checkedRowKeys"
:columns="columns"
:data="data"
size="small"
:flex-height="!appStore.isMobile"
:scroll-x="778"
:loading="loading"
remote
:row-key="row => row.id"
:pagination="mobilePagination"
class="sm:h-full"
/>
<LeaveOperateDrawer
v-model:visible="drawerVisible"
:operate-type="operateType"
:row-data="editingData"
@submitted="getDataByPage"
/>
</NCard>
</div>
</template>
<style scoped></style>

View File

@ -0,0 +1,139 @@
<script setup lang="ts">
import { computed, reactive, watch } from 'vue';
import { leaveTypeOptions } from '@/constants/workflow';
import { fetchCreateLeave, fetchUpdateLeave } from '@/service/api/workflow';
import { useFormRules, useNaiveForm } from '@/hooks/common/form';
import { $t } from '@/locales';
defineOptions({
name: 'LeaveOperateDrawer'
});
interface Props {
/** the type of operation */
operateType: NaiveUI.TableOperateType;
/** the edit row data */
rowData?: Api.Workflow.Leave | null;
}
const props = defineProps<Props>();
interface Emits {
(e: 'submitted'): void;
}
const emit = defineEmits<Emits>();
const visible = defineModel<boolean>('visible', {
default: false
});
const { formRef, validate, restoreValidation } = useNaiveForm();
const { createRequiredRule } = useFormRules();
const title = computed(() => {
const titles: Record<NaiveUI.TableOperateType, string> = {
add: '新增请假申请',
edit: '编辑请假申请'
};
return titles[props.operateType];
});
type Model = Api.Workflow.LeaveOperateParams;
const model: Model = reactive(createDefaultModel());
function createDefaultModel(): Model {
return {
leaveType: null,
startDate: null,
endDate: null,
leaveDays: null,
remark: ''
};
}
type RuleKey = Extract<keyof Model, 'id' | 'leaveType' | 'startDate' | 'endDate' | 'leaveDays'>;
const rules: Record<RuleKey, App.Global.FormRule> = {
id: createRequiredRule('id不能为空'),
leaveType: createRequiredRule('请假类型不能为空'),
startDate: createRequiredRule('开始时间不能为空'),
endDate: createRequiredRule('结束时间不能为空'),
leaveDays: createRequiredRule('请假天数不能为空')
};
function handleUpdateModelWhenEdit() {
if (props.operateType === 'add') {
Object.assign(model, createDefaultModel());
return;
}
if (props.operateType === 'edit' && props.rowData) {
Object.assign(model, props.rowData);
}
}
function closeDrawer() {
visible.value = false;
}
async function handleSubmit() {
await validate();
// request
if (props.operateType === 'add') {
const { leaveType, startDate, endDate, leaveDays, remark } = model;
const { error } = await fetchCreateLeave({ leaveType, startDate, endDate, leaveDays, remark });
if (error) return;
}
if (props.operateType === 'edit') {
const { id, leaveType, startDate, endDate, leaveDays, remark } = model;
const { error } = await fetchUpdateLeave({ id, leaveType, startDate, endDate, leaveDays, remark });
if (error) return;
}
window.$message?.success($t('common.updateSuccess'));
closeDrawer();
emit('submitted');
}
watch(visible, () => {
if (visible.value) {
handleUpdateModelWhenEdit();
restoreValidation();
}
});
</script>
<template>
<NDrawer v-model:show="visible" :title="title" display-directive="show" :width="800" class="max-w-90%">
<NDrawerContent :title="title" :native-scrollbar="false" closable>
<NForm ref="formRef" :model="model" :rules="rules">
<NFormItem label="请假类型" path="leaveType">
<NSelect v-model:value="model.leaveType" placeholder="请输入请假类型" :options="leaveTypeOptions" />
</NFormItem>
<NFormItem label="开始时间" path="startDate">
<NDatePicker v-model:formatted-value="model.startDate" type="date" value-format="yyyy-MM-dd" clearable />
</NFormItem>
<NFormItem label="结束时间" path="endDate">
<NDatePicker v-model:formatted-value="model.endDate" type="date" value-format="yyyy-MM-dd" clearable />
</NFormItem>
<NFormItem label="请假天数" path="leaveDays">
<NInputNumber v-model:value="model.leaveDays" placeholder="请输入请假天数" />
</NFormItem>
<NFormItem label="请假原因" path="remark">
<NInput v-model:value="model.remark" placeholder="请输入请假原因" />
</NFormItem>
</NForm>
<template #footer>
<NSpace :size="16">
<NButton @click="closeDrawer">{{ $t('common.cancel') }}</NButton>
<NButton type="primary" @click="handleSubmit">{{ $t('common.confirm') }}</NButton>
</NSpace>
</template>
</NDrawerContent>
</NDrawer>
</template>
<style scoped></style>

View File

@ -0,0 +1,64 @@
<script setup lang="ts">
import { useNaiveForm } from '@/hooks/common/form';
import { $t } from '@/locales';
defineOptions({
name: 'LeaveSearch'
});
interface Emits {
(e: 'reset'): void;
(e: 'search'): void;
}
const emit = defineEmits<Emits>();
const { formRef, validate, restoreValidation } = useNaiveForm();
const model = defineModel<Api.Workflow.LeaveSearchParams>('model', { required: true });
async function reset() {
Object.assign(model.value.params!, {});
await restoreValidation();
emit('reset');
}
async function search() {
await validate();
emit('search');
}
</script>
<template>
<NCard :bordered="false" size="small" class="card-wrapper">
<NCollapse>
<NCollapseItem :title="$t('common.search')" name="user-search">
<NForm ref="formRef" :model="model" label-placement="left" :label-width="80">
<NGrid responsive="screen" item-responsive>
<NFormItemGi span="24 s:12 m:6" label="请假天数" path="leaveDays" class="pr-24px">
<NInputNumber v-model:value="model.leaveDays" placeholder="请输入请假天数" />
</NFormItemGi>
<NFormItemGi span="24" class="pr-24px">
<NSpace class="w-full" justify="end">
<NButton @click="reset">
<template #icon>
<icon-ic-round-refresh class="text-icon" />
</template>
{{ $t('common.reset') }}
</NButton>
<NButton type="primary" ghost @click="search">
<template #icon>
<icon-ic-round-search class="text-icon" />
</template>
{{ $t('common.search') }}
</NButton>
</NSpace>
</NFormItemGi>
</NGrid>
</NForm>
</NCollapseItem>
</NCollapse>
</NCard>
</template>
<style scoped></style>