项目初始化

This commit is contained in:
2026-03-26 15:57:14 +08:00
parent 9d974067b6
commit 1dff06a35a
10 changed files with 969 additions and 200 deletions

View File

@@ -0,0 +1,26 @@
package com.jeesite.modules.apps.Module;
import lombok.Data;
import java.io.Serializable;
@Data
public class ChartInfo implements Serializable {
private String key;
private String label;
private double value;
private String color;
private String remark;
public ChartInfo() {
}
public ChartInfo(String key, String label, double value, String color,String remark) {
this.key = key;
this.label = label;
this.value = value;
this.color = color;
this.remark = remark;
}
}

View File

@@ -1,17 +0,0 @@
package com.jeesite.modules.apps.Module;
import lombok.Data;
import java.io.Serializable;
@Data
public class HostInfo implements Serializable {
private Integer cpuNum;
public HostInfo(){}
public HostInfo(Integer cpuNum){
this.cpuNum = cpuNum;
}
}

View File

@@ -6,10 +6,5 @@ public class Start {
public static void main(String[] args) {
}
}

View File

@@ -1,14 +1,39 @@
package com.jeesite.modules.apps.web;
import cn.hutool.system.SystemUtil;
import cn.hutool.core.date.DateUtil;
import cn.hutool.system.oshi.CpuInfo;
import cn.hutool.system.oshi.OshiUtil;
import com.jeesite.modules.apps.Module.ChartInfo;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import java.util.ArrayList;
import java.util.List;
@Controller
@RequestMapping(value = "${adminPath}/sys/analysis")
public class SysAnalysisController {
public void sss(){
SystemUtil.getJvmInfo();
@RequestMapping(value = "getHostInfo")
@ResponseBody
public List<ChartInfo> getHostInfo() {
List<ChartInfo> chartInfoList = new ArrayList<>();
CpuInfo cpuInfo = OshiUtil.getCpuInfo();
long uptimeMs = OshiUtil.getOs().getSystemUptime();
String runTime = DateUtil.formatBetween(uptimeMs);
// 系统使用率(红色)
chartInfoList.add(new ChartInfo("sys", "系统使用率", cpuInfo.getSys(), "#F43F5E", runTime));
// 用户使用率(蓝色)
chartInfoList.add(new ChartInfo("user", "用户使用率", cpuInfo.getUser(), "#3B82F6", runTime));
// CPU等待率橙色
chartInfoList.add(new ChartInfo("wait", "CPU等待率", cpuInfo.getWait(), "#F97316", runTime));
// CPU空闲率绿色
chartInfoList.add(new ChartInfo("free", "CPU空闲率", cpuInfo.getFree(), "#10B981", runTime));
// CPU总使用率粉色
chartInfoList.add(new ChartInfo("used", "CPU总使用率", cpuInfo.getUsed(), "#EC4899", runTime));
// 系统运行时间(紫色)
chartInfoList.add(new ChartInfo("time", "系统运行时间", 0, "#8B5CF6", runTime));
return chartInfoList;
}
}

View File

@@ -167,6 +167,9 @@ public class MyNoticeTodoController extends BaseController {
@RequestMapping(value = "fileList")
@ResponseBody
public List<FileList> fileList(MyNoticeTodo myNoticeTodo) {
MyNoticeTodo noticeTodo = myNoticeTodoService.get(myNoticeTodo);
noticeTodo.setReadFlag("1");
myNoticeTodoService.save(noticeTodo);
List<FileList> fileLists = new ArrayList<>();
List<FileUpload> fileUploadList = FileUploadUtils.findFileUpload(myNoticeTodo.getId(), "myNoticeTodo_file");
for (FileUpload fileUpload : fileUploadList) {

View File

@@ -1,33 +0,0 @@
package com.jeesite.modules.utils;
import cn.hutool.system.oshi.CpuInfo;
import cn.hutool.system.oshi.OshiUtil;
import com.jeesite.modules.apps.Module.HostInfo;
public class HostUtils {
public static HostInfo getHostInfo() {
// return "CpuInfo{CPU核心数=" + this.cpuNum + ", CPU总的使用率=" + this.toTal + ",
// CPU系统使用率=" + this.sys + ", CPU用户使用率=" + this.user + ", CPU当前等待率=" + this.wait + ",
// CPU当前空闲率=" + this.free + ", CPU利用率=" + this.getUsed() + ", CPU型号信息='" + this.cpuModel + '\'' + '}';
CpuInfo cpuInfo = OshiUtil.getCpuInfo();
System.out.println(cpuInfo);
System.out.println("================");
System.out.println(cpuInfo.getCpuNum());
System.out.println(cpuInfo.getToTal());
System.out.println(cpuInfo.getSys());
System.out.println(cpuInfo.getUser());
System.out.println(cpuInfo.getWait());
System.out.println(cpuInfo.getFree());
System.out.println(cpuInfo.getUsed());
return new HostInfo();
}
public static void main(String[] args) {
getHostInfo();
}
}

View File

@@ -0,0 +1,20 @@
/**
* Copyright (c) 2013-Now https://jeesite.com All rights reserved.
* No deletion without permission, or be held responsible to law.
* @author gaoxq
*/
import { defHttp } from '@jeesite/core/utils/http/axios';
import { useGlobSetting } from '@jeesite/core/hooks/setting';
import { BasicModel } from '@jeesite/core/api/model/baseModel';
const { adminPath } = useGlobSetting();
export interface ChartInfo extends BasicModel<ChartInfo> {
key: string; // 指标Key
label: string; // 指标名称
value: number; // 指标数值
color: string; // 指标颜色
remark?: string; // 运行时长
}
export const HostInfoData = () => defHttp.get<ChartInfo[]>({ url: adminPath + '/sys/analysis/getHostInfo' });

View File

@@ -1,12 +1,24 @@
<template>
<div ref="hostCardRef" class="host-card">
<div class="card-title">
<span>主机信息</span>
<div class="card-title__main">
<span>主机信息</span>
<el-tooltip v-if="runtimeText" :content="`运行时长:${runtimeText}`" placement="top" :show-after="200">
<el-icon class="card-title__tips"><QuestionFilled /></el-icon>
</el-tooltip>
</div>
</div>
<div class="card-content">
<div class="gauge-list">
<div v-for="item in gauges" :key="item.label" class="gauge-item">
<div :ref="(el) => setChartRef(el, item.label)" class="gauge-chart"></div>
<div v-loading="loading" class="host-overview">
<div class="host-gauge-panel">
<div ref="chartRef" class="gauge-chart"></div>
</div>
<div class="host-metrics">
<div v-for="item in sideMetrics" :key="item.key" class="metric-item">
<div class="metric-item__value" :style="{ color: item.color }">{{ item.value }}%</div>
<div class="metric-item__label">{{ item.label }}</div>
</div>
</div>
</div>
</div>
@@ -14,89 +26,157 @@
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue';
import { computed, nextTick, onMounted, onUnmounted, ref } from 'vue';
import * as echarts from 'echarts';
import { QuestionFilled } from '@element-plus/icons-vue';
import { ChartInfo, HostInfoData } from '@jeesite/biz/api/biz/myAnalysis';
const gauges = ref([
{ label: 'CPU系统使用率', value: 79, color: '#3b82f6' },
{ label: 'CPU用户使用率', value: 68, color: '#10b981' },
{ label: 'CPU利用率', value: 82, color: '#ef4444' },
]);
const chartInstances = new Map();
const chartRefs = new Map();
const hostCardRef = ref<HTMLElement>();
const chartRef = ref<HTMLElement>();
const cpuData = ref<ChartInfo[]>([]);
const loading = ref(false);
const totalMetric = computed(() => cpuData.value.find((item) => item.key === 'used'));
const runtimeText = computed(() => cpuData.value.find((item) => item.key === 'time')?.remark || '');
const sideMetrics = computed(() => {
const metricOrder = ['sys', 'wait', 'user', 'free'];
return metricOrder
.map((key) => cpuData.value.find((item) => item.key === key))
.filter((item): item is ChartInfo => Boolean(item));
});
let chartInstance: echarts.ECharts | null = null;
let resizeObserver: ResizeObserver | null = null;
function resizeCharts() {
chartInstances.forEach((chart) => {
chart.resize();
async function getList() {
loading.value = true;
try {
const res = await HostInfoData();
cpuData.value = res || [];
} catch (error) {
console.error('获取主机信息失败:', error);
cpuData.value = [];
} finally {
loading.value = false;
nextTick(() => {
renderChart();
});
}
}
function getTotalValue() {
return totalMetric.value?.value || 0;
}
function getTotalLabel() {
return totalMetric.value?.label || 'CPU使用率';
}
function getTotalColor() {
return totalMetric.value?.color || '#ef4444';
}
function renderChart() {
if (!chartRef.value) return;
if (!chartInstance) {
chartInstance = echarts.init(chartRef.value);
}
const totalValue = getTotalValue();
const totalLabel = getTotalLabel();
const totalColor = getTotalColor();
chartInstance.setOption({
series: [
{
type: 'gauge',
startAngle: 210,
endAngle: -30,
radius: '72%',
center: ['50%', '56%'],
min: 0,
max: 100,
splitNumber: 5,
progress: {
show: true,
width: 10,
itemStyle: {
color: totalColor,
},
},
axisLine: {
lineStyle: {
width: 10,
color: [[1, '#e5e7eb']],
},
},
pointer: {
show: true,
length: '52%',
width: 5,
offsetCenter: [0, '0%'],
itemStyle: {
color: totalColor,
},
},
axisTick: {
distance: -10,
splitNumber: 4,
lineStyle: {
width: 1,
color: '#cbd5e1',
},
},
splitLine: {
distance: -10,
length: 7,
lineStyle: {
width: 2,
color: '#94a3b8',
},
},
axisLabel: {
distance: -18,
color: '#64748b',
fontSize: 9,
},
anchor: {
show: true,
showAbove: true,
size: 8,
itemStyle: {
color: totalColor,
borderWidth: 2,
borderColor: '#fff',
},
},
title: {
show: true,
offsetCenter: [0, '44%'],
fontSize: 11,
color: '#64748b',
},
detail: {
valueAnimation: true,
formatter: '{value}%',
color: '#0f172a',
fontSize: 22,
fontWeight: 'bold',
offsetCenter: [0, '2%'],
},
data: [{ value: totalValue, name: totalLabel }],
},
],
});
}
function setChartRef(el: any, label: string) {
if (el) chartRefs.set(label, el);
function resizeCharts() {
chartInstance?.resize();
}
onMounted(() => {
gauges.value.forEach((item) => {
const el = chartRefs.get(item.label);
if (!el) return;
const chart = echarts.init(el);
chart.setOption({
series: [
{
type: 'gauge',
startAngle: 200,
endAngle: -20,
radius: '75%',
center: ['50%', '50%'],
min: 0,
max: 100,
splitNumber: 5,
axisLine: {
lineStyle: {
width: 10,
color: [
[item.value / 100, item.color],
[1, '#e5e7eb'],
],
},
},
pointer: {
length: '55%',
width: 4,
itemStyle: { color: item.color },
},
axisTick: { show: false },
splitLine: {
length: 8,
lineStyle: { color: '#fff', width: 2 },
},
axisLabel: {
show: false,
},
title: {
show: true,
offsetCenter: [0, '120%'],
fontSize: 11,
color: '#666',
},
detail: {
valueAnimation: true,
formatter: '{value}%',
color: item.color,
fontSize: 16,
fontWeight: 'bold',
offsetCenter: [0, '0%'],
},
data: [{ value: item.value, name: item.label }],
},
],
});
chartInstances.set(item.label, chart);
});
getList();
if (hostCardRef.value) {
resizeObserver = new ResizeObserver(() => {
resizeCharts();
@@ -110,13 +190,12 @@
onUnmounted(() => {
resizeObserver?.disconnect();
window.removeEventListener('resize', resizeCharts);
chartInstances.forEach((chart) => chart.dispose());
chartInstance?.dispose();
chartInstance = null;
});
</script>
<style lang="less">
@dark-bg: #141414;
.host-card {
width: 100%;
height: 100%;
@@ -136,62 +215,180 @@
color: rgb(51 65 85);
border-bottom: 1px solid rgb(226 232 240);
background: transparent;
&__main {
display: inline-flex;
align-items: center;
gap: 6px;
}
&__tips {
font-size: 14px;
color: rgb(100 116 139);
cursor: pointer;
transition: color 0.2s ease;
}
&__tips:hover {
color: rgb(59 130 246);
}
}
.card-content {
flex: 1;
min-height: 0;
padding: 16px;
color: rgb(71 85 105);
font-size: 14px;
line-height: 22px;
overflow: auto;
overflow: hidden;
background: transparent;
}
.gauge-list {
display: flex;
justify-content: space-around;
align-items: center;
.host-overview {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 12px;
height: 100%;
align-items: stretch;
min-height: 0;
}
.gauge-item {
flex: 1;
.host-gauge-panel {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
justify-content: center;
min-width: 0;
min-height: 160px;
padding: 8px;
border-radius: 14px;
background: rgb(248, 250, 252);
box-shadow: 0 10px 28px rgb(148 163 184 / 16%);
overflow: hidden;
}
.gauge-chart {
width: 100%;
height: 140px;
max-width: 140px;
height: 100%;
min-height: 160px;
max-width: 280px;
margin: 0 auto;
}
.gauge-label {
font-size: 11px;
color: rgb(100 116 139);
text-align: center;
.host-metrics {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
grid-template-rows: repeat(2, minmax(0, 1fr));
gap: 10px;
padding: 0;
background: transparent;
min-width: 0;
min-height: 0;
height: 100%;
}
.metric-item {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-width: 0;
min-height: 0;
padding: 10px 12px;
border-radius: 10px;
background: rgb(248, 250, 252);
box-shadow: 0 8px 24px rgb(148 163 184 / 14%);
overflow: hidden;
transition:
transform 0.2s ease,
box-shadow 0.2s ease,
background 0.2s ease;
&__label {
margin-top: 8px;
color: rgb(71 85 105);
font-size: 12px;
line-height: 16px;
text-align: center;
word-break: break-word;
}
&__value {
font-size: 20px;
font-weight: 700;
line-height: 1;
white-space: nowrap;
}
&:hover {
transform: translateY(-2px);
box-shadow: 0 12px 28px rgb(96 165 250 / 20%);
}
}
}
html[data-theme='dark'] .host-card {
box-shadow: none;
.card-title {
color: rgb(203 213 225);
border-bottom-color: rgb(51 65 85);
&__tips {
color: rgb(148 163 184);
}
&__tips:hover {
color: rgb(147 197 253);
}
}
.card-content {
color: rgb(148 163 184);
.host-gauge-panel,
.host-metrics {
background: transparent;
}
.gauge-label {
color: rgb(148 163 184);
.host-gauge-panel {
background: linear-gradient(180deg, rgb(20, 20, 20) 0%, rgb(28 28 28) 100%);
box-shadow: 0 12px 30px rgb(0 0 0 / 28%);
}
.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%);
&__label {
color: rgb(148 163 184);
}
&:hover {
box-shadow: 0 14px 32px rgb(37 99 235 / 22%);
}
}
}
@media (max-width: 900px) {
.host-card {
.host-overview {
grid-template-columns: 1fr;
grid-template-rows: auto 1fr;
}
.host-metrics {
grid-template-columns: repeat(2, minmax(0, 1fr));
grid-template-rows: repeat(2, minmax(88px, 1fr));
}
.host-gauge-panel {
min-height: 160px;
}
.gauge-chart {
min-height: 160px;
}
}
}
@media (max-width: 560px) {
.host-card {
.host-metrics {
grid-template-columns: 1fr;
}
}
}
</style>

View File

@@ -15,15 +15,27 @@
</div>
<div class="card-content">
<div ref="tableWrapRef" class="table-container">
<el-table :data="listData" :height="tableHeight" :show-header="true" :border="false">
<el-table-column prop="title" label="标题" min-width="120" show-overflow-tooltip="true" />
<el-table-column prop="extraDesc" label="待办意见" width="120" show-overflow-tooltip="true" />
<el-table-column prop="datetime" label="到期时间" width="180" />
<el-table v-loading="tableLoading" :data="listData" :height="tableHeight" :show-header="true" :border="false">
<el-table-column prop="title" label="标题" min-width="80" show-overflow-tooltip="true" />
<el-table-column prop="type" label="类型" width="60">
<template #default="{ row }">
{{ formatNoticeType(row.type) }}
</template>
</el-table-column>
<el-table-column prop="createTime" label="记录时间" width="150" show-overflow-tooltip="true" />
<el-table-column prop="datetime" label="截至时间" width="150" show-overflow-tooltip="true" />
<el-table-column prop="createUser" label="发布人员" width="120" show-overflow-tooltip="true" />
<el-table-column label="操作" width="90" align="center" fixed="right">
<template #default="{ row }">
<el-button class="notice-action-button" link type="primary" @click="openNoticeDialog(row)">
查看
</el-button>
<el-tooltip content="查看" placement="top" :show-after="200">
<el-button
class="notice-action-button"
link
type="primary"
:icon="View"
@click="openNoticeDialog(row)"
/>
</el-tooltip>
</template>
</el-table-column>
</el-table>
@@ -39,7 +51,7 @@
<div class="notice-dialog__time">
<div class="notice-dialog__time-left">
<span>发布时间{{ selectedNotice.createTime }}</span>
<span>发布人{{ selectedNotice.createUser }}</span>
<span>发布人{{ selectedNotice.createUser }}</span>
</div>
<div class="notice-dialog__time-right">截至时间{{ selectedNotice.datetime }}</div>
</div>
@@ -61,13 +73,12 @@
<div class="notice-dialog__attachment-actions">
<span class="notice-dialog__attachment-size">{{ item.fileSize }}</span>
<el-button
class="notice-dialog__attachment-download"
type="primary"
link
:icon="Download"
class="notice-dialog__attachment-download"
@click.stop="handleDownload(item)"
>下载</el-button
>
></el-button>
</div>
</div>
</div>
@@ -79,6 +90,7 @@
</template>
<script setup lang="ts">
import { View } from '@element-plus/icons-vue';
import { ElMessage } from 'element-plus';
import { Icon } from '@jeesite/core/components/Icon';
import { Document, Download } from '@element-plus/icons-vue';
@@ -103,6 +115,7 @@
const tableWrapRef = ref<HTMLElement>();
const tableHeight = ref(0);
const tableLoading = ref(false);
const dialogVisible = ref(false);
const selectedNotice = ref<MyNoticeTodo | null>(null);
@@ -113,7 +126,16 @@
selectedNotice.value = notice;
dialogVisible.value = true;
}
function formatNoticeType(type?: string | number) {
const typeMap: Record<string, string> = {
'1': '通知',
'2': '消息',
'3': '待办',
};
return typeMap[String(type ?? '')] || '-';
}
async function handleDownload(item: MyFileList) {
const { ctxAdminPath } = useGlobSetting();
await downloadByUrl({
@@ -137,17 +159,19 @@
}
const getDataList = async () => {
tableLoading.value = true;
try {
const reqParams = {
type: '1',
ustatus: '1',
clickClose: '0',
ustatus: '1',
readFlag: activeTab.value,
};
const result = await myNoticeTodoListAll(reqParams);
listData.value = result || [];
} catch (error) {
listData.value = [];
} finally {
tableLoading.value = false;
}
};

View File

@@ -7,7 +7,7 @@
v-for="item in tabs"
:key="item.key"
:class="['tab-item', { active: activeTab === item.key }]"
@click="activeTab = item.key"
@click="handleTabChange(item.key)"
>
{{ item.label }}
</button>
@@ -15,18 +15,108 @@
</div>
<div class="card-content">
<div ref="tableWrapRef" class="table-container">
<el-table :data="currentList" :height="tableHeight" :show-header="true" :border="false">
<el-table-column prop="title" label="标题" min-width="120" />
<el-table-column prop="time" label="时间" width="180" />
<el-table-column prop="content" label="内容" min-width="200" show-overflow-tooltip="true" />
<el-table v-loading="tableLoading" :data="listData" :height="tableHeight" :show-header="true" :border="false">
<el-table-column prop="title" label="标题" min-width="80" show-overflow-tooltip="true" />
<el-table-column prop="type" label="类型" width="60">
<template #default="{ row }">
{{ formatNoticeType(row.type) }}
</template>
</el-table-column>
<el-table-column prop="createTime" label="记录时间" width="150" show-overflow-tooltip="true" />
<el-table-column prop="datetime" label="截至时间" width="150" show-overflow-tooltip="true" />
<el-table-column prop="extraDesc" label="处理意见" width="120" show-overflow-tooltip="true" />
<el-table-column label="操作" width="90" align="center" fixed="right">
<template #default="{ row }">
<el-tooltip content="编辑" placement="top" :show-after="200">
<el-button
class="notice-action-button"
link
type="primary"
:icon="Edit"
@click="openNoticeDialog(row)"
/>
</el-tooltip>
</template>
</el-table-column>
</el-table>
</div>
</div>
<el-dialog
v-model="dialogVisible"
:class="['todo-info-dialog', { 'todo-info-dialog--no-footer': selectedNotice?.clickClose === '1' }]"
title="待办详情"
width="50%"
destroy-on-close
>
<template v-if="selectedNotice">
<div class="todo-dialog">
<div class="todo-dialog__header">
<div class="todo-dialog__title">{{ selectedNotice.title }}</div>
<div class="todo-dialog__header-divider"></div>
<div class="todo-dialog__time">
<div class="todo-dialog__time-left">
<span>创建时间{{ selectedNotice.createTime || '-' }}</span>
<span>创建人{{ selectedNotice.createUser || '-' }}</span>
<span>类型{{ formatNoticeType(selectedNotice.type) }}</span>
</div>
<div class="todo-dialog__time-right">截至时间{{ selectedNotice.datetime || '-' }}</div>
</div>
</div>
<el-divider class="todo-dialog__divider" />
<div class="todo-dialog__content-panel">
<div class="todo-dialog__section-title">待办内容</div>
<div class="todo-dialog__content" v-html="selectedNotice.description || '-'"></div>
</div>
<div class="todo-dialog__form-panel">
<div class="todo-dialog__section-title">处理意见</div>
<el-form label-position="top">
<el-form-item class="todo-dialog__form-item">
<el-input
v-model="opinionForm.extraDesc"
class="todo-dialog__textarea"
type="textarea"
:rows="5"
resize="none"
maxlength="1000"
show-word-limit
:readonly="isReadonly"
:placeholder="isReadonly ? '暂无处理意见' : '请输入处理意见'"
/>
</el-form-item>
</el-form>
</div>
</div>
</template>
<template #footer>
<div class="todo-dialog__footer">
<div v-if="!isReadonly" class="todo-dialog__button-group">
<el-button @click="handleResetOpinion">重置</el-button>
<el-button type="primary" :loading="submitLoading" @click="handleSaveOpinion">保存</el-button>
</div>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { computed, nextTick, onBeforeUnmount, onMounted, ref } from 'vue';
import { Edit } from '@element-plus/icons-vue';
import { ElMessage } from 'element-plus';
import { computed, nextTick, onBeforeUnmount, onMounted, reactive, ref } from 'vue';
import { useUserStore } from '@jeesite/core/store/modules/user';
import {
MyNoticeTodo,
myNoticeTodoForm,
myNoticeTodoListAll,
myNoticeTodoSave,
} from '@jeesite/biz/api/biz/myNoticeTodo';
const userStore = useUserStore();
const userinfo = computed(() => userStore.getUserInfo);
const tabs = [
{ key: '0', label: '待办' },
@@ -34,26 +124,88 @@
];
const activeTab = ref('0');
const noticeData = {
0: [
{ title: '系统升级通知', time: '2025-03-25 10:00', content: '系统将于今晚进行升级维护,请提前保存工作内容。' },
{ title: '安全检查提醒', time: '2025-03-25 09:30', content: '请及时完成本周安全检查任务,确保系统运行稳定。' },
{ title: '备份任务完成', time: '2025-03-25 08:15', content: '数据库备份任务已完成,备份文件已存储至指定位置。' },
],
1: [
{ title: '巡检报告提交', time: '2025-03-24 16:20', content: '昨日巡检报告已提交并审核通过。' },
{ title: '权限变更通知', time: '2025-03-24 14:10', content: '用户权限调整已生效,请相关人员知悉。' },
],
};
const currentList = computed(() => noticeData[activeTab.value] || []);
const listData = ref<MyNoticeTodo[]>([]);
const tableWrapRef = ref<HTMLElement>();
const tableHeight = ref(0);
const tableLoading = ref(false);
const dialogVisible = ref(false);
const submitLoading = ref(false);
const selectedNotice = ref<MyNoticeTodo | null>(null);
const opinionForm = reactive({
extraDesc: '',
});
const isReadonly = computed(() => activeTab.value === '1');
let resizeObserver: ResizeObserver | null = null;
function formatNoticeType(type?: string | number) {
const typeMap: Record<string, string> = {
'1': '通知',
'2': '消息',
'3': '待办',
};
return typeMap[String(type ?? '')] || '-';
}
async function openNoticeDialog(notice: MyNoticeTodo) {
dialogVisible.value = true;
selectedNotice.value = notice;
opinionForm.extraDesc = notice.extraDesc || '';
try {
const res = await myNoticeTodoForm({ id: notice.id });
const detail = ((res as any)?.myNoticeTodo || res || notice) as MyNoticeTodo;
selectedNotice.value = detail;
opinionForm.extraDesc = detail.extraDesc || '';
} catch (error) {
selectedNotice.value = notice;
}
}
async function handleSaveOpinion() {
if (!selectedNotice.value?.id) return;
if (!opinionForm.extraDesc.trim()) {
ElMessage.warning('请输入处理意见');
return;
}
submitLoading.value = true;
try {
const data: MyNoticeTodo = {
...selectedNotice.value,
readFlag: '1',
clickClose: '1',
extraDesc: opinionForm.extraDesc.trim(),
};
const params = {
isNewRecord: selectedNotice.value.isNewRecord,
id: selectedNotice.value.id,
};
const res = await myNoticeTodoSave(params, data);
ElMessage.success((res as any)?.message);
selectedNotice.value = {
...selectedNotice.value,
extraDesc: opinionForm.extraDesc.trim(),
};
listData.value = listData.value.map((item) =>
item.id === selectedNotice.value?.id ? { ...item, extraDesc: opinionForm.extraDesc.trim() } : item,
);
getDataList();
dialogVisible.value = false;
} catch (error) {
console.error('保存失败,请稍后重试');
} finally {
submitLoading.value = false;
}
}
function handleResetOpinion() {
opinionForm.extraDesc = selectedNotice.value?.extraDesc || '';
}
function updateTableHeight() {
nextTick(() => {
const tableWrapEl = tableWrapRef.value;
@@ -62,7 +214,32 @@
});
}
function handleTabChange(tabKey: string) {
if (activeTab.value === tabKey) return;
activeTab.value = tabKey;
getDataList();
}
async function getDataList() {
tableLoading.value = true;
try {
const reqParams = {
type: '3',
ustatus: '1',
clickClose: activeTab.value,
loginUser: userinfo.value.loginCode,
};
const result = await myNoticeTodoListAll(reqParams);
listData.value = result || [];
} catch (error) {
listData.value = [];
} finally {
tableLoading.value = false;
}
}
onMounted(() => {
getDataList();
updateTableHeight();
if (tableWrapRef.value) {
resizeObserver = new ResizeObserver(() => {
@@ -78,8 +255,6 @@
</script>
<style lang="less">
@dark-bg: #141414;
.notice-card {
width: 100%;
height: 100%;
@@ -188,14 +363,207 @@
.cell {
line-height: 20px;
}
.notice-action-button {
padding: 0;
font-weight: 500;
}
}
}
}
.todo-dialog {
display: flex;
flex-direction: column;
gap: 0;
&__header {
display: flex;
flex-direction: column;
gap: 8px;
}
&__title {
font-size: 18px;
font-weight: 600;
color: rgb(30 41 59);
text-align: center;
}
&__header-divider {
width: 100%;
height: 1px;
background: rgb(226 232 240);
}
&__time {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
font-size: 13px;
color: rgb(100 116 139);
}
&__time-left {
display: flex;
align-items: center;
gap: 16px;
min-width: 0;
flex-wrap: wrap;
}
&__time-right {
flex-shrink: 0;
text-align: right;
}
&__divider {
margin: 16px 0;
}
&__content-panel,
&__form-panel {
padding: 2px;
border-radius: 8px;
background: rgb(248 252 255);
box-sizing: border-box;
}
&__form-panel {
margin-top: 16px;
}
&__section-title {
display: flex;
align-items: center;
width: 100%;
min-height: 36px;
margin-bottom: 12px;
padding: 0 12px;
border-radius: 6px;
background: rgb(239 246 255);
font-size: 14px;
font-weight: 600;
color: rgb(51 65 85);
box-sizing: border-box;
}
&__content {
min-height: 220px;
max-height: 320px;
padding: 14px 16px;
border: 1px solid rgb(191 219 254);
border-radius: 6px;
background: rgb(255 255 255);
font-size: 14px;
line-height: 24px;
color: rgb(51 65 85);
overflow-y: auto;
scrollbar-width: thin;
scrollbar-color: rgb(191 219 254) transparent;
white-space: pre-wrap;
word-break: break-all;
&::-webkit-scrollbar {
width: 6px;
}
&::-webkit-scrollbar-track {
background: transparent;
}
&::-webkit-scrollbar-thumb {
border-radius: 999px;
background: rgb(191 219 254);
}
:deep(p) {
margin: 0 0 8px;
}
:deep(ul) {
margin: 0;
padding-left: 20px;
}
:deep(li) {
margin: 4px 0;
}
}
&__form-item {
margin-bottom: 0;
}
&__textarea :deep(.el-textarea__inner) {
min-height: 140px !important;
padding: 14px 16px;
border-radius: 6px;
border-color: rgb(191 219 254);
background: rgb(255 255 255);
color: rgb(51 65 85);
box-shadow: none;
}
&__footer {
display: flex;
justify-content: flex-end;
padding-top: 6px;
border-top: 1px solid rgb(226 232 240);
}
&__button-group {
:deep(.el-button) {
min-width: 72px;
}
:deep(.el-button + .el-button) {
margin-left: 6px;
}
}
}
}
html[data-theme='dark'] .notice-card {
box-shadow: none;
.todo-info-dialog {
.el-dialog {
background: rgb(255 255 255) !important;
--el-dialog-bg-color: rgb(255 255 255);
--el-bg-color: rgb(255 255 255);
box-shadow: 0 12px 32px rgb(15 23 42 / 18%);
}
.el-dialog__header {
margin-right: 0;
padding: 12px 12px 10px;
border-bottom: 1px solid rgb(226 232 240) !important;
background: rgb(255 255 255) !important;
}
.el-dialog__body {
padding: 2px;
background: rgb(255 255 255) !important;
}
.el-dialog__footer {
padding: 0 8px 4px;
background: rgb(255 255 255) !important;
}
.el-divider {
--el-border-color: rgb(226 232 240);
}
&.todo-info-dialog--no-footer {
.el-dialog__footer {
display: none !important;
padding: 0 !important;
border: none !important;
}
}
}
html[data-theme='dark'] .notice-card {
.card-title {
color: rgb(203 213 225);
border-bottom-color: rgb(51 65 85);
@@ -237,5 +605,166 @@
}
}
}
.todo-dialog {
&__title {
color: rgb(226 232 240);
}
&__time {
color: rgb(148 163 184);
}
&__header-divider {
background: rgb(51 65 85);
}
&__content-panel,
&__form-panel,
&__content,
&__section-title {
background: rgb(20, 20, 20);
}
&__content {
color: rgb(226 232 240);
border-color: rgb(51 65 85);
scrollbar-color: rgb(71 85 105) transparent;
}
&__content::-webkit-scrollbar-thumb {
background: rgb(71 85 105);
}
&__section-title {
color: rgb(226 232 240);
}
&__textarea :deep(.el-textarea__inner) {
border: 1px solid rgb(71 85 105) !important;
border-color: rgb(71 85 105);
background: rgb(20, 20, 20);
color: rgb(226 232 240);
box-shadow: none !important;
}
&__textarea :deep(.el-textarea__inner:focus) {
border-color: rgb(96 165 250) !important;
}
&__footer {
border-top-color: rgb(51 65 85);
}
&__textarea :deep(.el-textarea__inner::placeholder) {
color: rgb(100 116 139);
}
&__textarea :deep(.el-input__count) {
color: rgb(148 163 184);
background: rgb(20, 20, 20);
}
}
}
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-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%);
}
.el-dialog__wrapper,
.el-overlay-dialog,
.el-dialog__content {
background: rgb(20, 20, 20) !important;
}
.el-dialog__header {
border-bottom: 1px solid rgb(51 65 85) !important;
background: rgb(20, 20, 20) !important;
}
.el-dialog__body,
.el-dialog__footer {
background: rgb(20, 20, 20) !important;
}
.el-textarea,
.el-textarea__inner,
.el-input__wrapper {
--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;
box-shadow: none !important;
}
.el-textarea__inner {
border: 1px solid rgb(71 85 105) !important;
}
.el-dialog__title {
color: rgb(226 232 240);
}
.el-dialog__headerbtn .el-dialog__close {
color: rgb(148 163 184);
}
.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-divider {
--el-border-color: rgb(51 65 85);
}
.el-form-item__label {
color: rgb(226 232 240);
}
.todo-dialog__footer {
.el-button:not(.el-button--primary) {
border-color: rgb(71 85 105);
background: rgb(30 41 59);
color: rgb(226 232 240);
}
.el-button:not(.el-button--primary):hover {
border-color: rgb(96 165 250);
background: rgb(37 99 235 / 22%);
color: rgb(241 245 249);
}
.el-button--primary {
border-color: rgb(59 130 246);
background: rgb(37 99 235);
color: rgb(248 250 252);
}
.el-button--primary:hover {
border-color: rgb(96 165 250);
background: rgb(59 130 246);
}
}
}
html[data-theme='dark'] .todo-info-dialog.todo-info-dialog--no-footer {
.el-dialog__footer {
display: none !important;
padding: 0 !important;
border: none !important;
}
}
</style>