Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 17690283f6 | |||
| 030dc737fc | |||
| 609a01dc8a | |||
| 80f028bcb9 | |||
| 5061eced32 | |||
| 6896a86130 | |||
| 0652a24c5e | |||
| d53a8dfae5 | |||
| 2e369b23a9 | |||
| b72ad00912 | |||
| 7cc29e0a35 | |||
| 39458386ae | |||
| acef4418d8 |
4
.env
4
.env
@@ -2,9 +2,9 @@
|
|||||||
# 如果部署在子目录下,结尾必须带 "/",例如 "/admin/",不能写成 "/admin"
|
# 如果部署在子目录下,结尾必须带 "/",例如 "/admin/",不能写成 "/admin"
|
||||||
VITE_BASE_URL=/
|
VITE_BASE_URL=/
|
||||||
|
|
||||||
VITE_APP_TITLE=研发内部管理系统
|
VITE_APP_TITLE=研发管理系统
|
||||||
|
|
||||||
VITE_APP_DESC=Frontend application for 灿能研发内部管理系统
|
VITE_APP_DESC=Frontend application for 灿能研发管理系统
|
||||||
|
|
||||||
# 图标名称前缀
|
# 图标名称前缀
|
||||||
VITE_ICON_PREFIX=icon
|
VITE_ICON_PREFIX=icon
|
||||||
|
|||||||
2
.trae/rules/vue-need.md
Normal file
2
.trae/rules/vue-need.md
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
1. 每次开发新功能、编写代码时都添加好相应的注释。
|
||||||
|
2. 所有的vue文件编码必须是UTF-8的。
|
||||||
@@ -131,16 +131,23 @@ export function setupElegantRouter() {
|
|||||||
order: 1,
|
order: 1,
|
||||||
keepAlive: true
|
keepAlive: true
|
||||||
},
|
},
|
||||||
'personal-center_my-weekly': {
|
'personal-center_work-report': {
|
||||||
icon: 'mdi:calendar-week-outline',
|
icon: 'mdi:file-chart-outline',
|
||||||
order: 2,
|
|
||||||
keepAlive: true
|
|
||||||
},
|
|
||||||
'personal-center_my-monthly': {
|
|
||||||
icon: 'mdi:calendar-month-outline',
|
|
||||||
order: 3,
|
order: 3,
|
||||||
keepAlive: true
|
keepAlive: true
|
||||||
},
|
},
|
||||||
|
'personal-center_work-report_weekly': {
|
||||||
|
hideInMenu: true,
|
||||||
|
activeMenu: 'personal-center_work-report'
|
||||||
|
},
|
||||||
|
'personal-center_work-report_monthly': {
|
||||||
|
hideInMenu: true,
|
||||||
|
activeMenu: 'personal-center_work-report'
|
||||||
|
},
|
||||||
|
'personal-center_work-report_project': {
|
||||||
|
hideInMenu: true,
|
||||||
|
activeMenu: 'personal-center_work-report'
|
||||||
|
},
|
||||||
'personal-center_my-performance': {
|
'personal-center_my-performance': {
|
||||||
icon: 'mdi:trophy-outline',
|
icon: 'mdi:trophy-outline',
|
||||||
order: 4,
|
order: 4,
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"generatedAt": "2026-06-01T01:55:51.875Z",
|
"generatedAt": "2026-06-05T03:08:01.803Z",
|
||||||
"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": 23,
|
"total": 22,
|
||||||
"items": [
|
"items": [
|
||||||
{
|
{
|
||||||
"name": "product_list",
|
"name": "product_list",
|
||||||
@@ -306,15 +306,15 @@
|
|||||||
"source": "generated"
|
"source": "generated"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "personal-center_my-weekly",
|
"name": "personal-center_work-report",
|
||||||
"path": "/personal-center/my-weekly",
|
"path": "/personal-center/work-report",
|
||||||
"component": "view.personal-center_my-weekly",
|
"component": "view.personal-center_work-report",
|
||||||
"title": "我的周报",
|
"title": "工作报告",
|
||||||
"routeTitle": "personal-center_my-weekly",
|
"routeTitle": "personal-center_work-report",
|
||||||
"i18nKey": "route.personal-center_my-weekly",
|
"i18nKey": "route.personal-center_work-report",
|
||||||
"icon": "mdi:calendar-week-outline",
|
"icon": "mdi:file-chart-outline",
|
||||||
"localIcon": null,
|
"localIcon": null,
|
||||||
"order": 1,
|
"order": 3,
|
||||||
"hideInMenu": false,
|
"hideInMenu": false,
|
||||||
"keepAlive": true,
|
"keepAlive": true,
|
||||||
"activeMenu": null,
|
"activeMenu": null,
|
||||||
@@ -323,44 +323,11 @@
|
|||||||
"redirect": null,
|
"redirect": null,
|
||||||
"props": null,
|
"props": null,
|
||||||
"meta": {
|
"meta": {
|
||||||
"title": "我的周报",
|
"title": "工作报告",
|
||||||
"i18nKey": "route.personal-center_my-weekly",
|
"i18nKey": "route.personal-center_work-report",
|
||||||
"icon": "mdi:calendar-week-outline",
|
"icon": "mdi:file-chart-outline",
|
||||||
"localIcon": null,
|
"localIcon": null,
|
||||||
"order": 1,
|
"order": 3,
|
||||||
"keepAlive": true,
|
|
||||||
"hideInMenu": false,
|
|
||||||
"activeMenu": null,
|
|
||||||
"multiTab": false,
|
|
||||||
"fixedIndexInTab": null
|
|
||||||
},
|
|
||||||
"parentName": "personal-center",
|
|
||||||
"pageType": "leaf",
|
|
||||||
"source": "generated"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "personal-center_my-monthly",
|
|
||||||
"path": "/personal-center/my-monthly",
|
|
||||||
"component": "view.personal-center_my-monthly",
|
|
||||||
"title": "我的月报",
|
|
||||||
"routeTitle": "personal-center_my-monthly",
|
|
||||||
"i18nKey": "route.personal-center_my-monthly",
|
|
||||||
"icon": "mdi:calendar-month-outline",
|
|
||||||
"localIcon": null,
|
|
||||||
"order": 2,
|
|
||||||
"hideInMenu": false,
|
|
||||||
"keepAlive": true,
|
|
||||||
"activeMenu": null,
|
|
||||||
"multiTab": false,
|
|
||||||
"fixedIndexInTab": null,
|
|
||||||
"redirect": null,
|
|
||||||
"props": null,
|
|
||||||
"meta": {
|
|
||||||
"title": "我的月报",
|
|
||||||
"i18nKey": "route.personal-center_my-monthly",
|
|
||||||
"icon": "mdi:calendar-month-outline",
|
|
||||||
"localIcon": null,
|
|
||||||
"order": 2,
|
|
||||||
"keepAlive": true,
|
"keepAlive": true,
|
||||||
"hideInMenu": false,
|
"hideInMenu": false,
|
||||||
"activeMenu": null,
|
"activeMenu": null,
|
||||||
@@ -380,7 +347,7 @@
|
|||||||
"i18nKey": "route.personal-center_my-performance",
|
"i18nKey": "route.personal-center_my-performance",
|
||||||
"icon": "mdi:trophy-outline",
|
"icon": "mdi:trophy-outline",
|
||||||
"localIcon": null,
|
"localIcon": null,
|
||||||
"order": 3,
|
"order": 4,
|
||||||
"hideInMenu": false,
|
"hideInMenu": false,
|
||||||
"keepAlive": true,
|
"keepAlive": true,
|
||||||
"activeMenu": null,
|
"activeMenu": null,
|
||||||
@@ -393,7 +360,7 @@
|
|||||||
"i18nKey": "route.personal-center_my-performance",
|
"i18nKey": "route.personal-center_my-performance",
|
||||||
"icon": "mdi:trophy-outline",
|
"icon": "mdi:trophy-outline",
|
||||||
"localIcon": null,
|
"localIcon": null,
|
||||||
"order": 3,
|
"order": 4,
|
||||||
"keepAlive": true,
|
"keepAlive": true,
|
||||||
"hideInMenu": false,
|
"hideInMenu": false,
|
||||||
"activeMenu": null,
|
"activeMenu": null,
|
||||||
@@ -413,7 +380,7 @@
|
|||||||
"i18nKey": "route.personal-center_my-application",
|
"i18nKey": "route.personal-center_my-application",
|
||||||
"icon": "mdi:file-document-outline",
|
"icon": "mdi:file-document-outline",
|
||||||
"localIcon": null,
|
"localIcon": null,
|
||||||
"order": 4,
|
"order": 5,
|
||||||
"hideInMenu": false,
|
"hideInMenu": false,
|
||||||
"keepAlive": true,
|
"keepAlive": true,
|
||||||
"activeMenu": null,
|
"activeMenu": null,
|
||||||
@@ -426,7 +393,7 @@
|
|||||||
"i18nKey": "route.personal-center_my-application",
|
"i18nKey": "route.personal-center_my-application",
|
||||||
"icon": "mdi:file-document-outline",
|
"icon": "mdi:file-document-outline",
|
||||||
"localIcon": null,
|
"localIcon": null,
|
||||||
"order": 4,
|
"order": 5,
|
||||||
"keepAlive": true,
|
"keepAlive": true,
|
||||||
"hideInMenu": false,
|
"hideInMenu": false,
|
||||||
"activeMenu": null,
|
"activeMenu": null,
|
||||||
@@ -446,7 +413,7 @@
|
|||||||
"i18nKey": "route.personal-center_pending-approval",
|
"i18nKey": "route.personal-center_pending-approval",
|
||||||
"icon": "mdi:check-decagram-outline",
|
"icon": "mdi:check-decagram-outline",
|
||||||
"localIcon": null,
|
"localIcon": null,
|
||||||
"order": 5,
|
"order": 7,
|
||||||
"hideInMenu": false,
|
"hideInMenu": false,
|
||||||
"keepAlive": true,
|
"keepAlive": true,
|
||||||
"activeMenu": null,
|
"activeMenu": null,
|
||||||
@@ -459,7 +426,7 @@
|
|||||||
"i18nKey": "route.personal-center_pending-approval",
|
"i18nKey": "route.personal-center_pending-approval",
|
||||||
"icon": "mdi:check-decagram-outline",
|
"icon": "mdi:check-decagram-outline",
|
||||||
"localIcon": null,
|
"localIcon": null,
|
||||||
"order": 5,
|
"order": 7,
|
||||||
"keepAlive": true,
|
"keepAlive": true,
|
||||||
"hideInMenu": false,
|
"hideInMenu": false,
|
||||||
"activeMenu": null,
|
"activeMenu": null,
|
||||||
|
|||||||
25
package.json
25
package.json
@@ -37,60 +37,39 @@
|
|||||||
"update-pkg": "sa update-pkg"
|
"update-pkg": "sa update-pkg"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@antv/data-set": "0.11.8",
|
|
||||||
"@antv/g2": "5.4.0",
|
|
||||||
"@antv/g6": "5.0.49",
|
|
||||||
"@better-scroll/core": "2.5.1",
|
"@better-scroll/core": "2.5.1",
|
||||||
"@iconify-vue/mingcute": "^1.0.5",
|
|
||||||
"@iconify/vue": "5.0.0",
|
"@iconify/vue": "5.0.0",
|
||||||
"@sa/axios": "workspace:*",
|
"@sa/axios": "workspace:*",
|
||||||
"@sa/color": "workspace:*",
|
"@sa/color": "workspace:*",
|
||||||
"@sa/hooks": "workspace:*",
|
"@sa/hooks": "workspace:*",
|
||||||
"@sa/materials": "workspace:*",
|
"@sa/materials": "workspace:*",
|
||||||
"@sa/utils": "workspace:*",
|
"@sa/utils": "workspace:*",
|
||||||
"@visactor/vchart": "2.0.4",
|
|
||||||
"@visactor/vchart-theme": "1.12.2",
|
|
||||||
"@visactor/vtable-editors": "1.19.8",
|
|
||||||
"@visactor/vtable-gantt": "1.19.8",
|
|
||||||
"@visactor/vue-vtable": "1.19.8",
|
|
||||||
"@vueuse/components": "13.9.0",
|
|
||||||
"@vueuse/core": "13.9.0",
|
"@vueuse/core": "13.9.0",
|
||||||
"@wangeditor/editor": "^5.1.23",
|
"@wangeditor/editor": "^5.1.23",
|
||||||
"@wangeditor/editor-for-vue": "^5.1.12",
|
"@wangeditor/editor-for-vue": "^5.1.12",
|
||||||
"clipboard": "2.0.11",
|
"clipboard": "2.0.11",
|
||||||
"dayjs": "1.11.18",
|
"dayjs": "1.11.18",
|
||||||
"defu": "^6.1.4",
|
"defu": "^6.1.4",
|
||||||
"dhtmlx-gantt": "9.0.14",
|
|
||||||
"dompurify": "3.2.6",
|
"dompurify": "3.2.6",
|
||||||
"echarts": "6.0.0",
|
"echarts": "6.0.0",
|
||||||
"element-plus": "^2.11.1",
|
"element-plus": "^2.11.1",
|
||||||
"jsbarcode": "3.12.1",
|
"grid-layout-plus": "^1.1.1",
|
||||||
"jsencrypt": "^3.5.4",
|
"jsencrypt": "^3.5.4",
|
||||||
"json5": "2.2.3",
|
"json5": "2.2.3",
|
||||||
"nprogress": "0.2.0",
|
"nprogress": "0.2.0",
|
||||||
"pinia": "3.0.3",
|
"pinia": "3.0.3",
|
||||||
"pinyin-pro": "3.27.0",
|
|
||||||
"print-js": "1.6.0",
|
|
||||||
"swiper": "11.2.10",
|
|
||||||
"tailwind-merge": "3.3.1",
|
"tailwind-merge": "3.3.1",
|
||||||
"typeit": "8.8.7",
|
|
||||||
"vditor": "3.11.2",
|
|
||||||
"vue": "3.5.20",
|
"vue": "3.5.20",
|
||||||
"vue-draggable-plus": "0.6.0",
|
"vue-draggable-plus": "0.6.0",
|
||||||
"vue-i18n": "11.1.11",
|
"vue-i18n": "11.1.11",
|
||||||
"vue-pdf-embed": "2.1.3",
|
"vue-router": "4.5.1"
|
||||||
"vue-router": "4.5.1",
|
|
||||||
"xgplayer": "3.0.23",
|
|
||||||
"xlsx": "0.18.5"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@amap/amap-jsapi-types": "0.0.15",
|
|
||||||
"@elegant-router/vue": "0.3.8",
|
"@elegant-router/vue": "0.3.8",
|
||||||
"@iconify/json": "2.2.380",
|
"@iconify/json": "2.2.380",
|
||||||
"@sa/scripts": "workspace:*",
|
"@sa/scripts": "workspace:*",
|
||||||
"@sa/uno-preset": "workspace:*",
|
"@sa/uno-preset": "workspace:*",
|
||||||
"@soybeanjs/eslint-config": "1.7.1",
|
"@soybeanjs/eslint-config": "1.7.1",
|
||||||
"@types/bmapgl": "0.0.7",
|
|
||||||
"@types/node": "24.3.0",
|
"@types/node": "24.3.0",
|
||||||
"@types/nprogress": "0.2.3",
|
"@types/nprogress": "0.2.3",
|
||||||
"@unocss/eslint-config": "66.5.0",
|
"@unocss/eslint-config": "66.5.0",
|
||||||
|
|||||||
2458
pnpm-lock.yaml
generated
2458
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -18,6 +18,12 @@ export interface SearchField {
|
|||||||
label: string;
|
label: string;
|
||||||
/** 字段类型 */
|
/** 字段类型 */
|
||||||
type: 'input' | 'select' | 'date' | 'dateRange' | 'dict';
|
type: 'input' | 'select' | 'date' | 'dateRange' | 'dict';
|
||||||
|
/** date 字段的日期粒度 */
|
||||||
|
dateType?: 'date' | 'month';
|
||||||
|
/** dateRange 字段的日期范围粒度 */
|
||||||
|
dateRangeType?: 'daterange' | 'monthrange';
|
||||||
|
/** 日期字段提交格式 */
|
||||||
|
valueFormat?: string;
|
||||||
/** 占位列数,默认 1 */
|
/** 占位列数,默认 1 */
|
||||||
span?: number;
|
span?: number;
|
||||||
/** select 类型的选项 */
|
/** select 类型的选项 */
|
||||||
@@ -156,23 +162,23 @@ function handleSearch() {
|
|||||||
<ElDatePicker
|
<ElDatePicker
|
||||||
v-else-if="field.type === 'date'"
|
v-else-if="field.type === 'date'"
|
||||||
:model-value="props.modelValue[field.key]"
|
:model-value="props.modelValue[field.key]"
|
||||||
type="date"
|
:type="field.dateType || 'date'"
|
||||||
:placeholder="field.placeholder"
|
:placeholder="field.placeholder"
|
||||||
clearable
|
clearable
|
||||||
:disabled="props.disabled"
|
:disabled="props.disabled"
|
||||||
value-format="YYYY-MM-DD"
|
:value-format="field.valueFormat || 'YYYY-MM-DD'"
|
||||||
@update:model-value="val => (props.modelValue[field.key] = val)"
|
@update:model-value="val => (props.modelValue[field.key] = val)"
|
||||||
/>
|
/>
|
||||||
<ElDatePicker
|
<ElDatePicker
|
||||||
v-else-if="field.type === 'dateRange'"
|
v-else-if="field.type === 'dateRange'"
|
||||||
:model-value="props.modelValue[field.key]"
|
:model-value="props.modelValue[field.key]"
|
||||||
type="daterange"
|
:type="field.dateRangeType || 'daterange'"
|
||||||
:placeholder="field.placeholder"
|
:placeholder="field.placeholder"
|
||||||
clearable
|
clearable
|
||||||
:disabled="props.disabled"
|
:disabled="props.disabled"
|
||||||
value-format="YYYY-MM-DD"
|
:value-format="field.valueFormat || 'YYYY-MM-DD'"
|
||||||
start-placeholder="开始日期"
|
:start-placeholder="field.dateRangeType === 'monthrange' ? '开始月份' : '开始日期'"
|
||||||
end-placeholder="结束日期"
|
:end-placeholder="field.dateRangeType === 'monthrange' ? '结束月份' : '结束日期'"
|
||||||
@update:model-value="val => (props.modelValue[field.key] = val)"
|
@update:model-value="val => (props.modelValue[field.key] = val)"
|
||||||
/>
|
/>
|
||||||
<DictSelect
|
<DictSelect
|
||||||
@@ -253,23 +259,23 @@ function handleSearch() {
|
|||||||
<ElDatePicker
|
<ElDatePicker
|
||||||
v-else-if="field.type === 'date'"
|
v-else-if="field.type === 'date'"
|
||||||
:model-value="props.modelValue[field.key]"
|
:model-value="props.modelValue[field.key]"
|
||||||
type="date"
|
:type="field.dateType || 'date'"
|
||||||
:placeholder="field.placeholder"
|
:placeholder="field.placeholder"
|
||||||
clearable
|
clearable
|
||||||
:disabled="props.disabled"
|
:disabled="props.disabled"
|
||||||
value-format="YYYY-MM-DD"
|
:value-format="field.valueFormat || 'YYYY-MM-DD'"
|
||||||
@update:model-value="val => (props.modelValue[field.key] = val)"
|
@update:model-value="val => (props.modelValue[field.key] = val)"
|
||||||
/>
|
/>
|
||||||
<ElDatePicker
|
<ElDatePicker
|
||||||
v-else-if="field.type === 'dateRange'"
|
v-else-if="field.type === 'dateRange'"
|
||||||
:model-value="props.modelValue[field.key]"
|
:model-value="props.modelValue[field.key]"
|
||||||
type="daterange"
|
:type="field.dateRangeType || 'daterange'"
|
||||||
:placeholder="field.placeholder"
|
:placeholder="field.placeholder"
|
||||||
clearable
|
clearable
|
||||||
:disabled="props.disabled"
|
:disabled="props.disabled"
|
||||||
value-format="YYYY-MM-DD"
|
:value-format="field.valueFormat || 'YYYY-MM-DD'"
|
||||||
start-placeholder="开始日期"
|
:start-placeholder="field.dateRangeType === 'monthrange' ? '开始月份' : '开始日期'"
|
||||||
end-placeholder="结束日期"
|
:end-placeholder="field.dateRangeType === 'monthrange' ? '结束月份' : '结束日期'"
|
||||||
@update:model-value="val => (props.modelValue[field.key] = val)"
|
@update:model-value="val => (props.modelValue[field.key] = val)"
|
||||||
/>
|
/>
|
||||||
<DictSelect
|
<DictSelect
|
||||||
|
|||||||
@@ -1,61 +0,0 @@
|
|||||||
<script lang="ts" setup>
|
|
||||||
import { computed } from 'vue';
|
|
||||||
import { getPaletteColorByNumber } from '@sa/color';
|
|
||||||
|
|
||||||
defineOptions({ name: 'WaveBg' });
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
/** Theme color */
|
|
||||||
themeColor: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const props = defineProps<Props>();
|
|
||||||
|
|
||||||
const lightColor = computed(() => getPaletteColorByNumber(props.themeColor, 200));
|
|
||||||
const darkColor = computed(() => getPaletteColorByNumber(props.themeColor, 500));
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="absolute-lt z-1 size-full overflow-hidden">
|
|
||||||
<div class="absolute -right-300px -top-900px lt-sm:(-right-100px -top-1170px)">
|
|
||||||
<svg height="1337" width="1337">
|
|
||||||
<defs>
|
|
||||||
<path
|
|
||||||
id="path-1"
|
|
||||||
opacity="1"
|
|
||||||
fill-rule="evenodd"
|
|
||||||
d="M1337,668.5 C1337,1037.455193874239 1037.455193874239,1337 668.5,1337 C523.6725684305388,1337 337,1236 370.50000000000006,1094 C434.03835568300906,824.6732385973953 6.906089672974592e-14,892.6277623047779 0,668.5000000000001 C0,299.5448061257611 299.5448061257609,1.1368683772161603e-13 668.4999999999999,0 C1037.455193874239,0 1337,299.544806125761 1337,668.5Z"
|
|
||||||
/>
|
|
||||||
<linearGradient id="linearGradient-2" x1="0.79" y1="0.62" x2="0.21" y2="0.86">
|
|
||||||
<stop offset="0" :stop-color="lightColor" stop-opacity="1" />
|
|
||||||
<stop offset="1" :stop-color="darkColor" stop-opacity="1" />
|
|
||||||
</linearGradient>
|
|
||||||
</defs>
|
|
||||||
<g opacity="1">
|
|
||||||
<use xlink:href="#path-1" fill="url(#linearGradient-2)" fill-opacity="1" />
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<div class="absolute -bottom-400px -left-200px lt-sm:(-bottom-760px -left-100px)">
|
|
||||||
<svg height="896" width="967.8852157128662">
|
|
||||||
<defs>
|
|
||||||
<path
|
|
||||||
id="path-2"
|
|
||||||
opacity="1"
|
|
||||||
fill-rule="evenodd"
|
|
||||||
d="M896,448 C1142.6325445712241,465.5747656464056 695.2579309733121,896 448,896 C200.74206902668806,896 5.684341886080802e-14,695.2579309733121 0,448.0000000000001 C0,200.74206902668806 200.74206902668791,5.684341886080802e-14 447.99999999999994,0 C695.2579309733121,0 475,418 896,448Z"
|
|
||||||
/>
|
|
||||||
<linearGradient id="linearGradient-3" x1="0.5" y1="0" x2="0.5" y2="1">
|
|
||||||
<stop offset="0" :stop-color="darkColor" stop-opacity="1" />
|
|
||||||
<stop offset="1" :stop-color="lightColor" stop-opacity="1" />
|
|
||||||
</linearGradient>
|
|
||||||
</defs>
|
|
||||||
<g opacity="1">
|
|
||||||
<use xlink:href="#path-2" fill="url(#linearGradient-3)" fill-opacity="1" />
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped></style>
|
|
||||||
@@ -112,14 +112,6 @@ export const RDMS_REQ_CAN_DELETE_STATUS_DICT_CODE = 'rdms_req_can_delete_status'
|
|||||||
*/
|
*/
|
||||||
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';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 加班时长快捷选项字典编码
|
* 加班时长快捷选项字典编码
|
||||||
*
|
*
|
||||||
@@ -127,3 +119,12 @@ export const RDMS_OVERTIME_APPLICATION_STATUS_DICT_CODE = 'rdms_overtime_applica
|
|||||||
* 来源口径:`overtime-application-design.md` 明确时长下拉字典为 rdms_overtime_duration
|
* 来源口径:`overtime-application-design.md` 明确时长下拉字典为 rdms_overtime_duration
|
||||||
*/
|
*/
|
||||||
export const RDMS_OVERTIME_DURATION_DICT_CODE = 'rdms_overtime_duration';
|
export const RDMS_OVERTIME_DURATION_DICT_CODE = 'rdms_overtime_duration';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 站内信消息等级字典编码
|
||||||
|
*
|
||||||
|
* 对应业务字段:站内信 NotifyMessage.level(1=普通 2=提醒 3=警告 4=严重,数字越大越紧急)
|
||||||
|
* 来源口径:`2026-06-13-站内信消息等级-前端对接.html` 明确等级字典为 notify_message_level,
|
||||||
|
* 显示名与颜色(hex)均走字典,前端按 level 取色不硬编码。
|
||||||
|
*/
|
||||||
|
export const NOTIFY_MESSAGE_LEVEL_DICT_CODE = 'notify_message_level';
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
/** baidu map sdk url */
|
|
||||||
export const BAIDU_MAP_SDK_URL = `https://api.map.baidu.com/getscript?v=3.0&ak=KSezYymXPth1DIGILRX3oYN9PxbOQQmU&services=&t=20210201100830&s=1`;
|
|
||||||
|
|
||||||
/** Amap sdk url */
|
|
||||||
export const AMAP_SDK_URL = 'https://webapi.amap.com/maps?v=2.0&key=e7bd02bd504062087e6563daf4d6721d';
|
|
||||||
|
|
||||||
/** tencent sdk url */
|
|
||||||
export const TENCENT_MAP_SDK_URL = 'https://map.qq.com/api/gljs?v=1.exp&key=A6DBZ-KXPLW-JKSRY-ONZF4-CPHY3-K6BL7';
|
|
||||||
@@ -14,8 +14,10 @@ export type StatusDomain =
|
|||||||
| 'taskAssigneeMember'
|
| 'taskAssigneeMember'
|
||||||
| 'project'
|
| 'project'
|
||||||
| 'product'
|
| 'product'
|
||||||
| 'requirement'
|
| 'productRequirement'
|
||||||
|
| 'projectRequirement'
|
||||||
| 'workOrder'
|
| 'workOrder'
|
||||||
|
| 'workReport'
|
||||||
| 'personalItem'
|
| 'personalItem'
|
||||||
| 'overtimeApplication';
|
| 'overtimeApplication';
|
||||||
|
|
||||||
@@ -52,10 +54,40 @@ const statusTagTypeRegistry: Record<StatusDomain, Record<string, StatusTagType>>
|
|||||||
project: {},
|
project: {},
|
||||||
// 产品(待补全)
|
// 产品(待补全)
|
||||||
product: {},
|
product: {},
|
||||||
// 需求(待补全)
|
// 产品需求
|
||||||
requirement: {},
|
productRequirement: {
|
||||||
|
pending_claim: 'info',
|
||||||
|
pending_review: 'info',
|
||||||
|
pending_dispatch: 'primary',
|
||||||
|
reviewed: 'success',
|
||||||
|
review_rejected: 'danger',
|
||||||
|
implementing: 'primary',
|
||||||
|
accepted: 'success',
|
||||||
|
closed: 'danger',
|
||||||
|
rejected: 'danger',
|
||||||
|
cancelled: 'danger'
|
||||||
|
},
|
||||||
|
// 项目需求
|
||||||
|
projectRequirement: {
|
||||||
|
pending_claim: 'info',
|
||||||
|
pending_review: 'info',
|
||||||
|
reviewed: 'success',
|
||||||
|
review_rejected: 'danger',
|
||||||
|
implementing: 'primary',
|
||||||
|
accepted: 'success',
|
||||||
|
closed: 'danger',
|
||||||
|
rejected: 'danger',
|
||||||
|
cancelled: 'danger'
|
||||||
|
},
|
||||||
// 工单(待补全)
|
// 工单(待补全)
|
||||||
workOrder: {},
|
workOrder: {},
|
||||||
|
// 工作报告
|
||||||
|
workReport: {
|
||||||
|
draft: 'info',
|
||||||
|
pending_approval: 'warning',
|
||||||
|
approved: 'success',
|
||||||
|
rejected: 'danger'
|
||||||
|
},
|
||||||
// 个人事项
|
// 个人事项
|
||||||
personalItem: {
|
personalItem: {
|
||||||
pending: 'info',
|
pending: 'info',
|
||||||
@@ -67,8 +99,7 @@ const statusTagTypeRegistry: Record<StatusDomain, Record<string, StatusTagType>>
|
|||||||
overtimeApplication: {
|
overtimeApplication: {
|
||||||
pending: 'warning',
|
pending: 'warning',
|
||||||
approved: 'success',
|
approved: 'success',
|
||||||
rejected: 'danger',
|
rejected: 'danger'
|
||||||
cancelled: 'info'
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -83,7 +114,3 @@ export function getStatusTagType(domain: StatusDomain, statusCode: string | null
|
|||||||
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);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -131,12 +131,14 @@ export function useEcharts<T extends ECOption>(optionsFactory: () => T, hooks: C
|
|||||||
* @param callback callback function
|
* @param callback callback function
|
||||||
*/
|
*/
|
||||||
async function updateOptions(callback: (opts: T, optsFactory: () => T) => ECOption = () => chartOptions) {
|
async function updateOptions(callback: (opts: T, optsFactory: () => T) => ECOption = () => chartOptions) {
|
||||||
if (!isRendered()) return;
|
|
||||||
|
|
||||||
const updatedOpts = callback(chartOptions, optionsFactory);
|
const updatedOpts = callback(chartOptions, optionsFactory);
|
||||||
|
|
||||||
Object.assign(chartOptions, updatedOpts);
|
Object.assign(chartOptions, updatedOpts);
|
||||||
|
|
||||||
|
// 图表未初始化(容器尺寸未就绪)时只缓存最新 options,待 render() 初始化时一并应用;
|
||||||
|
// 否则数据先于初始化到达会被静默丢弃,首屏永远停留在空数据
|
||||||
|
if (!isRendered()) return;
|
||||||
|
|
||||||
if (isRendered()) {
|
if (isRendered()) {
|
||||||
chart?.clear();
|
chart?.clear();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,158 +0,0 @@
|
|||||||
import { computed, effectScope, onScopeDispose, ref, watch } from 'vue';
|
|
||||||
import { useElementSize } from '@vueuse/core';
|
|
||||||
import VChart, { registerLiquidChart } from '@visactor/vchart';
|
|
||||||
import type { ISpec, ITheme } from '@visactor/vchart';
|
|
||||||
import light from '@visactor/vchart-theme/public/light.json';
|
|
||||||
import dark from '@visactor/vchart-theme/public/dark.json';
|
|
||||||
import { useThemeStore } from '@/store/modules/theme';
|
|
||||||
|
|
||||||
registerLiquidChart();
|
|
||||||
|
|
||||||
// register the theme
|
|
||||||
VChart.ThemeManager.registerTheme('light', light as ITheme);
|
|
||||||
VChart.ThemeManager.registerTheme('dark', dark as ITheme);
|
|
||||||
|
|
||||||
interface ChartHooks {
|
|
||||||
onRender?: (chart: VChart) => void | Promise<void>;
|
|
||||||
onUpdated?: (chart: VChart) => void | Promise<void>;
|
|
||||||
onDestroy?: (chart: VChart) => void | Promise<void>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useVChart<T extends ISpec>(specFactory: () => T, hooks: ChartHooks = {}) {
|
|
||||||
const scope = effectScope();
|
|
||||||
const themeStore = useThemeStore();
|
|
||||||
const darkMode = computed(() => themeStore.darkMode);
|
|
||||||
|
|
||||||
const domRef = ref<HTMLElement | null>(null);
|
|
||||||
const initialSize = { width: 0, height: 0 };
|
|
||||||
const { width, height } = useElementSize(domRef, initialSize);
|
|
||||||
|
|
||||||
let chart: VChart | null = null;
|
|
||||||
const spec: T = specFactory();
|
|
||||||
|
|
||||||
const { onRender, onUpdated, onDestroy } = hooks;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* whether can render chart
|
|
||||||
*
|
|
||||||
* when domRef is ready and initialSize is valid
|
|
||||||
*/
|
|
||||||
function canRender() {
|
|
||||||
return domRef.value && initialSize.width > 0 && initialSize.height > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** is chart rendered */
|
|
||||||
function isRendered() {
|
|
||||||
return Boolean(domRef.value && chart);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* update chart spec
|
|
||||||
*
|
|
||||||
* @param callback callback function
|
|
||||||
*/
|
|
||||||
async function updateSpec(callback: (opts: T, optsFactory: () => T) => ISpec = () => spec) {
|
|
||||||
if (!isRendered()) return;
|
|
||||||
|
|
||||||
const updatedOpts = callback(spec, specFactory);
|
|
||||||
|
|
||||||
Object.assign(spec, updatedOpts);
|
|
||||||
|
|
||||||
// if (isRendered()) {
|
|
||||||
// chart?.release();
|
|
||||||
// }
|
|
||||||
|
|
||||||
chart?.updateSpec({ ...updatedOpts }, true);
|
|
||||||
|
|
||||||
await onUpdated?.(chart!);
|
|
||||||
}
|
|
||||||
|
|
||||||
function setSpec(newSpec: T) {
|
|
||||||
chart?.updateSpec(newSpec);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** render chart */
|
|
||||||
async function render() {
|
|
||||||
if (!isRendered()) {
|
|
||||||
// apply the theme
|
|
||||||
if (darkMode.value) {
|
|
||||||
VChart.ThemeManager.setCurrentTheme('dark');
|
|
||||||
} else {
|
|
||||||
VChart.ThemeManager.setCurrentTheme('light');
|
|
||||||
}
|
|
||||||
|
|
||||||
chart = new VChart(spec, { dom: domRef.value as HTMLElement });
|
|
||||||
chart.renderSync();
|
|
||||||
|
|
||||||
await onRender?.(chart);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** resize chart */
|
|
||||||
function resize() {
|
|
||||||
// chart?.resize();
|
|
||||||
}
|
|
||||||
|
|
||||||
/** destroy chart */
|
|
||||||
async function destroy() {
|
|
||||||
if (!chart) return;
|
|
||||||
|
|
||||||
await onDestroy?.(chart);
|
|
||||||
chart?.release();
|
|
||||||
chart = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** change chart theme */
|
|
||||||
async function changeTheme() {
|
|
||||||
await destroy();
|
|
||||||
await render();
|
|
||||||
await onUpdated?.(chart!);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* render chart by size
|
|
||||||
*
|
|
||||||
* @param w width
|
|
||||||
* @param h height
|
|
||||||
*/
|
|
||||||
async function renderChartBySize(w: number, h: number) {
|
|
||||||
initialSize.width = w;
|
|
||||||
initialSize.height = h;
|
|
||||||
|
|
||||||
// size is abnormal, destroy chart
|
|
||||||
if (!canRender()) {
|
|
||||||
await destroy();
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// resize chart
|
|
||||||
if (isRendered()) {
|
|
||||||
resize();
|
|
||||||
}
|
|
||||||
|
|
||||||
// render chart
|
|
||||||
await render();
|
|
||||||
}
|
|
||||||
|
|
||||||
scope.run(() => {
|
|
||||||
watch([width, height], ([newWidth, newHeight]) => {
|
|
||||||
renderChartBySize(newWidth, newHeight);
|
|
||||||
});
|
|
||||||
|
|
||||||
watch(darkMode, () => {
|
|
||||||
changeTheme();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
onScopeDispose(() => {
|
|
||||||
destroy();
|
|
||||||
scope.stop();
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
domRef,
|
|
||||||
updateSpec,
|
|
||||||
setSpec
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,76 +1,133 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref, watch } from 'vue';
|
import { computed, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue';
|
||||||
import { useInfiniteScroll } from '@vueuse/core';
|
import { useDebounceFn, useInfiniteScroll } from '@vueuse/core';
|
||||||
|
import { NOTIFY_MESSAGE_LEVEL_DICT_CODE } from '@/constants/dict';
|
||||||
|
import {
|
||||||
|
fetchGetMyNotifyMessagePage,
|
||||||
|
fetchGetUnreadNotifyCount,
|
||||||
|
fetchUpdateAllNotifyMessageRead,
|
||||||
|
fetchUpdateNotifyMessageRead
|
||||||
|
} from '@/service/api';
|
||||||
|
import { useDictStore } from '@/store/modules/dict';
|
||||||
|
import { formatDateTime, formatRelativeTime } from '@/utils/datetime';
|
||||||
|
|
||||||
defineOptions({ name: 'NotificationBell' });
|
defineOptions({ name: 'NotificationBell' });
|
||||||
|
|
||||||
interface NotificationItem {
|
const dictStore = useDictStore();
|
||||||
id: string;
|
|
||||||
title: string;
|
|
||||||
timeLabel: string;
|
|
||||||
unread: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
const PAGE_SIZE = 10;
|
const PAGE_SIZE = 10;
|
||||||
|
const UNREAD_COUNT_POLL_INTERVAL = 15 * 1000;
|
||||||
|
|
||||||
// 通知 mock:扩到 60 条以演示分页 / 搜索;等真接口落地后整体迁移
|
type TabKey = 'unread' | 'read';
|
||||||
function buildMockNotifications(): NotificationItem[] {
|
|
||||||
const titles = [
|
interface MessageListState {
|
||||||
'你被指派为执行「迭代 24.06」负责人',
|
items: Api.NotifyMessage.NotifyMessage[];
|
||||||
'任务「SSO 改造」状态变更:开发中 → 待验收',
|
pageNo: number;
|
||||||
'需求「多币种支持」评审通过',
|
total: number;
|
||||||
'工单 #1042 已分派给你',
|
loading: boolean;
|
||||||
'需求「订单导出」被退回,请补充材料',
|
/** 是否已按当前关键字拉过第一页(tab 懒加载 / 失效重拉用) */
|
||||||
'@ 你的评论已被回复',
|
loaded: boolean;
|
||||||
'项目「客户中心 2.0」周报已生成',
|
/** 竞态令牌:重置后递增,过期响应直接丢弃 */
|
||||||
'工单 #1098 客户回复待处理',
|
token: number;
|
||||||
'执行「迭代 24.05」已结束',
|
|
||||||
'需求「批量审批」分配给你'
|
|
||||||
];
|
|
||||||
const times = ['10min 前', '30min 前', '1h 前', '2h 前', '4h 前', '昨日', '前天', '3 天前', '1 周前', '2 周前'];
|
|
||||||
return Array.from({ length: 60 }, (_, i) => ({
|
|
||||||
id: `m${i + 1}`,
|
|
||||||
title: `${titles[i % titles.length]}(#${i + 1})`,
|
|
||||||
timeLabel: times[Math.floor(i / 6) % times.length],
|
|
||||||
unread: i < 14
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const notifications = ref<NotificationItem[]>(buildMockNotifications());
|
function createListState(): MessageListState {
|
||||||
|
return { items: [], pageNo: 1, total: 0, loading: false, loaded: false, token: 0 };
|
||||||
|
}
|
||||||
|
|
||||||
const unreadAll = computed(() => notifications.value.filter(n => n.unread));
|
const listStates = reactive<Record<TabKey, MessageListState>>({
|
||||||
const readAll = computed(() => notifications.value.filter(n => !n.unread));
|
unread: createListState(),
|
||||||
const unreadCount = computed(() => unreadAll.value.length);
|
read: createListState()
|
||||||
|
});
|
||||||
|
|
||||||
|
const unreadCount = ref(0);
|
||||||
const badgeLabel = computed(() => (unreadCount.value > 99 ? '99+' : String(unreadCount.value)));
|
const badgeLabel = computed(() => (unreadCount.value > 99 ? '99+' : String(unreadCount.value)));
|
||||||
|
|
||||||
const drawerOpen = ref(false);
|
const drawerOpen = ref(false);
|
||||||
const activeTab = ref<'unread' | 'read'>('unread');
|
const activeTab = ref<TabKey>('unread');
|
||||||
const searchKeyword = ref('');
|
const searchKeyword = ref('');
|
||||||
|
|
||||||
function matchesKeyword(item: NotificationItem) {
|
const detailVisible = ref(false);
|
||||||
const kw = searchKeyword.value.trim();
|
const detailMessage = ref<Api.NotifyMessage.NotifyMessage | null>(null);
|
||||||
if (!kw) return true;
|
|
||||||
return item.title.toLowerCase().includes(kw.toLowerCase());
|
function keywordParam() {
|
||||||
|
return searchKeyword.value.trim() || undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const filteredUnread = computed(() => unreadAll.value.filter(matchesKeyword));
|
/** 列表圆点颜色:跟随消息等级(与等级徽标同一字典色源);取不到时回 undefined,由 CSS 兜底 */
|
||||||
const filteredRead = computed(() => readAll.value.filter(matchesKeyword));
|
function levelDotColor(level: number) {
|
||||||
|
return dictStore.getDictItem(NOTIFY_MESSAGE_LEVEL_DICT_CODE, level)?.colorType ?? undefined;
|
||||||
|
}
|
||||||
|
|
||||||
const unreadPageSize = ref(PAGE_SIZE);
|
async function refreshUnreadCount() {
|
||||||
const readPageSize = ref(PAGE_SIZE);
|
const { data, error } = await fetchGetUnreadNotifyCount();
|
||||||
|
if (!error && typeof data === 'number') {
|
||||||
|
unreadCount.value = data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const visibleUnread = computed(() => filteredUnread.value.slice(0, unreadPageSize.value));
|
function resetList(tab: TabKey) {
|
||||||
const visibleRead = computed(() => filteredRead.value.slice(0, readPageSize.value));
|
const state = listStates[tab];
|
||||||
|
state.token += 1;
|
||||||
|
state.items = [];
|
||||||
|
state.pageNo = 1;
|
||||||
|
state.total = 0;
|
||||||
|
state.loading = false;
|
||||||
|
state.loaded = false;
|
||||||
|
}
|
||||||
|
|
||||||
const hasMoreUnread = computed(() => unreadPageSize.value < filteredUnread.value.length);
|
async function loadPage(tab: TabKey) {
|
||||||
const hasMoreRead = computed(() => readPageSize.value < filteredRead.value.length);
|
const state = listStates[tab];
|
||||||
|
if (state.loading) return;
|
||||||
|
|
||||||
|
const token = state.token;
|
||||||
|
state.loading = true;
|
||||||
|
|
||||||
|
const { data, error } = await fetchGetMyNotifyMessagePage({
|
||||||
|
pageNo: state.pageNo,
|
||||||
|
pageSize: PAGE_SIZE,
|
||||||
|
readStatus: tab === 'read',
|
||||||
|
keyword: keywordParam()
|
||||||
|
});
|
||||||
|
|
||||||
|
if (token !== state.token) return;
|
||||||
|
|
||||||
|
state.loading = false;
|
||||||
|
state.loaded = true;
|
||||||
|
|
||||||
|
if (error || !data) return;
|
||||||
|
|
||||||
|
state.items.push(...data.list);
|
||||||
|
state.total = data.total;
|
||||||
|
state.pageNo += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasMore(tab: TabKey) {
|
||||||
|
const state = listStates[tab];
|
||||||
|
return state.loaded && state.items.length < state.total;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureLoaded(tab: TabKey) {
|
||||||
|
const state = listStates[tab];
|
||||||
|
if (!state.loaded && !state.loading) {
|
||||||
|
loadPage(tab);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const applyKeywordSearch = useDebounceFn(() => {
|
||||||
|
if (!drawerOpen.value) return;
|
||||||
|
resetList('unread');
|
||||||
|
resetList('read');
|
||||||
|
loadPage(activeTab.value);
|
||||||
|
}, 300);
|
||||||
|
|
||||||
watch(searchKeyword, () => {
|
watch(searchKeyword, () => {
|
||||||
unreadPageSize.value = PAGE_SIZE;
|
applyKeywordSearch();
|
||||||
readPageSize.value = PAGE_SIZE;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// 已读列表数量会因"标已读"动态增长 / 未读会缩小;切换 tab 不重置已展示页数,体感更自然
|
watch(activeTab, tab => {
|
||||||
|
ensureLoaded(tab);
|
||||||
|
});
|
||||||
|
|
||||||
type ScrollbarRefValue = { wrapRef?: HTMLElement } | null;
|
type ScrollbarRefValue = { wrapRef?: HTMLElement } | null;
|
||||||
const unreadScrollbar = ref<ScrollbarRefValue>(null);
|
const unreadScrollbar = ref<ScrollbarRefValue>(null);
|
||||||
@@ -79,7 +136,9 @@ const readScrollbar = ref<ScrollbarRefValue>(null);
|
|||||||
useInfiniteScroll(
|
useInfiniteScroll(
|
||||||
() => unreadScrollbar.value?.wrapRef,
|
() => unreadScrollbar.value?.wrapRef,
|
||||||
() => {
|
() => {
|
||||||
if (hasMoreUnread.value) unreadPageSize.value += PAGE_SIZE;
|
if (drawerOpen.value && hasMore('unread') && !listStates.unread.loading) {
|
||||||
|
loadPage('unread');
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{ distance: 48 }
|
{ distance: 48 }
|
||||||
);
|
);
|
||||||
@@ -87,43 +146,90 @@ useInfiniteScroll(
|
|||||||
useInfiniteScroll(
|
useInfiniteScroll(
|
||||||
() => readScrollbar.value?.wrapRef,
|
() => readScrollbar.value?.wrapRef,
|
||||||
() => {
|
() => {
|
||||||
if (hasMoreRead.value) readPageSize.value += PAGE_SIZE;
|
if (drawerOpen.value && hasMore('read') && !listStates.read.loading) {
|
||||||
|
loadPage('read');
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{ distance: 48 }
|
{ distance: 48 }
|
||||||
);
|
);
|
||||||
|
|
||||||
function openDrawer() {
|
function openDrawer() {
|
||||||
drawerOpen.value = true;
|
drawerOpen.value = true;
|
||||||
|
// 每次打开面板都从第 1 页重拉(与后端对齐的消费口径)
|
||||||
|
resetList('unread');
|
||||||
|
resetList('read');
|
||||||
|
loadPage(activeTab.value);
|
||||||
|
refreshUnreadCount();
|
||||||
}
|
}
|
||||||
|
|
||||||
function closeDrawer() {
|
function closeDrawer() {
|
||||||
drawerOpen.value = false;
|
drawerOpen.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function markRead(item: NotificationItem) {
|
|
||||||
if (!item.unread) return;
|
|
||||||
item.unread = false;
|
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
console.warn('[notification] mark-read', item.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
function markAllRead() {
|
|
||||||
notifications.value.forEach(item => {
|
|
||||||
item.unread = false;
|
|
||||||
});
|
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
console.warn('[notification] mark-all-read');
|
|
||||||
}
|
|
||||||
|
|
||||||
function openItem(item: NotificationItem) {
|
|
||||||
markRead(item);
|
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
console.warn('[notification] open', item.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
function onDrawerClosed() {
|
function onDrawerClosed() {
|
||||||
searchKeyword.value = '';
|
searchKeyword.value = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function markRead(item: Api.NotifyMessage.NotifyMessage) {
|
||||||
|
const { error } = await fetchUpdateNotifyMessageRead([item.id]);
|
||||||
|
if (error) return;
|
||||||
|
|
||||||
|
// 本地移除、不按原页号回拉,避免未读集合收缩导致的分页漂移
|
||||||
|
const state = listStates.unread;
|
||||||
|
const index = state.items.findIndex(row => row.id === item.id);
|
||||||
|
if (index >= 0) {
|
||||||
|
state.items.splice(index, 1);
|
||||||
|
state.total = Math.max(0, state.total - 1);
|
||||||
|
}
|
||||||
|
unreadCount.value = Math.max(0, unreadCount.value - 1);
|
||||||
|
|
||||||
|
// 已读列表失效,下次进入已读 tab 时从第 1 页重拉
|
||||||
|
resetList('read');
|
||||||
|
|
||||||
|
// 移除后剩余条目不足一页且还有更多时补拉,防止列表不再触发滚动加载
|
||||||
|
if (state.items.length < PAGE_SIZE && hasMore('unread')) {
|
||||||
|
loadPage('unread');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function openDetail(row: Api.NotifyMessage.NotifyMessage) {
|
||||||
|
// 弹框持有该行引用,正文不随未读列表移除而消失
|
||||||
|
detailMessage.value = row;
|
||||||
|
detailVisible.value = true;
|
||||||
|
// 未读消息「打开即已读」:后台静默标记,避免"看一半就跑到已读"
|
||||||
|
if (!row.readStatus) {
|
||||||
|
markRead(row);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function markAllRead() {
|
||||||
|
const { error } = await fetchUpdateAllNotifyMessageRead();
|
||||||
|
if (error) return;
|
||||||
|
|
||||||
|
unreadCount.value = 0;
|
||||||
|
resetList('unread');
|
||||||
|
resetList('read');
|
||||||
|
loadPage(activeTab.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
let pollTimer: ReturnType<typeof setInterval> | null = null;
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
// 等级徽标颜色/文案走字典:若未在登录缓存内则按编码补拉一次(已缓存时不发请求)
|
||||||
|
dictStore.ensureDictData(NOTIFY_MESSAGE_LEVEL_DICT_CODE);
|
||||||
|
refreshUnreadCount();
|
||||||
|
pollTimer = setInterval(() => {
|
||||||
|
if (document.hidden) return;
|
||||||
|
refreshUnreadCount();
|
||||||
|
}, UNREAD_COUNT_POLL_INTERVAL);
|
||||||
|
});
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
if (pollTimer) {
|
||||||
|
clearInterval(pollTimer);
|
||||||
|
pollTimer = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -137,21 +243,18 @@ function onDrawerClosed() {
|
|||||||
<span v-if="unreadCount > 0" class="notification-bell__badge">{{ badgeLabel }}</span>
|
<span v-if="unreadCount > 0" class="notification-bell__badge">{{ badgeLabel }}</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<ElDrawer v-model="drawerOpen" size="480px" :with-header="false" @closed="onDrawerClosed">
|
<ElDrawer v-model="drawerOpen" size="480px" @closed="onDrawerClosed">
|
||||||
<div class="notification-bell__panel">
|
<template #header>
|
||||||
<header class="notification-bell__header">
|
<div class="notification-bell__header-main">
|
||||||
<span class="notification-bell__title">
|
<span class="notification-bell__title">
|
||||||
通知
|
通知
|
||||||
<span v-if="unreadCount > 0" class="notification-bell__title-count">未读 {{ unreadCount }}</span>
|
<span v-if="unreadCount > 0" class="notification-bell__title-count">未读 {{ unreadCount }}</span>
|
||||||
</span>
|
</span>
|
||||||
<span class="notification-bell__header-actions">
|
<ElButton v-if="unreadCount > 0" link size="small" @click="markAllRead">全部已读</ElButton>
|
||||||
<ElButton v-if="unreadCount > 0" link size="small" @click="markAllRead">全部已读</ElButton>
|
</div>
|
||||||
<button class="notification-bell__close" type="button" aria-label="关闭" @click="closeDrawer">
|
</template>
|
||||||
<SvgIcon icon="mdi:close" />
|
|
||||||
</button>
|
|
||||||
</span>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
|
<div class="notification-bell__panel">
|
||||||
<div class="notification-bell__search">
|
<div class="notification-bell__search">
|
||||||
<ElInput v-model="searchKeyword" placeholder="搜索通知" clearable>
|
<ElInput v-model="searchKeyword" placeholder="搜索通知" clearable>
|
||||||
<template #prefix>
|
<template #prefix>
|
||||||
@@ -165,61 +268,95 @@ function onDrawerClosed() {
|
|||||||
<template #label>
|
<template #label>
|
||||||
<span class="notification-bell__tab-label">
|
<span class="notification-bell__tab-label">
|
||||||
未读
|
未读
|
||||||
<span class="notification-bell__tab-count">{{ filteredUnread.length }}</span>
|
<span class="notification-bell__tab-count">{{ listStates.unread.total }}</span>
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
<ElScrollbar ref="unreadScrollbar" class="notification-bell__scroll">
|
<ElScrollbar ref="unreadScrollbar" class="notification-bell__scroll">
|
||||||
<ul v-if="visibleUnread.length > 0" class="notification-bell__list">
|
<ul v-if="listStates.unread.items.length > 0" class="notification-bell__list">
|
||||||
<li
|
<li
|
||||||
v-for="row in visibleUnread"
|
v-for="row in listStates.unread.items"
|
||||||
:key="row.id"
|
:key="row.id"
|
||||||
class="notification-bell__row is-unread"
|
class="notification-bell__row is-unread"
|
||||||
@click="openItem(row)"
|
@click="openDetail(row)"
|
||||||
>
|
>
|
||||||
<span class="notification-bell__row-dot" />
|
<span class="notification-bell__row-dot" :style="{ backgroundColor: levelDotColor(row.level) }" />
|
||||||
<div class="notification-bell__row-body">
|
<div class="notification-bell__row-body">
|
||||||
<div class="notification-bell__row-title">{{ row.title }}</div>
|
<div class="notification-bell__row-title">{{ row.templateContent }}</div>
|
||||||
<div class="notification-bell__row-time">{{ row.timeLabel }}</div>
|
<div class="notification-bell__row-meta">
|
||||||
|
<DictTag :dict-code="NOTIFY_MESSAGE_LEVEL_DICT_CODE" :value="row.level" size="small" round />
|
||||||
|
<span class="notification-bell__row-time">{{ formatRelativeTime(row.createTime) }}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<div v-else class="notification-bell__empty">
|
<div v-else class="notification-bell__empty">
|
||||||
{{ searchKeyword ? '没有匹配的通知' : '暂无未读通知' }}
|
{{ listStates.unread.loading ? '加载中…' : searchKeyword ? '没有匹配的通知' : '暂无未读通知' }}
|
||||||
</div>
|
</div>
|
||||||
<div v-if="visibleUnread.length > 0" class="notification-bell__footer-hint">
|
<div v-if="listStates.unread.items.length > 0" class="notification-bell__footer-hint">
|
||||||
{{ hasMoreUnread ? '滚动加载更多…' : '— 已经到底了 —' }}
|
{{ listStates.unread.loading ? '加载中…' : hasMore('unread') ? '滚动加载更多…' : '— 已经到底了 —' }}
|
||||||
</div>
|
</div>
|
||||||
</ElScrollbar>
|
</ElScrollbar>
|
||||||
</ElTabPane>
|
</ElTabPane>
|
||||||
|
|
||||||
<ElTabPane name="read">
|
<ElTabPane name="read">
|
||||||
<template #label>
|
<template #label>
|
||||||
<span class="notification-bell__tab-label">
|
<span class="notification-bell__tab-label">已读</span>
|
||||||
已读
|
|
||||||
<span class="notification-bell__tab-count">{{ filteredRead.length }}</span>
|
|
||||||
</span>
|
|
||||||
</template>
|
</template>
|
||||||
<ElScrollbar ref="readScrollbar" class="notification-bell__scroll">
|
<ElScrollbar ref="readScrollbar" class="notification-bell__scroll">
|
||||||
<ul v-if="visibleRead.length > 0" class="notification-bell__list">
|
<ul v-if="listStates.read.items.length > 0" class="notification-bell__list">
|
||||||
<li v-for="row in visibleRead" :key="row.id" class="notification-bell__row" @click="openItem(row)">
|
<li
|
||||||
<span class="notification-bell__row-dot" />
|
v-for="row in listStates.read.items"
|
||||||
|
:key="row.id"
|
||||||
|
class="notification-bell__row"
|
||||||
|
@click="openDetail(row)"
|
||||||
|
>
|
||||||
|
<span class="notification-bell__row-dot" :style="{ backgroundColor: levelDotColor(row.level) }" />
|
||||||
<div class="notification-bell__row-body">
|
<div class="notification-bell__row-body">
|
||||||
<div class="notification-bell__row-title">{{ row.title }}</div>
|
<div class="notification-bell__row-title">{{ row.templateContent }}</div>
|
||||||
<div class="notification-bell__row-time">{{ row.timeLabel }}</div>
|
<div class="notification-bell__row-meta">
|
||||||
|
<DictTag :dict-code="NOTIFY_MESSAGE_LEVEL_DICT_CODE" :value="row.level" size="small" round />
|
||||||
|
<span class="notification-bell__row-time">{{ formatRelativeTime(row.createTime) }}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<div v-else class="notification-bell__empty">
|
<div v-else class="notification-bell__empty">
|
||||||
{{ searchKeyword ? '没有匹配的通知' : '暂无已读通知' }}
|
{{ listStates.read.loading ? '加载中…' : searchKeyword ? '没有匹配的通知' : '暂无已读通知' }}
|
||||||
</div>
|
</div>
|
||||||
<div v-if="visibleRead.length > 0" class="notification-bell__footer-hint">
|
<div v-if="listStates.read.items.length > 0" class="notification-bell__footer-hint">
|
||||||
{{ hasMoreRead ? '滚动加载更多…' : '— 已经到底了 —' }}
|
{{ listStates.read.loading ? '加载中…' : hasMore('read') ? '滚动加载更多…' : '— 已经到底了 —' }}
|
||||||
</div>
|
</div>
|
||||||
</ElScrollbar>
|
</ElScrollbar>
|
||||||
</ElTabPane>
|
</ElTabPane>
|
||||||
</ElTabs>
|
</ElTabs>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<ElButton @click="closeDrawer">关闭</ElButton>
|
||||||
|
</template>
|
||||||
</ElDrawer>
|
</ElDrawer>
|
||||||
|
|
||||||
|
<ElDialog v-model="detailVisible" width="520px" align-center class="notification-bell__detail">
|
||||||
|
<template #header>
|
||||||
|
<div class="notification-bell__detail-head">
|
||||||
|
<span class="notification-bell__detail-sender">{{ detailMessage?.templateNickname || '系统通知' }}</span>
|
||||||
|
<DictTag
|
||||||
|
v-if="detailMessage"
|
||||||
|
:dict-code="NOTIFY_MESSAGE_LEVEL_DICT_CODE"
|
||||||
|
:value="detailMessage.level"
|
||||||
|
size="small"
|
||||||
|
round
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div v-if="detailMessage" class="notification-bell__detail-body">
|
||||||
|
<div class="notification-bell__detail-content">{{ detailMessage.templateContent }}</div>
|
||||||
|
<div class="notification-bell__detail-time">收到于 {{ formatDateTime(detailMessage.createTime) }}</div>
|
||||||
|
</div>
|
||||||
|
<template #footer>
|
||||||
|
<ElButton @click="detailVisible = false">关闭</ElButton>
|
||||||
|
</template>
|
||||||
|
</ElDialog>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@@ -258,18 +395,53 @@ function onDrawerClosed() {
|
|||||||
|
|
||||||
.notification-bell__badge {
|
.notification-bell__badge {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 4px;
|
top: 2px;
|
||||||
right: 4px;
|
right: 2px;
|
||||||
min-width: 16px;
|
min-width: 18px;
|
||||||
height: 16px;
|
height: 18px;
|
||||||
padding: 0 4px;
|
padding: 0 5px;
|
||||||
|
border: 1px solid #fff;
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
background-color: var(--el-color-danger);
|
background-color: var(--el-color-danger);
|
||||||
color: #fff;
|
color: #fff;
|
||||||
font-size: 10px;
|
font-size: 11px;
|
||||||
font-weight: 600;
|
font-weight: 700;
|
||||||
line-height: 16px;
|
line-height: 16px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
animation: notification-badge-pulse 1.6s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 扩散波纹:跟随心跳节奏向外晕开,增强未读提醒的醒目度 */
|
||||||
|
.notification-bell__badge::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
inset: -1px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background-color: var(--el-color-danger);
|
||||||
|
animation: notification-badge-ping 1.6s ease-out infinite;
|
||||||
|
z-index: -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes notification-badge-pulse {
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: scale(1.18);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes notification-badge-ping {
|
||||||
|
0% {
|
||||||
|
transform: scale(1);
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
70%,
|
||||||
|
100% {
|
||||||
|
transform: scale(1.9);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.notification-bell__panel {
|
.notification-bell__panel {
|
||||||
@@ -278,13 +450,14 @@ function onDrawerClosed() {
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.notification-bell__header {
|
.notification-bell__header-main {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
padding-bottom: 12px;
|
min-width: 0;
|
||||||
border-bottom: 1px solid var(--el-border-color-lighter);
|
margin-right: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.notification-bell__title {
|
.notification-bell__title {
|
||||||
@@ -305,37 +478,8 @@ function onDrawerClosed() {
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.notification-bell__header-actions {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.notification-bell__close {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
width: 28px;
|
|
||||||
height: 28px;
|
|
||||||
padding: 0;
|
|
||||||
border: none;
|
|
||||||
border-radius: 6px;
|
|
||||||
background-color: transparent;
|
|
||||||
color: var(--el-text-color-secondary);
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 18px;
|
|
||||||
transition:
|
|
||||||
background-color 120ms ease,
|
|
||||||
color 120ms ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.notification-bell__close:hover {
|
|
||||||
background-color: var(--el-fill-color-light);
|
|
||||||
color: var(--el-text-color-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.notification-bell__search {
|
.notification-bell__search {
|
||||||
padding: 12px 0 4px;
|
padding: 0 0 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.notification-bell__tabs {
|
.notification-bell__tabs {
|
||||||
@@ -393,8 +537,8 @@ function onDrawerClosed() {
|
|||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
padding: 12px 4px;
|
padding: 12px 4px;
|
||||||
cursor: pointer;
|
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
transition: background-color 120ms ease;
|
transition: background-color 120ms ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -434,8 +578,14 @@ function onDrawerClosed() {
|
|||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.notification-bell__row-meta {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
.notification-bell__row-time {
|
.notification-bell__row-time {
|
||||||
margin-top: 4px;
|
|
||||||
color: var(--el-text-color-secondary);
|
color: var(--el-text-color-secondary);
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
@@ -454,4 +604,41 @@ function onDrawerClosed() {
|
|||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.notification-bell__detail-body {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-bell__detail-head {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
padding-right: 8px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-bell__detail-sender {
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
color: var(--el-text-color-primary);
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-bell__detail-content {
|
||||||
|
color: var(--el-text-color-regular);
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.7;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-bell__detail-time {
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -41,11 +41,6 @@ const { isFullscreen, toggle } = useFullscreen();
|
|||||||
<div>
|
<div>
|
||||||
<FullScreen v-if="!appStore.isMobile" :full="isFullscreen" @click="toggle" />
|
<FullScreen v-if="!appStore.isMobile" :full="isFullscreen" @click="toggle" />
|
||||||
</div>
|
</div>
|
||||||
<ThemeSchemaSwitch
|
|
||||||
:theme-schema="themeStore.themeScheme"
|
|
||||||
:is-dark="themeStore.darkMode"
|
|
||||||
@switch="themeStore.toggleThemeScheme"
|
|
||||||
/>
|
|
||||||
<div>
|
<div>
|
||||||
<ThemeButton />
|
<ThemeButton />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ const { selectedKeyDummy, handleSelect } = useMenu();
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Teleport :to="`#${GLOBAL_HEADER_MENU_ID}`">
|
<Teleport defer :to="`#${GLOBAL_HEADER_MENU_ID}`">
|
||||||
<ElMenu
|
<ElMenu
|
||||||
ellipsis
|
ellipsis
|
||||||
class="w-full"
|
class="w-full"
|
||||||
|
|||||||
@@ -93,7 +93,8 @@ function isMenuActive(menu: App.Global.Menu | App.ObjectContext.Menu): boolean {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Teleport :to="`#${GLOBAL_HEADER_MENU_ID}`">
|
<!-- defer:BaseLayout 二次挂载时 GlobalMenu 已缓存为同步挂载,目标 div 还未插入 document,不延迟解析会静默失败且不重试 -->
|
||||||
|
<Teleport defer :to="`#${GLOBAL_HEADER_MENU_ID}`">
|
||||||
<div class="mix-header-nav size-full min-w-0 flex-y-center">
|
<div class="mix-header-nav size-full min-w-0 flex-y-center">
|
||||||
<button
|
<button
|
||||||
v-if="activeFirstLevelMenu"
|
v-if="activeFirstLevelMenu"
|
||||||
@@ -161,7 +162,7 @@ function isMenuActive(menu: App.Global.Menu | App.ObjectContext.Menu): boolean {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Teleport>
|
</Teleport>
|
||||||
<Teleport :to="`#${GLOBAL_SIDER_MENU_ID}`">
|
<Teleport defer :to="`#${GLOBAL_SIDER_MENU_ID}`">
|
||||||
<FirstLevelMenu
|
<FirstLevelMenu
|
||||||
:menus="allMenus"
|
:menus="allMenus"
|
||||||
:active-menu-key="activeFirstLevelMenuKey"
|
:active-menu-key="activeFirstLevelMenuKey"
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ watch(
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Teleport :to="`#${GLOBAL_HEADER_MENU_ID}`">
|
<Teleport defer :to="`#${GLOBAL_HEADER_MENU_ID}`">
|
||||||
<ElMenu
|
<ElMenu
|
||||||
ellipsis
|
ellipsis
|
||||||
class="w-full"
|
class="w-full"
|
||||||
@@ -66,7 +66,7 @@ watch(
|
|||||||
<MenuItem v-for="item in firstLevelMenus" :key="item.key" :item="item" :index="item.key" />
|
<MenuItem v-for="item in firstLevelMenus" :key="item.key" :item="item" :index="item.key" />
|
||||||
</ElMenu>
|
</ElMenu>
|
||||||
</Teleport>
|
</Teleport>
|
||||||
<Teleport :to="`#${GLOBAL_SIDER_MENU_ID}`">
|
<Teleport defer :to="`#${GLOBAL_SIDER_MENU_ID}`">
|
||||||
<SimpleScrollbar>
|
<SimpleScrollbar>
|
||||||
<ElMenu
|
<ElMenu
|
||||||
mode="vertical"
|
mode="vertical"
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ watch(
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Teleport :to="`#${GLOBAL_SIDER_MENU_ID}`">
|
<Teleport defer :to="`#${GLOBAL_SIDER_MENU_ID}`">
|
||||||
<SimpleScrollbar>
|
<SimpleScrollbar>
|
||||||
<ElMenu
|
<ElMenu
|
||||||
mode="vertical"
|
mode="vertical"
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ watch(
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Teleport :to="`#${GLOBAL_SIDER_MENU_ID}`">
|
<Teleport defer :to="`#${GLOBAL_SIDER_MENU_ID}`">
|
||||||
<div class="h-full flex" @mouseleave="handleResetActiveMenu">
|
<div class="h-full flex" @mouseleave="handleResetActiveMenu">
|
||||||
<FirstLevelMenu
|
<FirstLevelMenu
|
||||||
:menus="allMenus"
|
:menus="allMenus"
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
import { themeSchemaRecord } from '@/constants/app';
|
|
||||||
import { useThemeStore } from '@/store/modules/theme';
|
import { useThemeStore } from '@/store/modules/theme';
|
||||||
import { $t } from '@/locales';
|
import { $t } from '@/locales';
|
||||||
import SettingItem from '../components/setting-item.vue';
|
import SettingItem from '../components/setting-item.vue';
|
||||||
@@ -9,16 +8,6 @@ defineOptions({ name: 'DarkMode' });
|
|||||||
|
|
||||||
const themeStore = useThemeStore();
|
const themeStore = useThemeStore();
|
||||||
|
|
||||||
const icons: Record<UnionKey.ThemeScheme, string> = {
|
|
||||||
light: 'material-symbols:sunny',
|
|
||||||
dark: 'material-symbols:nightlight-rounded',
|
|
||||||
auto: 'material-symbols:hdr-auto'
|
|
||||||
};
|
|
||||||
|
|
||||||
function handleSegmentChange(value: string | number) {
|
|
||||||
themeStore.setThemeScheme(value as UnionKey.ThemeScheme);
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleGrayscaleChange(value: boolean) {
|
function handleGrayscaleChange(value: boolean) {
|
||||||
themeStore.setGrayscale(value);
|
themeStore.setGrayscale(value);
|
||||||
}
|
}
|
||||||
@@ -33,15 +22,6 @@ const showSiderInverted = computed(() => !themeStore.darkMode && themeStore.layo
|
|||||||
<template>
|
<template>
|
||||||
<ElDivider>{{ $t('theme.themeSchema.title') }}</ElDivider>
|
<ElDivider>{{ $t('theme.themeSchema.title') }}</ElDivider>
|
||||||
<div class="flex-col-stretch gap-16px">
|
<div class="flex-col-stretch gap-16px">
|
||||||
<div class="i-flex-center">
|
|
||||||
<ElTabs v-model="themeStore.themeScheme" type="border-card" class="segment" @tab-change="handleSegmentChange">
|
|
||||||
<ElTabPane v-for="(_, key) in themeSchemaRecord" :key="key" :name="key">
|
|
||||||
<template #label>
|
|
||||||
<SvgIcon :icon="icons[key]" class="h-23px text-icon-small" />
|
|
||||||
</template>
|
|
||||||
</ElTabPane>
|
|
||||||
</ElTabs>
|
|
||||||
</div>
|
|
||||||
<Transition name="sider-inverted">
|
<Transition name="sider-inverted">
|
||||||
<SettingItem v-if="showSiderInverted" :label="$t('theme.sider.inverted')">
|
<SettingItem v-if="showSiderInverted" :label="$t('theme.sider.inverted')">
|
||||||
<ElSwitch v-model="themeStore.sider.inverted" />
|
<ElSwitch v-model="themeStore.sider.inverted" />
|
||||||
|
|||||||
@@ -169,8 +169,10 @@ const local: App.I18n.Schema = {
|
|||||||
'personal-center': 'Personal Center',
|
'personal-center': 'Personal Center',
|
||||||
'personal-center_my-profile': 'My Profile',
|
'personal-center_my-profile': 'My Profile',
|
||||||
'personal-center_my-item': 'My Items',
|
'personal-center_my-item': 'My Items',
|
||||||
'personal-center_my-weekly': 'My Weekly Report',
|
'personal-center_work-report': 'Work Report',
|
||||||
'personal-center_my-monthly': 'My Monthly Report',
|
'personal-center_work-report_weekly': 'Weekly Report',
|
||||||
|
'personal-center_work-report_monthly': 'Monthly Report',
|
||||||
|
'personal-center_work-report_project': 'Project Fortnightly 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_overtime-application': 'Overtime Application',
|
||||||
@@ -178,16 +180,6 @@ const local: App.I18n.Schema = {
|
|||||||
infra: 'Infra',
|
infra: 'Infra',
|
||||||
'infra_state-machine': 'State Machine',
|
'infra_state-machine': 'State Machine',
|
||||||
'infra_rd-code': 'R&D Code',
|
'infra_rd-code': 'R&D Code',
|
||||||
function: 'System Function',
|
|
||||||
function_tab: 'Tab',
|
|
||||||
'function_multi-tab': 'Multi Tab',
|
|
||||||
'function_hide-child': 'Hide Child',
|
|
||||||
'function_hide-child_one': 'Hide Child',
|
|
||||||
'function_hide-child_two': 'Two',
|
|
||||||
'function_hide-child_three': 'Three',
|
|
||||||
function_request: 'Request',
|
|
||||||
'function_toggle-auth': 'Toggle Auth',
|
|
||||||
'function_super-page': 'Super Admin Visible',
|
|
||||||
product: 'Product',
|
product: 'Product',
|
||||||
product_list: 'Product List',
|
product_list: 'Product List',
|
||||||
product_dashboard: 'Dashboard',
|
product_dashboard: 'Dashboard',
|
||||||
@@ -211,28 +203,7 @@ const local: App.I18n.Schema = {
|
|||||||
exception: 'Exception',
|
exception: 'Exception',
|
||||||
exception_403: '403',
|
exception_403: '403',
|
||||||
exception_404: '404',
|
exception_404: '404',
|
||||||
exception_500: '500',
|
exception_500: '500'
|
||||||
plugin: 'Plugin',
|
|
||||||
plugin_copy: 'Copy',
|
|
||||||
plugin_charts: 'Charts',
|
|
||||||
plugin_charts_echarts: 'ECharts',
|
|
||||||
plugin_charts_antv: 'AntV',
|
|
||||||
plugin_charts_vchart: 'VChart',
|
|
||||||
plugin_icon: 'Icon',
|
|
||||||
plugin_map: 'Map',
|
|
||||||
plugin_print: 'Print',
|
|
||||||
plugin_swiper: 'Swiper',
|
|
||||||
plugin_video: 'Video',
|
|
||||||
plugin_barcode: 'Barcode',
|
|
||||||
plugin_pinyin: 'pinyin',
|
|
||||||
plugin_excel: 'Excel',
|
|
||||||
plugin_pdf: 'PDF preview',
|
|
||||||
plugin_gantt: 'Gantt Chart',
|
|
||||||
plugin_gantt_dhtmlx: 'dhtmlxGantt',
|
|
||||||
plugin_gantt_vtable: 'VTableGantt',
|
|
||||||
plugin_typeit: 'Typeit',
|
|
||||||
plugin_tables: 'Tables',
|
|
||||||
plugin_tables_vtable: 'VTable'
|
|
||||||
},
|
},
|
||||||
page: {
|
page: {
|
||||||
login: {
|
login: {
|
||||||
@@ -328,45 +299,6 @@ const local: App.I18n.Schema = {
|
|||||||
},
|
},
|
||||||
creativity: 'Creativity'
|
creativity: 'Creativity'
|
||||||
},
|
},
|
||||||
function: {
|
|
||||||
tab: {
|
|
||||||
tabOperate: {
|
|
||||||
title: 'Tab Operation',
|
|
||||||
addTab: 'Add Tab',
|
|
||||||
addTabDesc: 'To user management page',
|
|
||||||
closeTab: 'Close Tab',
|
|
||||||
closeCurrentTab: 'Close Current Tab',
|
|
||||||
closeAboutTab: 'Close "User Management" Tab',
|
|
||||||
addMultiTab: 'Add Multi Tab',
|
|
||||||
addMultiTabDesc1: 'To MultiTab page',
|
|
||||||
addMultiTabDesc2: 'To MultiTab page(with query params)'
|
|
||||||
},
|
|
||||||
tabTitle: {
|
|
||||||
title: 'Tab Title',
|
|
||||||
changeTitle: 'Change Title',
|
|
||||||
change: 'Change',
|
|
||||||
resetTitle: 'Reset Title',
|
|
||||||
reset: 'Reset'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
multiTab: {
|
|
||||||
routeParam: 'Route Param',
|
|
||||||
backTab: 'Back function_tab'
|
|
||||||
},
|
|
||||||
toggleAuth: {
|
|
||||||
toggleAccount: 'Toggle Account',
|
|
||||||
authHook: 'Auth Hook Function `hasAuth`',
|
|
||||||
superAdminVisible: 'Super Admin Visible',
|
|
||||||
adminVisible: 'Admin Visible',
|
|
||||||
adminOrUserVisible: 'Admin and User Visible'
|
|
||||||
},
|
|
||||||
request: {
|
|
||||||
repeatedErrorOccurOnce: 'Repeated Request Error Occurs Once',
|
|
||||||
repeatedError: 'Repeated Request Error',
|
|
||||||
repeatedErrorMsg1: 'Custom Request Error 1',
|
|
||||||
repeatedErrorMsg2: 'Custom Request Error 2'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
system: {
|
system: {
|
||||||
common: {
|
common: {
|
||||||
status: {
|
status: {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
const local: App.I18n.Schema = {
|
const local: App.I18n.Schema = {
|
||||||
system: {
|
system: {
|
||||||
title: '研发内部管理系统'
|
title: '研发管理系统'
|
||||||
},
|
},
|
||||||
common: {
|
common: {
|
||||||
action: '操作',
|
action: '操作',
|
||||||
@@ -169,8 +169,10 @@ const local: App.I18n.Schema = {
|
|||||||
'personal-center': '个人中心',
|
'personal-center': '个人中心',
|
||||||
'personal-center_my-profile': '个人信息',
|
'personal-center_my-profile': '个人信息',
|
||||||
'personal-center_my-item': '我的事项',
|
'personal-center_my-item': '我的事项',
|
||||||
'personal-center_my-weekly': '我的周报',
|
'personal-center_work-report': '工作报告',
|
||||||
'personal-center_my-monthly': '我的月报',
|
'personal-center_work-report_weekly': '个人周报',
|
||||||
|
'personal-center_work-report_monthly': '个人月报',
|
||||||
|
'personal-center_work-report_project': '项目半月报',
|
||||||
'personal-center_my-performance': '我的绩效',
|
'personal-center_my-performance': '我的绩效',
|
||||||
'personal-center_my-application': '我的申请',
|
'personal-center_my-application': '我的申请',
|
||||||
'personal-center_overtime-application': '加班申请',
|
'personal-center_overtime-application': '加班申请',
|
||||||
@@ -178,16 +180,6 @@ const local: App.I18n.Schema = {
|
|||||||
infra: '基础设施',
|
infra: '基础设施',
|
||||||
'infra_state-machine': '状态机管理',
|
'infra_state-machine': '状态机管理',
|
||||||
'infra_rd-code': '研发令号',
|
'infra_rd-code': '研发令号',
|
||||||
function: '系统功能',
|
|
||||||
function_tab: '标签页',
|
|
||||||
'function_multi-tab': '多标签页',
|
|
||||||
'function_hide-child': '隐藏子菜单',
|
|
||||||
'function_hide-child_one': '隐藏子菜单',
|
|
||||||
'function_hide-child_two': '菜单二',
|
|
||||||
'function_hide-child_three': '菜单三',
|
|
||||||
function_request: '请求',
|
|
||||||
'function_toggle-auth': '切换权限',
|
|
||||||
'function_super-page': '超级管理员可见',
|
|
||||||
product: '产品管理',
|
product: '产品管理',
|
||||||
product_list: '产品列表',
|
product_list: '产品列表',
|
||||||
product_dashboard: '产品仪表盘',
|
product_dashboard: '产品仪表盘',
|
||||||
@@ -211,28 +203,7 @@ const local: App.I18n.Schema = {
|
|||||||
exception: '异常页',
|
exception: '异常页',
|
||||||
exception_403: '403',
|
exception_403: '403',
|
||||||
exception_404: '404',
|
exception_404: '404',
|
||||||
exception_500: '500',
|
exception_500: '500'
|
||||||
plugin: '插件示例',
|
|
||||||
plugin_copy: '剪贴板',
|
|
||||||
plugin_charts: '图表',
|
|
||||||
plugin_charts_echarts: 'ECharts',
|
|
||||||
plugin_charts_antv: 'AntV',
|
|
||||||
plugin_charts_vchart: 'VChart',
|
|
||||||
plugin_icon: '图标',
|
|
||||||
plugin_map: '地图',
|
|
||||||
plugin_print: '打印',
|
|
||||||
plugin_swiper: 'Swiper',
|
|
||||||
plugin_video: '视频',
|
|
||||||
plugin_barcode: '条形码',
|
|
||||||
plugin_pinyin: '拼音',
|
|
||||||
plugin_excel: 'Excel',
|
|
||||||
plugin_pdf: 'PDF 预览',
|
|
||||||
plugin_gantt: '甘特图',
|
|
||||||
plugin_gantt_dhtmlx: 'dhtmlxGantt',
|
|
||||||
plugin_gantt_vtable: 'VTableGantt',
|
|
||||||
plugin_typeit: '打字机',
|
|
||||||
plugin_tables: '表格',
|
|
||||||
plugin_tables_vtable: 'VTable'
|
|
||||||
},
|
},
|
||||||
page: {
|
page: {
|
||||||
login: {
|
login: {
|
||||||
@@ -284,7 +255,7 @@ const local: App.I18n.Schema = {
|
|||||||
about: {
|
about: {
|
||||||
title: '关于',
|
title: '关于',
|
||||||
introduction:
|
introduction:
|
||||||
'灿能研发内部管理系统是灿能电力内部使用的研发管理前端系统,用于承载内部业务模块、工程协作流程和日常管理能力。',
|
'灿能研发管理系统是灿能电力内部使用的研发管理前端系统,用于承载内部业务模块、工程协作流程和日常管理能力。',
|
||||||
projectInfo: {
|
projectInfo: {
|
||||||
title: '项目信息',
|
title: '项目信息',
|
||||||
version: '版本',
|
version: '版本',
|
||||||
@@ -327,45 +298,6 @@ const local: App.I18n.Schema = {
|
|||||||
},
|
},
|
||||||
creativity: '创意'
|
creativity: '创意'
|
||||||
},
|
},
|
||||||
function: {
|
|
||||||
tab: {
|
|
||||||
tabOperate: {
|
|
||||||
title: '标签页操作',
|
|
||||||
addTab: '添加标签页',
|
|
||||||
addTabDesc: '跳转到用户管理页面',
|
|
||||||
closeTab: '关闭标签页',
|
|
||||||
closeCurrentTab: '关闭当前标签页',
|
|
||||||
closeAboutTab: '关闭"用户管理"标签页',
|
|
||||||
addMultiTab: '添加多标签页',
|
|
||||||
addMultiTabDesc1: '跳转到多标签页页面',
|
|
||||||
addMultiTabDesc2: '跳转到多标签页页面(带有查询参数)'
|
|
||||||
},
|
|
||||||
tabTitle: {
|
|
||||||
title: '标签页标题',
|
|
||||||
changeTitle: '修改标题',
|
|
||||||
change: '修改',
|
|
||||||
resetTitle: '重置标题',
|
|
||||||
reset: '重置'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
multiTab: {
|
|
||||||
routeParam: '路由参数',
|
|
||||||
backTab: '返回 function_tab'
|
|
||||||
},
|
|
||||||
toggleAuth: {
|
|
||||||
toggleAccount: '切换账号',
|
|
||||||
authHook: '权限钩子函数 `hasAuth`',
|
|
||||||
superAdminVisible: '超级管理员可见',
|
|
||||||
adminVisible: '管理员可见',
|
|
||||||
adminOrUserVisible: '管理员和用户可见'
|
|
||||||
},
|
|
||||||
request: {
|
|
||||||
repeatedErrorOccurOnce: '重复请求错误只出现一次',
|
|
||||||
repeatedError: '重复请求错误',
|
|
||||||
repeatedErrorMsg1: '自定义请求错误 1',
|
|
||||||
repeatedErrorMsg2: '自定义请求错误 2'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
system: {
|
system: {
|
||||||
common: {
|
common: {
|
||||||
status: {
|
status: {
|
||||||
|
|||||||
@@ -3,6 +3,3 @@ import 'element-plus/dist/index.css';
|
|||||||
import 'element-plus/theme-chalk/dark/css-vars.css';
|
import 'element-plus/theme-chalk/dark/css-vars.css';
|
||||||
import 'uno.css';
|
import 'uno.css';
|
||||||
import '../styles/css/global.css';
|
import '../styles/css/global.css';
|
||||||
import 'swiper/css';
|
|
||||||
import 'swiper/css/navigation';
|
|
||||||
import 'swiper/css/pagination';
|
|
||||||
|
|||||||
@@ -20,14 +20,6 @@ export const views: Record<LastLevelRouteKey, RouteComponent | (() => Promise<Ro
|
|||||||
500: () => import("@/views/_builtin/500/index.vue"),
|
500: () => import("@/views/_builtin/500/index.vue"),
|
||||||
"iframe-page": () => import("@/views/_builtin/iframe-page/[url].vue"),
|
"iframe-page": () => import("@/views/_builtin/iframe-page/[url].vue"),
|
||||||
login: () => import("@/views/_builtin/login/index.vue"),
|
login: () => import("@/views/_builtin/login/index.vue"),
|
||||||
"function_hide-child_one": () => import("@/views/function/hide-child/one/index.vue"),
|
|
||||||
"function_hide-child_three": () => import("@/views/function/hide-child/three/index.vue"),
|
|
||||||
"function_hide-child_two": () => import("@/views/function/hide-child/two/index.vue"),
|
|
||||||
"function_multi-tab": () => import("@/views/function/multi-tab/index.vue"),
|
|
||||||
function_request: () => import("@/views/function/request/index.vue"),
|
|
||||||
"function_super-page": () => import("@/views/function/super-page/index.vue"),
|
|
||||||
function_tab: () => import("@/views/function/tab/index.vue"),
|
|
||||||
"function_toggle-auth": () => import("@/views/function/toggle-auth/index.vue"),
|
|
||||||
"infra_rd-code": () => import("@/views/infra/rd-code/index.vue"),
|
"infra_rd-code": () => import("@/views/infra/rd-code/index.vue"),
|
||||||
"infra_state-machine": () => import("@/views/infra/state-machine/index.vue"),
|
"infra_state-machine": () => import("@/views/infra/state-machine/index.vue"),
|
||||||
"metrics_member-efficiency": () => import("@/views/metrics/member-efficiency/index.vue"),
|
"metrics_member-efficiency": () => import("@/views/metrics/member-efficiency/index.vue"),
|
||||||
@@ -35,29 +27,14 @@ export const views: Record<LastLevelRouteKey, RouteComponent | (() => Promise<Ro
|
|||||||
metrics_worktime: () => import("@/views/metrics/worktime/index.vue"),
|
metrics_worktime: () => import("@/views/metrics/worktime/index.vue"),
|
||||||
"personal-center_my-application": () => import("@/views/personal-center/my-application/index.vue"),
|
"personal-center_my-application": () => import("@/views/personal-center/my-application/index.vue"),
|
||||||
"personal-center_my-item": () => import("@/views/personal-center/my-item/index.vue"),
|
"personal-center_my-item": () => import("@/views/personal-center/my-item/index.vue"),
|
||||||
"personal-center_my-monthly": () => import("@/views/personal-center/my-monthly/index.vue"),
|
|
||||||
"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_overtime-application": () => import("@/views/personal-center/overtime-application/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"),
|
"personal-center_work-report": () => import("@/views/personal-center/work-report/index.vue"),
|
||||||
plugin_charts_antv: () => import("@/views/plugin/charts/antv/index.vue"),
|
"personal-center_work-report_monthly": () => import("@/views/personal-center/work-report/monthly/index.vue"),
|
||||||
plugin_charts_echarts: () => import("@/views/plugin/charts/echarts/index.vue"),
|
"personal-center_work-report_project": () => import("@/views/personal-center/work-report/project/index.vue"),
|
||||||
plugin_charts_vchart: () => import("@/views/plugin/charts/vchart/index.vue"),
|
"personal-center_work-report_weekly": () => import("@/views/personal-center/work-report/weekly/index.vue"),
|
||||||
plugin_copy: () => import("@/views/plugin/copy/index.vue"),
|
|
||||||
plugin_excel: () => import("@/views/plugin/excel/index.vue"),
|
|
||||||
plugin_gantt_dhtmlx: () => import("@/views/plugin/gantt/dhtmlx/index.vue"),
|
|
||||||
plugin_gantt_vtable: () => import("@/views/plugin/gantt/vtable/index.vue"),
|
|
||||||
plugin_icon: () => import("@/views/plugin/icon/index.vue"),
|
|
||||||
plugin_map: () => import("@/views/plugin/map/index.vue"),
|
|
||||||
plugin_pdf: () => import("@/views/plugin/pdf/index.vue"),
|
|
||||||
plugin_pinyin: () => import("@/views/plugin/pinyin/index.vue"),
|
|
||||||
plugin_print: () => import("@/views/plugin/print/index.vue"),
|
|
||||||
plugin_swiper: () => import("@/views/plugin/swiper/index.vue"),
|
|
||||||
plugin_tables_vtable: () => import("@/views/plugin/tables/vtable/index.vue"),
|
|
||||||
plugin_typeit: () => import("@/views/plugin/typeit/index.vue"),
|
|
||||||
plugin_video: () => import("@/views/plugin/video/index.vue"),
|
|
||||||
product_dashboard: () => import("@/views/product/dashboard/index.vue"),
|
product_dashboard: () => import("@/views/product/dashboard/index.vue"),
|
||||||
product_list: () => import("@/views/product/list/index.vue"),
|
product_list: () => import("@/views/product/list/index.vue"),
|
||||||
product_requirement: () => import("@/views/product/requirement/index.vue"),
|
product_requirement: () => import("@/views/product/requirement/index.vue"),
|
||||||
|
|||||||
@@ -39,124 +39,6 @@ export const generatedRoutes: GeneratedRoute[] = [
|
|||||||
hideInMenu: true
|
hideInMenu: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: 'function',
|
|
||||||
path: '/function',
|
|
||||||
component: 'layout.base',
|
|
||||||
meta: {
|
|
||||||
title: 'function',
|
|
||||||
i18nKey: 'route.function',
|
|
||||||
icon: 'icon-park-outline:all-application',
|
|
||||||
order: 6
|
|
||||||
},
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
name: 'function_hide-child',
|
|
||||||
path: '/function/hide-child',
|
|
||||||
meta: {
|
|
||||||
title: 'function_hide-child',
|
|
||||||
i18nKey: 'route.function_hide-child',
|
|
||||||
icon: 'material-symbols:filter-list-off',
|
|
||||||
order: 2
|
|
||||||
},
|
|
||||||
redirect: '/function/hide-child/one',
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
name: 'function_hide-child_one',
|
|
||||||
path: '/function/hide-child/one',
|
|
||||||
component: 'view.function_hide-child_one',
|
|
||||||
meta: {
|
|
||||||
title: 'function_hide-child_one',
|
|
||||||
i18nKey: 'route.function_hide-child_one',
|
|
||||||
icon: 'material-symbols:filter-list-off',
|
|
||||||
hideInMenu: true,
|
|
||||||
activeMenu: 'function_hide-child'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'function_hide-child_three',
|
|
||||||
path: '/function/hide-child/three',
|
|
||||||
component: 'view.function_hide-child_three',
|
|
||||||
meta: {
|
|
||||||
title: 'function_hide-child_three',
|
|
||||||
i18nKey: 'route.function_hide-child_three',
|
|
||||||
hideInMenu: true,
|
|
||||||
activeMenu: 'function_hide-child'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'function_hide-child_two',
|
|
||||||
path: '/function/hide-child/two',
|
|
||||||
component: 'view.function_hide-child_two',
|
|
||||||
meta: {
|
|
||||||
title: 'function_hide-child_two',
|
|
||||||
i18nKey: 'route.function_hide-child_two',
|
|
||||||
hideInMenu: true,
|
|
||||||
activeMenu: 'function_hide-child'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'function_multi-tab',
|
|
||||||
path: '/function/multi-tab',
|
|
||||||
component: 'view.function_multi-tab',
|
|
||||||
meta: {
|
|
||||||
title: 'function_multi-tab',
|
|
||||||
i18nKey: 'route.function_multi-tab',
|
|
||||||
icon: 'ic:round-tab',
|
|
||||||
multiTab: true,
|
|
||||||
hideInMenu: true,
|
|
||||||
activeMenu: 'function_tab'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'function_request',
|
|
||||||
path: '/function/request',
|
|
||||||
component: 'view.function_request',
|
|
||||||
meta: {
|
|
||||||
title: 'function_request',
|
|
||||||
i18nKey: 'route.function_request',
|
|
||||||
icon: 'carbon:network-overlay',
|
|
||||||
order: 3
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'function_super-page',
|
|
||||||
path: '/function/super-page',
|
|
||||||
component: 'view.function_super-page',
|
|
||||||
meta: {
|
|
||||||
title: 'function_super-page',
|
|
||||||
i18nKey: 'route.function_super-page',
|
|
||||||
icon: 'ic:round-supervisor-account',
|
|
||||||
order: 5,
|
|
||||||
roles: ['R_SUPER']
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'function_tab',
|
|
||||||
path: '/function/tab',
|
|
||||||
component: 'view.function_tab',
|
|
||||||
meta: {
|
|
||||||
title: 'function_tab',
|
|
||||||
i18nKey: 'route.function_tab',
|
|
||||||
icon: 'ic:round-tab',
|
|
||||||
order: 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'function_toggle-auth',
|
|
||||||
path: '/function/toggle-auth',
|
|
||||||
component: 'view.function_toggle-auth',
|
|
||||||
meta: {
|
|
||||||
title: 'function_toggle-auth',
|
|
||||||
i18nKey: 'route.function_toggle-auth',
|
|
||||||
icon: 'ic:round-construction',
|
|
||||||
order: 4
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: 'iframe-page',
|
name: 'iframe-page',
|
||||||
path: '/iframe-page/:url',
|
path: '/iframe-page/:url',
|
||||||
@@ -303,18 +185,6 @@ export const generatedRoutes: GeneratedRoute[] = [
|
|||||||
keepAlive: true
|
keepAlive: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: 'personal-center_my-monthly',
|
|
||||||
path: '/personal-center/my-monthly',
|
|
||||||
component: 'view.personal-center_my-monthly',
|
|
||||||
meta: {
|
|
||||||
title: 'personal-center_my-monthly',
|
|
||||||
i18nKey: 'route.personal-center_my-monthly',
|
|
||||||
icon: 'mdi:calendar-month-outline',
|
|
||||||
order: 2,
|
|
||||||
keepAlive: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: 'personal-center_my-performance',
|
name: 'personal-center_my-performance',
|
||||||
path: '/personal-center/my-performance',
|
path: '/personal-center/my-performance',
|
||||||
@@ -339,18 +209,6 @@ export const generatedRoutes: GeneratedRoute[] = [
|
|||||||
keepAlive: true
|
keepAlive: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: 'personal-center_my-weekly',
|
|
||||||
path: '/personal-center/my-weekly',
|
|
||||||
component: 'view.personal-center_my-weekly',
|
|
||||||
meta: {
|
|
||||||
title: 'personal-center_my-weekly',
|
|
||||||
i18nKey: 'route.personal-center_my-weekly',
|
|
||||||
icon: 'mdi:calendar-week-outline',
|
|
||||||
order: 1,
|
|
||||||
keepAlive: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: 'personal-center_overtime-application',
|
name: 'personal-center_overtime-application',
|
||||||
path: '/personal-center/overtime-application',
|
path: '/personal-center/overtime-application',
|
||||||
@@ -374,223 +232,53 @@ export const generatedRoutes: GeneratedRoute[] = [
|
|||||||
order: 7,
|
order: 7,
|
||||||
keepAlive: true
|
keepAlive: true
|
||||||
}
|
}
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'plugin',
|
|
||||||
path: '/plugin',
|
|
||||||
component: 'layout.base',
|
|
||||||
meta: {
|
|
||||||
title: '插件示例',
|
|
||||||
i18nKey: 'route.plugin',
|
|
||||||
order: 7,
|
|
||||||
icon: 'clarity:plugin-line'
|
|
||||||
},
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
name: 'plugin_barcode',
|
|
||||||
path: '/plugin/barcode',
|
|
||||||
component: 'view.plugin_barcode',
|
|
||||||
meta: {
|
|
||||||
title: 'plugin_barcode',
|
|
||||||
i18nKey: 'route.plugin_barcode',
|
|
||||||
icon: 'ic:round-barcode'
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'plugin_charts',
|
name: 'personal-center_work-report',
|
||||||
path: '/plugin/charts',
|
path: '/personal-center/work-report',
|
||||||
|
component: 'view.personal-center_work-report',
|
||||||
meta: {
|
meta: {
|
||||||
title: 'plugin_charts',
|
title: 'personal-center_work-report',
|
||||||
i18nKey: 'route.plugin_charts',
|
i18nKey: 'route.personal-center_work-report',
|
||||||
icon: 'mdi:chart-areaspline'
|
icon: 'mdi:file-chart-outline',
|
||||||
},
|
order: 3,
|
||||||
children: [
|
|
||||||
{
|
|
||||||
name: 'plugin_charts_antv',
|
|
||||||
path: '/plugin/charts/antv',
|
|
||||||
component: 'view.plugin_charts_antv',
|
|
||||||
meta: {
|
|
||||||
title: 'plugin_charts_antv',
|
|
||||||
i18nKey: 'route.plugin_charts_antv',
|
|
||||||
icon: 'hugeicons:flow-square'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'plugin_charts_echarts',
|
|
||||||
path: '/plugin/charts/echarts',
|
|
||||||
component: 'view.plugin_charts_echarts',
|
|
||||||
meta: {
|
|
||||||
title: 'plugin_charts_echarts',
|
|
||||||
i18nKey: 'route.plugin_charts_echarts',
|
|
||||||
icon: 'simple-icons:apacheecharts'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'plugin_charts_vchart',
|
|
||||||
path: '/plugin/charts/vchart',
|
|
||||||
component: 'view.plugin_charts_vchart',
|
|
||||||
meta: {
|
|
||||||
title: 'plugin_charts_vchart',
|
|
||||||
i18nKey: 'route.plugin_charts_vchart',
|
|
||||||
localIcon: 'visactor'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'plugin_copy',
|
|
||||||
path: '/plugin/copy',
|
|
||||||
component: 'view.plugin_copy',
|
|
||||||
meta: {
|
|
||||||
title: 'plugin_copy',
|
|
||||||
i18nKey: 'route.plugin_copy',
|
|
||||||
icon: 'mdi:clipboard-outline'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'plugin_excel',
|
|
||||||
path: '/plugin/excel',
|
|
||||||
component: 'view.plugin_excel',
|
|
||||||
meta: {
|
|
||||||
title: 'plugin_excel',
|
|
||||||
i18nKey: 'route.plugin_excel',
|
|
||||||
icon: 'ri:file-excel-2-line',
|
|
||||||
keepAlive: true
|
keepAlive: true
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'plugin_gantt',
|
|
||||||
path: '/plugin/gantt',
|
|
||||||
meta: {
|
|
||||||
title: 'plugin_gantt',
|
|
||||||
i18nKey: 'route.plugin_gantt',
|
|
||||||
icon: 'ant-design:bar-chart-outlined'
|
|
||||||
},
|
},
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
name: 'plugin_gantt_dhtmlx',
|
name: 'personal-center_work-report_monthly',
|
||||||
path: '/plugin/gantt/dhtmlx',
|
path: '/personal-center/work-report/monthly',
|
||||||
component: 'view.plugin_gantt_dhtmlx',
|
component: 'view.personal-center_work-report_monthly',
|
||||||
meta: {
|
meta: {
|
||||||
title: 'plugin_gantt_dhtmlx',
|
title: 'personal-center_work-report_monthly',
|
||||||
i18nKey: 'route.plugin_gantt_dhtmlx',
|
i18nKey: 'route.personal-center_work-report_monthly',
|
||||||
icon: 'gridicons:posts'
|
hideInMenu: true,
|
||||||
|
activeMenu: 'personal-center_work-report'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'plugin_gantt_vtable',
|
name: 'personal-center_work-report_project',
|
||||||
path: '/plugin/gantt/vtable',
|
path: '/personal-center/work-report/project',
|
||||||
component: 'view.plugin_gantt_vtable',
|
component: 'view.personal-center_work-report_project',
|
||||||
meta: {
|
meta: {
|
||||||
title: 'plugin_gantt_vtable',
|
title: 'personal-center_work-report_project',
|
||||||
i18nKey: 'route.plugin_gantt_vtable',
|
i18nKey: 'route.personal-center_work-report_project',
|
||||||
localIcon: 'visactor'
|
hideInMenu: true,
|
||||||
|
activeMenu: 'personal-center_work-report'
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'plugin_icon',
|
|
||||||
path: '/plugin/icon',
|
|
||||||
component: 'view.plugin_icon',
|
|
||||||
meta: {
|
|
||||||
title: 'plugin_icon',
|
|
||||||
i18nKey: 'route.plugin_icon',
|
|
||||||
localIcon: 'custom-icon'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'plugin_map',
|
|
||||||
path: '/plugin/map',
|
|
||||||
component: 'view.plugin_map',
|
|
||||||
meta: {
|
|
||||||
title: 'plugin_map',
|
|
||||||
i18nKey: 'route.plugin_map',
|
|
||||||
icon: 'mdi:map'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'plugin_pdf',
|
|
||||||
path: '/plugin/pdf',
|
|
||||||
component: 'view.plugin_pdf',
|
|
||||||
meta: {
|
|
||||||
title: 'plugin_pdf',
|
|
||||||
i18nKey: 'route.plugin_pdf',
|
|
||||||
icon: 'uiw:file-pdf'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'plugin_pinyin',
|
|
||||||
path: '/plugin/pinyin',
|
|
||||||
component: 'view.plugin_pinyin',
|
|
||||||
meta: {
|
|
||||||
title: 'plugin_pinyin',
|
|
||||||
i18nKey: 'route.plugin_pinyin',
|
|
||||||
icon: 'entypo-social:google-hangouts'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'plugin_print',
|
|
||||||
path: '/plugin/print',
|
|
||||||
component: 'view.plugin_print',
|
|
||||||
meta: {
|
|
||||||
title: 'plugin_print',
|
|
||||||
i18nKey: 'route.plugin_print',
|
|
||||||
icon: 'mdi:printer'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'plugin_swiper',
|
|
||||||
path: '/plugin/swiper',
|
|
||||||
component: 'view.plugin_swiper',
|
|
||||||
meta: {
|
|
||||||
title: 'plugin_swiper',
|
|
||||||
i18nKey: 'route.plugin_swiper',
|
|
||||||
icon: 'simple-icons:swiper'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'plugin_tables',
|
|
||||||
path: '/plugin/tables',
|
|
||||||
meta: {
|
|
||||||
title: 'plugin_tables',
|
|
||||||
i18nKey: 'route.plugin_tables',
|
|
||||||
icon: 'icon-park-outline:table'
|
|
||||||
},
|
|
||||||
children: [
|
|
||||||
{
|
{
|
||||||
name: 'plugin_tables_vtable',
|
name: 'personal-center_work-report_weekly',
|
||||||
path: '/plugin/tables/vtable',
|
path: '/personal-center/work-report/weekly',
|
||||||
component: 'view.plugin_tables_vtable',
|
component: 'view.personal-center_work-report_weekly',
|
||||||
meta: {
|
meta: {
|
||||||
title: 'plugin_tables_vtable',
|
title: 'personal-center_work-report_weekly',
|
||||||
i18nKey: 'route.plugin_tables_vtable',
|
i18nKey: 'route.personal-center_work-report_weekly',
|
||||||
localIcon: 'visactor'
|
hideInMenu: true,
|
||||||
|
activeMenu: 'personal-center_work-report'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'plugin_typeit',
|
|
||||||
path: '/plugin/typeit',
|
|
||||||
component: 'view.plugin_typeit',
|
|
||||||
meta: {
|
|
||||||
title: 'plugin_typeit',
|
|
||||||
i18nKey: 'route.plugin_typeit',
|
|
||||||
icon: 'mdi:typewriter'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'plugin_video',
|
|
||||||
path: '/plugin/video',
|
|
||||||
component: 'view.plugin_video',
|
|
||||||
meta: {
|
|
||||||
title: 'plugin_video',
|
|
||||||
i18nKey: 'route.plugin_video',
|
|
||||||
icon: 'mdi:video'
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -170,16 +170,6 @@ const routeMap: RouteMap = {
|
|||||||
"403": "/403",
|
"403": "/403",
|
||||||
"404": "/404",
|
"404": "/404",
|
||||||
"500": "/500",
|
"500": "/500",
|
||||||
"function": "/function",
|
|
||||||
"function_hide-child": "/function/hide-child",
|
|
||||||
"function_hide-child_one": "/function/hide-child/one",
|
|
||||||
"function_hide-child_three": "/function/hide-child/three",
|
|
||||||
"function_hide-child_two": "/function/hide-child/two",
|
|
||||||
"function_multi-tab": "/function/multi-tab",
|
|
||||||
"function_request": "/function/request",
|
|
||||||
"function_super-page": "/function/super-page",
|
|
||||||
"function_tab": "/function/tab",
|
|
||||||
"function_toggle-auth": "/function/toggle-auth",
|
|
||||||
"iframe-page": "/iframe-page/:url",
|
"iframe-page": "/iframe-page/:url",
|
||||||
"infra": "/infra",
|
"infra": "/infra",
|
||||||
"infra_rd-code": "/infra/rd-code",
|
"infra_rd-code": "/infra/rd-code",
|
||||||
@@ -192,33 +182,14 @@ const routeMap: RouteMap = {
|
|||||||
"personal-center": "/personal-center",
|
"personal-center": "/personal-center",
|
||||||
"personal-center_my-application": "/personal-center/my-application",
|
"personal-center_my-application": "/personal-center/my-application",
|
||||||
"personal-center_my-item": "/personal-center/my-item",
|
"personal-center_my-item": "/personal-center/my-item",
|
||||||
"personal-center_my-monthly": "/personal-center/my-monthly",
|
|
||||||
"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_overtime-application": "/personal-center/overtime-application",
|
"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",
|
"personal-center_work-report": "/personal-center/work-report",
|
||||||
"plugin_barcode": "/plugin/barcode",
|
"personal-center_work-report_monthly": "/personal-center/work-report/monthly",
|
||||||
"plugin_charts": "/plugin/charts",
|
"personal-center_work-report_project": "/personal-center/work-report/project",
|
||||||
"plugin_charts_antv": "/plugin/charts/antv",
|
"personal-center_work-report_weekly": "/personal-center/work-report/weekly",
|
||||||
"plugin_charts_echarts": "/plugin/charts/echarts",
|
|
||||||
"plugin_charts_vchart": "/plugin/charts/vchart",
|
|
||||||
"plugin_copy": "/plugin/copy",
|
|
||||||
"plugin_excel": "/plugin/excel",
|
|
||||||
"plugin_gantt": "/plugin/gantt",
|
|
||||||
"plugin_gantt_dhtmlx": "/plugin/gantt/dhtmlx",
|
|
||||||
"plugin_gantt_vtable": "/plugin/gantt/vtable",
|
|
||||||
"plugin_icon": "/plugin/icon",
|
|
||||||
"plugin_map": "/plugin/map",
|
|
||||||
"plugin_pdf": "/plugin/pdf",
|
|
||||||
"plugin_pinyin": "/plugin/pinyin",
|
|
||||||
"plugin_print": "/plugin/print",
|
|
||||||
"plugin_swiper": "/plugin/swiper",
|
|
||||||
"plugin_tables": "/plugin/tables",
|
|
||||||
"plugin_tables_vtable": "/plugin/tables/vtable",
|
|
||||||
"plugin_typeit": "/plugin/typeit",
|
|
||||||
"plugin_video": "/plugin/video",
|
|
||||||
"product": "/product",
|
"product": "/product",
|
||||||
"product_dashboard": "/product/dashboard",
|
"product_dashboard": "/product/dashboard",
|
||||||
"product_list": "/product/list",
|
"product_list": "/product/list",
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ function createBatchDeleteQuery(ids: number[]) {
|
|||||||
type DictDataResponse = Omit<Api.Dict.DictData, 'colorType'> & {
|
type DictDataResponse = Omit<Api.Dict.DictData, 'colorType'> & {
|
||||||
colorType?: string | null;
|
colorType?: string | null;
|
||||||
color_type?: string | null;
|
color_type?: string | null;
|
||||||
|
css_class?: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
type DictDataPageResponse = Omit<Api.Dict.PageResult<Api.Dict.DictData>, 'list'> & {
|
type DictDataPageResponse = Omit<Api.Dict.PageResult<Api.Dict.DictData>, 'list'> & {
|
||||||
@@ -28,6 +29,7 @@ type DictDataPageResponse = Omit<Api.Dict.PageResult<Api.Dict.DictData>, 'list'>
|
|||||||
type FrontendDictDataResponse = Omit<Api.Dict.FrontendDictData, 'colorType'> & {
|
type FrontendDictDataResponse = Omit<Api.Dict.FrontendDictData, 'colorType'> & {
|
||||||
colorType?: string | null;
|
colorType?: string | null;
|
||||||
color_type?: string | null;
|
color_type?: string | null;
|
||||||
|
css_class?: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
type FrontendDictCacheResponse = Record<string, FrontendDictDataResponse[]>;
|
type FrontendDictCacheResponse = Record<string, FrontendDictDataResponse[]>;
|
||||||
@@ -37,20 +39,22 @@ function normalizeColorType(value?: string | null) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function normalizeDictData(data: DictDataResponse): Api.Dict.DictData {
|
function normalizeDictData(data: DictDataResponse): Api.Dict.DictData {
|
||||||
const { color_type: colorTypeFromSnakeCase, ...rest } = data;
|
const { color_type: colorTypeFromSnakeCase, css_class: cssClassFromSnakeCase, ...rest } = data;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...rest,
|
...rest,
|
||||||
colorType: normalizeColorType(data.colorType ?? colorTypeFromSnakeCase)
|
colorType: normalizeColorType(data.colorType ?? colorTypeFromSnakeCase),
|
||||||
|
cssClass: normalizeColorType(data.cssClass ?? cssClassFromSnakeCase)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeFrontendDictData(data: FrontendDictDataResponse): Api.Dict.FrontendDictData {
|
function normalizeFrontendDictData(data: FrontendDictDataResponse): Api.Dict.FrontendDictData {
|
||||||
const { color_type: colorTypeFromSnakeCase, ...rest } = data;
|
const { color_type: colorTypeFromSnakeCase, css_class: cssClassFromSnakeCase, ...rest } = data;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...rest,
|
...rest,
|
||||||
colorType: normalizeColorType(data.colorType ?? colorTypeFromSnakeCase)
|
colorType: normalizeColorType(data.colorType ?? colorTypeFromSnakeCase),
|
||||||
|
cssClass: normalizeColorType(data.cssClass ?? cssClassFromSnakeCase)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,11 +2,15 @@ export * from './auth';
|
|||||||
export * from './dict';
|
export * from './dict';
|
||||||
export * from './file';
|
export * from './file';
|
||||||
export * from './infra';
|
export * from './infra';
|
||||||
|
export * from './notice';
|
||||||
|
export * from './notify-message';
|
||||||
export * from './object-context';
|
export * from './object-context';
|
||||||
export * from './overtime-application';
|
export * from './overtime-application';
|
||||||
export * from './personal-item';
|
export * from './personal-item';
|
||||||
export * from './product';
|
export * from './product';
|
||||||
export * from './project';
|
export * from './project';
|
||||||
|
export * from './project-group';
|
||||||
export * from './project-shared';
|
export * from './project-shared';
|
||||||
export * from './route';
|
export * from './route';
|
||||||
export * from './system-manage';
|
export * from './system-manage';
|
||||||
|
export * from './work-report';
|
||||||
|
|||||||
28
src/service/api/notice.ts
Normal file
28
src/service/api/notice.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import { SYSTEM_SERVICE_PREFIX } from '@/constants/service';
|
||||||
|
import { request } from '../request';
|
||||||
|
import { type ServiceRequestResult, mapServiceResult, normalizeStringId, safeJsonRequestConfig } from './shared';
|
||||||
|
|
||||||
|
const NOTICE_PREFIX = `${SYSTEM_SERVICE_PREFIX}/notice`;
|
||||||
|
|
||||||
|
type NoticeResponse = Omit<Api.Notice.Notice, 'id'> & {
|
||||||
|
id: string | number;
|
||||||
|
};
|
||||||
|
|
||||||
|
function normalizeNotice(data: NoticeResponse): Api.Notice.Notice {
|
||||||
|
return {
|
||||||
|
...data,
|
||||||
|
id: normalizeStringId(data.id)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取最近公告(status=0,按 id 倒序;登录即可,工作台公告卡片用) */
|
||||||
|
export async function fetchGetRecentNotices(size?: number) {
|
||||||
|
const result = await request<NoticeResponse[]>({
|
||||||
|
url: `${NOTICE_PREFIX}/recent`,
|
||||||
|
method: 'get',
|
||||||
|
params: { size },
|
||||||
|
...safeJsonRequestConfig
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapServiceResult(result as ServiceRequestResult<NoticeResponse[]>, data => data.map(normalizeNotice));
|
||||||
|
}
|
||||||
63
src/service/api/notify-message.ts
Normal file
63
src/service/api/notify-message.ts
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
import { SYSTEM_SERVICE_PREFIX } from '@/constants/service';
|
||||||
|
import { request } from '../request';
|
||||||
|
import { type ServiceRequestResult, mapServiceResult, normalizeStringId, safeJsonRequestConfig } from './shared';
|
||||||
|
|
||||||
|
const NOTIFY_MESSAGE_PREFIX = `${SYSTEM_SERVICE_PREFIX}/notify-message`;
|
||||||
|
|
||||||
|
type NotifyMessageResponse = Omit<Api.NotifyMessage.NotifyMessage, 'id' | 'level'> & {
|
||||||
|
id: string | number;
|
||||||
|
/** 后端老消息可能不带 level,按可空接收,normalize 时回落普通(1) */
|
||||||
|
level?: number | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
type MyNotifyMessagePageResponse = Omit<Api.NotifyMessage.PageResult<Api.NotifyMessage.NotifyMessage>, 'list'> & {
|
||||||
|
list: NotifyMessageResponse[];
|
||||||
|
};
|
||||||
|
|
||||||
|
function normalizeNotifyMessage(data: NotifyMessageResponse): Api.NotifyMessage.NotifyMessage {
|
||||||
|
return {
|
||||||
|
...data,
|
||||||
|
id: normalizeStringId(data.id),
|
||||||
|
level: data.level ?? 1
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取当前用户未读站内信数量(铃铛红点轮询用) */
|
||||||
|
export function fetchGetUnreadNotifyCount() {
|
||||||
|
return request<number>({
|
||||||
|
url: `${NOTIFY_MESSAGE_PREFIX}/get-unread-count`,
|
||||||
|
method: 'get'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 分页获取我的站内信(消息列表唯一数据源;未读传 readStatus=false、已读传 true) */
|
||||||
|
export async function fetchGetMyNotifyMessagePage(params: Api.NotifyMessage.MyPageParams) {
|
||||||
|
const result = await request<MyNotifyMessagePageResponse>({
|
||||||
|
url: `${NOTIFY_MESSAGE_PREFIX}/my-page`,
|
||||||
|
method: 'get',
|
||||||
|
params,
|
||||||
|
...safeJsonRequestConfig
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapServiceResult(result as ServiceRequestResult<MyNotifyMessagePageResponse>, data => ({
|
||||||
|
...data,
|
||||||
|
list: data.list.map(normalizeNotifyMessage)
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 批量标记站内信已读(后端幂等:重复提交、非本人条目均安全) */
|
||||||
|
export function fetchUpdateNotifyMessageRead(ids: string[]) {
|
||||||
|
// 后端约定 ids 逗号分隔
|
||||||
|
return request<boolean>({
|
||||||
|
url: `${NOTIFY_MESSAGE_PREFIX}/update-read?ids=${ids.join(',')}`,
|
||||||
|
method: 'put'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 当前用户全部站内信标记已读 */
|
||||||
|
export function fetchUpdateAllNotifyMessageRead() {
|
||||||
|
return request<boolean>({
|
||||||
|
url: `${NOTIFY_MESSAGE_PREFIX}/update-all-read`,
|
||||||
|
method: 'put'
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,13 +1,7 @@
|
|||||||
import { WEB_SERVICE_PREFIX } from '@/constants/service';
|
import { WEB_SERVICE_PREFIX } from '@/constants/service';
|
||||||
import { request } from '../request';
|
import { request } from '../request';
|
||||||
import { type ProjectLocalDateValue, normalizeProjectLocalDate } from './project-shared';
|
import { type ProjectLocalDateValue, normalizeProjectLocalDate } from './project-shared';
|
||||||
import {
|
import { type ServiceRequestResult, mapServiceResult, normalizeStringId, safeJsonRequestConfig } from './shared';
|
||||||
type ServiceRequestResult,
|
|
||||||
mapServiceResult,
|
|
||||||
normalizeNullableStringId,
|
|
||||||
normalizeStringId,
|
|
||||||
safeJsonRequestConfig
|
|
||||||
} from './shared';
|
|
||||||
|
|
||||||
const OVERTIME_APPLICATION_PREFIX = `${WEB_SERVICE_PREFIX}/project/overtime-applications`;
|
const OVERTIME_APPLICATION_PREFIX = `${WEB_SERVICE_PREFIX}/project/overtime-applications`;
|
||||||
|
|
||||||
@@ -30,14 +24,14 @@ type OvertimeApplicationPageResponse = Omit<Api.OvertimeApplication.OvertimeAppl
|
|||||||
list: OvertimeApplicationResponse[];
|
list: OvertimeApplicationResponse[];
|
||||||
};
|
};
|
||||||
|
|
||||||
type OvertimeApplicationStatusLogResponse = Omit<
|
type OvertimeApplicationApprovalRecordResponse = Omit<
|
||||||
Api.OvertimeApplication.OvertimeApplicationStatusLog,
|
Api.OvertimeApplication.OvertimeApplicationApprovalRecord,
|
||||||
'id' | 'applicationId' | 'operatorUserId' | 'overtimeDateSnapshot'
|
'id' | 'overtimeApplicationId' | 'statusLogId' | 'auditorUserId'
|
||||||
> & {
|
> & {
|
||||||
id: StringIdResponse;
|
id: StringIdResponse;
|
||||||
applicationId: StringIdResponse;
|
overtimeApplicationId: StringIdResponse;
|
||||||
operatorUserId: StringIdResponse;
|
statusLogId: StringIdResponse;
|
||||||
overtimeDateSnapshot: ProjectLocalDateValue;
|
auditorUserId: StringIdResponse;
|
||||||
};
|
};
|
||||||
|
|
||||||
function normalizeBooleanFlag(value: boolean | number | string | null | undefined) {
|
function normalizeBooleanFlag(value: boolean | number | string | null | undefined) {
|
||||||
@@ -81,18 +75,16 @@ function normalizeOvertimeApplication(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeStatusLog(
|
function normalizeApprovalRecord(
|
||||||
response: OvertimeApplicationStatusLogResponse
|
response: OvertimeApplicationApprovalRecordResponse
|
||||||
): Api.OvertimeApplication.OvertimeApplicationStatusLog {
|
): Api.OvertimeApplication.OvertimeApplicationApprovalRecord {
|
||||||
return {
|
return {
|
||||||
...response,
|
...response,
|
||||||
id: normalizeStringId(response.id),
|
id: normalizeStringId(response.id),
|
||||||
applicationId: normalizeStringId(response.applicationId),
|
overtimeApplicationId: normalizeStringId(response.overtimeApplicationId),
|
||||||
operatorUserId: normalizeStringId(response.operatorUserId),
|
statusLogId: normalizeStringId(response.statusLogId),
|
||||||
overtimeDateSnapshot: normalizeProjectLocalDate(response.overtimeDateSnapshot) ?? '',
|
auditorUserId: normalizeStringId(response.auditorUserId),
|
||||||
fromStatus: normalizeNullableStringId(response.fromStatus),
|
opinion: response.opinion ?? null
|
||||||
reason: response.reason ?? null,
|
|
||||||
remark: response.remark ?? null
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -240,12 +232,25 @@ export function fetchRejectOvertimeApplication(id: string, data: Api.OvertimeApp
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function fetchCancelOvertimeApplication(id: string, data: Api.OvertimeApplication.StatusActionParams) {
|
export function fetchBatchApproveOvertimeApplication(
|
||||||
return request<boolean>({
|
data: Api.OvertimeApplication.OvertimeApplicationBatchActionParams
|
||||||
|
) {
|
||||||
|
return request<Api.OvertimeApplication.OvertimeApplicationBatchActionResult>({
|
||||||
...safeJsonRequestConfig,
|
...safeJsonRequestConfig,
|
||||||
url: `${OVERTIME_APPLICATION_PREFIX}/${id}/cancel`,
|
url: `${OVERTIME_APPLICATION_PREFIX}/batch-approve`,
|
||||||
method: 'post',
|
method: 'post',
|
||||||
data: toStatusActionRequest(data)
|
data
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchBatchRejectOvertimeApplication(
|
||||||
|
data: Api.OvertimeApplication.OvertimeApplicationBatchActionParams
|
||||||
|
) {
|
||||||
|
return request<Api.OvertimeApplication.OvertimeApplicationBatchActionResult>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${OVERTIME_APPLICATION_PREFIX}/batch-reject`,
|
||||||
|
method: 'post',
|
||||||
|
data
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -257,15 +262,28 @@ export function fetchDeleteOvertimeApplication(id: string) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchGetOvertimeApplicationStatusLogs(id: string) {
|
export async function fetchGetOvertimeApplicationApprovalRecords(id: string) {
|
||||||
const result = await request<OvertimeApplicationStatusLogResponse[]>({
|
const result = await request<OvertimeApplicationApprovalRecordResponse[]>({
|
||||||
...safeJsonRequestConfig,
|
...safeJsonRequestConfig,
|
||||||
url: `${OVERTIME_APPLICATION_PREFIX}/${id}/status-logs`,
|
url: `${OVERTIME_APPLICATION_PREFIX}/${id}/approval-records`,
|
||||||
method: 'get'
|
method: 'get'
|
||||||
});
|
});
|
||||||
|
|
||||||
return mapServiceResult(result as ServiceRequestResult<OvertimeApplicationStatusLogResponse[]>, data =>
|
return mapServiceResult(result as ServiceRequestResult<OvertimeApplicationApprovalRecordResponse[]>, data =>
|
||||||
data.map(normalizeStatusLog)
|
data.map(normalizeApprovalRecord)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchGetOvertimeApplicationStatusDict() {
|
||||||
|
const result = await request<Api.OvertimeApplication.OvertimeApplicationStatusDict[]>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${OVERTIME_APPLICATION_PREFIX}/status/dict`,
|
||||||
|
method: 'get'
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapServiceResult(
|
||||||
|
result as ServiceRequestResult<Api.OvertimeApplication.OvertimeApplicationStatusDict[]>,
|
||||||
|
data => data
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -106,13 +106,34 @@ export async function fetchGetProductPage(params?: Api.Product.ProductSearchPara
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ProductOverviewSummaryResponse = Omit<Api.Product.ProductOverviewSummary, 'total' | 'items'> & {
|
||||||
|
/** 后端 overview-summary 升级(total/items)灰度期间可能缺省,适配层兜底 */
|
||||||
|
total?: number | null;
|
||||||
|
items?: Api.Product.OverviewStatusItem[] | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 归一化产品概览统计:total/items 兜底,保证业务层拿到完整结构 */
|
||||||
|
function normalizeProductOverviewSummary(data: ProductOverviewSummaryResponse): Api.Product.ProductOverviewSummary {
|
||||||
|
return {
|
||||||
|
...data,
|
||||||
|
statusCounts: data.statusCounts ?? {},
|
||||||
|
total: data.total ?? 0,
|
||||||
|
items: data.items ?? []
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/** 获取产品入口页概览统计 */
|
/** 获取产品入口页概览统计 */
|
||||||
export function fetchGetProductOverviewSummary() {
|
export async function fetchGetProductOverviewSummary() {
|
||||||
return request<Api.Product.ProductOverviewSummary>({
|
const result = await request<ProductOverviewSummaryResponse>({
|
||||||
...safeJsonRequestConfig,
|
...safeJsonRequestConfig,
|
||||||
url: `${PRODUCT_PREFIX}/overview-summary`,
|
url: `${PRODUCT_PREFIX}/overview-summary`,
|
||||||
method: 'get'
|
method: 'get'
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return mapServiceResult(
|
||||||
|
result as ServiceRequestResult<ProductOverviewSummaryResponse>,
|
||||||
|
normalizeProductOverviewSummary
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 获取产品详情 */
|
/** 获取产品详情 */
|
||||||
|
|||||||
62
src/service/api/project-group.ts
Normal file
62
src/service/api/project-group.ts
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import { WEB_SERVICE_PREFIX } from '@/constants/service';
|
||||||
|
import { request } from '../request';
|
||||||
|
import {
|
||||||
|
type ServiceRequestResult,
|
||||||
|
mapServiceResult,
|
||||||
|
normalizeNullableStringId,
|
||||||
|
safeJsonRequestConfig
|
||||||
|
} from './shared';
|
||||||
|
import { type ProjectResponse, normalizeProject } from './project';
|
||||||
|
|
||||||
|
const PROJECT_PREFIX = `${WEB_SERVICE_PREFIX}/project/project`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* group-page 原始响应。
|
||||||
|
* 组级 managerUserId、productId:后端对小数值 Long(如 1001)仍按数字返回,需 String() 归一;
|
||||||
|
* projects 字段与 page 接口项目行完全一致,复用 ProjectResponse / normalizeProject。
|
||||||
|
*/
|
||||||
|
type ProjectGroupResponse = Omit<Api.Project.ProjectGroup, 'productId' | 'managerUserId' | 'projects'> & {
|
||||||
|
productId?: string | number | null;
|
||||||
|
managerUserId?: string | number | null;
|
||||||
|
projects: ProjectResponse[];
|
||||||
|
};
|
||||||
|
|
||||||
|
type ProjectGroupPageResponse = Omit<Api.Project.ProjectGroupPageResult, 'list'> & {
|
||||||
|
list: ProjectGroupResponse[];
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 归一化分组:组级 ID String 化,组内项目复用 normalizeProject(id/managerUserId/productId/日期统一口径) */
|
||||||
|
function normalizeProjectGroup(group: ProjectGroupResponse): Api.Project.ProjectGroup {
|
||||||
|
return {
|
||||||
|
...group,
|
||||||
|
productId: normalizeNullableStringId(group.productId),
|
||||||
|
managerUserId: normalizeNullableStringId(group.managerUserId),
|
||||||
|
projects: Array.isArray(group.projects) ? group.projects.map(normalizeProject) : []
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 项目列表「按产品分组」分页。
|
||||||
|
*
|
||||||
|
* 后端契约见《项目列表产品分组-前端API-2026-06-10》:
|
||||||
|
* - pageNo/pageSize 为产品组维度分页;statusCode 不传 = 「全部」口径(后端从状态机推导,
|
||||||
|
* 当前等价 pending/active/paused/completed,不含 cancelled/archived)。
|
||||||
|
* - 组内 projects 仅返前 topN 条(默认 5),projectTotal 为该口径组内全量计数;
|
||||||
|
* 剩余项目由页面按 productId / orphanOnly + statusCodes 走 page 接口展开拉取。
|
||||||
|
* - typeCounts / hasBaseline 现状恒按「全部」口径统计,不随 statusCode 变化;其中 typeCounts 已提需求
|
||||||
|
* 改为与 projectTotal 同口径(见《2026-06-11-项目分组接口typeCounts口径-后端接口需求》),后端落地后更新本注释;
|
||||||
|
* hasBaseline = 存在非已取消的主线项目(已归档/完成也算占坑),前端直接消费、不自行推导。
|
||||||
|
*/
|
||||||
|
export async function fetchGetProjectGroupPage(params?: Api.Project.ProjectGroupSearchParams) {
|
||||||
|
const result = await request<ProjectGroupPageResponse>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${PROJECT_PREFIX}/group-page`,
|
||||||
|
method: 'get',
|
||||||
|
params
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapServiceResult(result as ServiceRequestResult<ProjectGroupPageResponse>, data => ({
|
||||||
|
...data,
|
||||||
|
list: Array.isArray(data.list) ? data.list.map(normalizeProjectGroup) : []
|
||||||
|
}));
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import dayjs from 'dayjs';
|
||||||
import { normalizeNullableStringId, normalizeStringId } from './shared';
|
import { normalizeNullableStringId, normalizeStringId } from './shared';
|
||||||
|
|
||||||
type ProjectStatusCode = Api.Project.ProjectStatusCode;
|
type ProjectStatusCode = Api.Project.ProjectStatusCode;
|
||||||
@@ -40,6 +41,96 @@ export type ProjectExecutionResponse = Omit<
|
|||||||
priorityName?: string | null;
|
priorityName?: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type MyExecutionResponse = Omit<
|
||||||
|
Api.Project.MyExecutionItem,
|
||||||
|
| 'id'
|
||||||
|
| 'projectId'
|
||||||
|
| 'projectRequirementId'
|
||||||
|
| 'priority'
|
||||||
|
| 'progressRate'
|
||||||
|
| 'plannedStartDate'
|
||||||
|
| 'plannedEndDate'
|
||||||
|
| 'actualStartDate'
|
||||||
|
| 'actualEndDate'
|
||||||
|
> & {
|
||||||
|
id: StringIdResponse;
|
||||||
|
projectId: StringIdResponse;
|
||||||
|
projectRequirementId?: StringIdResponse | null;
|
||||||
|
priority?: string | number | null;
|
||||||
|
progressRate?: number | null;
|
||||||
|
plannedStartDate?: ProjectLocalDateValue;
|
||||||
|
plannedEndDate?: ProjectLocalDateValue;
|
||||||
|
actualStartDate?: ProjectLocalDateValue;
|
||||||
|
actualEndDate?: ProjectLocalDateValue;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type MyParticipatedProjectResponse = Omit<Api.Project.MyParticipatedProjectItem, 'id'> & {
|
||||||
|
id: StringIdResponse;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type MyOwnedProjectMemberResponse = Omit<Api.Project.MyOwnedProjectMember, 'userId'> & {
|
||||||
|
userId: StringIdResponse;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type MyOwnedProjectResponse = Omit<Api.Project.MyOwnedProjectItem, 'id' | 'members'> & {
|
||||||
|
id: StringIdResponse;
|
||||||
|
members?: MyOwnedProjectMemberResponse[] | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type MyTaskResponse = Omit<
|
||||||
|
Api.Project.MyTaskItem,
|
||||||
|
| 'id'
|
||||||
|
| 'projectId'
|
||||||
|
| 'executionId'
|
||||||
|
| 'priority'
|
||||||
|
| 'plannedEndDate'
|
||||||
|
| 'progressRate'
|
||||||
|
| 'createTime'
|
||||||
|
| 'parentTaskId'
|
||||||
|
| 'availableActions'
|
||||||
|
> & {
|
||||||
|
id: StringIdResponse;
|
||||||
|
projectId: StringIdResponse;
|
||||||
|
executionId?: StringIdResponse | null;
|
||||||
|
priority?: string | number | null;
|
||||||
|
plannedEndDate?: ProjectLocalDateValue;
|
||||||
|
progressRate?: number | string | null;
|
||||||
|
createTime?: string | number | null;
|
||||||
|
parentTaskId?: StringIdResponse | null;
|
||||||
|
availableActions?: LifecycleActionResponse<Api.Project.ProjectTaskActionCode>[] | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TeamLoadDistributionItemResponse = Omit<Api.Project.TeamLoadDistributionItem, 'projectId'> & {
|
||||||
|
projectId?: StringIdResponse | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TeamLoadMemberResponse = Omit<Api.Project.TeamLoadMember, 'userId' | 'items'> & {
|
||||||
|
userId: StringIdResponse;
|
||||||
|
items?: TeamLoadDistributionItemResponse[] | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TeamLoadResponse = {
|
||||||
|
members?: TeamLoadMemberResponse[] | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type WorklogDistributionItemResponse = Omit<Api.Project.WorklogDistributionItem, 'projectId'> & {
|
||||||
|
projectId?: StringIdResponse | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type MyWorklogWeekResponse = Omit<Api.Project.MyWorklogWeekResult, 'dailyHours' | 'distribution'> & {
|
||||||
|
dailyHours?: number[] | null;
|
||||||
|
distribution?: WorklogDistributionItemResponse[] | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TeamWorklogWeekMemberResponse = Omit<Api.Project.TeamWorklogWeekMember, 'userId' | 'items'> & {
|
||||||
|
userId: StringIdResponse;
|
||||||
|
items?: WorklogDistributionItemResponse[] | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TeamWorklogWeekResponse = Omit<Api.Project.TeamWorklogWeekResult, 'members'> & {
|
||||||
|
members?: TeamWorklogWeekMemberResponse[] | null;
|
||||||
|
};
|
||||||
|
|
||||||
export type ExecutionAssigneeResponse = Omit<Api.Project.ExecutionAssignee, 'id' | 'executionId' | 'userId'> & {
|
export type ExecutionAssigneeResponse = Omit<Api.Project.ExecutionAssignee, 'id' | 'executionId' | 'userId'> & {
|
||||||
id: StringIdResponse;
|
id: StringIdResponse;
|
||||||
executionId: StringIdResponse;
|
executionId: StringIdResponse;
|
||||||
@@ -227,6 +318,28 @@ export function normalizeProjectLocalDate(value: ProjectLocalDateValue | undefin
|
|||||||
return String(value);
|
return String(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 后端 LocalDateTime 统一序列化为毫秒时间戳(也可能是数字字符串/格式化字符串),
|
||||||
|
* 归一为 'YYYY-MM-DD HH:mm:ss' 供展示与 dayjs 解析。
|
||||||
|
*/
|
||||||
|
export function normalizeProjectDateTime(value: string | number | null | undefined): string {
|
||||||
|
if (value === null || value === undefined || value === '') {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
let parsed: dayjs.Dayjs;
|
||||||
|
if (typeof value === 'number') {
|
||||||
|
parsed = dayjs(value);
|
||||||
|
} else if (/^\d+$/.test(value)) {
|
||||||
|
// 字符串形态的毫秒时间戳:dayjs 无法直接解析,先转数值(时间值非 ID,安全整数范围内)
|
||||||
|
parsed = dayjs(Number(value));
|
||||||
|
} else {
|
||||||
|
parsed = dayjs(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return parsed.isValid() ? parsed.format('YYYY-MM-DD HH:mm:ss') : '';
|
||||||
|
}
|
||||||
|
|
||||||
export function normalizeLifecycleActions<ActionCode extends string>(
|
export function normalizeLifecycleActions<ActionCode extends string>(
|
||||||
actions: LifecycleActionResponse<ActionCode>[] | null | undefined
|
actions: LifecycleActionResponse<ActionCode>[] | null | undefined
|
||||||
): Api.Project.LifecycleAction<ActionCode>[] {
|
): Api.Project.LifecycleAction<ActionCode>[] {
|
||||||
@@ -260,6 +373,15 @@ function normalizePriority(value: string | number | null | undefined): string {
|
|||||||
return String(value);
|
return String(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeProgressRate(value: number | string | null | undefined) {
|
||||||
|
if (value === null || value === undefined || value === '') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const numeric = typeof value === 'number' ? value : Number(value ?? 0);
|
||||||
|
return Number.isFinite(numeric) ? numeric : null;
|
||||||
|
}
|
||||||
|
|
||||||
export function normalizeProjectExecution(response: ProjectExecutionResponse): Api.Project.ProjectExecution {
|
export function normalizeProjectExecution(response: ProjectExecutionResponse): Api.Project.ProjectExecution {
|
||||||
return {
|
return {
|
||||||
...response,
|
...response,
|
||||||
@@ -286,6 +408,119 @@ export function normalizeProjectExecution(response: ProjectExecutionResponse): A
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function normalizeMyExecution(response: MyExecutionResponse): Api.Project.MyExecutionItem {
|
||||||
|
return {
|
||||||
|
...response,
|
||||||
|
id: normalizeStringId(response.id),
|
||||||
|
projectId: normalizeStringId(response.projectId),
|
||||||
|
statusName: response.statusName ?? null,
|
||||||
|
priority: normalizePriority(response.priority),
|
||||||
|
progressRate: typeof response.progressRate === 'number' ? response.progressRate : 0,
|
||||||
|
plannedStartDate: normalizeProjectLocalDate(response.plannedStartDate),
|
||||||
|
plannedEndDate: normalizeProjectLocalDate(response.plannedEndDate),
|
||||||
|
actualStartDate: normalizeProjectLocalDate(response.actualStartDate),
|
||||||
|
actualEndDate: normalizeProjectLocalDate(response.actualEndDate),
|
||||||
|
projectRequirementId: normalizeNullableStringId(response.projectRequirementId),
|
||||||
|
projectRequirementName: response.projectRequirementName ?? null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizeMyParticipatedProject(
|
||||||
|
response: MyParticipatedProjectResponse
|
||||||
|
): Api.Project.MyParticipatedProjectItem {
|
||||||
|
return {
|
||||||
|
...response,
|
||||||
|
id: normalizeStringId(response.id),
|
||||||
|
code: response.code ?? null,
|
||||||
|
statusName: response.statusName ?? null,
|
||||||
|
myRole: response.myRole ?? null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizeMyOwnedProject(response: MyOwnedProjectResponse): Api.Project.MyOwnedProjectItem {
|
||||||
|
return {
|
||||||
|
...response,
|
||||||
|
id: normalizeStringId(response.id),
|
||||||
|
code: response.code ?? null,
|
||||||
|
myRole: response.myRole ?? null,
|
||||||
|
plannedEndDate: response.plannedEndDate ?? null,
|
||||||
|
members: (response.members ?? []).map(member => ({
|
||||||
|
...member,
|
||||||
|
userId: normalizeStringId(member.userId),
|
||||||
|
userName: member.userName ?? null
|
||||||
|
}))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizeMyTask(response: MyTaskResponse): Api.Project.MyTaskItem {
|
||||||
|
return {
|
||||||
|
...response,
|
||||||
|
id: normalizeStringId(response.id),
|
||||||
|
projectId: normalizeStringId(response.projectId),
|
||||||
|
executionId: normalizeNullableStringId(response.executionId),
|
||||||
|
executionName: response.executionName ?? null,
|
||||||
|
statusName: response.statusName ?? null,
|
||||||
|
priority: normalizePriority(response.priority),
|
||||||
|
plannedEndDate: normalizeProjectLocalDate(response.plannedEndDate),
|
||||||
|
progressRate: normalizeProgressRate(response.progressRate) ?? 0,
|
||||||
|
createTime: normalizeProjectDateTime(response.createTime),
|
||||||
|
parentTaskId: normalizeNullableStringId(response.parentTaskId),
|
||||||
|
terminal: Boolean(response.terminal),
|
||||||
|
allowEdit: Boolean(response.allowEdit),
|
||||||
|
availableActions: normalizeLifecycleActions(response.availableActions)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeWorklogDistributionItem(
|
||||||
|
response: WorklogDistributionItemResponse | TeamLoadDistributionItemResponse
|
||||||
|
): { projectId: string | null; projectName: string | null; kind: 'project' | 'personal' | 'other' } {
|
||||||
|
return {
|
||||||
|
projectId: normalizeNullableStringId(response.projectId),
|
||||||
|
projectName: response.projectName ?? null,
|
||||||
|
kind: response.kind
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizeTeamLoad(response: TeamLoadResponse): Api.Project.TeamLoadResult {
|
||||||
|
return {
|
||||||
|
members: (response.members ?? []).map(member => ({
|
||||||
|
userId: normalizeStringId(member.userId),
|
||||||
|
userNickname: member.userNickname ?? '',
|
||||||
|
items: (member.items ?? []).map(item => ({
|
||||||
|
...normalizeWorklogDistributionItem(item),
|
||||||
|
count: typeof item.count === 'number' ? item.count : 0
|
||||||
|
})),
|
||||||
|
dueSoonCount: typeof member.dueSoonCount === 'number' ? member.dueSoonCount : 0,
|
||||||
|
overdueCount: typeof member.overdueCount === 'number' ? member.overdueCount : 0
|
||||||
|
}))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizeMyWorklogWeek(response: MyWorklogWeekResponse): Api.Project.MyWorklogWeekResult {
|
||||||
|
return {
|
||||||
|
weekStart: response.weekStart ?? '',
|
||||||
|
dailyHours: response.dailyHours ?? [0, 0, 0, 0, 0],
|
||||||
|
distribution: (response.distribution ?? []).map(item => ({
|
||||||
|
...normalizeWorklogDistributionItem(item),
|
||||||
|
hours: typeof item.hours === 'number' ? item.hours : 0
|
||||||
|
}))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizeTeamWorklogWeek(response: TeamWorklogWeekResponse): Api.Project.TeamWorklogWeekResult {
|
||||||
|
return {
|
||||||
|
weekStart: response.weekStart ?? '',
|
||||||
|
members: (response.members ?? []).map(member => ({
|
||||||
|
userId: normalizeStringId(member.userId),
|
||||||
|
userNickname: member.userNickname ?? '',
|
||||||
|
items: (member.items ?? []).map(item => ({
|
||||||
|
...normalizeWorklogDistributionItem(item),
|
||||||
|
hours: typeof item.hours === 'number' ? item.hours : 0
|
||||||
|
}))
|
||||||
|
}))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function normalizeExecutionAssignee(response: ExecutionAssigneeResponse): Api.Project.ExecutionAssignee {
|
export function normalizeExecutionAssignee(response: ExecutionAssigneeResponse): Api.Project.ExecutionAssignee {
|
||||||
return {
|
return {
|
||||||
...response,
|
...response,
|
||||||
|
|||||||
@@ -10,6 +10,11 @@ import {
|
|||||||
import {
|
import {
|
||||||
type ExecutionAssigneeLogResponse,
|
type ExecutionAssigneeLogResponse,
|
||||||
type ExecutionAssigneeResponse,
|
type ExecutionAssigneeResponse,
|
||||||
|
type MyExecutionResponse,
|
||||||
|
type MyOwnedProjectResponse,
|
||||||
|
type MyParticipatedProjectResponse,
|
||||||
|
type MyTaskResponse,
|
||||||
|
type MyWorklogWeekResponse,
|
||||||
type ProjectExecutionResponse,
|
type ProjectExecutionResponse,
|
||||||
type ProjectLocalDateValue,
|
type ProjectLocalDateValue,
|
||||||
type ProjectMemberResponse,
|
type ProjectMemberResponse,
|
||||||
@@ -17,21 +22,30 @@ import {
|
|||||||
type TaskAssigneeFromApiResponse,
|
type TaskAssigneeFromApiResponse,
|
||||||
type TaskAssigneeLogResponse,
|
type TaskAssigneeLogResponse,
|
||||||
type TaskWorklogResponse,
|
type TaskWorklogResponse,
|
||||||
|
type TeamLoadResponse,
|
||||||
|
type TeamWorklogWeekResponse,
|
||||||
getProjectLifecycleActions,
|
getProjectLifecycleActions,
|
||||||
normalizeExecutionAssignee,
|
normalizeExecutionAssignee,
|
||||||
normalizeExecutionAssigneeLog,
|
normalizeExecutionAssigneeLog,
|
||||||
|
normalizeMyExecution,
|
||||||
|
normalizeMyOwnedProject,
|
||||||
|
normalizeMyParticipatedProject,
|
||||||
|
normalizeMyTask,
|
||||||
|
normalizeMyWorklogWeek,
|
||||||
normalizeProjectExecution,
|
normalizeProjectExecution,
|
||||||
normalizeProjectLocalDate,
|
normalizeProjectLocalDate,
|
||||||
normalizeProjectMember,
|
normalizeProjectMember,
|
||||||
normalizeProjectTask,
|
normalizeProjectTask,
|
||||||
normalizeTaskAssignee,
|
normalizeTaskAssignee,
|
||||||
normalizeTaskAssigneeLog,
|
normalizeTaskAssigneeLog,
|
||||||
normalizeTaskWorklog
|
normalizeTaskWorklog,
|
||||||
|
normalizeTeamLoad,
|
||||||
|
normalizeTeamWorklogWeek
|
||||||
} from './project-shared';
|
} from './project-shared';
|
||||||
|
|
||||||
const PROJECT_PREFIX = `${WEB_SERVICE_PREFIX}/project/project`;
|
const PROJECT_PREFIX = `${WEB_SERVICE_PREFIX}/project/project`;
|
||||||
|
|
||||||
type ProjectResponse = Omit<
|
export type ProjectResponse = Omit<
|
||||||
Api.Project.Project,
|
Api.Project.Project,
|
||||||
'id' | 'managerUserId' | 'productId' | 'plannedStartDate' | 'plannedEndDate' | 'actualStartDate' | 'actualEndDate'
|
'id' | 'managerUserId' | 'productId' | 'plannedStartDate' | 'plannedEndDate' | 'actualStartDate' | 'actualEndDate'
|
||||||
> & {
|
> & {
|
||||||
@@ -73,7 +87,7 @@ function getTaskPrefix(projectId: string, executionId: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** 归一化项目数据 */
|
/** 归一化项目数据 */
|
||||||
function normalizeProject(project: ProjectResponse): Api.Project.Project {
|
export function normalizeProject(project: ProjectResponse): Api.Project.Project {
|
||||||
return {
|
return {
|
||||||
...project,
|
...project,
|
||||||
id: normalizeStringId(project.id),
|
id: normalizeStringId(project.id),
|
||||||
@@ -130,13 +144,34 @@ export async function fetchGetProjectPage(params?: Api.Project.ProjectSearchPara
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ProjectOverviewSummaryResponse = Omit<Api.Project.ProjectOverviewSummary, 'total' | 'items'> & {
|
||||||
|
/** 后端 overview-summary 升级(total/items)灰度期间可能缺省,适配层兜底 */
|
||||||
|
total?: number | null;
|
||||||
|
items?: Api.Project.OverviewStatusItem[] | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 归一化项目概览统计:total/items 兜底,保证业务层拿到完整结构 */
|
||||||
|
function normalizeProjectOverviewSummary(data: ProjectOverviewSummaryResponse): Api.Project.ProjectOverviewSummary {
|
||||||
|
return {
|
||||||
|
...data,
|
||||||
|
statusCounts: data.statusCounts ?? {},
|
||||||
|
total: data.total ?? 0,
|
||||||
|
items: data.items ?? []
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/** 获取项目入口页概览统计 */
|
/** 获取项目入口页概览统计 */
|
||||||
export function fetchGetProjectOverviewSummary() {
|
export async function fetchGetProjectOverviewSummary() {
|
||||||
return request<Api.Project.ProjectOverviewSummary>({
|
const result = await request<ProjectOverviewSummaryResponse>({
|
||||||
...safeJsonRequestConfig,
|
...safeJsonRequestConfig,
|
||||||
url: `${PROJECT_PREFIX}/overview-summary`,
|
url: `${PROJECT_PREFIX}/overview-summary`,
|
||||||
method: 'get'
|
method: 'get'
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return mapServiceResult(
|
||||||
|
result as ServiceRequestResult<ProjectOverviewSummaryResponse>,
|
||||||
|
normalizeProjectOverviewSummary
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 获取项目详情 */
|
/** 获取项目详情 */
|
||||||
@@ -365,6 +400,105 @@ export async function fetchGetProjectExecutionPage(
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 获取工作台「我负责的执行」(跨项目聚合,owner 隐式取当前登录用户) */
|
||||||
|
export async function fetchGetMyExecutionPage(params?: Api.Project.MyExecutionSearchParams) {
|
||||||
|
type MyExecutionPageResponse = Api.Project.PageResult<MyExecutionResponse>;
|
||||||
|
const result = await request<MyExecutionPageResponse>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${PROJECT_PREFIX}/me/executions/page`,
|
||||||
|
method: 'get',
|
||||||
|
params
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapServiceResult(result as ServiceRequestResult<MyExecutionPageResponse>, data => ({
|
||||||
|
...data,
|
||||||
|
list: data.list.map(normalizeMyExecution)
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取工作台「我参与的项目」(成员视角,附我的角色与任务量;隐式取当前登录用户) */
|
||||||
|
export async function fetchGetMyParticipatedProjectPage(params?: Api.Project.MyProjectSearchParams) {
|
||||||
|
type MyParticipatedProjectPageResponse = Api.Project.PageResult<MyParticipatedProjectResponse>;
|
||||||
|
const result = await request<MyParticipatedProjectPageResponse>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${PROJECT_PREFIX}/me/participated/page`,
|
||||||
|
method: 'get',
|
||||||
|
params
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapServiceResult(result as ServiceRequestResult<MyParticipatedProjectPageResponse>, data => ({
|
||||||
|
...data,
|
||||||
|
list: data.list.map(normalizeMyParticipatedProject)
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取工作台「我负责的项目」(项目负责人视角,附聚合统计与成员负载;隐式取当前登录用户) */
|
||||||
|
export async function fetchGetMyOwnedProjectPage(params?: Api.Project.MyProjectSearchParams) {
|
||||||
|
type MyOwnedProjectPageResponse = Api.Project.PageResult<MyOwnedProjectResponse>;
|
||||||
|
const result = await request<MyOwnedProjectPageResponse>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${PROJECT_PREFIX}/me/owned/page`,
|
||||||
|
method: 'get',
|
||||||
|
params
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapServiceResult(result as ServiceRequestResult<MyOwnedProjectPageResponse>, data => ({
|
||||||
|
...data,
|
||||||
|
list: data.list.map(normalizeMyOwnedProject)
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取工作台「我的任务」(跨项目聚合,负责人/在岗协办人口径,只返回非终态;隐式取当前登录用户) */
|
||||||
|
export async function fetchGetMyTaskPage(params?: Api.Project.MyTaskSearchParams) {
|
||||||
|
type MyTaskPageResponse = Api.Project.PageResult<MyTaskResponse>;
|
||||||
|
const result = await request<MyTaskPageResponse>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${PROJECT_PREFIX}/me/tasks/page`,
|
||||||
|
method: 'get',
|
||||||
|
params
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapServiceResult(result as ServiceRequestResult<MyTaskPageResponse>, data => ({
|
||||||
|
...data,
|
||||||
|
list: data.list.map(normalizeMyTask)
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取工作台「团队负载」(团队 = 当前用户 + 管理链路直接下级,members[0] 恒为当前用户) */
|
||||||
|
export async function fetchGetMyTeamLoad() {
|
||||||
|
const result = await request<TeamLoadResponse>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${PROJECT_PREFIX}/me/team-load`,
|
||||||
|
method: 'get'
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapServiceResult(result as ServiceRequestResult<TeamLoadResponse>, normalizeTeamLoad);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取工作台「我的工时周聚合」(weekStart 传任意日期,后端归一到所在周周一;逐日工时为均摊推算值) */
|
||||||
|
export async function fetchGetMyWorklogWeek(params: Api.Project.WorklogWeekParams) {
|
||||||
|
const result = await request<MyWorklogWeekResponse>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${PROJECT_PREFIX}/me/worklog-week`,
|
||||||
|
method: 'get',
|
||||||
|
params
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapServiceResult(result as ServiceRequestResult<MyWorklogWeekResponse>, normalizeMyWorklogWeek);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取工作台「团队工时周聚合」(成员集合与团队负载同口径;周标准工时后端不返回,前端落常量) */
|
||||||
|
export async function fetchGetTeamWorklogWeek(params: Api.Project.WorklogWeekParams) {
|
||||||
|
const result = await request<TeamWorklogWeekResponse>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${PROJECT_PREFIX}/me/team-worklog-week`,
|
||||||
|
method: 'get',
|
||||||
|
params
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapServiceResult(result as ServiceRequestResult<TeamWorklogWeekResponse>, normalizeTeamWorklogWeek);
|
||||||
|
}
|
||||||
|
|
||||||
/** 获取项目执行状态看板 */
|
/** 获取项目执行状态看板 */
|
||||||
export function fetchGetProjectExecutionStatusBoard(
|
export function fetchGetProjectExecutionStatusBoard(
|
||||||
projectId: string,
|
projectId: string,
|
||||||
|
|||||||
914
src/service/api/work-report.ts
Normal file
914
src/service/api/work-report.ts
Normal file
@@ -0,0 +1,914 @@
|
|||||||
|
import { WEB_SERVICE_PREFIX } from '@/constants/service';
|
||||||
|
import { request } from '../request';
|
||||||
|
import {
|
||||||
|
type ServiceRequestResult,
|
||||||
|
mapServiceResult,
|
||||||
|
normalizeNullableStringId,
|
||||||
|
normalizeStringId,
|
||||||
|
safeJsonRequestConfig
|
||||||
|
} from './shared';
|
||||||
|
|
||||||
|
const WORK_REPORT_PREFIX = `${WEB_SERVICE_PREFIX}/project/work-reports`;
|
||||||
|
const WEEKLY_PREFIX = `${WORK_REPORT_PREFIX}/weekly`;
|
||||||
|
const MONTHLY_PREFIX = `${WORK_REPORT_PREFIX}/monthly`;
|
||||||
|
const PROJECT_PREFIX = `${WORK_REPORT_PREFIX}/project`;
|
||||||
|
|
||||||
|
type StringIdResponse = string | number;
|
||||||
|
type MaybeStringIdResponse = string | number | null | undefined;
|
||||||
|
|
||||||
|
type PageResponse<T> = {
|
||||||
|
total: number | string;
|
||||||
|
list: T[];
|
||||||
|
};
|
||||||
|
|
||||||
|
type ReviewItemResponse = Omit<Api.WorkReport.Common.PersonalReportReviewItem, 'id'> & {
|
||||||
|
id?: MaybeStringIdResponse;
|
||||||
|
};
|
||||||
|
|
||||||
|
type PlanItemResponse = Omit<Api.WorkReport.Common.PersonalReportPlanItem, 'id'> & {
|
||||||
|
id?: MaybeStringIdResponse;
|
||||||
|
};
|
||||||
|
|
||||||
|
type WeeklyTravelSegmentResponse = Omit<Api.WorkReport.Weekly.WeeklyReportTravelSegment, 'id'> & {
|
||||||
|
id?: MaybeStringIdResponse;
|
||||||
|
};
|
||||||
|
|
||||||
|
type WeeklyReportResponse = Omit<
|
||||||
|
Api.WorkReport.Weekly.WeeklyReport,
|
||||||
|
'id' | 'reporterId' | 'supervisorUserId' | 'reviewItems' | 'planItems' | 'travelSegments'
|
||||||
|
> & {
|
||||||
|
id: StringIdResponse;
|
||||||
|
reporterId: StringIdResponse;
|
||||||
|
supervisorUserId: StringIdResponse;
|
||||||
|
reviewItems?: ReviewItemResponse[] | null;
|
||||||
|
planItems?: PlanItemResponse[] | null;
|
||||||
|
travelSegments?: WeeklyTravelSegmentResponse[] | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
type MonthlyReportResponse = Omit<
|
||||||
|
Api.WorkReport.Monthly.MonthlyReport,
|
||||||
|
'id' | 'reporterId' | 'supervisorUserId' | 'reviewItems' | 'planItems'
|
||||||
|
> & {
|
||||||
|
id: StringIdResponse;
|
||||||
|
reporterId: StringIdResponse;
|
||||||
|
supervisorUserId: StringIdResponse;
|
||||||
|
reviewItems?: ReviewItemResponse[] | null;
|
||||||
|
planItems?: PlanItemResponse[] | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
type MemberSnapshotResponse = Omit<Api.WorkReport.Project.WorkReportMemberSnapshot, 'userId'> & {
|
||||||
|
userId: StringIdResponse;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ProjectReportItemResponse = Omit<Api.WorkReport.Project.ProjectReportItem, 'id'> & {
|
||||||
|
id?: MaybeStringIdResponse;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ProjectReportResponse = Omit<
|
||||||
|
Api.WorkReport.Project.ProjectReport,
|
||||||
|
'id' | 'projectId' | 'projectOwnerId' | 'projectMemberSnapshot' | 'supervisorUserId' | 'currentItems' | 'nextItems'
|
||||||
|
> & {
|
||||||
|
id: StringIdResponse;
|
||||||
|
projectId: StringIdResponse;
|
||||||
|
projectOwnerId: StringIdResponse;
|
||||||
|
projectMemberSnapshot?: MemberSnapshotResponse[] | null;
|
||||||
|
supervisorUserId: StringIdResponse;
|
||||||
|
currentItems?: ProjectReportItemResponse[] | null;
|
||||||
|
nextItems?: ProjectReportItemResponse[] | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ApprovalRecordResponse = Omit<
|
||||||
|
Api.WorkReport.Common.WorkReportApprovalRecord,
|
||||||
|
'id' | 'statusLogId' | 'auditorUserId'
|
||||||
|
> & {
|
||||||
|
id: StringIdResponse;
|
||||||
|
statusLogId: StringIdResponse;
|
||||||
|
auditorUserId: StringIdResponse;
|
||||||
|
};
|
||||||
|
|
||||||
|
type MonthlyApprovalRecordResponse = Omit<
|
||||||
|
Api.WorkReport.Monthly.MonthlyReportApprovalRecord,
|
||||||
|
'id' | 'statusLogId' | 'auditorUserId'
|
||||||
|
> & {
|
||||||
|
id: StringIdResponse;
|
||||||
|
statusLogId: StringIdResponse;
|
||||||
|
auditorUserId: StringIdResponse;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ProjectOptionResponse = Omit<Api.WorkReport.Project.ProjectReportOwnerProjectOption, 'id'> & {
|
||||||
|
id: StringIdResponse;
|
||||||
|
};
|
||||||
|
|
||||||
|
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 normalizeApprovalConclusion(value: unknown) {
|
||||||
|
const conclusion = String(value || '')
|
||||||
|
.trim()
|
||||||
|
.toLowerCase();
|
||||||
|
|
||||||
|
if (conclusion === 'approve') return 'approved';
|
||||||
|
if (conclusion === 'reject') return 'rejected';
|
||||||
|
|
||||||
|
return conclusion;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeDateText(value: unknown) {
|
||||||
|
if (value === null || value === undefined) return undefined;
|
||||||
|
const text = String(value).trim();
|
||||||
|
const commaDateMatch = text.match(/^(\d{4}),(\d{1,2}),(\d{1,2})$/);
|
||||||
|
|
||||||
|
if (commaDateMatch) {
|
||||||
|
const [, year, month, day] = commaDateMatch;
|
||||||
|
return `${year}-${month.padStart(2, '0')}-${day.padStart(2, '0')}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return text || undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeTotal(total: number | string) {
|
||||||
|
const value = Number(total);
|
||||||
|
return Number.isFinite(value) ? Math.max(0, value) : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function sumWorkHours(items: Array<{ workHours?: number | string | null }> = []) {
|
||||||
|
return items.reduce((sum, item) => {
|
||||||
|
const value = Number(item.workHours ?? 0);
|
||||||
|
return Number.isFinite(value) ? sum + value : sum;
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeReportTotalWorkHours(
|
||||||
|
totalWorkHours: number | string | null | undefined,
|
||||||
|
fallbackTotalWorkHours: number
|
||||||
|
) {
|
||||||
|
const normalizedTotal = Number(totalWorkHours ?? 0);
|
||||||
|
|
||||||
|
if (
|
||||||
|
(totalWorkHours === null ||
|
||||||
|
totalWorkHours === undefined ||
|
||||||
|
totalWorkHours === '' ||
|
||||||
|
(Number.isFinite(normalizedTotal) && normalizedTotal === 0)) &&
|
||||||
|
fallbackTotalWorkHours > 0
|
||||||
|
) {
|
||||||
|
return fallbackTotalWorkHours;
|
||||||
|
}
|
||||||
|
|
||||||
|
return totalWorkHours ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function appendValue(query: URLSearchParams, key: string, value: unknown) {
|
||||||
|
if (value === null || value === undefined || value === '') return;
|
||||||
|
query.append(key, String(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
function appendArray(query: URLSearchParams, key: string, values?: Array<string | null | undefined> | null) {
|
||||||
|
values?.forEach(value => appendValue(query, key, value));
|
||||||
|
}
|
||||||
|
|
||||||
|
function createBasePageQuery(params: Api.WorkReport.Common.WorkReportBaseSearchParams = {}) {
|
||||||
|
const query = new URLSearchParams();
|
||||||
|
|
||||||
|
appendValue(query, 'pageNo', params.pageNo ?? 1);
|
||||||
|
appendValue(query, 'pageSize', params.pageSize ?? 10);
|
||||||
|
appendValue(query, 'keyword', params.keyword);
|
||||||
|
appendValue(query, 'statusCode', params.statusCode);
|
||||||
|
appendValue(query, 'supervisorName', params.supervisorName);
|
||||||
|
appendArray(query, 'periodStartDate', params.periodStartDate);
|
||||||
|
appendArray(query, 'submitTime', params.submitTime);
|
||||||
|
|
||||||
|
return query;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createWeeklyPageQuery(params: Api.WorkReport.Weekly.WeeklyReportSearchParams = {}) {
|
||||||
|
const query = createBasePageQuery(params);
|
||||||
|
appendValue(query, 'isBusinessTrip', params.isBusinessTrip);
|
||||||
|
return query.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
function createMonthlyPageQuery(params: Api.WorkReport.Monthly.MonthlyReportSearchParams = {}) {
|
||||||
|
return createBasePageQuery(params).toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
function createProjectPageQuery(params: Api.WorkReport.Project.ProjectReportSearchParams = {}) {
|
||||||
|
const query = createBasePageQuery(params);
|
||||||
|
appendValue(query, 'projectId', params.projectId);
|
||||||
|
appendValue(query, 'flag', params.flag);
|
||||||
|
return query.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeReviewItem(item: ReviewItemResponse): Api.WorkReport.Common.PersonalReportReviewItem {
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
id: normalizeNullableStringId(item.id) ?? undefined
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizePlanItem(item: PlanItemResponse): Api.WorkReport.Common.PersonalReportPlanItem {
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
id: normalizeNullableStringId(item.id) ?? undefined
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeWeeklyTravelSegment(
|
||||||
|
item: WeeklyTravelSegmentResponse
|
||||||
|
): Api.WorkReport.Weekly.WeeklyReportTravelSegment {
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
id: normalizeNullableStringId(item.id) ?? undefined,
|
||||||
|
startDate: normalizeDateText(item.startDate),
|
||||||
|
endDate: normalizeDateText(item.endDate)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeWeeklyReport(response: WeeklyReportResponse): Api.WorkReport.Weekly.WeeklyReport {
|
||||||
|
const fallbackTotalWorkHours = sumWorkHours(response.reviewItems ?? []);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...response,
|
||||||
|
id: normalizeStringId(response.id),
|
||||||
|
reporterId: normalizeStringId(response.reporterId),
|
||||||
|
supervisorUserId: normalizeStringId(response.supervisorUserId),
|
||||||
|
reporterDeptName: response.reporterDeptName ?? null,
|
||||||
|
reporterPostName: response.reporterPostName ?? null,
|
||||||
|
statusName: response.statusName || response.statusCode,
|
||||||
|
allowEdit: normalizeBooleanFlag(response.allowEdit),
|
||||||
|
terminal: normalizeBooleanFlag(response.terminal),
|
||||||
|
isBusinessTrip: normalizeBooleanFlag(response.isBusinessTrip),
|
||||||
|
totalWorkHours: normalizeReportTotalWorkHours(response.totalWorkHours, fallbackTotalWorkHours),
|
||||||
|
submitTime: response.submitTime ?? null,
|
||||||
|
reviewItems: response.reviewItems?.map(normalizeReviewItem) ?? [],
|
||||||
|
planItems: response.planItems?.map(normalizePlanItem) ?? [],
|
||||||
|
travelSegments: response.travelSegments?.map(normalizeWeeklyTravelSegment) ?? []
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeMonthlyReport(response: MonthlyReportResponse): Api.WorkReport.Monthly.MonthlyReport {
|
||||||
|
const fallbackTotalWorkHours = sumWorkHours(response.reviewItems ?? []);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...response,
|
||||||
|
id: normalizeStringId(response.id),
|
||||||
|
reporterId: normalizeStringId(response.reporterId),
|
||||||
|
supervisorUserId: normalizeStringId(response.supervisorUserId),
|
||||||
|
reporterDeptName: response.reporterDeptName ?? null,
|
||||||
|
reporterPostName: response.reporterPostName ?? null,
|
||||||
|
statusName: response.statusName || response.statusCode,
|
||||||
|
allowEdit: normalizeBooleanFlag(response.allowEdit),
|
||||||
|
terminal: normalizeBooleanFlag(response.terminal),
|
||||||
|
totalWorkHours: normalizeReportTotalWorkHours(response.totalWorkHours, fallbackTotalWorkHours),
|
||||||
|
submitTime: response.submitTime ?? null,
|
||||||
|
reviewItems: response.reviewItems?.map(normalizeReviewItem) ?? [],
|
||||||
|
planItems: response.planItems?.map(normalizePlanItem) ?? []
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeMemberSnapshot(item: MemberSnapshotResponse): Api.WorkReport.Project.WorkReportMemberSnapshot {
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
userId: normalizeStringId(item.userId)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeProjectReportItem(item: ProjectReportItemResponse): Api.WorkReport.Project.ProjectReportItem {
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
id: normalizeNullableStringId(item.id) ?? undefined
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeProjectReport(response: ProjectReportResponse): Api.WorkReport.Project.ProjectReport {
|
||||||
|
const fallbackTotalWorkHours = sumWorkHours(response.currentItems ?? []);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...response,
|
||||||
|
id: normalizeStringId(response.id),
|
||||||
|
projectId: normalizeStringId(response.projectId),
|
||||||
|
projectOwnerId: normalizeStringId(response.projectOwnerId),
|
||||||
|
projectMemberSnapshot: response.projectMemberSnapshot?.map(normalizeMemberSnapshot) ?? [],
|
||||||
|
supervisorUserId: normalizeStringId(response.supervisorUserId),
|
||||||
|
statusName: response.statusName || response.statusCode,
|
||||||
|
allowEdit: normalizeBooleanFlag(response.allowEdit),
|
||||||
|
terminal: normalizeBooleanFlag(response.terminal),
|
||||||
|
totalWorkHours: normalizeReportTotalWorkHours(response.totalWorkHours, fallbackTotalWorkHours),
|
||||||
|
submitTime: response.submitTime ?? null,
|
||||||
|
currentItems: response.currentItems?.map(normalizeProjectReportItem) ?? [],
|
||||||
|
nextItems: response.nextItems?.map(normalizeProjectReportItem) ?? []
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeApprovalRecord(response: ApprovalRecordResponse): Api.WorkReport.Common.WorkReportApprovalRecord {
|
||||||
|
return {
|
||||||
|
...response,
|
||||||
|
id: normalizeStringId(response.id),
|
||||||
|
statusLogId: normalizeStringId(response.statusLogId),
|
||||||
|
auditorUserId: normalizeStringId(response.auditorUserId),
|
||||||
|
conclusion: normalizeApprovalConclusion(response.conclusion),
|
||||||
|
opinion: response.opinion ?? null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeMonthlyApprovalRecord(
|
||||||
|
response: MonthlyApprovalRecordResponse
|
||||||
|
): Api.WorkReport.Monthly.MonthlyReportApprovalRecord {
|
||||||
|
return {
|
||||||
|
...response,
|
||||||
|
id: normalizeStringId(response.id),
|
||||||
|
statusLogId: normalizeStringId(response.statusLogId),
|
||||||
|
auditorUserId: normalizeStringId(response.auditorUserId),
|
||||||
|
conclusion: normalizeApprovalConclusion(response.conclusion),
|
||||||
|
opinion: response.opinion ?? null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeProjectOption(
|
||||||
|
response: ProjectOptionResponse
|
||||||
|
): Api.WorkReport.Project.ProjectReportOwnerProjectOption {
|
||||||
|
return {
|
||||||
|
...response,
|
||||||
|
id: normalizeStringId(response.id)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapPage<TInput, TOutput>(data: PageResponse<TInput>, mapper: (item: TInput) => TOutput) {
|
||||||
|
return {
|
||||||
|
total: normalizeTotal(data.total),
|
||||||
|
list: data.list.map(mapper)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function toStatusActionRequest(data: Api.WorkReport.Common.StatusActionParams = {}) {
|
||||||
|
return {
|
||||||
|
reason: data.reason?.trim() || undefined
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function toPersonalReviewItems(items: Api.WorkReport.Common.PersonalReportReviewItem[] = []) {
|
||||||
|
return items.map((item, index) => ({
|
||||||
|
itemNumber: item.itemNumber ?? index + 1,
|
||||||
|
itemTitle: item.itemTitle?.trim() || '',
|
||||||
|
workHours: item.workHours ?? 0,
|
||||||
|
contentText: item.contentText?.trim() || '',
|
||||||
|
contentJson: item.contentJson ?? null,
|
||||||
|
reflectionText: item.reflectionText?.trim() || ''
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
function toPersonalPlanItems(items: Api.WorkReport.Common.PersonalReportPlanItem[] = []) {
|
||||||
|
return items.map((item, index) => ({
|
||||||
|
itemNumber: item.itemNumber ?? index + 1,
|
||||||
|
itemTitle: item.itemTitle?.trim() || '',
|
||||||
|
targetText: item.targetText?.trim() || '',
|
||||||
|
targetJson: item.targetJson ?? null,
|
||||||
|
supportNeed: item.supportNeed?.trim() || ''
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
function toWeeklySaveRequest(data: Api.WorkReport.Weekly.WeeklyReportSaveParams) {
|
||||||
|
return {
|
||||||
|
periodKey: data.periodKey,
|
||||||
|
periodLabel: data.periodLabel,
|
||||||
|
periodStartDate: data.periodStartDate,
|
||||||
|
periodEndDate: data.periodEndDate,
|
||||||
|
isBusinessTrip: data.isBusinessTrip,
|
||||||
|
reviewItems: toPersonalReviewItems(data.reviewItems),
|
||||||
|
planItems: toPersonalPlanItems(data.planItems),
|
||||||
|
travelSegments: data.isBusinessTrip
|
||||||
|
? data.travelSegments.map((item, index) => ({
|
||||||
|
sort: item.sort ?? index + 1,
|
||||||
|
startDate: item.startDate || undefined,
|
||||||
|
endDate: item.endDate || undefined,
|
||||||
|
travelDays: item.travelDays ?? 0,
|
||||||
|
location: item.location?.trim() || ''
|
||||||
|
}))
|
||||||
|
: []
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function toMonthlySaveRequest(data: Api.WorkReport.Monthly.MonthlyReportSaveParams) {
|
||||||
|
return {
|
||||||
|
periodKey: data.periodKey,
|
||||||
|
periodLabel: data.periodLabel,
|
||||||
|
periodStartDate: data.periodStartDate,
|
||||||
|
periodEndDate: data.periodEndDate,
|
||||||
|
reviewItems: toPersonalReviewItems(data.reviewItems),
|
||||||
|
planItems: toPersonalPlanItems(data.planItems)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function toProjectItems(items: Api.WorkReport.Project.ProjectReportItem[] = []) {
|
||||||
|
return items.map(item => ({
|
||||||
|
itemTitle: item.itemTitle?.trim() || '',
|
||||||
|
workHours: item.workHours ?? 0,
|
||||||
|
priorityCode: item.priorityCode || undefined,
|
||||||
|
progressRate: item.progressRate ?? 0
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
function toProjectSaveRequest(data: Api.WorkReport.Project.ProjectReportSaveParams) {
|
||||||
|
return {
|
||||||
|
projectId: data.projectId,
|
||||||
|
periodKey: data.periodKey,
|
||||||
|
periodLabel: data.periodLabel,
|
||||||
|
periodStartDate: data.periodStartDate,
|
||||||
|
periodEndDate: data.periodEndDate,
|
||||||
|
flag: data.flag,
|
||||||
|
projectStatusDesc: data.projectStatusDesc?.trim() || '',
|
||||||
|
projectProgressPlan: data.projectProgressPlan?.trim() || '',
|
||||||
|
projectKeyPoints: data.projectKeyPoints?.trim() || '',
|
||||||
|
projectProblems: data.projectProblems?.trim() || '',
|
||||||
|
currentItems: toProjectItems(data.currentItems),
|
||||||
|
nextItems: toProjectItems(data.nextItems)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchGetWorkReportStatusDict() {
|
||||||
|
const result = await request<Api.WorkReport.Common.WorkReportStatusDict[]>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${WORK_REPORT_PREFIX}/status/dict`,
|
||||||
|
method: 'get'
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapServiceResult(result as ServiceRequestResult<Api.WorkReport.Common.WorkReportStatusDict[]>, data => data);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchGetWeeklyReportPage(params: Api.WorkReport.Weekly.WeeklyReportSearchParams = {}) {
|
||||||
|
const query = createWeeklyPageQuery(params);
|
||||||
|
const result = await request<PageResponse<WeeklyReportResponse>>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: query ? `${WEEKLY_PREFIX}/page?${query}` : `${WEEKLY_PREFIX}/page`,
|
||||||
|
method: 'get'
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapServiceResult(result as ServiceRequestResult<PageResponse<WeeklyReportResponse>>, data =>
|
||||||
|
mapPage(data, normalizeWeeklyReport)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchGetWeeklyReportApprovalPage(params: Api.WorkReport.Weekly.WeeklyReportSearchParams = {}) {
|
||||||
|
const query = createWeeklyPageQuery(params);
|
||||||
|
const result = await request<PageResponse<WeeklyReportResponse>>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: query ? `${WEEKLY_PREFIX}/approval-page?${query}` : `${WEEKLY_PREFIX}/approval-page`,
|
||||||
|
method: 'get'
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapServiceResult(result as ServiceRequestResult<PageResponse<WeeklyReportResponse>>, data =>
|
||||||
|
mapPage(data, normalizeWeeklyReport)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchGetWeeklyReportDetail(id: string) {
|
||||||
|
const result = await request<WeeklyReportResponse>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${WEEKLY_PREFIX}/${id}`,
|
||||||
|
method: 'get'
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapServiceResult(result as ServiceRequestResult<WeeklyReportResponse>, normalizeWeeklyReport);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchInitWeeklyReport() {
|
||||||
|
const result = await request<WeeklyReportResponse>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${WEEKLY_PREFIX}/init`,
|
||||||
|
method: 'get'
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapServiceResult(result as ServiceRequestResult<WeeklyReportResponse>, normalizeWeeklyReport);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchPreviewWeeklyReportDefaultDraft(
|
||||||
|
params: Api.WorkReport.Weekly.WeeklyReportDefaultDraftParams
|
||||||
|
) {
|
||||||
|
const result = await request<WeeklyReportResponse>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${WEEKLY_PREFIX}/default-draft`,
|
||||||
|
method: 'get',
|
||||||
|
params
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapServiceResult(result as ServiceRequestResult<WeeklyReportResponse>, normalizeWeeklyReport);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchRefreshWeeklyReportDraft(data: Api.WorkReport.Weekly.WeeklyReportRefreshDraftParams) {
|
||||||
|
const result = await request<WeeklyReportResponse>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${WEEKLY_PREFIX}/refresh-draft`,
|
||||||
|
method: 'post',
|
||||||
|
data: toWeeklySaveRequest(data)
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapServiceResult(result as ServiceRequestResult<WeeklyReportResponse>, normalizeWeeklyReport);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchCreateWeeklyReport(data: Api.WorkReport.Weekly.WeeklyReportSaveParams) {
|
||||||
|
const result = await request<StringIdResponse>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: WEEKLY_PREFIX,
|
||||||
|
method: 'post',
|
||||||
|
data: toWeeklySaveRequest(data)
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapServiceResult(result as ServiceRequestResult<StringIdResponse>, normalizeStringId);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchUpdateWeeklyReport(id: string, data: Api.WorkReport.Weekly.WeeklyReportSaveParams) {
|
||||||
|
return request<boolean>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${WEEKLY_PREFIX}/${id}`,
|
||||||
|
method: 'put',
|
||||||
|
data: toWeeklySaveRequest(data)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchSubmitWeeklyReport(id: string) {
|
||||||
|
return request<boolean>({ ...safeJsonRequestConfig, url: `${WEEKLY_PREFIX}/${id}/submit`, method: 'post' });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchApproveWeeklyReport(id: string, data: Api.WorkReport.Common.StatusActionParams = {}) {
|
||||||
|
return request<boolean>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${WEEKLY_PREFIX}/${id}/approve`,
|
||||||
|
method: 'post',
|
||||||
|
data: toStatusActionRequest(data)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchRejectWeeklyReport(id: string, data: Api.WorkReport.Common.StatusActionParams) {
|
||||||
|
return request<boolean>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${WEEKLY_PREFIX}/${id}/reject`,
|
||||||
|
method: 'post',
|
||||||
|
data: toStatusActionRequest(data)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchDeleteWeeklyReport(id: string) {
|
||||||
|
return request<boolean>({ ...safeJsonRequestConfig, url: `${WEEKLY_PREFIX}/${id}`, method: 'delete' });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchGetWeeklyReportApprovalRecords(id: string) {
|
||||||
|
const result = await request<ApprovalRecordResponse[]>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${WEEKLY_PREFIX}/${id}/approval-records`,
|
||||||
|
method: 'get'
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapServiceResult(result as ServiceRequestResult<ApprovalRecordResponse[]>, data =>
|
||||||
|
data.map(normalizeApprovalRecord)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchExportWeeklyReports(params: Api.WorkReport.Weekly.WeeklyReportSearchParams = {}) {
|
||||||
|
const query = createWeeklyPageQuery(params);
|
||||||
|
return request<Blob, 'blob'>({
|
||||||
|
url: query ? `${WEEKLY_PREFIX}/export?${query}` : `${WEEKLY_PREFIX}/export`,
|
||||||
|
method: 'get',
|
||||||
|
responseType: 'blob'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchExportWeeklyReportContent(
|
||||||
|
data: Api.WorkReport.Common.ContentExportParams<Api.WorkReport.Weekly.WeeklyReportSearchParams>
|
||||||
|
) {
|
||||||
|
return request<Blob, 'blob'>({
|
||||||
|
url: `${WEEKLY_PREFIX}/content-export`,
|
||||||
|
method: 'post',
|
||||||
|
data,
|
||||||
|
responseType: 'blob'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchGetMonthlyReportPage(params: Api.WorkReport.Monthly.MonthlyReportSearchParams = {}) {
|
||||||
|
const query = createMonthlyPageQuery(params);
|
||||||
|
const result = await request<PageResponse<MonthlyReportResponse>>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: query ? `${MONTHLY_PREFIX}/page?${query}` : `${MONTHLY_PREFIX}/page`,
|
||||||
|
method: 'get'
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapServiceResult(result as ServiceRequestResult<PageResponse<MonthlyReportResponse>>, data =>
|
||||||
|
mapPage(data, normalizeMonthlyReport)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchGetMonthlyReportApprovalPage(params: Api.WorkReport.Monthly.MonthlyReportSearchParams = {}) {
|
||||||
|
const query = createMonthlyPageQuery(params);
|
||||||
|
const result = await request<PageResponse<MonthlyReportResponse>>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: query ? `${MONTHLY_PREFIX}/approval-page?${query}` : `${MONTHLY_PREFIX}/approval-page`,
|
||||||
|
method: 'get'
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapServiceResult(result as ServiceRequestResult<PageResponse<MonthlyReportResponse>>, data =>
|
||||||
|
mapPage(data, normalizeMonthlyReport)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchGetMonthlyReportDetail(id: string) {
|
||||||
|
const result = await request<MonthlyReportResponse>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${MONTHLY_PREFIX}/${id}`,
|
||||||
|
method: 'get'
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapServiceResult(result as ServiceRequestResult<MonthlyReportResponse>, normalizeMonthlyReport);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchInitMonthlyReport() {
|
||||||
|
const result = await request<MonthlyReportResponse>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${MONTHLY_PREFIX}/init`,
|
||||||
|
method: 'get'
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapServiceResult(result as ServiceRequestResult<MonthlyReportResponse>, normalizeMonthlyReport);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchPreviewMonthlyReportDefaultDraft(
|
||||||
|
params: Api.WorkReport.Monthly.MonthlyReportDefaultDraftParams
|
||||||
|
) {
|
||||||
|
const result = await request<MonthlyReportResponse>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${MONTHLY_PREFIX}/default-draft`,
|
||||||
|
method: 'get',
|
||||||
|
params
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapServiceResult(result as ServiceRequestResult<MonthlyReportResponse>, normalizeMonthlyReport);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchRefreshMonthlyReportDraft(data: Api.WorkReport.Monthly.MonthlyReportRefreshDraftParams) {
|
||||||
|
const result = await request<MonthlyReportResponse>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${MONTHLY_PREFIX}/refresh-draft`,
|
||||||
|
method: 'post',
|
||||||
|
data: toMonthlySaveRequest(data)
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapServiceResult(result as ServiceRequestResult<MonthlyReportResponse>, normalizeMonthlyReport);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchCreateMonthlyReport(data: Api.WorkReport.Monthly.MonthlyReportSaveParams) {
|
||||||
|
const result = await request<StringIdResponse>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: MONTHLY_PREFIX,
|
||||||
|
method: 'post',
|
||||||
|
data: toMonthlySaveRequest(data)
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapServiceResult(result as ServiceRequestResult<StringIdResponse>, normalizeStringId);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchUpdateMonthlyReport(id: string, data: Api.WorkReport.Monthly.MonthlyReportSaveParams) {
|
||||||
|
return request<boolean>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${MONTHLY_PREFIX}/${id}`,
|
||||||
|
method: 'put',
|
||||||
|
data: toMonthlySaveRequest(data)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchSubmitMonthlyReport(id: string) {
|
||||||
|
return request<boolean>({ ...safeJsonRequestConfig, url: `${MONTHLY_PREFIX}/${id}/submit`, method: 'post' });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchApproveMonthlyReport(id: string, data: Api.WorkReport.Monthly.MonthlyReportApproveParams) {
|
||||||
|
return request<boolean>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${MONTHLY_PREFIX}/${id}/approve`,
|
||||||
|
method: 'post',
|
||||||
|
data
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchRejectMonthlyReport(id: string, data: Api.WorkReport.Common.StatusActionParams) {
|
||||||
|
return request<boolean>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${MONTHLY_PREFIX}/${id}/reject`,
|
||||||
|
method: 'post',
|
||||||
|
data: toStatusActionRequest(data)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchDeleteMonthlyReport(id: string) {
|
||||||
|
return request<boolean>({ ...safeJsonRequestConfig, url: `${MONTHLY_PREFIX}/${id}`, method: 'delete' });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchGetMonthlyReportApprovalRecords(id: string) {
|
||||||
|
const result = await request<MonthlyApprovalRecordResponse[]>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${MONTHLY_PREFIX}/${id}/approval-records`,
|
||||||
|
method: 'get'
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapServiceResult(result as ServiceRequestResult<MonthlyApprovalRecordResponse[]>, data =>
|
||||||
|
data.map(normalizeMonthlyApprovalRecord)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchExportMonthlyReports(params: Api.WorkReport.Monthly.MonthlyReportSearchParams = {}) {
|
||||||
|
const query = createMonthlyPageQuery(params);
|
||||||
|
return request<Blob, 'blob'>({
|
||||||
|
url: query ? `${MONTHLY_PREFIX}/export?${query}` : `${MONTHLY_PREFIX}/export`,
|
||||||
|
method: 'get',
|
||||||
|
responseType: 'blob'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchExportMonthlyReportContent(
|
||||||
|
data: Api.WorkReport.Common.ContentExportParams<Api.WorkReport.Monthly.MonthlyReportSearchParams>
|
||||||
|
) {
|
||||||
|
return request<Blob, 'blob'>({
|
||||||
|
url: `${MONTHLY_PREFIX}/content-export`,
|
||||||
|
method: 'post',
|
||||||
|
data,
|
||||||
|
responseType: 'blob'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchGetProjectReportOwnerProjectOptions() {
|
||||||
|
const result = await request<ProjectOptionResponse[]>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${PROJECT_PREFIX}/owner-project-options`,
|
||||||
|
method: 'get'
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapServiceResult(result as ServiceRequestResult<ProjectOptionResponse[]>, data =>
|
||||||
|
data.map(normalizeProjectOption)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchGetProjectReportPage(params: Api.WorkReport.Project.ProjectReportSearchParams = {}) {
|
||||||
|
const query = createProjectPageQuery(params);
|
||||||
|
const result = await request<PageResponse<ProjectReportResponse>>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: query ? `${PROJECT_PREFIX}/page?${query}` : `${PROJECT_PREFIX}/page`,
|
||||||
|
method: 'get'
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapServiceResult(result as ServiceRequestResult<PageResponse<ProjectReportResponse>>, data =>
|
||||||
|
mapPage(data, normalizeProjectReport)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchGetProjectReportApprovalPage(params: Api.WorkReport.Project.ProjectReportSearchParams = {}) {
|
||||||
|
const query = createProjectPageQuery(params);
|
||||||
|
const result = await request<PageResponse<ProjectReportResponse>>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: query ? `${PROJECT_PREFIX}/approval-page?${query}` : `${PROJECT_PREFIX}/approval-page`,
|
||||||
|
method: 'get'
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapServiceResult(result as ServiceRequestResult<PageResponse<ProjectReportResponse>>, data =>
|
||||||
|
mapPage(data, normalizeProjectReport)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchGetProjectReportDetail(id: string) {
|
||||||
|
const result = await request<ProjectReportResponse>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${PROJECT_PREFIX}/${id}`,
|
||||||
|
method: 'get'
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapServiceResult(result as ServiceRequestResult<ProjectReportResponse>, normalizeProjectReport);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchInitProjectReport(projectId: string) {
|
||||||
|
const result = await request<ProjectReportResponse>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${PROJECT_PREFIX}/init`,
|
||||||
|
method: 'get',
|
||||||
|
params: { projectId }
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapServiceResult(result as ServiceRequestResult<ProjectReportResponse>, normalizeProjectReport);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchPreviewProjectReportDefaultDraft(
|
||||||
|
projectId: string,
|
||||||
|
params: Api.WorkReport.Project.ProjectReportDefaultDraftParams
|
||||||
|
) {
|
||||||
|
const result = await request<ProjectReportResponse>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${PROJECT_PREFIX}/${projectId}/default-draft`,
|
||||||
|
method: 'get',
|
||||||
|
params
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapServiceResult(result as ServiceRequestResult<ProjectReportResponse>, normalizeProjectReport);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchRefreshProjectReportDraft(
|
||||||
|
projectId: string,
|
||||||
|
data: Api.WorkReport.Project.ProjectReportRefreshDraftParams
|
||||||
|
) {
|
||||||
|
const result = await request<ProjectReportResponse>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${PROJECT_PREFIX}/${projectId}/refresh-draft`,
|
||||||
|
method: 'post',
|
||||||
|
data: {
|
||||||
|
periodKey: data.periodKey,
|
||||||
|
periodLabel: data.periodLabel,
|
||||||
|
periodStartDate: data.periodStartDate,
|
||||||
|
periodEndDate: data.periodEndDate,
|
||||||
|
flag: data.flag,
|
||||||
|
projectStatusDesc: data.projectStatusDesc?.trim() || '',
|
||||||
|
projectProgressPlan: data.projectProgressPlan?.trim() || '',
|
||||||
|
projectKeyPoints: data.projectKeyPoints?.trim() || '',
|
||||||
|
projectProblems: data.projectProblems?.trim() || '',
|
||||||
|
currentItems: toProjectItems(data.currentItems),
|
||||||
|
nextItems: toProjectItems(data.nextItems)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapServiceResult(result as ServiceRequestResult<ProjectReportResponse>, normalizeProjectReport);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchCreateProjectReport(data: Api.WorkReport.Project.ProjectReportSaveParams) {
|
||||||
|
const result = await request<StringIdResponse>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: PROJECT_PREFIX,
|
||||||
|
method: 'post',
|
||||||
|
data: toProjectSaveRequest(data)
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapServiceResult(result as ServiceRequestResult<StringIdResponse>, normalizeStringId);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchUpdateProjectReport(id: string, data: Api.WorkReport.Project.ProjectReportSaveParams) {
|
||||||
|
return request<boolean>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${PROJECT_PREFIX}/${id}`,
|
||||||
|
method: 'put',
|
||||||
|
data: toProjectSaveRequest(data)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchSubmitProjectReport(id: string) {
|
||||||
|
return request<boolean>({ ...safeJsonRequestConfig, url: `${PROJECT_PREFIX}/${id}/submit`, method: 'post' });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchApproveProjectReport(id: string, data: Api.WorkReport.Common.StatusActionParams = {}) {
|
||||||
|
return request<boolean>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${PROJECT_PREFIX}/${id}/approve`,
|
||||||
|
method: 'post',
|
||||||
|
data: toStatusActionRequest(data)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchRejectProjectReport(id: string, data: Api.WorkReport.Common.StatusActionParams) {
|
||||||
|
return request<boolean>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${PROJECT_PREFIX}/${id}/reject`,
|
||||||
|
method: 'post',
|
||||||
|
data: toStatusActionRequest(data)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchDeleteProjectReport(id: string) {
|
||||||
|
return request<boolean>({ ...safeJsonRequestConfig, url: `${PROJECT_PREFIX}/${id}`, method: 'delete' });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchGetProjectReportApprovalRecords(id: string) {
|
||||||
|
const result = await request<ApprovalRecordResponse[]>({
|
||||||
|
...safeJsonRequestConfig,
|
||||||
|
url: `${PROJECT_PREFIX}/${id}/approval-records`,
|
||||||
|
method: 'get'
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapServiceResult(result as ServiceRequestResult<ApprovalRecordResponse[]>, data =>
|
||||||
|
data.map(normalizeApprovalRecord)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchExportProjectReports(params: Api.WorkReport.Project.ProjectReportSearchParams = {}) {
|
||||||
|
const query = createProjectPageQuery(params);
|
||||||
|
return request<Blob, 'blob'>({
|
||||||
|
url: query ? `${PROJECT_PREFIX}/export?${query}` : `${PROJECT_PREFIX}/export`,
|
||||||
|
method: 'get',
|
||||||
|
responseType: 'blob'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchExportProjectReportContent(
|
||||||
|
data: Api.WorkReport.Common.ContentExportParams<Api.WorkReport.Project.ProjectReportSearchParams>
|
||||||
|
) {
|
||||||
|
return request<Blob, 'blob'>({
|
||||||
|
url: `${PROJECT_PREFIX}/content-export`,
|
||||||
|
method: 'post',
|
||||||
|
data,
|
||||||
|
responseType: 'blob'
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -12,11 +12,13 @@ import type { RequestInstanceState } from './type';
|
|||||||
|
|
||||||
const isHttpProxy = import.meta.env.DEV && import.meta.env.VITE_HTTP_PROXY === 'Y';
|
const isHttpProxy = import.meta.env.DEV && import.meta.env.VITE_HTTP_PROXY === 'Y';
|
||||||
const { baseURL, otherBaseURL } = getServiceBaseURL(import.meta.env, isHttpProxy);
|
const { baseURL, otherBaseURL } = getServiceBaseURL(import.meta.env, isHttpProxy);
|
||||||
|
const REQUEST_TIMEOUT = 15 * 1000;
|
||||||
|
|
||||||
export const request = withDedupe(
|
export const request = withDedupe(
|
||||||
createFlatRequest(
|
createFlatRequest(
|
||||||
{
|
{
|
||||||
baseURL,
|
baseURL,
|
||||||
|
timeout: REQUEST_TIMEOUT,
|
||||||
headers: {
|
headers: {
|
||||||
apifoxToken: 'XL299LiMEDZ0H5h3A29PxwQXdMJqWyY2'
|
apifoxToken: 'XL299LiMEDZ0H5h3A29PxwQXdMJqWyY2'
|
||||||
}
|
}
|
||||||
@@ -126,6 +128,10 @@ export const request = withDedupe(
|
|||||||
let message = error.message;
|
let message = error.message;
|
||||||
let backendErrorCode = '';
|
let backendErrorCode = '';
|
||||||
|
|
||||||
|
if (error.code === 'ECONNABORTED' && error.message.includes('timeout')) {
|
||||||
|
message = '请求超时,请稍后重试';
|
||||||
|
}
|
||||||
|
|
||||||
// 获取后端错误信息和错误码
|
// 获取后端错误信息和错误码
|
||||||
if (error.code === BACKEND_ERROR_CODE) {
|
if (error.code === BACKEND_ERROR_CODE) {
|
||||||
message = error.response?.data?.msg || message;
|
message = error.response?.data?.msg || message;
|
||||||
|
|||||||
@@ -131,6 +131,12 @@ export const useAuthStore = defineStore(SetupStoreId.Auth, () => {
|
|||||||
// If the tab needs to be cleared,it means we don't need to redirect.
|
// If the tab needs to be cleared,it means we don't need to redirect.
|
||||||
needRedirect = false;
|
needRedirect = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 跳首页前先把权限路由建好:菜单/路由/首页 redirect 全部就绪后再导航,
|
||||||
|
// 否则依赖守卫在"跳首页"那次导航里懒加载,会出现首页先以空 menus 渲染、
|
||||||
|
// 之后无新导航补灌、菜单一直空到手动刷新才恢复的竞态。
|
||||||
|
await routeStore.initAuthRoute();
|
||||||
|
|
||||||
await redirectFromLogin(needRedirect);
|
await redirectFromLogin(needRedirect);
|
||||||
|
|
||||||
window.$notification?.success({
|
window.$notification?.success({
|
||||||
|
|||||||
@@ -28,6 +28,15 @@ function normalizeColorType(raw: unknown): string | null {
|
|||||||
return HEX_COLOR_PATTERN.test(trimmed) ? trimmed : null;
|
return HEX_COLOR_PATTERN.test(trimmed) ? trimmed : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析字典项最终展示色(hex)。
|
||||||
|
* 精确色 cssClass 优先(覆盖 colorType 落到语义色无法区分黄/橙等场景),其次 colorType;
|
||||||
|
* 两者都不是合法 hex 时回落 null(默认渲染)。
|
||||||
|
*/
|
||||||
|
function resolveDisplayColor(colorType: unknown, cssClass: unknown): string | null {
|
||||||
|
return normalizeColorType(cssClass) ?? normalizeColorType(colorType);
|
||||||
|
}
|
||||||
|
|
||||||
function normalizeFrontendDictData(
|
function normalizeFrontendDictData(
|
||||||
dictType: string,
|
dictType: string,
|
||||||
list: Api.Dict.FrontendDictData[],
|
list: Api.Dict.FrontendDictData[],
|
||||||
@@ -40,7 +49,7 @@ function normalizeFrontendDictData(
|
|||||||
dictType: item.dictType || dictType,
|
dictType: item.dictType || dictType,
|
||||||
sort: item.sort,
|
sort: item.sort,
|
||||||
status: item.status ?? 0,
|
status: item.status ?? 0,
|
||||||
colorType: normalizeColorType(item.colorType),
|
colorType: resolveDisplayColor(item.colorType, item.cssClass),
|
||||||
remark: item.remark ?? null,
|
remark: item.remark ?? null,
|
||||||
createTime: 0
|
createTime: 0
|
||||||
}));
|
}));
|
||||||
@@ -54,7 +63,7 @@ function normalizeDictDataItem(item: Api.Dict.DictData, dictType: string): Api.D
|
|||||||
value: String(item.value),
|
value: String(item.value),
|
||||||
dictType: item.dictType || dictType,
|
dictType: item.dictType || dictType,
|
||||||
status: item.status ?? 0,
|
status: item.status ?? 0,
|
||||||
colorType: normalizeColorType(item.colorType),
|
colorType: resolveDisplayColor(item.colorType, item.cssClass),
|
||||||
remark: item.remark ?? null
|
remark: item.remark ?? null
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -406,6 +406,7 @@ html .el-collapse {
|
|||||||
.business-table-action-cell {
|
.business-table-action-cell {
|
||||||
display: flex;
|
display: flex;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
padding: 0 8px;
|
padding: 0 8px;
|
||||||
|
|||||||
@@ -89,4 +89,7 @@ export const themeSettings: App.Theme.ThemeSetting = {
|
|||||||
*
|
*
|
||||||
* If publish new version, use `overrideThemeSettings` to override certain theme settings
|
* If publish new version, use `overrideThemeSettings` to override certain theme settings
|
||||||
*/
|
*/
|
||||||
export const overrideThemeSettings: Partial<App.Theme.ThemeSetting> = {};
|
// 系统固定亮色主题:切换入口已全部移除,发新版时把老用户缓存的暗色设置刷回亮色
|
||||||
|
export const overrideThemeSettings: Partial<App.Theme.ThemeSetting> = {
|
||||||
|
themeScheme: 'light'
|
||||||
|
};
|
||||||
|
|||||||
4
src/typings/api/dict.d.ts
vendored
4
src/typings/api/dict.d.ts
vendored
@@ -57,6 +57,8 @@ declare namespace Api {
|
|||||||
status: DictStatus;
|
status: DictStatus;
|
||||||
/** 颜色(hex,#xxxxxx);nullable,无值时前端按默认渲染 */
|
/** 颜色(hex,#xxxxxx);nullable,无值时前端按默认渲染 */
|
||||||
colorType?: string | null;
|
colorType?: string | null;
|
||||||
|
/** 精确颜色(hex,#xxxxxx);存在时优先于 colorType,用于 colorType 落到语义色无法区分的场景 */
|
||||||
|
cssClass?: string | null;
|
||||||
/** remark */
|
/** remark */
|
||||||
remark?: string | null;
|
remark?: string | null;
|
||||||
/** create time */
|
/** create time */
|
||||||
@@ -77,6 +79,8 @@ declare namespace Api {
|
|||||||
status?: DictStatus;
|
status?: DictStatus;
|
||||||
/** 颜色(hex,#xxxxxx);nullable,无值时前端按默认渲染 */
|
/** 颜色(hex,#xxxxxx);nullable,无值时前端按默认渲染 */
|
||||||
colorType?: string | null;
|
colorType?: string | null;
|
||||||
|
/** 精确颜色(hex,#xxxxxx);存在时优先于 colorType */
|
||||||
|
cssClass?: string | null;
|
||||||
/** 备注,可用于下拉中文释义展示 */
|
/** 备注,可用于下拉中文释义展示 */
|
||||||
remark?: string | null;
|
remark?: string | null;
|
||||||
}
|
}
|
||||||
|
|||||||
24
src/typings/api/notice.d.ts
vendored
Normal file
24
src/typings/api/notice.d.ts
vendored
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
declare namespace Api {
|
||||||
|
/**
|
||||||
|
* namespace Notice
|
||||||
|
*
|
||||||
|
* backend api module: "notice"(通知公告)
|
||||||
|
*/
|
||||||
|
namespace Notice {
|
||||||
|
/** 公告(ID 在 API 适配层已统一为 string) */
|
||||||
|
interface Notice {
|
||||||
|
/** 公告编号 */
|
||||||
|
id: string;
|
||||||
|
/** 公告标题 */
|
||||||
|
title: string;
|
||||||
|
/** 公告类型,字典 system_notice_type */
|
||||||
|
type: number;
|
||||||
|
/** 公告内容(富文本 / 纯文本,由录入决定) */
|
||||||
|
content: string;
|
||||||
|
/** 状态:0 开启 / 1 关闭 */
|
||||||
|
status: number;
|
||||||
|
/** 创建时间 */
|
||||||
|
createTime: string | number;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
46
src/typings/api/notify-message.d.ts
vendored
Normal file
46
src/typings/api/notify-message.d.ts
vendored
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
declare namespace Api {
|
||||||
|
/**
|
||||||
|
* namespace NotifyMessage
|
||||||
|
*
|
||||||
|
* backend api module: "notify-message"(站内信 · 我的收件箱)
|
||||||
|
*/
|
||||||
|
namespace NotifyMessage {
|
||||||
|
interface PageParams {
|
||||||
|
pageNo: number;
|
||||||
|
pageSize: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PageResult<T = any> {
|
||||||
|
total: number;
|
||||||
|
list: T[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 站内信(铃铛 / 收件箱展示用;ID 在 API 适配层已统一为 string) */
|
||||||
|
interface NotifyMessage {
|
||||||
|
/** 站内信编号(雪花 Long,按 string 接收) */
|
||||||
|
id: string;
|
||||||
|
/** 发送人名称(模板配置的发件人显示名) */
|
||||||
|
templateNickname: string;
|
||||||
|
/** 最终消息正文(占位符已渲染,直接展示) */
|
||||||
|
templateContent: string;
|
||||||
|
/** 消息类型,字典 system_notify_template_type */
|
||||||
|
templateType: number;
|
||||||
|
/** 消息等级(字典 notify_message_level,1=普通 2=提醒 3=警告 4=严重,数字越大越紧急);老消息缺省为普通(1) */
|
||||||
|
level: number;
|
||||||
|
/** 是否已读 */
|
||||||
|
readStatus: boolean;
|
||||||
|
/** 阅读时间;未读为 null */
|
||||||
|
readTime: string | number | null;
|
||||||
|
/** 收到时间 */
|
||||||
|
createTime: string | number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 我的站内信分页查询参数 */
|
||||||
|
interface MyPageParams extends PageParams {
|
||||||
|
/** true 只看已读 / false 只看未读 / 不传 = 全部 */
|
||||||
|
readStatus?: boolean;
|
||||||
|
/** 关键字,后端对消息正文模糊匹配;不传或空串 = 不过滤 */
|
||||||
|
keyword?: string;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
49
src/typings/api/overtime-application.d.ts
vendored
49
src/typings/api/overtime-application.d.ts
vendored
@@ -5,9 +5,9 @@ declare namespace Api {
|
|||||||
pageSize: number;
|
pageSize: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
type OvertimeApplicationStatusCode = 'pending' | 'approved' | 'rejected' | 'cancelled';
|
type OvertimeApplicationStatusCode = 'pending' | 'approved' | 'rejected';
|
||||||
|
|
||||||
type OvertimeApplicationActionType = 'submit' | 'resubmit' | 'approve' | 'reject' | 'cancel';
|
type OvertimeApplicationActionType = 'submit' | 'resubmit' | 'approve' | 'reject';
|
||||||
|
|
||||||
interface OvertimeApplication {
|
interface OvertimeApplication {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -59,20 +59,41 @@ declare namespace Api {
|
|||||||
reason?: string | null;
|
reason?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface OvertimeApplicationStatusLog {
|
interface OvertimeApplicationBatchActionParams {
|
||||||
id: string;
|
ids: string[];
|
||||||
applicationId: string;
|
|
||||||
actionType: OvertimeApplicationActionType;
|
|
||||||
fromStatus?: string | null;
|
|
||||||
toStatus: string;
|
|
||||||
reason?: string | null;
|
reason?: string | null;
|
||||||
operatorUserId: string;
|
}
|
||||||
operatorName: string;
|
|
||||||
applicantNameSnapshot: string;
|
interface OvertimeApplicationBatchFailItem {
|
||||||
overtimeDateSnapshot: string;
|
id: string;
|
||||||
overtimeDurationSnapshot: string;
|
reason: string;
|
||||||
remark?: string | null;
|
}
|
||||||
|
|
||||||
|
interface OvertimeApplicationBatchActionResult {
|
||||||
|
successCount: number;
|
||||||
|
failCount: number;
|
||||||
|
failItems: OvertimeApplicationBatchFailItem[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface OvertimeApplicationApprovalRecord {
|
||||||
|
id: string;
|
||||||
|
overtimeApplicationId: string;
|
||||||
|
statusLogId: string;
|
||||||
|
approvalRound: number;
|
||||||
|
conclusion: string;
|
||||||
|
opinion?: string | null;
|
||||||
|
auditorUserId: string;
|
||||||
|
auditorName: string;
|
||||||
createTime: string;
|
createTime: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface OvertimeApplicationStatusDict {
|
||||||
|
statusCode: string;
|
||||||
|
statusName: string;
|
||||||
|
sort: number;
|
||||||
|
initialFlag: boolean;
|
||||||
|
terminalFlag: boolean;
|
||||||
|
allowEdit: boolean;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
23
src/typings/api/product.d.ts
vendored
23
src/typings/api/product.d.ts
vendored
@@ -21,10 +21,27 @@ declare namespace Api {
|
|||||||
list: T[];
|
list: T[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 入口页概览统计状态看板项(状态机全部启用状态,按 sort 升序,计数为 0 也返回;与项目域契约同构) */
|
||||||
|
interface OverviewStatusItem {
|
||||||
|
statusCode: string;
|
||||||
|
/** 状态展示名(状态机配置中文名,前端直接渲染,不做本地名称映射) */
|
||||||
|
statusName: string;
|
||||||
|
count: number;
|
||||||
|
sort: number;
|
||||||
|
/** 是否终态(状态机 terminal_flag) */
|
||||||
|
terminal: boolean;
|
||||||
|
/** 是否计入"全部";当前口径无排除项恒为 true(产品列表暂无"全部"视图,按同构契约返回) */
|
||||||
|
includeInAll: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
/** 产品入口页概览统计 */
|
/** 产品入口页概览统计 */
|
||||||
interface ProductOverviewSummary {
|
interface ProductOverviewSummary {
|
||||||
/** 产品状态数量映射,key 为后端状态编码 */
|
/** 产品状态数量映射,key 为后端状态编码(过渡兼容字段,前端迁移完成后由后端删除) */
|
||||||
statusCounts: Record<string, number>;
|
statusCounts: Record<string, number>;
|
||||||
|
/** "全部"口径总数 = items 各状态 count 之和 */
|
||||||
|
total: number;
|
||||||
|
/** 状态看板项,覆盖状态机全部启用状态,按 sort 升序 */
|
||||||
|
items: OverviewStatusItem[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Product {
|
interface Product {
|
||||||
@@ -172,8 +189,10 @@ declare namespace Api {
|
|||||||
|
|
||||||
type ProductSearchParams = CommonType.RecordNullable<
|
type ProductSearchParams = CommonType.RecordNullable<
|
||||||
Pick<PageParams, 'pageNo' | 'pageSize'> &
|
Pick<PageParams, 'pageNo' | 'pageSize'> &
|
||||||
Pick<Product, 'directionCode' | 'managerUserId' | 'statusCode'> & {
|
Pick<Product, 'directionCode' | 'managerUserId'> & {
|
||||||
keyword: string;
|
keyword: string;
|
||||||
|
/** 状态编码来自状态机(overview-summary items 动态下发),不再用前端字面量联合约束 */
|
||||||
|
statusCode: string;
|
||||||
updateTime: string[];
|
updateTime: string[];
|
||||||
}
|
}
|
||||||
>;
|
>;
|
||||||
|
|||||||
294
src/typings/api/project.d.ts
vendored
294
src/typings/api/project.d.ts
vendored
@@ -304,6 +304,213 @@ declare namespace Api {
|
|||||||
updateTime: string[];
|
updateTime: string[];
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
|
/** 工作台「我负责的执行」(跨项目)查询入参 */
|
||||||
|
type MyExecutionSearchParams = CommonType.RecordNullable<
|
||||||
|
Pick<PageParams, 'pageNo' | 'pageSize'> & {
|
||||||
|
/** 预留:单状态精确过滤,不传走后端默认口径 */
|
||||||
|
statusCode: string;
|
||||||
|
/** 预留:执行名称模糊匹配 */
|
||||||
|
keyword: string;
|
||||||
|
}
|
||||||
|
>;
|
||||||
|
|
||||||
|
/** 工作台「我负责的执行」单项(跨项目聚合,owner 恒为当前登录用户) */
|
||||||
|
interface MyExecutionItem {
|
||||||
|
/** 执行 ID(雪花 ID,字符串) */
|
||||||
|
id: string;
|
||||||
|
executionName: string;
|
||||||
|
/** 所属项目 */
|
||||||
|
projectId: string;
|
||||||
|
projectName: string;
|
||||||
|
/** 执行状态编码:pending / active / paused */
|
||||||
|
statusCode: string;
|
||||||
|
/** 执行状态名称 */
|
||||||
|
statusName: string | null;
|
||||||
|
/** 优先级字典 value(rdms_req_priority,"0"~"3") */
|
||||||
|
priority: string;
|
||||||
|
/** 计划起止(YYYY-MM-DD) */
|
||||||
|
plannedStartDate: string | null;
|
||||||
|
plannedEndDate: string | null;
|
||||||
|
/** 实际起止(YYYY-MM-DD) */
|
||||||
|
actualStartDate: string | null;
|
||||||
|
actualEndDate: string | null;
|
||||||
|
/** 进度(0-100 整数) */
|
||||||
|
progressRate: number;
|
||||||
|
/** 关联项目需求 */
|
||||||
|
projectRequirementId: string | null;
|
||||||
|
projectRequirementName: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 工作台「我的项目」查询入参(我参与的 / 我负责的 共用) */
|
||||||
|
type MyProjectSearchParams = CommonType.RecordNullable<
|
||||||
|
Pick<PageParams, 'pageNo' | 'pageSize'> & {
|
||||||
|
/** 预留:项目名称/编码模糊关键字,后端本期不过滤 */
|
||||||
|
keyword: string;
|
||||||
|
}
|
||||||
|
>;
|
||||||
|
|
||||||
|
/** 工作台「我参与的项目」单项(成员视角,附带我的角色与任务量) */
|
||||||
|
interface MyParticipatedProjectItem {
|
||||||
|
/** 项目 ID(字符串) */
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
/** 项目编码,可空 */
|
||||||
|
code: string | null;
|
||||||
|
/** 项目状态编码(如 active) */
|
||||||
|
statusCode: string;
|
||||||
|
/** 项目状态名称,可空 */
|
||||||
|
statusName: string | null;
|
||||||
|
/** 项目整体进度 0-100 */
|
||||||
|
progress: number;
|
||||||
|
/** 我在该项目中的角色名(多角色拼接),可空 */
|
||||||
|
myRole: string | null;
|
||||||
|
/** 我负责的任务总数(按负责人,含已完成) */
|
||||||
|
myTaskCount: number;
|
||||||
|
/** 我负责的未完成任务数 */
|
||||||
|
myPendingTaskCount: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 工作台「我负责的项目」成员负载子项 */
|
||||||
|
interface MyOwnedProjectMember {
|
||||||
|
/** 成员用户 ID(字符串) */
|
||||||
|
userId: string;
|
||||||
|
/** 成员姓名/昵称,可空 */
|
||||||
|
userName: string | null;
|
||||||
|
/** 该成员在本项目下进行中任务数(按负责人) */
|
||||||
|
activeTaskCount: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 工作台「我负责的项目」单项(项目负责人视角,附聚合统计与成员负载) */
|
||||||
|
interface MyOwnedProjectItem {
|
||||||
|
/** 项目 ID(字符串) */
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
/** 项目编码,可空 */
|
||||||
|
code: string | null;
|
||||||
|
/** 项目整体进度 0-100 */
|
||||||
|
progress: number;
|
||||||
|
/** 我在该项目中的角色名,可空 */
|
||||||
|
myRole: string | null;
|
||||||
|
/** 项目计划结束日期 YYYY-MM-DD,可空 */
|
||||||
|
plannedEndDate: string | null;
|
||||||
|
/** 项目下进行中执行数 */
|
||||||
|
executionCount: number;
|
||||||
|
/** 项目下进行中任务数 */
|
||||||
|
taskCount: number;
|
||||||
|
/** 项目下逾期任务数 */
|
||||||
|
overdueCount: number;
|
||||||
|
/** 项目当前有效成员数(多角色去重) */
|
||||||
|
memberCount: number;
|
||||||
|
/** 成员负载列表(无成员为 []) */
|
||||||
|
members: MyOwnedProjectMember[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 工作台「我的任务」(跨项目)查询入参 */
|
||||||
|
type MyTaskSearchParams = CommonType.RecordNullable<
|
||||||
|
Pick<PageParams, 'pageNo' | 'pageSize'> & {
|
||||||
|
/** 身份过滤:owner 我负责 / collaborator 我协办;缺省 = 两者并集 */
|
||||||
|
involveType: 'owner' | 'collaborator';
|
||||||
|
}
|
||||||
|
>;
|
||||||
|
|
||||||
|
/** 工作台「我的任务」单项(跨项目;当前用户为负责人或在岗协办人,接口只返回非终态任务) */
|
||||||
|
interface MyTaskItem {
|
||||||
|
/** 任务 ID(雪花 ID,字符串) */
|
||||||
|
id: string;
|
||||||
|
taskTitle: string;
|
||||||
|
/** 所属项目 */
|
||||||
|
projectId: string;
|
||||||
|
projectName: string;
|
||||||
|
/** 所属执行,未挂执行为 null */
|
||||||
|
executionId: string | null;
|
||||||
|
executionName: string | null;
|
||||||
|
/** 任务状态:pending / active / paused(非终态) */
|
||||||
|
statusCode: ProjectTaskStatusCode;
|
||||||
|
statusName: string | null;
|
||||||
|
/** 优先级字典 value(rdms_req_priority,"0"~"3",数字越小越高) */
|
||||||
|
priority: string;
|
||||||
|
/** 计划结束日期(YYYY-MM-DD),可空 */
|
||||||
|
plannedEndDate: string | null;
|
||||||
|
/** 任务进度(0-100);后端定稿直接返回,无进度明确返 0 */
|
||||||
|
progressRate: number;
|
||||||
|
/** 创建时间(YYYY-MM-DD HH:mm:ss;后端返毫秒时间戳,适配层归一) */
|
||||||
|
createTime: string;
|
||||||
|
/** 我的角色:owner 负责人 / collaborator 协办人;双重身份只返 owner */
|
||||||
|
myRole: 'owner' | 'collaborator';
|
||||||
|
/** 父任务 ID(字符串),一级任务为 null */
|
||||||
|
parentTaskId: string | null;
|
||||||
|
/** 是否终态;本接口只返非终态任务,正常恒为 false */
|
||||||
|
terminal: boolean;
|
||||||
|
/** 当前状态是否允许编辑任务 */
|
||||||
|
allowEdit: boolean;
|
||||||
|
/** 当前登录用户可执行的生命周期动作(与任务详情同口径;auto_start 不返回),无动作为 [] */
|
||||||
|
availableActions: LifecycleAction<ProjectTaskActionCode>[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 工作台「团队负载」分布子项(kind != project 时 projectId / projectName 为 null) */
|
||||||
|
interface TeamLoadDistributionItem {
|
||||||
|
projectId: string | null;
|
||||||
|
projectName: string | null;
|
||||||
|
/** project 项目任务 / personal 个人事项 / other 无法归类的残留 */
|
||||||
|
kind: 'project' | 'personal' | 'other';
|
||||||
|
/** 未完成任务数(含待开始/已暂停) */
|
||||||
|
count: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 工作台「团队负载」成员(members[0] 恒为当前用户) */
|
||||||
|
interface TeamLoadMember {
|
||||||
|
/** 用户 ID(字符串) */
|
||||||
|
userId: string;
|
||||||
|
userNickname: string;
|
||||||
|
/** 未完成任务按归属分布,无任务为 [] */
|
||||||
|
items: TeamLoadDistributionItem[];
|
||||||
|
/** 临期:今天 ≤ 计划结束 ≤ 今天+3 天,且未完成(与逾期互斥) */
|
||||||
|
dueSoonCount: number;
|
||||||
|
/** 逾期:计划结束 < 今天,且未完成 */
|
||||||
|
overdueCount: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 工作台「团队负载」响应(GET /project/project/me/team-load,团队 = 自己 + 管理链路直接下级) */
|
||||||
|
interface TeamLoadResult {
|
||||||
|
members: TeamLoadMember[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 工作台工时分布子项(kind != project 时 projectId / projectName 为 null;hours=0 的行后端不输出) */
|
||||||
|
interface WorklogDistributionItem {
|
||||||
|
projectId: string | null;
|
||||||
|
projectName: string | null;
|
||||||
|
kind: 'project' | 'personal' | 'other';
|
||||||
|
hours: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 工作台「我的工时周聚合」响应(GET /project/project/me/worklog-week) */
|
||||||
|
interface MyWorklogWeekResult {
|
||||||
|
/** 归一后的周一日期 YYYY-MM-DD */
|
||||||
|
weekStart: string;
|
||||||
|
/** 周一~周五逐日工时(固定 5 元素;均摊推算值,周末份额归周五) */
|
||||||
|
dailyHours: number[];
|
||||||
|
/** 本周工时按归属分布,hours 降序 */
|
||||||
|
distribution: WorklogDistributionItem[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 工作台「团队工时周聚合」成员(members[0] 恒为当前用户;该周未填报成员 items 为 []) */
|
||||||
|
interface TeamWorklogWeekMember {
|
||||||
|
userId: string;
|
||||||
|
userNickname: string;
|
||||||
|
items: WorklogDistributionItem[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 工作台「团队工时周聚合」响应(GET /project/project/me/team-worklog-week;周标准工时后端不返回,前端落常量 35) */
|
||||||
|
interface TeamWorklogWeekResult {
|
||||||
|
weekStart: string;
|
||||||
|
members: TeamWorklogWeekMember[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 工作台工时周聚合查询入参(weekStart 传任意日期,后端归一到所在周周一) */
|
||||||
|
interface WorklogWeekParams {
|
||||||
|
weekStart: string;
|
||||||
|
}
|
||||||
|
|
||||||
/** 创建执行入参(含 ownerId + assigneeUserIds) */
|
/** 创建执行入参(含 ownerId + assigneeUserIds) */
|
||||||
interface CreateProjectExecutionParams {
|
interface CreateProjectExecutionParams {
|
||||||
executionName: string;
|
executionName: string;
|
||||||
@@ -580,10 +787,29 @@ declare namespace Api {
|
|||||||
list: T[];
|
list: T[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 入口页概览统计状态看板项(状态机全部启用状态,按 sort 升序,计数为 0 也返回) */
|
||||||
|
interface OverviewStatusItem {
|
||||||
|
statusCode: string;
|
||||||
|
/** 状态展示名(状态机配置中文名,前端直接渲染,不做本地名称映射) */
|
||||||
|
statusName: string;
|
||||||
|
count: number;
|
||||||
|
sort: number;
|
||||||
|
/** 是否终态(状态机 terminal_flag);不能用于"全部"排除或左栏分区(completed 也可能是终态) */
|
||||||
|
terminal: boolean;
|
||||||
|
/** 是否计入"全部";当前口径无排除项恒为 true,将来恢复排除项由该字段表达 */
|
||||||
|
includeInAll: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
/** 项目入口页概览统计 */
|
/** 项目入口页概览统计 */
|
||||||
interface ProjectOverviewSummary {
|
interface ProjectOverviewSummary {
|
||||||
/** 项目状态数量映射,key 为后端状态编码 */
|
/** 项目状态数量映射,key 为后端状态编码(过渡兼容字段,前端迁移完成后由后端删除) */
|
||||||
statusCounts: Record<string, number>;
|
statusCounts: Record<string, number>;
|
||||||
|
/** "全部"口径总数 = items 各状态 count 之和(作废/归档计入) */
|
||||||
|
total: number;
|
||||||
|
/** 状态看板项,覆盖状态机全部启用状态,按 sort 升序 */
|
||||||
|
items: OverviewStatusItem[];
|
||||||
|
/** 游离项目计数 = 所有未挂产品的项目(不按状态过滤),左栏游离入口据此显隐 */
|
||||||
|
orphanCount?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Project {
|
interface Project {
|
||||||
@@ -686,11 +912,75 @@ declare namespace Api {
|
|||||||
projectType: string;
|
projectType: string;
|
||||||
productId: string;
|
productId: string;
|
||||||
managerUserId: string;
|
managerUserId: string;
|
||||||
statusCode: ProjectStatusCode;
|
/** 状态编码来自状态机(overview-summary items 动态下发),不再用前端字面量联合约束 */
|
||||||
|
statusCode: string;
|
||||||
|
/** 多值状态筛选(存在时后端优先于单值 statusCode);分组页"展开剩余"按"全部"口径传 items 派生的全量编码 */
|
||||||
|
statusCodes: string[];
|
||||||
|
/** 仅查游离项目(productId 为空);与 productId 互斥,分组页展开游离组剩余时用 */
|
||||||
|
orphanOnly: boolean;
|
||||||
updateTime: string[];
|
updateTime: string[];
|
||||||
}
|
}
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 项目列表"按产品分组"查询入参(GET /project/project/group-page)。
|
||||||
|
*
|
||||||
|
* - pageNo / pageSize 为**产品组维度**分页(一页 M 个产品组),不是项目行分页。
|
||||||
|
* - statusCode 不传 = "全部"视图(后端从状态机推导;2026-06-11 口径变更后无排除项,作废/归档计入)。
|
||||||
|
* - orphanOnly = true 仅返回游离组(productId 为空的项目);不可与 productId 同传。
|
||||||
|
* - topN:每组返回项目条数上限(后端默认 5,范围 1~50),超出由页面展开拉取。
|
||||||
|
*/
|
||||||
|
type ProjectGroupSearchParams = CommonType.RecordNullable<
|
||||||
|
Pick<PageParams, 'pageNo' | 'pageSize'> & {
|
||||||
|
keyword: string;
|
||||||
|
productId: string;
|
||||||
|
projectType: string;
|
||||||
|
/** 状态编码来自状态机(overview-summary items 动态下发),不再用前端字面量联合约束 */
|
||||||
|
statusCode: string;
|
||||||
|
orphanOnly: boolean;
|
||||||
|
topN: number;
|
||||||
|
}
|
||||||
|
>;
|
||||||
|
|
||||||
|
/** 按产品聚合的项目分组 */
|
||||||
|
interface ProjectGroup {
|
||||||
|
/** 产品 ID;游离组为 null */
|
||||||
|
productId: string | null;
|
||||||
|
/** 产品名称;游离组固定为"游离项目" */
|
||||||
|
productName: string;
|
||||||
|
/** 产品编码;游离组为 null */
|
||||||
|
productCode: string | null;
|
||||||
|
/** 产品方向字典值;游离组为空串 */
|
||||||
|
directionCode: string;
|
||||||
|
/** 产品经理用户 ID */
|
||||||
|
managerUserId: string | null;
|
||||||
|
/** 产品经理昵称(后端回填;游离组为 null,前端 managerLabelMap 兜底) */
|
||||||
|
managerUserNickname: string | null;
|
||||||
|
/** 当前筛选口径下组内项目总数 */
|
||||||
|
projectTotal: number;
|
||||||
|
/** 组内项目前 topN 条,按最近更新倒序;剩余由页面按 productId/orphanOnly + statusCodes 走 page 接口展开拉取 */
|
||||||
|
projects: Project[];
|
||||||
|
/** 组内按项目类型字典 value 的计数(现状按"全部口径"统计;已提需求改为跟随 statusCode 与 projectTotal 同口径,后端落地后更新本注释) */
|
||||||
|
typeCounts: Record<string, number>;
|
||||||
|
/** 是否已有主线项目(口径=存在非已取消 cancelled 的主线,已归档/完成也算占坑);前端直接消费、不用 typeCounts 推导 */
|
||||||
|
hasBaseline: boolean;
|
||||||
|
/** 是否游离组(未挂产品) */
|
||||||
|
orphan: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 产品分组分页结果 */
|
||||||
|
interface ProjectGroupPageResult {
|
||||||
|
/** 当前筛选口径下产品组总数(分页 total,含游离组) */
|
||||||
|
total: number;
|
||||||
|
/** 当前筛选口径下项目总数(标题 meta 用) */
|
||||||
|
projectTotal: number;
|
||||||
|
/** 当前筛选口径下可见产品跨方向数(≥2 时前端渲染方向层) */
|
||||||
|
directionCount: number;
|
||||||
|
/** 当前筛选口径下游离项目数(标题/分页用);左栏常驻游离计数改用 overview-summary 的 orphanCount 全口径 */
|
||||||
|
orphanTotal: number;
|
||||||
|
list: ProjectGroup[];
|
||||||
|
}
|
||||||
|
|
||||||
/** 创建/保存项目参数 */
|
/** 创建/保存项目参数 */
|
||||||
type SaveProjectParams = Pick<Project, 'projectName' | 'directionCode' | 'projectType' | 'projectDesc'> & {
|
type SaveProjectParams = Pick<Project, 'projectName' | 'directionCode' | 'projectType' | 'projectDesc'> & {
|
||||||
projectCode: string | null;
|
projectCode: string | null;
|
||||||
|
|||||||
296
src/typings/api/work-report.d.ts
vendored
Normal file
296
src/typings/api/work-report.d.ts
vendored
Normal file
@@ -0,0 +1,296 @@
|
|||||||
|
declare namespace Api {
|
||||||
|
namespace WorkReport {
|
||||||
|
namespace Common {
|
||||||
|
interface PageParams {
|
||||||
|
pageNo: number;
|
||||||
|
pageSize: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
type ReportType = 'weekly' | 'monthly' | 'project';
|
||||||
|
type WorkReportStatusCode = 'draft' | 'pending_approval' | 'approved' | 'rejected';
|
||||||
|
|
||||||
|
interface WorkReportStatusDict {
|
||||||
|
statusCode: WorkReportStatusCode | string;
|
||||||
|
statusName: string;
|
||||||
|
sort: number;
|
||||||
|
initialFlag: boolean;
|
||||||
|
terminalFlag: boolean;
|
||||||
|
allowEdit: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface WorkReportApprovalRecord {
|
||||||
|
id: string;
|
||||||
|
statusLogId: string;
|
||||||
|
approvalRound: number;
|
||||||
|
conclusion: string;
|
||||||
|
opinion?: string | null;
|
||||||
|
auditorUserId: string;
|
||||||
|
auditorName: string;
|
||||||
|
createTime: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PersonalReportReviewItem {
|
||||||
|
id?: string;
|
||||||
|
itemNumber?: number | null;
|
||||||
|
itemTitle: string;
|
||||||
|
workHours?: number | null;
|
||||||
|
contentText?: string | null;
|
||||||
|
contentJson?: unknown;
|
||||||
|
reflectionText?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PersonalReportPlanItem {
|
||||||
|
id?: string;
|
||||||
|
itemNumber?: number | null;
|
||||||
|
itemTitle: string;
|
||||||
|
targetText?: string | null;
|
||||||
|
targetJson?: unknown;
|
||||||
|
supportNeed?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
type WorkReportBaseSearchParams = CommonType.RecordNullable<
|
||||||
|
Pick<PageParams, 'pageNo' | 'pageSize'> & {
|
||||||
|
keyword: string;
|
||||||
|
statusCode: WorkReportStatusCode | string;
|
||||||
|
periodStartDate: string[];
|
||||||
|
submitTime: string[];
|
||||||
|
supervisorName: string;
|
||||||
|
}
|
||||||
|
>;
|
||||||
|
|
||||||
|
type ContentExportParams<TSearch> = Partial<TSearch> & {
|
||||||
|
exportAll?: boolean;
|
||||||
|
ids?: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
interface StatusActionParams {
|
||||||
|
reason?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PageResult<T> {
|
||||||
|
total: number;
|
||||||
|
list: T[];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace Weekly {
|
||||||
|
interface WeeklyReportTravelSegment {
|
||||||
|
id?: string;
|
||||||
|
sort?: number | null;
|
||||||
|
startDate?: string | null;
|
||||||
|
endDate?: string | null;
|
||||||
|
travelDays?: number | null;
|
||||||
|
location?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface WeeklyReport {
|
||||||
|
id: string;
|
||||||
|
reporterId: string;
|
||||||
|
reporterName: string;
|
||||||
|
reporterDeptName?: string | null;
|
||||||
|
reporterPostName?: string | null;
|
||||||
|
supervisorUserId: string;
|
||||||
|
supervisorName: string;
|
||||||
|
periodKey: string;
|
||||||
|
periodLabel: string;
|
||||||
|
periodStartDate: string;
|
||||||
|
periodEndDate: string;
|
||||||
|
statusCode: Common.WorkReportStatusCode | string;
|
||||||
|
statusName: string;
|
||||||
|
allowEdit: boolean;
|
||||||
|
terminal: boolean;
|
||||||
|
isBusinessTrip: boolean;
|
||||||
|
totalTravelDays?: number | string | null;
|
||||||
|
totalWorkHours?: number | string | null;
|
||||||
|
approvalComment?: string | null;
|
||||||
|
lastStatusReason?: string | null;
|
||||||
|
submitTime?: string | null;
|
||||||
|
approvalTime?: string | null;
|
||||||
|
createTime?: string | null;
|
||||||
|
updateTime?: string | null;
|
||||||
|
reviewItems: Common.PersonalReportReviewItem[];
|
||||||
|
planItems: Common.PersonalReportPlanItem[];
|
||||||
|
travelSegments: WeeklyReportTravelSegment[];
|
||||||
|
}
|
||||||
|
|
||||||
|
type WeeklyReportSearchParams = Common.WorkReportBaseSearchParams & {
|
||||||
|
isBusinessTrip?: boolean | string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
interface WeeklyReportSaveParams {
|
||||||
|
periodKey: string;
|
||||||
|
periodLabel: string;
|
||||||
|
periodStartDate: string;
|
||||||
|
periodEndDate: string;
|
||||||
|
isBusinessTrip: boolean;
|
||||||
|
reviewItems: Common.PersonalReportReviewItem[];
|
||||||
|
planItems: Common.PersonalReportPlanItem[];
|
||||||
|
travelSegments: WeeklyReportTravelSegment[];
|
||||||
|
}
|
||||||
|
|
||||||
|
type WeeklyReportDefaultDraftParams = Pick<
|
||||||
|
WeeklyReportSaveParams,
|
||||||
|
'periodKey' | 'periodLabel' | 'periodStartDate' | 'periodEndDate'
|
||||||
|
>;
|
||||||
|
|
||||||
|
type WeeklyReportRefreshDraftParams = WeeklyReportSaveParams;
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace Monthly {
|
||||||
|
interface MonthlyReport {
|
||||||
|
id: string;
|
||||||
|
reporterId: string;
|
||||||
|
reporterName: string;
|
||||||
|
reporterDeptName?: string | null;
|
||||||
|
reporterPostName?: string | null;
|
||||||
|
supervisorUserId: string;
|
||||||
|
supervisorName: string;
|
||||||
|
periodKey: string;
|
||||||
|
periodLabel: string;
|
||||||
|
periodStartDate: string;
|
||||||
|
periodEndDate: string;
|
||||||
|
statusCode: Common.WorkReportStatusCode | string;
|
||||||
|
statusName: string;
|
||||||
|
allowEdit: boolean;
|
||||||
|
terminal: boolean;
|
||||||
|
totalWorkHours?: number | string | null;
|
||||||
|
approvalComment?: string | null;
|
||||||
|
lastStatusReason?: string | null;
|
||||||
|
submitTime?: string | null;
|
||||||
|
approvalTime?: string | null;
|
||||||
|
createTime?: string | null;
|
||||||
|
updateTime?: string | null;
|
||||||
|
reviewItems: Common.PersonalReportReviewItem[];
|
||||||
|
planItems: Common.PersonalReportPlanItem[];
|
||||||
|
}
|
||||||
|
|
||||||
|
type MonthlyReportSearchParams = Common.WorkReportBaseSearchParams;
|
||||||
|
|
||||||
|
interface MonthlyReportSaveParams {
|
||||||
|
periodKey: string;
|
||||||
|
periodLabel: string;
|
||||||
|
periodStartDate: string;
|
||||||
|
periodEndDate: string;
|
||||||
|
reviewItems: Common.PersonalReportReviewItem[];
|
||||||
|
planItems: Common.PersonalReportPlanItem[];
|
||||||
|
}
|
||||||
|
|
||||||
|
type MonthlyReportDefaultDraftParams = Pick<
|
||||||
|
MonthlyReportSaveParams,
|
||||||
|
'periodKey' | 'periodLabel' | 'periodStartDate' | 'periodEndDate'
|
||||||
|
>;
|
||||||
|
|
||||||
|
type MonthlyReportRefreshDraftParams = MonthlyReportSaveParams;
|
||||||
|
|
||||||
|
interface MonthlyReportApproveParams extends Common.StatusActionParams {
|
||||||
|
meetingDate?: string | null;
|
||||||
|
strengthDesc?: string | null;
|
||||||
|
strengthExample?: string | null;
|
||||||
|
weaknessDesc?: string | null;
|
||||||
|
weaknessExample?: string | null;
|
||||||
|
improvementSuggestion?: string | null;
|
||||||
|
performanceResult?: string | null;
|
||||||
|
employeeSignName?: string | null;
|
||||||
|
employeeSignedDate?: string | null;
|
||||||
|
supervisorSignName?: string | null;
|
||||||
|
supervisorSignedDate?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MonthlyReportApprovalRecord extends Common.WorkReportApprovalRecord {
|
||||||
|
meetingDate?: string | null;
|
||||||
|
strengthDesc?: string | null;
|
||||||
|
strengthExample?: string | null;
|
||||||
|
weaknessDesc?: string | null;
|
||||||
|
weaknessExample?: string | null;
|
||||||
|
improvementSuggestion?: string | null;
|
||||||
|
performanceResult?: string | null;
|
||||||
|
employeeSignName?: string | null;
|
||||||
|
employeeSignedDate?: string | null;
|
||||||
|
supervisorSignName?: string | null;
|
||||||
|
supervisorSignedDate?: string | null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace Project {
|
||||||
|
interface WorkReportMemberSnapshot {
|
||||||
|
userId: string;
|
||||||
|
userName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ProjectReportItem {
|
||||||
|
id?: string;
|
||||||
|
itemTitle: string;
|
||||||
|
workHours?: number | null;
|
||||||
|
priorityCode?: string | null;
|
||||||
|
progressRate?: number | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ProjectReportOwnerProjectOption {
|
||||||
|
id: string;
|
||||||
|
projectCode: string;
|
||||||
|
projectName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ProjectReport {
|
||||||
|
id: string;
|
||||||
|
projectId: string;
|
||||||
|
projectName: string;
|
||||||
|
projectOwnerId: string;
|
||||||
|
projectOwnerName: string;
|
||||||
|
technicalOwnerName?: string | null;
|
||||||
|
projectMemberSnapshot: WorkReportMemberSnapshot[];
|
||||||
|
supervisorUserId: string;
|
||||||
|
supervisorName: string;
|
||||||
|
periodKey: string;
|
||||||
|
periodLabel: string;
|
||||||
|
periodStartDate: string;
|
||||||
|
periodEndDate: string;
|
||||||
|
flag: number;
|
||||||
|
statusCode: Common.WorkReportStatusCode | string;
|
||||||
|
statusName: string;
|
||||||
|
allowEdit: boolean;
|
||||||
|
terminal: boolean;
|
||||||
|
projectStatusDesc?: string | null;
|
||||||
|
projectProgressPlan?: string | null;
|
||||||
|
projectKeyPoints?: string | null;
|
||||||
|
projectProblems?: string | null;
|
||||||
|
totalWorkHours?: number | string | null;
|
||||||
|
approvalComment?: string | null;
|
||||||
|
lastStatusReason?: string | null;
|
||||||
|
submitTime?: string | null;
|
||||||
|
approvalTime?: string | null;
|
||||||
|
createTime?: string | null;
|
||||||
|
updateTime?: string | null;
|
||||||
|
currentItems: ProjectReportItem[];
|
||||||
|
nextItems: ProjectReportItem[];
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProjectReportSearchParams = Common.WorkReportBaseSearchParams & {
|
||||||
|
projectId?: string | null;
|
||||||
|
flag?: number | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
interface ProjectReportSaveParams {
|
||||||
|
projectId: string;
|
||||||
|
periodKey: string;
|
||||||
|
periodLabel: string;
|
||||||
|
periodStartDate: string;
|
||||||
|
periodEndDate: string;
|
||||||
|
flag: number;
|
||||||
|
projectStatusDesc?: string | null;
|
||||||
|
projectProgressPlan?: string | null;
|
||||||
|
projectKeyPoints?: string | null;
|
||||||
|
projectProblems?: string | null;
|
||||||
|
currentItems: ProjectReportItem[];
|
||||||
|
nextItems: ProjectReportItem[];
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProjectReportDefaultDraftParams = Pick<
|
||||||
|
ProjectReportSaveParams,
|
||||||
|
'periodKey' | 'periodLabel' | 'periodStartDate' | 'periodEndDate' | 'flag'
|
||||||
|
>;
|
||||||
|
|
||||||
|
type ProjectReportRefreshDraftParams = Omit<ProjectReportSaveParams, 'projectId'>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
39
src/typings/app.d.ts
vendored
39
src/typings/app.d.ts
vendored
@@ -504,45 +504,6 @@ declare namespace App {
|
|||||||
};
|
};
|
||||||
creativity: string;
|
creativity: string;
|
||||||
};
|
};
|
||||||
function: {
|
|
||||||
tab: {
|
|
||||||
tabOperate: {
|
|
||||||
title: string;
|
|
||||||
addTab: string;
|
|
||||||
addTabDesc: string;
|
|
||||||
closeTab: string;
|
|
||||||
closeCurrentTab: string;
|
|
||||||
closeAboutTab: string;
|
|
||||||
addMultiTab: string;
|
|
||||||
addMultiTabDesc1: string;
|
|
||||||
addMultiTabDesc2: string;
|
|
||||||
};
|
|
||||||
tabTitle: {
|
|
||||||
title: string;
|
|
||||||
changeTitle: string;
|
|
||||||
change: string;
|
|
||||||
resetTitle: string;
|
|
||||||
reset: string;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
multiTab: {
|
|
||||||
routeParam: string;
|
|
||||||
backTab: string;
|
|
||||||
};
|
|
||||||
toggleAuth: {
|
|
||||||
toggleAccount: string;
|
|
||||||
authHook: string;
|
|
||||||
superAdminVisible: string;
|
|
||||||
adminVisible: string;
|
|
||||||
adminOrUserVisible: string;
|
|
||||||
};
|
|
||||||
request: {
|
|
||||||
repeatedErrorOccurOnce: string;
|
|
||||||
repeatedError: string;
|
|
||||||
repeatedErrorMsg1: string;
|
|
||||||
repeatedErrorMsg2: string;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
system: {
|
system: {
|
||||||
common: {
|
common: {
|
||||||
status: {
|
status: {
|
||||||
|
|||||||
2
src/typings/components.d.ts
vendored
2
src/typings/components.d.ts
vendored
@@ -129,6 +129,8 @@ declare module 'vue' {
|
|||||||
IconIcRoundRefresh: typeof import('~icons/ic/round-refresh')['default']
|
IconIcRoundRefresh: typeof import('~icons/ic/round-refresh')['default']
|
||||||
IconIcRoundRemove: typeof import('~icons/ic/round-remove')['default']
|
IconIcRoundRemove: typeof import('~icons/ic/round-remove')['default']
|
||||||
IconIcRoundSearch: typeof import('~icons/ic/round-search')['default']
|
IconIcRoundSearch: typeof import('~icons/ic/round-search')['default']
|
||||||
|
IconIcRoundUnfoldLess: typeof import('~icons/ic/round-unfold-less')['default']
|
||||||
|
IconIcRoundUnfoldMore: typeof import('~icons/ic/round-unfold-more')['default']
|
||||||
IconLocalActivity: typeof import('~icons/local/activity')['default']
|
IconLocalActivity: typeof import('~icons/local/activity')['default']
|
||||||
IconLocalBanner: typeof import('~icons/local/banner')['default']
|
IconLocalBanner: typeof import('~icons/local/banner')['default']
|
||||||
IconLocalCast: typeof import('~icons/local/cast')['default']
|
IconLocalCast: typeof import('~icons/local/cast')['default']
|
||||||
|
|||||||
70
src/typings/elegant-router.d.ts
vendored
70
src/typings/elegant-router.d.ts
vendored
@@ -24,16 +24,6 @@ declare module "@elegant-router/types" {
|
|||||||
"403": "/403";
|
"403": "/403";
|
||||||
"404": "/404";
|
"404": "/404";
|
||||||
"500": "/500";
|
"500": "/500";
|
||||||
"function": "/function";
|
|
||||||
"function_hide-child": "/function/hide-child";
|
|
||||||
"function_hide-child_one": "/function/hide-child/one";
|
|
||||||
"function_hide-child_three": "/function/hide-child/three";
|
|
||||||
"function_hide-child_two": "/function/hide-child/two";
|
|
||||||
"function_multi-tab": "/function/multi-tab";
|
|
||||||
"function_request": "/function/request";
|
|
||||||
"function_super-page": "/function/super-page";
|
|
||||||
"function_tab": "/function/tab";
|
|
||||||
"function_toggle-auth": "/function/toggle-auth";
|
|
||||||
"iframe-page": "/iframe-page/:url";
|
"iframe-page": "/iframe-page/:url";
|
||||||
"infra": "/infra";
|
"infra": "/infra";
|
||||||
"infra_rd-code": "/infra/rd-code";
|
"infra_rd-code": "/infra/rd-code";
|
||||||
@@ -46,33 +36,14 @@ declare module "@elegant-router/types" {
|
|||||||
"personal-center": "/personal-center";
|
"personal-center": "/personal-center";
|
||||||
"personal-center_my-application": "/personal-center/my-application";
|
"personal-center_my-application": "/personal-center/my-application";
|
||||||
"personal-center_my-item": "/personal-center/my-item";
|
"personal-center_my-item": "/personal-center/my-item";
|
||||||
"personal-center_my-monthly": "/personal-center/my-monthly";
|
|
||||||
"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_overtime-application": "/personal-center/overtime-application";
|
"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";
|
"personal-center_work-report": "/personal-center/work-report";
|
||||||
"plugin_barcode": "/plugin/barcode";
|
"personal-center_work-report_monthly": "/personal-center/work-report/monthly";
|
||||||
"plugin_charts": "/plugin/charts";
|
"personal-center_work-report_project": "/personal-center/work-report/project";
|
||||||
"plugin_charts_antv": "/plugin/charts/antv";
|
"personal-center_work-report_weekly": "/personal-center/work-report/weekly";
|
||||||
"plugin_charts_echarts": "/plugin/charts/echarts";
|
|
||||||
"plugin_charts_vchart": "/plugin/charts/vchart";
|
|
||||||
"plugin_copy": "/plugin/copy";
|
|
||||||
"plugin_excel": "/plugin/excel";
|
|
||||||
"plugin_gantt": "/plugin/gantt";
|
|
||||||
"plugin_gantt_dhtmlx": "/plugin/gantt/dhtmlx";
|
|
||||||
"plugin_gantt_vtable": "/plugin/gantt/vtable";
|
|
||||||
"plugin_icon": "/plugin/icon";
|
|
||||||
"plugin_map": "/plugin/map";
|
|
||||||
"plugin_pdf": "/plugin/pdf";
|
|
||||||
"plugin_pinyin": "/plugin/pinyin";
|
|
||||||
"plugin_print": "/plugin/print";
|
|
||||||
"plugin_swiper": "/plugin/swiper";
|
|
||||||
"plugin_tables": "/plugin/tables";
|
|
||||||
"plugin_tables_vtable": "/plugin/tables/vtable";
|
|
||||||
"plugin_typeit": "/plugin/typeit";
|
|
||||||
"plugin_video": "/plugin/video";
|
|
||||||
"product": "/product";
|
"product": "/product";
|
||||||
"product_dashboard": "/product/dashboard";
|
"product_dashboard": "/product/dashboard";
|
||||||
"product_list": "/product/list";
|
"product_list": "/product/list";
|
||||||
@@ -135,13 +106,11 @@ declare module "@elegant-router/types" {
|
|||||||
| "403"
|
| "403"
|
||||||
| "404"
|
| "404"
|
||||||
| "500"
|
| "500"
|
||||||
| "function"
|
|
||||||
| "iframe-page"
|
| "iframe-page"
|
||||||
| "infra"
|
| "infra"
|
||||||
| "login"
|
| "login"
|
||||||
| "metrics"
|
| "metrics"
|
||||||
| "personal-center"
|
| "personal-center"
|
||||||
| "plugin"
|
|
||||||
| "product"
|
| "product"
|
||||||
| "project"
|
| "project"
|
||||||
| "system"
|
| "system"
|
||||||
@@ -169,14 +138,6 @@ declare module "@elegant-router/types" {
|
|||||||
| "500"
|
| "500"
|
||||||
| "iframe-page"
|
| "iframe-page"
|
||||||
| "login"
|
| "login"
|
||||||
| "function_hide-child_one"
|
|
||||||
| "function_hide-child_three"
|
|
||||||
| "function_hide-child_two"
|
|
||||||
| "function_multi-tab"
|
|
||||||
| "function_request"
|
|
||||||
| "function_super-page"
|
|
||||||
| "function_tab"
|
|
||||||
| "function_toggle-auth"
|
|
||||||
| "infra_rd-code"
|
| "infra_rd-code"
|
||||||
| "infra_state-machine"
|
| "infra_state-machine"
|
||||||
| "metrics_member-efficiency"
|
| "metrics_member-efficiency"
|
||||||
@@ -184,29 +145,14 @@ declare module "@elegant-router/types" {
|
|||||||
| "metrics_worktime"
|
| "metrics_worktime"
|
||||||
| "personal-center_my-application"
|
| "personal-center_my-application"
|
||||||
| "personal-center_my-item"
|
| "personal-center_my-item"
|
||||||
| "personal-center_my-monthly"
|
|
||||||
| "personal-center_my-performance"
|
| "personal-center_my-performance"
|
||||||
| "personal-center_my-profile"
|
| "personal-center_my-profile"
|
||||||
| "personal-center_my-weekly"
|
|
||||||
| "personal-center_overtime-application"
|
| "personal-center_overtime-application"
|
||||||
| "personal-center_pending-approval"
|
| "personal-center_pending-approval"
|
||||||
| "plugin_barcode"
|
| "personal-center_work-report"
|
||||||
| "plugin_charts_antv"
|
| "personal-center_work-report_monthly"
|
||||||
| "plugin_charts_echarts"
|
| "personal-center_work-report_project"
|
||||||
| "plugin_charts_vchart"
|
| "personal-center_work-report_weekly"
|
||||||
| "plugin_copy"
|
|
||||||
| "plugin_excel"
|
|
||||||
| "plugin_gantt_dhtmlx"
|
|
||||||
| "plugin_gantt_vtable"
|
|
||||||
| "plugin_icon"
|
|
||||||
| "plugin_map"
|
|
||||||
| "plugin_pdf"
|
|
||||||
| "plugin_pinyin"
|
|
||||||
| "plugin_print"
|
|
||||||
| "plugin_swiper"
|
|
||||||
| "plugin_tables_vtable"
|
|
||||||
| "plugin_typeit"
|
|
||||||
| "plugin_video"
|
|
||||||
| "product_dashboard"
|
| "product_dashboard"
|
||||||
| "product_list"
|
| "product_list"
|
||||||
| "product_requirement"
|
| "product_requirement"
|
||||||
|
|||||||
20
src/typings/package.d.ts
vendored
20
src/typings/package.d.ts
vendored
@@ -1,20 +0,0 @@
|
|||||||
/// <reference types="@amap/amap-jsapi-types" />
|
|
||||||
/// <reference types="bmapgl" />
|
|
||||||
|
|
||||||
declare namespace BMap {
|
|
||||||
class Map extends BMapGL.Map {}
|
|
||||||
class Point extends BMapGL.Point {}
|
|
||||||
}
|
|
||||||
|
|
||||||
declare const TMap: any;
|
|
||||||
|
|
||||||
interface Window {
|
|
||||||
/**
|
|
||||||
* make baidu map request under https protocol
|
|
||||||
*
|
|
||||||
* - 0: http
|
|
||||||
* - 1: https
|
|
||||||
* - 2: https
|
|
||||||
*/
|
|
||||||
HOST_TYPE: '0' | '1' | '2';
|
|
||||||
}
|
|
||||||
27
src/utils/datetime.ts
Normal file
27
src/utils/datetime.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
|
/** 相对时间展示:刚刚 / N 分钟前 / N 小时前 / N 天前,超过 7 天回退完整日期 */
|
||||||
|
export function formatRelativeTime(value: string | number) {
|
||||||
|
const time = dayjs(value);
|
||||||
|
if (!time.isValid()) return '';
|
||||||
|
|
||||||
|
const now = dayjs();
|
||||||
|
const diffMinutes = now.diff(time, 'minute');
|
||||||
|
if (diffMinutes < 1) return '刚刚';
|
||||||
|
if (diffMinutes < 60) return `${diffMinutes} 分钟前`;
|
||||||
|
|
||||||
|
const diffHours = now.diff(time, 'hour');
|
||||||
|
if (diffHours < 24) return `${diffHours} 小时前`;
|
||||||
|
|
||||||
|
const diffDays = now.diff(time, 'day');
|
||||||
|
if (diffDays < 7) return `${diffDays} 天前`;
|
||||||
|
|
||||||
|
return time.format('YYYY-MM-DD HH:mm');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 绝对时间展示:YYYY-MM-DD HH:mm;空值或非法值回空串 */
|
||||||
|
export function formatDateTime(value: string | number | null | undefined) {
|
||||||
|
if (value === null || value === undefined || value === '') return '';
|
||||||
|
const time = dayjs(value);
|
||||||
|
return time.isValid() ? time.format('YYYY-MM-DD HH:mm') : '';
|
||||||
|
}
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue';
|
import { computed, reactive } from 'vue';
|
||||||
import type { Component } from 'vue';
|
import type { CSSProperties, Component } from 'vue';
|
||||||
import { getPaletteColorByNumber, mixColor } from '@sa/color';
|
|
||||||
import { loginModuleRecord } from '@/constants/app';
|
import { loginModuleRecord } from '@/constants/app';
|
||||||
import { useThemeStore } from '@/store/modules/theme';
|
import { useThemeStore } from '@/store/modules/theme';
|
||||||
import { $t } from '@/locales';
|
import { $t } from '@/locales';
|
||||||
@@ -31,46 +30,791 @@ const moduleMap: Record<UnionKey.LoginModule, LoginModule> = {
|
|||||||
|
|
||||||
const activeModule = computed(() => moduleMap[props.module || 'pwd-login']);
|
const activeModule = computed(() => moduleMap[props.module || 'pwd-login']);
|
||||||
|
|
||||||
const bgThemeColor = computed(() =>
|
const currentYear = new Date().getFullYear();
|
||||||
themeStore.darkMode ? getPaletteColorByNumber(themeStore.themeColor, 600) : themeStore.themeColor
|
|
||||||
);
|
|
||||||
|
|
||||||
const bgColor = computed(() => {
|
/** 登录页品牌色:取自公司 logo 的湛蓝,不跟随系统主题色(主题色偏紫,与企业蓝不符) */
|
||||||
const COLOR_WHITE = '#ffffff';
|
const LOGIN_BRAND = '#1e80df';
|
||||||
|
|
||||||
const ratio = themeStore.darkMode ? 0.5 : 0.2;
|
/** 鼠标视差:归一化指针位置,不同景深的层按系数反向位移 */
|
||||||
|
const pointer = reactive({ x: 0, y: 0 });
|
||||||
|
|
||||||
return mixColor(COLOR_WHITE, themeStore.themeColor, ratio);
|
function onPointerMove(event: MouseEvent) {
|
||||||
});
|
pointer.x = (event.clientX / window.innerWidth - 0.5) * 2;
|
||||||
|
pointer.y = (event.clientY / window.innerHeight - 0.5) * 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
function layerStyle(depth: number) {
|
||||||
|
return {
|
||||||
|
transform: `translate3d(${(-pointer.x * depth).toFixed(1)}px, ${(-pointer.y * depth).toFixed(1)}px, 0)`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 协作分支:角色 → 颜色 → 汇入主干的路径(git 分支汇流的意象),曲线两端均水平相切,过渡柔和 */
|
||||||
|
const branches = [
|
||||||
|
{
|
||||||
|
key: 'demand',
|
||||||
|
label: '需求',
|
||||||
|
color: '#f59e0b',
|
||||||
|
y0: 195,
|
||||||
|
mergeX: 660,
|
||||||
|
path: 'M -80,195 C 320,195 440,470 660,470',
|
||||||
|
dur: '7.5s',
|
||||||
|
begin: '0s'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'design',
|
||||||
|
label: '设计',
|
||||||
|
color: '#ec4899',
|
||||||
|
y0: 330,
|
||||||
|
mergeX: 780,
|
||||||
|
path: 'M -80,330 C 360,330 540,470 780,470',
|
||||||
|
dur: '6.5s',
|
||||||
|
begin: '1.2s'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'dev',
|
||||||
|
label: '开发',
|
||||||
|
color: '#0ea5e9',
|
||||||
|
y0: 615,
|
||||||
|
mergeX: 880,
|
||||||
|
path: 'M -80,615 C 380,615 560,470 880,470',
|
||||||
|
dur: '7s',
|
||||||
|
begin: '2.1s'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'test',
|
||||||
|
label: '测试',
|
||||||
|
color: '#22c55e',
|
||||||
|
y0: 745,
|
||||||
|
mergeX: 970,
|
||||||
|
path: 'M -80,745 C 420,745 620,470 970,470',
|
||||||
|
dur: '8s',
|
||||||
|
begin: '0.6s'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
/** 分支汇入主干的节点位置 */
|
||||||
|
const mergePoints = [
|
||||||
|
{ x: 660, color: '#f59e0b' },
|
||||||
|
{ x: 780, color: '#ec4899' },
|
||||||
|
{ x: 880, color: '#0ea5e9' },
|
||||||
|
{ x: 970, color: '#22c55e' }
|
||||||
|
];
|
||||||
|
|
||||||
|
/** 角色徽章在场景中的落位(跟随分支起始段) */
|
||||||
|
const roleChips: { label: string; color: string; style: CSSProperties }[] = [
|
||||||
|
{ label: '需求', color: '#f59e0b', style: { left: '5%', top: '20%', '--float-d': '0s' } },
|
||||||
|
{ label: '设计', color: '#ec4899', style: { left: '11%', top: '35%', '--float-d': '0.8s' } },
|
||||||
|
{ label: '开发', color: '#0ea5e9', style: { left: '8%', top: '66%', '--float-d': '1.6s' } },
|
||||||
|
{ label: '测试', color: '#22c55e', style: { left: '13%', top: '80%', '--float-d': '2.4s' } }
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 电能质量波形(公司主营:电能质量监测)
|
||||||
|
*
|
||||||
|
* 主干汇流完成后,尾段"输出"为基波 + 谐波叠加的正弦波组,寓意协作成果守护电能质量。
|
||||||
|
* 用二次贝塞尔 Q/T 拼接出周期波形,CSS 平移一个整周期实现无缝流动。
|
||||||
|
*/
|
||||||
|
interface WaveShape {
|
||||||
|
/** 波形中线 y */
|
||||||
|
mid: number;
|
||||||
|
/** 振幅 */
|
||||||
|
amp: number;
|
||||||
|
/** 半周期(x 方向) */
|
||||||
|
half: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildWavePath(shape: WaveShape, from: number, to: number) {
|
||||||
|
const { mid, amp, half } = shape;
|
||||||
|
let d = `M ${from} ${mid} Q ${from + half / 2} ${mid - amp} ${from + half} ${mid}`;
|
||||||
|
for (let x = from + 2 * half; x <= to; x += half) {
|
||||||
|
d += ` T ${x} ${mid}`;
|
||||||
|
}
|
||||||
|
return d;
|
||||||
|
}
|
||||||
|
|
||||||
|
const waves = [
|
||||||
|
// 基波:主题色,振幅最大
|
||||||
|
{ key: 'fundamental', mid: 470, amp: 26, half: 110, color: 'var(--brand)', width: 2, opacity: 0.5, dur: '7s' },
|
||||||
|
// 高次谐波:短周期小振幅
|
||||||
|
{ key: 'harmonic', mid: 470, amp: 10, half: 55, color: '#0ea5e9', width: 1.5, opacity: 0.45, dur: '4.5s' },
|
||||||
|
// 低频包络:慢速衬底
|
||||||
|
{ key: 'flux', mid: 474, amp: 40, half: 220, color: '#60a5fa', width: 2, opacity: 0.22, dur: '14s' }
|
||||||
|
].map(wave => ({
|
||||||
|
...wave,
|
||||||
|
path: buildWavePath(wave, 900 - wave.half * 2, 2000 + wave.half * 2),
|
||||||
|
shift: `${-2 * wave.half}px`
|
||||||
|
}));
|
||||||
|
|
||||||
|
/** 电力场景剪影:输电铁塔(底部接地,局部坐标基点为塔脚中心) */
|
||||||
|
const towers = [
|
||||||
|
{ x: 150, s: 1 },
|
||||||
|
{ x: 540, s: 0.85 },
|
||||||
|
{ x: 1280, s: 0.7 }
|
||||||
|
];
|
||||||
|
|
||||||
|
/** 塔间悬垂导线(悬链线意象),坐标对应各塔最宽横担端点 */
|
||||||
|
const powerLines = [
|
||||||
|
{ path: 'M -60,762 Q 30,800 92,750' },
|
||||||
|
{ path: 'M 208,750 Q 350,819 491,768' },
|
||||||
|
{ path: 'M 589,768 Q 914,867 1239,786' },
|
||||||
|
{ path: 'M 1321,786 Q 1430,824 1520,804' }
|
||||||
|
];
|
||||||
|
|
||||||
|
/** 导线上滑过的电流光点 */
|
||||||
|
const lineSparks = [
|
||||||
|
{ key: 'spark-1', path: 'M 208,750 Q 350,819 491,768', dur: '5s', begin: '0s' },
|
||||||
|
{ key: 'spark-2', path: 'M 589,768 Q 914,867 1239,786', dur: '7s', begin: '2s' }
|
||||||
|
];
|
||||||
|
|
||||||
|
/** 风机(新能源应用场景),dur 为叶轮旋转周期 */
|
||||||
|
const turbines = [
|
||||||
|
{ key: 'turbine-1', x: 715, s: 0.9, dur: '9s' },
|
||||||
|
{ key: 'turbine-2', x: 828, s: 0.6, dur: '13s' }
|
||||||
|
];
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="relative size-full flex-center overflow-hidden" :style="{ backgroundColor: bgColor }">
|
<div class="login-scene" :style="{ '--brand': LOGIN_BRAND }" @mousemove="onPointerMove">
|
||||||
<WaveBg :theme-color="bgThemeColor" />
|
<!-- 远景:浮尘微粒 -->
|
||||||
<ElCard class="relative z-4 w-auto rd-12px">
|
<div class="scene-motes" :style="layerStyle(6)"></div>
|
||||||
<div class="w-400px lt-sm:w-300px">
|
|
||||||
<header class="flex-y-center justify-between">
|
<!-- 中景:协作汇流图(需求/设计/开发/测试 → 主干 → 登录入口) -->
|
||||||
<SystemLogo class="text-64px text-primary lt-sm:text-48px" />
|
<div class="scene-graph" :style="layerStyle(14)">
|
||||||
<h3 class="text-28px text-primary font-500 lt-sm:text-22px">{{ $t('system.title') }}</h3>
|
<svg class="scene-graph__svg" viewBox="0 0 1440 900" preserveAspectRatio="xMidYMid slice" aria-hidden="true">
|
||||||
<div class="i-flex-col">
|
<defs>
|
||||||
<ThemeSchemaSwitch
|
<linearGradient id="trunk-grad" x1="0" y1="0" x2="1" y2="0">
|
||||||
:theme-schema="themeStore.themeScheme"
|
<!-- presentation attribute 不解析 CSS var,必须用 style -->
|
||||||
:show-tooltip="false"
|
<stop offset="0" style="stop-color: var(--brand); stop-opacity: 0" />
|
||||||
class="text-20px lt-sm:text-18px"
|
<stop offset="0.45" style="stop-color: var(--brand); stop-opacity: 0.85" />
|
||||||
@switch="themeStore.toggleThemeScheme"
|
<stop offset="1" style="stop-color: #0ea5e9; stop-opacity: 0.9" />
|
||||||
|
</linearGradient>
|
||||||
|
<!-- 每条分支一个渐变:起点透明、临近汇入处渐显,模拟光流自然汇聚 -->
|
||||||
|
<linearGradient
|
||||||
|
v-for="branch in branches"
|
||||||
|
:id="`branch-grad-${branch.key}`"
|
||||||
|
:key="`grad-${branch.key}`"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
:x1="-80"
|
||||||
|
:y1="branch.y0"
|
||||||
|
:x2="branch.mergeX"
|
||||||
|
:y2="470"
|
||||||
|
>
|
||||||
|
<stop offset="0" :stop-color="branch.color" stop-opacity="0" />
|
||||||
|
<stop offset="0.45" :stop-color="branch.color" stop-opacity="0.2" />
|
||||||
|
<stop offset="1" :stop-color="branch.color" stop-opacity="0.65" />
|
||||||
|
</linearGradient>
|
||||||
|
<filter id="trunk-glow" x="-30%" y="-300%" width="160%" height="700%">
|
||||||
|
<feGaussianBlur stdDeviation="5" result="blur" />
|
||||||
|
<feMerge>
|
||||||
|
<feMergeNode in="blur" />
|
||||||
|
<feMergeNode in="SourceGraphic" />
|
||||||
|
</feMerge>
|
||||||
|
</filter>
|
||||||
|
<!-- 波形渐显遮罩:汇流完成后波形才"长出来",遮罩静止、波形在其下平移 -->
|
||||||
|
<linearGradient id="wave-fade" gradientUnits="userSpaceOnUse" x1="920" y1="0" x2="1980" y2="0">
|
||||||
|
<stop offset="0" stop-color="#fff" stop-opacity="0" />
|
||||||
|
<stop offset="0.3" stop-color="#fff" stop-opacity="0.9" />
|
||||||
|
<stop offset="1" stop-color="#fff" stop-opacity="1" />
|
||||||
|
</linearGradient>
|
||||||
|
<mask id="wave-mask">
|
||||||
|
<rect x="920" y="330" width="1100" height="300" fill="url(#wave-fade)" />
|
||||||
|
</mask>
|
||||||
|
</defs>
|
||||||
|
|
||||||
|
<!-- 主干 -->
|
||||||
|
<path class="trunk" d="M -60,470 L 1520,470" stroke="url(#trunk-grad)" filter="url(#trunk-glow)" />
|
||||||
|
|
||||||
|
<!-- 四条角色分支 -->
|
||||||
|
<path
|
||||||
|
v-for="(branch, index) in branches"
|
||||||
|
:key="branch.key"
|
||||||
|
class="branch"
|
||||||
|
:d="branch.path"
|
||||||
|
:stroke="`url(#branch-grad-${branch.key})`"
|
||||||
|
:style="{ '--breathe-d': `${index * 1.4}s` }"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- 分支上行进的光点 -->
|
||||||
|
<circle
|
||||||
|
v-for="branch in branches"
|
||||||
|
:key="`dot-${branch.key}`"
|
||||||
|
r="3.5"
|
||||||
|
:fill="branch.color"
|
||||||
|
class="travel-dot"
|
||||||
|
:style="{ color: branch.color }"
|
||||||
|
>
|
||||||
|
<animateMotion :dur="branch.dur" :begin="branch.begin" repeatCount="indefinite" :path="branch.path" />
|
||||||
|
</circle>
|
||||||
|
|
||||||
|
<!-- 主干上行进的光点 -->
|
||||||
|
<circle r="3" fill="#5db1f5" class="travel-dot" style="color: #5db1f5">
|
||||||
|
<animateMotion dur="4.5s" begin="0.5s" repeatCount="indefinite" path="M 660,470 L 1520,470" />
|
||||||
|
</circle>
|
||||||
|
<circle r="2.5" fill="#5db1f5" class="travel-dot" style="color: #5db1f5">
|
||||||
|
<animateMotion dur="5.5s" begin="2.6s" repeatCount="indefinite" path="M 660,470 L 1520,470" />
|
||||||
|
</circle>
|
||||||
|
|
||||||
|
<!-- 电能质量波形:基波 + 谐波 + 低频包络,沿主干尾段流出 -->
|
||||||
|
<g mask="url(#wave-mask)">
|
||||||
|
<path
|
||||||
|
v-for="wave in waves"
|
||||||
|
:key="wave.key"
|
||||||
|
class="wave"
|
||||||
|
:d="wave.path"
|
||||||
|
:stroke-width="wave.width"
|
||||||
|
:style="{
|
||||||
|
stroke: wave.color,
|
||||||
|
opacity: wave.opacity,
|
||||||
|
'--wave-shift': wave.shift,
|
||||||
|
'--wave-dur': wave.dur
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<!-- 电力场景剪影:输电铁塔 + 悬垂导线 + 风机 -->
|
||||||
|
<g class="industry">
|
||||||
|
<!-- 输电铁塔 -->
|
||||||
|
<g
|
||||||
|
v-for="tower in towers"
|
||||||
|
:key="`tower-${tower.x}`"
|
||||||
|
:transform="`translate(${tower.x}, 870) scale(${tower.s})`"
|
||||||
|
>
|
||||||
|
<path d="M -32 0 L -10 -150 L 0 -178 L 10 -150 L 32 0" />
|
||||||
|
<path
|
||||||
|
d="M -28 -25 L 28 -45 M 28 -25 L -28 -45 M -24 -70 L 24 -88 M 24 -70 L -24 -88 M -19 -112 L 19 -126 M 19 -112 L -19 -126"
|
||||||
/>
|
/>
|
||||||
</div>
|
<path d="M -58 -120 L 58 -120 M -46 -150 L 46 -150" />
|
||||||
</header>
|
<path d="M -58 -120 L -58 -110 M 58 -120 L 58 -110 M -46 -150 L -46 -140 M 46 -150 L 46 -140" />
|
||||||
<main class="pt-15px">
|
</g>
|
||||||
<div class="pt-15px">
|
|
||||||
<Transition :name="themeStore.page.animateMode" mode="out-in" appear>
|
<!-- 塔间导线 -->
|
||||||
<component :is="activeModule.component" />
|
<path v-for="line in powerLines" :key="line.path" :d="line.path" />
|
||||||
</Transition>
|
|
||||||
</div>
|
<!-- 风机 -->
|
||||||
</main>
|
<g
|
||||||
</div>
|
v-for="turbine in turbines"
|
||||||
</ElCard>
|
:key="turbine.key"
|
||||||
|
:transform="`translate(${turbine.x}, 870) scale(${turbine.s})`"
|
||||||
|
>
|
||||||
|
<path d="M -3 0 L 0 -120 M 3 0 L 0 -120" />
|
||||||
|
<g transform="translate(0, -120)">
|
||||||
|
<g>
|
||||||
|
<path d="M 0 0 L 0 -52" />
|
||||||
|
<path d="M 0 0 L 0 -52" transform="rotate(120)" />
|
||||||
|
<path d="M 0 0 L 0 -52" transform="rotate(240)" />
|
||||||
|
<animateTransform
|
||||||
|
attributeName="transform"
|
||||||
|
type="rotate"
|
||||||
|
from="0 0 0"
|
||||||
|
to="360 0 0"
|
||||||
|
:dur="turbine.dur"
|
||||||
|
repeatCount="indefinite"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<circle cx="0" cy="-120" r="3" class="industry__hub" />
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<!-- 导线上的电流光点 -->
|
||||||
|
<circle v-for="spark in lineSparks" :key="spark.key" r="2.5" class="travel-dot industry__spark">
|
||||||
|
<animateMotion :dur="spark.dur" :begin="spark.begin" repeatCount="indefinite" :path="spark.path" />
|
||||||
|
</circle>
|
||||||
|
|
||||||
|
<!-- 汇入节点:脉冲 -->
|
||||||
|
<g v-for="point in mergePoints" :key="`merge-${point.x}`">
|
||||||
|
<circle :cx="point.x" cy="470" r="5" :fill="point.color" />
|
||||||
|
<circle :cx="point.x" cy="470" r="5" :stroke="point.color" class="merge-pulse" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
<!-- 角色徽章 -->
|
||||||
|
<span v-for="chip in roleChips" :key="chip.label" class="role-chip" :style="chip.style">
|
||||||
|
<i class="role-chip__dot" :style="{ backgroundColor: chip.color }"></i>
|
||||||
|
{{ chip.label }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 顶部:品牌 -->
|
||||||
|
<header class="scene-header reveal" style="--d: 0s">
|
||||||
|
<SystemLogo class="text-36px" />
|
||||||
|
<span class="scene-header__name">{{ $t('system.title') }}</span>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- 主文案 -->
|
||||||
|
<div class="scene-hero" :style="layerStyle(10)">
|
||||||
|
<p class="scene-hero__eyebrow reveal" style="--d: 0.15s">BUILD TOGETHER · GUARD POWER QUALITY</p>
|
||||||
|
<h1 class="scene-hero__slogan reveal" style="--d: 0.25s">
|
||||||
|
独行快
|
||||||
|
<span class="scene-hero__comma">,</span>
|
||||||
|
<br />
|
||||||
|
众行
|
||||||
|
<em>远</em>
|
||||||
|
</h1>
|
||||||
|
<p class="scene-hero__sub reveal" style="--d: 0.4s">每一次提交,都让电能质量的守护更进一步</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 登录卡片 -->
|
||||||
|
<div class="login-card">
|
||||||
|
<header class="login-card__header reveal" style="--d: 0.3s">
|
||||||
|
<SystemLogo class="login-card__logo text-52px" />
|
||||||
|
<h2 class="login-card__title">{{ $t('system.title') }}</h2>
|
||||||
|
<p class="login-card__subtitle">欢迎回来,开始今天的协作</p>
|
||||||
|
</header>
|
||||||
|
<main class="reveal" style="--d: 0.45s">
|
||||||
|
<Transition :name="themeStore.page.animateMode" mode="out-in" appear>
|
||||||
|
<component :is="activeModule.component" />
|
||||||
|
</Transition>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<footer class="scene-footer reveal" style="--d: 0.6s">
|
||||||
|
© {{ currentYear }} 南京灿能电力自动化股份有限公司 · {{ $t('system.title') }}
|
||||||
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped></style>
|
<style scoped lang="scss">
|
||||||
|
.login-scene {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-end;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
padding-right: 9vw;
|
||||||
|
overflow: hidden;
|
||||||
|
background:
|
||||||
|
radial-gradient(90% 70% at 80% 42%, color-mix(in srgb, var(--brand) 10%, transparent) 0%, transparent 60%),
|
||||||
|
radial-gradient(80% 60% at 6% 92%, rgb(56 189 248 / 10%) 0%, transparent 60%),
|
||||||
|
radial-gradient(70% 50% at 18% 8%, rgb(14 165 233 / 6%) 0%, transparent 55%),
|
||||||
|
linear-gradient(160deg, #f5f9ff 0%, #ecf2fb 50%, #fafcff 100%);
|
||||||
|
|
||||||
|
@media (max-width: 1023px) {
|
||||||
|
justify-content: center;
|
||||||
|
padding-right: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- 远景浮尘微粒 ---------- */
|
||||||
|
.scene-motes {
|
||||||
|
position: absolute;
|
||||||
|
inset: -40px;
|
||||||
|
background-image:
|
||||||
|
radial-gradient(2px 2px at 12% 22%, rgb(30 128 223 / 30%) 50%, transparent 51%),
|
||||||
|
radial-gradient(1.5px 1.5px at 28% 68%, rgb(23 44 84 / 16%) 50%, transparent 51%),
|
||||||
|
radial-gradient(2.5px 2.5px at 44% 12%, rgb(30 128 223 / 22%) 50%, transparent 51%),
|
||||||
|
radial-gradient(1.5px 1.5px at 58% 44%, rgb(23 44 84 / 12%) 50%, transparent 51%),
|
||||||
|
radial-gradient(2px 2px at 72% 78%, rgb(56 189 248 / 25%) 50%, transparent 51%),
|
||||||
|
radial-gradient(1.5px 1.5px at 86% 28%, rgb(23 44 84 / 14%) 50%, transparent 51%),
|
||||||
|
radial-gradient(2px 2px at 94% 62%, rgb(30 128 223 / 20%) 50%, transparent 51%),
|
||||||
|
radial-gradient(1.5px 1.5px at 6% 86%, rgb(56 189 248 / 18%) 50%, transparent 51%);
|
||||||
|
background-size: 520px 520px;
|
||||||
|
background-repeat: repeat;
|
||||||
|
animation: motes-breathe 6s ease-in-out infinite alternate;
|
||||||
|
transition: transform 0.25s ease-out;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes motes-breathe {
|
||||||
|
from {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- 协作汇流图 ---------- */
|
||||||
|
.scene-graph {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
transition: transform 0.25s ease-out;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scene-graph__svg {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trunk {
|
||||||
|
fill: none;
|
||||||
|
stroke-width: 2.5;
|
||||||
|
stroke-linecap: round;
|
||||||
|
}
|
||||||
|
|
||||||
|
.branch {
|
||||||
|
fill: none;
|
||||||
|
stroke-width: 2;
|
||||||
|
stroke-linecap: round;
|
||||||
|
animation: branch-breathe 6s ease-in-out infinite alternate;
|
||||||
|
animation-delay: var(--breathe-d, 0s);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes branch-breathe {
|
||||||
|
from {
|
||||||
|
opacity: 0.55;
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.travel-dot {
|
||||||
|
filter: drop-shadow(0 0 6px currentColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wave {
|
||||||
|
fill: none;
|
||||||
|
stroke-linecap: round;
|
||||||
|
animation: wave-drift var(--wave-dur) linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes wave-drift {
|
||||||
|
to {
|
||||||
|
transform: translateX(var(--wave-shift));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 电力场景剪影 */
|
||||||
|
.industry {
|
||||||
|
fill: none;
|
||||||
|
stroke: #424a8c;
|
||||||
|
stroke-width: 1.5;
|
||||||
|
stroke-linecap: round;
|
||||||
|
opacity: 0.22;
|
||||||
|
}
|
||||||
|
|
||||||
|
.industry__hub {
|
||||||
|
fill: #424a8c;
|
||||||
|
stroke: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.industry__spark {
|
||||||
|
fill: var(--brand);
|
||||||
|
color: var(--brand);
|
||||||
|
opacity: 0.65;
|
||||||
|
}
|
||||||
|
|
||||||
|
.merge-pulse {
|
||||||
|
fill: none;
|
||||||
|
stroke-width: 1.5;
|
||||||
|
transform-box: fill-box;
|
||||||
|
transform-origin: center;
|
||||||
|
animation: merge-pulse 2.6s ease-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes merge-pulse {
|
||||||
|
0% {
|
||||||
|
opacity: 0.8;
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
70%,
|
||||||
|
100% {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(3.2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 角色徽章 */
|
||||||
|
.role-chip {
|
||||||
|
position: absolute;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 7px 16px;
|
||||||
|
border: 1px solid rgb(30 35 80 / 10%);
|
||||||
|
border-radius: 999px;
|
||||||
|
font-size: 13px;
|
||||||
|
letter-spacing: 0.14em;
|
||||||
|
color: rgb(30 35 80 / 72%);
|
||||||
|
background: rgb(255 255 255 / 65%);
|
||||||
|
box-shadow: 0 6px 18px -8px rgb(23 92 171 / 22%);
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
animation: chip-float 5.5s ease-in-out infinite alternate;
|
||||||
|
animation-delay: var(--float-d, 0s);
|
||||||
|
}
|
||||||
|
|
||||||
|
.role-chip__dot {
|
||||||
|
width: 7px;
|
||||||
|
height: 7px;
|
||||||
|
border-radius: 50%;
|
||||||
|
box-shadow: 0 0 8px 1px currentcolor;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes chip-float {
|
||||||
|
from {
|
||||||
|
transform: translateY(-6px);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
transform: translateY(8px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- 品牌与文案 ---------- */
|
||||||
|
.scene-header {
|
||||||
|
position: absolute;
|
||||||
|
top: 40px;
|
||||||
|
left: 56px;
|
||||||
|
z-index: 2;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
color: #232850;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scene-header__name {
|
||||||
|
font-size: 17px;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scene-hero {
|
||||||
|
position: absolute;
|
||||||
|
top: 24%;
|
||||||
|
left: 6.5%;
|
||||||
|
z-index: 2;
|
||||||
|
color: #1b2050;
|
||||||
|
transition: transform 0.25s ease-out;
|
||||||
|
pointer-events: none;
|
||||||
|
|
||||||
|
@media (max-width: 1023px) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.scene-hero__eyebrow {
|
||||||
|
margin-bottom: 26px;
|
||||||
|
font-family: Georgia, 'Times New Roman', serif;
|
||||||
|
font-size: 13px;
|
||||||
|
letter-spacing: 0.46em;
|
||||||
|
color: rgb(30 35 80 / 45%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.scene-hero__slogan {
|
||||||
|
font-size: 64px;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1.3;
|
||||||
|
letter-spacing: 0.14em;
|
||||||
|
text-shadow: 0 8px 32px rgb(255 255 255 / 70%);
|
||||||
|
|
||||||
|
em {
|
||||||
|
font-style: normal;
|
||||||
|
background: linear-gradient(120deg, var(--brand) 0%, #0b66c3 55%, #38bdf8 100%);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
background-clip: text;
|
||||||
|
color: transparent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.scene-hero__comma {
|
||||||
|
color: rgb(30 35 80 / 28%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.scene-hero__sub {
|
||||||
|
margin-top: 26px;
|
||||||
|
font-size: 16px;
|
||||||
|
letter-spacing: 0.22em;
|
||||||
|
color: rgb(30 35 80 / 52%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.scene-footer {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 26px;
|
||||||
|
left: 56px;
|
||||||
|
z-index: 2;
|
||||||
|
font-size: 12px;
|
||||||
|
letter-spacing: 0.1em;
|
||||||
|
color: rgb(30 35 80 / 32%);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- 登录卡片(白玻璃质感) ---------- */
|
||||||
|
.login-card {
|
||||||
|
position: relative;
|
||||||
|
z-index: 3;
|
||||||
|
width: 420px;
|
||||||
|
padding: 44px 40px 40px;
|
||||||
|
border: 1px solid rgb(255 255 255 / 75%);
|
||||||
|
border-radius: 20px;
|
||||||
|
background: linear-gradient(168deg, rgb(255 255 255 / 82%) 0%, rgb(248 250 255 / 88%) 100%);
|
||||||
|
backdrop-filter: blur(20px) saturate(140%);
|
||||||
|
box-shadow:
|
||||||
|
0 30px 70px -24px rgb(23 92 171 / 26%),
|
||||||
|
0 0 0 1px rgb(30 35 80 / 5%),
|
||||||
|
0 1px 0 rgb(255 255 255 / 90%) inset;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-card__header {
|
||||||
|
margin-bottom: 32px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-card__logo {
|
||||||
|
display: block;
|
||||||
|
margin: 0 auto 16px;
|
||||||
|
filter: drop-shadow(0 10px 26px color-mix(in srgb, var(--brand) 35%, transparent));
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-card__title {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.1em;
|
||||||
|
color: #20254d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-card__subtitle {
|
||||||
|
margin-top: 10px;
|
||||||
|
font-size: 13.5px;
|
||||||
|
letter-spacing: 0.06em;
|
||||||
|
color: rgb(30 35 80 / 48%);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 卡片内表单:浅色质感统一覆盖(作用于子模块) */
|
||||||
|
.login-card :deep(.el-input__wrapper) {
|
||||||
|
height: 48px;
|
||||||
|
padding: 0 14px;
|
||||||
|
border-radius: 10px;
|
||||||
|
background-color: rgb(30 128 223 / 5%);
|
||||||
|
box-shadow: 0 0 0 1px rgb(30 35 80 / 12%) inset;
|
||||||
|
transition:
|
||||||
|
box-shadow 0.2s ease,
|
||||||
|
background-color 0.2s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
box-shadow: 0 0 0 1px color-mix(in srgb, var(--brand) 55%, rgb(30 35 80 / 20%)) inset;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-focus {
|
||||||
|
background-color: #fff;
|
||||||
|
box-shadow:
|
||||||
|
0 0 0 1.5px var(--brand) inset,
|
||||||
|
0 0 0 4px color-mix(in srgb, var(--brand) 14%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-input__inner {
|
||||||
|
color: #1f244a;
|
||||||
|
caret-color: var(--brand);
|
||||||
|
|
||||||
|
&::placeholder {
|
||||||
|
color: rgb(30 35 80 / 34%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-input__prefix,
|
||||||
|
.el-input__suffix {
|
||||||
|
font-size: 18px;
|
||||||
|
color: rgb(30 35 80 / 35%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-input__prefix {
|
||||||
|
margin-right: 6px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-card :deep(.el-form-item) {
|
||||||
|
margin-bottom: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-card :deep(.el-checkbox__label) {
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
color: rgb(30 35 80 / 58%);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 选中态跟随登录页品牌蓝,而非系统主题色 */
|
||||||
|
.login-card :deep(.el-checkbox__input.is-checked .el-checkbox__inner) {
|
||||||
|
border-color: var(--brand);
|
||||||
|
background-color: var(--brand);
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-card :deep(.el-checkbox__input.is-checked + .el-checkbox__label) {
|
||||||
|
color: var(--brand);
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-card :deep(.login-submit-button) {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 48px;
|
||||||
|
margin-top: 4px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 10px;
|
||||||
|
overflow: hidden;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.32em;
|
||||||
|
text-indent: 0.32em;
|
||||||
|
background: linear-gradient(135deg, var(--brand) 0%, color-mix(in srgb, var(--brand) 68%, #0a3f8f) 100%);
|
||||||
|
box-shadow: 0 12px 26px -10px color-mix(in srgb, var(--brand) 60%, transparent);
|
||||||
|
transition:
|
||||||
|
transform 0.2s ease,
|
||||||
|
box-shadow 0.2s ease,
|
||||||
|
filter 0.2s ease;
|
||||||
|
|
||||||
|
/* 流光扫过 */
|
||||||
|
&::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: -60%;
|
||||||
|
width: 40%;
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(100deg, transparent 0%, rgb(255 255 255 / 35%) 50%, transparent 100%);
|
||||||
|
transform: skewX(-20deg);
|
||||||
|
transition: left 0.55s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
filter: brightness(1.06);
|
||||||
|
box-shadow: 0 16px 32px -10px color-mix(in srgb, var(--brand) 70%, transparent);
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
left: 130%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-card :deep(.login-back-button) {
|
||||||
|
width: 100%;
|
||||||
|
height: 44px;
|
||||||
|
margin-top: 14px;
|
||||||
|
margin-left: 0;
|
||||||
|
border: 1px solid rgb(30 35 80 / 15%);
|
||||||
|
border-radius: 10px;
|
||||||
|
color: rgb(30 35 80 / 70%);
|
||||||
|
background: transparent;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: color-mix(in srgb, var(--brand) 60%, transparent);
|
||||||
|
color: var(--brand);
|
||||||
|
background: color-mix(in srgb, var(--brand) 6%, transparent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- 入场动效 ---------- */
|
||||||
|
.reveal {
|
||||||
|
animation: reveal-up 0.7s cubic-bezier(0.22, 0.61, 0.36, 1) both;
|
||||||
|
animation-delay: var(--d, 0s);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes reveal-up {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(18px);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -1,85 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import { computed } from 'vue';
|
|
||||||
import type { Component } from 'vue';
|
|
||||||
import { getPaletteColorByNumber, mixColor } from '@sa/color';
|
|
||||||
import { loginModuleRecord } from '@/constants/app';
|
|
||||||
import { useAppStore } from '@/store/modules/app';
|
|
||||||
import { useThemeStore } from '@/store/modules/theme';
|
|
||||||
import { $t } from '@/locales';
|
|
||||||
import PwdLogin from './modules/pwd-login.vue';
|
|
||||||
import ResetPwd from './modules/reset-pwd.vue';
|
|
||||||
|
|
||||||
defineOptions({ name: 'LoginPage' });
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
/** The login module */
|
|
||||||
module?: UnionKey.LoginModule;
|
|
||||||
}
|
|
||||||
|
|
||||||
const props = defineProps<Props>();
|
|
||||||
|
|
||||||
const appStore = useAppStore();
|
|
||||||
const themeStore = useThemeStore();
|
|
||||||
|
|
||||||
interface LoginModule {
|
|
||||||
label: App.I18n.I18nKey;
|
|
||||||
component: Component;
|
|
||||||
}
|
|
||||||
|
|
||||||
const moduleMap: Record<UnionKey.LoginModule, LoginModule> = {
|
|
||||||
'pwd-login': { label: loginModuleRecord['pwd-login'], component: PwdLogin },
|
|
||||||
'reset-pwd': { label: loginModuleRecord['reset-pwd'], component: ResetPwd }
|
|
||||||
};
|
|
||||||
|
|
||||||
const activeModule = computed(() => moduleMap[props.module || 'pwd-login']);
|
|
||||||
|
|
||||||
const bgThemeColor = computed(() =>
|
|
||||||
themeStore.darkMode ? getPaletteColorByNumber(themeStore.themeColor, 600) : themeStore.themeColor
|
|
||||||
);
|
|
||||||
|
|
||||||
const bgColor = computed(() => {
|
|
||||||
const COLOR_WHITE = '#ffffff';
|
|
||||||
|
|
||||||
const ratio = themeStore.darkMode ? 0.5 : 0.2;
|
|
||||||
|
|
||||||
return mixColor(COLOR_WHITE, themeStore.themeColor, ratio);
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="relative size-full flex-center overflow-hidden" :style="{ backgroundColor: bgColor }">
|
|
||||||
<WaveBg :theme-color="bgThemeColor" />
|
|
||||||
<ElCard class="relative z-4 w-auto rd-12px">
|
|
||||||
<div class="w-400px lt-sm:w-300px">
|
|
||||||
<header class="flex-y-center justify-between">
|
|
||||||
<SystemLogo class="text-64px text-primary lt-sm:text-48px" />
|
|
||||||
<h3 class="text-28px text-primary font-500 lt-sm:text-22px">{{ $t('system.title') }}</h3>
|
|
||||||
<div class="i-flex-col">
|
|
||||||
<ThemeSchemaSwitch
|
|
||||||
:theme-schema="themeStore.themeScheme"
|
|
||||||
:show-tooltip="false"
|
|
||||||
class="text-20px lt-sm:text-18px"
|
|
||||||
@switch="themeStore.toggleThemeScheme"
|
|
||||||
/>
|
|
||||||
<LangSwitch
|
|
||||||
v-if="themeStore.header.multilingual.visible"
|
|
||||||
:lang="appStore.locale"
|
|
||||||
:lang-options="appStore.localeOptions"
|
|
||||||
:show-tooltip="false"
|
|
||||||
@change-lang="appStore.changeLocale"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
<main class="pt-15px">
|
|
||||||
<div class="pt-15px">
|
|
||||||
<Transition :name="themeStore.page.animateMode" mode="out-in" appear>
|
|
||||||
<component :is="activeModule.component" />
|
|
||||||
</Transition>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
</ElCard>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped></style>
|
|
||||||
@@ -36,24 +36,38 @@ async function handleSubmit() {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<ElForm ref="formRef" :model="model" :rules="rules" size="large" :show-label="false" @keyup.enter="handleSubmit">
|
<ElForm ref="formRef" :model="model" :rules="rules" size="large" @keyup.enter="handleSubmit">
|
||||||
<ElFormItem prop="userName">
|
<ElFormItem prop="userName">
|
||||||
<ElInput v-model="model.userName" :placeholder="$t('page.login.common.userNamePlaceholder')" />
|
<ElInput v-model="model.userName" :placeholder="$t('page.login.common.userNamePlaceholder')">
|
||||||
|
<template #prefix>
|
||||||
|
<SvgIcon icon="mdi:account-outline" />
|
||||||
|
</template>
|
||||||
|
</ElInput>
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
<ElFormItem prop="password">
|
<ElFormItem prop="password">
|
||||||
<ElInput
|
<ElInput
|
||||||
v-model="model.password"
|
v-model="model.password"
|
||||||
type="password"
|
type="password"
|
||||||
show-password-on="click"
|
show-password
|
||||||
:placeholder="$t('page.login.common.passwordPlaceholder')"
|
:placeholder="$t('page.login.common.passwordPlaceholder')"
|
||||||
/>
|
>
|
||||||
|
<template #prefix>
|
||||||
|
<SvgIcon icon="mdi:lock-outline" />
|
||||||
|
</template>
|
||||||
|
</ElInput>
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
<ElSpace direction="vertical" :size="24" class="w-full" fill>
|
<div class="pb-18px">
|
||||||
<ElCheckbox>{{ $t('page.login.pwdLogin.rememberMe') }}</ElCheckbox>
|
<ElCheckbox>{{ $t('page.login.pwdLogin.rememberMe') }}</ElCheckbox>
|
||||||
<ElButton type="primary" size="large" round block :loading="authStore.loginLoading" @click="handleSubmit">
|
</div>
|
||||||
{{ $t('common.confirm') }}
|
<ElButton
|
||||||
</ElButton>
|
type="primary"
|
||||||
</ElSpace>
|
size="large"
|
||||||
|
class="login-submit-button"
|
||||||
|
:loading="authStore.loginLoading"
|
||||||
|
@click="handleSubmit"
|
||||||
|
>
|
||||||
|
{{ $t('route.login') }}
|
||||||
|
</ElButton>
|
||||||
</ElForm>
|
</ElForm>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -43,38 +43,60 @@ async function handleSubmit() {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<ElForm ref="formRef" :model="model" :rules="rules" size="large" :show-label="false" @keyup.enter="handleSubmit">
|
<ElForm ref="formRef" :model="model" :rules="rules" size="large" @keyup.enter="handleSubmit">
|
||||||
<ElFormItem prop="phone">
|
<ElFormItem prop="phone">
|
||||||
<ElInput v-model="model.phone" :placeholder="$t('page.login.common.phonePlaceholder')" />
|
<ElInput v-model="model.phone" :placeholder="$t('page.login.common.phonePlaceholder')">
|
||||||
|
<template #prefix>
|
||||||
|
<SvgIcon icon="mdi:cellphone" />
|
||||||
|
</template>
|
||||||
|
</ElInput>
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
<ElFormItem prop="code">
|
<ElFormItem prop="code">
|
||||||
<ElInput v-model="model.code" :placeholder="$t('page.login.common.codePlaceholder')" />
|
<ElInput v-model="model.code" :placeholder="$t('page.login.common.codePlaceholder')">
|
||||||
|
<template #prefix>
|
||||||
|
<SvgIcon icon="mdi:shield-check-outline" />
|
||||||
|
</template>
|
||||||
|
</ElInput>
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
<ElFormItem prop="password">
|
<ElFormItem prop="password">
|
||||||
<ElInput
|
<ElInput
|
||||||
v-model="model.password"
|
v-model="model.password"
|
||||||
type="password"
|
type="password"
|
||||||
show-password-on="click"
|
show-password
|
||||||
:placeholder="$t('page.login.common.passwordPlaceholder')"
|
:placeholder="$t('page.login.common.passwordPlaceholder')"
|
||||||
/>
|
>
|
||||||
|
<template #prefix>
|
||||||
|
<SvgIcon icon="mdi:lock-outline" />
|
||||||
|
</template>
|
||||||
|
</ElInput>
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
<ElFormItem prop="confirmPassword">
|
<ElFormItem prop="confirmPassword">
|
||||||
<ElInput
|
<ElInput
|
||||||
v-model="model.confirmPassword"
|
v-model="model.confirmPassword"
|
||||||
type="password"
|
type="password"
|
||||||
show-password-on="click"
|
show-password
|
||||||
:placeholder="$t('page.login.common.confirmPasswordPlaceholder')"
|
:placeholder="$t('page.login.common.confirmPasswordPlaceholder')"
|
||||||
/>
|
>
|
||||||
|
<template #prefix>
|
||||||
|
<SvgIcon icon="mdi:lock-check-outline" />
|
||||||
|
</template>
|
||||||
|
</ElInput>
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
<ElSpace direction="vertical" fill :size="18" class="w-full">
|
<ElButton type="primary" size="large" class="login-submit-button" @click="handleSubmit">
|
||||||
<ElButton type="primary" size="large" round @click="handleSubmit">
|
{{ $t('common.confirm') }}
|
||||||
{{ $t('common.confirm') }}
|
</ElButton>
|
||||||
</ElButton>
|
<ElButton size="large" class="login-back-button" @click="toggleLoginModule('pwd-login')">
|
||||||
<ElButton size="large" round @click="toggleLoginModule('pwd-login')">
|
{{ $t('page.login.common.back') }}
|
||||||
{{ $t('page.login.common.back') }}
|
</ElButton>
|
||||||
</ElButton>
|
|
||||||
</ElSpace>
|
|
||||||
</ElForm>
|
</ElForm>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped></style>
|
<style scoped>
|
||||||
|
.login-back-button {
|
||||||
|
width: 100%;
|
||||||
|
height: 44px;
|
||||||
|
margin-top: 14px;
|
||||||
|
margin-left: 0;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
<script setup lang="ts"></script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<LookForward />
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped></style>
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
<script setup lang="ts"></script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<LookForward />
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped></style>
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
<script setup lang="ts"></script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<LookForward />
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped></style>
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import { computed } from 'vue';
|
|
||||||
import { useRoute } from 'vue-router';
|
|
||||||
import { useRouterPush } from '@/hooks/common/router';
|
|
||||||
import { $t } from '@/locales';
|
|
||||||
|
|
||||||
const route = useRoute();
|
|
||||||
const { routerPushByKey } = useRouterPush();
|
|
||||||
|
|
||||||
const routeQuery = computed(() => JSON.stringify(route.query));
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div>
|
|
||||||
<LookForward>
|
|
||||||
<div>
|
|
||||||
<ElButton @click="routerPushByKey('function_tab')">{{ $t('page.function.multiTab.backTab') }}</ElButton>
|
|
||||||
<div class="py-24px">{{ $t('page.function.multiTab.routeParam') }}: {{ routeQuery }}</div>
|
|
||||||
</div>
|
|
||||||
</LookForward>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped></style>
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import { fetchCustomBackendError } from '@/service/api';
|
|
||||||
import { $t } from '@/locales';
|
|
||||||
|
|
||||||
async function logout() {
|
|
||||||
await fetchCustomBackendError('8888', $t('request.logoutMsg'));
|
|
||||||
}
|
|
||||||
|
|
||||||
async function logoutWithModal() {
|
|
||||||
await fetchCustomBackendError('7777', $t('request.logoutWithModalMsg'));
|
|
||||||
}
|
|
||||||
|
|
||||||
async function refreshToken() {
|
|
||||||
await fetchCustomBackendError('9999', $t('request.tokenExpired'));
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleRepeatedMessageError() {
|
|
||||||
await Promise.all([
|
|
||||||
fetchCustomBackendError('2222', $t('page.function.request.repeatedErrorMsg1')),
|
|
||||||
fetchCustomBackendError('2222', $t('page.function.request.repeatedErrorMsg1')),
|
|
||||||
fetchCustomBackendError('2222', $t('page.function.request.repeatedErrorMsg1')),
|
|
||||||
fetchCustomBackendError('3333', $t('page.function.request.repeatedErrorMsg2')),
|
|
||||||
fetchCustomBackendError('3333', $t('page.function.request.repeatedErrorMsg2')),
|
|
||||||
fetchCustomBackendError('3333', $t('page.function.request.repeatedErrorMsg2'))
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleRepeatedModalError() {
|
|
||||||
await Promise.all([
|
|
||||||
fetchCustomBackendError('7777', $t('request.logoutWithModalMsg')),
|
|
||||||
fetchCustomBackendError('7777', $t('request.logoutWithModalMsg')),
|
|
||||||
fetchCustomBackendError('7777', $t('request.logoutWithModalMsg'))
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<ElSpace direction="vertical" fill :size="16">
|
|
||||||
<ElCard :header="$t('request.logout')" class="card-wrapper">
|
|
||||||
<ElButton @click="logout">{{ $t('common.trigger') }}</ElButton>
|
|
||||||
</ElCard>
|
|
||||||
<ElCard :header="$t('request.logoutWithModal')" class="card-wrapper">
|
|
||||||
<ElButton @click="logoutWithModal">{{ $t('common.trigger') }}</ElButton>
|
|
||||||
</ElCard>
|
|
||||||
<ElCard :header="$t('request.refreshToken')" class="card-wrapper">
|
|
||||||
<ElButton @click="refreshToken">{{ $t('common.trigger') }}</ElButton>
|
|
||||||
</ElCard>
|
|
||||||
<ElCard :header="$t('page.function.request.repeatedErrorOccurOnce')" class="card-wrapper">
|
|
||||||
<ElButton @click="handleRepeatedMessageError">{{ $t('page.function.request.repeatedError') }}(Message)</ElButton>
|
|
||||||
<ElButton class="ml-12px" @click="handleRepeatedModalError">
|
|
||||||
{{ $t('page.function.request.repeatedError') }}(Modal)
|
|
||||||
</ElButton>
|
|
||||||
</ElCard>
|
|
||||||
</ElSpace>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped></style>
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
<script setup lang="ts"></script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<LookForward />
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped></style>
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import { ref } from 'vue';
|
|
||||||
import { useTabStore } from '@/store/modules/tab';
|
|
||||||
import { useRouterPush } from '@/hooks/common/router';
|
|
||||||
import { $t } from '@/locales';
|
|
||||||
|
|
||||||
defineOptions({ name: 'TabPage' });
|
|
||||||
|
|
||||||
const tabStore = useTabStore();
|
|
||||||
const { routerPushByKey } = useRouterPush();
|
|
||||||
|
|
||||||
const tabLabel = ref('');
|
|
||||||
|
|
||||||
function changeTabLabel() {
|
|
||||||
tabStore.setTabLabel(tabLabel.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
function resetTabLabel() {
|
|
||||||
tabStore.resetTabLabel();
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<ElSpace direction="vertical" fill :size="16">
|
|
||||||
<ElCard :header="$t('page.function.tab.tabOperate.title')" class="card-wrapper">
|
|
||||||
<ElDivider content-position="left">{{ $t('page.function.tab.tabOperate.addTab') }}</ElDivider>
|
|
||||||
<ElButton @click="routerPushByKey('system_user')">{{ $t('page.function.tab.tabOperate.addTabDesc') }}</ElButton>
|
|
||||||
<ElDivider content-position="left">{{ $t('page.function.tab.tabOperate.closeTab') }}</ElDivider>
|
|
||||||
<ElSpace>
|
|
||||||
<ElButton @click="tabStore.removeActiveTab">
|
|
||||||
{{ $t('page.function.tab.tabOperate.closeCurrentTab') }}
|
|
||||||
</ElButton>
|
|
||||||
<ElButton @click="tabStore.removeTabByRouteName('system_user')">
|
|
||||||
{{ $t('page.function.tab.tabOperate.closeAboutTab') }}
|
|
||||||
</ElButton>
|
|
||||||
</ElSpace>
|
|
||||||
<ElDivider content-position="left">{{ $t('page.function.tab.tabOperate.addMultiTab') }}</ElDivider>
|
|
||||||
<ElSpace>
|
|
||||||
<ElButton @click="routerPushByKey('function_multi-tab')">
|
|
||||||
{{ $t('page.function.tab.tabOperate.addMultiTabDesc1') }}
|
|
||||||
</ElButton>
|
|
||||||
<ElButton @click="routerPushByKey('function_multi-tab', { query: { a: '1' } })">
|
|
||||||
{{ $t('page.function.tab.tabOperate.addMultiTabDesc2') }}
|
|
||||||
</ElButton>
|
|
||||||
</ElSpace>
|
|
||||||
</ElCard>
|
|
||||||
<ElCard :header="$t('page.function.tab.tabTitle.title')" class="card-wrapper">
|
|
||||||
<ElDivider content-position="left">{{ $t('page.function.tab.tabTitle.changeTitle') }}</ElDivider>
|
|
||||||
<ElInput v-model="tabLabel" class="max-w-240px">
|
|
||||||
<template #append>
|
|
||||||
<ElButton type="primary" @click="changeTabLabel">{{ $t('page.function.tab.tabTitle.change') }}</ElButton>
|
|
||||||
</template>
|
|
||||||
</ElInput>
|
|
||||||
<ElDivider content-position="left">{{ $t('page.function.tab.tabTitle.resetTitle') }}</ElDivider>
|
|
||||||
<ElButton type="danger" plain class="w-80px" @click="resetTabLabel">
|
|
||||||
{{ $t('page.function.tab.tabTitle.reset') }}
|
|
||||||
</ElButton>
|
|
||||||
</ElCard>
|
|
||||||
</ElSpace>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped></style>
|
|
||||||
@@ -1,99 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import { computed, ref } from 'vue';
|
|
||||||
import { useRoute } from 'vue-router';
|
|
||||||
import { useLoading } from '@sa/hooks';
|
|
||||||
import { useAppStore } from '@/store/modules/app';
|
|
||||||
import { useAuthStore } from '@/store/modules/auth';
|
|
||||||
import { useTabStore } from '@/store/modules/tab';
|
|
||||||
import { useAuth } from '@/hooks/business/auth';
|
|
||||||
import { $t } from '@/locales';
|
|
||||||
|
|
||||||
defineOptions({ name: 'ToggleAuth' });
|
|
||||||
|
|
||||||
const route = useRoute();
|
|
||||||
const appStore = useAppStore();
|
|
||||||
const authStore = useAuthStore();
|
|
||||||
const tabStore = useTabStore();
|
|
||||||
const { hasAuth } = useAuth();
|
|
||||||
const { loading, startLoading, endLoading } = useLoading();
|
|
||||||
|
|
||||||
type AccountKey = 'super' | 'admin' | 'user';
|
|
||||||
|
|
||||||
interface Account {
|
|
||||||
key: AccountKey;
|
|
||||||
label: string;
|
|
||||||
userName: string;
|
|
||||||
password: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const accounts = computed<Account[]>(() => [
|
|
||||||
{
|
|
||||||
key: 'super',
|
|
||||||
label: $t('page.login.pwdLogin.superAdmin'),
|
|
||||||
userName: 'Super',
|
|
||||||
password: '123456'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'admin',
|
|
||||||
label: $t('page.login.pwdLogin.admin'),
|
|
||||||
userName: 'Admin',
|
|
||||||
password: '123456'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'user',
|
|
||||||
label: $t('page.login.pwdLogin.user'),
|
|
||||||
userName: 'User',
|
|
||||||
password: '123456'
|
|
||||||
}
|
|
||||||
]);
|
|
||||||
|
|
||||||
const loginAccount = ref<AccountKey>('super');
|
|
||||||
|
|
||||||
async function handleToggleAccount(account: Account) {
|
|
||||||
loginAccount.value = account.key;
|
|
||||||
|
|
||||||
startLoading();
|
|
||||||
await authStore.login(account.userName, account.password, false);
|
|
||||||
tabStore.initTabStore(route);
|
|
||||||
endLoading();
|
|
||||||
appStore.reloadPage();
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<ElSpace direction="vertical" fill :size="16">
|
|
||||||
<ElCard :header="$t('route.function_toggle-auth')" class="card-wrapper">
|
|
||||||
<ElDescriptions direction="vertical" border :column="1">
|
|
||||||
<ElDescriptionsItem :label="$t('page.system.user.userRole')">
|
|
||||||
<ElSpace>
|
|
||||||
<ElTag v-for="role in authStore.userInfo.roles" :key="role">{{ role }}</ElTag>
|
|
||||||
</ElSpace>
|
|
||||||
</ElDescriptionsItem>
|
|
||||||
<ElDescriptionsItem ions-item :label="$t('page.function.toggleAuth.toggleAccount')">
|
|
||||||
<ElSpace>
|
|
||||||
<ElButton
|
|
||||||
v-for="account in accounts"
|
|
||||||
:key="account.key"
|
|
||||||
:loading="loading && loginAccount === account.key"
|
|
||||||
:disabled="loading && loginAccount !== account.key"
|
|
||||||
@click="handleToggleAccount(account)"
|
|
||||||
>
|
|
||||||
{{ account.label }}
|
|
||||||
</ElButton>
|
|
||||||
</ElSpace>
|
|
||||||
</ElDescriptionsItem>
|
|
||||||
</ElDescriptions>
|
|
||||||
</ElCard>
|
|
||||||
<ElCard :header="$t('page.function.toggleAuth.authHook')" class="card-wrapper">
|
|
||||||
<ElSpace>
|
|
||||||
<ElButton v-if="hasAuth('B_CODE1')">{{ $t('page.function.toggleAuth.superAdminVisible') }}</ElButton>
|
|
||||||
<ElButton v-if="hasAuth('B_CODE2')">{{ $t('page.function.toggleAuth.adminVisible') }}</ElButton>
|
|
||||||
<ElButton v-if="hasAuth('B_CODE3')">
|
|
||||||
{{ $t('page.function.toggleAuth.adminOrUserVisible') }}
|
|
||||||
</ElButton>
|
|
||||||
</ElSpace>
|
|
||||||
</ElCard>
|
|
||||||
</ElSpace>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped></style>
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
<template>
|
|
||||||
<LookForward title="我的月报" subtitle="功能建设中,敬请期待" />
|
|
||||||
</template>
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
<template>
|
|
||||||
<LookForward title="我的周报" subtitle="功能建设中,敬请期待" />
|
|
||||||
</template>
|
|
||||||
@@ -1,20 +1,14 @@
|
|||||||
<script setup lang="tsx">
|
<script setup lang="tsx">
|
||||||
import { computed, markRaw, reactive, ref } from 'vue';
|
import { computed, markRaw, reactive, ref } from 'vue';
|
||||||
import { ElButton, ElMessageBox, ElTag } from 'element-plus';
|
import { ElButton, ElTag } from 'element-plus';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import {
|
import { fetchExportOvertimeApplications, fetchGetOvertimeApplicationPage } from '@/service/api';
|
||||||
fetchCancelOvertimeApplication,
|
|
||||||
fetchDeleteOvertimeApplication,
|
|
||||||
fetchExportOvertimeApplications,
|
|
||||||
fetchGetOvertimeApplicationPage
|
|
||||||
} from '@/service/api';
|
|
||||||
import { useUIPaginatedTable } from '@/hooks/common/table';
|
import { useUIPaginatedTable } from '@/hooks/common/table';
|
||||||
import BusinessTableActionCell, { type BusinessTableAction } from '@/components/custom/business-table-action-cell';
|
import BusinessTableActionCell, { type BusinessTableAction } from '@/components/custom/business-table-action-cell';
|
||||||
import OvertimeApplicationActionDialog from './modules/overtime-application-action-dialog.vue';
|
import OvertimeApplicationApprovalRecordDialog from './modules/overtime-application-approval-record-dialog.vue';
|
||||||
import OvertimeApplicationDetailDialog from './modules/overtime-application-detail-dialog.vue';
|
import OvertimeApplicationDetailDialog from './modules/overtime-application-detail-dialog.vue';
|
||||||
import OvertimeApplicationOperateDialog from './modules/overtime-application-operate-dialog.vue';
|
import OvertimeApplicationOperateDialog from './modules/overtime-application-operate-dialog.vue';
|
||||||
import OvertimeApplicationSearch from './modules/overtime-application-search.vue';
|
import OvertimeApplicationSearch from './modules/overtime-application-search.vue';
|
||||||
import OvertimeApplicationStatusLogDialog from './modules/overtime-application-status-log-dialog.vue';
|
|
||||||
import {
|
import {
|
||||||
downloadBlob,
|
downloadBlob,
|
||||||
formatEmptyText,
|
formatEmptyText,
|
||||||
@@ -23,16 +17,13 @@ import {
|
|||||||
getOvertimeApplicationStatusLabel,
|
getOvertimeApplicationStatusLabel,
|
||||||
resolveOvertimeApplicationStatusTagType
|
resolveOvertimeApplicationStatusTagType
|
||||||
} from './modules/overtime-application-shared';
|
} 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 IconMdiEyeOutline from '~icons/mdi/eye-outline';
|
||||||
import IconMdiHistory from '~icons/mdi/history';
|
import IconMdiFileDocumentCheckOutline from '~icons/mdi/file-document-check-outline';
|
||||||
import IconMdiPencilOutline from '~icons/mdi/pencil-outline';
|
import IconMdiPencilOutline from '~icons/mdi/pencil-outline';
|
||||||
|
|
||||||
defineOptions({ name: 'OvertimeApplication' });
|
defineOptions({ name: 'OvertimeApplication' });
|
||||||
|
|
||||||
type OvertimeApplicationPageResponse = Awaited<ReturnType<typeof fetchGetOvertimeApplicationPage>>;
|
type OvertimeApplicationPageResponse = Awaited<ReturnType<typeof fetchGetOvertimeApplicationPage>>;
|
||||||
type ActionType = 'cancel';
|
|
||||||
|
|
||||||
function getInitSearchParams(): Api.OvertimeApplication.OvertimeApplicationSearchParams {
|
function getInitSearchParams(): Api.OvertimeApplication.OvertimeApplicationSearchParams {
|
||||||
return {
|
return {
|
||||||
@@ -69,20 +60,15 @@ function transformPageResult(response: OvertimeApplicationPageResponse, pageNo:
|
|||||||
const searchParams = reactive(getInitSearchParams());
|
const searchParams = reactive(getInitSearchParams());
|
||||||
const operateVisible = ref(false);
|
const operateVisible = ref(false);
|
||||||
const detailVisible = ref(false);
|
const detailVisible = ref(false);
|
||||||
const statusLogVisible = ref(false);
|
const approvalRecordVisible = ref(false);
|
||||||
const actionVisible = ref(false);
|
|
||||||
const operateType = ref<'add' | 'edit'>('add');
|
const operateType = ref<'add' | 'edit'>('add');
|
||||||
const currentRow = ref<Api.OvertimeApplication.OvertimeApplication | null>(null);
|
const currentRow = ref<Api.OvertimeApplication.OvertimeApplication | null>(null);
|
||||||
const currentActionType = ref<ActionType>('cancel');
|
|
||||||
const actionSubmitting = ref(false);
|
|
||||||
const exporting = ref(false);
|
const exporting = ref(false);
|
||||||
|
|
||||||
const ACTION_ICON_MAP = {
|
const ACTION_ICON_MAP = {
|
||||||
detail: markRaw(IconMdiEyeOutline),
|
detail: markRaw(IconMdiEyeOutline),
|
||||||
statusLog: markRaw(IconMdiHistory),
|
approvalRecord: markRaw(IconMdiFileDocumentCheckOutline),
|
||||||
edit: markRaw(IconMdiPencilOutline),
|
edit: markRaw(IconMdiPencilOutline)
|
||||||
cancel: markRaw(IconMdiCloseCircleOutline),
|
|
||||||
delete: markRaw(IconMdiDeleteOutline)
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const { columns, columnChecks, data, loading, getDataByPage, mobilePagination } = useUIPaginatedTable<
|
const { columns, columnChecks, data, loading, getDataByPage, mobilePagination } = useUIPaginatedTable<
|
||||||
@@ -113,14 +99,14 @@ const { columns, columnChecks, data, loading, getDataByPage, mobilePagination }
|
|||||||
prop: 'overtimeReason',
|
prop: 'overtimeReason',
|
||||||
label: '加班原因',
|
label: '加班原因',
|
||||||
minWidth: 180,
|
minWidth: 180,
|
||||||
showOverflowTooltip: true,
|
className: 'overtime-application__cell-ellipsis',
|
||||||
formatter: row => formatEmptyText(row.overtimeReason)
|
formatter: row => formatEmptyText(row.overtimeReason)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
prop: 'overtimeContent',
|
prop: 'overtimeContent',
|
||||||
label: '加班内容',
|
label: '加班内容',
|
||||||
minWidth: 200,
|
minWidth: 200,
|
||||||
showOverflowTooltip: true,
|
className: 'overtime-application__cell-ellipsis',
|
||||||
formatter: row => formatEmptyText(row.overtimeContent)
|
formatter: row => formatEmptyText(row.overtimeContent)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -134,17 +120,17 @@ const { columns, columnChecks, data, loading, getDataByPage, mobilePagination }
|
|||||||
</ElTag>
|
</ElTag>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
{ prop: 'approverName', label: '审核人', minWidth: 120, showOverflowTooltip: true },
|
{ prop: 'approverName', label: '审核人', minWidth: 80, showOverflowTooltip: true },
|
||||||
{
|
{
|
||||||
prop: 'submitTime',
|
prop: 'submitTime',
|
||||||
label: '提交时间',
|
label: '提交时间',
|
||||||
minWidth: 170,
|
minWidth: 150,
|
||||||
formatter: row => formatOvertimeDateTime(row.submitTime)
|
formatter: row => formatOvertimeDateTime(row.submitTime)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
prop: 'approvalTime',
|
prop: 'approvalTime',
|
||||||
label: '审核时间',
|
label: '审核时间',
|
||||||
minWidth: 170,
|
minWidth: 150,
|
||||||
formatter: row => formatOvertimeDateTime(row.approvalTime)
|
formatter: row => formatOvertimeDateTime(row.approvalTime)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -171,7 +157,7 @@ function getRowActions(row: Api.OvertimeApplication.OvertimeApplication): Busine
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
if ((row.statusCode === 'rejected' || row.statusCode === 'cancelled') && row.allowEdit) {
|
if (row.statusCode === 'rejected' && row.allowEdit) {
|
||||||
actions.push({
|
actions.push({
|
||||||
key: 'edit',
|
key: 'edit',
|
||||||
label: '修改',
|
label: '修改',
|
||||||
@@ -181,31 +167,13 @@ function getRowActions(row: Api.OvertimeApplication.OvertimeApplication): Busine
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
actions.push({
|
if (['approved', 'rejected'].includes(row.statusCode)) {
|
||||||
key: 'status-log',
|
|
||||||
label: '状态日志',
|
|
||||||
buttonType: 'info',
|
|
||||||
icon: ACTION_ICON_MAP.statusLog,
|
|
||||||
onClick: () => openStatusLog(row)
|
|
||||||
});
|
|
||||||
|
|
||||||
if (row.statusCode === 'pending') {
|
|
||||||
actions.push({
|
actions.push({
|
||||||
key: 'cancel',
|
key: 'approval-record',
|
||||||
label: '撤销',
|
label: '审批记录',
|
||||||
buttonType: 'danger',
|
buttonType: 'info',
|
||||||
icon: ACTION_ICON_MAP.cancel,
|
icon: ACTION_ICON_MAP.approvalRecord,
|
||||||
onClick: () => openCancel(row)
|
onClick: () => openApprovalRecord(row)
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (row.statusCode === 'cancelled') {
|
|
||||||
actions.push({
|
|
||||||
key: 'delete',
|
|
||||||
label: '删除',
|
|
||||||
buttonType: 'danger',
|
|
||||||
icon: ACTION_ICON_MAP.delete,
|
|
||||||
onClick: () => handleDelete(row)
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -229,15 +197,9 @@ function openDetail(row: Api.OvertimeApplication.OvertimeApplication) {
|
|||||||
detailVisible.value = true;
|
detailVisible.value = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function openStatusLog(row: Api.OvertimeApplication.OvertimeApplication) {
|
function openApprovalRecord(row: Api.OvertimeApplication.OvertimeApplication) {
|
||||||
currentRow.value = row;
|
currentRow.value = row;
|
||||||
statusLogVisible.value = true;
|
approvalRecordVisible.value = true;
|
||||||
}
|
|
||||||
|
|
||||||
function openCancel(row: Api.OvertimeApplication.OvertimeApplication) {
|
|
||||||
currentRow.value = row;
|
|
||||||
currentActionType.value = 'cancel';
|
|
||||||
actionVisible.value = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function reloadTable(page = searchParams.pageNo ?? 1) {
|
async function reloadTable(page = searchParams.pageNo ?? 1) {
|
||||||
@@ -259,49 +221,6 @@ function handleSubmitted() {
|
|||||||
reloadTable(searchParams.pageNo ?? 1);
|
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() {
|
async function handleExport() {
|
||||||
exporting.value = true;
|
exporting.value = true;
|
||||||
const { error, data: blob } = await fetchExportOvertimeApplications(searchParams);
|
const { error, data: blob } = await fetchExportOvertimeApplications(searchParams);
|
||||||
@@ -373,14 +292,7 @@ async function handleExport() {
|
|||||||
|
|
||||||
<OvertimeApplicationDetailDialog v-model:visible="detailVisible" :row-data="currentRow" />
|
<OvertimeApplicationDetailDialog v-model:visible="detailVisible" :row-data="currentRow" />
|
||||||
|
|
||||||
<OvertimeApplicationStatusLogDialog v-model:visible="statusLogVisible" :row-data="currentRow" />
|
<OvertimeApplicationApprovalRecordDialog v-model:visible="approvalRecordVisible" :row-data="currentRow" />
|
||||||
|
|
||||||
<OvertimeApplicationActionDialog
|
|
||||||
v-model:visible="actionVisible"
|
|
||||||
:action-type="currentActionType"
|
|
||||||
:loading="actionSubmitting"
|
|
||||||
@submit="handleActionSubmit"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -398,4 +310,12 @@ async function handleExport() {
|
|||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 加班原因/加班内容:单元格内容溢出时仅显示省略号,不弹出 tooltip */
|
||||||
|
:deep(.overtime-application__cell-ellipsis .cell) {
|
||||||
|
display: block;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import BusinessFormDialog from '@/components/custom/business-form-dialog.vue';
|
|||||||
|
|
||||||
defineOptions({ name: 'OvertimeApplicationActionDialog' });
|
defineOptions({ name: 'OvertimeApplicationActionDialog' });
|
||||||
|
|
||||||
type ActionType = 'approve' | 'reject' | 'cancel';
|
type ActionType = 'approve' | 'reject';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
actionType: ActionType;
|
actionType: ActionType;
|
||||||
@@ -34,8 +34,7 @@ const model = reactive({
|
|||||||
const title = computed(() => {
|
const title = computed(() => {
|
||||||
const map: Record<ActionType, string> = {
|
const map: Record<ActionType, string> = {
|
||||||
approve: '通过加班申请',
|
approve: '通过加班申请',
|
||||||
reject: '退回加班申请',
|
reject: '退回加班申请'
|
||||||
cancel: '撤销加班申请'
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return map[props.actionType];
|
return map[props.actionType];
|
||||||
@@ -44,8 +43,7 @@ const title = computed(() => {
|
|||||||
const reasonLabel = computed(() => {
|
const reasonLabel = computed(() => {
|
||||||
const map: Record<ActionType, string> = {
|
const map: Record<ActionType, string> = {
|
||||||
approve: '审核意见',
|
approve: '审核意见',
|
||||||
reject: '退回原因',
|
reject: '退回原因'
|
||||||
cancel: '撤销原因'
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return map[props.actionType];
|
return map[props.actionType];
|
||||||
@@ -58,7 +56,7 @@ const reasonPlaceholder = computed(() => {
|
|||||||
return `请输入${reasonLabel.value}`;
|
return `请输入${reasonLabel.value}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return props.actionType === 'cancel' ? '可填写撤销原因' : '可填写审核意见';
|
return '可填写审核意见';
|
||||||
});
|
});
|
||||||
|
|
||||||
const rules = computed(() => ({
|
const rules = computed(() => ({
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, watch } from 'vue';
|
||||||
|
import { fetchGetOvertimeApplicationApprovalRecords } from '@/service/api';
|
||||||
|
import BusinessFormDialog from '@/components/custom/business-form-dialog.vue';
|
||||||
|
import {
|
||||||
|
formatEmptyText,
|
||||||
|
formatOvertimeDateTime,
|
||||||
|
getOvertimeApplicationStatusLabel
|
||||||
|
} from './overtime-application-shared';
|
||||||
|
|
||||||
|
defineOptions({ name: 'OvertimeApplicationApprovalRecordDialog' });
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
rowData?: Api.OvertimeApplication.OvertimeApplication | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<Props>();
|
||||||
|
|
||||||
|
const visible = defineModel<boolean>('visible', {
|
||||||
|
default: false
|
||||||
|
});
|
||||||
|
|
||||||
|
const loading = ref(false);
|
||||||
|
const records = ref<Api.OvertimeApplication.OvertimeApplicationApprovalRecord[]>([]);
|
||||||
|
|
||||||
|
async function loadRecords() {
|
||||||
|
if (!props.rowData?.id) {
|
||||||
|
records.value = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
loading.value = true;
|
||||||
|
const { error, data } = await fetchGetOvertimeApplicationApprovalRecords(props.rowData.id);
|
||||||
|
loading.value = false;
|
||||||
|
|
||||||
|
records.value = error || !data ? [] : data;
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => visible.value,
|
||||||
|
value => {
|
||||||
|
if (value) {
|
||||||
|
loadRecords();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<BusinessFormDialog
|
||||||
|
v-model="visible"
|
||||||
|
title="加班申请审批记录"
|
||||||
|
width="820px"
|
||||||
|
:loading="loading"
|
||||||
|
:show-footer="false"
|
||||||
|
max-body-height="72vh"
|
||||||
|
>
|
||||||
|
<ElTable border :data="records">
|
||||||
|
<ElTableColumn prop="approvalRound" label="轮次" width="80" />
|
||||||
|
<ElTableColumn label="结论" width="110">
|
||||||
|
<template #default="{ row }">{{ getOvertimeApplicationStatusLabel(row.conclusion) }}</template>
|
||||||
|
</ElTableColumn>
|
||||||
|
<ElTableColumn label="审批意见" min-width="240" show-overflow-tooltip>
|
||||||
|
<template #default="{ row }">{{ formatEmptyText(row.opinion) }}</template>
|
||||||
|
</ElTableColumn>
|
||||||
|
<ElTableColumn prop="auditorName" label="审批人" width="130" show-overflow-tooltip />
|
||||||
|
<ElTableColumn label="审批时间" width="170">
|
||||||
|
<template #default="{ row }">{{ formatOvertimeDateTime(row.createTime) }}</template>
|
||||||
|
</ElTableColumn>
|
||||||
|
</ElTable>
|
||||||
|
</BusinessFormDialog>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,239 @@
|
|||||||
|
<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 { formatOvertimeDate, formatOvertimeDateTime } from './overtime-application-shared';
|
||||||
|
import IconMdiCheckCircleOutline from '~icons/mdi/check-circle-outline';
|
||||||
|
import IconMdiCloseCircleOutline from '~icons/mdi/close-circle-outline';
|
||||||
|
import IconMdiChevronLeft from '~icons/mdi/chevron-left';
|
||||||
|
import IconMdiChevronRight from '~icons/mdi/chevron-right';
|
||||||
|
|
||||||
|
defineOptions({ name: 'OvertimeApplicationBatchDetailDialog' });
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
/** 选中的加班申请 id 列表(原始 id) */
|
||||||
|
selectedIds: string[];
|
||||||
|
/** 全部加班申请行数据,用于通过 id 查找 */
|
||||||
|
rows: Api.OvertimeApplication.OvertimeApplication[];
|
||||||
|
actionLoading?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
actionLoading: false
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
approve: [];
|
||||||
|
reject: [];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const visible = defineModel<boolean>('visible', {
|
||||||
|
default: false
|
||||||
|
});
|
||||||
|
|
||||||
|
const currentIndex = ref(0);
|
||||||
|
const detailData = ref<Api.OvertimeApplication.OvertimeApplication | null>(null);
|
||||||
|
const detailLoading = ref(false);
|
||||||
|
|
||||||
|
const currentId = computed(() => props.selectedIds[currentIndex.value] ?? null);
|
||||||
|
|
||||||
|
const total = computed(() => props.selectedIds.length);
|
||||||
|
|
||||||
|
const canGoPrev = computed(() => currentIndex.value > 0);
|
||||||
|
const canGoNext = computed(() => currentIndex.value < props.selectedIds.length - 1);
|
||||||
|
|
||||||
|
async function loadDetail() {
|
||||||
|
const id = currentId.value;
|
||||||
|
if (!id) {
|
||||||
|
detailData.value = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const row = props.rows.find(r => r.id === id);
|
||||||
|
if (!row) {
|
||||||
|
detailData.value = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
detailLoading.value = true;
|
||||||
|
const { error, data } = await fetchGetOvertimeApplicationDetail(id);
|
||||||
|
detailLoading.value = false;
|
||||||
|
|
||||||
|
detailData.value = error || !data ? row : data;
|
||||||
|
}
|
||||||
|
|
||||||
|
function goPrev() {
|
||||||
|
if (!canGoPrev.value) return;
|
||||||
|
currentIndex.value -= 1;
|
||||||
|
loadDetail();
|
||||||
|
}
|
||||||
|
|
||||||
|
function goNext() {
|
||||||
|
if (!canGoNext.value) return;
|
||||||
|
currentIndex.value += 1;
|
||||||
|
loadDetail();
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => visible.value,
|
||||||
|
value => {
|
||||||
|
if (value) {
|
||||||
|
currentIndex.value = 0;
|
||||||
|
loadDetail();
|
||||||
|
} else {
|
||||||
|
detailData.value = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<BusinessFormDialog v-model="visible" title="批量审批" preset="md" :loading="detailLoading" :show-footer="true">
|
||||||
|
<!-- 左右导航 -->
|
||||||
|
<div class="batch-detail__nav">
|
||||||
|
<button type="button" class="batch-detail__nav-btn" :disabled="!canGoPrev" @click.stop="goPrev">
|
||||||
|
<IconMdiChevronLeft class="text-20px" />
|
||||||
|
</button>
|
||||||
|
<span class="batch-detail__nav-counter">{{ currentIndex + 1 }} / {{ total }}</span>
|
||||||
|
<button type="button" class="batch-detail__nav-btn" :disabled="!canGoNext" @click.stop="goNext">
|
||||||
|
<IconMdiChevronRight class="text-20px" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ElDescriptions v-if="detailData" class="overtime-application-detail-dialog__descriptions" :column="2" border>
|
||||||
|
<ElDescriptionsItem label="申请人" label-class-name="overtime-application-detail-dialog__label">
|
||||||
|
{{ detailData.applicantName }}
|
||||||
|
</ElDescriptionsItem>
|
||||||
|
<ElDescriptionsItem label="加班日期" label-class-name="overtime-application-detail-dialog__label--compact">
|
||||||
|
{{ formatOvertimeDate(detailData.overtimeDate) }}
|
||||||
|
</ElDescriptionsItem>
|
||||||
|
<ElDescriptionsItem label="加班时长" label-class-name="overtime-application-detail-dialog__label">
|
||||||
|
{{ detailData.overtimeDuration }}
|
||||||
|
</ElDescriptionsItem>
|
||||||
|
<ElDescriptionsItem label="提交时间" label-class-name="overtime-application-detail-dialog__label--compact">
|
||||||
|
{{ formatOvertimeDateTime(detailData.submitTime) }}
|
||||||
|
</ElDescriptionsItem>
|
||||||
|
<ElDescriptionsItem label="加班原因" :span="2" label-class-name="overtime-application-detail-dialog__label">
|
||||||
|
{{ detailData.overtimeReason }}
|
||||||
|
</ElDescriptionsItem>
|
||||||
|
<ElDescriptionsItem label="加班内容" :span="2" label-class-name="overtime-application-detail-dialog__label">
|
||||||
|
{{ detailData.overtimeContent }}
|
||||||
|
</ElDescriptionsItem>
|
||||||
|
</ElDescriptions>
|
||||||
|
<ElEmpty v-else description="未获取到加班申请详情" />
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<div class="batch-detail__footer">
|
||||||
|
<span class="batch-detail__footer-hint">将对全部 {{ total }} 项统一执行操作</span>
|
||||||
|
<div class="batch-detail__footer-actions">
|
||||||
|
<ElButton
|
||||||
|
class="batch-detail__approve-btn"
|
||||||
|
type="success"
|
||||||
|
:loading="props.actionLoading"
|
||||||
|
:disabled="props.actionLoading || !detailData"
|
||||||
|
@click="emit('approve')"
|
||||||
|
>
|
||||||
|
<template #icon>
|
||||||
|
<IconMdiCheckCircleOutline />
|
||||||
|
</template>
|
||||||
|
通过
|
||||||
|
</ElButton>
|
||||||
|
<ElButton type="danger" plain :disabled="props.actionLoading || !detailData" @click="emit('reject')">
|
||||||
|
<template #icon>
|
||||||
|
<IconMdiCloseCircleOutline />
|
||||||
|
</template>
|
||||||
|
退回
|
||||||
|
</ElButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</BusinessFormDialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.batch-detail__nav {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 16px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.batch-detail__nav-btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border: 1px solid rgb(226 232 240 / 90%);
|
||||||
|
border-radius: 8px;
|
||||||
|
background-color: rgb(255 255 255 / 98%);
|
||||||
|
color: rgb(71 85 105 / 94%);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 160ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.batch-detail__nav-btn:hover:not(:disabled) {
|
||||||
|
border-color: rgb(14 116 144 / 60%);
|
||||||
|
color: rgb(14 116 144 / 96%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.batch-detail__nav-btn:disabled {
|
||||||
|
opacity: 0.35;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.batch-detail__nav-counter {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: rgb(15 23 42 / 96%);
|
||||||
|
min-width: 60px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.batch-detail__footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.batch-detail__footer-hint {
|
||||||
|
font-size: 13px;
|
||||||
|
color: rgb(100 116 139 / 92%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.batch-detail__footer-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.batch-detail__approve-btn {
|
||||||
|
--el-button-bg-color: #0f766e;
|
||||||
|
--el-button-border-color: #0f766e;
|
||||||
|
--el-button-hover-bg-color: #115e59;
|
||||||
|
--el-button-hover-border-color: #115e59;
|
||||||
|
--el-button-active-bg-color: #134e4a;
|
||||||
|
--el-button-active-border-color: #134e4a;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.overtime-application-detail-dialog__descriptions .el-descriptions__cell) {
|
||||||
|
line-height: 1.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.overtime-application-detail-dialog__label),
|
||||||
|
:deep(.overtime-application-detail-dialog__label--compact) {
|
||||||
|
white-space: nowrap;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.overtime-application-detail-dialog__label) {
|
||||||
|
width: 96px;
|
||||||
|
min-width: 96px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.overtime-application-detail-dialog__label--compact) {
|
||||||
|
width: 86px;
|
||||||
|
min-width: 86px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,22 +1,28 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref, watch } from 'vue';
|
import { ref, watch } from 'vue';
|
||||||
import { fetchGetOvertimeApplicationDetail } from '@/service/api';
|
import { fetchGetOvertimeApplicationDetail } from '@/service/api';
|
||||||
import BusinessFormDialog from '@/components/custom/business-form-dialog.vue';
|
import BusinessFormDialog from '@/components/custom/business-form-dialog.vue';
|
||||||
import {
|
import { formatOvertimeDate, formatOvertimeDateTime } from './overtime-application-shared';
|
||||||
formatEmptyText,
|
import IconMdiCheckCircleOutline from '~icons/mdi/check-circle-outline';
|
||||||
formatOvertimeDate,
|
import IconMdiCloseCircleOutline from '~icons/mdi/close-circle-outline';
|
||||||
formatOvertimeDateTime,
|
|
||||||
getOvertimeApplicationStatusLabel,
|
|
||||||
resolveOvertimeApplicationStatusTagType
|
|
||||||
} from './overtime-application-shared';
|
|
||||||
|
|
||||||
defineOptions({ name: 'OvertimeApplicationDetailDialog' });
|
defineOptions({ name: 'OvertimeApplicationDetailDialog' });
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
rowData?: Api.OvertimeApplication.OvertimeApplication | null;
|
rowData?: Api.OvertimeApplication.OvertimeApplication | null;
|
||||||
|
showApprovalActions?: boolean;
|
||||||
|
actionLoading?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = defineProps<Props>();
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
showApprovalActions: false,
|
||||||
|
actionLoading: false
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
approve: [];
|
||||||
|
reject: [];
|
||||||
|
}>();
|
||||||
|
|
||||||
const visible = defineModel<boolean>('visible', {
|
const visible = defineModel<boolean>('visible', {
|
||||||
default: false
|
default: false
|
||||||
@@ -25,11 +31,6 @@ const visible = defineModel<boolean>('visible', {
|
|||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const detailData = ref<Api.OvertimeApplication.OvertimeApplication | null>(null);
|
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() {
|
async function loadDetail() {
|
||||||
if (!props.rowData?.id) {
|
if (!props.rowData?.id) {
|
||||||
detailData.value = null;
|
detailData.value = null;
|
||||||
@@ -54,30 +55,96 @@ watch(
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<BusinessFormDialog v-model="visible" title="加班申请详情" preset="md" :loading="loading" :show-footer="false">
|
<BusinessFormDialog
|
||||||
<ElDescriptions v-if="detailData" :column="2" border>
|
v-model="visible"
|
||||||
<ElDescriptionsItem label="状态">
|
title="加班申请详情"
|
||||||
<ElTag :type="statusTagType">{{ statusLabel }}</ElTag>
|
preset="md"
|
||||||
</ElDescriptionsItem>
|
:loading="loading"
|
||||||
<ElDescriptionsItem label="申请人">
|
:show-footer="props.showApprovalActions"
|
||||||
|
>
|
||||||
|
<ElDescriptions v-if="detailData" class="overtime-application-detail-dialog__descriptions" :column="2" border>
|
||||||
|
<ElDescriptionsItem label="申请人" label-class-name="overtime-application-detail-dialog__label">
|
||||||
{{ detailData.applicantName }}
|
{{ detailData.applicantName }}
|
||||||
</ElDescriptionsItem>
|
</ElDescriptionsItem>
|
||||||
<ElDescriptionsItem label="加班日期">{{ formatOvertimeDate(detailData.overtimeDate) }}</ElDescriptionsItem>
|
<ElDescriptionsItem label="加班日期" label-class-name="overtime-application-detail-dialog__label--compact">
|
||||||
<ElDescriptionsItem label="加班时长">{{ detailData.overtimeDuration }}</ElDescriptionsItem>
|
{{ formatOvertimeDate(detailData.overtimeDate) }}
|
||||||
<ElDescriptionsItem label="审核人">
|
</ElDescriptionsItem>
|
||||||
{{ detailData.approverName }}
|
<ElDescriptionsItem label="加班时长" label-class-name="overtime-application-detail-dialog__label">
|
||||||
|
{{ detailData.overtimeDuration }}
|
||||||
|
</ElDescriptionsItem>
|
||||||
|
<ElDescriptionsItem label="提交时间" label-class-name="overtime-application-detail-dialog__label--compact">
|
||||||
|
{{ formatOvertimeDateTime(detailData.submitTime) }}
|
||||||
|
</ElDescriptionsItem>
|
||||||
|
<ElDescriptionsItem label="加班原因" :span="2" label-class-name="overtime-application-detail-dialog__label">
|
||||||
|
{{ detailData.overtimeReason }}
|
||||||
|
</ElDescriptionsItem>
|
||||||
|
<ElDescriptionsItem label="加班内容" :span="2" label-class-name="overtime-application-detail-dialog__label">
|
||||||
|
{{ detailData.overtimeContent }}
|
||||||
</ElDescriptionsItem>
|
</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>
|
</ElDescriptions>
|
||||||
<ElEmpty v-else description="未获取到加班申请详情" />
|
<ElEmpty v-else description="未获取到加班申请详情" />
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<div class="overtime-application-detail-dialog__footer">
|
||||||
|
<ElButton
|
||||||
|
class="overtime-application-detail-dialog__approve-btn"
|
||||||
|
type="success"
|
||||||
|
:loading="props.actionLoading"
|
||||||
|
:disabled="props.actionLoading || !detailData"
|
||||||
|
@click="emit('approve')"
|
||||||
|
>
|
||||||
|
<template #icon>
|
||||||
|
<IconMdiCheckCircleOutline />
|
||||||
|
</template>
|
||||||
|
通过
|
||||||
|
</ElButton>
|
||||||
|
<ElButton type="danger" plain :disabled="props.actionLoading || !detailData" @click="emit('reject')">
|
||||||
|
<template #icon>
|
||||||
|
<IconMdiCloseCircleOutline />
|
||||||
|
</template>
|
||||||
|
退回
|
||||||
|
</ElButton>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
</BusinessFormDialog>
|
</BusinessFormDialog>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
.overtime-application-detail-dialog__footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overtime-application-detail-dialog__approve-btn {
|
||||||
|
--el-button-bg-color: #0f766e;
|
||||||
|
--el-button-border-color: #0f766e;
|
||||||
|
--el-button-hover-bg-color: #115e59;
|
||||||
|
--el-button-hover-border-color: #115e59;
|
||||||
|
--el-button-active-bg-color: #134e4a;
|
||||||
|
--el-button-active-border-color: #134e4a;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.overtime-application-detail-dialog__descriptions .el-descriptions__cell) {
|
||||||
|
line-height: 1.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.overtime-application-detail-dialog__label),
|
||||||
|
:deep(.overtime-application-detail-dialog__label--compact) {
|
||||||
|
white-space: nowrap;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.overtime-application-detail-dialog__label) {
|
||||||
|
width: 96px;
|
||||||
|
min-width: 96px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.overtime-application-detail-dialog__label--compact) {
|
||||||
|
width: 86px;
|
||||||
|
min-width: 86px;
|
||||||
|
}
|
||||||
|
|
||||||
:deep(.overtime-application-detail-dialog__readonly-input .el-input__wrapper) {
|
: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);
|
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;
|
box-shadow: 0 0 0 1px rgb(203 213 225 / 96%) inset;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, nextTick, reactive, ref, watch } from 'vue';
|
import { computed, nextTick, reactive, ref, watch } from 'vue';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
import { RDMS_OVERTIME_DURATION_DICT_CODE } from '@/constants/dict';
|
import { RDMS_OVERTIME_DURATION_DICT_CODE } from '@/constants/dict';
|
||||||
import {
|
import {
|
||||||
fetchCreateOvertimeApplication,
|
fetchCreateOvertimeApplication,
|
||||||
@@ -85,8 +86,8 @@ const rules = computed(
|
|||||||
|
|
||||||
function createDefaultModel(): Api.OvertimeApplication.SaveOvertimeApplicationParams {
|
function createDefaultModel(): Api.OvertimeApplication.SaveOvertimeApplicationParams {
|
||||||
return {
|
return {
|
||||||
overtimeDate: '',
|
overtimeDate: dayjs().format('YYYY-MM-DD'),
|
||||||
overtimeDuration: '',
|
overtimeDuration: '0.5',
|
||||||
overtimeReason: '',
|
overtimeReason: '',
|
||||||
overtimeContent: '',
|
overtimeContent: '',
|
||||||
approverId: ''
|
approverId: ''
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, reactive, watch } from 'vue';
|
import { computed, onMounted, reactive, ref, watch } from 'vue';
|
||||||
import { RDMS_OVERTIME_APPLICATION_STATUS_DICT_CODE } from '@/constants/dict';
|
import { fetchGetOvertimeApplicationStatusDict } from '@/service/api';
|
||||||
import TableSearchFields, { type SearchField } from '@/components/custom/table-search-fields.vue';
|
import TableSearchFields, { type SearchField } from '@/components/custom/table-search-fields.vue';
|
||||||
|
|
||||||
defineOptions({ name: 'OvertimeApplicationSearch' });
|
defineOptions({ name: 'OvertimeApplicationSearch' });
|
||||||
@@ -21,6 +21,8 @@ const searchModel = reactive<Record<string, any>>({
|
|||||||
approverName: ''
|
approverName: ''
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const statusOptions = ref<Array<{ label: string; value: string }>>([]);
|
||||||
|
|
||||||
let syncingFromSource = false;
|
let syncingFromSource = false;
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
@@ -53,6 +55,24 @@ watch(
|
|||||||
{ flush: 'sync' }
|
{ flush: 'sync' }
|
||||||
);
|
);
|
||||||
|
|
||||||
|
async function loadStatusOptions() {
|
||||||
|
const { error, data } = await fetchGetOvertimeApplicationStatusDict();
|
||||||
|
|
||||||
|
if (error || !data) {
|
||||||
|
statusOptions.value = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
statusOptions.value = data.map(item => ({
|
||||||
|
label: item.statusName,
|
||||||
|
value: item.statusCode
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await loadStatusOptions();
|
||||||
|
});
|
||||||
|
|
||||||
const fields = computed<SearchField[]>(() => [
|
const fields = computed<SearchField[]>(() => [
|
||||||
{
|
{
|
||||||
key: 'applicantName',
|
key: 'applicantName',
|
||||||
@@ -69,8 +89,8 @@ const fields = computed<SearchField[]>(() => [
|
|||||||
{
|
{
|
||||||
key: 'statusCode',
|
key: 'statusCode',
|
||||||
label: '状态',
|
label: '状态',
|
||||||
type: 'dict',
|
type: 'select',
|
||||||
dictCode: RDMS_OVERTIME_APPLICATION_STATUS_DICT_CODE,
|
options: statusOptions.value,
|
||||||
placeholder: '请选择状态'
|
placeholder: '请选择状态'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -7,16 +7,14 @@ export const overtimeApplicationStatusOptions: Array<{
|
|||||||
}> = [
|
}> = [
|
||||||
{ label: '待审批', value: 'pending' },
|
{ label: '待审批', value: 'pending' },
|
||||||
{ label: '已通过', value: 'approved' },
|
{ label: '已通过', value: 'approved' },
|
||||||
{ label: '已退回', value: 'rejected' },
|
{ label: '已退回', value: 'rejected' }
|
||||||
{ label: '已撤销', value: 'cancelled' }
|
|
||||||
];
|
];
|
||||||
|
|
||||||
export const overtimeApplicationActionNameMap: Record<Api.OvertimeApplication.OvertimeApplicationActionType, string> = {
|
export const overtimeApplicationActionNameMap: Record<Api.OvertimeApplication.OvertimeApplicationActionType, string> = {
|
||||||
submit: '提交',
|
submit: '提交',
|
||||||
resubmit: '重新提交',
|
resubmit: '重新提交',
|
||||||
approve: '通过',
|
approve: '通过',
|
||||||
reject: '退回',
|
reject: '退回'
|
||||||
cancel: '撤销'
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export function getOvertimeApplicationStatusLabel(statusCode?: string | null, statusName?: string | null) {
|
export function getOvertimeApplicationStatusLabel(statusCode?: string | null, statusName?: string | null) {
|
||||||
|
|||||||
@@ -1,89 +0,0 @@
|
|||||||
<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>
|
|
||||||
241
src/views/personal-center/work-report/index.vue
Normal file
241
src/views/personal-center/work-report/index.vue
Normal file
@@ -0,0 +1,241 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, onMounted, ref } from 'vue';
|
||||||
|
import { onBeforeRouteLeave } from 'vue-router';
|
||||||
|
import { fetchGetProjectReportOwnerProjectOptions } from '@/service/api';
|
||||||
|
import { useAuth } from '@/hooks/business/auth';
|
||||||
|
import WorkReportCreateDialog from './shared/components/create-dialog.vue';
|
||||||
|
import WorkReportPrototypePageDialog from './shared/components/prototype-page-dialog.vue';
|
||||||
|
import WorkReportTabs from './shared/components/tabs.vue';
|
||||||
|
import {
|
||||||
|
WORK_REPORT_PROJECT_OWNER_PERMISSION,
|
||||||
|
WORK_REPORT_TYPE_LABEL,
|
||||||
|
type WorkReportRow,
|
||||||
|
type WorkReportType
|
||||||
|
} from './shared/types';
|
||||||
|
import WeeklyReportIndex from './weekly/index.vue';
|
||||||
|
import WeeklyReportApprovalRecordDialog from './weekly/modules/approval-record-dialog.vue';
|
||||||
|
import MonthlyReportIndex from './monthly/index.vue';
|
||||||
|
import MonthlyReportApprovalRecordDialog from './monthly/modules/approval-record-dialog.vue';
|
||||||
|
import ProjectReportIndex from './project/index.vue';
|
||||||
|
import ProjectReportApprovalRecordDialog from './project/modules/approval-record-dialog.vue';
|
||||||
|
|
||||||
|
defineOptions({ name: 'PersonalCenterWorkReport' });
|
||||||
|
|
||||||
|
type PageDialogMode = 'add' | 'edit' | 'detail';
|
||||||
|
type ReportListExpose = {
|
||||||
|
reload: (page?: number) => Promise<void>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const { hasAuth } = useAuth();
|
||||||
|
|
||||||
|
const activeTab = ref<WorkReportType>('weekly');
|
||||||
|
const createVisible = ref(false);
|
||||||
|
const pageDialogVisible = ref(false);
|
||||||
|
const pageDialogMode = ref<PageDialogMode>('detail');
|
||||||
|
const approvalRecordVisible = ref(false);
|
||||||
|
const currentReportType = ref<WorkReportType>('weekly');
|
||||||
|
const currentRow = ref<WorkReportRow | null>(null);
|
||||||
|
const initialPeriod = ref<{
|
||||||
|
periodKey: string;
|
||||||
|
periodLabel: string;
|
||||||
|
periodStartDate: string;
|
||||||
|
periodEndDate: string;
|
||||||
|
} | null>(null);
|
||||||
|
const initialProjectId = ref('');
|
||||||
|
const initialFlag = ref(1);
|
||||||
|
const projectOptions = ref<Api.WorkReport.Project.ProjectReportOwnerProjectOption[]>([]);
|
||||||
|
|
||||||
|
const weeklyRef = ref<ReportListExpose | null>(null);
|
||||||
|
const monthlyRef = ref<ReportListExpose | null>(null);
|
||||||
|
const projectRef = ref<ReportListExpose | null>(null);
|
||||||
|
|
||||||
|
const canShowProjectTab = computed(() => hasAuth(WORK_REPORT_PROJECT_OWNER_PERMISSION));
|
||||||
|
|
||||||
|
/** 项目选项是否加载成功(用于项目半月报列表内部判断) */
|
||||||
|
const projectOptionsLoaded = ref(false);
|
||||||
|
|
||||||
|
const visibleTabs = computed<Array<{ label: string; name: WorkReportType }>>(() => {
|
||||||
|
const tabs: Array<{ label: string; name: WorkReportType }> = [
|
||||||
|
{ label: WORK_REPORT_TYPE_LABEL.weekly, name: 'weekly' },
|
||||||
|
{ label: WORK_REPORT_TYPE_LABEL.monthly, name: 'monthly' }
|
||||||
|
];
|
||||||
|
|
||||||
|
if (canShowProjectTab.value) {
|
||||||
|
tabs.push({ label: WORK_REPORT_TYPE_LABEL.project, name: 'project' });
|
||||||
|
}
|
||||||
|
|
||||||
|
return tabs;
|
||||||
|
});
|
||||||
|
|
||||||
|
const currentApprovalRecordDialogComponent = computed(() => {
|
||||||
|
if (currentReportType.value === 'monthly') return MonthlyReportApprovalRecordDialog;
|
||||||
|
if (currentReportType.value === 'project') return ProjectReportApprovalRecordDialog;
|
||||||
|
return WeeklyReportApprovalRecordDialog;
|
||||||
|
});
|
||||||
|
|
||||||
|
function getListRef(reportType: WorkReportType) {
|
||||||
|
if (reportType === 'monthly') return monthlyRef.value;
|
||||||
|
if (reportType === 'project') return projectRef.value;
|
||||||
|
return weeklyRef.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadProjectOptions() {
|
||||||
|
if (!canShowProjectTab.value) return;
|
||||||
|
|
||||||
|
const { error, data } = await fetchGetProjectReportOwnerProjectOptions();
|
||||||
|
projectOptions.value = error || !data ? [] : data;
|
||||||
|
projectOptionsLoaded.value = !error;
|
||||||
|
}
|
||||||
|
|
||||||
|
function openCreate(reportType: WorkReportType) {
|
||||||
|
currentReportType.value = reportType;
|
||||||
|
createVisible.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleCreateConfirm(
|
||||||
|
payload:
|
||||||
|
| { reportType: 'weekly' | 'monthly'; period: typeof initialPeriod.value extends infer T ? T : never }
|
||||||
|
| {
|
||||||
|
reportType: 'project';
|
||||||
|
projectId: string;
|
||||||
|
flag: number;
|
||||||
|
period: typeof initialPeriod.value extends infer T ? T : never;
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
currentReportType.value = payload.reportType;
|
||||||
|
pageDialogMode.value = 'add';
|
||||||
|
currentRow.value = null;
|
||||||
|
initialPeriod.value = payload.period as typeof initialPeriod.value;
|
||||||
|
initialProjectId.value = 'projectId' in payload ? payload.projectId : '';
|
||||||
|
initialFlag.value = 'flag' in payload ? payload.flag : 1;
|
||||||
|
pageDialogVisible.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function openEdit(reportType: WorkReportType, row: WorkReportRow) {
|
||||||
|
currentReportType.value = reportType;
|
||||||
|
pageDialogMode.value = 'edit';
|
||||||
|
currentRow.value = row;
|
||||||
|
initialPeriod.value = null;
|
||||||
|
pageDialogVisible.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function openDetail(reportType: WorkReportType, row: WorkReportRow) {
|
||||||
|
currentReportType.value = reportType;
|
||||||
|
pageDialogMode.value = 'detail';
|
||||||
|
currentRow.value = row;
|
||||||
|
initialPeriod.value = null;
|
||||||
|
pageDialogVisible.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function openApprovalRecord(reportType: WorkReportType, row: WorkReportRow) {
|
||||||
|
currentReportType.value = reportType;
|
||||||
|
currentRow.value = row;
|
||||||
|
approvalRecordVisible.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleTabChange(tab: WorkReportType) {
|
||||||
|
activeTab.value = tab;
|
||||||
|
getListRef(tab)?.reload(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function reloadReport(reportType = currentReportType.value) {
|
||||||
|
await getListRef(reportType)?.reload();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSubmitted() {
|
||||||
|
pageDialogVisible.value = false;
|
||||||
|
reloadReport(currentReportType.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeFloatingPanels() {
|
||||||
|
createVisible.value = false;
|
||||||
|
pageDialogVisible.value = false;
|
||||||
|
approvalRecordVisible.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await loadProjectOptions();
|
||||||
|
});
|
||||||
|
|
||||||
|
onBeforeRouteLeave(() => {
|
||||||
|
closeFloatingPanels();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="work-report-page-shell min-h-560px gap-16px overflow-hidden xl:grid xl:grid-cols-[240px_minmax(0,1fr)] lt-xl:flex lt-xl:flex-col lt-xl:overflow-auto"
|
||||||
|
>
|
||||||
|
<!-- 左侧:报告类型导航 -->
|
||||||
|
<div class="flex-col-stretch gap-16px xl:min-h-0">
|
||||||
|
<WorkReportTabs :active-tab="activeTab" :tabs="visibleTabs" @update:active-tab="handleTabChange" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 右侧:搜索区 + 列表区 -->
|
||||||
|
<div class="flex-col-stretch gap-16px xl:min-h-0">
|
||||||
|
<WeeklyReportIndex
|
||||||
|
v-show="activeTab === 'weekly'"
|
||||||
|
ref="weeklyRef"
|
||||||
|
class="flex-1-hidden"
|
||||||
|
@create="openCreate('weekly')"
|
||||||
|
@edit="openEdit('weekly', $event)"
|
||||||
|
@detail="openDetail('weekly', $event)"
|
||||||
|
@approval-record="openApprovalRecord('weekly', $event)"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<MonthlyReportIndex
|
||||||
|
v-show="activeTab === 'monthly'"
|
||||||
|
ref="monthlyRef"
|
||||||
|
class="flex-1-hidden"
|
||||||
|
@create="openCreate('monthly')"
|
||||||
|
@edit="openEdit('monthly', $event)"
|
||||||
|
@detail="openDetail('monthly', $event)"
|
||||||
|
@approval-record="openApprovalRecord('monthly', $event)"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ProjectReportIndex
|
||||||
|
v-if="canShowProjectTab"
|
||||||
|
v-show="activeTab === 'project'"
|
||||||
|
ref="projectRef"
|
||||||
|
class="flex-1-hidden"
|
||||||
|
:project-options="projectOptions"
|
||||||
|
:project-options-loaded="projectOptionsLoaded"
|
||||||
|
@create="openCreate('project')"
|
||||||
|
@edit="openEdit('project', $event)"
|
||||||
|
@detail="openDetail('project', $event)"
|
||||||
|
@approval-record="openApprovalRecord('project', $event)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<WorkReportCreateDialog
|
||||||
|
v-model:visible="createVisible"
|
||||||
|
:default-report-type="currentReportType"
|
||||||
|
:project-visible="canShowProjectTab"
|
||||||
|
:project-options="projectOptions"
|
||||||
|
@confirm="handleCreateConfirm"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<WorkReportPrototypePageDialog
|
||||||
|
v-model:visible="pageDialogVisible"
|
||||||
|
:mode="pageDialogMode"
|
||||||
|
scene="fill"
|
||||||
|
:report-type="currentReportType"
|
||||||
|
:row-data="currentRow"
|
||||||
|
:initial-period="initialPeriod"
|
||||||
|
:initial-project-id="initialProjectId"
|
||||||
|
:initial-flag="initialFlag"
|
||||||
|
@submitted="handleSubmitted"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<component
|
||||||
|
:is="currentApprovalRecordDialogComponent"
|
||||||
|
v-model:visible="approvalRecordVisible"
|
||||||
|
:row-data="currentRow"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.work-report-page-shell {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
345
src/views/personal-center/work-report/monthly/index.vue
Normal file
345
src/views/personal-center/work-report/monthly/index.vue
Normal file
@@ -0,0 +1,345 @@
|
|||||||
|
<script setup lang="tsx">
|
||||||
|
/* eslint-disable no-void */
|
||||||
|
import { markRaw, reactive, ref } from 'vue';
|
||||||
|
import { ElMessageBox, ElTag } from 'element-plus';
|
||||||
|
import {
|
||||||
|
fetchDeleteMonthlyReport,
|
||||||
|
fetchExportMonthlyReportContent,
|
||||||
|
fetchGetMonthlyReportPage,
|
||||||
|
fetchSubmitMonthlyReport
|
||||||
|
} from '@/service/api';
|
||||||
|
import { useAuth } from '@/hooks/business/auth';
|
||||||
|
import { useUIPaginatedTable } from '@/hooks/common/table';
|
||||||
|
import BusinessTableActionCell, { type BusinessTableAction } from '@/components/custom/business-table-action-cell';
|
||||||
|
import {
|
||||||
|
WORK_REPORT_TYPE_LABEL,
|
||||||
|
type WorkReportRow,
|
||||||
|
createMonthlySearchParams,
|
||||||
|
createWorkReportContentExportFallbackName,
|
||||||
|
downloadBlob,
|
||||||
|
formatDateTime,
|
||||||
|
formatEmptyText,
|
||||||
|
formatPeriod,
|
||||||
|
getWorkReportStatusLabel,
|
||||||
|
resolveExportFilename,
|
||||||
|
resolveWorkReportStatusTagType,
|
||||||
|
transformWorkReportPage
|
||||||
|
} from '../shared/types';
|
||||||
|
import MonthlyReportSearch from './modules/search-panel.vue';
|
||||||
|
import IconMdiDeleteOutline from '~icons/mdi/delete-outline';
|
||||||
|
import IconMdiEyeOutline from '~icons/mdi/eye-outline';
|
||||||
|
import IconMdiFileDocumentCheckOutline from '~icons/mdi/file-document-check-outline';
|
||||||
|
import IconMdiPencilOutline from '~icons/mdi/pencil-outline';
|
||||||
|
import IconMdiSendOutline from '~icons/mdi/send-outline';
|
||||||
|
|
||||||
|
defineOptions({ name: 'MonthlyWorkReportIndex' });
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'create'): void;
|
||||||
|
(e: 'edit', row: WorkReportRow): void;
|
||||||
|
(e: 'detail', row: WorkReportRow): void;
|
||||||
|
(e: 'approvalRecord', row: WorkReportRow): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const { hasAuth } = useAuth();
|
||||||
|
const exporting = ref(false);
|
||||||
|
const selectedRows = ref<Api.WorkReport.Monthly.MonthlyReport[]>([]);
|
||||||
|
const searchParams = reactive(createMonthlySearchParams());
|
||||||
|
|
||||||
|
const ACTION_ICON_MAP = {
|
||||||
|
detail: markRaw(IconMdiEyeOutline),
|
||||||
|
edit: markRaw(IconMdiPencilOutline),
|
||||||
|
submit: markRaw(IconMdiSendOutline),
|
||||||
|
delete: markRaw(IconMdiDeleteOutline),
|
||||||
|
approvalRecord: markRaw(IconMdiFileDocumentCheckOutline)
|
||||||
|
};
|
||||||
|
|
||||||
|
const table = useUIPaginatedTable<
|
||||||
|
Awaited<ReturnType<typeof fetchGetMonthlyReportPage>>,
|
||||||
|
Api.WorkReport.Monthly.MonthlyReport
|
||||||
|
>({
|
||||||
|
paginationProps: { currentPage: searchParams.pageNo, pageSize: searchParams.pageSize },
|
||||||
|
api: () => fetchGetMonthlyReportPage(searchParams),
|
||||||
|
transform: response => transformWorkReportPage(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: 'periodLabel', label: '月份', minWidth: 80, formatter: row => formatPeriod(row) },
|
||||||
|
{
|
||||||
|
prop: 'reporterDeptName',
|
||||||
|
label: '部门',
|
||||||
|
minWidth: 80,
|
||||||
|
showOverflowTooltip: true,
|
||||||
|
formatter: row => row.reporterDeptName || '--'
|
||||||
|
},
|
||||||
|
{ prop: 'supervisorName', label: '直属上级', minWidth: 80 },
|
||||||
|
{ prop: 'totalWorkHours', label: '总工时', minWidth: 80, formatter: row => formatEmptyText(row.totalWorkHours) },
|
||||||
|
{
|
||||||
|
prop: 'statusCode',
|
||||||
|
label: '状态',
|
||||||
|
minWidth: 80,
|
||||||
|
align: 'center',
|
||||||
|
formatter: row => (
|
||||||
|
<ElTag type={resolveWorkReportStatusTagType(row.statusCode)}>
|
||||||
|
{getWorkReportStatusLabel(row.statusCode, row.statusName)}
|
||||||
|
</ElTag>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{ prop: 'submitTime', label: '提交时间', minWidth: 100, formatter: row => formatDateTime(row.submitTime) },
|
||||||
|
{ prop: 'approvalTime', label: '审批时间', minWidth: 100, formatter: row => formatDateTime(row.approvalTime) },
|
||||||
|
{
|
||||||
|
prop: 'operate',
|
||||||
|
label: '操作',
|
||||||
|
width: 180,
|
||||||
|
align: 'center',
|
||||||
|
fixed: 'right',
|
||||||
|
formatter: row => <BusinessTableActionCell actions={getRowActions(row)} variant="icon" />
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
function getRowActions(row: Api.WorkReport.Monthly.MonthlyReport): BusinessTableAction[] {
|
||||||
|
const actions: BusinessTableAction[] = [
|
||||||
|
{
|
||||||
|
key: 'detail',
|
||||||
|
label: '详情',
|
||||||
|
buttonType: 'primary',
|
||||||
|
icon: ACTION_ICON_MAP.detail,
|
||||||
|
onClick: () => emit('detail', row)
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
if (['draft', 'rejected'].includes(row.statusCode) && row.allowEdit && hasAuth('project:work-report:update')) {
|
||||||
|
actions.push({
|
||||||
|
key: 'edit',
|
||||||
|
label: '编辑',
|
||||||
|
buttonType: 'primary',
|
||||||
|
icon: ACTION_ICON_MAP.edit,
|
||||||
|
onClick: () => emit('edit', row)
|
||||||
|
});
|
||||||
|
actions.push({
|
||||||
|
key: 'submit',
|
||||||
|
label: row.statusCode === 'draft' ? '提交' : '重新提交',
|
||||||
|
buttonType: 'success',
|
||||||
|
icon: ACTION_ICON_MAP.submit,
|
||||||
|
onClick: () => handleSubmitReport(row)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (row.statusCode === 'draft' && hasAuth('project:work-report:delete')) {
|
||||||
|
actions.push({
|
||||||
|
key: 'delete',
|
||||||
|
label: '删除',
|
||||||
|
buttonType: 'danger',
|
||||||
|
icon: ACTION_ICON_MAP.delete,
|
||||||
|
onClick: () => handleDelete(row)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (['approved', 'rejected'].includes(row.statusCode)) {
|
||||||
|
actions.push({
|
||||||
|
key: 'approval-record',
|
||||||
|
label: '审批记录',
|
||||||
|
buttonType: 'info',
|
||||||
|
icon: ACTION_ICON_MAP.approvalRecord,
|
||||||
|
onClick: () => emit('approvalRecord', row)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return actions;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function reload(page?: number) {
|
||||||
|
await table.getDataByPage(page ?? searchParams.pageNo ?? 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetSearchParams() {
|
||||||
|
const pageSize = searchParams.pageSize ?? 10;
|
||||||
|
Object.assign(searchParams, createMonthlySearchParams(), { pageSize });
|
||||||
|
reload(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSearch() {
|
||||||
|
reload(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSubmitReport(row: Api.WorkReport.Monthly.MonthlyReport) {
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm('确认提交该报告吗?', '提交确认', {
|
||||||
|
type: 'warning',
|
||||||
|
confirmButtonText: row.statusCode === 'draft' ? '确认提交' : '确认重新提交',
|
||||||
|
cancelButtonText: '取消'
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await fetchSubmitMonthlyReport(row.id);
|
||||||
|
|
||||||
|
if (result.error) return;
|
||||||
|
window.$message?.success('工作报告已提交');
|
||||||
|
await reload();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleDelete(row: Api.WorkReport.Monthly.MonthlyReport) {
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm(`确认删除 ${formatPeriod(row)} 吗?`, '删除确认', {
|
||||||
|
type: 'warning',
|
||||||
|
confirmButtonText: '确认',
|
||||||
|
cancelButtonText: '取消'
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await fetchDeleteMonthlyReport(row.id);
|
||||||
|
|
||||||
|
if (result.error) return;
|
||||||
|
|
||||||
|
window.$message?.success('工作报告已删除');
|
||||||
|
await reload();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSelectionChange(rows: Api.WorkReport.Monthly.MonthlyReport[]) {
|
||||||
|
selectedRows.value = rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createExportSearchParams() {
|
||||||
|
const { pageNo: _pageNo, pageSize: _pageSize, ...params } = searchParams;
|
||||||
|
return params;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function exportReportContent(
|
||||||
|
params: Api.WorkReport.Common.ContentExportParams<Api.WorkReport.Monthly.MonthlyReportSearchParams>,
|
||||||
|
reportCount: number
|
||||||
|
) {
|
||||||
|
exporting.value = true;
|
||||||
|
const result = await fetchExportMonthlyReportContent(params);
|
||||||
|
exporting.value = false;
|
||||||
|
|
||||||
|
if (result.error || !result.data) return;
|
||||||
|
|
||||||
|
const fallbackName = createWorkReportContentExportFallbackName('monthly', reportCount);
|
||||||
|
downloadBlob(result.data, resolveExportFilename(result, fallbackName));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleExportSelected() {
|
||||||
|
if (!selectedRows.value.length) {
|
||||||
|
window.$message?.warning('请选择要导出的报告');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await exportReportContent(
|
||||||
|
{
|
||||||
|
exportAll: false,
|
||||||
|
ids: selectedRows.value.map(item => item.id)
|
||||||
|
},
|
||||||
|
selectedRows.value.length
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleExportAll() {
|
||||||
|
const total = table.mobilePagination.value.total || 0;
|
||||||
|
if (!total) {
|
||||||
|
window.$message?.warning('暂无可导出的报告');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await exportReportContent(
|
||||||
|
{
|
||||||
|
...createExportSearchParams(),
|
||||||
|
exportAll: true,
|
||||||
|
ids: []
|
||||||
|
},
|
||||||
|
total
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleExportCommand(command: 'selected' | 'all') {
|
||||||
|
if (command === 'selected') {
|
||||||
|
await handleExportSelected();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await handleExportAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({ reload });
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex-col-stretch gap-16px overflow-hidden">
|
||||||
|
<MonthlyReportSearch 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">{{ WORK_REPORT_TYPE_LABEL.monthly }}</p>
|
||||||
|
<ElTag effect="plain">{{ table.mobilePagination.value.total || 0 }}</ElTag>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<TableHeaderOperation
|
||||||
|
v-model:columns="table.columnChecks.value"
|
||||||
|
:loading="table.loading.value"
|
||||||
|
@refresh="reload()"
|
||||||
|
>
|
||||||
|
<template #default>
|
||||||
|
<ElDropdown v-auth="'project:work-report:export'" trigger="click" @command="handleExportCommand">
|
||||||
|
<ElButton plain :loading="exporting">
|
||||||
|
<template #icon>
|
||||||
|
<icon-mdi-download class="text-icon" />
|
||||||
|
</template>
|
||||||
|
导出
|
||||||
|
</ElButton>
|
||||||
|
<template #dropdown>
|
||||||
|
<ElDropdownMenu>
|
||||||
|
<ElDropdownItem command="selected" :disabled="exporting || !selectedRows.length">
|
||||||
|
导出选中
|
||||||
|
</ElDropdownItem>
|
||||||
|
<ElDropdownItem command="all" :disabled="exporting">导出全部</ElDropdownItem>
|
||||||
|
</ElDropdownMenu>
|
||||||
|
</template>
|
||||||
|
</ElDropdown>
|
||||||
|
<ElButton v-auth="'project:work-report:create'" plain type="primary" @click="emit('create')">
|
||||||
|
<template #icon>
|
||||||
|
<icon-ic-round-plus class="text-icon" />
|
||||||
|
</template>
|
||||||
|
新增
|
||||||
|
</ElButton>
|
||||||
|
</template>
|
||||||
|
</TableHeaderOperation>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div class="flex-1">
|
||||||
|
<ElTable
|
||||||
|
v-loading="table.loading.value"
|
||||||
|
height="100%"
|
||||||
|
border
|
||||||
|
row-key="id"
|
||||||
|
:data="table.data.value"
|
||||||
|
@selection-change="handleSelectionChange"
|
||||||
|
>
|
||||||
|
<ElTableColumn type="selection" width="48" />
|
||||||
|
<template v-for="col in table.columns.value" :key="String(col.prop)">
|
||||||
|
<ElTableColumn v-bind="col" />
|
||||||
|
</template>
|
||||||
|
</ElTable>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-20px flex justify-end">
|
||||||
|
<ElPagination
|
||||||
|
v-if="table.mobilePagination.value.total"
|
||||||
|
layout="total,prev,pager,next,sizes"
|
||||||
|
v-bind="table.mobilePagination.value"
|
||||||
|
@current-change="table.mobilePagination.value['current-change']"
|
||||||
|
@size-change="table.mobilePagination.value['size-change']"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</ElCard>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,16 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import SharedWorkReportApprovalRecordDialog from '../../shared/components/approval-record-dialog.vue';
|
||||||
|
import type { WorkReportRow } from '../../shared/types';
|
||||||
|
|
||||||
|
defineOptions({ name: 'MonthlyReportApprovalRecordDialog' });
|
||||||
|
|
||||||
|
const visible = defineModel<boolean>('visible', { default: false });
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
rowData?: WorkReportRow | null;
|
||||||
|
}>();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<SharedWorkReportApprovalRecordDialog v-model:visible="visible" report-type="monthly" :row-data="rowData" />
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import SharedWorkReportDetailDialog from '../../shared/components/detail-dialog.vue';
|
||||||
|
import type { WorkReportRow } from '../../shared/types';
|
||||||
|
|
||||||
|
defineOptions({ name: 'MonthlyReportDetailPage' });
|
||||||
|
|
||||||
|
const visible = defineModel<boolean>('visible', { default: false });
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
rowData?: WorkReportRow | null;
|
||||||
|
}>();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<SharedWorkReportDetailDialog v-model:visible="visible" report-type="monthly" :row-data="rowData" />
|
||||||
|
</template>
|
||||||
2294
src/views/personal-center/work-report/monthly/modules/fill-page.vue
Normal file
2294
src/views/personal-center/work-report/monthly/modules/fill-page.vue
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,26 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import SharedWorkReportSearch from '../../shared/components/search-panel.vue';
|
||||||
|
|
||||||
|
defineOptions({ name: 'MonthlyReportSearch' });
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
projectOptions?: Api.WorkReport.Project.ProjectReportOwnerProjectOption[];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const model = defineModel<Api.WorkReport.Monthly.MonthlyReportSearchParams>('model', { required: true });
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'reset'): void;
|
||||||
|
(e: 'search'): void;
|
||||||
|
}>();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<SharedWorkReportSearch
|
||||||
|
v-model:model="model"
|
||||||
|
report-type="monthly"
|
||||||
|
:project-options="projectOptions"
|
||||||
|
@reset="emit('reset')"
|
||||||
|
@search="emit('search')"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
363
src/views/personal-center/work-report/project/index.vue
Normal file
363
src/views/personal-center/work-report/project/index.vue
Normal file
@@ -0,0 +1,363 @@
|
|||||||
|
<script setup lang="tsx">
|
||||||
|
/* eslint-disable no-void */
|
||||||
|
import { markRaw, reactive, ref } from 'vue';
|
||||||
|
import { ElMessageBox, ElTag } from 'element-plus';
|
||||||
|
import {
|
||||||
|
fetchDeleteProjectReport,
|
||||||
|
fetchExportProjectReportContent,
|
||||||
|
fetchGetProjectReportPage,
|
||||||
|
fetchSubmitProjectReport
|
||||||
|
} from '@/service/api';
|
||||||
|
import { useAuth } from '@/hooks/business/auth';
|
||||||
|
import { useUIPaginatedTable } from '@/hooks/common/table';
|
||||||
|
import BusinessTableActionCell, { type BusinessTableAction } from '@/components/custom/business-table-action-cell';
|
||||||
|
import {
|
||||||
|
WORK_REPORT_TYPE_LABEL,
|
||||||
|
type WorkReportRow,
|
||||||
|
createProjectSearchParams,
|
||||||
|
createWorkReportContentExportFallbackName,
|
||||||
|
downloadBlob,
|
||||||
|
formatDateTime,
|
||||||
|
formatEmptyText,
|
||||||
|
formatPeriod,
|
||||||
|
getProjectReportFlagLabel,
|
||||||
|
getWorkReportStatusLabel,
|
||||||
|
resolveExportFilename,
|
||||||
|
resolveWorkReportStatusTagType,
|
||||||
|
transformWorkReportPage
|
||||||
|
} from '../shared/types';
|
||||||
|
import ProjectReportSearch from './modules/search-panel.vue';
|
||||||
|
import IconMdiDeleteOutline from '~icons/mdi/delete-outline';
|
||||||
|
import IconMdiEyeOutline from '~icons/mdi/eye-outline';
|
||||||
|
import IconMdiFileDocumentCheckOutline from '~icons/mdi/file-document-check-outline';
|
||||||
|
import IconMdiPencilOutline from '~icons/mdi/pencil-outline';
|
||||||
|
import IconMdiSendOutline from '~icons/mdi/send-outline';
|
||||||
|
|
||||||
|
defineOptions({ name: 'ProjectWorkReportIndex' });
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
projectOptions: Api.WorkReport.Project.ProjectReportOwnerProjectOption[];
|
||||||
|
projectOptionsLoaded: boolean;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'create'): void;
|
||||||
|
(e: 'edit', row: WorkReportRow): void;
|
||||||
|
(e: 'detail', row: WorkReportRow): void;
|
||||||
|
(e: 'approvalRecord', row: WorkReportRow): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const { hasAuth } = useAuth();
|
||||||
|
const exporting = ref(false);
|
||||||
|
const selectedRows = ref<Api.WorkReport.Project.ProjectReport[]>([]);
|
||||||
|
const searchParams = reactive(createProjectSearchParams());
|
||||||
|
|
||||||
|
const ACTION_ICON_MAP = {
|
||||||
|
detail: markRaw(IconMdiEyeOutline),
|
||||||
|
edit: markRaw(IconMdiPencilOutline),
|
||||||
|
submit: markRaw(IconMdiSendOutline),
|
||||||
|
delete: markRaw(IconMdiDeleteOutline),
|
||||||
|
approvalRecord: markRaw(IconMdiFileDocumentCheckOutline)
|
||||||
|
};
|
||||||
|
|
||||||
|
const table = useUIPaginatedTable<
|
||||||
|
Awaited<ReturnType<typeof fetchGetProjectReportPage>>,
|
||||||
|
Api.WorkReport.Project.ProjectReport
|
||||||
|
>({
|
||||||
|
paginationProps: { currentPage: searchParams.pageNo, pageSize: searchParams.pageSize },
|
||||||
|
api: () => fetchGetProjectReportPage(searchParams),
|
||||||
|
transform: response => transformWorkReportPage(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: 'projectName', label: '项目名称', minWidth: 200, showOverflowTooltip: true },
|
||||||
|
{ prop: 'periodLabel', label: '半月周期', minWidth: 120, formatter: row => formatPeriod(row) },
|
||||||
|
// { prop: 'flag', label: '半月', width: 90, formatter: row => getProjectReportFlagLabel(row.flag) },
|
||||||
|
{ prop: 'projectOwnerName', label: '项目负责人', minWidth: 80 },
|
||||||
|
{
|
||||||
|
prop: 'technicalOwnerName',
|
||||||
|
label: '技术负责人',
|
||||||
|
minWidth: 80,
|
||||||
|
formatter: row => row.technicalOwnerName || '--'
|
||||||
|
},
|
||||||
|
{ prop: 'supervisorName', label: '直属上级', minWidth: 80 },
|
||||||
|
{ prop: 'totalWorkHours', label: '总工时', minWidth: 60, formatter: row => formatEmptyText(row.totalWorkHours) },
|
||||||
|
{
|
||||||
|
prop: 'statusCode',
|
||||||
|
label: '状态',
|
||||||
|
minWidth: 60,
|
||||||
|
align: 'center',
|
||||||
|
formatter: row => (
|
||||||
|
<ElTag type={resolveWorkReportStatusTagType(row.statusCode)}>
|
||||||
|
{getWorkReportStatusLabel(row.statusCode, row.statusName)}
|
||||||
|
</ElTag>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{ prop: 'submitTime', label: '提交时间', minWidth: 100, formatter: row => formatDateTime(row.submitTime) },
|
||||||
|
{ prop: 'approvalTime', label: '审批时间', minWidth: 100, formatter: row => formatDateTime(row.approvalTime) },
|
||||||
|
{
|
||||||
|
prop: 'operate',
|
||||||
|
label: '操作',
|
||||||
|
width: 180,
|
||||||
|
align: 'center',
|
||||||
|
fixed: 'right',
|
||||||
|
formatter: row => <BusinessTableActionCell actions={getRowActions(row)} variant="icon" />
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
function getRowActions(row: Api.WorkReport.Project.ProjectReport): BusinessTableAction[] {
|
||||||
|
const actions: BusinessTableAction[] = [
|
||||||
|
{
|
||||||
|
key: 'detail',
|
||||||
|
label: '详情',
|
||||||
|
buttonType: 'primary',
|
||||||
|
icon: ACTION_ICON_MAP.detail,
|
||||||
|
onClick: () => emit('detail', row)
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
if (['draft', 'rejected'].includes(row.statusCode) && row.allowEdit && hasAuth('project:work-report:update')) {
|
||||||
|
actions.push({
|
||||||
|
key: 'edit',
|
||||||
|
label: '编辑',
|
||||||
|
buttonType: 'primary',
|
||||||
|
icon: ACTION_ICON_MAP.edit,
|
||||||
|
onClick: () => emit('edit', row)
|
||||||
|
});
|
||||||
|
actions.push({
|
||||||
|
key: 'submit',
|
||||||
|
label: row.statusCode === 'draft' ? '提交' : '重新提交',
|
||||||
|
buttonType: 'success',
|
||||||
|
icon: ACTION_ICON_MAP.submit,
|
||||||
|
onClick: () => handleSubmitReport(row)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (row.statusCode === 'draft' && hasAuth('project:work-report:delete')) {
|
||||||
|
actions.push({
|
||||||
|
key: 'delete',
|
||||||
|
label: '删除',
|
||||||
|
buttonType: 'danger',
|
||||||
|
icon: ACTION_ICON_MAP.delete,
|
||||||
|
onClick: () => handleDelete(row)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (['approved', 'rejected'].includes(row.statusCode)) {
|
||||||
|
actions.push({
|
||||||
|
key: 'approval-record',
|
||||||
|
label: '审批记录',
|
||||||
|
buttonType: 'info',
|
||||||
|
icon: ACTION_ICON_MAP.approvalRecord,
|
||||||
|
onClick: () => emit('approvalRecord', row)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return actions;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function reload(page?: number) {
|
||||||
|
await table.getDataByPage(page ?? searchParams.pageNo ?? 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetSearchParams() {
|
||||||
|
const pageSize = searchParams.pageSize ?? 10;
|
||||||
|
Object.assign(searchParams, createProjectSearchParams(), { pageSize });
|
||||||
|
reload(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSearch() {
|
||||||
|
reload(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSubmitReport(row: Api.WorkReport.Project.ProjectReport) {
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm('确认提交该报告吗?', '提交确认', {
|
||||||
|
type: 'warning',
|
||||||
|
confirmButtonText: row.statusCode === 'draft' ? '确认提交' : '确认重新提交',
|
||||||
|
cancelButtonText: '取消'
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await fetchSubmitProjectReport(row.id);
|
||||||
|
|
||||||
|
if (result.error) return;
|
||||||
|
window.$message?.success('工作报告已提交');
|
||||||
|
await reload();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleDelete(row: Api.WorkReport.Project.ProjectReport) {
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm(`确认删除 ${formatPeriod(row)} 吗?`, '删除确认', {
|
||||||
|
type: 'warning',
|
||||||
|
confirmButtonText: '确认',
|
||||||
|
cancelButtonText: '取消'
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await fetchDeleteProjectReport(row.id);
|
||||||
|
|
||||||
|
if (result.error) return;
|
||||||
|
|
||||||
|
window.$message?.success('工作报告已删除');
|
||||||
|
await reload();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSelectionChange(rows: Api.WorkReport.Project.ProjectReport[]) {
|
||||||
|
selectedRows.value = rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createExportSearchParams() {
|
||||||
|
const { pageNo: _pageNo, pageSize: _pageSize, ...params } = searchParams;
|
||||||
|
return params;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function exportReportContent(
|
||||||
|
params: Api.WorkReport.Common.ContentExportParams<Api.WorkReport.Project.ProjectReportSearchParams>,
|
||||||
|
reportCount: number
|
||||||
|
) {
|
||||||
|
exporting.value = true;
|
||||||
|
const result = await fetchExportProjectReportContent(params);
|
||||||
|
exporting.value = false;
|
||||||
|
|
||||||
|
if (result.error || !result.data) return;
|
||||||
|
|
||||||
|
const fallbackName = createWorkReportContentExportFallbackName('project', reportCount);
|
||||||
|
downloadBlob(result.data, resolveExportFilename(result, fallbackName));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleExportSelected() {
|
||||||
|
if (!selectedRows.value.length) {
|
||||||
|
window.$message?.warning('请选择要导出的报告');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await exportReportContent(
|
||||||
|
{
|
||||||
|
exportAll: false,
|
||||||
|
ids: selectedRows.value.map(item => item.id)
|
||||||
|
},
|
||||||
|
selectedRows.value.length
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleExportAll() {
|
||||||
|
const total = table.mobilePagination.value.total || 0;
|
||||||
|
if (!total) {
|
||||||
|
window.$message?.warning('暂无可导出的报告');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await exportReportContent(
|
||||||
|
{
|
||||||
|
...createExportSearchParams(),
|
||||||
|
exportAll: true,
|
||||||
|
ids: []
|
||||||
|
},
|
||||||
|
total
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleExportCommand(command: 'selected' | 'all') {
|
||||||
|
if (command === 'selected') {
|
||||||
|
await handleExportSelected();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await handleExportAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({ reload });
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex-col-stretch gap-16px overflow-hidden">
|
||||||
|
<!-- 项目选项加载失败时的提示 -->
|
||||||
|
<ElAlert v-if="!projectOptionsLoaded" type="warning" :closable="false" show-icon>
|
||||||
|
项目数据加载失败,部分功能可能不可用,请刷新页面重试
|
||||||
|
</ElAlert>
|
||||||
|
|
||||||
|
<ProjectReportSearch
|
||||||
|
v-model:model="searchParams"
|
||||||
|
:project-options="projectOptions"
|
||||||
|
@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">{{ WORK_REPORT_TYPE_LABEL.project }}</p>
|
||||||
|
<ElTag effect="plain">{{ table.mobilePagination.value.total || 0 }}</ElTag>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<TableHeaderOperation
|
||||||
|
v-model:columns="table.columnChecks.value"
|
||||||
|
:loading="table.loading.value"
|
||||||
|
@refresh="reload()"
|
||||||
|
>
|
||||||
|
<template #default>
|
||||||
|
<ElDropdown v-auth="'project:work-report:export'" trigger="click" @command="handleExportCommand">
|
||||||
|
<ElButton plain :loading="exporting">
|
||||||
|
<template #icon>
|
||||||
|
<icon-mdi-download class="text-icon" />
|
||||||
|
</template>
|
||||||
|
导出
|
||||||
|
</ElButton>
|
||||||
|
<template #dropdown>
|
||||||
|
<ElDropdownMenu>
|
||||||
|
<ElDropdownItem command="selected" :disabled="exporting || !selectedRows.length">
|
||||||
|
导出选中
|
||||||
|
</ElDropdownItem>
|
||||||
|
<ElDropdownItem command="all" :disabled="exporting">导出全部</ElDropdownItem>
|
||||||
|
</ElDropdownMenu>
|
||||||
|
</template>
|
||||||
|
</ElDropdown>
|
||||||
|
<ElButton v-auth="'project:work-report:create'" plain type="primary" @click="emit('create')">
|
||||||
|
<template #icon>
|
||||||
|
<icon-ic-round-plus class="text-icon" />
|
||||||
|
</template>
|
||||||
|
新增
|
||||||
|
</ElButton>
|
||||||
|
</template>
|
||||||
|
</TableHeaderOperation>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div class="flex-1">
|
||||||
|
<ElTable
|
||||||
|
v-loading="table.loading.value"
|
||||||
|
height="100%"
|
||||||
|
border
|
||||||
|
row-key="id"
|
||||||
|
:data="table.data.value"
|
||||||
|
@selection-change="handleSelectionChange"
|
||||||
|
>
|
||||||
|
<ElTableColumn type="selection" width="48" />
|
||||||
|
<template v-for="col in table.columns.value" :key="String(col.prop)">
|
||||||
|
<ElTableColumn v-bind="col" />
|
||||||
|
</template>
|
||||||
|
</ElTable>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-20px flex justify-end">
|
||||||
|
<ElPagination
|
||||||
|
v-if="table.mobilePagination.value.total"
|
||||||
|
layout="total,prev,pager,next,sizes"
|
||||||
|
v-bind="table.mobilePagination.value"
|
||||||
|
@current-change="table.mobilePagination.value['current-change']"
|
||||||
|
@size-change="table.mobilePagination.value['size-change']"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</ElCard>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import SharedWorkReportApprovalRecordDialog from '../../shared/components/approval-record-dialog.vue';
|
||||||
|
import type { WorkReportRow } from '../../shared/types';
|
||||||
|
|
||||||
|
defineOptions({ name: 'ProjectReportApprovalRecordDialog' });
|
||||||
|
|
||||||
|
const visible = defineModel<boolean>('visible', { default: false });
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
rowData?: WorkReportRow | null;
|
||||||
|
}>();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<SharedWorkReportApprovalRecordDialog v-model:visible="visible" report-type="project" :row-data="rowData" />
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import SharedWorkReportDetailDialog from '../../shared/components/detail-dialog.vue';
|
||||||
|
import type { WorkReportRow } from '../../shared/types';
|
||||||
|
|
||||||
|
defineOptions({ name: 'ProjectReportDetailPage' });
|
||||||
|
|
||||||
|
const visible = defineModel<boolean>('visible', { default: false });
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
rowData?: WorkReportRow | null;
|
||||||
|
}>();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<SharedWorkReportDetailDialog v-model:visible="visible" report-type="project" :row-data="rowData" />
|
||||||
|
</template>
|
||||||
1073
src/views/personal-center/work-report/project/modules/fill-page.vue
Normal file
1073
src/views/personal-center/work-report/project/modules/fill-page.vue
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,26 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import SharedWorkReportSearch from '../../shared/components/search-panel.vue';
|
||||||
|
|
||||||
|
defineOptions({ name: 'ProjectReportSearch' });
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
projectOptions?: Api.WorkReport.Project.ProjectReportOwnerProjectOption[];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const model = defineModel<Api.WorkReport.Project.ProjectReportSearchParams>('model', { required: true });
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'reset'): void;
|
||||||
|
(e: 'search'): void;
|
||||||
|
}>();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<SharedWorkReportSearch
|
||||||
|
v-model:model="model"
|
||||||
|
report-type="project"
|
||||||
|
:project-options="projectOptions"
|
||||||
|
@reset="emit('reset')"
|
||||||
|
@search="emit('search')"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,403 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, nextTick, reactive, watch } from 'vue';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
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 { WORK_REPORT_TYPE_LABEL, type WorkReportType } from '../types';
|
||||||
|
|
||||||
|
defineOptions({ name: 'WorkReportActionDialog' });
|
||||||
|
|
||||||
|
type ActionType = 'approve' | 'reject';
|
||||||
|
type ApprovalConclusion = 'approve' | 'reject';
|
||||||
|
|
||||||
|
const visible = defineModel<boolean>('visible', { default: false });
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
reportType: WorkReportType;
|
||||||
|
actionType: ActionType;
|
||||||
|
initialMonthlyApproveData?: Partial<Api.WorkReport.Monthly.MonthlyReportApproveParams> | null;
|
||||||
|
loading?: boolean;
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
initialMonthlyApproveData: null,
|
||||||
|
loading: false
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(
|
||||||
|
e: 'submit',
|
||||||
|
payload: Api.WorkReport.Common.StatusActionParams | Api.WorkReport.Monthly.MonthlyReportApproveParams,
|
||||||
|
actionType?: ActionType
|
||||||
|
): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const { formRef, validate } = useForm();
|
||||||
|
const { createRequiredRule } = useFormRules();
|
||||||
|
|
||||||
|
const reasonModel = reactive<Api.WorkReport.Common.StatusActionParams>({
|
||||||
|
reason: ''
|
||||||
|
});
|
||||||
|
const commonApprovalModel = reactive<{
|
||||||
|
conclusion: ApprovalConclusion | '';
|
||||||
|
opinion: string;
|
||||||
|
}>({
|
||||||
|
conclusion: 'approve',
|
||||||
|
opinion: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
const monthlyModel = reactive<Api.WorkReport.Monthly.MonthlyReportApproveParams>({
|
||||||
|
reason: '',
|
||||||
|
meetingDate: '',
|
||||||
|
strengthDesc: '',
|
||||||
|
strengthExample: '',
|
||||||
|
weaknessDesc: '',
|
||||||
|
weaknessExample: '',
|
||||||
|
improvementSuggestion: '',
|
||||||
|
performanceResult: '',
|
||||||
|
employeeSignName: '',
|
||||||
|
employeeSignedDate: '',
|
||||||
|
supervisorSignName: '',
|
||||||
|
supervisorSignedDate: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
const isMonthlyApprove = computed(() => props.reportType === 'monthly' && props.actionType === 'approve');
|
||||||
|
const isCommonApprove = computed(() => props.reportType !== 'monthly' && props.actionType === 'approve');
|
||||||
|
const title = computed(() => {
|
||||||
|
if (isCommonApprove.value) {
|
||||||
|
return `审批${WORK_REPORT_TYPE_LABEL[props.reportType]}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const actionLabel = props.actionType === 'approve' ? '审批通过' : '退回';
|
||||||
|
|
||||||
|
return `${actionLabel}${WORK_REPORT_TYPE_LABEL[props.reportType]}`;
|
||||||
|
});
|
||||||
|
|
||||||
|
const preset = computed(() => (isMonthlyApprove.value ? 'lg' : 'sm'));
|
||||||
|
const rejectOpinionRequired = computed(() => isCommonApprove.value && commonApprovalModel.conclusion === 'reject');
|
||||||
|
const opinionLabel = computed(() => (rejectOpinionRequired.value ? '退回原因' : '审批意见'));
|
||||||
|
const opinionPlaceholder = computed(() =>
|
||||||
|
rejectOpinionRequired.value ? `请输入${opinionLabel.value}` : '可填写审批意见'
|
||||||
|
);
|
||||||
|
const confirmText = computed(() => {
|
||||||
|
if (isCommonApprove.value) return '确认提交';
|
||||||
|
if (props.actionType === 'approve') return '通过';
|
||||||
|
return '退回';
|
||||||
|
});
|
||||||
|
const confirmDisabled = computed(() => isCommonApprove.value && !commonApprovalModel.conclusion);
|
||||||
|
const commonRules = computed(() => ({
|
||||||
|
opinion: rejectOpinionRequired.value
|
||||||
|
? [
|
||||||
|
createRequiredRule(`请输入${opinionLabel.value}`),
|
||||||
|
{
|
||||||
|
validator: (_rule, value: string, callback) => {
|
||||||
|
if (!value?.trim()) {
|
||||||
|
callback(new Error(`请输入${opinionLabel.value}`));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
callback();
|
||||||
|
},
|
||||||
|
trigger: 'blur'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
: []
|
||||||
|
}));
|
||||||
|
|
||||||
|
watch(visible, isVisible => {
|
||||||
|
if (!isVisible) return;
|
||||||
|
reasonModel.reason = '';
|
||||||
|
Object.assign(commonApprovalModel, {
|
||||||
|
conclusion: 'approve',
|
||||||
|
opinion: ''
|
||||||
|
});
|
||||||
|
Object.assign(monthlyModel, {
|
||||||
|
reason: '',
|
||||||
|
meetingDate: dayjs().format('YYYY-MM-DD'),
|
||||||
|
strengthDesc: '',
|
||||||
|
strengthExample: '',
|
||||||
|
weaknessDesc: '',
|
||||||
|
weaknessExample: '',
|
||||||
|
improvementSuggestion: '',
|
||||||
|
performanceResult: '',
|
||||||
|
employeeSignName: '',
|
||||||
|
employeeSignedDate: dayjs().format('YYYY-MM-DD'),
|
||||||
|
supervisorSignName: '',
|
||||||
|
supervisorSignedDate: dayjs().format('YYYY-MM-DD')
|
||||||
|
});
|
||||||
|
|
||||||
|
if (props.initialMonthlyApproveData) {
|
||||||
|
Object.assign(monthlyModel, props.initialMonthlyApproveData);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => visible.value,
|
||||||
|
async isVisible => {
|
||||||
|
if (!isVisible || !isCommonApprove.value) return;
|
||||||
|
|
||||||
|
await nextTick();
|
||||||
|
formRef.value?.clearValidate();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
watch(rejectOpinionRequired, async () => {
|
||||||
|
if (!visible.value || !isCommonApprove.value) return;
|
||||||
|
|
||||||
|
await nextTick();
|
||||||
|
formRef.value?.clearValidate('opinion');
|
||||||
|
});
|
||||||
|
|
||||||
|
async function handleSubmit() {
|
||||||
|
if (isCommonApprove.value) {
|
||||||
|
if (!commonApprovalModel.conclusion) {
|
||||||
|
window.$message?.warning('请选择审批结论');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await validate();
|
||||||
|
|
||||||
|
emit(
|
||||||
|
'submit',
|
||||||
|
{
|
||||||
|
reason: commonApprovalModel.opinion.trim() || (commonApprovalModel.conclusion === 'approve' ? '通过' : '退回')
|
||||||
|
},
|
||||||
|
commonApprovalModel.conclusion
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
emit('submit', isMonthlyApprove.value ? { ...monthlyModel } : { ...reasonModel });
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<BusinessFormDialog
|
||||||
|
v-model="visible"
|
||||||
|
:title="title"
|
||||||
|
:preset="preset"
|
||||||
|
:confirm-loading="loading"
|
||||||
|
:confirm-disabled="confirmDisabled"
|
||||||
|
:confirm-text="confirmText"
|
||||||
|
max-body-height="76vh"
|
||||||
|
@confirm="handleSubmit"
|
||||||
|
>
|
||||||
|
<template v-if="isCommonApprove">
|
||||||
|
<div class="audit-form">
|
||||||
|
<div class="audit-field">
|
||||||
|
<label>审批结论</label>
|
||||||
|
<div class="audit-conclusion">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="conclusion-btn"
|
||||||
|
:class="{
|
||||||
|
active: commonApprovalModel.conclusion === 'approve',
|
||||||
|
pass: commonApprovalModel.conclusion === 'approve'
|
||||||
|
}"
|
||||||
|
@click="commonApprovalModel.conclusion = 'approve'"
|
||||||
|
>
|
||||||
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
|
||||||
|
<circle cx="8" cy="8" r="7" stroke="currentColor" stroke-width="1.5" />
|
||||||
|
<path
|
||||||
|
d="M5 8.5L7 10.5L11 6"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="1.5"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
通过
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="conclusion-btn"
|
||||||
|
:class="{
|
||||||
|
active: commonApprovalModel.conclusion === 'reject',
|
||||||
|
reject: commonApprovalModel.conclusion === 'reject'
|
||||||
|
}"
|
||||||
|
@click="commonApprovalModel.conclusion = 'reject'"
|
||||||
|
>
|
||||||
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
|
||||||
|
<circle cx="8" cy="8" r="7" stroke="currentColor" stroke-width="1.5" />
|
||||||
|
<path d="M6 6L10 10M10 6L6 10" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" />
|
||||||
|
</svg>
|
||||||
|
退回
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ElForm
|
||||||
|
ref="formRef"
|
||||||
|
:model="commonApprovalModel"
|
||||||
|
:rules="commonRules"
|
||||||
|
label-position="top"
|
||||||
|
:validate-on-rule-change="false"
|
||||||
|
>
|
||||||
|
<ElFormItem :label="opinionLabel" prop="opinion">
|
||||||
|
<ElInput
|
||||||
|
v-model="commonApprovalModel.opinion"
|
||||||
|
type="textarea"
|
||||||
|
:rows="5"
|
||||||
|
maxlength="1000"
|
||||||
|
show-word-limit
|
||||||
|
:placeholder="opinionPlaceholder"
|
||||||
|
/>
|
||||||
|
</ElFormItem>
|
||||||
|
</ElForm>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-else-if="isMonthlyApprove">
|
||||||
|
<BusinessFormSection title="当期工作反馈">
|
||||||
|
<ElRow :gutter="16">
|
||||||
|
<ElCol :span="12">
|
||||||
|
<ElFormItem label="面谈时间">
|
||||||
|
<ElDatePicker v-model="monthlyModel.meetingDate" class="w-full" type="date" value-format="YYYY-MM-DD" />
|
||||||
|
</ElFormItem>
|
||||||
|
</ElCol>
|
||||||
|
<ElCol :span="12">
|
||||||
|
<ElFormItem label="绩效考核结果">
|
||||||
|
<ElInput v-model="monthlyModel.performanceResult" placeholder="请输入绩效结果" />
|
||||||
|
</ElFormItem>
|
||||||
|
</ElCol>
|
||||||
|
<ElCol :span="24">
|
||||||
|
<ElFormItem label="审批意见">
|
||||||
|
<ElInput v-model="monthlyModel.reason" type="textarea" :rows="3" />
|
||||||
|
</ElFormItem>
|
||||||
|
</ElCol>
|
||||||
|
</ElRow>
|
||||||
|
</BusinessFormSection>
|
||||||
|
|
||||||
|
<BusinessFormSection title="优势与不足">
|
||||||
|
<ElRow :gutter="16">
|
||||||
|
<ElCol :span="12">
|
||||||
|
<ElFormItem label="优势描述">
|
||||||
|
<ElInput v-model="monthlyModel.strengthDesc" type="textarea" :rows="3" />
|
||||||
|
</ElFormItem>
|
||||||
|
</ElCol>
|
||||||
|
<ElCol :span="12">
|
||||||
|
<ElFormItem label="优势行为事例">
|
||||||
|
<ElInput v-model="monthlyModel.strengthExample" type="textarea" :rows="3" />
|
||||||
|
</ElFormItem>
|
||||||
|
</ElCol>
|
||||||
|
<ElCol :span="12">
|
||||||
|
<ElFormItem label="劣势描述">
|
||||||
|
<ElInput v-model="monthlyModel.weaknessDesc" type="textarea" :rows="3" />
|
||||||
|
</ElFormItem>
|
||||||
|
</ElCol>
|
||||||
|
<ElCol :span="12">
|
||||||
|
<ElFormItem label="劣势行为事例">
|
||||||
|
<ElInput v-model="monthlyModel.weaknessExample" type="textarea" :rows="3" />
|
||||||
|
</ElFormItem>
|
||||||
|
</ElCol>
|
||||||
|
<ElCol :span="24">
|
||||||
|
<ElFormItem label="改进建议">
|
||||||
|
<ElInput v-model="monthlyModel.improvementSuggestion" type="textarea" :rows="3" />
|
||||||
|
</ElFormItem>
|
||||||
|
</ElCol>
|
||||||
|
</ElRow>
|
||||||
|
</BusinessFormSection>
|
||||||
|
|
||||||
|
<BusinessFormSection title="签字区">
|
||||||
|
<ElRow :gutter="16">
|
||||||
|
<ElCol :span="12">
|
||||||
|
<ElFormItem label="被考核人签名">
|
||||||
|
<ElInput v-model="monthlyModel.employeeSignName" />
|
||||||
|
</ElFormItem>
|
||||||
|
</ElCol>
|
||||||
|
<ElCol :span="12">
|
||||||
|
<ElFormItem label="被考核人签字日期">
|
||||||
|
<ElDatePicker
|
||||||
|
v-model="monthlyModel.employeeSignedDate"
|
||||||
|
class="w-full"
|
||||||
|
type="date"
|
||||||
|
value-format="YYYY-MM-DD"
|
||||||
|
/>
|
||||||
|
</ElFormItem>
|
||||||
|
</ElCol>
|
||||||
|
<ElCol :span="12">
|
||||||
|
<ElFormItem label="上级签名">
|
||||||
|
<ElInput v-model="monthlyModel.supervisorSignName" />
|
||||||
|
</ElFormItem>
|
||||||
|
</ElCol>
|
||||||
|
<ElCol :span="12">
|
||||||
|
<ElFormItem label="上级签字日期">
|
||||||
|
<ElDatePicker
|
||||||
|
v-model="monthlyModel.supervisorSignedDate"
|
||||||
|
class="w-full"
|
||||||
|
type="date"
|
||||||
|
value-format="YYYY-MM-DD"
|
||||||
|
/>
|
||||||
|
</ElFormItem>
|
||||||
|
</ElCol>
|
||||||
|
</ElRow>
|
||||||
|
</BusinessFormSection>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<ElForm v-else label-position="top">
|
||||||
|
<ElFormItem :label="actionType === 'approve' ? '审批意见' : '原因'">
|
||||||
|
<ElInput v-model="reasonModel.reason" type="textarea" :rows="5" placeholder="请输入原因或意见" />
|
||||||
|
</ElFormItem>
|
||||||
|
</ElForm>
|
||||||
|
</BusinessFormDialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.audit-form {
|
||||||
|
display: grid;
|
||||||
|
gap: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.audit-field {
|
||||||
|
display: grid;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.audit-field label {
|
||||||
|
color: #475467;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.audit-conclusion {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.conclusion-btn {
|
||||||
|
height: 44px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
|
border: 1px solid #d8e0e8;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #fff;
|
||||||
|
color: #475467;
|
||||||
|
font: inherit;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 800;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.18s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.conclusion-btn:hover {
|
||||||
|
border-color: #0f766e;
|
||||||
|
color: #0f766e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.conclusion-btn.active.pass {
|
||||||
|
border-color: #0f766e;
|
||||||
|
background: #f0fdfa;
|
||||||
|
color: #0f766e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.conclusion-btn.active.reject {
|
||||||
|
border-color: #dc2626;
|
||||||
|
background: #fef2f2;
|
||||||
|
color: #dc2626;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
/* eslint-disable no-void */
|
||||||
|
import { computed, ref, watch } from 'vue';
|
||||||
|
import {
|
||||||
|
fetchGetMonthlyReportApprovalRecords,
|
||||||
|
fetchGetProjectReportApprovalRecords,
|
||||||
|
fetchGetWeeklyReportApprovalRecords
|
||||||
|
} from '@/service/api';
|
||||||
|
import BusinessFormDialog from '@/components/custom/business-form-dialog.vue';
|
||||||
|
import {
|
||||||
|
WORK_REPORT_TYPE_LABEL,
|
||||||
|
type WorkReportRow,
|
||||||
|
type WorkReportType,
|
||||||
|
formatDate,
|
||||||
|
formatDateTime,
|
||||||
|
getWorkReportStatusLabel
|
||||||
|
} from '../types';
|
||||||
|
|
||||||
|
/** 格式化文本,空值显示 -- */
|
||||||
|
function formatTextOrDash(value?: string | number | null) {
|
||||||
|
if (value === null || value === undefined || value === '') {
|
||||||
|
return '--';
|
||||||
|
}
|
||||||
|
return String(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
defineOptions({ name: 'WorkReportApprovalRecordDialog' });
|
||||||
|
|
||||||
|
const visible = defineModel<boolean>('visible', { default: false });
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
reportType: WorkReportType;
|
||||||
|
rowData?: WorkReportRow | null;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const loading = ref(false);
|
||||||
|
const records = ref<
|
||||||
|
Array<Api.WorkReport.Common.WorkReportApprovalRecord | Api.WorkReport.Monthly.MonthlyReportApprovalRecord>
|
||||||
|
>([]);
|
||||||
|
const title = computed(() => `${WORK_REPORT_TYPE_LABEL[props.reportType]}审批记录`);
|
||||||
|
const monthlyRecords = computed(() => records.value as Api.WorkReport.Monthly.MonthlyReportApprovalRecord[]);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
[visible, () => props.rowData?.id, () => props.reportType],
|
||||||
|
([isVisible, currentId]) => {
|
||||||
|
if (!isVisible) return;
|
||||||
|
// visible 为 true(首次打开、换行、换报告类型)时都重新加载记录
|
||||||
|
if (currentId) {
|
||||||
|
loadRecords();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
async function loadRecords() {
|
||||||
|
if (!props.rowData?.id) return;
|
||||||
|
|
||||||
|
loading.value = true;
|
||||||
|
let result;
|
||||||
|
|
||||||
|
if (props.reportType === 'weekly') {
|
||||||
|
result = await fetchGetWeeklyReportApprovalRecords(props.rowData.id);
|
||||||
|
} else if (props.reportType === 'monthly') {
|
||||||
|
result = await fetchGetMonthlyReportApprovalRecords(props.rowData.id);
|
||||||
|
} else {
|
||||||
|
result = await fetchGetProjectReportApprovalRecords(props.rowData.id);
|
||||||
|
}
|
||||||
|
loading.value = false;
|
||||||
|
|
||||||
|
records.value = !result.error && result.data ? result.data : [];
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<BusinessFormDialog v-model="visible" :title="title" preset="lg" :loading="loading" :show-footer="false">
|
||||||
|
<ElTable v-if="reportType !== 'monthly'" border :data="records">
|
||||||
|
<ElTableColumn prop="approvalRound" label="轮次" width="80" />
|
||||||
|
<ElTableColumn label="结论" width="100">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ getWorkReportStatusLabel(row.conclusion) }}
|
||||||
|
</template>
|
||||||
|
</ElTableColumn>
|
||||||
|
<ElTableColumn prop="opinion" label="审批意见" min-width="220" show-overflow-tooltip />
|
||||||
|
<ElTableColumn prop="auditorName" label="审批人" width="120" />
|
||||||
|
<ElTableColumn label="审批时间" width="170">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ formatDateTime(row.createTime) }}
|
||||||
|
</template>
|
||||||
|
</ElTableColumn>
|
||||||
|
</ElTable>
|
||||||
|
|
||||||
|
<div v-else class="work-report-approval-records">
|
||||||
|
<ElCard v-for="item in monthlyRecords" :key="item.id">
|
||||||
|
<template #header>
|
||||||
|
<div class="flex items-center justify-between gap-12px">
|
||||||
|
<span>第 {{ item.approvalRound }} 轮 · {{ getWorkReportStatusLabel(item.conclusion) }}</span>
|
||||||
|
<span class="text-12px text-#64748b">{{ item.auditorName }} · {{ formatDateTime(item.createTime) }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<ElDescriptions :column="2" border size="small">
|
||||||
|
<ElDescriptionsItem label="审批意见" :span="2">{{ formatTextOrDash(item.opinion) }}</ElDescriptionsItem>
|
||||||
|
<ElDescriptionsItem label="面谈时间">{{ formatDate(item.meetingDate) }}</ElDescriptionsItem>
|
||||||
|
<ElDescriptionsItem label="绩效结果">{{ formatTextOrDash(item.performanceResult) }}</ElDescriptionsItem>
|
||||||
|
<ElDescriptionsItem label="优势描述">{{ formatTextOrDash(item.strengthDesc) }}</ElDescriptionsItem>
|
||||||
|
<ElDescriptionsItem label="优势事例">{{ formatTextOrDash(item.strengthExample) }}</ElDescriptionsItem>
|
||||||
|
<ElDescriptionsItem label="劣势描述">{{ formatTextOrDash(item.weaknessDesc) }}</ElDescriptionsItem>
|
||||||
|
<ElDescriptionsItem label="劣势事例">{{ formatTextOrDash(item.weaknessExample) }}</ElDescriptionsItem>
|
||||||
|
<ElDescriptionsItem label="改进建议" :span="2">
|
||||||
|
{{ formatTextOrDash(item.improvementSuggestion) }}
|
||||||
|
</ElDescriptionsItem>
|
||||||
|
<ElDescriptionsItem label="被考核人签字">
|
||||||
|
{{ formatTextOrDash(item.employeeSignName) }} / {{ formatDate(item.employeeSignedDate) }}
|
||||||
|
</ElDescriptionsItem>
|
||||||
|
<ElDescriptionsItem label="上级签字">
|
||||||
|
{{ formatTextOrDash(item.supervisorSignName) }} / {{ formatDate(item.supervisorSignedDate) }}
|
||||||
|
</ElDescriptionsItem>
|
||||||
|
</ElDescriptions>
|
||||||
|
</ElCard>
|
||||||
|
</div>
|
||||||
|
</BusinessFormDialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.work-report-approval-records {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,534 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, ref, watch } from 'vue';
|
||||||
|
import { Calendar } from '@element-plus/icons-vue';
|
||||||
|
import BusinessFormDialog from '@/components/custom/business-form-dialog.vue';
|
||||||
|
import {
|
||||||
|
type WorkReportPeriodOption,
|
||||||
|
buildMonthlyPeriodFromMonth,
|
||||||
|
buildProjectPeriodFromMonth,
|
||||||
|
buildWeeklyPeriodFromDate,
|
||||||
|
formatPeriodDisplayLabel,
|
||||||
|
getReportTypePeriodOptions
|
||||||
|
} from '../utils';
|
||||||
|
import { WORK_REPORT_TYPE_LABEL, type WorkReportType } from '../types';
|
||||||
|
|
||||||
|
defineOptions({ name: 'WorkReportCreateDialog' });
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
defaultReportType?: WorkReportType;
|
||||||
|
projectVisible?: boolean;
|
||||||
|
projectOptions?: Api.WorkReport.Project.ProjectReportOwnerProjectOption[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
defaultReportType: 'weekly',
|
||||||
|
projectVisible: false,
|
||||||
|
projectOptions: () => []
|
||||||
|
});
|
||||||
|
|
||||||
|
const visible = defineModel<boolean>('visible', { default: false });
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(
|
||||||
|
e: 'confirm',
|
||||||
|
payload:
|
||||||
|
| { reportType: 'weekly' | 'monthly'; period: WorkReportPeriodOption['period'] }
|
||||||
|
| {
|
||||||
|
reportType: 'project';
|
||||||
|
projectId: string;
|
||||||
|
flag: number;
|
||||||
|
period: WorkReportPeriodOption['period'];
|
||||||
|
}
|
||||||
|
): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const selectedPeriodKey = ref('');
|
||||||
|
const selectedProjectId = ref('');
|
||||||
|
const customWeekDate = ref('');
|
||||||
|
const customMonth = ref('');
|
||||||
|
const customProjectMonth = ref('');
|
||||||
|
const customProjectFlag = ref(1);
|
||||||
|
|
||||||
|
const selectedReportType = computed<WorkReportType>(() => {
|
||||||
|
if (props.defaultReportType === 'project' && !props.projectVisible) return 'weekly';
|
||||||
|
return props.defaultReportType;
|
||||||
|
});
|
||||||
|
|
||||||
|
const periodOptionMap = computed(() => getReportTypePeriodOptions());
|
||||||
|
const activePeriodOptions = computed(() => periodOptionMap.value[selectedReportType.value]);
|
||||||
|
const dialogTitle = computed(() => `新增${WORK_REPORT_TYPE_LABEL[selectedReportType.value]}`);
|
||||||
|
const projectHalfOptions = [
|
||||||
|
{ label: '上半月', value: 1 },
|
||||||
|
{ label: '下半月', value: 2 }
|
||||||
|
];
|
||||||
|
|
||||||
|
const defaultCustomMonth = computed(() => {
|
||||||
|
const period = activePeriodOptions.value[0]?.period;
|
||||||
|
return period?.periodStartDate.slice(0, 7) || '';
|
||||||
|
});
|
||||||
|
|
||||||
|
const customPeriod = computed<WorkReportPeriodOption['period'] | null>(() => {
|
||||||
|
if (selectedPeriodKey.value !== 'custom') return null;
|
||||||
|
|
||||||
|
if (selectedReportType.value === 'weekly') {
|
||||||
|
if (!customWeekDate.value) return null;
|
||||||
|
return buildWeeklyPeriodFromDate(customWeekDate.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectedReportType.value === 'monthly') {
|
||||||
|
if (!customMonth.value) return null;
|
||||||
|
return buildMonthlyPeriodFromMonth(customMonth.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!customProjectMonth.value) return null;
|
||||||
|
return buildProjectPeriodFromMonth(customProjectMonth.value, customProjectFlag.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
const selectedPeriod = computed(
|
||||||
|
() => activePeriodOptions.value.find(item => item.key === selectedPeriodKey.value) ?? activePeriodOptions.value[0]
|
||||||
|
);
|
||||||
|
|
||||||
|
const selectedPeriodValue = computed(() =>
|
||||||
|
selectedPeriodKey.value === 'custom' ? customPeriod.value : selectedPeriod.value?.period
|
||||||
|
);
|
||||||
|
const customPeriodPreviewLabel = computed(() =>
|
||||||
|
customPeriod.value ? formatPeriodDisplayLabel(customPeriod.value.periodLabel) : ''
|
||||||
|
);
|
||||||
|
|
||||||
|
const confirmDisabled = computed(() => {
|
||||||
|
if (!selectedPeriodValue.value) return true;
|
||||||
|
if (selectedReportType.value === 'project' && !selectedProjectId.value) return true;
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(
|
||||||
|
selectedReportType,
|
||||||
|
type => {
|
||||||
|
selectedPeriodKey.value = periodOptionMap.value[type][0]?.key || '';
|
||||||
|
|
||||||
|
if (type === 'project' && !selectedProjectId.value) {
|
||||||
|
selectedProjectId.value = props.projectOptions[0]?.id || '';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
watch(visible, isVisible => {
|
||||||
|
if (!isVisible) return;
|
||||||
|
|
||||||
|
selectedProjectId.value = props.projectOptions[0]?.id || '';
|
||||||
|
selectedPeriodKey.value = periodOptionMap.value[selectedReportType.value][0]?.key || '';
|
||||||
|
customWeekDate.value = activePeriodOptions.value[0]?.period.periodStartDate || '';
|
||||||
|
customMonth.value = defaultCustomMonth.value;
|
||||||
|
customProjectMonth.value = defaultCustomMonth.value;
|
||||||
|
customProjectFlag.value = activePeriodOptions.value[0]?.flag || 1;
|
||||||
|
});
|
||||||
|
|
||||||
|
function handleConfirm() {
|
||||||
|
const period = selectedPeriodValue.value;
|
||||||
|
if (!period) return;
|
||||||
|
|
||||||
|
if (selectedReportType.value === 'project') {
|
||||||
|
emit('confirm', {
|
||||||
|
reportType: 'project',
|
||||||
|
projectId: selectedProjectId.value,
|
||||||
|
flag: selectedPeriodKey.value === 'custom' ? customProjectFlag.value : selectedPeriod.value.flag || 1,
|
||||||
|
period
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
emit('confirm', {
|
||||||
|
reportType: selectedReportType.value,
|
||||||
|
period
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
visible.value = false;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<BusinessFormDialog
|
||||||
|
v-model="visible"
|
||||||
|
:title="dialogTitle"
|
||||||
|
class="work-report-create-dialog"
|
||||||
|
preset="md"
|
||||||
|
confirm-text="确认新增"
|
||||||
|
append-to-body
|
||||||
|
:close-on-click-modal="false"
|
||||||
|
@confirm="handleConfirm"
|
||||||
|
>
|
||||||
|
<div v-if="selectedReportType === 'project'" class="work-report-create-dialog__project-select">
|
||||||
|
<label class="work-report-create-dialog__label">项目</label>
|
||||||
|
<ElSelect v-model="selectedProjectId" class="w-full" placeholder="请选择项目" filterable>
|
||||||
|
<ElOption
|
||||||
|
v-for="item in props.projectOptions"
|
||||||
|
:key="item.id"
|
||||||
|
:label="item.projectCode ? `${item.projectName}(${item.projectCode})` : item.projectName"
|
||||||
|
:value="item.id"
|
||||||
|
/>
|
||||||
|
</ElSelect>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="work-report-create-dialog__section">
|
||||||
|
<div class="work-report-create-dialog__grid is-period">
|
||||||
|
<button
|
||||||
|
v-for="item in activePeriodOptions"
|
||||||
|
:key="item.key"
|
||||||
|
type="button"
|
||||||
|
class="work-report-create-dialog__choice"
|
||||||
|
:class="{ 'is-active': selectedPeriodKey === item.key }"
|
||||||
|
@click="selectedPeriodKey = item.key"
|
||||||
|
>
|
||||||
|
<div class="work-report-create-dialog__choice-title">{{ item.label }}</div>
|
||||||
|
<div class="work-report-create-dialog__choice-desc">{{ item.description }}</div>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="work-report-create-dialog__choice"
|
||||||
|
:class="{ 'is-active': selectedPeriodKey === 'custom' }"
|
||||||
|
@click="selectedPeriodKey = 'custom'"
|
||||||
|
>
|
||||||
|
<div class="work-report-create-dialog__choice-title">自定义周期</div>
|
||||||
|
<div class="work-report-create-dialog__choice-desc">
|
||||||
|
{{
|
||||||
|
selectedReportType === 'weekly'
|
||||||
|
? '选择某一周作为周报周期。'
|
||||||
|
: selectedReportType === 'monthly'
|
||||||
|
? '选择某一月作为月报周期。'
|
||||||
|
: '选择某个月的上半月或下半月。'
|
||||||
|
}}
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="selectedPeriodKey === 'custom'" class="work-report-create-dialog__custom-period">
|
||||||
|
<div v-if="selectedReportType === 'weekly'" class="work-report-create-dialog__custom-row">
|
||||||
|
<div class="work-report-create-dialog__field work-report-create-dialog__field--inline">
|
||||||
|
<label class="work-report-create-dialog__label">周报周期</label>
|
||||||
|
<ElDatePicker
|
||||||
|
v-model="customWeekDate"
|
||||||
|
type="date"
|
||||||
|
format="YYYY[年第]ww[周]"
|
||||||
|
value-format="YYYY-MM-DD"
|
||||||
|
popper-class="work-report-create-date-popper"
|
||||||
|
placeholder="请选择周报周期"
|
||||||
|
/>
|
||||||
|
<div v-if="customPeriodPreviewLabel" class="work-report-create-dialog__period-preview">
|
||||||
|
{{ customPeriodPreviewLabel }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="selectedReportType === 'monthly'" class="work-report-create-dialog__custom-row">
|
||||||
|
<div class="work-report-create-dialog__field work-report-create-dialog__field--inline">
|
||||||
|
<label class="work-report-create-dialog__label">月报周期</label>
|
||||||
|
<ElDatePicker
|
||||||
|
v-model="customMonth"
|
||||||
|
type="month"
|
||||||
|
value-format="YYYY-MM"
|
||||||
|
popper-class="work-report-create-date-popper"
|
||||||
|
placeholder="请选择月份"
|
||||||
|
/>
|
||||||
|
<div v-if="customPeriodPreviewLabel" class="work-report-create-dialog__period-preview">
|
||||||
|
{{ customPeriodPreviewLabel }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="work-report-create-dialog__custom-project">
|
||||||
|
<div class="work-report-create-dialog__custom-project-grid">
|
||||||
|
<div class="work-report-create-dialog__custom-project-item">
|
||||||
|
<div class="work-report-create-dialog__custom-project-item-label">选择月份</div>
|
||||||
|
<ElDatePicker
|
||||||
|
v-model="customProjectMonth"
|
||||||
|
class="w-full"
|
||||||
|
type="month"
|
||||||
|
value-format="YYYY-MM"
|
||||||
|
popper-class="work-report-create-date-popper"
|
||||||
|
placeholder="请选择月份"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="work-report-create-dialog__custom-project-item">
|
||||||
|
<div class="work-report-create-dialog__custom-project-item-label">选择半月</div>
|
||||||
|
<ElSegmented
|
||||||
|
v-model="customProjectFlag"
|
||||||
|
:options="projectHalfOptions"
|
||||||
|
class="work-report-create-dialog__half-segmented"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="customPeriodPreviewLabel" class="work-report-create-dialog__period-preview">
|
||||||
|
<ElIcon class="work-report-create-dialog__period-preview-icon"><Calendar /></ElIcon>
|
||||||
|
<span class="work-report-create-dialog__period-preview-text">已选周期:</span>
|
||||||
|
<span class="work-report-create-dialog__period-preview-value">{{ customPeriodPreviewLabel }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template #footer="{ close }">
|
||||||
|
<div class="work-report-create-dialog__footer">
|
||||||
|
<ElButton @click="close">取消</ElButton>
|
||||||
|
<ElButton type="primary" :disabled="confirmDisabled" @click="handleConfirm">确认新增</ElButton>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</BusinessFormDialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.work-report-create-dialog__header {
|
||||||
|
padding: 0 0 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.work-report-create-dialog__title {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 900;
|
||||||
|
}
|
||||||
|
|
||||||
|
.work-report-create-dialog__subtitle {
|
||||||
|
margin-top: 5px;
|
||||||
|
color: #667085;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.work-report-create-dialog__section + .work-report-create-dialog__section {
|
||||||
|
margin-top: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.work-report-create-dialog__grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.work-report-create-dialog__grid.is-period {
|
||||||
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
.work-report-create-dialog__choice {
|
||||||
|
padding: 16px;
|
||||||
|
border: 2px solid #e5edf1;
|
||||||
|
border-radius: 16px;
|
||||||
|
background: #fbfdfe;
|
||||||
|
text-align: left;
|
||||||
|
cursor: pointer;
|
||||||
|
transition:
|
||||||
|
border-color 0.16s ease,
|
||||||
|
background 0.16s ease,
|
||||||
|
box-shadow 0.16s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.work-report-create-dialog__choice:hover {
|
||||||
|
border-color: rgba(15, 118, 110, 0.28);
|
||||||
|
box-shadow: 0 8px 20px rgba(15, 23, 42, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.work-report-create-dialog__choice.is-active {
|
||||||
|
border-color: #0f766e;
|
||||||
|
background: #ecfdf5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.work-report-create-dialog__choice-title {
|
||||||
|
font-weight: 900;
|
||||||
|
color: #14213d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.work-report-create-dialog__choice-desc {
|
||||||
|
margin-top: 7px;
|
||||||
|
color: #667085;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.work-report-create-dialog__project-select {
|
||||||
|
margin: 4px 0 18px;
|
||||||
|
display: grid;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.work-report-create-dialog__field {
|
||||||
|
display: grid;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 行内字段:label 和控件在同一行,绿色 label 紧贴日期选择器右边 */
|
||||||
|
.work-report-create-dialog__field--inline {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.work-report-create-dialog__field--inline .work-report-create-dialog__label {
|
||||||
|
flex-shrink: 0;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.work-report-create-dialog__field--inline :deep(.el-date-editor) {
|
||||||
|
width: auto;
|
||||||
|
min-width: 160px;
|
||||||
|
max-width: 240px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.work-report-create-dialog__label {
|
||||||
|
color: #667085;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.work-report-create-dialog__custom-period {
|
||||||
|
margin-top: 14px;
|
||||||
|
padding: 16px;
|
||||||
|
border: 1px solid rgba(15, 118, 110, 0.18);
|
||||||
|
border-radius: 14px;
|
||||||
|
background: linear-gradient(180deg, #f8fffd 0%, #ffffff 100%);
|
||||||
|
box-shadow: 0 8px 24px rgba(15, 23, 42, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.work-report-create-dialog__custom-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
align-items: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.work-report-create-dialog__custom-row > .work-report-create-dialog__field--inline {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.work-report-create-dialog__custom-project {
|
||||||
|
display: grid;
|
||||||
|
gap: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.work-report-create-dialog__custom-project-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1.4fr) minmax(0, 1fr);
|
||||||
|
gap: 14px;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.work-report-create-dialog__custom-project-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 12px 14px;
|
||||||
|
border: 1px solid #e5edf1;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: #fff;
|
||||||
|
transition: border-color 0.18s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.work-report-create-dialog__custom-project-item:hover {
|
||||||
|
border-color: rgba(15, 118, 110, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.work-report-create-dialog__custom-project-item-label {
|
||||||
|
color: #475467;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.work-report-create-dialog__custom-project-item :deep(.el-date-editor) {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.work-report-create-dialog__half-segmented {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.work-report-create-dialog__half-segmented :deep(.el-segmented__group) {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
width: 100%;
|
||||||
|
gap: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.work-report-create-dialog__half-segmented :deep(.el-segmented__item) {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.work-report-create-dialog__period-preview {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
min-height: 32px;
|
||||||
|
padding: 0 14px;
|
||||||
|
border: 1px solid rgba(15, 118, 110, 0.18);
|
||||||
|
border-radius: 999px;
|
||||||
|
background: #ecfdf5;
|
||||||
|
color: #0f766e;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 700;
|
||||||
|
white-space: nowrap;
|
||||||
|
width: fit-content;
|
||||||
|
}
|
||||||
|
|
||||||
|
.work-report-create-dialog__period-preview-icon {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #0f766e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.work-report-create-dialog__period-preview-text {
|
||||||
|
color: #475467;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.work-report-create-dialog__period-preview-value {
|
||||||
|
color: #0f766e;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.work-report-create-dialog__footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (width <= 900px) {
|
||||||
|
.work-report-create-dialog__grid,
|
||||||
|
.work-report-create-dialog__grid.is-period {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.work-report-create-dialog__custom-row,
|
||||||
|
.work-report-create-dialog__custom-project-grid {
|
||||||
|
flex-direction: column;
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.work-report-create-dialog__field--inline {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.work-report-create-dialog__field--inline :deep(.el-date-editor) {
|
||||||
|
max-width: 100%;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.work-report-create-dialog__period-preview {
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.work-report-create-date-popper) {
|
||||||
|
border-radius: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.work-report-create-date-popper .el-picker-panel__body-wrapper) {
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.work-report-create-date-popper .el-date-table td.current:not(.disabled) .el-date-table-cell__text),
|
||||||
|
:global(.work-report-create-date-popper .el-month-table td.current:not(.disabled) .cell) {
|
||||||
|
background-color: #0f766e;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,187 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, ref, watch } from 'vue';
|
||||||
|
import { fetchGetMonthlyReportDetail, fetchGetProjectReportDetail, fetchGetWeeklyReportDetail } from '@/service/api';
|
||||||
|
import BusinessFormDialog from '@/components/custom/business-form-dialog.vue';
|
||||||
|
import BusinessFormSection from '@/components/custom/business-form-section.vue';
|
||||||
|
import {
|
||||||
|
WORK_REPORT_TYPE_LABEL,
|
||||||
|
type WorkReportRow,
|
||||||
|
type WorkReportType,
|
||||||
|
formatDate,
|
||||||
|
formatEmptyText,
|
||||||
|
formatPeriod,
|
||||||
|
formatPeriodDateRange,
|
||||||
|
formatWeeklyPeriodLabel,
|
||||||
|
getProjectReportFlagLabel,
|
||||||
|
getWorkReportStatusLabel
|
||||||
|
} from '../types';
|
||||||
|
|
||||||
|
defineOptions({ name: 'WorkReportDetailDialog' });
|
||||||
|
|
||||||
|
const visible = defineModel<boolean>('visible', { default: false });
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
reportType: WorkReportType;
|
||||||
|
rowData?: WorkReportRow | null;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const loading = ref(false);
|
||||||
|
const detail = ref<WorkReportRow | null>(null);
|
||||||
|
const title = computed(() => `${WORK_REPORT_TYPE_LABEL[props.reportType]}详情`);
|
||||||
|
const weeklyDetail = computed(() =>
|
||||||
|
props.reportType === 'weekly' ? (detail.value as Api.WorkReport.Weekly.WeeklyReport | null) : null
|
||||||
|
);
|
||||||
|
const periodText = computed(() => {
|
||||||
|
if (!detail.value) return '--';
|
||||||
|
return props.reportType === 'weekly' ? formatWeeklyPeriodLabel(detail.value) : formatPeriod(detail.value);
|
||||||
|
});
|
||||||
|
const periodTooltip = computed(() => {
|
||||||
|
if (!detail.value || props.reportType !== 'weekly') return '';
|
||||||
|
return formatPeriodDateRange(detail.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(visible, isVisible => {
|
||||||
|
if (isVisible) loadDetail();
|
||||||
|
});
|
||||||
|
|
||||||
|
async function loadDetail() {
|
||||||
|
if (!props.rowData?.id) return;
|
||||||
|
|
||||||
|
loading.value = true;
|
||||||
|
let result;
|
||||||
|
|
||||||
|
if (props.reportType === 'weekly') {
|
||||||
|
result = await fetchGetWeeklyReportDetail(props.rowData.id);
|
||||||
|
} else if (props.reportType === 'monthly') {
|
||||||
|
result = await fetchGetMonthlyReportDetail(props.rowData.id);
|
||||||
|
} else {
|
||||||
|
result = await fetchGetProjectReportDetail(props.rowData.id);
|
||||||
|
}
|
||||||
|
loading.value = false;
|
||||||
|
|
||||||
|
if (!result.error && result.data) {
|
||||||
|
detail.value = result.data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getProjectDetail() {
|
||||||
|
return detail.value as Api.WorkReport.Project.ProjectReport | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPersonalDetail() {
|
||||||
|
return detail.value as Api.WorkReport.Weekly.WeeklyReport | Api.WorkReport.Monthly.MonthlyReport | null;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<BusinessFormDialog v-model="visible" :title="title" preset="lg" :loading="loading" :show-footer="false">
|
||||||
|
<div v-if="detail" class="work-report-detail">
|
||||||
|
<BusinessFormSection title="基础信息">
|
||||||
|
<ElDescriptions :column="3" border size="small">
|
||||||
|
<ElDescriptionsItem label="报告周期">
|
||||||
|
<ElTooltip :disabled="!periodTooltip || periodTooltip === '--'" :content="periodTooltip" placement="top">
|
||||||
|
<span>{{ periodText }}</span>
|
||||||
|
</ElTooltip>
|
||||||
|
</ElDescriptionsItem>
|
||||||
|
<ElDescriptionsItem label="状态">
|
||||||
|
{{ getWorkReportStatusLabel(detail.statusCode, detail.statusName) }}
|
||||||
|
</ElDescriptionsItem>
|
||||||
|
<ElDescriptionsItem label="直属上级">{{ detail.supervisorName }}</ElDescriptionsItem>
|
||||||
|
<ElDescriptionsItem label="开始日期">{{ formatDate(detail.periodStartDate) }}</ElDescriptionsItem>
|
||||||
|
<ElDescriptionsItem label="结束日期">{{ formatDate(detail.periodEndDate) }}</ElDescriptionsItem>
|
||||||
|
<ElDescriptionsItem label="总工时">{{ formatEmptyText(detail.totalWorkHours) }}</ElDescriptionsItem>
|
||||||
|
<ElDescriptionsItem label="提交时间">{{ formatEmptyText(detail.submitTime) }}</ElDescriptionsItem>
|
||||||
|
<ElDescriptionsItem label="审批时间">{{ formatEmptyText(detail.approvalTime) }}</ElDescriptionsItem>
|
||||||
|
<ElDescriptionsItem label="审批意见">{{ formatEmptyText(detail.approvalComment) }}</ElDescriptionsItem>
|
||||||
|
</ElDescriptions>
|
||||||
|
</BusinessFormSection>
|
||||||
|
|
||||||
|
<template v-if="reportType === 'project'">
|
||||||
|
<BusinessFormSection title="项目信息">
|
||||||
|
<ElDescriptions :column="2" border size="small">
|
||||||
|
<ElDescriptionsItem label="项目名称">{{ getProjectDetail()?.projectName }}</ElDescriptionsItem>
|
||||||
|
<ElDescriptionsItem label="半月周期">
|
||||||
|
{{ getProjectReportFlagLabel(getProjectDetail()?.flag) }}
|
||||||
|
</ElDescriptionsItem>
|
||||||
|
<ElDescriptionsItem label="项目负责人">{{ getProjectDetail()?.projectOwnerName }}</ElDescriptionsItem>
|
||||||
|
<ElDescriptionsItem label="技术负责人">
|
||||||
|
{{ formatEmptyText(getProjectDetail()?.technicalOwnerName) }}
|
||||||
|
</ElDescriptionsItem>
|
||||||
|
<ElDescriptionsItem label="项目状态" :span="2">
|
||||||
|
{{ formatEmptyText(getProjectDetail()?.projectStatusDesc) }}
|
||||||
|
</ElDescriptionsItem>
|
||||||
|
<ElDescriptionsItem label="整体计划进度" :span="2">
|
||||||
|
{{ formatEmptyText(getProjectDetail()?.projectProgressPlan) }}
|
||||||
|
</ElDescriptionsItem>
|
||||||
|
<ElDescriptionsItem label="要点描述" :span="2">
|
||||||
|
{{ formatEmptyText(getProjectDetail()?.projectKeyPoints) }}
|
||||||
|
</ElDescriptionsItem>
|
||||||
|
<ElDescriptionsItem label="项目问题" :span="2">
|
||||||
|
{{ formatEmptyText(getProjectDetail()?.projectProblems) }}
|
||||||
|
</ElDescriptionsItem>
|
||||||
|
</ElDescriptions>
|
||||||
|
</BusinessFormSection>
|
||||||
|
|
||||||
|
<BusinessFormSection title="本期工作内容">
|
||||||
|
<ElTable border :data="getProjectDetail()?.currentItems || []">
|
||||||
|
<ElTableColumn prop="itemTitle" label="工作内容" min-width="180" />
|
||||||
|
<ElTableColumn prop="workHours" label="工时" width="100" />
|
||||||
|
<ElTableColumn prop="priorityCode" label="优先级" width="120" />
|
||||||
|
<ElTableColumn prop="progressRate" label="进度" width="100" />
|
||||||
|
</ElTable>
|
||||||
|
</BusinessFormSection>
|
||||||
|
|
||||||
|
<BusinessFormSection title="下期计划工作内容">
|
||||||
|
<ElTable border :data="getProjectDetail()?.nextItems || []">
|
||||||
|
<ElTableColumn prop="itemTitle" label="工作内容" min-width="180" />
|
||||||
|
<ElTableColumn prop="workHours" label="工时" width="100" />
|
||||||
|
<ElTableColumn prop="priorityCode" label="优先级" width="120" />
|
||||||
|
<ElTableColumn prop="progressRate" label="进度" width="100" />
|
||||||
|
</ElTable>
|
||||||
|
</BusinessFormSection>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-else>
|
||||||
|
<BusinessFormSection title="当期重点工作回顾">
|
||||||
|
<ElTable border :data="getPersonalDetail()?.reviewItems || []">
|
||||||
|
<ElTableColumn prop="itemTitle" label="事项" min-width="160" />
|
||||||
|
<ElTableColumn prop="workHours" label="工时" width="100" />
|
||||||
|
<ElTableColumn prop="contentText" label="工作内容" min-width="220" show-overflow-tooltip />
|
||||||
|
<ElTableColumn prop="reflectionText" label="复盘反思" min-width="220" show-overflow-tooltip />
|
||||||
|
</ElTable>
|
||||||
|
</BusinessFormSection>
|
||||||
|
|
||||||
|
<BusinessFormSection title="下周期重点工作计划">
|
||||||
|
<ElTable border :data="getPersonalDetail()?.planItems || []">
|
||||||
|
<ElTableColumn prop="itemTitle" label="事项" min-width="160" />
|
||||||
|
<ElTableColumn prop="targetText" label="目标" min-width="220" show-overflow-tooltip />
|
||||||
|
<ElTableColumn prop="supportNeed" label="支持需求" min-width="220" show-overflow-tooltip />
|
||||||
|
</ElTable>
|
||||||
|
</BusinessFormSection>
|
||||||
|
|
||||||
|
<BusinessFormSection v-if="reportType === 'weekly'" title="出差信息">
|
||||||
|
<ElDescriptions :column="3" border size="small">
|
||||||
|
<ElDescriptionsItem label="是否出差">
|
||||||
|
{{ weeklyDetail?.isBusinessTrip ? '是' : '否' }}
|
||||||
|
</ElDescriptionsItem>
|
||||||
|
<ElDescriptionsItem label="出差天数">
|
||||||
|
{{ formatEmptyText(weeklyDetail?.totalTravelDays) }}
|
||||||
|
</ElDescriptionsItem>
|
||||||
|
</ElDescriptions>
|
||||||
|
<ElTable class="mt-12px" border :data="weeklyDetail?.travelSegments || []">
|
||||||
|
<ElTableColumn prop="startDate" label="开始日期" width="120" />
|
||||||
|
<ElTableColumn prop="endDate" label="结束日期" width="120" />
|
||||||
|
<ElTableColumn prop="travelDays" label="天数" width="100" />
|
||||||
|
<ElTableColumn prop="location" label="地点" min-width="160" />
|
||||||
|
</ElTable>
|
||||||
|
</BusinessFormSection>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</BusinessFormDialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.work-report-detail {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,640 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, reactive, ref, watch } from 'vue';
|
||||||
|
import { ElMessageBox } from 'element-plus';
|
||||||
|
import {
|
||||||
|
fetchCreateMonthlyReport,
|
||||||
|
fetchCreateProjectReport,
|
||||||
|
fetchCreateWeeklyReport,
|
||||||
|
fetchGetMonthlyReportDetail,
|
||||||
|
fetchGetProjectReportDetail,
|
||||||
|
fetchGetWeeklyReportDetail,
|
||||||
|
fetchInitMonthlyReport,
|
||||||
|
fetchInitProjectReport,
|
||||||
|
fetchInitWeeklyReport,
|
||||||
|
fetchPreviewMonthlyReportDefaultDraft,
|
||||||
|
fetchPreviewProjectReportDefaultDraft,
|
||||||
|
fetchPreviewWeeklyReportDefaultDraft,
|
||||||
|
fetchRefreshMonthlyReportDraft,
|
||||||
|
fetchRefreshProjectReportDraft,
|
||||||
|
fetchRefreshWeeklyReportDraft,
|
||||||
|
fetchUpdateMonthlyReport,
|
||||||
|
fetchUpdateProjectReport,
|
||||||
|
fetchUpdateWeeklyReport
|
||||||
|
} from '@/service/api';
|
||||||
|
import BusinessFormDialog from '@/components/custom/business-form-dialog.vue';
|
||||||
|
import BusinessFormSection from '@/components/custom/business-form-section.vue';
|
||||||
|
import {
|
||||||
|
WORK_REPORT_TYPE_LABEL,
|
||||||
|
type WorkReportRow,
|
||||||
|
type WorkReportType,
|
||||||
|
createBlankPlanItem,
|
||||||
|
createBlankProjectItem,
|
||||||
|
createBlankReviewItem,
|
||||||
|
createMonthlySaveParams,
|
||||||
|
createProjectSaveParams,
|
||||||
|
createWeeklySaveParams,
|
||||||
|
normalizePlanItems,
|
||||||
|
normalizeProjectItems,
|
||||||
|
normalizeReviewItems
|
||||||
|
} from '../types';
|
||||||
|
|
||||||
|
defineOptions({ name: 'WorkReportOperateDialog' });
|
||||||
|
|
||||||
|
interface PeriodPayload {
|
||||||
|
periodKey: string;
|
||||||
|
periodLabel: string;
|
||||||
|
periodStartDate: string;
|
||||||
|
periodEndDate: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
operateType: 'add' | 'edit';
|
||||||
|
reportType: WorkReportType;
|
||||||
|
rowData?: WorkReportRow | null;
|
||||||
|
initialPeriod?: PeriodPayload | null;
|
||||||
|
initialProjectId?: string;
|
||||||
|
initialFlag?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
rowData: null,
|
||||||
|
initialPeriod: null,
|
||||||
|
initialProjectId: '',
|
||||||
|
initialFlag: 1
|
||||||
|
});
|
||||||
|
|
||||||
|
const visible = defineModel<boolean>('visible', { default: false });
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'submitted'): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const loading = ref(false);
|
||||||
|
const submitting = ref(false);
|
||||||
|
const baseInfo = ref<WorkReportRow | null>(null);
|
||||||
|
|
||||||
|
const weeklyModel = reactive<Api.WorkReport.Weekly.WeeklyReportSaveParams>(createWeeklySaveParams());
|
||||||
|
const monthlyModel = reactive<Api.WorkReport.Monthly.MonthlyReportSaveParams>(createMonthlySaveParams());
|
||||||
|
const projectModel = reactive<Api.WorkReport.Project.ProjectReportSaveParams>(createProjectSaveParams());
|
||||||
|
|
||||||
|
const title = computed(
|
||||||
|
() => `${props.operateType === 'add' ? '新增' : '编辑'}${WORK_REPORT_TYPE_LABEL[props.reportType]}`
|
||||||
|
);
|
||||||
|
const dialogPreset = computed(() => (props.reportType === 'weekly' ? 'md' : 'lg'));
|
||||||
|
const activeModel = computed(() => {
|
||||||
|
if (props.reportType === 'monthly') return monthlyModel;
|
||||||
|
if (props.reportType === 'project') return projectModel;
|
||||||
|
return weeklyModel;
|
||||||
|
});
|
||||||
|
const baseReporterName = computed(() => {
|
||||||
|
if (!baseInfo.value) return '--';
|
||||||
|
if ('projectOwnerName' in baseInfo.value) return baseInfo.value.projectOwnerName || '--';
|
||||||
|
return baseInfo.value.reporterName || '--';
|
||||||
|
});
|
||||||
|
const baseDeptName = computed(() => {
|
||||||
|
if (!baseInfo.value || 'projectOwnerName' in baseInfo.value) return '--';
|
||||||
|
return baseInfo.value.reporterDeptName || '--';
|
||||||
|
});
|
||||||
|
const basePostName = computed(() => {
|
||||||
|
if (!baseInfo.value || 'projectOwnerName' in baseInfo.value) return '--';
|
||||||
|
return baseInfo.value.reporterPostName || '--';
|
||||||
|
});
|
||||||
|
|
||||||
|
function patchPeriod(target: {
|
||||||
|
periodKey: string;
|
||||||
|
periodLabel: string;
|
||||||
|
periodStartDate: string;
|
||||||
|
periodEndDate: string;
|
||||||
|
}) {
|
||||||
|
if (!props.initialPeriod) return;
|
||||||
|
Object.assign(target, props.initialPeriod);
|
||||||
|
}
|
||||||
|
|
||||||
|
function patchWeekly(report?: Partial<Api.WorkReport.Weekly.WeeklyReport>) {
|
||||||
|
Object.assign(
|
||||||
|
weeklyModel,
|
||||||
|
createWeeklySaveParams({
|
||||||
|
...report,
|
||||||
|
reviewItems: report?.reviewItems,
|
||||||
|
planItems: report?.planItems,
|
||||||
|
travelSegments: report?.travelSegments
|
||||||
|
})
|
||||||
|
);
|
||||||
|
patchPeriod(weeklyModel);
|
||||||
|
}
|
||||||
|
|
||||||
|
function patchMonthly(report?: Partial<Api.WorkReport.Monthly.MonthlyReport>) {
|
||||||
|
Object.assign(
|
||||||
|
monthlyModel,
|
||||||
|
createMonthlySaveParams({
|
||||||
|
...report,
|
||||||
|
reviewItems: report?.reviewItems,
|
||||||
|
planItems: report?.planItems
|
||||||
|
})
|
||||||
|
);
|
||||||
|
patchPeriod(monthlyModel);
|
||||||
|
}
|
||||||
|
|
||||||
|
function patchProject(report?: Partial<Api.WorkReport.Project.ProjectReport>) {
|
||||||
|
Object.assign(
|
||||||
|
projectModel,
|
||||||
|
createProjectSaveParams({
|
||||||
|
...report,
|
||||||
|
projectId: report?.projectId || props.initialProjectId,
|
||||||
|
flag: report?.flag ?? props.initialFlag,
|
||||||
|
currentItems: report?.currentItems,
|
||||||
|
nextItems: report?.nextItems
|
||||||
|
})
|
||||||
|
);
|
||||||
|
patchPeriod(projectModel);
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyWeeklyEditableFields(draft: Api.WorkReport.Weekly.WeeklyReport) {
|
||||||
|
weeklyModel.reviewItems = normalizeReviewItems(draft.reviewItems);
|
||||||
|
weeklyModel.planItems = normalizePlanItems(draft.planItems);
|
||||||
|
weeklyModel.travelSegments = draft.travelSegments || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyMonthlyEditableFields(draft: Api.WorkReport.Monthly.MonthlyReport) {
|
||||||
|
monthlyModel.reviewItems = normalizeReviewItems(draft.reviewItems);
|
||||||
|
monthlyModel.planItems = normalizePlanItems(draft.planItems);
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyProjectEditableFields(draft: Api.WorkReport.Project.ProjectReport) {
|
||||||
|
projectModel.projectStatusDesc = draft.projectStatusDesc || '';
|
||||||
|
projectModel.projectProgressPlan = draft.projectProgressPlan || '';
|
||||||
|
projectModel.projectKeyPoints = draft.projectKeyPoints || '';
|
||||||
|
projectModel.projectProblems = draft.projectProblems || '';
|
||||||
|
projectModel.currentItems = normalizeProjectItems(draft.currentItems);
|
||||||
|
projectModel.nextItems = normalizeProjectItems(draft.nextItems);
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyEditableFieldsByReportType(
|
||||||
|
draft:
|
||||||
|
| Api.WorkReport.Weekly.WeeklyReport
|
||||||
|
| Api.WorkReport.Monthly.MonthlyReport
|
||||||
|
| Api.WorkReport.Project.ProjectReport
|
||||||
|
) {
|
||||||
|
if (props.reportType === 'weekly') {
|
||||||
|
applyWeeklyEditableFields(draft as Api.WorkReport.Weekly.WeeklyReport);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.reportType === 'monthly') {
|
||||||
|
applyMonthlyEditableFields(draft as Api.WorkReport.Monthly.MonthlyReport);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
applyProjectEditableFields(draft as Api.WorkReport.Project.ProjectReport);
|
||||||
|
}
|
||||||
|
|
||||||
|
function createCurrentPeriodPayload(): PeriodPayload {
|
||||||
|
return {
|
||||||
|
periodKey: activeModel.value.periodKey,
|
||||||
|
periodLabel: activeModel.value.periodLabel,
|
||||||
|
periodStartDate: activeModel.value.periodStartDate,
|
||||||
|
periodEndDate: activeModel.value.periodEndDate
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function confirmDraftOverwrite(confirmOverwrite: boolean) {
|
||||||
|
if (!confirmOverwrite || props.operateType === 'edit') return true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm('重新拉取默认稿会覆盖当前已编辑内容,是否继续?', '覆盖确认', {
|
||||||
|
type: 'warning',
|
||||||
|
confirmButtonText: '继续',
|
||||||
|
cancelButtonText: '取消'
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchEditDraftRefresh() {
|
||||||
|
if (props.reportType === 'weekly') {
|
||||||
|
return fetchRefreshWeeklyReportDraft(weeklyModel);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.reportType === 'monthly') {
|
||||||
|
return fetchRefreshMonthlyReportDraft(monthlyModel);
|
||||||
|
}
|
||||||
|
|
||||||
|
return fetchRefreshProjectReportDraft(projectModel.projectId, {
|
||||||
|
periodKey: projectModel.periodKey,
|
||||||
|
periodLabel: projectModel.periodLabel,
|
||||||
|
periodStartDate: projectModel.periodStartDate,
|
||||||
|
periodEndDate: projectModel.periodEndDate,
|
||||||
|
flag: projectModel.flag,
|
||||||
|
projectStatusDesc: projectModel.projectStatusDesc,
|
||||||
|
projectProgressPlan: projectModel.projectProgressPlan,
|
||||||
|
projectKeyPoints: projectModel.projectKeyPoints,
|
||||||
|
projectProblems: projectModel.projectProblems,
|
||||||
|
currentItems: projectModel.currentItems,
|
||||||
|
nextItems: projectModel.nextItems
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchDefaultDraftPreview() {
|
||||||
|
const period = createCurrentPeriodPayload();
|
||||||
|
|
||||||
|
if (props.reportType === 'weekly') {
|
||||||
|
return fetchPreviewWeeklyReportDefaultDraft(period);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.reportType === 'monthly') {
|
||||||
|
return fetchPreviewMonthlyReportDefaultDraft(period);
|
||||||
|
}
|
||||||
|
|
||||||
|
return fetchPreviewProjectReportDefaultDraft(projectModel.projectId, {
|
||||||
|
...period,
|
||||||
|
flag: projectModel.flag
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadDetail() {
|
||||||
|
if (!props.rowData?.id) return;
|
||||||
|
|
||||||
|
loading.value = true;
|
||||||
|
let result;
|
||||||
|
|
||||||
|
if (props.reportType === 'weekly') {
|
||||||
|
result = await fetchGetWeeklyReportDetail(props.rowData.id);
|
||||||
|
} else if (props.reportType === 'monthly') {
|
||||||
|
result = await fetchGetMonthlyReportDetail(props.rowData.id);
|
||||||
|
} else {
|
||||||
|
result = await fetchGetProjectReportDetail(props.rowData.id);
|
||||||
|
}
|
||||||
|
loading.value = false;
|
||||||
|
|
||||||
|
if (result.error || !result.data) return;
|
||||||
|
|
||||||
|
baseInfo.value = result.data;
|
||||||
|
|
||||||
|
if (props.reportType === 'weekly') patchWeekly(result.data as Api.WorkReport.Weekly.WeeklyReport);
|
||||||
|
if (props.reportType === 'monthly') patchMonthly(result.data as Api.WorkReport.Monthly.MonthlyReport);
|
||||||
|
if (props.reportType === 'project') patchProject(result.data as Api.WorkReport.Project.ProjectReport);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadInitAndDraft() {
|
||||||
|
loading.value = true;
|
||||||
|
let initResult;
|
||||||
|
|
||||||
|
if (props.reportType === 'weekly') {
|
||||||
|
initResult = await fetchInitWeeklyReport();
|
||||||
|
} else if (props.reportType === 'monthly') {
|
||||||
|
initResult = await fetchInitMonthlyReport();
|
||||||
|
} else {
|
||||||
|
initResult = await fetchInitProjectReport(props.initialProjectId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!initResult.error && initResult.data) {
|
||||||
|
baseInfo.value = initResult.data;
|
||||||
|
if (props.reportType === 'weekly') patchWeekly(initResult.data as Api.WorkReport.Weekly.WeeklyReport);
|
||||||
|
if (props.reportType === 'monthly') patchMonthly(initResult.data as Api.WorkReport.Monthly.MonthlyReport);
|
||||||
|
if (props.reportType === 'project') patchProject(initResult.data as Api.WorkReport.Project.ProjectReport);
|
||||||
|
}
|
||||||
|
|
||||||
|
await pullDefaultDraft(false);
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function pullDefaultDraft(confirmOverwrite = true) {
|
||||||
|
const confirmed = await confirmDraftOverwrite(confirmOverwrite);
|
||||||
|
if (!confirmed) return;
|
||||||
|
|
||||||
|
if (props.operateType === 'edit') {
|
||||||
|
const refreshResult = await fetchEditDraftRefresh();
|
||||||
|
|
||||||
|
if (refreshResult.error || !refreshResult.data) return;
|
||||||
|
|
||||||
|
applyEditableFieldsByReportType(refreshResult.data);
|
||||||
|
|
||||||
|
window.$message?.success('最新数据已刷新');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await fetchDefaultDraftPreview();
|
||||||
|
|
||||||
|
if (result.error || !result.data) return;
|
||||||
|
|
||||||
|
applyEditableFieldsByReportType(result.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(visible, isVisible => {
|
||||||
|
if (!isVisible) return;
|
||||||
|
baseInfo.value = null;
|
||||||
|
if (props.operateType === 'edit') {
|
||||||
|
loadDetail();
|
||||||
|
} else {
|
||||||
|
loadInitAndDraft();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function addReviewItem(items: Api.WorkReport.Common.PersonalReportReviewItem[]) {
|
||||||
|
items.push(createBlankReviewItem(items.length));
|
||||||
|
}
|
||||||
|
|
||||||
|
function addPlanItem(items: Api.WorkReport.Common.PersonalReportPlanItem[]) {
|
||||||
|
items.push(createBlankPlanItem(items.length));
|
||||||
|
}
|
||||||
|
|
||||||
|
function addProjectItem(items: Api.WorkReport.Project.ProjectReportItem[]) {
|
||||||
|
items.push(createBlankProjectItem());
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeItem<T>(items: T[], index: number) {
|
||||||
|
if (items.length <= 1) return;
|
||||||
|
items.splice(index, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateBase() {
|
||||||
|
if (!activeModel.value.periodKey || !activeModel.value.periodStartDate || !activeModel.value.periodEndDate) {
|
||||||
|
window.$message?.warning('请先选择报告周期');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.reportType === 'project' && !projectModel.projectId) {
|
||||||
|
window.$message?.warning('请选择项目');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSubmit() {
|
||||||
|
if (!validateBase()) return;
|
||||||
|
|
||||||
|
submitting.value = true;
|
||||||
|
let result;
|
||||||
|
|
||||||
|
if (props.reportType === 'weekly') {
|
||||||
|
result =
|
||||||
|
props.operateType === 'add'
|
||||||
|
? await fetchCreateWeeklyReport(weeklyModel)
|
||||||
|
: await fetchUpdateWeeklyReport(props.rowData!.id, weeklyModel);
|
||||||
|
} else if (props.reportType === 'monthly') {
|
||||||
|
result =
|
||||||
|
props.operateType === 'add'
|
||||||
|
? await fetchCreateMonthlyReport(monthlyModel)
|
||||||
|
: await fetchUpdateMonthlyReport(props.rowData!.id, monthlyModel);
|
||||||
|
} else {
|
||||||
|
result =
|
||||||
|
props.operateType === 'add'
|
||||||
|
? await fetchCreateProjectReport(projectModel)
|
||||||
|
: await fetchUpdateProjectReport(props.rowData!.id, projectModel);
|
||||||
|
}
|
||||||
|
submitting.value = false;
|
||||||
|
|
||||||
|
if (result.error) return;
|
||||||
|
|
||||||
|
window.$message?.success(props.operateType === 'add' ? '工作报告已创建' : '工作报告已保存');
|
||||||
|
visible.value = false;
|
||||||
|
emit('submitted');
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<BusinessFormDialog
|
||||||
|
v-model="visible"
|
||||||
|
:title="title"
|
||||||
|
:preset="dialogPreset"
|
||||||
|
:loading="loading"
|
||||||
|
:confirm-loading="submitting"
|
||||||
|
max-body-height="76vh"
|
||||||
|
@confirm="handleSubmit"
|
||||||
|
>
|
||||||
|
<div class="work-report-operate">
|
||||||
|
<BusinessFormSection title="基础信息">
|
||||||
|
<ElDescriptions :column="3" border size="small">
|
||||||
|
<ElDescriptionsItem label="填报人">
|
||||||
|
{{ baseReporterName }}
|
||||||
|
</ElDescriptionsItem>
|
||||||
|
<ElDescriptionsItem label="部门">{{ baseDeptName }}</ElDescriptionsItem>
|
||||||
|
<ElDescriptionsItem label="岗位">{{ basePostName }}</ElDescriptionsItem>
|
||||||
|
<ElDescriptionsItem label="直属上级">{{ baseInfo?.supervisorName || '--' }}</ElDescriptionsItem>
|
||||||
|
<ElDescriptionsItem label="周期" :span="2">{{ activeModel.periodLabel || '--' }}</ElDescriptionsItem>
|
||||||
|
</ElDescriptions>
|
||||||
|
|
||||||
|
<div class="mt-12px flex justify-end">
|
||||||
|
<ElButton plain type="primary" @click="pullDefaultDraft(true)">
|
||||||
|
<template #icon>
|
||||||
|
<icon-mdi-refresh class="text-icon" />
|
||||||
|
</template>
|
||||||
|
刷新
|
||||||
|
</ElButton>
|
||||||
|
</div>
|
||||||
|
</BusinessFormSection>
|
||||||
|
|
||||||
|
<template v-if="reportType === 'project'">
|
||||||
|
<BusinessFormSection title="项目状况">
|
||||||
|
<ElRow :gutter="16">
|
||||||
|
<ElCol :span="12">
|
||||||
|
<ElFormItem label="项目状态">
|
||||||
|
<ElInput v-model="projectModel.projectStatusDesc" type="textarea" :rows="3" />
|
||||||
|
</ElFormItem>
|
||||||
|
</ElCol>
|
||||||
|
<ElCol :span="12">
|
||||||
|
<ElFormItem label="整体计划进度">
|
||||||
|
<ElInput v-model="projectModel.projectProgressPlan" type="textarea" :rows="3" />
|
||||||
|
</ElFormItem>
|
||||||
|
</ElCol>
|
||||||
|
<ElCol :span="12">
|
||||||
|
<ElFormItem label="要点描述">
|
||||||
|
<ElInput v-model="projectModel.projectKeyPoints" type="textarea" :rows="3" />
|
||||||
|
</ElFormItem>
|
||||||
|
</ElCol>
|
||||||
|
<ElCol :span="12">
|
||||||
|
<ElFormItem label="项目问题">
|
||||||
|
<ElInput v-model="projectModel.projectProblems" type="textarea" :rows="3" />
|
||||||
|
</ElFormItem>
|
||||||
|
</ElCol>
|
||||||
|
</ElRow>
|
||||||
|
</BusinessFormSection>
|
||||||
|
|
||||||
|
<BusinessFormSection title="本期工作内容">
|
||||||
|
<div class="work-report-operate__items">
|
||||||
|
<div v-for="(item, index) in projectModel.currentItems" :key="index" class="work-report-operate__item">
|
||||||
|
<ElInput v-model="item.itemTitle" placeholder="工作内容" />
|
||||||
|
<ElInputNumber v-model="item.workHours" :min="0" :precision="1" placeholder="工时" />
|
||||||
|
<ElInput v-model="item.priorityCode" placeholder="优先级" />
|
||||||
|
<ElInputNumber v-model="item.progressRate" :min="0" :max="100" :precision="0" placeholder="进度" />
|
||||||
|
<ElButton link type="danger" @click="removeItem(projectModel.currentItems, index)">删除</ElButton>
|
||||||
|
</div>
|
||||||
|
<ElButton plain @click="addProjectItem(projectModel.currentItems)">新增本期工作</ElButton>
|
||||||
|
</div>
|
||||||
|
</BusinessFormSection>
|
||||||
|
|
||||||
|
<BusinessFormSection title="下期计划工作内容">
|
||||||
|
<div class="work-report-operate__items">
|
||||||
|
<div v-for="(item, index) in projectModel.nextItems" :key="index" class="work-report-operate__item">
|
||||||
|
<ElInput v-model="item.itemTitle" placeholder="工作内容" />
|
||||||
|
<ElInputNumber v-model="item.workHours" :min="0" :precision="1" placeholder="工时" />
|
||||||
|
<ElInput v-model="item.priorityCode" placeholder="优先级" />
|
||||||
|
<ElInputNumber v-model="item.progressRate" :min="0" :max="100" :precision="0" placeholder="进度" />
|
||||||
|
<ElButton link type="danger" @click="removeItem(projectModel.nextItems, index)">删除</ElButton>
|
||||||
|
</div>
|
||||||
|
<ElButton plain @click="addProjectItem(projectModel.nextItems)">新增下期工作</ElButton>
|
||||||
|
</div>
|
||||||
|
</BusinessFormSection>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-else>
|
||||||
|
<BusinessFormSection title="当期重点工作回顾">
|
||||||
|
<div class="work-report-operate__cards">
|
||||||
|
<div
|
||||||
|
v-for="(item, index) in reportType === 'weekly' ? weeklyModel.reviewItems : monthlyModel.reviewItems"
|
||||||
|
:key="index"
|
||||||
|
class="work-report-operate__card"
|
||||||
|
>
|
||||||
|
<ElRow :gutter="16">
|
||||||
|
<ElCol :span="14">
|
||||||
|
<ElFormItem label="事项标题">
|
||||||
|
<ElInput v-model="item.itemTitle" />
|
||||||
|
</ElFormItem>
|
||||||
|
</ElCol>
|
||||||
|
<ElCol :span="6">
|
||||||
|
<ElFormItem label="工时">
|
||||||
|
<ElInputNumber v-model="item.workHours" class="w-full" :min="0" :precision="1" />
|
||||||
|
</ElFormItem>
|
||||||
|
</ElCol>
|
||||||
|
<ElCol :span="4" class="flex items-end justify-end pb-16px">
|
||||||
|
<ElButton
|
||||||
|
link
|
||||||
|
type="danger"
|
||||||
|
@click="
|
||||||
|
removeItem(reportType === 'weekly' ? weeklyModel.reviewItems : monthlyModel.reviewItems, index)
|
||||||
|
"
|
||||||
|
>
|
||||||
|
删除
|
||||||
|
</ElButton>
|
||||||
|
</ElCol>
|
||||||
|
<ElCol :span="12">
|
||||||
|
<ElFormItem label="工作内容">
|
||||||
|
<ElInput v-model="item.contentText" type="textarea" :rows="3" />
|
||||||
|
</ElFormItem>
|
||||||
|
</ElCol>
|
||||||
|
<ElCol :span="12">
|
||||||
|
<ElFormItem label="复盘反思">
|
||||||
|
<ElInput v-model="item.reflectionText" type="textarea" :rows="3" />
|
||||||
|
</ElFormItem>
|
||||||
|
</ElCol>
|
||||||
|
</ElRow>
|
||||||
|
</div>
|
||||||
|
<ElButton
|
||||||
|
plain
|
||||||
|
@click="addReviewItem(reportType === 'weekly' ? weeklyModel.reviewItems : monthlyModel.reviewItems)"
|
||||||
|
>
|
||||||
|
新增回顾项
|
||||||
|
</ElButton>
|
||||||
|
</div>
|
||||||
|
</BusinessFormSection>
|
||||||
|
|
||||||
|
<BusinessFormSection title="下周期重点工作计划">
|
||||||
|
<div class="work-report-operate__cards">
|
||||||
|
<div
|
||||||
|
v-for="(item, index) in reportType === 'weekly' ? weeklyModel.planItems : monthlyModel.planItems"
|
||||||
|
:key="index"
|
||||||
|
class="work-report-operate__card"
|
||||||
|
>
|
||||||
|
<ElRow :gutter="16">
|
||||||
|
<ElCol :span="20">
|
||||||
|
<ElFormItem label="计划标题">
|
||||||
|
<ElInput v-model="item.itemTitle" />
|
||||||
|
</ElFormItem>
|
||||||
|
</ElCol>
|
||||||
|
<ElCol :span="4" class="flex items-end justify-end pb-16px">
|
||||||
|
<ElButton
|
||||||
|
link
|
||||||
|
type="danger"
|
||||||
|
@click="removeItem(reportType === 'weekly' ? weeklyModel.planItems : monthlyModel.planItems, index)"
|
||||||
|
>
|
||||||
|
删除
|
||||||
|
</ElButton>
|
||||||
|
</ElCol>
|
||||||
|
<ElCol :span="12">
|
||||||
|
<ElFormItem label="目标">
|
||||||
|
<ElInput v-model="item.targetText" type="textarea" :rows="3" />
|
||||||
|
</ElFormItem>
|
||||||
|
</ElCol>
|
||||||
|
<ElCol :span="12">
|
||||||
|
<ElFormItem label="支持需求">
|
||||||
|
<ElInput v-model="item.supportNeed" type="textarea" :rows="3" />
|
||||||
|
</ElFormItem>
|
||||||
|
</ElCol>
|
||||||
|
</ElRow>
|
||||||
|
</div>
|
||||||
|
<ElButton
|
||||||
|
plain
|
||||||
|
@click="addPlanItem(reportType === 'weekly' ? weeklyModel.planItems : monthlyModel.planItems)"
|
||||||
|
>
|
||||||
|
新增计划项
|
||||||
|
</ElButton>
|
||||||
|
</div>
|
||||||
|
</BusinessFormSection>
|
||||||
|
|
||||||
|
<BusinessFormSection v-if="reportType === 'weekly'" title="出差信息">
|
||||||
|
<ElFormItem label="是否出差">
|
||||||
|
<ElSwitch v-model="weeklyModel.isBusinessTrip" />
|
||||||
|
</ElFormItem>
|
||||||
|
<div v-if="weeklyModel.isBusinessTrip" class="work-report-operate__items">
|
||||||
|
<div v-for="(item, index) in weeklyModel.travelSegments" :key="index" class="work-report-operate__item">
|
||||||
|
<ElDatePicker v-model="item.startDate" type="date" value-format="YYYY-MM-DD" placeholder="开始日期" />
|
||||||
|
<ElDatePicker v-model="item.endDate" type="date" value-format="YYYY-MM-DD" placeholder="结束日期" />
|
||||||
|
<ElInputNumber v-model="item.travelDays" :min="0" :precision="1" placeholder="天数" />
|
||||||
|
<ElInput v-model="item.location" placeholder="地点" />
|
||||||
|
<ElButton link type="danger" @click="removeItem(weeklyModel.travelSegments, index)">删除</ElButton>
|
||||||
|
</div>
|
||||||
|
<ElButton
|
||||||
|
plain
|
||||||
|
@click="
|
||||||
|
weeklyModel.travelSegments.push({
|
||||||
|
sort: weeklyModel.travelSegments.length + 1,
|
||||||
|
travelDays: 0,
|
||||||
|
location: ''
|
||||||
|
})
|
||||||
|
"
|
||||||
|
>
|
||||||
|
新增出差分段
|
||||||
|
</ElButton>
|
||||||
|
</div>
|
||||||
|
</BusinessFormSection>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</BusinessFormDialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.work-report-operate {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.work-report-operate__cards,
|
||||||
|
.work-report-operate__items {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.work-report-operate__card,
|
||||||
|
.work-report-operate__item {
|
||||||
|
padding: 12px;
|
||||||
|
border: 1px solid var(--el-border-color-lighter);
|
||||||
|
border-radius: 6px;
|
||||||
|
background-color: var(--el-fill-color-extra-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.work-report-operate__item {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1fr) 120px 120px 120px auto;
|
||||||
|
gap: 10px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (width <= 900px) {
|
||||||
|
.work-report-operate__item {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
||||||
|
import { useRoute } from 'vue-router';
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: 'WorkReportPageDialog',
|
||||||
|
inheritAttrs: false
|
||||||
|
});
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
title?: string;
|
||||||
|
loading?: boolean;
|
||||||
|
showFooter?: boolean;
|
||||||
|
approvalMode?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
title: '',
|
||||||
|
loading: false,
|
||||||
|
showFooter: false,
|
||||||
|
approvalMode: false
|
||||||
|
});
|
||||||
|
|
||||||
|
const visible = defineModel<boolean>('visible', { default: false });
|
||||||
|
const route = useRoute();
|
||||||
|
const viewportWidth = ref(typeof window === 'undefined' ? 1920 : window.innerWidth);
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'close'): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const drawerSize = computed(() => (viewportWidth.value >= 2560 ? '60%' : '75%'));
|
||||||
|
|
||||||
|
function handleClose() {
|
||||||
|
visible.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncViewportWidth() {
|
||||||
|
viewportWidth.value = window.innerWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 抽屉关闭动画结束后触发 close 事件 */
|
||||||
|
function onDrawerClosed() {
|
||||||
|
emit('close');
|
||||||
|
}
|
||||||
|
|
||||||
|
const drawerBodyClass = props.approvalMode
|
||||||
|
? 'work-report-page-drawer__body work-report-page-drawer__body--approval'
|
||||||
|
: 'work-report-page-drawer__body';
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => route.fullPath,
|
||||||
|
() => {
|
||||||
|
if (visible.value) {
|
||||||
|
visible.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
syncViewportWidth();
|
||||||
|
window.addEventListener('resize', syncViewportWidth);
|
||||||
|
});
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
window.removeEventListener('resize', syncViewportWidth);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<ElDrawer
|
||||||
|
v-model="visible"
|
||||||
|
class="work-report-page-drawer"
|
||||||
|
:class="{ 'work-report-page-drawer--approval': props.approvalMode }"
|
||||||
|
:body-class="drawerBodyClass"
|
||||||
|
:title="props.title"
|
||||||
|
:size="drawerSize"
|
||||||
|
:close-on-click-modal="false"
|
||||||
|
append-to-body
|
||||||
|
@closed="onDrawerClosed"
|
||||||
|
>
|
||||||
|
<div v-loading="props.loading" class="work-report-page-drawer__content">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="props.showFooter" class="work-report-page-drawer__footer">
|
||||||
|
<slot name="footer" :close="handleClose" />
|
||||||
|
</div>
|
||||||
|
</ElDrawer>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
:global(.work-report-page-drawer__body) {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 0;
|
||||||
|
padding-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.work-report-page-drawer__body--approval) {
|
||||||
|
padding-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.work-report-page-drawer__content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 0;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.work-report-page-drawer__content :deep(.form-page) {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex: 1 0 auto;
|
||||||
|
min-height: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.work-report-page-drawer__footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 12px 20px;
|
||||||
|
border-top: 1px solid var(--el-border-color-lighter);
|
||||||
|
background: var(--el-bg-color);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,857 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
/* eslint-disable complexity, no-nested-ternary, no-void, vue/no-deprecated-filter */
|
||||||
|
import { computed, reactive, ref, watch } from 'vue';
|
||||||
|
import { ElMessageBox } from 'element-plus';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
import {
|
||||||
|
fetchApproveMonthlyReport,
|
||||||
|
fetchApproveProjectReport,
|
||||||
|
fetchApproveWeeklyReport,
|
||||||
|
fetchCreateMonthlyReport,
|
||||||
|
fetchCreateProjectReport,
|
||||||
|
fetchCreateWeeklyReport,
|
||||||
|
fetchGetMonthlyReportDetail,
|
||||||
|
fetchGetProjectReportDetail,
|
||||||
|
fetchGetWeeklyReportDetail,
|
||||||
|
fetchInitMonthlyReport,
|
||||||
|
fetchInitProjectReport,
|
||||||
|
fetchInitWeeklyReport,
|
||||||
|
fetchPreviewMonthlyReportDefaultDraft,
|
||||||
|
fetchPreviewProjectReportDefaultDraft,
|
||||||
|
fetchPreviewWeeklyReportDefaultDraft,
|
||||||
|
fetchRefreshMonthlyReportDraft,
|
||||||
|
fetchRefreshProjectReportDraft,
|
||||||
|
fetchRefreshWeeklyReportDraft,
|
||||||
|
fetchRejectMonthlyReport,
|
||||||
|
fetchRejectProjectReport,
|
||||||
|
fetchRejectWeeklyReport,
|
||||||
|
fetchSubmitMonthlyReport,
|
||||||
|
fetchSubmitProjectReport,
|
||||||
|
fetchSubmitWeeklyReport,
|
||||||
|
fetchUpdateMonthlyReport,
|
||||||
|
fetchUpdateProjectReport,
|
||||||
|
fetchUpdateWeeklyReport
|
||||||
|
} from '@/service/api';
|
||||||
|
import type { WorkReportRow, WorkReportType } from '../types';
|
||||||
|
import {
|
||||||
|
createMonthlySaveParams,
|
||||||
|
createProjectSaveParams,
|
||||||
|
createWeeklySaveParams,
|
||||||
|
formatPeriodLabel,
|
||||||
|
getStructuredSections,
|
||||||
|
normalizePlanItems,
|
||||||
|
normalizeProjectItems,
|
||||||
|
normalizeReviewItems
|
||||||
|
} from '../types';
|
||||||
|
import WeeklyReportPage from '../../weekly/modules/fill-page.vue';
|
||||||
|
import MonthlyReportPage from '../../monthly/modules/fill-page.vue';
|
||||||
|
import ProjectReportPage from '../../project/modules/fill-page.vue';
|
||||||
|
import MonthlyReportApprovalPage from '../../monthly/modules/approval-page.vue';
|
||||||
|
import WorkReportActionDialog from './action-dialog.vue';
|
||||||
|
import WorkReportPageDialog from './page-dialog.vue';
|
||||||
|
|
||||||
|
defineOptions({ name: 'WorkReportPrototypePageDialog' });
|
||||||
|
|
||||||
|
interface PeriodPayload {
|
||||||
|
periodKey: string;
|
||||||
|
periodLabel: string;
|
||||||
|
periodStartDate: string;
|
||||||
|
periodEndDate: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
reportType: WorkReportType;
|
||||||
|
mode?: 'add' | 'edit' | 'detail';
|
||||||
|
scene?: 'fill' | 'detail' | 'approval';
|
||||||
|
rowData?: WorkReportRow | null;
|
||||||
|
initialPeriod?: PeriodPayload | null;
|
||||||
|
initialProjectId?: string;
|
||||||
|
initialFlag?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
mode: 'detail',
|
||||||
|
scene: 'detail',
|
||||||
|
rowData: null,
|
||||||
|
initialPeriod: null,
|
||||||
|
initialProjectId: '',
|
||||||
|
initialFlag: 1
|
||||||
|
});
|
||||||
|
|
||||||
|
const visible = defineModel<boolean>('visible', { default: false });
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'submitted'): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const REPORT_TYPE_TEXT: Record<WorkReportType, string> = {
|
||||||
|
weekly: '个人周报',
|
||||||
|
monthly: '个人月报',
|
||||||
|
project: '项目半月报'
|
||||||
|
};
|
||||||
|
|
||||||
|
const loading = ref(false);
|
||||||
|
const actionVisible = ref(false);
|
||||||
|
const actionSubmitting = ref(false);
|
||||||
|
const currentActionType = ref<'approve' | 'reject'>('approve');
|
||||||
|
const currentStage = ref<'form' | 'approval'>('form');
|
||||||
|
const currentReportId = ref('');
|
||||||
|
const baseInfo = ref<WorkReportRow | null>(null);
|
||||||
|
const monthlyApprovalDraft = reactive<Api.WorkReport.Monthly.MonthlyReportApproveParams>({
|
||||||
|
reason: '',
|
||||||
|
meetingDate: '',
|
||||||
|
strengthDesc: '',
|
||||||
|
strengthExample: '',
|
||||||
|
weaknessDesc: '',
|
||||||
|
weaknessExample: '',
|
||||||
|
improvementSuggestion: '',
|
||||||
|
performanceResult: '',
|
||||||
|
employeeSignName: '',
|
||||||
|
employeeSignedDate: '',
|
||||||
|
supervisorSignName: '',
|
||||||
|
supervisorSignedDate: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
const weeklyModel = reactive<Api.WorkReport.Weekly.WeeklyReportSaveParams>(createWeeklySaveParams());
|
||||||
|
const monthlyModel = reactive<Api.WorkReport.Monthly.MonthlyReportSaveParams>(createMonthlySaveParams());
|
||||||
|
const projectModel = reactive<Api.WorkReport.Project.ProjectReportSaveParams>(createProjectSaveParams());
|
||||||
|
|
||||||
|
const currentModel = computed(() => {
|
||||||
|
if (props.reportType === 'monthly') return monthlyModel;
|
||||||
|
if (props.reportType === 'project') return projectModel;
|
||||||
|
return weeklyModel;
|
||||||
|
});
|
||||||
|
|
||||||
|
const currentScene = computed(() => {
|
||||||
|
if (props.reportType === 'monthly' && currentStage.value === 'approval') {
|
||||||
|
return 'approval';
|
||||||
|
}
|
||||||
|
|
||||||
|
return props.scene;
|
||||||
|
});
|
||||||
|
|
||||||
|
const dialogTitle = computed(() => {
|
||||||
|
if (props.reportType === 'monthly' && currentStage.value === 'approval') {
|
||||||
|
return `${REPORT_TYPE_TEXT.monthly}审批页`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentScene.value === 'approval') {
|
||||||
|
return `${REPORT_TYPE_TEXT[props.reportType]}审批`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.mode === 'add') return `${REPORT_TYPE_TEXT[props.reportType]}填报页`;
|
||||||
|
if (props.mode === 'edit') return `${REPORT_TYPE_TEXT[props.reportType]}编辑页`;
|
||||||
|
return `${REPORT_TYPE_TEXT[props.reportType]}查看页`;
|
||||||
|
});
|
||||||
|
|
||||||
|
const periodText = computed(() => {
|
||||||
|
const label = currentModel.value.periodLabel || props.rowData?.periodLabel || props.initialPeriod?.periodLabel;
|
||||||
|
return formatPeriodLabel(label) || '当前周期';
|
||||||
|
});
|
||||||
|
|
||||||
|
function resetModels() {
|
||||||
|
Object.assign(weeklyModel, createWeeklySaveParams());
|
||||||
|
Object.assign(monthlyModel, createMonthlySaveParams());
|
||||||
|
Object.assign(projectModel, createProjectSaveParams());
|
||||||
|
Object.assign(monthlyApprovalDraft, {
|
||||||
|
reason: '',
|
||||||
|
meetingDate: '',
|
||||||
|
strengthDesc: '',
|
||||||
|
strengthExample: '',
|
||||||
|
weaknessDesc: '',
|
||||||
|
weaknessExample: '',
|
||||||
|
improvementSuggestion: '',
|
||||||
|
performanceResult: '',
|
||||||
|
employeeSignName: '',
|
||||||
|
employeeSignedDate: '',
|
||||||
|
supervisorSignName: '',
|
||||||
|
supervisorSignedDate: ''
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function patchMonthlyApprovalDefaults(report?: Partial<Api.WorkReport.Monthly.MonthlyReport> | null) {
|
||||||
|
const today = dayjs().format('YYYY-MM-DD');
|
||||||
|
|
||||||
|
Object.assign(monthlyApprovalDraft, {
|
||||||
|
employeeSignName: monthlyApprovalDraft.employeeSignName || report?.reporterName || '',
|
||||||
|
employeeSignedDate: monthlyApprovalDraft.employeeSignedDate || today,
|
||||||
|
supervisorSignName: monthlyApprovalDraft.supervisorSignName || report?.supervisorName || '',
|
||||||
|
supervisorSignedDate: monthlyApprovalDraft.supervisorSignedDate || today
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function patchPeriod(target: {
|
||||||
|
periodKey: string;
|
||||||
|
periodLabel: string;
|
||||||
|
periodStartDate: string;
|
||||||
|
periodEndDate: string;
|
||||||
|
}) {
|
||||||
|
if (!props.initialPeriod) return;
|
||||||
|
target.periodKey = props.initialPeriod.periodKey;
|
||||||
|
target.periodLabel = props.initialPeriod.periodLabel;
|
||||||
|
target.periodStartDate = props.initialPeriod.periodStartDate;
|
||||||
|
target.periodEndDate = props.initialPeriod.periodEndDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
function patchWeekly(report?: Partial<Api.WorkReport.Weekly.WeeklyReport>) {
|
||||||
|
Object.assign(
|
||||||
|
weeklyModel,
|
||||||
|
createWeeklySaveParams({
|
||||||
|
...report,
|
||||||
|
reviewItems: report?.reviewItems,
|
||||||
|
planItems: report?.planItems,
|
||||||
|
travelSegments: report?.travelSegments
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
if (report && Array.isArray(report.reviewItems) && !report.reviewItems.length) weeklyModel.reviewItems = [];
|
||||||
|
if (report && Array.isArray(report.planItems) && !report.planItems.length) weeklyModel.planItems = [];
|
||||||
|
if (report && Array.isArray(report.travelSegments) && !report.travelSegments.length) weeklyModel.travelSegments = [];
|
||||||
|
|
||||||
|
if (props.mode === 'add') patchPeriod(weeklyModel);
|
||||||
|
}
|
||||||
|
|
||||||
|
function patchMonthly(report?: Partial<Api.WorkReport.Monthly.MonthlyReport>) {
|
||||||
|
Object.assign(
|
||||||
|
monthlyModel,
|
||||||
|
createMonthlySaveParams({
|
||||||
|
...report,
|
||||||
|
reviewItems: report?.reviewItems,
|
||||||
|
planItems: report?.planItems
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
if (report && Array.isArray(report.reviewItems) && !report.reviewItems.length) monthlyModel.reviewItems = [];
|
||||||
|
if (report && Array.isArray(report.planItems) && !report.planItems.length) monthlyModel.planItems = [];
|
||||||
|
|
||||||
|
if (props.mode === 'add') patchPeriod(monthlyModel);
|
||||||
|
patchMonthlyApprovalDefaults(report);
|
||||||
|
}
|
||||||
|
|
||||||
|
function patchProject(report?: Partial<Api.WorkReport.Project.ProjectReport>) {
|
||||||
|
Object.assign(
|
||||||
|
projectModel,
|
||||||
|
createProjectSaveParams({
|
||||||
|
...report,
|
||||||
|
projectId: report?.projectId || props.initialProjectId,
|
||||||
|
flag: report?.flag ?? props.initialFlag,
|
||||||
|
currentItems: report?.currentItems,
|
||||||
|
nextItems: report?.nextItems
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
if (report && Array.isArray(report.currentItems) && !report.currentItems.length) projectModel.currentItems = [];
|
||||||
|
if (report && Array.isArray(report.nextItems) && !report.nextItems.length) projectModel.nextItems = [];
|
||||||
|
|
||||||
|
if (props.mode === 'add') patchPeriod(projectModel);
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyWeeklyEditableFields(draft: Api.WorkReport.Weekly.WeeklyReport) {
|
||||||
|
weeklyModel.isBusinessTrip = draft.isBusinessTrip;
|
||||||
|
weeklyModel.reviewItems = draft.reviewItems?.length ? normalizeReviewItems(draft.reviewItems) : [];
|
||||||
|
weeklyModel.planItems = draft.planItems?.length ? normalizePlanItems(draft.planItems) : [];
|
||||||
|
weeklyModel.travelSegments = draft.travelSegments || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyMonthlyEditableFields(draft: Api.WorkReport.Monthly.MonthlyReport) {
|
||||||
|
monthlyModel.reviewItems = draft.reviewItems?.length ? normalizeReviewItems(draft.reviewItems) : [];
|
||||||
|
monthlyModel.planItems = draft.planItems?.length ? normalizePlanItems(draft.planItems) : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyProjectEditableFields(draft: Api.WorkReport.Project.ProjectReport) {
|
||||||
|
projectModel.projectStatusDesc = draft.projectStatusDesc || '';
|
||||||
|
projectModel.projectProgressPlan = draft.projectProgressPlan || '';
|
||||||
|
projectModel.projectKeyPoints = draft.projectKeyPoints || '';
|
||||||
|
projectModel.projectProblems = draft.projectProblems || '';
|
||||||
|
projectModel.currentItems = draft.currentItems?.length ? normalizeProjectItems(draft.currentItems) : [];
|
||||||
|
projectModel.nextItems = draft.nextItems?.length ? normalizeProjectItems(draft.nextItems) : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function firstMeaningfulValue<T>(...values: Array<T | null | undefined | ''>) {
|
||||||
|
return values.find(value => value !== null && value !== undefined && value !== '') as T | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function firstPositiveWorkHours(...values: Array<string | number | null | undefined>) {
|
||||||
|
const matchedValue = values.find(value => {
|
||||||
|
const numberValue = Number(value ?? 0);
|
||||||
|
return Number.isFinite(numberValue) && numberValue > 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
return matchedValue ?? firstMeaningfulValue(...values);
|
||||||
|
}
|
||||||
|
|
||||||
|
function mergePersonalDetailBaseInfo<
|
||||||
|
T extends Api.WorkReport.Weekly.WeeklyReport | Api.WorkReport.Monthly.MonthlyReport
|
||||||
|
>(detail: T) {
|
||||||
|
const rowData = props.rowData as Partial<T> | null;
|
||||||
|
if (!rowData) return detail;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...rowData,
|
||||||
|
...detail,
|
||||||
|
reporterDeptName: firstMeaningfulValue(detail.reporterDeptName, rowData.reporterDeptName) ?? null,
|
||||||
|
reporterPostName: firstMeaningfulValue(detail.reporterPostName, rowData.reporterPostName) ?? null,
|
||||||
|
submitTime: firstMeaningfulValue(detail.submitTime, rowData.submitTime) ?? null,
|
||||||
|
totalWorkHours: firstPositiveWorkHours(detail.totalWorkHours, rowData.totalWorkHours)
|
||||||
|
} as T;
|
||||||
|
}
|
||||||
|
|
||||||
|
function mergeProjectDetailBaseInfo(detail: Api.WorkReport.Project.ProjectReport) {
|
||||||
|
const rowData = props.rowData as Partial<Api.WorkReport.Project.ProjectReport> | null;
|
||||||
|
if (!rowData) return detail;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...rowData,
|
||||||
|
...detail,
|
||||||
|
submitTime: firstMeaningfulValue(detail.submitTime, rowData.submitTime) ?? null,
|
||||||
|
totalWorkHours: firstPositiveWorkHours(detail.totalWorkHours, rowData.totalWorkHours)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadDetail(id: string) {
|
||||||
|
loading.value = true;
|
||||||
|
let result;
|
||||||
|
|
||||||
|
if (props.reportType === 'weekly') {
|
||||||
|
result = await fetchGetWeeklyReportDetail(id);
|
||||||
|
} else if (props.reportType === 'monthly') {
|
||||||
|
result = await fetchGetMonthlyReportDetail(id);
|
||||||
|
} else {
|
||||||
|
result = await fetchGetProjectReportDetail(id);
|
||||||
|
}
|
||||||
|
loading.value = false;
|
||||||
|
|
||||||
|
if (result.error || !result.data) return;
|
||||||
|
|
||||||
|
const detail =
|
||||||
|
props.reportType === 'weekly'
|
||||||
|
? mergePersonalDetailBaseInfo(result.data as Api.WorkReport.Weekly.WeeklyReport)
|
||||||
|
: props.reportType === 'monthly'
|
||||||
|
? mergePersonalDetailBaseInfo(result.data as Api.WorkReport.Monthly.MonthlyReport)
|
||||||
|
: mergeProjectDetailBaseInfo(result.data as Api.WorkReport.Project.ProjectReport);
|
||||||
|
|
||||||
|
currentReportId.value = detail.id;
|
||||||
|
baseInfo.value = detail;
|
||||||
|
|
||||||
|
if (props.reportType === 'weekly') patchWeekly(detail as Api.WorkReport.Weekly.WeeklyReport);
|
||||||
|
if (props.reportType === 'monthly') patchMonthly(detail as Api.WorkReport.Monthly.MonthlyReport);
|
||||||
|
if (props.reportType === 'project') patchProject(detail as Api.WorkReport.Project.ProjectReport);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function pullDefaultDraft(confirmOverwrite = false) {
|
||||||
|
const period = {
|
||||||
|
periodKey: currentModel.value.periodKey,
|
||||||
|
periodLabel: currentModel.value.periodLabel,
|
||||||
|
periodStartDate: currentModel.value.periodStartDate,
|
||||||
|
periodEndDate: currentModel.value.periodEndDate
|
||||||
|
};
|
||||||
|
|
||||||
|
loading.value = true;
|
||||||
|
let result;
|
||||||
|
|
||||||
|
if (props.reportType === 'weekly') {
|
||||||
|
result = await fetchPreviewWeeklyReportDefaultDraft(period);
|
||||||
|
} else if (props.reportType === 'monthly') {
|
||||||
|
result = await fetchPreviewMonthlyReportDefaultDraft(period);
|
||||||
|
} else {
|
||||||
|
result = await fetchPreviewProjectReportDefaultDraft(projectModel.projectId, {
|
||||||
|
...period,
|
||||||
|
flag: projectModel.flag
|
||||||
|
});
|
||||||
|
}
|
||||||
|
loading.value = false;
|
||||||
|
|
||||||
|
if (result.error || !result.data) return;
|
||||||
|
|
||||||
|
baseInfo.value = {
|
||||||
|
...(baseInfo.value || {}),
|
||||||
|
...result.data
|
||||||
|
} as WorkReportRow;
|
||||||
|
|
||||||
|
if (props.reportType === 'weekly') {
|
||||||
|
const data = result.data as Api.WorkReport.Weekly.WeeklyReport;
|
||||||
|
applyWeeklyEditableFields(data);
|
||||||
|
if (confirmOverwrite) {
|
||||||
|
weeklyModel.travelSegments = data.travelSegments || [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.reportType === 'monthly') {
|
||||||
|
applyMonthlyEditableFields(result.data as Api.WorkReport.Monthly.MonthlyReport);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.reportType === 'project') {
|
||||||
|
applyProjectEditableFields(result.data as Api.WorkReport.Project.ProjectReport);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refreshDraft() {
|
||||||
|
if (props.mode !== 'edit') {
|
||||||
|
await pullDefaultDraft(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
loading.value = true;
|
||||||
|
let result;
|
||||||
|
|
||||||
|
if (props.reportType === 'weekly') {
|
||||||
|
result = await fetchRefreshWeeklyReportDraft(weeklyModel);
|
||||||
|
} else if (props.reportType === 'monthly') {
|
||||||
|
result = await fetchRefreshMonthlyReportDraft(monthlyModel);
|
||||||
|
} else {
|
||||||
|
result = await fetchRefreshProjectReportDraft(projectModel.projectId, {
|
||||||
|
periodKey: projectModel.periodKey,
|
||||||
|
periodLabel: projectModel.periodLabel,
|
||||||
|
periodStartDate: projectModel.periodStartDate,
|
||||||
|
periodEndDate: projectModel.periodEndDate,
|
||||||
|
flag: projectModel.flag,
|
||||||
|
projectStatusDesc: projectModel.projectStatusDesc,
|
||||||
|
projectProgressPlan: projectModel.projectProgressPlan,
|
||||||
|
projectKeyPoints: projectModel.projectKeyPoints,
|
||||||
|
projectProblems: projectModel.projectProblems,
|
||||||
|
currentItems: projectModel.currentItems,
|
||||||
|
nextItems: projectModel.nextItems
|
||||||
|
});
|
||||||
|
}
|
||||||
|
loading.value = false;
|
||||||
|
|
||||||
|
if (result.error || !result.data) return;
|
||||||
|
|
||||||
|
if (props.reportType === 'weekly') {
|
||||||
|
applyWeeklyEditableFields(result.data as Api.WorkReport.Weekly.WeeklyReport);
|
||||||
|
} else if (props.reportType === 'monthly') {
|
||||||
|
applyMonthlyEditableFields(result.data as Api.WorkReport.Monthly.MonthlyReport);
|
||||||
|
} else {
|
||||||
|
applyProjectEditableFields(result.data as Api.WorkReport.Project.ProjectReport);
|
||||||
|
}
|
||||||
|
|
||||||
|
window.$message?.success('最新数据已刷新');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadInitData() {
|
||||||
|
loading.value = true;
|
||||||
|
let result;
|
||||||
|
|
||||||
|
if (props.reportType === 'weekly') {
|
||||||
|
result = await fetchInitWeeklyReport();
|
||||||
|
} else if (props.reportType === 'monthly') {
|
||||||
|
result = await fetchInitMonthlyReport();
|
||||||
|
} else {
|
||||||
|
result = await fetchInitProjectReport(props.initialProjectId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!result.error && result.data) {
|
||||||
|
baseInfo.value = result.data;
|
||||||
|
if (props.reportType === 'weekly') patchWeekly(result.data as Api.WorkReport.Weekly.WeeklyReport);
|
||||||
|
if (props.reportType === 'monthly') patchMonthly(result.data as Api.WorkReport.Monthly.MonthlyReport);
|
||||||
|
if (props.reportType === 'project') patchProject(result.data as Api.WorkReport.Project.ProjectReport);
|
||||||
|
}
|
||||||
|
|
||||||
|
loading.value = false;
|
||||||
|
await pullDefaultDraft();
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(visible, async isVisible => {
|
||||||
|
if (!isVisible) return;
|
||||||
|
|
||||||
|
currentStage.value = props.reportType === 'monthly' && props.scene === 'approval' ? 'approval' : 'form';
|
||||||
|
currentReportId.value = props.rowData?.id || '';
|
||||||
|
baseInfo.value = null;
|
||||||
|
resetModels();
|
||||||
|
|
||||||
|
if (props.mode === 'add') {
|
||||||
|
if (props.reportType === 'project') {
|
||||||
|
projectModel.projectId = props.initialProjectId;
|
||||||
|
projectModel.flag = props.initialFlag;
|
||||||
|
}
|
||||||
|
patchPeriod(currentModel.value);
|
||||||
|
await loadInitData();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.rowData?.id) {
|
||||||
|
await loadDetail(props.rowData.id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function hasTextValue(value: unknown) {
|
||||||
|
const text = String(value ?? '')
|
||||||
|
.replace(/<[^>]*>/g, '')
|
||||||
|
.replace(/ /g, ' ')
|
||||||
|
.trim();
|
||||||
|
|
||||||
|
return text.length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasMeaningfulStructuredValue(value: unknown): boolean {
|
||||||
|
if (value === null || value === undefined) return false;
|
||||||
|
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
const text = value.trim();
|
||||||
|
if (!text || text === '{}' || text === '[]') return false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
return hasMeaningfulStructuredValue(JSON.parse(text));
|
||||||
|
} catch {
|
||||||
|
return hasTextValue(text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
return value.some(item => hasMeaningfulStructuredValue(item));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof value === 'object') {
|
||||||
|
return Object.values(value as Record<string, unknown>).some(item => hasMeaningfulStructuredValue(item));
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasReviewContent(item: Api.WorkReport.Common.PersonalReportReviewItem) {
|
||||||
|
return hasTextValue(item.contentText) || hasMeaningfulStructuredValue(item.contentJson);
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasPlanTarget(item: Api.WorkReport.Common.PersonalReportPlanItem) {
|
||||||
|
return hasTextValue(item.targetText) || hasMeaningfulStructuredValue(item.targetJson);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isCompleteWeeklyTravelSegment(segment: Api.WorkReport.Weekly.WeeklyReportTravelSegment) {
|
||||||
|
const travelDays = Number(segment.travelDays);
|
||||||
|
|
||||||
|
return Boolean(
|
||||||
|
segment.startDate &&
|
||||||
|
segment.endDate &&
|
||||||
|
Number.isFinite(travelDays) &&
|
||||||
|
travelDays >= 0.5 &&
|
||||||
|
Number.isInteger(travelDays * 2)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasCompleteWeeklyTravelSegment(items: Api.WorkReport.Weekly.WeeklyReportTravelSegment[]) {
|
||||||
|
return items.some(isCompleteWeeklyTravelSegment);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 周报"具体工作内容及成果描述"中已记录出差信息("本周差旅"分类、或含"差旅"分类、
|
||||||
|
* 或结构化任务里带 kind=travel)时,视为已经有完整出差分段,不再强制弹出
|
||||||
|
* "请至少新增一条完整的出差分段"。避免出差点位等历史录入原因导致保存时反复提示。
|
||||||
|
*/
|
||||||
|
function hasTravelInfoInWeeklyReview(items: Api.WorkReport.Common.PersonalReportReviewItem[]) {
|
||||||
|
return items.some(item => {
|
||||||
|
if (item.itemTitle?.trim() === '本周差旅') return true;
|
||||||
|
const sections = getStructuredSections(item.contentJson);
|
||||||
|
if (sections.some(section => section.category?.trim() === '差旅')) return true;
|
||||||
|
if (sections.some(section => section.tasks.some(task => task.kind === 'travel'))) return true;
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasCompletePersonalReviewItem(items: Api.WorkReport.Common.PersonalReportReviewItem[]) {
|
||||||
|
return items.some(item => hasTextValue(item.itemTitle) && hasReviewContent(item));
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasCompletePersonalPlanItem(items: Api.WorkReport.Common.PersonalReportPlanItem[]) {
|
||||||
|
return items.some(item => hasTextValue(item.itemTitle) && hasPlanTarget(item));
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPersonalReviewValidationMessage(label: string, items: Api.WorkReport.Common.PersonalReportReviewItem[]) {
|
||||||
|
if (!items.length) return `至少要有一项${label}`;
|
||||||
|
if (!hasCompletePersonalReviewItem(items)) {
|
||||||
|
return `请完善${label},项目名或我的事项、具体工作内容及成果描述不能为空`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasIncompleteItem = items.some(item => !hasTextValue(item.itemTitle) || !hasReviewContent(item));
|
||||||
|
return hasIncompleteItem ? `请完善${label},项目名或我的事项、具体工作内容及成果描述不能为空` : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPersonalPlanValidationMessage(label: string, items: Api.WorkReport.Common.PersonalReportPlanItem[]) {
|
||||||
|
if (!items.length) return `至少要有一项${label}`;
|
||||||
|
if (!hasCompletePersonalPlanItem(items)) {
|
||||||
|
return `请完善${label},项目名或我的事项、具体目标不能为空`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasIncompleteItem = items.some(item => !hasTextValue(item.itemTitle) || !hasPlanTarget(item));
|
||||||
|
return hasIncompleteItem ? `请完善${label},项目名或我的事项、具体目标不能为空` : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasProjectItem(items: Api.WorkReport.Project.ProjectReportItem[]) {
|
||||||
|
return items.some(item => hasTextValue(item.itemTitle));
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateRequiredReportItems() {
|
||||||
|
const messages: string[] = [];
|
||||||
|
|
||||||
|
if (props.reportType === 'weekly') {
|
||||||
|
// 出差信息既可能来自 travelSegments,也可能来自具体工作内容及成果描述里的"差旅"段,
|
||||||
|
// 任何一方已有完整出差信息就不再提示"请至少新增一条完整的出差分段"。
|
||||||
|
const hasTravelReview =
|
||||||
|
weeklyModel.isBusinessTrip &&
|
||||||
|
(hasCompleteWeeklyTravelSegment(weeklyModel.travelSegments) ||
|
||||||
|
hasTravelInfoInWeeklyReview(weeklyModel.reviewItems));
|
||||||
|
const reviewMessage = hasTravelReview
|
||||||
|
? weeklyModel.reviewItems.length
|
||||||
|
? getPersonalReviewValidationMessage('当期重点工作回顾', weeklyModel.reviewItems)
|
||||||
|
: ''
|
||||||
|
: getPersonalReviewValidationMessage('当期重点工作回顾', weeklyModel.reviewItems);
|
||||||
|
const planMessage = getPersonalPlanValidationMessage('下周期重点工作计划', weeklyModel.planItems);
|
||||||
|
if (weeklyModel.isBusinessTrip && !hasTravelReview) messages.push('请至少新增一条完整的出差分段');
|
||||||
|
if (reviewMessage) messages.push(reviewMessage);
|
||||||
|
if (planMessage) messages.push(planMessage);
|
||||||
|
} else if (props.reportType === 'monthly') {
|
||||||
|
const reviewMessage = getPersonalReviewValidationMessage('当期重点工作回顾', monthlyModel.reviewItems);
|
||||||
|
const planMessage = getPersonalPlanValidationMessage('下周期重点工作计划', monthlyModel.planItems);
|
||||||
|
if (reviewMessage) messages.push(reviewMessage);
|
||||||
|
if (planMessage) messages.push(planMessage);
|
||||||
|
} else {
|
||||||
|
const missingLabels: string[] = [];
|
||||||
|
|
||||||
|
if (!hasProjectItem(projectModel.currentItems)) {
|
||||||
|
missingLabels.push('本期工作内容');
|
||||||
|
}
|
||||||
|
if (!hasProjectItem(projectModel.nextItems)) {
|
||||||
|
missingLabels.push('下期计划工作内容');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (missingLabels.length) messages.push(`至少要有一项${missingLabels.join('、')}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!messages.length) return true;
|
||||||
|
|
||||||
|
window.$message?.warning(messages.join(';'));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function persistReport(submitAfterSave: boolean) {
|
||||||
|
if (!validateRequiredReportItems()) return false;
|
||||||
|
|
||||||
|
let result;
|
||||||
|
|
||||||
|
if (props.reportType === 'weekly') {
|
||||||
|
if (currentReportId.value) {
|
||||||
|
result = await fetchUpdateWeeklyReport(currentReportId.value, weeklyModel);
|
||||||
|
} else {
|
||||||
|
result = await fetchCreateWeeklyReport(weeklyModel);
|
||||||
|
if (!result.error && result.data) currentReportId.value = result.data;
|
||||||
|
}
|
||||||
|
} else if (props.reportType === 'monthly') {
|
||||||
|
if (currentReportId.value) {
|
||||||
|
result = await fetchUpdateMonthlyReport(currentReportId.value, monthlyModel);
|
||||||
|
} else {
|
||||||
|
result = await fetchCreateMonthlyReport(monthlyModel);
|
||||||
|
if (!result.error && result.data) currentReportId.value = result.data;
|
||||||
|
}
|
||||||
|
} else if (currentReportId.value) {
|
||||||
|
result = await fetchUpdateProjectReport(currentReportId.value, projectModel);
|
||||||
|
} else {
|
||||||
|
result = await fetchCreateProjectReport(projectModel);
|
||||||
|
if (!result.error && result.data) currentReportId.value = result.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.error) return false;
|
||||||
|
|
||||||
|
if (submitAfterSave) {
|
||||||
|
if (props.reportType === 'weekly') {
|
||||||
|
const submitResult = await fetchSubmitWeeklyReport(currentReportId.value);
|
||||||
|
if (submitResult.error) return false;
|
||||||
|
} else if (props.reportType === 'monthly') {
|
||||||
|
const submitResult = await fetchSubmitMonthlyReport(currentReportId.value);
|
||||||
|
if (submitResult.error) return false;
|
||||||
|
} else {
|
||||||
|
const submitResult = await fetchSubmitProjectReport(currentReportId.value);
|
||||||
|
if (submitResult.error) return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSaveDraft() {
|
||||||
|
loading.value = true;
|
||||||
|
const success = await persistReport(false);
|
||||||
|
loading.value = false;
|
||||||
|
|
||||||
|
if (!success) return;
|
||||||
|
|
||||||
|
window.$message?.success('工作报告已保存');
|
||||||
|
visible.value = false;
|
||||||
|
emit('submitted');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSubmitReport() {
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm('确认提交当前工作报告吗?', '提交确认', {
|
||||||
|
type: 'warning',
|
||||||
|
confirmButtonText: '确认提交',
|
||||||
|
cancelButtonText: '取消'
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
loading.value = true;
|
||||||
|
const success = await persistReport(true);
|
||||||
|
loading.value = false;
|
||||||
|
|
||||||
|
if (!success) return;
|
||||||
|
|
||||||
|
window.$message?.success('工作报告已提交');
|
||||||
|
visible.value = false;
|
||||||
|
emit('submitted');
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleBack() {
|
||||||
|
visible.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleViewApproval() {
|
||||||
|
currentStage.value = 'approval';
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleRequestApprove() {
|
||||||
|
if (props.reportType === 'monthly') {
|
||||||
|
handleActionSubmit({ ...monthlyApprovalDraft }, 'approve');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
currentActionType.value = 'approve';
|
||||||
|
actionVisible.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleRequestReject() {
|
||||||
|
if (props.reportType === 'monthly') {
|
||||||
|
handleActionSubmit({ reason: monthlyApprovalDraft.reason }, 'reject');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
currentActionType.value = 'reject';
|
||||||
|
actionVisible.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handlePullDefaultDraft() {
|
||||||
|
refreshDraft();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleMonthlyApprovalChange(payload: Api.WorkReport.Monthly.MonthlyReportApproveParams) {
|
||||||
|
Object.assign(monthlyApprovalDraft, payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleActionSubmit(
|
||||||
|
payload: Api.WorkReport.Common.StatusActionParams | Api.WorkReport.Monthly.MonthlyReportApproveParams,
|
||||||
|
actionTypeOverride?: 'approve' | 'reject'
|
||||||
|
) {
|
||||||
|
if (!currentReportId.value) return;
|
||||||
|
|
||||||
|
const actionType = actionTypeOverride || currentActionType.value;
|
||||||
|
actionSubmitting.value = true;
|
||||||
|
let result;
|
||||||
|
|
||||||
|
if (props.reportType === 'weekly') {
|
||||||
|
result =
|
||||||
|
actionType === 'approve'
|
||||||
|
? await fetchApproveWeeklyReport(currentReportId.value, payload)
|
||||||
|
: await fetchRejectWeeklyReport(currentReportId.value, payload);
|
||||||
|
} else if (props.reportType === 'monthly') {
|
||||||
|
result =
|
||||||
|
actionType === 'approve'
|
||||||
|
? await fetchApproveMonthlyReport(
|
||||||
|
currentReportId.value,
|
||||||
|
payload as Api.WorkReport.Monthly.MonthlyReportApproveParams
|
||||||
|
)
|
||||||
|
: await fetchRejectMonthlyReport(currentReportId.value, payload);
|
||||||
|
} else {
|
||||||
|
result =
|
||||||
|
actionType === 'approve'
|
||||||
|
? await fetchApproveProjectReport(currentReportId.value, payload)
|
||||||
|
: await fetchRejectProjectReport(currentReportId.value, payload);
|
||||||
|
}
|
||||||
|
actionSubmitting.value = false;
|
||||||
|
|
||||||
|
if (result.error) return;
|
||||||
|
|
||||||
|
actionVisible.value = false;
|
||||||
|
window.$message?.success(actionType === 'approve' ? '审批已通过' : '工作报告已退回');
|
||||||
|
visible.value = false;
|
||||||
|
emit('submitted');
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<WorkReportPageDialog
|
||||||
|
v-model:visible="visible"
|
||||||
|
:title="dialogTitle"
|
||||||
|
:loading="loading"
|
||||||
|
:approval-mode="currentScene === 'approval'"
|
||||||
|
@close="currentStage = 'form'"
|
||||||
|
>
|
||||||
|
<WeeklyReportPage
|
||||||
|
v-if="reportType === 'weekly'"
|
||||||
|
:report-type="REPORT_TYPE_TEXT[reportType]"
|
||||||
|
:period="periodText"
|
||||||
|
:mode="mode"
|
||||||
|
:scene="currentScene"
|
||||||
|
:base-info="baseInfo as Api.WorkReport.Weekly.WeeklyReport | null"
|
||||||
|
:model="weeklyModel"
|
||||||
|
@back="handleBack"
|
||||||
|
@save="handleSaveDraft"
|
||||||
|
@submit="handleSubmitReport"
|
||||||
|
@request-approve="handleRequestApprove"
|
||||||
|
@request-reject="handleRequestReject"
|
||||||
|
@pull-default-draft="handlePullDefaultDraft"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<MonthlyReportApprovalPage
|
||||||
|
v-else-if="reportType === 'monthly' && currentStage === 'approval'"
|
||||||
|
:report-type="REPORT_TYPE_TEXT[reportType]"
|
||||||
|
:period="periodText"
|
||||||
|
:mode="mode"
|
||||||
|
scene="approval"
|
||||||
|
:base-info="baseInfo as Api.WorkReport.Monthly.MonthlyReport | null"
|
||||||
|
:model="monthlyModel"
|
||||||
|
:approval-model="monthlyApprovalDraft"
|
||||||
|
@back="handleBack"
|
||||||
|
@change-approval="handleMonthlyApprovalChange"
|
||||||
|
@request-approve="handleRequestApprove"
|
||||||
|
@request-reject="handleRequestReject"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<MonthlyReportPage
|
||||||
|
v-else-if="reportType === 'monthly'"
|
||||||
|
:report-type="REPORT_TYPE_TEXT[reportType]"
|
||||||
|
:period="periodText"
|
||||||
|
:mode="mode"
|
||||||
|
:scene="currentScene"
|
||||||
|
:base-info="baseInfo as Api.WorkReport.Monthly.MonthlyReport | null"
|
||||||
|
:model="monthlyModel"
|
||||||
|
@back="handleBack"
|
||||||
|
@save="handleSaveDraft"
|
||||||
|
@submit="handleSubmitReport"
|
||||||
|
@view-approval="handleViewApproval"
|
||||||
|
@pull-default-draft="handlePullDefaultDraft"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ProjectReportPage
|
||||||
|
v-else
|
||||||
|
:report-type="REPORT_TYPE_TEXT[reportType]"
|
||||||
|
:period="periodText"
|
||||||
|
:mode="mode"
|
||||||
|
:scene="currentScene"
|
||||||
|
:base-info="baseInfo as Api.WorkReport.Project.ProjectReport | null"
|
||||||
|
:model="projectModel"
|
||||||
|
@back="handleBack"
|
||||||
|
@save="handleSaveDraft"
|
||||||
|
@submit="handleSubmitReport"
|
||||||
|
@request-approve="handleRequestApprove"
|
||||||
|
@request-reject="handleRequestReject"
|
||||||
|
@pull-default-draft="handlePullDefaultDraft"
|
||||||
|
/>
|
||||||
|
</WorkReportPageDialog>
|
||||||
|
|
||||||
|
<WorkReportActionDialog
|
||||||
|
v-model:visible="actionVisible"
|
||||||
|
:report-type="reportType"
|
||||||
|
:action-type="currentActionType"
|
||||||
|
:initial-monthly-approve-data="reportType === 'monthly' ? monthlyApprovalDraft : null"
|
||||||
|
:loading="actionSubmitting"
|
||||||
|
append-to-body
|
||||||
|
@submit="handleActionSubmit"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
/* eslint-disable no-void */
|
||||||
|
import { computed, onMounted, ref } from 'vue';
|
||||||
|
import { fetchGetWorkReportStatusDict } from '@/service/api';
|
||||||
|
import type { SearchField } from '@/components/custom/table-search-fields.vue';
|
||||||
|
import TableSearchFields from '@/components/custom/table-search-fields.vue';
|
||||||
|
import { BOOLEAN_TRUE_FALSE_OPTIONS, type WorkReportSearchParams, type WorkReportType } from '../types';
|
||||||
|
|
||||||
|
defineOptions({ name: 'WorkReportSearch' });
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
reportType: WorkReportType;
|
||||||
|
projectOptions?: Api.WorkReport.Project.ProjectReportOwnerProjectOption[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
projectOptions: () => []
|
||||||
|
});
|
||||||
|
|
||||||
|
const model = defineModel<WorkReportSearchParams>('model', { required: true });
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'reset'): void;
|
||||||
|
(e: 'search'): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const statusDict = ref<Api.WorkReport.Common.WorkReportStatusDict[]>([]);
|
||||||
|
|
||||||
|
const statusOptions = computed(() =>
|
||||||
|
[...statusDict.value]
|
||||||
|
.sort((a, b) => Number(a.sort || 0) - Number(b.sort || 0))
|
||||||
|
.map(item => ({
|
||||||
|
label: item.statusName || item.statusCode,
|
||||||
|
value: item.statusCode
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
|
||||||
|
const fields = computed<SearchField[]>(() => {
|
||||||
|
const baseFields: SearchField[] = [
|
||||||
|
{ key: 'statusCode', label: '状态', type: 'select', options: statusOptions.value, placeholder: '请选择状态' },
|
||||||
|
{ key: 'periodStartDate', label: '周期', type: 'dateRange', placeholder: '请选择周期' }
|
||||||
|
];
|
||||||
|
|
||||||
|
const monthPeriodField: SearchField = {
|
||||||
|
key: 'periodStartDate',
|
||||||
|
label: props.reportType === 'project' ? '月份' : '月份',
|
||||||
|
type: 'dateRange',
|
||||||
|
dateRangeType: 'monthrange',
|
||||||
|
valueFormat: 'YYYY-MM-DD',
|
||||||
|
placeholder: '请选择月份'
|
||||||
|
};
|
||||||
|
|
||||||
|
if (props.reportType === 'weekly') {
|
||||||
|
return [
|
||||||
|
...baseFields,
|
||||||
|
{
|
||||||
|
key: 'isBusinessTrip',
|
||||||
|
label: '是否出差',
|
||||||
|
type: 'select',
|
||||||
|
options: BOOLEAN_TRUE_FALSE_OPTIONS,
|
||||||
|
placeholder: '请选择'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.reportType === 'project') {
|
||||||
|
return [
|
||||||
|
baseFields[0],
|
||||||
|
monthPeriodField,
|
||||||
|
{
|
||||||
|
key: 'projectId',
|
||||||
|
label: '项目',
|
||||||
|
type: 'select',
|
||||||
|
options: props.projectOptions.map(item => ({
|
||||||
|
label: item.projectCode ? `${item.projectName}(${item.projectCode})` : item.projectName,
|
||||||
|
value: item.id
|
||||||
|
})),
|
||||||
|
placeholder: '请选择项目'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.reportType === 'monthly') {
|
||||||
|
return [baseFields[0], monthPeriodField];
|
||||||
|
}
|
||||||
|
|
||||||
|
return baseFields;
|
||||||
|
});
|
||||||
|
|
||||||
|
async function loadStatusDict() {
|
||||||
|
const { error, data } = await fetchGetWorkReportStatusDict();
|
||||||
|
statusDict.value = error || !data ? [] : data;
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
loadStatusDict();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<TableSearchFields v-model="model" :columns="4" :fields="fields" @reset="emit('reset')" @search="emit('search')" />
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { WORK_REPORT_TYPE_LABEL, type WorkReportType } from '../types';
|
||||||
|
|
||||||
|
defineOptions({ name: 'WorkReportTabs' });
|
||||||
|
|
||||||
|
interface TabOption {
|
||||||
|
label: string;
|
||||||
|
name: WorkReportType;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
tabs: TabOption[];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const activeTab = defineModel<WorkReportType>('activeTab', { required: true });
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<ElCard class="work-report-sidebar" body-class="work-report-sidebar__body">
|
||||||
|
<div class="work-report-sidebar__header">报告类型</div>
|
||||||
|
<div class="work-report-sidebar__list">
|
||||||
|
<div
|
||||||
|
v-for="tab in tabs"
|
||||||
|
:key="tab.name"
|
||||||
|
class="work-report-sidebar__item"
|
||||||
|
:class="{ 'work-report-sidebar__item--active': activeTab === tab.name }"
|
||||||
|
@click="activeTab = tab.name"
|
||||||
|
>
|
||||||
|
<span class="work-report-sidebar__label">{{ tab.label || WORK_REPORT_TYPE_LABEL[tab.name] }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ElCard>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.work-report-sidebar {
|
||||||
|
height: 100%;
|
||||||
|
border: 1px solid var(--el-border-color-light);
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.work-report-sidebar :deep(.work-report-sidebar__body) {
|
||||||
|
padding: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.work-report-sidebar__header {
|
||||||
|
padding: 20px 20px 12px;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.work-report-sidebar__list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
padding: 0 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.work-report-sidebar__item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 12px 16px;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.work-report-sidebar__item:hover {
|
||||||
|
background: var(--el-fill-color-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.work-report-sidebar__item--active {
|
||||||
|
background: var(--el-color-primary-light-9);
|
||||||
|
}
|
||||||
|
|
||||||
|
.work-report-sidebar__item--active .work-report-sidebar__label {
|
||||||
|
color: var(--el-color-primary);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.work-report-sidebar__label {
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--el-text-color-regular);
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
558
src/views/personal-center/work-report/shared/types.ts
Normal file
558
src/views/personal-center/work-report/shared/types.ts
Normal file
@@ -0,0 +1,558 @@
|
|||||||
|
import dayjs from 'dayjs';
|
||||||
|
import isoWeek from 'dayjs/plugin/isoWeek';
|
||||||
|
import type { PaginationData } from '@sa/hooks';
|
||||||
|
import { getStatusTagType } from '@/constants/status-tag';
|
||||||
|
|
||||||
|
dayjs.extend(isoWeek);
|
||||||
|
|
||||||
|
export type WorkReportType = Api.WorkReport.Common.ReportType;
|
||||||
|
export type WorkReportRow =
|
||||||
|
| Api.WorkReport.Weekly.WeeklyReport
|
||||||
|
| Api.WorkReport.Monthly.MonthlyReport
|
||||||
|
| Api.WorkReport.Project.ProjectReport;
|
||||||
|
export type WorkReportSearchParams =
|
||||||
|
| Api.WorkReport.Weekly.WeeklyReportSearchParams
|
||||||
|
| Api.WorkReport.Monthly.MonthlyReportSearchParams
|
||||||
|
| Api.WorkReport.Project.ProjectReportSearchParams;
|
||||||
|
export type WorkReportSaveParams =
|
||||||
|
| Api.WorkReport.Weekly.WeeklyReportSaveParams
|
||||||
|
| Api.WorkReport.Monthly.MonthlyReportSaveParams
|
||||||
|
| Api.WorkReport.Project.ProjectReportSaveParams;
|
||||||
|
|
||||||
|
export interface WorkReportStructuredTask {
|
||||||
|
title: string;
|
||||||
|
detail?: string;
|
||||||
|
priority?: string | null;
|
||||||
|
progress?: number | null;
|
||||||
|
hours?: number | null;
|
||||||
|
kind?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WorkReportStructuredSection {
|
||||||
|
category: string;
|
||||||
|
tasks: WorkReportStructuredTask[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const WORK_REPORT_PROJECT_OWNER_PERMISSION = 'project:work-report:project-owner';
|
||||||
|
|
||||||
|
export const WORK_REPORT_TYPE_LABEL: Record<WorkReportType, string> = {
|
||||||
|
weekly: '个人周报',
|
||||||
|
monthly: '个人月报',
|
||||||
|
project: '项目半月报'
|
||||||
|
};
|
||||||
|
|
||||||
|
export const WORK_REPORT_STATUS_LABEL: Record<string, string> = {
|
||||||
|
draft: '待提交',
|
||||||
|
pending_approval: '待审批',
|
||||||
|
approved: '已通过',
|
||||||
|
rejected: '已退回'
|
||||||
|
};
|
||||||
|
|
||||||
|
export const PROJECT_REPORT_FLAG_OPTIONS = [
|
||||||
|
{ label: '上半月', value: 1 },
|
||||||
|
{ label: '下半月', value: 2 }
|
||||||
|
];
|
||||||
|
|
||||||
|
export const BOOLEAN_TRUE_FALSE_OPTIONS = [
|
||||||
|
{ label: '是', value: 'true' },
|
||||||
|
{ label: '否', value: 'false' }
|
||||||
|
];
|
||||||
|
|
||||||
|
export function getProjectReportFlagLabel(flag?: number | null) {
|
||||||
|
return PROJECT_REPORT_FLAG_OPTIONS.find(item => item.value === flag)?.label || '--';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getWorkReportStatusLabel(statusCode?: string | null, statusName?: string | null) {
|
||||||
|
return statusName || WORK_REPORT_STATUS_LABEL[statusCode || ''] || statusCode || '--';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveWorkReportStatusTagType(statusCode?: string | null) {
|
||||||
|
return getStatusTagType('workReport', statusCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatEmptyText(value?: string | number | null) {
|
||||||
|
if (value === null || value === undefined || value === '') {
|
||||||
|
return '0';
|
||||||
|
}
|
||||||
|
|
||||||
|
return String(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatDate(value?: string | null) {
|
||||||
|
return value ? dayjs(value).format('YYYY-MM-DD') : '--';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatDateTime(value?: string | null) {
|
||||||
|
return value ? dayjs(value).format('YYYY-MM-DD HH:mm') : '--';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatPeriodDateRange(
|
||||||
|
rowOrStart: Pick<WorkReportRow, 'periodStartDate' | 'periodEndDate'> | string | null | undefined,
|
||||||
|
endDate?: string | null
|
||||||
|
) {
|
||||||
|
const startDate = typeof rowOrStart === 'object' && rowOrStart ? rowOrStart.periodStartDate : rowOrStart;
|
||||||
|
const rangeEndDate = typeof rowOrStart === 'object' && rowOrStart ? rowOrStart.periodEndDate : endDate;
|
||||||
|
const startText = formatDate(startDate);
|
||||||
|
const endText = formatDate(rangeEndDate);
|
||||||
|
|
||||||
|
if (startText === '--' && endText === '--') {
|
||||||
|
return '--';
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${startText} 至 ${endText}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatWeeklyPeriodLabel(
|
||||||
|
rowOrStart: Pick<WorkReportRow, 'periodLabel' | 'periodStartDate' | 'periodEndDate'> | string | null | undefined,
|
||||||
|
endDate?: string | null,
|
||||||
|
periodLabel?: string | null
|
||||||
|
) {
|
||||||
|
const startDate = typeof rowOrStart === 'object' && rowOrStart ? rowOrStart.periodStartDate : rowOrStart;
|
||||||
|
const rangeEndDate = typeof rowOrStart === 'object' && rowOrStart ? rowOrStart.periodEndDate : endDate;
|
||||||
|
const fallbackLabel = typeof rowOrStart === 'object' && rowOrStart ? rowOrStart.periodLabel : periodLabel;
|
||||||
|
const referenceDate = dayjs(startDate || rangeEndDate);
|
||||||
|
|
||||||
|
if (referenceDate.isValid()) {
|
||||||
|
const weekYear = referenceDate.startOf('isoWeek').add(3, 'day').format('YYYY');
|
||||||
|
return `${weekYear}年第${String(referenceDate.isoWeek()).padStart(2, '0')}周`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return formatPeriodLabel(fallbackLabel) || formatPeriodDateRange(startDate, rangeEndDate);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatPeriodLabel(value?: string | null) {
|
||||||
|
return String(value || '')
|
||||||
|
.trim()
|
||||||
|
.replace(/\s*(周报|月报|项目半月报|半月报)\s*$/u, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatPeriod(row: Pick<WorkReportRow, 'periodLabel' | 'periodStartDate' | 'periodEndDate'>) {
|
||||||
|
return formatPeriodLabel(row.periodLabel) || `${formatDate(row.periodStartDate)} 至 ${formatDate(row.periodEndDate)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createInitBaseSearchParams() {
|
||||||
|
return {
|
||||||
|
pageNo: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
keyword: undefined,
|
||||||
|
statusCode: undefined,
|
||||||
|
periodStartDate: undefined,
|
||||||
|
submitTime: undefined,
|
||||||
|
supervisorName: undefined
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createWeeklySearchParams(): Api.WorkReport.Weekly.WeeklyReportSearchParams {
|
||||||
|
return {
|
||||||
|
...createInitBaseSearchParams(),
|
||||||
|
isBusinessTrip: undefined
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createMonthlySearchParams(): Api.WorkReport.Monthly.MonthlyReportSearchParams {
|
||||||
|
return createInitBaseSearchParams();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createProjectSearchParams(): Api.WorkReport.Project.ProjectReportSearchParams {
|
||||||
|
return {
|
||||||
|
...createInitBaseSearchParams(),
|
||||||
|
projectId: undefined,
|
||||||
|
flag: undefined
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function transformWorkReportPage<T>(
|
||||||
|
response: { data: Api.WorkReport.Common.PageResult<T> | null; error: unknown },
|
||||||
|
pageNo: number,
|
||||||
|
pageSize: number
|
||||||
|
): PaginationData<T> {
|
||||||
|
if (!response.error && response.data) {
|
||||||
|
return {
|
||||||
|
data: response.data.list,
|
||||||
|
pageNum: pageNo,
|
||||||
|
pageSize,
|
||||||
|
total: response.data.total
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: [],
|
||||||
|
pageNum: 1,
|
||||||
|
pageSize,
|
||||||
|
total: 0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createBlankReviewItem(index = 0): Api.WorkReport.Common.PersonalReportReviewItem {
|
||||||
|
return {
|
||||||
|
itemNumber: index + 1,
|
||||||
|
itemTitle: '',
|
||||||
|
workHours: 0,
|
||||||
|
contentText: '',
|
||||||
|
contentJson: null,
|
||||||
|
reflectionText: ''
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createBlankPlanItem(index = 0): Api.WorkReport.Common.PersonalReportPlanItem {
|
||||||
|
return {
|
||||||
|
itemNumber: index + 1,
|
||||||
|
itemTitle: '',
|
||||||
|
targetText: '',
|
||||||
|
targetJson: null,
|
||||||
|
supportNeed: ''
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createBlankProjectItem(): Api.WorkReport.Project.ProjectReportItem {
|
||||||
|
return {
|
||||||
|
itemTitle: '',
|
||||||
|
workHours: 0,
|
||||||
|
priorityCode: undefined,
|
||||||
|
progressRate: 0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||||
|
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeNumber(value: unknown): number | null {
|
||||||
|
if (typeof value === 'number' && Number.isFinite(value)) return value;
|
||||||
|
if (typeof value === 'string' && value.trim()) {
|
||||||
|
const numberValue = Number(value);
|
||||||
|
return Number.isFinite(numberValue) ? numberValue : null;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeStructuredTask(value: unknown): WorkReportStructuredTask | null {
|
||||||
|
if (!isRecord(value)) return null;
|
||||||
|
|
||||||
|
const title = String(value.title ?? value.name ?? '').trim();
|
||||||
|
if (!title) return null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
title,
|
||||||
|
detail: String(value.detail ?? value.content ?? '').trim(),
|
||||||
|
priority: value.priority === null || value.priority === undefined ? null : String(value.priority),
|
||||||
|
progress: normalizeNumber(value.progress),
|
||||||
|
hours: normalizeNumber(value.hours),
|
||||||
|
kind: value.kind === null || value.kind === undefined ? null : String(value.kind)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeStructuredSection(value: unknown): WorkReportStructuredSection | null {
|
||||||
|
if (!isRecord(value)) return null;
|
||||||
|
|
||||||
|
const category = String(value.category ?? value.title ?? value.name ?? '').trim();
|
||||||
|
const rawTasks = Array.isArray(value.tasks) ? value.tasks : [];
|
||||||
|
const tasks = rawTasks.map(normalizeStructuredTask).filter(Boolean) as WorkReportStructuredTask[];
|
||||||
|
|
||||||
|
if (!category && !tasks.length) return null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
category: category || '工作内容',
|
||||||
|
tasks
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseJsonLike(value: unknown): unknown {
|
||||||
|
if (typeof value !== 'string') return value;
|
||||||
|
|
||||||
|
try {
|
||||||
|
return JSON.parse(value);
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getStructuredTasks(value: unknown): WorkReportStructuredTask[] {
|
||||||
|
const parsed = parseJsonLike(value);
|
||||||
|
|
||||||
|
if (Array.isArray(parsed)) {
|
||||||
|
return parsed.map(normalizeStructuredTask).filter(Boolean) as WorkReportStructuredTask[];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isRecord(parsed) && Array.isArray(parsed.tasks)) {
|
||||||
|
return parsed.tasks.map(normalizeStructuredTask).filter(Boolean) as WorkReportStructuredTask[];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getStructuredSections(value: unknown): WorkReportStructuredSection[] {
|
||||||
|
const parsed = parseJsonLike(value);
|
||||||
|
|
||||||
|
if (Array.isArray(parsed)) {
|
||||||
|
return parsed.map(normalizeStructuredSection).filter(Boolean) as WorkReportStructuredSection[];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isRecord(parsed) && Array.isArray(parsed.sections)) {
|
||||||
|
return parsed.sections.map(normalizeStructuredSection).filter(Boolean) as WorkReportStructuredSection[];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function joinTextValues(...values: Array<string | null | undefined>) {
|
||||||
|
return values
|
||||||
|
.map(value => value?.trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
.join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
function mergeStructuredSections(sections: WorkReportStructuredSection[]) {
|
||||||
|
const sectionMap = new Map<string, WorkReportStructuredSection>();
|
||||||
|
|
||||||
|
sections.forEach(section => {
|
||||||
|
const category = section.category || '工作内容';
|
||||||
|
const existing = sectionMap.get(category);
|
||||||
|
if (existing) {
|
||||||
|
existing.tasks.push(...section.tasks);
|
||||||
|
} else {
|
||||||
|
sectionMap.set(category, { category, tasks: [...section.tasks] });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return Array.from(sectionMap.values());
|
||||||
|
}
|
||||||
|
|
||||||
|
function mergeStructuredJson(current: unknown, next: unknown) {
|
||||||
|
const currentSections = getStructuredSections(current);
|
||||||
|
const nextSections = getStructuredSections(next);
|
||||||
|
const sections = mergeStructuredSections([...currentSections, ...nextSections]);
|
||||||
|
if (sections.length) return JSON.stringify({ sections });
|
||||||
|
|
||||||
|
const tasks = [...getStructuredTasks(current), ...getStructuredTasks(next)];
|
||||||
|
if (tasks.length) return JSON.stringify({ tasks });
|
||||||
|
|
||||||
|
return current ?? next ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function groupPersonalReportItemsByTitle<T extends { itemTitle: string }>(
|
||||||
|
items: T[],
|
||||||
|
merge: (target: T, source: T) => void
|
||||||
|
) {
|
||||||
|
const groupedItems: T[] = [];
|
||||||
|
const itemMap = new Map<string, T>();
|
||||||
|
|
||||||
|
items.forEach((item, index) => {
|
||||||
|
const title = item.itemTitle.trim();
|
||||||
|
const key = title || `__blank_${index}`;
|
||||||
|
const existing = itemMap.get(key);
|
||||||
|
|
||||||
|
if (existing) {
|
||||||
|
merge(existing, item);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
itemMap.set(key, item);
|
||||||
|
groupedItems.push(item);
|
||||||
|
});
|
||||||
|
|
||||||
|
return groupedItems.map((item, index) => ({
|
||||||
|
...item,
|
||||||
|
itemNumber: index + 1
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizeReviewItems(items?: Api.WorkReport.Common.PersonalReportReviewItem[] | null) {
|
||||||
|
const list = items?.length ? items : [createBlankReviewItem()];
|
||||||
|
|
||||||
|
const normalizedItems = list.map((item, index) => ({
|
||||||
|
...item,
|
||||||
|
itemNumber: item.itemNumber ?? index + 1,
|
||||||
|
itemTitle: item.itemTitle || '',
|
||||||
|
workHours: item.workHours ?? 0,
|
||||||
|
contentText: item.contentText || '',
|
||||||
|
contentJson: item.contentJson ?? null,
|
||||||
|
reflectionText: item.reflectionText || ''
|
||||||
|
}));
|
||||||
|
|
||||||
|
return groupPersonalReportItemsByTitle(normalizedItems, (target, source) => {
|
||||||
|
target.workHours = Number(target.workHours || 0) + Number(source.workHours || 0);
|
||||||
|
target.contentText = joinTextValues(target.contentText, source.contentText);
|
||||||
|
target.contentJson = mergeStructuredJson(target.contentJson, source.contentJson);
|
||||||
|
target.reflectionText = joinTextValues(target.reflectionText, source.reflectionText);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizePlanItems(items?: Api.WorkReport.Common.PersonalReportPlanItem[] | null) {
|
||||||
|
const list = items?.length ? items : [createBlankPlanItem()];
|
||||||
|
|
||||||
|
const normalizedItems = list.map((item, index) => ({
|
||||||
|
...item,
|
||||||
|
itemNumber: item.itemNumber ?? index + 1,
|
||||||
|
itemTitle: item.itemTitle || '',
|
||||||
|
targetText: item.targetText || '',
|
||||||
|
targetJson: item.targetJson ?? null,
|
||||||
|
supportNeed: item.supportNeed || ''
|
||||||
|
}));
|
||||||
|
|
||||||
|
return groupPersonalReportItemsByTitle(normalizedItems, (target, source) => {
|
||||||
|
target.targetText = joinTextValues(target.targetText, source.targetText);
|
||||||
|
target.targetJson = mergeStructuredJson(target.targetJson, source.targetJson);
|
||||||
|
target.supportNeed = joinTextValues(target.supportNeed, source.supportNeed);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizeProjectItems(items?: Api.WorkReport.Project.ProjectReportItem[] | null) {
|
||||||
|
const list = items?.length ? items : [createBlankProjectItem()];
|
||||||
|
|
||||||
|
return list.map(item => ({
|
||||||
|
...item,
|
||||||
|
itemTitle: item.itemTitle || '',
|
||||||
|
workHours: item.workHours ?? 0,
|
||||||
|
priorityCode: item.priorityCode || undefined,
|
||||||
|
progressRate: item.progressRate ?? 0
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createWeeklySaveParams(
|
||||||
|
base?: Partial<Api.WorkReport.Weekly.WeeklyReportSaveParams>
|
||||||
|
): Api.WorkReport.Weekly.WeeklyReportSaveParams {
|
||||||
|
return {
|
||||||
|
periodKey: base?.periodKey || '',
|
||||||
|
periodLabel: base?.periodLabel || '',
|
||||||
|
periodStartDate: base?.periodStartDate || '',
|
||||||
|
periodEndDate: base?.periodEndDate || '',
|
||||||
|
isBusinessTrip: base?.isBusinessTrip ?? false,
|
||||||
|
reviewItems: normalizeReviewItems(base?.reviewItems),
|
||||||
|
planItems: normalizePlanItems(base?.planItems),
|
||||||
|
travelSegments: base?.travelSegments?.length ? base.travelSegments : []
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createMonthlySaveParams(
|
||||||
|
base?: Partial<Api.WorkReport.Monthly.MonthlyReportSaveParams>
|
||||||
|
): Api.WorkReport.Monthly.MonthlyReportSaveParams {
|
||||||
|
return {
|
||||||
|
periodKey: base?.periodKey || '',
|
||||||
|
periodLabel: base?.periodLabel || '',
|
||||||
|
periodStartDate: base?.periodStartDate || '',
|
||||||
|
periodEndDate: base?.periodEndDate || '',
|
||||||
|
reviewItems: normalizeReviewItems(base?.reviewItems),
|
||||||
|
planItems: normalizePlanItems(base?.planItems)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createProjectSaveParams(
|
||||||
|
base?: Partial<Api.WorkReport.Project.ProjectReportSaveParams>
|
||||||
|
): Api.WorkReport.Project.ProjectReportSaveParams {
|
||||||
|
const defaultParams: Api.WorkReport.Project.ProjectReportSaveParams = {
|
||||||
|
projectId: '',
|
||||||
|
periodKey: '',
|
||||||
|
periodLabel: '',
|
||||||
|
periodStartDate: '',
|
||||||
|
periodEndDate: '',
|
||||||
|
flag: 1,
|
||||||
|
projectStatusDesc: '',
|
||||||
|
projectProgressPlan: '',
|
||||||
|
projectKeyPoints: '',
|
||||||
|
projectProblems: '',
|
||||||
|
currentItems: [createBlankProjectItem()],
|
||||||
|
nextItems: [createBlankProjectItem()]
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
...Object.assign(defaultParams, base),
|
||||||
|
currentItems: normalizeProjectItems(base?.currentItems),
|
||||||
|
nextItems: normalizeProjectItems(base?.nextItems)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
type NavigatorWithLegacySave = Navigator & {
|
||||||
|
msSaveOrOpenBlob?: (blob: Blob, defaultName?: string) => boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function downloadBlob(blob: Blob, filename: string) {
|
||||||
|
if (!(blob instanceof Blob)) {
|
||||||
|
window.$message?.error(getBlobErrorMessage(blob) || '导出失败:接口未返回文件流');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const downloadFile =
|
||||||
|
blob instanceof File ? blob : new File([blob], filename, { type: blob.type || 'application/octet-stream' });
|
||||||
|
const legacyNavigator = window.navigator as NavigatorWithLegacySave;
|
||||||
|
|
||||||
|
if (typeof legacyNavigator.msSaveOrOpenBlob === 'function') {
|
||||||
|
legacyNavigator.msSaveOrOpenBlob(downloadFile, filename);
|
||||||
|
window.$message?.success('导出文件已开始下载');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = window.URL.createObjectURL(downloadFile);
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = url;
|
||||||
|
link.download = filename;
|
||||||
|
link.style.display = 'none';
|
||||||
|
link.rel = 'noopener';
|
||||||
|
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
document.body.removeChild(link);
|
||||||
|
|
||||||
|
window.setTimeout(() => {
|
||||||
|
window.URL.revokeObjectURL(url);
|
||||||
|
}, 1000);
|
||||||
|
window.$message?.success('导出文件已开始下载');
|
||||||
|
}
|
||||||
|
|
||||||
|
function getBlobErrorMessage(value: unknown) {
|
||||||
|
if (!isRecord(value)) return '';
|
||||||
|
|
||||||
|
const message = value.msg || value.message || value.error;
|
||||||
|
return typeof message === 'string' && message.trim() ? message.trim() : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function safeDecodeFilename(value: string) {
|
||||||
|
try {
|
||||||
|
return decodeURIComponent(value);
|
||||||
|
} catch {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getResponseHeader(headers: unknown, headerName: string) {
|
||||||
|
if (!headers) return '';
|
||||||
|
|
||||||
|
if (typeof (headers as { get?: unknown }).get === 'function') {
|
||||||
|
const value = (headers as { get: (name: string) => unknown }).get(headerName);
|
||||||
|
return value === null || value === undefined ? '' : String(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isRecord(headers)) return '';
|
||||||
|
|
||||||
|
const matchedKey = Object.keys(headers).find(key => key.toLowerCase() === headerName.toLowerCase());
|
||||||
|
if (!matchedKey) return '';
|
||||||
|
|
||||||
|
const value = headers[matchedKey];
|
||||||
|
return value === null || value === undefined ? '' : String(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getFilenameFromDisposition(disposition?: string | null) {
|
||||||
|
if (!disposition) return '';
|
||||||
|
|
||||||
|
const filenameStarMatch = disposition.match(/filename\*=UTF-8''([^;]+)/i);
|
||||||
|
if (filenameStarMatch?.[1]) {
|
||||||
|
return safeDecodeFilename(filenameStarMatch[1].replace(/^"|"$/g, ''));
|
||||||
|
}
|
||||||
|
|
||||||
|
const filenameMatch = disposition.match(/filename="?([^";]+)"?/i);
|
||||||
|
if (filenameMatch?.[1]) {
|
||||||
|
return safeDecodeFilename(filenameMatch[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveExportFilename(result: { response?: { headers?: unknown } }, fallbackName: string) {
|
||||||
|
const disposition = getResponseHeader(result.response?.headers, 'content-disposition');
|
||||||
|
return getFilenameFromDisposition(disposition) || fallbackName;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createWorkReportContentExportFallbackName(reportType: WorkReportType, reportCount: number) {
|
||||||
|
const extension = reportCount === 1 ? 'docx' : 'zip';
|
||||||
|
return `${WORK_REPORT_TYPE_LABEL[reportType]}_${dayjs().format('YYYY-MM-DD')}.${extension}`;
|
||||||
|
}
|
||||||
194
src/views/personal-center/work-report/shared/utils.ts
Normal file
194
src/views/personal-center/work-report/shared/utils.ts
Normal file
@@ -0,0 +1,194 @@
|
|||||||
|
import dayjs from 'dayjs';
|
||||||
|
import isoWeek from 'dayjs/plugin/isoWeek';
|
||||||
|
import weekday from 'dayjs/plugin/weekday';
|
||||||
|
import type { WorkReportType } from './types';
|
||||||
|
|
||||||
|
dayjs.extend(isoWeek);
|
||||||
|
dayjs.extend(weekday);
|
||||||
|
|
||||||
|
export interface WorkReportPeriodOption {
|
||||||
|
key: string;
|
||||||
|
label: string;
|
||||||
|
description: string;
|
||||||
|
reportType: WorkReportType;
|
||||||
|
flag?: number;
|
||||||
|
period: {
|
||||||
|
periodKey: string;
|
||||||
|
periodLabel: string;
|
||||||
|
periodStartDate: string;
|
||||||
|
periodEndDate: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatRangeLabel(start: dayjs.Dayjs, end: dayjs.Dayjs) {
|
||||||
|
return `${start.format('YYYY-MM-DD')} 至 ${end.format('YYYY-MM-DD')}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatPeriodDisplayLabel(label?: string | null) {
|
||||||
|
return String(label || '')
|
||||||
|
.trim()
|
||||||
|
.replace(/\s*(周报|月报|项目半月报|半月报)\s*$/u, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getIsoWeekDisplay(date: string | dayjs.Dayjs) {
|
||||||
|
const selectedDate = dayjs(date);
|
||||||
|
if (!selectedDate.isValid()) return '';
|
||||||
|
return `${selectedDate.format('YYYY')} 第${String(selectedDate.isoWeek()).padStart(2, '0')} 周`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* eslint-disable-next-line max-params */
|
||||||
|
function buildPeriod(reportType: WorkReportType, start: dayjs.Dayjs, end: dayjs.Dayjs, label: string, flag?: number) {
|
||||||
|
const startText = start.format('YYYY-MM-DD');
|
||||||
|
const endText = end.format('YYYY-MM-DD');
|
||||||
|
|
||||||
|
return {
|
||||||
|
periodKey: flag ? `${reportType}-${startText}-${endText}-${flag}` : `${reportType}-${startText}-${endText}`,
|
||||||
|
periodLabel: label,
|
||||||
|
periodStartDate: startText,
|
||||||
|
periodEndDate: endText
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildWeeklyPeriodFromDate(date: string | dayjs.Dayjs) {
|
||||||
|
const selectedDate = dayjs(date);
|
||||||
|
const start = selectedDate.startOf('isoWeek');
|
||||||
|
const end = selectedDate.endOf('isoWeek');
|
||||||
|
|
||||||
|
return buildPeriod('weekly', start, end, formatRangeLabel(start, end));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildMonthlyPeriodFromMonth(month: string | dayjs.Dayjs) {
|
||||||
|
const selectedMonth = dayjs(month);
|
||||||
|
const start = selectedMonth.startOf('month');
|
||||||
|
const end = selectedMonth.endOf('month');
|
||||||
|
|
||||||
|
return buildPeriod('monthly', start, end, selectedMonth.format('YYYY-MM'));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildProjectPeriodFromMonth(month: string | dayjs.Dayjs, flag: number) {
|
||||||
|
const selectedMonth = dayjs(month);
|
||||||
|
const monthStart = selectedMonth.startOf('month');
|
||||||
|
|
||||||
|
if (flag === 2) {
|
||||||
|
const start = monthStart.date(16);
|
||||||
|
const end = selectedMonth.endOf('month');
|
||||||
|
return buildPeriod('project', start, end, `${selectedMonth.format('YYYY-MM')} 下半月`, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
const start = monthStart.startOf('month');
|
||||||
|
const end = monthStart.date(15);
|
||||||
|
return buildPeriod('project', start, end, `${selectedMonth.format('YYYY-MM')} 上半月`, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getWeeklyPeriodOptions(now = dayjs()): WorkReportPeriodOption[] {
|
||||||
|
const thisWeekStart = now.startOf('isoWeek');
|
||||||
|
const thisWeekEnd = now.endOf('isoWeek');
|
||||||
|
const lastWeekStart = thisWeekStart.subtract(1, 'week');
|
||||||
|
const lastWeekEnd = thisWeekEnd.subtract(1, 'week');
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
key: 'current-week',
|
||||||
|
label: '本周',
|
||||||
|
description: formatRangeLabel(thisWeekStart, thisWeekEnd),
|
||||||
|
reportType: 'weekly',
|
||||||
|
period: buildPeriod('weekly', thisWeekStart, thisWeekEnd, formatRangeLabel(thisWeekStart, thisWeekEnd))
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'last-week',
|
||||||
|
label: '上周',
|
||||||
|
description: formatRangeLabel(lastWeekStart, lastWeekEnd),
|
||||||
|
reportType: 'weekly',
|
||||||
|
period: buildPeriod('weekly', lastWeekStart, lastWeekEnd, formatRangeLabel(lastWeekStart, lastWeekEnd))
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getMonthlyPeriodOptions(now = dayjs()): WorkReportPeriodOption[] {
|
||||||
|
const thisMonthStart = now.startOf('month');
|
||||||
|
const thisMonthEnd = now.endOf('month');
|
||||||
|
const lastMonth = now.subtract(1, 'month');
|
||||||
|
const lastMonthStart = lastMonth.startOf('month');
|
||||||
|
const lastMonthEnd = lastMonth.endOf('month');
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
key: 'current-month',
|
||||||
|
label: '本月',
|
||||||
|
description: thisMonthStart.format('YYYY-MM'),
|
||||||
|
reportType: 'monthly',
|
||||||
|
period: buildPeriod('monthly', thisMonthStart, thisMonthEnd, thisMonthStart.format('YYYY-MM'))
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'last-month',
|
||||||
|
label: '上月',
|
||||||
|
description: lastMonthStart.format('YYYY-MM'),
|
||||||
|
reportType: 'monthly',
|
||||||
|
period: buildPeriod('monthly', lastMonthStart, lastMonthEnd, lastMonthStart.format('YYYY-MM'))
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getProjectPeriodOptions(now = dayjs()): WorkReportPeriodOption[] {
|
||||||
|
const currentMonthStart = now.startOf('month');
|
||||||
|
const currentMonthEnd = now.endOf('month');
|
||||||
|
const currentFirstHalfEnd = currentMonthStart.date(15);
|
||||||
|
const currentSecondHalfStart = currentMonthStart.date(16);
|
||||||
|
const previousMonth = now.subtract(1, 'month');
|
||||||
|
const previousMonthStart = previousMonth.startOf('month');
|
||||||
|
const previousMonthEnd = previousMonth.endOf('month');
|
||||||
|
const previousSecondHalfStart = previousMonthStart.date(16);
|
||||||
|
const isCurrentFirstHalf = now.date() <= 15;
|
||||||
|
|
||||||
|
const currentOption: WorkReportPeriodOption = isCurrentFirstHalf
|
||||||
|
? {
|
||||||
|
key: 'current-first-half',
|
||||||
|
label: '上半月',
|
||||||
|
description: `${now.format('YYYY-MM')} 上半月`,
|
||||||
|
reportType: 'project',
|
||||||
|
flag: 1,
|
||||||
|
period: buildPeriod('project', currentMonthStart, currentFirstHalfEnd, `${now.format('YYYY-MM')} 上半月`, 1)
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
key: 'current-second-half',
|
||||||
|
label: '下半月',
|
||||||
|
description: `${now.format('YYYY-MM')} 下半月`,
|
||||||
|
reportType: 'project',
|
||||||
|
flag: 2,
|
||||||
|
period: buildPeriod('project', currentSecondHalfStart, currentMonthEnd, `${now.format('YYYY-MM')} 下半月`, 2)
|
||||||
|
};
|
||||||
|
|
||||||
|
const previousOption: WorkReportPeriodOption = isCurrentFirstHalf
|
||||||
|
? {
|
||||||
|
key: 'previous-second-half',
|
||||||
|
label: '下半月',
|
||||||
|
description: `${previousMonth.format('YYYY-MM')} 下半月`,
|
||||||
|
reportType: 'project',
|
||||||
|
flag: 2,
|
||||||
|
period: buildPeriod(
|
||||||
|
'project',
|
||||||
|
previousSecondHalfStart,
|
||||||
|
previousMonthEnd,
|
||||||
|
`${previousMonth.format('YYYY-MM')} 下半月`,
|
||||||
|
2
|
||||||
|
)
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
key: 'previous-first-half',
|
||||||
|
label: '上半月',
|
||||||
|
description: `${now.format('YYYY-MM')} 上半月`,
|
||||||
|
reportType: 'project',
|
||||||
|
flag: 1,
|
||||||
|
period: buildPeriod('project', currentMonthStart, currentFirstHalfEnd, `${now.format('YYYY-MM')} 上半月`, 1)
|
||||||
|
};
|
||||||
|
|
||||||
|
return [currentOption, previousOption];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getReportTypePeriodOptions(now = dayjs()) {
|
||||||
|
return {
|
||||||
|
weekly: getWeeklyPeriodOptions(now),
|
||||||
|
monthly: getMonthlyPeriodOptions(now),
|
||||||
|
project: getProjectPeriodOptions(now)
|
||||||
|
} as const;
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user