Compare commits

...

10 Commits

Author SHA1 Message Date
yangjl c2e098d319 焊接日报 2026-07-08 09:51:13 +08:00
yangjl f551bbf984 组队 2026-07-07 18:02:29 +08:00
yangjl 542086a34c 下料完成 2026-07-07 15:15:23 +08:00
yangjl 6ed757e75f 下料抽查 2026-07-06 18:23:35 +08:00
yangjl 52ac32ff95 样式优化 2026-07-06 17:56:55 +08:00
yangjl dd792ab197 样式优化 2026-07-06 17:05:24 +08:00
yangjl 21d11697a0 下料 2026-07-06 16:46:07 +08:00
yangjl ae914b5c24 扫码过滤 2026-07-03 10:36:05 +08:00
yangjl 451e8b4bc3 防腐记录 2026-07-03 10:23:03 +08:00
yangjl cc4b55ed8e 防腐 2026-07-02 19:36:49 +08:00
21 changed files with 2830 additions and 1885 deletions
-7
View File
@@ -1,7 +0,0 @@
{
"permissions": {
"allow": [
"Bash(git checkout *)"
]
}
}
+5
View File
@@ -26,6 +26,11 @@ export const reqUnitByProjectIdUnitType = (projectId, unitType="")=>get(`Unit/ge
*/
export const reqToDoItemByProjectIdUserId = (projectId, personId)=>get(`ToDoItem/getToDoItemByProjectIdUserId?projectId=${projectId}&personId=${personId}`)
/**
* 获取坡口类型列表
*/
export const reqGetGrooveTypeList = ()=>get(`BaseInfo/GetGrooveType`)
/**
* 附件上传
* @param {string} filePath - 本地文件路径
+38 -9
View File
@@ -150,20 +150,49 @@ export const reqMaterialInfoByMaterialCode = (materialCode)=>get(`BaseInfo/GetMa
* @param {String} WeldJointId
*/
export const reqPreWeldJointByWeldJointId = (WeldJointId)=>get(`PreWeldInspection/getPreWeldJointByWeldJointId?WeldJointId=${WeldJointId}`)
/**
* 保存焊前基础信息(材料编码及炉批号校验、材料数量校验)
* @param {Object} data
*/
export const reqSaveCuttingCheck = (data)=> post(`PreWeldInspection/SaveCuttingCheck`, data)
/**
* 获取坡口类型列表
* TODO: 确认接口地址
* 获取下料检查记录
* @param {String} weldJointId
*/
export const reqGetGrooveTypeList = ()=>get(`BaseInfo/GetGrooveType`)
export const reqCuttingCheckByWeldJointId = (weldJointId)=>get(`/PreWeldCuttingCheck/GetRecordsByWeldJointId?weldJointId=${weldJointId}`)
/**
* 保存下料检查记录
* @param {Object} data
*/
export const reqSaveCuttingCheck = (data)=> post(`/PreWeldCuttingCheck/SaveRecord`, data)
/**
* 保存组对抽查记录
*
*/
export const reqSaveFitupCheck = (data)=>post(`PreWeldInspection/SaveFitupCheck`, data)
export const reqSaveFitupCheck = (data)=>post(`/PreWeldFitupCheck/SaveRecord`, data)
/**
* 获取组对检查记录
* @param {String} weldJointId
*/
export const reqFitupCheckByWeldJointId = (weldJointId)=>get(`/PreWeldFitupCheck/GetRecordsByWeldJointId?weldJointId=${weldJointId}`)
/**
* 获取防腐等级
* @returns
*/
export const reqPaintCodeList = ()=>get(`/AntiCorrosionCheck/GetPaintCodeList`)
/**
* 保存防腐填报记录
* @param {Object} data
*/
export const reqSaveAntiCorrosionCheck = (data={})=>post(`/AntiCorrosionCheck/SaveRecord`,data)
/**
* 获取防腐填报记录
* @param {Object} params - { materialCode, projectId }
*/
export const reqGetAntiCorrosionCheck = (params = {}) => {
return get('/AntiCorrosionCheck/GetRecordsByMaterialCode', params)
}
+262 -2
View File
@@ -943,6 +943,12 @@
padding-bottom: 160rpx;
}
// 详情页容器(scanpages 页面统一使用,背景色由各页覆盖)
.detail-page {
min-height: 100vh;
padding-bottom: 180rpx;
}
// 信息分组
.info-group {
background: #ffffff;
@@ -970,7 +976,7 @@
.info-label {
flex-shrink: 0;
width: 180rpx;
width: 200rpx;
font-size: 28rpx;
color: #666;
line-height: 40rpx;
@@ -1023,7 +1029,7 @@
// 可点击行(扫码添加)
&.info-link {
.info-label {
flex: 1;
width: 200rpx;
}
.scan-icon {
@@ -1177,6 +1183,39 @@
}
}
// 主渐变按钮(scanpages 多页面复用)
.btn-primary-gradient {
display: flex;
align-items: center;
justify-content: center;
padding: 24rpx;
background: linear-gradient(135deg, $app-primary, #5a9aff);
border-radius: 14rpx;
box-shadow: 0 6rpx 24rpx rgba($app-primary, 0.3);
&:active {
opacity: 0.9;
transform: scale(0.98);
}
&--disabled {
background: #c5cbd5;
box-shadow: none;
&:active {
opacity: 1;
transform: none;
}
}
&__text {
font-size: 30rpx;
font-weight: 600;
color: #ffffff;
letter-spacing: 2rpx;
}
}
// 网格页面(试压管理等)
.page-grid {
min-height: 100vh;
@@ -1667,3 +1706,224 @@
}
}
}
// ===== 区块标题 =====
.section {
margin: 0 24rpx 20rpx;
animation: fadeUp 0.5s ease-out both;
&:nth-child(2) {
animation-delay: 0.05s;
}
&:nth-child(3) {
animation-delay: 0.15s;
}
&__header {
display: flex;
align-items: center;
gap: 12rpx;
margin-bottom: 14rpx;
padding: 0 6rpx;
}
&__bar {
width: 6rpx;
height: 26rpx;
background: linear-gradient(180deg, $app-primary, #6ba3ff);
border-radius: 3rpx;
flex-shrink: 0;
animation: breathe 2.5s ease-in-out infinite;
}
&__title {
font-size: 28rpx;
font-weight: 600;
color: $app-text;
letter-spacing: 0.5rpx;
}
.img-box {
padding: 20rpx;
border-radius: 16rpx;
background-color: #ffffff;
}
}
// ===== 信息卡片 =====
$card-bg: #ffffff;
$shadow-md: 0 2rpx 12rpx rgba(0, 0, 0, 0.06);
.card {
background: $card-bg;
border-radius: $app-radius;
overflow: hidden;
box-shadow: $shadow-md;
&__row {
display: flex;
align-items: center;
padding: 22rpx 28rpx;
border-bottom: 1rpx solid $app-border;
transition: background 0.25s ease;
&:last-child {
border-bottom: none;
}
&:active {
background: #f5f4f1;
}
&--stack {
flex-direction: column;
align-items: flex-start;
.card__label {
width: auto;
padding-left: 16rpx;
margin-bottom: 12rpx;
&::before {
content: '';
position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%);
width: 4rpx;
height: 16rpx;
background: $app-primary-mid;
border-radius: 2rpx;
}
}
.card__value {
width: 100%;
text-align: left;
}
.card__value-wrap {
width: 100%;
justify-content: flex-start;
}
.card__input {
text-align: left;
min-height: 132rpx;
}
.remark-input {
width: 100%;
text-align: left;
font-size: 28rpx;
color: $app-text;
padding: 20rpx 24rpx;
background: #f7f8fa;
border-radius: 12rpx;
line-height: 1.6;
min-height: 140rpx;
box-sizing: border-box;
&::placeholder {
color: #b0bec5;
}
}
}
}
&__label {
width: 180rpx;
font-size: 26rpx;
color: $app-text-secondary;
flex-shrink: 0;
position: relative;
padding-left: 16rpx;
&::before {
content: '';
position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%);
width: 4rpx;
height: 16rpx;
background: $app-primary-mid;
border-radius: 2rpx;
}
&.required::before {
background: #ff0000;
}
}
&__value {
flex: 1;
font-family: "SF Mono", "Menlo", "Consolas", monospace;
font-size: 28rpx;
font-weight: 500;
color: #0f1724;
letter-spacing: 0.3rpx;
text-align: left;
&--placeholder {
color: #888888;
font-weight: 400;
}
}
&__value-wrap {
flex: 1;
display: flex;
align-items: center;
justify-content: flex-start;
gap: 8rpx;
&--clickable {
min-height: 48rpx;
justify-content: flex-end;
}
}
&__unit {
font-size: 24rpx;
color: $app-text-secondary;
flex-shrink: 0;
}
&__input {
flex: 1;
font-size: 28rpx;
color: $app-text;
text-align: left;
height: 48rpx;
&::placeholder {
color: #b0bec5;
}
}
}
// ===== 不合格区域切换动画 =====
.fail-section {
max-height: 0;
overflow: hidden;
opacity: 0;
transform: translateY(-20rpx);
transition: max-height 0.35s ease, opacity 0.25s ease, transform 0.25s ease;
&.is-visible {
max-height: 600rpx;
}
&.is-enter {
opacity: 1;
transform: translateY(0);
}
&.is-leave {
max-height: 0;
opacity: 0;
transform: translateY(-20rpx);
}
}
+8 -4
View File
@@ -1,8 +1,8 @@
@font-face {
font-family: "nbd-icon"; /* Project id 5193086 */
src: url('//at.alicdn.com/t/c/font_5193086_0r8nbjxfoygi.woff2?t=1781694668587') format('woff2'),
url('//at.alicdn.com/t/c/font_5193086_0r8nbjxfoygi.woff?t=1781694668587') format('woff'),
url('//at.alicdn.com/t/c/font_5193086_0r8nbjxfoygi.ttf?t=1781694668587') format('truetype');
src: url('//at.alicdn.com/t/c/font_5193086_mkaqbwmfan.woff2?t=1782898376381') format('woff2'),
url('//at.alicdn.com/t/c/font_5193086_mkaqbwmfan.woff?t=1782898376381') format('woff'),
url('//at.alicdn.com/t/c/font_5193086_mkaqbwmfan.ttf?t=1782898376381') format('truetype');
}
.nbd-icon {
@@ -13,8 +13,12 @@
-moz-osx-font-smoothing: grayscale;
}
.nbd-icon-chuxiufangfu:before {
content: "\e65a";
}
.nbd-icon-zudui:before {
content: "\e619";
content: "\ebf6";
}
.nbd-icon-xialiaochoujianjilu:before {
-673
View File
@@ -1,673 +0,0 @@
<template>
<view>
<view class="uni-list">
<!-- 隐藏画布,用于标签绘制 -->
<canvas :id="canvasId" :canvas-id="canvasId" type="2d"
:style="{ width: labelWidth + 'px', height: labelHeight + 'px' }"
style="position: fixed; left: -999999rpx; top: -999999rpx" />
<!-- 打印机列表 -->
<view class="uni-list-cell">
<view class="uni-list-cell-left">打印机:</view>
<view class="uni-list-cell-db">
<picker :value="deviceIndex" :range="deviceList" @change="onDeviceChanged" range-key="name">
<view class="uni-input">{{ deviceList[deviceIndex].name }}</view>
</picker>
</view>
</view>
<!-- 出纸方向 -->
<view class="uni-list-cell">
<view class="uni-list-cell-left">出纸方向:</view>
<view class="uni-list-cell-db">
<picker :value="rotationIndex" :range="rotationList" @change="onRotationChanged" range-key="name">
<view class="uni-input">{{ rotationList[rotationIndex].name }}</view>
</picker>
</view>
</view>
<!-- 标签纸类型 -->
<view class="uni-list-cell">
<view class="uni-list-cell-left">纸张类型:</view>
<view class="uni-list-cell-db">
<picker :value="gapIndex" :range="gapList" @change="onGapTypeChanged" range-key="name">
<view class="uni-input">{{ gapList[gapIndex].name }}</view>
</picker>
</view>
</view>
<!-- 打印浓度 -->
<view class="uni-list-cell">
<view class="uni-list-cell-left">打印浓度:</view>
<view class="uni-list-cell-db">
<picker :value="darknessIndex" :range="darknessList" @change="onDarknessChanged" range-key="name">
<view class="uni-input">{{ darknessList[darknessIndex].name }}</view>
</picker>
</view>
</view>
<!-- 打印速度 -->
<view class="uni-list-cell">
<view class="uni-list-cell-left">打印速度:</view>
<view class="uni-list-cell-db">
<picker :value="speedIndex" :range="speedList" @change="onPrintSpeedChanged" range-key="name">
<view class="uni-input">{{ speedList[speedIndex].name }}</view>
</picker>
</view>
</view>
<!-- 图片处理灰度阈值 -->
<view class="uni-list-cell">
<view class="uni-list-cell-left">灰度阈值:</view>
<view class="uni-list-cell-db">
<input class="uni-input" type="number" v-model="threshold" />
</view>
</view>
<!-- 打印方式,两种不同的写法 -->
<view class="uni-list-cell uni-list-cell-pd">
<view class="uni-list-cell-db">JSON配置模式</view>
<switch :checked="jsonMode" @change="onPrintModeChanged" style="transform: scale(0.75)" />
</view>
</view>
<view class="button-sp-area">
<button class="mini-btn" type="warn" size="mini" plain="true" @click="handleStopDiscovery">停止搜索</button>
<button class="mini-btn" type="primary" size="mini" plain="true" @click="handleStartDiscovery">
搜索打印机
</button>
</view>
<view class="button-sp-area">
<button class="mini-btn" type="warn" size="mini" plain="true" @click="handleClosePrinter">
关闭打印机
</button>
<button class="mini-btn" type="primary" size="mini" plain="true" @click="handleOpenPrinter">
打开打印机
</button>
<button class="mini-btn" type="primary" size="mini" @click="handlePrintLabel">打印</button>
</view>
<view style="text-align: center; padding: 10px">
<view style="margin: 10px; border: solid lightgray 1px">
<image v-for="item in previewList" :src="item.value" :key="item.key" class="image" mode="widthFix"
style="margin: 10rpx; border: dashed lightgray 1px" />
</view>
</view>
</view>
</template>
<script>
import {
DrawType,
LPAPIFactory,
LPAPI
} from "../../uni_modules/dothan-lpapi-ble/js_sdk";
export default {
data() {
return {
canvasId: "lpapi-ble-uni1",
labelWidth: 960,
labelHeight: 960,
deviceList: [{
name: "未检测到打印机",
}, ],
rotationIndex: 0,
rotationList: [{
name: "横向打印",
value: 0
},
{
name: "右转90度",
value: 90
},
{
name: "转转180度",
value: 180
},
{
name: "左转90度",
value: 270
},
],
deviceIndex: 0,
gapList: [{
name: "随打印机设置",
value: 255
},
{
name: "小票纸",
value: 0
},
{
name: "不干胶",
value: 2
},
{
name: "卡纸",
value: 3
},
],
gapIndex: 0,
darknessList: [{
name: "随打印机设置",
value: 255
},
{
name: "6 (正常)",
value: 6
},
{
name: "7",
value: 7
},
{
name: "8",
value: 8
},
{
name: "9",
value: 9
},
{
name: "10 (较浓)",
value: 10
},
{
name: "11",
value: 11
},
{
name: "12",
value: 12
},
{
name: "13",
value: 13
},
{
name: "14",
value: 14
},
{
name: "15 (最浓)",
value: 15
},
],
darknessIndex: 0,
speedList: [{
name: "随打印机设置",
value: 255
},
{
name: "最慢",
value: 1
},
{
name: "较慢",
value: 2
},
{
name: "正常",
value: 3
},
{
name: "较快",
value: 4
},
{
name: "最快",
value: 5
},
],
speedIndex: 0,
isAppPlus: false,
isWeiXin: false,
isAlipay: false,
/**
* 图片预览列表
* @type {{value: string; key: string;}[]}
***/
previewList: [],
threshold: 128,
jsonMode: false,
/**
* @type {LPAPI}
*/
lpapi: undefined,
};
},
onLoad(options) {
console.log("========== onLoad ==========");
//option为object类型,会序列化上个页面传递的参数
console.log(`---- type = ${options.type}`, options);
// #ifdef APP-PLUS
this.isAppPlus = true;
// #endif
// #ifdef MP-ALIPAY
this.isAlipay = true;
// #endif
// #ifdef MP-WEIXIN
this.isWeiXin = true;
// #endif
//
this.lpapi = LPAPIFactory.getInstance({
// 日志信息显示级别,值为 0 - 4,0表示不显示调试信息,4表示显示所有调试信息
showLog: 4,
// bleAdapter: bleAdapter,
// writeExtendWaits: 3,
// dataSendMode: 1,
});
// 搜索蓝牙设备
this.lpapi.startDiscovery({
timeout: 0,
deviceFound: (devices) => {
this.onDeviceFound(devices);
},
});
// 微信小程序、支付宝小程序创建绘制上下文的时候,可以不用传递 canvasId,此时会通过离屏Canvas来创建绘制上下文环境。
if (this.isWeiXin) {
this.context = this.lpapi.createDrawContext();
} else {
// 备注:支付宝小程序新版本有问题,需要去掉 canvas 中的 type="2d"才可以。
this.context = this.lpapi.createDrawContext({
// 用于进行标签绘制的画布ID
canvasId: this.canvasId,
});
}
//
if (this.context) {
this.lpapi.setDrawContext(this.context);
// 预览标签内容
this.handlePreviewLabel();
}
},
onCanvasReady() {
console.log("========== onCanvasReady ==========");
console.log(`---- isAlipay: ${this.isAlipay}`);
if (this.isAlipay) {
// 支付宝小程序中在屏 Canvas的使用的时候,1:配置 type="2d",2:设置 onReady 事件,在onReady中通过 selectorQuery来获取canvas实例。
// this.queryCanvas((canvas) => {
// this.context = this.lpapi.createDrawContext({
// canvas: canvas,
// });
// if (this.context) {
// this.lpapi.setDrawContext(this.context);
// }
// this.handlePreviewLabel();
// });
}
},
onShow() {
console.log("========== onShow ==========");
// 建议在 onShow的时候,修改下标签的绘制上下文,避免因在context没有更新导致的 canvas不显示的问题。
if (this.context) {
this.lpapi.setDrawContext(this.context);
}
},
onUnload() {
console.log("========== onUnload ==========");
this.handleClosePrinter();
},
methods: {
queryCanvas(callback) {
// 微信小程序、支付宝小程序等 最新版本初始化方式
// 注意:canvas 中必须配置 type="2d" 才能生效。
try {
uni.createSelectorQuery()
.select(`#${this.canvasId}`)
.node()
.exec((res) => {
console.log(`------- exec.result: --------`);
console.log(res);
const canvas = res[0]?.node;
// this.printCanvas = canvas;
callback(canvas);
});
} catch (e) {
console.warn(`---- 不支持通过该方式获取 Canvas 对象!`);
console.warn(e);
callback(undefined);
}
},
onDeviceChanged(e) {
console.log(`---------- onDeviceChanged:${e.detail.value} ----------`);
this.deviceIndex = e ? e.detail.value : 0;
},
onRotationChanged(e) {
console.log(`---------- onRotationChanged: ${e.detail.value} ----------`);
this.rotationIndex = e ? e.detail.value : 0;
},
onGapTypeChanged(e) {
console.log(`---------- onGapTypeChanged:${e.detail.value} ----------`);
this.gapIndex = e ? e.detail.value : 0;
},
onDarknessChanged(e) {
console.log(`---------- onDarknessChanged:${e.detail.value} ----------`);
this.darknessIndex = e ? e.detail.value : 0;
},
onPrintSpeedChanged(e) {
console.log(`---------- onPrintSpeedChanged:${e.detail.value} ----------`);
this.speedIndex = e ? e.detail.value : 0;
},
onPrintModeChanged(e) {
console.log(`---------- onPrintModeChanged: ${e.detail.value} ----------`);
this.jsonMode = e.detail.value;
this.handlePreviewLabel();
},
handleStartDiscovery() {
uni.showLoading({
title: "正在搜索打印机...",
});
this.lpapi.startDiscovery({
timeout: 5000,
deviceFound: (devices) => {
this.onDeviceFound(devices);
},
adapterStateChange: (result) => {
if (!result.discovering) {
uni.hideLoading();
}
},
});
},
handleStopDiscovery() {
this.lpapi.stopDiscovery();
},
handleOpenPrinter() {
const currDevice = this.getDevice();
const isAlipay = this.isAlipay;
if (currDevice && currDevice.deviceId) {
//
uni.showLoading({
title: "正在链接打印机...",
});
return this.lpapi
.openPrinter({
name: currDevice.name,
deviceId: currDevice.deviceId,
// 如果打印机链接失败的话,可以尝试连续进行多次链接。
tryTimes: isAlipay ? 1 : 5,
success: (resp) => {
console.log(`---- 【打印机链接成功】`);
console.log(resp.resultInfo);
uni.hideLoading();
//
uni.showToast({
title: "打印机链接成功!",
icon: "success",
});
},
fail: (resp) => {
console.warn(`---- 【打印机链接失败】:`);
console.warn(JSON.stringify(resp));
//
uni.hideLoading();
//
uni.showToast({
title: "打印机链接失败!",
icon: "fail",
});
},
})
.then((res) => {
return res.statusCode === 0;
});
} else {
console.warn("---- 未检测到打印机!");
uni.showToast({
title: "未检测到打印机",
icon: "fail",
});
if (typeof callback === "function") {
callback(false);
}
return false;
}
},
handleClosePrinter() {
console.log(`---- 关闭打印机!`);
this.lpapi.closePrinter();
},
onDeviceFound(devices) {
console.log(`---- 检测到打印机:`);
console.log(devices);
if (devices && devices.length > 0) {
this.deviceList.splice(0);
for (let item of devices) {
this.deviceList.push(item);
//
// const advertisData = item.advertisData ? new Uint8Array(item.advertisData) : undefined;
// const advDataStr = advertisData ? LPAUtils.arrayBufferToHex16(advertisData) : "-";
// console.log(`---- advertisData [${item.name}] : [${advDataStr}]`);
}
}
},
getDevice() {
return this.deviceList[this.deviceIndex];
},
getOrientation() {
return this.rotationList[this.rotationIndex].value;
},
getGapType() {
return this.gapList[this.gapIndex].value;
},
getPrintDarkness() {
return this.darknessList[this.darknessIndex].value;
},
getPrintSpeed() {
return this.speedList[this.speedIndex].value;
},
getThreshold() {
return Number(this.threshold);
},
updateCanvas(jobInfo) {
// 如果通过 createSelectorQuery 获取到了 Canvas 实例,则不需要延迟处理;
// 在微信小程序、支付宝小程序中是支持该功能的;
return new Promise((resolve) => {
// 更新canvas大小;
if (jobInfo.canvas) {
this.labelWidth = jobInfo.canvas.width;
this.labelHeight = jobInfo.canvas.height;
// 等 Canvas 大小更新后在执行后续绘制操作;
setTimeout(() => {
resolve(true);
}, 100);
} else {
resolve(true);
}
});
},
handlePreviewLabel() {
// 1. 先清空当前的图片列表
this.previewList.splice(0);
// 2. 创建并绘制标签内容
this.printLabel(true).then((res) => {
console.warn(`----- previewLabel.result: ${res.statusCode}`, res.errMsg);
// 3. 标签绘制完毕后,预览标签内容
if (res.statusCode === 0) {
console.warn(`---- 所有页面全部生成完毕!`);
res.previewData.forEach((value) => {
this.previewList.push({
value: value,
// 为了保证 v-for循环的唯一性,随便生成一个随机ID
key: `${new Date().getTime()}-${Math.random() * 1000}`,
});
});
} else {
console.warn(`---- 标签预览失败!`, res);
}
});
},
async handlePrintLabel() {
// 1. 连接打印机
const openResult = await this.handleOpenPrinter();
if (!openResult) {
uni.showToast({
title: "打印机连接失败!",
icon: "success",
});
return;
}
// 2. 创建标签后直接打印
this.printLabel(false).then((res) => {
if (res.statusCode === 0) {
uni.showToast({
title: "打印成功!",
icon: "success",
});
} else {
console.warn(`---- 打印失败!`);
if (res.errMsg) {
uni.showToast({
title: res.errMsg,
icon: "error",
});
}
}
});
},
printLabel(isPreview) {
if (this.jsonMode) {
return this.printLabelWithJsonMode(isPreview);
} else {
return this.printLabelWithDrawMode(isPreview);
}
},
/**
* 通过 draw 的方式绘制标签内容。
*/
async printLabelWithDrawMode(isPreview) {
console.log(`---- qrcodePrintWithDrawMode:`);
const api = this.lpapi;
//
const labelWidth = 40;
const labelHeight = 30;
const margin = 2;
//
const jobInfo = api.startJob({
context: this.context,
width: labelWidth,
height: labelHeight,
orientation: this.getOrientation(),
isPreview: isPreview,
});
//
await this.updateCanvas(jobInfo);
// 绘制外边框
api.drawRectangle({
width: labelWidth,
height: labelHeight,
});
api.draw1DBarcode({
text: "12345678",
x: margin,
y: margin,
width: labelWidth - margin * 2,
height: labelHeight - margin * 2,
textHeight: 5,
barcodeType: 22,
});
//
return api.commitJob({
gapType: this.getGapType(),
printDarkness: this.getPrintDarkness(),
printSpeed: this.getPrintSpeed(),
});
},
/**
* 通过 JSON 方式绘制标签内容。
*/
printLabelWithJsonMode(isPreview) {
console.log(`---- qrcodePrintWithJsonMode:`);
const device = this.getDevice();
const labelWidth = 40;
const labelHeight = 30;
const margin = 2;
//
return this.lpapi.print({
// 配置标签信息
jobInfo: {
jobWidth: labelWidth,
jobHeight: labelHeight,
orientation: this.getOrientation(),
isPreview: isPreview, // 当前任务是否是预览任务
},
// 配置打印机信息
printerInfo: {
name: device?.name,
deviceId: device?.deviceId,
},
jobPage: [{
type: DrawType.rect,
width: labelWidth,
height: labelHeight,
},
{
type: DrawType.barcode,
text: "12345678",
x: margin,
y: margin,
width: labelWidth - margin * 2,
height: labelHeight - margin * 2,
textHeight: 5,
barcodeType: 22,
},
],
onJobCreated: (jobInfo) => {
// 通过双向绑定方式更新画布大小
return this.updateCanvas(jobInfo);
},
onPageComplete(res) {
// 标签页面渲染完毕时的回调函数,预览模式下可展示标签打印效果。
},
onPagePrintComplete(res) {
// 标签页面打印完毕时的回调函数。
},
});
},
},
};
</script>
<style>
button {
margin-top: 15upx;
margin-bottom: 15upx;
height: 40px;
}
.content {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.logo {
height: 200rpx;
width: 200rpx;
margin: 200rpx auto 50rpx auto;
}
.text-area {
display: flex;
justify-content: center;
}
.title {
font-size: 36rpx;
color: #8f8f94;
}
button {
margin-top: 30rpx;
margin-bottom: 30rpx;
}
.button-sp-area {
margin: 0 auto;
width: 90%;
display: flex;
}
.mini-btn {
flex: 1;
margin-right: 10rpx;
height: 40px;
line-height: 40px;
font-size: 18px;
}
</style>
+118
View File
@@ -0,0 +1,118 @@
<template>
<view class="check-seg">
<view
class="check-seg__item"
:class="{ 'check-seg__item--active': isPass, 'is-active-anim': isPass && animKey === 'pass' }"
@click="select(passKey)"
>
<text class="check-seg__icon">{{ '\u2713' }}</text>
<text class="check-seg__text">{{ passText }}</text>
</view>
<view
class="check-seg__item"
:class="{ 'check-seg__item--active': isFail, 'is-active-anim': isFail && animKey === 'fail' }"
@click="select(failKey)"
>
<text class="check-seg__icon">{{ '\u2717' }}</text>
<text class="check-seg__text">{{ failText }}</text>
</view>
</view>
</template>
<script setup>
import { computed, ref, watch, nextTick } from 'vue'
const props = defineProps({
modelValue: { type: [Boolean, Object], default: null },
passKey: { type: String, default: 'pass' },
failKey: { type: String, default: 'fail' },
passText: { type: String, default: '通过' },
failText: { type: String, default: '不通过' }
})
const emit = defineEmits(['update:modelValue'])
const isPass = computed(() => props.modelValue === props.passKey)
const isFail = computed(() => props.modelValue === props.failKey)
const animKey = ref('')
watch(() => props.modelValue, async () => {
animKey.value = ''
await nextTick()
animKey.value = isPass.value ? 'pass' : 'fail'
})
const select = (key) => {
emit('update:modelValue', key)
}
</script>
<style lang="scss" scoped>
.check-seg {
display: inline-flex;
border-radius: 12rpx;
overflow: hidden;
border: 1rpx solid #e5e7eb;
&__item {
display: flex;
align-items: center;
gap: 6rpx;
padding: 12rpx 24rpx;
transition: all 0.25s ease;
background: #f9fafb;
&:first-child {
border-right: 1rpx solid #e5e7eb;
}
&:active {
transform: scale(0.96);
}
&--active {
background: #ebf9f0;
.check-seg__icon { color: #22c55e; }
.check-seg__text { color: #16a34a; font-weight: 600; }
&:first-child {
border-right-color: #b7ebc6;
}
}
&--active:not(:first-child) {
background: #fef2f2;
.check-seg__icon { color: #ef4444; }
.check-seg__text { color: #dc2626; font-weight: 600; }
}
}
&__icon {
font-size: 24rpx;
color: #9ca3af;
font-weight: 700;
transition: color 0.25s ease;
}
&__text {
font-size: 24rpx;
color: #6b7280;
font-weight: 500;
transition: color 0.25s ease;
}
}
// ===== 激活动画(双选分段) =====
@keyframes popIn {
0% { transform: scale(0.5); opacity: 0; }
60% { transform: scale(1.15); }
100% { transform: scale(1); opacity: 1; }
}
.check-seg__item.is-active-anim .check-seg__icon {
animation: popIn 0.3s ease-out;
}
</style>
+85
View File
@@ -0,0 +1,85 @@
<template>
<view class="nvd-photo-view">
<!-- 空状态 -->
<view v-if="photos.length === 0" class="nvd-photo-view__empty">
<text class="nvd-photo-view__empty-text">暂无图片</text>
</view>
<!-- 图片网格 -->
<view v-else class="nvd-photo-view__grid">
<view
v-for="(photo, index) in photos"
:key="photo._id"
class="nvd-photo-view__item"
:style="{ width: `${750 / cols}rpx`, height: `${750 / cols}rpx` }"
@click="handlePreview(index)"
>
<image
class="nvd-photo-view__img"
:src="photo.src"
mode="aspectFill"
/>
</view>
</view>
</view>
</template>
<script setup>
import { computed } from 'vue'
import { parseAttachUrls } from '@/utils/request.js'
const props = defineProps({
/** 逗号分隔的附件路径字符串,如 "FileUpLoad/a.png,FileUpLoad/b.png" */
attachUrl: { type: String, default: '' },
/** 每行显示列数 */
cols: { type: Number, default: 3 }
})
const photos = computed(() => {
if (!props.attachUrl) return []
return parseAttachUrls(props.attachUrl)
})
const handlePreview = (index) => {
const urls = photos.value.map((p) => p.src)
uni.previewImage({ current: index, urls })
}
</script>
<style lang="scss" scoped>
$radius: 10rpx;
$gap: 10rpx;
.nvd-photo-view {
width: 100%;
&__empty {
padding: 48rpx 24rpx;
text-align: center;
&-text {
font-size: 26rpx;
color: #8e99a4;
}
}
&__grid {
display: flex;
flex-wrap: wrap;
}
&__item {
padding: 12rpx;
box-sizing: border-box;
}
&__img {
display: block;
width: 100%;
height: 100%;
border-radius: $radius;
background: #f0f2f5;
border: 1px solid #e0e0e0;
}
}
</style>
+360
View File
@@ -0,0 +1,360 @@
<template>
<view class="record-card">
<!-- 加载中 -->
<view v-if="list.length === 0" class="record-card__empty">
<text class="record-card__empty-text">暂无{{ title }}记录</text>
</view>
<!-- 有记录 -->
<template v-else>
<view class="record-card__section" :style="{ ...props.customStyle }">
<view class="record-card__header">
<view class="record-card__bar"></view>
<text class="record-card__title">{{ title }}记录</text>
<text v-if="list.length > 1" class="record-card__more" @click="showPopup = true">查看全部({{ list.length }})</text>
</view>
<!-- 最新一条记录 -->
<view class="record-card__card" v-if="latest"
:class="cardStatus(latest) === 'pass' ? 'record-card__card--pass' : 'record-card__card--fail'">
<view v-for="field in fields" :key="field.key">
<view v-if="isVisible(field, latest)" :class="field.type === 'photo' ? 'record-card__row--stack' : 'record-card__row'">
<text :class="field.type === 'photo' ? 'record-card__label--stack' : 'record-card__label'">{{ field.label }}</text>
<!-- text(默认) -->
<text v-if="!field.type || field.type === 'text'" class="record-card__value">
{{ latest[field.key] || '--' }}
</text>
<!-- tag -->
<view v-else-if="field.type === 'tag'" class="record-card__tag"
:style="tagBgStyle(field, latest)">
<text class="record-card__tag-icon" :style="tagIconStyle(field, latest)">
{{ tagPass(field, latest) ? '\u2713' : '\u2717' }}
</text>
<text class="record-card__tag-text" :style="tagTextStyle(field, latest)">
{{ tagDisplayText(field, latest) }}
</text>
</view>
<!-- photo -->
<view v-else-if="field.type === 'photo'" class="record-card__value--stack">
<nbd-photo-view :attachUrl="latest[field.key]" :cols="field.cols || 4" />
</view>
</view>
</view>
</view>
</view>
<!-- 弹窗:全部记录 -->
<u-popup v-model="showPopup" mode="bottom" :safe-area-inset-bottom="true">
<view class="record-card__popup">
<view class="record-card__popup-hd">
<text class="record-card__popup-title">全部{{ title }}记录</text>
<text class="record-card__popup-close" @click="showPopup = false">✕</text>
</view>
<scroll-view class="record-card__popup-bd" scroll-y>
<view v-for="(item, index) in list" :key="index"
class="record-card__card record-card__card--popup"
:class="cardStatus(item) === 'pass' ? 'record-card__card--pass' : 'record-card__card--fail'">
<view v-for="field in fields" :key="field.key">
<view v-if="isVisible(field, item)" :class="field.type === 'photo' ? 'record-card__row--stack' : 'record-card__row'">
<text :class="field.type === 'photo' ? 'record-card__label--stack' : 'record-card__label'">{{ field.label }}</text>
<!-- text -->
<text v-if="!field.type || field.type === 'text'" class="record-card__value">
{{ item[field.key] || '--' }}
</text>
<!-- tag -->
<view v-else-if="field.type === 'tag'" class="record-card__tag"
:style="tagBgStyle(field, item)">
<text class="record-card__tag-icon" :style="tagIconStyle(field, item)">
{{ tagPass(field, item) ? '\u2713' : '\u2717' }}
</text>
<text class="record-card__tag-text" :style="tagTextStyle(field, item)">
{{ tagDisplayText(field, item) }}
</text>
</view>
<!-- photo -->
<view v-else-if="field.type === 'photo'" class="record-card__value--stack">
<nbd-photo-view :attachUrl="item[field.key]" :cols="field.cols || 4" />
</view>
</view>
</view>
</view>
</scroll-view>
</view>
</u-popup>
</template>
</view>
</template>
<script setup>
import { ref, computed } from 'vue'
const props = defineProps({
fields: { type: Array, default: () => [] },
list: { type: Array, default: () => [] },
title: { type: String, default: '' },
customStyle: { type: Object, default: () => ({}) },
statusKey: { type: String, default: '' },
statusMatch: { default: '合格' },
})
const showPopup = ref(false)
const latest = computed(() => props.list[0] || null)
// ---- 字段可见性 ----
const isVisible = (field, item) => {
const cond = field.showWhen
if (!cond) return true
const val = item[cond.key]
if (cond.notEqual !== undefined) return val !== cond.notEqual
if (cond.equal !== undefined) return val === cond.equal
return true
}
// ---- tag 辅助 ----
const tagPass = (field, item) => item[field.key] === field.tagMatch
const tagIconStyle = (field, item) => ({
color: tagPass(field, item)
? (field.passColor || '#22c55e')
: (field.failColor || '#ef4444'),
})
const tagTextStyle = (field, item) => ({
color: tagPass(field, item)
? (field.passColor || '#16a34a')
: (field.failColor || '#dc2626'),
})
const tagBgStyle = (field, item) => ({
background: tagPass(field, item)
? (field.passBg || '#ebf9f0')
: (field.failBg || '#fef2f2'),
})
const tagDisplayText = (field, item) =>
tagPass(field, item)
? (field.tagPassText ?? item[field.key] ?? '--')
: (field.tagFailText ?? item[field.key] ?? '--')
// ---- 卡片状态 ----
const cardStatus = (item) => {
if (!props.statusKey) return ''
return item[props.statusKey] === props.statusMatch ? 'pass' : 'fail'
}
</script>
<style lang="scss" scoped>
$primary: #2979ff;
$card-bg: #fff;
$card-shadow: 0 2rpx 16rpx rgba(0, 0, 0, 0.06);
$text: #1a1a2e;
$text-secondary: #8e99a4;
.record-card {
&__empty {
padding: 40rpx 24rpx;
text-align: center;
}
&__empty-text {
font-size: 26rpx;
color: $text-secondary;
}
&__header {
display: flex;
align-items: center;
margin-bottom: 12rpx;
padding: 0 6rpx;
}
&__bar {
width: 6rpx;
height: 32rpx;
border-radius: 3rpx;
background: $primary;
margin-right: 14rpx;
}
&__title {
font-size: 28rpx;
font-weight: 700;
color: $text;
flex: 1;
}
&__more {
font-size: 24rpx;
color: $primary;
padding: 6rpx 16rpx;
background: rgba($primary, 0.08);
border-radius: 20rpx;
}
&__card {
position: relative;
box-sizing: border-box;
width: 100%;
background: $card-bg;
border-radius: 12rpx;
padding: 20rpx;
box-shadow: $card-shadow;
overflow: hidden;
&::before {
content: '';
position: absolute;
left: 0;
top: 0;
right: 0;
height: 6rpx;
transition: background 0.3s;
}
&--pass::before {
background: #22c55e;
}
&--fail::before {
background: #ef4444;
}
}
&__row {
display: flex;
align-items: center;
padding: 8rpx 0;
border-bottom: 1rpx solid #f0f0f0;
&--stack {
flex-direction: column;
align-items: flex-start;
.record-card__value {
margin-top: 8rpx;
width: 100%;
}
}
}
&__label {
font-size: 26rpx;
color: $text-secondary;
flex-shrink: 0;
margin-right: 24rpx;
width: 160rpx;
&--stack {
font-size: 26rpx;
color: $text-secondary;
flex-shrink: 0;
margin-right: 24rpx;
width: 140rpx;
margin-top: 8rpx;
}
}
&__value {
color: $text;
flex: 1;
text-align: left;
font-family: "SF Mono", "Menlo", "Consolas", monospace;
font-size: 28rpx;
font-weight: 500;
color: #0f1724;
letter-spacing: 0.3rpx;
&--stack {
padding-left: 20rpx;
padding-bottom: 8rpx;
padding-top: 8rpx;
font-family: "SF Mono", "Menlo", "Consolas", monospace;
font-size: 28rpx;
font-weight: 500;
color: #0f1724;
letter-spacing: 0.3rpx;
}
}
&__tag {
display: inline-flex;
align-items: center;
gap: 6rpx;
padding: 6rpx 16rpx;
border-radius: 8rpx;
min-width: 100rpx;
justify-content: center;
// background 由 tagBgStyle() 内联控制
&-icon {
font-size: 22rpx;
font-weight: 700;
// color 由 tagIconStyle() 内联控制
}
&-text {
font-size: 24rpx;
font-weight: 600;
// color 由 tagTextStyle() 内联控制
}
}
// ===== 弹窗 =====
&__popup {
background: #fff;
display: flex;
flex-direction: column;
height: 80vh;
border-radius: 24rpx 24rpx 0 0;
}
&__popup-hd {
display: flex;
align-items: center;
justify-content: space-between;
padding: 24rpx 28rpx 12rpx;
border-bottom: 1rpx solid #f0f0f0;
}
&__popup-title {
font-size: 30rpx;
font-weight: 700;
color: $text;
}
&__popup-close {
font-size: 32rpx;
color: $text-secondary;
padding: 8rpx;
}
&__popup-bd {
flex: 1;
box-sizing: border-box;
overflow-y: auto;
background: #f5f5f5;
padding: 12rpx 20rpx 20rpx;
}
&__card--popup {
box-sizing: border-box;
width: 100%;
margin-bottom: 12rpx;
box-shadow: $card-shadow;
}
&__card--popup:last-child {
margin-bottom: 0;
}
}
</style>
+91
View File
@@ -0,0 +1,91 @@
<template>
<view class="submit-bar">
<view
class="submit-btn"
:class="{ 'submit-btn--loading': loading }"
:style="btnStyle"
@click="handleClick"
>
<text v-if="icon" class="submit-btn__icon">{{ icon }}</text>
<text class="submit-btn__text">{{ loading ? '提交中...' : text }}</text>
</view>
</view>
</template>
<script setup>
import { computed } from 'vue'
const props = defineProps({
text: { type: String, default: '提交' },
icon: { type: String, default: '' },
color: { type: String, default: '#2979ff' },
loading: { type: Boolean, default: false }
})
const emit = defineEmits(['click'])
const lighten = (hex, amount = 0.25) => {
const c = hex.replace('#', '')
const r = Math.round(parseInt(c.substring(0, 2), 16) * (1 - amount) + 255 * amount)
const g = Math.round(parseInt(c.substring(2, 4), 16) * (1 - amount) + 255 * amount)
const b = Math.round(parseInt(c.substring(4, 6), 16) * (1 - amount) + 255 * amount)
return `rgb(${r},${g},${b})`
}
const btnStyle = computed(() => ({
background: `linear-gradient(135deg, ${props.color}, ${lighten(props.color)})`,
boxShadow: `0 6rpx 24rpx ${props.color}4d`
}))
const handleClick = () => {
if (!props.loading) emit('click')
}
</script>
<style lang="scss" scoped>
$primary: #2979ff;
$bg: #f4f2ee;
.submit-bar {
position: fixed;
left: 0;
right: 0;
bottom: 0;
z-index: 100;
padding: 20rpx 24rpx;
padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
background: linear-gradient(180deg, rgba($bg, 0) 0%, $bg 20%);
}
.submit-btn {
display: flex;
align-items: center;
justify-content: center;
padding: 24rpx;
border-radius: 14rpx;
transition: opacity 0.2s ease, transform 0.2s ease;
&:active {
opacity: 0.9;
transform: scale(0.98);
}
&--loading {
opacity: 0.7;
pointer-events: none;
}
&__icon {
font-size: 30rpx;
color: #fff;
margin-right: 10rpx;
}
&__text {
font-size: 30rpx;
font-weight: 600;
color: #fff;
letter-spacing: 2rpx;
}
}
</style>
+1
View File
@@ -21,6 +21,7 @@
})()
import App from './App'
import './utils/formatTime'
// #ifndef VUE3
import Vue from 'vue'
+6
View File
@@ -145,6 +145,12 @@
"style": {
"navigationBarTitleText": "焊接日报"
}
},
{
"path": "hj/do_anti_rust",
"style": {
"navigationBarTitleText": "防腐处理"
}
}
]
}
+315 -95
View File
@@ -1,91 +1,152 @@
<template>
<view class="page-detail">
<!-- 信息列表 -->
<view class="info-group">
<view class="info-item">
<text class="info-label">管线</text>
<text class="info-value">{{ form.PipelineCode || '--' }}</text>
</view>
<view class="info-item">
<text class="info-label">焊口</text>
<text class="info-value">{{ form.WeldJointCode || '--' }}</text>
</view>
<view class="info-item info-link" @click="handleShowLocationSelect">
<text class="info-label">焊口位置</text>
<text class="info-value">{{ form.WeldingLocationCode ? `${form.WeldingLocationCode}(${form.WeldingLocationName})` : '请选择' }}</text>
<u-icon name="arrow-right" :size="28" color="#ccc"></u-icon>
</view>
<view class="info-item">
<text class="info-label">焊接日期</text>
<text class="info-value">{{ form.WeldingDateTemp || '--' }}</text>
</view>
<view class="info-item info-link" @click="showTypePicker = true">
<text class="info-label">焊工类型</text>
<text class="info-value">{{ form.welderTypeName || '请选择' }}</text>
<u-icon name="arrow-right" :size="28" color="#ccc"></u-icon>
</view>
<view class="info-item">
<text class="info-label">焊工</text>
<text class="info-value">{{ form.welderName || '--' }}</text>
</view>
<view class="info-item">
<text class="info-label">材质1</text>
<text class="info-value">{{ form.Material1Code || '--' }}</text>
</view>
<view class="info-item">
<text class="info-label">材质2</text>
<text class="info-value">{{ form.Material2Code || '--' }}</text>
</view>
<view class="info-item">
<text class="info-label">达因</text>
<text class="info-value">{{ form.Size || '--' }}</text>
</view>
<view class="info-item">
<text class="info-label">外径</text>
<text class="info-value">{{ form.Dia || '--' }}</text>
</view>
<view class="info-item">
<text class="info-label">DN公称直径</text>
<text class="info-value">{{ form.DNDia || '--' }}</text>
</view>
<view class="info-item">
<text class="info-label">壁厚</text>
<text class="info-value">{{ form.Thickness || '--' }}</text>
</view>
<view class="info-item">
<text class="info-label">规格</text>
<text class="info-value">{{ form.Specification || '--' }}</text>
</view>
<view class="info-item">
<text class="info-label">焊接方法</text>
<text class="info-value">{{ form.WeldingMethodCode || '--' }}</text>
</view>
<view class="info-item">
<text class="info-label">焊缝类型</text>
<text class="info-value">{{ form.WeldTypeCode || '--' }}</text>
<view class="detail-page">
<!-- 焊口主标识 -->
<view class="joint-hero">
<text class="joint-hero__code">{{ form.WeldJointCode || '--' }}</text>
<view class="joint-hero__meta">
<view class="joint-hero__pill">
<text class="pill-label">管线编码</text>
<text class="pill-value">{{ form.PipelineCode || '--' }}</text>
</view>
</view>
</view>
<!-- 焊前拍照 -->
<!-- 焊口信息 -->
<view class="section">
<view class="section-header">
<text class="section-title">焊前拍照</text>
<view class="section__header">
<view class="section__bar"></view>
<text class="section__title">焊口信息</text>
</view>
<view class="section-body">
<nbd-photo-upload ref="beforePhotoRef" v-model:attachUrl="preWeldAttachUrl" typeName="WeldingDaily_Before" />
<view class="card">
<!-- 材质1 -->
<view class="card__row">
<text class="card__label">材质1</text>
<text class="card__value">{{ form.Material1Code || '--' }}</text>
</view>
<!-- 材质2 -->
<view class="card__row">
<text class="card__label">材质2</text>
<text class="card__value">{{ form.Material2Code || '--' }}</text>
</view>
<!-- 达因 -->
<view class="card__row">
<text class="card__label">达因</text>
<text class="card__value">{{ form.Size || '--' }}</text>
</view>
<!-- 外径 -->
<view class="card__row">
<text class="card__label">外径</text>
<text class="card__value">{{ form.Dia || '--' }}</text>
</view>
<!-- DN公称直径 -->
<view class="card__row">
<text class="card__label">DN公称直径</text>
<text class="card__value">{{ form.DNDia || '--' }}</text>
</view>
<!-- 壁厚 -->
<view class="card__row">
<text class="card__label">壁厚</text>
<text class="card__value">{{ form.Thickness || '--' }}</text>
</view>
<!-- 规格 -->
<view class="card__row">
<text class="card__label">规格</text>
<text class="card__value">{{ form.Specification || '--' }}</text>
</view>
<!-- 焊缝类型 -->
<view class="card__row">
<text class="card__label">焊缝类型</text>
<text class="card__value">{{ form.WeldTypeCode || '--' }}</text>
</view>
</view>
</view>
<!-- 焊后拍照 -->
<!-- 焊口填报 -->
<view class="section">
<view class="section-header">
<text class="section-title">焊后拍照</text>
<view class="section__header">
<view class="section__bar"></view>
<text class="section__title">焊口填报</text>
</view>
<view class="section-body">
<nbd-photo-upload ref="afterPhotoRef" v-model:attachUrl="postWeldAttachUrl" typeName="WeldingDaily_After" />
<view class="card">
<!-- 焊口位置 -->
<view class="card__row" @click="handleShowLocationSelect">
<text class="card__label">焊口位置</text>
<view class="card__value-wrap card__value-wrap--clickable">
<text class="card__value" :class="{ 'card__value--placeholder': !form.WeldingLocationCode }">
{{ form.WeldingLocationCode ? `${form.WeldingLocationCode}(${form.WeldingLocationName || '--'})` : '请选择' }}
</text>
<u-icon name="arrow-right" :size="24" color="#bbb" />
</view>
</view>
<!-- 焊接日期 -->
<view class="card__row">
<text class="card__label">焊接日期</text>
<view class="card__value-wrap">
<input class="card__input" v-model="form.WeldingDateTemp" type="text" placeholder="请输入焊接日期" />
</view>
</view>
<!-- 焊工类型 -->
<view class="card__row" @click="showTypePicker = true">
<text class="card__label">焊工类型</text>
<view class="card__value-wrap card__value-wrap--clickable">
<text class="card__value" :class="{ 'card__value--placeholder': !form.welderTypeName }">
{{ form.welderTypeName || '请选择' }}
</text>
<u-icon name="arrow-right" :size="24" color="#bbb" />
</view>
</view>
<!-- 焊工 -->
<view class="card__row">
<text class="card__label">焊工</text>
<view class="card__value-wrap">
<input class="card__input" v-model="form.welderName" type="text" placeholder="请输入焊工" />
</view>
</view>
<!-- 焊接方法 -->
<view class="card__row">
<text class="card__label">焊接方法</text>
<view class="card__value-wrap">
<input class="card__input" v-model="form.WeldingMethodCode" type="text" placeholder="请输入焊接方法" />
</view>
</view>
<!-- 备注 -->
<view class="card__row card__row--stack">
<text class="card__label">备注</text>
<view class="card__value-wrap">
<textarea class="remark-input" v-model="form.Remark" placeholder="请输入备注" :maxlength="200" auto-height />
</view>
</view>
<!-- 焊前拍照 -->
<view class="card__row card__row--stack">
<text class="card__label">焊前拍照</text>
<view class="card__value-wrap">
<nbd-photo-upload ref="beforePhotoRef" v-model:attachUrl="preWeldAttachUrl" typeName="WeldingDaily_Before" />
</view>
</view>
<!-- 焊后拍照 -->
<view class="card__row card__row--stack">
<text class="card__label">焊后拍照</text>
<view class="card__value-wrap">
<nbd-photo-upload ref="afterPhotoRef" v-model:attachUrl="postWeldAttachUrl" typeName="WeldingDaily_After" />
</view>
</view>
</view>
</view>
<!-- 互检上一工序 -->
<view class="nav-row" @click="handleMutualCheck">
<view class="nav-row__left">
<view class="nav-row__dot"></view>
<text class="nav-row__label">互检上一工序</text>
</view>
<text class="nav-row__arrow">›</text>
</view>
<!-- 组对记录 -->
<view class="section">
<nbd-record-card title="组对" :list="fitupList" :fields="fitupFields" status-key="CheckResult" status-match="合格" />
</view>
<!-- 底部按钮 -->
<view class="bottom-btns">
<u-button v-if="!form.WeldingDate" class="btn-submit" size="medium" type="primary" @click="handleReport">填报</u-button>
@@ -104,7 +165,7 @@
<script setup>
import { ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { reqGetWeldJointByWeldJointId, reqSaveWeldingDailyByWeldJointId, reqGetManualPointSave } from '@/api/hj.js'
import { reqGetWeldJointByWeldJointId, reqSaveWeldingDailyByWeldJointId, reqGetManualPointSave, reqFitupCheckByWeldJointId } from '@/api/hj.js'
import { reqWeldingLocation } from '@/api/base.js'
import { useUserStore } from '@/store'
@@ -130,6 +191,28 @@
const beforePhotoRef = ref(null)
const afterPhotoRef = ref(null)
// ===== 组对记录 =====
const weldJointId = ref('')
const fitupList = ref([])
const fitupFields = [
{ label: '坡口类型名称', key: 'GrooveTypeName' },
{ label: '坡口类型编码', key: 'GrooveTypeCode' },
{ label: '坡口加工类型', key: 'GrooveProcessType' },
{ label: '坡口角度', key: 'GrooveAngle' },
{ label: '组对间隙', key: 'FitupGap' },
{ label: '错边量', key: 'Misalignment' },
{ label: '焊缝余高', key: 'WeldReinforcement' },
{ label: '焊缝高度', key: 'WeldHeight' },
{ label: '焊缝宽度', key: 'WeldWidth' },
{ label: '表面缺陷', key: 'SurfaceDefect' },
{ label: '检查人', key: 'CheckPersonName' },
{ label: '检查时间', key: 'CheckTime' },
{ label: '检查结果', key: 'CheckResult', type: 'tag', tagMatch: '合格' },
{ label: '不合格原因', key: 'UnqualifiedReason', showWhen: { key: 'CheckResult', notEqual: '合格' } },
{ label: '整改要求', key: 'RectifyRequirement', showWhen: { key: 'CheckResult', notEqual: '合格' } },
{ label: '照片', key: 'AttachUrl1', type: 'photo', cols: 4 },
]
// ===== 弹窗 ========
const handleShowLocationSelect = () => {
reqWeldingLocation().then(res => {
@@ -192,8 +275,14 @@
const handleClose = () => { uni.navigateBack() }
const handleMutualCheck = () => {
uni.navigateTo({ url: '/scanpages/hj/make_right?id=' + weldJointId.value })
}
// ===== 生命周期 =====
onLoad((opt) => {
weldJointId.value = opt.id || ''
reqGetWeldJointByWeldJointId(opt.id).then(res => {
form.value = res.data || {}
if (form.value.CoverWelderName) {
@@ -212,32 +301,163 @@
preWeldAttachUrl.value = form.value.PreWeldAttachUrl || ''
postWeldAttachUrl.value = form.value.PostWeldAttachUrl || ''
})
reqFitupCheckByWeldJointId(opt.id).then(res => {
if (res.code === 1 && res.data) {
fitupList.value = res.data
}
})
})
</script>
<style scoped lang="scss">
.section {
background: #ffffff;
margin: 24rpx;
border-radius: 16rpx;
overflow: hidden;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.06);
<style lang="scss" scoped>
// 继承全局变量(定义在 uni.scss)
$primary: $app-primary;
$primary-light: $app-primary-light;
$primary-mid: $app-primary-mid;
$primary-dark: $app-primary-dark;
$text: $app-text;
$text-secondary: $app-text-secondary;
// 页面专属
$bg: #f0f2f5;
$card-bg: #ffffff;
$border: #e8ecf1;
$shadow-md: 0 4rpx 16rpx rgba(0, 0, 0, 0.06);
.section-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 24rpx 32rpx 16rpx;
.detail-page {
background: $bg;
min-height: 100vh;
padding-bottom: 180rpx;
}
.section-title {
font-size: 28rpx;
font-weight: 600;
color: #333;
}
}
// ===== 焊口主标识 =====
.joint-hero {
position: relative;
margin: 24rpx;
padding: 36rpx 32rpx 28rpx;
background: $card-bg;
border-radius: $app-radius;
box-shadow: $shadow-md;
overflow: hidden;
.section-body {
padding: 0 24rpx 24rpx;
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 4rpx;
background: linear-gradient(90deg, $primary, #6ba3ff, $primary);
background-size: 200% 100%;
animation: shimmer 3s ease-in-out infinite;
}
@keyframes shimmer {
0%, 100% { background-position: 0% 0; }
50% { background-position: 100% 0; }
}
&__code {
display: block;
font-size: 52rpx;
font-weight: 800;
color: $text;
line-height: 1.2;
word-break: break-all;
font-family: monospace;
letter-spacing: 2rpx;
margin-bottom: 28rpx;
position: relative;
padding-left: 24rpx;
&::before {
content: '';
position: absolute;
left: 0;
top: 6rpx;
bottom: 6rpx;
width: 4rpx;
background: $primary;
border-radius: 2rpx;
}
}
&__meta {
display: flex;
flex-wrap: wrap;
gap: 12rpx;
}
&__pill {
display: inline-flex;
width: 100%;
align-items: center;
gap: 8rpx;
background: $primary-light;
padding: 10rpx 18rpx;
border-radius: 10rpx;
border: 1rpx solid $primary-mid;
.pill-label {
font-size: 22rpx;
color: $text-secondary;
width: 60rpx;
}
.pill-value {
font-size: 24rpx;
font-weight: 600;
color: $primary-dark;
font-family: monospace;
}
}
}
// ---- 互检导航按钮 ----
.nav-row {
display: flex;
align-items: center;
justify-content: space-between;
margin: 0 24rpx;
padding: 24rpx 28rpx;
background: $card-bg;
border-radius: $app-radius;
border-top: 1rpx solid $border;
box-shadow: none;
transition: background 0.2s ease;
margin-bottom: 28rpx;
&:active {
background: #f5f7fa;
}
&__left {
display: flex;
align-items: center;
gap: 14rpx;
}
&__dot {
width: 10rpx;
height: 10rpx;
border-radius: 50%;
background: $primary;
flex-shrink: 0;
}
&__label {
font-size: 28rpx;
font-weight: 600;
color: $text;
letter-spacing: 1rpx;
}
&__arrow {
font-size: 36rpx;
color: $text-secondary;
font-weight: 300;
line-height: 1;
margin-right: -4rpx;
}
}
</style>
+480
View File
@@ -0,0 +1,480 @@
<template>
<view class="detail-page">
<!-- 焊口主标识 -->
<view class="joint-hero">
<text class="joint-hero__code">{{ form.MaterialCode || '--' }}</text>
<view class="joint-hero__meta">
<view class="joint-hero__pill">
<text class="pill-label">材料编码</text>
<text class="pill-value">{{ form.Code || '--' }}</text>
</view>
<view class="joint-hero__pill">
<text class="pill-label">材料名称</text>
<text class="pill-value">{{ form.MaterialName }}</text>
</view>
</view>
</view>
<!-- 材料信息 -->
<view class="section">
<view class="section__header">
<view class="section__bar"></view>
<text class="section__title">材料信息</text>
</view>
<view class="card">
<view class="card__row">
<text class="card__label">炉号</text>
<text class="card__value">
{{ form.HeatNo || '--' }}
</text>
</view>
<view class="card__row">
<text class="card__label">批号</text>
<text class="card__value">{{ form.BatchNo || '--' }}</text>
</view>
<view class="card__row">
<text class="card__label">规格</text>
<text class="card__value">{{ form.MaterialSpec || '--' }}</text>
</view>
<view class="card__row">
<text class="card__label">单位</text>
<text class="card__value">{{ form.MaterialUnit || '--' }}</text>
</view>
<view class="card__row card__row--stack">
<text class="card__label">材料描述</text>
<text class="card__value">{{ form.MaterialDef || '--' }}</text>
</view>
</view>
</view>
<!-- 防腐填报 -->
<view class="section">
<view class="section__header">
<view class="section__bar"></view>
<text class="section__title">防腐填报</text>
</view>
<view class="card">
<!-- 防腐等级/涂漆代码(下拉选择) -->
<view class="card__row" @click="showPaintCodePicker = true">
<text class="card__label required">防腐等级</text>
<view class="card__value-wrap card__value-wrap--clickable">
<text class="card__value" :class="{ 'card__value--placeholder': !antiForm.AnticorrosionLevel }">
{{ antiForm.AnticorrosionLevel || '请选择防腐等级' }}
</text>
<u-icon name="arrow-down" :size="24" color="#bbb" />
</view>
</view>
<!-- 中间漆 -->
<view class="card__row">
<text class="card__label">中间漆</text>
<view class="card__value-wrap">
{{ antiForm.IntermediatePaint || '--' }}
</view>
</view>
<!-- 面漆 -->
<view class="card__row">
<text class="card__label">面漆</text>
<view class="card__value">
{{ antiForm.Topcoat || '--' }}
</view>
</view>
<!-- 漆膜厚度 -->
<view class="card__row">
<text class="card__label required">漆膜厚度</text>
<view class="card__value-wrap">
<input class="card__input" v-model="antiForm.FilmThickness" type="digit"
placeholder="请输入漆膜厚度" />
<text class="card__unit">μm</text>
</view>
</view>
<!-- 喷砂情况 -->
<view class="card__row card__row--stack">
<text class="card__label">喷砂情况</text>
<view class="card__value-wrap">
<textarea class="remark-input" v-model="antiForm.SandBlasting" placeholder="请输入"
:maxlength="200" auto-height />
</view>
</view>
<!-- 除锈喷砂情况 -->
<view class="card__row card__row--stack">
<text class="card__label">除锈喷砂情况</text>
<view class="card__value-wrap">
<textarea class="remark-input" v-model="antiForm.RustSandBlasting" placeholder="请输入"
:maxlength="200" auto-height />
</view>
</view>
</view>
</view>
<!-- 自检 -->
<view class="section">
<view class="section__header">
<view class="section__bar"></view>
<text class="section__title">自检信息</text>
</view>
<view class="card">
<!-- 自检结果 -->
<view class="card__row">
<text class="card__label">自检结果</text>
<view class="card__value-wrap card__value-wrap--clickable">
<nbd-check-btn v-model="antiForm.CheckResult" passKey="合格" failKey="不合格" passText="合格"
failText="不合格" />
</view>
</view>
<view class="fail-section"
:class="{ 'is-visible': failVisible, 'is-enter': failAnimating, 'is-leave': failLeaving }">
<!-- 不合格原因 -->
<view class="card__row card__row--stack">
<text class="card__label">不合格原因</text>
<view class="card__value-wrap">
<textarea class="remark-input" v-model="antiForm.UnqualifiedReason" placeholder="请输入"
:maxlength="200" auto-height />
</view>
</view>
<!-- 整改要求 -->
<view class="card__row card__row--stack">
<text class="card__label">整改要求</text>
<view class="card__value-wrap">
<textarea class="remark-input" v-model="antiForm.RectifyRequirement" placeholder="请输入"
:maxlength="200" auto-height />
</view>
</view>
</view>
<!-- 备注 -->
<view class="card__row card__row--stack">
<text class="card__label">备注</text>
<view class="card__value-wrap">
<textarea class="remark-input" v-model="antiForm.Remark" placeholder="请输入备注信息(选填)"
:maxlength="200" auto-height />
</view>
</view>
<!-- 照片 -->
<view class="card__row card__row--stack">
<text class="card__label">照片</text>
<view class="card__value-wrap">
<nbd-photo-upload ref="photoRef" v-model:attachUrl="files" typeName="AntiRust" />
</view>
</view>
</view>
</view>
<!-- 防腐记录 -->
<view class="section">
<nbd-record-card title="防腐" :list="antiList" :fields="antiFields" status-key="CheckResult" status-match="合格" />
</view>
<nbd-submit-bar text="提交" :loading="submitting" @click="handleSubmit" />
</view>
<nbd-select v-model="showPaintCodePicker" title="选择防腐等级" :list="paintCodeList" label-key="PaintCode" value-key="Id"
@confirm="selectPaintCode" />
</template>
<script setup>
import { ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { storeToRefs } from 'pinia'
import { watch, nextTick } from 'vue'
import { useUserStore } from '@/store'
import { reqMaterialInfoByMaterialCode, reqPaintCodeList, reqSaveAntiCorrosionCheck, reqGetAntiCorrosionCheck } from '@/api/hj.js'
// 防腐记录字段
const antiFields = [
{ label: '防腐等级', key: 'AnticorrosionLevel' },
{ label: '中间漆', key: 'IntermediatePaint' },
{ label: '面漆', key: 'Topcoat' },
{ label: '漆膜厚度', key: 'FilmThickness' },
{ label: '喷砂情况', key: 'SandBlasting' },
{ label: '除锈喷砂情况', key: 'RustSandBlasting' },
{ label: '检查人', key: 'CheckPersonName' },
{ label: '检查时间', key: 'CheckTime' },
{ label: '检查结果', key: 'CheckResult', type: 'tag', tagMatch: '合格' },
{ label: '不合格原因', key: 'UnqualifiedReason', showWhen: { key: 'CheckResult', notEqual: '合格' } },
{ label: '整改要求', key: 'RectifyRequirement', showWhen: { key: 'CheckResult', notEqual: '合格' } },
{ label: '照片', key: 'AttachUrl1', type: 'photo', cols: 4 },
]
const antiList = ref([])
const userStore = useUserStore()
const { userInfo, currentProject } = storeToRefs(userStore)
const form = ref({})
const showPaintCodePicker = ref(false)
const paintCodeList = ref([])
const files = ref('')
// 防腐填报
const antiForm = ref({
AnticorrosionLevel: '', // 防腐等级(显示用)
PaintId: '', // 防腐代码字典ID
IntermediatePaint: '', // 中间漆
Topcoat: '', // 面漆
FilmThickness: '', // 漆膜厚度 (两位小数,单位μm)
RustSandBlasting: '', // 除锈喷砂信息
SandBlasting: '', // 喷砂信息
Remark: '', // 备注
CheckResult: '合格', // 检查结果
UnqualifiedReason: '', // 不合格原因
RectifyRequirement: '', // 整改要求
})
const photoRef = ref(null)
const submitting = ref(false)
const failVisible = ref(antiForm.value.CheckResult === '不合格')
const failAnimating = ref(false)
const failLeaving = ref(false)
watch(() => antiForm.value.CheckResult, async (val) => {
if (val === '不合格') {
failVisible.value = true
await nextTick()
failAnimating.value = true
failLeaving.value = false
} else {
failLeaving.value = true
failAnimating.value = false
setTimeout(() => {
failVisible.value = false
failLeaving.value = false
}, 250)
}
})
const fetchPaintCodes = async () => {
try {
const res = await reqPaintCodeList()
if (res.code === 1 && res.data) {
paintCodeList.value = res.data
}
} catch (err) {
console.error('获取防腐等级列表失败:', err)
}
}
const selectPaintCode = (item) => {
antiForm.value.PaintId = item.Id
antiForm.value.AnticorrosionLevel = item.PaintCode
antiForm.value.IntermediatePaint = item.IntermediatePaint
antiForm.value.Topcoat = item.Topcoat
showPaintCodePicker.value = false
}
onLoad((opt) => {
fetchPaintCodes()
if (opt?.id) {
loadData(opt.id)
}
})
const loadData = async (id) => {
try {
uni.showLoading({ title: '加载中...', mask: true })
const [res, res1] = await Promise.all([
reqMaterialInfoByMaterialCode(encodeURIComponent(id)),
reqGetAntiCorrosionCheck({ materialCode: id, projectId: currentProject.value.ProjectId })
])
if (res.code === 1 && res.data) {
form.value = res.data
}
if (res1.code === 1 && res1.data) {
antiList.value = res1.data
let antiData = res1.data
if (antiData && antiData.length > 0) {
antiForm.value = {
...antiData[0],
CheckResult: '合格',
UnqualifiedReason: '',
RectifyRequirement: '',
Remark: ''
}
}
}
} catch (err) {
console.error('加载焊口数据失败:', err)
uni.showToast({ title: '加载失败', icon: 'none' })
} finally {
uni.hideLoading()
}
}
// 提交
const handleSubmit = async () => {
if (!antiForm.value.PaintId) {
uni.showToast({ title: '请选择防腐等级', icon: 'none' })
return
}
if (!antiForm.value.UnqualifiedReason && antiForm.value.CheckResult === '不合格') {
uni.showToast({ title: '请输入不合格原因', icon: 'none' })
return
}
if (!antiForm.value.RectifyRequirement && antiForm.value.CheckResult === '不合格') {
uni.showToast({ title: '请输入整改要求', icon: 'none' })
return
}
uni.showModal({
title: '确认提交',
content: '确定提交防腐处理结果吗?',
success: async (res) => {
if (res.confirm) {
submitting.value = true
if (photoRef.value?.hasPending()) {
uni.showLoading({ title: '上传照片中...', mask: true })
await photoRef.value.uploadAll()
uni.hideLoading()
}
let data = {
ProjectId: currentProject.value.ProjectId, // 项目ID
MaterialCode: form.value.MaterialCode, // 材料编码
AnticorrosionLevel: antiForm.value.AnticorrosionLevel, // 防腐等级
PaintId: antiForm.value.PaintId, // 防腐代码字典ID
FilmThickness: antiForm.value.FilmThickness, // 漆膜厚度
SandBlasting: antiForm.value.SandBlasting, // 喷砂信息
RustSandBlasting: antiForm.value.RustSandBlasting, // 除锈喷砂信息
Remark: antiForm.value.Remark, // 备注
CheckResult: antiForm.value.CheckResult, // 检查结果
UnqualifiedReason: antiForm.value.UnqualifiedReason, // 不合格原因
RectifyRequirement: antiForm.value.RectifyRequirement, // 整改要求
CheckPerson: userInfo.value.PersonId, // 检查人员
CheckTime: uni.formatTime(new Date().getTime(), 'YYYY-MM-DD HH:mm'), // 检查时间
CreateUser: userInfo.PersonId, // 创建人员
AttachUrl1: files.value, // 照片
}
console.log("提交数据==============>", data)
try {
const res = await reqSaveAntiCorrosionCheck(data)
if (res.code === 1) {
uni.showToast({ title: '提交成功', icon: 'success' })
setTimeout(() => uni.navigateBack(), 1500)
} else {
uni.showToast({ title: res.msg || '提交失败', icon: 'none' })
}
} catch (err) {
console.error('提交防腐填报失败:', err)
uni.showToast({ title: '提交失败', icon: 'none' })
} finally {
submitting.value = false
}
}
}
})
}
</script>
<style lang="scss" scoped>
// 继承全局变量(定义在 uni.scss)
$primary: $app-primary;
$primary-light: $app-primary-light;
$primary-mid: $app-primary-mid;
$primary-dark: $app-primary-dark;
$text: $app-text;
$text-secondary: $app-text-secondary;
$border: $app-border;
$radius: $app-radius;
// 页面专属
$bg: #f4f2ee;
$card-bg: #fefdfb;
$shadow-md: 0 2rpx 12rpx rgba(0, 0, 0, 0.04), 0 8rpx 24rpx rgba(0, 0, 0, 0.06);
.detail-page {
background: $bg;
}
// ===== 焊口主标识 =====
.joint-hero {
position: relative;
margin: 24rpx;
padding: 36rpx 32rpx 28rpx;
background: $card-bg;
border-radius: $radius;
box-shadow: $shadow-md;
overflow: hidden;
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 4rpx;
background: linear-gradient(90deg, $primary, #6ba3ff, $primary);
background-size: 200% 100%;
animation: shimmer 3s ease-in-out infinite;
}
@keyframes shimmer {
0%,
100% {
background-position: 0% 0;
}
50% {
background-position: 100% 0;
}
}
&__code {
display: block;
font-size: 52rpx;
font-weight: 800;
color: $text;
line-height: 1.2;
word-break: break-all;
font-family: monospace;
letter-spacing: 2rpx;
margin-bottom: 28rpx;
position: relative;
padding-left: 24rpx;
&::before {
content: '';
position: absolute;
left: 0;
top: 6rpx;
bottom: 6rpx;
width: 4rpx;
background: $primary;
border-radius: 2rpx;
}
}
&__meta {
display: flex;
flex-wrap: wrap;
gap: 12rpx;
}
&__pill {
display: inline-flex;
width: 100%;
align-items: center;
gap: 8rpx;
background: $primary-light;
padding: 10rpx 18rpx;
border-radius: 10rpx;
border: 1rpx solid $primary-mid;
.pill-label {
font-size: 22rpx;
color: $text-secondary;
width: 60rpx;
}
.pill-value {
font-size: 24rpx;
font-weight: 600;
color: $primary-dark;
font-family: monospace;
}
}
}
</style>
+422 -296
View File
@@ -2,203 +2,334 @@
<view class="detail-page">
<!-- 焊口主标识 -->
<view class="joint-hero">
<text class="joint-hero__code">{{ form.WeldJointCode || '焊口编码' }}</text>
<text class="joint-hero__code">{{ info.WeldJointCode || '--' }}</text>
<view class="joint-hero__meta">
<view class="joint-hero__pill">
<text class="pill-label">管线编码</text>
<text class="pill-value">{{ form.PipelineCode || '--' }}</text>
<text class="pill-value">{{ info.PipelineCode || '--' }}</text>
</view>
<view class="joint-hero__pill">
<text class="pill-label">项目编号</text>
<text class="pill-value">{{ form.ProjectName || form.ProjectId || '--' }}</text>
<text class="pill-label">项目名称</text>
<text class="pill-value">{{ info.ProjectName || info.ProjectId || '--' }}</text>
</view>
</view>
</view>
<!-- 坡口类型 -->
<!-- 组对填报 -->
<view class="section">
<view class="section__header">
<view class="section__bar"></view>
<text class="section__title">坡口类型</text>
<text class="section__title">组对填报</text>
</view>
<view class="card">
<view class="card__row" :class="{ 'card__row--warn': !form.GrooveTypeName }"
@click="showGroovePicker = true">
<text class="card__label"><text class="required">*</text>坡口类型名称</text>
<!-- 坡口类型名称 -->
<view class="card__row" @click="showGrooveTypePicker = true">
<text class="card__label">坡口类型名称</text>
<view class="card__value-wrap card__value-wrap--clickable">
<text class="card__value" :class="{ 'card__value--placeholder': !form.GrooveTypeName }">
{{ form.GrooveTypeName || '请选择坡口类型' }}
<text class="card__value"
:class="{ 'card__value--placeholder': !makeRightForm.GrooveTypeName }">
{{ makeRightForm.GrooveTypeName || '请选择坡口类型' }}
</text>
<u-icon name="arrow-down" :size="24" color="#bbb" />
</view>
</view>
<!-- 坡口类型编码 -->
<view class="card__row">
<text class="card__label">坡口类型编码</text>
<text class="card__value">{{ form.GrooveTypeCode || '--' }}</text>
<view class="card__value-wrap">
{{ makeRightForm.GrooveTypeCode || '--' }}
</view>
</view>
<!-- 坡口加工类型 -->
<view class="card__row">
<text class="card__label">坡口加工类型</text>
<view class="card__value-wrap">
<input class="card__input" v-model="form.GrooveProcessType" type="digit" placeholder="请输入" />
<input class="card__input" v-model="makeRightForm.GrooveProcessType" type="text"
placeholder="请输入坡口加工类型" />
</view>
</view>
</view>
</view>
<!-- 坡口参数 -->
<view class="section">
<view class="section__header">
<view class="section__bar"></view>
<text class="section__title">坡口参数</text>
</view>
<view class="card">
<!-- 坡口角度 -->
<view class="card__row">
<text class="card__label">坡口角度</text>
<view class="card__value-wrap">
<input class="card__input" v-model="form.GrooveAngle" type="digit" placeholder="请输入" />
<text class="card__unit">°</text>
<input class="card__input" v-model="makeRightForm.GrooveAngle" type="digit"
placeholder="请输入坡口角度" />
</view>
</view>
<!-- 组对间隙 -->
<view class="card__row">
<text class="card__label">组对间隙</text>
<view class="card__value-wrap">
<input class="card__input" v-model="form.FitupGap" type="digit" placeholder="请输入" />
<text class="card__unit">mm</text>
<input class="card__input" v-model="makeRightForm.FitupGap" type="digit"
placeholder="请输入组对间隙" />
</view>
</view>
<!-- 错边量 -->
<view class="card__row">
<text class="card__label">错边量</text>
<view class="card__value-wrap">
<input class="card__input" v-model="form.Misalignment" type="digit" placeholder="请输入" />
<text class="card__unit">mm</text>
<input class="card__input" v-model="makeRightForm.Misalignment" type="digit"
placeholder="请输入错边量" />
</view>
</view>
<!-- 焊缝余高 -->
<view class="card__row">
<text class="card__label">焊缝余高</text>
<view class="card__value-wrap">
<input class="card__input" v-model="makeRightForm.WeldReinforcement" type="digit"
placeholder="请输入焊缝余高" />
</view>
</view>
<!-- 焊缝高度 -->
<view class="card__row">
<text class="card__label">焊缝高度</text>
<view class="card__value-wrap">
<input class="card__input" v-model="makeRightForm.WeldHeight" type="digit"
placeholder="请输入焊缝高度" />
</view>
</view>
<!-- 焊缝宽度 -->
<view class="card__row">
<text class="card__label">焊缝宽度</text>
<view class="card__value-wrap">
<input class="card__input" v-model="makeRightForm.WeldWidth" type="digit"
placeholder="请输入焊缝宽度" />
</view>
</view>
<!-- 表面缺陷 -->
<view class="card__row">
<text class="card__label">表面缺陷</text>
<view class="card__value-wrap">
<input class="card__input" v-model="makeRightForm.SurfaceDefect" type="text"
placeholder="请输入表面缺陷" />
</view>
</view>
</view>
</view>
<!-- 备注 -->
<!-- 检查信息 -->
<view class="section">
<view class="section__header">
<view class="section__bar"></view>
<text class="section__title">备注</text>
<text class="section__title">检查信息</text>
</view>
<view class="card">
<!-- 检查结果 -->
<view class="card__row">
<textarea class="remark-input" v-model="remark" placeholder="请输入备注信息(选填)" :maxlength="200" auto-height />
<text class="card__label">检查结果</text>
<view class="card__value-wrap card__value-wrap--clickable">
<nbd-check-btn v-model="makeRightForm.CheckResult" passKey="合格" failKey="不合格" passText="合格"
failText="不合格" />
</view>
</view>
<view class="fail-section"
:class="{ 'is-visible': failVisible, 'is-enter': failAnimating, 'is-leave': failLeaving }">
<!-- 不合格原因 -->
<view class="card__row card__row--stack">
<text class="card__label">不合格原因</text>
<view class="card__value-wrap">
<textarea class="remark-input" v-model="makeRightForm.UnqualifiedReason" placeholder="请输入"
:maxlength="200" auto-height />
</view>
</view>
<!-- 整改要求 -->
<view class="card__row card__row--stack">
<text class="card__label">整改要求</text>
<view class="card__value-wrap">
<textarea class="remark-input" v-model="makeRightForm.RectifyRequirement" placeholder="请输入"
:maxlength="200" auto-height />
</view>
</view>
</view>
<!-- 备注 -->
<view class="card__row card__row--stack">
<text class="card__label">备注</text>
<view class="card__value-wrap">
<textarea class="remark-input" v-model="makeRightForm.Remark" placeholder="请输入备注信息(选填)"
:maxlength="200" auto-height />
</view>
</view>
<!-- 照片 -->
<view class="card__row card__row--stack">
<text class="card__label">照片</text>
<view class="card__value-wrap">
<nbd-photo-upload ref="photoRef" v-model:attachUrl="formAttachUrl" typeName="AntiRust" />
</view>
</view>
</view>
</view>
<!-- 现场照片 -->
<!-- 组对记录 -->
<view class="section">
<view class="section__header">
<view class="section__bar"></view>
<text class="section__title">照片</text>
</view>
<view class="img-box">
<nbd-photo-upload ref="photoRef" v-model:attachUrl="attachUrl" typeName="MakeRight" />
<nbd-record-card title="组对" :list="fitupList" :fields="fitupFields" status-key="CheckResult"
status-match="合格" />
</view>
<!-- 下料记录 -->
<view class="section">
<nbd-record-card title="下料" :list="cuttingList" :fields="cuttingFields" status-key="CheckResult"
status-match="合格" />
<view class="nav-row" @click="handleMutualCheck">
<view class="nav-row__left">
<view class="nav-row__dot"></view>
<text class="nav-row__label">互检上一工序</text>
</view>
<text class="nav-row__arrow">›</text>
</view>
</view>
<block v-for="(item, index) in info.preWeldMaterialItems" :key="item.MaterialCode || index">
<!-- 材料信息 -->
<view class="section">
<view class="section__header">
<view class="section__bar"></view>
<text class="section__title">{{ item.MaterialName || item.MaterialCode || '--' }}</text>
</view>
<view class="card">
<view class="card__row">
<text class="card__label">炉号</text>
<text class="card__value">
{{ item.HeatNo || '--' }}
</text>
</view>
<view class="card__row">
<text class="card__label">批号</text>
<text class="card__value">{{ item.BatchNo || '--' }}</text>
</view>
<view class="card__row">
<text class="card__label">规格</text>
<text class="card__value">{{ item.MaterialSpec || '--' }}</text>
</view>
<view class="card__row">
<text class="card__label">单位</text>
<text class="card__value">{{ item.MaterialUnit || '--' }}</text>
</view>
<view class="card__row card__row--stack">
<text class="card__label">材料描述</text>
<text class="card__value">{{ item.MaterialDef || '--' }}</text>
</view>
</view>
</view>
</block>
<!-- 提交按钮 -->
<view class="submit-bar">
<view class="submit-btn" @click="handleSubmit">
<text class="submit-btn__text">提交</text>
<view class="btn-primary-gradient" @click="handleSubmit">
<text class="btn-primary-gradient__text">提交结果</text>
</view>
</view>
<!-- 坡口类型选择器 -->
<nbd-select v-model="showGroovePicker" title="选择坡口类型" :list="grooveTypeOptions" label-key="BaseInfoName"
value-key="BaseInfoCode" @confirm="selectGrooveType" />
</view>
<nbd-select v-model="showGrooveTypePicker" title="选择坡口类型" :list="grooveTypeList" label-key="BaseInfoName"
value-key="BaseInfoCode" @confirm="selectGrooveType" />
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { ref, watch, nextTick } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { storeToRefs } from 'pinia'
import { useUserStore } from '@/store'
import { reqPreWeldJointByWeldJointId, reqGetGrooveTypeList, reqSaveFitupCheck } from '@/api/hj'
import { reqPreWeldJointByWeldJointId, reqSaveFitupCheck, reqCuttingCheckByWeldJointId, reqFitupCheckByWeldJointId } from '@/api/hj'
import { reqGetGrooveTypeList } from '@/api/base'
const cuttingFields = [
{ label: '下料长度', key: 'CuttingLength' },
{ label: '坡口形式', key: 'GrooveForm' },
{ label: '坡口角度', key: 'GrooveAngle' },
{ label: '材料编码批次', key: 'IsMaterialCodeBatchNoAccurate', type: 'tag', tagMatch: true, tagPassText: '准确', tagFailText: '不准确' },
{ label: '材料数量', key: 'IsMaterialQuantityAccurate', type: 'tag', tagMatch: true, tagPassText: '准确', tagFailText: '不准确' },
{ label: '检查人', key: 'CheckPersonName' },
{ label: '检查时间', key: 'CheckTime' },
{ label: '检查结果', key: 'CheckResult', type: 'tag', tagMatch: '合格' },
{ label: '不合格原因', key: 'UnqualifiedReason', showWhen: { key: 'CheckResult', notEqual: '合格' } },
{ label: '整改要求', key: 'RectifyRequirement', showWhen: { key: 'CheckResult', notEqual: '合格' } },
{ label: '照片', key: 'AttachUrl1', type: 'photo', cols: 4 },
]
const fitupFields = [
{ label: '坡口类型名称', key: 'GrooveTypeName' },
{ label: '坡口类型编码', key: 'GrooveTypeCode' },
{ label: '坡口加工类型', key: 'GrooveProcessType' },
{ label: '坡口角度', key: 'GrooveAngle' },
{ label: '组对间隙', key: 'FitupGap' },
{ label: '错边量', key: 'Misalignment' },
{ label: '焊缝余高', key: 'WeldReinforcement' },
{ label: '焊缝高度', key: 'WeldHeight' },
{ label: '焊缝宽度', key: 'WeldWidth' },
{ label: '表面缺陷', key: 'SurfaceDefect' },
{ label: '检查人', key: 'CheckPersonName' },
{ label: '检查时间', key: 'CheckTime' },
{ label: '检查结果', key: 'CheckResult', type: 'tag', tagMatch: '合格' },
{ label: '不合格原因', key: 'UnqualifiedReason', showWhen: { key: 'CheckResult', notEqual: '合格' } },
{ label: '整改要求', key: 'RectifyRequirement', showWhen: { key: 'CheckResult', notEqual: '合格' } },
{ label: '照片', key: 'AttachUrl1', type: 'photo', cols: 4 },
]
const cuttingList = ref([])
const fitupList = ref([])
const userStore = useUserStore()
const { userInfo, currentProject } = storeToRefs(userStore)
const showGroovePicker = ref(false)
const grooveTypeOptions = ref([])
const attachUrl = ref('')
const info = ref({})
const remark = ref('')
const formAttachUrl = ref('')
const id = ref('')
const makeRightForm = ref({
GrooveTypeId: '', // 坡口类型ID
GrooveTypeName: '', // 坡口类型名称
GrooveTypeCode: '', // 坡口类型编码
GrooveProcessType: '', // 坡口加工类型
GrooveAngle: '', // 坡口角度
FitupGap: '', // 组对间隙
Misalignment: '', // 错边量
WeldReinforcement: '', // 焊缝余高
WeldHeight: '', // 焊缝高度
WeldWidth: '', // 焊缝宽度
SurfaceDefect: '', // 表面缺陷
CheckResult: '合格', // 检查结果
UnqualifiedReason: '', // 不合格原因
RectifyRequirement: '', // 整改要求
Remark: '', // 备注
})
const showGrooveTypePicker = ref(false)
const grooveTypeList = ref([])
// 照片组件引用
const photoRef = ref(null)
const form = ref({
WeldJointId: '',
WeldJointCode: '',
PipelineId: '',
PipelineCode: '',
ProjectId: '',
GrooveTypeId: '',
GrooveTypeCode: '',
GrooveTypeName: '',
GrooveProcessType: '',
GrooveAngle: '',
FitupGap: '',
Misalignment: ''
})
const failVisible = ref(makeRightForm.value.CheckResult === '不合格')
const failAnimating = ref(false)
const failLeaving = ref(false)
const fetchGrooveTypes = async () => {
try {
const res = await reqGetGrooveTypeList()
if (res.code === 1 && res.data) {
grooveTypeOptions.value = res.data
}
} catch (err) {
console.error('获取坡口类型列表失败:', err)
}
}
const selectGrooveType = (item) => {
form.value.GrooveTypeId = item.BaseInfoId
form.value.GrooveTypeCode = item.BaseInfoCode
form.value.GrooveTypeName = item.BaseInfoName
showGroovePicker.value = false
}
onMounted(() => {
fetchGrooveTypes()
})
onLoad((opt) => {
if (opt?.id) {
loadData(opt.id)
}
if (currentProject.value?.ProjectId) {
form.value.ProjectId = currentProject.value.ProjectId
watch(() => makeRightForm.value.CheckResult, async (val) => {
if (val === '不合格') {
failVisible.value = true
await nextTick()
failAnimating.value = true
failLeaving.value = false
} else {
failLeaving.value = true
failAnimating.value = false
setTimeout(() => {
failVisible.value = false
failLeaving.value = false
}, 250)
}
})
const loadData = async (id) => {
try {
uni.showLoading({ title: '加载中...', mask: true })
const res = await reqPreWeldJointByWeldJointId(id)
if (res.code === 1 && res.data) {
form.value = res.data
attachUrl.value = res.data.FitupAttachUrl1 || ""
}
} catch (err) {
console.error('加载焊口数据失败:', err)
uni.showToast({ title: '加载失败', icon: 'none' })
} finally {
uni.hideLoading()
}
const handleMutualCheck = () => {
uni.navigateTo({ url: '/scanpages/hj/material_sampling?id=' + id.value })
}
const handleSubmit = () => {
if (!form.value.GrooveTypeName) {
uni.showToast({ title: '请选择坡口类型', icon: 'none' })
return
}
uni.showModal({
title: '确认提交',
content: '确定提交组对检查结果吗?',
content: '确定提交吗?',
success: async (res) => {
if (res.confirm) {
if (photoRef.value?.hasPending()) {
@@ -206,28 +337,32 @@ const handleSubmit = () => {
await photoRef.value.uploadAll()
uni.hideLoading()
}
const CuttingAttachUrl1 = formAttachUrl.value||''
const checkTime = new Date().toISOString()
const params = {
FitupCheckId: form.value.FitupCheckId || '',
ProjectId: form.value?.ProjectId,
WeldJointId: form.value.WeldJointId,
PipelineCode: form.value.PipelineCode || '',
WeldJointCode: form.value.WeldJointCode || '',
GrooveTypeId: form.value.GrooveTypeId,
GrooveTypeCode: form.value.GrooveTypeCode || '',
GrooveTypeName: form.value.GrooveTypeName || '',
GrooveProcessType: form.value.GrooveProcessType || '',
GrooveAngle: Number(form.value.GrooveAngle) || 0,
FitupGap: Number(form.value.FitupGap) || 0,
Misalignment: Number(form.value.Misalignment) || 0,
CheckPerson: userInfo.value?.PersonId || '',
CheckPersonName: userInfo.value?.PersonName || '',
CheckTime: checkTime,
FitupAttachUrl1: attachUrl.value || form.value.FitupAttachUrl1 || '',
Remark: remark.value || ''
}
ProjectId: currentProject.value?.ProjectId, // 项目ID
WeldJointId: info.value?.WeldJointId, // 焊口ID
GrooveTypeId: makeRightForm.value.GrooveTypeId, // 坡口类型ID
GrooveTypeCode: makeRightForm.value.GrooveTypeCode, // 坡口类型编码
GrooveTypeName: makeRightForm.value.GrooveTypeName, // 坡口类型名称
GrooveProcessType: makeRightForm.value.GrooveProcessType, // 坡口加工类型
GrooveAngle: makeRightForm.value.GrooveAngle, // 坡口角度
FitupGap: makeRightForm.value.FitupGap, // 组对间隙
Misalignment: makeRightForm.value.Misalignment, // 错边量
WeldReinforcement: makeRightForm.value.WeldReinforcement, // 焊缝余高
WeldHeight: makeRightForm.value.WeldHeight, // 焊缝高度
WeldWidth: makeRightForm.value.WeldWidth, // 焊缝宽度
SurfaceDefect: makeRightForm.value.SurfaceDefect, // 表面缺陷
CheckTime: uni.formatTime(new Date().getTime(), 'YYYY-MM-DD HH:mm'), // 检查时间
CreateUser: userInfo.value?.PersonId, // 创建人员
CheckPerson: userInfo.value?.PersonId, // 检查人员
CheckResult: makeRightForm.value.CheckResult, // 检查结果
Remark: remark.value, // 备注
UnqualifiedReason: makeRightForm.value.UnqualifiedReason, // 不合格原因
RectifyRequirement: makeRightForm.value.RectifyRequirement, // 整改要求
AttachUrl1: CuttingAttachUrl1,
}
console.log('提交参数:', params)
await reqSaveFitupCheck(params)
uni.showToast({ title: '提交成功', icon: 'none' })
@@ -236,26 +371,93 @@ const handleSubmit = () => {
}
})
}
const selectGrooveType = (item) => {
makeRightForm.value.GrooveTypeId = item.BaseInfoId
makeRightForm.value.GrooveTypeCode = item.BaseInfoCode
makeRightForm.value.GrooveTypeName = item.BaseInfoName
showGrooveTypePicker.value = false
}
const fetchGrooveTypes = async () => {
try {
const res = await reqGetGrooveTypeList()
if (res.code === 1 && res.data) {
grooveTypeList.value = res.data
}
} catch (err) {
console.error('获取坡口类型列表失败:', err)
}
}
onLoad((opt) => {
fetchGrooveTypes()
if (opt.id) {
id.value = opt.id
getInfo(opt.id)
}
})
const getInfo = async (id) => {
try {
uni.showLoading({ title: '加载中...', mask: true })
const res = await reqPreWeldJointByWeldJointId(id)
if (res.code === 1) {
info.value = res.data
// 获取下料记录
if (info.value.WeldJointId) {
const cuttingRes = await reqCuttingCheckByWeldJointId(info.value.WeldJointId)
if (cuttingRes.code === 1 && cuttingRes.data) {
cuttingList.value = cuttingRes.data
}
const fitupRes = await reqFitupCheckByWeldJointId(info.value.WeldJointId)
if (fitupRes.code === 1 && fitupRes.data) {
fitupList.value = fitupRes.data
if (fitupRes.data.length > 0) {
makeRightForm.value.GrooveTypeId = fitupRes.data[0].GrooveTypeId
makeRightForm.value.GrooveTypeCode = fitupRes.data[0].GrooveTypeCode
makeRightForm.value.GrooveTypeName = fitupRes.data[0].GrooveTypeName
makeRightForm.value.GrooveProcessType = fitupRes.data[0].GrooveProcessType
makeRightForm.value.GrooveAngle = fitupRes.data[0].GrooveAngle
makeRightForm.value.FitupGap = fitupRes.data[0].FitupGap
makeRightForm.value.Misalignment = fitupRes.data[0].Misalignment
makeRightForm.value.WeldReinforcement = fitupRes.data[0].WeldReinforcement
makeRightForm.value.WeldHeight = fitupRes.data[0].WeldHeight
makeRightForm.value.WeldWidth = fitupRes.data[0].WeldWidth
makeRightForm.value.SurfaceDefect = fitupRes.data[0].SurfaceDefect
}
}
}
}
} catch (error) {
console.error('获取焊口详情失败:', error)
uni.showToast({ title: '加载失败', icon: 'none' })
} finally {
uni.hideLoading()
}
}
</script>
<style lang="scss" scoped>
$primary: #2979ff;
$primary-light: #e8f0fe;
$primary-mid: #d0ddff;
$primary-dark: #1a56cc;
// 继承全局变量(定义在 uni.scss)
$primary: $app-primary;
$primary-light: $app-primary-light;
$primary-mid: $app-primary-mid;
$primary-dark: $app-primary-dark;
$text: $app-text;
$text-secondary: $app-text-secondary;
$border: $app-border;
$radius: $app-radius;
// 页面专属
$bg: #f0f2f5;
$text: #0f1724;
$text-secondary: #6b7c93;
$border: #e8ecf1;
$card-bg: #ffffff;
$radius: 16rpx;
$shadow-sm: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
$shadow-md: 0 4rpx 16rpx rgba(0, 0, 0, 0.06);
.detail-page {
min-height: 100vh;
background: $bg;
padding-bottom: 180rpx;
}
// ===== 焊口主标识 =====
@@ -348,147 +550,7 @@ $shadow-md: 0 4rpx 16rpx rgba(0, 0, 0, 0.06);
}
}
// ===== 区块标题 =====
.section {
margin: 0 24rpx 20rpx;
&__header {
display: flex;
align-items: center;
gap: 12rpx;
margin-bottom: 14rpx;
padding: 0 6rpx;
}
&__bar {
width: 6rpx;
height: 26rpx;
background: linear-gradient(180deg, $primary, #6ba3ff);
border-radius: 3rpx;
flex-shrink: 0;
}
&__title {
font-size: 28rpx;
font-weight: 600;
color: $text;
letter-spacing: 0.5rpx;
}
.img-box {
padding: 20rpx;
border-radius: 16rpx;
background-color: #ffffff;
}
}
// ===== 信息卡片 =====
.card {
background: $card-bg;
border-radius: $radius;
overflow: hidden;
box-shadow: $shadow-md;
&__row {
display: flex;
align-items: center;
padding: 22rpx 28rpx;
border-bottom: 1rpx solid $border;
&:last-child {
border-bottom: none;
}
&:active {
background: #fafbfc;
}
&--warn .card__label::before {
background: #ef4444;
}
}
&__label {
width: 180rpx;
font-size: 26rpx;
color: $text-secondary;
flex-shrink: 0;
position: relative;
padding-left: 16rpx;
&::before {
content: '';
position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%);
width: 4rpx;
height: 16rpx;
background: $primary-mid;
border-radius: 2rpx;
}
}
&__value {
flex: 1;
font-size: 28rpx;
color: $text;
font-weight: 500;
text-align: right;
&--placeholder {
color: #b0bec5;
font-weight: 400;
}
}
&__value-wrap {
flex: 1;
display: flex;
align-items: center;
justify-content: flex-end;
gap: 8rpx;
&--clickable {
min-height: 48rpx;
}
}
&__input {
flex: 1;
font-size: 28rpx;
color: $text;
text-align: right;
height: 48rpx;
padding: 0;
&::placeholder {
color: #b0bec5;
}
}
&__unit {
font-size: 24rpx;
color: $text-secondary;
flex-shrink: 0;
}
}
// ===== 备注 =====
.remark-input {
width: 100%;
font-size: 26rpx;
color: $text;
min-height: 120rpx;
padding: 8rpx 0;
line-height: 1.6;
&::placeholder {
color: #b0bec5;
}
}
// ===== 提交栏 =====
// ===== 提交栏(悬浮底部) =====
.submit-bar {
position: fixed;
left: 0;
@@ -496,28 +558,92 @@ $shadow-md: 0 4rpx 16rpx rgba(0, 0, 0, 0.06);
bottom: 0;
padding: 20rpx 24rpx;
padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
background: linear-gradient(180deg, rgba($bg, 0) 0%, $bg 20%);
background: linear-gradient(180deg, rgba(240, 242, 245, 0) 0%, $bg 20%);
}
.submit-btn {
// ================================================================
// 材料质检区块 —— 三合一 section
// ================================================================
// section 容器:微背景 + 内边距聚合三个子元素
.section {
background: #fafbfc;
border-radius: 20rpx;
padding: 24rpx 0 0;
margin-bottom: 28rpx;
box-shadow: $shadow-md;
}
// ---- 材料详情卡片 ----
// 底部圆角清零,与下方 nav-row 无缝衔接
.card {
border-radius: $radius $radius 0 0;
box-shadow: none;
}
// 编码类字段(炉号 / 批号 / 规格 / 单位)→ monospace
.card__value {
font-family: 'SF Mono', 'Menlo', 'Consolas', monospace;
font-size: 28rpx;
font-weight: 500;
color: $text;
letter-spacing: 0.3rpx;
}
// 材料描述 → 回退系统字体
.card__row--stack .card__value {
font-family: inherit;
font-size: 26rpx;
font-weight: 400;
letter-spacing: normal;
color: $text-secondary;
}
// ---- 互检导航按钮 ----
.nav-row {
display: flex;
align-items: center;
justify-content: center;
padding: 24rpx;
background: linear-gradient(135deg, $primary, #5a9aff);
border-radius: 14rpx;
box-shadow: 0 6rpx 24rpx rgba($primary, 0.3);
justify-content: space-between;
margin: 0;
padding: 24rpx 28rpx;
background: $card-bg;
border-radius: 0;
border-top: 1rpx solid $border;
box-shadow: none;
transition: background 0.2s ease;
margin-bottom: 28rpx;
&:active {
opacity: 0.9;
transform: scale(0.98);
background: #f5f7fa;
}
&__text {
font-size: 30rpx;
&__left {
display: flex;
align-items: center;
gap: 14rpx;
}
&__dot {
width: 10rpx;
height: 10rpx;
border-radius: 50%;
background: $primary;
flex-shrink: 0;
}
&__label {
font-size: 28rpx;
font-weight: 600;
color: #fff;
letter-spacing: 2rpx;
color: $text;
letter-spacing: 1rpx;
}
&__arrow {
font-size: 36rpx;
color: $text-secondary;
font-weight: 300;
line-height: 1;
margin-right: -4rpx;
}
}
File diff suppressed because it is too large Load Diff
+17 -42
View File
@@ -102,18 +102,19 @@ const showPrint = ref(false)
</script>
<style lang="scss" scoped>
$primary: #2979ff;
$primary-light: #e8f0fe;
$primary-dark: #1a56cc;
// 继承全局变量(定义在 uni.scss)
$primary: $app-primary;
$primary-light: $app-primary-light;
$primary-dark: $app-primary-dark;
$text-secondary: $app-text-secondary;
$radius: $app-radius;
// 页面专属
$bg: #f5f7fa;
$text: #1a2332;
$text-secondary: #6b7c93;
$border: #e2e8f0;
$card-bg: #ffffff;
$radius: 16rpx;
.detail-page {
min-height: 100vh;
background: $bg;
padding-bottom: 160rpx;
}
@@ -155,64 +156,41 @@ const showPrint = ref(false)
}
// ===== 区块标题 =====
// ===== 区块标题(页面覆盖)=====
.section {
margin: 0 24rpx 20rpx;
// 无动画
animation: none;
&__header {
display: flex;
align-items: center;
gap: 12rpx;
margin-bottom: 16rpx;
padding: 0 4rpx;
}
&__bar {
width: 6rpx;
height: 28rpx;
background: $primary;
border-radius: 3rpx;
flex-shrink: 0;
}
&__title {
font-size: 28rpx;
font-weight: 600;
color: $text;
animation: none;
}
}
// ===== 卡片 =====
// ===== 卡片(页面覆盖,继承 page.scss 公共 .card 样式)=====
.card {
background: $card-bg;
border-radius: $radius;
overflow: hidden;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.06);
&__row {
display: flex;
align-items: center;
padding: 24rpx 28rpx;
border-bottom: 1rpx solid $border;
&:last-child {
border-bottom: none;
}
}
&__label {
width: 120rpx;
font-size: 26rpx;
color: $text-secondary;
flex-shrink: 0;
padding-left: 0;
&::before {
display: none;
}
}
&__value {
flex: 1;
font-size: 28rpx;
color: $text;
font-weight: 500;
&--mono {
font-family: monospace;
color: $primary-dark;
@@ -220,9 +198,6 @@ const showPrint = ref(false)
}
&__value-wrap {
flex: 1;
display: flex;
align-items: center;
gap: 12rpx;
}
}
+10
View File
@@ -13,6 +13,16 @@
*/
@import 'uview-pro/theme.scss';
// ========== 项目共用变量(scanpages 多页面复用)==========
$app-primary: #2979ff;
$app-primary-light: #e8f0fe;
$app-primary-mid: #d0ddff;
$app-primary-dark: #1a56cc;
$app-text: #0f1724;
$app-text-secondary: #6b7c93;
$app-border: #e8ecf1;
$app-radius: 16rpx;
/* 颜色变量 */
/* 行为相关颜色 */
+9
View File
@@ -67,5 +67,14 @@ export const defaultApps = [{
isScan: true,
prefix: 'nbd-icon',
selected: false
},
{
name: '防腐处理',
icon: 'chuxiufangfu',
menuId: "",
path: "/scanpages/hj/do_anti_rust",
isScan: true,
prefix: 'nbd-icon',
selected: false
}
]
+38
View File
@@ -0,0 +1,38 @@
/**
* 时间格式化工具
* 挂载到 uni.formatTime,支持以下格式:
* uni.formatTime(date, 'YYYY-MM-DD HH:mm:ss')
* uni.formatTime(timestamp, 'YYYY/MM/DD')
*/
const pad = (n) => String(n).padStart(2, '0')
const formatTime = (date, fmt = 'YYYY-MM-DD HH:mm:ss') => {
if (!date) return ''
const d = date instanceof Date ? date : new Date(date)
if (isNaN(d.getTime())) return ''
const tokens = {
'Y+': d.getFullYear(),
'M+': d.getMonth() + 1,
'D+': d.getDate(),
'H+': d.getHours(),
'm+': d.getMinutes(),
's+': d.getSeconds(),
}
for (const [key, val] of Object.entries(tokens)) {
const match = fmt.match(new RegExp(key))
if (match) {
const len = match[0].length
const str = key === 'Y+' ? String(val).slice(-len) : len === 1 ? String(val) : pad(val)
fmt = fmt.replace(match[0], str)
}
}
return fmt
}
uni.formatTime = formatTime
export default formatTime
+97 -144
View File
@@ -1,153 +1,115 @@
import { useUserStore } from '@/store'
// TODO: 替换为实际的 API 地址
export const baseUrl = 'https://lygcgs.com.cn:8078/shjapi/api/'
export const baseFileUrl = 'https://lygcgs.com.cn:8078/shj/'
// ===== 防止重复请求 =====
const pendingRequests = new Map()
/**
* 生成请求唯一标识
* 将参数对象序列化为 URL query string
* 手动构建确保跨平台(小程序 / H5 / App)行为一致
* - 过滤 undefined 和 null
* - encodeURIComponent 编码中文和特殊字符
*/
function getRequestKey(config) {
const { url = '', method = 'GET', data = {} } = config
const paramStr = JSON.stringify(data)
return `${method}:${url}:${paramStr}`
function toQueryString(params) {
if (!params) return ''
const parts = Object.keys(params)
.filter((k) => params[k] !== undefined && params[k] !== null)
.map((k) => `${encodeURIComponent(k)}=${encodeURIComponent(params[k])}`)
return parts.length ? '?' + parts.join('&') : ''
}
/**
* 检查是否为重复请求
* 核心请求方法(兼容 小程序 / H5 / App)
*
* @param {string} url - API 路径(不含 baseUrl)
* @param {object} options - 可选配置
* {string} method - GET | POST,默认 GET
* {object} data - GET 时自动拼入 query string,POST 时作为请求体
* {object} header - 自定义请求头
* {boolean} loading - 是否显示 loading,默认 true
* {boolean} toast - 是否在失败时弹 toast,默认 true
*/
function checkDuplicate(config) {
const key = getRequestKey(config)
if (pendingRequests.has(key)) {
const meta = config.meta || {}
if (meta.toast !== false) {
uni.showToast({ title: '请求过于频繁,请稍后再试', icon: 'none' })
}
return new Error('duplicate request')
export function request(url, options = {}) {
const {
method = 'GET',
data,
header = {},
loading = true,
toast = true
} = options
const userStore = useUserStore()
const headers = {
'content-type': 'application/json',
...header
}
if (userStore.token) {
headers.token = userStore.token
}
pendingRequests.set(key, true)
return null
}
/**
* 移除请求标识
*/
function removePending(config) {
const key = getRequestKey(config)
pendingRequests.delete(key)
}
// ========================
// GET: 手动拼 query string(不依赖 uni.request 各平台差异)
let fullUrl = `${baseUrl}${url}`
if (method === 'GET' && data) {
fullUrl += toQueryString(data)
}
// uview-pro 全局请求配置
export const httpRequestConfig = {
baseUrl,
header: {
'content-type': 'application/json'
},
meta: {
originalData: true,
toast: true,
loading: true
}
}
if (loading) {
uni.showLoading({ title: '加载中...', mask: true })
}
// 全局请求/响应拦截器
export const httpInterceptor = {
request: (config) => {
// 检查重复请求
const dupError = checkDuplicate(config)
if (dupError) throw dupError
const meta = config.meta || {}
if (meta.loading) {
uni.showLoading({ title: '加载中...', mask: true })
}
const userStore = useUserStore()
if (userStore.token) {
config.header.token = userStore.token
}
return config
},
response: (response) => {
const meta = response.config?.meta || {}
if (meta.loading) {
uni.hideLoading()
}
// 请求完成,移除标识
removePending(response.config)
// 根据业务处理错误、例如登录失效等处理接口返回错误码
if (response.data.code !== 1) {
if (meta.toast) {
uni.showToast({
title: response.data.message || '请求失败',
icon: 'none'
})
}
// 401 登录失效,跳转登录页
if (response.data.code === 401) {
const userStore = useUserStore()
userStore.logout()
uni.reLaunch({ url: '/pages/login/index' })
}
throw new Error(response.data.message || '请求失败')
}
uni.showToast({
title: response.data.message || '请求成功',
icon: 'none'
return new Promise((resolve, reject) => {
uni.request({
url: fullUrl,
method,
data: method === 'POST' ? data : undefined,
header: headers,
success: (res) => {
if (loading) uni.hideLoading()
const body = res.data
if (body.code !== 1) {
if (toast) {
uni.showToast({
title: body.message || '请求失败',
icon: 'none'
})
}
if (body.code === 401) {
userStore.logout()
uni.reLaunch({ url: '/pages/login/index' })
}
reject(new Error(body.message || '请求失败'))
return
}
resolve(body)
},
fail: (err) => {
if (loading) uni.hideLoading()
uni.showToast({ title: '网络连接失败', icon: 'none' })
reject(err)
}
})
return response.data
}
})
}
/**
* 便捷请求方法(基于 uni.request 封装)
* 也可用 uni.$http.get / uni.$http.post(uview-pro)
* GET 快捷方法
* 兼容两种调用方式:
* get('/path?key=val') 旧风格(URL 自带参数)
* get('/path', { key: 'val' }) 新风格(参数对象,自动编码)
*/
export function request(options) {
return new Promise((resolve, reject) => {
// 运行请求拦截器
const config = { ...httpRequestConfig, ...options }
const finalConfig = httpInterceptor.request({
...config,
header: { ...httpRequestConfig.header, ...options.header }
})
uni.request({
...finalConfig,
url: `${finalConfig.baseUrl}${finalConfig.url}`,
success: (res) => {
try {
const data = httpInterceptor.response({ data: res.data, config: finalConfig })
resolve(data)
} catch (err) {
reject(err)
}
},
fail: (err) => {
removePending(finalConfig) // 网络失败也要清理标识
uni.hideLoading()
uni.showToast({ title: '网络连接失败', icon: 'none' })
reject(err)
}
})
})
export const get = (url, params) => {
if (params) {
url += toQueryString(params)
}
return request(url, { method: 'GET' })
}
// GET 快捷方法
export const get = (url, params = {}) => {
return request({ url, method: 'GET', data: params })
/**
* POST 快捷方法
*/
export const post = (url, data) => {
return request(url, { method: 'POST', data })
}
// POST 快捷方法
export const post = (url, data = {}) => {
return request({ url, method: 'POST', data })
}
/**
* 解析附件 URL 字符串为可展示的照片列表
* @param {string} urlStr - 逗号分隔的附件路径,如 "FileUpLoad/xxx.png,FileUpLoad/yyy.png"
@@ -157,7 +119,7 @@ export const post = (url, data = {}) => {
export const parseAttachUrls = (urlStr, startId = 0) => {
if (!urlStr) return []
let id = startId
return urlStr.split(',').filter(Boolean).map(url => ({
return urlStr.split(',').filter(Boolean).map((url) => ({
_id: ++id,
src: `${baseFileUrl}${url}`,
_attachUrl: url,
@@ -166,20 +128,11 @@ export const parseAttachUrls = (urlStr, startId = 0) => {
}
export const getUrlParam = (url, name) => {
if (!url || !name) return null;
// 构造正则:匹配 ?name= 或 &name= 后面的值
// [?&] : 匹配起始分隔符
// ${name}: 匹配具体的参数名
// = : 匹配等号
// ([^&]*) : 捕获组,匹配直到下一个 & 或字符串结束的内容
const regex = new RegExp(`[?&]${name}=([^&]*)`, 'i'); // 'i' 表示忽略大小写
const match = url.match(regex);
if (match && match[1]) {
return decodeURIComponent(match[1]);
}
return null;
};
if (!url || !name) return null
const regex = new RegExp(`[?&]${name}=([^&]*)`, 'i')
const match = url.match(regex)
if (match && match[1]) {
return decodeURIComponent(match[1])
}
return null
}