|
|
<template>
|
|
|
<view class="ship-detail-bg" :class="{ 'wechat-browser': isWeixinBrowser }">
|
|
|
<view class="fixed-nav" v-if="!isWeixinBrowser">
|
|
|
<NavBar :title="isEdit ? '编辑船只' : '添加船只'" />
|
|
|
</view>
|
|
|
<view class="content-area">
|
|
|
<!-- 单价和计算规则 -->
|
|
|
|
|
|
<!-- 步骤条 -->
|
|
|
<view class="step-bar">
|
|
|
<view class="step-group" v-for="(step, idx) in steps" :key="idx">
|
|
|
<view
|
|
|
class="step-circle"
|
|
|
:class="{ active: idx + 1 === currentStep }"
|
|
|
>{{ idx + 1 }}</view
|
|
|
>
|
|
|
<view
|
|
|
class="step-label"
|
|
|
:class="{ active: idx + 1 === currentStep }"
|
|
|
>{{ step }}</view
|
|
|
>
|
|
|
<view v-if="idx < steps.length - 1" class="step-line"></view>
|
|
|
</view>
|
|
|
</view>
|
|
|
<!-- 第一步:基本信息 -->
|
|
|
<view v-if="currentStep === 1" class="info-card">
|
|
|
<view class="info-title">基本信息</view>
|
|
|
<view class="info-form">
|
|
|
<view class="form-row">
|
|
|
<text class="form-label required">船舶所有人</text>
|
|
|
<input
|
|
|
class="form-input"
|
|
|
v-model="form.owner"
|
|
|
placeholder="姓名与身份证一致"
|
|
|
/>
|
|
|
</view>
|
|
|
<view class="form-row">
|
|
|
<text class="form-label required">身份证号</text>
|
|
|
<input
|
|
|
class="form-input"
|
|
|
v-model="form.idCard"
|
|
|
placeholder="输入正确的18位身份证号码"
|
|
|
/>
|
|
|
</view>
|
|
|
<view class="form-row">
|
|
|
<text class="form-label required">联系电话</text>
|
|
|
<input
|
|
|
class="form-input"
|
|
|
v-model="form.phone"
|
|
|
placeholder="11位常用的手机号"
|
|
|
/>
|
|
|
</view>
|
|
|
<view class="form-row">
|
|
|
<text class="form-label required">船名</text>
|
|
|
<input
|
|
|
class="form-input"
|
|
|
v-model="form.shipNo"
|
|
|
placeholder="请输入船名"
|
|
|
/>
|
|
|
</view>
|
|
|
<view class="form-row">
|
|
|
<text class="form-label required">船舶类型</text>
|
|
|
<view class="fee-type-group">
|
|
|
<view
|
|
|
v-for="(value, label) in feeTypeEnum"
|
|
|
:key="value.value"
|
|
|
class="fee-type-item"
|
|
|
>
|
|
|
<radio
|
|
|
class="form-radio"
|
|
|
:value="String(value.value)"
|
|
|
:checked="form.feeType === value.value"
|
|
|
@tap="onFeeTypeChange({ detail: { value: value.value } })"
|
|
|
/>
|
|
|
<view class="fee-type-info">
|
|
|
<text class="fee-type-label">{{ label }}</text>
|
|
|
<text class="fee-type-desc">{{ value.desc }}</text>
|
|
|
</view>
|
|
|
</view>
|
|
|
</view>
|
|
|
</view>
|
|
|
</view>
|
|
|
</view>
|
|
|
<!-- 第二步:船舶参数 -->
|
|
|
<view v-if="currentStep === 2" class="info-card">
|
|
|
<view class="info-title"
|
|
|
>船舶参数
|
|
|
<text class="info-desc">(请按船舶检验证书填写)</text></view
|
|
|
>
|
|
|
<!-- 载重吨位 -->
|
|
|
<view class="form-row">
|
|
|
<text class="form-label required">载重吨位</text>
|
|
|
<input
|
|
|
class="form-input"
|
|
|
type="digit"
|
|
|
v-model="form.ton"
|
|
|
placeholder="请输入载重吨位"
|
|
|
/>
|
|
|
<text class="form-unit">吨</text>
|
|
|
</view>
|
|
|
<!-- <view v-if="isEdit" class="form-row" style="align-items: flex-start;">
|
|
|
<text class="form-label">单次过闸收费</text>
|
|
|
<text class="form-unit" style="flex:1;text-align:right;">
|
|
|
<view style="text-align: right;">{{ unitPrice }}元</view>{{ calculationDescription }}
|
|
|
</text>
|
|
|
</view> -->
|
|
|
<!-- 总长度 -->
|
|
|
<view class="form-row">
|
|
|
<text class="form-label required">总长度</text>
|
|
|
<input
|
|
|
class="form-input"
|
|
|
type="digit"
|
|
|
v-model="form.length"
|
|
|
placeholder="请输入总长度"
|
|
|
/>
|
|
|
<text class="form-unit">米</text>
|
|
|
</view>
|
|
|
<!-- 总宽 -->
|
|
|
<view class="form-row">
|
|
|
<text class="form-label required">总宽</text>
|
|
|
<input
|
|
|
class="form-input"
|
|
|
type="digit"
|
|
|
v-model="form.width"
|
|
|
placeholder="请输入总宽"
|
|
|
/>
|
|
|
<text class="form-unit">米</text>
|
|
|
</view>
|
|
|
<!-- 型深 -->
|
|
|
<view class="form-row">
|
|
|
<text class="form-label required">型深</text>
|
|
|
<input
|
|
|
class="form-input"
|
|
|
type="digit"
|
|
|
v-model="form.depth"
|
|
|
placeholder="请输入型深"
|
|
|
/>
|
|
|
<text class="form-unit">米</text>
|
|
|
</view>
|
|
|
<!-- 参考载重吨位 -->
|
|
|
<view class="form-row">
|
|
|
<text class="form-label required">参考载重吨位</text>
|
|
|
<radio-group
|
|
|
class="form-radio-group"
|
|
|
v-model="form.tonLevel"
|
|
|
@change="onTonLevelChange"
|
|
|
>
|
|
|
<radio
|
|
|
v-for="(value, label) in tonnageClassEnum"
|
|
|
:key="value"
|
|
|
class="form-radio"
|
|
|
:value="String(value)"
|
|
|
:checked="form.tonLevel === value"
|
|
|
>{{ label }}</radio
|
|
|
>
|
|
|
</radio-group>
|
|
|
</view>
|
|
|
<!-- 船型 -->
|
|
|
<view class="form-row">
|
|
|
<text class="form-label required">船型</text>
|
|
|
<radio-group
|
|
|
class="form-radio-group"
|
|
|
v-model="form.shipType"
|
|
|
@change="onShipTypeChange"
|
|
|
>
|
|
|
<radio
|
|
|
v-for="(value, label) in shipTypeEnum"
|
|
|
:key="value"
|
|
|
class="form-radio"
|
|
|
:value="value.toString()"
|
|
|
:checked="form.shipType === value.toString()"
|
|
|
>{{ label }}</radio
|
|
|
>
|
|
|
</radio-group>
|
|
|
</view>
|
|
|
</view>
|
|
|
<!-- 第三步:船检簿上传 -->
|
|
|
<view v-if="currentStep === 3" class="info-card">
|
|
|
<view class="info-title-row">
|
|
|
<view class="info-title">船检簿上传</view>
|
|
|
<!--<button class="example-btn" @click="viewExample">查看示例</button>-->
|
|
|
</view>
|
|
|
<!-- 第一页 -->
|
|
|
<view class="upload-section">
|
|
|
<text class="form-label required">第一页</text>
|
|
|
<view class="upload-row">
|
|
|
<view class="upload-img-box">
|
|
|
<image v-if="page1Img" :src="page1Img" class="upload-img" />
|
|
|
<view v-else class="upload-add" @click="chooseImage('page1')">
|
|
|
<text>+</text>
|
|
|
<text>添加图片</text>
|
|
|
</view>
|
|
|
<view
|
|
|
v-if="page1Img"
|
|
|
class="upload-del"
|
|
|
@click="deleteImage('page1')"
|
|
|
>×</view
|
|
|
>
|
|
|
</view>
|
|
|
<view class="upload-desc">
|
|
|
<view v-if="getExampleByIndex(0)" class="example-info">
|
|
|
<image
|
|
|
v-if="getExampleByIndex(0).image_url"
|
|
|
:src="getExampleByIndex(0).image_url"
|
|
|
class="example-img"
|
|
|
mode="aspectFit"
|
|
|
@click="previewExample(getExampleByIndex(0).image_url)"
|
|
|
></image>
|
|
|
<text class="example-text">{{
|
|
|
getExampleByIndex(0).description || "第一页相关说明"
|
|
|
}}</text>
|
|
|
</view>
|
|
|
<text v-else class="example-text">第一页相关说明</text>
|
|
|
</view>
|
|
|
</view>
|
|
|
</view>
|
|
|
<view class="divider"></view>
|
|
|
<!-- 第二页 -->
|
|
|
<view class="upload-section">
|
|
|
<text class="form-label required">第二页</text>
|
|
|
<view class="upload-row">
|
|
|
<view class="upload-img-box">
|
|
|
<image v-if="page2Img" :src="page2Img" class="upload-img" />
|
|
|
<view v-else class="upload-add" @click="chooseImage('page2')">
|
|
|
<text>+</text>
|
|
|
<text>添加图片</text>
|
|
|
</view>
|
|
|
<view
|
|
|
v-if="page2Img"
|
|
|
class="upload-del"
|
|
|
@click="deleteImage('page2')"
|
|
|
>×</view
|
|
|
>
|
|
|
</view>
|
|
|
<view class="upload-desc">
|
|
|
<view v-if="getExampleByIndex(1)" class="example-info">
|
|
|
<image
|
|
|
v-if="getExampleByIndex(1).image_url"
|
|
|
:src="getExampleByIndex(1).image_url"
|
|
|
class="example-img"
|
|
|
mode="aspectFit"
|
|
|
@click="previewExample(getExampleByIndex(1).image_url)"
|
|
|
></image>
|
|
|
<text class="example-text">{{
|
|
|
getExampleByIndex(1).description || "第二页相关说明"
|
|
|
}}</text>
|
|
|
</view>
|
|
|
<text v-else class="example-text">第二页相关说明</text>
|
|
|
</view>
|
|
|
</view>
|
|
|
</view>
|
|
|
<view class="divider"></view>
|
|
|
<!-- 第三页 -->
|
|
|
<view class="upload-section">
|
|
|
<text class="form-label required">第三页</text>
|
|
|
<view class="upload-row">
|
|
|
<view class="upload-img-box">
|
|
|
<image v-if="page3Img" :src="page3Img" class="upload-img" />
|
|
|
<view v-else class="upload-add" @click="chooseImage('page3')">
|
|
|
<text>+</text>
|
|
|
<text>添加图片</text>
|
|
|
</view>
|
|
|
<view
|
|
|
v-if="page3Img"
|
|
|
class="upload-del"
|
|
|
@click="deleteImage('page3')"
|
|
|
>×</view
|
|
|
>
|
|
|
</view>
|
|
|
<view class="upload-desc">
|
|
|
<view v-if="getExampleByIndex(2)" class="example-info">
|
|
|
<image
|
|
|
v-if="getExampleByIndex(2).image_url"
|
|
|
:src="getExampleByIndex(2).image_url"
|
|
|
class="example-img"
|
|
|
mode="aspectFit"
|
|
|
@click="previewExample(getExampleByIndex(2).image_url)"
|
|
|
></image>
|
|
|
<text class="example-text">{{
|
|
|
getExampleByIndex(2).description || "第三页相关说明"
|
|
|
}}</text>
|
|
|
</view>
|
|
|
<text v-else class="example-text">第三页相关说明</text>
|
|
|
</view>
|
|
|
</view>
|
|
|
</view>
|
|
|
</view>
|
|
|
<!-- 第四步:签名确认 -->
|
|
|
<view v-if="currentStep === 4" class="info-card">
|
|
|
<view class="info-title">签名确认</view>
|
|
|
<view class="sign-declare-row">
|
|
|
<view style="position: relative; display: flex; align-items: center">
|
|
|
<checkbox :checked="signChecked" />
|
|
|
<view
|
|
|
style="
|
|
|
position: absolute;
|
|
|
left: 0;
|
|
|
top: 0;
|
|
|
right: 0;
|
|
|
bottom: 0;
|
|
|
background: rgba(0, 0, 0, 0);
|
|
|
"
|
|
|
@tap="toggleSignChecked"
|
|
|
></view>
|
|
|
</view>
|
|
|
<text class="sign-declare-text">
|
|
|
本人承诺所提供材料皆真实有效;如有虚假,本人承担因此造成的全部责任。
|
|
|
</text>
|
|
|
</view>
|
|
|
<view class="divider"></view>
|
|
|
<view
|
|
|
class="form-label required"
|
|
|
style="margin-bottom: 16rpx; color: #217aff"
|
|
|
>手写签名</view
|
|
|
>
|
|
|
<view class="sign-area">
|
|
|
<canvas
|
|
|
v-if="!signImg"
|
|
|
id="signCanvas"
|
|
|
canvas-id="signCanvas"
|
|
|
class="sign-canvas"
|
|
|
@touchstart="startSign"
|
|
|
@touchmove="moveSign"
|
|
|
@touchend="endSign"
|
|
|
disable-scroll="true"
|
|
|
></canvas>
|
|
|
<text class="sign-placeholder" v-if="!signImg && !hasSigned"
|
|
|
>此处签名</text
|
|
|
>
|
|
|
<image v-if="signImg" :src="signImg" class="sign-img" />
|
|
|
</view>
|
|
|
<view class="sign-btn-bar">
|
|
|
<button class="sign-btn reset-btn" @click="resetSign">
|
|
|
重新签名
|
|
|
</button>
|
|
|
<button class="sign-btn preview-btn" @click="previewSign">
|
|
|
预览签名
|
|
|
</button>
|
|
|
</view>
|
|
|
</view>
|
|
|
</view>
|
|
|
<!-- 步骤按钮区 -->
|
|
|
<view class="step-btn-bar">
|
|
|
<button
|
|
|
v-if="currentStep > 1"
|
|
|
class="step-btn prev-btn"
|
|
|
@click="prevStep"
|
|
|
>
|
|
|
上一步
|
|
|
</button>
|
|
|
<button
|
|
|
v-if="currentStep < 4"
|
|
|
class="step-btn next-btn"
|
|
|
@click="nextStep"
|
|
|
:class="{ 'single-btn': currentStep === 1 }"
|
|
|
:style="currentStep === 1 ? 'margin: 0 auto;' : ''"
|
|
|
>
|
|
|
下一步
|
|
|
</button>
|
|
|
<button
|
|
|
v-if="currentStep === 4"
|
|
|
class="step-btn next-btn"
|
|
|
@click="submit"
|
|
|
>
|
|
|
提交
|
|
|
</button>
|
|
|
</view>
|
|
|
</view>
|
|
|
</template>
|
|
|
|
|
|
<script>
|
|
|
import NavBar from "@/components/NavBar.vue";
|
|
|
import { API } from "@/config/index.js";
|
|
|
|
|
|
export default {
|
|
|
name: "ShipManagerPage",
|
|
|
components: { NavBar },
|
|
|
data() {
|
|
|
return {
|
|
|
steps: ["基本信息", "船舶参数", "船检簿上传", "签名确认"],
|
|
|
currentStep: 1,
|
|
|
page1Img: "",
|
|
|
page2Img: "",
|
|
|
page3Img: "",
|
|
|
originalPage1Img: "",
|
|
|
originalPage2Img: "",
|
|
|
originalPage3Img: "",
|
|
|
signChecked: false,
|
|
|
signImg: "",
|
|
|
isSigning: false,
|
|
|
lastPoint: null,
|
|
|
canvasWidth: 0,
|
|
|
canvasHeight: 0,
|
|
|
hasDrawn: false,
|
|
|
hasSigned: false,
|
|
|
originalSignature: "", // 后端已有的签名(编辑模式)
|
|
|
isEdit: false,
|
|
|
shipTypeEnum: {}, // 船型枚举
|
|
|
tonnageClassEnum: {}, // 载重吨位枚举
|
|
|
feeTypeEnum: {}, // 船舶类型枚举
|
|
|
unitPrice: "", // 单价
|
|
|
calculationDescription: "", // 计算规则
|
|
|
shipInspectionExamples: [], // 船检簿示例数据
|
|
|
form: {
|
|
|
owner: "",
|
|
|
idCard: "",
|
|
|
phone: "",
|
|
|
shipNo: "",
|
|
|
feeType: 1, // 默认货船
|
|
|
ton: "",
|
|
|
length: "",
|
|
|
width: "",
|
|
|
depth: "",
|
|
|
tonLevel: "B",
|
|
|
shipType: "1",
|
|
|
page1FileId: "",
|
|
|
page2FileId: "",
|
|
|
page3FileId: "",
|
|
|
},
|
|
|
isWeixinBrowser: false,
|
|
|
};
|
|
|
},
|
|
|
onLoad(options) {
|
|
|
|
|
|
this.fetchShipInspectionExample();
|
|
|
// 获取船舶属性枚举
|
|
|
this.fetchShipPropertyEnum().then((success) => {
|
|
|
if (!success) {
|
|
|
// 如果获取失败,返回上一页
|
|
|
setTimeout(() => {
|
|
|
uni.navigateBack();
|
|
|
}, 1500);
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
// 编辑模式:优先通过 id 从后端获取详情
|
|
|
if (options && options.edit === "1") {
|
|
|
this.isEdit = true;
|
|
|
// 获取单价和计算规则
|
|
|
// this.fetchUnitPrice();
|
|
|
if (options.id) {
|
|
|
// 通过 id 请求详情
|
|
|
this.fetchShipDetailForEdit(options.id);
|
|
|
} else if (options.ship) {
|
|
|
// 兼容旧逻辑:从 URL 直接解析 ship 对象
|
|
|
try {
|
|
|
const ship = JSON.parse(decodeURIComponent(options.ship));
|
|
|
console.log("填充数据(兼容旧参数 ship):", ship.fee_type);
|
|
|
// 填充表单
|
|
|
this.form.id = ship.id || "";
|
|
|
this.form.owner = ship.owner_name || "";
|
|
|
this.form.idCard = ship.id_card || "";
|
|
|
this.form.phone = ship.phone || "";
|
|
|
this.form.shipNo = ship.ship_number || "";
|
|
|
this.form.ton = ship.total_tonnage || "";
|
|
|
this.form.length = ship.total_length || "";
|
|
|
this.form.width = ship.total_width || "";
|
|
|
this.form.depth = ship.molded_depth || "";
|
|
|
this.form.tonLevel = ship.tonnage_class || "";
|
|
|
this.form.shipType = ship.ship_type || "";
|
|
|
this.form.feeType = ship.fee_type || 1; // 默认货船
|
|
|
this.form.page1FileId = ship.picture1 || "";
|
|
|
this.form.page2FileId = ship.picture2 || "";
|
|
|
this.form.page3FileId = ship.picture3 || "";
|
|
|
// 图片预览
|
|
|
this.page1Img = this.getFileUrl(ship.picture1);
|
|
|
this.page2Img = this.getFileUrl(ship.picture2);
|
|
|
this.page3Img = this.getFileUrl(ship.picture3);
|
|
|
this.originalPage1Img = this.page1Img;
|
|
|
this.originalPage2Img = this.page2Img;
|
|
|
this.originalPage3Img = this.page3Img;
|
|
|
} catch (e) {
|
|
|
console.error("解析 ship 参数失败:", e);
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
});
|
|
|
|
|
|
// #ifdef H5
|
|
|
this.isWeixinBrowser = /MicroMessenger/i.test(navigator.userAgent);
|
|
|
// #endif
|
|
|
},
|
|
|
methods: {
|
|
|
// 获取单价和计算规则
|
|
|
async fetchUnitPrice() {
|
|
|
const token = uni.getStorageSync("token");
|
|
|
if (!token) {
|
|
|
return;
|
|
|
}
|
|
|
const res = await new Promise((resolve, reject) => {
|
|
|
uni.request({
|
|
|
url: `${API.GET_UNIT_PRICE}?token=${token}`,
|
|
|
method: "get",
|
|
|
success: resolve,
|
|
|
fail: reject,
|
|
|
});
|
|
|
});
|
|
|
if (res.data && res.data.errcode === 0) {
|
|
|
const data = res.data.data;
|
|
|
this.unitPrice = data.price || "";
|
|
|
this.calculationDescription = data.price_desc || "";
|
|
|
}
|
|
|
},
|
|
|
// 计算单次过闸收费
|
|
|
// totalPriceText() {
|
|
|
// const unit = Number(this.unitPrice);
|
|
|
// if (isNaN(unit)) return "-";
|
|
|
// return `${unit}元(${this.calculationDescription})`;
|
|
|
// },
|
|
|
async fetchShipInspectionExample() {
|
|
|
const token = uni.getStorageSync("token");
|
|
|
if (!token) {
|
|
|
return;
|
|
|
}
|
|
|
const res = await new Promise((resolve, reject) => {
|
|
|
uni.request({
|
|
|
url: `${API.GET_SHIP_INSPECTION_EXAMPLES}?token=${token}`,
|
|
|
method: "get",
|
|
|
success: resolve,
|
|
|
fail: reject,
|
|
|
});
|
|
|
});
|
|
|
if (res.data && res.data.errcode === 0) {
|
|
|
const data = res.data.data;
|
|
|
this.shipInspectionExamples = data || [];
|
|
|
}
|
|
|
},
|
|
|
// 编辑模式:根据 id 获取船舶详情并填充表单
|
|
|
async fetchShipDetailForEdit(id) {
|
|
|
const token = uni.getStorageSync("token");
|
|
|
if (!token) {
|
|
|
uni.showToast({ title: "请先登录", icon: "none" });
|
|
|
return;
|
|
|
}
|
|
|
if (!id) {
|
|
|
uni.showToast({ title: "无效的船舶ID", icon: "none" });
|
|
|
return;
|
|
|
}
|
|
|
uni.showLoading({ title: "加载中..." });
|
|
|
try {
|
|
|
const res = await new Promise((resolve, reject) => {
|
|
|
uni.request({
|
|
|
url: `${API.SHIP_DETAIL}/${id}`,
|
|
|
method: "GET",
|
|
|
data: { token },
|
|
|
success: resolve,
|
|
|
fail: reject,
|
|
|
});
|
|
|
});
|
|
|
uni.hideLoading();
|
|
|
if (res.data && res.data.errcode === 0 && res.data.data) {
|
|
|
const ship = res.data.data;
|
|
|
console.log("编辑模式-详情数据:", ship);
|
|
|
this.isEdit = true;
|
|
|
// 填充表单基础信息
|
|
|
this.form.id = ship.id || id;
|
|
|
this.form.owner = ship.owner_name || "";
|
|
|
this.form.idCard = ship.id_card || "";
|
|
|
this.form.phone = ship.phone || "";
|
|
|
this.form.shipNo = ship.ship_number || "";
|
|
|
this.form.ton = ship.total_tonnage || "";
|
|
|
this.form.length = ship.total_length || "";
|
|
|
this.form.width = ship.total_width || "";
|
|
|
this.form.depth = ship.molded_depth || "";
|
|
|
this.form.tonLevel = ship.tonnage_class || "";
|
|
|
this.form.shipType = (ship.ship_type || "").toString();
|
|
|
this.form.feeType = ship.fee_type || 1; // 默认货船
|
|
|
|
|
|
// 船检簿图片 fileId
|
|
|
this.form.page1FileId = ship.picture1 || "";
|
|
|
this.form.page2FileId = ship.picture2 || "";
|
|
|
this.form.page3FileId = ship.picture3 || "";
|
|
|
|
|
|
// 预览图片:优先使用 picture*_file.url,其次回退到预览接口
|
|
|
const p1Url =
|
|
|
ship.picture1_file && ship.picture1_file.url
|
|
|
? ship.picture1_file.url
|
|
|
: this.getFileUrl(ship.picture1);
|
|
|
const p2Url =
|
|
|
ship.picture2_file && ship.picture2_file.url
|
|
|
? ship.picture2_file.url
|
|
|
: this.getFileUrl(ship.picture2);
|
|
|
const p3Url =
|
|
|
ship.picture3_file && ship.picture3_file.url
|
|
|
? ship.picture3_file.url
|
|
|
: this.getFileUrl(ship.picture3);
|
|
|
|
|
|
this.page1Img = p1Url;
|
|
|
this.page2Img = p2Url;
|
|
|
this.page3Img = p3Url;
|
|
|
|
|
|
this.originalPage1Img = p1Url;
|
|
|
this.originalPage2Img = p2Url;
|
|
|
this.originalPage3Img = p3Url;
|
|
|
|
|
|
// 手写签名显示:后端返回字段为 signature
|
|
|
// 有签名时回显图片,并视为已签名,避免再次提示“请完成签名”
|
|
|
if (ship.signature) {
|
|
|
this.signImg = ship.signature;
|
|
|
this.originalSignature = ship.signature;
|
|
|
this.hasSigned = true;
|
|
|
}
|
|
|
} else {
|
|
|
uni.showToast({
|
|
|
title: (res.data && res.data.errmsg) || "获取船舶详情失败",
|
|
|
icon: "none",
|
|
|
});
|
|
|
}
|
|
|
} catch (error) {
|
|
|
uni.hideLoading();
|
|
|
console.error("获取船舶详情异常:", error);
|
|
|
uni.showToast({ title: error.message || "网络错误", icon: "none" });
|
|
|
}
|
|
|
},
|
|
|
|
|
|
async fetchShipPropertyEnum() {
|
|
|
const token = uni.getStorageSync("token");
|
|
|
if (!token) {
|
|
|
uni.showToast({ title: "请先登录", icon: "none" });
|
|
|
return false;
|
|
|
}
|
|
|
uni.showLoading({ title: "加载中..." });
|
|
|
try {
|
|
|
const res = await new Promise((resolve, reject) => {
|
|
|
uni.request({
|
|
|
url: API.SHIP_PROPERTY_ENUM,
|
|
|
method: "GET",
|
|
|
data: { token },
|
|
|
success: resolve,
|
|
|
fail: reject,
|
|
|
});
|
|
|
});
|
|
|
uni.hideLoading();
|
|
|
if (res.data && res.data.errcode === 0) {
|
|
|
const enumData = res.data.data;
|
|
|
this.shipTypeEnum = enumData.ship_type;
|
|
|
this.tonnageClassEnum = enumData.tonnage_class;
|
|
|
this.feeTypeEnum = enumData.fee_type;
|
|
|
// 设置默认值
|
|
|
if (!this.isEdit) {
|
|
|
this.form.shipType = Object.values(this.shipTypeEnum)[0].toString();
|
|
|
this.form.tonLevel = Object.values(this.tonnageClassEnum)[0];
|
|
|
this.form.feeType = 1; // 默认货船
|
|
|
}
|
|
|
return true;
|
|
|
} else {
|
|
|
uni.showToast({
|
|
|
title: res.data.errmsg || "获取枚举失败",
|
|
|
icon: "none",
|
|
|
});
|
|
|
return false;
|
|
|
}
|
|
|
} catch (error) {
|
|
|
uni.hideLoading();
|
|
|
uni.showToast({ title: error.message || "网络错误", icon: "none" });
|
|
|
return false;
|
|
|
}
|
|
|
},
|
|
|
getFileUrl(fileId) {
|
|
|
if (!fileId) return "";
|
|
|
return `${API.BASE_URL}/api/customer/upload-file/preview?id=${fileId}`;
|
|
|
},
|
|
|
async uploadFile(filePath) {
|
|
|
return new Promise((resolve, reject) => {
|
|
|
const token = uni.getStorageSync("token");
|
|
|
if (!token) {
|
|
|
reject(new Error("未登录或登录已过期"));
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
uni.uploadFile({
|
|
|
url: API.UPLOAD_FILE,
|
|
|
filePath: filePath,
|
|
|
name: "file",
|
|
|
formData: {
|
|
|
token: token,
|
|
|
},
|
|
|
success: (res) => {
|
|
|
if (res.statusCode === 200) {
|
|
|
const data = JSON.parse(res.data);
|
|
|
if (data.errcode && data.errcode !== 0) {
|
|
|
reject(new Error(data.errmsg || "Upload failed"));
|
|
|
} else {
|
|
|
resolve(data);
|
|
|
}
|
|
|
} else {
|
|
|
reject(new Error("Upload failed"));
|
|
|
}
|
|
|
},
|
|
|
fail: (err) => {
|
|
|
reject(err);
|
|
|
},
|
|
|
});
|
|
|
});
|
|
|
},
|
|
|
// 添加数字校验方法
|
|
|
isValidNumber(value) {
|
|
|
if (!value) return false;
|
|
|
// 允许小数点的数字
|
|
|
return /^\d+(\.\d+)?$/.test(value);
|
|
|
},
|
|
|
async nextStep() {
|
|
|
if (this.currentStep === 1) {
|
|
|
// 基本信息校验
|
|
|
if (!this.form.owner) {
|
|
|
uni.showToast({ title: "请填写船舶所有人", icon: "none" });
|
|
|
return;
|
|
|
}
|
|
|
if (!this.form.idCard) {
|
|
|
uni.showToast({ title: "请填写身份证号", icon: "none" });
|
|
|
return;
|
|
|
}
|
|
|
if (!this.form.phone) {
|
|
|
uni.showToast({ title: "请填写联系电话", icon: "none" });
|
|
|
return;
|
|
|
}
|
|
|
if (!/^\d{11}$/.test(this.form.phone)) {
|
|
|
uni.showToast({ title: "联系电话格式不正确", icon: "none" });
|
|
|
return;
|
|
|
}
|
|
|
if (!this.form.shipNo) {
|
|
|
uni.showToast({ title: "请填写船名", icon: "none" });
|
|
|
return;
|
|
|
}
|
|
|
}
|
|
|
if (this.currentStep === 2) {
|
|
|
if (!this.form.ton) {
|
|
|
uni.showToast({ title: "请填写载重吨位", icon: "none" });
|
|
|
return;
|
|
|
}
|
|
|
if (!this.isValidNumber(this.form.ton)) {
|
|
|
uni.showToast({ title: "载重吨位必须是数字", icon: "none" });
|
|
|
return;
|
|
|
}
|
|
|
if (!this.form.length) {
|
|
|
uni.showToast({ title: "请填写总长度", icon: "none" });
|
|
|
return;
|
|
|
}
|
|
|
if (!this.isValidNumber(this.form.length)) {
|
|
|
uni.showToast({ title: "总长度必须是数字", icon: "none" });
|
|
|
return;
|
|
|
}
|
|
|
if (!this.form.width) {
|
|
|
uni.showToast({ title: "请填写总宽", icon: "none" });
|
|
|
return;
|
|
|
}
|
|
|
if (!this.isValidNumber(this.form.width)) {
|
|
|
uni.showToast({ title: "总宽必须是数字", icon: "none" });
|
|
|
return;
|
|
|
}
|
|
|
if (!this.form.depth) {
|
|
|
uni.showToast({ title: "请填写型深", icon: "none" });
|
|
|
return;
|
|
|
}
|
|
|
if (!this.isValidNumber(this.form.depth)) {
|
|
|
uni.showToast({ title: "型深必须是数字", icon: "none" });
|
|
|
return;
|
|
|
}
|
|
|
if (!this.form.tonLevel) {
|
|
|
uni.showToast({ title: "请选择参考载重吨位", icon: "none" });
|
|
|
return;
|
|
|
}
|
|
|
if (!this.form.shipType) {
|
|
|
uni.showToast({ title: "请选择船型", icon: "none" });
|
|
|
return;
|
|
|
}
|
|
|
}
|
|
|
if (this.currentStep === 3) {
|
|
|
// 上传船检簿图片
|
|
|
if (!this.page1Img) {
|
|
|
uni.showToast({ title: "请上传第一页图片", icon: "none" });
|
|
|
return;
|
|
|
}
|
|
|
if (!this.page2Img) {
|
|
|
uni.showToast({ title: "请上传第二页图片", icon: "none" });
|
|
|
return;
|
|
|
}
|
|
|
if (!this.page3Img) {
|
|
|
uni.showToast({ title: "请上传第三页图片", icon: "none" });
|
|
|
return;
|
|
|
}
|
|
|
try {
|
|
|
uni.showLoading({ title: "上传中..." });
|
|
|
// 检查第一页是否需要上传
|
|
|
if (
|
|
|
this.page1Img !== this.originalPage1Img ||
|
|
|
!this.form.page1FileId
|
|
|
) {
|
|
|
const page1Result = await this.uploadFile(this.page1Img);
|
|
|
this.form.page1FileId = page1Result.data.id;
|
|
|
}
|
|
|
// 检查第二页是否需要上传
|
|
|
if (
|
|
|
this.page2Img !== this.originalPage2Img ||
|
|
|
!this.form.page2FileId
|
|
|
) {
|
|
|
const page2Result = await this.uploadFile(this.page2Img);
|
|
|
this.form.page2FileId = page2Result.data.id;
|
|
|
}
|
|
|
// 检查第三页是否需要上传
|
|
|
if (
|
|
|
this.page3Img !== this.originalPage3Img ||
|
|
|
!this.form.page3FileId
|
|
|
) {
|
|
|
const page3Result = await this.uploadFile(this.page3Img);
|
|
|
this.form.page3FileId = page3Result.data.id;
|
|
|
}
|
|
|
uni.hideLoading();
|
|
|
} catch (error) {
|
|
|
uni.hideLoading();
|
|
|
uni.showToast({
|
|
|
title: error.message || "上传失败,请重试",
|
|
|
icon: "none",
|
|
|
});
|
|
|
return;
|
|
|
}
|
|
|
}
|
|
|
if (this.currentStep < 4) this.currentStep++;
|
|
|
},
|
|
|
async submit() {
|
|
|
// 验证签名确认
|
|
|
if (!this.signChecked) {
|
|
|
uni.showToast({ title: "请勾选承诺声明", icon: "none" });
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
if (!this.hasSigned) {
|
|
|
uni.showToast({ title: "请完成签名", icon: "none" });
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
try {
|
|
|
uni.showLoading({ title: "处理中..." });
|
|
|
|
|
|
// 处理签名:
|
|
|
// - 如果是编辑模式且没有重新绘制签名(hasDrawn 为 false),直接使用原有 signature
|
|
|
// - 如果用户重新签名,则从画布生成新的 base64
|
|
|
let signValue = "";
|
|
|
if (this.isEdit && this.originalSignature && !this.hasDrawn) {
|
|
|
signValue = this.originalSignature;
|
|
|
} else {
|
|
|
signValue = await this.getSignBase64();
|
|
|
}
|
|
|
const token = uni.getStorageSync("token");
|
|
|
if (!token) {
|
|
|
uni.hideLoading();
|
|
|
uni.showToast({ title: "登录已失效", icon: "none" });
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
// 组装接口参数
|
|
|
const params = {
|
|
|
token: token,
|
|
|
owner_name: this.form.owner,
|
|
|
id_card: this.form.idCard,
|
|
|
phone: this.form.phone,
|
|
|
ship_number: this.form.shipNo,
|
|
|
total_tonnage: this.form.ton,
|
|
|
total_length: this.form.length,
|
|
|
total_width: this.form.width,
|
|
|
molded_depth: this.form.depth,
|
|
|
tonnage_class: this.form.tonLevel,
|
|
|
ship_type: this.form.shipType,
|
|
|
fee_type: this.form.feeType,
|
|
|
picture1: this.form.page1FileId,
|
|
|
picture2: this.form.page2FileId,
|
|
|
picture3: this.form.page3FileId,
|
|
|
signature: signValue,
|
|
|
};
|
|
|
|
|
|
// 编辑模式下调用更新接口
|
|
|
let url = API.SHIP_CREATE;
|
|
|
let method = "POST";
|
|
|
if (this.isEdit && this.form.id) {
|
|
|
url = `${API.SHIP_UPDATE}/${encodeURIComponent(this.form.id)}`;
|
|
|
method = "POST";
|
|
|
}
|
|
|
|
|
|
console.log("提交参数:", params, url);
|
|
|
|
|
|
// 提交表单
|
|
|
const res = await new Promise((resolve, reject) => {
|
|
|
uni.request({
|
|
|
url: url,
|
|
|
method: method,
|
|
|
data: params,
|
|
|
header: {
|
|
|
"Content-Type": "application/json",
|
|
|
},
|
|
|
success: resolve,
|
|
|
fail: reject,
|
|
|
});
|
|
|
});
|
|
|
|
|
|
uni.hideLoading();
|
|
|
if (res.data && res.data.errcode === 0) {
|
|
|
uni.showToast({ title: "提交成功", icon: "success" });
|
|
|
setTimeout(() => {
|
|
|
uni.navigateBack();
|
|
|
}, 800);
|
|
|
} else {
|
|
|
console.log("提交失败返回:", res.data);
|
|
|
const resp = res.data || {};
|
|
|
const detail = resp.data || {};
|
|
|
let msg = "";
|
|
|
|
|
|
// 针对手写签名字段的错误提示,后端返回格式类似:
|
|
|
// { signature: ['签名不能为空'] }
|
|
|
if (detail && typeof detail === "object") {
|
|
|
if (Array.isArray(detail.signature) && detail.signature.length) {
|
|
|
msg = `签名:${detail.signature.join(",")}`;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
if (!msg) {
|
|
|
msg = resp.errmsg || "提交失败";
|
|
|
}
|
|
|
|
|
|
uni.showToast({ title: msg, icon: "none" });
|
|
|
}
|
|
|
} catch (error) {
|
|
|
uni.hideLoading();
|
|
|
uni.showToast({
|
|
|
title: error.message || "提交失败,请重试",
|
|
|
icon: "none",
|
|
|
});
|
|
|
}
|
|
|
},
|
|
|
prevStep() {
|
|
|
if (this.currentStep > 1) this.currentStep--;
|
|
|
},
|
|
|
chooseImage(page) {
|
|
|
uni.chooseImage({
|
|
|
count: 1,
|
|
|
success: (res) => {
|
|
|
if (page === "page1") {
|
|
|
this.page1Img = res.tempFilePaths[0];
|
|
|
this.originalPage1Img = res.tempFilePaths[0];
|
|
|
}
|
|
|
if (page === "page2") {
|
|
|
this.page2Img = res.tempFilePaths[0];
|
|
|
this.originalPage2Img = res.tempFilePaths[0];
|
|
|
}
|
|
|
if (page === "page3") {
|
|
|
this.page3Img = res.tempFilePaths[0];
|
|
|
this.originalPage3Img = res.tempFilePaths[0];
|
|
|
}
|
|
|
},
|
|
|
});
|
|
|
},
|
|
|
deleteImage(page) {
|
|
|
if (page === "page1") {
|
|
|
this.page1Img = "";
|
|
|
this.originalPage1Img = "";
|
|
|
this.form.page1FileId = "";
|
|
|
}
|
|
|
if (page === "page2") {
|
|
|
this.page2Img = "";
|
|
|
this.originalPage2Img = "";
|
|
|
this.form.page2FileId = "";
|
|
|
}
|
|
|
if (page === "page3") {
|
|
|
this.page3Img = "";
|
|
|
this.originalPage3Img = "";
|
|
|
this.form.page3FileId = "";
|
|
|
}
|
|
|
},
|
|
|
viewExample() {
|
|
|
uni.showToast({ title: "查看示例", icon: "none" });
|
|
|
},
|
|
|
// 根据索引获取对应的示例数据(直接按数组顺序,最多显示前3条)
|
|
|
getExampleByIndex(index) {
|
|
|
if (
|
|
|
!Array.isArray(this.shipInspectionExamples) ||
|
|
|
this.shipInspectionExamples.length === 0
|
|
|
) {
|
|
|
return null;
|
|
|
}
|
|
|
// 只显示前3条数据,索引范围 0-2
|
|
|
if (index < 0 || index >= 3) {
|
|
|
return null;
|
|
|
}
|
|
|
// 如果索引超出数组长度,返回 null
|
|
|
if (index >= this.shipInspectionExamples.length) {
|
|
|
return null;
|
|
|
}
|
|
|
return this.shipInspectionExamples[index] || null;
|
|
|
},
|
|
|
// 预览示例图片
|
|
|
previewExample(imageUrl) {
|
|
|
if (!imageUrl) return;
|
|
|
uni.previewImage({
|
|
|
urls: [imageUrl],
|
|
|
current: imageUrl,
|
|
|
});
|
|
|
},
|
|
|
startSign(e) {
|
|
|
this.isSigning = true;
|
|
|
this.hasSigned = true;
|
|
|
const ctx = uni.createCanvasContext("signCanvas", this);
|
|
|
const { x, y } = e.touches[0];
|
|
|
// 填充白底(只在第一次签名时填充)
|
|
|
if (!this.hasDrawn) {
|
|
|
ctx.setFillStyle("#fff");
|
|
|
ctx.fillRect(0, 0, this.canvasWidth, this.canvasHeight);
|
|
|
this.hasDrawn = true;
|
|
|
}
|
|
|
ctx.moveTo(x, y);
|
|
|
ctx.setStrokeStyle("#222");
|
|
|
ctx.setLineWidth(4);
|
|
|
this.lastPoint = { x, y };
|
|
|
ctx.beginPath();
|
|
|
ctx.draw(true);
|
|
|
},
|
|
|
moveSign(e) {
|
|
|
if (!this.isSigning) return;
|
|
|
const ctx = uni.createCanvasContext("signCanvas", this);
|
|
|
const { x, y } = e.touches[0];
|
|
|
ctx.moveTo(this.lastPoint.x, this.lastPoint.y);
|
|
|
ctx.lineTo(x, y);
|
|
|
ctx.setStrokeStyle("#222");
|
|
|
ctx.setLineWidth(4);
|
|
|
ctx.stroke();
|
|
|
ctx.draw(true);
|
|
|
this.lastPoint = { x, y };
|
|
|
},
|
|
|
endSign() {
|
|
|
this.isSigning = false;
|
|
|
},
|
|
|
resetSign() {
|
|
|
this.signImg = "";
|
|
|
this.hasDrawn = false;
|
|
|
this.hasSigned = false;
|
|
|
const ctx = uni.createCanvasContext("signCanvas", this);
|
|
|
ctx.setFillStyle("#fff");
|
|
|
ctx.fillRect(0, 0, this.canvasWidth, this.canvasHeight);
|
|
|
ctx.draw();
|
|
|
},
|
|
|
previewSign() {
|
|
|
uni
|
|
|
.createSelectorQuery()
|
|
|
.select("#signCanvas")
|
|
|
.boundingClientRect((rect) => {
|
|
|
uni.canvasToTempFilePath(
|
|
|
{
|
|
|
canvasId: "signCanvas",
|
|
|
width: rect.width,
|
|
|
height: rect.height,
|
|
|
success: (res) => {
|
|
|
// this.signImg = res.tempFilePath
|
|
|
uni.previewImage({
|
|
|
urls: [res.tempFilePath],
|
|
|
});
|
|
|
},
|
|
|
fail: (err) => {
|
|
|
uni.showToast({ title: "签名生成失败", icon: "none" });
|
|
|
},
|
|
|
},
|
|
|
this
|
|
|
);
|
|
|
})
|
|
|
.exec();
|
|
|
},
|
|
|
onTonLevelChange(e) {
|
|
|
this.form.tonLevel = e.detail.value;
|
|
|
console.log(e.detail.value);
|
|
|
},
|
|
|
onShipTypeChange(e) {
|
|
|
this.form.shipType = e.detail.value;
|
|
|
},
|
|
|
onFeeTypeChange(e) {
|
|
|
this.form.feeType = e.detail.value;
|
|
|
},
|
|
|
toggleSignChecked() {
|
|
|
this.signChecked = !this.signChecked;
|
|
|
},
|
|
|
// 获取签名base64
|
|
|
getSignBase64() {
|
|
|
return new Promise((resolve, reject) => {
|
|
|
uni.canvasToTempFilePath(
|
|
|
{
|
|
|
canvasId: "signCanvas",
|
|
|
success: (res) => {
|
|
|
// #ifdef H5
|
|
|
// H5 端直接用 base64
|
|
|
resolve(res.tempFilePath);
|
|
|
// #endif
|
|
|
|
|
|
// #ifndef H5
|
|
|
// 小程序端用 FileSystemManager 读取为 base64
|
|
|
if (typeof wx !== "undefined" && wx.getFileSystemManager) {
|
|
|
wx.getFileSystemManager().readFile({
|
|
|
filePath: res.tempFilePath,
|
|
|
encoding: "base64",
|
|
|
success: (fileRes) => {
|
|
|
resolve(fileRes.data);
|
|
|
},
|
|
|
fail: reject,
|
|
|
});
|
|
|
} else {
|
|
|
// 其他端 fallback
|
|
|
resolve(res.tempFilePath);
|
|
|
}
|
|
|
// #endif
|
|
|
},
|
|
|
fail: reject,
|
|
|
},
|
|
|
this
|
|
|
);
|
|
|
});
|
|
|
},
|
|
|
},
|
|
|
};
|
|
|
</script>
|
|
|
|
|
|
<style lang="scss" scoped>
|
|
|
@import "@/styles/common.scss";
|
|
|
|
|
|
.ship-detail-bg {
|
|
|
min-height: 100vh;
|
|
|
background: linear-gradient(180deg, #cbe6ff 0%, #f6faff 100%);
|
|
|
padding-bottom: 32rpx;
|
|
|
}
|
|
|
|
|
|
.wechat-browser {
|
|
|
padding-top: 10rpx;
|
|
|
}
|
|
|
|
|
|
.wechat-browser .content-area {
|
|
|
padding-top: 0;
|
|
|
}
|
|
|
|
|
|
.fixed-nav {
|
|
|
position: fixed;
|
|
|
top: 0;
|
|
|
left: 0;
|
|
|
right: 0;
|
|
|
z-index: 100;
|
|
|
background: linear-gradient(180deg, #cbe6ff 0%, #f6faff 100%);
|
|
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
|
|
}
|
|
|
.content-area {
|
|
|
padding-top: 90px;
|
|
|
}
|
|
|
.step-bar {
|
|
|
display: flex;
|
|
|
align-items: flex-start;
|
|
|
justify-content: space-between;
|
|
|
width: 100%;
|
|
|
margin: 64rpx auto 0 auto;
|
|
|
padding: 0 32rpx;
|
|
|
position: relative;
|
|
|
}
|
|
|
.step-group {
|
|
|
display: flex;
|
|
|
flex-direction: column;
|
|
|
align-items: center;
|
|
|
flex: 1;
|
|
|
position: relative;
|
|
|
}
|
|
|
.step-circle {
|
|
|
width: 60rpx;
|
|
|
height: 60rpx;
|
|
|
border-radius: 50%;
|
|
|
background: #e3eaf7;
|
|
|
color: #b0b8c6;
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
justify-content: center;
|
|
|
font-size: 32rpx;
|
|
|
font-weight: bold;
|
|
|
transition: background 0.2s, color 0.2s;
|
|
|
z-index: 1;
|
|
|
}
|
|
|
.step-circle.active {
|
|
|
background: #fff;
|
|
|
color: #217aff;
|
|
|
border: 4rpx solid #217aff;
|
|
|
}
|
|
|
.step-label {
|
|
|
margin-top: 18rpx;
|
|
|
font-size: 24rpx;
|
|
|
color: #222;
|
|
|
font-weight: normal;
|
|
|
text-align: center;
|
|
|
min-width: 60rpx;
|
|
|
white-space: nowrap;
|
|
|
}
|
|
|
.step-label.active {
|
|
|
color: #217aff;
|
|
|
font-weight: bold;
|
|
|
}
|
|
|
.step-line {
|
|
|
position: absolute;
|
|
|
top: 30rpx;
|
|
|
left: 50%;
|
|
|
width: 100%;
|
|
|
height: 4rpx;
|
|
|
background: #e3eaf7;
|
|
|
z-index: 0;
|
|
|
}
|
|
|
.info-card {
|
|
|
background: #fff;
|
|
|
border-radius: 24rpx;
|
|
|
margin: 0 32rpx;
|
|
|
padding: 32rpx 24rpx;
|
|
|
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.06);
|
|
|
margin-top: 60rpx;
|
|
|
}
|
|
|
.info-title {
|
|
|
@include font-primary;
|
|
|
font-weight: bold;
|
|
|
margin-bottom: 24rpx;
|
|
|
color: #222;
|
|
|
}
|
|
|
.info-form {
|
|
|
display: flex;
|
|
|
flex-direction: column;
|
|
|
gap: 18rpx;
|
|
|
}
|
|
|
.form-row {
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
border-bottom: 1px solid #f0f0f0;
|
|
|
padding: 16rpx 0;
|
|
|
}
|
|
|
.form-label {
|
|
|
width: 200rpx;
|
|
|
@include font-secondary;
|
|
|
color: #222;
|
|
|
}
|
|
|
.form-label.required::before {
|
|
|
content: "*";
|
|
|
color: #ff5c5c;
|
|
|
margin-right: 6rpx;
|
|
|
}
|
|
|
.form-input {
|
|
|
flex: 1;
|
|
|
@include font-secondary;
|
|
|
color: #333;
|
|
|
border: none;
|
|
|
outline: none;
|
|
|
background: transparent;
|
|
|
}
|
|
|
.form-static {
|
|
|
flex: 1;
|
|
|
@include font-secondary;
|
|
|
color: #222;
|
|
|
text-align: left;
|
|
|
}
|
|
|
.step-btn-bar {
|
|
|
display: flex;
|
|
|
justify-content: center;
|
|
|
gap: 32rpx;
|
|
|
margin: 64rpx 0 0 0;
|
|
|
margin-top: 100rpx;
|
|
|
}
|
|
|
.step-btn {
|
|
|
min-width: 270rpx;
|
|
|
height: 80rpx;
|
|
|
border-radius: 40rpx;
|
|
|
@include font-secondary;
|
|
|
font-weight: 500;
|
|
|
border: none;
|
|
|
outline: none;
|
|
|
background: #217aff;
|
|
|
color: #fff;
|
|
|
box-shadow: 0 4rpx 16rpx rgba(33, 122, 255, 0.08);
|
|
|
transition: background 0.2s;
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
justify-content: center;
|
|
|
}
|
|
|
.step-btn.single-btn {
|
|
|
min-width: 420rpx;
|
|
|
}
|
|
|
.prev-btn {
|
|
|
background: #e3eaf7;
|
|
|
color: black;
|
|
|
border: none;
|
|
|
outline: none;
|
|
|
&::after {
|
|
|
border: none;
|
|
|
}
|
|
|
}
|
|
|
.next-btn {
|
|
|
background: #217aff;
|
|
|
color: #fff;
|
|
|
}
|
|
|
.info-desc {
|
|
|
@include font-tertiary;
|
|
|
color: #888;
|
|
|
margin-left: 12rpx;
|
|
|
font-weight: normal;
|
|
|
}
|
|
|
.form-unit {
|
|
|
@include font-tertiary;
|
|
|
color: #888;
|
|
|
margin-left: 8rpx;
|
|
|
}
|
|
|
.form-tip {
|
|
|
@include font-tertiary;
|
|
|
color: #b0b8c6;
|
|
|
margin-bottom: 8rpx;
|
|
|
margin-left: 180rpx;
|
|
|
}
|
|
|
.form-radio-group {
|
|
|
display: flex;
|
|
|
gap: 32rpx;
|
|
|
margin-left: 24rpx;
|
|
|
}
|
|
|
.form-radio {
|
|
|
@include font-secondary;
|
|
|
color: #222;
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
}
|
|
|
/* 船舶类型特殊样式 */
|
|
|
.fee-type-group {
|
|
|
display: flex;
|
|
|
flex-direction: column;
|
|
|
gap: 24rpx;
|
|
|
margin-left: 24rpx;
|
|
|
width: 100%;
|
|
|
}
|
|
|
.fee-type-item {
|
|
|
display: flex;
|
|
|
align-items: flex-start;
|
|
|
width: 100%;
|
|
|
}
|
|
|
.fee-type-info {
|
|
|
display: flex;
|
|
|
flex-direction: column;
|
|
|
margin-left: 8rpx;
|
|
|
flex: 1;
|
|
|
}
|
|
|
.fee-type-label {
|
|
|
font-size: 28rpx;
|
|
|
color: #222;
|
|
|
}
|
|
|
.fee-type-desc {
|
|
|
font-size: 24rpx;
|
|
|
color: #888;
|
|
|
margin-top: 4rpx;
|
|
|
}
|
|
|
.info-title-row {
|
|
|
display: flex;
|
|
|
justify-content: space-between;
|
|
|
align-items: center;
|
|
|
margin-bottom: 24rpx;
|
|
|
}
|
|
|
.example-btn {
|
|
|
background: #edf0f5;
|
|
|
@include font-secondary;
|
|
|
color: #222;
|
|
|
border-radius: 40rpx;
|
|
|
font-weight: 500;
|
|
|
padding: 18rpx 78rpx;
|
|
|
border: none;
|
|
|
height: 64rpx;
|
|
|
line-height: 1;
|
|
|
box-shadow: none;
|
|
|
margin-right: 20rpx;
|
|
|
}
|
|
|
.upload-section {
|
|
|
margin-bottom: 32rpx;
|
|
|
}
|
|
|
.upload-row {
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
gap: 24rpx;
|
|
|
margin-top: 12rpx;
|
|
|
}
|
|
|
.upload-img-box {
|
|
|
position: relative;
|
|
|
width: 140rpx;
|
|
|
height: 140rpx;
|
|
|
background: #f0f0f0;
|
|
|
border-radius: 12rpx;
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
justify-content: center;
|
|
|
overflow: hidden;
|
|
|
}
|
|
|
.upload-img {
|
|
|
width: 100%;
|
|
|
height: 100%;
|
|
|
object-fit: cover;
|
|
|
border-radius: 12rpx;
|
|
|
}
|
|
|
.upload-add {
|
|
|
width: 140rpx;
|
|
|
height: 140rpx;
|
|
|
color: #fff;
|
|
|
@include font-secondary;
|
|
|
display: flex;
|
|
|
flex-direction: column;
|
|
|
align-items: center;
|
|
|
justify-content: center;
|
|
|
background: #a6a8ab;
|
|
|
border-radius: 12rpx;
|
|
|
cursor: pointer;
|
|
|
font-weight: 500;
|
|
|
letter-spacing: 1rpx;
|
|
|
margin-bottom: 20rpx;
|
|
|
}
|
|
|
.upload-add text:first-child {
|
|
|
font-size: 48rpx;
|
|
|
}
|
|
|
.upload-add text:last-child {
|
|
|
@include font-tertiary;
|
|
|
}
|
|
|
.upload-del {
|
|
|
position: absolute;
|
|
|
top: 0;
|
|
|
right: 0;
|
|
|
width: 36rpx;
|
|
|
height: 36rpx;
|
|
|
background: rgba(0, 0, 0, 0.5);
|
|
|
color: #fff;
|
|
|
@include font-tertiary;
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
justify-content: center;
|
|
|
border-radius: 0 12rpx 0 12rpx;
|
|
|
cursor: pointer;
|
|
|
}
|
|
|
.upload-desc {
|
|
|
flex: 1;
|
|
|
background: #f5f7fa;
|
|
|
@include font-tertiary;
|
|
|
color: #b0b8c6;
|
|
|
border-radius: 12rpx;
|
|
|
padding: 0 24rpx;
|
|
|
min-height: 140rpx;
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
}
|
|
|
.example-info {
|
|
|
display: flex;
|
|
|
flex-direction: row;
|
|
|
align-items: center;
|
|
|
justify-content: flex-start;
|
|
|
width: 100%;
|
|
|
gap: 24rpx;
|
|
|
}
|
|
|
.example-img {
|
|
|
width: 120rpx;
|
|
|
height: 120rpx;
|
|
|
border-radius: 8rpx;
|
|
|
cursor: pointer;
|
|
|
object-fit: contain;
|
|
|
background: #fff;
|
|
|
flex-shrink: 0;
|
|
|
}
|
|
|
.example-text {
|
|
|
@include font-tertiary;
|
|
|
color: #666;
|
|
|
text-align: left;
|
|
|
line-height: 1.5;
|
|
|
font-size: 24rpx;
|
|
|
flex: 1;
|
|
|
}
|
|
|
.divider {
|
|
|
height: 2rpx;
|
|
|
background: #f0f0f0;
|
|
|
margin: 32rpx 0;
|
|
|
}
|
|
|
.sign-declare-row {
|
|
|
display: flex;
|
|
|
align-items: flex-start;
|
|
|
margin-bottom: 24rpx;
|
|
|
}
|
|
|
.sign-declare-text {
|
|
|
@include font-secondary;
|
|
|
color: #666;
|
|
|
margin-left: 16rpx;
|
|
|
line-height: 1.6;
|
|
|
}
|
|
|
.sign-area {
|
|
|
width: 100%;
|
|
|
min-height: 460rpx;
|
|
|
background: #f5f7fa;
|
|
|
border-radius: 16rpx;
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
justify-content: center;
|
|
|
margin-bottom: 32rpx;
|
|
|
position: relative;
|
|
|
}
|
|
|
.sign-placeholder {
|
|
|
@include font-tertiary;
|
|
|
color: #b0b8c6;
|
|
|
}
|
|
|
.sign-img {
|
|
|
width: 100%;
|
|
|
height: 460rpx;
|
|
|
object-fit: contain;
|
|
|
border-radius: 16rpx;
|
|
|
}
|
|
|
.sign-btn-bar {
|
|
|
display: flex;
|
|
|
justify-content: center;
|
|
|
gap: 32rpx;
|
|
|
margin-top: 48rpx;
|
|
|
}
|
|
|
.sign-btn {
|
|
|
min-width: 220rpx;
|
|
|
height: 70rpx;
|
|
|
border-radius: 35rpx;
|
|
|
@include font-secondary;
|
|
|
font-weight: 500;
|
|
|
border: none;
|
|
|
outline: none;
|
|
|
transition: background 0.2s;
|
|
|
}
|
|
|
.reset-btn {
|
|
|
background: #f5f7fa;
|
|
|
color: #888;
|
|
|
}
|
|
|
.preview-btn {
|
|
|
background: #217aff;
|
|
|
color: #fff;
|
|
|
}
|
|
|
.sign-canvas {
|
|
|
width: 100%;
|
|
|
height: 460rpx;
|
|
|
background: transparent;
|
|
|
border-radius: 16rpx;
|
|
|
position: absolute;
|
|
|
left: 0;
|
|
|
top: 0;
|
|
|
}
|
|
|
|
|
|
// 统一设置占位文字样式
|
|
|
::v-deep input::placeholder {
|
|
|
font-size: 24rpx !important;
|
|
|
color: #b0b8c6;
|
|
|
}
|
|
|
.price-info-section {
|
|
|
background: #fff;
|
|
|
border-radius: 24rpx;
|
|
|
margin: 0 24rpx 32rpx 24rpx;
|
|
|
box-shadow: 0 4rpx 16rpx rgba(59, 124, 255, 0.08);
|
|
|
padding: 32rpx 24rpx;
|
|
|
margin-top: 20px;
|
|
|
}
|
|
|
.price-info-item {
|
|
|
display: flex;
|
|
|
align-items: flex-start;
|
|
|
margin-bottom: 16rpx;
|
|
|
font-size: 28rpx;
|
|
|
}
|
|
|
.price-info-item:last-child {
|
|
|
margin-bottom: 0;
|
|
|
}
|
|
|
.price-label {
|
|
|
color: #666;
|
|
|
min-width: 140rpx;
|
|
|
flex-shrink: 0;
|
|
|
}
|
|
|
.price-value {
|
|
|
color: #217aff;
|
|
|
font-weight: 600;
|
|
|
flex: 1;
|
|
|
}
|
|
|
.price-desc {
|
|
|
color: #222;
|
|
|
flex: 1;
|
|
|
line-height: 1.6;
|
|
|
}
|
|
|
</style> |