项目初始化

This commit is contained in:
2026-03-27 12:27:01 +08:00
parent 9571ca8b5c
commit 1de218de1a
5 changed files with 605 additions and 43 deletions

View File

@@ -100,9 +100,9 @@ public class MyProjectInfo extends DataEntity<MyProjectInfo> implements Serializ
@ExcelField(title = "预计结束日期", attrName = "endDate", align = Align.CENTER, sort = 100, dataFormat = "yyyy-MM-dd hh:mm"),
@ExcelField(title = "实际结束日期", attrName = "actualEndDate", align = Align.CENTER, sort = 110, dataFormat = "yyyy-MM-dd hh:mm"),
@ExcelField(title = "项目预算", attrName = "budget", align = Align.CENTER, sort = 120),
@ExcelField(title = "项目类型", attrName = "projectType", align = Align.CENTER, sort = 130),
@ExcelField(title = "项目类型", attrName = "projectType", dictType = "project_type", align = Align.CENTER, sort = 130),
@ExcelField(title = "级别", attrName = "priority", dictType = "biz_priority", align = Align.CENTER, sort = 135),
@ExcelField(title = "项目状态", attrName = "projectStatus", align = Align.CENTER, sort = 140),
@ExcelField(title = "项目状态", attrName = "projectStatus", dictType = "project_status",align = Align.CENTER, sort = 140),
@ExcelField(title = "更新时间", attrName = "updateTime", align = Align.CENTER, sort = 150, dataFormat = "yyyy-MM-dd hh:mm"),
})
public MyProjectInfo() {

View File

@@ -11,16 +11,22 @@
</el-form-item>
<el-form-item label="项目类型">
<el-select v-model="searchForm.projectType" placeholder="请选择项目类型" clearable>
<el-option label="新增" value="新增" />
<el-option label="修改" value="修改" />
<el-option label="删除" value="删除" />
<el-option label="导出" value="导出" />
<el-option
v-for="item in projectTypeDict"
:key="item.dictValue"
:label="item.dictLabelRaw"
:value="item.dictValue"
/>
</el-select>
</el-form-item>
<el-form-item label="项目状态">
<el-select v-model="searchForm.projectStatus" placeholder="请选择项目状态" clearable>
<el-option label="成功" value="成功" />
<el-option label="失败" value="失败" />
<el-option
v-for="item in projectStatusDict"
:key="item.dictValue"
:label="item.dictLabelRaw"
:value="item.dictValue"
/>
</el-select>
</el-form-item>
<el-form-item class="query-form__actions">
@@ -43,10 +49,22 @@
>
<el-table-column prop="projectCode" label="项目编码" min-width="100" show-overflow-tooltip />
<el-table-column prop="projectName" label="项目名称" min-width="120" show-overflow-tooltip />
<el-table-column prop="projectType" label="项目类型" width="120" show-overflow-tooltip />
<el-table-column prop="priority" label="项目级别" width="100" show-overflow-tooltip />
<el-table-column prop="projectStatus" label="项目状态" width="100" />
<el-table-column prop="budget" label="项目预算" width="120" show-overflow-tooltip />
<el-table-column prop="projectType" label="项目类型" width="120" show-overflow-tooltip>
<template #default="{ row }">
{{ getDictLabel(projectTypeDict, row.projectType) }}
</template>
</el-table-column>
<el-table-column prop="priority" label="项目级别" width="100" show-overflow-tooltip>
<template #default="{ row }">
{{ getDictLabel(priorityDict, row.priority) }}
</template>
</el-table-column>
<el-table-column prop="projectStatus" label="项目状态" width="100">
<template #default="{ row }">
{{ getDictLabel(projectStatusDict, row.projectStatus) }}
</template>
</el-table-column>
<el-table-column prop="budget" label="项目预算(元)" width="120" show-overflow-tooltip />
<el-table-column prop="startDate" label="开始日期" width="150" show-overflow-tooltip />
<el-table-column prop="endDate" label="结束日期" width="150" show-overflow-tooltip />
</el-table>
@@ -59,7 +77,6 @@
:total="total"
:page-sizes="[10, 20, 50, 99]"
layout="total, sizes, prev, pager, next, jumper"
size="small"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
background
@@ -77,7 +94,11 @@
import { MyProjectInfo, myProjectInfoPageList } from '@jeesite/biz/api/biz/myProjectInfo';
import { DictData, dictDataListData } from '@jeesite/core/api/sys/dictData';
const priorityDict = ref<DictData[]>([]);
const projectTypeDict = ref<DictData[]>([]);
const projectStatusDict = ref<DictData[]>([]);
const sourceData = ref<MyProjectInfo[]>([]);
const loading = ref(false);
const searchForm = reactive({
@@ -116,6 +137,25 @@
getList();
};
async function getDict() {
try {
priorityDict.value = await dictDataListData({ dictType: 'biz_priority' });
projectTypeDict.value = await dictDataListData({ dictType: 'project_type' });
projectStatusDict.value = await dictDataListData({ dictType: 'project_status' });
} catch (error) {
priorityDict.value = [];
projectTypeDict.value = [];
projectStatusDict.value = [];
console.error('获取数据字典失败:', error);
}
}
function getDictLabel(dictList: DictData[] | undefined, value?: string) {
if (!value) return '-';
const target = dictList?.find((item) => item.dictValue === value);
return target?.dictLabelRaw || value;
}
async function getList() {
loading.value = true;
try {
@@ -138,6 +178,7 @@
onMounted(() => {
getList();
getDict();
});
function getSummaries({ columns, data }: { columns: TableColumnCtx<MyProjectInfo>[]; data: MyProjectInfo[] }) {

View File

@@ -17,15 +17,15 @@
<div class="analysis-right">
<div class="analysis-right-top">
<section class="analysis-panel">
<QuickLogin />
</section>
<QuickLogin />
</section>
<section class="analysis-panel">
<BizApps />
</section>
<BizApps />
</section>
</div>
<section class="analysis-panel">
<ProjectInfo />
</section>
<ProjectInfo />
</section>
</div>
</div>
</div>
@@ -34,13 +34,12 @@
</template>
<script lang="ts" setup name="Analysis">
import { PageWrapper } from '@jeesite/core/components/Page';
import BizApps from './components/BizApps.vue'
import BizApps from './components/BizApps.vue';
import HostInfo from './components/HostInfo.vue';
import TodoInfo from './components/TodoInfo.vue';
import NoticeInfo from './components/NoticeInfo.vue';
import QuickLogin from './components/QuickLogin.vue';
import ProjectInfo from './components/ProjectInfo.vue'
import ProjectInfo from './components/ProjectInfo.vue';
</script>
<style lang="less">
@dark-bg: #141414;
@@ -142,7 +141,7 @@
padding: 0;
border-radius: 10px;
border: 1px solid rgb(226 232 240);
background: rgb(248 250 252);
background: rgb(255, 255, 255);
box-shadow: 0 1px 3px rgb(15 23 42 / 0.06);
overflow: hidden;
color: rgb(71 85 105);

View File

@@ -0,0 +1,524 @@
<template>
<div ref="noteCardRef" class="note-card">
<div class="card-title">
<span>便签信息</span>
<el-tooltip content="刷新" placement="top" :show-after="200">
<el-button
class="card-title__refresh"
link
type="primary"
:icon="RefreshRight"
:loading="loading"
@click="getList"
/>
</el-tooltip>
</div>
<div class="card-content">
<div class="note-overview">
<div class="note-metrics">
<div v-for="item in metricCards" :key="item.key" class="metric-item">
<div class="metric-item__main">
<div class="metric-item__pane metric-item__pane--left">
<div class="metric-item__value" :style="{ color: item.color }">{{ item.total }}</div>
<div class="metric-item__extra">总数</div>
</div>
<div class="metric-item__pane metric-item__pane--right">
<div class="metric-item__value metric-item__value--small" :style="{ color: item.color }">{{
item.finished
}}</div>
<div class="metric-item__extra">已完成</div>
</div>
</div>
<div class="metric-item__label">{{ item.label }}</div>
</div>
</div>
<div class="note-chart-panel">
<div ref="chartRef" class="note-chart"></div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { computed, nextTick, onMounted, onUnmounted, ref } from 'vue';
import * as echarts from 'echarts';
import dayjs from 'dayjs';
import { RefreshRight } from '@element-plus/icons-vue';
import { DictData, dictDataListData } from '@jeesite/core/api/sys/dictData';
import { MyNotes, myNotesListData } from '@jeesite/biz/api/biz/myNotes';
interface MetricCard {
key: string;
label: string;
value: number;
color: string;
total: number;
finished: number;
}
const noteCardRef = ref<HTMLElement>();
const chartRef = ref<HTMLElement>();
const loading = ref(false);
const noteList = ref<MyNotes[]>([]);
const typeDict = ref<DictData[]>([]);
const statusDict = ref<DictData[]>([]);
const statusGroups = [
{ key: 'pending', label: '待开始', color: '#F97316' },
{ key: 'processing', label: '进行中', color: '#3B82F6' },
{ key: 'finished', label: '已完成', color: '#10B981' },
];
const metricTypeGroups = [
{ key: 'work', label: '工作', color: '#3B82F6' },
{ key: 'life', label: '生活', color: '#10B981' },
{ key: 'study', label: '学习', color: '#F97316' },
{ key: 'other', label: '其他', color: '#8B5CF6' },
];
let chartInstance: echarts.ECharts | null = null;
let resizeObserver: ResizeObserver | null = null;
let themeObserver: MutationObserver | null = null;
const metricCards = computed<MetricCard[]>(() => {
return metricTypeGroups.map((group) => ({
key: group.key,
label: group.label,
color: group.color,
value: noteList.value.filter((item) => matchTypeGroup(item.type, group.label)).length,
total: noteList.value.filter((item) => matchTypeGroup(item.type, group.label)).length,
finished: noteList.value.filter((item) => matchTypeGroup(item.type, group.label) && item.ustatus === '1').length,
}));
});
function getDictLabel(dictList: DictData[], value?: string) {
return dictList.find((item) => item.dictValue === value)?.dictLabelRaw || value || '-';
}
function matchTypeGroup(typeValue: string | undefined, targetLabel: string) {
return getDictLabel(typeDict.value, typeValue) === targetLabel;
}
function getStatusGroup(item: MyNotes) {
const label = getDictLabel(statusDict.value, item.ustatus);
if (label.includes('完成')) return '已完成';
if (label.includes('进行')) return '进行中';
if (label.includes('开始')) return '待开始';
if (item.ustatus === '1') return '已完成';
if (item.ustatus === '2') return '进行中';
return '待开始';
}
function getMonthList() {
return Array.from({ length: 12 }, (_, index) => `${index + 1}`);
}
function getNoteMonth(item: MyNotes) {
const dateValue = item.createTime || item.startTime || item.deadline || item.updateTime;
return dateValue ? dayjs(dateValue).format('M月') : '';
}
async function getDict() {
try {
const [typeRes, statusRes] = await Promise.all([
dictDataListData({ dictType: 'note_type' }),
dictDataListData({ dictType: 'note_status' }),
]);
typeDict.value = typeRes || [];
statusDict.value = statusRes || [];
} catch (error) {
typeDict.value = [];
statusDict.value = [];
}
}
async function getList() {
loading.value = true;
try {
const res = await myNotesListData({ pageNum: 1, pageSize: 999 });
noteList.value = res?.list || [];
} catch (error) {
noteList.value = [];
} finally {
loading.value = false;
nextTick(() => {
renderChart();
});
}
}
function buildChartData() {
const months = getMonthList();
const totals = months.map((month) => {
return statusGroups.reduce((sum, status) => {
return (
sum +
noteList.value.filter((item) => getNoteMonth(item) === month && getStatusGroup(item) === status.label).length
);
}, 0);
});
const series: echarts.BarSeriesOption[] = statusGroups.map((group) => {
return {
name: group.label,
type: 'bar',
stack: 'total',
barWidth: '24%',
emphasis: {
focus: 'series',
},
label: {
show: true,
position: 'inside',
formatter: ({ value }) => (Number(value) > 0 ? `${value}` : ''),
color: '#ffffff',
fontSize: 11,
},
itemStyle: {
color: group.color,
borderRadius: 0,
},
data: months.map((month) => {
return noteList.value.filter((item) => getNoteMonth(item) === month && getStatusGroup(item) === group.label)
.length;
}),
};
});
series.push({
name: '总数',
type: 'bar',
stack: 'total',
barWidth: '24%',
silent: true,
legendHoverLink: false,
itemStyle: {
color: 'rgba(0,0,0,0)',
borderRadius: 0,
},
tooltip: {
show: false,
},
label: {
show: true,
position: 'top',
formatter: ({ dataIndex }) => (totals[dataIndex] > 0 ? `${totals[dataIndex]}` : ''),
color: '#475569',
fontSize: 11,
},
data: totals.map(() => 0),
z: 10,
});
return {
categories: months,
series,
};
}
function renderChart() {
if (!chartRef.value) return;
if (!chartInstance) {
chartInstance = echarts.init(chartRef.value);
}
const { categories, series } = buildChartData();
const isDark = document.documentElement.getAttribute('data-theme') === 'dark';
const totalSeries = series.find((item) => item.name === '总数');
if (totalSeries?.label) {
totalSeries.label.color = isDark ? '#cbd5e1' : '#475569';
}
chartInstance.setOption({
grid: {
left: 12,
right: 12,
top: 40,
bottom: 6,
containLabel: true,
},
tooltip: {
trigger: 'axis',
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',
},
axisPointer: {
type: 'shadow',
},
},
legend: {
top: 4,
left: 'center',
selectedMode: true,
itemGap: 16,
data: statusGroups.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',
margin: 8,
rotate: 30,
},
},
yAxis: {
type: 'value',
name: '数量',
nameTextStyle: {
color: isDark ? '#94a3b8' : '#64748b',
padding: [0, 0, 4, 0],
},
splitLine: {
lineStyle: {
color: isDark ? 'rgba(71, 85, 105, 0.35)' : 'rgba(203, 213, 225, 0.55)',
},
},
axisLabel: {
color: isDark ? '#94a3b8' : '#64748b',
},
},
series,
});
}
function resizeChart() {
chartInstance?.resize();
}
onMounted(async () => {
await getDict();
await getList();
if (noteCardRef.value) {
resizeObserver = new ResizeObserver(() => {
resizeChart();
});
resizeObserver.observe(noteCardRef.value);
}
window.addEventListener('resize', resizeChart);
themeObserver = new MutationObserver(() => {
nextTick(() => {
renderChart();
});
});
themeObserver.observe(document.documentElement, {
attributes: true,
attributeFilter: ['data-theme'],
});
});
onUnmounted(() => {
resizeObserver?.disconnect();
themeObserver?.disconnect();
window.removeEventListener('resize', resizeChart);
chartInstance?.dispose();
chartInstance = null;
});
</script>
<style lang="less">
.note-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;
&__refresh {
padding: 0;
font-size: 16px;
}
}
.card-content {
flex: 1;
min-height: 0;
padding: 16px;
overflow: hidden;
background: transparent;
}
.note-overview {
display: grid;
grid-template-columns: minmax(220px, 0.9fr) minmax(0, 1.6fr);
gap: 12px;
height: 100%;
min-height: 0;
}
.note-metrics {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
grid-template-rows: repeat(2, minmax(0, 1fr));
gap: 10px;
min-height: 0;
}
.metric-item,
.note-chart-panel {
border-radius: 12px;
background: rgb(255, 255, 255);
box-shadow: 0 8px 24px rgb(148 163 184 / 14%);
}
.metric-item {
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: stretch;
padding: 8px;
&__main {
display: flex;
flex: 1;
width: 100%;
min-height: 0;
gap: 8px;
padding: 6px;
}
&__pane {
flex: 1 1 0;
min-width: 0;
min-height: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
border-radius: 8px;
background: rgb(255, 255, 255);
box-shadow: 0 8px 24px rgb(148 163 184 / 14%);
}
&__extra {
margin-top: 8px;
color: rgb(100 116 139);
font-size: 12px;
line-height: 16px;
text-align: center;
}
&__value {
font-size: 28px;
font-weight: 700;
line-height: 1;
text-align: center;
&--small {
font-size: 24px;
}
}
&__label {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
min-height: 28px;
margin-top: 2px;
padding-top: 4px;
border-top: 1px solid rgb(226 232 240);
color: rgb(71 85 105);
font-size: 12px;
line-height: 16px;
text-align: center;
}
}
.note-chart-panel {
min-width: 0;
min-height: 0;
padding: 12px;
}
.note-chart {
width: 100%;
height: 100%;
min-height: 0;
}
}
html[data-theme='dark'] .note-card {
.card-title {
color: rgb(203 213 225);
border-bottom-color: rgb(51 65 85);
&__refresh:deep(.el-icon) {
color: rgb(147 197 253);
}
}
.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%);
}
.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%);
}
&__extra {
color: rgb(148 163 184);
}
&__label {
border-top-color: rgb(51 65 85);
color: rgb(148 163 184);
}
}
}
@media (max-width: 900px) {
.note-card {
.note-overview {
grid-template-columns: 1fr;
}
.note-metrics {
grid-template-rows: repeat(2, minmax(88px, 1fr));
}
.note-chart {
min-height: 0;
}
}
}
</style>

