| @@ -22,4 +22,25 @@ | |||
| } | |||
| /* 每个页面公共css */ | |||
| /* #ifdef H5 */ | |||
| h1, h2, h3, h4, h5, h6 { | |||
| font-size: inherit; | |||
| font-weight: inherit; | |||
| } | |||
| ol, ul, menu { | |||
| list-style: none; | |||
| } | |||
| ul, ol { | |||
| list-style: none; | |||
| margin: 0; | |||
| padding: 0; | |||
| } | |||
| .page { | |||
| min-height: calc(100vh - 50px) !important; | |||
| } | |||
| uni-page-head .uni-page-head { | |||
| box-shadow: 0 1px 6px rgba(0, 0, 0, 0.08); | |||
| } | |||
| /* #endif */ | |||
| </style> | |||
| @@ -0,0 +1,303 @@ | |||
| <template> | |||
| <view v-if="show" class="sign-page" @touchmove.stop.prevent> | |||
| <view class="canvas-wrap"> | |||
| <view class="hint-text"> | |||
| <text>请在此处手写签名</text> | |||
| <text class="hint-sub">请横屏书写</text> | |||
| </view> | |||
| <view id="signContainer" class="sign-container" /> | |||
| </view> | |||
| <view class="bottom-btns"> | |||
| <view class="btn-close" @click="handleClose"> | |||
| <text>取消</text> | |||
| </view> | |||
| <view class="btn-clear" @click="handleClear"> | |||
| <text>清除</text> | |||
| </view> | |||
| <view class="btn-save" @click="handleSave"> | |||
| <text>{{ saving ? '上传中' : '保存' }}</text> | |||
| </view> | |||
| </view> | |||
| </view> | |||
| </template> | |||
| <script setup> | |||
| import { ref, watch, nextTick } from 'vue' | |||
| import { upload } from '@/utils/request.js' | |||
| const props = defineProps({ | |||
| show: { type: Boolean, default: false } | |||
| }) | |||
| const emit = defineEmits(['close', 'confirm']) | |||
| let signature = null | |||
| const hasDrawn = ref(false) | |||
| const saving = ref(false) | |||
| let scriptLoaded = false | |||
| const loadScript = () => { | |||
| return new Promise((resolve, reject) => { | |||
| if (window.SmoothSignature) { resolve(); return } | |||
| if (scriptLoaded) { | |||
| const check = setInterval(() => { | |||
| if (window.SmoothSignature) { clearInterval(check); resolve() } | |||
| }, 50) | |||
| return | |||
| } | |||
| scriptLoaded = true | |||
| const s = document.createElement('script') | |||
| s.src = 'https://cdn.csybhelp.com/lib/smooth-signature.umd.min.js' | |||
| s.onload = resolve | |||
| s.onerror = reject | |||
| document.head.appendChild(s) | |||
| }) | |||
| } | |||
| const initSignature = async () => { | |||
| await loadScript() | |||
| await nextTick() | |||
| setTimeout(() => { | |||
| const container = document.getElementById('signContainer') | |||
| if (!container) return | |||
| container.innerHTML = '' | |||
| const canvas = document.createElement('canvas') | |||
| canvas.style.width = '100%' | |||
| canvas.style.height = '100%' | |||
| container.appendChild(canvas) | |||
| signature = new window.SmoothSignature(canvas, { | |||
| width: container.offsetWidth, | |||
| height: container.offsetHeight, | |||
| minWidth: 2, | |||
| maxWidth: 6, | |||
| color: '#333', | |||
| bgColor: 'transparent' | |||
| }) | |||
| canvas.addEventListener('touchstart', () => { hasDrawn.value = true }) | |||
| canvas.addEventListener('mousedown', () => { hasDrawn.value = true }) | |||
| }, 200) | |||
| } | |||
| watch(() => props.show, async (val) => { | |||
| if (val) { | |||
| hasDrawn.value = false | |||
| saving.value = false | |||
| signature = null | |||
| await initSignature() | |||
| } | |||
| }) | |||
| const handleClear = () => { | |||
| if (signature) signature.clear() | |||
| hasDrawn.value = false | |||
| } | |||
| const handleClose = () => { emit('close') } | |||
| const handleSave = async () => { | |||
| if (!hasDrawn.value || !signature) { | |||
| return uni.showToast({ title: '请先签名', icon: 'none' }) | |||
| } | |||
| if (saving.value) return | |||
| saving.value = true | |||
| uni.showLoading({ title: '保存中...' }) | |||
| try { | |||
| // 从 smooth-signature 获取 PNG DataURL | |||
| const dataUrl = signature.getPNG() | |||
| if (!dataUrl) throw { msg: '获取签名失败' } | |||
| // 旋转签名(逆时针90度,横屏→竖屏,与小程序端保持一致) | |||
| const rotatedDataUrl = await rotateImage(dataUrl, -90) | |||
| const blob = dataURLtoBlob(rotatedDataUrl) | |||
| const formData = new FormData() | |||
| formData.append('file', blob, 'signature.png') | |||
| // H5 下直接用 XMLHttpRequest 上传(uni.uploadFile 需要 filePath) | |||
| const token = uni.getStorageSync('pap-cytx-token') || '' | |||
| const baseUrl = uni.$u && uni.$u.http && uni.$u.http.config | |||
| ? uni.$u.http.config.baseURL | |||
| : '/pro-api' | |||
| const url = baseUrl + '/api/mp/upload' | |||
| const result = await new Promise((resolve, reject) => { | |||
| const xhr = new XMLHttpRequest() | |||
| xhr.open('POST', url) | |||
| if (token) xhr.setRequestHeader('Authorization', 'Bearer ' + token) | |||
| xhr.onload = () => { | |||
| try { | |||
| const res = JSON.parse(xhr.responseText) | |||
| if (res.code === 0 && res.data && res.data.url) resolve(res.data.url) | |||
| else reject({ msg: res.msg || '上传失败' }) | |||
| } catch (e) { reject({ msg: '上传失败' }) } | |||
| } | |||
| xhr.onerror = () => reject({ msg: '网络错误' }) | |||
| xhr.send(formData) | |||
| }) | |||
| uni.hideLoading() | |||
| emit('confirm', { url: result }) | |||
| } catch (err) { | |||
| uni.hideLoading() | |||
| uni.showToast({ title: err.msg || '上传失败', icon: 'none' }) | |||
| } finally { | |||
| saving.value = false | |||
| } | |||
| } | |||
| const dataURLtoBlob = (dataUrl) => { | |||
| const arr = dataUrl.split(',') | |||
| const mime = arr[0].match(/:(.*?);/)[1] | |||
| const bstr = atob(arr[1]) | |||
| let n = bstr.length | |||
| const u8arr = new Uint8Array(n) | |||
| while (n--) u8arr[n] = bstr.charCodeAt(n) | |||
| return new Blob([u8arr], { type: mime }) | |||
| } | |||
| const rotateImage = (dataUrl, degrees) => { | |||
| return new Promise((resolve) => { | |||
| const img = new Image() | |||
| img.onload = () => { | |||
| const canvas = document.createElement('canvas') | |||
| const rad = (degrees * Math.PI) / 180 | |||
| // 旋转 -90 度:宽高互换 | |||
| if (Math.abs(degrees) === 90) { | |||
| canvas.width = img.height | |||
| canvas.height = img.width | |||
| } else { | |||
| canvas.width = img.width | |||
| canvas.height = img.height | |||
| } | |||
| const ctx = canvas.getContext('2d') | |||
| ctx.translate(canvas.width / 2, canvas.height / 2) | |||
| ctx.rotate(rad) | |||
| ctx.drawImage(img, -img.width / 2, -img.height / 2) | |||
| resolve(canvas.toDataURL('image/png')) | |||
| } | |||
| img.src = dataUrl | |||
| }) | |||
| } | |||
| </script> | |||
| <style lang="scss" scoped> | |||
| .sign-page { | |||
| position: fixed; | |||
| top: 30rpx; | |||
| left: 30rpx; | |||
| right: 30rpx; | |||
| bottom: 30rpx; | |||
| z-index: 9999; | |||
| display: flex; | |||
| flex-direction: column; | |||
| background: #edf2fc; | |||
| border-radius: 16rpx; | |||
| overflow: hidden; | |||
| box-shadow: 0 0 0 9999px rgba(0, 0, 0, 0.5); | |||
| } | |||
| .canvas-wrap { | |||
| flex: 1; | |||
| position: relative; | |||
| overflow: hidden; | |||
| .hint-text { | |||
| position: absolute; | |||
| top: 50%; | |||
| left: 50%; | |||
| transform: translate(-50%, -50%) rotate(90deg); | |||
| pointer-events: none; | |||
| z-index: 0; | |||
| display: flex; | |||
| flex-direction: column; | |||
| align-items: center; | |||
| gap: 16rpx; | |||
| width: 80vh; | |||
| text { | |||
| font-size: 48rpx; | |||
| color: #c0c8d8; | |||
| font-weight: 300; | |||
| letter-spacing: 16rpx; | |||
| white-space: nowrap; | |||
| } | |||
| .hint-sub { | |||
| font-size: 28rpx; | |||
| letter-spacing: 8rpx; | |||
| } | |||
| } | |||
| .sign-container { | |||
| width: 100%; | |||
| height: 100%; | |||
| position: relative; | |||
| z-index: 1; | |||
| } | |||
| } | |||
| .bottom-btns { | |||
| display: flex; | |||
| height: calc(100rpx + env(safe-area-inset-bottom)); | |||
| flex-shrink: 0; | |||
| gap: 12rpx; | |||
| padding: 12rpx; | |||
| padding-bottom: calc(12rpx + env(safe-area-inset-bottom)); | |||
| background: #edf2fc; | |||
| .btn-close { | |||
| flex: 1; | |||
| display: flex; | |||
| align-items: center; | |||
| justify-content: center; | |||
| background: #8c939d; | |||
| border-radius: 12rpx; | |||
| text { | |||
| writing-mode: vertical-rl; | |||
| transform: rotate(90deg); | |||
| font-size: 32rpx; | |||
| color: #fff; | |||
| letter-spacing: 4rpx; | |||
| height: 120rpx; | |||
| text-align: center; | |||
| } | |||
| } | |||
| .btn-clear { | |||
| flex: 1; | |||
| display: flex; | |||
| align-items: center; | |||
| justify-content: center; | |||
| background: #ff4040; | |||
| border-radius: 12rpx; | |||
| text { | |||
| writing-mode: vertical-rl; | |||
| transform: rotate(90deg); | |||
| font-size: 32rpx; | |||
| color: #fff; | |||
| letter-spacing: 4rpx; | |||
| height: 120rpx; | |||
| text-align: center; | |||
| } | |||
| } | |||
| .btn-save { | |||
| flex: 2; | |||
| display: flex; | |||
| align-items: center; | |||
| justify-content: center; | |||
| background: #0e63e3; | |||
| border-radius: 12rpx; | |||
| text { | |||
| writing-mode: vertical-rl; | |||
| transform: rotate(90deg); | |||
| font-size: 32rpx; | |||
| color: #fff; | |||
| letter-spacing: 4rpx; | |||
| height: 120rpx; | |||
| text-align: center; | |||
| } | |||
| } | |||
| } | |||
| </style> | |||
| @@ -10,7 +10,22 @@ const envConf = { | |||
| // 正式版-正式环境 | |||
| release: { | |||
| BASE_URL: 'https://cytx.csybhelp.com', | |||
| }, | |||
| // H5 环境 | |||
| h5: { | |||
| BASE_URL: '/pro-api', | |||
| } | |||
| } | |||
| export default envConf[uni.getAccountInfoSync().miniProgram.envVersion || 'develop']; // 可在此处手 | |||
| // export default envConf['develop']; | |||
| function getEnvConfig() { | |||
| // #ifdef H5 | |||
| return envConf['h5'] | |||
| // #endif | |||
| // #ifdef MP-WEIXIN | |||
| return envConf[uni.getAccountInfoSync().miniProgram.envVersion || 'develop'] | |||
| // #endif | |||
| // eslint-disable-next-line no-unreachable | |||
| return envConf['develop'] | |||
| } | |||
| export default getEnvConfig() | |||
| @@ -60,5 +60,13 @@ | |||
| "uniStatistics" : { | |||
| "enable" : false | |||
| }, | |||
| "h5" : { | |||
| "title" : "肠愈同行", | |||
| "router" : { | |||
| "mode" : "history", | |||
| "base" : "/" | |||
| }, | |||
| "devServer" : {} | |||
| }, | |||
| "vueVersion" : "3" | |||
| } | |||
| @@ -11,20 +11,20 @@ | |||
| { | |||
| "path": "pages/index/index", | |||
| "style": { | |||
| "navigationBarTitleText": "" | |||
| "navigationBarTitleText": "肠愈同行" | |||
| } | |||
| }, | |||
| { | |||
| "path": "pages/profile/profile", | |||
| "style": { | |||
| "navigationBarTitleText": "" | |||
| "navigationBarTitleText": "我的" | |||
| } | |||
| }, | |||
| { | |||
| "path": "pages/content/content", | |||
| "style": { | |||
| "navigationStyle": "default", | |||
| "navigationBarTitleText": "" | |||
| "navigationBarTitleText": "详情" | |||
| } | |||
| }, | |||
| { | |||
| @@ -1,7 +1,7 @@ | |||
| <template> | |||
| <view class="page"> | |||
| <view class="content-body" v-if="loaded"> | |||
| <mp-html :content="content" /> | |||
| <mp-html :content="content" :tag-style="mpHtmlTagStyle" /> | |||
| </view> | |||
| <view v-else class="loading"> | |||
| <u-loading-icon /> | |||
| @@ -15,6 +15,18 @@ import { onLoad } from '@dcloudio/uni-app' | |||
| import { get } from '@/utils/request.js' | |||
| import mpHtml from '@/uni_modules/mp-html/components/mp-html/mp-html.vue' | |||
| const mpHtmlTagStyle = { | |||
| h1: 'font-size:inherit;font-weight:inherit;', | |||
| h2: 'font-size:inherit;font-weight:inherit;', | |||
| h3: 'font-size:inherit;font-weight:inherit;', | |||
| h4: 'font-size:inherit;font-weight:inherit;', | |||
| h5: 'font-size:inherit;font-weight:inherit;', | |||
| h6: 'font-size:inherit;font-weight:inherit;', | |||
| ol: 'list-style:none;margin:0;padding:0;', | |||
| ul: 'list-style:none;margin:0;padding:0;', | |||
| menu: 'list-style:none;' | |||
| } | |||
| const content = ref('') | |||
| const loaded = ref(false) | |||
| @@ -1,13 +1,20 @@ | |||
| <template> | |||
| <view class="page"> | |||
| <!-- 顶部横幅 --> | |||
| <!-- #ifdef MP-WEIXIN --> | |||
| <image class="banner" src="https://cdn.csybhelp.com/images/cytx/index-bg.png" mode="widthFix" /> | |||
| <!-- #endif --> | |||
| <!-- #ifdef H5 --> | |||
| <view class="banner-h5"> | |||
| <img class="banner-img" src="https://cdn.csybhelp.com/images/cytx/index-bg.png" /> | |||
| </view> | |||
| <!-- #endif --> | |||
| <!-- 正文内容区域 --> | |||
| <view class="content-area"> | |||
| <view v-if="content" class="letter"> | |||
| <view class="body"> | |||
| <mp-html :content="content" /> | |||
| <mp-html :content="content" :tag-style="mpHtmlTagStyle" /> | |||
| </view> | |||
| </view> | |||
| <view v-else-if="!loaded" class="loading-text">加载中...</view> | |||
| @@ -24,6 +31,18 @@ import { get } from '@/utils/request.js' | |||
| import { getToken, getUserInfo, setUserInfo } from '@/utils/cache.js' | |||
| import mpHtml from '@/uni_modules/mp-html/components/mp-html/mp-html.vue' | |||
| const mpHtmlTagStyle = { | |||
| h1: 'font-size:inherit;font-weight:inherit;', | |||
| h2: 'font-size:inherit;font-weight:inherit;', | |||
| h3: 'font-size:inherit;font-weight:inherit;', | |||
| h4: 'font-size:inherit;font-weight:inherit;', | |||
| h5: 'font-size:inherit;font-weight:inherit;', | |||
| h6: 'font-size:inherit;font-weight:inherit;', | |||
| ol: 'list-style:none;margin:0;padding:0;', | |||
| ul: 'list-style:none;margin:0;padding:0;', | |||
| menu: 'list-style:none;' | |||
| } | |||
| const CONTENT_CACHE_KEY = 'cytx-index-content' | |||
| const content = ref('') | |||
| @@ -146,6 +165,20 @@ onShow(() => { | |||
| display: block; | |||
| } | |||
| .banner-h5 { | |||
| width: 100%; | |||
| height: 350px; | |||
| overflow: hidden; | |||
| .banner-img { | |||
| width: 100%; | |||
| height: 100%; | |||
| object-fit: cover; | |||
| object-position: center bottom; | |||
| display: block; | |||
| } | |||
| } | |||
| .content-area { | |||
| background: #fff; | |||
| border-radius: 24rpx; | |||
| @@ -153,6 +186,9 @@ onShow(() => { | |||
| position: relative; | |||
| z-index: 1; | |||
| padding: 56rpx 40rpx 32rpx; | |||
| /* #ifdef H5 */ | |||
| margin-top: -172px; | |||
| /* #endif */ | |||
| } | |||
| .letter { | |||
| @@ -7,9 +7,29 @@ | |||
| </view> | |||
| <view class="btn-area"> | |||
| <button class="login-btn" @tap="handleLogin" :loading="loading"> | |||
| <!-- #ifdef MP-WEIXIN --> | |||
| <button class="login-btn" @tap="handleWxLogin" :loading="loading"> | |||
| 微信一键登录 | |||
| </button> | |||
| <!-- #endif --> | |||
| <!-- #ifdef H5 --> | |||
| <view class="phone-form"> | |||
| <view class="form-item"> | |||
| <input class="form-input" type="number" v-model="phone" placeholder="请输入手机号" maxlength="11" /> | |||
| </view> | |||
| <view class="form-item code-row"> | |||
| <input class="form-input code-input" type="number" v-model="smsCode" placeholder="请输入验证码" maxlength="6" /> | |||
| <button class="sms-btn" :disabled="countdown > 0" @tap="sendSms"> | |||
| {{ countdown > 0 ? countdown + 's' : '获取验证码' }} | |||
| </button> | |||
| </view> | |||
| <button class="login-btn" @tap="handlePhoneLogin" :loading="loading"> | |||
| 登录 | |||
| </button> | |||
| </view> | |||
| <!-- #endif --> | |||
| <view class="tip">登录即表示同意 | |||
| <text class="link" @tap="goPrivacy">《隐私协议》</text> | |||
| </view> | |||
| @@ -18,13 +38,36 @@ | |||
| </template> | |||
| <script setup> | |||
| import { ref } from 'vue' | |||
| import { ref, onUnmounted } from 'vue' | |||
| import { post } from '@/utils/request.js' | |||
| import { setToken, setUserInfo } from '@/utils/cache.js' | |||
| const loading = ref(false) | |||
| const phone = ref('') | |||
| const smsCode = ref('') | |||
| const countdown = ref(0) | |||
| let timer = null | |||
| onUnmounted(() => { | |||
| if (timer) { clearInterval(timer); timer = null } | |||
| }) | |||
| const handleLogin = async () => { | |||
| const loginSuccess = (res) => { | |||
| setToken(res.data.token) | |||
| setUserInfo(res.data.userInfo) | |||
| uni.showToast({ title: '登录成功', icon: 'success' }) | |||
| setTimeout(() => { | |||
| const pages = getCurrentPages() | |||
| if (pages.length > 1) { | |||
| uni.navigateBack() | |||
| } else { | |||
| uni.switchTab({ url: '/pages/profile/profile' }) | |||
| } | |||
| }, 500) | |||
| } | |||
| // #ifdef MP-WEIXIN | |||
| const handleWxLogin = async () => { | |||
| if (loading.value) return | |||
| loading.value = true | |||
| try { | |||
| @@ -36,26 +79,65 @@ const handleLogin = async () => { | |||
| }) | |||
| }) | |||
| const res = await post('/api/mp/login', { code }) | |||
| setToken(res.data.token) | |||
| setUserInfo(res.data.userInfo) | |||
| uni.showToast({ title: '登录成功', icon: 'success' }) | |||
| setTimeout(() => { | |||
| const pages = getCurrentPages() | |||
| if (pages.length > 1) { | |||
| uni.navigateBack() | |||
| } else { | |||
| uni.switchTab({ url: '/pages/profile/profile' }) | |||
| } | |||
| }, 500) | |||
| loginSuccess(res) | |||
| } catch (e) { | |||
| if (e && e.msg) uni.showToast({ title: e.msg, icon: 'none' }) | |||
| } finally { | |||
| loading.value = false | |||
| } | |||
| } | |||
| // #endif | |||
| // #ifdef H5 | |||
| const sendSms = async () => { | |||
| if (countdown.value > 0) return | |||
| if (!phone.value || phone.value.length !== 11) { | |||
| return uni.showToast({ title: '请输入正确的手机号', icon: 'none' }) | |||
| } | |||
| try { | |||
| const res = await post('/api/mp/sendSmsCode', { mobile: phone.value, bizType: 'login' }) | |||
| if (res.data && res.data.code) { | |||
| uni.showToast({ title: `验证码: ${res.data.code}`, icon: 'none', duration: 3000 }) | |||
| } else { | |||
| uni.showToast({ title: '验证码已发送', icon: 'none' }) | |||
| } | |||
| countdown.value = 60 | |||
| timer = setInterval(() => { | |||
| countdown.value-- | |||
| if (countdown.value <= 0) { clearInterval(timer); timer = null } | |||
| }, 1000) | |||
| } catch (e) { | |||
| if (e && e.msg) uni.showToast({ title: e.msg, icon: 'none' }) | |||
| } | |||
| } | |||
| const handlePhoneLogin = async () => { | |||
| if (loading.value) return | |||
| if (!phone.value || phone.value.length !== 11) { | |||
| return uni.showToast({ title: '请输入正确的手机号', icon: 'none' }) | |||
| } | |||
| if (!smsCode.value || smsCode.value.length !== 6) { | |||
| return uni.showToast({ title: '请输入6位验证码', icon: 'none' }) | |||
| } | |||
| loading.value = true | |||
| try { | |||
| const res = await post('/api/mp/phoneLogin', { mobile: phone.value, code: smsCode.value }) | |||
| loginSuccess(res) | |||
| } catch (e) { | |||
| if (e && e.msg) uni.showToast({ title: e.msg, icon: 'none' }) | |||
| } finally { | |||
| loading.value = false | |||
| } | |||
| } | |||
| // #endif | |||
| const goPrivacy = () => { | |||
| // #ifdef H5 | |||
| uni.navigateTo({ url: '/pages/content/content?key=privacy_policy_h5' }) | |||
| // #endif | |||
| // #ifdef MP-WEIXIN | |||
| uni.navigateTo({ url: '/pages/content/content?key=privacy_policy' }) | |||
| // #endif | |||
| } | |||
| </script> | |||
| @@ -67,6 +149,9 @@ const goPrivacy = () => { | |||
| flex-direction: column; | |||
| align-items: center; | |||
| padding-top: 25vh; | |||
| /* #ifdef H5 */ | |||
| padding-top: 10vh; | |||
| /* #endif */ | |||
| } | |||
| .logo-area { | |||
| @@ -94,8 +179,6 @@ const goPrivacy = () => { | |||
| color: #b0b3b8; | |||
| letter-spacing: 2rpx; | |||
| } | |||
| } | |||
| .btn-area { | |||
| @@ -133,4 +216,59 @@ const goPrivacy = () => { | |||
| } | |||
| } | |||
| } | |||
| .phone-form { | |||
| width: 100%; | |||
| .form-item { | |||
| margin-bottom: 24rpx; | |||
| } | |||
| .form-input { | |||
| width: 100%; | |||
| height: 88rpx; | |||
| border: 1rpx solid #dcdfe6; | |||
| border-radius: 44rpx; | |||
| padding: 0 36rpx; | |||
| font-size: 30rpx; | |||
| box-sizing: border-box; | |||
| background: #f8f9fa; | |||
| } | |||
| .code-row { | |||
| display: flex; | |||
| gap: 20rpx; | |||
| .code-input { | |||
| flex: 2; | |||
| } | |||
| } | |||
| .sms-btn { | |||
| flex: 1; | |||
| height: 88rpx; | |||
| line-height: 88rpx; | |||
| padding: 0; | |||
| background: #0F78E9; | |||
| color: #fff; | |||
| border: none; | |||
| border-radius: 44rpx; | |||
| font-size: 26rpx; | |||
| white-space: nowrap; | |||
| text-align: center; | |||
| &::after { | |||
| border: none; | |||
| } | |||
| &[disabled] { | |||
| background: #a0cfff; | |||
| color: #fff; | |||
| } | |||
| } | |||
| .login-btn { | |||
| margin-top: 16rpx; | |||
| } | |||
| } | |||
| </style> | |||
| @@ -13,7 +13,7 @@ | |||
| </view> | |||
| <view class="info" @tap="!isLoggedIn && goLogin()"> | |||
| <view class="name">{{ displayName }}</view> | |||
| <view v-if="isLoggedIn && userInfo && userInfo.patient && userInfo.patient.patient_no" class="patient-no">{{ userInfo.patient.patient_no }}</view> | |||
| <view v-if="isLoggedIn && userInfo && userInfo.patient && userInfo.patient.patient_no" class="patient-no">No·{{ userInfo.patient.patient_no }}</view> | |||
| </view> | |||
| </view> | |||
| </view> | |||
| @@ -65,7 +65,7 @@ | |||
| <text class="text">修改手机号</text> | |||
| <u-icon name="arrow-right" size="16" color="#c0c4cc" /> | |||
| </view> | |||
| <view class="menu-item" @tap="goTo('/pages/content/content?key=privacy_policy')"> | |||
| <view class="menu-item" @tap="goPrivacy"> | |||
| <view class="menu-icon"> | |||
| <u-icon name="lock-fill" size="20" color="#909399" /> | |||
| </view> | |||
| @@ -104,10 +104,18 @@ const patientStatus = computed(() => { | |||
| }) | |||
| const isRejected = computed(() => patientStatus.value === 2) | |||
| const randomTag = () => { | |||
| const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789' | |||
| let s = '' | |||
| for (let i = 0; i < 4; i++) s += chars.charAt(Math.floor(Math.random() * chars.length)) | |||
| return s | |||
| } | |||
| const _userTag = randomTag() | |||
| const displayName = computed(() => { | |||
| if (!isLoggedIn.value) return '点击登录' | |||
| if (isAuthed.value && userInfo.value && userInfo.value.patient && userInfo.value.patient.name) return userInfo.value.patient.name | |||
| return (userInfo.value && userInfo.value.nickname) || '微信用户' | |||
| return (userInfo.value && userInfo.value.nickname) || ('用户' + _userTag) | |||
| }) | |||
| onShow(() => { | |||
| @@ -163,6 +171,15 @@ const goTo = (url) => { | |||
| uni.navigateTo({ url }) | |||
| } | |||
| const goPrivacy = () => { | |||
| // #ifdef H5 | |||
| uni.navigateTo({ url: '/pages/content/content?key=privacy_policy_h5' }) | |||
| // #endif | |||
| // #ifdef MP-WEIXIN | |||
| uni.navigateTo({ url: '/pages/content/content?key=privacy_policy' }) | |||
| // #endif | |||
| } | |||
| const goMyInfo = () => { | |||
| if (!checkAuth()) return | |||
| uni.navigateTo({ url: '/pages/myinfo/myinfo' }) | |||
| @@ -265,6 +282,9 @@ const handleLogout = () => { | |||
| position: relative; | |||
| padding: 220rpx 40rpx 60rpx; | |||
| overflow: hidden; | |||
| /* #ifdef H5 */ | |||
| padding-top: 120rpx; | |||
| /* #endif */ | |||
| } | |||
| .header-bg { | |||
| @@ -35,7 +35,7 @@ | |||
| <!-- 签名区域 --> | |||
| <view class="section"> | |||
| <view class="sign-label">请在下方签名确认</view> | |||
| <view class="canvas-wrap" @tap="goSignature"> | |||
| <view class="canvas-wrap" @tap="openSignature"> | |||
| <image v-if="signImageUrl" class="sign-preview" :src="signImageUrl" mode="aspectFit" /> | |||
| <view v-else class="canvas-placeholder"> | |||
| <u-icon name="edit-pen" size="28" color="#ccc" /> | |||
| @@ -43,7 +43,7 @@ | |||
| </view> | |||
| </view> | |||
| <view v-if="signImageUrl" class="sign-actions"> | |||
| <view class="clear-btn" @tap="goSignature">重新签名</view> | |||
| <view class="clear-btn" @tap="openSignature">重新签名</view> | |||
| </view> | |||
| </view> | |||
| @@ -51,6 +51,10 @@ | |||
| <view class="btn-wrap"> | |||
| <u-button text="确认签署" :loading="submitting" @click="confirmSign" color="#0E63E3" size="large" /> | |||
| </view> | |||
| <!-- #ifdef H5 --> | |||
| <h5-signature :show="showH5Sign" @close="showH5Sign = false" @confirm="onH5SignConfirm" /> | |||
| <!-- #endif --> | |||
| </view> | |||
| </template> | |||
| @@ -68,6 +72,9 @@ const guardianIdCard = ref('') | |||
| const guardianRelation = ref('') | |||
| const signImageUrl = ref('') | |||
| const submitting = ref(false) | |||
| // #ifdef H5 | |||
| const showH5Sign = ref(false) | |||
| // #endif | |||
| const onSignatureResult = (data) => { | |||
| if (data.url) signImageUrl.value = data.url | |||
| @@ -75,17 +82,20 @@ const onSignatureResult = (data) => { | |||
| onLoad((options) => { | |||
| signType.value = options.type || 'privacy' | |||
| // 回显上次填写的数据 | |||
| if (options.amount) incomeAmount.value = decodeURIComponent(options.amount) | |||
| if (options.guardianName) guardianName.value = decodeURIComponent(options.guardianName) | |||
| if (options.guardianIdCard) guardianIdCard.value = decodeURIComponent(options.guardianIdCard) | |||
| if (options.guardianRelation) guardianRelation.value = decodeURIComponent(options.guardianRelation) | |||
| // #ifdef MP-WEIXIN | |||
| uni.$on('signatureResult', onSignatureResult) | |||
| // #endif | |||
| loadContent() | |||
| }) | |||
| onBeforeUnmount(() => { | |||
| // #ifdef MP-WEIXIN | |||
| uni.$off('signatureResult', onSignatureResult) | |||
| // #endif | |||
| }) | |||
| const loadContent = async () => { | |||
| @@ -97,9 +107,21 @@ const loadContent = async () => { | |||
| } catch (e) {} | |||
| } | |||
| const goSignature = () => { | |||
| const openSignature = () => { | |||
| // #ifdef MP-WEIXIN | |||
| uni.navigateTo({ url: '/pages/sign/signature' }) | |||
| // #endif | |||
| // #ifdef H5 | |||
| showH5Sign.value = true | |||
| // #endif | |||
| } | |||
| // #ifdef H5 | |||
| const onH5SignConfirm = (data) => { | |||
| showH5Sign.value = false | |||
| if (data.url) signImageUrl.value = data.url | |||
| } | |||
| // #endif | |||
| const confirmSign = async () => { | |||
| if (!signImageUrl.value) { | |||
| @@ -3,6 +3,7 @@ | |||
| <view class="canvas-wrap"> | |||
| <view class="hint-text"> | |||
| <text>请在此处手写签名</text> | |||
| <text class="hint-sub">请横屏书写</text> | |||
| </view> | |||
| <canvas id="signCanvas" canvas-id="signCanvas" type="2d" class="sign-canvas" disable-scroll | |||
| @touchstart="onTouchStart" @touchmove="onTouchMove" @touchend="onTouchEnd" /> | |||
| @@ -181,6 +182,10 @@ const handleSave = () => { | |||
| pointer-events: none; | |||
| white-space: nowrap; | |||
| z-index: 0; | |||
| display: flex; | |||
| flex-direction: column; | |||
| align-items: center; | |||
| gap: 16rpx; | |||
| text { | |||
| font-size: 48rpx; | |||
| @@ -188,6 +193,11 @@ const handleSave = () => { | |||
| font-weight: 300; | |||
| letter-spacing: 16rpx; | |||
| } | |||
| .hint-sub { | |||
| font-size: 28rpx; | |||
| letter-spacing: 8rpx; | |||
| } | |||
| } | |||
| .sign-canvas { | |||
| @@ -0,0 +1,15 @@ | |||
| import { defineConfig } from 'vite' | |||
| import uni from '@dcloudio/vite-plugin-uni' | |||
| export default defineConfig({ | |||
| plugins: [uni()], | |||
| server: { | |||
| proxy: { | |||
| '/pro-api': { | |||
| target: 'http://192.168.3.66:8361', | |||
| changeOrigin: true, | |||
| rewrite: (path) => path.replace(/^\/pro-api/, '') | |||
| } | |||
| } | |||
| } | |||
| }) | |||