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.

1048 lines
30 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="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>
</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" v-model="form.ton" 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" v-model="form.length" 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" v-model="form.width" 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" v-model="form.depth" 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" 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-tip">请选择船舶的参考载重吨位等级</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 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>
<input class="upload-desc" v-model="page1Desc" placeholder="请输入第一页说明" />
</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>
<input class="upload-desc" v-model="page2Desc" placeholder="请输入第二页说明" />
</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>
<input class="upload-desc" v-model="page3Desc" placeholder="请输入第三页说明" />
</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,
isEdit: false,
shipTypeEnum: {}, // 船型枚举
tonnageClassEnum: {}, // 载重吨位枚举
form: {
owner: 'sdf',
idCard: '510521198904074359',
phone: '12312312311',
shipNo: '111',
ton: '',
length: '',
width: '',
depth: '',
tonLevel: 'B',
shipType: '1',
page1FileId: '',
page2FileId: '',
page3FileId: ''
},
page1Desc: '',
page2Desc: '',
page3Desc: ''
}
},
onLoad(options) {
// 获取船舶属性枚举
this.fetchShipPropertyEnum().then(success => {
if (!success) {
// 如果获取失败,返回上一页
setTimeout(() => {
uni.navigateBack();
}, 1500);
return;
}
if (options.edit === '1' && options.ship) {
try {
const ship = JSON.parse(decodeURIComponent(options.ship));
this.isEdit = true;
// 填充表单
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.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) {}
}
});
},
methods: {
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;
// 设置默认值
if (!this.isEdit) {
this.form.shipType = Object.values(this.shipTypeEnum)[0].toString();
this.form.tonLevel = Object.values(this.tonnageClassEnum)[0];
}
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',
header: {
'Authorization': `Bearer ${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)
}
})
})
},
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 (!/^[1-9]\d{5}(18|19|20)?\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])\d{3}[0-9Xx]$/.test(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.form.length) {
uni.showToast({ title: '请填写总长度', icon: 'none' });
return;
}
if (!this.form.width) {
uni.showToast({ title: '请填写总宽', icon: 'none' });
return;
}
if (!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;
}
if (!this.page1Desc) {
uni.showToast({ title: '请填写第一页说明', icon: 'none' });
return;
}
if (!this.page2Desc) {
uni.showToast({ title: '请填写第二页说明', icon: 'none' });
return;
}
if (!this.page3Desc) {
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: '处理中...' });
const signBase64 = await this.getSignBase64();
console.log("signBase64",signBase64)
const token = uni.getStorageSync('token');
if (!token) {
uni.hideLoading();
uni.showToast({ title: '登录已失效', icon: 'none' });
return;
}
// 组装接口参数
const params = {
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,
picture1: this.form.page1FileId,
picture2: this.form.page2FileId,
picture3: this.form.page3FileId,
signature: signBase64
};
// 提交表单
const res = await new Promise((resolve, reject) => {
uni.request({
url: API.SHIP_CREATE,
method: 'POST',
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 {
uni.showToast({ title: res.data.errmsg || '提交失败', 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);
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()
},
onTonLevelChange(e) {
this.form.tonLevel = e.detail.value
console.log(e.detail.value)
},
onShipTypeChange(e) {
this.form.shipType = 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 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>