This commit is contained in:
2026-07-06 16:46:07 +08:00
parent ae914b5c24
commit 21d11697a0
8 changed files with 899 additions and 1378 deletions
+4 -2
View File
@@ -181,8 +181,10 @@ export const reqSaveAntiCorrosionCheck = (data={})=>post(`/AntiCorrosionCheck/Sa
/**
* 获取防腐填报记录
* @param {String} materialCode
* @param {Object} params - { materialCode, projectId }
*/
export const reqGetAntiCorrosionCheck = (params) => get(`/AntiCorrosionCheck/GetRecordsByMaterialCode?materialCode=${params.materialCode}&projectId=${params.projectId}`)
export const reqGetAntiCorrosionCheck = (params = {}) => {
return get('/AntiCorrosionCheck/GetRecordsByMaterialCode', params)
}
+97
View File
@@ -1667,3 +1667,100 @@
}
}
}
// ===== 详情页区块与卡片通用样式(do_anti_rust / 焊口详情页等)=====
.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: #2979ff;
border-radius: 3rpx;
flex-shrink: 0;
}
&__title {
font-size: 28rpx;
font-weight: 600;
color: #0f1724;
letter-spacing: 0.5rpx;
}
}
.card {
background: #fff;
border-radius: 16rpx;
overflow: hidden;
box-shadow: 0 2rpx 16rpx rgba(0, 0, 0, 0.06);
&__row {
display: flex;
align-items: center;
padding: 22rpx 28rpx;
border-bottom: 1rpx solid #e8ecf1;
&:last-child {
border-bottom: none;
}
&--stack {
flex-direction: column;
align-items: flex-start;
.card__label {
margin-bottom: 8rpx;
}
.card__value-wrap {
width: 100%;
justify-content: flex-start;
}
}
}
&__label {
width: 180rpx;
font-size: 26rpx;
color: #6b7c93;
flex-shrink: 0;
}
&__value {
flex: 1;
font-size: 28rpx;
color: #0f1724;
text-align: left;
}
&__value-wrap {
flex: 1;
display: flex;
align-items: center;
gap: 8rpx;
}
&__unit {
font-size: 24rpx;
color: #6b7c93;
flex-shrink: 0;
}
&__input {
flex: 1;
font-size: 28rpx;
color: #0f1724;
text-align: left;
height: 48rpx;
&::placeholder {
color: #b0bec5;
}
}
}
-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>
+62 -19
View File
@@ -12,7 +12,7 @@
<!-- 有记录 -->
<template v-else>
<view class="anti-record__section">
<view class="anti-record__section" :style="{ ...props.customStyle }">
<view class="anti-record__header">
<view class="anti-record__bar"></view>
<text class="anti-record__title">防腐记录</text>
@@ -39,6 +39,18 @@
<text class="anti-record__label">漆膜厚度</text>
<text class="anti-record__value">{{ latest.FilmThickness || '--' }}</text>
</view>
<view class="anti-record__row">
<text class="anti-record__label">喷砂情况</text>
<view class="anti-record__value">
{{ latest.SandBlasting || '--' }}
</view>
</view>
<view class="anti-record__row">
<text class="anti-record__label">除锈喷砂情况</text>
<view class="anti-record__value">
{{ latest.RustSandBlasting || '--' }}
</view>
</view>
<view class="anti-record__row">
<text class="anti-record__label">检查人</text>
<text class="anti-record__value">{{ latest.CheckPersonName || '--' }}</text>
@@ -56,15 +68,21 @@
<text class="anti-record__tag-text">{{ latest.CheckResult || '--' }}</text>
</view>
</view>
<view class="anti-record__row anti-record__row--stack">
<text class="anti-record__label--stack">不合格原因</text>
<text class="anti-record__value anti-record__value--stack">{{ latest.UnqualifiedReason }}</text>
<view class="anti-record__row" v-if="latest.CheckResult !== '合格'">
<view class="anti-record__label">不合格原因</view>
<text class="anti-record__value">{{ latest.UnqualifiedReason }}</text>
</view>
<view class="anti-record__row anti-record__row--stack">
<text class="anti-record__label--stack">整改要求</text>
<text class="anti-record__value anti-record__value--stack">{{ latest.RectifyRequirement
<view class="anti-record__row" v-if="latest.CheckResult !== '合格'">
<view class="anti-record__label">整改要求</view>
<text class="anti-record__value">{{ latest.RectifyRequirement
}}</text>
</view>
<view class="anti-record__row--stack">
<text class="anti-record__label--stack">照片</text>
<view class="anti-record__value--stack">
<nbd-photo-view :attachUrl="latest.AttachUrl1" :cols="4" />
</view>
</view>
</view>
</view>
@@ -95,6 +113,18 @@
<text class="anti-record__label">漆膜厚度</text>
<text class="anti-record__value">{{ item.FilmThickness || '--' }}</text>
</view>
<view class="anti-record__row">
<text class="anti-record__label">喷砂情况</text>
<view class="anti-record__value">
{{ item.SandBlasting || '--' }}
</view>
</view>
<view class="anti-record__row">
<text class="anti-record__label">除锈喷砂情况</text>
<view class="anti-record__value">
{{ item.RustSandBlasting || '--' }}
</view>
</view>
<view class="anti-record__row">
<text class="anti-record__label">检查人</text>
<text class="anti-record__value">{{ item.CheckPersonName || '--' }}</text>
@@ -109,19 +139,25 @@
:class="item.CheckResult === '合格' ? 'anti-record__tag--pass' : 'anti-record__tag--fail'">
<text class="anti-record__tag-icon">{{ item.CheckResult === '合格' ? '\u2713' :
'\u2717'
}}</text>
}}</text>
<text class="anti-record__tag-text">{{ item.CheckResult || '--' }}</text>
</view>
</view>
<view class="anti-record__row anti-record__row--stack" v-if="item.CheckResult === '不合格'">
<text class="anti-record__label--stack">不合格原因</text>
<text class="anti-record__value anti-record__value--stack">{{ item.UnqualifiedReason
}}</text>
<view class="anti-record__row" v-if="item.CheckResult === '不合格'">
<text class="anti-record__label">不合格原因</text>
<view class="anti-record__value">{{ item.UnqualifiedReason
}}</view>
</view>
<view class="anti-record__row anti-record__row--stack" v-if="item.CheckResult === '不合格'">
<text class="anti-record__label--stack">整改要求</text>
<text class="anti-record__value anti-record__value--stack">{{ item.RectifyRequirement
}}</text>
<view class="anti-record__row" v-if="item.CheckResult === '不合格'">
<text class="anti-record__label">整改要求</text>
<view class="anti-record__value-">{{ item.RectifyRequirement
}}</view>
</view>
<view class="anti-record__row--stack">
<text class="anti-record__label--stack">照片</text>
<view class="anti-record__value--stack">
<nbd-photo-view :attachUrl="item.AttachUrl1" :cols="4" />
</view>
</view>
</view>
</scroll-view>
@@ -136,7 +172,8 @@ import { ref, computed, watch } from 'vue'
import { reqGetAntiCorrosionCheck } from '@/api/hj.js'
const props = defineProps({
params: { type: Object, default: () => ({}) }
params: { type: Object, default: () => ({}) },
customStyle: { type: Object, default: () => ({}) },
})
const list = ref([])
@@ -146,9 +183,11 @@ const showPopup = ref(false)
const latest = computed(() => list.value[0] || null)
const fetchRecords = async () => {
const { materialCode, projectId } = props.params
if (!materialCode || !projectId) return
loading.value = true
try {
const res = await reqGetAntiCorrosionCheck(props.params)
if (res.code === 1 && res.data) {
@@ -261,6 +300,7 @@ $text-secondary: #8e99a4;
}
&--stack {
flex-direction: column;
align-items: flex-start;
.anti-record__value {
@@ -275,7 +315,7 @@ $text-secondary: #8e99a4;
color: $text-secondary;
flex-shrink: 0;
margin-right: 24rpx;
width: 140rpx;
width: 160rpx;
&--stack {
font-size: 26rpx;
@@ -295,6 +335,9 @@ $text-secondary: #8e99a4;
&--stack {
font-size: 24rpx;
padding-left: 20rpx;
padding-bottom: 8rpx;
padding-top: 8rpx;
}
}
@@ -355,7 +398,7 @@ $text-secondary: #8e99a4;
&__popup {
width: 100%;
height: 70vh;
height: 80vh;
background: #fff;
border-radius: 24rpx 24rpx 0 0;
display: flex;
+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>
+49 -22
View File
@@ -55,7 +55,7 @@
<view class="card">
<!-- 防腐等级/涂漆代码下拉选择 -->
<view class="card__row" @click="showPaintCodePicker = true">
<text class="card__label">防腐等级</text>
<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 || '请选择防腐等级' }}
@@ -79,7 +79,7 @@
</view>
<!-- 漆膜厚度 -->
<view class="card__row">
<text class="card__label">漆膜厚度</text>
<text class="card__label required">漆膜厚度</text>
<view class="card__value-wrap">
<input class="card__input" v-model="antiForm.FilmThickness" type="digit"
placeholder="请输入漆膜厚度" />
@@ -107,7 +107,7 @@
</view>
</view>
<!-- 自检 -->
<!-- 自检 -->
<view class="section">
<view class="section__header">
<view class="section__bar"></view>
@@ -118,14 +118,13 @@
<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="不合格" />
<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="fail-section"
:class="{ 'is-visible': failVisible, 'is-enter': failAnimating, 'is-leave': failLeaving }">
<!-- 不合格原因 -->
<view class="card__row card__row--stack">
<text class="card__label">不合格原因</text>
@@ -164,10 +163,10 @@
<!-- 防腐记录 -->
<nbd-anti-record :params="{ materialCode: form.MaterialCode, projectId: currentProject.ProjectId }" />
<nbd-submit-bar text="提交" :loading="submitting" @click="handleSubmit" />
<nbd-submit-bar text="提交" :loading="submitting" @click="handleSubmit" />
</view>
<nbd-select v-model="showPaintCodePicker" title="选择防腐等级" :list="paintCodeList" label-key="PaintCode" value-key="Id"
@@ -180,7 +179,7 @@ import { onLoad } from '@dcloudio/uni-app'
import { storeToRefs } from 'pinia'
import { watch, nextTick } from 'vue'
import { useUserStore } from '@/store'
import { reqMaterialInfoByMaterialCode, reqPaintCodeList, reqSaveAntiCorrosionCheck } from '@/api/hj.js'
import { reqMaterialInfoByMaterialCode, reqPaintCodeList, reqSaveAntiCorrosionCheck , reqGetAntiCorrosionCheck } from '@/api/hj.js'
const userStore = useUserStore()
const { userInfo, currentProject } = storeToRefs(userStore)
@@ -258,10 +257,25 @@ onLoad((opt) => {
const loadData = async (id) => {
try {
uni.showLoading({ title: '加载中...', mask: true })
const res = await reqMaterialInfoByMaterialCode(encodeURIComponent(id))
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) {
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' })
@@ -271,15 +285,15 @@ const loadData = async (id) => {
}
const handleSubmit = async () => {
// if (!antiForm.value.PaintId) {
// uni.showToast({ title: '请选择防腐等级', icon: 'none' })
// return
// }
if( !antiForm.value.UnqualifiedReason && antiForm.value.CheckResult === '不合格') {
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 === '不合格') {
if (!antiForm.value.RectifyRequirement && antiForm.value.CheckResult === '不合格') {
uni.showToast({ title: '请输入整改要求', icon: 'none' })
return
}
@@ -311,7 +325,7 @@ const handleSubmit = async () => {
CreateUser: userInfo.PersonId, // 创建人员
AttachUrl1: files.value, // 照片
}
console.log("提交数据==============>",data)
console.log("提交数据==============>", data)
try {
const res = await reqSaveAntiCorrosionCheck(data)
if (res.code === 1) {
@@ -448,6 +462,7 @@ $shadow-md: 0 2rpx 12rpx rgba(0, 0, 0, 0.04), 0 8rpx 24rpx rgba(0, 0, 0, 0.06);
opacity: 0;
transform: translateY(24rpx);
}
to {
opacity: 1;
transform: translateY(0);
@@ -455,9 +470,12 @@ $shadow-md: 0 2rpx 12rpx rgba(0, 0, 0, 0.04), 0 8rpx 24rpx rgba(0, 0, 0, 0.06);
}
@keyframes breathe {
0%, 100% {
0%,
100% {
opacity: 0.6;
}
50% {
opacity: 1;
}
@@ -468,8 +486,13 @@ $shadow-md: 0 2rpx 12rpx rgba(0, 0, 0, 0.04), 0 8rpx 24rpx rgba(0, 0, 0, 0.06);
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; }
&:nth-child(2) {
animation-delay: 0.05s;
}
&:nth-child(3) {
animation-delay: 0.15s;
}
&__header {
display: flex;
@@ -599,6 +622,10 @@ $shadow-md: 0 2rpx 12rpx rgba(0, 0, 0, 0.04), 0 8rpx 24rpx rgba(0, 0, 0, 0.06);
background: $primary-mid;
border-radius: 2rpx;
}
&.required::before {
background: #ff0000;
}
}
&__value {
File diff suppressed because it is too large Load Diff
+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 stringPOST 时作为请求体
* {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.postuview-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
}