提交 a31f0e53 作者: zhongye-dev

feat: 完善隔离中心各业务模块功能

- 新增田间调查(FieldSurvey)、疫情处置(PestHandle)、设备总览(DeviceOverview)、监测数据分析(MonitorData)等模块
- 优化报告管理:弹窗只读、应检有害生物名单列表显示、表格列调整、删除审批数量列
- 优化 header:高度加大到 56px,logo 区域与 header 同色
- 优化菜单导航与路由配置
- 新增实验检测、样品检测 store 与数据
- 新增植物检疫计划报告 Word 模板
上级 cb77bc2f
...@@ -10,6 +10,7 @@ ...@@ -10,6 +10,7 @@
"dependencies": { "dependencies": {
"@element-plus/icons-vue": "^2.3.1", "@element-plus/icons-vue": "^2.3.1",
"element-plus": "^2.6.2", "element-plus": "^2.6.2",
"jszip": "^3.10.2",
"pinia": "^2.1.7", "pinia": "^2.1.7",
"vue": "^3.4.21", "vue": "^3.4.21",
"vue-router": "^4.3.0" "vue-router": "^4.3.0"
...@@ -1115,6 +1116,12 @@ ...@@ -1115,6 +1116,12 @@
"integrity": "sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==", "integrity": "sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/core-util-is": {
"version": "1.0.3",
"resolved": "https://registry.npmmirror.com/core-util-is/-/core-util-is-1.0.3.tgz",
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
"license": "MIT"
},
"node_modules/csstype": { "node_modules/csstype": {
"version": "3.2.3", "version": "3.2.3",
"resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.2.3.tgz", "resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.2.3.tgz",
...@@ -1225,6 +1232,45 @@ ...@@ -1225,6 +1232,45 @@
"node": "^8.16.0 || ^10.6.0 || >=11.0.0" "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
} }
}, },
"node_modules/immediate": {
"version": "3.0.6",
"resolved": "https://registry.npmmirror.com/immediate/-/immediate-3.0.6.tgz",
"integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==",
"license": "MIT"
},
"node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmmirror.com/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"license": "ISC"
},
"node_modules/isarray": {
"version": "1.0.0",
"resolved": "https://registry.npmmirror.com/isarray/-/isarray-1.0.0.tgz",
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
"license": "MIT"
},
"node_modules/jszip": {
"version": "3.10.2",
"resolved": "https://registry.npmmirror.com/jszip/-/jszip-3.10.2.tgz",
"integrity": "sha512-3l+rb15IOWtUhU0H5MFqES/T6Kh7abYwjosBey/vD6hDt8zoEffkSC5Ws5SGtgVw3gBx2NEbhTeSW1+kWkpyTQ==",
"license": "(MIT OR GPL-3.0-or-later)",
"dependencies": {
"lie": "~3.3.0",
"pako": "~1.0.2",
"readable-stream": "~2.3.6",
"setimmediate": "^1.0.5"
}
},
"node_modules/lie": {
"version": "3.3.0",
"resolved": "https://registry.npmmirror.com/lie/-/lie-3.3.0.tgz",
"integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==",
"license": "MIT",
"dependencies": {
"immediate": "~3.0.5"
}
},
"node_modules/lodash": { "node_modules/lodash": {
"version": "4.18.1", "version": "4.18.1",
"resolved": "https://registry.npmmirror.com/lodash/-/lodash-4.18.1.tgz", "resolved": "https://registry.npmmirror.com/lodash/-/lodash-4.18.1.tgz",
...@@ -1287,6 +1333,12 @@ ...@@ -1287,6 +1333,12 @@
"integrity": "sha512-Wj7+EJQ8mSuXr2iWfnujrimU35R2W4FAErEyTmJoJ7ucwTn2hOUSsRehMb5RSYkxXGTM7Y9QpvPmp++w5ftoJw==", "integrity": "sha512-Wj7+EJQ8mSuXr2iWfnujrimU35R2W4FAErEyTmJoJ7ucwTn2hOUSsRehMb5RSYkxXGTM7Y9QpvPmp++w5ftoJw==",
"license": "BSD-3-Clause" "license": "BSD-3-Clause"
}, },
"node_modules/pako": {
"version": "1.0.11",
"resolved": "https://registry.npmmirror.com/pako/-/pako-1.0.11.tgz",
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
"license": "(MIT AND Zlib)"
},
"node_modules/picocolors": { "node_modules/picocolors": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz", "resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz",
...@@ -1343,6 +1395,27 @@ ...@@ -1343,6 +1395,27 @@
"node": "^10 || ^12 || >=14" "node": "^10 || ^12 || >=14"
} }
}, },
"node_modules/process-nextick-args": {
"version": "2.0.1",
"resolved": "https://registry.npmmirror.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
"license": "MIT"
},
"node_modules/readable-stream": {
"version": "2.3.8",
"resolved": "https://registry.npmmirror.com/readable-stream/-/readable-stream-2.3.8.tgz",
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
"license": "MIT",
"dependencies": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.3",
"isarray": "~1.0.0",
"process-nextick-args": "~2.0.0",
"safe-buffer": "~5.1.1",
"string_decoder": "~1.1.1",
"util-deprecate": "~1.0.1"
}
},
"node_modules/rollup": { "node_modules/rollup": {
"version": "4.63.1", "version": "4.63.1",
"resolved": "https://registry.npmmirror.com/rollup/-/rollup-4.63.1.tgz", "resolved": "https://registry.npmmirror.com/rollup/-/rollup-4.63.1.tgz",
...@@ -1389,6 +1462,18 @@ ...@@ -1389,6 +1462,18 @@
"fsevents": "~2.3.2" "fsevents": "~2.3.2"
} }
}, },
"node_modules/safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
"license": "MIT"
},
"node_modules/setimmediate": {
"version": "1.0.5",
"resolved": "https://registry.npmmirror.com/setimmediate/-/setimmediate-1.0.5.tgz",
"integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==",
"license": "MIT"
},
"node_modules/source-map-js": { "node_modules/source-map-js": {
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz", "resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz",
...@@ -1398,6 +1483,21 @@ ...@@ -1398,6 +1483,21 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/string_decoder": {
"version": "1.1.1",
"resolved": "https://registry.npmmirror.com/string_decoder/-/string_decoder-1.1.1.tgz",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"license": "MIT",
"dependencies": {
"safe-buffer": "~5.1.0"
}
},
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmmirror.com/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
"license": "MIT"
},
"node_modules/vite": { "node_modules/vite": {
"version": "5.4.21", "version": "5.4.21",
"resolved": "https://registry.npmmirror.com/vite/-/vite-5.4.21.tgz", "resolved": "https://registry.npmmirror.com/vite/-/vite-5.4.21.tgz",
......
...@@ -9,11 +9,12 @@ ...@@ -9,11 +9,12 @@
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"vue": "^3.4.21", "@element-plus/icons-vue": "^2.3.1",
"vue-router": "^4.3.0",
"pinia": "^2.1.7",
"element-plus": "^2.6.2", "element-plus": "^2.6.2",
"@element-plus/icons-vue": "^2.3.1" "jszip": "^3.10.2",
"pinia": "^2.1.7",
"vue": "^3.4.21",
"vue-router": "^4.3.0"
}, },
"devDependencies": { "devDependencies": {
"@vitejs/plugin-vue": "^5.0.4", "@vitejs/plugin-vue": "^5.0.4",
......
...@@ -23,7 +23,7 @@ input, select, textarea, button { font-family: inherit; font-size: inherit; outl ...@@ -23,7 +23,7 @@ input, select, textarea, button { font-family: inherit; font-size: inherit; outl
/* ========== 顶部导航 ========== */ /* ========== 顶部导航 ========== */
.app-header { .app-header {
height: 40px; height: 56px;
background: #d71e1e; background: #d71e1e;
color: #fff; color: #fff;
display: flex; display: flex;
...@@ -42,19 +42,18 @@ input, select, textarea, button { font-family: inherit; font-size: inherit; outl ...@@ -42,19 +42,18 @@ input, select, textarea, button { font-family: inherit; font-size: inherit; outl
font-size: 18px; font-size: 18px;
font-weight: bold; font-weight: bold;
letter-spacing: 1px; letter-spacing: 1px;
background: #c01a1a; background: #d71e1e;
border-right: 1px solid #b51616;
} }
.app-logo .logo-icon { .app-logo .logo-icon {
width: 22px; height: 22px; width: 30px; height: 30px;
margin-right: 8px; margin-right: 10px;
background: #fff; background: #fff;
color: #d71e1e; color: #d71e1e;
border-radius: 4px; border-radius: 4px;
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
font-size: 13px; font-size: 16px;
font-weight: bold; font-weight: bold;
} }
.app-nav { display: flex; align-items: center; height: 100%; flex: 1; } .app-nav { display: flex; align-items: center; height: 100%; flex: 1; }
...@@ -79,7 +78,7 @@ input, select, textarea, button { font-family: inherit; font-size: inherit; outl ...@@ -79,7 +78,7 @@ input, select, textarea, button { font-family: inherit; font-size: inherit; outl
font-size: 14px; font-size: 14px;
} }
.app-user .user-avatar { .app-user .user-avatar {
width: 24px; height: 24px; width: 30px; height: 30px;
border-radius: 50%; border-radius: 50%;
background: #fff; background: #fff;
color: #d71e1e; color: #d71e1e;
......
...@@ -22,13 +22,33 @@ ...@@ -22,13 +22,33 @@
<span class="menu-arrow">▸</span> <span class="menu-arrow">▸</span>
</div> </div>
<div class="sub-menu" :class="{ expanded: isExpanded(item) }"> <div class="sub-menu" :class="{ expanded: isExpanded(item) }">
<template v-for="sub in item.children" :key="sub.id">
<div <div
v-for="sub in item.children" v-if="!sub.children"
:key="sub.id"
class="sub-item" class="sub-item"
:class="{ active: isActive(sub.path) }" :class="{ active: isActive(sub.path) }"
@click="go(sub.path)" @click="go(sub.path)"
>{{ sub.name }}</div> >{{ sub.name }}</div>
<template v-else>
<div
class="sub-item has-sub-sub"
:class="{ expanded: isExpanded(sub), active: isChildActive(sub) }"
@click.stop="toggle(sub)"
>
<span class="sub-text">{{ sub.name }}</span>
<span class="sub-arrow">▸</span>
</div>
<div class="sub-sub-menu" :class="{ expanded: isExpanded(sub) }">
<div
v-for="leaf in sub.children"
:key="leaf.id"
class="sub-sub-item"
:class="{ active: isActive(leaf.path) }"
@click="go(leaf.path)"
>{{ leaf.name }}</div>
</div>
</template>
</template>
</div> </div>
</template> </template>
</template> </template>
......
// 实验检验列表数据(初始为空)
export const experimentTestData = [];
// 田间调查列表数据(与样本抽检对应,10 条)
export const fieldSurveyData = [
{ id: 1, cert: "6204122022007095", unit: "甘肃富赢种子高新科技有限责任公司", plant: "翠菊", variety: "翠菊", origin: "智利", contact: "刘玉堂", phone: "18993607501", approveQty: "40 千克", importQty: "0.0014 千克", qty: "150克", surveyDate: "2022-03-20", testOrg: "甘州区农业技术推广中心", surveyProvince: "甘肃省", surveyCity: "张掖市", surveyDistrict: "甘州区", surveyTownship: "甘州镇", surveyAddress: "甘肃省张掖市甘州区甘州镇示范园区 A-12 号", longitude: "100.454800", latitude: "38.930100", altitude: "1490", useType: "种子", plantType: "经作类种子", plantPeriodStart: "2022-03-20", plantPeriodEnd: "2022-05-31", totalArea: "0.1", growthPeriod: "生长期", surveyMethods: "随机抽样", surveyMethodsOther: "", targetPests: "Ambrosia spp. 豚草(属)\nXiphinema Cobb(The species transmit viruses) 剑线虫属(传毒种类)\n中华人民共和国法律规定的其它检疫性有害生物", remarks: "田间观察到疑似检疫性有害生物症状,现场无法确诊,需采样送实验室进行进一步检验。", conclusion: "现场不能确诊,需要采样进行实验室检验", recorder: "甘州区系统管理员", partySignature: "刘玉堂", inspector1: "胡琴", inspector2: "刘洋", inspectorPhone: "0936-8212469", sampleUnit: "甘州区农业技术推广中心", sampleContact: "甘州区系统管理员", samplePhone: "0936-8212469", symptomDesc: "为害症状描述示例", handlingReason: "发生与处理情况示例", sampleMethod: "抽样方法示例", labResult: "实验室尚未出检验结果", entryDate: "2022-03-25" },
{ id: 2, cert: "6205022022007347", unit: "甘肃富赢种子高新科技有限责任公司", plant: "莴苣", variety: "生菜", origin: "美国", contact: "刘玉堂", phone: "18993607501", approveQty: "50 千克", importQty: "0.2950 千克", qty: "150克", surveyDate: "2022-04-10", testOrg: "甘州区农业技术推广中心", surveyProvince: "甘肃省", surveyCity: "张掖市", surveyDistrict: "甘州区", surveyTownship: "梁家墩镇", surveyAddress: "甘肃省张掖市甘州区梁家墩镇清嘉公路 3 号", longitude: "100.464500", latitude: "38.945500", altitude: "1502", useType: "种子", plantType: "经作类种子", plantPeriodStart: "2022-04-01", plantPeriodEnd: "2022-06-15", totalArea: "0.3", growthPeriod: "苗期", surveyMethods: "对角线,Z字型", surveyMethodsOther: "", targetPests: "Meloidogyne chitwoodi 奇氏根结线虫\\nLiriomyza spp. 斑潜蝇属\nHelicoverpa zea (Boddie) 谷实夜蛾\nHelix aspersa Müller 散大蜗牛\nCenchrus spp. (non-Chinese species) 蒺藜草(属)(非中国种)\n中华人民共和国法律规定的其它检疫性有害生物", remarks: "田间现场确诊发现检疫性有害生物,需采取相应除害处置措施。", conclusion: "现场确诊发现应检有害生物", recorder: "甘州区系统管理员", partySignature: "刘玉堂", inspector1: "胡琴", inspector2: "刘洋", inspectorPhone: "0936-8212469", sampleUnit: "甘州区农业技术推广中心", sampleContact: "甘州区系统管理员", samplePhone: "0936-8212469", symptomDesc: "", handlingReason: "", sampleMethod: "", labResult: "", entryDate: "2022-04-12" , pestDetail: "田间现场调查发现莴苣叶片出现典型应检有害生物为害症状,植株生长点及叶背可见活体害虫及为害斑,经现场形态学鉴定,确认为检疫性有害生物。", noticeNo: "DC202209080002", noticeTestResult: "经现场鉴定及实验室复核,确认该批莴苣种子携带有检疫性有害生物,依据《植物检疫条例》及国家相关技术规范,需立即采取除害处置措施。", noticeHandleOpinion: "经现场鉴定及实验室复核,确认该批莴苣种子携带有检疫性有害生物,依据《植物检疫条例》及国家相关技术规范,需立即采取除害处置措施。", certNo: "RD202209080002", certOpinionType: "疫情处理合格", certLeader: "刘玉堂", certQuarantineOfficer: "胡琴", certHandleOpinion: "除害处理已完成,处理过程符合相关技术规范。", certImplementation: "已按处理方案要求完成除害处理,处理过程符合相关技术规范,处理结果经现场检查和实验室检测确认合格。", certDate: "2022-09-08"},
{ id: 3, cert: "6205022022007269", unit: "甘肃富赢种子高新科技有限责任公司", plant: "青花菜 (西兰花)", variety: "西蓝花 VBR007", origin: "美国", contact: "刘玉堂", phone: "18993607501", approveQty: "50 千克", importQty: "2.3920 千克", qty: "150克", surveyDate: "2022-04-12", testOrg: "甘州区农业技术推广中心", surveyProvince: "甘肃省", surveyCity: "张掖市", surveyDistrict: "甘州区", surveyTownship: "上秦镇", surveyAddress: "甘肃省张掖市甘州区上秦镇徐赵寨村 5 号", longitude: "100.442100", latitude: "38.951200", altitude: "1510", useType: "种子", plantType: "经作类种子", plantPeriodStart: "2022-04-05", plantPeriodEnd: "2022-07-20", totalArea: "0.5", growthPeriod: "苗期", surveyMethods: "5点取样,平行线", surveyMethodsOther: "", targetPests: "Meloidogyne enterolobii 象耳豆根结线虫\\nPlutella xylostella 小菜蛾\nHelicoverpa zea (Boddie) 谷实夜蛾\nHelix aspersa Müller 散大蜗牛\nCenchrus spp. (non-Chinese species) 蒺藜草(属)(非中国种)\n中华人民共和国法律规定的其它检疫性有害生物", remarks: "田间观察未发现明显检疫性有害生物症状,植株整体长势正常。", conclusion: "现场确诊未发现应检有害生物", recorder: "甘州区系统管理员", partySignature: "刘玉堂", inspector1: "胡琴", inspector2: "刘洋", inspectorPhone: "0936-8212469", sampleUnit: "甘州区农业技术推广中心", sampleContact: "甘州区系统管理员", samplePhone: "0936-8212469", symptomDesc: "", handlingReason: "", sampleMethod: "", labResult: "", entryDate: "2022-04-15" },
{ id: 4, cert: "6204122022007087", unit: "甘肃富赢种子高新科技有限责任公司", plant: "青花菜 (西兰花)", variety: "西蓝花 VBR005", origin: "智利", contact: "刘玉堂", phone: "18993607501", approveQty: "50 千克", importQty: "0.3700 千克", qty: "150克", surveyDate: "2022-04-12", testOrg: "甘州区农业技术推广中心", surveyProvince: "甘肃省", surveyCity: "张掖市", surveyDistrict: "甘州区", surveyTownship: "上秦镇", surveyAddress: "甘肃省张掖市甘州区上秦镇付家寨村 2 号", longitude: "100.449800", latitude: "38.956600", altitude: "1508", useType: "种子", plantType: "经作类种子", plantPeriodStart: "2022-04-05", plantPeriodEnd: "2022-07-18", totalArea: "0.4", growthPeriod: "苗期", surveyMethods: "9点取样,棋盘式", surveyMethodsOther: "", targetPests: "Meloidogyne enterolobii 象耳豆根结线虫\nHelicoverpa zea (Boddie) 谷实夜蛾\nHelix aspersa Müller 散大蜗牛\nCenchrus spp. (non-Chinese species) 蒺藜草(属)(非中国种)\n中华人民共和国法律规定的其它检疫性有害生物", remarks: "田间观察未发现明显检疫性有害生物症状,植株长势正常。", conclusion: "现场确诊未发现应检有害生物", recorder: "甘州区系统管理员", partySignature: "刘玉堂", inspector1: "胡琴", inspector2: "刘洋", inspectorPhone: "0936-8212469", sampleUnit: "甘州区农业技术推广中心", sampleContact: "甘州区系统管理员", samplePhone: "0936-8212469", symptomDesc: "", handlingReason: "", sampleMethod: "", labResult: "", entryDate: "2022-04-15" },
{ id: 5, cert: "6202442022007001", unit: "甘肃富赢种子高新科技有限责任公司", plant: "胡萝卜", variety: "胡萝卜 VCR324", origin: "南非", contact: "刘玉堂", phone: "18993607501", approveQty: "50 千克", importQty: "0.6150 千克", qty: "150克", surveyDate: "2022-04-15", testOrg: "甘州区农业技术推广中心", surveyProvince: "甘肃省", surveyCity: "张掖市", surveyDistrict: "甘州区", surveyTownship: "三闸镇", surveyAddress: "甘肃省张掖市甘州区三闸镇三闸村 1 号", longitude: "100.472200", latitude: "38.970100", altitude: "1505", useType: "种子", plantType: "经作类种子", plantPeriodStart: "2022-04-10", plantPeriodEnd: "2022-08-30", totalArea: "0.2", growthPeriod: "苗期", surveyMethods: "5点取样", surveyMethodsOther: "", targetPests: "Heterodera carotae 胡萝卜胞囊线虫\nHelicoverpa zea (Boddie) 谷实夜蛾\nHelix aspersa Müller 散大蜗牛\nCenchrus spp. (non-Chinese species) 蒺藜草(属)(非中国种)\n中华人民共和国法律规定的其它检疫性有害生物", remarks: "田间观察未发现明显检疫性有害生物症状,出苗整齐。", conclusion: "现场确诊未发现应检有害生物", recorder: "甘州区系统管理员", partySignature: "刘玉堂", inspector1: "胡琴", inspector2: "刘洋", inspectorPhone: "0936-8212469", sampleUnit: "甘州区农业技术推广中心", sampleContact: "甘州区系统管理员", samplePhone: "0936-8212469", symptomDesc: "", handlingReason: "", sampleMethod: "", labResult: "", entryDate: "2022-04-18" },
{ id: 6, cert: "6203122022007067", unit: "甘肃杰尼尔种子有限公司", plant: "辣椒", variety: "80", origin: "西班牙", contact: "赵文言", phone: "18993755577", approveQty: "10 千克", importQty: "10.0 千克", qty: "150克", surveyDate: "2022-04-25", testOrg: "甘州区农业技术推广中心", surveyProvince: "甘肃省", surveyCity: "张掖市", surveyDistrict: "甘州区", surveyTownship: "党寨镇", surveyAddress: "甘肃省张掖市甘州区党寨镇上寨村 7 号", longitude: "100.481500", latitude: "38.932100", altitude: "1498", useType: "种子", plantType: "经作类种子", plantPeriodStart: "2022-04-20", plantPeriodEnd: "2022-09-10", totalArea: "0.15", growthPeriod: "苗期", surveyMethods: "对角线,随机抽样", surveyMethodsOther: "", targetPests: "Pepper mild mottle virus 辣椒轻斑驳病毒\nHelicoverpa zea (Boddie) 谷实夜蛾\nHelix aspersa Müller 散大蜗牛\nCenchrus spp. (non-Chinese species) 蒺藜草(属)(非中国种)\n中华人民共和国法律规定的其它检疫性有害生物", remarks: "田间观察未发现明显检疫性有害生物症状,植株长势正常。", conclusion: "现场确诊未发现应检有害生物", recorder: "甘州区系统管理员", partySignature: "赵文言", inspector1: "胡琴", inspector2: "刘洋", inspectorPhone: "0936-8212469", sampleUnit: "甘州区农业技术推广中心", sampleContact: "甘州区系统管理员", samplePhone: "0936-8212469", symptomDesc: "", handlingReason: "", sampleMethod: "", labResult: "", entryDate: "2022-04-28" },
{ id: 7, cert: "6204122022007065", unit: "甘肃富赢种子高新科技有限责任公司", plant: "翠菊", variety: "翠菊", origin: "智利", contact: "刘玉堂", phone: "18993607501", approveQty: "40 千克", importQty: "0.0014 千克", qty: "150克", surveyDate: "2022-03-25", testOrg: "甘州区农业技术推广中心", surveyProvince: "甘肃省", surveyCity: "张掖市", surveyDistrict: "甘州区", surveyTownship: "甘州镇", surveyAddress: "甘肃省张掖市甘州区甘州镇示范园区 B-3 号", longitude: "100.455500", latitude: "38.931200", altitude: "1492", useType: "种子", plantType: "经作类种子", plantPeriodStart: "2022-03-20", plantPeriodEnd: "2022-05-31", totalArea: "0.1", growthPeriod: "生长期", surveyMethods: "随机抽样", surveyMethodsOther: "", targetPests: "Nacobbus aberrans\nHelicoverpa zea (Boddie) 谷实夜蛾\nHelix aspersa Müller 散大蜗牛\nCenchrus spp. (non-Chinese species) 蒺藜草(属)(非中国种)\n中华人民共和国法律规定的其它检疫性有害生物", remarks: "田间观察未发现明显检疫性有害生物症状,植株长势正常。", conclusion: "现场确诊未发现应检有害生物", recorder: "甘州区系统管理员", partySignature: "刘玉堂", inspector1: "胡琴", inspector2: "刘洋", inspectorPhone: "0936-8212469", sampleUnit: "甘州区农业技术推广中心", sampleContact: "甘州区系统管理员", samplePhone: "0936-8212469", symptomDesc: "", handlingReason: "", sampleMethod: "", labResult: "", entryDate: "2022-03-30" },
{ id: 8, cert: "6203072022007093", unit: "酒泉市酒洲种子有限责任公司", plant: "番茄", variety: "番茄", origin: "意大利", contact: "马世锋", phone: "13993708175", approveQty: "50 千克", importQty: "1.9540 千克", qty: "150克", surveyDate: "2022-04-05", testOrg: "甘州区农业技术推广中心", surveyProvince: "甘肃省", surveyCity: "张掖市", surveyDistrict: "甘州区", surveyTownship: "新墩镇", surveyAddress: "甘肃省张掖市甘州区新墩镇南华村 4 号", longitude: "100.461200", latitude: "38.948800", altitude: "1503", useType: "种子", plantType: "经作类种子", plantPeriodStart: "2022-04-01", plantPeriodEnd: "2022-08-15", totalArea: "0.4", growthPeriod: "苗期", surveyMethods: "Z字型,随机抽样", surveyMethodsOther: "", targetPests: "Tomato yellow leaf curl virus 番茄黄化曲叶病毒\\nLiriomyza trifolii 三叶斑潜蝇\nHelicoverpa zea (Boddie) 谷实夜蛾\nHelix aspersa Müller 散大蜗牛\nCenchrus spp. (non-Chinese species) 蒺藜草(属)(非中国种)\n中华人民共和国法律规定的其它检疫性有害生物", remarks: "田间观察未发现明显检疫性有害生物症状,植株整体表现良好。", conclusion: "现场确诊未发现应检有害生物", recorder: "甘州区系统管理员", partySignature: "马世锋", inspector1: "胡琴", inspector2: "刘洋", inspectorPhone: "0936-8212469", sampleUnit: "甘州区农业技术推广中心", sampleContact: "甘州区系统管理员", samplePhone: "0936-8212469", symptomDesc: "", handlingReason: "", sampleMethod: "", labResult: "", entryDate: "2022-04-08" },
{ id: 9, cert: "6203092022007071", unit: "张掖市瑞真种业有限公司", plant: "五色菊", variety: "五色菊", origin: "荷兰", contact: "吴多志", phone: "13993662953", approveQty: "5 千克", importQty: "2.0000 千克", qty: "150克", surveyDate: "2022-04-18", testOrg: "甘州区农业技术推广中心", surveyProvince: "甘肃省", surveyCity: "张掖市", surveyDistrict: "甘州区", surveyTownship: "沙井镇", surveyAddress: "甘肃省张掖市甘州区沙井镇沙井村 2 号", longitude: "100.438800", latitude: "38.972200", altitude: "1512", useType: "种子", plantType: "经作类种子", plantPeriodStart: "2022-04-15", plantPeriodEnd: "2022-06-30", totalArea: "0.08", growthPeriod: "生长期", surveyMethods: "5点取样,棋盘式", surveyMethodsOther: "", targetPests: "Liriomyza huidobrensis 南美斑潜蝇\nHelicoverpa zea (Boddie) 谷实夜蛾\nHelix aspersa Müller 散大蜗牛\nCenchrus spp. (non-Chinese species) 蒺藜草(属)(非中国种)\n中华人民共和国法律规定的其它检疫性有害生物", remarks: "田间观察未发现明显检疫性有害生物症状,无异常表现。", conclusion: "现场确诊未发现应检有害生物", recorder: "甘州区系统管理员", partySignature: "吴多志", inspector1: "胡琴", inspector2: "刘洋", inspectorPhone: "0936-8212469", sampleUnit: "甘州区农业技术推广中心", sampleContact: "甘州区系统管理员", samplePhone: "0936-8212469", symptomDesc: "", handlingReason: "", sampleMethod: "", labResult: "", entryDate: "2022-04-22" },
{ id: 10, cert: "6201162022007023", unit: "张掖市亚太种业有限责任公司", plant: "南瓜", variety: "南瓜", origin: "日本", contact: "姚庆库", phone: "18919369888", approveQty: "5 千克", importQty: "3.4000 千克", qty: "150克", surveyDate: "2022-04-22", testOrg: "甘州区农业技术推广中心", surveyProvince: "甘肃省", surveyCity: "张掖市", surveyDistrict: "甘州区", surveyTownship: "大满镇", surveyAddress: "甘肃省张掖市甘州区大满镇大满村 1 号", longitude: "100.425500", latitude: "38.958800", altitude: "1518", useType: "种子", plantType: "经作类种子", plantPeriodStart: "2022-04-20", plantPeriodEnd: "2022-09-15", totalArea: "0.25", growthPeriod: "苗期", surveyMethods: "平行线,随机抽样", surveyMethodsOther: "", targetPests: "Cucurbit chlorotic yellows virus 瓜类褪绿黄化病毒\nHelicoverpa zea (Boddie) 谷实夜蛾\nHelix aspersa Müller 散大蜗牛\nCenchrus spp. (non-Chinese species) 蒺藜草(属)(非中国种)\n中华人民共和国法律规定的其它检疫性有害生物", remarks: "田间观察未发现明显检疫性有害生物症状,植株生长整齐。", conclusion: "现场确诊未发现应检有害生物", recorder: "甘州区系统管理员", partySignature: "姚庆库", inspector1: "胡琴", inspector2: "刘洋", inspectorPhone: "0936-8212469", sampleUnit: "甘州区农业技术推广中心", sampleContact: "甘州区系统管理员", samplePhone: "0936-8212469", symptomDesc: "", handlingReason: "", sampleMethod: "", labResult: "", entryDate: "2022-04-26" }
];
...@@ -22,10 +22,18 @@ export const menuConfig = [ ...@@ -22,10 +22,18 @@ export const menuConfig = [
] ]
}, },
{ {
id: "isolation", name: "隔离检疫", icon: "Lock", path: "/isolation/quarantine" id: "isolation", name: "田间调查", icon: "LocationInformation", path: "/field/survey"
}, },
{ {
id: "disposal", name: "除害处置", icon: "Delete", path: "/disposal" id: "disposal", name: "除害处理", icon: "Delete", path: "/disposal"
},
{
id: "isolation-monitor", name: "隔离监测", icon: "Monitor",
children: [
{ id: "isolation-device", name: "设备总览", path: "/isolation/device" },
{ id: "isolation-data", name: "监测数据", path: "/isolation/data" },
{ id: "isolation-analysis", name: "监测数据分析", path: "/isolation/analysis" }
]
}, },
{ {
id: "analysis", name: "数据分析", icon: "DataAnalysis", path: "/analysis" id: "analysis", name: "数据分析", icon: "DataAnalysis", path: "/analysis"
......
// 样本抽检列表数据(与样本登记入库保持对应,每条 150 克抽检量) // 样本抽检列表数据(与样本登记入库保持对应,每条 150 克抽检量)
export const sampleTestData = [ export const sampleTestData = [
{ id: 1, cert: "6204122022007095", unit: "甘肃富赢种子高新科技有限责任公司", plant: "翠菊", variety: "翠菊", origin: "智利", contact: "刘玉堂", phone: "18993607501", approveQty: "40 千克", qty: "150克", testDate: "2022-03-05", testOrg: "甘州区农业技术推广中心", content: "现场对该批种苗进行抽样查验,按 GB/T 3543.2-2009 标准扦取检测样品 150 克,未发现植株携带可见检疫性有害生物症状,实验室镜检未见虫卵及病原孢子。", conclusion: "合格,未检出检疫性有害生物", inspector1: "胡琴", inspector2: "刘洋", inspectorPhone: "0931-8665155", entryDate: "2022-03-05" }, { id: 1, cert: "6204122022007095", unit: "甘肃富赢种子高新科技有限责任公司", plant: "翠菊", variety: "翠菊", origin: "智利", contact: "刘玉堂", phone: "18993607501", approveQty: "40 千克", importQty: "0.0014 千克", qty: "150克", testDate: "2022-03-05", testOrg: "甘州区农业技术推广中心", content: "现场对该批种苗进行抽样查验,按 GB/T 3543.2-2009 标准扦取检测样品 150 克,未发现植株携带可见检疫性有害生物症状,实验室镜检未见虫卵及病原孢子。", conclusion: "合格,未检出检疫性有害生物", inspector1: "胡琴", inspector2: "刘洋", inspectorPhone: "0931-8665155", entryDate: "2022-03-05" },
{ id: 2, cert: "6205022022007347", unit: "甘肃富赢种子高新科技有限责任公司", plant: "莴苣", variety: "生菜", origin: "美国", contact: "刘玉堂", phone: "18993607501", approveQty: "50 千克", qty: "150克", testDate: "2022-04-01", testOrg: "甘州区农业技术推广中心", content: "现场对该批种苗进行抽样查验,按 GB/T 3543.2-2009 标准扦取检测样品 150 克,未发现植株携带可见检疫性有害生物症状,实验室镜检未见虫卵及病原孢子。", conclusion: "合格,未检出检疫性有害生物", inspector1: "胡琴", inspector2: "刘洋", inspectorPhone: "0931-8665155", entryDate: "2022-04-01" }, { id: 2, cert: "6205022022007347", unit: "甘肃富赢种子高新科技有限责任公司", plant: "莴苣", variety: "生菜", origin: "美国", contact: "刘玉堂", phone: "18993607501", approveQty: "50 千克", importQty: "0.2950 千克", qty: "150克", testDate: "2022-04-01", testOrg: "甘州区农业技术推广中心", content: "现场对该批种苗进行抽样查验,按 GB/T 3543.2-2009 标准扦取检测样品 150 克,未发现植株携带可见检疫性有害生物症状,实验室镜检未见虫卵及病原孢子。", conclusion: "合格,未检出检疫性有害生物", inspector1: "胡琴", inspector2: "刘洋", inspectorPhone: "0931-8665155", entryDate: "2022-04-01" },
{ id: 3, cert: "6205022022007269", unit: "甘肃富赢种子高新科技有限责任公司", plant: "青花菜 (西兰花)", variety: "西蓝花 VBR007", origin: "美国", contact: "刘玉堂", phone: "18993607501", approveQty: "50 千克", qty: "150克", testDate: "2022-04-01", testOrg: "甘州区农业技术推广中心", content: "现场对该批种苗进行抽样查验,按 GB/T 3543.2-2009 标准扦取检测样品 150 克,未发现植株携带可见检疫性有害生物症状,实验室镜检未见虫卵及病原孢子。", conclusion: "合格,未检出检疫性有害生物", inspector1: "胡琴", inspector2: "刘洋", inspectorPhone: "0931-8665155", entryDate: "2022-04-01" }, { id: 3, cert: "6205022022007269", unit: "甘肃富赢种子高新科技有限责任公司", plant: "青花菜 (西兰花)", variety: "西蓝花 VBR007", origin: "美国", contact: "刘玉堂", phone: "18993607501", approveQty: "50 千克", importQty: "2.3920 千克", qty: "150克", testDate: "2022-04-01", testOrg: "甘州区农业技术推广中心", content: "现场对该批种苗进行抽样查验,按 GB/T 3543.2-2009 标准扦取检测样品 150 克,未发现植株携带可见检疫性有害生物症状,实验室镜检未见虫卵及病原孢子。", conclusion: "合格,未检出检疫性有害生物", inspector1: "胡琴", inspector2: "刘洋", inspectorPhone: "0931-8665155", entryDate: "2022-04-01" },
{ id: 4, cert: "6204122022007087", unit: "甘肃富赢种子高新科技有限责任公司", plant: "青花菜 (西兰花)", variety: "西蓝花 VBR005", origin: "智利", contact: "刘玉堂", phone: "18993607501", approveQty: "50 千克", qty: "150克", testDate: "2022-04-01", testOrg: "甘州区农业技术推广中心", content: "现场对该批种苗进行抽样查验,按 GB/T 3543.2-2009 标准扦取检测样品 150 克,未发现植株携带可见检疫性有害生物症状,实验室镜检未见虫卵及病原孢子。", conclusion: "合格,未检出检疫性有害生物", inspector1: "胡琴", inspector2: "刘洋", inspectorPhone: "0931-8665155", entryDate: "2022-04-01" }, { id: 4, cert: "6204122022007087", unit: "甘肃富赢种子高新科技有限责任公司", plant: "青花菜 (西兰花)", variety: "西蓝花 VBR005", origin: "智利", contact: "刘玉堂", phone: "18993607501", approveQty: "50 千克", importQty: "0.3700 千克", qty: "150克", testDate: "2022-04-01", testOrg: "甘州区农业技术推广中心", content: "现场对该批种苗进行抽样查验,按 GB/T 3543.2-2009 标准扦取检测样品 150 克,未发现植株携带可见检疫性有害生物症状,实验室镜检未见虫卵及病原孢子。", conclusion: "合格,未检出检疫性有害生物", inspector1: "胡琴", inspector2: "刘洋", inspectorPhone: "0931-8665155", entryDate: "2022-04-01" },
{ id: 5, cert: "6202442022007001", unit: "甘肃富赢种子高新科技有限责任公司", plant: "胡萝卜", variety: "胡萝卜 VCR324", origin: "南非", contact: "刘玉堂", phone: "18993607501", approveQty: "50 千克", qty: "150克", testDate: "2022-04-05", testOrg: "甘州区农业技术推广中心", content: "现场对该批种苗进行抽样查验,按 GB/T 3543.2-2009 标准扦取检测样品 150 克,未发现植株携带可见检疫性有害生物症状,实验室镜检未见虫卵及病原孢子。", conclusion: "合格,未检出检疫性有害生物", inspector1: "胡琴", inspector2: "刘洋", inspectorPhone: "0931-8665155", entryDate: "2022-04-05" }, { id: 5, cert: "6202442022007001", unit: "甘肃富赢种子高新科技有限责任公司", plant: "胡萝卜", variety: "胡萝卜 VCR324", origin: "南非", contact: "刘玉堂", phone: "18993607501", approveQty: "50 千克", importQty: "0.6150 千克", qty: "150克", testDate: "2022-04-05", testOrg: "甘州区农业技术推广中心", content: "现场对该批种苗进行抽样查验,按 GB/T 3543.2-2009 标准扦取检测样品 150 克,未发现植株携带可见检疫性有害生物症状,实验室镜检未见虫卵及病原孢子。", conclusion: "合格,未检出检疫性有害生物", inspector1: "胡琴", inspector2: "刘洋", inspectorPhone: "0931-8665155", entryDate: "2022-04-05" },
{ id: 6, cert: "6203122022007067", unit: "甘肃杰尼尔种子有限公司", plant: "辣椒", variety: "80", origin: "西班牙", contact: "刘玉堂", phone: "18993607501", approveQty: "10 千克", qty: "150克", testDate: "2022-04-15", testOrg: "甘州区农业技术推广中心", content: "现场对该批种苗进行抽样查验,按 GB/T 3543.2-2009 标准扦取检测样品 150 克,未发现植株携带可见检疫性有害生物症状,实验室镜检未见虫卵及病原孢子。", conclusion: "合格,未检出检疫性有害生物", inspector1: "胡琴", inspector2: "刘洋", inspectorPhone: "0931-8665155", entryDate: "2022-04-15" }, { id: 6, cert: "6203122022007067", unit: "甘肃杰尼尔种子有限公司", plant: "辣椒", variety: "80", origin: "西班牙", contact: "刘玉堂", phone: "18993607501", approveQty: "10 千克", importQty: "10.0 千克", qty: "150克", testDate: "2022-04-15", testOrg: "甘州区农业技术推广中心", content: "现场对该批种苗进行抽样查验,按 GB/T 3543.2-2009 标准扦取检测样品 150 克,未发现植株携带可见检疫性有害生物症状,实验室镜检未见虫卵及病原孢子。", conclusion: "合格,未检出检疫性有害生物", inspector1: "胡琴", inspector2: "刘洋", inspectorPhone: "0931-8665155", entryDate: "2022-04-15" },
{ id: 7, cert: "6204122022007065", unit: "甘肃富赢种子高新科技有限责任公司", plant: "翠菊", variety: "翠菊", origin: "智利", contact: "刘玉堂", phone: "18993607501", approveQty: "40 千克", qty: "150克", testDate: "2022-03-10", testOrg: "甘州区农业技术推广中心", content: "现场对该批种苗进行抽样查验,按 GB/T 3543.2-2009 标准扦取检测样品 150 克,未发现植株携带可见检疫性有害生物症状,实验室镜检未见虫卵及病原孢子。", conclusion: "合格,未检出检疫性有害生物", inspector1: "胡琴", inspector2: "刘洋", inspectorPhone: "0931-8665155", entryDate: "2022-03-10" }, { id: 7, cert: "6204122022007065", unit: "甘肃富赢种子高新科技有限责任公司", plant: "翠菊", variety: "翠菊", origin: "智利", contact: "刘玉堂", phone: "18993607501", approveQty: "40 千克", importQty: "0.0014 千克", qty: "150克", testDate: "2022-03-10", testOrg: "甘州区农业技术推广中心", content: "现场对该批种苗进行抽样查验,按 GB/T 3543.2-2009 标准扦取检测样品 150 克,未发现植株携带可见检疫性有害生物症状,实验室镜检未见虫卵及病原孢子。", conclusion: "合格,未检出检疫性有害生物", inspector1: "胡琴", inspector2: "刘洋", inspectorPhone: "0931-8665155", entryDate: "2022-03-10" },
{ id: 8, cert: "6203072022007093", unit: "酒泉市酒洲种子有限责任公司", plant: "番茄", variety: "番茄", origin: "意大利", contact: "刘玉堂", phone: "18993607501", approveQty: "50 千克", qty: "150克", testDate: "2022-03-25", testOrg: "甘州区农业技术推广中心", content: "现场对该批种苗进行抽样查验,按 GB/T 3543.2-2009 标准扦取检测样品 150 克,未发现植株携带可见检疫性有害生物症状,实验室镜检未见虫卵及病原孢子。", conclusion: "合格,未检出检疫性有害生物", inspector1: "胡琴", inspector2: "刘洋", inspectorPhone: "0931-8665155", entryDate: "2022-03-25" }, { id: 8, cert: "6203072022007093", unit: "酒泉市酒洲种子有限责任公司", plant: "番茄", variety: "番茄", origin: "意大利", contact: "刘玉堂", phone: "18993607501", approveQty: "50 千克", importQty: "1.9540 千克", qty: "150克", testDate: "2022-03-25", testOrg: "甘州区农业技术推广中心", content: "现场对该批种苗进行抽样查验,按 GB/T 3543.2-2009 标准扦取检测样品 150 克,未发现植株携带可见检疫性有害生物症状,实验室镜检未见虫卵及病原孢子。", conclusion: "合格,未检出检疫性有害生物", inspector1: "胡琴", inspector2: "刘洋", inspectorPhone: "0931-8665155", entryDate: "2022-03-25" },
{ id: 9, cert: "6203092022007071", unit: "张掖市瑞真种业有限公司", plant: "五色菊", variety: "五色菊", origin: "荷兰", contact: "刘玉堂", phone: "18993607501", approveQty: "5 千克", qty: "150克", testDate: "2022-04-05", testOrg: "甘州区农业技术推广中心", content: "现场对该批种苗进行抽样查验,按 GB/T 3543.2-2009 标准扦取检测样品 150 克,未发现植株携带可见检疫性有害生物症状,实验室镜检未见虫卵及病原孢子。", conclusion: "合格,未检出检疫性有害生物", inspector1: "胡琴", inspector2: "刘洋", inspectorPhone: "0931-8665155", entryDate: "2022-04-05" }, { id: 9, cert: "6203092022007071", unit: "张掖市瑞真种业有限公司", plant: "五色菊", variety: "五色菊", origin: "荷兰", contact: "刘玉堂", phone: "18993607501", approveQty: "5 千克", importQty: "2.0000 千克", qty: "150克", testDate: "2022-04-05", testOrg: "甘州区农业技术推广中心", content: "现场对该批种苗进行抽样查验,按 GB/T 3543.2-2009 标准扦取检测样品 150 克,未发现植株携带可见检疫性有害生物症状,实验室镜检未见虫卵及病原孢子。", conclusion: "合格,未检出检疫性有害生物", inspector1: "胡琴", inspector2: "刘洋", inspectorPhone: "0931-8665155", entryDate: "2022-04-05" },
{ id: 10, cert: "6201162022007023", unit: "张掖市亚太种业有限责任公司", plant: "南瓜", variety: "南瓜", origin: "日本", contact: "刘玉堂", phone: "18993607501", approveQty: "5 千克", qty: "150克", testDate: "2022-04-10", testOrg: "甘州区农业技术推广中心", content: "现场对该批种苗进行抽样查验,按 GB/T 3543.2-2009 标准扦取检测样品 150 克,未发现植株携带可见检疫性有害生物症状,实验室镜检未见虫卵及病原孢子。", conclusion: "合格,未检出检疫性有害生物", inspector1: "胡琴", inspector2: "刘洋", inspectorPhone: "0931-8665155", entryDate: "2022-04-10" } { id: 10, cert: "6201162022007023", unit: "张掖市亚太种业有限责任公司", plant: "南瓜", variety: "南瓜", origin: "日本", contact: "刘玉堂", phone: "18993607501", approveQty: "5 千克", importQty: "3.4000 千克", qty: "150克", testDate: "2022-04-10", testOrg: "甘州区农业技术推广中心", content: "现场对该批种苗进行抽样查验,按 GB/T 3543.2-2009 标准扦取检测样品 150 克,未发现植株携带可见检疫性有害生物症状,实验室镜检未见虫卵及病原孢子。", conclusion: "合格,未检出检疫性有害生物", inspector1: "胡琴", inspector2: "刘洋", inspectorPhone: "0931-8665155", entryDate: "2022-04-10" }
]; ];
\ No newline at end of file
...@@ -13,8 +13,11 @@ const routes = [ ...@@ -13,8 +13,11 @@ const routes = [
{ path: "/sample/out", name: "sample-out", component: () => import("@/views/sample/SampleOut.vue"), meta: { title: "样本出库登记", breadcrumb: "样本管理 / 样本出库登记" } }, { path: "/sample/out", name: "sample-out", component: () => import("@/views/sample/SampleOut.vue"), meta: { title: "样本出库登记", breadcrumb: "样本管理 / 样本出库登记" } },
{ path: "/lab/sample-test", name: "lab-sample-test", component: () => import("@/views/lab/SampleTest.vue"), meta: { title: "样本抽检", breadcrumb: "实验室检测 / 样本抽检" } }, { path: "/lab/sample-test", name: "lab-sample-test", component: () => import("@/views/lab/SampleTest.vue"), meta: { title: "样本抽检", breadcrumb: "实验室检测 / 样本抽检" } },
{ path: "/lab/experiment-test", name: "lab-experiment-test", component: () => import("@/views/lab/ExperimentTest.vue"), meta: { title: "实验检验", breadcrumb: "实验室检测 / 实验检验" } }, { path: "/lab/experiment-test", name: "lab-experiment-test", component: () => import("@/views/lab/ExperimentTest.vue"), meta: { title: "实验检验", breadcrumb: "实验室检测 / 实验检验" } },
{ path: "/isolation/quarantine", name: "isolation-quarantine", component: () => import("@/views/isolation/IsolationQuarantine.vue"), meta: { title: "隔离检疫", breadcrumb: "隔离检疫" } }, { path: "/field/survey", name: "field-survey", component: () => import("@/views/field/FieldSurvey.vue"), meta: { title: "田间调查", breadcrumb: "田间调查" } },
{ path: "/disposal", name: "disposal", component: () => import("@/views/disposal/Disposal.vue"), meta: { title: "除害处置", breadcrumb: "除害处置" } }, { path: "/disposal", name: "disposal-list", component: () => import("@/views/pest/PestHandle.vue"), meta: { title: "除害处理", breadcrumb: "除害处理 / 除害处理" } },
{ path: "/isolation/device", name: "isolation-device", component: () => import("@/views/isolation/DeviceOverview.vue"), meta: { title: "设备总览", breadcrumb: "隔离监测 / 设备总览" } },
{ path: "/isolation/data", name: "isolation-data", component: () => import("@/views/isolation/MonitorData.vue"), meta: { title: "监测数据", breadcrumb: "隔离监测 / 监测数据" } },
{ path: "/isolation/analysis", name: "isolation-analysis", component: () => import("@/views/isolation/MonitorAnalysis.vue"), meta: { title: "监测数据分析", breadcrumb: "隔离监测 / 监测数据分析" } },
{ path: "/analysis", name: "analysis", component: () => import("@/views/analysis/DataAnalysis.vue"), meta: { title: "数据分析", breadcrumb: "数据分析" } }, { path: "/analysis", name: "analysis", component: () => import("@/views/analysis/DataAnalysis.vue"), meta: { title: "数据分析", breadcrumb: "数据分析" } },
{ path: "/risk", name: "risk", component: () => import("@/views/risk/RiskWarning.vue"), meta: { title: "风险预警", breadcrumb: "风险预警" } }, { path: "/risk", name: "risk", component: () => import("@/views/risk/RiskWarning.vue"), meta: { title: "风险预警", breadcrumb: "风险预警" } },
{ path: "/report", name: "report", component: () => import("@/views/report/ReportManage.vue"), meta: { title: "报告管理", breadcrumb: "报告管理" } }, { path: "/report", name: "report", component: () => import("@/views/report/ReportManage.vue"), meta: { title: "报告管理", breadcrumb: "报告管理" } },
......
// 实验检验数据存储(与田间调查联动:从田间调查保存"现场不能确诊,需要采样进行实验室检验"的记录自动流入)
import { ref, watch } from "vue";
const STORAGE_KEY = "experiment_test_records";
const list = ref([]);
const load = () => {
try {
const stored = localStorage.getItem(STORAGE_KEY);
if (stored) {
list.value = JSON.parse(stored);
}
} catch (e) {
console.error("Failed to load experiment test records:", e);
}
};
const save = () => {
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(list.value));
} catch (e) {
console.error("Failed to save experiment test records:", e);
}
};
load();
watch(list, save, { deep: true });
const syncFromSurvey = (surveyRecord) => {
if (!surveyRecord) return;
if (surveyRecord.conclusion !== "现场不能确诊,需要采样进行实验室检验") {
removeBySurveyId(surveyRecord.id);
return;
}
const expRecord = {
cert: surveyRecord.cert,
unit: surveyRecord.unit,
plant: surveyRecord.plant,
variety: surveyRecord.variety,
origin: surveyRecord.origin,
contact: surveyRecord.contact,
phone: surveyRecord.phone,
approveQty: surveyRecord.approveQty,
importQty: surveyRecord.importQty,
qty: "150克",
testDate: surveyRecord.surveyDate,
testOrg: "甘州区农业技术推广中心",
content: "",
conclusion: surveyRecord.labResult || "",
inspector1: surveyRecord.inspector1,
inspector2: surveyRecord.inspector2,
inspectorPhone: surveyRecord.inspectorPhone || "0936-8212469",
entryDate: surveyRecord.entryDate,
sourceSurveyId: surveyRecord.id,
symptomDesc: surveyRecord.symptomDesc || "",
handlingReason: surveyRecord.handlingReason || "",
sampleMethod: surveyRecord.sampleMethod || "",
// 实验检验结果(独立字段,不复制田间调查的 labResult)
labResult: "",
// 默认检验方法
inspectionMethod: "按照 GB/T 3543.2-2009 标准进行实验室检验,镜检结合形态学鉴定方法对样品进行检测分析。",
sampleUnit: surveyRecord.sampleUnit || "甘州区农业技术推广中心",
sampleContact: surveyRecord.sampleContact || "甘州区系统管理员",
samplePhone: surveyRecord.samplePhone || "0936-8212469",
// 田间调查位置信息
surveyProvince: surveyRecord.surveyProvince || "甘肃省",
surveyCity: surveyRecord.surveyCity || "张掖市",
surveyDistrict: surveyRecord.surveyDistrict || "甘州区",
surveyTownship: surveyRecord.surveyTownship || "",
surveyAddress: surveyRecord.surveyAddress || "",
longitude: surveyRecord.longitude || "",
latitude: surveyRecord.latitude || "",
altitude: surveyRecord.altitude || "",
targetPests: surveyRecord.targetPests || "",
// 实验检验报告单专有字段
receiver: surveyRecord.receiver || "",
receiveDate: surveyRecord.receiveDate || "",
inspectionMethod: surveyRecord.inspectionMethod || "按照 GB/T 3543.2-2009 标准进行实验室检验,镜检结合形态学鉴定方法对样品进行检测分析。",
reviewPerson: surveyRecord.reviewPerson || "",
remarks: "" // 实验检验备注(独立字段,不复制田间调查 remarks)
};
const existingIndex = list.value.findIndex((r) => r.sourceSurveyId === surveyRecord.id);
if (existingIndex >= 0) {
list.value[existingIndex] = { ...list.value[existingIndex], ...expRecord };
} else {
const next = (list.value.length ? Math.max(...list.value.map((r) => r.id || 0)) : 0) + 1;
list.value = [...list.value, { id: next, ...expRecord }];
}
};
const removeBySurveyId = (surveyId) => {
const before = list.value.length;
list.value = list.value.filter((r) => r.sourceSurveyId !== surveyId);
return before !== list.value.length;
};
const removeById = (id) => {
list.value = list.value.filter((r) => r.id !== id);
};
const addRecord = (record) => {
const next = (list.value.length ? Math.max(...list.value.map((r) => r.id || 0)) : 0) + 1;
const newRec = { id: next, ...record };
list.value = [...list.value, newRec];
return newRec;
};
const updateRecord = (record) => {
const i = list.value.findIndex((r) => r.id === record.id);
if (i >= 0) {
list.value[i] = { ...record };
return true;
}
return false;
};
export { list, syncFromSurvey, removeBySurveyId, removeById, addRecord, updateRecord, save, load };
// 除害处理数据存储(与田间调查联动:从田间调查保存"现场确诊发现应检有害生物"的记录自动流入)
import { ref, watch } from "vue";
const STORAGE_KEY = "pest_handle_records";
const list = ref([]);
const load = () => {
try {
const stored = localStorage.getItem(STORAGE_KEY);
if (stored) {
list.value = JSON.parse(stored);
}
} catch (e) {
console.error("Failed to load pest disposal records:", e);
}
};
const save = () => {
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(list.value));
} catch (e) {
console.error("Failed to save pest disposal records:", e);
}
};
load();
watch(list, save, { deep: true });
// 同步:田间调查"现场确诊发现应检有害生物"的记录 → 除害处理列表
const syncFromSurvey = (surveyRecord) => {
if (!surveyRecord) return;
if (surveyRecord.conclusion !== "现场确诊发现应检有害生物") {
removeBySurveyId(surveyRecord.id);
return;
}
const dispRecord = {
cert: surveyRecord.cert,
unit: surveyRecord.unit,
plant: surveyRecord.plant,
variety: surveyRecord.variety,
origin: surveyRecord.origin,
contact: surveyRecord.contact,
phone: surveyRecord.phone,
approveQty: surveyRecord.approveQty,
importQty: surveyRecord.importQty,
qty: surveyRecord.qty || "150克",
surveyDate: surveyRecord.surveyDate,
surveyOrg: "甘州区农业技术推广中心",
pestDetail: surveyRecord.pestDetail || "",
inspector1: surveyRecord.inspector1,
inspector2: surveyRecord.inspector2,
inspectorPhone: surveyRecord.inspectorPhone || "0936-8212469",
entryDate: surveyRecord.entryDate,
sourceSurveyId: surveyRecord.id,
surveyProvince: surveyRecord.surveyProvince || "甘肃省",
surveyCity: surveyRecord.surveyCity || "张掖市",
surveyDistrict: surveyRecord.surveyDistrict || "甘州区",
surveyTownship: surveyRecord.surveyTownship || "",
surveyAddress: surveyRecord.surveyAddress || "",
longitude: surveyRecord.longitude || "",
latitude: surveyRecord.latitude || "",
altitude: surveyRecord.altitude || "",
targetPests: surveyRecord.targetPests || "",
noticeNo: surveyRecord.noticeNo || "",
noticeTestResult: surveyRecord.noticeTestResult || "",
noticeHandleOpinion: surveyRecord.noticeHandleOpinion || "",
disposalMethod: "",
disposalDate: "",
disposalPerson: "",
disposalResult: "",
remarks: ""
};
const existingIndex = list.value.findIndex((r) => r.sourceSurveyId === surveyRecord.id);
if (existingIndex >= 0) {
list.value[existingIndex] = { ...list.value[existingIndex], ...dispRecord };
} else {
const next = (list.value.length ? Math.max(...list.value.map((r) => r.id || 0)) : 0) + 1;
list.value = [...list.value, { id: next, ...dispRecord }];
}
};
const removeBySurveyId = (surveyId) => {
const before = list.value.length;
list.value = list.value.filter((r) => r.sourceSurveyId !== surveyId);
return before !== list.value.length;
};
const removeById = (id) => {
list.value = list.value.filter((r) => r.id !== id);
};
const addRecord = (record) => {
const next = (list.value.length ? Math.max(...list.value.map((r) => r.id || 0)) : 0) + 1;
const newRec = { id: next, ...record };
list.value = [...list.value, newRec];
return newRec;
};
const updateRecord = (record) => {
const i = list.value.findIndex((r) => r.id === record.id);
if (i >= 0) {
list.value[i] = { ...record };
return true;
}
return false;
};
export { list, syncFromSurvey, removeBySurveyId, removeById, addRecord, updateRecord, save, load };
<template>
<div class="page-list">
<div class="filter-box">
<div class="filter-row">
<div class="filter-item">
<label>调查日期:</label>
<DatePicker v-model="filters.surveyStart" class="dp-160" placeholder="开始日期" />
<span class="dash">至</span>
<DatePicker v-model="filters.surveyEnd" class="dp-160" placeholder="结束日期" />
</div>
<div class="filter-item">
<label>检疫单证号:</label>
<input class="ipt" v-model="filters.cert" placeholder="请输入关键字" />
</div>
<div class="filter-item">
<label>植物名称:</label>
<input class="ipt" v-model="filters.plant" placeholder="请输入关键字" />
</div>
</div>
<div class="filter-row">
<div class="filter-item">
<label>品种名称:</label>
<input class="ipt" v-model="filters.variety" placeholder="请输入关键字" />
</div>
<div class="filter-item">
<label>检验机构:</label>
<input class="ipt" v-model="filters.testOrg" placeholder="请输入关键字" />
</div>
<div class="filter-item">
<button class="btn" @click="onQuery">查询</button>
<button class="btn btn-default" @click="onReset">重置</button>
</div>
<div class="filter-item filter-add">
<button class="btn btn-add" @click="onAdd">+ 新增调查</button>
</div>
</div>
</div>
<div class="tbl-wrap">
<table class="simple-table">
<colgroup>
<col style="width:5%" />
<col style="width:14%" />
<col style="width:22%" />
<col style="width:9%" />
<col style="width:9%" />
<col style="width:9%" />
<col style="width:11%" />
<col style="width:13%" />
<col style="width:8%" />
</colgroup>
<thead>
<tr>
<th>序号</th>
<th>检疫单证号</th>
<th>申报单位</th>
<th>植物名称</th>
<th>品种名称</th>
<th>调查数量</th>
<th>调查日期</th>
<th>检验机构</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr v-for="(r, i) in pagedData" :key="r.id">
<td>{{ (currentPage - 1) * pageSize + i + 1 }}</td>
<td>{{ r.cert }}</td>
<td class="col-unit">{{ r.unit }}</td>
<td>{{ r.plant }}</td>
<td>{{ r.variety }}</td>
<td>{{ r.qty }}</td>
<td>{{ r.surveyDate }}</td>
<td>{{ r.testOrg }}</td>
<td class="col-action">
<a class="op-link" @click="onView(r)">查看</a>
<a class="op-link" @click="onEdit(r)">编辑</a>
<a class="op-link" @click="onDelete(r)">删除</a>
</td>
</tr>
<tr v-if="!pagedData.length">
<td colspan="9" class="empty">暂无数据</td>
</tr>
</tbody>
</table>
</div>
<div class="pagination">
<span class="total" style="margin-right:auto;">共 {{ filteredData.length }} 条记录</span>
<button class="page-btn" :disabled="currentPage === 1" @click="goPage(1)">首页</button>
<button class="page-btn" :disabled="currentPage === 1" @click="goPage(currentPage - 1)">上一页</button>
<button v-for="p in pageNumbers" :key="p" class="page-btn" :class="{ active: p === currentPage }" @click="goPage(p)">{{ p }}</button>
<button class="page-btn" :disabled="currentPage === totalPages" @click="goPage(currentPage + 1)">下一页</button>
<button class="page-btn" :disabled="currentPage === totalPages" @click="goPage(totalPages)">末页</button>
<span class="jump">跳转至 <input class="jump-input" v-model.number="jumpInput" @keyup.enter="onJump" /> 页</span>
</div>
<div v-if="viewVisible" class="modal-mask" @click.self="viewVisible = false">
<div class="modal-box modal-wide">
<div class="modal-top"><span class="modal-title">田间调查详情</span><span class="modal-close" @click="viewVisible = false">×</span></div>
<div class="modal-body">
<div class="related-files">
<div class="related-title">相关文件</div>
<div class="related-list">
<div class="related-item">国(境)外引进农业种苗检疫审批单</div>
<div class="related-item">国(境)外引进农业种苗入境后疫情监测申请书</div>
<div class="related-item">国(境)外引进农业种苗入境后疫情监测委托书</div>
</div>
</div>
<div class="section">
<div class="section-title">基本信息</div>
<div class="kv-grid">
<div class="kv"><span class="kv-label">受检单位:</span><span class="kv-value">{{ form.unit }}</span></div>
<div class="kv"><span class="kv-label">联系人:</span><span class="kv-value">{{ form.contact }}</span></div>
<div class="kv"><span class="kv-label">联系电话:</span><span class="kv-value">{{ form.phone }}</span></div>
<div class="kv"><span class="kv-label req">调查日期:</span><span class="kv-value">{{ form.surveyDate }}</span></div>
<div class="kv kv-wide">
<span class="kv-label">调查地点:</span>
<span class="kv-value">
<span class="addr-cell">{{ form.surveyProvince }}</span>
<span class="addr-cell">{{ form.surveyCity }}</span>
<span class="addr-cell">{{ form.surveyDistrict }}</span>
<span class="addr-cell">{{ form.surveyTownship }}</span>
<span class="addr-cell addr-input">{{ form.surveyAddress }}</span>
</span>
</div>
<div class="kv kv-wide">
<div class="geo-row">
<div class="geo-cell"><span class="kv-label">经度:</span><span class="kv-value">{{ form.longitude }}</span></div>
<div class="geo-cell"><span class="kv-label">纬度:</span><span class="kv-value">{{ form.latitude }}</span></div>
<div class="geo-cell"><span class="kv-label">海拔:</span><span class="kv-value">{{ form.altitude }}</span></div>
</div>
</div>
<div class="kv"><span class="kv-label">植物名称:</span><span class="kv-value">{{ form.plant }}</span></div>
<div class="kv"><span class="kv-label">产出用途:</span><span class="kv-value">{{ form.useType }}</span></div>
<div class="kv"><span class="kv-label req">植物类型:</span><span class="kv-value">{{ form.plantType }}</span></div>
<div class="kv">
<span class="kv-label">种植期限:</span>
<span class="kv-value">
<span class="addr-cell">{{ form.plantPeriodStart }}</span>
<span class="dash-inline">至</span>
<span class="addr-cell">{{ form.plantPeriodEnd }}</span>
</span>
</div>
<div class="kv"><span class="kv-label">总种植面积:</span><span class="kv-value">{{ form.totalArea }}</span></div>
<div class="kv"><span class="kv-label req">植物生育期:</span><span class="kv-value">{{ form.growthPeriod }}</span></div>
<div class="kv kv-wide">
<span class="kv-label req">调查方法(至少一项):</span>
<span class="kv-value">
<span class="method-chk" v-for="m in methodOptions" :key="m" :class="{ on: (form.surveyMethods||'').split(',').includes(m) }">{{ m }}</span>
<span v-if="form.surveyMethodsOther" class="method-other">其他:{{ form.surveyMethodsOther }}</span>
</span>
</div>
<div class="kv kv-wide">
<span class="kv-label">目标有害生物:</span>
<span class="kv-value area">{{ form.targetPests }}</span>
</div>
<div class="kv kv-wide">
<span class="kv-label">备注:</span>
<span class="kv-value area">{{ form.remarks }}</span>
</div>
<div class="kv kv-wide"><span class="kv-label req">本次调查结论:</span><span class="kv-value">{{ form.conclusion }}</span></div>
<div v-if="form.conclusion === '现场确诊发现应检有害生物'" class="pest-block">
<div class="pest-hint">请输入发现的应检有害生物名称及发生情况:</div>
<div class="ipt-cell area pest-readonly">{{ form.pestDetail || "—" }}</div>
</div>
</div>
</div>
<div class="section">
<div class="section-title">检疫机构信息</div>
<div class="kv-grid">
<div class="kv"><span class="kv-label">检疫单位:</span><span class="kv-value">{{ form.testOrg }}</span></div>
<div class="kv"><span class="kv-label">记录员:</span><span class="kv-value">{{ form.recorder }}</span></div>
<div class="kv"><span class="kv-label req">当事人(签名):</span><span class="kv-value">{{ form.partySignature }}</span></div>
<div class="kv"><span class="kv-label req">检疫员(签名)1:</span><span class="kv-value">{{ form.inspector1 }}</span></div>
<div class="kv"><span class="kv-label req">检疫员(签名)2:</span><span class="kv-value">{{ form.inspector2 }}</span></div>
<div class="kv"><span class="kv-label req">联系电话:</span><span class="kv-value">{{ form.inspectorPhone }}</span></div>
<div class="kv"><span class="kv-label">录入日期:</span><span class="kv-value">{{ form.entryDate }}</span></div>
</div>
</div>
</div>
</div>
</div>
<div v-if="editVisible" class="modal-mask" @click.self="editVisible = false">
<div class="modal-box modal-wide">
<div class="modal-top"><span class="modal-title">编辑田间调查</span><span class="modal-close" @click="editVisible = false">×</span></div>
<div class="modal-body">
<div class="related-files">
<div class="related-title">相关文件</div>
<div class="related-list">
<div class="related-item">国(境)外引进农业种苗检疫审批单</div>
<div class="related-item">国(境)外引进农业种苗入境后疫情监测申请书</div>
<div class="related-item">国(境)外引进农业种苗入境后疫情监测委托书</div>
</div>
</div>
<div class="section">
<div class="section-title">基本信息</div>
<div class="kv-grid">
<div class="kv"><span class="kv-label">受检单位:</span><input class="ipt-cell" v-model="form.unit" /></div>
<div class="kv"><span class="kv-label">联系人:</span><input class="ipt-cell" v-model="form.contact" /></div>
<div class="kv"><span class="kv-label">联系电话:</span><input class="ipt-cell" v-model="form.phone" /></div>
<div class="kv"><span class="kv-label req">调查日期:</span><DatePicker v-model="form.surveyDate" class="ipt-cell dp-cell" /></div>
<div class="kv kv-wide">
<span class="kv-label">调查地点:</span>
<span class="kv-value">
<input class="ipt-cell sm" v-model="form.surveyProvince" />
<input class="ipt-cell sm" v-model="form.surveyCity" />
<input class="ipt-cell sm" v-model="form.surveyDistrict" />
<select class="ipt-cell sm" v-model="form.surveyTownship"><option v-for="t in townshipOptions" :key="t" :value="t">{{ t }}</option></select>
<input class="ipt-cell lg" v-model="form.surveyAddress" placeholder="请输入详细地址" />
</span>
</div>
<div class="kv kv-wide">
<div class="geo-row">
<div class="geo-cell"><span class="kv-label">经度:</span><input class="ipt-cell" v-model="form.longitude" /></div>
<div class="geo-cell"><span class="kv-label">纬度:</span><input class="ipt-cell" v-model="form.latitude" /></div>
<div class="geo-cell"><span class="kv-label">海拔:</span><input class="ipt-cell" v-model="form.altitude" /></div>
</div>
</div>
<div class="kv"><span class="kv-label">植物名称:</span><input class="ipt-cell" v-model="form.plant" /></div>
<div class="kv"><span class="kv-label">产出用途:</span><input class="ipt-cell" v-model="form.useType" /></div>
<div class="kv"><span class="kv-label req">植物类型:</span>
<select class="ipt-cell" v-model="form.plantType">
<option value="">请选择</option>
<option v-for="t in plantTypeOptions" :key="t" :value="t">{{ t }}</option>
</select>
</div>
<div class="kv">
<span class="kv-label">种植期限:</span>
<span class="kv-value">
<DatePicker v-model="form.plantPeriodStart" class="ipt-cell dp-cell" />
<span class="dash-inline">至</span>
<DatePicker v-model="form.plantPeriodEnd" class="ipt-cell dp-cell" />
</span>
</div>
<div class="kv"><span class="kv-label">总种植面积:</span><input class="ipt-cell" v-model="form.totalArea" /></div>
<div class="kv"><span class="kv-label req">植物生育期:</span><input class="ipt-cell" v-model="form.growthPeriod" /></div>
<div class="kv kv-wide">
<span class="kv-label req">调查方法(至少一项):</span>
<span class="kv-value">
<label v-for="m in methodOptions" :key="m" class="chk-line">
<input type="checkbox" :value="m" v-model="editMethods" /> {{ m }}
</label>
<span class="other-line">其他:<input class="ipt-cell other-input" v-model="form.surveyMethodsOther" /></span>
</span>
</div>
<div class="kv kv-wide">
<span class="kv-label">目标有害生物:</span>
<textarea class="ipt-cell area" rows="4" v-model="form.targetPests"></textarea>
</div>
<div class="kv kv-wide">
<span class="kv-label">备注:</span>
<textarea class="ipt-cell area" rows="3" v-model="form.remarks" maxlength="500"></textarea>
</div>
<div class="kv kv-wide">
<span class="kv-label req">本次调查结论:</span>
<select class="ipt-cell" v-model="form.conclusion">
<option value="">请选择</option>
<option v-for="c in conclusionOptions" :key="c" :value="c">{{ c }}</option>
</select>
<button v-if="form.conclusion === '现场确诊发现应检有害生物'" type="button" class="btn btn-notice" @click="openNotice">疫情处理通知书</button>
</div>
<div v-if="form.conclusion === '现场确诊发现应检有害生物'" class="pest-block">
<div class="pest-hint">请输入发现的应检有害生物名称及发生情况:</div>
<textarea class="ipt-cell area" rows="4" placeholder="请输入发现的应检有害生物名称及发生情况" v-model="form.pestDetail"></textarea>
</div>
</div>
</div>
<div v-if="form.conclusion === '现场不能确诊,需要采样进行实验室检验'" class="section">
<div class="sample-block">
<div class="sample-title">抽样记录表</div>
<div class="sample-form">
<div class="sample-row"><span class="sample-label">抽样单位:</span><input class="sample-ipt" v-model="form.sampleUnit" /></div>
<div class="sample-row"><span class="sample-label req">联系电话:</span><input class="sample-ipt" v-model="form.samplePhone" /></div>
<div class="sample-row"><span class="sample-label">联系人:</span><input class="sample-ipt" v-model="form.sampleContact" /></div>
<div class="sample-row sample-col">
<span class="sample-label req">为害症状描述:</span>
<textarea class="sample-area" rows="4" maxlength="500" placeholder="描述为害症状及其他补充信息,输入500字以内" v-model="form.symptomDesc"></textarea>
<div class="char-count">{{ (form.symptomDesc || '').length }}/500</div>
</div>
<div class="sample-row sample-col">
<span class="sample-label req">发生与处理情况及原因:</span>
<textarea class="sample-area" rows="4" maxlength="500" placeholder="发生与防控情况及原因,输入500字以内" v-model="form.handlingReason"></textarea>
<div class="char-count">{{ (form.handlingReason || '').length }}/500</div>
</div>
<div class="sample-row sample-col">
<span class="sample-label">抽样方法、部位和抽样比例:</span>
<textarea class="sample-area" rows="4" maxlength="500" placeholder="抽样方法、部位和抽样比例,输入500字以内" v-model="form.sampleMethod"></textarea>
<div class="char-count">{{ (form.sampleMethod || '').length }}/500</div>
</div>
<div class="sample-subtitle">实验室调查结果</div>
<div class="sample-row sample-col">
<span class="sample-label">检验结果:</span>
<textarea class="sample-area" rows="4" placeholder="实验室尚未出检验结果" v-model="form.labResult"></textarea>
</div>
</div>
</div>
</div>
<div class="section">
<div class="section-title">检疫机构信息</div>
<div class="kv-grid">
<div class="kv"><span class="kv-label">检疫单位:</span><input class="ipt-cell" v-model="form.testOrg" /></div>
<div class="kv"><span class="kv-label">记录员:</span><input class="ipt-cell" v-model="form.recorder" /></div>
<div class="kv"><span class="kv-label req">当事人(签名):</span><input class="ipt-cell" v-model="form.partySignature" /></div>
<div class="kv"><span class="kv-label req">检疫员(签名)1:</span><input class="ipt-cell" v-model="form.inspector1" /></div>
<div class="kv"><span class="kv-label req">检疫员(签名)2:</span><input class="ipt-cell" v-model="form.inspector2" /></div>
<div class="kv"><span class="kv-label req">联系电话:</span><input class="ipt-cell" v-model="form.inspectorPhone" /></div>
<div class="kv"><span class="kv-label">录入日期:</span><DatePicker v-model="form.entryDate" class="ipt-cell dp-cell" /></div>
</div>
</div>
<div class="modal-actions">
<button class="btn btn-default" @click="editVisible = false">取消</button>
<button class="btn" @click="onConfirmEdit">保存</button>
</div>
</div>
</div>
</div>
<div v-if="addVisible" class="modal-mask" @click.self="addVisible = false">
<div class="modal-box modal-wide">
<div class="modal-top"><span class="modal-title">新增调查</span><span class="modal-close" @click="addVisible = false">×</span></div>
<div class="modal-body">
<div class="related-files">
<div class="related-title">相关文件</div>
<div class="related-list">
<div class="related-item">国(境)外引进农业种苗检疫审批单</div>
<div class="related-item">国(境)外引进农业种苗入境后疫情监测申请书</div>
<div class="related-item">国(境)外引进农业种苗入境后疫情监测委托书</div>
</div>
</div>
<div class="section">
<div class="section-title">基本信息</div>
<div class="kv-grid">
<div class="kv"><span class="kv-label">受检单位:</span><input class="ipt-cell" v-model="addForm.unit" /></div>
<div class="kv"><span class="kv-label">联系人:</span><input class="ipt-cell" v-model="addForm.contact" /></div>
<div class="kv"><span class="kv-label">联系电话:</span><input class="ipt-cell" v-model="addForm.phone" /></div>
<div class="kv"><span class="kv-label req">调查日期:</span><DatePicker v-model="addForm.surveyDate" class="ipt-cell dp-cell" /></div>
<div class="kv kv-wide">
<span class="kv-label">调查地点:</span>
<span class="kv-value">
<input class="ipt-cell sm" v-model="addForm.surveyProvince" />
<input class="ipt-cell sm" v-model="addForm.surveyCity" />
<input class="ipt-cell sm" v-model="addForm.surveyDistrict" />
<select class="ipt-cell sm" v-model="addForm.surveyTownship"><option v-for="t in townshipOptions" :key="t" :value="t">{{ t }}</option></select>
<input class="ipt-cell lg" v-model="addForm.surveyAddress" placeholder="请输入详细地址" />
</span>
</div>
<div class="kv kv-wide">
<div class="geo-row">
<div class="geo-cell"><span class="kv-label">经度:</span><input class="ipt-cell" v-model="addForm.longitude" /></div>
<div class="geo-cell"><span class="kv-label">纬度:</span><input class="ipt-cell" v-model="addForm.latitude" /></div>
<div class="geo-cell"><span class="kv-label">海拔:</span><input class="ipt-cell" v-model="addForm.altitude" /></div>
</div>
</div>
<div class="kv"><span class="kv-label">植物名称:</span><input class="ipt-cell" v-model="addForm.plant" /></div>
<div class="kv"><span class="kv-label">产出用途:</span><input class="ipt-cell" v-model="addForm.useType" /></div>
<div class="kv"><span class="kv-label req">植物类型:</span>
<select class="ipt-cell" v-model="addForm.plantType">
<option value="">请选择</option>
<option v-for="t in plantTypeOptions" :key="t" :value="t">{{ t }}</option>
</select>
</div>
<div class="kv">
<span class="kv-label">种植期限:</span>
<span class="kv-value">
<DatePicker v-model="addForm.plantPeriodStart" class="ipt-cell dp-cell" />
<span class="dash-inline">至</span>
<DatePicker v-model="addForm.plantPeriodEnd" class="ipt-cell dp-cell" />
</span>
</div>
<div class="kv"><span class="kv-label">总种植面积:</span><input class="ipt-cell" v-model="addForm.totalArea" /></div>
<div class="kv"><span class="kv-label req">植物生育期:</span><input class="ipt-cell" v-model="addForm.growthPeriod" /></div>
<div class="kv kv-wide">
<span class="kv-label req">调查方法(至少一项):</span>
<span class="kv-value">
<label v-for="m in methodOptions" :key="m" class="chk-line">
<input type="checkbox" :value="m" v-model="addMethods" /> {{ m }}
</label>
<span class="other-line">其他:<input class="ipt-cell other-input" v-model="addForm.surveyMethodsOther" /></span>
</span>
</div>
<div class="kv kv-wide">
<span class="kv-label">目标有害生物:</span>
<textarea class="ipt-cell area" rows="4" v-model="addForm.targetPests"></textarea>
</div>
<div class="kv kv-wide">
<span class="kv-label">备注:</span>
<textarea class="ipt-cell area" rows="3" v-model="addForm.remarks" maxlength="500"></textarea>
</div>
<div class="kv kv-wide">
<span class="kv-label req">本次调查结论:</span>
<select class="ipt-cell" v-model="addForm.conclusion">
<option value="">请选择</option>
<option v-for="c in conclusionOptions" :key="c" :value="c">{{ c }}</option>
</select>
<button v-if="addForm.conclusion === '现场确诊发现应检有害生物'" type="button" class="btn btn-notice" @click="openNotice">疫情处理通知书</button>
</div>
<div v-if="addForm.conclusion === '现场确诊发现应检有害生物'" class="pest-block">
<div class="pest-hint">请输入发现的应检有害生物名称及发生情况:</div>
<textarea class="ipt-cell area" rows="4" placeholder="请输入发现的应检有害生物名称及发生情况" v-model="addForm.pestDetail"></textarea>
</div>
</div>
</div>
<div v-if="addForm.conclusion === '现场不能确诊,需要采样进行实验室检验'" class="section">
<div class="sample-block">
<div class="sample-title">抽样记录表</div>
<div class="sample-form">
<div class="sample-row"><span class="sample-label">抽样单位:</span><input class="sample-ipt" v-model="addForm.sampleUnit" /></div>
<div class="sample-row"><span class="sample-label req">联系电话:</span><input class="sample-ipt" v-model="addForm.samplePhone" /></div>
<div class="sample-row"><span class="sample-label">联系人:</span><input class="sample-ipt" v-model="addForm.sampleContact" /></div>
<div class="sample-row sample-col">
<span class="sample-label req">为害症状描述:</span>
<textarea class="sample-area" rows="4" maxlength="500" placeholder="描述为害症状及其他补充信息,输入500字以内" v-model="addForm.symptomDesc"></textarea>
<div class="char-count">{{ (addForm.symptomDesc || '').length }}/500</div>
</div>
<div class="sample-row sample-col">
<span class="sample-label req">发生与处理情况及原因:</span>
<textarea class="sample-area" rows="4" maxlength="500" placeholder="发生与防控情况及原因,输入500字以内" v-model="addForm.handlingReason"></textarea>
<div class="char-count">{{ (addForm.handlingReason || '').length }}/500</div>
</div>
<div class="sample-row sample-col">
<span class="sample-label">抽样方法、部位和抽样比例:</span>
<textarea class="sample-area" rows="4" maxlength="500" placeholder="抽样方法、部位和抽样比例,输入500字以内" v-model="addForm.sampleMethod"></textarea>
<div class="char-count">{{ (addForm.sampleMethod || '').length }}/500</div>
</div>
<div class="sample-subtitle">实验室调查结果</div>
<div class="sample-row sample-col">
<span class="sample-label">检验结果:</span>
<textarea class="sample-area" rows="4" placeholder="实验室尚未出检验结果" v-model="addForm.labResult"></textarea>
</div>
</div>
</div>
</div>
<div class="section">
<div class="section-title">检疫机构信息</div>
<div class="kv-grid">
<div class="kv"><span class="kv-label">检疫单位:</span><input class="ipt-cell" v-model="addForm.testOrg" /></div>
<div class="kv"><span class="kv-label">记录员:</span><input class="ipt-cell" v-model="addForm.recorder" /></div>
<div class="kv"><span class="kv-label req">当事人(签名):</span><input class="ipt-cell" v-model="addForm.partySignature" /></div>
<div class="kv"><span class="kv-label req">检疫员(签名)1:</span><input class="ipt-cell" v-model="addForm.inspector1" /></div>
<div class="kv"><span class="kv-label req">检疫员(签名)2:</span><input class="ipt-cell" v-model="addForm.inspector2" /></div>
<div class="kv"><span class="kv-label req">联系电话:</span><input class="ipt-cell" v-model="addForm.inspectorPhone" /></div>
<div class="kv"><span class="kv-label">录入日期:</span><DatePicker v-model="addForm.entryDate" class="ipt-cell dp-cell" /></div>
</div>
</div>
<div class="modal-actions">
<button class="btn btn-default" @click="addVisible = false">取消</button>
<button class="btn" @click="onConfirmAdd">保存</button>
</div>
</div>
</div>
</div>
<div v-if="delVisible" class="modal-mask" @click.self="delVisible = false">
<div class="modal-box modal-small">
<div class="modal-top"><span class="modal-title">删除确认</span><span class="modal-close" @click="delVisible = false">×</span></div>
<div class="modal-body">
<p class="del-tip">确定要删除检疫单证号为 <b>{{ delRow?.cert }}</b>,植物名称为 <b>{{ delRow?.plant }}</b> 的记录吗?</p>
<div class="modal-actions">
<button class="btn btn-default" @click="delVisible = false">取消</button>
<button class="btn btn-danger" @click="onConfirmDelete">删除</button>
</div>
</div>
</div>
</div>
<div v-if="noticeVisible" class="modal-mask" @click.self="noticeVisible = false">
<div class="modal-box modal-wide">
<div class="notice-top">
<button class="back-btn" @click="onSaveNotice">← 返回</button>
<div class="notice-title">疫情处理通知书</div>
</div>
<div class="modal-body notice-body">
<div class="kv-grid">
<div class="kv"><span class="kv-label">编号:</span><input class="ipt-cell" :value="noticeId" readonly /></div>
<div class="kv"><span class="kv-label">受检单位:</span><input class="ipt-cell" :value="noticeSource.unit" readonly /></div>
<div class="kv"><span class="kv-label">联系人:</span><input class="ipt-cell" :value="noticeSource.contact" readonly /></div>
<div class="kv"><span class="kv-label">联系电话:</span><input class="ipt-cell" :value="noticeSource.phone" readonly /></div>
<div class="kv"><span class="kv-label">产出用途:</span><input class="ipt-cell" :value="noticeSource.useType" readonly /></div>
<div class="kv"><span class="kv-label">数量:</span><input class="ipt-cell" :value="noticeSource.totalArea" readonly /></div>
<div class="kv kv-wide">
<span class="kv-label">种植(存放)地点:</span>
<span class="kv-value">
<input class="ipt-cell sm" :value="noticeSource.surveyProvince" readonly />
<input class="ipt-cell sm" :value="noticeSource.surveyCity" readonly />
<input class="ipt-cell sm" :value="noticeSource.surveyDistrict" readonly />
<input class="ipt-cell sm" :value="noticeSource.surveyTownship" readonly />
<input class="ipt-cell lg" :value="noticeSource.surveyAddress" readonly />
</span>
</div>
<div class="kv kv-wide">
<span class="kv-label">检验结果:<span v-if="isConfirmedHarmful" class="req">*</span></span>
<textarea class="ipt-cell area block" rows="3" maxlength="500" placeholder="输入500字以内" v-model="noticeForm.testResult"></textarea>
</div>
<div class="kv kv-wide">
<span class="kv-label req">处理意见:</span>
<textarea class="ipt-cell area block" rows="3" maxlength="500" placeholder="输入500字以内" v-model="noticeForm.handleOpinion"></textarea>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed, watch } from "vue";
import { ElMessage } from "element-plus";
import DatePicker from "@/components/DatePicker.vue";
import { fieldSurveyData } from "@/data/fieldSurvey.js";
import { syncFromSurvey, removeBySurveyId, list as expList } from "@/store/experimentTest.js";
import { syncFromSurvey as syncHandle, removeBySurveyId as removeHandleBySurveyId } from "@/store/pestHandle.js";
// 初始化:扫描已有田间调查记录,将"现场不能确诊,需要采样进行实验室检验"的同步到实验检验列表
fieldSurveyData.forEach((r) => { if (r.conclusion === "现场不能确诊,需要采样进行实验室检验") syncFromSurvey(r); });
const dataList = ref([...fieldSurveyData]);
const defaultFilters = () => ({ surveyStart: "", surveyEnd: "", cert: "", plant: "", variety: "", testOrg: "" });
const filters = ref(defaultFilters());
const appliedFilters = ref(defaultFilters());
const currentPage = ref(1);
const pageSize = 10;
const jumpInput = ref(1);
const viewVisible = ref(false);
const editVisible = ref(false);
const addVisible = ref(false);
const delVisible = ref(false);
const delRow = ref(null);
const noticeVisible = ref(false);
const form = ref({});
const editMethods = ref([]);
const addMethods = ref([]);
const methodOptions = ["5点取样", "对角线", "9点取样", "平行线", "棋盘式", "Z字型", "随机抽样"];
const conclusionOptions = ["现场确诊未发现应检有害生物", "现场确诊发现应检有害生物", "现场不能确诊,需要采样进行实验室检验"];
const plantTypeOptions = ["粮谷类种子", "经作类种子", "瓜果类种苗", "粮谷类产品", "经作类产品", "瓜果类产品"];
const townshipOptions = ["", "甘州镇", "梁家墩镇", "上秦镇", "三闸镇", "党寨镇", "新墩镇", "沙井镇", "大满镇", "碱滩镇", "小满镇", "龙渠乡", "安阳乡", "花寨乡"];
const defaultAddForm = () => ({
cert: "", unit: "", contact: "", phone: "",
plant: "", variety: "", origin: "", approveQty: "", importQty: "", qty: "150克",
surveyDate: "", testOrg: "甘州区农业技术推广中心",
surveyProvince: "甘肃省", surveyCity: "张掖市", surveyDistrict: "甘州区", surveyTownship: "", surveyAddress: "",
longitude: "", latitude: "", altitude: "",
useType: "种子", plantType: "", plantPeriodStart: "", plantPeriodEnd: "",
totalArea: "", growthPeriod: "",
surveyMethods: "", surveyMethodsOther: "",
targetPests: "", remarks: "", conclusion: "",
pestDetail: "", noticeTestResult: "", noticeHandleOpinion: "",
sampleUnit: "甘州区农业技术推广中心", sampleContact: "甘州区系统管理员", samplePhone: "0936-8212469", symptomDesc: "", handlingReason: "", sampleMethod: "", labResult: "",
recorder: "甘州区系统管理员",
partySignature: "", inspector1: "", inspector2: "", inspectorPhone: "", entryDate: ""
});
const addForm = ref(defaultAddForm());
const filteredData = computed(() => {
const f = appliedFilters.value;
return dataList.value.filter((r) => {
if (f.cert && !r.cert.toLowerCase().includes(f.cert.toLowerCase())) return false;
if (f.plant && !r.plant.toLowerCase().includes(f.plant.toLowerCase())) return false;
if (f.variety && !r.variety.toLowerCase().includes(f.variety.toLowerCase())) return false;
if (f.testOrg && !r.testOrg.toLowerCase().includes(f.testOrg.toLowerCase())) return false;
if (f.surveyStart && r.surveyDate < f.surveyStart) return false;
if (f.surveyEnd && r.surveyDate > f.surveyEnd) return false;
return true;
});
});
const totalPages = computed(() => Math.max(1, Math.ceil(filteredData.value.length / pageSize)));
const pageNumbers = computed(() => {
const t = totalPages.value;
const c = currentPage.value;
if (t <= 7) return Array.from({ length: t }, (_, i) => i + 1);
if (c <= 4) return [1, 2, 3, 4, 5, "...", t];
if (c >= t - 3) return [1, "...", t - 4, t - 3, t - 2, t - 1, t];
return [1, "...", c - 1, c, c + 1, "...", t];
});
const pagedData = computed(() => {
const s = (currentPage.value - 1) * pageSize;
return filteredData.value.slice(s, s + pageSize);
});
watch(filteredData, () => { if (currentPage.value > totalPages.value) currentPage.value = 1; });
const onQuery = () => {
appliedFilters.value = JSON.parse(JSON.stringify(filters.value));
currentPage.value = 1;
ElMessage.success("查询完成,共 " + filteredData.value.length + " 条");
};
const onReset = () => {
filters.value = defaultFilters();
appliedFilters.value = defaultFilters();
currentPage.value = 1;
};
const goPage = (p) => { if (p < 1 || p > totalPages.value) return; currentPage.value = p; };
const onJump = () => { const p = parseInt(jumpInput.value); if (!isNaN(p)) goPage(p); else goPage(1); };
const onView = (row) => { form.value = { ...row }; viewVisible.value = true; };
const onEdit = (row) => {
form.value = { ...row };
form.value.sampleUnit = row.sampleUnit || row.testOrg || "甘州区农业技术推广中心";
form.value.sampleContact = row.sampleContact || row.recorder || "甘州区系统管理员";
form.value.samplePhone = row.samplePhone || row.inspectorPhone || "0936-8212469";
editMethods.value = (row.surveyMethods || "").split(",").filter(Boolean);
editVisible.value = true;
};
const onAdd = () => {
addForm.value = defaultAddForm();
addMethods.value = [];
addVisible.value = true;
};
const onDelete = (row) => { delRow.value = row; delVisible.value = true; };
const onConfirmEdit = () => {
form.value.surveyMethods = editMethods.value.join(",");
const i = dataList.value.findIndex((r) => r.id === form.value.id);
if (i >= 0) dataList.value[i] = { ...form.value };
// 联动:同步到实验检验 / 除害处置
syncFromSurvey(form.value);
syncHandle(form.value);
editVisible.value = false;
ElMessage.success("保存成功");
};
const onConfirmAdd = () => {
addForm.value.surveyMethods = addMethods.value.join(",");
const next = (dataList.value.length ? Math.max(...dataList.value.map((r) => r.id)) : 0) + 1;
const newRec = { id: next, ...addForm.value };
dataList.value = [...dataList.value, newRec];
// 联动:同步到实验检验 / 除害处置
syncFromSurvey(newRec);
syncHandle(newRec);
addVisible.value = false;
ElMessage.success("新增成功");
};
const onConfirmDelete = () => {
const deletedId = delRow.value.id;
dataList.value = dataList.value.filter((r) => r.id !== deletedId);
// 联动:从实验检验列表中移除关联记录
removeBySurveyId(deletedId);
// 联动:从除害处理列表中移除关联记录
removeHandleBySurveyId(deletedId);
delVisible.value = false;
ElMessage.success("删除成功");
};
// 疫情处理通知书
const noticeId = ref("");
const noticeSource = ref({});
const noticeForm = ref({ testResult: "", handleOpinion: "" });
const getCurrentForm = () => (editVisible.value ? form.value : addForm.value);
const openNotice = () => {
const src = getCurrentForm();
noticeSource.value = {
unit: src.unit, contact: src.contact, phone: src.phone, useType: src.useType, totalArea: src.totalArea,
surveyProvince: src.surveyProvince, surveyCity: src.surveyCity, surveyDistrict: src.surveyDistrict,
surveyTownship: src.surveyTownship, surveyAddress: src.surveyAddress
};
noticeForm.value = { sourceConclusion: src.conclusion || "", testResult: src.noticeTestResult || "", handleOpinion: src.noticeHandleOpinion || "" };
const stamp = new Date().toISOString().slice(0, 10).replace(/-/g, "");
noticeId.value = "DC" + stamp + (src.id || "").toString().padStart(4, "0");
noticeVisible.value = true;
};
// 调查结论是否为"现场确诊发现应检有害生物",用于决定检验结果是否必填
const isConfirmedHarmful = computed(() => {
return (noticeForm.value.sourceConclusion || "").includes("现场确诊发现应检有害生物");
});
const onSaveNotice = () => {
// 现场确诊发现应检有害生物时,检验结果必填
if (isConfirmedHarmful.value && !noticeForm.value.testResult.trim()) {
ElMessage.error("检验结果不能为空");
return;
}
const src = getCurrentForm();
src.noticeTestResult = noticeForm.value.testResult;
src.noticeHandleOpinion = noticeForm.value.handleOpinion;
noticeVisible.value = false;
ElMessage.success("疫情处理通知书已保存");
};
</script>
<style scoped>
.page-list { padding: 16px 20px; }
.filter-box { background: #fff; border: 1px solid #ebeef5; border-radius: 3px; padding: 18px 20px; margin-bottom: 12px; }
.filter-row { display: flex; align-items: center; flex-wrap: wrap; gap: 14px 24px; margin-bottom: 16px; }
.filter-row:last-child { margin-bottom: 0; }
.filter-item { display: flex; align-items: center; gap: 6px; flex-shrink: 0; min-width: 0; }
.filter-item .label { color: #555; font-size: 14px; white-space: nowrap; flex-shrink: 0; min-width: 84px; }
.filter-add { margin-left: auto; }
.dash { color: #999; }
.dp-160 { width: 160px; }
.ipt { width: 160px; height: 32px; padding: 0 8px; border: 1px solid #dcdfe6; border-radius: 3px; font-size: 14px; }
.ipt:focus { border-color: #d71e1e; outline: none; }
.btn { background: #d71e1e; color: #fff; border: 1px solid #d71e1e; height: 32px; padding: 0 18px; border-radius: 3px; font-size: 14px; cursor: pointer; }
.btn:hover { background: #b81818; border-color: #b81818; }
.btn-default { background: #fff; color: #555; border-color: #dcdfe6; }
.btn-default:hover { background: #fafafa; color: #d71e1e; border-color: #d71e1e; }
.btn-add { background: #d71e1e; color: #fff; border: 1px solid #d71e1e; }
.btn-danger { background: #fff; color: #d71e1e; border-color: #d71e1e; }
.btn-danger:hover { background: #d71e1e; color: #fff; }
.tbl-wrap { background: #fff; border: 1px solid #ebeef5; border-radius: 3px; overflow: hidden; }
.simple-table { width: 100%; border-collapse: collapse; table-layout: fixed; font-size: 14px; }
.simple-table th, .simple-table td { padding: 14px 6px; text-align: center; vertical-align: middle; border-bottom: 1px solid #ebeef5; }
.simple-table th { background: #fafafa; font-size: 14px; font-weight: 600; color: #333; }
.simple-table tbody tr:hover { background: #fafafa; }
.col-unit { text-align: left; padding-left: 12px; }
.col-action { white-space: nowrap; }
.op-link { color: #333; font-size: 14px; cursor: pointer; margin: 0 4px; }
.op-link:hover { color: #d71e1e; }
.empty { padding: 40px 0; color: #999; }
.pagination { display: flex; align-items: center; justify-content: flex-end; gap: 6px; padding: 14px 16px; background: #fff; border: 1px solid #ebeef5; border-top: 0; border-radius: 0 0 3px 3px; font-size: 14px; }
.total { color: #555; margin-right: 12px; }
.page-btn { min-width: 30px; height: 30px; padding: 0 8px; background: #fff; border: 1px solid #dcdfe6; color: #555; border-radius: 3px; cursor: pointer; font-size: 14px; }
.page-btn:hover:not(:disabled) { color: #d71e1e; border-color: #d71e1e; }
.page-btn.active { background: #d71e1e; color: #fff; border-color: #d71e1e; }
.page-btn:disabled { opacity: 0.5; cursor: not-allowed; }
.jump { margin-left: 12px; color: #555; }
.jump-input { width: 50px; height: 30px; text-align: center; border: 1px solid #dcdfe6; border-radius: 3px; margin: 0 4px; font-size: 14px; }
.modal-mask { position: fixed; inset: 0; background: rgba(0, 0, 0, 0.45); display: flex; align-items: center; justify-content: center; z-index: 1000; }
.modal-box { background: #fff; border-radius: 4px; width: 900px; max-width: 95vw; max-height: 90vh; display: flex; flex-direction: column; box-shadow: 0 8px 30px rgba(0, 0, 0, 0.2); overflow: hidden; }
.modal-wide { width: 1100px; }
.modal-small { width: 420px; }
.modal-top { display: flex; align-items: center; justify-content: space-between; padding: 12px 18px; background: #d71e1e; color: #fff; }
.modal-title { font-size: 16px; font-weight: 600; }
.modal-close { width: 22px; height: 22px; line-height: 18px; text-align: center; border: 1px solid #fff; border-radius: 3px; cursor: pointer; font-size: 16px; }
.modal-body { padding: 18px 22px; overflow-y: auto; }
.modal-actions { display: flex; justify-content: flex-end; gap: 10px; margin-top: 18px; padding-top: 14px; border-top: 1px solid #ebeef5; }
.related-files { background: #f6f6f6; border: 1px solid #e6e6e6; border-radius: 3px; padding: 10px 14px; margin-bottom: 16px; }
.related-title { font-size: 14px; font-weight: 600; color: #333; margin-bottom: 6px; }
.related-list { display: flex; flex-direction: column; gap: 4px; }
.related-item { font-size: 13px; color: #555; padding-left: 4px; }
.section { margin-bottom: 22px; }
.section:last-child { margin-bottom: 0; }
.section-title { font-size: 15px; font-weight: 600; color: #d71e1e; margin: 0 0 12px; padding-left: 10px; border-left: 3px solid #d71e1e; line-height: 1; }
.kv-grid { display: grid; grid-template-columns: 1fr 1fr; column-gap: 24px; row-gap: 14px; }
.kv { display: flex; align-items: center; gap: 6px; min-height: 32px; }
.kv-wide { grid-column: 1 / -1; }
.kv-label { width: 130px; text-align: right; color: #555; font-size: 14px; flex-shrink: 0; white-space: nowrap; }
.kv-label.req::before { content: "*"; color: #d71e1e; margin-right: 2px; }
.kv-value { flex: 1; display: flex; align-items: center; gap: 6px; flex-wrap: wrap; min-height: 32px; }
.kv-value.area { white-space: pre-wrap; line-height: 1.7; word-break: break-all; }
.addr-cell { display: inline-flex; align-items: center; height: 30px; padding: 0 10px; border: 1px solid #dcdfe6; border-radius: 3px; background: #fafafa; font-size: 14px; color: #333; }
.addr-input { flex: 1; min-width: 200px; }
.dash-inline { color: #999; }
.method-chk { display: inline-flex; align-items: center; margin-right: 14px; color: #555; font-size: 14px; }
.method-chk.on::before { content: "✓ "; color: #d71e1e; font-weight: 600; }
.method-chk.on { color: #333; }
.method-other { display: inline-flex; align-items: center; gap: 4px; color: #555; font-size: 14px; }
.ipt-cell { flex: 1; height: 32px; padding: 0 10px; border: 1px solid #dcdfe6; border-radius: 3px; font-size: 14px; min-width: 0; }
.ipt-cell.sm { width: 130px; flex: 0 0 auto; }
.ipt-cell.lg { flex: 1; min-width: 220px; }
.ipt-cell.dp-cell { padding: 0; }
.ipt-cell.area { height: auto; min-height: 70px; line-height: 1.6; padding: 8px 10px; resize: vertical; }
.ipt-cell:focus { border-color: #d71e1e; outline: none; }
.chk-line { display: inline-flex; align-items: center; margin-right: 14px; font-size: 14px; color: #555; }
.chk-line input { margin-right: 4px; }
.other-line { display: inline-flex; align-items: center; gap: 6px; font-size: 14px; color: #555; }
.other-input { width: 160px; }
.pest-block { display: flex; flex-direction: column; gap: 8px; grid-column: 1 / -1; }
.sample-block { background: #fafafa; border: 1px solid #ebeef5; border-radius: 4px; padding: 18px 20px; }
.sample-title { text-align: center; font-size: 16px; font-weight: 600; color: #333; margin-bottom: 16px; padding: 6px 0; border-top: 1px dashed #c0c4cc; border-bottom: 1px dashed #c0c4cc; }
.sample-form { display: flex; flex-direction: column; gap: 14px; }
.sample-row { display: flex; align-items: center; gap: 8px; }
.sample-row.sample-col { flex-direction: row; align-items: flex-start; flex-wrap: wrap; }
.sample-label { width: 160px; text-align: right; color: #555; font-size: 14px; flex-shrink: 0; white-space: nowrap; line-height: 32px; }
.sample-label.req::before { content: "*"; color: #d71e1e; margin-right: 2px; }
.sample-ipt { flex: 1; height: 32px; padding: 0 10px; border: 1px solid #dcdfe6; border-radius: 3px; font-size: 14px; min-width: 0; }
.sample-area { flex: 1; min-width: 280px; min-height: 80px; padding: 8px 10px; border: 1px solid #dcdfe6; border-radius: 3px; font-size: 14px; line-height: 1.6; resize: vertical; }
.sample-area:focus, .sample-ipt:focus { border-color: #d71e1e; outline: none; }
.sample-subtitle { text-align: center; font-size: 14px; font-weight: 600; color: #333; padding: 8px 0; border-top: 1px dashed #c0c4cc; border-bottom: 1px dashed #c0c4cc; margin: 4px 0; }
.pest-hint { font-size: 14px; color: #333; line-height: 1.6; }
.pest-readonly {
background: #fff;
border: 1px solid #ebeef5;
border-radius: 3px;
padding: 8px 12px;
min-height: 60px;
white-space: pre-wrap;
word-break: break-all;
line-height: 1.6;
color: #333;
font-size: 14px;
}
.pest-block .ipt-cell.area { width: 100%; }
.btn-notice { margin-left: 8px; background: #d71e1e; color: #fff; border: 1px solid #d71e1e; height: 32px; padding: 0 14px; border-radius: 3px; font-size: 14px; cursor: pointer; white-space: nowrap; }
.btn-notice:hover { background: #b81818; border-color: #b81818; }
.notice-top { display: flex; align-items: center; padding: 12px 18px; background: #d71e1e; color: #fff; position: relative; }
.notice-top .notice-title { position: absolute; left: 50%; transform: translateX(-50%); }
.notice-top .back-btn { background: #fff; color: #d71e1e; border: 1px solid #fff; height: 28px; padding: 0 12px; border-radius: 3px; font-size: 14px; cursor: pointer; }
.notice-top .back-btn:hover { background: #fcecec; }
.notice-title { flex: 1; text-align: center; font-size: 18px; font-weight: 600; }
.notice-body { padding-top: 18px; }
.geo-row { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 16px; align-items: center; width: 100%; }
.geo-cell { display: flex; align-items: center; gap: 6px; min-width: 0; }
.geo-cell .kv-label { width: 60px; }
.geo-cell .kv-value { min-height: 32px; }
.del-tip { font-size: 14px; color: #555; line-height: 1.8; padding: 8px 0 16px; }
.del-tip b { color: #d71e1e; }
</style>
<template>
<div class="device-overview">
<!-- 顶部:设备类型 Tab 筛选 -->
<div class="type-tabs">
<div
v-for="t in typeTabs"
:key="t.key"
class="type-tab"
:class="{ active: activeType === t.key }"
@click="activeType = t.key"
>
<el-icon class="t-icon"><component :is="t.icon" /></el-icon>
<span class="t-name">{{ t.name }}</span>
<span class="t-count">({{ t.count }})</span>
</div>
</div>
<!-- 中部:状态统计卡片 -->
<div class="status-row">
<div
v-for="s in statusCards"
:key="s.key"
class="status-card"
:class="['status-' + s.key, { active: activeStatus === s.key }]"
@click="activeStatus = s.key"
>
<div class="sc-icon">
<el-icon><component :is="s.icon" /></el-icon>
</div>
<div class="sc-info">
<div class="sc-label">{{ s.label }} <el-icon class="q"><QuestionFilled /></el-icon></div>
<div class="sc-value">{{ s.value }}</div>
<div class="sc-bar"><div class="sc-bar-fill" :style="{ width: s.barPct + '%', background: s.barColor }"></div></div>
</div>
</div>
</div>
<!-- 下部:设备卡片滚动区(显示所有设备) -->
<div class="device-scroll">
<div class="device-grid">
<div v-for="d in filteredDevices" :key="d.id" class="device-card">
<div class="dc-cover">
<img :src="d.cover" :alt="d.name" />
</div>
<div class="dc-body">
<div class="dc-title">{{ d.name }}</div>
<div class="dc-line">
<span class="dc-label">监测点:</span>
<span class="dc-val">{{ d.station }}</span>
<el-icon class="dc-loc"><LocationFilled /></el-icon>
</div>
<div class="dc-line">
<span class="dc-label">最新数据:</span>
<span class="dc-val">{{ d.lastTime }}</span>
<a class="dc-link" @click="goDevice(d)">查看数据</a>
</div>
<div class="dc-line">
<span class="dc-label">建设年份:</span>
<span class="dc-val">{{ d.year }}</span>
</div>
</div>
</div>
<div v-if="!filteredDevices.length" class="empty-block">暂无设备数据</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from "vue";
const activeType = ref("all");
const activeStatus = ref("all");
// 类型 Tab(语音图像 -> 苗情图像;删除 高空灯 / 病害监测预警仪;数量改为 16/2/4/3/4/3)
const typeTabs = [
{ key: "all", name: "全部", icon: "Grid", count: 16 },
{ key: "climate", name: "小气候仪", icon: "Sunny", count: 2 },
{ key: "realtime", name: "远程实时监控", icon: "VideoCamera", count: 4 },
{ key: "pestlamp", name: "虫情测报灯", icon: "MagicStick", count: 3 },
{ key: "image", name: "苗情图像", icon: "Picture", count: 4 },
{ key: "sextrap", name: "性诱监测仪", icon: "Aim", count: 3 }
];
const statusCards = [
{ key: "all", label: "全部", icon: "Grid", value: 16, barPct: 100, barColor: "#d71e1e" },
{ key: "online", label: "在线", icon: "CircleCheck", value: 16, barPct: 100, barColor: "#67c23a" },
{ key: "offline", label: "离线", icon: "CircleClose", value: 0, barPct: 0, barColor: "#909399" },
{ key: "abnormal", label: "异常", icon: "WarningFilled", value: 0, barPct: 0, barColor: "#e6a23c" }
];
// 设备占位图片(SVG dataURL)
const coverSvg = (label, color) => {
const svg = "<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 160 120'>" +
"<rect width='160' height='120' fill='" + color + "'/>" +
"<rect x='50' y='20' width='60' height='80' fill='#fff' stroke='#333' stroke-width='2'/>" +
"<circle cx='80' cy='60' r='18' fill='" + color + "' stroke='#333' stroke-width='2'/>" +
"<circle cx='80' cy='60' r='8' fill='#fff'/>" +
"<rect x='70' y='95' width='20' height='10' fill='#333'/>" +
"<text x='80' y='115' text-anchor='middle' font-size='10' fill='#fff' font-family='Microsoft YaHei'>" + label + "</text>" +
"</svg>";
return "data:image/svg+xml;utf8," + encodeURIComponent(svg);
};
// 16 台设备,删去前缀"大坪园区",监测点改为 1号/2号隔离大棚
// 性诱监测仪:1、2 号 -> 1号隔离大棚;3 号 -> 2号隔离大棚
const devices = [
// 性诱监测仪(3)
{ id: 1, type: "sextrap", name: "1号性诱监测仪", station: "1号隔离大棚", lastTime: "2026-09-09 11:24:11", year: "2025", cover: coverSvg("性诱监测仪", "#5b8def") },
{ id: 2, type: "sextrap", name: "2号性诱监测仪", station: "1号隔离大棚", lastTime: "2026-09-09 11:24:13", year: "2025", cover: coverSvg("性诱监测仪", "#5b8def") },
{ id: 3, type: "sextrap", name: "3号性诱监测仪", station: "2号隔离大棚", lastTime: "2026-09-09 11:25:02", year: "2025", cover: coverSvg("性诱监测仪", "#5b8def") },
// 苗情图像(4):1号大棚 → 1、2号;2号大棚 → 3、4号
{ id: 4, type: "image", name: "1号苗情图像", station: "1号隔离大棚", lastTime: "2026-09-09 11:58:06", year: "2025", cover: coverSvg("苗情图像", "#e6a23c") },
{ id: 6, type: "image", name: "2号苗情图像", station: "1号隔离大棚", lastTime: "2026-09-09 12:14:31", year: "2025", cover: coverSvg("苗情图像", "#e6a23c") },
{ id: 5, type: "image", name: "3号苗情图像", station: "2号隔离大棚", lastTime: "2026-09-09 12:02:18", year: "2025", cover: coverSvg("苗情图像", "#e6a23c") },
{ id: 7, type: "image", name: "4号苗情图像", station: "2号隔离大棚", lastTime: "2026-09-09 12:30:05", year: "2025", cover: coverSvg("苗情图像", "#e6a23c") },
// 虫情测报灯(3):1号大棚 → 1、2号;2号大棚 → 3号
{ id: 8, type: "pestlamp", name: "1号虫情测报灯", station: "1号隔离大棚", lastTime: "2026-09-09 06:54:52", year: "2025", cover: coverSvg("虫情测报灯", "#67c23a") },
{ id: 10, type: "pestlamp", name: "2号虫情测报灯", station: "1号隔离大棚", lastTime: "2026-09-09 06:55:10", year: "2025", cover: coverSvg("虫情测报灯", "#67c23a") },
{ id: 9, type: "pestlamp", name: "3号虫情测报灯", station: "2号隔离大棚", lastTime: "2026-09-09 15:00:00", year: "2025", cover: coverSvg("虫情测报灯", "#67c23a") },
// 小气候仪(2):1号大棚 → 1号;2号大棚 → 2号
{ id: 11, type: "climate", name: "1号小气候仪", station: "1号隔离大棚", lastTime: "2026-09-09 14:00:00", year: "2025", cover: coverSvg("小气候仪", "#d71e1e") },
{ id: 12, type: "climate", name: "2号小气候仪", station: "2号隔离大棚", lastTime: "2026-09-09 14:05:23", year: "2025", cover: coverSvg("小气候仪", "#d71e1e") },
// 远程实时监控(4):1号大棚 → 1、2号;2号大棚 → 3、4号
{ id: 13, type: "realtime", name: "1号远程实时监控", station: "1号隔离大棚", lastTime: "2026-09-09 14:10:02", year: "2025", cover: coverSvg("远程实时监控", "#9b59b6") },
{ id: 15, type: "realtime", name: "2号远程实时监控", station: "1号隔离大棚", lastTime: "2026-09-09 14:18:30", year: "2025", cover: coverSvg("远程实时监控", "#9b59b6") },
{ id: 14, type: "realtime", name: "3号远程实时监控", station: "2号隔离大棚", lastTime: "2026-09-09 14:12:48", year: "2025", cover: coverSvg("远程实时监控", "#9b59b6") },
{ id: 16, type: "realtime", name: "4号远程实时监控", station: "2号隔离大棚", lastTime: "2026-09-09 14:22:14", year: "2025", cover: coverSvg("远程实时监控", "#9b59b6") }
];
const filteredDevices = computed(() => {
if (activeType.value === "all") return devices;
return devices.filter((d) => d.type === activeType.value);
});
// 远程实时监控设备点击 → 跳转监测数据页并预选设备
import { useRouter } from "vue-router";
const router = useRouter();
const goDevice = (d) => {
router.push({ path: "/isolation/data", query: { deviceId: d.id } });
};
</script>
<style scoped>
.device-overview {
padding: 14px 16px;
display: flex;
flex-direction: column;
height: calc(100vh - 80px);
box-sizing: border-box;
}
/* ===== 顶部类型 Tab ===== */
.type-tabs {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 8px 10px;
padding: 10px 14px;
background: #fff;
border: 1px solid #f0f0f0;
border-radius: 4px;
margin-bottom: 12px;
flex-shrink: 0;
}
.type-tab {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 6px 12px;
background: #fafafa;
border: 1px solid #ebeef5;
border-radius: 16px;
font-size: 13px;
color: #555;
cursor: pointer;
transition: all 0.15s;
user-select: none;
}
.type-tab:hover { color: #d71e1e; border-color: #d71e1e; }
.type-tab.active { background: #d71e1e; border-color: #d71e1e; color: #fff; }
.type-tab .t-icon { font-size: 14px; }
.type-tab .t-count { opacity: 0.85; font-size: 12px; }
/* ===== 中部状态卡片 ===== */
.status-row {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 12px;
margin-bottom: 14px;
flex-shrink: 0;
}
.status-card {
display: flex;
align-items: center;
gap: 12px;
padding: 14px 18px;
background: #fff;
border: 1px solid #f0f0f0;
border-radius: 4px;
cursor: pointer;
transition: all 0.15s;
position: relative;
overflow: hidden;
}
.status-card:hover { border-color: #d71e1e; transform: translateY(-1px); }
.status-card.active { border-color: #d71e1e; box-shadow: 0 2px 8px rgba(215, 30, 30, 0.12); }
.status-card::before {
content: "";
position: absolute;
left: 0; top: 0; bottom: 0;
width: 3px;
background: #d71e1e;
}
.sc-icon {
width: 44px; height: 44px;
border-radius: 6px;
background: #fef2f2;
color: #d71e1e;
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 22px;
flex-shrink: 0;
}
.status-online .sc-icon { background: #f0f9eb; color: #67c23a; }
.status-offline .sc-icon { background: #f4f4f5; color: #909399; }
.status-abnormal .sc-icon { background: #fdf6ec; color: #e6a23c; }
.sc-info { flex: 1; min-width: 0; }
.sc-label {
display: flex;
align-items: center;
gap: 4px;
font-size: 13px;
color: #888;
}
.sc-label .q { font-size: 12px; color: #bbb; cursor: help; }
.sc-value { font-size: 22px; font-weight: bold; color: #d71e1e; line-height: 1.3; }
.status-online .sc-value { color: #67c23a; }
.status-offline .sc-value { color: #909399; }
.status-abnormal .sc-value { color: #e6a23c; }
.sc-bar {
margin-top: 4px;
height: 4px;
background: #f5f5f5;
border-radius: 2px;
overflow: hidden;
}
.sc-bar-fill { height: 100%; border-radius: 2px; transition: width 0.3s; }
/* ===== 设备滚动区 ===== */
.device-scroll {
flex: 1;
overflow-y: auto;
overflow-x: hidden;
padding-right: 4px;
background: #f5f6f8;
border-radius: 4px;
}
.device-scroll::-webkit-scrollbar { width: 6px; }
.device-scroll::-webkit-scrollbar-thumb { background: #d4d4d4; border-radius: 3px; }
.device-scroll::-webkit-scrollbar-track { background: transparent; }
.device-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 14px;
padding: 14px;
}
.device-card {
background: #fff;
border: 1px solid #f0f0f0;
border-radius: 4px;
display: flex;
padding: 12px;
gap: 14px;
transition: all 0.15s;
}
.device-card:hover { box-shadow: 0 2px 10px rgba(0,0,0,0.06); border-color: #e0e0e0; }
.dc-cover {
width: 130px;
height: 110px;
flex-shrink: 0;
border-radius: 4px;
overflow: hidden;
background: #f5f5f5;
display: flex;
align-items: center;
justify-content: center;
}
.dc-cover img { width: 100%; height: 100%; object-fit: cover; display: block; }
.dc-body { flex: 1; min-width: 0; display: flex; flex-direction: column; justify-content: center; gap: 6px; }
.dc-title {
font-size: 15px;
font-weight: 600;
color: #333;
margin-bottom: 2px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.dc-line {
display: flex;
align-items: center;
flex-wrap: wrap;
font-size: 13px;
color: #555;
line-height: 1.6;
}
.dc-label { color: #888; flex-shrink: 0; }
.dc-val { color: #555; }
.dc-loc { color: #5b8def; margin-left: 4px; font-size: 13px; }
.dc-link { color: #d71e1e; margin-left: 8px; cursor: pointer; font-size: 13px; }
.dc-link:hover { text-decoration: underline; }
.empty-block {
grid-column: 1 / -1;
text-align: center;
padding: 60px 0;
color: #999;
font-size: 14px;
}
</style>
<template> <template>
<PagePlaceholder title="隔离检疫" desc="隔离区检疫流程开发中" icon="Lock" /> <PagePlaceholder title="监测数据分析" desc="监测数据分析模块正在搭建中" icon="Monitor" />
</template> </template>
<script setup> <script setup>
......
<template>
<div class="monitor-data">
<!-- 左侧:设备类型 / 监测点 双 Tab 列表 -->
<aside class="md-sidebar">
<div class="md-side-tabs">
<div
v-for="t in sideTabs"
:key="t.key"
class="md-side-tab"
:class="{ active: activeSideTab === t.key }"
@click="activeSideTab = t.key"
>
<el-icon><component :is="t.icon" /></el-icon>
<span>{{ t.name }}</span>
</div>
</div>
<div class="md-side-list">
<template v-if="activeSideTab === 'type'">
<template v-for="g in typeGroups" :key="g.key">
<div class="md-side-row" :class="{ active: g.key === activeTypeKey }">
<span class="row-name" @click="onSelectType(g)">
<span class="dot" :style="{ background: g.color }"></span>
{{ g.name }}
</span>
<span class="row-meta">
<span class="row-count">·{{ g.count }}</span>
<el-icon class="row-chevron" @click.stop="toggleType(g.key)"><ArrowDown v-if="g.expanded" /><ArrowRight v-else /></el-icon>
</span>
</div>
<div v-if="g.expanded" class="md-sub-list">
<div
v-for="d in typeChildren(g.key)"
:key="d.id"
class="md-sub-item"
:class="{ active: d.id === activeDeviceId }"
@click="onSelectDevice(d)"
>
<div class="sub-thumb">
<span class="thumb-ico" :style="{ background: typeMap[g.key].color + '22', color: typeMap[g.key].color }">
<el-icon><Picture /></el-icon>
</span>
</div>
<div class="sub-info">
<div class="sub-name">{{ d.name }}</div>
<div class="sub-station">{{ d.station }}</div>
</div>
</div>
</div>
</template>
</template>
<template v-else>
<template v-for="g in stationGroups" :key="g.key">
<div class="md-side-row" :class="{ active: g.key === activeStationKey }">
<span class="row-name" @click="onSelectStation(g)">
<span class="dot" style="background:#d71e1e"></span>
{{ g.name }}
</span>
<span class="row-meta">
<span class="row-count">·{{ g.count }}</span>
<el-icon class="row-chevron" @click.stop="toggleStation(g.key)"><ArrowDown v-if="g.expanded" /><ArrowRight v-else /></el-icon>
</span>
</div>
<div v-if="g.expanded" class="md-sub-list">
<div
v-for="d in stationChildren(g.key)"
:key="d.id"
class="md-sub-item"
:class="{ active: d.id === activeDeviceId }"
@click="onSelectDevice(d)"
>
<div class="sub-thumb">
<span class="thumb-ico" :style="{ background: typeMap[d.type].color + '22', color: typeMap[d.type].color }">
<el-icon><Picture /></el-icon>
</span>
</div>
<div class="sub-info">
<div class="sub-name">{{ d.name }}</div>
<div class="sub-station">{{ typeMap[d.type].name }}</div>
</div>
</div>
</div>
</template>
</template>
</div>
</aside>
<!-- 右侧:数据详情 -->
<main class="md-main">
<div class="md-head">
<span v-if="!isRemote && !isImage && !isPestLamp && !isSexTrap" class="md-tag">气候监测数据</span>
<span v-if="isPestLamp" class="md-tag md-tag-btn" :class="{ active: pestTab === 'data' }" @click="togglePestTab('data')">灯诱监测数据</span>
<button v-if="isPestLamp" class="md-tag md-tag-btn" :class="{ active: pestTab === 'image' }" @click="togglePestTab('image')">灯诱图片</button>
<span v-if="isSexTrap" class="md-tag md-tag-btn" :class="{ active: sexTab !== 'image' }" @click="sexTab = 'data'">性诱监测数据</span>
<button v-if="isSexTrap" class="md-tag md-tag-btn" :class="{ active: sexTab === 'image' }" @click="sexTab = 'image'">性诱图片</button>
<span v-if="isImage" class="md-tag">苗情图像</span>
<button v-if="isRemote" class="md-tag md-tag-btn" :class="{ active: remoteTab === 'live' }" @click="toggleRemoteTab('live')">实时监控</button>
<button v-if="isRemote" class="md-tag md-tag-btn" :class="{ active: remoteTab === 'history' }" @click="toggleRemoteTab('history')">历史图片</button>
</div>
<div class="md-filter">
<span class="f-label">时间选择:</span>
<div class="time-tabs">
<div
v-for="t in timeTabs"
:key="t.key"
class="time-tab"
:class="{ active: activeTime === t.key }"
@click="onPickTime(t)"
>{{ t.name }}</div>
</div>
<input class="f-date" type="date" v-model="dateStart" />
<span class="f-dash">至</span>
<input class="f-date" type="date" v-model="dateEnd" />
<button class="f-btn" @click="onQuery">查询</button>
</div>
<!-- 关注害虫(独立行,位于时间筛选下方) -->
<div v-if="isPestLamp && pestTab === 'data'" class="md-pest-filter">
<span class="pf-label">关注害虫:</span>
<label class="pf-check">
<input type="checkbox" :checked="selectedPests.length === pestFocusTypes.length" @change="onToggleAllPests" />
<span>全部</span>
</label>
<label v-for="t in pestFocusTypes" :key="t.key" class="pf-check">
<input type="checkbox" :value="t.key" v-model="selectedPests" />
<span>{{ t.label }}</span>
</label>
</div>
<template v-if="!isRemote && !isImage && !isPestLamp && !isSexTrap">
<div class="md-panel">
<div class="panel-head">
<div class="panel-title">{{ activeDevice.name }} · ({{ activeMetric.label }})</div>
<div class="panel-tools">
<el-icon class="t-ico" :class="{ active: chartMode === 'line' }" @click="chartMode = 'line'" title="折线图"><DataLine /></el-icon>
<el-icon class="t-ico" :class="{ active: chartMode === 'bar' }" @click="chartMode = 'bar'" title="柱状图"><Histogram /></el-icon>
<el-icon class="t-ico" :class="{ active: chartMode === 'table' }" @click="chartMode = 'table'" title="表格"><Grid /></el-icon>
<el-icon class="t-ico" @click="onDownload" title="下载"><Download /></el-icon>
</div>
</div>
<div class="panel-body">
<div v-if="chartMode === 'line'" class="chart-wrap">
<div class="unit-row">单位:{{ activeMetric.unit }}</div>
<div class="chart-main" ref="lineSvgWrap">
<svg
ref="lineSvg"
:viewBox="'0 0 ' + chartW + ' ' + chartH"
preserveAspectRatio="none"
class="chart-svg"
@mousemove="onChartMove"
@mouseleave="hoverIdx = -1"
@click="onChartClick"
>
<!-- 网格背景:水平 + 垂直虚线 -->
<g class="grid">
<line v-for="(y, i) in yLines" :key="'hy' + i" :x1="40" :x2="chartW - 10" :y1="y" :y2="y" stroke="#bbb" stroke-width="0.6" stroke-dasharray="3 3" />
<line v-for="(x, i) in xGridLines" :key="'vx' + i" :x1="x" :x2="x" :y1="plotTop" :y2="plotBottom" stroke="#bbb" stroke-width="0.6" stroke-dasharray="3 3" />
</g>
<!-- Y 轴 -->
<line :x1="40" :x2="40" :y1="plotTop - 6" :y2="plotBottom" stroke="#000" stroke-width="1" />
<text v-for="(y, i) in yLines" :key="'yt' + i" :x="34" :y="y + 3" text-anchor="end" font-size="10" fill="#333">{{ yLabels[i] }}</text>
<!-- X 轴 -->
<line :x1="40" :x2="chartW - 6" :y1="plotBottom" :y2="plotBottom" stroke="#000" stroke-width="1" />
<g v-for="(x, i) in xLabels" :key="'xt' + i" class="xt-group">
<text
:x="x.pos"
:y="chartH - 4"
text-anchor="end"
font-size="11"
font-weight="500"
fill="#333"
:transform="`rotate(-30 ${x.pos} ${chartH - 6})`"
>{{ x.text }}</text>
</g>
<!-- 折线 -->
<polyline :points="linePoints" fill="none" stroke="#5b8def" stroke-width="1.5" />
<!-- 悬停指示线 + 标记点 -->
<template v-if="hoverIdx >= 0 && dotPoints[hoverIdx]">
<line :x1="dotPoints[hoverIdx].x" :x2="dotPoints[hoverIdx].x" :y1="plotTop" :y2="plotBottom" stroke="#d71e1e" stroke-width="1" stroke-dasharray="4 3" />
<circle :cx="dotPoints[hoverIdx].x" :cy="dotPoints[hoverIdx].y" r="3.5" fill="#d71e1e" stroke="#fff" stroke-width="1.5" />
</template>
</svg>
<!-- Tooltip 浮层 -->
<div
v-if="hoverIdx >= 0 && dotPoints[hoverIdx]"
class="chart-tip"
:style="{ left: tipLeft + 'px', top: tipTop + 'px' }"
>
<div class="tip-time">{{ xLabelsRaw[hoverIdx] }}</div>
<div class="tip-row">
<span class="tip-dot"></span>
<span class="tip-name">{{ activeMetric.label }}</span>
<span class="tip-value">{{ series[hoverIdx] }}</span>
</div>
</div>
</div>
<div class="chart-legend">
<span class="legend-item"><span class="legend-line" style="background:#5b8def"></span>{{ activeMetric.label }}</span>
</div>
</div>
<div v-else-if="chartMode === 'bar'" class="chart-wrap">
<div class="unit-row">单位:{{ activeMetric.unit }}</div>
<div class="chart-main" ref="barSvgWrap">
<svg :viewBox="'0 0 ' + chartW + ' ' + chartH" preserveAspectRatio="none" class="chart-svg" @mousemove="onChartMove" @mouseleave="hoverIdx = -1" @click="onChartClick">
<g class="grid">
<line v-for="(y, i) in yLines" :key="'by' + i" :x1="40" :x2="chartW - 10" :y1="y" :y2="y" stroke="#bbb" stroke-width="0.6" stroke-dasharray="3 3" />
<line v-for="(x, i) in xGridLines" :key="'bvx' + i" :x1="x" :x2="x" :y1="plotTop" :y2="plotBottom" stroke="#bbb" stroke-width="0.6" stroke-dasharray="3 3" />
</g>
<line :x1="40" :x2="40" :y1="plotTop - 6" :y2="plotBottom" stroke="#000" stroke-width="1" />
<text v-for="(y, i) in yLines" :key="'byt' + i" :x="34" :y="y + 3" text-anchor="end" font-size="10" fill="#333">{{ yLabels[i] }}</text>
<line :x1="40" :x2="chartW - 6" :y1="plotBottom" :y2="plotBottom" stroke="#000" stroke-width="1" />
<g v-for="(x, i) in xLabels" :key="'bxt' + i" class="xt-group">
<text
:x="x.pos"
:y="chartH - 4"
text-anchor="end"
font-size="11"
font-weight="500"
fill="#333"
:transform="`rotate(-30 ${x.pos} ${chartH - 6})`"
>{{ x.text }}</text>
</g>
<g>
<rect
v-for="(p, i) in barPoints"
:key="'br' + i"
:x="p.x - p.w / 2"
:y="p.y"
:width="p.w"
:height="p.h"
:fill="hoverIdx === i ? '#d71e1e' : '#5b8def'"
opacity="0.9"
/>
</g>
<template v-if="hoverIdx >= 0 && dotPoints[hoverIdx]">
<line :x1="dotPoints[hoverIdx].x" :x2="dotPoints[hoverIdx].x" :y1="plotTop" :y2="plotBottom" stroke="#d71e1e" stroke-width="1" stroke-dasharray="4 3" />
</template>
</svg>
<div
v-if="hoverIdx >= 0 && dotPoints[hoverIdx]"
class="chart-tip"
:style="{ left: tipLeft + 'px', top: tipTop + 'px' }"
>
<div class="tip-time">{{ xLabelsRaw[hoverIdx] }}</div>
<div class="tip-row">
<span class="tip-dot"></span>
<span class="tip-name">{{ activeMetric.label }}</span>
<span class="tip-value">{{ series[hoverIdx] }}</span>
</div>
</div>
</div>
<div class="chart-legend">
<span class="legend-item"><span class="legend-line" style="background:#5b8def"></span>{{ activeMetric.label }}</span>
</div>
</div>
<div v-else class="table-wrap">
<table class="md-table">
<thead>
<tr>
<th style="width:18%">序号</th>
<th style="width:45%">采集时间</th>
<th style="width:37%">{{ activeMetric.label }}</th>
</tr>
</thead>
<tbody>
<tr v-for="row in tableData" :key="row.idx">
<td>{{ row.idx }}</td>
<td>{{ row.time }}</td>
<td>{{ row.value }}</td>
</tr>
</tbody>
</table>
<div class="pager">
<span class="total">共 {{ totalRows }} 条</span>
<button class="pg-btn" :disabled="tablePage === 1" @click="goTablePage(tablePage - 1)">&lt;</button>
<button v-for="p in tablePageNumbers" :key="p" class="pg-btn" :class="{ active: p === tablePage }" :disabled="p === '…'" @click="goTablePage(p)">{{ p }}</button>
<button class="pg-btn" :disabled="tablePage === tableTotalPages" @click="goTablePage(tablePage + 1)">&gt;</button>
<select class="pg-size" v-model.number="tablePageSize">
<option :value="10">10条/页</option>
<option :value="20">20条/页</option>
<option :value="50">50条/页</option>
</select>
<span class="total">前往</span>
<input class="pg-input" v-model.number="tableJump" type="number" min="1" :max="tableTotalPages" />
<span class="total">页</span>
</div>
</div>
</div>
</div>
<div class="md-panel">
<div class="panel-head">
<div class="panel-title">最新数据</div>
<div class="panel-tools time-tip">于 {{ reportTime }} 上报</div>
</div>
<div class="panel-body">
<div class="metric-grid">
<div
v-for="m in metrics"
:key="m.key"
class="metric-card"
:class="{ active: m.key === activeMetric.key }"
@click="activeMetric = m"
>
<div class="m-label">{{ m.label }}</div>
<div class="m-row">
<span class="m-value">{{ m.value }}</span>
<span class="m-unit">{{ m.unit }}</span>
<el-icon class="m-ico"><component :is="m.icon" /></el-icon>
</div>
</div>
</div>
</div>
</div>
</template>
<!-- 苗情图像视图 -->
<template v-else-if="isImage">
<div class="history-panel">
<div class="history-grid">
<div
v-for="img in pagedImageItems"
:key="img.no"
class="history-card"
>
<div class="history-img" :style="{ background: imageBg[img.no % imageBg.length] }">
<div class="history-empty">
<el-icon class="empty-ico"><Picture /></el-icon>
<span>苗情 #{{ img.no }}</span>
</div>
</div>
<div class="history-meta">于 {{ img.time }} 拍摄</div>
</div>
</div>
<div class="history-pager">
<span class="total">共 {{ imageItems.length }} 条</span>
<button class="pg-btn" :disabled="imagePage === 1" @click="goImagePage(imagePage - 1)">&lt;</button>
<button v-for="p in imagePageNumbers" :key="'i' + p" class="pg-btn" :class="{ active: p === imagePage }" :disabled="p === '…'" @click="goImagePage(p)">{{ p }}</button>
<button class="pg-btn" :disabled="imagePage === imageTotalPages" @click="goImagePage(imagePage + 1)">&gt;</button>
<select class="pg-size" v-model.number="imagePageSize">
<option :value="12">12条/页</option>
<option :value="20">20条/页</option>
<option :value="50">50条/页</option>
</select>
<span class="total">前往</span>
<input class="pg-input" v-model.number="imageJump" type="number" min="1" :max="imageTotalPages" />
<span class="total">页</span>
</div>
</div>
</template>
<!-- 虫情测报灯视图 -->
<template v-else-if="isPestLamp">
<!-- 灯诱监测数据:图表 + 缩略图 -->
<template v-if="pestTab === 'data'">
<div class="md-panel">
<div class="panel-head">
<div class="panel-title">{{ activeDevice.name }} · (虫情趋势)</div>
</div>
<div class="panel-body pest-body">
<svg class="pest-svg" :viewBox="'0 0 ' + pestW + ' ' + pestH" preserveAspectRatio="none">
<!-- 横向网格 -->
<line v-for="(y, i) in pestYLines" :key="'yh' + i" :x1="40" :x2="pestW - 10" :y1="y" :y2="y" stroke="#dcdfe6" stroke-width="0.5" />
<!-- 纵向网格 -->
<line v-for="(x, i) in pestXLines" :key="'yv' + i" :x1="x" :x2="x" :y1="20" :y2="pestH - 40" stroke="#dcdfe6" stroke-width="0.5" />
<!-- Y 轴刻度 -->
<text v-for="(y, i) in pestYLines" :key="'yt' + i" :x="36" :y="y + 3" text-anchor="end" font-size="10" fill="#606266">{{ pestYLabels[i] }}</text>
<!-- X 轴刻度 -->
<text v-for="(x, i) in pestXLabels" :key="'xt' + i" :x="x.pos" :y="pestH - 24" text-anchor="middle" font-size="10" fill="#606266">{{ x.label }}</text>
<!-- Y 轴 -->
<line :x1="40" :x2="40" :y1="20" :y2="pestH - 40" stroke="#333" stroke-width="1" />
<!-- X 轴 -->
<line :x1="40" :x2="pestW - 10" :y1="pestH - 40" :y2="pestH - 40" stroke="#333" stroke-width="1" />
<!-- 曲线 -->
<polyline v-for="line in pestLines" :key="line.key" :points="line.points" fill="none" :stroke="line.color" stroke-width="1.2" />
</svg>
<div class="pest-legend">
<span v-for="t in pestTypes" :key="t.key" class="pest-leg-item" :class="{ off: !selectedPests.includes(t.key) }">
<span class="pest-leg-dot" :style="{ background: t.color }"></span>
<span>{{ t.label }}</span>
</span>
<span class="pest-page-num">{{ pestPage }} / 2</span>
<button class="pest-page-btn" @click="pestPage = pestPage === 1 ? 2 : 1">{{ pestPage === 1 ? '>' : '<' }}</button>
</div>
<div class="pest-thumbs">
<div v-for="(thumb, idx) in pestThumbs" :key="thumb.key" class="pest-thumb" :style="{ background: thumb.bg }">
<el-icon class="empty-ico" :style="{ color: thumb.color }"><Picture /></el-icon>
<div class="pest-thumb-meta">{{ thumb.label }} · 于 {{ thumb.time }} 拍摄</div>
</div>
<div class="pest-thumb pest-thumb-more" :style="{ background: lampBg[0] }" @click="togglePestTab('image')">
<span class="pest-thumb-more-text">查看全部图片 &gt;</span>
</div>
</div>
</div>
</div>
</template>
<!-- 灯诱图片:图片网格 -->
<template v-else>
<div class="history-panel">
<div class="history-grid">
<div
v-for="lamp in pagedLampItems"
:key="lamp.no"
class="history-card"
>
<div class="history-img" :style="{ background: lampBg[lamp.no % lampBg.length] }">
<div class="history-empty">
<el-icon class="empty-ico"><Picture /></el-icon>
<span>虫情 #{{ lamp.no }}</span>
</div>
</div>
<div class="history-meta">于 {{ lamp.time }} 拍摄</div>
</div>
</div>
<div class="history-pager">
<span class="total">共 {{ lampItems.length }} 条</span>
<button class="pg-btn" :disabled="lampPage === 1" @click="goLampPage(lampPage - 1)">&lt;</button>
<button v-for="p in lampPageNumbers" :key="'l' + p" class="pg-btn" :class="{ active: p === lampPage }" :disabled="p === '…'" @click="goLampPage(p)">{{ p }}</button>
<button class="pg-btn" :disabled="lampPage === lampTotalPages" @click="goLampPage(lampPage + 1)">&gt;</button>
<select class="pg-size" v-model.number="lampPageSize">
<option :value="12">12条/页</option>
<option :value="20">20条/页</option>
<option :value="50">50条/页</option>
</select>
<span class="total">前往</span>
<input class="pg-input" v-model.number="lampJump" type="number" min="1" :max="lampTotalPages" />
<span class="total">页</span>
</div>
</div>
</template>
</template>
<!-- 性诱监测仪视图 -->
<template v-else-if="isSexTrap">
<!-- 性诱监测数据:图表 + 缩略图 -->
<template v-if="sexTab === 'data'">
<!-- 最新数据卡片 -->
<div class="md-panel">
<div class="panel-head">
<div class="panel-title sex-meta">
<el-icon class="empty-ico sex-meta-ico"><DataLine /></el-icon>
<div>
<div class="sex-meta-title">最新数据</div>
<div class="sex-meta-time">于 {{ sexReportTime }} 上报</div>
</div>
</div>
<div class="sex-latest-grid">
<div v-for="m in sexLatestMetrics" :key="m.key" class="sex-latest-cell">
<div class="sex-latest-label">{{ m.label }}</div>
<div class="sex-latest-value" :style="{ color: m.color }">{{ m.value }}</div>
</div>
</div>
</div>
</div>
<!-- 虫量统计柱状图 -->
<div class="md-panel">
<div class="panel-head">
<div class="panel-title">{{ activeDevice.name }}-监测虫量数据统计(性诱害虫害(厂商未明确害虫害),杨小舟蛾)</div>
<div class="panel-tools">
<el-icon class="t-ico" :class="{ active: sexChartMode === 'line' }" @click="sexChartMode = 'line'" title="折线图"><DataLine /></el-icon>
<el-icon class="t-ico" :class="{ active: sexChartMode === 'bar' }" @click="sexChartMode = 'bar'" title="柱状图"><Histogram /></el-icon>
<el-icon class="t-ico" :class="{ active: sexChartMode === 'table' }" @click="sexChartMode = 'table'" title="表格"><Grid /></el-icon>
<el-icon class="t-ico" @click="onDownload" title="下载"><Download /></el-icon>
</div>
</div>
<div class="panel-body">
<div class="unit-row">单位:头</div>
<div v-if="sexChartMode !== 'table'" class="chart-wrap">
<div class="chart-main">
<svg :viewBox="'0 0 ' + pestW + ' ' + pestH" preserveAspectRatio="none" class="sex-chart-svg">
<!-- 横向网格 -->
<line v-for="(y, i) in pestYLines" :key="'syh' + i" :x1="40" :x2="pestW - 10" :y1="y" :y2="y" stroke="#dcdfe6" stroke-width="0.5" />
<!-- 纵向网格 -->
<line v-for="(x, i) in pestXLines" :key="'syv' + i" :x1="x" :x2="x" :y1="20" :y2="pestH - 40" stroke="#dcdfe6" stroke-width="0.5" />
<!-- Y 轴刻度 -->
<text v-for="(y, i) in pestYLines" :key="'syt' + i" :x="36" :y="y + 3" text-anchor="end" font-size="10" fill="#606266">{{ pestYLabels[i] }}</text>
<!-- X 轴刻度 -->
<text v-for="(x, i) in pestXLabels" :key="'sxt' + i" :x="x.pos" :y="pestH - 24" text-anchor="middle" font-size="10" fill="#606266">{{ x.label }}</text>
<!-- Y 轴 -->
<line :x1="40" :x2="40" :y1="20" :y2="pestH - 40" stroke="#333" stroke-width="1" />
<!-- X 轴 -->
<line :x1="40" :x2="pestW - 10" :y1="pestH - 40" :y2="pestH - 40" stroke="#333" stroke-width="1" />
<!-- 柱状图 -->
<rect
v-if="sexChartMode === 'bar'"
v-for="(bar, i) in sexBarRects"
:key="'br' + i"
:x="bar.x"
:y="bar.y"
:width="bar.w"
:height="bar.h"
fill="#5b8ff9"
rx="1"
/>
<!-- 折线图 -->
<polyline
v-else-if="sexChartMode === 'line'"
:points="sexLinePoints"
fill="none"
stroke="#5b8ff9"
stroke-width="1.5"
/>
<circle
v-else-if="sexChartMode === 'line'"
v-for="(pt, i) in sexLineDots"
:key="'cd' + i"
:cx="pt.x"
:cy="pt.y"
r="2"
fill="#5b8ff9"
/>
</svg>
</div>
</div>
<div v-else class="table-wrap">
<table class="data-table">
<thead>
<tr><th>采集时间</th><th>杨小舟蛾(头)</th></tr>
</thead>
<tbody>
<tr v-for="(row, i) in sexTableRows" :key="i">
<td>{{ row.time }}</td>
<td>{{ row.value }}</td>
</tr>
</tbody>
</table>
</div>
<!-- 图例(蝌蚪形圆点) -->
<div class="pest-legend">
<span class="pest-leg-item">
<span class="pest-leg-dot" style="color: #5b8ff9;"></span>
<span>性诱害虫害(厂商未明确害虫害)</span>
</span>
<span class="pest-leg-item">
<span class="pest-leg-dot" style="color: #5ad8a6;"></span>
<span>杨小舟蛾</span>
</span>
</div>
</div>
</div>
<!-- 底部 4 张图片 -->
<div class="pest-thumbs">
<div v-for="(thumb, idx) in sexThumbs" :key="thumb.key" class="pest-thumb" :style="{ background: thumb.bg }">
<el-icon class="empty-ico" :style="{ color: thumb.color }"><Picture /></el-icon>
<div class="pest-thumb-meta">{{ thumb.label }} · 于 {{ thumb.time }} 拍摄</div>
</div>
<div class="pest-thumb pest-thumb-more" :style="{ background: lampBg[0] }" @click="sexTab = 'image'">
<span class="pest-thumb-more-text">查看全部图片 &gt;</span>
</div>
</div>
</template>
<!-- 性诱图片:图片网格 -->
<template v-else>
<div class="history-panel">
<div class="history-grid">
<div
v-for="img in pagedSexImages"
:key="img.no"
class="history-card"
>
<div class="history-img" :style="{ background: img.bg }">
<div class="history-empty">
<el-icon class="empty-ico"><Picture /></el-icon>
<span>性诱 #{{ img.no }}</span>
</div>
</div>
<div class="history-meta">于 {{ img.time }} 拍摄</div>
</div>
</div>
<div class="history-pager">
<span class="total">共 {{ sexImages.length }} 条</span>
<button class="pg-btn" :disabled="sexImagePage === 1" @click="goSexImagePage(sexImagePage - 1)">&lt;</button>
<button v-for="p in sexImagePageNumbers" :key="'si' + p" class="pg-btn" :class="{ active: p === sexImagePage }" :disabled="p === '…'" @click="goSexImagePage(p)">{{ p }}</button>
<button class="pg-btn" :disabled="sexImagePage === sexImageTotalPages" @click="goSexImagePage(sexImagePage + 1)">&gt;</button>
<select class="pg-size" v-model.number="sexImagePageSize">
<option :value="12">12条/页</option>
<option :value="20">20条/页</option>
<option :value="50">50条/页</option>
</select>
<span class="total">前往</span>
<input class="pg-input" v-model.number="sexImageJump" type="number" min="1" :max="sexImageTotalPages" />
<span class="total">页</span>
</div>
</div>
</template>
</template>
<!-- 远程实时监控视图 -->
<template v-else>
<!-- 实时监控 Tab:视频 + 快照 -->
<template v-if="remoteTab === 'live'">
<div class="rm-panel">
<div class="rm-player">
<div class="rm-bg" :style="{ background: remoteBg[activeDevice.id] || remoteBg[1] }">
<div class="rm-empty">
<el-icon class="empty-ico"><VideoCamera /></el-icon>
<span>视频监控画面</span>
</div>
</div>
<div class="rm-time">{{ nowStr }}</div>
<div class="rm-tools">
<el-icon class="r-ico" title="截图"><Camera /></el-icon>
<el-icon class="r-ico" title="录制"><VideoPlay /></el-icon>
</div>
</div>
<div class="rm-ptz">
<div class="rm-pad">
<button class="rm-btn" @click="ptzAct('up')"><el-icon><ArrowUp /></el-icon></button>
<div class="rm-pad-row">
<button class="rm-btn" @click="ptzAct('left')"><el-icon><ArrowLeft /></el-icon></button>
<button class="rm-btn rm-center" @click="ptzAct('center')"><el-icon><Plus /></el-icon></button>
<button class="rm-btn" @click="ptzAct('right')"><el-icon><ArrowRight /></el-icon></button>
</div>
<button class="rm-btn" @click="ptzAct('down')"><el-icon><ArrowDown /></el-icon></button>
</div>
<div class="rm-row-line">
<span class="rm-tag">慢</span>
<div class="rm-slider"><div class="slider-fill" style="left:40%;width:20%"></div><div class="slider-thumb" style="left:60%"></div></div>
<span class="rm-tag">快</span>
</div>
<div class="rm-row-line">
<button class="rm-btn"><el-icon><ZoomIn /></el-icon></button>
<button class="rm-btn"><el-icon><ZoomOut /></el-icon></button>
</div>
<div class="rm-row-line">
<button class="rm-btn"><el-icon><Aim /></el-icon></button>
<button class="rm-btn"><el-icon><FullScreen /></el-icon></button>
</div>
<div class="rm-row-line">
<button class="hd-btn">HD</button>
<button class="hd-btn"><el-icon><FullScreen /></el-icon></button>
</div>
</div>
</div>
<div class="rm-snap-panel">
<div class="snap-grid">
<div v-for="s in snapshots" :key="s.id" class="snap-card" :class="{ active: activeSnapId === s.id }" @click="activeSnapId = s.id">
<div class="snap-img" :style="{ background: remoteBg[activeDevice.id] || remoteBg[1] }">
<div class="snap-empty"><el-icon class="empty-ico"><Picture /></el-icon><span>{{ s.label }}</span></div>
</div>
<div class="snap-meta">于 {{ s.time }} 拍摄</div>
</div>
</div>
</div>
</template>
<!-- 历史图片 Tab:图片网格 + 分页 -->
<template v-else>
<div class="rm-history-panel">
<div class="history-grid">
<div
v-for="img in pagedHistory"
:key="img.id"
class="history-card"
@click="activeSnapId = img.id"
>
<div class="history-img" :style="{ background: remoteBg[activeDevice.id] || remoteBg[1] }">
<div class="history-empty">
<el-icon class="empty-ico"><Picture /></el-icon>
<span>{{ img.id }}</span>
</div>
</div>
<div class="history-meta">{{ img.time }}</div>
</div>
</div>
<div class="history-pager">
<span class="total">共 {{ historyImgs.length }} 条</span>
<button class="pg-btn" :disabled="historyPage === 1" @click="goHistoryPage(historyPage - 1)">&lt;</button>
<button v-for="p in historyPageNumbers" :key="'h' + p" class="pg-btn" :class="{ active: p === historyPage }" :disabled="p === '…'" @click="goHistoryPage(p)">{{ p }}</button>
<button class="pg-btn" :disabled="historyPage === historyTotalPages" @click="goHistoryPage(historyPage + 1)">&gt;</button>
<select class="pg-size" v-model.number="historyPageSize">
<option :value="12">12条/页</option>
<option :value="20">20条/页</option>
<option :value="50">50条/页</option>
</select>
<span class="total">前往</span>
<input class="pg-input" v-model.number="historyJump" type="number" min="1" :max="historyTotalPages" />
<span class="total">页</span>
</div>
</div>
</template>
</template>
</main>
</div>
</template>
<script setup>
import { ref, computed, reactive, watch, onMounted } from "vue";
import { useRouter } from "vue-router";
const sideTabs = [
{ key: "type", name: "设备类型", icon: "List" },
{ key: "station", name: "监测点", icon: "LocationFilled" }
];
const activeSideTab = ref("type");
const typeMap = {
climate: { name: "小气候仪", color: "#d71e1e" },
realtime: { name: "远程实时监控", color: "#9b59b6" },
pestlamp: { name: "虫情测报灯", color: "#67c23a" },
image: { name: "苗情图像", color: "#e6a23c" },
sextrap: { name: "性诱监测仪", color: "#5b8def" }
};
// 设备原始数据(按 station 优先排序;编号规则:1号大棚 → 1,2..;2号大棚 → 接着 3,4..)
const rawDevices = [
{ id: 1, type: "sextrap", station: "1号大棚" },
{ id: 2, type: "sextrap", station: "1号大棚" },
{ id: 3, type: "sextrap", station: "2号大棚" },
{ id: 4, type: "image", station: "1号大棚" },
{ id: 5, type: "image", station: "2号大棚" },
{ id: 6, type: "image", station: "1号大棚" },
{ id: 7, type: "image", station: "2号大棚" },
{ id: 8, type: "pestlamp", station: "1号大棚" },
{ id: 9, type: "pestlamp", station: "2号大棚" },
{ id: 10, type: "pestlamp", station: "1号大棚" },
{ id: 11, type: "climate", station: "1号大棚" },
{ id: 12, type: "climate", station: "2号大棚" },
{ id: 13, type: "realtime", station: "1号大棚" },
{ id: 14, type: "realtime", station: "2号大棚" },
{ id: 15, type: "realtime", station: "1号大棚" },
{ id: 16, type: "realtime", station: "2号大棚" }
];
// 按 station 优先、再按 id 生成每类的设备编号
function buildDevices() {
const sorted = [...rawDevices].sort((a, b) => {
if (a.station !== b.station) return a.station < b.station ? -1 : 1;
return a.id - b.id;
});
const buckets = {};
for (const d of sorted) {
if (!buckets[d.type]) buckets[d.type] = [];
buckets[d.type].push(d);
}
return sorted.map((d) => {
const list = buckets[d.type];
const num = list.indexOf(d) + 1;
return { ...d, name: num + "号" + typeMap[d.type].name };
});
}
const devices = buildDevices();
const typeGroups = reactive([
{ key: "climate", name: "小气候仪", count: 2, expanded: true, color: "#d71e1e" },
{ key: "realtime", name: "远程实时监控", count: 4, expanded: false, color: "#9b59b6" },
{ key: "pestlamp", name: "虫情测报灯", count: 3, expanded: false, color: "#67c23a" },
{ key: "image", name: "苗情图像", count: 4, expanded: false, color: "#e6a23c" },
{ key: "sextrap", name: "性诱监测仪", count: 3, expanded: false, color: "#5b8def" }
]);
const activeTypeKey = ref("climate");
const stationGroups = reactive([
{ key: "g1", name: "1号隔离大棚", count: devices.filter(d => d.station === "1号大棚").length, expanded: false },
{ key: "g2", name: "2号隔离大棚", count: devices.filter(d => d.station === "2号大棚").length, expanded: false }
]);
const activeStationKey = ref("g1");
const activeDeviceId = ref(11);
const activeDevice = computed(() => devices.find((d) => d.id === activeDeviceId.value) || devices[0]);
const isRemote = computed(() => activeDevice.value.type === "realtime");
// 苗情图像缩略图背景渐变(4 种绿色调)
const imageBg = [
"linear-gradient(135deg, #e8f5e9, #c8e6c9)",
"linear-gradient(135deg, #dcedc8, #aed581)",
"linear-gradient(135deg, #c5e1a5, #9ccc65)",
"linear-gradient(135deg, #b9d6a3, #7cb342)"
];
const isImage = computed(() => activeDevice.value.type === "image");
const isPestLamp = computed(() => activeDevice.value.type === "pestlamp");
const isSexTrap = computed(() => activeDevice.value.type === "sextrap");
const pestTab = ref("data");
const togglePestTab = (which) => {
if (which === "data" || which === "image") pestTab.value = which;
else pestTab.value = pestTab.value === "data" ? "image" : "data";
};
const pestPage = ref(1);
const onlyFocus = ref(false);
const lampBg = [
"linear-gradient(135deg, #fdf3e3, #f7e2c5)",
"linear-gradient(135deg, #fff7e6, #ffd9a3)",
"linear-gradient(135deg, #fbeee0, #f5d4a0)",
"linear-gradient(135deg, #fdf0e0, #facd8a)"
];
const pestThumbs = computed(() => {
const keys = selectedPests.value.slice(0, 3);
return keys.map((k, i) => {
const t = pestTypes.find((p) => p.key === k);
return {
key: k,
label: t ? t.label : "未知",
color: t ? t.color : "#67c23a",
bg: lampBg[i % lampBg.length],
time: "2026-09-10 " + (8 - i).toString().padStart(2, "0") + ":30:" + ((i + 1) * 12).toString().padStart(2, "0")
};
});
});
const pestTypes = [
{ key: "hdlb", label: "黄地老虎", color: "#5470c6" },
{ key: "lmc", label: "绿盲蝽", color: "#67c23a" },
{ key: "xc", label: "小菜蛾", color: "#e6a23c" },
{ key: "dlb", label: "地老虎", color: "#f56c6c" },
{ key: "tcye", label: "甜菜夜蛾", color: "#9b59b6" },
{ key: "nc", label: "粘虫", color: "#1abc9c" },
{ key: "ncsum", label: "粘虫(合计)", color: "#ff5722" },
{ key: "edyye", label: "二点委夜蛾", color: "#3f51b5" },
{ key: "cdm", label: "草地螟", color: "#e91e63" },
{ key: "glye", label: "甘蓝夜蛾", color: "#009688" },
{ key: "mlc", label: "棉铃虫", color: "#607d8b" }
];
const pestFocusTypes = [
{ key: "tcye", label: "甜菜夜蛾", color: "#f56c6c" },
{ key: "mlc", label: "棉铃虫", color: "#607d8b" },
{ key: "xc", label: "小菜蛾", color: "#67c23a" },
{ key: "edyye", label: "二点委夜蛾", color: "#3f51b5" },
{ key: "kjk", label: "叩甲", color: "#795548" }
];
const selectedPests = ref(pestTypes.map((t) => t.key));
// ===== 性诱监测仪 =====
const sexChartMode = ref("line");
const sexTab = ref("data");
const sexReportTime = ref("2026-09-09 22:21:27");
const sexLatestMetrics = ref([
{ key: "yzc", label: "杨小舟蛾", value: "0头", color: "#5b8ff9" },
{ key: "temp", label: "空气温度", value: "12.2℃", color: "#f56c6c" },
{ key: "humi", label: "空气湿度", value: "71.7%", color: "#3f51b5" }
]);
const sexBarRects = computed(() => {
const months = pestXMonths.value;
const arr = [];
const plotW = pestW - 50;
const bw = plotW / months.length * 0.7;
for (let i = 0; i < months.length; i++) {
const v = Math.round(Math.random() * 2);
const x = 40 + (i / (months.length - 1)) * plotW + (plotW / months.length - bw) / 2;
const y = 20 + (1 - v / pestYMax) * (pestH - 60);
const h = (pestH - 60) * v / pestYMax;
arr.push({ x, y, w: bw, h: Math.max(0, h) });
}
return arr;
});
const sexLinePoints = computed(() => {
const months = pestXMonths.value;
const plotW = pestW - 50;
const pts = [];
for (let i = 0; i < months.length; i++) {
const v = Math.round(Math.random() * 2);
const x = 40 + (i / (months.length - 1)) * plotW;
const y = 20 + (1 - v / pestYMax) * (pestH - 60);
pts.push(x + "," + y);
}
return pts.join(" ");
});
const sexLineDots = computed(() => {
const months = pestXMonths.value;
const plotW = pestW - 50;
const arr = [];
for (let i = 0; i < months.length; i++) {
const v = Math.round(Math.random() * 2);
const x = 40 + (i / (months.length - 1)) * plotW;
const y = 20 + (1 - v / pestYMax) * (pestH - 60);
arr.push({ x, y });
}
return arr;
});
const sexTableRows = computed(() => {
const months = pestXMonths.value;
return months.map((m, i) => ({
time: "2026-" + m + " 12:00",
value: Math.round(Math.random() * 2)
}));
});
const sexThumbs = computed(() => {
return [
{ key: "yzc", label: "杨小舟蛾", color: "#5b8ff9", bg: lampBg[0], time: "2026-09-09 22:21:27" },
{ key: "yzc1", label: "杨小舟蛾", color: "#5b8ff9", bg: lampBg[1], time: "2026-09-09 18:00:15" },
{ key: "yzc2", label: "杨小舟蛾", color: "#5b8ff9", bg: lampBg[2], time: "2026-09-09 14:30:08" }
];
});
// ===== 性诱图片分页 =====
const sexImages = computed(() => {
const arr = [];
for (let i = 1; i <= 24; i++) {
const bgIdx = (i - 1) % lampBg.length;
arr.push({
no: i,
bg: lampBg[bgIdx],
time: "2026-09-09 " + (23 - Math.floor(i / 2)).toString().padStart(2, "0") + ":" + ((i % 2) * 30 + 10).toString().padStart(2, "0") + ":00"
});
}
return arr;
});
const sexImagePage = ref(1);
const sexImagePageSize = ref(12);
const sexImageJump = ref(1);
const sexImageTotalPages = computed(() => Math.max(1, Math.ceil(sexImages.value.length / sexImagePageSize.value)));
const pagedSexImages = computed(() => {
const start = (sexImagePage.value - 1) * sexImagePageSize.value;
return sexImages.value.slice(start, start + sexImagePageSize.value);
});
const sexImagePageNumbers = computed(() => {
const total = sexImageTotalPages.value;
const cur = sexImagePage.value;
if (total <= 7) return Array.from({ length: total }, (_, i) => i + 1);
if (cur <= 4) return [1, 2, 3, 4, 5, "…", total];
if (cur >= total - 3) return [1, "…", total - 4, total - 3, total - 2, total - 1, total];
return [1, "…", cur - 1, cur, cur + 1, "…", total];
});
const goSexImagePage = (p) => {
if (p === "…" || p < 1 || p > sexImageTotalPages.value) return;
sexImagePage.value = p;
sexImageJump.value = p;
};
watch(sexImageJump, (val) => {
if (val >= 1 && val <= sexImageTotalPages.value) sexImagePage.value = val;
});
const onToggleAllPests = (e) => {
if (e.target.checked) selectedPests.value = pestFocusTypes.map((t) => t.key);
else selectedPests.value = [];
};
const pestW = 700;
const pestH = 320;
const pestXMonths = computed(() => {
const start = dateStart.value ? new Date(dateStart.value) : new Date(2026, 0, 1);
const end = dateEnd.value ? new Date(dateEnd.value) : new Date(2026, 8, 10);
const totalDays = Math.max(1, Math.floor((end - start) / 86400000));
const step = Math.max(1, Math.floor(totalDays / 10));
const arr = [];
for (let i = 0; i <= 10; i++) {
const d = new Date(start.getTime() + i * step * 86400000);
const mm = (d.getMonth() + 1).toString().padStart(2, "0");
const dd = d.getDate().toString().padStart(2, "0");
arr.push(mm + "-" + dd);
}
return arr;
});
const pestYMax = 70;
const pestYStep = 10;
const pestXLines = computed(() => {
const arr = pestXMonths.value;
return arr.map((_, i) => 40 + (i / (arr.length - 1)) * (pestW - 50));
});
const pestXLabels = computed(() => {
const arr = pestXMonths.value;
return arr.map((m, i) => ({ pos: 40 + (i / (arr.length - 1)) * (pestW - 50), label: m }));
});
const pestYLines = computed(() => {
const arr = [];
for (let v = 0; v <= pestYMax; v += pestYStep) arr.push(20 + (1 - v / pestYMax) * (pestH - 60));
return arr;
});
const pestYLabels = computed(() => {
const arr = [];
for (let v = 0; v <= pestYMax; v += pestYStep) arr.push(v);
return arr;
});
const pestSeries = computed(() => {
const arr = pestXMonths.value;
const len = arr.length;
const out = {};
for (const t of pestTypes) {
const pts = [];
for (let i = 0; i < len; i++) {
let v = 0;
if (i > 8) v = Math.max(0, Math.sin((i - 8) * 0.5) * 15 + Math.random() * 25 + (t.key === "lc" ? 18 : 5));
if (i > 14) v = Math.max(v, Math.random() * 50 + (t.key === "lc" ? 30 : 8));
if (i > 19) v = Math.max(0, Math.sin((i - 19) * 0.7) * 8 + Math.random() * 18);
v = Math.min(pestYMax, Math.round(v));
const x = 40 + (i / (len - 1)) * (pestW - 50);
const y = 20 + (1 - v / pestYMax) * (pestH - 60);
pts.push(x + "," + y);
}
out[t.key] = pts.join(" ");
}
return out;
});
const pestLines = computed(() => {
return pestTypes.filter((t) => selectedPests.value.includes(t.key)).map((t) => ({
key: t.key,
color: t.color,
points: pestSeries.value[t.key]
}));
});
const imageItems = computed(() => {
return Array.from({ length: 24 }).map((_, i) => {
const date = new Date(2026, 8, 9, 9, 0, 10);
date.setMinutes(date.getMinutes() + (23 - i) * 30);
const y = date.getFullYear();
const mo = String(date.getMonth() + 1).padStart(2, "0");
const d = String(date.getDate()).padStart(2, "0");
const hh = String(date.getHours()).padStart(2, "0");
const mm = String(date.getMinutes()).padStart(2, "0");
const ss = String(date.getSeconds()).padStart(2, "0");
return { no: i + 1, time: y + "-" + mo + "-" + d + " " + hh + ":" + mm + ":" + ss };
});
});
const imagePage = ref(1);
const imagePageSize = ref(12);
const imageJump = ref(1);
const imageTotalPages = computed(() => Math.max(1, Math.ceil(imageItems.value.length / imagePageSize.value)));
const lampItems = computed(() => {
return Array.from({ length: 24 }).map((_, i) => {
const date = new Date(2026, 8, 9, 9, 0, 10);
date.setMinutes(date.getMinutes() + (23 - i) * 30);
const y = date.getFullYear();
const mo = String(date.getMonth() + 1).padStart(2, "0");
const d = String(date.getDate()).padStart(2, "0");
const hh = String(date.getHours()).padStart(2, "0");
const mm = String(date.getMinutes()).padStart(2, "0");
const ss = String(date.getSeconds()).padStart(2, "0");
return { no: i + 1, time: y + "-" + mo + "-" + d + " " + hh + ":" + mm + ":" + ss };
});
});
const lampPage = ref(1);
const lampPageSize = ref(12);
const lampJump = ref(1);
const lampTotalPages = computed(() => Math.max(1, Math.ceil(lampItems.value.length / lampPageSize.value)));
const pagedLampItems = computed(() => {
const start = (lampPage.value - 1) * lampPageSize.value;
return lampItems.value.slice(start, start + lampPageSize.value);
});
const lampPageNumbers = computed(() => {
const total = lampTotalPages.value;
const cur = lampPage.value;
if (total <= 7) return Array.from({ length: total }, (_, i) => i + 1);
const arr = [1];
if (cur > 4) arr.push("…");
for (let i = Math.max(2, cur - 2); i <= Math.min(total - 1, cur + 2); i++) arr.push(i);
if (cur < total - 3) arr.push("…");
arr.push(total);
return arr;
});
const goLampPage = (p) => {
if (p === "…") return;
lampPage.value = Math.max(1, Math.min(lampTotalPages.value, +p));
};
watch(onlyFocus, (val) => {
if (val) selectedPests.value = pestFocusTypes.map((t) => t.key);
else selectedPests.value = pestTypes.map((t) => t.key);
});
watch(lampJump, (v) => {
if (typeof v === "number" && v >= 1 && v <= lampTotalPages.value) lampPage.value = v;
});
const pagedImageItems = computed(() => {
const start = (imagePage.value - 1) * imagePageSize.value;
return imageItems.value.slice(start, start + imagePageSize.value);
});
const imagePageNumbers = computed(() => {
const total = imageTotalPages.value;
const cur = imagePage.value;
if (total <= 7) return Array.from({ length: total }, (_, i) => i + 1);
const arr = [1];
if (cur > 4) arr.push("…");
for (let i = Math.max(2, cur - 2); i <= Math.min(total - 1, cur + 2); i++) arr.push(i);
if (cur < total - 3) arr.push("…");
arr.push(total);
return arr;
});
const goImagePage = (p) => {
if (p === "…") return;
imagePage.value = Math.max(1, Math.min(imageTotalPages.value, +p));
};
watch(imageJump, (v) => {
if (typeof v === "number" && v >= 1 && v <= imageTotalPages.value) imagePage.value = v;
});
const typeChildren = (key) => devices.filter((d) => d.type === key);
const stationChildren = (key) => {
if (key === "g1") return devices.filter((d) => d.station === "1号大棚");
if (key === "g2") return devices.filter((d) => d.station === "2号大棚");
return [];
};
const onSelectType = (g) => { activeTypeKey.value = g.key; };
const onSelectStation = (g) => { activeStationKey.value = g.key; };
const toggleType = (key) => {
const g = typeGroups.find((x) => x.key === key);
if (g) g.expanded = !g.expanded;
};
const toggleStation = (key) => {
const g = stationGroups.find((x) => x.key === key);
if (g) g.expanded = !g.expanded;
};
const router = useRouter();
const onSelectDevice = (d) => {
activeDeviceId.value = d.id;
hoverIdx.value = -1;
};
// 时间筛选
const timeTabs = [
{ key: "m1", name: "近一月", months: 1 },
{ key: "m3", name: "近三月", months: 3 },
{ key: "y", name: "今年", months: 12 }
];
const activeTime = ref("m1");
const dateStart = ref("2026-08-09");
const dateEnd = ref("2026-09-09");
function fmtDate(d) {
const pad = (n) => String(n).padStart(2, "0");
return d.getFullYear() + "-" + pad(d.getMonth() + 1) + "-" + pad(d.getDate());
}
const onPickTime = (t) => {
activeTime.value = t.key;
const end = new Date("2026-09-09");
const start = new Date(end);
start.setMonth(end.getMonth() - t.months);
if (t.key === "y") {
dateStart.value = end.getFullYear() + "-01-01";
} else {
dateStart.value = fmtDate(start);
}
dateEnd.value = fmtDate(end);
};
const onQuery = () => { /* 重新触发响应即可 */ };
const metrics = [
{ key: "airTemp", label: "空气温度", value: "13.5", unit: "℃", yMin: 0, yMax: 40, yStep: 10, icon: "Histogram" },
{ key: "airHumi", label: "空气湿度", value: "92.1", unit: "%", yMin: 0, yMax: 100, yStep: 20, icon: "Goblet" },
{ key: "windDir", label: "风向风速", value: "0.0", unit: "°", yMin: 0, yMax: 360, yStep: 60, icon: "Aim" },
{ key: "windSpd", label: "风速", value: "0.0", unit: "m/s", yMin: 0, yMax: 15, yStep: 3, icon: "Promotion" },
{ key: "rain", label: "累计降水量", value: "0.9", unit: "mm", yMin: 0, yMax: 50, yStep: 10, icon: "HeavyRain" },
{ key: "light", label: "光照强度", value: "24020.6", unit: "LX", yMin: 0, yMax: 150000, yStep: 30000, icon: "Sunny" },
{ key: "pres", label: "大气压", value: "85.5", unit: "kpa", yMin: 70, yMax: 110, yStep: 10, icon: "DataLine" },
{ key: "dew", label: "露点温度", value: "12.3", unit: "℃", yMin: -10, yMax: 30, yStep: 10, icon: "Drizzling" },
{ key: "soilT20", label: "土壤温度20CM", value: "17.0", unit: "℃", yMin: 0, yMax: 40, yStep: 10, icon: "Refrigerator" },
{ key: "soilT40", label: "土壤温度40cm", value: "17.8", unit: "℃", yMin: 0, yMax: 40, yStep: 10, icon: "Refrigerator" },
{ key: "soilT60", label: "土壤温度60cm", value: "19.7", unit: "℃", yMin: 0, yMax: 40, yStep: 10, icon: "Refrigerator" },
{ key: "soilT80", label: "土壤温度80cm", value: "19.9", unit: "℃", yMin: 0, yMax: 40, yStep: 10, icon: "Refrigerator" },
{ key: "soilM20", label: "土壤水分20cm", value: "25.6", unit: "%RH", yMin: 0, yMax: 100, yStep: 20, icon: "Cloudy" },
{ key: "soilM40", label: "土壤水分40cm", value: "13.3", unit: "%RH", yMin: 0, yMax: 100, yStep: 20, icon: "Cloudy" },
{ key: "soilM60", label: "土壤水分60cm", value: "14.4", unit: "%RH", yMin: 0, yMax: 100, yStep: 20, icon: "Cloudy" },
{ key: "soilM80", label: "土壤水分80cm", value: "17.1", unit: "%RH", yMin: 0, yMax: 100, yStep: 20, icon: "Cloudy" }
];
const activeMetric = ref(metrics[0]);
const chartMode = ref("line");
const dataCount = 30;
function genSeries(metricKey) {
const m = metrics.find((x) => x.key === metricKey);
const range = m.yMax - m.yMin;
const arr = [];
let seed = metricKey.charCodeAt(0) + metricKey.charCodeAt(metricKey.length - 1);
function rand() { seed = (seed * 9301 + 49297) % 233280; return seed / 233280; }
let v = m.yMin + range * 0.65 + (rand() - 0.5) * range * 0.3;
for (let i = 0; i < dataCount; i++) {
v += (rand() - 0.5) * range * 0.18;
if (v > m.yMax * 0.95) v = m.yMax * 0.95;
if (v < m.yMin + range * 0.05) v = m.yMin + range * 0.05;
if (i > 18) v -= range * 0.012;
arr.push(+v.toFixed(1));
}
return { arr, m };
}
const series = computed(() => genSeries(activeMetric.value.key).arr);
const currentMetric = computed(() => metrics.find((x) => x.key === activeMetric.value.key));
const chartW = 720;
const chartH = 360;
const plotLeft = 40;
const plotRight = chartW - 10;
const plotTop = 20;
const plotBottom = chartH - 60; // 底部留 60 单位给旋转后的标签
const plotW = plotRight - plotLeft;
const plotH = plotBottom - plotTop;
const yLines = computed(() => {
const m = currentMetric.value;
const arr = [];
for (let v = m.yMin; v <= m.yMax + 0.001; v += m.yStep) {
arr.push(plotTop + (1 - (v - m.yMin) / (m.yMax - m.yMin)) * plotH);
}
return arr;
});
const yLabels = computed(() => {
const m = currentMetric.value;
const arr = [];
for (let v = m.yMin; v <= m.yMax + 0.001; v += m.yStep) arr.push(v);
return arr;
});
// X 轴标签(13 条,均匀分布在绘图区宽度上;每条对应一条完整的竖网格线)
const xLabelsRaw = [
"08-09 00:00", "08-11 08:00", "08-13 15:00", "08-15 23:00",
"08-18 07:00", "08-20 14:00", "08-22 21:00", "08-25 04:00",
"08-28 08:00", "08-31 07:00", "09-03 00:00", "09-05 23:00",
"09-08 21:00"
];
const xLabels = computed(() => xLabelsRaw.map((text, i) => ({
pos: plotLeft + (i / (xLabelsRaw.length - 1)) * plotW,
text
})));
const xGridLines = computed(() => xLabels.value.map((x) => x.pos));
const dotPoints = computed(() => {
const m = currentMetric.value;
return series.value.map((v, i) => ({
x: plotLeft + (i / (dataCount - 1)) * plotW,
y: plotTop + (1 - (v - m.yMin) / (m.yMax - m.yMin)) * plotH
}));
});
const linePoints = computed(() => dotPoints.value.map((p) => p.x + "," + p.y).join(" "));
const barPoints = computed(() => {
const m = currentMetric.value;
const barW = plotW / dataCount * 0.7;
const baseY = plotTop + (1 - (0 - m.yMin) / (m.yMax - m.yMin)) * plotH;
return series.value.map((v, i) => ({
x: plotLeft + (i / (dataCount - 1)) * plotW,
y: plotTop + (1 - (v - m.yMin) / (m.yMax - m.yMin)) * plotH,
w: barW,
h: Math.max(1, baseY - (plotTop + (1 - (v - m.yMin) / (m.yMax - m.yMin)) * plotH))
}));
});
// 鼠标悬停交互
const hoverIdx = ref(-1);
const tipLeft = ref(0);
const tipTop = ref(0);
function onChartMove(evt) {
const svg = evt.currentTarget;
const rect = svg.getBoundingClientRect();
const ratio = chartW / rect.width;
const x = (evt.clientX - rect.left) * ratio;
let best = 0;
let minDist = Infinity;
for (let i = 0; i < dotPoints.value.length; i++) {
const dx = Math.abs(dotPoints.value[i].x - x);
if (dx < minDist) { minDist = dx; best = i; }
}
hoverIdx.value = best;
// 计算浮层位置:屏幕像素
const screenX = (dotPoints.value[best].x / ratio);
const screenY = (dotPoints.value[best].y / ratio);
const w = rect.width;
const h = rect.height;
const tipW = 180;
const tipH = 70;
// 左侧放不下时放到右侧
let left = screenX + 12;
if (left + tipW > w - 8) left = screenX - tipW - 12;
let top = screenY - tipH - 8;
if (top < 4) top = screenY + 8;
tipLeft.value = left;
tipTop.value = top;
}
function onChartClick(evt) {
onChartMove(evt);
}
// 表格模式
const totalRows = 675;
const tablePageSize = ref(10);
const tablePage = ref(1);
const tableJump = ref(1);
const tableTotalPages = computed(() => Math.max(1, Math.ceil(totalRows / tablePageSize.value)));
watch(tablePageSize, () => { tablePage.value = 1; });
const tablePageNumbers = computed(() => {
const t = tableTotalPages.value;
const cur = tablePage.value;
const set = new Set([1, t, cur - 1, cur, cur + 1, cur + 2]);
const arr = [...set].filter((p) => p >= 1 && p <= t).sort((a, b) => a - b);
const out = [];
for (let i = 0; i < arr.length; i++) {
if (i > 0 && arr[i] - arr[i - 1] > 1) out.push("…");
out.push(arr[i]);
}
return out;
});
function buildTableRows() {
const rows = [];
const now = new Date("2026-09-09T16:00:00");
const m = currentMetric.value;
let seed = m.key.charCodeAt(0) + 17;
function rand() { seed = (seed * 9301 + 49297) % 233280; return seed / 233280; }
for (let i = 0; i < totalRows; i++) {
const t = new Date(now.getTime() - i * 60 * 60 * 1000);
const pad = (n) => String(n).padStart(2, "0");
const time = `${t.getFullYear()}-${pad(t.getMonth() + 1)}-${pad(t.getDate())} ${pad(t.getHours())}:00:00`;
const v = m.yMin + (m.yMax - m.yMin) * (0.4 + rand() * 0.5);
rows.push({ idx: i + 1, time, value: v.toFixed(2) });
}
return rows;
}
const allTableRows = computed(() => buildTableRows());
const tableData = computed(() => {
const start = (tablePage.value - 1) * tablePageSize.value;
return allTableRows.value.slice(start, start + tablePageSize.value);
});
const goTablePage = (p) => {
if (p === "…") return;
if (p < 1 || p > tableTotalPages.value) return;
tablePage.value = p;
};
const reportTime = ref("2026-09-09 15:00:00");
const onDownload = () => {
const m = currentMetric.value;
const lines = ["序号,采集时间," + m.label];
for (const r of allTableRows.value) {
lines.push([r.idx, r.time, r.value].join(","));
}
const blob = new Blob(["\ufeff" + lines.join("\n")], { type: "text/csv;charset=utf-8" });
const a = document.createElement("a");
a.href = URL.createObjectURL(blob);
a.download = `${activeDevice.value.name}-${m.label}-${dateStart.value}_${dateEnd.value}.csv`;
a.click();
URL.revokeObjectURL(a.href);
};
// 暴露给其它模块使用(如设备总览同步设备列表)
import { defineExpose } from "vue";
defineExpose({ devices });
// 远程实时监控:实时时钟、快照、PTZ
const nowStr = ref("");
let clockTimer = null;
function tick() {
const d = new Date();
const pad = (n) => String(n).padStart(2, "0");
const week = ["日", "一", "二", "三", "四", "五", "六"][d.getDay()];
nowStr.value = `${d.getFullYear()}年${pad(d.getMonth() + 1)}月${pad(d.getDate())}日 星期${week} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
}
onMounted(() => { tick(); clockTimer = setInterval(tick, 1000); });
import { onBeforeUnmount } from "vue";
onBeforeUnmount(() => { if (clockTimer) clearInterval(clockTimer); });
const snapshots = ref([
{ id: 1, label: "快照1", time: "09-09 17:05:33" },
{ id: 2, label: "快照2", time: "09-09 16:35:33" },
{ id: 3, label: "快照3", time: "09-09 16:05:33" }
]);
const activeSnapId = ref(1);
// 历史图片:94 条,按 30 分钟间隔
const remoteTab = ref("live");
const toggleRemoteTab = (which) => {
if (which === "live" || which === "history") {
remoteTab.value = which;
} else {
remoteTab.value = remoteTab.value === "live" ? "history" : "live";
}
};
const historyPage = ref(1);
const historyPageSize = ref(12);
const historyJump = ref(1);
const historyImgs = ref((() => {
const arr = [];
const start = new Date("2026-08-09 00:00:00");
for (let i = 0; i < 94; i++) {
const t = new Date(start.getTime() + i * 30 * 60 * 1000);
const pad = (n) => String(n).padStart(2, "0");
const time = `${pad(t.getMonth() + 1)}-${pad(t.getDate())} ${pad(t.getHours())}:${pad(t.getMinutes())}:${pad(t.getSeconds())}`;
arr.push({ id: i + 1, time });
}
return arr;
})());
const historyTotalPages = computed(() => Math.max(1, Math.ceil(historyImgs.value.length / historyPageSize.value)));
const pagedHistory = computed(() => {
const start = (historyPage.value - 1) * historyPageSize.value;
return historyImgs.value.slice(start, start + historyPageSize.value);
});
const historyPageNumbers = computed(() => {
const total = historyTotalPages.value;
const cur = historyPage.value;
const arr = [];
if (total <= 7) {
for (let i = 1; i <= total; i++) arr.push(i);
} else {
arr.push(1);
if (cur > 4) arr.push("…");
const s = Math.max(2, cur - 2);
const e = Math.min(total - 1, cur + 2);
for (let i = s; i <= e; i++) arr.push(i);
if (cur < total - 3) arr.push("…");
arr.push(total);
}
return arr;
});
const goHistoryPage = (p) => {
if (p === "…") return;
if (p < 1 || p > historyTotalPages.value) return;
historyPage.value = p;
};
// 历史图片选中:高亮对应 id(activeSnapId 供下方快照区联动使用)
const ptzAct = (dir) => { /* placeholder */ };
const remoteBg = {
13: "linear-gradient(135deg, #6dd5ed 0%, #2193b0 50%, #f7b733 100%)",
14: "linear-gradient(135deg, #84fab0 0%, #8fd3f4 100%)",
15: "linear-gradient(135deg, #f6d365 0%, #fda085 100%)",
16: "linear-gradient(135deg, #a18cd1 0%, #fbc2eb 100%)"
};
</script>
<style scoped>
.monitor-data {
display: flex;
height: calc(100vh - 80px);
background: #f5f6f8;
box-sizing: border-box;
}
/* ===== 左侧栏 ===== */
.md-sidebar {
width: 280px;
flex-shrink: 0;
background: #fff;
border-right: 1px solid #ebeef5;
display: flex;
flex-direction: column;
}
.md-side-tabs {
display: flex;
border-bottom: 1px solid #ebeef5;
background: #fafafa;
}
.md-side-tab {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
padding: 12px 0;
font-size: 14px;
color: #555;
cursor: pointer;
border-right: 1px solid #ebeef5;
transition: all 0.15s;
}
.md-side-tab:last-child { border-right: 0; }
.md-side-tab:hover { color: #d71e1e; background: #fef2f2; }
.md-side-tab.active {
background: #fff;
color: #d71e1e;
font-weight: 600;
border-bottom: 2px solid #d71e1e;
margin-bottom: -1px;
}
.md-side-list {
flex: 1;
overflow-y: auto;
padding: 8px 0;
}
.md-side-list::-webkit-scrollbar { width: 4px; }
.md-side-list::-webkit-scrollbar-thumb { background: #d4d4d4; border-radius: 2px; }
.md-side-row {
display: flex;
align-items: center;
padding: 10px 14px;
font-size: 14px;
color: #333;
cursor: pointer;
transition: background 0.15s;
}
.md-side-row:hover { background: #fafafa; }
.md-side-row.active { background: #fef2f2; color: #d71e1e; font-weight: 600; }
.row-name {
flex: 1;
display: flex;
align-items: center;
gap: 8px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.row-name .dot {
width: 4px;
height: 14px;
border-radius: 2px;
}
.row-meta {
display: inline-flex;
align-items: center;
gap: 4px;
color: #888;
font-size: 12px;
}
.row-chevron {
font-size: 12px;
color: #aaa;
transition: transform 0.2s;
cursor: pointer;
padding: 4px;
}
.row-chevron:hover { color: #d71e1e; }
.md-sub-list {
background: #fafafa;
padding: 4px 0 6px;
}
.md-sub-item {
display: flex;
align-items: center;
gap: 10px;
padding: 8px 14px 8px 20px;
font-size: 13px;
color: #555;
cursor: pointer;
transition: background 0.15s;
}
.md-sub-item:hover { background: #fef2f2; color: #d71e1e; }
.md-sub-item.active { background: #fff5f5; color: #d71e1e; font-weight: 600; }
.sub-thumb { flex-shrink: 0; }
.thumb-ico {
width: 38px;
height: 38px;
border-radius: 4px;
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 20px;
}
.sub-info { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.sub-name { font-weight: 500; color: inherit; }
.sub-station { font-size: 12px; color: #888; }
/* ===== 右侧主区 ===== */
.md-main {
flex: 1;
min-width: 0;
overflow-y: auto;
padding: 14px 18px;
background: #f5f6f8;
}
.md-main::-webkit-scrollbar { width: 6px; }
.md-main::-webkit-scrollbar-thumb { background: #d4d4d4; border-radius: 3px; }
.md-head {
display: flex;
align-items: center;
margin-bottom: 12px;
}
.md-tag {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 6px 14px;
background: #e6a23c;
color: #fff;
border-radius: 3px;
font-size: 14px;
font-weight: 600;
}
/* ===== 筛选行 ===== */
.md-filter {
display: flex;
align-items: center;
gap: 10px;
padding: 12px 16px;
background: #fff;
border: 1px solid #f0f0f0;
border-radius: 4px;
margin-bottom: 14px;
flex-wrap: wrap;
}
.f-label { color: #555; font-size: 14px; white-space: nowrap; }
.time-tabs { display: inline-flex; gap: 4px; }
.time-tab {
padding: 5px 14px;
background: #fafafa;
border: 1px solid #ebeef5;
border-radius: 3px;
font-size: 13px;
color: #555;
cursor: pointer;
transition: all 0.15s;
}
.time-tab:hover { color: #d71e1e; border-color: #d71e1e; }
.time-tab.active {
background: #d71e1e;
color: #fff;
border-color: #d71e1e;
}
.f-date {
height: 28px;
padding: 0 8px;
border: 1px solid #dcdfe6;
border-radius: 3px;
font-size: 13px;
background: #fff;
}
.f-date:focus { border-color: #d71e1e; outline: none; }
.f-dash { color: #999; }
.f-btn {
height: 28px;
padding: 0 18px;
background: #d71e1e;
color: #fff;
border: 1px solid #d71e1e;
border-radius: 3px;
font-size: 13px;
cursor: pointer;
transition: background 0.15s;
}
.f-btn:hover { background: #b81818; }
/* ===== 面板 ===== */
.md-panel {
background: #fff;
border: 1px solid #f0f0f0;
border-radius: 4px;
margin-bottom: 14px;
}
.panel-head {
display: flex;
align-items: center;
padding: 12px 16px;
border-bottom: 1px solid #f0f0f0;
background: #fafafa;
border-radius: 4px 4px 0 0;
}
.panel-title {
font-size: 15px;
font-weight: 600;
color: #333;
}
.panel-tools {
margin-left: auto;
display: inline-flex;
align-items: center;
gap: 14px;
color: #888;
}
.panel-tools.time-tip { font-size: 13px; color: #888; font-weight: normal; }
.t-ico {
font-size: 16px;
cursor: pointer;
transition: color 0.15s;
color: #888;
}
.t-ico:hover { color: #d71e1e; }
.t-ico.active { color: #d71e1e; font-weight: 700; }
.panel-body { padding: 14px 16px; }
/* ===== 图表 ===== */
.chart-wrap { width: 100%; }
.unit-row { font-size: 12px; color: #888; margin-bottom: 8px; }
.unit-label { background: #fafafa; padding: 2px 8px; border-radius: 3px; }
.chart-main {
width: 100%;
height: 340px;
background: #fff;
position: relative;
}
.chart-svg { width: 100%; height: 100%; display: block; cursor: crosshair; }
.chart-legend {
display: flex;
align-items: center;
justify-content: center;
gap: 18px;
padding-top: 8px;
font-size: 13px;
color: #555;
}
.legend-item { display: inline-flex; align-items: center; gap: 6px; }
.legend-line { display: inline-block; width: 14px; height: 2px; }
/* ===== Tooltip ===== */
.chart-tip {
position: absolute;
pointer-events: none;
background: #fff;
border: 1px solid #ebeef5;
border-radius: 4px;
padding: 8px 10px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12);
min-width: 170px;
z-index: 10;
font-size: 12px;
}
.tip-time { color: #999; margin-bottom: 4px; }
.tip-row {
display: flex;
align-items: center;
gap: 6px;
}
.tip-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: #5b8def;
}
.tip-name { color: #555; }
.tip-value {
margin-left: auto;
font-weight: 700;
color: #d71e1e;
font-size: 14px;
}
/* ===== 表格模式 ===== */
.table-wrap { width: 100%; }
.md-table {
width: 100%;
border-collapse: collapse;
font-size: 13px;
}
.md-table thead th {
background: #fafafa;
font-weight: 600;
color: #333;
padding: 10px 12px;
text-align: center;
border-bottom: 1px solid #ebeef5;
white-space: nowrap;
}
.md-table tbody td {
padding: 10px 12px;
text-align: center;
border-bottom: 1px solid #f5f5f5;
color: #555;
}
.md-table tbody tr:hover { background: #fafafa; }
.pager {
display: flex;
align-items: center;
gap: 6px;
padding: 14px 4px 6px;
font-size: 13px;
color: #555;
justify-content: flex-end;
flex-wrap: wrap;
}
.pager .total { color: #888; padding: 0 6px; }
.pg-btn {
min-width: 28px;
height: 28px;
padding: 0 8px;
background: #fff;
border: 1px solid #ebeef5;
border-radius: 3px;
font-size: 13px;
color: #555;
cursor: pointer;
transition: all 0.15s;
}
.pg-btn:hover { color: #d71e1e; border-color: #d71e1e; }
.pg-btn.active {
background: #d71e1e;
color: #fff;
border-color: #d71e1e;
}
.pg-btn:disabled { color: #ccc; cursor: not-allowed; border-color: #ebeef5; }
.pg-size {
height: 28px;
padding: 0 6px;
border: 1px solid #ebeef5;
border-radius: 3px;
background: #fff;
font-size: 13px;
color: #555;
cursor: pointer;
}
.pg-input {
width: 50px;
height: 28px;
padding: 0 6px;
border: 1px solid #ebeef5;
border-radius: 3px;
font-size: 13px;
text-align: center;
}
/* ===== 最新数据指标卡 ===== */
.metric-grid {
display: grid;
grid-template-columns: repeat(6, 1fr);
gap: 12px;
}
.metric-card {
background: #fff;
border: 1px solid #ebeef5;
border-radius: 4px;
padding: 12px 14px;
cursor: pointer;
transition: all 0.15s;
}
.metric-card:hover { border-color: #d71e1e; }
.metric-card.active {
border-color: #5b8def;
box-shadow: 0 0 0 2px rgba(91, 141, 239, 0.15);
}
.m-label {
font-size: 13px;
color: #888;
margin-bottom: 6px;
white-space: nowrap;
}
.m-row {
display: flex;
align-items: baseline;
gap: 4px;
}
.m-value {
font-size: 18px;
font-weight: 600;
color: #333;
line-height: 1.3;
}
.m-unit {
font-size: 13px;
color: #666;
}
.m-ico {
margin-left: auto;
font-size: 14px;
color: #67c23a;
align-self: center;
}
/* ===== 远程实时监控视图 ===== */
.rm-panel {
display: flex;
gap: 12px;
background: #fff;
border: 1px solid #f0f0f0;
border-radius: 4px;
padding: 12px;
margin-bottom: 12px;
}
.rm-player {
flex: 1;
height: 420px;
border-radius: 4px;
position: relative;
overflow: hidden;
background: #000;
}
.rm-bg {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
.rm-empty {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
color: #fff;
font-size: 14px;
background: rgba(0, 0, 0, 0.25);
padding: 14px 22px;
border-radius: 4px;
}
.empty-ico { font-size: 28px; }
.rm-time {
position: absolute;
left: 12px;
top: 12px;
color: #fff;
font-size: 13px;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.5);
font-family: Consolas, "Courier New", monospace;
}
.rm-tools {
position: absolute;
left: 12px;
bottom: 12px;
display: inline-flex;
gap: 8px;
}
.r-ico {
width: 30px;
height: 30px;
border-radius: 50%;
background: rgba(0, 0, 0, 0.5);
color: #fff;
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 14px;
cursor: pointer;
}
.r-ico:hover { background: rgba(0, 0, 0, 0.7); }
.rm-ptz {
width: 110px;
background: #2b2b2b;
border-radius: 4px;
padding: 12px 8px;
display: flex;
flex-direction: column;
gap: 8px;
align-items: center;
}
.rm-pad { display: flex; flex-direction: column; align-items: center; gap: 4px; }
.rm-pad-row { display: flex; gap: 4px; }
.rm-btn {
width: 30px;
height: 30px;
border-radius: 4px;
background: #3a3a3a;
border: 1px solid #4a4a4a;
color: #fff;
font-size: 14px;
cursor: pointer;
display: inline-flex;
align-items: center;
justify-content: center;
}
.rm-btn:hover { background: #4a4a4a; }
.rm-center { background: #1f6c2f; }
.rm-center:hover { background: #258b39; }
.rm-row-line {
display: flex;
align-items: center;
gap: 6px;
width: 100%;
justify-content: center;
}
.rm-tag { color: #ccc; font-size: 12px; width: 14px; text-align: center; }
.rm-slider {
position: relative;
flex: 1;
height: 4px;
background: #1a1a1a;
border-radius: 2px;
}
.slider-fill {
position: absolute;
top: 0;
bottom: 0;
background: #67c23a;
border-radius: 2px;
}
.slider-thumb {
position: absolute;
top: -3px;
width: 8px;
height: 10px;
background: #67c23a;
border-radius: 2px;
transform: translateX(-50%);
}
.hd-btn {
width: 38px;
height: 24px;
border-radius: 3px;
background: #3a3a3a;
border: 1px solid #4a4a4a;
color: #fff;
font-size: 12px;
cursor: pointer;
}
.hd-btn:hover { background: #4a4a4a; }
.rm-snap-panel {
background: #fff;
border: 1px solid #f0f0f0;
border-radius: 4px;
padding: 12px;
}
.snap-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12px;
}
.snap-card {
background: #fafafa;
border: 1px solid #ebeef5;
border-radius: 4px;
cursor: pointer;
transition: all 0.15s;
overflow: hidden;
}
.snap-card:hover { border-color: #67c23a; }
.snap-card.active { border-color: #67c23a; box-shadow: 0 0 0 2px rgba(103, 194, 58, 0.15); }
.snap-img {
width: 100%;
height: 160px;
display: flex;
align-items: center;
justify-content: center;
}
.snap-empty {
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
color: #fff;
font-size: 13px;
background: rgba(0, 0, 0, 0.25);
padding: 10px 16px;
border-radius: 4px;
}
.snap-meta {
padding: 8px 12px;
font-size: 13px;
color: #555;
border-top: 1px solid #ebeef5;
background: #fff;
}
/* ===== 历史图片按钮 + 弹窗 ===== */
.history-btn {
display: inline-flex;
align-items: center;
gap: 6px;
margin-left: 12px;
padding: 6px 14px;
background: #fff;
border: 1px solid #d71e1e;
border-radius: 3px;
color: #d71e1e;
font-size: 13px;
cursor: pointer;
transition: all 0.15s;
}
.history-btn:hover { background: #fef2f2; }
.history-btn .el-icon { font-size: 14px; }
.history-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 12px;
padding: 4px 0 16px;
}
.history-card {
background: #fafafa;
border: 1px solid #ebeef5;
border-radius: 4px;
cursor: pointer;
overflow: hidden;
transition: all 0.15s;
}
.history-card:hover { border-color: #d71e1e; box-shadow: 0 2px 8px rgba(215, 30, 30, 0.1); }
.history-img {
width: 100%;
height: 130px;
display: flex;
align-items: center;
justify-content: center;
}
.history-empty {
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
color: #fff;
font-size: 13px;
background: rgba(0, 0, 0, 0.25);
padding: 8px 14px;
border-radius: 4px;
}
.history-meta {
padding: 6px 10px;
font-size: 12px;
color: #555;
border-top: 1px solid #ebeef5;
background: #fff;
}
.history-pager {
display: flex;
align-items: center;
gap: 6px;
padding: 12px 0 4px;
border-top: 1px solid #ebeef5;
}
.pg-btn {
min-width: 30px;
height: 28px;
padding: 0 8px;
border: 1px solid #dcdfe6;
background: #fff;
border-radius: 3px;
font-size: 13px;
cursor: pointer;
color: #555;
}
.pg-btn:hover:not(:disabled) { color: #d71e1e; border-color: #d71e1e; }
.pg-btn:disabled { opacity: 0.4; cursor: not-allowed; }
.pg-btn.active { background: #d71e1e; color: #fff; border-color: #d71e1e; }
.pg-size {
height: 28px;
padding: 0 8px;
border: 1px solid #dcdfe6;
border-radius: 3px;
background: #fff;
font-size: 13px;
cursor: pointer;
margin-left: 6px;
}
.pg-input {
width: 56px;
height: 28px;
padding: 0 6px;
border: 1px solid #dcdfe6;
border-radius: 3px;
font-size: 13px;
text-align: center;
}
.pg-input:focus { border-color: #d71e1e; outline: none; }
.history-pager .total {
color: #555;
font-size: 13px;
margin: 0 4px;
}
/* ===== 实时监控 / 历史图片 切换按钮 ===== */
.md-tag-btn {
margin-left: 0;
cursor: pointer;
border: 1px solid #e6a23c;
font-family: inherit;
outline: none;
transition: all 0.15s;
background: #fff;
color: #e6a23c;
}
.md-tag-btn + .md-tag-btn { margin-left: 8px; }
.md-tag-btn:hover {
background: #fdf3e3;
}
.md-tag-btn.active {
background: #e6a23c;
color: #fff;
border-color: #e6a23c;
}
.rm-history-panel {
background: #fff;
border: 1px solid #f0f0f0;
border-radius: 4px;
padding: 16px;
}
/* ===== 苗情/历史 图片视图(共用 history-panel) ===== */
.history-panel {
background: #fff;
border-radius: 6px;
display: flex;
flex-direction: column;
flex: 1;
overflow: hidden;
}
/* ===== 虫情测报灯 ===== */
.pest-body { padding: 16px 20px; }
.pest-types {
display: flex;
align-items: center;
gap: 14px;
padding-bottom: 12px;
font-size: 13px;
color: #606266;
flex-wrap: wrap;
}
.pt-label { font-weight: 600; color: #303133; }
.pt-check {
display: inline-flex;
align-items: center;
gap: 4px;
cursor: pointer;
user-select: none;
}
.pt-check input { margin: 0; cursor: pointer; }
.pest-svg {
width: 100%;
height: 280px;
background: #fff;
display: block;
}
.pest-legend {
display: flex;
align-items: center;
gap: 10px;
padding: 8px 0;
font-size: 12px;
color: #606266;
flex-wrap: wrap;
position: relative;
}
.pest-leg-item {
display: inline-flex;
align-items: center;
gap: 4px;
transition: opacity 0.15s;
}
.pest-leg-item.off { opacity: 0.3; }
.pest-leg-dot {
display: inline-block;
width: 14px;
height: 14px;
position: relative;
flex-shrink: 0;
}
.pest-leg-dot::before {
content: "";
position: absolute;
left: 0;
bottom: 4px;
width: 8px;
height: 8px;
border-radius: 50%;
background: currentColor;
}
.pest-leg-dot::after {
content: "";
position: absolute;
left: 7px;
bottom: 6px;
width: 7px;
height: 2px;
background: currentColor;
border-radius: 0 1px 1px 0;
}
.pest-page-num {
position: absolute;
right: 36px;
font-size: 12px;
color: #606266;
}
.pest-page-btn {
position: absolute;
right: 12px;
border: 1px solid #dcdfe6;
background: #fff;
width: 22px;
height: 22px;
border-radius: 3px;
cursor: pointer;
font-size: 12px;
color: #606266;
}
.pest-page-btn:hover { color: #d71e1e; border-color: #d71e1e; }
.pest-thumbs {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 12px;
margin-top: 14px;
}
.pest-thumb {
height: 130px;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
color: #67c23a;
font-size: 28px;
border: 1px solid #ebeef5;
position: relative;
overflow: hidden;
cursor: pointer;
transition: all 0.15s;
}
.pest-thumb:hover {
border-color: #d71e1e;
box-shadow: 0 2px 8px rgba(215, 30, 30, 0.1);
}
.pest-thumb-more {
background: linear-gradient(135deg, #f0f0f0, #d6d6d6) !important;
cursor: pointer;
color: #fff;
font-size: 13px;
font-weight: 500;
transition: all 0.15s;
}
.pest-thumb-more:hover { background: linear-gradient(135deg, #d71e1e, #f29200) !important; color: #fff; }
/* ===== md-head 右侧开关 ===== */
.pest-only-switch {
margin-left: auto;
display: flex;
align-items: center;
gap: 8px;
padding: 0 4px;
}
.pest-only-label {
font-size: 13px;
color: #303133;
font-weight: 500;
white-space: nowrap;
}
.pest-thumb-meta {
position: absolute;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.55);
color: #fff;
font-size: 12px;
padding: 4px 8px;
text-align: left;
}
.pest-thumb-more {
cursor: pointer;
transition: all 0.15s;
position: relative;
}
.pest-thumb-more:hover {
filter: brightness(1.05);
box-shadow: 0 4px 12px rgba(215, 30, 30, 0.25);
}
.pest-thumb-more-text {
position: absolute;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.55);
color: #fff;
font-size: 12px;
padding: 4px 8px;
text-align: left;
}
/* ===== 关注害虫筛选行(位于时间筛选下方) ===== */
.md-pest-filter {
display: flex;
align-items: center;
gap: 14px;
padding: 10px 20px;
background: #fff;
border-bottom: 1px solid #ebeef5;
font-size: 13px;
color: #606266;
flex-wrap: wrap;
}
.pf-label {
font-weight: 600;
color: #303133;
}
.pf-check {
display: inline-flex;
align-items: center;
gap: 4px;
cursor: pointer;
user-select: none;
}
.pf-check input { margin: 0; cursor: pointer; }
/* ===== 时间筛选 + 关注害虫 同一框 ===== */
.f-divider {
width: 1px;
height: 20px;
background: #dcdfe6;
margin: 0 8px;
}
.md-filter .pf-check {
display: inline-flex;
align-items: center;
gap: 4px;
cursor: pointer;
user-select: none;
font-size: 13px;
color: #606266;
}
.md-filter .pf-check input {
margin: 0;
cursor: pointer;
}
/* ===== 关注害虫独立行 ===== */
.md-pest-filter {
display: flex;
align-items: center;
gap: 14px;
padding: 12px 20px;
background: #fff;
border-bottom: 1px solid #ebeef5;
font-size: 13px;
color: #606266;
flex-wrap: wrap;
}
.pf-label {
font-weight: 600;
color: #303133;
}
.md-pest-filter .pf-check {
display: inline-flex;
align-items: center;
gap: 4px;
cursor: pointer;
user-select: none;
}
.md-pest-filter .pf-check input { margin: 0; cursor: pointer; }
/* ===== 性诱监测仪 ===== */
.sex-meta {
display: flex;
align-items: center;
gap: 12px;
padding: 8px 0;
}
.sex-meta-ico {
width: 32px;
height: 32px;
border-radius: 6px;
background: #e8f3ff;
color: #5b8ff9;
padding: 6px;
font-size: 20px;
}
.sex-meta-title {
font-size: 14px;
font-weight: 600;
color: #303133;
}
.sex-meta-time {
font-size: 12px;
color: #909399;
margin-top: 2px;
}
.sex-latest-grid {
display: flex;
align-items: center;
gap: 28px;
margin-left: auto;
}
.sex-latest-cell {
text-align: right;
}
.sex-latest-label {
font-size: 12px;
color: #909399;
}
.sex-latest-value {
font-size: 16px;
font-weight: 600;
margin-top: 4px;
}
.sex-chart-svg {
width: 100%;
height: 320px;
background: #fff;
display: block;
}
.chart-wrap {
width: 100%;
position: relative;
}
.unit-row {
font-size: 12px;
color: #909399;
padding: 4px 0 8px 0;
}
.table-wrap {
width: 100%;
padding: 8px 0;
}
.data-table {
width: 100%;
border-collapse: collapse;
font-size: 13px;
}
.data-table th {
background: #fafafa;
font-weight: 600;
color: #303133;
padding: 8px 12px;
text-align: left;
border-bottom: 1px solid #ebeef5;
}
.data-table td {
padding: 8px 12px;
border-bottom: 1px solid #f5f5f5;
color: #555;
}
</style>
<template> <template>
<div class="page"> <div class="page-list">
<div class="placeholder">该模块待开发</div> <div class="filter-box">
<div class="filter-row">
<div class="filter-item">
<label>抽样日期:</label>
<DatePicker v-model="filters.testStart" class="dp-160" placeholder="开始日期" />
<span class="dash">至</span>
<DatePicker v-model="filters.testEnd" class="dp-160" placeholder="结束日期" />
</div>
<div class="filter-item">
<label>检疫单证号:</label>
<input class="ipt" v-model="filters.cert" placeholder="请输入关键字" />
</div>
<div class="filter-item">
<label>植物名称:</label>
<input class="ipt" v-model="filters.plant" placeholder="请输入关键字" />
</div>
</div>
<div class="filter-row">
<div class="filter-item">
<label>品种名称:</label>
<input class="ipt" v-model="filters.variety" placeholder="请输入关键字" />
</div>
<div class="filter-item">
<label>受检单位:</label>
<input class="ipt" v-model="filters.testOrg" placeholder="请输入关键字" />
</div>
<div class="filter-item">
<button class="btn" @click="onQuery">查询</button>
<button class="btn btn-default" @click="onReset">重置</button>
</div>
<div class="filter-item filter-add">
<button class="btn btn-add" @click="onAdd">+ 新建检验</button>
</div>
</div>
</div>
<div class="tbl-wrap">
<table class="simple-table">
<colgroup>
<col style="width:5%" />
<col style="width:14%" />
<col style="width:22%" />
<col style="width:9%" />
<col style="width:9%" />
<col style="width:9%" />
<col style="width:11%" />
<col style="width:13%" />
<col style="width:8%" />
</colgroup>
<thead>
<tr>
<th>序号</th>
<th>检疫单证号</th>
<th>受检单位</th>
<th>植物名称</th>
<th>品种名称</th>
<th>抽检数量</th>
<th>抽样日期</th>
<th>检验机构</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr v-for="(r, i) in pagedData" :key="r.id">
<td>{{ (currentPage - 1) * pageSize + i + 1 }}</td>
<td>{{ r.cert }}</td>
<td class="col-unit">{{ r.unit }}</td>
<td>{{ r.plant }}</td>
<td>{{ r.variety }}</td>
<td>{{ r.qty }}</td>
<td>{{ r.testDate }}</td>
<td>{{ r.testOrg }}</td>
<td class="col-action">
<a class="op-link" @click="onView(r)">查看</a>
<a class="op-link" @click="onEdit(r)">编辑</a>
<a class="op-link" @click="onDelete(r)">删除</a>
</td>
</tr>
<tr v-if="!pagedData.length">
<td colspan="9" class="empty">暂无数据</td>
</tr>
</tbody>
</table>
</div>
<div class="pagination">
<span class="total" style="margin-right:auto;">共 {{ filteredData.length }} 条记录</span>
<button class="page-btn" :disabled="currentPage === 1" @click="goPage(1)">首页</button>
<button class="page-btn" :disabled="currentPage === 1" @click="goPage(currentPage - 1)">上一页</button>
<button v-for="p in pageNumbers" :key="p" class="page-btn" :class="{ active: p === currentPage }" @click="goPage(p)">{{ p }}</button>
<button class="page-btn" :disabled="currentPage === totalPages" @click="goPage(currentPage + 1)">下一页</button>
<button class="page-btn" :disabled="currentPage === totalPages" @click="goPage(totalPages)">末页</button>
<span class="jump">跳转至 <input class="jump-input" v-model.number="jumpInput" @keyup.enter="onJump" /> 页</span>
</div>
<div v-if="viewVisible" class="modal-mask" @click.self="viewVisible = false">
<div class="modal-box modal-box-wide">
<div class="modal-top"><span class="modal-title">农业植物检疫实验室检验报告单</span><span class="modal-close" @click="viewVisible = false">×</span></div>
<div class="modal-body">
<div class="section">
<div class="section-title">受检单位信息</div>
<div class="form-row cols-2">
<div class="form-field"><span class="label">受检单位</span><span class="ctrl mb-input">{{ form.unit }}</span></div>
<div class="form-field"><span class="label">联系人</span><span class="ctrl mb-input">{{ form.contact }}</span></div>
<div class="form-field"><span class="label">联系电话</span><span class="ctrl mb-input">{{ form.phone }}</span></div>
</div>
</div>
<div class="section">
<div class="section-title">送样单位信息</div>
<div class="form-row cols-2">
<div class="form-field"><span class="label">送样单位</span><span class="ctrl mb-input">{{ form.testOrg }}</span></div>
<div class="form-field"><span class="label">联系人</span><span class="ctrl mb-input">{{ form.sampleContact }}</span></div>
<div class="form-field"><span class="label">联系电话</span><span class="ctrl mb-input">{{ form.samplePhone }}</span></div>
</div>
</div>
<div class="section">
<div class="section-title">样本信息</div>
<div class="form-row cols-2">
<div class="form-field span-2"><span class="label">种植地点</span>
<span class="ctrl mb-input addr-row">
<span class="addr-seg">{{ form.surveyProvince }}</span>
<span class="addr-seg">{{ form.surveyCity }}</span>
<span class="addr-seg">{{ form.surveyDistrict }}</span>
<span class="addr-seg">{{ form.surveyTownship }}</span>
<span class="addr-seg flex-1">{{ form.surveyAddress }}</span>
</span>
</div>
<div class="form-field"><span class="label">植物名称</span><span class="ctrl mb-input">{{ form.plant }}</span></div>
<div class="form-field"><span class="label">抽样日期</span><span class="ctrl mb-input">{{ form.testDate }}</span></div>
<div class="form-field span-2"><span class="label">目标有害生物</span><span class="ctrl mb-input area prewrap">{{ form.targetPests }}</span></div>
<div class="form-field"><span class="label"><span class="req">*</span>收样人</span><span class="ctrl mb-input">{{ form.receiver }}</span></div>
<div class="form-field"><span class="label"><span class="req">*</span>收样日期</span><span class="ctrl mb-input">{{ form.receiveDate }}</span></div>
<div class="form-field span-2"><span class="label"><span class="req">*</span>检验方法</span><span class="ctrl mb-input area">{{ form.inspectionMethod }}</span></div>
<div class="form-field span-2"><span class="label"><span class="req">*</span>检验结果</span><span class="ctrl mb-input area">{{ form.labResult }}</span></div>
<div class="form-field span-2"><span class="label">备注</span><span class="ctrl mb-input notes">{{ form.remarks }}</span></div>
</div>
</div>
<div class="section">
<div class="section-title">受理机构信息</div>
<div class="form-row cols-2">
<div class="form-field"><span class="label">检验人</span><span class="ctrl mb-input">{{ form.inspector1 }}</span></div>
<div class="form-field"><span class="label"><span class="req">*</span>审核人</span><span class="ctrl mb-input">{{ form.reviewPerson }}</span></div>
<div class="form-field"><span class="label">检验机构</span><span class="ctrl mb-input">{{ form.testOrg }}</span></div>
<div class="form-field"><span class="label">受理日期</span><span class="ctrl mb-input">{{ form.entryDate }}</span></div>
</div>
</div>
</div>
</div>
</div>
<div v-if="editVisible" class="modal-mask" @click.self="editVisible = false">
<div class="modal-box modal-box-wide">
<div class="modal-top"><span class="modal-title">编辑检验</span><span class="modal-close" @click="editVisible = false">×</span></div>
<div class="modal-body">
<div class="section">
<div class="section-title">受检单位信息</div>
<div class="form-row cols-2">
<div class="form-field"><span class="label">受检单位</span><input class="ctrl" v-model="form.unit" /></div>
<div class="form-field"><span class="label">联系人</span><input class="ctrl" v-model="form.contact" /></div>
<div class="form-field"><span class="label">联系电话</span><input class="ctrl" v-model="form.phone" /></div>
</div>
</div>
<div class="section">
<div class="section-title">送样单位信息</div>
<div class="form-row cols-2">
<div class="form-field"><span class="label">送样单位</span><input class="ctrl" v-model="form.testOrg" /></div>
<div class="form-field"><span class="label">联系人</span><input class="ctrl" v-model="form.sampleContact" /></div>
<div class="form-field"><span class="label">联系电话</span><input class="ctrl" v-model="form.samplePhone" /></div>
</div>
</div>
<div class="section">
<div class="section-title">样本信息</div>
<div class="form-row cols-2">
<div class="form-field span-2"><span class="label">种植地点</span>
<span class="addr-row">
<input class="ctrl addr-seg" v-model="form.surveyProvince" placeholder="省" />
<input class="ctrl addr-seg" v-model="form.surveyCity" placeholder="市" />
<input class="ctrl addr-seg" v-model="form.surveyDistrict" placeholder="区/县" />
<input class="ctrl addr-seg" v-model="form.surveyTownship" placeholder="乡镇" />
<input class="ctrl addr-seg flex-1" v-model="form.surveyAddress" placeholder="请输入详细地址" />
</span>
</div>
<div class="form-field"><span class="label">植物名称</span><input class="ctrl" v-model="form.plant" /></div>
<div class="form-field"><span class="label">抽样日期</span><DatePicker v-model="form.testDate" class="ctrl" /></div>
<div class="form-field span-2"><span class="label">目标有害生物</span><textarea class="ctrl area-input prewrap" v-model="form.targetPests" rows="4" placeholder="每条有害生物占一行"></textarea></div>
<div class="form-field"><span class="label"><span class="req">*</span>收样人</span><input class="ctrl" v-model="form.receiver" /></div>
<div class="form-field"><span class="label"><span class="req">*</span>收样日期</span><DatePicker v-model="form.receiveDate" class="ctrl" /></div>
<div class="form-field span-2"><span class="label"><span class="req">*</span>检验方法</span><textarea class="ctrl area-input" v-model="form.inspectionMethod" rows="3" maxlength="500" placeholder="请输入500字以内"></textarea></div>
<div class="form-field span-2"><span class="label"><span class="req">*</span>检验结果</span><textarea class="ctrl area-input" v-model="form.labResult" rows="3" maxlength="500" placeholder="请输入500字以内"></textarea></div>
<div class="form-field span-2"><span class="label">备注</span><textarea class="ctrl area-input notes" v-model="form.remarks" rows="2" maxlength="500" placeholder="请输入500字以内"></textarea></div>
</div>
</div>
<div class="section">
<div class="section-title">受理机构信息</div>
<div class="form-row cols-2">
<div class="form-field"><span class="label">检验人</span><input class="ctrl" v-model="form.inspector1" /></div>
<div class="form-field"><span class="label"><span class="req">*</span>审核人</span><input class="ctrl" v-model="form.reviewPerson" /></div>
<div class="form-field"><span class="label">检验机构</span><input class="ctrl" v-model="form.testOrg" /></div>
<div class="form-field"><span class="label">受理日期</span><DatePicker v-model="form.entryDate" class="ctrl" /></div>
</div>
</div>
<div class="modal-actions">
<button class="btn btn-default" @click="editVisible = false">取消</button>
<button class="btn" @click="onConfirmEdit">保存</button>
</div>
</div>
</div>
</div>
<div v-if="addVisible" class="modal-mask" @click.self="addVisible = false">
<div class="modal-box modal-box-wide">
<div class="modal-top"><span class="modal-title">新建检验</span><span class="modal-close" @click="addVisible = false">×</span></div>
<div class="modal-body">
<div class="section">
<div class="section-title">受检单位信息</div>
<div class="form-row cols-2">
<div class="form-field"><span class="label">受检单位</span><input class="ctrl" v-model="addForm.unit" /></div>
<div class="form-field"><span class="label">联系人</span><input class="ctrl" v-model="addForm.contact" /></div>
<div class="form-field"><span class="label">联系电话</span><input class="ctrl" v-model="addForm.phone" /></div>
</div>
</div>
<div class="section">
<div class="section-title">送样单位信息</div>
<div class="form-row cols-2">
<div class="form-field"><span class="label">送样单位</span><input class="ctrl" v-model="addForm.testOrg" /></div>
<div class="form-field"><span class="label">联系人</span><input class="ctrl" v-model="addForm.sampleContact" /></div>
<div class="form-field"><span class="label">联系电话</span><input class="ctrl" v-model="addForm.samplePhone" /></div>
</div>
</div>
<div class="section">
<div class="section-title">样本信息</div>
<div class="form-row cols-2">
<div class="form-field span-2"><span class="label">种植地点</span>
<span class="addr-row">
<input class="ctrl addr-seg" v-model="addForm.surveyProvince" placeholder="省" />
<input class="ctrl addr-seg" v-model="addForm.surveyCity" placeholder="市" />
<input class="ctrl addr-seg" v-model="addForm.surveyDistrict" placeholder="区/县" />
<input class="ctrl addr-seg" v-model="addForm.surveyTownship" placeholder="乡镇" />
<input class="ctrl addr-seg flex-1" v-model="addForm.surveyAddress" placeholder="请输入详细地址" />
</span>
</div>
<div class="form-field"><span class="label">植物名称</span><input class="ctrl" v-model="addForm.plant" /></div>
<div class="form-field"><span class="label">抽样日期</span><DatePicker v-model="addForm.testDate" class="ctrl" /></div>
<div class="form-field span-2"><span class="label">目标有害生物</span><textarea class="ctrl area-input prewrap" v-model="addForm.targetPests" rows="4" placeholder="每条有害生物占一行"></textarea></div>
<div class="form-field"><span class="label"><span class="req">*</span>收样人</span><input class="ctrl" v-model="addForm.receiver" /></div>
<div class="form-field"><span class="label"><span class="req">*</span>收样日期</span><DatePicker v-model="addForm.receiveDate" class="ctrl" /></div>
<div class="form-field span-2"><span class="label"><span class="req">*</span>检验方法</span><textarea class="ctrl area-input" v-model="addForm.inspectionMethod" rows="3" maxlength="500" placeholder="请输入500字以内"></textarea></div>
<div class="form-field span-2"><span class="label"><span class="req">*</span>检验结果</span><textarea class="ctrl area-input" v-model="addForm.labResult" rows="3" maxlength="500" placeholder="请输入500字以内"></textarea></div>
<div class="form-field span-2"><span class="label">备注</span><textarea class="ctrl area-input notes" v-model="addForm.remarks" rows="2" maxlength="500" placeholder="请输入500字以内"></textarea></div>
</div>
</div>
<div class="section">
<div class="section-title">受理机构信息</div>
<div class="form-row cols-2">
<div class="form-field"><span class="label">检验人</span><input class="ctrl" v-model="addForm.inspector1" /></div>
<div class="form-field"><span class="label"><span class="req">*</span>审核人</span><input class="ctrl" v-model="addForm.reviewPerson" /></div>
<div class="form-field"><span class="label">检验机构</span><input class="ctrl" v-model="addForm.testOrg" /></div>
<div class="form-field"><span class="label">受理日期</span><DatePicker v-model="addForm.entryDate" class="ctrl" /></div>
</div>
</div>
<div class="modal-actions">
<button class="btn btn-default" @click="addVisible = false">取消</button>
<button class="btn" @click="onConfirmAdd">保存</button>
</div>
</div>
</div>
</div>
<div v-if="delVisible" class="modal-mask" @click.self="delVisible = false">
<div class="modal-box modal-small">
<div class="modal-top"><span class="modal-title">删除确认</span><span class="modal-close" @click="delVisible = false">×</span></div>
<div class="modal-body">
<p class="del-tip">确定要删除检疫单证号为 <b>{{ delRow?.cert }}</b>,植物名称为 <b>{{ delRow?.plant }}</b> 的记录吗?</p>
<div class="modal-actions">
<button class="btn btn-default" @click="delVisible = false">取消</button>
<button class="btn btn-danger" @click="onConfirmDelete">删除</button>
</div>
</div>
</div>
</div>
</div> </div>
</template> </template>
<script setup> <script setup>
import { ref, computed, watch } from "vue";
import { ElMessage } from "element-plus";
import DatePicker from "@/components/DatePicker.vue";
import { list as storeList, removeById, addRecord, updateRecord } from "@/store/experimentTest.js";
import { fieldSurveyData } from "@/data/fieldSurvey.js";
// 从 store 读取实验检验列表(与田间调查联动:现场不能确诊的记录自动流入)
// 字段迁移:为已存在的旧记录补齐报告单所需字段
const DEFAULT_INSPECTION_METHOD = "按照 GB/T 3543.2-2009 标准进行实验室检验,镜检结合形态学鉴定方法对样品进行检测分析。";
const migrated = storeList.value.map((r) => {
// 对于来源田间调查的记录,强制清空实验检验独立的备注/结果字段
const isFromSurvey = !!r.sourceSurveyId;
let labResult = r.labResult || "";
let remarks = r.remarks || "";
if (isFromSurvey) {
// 来源田间调查的记录:实验检验结果与备注由用户后续填写,初始为空
labResult = "";
remarks = "";
}
// 兜底:清除占位符
if (labResult === "实验室尚未出检验结果") labResult = "";
// 从田间调查同步目标有害生物(始终以田间调查数据为最新来源)
const sourceSurvey = r.sourceSurveyId ? fieldSurveyData.find((fs) => fs.id === r.sourceSurveyId) : null;
const targetPests = sourceSurvey?.targetPests ?? r.targetPests ?? "";
return {
...r,
targetPests,
surveyProvince: r.surveyProvince || "甘肃省",
surveyCity: r.surveyCity || "张掖市",
surveyDistrict: r.surveyDistrict || "甘州区",
surveyTownship: r.surveyTownship || "",
surveyAddress: r.surveyAddress || "",
targetPests: r.targetPests || "",
receiver: r.receiver || "",
receiveDate: r.receiveDate || "",
inspectionMethod: r.inspectionMethod || DEFAULT_INSPECTION_METHOD,
reviewPerson: r.reviewPerson || "",
remarks,
labResult,
sampleContact: r.sampleContact || "甘州区系统管理员",
samplePhone: r.samplePhone || "0936-8212469"
};
});
// 预设检验方法与检验结果(针对特定记录)
const CUIJU_CERT = "6204122022007095";
const CUIJU_INSPECTION_METHOD = "1. 样品前处理:种子样品全量过筛(1.0mm、0.5mm 孔径套筛),收集筛下物及杂质;土壤样品混合均匀后四分法取样 500g 备用。\n2. 豚草属检测:采用过筛检验+形态鉴定,将筛下物置于体视显微镜下逐检,按豚草属种子形态特征(倒卵形、表面具瘤状或刺状突起、顶端具喙)鉴定至属(种);必要时辅以萌芽检验(沙土萌芽法)或分子检测复核。\n3. 剑线虫属(传毒种类)检测:采用贝尔曼漏斗法/浅盘法自土壤及根际样品中分离线虫(分离时间不少于 24h),收集线虫悬液,在生物显微镜下进行形态学鉴定(测量体长、齿针长度,观察尾形等特征),判定是否属剑线虫属及其传毒种类;必要时辅以 rDNA ITS/D2D3 区扩增测序确认。\n4. 其它检疫性有害生物排查:实施症状观察、直接镜检、过筛检查,必要时采用分离培养、血清学或 PCR 检测等方法。\n5. 检验方法有国家、行业或地方标准的按标准执行;无标准时根据受检样品和目标有害生物特点采用上述适当方法。";
const CUIJU_LAB_RESULT = "1.未检出豚草属(Ambrosia spp.)种子;\n2.未检出剑线虫属(Xiphinema Cobb)传毒种类线虫;\n3.未发现中华人民共和国法律规定的其它检疫性有害生物。\n综上,本批样品未检出目标检疫性有害生物,未检出目标检疫性有害生物,检验合格,符合检疫要求。";
// 针对翠菊记录预填检验方法与结果
const finalList = migrated.map((r) => {
if (r.cert === CUIJU_CERT) {
return { ...r, inspectionMethod: CUIJU_INSPECTION_METHOD, labResult: CUIJU_LAB_RESULT };
}
return r;
});
if (JSON.stringify(finalList) !== JSON.stringify(storeList.value)) {
storeList.value = finalList;
}
const dataList = ref([...storeList.value]);
watch(storeList, (v) => { dataList.value = [...v]; }, { deep: true });
const defaultFilters = () => ({ testStart: "", testEnd: "", cert: "", plant: "", variety: "", testOrg: "" });
const filters = ref(defaultFilters());
const appliedFilters = ref(defaultFilters());
const currentPage = ref(1);
const pageSize = 10;
const jumpInput = ref(1);
const viewVisible = ref(false);
const editVisible = ref(false);
const addVisible = ref(false);
const delVisible = ref(false);
const delRow = ref(null);
const form = ref({});
const defaultAddForm = () => ({
cert: "", unit: "", contact: "", phone: "", plant: "", variety: "", origin: "",
approveQty: "", importQty: "", qty: "150克", testDate: "", testOrg: "甘州区农业技术推广中心",
content: "", conclusion: "", inspector1: "", inspector2: "", inspectorPhone: "", entryDate: "",
// 报告单新增字段
surveyProvince: "甘肃省", surveyCity: "张掖市", surveyDistrict: "甘州区", surveyTownship: "", surveyAddress: "",
targetPests: "",
receiver: "", receiveDate: "",
inspectionMethod: "按照 GB/T 3543.2-2009 标准进行实验室检验,镜检结合形态学鉴定方法对样品进行检测分析。",
labResult: "", reviewPerson: "", remarks: "",
sampleContact: "甘州区系统管理员", samplePhone: "0936-8212469"
});
const addForm = ref(defaultAddForm());
const filteredData = computed(() => {
const f = appliedFilters.value;
return dataList.value.filter((r) => {
if (f.cert && !r.cert.toLowerCase().includes(f.cert.toLowerCase())) return false;
if (f.plant && !r.plant.toLowerCase().includes(f.plant.toLowerCase())) return false;
if (f.variety && !r.variety.toLowerCase().includes(f.variety.toLowerCase())) return false;
if (f.testOrg && !r.testOrg.toLowerCase().includes(f.testOrg.toLowerCase())) return false;
if (f.testStart && r.testDate < f.testStart) return false;
if (f.testEnd && r.testDate > f.testEnd) return false;
return true;
});
});
const totalPages = computed(() => Math.max(1, Math.ceil(filteredData.value.length / pageSize)));
const pageNumbers = computed(() => {
const t = totalPages.value;
const c = currentPage.value;
if (t <= 7) return Array.from({ length: t }, (_, i) => i + 1);
if (c <= 4) return [1, 2, 3, 4, 5, "...", t];
if (c >= t - 3) return [1, "...", t - 4, t - 3, t - 2, t - 1, t];
return [1, "...", c - 1, c, c + 1, "...", t];
});
const pagedData = computed(() => {
const s = (currentPage.value - 1) * pageSize;
return filteredData.value.slice(s, s + pageSize);
});
const onQuery = () => {
appliedFilters.value = JSON.parse(JSON.stringify(filters.value));
currentPage.value = 1;
};
const onReset = () => {
filters.value = defaultFilters();
appliedFilters.value = defaultFilters();
currentPage.value = 1;
};
const goPage = (p) => {
if (p < 1 || p > totalPages.value) return;
currentPage.value = p;
};
const onJump = () => {
const p = parseInt(jumpInput.value);
if (!isNaN(p)) goPage(p);
else goPage(1);
};
const onView = (row) => { form.value = { ...row }; viewVisible.value = true; };
const onEdit = (row) => { form.value = { ...row }; editVisible.value = true; };
const onAdd = () => { addForm.value = defaultAddForm(); addVisible.value = true; };
const onDelete = (row) => { delRow.value = row; delVisible.value = true; };
const onConfirmEdit = () => {
const i = dataList.value.findIndex((r) => r.id === form.value.id);
if (i >= 0) {
const updated = { ...form.value };
dataList.value[i] = updated;
// 同步到 store
if (!updated.sourceSurveyId) {
// 直接编辑/新建的记录
updateRecord(updated);
} else {
// 来源于田间调查的记录:只更新受检单位等不属于田间调查的字段
// 受检单位/联系人/电话/植物名称/抽样日期/收样人/收样日期/检验方法/检验结果/审核人/备注
const surveyFields = ["unit","contact","phone","plant","variety","origin","approveQty","importQty","testDate","testOrg","inspector1","inspector2","inspectorPhone","entryDate","surveyProvince","surveyCity","surveyDistrict","surveyTownship","surveyAddress","targetPests","sampleUnit","sampleContact","samplePhone"];
const merged = { ...updated };
updateRecord(merged);
}
}
editVisible.value = false;
ElMessage.success("保存成功");
};
const onConfirmAdd = () => {
const newRec = addRecord({ ...addForm.value });
dataList.value = [...storeList.value];
addVisible.value = false;
ElMessage.success("新增成功");
};
const onConfirmDelete = () => {
const id = delRow.value.id;
dataList.value = dataList.value.filter((r) => r.id !== id);
removeById(id);
delVisible.value = false;
ElMessage.success("删除成功");
};
</script> </script>
<style scoped> <style scoped>
.page { padding: 16px 20px; } .page-list { padding: 16px 20px; }
.placeholder { padding: 60px 0; text-align: center; color: #999; font-size: 14px; background: #fafafa; border-radius: 4px; } .filter-box { background: #fff; border: 1px solid #ebeef5; border-radius: 3px; padding: 18px 20px; margin-bottom: 12px; }
.filter-row { display: flex; align-items: center; flex-wrap: wrap; gap: 14px 24px; margin-bottom: 16px; }
.filter-row:last-child { margin-bottom: 0; }
.filter-item { display: flex; align-items: center; gap: 6px; flex-shrink: 0; min-width: 0; }
.filter-item .label { color: #555; font-size: 14px; white-space: nowrap; flex-shrink: 0; min-width: 84px; }
.filter-add { margin-left: auto; }
.dash { color: #999; }
.dp-160 { width: 160px; }
.ipt { width: 160px; height: 32px; padding: 0 8px; border: 1px solid #dcdfe6; border-radius: 3px; font-size: 14px; }
.ipt:focus { border-color: #d71e1e; outline: none; }
.btn { background: #d71e1e; color: #fff; border: 1px solid #d71e1e; height: 32px; padding: 0 18px; border-radius: 3px; font-size: 14px; cursor: pointer; }
.btn:hover { background: #b81818; border-color: #b81818; }
.btn-default { background: #fff; color: #555; border-color: #dcdfe6; }
.btn-default:hover { background: #fafafa; color: #d71e1e; border-color: #d71e1e; }
.btn-add { background: #d71e1e; color: #fff; border: 1px solid #d71e1e; }
.btn-danger { background: #fff; color: #d71e1e; border-color: #d71e1e; }
.btn-danger:hover { background: #d71e1e; color: #fff; }
.tbl-wrap { background: #fff; border: 1px solid #ebeef5; border-radius: 3px; overflow: hidden; }
.simple-table { width: 100%; border-collapse: collapse; table-layout: fixed; font-size: 14px; }
.simple-table th, .simple-table td { padding: 14px 6px; text-align: center; vertical-align: middle; border-bottom: 1px solid #ebeef5; }
.simple-table th { background: #fafafa; font-size: 14px; font-weight: 600; color: #333; }
.simple-table tbody tr:hover { background: #fafafa; }
.col-unit { text-align: left; padding-left: 12px; }
.col-action { white-space: nowrap; }
.op-link { color: #333; font-size: 14px; cursor: pointer; margin: 0 4px; }
.op-link:hover { color: #d71e1e; }
.empty { padding: 40px 0; color: #999; }
.pagination { display: flex; align-items: center; justify-content: flex-end; gap: 6px; padding: 14px 16px; background: #fff; border: 1px solid #ebeef5; border-top: 0; border-radius: 0 0 3px 3px; font-size: 14px; }
.total { color: #555; margin-right: 12px; }
.page-btn { min-width: 30px; height: 30px; padding: 0 8px; background: #fff; border: 1px solid #dcdfe6; color: #555; border-radius: 3px; cursor: pointer; font-size: 14px; }
.page-btn:hover:not(:disabled) { color: #d71e1e; border-color: #d71e1e; }
.page-btn.active { background: #d71e1e; color: #fff; border-color: #d71e1e; }
.page-btn:disabled { opacity: 0.5; cursor: not-allowed; }
.jump { margin-left: 12px; color: #555; }
.jump-input { width: 50px; height: 30px; text-align: center; border: 1px solid #dcdfe6; border-radius: 3px; margin: 0 4px; font-size: 14px; }
.modal-mask { position: fixed; inset: 0; background: rgba(0, 0, 0, 0.45); display: flex; align-items: center; justify-content: center; z-index: 1000; }
.modal-box { background: #fff; border-radius: 4px; width: 900px; max-width: 95vw; max-height: 90vh; display: flex; flex-direction: column; box-shadow: 0 8px 30px rgba(0, 0, 0, 0.2); overflow: hidden; }
.modal-box-wide { width: 1100px; max-width: 95vw; }
.form-row { row-gap: 20px; }
.form-field { min-height: 56px; display: flex; align-items: flex-start; }
.form-field .label { padding-top: 6px; }
.form-field .ctrl { min-height: 32px; }
.form-field .ctrl.area { min-height: 80px; max-height: 120px; line-height: 1.7; overflow-y: auto; align-items: flex-start !important; }
.form-field .ctrl.area::-webkit-scrollbar { width: 6px; }
.form-field .ctrl.area::-webkit-scrollbar-thumb { background: #c0c4cc; border-radius: 3px; }
.form-field .ctrl.area::-webkit-scrollbar-track { background: #f5f7fa; }
/* 备注输入框:内容少时整体小一点 */
.form-field .ctrl.notes { min-height: 40px; max-height: 80px; line-height: 1.7; overflow-y: auto; align-items: flex-start !important; }
.area-input.notes { min-height: 40px; max-height: 80px; padding: 6px 10px; line-height: 1.7; resize: vertical; }
.area-input.notes::-webkit-scrollbar { width: 6px; }
.area-input.notes::-webkit-scrollbar-thumb { background: #c0c4cc; border-radius: 3px; }
.area-input.notes::-webkit-scrollbar-track { background: #f5f7fa; }
.form-field .ctrl.notes::-webkit-scrollbar { width: 6px; }
.form-field .ctrl.notes::-webkit-scrollbar-thumb { background: #c0c4cc; border-radius: 3px; }
.form-field .ctrl.notes::-webkit-scrollbar-track { background: #f5f7fa; }
.form-row.cols-2 { row-gap: 18px; }
.modal-small { width: 420px; }
.addr-row { display: flex; gap: 8px; padding: 6px 8px; align-items: center; flex-wrap: wrap; }
.addr-seg { padding: 4px 10px; background: #fff; border: 1px solid #e0e0e0; border-radius: 2px; min-width: 80px; text-align: center; }
.addr-seg.flex-1 { flex: 1; min-width: 120px; text-align: left; }
.prewrap { white-space: pre-wrap; word-break: break-all; line-height: 1.7; }
.req { color: #d71e1e; margin-right: 2px; }
.modal-top { display: flex; align-items: center; justify-content: space-between; padding: 12px 18px; background: #d71e1e; color: #fff; }
.modal-title { font-size: 16px; font-weight: 600; }
.modal-close { width: 22px; height: 22px; line-height: 18px; text-align: center; border: 1px solid #fff; border-radius: 3px; cursor: pointer; font-size: 16px; }
.modal-body { padding: 18px 22px; overflow-y: auto; }
.form-row.cols-2 { display: grid; grid-template-columns: 1fr 1fr; column-gap: 32px; row-gap: 14px; }
.form-field.span-2 { grid-column: 1 / -1; }
.form-field .ctrl.area { min-height: 80px; max-height: 120px; line-height: 1.7; white-space: pre-wrap; word-break: break-all; overflow-y: auto; }
.form-field .ctrl.area::-webkit-scrollbar { width: 6px; }
.form-field .ctrl.area::-webkit-scrollbar-thumb { background: #c0c4cc; border-radius: 3px; }
.form-field .ctrl.area::-webkit-scrollbar-track { background: #f5f7fa; }
/* 备注输入框:内容少时整体小一点 */
.form-field .ctrl.notes { min-height: 40px; max-height: 80px; line-height: 1.7; overflow-y: auto; align-items: flex-start !important; }
.area-input.notes { min-height: 40px; max-height: 80px; padding: 6px 10px; line-height: 1.7; resize: vertical; }
.area-input.notes::-webkit-scrollbar { width: 6px; }
.area-input.notes::-webkit-scrollbar-thumb { background: #c0c4cc; border-radius: 3px; }
.area-input.notes::-webkit-scrollbar-track { background: #f5f7fa; }
.form-field .ctrl.notes::-webkit-scrollbar { width: 6px; }
.form-field .ctrl.notes::-webkit-scrollbar-thumb { background: #c0c4cc; border-radius: 3px; }
.form-field .ctrl.notes::-webkit-scrollbar-track { background: #f5f7fa; }
.area-input { min-height: 56px; line-height: 1.6; padding: 6px 10px; resize: vertical; }
.section { margin-bottom: 22px; }
.section:last-child { margin-bottom: 0; }
.section-title { font-size: 15px; font-weight: 600; color: #d71e1e; margin: 0 0 12px; padding-left: 10px; border-left: 3px solid #d71e1e; line-height: 1; }
.form-field { display: flex; align-items: center; gap: 8px; }
.form-field .label { width: 110px; text-align: right; color: #555; font-size: 14px; flex-shrink: 0; white-space: nowrap; }
.form-field .ctrl { flex: 1; height: 32px; padding: 0 10px; border: 1px solid #dcdfe6; border-radius: 3px; font-size: 14px; }
.mb-input { background: #fafafa; padding: 6px 12px; display: flex; align-items: center; min-height: 32px; line-height: 1.6; align-self: center; }
.modal-actions { display: flex; justify-content: flex-end; gap: 10px; margin-top: 18px; padding-top: 14px; border-top: 1px solid #ebeef5; }
.del-tip { font-size: 14px; color: #555; line-height: 1.8; padding: 8px 0 16px; }
.del-tip b { color: #d71e1e; }
</style> </style>
<template> <template>
<div class="page-list"> <div class="page-list">
<div class="filter-box"> <div class="filter-box">
<div class="filter-row"> <div class="filter-row">
...@@ -23,7 +23,7 @@ ...@@ -23,7 +23,7 @@
<input class="ipt" v-model="filters.variety" placeholder="请输入关键字" /> <input class="ipt" v-model="filters.variety" placeholder="请输入关键字" />
</div> </div>
<div class="filter-item"> <div class="filter-item">
<label>检验机构:</label> <label>受检单位:</label>
<input class="ipt" v-model="filters.testOrg" placeholder="请输入关键字" /> <input class="ipt" v-model="filters.testOrg" placeholder="请输入关键字" />
</div> </div>
<div class="filter-item"> <div class="filter-item">
...@@ -53,7 +53,7 @@ ...@@ -53,7 +53,7 @@
<tr> <tr>
<th>序号</th> <th>序号</th>
<th>检疫单证号</th> <th>检疫单证号</th>
<th>申报单位</th> <th>受检单位</th>
<th>植物名称</th> <th>植物名称</th>
<th>品种名称</th> <th>品种名称</th>
<th>抽检数量</th> <th>抽检数量</th>
...@@ -75,7 +75,7 @@ ...@@ -75,7 +75,7 @@
<td class="col-action"> <td class="col-action">
<a class="op-link" @click="onView(r)">查看</a> <a class="op-link" @click="onView(r)">查看</a>
<a class="op-link" @click="onEdit(r)">编辑</a> <a class="op-link" @click="onEdit(r)">编辑</a>
<a class="op-link op-danger" @click="onDelete(r)">删除</a> <a class="op-link" @click="onDelete(r)">删除</a>
</td> </td>
</tr> </tr>
<tr v-if="!pagedData.length"> <tr v-if="!pagedData.length">
...@@ -92,7 +92,7 @@ ...@@ -92,7 +92,7 @@
<button v-for="p in pageNumbers" :key="p" class="page-btn" :class="{ active: p === currentPage }" @click="goPage(p)">{{ p }}</button> <button v-for="p in pageNumbers" :key="p" class="page-btn" :class="{ active: p === currentPage }" @click="goPage(p)">{{ p }}</button>
<button class="page-btn" :disabled="currentPage === totalPages" @click="goPage(currentPage + 1)">下一页</button> <button class="page-btn" :disabled="currentPage === totalPages" @click="goPage(currentPage + 1)">下一页</button>
<button class="page-btn" :disabled="currentPage === totalPages" @click="goPage(totalPages)">末页</button> <button class="page-btn" :disabled="currentPage === totalPages" @click="goPage(totalPages)">末页</button>
<span class="jump">跳至 <input class="jump-input" v-model="jumpInput" @keyup.enter="onJump" /> 页</span> <span class="jump">跳转至 <input class="jump-input" v-model.number="jumpInput" @keyup.enter="onJump" /> 页</span>
</div> </div>
<div v-if="viewVisible" class="modal-mask" @click.self="viewVisible = false"> <div v-if="viewVisible" class="modal-mask" @click.self="viewVisible = false">
...@@ -103,7 +103,7 @@ ...@@ -103,7 +103,7 @@
<div class="section-title">基本信息</div> <div class="section-title">基本信息</div>
<div class="form-row cols-2"> <div class="form-row cols-2">
<div class="form-field"><span class="label">检疫单证号</span><span class="ctrl mb-input">{{ form.cert }}</span></div> <div class="form-field"><span class="label">检疫单证号</span><span class="ctrl mb-input">{{ form.cert }}</span></div>
<div class="form-field"><span class="label">申报单位</span><span class="ctrl mb-input">{{ form.unit }}</span></div> <div class="form-field"><span class="label">受检单位</span><span class="ctrl mb-input">{{ form.unit }}</span></div>
<div class="form-field"><span class="label">联系人</span><span class="ctrl mb-input">{{ form.contact }}</span></div> <div class="form-field"><span class="label">联系人</span><span class="ctrl mb-input">{{ form.contact }}</span></div>
<div class="form-field"><span class="label">联系电话</span><span class="ctrl mb-input">{{ form.phone }}</span></div> <div class="form-field"><span class="label">联系电话</span><span class="ctrl mb-input">{{ form.phone }}</span></div>
<div class="form-field"><span class="label">植物名称</span><span class="ctrl mb-input">{{ form.plant }}</span></div> <div class="form-field"><span class="label">植物名称</span><span class="ctrl mb-input">{{ form.plant }}</span></div>
...@@ -111,7 +111,7 @@ ...@@ -111,7 +111,7 @@
<div class="form-field"><span class="label">品种名称</span><span class="ctrl mb-input">{{ form.variety }}</span></div> <div class="form-field"><span class="label">品种名称</span><span class="ctrl mb-input">{{ form.variety }}</span></div>
<div class="form-field"><span class="label">种苗来源国</span><span class="ctrl mb-input">{{ form.origin }}</span></div> <div class="form-field"><span class="label">种苗来源国</span><span class="ctrl mb-input">{{ form.origin }}</span></div>
<div class="form-field"><span class="label">审批数量</span><span class="ctrl mb-input">{{ form.approveQty }}</span></div> <div class="form-field"><span class="label">审批数量</span><span class="ctrl mb-input">{{ form.approveQty }}</span></div>
<div class="form-field"><span class="label">引进数量</span><span class="ctrl mb-input">{{ form.approveQty }}</span></div> <div class="form-field"><span class="label">引进数量</span><span class="ctrl mb-input">{{ form.importQty }}</span></div>
<div class="form-field"><span class="label">入库时间</span><span class="ctrl mb-input">{{ form.testDate }}</span></div> <div class="form-field"><span class="label">入库时间</span><span class="ctrl mb-input">{{ form.testDate }}</span></div>
<div class="form-field"><span class="label">入库单位</span><span class="ctrl mb-input">{{ form.testOrg }}</span></div> <div class="form-field"><span class="label">入库单位</span><span class="ctrl mb-input">{{ form.testOrg }}</span></div>
</div> </div>
...@@ -147,7 +147,7 @@ ...@@ -147,7 +147,7 @@
<div class="section-title">基本信息</div> <div class="section-title">基本信息</div>
<div class="form-row cols-2"> <div class="form-row cols-2">
<div class="form-field"><span class="label">检疫单证号</span><input class="ctrl" v-model="form.cert" /></div> <div class="form-field"><span class="label">检疫单证号</span><input class="ctrl" v-model="form.cert" /></div>
<div class="form-field"><span class="label">申报单位</span><input class="ctrl" v-model="form.unit" /></div> <div class="form-field"><span class="label">受检单位</span><input class="ctrl" v-model="form.unit" /></div>
<div class="form-field"><span class="label">联系人</span><input class="ctrl" v-model="form.contact" /></div> <div class="form-field"><span class="label">联系人</span><input class="ctrl" v-model="form.contact" /></div>
<div class="form-field"><span class="label">联系电话</span><input class="ctrl" v-model="form.phone" /></div> <div class="form-field"><span class="label">联系电话</span><input class="ctrl" v-model="form.phone" /></div>
<div class="form-field"><span class="label">植物名称</span><input class="ctrl" v-model="form.plant" /></div> <div class="form-field"><span class="label">植物名称</span><input class="ctrl" v-model="form.plant" /></div>
...@@ -155,7 +155,7 @@ ...@@ -155,7 +155,7 @@
<div class="form-field"><span class="label">品种名称</span><input class="ctrl" v-model="form.variety" /></div> <div class="form-field"><span class="label">品种名称</span><input class="ctrl" v-model="form.variety" /></div>
<div class="form-field"><span class="label">种苗来源国</span><input class="ctrl" v-model="form.origin" /></div> <div class="form-field"><span class="label">种苗来源国</span><input class="ctrl" v-model="form.origin" /></div>
<div class="form-field"><span class="label">审批数量</span><input class="ctrl" v-model="form.approveQty" /></div> <div class="form-field"><span class="label">审批数量</span><input class="ctrl" v-model="form.approveQty" /></div>
<div class="form-field"><span class="label">引进数量</span><input class="ctrl" :value="form.approveQty" @input="form.approveQty = $event.target.value" /></div> <div class="form-field"><span class="label">引进数量</span><input class="ctrl" v-model="form.importQty" /></div>
<div class="form-field"><span class="label">入库时间</span><DatePicker v-model="form.testDate" class="ctrl" /></div> <div class="form-field"><span class="label">入库时间</span><DatePicker v-model="form.testDate" class="ctrl" /></div>
<div class="form-field"><span class="label">入库单位</span><input class="ctrl" v-model="form.testOrg" /></div> <div class="form-field"><span class="label">入库单位</span><input class="ctrl" v-model="form.testOrg" /></div>
</div> </div>
...@@ -195,7 +195,7 @@ ...@@ -195,7 +195,7 @@
<div class="section-title">基本信息</div> <div class="section-title">基本信息</div>
<div class="form-row cols-2"> <div class="form-row cols-2">
<div class="form-field"><span class="label">检疫单证号</span><input class="ctrl" v-model="addForm.cert" /></div> <div class="form-field"><span class="label">检疫单证号</span><input class="ctrl" v-model="addForm.cert" /></div>
<div class="form-field"><span class="label">申报单位</span><input class="ctrl" v-model="addForm.unit" /></div> <div class="form-field"><span class="label">受检单位</span><input class="ctrl" v-model="addForm.unit" /></div>
<div class="form-field"><span class="label">联系人</span><input class="ctrl" v-model="addForm.contact" /></div> <div class="form-field"><span class="label">联系人</span><input class="ctrl" v-model="addForm.contact" /></div>
<div class="form-field"><span class="label">联系电话</span><input class="ctrl" v-model="addForm.phone" /></div> <div class="form-field"><span class="label">联系电话</span><input class="ctrl" v-model="addForm.phone" /></div>
<div class="form-field"><span class="label">植物名称</span><input class="ctrl" v-model="addForm.plant" /></div> <div class="form-field"><span class="label">植物名称</span><input class="ctrl" v-model="addForm.plant" /></div>
...@@ -203,7 +203,7 @@ ...@@ -203,7 +203,7 @@
<div class="form-field"><span class="label">品种名称</span><input class="ctrl" v-model="addForm.variety" /></div> <div class="form-field"><span class="label">品种名称</span><input class="ctrl" v-model="addForm.variety" /></div>
<div class="form-field"><span class="label">种苗来源国</span><input class="ctrl" v-model="addForm.origin" /></div> <div class="form-field"><span class="label">种苗来源国</span><input class="ctrl" v-model="addForm.origin" /></div>
<div class="form-field"><span class="label">审批数量</span><input class="ctrl" v-model="addForm.approveQty" /></div> <div class="form-field"><span class="label">审批数量</span><input class="ctrl" v-model="addForm.approveQty" /></div>
<div class="form-field"><span class="label">引进数量</span><input class="ctrl" :value="addForm.approveQty" @input="addForm.approveQty = $event.target.value" /></div> <div class="form-field"><span class="label">引进数量</span><input class="ctrl" v-model="addForm.importQty" /></div>
<div class="form-field"><span class="label">入库时间</span><DatePicker v-model="addForm.testDate" class="ctrl" /></div> <div class="form-field"><span class="label">入库时间</span><DatePicker v-model="addForm.testDate" class="ctrl" /></div>
<div class="form-field"><span class="label">入库单位</span><input class="ctrl" v-model="addForm.testOrg" /></div> <div class="form-field"><span class="label">入库单位</span><input class="ctrl" v-model="addForm.testOrg" /></div>
</div> </div>
...@@ -264,7 +264,7 @@ const appliedFilters = ref(defaultFilters()); ...@@ -264,7 +264,7 @@ const appliedFilters = ref(defaultFilters());
const currentPage = ref(1); const currentPage = ref(1);
const pageSize = 10; const pageSize = 10;
const jumpInput = ref(""); const jumpInput = ref(1);
const viewVisible = ref(false); const viewVisible = ref(false);
const editVisible = ref(false); const editVisible = ref(false);
...@@ -273,7 +273,7 @@ const delVisible = ref(false); ...@@ -273,7 +273,7 @@ const delVisible = ref(false);
const delRow = ref(null); const delRow = ref(null);
const form = ref({}); const form = ref({});
const defaultAddForm = () => ({ cert: "", unit: "", contact: "", phone: "", plant: "", variety: "", origin: "", approveQty: "", qty: "150克", testDate: "", testOrg: "甘州区农业技术推广中心", content: "", conclusion: "", inspector1: "", inspector2: "", inspectorPhone: "", entryDate: "" }); const defaultAddForm = () => ({ cert: "", unit: "", contact: "", phone: "", plant: "", variety: "", origin: "", approveQty: "", importQty: "", qty: "150克", testDate: "", testOrg: "甘州区农业技术推广中心", content: "", conclusion: "", inspector1: "", inspector2: "", inspectorPhone: "", entryDate: "" });
const addForm = ref(defaultAddForm()); const addForm = ref(defaultAddForm());
const filteredData = computed(() => { const filteredData = computed(() => {
...@@ -319,6 +319,7 @@ const goPage = (p) => { ...@@ -319,6 +319,7 @@ const goPage = (p) => {
const onJump = () => { const onJump = () => {
const p = parseInt(jumpInput.value); const p = parseInt(jumpInput.value);
if (!isNaN(p)) goPage(p); if (!isNaN(p)) goPage(p);
else goPage(1);
}; };
const onView = (row) => { form.value = { ...row }; viewVisible.value = true; }; const onView = (row) => { form.value = { ...row }; viewVisible.value = true; };
......
<template>
<div class="page-list">
<div class="filter-box">
<div class="filter-row">
<div class="filter-item">
<label>处理日期:</label>
<DatePicker v-model="filters.handleStart" class="dp-160" placeholder="开始日期" />
<span class="dash">至</span>
<DatePicker v-model="filters.handleEnd" class="dp-160" placeholder="结束日期" />
</div>
<div class="filter-item">
<label>检疫单证号:</label>
<input class="ipt" v-model="filters.cert" placeholder="请输入关键字" />
</div>
<div class="filter-item">
<label>植物名称:</label>
<input class="ipt" v-model="filters.plant" placeholder="请输入关键字" />
</div>
<div class="filter-item">
<label>申报单位:</label>
<input class="ipt" v-model="filters.unit" placeholder="请输入关键字" />
</div>
</div>
<div class="filter-row">
<div class="filter-item">
<label>处理状态:</label>
<select class="ipt" v-model="filters.status">
<option value="">全部</option>
<option value="待处理">待处理</option>
<option value="已处理">已处理</option>
</select>
</div>
<div class="filter-item">
<button class="btn" @click="onQuery">查询</button>
<button class="btn btn-default" @click="onReset">重置</button>
</div>
</div>
</div>
<div class="tbl-wrap">
<table class="simple-table">
<colgroup>
<col style="width:5%" />
<col style="width:14%" />
<col style="width:22%" />
<col style="width:9%" />
<col style="width:9%" />
<col style="width:11%" />
<col style="width:13%" />
<col style="width:7%" />
<col style="width:10%" />
</colgroup>
<thead>
<tr>
<th>序号</th>
<th>检疫单证号</th>
<th>申报单位</th>
<th>植物名称</th>
<th>品种名称</th>
<th>处理日期</th>
<th>检疫机构</th>
<th>处理状态</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr v-for="(r, i) in pagedData" :key="r.id">
<td>{{ (currentPage - 1) * pageSize + i + 1 }}</td>
<td>{{ r.cert }}</td>
<td class="col-unit"><span class="org-name">{{ r.unit }}</span></td>
<td>{{ r.plant }}</td>
<td>{{ r.variety || "—" }}</td>
<td>{{ r.handleDate || "—" }}</td>
<td class="col-unit"><span class="org-name">{{ r.surveyOrg || "甘州区农业技术推广中心" }}</span></td>
<td>{{ r.handleDate ? "已处理" : "待处理" }}</td>
<td class="col-action">
<a class="op-link" @click="onProcess(r)">办理</a>
<a class="op-link" @click="onEdit(r)">编辑</a>
<a class="op-link" @click="onDelete(r)">删除</a>
</td>
</tr>
<tr v-if="!pagedData.length">
<td colspan="9" class="empty">暂无数据</td>
</tr>
</tbody>
</table>
</div>
<div class="pagination">
<span class="total" style="margin-right:auto;">共 {{ filteredData.length }} 条记录</span>
<button class="page-btn" :disabled="currentPage === 1" @click="goPage(1)">首页</button>
<button class="page-btn" :disabled="currentPage === 1" @click="goPage(currentPage - 1)">上一页</button>
<button v-for="p in pageNumbers" :key="p" class="page-btn" :class="{ active: p === currentPage }" @click="goPage(p)">{{ p }}</button>
<button class="page-btn" :disabled="currentPage === totalPages" @click="goPage(currentPage + 1)">下一页</button>
<button class="page-btn" :disabled="currentPage === totalPages" @click="goPage(totalPages)">末页</button>
<span class="jump">跳转至 <input class="jump-input" v-model.number="jumpInput" @keyup.enter="onJump" /> 页</span>
</div>
<!-- 查看弹窗 -->
<div v-if="viewVisible" class="modal-mask" @click.self="closeView">
<div class="modal-box">
<div class="modal-header">
<span class="modal-title">除害处理详情</span>
<span class="modal-close" @click="closeView">×</span>
</div>
<div class="modal-body">
<div class="section-title">基本信息</div>
<div class="form-row cols-2">
<div class="form-field"><span class="label">受检单位</span><span class="mb-input">{{ form.unit }}</span></div>
<div class="form-field"><span class="label">联系人</span><span class="mb-input">{{ form.contact }}</span></div>
<div class="form-field"><span class="label">联系电话</span><span class="mb-input">{{ form.phone }}</span></div>
<div class="form-field"><span class="label">植物名称</span><span class="mb-input">{{ form.plant }}</span></div>
<div class="form-field"><span class="label">品种名称</span><span class="mb-input">{{ form.variety }}</span></div>
<div class="form-field"><span class="label">种苗来源国(地区)</span><span class="mb-input">{{ form.origin }}</span></div>
<div class="form-field"><span class="label">调查日期</span><span class="mb-input">{{ form.surveyDate }}</span></div>
<div class="form-field"><span class="label">处理通知书编号</span><span class="mb-input">{{ form.noticeNo || "—" }}</span></div>
</div>
<div class="section-title">应检有害生物详情</div>
<div class="pest-block">
<div class="pest-hint">请输入发现的应检有害生物名称及发生情况:</div>
<div class="ipt-cell area pest-readonly">{{ form.pestDetail || "—" }}</div>
</div>
<div class="section-title">疫情处理通知书</div>
<div class="form-row cols-2">
<div class="form-field span-2"><span class="label">检验结果</span><span class="mb-input area notes">{{ form.noticeTestResult || "—" }}</span></div>
<div class="form-field span-2"><span class="label">处理意见</span><span class="mb-input area notes">{{ form.noticeHandleOpinion || "—" }}</span></div>
</div>
<div class="section-title">除害处理信息</div>
<div class="form-row cols-2">
<div class="form-field"><span class="label">处理方法</span><span class="mb-input">{{ form.handleMethod || "—" }}</span></div>
<div class="form-field"><span class="label">处理日期</span><span class="mb-input">{{ form.handleDate || "—" }}</span></div>
<div class="form-field"><span class="label">处理人</span><span class="mb-input">{{ form.handlePerson || "—" }}</span></div>
<div class="form-field"><span class="label">处理结果</span><span class="mb-input">{{ form.handleResult || "—" }}</span></div>
<div class="form-field span-2"><span class="label">备注</span><span class="mb-input area notes">{{ form.remarks || "—" }}</span></div>
</div>
</div>
</div>
</div>
<!-- 编辑弹窗 -->
<div v-if="editVisible" class="modal-mask" @click.self="closeEdit">
<div class="modal-box">
<div class="modal-header">
<span class="modal-title">编辑除害处理</span>
<span class="modal-close" @click="closeEdit">×</span>
</div>
<div class="modal-body">
<div class="section-title">基本信息</div>
<div class="form-row cols-2">
<div class="form-field"><span class="label">受检单位</span><input class="ctrl" v-model="form.unit" /></div>
<div class="form-field"><span class="label">联系人</span><input class="ctrl" v-model="form.contact" /></div>
<div class="form-field"><span class="label">联系电话</span><input class="ctrl" v-model="form.phone" /></div>
<div class="form-field"><span class="label">植物名称</span><input class="ctrl" v-model="form.plant" /></div>
<div class="form-field"><span class="label">品种名称</span><input class="ctrl" v-model="form.variety" /></div>
<div class="form-field"><span class="label">种苗来源国(地区)</span><input class="ctrl" v-model="form.origin" /></div>
<div class="form-field"><span class="label">调查日期</span><input class="ctrl" v-model="form.surveyDate" /></div>
</div>
<div class="section-title">除害处理信息</div>
<div class="form-row cols-2">
<div class="form-field">
<span class="label req">处理方法</span>
<select class="ctrl" v-model="form.handleMethod">
<option value="">请选择</option>
<option v-for="m in handleMethods" :key="m" :value="m">{{ m }}</option>
</select>
</div>
<div class="form-field">
<span class="label req">处理日期</span>
<input class="ctrl" type="date" v-model="form.handleDate" />
</div>
<div class="form-field">
<span class="label req">处理人</span>
<input class="ctrl" v-model="form.handlePerson" />
</div>
<div class="form-field">
<span class="label req">处理结果</span>
<select class="ctrl" v-model="form.handleResult">
<option value="">请选择</option>
<option value="已根除">已根除</option>
<option value="有效控制">有效控制</option>
<option value="部分控制">部分控制</option>
<option value="未控制">未控制</option>
</select>
</div>
<div class="form-field span-2"><span class="label">备注</span><textarea class="ctrl area-input notes" v-model="form.remarks" rows="2" maxlength="500" placeholder="请输入500字以内"></textarea></div>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-default" @click="closeEdit">取消</button>
<button class="btn" @click="onConfirmEdit">保存</button>
</div>
</div>
</div>
<!-- 删除弹窗 -->
<div v-if="delVisible" class="modal-mask" @click.self="delVisible = false">
<div class="modal-box sm">
<div class="modal-header">
<span class="modal-title">删除确认</span>
<span class="modal-close" @click="delVisible = false">×</span>
</div>
<div class="modal-body">
<p class="del-tip">确定要删除该除害处理记录吗?</p>
</div>
<div class="modal-footer">
<button class="btn btn-default" @click="delVisible = false">取消</button>
<button class="btn btn-danger" @click="onConfirmDelete">删除</button>
</div>
</div>
</div>
<!-- 办理弹窗(3 个 tab) -->
<div v-if="processVisible" class="modal-mask" @click.self="processVisible = false">
<div class="modal-box modal-wide">
<div class="notice-top">
<div class="notice-title">除害处理详情</div>
<span class="modal-close" @click="processVisible = false">×</span>
</div>
<div class="proc-tabs">
<div v-for="t in procTabs" :key="t.key" class="proc-tab" :class="{ active: activeProcTab === t.key }" @click="activeProcTab = t.key">{{ t.label }}</div>
</div>
<div class="modal-body notice-body">
<!-- Tab 1: 疫情处理通知书(只读,国家植保植检管理系统自动拉取) -->
<div v-show="activeProcTab === 'notice'" class="kv-grid">
<div class="kv"><span class="kv-label">编号:</span><input class="ipt-cell" :value="procId" readonly /></div>
<div class="kv"><span class="kv-label">受检单位:</span><input class="ipt-cell" :value="procSource.unit" readonly /></div>
<div class="kv"><span class="kv-label">联系人:</span><input class="ipt-cell" :value="procSource.contact" readonly /></div>
<div class="kv"><span class="kv-label">联系电话:</span><input class="ipt-cell" :value="procSource.phone" readonly /></div>
<div class="kv"><span class="kv-label">产出用途:</span><input class="ipt-cell" :value="procSource.useType || '粮谷类种子'" readonly /></div>
<div class="kv"><span class="kv-label">数量:</span><input class="ipt-cell" :value="procSource.totalArea || procSource.importQty || ''" readonly /></div>
<div class="kv kv-wide">
<span class="kv-label">种植(存放)地点:</span>
<span class="kv-value">
<input class="ipt-cell sm" :value="procSource.surveyProvince" readonly />
<input class="ipt-cell sm" :value="procSource.surveyCity" readonly />
<input class="ipt-cell sm" :value="procSource.surveyDistrict" readonly />
<input class="ipt-cell sm" :value="procSource.surveyTownship" readonly />
<input class="ipt-cell lg" :value="procSource.surveyAddress" readonly />
</span>
</div>
<div class="kv kv-wide">
<span class="kv-label">检验结果:</span>
<textarea class="ipt-cell area block" rows="4" readonly :value="procForm.noticeTestResult"></textarea>
</div>
<div class="kv kv-wide">
<span class="kv-label">处理意见:</span>
<textarea class="ipt-cell area block" rows="4" readonly :value="procForm.noticeTestResult"></textarea>
</div>
</div>
<!-- Tab 2: 植物检疫处理方案 -->
<div v-show="activeProcTab === 'plan'">
<div class="plan-toolbar">
<a class="plan-tool" :href="TEMPLATE_URL" download="植物检疫处理方案模板.docx">
<span class="ico">⬇</span> 模板下载
</a>
<label class="plan-tool plan-tool-upload">
<span class="ico">⬆</span> 方案上传
<input type="file" accept=".docx" class="hidden-file" @change="onPlanUpload" />
</label>
<span v-if="planUploadedName" class="plan-status">已上传:{{ planUploadedName }}(按模板内容已自动填写下方表单)</span>
</div>
<div v-show="planFormVisible" class="form-table">
<div class="ft-row">
<div class="ft-cell ft-label">义务主体名称(盖章):</div>
<div class="ft-cell ft-flex">
<input class="ipt-cell" v-model="procForm.planOrg" placeholder="请输入义务主体名称" />
</div>
<div class="ft-cell ft-label">提交海关时间:</div>
<div class="ft-cell ft-flex">
<input class="ipt-cell" type="date" v-model="procForm.planSubmitDate" />
</div>
</div>
<div class="ft-row">
<div class="ft-cell ft-label">编号及类别:</div>
<div class="ft-cell ft-flex">
<input class="ipt-cell" :value="planId" readonly />
<span class="cat-tag">处理方案</span>
</div>
</div>
<div class="ft-row ft-stack">
<div class="ft-cell ft-label">义务主体承诺:</div>
<div class="ft-cell ft-flex">
<div class="readonly-block">本单位/本人自行委托,按如下处理方案对处理对象实施植物检疫处理,承诺本方案使用药剂、器械和方法符合法律法规、标准、技术规范等要求,并承诺严格按照本实施方案开展动植物检疫处理。</div>
<div class="ft-inline">
<span class="ft-label-inline">联系人姓名:</span>
<input class="ipt-cell" :value="procSource.contact" readonly />
<span class="ft-label-inline">电话:</span>
<input class="ipt-cell" :value="procSource.phone" readonly />
</div>
</div>
</div>
<div class="ft-row">
<div class="ft-cell ft-label">处理原因:</div>
<div class="ft-cell ft-flex"><textarea class="ipt-cell area block" rows="2" maxlength="500" placeholder="请输入500字以内" v-model="procForm.planReason"></textarea></div>
</div>
<div class="ft-row">
<div class="ft-cell ft-label">处理对象:</div>
<div class="ft-cell ft-flex"><input class="ipt-cell" v-model="procForm.planTarget" placeholder="请输入处理对象" /></div>
</div>
<div class="ft-row">
<div class="ft-cell ft-label">处理方法:</div>
<div class="ft-cell ft-flex">
<div class="check-group">
<label v-for="m in planMethods" :key="m.v" class="check-item">
<input type="checkbox" :value="m.v" v-model="procForm.planMethodList" /> {{ m.l }}
</label>
</div>
<div class="ft-inline">
<span class="ft-label-inline">其他:</span>
<input class="ipt-cell" v-model="procForm.planMethodOther" placeholder="如选其他请说明" />
</div>
</div>
</div>
<div class="ft-row">
<div class="ft-cell ft-label">处理药剂:</div>
<div class="ft-cell ft-flex">
<div class="check-group">
<label v-for="d in planDrugs" :key="d.v" class="check-item">
<input type="checkbox" :value="d.v" v-model="procForm.planDrugList" /> {{ d.l }}
</label>
</div>
<div class="ft-inline">
<span class="ft-label-inline">其他:</span>
<input class="ipt-cell" v-model="procForm.planDrugOther" placeholder="如选其他请说明" />
</div>
</div>
</div>
<div class="ft-row">
<div class="ft-cell ft-label">处理剂量/浓度:</div>
<div class="ft-cell ft-flex"><input class="ipt-cell" v-model="procForm.planDose" placeholder="如:30g/m³" /></div>
<div class="ft-cell ft-label">用药总量:</div>
<div class="ft-cell ft-flex"><input class="ipt-cell" v-model="procForm.planTotalDrug" placeholder="如:3kg" /></div>
</div>
<div class="ft-row">
<div class="ft-cell ft-label">使用器械:</div>
<div class="ft-cell ft-flex"><input class="ipt-cell" v-model="procForm.planEquipment" placeholder="请输入使用器械" /></div>
<div class="ft-cell ft-label">药剂作用时长:</div>
<div class="ft-cell ft-flex"><input class="ipt-cell" v-model="procForm.planDuration" placeholder="如:24h" /></div>
</div>
<div class="ft-row">
<div class="ft-cell ft-label">处理场所:</div>
<div class="ft-cell ft-flex">
<span class="ipt-cell sm" readonly>{{ procSource.surveyProvince }}</span>
<span class="ipt-cell sm" readonly>{{ procSource.surveyCity }}</span>
<span class="ipt-cell sm" readonly>{{ procSource.surveyDistrict }}</span>
<span class="ipt-cell sm" readonly>{{ procSource.surveyTownship }}</span>
<span class="ipt-cell lg" readonly>{{ procSource.surveyAddress }}</span>
</div>
</div>
<div class="ft-row">
<div class="ft-cell ft-label">处理人员名称:</div>
<div class="ft-cell ft-flex"><input class="ipt-cell" v-model="procForm.planOperator" placeholder="请输入处理人员" /></div>
<div class="ft-cell ft-label">经过培训合格:</div>
<div class="ft-cell ft-flex">
<label class="rd"><input type="radio" value="是" v-model="procForm.planTrained" /> 是</label>
<label class="rd"><input type="radio" value="否" v-model="procForm.planTrained" /> 否</label>
</div>
</div>
<div class="ft-row">
<div class="ft-cell ft-label">预计作业开始时间:</div>
<div class="ft-cell ft-flex"><input class="ipt-cell" type="date" v-model="procForm.planStartDate" /></div>
</div>
<div class="ft-row">
<div class="ft-cell ft-label">处理过程及相关技术指标、标准或技术规范:</div>
<div class="ft-cell ft-flex"><textarea class="ipt-cell area block" rows="3" maxlength="500" placeholder="请输入500字以内" v-model="procForm.planProcess"></textarea></div>
</div>
<div class="ft-row">
<div class="ft-cell ft-label">个人防护要求:</div>
<div class="ft-cell ft-flex"><textarea class="ipt-cell area block notes" rows="2" maxlength="500" placeholder="请输入500字以内" v-model="procForm.planProtection"></textarea></div>
</div>
<div class="ft-row">
<div class="ft-cell ft-label">效果评价措施:</div>
<div class="ft-cell ft-flex">
<label class="rd"><input type="checkbox" value="过程评价" v-model="procForm.planEvaluation" /> 过程评价</label>
<label class="rd"><input type="checkbox" value="实验室评价" v-model="procForm.planEvaluation" /> 实验室评价</label>
</div>
</div>
<div class="ft-row ft-stack">
<div class="ft-cell ft-label ft-title">动植物检疫处理单位承诺</div>
<div class="ft-cell ft-flex">
<div class="readonly-block">我单位承诺本次作业的操作人员均具备作业能力,本次动植物检疫处理建立在确保现场作业人员职业健康安全的前提下,严格依照上述动植物检疫处理方案规范操作,用药合理规范,处理器械运行正常,计量合格有效,达到相关检疫处理标准要求,有害物质残留浓度低于人体允许的安全阈值。</div>
<div class="ft-inline">
<span class="ft-label-inline">联系人姓名:</span>
<input class="ipt-cell" :value="procSource.contact" readonly />
<span class="ft-label-inline">电话:</span>
<input class="ipt-cell" :value="procSource.phone" readonly />
</div>
<div class="ft-inline" style="text-align: right; margin-top: 8px;">
<span class="ft-label-inline">动植物检疫处理单位名称(盖章):</span>
<input class="ipt-cell" style="flex: 0 0 220px;" :value="procForm.planOrg" readonly />
</div>
</div>
</div>
</div>
</div>
<!-- Tab 3: 疫情处理认定书 -->
<div v-show="activeProcTab === 'cert'" class="cert-body">
<div class="cert-section">
<div class="cert-section-title">检疫机构对处理结果认定意见</div>
<div class="cert-row">
<span class="cert-label">认定意见:</span>
<select class="ipt-cell cert-select" v-model="procForm.certOpinionType" disabled>
<option value="">请选择</option>
<option>疫情处理合格</option>
<option>疫情处理不合格</option>
</select>
</div>
</div>
<div class="cert-section">
<div class="cert-row">
<span class="cert-label">受检单位:</span>
<input class="ipt-cell" :value="procSource.unit" readonly />
<span class="cert-label">联系人:</span>
<input class="ipt-cell" :value="procSource.contact" readonly />
</div>
<div class="cert-row">
<span class="cert-label">联系电话:</span>
<input class="ipt-cell" :value="procSource.phone" readonly />
<span class="cert-label">植物(货物)名称:</span>
<input class="ipt-cell" :value="procSource.plant" readonly />
</div>
<div class="cert-row">
<span class="cert-label">产出用途:</span>
<input class="ipt-cell" value="种子" readonly />
<span class="cert-label">数量:</span>
<input class="ipt-cell" :value="procSource.totalArea || procSource.importQty || ''" readonly />
</div>
<div class="cert-row">
<span class="cert-label">检疫员(签名):</span>
<input class="ipt-cell" :value="procForm.certQuarantineOfficer || '甘州区系统管理员'" readonly />
<span class="cert-label">检疫机构负责人(签名):</span>
<input class="ipt-cell" :value="procForm.certLeader" readonly />
</div>
<div class="cert-row">
<span class="cert-label">日期:</span>
<input class="ipt-cell" :value="procForm.certDate" readonly />
<span class="cert-label">检疫机构(盖章):</span>
<input class="ipt-cell" value="甘州区农业技术推广中心" readonly />
</div>
<div class="cert-row">
<span class="cert-label">种植(存放)地点:</span>
<span class="cert-addr">
<input class="ipt-cell sm" :value="procSource.surveyProvince" readonly />
<input class="ipt-cell sm" :value="procSource.surveyCity" readonly />
<input class="ipt-cell sm" :value="procSource.surveyDistrict" readonly />
<input class="ipt-cell sm" :value="procSource.surveyTownship" readonly />
<input class="ipt-cell lg" :value="procSource.surveyAddress" readonly />
</span>
</div>
<div class="cert-row cert-stack">
<span class="cert-label">检验结果:</span>
<textarea class="ipt-cell area block" rows="2" maxlength="500" placeholder="请输入500字以内" v-model="procForm.noticeTestResult"></textarea>
</div>
<div class="cert-row cert-stack">
<span class="cert-label">检疫机构处理意见:</span>
<textarea class="ipt-cell area block" rows="2" maxlength="500" placeholder="请输入500字以内" v-model="procForm.certHandleOpinion"></textarea>
</div>
<div class="cert-row cert-stack">
<span class="cert-label"><span class="req">*</span> 处理意见落实情况:</span>
<textarea class="ipt-cell area block cert-required" rows="3" maxlength="500" placeholder="请输入500字以内" v-model="procForm.certImplementation"></textarea>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed, watch, onMounted } from "vue";
import { ElMessage } from "element-plus";
import DatePicker from "@/components/DatePicker.vue";
import { list as storeList, removeById, updateRecord, addRecord } from "@/store/pestHandle.js";
import { fieldSurveyData } from "@/data/fieldSurvey.js";
import { syncFromSurvey as syncHandle } from "@/store/pestHandle.js";
import JSZip from "jszip";
const dataList = ref([]);
const viewVisible = ref(false);
const editVisible = ref(false);
const delVisible = ref(false);
const delRow = ref(null);
const form = ref({});
const filters = ref({
handleStart: "", handleEnd: "",
cert: "", plant: "", unit: "",
status: ""
});
const appliedFilters = ref({ ...filters.value });
const currentPage = ref(1);
const pageSize = 10;
const jumpInput = ref(1);
// 从 store 加载 + 同步田间调查
const refreshData = () => {
// 先同步田间调查中的"现场确诊发现应检有害生物"记录
fieldSurveyData.forEach((r) => syncHandle(r));
// 移除已不满足条件的记录
storeList.value = storeList.value.filter((r) => {
const source = fieldSurveyData.find((fs) => fs.id === r.sourceSurveyId);
return source && source.conclusion === "现场确诊发现应检有害生物";
});
dataList.value = [...storeList.value];
};
onMounted(() => {
refreshData();
});
watch(storeList, () => {
dataList.value = [...storeList.value];
}, { deep: true });
const filteredData = computed(() => {
let data = [...dataList.value];
const f = appliedFilters.value;
if (f.cert) data = data.filter((r) => (r.cert || "").includes(f.cert));
if (f.plant) data = data.filter((r) => (r.plant || "").includes(f.plant));
if (f.unit) data = data.filter((r) => (r.unit || "").includes(f.unit));
if (f.handleStart) data = data.filter((r) => (r.handleDate || "") >= f.handleStart);
if (f.handleEnd) data = data.filter((r) => (r.handleDate || "") <= f.handleEnd);
if (f.status) {
if (f.status === "已处理") data = data.filter((r) => !!r.handleDate);
if (f.status === "待处理") data = data.filter((r) => !r.handleDate);
}
return data;
});
const totalPages = computed(() => Math.max(1, Math.ceil(filteredData.value.length / pageSize)));
const pagedData = computed(() => {
const start = (currentPage.value - 1) * pageSize;
return filteredData.value.slice(start, start + pageSize);
});
const pageNumbers = computed(() => {
const arr = [];
for (let i = 1; i <= totalPages.value; i++) arr.push(i);
return arr;
});
const goPage = (p) => {
if (p < 1) p = 1;
if (p > totalPages.value) p = totalPages.value;
currentPage.value = p;
};
const onJump = () => {
if (jumpInput.value >= 1 && jumpInput.value <= totalPages.value) {
currentPage.value = jumpInput.value;
}
};
const onQuery = () => {
appliedFilters.value = JSON.parse(JSON.stringify(filters.value));
currentPage.value = 1;
};
const onReset = () => {
filters.value = {
handleStart: "", handleEnd: "",
cert: "", plant: "", unit: "",
status: ""
};
appliedFilters.value = { ...filters.value };
currentPage.value = 1;
};
const onView = (r) => {
form.value = { ...r };
viewVisible.value = true;
};
const closeView = () => { viewVisible.value = false; };
const onEdit = (r) => {
form.value = JSON.parse(JSON.stringify(r));
editVisible.value = true;
};
const closeEdit = () => { editVisible.value = false; };
const onConfirmEdit = () => {
if (!form.value.handleMethod) { ElMessage.error("请选择处理方法"); return; }
if (!form.value.handleDate) { ElMessage.error("请选择处理日期"); return; }
if (!form.value.handlePerson) { ElMessage.error("请输入处理人"); return; }
if (!form.value.handleResult) { ElMessage.error("请选择处理结果"); return; }
updateRecord(form.value);
editVisible.value = false;
ElMessage.success("保存成功");
};
const onDelete = (r) => {
delRow.value = r;
delVisible.value = true;
};
// 办理弹窗(3 个 tab)
const processVisible = ref(false);
const activeProcTab = ref("notice");
const procTabs = [
{ key: "notice", label: "疫情处理通知书" },
{ key: "plan", label: "植物检疫处理方案" },
{ key: "cert", label: "疫情处理认定书" }
];
const planMethods = [
{ v: "熏蒸", l: "熏蒸" },
{ v: "喷洒", l: "喷洒" },
{ v: "喷雾", l: "喷雾" },
{ v: "冷处理", l: "冷处理" },
{ v: "热处理", l: "热处理" },
{ v: "高温", l: "高温" },
{ v: "微波处理", l: "微波处理" },
{ v: "辐照处理", l: "辐照处理" },
{ v: "浸泡", l: "浸泡" },
{ v: "化制", l: "化制" },
{ v: "焚烧", l: "焚烧" },
{ v: "深埋", l: "深埋" },
{ v: "脱毒", l: "脱毒" },
{ v: "粉碎", l: "粉碎" },
{ v: "其他", l: "其他" }
];
const planDrugs = [
{ v: "硫酰氟", l: "硫酰氟" },
{ v: "溴甲烷", l: "溴甲烷" },
{ v: "磷化铝", l: "磷化铝" },
{ v: "其他", l: "其他" }
];
const TEMPLATE_URL = "/templates/plant_quarantine_plan_template.docx";
const planUploadedName = ref("");
const planFormVisible = ref(true);
// 解析 docx 文本
const extractDocxText = async (file) => {
const zip = await JSZip.loadAsync(file);
const xml = await zip.file("word/document.xml").async("string");
// Parse paragraphs: each <w:p>...</w:p> => string of <w:t> content joined by
const parser = new DOMParser();
const doc = parser.parseFromString(xml, "application/xml");
const paragraphs = [];
doc.querySelectorAll("w\:p, p").forEach((p) => {
const texts = [];
p.querySelectorAll("w\:t, t").forEach((t) => {
if (t.textContent) texts.push(t.textContent);
});
if (texts.length) paragraphs.push(texts.join(""));
});
return paragraphs;
};
// 处理上传
const onPlanUpload = async (e) => {
const file = e.target.files[0];
if (!file) return;
if (!file.name.endsWith(".docx")) {
ElMessage.error("请上传 .docx 格式的模板文件");
return;
}
try {
const paragraphs = await extractDocxText(file);
planUploadedName.value = file.name;
// 简单解析:按段落顺序识别可填充的字段
let currentLabel = "";
const collected = [];
paragraphs.forEach((para) => {
const trimmed = para.trim();
if (!trimmed) return;
// 识别表头标签(包含":")
const labelMatch = trimmed.match(/^(编号及类别|义务主体名称|处理原因|处理对象|处理方法|处理药剂|处理剂量\/浓度|用药总量|使用器械|药剂作用时长|处理场所|处理人员名称|经过培训合格|预计作业开始时间|处理过程及|个人防护要求|效果评价措施|动植物检疫处理单位名称|联系人姓名|电话|提交海关时间|提交时间|日期|经度|纬度|海拔|处理地点|处理方法|备注)(.*)$/);
if (labelMatch) {
if (labelMatch[2].trim()) {
collected.push({ label: labelMatch[1], value: labelMatch[2].trim() });
} else {
currentLabel = labelMatch[1];
}
} else if (currentLabel) {
collected.push({ label: currentLabel, value: trimmed });
currentLabel = "";
}
});
// 根据 collected 数据填充表单
const find = (k) => {
const item = collected.find((c) => c.label === k);
return item ? item.value : "";
};
// 义务主体名称(盖章)
const org = find("义务主体名称");
if (org) procForm.value.planOrg = org;
// 提交海关时间
const submit = find("提交海关时间") || find("提交时间");
if (submit) procForm.value.planSubmitDate = submit;
// 处理原因
const reason = find("处理原因");
if (reason) procForm.value.planReason = reason;
// 处理对象
const target = find("处理对象");
if (target) procForm.value.planTarget = target;
// 处理方法(复选框:根据 "□X" 模式识别)
const methodItems = paragraphs.filter((p) => /□/.test(p) && /(熏蒸|喷洒|喷雾|冷处理|热处理|高温|微波|辐照|浸泡|化制|焚烧|深埋|脱毒|粉碎|其他)/.test(p));
if (methodItems.length) {
const checked = [];
methodItems.forEach((m) => {
const matches = m.match(/[□☑]\s*([熏喷浸化烧深脱粉辐高冷热微碎脱]|其他)/g);
if (matches) {
matches.forEach((match) => {
const val = match.replace(/[□☑]\s*/, "").trim();
if (val) checked.push(val);
});
}
});
if (checked.length) procForm.value.planMethodList = checked;
}
// 处理剂量/浓度
const dose = find("处理剂量/浓度");
if (dose) procForm.value.planDose = dose;
// 用药总量
const totalDrug = find("用药总量");
if (totalDrug) procForm.value.planTotalDrug = totalDrug;
// 使用器械
const equip = find("使用器械");
if (equip) procForm.value.planEquipment = equip;
// 药剂作用时长
const duration = find("药剂作用时长");
if (duration) procForm.value.planDuration = duration;
// 处理人员名称
const operator = find("处理人员名称");
if (operator) procForm.value.planOperator = operator;
// 经过培训合格
const trained = find("经过培训合格");
if (trained) {
if (/是/.test(trained) && !/否/.test(trained)) procForm.value.planTrained = "是";
else if (/否/.test(trained)) procForm.value.planTrained = "否";
}
// 预计作业开始时间
const start = find("预计作业开始时间");
if (start) procForm.value.planStartDate = start;
// 处理过程及相关技术指标、标准或技术规范
const process = find("处理过程及");
if (process) procForm.value.planProcess = process;
// 个人防护要求
const prot = find("个人防护要求");
if (prot) procForm.value.planProtection = prot;
planFormVisible.value = true;
ElMessage.success("方案已上传,按模板内容已自动填写下方表单");
} catch (err) {
console.error(err);
ElMessage.error("模板解析失败:" + (err.message || "未知错误"));
}
// 重置 input value 以便再次上传同名文件
e.target.value = "";
};
const procId = ref("");
const planId = ref("");
const certId = ref("");
const procSource = ref({});
const procForm = ref({});
const procRowId = ref(null);
const genId = (prefix, rowId) => {
const stamp = new Date().toISOString().slice(0, 10).replace(/-/g, "");
return prefix + stamp + (rowId || "").toString().padStart(4, "0");
};
const onProcess = (r) => {
procSource.value = {
unit: r.unit, contact: r.contact, phone: r.phone,
useType: r.useType, totalArea: r.totalArea, importQty: r.importQty,
plant: r.plant, variety: r.variety,
surveyProvince: r.surveyProvince, surveyCity: r.surveyCity, surveyDistrict: r.surveyDistrict,
surveyTownship: r.surveyTownship, surveyAddress: r.surveyAddress,
targetPests: r.targetPests
};
procForm.value = {
noticeTestResult: r.noticeTestResult || "",
noticeHandleOpinion: r.noticeHandleOpinion || r.noticeTestResult || "",
// 植物检疫处理方案
planOrg: r.planOrg || r.unit || "",
planSubmitDate: r.planSubmitDate || new Date().toISOString().slice(0, 10),
planReason: r.planReason || "",
planTarget: r.planTarget || "",
planMethodList: r.planMethodList || [],
planMethodOther: r.planMethodOther || "",
planDrugList: r.planDrugList || [],
planDrugOther: r.planDrugOther || "",
planDose: r.planDose || "",
planTotalDrug: r.planTotalDrug || "",
planEquipment: r.planEquipment || "",
planDuration: r.planDuration || "",
planOperator: r.planOperator || "",
planTrained: r.planTrained || "",
planStartDate: r.planStartDate || "",
planProcess: r.planProcess || "",
planProtection: r.planProtection || "",
planEvaluation: r.planEvaluation || [],
// 疫情处理认定书
certOpinionType: r.certOpinionType || "",
certLeader: r.certLeader || "",
certQuarantineOfficer: r.certQuarantineOfficer || "",
certHandleOpinion: r.certHandleOpinion || "",
certImplementation: r.certImplementation || "",
certDate: r.certDate || new Date().toISOString().slice(0, 10)
};
procId.value = r.noticeNo || genId("DC", r.id);
planId.value = r.planNo || genId("FA", r.id);
certId.value = r.certNo || genId("RD", r.id);
procRowId.value = r.id;
activeProcTab.value = "notice";
processVisible.value = true;
};
// 弹窗关闭:直接关闭,不做保存操作
const onCloseProcess = () => {
processVisible.value = false;
};
const onConfirmDelete = () => {
if (delRow.value) {
removeById(delRow.value.id);
delVisible.value = false;
ElMessage.success("删除成功");
}
};
</script>
<style scoped>
.page-list { padding: 16px 20px; }
.filter-box { background: #fff; border: 1px solid #ebeef5; border-radius: 3px; padding: 18px 20px; margin-bottom: 12px; }
.filter-row { display: flex; align-items: center; flex-wrap: wrap; gap: 14px 24px; margin-bottom: 16px; }
.filter-row:last-child { margin-bottom: 0; }
.filter-item { display: flex; align-items: center; gap: 6px; flex-shrink: 0; min-width: 0; }
.filter-item .label { color: #555; font-size: 14px; white-space: nowrap; flex-shrink: 0; min-width: 84px; }
.dash { color: #999; }
.dp-160 { width: 160px; }
.ipt { width: 160px; height: 32px; padding: 0 8px; border: 1px solid #dcdfe6; border-radius: 3px; font-size: 14px; }
.ipt:focus { border-color: #d71e1e; outline: none; }
.btn { background: #d71e1e; color: #fff; border: 1px solid #d71e1e; height: 32px; padding: 0 18px; border-radius: 3px; font-size: 14px; cursor: pointer; }
.btn:hover { background: #b81818; border-color: #b81818; }
.btn-default { background: #fff; color: #555; border-color: #dcdfe6; }
.btn-default:hover { background: #fafafa; color: #d71e1e; border-color: #d71e1e; }
.btn-danger { background: #fff; color: #d71e1e; border-color: #d71e1e; }
.btn-danger:hover { background: #d71e1e; color: #fff; }
.tbl-wrap { background: #fff; border: 1px solid #ebeef5; border-radius: 3px; overflow: hidden; }
.simple-table { width: 100%; border-collapse: collapse; table-layout: fixed; font-size: 14px; }
.simple-table th, .simple-table td { padding: 12px 4px; text-align: center; vertical-align: middle; border-bottom: 1px solid #ebeef5; word-break: break-all; }
.simple-table th { background: #fafafa; font-size: 14px; font-weight: 600; color: #333; }
.simple-table tbody tr:hover { background: #fafafa; }
.col-unit { text-align: left; padding-left: 10px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 0; }
.org-name { display: block; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.col-action { white-space: nowrap; font-size: 13px; }
.op-link { color: #333; font-size: 14px; cursor: pointer; margin: 0 2px; }
.op-link:hover { color: #d71e1e; }
.empty { padding: 40px 0; color: #999; font-size: 14px; }
.pagination { display: flex; align-items: center; justify-content: flex-end; gap: 6px; padding: 14px 20px; background: #fff; border: 1px solid #ebeef5; border-top: 0; border-radius: 0 0 3px 3px; }
.total { color: #555; font-size: 14px; margin-right: 12px; }
.page-btn { background: #fff; border: 1px solid #dcdfe6; color: #555; height: 28px; min-width: 28px; padding: 0 8px; font-size: 14px; cursor: pointer; border-radius: 3px; }
.page-btn:hover:not(:disabled) { color: #d71e1e; border-color: #d71e1e; }
.page-btn.active { background: #d71e1e; border-color: #d71e1e; color: #fff; }
.page-btn:disabled { cursor: not-allowed; opacity: 0.5; }
.jump { color: #555; font-size: 14px; margin-left: 12px; }
.jump-input { width: 48px; height: 28px; text-align: center; border: 1px solid #dcdfe6; border-radius: 3px; margin: 0 4px; }
.jump-input:focus { border-color: #d71e1e; outline: none; }
.modal-mask { position: fixed; inset: 0; background: rgba(0, 0, 0, 0.45); display: flex; align-items: flex-start; justify-content: center; z-index: 9999; padding: 30px; overflow-y: auto; }
.modal-box { background: #fff; width: 1000px; max-width: 100%; border-radius: 4px; overflow: hidden; }
.modal-box.sm { width: 420px; }
.modal-header { background: #d71e1e; color: #fff; padding: 12px 18px; display: flex; align-items: center; justify-content: space-between; }
.modal-title { font-size: 16px; font-weight: 600; }
.modal-close { font-size: 20px; cursor: pointer; padding: 0 6px; }
.modal-body { padding: 18px 22px; max-height: 70vh; overflow-y: auto; }
.modal-footer { padding: 12px 18px; border-top: 1px solid #ebeef5; display: flex; justify-content: flex-end; gap: 10px; }
.section-title { font-size: 15px; font-weight: 600; color: #d71e1e; margin: 14px 0 10px; padding-left: 8px; border-left: 3px solid #d71e1e; }
.section-title:first-child { margin-top: 0; }
.form-row { display: grid; gap: 12px 20px; margin-bottom: 12px; }
.form-row.cols-2 { grid-template-columns: 1fr 1fr; }
.form-field { display: flex; align-items: center; gap: 8px; }
.form-field.span-2 { grid-column: 1 / -1; }
.form-field .label { color: #555; font-size: 14px; white-space: nowrap; min-width: 100px; text-align: right; }
.form-field .label.req::before { content: "*"; color: #d71e1e; margin-right: 2px; }
.form-field .ctrl { flex: 1; height: 32px; padding: 0 10px; border: 1px solid #dcdfe6; border-radius: 3px; font-size: 14px; }
.form-field .ctrl:focus { border-color: #d71e1e; outline: none; }
.form-field .area-input { height: auto; min-height: 40px; max-height: 80px; line-height: 1.7; padding: 6px 10px; resize: vertical; }
.mb-input { flex: 1; min-height: 32px; padding: 6px 12px; background: #fafafa; border: 1px solid #ebeef5; border-radius: 3px; display: flex; align-items: center; font-size: 14px; color: #333; line-height: 1.6; }
.mb-input.area { align-items: flex-start; white-space: pre-wrap; word-break: break-all; }
.mb-input.notes { min-height: 40px; max-height: 80px; }
.pest-block { margin-top: 10px; background: #fafafa; border: 1px solid #ebeef5; border-radius: 3px; padding: 10px 12px; }
.pest-hint { font-size: 13px; color: #555; margin-bottom: 6px; }
.pest-readonly { background: #fff; border: 1px solid #ebeef5; border-radius: 3px; padding: 8px 12px; min-height: 60px; white-space: pre-wrap; word-break: break-all; line-height: 1.6; color: #333; font-size: 14px; }
.del-tip { font-size: 14px; color: #333; padding: 10px 0; }
.modal-wide { width: 1100px; }
.notice-top { display: flex; align-items: center; justify-content: center; padding: 12px 18px; background: #d71e1e; color: #fff; position: relative; }
.notice-top .notice-title { font-size: 18px; font-weight: 600; }
.notice-top .modal-close { position: absolute; right: 14px; top: 50%; transform: translateY(-50%); color: #fff; font-size: 22px; cursor: pointer; line-height: 1; padding: 0 6px; }
.notice-top .modal-close:hover { opacity: 0.8; }
.notice-title { flex: 1; text-align: center; font-size: 18px; font-weight: 600; }
.notice-body { padding-top: 18px; }
.kv-grid { display: grid; grid-template-columns: 1fr 1fr; column-gap: 24px; row-gap: 14px; }
.kv { display: flex; align-items: center; gap: 6px; min-height: 32px; }
.kv-wide { grid-column: 1 / -1; }
.kv-label { color: #555; font-size: 14px; white-space: nowrap; min-width: 100px; text-align: right; }
.kv-label.req::before { content: "*"; color: #d71e1e; margin-right: 2px; }
.kv-value { display: flex; flex: 1; gap: 6px; flex-wrap: wrap; }
.ipt-cell { height: 32px; padding: 0 10px; border: 1px solid #dcdfe6; border-radius: 3px; font-size: 14px; background: #fafafa; flex: 1; min-width: 100px; }
.ipt-cell.sm { flex: 0 0 90px; min-width: 90px; }
.ipt-cell.lg { flex: 1 1 200px; }
.ipt-cell.area { height: auto; min-height: 60px; padding: 6px 10px; line-height: 1.7; resize: vertical; }
.ipt-cell.area.block { min-height: 60px; }
.ipt-cell:focus { border-color: #d71e1e; outline: none; }
.req { color: #d71e1e; }
.op-notice { color: #d71e1e; }
.proc-tabs { display: flex; border-bottom: 2px solid #ebeef5; background: #fafafa; }
.proc-tab { padding: 10px 22px; font-size: 14px; color: #555; cursor: pointer; border-right: 1px solid #ebeef5; transition: all 0.2s; }
.proc-tab:hover { color: #d71e1e; }
.proc-tab.active { color: #fff; background: #d71e1e; font-weight: 600; }
.ipt-cell.area.block.notes { min-height: 40px; max-height: 80px; }
/* 植物检疫处理方案 表格布局 */
.form-table { display: flex; flex-direction: column; gap: 0; border: 1px solid #ebeef5; border-radius: 3px; overflow: hidden; }
.plan-toolbar { display: flex; align-items: center; gap: 14px; padding: 14px 16px; background: #fafafa; border: 1px solid #ebeef5; border-radius: 3px; margin-bottom: 12px; flex-wrap: wrap; }
.plan-tool { display: inline-flex; align-items: center; gap: 6px; padding: 8px 18px; background: #d71e1e; color: #fff; border-radius: 3px; font-size: 14px; cursor: pointer; transition: background 0.15s; }
.plan-tool:hover { background: #b81818; }
.plan-tool-upload { position: relative; background: #fff; color: #d71e1e; border: 1px solid #d71e1e; }
.plan-tool-upload:hover { background: #fcecec; }
.plan-tool .ico { font-size: 16px; line-height: 1; }
.hidden-file { display: none; }
.plan-status { font-size: 13px; color: #67c23a; }
.plan-hint { font-size: 12px; color: #999; margin-left: auto; }
.ft-row { display: grid; grid-template-columns: 160px 1fr 160px 1fr; border-bottom: 1px solid #ebeef5; min-height: 44px; }
.ft-row:last-child { border-bottom: 0; }
.ft-cell { padding: 8px 10px; display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
.ft-label { background: #fafafa; color: #555; font-size: 14px; border-right: 1px solid #ebeef5; justify-content: flex-end; }
.ft-flex { grid-column: span 3; border-right: 1px solid #ebeef5; }
.ft-flex:last-child { border-right: 0; }
.ft-label.ft-title { background: #fff7f7; color: #d71e1e; font-weight: 600; justify-content: center; grid-column: 1 / -1; }
.ft-inline { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
.ft-label-inline { color: #555; font-size: 14px; white-space: nowrap; }
.check-group { display: flex; flex-wrap: wrap; gap: 8px 18px; }
.check-item { display: inline-flex; align-items: center; gap: 4px; font-size: 14px; color: #333; cursor: pointer; }
.rd { display: inline-flex; align-items: center; gap: 4px; font-size: 14px; color: #333; cursor: pointer; margin-right: 14px; }
.readonly-block { background: #fafafa; padding: 8px 10px; font-size: 13px; line-height: 1.7; color: #555; min-height: 50px; white-space: pre-wrap; }
/* ft-stack 上下结构:标签在上、内容在下 */
.ft-row.ft-stack { grid-template-columns: 1fr; }
.ft-row.ft-stack > .ft-label { grid-column: 1 / -1; border-right: 0; border-bottom: 1px solid #ebeef5; justify-content: flex-start; padding-left: 12px; font-weight: 600; color: #d71e1e; background: #fff7f7; }
.ft-row.ft-stack > .ft-flex { grid-column: 1 / -1; border-right: 0; padding: 12px; align-items: stretch; flex-direction: column; gap: 10px; }
.ft-row.ft-stack > .ft-flex .readonly-block { background: #fafafa; padding: 10px 12px; font-size: 13px; line-height: 1.8; color: #555; border: 1px solid #ebeef5; border-radius: 3px; white-space: pre-wrap; word-break: break-all; }
.ft-row.ft-stack > .ft-flex .ft-inline { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
.cat-tag { background: #fcecec; color: #d71e1e; font-size: 12px; padding: 2px 8px; border-radius: 3px; }
.ipt-cell.sm[readonly] { background: #fafafa; color: #555; cursor: default; }
/* 疫情处理认定书 布局 */
.cert-body { display: flex; flex-direction: column; gap: 16px; }
.cert-section { border: 1px solid #ebeef5; border-radius: 3px; overflow: hidden; }
.cert-section-title { background: #fff7f7; color: #d71e1e; font-size: 14px; font-weight: 600; padding: 10px 14px; border-bottom: 1px solid #ebeef5; }
.cert-row { display: grid; grid-template-columns: 130px 1fr 150px 1fr; padding: 10px 14px; border-bottom: 1px solid #ebeef5; align-items: center; gap: 8px; }
.cert-row:last-child { border-bottom: 0; }
.cert-row.cert-stack { grid-template-columns: 130px 1fr; }
.cert-label { color: #555; font-size: 14px; text-align: right; white-space: nowrap; }
.cert-addr { display: flex; gap: 6px; flex-wrap: wrap; }
.cert-select { max-width: 240px; }
.cert-required { border: 1.5px solid #d71e1e; }
.ipt-cell[readonly] { background: #fafafa; color: #555; cursor: default; }
</style>
<template> <template>
<PagePlaceholder title="报告管理" desc="检疫报告生成与管理开发中" icon="Notebook" /> <div class="page-list">
<div class="filter-box">
<div class="filter-row">
<div class="filter-item">
<label>审批日期:</label>
<DatePicker v-model="filters.approveStart" class="dp-160" placeholder="开始日期" />
<span class="dash">至</span>
<DatePicker v-model="filters.approveEnd" class="dp-160" placeholder="结束日期" />
</div>
<div class="filter-item">
<label>检疫单证号:</label>
<input class="ipt" v-model="filters.cert" placeholder="请输入关键字" />
</div>
<div class="filter-item">
<label>植物名称:</label>
<input class="ipt" v-model="filters.plant" placeholder="请输入关键字" />
</div>
<div class="filter-item">
<label>品种名称:</label>
<input class="ipt" v-model="filters.variety" placeholder="请输入关键字" />
</div>
</div>
<div class="filter-row">
<div class="filter-item">
<label>申报单位:</label>
<input class="ipt" v-model="filters.unit" placeholder="请输入关键字" />
</div>
<div class="filter-item">
<button class="btn" @click="onQuery">查询</button>
<button class="btn btn-default" @click="onReset">重置</button>
</div>
</div>
</div>
<div class="tbl-wrap">
<table class="simple-table">
<colgroup>
<col style="width:5%" />
<col style="width:14%" />
<col style="width:22%" />
<col style="width:10%" />
<col style="width:10%" />
<col style="width:14%" />
<col style="width:17%" />
<col style="width:8%" />
</colgroup>
<thead>
<tr>
<th>序号</th>
<th>检疫单证号</th>
<th>申报单位</th>
<th>植物名称</th>
<th>品种名称</th>
<th>审批日期</th>
<th>检验机构</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr v-for="(r, i) in pagedData" :key="r.id">
<td>{{ (currentPage - 1) * pageSize + i + 1 }}</td>
<td>{{ r.cert }}</td>
<td class="col-unit"><span class="org-name">{{ r.unit }}</span></td>
<td>{{ r.plant }}</td>
<td>{{ r.variety }}</td>
<td>{{ r.entryDate || r.surveyDate }}</td>
<td>{{ r.testOrg }}</td>
<td class="col-action">
<a class="op-link" @click="onView(r)">查看</a>
</td>
</tr>
<tr v-if="!pagedData.length">
<td colspan="8" class="empty">暂无数据</td>
</tr>
</tbody>
</table>
</div>
<div class="pagination">
<span class="total" style="margin-right:auto;">共 {{ filteredData.length }} 条记录</span>
<button class="page-btn" :disabled="currentPage === 1" @click="goPage(1)">首页</button>
<button class="page-btn" :disabled="currentPage === 1" @click="goPage(currentPage - 1)">上一页</button>
<button v-for="p in pageNumbers" :key="p" class="page-btn" :class="{ active: p === currentPage }" @click="goPage(p)">{{ p }}</button>
<button class="page-btn" :disabled="currentPage === totalPages" @click="goPage(currentPage + 1)">下一页</button>
<button class="page-btn" :disabled="currentPage === totalPages" @click="goPage(totalPages)">末页</button>
<span class="jump">跳转至 <input class="jump-input" v-model.number="jumpInput" @keyup.enter="onJump" /> 页</span>
</div>
<div v-if="viewVisible" class="modal-mask" @click.self="viewVisible = false">
<div class="modal-box modal-wide">
<div class="modal-top">
<span class="modal-title">流程详情</span>
<span class="modal-close" @click="viewVisible = false">×</span>
</div>
<div class="modal-body">
<div class="report-actions">
<button class="btn">打印预览</button>
<div class="report-title">引进种子、苗木入境后疫情监测报告</div>
</div>
<div class="report-page">
<div class="kv-grid">
<div class="kv"><span class="kv-label">引种单位:</span><input class="ipt-cell" :value="report.unit" readonly /></div>
<div class="kv"><span class="kv-label">联系人:</span><input class="ipt-cell" :value="report.contact" readonly /></div>
<div class="kv"><span class="kv-label">联系电话:</span><input class="ipt-cell" :value="report.phone" readonly /></div>
<div class="kv"><span class="kv-label">审批单位:</span><input class="ipt-cell" :value="report.approveOrg" readonly /></div>
<div class="kv"><span class="kv-label">地址:</span><input class="ipt-cell" :value="report.address" readonly /></div>
<div class="kv"><span class="kv-label">产出用途:</span><input class="ipt-cell" :value="report.useType" readonly /></div>
<div class="kv"><span class="kv-label">审批单编号:</span><input class="ipt-cell" :value="report.cert" readonly /></div>
<div class="kv"><span class="kv-label">审批数量:</span><input class="ipt-cell" :value="report.approveQty" readonly /></div>
<div class="kv"><span class="kv-label">植物名称:</span><input class="ipt-cell" :value="report.plant" readonly /></div>
<div class="kv"><span class="kv-label">品种名称:</span><input class="ipt-cell" :value="report.variety" readonly /></div>
<div class="kv"><span class="kv-label">种苗来源国(地区):</span><input class="ipt-cell" :value="report.origin" readonly /></div>
<div class="kv"><span class="kv-label">引进数量:</span><input class="ipt-cell" :value="report.importQty" readonly /></div>
<div class="kv">
<span class="kv-label">种植面积:</span>
<span class="kv-value">
<input class="ipt-cell" :value="report.totalArea" readonly />
<span class="unit-suffix">亩</span>
</span>
</div>
<div class="kv">
<span class="kv-label">种植期限:</span>
<span class="kv-value">
<input class="ipt-cell sm date-cell" :value="report.plantPeriodStart" readonly />
<span class="dash-inline">至</span>
<input class="ipt-cell sm date-cell" :value="report.plantPeriodEnd" readonly />
</span>
</div>
<div class="kv kv-wide">
<span class="kv-label">详细种植地点:</span>
<span class="kv-value">
<input class="ipt-cell sm" :value="report.surveyProvince" readonly />
<input class="ipt-cell sm" :value="report.surveyCity" readonly />
<input class="ipt-cell sm" :value="report.surveyDistrict" readonly />
<input class="ipt-cell sm" :value="report.surveyTownship || '请选择乡'" readonly />
<input class="ipt-cell lg" :value="report.surveyAddress" readonly />
</span>
</div>
<div class="kv kv-wide">
<span class="kv-label label-long">应检有害生物名单(中文和学名):</span>
<ul class="pest-list">
<li v-for="(p, idx) in targetPestList" :key="idx">{{ p }}</li>
</ul>
</div>
</div>
</div>
<div class="report-page">
<div class="kv-grid">
<div class="kv kv-wide">
<span class="kv-label">监测及检验方法:</span>
<div class="readonly-scroll">{{ report.surveyMethods }}</div>
</div>
<div class="kv kv-wide">
<span class="kv-label">疫情监测结果:</span>
<div class="kv-value column">
<div class="sub-row">
<div class="sub-label">1.发现危险性有害生物及危害程度:</div>
<div class="readonly-scroll sm">{{ report.dangerPest }}</div>
</div>
<div class="sub-row">
<div class="sub-label">2.发现可疑有害生物及危害程度:</div>
<div class="readonly-scroll sm">{{ report.suspiciousPest }}</div>
</div>
</div>
</div>
<div class="kv kv-wide">
<span class="kv-label">处理意见:</span>
<div class="kv-value column">
<label class="radio-line">
<input type="radio" disabled :checked="report.handleOption === 'compliant'" />
<span class="radio-text">符合国家检疫要求,入境后检疫合格。</span>
</label>
<label class="radio-line">
<input type="radio" disabled :checked="report.handleOption === 'process'" />
<span class="radio-text">发现危险性有害生物,经处理(处理方法附后)后继续检疫监测年,不宜再次从境外引进。</span>
</label>
<label class="radio-line">
<input type="radio" disabled :checked="report.handleOption === 'destroy'" />
<span class="radio-text">发现危险性有害生物,对全部种苗作销毁处理。</span>
</label>
<label class="radio-line">
<input type="radio" disabled :checked="report.handleOption === 'other'" />
<span class="radio-text">其他处理意见:</span>
<div class="readonly-scroll other-text">{{ report.handleOther }}</div>
</label>
</div>
</div>
</div>
</div>
<div class="report-page">
<div class="section-title">检疫单位</div>
<div class="kv-grid">
<div class="kv kv-wide">
<span class="kv-label">省级植物检疫机构(盖章):</span>
<input class="ipt-cell" :value="report.provinceOrg" readonly />
</div>
<div class="kv kv-wide">
<span class="kv-label">田间调查植物检疫机构:</span>
<input class="ipt-cell" :value="report.testOrg" readonly />
</div>
<div class="kv kv-wide">
<span class="kv-label">专职检疫员:</span>
<input class="ipt-cell" :value="report.inspector1" readonly />
</div>
<div class="kv kv-wide">
<span class="kv-label">审批日期:</span>
<input class="ipt-cell date-cell" :value="report.approveDate" readonly />
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template> </template>
<script setup> <script setup>
import PagePlaceholder from "@/components/PagePlaceholder.vue"; import { ref, computed } from "vue";
import { ElMessage } from "element-plus";
import DatePicker from "@/components/DatePicker.vue";
import { fieldSurveyData } from "@/data/fieldSurvey.js";
const dataList = ref([...fieldSurveyData]);
const defaultFilters = () => ({ approveStart: "", approveEnd: "", cert: "", plant: "", variety: "", unit: "" });
const filters = ref(defaultFilters());
const appliedFilters = ref(defaultFilters());
const currentPage = ref(1);
const pageSize = 10;
const jumpInput = ref(1);
const viewVisible = ref(false);
const report = ref({ handleOption: "compliant", handleOther: "" });
const targetPestList = computed(() => {
const text = report.value.targetPests || "";
return text.split(/\n/).map(s => s.trim()).filter(s => s);
});
const filteredData = computed(() => {
const f = appliedFilters.value;
return dataList.value.filter((r) => {
const approveDate = r.entryDate || r.surveyDate || "";
if (f.cert && !(r.cert || "").toLowerCase().includes(f.cert.toLowerCase())) return false;
if (f.plant && !(r.plant || "").toLowerCase().includes(f.plant.toLowerCase())) return false;
if (f.variety && !(r.variety || "").toLowerCase().includes(f.variety.toLowerCase())) return false;
if (f.unit && !(r.unit || "").includes(f.unit)) return false;
if (f.approveStart && approveDate < f.approveStart) return false;
if (f.approveEnd && approveDate > f.approveEnd) return false;
return true;
});
});
const totalPages = computed(() => Math.max(1, Math.ceil(filteredData.value.length / pageSize)));
const pageNumbers = computed(() => {
const t = totalPages.value;
const c = currentPage.value;
if (t <= 7) return Array.from({ length: t }, (_, i) => i + 1);
if (c <= 4) return [1, 2, 3, 4, 5, "...", t];
if (c >= t - 3) return [1, "...", t - 4, t - 3, t - 2, t - 1, t];
return [1, "...", c - 1, c, c + 1, "...", t];
});
const pagedData = computed(() => {
const s = (currentPage.value - 1) * pageSize;
return filteredData.value.slice(s, s + pageSize);
});
const onQuery = () => {
appliedFilters.value = JSON.parse(JSON.stringify(filters.value));
currentPage.value = 1;
ElMessage.success("查询完成,共 " + filteredData.value.length + " 条");
};
const onReset = () => {
filters.value = defaultFilters();
appliedFilters.value = defaultFilters();
currentPage.value = 1;
};
const goPage = (p) => { if (p < 1 || p > totalPages.value) return; currentPage.value = p; };
const onJump = () => { const p = parseInt(jumpInput.value); if (!isNaN(p)) goPage(p); else goPage(1); };
const onView = (row) => {
const address = [row.surveyProvince, row.surveyCity, row.surveyDistrict].filter(Boolean).join("") + (row.surveyAddress || "");
report.value = {
unit: row.unit || "",
contact: row.contact || "",
phone: row.phone || "",
approveOrg: "甘肃省植保植检站",
address: address,
useType: row.useType || "",
cert: row.cert || "",
approveQty: row.approveQty || "",
plant: row.plant || "",
variety: row.variety || "",
origin: row.origin || "",
importQty: row.importQty || "",
totalArea: row.totalArea || "",
plantPeriodStart: row.plantPeriodStart || "",
plantPeriodEnd: row.plantPeriodEnd || "",
surveyProvince: row.surveyProvince || "",
surveyCity: row.surveyCity || "",
surveyDistrict: row.surveyDistrict || "",
surveyTownship: row.surveyTownship || "",
surveyAddress: row.surveyAddress || "",
targetPests: row.targetPests || "",
surveyMethods: row.surveyMethods || "",
dangerPest: (row.conclusion === "现场确诊发现应检有害生物" && row.pestDetail) ? row.pestDetail : "无",
suspiciousPest: "无",
handleOption: "compliant",
handleOther: "",
provinceOrg: "甘肃省植保植检站",
testOrg: row.testOrg || "甘州区农业技术推广中心",
inspector1: row.inspector1 || "",
approveDate: row.entryDate || row.surveyDate || ""
};
viewVisible.value = true;
};
</script> </script>
<style scoped>
.page-list { padding: 16px 20px; }
.filter-box { background: #fff; border: 1px solid #ebeef5; border-radius: 3px; padding: 18px 20px; margin-bottom: 12px; }
.filter-row { display: flex; align-items: center; flex-wrap: wrap; gap: 14px 24px; margin-bottom: 16px; }
.filter-row:last-child { margin-bottom: 0; }
.filter-item { display: flex; align-items: center; gap: 6px; flex-shrink: 0; min-width: 0; }
.filter-item label { color: #555; font-size: 14px; white-space: nowrap; flex-shrink: 0; }
.dash { color: #999; }
.dp-160 { width: 160px; }
.ipt { width: 160px; height: 32px; padding: 0 8px; border: 1px solid #dcdfe6; border-radius: 3px; font-size: 14px; }
.ipt:focus { border-color: #d71e1e; outline: none; }
.btn { background: #d71e1e; color: #fff; border: 1px solid #d71e1e; height: 32px; padding: 0 18px; border-radius: 3px; font-size: 14px; cursor: pointer; }
.btn:hover { background: #b81818; border-color: #b81818; }
.btn-default { background: #fff; color: #555; border-color: #dcdfe6; }
.btn-default:hover { background: #fafafa; color: #d71e1e; border-color: #d71e1e; }
.tbl-wrap { background: #fff; border: 1px solid #ebeef5; border-radius: 3px; overflow: hidden; }
.simple-table { width: 100%; border-collapse: collapse; table-layout: fixed; font-size: 14px; }
.simple-table th, .simple-table td { padding: 14px 6px; text-align: center; vertical-align: middle; border-bottom: 1px solid #ebeef5; }
.simple-table th { background: #fafafa; font-size: 14px; font-weight: 600; color: #333; }
.simple-table tbody tr:hover { background: #fafafa; }
.col-unit { text-align: left; padding-left: 12px; }
.col-unit .org-name { word-break: break-all; }
.col-action { white-space: nowrap; }
.op-link { color: #333; font-size: 14px; cursor: pointer; margin: 0 4px; }
.op-link:hover { color: #d71e1e; }
.empty { padding: 40px 0; color: #999; }
.pagination { display: flex; align-items: center; justify-content: flex-end; gap: 6px; padding: 14px 16px; background: #fff; border: 1px solid #ebeef5; border-top: 0; border-radius: 0 0 3px 3px; font-size: 14px; }
.total { color: #555; margin-right: 12px; }
.page-btn { min-width: 30px; height: 30px; padding: 0 8px; background: #fff; border: 1px solid #dcdfe6; color: #555; border-radius: 3px; cursor: pointer; font-size: 14px; }
.page-btn:hover:not(:disabled) { color: #d71e1e; border-color: #d71e1e; }
.page-btn.active { background: #d71e1e; color: #fff; border-color: #d71e1e; }
.page-btn:disabled { opacity: 0.5; cursor: not-allowed; }
.jump { margin-left: 12px; color: #555; }
.jump-input { width: 50px; height: 30px; text-align: center; border: 1px solid #dcdfe6; border-radius: 3px; margin: 0 4px; font-size: 14px; }
.modal-mask { position: fixed; inset: 0; background: rgba(0, 0, 0, 0.45); display: flex; align-items: center; justify-content: center; z-index: 1000; }
.modal-box { background: #fff; border-radius: 4px; width: 900px; max-width: 95vw; max-height: 90vh; display: flex; flex-direction: column; box-shadow: 0 8px 30px rgba(0, 0, 0, 0.2); overflow: hidden; }
.modal-wide { width: 900px; }
.modal-top { display: flex; align-items: center; justify-content: space-between; padding: 12px 18px; background: #d71e1e; color: #fff; }
.modal-title { font-size: 16px; font-weight: 600; }
.modal-close { width: 22px; height: 22px; line-height: 18px; text-align: center; border: 1px solid #fff; border-radius: 3px; cursor: pointer; font-size: 16px; }
.modal-body { padding: 18px 22px; overflow-y: auto; }
.report-actions { display: flex; align-items: center; margin-bottom: 18px; }
.report-actions .btn { background: #d71e1e; color: #fff; border: 1px solid #d71e1e; padding: 6px 16px; }
.report-title { flex: 1; text-align: center; font-size: 18px; font-weight: 600; color: #333; }
.report-page { border: 1px solid #e6e6e6; border-radius: 4px; padding: 18px 20px; margin-bottom: 16px; background: #fff; }
.kv-grid { display: grid; grid-template-columns: 1fr 1fr; column-gap: 24px; row-gap: 14px; }
.kv { display: flex; align-items: center; gap: 6px; min-height: 32px; }
.kv-wide { grid-column: 1 / -1; }
.kv-label { min-width: 160px; max-width: 160px; width: 160px; text-align: right; color: #555; font-size: 14px; flex-shrink: 0; white-space: nowrap; line-height: 32px; }
.kv-label.label-long { width: 160px; min-width: 160px; max-width: 160px; white-space: normal; line-height: 22px; padding-top: 4px; }
.kv-value { flex: 1; display: flex; align-items: center; gap: 6px; flex-wrap: wrap; min-height: 32px; }
.kv-value.column { flex-direction: column; align-items: stretch; }
.ipt-cell { flex: 1; height: 32px; padding: 0 10px; border: 1px solid #dcdfe6; border-radius: 3px; font-size: 14px; min-width: 0; background: #fff; }
.ipt-cell.sm { width: 130px; flex: 0 0 auto; }
.ipt-cell.lg { flex: 1; min-width: 220px; }
.ipt-cell.area { height: auto; min-height: 70px; line-height: 1.6; padding: 8px 10px; resize: vertical; }
.ipt-cell.date-cell { background: #fafafa; }
.dash-inline { color: #999; }
.unit-suffix { margin-left: 6px; color: #555; font-size: 14px; }
.sub-row { display: flex; flex-direction: column; gap: 4px; margin-bottom: 8px; }
.sub-label { font-size: 14px; color: #555; }
.radio-line { display: flex; align-items: center; gap: 6px; margin-bottom: 8px; font-size: 14px; color: #333; }
.radio-line input { margin-right: 4px; }
.radio-text { flex: 1; }
.other-input { flex: 1; }
/* 应检有害生物名单列表 */
.pest-list { flex: 1; margin: 0; padding: 10px 14px 10px 28px; border: 1px solid #ebeef5; border-radius: 3px; background: #fff; font-size: 14px; line-height: 1.9; color: #333; list-style: disc; }
.pest-list li { padding: 2px 0; word-break: break-all; }
/* 只读滚动文本框 */
.readonly-scroll { flex: 1; min-height: 110px; padding: 8px 12px; border: 1px solid #ebeef5; border-radius: 3px; background: #fff; font-size: 14px; line-height: 1.8; color: #333; white-space: pre-wrap; word-break: break-all; overflow: visible; }
.readonly-scroll.sm { min-height: 70px; }
.readonly-scroll.other-text { min-height: 32px; flex: 1; }
.section-title { font-size: 15px; font-weight: 600; color: #d71e1e; margin: 0 0 14px; padding-left: 10px; border-left: 3px solid #d71e1e; line-height: 1; }
</style>
...@@ -171,6 +171,31 @@ input, select, textarea, button { font-family: inherit; font-size: inherit; outl ...@@ -171,6 +171,31 @@ input, select, textarea, button { font-family: inherit; font-size: inherit; outl
border-radius: 50%; border-radius: 50%;
background: #d71e1e; background: #d71e1e;
} }
.sub-item.has-sub-sub { display: flex; align-items: center; justify-content: space-between; }
.sub-item.has-sub-sub .sub-arrow { font-size: 11px; color: #999; transition: transform 0.2s; margin-right: 4px; }
.sub-item.has-sub-sub.expanded .sub-arrow { transform: rotate(90deg); color: #d71e1e; }
.sub-sub-menu { background: #fef6f6; overflow: hidden; max-height: 0; transition: max-height 0.25s ease; }
.sub-sub-menu.expanded { max-height: 500px; }
.sub-sub-item {
display: block;
padding: 8px 16px 8px 60px;
font-size: 14px;
color: #555;
cursor: pointer;
position: relative;
transition: background 0.15s, color 0.15s;
}
.sub-sub-item:hover { background: #fcecec; color: #d71e1e; }
.sub-sub-item.active { background: #fcecec; color: #d71e1e; font-weight: 600; }
.sub-sub-item.active::before {
content: "";
position: absolute;
left: 46px; top: 50%;
transform: translateY(-50%);
width: 4px; height: 4px;
border-radius: 50%;
background: #d71e1e;
}
/* ========== 内容区 ========== */ /* ========== 内容区 ========== */
.content { flex: 1; display: flex; flex-direction: column; min-width: 0; background: #fff; position: relative; } .content { flex: 1; display: flex; flex-direction: column; min-width: 0; background: #fff; position: relative; }
......
Markdown 格式
0% 或
您添加了 0 人 到此讨论。请谨慎行事。
请先完成此评论的编辑!
请 注册 或者 后发表评论