You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1250 lines
38 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<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="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 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="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>
</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>
</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>
</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: {}, // 船舶类型枚举
form: {
owner: '',
idCard: '',
phone: '',
shipNo: '',
feeType: 1, // 默认货船
ton: '',
length: '',
width: '',
depth: '',
tonLevel: 'B',
shipType: '1',
page1FileId: '',
page2FileId: '',
page3FileId: ''
},
isWeixinBrowser: false,
}
},
onLoad(options) {
// 获取船舶属性枚举
this.fetchShipPropertyEnum().then(success => {
if (!success) {
// 如果获取失败,返回上一页
setTimeout(() => {
uni.navigateBack();
}, 1500);
return;
}
// 编辑模式:优先通过 id 从后端获取详情
if (options && options.edit === '1') {
this.isEdit = true;
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: {
// 编辑模式:根据 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' })
},
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;
}
.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;
}
</style>