项目需求、任务以及模块精简

This commit is contained in:
2026-04-06 00:23:23 +08:00
parent 97564b82da
commit 477a08da9f
32 changed files with 1888 additions and 874 deletions

View File

@@ -93,19 +93,22 @@
});
};
const loadComponent = (vueName: string) => {
const loadComponent = (vueName: string): Component => {
return defineAsyncComponent({
loader: () => import(`./components/${vueName}.vue`),
delay: 200,
timeout: 5000,
onError(error) {
console.error('加载组件失败', error);
},
});
};
async function getChartList() {
try {
const reqParams = {
ustatus: '1',
chartCode: 'erp',
ustatus: '1',
chartCode: 'erp',
};
const res = await myChartInfoListAll(reqParams);
chartData.value = res || [];
@@ -132,7 +135,9 @@
await getChartList();
const newComponentMap: ComponentMap = {};
const uniqueComponents = [...new Set(chartData.value.map((item) => item.vueName))];
const uniqueComponents = [...new Set(chartData.value.map((item) => item.vueName))].filter(
(vueName) => vueName && vueName !== 'ChartTop',
);
for (const vueName of uniqueComponents) {
if (vueName) {
newComponentMap[vueName] = loadComponent(vueName);

View File

@@ -29,7 +29,7 @@
</template>
<script lang="ts" setup>
import { ref, watch, onMounted } from 'vue';
import { ref, watch, onMounted, defineAsyncComponent } from 'vue';
import type { Component } from 'vue';
import { useRoute } from 'vue-router';
import ChartTop from './components/ChartTop.vue';
@@ -68,21 +68,22 @@
});
};
const loadComponent = async (vueName: string): Promise<Component | null> => {
try {
const module = await import(`./components/${vueName}.vue`);
return module.default;
} catch (error) {
console.error('加载组件失败', error);
return null;
}
const loadComponent = (vueName: string): Component => {
return defineAsyncComponent({
loader: () => import(`./components/${vueName}.vue`),
delay: 200,
timeout: 5000,
onError(error) {
console.error('加载组件失败', error);
},
});
};
async function getChartList() {
try {
const reqParams = {
ustatus: '1',
chartCode: 'home',
ustatus: '1',
chartCode: 'home',
};
const res = await myChartInfoListAll(reqParams);
chartData.value = res || [];
@@ -95,15 +96,17 @@
onMounted(async () => {
try {
await getChartList();
const newComponentMap: ComponentMap = {};
const uniqueComponents = [...new Set(chartData.value.map((item) => item.vueName))];
const uniqueComponents = [...new Set(chartData.value.map((item) => item.vueName))].filter(
(vueName) => vueName && vueName !== 'ChartTop',
);
for (const vueName of uniqueComponents) {
if (vueName) {
newComponentMap[vueName] = loadComponent(vueName);
}
}
componentMap.value = newComponentMap;
} catch (error) {
console.error('加载图表数据失败:', error);

View File

@@ -29,7 +29,7 @@
</template>
<script lang="ts" setup>
import { ref, watch, onMounted } from 'vue';
import { ref, watch, onMounted, defineAsyncComponent } from 'vue';
import type { Component } from 'vue';
import { useRoute } from 'vue-router';
import ChartTop from './components/ChartTop.vue';
@@ -68,21 +68,22 @@
});
};
const loadComponent = async (vueName: string): Promise<Component | null> => {
try {
const module = await import(`./components/${vueName}.vue`);
return module.default;
} catch (error) {
console.error('加载组件失败', error);
return null;
}
const loadComponent = (vueName: string): Component => {
return defineAsyncComponent({
loader: () => import(`./components/${vueName}.vue`),
delay: 200,
timeout: 5000,
onError(error) {
console.error('加载组件失败', error);
},
});
};
async function getChartList() {
try {
const reqParams = {
ustatus: '1',
chartCode: 'sys',
ustatus: '1',
chartCode: 'sys',
};
const res = await myChartInfoListAll(reqParams);
chartData.value = res || [];
@@ -95,15 +96,17 @@
onMounted(async () => {
try {
await getChartList();
const newComponentMap: ComponentMap = {};
const uniqueComponents = [...new Set(chartData.value.map((item) => item.vueName))];
const uniqueComponents = [...new Set(chartData.value.map((item) => item.vueName))].filter(
(vueName) => vueName && vueName !== 'ChartTop',
);
for (const vueName of uniqueComponents) {
if (vueName) {
newComponentMap[vueName] = loadComponent(vueName);
}
}
componentMap.value = newComponentMap;
} catch (error) {
console.error('加载图表数据失败:', error);

View File

@@ -37,7 +37,7 @@
</template>
<script lang="ts" setup>
import { ref, watch, onMounted } from 'vue';
import { ref, watch, onMounted, defineAsyncComponent } from 'vue';
import type { Component } from 'vue';
import { useRoute } from 'vue-router';
import ChartTop from './components/ChartTop.vue';
@@ -80,21 +80,22 @@
});
};
const loadComponent = async (vueName: string): Promise<Component | null> => {
try {
const module = await import(`./components/${vueName}.vue`);
return module.default;
} catch (error) {
console.error('加载组件失败', error);
return null;
}
const loadComponent = (vueName: string): Component => {
return defineAsyncComponent({
loader: () => import(`./components/${vueName}.vue`),
delay: 200,
timeout: 5000,
onError(error) {
console.error('加载组件失败', error);
},
});
};
async function getChartList() {
try {
const reqParams = {
ustatus: '1',
chartCode: 'work',
ustatus: '1',
chartCode: 'work',
};
const res = await myChartInfoListAll(reqParams);
chartData.value = res || [];
@@ -107,15 +108,17 @@
onMounted(async () => {
try {
await getChartList();
const newComponentMap: ComponentMap = {};
const uniqueComponents = [...new Set(chartData.value.map((item) => item.vueName))];
const uniqueComponents = [...new Set(chartData.value.map((item) => item.vueName))].filter(
(vueName) => vueName && vueName !== 'ChartTop',
);
for (const vueName of uniqueComponents) {
if (vueName) {
newComponentMap[vueName] = loadComponent(vueName);
}
}
componentMap.value = newComponentMap;
} catch (error) {
console.error('加载图表数据失败:', error);

View File

@@ -1,8 +1,6 @@
<template>
<PageWrapper>
<template #headerContent>
</template>
<PageWrapper :contentFullHeight="true" :dense="true" title="false" contentClass="about-page-wrapper">
<template #headerContent> </template>
<div class="jeesite-workbench">
<div class="workbench-layout">
<div class="workbench-top">10% 区域</div>
@@ -14,10 +12,10 @@
<div class="workbench-col">30% 区域左侧</div>
<div class="workbench-col">30% 区域右侧</div>
</div>
<div class="workbench-row">
<div class="workbench-col">30% 区域左侧</div>
<div class="workbench-col">30% 区域右侧</div>
</div>
<div class="workbench-row">
<div class="workbench-col">30% 区域左侧</div>
<div class="workbench-col">30% 区域右侧</div>
</div>
</div>
</div>
</PageWrapper>
@@ -34,28 +32,48 @@
<style lang="less">
@dark-bg: #141414;
@desktop-page-gap: 12px;
@desktop-page-padding: 0;
@desktop-card-radius: 10px;
@desktop-card-border: 1px solid rgb(226 232 240);
@desktop-card-shadow: 0 1px 3px rgb(15 23 42 / 0.06);
@desktop-dark-border: rgb(51 65 85);
.about-page-wrapper {
display: flex;
flex-direction: column;
height: 100%;
min-height: 0;
padding: 0 !important;
overflow: hidden !important;
background: transparent !important;
}
.jeesite-workbench {
display: flex;
flex-direction: column;
flex: 1;
width: 100%;
height: 100%;
min-height: 0;
margin: 0;
background: #FFFFFF;
border-radius: 10px;
background: #ffffff;
border-radius: @desktop-card-radius;
}
.jeesite-workbench .workbench-layout {
display: flex;
flex: 1;
flex-direction: column;
gap: 12px;
gap: @desktop-page-gap;
width: 100%;
height: 100%;
min-height: 0;
padding: 4px;
padding: @desktop-page-padding;
box-sizing: border-box;
overflow: hidden;
background: transparent;
border-radius: 10px;
border-radius: @desktop-card-radius;
}
.jeesite-workbench .workbench-top {
@@ -66,7 +84,7 @@
.jeesite-workbench .workbench-row {
display: flex;
flex: 0 0 30%;
gap: 12px;
gap: @desktop-page-gap;
min-height: 0;
}
@@ -75,9 +93,10 @@
display: flex;
align-items: center;
justify-content: center;
border-radius: 10px;
border: 1px solid rgb(226 232 240);
background: #FFFFFF;
border-radius: @desktop-card-radius;
border: @desktop-card-border;
background: #ffffff;
box-shadow: @desktop-card-shadow;
color: rgb(71 85 105);
}
@@ -94,8 +113,9 @@
html[data-theme='dark'] .jeesite-workbench .workbench-top,
html[data-theme='dark'] .jeesite-workbench .workbench-col {
border-color: rgb(51 65 85);
border-color: @desktop-dark-border;
background: @dark-bg !important;
color: rgb(226 232 240);
box-shadow: none;
}
</style>

View File

@@ -43,6 +43,10 @@
</script>
<style lang="less">
@analysis-dark-bg: rgb(20, 20, 20);
@analysis-dark-hover-bg: rgb(30 41 59);
@analysis-dark-shadow: 0 10px 24px rgb(0 0 0 / 24%);
.biz-apps-card {
width: 100%;
height: 100%;
@@ -122,7 +126,7 @@
}
html[data-theme='dark'] .biz-apps-card {
background: rgb(20, 20, 20);
background: @analysis-dark-bg;
.card-title {
color: rgb(203 213 225);
@@ -131,12 +135,13 @@
.biz-apps-item {
border-color: rgb(51 65 85);
background: rgb(20, 20, 20);
box-shadow: 0 10px 24px rgb(0 0 0 / 24%);
background: @analysis-dark-bg;
box-shadow: @analysis-dark-shadow;
&:hover {
border-color: rgb(96 165 250);
box-shadow: 0 14px 32px rgb(37 99 235 / 22%);
background: @analysis-dark-hover-bg;
box-shadow: @analysis-dark-shadow;
}
&__name {

View File

@@ -276,6 +276,10 @@
</script>
<style lang="less">
@analysis-dark-bg: rgb(20, 20, 20);
@analysis-dark-hover-bg: rgb(30 41 59);
@analysis-dark-shadow: 0 10px 24px rgb(0 0 0 / 24%);
.host-card {
width: 100%;
height: 100%;
@@ -413,7 +417,7 @@
}
html[data-theme='dark'] .host-card {
background: rgb(20, 20, 20);
background: @analysis-dark-bg;
.card-title {
color: rgb(203 213 225);
@@ -438,20 +442,21 @@
}
.host-gauge-panel {
background: linear-gradient(180deg, rgb(20, 20, 20) 0%, rgb(28 28 28) 100%);
box-shadow: 0 10px 24px rgb(0 0 0 / 24%);
background: @analysis-dark-bg;
box-shadow: @analysis-dark-shadow;
}
.metric-item {
background: linear-gradient(180deg, rgb(20, 20, 20) 0%, rgb(28 28 28) 100%);
box-shadow: 0 10px 24px rgb(0 0 0 / 24%);
background: @analysis-dark-bg;
box-shadow: @analysis-dark-shadow;
&__label {
color: rgb(148 163 184);
}
&:hover {
box-shadow: 0 12px 28px rgb(96 165 250 / 20%);
background: @analysis-dark-hover-bg;
box-shadow: @analysis-dark-shadow;
}
}
}

View File

@@ -206,7 +206,9 @@
</script>
<style lang="less">
@dark-bg: #141414;
@analysis-dark-bg: rgb(20, 20, 20);
@analysis-dark-hover-bg: rgb(30 41 59);
@analysis-dark-shadow: 0 10px 24px rgb(0 0 0 / 24%);
.notice-card {
width: 100%;
@@ -591,7 +593,7 @@
html[data-theme='dark'] .notice-card {
box-shadow: none;
background: rgb(20, 20, 20);
background: @analysis-dark-bg;
.card-title {
color: rgb(203 213 225);
@@ -616,15 +618,27 @@
.table-container {
.el-table {
--el-table-border-color: transparent;
--el-table-header-bg-color: rgb(20, 20, 20);
--el-table-header-bg-color: @analysis-dark-bg;
--el-table-tr-bg-color: transparent;
--el-table-row-hover-bg-color: rgb(30 41 59);
--el-table-row-hover-bg-color: @analysis-dark-hover-bg;
--el-table-text-color: rgb(148 163 184);
--el-table-header-text-color: rgb(226 232 240);
background: transparent;
th.el-table__cell {
background: rgb(20, 20, 20);
background: @analysis-dark-bg;
}
.el-loading-mask {
background-color: rgb(20 20 20 / 72%);
}
.el-loading-spinner .path {
stroke: rgb(147 197 253);
}
.el-loading-spinner .el-loading-text {
color: rgb(203 213 225);
}
th.el-table__cell,
@@ -650,25 +664,25 @@
&__content-panel,
&__attachments-panel {
background: rgb(20, 20, 20);
background: @analysis-dark-bg;
}
&__attachments-panel {
border-color: rgb(51 65 85);
background: rgb(20, 20, 20);
background: @analysis-dark-bg;
}
&__content {
color: rgb(226 232 240);
background: rgb(20, 20, 20);
background: @analysis-dark-bg;
border-color: rgb(51 65 85);
}
&__attachment-item,
&__attachments-empty {
border-color: rgb(71 85 105);
background: rgb(20, 20, 20);
box-shadow: 0 10px 24px rgb(0 0 0 / 24%);
background: @analysis-dark-bg;
box-shadow: @analysis-dark-shadow;
color: rgb(226 232 240);
}
@@ -686,8 +700,8 @@
&__attachment-item:hover {
border-color: rgb(96 165 250);
background: rgb(37 99 235 / 22%);
box-shadow: 0 14px 32px rgb(37 99 235 / 22%);
background: @analysis-dark-hover-bg;
box-shadow: @analysis-dark-shadow;
color: rgb(241 245 249);
}
@@ -698,27 +712,27 @@
}
html[data-theme='dark'] .notice-info-dialog {
--el-bg-color: rgb(20, 20, 20);
--el-dialog-bg-color: rgb(20, 20, 20);
--el-fill-color-blank: rgb(20, 20, 20);
--el-bg-color: @analysis-dark-bg;
--el-dialog-bg-color: @analysis-dark-bg;
--el-fill-color-blank: @analysis-dark-bg;
.el-dialog {
background: rgb(20, 20, 20) !important;
--el-dialog-bg-color: rgb(20, 20, 20);
--el-bg-color: rgb(20, 20, 20);
--el-fill-color-blank: rgb(20, 20, 20);
box-shadow: 0 14px 36px rgb(0 0 0 / 42%);
background: @analysis-dark-bg !important;
--el-dialog-bg-color: @analysis-dark-bg;
--el-bg-color: @analysis-dark-bg;
--el-fill-color-blank: @analysis-dark-bg;
box-shadow: @analysis-dark-shadow;
}
.el-dialog__wrapper,
.el-overlay-dialog,
.el-dialog__content {
background: rgb(20, 20, 20) !important;
background: @analysis-dark-bg !important;
}
.el-dialog__header {
border-bottom: 1px solid rgb(51 65 85) !important;
background: rgb(20, 20, 20) !important;
background: @analysis-dark-bg !important;
}
.el-dialog__title {
@@ -730,14 +744,14 @@
}
.el-dialog__body {
background: rgb(20, 20, 20) !important;
background: @analysis-dark-bg !important;
}
.el-dialog__header,
.el-dialog__body,
.el-dialog__footer {
--el-bg-color: rgb(20, 20, 20);
--el-fill-color-blank: rgb(20, 20, 20);
--el-bg-color: @analysis-dark-bg;
--el-fill-color-blank: @analysis-dark-bg;
}
.el-divider {
@@ -746,7 +760,7 @@
.notice-dialog {
&__attachments-title {
background: rgb(20, 20, 20);
background: @analysis-dark-bg;
color: rgb(226 232 240);
}

View File

@@ -196,6 +196,10 @@
</script>
<style lang="less">
@analysis-dark-bg: rgb(20, 20, 20);
@analysis-dark-hover-bg: rgb(30 41 59);
@analysis-dark-shadow: 0 10px 24px rgb(0 0 0 / 24%);
.oper-log-card {
width: 100%;
height: 100%;
@@ -369,7 +373,7 @@
}
html[data-theme='dark'] .oper-log-card {
background: rgb(20, 20, 20);
background: @analysis-dark-bg;
.card-title {
color: rgb(203 213 225);
@@ -378,8 +382,8 @@
.query-panel,
.table-panel {
background: linear-gradient(180deg, rgb(20, 20, 20) 0%, rgb(28 28 28) 100%);
box-shadow: 0 10px 24px rgb(0 0 0 / 24%);
background: @analysis-dark-bg;
box-shadow: @analysis-dark-shadow;
}
.query-form {
@@ -391,7 +395,7 @@
:deep(.el-select__wrapper) {
--el-fill-color-blank: rgb(20, 20, 20);
--el-bg-color: rgb(20, 20, 20);
background: rgb(20, 20, 20);
background: @analysis-dark-bg;
box-shadow: 0 0 0 1px rgb(71 85 105) inset;
}
@@ -404,7 +408,7 @@
:deep(.el-select-dropdown),
:deep(.el-popper),
:deep(.el-select__popper.el-popper) {
background: rgb(20, 20, 20);
background: @analysis-dark-bg;
border-color: rgb(51 65 85);
}
@@ -415,7 +419,7 @@
:deep(.el-select-dropdown__item.hover),
:deep(.el-select-dropdown__item:hover),
:deep(.el-select-dropdown__item.is-hovering) {
background: rgb(30 41 59);
background: @analysis-dark-hover-bg;
color: rgb(241 245 249);
}
@@ -453,28 +457,28 @@
}
:deep(.el-button) {
--el-fill-color-blank: rgb(30 41 59);
--el-bg-color: rgb(30 41 59);
--el-fill-color-blank: @analysis-dark-bg;
--el-bg-color: @analysis-dark-bg;
border-color: rgb(71 85 105);
background: rgb(30 41 59);
background: @analysis-dark-bg;
color: rgb(226 232 240);
}
:deep(.el-button:hover) {
border-color: rgb(96 165 250);
background: rgb(37 99 235 / 22%);
background: @analysis-dark-hover-bg;
color: rgb(241 245 249);
}
:deep(.el-button--primary) {
border-color: rgb(59 130 246);
background: rgb(37 99 235);
border-color: rgb(96 165 250);
background: @analysis-dark-bg;
color: rgb(248 250 252);
}
:deep(.el-button--primary:hover) {
border-color: rgb(96 165 250);
background: rgb(59 130 246);
background: @analysis-dark-hover-bg;
color: rgb(248 250 252);
}
}
@@ -482,15 +486,15 @@
.table-panel {
.el-table {
--el-table-border-color: transparent;
--el-table-header-bg-color: rgb(20, 20, 20);
--el-table-header-bg-color: @analysis-dark-bg;
--el-table-tr-bg-color: transparent;
--el-table-row-hover-bg-color: rgb(30 41 59);
--el-table-row-hover-bg-color: @analysis-dark-hover-bg;
--el-table-text-color: rgb(148 163 184);
--el-table-header-text-color: rgb(226 232 240);
background: transparent;
th.el-table__cell {
background: rgb(20, 20, 20);
background: @analysis-dark-bg;
}
th.el-table__cell,
@@ -499,11 +503,23 @@
}
.el-table__footer-wrapper td.el-table__cell {
background: rgb(20, 20, 20);
background: @analysis-dark-bg;
border-top: 1px solid rgb(51 65 85);
border-bottom: none;
}
.el-loading-mask {
background-color: rgb(20 20 20 / 72%);
}
.el-loading-spinner .path {
stroke: rgb(147 197 253);
}
.el-loading-spinner .el-loading-text {
color: rgb(203 213 225);
}
.el-table__footer-wrapper .cell {
color: rgb(226 232 240);
}
@@ -517,7 +533,7 @@
:deep(.el-pagination) {
--el-pagination-text-color: rgb(226 232 240);
--el-pagination-button-color: rgb(226 232 240);
--el-pagination-button-bg-color: rgb(30 41 59);
--el-pagination-button-bg-color: @analysis-dark-bg;
--el-pagination-hover-color: rgb(147 197 253);
}
@@ -525,7 +541,7 @@
:deep(.btn-next),
:deep(.el-pager li),
:deep(.el-pagination button) {
background: rgb(30 41 59) !important;
background: @analysis-dark-bg !important;
color: rgb(226 232 240) !important;
}
@@ -578,7 +594,7 @@
html[data-theme='dark'] .el-select-dropdown__item.hover,
html[data-theme='dark'] .el-select-dropdown__item:hover,
html[data-theme='dark'] .el-select-dropdown__item.is-hovering {
background: rgb(30 41 59) !important;
background: @analysis-dark-hover-bg !important;
color: rgb(241 245 249) !important;
}
@@ -590,7 +606,7 @@
html[data-theme='dark'] .el-pagination .btn-next,
html[data-theme='dark'] .el-pagination .el-pager li,
html[data-theme='dark'] .el-pagination button {
background: rgb(30 41 59) !important;
background: @analysis-dark-bg !important;
color: rgb(226 232 240) !important;
}
@@ -632,7 +648,7 @@
html[data-theme='dark'] .oper-log-card .el-input.is-disabled .el-input__wrapper,
html[data-theme='dark'] .oper-log-card .el-select.is-disabled .el-select__wrapper {
background: rgb(30 41 59) !important;
background: @analysis-dark-bg !important;
}
html[data-theme='dark'] .oper-log-card .el-input__suffix,
@@ -644,16 +660,16 @@
}
html[data-theme='dark'] .oper-log-card .query-form__actions .el-button {
--el-button-bg-color: rgb(30 41 59) !important;
--el-button-bg-color: @analysis-dark-bg !important;
--el-button-border-color: rgb(71 85 105) !important;
--el-button-text-color: rgb(226 232 240) !important;
--el-button-hover-bg-color: rgb(37 99 235 / 22%) !important;
--el-button-hover-bg-color: @analysis-dark-hover-bg !important;
--el-button-hover-border-color: rgb(96 165 250) !important;
--el-button-hover-text-color: rgb(241 245 249) !important;
--el-button-active-bg-color: rgb(37 99 235 / 28%) !important;
--el-button-active-bg-color: @analysis-dark-hover-bg !important;
--el-button-active-border-color: rgb(96 165 250) !important;
--el-button-active-text-color: rgb(241 245 249) !important;
background: rgb(30 41 59) !important;
background: @analysis-dark-bg !important;
border-color: rgb(71 85 105) !important;
color: rgb(226 232 240) !important;
box-shadow: none !important;
@@ -661,26 +677,26 @@
html[data-theme='dark'] .oper-log-card .query-form__actions .el-button:hover,
html[data-theme='dark'] .oper-log-card .query-form__actions .el-button:focus {
background: rgb(37 99 235 / 22%) !important;
background: @analysis-dark-hover-bg !important;
border-color: rgb(96 165 250) !important;
color: rgb(241 245 249) !important;
}
html[data-theme='dark'] .oper-log-card .query-form__actions .el-button--primary {
--el-button-bg-color: rgb(37 99 235) !important;
--el-button-border-color: rgb(59 130 246) !important;
--el-button-bg-color: @analysis-dark-bg !important;
--el-button-border-color: rgb(96 165 250) !important;
--el-button-text-color: rgb(248 250 252) !important;
--el-button-hover-bg-color: rgb(59 130 246) !important;
--el-button-hover-bg-color: @analysis-dark-hover-bg !important;
--el-button-hover-border-color: rgb(96 165 250) !important;
--el-button-hover-text-color: rgb(248 250 252) !important;
background: rgb(37 99 235) !important;
border-color: rgb(59 130 246) !important;
background: @analysis-dark-bg !important;
border-color: rgb(96 165 250) !important;
color: rgb(248 250 252) !important;
}
html[data-theme='dark'] .oper-log-card .query-form__actions .el-button--primary:hover,
html[data-theme='dark'] .oper-log-card .query-form__actions .el-button--primary:focus {
background: rgb(59 130 246) !important;
background: @analysis-dark-hover-bg !important;
border-color: rgb(96 165 250) !important;
color: rgb(248 250 252) !important;
}

View File

@@ -184,6 +184,9 @@
</script>
<style lang="less">
@analysis-dark-bg: rgb(20, 20, 20);
@analysis-dark-shadow: 0 10px 24px rgb(0 0 0 / 24%);
.province-card {
width: 100%;
height: 100%;
@@ -241,7 +244,7 @@
}
html[data-theme='dark'] .province-card {
background: rgb(20, 20, 20);
background: @analysis-dark-bg;
.card-title {
color: rgb(203 213 225);
@@ -253,8 +256,8 @@
}
.province-chart-panel {
background: linear-gradient(180deg, rgb(20, 20, 20) 0%, rgb(28 28 28) 100%);
box-shadow: 0 10px 24px rgb(0 0 0 / 24%);
background: @analysis-dark-bg;
box-shadow: @analysis-dark-shadow;
}
}
</style>

View File

@@ -157,6 +157,10 @@
</script>
<style lang="less">
@analysis-dark-bg: rgb(20, 20, 20);
@analysis-dark-hover-bg: rgb(30 41 59);
@analysis-dark-shadow: 0 10px 24px rgb(0 0 0 / 24%);
.quick-login-card {
width: 100%;
height: 100%;
@@ -292,7 +296,7 @@
}
html[data-theme='dark'] .quick-login-card {
background: rgb(20, 20, 20);
background: @analysis-dark-bg;
.card-title {
color: rgb(203 213 225);
@@ -301,12 +305,13 @@
.quick-login-item {
border-color: rgb(51 65 85);
background: rgb(20, 20, 20);
box-shadow: 0 10px 24px rgb(0 0 0 / 24%);
background: @analysis-dark-bg;
box-shadow: @analysis-dark-shadow;
&:hover {
border-color: rgb(96 165 250);
box-shadow: 0 14px 32px rgb(37 99 235 / 22%);
background: @analysis-dark-hover-bg;
box-shadow: @analysis-dark-shadow;
}
&__name {
@@ -314,7 +319,7 @@
}
&__image-wrap {
background: rgb(30 41 59);
background: @analysis-dark-bg;
border-color: rgb(59 130 246 / 35%);
box-shadow: 0 6px 14px rgb(37 99 235 / 16%);
}

View File

@@ -255,6 +255,10 @@
</script>
<style lang="less">
@analysis-dark-bg: rgb(20, 20, 20);
@analysis-dark-hover-bg: rgb(30 41 59);
@analysis-dark-shadow: 0 10px 24px rgb(0 0 0 / 24%);
.notice-card {
width: 100%;
height: 100%;
@@ -568,7 +572,7 @@
}
html[data-theme='dark'] .notice-card {
background: rgb(20, 20, 20);
background: @analysis-dark-bg;
.card-title {
color: rgb(203 213 225);
@@ -593,15 +597,27 @@
.table-container {
.el-table {
--el-table-border-color: transparent;
--el-table-header-bg-color: rgb(20, 20, 20);
--el-table-header-bg-color: @analysis-dark-bg;
--el-table-tr-bg-color: transparent;
--el-table-row-hover-bg-color: rgb(30 41 59);
--el-table-row-hover-bg-color: @analysis-dark-hover-bg;
--el-table-text-color: rgb(148 163 184);
--el-table-header-text-color: rgb(226 232 240);
background: transparent;
th.el-table__cell {
background: rgb(20, 20, 20);
background: @analysis-dark-bg;
}
.el-loading-mask {
background-color: rgb(20 20 20 / 72%);
}
.el-loading-spinner .path {
stroke: rgb(147 197 253);
}
.el-loading-spinner .el-loading-text {
color: rgb(203 213 225);
}
th.el-table__cell,
@@ -629,7 +645,7 @@
&__form-panel,
&__content,
&__section-title {
background: rgb(20, 20, 20);
background: @analysis-dark-bg;
}
&__content {
@@ -649,7 +665,7 @@
&__textarea :deep(.el-textarea__inner) {
border: 1px solid rgb(71 85 105) !important;
border-color: rgb(71 85 105);
background: rgb(20, 20, 20);
background: @analysis-dark-bg;
color: rgb(226 232 240);
box-shadow: none !important;
}
@@ -668,38 +684,38 @@
&__textarea :deep(.el-input__count) {
color: rgb(148 163 184);
background: rgb(20, 20, 20);
background: @analysis-dark-bg;
}
}
}
html[data-theme='dark'] .todo-info-dialog {
--el-bg-color: rgb(20, 20, 20);
--el-dialog-bg-color: rgb(20, 20, 20);
--el-fill-color-blank: rgb(20, 20, 20);
--el-bg-color: @analysis-dark-bg;
--el-dialog-bg-color: @analysis-dark-bg;
--el-fill-color-blank: @analysis-dark-bg;
.el-dialog {
background: rgb(20, 20, 20) !important;
--el-dialog-bg-color: rgb(20, 20, 20);
--el-bg-color: rgb(20, 20, 20);
--el-fill-color-blank: rgb(20, 20, 20);
box-shadow: 0 14px 36px rgb(0 0 0 / 42%);
background: @analysis-dark-bg !important;
--el-dialog-bg-color: @analysis-dark-bg;
--el-bg-color: @analysis-dark-bg;
--el-fill-color-blank: @analysis-dark-bg;
box-shadow: @analysis-dark-shadow;
}
.el-dialog__wrapper,
.el-overlay-dialog,
.el-dialog__content {
background: rgb(20, 20, 20) !important;
background: @analysis-dark-bg !important;
}
.el-dialog__header {
border-bottom: 1px solid rgb(51 65 85) !important;
background: rgb(20, 20, 20) !important;
background: @analysis-dark-bg !important;
}
.el-dialog__body,
.el-dialog__footer {
background: rgb(20, 20, 20) !important;
background: @analysis-dark-bg !important;
}
.el-textarea,
@@ -708,8 +724,8 @@
--el-input-border-color: rgb(71 85 105);
--el-input-hover-border-color: rgb(96 165 250);
--el-input-focus-border-color: rgb(96 165 250);
--el-fill-color-blank: rgb(20, 20, 20);
background: rgb(20, 20, 20) !important;
--el-fill-color-blank: @analysis-dark-bg;
background: @analysis-dark-bg !important;
box-shadow: none !important;
}
@@ -728,8 +744,8 @@
.el-dialog__header,
.el-dialog__body,
.el-dialog__footer {
--el-bg-color: rgb(20, 20, 20);
--el-fill-color-blank: rgb(20, 20, 20);
--el-bg-color: @analysis-dark-bg;
--el-fill-color-blank: @analysis-dark-bg;
}
.el-divider {
@@ -742,26 +758,26 @@
.todo-dialog__footer {
.el-button:not(.el-button--primary) {
border-color: rgb(71 85 105);
background: rgb(30 41 59);
border-color: rgb(96 165 250);
background: @analysis-dark-bg;
color: rgb(226 232 240);
}
.el-button:not(.el-button--primary):hover {
border-color: rgb(96 165 250);
background: rgb(37 99 235 / 22%);
background: @analysis-dark-hover-bg;
color: rgb(241 245 249);
}
.el-button--primary {
border-color: rgb(59 130 246);
background: rgb(37 99 235);
border-color: rgb(96 165 250);
background: @analysis-dark-bg;
color: rgb(248 250 252);
}
.el-button--primary:hover {
border-color: rgb(96 165 250);
background: rgb(59 130 246);
background: @analysis-dark-hover-bg;
}
}
}

View File

@@ -43,6 +43,12 @@
</script>
<style lang="less">
@dark-bg: #141414;
@desktop-page-gap: 12px;
@desktop-page-padding: 0;
@desktop-card-radius: 10px;
@desktop-card-border: 1px solid rgb(226 232 240);
@desktop-card-shadow: 0 1px 3px rgb(15 23 42 / 0.06);
@desktop-dark-border: rgb(51 65 85);
.analysis-page-wrapper {
display: flex;
@@ -97,18 +103,19 @@
--analysis-card-title-padding: 8px 16px;
--analysis-card-content-padding: 8px 12px 12px;
--analysis-card-item-gap: 8px;
--analysis-card-radius: 10px;
--analysis-gap: @desktop-page-gap;
--analysis-card-radius: @desktop-card-radius;
display: flex;
width: 100%;
height: 100%;
max-height: 100%;
min-height: 0;
gap: var(--analysis-gap);
padding: 0;
padding: @desktop-page-padding;
box-sizing: border-box;
overflow: hidden;
background: transparent;
border-radius: 10px;
border-radius: @desktop-card-radius;
}
.mySpring-analysis .analysis-left,
@@ -148,16 +155,16 @@
min-width: 0;
padding: 0;
border-radius: var(--analysis-card-radius);
border: 1px solid rgb(226 232 240);
border: @desktop-card-border;
background: rgb(255, 255, 255);
box-shadow: 0 1px 3px rgb(15 23 42 / 0.06);
box-shadow: @desktop-card-shadow;
overflow: hidden;
color: rgb(71 85 105);
font-size: 16px;
}
html[data-theme='dark'] .mySpring-analysis .analysis-panel {
border-color: rgb(51 65 85);
border-color: @desktop-dark-border;
background: @dark-bg !important;
color: rgb(203 213 225);
box-shadow: none;
@@ -171,7 +178,7 @@
}
html[data-theme='dark'] .mySpring-analysis {
border-radius: 10px;
border-radius: @desktop-card-radius;
}
html[data-theme='dark'] .mySpring-analysis,

View File

@@ -0,0 +1,585 @@
<template>
<div ref="contractCardRef" class="contract-info-card">
<div class="card-title">
<span>项目合同</span>
<el-button class="card-title__more" link type="primary" @click="goMore">更多</el-button>
</div>
<div class="card-content">
<div class="contract-overview">
<div class="task-chart-panel task-chart-panel--pie">
<div ref="pieChartRef" class="task-chart"></div>
</div>
<div class="task-chart-panel task-chart-panel--bar">
<div ref="barChartRef" class="task-chart"></div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { computed, nextTick, onMounted, onUnmounted, ref } from 'vue';
import { useRouter } from 'vue-router';
import * as echarts from 'echarts';
import { MyProjectContract, myProjectContractListAll } from '@jeesite/biz/api/biz/myProjectContract';
import { DictData, dictDataListData } from '@jeesite/core/api/sys/dictData';
import { useUserStore } from '@jeesite/core/store/modules/user';
import { firstCurrentYear, formatToDate } from '@jeesite/core/utils/dateUtil';
const router = useRouter();
const userStore = useUserStore();
const userinfo = computed(() => userStore.getUserInfo);
const contractCardRef = ref<HTMLElement>();
const pieChartRef = ref<HTMLElement>();
const barChartRef = ref<HTMLElement>();
const sourceData = ref<MyProjectContract[]>([]);
const statusDict = ref<DictData[]>([]);
const STATUS_COLORS = ['#3B82F6', '#10B981', '#F97316', '#EC4899', '#8B5CF6', '#06B6D4'];
let pieChartInstance: echarts.ECharts | null = null;
let barChartInstance: echarts.ECharts | null = null;
let resizeObserver: ResizeObserver | null = null;
let themeObserver: MutationObserver | null = null;
function goMore() {
router.push('/biz/myProjectContract/list');
}
const pieChartData = computed(() => {
if (!statusDict.value.length) {
const fallbackMap = new Map<string, number>();
sourceData.value.forEach((item) => {
const label = item.contractStatus || '未设置';
fallbackMap.set(label, (fallbackMap.get(label) || 0) + 1);
});
return Array.from(fallbackMap.entries()).map(([name, value], index) => ({
name,
value,
itemStyle: { color: STATUS_COLORS[index % STATUS_COLORS.length] },
}));
}
const countMap = new Map<string, number>();
sourceData.value.forEach((item) => {
const key = item.contractStatus || 'unknown';
countMap.set(key, (countMap.get(key) || 0) + 1);
});
return statusDict.value
.map((item, index) => ({
name: item.dictLabelRaw,
value: countMap.get(item.dictValue || '') || 0,
itemStyle: { color: STATUS_COLORS[index % STATUS_COLORS.length] },
}))
.filter((item) => item.value > 0);
});
const barChartData = computed(() => {
const statusItems = statusDict.value.length
? statusDict.value.map((item, index) => ({
key: item.dictValue,
label: item.dictLabelRaw,
color: STATUS_COLORS[index % STATUS_COLORS.length],
}))
: [];
const monthStatusMap = new Map<string, Map<string, number>>();
const monthOrderMap = new Map<string, number>();
sourceData.value.forEach((item) => {
if (!item.signDate) return;
const date = new Date(item.signDate);
if (Number.isNaN(date.getTime())) return;
const monthLabel = `${date.getMonth() + 1}`;
monthOrderMap.set(monthLabel, date.getMonth() + 1);
const statusKey = item.contractStatus || 'unknown';
if (!monthStatusMap.has(monthLabel)) {
monthStatusMap.set(monthLabel, new Map<string, number>());
}
const monthMap = monthStatusMap.get(monthLabel)!;
monthMap.set(statusKey, (monthMap.get(statusKey) || 0) + 1);
});
const months = Array.from(monthStatusMap.keys()).sort((a, b) => {
return (monthOrderMap.get(a) || 0) - (monthOrderMap.get(b) || 0);
});
const totalByMonth = months.map((month) => {
const monthMap = monthStatusMap.get(month);
return Array.from(monthMap?.values() || []).reduce((sum, count) => sum + count, 0);
});
const fallbackStatusMap = new Map<string, number>();
sourceData.value.forEach((item) => {
if (!statusDict.value.length) {
const key = item.contractStatus || '未设置';
fallbackStatusMap.set(key, (fallbackStatusMap.get(key) || 0) + 1);
}
});
const fallbackSeries = Array.from(fallbackStatusMap.keys()).map((key, index) => ({
key,
label: key,
color: STATUS_COLORS[index % STATUS_COLORS.length],
}));
return {
months,
totals: totalByMonth,
statusItems: statusItems.length ? statusItems : fallbackSeries,
monthStatusMap,
};
});
async function getDict() {
try {
statusDict.value = await dictDataListData({ dictType: 'contract_status' });
} catch (error) {
statusDict.value = [];
}
}
async function getList() {
try {
const reqParams = {
createTime_gte: firstCurrentYear(),
createUser: userinfo.value.loginCode,
};
const res = await myProjectContractListAll(reqParams);
sourceData.value = res || [];
} catch (error) {
sourceData.value = [];
}
}
function renderPieChart() {
if (!pieChartRef.value) return;
if (!pieChartInstance) {
pieChartInstance = echarts.init(pieChartRef.value);
}
const isDark = document.documentElement.getAttribute('data-theme') === 'dark';
const total = pieChartData.value.reduce((sum, item) => sum + Number(item.value || 0), 0);
const data = total
? pieChartData.value
: statusDict.value.map((item, index) => ({
name: item.dictLabelRaw,
value: 0,
itemStyle: { color: STATUS_COLORS[index % STATUS_COLORS.length] },
}));
const legendData = statusDict.value.length
? statusDict.value.map((item) => item.dictLabelRaw)
: pieChartData.value.map((item) => item.name);
const series: echarts.SeriesOption[] = [];
if (!total) {
series.push({
name: '空态环',
type: 'pie',
radius: ['46%', '72%'],
center: ['50%', '44%'],
silent: true,
legendHoverLink: false,
tooltip: {
show: false,
},
label: {
show: false,
},
labelLine: {
show: false,
},
itemStyle: {
color: isDark ? 'rgba(148, 163, 184, 0.18)' : 'rgba(203, 213, 225, 0.55)',
borderRadius: 10,
borderColor: isDark ? '#141414' : '#ffffff',
borderWidth: 3,
},
data: [{ name: 'empty-ring', value: 1 }],
});
}
series.push({
name: '合同状态',
type: 'pie',
radius: total ? ['46%', '72%'] : [0, 0],
center: ['50%', '44%'],
avoidLabelOverlap: true,
minAngle: total ? 4 : 0,
itemStyle: {
borderRadius: 10,
borderColor: isDark ? '#141414' : '#ffffff',
borderWidth: 3,
},
label: {
show: total,
formatter: ({ name, value, percent }) => (total ? `${name}\n${value}项 / ${percent}%` : ''),
color: isDark ? '#e2e8f0' : '#334155',
fontSize: 12,
},
emphasis: {
scale: true,
scaleSize: 6,
},
data,
silent: !total,
labelLine: {
show: total,
},
});
pieChartInstance.setOption({
tooltip: {
trigger: 'item',
backgroundColor: isDark ? 'rgba(20, 20, 20, 0.96)' : 'rgba(255, 255, 255, 0.96)',
borderColor: isDark ? 'rgb(51 65 85)' : 'rgb(226 232 240)',
borderWidth: 1,
textStyle: {
color: isDark ? '#e2e8f0' : '#334155',
},
formatter: ({ name, value, percent }) => {
if (!total) return `${name}<br/>数量0`;
return `${name}<br/>数量:${value}<br/>占比:${percent}%`;
},
},
legend: {
show: legendData.length > 0,
bottom: 4,
left: 'center',
itemWidth: 10,
itemHeight: 10,
data: legendData,
textStyle: {
color: isDark ? '#cbd5e1' : '#475569',
fontSize: 12,
},
},
series,
graphic: total
? [
{
type: 'text',
left: 'center',
top: '34%',
style: {
text: '合同总数',
fill: isDark ? '#94a3b8' : '#64748b',
fontSize: 12,
},
},
{
type: 'text',
left: 'center',
top: '41%',
style: {
text: `${total}`,
fill: isDark ? '#f8fafc' : '#0f172a',
fontSize: 24,
fontWeight: 700,
},
},
]
: [],
});
}
function renderBarChart() {
if (!barChartRef.value) return;
if (!barChartInstance) {
barChartInstance = echarts.init(barChartRef.value);
}
const isDark = document.documentElement.getAttribute('data-theme') === 'dark';
const { months, totals, statusItems, monthStatusMap } = barChartData.value;
const hasData = totals.some((item) => item > 0);
const categories = months.length ? months : [];
const series = statusItems.length
? statusItems.map((status) => ({
name: status.label,
type: 'bar',
stack: 'month-total',
barWidth: '12%',
itemStyle: {
color: status.color,
borderRadius: 0,
},
emphasis: {
focus: 'series',
},
label: {
show: true,
position: 'inside',
formatter: ({ value }) => (Number(value) > 0 ? `${value}` : ''),
color: '#ffffff',
fontSize: 11,
},
data: categories.map((month) => monthStatusMap.get(month)?.get(status.key) || 0),
}))
: [
{
type: 'bar',
barWidth: '12%',
itemStyle: {
color: isDark ? '#334155' : '#E2E8F0',
borderRadius: [10, 10, 0, 0],
},
data: categories.map(() => 0),
},
];
if (hasData) {
series.push({
name: '总数',
type: 'bar',
stack: 'month-total',
silent: true,
legendHoverLink: false,
itemStyle: {
color: 'rgba(0,0,0,0)',
},
tooltip: {
show: false,
},
label: {
show: true,
position: 'top',
color: isDark ? '#cbd5e1' : '#475569',
fontSize: 11,
formatter: ({ dataIndex }) => (totals[dataIndex] > 0 ? `${totals[dataIndex]}` : ''),
},
data: categories.map(() => 0),
z: 10,
});
}
barChartInstance.setOption({
grid: {
left: 12,
right: 12,
top: 52,
bottom: 10,
containLabel: true,
},
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow',
},
backgroundColor: isDark ? 'rgba(20, 20, 20, 0.96)' : 'rgba(255, 255, 255, 0.96)',
borderColor: isDark ? 'rgb(51 65 85)' : 'rgb(226 232 240)',
borderWidth: 1,
textStyle: {
color: isDark ? '#e2e8f0' : '#334155',
},
formatter: (params) => {
const dataIndex = params[0]?.dataIndex || 0;
if (!hasData) return '';
const lines = params
.filter((item) => item.seriesName !== '总数' && Number(item.value) > 0)
.map((item) => `${item.marker}${item.seriesName}${item.value}`);
lines.push(`总数:${totals[dataIndex]}`);
return `${categories[dataIndex]}<br/>${lines.join('<br/>')}`;
},
},
legend: {
top: 6,
left: 'center',
itemGap: 16,
data: statusItems.map((item) => item.label),
textStyle: {
color: isDark ? '#e2e8f0' : '#475569',
},
},
xAxis: {
type: 'category',
data: categories,
boundaryGap: ['2%', '2%'],
axisTick: {
alignWithLabel: true,
},
axisLine: {
lineStyle: {
color: isDark ? '#475569' : '#cbd5e1',
},
},
axisLabel: {
color: isDark ? '#cbd5e1' : '#64748b',
interval: 0,
margin: 8,
rotate: 30,
},
},
yAxis: {
type: 'value',
minInterval: 1,
name: '数量',
nameTextStyle: {
color: isDark ? '#94a3b8' : '#64748b',
padding: [0, 0, 2, 0],
},
splitLine: {
lineStyle: {
color: isDark ? 'rgba(71, 85, 105, 0.35)' : 'rgba(203, 213, 225, 0.55)',
},
},
axisLabel: {
color: isDark ? '#94a3b8' : '#64748b',
},
},
series,
});
}
function renderCharts() {
renderPieChart();
renderBarChart();
}
function resizeCharts() {
pieChartInstance?.resize();
barChartInstance?.resize();
}
onMounted(async () => {
await Promise.all([getDict(), getList()]);
await nextTick();
renderCharts();
if (contractCardRef.value) {
resizeObserver = new ResizeObserver(() => {
resizeCharts();
});
resizeObserver.observe(contractCardRef.value);
}
window.addEventListener('resize', resizeCharts);
themeObserver = new MutationObserver(() => {
nextTick(() => {
renderCharts();
});
});
themeObserver.observe(document.documentElement, {
attributes: true,
attributeFilter: ['data-theme'],
});
});
onUnmounted(() => {
resizeObserver?.disconnect();
themeObserver?.disconnect();
window.removeEventListener('resize', resizeCharts);
pieChartInstance?.dispose();
barChartInstance?.dispose();
pieChartInstance = null;
barChartInstance = null;
});
</script>
<style lang="less">
@workbench-dark-panel-bg: rgb(20, 20, 20);
@workbench-dark-shadow: 0 10px 24px rgb(0 0 0 / 24%);
.contract-info-card {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
box-sizing: border-box;
overflow: hidden;
.card-title {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 16px;
font-size: 14px;
font-weight: 500;
line-height: 20px;
color: rgb(51 65 85);
border-bottom: 1px solid rgb(226 232 240);
background: transparent;
&__more {
padding: 0;
font-size: 13px;
}
}
.card-content {
flex: 1;
min-height: 0;
padding: 16px;
overflow: hidden;
background: transparent;
}
.contract-overview {
display: grid;
grid-template-columns: minmax(220px, 0.9fr) minmax(0, 1.6fr);
gap: 12px;
height: 100%;
min-height: 0;
}
.task-chart-panel {
display: flex;
flex-direction: column;
min-width: 0;
min-height: 0;
padding: 12px;
border-radius: 12px;
background: rgb(255, 255, 255);
box-shadow: 0 8px 24px rgb(148 163 184 / 14%);
}
.task-chart {
flex: 1;
min-height: 0;
width: 100%;
}
}
html[data-theme='dark'] .contract-info-card {
.card-title {
color: rgb(203 213 225);
border-bottom-color: rgb(51 65 85);
&__more:deep(.el-button) {
color: rgb(147 197 253);
}
}
.task-chart-panel {
background: @workbench-dark-panel-bg;
box-shadow: @workbench-dark-shadow;
}
}
@media (max-width: 1100px) {
.contract-info-card {
.contract-overview {
grid-template-columns: 1fr;
}
}
}
@media (max-width: 768px) {
.contract-info-card {
.card-content {
padding: 12px;
}
.task-chart-panel {
padding: 10px;
}
.task-chart {
min-height: 220px;
}
}
}
</style>

View File

@@ -331,6 +331,10 @@
</script>
<style lang="less">
@workbench-dark-panel-bg: rgb(20, 20, 20);
@workbench-dark-solid-bg: rgb(20, 20, 20);
@workbench-dark-shadow: 0 10px 24px rgb(0 0 0 / 24%);
.note-card {
width: 100%;
height: 100%;
@@ -508,14 +512,14 @@
.metric-item,
.note-chart-panel {
background: linear-gradient(180deg, rgb(20, 20, 20) 0%, rgb(28 28 28) 100%);
box-shadow: 0 10px 24px rgb(0 0 0 / 24%);
background: @workbench-dark-panel-bg;
box-shadow: @workbench-dark-shadow;
}
.metric-item {
&__pane {
background: linear-gradient(180deg, rgb(20, 20, 20) 0%, rgb(28 28 28) 100%);
box-shadow: 0 10px 24px rgb(0 0 0 / 24%);
background: @workbench-dark-panel-bg;
box-shadow: @workbench-dark-shadow;
}
&__extra,
@@ -525,12 +529,12 @@
&__label {
border-color: rgb(51 65 85);
background: rgb(20, 20, 20);
background: @workbench-dark-solid-bg;
}
&__label--active {
border-color: rgb(59 130 246 / 40%);
background: rgb(37 99 235 / 14%) !important;
background: @workbench-dark-solid-bg !important;
color: rgb(191 219 254) !important;
}
}

View File

@@ -0,0 +1,612 @@
<template>
<div ref="projectTaskCardRef" class="project-task-card">
<div class="card-title">
<span>项目任务</span>
<el-button class="card-title__more" link type="primary" @click="goMore">更多</el-button>
</div>
<div class="card-content">
<div class="project-task-overview">
<div class="task-chart-panel task-chart-panel--pie">
<div ref="pieChartRef" class="task-chart"></div>
</div>
<div class="task-chart-panel task-chart-panel--bar">
<div ref="barChartRef" class="task-chart"></div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { computed, nextTick, onMounted, onUnmounted, ref } from 'vue';
import { useRouter } from 'vue-router';
import * as echarts from 'echarts';
import { MyProjectTask, myProjectTaskListAll } from '@jeesite/biz/api/biz/myProjectTask';
import { DictData, dictDataListData } from '@jeesite/core/api/sys/dictData';
import { useUserStore } from '@jeesite/core/store/modules/user';
import { firstCurrentYear, formatToDate } from '@jeesite/core/utils/dateUtil';
const router = useRouter();
const userStore = useUserStore();
const userinfo = computed(() => userStore.getUserInfo);
const projectTaskCardRef = ref<HTMLElement>();
const pieChartRef = ref<HTMLElement>();
const barChartRef = ref<HTMLElement>();
const sourceData = ref<MyProjectTask[]>([]);
const statusDict = ref<DictData[]>([]);
const STATUS_COLORS = ['#3B82F6', '#10B981', '#F97316', '#EC4899', '#8B5CF6', '#06B6D4'];
let pieChartInstance: echarts.ECharts | null = null;
let barChartInstance: echarts.ECharts | null = null;
let resizeObserver: ResizeObserver | null = null;
let themeObserver: MutationObserver | null = null;
function goMore() {
router.push('/biz/myProjectTask/list');
}
const pieChartData = computed(() => {
if (!statusDict.value.length) {
const fallbackMap = new Map<string, number>();
sourceData.value.forEach((item) => {
const label = item.taskStatus || '未设置';
fallbackMap.set(label, (fallbackMap.get(label) || 0) + 1);
});
return Array.from(fallbackMap.entries()).map(([name, value], index) => ({
name,
value,
itemStyle: { color: STATUS_COLORS[index % STATUS_COLORS.length] },
}));
}
const countMap = new Map<string, number>();
sourceData.value.forEach((item) => {
const key = item.taskStatus || 'unknown';
countMap.set(key, (countMap.get(key) || 0) + 1);
});
return statusDict.value
.map((item, index) => ({
name: item.dictLabelRaw,
value: countMap.get(item.dictValue || '') || 0,
itemStyle: { color: STATUS_COLORS[index % STATUS_COLORS.length] },
}))
.filter((item) => item.value > 0);
});
const barChartData = computed(() => {
const statusItems = statusDict.value.length
? statusDict.value.map((item, index) => ({
key: item.dictValue,
label: item.dictLabelRaw,
color: STATUS_COLORS[index % STATUS_COLORS.length],
}))
: [];
const monthStatusMap = new Map<string, Map<string, number>>();
const monthOrderMap = new Map<string, number>();
sourceData.value.forEach((item) => {
if (!item.createTime) return;
const date = new Date(item.createTime);
if (Number.isNaN(date.getTime())) return;
const monthLabel = `${date.getMonth() + 1}`;
monthOrderMap.set(monthLabel, date.getMonth() + 1);
const statusKey = item.taskStatus || 'unknown';
if (!monthStatusMap.has(monthLabel)) {
monthStatusMap.set(monthLabel, new Map<string, number>());
}
const monthMap = monthStatusMap.get(monthLabel)!;
monthMap.set(statusKey, (monthMap.get(statusKey) || 0) + 1);
});
const months = Array.from(monthStatusMap.keys()).sort((a, b) => {
return (monthOrderMap.get(a) || 0) - (monthOrderMap.get(b) || 0);
});
const totalByMonth = months.map((month) => {
const monthMap = monthStatusMap.get(month);
return Array.from(monthMap?.values() || []).reduce((sum, count) => sum + count, 0);
});
const fallbackStatusMap = new Map<string, number>();
sourceData.value.forEach((item) => {
if (!statusDict.value.length) {
const key = item.taskStatus || '未设置';
fallbackStatusMap.set(key, (fallbackStatusMap.get(key) || 0) + 1);
}
});
const fallbackSeries = Array.from(fallbackStatusMap.keys()).map((key, index) => ({
key,
label: key,
color: STATUS_COLORS[index % STATUS_COLORS.length],
}));
return {
months,
totals: totalByMonth,
statusItems: statusItems.length ? statusItems : fallbackSeries,
monthStatusMap,
};
});
async function getDict() {
try {
statusDict.value = await dictDataListData({ dictType: 'task_status' });
} catch (error) {
statusDict.value = [];
}
}
async function getList() {
try {
const reqParams = {
createTime_gte: firstCurrentYear(),
createUser: userinfo.value.loginCode,
};
const res = await myProjectTaskListAll(reqParams);
sourceData.value = res || [];
} catch (error) {
sourceData.value = [];
}
}
function renderPieChart() {
if (!pieChartRef.value) return;
if (!pieChartInstance) {
pieChartInstance = echarts.init(pieChartRef.value);
}
const isDark = document.documentElement.getAttribute('data-theme') === 'dark';
const total = pieChartData.value.reduce((sum, item) => sum + Number(item.value || 0), 0);
const data = total
? pieChartData.value
: statusDict.value.map((item, index) => ({
name: item.dictLabelRaw,
value: 0,
itemStyle: { color: STATUS_COLORS[index % STATUS_COLORS.length] },
}));
const legendData = statusDict.value.length
? statusDict.value.map((item) => item.dictLabelRaw)
: pieChartData.value.map((item) => item.name);
const series: echarts.SeriesOption[] = [];
if (!total) {
series.push({
name: '空态环',
type: 'pie',
radius: ['46%', '72%'],
center: ['50%', '44%'],
silent: true,
legendHoverLink: false,
tooltip: {
show: false,
},
label: {
show: false,
},
labelLine: {
show: false,
},
itemStyle: {
color: isDark ? 'rgba(148, 163, 184, 0.18)' : 'rgba(203, 213, 225, 0.55)',
borderRadius: 10,
borderColor: isDark ? '#141414' : '#ffffff',
borderWidth: 3,
},
data: [{ name: 'empty-ring', value: 1 }],
});
}
series.push({
name: '任务状态',
type: 'pie',
radius: total ? ['46%', '72%'] : [0, 0],
center: ['50%', '44%'],
avoidLabelOverlap: true,
minAngle: total ? 4 : 0,
itemStyle: {
borderRadius: 10,
borderColor: isDark ? '#141414' : '#ffffff',
borderWidth: 3,
},
label: {
show: total,
formatter: ({ name, value, percent }) => (total ? `${name}\n${value}项 / ${percent}%` : ''),
color: isDark ? '#e2e8f0' : '#334155',
fontSize: 12,
},
emphasis: {
scale: true,
scaleSize: 6,
},
data,
silent: !total,
labelLine: {
show: total,
},
});
pieChartInstance.setOption({
tooltip: {
trigger: 'item',
backgroundColor: isDark ? 'rgba(20, 20, 20, 0.96)' : 'rgba(255, 255, 255, 0.96)',
borderColor: isDark ? 'rgb(51 65 85)' : 'rgb(226 232 240)',
borderWidth: 1,
textStyle: {
color: isDark ? '#e2e8f0' : '#334155',
},
formatter: ({ name, value, percent }) => {
if (!total) return `${name}<br/>数量0`;
return `${name}<br/>数量:${value}<br/>占比:${percent}%`;
},
},
legend: {
show: legendData.length > 0,
bottom: 4,
left: 'center',
itemWidth: 10,
itemHeight: 10,
data: legendData,
textStyle: {
color: isDark ? '#cbd5e1' : '#475569',
fontSize: 12,
},
},
series,
graphic: total
? [
{
type: 'text',
left: 'center',
top: '34%',
style: {
text: '任务总数',
fill: isDark ? '#94a3b8' : '#64748b',
fontSize: 12,
},
},
{
type: 'text',
left: 'center',
top: '41%',
style: {
text: `${total}`,
fill: isDark ? '#f8fafc' : '#0f172a',
fontSize: 24,
fontWeight: 700,
},
},
]
: [],
});
}
function renderBarChart() {
if (!barChartRef.value) return;
if (!barChartInstance) {
barChartInstance = echarts.init(barChartRef.value);
}
const isDark = document.documentElement.getAttribute('data-theme') === 'dark';
const { months, totals, statusItems, monthStatusMap } = barChartData.value;
const hasData = totals.some((item) => item > 0);
const categories = months.length ? months : [];
const series = statusItems.length
? statusItems.map((status) => ({
name: status.label,
type: 'bar',
stack: 'month-total',
barWidth: '12%',
itemStyle: {
color: status.color,
borderRadius: 0,
},
emphasis: {
focus: 'series',
},
label: {
show: true,
position: 'inside',
formatter: ({ value }) => (Number(value) > 0 ? `${value}` : ''),
color: '#ffffff',
fontSize: 11,
},
data: categories.map((month) => monthStatusMap.get(month)?.get(status.key) || 0),
}))
: [
{
type: 'bar',
barWidth: '12%',
itemStyle: {
color: isDark ? '#334155' : '#E2E8F0',
borderRadius: [10, 10, 0, 0],
},
data: categories.map(() => 0),
},
];
if (hasData) {
series.push({
name: '总数',
type: 'bar',
stack: 'month-total',
silent: true,
legendHoverLink: false,
itemStyle: {
color: 'rgba(0,0,0,0)',
},
tooltip: {
show: false,
},
label: {
show: true,
position: 'top',
color: isDark ? '#cbd5e1' : '#475569',
fontSize: 11,
formatter: ({ dataIndex }) => (totals[dataIndex] > 0 ? `${totals[dataIndex]}` : ''),
},
data: categories.map(() => 0),
z: 10,
});
}
barChartInstance.setOption({
grid: {
left: 12,
right: 12,
top: 52,
bottom: 10,
containLabel: true,
},
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow',
},
backgroundColor: isDark ? 'rgba(20, 20, 20, 0.96)' : 'rgba(255, 255, 255, 0.96)',
borderColor: isDark ? 'rgb(51 65 85)' : 'rgb(226 232 240)',
borderWidth: 1,
textStyle: {
color: isDark ? '#e2e8f0' : '#334155',
},
formatter: (params) => {
const dataIndex = params[0]?.dataIndex || 0;
if (!hasData) return '';
const lines = params
.filter((item) => item.seriesName !== '总数' && Number(item.value) > 0)
.map((item) => `${item.marker}${item.seriesName}${item.value}`);
lines.push(`总数:${totals[dataIndex]}`);
return `${categories[dataIndex]}<br/>${lines.join('<br/>')}`;
},
},
legend: {
top: 6,
left: 'center',
itemGap: 16,
data: statusItems.map((item) => item.label),
textStyle: {
color: isDark ? '#e2e8f0' : '#475569',
},
},
xAxis: {
type: 'category',
data: categories,
boundaryGap: ['2%', '2%'],
axisTick: {
alignWithLabel: true,
},
axisLine: {
lineStyle: {
color: isDark ? '#475569' : '#cbd5e1',
},
},
axisLabel: {
color: isDark ? '#cbd5e1' : '#64748b',
interval: 0,
margin: 8,
rotate: 30,
},
},
yAxis: {
type: 'value',
name: '数量',
nameTextStyle: {
color: isDark ? '#94a3b8' : '#64748b',
padding: [0, 0, 2, 0],
},
splitLine: {
lineStyle: {
color: isDark ? 'rgba(71, 85, 105, 0.35)' : 'rgba(203, 213, 225, 0.55)',
},
},
axisLabel: {
color: isDark ? '#94a3b8' : '#64748b',
},
},
series,
});
}
function renderCharts() {
renderPieChart();
renderBarChart();
}
function resizeCharts() {
pieChartInstance?.resize();
barChartInstance?.resize();
}
onMounted(async () => {
await Promise.all([getDict(), getList()]);
await nextTick();
renderCharts();
if (projectTaskCardRef.value) {
resizeObserver = new ResizeObserver(() => {
resizeCharts();
});
resizeObserver.observe(projectTaskCardRef.value);
}
window.addEventListener('resize', resizeCharts);
themeObserver = new MutationObserver(() => {
nextTick(() => {
renderCharts();
});
});
themeObserver.observe(document.documentElement, {
attributes: true,
attributeFilter: ['data-theme'],
});
});
onUnmounted(() => {
resizeObserver?.disconnect();
themeObserver?.disconnect();
window.removeEventListener('resize', resizeCharts);
pieChartInstance?.dispose();
barChartInstance?.dispose();
pieChartInstance = null;
barChartInstance = null;
});
</script>
<style lang="less">
@workbench-dark-panel-bg: rgb(20, 20, 20);
@workbench-dark-shadow: 0 10px 24px rgb(0 0 0 / 24%);
.project-task-card {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
box-sizing: border-box;
overflow: hidden;
.card-title {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 16px;
font-size: 14px;
font-weight: 500;
line-height: 20px;
color: rgb(51 65 85);
border-bottom: 1px solid rgb(226 232 240);
background: transparent;
&__more {
padding: 0;
font-size: 13px;
}
}
.card-content {
flex: 1;
min-height: 0;
padding: 16px;
overflow: hidden;
background: transparent;
}
.project-task-overview {
display: grid;
grid-template-columns: minmax(220px, 0.9fr) minmax(0, 1.6fr);
gap: 12px;
height: 100%;
min-height: 0;
}
.task-chart-panel {
display: flex;
flex-direction: column;
min-width: 0;
min-height: 0;
padding: 12px;
border-radius: 12px;
background: rgb(255, 255, 255);
box-shadow: 0 8px 24px rgb(148 163 184 / 14%);
}
.panel-header {
display: flex;
flex-direction: column;
gap: 2px;
margin-bottom: 8px;
}
.panel-title {
color: rgb(51 65 85);
font-size: 14px;
font-weight: 600;
line-height: 20px;
}
.panel-subtitle {
color: rgb(100 116 139);
font-size: 12px;
line-height: 16px;
}
.task-chart {
flex: 1;
min-height: 0;
width: 100%;
}
}
html[data-theme='dark'] .project-task-card {
.card-title {
color: rgb(203 213 225);
border-bottom-color: rgb(51 65 85);
&__more:deep(.el-button) {
color: rgb(147 197 253);
}
}
.task-chart-panel {
background: @workbench-dark-panel-bg;
box-shadow: @workbench-dark-shadow;
}
.panel-title {
color: rgb(226 232 240);
}
.panel-subtitle {
color: rgb(148 163 184);
}
}
@media (max-width: 1100px) {
.project-task-card {
.project-task-overview {
grid-template-columns: 1fr;
}
}
}
@media (max-width: 768px) {
.project-task-card {
.card-content {
padding: 12px;
}
.task-chart-panel {
padding: 10px;
}
.task-chart {
min-height: 220px;
}
}
}
</style>

View File

@@ -314,6 +314,10 @@
</script>
<style lang="less">
@workbench-dark-panel-bg: rgb(20, 20, 20);
@workbench-dark-solid-bg: rgb(20, 20, 20);
@workbench-dark-shadow: 0 10px 24px rgb(0 0 0 / 24%);
.schedule-card {
width: 100%;
height: 100%;
@@ -700,14 +704,14 @@
.summary-item,
.schedule-panel {
background: linear-gradient(180deg, rgb(20, 20, 20) 0%, rgb(28 28 28) 100%);
box-shadow: 0 10px 24px rgb(0 0 0 / 24%);
background: @workbench-dark-panel-bg;
box-shadow: @workbench-dark-shadow;
}
.summary-item {
&__pane {
background: linear-gradient(180deg, rgb(20, 20, 20) 0%, rgb(28 28 28) 100%);
box-shadow: 0 10px 24px rgb(0 0 0 / 24%);
background: @workbench-dark-panel-bg;
box-shadow: @workbench-dark-shadow;
}
&__extra,
@@ -717,7 +721,7 @@
&__label {
border-color: rgb(51 65 85);
background: rgb(20, 20, 20);
background: @workbench-dark-solid-bg;
}
}
@@ -734,12 +738,12 @@
.panel-tag,
.timeline-scroll {
background: linear-gradient(180deg, rgb(20, 20, 20) 0%, rgb(28 28 28) 100%);
box-shadow: 0 10px 24px rgb(0 0 0 / 24%);
background: @workbench-dark-panel-bg;
box-shadow: @workbench-dark-shadow;
}
.timeline-row__hint {
background: rgb(30 41 59 / 75%);
background: @workbench-dark-solid-bg;
}
.timeline-row__hint-title {
@@ -756,7 +760,7 @@
.timeline-event {
border-color: rgb(51 65 85);
background: rgb(30 41 59 / 55%);
background: @workbench-dark-solid-bg;
color: rgb(226 232 240);
&:hover {
@@ -778,7 +782,7 @@
}
&__label {
background: rgb(69 10 10);
background: @workbench-dark-solid-bg;
color: rgb(254 202 202);
}
}

View File

@@ -1,5 +1,5 @@
<template>
<PageWrapper :contentFullHeight="true">
<PageWrapper :contentFullHeight="true" :dense="true" title="false" contentClass="workbench-page-wrapper">
<template #headerContent>
<WorkbenchHeader />
</template>
@@ -10,12 +10,16 @@
<NoteInfo />
</div>
<div class="workbench-col">
<ScheduleInfo />
<ScheduleInfo />
</div>
</div>
<div class="workbench-row">
<div class="workbench-col">下左</div>
<div class="workbench-col">下右</div>
<div class="workbench-col">
<ProjectTask />
</div>
<div class="workbench-col">
<ContractInfo />
</div>
</div>
</div>
</div>
@@ -26,6 +30,8 @@
import { PageWrapper } from '@jeesite/core/components/Page';
import WorkbenchHeader from './components/WorkbenchHeader.vue';
import NoteInfo from './components/NoteInfo.vue';
import ProjectTask from './components/ProjectTask.vue';
import ContractInfo from './components/ContractInfo.vue';
import ScheduleInfo from './components/ScheduleInfo.vue';
const loading = ref(true);
@@ -36,44 +42,64 @@
<style lang="less">
@dark-bg: #141414;
@desktop-page-gap: 12px;
@desktop-page-padding: 0;
@desktop-card-radius: 10px;
@desktop-card-border: 1px solid rgb(226 232 240);
@desktop-card-shadow: 0 1px 3px rgb(15 23 42 / 0.06);
@desktop-dark-border: rgb(51 65 85);
.workbench-page-wrapper {
display: flex;
flex-direction: column;
height: 100%;
min-height: 0;
padding: 0 !important;
overflow: hidden !important;
background: transparent !important;
}
.jeesite-workbench {
display: flex;
flex-direction: column;
flex: 1;
width: 100%;
height: 100%;
min-height: 0;
margin: 0;
background: rgb(255, 255, 255);
border-radius: 10px;
border-radius: @desktop-card-radius;
}
.jeesite-workbench .workbench-layout {
display: flex;
flex: 1;
flex-direction: column;
gap: 8px;
gap: @desktop-page-gap;
width: 100%;
height: 100%;
min-height: 0;
padding: 0;
padding: @desktop-page-padding;
box-sizing: border-box;
overflow: hidden;
background: transparent;
border-radius: 10px;
border-radius: @desktop-card-radius;
}
.jeesite-workbench .workbench-row {
display: flex;
flex: 1 1 0;
gap: 8px;
gap: @desktop-page-gap;
min-height: 0;
}
.jeesite-workbench .workbench-col {
display: flex;
flex-direction: column;
border-radius: 10px;
border: 1px solid rgb(226 232 240);
border-radius: @desktop-card-radius;
border: @desktop-card-border;
background: rgb(255, 255, 255);
box-shadow: 0 1px 3px rgb(15 23 42 / 0.06);
box-shadow: @desktop-card-shadow;
color: rgb(71 85 105);
font-size: 16px;
overflow: hidden;
@@ -91,7 +117,7 @@
}
html[data-theme='dark'] .jeesite-workbench .workbench-col {
border-color: rgb(51 65 85);
border-color: @desktop-dark-border;
background: @dark-bg !important;
color: rgb(226 232 240);
box-shadow: none;

View File

@@ -19,4 +19,12 @@ export function formatToDate(date: dayjs.ConfigType | undefined = undefined, for
return dayjs(date).format(format);
}
export function firstCurrentYear(format = DATE_FORMAT): string {
return dayjs().startOf('year').format(format);
}
export function firstCurrentMonth(format = DATE_FORMAT): string {
return dayjs().startOf('month').format(format);
}
export const dateUtil = dayjs;