feat(新增加班申请功能): 新增申请功能,可在工作台进行审核。
fix(dict_data): 在字典数据新增、编辑时可以操作颜色类型字段(color_type)。
This commit is contained in:
@@ -151,9 +151,14 @@ export function setupElegantRouter() {
|
||||
order: 5,
|
||||
keepAlive: true
|
||||
},
|
||||
'personal-center_overtime-application': {
|
||||
icon: 'mdi:clock-plus-outline',
|
||||
order: 6,
|
||||
keepAlive: true
|
||||
},
|
||||
'personal-center_pending-approval': {
|
||||
icon: 'mdi:check-decagram-outline',
|
||||
order: 6,
|
||||
order: 7,
|
||||
keepAlive: true
|
||||
},
|
||||
system: {
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"generatedAt": "2026-05-19T07:08:28.081Z",
|
||||
"generatedAt": "2026-06-01T01:55:51.875Z",
|
||||
"description": "Frontend visible page resource whitelist for backend route/menu configuration.",
|
||||
"rules": {
|
||||
"directoryComponent": "layout.base",
|
||||
"pageComponentPattern": "view.<routeName>",
|
||||
"singlePageComponentPattern": "layout.<layoutName>$view.<routeName>"
|
||||
},
|
||||
"total": 22,
|
||||
"total": 23,
|
||||
"items": [
|
||||
{
|
||||
"name": "product_list",
|
||||
@@ -470,6 +470,39 @@
|
||||
"pageType": "leaf",
|
||||
"source": "generated"
|
||||
},
|
||||
{
|
||||
"name": "personal-center_overtime-application",
|
||||
"path": "/personal-center/overtime-application",
|
||||
"component": "view.personal-center_overtime-application",
|
||||
"title": "加班申请",
|
||||
"routeTitle": "personal-center_overtime-application",
|
||||
"i18nKey": "route.personal-center_overtime-application",
|
||||
"icon": "mdi:clock-plus-outline",
|
||||
"localIcon": null,
|
||||
"order": 6,
|
||||
"hideInMenu": false,
|
||||
"keepAlive": true,
|
||||
"activeMenu": null,
|
||||
"multiTab": false,
|
||||
"fixedIndexInTab": null,
|
||||
"redirect": null,
|
||||
"props": null,
|
||||
"meta": {
|
||||
"title": "加班申请",
|
||||
"i18nKey": "route.personal-center_overtime-application",
|
||||
"icon": "mdi:clock-plus-outline",
|
||||
"localIcon": null,
|
||||
"order": 6,
|
||||
"keepAlive": true,
|
||||
"hideInMenu": false,
|
||||
"activeMenu": null,
|
||||
"multiTab": false,
|
||||
"fixedIndexInTab": null
|
||||
},
|
||||
"parentName": "personal-center",
|
||||
"pageType": "leaf",
|
||||
"source": "generated"
|
||||
},
|
||||
{
|
||||
"name": "system_user",
|
||||
"path": "/system/user",
|
||||
|
||||
@@ -111,3 +111,19 @@ export const RDMS_REQ_CAN_DELETE_STATUS_DICT_CODE = 'rdms_req_can_delete_status'
|
||||
* 来源口径:用户明确指定任务/个人事项工作日志难度下拉来自运行时字典 rdms_task_item_worklog_difficulty
|
||||
*/
|
||||
export const RDMS_WORKLOG_DIFFICULTY_DICT_CODE = 'rdms_task_item_worklog_difficulty';
|
||||
|
||||
/**
|
||||
* 加班申请状态字典编码
|
||||
*
|
||||
* 对应业务字段:加班申请中的 statusCode
|
||||
* 来源口径:`overtime-application-design.md` 明确状态字典为 rdms_overtime_application_status
|
||||
*/
|
||||
export const RDMS_OVERTIME_APPLICATION_STATUS_DICT_CODE = 'rdms_overtime_application_status';
|
||||
|
||||
/**
|
||||
* 加班时长快捷选项字典编码
|
||||
*
|
||||
* 对应业务字段:加班申请中的 overtimeDuration
|
||||
* 来源口径:`overtime-application-design.md` 明确时长下拉字典为 rdms_overtime_duration
|
||||
*/
|
||||
export const RDMS_OVERTIME_DURATION_DICT_CODE = 'rdms_overtime_duration';
|
||||
|
||||
@@ -16,7 +16,8 @@ export type StatusDomain =
|
||||
| 'product'
|
||||
| 'requirement'
|
||||
| 'workOrder'
|
||||
| 'personalItem';
|
||||
| 'personalItem'
|
||||
| 'overtimeApplication';
|
||||
|
||||
const statusTagTypeRegistry: Record<StatusDomain, Record<string, StatusTagType>> = {
|
||||
// 项目-执行
|
||||
@@ -61,6 +62,13 @@ const statusTagTypeRegistry: Record<StatusDomain, Record<string, StatusTagType>>
|
||||
active: 'primary',
|
||||
completed: 'success',
|
||||
cancelled: 'danger'
|
||||
},
|
||||
// 加班申请
|
||||
overtimeApplication: {
|
||||
pending: 'warning',
|
||||
approved: 'success',
|
||||
rejected: 'danger',
|
||||
cancelled: 'info'
|
||||
}
|
||||
};
|
||||
|
||||
@@ -69,9 +77,13 @@ export function getStatusTagType(domain: StatusDomain, statusCode: string | null
|
||||
return 'info';
|
||||
}
|
||||
|
||||
return statusTagTypeRegistry[domain][statusCode] || 'info';
|
||||
return statusTagTypeRegistry[domain]?.[statusCode] || 'info';
|
||||
}
|
||||
|
||||
export function getPersonalItemStatusTagType(statusCode: string | null | undefined) {
|
||||
return getStatusTagType('personalItem', statusCode);
|
||||
}
|
||||
|
||||
export function getOvertimeApplicationStatusTagType(statusCode: string | null | undefined) {
|
||||
return getStatusTagType('overtimeApplication', statusCode);
|
||||
}
|
||||
|
||||
@@ -173,6 +173,7 @@ const local: App.I18n.Schema = {
|
||||
'personal-center_my-monthly': 'My Monthly Report',
|
||||
'personal-center_my-performance': 'My Performance',
|
||||
'personal-center_my-application': 'My Application',
|
||||
'personal-center_overtime-application': 'Overtime Application',
|
||||
'personal-center_pending-approval': 'Pending Approval',
|
||||
infra: 'Infra',
|
||||
'infra_state-machine': 'State Machine',
|
||||
@@ -708,6 +709,7 @@ const local: App.I18n.Schema = {
|
||||
dictStatus: 'Dictionary Status',
|
||||
dictLabel: 'Dictionary Label',
|
||||
dictValue: 'Dictionary Value',
|
||||
colorType: 'Color Type',
|
||||
sort: 'Sort',
|
||||
remark: 'Remark',
|
||||
form: {
|
||||
@@ -716,6 +718,7 @@ const local: App.I18n.Schema = {
|
||||
dictStatus: 'Please select dictionary status',
|
||||
dictLabel: 'Please enter dictionary label',
|
||||
dictValue: 'Please enter dictionary value',
|
||||
colorType: 'Please enter color type',
|
||||
sort: 'Please enter sort',
|
||||
remark: 'Please enter remark'
|
||||
},
|
||||
|
||||
@@ -173,6 +173,7 @@ const local: App.I18n.Schema = {
|
||||
'personal-center_my-monthly': '我的月报',
|
||||
'personal-center_my-performance': '我的绩效',
|
||||
'personal-center_my-application': '我的申请',
|
||||
'personal-center_overtime-application': '加班申请',
|
||||
'personal-center_pending-approval': '待我审批',
|
||||
infra: '基础设施',
|
||||
'infra_state-machine': '状态机管理',
|
||||
@@ -696,6 +697,7 @@ const local: App.I18n.Schema = {
|
||||
dictStatus: '字典状态',
|
||||
dictLabel: '字典标签',
|
||||
dictValue: '字典键值',
|
||||
colorType: '颜色类型',
|
||||
sort: '排序',
|
||||
remark: '备注',
|
||||
form: {
|
||||
@@ -704,6 +706,7 @@ const local: App.I18n.Schema = {
|
||||
dictStatus: '请选择字典状态',
|
||||
dictLabel: '请输入字典标签',
|
||||
dictValue: '请输入字典键值',
|
||||
colorType: '请输入颜色类型',
|
||||
sort: '请输入排序',
|
||||
remark: '请输入备注'
|
||||
},
|
||||
|
||||
@@ -39,6 +39,7 @@ export const views: Record<LastLevelRouteKey, RouteComponent | (() => Promise<Ro
|
||||
"personal-center_my-performance": () => import("@/views/personal-center/my-performance/index.vue"),
|
||||
"personal-center_my-profile": () => import("@/views/personal-center/my-profile/index.vue"),
|
||||
"personal-center_my-weekly": () => import("@/views/personal-center/my-weekly/index.vue"),
|
||||
"personal-center_overtime-application": () => import("@/views/personal-center/overtime-application/index.vue"),
|
||||
"personal-center_pending-approval": () => import("@/views/personal-center/pending-approval/index.vue"),
|
||||
plugin_barcode: () => import("@/views/plugin/barcode/index.vue"),
|
||||
plugin_charts_antv: () => import("@/views/plugin/charts/antv/index.vue"),
|
||||
|
||||
@@ -351,6 +351,18 @@ export const generatedRoutes: GeneratedRoute[] = [
|
||||
keepAlive: true
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'personal-center_overtime-application',
|
||||
path: '/personal-center/overtime-application',
|
||||
component: 'view.personal-center_overtime-application',
|
||||
meta: {
|
||||
title: 'personal-center_overtime-application',
|
||||
i18nKey: 'route.personal-center_overtime-application',
|
||||
icon: 'mdi:clock-plus-outline',
|
||||
order: 6,
|
||||
keepAlive: true
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'personal-center_pending-approval',
|
||||
path: '/personal-center/pending-approval',
|
||||
@@ -359,7 +371,7 @@ export const generatedRoutes: GeneratedRoute[] = [
|
||||
title: 'personal-center_pending-approval',
|
||||
i18nKey: 'route.personal-center_pending-approval',
|
||||
icon: 'mdi:check-decagram-outline',
|
||||
order: 5,
|
||||
order: 7,
|
||||
keepAlive: true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -196,6 +196,7 @@ const routeMap: RouteMap = {
|
||||
"personal-center_my-performance": "/personal-center/my-performance",
|
||||
"personal-center_my-profile": "/personal-center/my-profile",
|
||||
"personal-center_my-weekly": "/personal-center/my-weekly",
|
||||
"personal-center_overtime-application": "/personal-center/overtime-application",
|
||||
"personal-center_pending-approval": "/personal-center/pending-approval",
|
||||
"plugin": "/plugin",
|
||||
"plugin_barcode": "/plugin/barcode",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { SYSTEM_SERVICE_PREFIX } from '@/constants/service';
|
||||
import { request } from '../request';
|
||||
import { type ServiceRequestResult, mapServiceResult } from './shared';
|
||||
|
||||
const DICT_TYPE_PREFIX = `${SYSTEM_SERVICE_PREFIX}/dict-type`;
|
||||
const DICT_DATA_PREFIX = `${SYSTEM_SERVICE_PREFIX}/dict-data`;
|
||||
@@ -15,6 +16,52 @@ function createBatchDeleteQuery(ids: number[]) {
|
||||
return query.toString();
|
||||
}
|
||||
|
||||
type DictDataResponse = Omit<Api.Dict.DictData, 'colorType'> & {
|
||||
colorType?: string | null;
|
||||
color_type?: string | null;
|
||||
};
|
||||
|
||||
type DictDataPageResponse = Omit<Api.Dict.PageResult<Api.Dict.DictData>, 'list'> & {
|
||||
list: DictDataResponse[];
|
||||
};
|
||||
|
||||
type FrontendDictDataResponse = Omit<Api.Dict.FrontendDictData, 'colorType'> & {
|
||||
colorType?: string | null;
|
||||
color_type?: string | null;
|
||||
};
|
||||
|
||||
type FrontendDictCacheResponse = Record<string, FrontendDictDataResponse[]>;
|
||||
|
||||
function normalizeColorType(value?: string | null) {
|
||||
return value?.trim() || null;
|
||||
}
|
||||
|
||||
function normalizeDictData(data: DictDataResponse): Api.Dict.DictData {
|
||||
const { color_type: colorTypeFromSnakeCase, ...rest } = data;
|
||||
|
||||
return {
|
||||
...rest,
|
||||
colorType: normalizeColorType(data.colorType ?? colorTypeFromSnakeCase)
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeFrontendDictData(data: FrontendDictDataResponse): Api.Dict.FrontendDictData {
|
||||
const { color_type: colorTypeFromSnakeCase, ...rest } = data;
|
||||
|
||||
return {
|
||||
...rest,
|
||||
colorType: normalizeColorType(data.colorType ?? colorTypeFromSnakeCase)
|
||||
};
|
||||
}
|
||||
|
||||
function toSaveDictDataRequest(data: Api.Dict.SaveDictDataParams) {
|
||||
return {
|
||||
...data,
|
||||
colorType: normalizeColorType(data.colorType),
|
||||
remark: data.remark?.trim() || null
|
||||
};
|
||||
}
|
||||
|
||||
/** 获取字典类型分页 */
|
||||
export function fetchGetDictTypePage(params?: Api.Dict.DictTypeSearchParams) {
|
||||
return request<Api.Dict.PageResult<Api.Dict.DictType>>({
|
||||
@@ -60,20 +107,40 @@ export function fetchBatchDeleteDictType(ids: number[]) {
|
||||
}
|
||||
|
||||
/** 获取字典数据分页 */
|
||||
export function fetchGetDictDataPage(params: Api.Dict.DictDataSearchParams) {
|
||||
return request<Api.Dict.PageResult<Api.Dict.DictData>>({
|
||||
export async function fetchGetDictDataPage(params: Api.Dict.DictDataSearchParams) {
|
||||
const result = await request<DictDataPageResponse>({
|
||||
url: `${DICT_DATA_PREFIX}/page`,
|
||||
method: 'get',
|
||||
params
|
||||
});
|
||||
|
||||
if (result.error || !result.data) {
|
||||
return result as unknown as Awaited<ReturnType<typeof request<Api.Dict.PageResult<Api.Dict.DictData>>>>;
|
||||
}
|
||||
|
||||
return {
|
||||
...result,
|
||||
data: {
|
||||
...result.data,
|
||||
list: result.data.list.map(normalizeDictData)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/** 获取前端运行时字典缓存 */
|
||||
export function fetchGetFrontendDictCache() {
|
||||
return request<Api.Dict.FrontendDictCache>({
|
||||
export async function fetchGetFrontendDictCache() {
|
||||
const result = await request<FrontendDictCacheResponse>({
|
||||
url: `${DICT_DATA_PREFIX}/frontend-cache`,
|
||||
method: 'get'
|
||||
});
|
||||
|
||||
return mapServiceResult(
|
||||
result as ServiceRequestResult<FrontendDictCacheResponse>,
|
||||
data =>
|
||||
Object.fromEntries(
|
||||
Object.entries(data).map(([dictType, list]) => [dictType, list.map(normalizeFrontendDictData)])
|
||||
) as Api.Dict.FrontendDictCache
|
||||
);
|
||||
}
|
||||
|
||||
/** 创建字典数据 */
|
||||
@@ -81,7 +148,7 @@ export function fetchCreateDictData(data: Api.Dict.SaveDictDataParams) {
|
||||
return request<number>({
|
||||
url: `${DICT_DATA_PREFIX}/create`,
|
||||
method: 'post',
|
||||
data
|
||||
data: toSaveDictDataRequest(data)
|
||||
});
|
||||
}
|
||||
|
||||
@@ -90,7 +157,7 @@ export function fetchUpdateDictData(data: { id: number } & Api.Dict.SaveDictData
|
||||
return request<boolean>({
|
||||
url: `${DICT_DATA_PREFIX}/update`,
|
||||
method: 'put',
|
||||
data
|
||||
data: toSaveDictDataRequest(data)
|
||||
});
|
||||
}
|
||||
|
||||
@@ -112,9 +179,14 @@ export function fetchBatchDeleteDictData(ids: number[]) {
|
||||
}
|
||||
|
||||
/** 通过岗位编码获取该字典的所有字典数据 */
|
||||
export function fetchGetDictDataByCode(code: string) {
|
||||
return request<Api.Dict.PageResult<Api.Dict.DictData>>({
|
||||
export async function fetchGetDictDataByCode(code: string) {
|
||||
const result = await request<DictDataPageResponse>({
|
||||
url: `${DICT_DATA_PREFIX}/code?code=${code}`,
|
||||
method: 'get'
|
||||
});
|
||||
|
||||
return mapServiceResult(result as ServiceRequestResult<DictDataPageResponse>, data => ({
|
||||
...data,
|
||||
list: data.list.map(normalizeDictData)
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ export * from './dict';
|
||||
export * from './file';
|
||||
export * from './infra';
|
||||
export * from './object-context';
|
||||
export * from './overtime-application';
|
||||
export * from './personal-item';
|
||||
export * from './product';
|
||||
export * from './project';
|
||||
|
||||
280
src/service/api/overtime-application.ts
Normal file
280
src/service/api/overtime-application.ts
Normal file
@@ -0,0 +1,280 @@
|
||||
import { WEB_SERVICE_PREFIX } from '@/constants/service';
|
||||
import { request } from '../request';
|
||||
import { type ProjectLocalDateValue, normalizeProjectLocalDate } from './project-shared';
|
||||
import {
|
||||
type ServiceRequestResult,
|
||||
mapServiceResult,
|
||||
normalizeNullableStringId,
|
||||
normalizeStringId,
|
||||
safeJsonRequestConfig
|
||||
} from './shared';
|
||||
|
||||
const OVERTIME_APPLICATION_PREFIX = `${WEB_SERVICE_PREFIX}/project/overtime-applications`;
|
||||
|
||||
type StringIdResponse = string | number;
|
||||
|
||||
type OvertimeApplicationResponse = Omit<
|
||||
Api.OvertimeApplication.OvertimeApplication,
|
||||
'id' | 'applicantId' | 'approverId' | 'overtimeDate' | 'allowEdit' | 'terminal'
|
||||
> & {
|
||||
id: StringIdResponse;
|
||||
applicantId: StringIdResponse;
|
||||
approverId: StringIdResponse;
|
||||
overtimeDate: ProjectLocalDateValue;
|
||||
allowEdit?: boolean | number | string | null;
|
||||
terminal?: boolean | number | string | null;
|
||||
};
|
||||
|
||||
type OvertimeApplicationPageResponse = Omit<Api.OvertimeApplication.OvertimeApplicationPageResult, 'total' | 'list'> & {
|
||||
total: number | string;
|
||||
list: OvertimeApplicationResponse[];
|
||||
};
|
||||
|
||||
type OvertimeApplicationStatusLogResponse = Omit<
|
||||
Api.OvertimeApplication.OvertimeApplicationStatusLog,
|
||||
'id' | 'applicationId' | 'operatorUserId' | 'overtimeDateSnapshot'
|
||||
> & {
|
||||
id: StringIdResponse;
|
||||
applicationId: StringIdResponse;
|
||||
operatorUserId: StringIdResponse;
|
||||
overtimeDateSnapshot: ProjectLocalDateValue;
|
||||
};
|
||||
|
||||
function normalizeBooleanFlag(value: boolean | number | string | null | undefined) {
|
||||
if (typeof value === 'boolean') {
|
||||
return value;
|
||||
}
|
||||
|
||||
if (typeof value === 'number') {
|
||||
return value === 1;
|
||||
}
|
||||
|
||||
if (typeof value === 'string') {
|
||||
const normalized = value.trim().toLowerCase();
|
||||
|
||||
return !['', '0', 'false', 'n', 'no'].includes(normalized);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function normalizeTotal(total: number | string) {
|
||||
const value = Number(total);
|
||||
|
||||
return Number.isFinite(value) ? Math.max(0, value) : 0;
|
||||
}
|
||||
|
||||
function normalizeOvertimeApplication(
|
||||
response: OvertimeApplicationResponse
|
||||
): Api.OvertimeApplication.OvertimeApplication {
|
||||
return {
|
||||
...response,
|
||||
id: normalizeStringId(response.id),
|
||||
applicantId: normalizeStringId(response.applicantId),
|
||||
approverId: normalizeStringId(response.approverId),
|
||||
overtimeDate: normalizeProjectLocalDate(response.overtimeDate) ?? '',
|
||||
statusName: response.statusName || response.statusCode,
|
||||
allowEdit: normalizeBooleanFlag(response.allowEdit),
|
||||
terminal: normalizeBooleanFlag(response.terminal),
|
||||
approvalComment: response.approvalComment ?? null,
|
||||
approvalTime: response.approvalTime ?? null
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeStatusLog(
|
||||
response: OvertimeApplicationStatusLogResponse
|
||||
): Api.OvertimeApplication.OvertimeApplicationStatusLog {
|
||||
return {
|
||||
...response,
|
||||
id: normalizeStringId(response.id),
|
||||
applicationId: normalizeStringId(response.applicationId),
|
||||
operatorUserId: normalizeStringId(response.operatorUserId),
|
||||
overtimeDateSnapshot: normalizeProjectLocalDate(response.overtimeDateSnapshot) ?? '',
|
||||
fromStatus: normalizeNullableStringId(response.fromStatus),
|
||||
reason: response.reason ?? null,
|
||||
remark: response.remark ?? null
|
||||
};
|
||||
}
|
||||
|
||||
function createPageQuery(params: Api.OvertimeApplication.OvertimeApplicationSearchParams = {}) {
|
||||
const query = new URLSearchParams();
|
||||
|
||||
query.append('pageNo', String(params.pageNo ?? 1));
|
||||
query.append('pageSize', String(params.pageSize ?? 10));
|
||||
|
||||
if (params.keyword) {
|
||||
query.append('keyword', params.keyword);
|
||||
}
|
||||
|
||||
if (params.applicantName) {
|
||||
query.append('applicantName', params.applicantName);
|
||||
}
|
||||
|
||||
if (params.approverId) {
|
||||
query.append('approverId', params.approverId);
|
||||
}
|
||||
|
||||
if (params.approverName) {
|
||||
query.append('approverName', params.approverName);
|
||||
}
|
||||
|
||||
if (params.statusCode) {
|
||||
query.append('statusCode', params.statusCode);
|
||||
}
|
||||
|
||||
params.overtimeDate?.forEach(item => {
|
||||
if (item) {
|
||||
query.append('overtimeDate', item);
|
||||
}
|
||||
});
|
||||
|
||||
params.createTime?.forEach(item => {
|
||||
if (item) {
|
||||
query.append('createTime', item);
|
||||
}
|
||||
});
|
||||
|
||||
return query.toString();
|
||||
}
|
||||
|
||||
function toSaveRequest(data: Api.OvertimeApplication.SaveOvertimeApplicationParams) {
|
||||
return {
|
||||
overtimeDate: data.overtimeDate,
|
||||
overtimeDuration: data.overtimeDuration,
|
||||
overtimeReason: data.overtimeReason.trim(),
|
||||
overtimeContent: data.overtimeContent.trim(),
|
||||
approverId: data.approverId
|
||||
};
|
||||
}
|
||||
|
||||
function toStatusActionRequest(data: Api.OvertimeApplication.StatusActionParams = {}) {
|
||||
return {
|
||||
reason: data.reason?.trim() || undefined
|
||||
};
|
||||
}
|
||||
|
||||
export async function fetchGetOvertimeApplicationPage(
|
||||
params: Api.OvertimeApplication.OvertimeApplicationSearchParams = {}
|
||||
) {
|
||||
const query = createPageQuery(params);
|
||||
|
||||
const result = await request<OvertimeApplicationPageResponse>({
|
||||
...safeJsonRequestConfig,
|
||||
url: query ? `${OVERTIME_APPLICATION_PREFIX}/page?${query}` : `${OVERTIME_APPLICATION_PREFIX}/page`,
|
||||
method: 'get'
|
||||
});
|
||||
|
||||
return mapServiceResult(result as ServiceRequestResult<OvertimeApplicationPageResponse>, data => ({
|
||||
total: normalizeTotal(data.total),
|
||||
list: data.list.map(normalizeOvertimeApplication)
|
||||
}));
|
||||
}
|
||||
|
||||
export async function fetchGetOvertimeApplicationApprovalPage(
|
||||
params: Api.OvertimeApplication.OvertimeApplicationSearchParams = {}
|
||||
) {
|
||||
const query = createPageQuery(params);
|
||||
|
||||
const result = await request<OvertimeApplicationPageResponse>({
|
||||
...safeJsonRequestConfig,
|
||||
url: query
|
||||
? `${OVERTIME_APPLICATION_PREFIX}/approval-page?${query}`
|
||||
: `${OVERTIME_APPLICATION_PREFIX}/approval-page`,
|
||||
method: 'get'
|
||||
});
|
||||
|
||||
return mapServiceResult(result as ServiceRequestResult<OvertimeApplicationPageResponse>, data => ({
|
||||
total: normalizeTotal(data.total),
|
||||
list: data.list.map(normalizeOvertimeApplication)
|
||||
}));
|
||||
}
|
||||
|
||||
export async function fetchGetOvertimeApplicationDetail(id: string) {
|
||||
const result = await request<OvertimeApplicationResponse>({
|
||||
...safeJsonRequestConfig,
|
||||
url: `${OVERTIME_APPLICATION_PREFIX}/${id}`,
|
||||
method: 'get'
|
||||
});
|
||||
|
||||
return mapServiceResult(result as ServiceRequestResult<OvertimeApplicationResponse>, normalizeOvertimeApplication);
|
||||
}
|
||||
|
||||
export async function fetchCreateOvertimeApplication(data: Api.OvertimeApplication.SaveOvertimeApplicationParams) {
|
||||
const result = await request<string | number>({
|
||||
...safeJsonRequestConfig,
|
||||
url: OVERTIME_APPLICATION_PREFIX,
|
||||
method: 'post',
|
||||
data: toSaveRequest(data)
|
||||
});
|
||||
|
||||
return mapServiceResult(result as ServiceRequestResult<string | number>, normalizeStringId);
|
||||
}
|
||||
|
||||
export function fetchUpdateRejectedOvertimeApplication(
|
||||
id: string,
|
||||
data: Api.OvertimeApplication.SaveOvertimeApplicationParams
|
||||
) {
|
||||
return request<boolean>({
|
||||
...safeJsonRequestConfig,
|
||||
url: `${OVERTIME_APPLICATION_PREFIX}/${id}`,
|
||||
method: 'put',
|
||||
data: toSaveRequest(data)
|
||||
});
|
||||
}
|
||||
|
||||
export function fetchApproveOvertimeApplication(id: string, data: Api.OvertimeApplication.StatusActionParams = {}) {
|
||||
return request<boolean>({
|
||||
...safeJsonRequestConfig,
|
||||
url: `${OVERTIME_APPLICATION_PREFIX}/${id}/approve`,
|
||||
method: 'post',
|
||||
data: toStatusActionRequest(data)
|
||||
});
|
||||
}
|
||||
|
||||
export function fetchRejectOvertimeApplication(id: string, data: Api.OvertimeApplication.StatusActionParams) {
|
||||
return request<boolean>({
|
||||
...safeJsonRequestConfig,
|
||||
url: `${OVERTIME_APPLICATION_PREFIX}/${id}/reject`,
|
||||
method: 'post',
|
||||
data: toStatusActionRequest(data)
|
||||
});
|
||||
}
|
||||
|
||||
export function fetchCancelOvertimeApplication(id: string, data: Api.OvertimeApplication.StatusActionParams) {
|
||||
return request<boolean>({
|
||||
...safeJsonRequestConfig,
|
||||
url: `${OVERTIME_APPLICATION_PREFIX}/${id}/cancel`,
|
||||
method: 'post',
|
||||
data: toStatusActionRequest(data)
|
||||
});
|
||||
}
|
||||
|
||||
export function fetchDeleteOvertimeApplication(id: string) {
|
||||
return request<boolean>({
|
||||
...safeJsonRequestConfig,
|
||||
url: `${OVERTIME_APPLICATION_PREFIX}/${id}`,
|
||||
method: 'delete'
|
||||
});
|
||||
}
|
||||
|
||||
export async function fetchGetOvertimeApplicationStatusLogs(id: string) {
|
||||
const result = await request<OvertimeApplicationStatusLogResponse[]>({
|
||||
...safeJsonRequestConfig,
|
||||
url: `${OVERTIME_APPLICATION_PREFIX}/${id}/status-logs`,
|
||||
method: 'get'
|
||||
});
|
||||
|
||||
return mapServiceResult(result as ServiceRequestResult<OvertimeApplicationStatusLogResponse[]>, data =>
|
||||
data.map(normalizeStatusLog)
|
||||
);
|
||||
}
|
||||
|
||||
export function fetchExportOvertimeApplications(params: Api.OvertimeApplication.OvertimeApplicationSearchParams = {}) {
|
||||
const query = createPageQuery(params);
|
||||
|
||||
return request<Blob, 'blob'>({
|
||||
url: query ? `${OVERTIME_APPLICATION_PREFIX}/export?${query}` : `${OVERTIME_APPLICATION_PREFIX}/export`,
|
||||
method: 'get',
|
||||
responseType: 'blob'
|
||||
});
|
||||
}
|
||||
@@ -455,6 +455,19 @@ export async function fetchGetUserSimpleList() {
|
||||
);
|
||||
}
|
||||
|
||||
/** 获取当前登录人的直属上级 */
|
||||
export async function fetchGetLoginUserDirectManager() {
|
||||
return request<UserSimpleResponse | null>({
|
||||
...safeJsonRequestConfig,
|
||||
url: `${USER_PREFIX}/profile/direct-manager`,
|
||||
method: 'get'
|
||||
}).then(result =>
|
||||
mapServiceResult(result as ServiceRequestResult<UserSimpleResponse | null>, data =>
|
||||
data ? normalizeUserSimple(data) : null
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/** 获取用户分页 */
|
||||
export function fetchGetUserPage(params?: Api.SystemManage.UserSearchParams) {
|
||||
return request<Api.SystemManage.UserList>({
|
||||
|
||||
2
src/typings/api/dict.d.ts
vendored
2
src/typings/api/dict.d.ts
vendored
@@ -88,7 +88,7 @@ declare namespace Api {
|
||||
type DictDataSearchParams = CommonType.RecordNullable<Pick<DictData, 'label' | 'dictType' | 'status'>> & PageParams;
|
||||
|
||||
/** dict data save params */
|
||||
type SaveDictDataParams = Pick<DictData, 'label' | 'value' | 'dictType' | 'sort' | 'status'> & {
|
||||
type SaveDictDataParams = Pick<DictData, 'label' | 'value' | 'dictType' | 'sort' | 'status' | 'colorType'> & {
|
||||
remark?: string | null;
|
||||
};
|
||||
}
|
||||
|
||||
78
src/typings/api/overtime-application.d.ts
vendored
Normal file
78
src/typings/api/overtime-application.d.ts
vendored
Normal file
@@ -0,0 +1,78 @@
|
||||
declare namespace Api {
|
||||
namespace OvertimeApplication {
|
||||
interface PageParams {
|
||||
pageNo: number;
|
||||
pageSize: number;
|
||||
}
|
||||
|
||||
type OvertimeApplicationStatusCode = 'pending' | 'approved' | 'rejected' | 'cancelled';
|
||||
|
||||
type OvertimeApplicationActionType = 'submit' | 'resubmit' | 'approve' | 'reject' | 'cancel';
|
||||
|
||||
interface OvertimeApplication {
|
||||
id: string;
|
||||
applicantId: string;
|
||||
applicantName: string;
|
||||
overtimeDate: string;
|
||||
overtimeDuration: string;
|
||||
overtimeReason: string;
|
||||
overtimeContent: string;
|
||||
approverId: string;
|
||||
approverName: string;
|
||||
statusCode: OvertimeApplicationStatusCode;
|
||||
statusName: string;
|
||||
allowEdit: boolean;
|
||||
terminal: boolean;
|
||||
approvalComment?: string | null;
|
||||
submitTime: string;
|
||||
approvalTime?: string | null;
|
||||
createTime: string;
|
||||
updateTime: string;
|
||||
}
|
||||
|
||||
type OvertimeApplicationSearchParams = CommonType.RecordNullable<
|
||||
Pick<PageParams, 'pageNo' | 'pageSize'> & {
|
||||
keyword: string;
|
||||
applicantName: string;
|
||||
approverId: string;
|
||||
approverName: string;
|
||||
statusCode: OvertimeApplicationStatusCode;
|
||||
overtimeDate: string[];
|
||||
createTime: string[];
|
||||
}
|
||||
>;
|
||||
|
||||
interface OvertimeApplicationPageResult {
|
||||
total: number;
|
||||
list: OvertimeApplication[];
|
||||
}
|
||||
|
||||
interface SaveOvertimeApplicationParams {
|
||||
overtimeDate: string;
|
||||
overtimeDuration: string;
|
||||
overtimeReason: string;
|
||||
overtimeContent: string;
|
||||
approverId: string;
|
||||
}
|
||||
|
||||
interface StatusActionParams {
|
||||
reason?: string | null;
|
||||
}
|
||||
|
||||
interface OvertimeApplicationStatusLog {
|
||||
id: string;
|
||||
applicationId: string;
|
||||
actionType: OvertimeApplicationActionType;
|
||||
fromStatus?: string | null;
|
||||
toStatus: string;
|
||||
reason?: string | null;
|
||||
operatorUserId: string;
|
||||
operatorName: string;
|
||||
applicantNameSnapshot: string;
|
||||
overtimeDateSnapshot: string;
|
||||
overtimeDurationSnapshot: string;
|
||||
remark?: string | null;
|
||||
createTime: string;
|
||||
}
|
||||
}
|
||||
}
|
||||
2
src/typings/app.d.ts
vendored
2
src/typings/app.d.ts
vendored
@@ -866,6 +866,7 @@ declare namespace App {
|
||||
dictStatus: string;
|
||||
dictLabel: string;
|
||||
dictValue: string;
|
||||
colorType: string;
|
||||
sort: string;
|
||||
remark: string;
|
||||
form: {
|
||||
@@ -874,6 +875,7 @@ declare namespace App {
|
||||
dictStatus: string;
|
||||
dictLabel: string;
|
||||
dictValue: string;
|
||||
colorType: string;
|
||||
sort: string;
|
||||
remark: string;
|
||||
};
|
||||
|
||||
1
src/typings/components.d.ts
vendored
1
src/typings/components.d.ts
vendored
@@ -150,6 +150,7 @@ declare module 'vue' {
|
||||
IconMdiCrown: typeof import('~icons/mdi/crown')['default']
|
||||
IconMdiDeleteOutline: typeof import('~icons/mdi/delete-outline')['default']
|
||||
IconMdiDotsHorizontal: typeof import('~icons/mdi/dots-horizontal')['default']
|
||||
IconMdiDownload: typeof import('~icons/mdi/download')['default']
|
||||
IconMdiDrag: typeof import('~icons/mdi/drag')['default']
|
||||
IconMdiFilterVariant: typeof import('~icons/mdi/filter-variant')['default']
|
||||
IconMdiFolderOpen: typeof import('~icons/mdi/folder-open')['default']
|
||||
|
||||
2
src/typings/elegant-router.d.ts
vendored
2
src/typings/elegant-router.d.ts
vendored
@@ -50,6 +50,7 @@ declare module "@elegant-router/types" {
|
||||
"personal-center_my-performance": "/personal-center/my-performance";
|
||||
"personal-center_my-profile": "/personal-center/my-profile";
|
||||
"personal-center_my-weekly": "/personal-center/my-weekly";
|
||||
"personal-center_overtime-application": "/personal-center/overtime-application";
|
||||
"personal-center_pending-approval": "/personal-center/pending-approval";
|
||||
"plugin": "/plugin";
|
||||
"plugin_barcode": "/plugin/barcode";
|
||||
@@ -187,6 +188,7 @@ declare module "@elegant-router/types" {
|
||||
| "personal-center_my-performance"
|
||||
| "personal-center_my-profile"
|
||||
| "personal-center_my-weekly"
|
||||
| "personal-center_overtime-application"
|
||||
| "personal-center_pending-approval"
|
||||
| "plugin_barcode"
|
||||
| "plugin_charts_antv"
|
||||
|
||||
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