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.

637 lines
16 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="reservation-page">
<view class="fixed-nav">
<NavBar title="添加船只" />
</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" placeholder="姓名与身份证一致" />
</view>
<view class="form-row">
<text class="form-label required">身份证号</text>
<input class="form-input" placeholder="输入正确的18位身份证号码" />
</view>
<view class="form-row">
<text class="form-label required">联系电话</text>
<input class="form-input" placeholder="11位常用的手机号" />
</view>
<view class="form-row">
<text class="form-label required">船舶编号</text>
<text class="form-static">ZJ12345</text>
</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" placeholder="请输入总吨位" />
<text class="form-unit">吨</text>
</view>
<view class="form-tip">填写船舶的总吨位数值</view>
<!-- 总长度 -->
<view class="form-row">
<text class="form-label required">总长度</text>
<input class="form-input" placeholder="请输入总长度" />
<text class="form-unit">米</text>
</view>
<view class="form-tip">填写船舶的总长度数值</view>
<!-- 总宽 -->
<view class="form-row">
<text class="form-label required">总宽</text>
<input class="form-input" placeholder="请输入总宽" />
<text class="form-unit">米</text>
</view>
<view class="form-tip">填写船舶的总宽数值</view>
<!-- 型深 -->
<view class="form-row">
<text class="form-label required">型深</text>
<input class="form-input" placeholder="请输入型深" />
<text class="form-unit">米</text>
</view>
<view class="form-tip">填写船舶的型深数值</view>
<!-- 参考载重吨位 -->
<view class="form-row">
<text class="form-label required">参考载重吨位</text>
<radio-group class="form-radio-group">
<label class="form-radio"><radio value="B级" />B级</label>
<label class="form-radio"><radio value="C级" />C级</label>
</radio-group>
</view>
<view class="form-tip">请选择船舶的参考载重吨位等级</view>
<!-- 船型 -->
<view class="form-row">
<text class="form-label required">船型</text>
<radio-group class="form-radio-group">
<label class="form-radio"><radio value="平" />平</label>
<label class="form-radio"><radio value="尖" />尖</label>
<label class="form-radio"><radio value="槽罐船" />槽罐船</label>
</radio-group>
</view>
<view class="form-tip">请选择船舶的类型</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>
<!-- 第四步:签名确认 -->
<view v-if="currentStep === 4" class="info-card">
<view class="info-title">签名确认</view>
<view class="sign-declare-row">
<checkbox v-model="signChecked" />
<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'
export default {
name: 'ShipManagerPage',
components: { NavBar },
data() {
return {
steps: ['基本信息', '船舶参数', '船检簿上传', '签名确认'],
currentStep: 1,
page1Img: '',
page2Img: '',
signChecked: false,
signImg: '',
isSigning: false,
lastPoint: null,
canvasWidth: 0,
canvasHeight: 0,
hasDrawn: false,
hasSigned: false
}
},
methods: {
nextStep() {
if (this.currentStep < 4) this.currentStep++
},
prevStep() {
if (this.currentStep > 1) this.currentStep--
},
submit() {
uni.showToast({ title: '提交成功', icon: 'success' })
},
chooseImage(page) {
uni.chooseImage({
count: 1,
success: (res) => {
if (page === 'page1') this.page1Img = res.tempFilePaths[0]
if (page === 'page2') this.page2Img = res.tempFilePaths[0]
}
})
},
deleteImage(page) {
if (page === 'page1') this.page1Img = ''
if (page === 'page2') this.page2Img = ''
},
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)
ctx.beginPath()
this.lastPoint = { x, y }
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()
}
}
}
</script>
<style scoped>
.reservation-page {
background: linear-gradient(180deg, #eaf3ff 0%, #f6faff 100%);
min-height: 100vh;
padding-bottom: 40rpx;
}
.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 {
font-size: 28rpx;
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: 180rpx;
font-size: 25rpx;
color: #222;
}
.form-label.required::before {
content: '*';
color: #ff5c5c;
margin-right: 6rpx;
}
.form-input {
flex: 1;
font-size: 25rpx;
color: #333;
border: none;
outline: none;
background: transparent;
}
.form-static {
flex: 1;
font-size: 25rpx;
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;
font-size: 32rpx;
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;
}
.step-btn.single-btn {
min-width: 420rpx;
}
.prev-btn {
background: #e3eaf7;
color: black;
}
.next-btn {
background: #217aff;
color: #fff;
}
.info-desc {
font-size: 20rpx;
color: #888;
margin-left: 12rpx;
font-weight: normal;
}
.form-unit {
font-size: 22rpx;
color: #888;
margin-left: 8rpx;
}
.form-tip {
font-size: 20rpx;
color: #b0b8c6;
margin-bottom: 8rpx;
margin-left: 180rpx;
}
.form-radio-group {
display: flex;
gap: 32rpx;
margin-left: 24rpx;
}
.form-radio {
font-size: 25rpx;
color: #222;
display: flex;
align-items: center;
}
.info-title-row {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24rpx;
}
.example-btn {
background: #edf0f5;
color: #222;
border-radius: 40rpx;
font-size: 30rpx;
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;
font-size: 32rpx;
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 {
font-size: 22rpx;
}
.upload-del {
position: absolute;
top: 0;
right: 0;
width: 36rpx;
height: 36rpx;
background: rgba(0,0,0,0.5);
color: #fff;
font-size: 28rpx;
display: flex;
align-items: center;
justify-content: center;
border-radius: 0 12rpx 0 12rpx;
cursor: pointer;
}
.upload-desc {
flex: 1;
background: #f5f7fa;
color: #b0b8c6;
font-size: 24rpx;
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 {
font-size: 25rpx;
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 {
color: #b0b8c6;
font-size: 28rpx;
}
.sign-img {
width: 100%;
height: 100%;
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;
font-size: 28rpx;
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;
}
</style>