feat: 搭建VS2017解决方案骨架(8项目+前端文件夹),编译通过0错误

- CncDataSystem.sln: 4层源码项目 + 4测试项目 + frontend解决方案文件夹
- CncModels: ApiResponse/PagedResult/PagedQuery/ErrorCode 基础类
- CncRepository: BaseRepository 双库连接管理(BusinessRepository/LogRepository)
- CncService: BusinessException 业务异常基类
- CncWebApi: WebApiConfig路由配置/GlobalExceptionFilter/HealthController/Web.config
- 测试项目: xUnit + Moq + Microsoft.NET.Test.Sdk
- NuGet: Dapper + MySqlConnector + Newtonsoft.Json + log4net + JWT
- 编译: dotnet build 8项目全部通过
main
haoliang 1 week ago
parent 9bb4b8246d
commit 0ba5cede73

6
.gitignore vendored

@ -33,3 +33,9 @@ npm-debug.log*
# TypeScript cache
*.tsbuildinfo
# === 后端编译输出 ===
src/**/bin/
src/**/obj/
tests/**/bin/
tests/**/obj/

@ -5,22 +5,35 @@
## 概述
多品牌CNC机床统一化数据采集系统的产品设计文档库。技术栈Vue3前端 + ASP.NET Web API 2后端 + Windows Service采集服务 + MariaDB数据库。本地IIS部署局域网场景。
多品牌CNC机床统一化数据采集系统。技术栈Vue3前端 + ASP.NET Web API 2后端 + Windows Service采集服务 + MariaDB数据库。本地IIS部署局域网场景。
## 项目结构
```
.
├── docs/ # 全部设计文档
├── CncDataSystem.sln ← VS2017 解决方案4层+3测试项目+前端文件夹)
├── src/ ← 后端源码
│ ├── CncModels/ ← 数据模型层Entity + Dto + Enum + Constants
│ ├── CncRepository/ ← 数据访问层Dapper + MariaDB双库
│ ├── CncService/ ← 业务逻辑层(接口 + 实现)
│ └── CncWebApi/ ← Web API 主项目Controllers + Filters + Infrastructure
├── tests/ ← 测试项目xUnit + Moq100%方法覆盖 ≥95%分支覆盖)
│ ├── CncModels.Tests/
│ ├── CncRepository.Tests/
│ ├── CncService.Tests/
│ └── CncWebApi.Tests/
├── database/ ← 数据库脚本幂等DDL+预置数据)
├── docs/ ← 全部设计文档
│ ├── 00-需求与设计文档.md # 核心需求、架构决策、技术选型
│ ├── 01-数据库设计.md # 双库设计(业务库+日志库)17张表DDL
│ ├── 01-数据库设计.md # 双库设计(业务库+日志库)20张表DDL已定稿
│ ├── 02-功能清单/ # 按模块拆分的功能清单
│ │ ├── 02-文件夹创建规范.md # ⚠️ 文档结构规范,新增模块必读
│ │ ├── 02-前端全局规范.md # 前端全局规范+CRUD必填项+模块进度+工程开发规范
│ │ ├── 03-界面变更执行规范.md # ⚠️ AI助手执行界面变更时必读
│ │ ├── 大屏/ # 大屏看板模块
│ │ └── 管理后台/ # 12个子模块登录/仪表盘/设备/品牌/采集地址/员工/产量/告警/系统/车间/操作日志/大屏配置)
│ └── ...
│ ├── 03-API接口设计.md # 13模块83端点含Mock映射+DB表对照
│ └── 04-后端开发规范.md # ⚠️ 后端必读:技术栈/项目结构/命名/注释/测试/分层/依赖注入规范
├── frontend/ # 前端工程Vue3+Vite+TypeScript+Element Plus
│ ├── mock/ # Mock数据定义按模块拆分
│ ├── src/ # 源码(路由/组件/样式/类型)
@ -38,6 +51,8 @@
| 前端工程开发规范 | `docs/02-前端全局规范.md` → 前端工程开发规范章节 | 技术栈/Mock方案/目录结构/CSS规范 |
| 新增功能模块文档 | `docs/02-功能清单/02-文件夹创建规范.md` | 目录结构/命名/内容模板规范 |
| AI助手执行界面变更 | `docs/02-功能清单/03-界面变更执行规范.md` | ⚠️ 必读:最小必读文件+变更清单+联动同步规则 |
| 后端开发规范 | `docs/04-后端开发规范.md` | ⚠️ 后端必读:技术栈/项目结构/命名/注释/测试/分层/依赖注入 |
| API接口设计 | `docs/03-API接口设计.md` | 13模块83端点含Mock映射+DB表对照 |
| 品牌采集数据格式 | `发那科系统采集示例.txt` | FANUC品牌JSON结构示例 |
| 管理后台某模块设计 | `docs/02-功能清单/管理后台/{编号}-{模块名}/` | 每模块含索引+规范+页面文件 |
| 大屏看板设计 | `docs/02-功能清单/大屏/` | 索引+规范+页面 |
@ -85,10 +100,15 @@
## 待设计模块
- API接口设计后置界面全部确认后启动
- 采集服务核心逻辑设计(后置,界面全部确认后启动)
- 前端管理后台设计(✅ 已完成,见 `02-前端全局规范.md` + `02-功能清单/管理后台/`
- 大屏看板设计(✅ 已完成,见 `02-功能清单/大屏/`
- 采集服务核心逻辑设计后置后端API完成后启动
## 已完成模块
- ✅ 前端管理后台设计(见 `02-前端全局规范.md` + `02-功能清单/管理后台/`
- ✅ 大屏看板设计(见 `02-功能清单/大屏/`
- ✅ 数据库设计定稿(见 `01-数据库设计.md`20张表已落地MariaDB
- ✅ API接口设计`03-API接口设计.md`13模块83端点
- ✅ 后端开发规范(见 `04-后端开发规范.md`
## 强制要求(必须遵守)
@ -125,6 +145,6 @@
## 注意事项
- 部分索引/规范文件内容为空(尚未填写)→ 已全部填写
- 数据库设计为草案状态(状态:草案),界面确认后定稿
- 数据库设计已定稿(状态:定稿)20张表已落地MariaDB
- 前端界面设计全部完成(2026-04-26)13个模块16个页面均已设计
- 品牌预置FANUC已预置其他品牌手动添加

@ -0,0 +1,72 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17.VisualStudioVersion = 17.0.31903.59
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CncModels", "src\CncModels\CncModels.csproj", "{898BBE60-B7CB-4602-BFC6-7651B062F986}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CncRepository", "src\CncRepository\CncRepository.csproj", "{534CA636-CA2C-4038-B02E-5258981479D3}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CncService", "src\CncService\CncService.csproj", "{8B105015-DFDF-4BFC-B0BA-471E0FA1AAC8}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CncWebApi", "src\CncWebApi\CncWebApi.csproj", "{A39DE219-F8CB-44CD-9907-8FCAA3766C8B}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CncModels.Tests", "tests\CncModels.Tests\CncModels.Tests.csproj", "{9AAF88A1-B04C-403E-A559-7622B13F9872}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CncRepository.Tests", "tests\CncRepository.Tests\CncRepository.Tests.csproj", "{D581A6D4-25A1-4143-8155-2A895AC52E5E}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CncService.Tests", "tests\CncService.Tests\CncService.Tests.csproj", "{BD63C81D-29E3-4B53-A41A-F10A04ED1052}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CncWebApi.Tests", "tests\CncWebApi.Tests\CncWebApi.Tests.csproj", "{4DAE1DA8-E028-4025-AA80-1E698976F74C}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "frontend", "frontend", "{D099B5F2-7E64-4A0A-B9ED-D085421E996E}"
ProjectSection(SolutionItems) = preProject
frontend\package.json = frontend\package.json
frontend\vite.config.ts = frontend\vite.config.ts
frontend\tsconfig.json = frontend\tsconfig.json
EndProjectSection
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{898BBE60-B7CB-4602-BFC6-7651B062F986}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{898BBE60-B7CB-4602-BFC6-7651B062F986}.Debug|Any CPU.Build.0 = Debug|Any CPU
{898BBE60-B7CB-4602-BFC6-7651B062F986}.Release|Any CPU.ActiveCfg = Release|Any CPU
{898BBE60-B7CB-4602-BFC6-7651B062F986}.Release|Any CPU.Build.0 = Release|Any CPU
{534CA636-CA2C-4038-B02E-5258981479D3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{534CA636-CA2C-4038-B02E-5258981479D3}.Debug|Any CPU.Build.0 = Debug|Any CPU
{534CA636-CA2C-4038-B02E-5258981479D3}.Release|Any CPU.ActiveCfg = Release|Any CPU
{534CA636-CA2C-4038-B02E-5258981479D3}.Release|Any CPU.Build.0 = Release|Any CPU
{8B105015-DFDF-4BFC-B0BA-471E0FA1AAC8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8B105015-DFDF-4BFC-B0BA-471E0FA1AAC8}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8B105015-DFDF-4BFC-B0BA-471E0FA1AAC8}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8B105015-DFDF-4BFC-B0BA-471E0FA1AAC8}.Release|Any CPU.Build.0 = Release|Any CPU
{A39DE219-F8CB-44CD-9907-8FCAA3766C8B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A39DE219-F8CB-44CD-9907-8FCAA3766C8B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A39DE219-F8CB-44CD-9907-8FCAA3766C8B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A39DE219-F8CB-44CD-9907-8FCAA3766C8B}.Release|Any CPU.Build.0 = Release|Any CPU
{9AAF88A1-B04C-403E-A559-7622B13F9872}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{9AAF88A1-B04C-403E-A559-7622B13F9872}.Debug|Any CPU.Build.0 = Debug|Any CPU
{9AAF88A1-B04C-403E-A559-7622B13F9872}.Release|Any CPU.ActiveCfg = Release|Any CPU
{9AAF88A1-B04C-403E-A559-7622B13F9872}.Release|Any CPU.Build.0 = Release|Any CPU
{D581A6D4-25A1-4143-8155-2A895AC52E5E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D581A6D4-25A1-4143-8155-2A895AC52E5E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D581A6D4-25A1-4143-8155-2A895AC52E5E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D581A6D4-25A1-4143-8155-2A895AC52E5E}.Release|Any CPU.Build.0 = Release|Any CPU
{BD63C81D-29E3-4B53-A41A-F10A04ED1052}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{BD63C81D-29E3-4B53-A41A-F10A04ED1052}.Debug|Any CPU.Build.0 = Debug|Any CPU
{BD63C81D-29E3-4B53-A41A-F10A04ED1052}.Release|Any CPU.ActiveCfg = Release|Any CPU
{BD63C81D-29E3-4B53-A41A-F10A04ED1052}.Release|Any CPU.Build.0 = Release|Any CPU
{4DAE1DA8-E028-4025-AA80-1E698976F74C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4DAE1DA8-E028-4025-AA80-1E698976F74C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4DAE1DA8-E028-4025-AA80-1E698976F74C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4DAE1DA8-E028-4025-AA80-1E698976F74C}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
EndGlobalSection
EndGlobal

@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

@ -0,0 +1,5 @@
# Vue 3 + TypeScript + Vite
This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
Learn more about the recommended Project Setup and IDE Support in the [Vue Docs TypeScript Guide](https://vuejs.org/guide/typescript/overview.html#project-setup).

@ -0,0 +1,77 @@
<template>
<div class="screen-page">
<!-- 顶部栏 -->
<header class="screen-header">
<div class="header-left">CNC机床数据采集系统</div>
<div class="header-right">
<el-select v-model="filterWorkshop" placeholder="车间" size="small" style="width:120px;margin-right:8px" @change="refreshAll">
<el-option label="全部" value="" />
<el-option v-for="w in workshopOptions" :key="w" :label="w" :value="w" />
</el-select>
<el-select v-model="filterBrand" placeholder="品牌" size="small" style="width:120px;margin-right:16px" @change="refreshAll">
<el-option label="全部" value="" />
<el-option v-for="b in brandOptions" :key="b" :label="b" :value="b" />
</el-select>
<span class="header-time">{{ currentTime }}</span>
</div>
</header>
<!-- 第一行统计数字卡片 -->
<div class="stat-row">
<div class="stat-card" v-for="s in statCards" :key="s.key">
<div class="stat-label">{{ s.label }}</div>
<div class="stat-value" :style="{ color: s.color }">{{ s.value }}</div>
<div class="stat-sub" v-if="s.sub">{{ s.sub }}</div>
</div>
</div>
<!-- 第二行图表区 -->
<div class="chart-row">
<div class="chart-card">
<div class="chart-title">各车间产量</div>
<div ref="barChartRef" class="chart-container"></div>
</div>
<div class="chart-card">
<div class="chart-title">产量趋势(7)</div>
<div ref="lineChartRef" class="chart-container"></div>
</div>
</div>
<!-- 第三行排行+状态 -->
<div class="bottom-row">
<div class="rank-card">
<div class="chart-title">机床产量排行</div>
<div class="rank-list">
<div v-if="machineRank.length === 0" class="no-data"></div>
<div v-for="item in machineRank" :key="item.rank" class="rank-item">
<span class="rank-num" :class="{ 'rank-top': item.rank <= 3 }">{{ item.rank }}</span>
<span class="rank-name">{{ item.machineName }}</span>
<span class="rank-value">{{ item.quantity }}</span>
</div>
</div>
</div>
<div class="rank-card">
<div class="chart-title">工人产量排行</div>
<div class="rank-list">
<div v-if="workerRank.length === 0" class="no-data"></div>
<div v-for="item in workerRank" :key="item.rank" class="rank-item">
<span class="rank-num" :class="{ 'rank-top': item.rank <= 3 }">{{ item.rank }}</span>
<span class="rank-name">{{ item.workerName }}</span>
<span class="rank-value">{{ item.quantity }}</span>
</div>
</div>
</div>
<div class="status-card">
<div class="chart-title">机床状态总览</div>
<div class="status-grid">
<div v-if="machineStatus.length === 0" class="no-data"></div>
<div v-for="m in machineStatus" :key="m.machineId" class="status-block" :class="m.isOnline ? 'online' : 'offline'" :title="m.machineName"></div>
</div>
<div class="status-legend">
<span class="legend-item"><span class="legend-block online"></span> 在线</span>
<span class="legend-item"><span class="legend-block offline"></span> 离线</span>
</div>
</div>
</div>
</div>
</template>

@ -0,0 +1,191 @@

<script setup lang="ts">
import { ref, reactive, onMounted, onUnmounted, nextTick } from 'vue'
import * as echarts from 'echarts'
import { ElMessage } from 'element-plus'
import request from '@/utils/request'
const filterWorkshop = ref('')
const filterBrand = ref('')
const workshopOptions = ref<string[]>([])
const brandOptions = ref<string[]>([])
const currentTime = ref('')
let timeTimer: ReturnType<typeof setInterval> | null = null
const statCards = reactive([
{ key: 'online', label: '在线机床', value: '--', sub: '', color: '#00e5ff' },
{ key: 'production', label: '今日总产量', value: '--', sub: '', color: '#76ff03' },
{ key: 'collector', label: '采集服务', value: '--', sub: '', color: '#69f0ae' },
{ key: 'alerts', label: '活跃告警', value: '--', sub: '', color: '#ffd740' },
{ key: 'avg', label: '平均产量/台', value: '--', sub: '', color: '#e0e0e0' },
])
const barChartRef = ref<HTMLElement>()
const lineChartRef = ref<HTMLElement>()
let barChart: echarts.ECharts | null = null
let lineChart: echarts.ECharts | null = null
const machineRank = ref<any[]>([])
const workerRank = ref<any[]>([])
const machineStatus = ref<any[]>([])
let refreshTimer: ReturnType<typeof setInterval> | null = null
const REFRESH_INTERVAL = 10000
function updateTime() {
const now = new Date()
currentTime.value = now.toLocaleString('zh-CN', {
year: 'numeric', month: '2-digit', day: '2-digit',
hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false,
})
}
function filterParams(): Record<string, string> {
const p: Record<string, string> = {}
if (filterWorkshop.value) p.workshop = filterWorkshop.value
if (filterBrand.value) p.brand = filterBrand.value
return p
}
async function loadFilters() {
try {
const r: any = await request.get('/screen/filters')
const items = r.data?.items || []
workshopOptions.value = items.filter((i: any) => i.filterType === 'workshop').map((i: any) => i.filterValue)
brandOptions.value = items.filter((i: any) => i.filterType === 'brand').map((i: any) => i.filterValue)
const defW = items.find((i: any) => i.filterType === 'workshop' && i.isDefault === 1)
if (defW) filterWorkshop.value = defW.filterValue
const defB = items.find((i: any) => i.filterType === 'brand' && i.isDefault === 1)
if (defB) filterBrand.value = defB.filterValue
} catch { /* 静默 */ }
}
async function loadSummary() {
try {
const r: any = await request.get('/screen/summary', { params: filterParams() })
const d = r.data || {}
statCards[0].value = d.onlineCount != null ? String(d.onlineCount) : '--'
statCards[0].sub = d.totalMachines ? '/ ' + d.totalMachines : ''
statCards[1].value = d.todayProduction != null ? d.todayProduction.toLocaleString() : '--'
statCards[3].value = d.activeAlerts != null ? String(d.activeAlerts) : '--'
statCards[4].value = d.avgQuantityPerMachine != null ? String(d.avgQuantityPerMachine) : '--'
} catch { /* 静默 */ }
}
async function loadCollectorStatus() {
try {
const r: any = await request.get('/screen/collector-status')
const d = r.data || {}
if (d.status === 'running') {
statCards[2].value = '运行中'
statCards[2].sub = d.uptime || ''
statCards[2].color = '#69f0ae'
} else {
statCards[2].value = '已停止'
statCards[2].sub = ''
statCards[2].color = '#ff5252'
}
} catch { /* 静默 */ }
}
async function loadWorkshopProduction() {
try {
const r: any = await request.get('/screen/workshop-production', { params: filterParams() })
const items = r.data?.items || []
if (!barChart) return
barChart.setOption({
tooltip: { trigger: 'axis' },
grid: { left: 60, right: 20, top: 20, bottom: 30 },
xAxis: { type: 'category', data: items.map((i: any) => i.name), axisLabel: { color: '#aaa' }, axisLine: { lineStyle: { color: '#333' } } },
yAxis: { type: 'value', axisLabel: { color: '#aaa' }, splitLine: { lineStyle: { color: '#222' } } },
series: [{ type: 'bar', data: items.map((i: any) => i.quantity), itemStyle: { color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [{ offset: 0, color: '#00e5ff' }, { offset: 1, color: '#006064' }]) }, barWidth: 30 }],
})
} catch { /* 静默 */ }
}
async function loadProductionTrend() {
try {
const r: any = await request.get('/screen/production-trend', { params: filterParams() })
const items = r.data?.items || []
if (!lineChart) return
lineChart.setOption({
tooltip: { trigger: 'axis' },
grid: { left: 60, right: 20, top: 20, bottom: 30 },
xAxis: { type: 'category', data: items.map((i: any) => i.date), axisLabel: { color: '#aaa' }, axisLine: { lineStyle: { color: '#333' } } },
yAxis: { type: 'value', axisLabel: { color: '#aaa' }, splitLine: { lineStyle: { color: '#222' } } },
series: [{
type: 'line', data: items.map((i: any) => i.quantity), smooth: true,
lineStyle: { color: '#76ff03', width: 2 }, itemStyle: { color: '#76ff03' },
areaStyle: { color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [{ offset: 0, color: 'rgba(118,255,3,0.3)' }, { offset: 1, color: 'rgba(118,255,3,0)' }]) },
}],
})
} catch { /* 静默 */ }
}
async function loadMachineRank() {
try {
const r: any = await request.get('/screen/machine-rank', { params: filterParams() })
machineRank.value = r.data?.items || []
} catch { /* 静默 */ }
}
async function loadWorkerRank() {
try {
const r: any = await request.get('/screen/worker-rank', { params: filterParams() })
workerRank.value = r.data?.items || []
} catch { /* 静默 */ }
}
async function loadMachineStatus() {
try {
const r: any = await request.get('/screen/machine-status', { params: filterParams() })
machineStatus.value = r.data?.items || []
} catch { /* 静默 */ }
}
function refreshAll() {
loadSummary()
loadCollectorStatus()
loadWorkshopProduction()
loadProductionTrend()
loadMachineRank()
loadWorkerRank()
loadMachineStatus()
}
function initCharts() {
if (barChartRef.value) {
barChart = echarts.init(barChartRef.value)
barChart.setOption({ backgroundColor: 'transparent' })
}
if (lineChartRef.value) {
lineChart = echarts.init(lineChartRef.value)
lineChart.setOption({ backgroundColor: 'transparent' })
}
}
function handleResize() {
barChart?.resize()
lineChart?.resize()
}
onMounted(async () => {
updateTime()
timeTimer = setInterval(updateTime, 1000)
ElMessage({ message: '按F11进入全屏模式', type: 'info', duration: 5000 })
await loadFilters()
await nextTick()
initCharts()
refreshAll()
refreshTimer = setInterval(refreshAll, REFRESH_INTERVAL)
window.addEventListener('resize', handleResize)
})
onUnmounted(() => {
if (timeTimer) clearInterval(timeTimer)
if (refreshTimer) clearInterval(refreshTimer)
barChart?.dispose()
lineChart?.dispose()
window.removeEventListener('resize', handleResize)
})
</script>

@ -0,0 +1,207 @@

<style scoped>
.screen-page {
background: #0f0f1a;
color: #e0e0e0;
min-height: 100vh;
display: flex;
flex-direction: column;
font-family: 'Microsoft YaHei', sans-serif;
overflow: hidden;
}
.screen-header {
height: 50px;
background: #1a1a2e;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 24px;
border-bottom: 1px solid #2a2a3e;
flex-shrink: 0;
}
.header-left {
font-size: 20px;
font-weight: bold;
color: #00e5ff;
letter-spacing: 2px;
}
.header-right {
display: flex;
align-items: center;
}
.header-time {
color: #aaa;
font-size: 14px;
font-family: 'Consolas', monospace;
}
.stat-row {
display: flex;
gap: 16px;
padding: 16px 24px;
flex-shrink: 0;
}
.stat-card {
flex: 1;
background: #1a1a2e;
border-radius: 8px;
padding: 12px 16px;
text-align: center;
border: 1px solid #2a2a3e;
transition: box-shadow 0.3s;
}
.stat-card:hover {
box-shadow: 0 0 12px rgba(0, 229, 255, 0.15);
}
.stat-label {
font-size: 13px;
color: #909399;
margin-bottom: 4px;
}
.stat-value {
font-size: 28px;
font-weight: bold;
letter-spacing: 1px;
}
.stat-sub {
font-size: 12px;
color: #606266;
margin-top: 2px;
}
.chart-row {
display: flex;
gap: 16px;
padding: 0 24px 16px;
flex-shrink: 0;
}
.chart-card {
flex: 1;
background: #1a1a2e;
border-radius: 8px;
padding: 12px;
border: 1px solid #2a2a3e;
height: 300px;
}
.chart-title {
font-size: 14px;
color: #bbb;
margin-bottom: 8px;
}
.chart-container {
width: 100%;
height: calc(100% - 30px);
}
.bottom-row {
display: flex;
gap: 16px;
padding: 0 24px 16px;
flex: 1;
min-height: 0;
}
.rank-card {
flex: 1;
background: #1a1a2e;
border-radius: 8px;
padding: 12px;
border: 1px solid #2a2a3e;
display: flex;
flex-direction: column;
}
.rank-list {
flex: 1;
overflow-y: auto;
}
.rank-item {
display: flex;
align-items: center;
padding: 6px 0;
border-bottom: 1px solid #2a2a3e;
}
.rank-item:last-child { border-bottom: none; }
.rank-num {
width: 24px;
height: 24px;
border-radius: 4px;
background: #2a2a3e;
color: #909399;
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 12px;
margin-right: 10px;
flex-shrink: 0;
}
.rank-num.rank-top {
background: #00e5ff;
color: #0f0f1a;
font-weight: bold;
}
.rank-name {
flex: 1;
font-size: 13px;
}
.rank-value {
font-size: 14px;
font-weight: bold;
color: #76ff03;
}
.status-card {
flex: 1;
background: #1a1a2e;
border-radius: 8px;
padding: 12px;
border: 1px solid #2a2a3e;
display: flex;
flex-direction: column;
}
.status-grid {
flex: 1;
display: flex;
flex-wrap: wrap;
gap: 6px;
align-content: flex-start;
overflow-y: auto;
}
.status-block {
width: 18px;
height: 18px;
border-radius: 3px;
cursor: default;
}
.status-block.online {
background: #4caf50;
box-shadow: 0 0 4px rgba(76, 175, 80, 0.5);
}
.status-block.offline {
background: #555;
}
.status-legend {
display: flex;
gap: 16px;
margin-top: 8px;
font-size: 12px;
color: #999;
}
.legend-item {
display: flex;
align-items: center;
gap: 4px;
}
.legend-block {
width: 12px;
height: 12px;
border-radius: 2px;
display: inline-block;
}
.legend-block.online {
background: #4caf50;
}
.legend-block.offline {
background: #555;
}
.no-data {
color: #666;
text-align: center;
padding: 20px;
font-size: 13px;
}
</style>

18
frontend/env.d.ts vendored

@ -0,0 +1,18 @@
/// <reference types="vite/client" />
declare module '*.vue' {
import type { DefineComponent } from 'vue'
const component: DefineComponent<{}, {}, any>
export default component
}
declare module 'vite-plugin-mock' {
import type { Plugin } from 'vite'
interface ViteMockOptions {
mockDir?: string
localEnabled?: boolean
prodEnabled?: boolean
injectCode?: string
}
export function viteMockServe(options: ViteMockOptions): Plugin
}

@ -0,0 +1,9 @@
const fs = require("fs");
const path = require("path");
const target = path.join(__dirname, "src/views/screen/ScreenPage.vue");
const parts = [];
parts.push(fs.readFileSync(path.join(__dirname, "_screen_part1.vue"), "utf8"));
parts.push(fs.readFileSync(path.join(__dirname, "_screen_part2.vue"), "utf8"));
parts.push(fs.readFileSync(path.join(__dirname, "_screen_part3.vue"), "utf8"));
fs.writeFileSync(target, parts.join(""), "utf8");
console.log("ScreenPage.vue generated successfully");

@ -0,0 +1,104 @@
// 批量生成所有页面组件 - 第1部分
const fs = require('fs');
const path = require('path');
const b = 'D:/opencode/haoliang/frontend/src/views';
function w(d, f, c) {
const p = path.join(b, d, f);
fs.writeFileSync(p, c, 'utf8');
console.log('OK: ' + d + '/' + f);
}
// ==================== 品牌列表 ====================
w('brand', 'BrandListPage.vue', `<template>
<div>
<div style="margin-bottom:16px"><el-button type="primary" @click="goAdd">+ 新增品牌</el-button></div>
<el-table :data="tableData" border stripe v-loading="loading">
<el-table-column prop="brandName" label="品牌名称" width="150"/>
<el-table-column prop="deviceField" label="device字段" width="120"/>
<el-table-column prop="tagsPath" label="tags路径" width="120"/>
<el-table-column label="状态" width="80" align="center"><template #default="{row}"><el-tag :type="row.isEnabled?'success':'danger'" size="small">{{row.isEnabled?'启用':'停用'}}</el-tag></template></el-table-column>
<el-table-column prop="fieldCount" label="字段数" width="80" align="center"/>
<el-table-column label="操作" width="200" align="center"><template #default="{row}">
<el-button link type="primary" @click="goEdit(row.id)">编辑</el-button>
<el-button link type="primary" @click="handleToggle(row)">{{row.isEnabled?'禁用':'启用'}}</el-button>
<el-button link type="danger" @click="handleDelete(row)">删除</el-button>
</template></el-table-column>
</el-table>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus'
import request from '@/utils/request'
import { useMockMode } from '@/composables/useMockMode'
const router = useRouter()
const { isMock } = useMockMode()
const loading = ref(false)
const tableData = ref<any[]>([])
function goAdd() { router.push(isMock.value ? '/mock/brand/create' : '/brand/create') }
function goEdit(id: number) { router.push((isMock.value ? '/mock/brand/' : '/brand/') + id + '/edit') }
async function loadData() { loading.value = true; try { const r: any = await request.get('/admin/brand'); tableData.value = r.data?.items || [] } finally { loading.value = false } }
async function handleToggle(row: any) { await ElMessageBox.confirm('确定' + (row.isEnabled ? '禁用' : '启用') + '', '提示', { type: 'warning' }); await request.post('/admin/brand/toggle', { id: row.id }); ElMessage.success('操作成功'); loadData() }
async function handleDelete(row: any) { await ElMessageBox.confirm('确定删除【' + row.brandName + '】?此操作不可恢复。', '提示', { type: 'warning' }); await request.post('/admin/brand/delete', { id: row.id }); ElMessage.success('已删除'); loadData() }
onMounted(loadData)
</script>`);
// ==================== 品牌编辑 ====================
w('brand', 'BrandEditPage.vue', `<template>
<div>
<div style="display:flex;align-items:center;gap:12px;margin-bottom:20px">
<el-button @click="$router.back()"><el-icon><ArrowLeft /></el-icon> </el-button>
<span style="font-size:16px;font-weight:bold">{{ isEdit ? '编辑品牌' : '新增品牌' }}</span>
</div>
<el-card shadow="hover" style="margin-bottom:20px">
<el-form :model="form" label-width="100px">
<el-form-item label="品牌名称" required><el-input v-model="form.brandName" maxlength="50" /></el-form-item>
<el-form-item label="device字段" required><el-input v-model="form.deviceField" /></el-form-item>
<el-form-item label="tags路径" required><el-input v-model="form.tagsPath" /></el-form-item>
</el-form>
</el-card>
<el-card shadow="hover">
<template #header><div style="display:flex;justify-content:space-between"><span>字段映射列表</span><el-button size="small" @click="addMapping">+ </el-button></div></template>
<el-table :data="form.mappings" border stripe size="small">
<el-table-column label="标准字段" width="160"><template #default="{row}"><el-select v-model="row.standardField"><el-option v-for="f in standardFields" :key="f" :label="f" :value="f" /></el-select></template></el-table-column>
<el-table-column label="字段名" width="120"><template #default="{row}"><el-input v-model="row.fieldName" /></template></el-table-column>
<el-table-column label="匹配方式" width="100"><template #default="{row}"><el-select v-model="row.matchBy"><el-option label="id" value="id" /><el-option label="desc" value="desc" /></el-select></template></el-table-column>
<el-table-column label="数据类型" width="100"><template #default="{row}"><el-select v-model="row.dataType"><el-option label="string" value="string" /><el-option label="number" value="number" /></el-select></template></el-table-column>
<el-table-column label="必填" width="60" align="center"><template #default="{row}"><el-checkbox v-model="row.isRequired" :true-value="1" :false-value="0" /></template></el-table-column>
<el-table-column label="操作" width="80" align="center"><template #default="{ $index }"><el-button link type="danger" @click="form.mappings.splice($index, 1)">删除</el-button></template></el-table-column>
</el-table>
</el-card>
<div style="margin-top:20px;text-align:right">
<el-button @click="$router.back()">取消</el-button>
<el-button type="primary" :loading="submitting" @click="handleSave">保存</el-button>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus'
import request from '@/utils/request'
const route = useRoute()
const router = useRouter()
const isEdit = !!route.params.id
const submitting = ref(false)
const standardFields = ['program_name','part_count','device_status','run_status','operate_mode','spindle_speed_set','feed_speed_set','spindle_speed_actual','feed_speed_actual','spindle_load','spindle_override','power_on_time','run_time','cutting_time','cycle_time','machining_status']
const form = reactive({ brandName: '', deviceField: 'device', tagsPath: 'tags', mappings: [] as any[] })
function addMapping() { form.mappings.push({ standardField: '', fieldName: '', matchBy: 'id', dataType: 'string', isRequired: 0 }) }
async function loadData() {
if (!isEdit) return
const r: any = 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 || [] }
}
async function handleSave() {
await ElMessageBox.confirm('品牌模板修改不影响历史数据,确定保存?', '提示', { type: 'warning' })
submitting.value = true
try { await request.post(isEdit ? '/admin/brand/update' : '/admin/brand', { ...form, id: isEdit ? route.params.id : undefined }); ElMessage.success('保存成功'); router.back() } finally { submitting.value = false }
}
onMounted(loadData)
</script>`);
console.log('Part 1 done: brand pages');

@ -0,0 +1,188 @@
const fs = require('fs');
const path = require('path');
const b = 'D:/opencode/haoliang/frontend/src/views';
function w(d, f, c) { fs.writeFileSync(path.join(b, d, f), c, 'utf8'); console.log('OK: ' + d + '/' + f); }
// MachineDetailPage
w('machine', 'MachineDetailPage.vue', `<template>
<div>
<div style="display:flex;align-items:center;gap:12px;margin-bottom:20px">
<el-button @click="$router.back()"><el-icon><ArrowLeft /></el-icon> </el-button>
<span style="font-size:16px;font-weight:bold">机床详情{{detail.name}}</span>
</div>
<el-row :gutter="20">
<el-col :span="12"><el-card shadow="hover"><template #header><span>基本信息</span></template>
<el-descriptions :column="2" border size="small">
<el-descriptions-item label="机床名称">{{detail.name}}</el-descriptions-item>
<el-descriptions-item label="device_code">{{detail.deviceCode}}</el-descriptions-item>
<el-descriptions-item label="车间">{{detail.workshopName}}</el-descriptions-item>
<el-descriptions-item label="品牌">{{detail.brandName}}</el-descriptions-item>
<el-descriptions-item label="IP地址">{{detail.ipAddress}}</el-descriptions-item>
<el-descriptions-item label="绑定工人">{{detail.workerName||'-'}}</el-descriptions-item>
<el-descriptions-item label="在线"><el-tag :type="detail.isOnline?'success':'danger'" size="small">{{detail.isOnline?'在线':'离线'}}</el-tag></el-descriptions-item>
<el-descriptions-item label="启用"><el-tag :type="detail.isEnabled?'success':'danger'" size="small">{{detail.isEnabled?'启用':'停用'}}</el-tag></el-descriptions-item>
</el-descriptions>
</el-card></el-col>
<el-col :span="12"><el-card shadow="hover"><template #header><span>实时状态</span></template>
<el-descriptions :column="2" border size="small">
<el-descriptions-item label="NC程序名">{{status.programName||'-'}}</el-descriptions-item>
<el-descriptions-item label="零件数">{{status.partCount??'-'}}</el-descriptions-item>
<el-descriptions-item label="运行状态">{{status.runStatus||'-'}}</el-descriptions-item>
<el-descriptions-item label="操作模式">{{status.operateMode||'-'}}</el-descriptions-item>
<el-descriptions-item label="主轴设定">{{status.spindleSpeedSet??'-'}}</el-descriptions-item>
<el-descriptions-item label="进给设定">{{status.feedSpeedSet??'-'}}</el-descriptions-item>
<el-descriptions-item label="主轴实际">{{status.spindleSpeedActual??'-'}}</el-descriptions-item>
<el-descriptions-item label="主轴负载">{{status.spindleLoad??'-'}}%</el-descriptions-item>
</el-descriptions>
</el-card></el-col>
</el-row>
<el-row :gutter="20" style="margin-top:20px">
<el-col :span="12"><el-card shadow="hover"><template #header><span>今日产量</span></template>
<el-table :data="todayProd" border stripe size="small">
<el-table-column prop="programName" label="程序名"/><el-table-column prop="quantity" label="产量" align="center"/><el-table-column prop="runTime" label="运行时间" align="center"/><el-table-column prop="cuttingTime" label="切削时间" align="center"/>
</el-table>
</el-card></el-col>
<el-col :span="12"><el-card shadow="hover"><template #header><span>7天产量趋势</span></template>
<div ref="chartRef" style="height:250px"></div>
</el-card></el-col>
</el-row>
<el-card shadow="hover" style="margin-top:20px"><template #header><span>最近采集记录</span></template>
<el-table :data="records" border stripe size="small">
<el-table-column prop="collectTime" label="采集时间"/><el-table-column prop="programName" label="程序名"/><el-table-column prop="partCount" label="零件数" align="center"/><el-table-column prop="runStatus" label="运行状态"/>
</el-table>
</el-card>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, nextTick, onBeforeUnmount } from 'vue'
import { useRoute } from 'vue-router'
import request from '@/utils/request'
import * as echarts from 'echarts'
const route = useRoute()
const detail = ref<any>({}); const status = ref<any>({})
const todayProd = ref<any[]>([]); const records = ref<any[]>([])
const chartRef = ref<HTMLElement>()
let chart: echarts.ECharts | null = null
async function loadData() {
const id = route.params.id
const [d, s, t, r]: any[] = 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 } }),
])
detail.value = d.data || {}; status.value = s.data || {}
todayProd.value = t.data?.items || []; records.value = r.data?.items || []
const trend: any = await request.get('/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: any) => i.date.slice(5)) }, yAxis: { type: 'value' }, series: [{ type: 'line', data: items.map((i: any) => i.quantity), smooth: true, areaStyle: { opacity: 0.1 } }], tooltip: { trigger: 'axis' }, grid: { left: 40, right: 20, top: 10, bottom: 30 } })
}
}
onMounted(loadData)
onBeforeUnmount(() => { chart?.dispose() })
</script>`);
// CollectAddressListPage
w('collect-address', 'CollectAddressListPage.vue', `<template>
<div>
<div style="margin-bottom:16px"><el-button type="primary" @click="handleAdd">+ 新增地址</el-button></div>
<el-form :inline="true" style="margin-bottom:16px">
<el-form-item label="品牌"><el-select v-model="query.brandId" clearable><el-option v-for="b in brandList" :key="b.id" :label="b.brandName" :value="b.id"/></el-select></el-form-item>
<el-form-item><el-button type="primary" @click="loadData">查询</el-button><el-button @click="query={};loadData()"></el-button></el-form-item>
</el-form>
<el-table :data="tableData" border stripe v-loading="loading">
<el-table-column prop="name" label="名称" width="150"><template #default="{row}"><el-link type="primary" @click="goDetail(row.id)">{{row.name}}</el-link></template></el-table-column>
<el-table-column prop="url" label="URL" show-overflow-tooltip/>
<el-table-column prop="brandName" label="品牌" width="100" align="center"/>
<el-table-column prop="collectInterval" label="采集间隔" width="100" align="center"/>
<el-table-column label="状态" width="80" align="center"><template #default="{row}"><el-tag :type="row.isEnabled?'success':'danger'" size="small">{{row.isEnabled?'启用':'停用'}}</el-tag></template></el-table-column>
<el-table-column prop="lastCollectTime" label="最后采集" width="170"/>
<el-table-column prop="machineCount" label="机床数" width="80" align="center"/>
<el-table-column label="操作" width="160" align="center"><template #default="{row}">
<el-button link type="primary" @click="handleEdit(row)">编辑</el-button>
<el-button link type="danger" @click="handleDelete(row)">删除</el-button>
</template></el-table-column>
</el-table>
<el-dialog v-model="dialogVisible" :title="editingId?'编辑地址':'新增地址'" width="600px" destroy-on-close>
<el-form :model="form" label-width="100px">
<el-form-item label="名称" required><el-input v-model="form.name"/></el-form-item>
<el-form-item label="URL" required><el-input v-model="form.url"/></el-form-item>
<el-form-item label="品牌" required><el-select v-model="form.brandId"><el-option v-for="b in brandList" :key="b.id" :label="b.brandName" :value="b.id"/></el-select></el-form-item>
<el-form-item label="采集间隔" required><el-input-number v-model="form.collectInterval" :min="5"/></el-form-item>
</el-form>
<template #footer><el-button @click="dialogVisible=false">取消</el-button><el-button type="primary" :loading="submitting" @click="handleSubmit"></el-button></template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import {ref,reactive,onMounted} from 'vue'
import {useRouter} from 'vue-router'
import {ElMessage,ElMessageBox} from 'element-plus'
import request from '@/utils/request'
import {useMockMode} from '@/composables/useMockMode'
const router=useRouter();const{isMock}=useMockMode()
const loading=ref(false);const tableData=ref<any[]>([]);const brandList=ref<any[]>([])
const dialogVisible=ref(false);const submitting=ref(false);const editingId=ref<number|null>(null)
const query=reactive({brandId:undefined as any})
const form=reactive({name:'',url:'',brandId:undefined as any,collectInterval:30})
function goDetail(id:number){router.push((isMock.value?'/mock/collect-address/':'/collect-address/')+id)}
async function loadData(){loading.value=true;try{const r:any=await request.get('/admin/collect-address');tableData.value=r.data?.items||[]}finally{loading.value=false}}
function handleAdd(){editingId.value=null;Object.assign(form,{name:'',url:'',brandId:undefined,collectInterval:30});dialogVisible.value=true}
function handleEdit(row:any){editingId.value=row.id;Object.assign(form,row);dialogVisible.value=true}
async function handleSubmit(){submitting.value=true;try{await request.post(editingId.value?'/admin/collect-address/update':'/admin/collect-address',{...form,id:editingId.value});ElMessage.success('保存成功');dialogVisible.value=false;loadData()}finally{submitting.value=false}}
async function handleDelete(row:any){await ElMessageBox.confirm('确定删除【'+row.name+'】?此操作不可恢复。','提示',{type:'warning'});await request.post('/admin/collect-address/delete',{id:row.id});ElMessage.success('已删除');loadData()}
async function loadDrops(){const r:any=await request.get('/admin/brand/list');brandList.value=r.data?.items||[]}
onMounted(()=>{loadData();loadDrops()})
</script>`);
// CollectAddressDetailPage
w('collect-address', 'CollectAddressDetailPage.vue', `<template>
<div>
<div style="display:flex;align-items:center;gap:12px;margin-bottom:20px">
<el-button @click="$router.back()"><el-icon><ArrowLeft /></el-icon> </el-button>
<span style="font-size:16px;font-weight:bold">采集地址详情{{detail.name}}</span>
</div>
<el-card shadow="hover" style="margin-bottom:20px"><template #header><span>基本信息</span></template>
<el-descriptions :column="2" border size="small">
<el-descriptions-item label="名称">{{detail.name}}</el-descriptions-item>
<el-descriptions-item label="URL">{{detail.url}}</el-descriptions-item>
<el-descriptions-item label="品牌">{{detail.brandName}}</el-descriptions-item>
<el-descriptions-item label="采集间隔">{{detail.collectInterval}}</el-descriptions-item>
<el-descriptions-item label="状态"><el-tag :type="detail.isEnabled?'success':'danger'" size="small">{{detail.isEnabled?'启用':'停用'}}</el-tag></el-descriptions-item>
<el-descriptions-item label="最后采集">{{detail.lastCollectTime||'-'}}</el-descriptions-item>
</el-descriptions>
</el-card>
<el-card shadow="hover" style="margin-bottom:20px"><template #header><span>关联机床</span></template>
<el-table :data="machines" border stripe size="small">
<el-table-column prop="machineName" label="机床名称"/><el-table-column prop="deviceCode" label="device_code"/><el-table-column prop="workshopName" label="车间"/>
<el-table-column label="在线" align="center"><template #default="{row}"><el-tag :type="row.isOnline?'success':'danger'" size="small">{{row.isOnline?'在线':'离线'}}</el-tag></template></el-table-column>
<el-table-column prop="programName" label="当前程序"/>
</el-table>
</el-card>
<el-card shadow="hover"><template #header><span>最近采集记录</span></template>
<el-table :data="records" border stripe size="small">
<el-table-column prop="requestTime" label="请求时间"/><el-table-column prop="duration" label="耗时(ms)" align="center"/>
<el-table-column label="状态" align="center"><template #default="{row}"><el-tag :type="row.isSuccess?'success':'danger'" size="small">{{row.isSuccess?'成功':'失败'}}</el-tag></template></el-table-column>
<el-table-column prop="machineCount" label="机床数" align="center"/>
</el-table>
</el-card>
</div>
</template>
<script setup lang="ts">
import {ref,onMounted} from 'vue'
import {useRoute} from 'vue-router'
import request from '@/utils/request'
const route=useRoute()
const detail=ref<any>({});const machines=ref<any[]>([]);const records=ref<any[]>([])
async function loadData(){
const id=route.params.id
const[d,m,r]:any[]=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}})])
detail.value=d.data||{};machines.value=m.data?.items||[];records.value=r.data?.items||[]
}
onMounted(loadData)
</script>`);
console.log('gen2 done: machine detail, collect-address list+detail');

@ -0,0 +1,158 @@
const fs = require('fs');
const path = require('path');
const b = 'D:/opencode/haoliang/frontend/src/views';
function w(d, f, c) { fs.writeFileSync(path.join(b, d, f), c, 'utf8'); console.log('OK: ' + d + '/' + f); }
// WorkerListPage
w('worker', 'WorkerListPage.vue', `<template>
<div>
<div style="margin-bottom:16px"><el-button type="primary" @click="handleAdd">+ 新增工人</el-button></div>
<el-form :inline="true" style="margin-bottom:16px">
<el-form-item label="状态"><el-select v-model="query.isEnabled" clearable><el-option label="启用" :value="1"/><el-option label="停用" :value="0"/></el-select></el-form-item>
<el-form-item><el-input v-model="query.keyword" placeholder="工号/姓名" clearable/></el-form-item>
<el-form-item><el-button type="primary" @click="loadData">查询</el-button><el-button @click="query={};loadData()"></el-button></el-form-item>
</el-form>
<el-table :data="tableData" border stripe v-loading="loading">
<el-table-column type="selection" width="40"/>
<el-table-column prop="code" label="工号" width="120"/>
<el-table-column prop="name" label="姓名" width="120"><template #default="{row}"><el-link type="primary" @click="goDetail(row.id)">{{row.name}}</el-link></template></el-table-column>
<el-table-column label="状态" width="80" align="center"><template #default="{row}"><el-tag :type="row.isEnabled?'success':'danger'" size="small">{{row.isEnabled?'启用':'停用'}}</el-tag></template></el-table-column>
<el-table-column prop="machineCount" label="绑定机床数" width="100" align="center"/>
<el-table-column prop="machineNames" label="绑定机床" show-overflow-tooltip/>
<el-table-column label="操作" width="120" align="center"><template #default="{row}">
<el-button link type="primary" @click="handleEdit(row)">编辑</el-button>
<el-button link type="danger" @click="handleDelete(row)" :disabled="row.machineCount>0">删除</el-button>
</template></el-table-column>
</el-table>
<div v-if="selectedRows.length" style="margin-top:12px;padding:8px 12px;background:#ecf5ff;border-radius:4px">
已选{{selectedRows.length}} <el-button size="small" @click="batchStatus(1)">批量启用</el-button><el-button size="small" @click="batchStatus(0)"></el-button>
</div>
<el-dialog v-model="dialogVisible" :title="editingId?'编辑工人':'新增工人'" width="500px" destroy-on-close>
<el-form :model="form" label-width="100px">
<el-form-item label="工号" required><el-input v-model="form.code" maxlength="50"/></el-form-item>
<el-form-item label="姓名" required><el-input v-model="form.name" maxlength="50"/></el-form-item>
<el-form-item label="绑定机床"><el-select v-model="form.machineIds" multiple filterable><el-option v-for="m in availableMachines" :key="m.id" :label="m.name" :value="m.id"/></el-select></el-form-item>
</el-form>
<template #footer><el-button @click="dialogVisible=false">取消</el-button><el-button type="primary" :loading="submitting" @click="handleSubmit"></el-button></template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import {ref,reactive,onMounted} from 'vue'
import {useRouter} from 'vue-router'
import {ElMessage,ElMessageBox} from 'element-plus'
import request from '@/utils/request'
import {useMockMode} from '@/composables/useMockMode'
const router=useRouter();const{isMock}=useMockMode()
const loading=ref(false);const tableData=ref<any[]>([]);const selectedRows=ref<any[]>([])
const dialogVisible=ref(false);const submitting=ref(false);const editingId=ref<number|null>(null)
const availableMachines=ref<any[]>([])
const query=reactive({isEnabled:undefined as any,keyword:''})
const form=reactive({code:'',name:'',machineIds:[] as number[]})
function goDetail(id:number){router.push((isMock.value?'/mock/worker/':'/worker/')+id)}
async function loadData(){loading.value=true;try{const r:any=await request.get('/admin/worker',{params:query});tableData.value=r.data?.items||[]}finally{loading.value=false}}
function handleAdd(){editingId.value=null;Object.assign(form,{code:'',name:'',machineIds:[]});dialogVisible.value=true}
function handleEdit(row:any){editingId.value=row.id;Object.assign(form,{code:row.code,name:row.name,machineIds:[]});dialogVisible.value=true}
async function handleSubmit(){submitting.value=true;try{await request.post(editingId.value?'/admin/worker/update':'/admin/worker',{...form,id:editingId.value});ElMessage.success('保存成功');dialogVisible.value=false;loadData()}finally{submitting.value=false}}
async function handleDelete(row:any){await ElMessageBox.confirm('确定删除【'+row.name+'】?此操作不可恢复。','提示',{type:'warning'});await request.post('/admin/worker/delete',{id:row.id});ElMessage.success('已删除');loadData()}
async function batchStatus(isEnabled:number){await ElMessageBox.confirm('确定对选中的'+selectedRows.value.length+'项操作?','提示',{type:'warning'});await request.post('/admin/worker/batch-status',{ids:selectedRows.value.map((r:any)=>r.id),isEnabled});ElMessage.success('操作成功');loadData()}
async function loadDrops(){const r:any=await request.get('/admin/worker/available-machines');availableMachines.value=r.data?.items||[]}
onMounted(()=>{loadData();loadDrops()})
</script>`);
// WorkerDetailPage
w('worker', 'WorkerDetailPage.vue', `<template>
<div>
<div style="display:flex;align-items:center;gap:12px;margin-bottom:20px">
<el-button @click="$router.back()"><el-icon><ArrowLeft /></el-icon> </el-button>
<span style="font-size:16px;font-weight:bold">工人详情{{detail.name}}</span>
</div>
<el-row :gutter="20">
<el-col :span="12"><el-card shadow="hover"><template #header><span>基本信息</span></template>
<el-descriptions :column="2" border size="small">
<el-descriptions-item label="工号">{{detail.code}}</el-descriptions-item>
<el-descriptions-item label="姓名">{{detail.name}}</el-descriptions-item>
<el-descriptions-item label="状态"><el-tag :type="detail.isEnabled?'success':'danger'" size="small">{{detail.isEnabled?'启用':'停用'}}</el-tag></el-descriptions-item>
<el-descriptions-item label="绑定机床数">{{detail.machineCount||0}}</el-descriptions-item>
</el-descriptions>
</el-card></el-col>
<el-col :span="12"><el-card shadow="hover"><template #header><span>7天产量趋势</span></template>
<div ref="chartRef" style="height:200px"></div>
</el-card></el-col>
</el-row>
<el-card shadow="hover" style="margin-top:20px"><template #header><span>绑定机床</span></template>
<el-table :data="machines" border stripe size="small">
<el-table-column prop="machineName" label="机床名称"/><el-table-column prop="deviceCode" label="device_code"/><el-table-column prop="workshopName" label="车间"/><el-table-column prop="brandName" label="品牌"/>
<el-table-column label="在线" align="center"><template #default="{row}"><el-tag :type="row.isOnline?'success':'danger'" size="small">{{row.isOnline?'在线':'离线'}}</el-tag></template></el-table-column>
<el-table-column prop="programName" label="当前程序"/>
</el-table>
</el-card>
<el-card shadow="hover" style="margin-top:20px"><template #header><span>今日产量</span></template>
<el-table :data="todayProd" border stripe size="small">
<el-table-column prop="machineName" label="机床名称"/><el-table-column prop="programName" label="程序名"/><el-table-column prop="quantity" label="产量" align="center"/><el-table-column prop="runTime" label="运行时间" align="center"/><el-table-column prop="cuttingTime" label="切削时间" align="center"/>
</el-table>
</el-card>
</div>
</template>
<script setup lang="ts">
import {ref,onMounted,nextTick,onBeforeUnmount} from 'vue'
import {useRoute} from 'vue-router'
import request from '@/utils/request'
import * as echarts from 'echarts'
const route=useRoute()
const detail=ref<any>({});const machines=ref<any[]>([]);const todayProd=ref<any[]>([])
const chartRef=ref<HTMLElement>();let chart:echarts.ECharts|null=null
async function loadData(){
const id=route.params.id
const[d,m,t]:any[]=await Promise.all([request.get('/admin/worker/detail',{params:{id}}),request.get('/admin/worker/machines',{params:{id}}),request.get('/admin/worker/production/today',{params:{id}})])
detail.value=d.data||{};machines.value=m.data?.items||[];todayProd.value=t.data?.items||[]
const trend:any=await request.get('/admin/worker/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:any)=>i.date.slice(5))},yAxis:{type:'value'},series:[{type:'line',data:items.map((i:any)=>i.quantity),smooth:true,areaStyle:{opacity:0.1}}],tooltip:{trigger:'axis'},grid:{left:40,right:20,top:10,bottom:30}})}
}
onMounted(loadData);onBeforeUnmount(()=>{chart?.dispose()})
</script>`);
// ProductionPage
w('production', 'ProductionPage.vue', `<template>
<div>
<el-row :gutter="20" style="margin-bottom:20px">
<el-col :span="6"><el-card shadow="hover"><div style="text-align:center;padding:10px"><div style="color:#909399;font-size:14px">总产量</div><div style="font-size:28px;font-weight:bold">{{summary.totalQuantity?.toLocaleString()}}</div></div></el-card></el-col>
<el-col :span="6"><el-card shadow="hover"><div style="text-align:center;padding:10px"><div style="color:#909399;font-size:14px">运行机床</div><div style="font-size:28px;font-weight:bold">{{summary.activeMachineCount}}</div></div></el-card></el-col>
<el-col :span="6"><el-card shadow="hover"><div style="text-align:center;padding:10px"><div style="color:#909399;font-size:14px">切削总时</div><div style="font-size:28px;font-weight:bold">{{summary.totalCuttingTime}}</div></div></el-card></el-col>
<el-col :span="6"><el-card shadow="hover"><div style="text-align:center;padding:10px"><div style="color:#909399;font-size:14px">平均产量</div><div style="font-size:28px;font-weight:bold">{{summary.avgQuantityPerMachine}}</div></div></el-card></el-col>
</el-row>
<el-table :data="tableData" border stripe v-loading="loading">
<el-table-column prop="date" label="日期" width="110"/><el-table-column prop="machineName" label="机床" width="120"/><el-table-column prop="programName" label="程序名" width="140" show-overflow-tooltip/>
<el-table-column label="产量" width="80" align="center"><template #default="{row}">{{row.dataStatus==='data_missing'?'-':row.quantity}}</template></el-table-column>
<el-table-column prop="runTime" label="运行时间" width="100" align="center"/><el-table-column prop="cuttingTime" label="切削时间" width="100" align="center"/>
<el-table-column label="日状态" width="80" align="center"><template #default="{row}"><el-tag :type="row.dataStatus==='normal'?'info':row.dataStatus==='offline'?'danger':'warning'" size="small">{{row.dataStatus==='normal'?'正常':row.dataStatus==='offline'?'离线':'缺失'}}</el-tag></template></el-table-column>
<el-table-column label="修正" width="80" align="center"><template #default="{row}">{{row.isAdjusted?'✓':'-'}}</template></el-table-column>
<el-table-column label="操作" width="100" align="center"><template #default="{row}"><el-button link type="primary" @click="handleAdjust(row)">修正</el-button></template></el-table-column>
</el-table>
<el-pagination v-model:current-page="page.page" v-model:page-size="page.pageSize" :page-sizes="[20,50,100]" :total="page.total" background layout="total,sizes,prev,pager,next,jumper"/>
<el-dialog v-model="adjustVisible" title="修正产量" width="500px" destroy-on-close>
<el-form :model="adjustForm" label-width="100px">
<el-form-item label="当前产量"><el-input :model-value="adjustForm.currentQty" disabled/></el-form-item>
<el-form-item label="修正后产量" required><el-input-number v-model="adjustForm.newQty" :min="0"/></el-form-item>
<el-form-item label="修正原因" required><el-input v-model="adjustForm.reason" type="textarea" maxlength="500"/></el-form-item>
</el-form>
<template #footer><el-button @click="adjustVisible=false">取消</el-button><el-button type="primary" :loading="submitting" @click="doAdjust"></el-button></template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import {ref,reactive,onMounted} from 'vue'
import {ElMessage,ElMessageBox} from 'element-plus'
import request from '@/utils/request'
const loading=ref(false);const tableData=ref<any[]>([]);const summary=ref<any>({})
const page=reactive({page:1,pageSize:20,total:0})
const adjustVisible=ref(false);const submitting=ref(false)
const adjustForm=reactive({currentQty:0,newQty:0,reason:'',id:0})
async function loadData(){loading.value=true;try{const[s,d]:any[]=await Promise.all([request.get('/admin/production/daily-summary'),request.get('/admin/production/daily',{params:page})]);summary.value=s.data||{};tableData.value=d.data?.items||[];page.total=d.data?.total||0}finally{loading.value=false}}
function handleAdjust(row:any){adjustForm.currentQty=row.quantity;adjustForm.newQty=row.quantity||0;adjustForm.reason='';adjustForm.id=row.id;adjustVisible.value=true}
async function doAdjust(){await ElMessageBox.confirm('确定修正产量?修正后将记录审计日志。','提示',{type:'warning'});submitting.value=true;try{await request.post('/admin/production/adjust',adjustForm);ElMessage.success('修正成功');adjustVisible.value=false;loadData()}finally{submitting.value=false}}
onMounted(loadData)
</script>`);
console.log('gen3 done: worker list+detail, production');

@ -0,0 +1,107 @@
const fs = require('fs');
const path = require('path');
const base = 'D:/opencode/haoliang/frontend/src/views';
function w(dir, file, content) {
const p = path.join(base, dir, file);
fs.writeFileSync(p, content, 'utf8');
console.log('Created: ' + p);
}
// ===== MachineDetailPage =====
w('machine', 'MachineDetailPage.vue', `<template>
<div class="machine-detail-page">
<div class="page-header">
<el-button @click="$router.back()"><el-icon><ArrowLeft /></el-icon> </el-button>
<span class="page-title">机床详情{{ detail.name }}</span>
</div>
<el-row :gutter="20">
<el-col :span="12">
<el-card shadow="hover"><template #header><span>基本信息</span></template>
<el-descriptions :column="2" border size="small">
<el-descriptions-item label="机床名称">{{ detail.name }}</el-descriptions-item>
<el-descriptions-item label="device_code">{{ detail.deviceCode }}</el-descriptions-item>
<el-descriptions-item label="车间">{{ detail.workshopName }}</el-descriptions-item>
<el-descriptions-item label="品牌">{{ detail.brandName }}</el-descriptions-item>
<el-descriptions-item label="IP地址">{{ detail.ipAddress }}</el-descriptions-item>
<el-descriptions-item label="绑定工人">{{ detail.workerName || '-' }}</el-descriptions-item>
<el-descriptions-item label="在线状态"><el-tag :type="detail.isOnline ? 'success' : 'danger'" size="small">{{ detail.isOnline ? '在线' : '离线' }}</el-tag></el-descriptions-item>
<el-descriptions-item label="启用状态"><el-tag :type="detail.isEnabled ? 'success' : 'danger'" size="small">{{ detail.isEnabled ? '启用' : '停用' }}</el-tag></el-descriptions-item>
</el-descriptions>
</el-card>
</el-col>
<el-col :span="12">
<el-card shadow="hover"><template #header><span>实时状态</span></template>
<el-descriptions :column="2" border size="small">
<el-descriptions-item label="NC程序名">{{ status.programName || '-' }}</el-descriptions-item>
<el-descriptions-item label="零件数">{{ status.partCount ?? '-' }}</el-descriptions-item>
<el-descriptions-item label="运行状态">{{ status.runStatus || '-' }}</el-descriptions-item>
<el-descriptions-item label="操作模式">{{ status.operateMode || '-' }}</el-descriptions-item>
<el-descriptions-item label="主轴设定速度">{{ status.spindleSpeedSet ?? '-' }}</el-descriptions-item>
<el-descriptions-item label="进给设定速度">{{ status.feedSpeedSet ?? '-' }}</el-descriptions-item>
<el-descriptions-item label="主轴实际速度">{{ status.spindleSpeedActual ?? '-' }}</el-descriptions-item>
<el-descriptions-item label="主轴负载">{{ status.spindleLoad ?? '-' }}%</el-descriptions-item>
</el-descriptions>
</el-card>
</el-col>
</el-row>
<el-row :gutter="20" style="margin-top:20px">
<el-col :span="12">
<el-card shadow="hover"><template #header><span>今日产量</span></template>
<el-table :data="todayProduction" border stripe size="small">
<el-table-column prop="programName" label="程序名" /><el-table-column prop="quantity" label="产量" align="center" /><el-table-column prop="runTime" label="运行时间" align="center" /><el-table-column prop="cuttingTime" label="切削时间" align="center" />
</el-table>
</el-card>
</el-col>
<el-col :span="12">
<el-card shadow="hover"><template #header><span>7天产量趋势</span></template>
<div ref="trendChartRef" style="height:250px"></div>
</el-card>
</el-col>
</el-row>
<el-card shadow="hover" style="margin-top:20px"><template #header><span>最近采集记录</span></template>
<el-table :data="collectRecords" border stripe size="small">
<el-table-column prop="collectTime" label="采集时间" /><el-table-column prop="programName" label="程序名" /><el-table-column prop="partCount" label="零件数" align="center" /><el-table-column prop="runStatus" label="运行状态" />
</el-table>
</el-card>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, nextTick, onBeforeUnmount } from 'vue'
import { useRoute } from 'vue-router'
import request from '@/utils/request'
import * as echarts from 'echarts'
const route = useRoute()
const detail = ref<any>({})
const status = ref<any>({})
const todayProduction = ref<any[]>([])
const collectRecords = ref<any[]>([])
const trendChartRef = ref<HTMLElement>()
let chartInstance: echarts.ECharts | null = null
async function loadData() {
const id = route.params.id
const [d, s, t, r]: any[] = 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 } }),
])
detail.value = d.data || {}; status.value = s.data || {}; todayProduction.value = t.data?.items || []; collectRecords.value = r.data?.items || []
const trend: any = await request.get('/admin/machine/production/trend', { params: { id } })
await nextTick()
if (trendChartRef.value) {
chartInstance = echarts.init(trendChartRef.value)
chartInstance.setOption({ xAxis: { type: 'category', data: (trend.data?.items || []).map((i: any) => i.date.slice(5)) }, yAxis: { type: 'value' }, series: [{ type: 'line', data: (trend.data?.items || []).map((i: any) => i.quantity), smooth: true, areaStyle: { opacity: 0.1 } }], tooltip: { trigger: 'axis' }, grid: { left: 40, right: 20, top: 10, bottom: 30 } })
}
}
onMounted(loadData)
onBeforeUnmount(() => { chartInstance?.dispose() })
</script>
<style scoped lang="scss">
.page-header { display: flex; align-items: center; gap: 12px; margin-bottom: 20px; .page-title { font-size: 16px; font-weight: bold; } }
</style>`);
console.log('All detail pages created');

@ -0,0 +1,41 @@
const fs = require('fs');
const path = require('path');
const d = 'E:/opencode/haoliang/frontend/src/views';
const fixes = [
['(rows: any[])', '(rows: Record<string,unknown>[])'],
['(record: any)', '(record: Record<string,unknown>)'],
['const params: any =', 'const params: Record<string,unknown> ='],
['(r: any) => r.id', '(r: {id:number}) => r.id'],
['function onFileChange(file: any)', 'function onFileChange(file: Record<string,unknown>)'],
['params: { row: any; rowIndex', 'params: { row: Record<string,unknown>; rowIndex'],
['const payload: any =', 'const payload: Record<string,unknown> ='],
['(c: any) => c.configKey.toLowerCase().includes(kw) || c.description.t',
'(c: {configKey:string;description:string}) => c.configKey.toLowerCase().includes(kw) || c.description.t'],
['_rule: any, value: string, callback: any', '_rule: unknown, value: string, callback: (err?:Error)=>void'],
['rule: any, value: any, callback: any', 'rule: unknown, value: string, callback: (err?:Error)=>void'],
];
let total = 0;
fs.readdirSync(d).forEach(sub => {
const subDir = path.join(d, sub);
if (!fs.statSync(subDir).isDirectory()) return;
fs.readdirSync(subDir).forEach(f => {
if (!f.endsWith('.vue')) return;
const fp = path.join(subDir, f);
let c = fs.readFileSync(fp, 'utf8');
let n = 0;
fixes.forEach(([from, to]) => {
if (c.includes(from)) {
c = c.split(from).join(to);
n++;
}
});
if (n > 0) {
fs.writeFileSync(fp, c, 'utf8');
console.log(f + ': ' + n);
total += n;
}
});
});
console.log('Total fixed: ' + total);

@ -0,0 +1,66 @@
// Simple, targeted refactor: replace common inline styles with CSS utility classes
// Only handles exact pattern matches and only when the tag doesn't already have a class attribute.
// This is a safe, incremental step. Run this script from the project root: `node frontend/scripts/refactor_inline_styles.js`
const fs = require('fs');
const path = require('path');
function walkVueFiles(dir) {
const res = [];
const items = fs.readdirSync(dir, { withFileTypes: true });
for (const it of items) {
const full = path.join(dir, it.name);
if (it.isDirectory()) {
res.push(...walkVueFiles(full));
} else if (it.isFile() && full.endsWith('.vue')) {
res.push(full);
}
}
return res;
}
const projectRoot = path.resolve(__dirname, '..');
const targetDir = path.resolve(projectRoot, 'src', 'views');
const files = walkVueFiles(targetDir);
const replacements = [
{ find: 'style="margin-bottom:16px"', replace: 'class="mb-16"' },
{ find: "style='margin-bottom:16px'", replace: "class='mb-16'" },
{ find: 'style="margin-top:20px"', replace: 'class="mt-20"' },
{ find: "style='margin-top:20px'", replace: "class='mt-20'" },
{ find: 'style="display:flex;align-items:center;gap:12px"', replace: 'class="flex-gap"' },
{ find: "style='display:flex;align-items:center;gap:12px'", replace: "class='flex-gap'" },
{ find: 'style="font-size:16px;font-weight:bold"', replace: 'class="page-title"' },
{ find: "style='font-size:16px;font-weight:bold'", replace: "class='page-title'" },
{ find: 'style="color: var(--el-color-primary); cursor: pointer; text-decoration: none;"', replace: 'class="link-text"' },
{ find: "style='color: var(--el-color-primary); cursor: pointer; text-decoration: none;'", replace: "class='link-text'" },
];
let totalReplacements = 0;
let totalFilesLaunched = 0;
for (const file of files) {
let content = fs.readFileSync(file, 'utf8');
let modified = false;
const lines = content.split(/\r?\n/);
for (let i = 0; i < lines.length; i++) {
let line = lines[i];
// Quick skip if line already has a class attribute
if (line.includes('style=') && !line.includes('class=')) {
for (const r of replacements) {
if (line.includes(r.find)) {
line = line.replace(r.find, r.replace);
modified = true;
totalReplacements++;
}
}
}
lines[i] = line;
}
if (modified) {
totalFilesLaunched++;
fs.writeFileSync(file, lines.join('\n'), 'utf8');
}
}
console.log(`refactor_inline_styles.js: applied ${totalReplacements} replacements across ${totalFilesLaunched} files.`);

@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net472</TargetFramework>
<PlatformTarget>x64</PlatformTarget>
<RootNamespace>CncModels</RootNamespace>
<AssemblyName>CncModels</AssemblyName>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<!-- 不生成 AssemblyInfo手动维护 -->
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
</PropertyGroup>
<ItemGroup>
<!-- 允许在没有 .NET Framework 目标包的机器上编译 -->
<PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies.net472" Version="1.0.3" PrivateAssets="all" />
<!-- JSON序列化 -->
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
</ItemGroup>
</Project>

@ -0,0 +1,31 @@
using System;
namespace CncModels.Constants
{
/// <summary>
/// 全局错误码
/// 编号规则5位数字前2位为模块后3位为具体错误
/// </summary>
public static class ErrorCode
{
// ======== 通用错误40xxx========
/// <summary>请求参数错误</summary>
public const int BadRequest = 40000;
/// <summary>未认证Token缺失或无效</summary>
public const int Unauthorized = 40101;
/// <summary>资源不存在</summary>
public const int NotFound = 40002;
/// <summary>数据冲突(唯一性校验失败)</summary>
public const int Conflict = 40003;
/// <summary>数据已被引用,不允许操作</summary>
public const int DataReferenced = 40001;
/// <summary>服务器内部错误</summary>
public const int InternalError = 50001;
}
}

@ -0,0 +1,53 @@
using System;
namespace CncModels.Dto
{
/// <summary>
/// 统一API响应包装类
/// 前端约定code=0 表示成功非0表示失败
/// </summary>
/// <typeparam name="T">业务数据类型</typeparam>
public class ApiResponse<T>
{
/// <summary>错误码0=成功</summary>
public int Code { get; set; }
/// <summary>提示信息</summary>
public string Message { get; set; }
/// <summary>业务数据</summary>
public T Data { get; set; }
/// <summary>
/// 构建成功响应
/// </summary>
/// <param name="data">业务数据</param>
/// <param name="message">提示信息,默认 "success"</param>
/// <returns>成功响应</returns>
public static ApiResponse<T> Success(T data, string message = "success")
{
return new ApiResponse<T>
{
Code = 0,
Message = message,
Data = data
};
}
/// <summary>
/// 构建失败响应
/// </summary>
/// <param name="code">错误码非0</param>
/// <param name="message">错误信息</param>
/// <returns>失败响应</returns>
public static ApiResponse<T> Fail(int code, string message)
{
return new ApiResponse<T>
{
Code = code,
Message = message,
Data = default(T)
};
}
}
}

@ -0,0 +1,34 @@
using System;
namespace CncModels.Dto
{
/// <summary>
/// 分页查询参数基类
/// 默认值page=1, pageSize=20
/// 约束pageSize最大100page最小1
/// </summary>
public class PagedQuery
{
private int _page = 1;
private int _pageSize = 20;
/// <summary>页码从1开始小于1自动修正为1</summary>
public int Page
{
get => _page;
set => _page = value < 1 ? 1 : value;
}
/// <summary>每页条数范围1-100超出自动修正</summary>
public int PageSize
{
get => _pageSize;
set => _pageSize = value < 1 ? 20 : (value > 100 ? 100 : value);
}
/// <summary>
/// 计算SQL偏移量OFFSET
/// </summary>
public int Offset => (Page - 1) * PageSize;
}
}

@ -0,0 +1,27 @@
using System;
using System.Collections.Generic;
namespace CncModels.Dto
{
/// <summary>
/// 分页结果
/// </summary>
/// <typeparam name="T">数据项类型</typeparam>
public class PagedResult<T>
{
/// <summary>数据列表</summary>
public List<T> Items { get; set; } = new List<T>();
/// <summary>总记录数</summary>
public int Total { get; set; }
/// <summary>当前页码从1开始</summary>
public int Page { get; set; }
/// <summary>每页条数</summary>
public int PageSize { get; set; }
/// <summary>总页数</summary>
public int TotalPages => PageSize > 0 ? (int)Math.Ceiling((double)Total / PageSize) : 0;
}
}

@ -0,0 +1,19 @@
using System.Reflection;
using System.Runtime.InteropServices;
// 通用信息
[assembly: AssemblyTitle("CncModels")]
[assembly: AssemblyDescription("CNC机床数据采集系统 - 数据模型层")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("CncDataSystem")]
[assembly: AssemblyCopyright("Copyright © 2026")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
// COM 可见性
[assembly: ComVisible(false)]
// 版本号
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]

@ -0,0 +1,39 @@
using System;
using System.Data;
using MySqlConnector;
namespace CncRepository.Base
{
/// <summary>
/// 仓储基类,提供数据库连接管理
/// 每个仓储方法应 using(CreateConnection()) 自动释放连接
/// </summary>
public abstract class BaseRepository
{
private readonly string _connectionString;
/// <summary>
/// 初始化仓储基类
/// </summary>
/// <param name="connectionString">数据库连接字符串</param>
protected BaseRepository(string connectionString)
{
_connectionString = connectionString ?? throw new ArgumentNullException(nameof(connectionString));
}
/// <summary>
/// 创建新的数据库连接
/// 调用方需使用 using 语句确保连接释放
/// </summary>
/// <example>
/// using (var conn = CreateConnection())
/// {
/// return conn.QueryFirstOrDefault&lt;Machine&gt;("SELECT * FROM cnc_machine WHERE id = @Id", new { Id = id });
/// }
/// </example>
protected IDbConnection CreateConnection()
{
return new MySqlConnection(_connectionString);
}
}
}

@ -0,0 +1,24 @@
using System;
using System.Data;
using MySqlConnector;
namespace CncRepository.Base
{
/// <summary>
/// 业务库仓储基类cnc_business
/// 所有操作业务库的仓储继承此类
/// </summary>
public class BusinessRepository : BaseRepository
{
public BusinessRepository(string connectionString) : base(connectionString) { }
}
/// <summary>
/// 日志库仓储基类cnc_log
/// 所有操作日志库的仓储继承此类
/// </summary>
public class LogRepository : BaseRepository
{
public LogRepository(string connectionString) : base(connectionString) { }
}
}

@ -0,0 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net472</TargetFramework>
<PlatformTarget>x64</PlatformTarget>
<RootNamespace>CncRepository</RootNamespace>
<AssemblyName>CncRepository</AssemblyName>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies.net472" Version="1.0.3" PrivateAssets="all" />
<!-- ORM -->
<PackageReference Include="Dapper" Version="2.1.35" />
<!-- MariaDB驱动非Oracle的MySql.Data -->
<PackageReference Include="MySqlConnector" Version="2.3.7" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\CncModels\CncModels.csproj" />
</ItemGroup>
</Project>

@ -0,0 +1,14 @@
using System.Reflection;
using System.Runtime.InteropServices;
[assembly: AssemblyTitle("CncRepository")]
[assembly: AssemblyDescription("CNC机床数据采集系统 - 数据访问层")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("CncDataSystem")]
[assembly: AssemblyCopyright("Copyright © 2026")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
[assembly: ComVisible(false)]
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]

@ -0,0 +1,24 @@
using System;
namespace CncService
{
/// <summary>
/// 业务异常
/// Service层抛出此异常由全局异常过滤器统一捕获并转换为ApiResponse
/// </summary>
public class BusinessException : Exception
{
/// <summary>业务错误码非0</summary>
public int Code { get; }
/// <summary>
/// 创建业务异常
/// </summary>
/// <param name="code">错误码,参见 CncModels.Constants.ErrorCode</param>
/// <param name="message">错误信息,会返回给前端展示</param>
public BusinessException(int code, string message) : base(message)
{
Code = code;
}
}
}

@ -0,0 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net472</TargetFramework>
<PlatformTarget>x64</PlatformTarget>
<RootNamespace>CncService</RootNamespace>
<AssemblyName>CncService</AssemblyName>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies.net472" Version="1.0.3" PrivateAssets="all" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\CncModels\CncModels.csproj" />
<ProjectReference Include="..\CncRepository\CncRepository.csproj" />
</ItemGroup>
</Project>

@ -0,0 +1,14 @@
using System.Reflection;
using System.Runtime.InteropServices;
[assembly: AssemblyTitle("CncService")]
[assembly: AssemblyDescription("CNC机床数据采集系统 - 业务逻辑层")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("CncDataSystem")]
[assembly: AssemblyCopyright("Copyright © 2026")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
[assembly: ComVisible(false)]
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]

@ -0,0 +1,38 @@
using System.Web.Http;
using System.Web.Http.Cors;
namespace CncWebApi.App_Start
{
/// <summary>
/// Web API 路由和过滤器配置
/// 在 Global.asax Application_Start 中调用
/// </summary>
public static class WebApiConfig
{
/// <summary>
/// 注册Web API配置
/// </summary>
/// <param name="config">HTTP配置对象</param>
public static void Register(HttpConfiguration config)
{
// 跨域配置(局域网场景,允许所有来源)
config.EnableCors(new EnableCorsAttribute("*", "*", "*"));
// 路由注册
// 默认路由模板api/{controller}/{id}
// 各Controller使用 [RoutePrefix] + [Route] 属性路由覆盖此默认
config.MapHttpAttributeRoutes();
// 全局异常过滤器
config.Filters.Add(new Filters.GlobalExceptionFilter());
// 统一JSON序列化设置
config.Formatters.JsonFormatter.SerializerSettings.NullValueHandling =
Newtonsoft.Json.NullValueHandling.Ignore;
config.Formatters.JsonFormatter.SerializerSettings.DateFormatString =
"yyyy-MM-dd HH:mm:ss";
// 移除XML格式器统一使用JSON
config.Formatters.Remove(config.Formatters.XmlFormatter);
}
}
}

@ -0,0 +1,44 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net472</TargetFramework>
<PlatformTarget>x64</PlatformTarget>
<RootNamespace>CncWebApi</RootNamespace>
<AssemblyName>CncWebApi</AssemblyName>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<!-- Web API 项目输出到 bin 目录 -->
<OutputType>Library</OutputType>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies.net472" Version="1.0.3" PrivateAssets="all" />
<!-- ASP.NET Web API 2 核心包 -->
<PackageReference Include="Microsoft.AspNet.WebApi.Core" Version="5.3.0" />
<PackageReference Include="Microsoft.AspNet.WebApi.WebHost" Version="5.3.0" />
<PackageReference Include="Microsoft.AspNet.WebApi.Owin" Version="5.3.0" />
<PackageReference Include="Microsoft.AspNet.WebApi.Cors" Version="5.3.0" />
<PackageReference Include="Microsoft.Owin.Host.SystemWeb" Version="4.2.2" />
<PackageReference Include="Owin" Version="1.0" />
<!-- JWT认证 -->
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="6.35.0" />
<PackageReference Include="Microsoft.IdentityModel.Tokens" Version="6.35.0" />
<!-- JSON序列化锁定12.0.3VS2017兼容 -->
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<!-- 日志 -->
<PackageReference Include="log4net" Version="2.0.15" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\CncModels\CncModels.csproj" />
<ProjectReference Include="..\CncRepository\CncRepository.csproj" />
<ProjectReference Include="..\CncService\CncService.csproj" />
</ItemGroup>
<!-- 将 Web.config、Global.asax 等复制到输出目录 -->
<ItemGroup>
<Content Include="Web.config" CopyToOutputDirectory="PreserveNewest" />
<Content Include="Global.asax" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
</Project>

@ -0,0 +1,28 @@
using System;
using System.Web.Http;
namespace CncWebApi.Controllers
{
/// <summary>
/// 健康检查控制器
/// 用于验证API服务是否正常运行
/// </summary>
[RoutePrefix("api")]
public class HealthController : ApiController
{
/// <summary>
/// 健康检查端点
/// GET /api/health
/// </summary>
[HttpGet]
[Route("health")]
public IHttpActionResult Check()
{
return Ok(new
{
status = "healthy",
timestamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")
});
}
}
}

@ -0,0 +1,42 @@
using System.Net;
using System.Net.Http;
using System.Web.Http;
using System.Web.Http.Filters;
using CncModels.Dto;
using CncService;
namespace CncWebApi.Filters
{
/// <summary>
/// 全局异常过滤器
/// 统一捕获所有未处理异常转换为标准ApiResponse格式
/// Controller层不需要 try-catch异常会自动被此过滤器捕获
/// </summary>
public class GlobalExceptionFilter : ExceptionFilterAttribute
{
/// <summary>
/// 异常发生时自动调用
/// </summary>
/// <param name="context">异常上下文</param>
public override void OnException(HttpActionExecutedContext context)
{
if (context.Exception is BusinessException bex)
{
// 业务异常:返回对应的错误码和消息
var response = ApiResponse<object>.Fail(bex.Code, bex.Message);
context.Response = context.Request.CreateResponse(
HttpStatusCode.OK, response);
}
else
{
// 未预期异常:记录日志,返回通用错误
// TODO: 接入 log4net 记录完整异常堆栈
var response = ApiResponse<object>.Fail(
CncModels.Constants.ErrorCode.InternalError,
"服务器内部错误");
context.Response = context.Request.CreateResponse(
HttpStatusCode.InternalServerError, response);
}
}
}
}

@ -0,0 +1 @@
<%@ Application Codebehind="Global.asax.cs" Inherits="CncWebApi.WebApiApplication" Language="C#" %>

@ -0,0 +1,22 @@
using System;
using System.Web;
using System.Web.Http;
using CncWebApi.App_Start;
namespace CncWebApi
{
/// <summary>
/// Web API 应用程序入口
/// IIS 启动时自动调用 Application_Start
/// </summary>
public class WebApiApplication : HttpApplication
{
/// <summary>
/// 应用启动时执行,注册路由和全局配置
/// </summary>
protected void Application_Start()
{
GlobalConfiguration.Configure(WebApiConfig.Register);
}
}
}

@ -0,0 +1,14 @@
using System.Reflection;
using System.Runtime.InteropServices;
[assembly: AssemblyTitle("CncWebApi")]
[assembly: AssemblyDescription("CNC机床数据采集系统 - Web API 主项目")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("CncDataSystem")]
[assembly: AssemblyCopyright("Copyright © 2026")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
[assembly: ComVisible(false)]
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]

@ -0,0 +1,45 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Web API 应用配置文件
部署到 IIS 时,需要将此文件复制到站点根目录
-->
<configuration>
<connectionStrings>
<!-- 业务库连接串 -->
<add name="BusinessConnection"
connectionString="Server=localhost;Database=cnc_business;Uid=root;Pwd=root;Charset=utf8mb4;SslMode=None;" />
<!-- 日志库连接串 -->
<add name="LogConnection"
connectionString="Server=localhost;Database=cnc_log;Uid=root;Pwd=root;Charset=utf8mb4;SslMode=None;" />
</connectionStrings>
<appSettings>
<!-- JWT认证密钥生产环境请更换 -->
<add key="JwtSecret" value="CncDataSystem_2026_SecretKey_For_Jwt_Token_Generation_Min32Chars" />
<!-- Token过期时间小时 -->
<add key="TokenExpirationHours" value="24" />
</appSettings>
<system.web>
<compilation debug="true" targetFramework="4.7.2" />
<httpRuntime targetFramework="4.7.2" />
</system.web>
<system.webServer>
<handlers>
<remove name="ExtensionlessUrlHandler-Integrated-4.0" />
<remove name="OPTIONSVerbHandler" />
<remove name="TRACEVerbHandler" />
<add name="ExtensionlessUrlHandler-Integrated-4.0" path="*." verb="*" type="System.Web.Handlers.TransferRequestHandler" preCondition="integratedMode,runtimeVersionv4.0" />
</handlers>
</system.webServer>
<runtime>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="Newtonsoft.Json" culture="neutral" publicKeyToken="30ad4fe6b2a6aeed" />
<bindingRedirect oldVersion="0.0.0.0-13.0.0.0" newVersion="13.0.0.0" />
</dependentAssembly>
</assemblyBinding>
</runtime>
</configuration>

@ -0,0 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net472</TargetFramework>
<PlatformTarget>x64</PlatformTarget>
<RootNamespace>CncModels.Tests</RootNamespace>
<AssemblyName>CncModels.Tests</AssemblyName>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<!-- 测试项目不生成XML文档 -->
<GenerateDocumentationFile>false</GenerateDocumentationFile>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies.net472" Version="1.0.3" PrivateAssets="all" />
<PackageReference Include="xunit" Version="2.8.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.10.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\CncModels\CncModels.csproj" />
</ItemGroup>
</Project>

@ -0,0 +1,26 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net472</TargetFramework>
<PlatformTarget>x64</PlatformTarget>
<RootNamespace>CncRepository.Tests</RootNamespace>
<AssemblyName>CncRepository.Tests</AssemblyName>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>false</GenerateDocumentationFile>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies.net472" Version="1.0.3" PrivateAssets="all" />
<PackageReference Include="xunit" Version="2.8.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.10.0" />
<PackageReference Include="Dapper" Version="2.1.35" />
<PackageReference Include="MySqlConnector" Version="2.3.7" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\CncModels\CncModels.csproj" />
<ProjectReference Include="..\..\src\CncRepository\CncRepository.csproj" />
</ItemGroup>
</Project>

@ -0,0 +1,26 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net472</TargetFramework>
<PlatformTarget>x64</PlatformTarget>
<RootNamespace>CncService.Tests</RootNamespace>
<AssemblyName>CncService.Tests</AssemblyName>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>false</GenerateDocumentationFile>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies.net472" Version="1.0.3" PrivateAssets="all" />
<PackageReference Include="xunit" Version="2.8.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.10.0" />
<PackageReference Include="Moq" Version="4.20.72" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\CncModels\CncModels.csproj" />
<ProjectReference Include="..\..\src\CncRepository\CncRepository.csproj" />
<ProjectReference Include="..\..\src\CncService\CncService.csproj" />
</ItemGroup>
</Project>

@ -0,0 +1,30 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net472</TargetFramework>
<PlatformTarget>x64</PlatformTarget>
<RootNamespace>CncWebApi.Tests</RootNamespace>
<AssemblyName>CncWebApi.Tests</AssemblyName>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>false</GenerateDocumentationFile>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies.net472" Version="1.0.3" PrivateAssets="all" />
<PackageReference Include="xunit" Version="2.8.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.10.0" />
<PackageReference Include="Moq" Version="4.20.72" />
<!-- 用于测试 Controller 的 HTTP 相关类 -->
<PackageReference Include="Microsoft.AspNet.WebApi.Core" Version="5.3.0" />
<PackageReference Include="Microsoft.AspNet.WebApi.Owin" Version="5.3.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\CncModels\CncModels.csproj" />
<ProjectReference Include="..\..\src\CncRepository\CncRepository.csproj" />
<ProjectReference Include="..\..\src\CncService\CncService.csproj" />
<ProjectReference Include="..\..\src\CncWebApi\CncWebApi.csproj" />
</ItemGroup>
</Project>
Loading…
Cancel
Save