feat(新增加班申请功能): 新增申请功能,可在工作台进行审核。
fix(dict_data): 在字典数据新增、编辑时可以操作颜色类型字段(color_type)。
This commit is contained in:
@@ -151,9 +151,14 @@ export function setupElegantRouter() {
|
|||||||
order: 5,
|
order: 5,
|
||||||
keepAlive: true
|
keepAlive: true
|
||||||
},
|
},
|
||||||
|
'personal-center_overtime-application': {
|
||||||
|
icon: 'mdi:clock-plus-outline',
|
||||||
|
order: 6,
|
||||||
|
keepAlive: true
|
||||||
|
},
|
||||||
'personal-center_pending-approval': {
|
'personal-center_pending-approval': {
|
||||||
icon: 'mdi:check-decagram-outline',
|
icon: 'mdi:check-decagram-outline',
|
||||||
order: 6,
|
order: 7,
|
||||||
keepAlive: true
|
keepAlive: true
|
||||||
},
|
},
|
||||||
system: {
|
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.",
|
"description": "Frontend visible page resource whitelist for backend route/menu configuration.",
|
||||||
"rules": {
|
"rules": {
|
||||||
"directoryComponent": "layout.base",
|
"directoryComponent": "layout.base",
|
||||||
"pageComponentPattern": "view.<routeName>",
|
"pageComponentPattern": "view.<routeName>",
|
||||||
"singlePageComponentPattern": "layout.<layoutName>$view.<routeName>"
|
"singlePageComponentPattern": "layout.<layoutName>$view.<routeName>"
|
||||||
},
|
},
|
||||||
"total": 22,
|
"total": 23,
|
||||||
"items": [
|
"items": [
|
||||||
{
|
{
|
||||||
"name": "product_list",
|
"name": "product_list",
|
||||||
@@ -470,6 +470,39 @@
|
|||||||
"pageType": "leaf",
|
"pageType": "leaf",
|
||||||
"source": "generated"
|
"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",
|
"name": "system_user",
|
||||||
"path": "/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
|
* 来源口径:用户明确指定任务/个人事项工作日志难度下拉来自运行时字典 rdms_task_item_worklog_difficulty
|
||||||
*/
|
*/
|
||||||
export const RDMS_WORKLOG_DIFFICULTY_DICT_CODE = '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'
|
| 'product'
|
||||||
| 'requirement'
|
| 'requirement'
|
||||||
| 'workOrder'
|
| 'workOrder'
|
||||||
| 'personalItem';
|
| 'personalItem'
|
||||||
|
| 'overtimeApplication';
|
||||||
|
|
||||||
const statusTagTypeRegistry: Record<StatusDomain, Record<string, StatusTagType>> = {
|
const statusTagTypeRegistry: Record<StatusDomain, Record<string, StatusTagType>> = {
|
||||||
// 项目-执行
|
// 项目-执行
|
||||||
@@ -61,6 +62,13 @@ const statusTagTypeRegistry: Record<StatusDomain, Record<string, StatusTagType>>
|
|||||||
active: 'primary',
|
active: 'primary',
|
||||||
completed: 'success',
|
completed: 'success',
|
||||||
cancelled: 'danger'
|
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 'info';
|
||||||
}
|
}
|
||||||
|
|
||||||
return statusTagTypeRegistry[domain][statusCode] || 'info';
|
return statusTagTypeRegistry[domain]?.[statusCode] || 'info';
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getPersonalItemStatusTagType(statusCode: string | null | undefined) {
|
export function getPersonalItemStatusTagType(statusCode: string | null | undefined) {
|
||||||
return getStatusTagType('personalItem', statusCode);
|
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-monthly': 'My Monthly Report',
|
||||||
'personal-center_my-performance': 'My Performance',
|
'personal-center_my-performance': 'My Performance',
|
||||||
'personal-center_my-application': 'My Application',
|
'personal-center_my-application': 'My Application',
|
||||||
|
'personal-center_overtime-application': 'Overtime Application',
|
||||||
'personal-center_pending-approval': 'Pending Approval',
|
'personal-center_pending-approval': 'Pending Approval',
|
||||||
infra: 'Infra',
|
infra: 'Infra',
|
||||||
'infra_state-machine': 'State Machine',
|
'infra_state-machine': 'State Machine',
|
||||||
@@ -708,6 +709,7 @@ const local: App.I18n.Schema = {
|
|||||||
dictStatus: 'Dictionary Status',
|
dictStatus: 'Dictionary Status',
|
||||||
dictLabel: 'Dictionary Label',
|
dictLabel: 'Dictionary Label',
|
||||||
dictValue: 'Dictionary Value',
|
dictValue: 'Dictionary Value',
|
||||||
|
colorType: 'Color Type',
|
||||||
sort: 'Sort',
|
sort: 'Sort',
|
||||||
remark: 'Remark',
|
remark: 'Remark',
|
||||||
form: {
|
form: {
|
||||||
@@ -716,6 +718,7 @@ const local: App.I18n.Schema = {
|
|||||||
dictStatus: 'Please select dictionary status',
|
dictStatus: 'Please select dictionary status',
|
||||||
dictLabel: 'Please enter dictionary label',
|
dictLabel: 'Please enter dictionary label',
|
||||||
dictValue: 'Please enter dictionary value',
|
dictValue: 'Please enter dictionary value',
|
||||||
|
colorType: 'Please enter color type',
|
||||||
sort: 'Please enter sort',
|
sort: 'Please enter sort',
|
||||||
remark: 'Please enter remark'
|
remark: 'Please enter remark'
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -173,6 +173,7 @@ const local: App.I18n.Schema = {
|
|||||||
'personal-center_my-monthly': '我的月报',
|
'personal-center_my-monthly': '我的月报',
|
||||||
'personal-center_my-performance': '我的绩效',
|
'personal-center_my-performance': '我的绩效',
|
||||||
'personal-center_my-application': '我的申请',
|
'personal-center_my-application': '我的申请',
|
||||||
|
'personal-center_overtime-application': '加班申请',
|
||||||
'personal-center_pending-approval': '待我审批',
|
'personal-center_pending-approval': '待我审批',
|
||||||
infra: '基础设施',
|
infra: '基础设施',
|
||||||
'infra_state-machine': '状态机管理',
|
'infra_state-machine': '状态机管理',
|
||||||
@@ -696,6 +697,7 @@ const local: App.I18n.Schema = {
|
|||||||
dictStatus: '字典状态',
|
dictStatus: '字典状态',
|
||||||
dictLabel: '字典标签',
|
dictLabel: '字典标签',
|
||||||
dictValue: '字典键值',
|
dictValue: '字典键值',
|
||||||
|
colorType: '颜色类型',
|
||||||
sort: '排序',
|
sort: '排序',
|
||||||
remark: '备注',
|
remark: '备注',
|
||||||
form: {
|
form: {
|
||||||
@@ -704,6 +706,7 @@ const local: App.I18n.Schema = {
|
|||||||
dictStatus: '请选择字典状态',
|
dictStatus: '请选择字典状态',
|
||||||
dictLabel: '请输入字典标签',
|
dictLabel: '请输入字典标签',
|
||||||
dictValue: '请输入字典键值',
|
dictValue: '请输入字典键值',
|
||||||
|
colorType: '请输入颜色类型',
|
||||||
sort: '请输入排序',
|
sort: '请输入排序',
|
||||||
remark: '请输入备注'
|
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-performance": () => import("@/views/personal-center/my-performance/index.vue"),
|
||||||
"personal-center_my-profile": () => import("@/views/personal-center/my-profile/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_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"),
|
"personal-center_pending-approval": () => import("@/views/personal-center/pending-approval/index.vue"),
|
||||||
plugin_barcode: () => import("@/views/plugin/barcode/index.vue"),
|
plugin_barcode: () => import("@/views/plugin/barcode/index.vue"),
|
||||||
plugin_charts_antv: () => import("@/views/plugin/charts/antv/index.vue"),
|
plugin_charts_antv: () => import("@/views/plugin/charts/antv/index.vue"),
|
||||||
|
|||||||
@@ -351,6 +351,18 @@ export const generatedRoutes: GeneratedRoute[] = [
|
|||||||
keepAlive: true
|
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',
|
name: 'personal-center_pending-approval',
|
||||||
path: '/personal-center/pending-approval',
|
path: '/personal-center/pending-approval',
|
||||||
@@ -359,7 +371,7 @@ export const generatedRoutes: GeneratedRoute[] = [
|
|||||||
title: 'personal-center_pending-approval',
|
title: 'personal-center_pending-approval',
|
||||||
i18nKey: 'route.personal-center_pending-approval',
|
i18nKey: 'route.personal-center_pending-approval',
|
||||||
icon: 'mdi:check-decagram-outline',
|
icon: 'mdi:check-decagram-outline',
|
||||||
order: 5,
|
order: 7,
|
||||||
keepAlive: true
|
keepAlive: true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -196,6 +196,7 @@ const routeMap: RouteMap = {
|
|||||||
"personal-center_my-performance": "/personal-center/my-performance",
|
"personal-center_my-performance": "/personal-center/my-performance",
|
||||||
"personal-center_my-profile": "/personal-center/my-profile",
|
"personal-center_my-profile": "/personal-center/my-profile",
|
||||||
"personal-center_my-weekly": "/personal-center/my-weekly",
|
"personal-center_my-weekly": "/personal-center/my-weekly",
|
||||||
|
"personal-center_overtime-application": "/personal-center/overtime-application",
|
||||||
"personal-center_pending-approval": "/personal-center/pending-approval",
|
"personal-center_pending-approval": "/personal-center/pending-approval",
|
||||||
"plugin": "/plugin",
|
"plugin": "/plugin",
|
||||||
"plugin_barcode": "/plugin/barcode",
|
"plugin_barcode": "/plugin/barcode",
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { SYSTEM_SERVICE_PREFIX } from '@/constants/service';
|
import { SYSTEM_SERVICE_PREFIX } from '@/constants/service';
|
||||||
import { request } from '../request';
|
import { request } from '../request';
|
||||||
|
import { type ServiceRequestResult, mapServiceResult } from './shared';
|
||||||
|
|
||||||
const DICT_TYPE_PREFIX = `${SYSTEM_SERVICE_PREFIX}/dict-type`;
|
const DICT_TYPE_PREFIX = `${SYSTEM_SERVICE_PREFIX}/dict-type`;
|
||||||
const DICT_DATA_PREFIX = `${SYSTEM_SERVICE_PREFIX}/dict-data`;
|
const DICT_DATA_PREFIX = `${SYSTEM_SERVICE_PREFIX}/dict-data`;
|
||||||
@@ -15,6 +16,52 @@ function createBatchDeleteQuery(ids: number[]) {
|
|||||||
return query.toString();
|
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) {
|
export function fetchGetDictTypePage(params?: Api.Dict.DictTypeSearchParams) {
|
||||||
return request<Api.Dict.PageResult<Api.Dict.DictType>>({
|
return request<Api.Dict.PageResult<Api.Dict.DictType>>({
|
||||||
@@ -60,20 +107,40 @@ export function fetchBatchDeleteDictType(ids: number[]) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** 获取字典数据分页 */
|
/** 获取字典数据分页 */
|
||||||
export function fetchGetDictDataPage(params: Api.Dict.DictDataSearchParams) {
|
export async function fetchGetDictDataPage(params: Api.Dict.DictDataSearchParams) {
|
||||||
return request<Api.Dict.PageResult<Api.Dict.DictData>>({
|
const result = await request<DictDataPageResponse>({
|
||||||
url: `${DICT_DATA_PREFIX}/page`,
|
url: `${DICT_DATA_PREFIX}/page`,
|
||||||
method: 'get',
|
method: 'get',
|
||||||
params
|
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() {
|
export async function fetchGetFrontendDictCache() {
|
||||||
return request<Api.Dict.FrontendDictCache>({
|
const result = await request<FrontendDictCacheResponse>({
|
||||||
url: `${DICT_DATA_PREFIX}/frontend-cache`,
|
url: `${DICT_DATA_PREFIX}/frontend-cache`,
|
||||||
method: 'get'
|
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>({
|
return request<number>({
|
||||||
url: `${DICT_DATA_PREFIX}/create`,
|
url: `${DICT_DATA_PREFIX}/create`,
|
||||||
method: 'post',
|
method: 'post',
|
||||||
data
|
data: toSaveDictDataRequest(data)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,7 +157,7 @@ export function fetchUpdateDictData(data: { id: number } & Api.Dict.SaveDictData
|
|||||||
return request<boolean>({
|
return request<boolean>({
|
||||||
url: `${DICT_DATA_PREFIX}/update`,
|
url: `${DICT_DATA_PREFIX}/update`,
|
||||||
method: 'put',
|
method: 'put',
|
||||||
data
|
data: toSaveDictDataRequest(data)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,9 +179,14 @@ export function fetchBatchDeleteDictData(ids: number[]) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** 通过岗位编码获取该字典的所有字典数据 */
|
/** 通过岗位编码获取该字典的所有字典数据 */
|
||||||
export function fetchGetDictDataByCode(code: string) {
|
export async function fetchGetDictDataByCode(code: string) {
|
||||||
return request<Api.Dict.PageResult<Api.Dict.DictData>>({
|
const result = await request<DictDataPageResponse>({
|
||||||
url: `${DICT_DATA_PREFIX}/code?code=${code}`,
|
url: `${DICT_DATA_PREFIX}/code?code=${code}`,
|
||||||
method: 'get'
|
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 './file';
|
||||||
export * from './infra';
|
export * from './infra';
|
||||||
export * from './object-context';
|
export * from './object-context';
|
||||||
|
export * from './overtime-application';
|
||||||
export * from './personal-item';
|
export * from './personal-item';
|
||||||
export * from './product';
|
export * from './product';
|
||||||
export * from './project';
|
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) {
|
export function fetchGetUserPage(params?: Api.SystemManage.UserSearchParams) {
|
||||||
return request<Api.SystemManage.UserList>({
|
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;
|
type DictDataSearchParams = CommonType.RecordNullable<Pick<DictData, 'label' | 'dictType' | 'status'>> & PageParams;
|
||||||
|
|
||||||
/** dict data save params */
|
/** 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;
|
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;
|
dictStatus: string;
|
||||||
dictLabel: string;
|
dictLabel: string;
|
||||||
dictValue: string;
|
dictValue: string;
|
||||||
|
colorType: string;
|
||||||
sort: string;
|
sort: string;
|
||||||
remark: string;
|
remark: string;
|
||||||
form: {
|
form: {
|
||||||
@@ -874,6 +875,7 @@ declare namespace App {
|
|||||||
dictStatus: string;
|
dictStatus: string;
|
||||||
dictLabel: string;
|
dictLabel: string;
|
||||||
dictValue: string;
|
dictValue: string;
|
||||||
|
colorType: string;
|
||||||
sort: string;
|
sort: string;
|
||||||
remark: 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']
|
IconMdiCrown: typeof import('~icons/mdi/crown')['default']
|
||||||
IconMdiDeleteOutline: typeof import('~icons/mdi/delete-outline')['default']
|
IconMdiDeleteOutline: typeof import('~icons/mdi/delete-outline')['default']
|
||||||
IconMdiDotsHorizontal: typeof import('~icons/mdi/dots-horizontal')['default']
|
IconMdiDotsHorizontal: typeof import('~icons/mdi/dots-horizontal')['default']
|
||||||
|
IconMdiDownload: typeof import('~icons/mdi/download')['default']
|
||||||
IconMdiDrag: typeof import('~icons/mdi/drag')['default']
|
IconMdiDrag: typeof import('~icons/mdi/drag')['default']
|
||||||
IconMdiFilterVariant: typeof import('~icons/mdi/filter-variant')['default']
|
IconMdiFilterVariant: typeof import('~icons/mdi/filter-variant')['default']
|
||||||
IconMdiFolderOpen: typeof import('~icons/mdi/folder-open')['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-performance": "/personal-center/my-performance";
|
||||||
"personal-center_my-profile": "/personal-center/my-profile";
|
"personal-center_my-profile": "/personal-center/my-profile";
|
||||||
"personal-center_my-weekly": "/personal-center/my-weekly";
|
"personal-center_my-weekly": "/personal-center/my-weekly";
|
||||||
|
"personal-center_overtime-application": "/personal-center/overtime-application";
|
||||||
"personal-center_pending-approval": "/personal-center/pending-approval";
|
"personal-center_pending-approval": "/personal-center/pending-approval";
|
||||||
"plugin": "/plugin";
|
"plugin": "/plugin";
|
||||||
"plugin_barcode": "/plugin/barcode";
|
"plugin_barcode": "/plugin/barcode";
|
||||||
@@ -187,6 +188,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_pending-approval"
|
| "personal-center_pending-approval"
|
||||||
| "plugin_barcode"
|
| "plugin_barcode"
|
||||||
| "plugin_charts_antv"
|
| "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';
|
return status === 0 ? 'success' : 'info';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatColorType(value?: string | null) {
|
||||||
|
return value?.trim() || '--';
|
||||||
|
}
|
||||||
|
|
||||||
const dictTypeSearchParams = reactive(getInitDictTypeSearchParams());
|
const dictTypeSearchParams = reactive(getInitDictTypeSearchParams());
|
||||||
const dictDataSearchParams = reactive(getInitDictDataSearchParams());
|
const dictDataSearchParams = reactive(getInitDictDataSearchParams());
|
||||||
|
|
||||||
@@ -177,6 +181,13 @@ const {
|
|||||||
{ prop: 'index', type: 'index', label: $t('common.index'), width: 64 },
|
{ prop: 'index', type: 'index', label: $t('common.index'), width: 64 },
|
||||||
{ prop: 'label', label: $t('page.system.dict.dictLabel'), minWidth: 160 },
|
{ prop: 'label', label: $t('page.system.dict.dictLabel'), minWidth: 160 },
|
||||||
{ prop: 'value', label: $t('page.system.dict.dictValue'), minWidth: 180 },
|
{ 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: 'sort', label: $t('page.system.dict.sort'), width: 90, align: 'center' },
|
||||||
{
|
{
|
||||||
prop: 'status',
|
prop: 'status',
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ function createDefaultModel(): Model {
|
|||||||
label: '',
|
label: '',
|
||||||
value: '',
|
value: '',
|
||||||
dictType: '',
|
dictType: '',
|
||||||
|
colorType: '',
|
||||||
sort: 0,
|
sort: 0,
|
||||||
status: 0,
|
status: 0,
|
||||||
remark: ''
|
remark: ''
|
||||||
@@ -81,6 +82,7 @@ function handleInitModel() {
|
|||||||
label: props.rowData.label,
|
label: props.rowData.label,
|
||||||
value: props.rowData.value,
|
value: props.rowData.value,
|
||||||
dictType: props.rowData.dictType,
|
dictType: props.rowData.dictType,
|
||||||
|
colorType: props.rowData.colorType ?? '',
|
||||||
sort: props.rowData.sort,
|
sort: props.rowData.sort,
|
||||||
status: props.rowData.status,
|
status: props.rowData.status,
|
||||||
remark: props.rowData.remark ?? ''
|
remark: props.rowData.remark ?? ''
|
||||||
@@ -168,6 +170,11 @@ watch(visible, value => {
|
|||||||
<ElInput v-model="model.value" :placeholder="$t('page.system.dict.form.dictValue')" />
|
<ElInput v-model="model.value" :placeholder="$t('page.system.dict.form.dictValue')" />
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
</ElCol>
|
</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">
|
<ElCol :span="12">
|
||||||
<ElFormItem :label="$t('page.system.dict.sort')" prop="sort">
|
<ElFormItem :label="$t('page.system.dict.sort')" prop="sort">
|
||||||
<ElInputNumber
|
<ElInputNumber
|
||||||
|
|||||||
@@ -62,6 +62,10 @@ export interface WorkbenchTodoItemSource {
|
|||||||
overdue?: boolean;
|
overdue?: boolean;
|
||||||
/** 跳转路由 key(可选,未配则不跳转) */
|
/** 跳转路由 key(可选,未配则不跳转) */
|
||||||
routeKey?: string;
|
routeKey?: string;
|
||||||
|
/** 审批业务类型 */
|
||||||
|
approvalBizType?: 'overtime_application' | string;
|
||||||
|
/** 审批业务 ID */
|
||||||
|
approvalBizId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface WorkbenchTodoItem extends Omit<WorkbenchTodoItemSource, 'deadline'> {
|
export interface WorkbenchTodoItem extends Omit<WorkbenchTodoItemSource, 'deadline'> {
|
||||||
|
|||||||
@@ -1,8 +1,16 @@
|
|||||||
<script setup lang="ts">
|
<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 type { RouteKey } from '@elegant-router/types';
|
||||||
|
import {
|
||||||
|
fetchApproveOvertimeApplication,
|
||||||
|
fetchGetOvertimeApplicationApprovalPage,
|
||||||
|
fetchRejectOvertimeApplication
|
||||||
|
} from '@/service/api';
|
||||||
import { useRouterPush } from '@/hooks/common/router';
|
import { useRouterPush } from '@/hooks/common/router';
|
||||||
import PersonalItemOperateDialog from '@/views/personal-center/my-item/modules/personal-item-operate-dialog.vue';
|
import PersonalItemOperateDialog from '@/views/personal-center/my-item/modules/personal-item-operate-dialog.vue';
|
||||||
|
import 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 {
|
import {
|
||||||
type WorkbenchTodoDeadlineFilter,
|
type WorkbenchTodoDeadlineFilter,
|
||||||
type WorkbenchTodoItem,
|
type WorkbenchTodoItem,
|
||||||
@@ -15,8 +23,14 @@ import {
|
|||||||
} from '../homepage';
|
} from '../homepage';
|
||||||
import { workbenchTodoMock } from '../mock';
|
import { workbenchTodoMock } from '../mock';
|
||||||
import WorkbenchModuleCard from './workbench-module-card.vue';
|
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 SortKey = 'created' | 'priority' | 'deadline';
|
||||||
|
type OvertimeApprovalActionType = 'approve' | 'reject';
|
||||||
|
type ApprovalBizType = 'overtime_application';
|
||||||
|
|
||||||
defineOptions({ name: 'WorkbenchTodoPanel' });
|
defineOptions({ name: 'WorkbenchTodoPanel' });
|
||||||
|
|
||||||
@@ -38,6 +52,7 @@ const PAGE_SIZE = 5;
|
|||||||
|
|
||||||
const activeTab = ref<WorkbenchTodoMainTab>('all');
|
const activeTab = ref<WorkbenchTodoMainTab>('all');
|
||||||
const activeDeadlineFilter = ref<WorkbenchTodoDeadlineFilter>(null);
|
const activeDeadlineFilter = ref<WorkbenchTodoDeadlineFilter>(null);
|
||||||
|
const activeApprovalBizType = ref<ApprovalBizType>('overtime_application');
|
||||||
const activeSort = ref<SortKey>('deadline');
|
const activeSort = ref<SortKey>('deadline');
|
||||||
const currentPage = ref(1);
|
const currentPage = ref(1);
|
||||||
|
|
||||||
@@ -66,9 +81,33 @@ const deadlineFilters: Array<{ key: Exclude<WorkbenchTodoDeadlineFilter, null>;
|
|||||||
{ key: 'week', label: '本周到期' }
|
{ key: 'week', label: '本周到期' }
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const approvalBizTabs: Array<{ key: ApprovalBizType; label: string }> = [
|
||||||
|
{ key: 'overtime_application', label: '加班申请' }
|
||||||
|
];
|
||||||
|
|
||||||
const allItems = computed(() => buildWorkbenchTodoItems(workbenchTodoMock));
|
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 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() {
|
function handleOpenAdd() {
|
||||||
addDialogVisible.value = true;
|
addDialogVisible.value = true;
|
||||||
@@ -81,13 +120,13 @@ function handleAddSubmitted() {
|
|||||||
|
|
||||||
const tabCounts = computed(() => {
|
const tabCounts = computed(() => {
|
||||||
const counts: Record<WorkbenchTodoMainTab, number> = {
|
const counts: Record<WorkbenchTodoMainTab, number> = {
|
||||||
all: allItems.value.length,
|
all: mergedItems.value.length,
|
||||||
task: 0,
|
task: 0,
|
||||||
ticket: 0,
|
ticket: 0,
|
||||||
personal: 0,
|
personal: 0,
|
||||||
approval: 0
|
approval: 0
|
||||||
};
|
};
|
||||||
allItems.value.forEach(item => {
|
mergedItems.value.forEach(item => {
|
||||||
counts[item.category] += 1;
|
counts[item.category] += 1;
|
||||||
});
|
});
|
||||||
return counts;
|
return counts;
|
||||||
@@ -101,7 +140,7 @@ const tabOverdueCount = computed(() => {
|
|||||||
personal: 0,
|
personal: 0,
|
||||||
approval: 0
|
approval: 0
|
||||||
};
|
};
|
||||||
allItems.value.forEach(item => {
|
mergedItems.value.forEach(item => {
|
||||||
if (!isWorkbenchTodoOverdue(item)) return;
|
if (!isWorkbenchTodoOverdue(item)) return;
|
||||||
map.all += 1;
|
map.all += 1;
|
||||||
map[item.category] += 1;
|
map[item.category] += 1;
|
||||||
@@ -109,9 +148,29 @@ const tabOverdueCount = computed(() => {
|
|||||||
return map;
|
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 sortedItems = computed(() => {
|
||||||
const base = filteredItems.value;
|
const base = filteredItems.value;
|
||||||
@@ -155,20 +214,121 @@ function handleSelectDeadlineFilter(key: Exclude<WorkbenchTodoDeadlineFilter, nu
|
|||||||
activeDeadlineFilter.value = activeDeadlineFilter.value === key ? null : key;
|
activeDeadlineFilter.value = activeDeadlineFilter.value === key ? null : key;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleSelectApprovalBizType(key: ApprovalBizType) {
|
||||||
|
activeApprovalBizType.value = key;
|
||||||
|
}
|
||||||
|
|
||||||
function handleSelectSort(key: SortKey) {
|
function handleSelectSort(key: SortKey) {
|
||||||
activeSort.value = key;
|
activeSort.value = key;
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleClickItem(item: WorkbenchTodoItem) {
|
function handleClickItem(item: WorkbenchTodoItem) {
|
||||||
|
if (item.approvalBizType === 'overtime_application') {
|
||||||
|
openOvertimeDetail(item);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!item.routeKey) return;
|
if (!item.routeKey) return;
|
||||||
routerPushByKey(item.routeKey as RouteKey);
|
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) {
|
function getDeadlineToneClass(item: WorkbenchTodoItem) {
|
||||||
if (isWorkbenchTodoOverdue(item)) return 'workbench-todo__deadline--rose';
|
if (isWorkbenchTodoOverdue(item)) return 'workbench-todo__deadline--rose';
|
||||||
if (item.remainingDays === 0) return 'workbench-todo__deadline--amber';
|
if (item.remainingDays === 0) return 'workbench-todo__deadline--amber';
|
||||||
return 'workbench-todo__deadline--slate';
|
return 'workbench-todo__deadline--slate';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onMounted(loadOvertimeApprovalItems);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -222,7 +382,19 @@ function getDeadlineToneClass(item: WorkbenchTodoItem) {
|
|||||||
{{ filter.label }}
|
{{ filter.label }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</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">
|
<ElDropdown trigger="click" placement="bottom-end" @command="handleSelectSort">
|
||||||
<span class="workbench-todo__sort">
|
<span class="workbench-todo__sort">
|
||||||
@@ -250,7 +422,7 @@ function getDeadlineToneClass(item: WorkbenchTodoItem) {
|
|||||||
v-for="item in pagedItems"
|
v-for="item in pagedItems"
|
||||||
:key="item.id"
|
:key="item.id"
|
||||||
class="workbench-todo__item"
|
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)"
|
@click="handleClickItem(item)"
|
||||||
>
|
>
|
||||||
<div class="workbench-todo__leading">
|
<div class="workbench-todo__leading">
|
||||||
@@ -268,6 +440,38 @@ function getDeadlineToneClass(item: WorkbenchTodoItem) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="workbench-todo__trailing">
|
<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)">
|
<span class="workbench-todo__deadline" :class="getDeadlineToneClass(item)">
|
||||||
{{ item.deadlineLabel }}
|
{{ item.deadlineLabel }}
|
||||||
</span>
|
</span>
|
||||||
@@ -295,6 +499,18 @@ function getDeadlineToneClass(item: WorkbenchTodoItem) {
|
|||||||
:row-data="null"
|
:row-data="null"
|
||||||
@submitted="handleAddSubmitted"
|
@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>
|
</WorkbenchModuleCard>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -468,6 +684,12 @@ function getDeadlineToneClass(item: WorkbenchTodoItem) {
|
|||||||
color: rgb(190 18 60 / 96%);
|
color: rgb(190 18 60 / 96%);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.workbench-todo__filter-count {
|
||||||
|
margin-left: 4px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
.workbench-todo__content {
|
.workbench-todo__content {
|
||||||
min-height: 400px;
|
min-height: 400px;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -596,6 +818,25 @@ function getDeadlineToneClass(item: WorkbenchTodoItem) {
|
|||||||
.workbench-todo__trailing {
|
.workbench-todo__trailing {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
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 {
|
.workbench-todo__deadline {
|
||||||
|
|||||||
Reference in New Issue
Block a user