| @@ -22,4 +22,25 @@ | |||||
| } | } | ||||
| /* 每个页面公共css */ | /* 每个页面公共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> | </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: { | release: { | ||||
| BASE_URL: 'https://cytx.csybhelp.com', | 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" : { | "uniStatistics" : { | ||||
| "enable" : false | "enable" : false | ||||
| }, | }, | ||||
| "h5" : { | |||||
| "title" : "肠愈同行", | |||||
| "router" : { | |||||
| "mode" : "history", | |||||
| "base" : "/" | |||||
| }, | |||||
| "devServer" : {} | |||||
| }, | |||||
| "vueVersion" : "3" | "vueVersion" : "3" | ||||
| } | } | ||||
| @@ -11,20 +11,20 @@ | |||||
| { | { | ||||
| "path": "pages/index/index", | "path": "pages/index/index", | ||||
| "style": { | "style": { | ||||
| "navigationBarTitleText": "" | |||||
| "navigationBarTitleText": "肠愈同行" | |||||
| } | } | ||||
| }, | }, | ||||
| { | { | ||||
| "path": "pages/profile/profile", | "path": "pages/profile/profile", | ||||
| "style": { | "style": { | ||||
| "navigationBarTitleText": "" | |||||
| "navigationBarTitleText": "我的" | |||||
| } | } | ||||
| }, | }, | ||||
| { | { | ||||
| "path": "pages/content/content", | "path": "pages/content/content", | ||||
| "style": { | "style": { | ||||
| "navigationStyle": "default", | "navigationStyle": "default", | ||||
| "navigationBarTitleText": "" | |||||
| "navigationBarTitleText": "详情" | |||||
| } | } | ||||
| }, | }, | ||||
| { | { | ||||
| @@ -1,7 +1,7 @@ | |||||
| <template> | <template> | ||||
| <view class="page"> | <view class="page"> | ||||
| <view class="content-body" v-if="loaded"> | <view class="content-body" v-if="loaded"> | ||||
| <mp-html :content="content" /> | |||||
| <mp-html :content="content" :tag-style="mpHtmlTagStyle" /> | |||||
| </view> | </view> | ||||
| <view v-else class="loading"> | <view v-else class="loading"> | ||||
| <u-loading-icon /> | <u-loading-icon /> | ||||
| @@ -15,6 +15,18 @@ import { onLoad } from '@dcloudio/uni-app' | |||||
| import { get } from '@/utils/request.js' | import { get } from '@/utils/request.js' | ||||
| import mpHtml from '@/uni_modules/mp-html/components/mp-html/mp-html.vue' | 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 content = ref('') | ||||
| const loaded = ref(false) | const loaded = ref(false) | ||||
| @@ -1,13 +1,20 @@ | |||||
| <template> | <template> | ||||
| <view class="page"> | <view class="page"> | ||||
| <!-- 顶部横幅 --> | <!-- 顶部横幅 --> | ||||
| <!-- #ifdef MP-WEIXIN --> | |||||
| <image class="banner" src="https://cdn.csybhelp.com/images/cytx/index-bg.png" mode="widthFix" /> | <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 class="content-area"> | ||||
| <view v-if="content" class="letter"> | <view v-if="content" class="letter"> | ||||
| <view class="body"> | <view class="body"> | ||||
| <mp-html :content="content" /> | |||||
| <mp-html :content="content" :tag-style="mpHtmlTagStyle" /> | |||||
| </view> | </view> | ||||
| </view> | </view> | ||||
| <view v-else-if="!loaded" class="loading-text">加载中...</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 { getToken, getUserInfo, setUserInfo } from '@/utils/cache.js' | ||||
| import mpHtml from '@/uni_modules/mp-html/components/mp-html/mp-html.vue' | 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_CACHE_KEY = 'cytx-index-content' | ||||
| const content = ref('') | const content = ref('') | ||||
| @@ -146,6 +165,20 @@ onShow(() => { | |||||
| display: block; | 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 { | .content-area { | ||||
| background: #fff; | background: #fff; | ||||
| border-radius: 24rpx; | border-radius: 24rpx; | ||||
| @@ -153,6 +186,9 @@ onShow(() => { | |||||
| position: relative; | position: relative; | ||||
| z-index: 1; | z-index: 1; | ||||
| padding: 56rpx 40rpx 32rpx; | padding: 56rpx 40rpx 32rpx; | ||||
| /* #ifdef H5 */ | |||||
| margin-top: -172px; | |||||
| /* #endif */ | |||||
| } | } | ||||
| .letter { | .letter { | ||||
| @@ -7,9 +7,29 @@ | |||||
| </view> | </view> | ||||
| <view class="btn-area"> | <view class="btn-area"> | ||||
| <button class="login-btn" @tap="handleLogin" :loading="loading"> | |||||
| <!-- #ifdef MP-WEIXIN --> | |||||
| <button class="login-btn" @tap="handleWxLogin" :loading="loading"> | |||||
| 微信一键登录 | 微信一键登录 | ||||
| </button> | </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">登录即表示同意 | <view class="tip">登录即表示同意 | ||||
| <text class="link" @tap="goPrivacy">《隐私协议》</text> | <text class="link" @tap="goPrivacy">《隐私协议》</text> | ||||
| </view> | </view> | ||||
| @@ -18,13 +38,36 @@ | |||||
| </template> | </template> | ||||
| <script setup> | <script setup> | ||||
| import { ref } from 'vue' | |||||
| import { ref, onUnmounted } from 'vue' | |||||
| import { post } from '@/utils/request.js' | import { post } from '@/utils/request.js' | ||||
| import { setToken, setUserInfo } from '@/utils/cache.js' | import { setToken, setUserInfo } from '@/utils/cache.js' | ||||
| const loading = ref(false) | 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 | if (loading.value) return | ||||
| loading.value = true | loading.value = true | ||||
| try { | try { | ||||
| @@ -36,26 +79,65 @@ const handleLogin = async () => { | |||||
| }) | }) | ||||
| }) | }) | ||||
| const res = await post('/api/mp/login', { code }) | 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) { | } catch (e) { | ||||
| if (e && e.msg) uni.showToast({ title: e.msg, icon: 'none' }) | if (e && e.msg) uni.showToast({ title: e.msg, icon: 'none' }) | ||||
| } finally { | } finally { | ||||
| loading.value = false | 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 = () => { | 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' }) | uni.navigateTo({ url: '/pages/content/content?key=privacy_policy' }) | ||||
| // #endif | |||||
| } | } | ||||
| </script> | </script> | ||||
| @@ -67,6 +149,9 @@ const goPrivacy = () => { | |||||
| flex-direction: column; | flex-direction: column; | ||||
| align-items: center; | align-items: center; | ||||
| padding-top: 25vh; | padding-top: 25vh; | ||||
| /* #ifdef H5 */ | |||||
| padding-top: 10vh; | |||||
| /* #endif */ | |||||
| } | } | ||||
| .logo-area { | .logo-area { | ||||
| @@ -94,8 +179,6 @@ const goPrivacy = () => { | |||||
| color: #b0b3b8; | color: #b0b3b8; | ||||
| letter-spacing: 2rpx; | letter-spacing: 2rpx; | ||||
| } | } | ||||
| } | } | ||||
| .btn-area { | .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> | </style> | ||||
| @@ -13,7 +13,7 @@ | |||||
| </view> | </view> | ||||
| <view class="info" @tap="!isLoggedIn && goLogin()"> | <view class="info" @tap="!isLoggedIn && goLogin()"> | ||||
| <view class="name">{{ displayName }}</view> | <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> | </view> | ||||
| </view> | </view> | ||||
| @@ -65,7 +65,7 @@ | |||||
| <text class="text">修改手机号</text> | <text class="text">修改手机号</text> | ||||
| <u-icon name="arrow-right" size="16" color="#c0c4cc" /> | <u-icon name="arrow-right" size="16" color="#c0c4cc" /> | ||||
| </view> | </view> | ||||
| <view class="menu-item" @tap="goTo('/pages/content/content?key=privacy_policy')"> | |||||
| <view class="menu-item" @tap="goPrivacy"> | |||||
| <view class="menu-icon"> | <view class="menu-icon"> | ||||
| <u-icon name="lock-fill" size="20" color="#909399" /> | <u-icon name="lock-fill" size="20" color="#909399" /> | ||||
| </view> | </view> | ||||
| @@ -104,10 +104,18 @@ const patientStatus = computed(() => { | |||||
| }) | }) | ||||
| const isRejected = computed(() => patientStatus.value === 2) | 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(() => { | const displayName = computed(() => { | ||||
| if (!isLoggedIn.value) return '点击登录' | if (!isLoggedIn.value) return '点击登录' | ||||
| if (isAuthed.value && userInfo.value && userInfo.value.patient && userInfo.value.patient.name) return userInfo.value.patient.name | 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(() => { | onShow(() => { | ||||
| @@ -163,6 +171,15 @@ const goTo = (url) => { | |||||
| uni.navigateTo({ 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 = () => { | const goMyInfo = () => { | ||||
| if (!checkAuth()) return | if (!checkAuth()) return | ||||
| uni.navigateTo({ url: '/pages/myinfo/myinfo' }) | uni.navigateTo({ url: '/pages/myinfo/myinfo' }) | ||||
| @@ -265,6 +282,9 @@ const handleLogout = () => { | |||||
| position: relative; | position: relative; | ||||
| padding: 220rpx 40rpx 60rpx; | padding: 220rpx 40rpx 60rpx; | ||||
| overflow: hidden; | overflow: hidden; | ||||
| /* #ifdef H5 */ | |||||
| padding-top: 120rpx; | |||||
| /* #endif */ | |||||
| } | } | ||||
| .header-bg { | .header-bg { | ||||
| @@ -35,7 +35,7 @@ | |||||
| <!-- 签名区域 --> | <!-- 签名区域 --> | ||||
| <view class="section"> | <view class="section"> | ||||
| <view class="sign-label">请在下方签名确认</view> | <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" /> | <image v-if="signImageUrl" class="sign-preview" :src="signImageUrl" mode="aspectFit" /> | ||||
| <view v-else class="canvas-placeholder"> | <view v-else class="canvas-placeholder"> | ||||
| <u-icon name="edit-pen" size="28" color="#ccc" /> | <u-icon name="edit-pen" size="28" color="#ccc" /> | ||||
| @@ -43,7 +43,7 @@ | |||||
| </view> | </view> | ||||
| </view> | </view> | ||||
| <view v-if="signImageUrl" class="sign-actions"> | <view v-if="signImageUrl" class="sign-actions"> | ||||
| <view class="clear-btn" @tap="goSignature">重新签名</view> | |||||
| <view class="clear-btn" @tap="openSignature">重新签名</view> | |||||
| </view> | </view> | ||||
| </view> | </view> | ||||
| @@ -51,6 +51,10 @@ | |||||
| <view class="btn-wrap"> | <view class="btn-wrap"> | ||||
| <u-button text="确认签署" :loading="submitting" @click="confirmSign" color="#0E63E3" size="large" /> | <u-button text="确认签署" :loading="submitting" @click="confirmSign" color="#0E63E3" size="large" /> | ||||
| </view> | </view> | ||||
| <!-- #ifdef H5 --> | |||||
| <h5-signature :show="showH5Sign" @close="showH5Sign = false" @confirm="onH5SignConfirm" /> | |||||
| <!-- #endif --> | |||||
| </view> | </view> | ||||
| </template> | </template> | ||||
| @@ -68,6 +72,9 @@ const guardianIdCard = ref('') | |||||
| const guardianRelation = ref('') | const guardianRelation = ref('') | ||||
| const signImageUrl = ref('') | const signImageUrl = ref('') | ||||
| const submitting = ref(false) | const submitting = ref(false) | ||||
| // #ifdef H5 | |||||
| const showH5Sign = ref(false) | |||||
| // #endif | |||||
| const onSignatureResult = (data) => { | const onSignatureResult = (data) => { | ||||
| if (data.url) signImageUrl.value = data.url | if (data.url) signImageUrl.value = data.url | ||||
| @@ -75,17 +82,20 @@ const onSignatureResult = (data) => { | |||||
| onLoad((options) => { | onLoad((options) => { | ||||
| signType.value = options.type || 'privacy' | signType.value = options.type || 'privacy' | ||||
| // 回显上次填写的数据 | |||||
| if (options.amount) incomeAmount.value = decodeURIComponent(options.amount) | if (options.amount) incomeAmount.value = decodeURIComponent(options.amount) | ||||
| if (options.guardianName) guardianName.value = decodeURIComponent(options.guardianName) | if (options.guardianName) guardianName.value = decodeURIComponent(options.guardianName) | ||||
| if (options.guardianIdCard) guardianIdCard.value = decodeURIComponent(options.guardianIdCard) | if (options.guardianIdCard) guardianIdCard.value = decodeURIComponent(options.guardianIdCard) | ||||
| if (options.guardianRelation) guardianRelation.value = decodeURIComponent(options.guardianRelation) | if (options.guardianRelation) guardianRelation.value = decodeURIComponent(options.guardianRelation) | ||||
| // #ifdef MP-WEIXIN | |||||
| uni.$on('signatureResult', onSignatureResult) | uni.$on('signatureResult', onSignatureResult) | ||||
| // #endif | |||||
| loadContent() | loadContent() | ||||
| }) | }) | ||||
| onBeforeUnmount(() => { | onBeforeUnmount(() => { | ||||
| // #ifdef MP-WEIXIN | |||||
| uni.$off('signatureResult', onSignatureResult) | uni.$off('signatureResult', onSignatureResult) | ||||
| // #endif | |||||
| }) | }) | ||||
| const loadContent = async () => { | const loadContent = async () => { | ||||
| @@ -97,9 +107,21 @@ const loadContent = async () => { | |||||
| } catch (e) {} | } catch (e) {} | ||||
| } | } | ||||
| const goSignature = () => { | |||||
| const openSignature = () => { | |||||
| // #ifdef MP-WEIXIN | |||||
| uni.navigateTo({ url: '/pages/sign/signature' }) | 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 () => { | const confirmSign = async () => { | ||||
| if (!signImageUrl.value) { | if (!signImageUrl.value) { | ||||
| @@ -3,6 +3,7 @@ | |||||
| <view class="canvas-wrap"> | <view class="canvas-wrap"> | ||||
| <view class="hint-text"> | <view class="hint-text"> | ||||
| <text>请在此处手写签名</text> | <text>请在此处手写签名</text> | ||||
| <text class="hint-sub">请横屏书写</text> | |||||
| </view> | </view> | ||||
| <canvas id="signCanvas" canvas-id="signCanvas" type="2d" class="sign-canvas" disable-scroll | <canvas id="signCanvas" canvas-id="signCanvas" type="2d" class="sign-canvas" disable-scroll | ||||
| @touchstart="onTouchStart" @touchmove="onTouchMove" @touchend="onTouchEnd" /> | @touchstart="onTouchStart" @touchmove="onTouchMove" @touchend="onTouchEnd" /> | ||||
| @@ -181,6 +182,10 @@ const handleSave = () => { | |||||
| pointer-events: none; | pointer-events: none; | ||||
| white-space: nowrap; | white-space: nowrap; | ||||
| z-index: 0; | z-index: 0; | ||||
| display: flex; | |||||
| flex-direction: column; | |||||
| align-items: center; | |||||
| gap: 16rpx; | |||||
| text { | text { | ||||
| font-size: 48rpx; | font-size: 48rpx; | ||||
| @@ -188,6 +193,11 @@ const handleSave = () => { | |||||
| font-weight: 300; | font-weight: 300; | ||||
| letter-spacing: 16rpx; | letter-spacing: 16rpx; | ||||
| } | } | ||||
| .hint-sub { | |||||
| font-size: 28rpx; | |||||
| letter-spacing: 8rpx; | |||||
| } | |||||
| } | } | ||||
| .sign-canvas { | .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/, '') | |||||
| } | |||||
| } | |||||
| } | |||||
| }) | |||||