Files
zhxg_app/pages/dormitory/outsideAccommodation/applicationForm.vue
2025-12-18 23:42:58 +08:00

2024 lines
56 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<view class="app-container">
<!-- 顶部导航栏 -->
<!-- <view class="nav-bar" @click="goBack">
<uni-icons type="back" size="20" color="#fff"></uni-icons>
<text class="nav-title">外宿申请表单</text>
</view> -->
<!-- 表单主体容器 -->
<view class="main-container">
<!-- 选项卡容器 - 原生选项卡 -->
<view class="tabs-container">
<view class="tabs-header">
<!-- 详情模式下禁用选项卡切换 -->
<view class="tab-item" :class="{ active: activeTab === 0 }" @click="switchTab(0)">
<uni-icons type="contact" size="16" :color="activeTab === 0 ? '#409EFF' : '#666'"></uni-icons>
<text class="tab-text">基本信息</text>
</view>
<view class="tab-item" :class="{ active: activeTab === 1 }" @click="switchTab(1)">
<uni-icons type="chat" size="16" :color="activeTab === 1 ? '#409EFF' : '#666'"></uni-icons>
<text class="tab-text">外宿原因</text>
</view>
<view class="tab-item" :class="{ active: activeTab === 2 }" @click="switchTab(2)">
<uni-icons type="location" size="16" :color="activeTab === 2 ? '#409EFF' : '#666'"></uni-icons>
<text class="tab-text">地址信息</text>
</view>
<view class="tab-item" :class="{ active: activeTab === 3 }" @click="switchTab(3)">
<uni-icons type="person-filled" size="16"
:color="activeTab === 3 ? '#409EFF' : '#666'"></uni-icons>
<text class="tab-text">家长信息</text>
</view>
<view class="tab-item" :class="{ active: activeTab === 4 }" @click="switchTab(4)">
<uni-icons type="checkbox" size="16" :color="activeTab === 4 ? '#409EFF' : '#666'"></uni-icons>
<text class="tab-text">本人承诺</text>
</view>
</view>
</view>
<!-- 表单内容区域 -->
<scroll-view scroll-y class="form-scroll" :style="{ height: formScrollHeight }">
<view class="form-wrapper">
<!-- 1. 基本信息标签页 -->
<view v-show="activeTab === 0" class="tab-panel">
<view class="form-card">
<view class="card-title">基本信息</view>
<view class="form-item">
<label class="form-label">原宿舍号</label>
<!-- 详情模式下禁用输入框 -->
<input v-model="form.originalDormitory" placeholder="如1栋302"
class="form-input" :disabled="isDetailMode"></input>
</view>
<view class="form-item">
<label class="form-label">姓名</label>
<input v-model="form.studentName" placeholder="请输入姓名" class="form-input" :disabled="isDetailMode"></input>
</view>
<view class="form-item">
<label class="form-label">性别</label>
<!-- 详情模式下禁用选择器 -->
<picker mode="selector" :range="genderOptions" :range-key="'text'" v-model="form.gender"
@change="handleGenderChange" :disabled="isDetailMode">
<view class="picker-input">
{{ form.gender ? getGenderText(form.gender) : '请选择性别' }}
</view>
</picker>
</view>
<view class="form-item">
<label class="form-label">出生年月</label>
<picker mode="date" :value="form.birthDate" @change="handleBirthDateChange" :disabled="isDetailMode">
<view class="picker-input">
{{ form.birthDate || '请选择出生年月' }}
</view>
</picker>
</view>
<view class="form-item">
<label class="form-label">专业系</label>
<input v-model="form.majorName" placeholder="请输入专业系" class="form-input" :disabled="isDetailMode"></input>
</view>
<view class="form-item">
<label class="form-label">班级</label>
<input v-model="form.className" placeholder="请输入班级" class="form-input" :disabled="isDetailMode"></input>
</view>
<view class="form-item">
<label class="form-label">学号</label>
<input v-model="form.studentNo" placeholder="请输入学号" class="form-input" :disabled="isDetailMode"></input>
</view>
<view class="form-item">
<label class="form-label">身份证号码</label>
<input v-model="form.idCard" placeholder="请输入身份证号码" class="form-input" :disabled="isDetailMode"></input>
</view>
<view class="form-item">
<label class="form-label">联系电话</label>
<input v-model="form.studentPhone" placeholder="请输入联系电话" class="form-input"
type="number" :disabled="isDetailMode"></input>
</view>
<view class="form-item">
<label class="form-label">宿费交纳情况</label>
<view class="fee-status">{{ form.accommodationFee || '暂无数据' }}</view>
</view>
</view>
</view>
<!-- 2. 外宿原因标签页 -->
<view v-show="activeTab === 1" class="tab-panel">
<view class="form-card">
<view class="card-title card-apply">外宿原因及附件</view>
<view class="form-item">
<label class="form-label">外宿原因</label>
<textarea v-model="form.applyReason" placeholder="请详细描述外宿原因" class="form-textarea"
rows="5" :disabled="isDetailMode"></textarea>
</view>
<view class="form-item">
<label class="form-label">佐证附件</label>
<!-- 详情模式下禁用文件上传 -->
<view class="upload-btn" @click="chooseAffixFile" v-if="!isDetailMode">
<text class="upload-icon">+</text>
<text>上传文件</text>
</view>
<view class="file-list" v-if="affixFiles.length">
<view class="file-item" v-for="(file, index) in affixFiles" :key="index">
<text class="file-name" @click="previewImage(baseUrl + file.savePath)">{{ file.attachmentName || file.trueName }}</text>
<!-- 详情模式下禁用删除按钮 -->
<!-- <button size="mini" type="warn" @click="deleteAffixFile(index)" v-if="!isDetailMode"
class="delete-btn" :disabled="isDetailMode">删除</button> -->
<uni-icons type="trash-filled" size="30" @click="deleteAffixFile(index)" v-if="!isDetailMode"
class="delete-btn"></uni-icons>
</view>
</view>
<!-- <uni-file-picker :auto-upload="false" @select="chooseAffixFile" @delete="deleteAffixFile"></uni-file-picker> -->
<view class="upload-tip">支持上传jpg/png/pdf格式文件单个文件不超过10MB如病例住房证明等</view>
</view>
<view class="form-item">
<label class="form-label">电子签名</label>
<view class="signature-container">
<!-- 详情模式下隐藏签名按钮 -->
<button size="mini" type="primary" @click="!isDetailMode && openSignature('student')"
class="sign-btn" v-if="!isDetailMode">
签署电子签名
</button>
<image v-if="form.studentSignature" :src="baseUrl + form.studentSignature"
class="signature-preview" mode="widthFix"
@click="previewImage(baseUrl + form.studentSignature)"></image>
</view>
</view>
</view>
</view>
<!-- 3. 地址信息标签页 -->
<view v-show="activeTab === 2" class="tab-panel">
<view class="form-card">
<view class="card-title card-area">外宿地址信息</view>
<!-- 替换为省市区选择器 -->
<view class="form-item">
<label class="form-label">外宿地区</label>
<!-- 详情模式下禁用省市区选择器 -->
<view class="area-picker" @click="!isDetailMode && (showAreaPicker = true)" :class="{disabled: isDetailMode}">
<text class="picker-text">{{ areaText || '请选择省/市/区' }}</text>
<uni-icons type="arrowright" size="16" color="#999" class="picker-icon"></uni-icons>
</view>
</view>
<view class="form-item">
<label class="form-label">详细门牌号</label>
<input v-model="form.outsideAddress" placeholder="请输入详细门牌号XX小区3栋502"
class="form-input" :disabled="isDetailMode"></input>
</view>
<view class="form-item">
<label class="form-label">紧急联系人</label>
<input v-model="form.emergencyContact" placeholder="请输入紧急联系人姓名"
class="form-input" :disabled="isDetailMode"></input>
</view>
<view class="form-item">
<label class="form-label">紧急联系电话</label>
<input v-model="form.emergencyPhone" placeholder="请输入紧急联系人电话" class="form-input"
type="number" :disabled="isDetailMode"></input>
</view>
</view>
</view>
<!-- 4. 家长信息标签页 -->
<view v-show="activeTab === 3" class="tab-panel">
<view class="form-card">
<view class="card-title card-parent">家长意见及联系方式</view>
<view class="form-item">
<label class="form-label">家长意见</label>
<!-- 详情模式下禁用选择器 -->
<picker mode="selector" :range="parentOpinionOptions" :range-key="'text'"
v-model="form.parentOpinion" @change="handleParentOpinionChange" :disabled="isDetailMode">
<view class="picker-input">
{{ form.parentOpinion ? getParentOpinionText(form.parentOpinion) : '请选择家长意见' }}
</view>
</picker>
</view>
<view class="form-item">
<label class="form-label">家长签字附件</label>
<!-- 详情模式下禁用图片上传 -->
<view class="upload-btn" @click="handleUpload" v-if="!isDetailMode">
<text class="upload-icon">+</text>
<text>上传图片</text>
</view>
<view class="upload-preview" v-if="form.parentSignAttachment">
<image :src="baseUrl + form.parentSignAttachment" mode="aspectFill" />
</view>
<view class="upload-tip">只能上传jpg/png文件且不超过2M</view>
</view>
<view class="form-item">
<label class="form-label">家长联系电话</label>
<input v-model="form.parentPhone" placeholder="请输入家长联系电话" class="form-input"
type="number" :disabled="isDetailMode"></input>
</view>
<!-- 家长地址省市区选择器 -->
<view class="form-item">
<label class="form-label">家长通讯地区</label>
<!-- 详情模式下禁用省市区选择器 -->
<view class="area-picker" @click="!isDetailMode && (showParentAreaPicker = true)" :class="{disabled: isDetailMode}">
<text class="picker-text">{{ parentAreaText || '请选择省/市/区' }}</text>
<uni-icons type="arrowright" size="16" color="#999" class="picker-icon"></uni-icons>
</view>
</view>
<view class="form-item">
<label class="form-label">家长详细地址</label>
<input v-model="form.parentDetailAddress" placeholder="请输入家长详细通讯地址"
class="form-input" :disabled="isDetailMode"></input>
</view>
</view>
</view>
<!-- 5. 本人承诺标签页 -->
<view v-show="activeTab === 4" class="tab-panel">
<view class="form-card">
<view class="card-title">本人承诺</view>
<view class="form-item">
<label class="form-label">承诺内容</label>
<view class="promise-content">
<text>1.自觉遵守国家法律法规</text>
<text>2.自觉遵守学生行为规范和学校的规章制度遵守社会公德</text>
<text>3.自觉遵守外宿住址所在社区的有关管理规定</text>
<text>4.本人申请外宿属个人自愿行为外宿期间发生的一切事故造成本人他人或集体的人身财产损害的学校不负责任</text>
</view>
</view>
<view class="form-item">
<label class="form-label">承诺签名</label>
<view class="signature-container">
<!-- 详情模式下隐藏签名按钮 -->
<button size="mini" type="primary" @click="!isDetailMode && openSignature('promise')"
class="sign-btn" v-if="!isDetailMode">
签署承诺签名
</button>
<image v-if="form.studentPromiseSign" :src="baseUrl + form.studentPromiseSign"
class="signature-preview" mode="widthFix"
@click="previewImage(baseUrl + form.studentPromiseSign)"></image>
<view class="date-item">
<label class="form-label">签署日期</label>
<!-- 详情模式下禁用日期选择器 -->
<picker mode="date" :value="form.promiseDate" @change="handlePromiseDateChange" :disabled="isDetailMode">
<view class="picker-input">
{{ form.promiseDate || '请选择签署日期' }}
</view>
</picker>
</view>
</view>
</view>
</view>
</view>
<!-- 提交按钮区域 -->
<!-- 详情模式下隐藏所有操作按钮 -->
<view class="submit-container" v-if="!isDetailMode">
<button type="primary" @click="submitForm(0)" class="submit-btn">
<uni-icons type="folder" size="16"></uni-icons> 保存
</button>
<button type="primary" @click="submitForm(1)" class="submit-btn submit-primary"
v-if="form.status == 0">
<uni-icons type="checkmark" size="16"></uni-icons> 提交申请
</button>
<button type="default" @click="resetForm" class="reset-btn" v-if="!currentId">
<uni-icons type="refresh" size="16"></uni-icons> 重置
</button>
</view>
</view>
</scroll-view>
</view>
<!-- 申请须知弹窗5秒自动关闭 -->
<!-- 详情模式下不显示该弹窗 -->
<transition name="popup-fade" v-if="!isDetailMode">
<view class="mask" v-if="showNoticePopup" @click.stop="() => {}"></view>
</transition>
<transition name="popup-slide" v-if="!isDetailMode">
<view class="notice-popup" v-if="showNoticePopup">
<view class="popup-header">
<text class="popup-title">申请须知</text>
<text class="countdown" v-if="countdown > 0">剩余 {{countdown}} </text>
</view>
<view class="popup-content">
<view class="notice-item">1. 仅限南宁市学生申请外宿需完整填写所有必填项并上传对应附件</view>
<view class="notice-item">2. 外宿原因需上传佐证材料因病提供病例外宿居所提供住房证明等</view>
<view class="notice-item">3. 家长意见需上传签字扫描件本人承诺需完成电子签名</view>
<view class="notice-item">4. 审批通过后需经学院领导签署意见方可办理退房手续</view>
</view>
<view class="popup-footer">
<button type="primary" @click="closeNoticePopup" :disabled="countdown > 0" class="popup-btn">
我已阅读并知晓
</button>
</view>
</view>
</transition>
<!-- 通用签名弹窗使用 uni-app 内置绘图 API -->
<!-- 详情模式下不显示签名弹窗 -->
<view class="mask" v-if="!isDetailMode && showSignPopup" @click="closeSignPopup"></view>
<view class="sign-popup" v-if="!isDetailMode && showSignPopup">
<view class="sign-popup-header">
<text class="sign-title">{{ currentSignType === 'student' ? '电子签名' : '承诺签名' }}</text>
<uni-icons type="close" size="20" @click="closeSignPopup" class="close-icon"></uni-icons>
</view>
<!-- 扩大的签名画布 -->
<canvas class="sign-canvas" canvas-id="commonSignCanvas" @touchstart="handleSignStart"
@touchmove="handleSignMove" @touchend="handleSignEnd" @mousedown="handleSignStart"
@mousemove="handleSignMove" @mouseup="handleSignEnd" @mouseleave="handleSignEnd"></canvas>
<view class="sign-popup-footer">
<button size="mini" type="default" @click="clearSign" class="sign-btn-default">清除</button>
<button size="mini" type="primary" @click="saveSign" class="sign-btn-primary">确认</button>
</view>
</view>
<!-- 外宿地址省市区选择器弹窗 -->
<!-- 详情模式下不显示省市区选择器弹窗 -->
<view class="mask" v-if="!isDetailMode && showAreaPicker" @click="showAreaPicker = false"></view>
<view class="area-popup" v-if="!isDetailMode && showAreaPicker">
<view class="area-popup-header">
<button size="mini" type="default" @click="showAreaPicker = false" class="btn-cancel">取消</button>
<button size="mini" type="primary" @click="confirmArea" class="btn-confirm">确认</button>
</view>
<picker-view :value="areaValue" @change="onAreaChange" class="picker-view" indicator-style="height: 50px;">
<picker-view-column>
<view class="picker-item" v-for="(item, index) in provinces" :key="index">{{ item.value }}</view>
</picker-view-column>
<picker-view-column>
<view class="picker-item" v-for="(item, index) in cities" :key="index">{{ item.value }}</view>
</picker-view-column>
<picker-view-column>
<view class="picker-item" v-for="(item, index) in areas" :key="index">{{ item.value }}</view>
</picker-view-column>
</picker-view>
</view>
<!-- 家长地址省市区选择器弹窗 -->
<!-- 详情模式下不显示省市区选择器弹窗 -->
<view class="mask" v-if="!isDetailMode && showParentAreaPicker" @click="showParentAreaPicker = false"></view>
<view class="area-popup" v-if="!isDetailMode && showParentAreaPicker">
<view class="area-popup-header">
<button size="mini" type="default" @click="showParentAreaPicker = false" class="btn-cancel">取消</button>
<button size="mini" type="primary" @click="confirmParentArea" class="btn-confirm">确认</button>
</view>
<picker-view :value="parentAreaValue" @change="onParentAreaChange" class="picker-view"
indicator-style="height: 50px;">
<picker-view-column>
<view class="picker-item" v-for="(item, index) in provinces" :key="index">{{ item.value }}</view>
</picker-view-column>
<picker-view-column>
<view class="picker-item" v-for="(item, index) in parentCities" :key="index">{{ item.value }}</view>
</picker-view-column>
<picker-view-column>
<view class="picker-item" v-for="(item, index) in parentAreas" :key="index">{{ item.value }}</view>
</picker-view-column>
</picker-view>
</view>
</view>
</template>
<script>
import {
getOwnInfo,
listStudent,
getOutsideAccommodationApply,
updateOutsideAccommodationApply,
addOutsideAccommodationApply,
getOwnLog
} from '@/api/dms/outsideAccommodation/outsideAccommodationApply'
import {
batchAddOutsideAccommodationAttachment,
deleteOutsideAccommodationAttachmentNameAndStuName
} from "@/api/dms/outsideAccommodation/outsideAccommodationAttachment";
import {
getUserProfile
} from '@/api/system/user'
import area from '@/components/complex-picker-region/area.js'
import config from '@/config'
import {
checkPic
} from "@/utils/checkPic.js";
import uploadFile from "@/plugins/upload.js";
import {
getAffixItems, deleteAffix
} from "@/api/affix.js";
export default {
name: 'OutsideAccommodationApply',
data() {
return {
// 新增:是否为详情查看模式(核心控制变量)
isDetailMode: false,
// 激活的标签页索引
activeTab: 0,
// 表单滚动高度
formScrollHeight: '70vh',
// 申请须知弹窗控制
showNoticePopup: false,
countdown: 5,
// 通用签名弹窗控制
showSignPopup: false,
currentSignType: '', // student / promise
signCtx: null, // 签名绘图上下文
isSigning: false, // 是否正在签名
hasSigned: false, // 是否有签名内容
canvasWidth: 680, // 画布宽度扩大rpx转px
canvasHeight: 300, // 画布高度扩大rpx转px
// 省市区选择器控制
showAreaPicker: false,
showParentAreaPicker: false,
provinces: [],
cities: [],
areas: [],
parentCities: [],
parentAreas: [],
areaValue: [0, 0, 0],
parentAreaValue: [0, 0, 0],
areaText: '',
parentAreaText: '',
// 表单数据
form: {
originalDormitory: '',
studentName: '',
gender: '',
birthDate: '',
majorName: '',
className: '',
studentNo: '',
deptName: '',
teacherName: '',
idCard: '',
studentPhone: '',
accommodationFeeStatus: 1,
applyReason: '',
address: '', // 省市区拼接结果(格式:北京市/北京市/东城区)
outsideAddress: '',
emergencyContact: '',
emergencyPhone: '',
parentOpinion: 1, // 默认同意
parentPhone: '',
parentAddress: '', // 家长省市区拼接结果
parentDetailAddress: '',
studentSignature: '',
studentPromiseSign: '',
promiseDate: '',
parentSignAttachment: '',
accommodationFee: '',
affixId: '',
status: 0,
id: '',
applyNo: '',
endDate: '',
promiseContent: ''
},
// 下拉选项
genderOptions: [{
text: '男',
value: '1'
},
{
text: '女',
value: '0'
}
],
parentOpinionOptions: [{
text: '同意外宿',
value: 1
},
{
text: '不同意外宿',
value: 0
}
],
// 文件上传相关
affixFiles: [],
parentSignFiles: "",
// 页面状态
currentId: null,
loading: false,
roleGroup: null,
// 基础配置
baseUrl: config.baseUrl || '',
uploadImgUrl: (config.baseUrl || '') + '/common/upload'
}
},
onLoad(options) {
// 新增:判断是否为详情模式(核心逻辑)
this.isDetailMode = options.type === 'detail';
// 1. 初始化省市区数据
this.initAreaData();
// 2. 初始化页面高度
this.initPageHeight();
// 3. 加载表单数据
if (options.id) {
this.currentId = options.id;
this.loadFormData(options.id);
} else {
this.getUser();
}
},
onReady() {
// 新增:详情模式下不打开申请须知弹窗
if (!this.isDetailMode) {
// 1. 打开申请须知弹窗
this.openNoticePopup();
}
},
methods: {
// ========== 省市区选择器核心方法 ==========
initAreaData() {
this.provinces = area || [];
if (this.provinces.length > 0) {
this.cities = this.provinces[0].children || [];
this.parentCities = this.provinces[0].children || [];
if (this.cities.length > 0) {
this.areas = this.cities[0].children || [];
this.parentAreas = this.cities[0].children || [];
}
}
},
onAreaChange(e) {
const val = e.detail.value;
this.areaValue = val;
if (this.provinces[val[0]]) {
this.cities = this.provinces[val[0]].children || [];
if (this.cities[val[1]]) {
this.areas = this.cities[val[1]].children || [];
}
}
},
onParentAreaChange(e) {
const val = e.detail.value;
this.parentAreaValue = val;
if (this.provinces[val[0]]) {
this.parentCities = this.provinces[val[0]].children || [];
if (this.parentCities[val[1]]) {
this.parentAreas = this.parentCities[val[1]].children || [];
}
}
},
confirmArea() {
const [pIdx, cIdx, aIdx] = this.areaValue;
const province = this.provinces[pIdx]?.value || '';
const city = this.cities[cIdx]?.value || '';
const area = this.areas[aIdx]?.value || '';
this.areaText = `${province}/${city}/${area}`;
this.form.address = this.areaText;
this.showAreaPicker = false;
},
confirmParentArea() {
const [pIdx, cIdx, aIdx] = this.parentAreaValue;
const province = this.provinces[pIdx]?.value || '';
const city = this.parentCities[cIdx]?.value || '';
const area = this.parentAreas[aIdx]?.value || '';
this.parentAreaText = `${province}/${city}/${area}`;
this.form.parentAddress = this.parentAreaText;
this.showParentAreaPicker = false;
},
// ========== 表单基础方法 ==========
getGenderText(value) {
const item = this.genderOptions.find(item => item.value === value);
return item ? item.text : '';
},
getParentOpinionText(value) {
const item = this.parentOpinionOptions.find(item => item.value === value);
return item ? item.text : '';
},
handleGenderChange(e) {
this.form.gender = e.detail.value;
},
handleBirthDateChange(e) {
this.form.birthDate = e.detail.value;
},
handleParentOpinionChange(e) {
this.form.parentOpinion = e.detail.value;
},
handlePromiseDateChange(e) {
this.form.promiseDate = e.detail.value;
},
initPageHeight() {
try {
const systemInfo = uni.getSystemInfoSync();
const navHeight = systemInfo.statusBarHeight + 44;
const tabsHeight = 80;
const submitHeight = 100;
const totalFixedHeight = (navHeight * 2) + (tabsHeight + submitHeight) / 2;
this.formScrollHeight = 'calc(100vh - ' + totalFixedHeight + 'px)';
// 计算画布实际像素尺寸rpx转px - 扩大后的尺寸
const pxRatio = systemInfo.windowWidth / 750; // 750rpx = 屏幕宽度
this.canvasWidth = 645 * pxRatio; // 扩大签名画布宽度
this.canvasHeight = 300 * pxRatio; // 扩大签名画布高度
} catch (e) {
this.formScrollHeight = '80vh';
this.canvasWidth = 340; // 默认值扩大
this.canvasHeight = 150; // 默认值扩大
}
},
openNoticePopup() {
this.showNoticePopup = true;
this.startCountdown();
},
startCountdown() {
this.countdown = 5;
const timer = setInterval(() => {
this.countdown--;
if (this.countdown <= 0) {
clearInterval(timer);
}
}, 1000);
},
closeNoticePopup() {
this.showNoticePopup = false;
this.countdown = 0;
},
switchTab(index) {
this.activeTab = index;
uni.pageScrollTo({
scrollTop: 0,
duration: 0
});
},
// ========== 通用签名功能(修复无法签名+虚线问题) ==========
// 初始化签名画布(兼容所有端)
initSignCanvas() {
try {
// 获取绘图上下文uni-app 兼容所有端的方式)
this.signCtx = uni.createCanvasContext('commonSignCanvas', this);
// 设置画笔样式(关键:调整线条参数确保实线效果)
this.signCtx.setStrokeStyle('#000000');
this.signCtx.setLineWidth(3); // 线条宽度
this.signCtx.setLineCap('round'); // 线条端点圆润
this.signCtx.setLineJoin('round'); // 线条拐角圆润
this.signCtx.setMiterLimit(1); // 限制斜接长度
// 清空画布
this.clearSign();
console.log('签名画布初始化成功');
} catch (e) {
console.error('初始化签名画布失败:', e);
uni.showToast({
title: '签名画布初始化失败',
icon: 'none'
});
}
},
// 打开签名弹窗
openSignature(type) {
this.currentSignType = type;
this.showSignPopup = true;
this.hasSigned = false;
this.isSigning = false;
// 延迟初始化画布,确保 DOM 渲染完成
setTimeout(() => {
this.initSignCanvas();
}, 300);
},
// 关闭签名弹窗
closeSignPopup() {
this.showSignPopup = false;
this.currentSignType = '';
this.isSigning = false;
},
// 开始签名(修复核心:简化坐标计算)
handleSignStart(e) {
if (!this.signCtx) return;
try {
this.isSigning = true;
this.hasSigned = true;
// 获取触摸/鼠标位置
const touch = e.type.includes('touch') ? e.touches[0] : e;
// 直接使用页面坐标(更稳定)
const x = touch.x || touch.clientX;
const y = touch.y || touch.clientY;
// 开始新路径
this.signCtx.beginPath();
this.signCtx.moveTo(x, y);
} catch (e) {
console.error('开始签名失败:', e);
}
},
// 签名绘制(修复核心:增量绘制+实时stroke
handleSignMove(e) {
if (!this.isSigning || !this.signCtx) return;
try {
// 获取触摸/鼠标位置
const touch = e.type.includes('touch') ? e.touches[0] : e;
const x = touch.x || touch.clientX;
const y = touch.y || touch.clientY;
// 绘制线条(增量方式,保证实线)
this.signCtx.lineTo(x, y);
this.signCtx.stroke(); // 实时绘制,确保线条连续
this.signCtx.draw(true); // 保留之前的绘制内容
// 重新开始路径,避免重复绘制
this.signCtx.beginPath();
this.signCtx.moveTo(x, y);
} catch (e) {
console.error('签名绘制失败:', e);
}
},
// 结束签名
handleSignEnd() {
this.isSigning = false;
// 最后一次绘制,确保所有线条都显示
if (this.signCtx) {
this.signCtx.stroke();
this.signCtx.draw(true);
}
},
// 清空签名(简化版,确保可用)
clearSign() {
if (!this.signCtx) return;
try {
// 清空画布 - 使用扩大后的尺寸
this.signCtx.clearRect(0, 0, this.canvasWidth, this.canvasHeight);
this.signCtx.fillStyle = '#ffffff';
this.signCtx.fillRect(0, 0, this.canvasWidth, this.canvasHeight);
this.signCtx.draw(true);
this.hasSigned = false;
} catch (e) {
console.error('清空签名失败:', e);
// 降级方案
uni.createCanvasContext('commonSignCanvas', this).draw(true);
this.hasSigned = false;
}
},
// 保存签名(转换为图片并上传)
saveSign() {
if (!this.hasSigned) {
uni.showToast({
title: '请先完成签名',
icon: 'none'
});
return;
}
this.loading = true;
// 把画布内容转换为临时文件
uni.canvasToTempFilePath({
canvasId: 'commonSignCanvas',
quality: 1.0,
success: (res) => {
// 上传签名图片
this.uploadSignImage(res.tempFilePath);
},
fail: (err) => {
console.error('转换签名为图片失败:', err);
uni.showToast({
title: '签名保存失败',
icon: 'none'
});
this.loading = false;
}
}, this);
},
// 上传签名图片
uploadSignImage(filePath) {
uni.uploadFile({
url: this.uploadImgUrl,
filePath: filePath,
name: 'file',
formData: {
type: this.currentSignType + '_signature',
timestamp: new Date().getTime()
},
success: (res) => {
try {
const data = JSON.parse(res.data);
if (data.code === 200) {
// 保存签名地址到表单
if (this.currentSignType === 'student') {
this.form.studentSignature = data.fileName;
} else {
this.form.studentPromiseSign = data.fileName;
}
uni.showToast({
title: '签名保存成功',
icon: 'success'
});
this.closeSignPopup();
} else {
uni.showToast({
title: '签名上传失败: ' + data.msg,
icon: 'none'
});
}
} catch (e) {
console.error('解析签名上传结果失败:', e);
uni.showToast({
title: '签名上传失败',
icon: 'none'
});
}
},
fail: (err) => {
console.error('上传签名失败:', err);
uni.showToast({
title: '签名上传失败',
icon: 'none'
});
},
complete: () => {
this.loading = false;
}
});
},
// ========== 文件上传相关 ==========
chooseAffixFile() {
// 1. 定义affixId生成工具函数确保uuid唯一性
const generateUUID = () => {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
const r = Math.random() * 16 | 0;
const v = c === 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
};
uni.chooseFile({
count: 10, // 最多选择10个文件
// extension: ['.jpg', '.png', '.pdf'], // 可根据需求放开文件类型限制
success: async (chooseRes) => {
// 2. 初始化affixId如果为空则生成唯一ID
if (!this.form.affixId) {
this.form.affixId = generateUUID();
}
// 3. 遍历选择的文件,逐个上传(支持多文件)
for (const file of chooseRes.tempFiles) {
try {
// 4. 构造上传参数(放在循环内,确保每个文件参数正确)
const formDataObj = {
affixId: this.form.affixId,
fileName: file.name,
fileSize: file.size
};
// 5. 上传文件await确保上传完成后再处理下一步
const uploadRes = await uploadFile('/affix/upload', file.path, formDataObj);
const result = typeof uploadRes === 'string' ? JSON.parse(uploadRes) :
uploadRes;
// 6. 上传结果校验
if (result && result.code === 200) {
// 7. 构造文件信息对象(去重逻辑优化)
const fileInfo = {
applyId: this.form.id || '',
attachmentName: file.name,
attachmentUrl: result.savePath, // 本地路径
serverUrl: result.savePath || '', // 服务器返回的文件路径
fileId: result.id || '', // 服务器返回的文件ID
fileSize: file.size,
fileSuffix: file.name.split('.').pop().toLowerCase(),
studentName: this.form.studentName,
studentNo: this.form.studentNo,
uploadStatus: 'success' // 上传状态标记
};
// 8. 去重逻辑(优化:通过文件名+大小双重校验,避免路径重复问题)
const isDuplicate = this.affixFiles.some(item =>
item.attachmentName === file.name && item.fileSize === file.size
);
if (!isDuplicate) {
this.affixFiles.push(fileInfo);
}
console.log(this.affixFiles);
uni.showToast({
title: `文件 ${file.name} 上传成功`,
icon: 'success',
duration: 1500
});
} else {
// 上传失败处理
uni.showToast({
title: `文件 ${file.name} 上传失败:${result.message || '未知错误'}`,
icon: 'none',
duration: 2000
});
}
} catch (error) {
// 9. 异常捕获(网络错误/上传接口异常)
console.error(`文件 ${file.name} 上传异常:`, error);
uni.showToast({
title: `文件 ${file.name} 上传异常,请重试`,
icon: 'none',
duration: 2000
});
}
}
},
// 10. 取消选择文件的处理
fail: (err) => {
console.error('选择文件失败:', err);
uni.showToast({
title: '选择文件失败,请重试',
icon: 'none'
});
}
});
},
deleteAffixFile(index) {
const deletedFile = this.affixFiles[index];
this.affixFiles.splice(index, 1);
// this.form.affixId = this.affixFiles.length ? 'uploaded' : '';
let fileId = deletedFile.id || deletedFile.fileId
deleteAffix(fileId).then(() => {
deleteOutsideAccommodationAttachmentNameAndStuName({
attachmentName: deletedFile.trueName || deletedFile.attachmentName,
studentName: this.form.studentName
}).then(() => {
uni.showToast({
title: '删除成功',
icon: 'success'
});
}).catch(() => {
uni.showToast({
title: '删除失败',
icon: 'none'
});
});
})
},
// 家长附件上传
handleUpload() {
uni.chooseImage({
count: 3,
success: async (img) => {
let bool = await checkPic(img.tempFiles[0]);
if (bool) {
uploadFile('/common/upload', img.tempFilePaths[0]).then((res) => {
// if (this.form.photo !== "") {
// this.form.photo = this.form.photo + "," + JSON.parse(res)
// .fileName;
// } else {
// this.form.photo = JSON.parse(res).fileName;
// }
this.parentSignFiles = img.tempFilePaths[0];
this.form.parentSignAttachment = JSON.parse(res).fileName;
})
}
}
})
},
previewImage(url) {
uni.previewImage({
urls: [url],
current: url
});
},
// ========== 表单数据加载与提交 ==========
loadFormData(id) {
this.loading = true;
getOutsideAccommodationApply(id).then(res => {
this.form = {
...res.data,
address: res.data.address || '',
parentAddress: res.data.parentAddress || '',
parentOpinion: res.data.parentOpinion || '1'
};
this.areaText = res.data.address || '';
this.parentAreaText = res.data.parentAddress || '';
// if (this.form.parentSignAttachment) {
// this.parentSignFiles = [{
// name: this.form.parentSignAttachment.split('/').pop(),
// url: this.baseUrl + this.form.parentSignAttachment
// }];
// }
this.getAffixs(this.form.affixId)
this.loading = false;
}).catch(() => {
this.loading = false;
uni.showToast({
title: '加载数据失败',
icon: 'none'
});
});
},
getUser() {
this.loading = true;
getUserProfile().then((response) => {
this.roleGroup = response.roleGroup;
if (response.data) {
this.form.studentName = response.data.nickName;
this.form.gender = response.data.sex || '';
getOwnInfo().then((res) => {
if (res.data) {
this.form.studentId = res.data.stuId;
this.form.studentNo = res.data.stuNo;
this.form.majorName = res.data.majorName;
this.form.deptName = res.data.deptName
this.form.className = res.data.className;
this.form.idCard = res.data.idCard;
this.form.teacherName = res.data.teacherName
this.form.studentPhone = res.data.stuPhone;
this.form.birthDate = res.data.birthday || '';
getOwnLog().then(response => {
try {
const currentDormId = response.data.dormStu.dormitoryId;
const matchedRecord = response.data.record.find(item =>
item.roomId === currentDormId);
if (matchedRecord) {
const baseStuYearName = matchedRecord.stuYearName ||
'';
if (baseStuYearName) {
const isAllSameYear = response.data.record.every(
item => (item.stuYearName || '') ===
baseStuYearName);
if (isAllSameYear) {
const totalMoney = response.data.record.reduce(
(sum, item) => {
return sum + Number(item
.needMoney || 0);
}, 0);
this.form.accommodationFee = '已交' +
baseStuYearName + '年度住宿费' + totalMoney +
'人民币';
} else {
this.form.accommodationFee = '已交' +
baseStuYearName + '年度住宿费 存在不同年度费用数据';
}
} else {
this.form.accommodationFee = '已交未知年度住宿费 暂无有效学年信息';
}
} else {
const firstStuYearName = response.data.record[0]
?.stuYearName || '';
this.form.accommodationFee = '已交' + (
firstStuYearName || '未知') + '年度住宿费 暂无匹配数据';
}
} catch (e) {
this.form.accommodationFee = '获取缴费信息失败';
}
}).catch(() => {
this.form.accommodationFee = '获取缴费信息失败';
});
this.getStuDom();
}
this.loading = false;
}).catch(() => {
this.loading = false;
uni.showToast({
title: '获取学生信息失败',
icon: 'none'
});
});
}
}).catch(() => {
this.loading = false;
uni.showToast({
title: '获取用户信息失败',
icon: 'none'
});
});
},
// 获取学生宿舍
getStuDom() {
listStudent({
stuNo: this.form.studentNo,
stuName: this.form.studentName
}).then((response) => {
if (response.rows && response.rows[0]) {
this.form.originalDormitory =
response.rows[0].campusName +
' ' +
response.rows[0].parkName +
' ' +
response.rows[0].buildingName +
response.rows[0].roomNo;
}
}).catch(() => {
uni.showToast({
title: '获取宿舍信息失败',
icon: 'none'
});
});
},
// 获取上传的附件数据
getAffixs(affix) {
getAffixItems({
affixId: affix
}).then(file => {
this.affixFiles = file.data;
})
},
submitForm(status) {
// 定义英文字段名和中文提示的映射关系
const fieldMap = {
'originalDormitory': '原宿舍号',
'studentName': '姓名',
'gender': '性别',
'birthDate': '出生日期',
'majorName': '专业',
'className': '班级',
'studentNo': '学号',
'idCard': '身份证',
'studentPhone': '手机号码',
'applyReason': '外宿原因',
'address': '外宿地址省市区',
'outsideAddress': '详细门牌号',
'emergencyContact': '紧急联系人',
'emergencyPhone': '紧急联系电话',
'parentOpinion': '家长意见',
'parentPhone': '家长联系电话',
'parentAddress': '家长地址省市区',
'parentDetailAddress': '家长地址详细',
'promiseDate': '签署日期'
};
// 提取所有需要验证的英文字段名
const requiredFields = Object.keys(fieldMap);
// 筛选出为空的英文字段
const emptyFields = requiredFields.filter(field => !this.form[field]);
if (emptyFields.length) {
// 根据空的英文字段,找到对应的中文提示
const emptyFieldCn = fieldMap[emptyFields[0]];
uni.showToast({
title: '请填写' + emptyFieldCn,
icon: 'none'
});
return;
}
if (!this.form.studentSignature) {
uni.showToast({
title: '请完成电子签名',
icon: 'none'
});
return;
}
if (!this.form.studentPromiseSign) {
uni.showToast({
title: '请完成承诺签名',
icon: 'none'
});
return;
}
if (!this.form.affixId) {
uni.showToast({
title: '请上传佐证材料',
icon: 'none'
});
return;
}
if (!this.form.parentSignAttachment) {
uni.showToast({
title: '请上传家长签字附件',
icon: 'none'
});
return;
}
if (!this.form.applyNo) {
const year = new Date().getFullYear();
const randomNo = Math.floor(Math.random() * 1000000).toString().padStart(6, '0');
this.form.applyNo = 'WS' + year + randomNo;
}
this.form.endDate = this.getOutsideDefaultEndTime();
this.form.promiseContent = `
<p>1.自觉遵守国家法律、法规;</p>
<p>2.自觉遵守学生行为规范和学校的规章制度,遵守社会公德;</p>
<p>3.自觉遵守外宿住址所在社区的有关管理规定;</p>
<p>4.本人申请外宿,属个人自愿行为,外宿期间发生的一切事故,造成本人、他人或集体的人身、财产损害的,学校不负责任。</p>
`
const submitForm = {
...this.form,
status: this.form.status != 0 ? this.form.status : status
};
this.loading = true;
const requestPromise = this.form.id ?
updateOutsideAccommodationApply(submitForm) :
addOutsideAccommodationApply(submitForm);
requestPromise.then((response) => {
if (this.affixFiles.length > 0) {
this.affixFiles.forEach(item => {
item.applyId = this.form.id || response.data.id;
});
batchAddOutsideAccommodationAttachment(this.affixFiles);
}
uni.showToast({
title: this.form.id ? '修改成功' : '新增成功',
icon: 'success'
});
setTimeout(() => {
this.goBack();
}, 800);
}).catch(() => {
uni.showToast({
title: '提交失败',
icon: 'none'
});
}).finally(() => {
this.loading = false;
});
},
resetForm() {
try {
this.form = {
originalDormitory: '',
studentName: '',
gender: '',
birthDate: '',
majorName: '',
className: '',
studentNo: '',
idCard: '',
studentPhone: '',
accommodationFeeStatus: 1,
applyReason: '',
address: '',
outsideAddress: '',
emergencyContact: '',
emergencyPhone: '',
parentOpinion: '1',
parentPhone: '',
parentAddress: '',
parentDetailAddress: '',
studentSignature: '',
studentPromiseSign: '',
promiseDate: '',
parentSignAttachment: '',
accommodationFee: '',
affixId: '',
status: 0
};
this.affixFiles = [];
this.parentSignFiles = "";
this.areaText = '';
this.parentAreaText = '';
this.activeTab = 0;
uni.showToast({
title: '表单已重置',
icon: 'success'
});
} catch (e) {
console.warn('重置表单失败:', e);
uni.showToast({
title: '重置失败',
icon: 'none'
});
}
},
goBack() {
uni.navigateBack({
delta: 1
});
},
getOutsideDefaultEndTime() {
const now = new Date();
const nextYear = now.getFullYear() + 1;
const endDate = new Date(nextYear, 7, 31);
return this.formatDate(endDate);
},
formatDate(date) {
const year = date.getFullYear();
const month = (date.getMonth() + 1).toString().padStart(2, '0');
const day = date.getDate().toString().padStart(2, '0');
return year + '-' + month + '-' + day;
}
}
}
</script>
<style scoped>
/* 核心容器样式 */
.app-container {
background-color: #f8f9fa;
min-height: 100vh;
width: 100%;
box-sizing: border-box;
}
/* 自定义导航栏 */
.nav-bar {
height: 44px;
line-height: 44px;
background: linear-gradient(135deg, #409EFF 0%, #53a8ff 100%);
color: #fff;
padding: 0 15px;
display: flex;
align-items: center;
box-shadow: 0 2px 10px rgba(64, 158, 255, 0.1);
}
.nav-title {
font-size: 18px;
font-weight: 600;
margin-left: 10px;
}
.main-container {
width: 100%;
height: calc(100vh - 44px);
display: flex;
flex-direction: column;
}
/* 原生选项卡样式 */
.tabs-container {
width: 100%;
background-color: #fff;
border-bottom: 1px solid #e8eaec;
}
.tabs-header {
display: flex;
overflow-x: auto;
white-space: nowrap;
padding: 0 5rpx;
justify-content: space-around;
}
.tab-item {
display: flex;
align-items: center;
justify-content: center;
padding: 0 15rpx;
height: 88rpx;
min-width: 120rpx;
flex-direction: column;
position: relative;
transition: all 0.3s ease;
}
.tab-item.active {
color: #409EFF;
}
.tab-item.active::after {
content: '';
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 120rpx;
height: 6rpx;
background-color: #409EFF;
border-radius: 3rpx;
}
.tab-text {
font-size: 24rpx;
margin-top: 8rpx;
font-weight: 500;
}
/* 表单滚动区域 */
.form-scroll {
flex: 1;
width: 100%;
padding: 15rpx;
box-sizing: border-box;
overflow-y: auto;
}
/* 标签页面板 */
.tab-panel {
width: 100%;
margin-bottom: 15rpx;
}
/* 表单卡片 */
.form-card {
background-color: #fff;
border-radius: 12rpx;
padding: 25rpx;
margin-bottom: 20rpx;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.03);
border: 1px solid #f0f2f5;
}
.card-title {
font-size: 32rpx;
font-weight: 600;
margin-bottom: 25rpx;
color: #1f2937;
padding-bottom: 15rpx;
border-bottom: 1px solid #f0f2f5;
position: relative;
}
.card-title::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
width: 130rpx;
height: 4rpx;
background-color: #409EFF;
border-radius: 2rpx;
}
.card-parent::after {
width: 290rpx;
}
.card-area::after {
width: 200rpx;
}
.card-apply::after {
width: 230rpx;
}
/* 表单项 */
.form-item {
margin-bottom: 25rpx;
display: flex;
flex-direction: column;
}
.form-label {
font-size: 28rpx;
color: #374151;
margin-bottom: 12rpx;
font-weight: 500;
}
.form-input {
height: 80rpx;
line-height: 80rpx;
border: 1px solid #e5e7eb;
border-radius: 8rpx;
padding: 0 20rpx;
font-size: 28rpx;
transition: border-color 0.3s ease;
}
.form-input:focus {
border-color: #409EFF;
box-shadow: 0 0 0 4rpx rgba(64, 158, 255, 0.1);
}
.form-textarea {
border: 1px solid #e5e7eb;
border-radius: 8rpx;
padding: 20rpx;
font-size: 28rpx;
line-height: 1.8;
transition: border-color 0.3s ease;
min-height: 200rpx;
width: 100%;
}
.form-textarea:focus {
border-color: #409EFF;
box-shadow: 0 0 0 4rpx rgba(64, 158, 255, 0.1);
}
// 图片上传样式
.upload-btn {
width: 200rpx;
height: 80rpx;
border: 1px dashed #ccc;
border-radius: 8rpx;
display: flex;
align-items: center;
justify-content: center;
gap: 10rpx;
font-size: 28rpx;
color: #666;
margin-bottom: 20rpx;
}
.upload-icon {
font-size: 40rpx;
font-weight: bold;
}
.upload-tip {
font-size: 22rpx;
color: #999;
margin-bottom: 20rpx;
}
.upload-preview {
width: 200rpx;
height: 200rpx;
border-radius: 8rpx;
overflow: hidden;
}
.upload-preview image {
width: 100%;
height: 100%;
}
/* 选择器样式 */
.picker-input {
height: 80rpx;
line-height: 80rpx;
border: 1px solid #e5e7eb;
border-radius: 8rpx;
padding: 0 20rpx;
font-size: 28rpx;
color: #4b5563;
transition: border-color 0.3s ease;
}
.picker-input:active {
border-color: #409EFF;
}
/* 省市区选择器样式 */
.area-picker {
height: 80rpx;
line-height: 80rpx;
border: 1px solid #e5e7eb;
border-radius: 8rpx;
padding: 0 20rpx;
font-size: 28rpx;
color: #4b5563;
display: flex;
justify-content: space-between;
align-items: center;
transition: border-color 0.3s ease;
}
.area-picker:active {
border-color: #409EFF;
}
/* 新增:禁用状态样式(可选) */
.area-picker.disabled {
opacity: 0.7;
pointer-events: none;
}
.picker-text {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.picker-icon {
flex-shrink: 0;
color: #9ca3af;
}
/* 省市区选择器弹窗样式 */
.area-popup {
position: fixed;
bottom: 0;
left: 0;
width: 100%;
background-color: #fff;
z-index: 1001;
border-radius: 16rpx 16rpx 0 0;
box-shadow: 0 -4rpx 20rpx rgba(0, 0, 0, 0.1);
}
.area-popup-header {
display: flex;
justify-content: space-between;
padding: 20rpx;
border-bottom: 1px solid #e5e7eb;
}
.picker-view {
height: 400rpx;
width: 100%;
}
.picker-item {
height: 50px;
line-height: 50px;
text-align: center;
font-size: 28rpx;
color: #1f2937;
}
/* 文件列表 */
.file-list {
margin-top: 15rpx;
}
.file-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 15rpx;
border: 1px solid #e5e7eb;
border-radius: 8rpx;
margin-bottom: 10rpx;
background-color: #f9fafb;
}
.file-name {
font-size: 26rpx;
color: #4b5563;
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* 上传按钮样式 */
.upload-btn {
background-color: #f0f9ff;
color: #409EFF;
border: 1px solid #dbeafe;
border-radius: 8rpx;
padding: 12rpx 24rpx;
font-size: 26rpx;
}
.delete-btn {
background-color: #fef2f2;
color: #ef4444;
border: 1px solid #fee2e2;
border-radius: 6rpx;
/* padding: 8rpx 16rpx; */
/* font-size: 24rpx; */
}
/* 上传提示 */
.upload-tip {
font-size: 24rpx;
color: #9ca3af;
margin-top: 12rpx;
line-height: 1.6;
}
/* 宿费状态 */
.fee-status {
line-height: 1.8;
color: #4b5563;
font-size: 28rpx;
padding: 15rpx;
background-color: #f9fafb;
border-radius: 8rpx;
border: 1px solid #e5e7eb;
}
/* 签名容器 */
.signature-container {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 20rpx;
}
.sign-btn {
background-color: #409EFF;
color: #fff;
border-radius: 8rpx;
padding: 12rpx 24rpx;
font-size: 26rpx;
margin: 0;
}
.signature-preview {
width: 220rpx;
height: 110rpx;
border: 1px solid #e5e7eb;
border-radius: 8rpx;
padding: 5rpx;
background-color: #f9fafb;
}
/* 日期项 */
.date-item {
margin-top: 20rpx;
width: 100%;
}
/* 提交按钮区域 */
.submit-container {
display: flex;
justify-content: center;
gap: 20rpx;
margin-top: 30rpx;
padding: 25rpx;
background-color: #fff;
border-radius: 12rpx;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.03);
border: 1px solid #f0f2f5;
}
.submit-btn {
width: 220rpx;
height: 88rpx;
line-height: 88rpx;
border-radius: 8rpx;
font-size: 28rpx;
font-weight: 500;
}
.submit-primary {
background: linear-gradient(135deg, #409EFF 0%, #53a8ff 100%);
border: none;
}
.reset-btn {
width: 220rpx;
height: 88rpx;
line-height: 88rpx;
border-radius: 8rpx;
font-size: 28rpx;
background-color: #f9fafb;
color: #4b5563;
border: 1px solid #e5e7eb;
}
/* 弹窗遮罩 */
.mask {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
z-index: 999;
backdrop-filter: blur(2rpx);
}
/* 申请须知弹窗样式 */
.notice-popup {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 600rpx;
background-color: #fff;
border-radius: 16rpx;
padding: 35rpx;
z-index: 1000;
box-shadow: 0 10rpx 40rpx rgba(0, 0, 0, 0.15);
}
.popup-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 25rpx;
padding-bottom: 15rpx;
border-bottom: 1px solid #e5e7eb;
}
.popup-title {
font-size: 34rpx;
font-weight: 600;
color: #1f2937;
}
.countdown {
font-size: 24rpx;
color: #f97316;
font-weight: 500;
}
.popup-content {
max-height: 400rpx;
overflow-y: auto;
margin-bottom: 35rpx;
}
.notice-item {
line-height: 2;
margin-bottom: 15rpx;
color: #4b5563;
font-size: 28rpx;
}
.popup-btn {
width: 100%;
height: 88rpx;
line-height: 88rpx;
border-radius: 8rpx;
font-size: 28rpx;
background: linear-gradient(135deg, #409EFF 0%, #53a8ff 100%);
border: none;
}
/* 签名弹窗样式 - 扩大尺寸 */
.sign-popup {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 700rpx;
/* 弹窗宽度扩大 */
background-color: #fff;
border-radius: 16rpx;
padding: 30rpx;
z-index: 1000;
box-shadow: 0 10rpx 40rpx rgba(0, 0, 0, 0.15);
}
.sign-popup-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 25rpx;
}
.sign-title {
font-size: 34rpx;
font-weight: 600;
color: #1f2937;
}
.close-icon {
color: #6b7280;
transition: color 0.3s ease;
}
.close-icon:active {
color: #ef4444;
}
/* 签名画布样式 - 大幅扩大尺寸 */
.sign-canvas {
width: 645rpx;
/* 画布宽度大幅增加 */
height: 300rpx;
/* 画布高度大幅增加 */
border: 1px solid #d1d5db;
background-color: #fff;
touch-action: none;
cursor: crosshair;
user-select: none;
margin-bottom: 25rpx;
border-radius: 8rpx;
/* 增加点击区域响应 */
pointer-events: auto;
z-index: 1001;
}
.sign-popup-footer {
display: flex;
justify-content: flex-end;
gap: 20rpx;
}
.sign-btn-default {
padding: 12rpx 24rpx;
border-radius: 8rpx;
font-size: 28rpx;
background-color: #f9fafb;
color: #4b5563;
border: 1px solid #e5e7eb;
}
.sign-btn-primary {
padding: 12rpx 24rpx;
border-radius: 8rpx;
font-size: 28rpx;
background: linear-gradient(135deg, #409EFF 0%, #53a8ff 100%);
border: none;
color: #fff;
}
/* 承诺内容样式 */
.promise-content {
line-height: 2;
color: #4b5563;
padding: 20rpx;
background-color: #f9fafb;
border-radius: 8rpx;
font-size: 28rpx;
border: 1px solid #e5e7eb;
}
.promise-content text {
display: block;
margin-bottom: 15rpx;
}
/* 按钮通用样式优化 */
.btn-cancel {
background-color: #f9fafb;
color: #4b5563;
border: 1px solid #e5e7eb;
border-radius: 8rpx;
padding: 12rpx 24rpx;
font-size: 26rpx;
}
.btn-confirm {
background: linear-gradient(135deg, #409EFF 0%, #53a8ff 100%);
color: #fff;
border: none;
border-radius: 8rpx;
padding: 12rpx 24rpx;
font-size: 26rpx;
}
/* 添加过渡样式 */
.popup-fade-enter-active,
.popup-fade-leave-active {
transition: opacity 0.3s ease;
}
.popup-fade-enter-from,
.popup-fade-leave-to {
opacity: 0;
}
.popup-slide-enter-active,
.popup-slide-leave-active {
transition: all 0.4s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.popup-slide-enter-from {
opacity: 0;
transform: translate(-50%, -50%) translateY(50px);
}
.popup-slide-leave-to {
opacity: 0;
transform: translate(-50%, -50%) translateY(50px);
}
/* 蒙层渐入渐出动画 */
.popup-fade-enter-active,
.popup-fade-leave-active {
transition: opacity 0.3s ease;
}
/* 进入前/离开后状态(透明) */
.popup-fade-enter,
.popup-fade-leave-to {
opacity: 0;
}
/* 弹窗滑入滑出动画(核心) */
.popup-slide-enter-active,
.popup-slide-leave-active {
transition: all 0.4s cubic-bezier(0.34, 1.56, 0.64, 1);
/* 缓动曲线更丝滑 */
}
/* 进入前状态:透明 + 向下偏移50px */
.popup-slide-enter {
opacity: 0;
transform: translate(-50%, -50%) translateY(50px);
}
/* 离开后状态:透明 + 向下偏移50px */
.popup-slide-leave-to {
opacity: 0;
transform: translate(-50%, -50%) translateY(50px);
}
</style>