feat: 新增电工APP端与定时任务管理功能
- 新增电工APP独立鉴权中间件与登录注册接口 - 新增定时任务管理模块,支持任务配置、执行日志与手动触发 - 新增运维费定价与电工派单成本价目管理 - 新增电工资质证照管理与到期提醒定时任务 - 完善数据库实体与DTO校验,优化权限控制
This commit is contained in:
1 parent
1803ad679c
commit
f4454ad014
57 files changed
+9388
-200
No files matched your search
@@ -233,12 +233,58 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 电工资质到期提醒 -->
|
||||
<div class="remind-grid">
|
||||
<div class="card">
|
||||
<div class="card-head">
|
||||
<div>
|
||||
<div class="card-title-row">
|
||||
<h3 class="card-title">电工资质到期提醒</h3>
|
||||
<span class="card-tag">CERT ALERT</span>
|
||||
</div>
|
||||
<p class="card-sub">临期(30 天内)与已过期的电工资质证照清单,已过期优先展示</p>
|
||||
</div>
|
||||
<a class="card-link" @click="goWorkerRegistry">
|
||||
查看全部
|
||||
<ArrowRightOutlined />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="table-scroll">
|
||||
<table class="dw-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>电工姓名</th>
|
||||
<th>证书类型</th>
|
||||
<th>有效期至</th>
|
||||
<th class="ta-r">到期状态</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="row in certReminders" :key="row.key">
|
||||
<td class="strong">{{ row.workerName }}</td>
|
||||
<td>{{ row.certType }}</td>
|
||||
<td class="mono muted">{{ row.expireDate }}</td>
|
||||
<td class="ta-r">
|
||||
<span class="tag" :class="row.expired ? 'tag-danger' : 'tag-warn'">
|
||||
{{ row.expired ? `已过期 ${Math.abs(row.days)} 天` : `剩余 ${row.days} 天` }}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="table-empty" v-if="!certReminders.length">暂无临期或已过期的电工资质</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-spin>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, onUnmounted, nextTick, markRaw } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import {
|
||||
Button,
|
||||
Spin,
|
||||
@@ -258,8 +304,11 @@ import {
|
||||
ArrowRightOutlined,
|
||||
} from '@ant-design/icons-vue';
|
||||
import * as echarts from 'echarts';
|
||||
import dayjs from 'dayjs';
|
||||
import { dashboardApi, type DashboardStats } from '@/api/dashboard';
|
||||
import { workerRegistryApi } from '@/api/workerRegistry';
|
||||
|
||||
const router = useRouter();
|
||||
const AButton = Button;
|
||||
const ASpin = Spin;
|
||||
|
||||
@@ -272,6 +321,63 @@ let donutChart: echarts.ECharts | null = null;
|
||||
let refreshTimer: ReturnType<typeof setInterval> | null = null;
|
||||
const lastUpdateTime = ref('--');
|
||||
|
||||
// ===== 电工资质到期提醒 =====
|
||||
interface CertReminderRow {
|
||||
key: string;
|
||||
workerId: number;
|
||||
workerName: string;
|
||||
certType: string;
|
||||
expireDate: string;
|
||||
days: number;
|
||||
expired: boolean;
|
||||
}
|
||||
const certReminders = ref<CertReminderRow[]>([]);
|
||||
|
||||
// 取临期与已过期各前 5 条电工,展开证书后按到期天数排序,已过期的排前面
|
||||
async function loadCertReminders() {
|
||||
try {
|
||||
const [expiredRes, expiringRes] = await Promise.all([
|
||||
workerRegistryApi.getList({ page: 1, pageSize: 5, certStatus: 'expired' }),
|
||||
workerRegistryApi.getList({ page: 1, pageSize: 5, certStatus: 'expiring' }),
|
||||
]);
|
||||
const today = dayjs().startOf('day');
|
||||
const buildRows = (list: any[], expiredFlag: boolean): CertReminderRow[] => {
|
||||
const rows: CertReminderRow[] = [];
|
||||
for (const worker of list || []) {
|
||||
for (const cert of worker.certs || []) {
|
||||
if (!cert.expireDate) continue;
|
||||
const expire = String(cert.expireDate).slice(0, 10);
|
||||
const days = dayjs(expire).startOf('day').diff(today, 'day');
|
||||
// 已过期组取 < 0,临期组取 0 ~ 30 天
|
||||
const inRange = expiredFlag ? days < 0 : days >= 0 && days <= 30;
|
||||
if (!inRange) continue;
|
||||
rows.push({
|
||||
key: `${worker.id}-${cert.id}`,
|
||||
workerId: Number(worker.id),
|
||||
workerName: worker.workerName,
|
||||
certType: cert.certType || '资质证书',
|
||||
expireDate: expire,
|
||||
days,
|
||||
expired: expiredFlag,
|
||||
});
|
||||
}
|
||||
}
|
||||
rows.sort((a, b) => a.days - b.days);
|
||||
return rows.slice(0, 5);
|
||||
};
|
||||
const expiredRows = buildRows(expiredRes.code === 200 ? expiredRes.data.list : [], true);
|
||||
const expiringRows = buildRows(expiringRes.code === 200 ? expiringRes.data.list : [], false);
|
||||
certReminders.value = [...expiredRows, ...expiringRows];
|
||||
} catch (error) {
|
||||
// 提醒数据加载失败不影响主看板
|
||||
certReminders.value = [];
|
||||
}
|
||||
}
|
||||
|
||||
function goWorkerRegistry() {
|
||||
router.push('/dispatch/worker');
|
||||
}
|
||||
|
||||
const totalStatus = computed(() =>
|
||||
(stats.value?.customerStatus || []).reduce((s, it) => s + it.value, 0)
|
||||
);
|
||||
@@ -396,6 +502,7 @@ async function loadStats() {
|
||||
lastUpdateTime.value = new Date().toLocaleTimeString('zh-CN');
|
||||
updateCharts();
|
||||
}
|
||||
await loadCertReminders();
|
||||
} catch (error) {
|
||||
console.error('加载仪表盘数据失败:', error);
|
||||
message.error('加载数据失败,请稍后重试');
|
||||
@@ -721,6 +828,12 @@ function disposeCharts() {
|
||||
.card { } // col-7 / col-5
|
||||
}
|
||||
|
||||
// 电工资质到期提醒
|
||||
.remind-grid {
|
||||
margin-top: 16px;
|
||||
.card { }
|
||||
}
|
||||
|
||||
.live-alerts { font-size: 11px; font-weight: 700; color: var(--dw-error); background: rgba(220,38,38,.1); padding: 2px 8px; border-radius: 6px; animation: dw-pulse 1.8s infinite; }
|
||||
|
||||
// ===== 表格 =====
|
||||
@@ -760,6 +873,7 @@ function disposeCharts() {
|
||||
&-primary { color: var(--dw-primary); background: rgba(0,84,205,.12); }
|
||||
&-warn { color: var(--dw-warn); background: rgba(224,133,43,.14); }
|
||||
&-tertiary { color: var(--dw-tertiary); background: rgba(0,104,95,.12); }
|
||||
&-danger { color: var(--dw-error); background: rgba(220,38,38,.12); }
|
||||
}
|
||||
|
||||
// ===== 看板 =====
|
||||
|
||||
Reference in new issue
Block a user