diff --git a/.gitignore b/.gitignore index 5688e86..eb59fff 100644 --- a/.gitignore +++ b/.gitignore @@ -39,3 +39,6 @@ src/**/bin/ src/**/obj/ tests/**/bin/ tests/**/obj/ + +# === 前端部署到CncWebApi/admin === +src/CncWebApi/admin/ diff --git a/deploy-admin.ps1 b/deploy-admin.ps1 new file mode 100644 index 0000000..7e9609a --- /dev/null +++ b/deploy-admin.ps1 @@ -0,0 +1,67 @@ +# ============================================================ +# deploy-admin.ps1 — 一键编译后端+前端并部署到 admin 目录 +# 用法:在项目根目录执行 .\deploy-admin.ps1 +# ============================================================ + +[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 +[Console]::InputEncoding = [System.Text.Encoding]::UTF8 + +$ErrorActionPreference = "Stop" +$projectRoot = $PSScriptRoot + +Write-Host "" +Write-Host "========================================" -ForegroundColor Cyan +Write-Host " CNC 系统一键部署脚本" -ForegroundColor Cyan +Write-Host "========================================" -ForegroundColor Cyan +Write-Host "" + +# -------------------------------------------------- +# 第1步:编译后端 +# -------------------------------------------------- +Write-Host "[1/2] 编译后端 API ..." -ForegroundColor Yellow +dotnet build "$projectRoot\CncDataSystem.sln" +if ($LASTEXITCODE -ne 0) { + Write-Host "后端编译失败!" -ForegroundColor Red + exit 1 +} +Write-Host "后端编译完成 ✓" -ForegroundColor Green +Write-Host "" + +# -------------------------------------------------- +# 第2步:编译前端并输出到 admin 目录 +# -------------------------------------------------- +Write-Host "[2/2] 编译前端(输出到 src\CncWebApi\admin\)..." -ForegroundColor Yellow + +$frontendDir = Join-Path $projectRoot "frontend" + +# 安装依赖(如果 node_modules 不存在) +if (-not (Test-Path "$frontendDir\node_modules")) { + Write-Host " 安装前端依赖 ..." -ForegroundColor Gray + npm install --prefix $frontendDir + if ($LASTEXITCODE -ne 0) { + Write-Host "前端依赖安装失败!" -ForegroundColor Red + exit 1 + } +} + +# 构建前端(vite.config.ts 已配置 outDir 指向 ../src/CncWebApi/admin) +npm run build --prefix $frontendDir +if ($LASTEXITCODE -ne 0) { + Write-Host "前端编译失败!" -ForegroundColor Red + exit 1 +} + +$adminDir = Join-Path $projectRoot "src\CncWebApi\admin" +$fileCount = (Get-ChildItem $adminDir -Recurse -File).Count +Write-Host "前端编译完成 ✓($fileCount 个文件)" -ForegroundColor Green +Write-Host "" + +# -------------------------------------------------- +# 完成 +# -------------------------------------------------- +Write-Host "========================================" -ForegroundColor Cyan +Write-Host " 部署完成!" -ForegroundColor Cyan +Write-Host " 后端 API:http://192.168.1.202/api/health" -ForegroundColor White +Write-Host " 前端页面:http://192.168.1.202/admin/" -ForegroundColor White +Write-Host "========================================" -ForegroundColor Cyan +Write-Host "" diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts index a9d1a0c..a47f4ae 100644 --- a/frontend/src/types/index.ts +++ b/frontend/src/types/index.ts @@ -39,6 +39,12 @@ export interface Machine { isEnabled: boolean workerName?: string collectAddressName?: string + /** 编辑表单用:所属车间ID */ + workshopId?: number + /** 编辑表单用:采集地址ID */ + collectAddressId?: number + /** 编辑表单用:绑定工人ID */ + workerId?: number } /** 机床状态 */ @@ -47,6 +53,12 @@ export interface MachineStatus { partCount: number runStatus: string operationMode: string + /** 主轴设定转速 */ + spindleSpeedSet?: number + /** 进给设定速度 */ + feedSpeedSet?: number + /** 主轴实际转速 */ + spindleSpeedActual?: number spindleSpeed: number feedRate: number spindleLoad: number @@ -81,10 +93,14 @@ export interface CollectAddress { url: string brandName: string interval: number + /** 详情页显示用:采集间隔(秒) */ + collectInterval?: number isEnabled: boolean lastCollectTime: string machineCount: number failCount: number + /** 详情页:原始JSON */ + rawJson?: string } /** 工人 */ @@ -103,6 +119,8 @@ export interface Alert { alertType: string machineName: string message: string + title?: string + detail?: string isResolved: boolean createdAt: string resolvedAt?: string @@ -151,12 +169,21 @@ export interface Workshop { /** 操作日志 */ export interface OperationLog { id: number - timestamp: string - level: string + /** 日志时间 */ + createdAt: string + /** 日志级别(ERROR/WARN/INFO/DEBUG) */ + logLevel: string source: string message: string stackTrace?: string extraData?: string + /** 产量修正日志扩展字段 */ + targetTable?: string + targetId?: number + oldValue?: number + newValue?: number + reason?: string + operatorIp?: string } /** 大屏卡片配置 */ @@ -170,6 +197,13 @@ export interface ScreenCard { sortOrder: number isEnabled: boolean chartConfig?: string + /** 前端展示用标题(同 cardName) */ + title?: string + /** 前端展示用(同 cardKey) */ + screenKey?: string + filterType?: string + filterValue?: string + isDefault?: boolean | number } /** 大屏筛选配置 */ diff --git a/frontend/src/utils/request.ts b/frontend/src/utils/request.ts index 7fc9f1d..2a3704f 100644 --- a/frontend/src/utils/request.ts +++ b/frontend/src/utils/request.ts @@ -1,6 +1,7 @@ -import axios, { type AxiosResponse, type InternalAxiosRequestConfig } from 'axios' +import axios, { type AxiosResponse, type InternalAxiosRequestConfig, type Method } from 'axios' import { ElMessage } from 'element-plus' import router from '@/router' +import type { ApiResponse } from '@/types' // 创建axios实例 const service = axios.create({ @@ -24,10 +25,10 @@ service.interceptors.request.use( (error) => Promise.reject(error) ) -// 响应拦截器 +// 响应拦截器:解包 AxiosResponse → ApiResponse service.interceptors.response.use( - (response: AxiosResponse) => { - const res = response.data + (response: AxiosResponse): any => { + const res = response.data as ApiResponse if (res.code === 0) { return res } @@ -63,4 +64,24 @@ service.interceptors.response.use( } ) -export default service +/** + * 类型安全的请求封装 + * 响应拦截器已将 AxiosResponse 解包为 ApiResponse + * 调用方拿到的就是 ApiResponse,可直接访问 .data / .code / .message + */ +const request = { + get(url: string, config?: object): Promise> { + return service.get(url, config) as unknown as Promise> + }, + post(url: string, data?: unknown, config?: object): Promise> { + return service.post(url, data, config) as unknown as Promise> + }, + put(url: string, data?: unknown, config?: object): Promise> { + return service.put(url, data, config) as unknown as Promise> + }, + delete(url: string, config?: object): Promise> { + return service.delete(url, config) as unknown as Promise> + }, +} + +export default request diff --git a/frontend/src/views/alert/AlertPage.vue b/frontend/src/views/alert/AlertPage.vue index 8f5ae3d..646ce07 100644 --- a/frontend/src/views/alert/AlertPage.vue +++ b/frontend/src/views/alert/AlertPage.vue @@ -45,11 +45,11 @@
- 批量标记已处理 + 批量标记已处理
- + @@ -107,12 +107,22 @@ import { ref, reactive, onMounted } from 'vue' import { ElMessage, ElMessageBox } from 'element-plus' import request from '@/utils/request' +import type { ApiResponse, Alert, Machine } from '@/types' + +/** 告警统计信息 */ +interface AlertStats { + unresolved: number + collectFail: number + deviceOffline: number + productionAnomaly: number + unknownDevice: number +} const loading = ref(false) const tableData = ref([]) const selectedRows = ref([]) -const stats = ref({} as Alert) -const machineList = ref([]) +const stats = ref({} as AlertStats) +const machineList = ref([]) const detailVisible = ref(false) const detailRow = ref({} as Alert) @@ -152,11 +162,11 @@ function alertTypeLabel(type: string): string { async function loadData() { loading.value = true try { - const [s, d]: Record[] = await Promise.all([ - request.get('/admin/alert/statistics'), - request.get('/admin/alert', { params: { ...query, ...page } }), + const [s, d] = await Promise.all([ + request.get('/admin/alert/statistics'), + request.get<{ items: Alert[]; total: number }>('/admin/alert', { params: { ...query, ...page } }), ]) - stats.value = s.data || {} + stats.value = s.data || {} as AlertStats tableData.value = d.data?.items || [] page.total = d.data?.total || 0 } finally { @@ -177,9 +187,9 @@ async function handleResolve(row: Alert) { } async function batchResolve() { - const unresolved = selectedRows.value.filter(r => !r.isResolved) + const unresolved = selectedRows.value.filter((r: Alert) => !r.isResolved) await ElMessageBox.confirm(`确定对选中的${unresolved.length}项标记为已处理?`, '提示', { type: 'warning' }) - await request.post('/admin/alert/batch-resolve', { ids: unresolved.map(r => r.id) }) + await request.post('/admin/alert/batch-resolve', { ids: unresolved.map((r: Alert) => r.id) }) ElMessage.success('批量标记成功') loadData() } @@ -190,7 +200,7 @@ function viewDetail(row: Alert) { } async function loadDrops() { - const r: Record = await request.get('/admin/machine/list') + const r = await request.get<{ items: Machine[] }>('/admin/machine/list') machineList.value = r.data?.items || [] } diff --git a/frontend/src/views/brand/BrandEditPage.vue b/frontend/src/views/brand/BrandEditPage.vue index 8f4cd3d..6c7d3fa 100644 --- a/frontend/src/views/brand/BrandEditPage.vue +++ b/frontend/src/views/brand/BrandEditPage.vue @@ -44,8 +44,8 @@ const form = reactive({ brandName: '', deviceField: 'device', tagsPath: 'tags', function addMapping() { form.mappings.push({ standardField: '', fieldName: '', matchBy: 'id', dataType: 'string', isRequired: 0 }) } async function loadData() { if (!isEdit) return - const r: Record = await request.get('/admin/brand/detail', { params: { id: route.params.id } }) - if (r.data) { form.brandName = r.data.brandName; form.deviceField = r.data.deviceField; form.tagsPath = r.data.tagsPath; form.mappings = r.data.mappings || [] } + const r = await request.get('/admin/brand/detail', { params: { id: route.params.id } }) + if (r.data) { form.brandName = r.data.brandName; form.deviceField = r.data.deviceField; form.tagsPath = r.data.tagsPath; form.mappings = (r.data as any).mappings || [] } } async function handleSave() { await ElMessageBox.confirm('品牌模板修改不影响历史数据,确定保存?', '提示', { type: 'warning' }) diff --git a/frontend/src/views/collect-address/CollectAddressDetailPage.vue b/frontend/src/views/collect-address/CollectAddressDetailPage.vue index 647776f..e714bf0 100644 --- a/frontend/src/views/collect-address/CollectAddressDetailPage.vue +++ b/frontend/src/views/collect-address/CollectAddressDetailPage.vue @@ -15,7 +15,7 @@ {{detail.name}} {{detail.url}} {{detail.brandName}} - {{detail.collectInterval}}秒 + {{detail.collectInterval ?? detail.interval}}秒 {{detail.isEnabled?'启用':'停用'}} {{detail.lastCollectTime||'-'}} @@ -46,35 +46,43 @@ import {ref,onMounted} from 'vue' import {useRoute} from 'vue-router' import request from '@/utils/request' -import type { ApiResponse, CollectAddress, Machine } from '@/types' +import type { ApiResponse, CollectAddress } from '@/types' import PageHeader from '@/components/PageHeader.vue' import { useMockPath } from '@/composables/useMockPath' + +type CollectMachineRow = { machineName: string; deviceCode?: string; workshopName?: string; isOnline?: boolean; programName?: string } +type CollectRecordRow = { requestTime: string; duration: number; isSuccess: boolean; machineCount: number; machineName?: string } + const { isMock } = useMockPath() const homePath = isMock ? '/mock/dashboard' : '/dashboard' const collectAddressPath = isMock ? '/mock/collect-address' : '/collect-address' const route=useRoute() -const detail=ref({} as CollectAddress); type CollectMachineRow = { machineName: string; deviceCode?: string; workshopName?: string; isOnline?: boolean; programName?: string }; const machines=ref([]); const records=ref([]) +const detail=ref({} as CollectAddress) +const machines=ref([]) +const records=ref([]) // 原始JSON弹窗相关 const rawJsonDialogVisible = ref(false) const rawJsonContent = ref('') const rawJsonTitle = ref('原始采集数据') -async function viewRawJson(record: Record){ - // 请求原始JSON(mock 接口) + +async function viewRawJson(record: CollectRecordRow){ + // 请求原始JSON const id = route.params.id - const resp: Record = await request.get('/admin/collect-address/raw-json', { params: { id, recordId: record.requestTime } }) - const raw = resp?.data?.rawJson ?? '[]' + const resp = await request.get<{ rawJson: string }>('/admin/collect-address/raw-json', { params: { id, recordId: record.requestTime } }) + const raw = resp.data?.rawJson ?? '[]' let parsed: unknown try { parsed = JSON.parse(raw) } catch { parsed = raw } rawJsonContent.value = JSON.stringify(parsed, null, 2) rawJsonTitle.value = `原始采集数据 - ${record.machineName ?? ''}` rawJsonDialogVisible.value = true } + async function loadData(){ const id=route.params.id - const [d,m,r]: Record[] = await Promise.all([ - request.get('/admin/collect-address/detail', { params: { id } }), - request.get('/admin/collect-address/machines', { params: { id } }), - request.get('/admin/collect-address/collect-records', { params: { id } }) + const [d,m,r] = await Promise.all([ + request.get('/admin/collect-address/detail', { params: { id } }), + request.get<{ items: CollectMachineRow[] }>('/admin/collect-address/machines', { params: { id } }), + request.get<{ items: CollectRecordRow[] }>('/admin/collect-address/collect-records', { params: { id } }), ]) detail.value = d.data ?? ({} as CollectAddress) machines.value = m.data?.items ?? [] diff --git a/frontend/src/views/dashboard/DashboardPage.vue b/frontend/src/views/dashboard/DashboardPage.vue index 6d14633..b628884 100644 --- a/frontend/src/views/dashboard/DashboardPage.vue +++ b/frontend/src/views/dashboard/DashboardPage.vue @@ -446,7 +446,7 @@ function disposeCharts() { async function loadWorkshopData() { try { const { startDate, endDate } = getDateRange(workshopDateType.value, workshopDateRange.value) - const res: ApiResponse<{ items: WorkshopProduction }> = await request.get('/admin/dashboard/workshop-production', { params: { startDate, endDate } }) + const res = await request.get<{ items: WorkshopProduction[] }>('/admin/dashboard/workshop-production', { params: { startDate, endDate } }) workshopData.value = res.data?.items || [] workshopChart?.dispose(); workshopChart = null await nextTick(); initWorkshopChart() @@ -471,17 +471,12 @@ async function loadWorkerRankData() { async function loadData() { try { - const [summaryRes, collectorRes, trendRes, statusRes, alertsRes]: [ - ApiResponse, ApiResponse, - ApiResponse<{ items: DashboardTrendItem[] }>, - ApiResponse, - ApiResponse<{ items: RecentAlert[] }> - ] = await Promise.all([ - request.get('/admin/dashboard/summary'), - request.get('/admin/collector/status'), - request.get('/admin/dashboard/trend'), - request.get('/admin/dashboard/machine-status-distribution'), - request.get('/admin/dashboard/recent-alerts'), + const [summaryRes, collectorRes, trendRes, statusRes, alertsRes] = await Promise.all([ + request.get('/admin/dashboard/summary'), + request.get('/admin/collector/status'), + request.get<{ items: DashboardTrendItem[] }>('/admin/dashboard/trend'), + request.get('/admin/dashboard/machine-status-distribution'), + request.get<{ items: RecentAlert[] }>('/admin/dashboard/recent-alerts'), ]) summary.value = summaryRes.data || summary.value collectorStatus.value = collectorRes.data || collectorStatus.value diff --git a/frontend/src/views/log/LogPage.vue b/frontend/src/views/log/LogPage.vue index 3da06be..2323167 100644 --- a/frontend/src/views/log/LogPage.vue +++ b/frontend/src/views/log/LogPage.vue @@ -145,6 +145,7 @@ import { ref, reactive, onMounted } from 'vue' import { ElMessage } from 'element-plus' import request from '@/utils/request' +import type { ApiResponse, OperationLog } from '@/types' const activeTab = ref('adjustment') @@ -169,11 +170,11 @@ function targetTableLabel(table: string): string { async function loadAdjustment() { adjLoading.value = true try { - const params: Record = { page: adjPage.page, pageSize: adjPage.pageSize } + const params: Record = { page: adjPage.page, pageSize: adjPage.pageSize } if (adjQuery.dateRange?.length === 2) { params.startDate = adjQuery.dateRange[0]; params.endDate = adjQuery.dateRange[1] } if (adjQuery.targetTable) params.targetTable = adjQuery.targetTable if (adjQuery.keyword) params.keyword = adjQuery.keyword - const r: Record = await request.get('/admin/log/adjustment', { params }) + const r = await request.get<{ items: OperationLog[]; total: number }>('/admin/log/adjustment', { params }) adjList.value = r.data?.items || [] adjPage.total = r.data?.total || 0 } finally { adjLoading.value = false } @@ -187,14 +188,15 @@ function resetAdjQuery() { async function exportAdjustment() { try { - const params: Record = {} + const params: Record = {} if (adjQuery.dateRange?.length === 2) { params.startDate = adjQuery.dateRange[0]; params.endDate = adjQuery.dateRange[1] } if (adjQuery.targetTable) params.targetTable = adjQuery.targetTable if (adjQuery.keyword) params.keyword = adjQuery.keyword // 导出Excel:直接打开新窗口下载 - const baseURL = (request as any).defaults?.baseURL || '' const qs = new URLSearchParams(params).toString() - window.open(`${baseURL}/admin/log/adjustment/export?${qs}`, '_blank') + const isMock = window.location.pathname.startsWith('/mock') + const base = isMock ? '/mock-api' : '/api' + window.open(`${base}/admin/log/adjustment/export?${qs}`, '_blank') ElMessage.success('正在导出...') } catch { ElMessage.error('导出失败') } } @@ -220,12 +222,12 @@ function logLevelTag(level: string): string { async function loadSystem() { sysLoading.value = true try { - const params: Record = { page: sysPage.page, pageSize: sysPage.pageSize } + const params: Record = { page: sysPage.page, pageSize: sysPage.pageSize } if (sysQuery.logLevel) params.logLevel = sysQuery.logLevel if (sysQuery.source) params.source = sysQuery.source if (sysQuery.dateRange?.length === 2) { params.startDate = sysQuery.dateRange[0]; params.endDate = sysQuery.dateRange[1] } if (sysQuery.keyword) params.keyword = sysQuery.keyword - const r: Record = await request.get('/admin/log/system', { params }) + const r = await request.get<{ items: OperationLog[]; total: number }>('/admin/log/system', { params }) sysList.value = r.data?.items || [] sysPage.total = r.data?.total || 0 } finally { sysLoading.value = false } diff --git a/frontend/src/views/machine/MachineDetailPage.vue b/frontend/src/views/machine/MachineDetailPage.vue index c3ab28c..c2291e6 100644 --- a/frontend/src/views/machine/MachineDetailPage.vue +++ b/frontend/src/views/machine/MachineDetailPage.vue @@ -28,7 +28,7 @@ {{status.programName||'-'}} {{status.partCount??'-'}} {{status.runStatus||'-'}} - {{status.operateMode||'-'}} + {{status.operationMode||'-'}} {{status.spindleSpeedSet??'-'}} {{status.feedSpeedSet??'-'}} {{status.spindleSpeedActual??'-'}} @@ -59,41 +59,54 @@ import { ref, onMounted, nextTick, onBeforeUnmount } from 'vue' import { useRoute } from 'vue-router' import request from '@/utils/request' import echarts from '@/utils/echarts' -// 引入 ECharts 的类型定义,便于 TS 断言 import type { ECharts } from 'echarts/core' +import type { ApiResponse, Machine, MachineStatus } from '@/types' + +/** 今日产量行 */ +interface TodayProdRow { programName: string; quantity: number; runTime: number; cuttingTime: number } +/** 采集记录行 */ +interface CollectRecordRow { collectTime: string; programName: string; partCount: number; runStatus: string } +/** 趋势数据项 */ +interface TrendItem { date: string; quantity: number } + const route = useRoute() // Mock 模式路径前缀处理 const isMockPath = typeof window !== 'undefined' && window.location.pathname.startsWith('/mock') const homePath = isMockPath ? '/mock/dashboard' : '/dashboard' const machinePath = isMockPath ? '/mock/machine' : '/machine' const detail = ref({} as Machine) -const status = ref({} as Machine) +const status = ref({} as MachineStatus) let statusInterval: number | undefined -const todayProd = ref([]); const records = ref([]) +const todayProd = ref([]) +const records = ref([]) const chartRef = ref() let chart: ECharts | null = null + async function loadData() { const id = route.params.id - const [d, s, t, r]: Record[] = await Promise.all([ - request.get('/admin/machine/detail', { params: { id } }), - request.get('/admin/machine/status', { params: { id } }), - request.get('/admin/machine/production/today', { params: { id } }), - request.get('/admin/machine/collect-records', { params: { id } }), + const [d, s, t, r] = await Promise.all([ + request.get('/admin/machine/detail', { params: { id } }), + request.get('/admin/machine/status', { params: { id } }), + request.get<{ items: TodayProdRow[] }>('/admin/machine/production/today', { params: { id } }), + request.get<{ items: CollectRecordRow[] }>('/admin/machine/collect-records', { params: { id } }), ]) - detail.value = d.data || {}; status.value = s.data || {} - todayProd.value = t.data?.items || []; records.value = r.data?.items || [] - const trend: Record = await request.get('/admin/machine/production/trend', { params: { id } }) + detail.value = d.data || {} as Machine + status.value = s.data || {} as MachineStatus + todayProd.value = t.data?.items || [] + records.value = r.data?.items || [] + const trend = await request.get<{ items: TrendItem[] }>('/admin/machine/production/trend', { params: { id } }) await nextTick() if (chartRef.value) { chart = echarts.init(chartRef.value) const items = trend.data?.items || [] - chart.setOption({ xAxis: { type: 'category', data: items.map((i: Record) => i.date.slice(5)) }, yAxis: { type: 'value' }, series: [{ type: 'line', data: items.map((i: Record) => i.quantity), smooth: true, areaStyle: { opacity: 0.1 } }], tooltip: { trigger: 'axis' }, grid: { left: 40, right: 20, top: 10, bottom: 30 } }) + chart.setOption({ xAxis: { type: 'category', data: items.map((i: TrendItem) => i.date.slice(5)) }, yAxis: { type: 'value' }, series: [{ type: 'line', data: items.map((i: TrendItem) => i.quantity), smooth: true, areaStyle: { opacity: 0.1 } }], tooltip: { trigger: 'axis' }, grid: { left: 40, right: 20, top: 10, bottom: 30 } }) } } + async function fetchStatus() { const id = route.params.id - const r: Record = await request.get('/admin/machine/status', { params: { id } }) - status.value = r.data || {} + const r = await request.get('/admin/machine/status', { params: { id } }) + status.value = r.data || {} as MachineStatus } onMounted(() => { diff --git a/frontend/src/views/machine/MachineListPage.vue b/frontend/src/views/machine/MachineListPage.vue index bd913e3..9b34a50 100644 --- a/frontend/src/views/machine/MachineListPage.vue +++ b/frontend/src/views/machine/MachineListPage.vue @@ -32,7 +32,7 @@ - +