View File

@@ -1,18 +1,19 @@
<template>
<PageWrapper>
<PageWrapper :contentFullHeight="true">
<template #headerContent>
<WorkbenchHeader />
</template>
<div class="jeesite-workbench">
<div class="workbench-layout">
<div class="workbench-top">10% 区域</div>
<div class="workbench-row">
<div class="workbench-col">30% 区域左侧</div>
<div class="workbench-col">30% 区域右侧</div>
<div class="workbench-col">
<NoteInfo />
</div>
<div class="workbench-col">上右</div>
</div>
<div class="workbench-row">
<div class="workbench-col">30% 区域左侧</div>
<div class="workbench-col">30% 区域右侧</div>
<div class="workbench-col">中左</div>
<div class="workbench-col">中右</div>
</div>
</div>
</div>
@@ -22,6 +23,7 @@
import { ref } from 'vue';
import { PageWrapper } from '@jeesite/core/components/Page';
import WorkbenchHeader from './components/WorkbenchHeader.vue';
import NoteInfo from './components/NoteInfo.vue';
const loading = ref(true);
setTimeout(() => {
@@ -37,7 +39,7 @@
height: 100%;
min-height: 0;
margin: 0;
background: #FFFFFF;
background: rgb(255, 255, 255);
border-radius: 10px;
}
@@ -48,34 +50,30 @@
width: 100%;
height: 100%;
min-height: 0;
padding: 4px;
padding: 2px;
box-sizing: border-box;
overflow: hidden;
background: transparent;
border-radius: 10px;
}
.jeesite-workbench .workbench-top {
flex: 0 0 10%;
min-height: 0;
}
.jeesite-workbench .workbench-row {
display: flex;
flex: 0 0 30%;
flex: 1 1 0;
gap: 12px;
min-height: 0;
}
.jeesite-workbench .workbench-col,
.jeesite-workbench .workbench-top {
.jeesite-workbench .workbench-col {
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
border-radius: 10px;
border: 1px solid rgb(226 232 240);
background: #FFFFFF;
background: rgb(255, 255, 255);
box-shadow: 0 1px 3px rgb(15 23 42 / 0.06);
color: rgb(71 85 105);
font-size: 16px;
overflow: hidden;
}
.jeesite-workbench .workbench-col {
@@ -89,10 +87,10 @@
background: @dark-bg !important;
}
html[data-theme='dark'] .jeesite-workbench .workbench-top,
html[data-theme='dark'] .jeesite-workbench .workbench-col {
border-color: rgb(51 65 85);
background: @dark-bg !important;
color: rgb(226 232 240);
box-shadow: none;
}
</style>