Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 12c2f25797 | |||
| 643f37b06e | |||
| dbfc8011c4 | |||
| f26d767cca | |||
| e6d61585dd | |||
| 1b5f3811b6 | |||
| 7c59d18596 | |||
| 57243dfdf3 | |||
| 85dca5c7a1 | |||
| c6bef45940 |
@@ -4,11 +4,19 @@ const { request } = require('./utils/api')
|
|||||||
|
|
||||||
App({
|
App({
|
||||||
onLaunch() {
|
onLaunch() {
|
||||||
// 自动静默登录:wx.login 获取 code,再请求后端接口换取 openid
|
// 优先从本地缓存恢复登录态,避免阻塞页面
|
||||||
|
const cached = wx.getStorageSync('userInfo')
|
||||||
|
if (cached && cached.openid) {
|
||||||
|
this.globalData.userInfo = cached
|
||||||
|
this.globalData.isLoggedIn = true
|
||||||
|
}
|
||||||
|
// 静默登录获取最新 session,不阻塞页面
|
||||||
this.silentLogin()
|
this.silentLogin()
|
||||||
},
|
},
|
||||||
|
|
||||||
silentLogin() {
|
silentLogin() {
|
||||||
|
this._loginPromise = new Promise((resolve) => {
|
||||||
|
this._loginResolve = resolve
|
||||||
wx.login({
|
wx.login({
|
||||||
success: (loginRes) => {
|
success: (loginRes) => {
|
||||||
if (loginRes.code) {
|
if (loginRes.code) {
|
||||||
@@ -23,6 +31,19 @@ App({
|
|||||||
this.handleLoginFail()
|
this.handleLoginFail()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 返回登录完成的 Promise,供页面 await 使用
|
||||||
|
* @param {boolean} useCache - 是否在已有缓存时立即 resolve
|
||||||
|
* @returns {Promise<object|null>}
|
||||||
|
*/
|
||||||
|
waitLogin(useCache = false) {
|
||||||
|
if (useCache && this.globalData.isLoggedIn) {
|
||||||
|
return Promise.resolve(this.globalData.userInfo)
|
||||||
|
}
|
||||||
|
return this._loginPromise || Promise.resolve(this.globalData.userInfo || null)
|
||||||
},
|
},
|
||||||
|
|
||||||
async loginWithCode(code) {
|
async loginWithCode(code) {
|
||||||
@@ -39,22 +60,25 @@ App({
|
|||||||
}
|
}
|
||||||
this.globalData.userInfo = userInfo
|
this.globalData.userInfo = userInfo
|
||||||
this.globalData.isLoggedIn = true
|
this.globalData.isLoggedIn = true
|
||||||
|
this.globalData.loginFailed = false
|
||||||
wx.setStorageSync('userInfo', userInfo)
|
wx.setStorageSync('userInfo', userInfo)
|
||||||
if (this.loginReadyCallback) {
|
if (this._loginResolve) this._loginResolve(userInfo)
|
||||||
this.loginReadyCallback(userInfo)
|
|
||||||
}
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('后端登录失败', err)
|
console.error('后端登录失败', err)
|
||||||
|
// 如果有缓存登录态,登录失败不立即清除,让页面继续使用缓存
|
||||||
|
if (!this.globalData.isLoggedIn) {
|
||||||
this.handleLoginFail()
|
this.handleLoginFail()
|
||||||
|
} else {
|
||||||
|
this.globalData.loginFailed = true
|
||||||
|
if (this._loginResolve) this._loginResolve(this.globalData.userInfo)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
handleLoginFail() {
|
handleLoginFail() {
|
||||||
this.globalData.isLoggedIn = false
|
this.globalData.isLoggedIn = false
|
||||||
this.globalData.loginFailed = true
|
this.globalData.loginFailed = true
|
||||||
if (this.loginReadyCallback) {
|
if (this._loginResolve) this._loginResolve(null)
|
||||||
this.loginReadyCallback(null)
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
globalData: {
|
globalData: {
|
||||||
|
|||||||
@@ -0,0 +1,134 @@
|
|||||||
|
// plate-input.js
|
||||||
|
// 车牌号输入组件:先选省份简称 → 再选城市代码 → 最后键盘输入号码
|
||||||
|
Component({
|
||||||
|
properties: {
|
||||||
|
value: { type: String, value: '' }
|
||||||
|
},
|
||||||
|
|
||||||
|
data: {
|
||||||
|
plateChars: [],
|
||||||
|
numValue: '',
|
||||||
|
inputFocus: false,
|
||||||
|
showProvince: false,
|
||||||
|
showCity: false,
|
||||||
|
hasValue: false,
|
||||||
|
provinces: [
|
||||||
|
'京', '津', '沪', '渝', '冀', '豫', '云', '辽', '黑', '湘',
|
||||||
|
'皖', '鲁', '新', '苏', '浙', '赣', '鄂', '桂', '甘', '晋',
|
||||||
|
'蒙', '陕', '吉', '闽', '贵', '粤', '川', '青', '藏', '琼', '宁'
|
||||||
|
],
|
||||||
|
cityLetters: [
|
||||||
|
'A', 'B', 'C', 'D', 'E', 'F', 'G',
|
||||||
|
'H', 'J', 'K', 'L', 'M', 'N', 'P',
|
||||||
|
'Q', 'R', 'S', 'T', 'U', 'V', 'W',
|
||||||
|
'X', 'Y', 'Z'
|
||||||
|
],
|
||||||
|
numSlots: [0, 1, 2, 3, 4, 5]
|
||||||
|
},
|
||||||
|
|
||||||
|
observers: {
|
||||||
|
'value': function (val) {
|
||||||
|
if (val && val !== this._lastEmitted) {
|
||||||
|
this._parseValue(val)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
lifetimes: {
|
||||||
|
attached() {
|
||||||
|
if (this.data.value) {
|
||||||
|
this._parseValue(this.data.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
_parseValue(val) {
|
||||||
|
const chars = val.split('')
|
||||||
|
const numValue = chars.slice(2).join('')
|
||||||
|
this.setData({
|
||||||
|
plateChars: chars,
|
||||||
|
numValue: numValue,
|
||||||
|
hasValue: chars.length > 0
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
_emit(chars) {
|
||||||
|
const value = chars.filter(Boolean).join('')
|
||||||
|
this._lastEmitted = value
|
||||||
|
this.triggerEvent('change', { value })
|
||||||
|
},
|
||||||
|
|
||||||
|
// 点击省份格
|
||||||
|
onProvinceTap() {
|
||||||
|
this.setData({ showProvince: true, showCity: false, inputFocus: false })
|
||||||
|
},
|
||||||
|
|
||||||
|
// 选择省份
|
||||||
|
selectProvince(e) {
|
||||||
|
const code = e.currentTarget.dataset.value
|
||||||
|
const chars = [code, this.data.plateChars[1]].filter(Boolean)
|
||||||
|
this.setData({ plateChars: chars, showProvince: false, showCity: true, hasValue: true })
|
||||||
|
this._emit(chars)
|
||||||
|
},
|
||||||
|
|
||||||
|
// 点击城市格
|
||||||
|
onCityTap() {
|
||||||
|
if (!this.data.plateChars[0]) {
|
||||||
|
this.setData({ showProvince: true })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.setData({ showCity: true, showProvince: false, inputFocus: false })
|
||||||
|
},
|
||||||
|
|
||||||
|
// 选择城市代码
|
||||||
|
selectCity(e) {
|
||||||
|
const letter = e.currentTarget.dataset.value
|
||||||
|
const numPart = this.data.plateChars.slice(2).join('')
|
||||||
|
const chars = [this.data.plateChars[0], letter].concat(numPart.split(''))
|
||||||
|
this.setData({
|
||||||
|
plateChars: chars,
|
||||||
|
showCity: false,
|
||||||
|
numValue: numPart,
|
||||||
|
inputFocus: true,
|
||||||
|
hasValue: true
|
||||||
|
})
|
||||||
|
this._emit(chars)
|
||||||
|
},
|
||||||
|
|
||||||
|
// 点击号码格 → 弹出键盘
|
||||||
|
onNumTap() {
|
||||||
|
if (!this.data.plateChars[0] || !this.data.plateChars[1]) {
|
||||||
|
this.setData({ showProvince: true })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.setData({ inputFocus: true, showProvince: false, showCity: false })
|
||||||
|
},
|
||||||
|
|
||||||
|
// 键盘输入号码
|
||||||
|
onNumInput(e) {
|
||||||
|
const raw = e.detail.value.toUpperCase().replace(/[^A-Z0-9]/g, '').slice(0, 6)
|
||||||
|
const chars = [this.data.plateChars[0], this.data.plateChars[1]].concat(raw.split(''))
|
||||||
|
this.setData({ plateChars: chars, numValue: raw, hasValue: true })
|
||||||
|
this._emit(chars)
|
||||||
|
},
|
||||||
|
|
||||||
|
// 关闭弹窗
|
||||||
|
hidePicker() {
|
||||||
|
this.setData({ showProvince: false, showCity: false })
|
||||||
|
},
|
||||||
|
|
||||||
|
noop() {},
|
||||||
|
|
||||||
|
// 清除车牌号
|
||||||
|
clearPlate() {
|
||||||
|
this.setData({
|
||||||
|
plateChars: [],
|
||||||
|
numValue: '',
|
||||||
|
inputFocus: false,
|
||||||
|
hasValue: false
|
||||||
|
})
|
||||||
|
this._emit([])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"component": true
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
<!--plate-input.wxml-->
|
||||||
|
<view class="plate-container">
|
||||||
|
<!-- 车牌号格子显示 -->
|
||||||
|
<view class="plate-body">
|
||||||
|
<view class="plate-cell {{plateChars[0] ? 'filled' : 'empty'}}" bindtap="onProvinceTap">
|
||||||
|
{{plateChars[0] || '省'}}
|
||||||
|
</view>
|
||||||
|
<view class="plate-cell {{plateChars[1] ? 'filled' : 'empty'}}" bindtap="onCityTap">
|
||||||
|
{{plateChars[1] || '市'}}
|
||||||
|
</view>
|
||||||
|
<view class="plate-sep">·</view>
|
||||||
|
<view
|
||||||
|
wx:for="{{numSlots}}" wx:key="*this"
|
||||||
|
class="plate-cell num {{plateChars[item + 2] ? 'filled' : 'empty'}} {{item >= 5 ? 'extra' : ''}}"
|
||||||
|
bindtap="onNumTap"
|
||||||
|
>
|
||||||
|
{{plateChars[item + 2] || (item < 5 ? '' : '新')}}
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 隐藏输入框(唤起键盘) -->
|
||||||
|
<input
|
||||||
|
class="plate-hidden-input"
|
||||||
|
type="text"
|
||||||
|
focus="{{inputFocus}}"
|
||||||
|
value="{{numValue}}"
|
||||||
|
maxlength="6"
|
||||||
|
bindinput="onNumInput"
|
||||||
|
adjust-position="{{true}}"
|
||||||
|
confirm-type="done"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- 清除按钮 -->
|
||||||
|
<view class="plate-clear" wx:if="{{hasValue}}" bindtap="clearPlate">清除</view>
|
||||||
|
|
||||||
|
<!-- 省份选择弹窗 -->
|
||||||
|
<view class="popup-mask" wx:if="{{showProvince}}" bindtap="hidePicker">
|
||||||
|
<view class="popup-panel" catchtap="noop">
|
||||||
|
<view class="popup-title">选择省份简称</view>
|
||||||
|
<scroll-view scroll-y class="popup-scroll">
|
||||||
|
<view class="popup-grid">
|
||||||
|
<view
|
||||||
|
class="popup-item"
|
||||||
|
wx:for="{{provinces}}" wx:key="*this"
|
||||||
|
data-value="{{item}}" bindtap="selectProvince"
|
||||||
|
>{{item}}</view>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
<view class="popup-cancel" bindtap="hidePicker">取消</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 城市选择弹窗 -->
|
||||||
|
<view class="popup-mask" wx:if="{{showCity}}" bindtap="hidePicker">
|
||||||
|
<view class="popup-panel" catchtap="noop">
|
||||||
|
<view class="popup-title">选择城市代码</view>
|
||||||
|
<scroll-view scroll-y class="popup-scroll">
|
||||||
|
<view class="popup-grid">
|
||||||
|
<view
|
||||||
|
class="popup-item"
|
||||||
|
wx:for="{{cityLetters}}" wx:key="*this"
|
||||||
|
data-value="{{item}}" bindtap="selectCity"
|
||||||
|
>{{item}}</view>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
<view class="popup-cancel" bindtap="hidePicker">取消</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
@@ -0,0 +1,154 @@
|
|||||||
|
/* plate-input.wxss */
|
||||||
|
|
||||||
|
.plate-container {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 车牌格子区域 */
|
||||||
|
.plate-body {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
background: #f5f7fa;
|
||||||
|
border: 2rpx solid #dce3ec;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
padding: 8rpx 16rpx;
|
||||||
|
height: 72rpx;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plate-cell {
|
||||||
|
width: 56rpx;
|
||||||
|
height: 56rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 30rpx;
|
||||||
|
border-radius: 8rpx;
|
||||||
|
margin: 0 4rpx;
|
||||||
|
flex-shrink: 0;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plate-cell.empty {
|
||||||
|
color: #b8c9db;
|
||||||
|
background: #eaf0f7;
|
||||||
|
font-size: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plate-cell.filled {
|
||||||
|
color: #2c3e50;
|
||||||
|
background: #ffffff;
|
||||||
|
border: 1rpx solid #dce3ec;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plate-cell.num {
|
||||||
|
width: 44rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plate-cell.num.extra {
|
||||||
|
border-style: dashed;
|
||||||
|
border-color: #c8d6e5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plate-sep {
|
||||||
|
font-size: 32rpx;
|
||||||
|
color: #b8c9db;
|
||||||
|
margin: 0 6rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 隐藏输入框(用于唤起键盘) */
|
||||||
|
.plate-hidden-input {
|
||||||
|
position: absolute;
|
||||||
|
left: -9999rpx;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 清除按钮 */
|
||||||
|
.plate-clear {
|
||||||
|
flex-shrink: 0;
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #ff4d4f;
|
||||||
|
padding: 8rpx 12rpx;
|
||||||
|
margin-left: 12rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 弹窗遮罩 */
|
||||||
|
.popup-mask {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
z-index: 999;
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 弹窗面板 */
|
||||||
|
.popup-panel {
|
||||||
|
width: 100%;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 24rpx 24rpx 0 0;
|
||||||
|
padding: 32rpx;
|
||||||
|
padding-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup-title {
|
||||||
|
font-size: 30rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #2c3e50;
|
||||||
|
margin-bottom: 24rpx;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup-scroll {
|
||||||
|
max-height: 400rpx;
|
||||||
|
padding-bottom: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup-grid {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 16rpx;
|
||||||
|
padding: 0 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup-item {
|
||||||
|
width: calc((100% - 96rpx) / 7);
|
||||||
|
height: 72rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: #f5f7fa;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
font-size: 30rpx;
|
||||||
|
color: #2c3e50;
|
||||||
|
border: 1rpx solid #dce3ec;
|
||||||
|
transition: all 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup-item:active {
|
||||||
|
background: #5b9bd5;
|
||||||
|
color: #fff;
|
||||||
|
border-color: #5b9bd5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup-cancel {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #999;
|
||||||
|
padding: 24rpx 0;
|
||||||
|
padding-bottom: calc(24rpx + env(safe-area-inset-bottom));
|
||||||
|
border-top: 1rpx solid #f0f0f0;
|
||||||
|
margin-top: 16rpx;
|
||||||
|
}
|
||||||
@@ -3,7 +3,7 @@ const { formatDate, appointmentDB } = require('../../utils/api')
|
|||||||
const app = getApp()
|
const app = getApp()
|
||||||
|
|
||||||
// 订阅消息模板ID
|
// 订阅消息模板ID
|
||||||
const SUBSCRIBE_TEMPLATE_ID = 'Csf_dJU7DhvVFt_03sphPPBCGlnmcWQSPhgqfxHZ5RQ'
|
const SUBSCRIBE_TEMPLATE_ID = 'EF5CDtuZwrGbt8iyOoi-sY7J6hZamX0AbWPLoK-qnEw'
|
||||||
|
|
||||||
Page({
|
Page({
|
||||||
data: {
|
data: {
|
||||||
@@ -15,41 +15,48 @@ Page({
|
|||||||
date: '',
|
date: '',
|
||||||
time: '',
|
time: '',
|
||||||
hostName: '',
|
hostName: '',
|
||||||
hostId: '',
|
area: '',
|
||||||
area: ''
|
plateNumber: ''
|
||||||
},
|
},
|
||||||
areas: ['A区-生产车间', 'B区-办公楼', 'C区-仓储区', 'D区-研发中心', 'E区-综合区'],
|
areaOptions: [],
|
||||||
areaMap: { 'A区-生产车间': 'A', 'B区-办公楼': 'B', 'C区-仓储区': 'C', 'D区-研发中心': 'D', 'E区-综合区': 'E' },
|
selectedAreas: {},
|
||||||
areaIndex: -1,
|
selectedAreasDisplay: '',
|
||||||
persons: [],
|
showAreaDropdown: false,
|
||||||
personNames: [],
|
|
||||||
personIndex: -1,
|
|
||||||
today: '',
|
today: '',
|
||||||
submitting: false
|
timeStart: '',
|
||||||
|
timeEnd: '',
|
||||||
|
submitting: false,
|
||||||
|
fieldErrors: {}
|
||||||
},
|
},
|
||||||
|
|
||||||
onLoad() {
|
onLoad() {
|
||||||
if (app.globalData.isLoggedIn) {
|
this._awaitLoginAndInit()
|
||||||
this.initPage()
|
},
|
||||||
} else if (app.globalData.loginFailed) {
|
|
||||||
this.showLoginTipAndGoBack()
|
async _awaitLoginAndInit() {
|
||||||
} else {
|
const userInfo = await app.waitLogin(true)
|
||||||
// 登录异步未完成,等待回调
|
|
||||||
app.loginReadyCallback = (userInfo) => {
|
|
||||||
app.loginReadyCallback = null
|
|
||||||
if (userInfo) {
|
if (userInfo) {
|
||||||
this.initPage()
|
this.initPage()
|
||||||
} else {
|
} else {
|
||||||
this.showLoginTipAndGoBack()
|
this.showLoginTipAndGoBack()
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
initPage() {
|
initPage() {
|
||||||
// 使用本地时区获取当天日期,用于 picker 最小日期限制
|
|
||||||
const today = formatDate(new Date())
|
const today = formatDate(new Date())
|
||||||
this.setData({ today })
|
this.setData({ today })
|
||||||
|
this.loadAreaOptions()
|
||||||
|
},
|
||||||
|
|
||||||
|
async loadAreaOptions() {
|
||||||
|
try {
|
||||||
|
const list = await appointmentDB.getDepartmentSelector()
|
||||||
|
const areaOptions = list.map(item => item.departmentName)
|
||||||
|
this.setData({ areaOptions })
|
||||||
|
} catch (err) {
|
||||||
|
console.error('获取拜访区域列表失败', err)
|
||||||
|
wx.showToast({ title: '获取拜访区域失败', icon: 'none' })
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
showLoginTipAndGoBack() {
|
showLoginTipAndGoBack() {
|
||||||
@@ -65,81 +72,99 @@ Page({
|
|||||||
|
|
||||||
onNameInput(e) {
|
onNameInput(e) {
|
||||||
this.setData({ 'form.name': e.detail.value })
|
this.setData({ 'form.name': e.detail.value })
|
||||||
|
this._clearFieldError('name')
|
||||||
},
|
},
|
||||||
onPhoneInput(e) {
|
onPhoneInput(e) {
|
||||||
this.setData({ 'form.phone': e.detail.value })
|
this.setData({ 'form.phone': e.detail.value })
|
||||||
|
this._clearFieldError('phone')
|
||||||
},
|
},
|
||||||
onCompanyInput(e) {
|
onCompanyInput(e) {
|
||||||
this.setData({ 'form.company': e.detail.value })
|
this.setData({ 'form.company': e.detail.value })
|
||||||
|
this._clearFieldError('company')
|
||||||
},
|
},
|
||||||
onReasonInput(e) {
|
onReasonInput(e) {
|
||||||
this.setData({ 'form.reason': e.detail.value })
|
this.setData({ 'form.reason': e.detail.value })
|
||||||
|
this._clearFieldError('reason')
|
||||||
},
|
},
|
||||||
onHostNameInput(e) {
|
onHostNameInput(e) {
|
||||||
this.setData({ 'form.hostName': e.detail.value })
|
this.setData({ 'form.hostName': e.detail.value })
|
||||||
},
|
},
|
||||||
|
onPlateNumberChange(e) {
|
||||||
|
this.setData({ 'form.plateNumber': e.detail.value })
|
||||||
|
},
|
||||||
|
|
||||||
onAreaChange(e) {
|
onNameBlur() {
|
||||||
const index = Number(e.detail.value)
|
if (!this.data.form.name.trim()) {
|
||||||
const areaName = this.data.areas[index]
|
this.setData({ 'fieldErrors.name': '姓名不能为空' })
|
||||||
const department = this.data.areaMap[areaName]
|
}
|
||||||
this.setData({
|
},
|
||||||
areaIndex: index,
|
onPhoneBlur() {
|
||||||
'form.area': areaName,
|
if (!this.data.form.phone.trim()) {
|
||||||
'form.hostId': '',
|
this.setData({ 'fieldErrors.phone': '手机号不能为空' })
|
||||||
'form.hostName': '',
|
}
|
||||||
persons: [],
|
},
|
||||||
personNames: [],
|
onCompanyBlur() {
|
||||||
personIndex: -1
|
if (!this.data.form.company.trim()) {
|
||||||
})
|
this.setData({ 'fieldErrors.company': '公司不能为空' })
|
||||||
if (department) {
|
}
|
||||||
this.loadPersons(department)
|
},
|
||||||
|
onReasonBlur() {
|
||||||
|
if (!this.data.form.reason.trim()) {
|
||||||
|
this.setData({ 'fieldErrors.reason': '来访事由不能为空' })
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
async loadPersons(department) {
|
_clearFieldError(field) {
|
||||||
wx.showLoading({ title: '加载被访人中...' })
|
if (this.data.fieldErrors[field]) {
|
||||||
try {
|
this.setData({ ['fieldErrors.' + field]: '' })
|
||||||
const list = await appointmentDB.getPersonSelector(department)
|
|
||||||
const persons = list.map(item => ({
|
|
||||||
personId: item.personId,
|
|
||||||
personName: item.personName
|
|
||||||
}))
|
|
||||||
this.setData({
|
|
||||||
persons,
|
|
||||||
personNames: persons.map(p => p.personName),
|
|
||||||
personIndex: -1
|
|
||||||
})
|
|
||||||
} catch (err) {
|
|
||||||
console.error('获取被访人列表失败', err)
|
|
||||||
wx.showToast({ title: '获取被访人列表失败', icon: 'none' })
|
|
||||||
} finally {
|
|
||||||
wx.hideLoading()
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
onPersonChange(e) {
|
toggleAreaDropdown() {
|
||||||
const index = Number(e.detail.value)
|
this.setData({ showAreaDropdown: !this.data.showAreaDropdown })
|
||||||
const person = this.data.persons[index]
|
},
|
||||||
if (person && person.personId) {
|
|
||||||
|
toggleArea(e) {
|
||||||
|
const value = e.currentTarget.dataset.value
|
||||||
|
const selectedAreas = { ...this.data.selectedAreas }
|
||||||
|
selectedAreas[value] = !selectedAreas[value]
|
||||||
|
const display = Object.keys(selectedAreas).filter(k => selectedAreas[k]).join('、')
|
||||||
this.setData({
|
this.setData({
|
||||||
personIndex: index,
|
selectedAreas,
|
||||||
'form.hostId': person.personId,
|
selectedAreasDisplay: display,
|
||||||
'form.hostName': person.personName
|
'form.area': display
|
||||||
})
|
})
|
||||||
}
|
this._clearFieldError('area')
|
||||||
},
|
},
|
||||||
|
|
||||||
onDateChange(e) {
|
onDateChange(e) {
|
||||||
this.setData({ 'form.date': e.detail.value })
|
this.setData({ 'form.date': e.detail.value })
|
||||||
},
|
},
|
||||||
|
|
||||||
onTimeChange(e) {
|
onTimeStartChange(e) {
|
||||||
this.setData({ 'form.time': e.detail.value })
|
this.setData({ timeStart: e.detail.value }, this._updateTimeRange)
|
||||||
|
},
|
||||||
|
|
||||||
|
onTimeEndChange(e) {
|
||||||
|
this.setData({ timeEnd: e.detail.value }, this._updateTimeRange)
|
||||||
|
},
|
||||||
|
|
||||||
|
_updateTimeRange() {
|
||||||
|
const { timeStart, timeEnd } = this.data
|
||||||
|
if (timeStart && timeEnd) {
|
||||||
|
if (timeEnd <= timeStart) {
|
||||||
|
this.setData({ 'form.time': '', 'fieldErrors.timeRange': '结束时间必须晚于开始时间' })
|
||||||
|
wx.showToast({ title: '结束时间必须晚于开始时间', icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.setData({ 'form.time': timeStart + '-' + timeEnd, 'fieldErrors.timeRange': '' })
|
||||||
|
} else {
|
||||||
|
this.setData({ 'form.time': '' })
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
validateForm() {
|
validateForm() {
|
||||||
const { name, phone, company, reason, date, time, hostName, area } = this.data.form
|
const { name, phone, company, reason, date, time, area } = this.data.form
|
||||||
if (!name.trim()) {
|
if (!name.trim()) {
|
||||||
wx.showToast({ title: '请输入访客姓名', icon: 'none' })
|
wx.showToast({ title: '请输入访客姓名', icon: 'none' })
|
||||||
return false
|
return false
|
||||||
@@ -164,10 +189,6 @@ Page({
|
|||||||
wx.showToast({ title: '请选择来访时间', icon: 'none' })
|
wx.showToast({ title: '请选择来访时间', icon: 'none' })
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if (!hostName.trim()) {
|
|
||||||
wx.showToast({ title: '请选择被访人', icon: 'none' })
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if (!area) {
|
if (!area) {
|
||||||
wx.showToast({ title: '请选择拜访区域', icon: 'none' })
|
wx.showToast({ title: '请选择拜访区域', icon: 'none' })
|
||||||
return false
|
return false
|
||||||
@@ -214,5 +235,18 @@ Page({
|
|||||||
console.error('提交预约失败', err)
|
console.error('提交预约失败', err)
|
||||||
wx.showToast({ title: '提交失败,请重试', icon: 'none' })
|
wx.showToast({ title: '提交失败,请重试', icon: 'none' })
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
onShareAppMessage(res) {
|
||||||
|
return {
|
||||||
|
title: '访客预约',
|
||||||
|
path: '/pages/index/index'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
onShareTimeline() {
|
||||||
|
return {
|
||||||
|
title: '访客预约'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
{
|
{
|
||||||
"usingComponents": {},
|
"usingComponents": {
|
||||||
|
"plate-input": "/components/plate-input/plate-input"
|
||||||
|
},
|
||||||
"navigationBarTitleText": "访客预约"
|
"navigationBarTitleText": "访客预约"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,31 +1,47 @@
|
|||||||
<!--appointment.wxml-->
|
<!--appointment.wxml-->
|
||||||
<view class="page">
|
<view class="page">
|
||||||
<!-- 预约人信息 -->
|
<!-- 来访人信息 -->
|
||||||
<view class="section">
|
<view class="section">
|
||||||
<view class="section-title">预约人信息</view>
|
<view class="section-title">来访人信息</view>
|
||||||
<view class="form-group">
|
<view class="form-group">
|
||||||
<text class="form-label">姓名</text>
|
<text class="form-label">姓名<text class="required">*</text></text>
|
||||||
<input class="form-input" placeholder="请输入访客姓名" value="{{form.name}}" bindinput="onNameInput" />
|
<view class="form-field-wrap">
|
||||||
|
<input class="form-input {{fieldErrors.name ? 'form-input-error' : ''}}" placeholder="请输入访客姓名" value="{{form.name}}" bindinput="onNameInput" bindblur="onNameBlur" />
|
||||||
|
<text wx:if="{{fieldErrors.name}}" class="form-error">{{fieldErrors.name}}</text>
|
||||||
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<view class="form-group">
|
<view class="form-group">
|
||||||
<text class="form-label">手机号</text>
|
<text class="form-label">手机号<text class="required">*</text></text>
|
||||||
<input class="form-input" type="number" maxlength="11" placeholder="请输入手机号" value="{{form.phone}}" bindinput="onPhoneInput" />
|
<view class="form-field-wrap">
|
||||||
|
<input class="form-input {{fieldErrors.phone ? 'form-input-error' : ''}}" type="number" maxlength="11" placeholder="请输入手机号" value="{{form.phone}}" bindinput="onPhoneInput" bindblur="onPhoneBlur" />
|
||||||
|
<text wx:if="{{fieldErrors.phone}}" class="form-error">{{fieldErrors.phone}}</text>
|
||||||
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<view class="form-group">
|
<view class="form-group">
|
||||||
<text class="form-label">公司</text>
|
<text class="form-label">公司<text class="required">*</text></text>
|
||||||
<input class="form-input" placeholder="请输入所属公司" value="{{form.company}}" bindinput="onCompanyInput" />
|
<view class="form-field-wrap">
|
||||||
|
<input class="form-input {{fieldErrors.company ? 'form-input-error' : ''}}" placeholder="请输入所属公司" value="{{form.company}}" bindinput="onCompanyInput" bindblur="onCompanyBlur" />
|
||||||
|
<text wx:if="{{fieldErrors.company}}" class="form-error">{{fieldErrors.company}}</text>
|
||||||
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<view class="form-group">
|
<view class="form-group">
|
||||||
<text class="form-label">来访事由</text>
|
<text class="form-label">来访事由<text class="required">*</text></text>
|
||||||
<input class="form-input" placeholder="请输入来访事由" value="{{form.reason}}" bindinput="onReasonInput" />
|
<view class="form-field-wrap">
|
||||||
|
<input class="form-input {{fieldErrors.reason ? 'form-input-error' : ''}}" placeholder="请输入来访事由" value="{{form.reason}}" bindinput="onReasonInput" bindblur="onReasonBlur" />
|
||||||
|
<text wx:if="{{fieldErrors.reason}}" class="form-error">{{fieldErrors.reason}}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="form-group">
|
||||||
|
<text class="form-label">车牌号</text>
|
||||||
|
<plate-input value="{{form.plateNumber}}" bindchange="onPlateNumberChange" />
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 预约时间 -->
|
<!-- 来访时间 -->
|
||||||
<view class="section">
|
<view class="section">
|
||||||
<view class="section-title">预约时间</view>
|
<view class="section-title">来访时间</view>
|
||||||
<view class="form-group">
|
<view class="form-group">
|
||||||
<text class="form-label">来访日期</text>
|
<text class="form-label">来访日期<text class="required">*</text></text>
|
||||||
<picker class="form-picker-wrap" mode="date" value="{{form.date}}" start="{{today}}" bindchange="onDateChange">
|
<picker class="form-picker-wrap" mode="date" value="{{form.date}}" start="{{today}}" bindchange="onDateChange">
|
||||||
<view class="form-picker">
|
<view class="form-picker">
|
||||||
<text class="{{form.date ? 'picker-value' : 'picker-placeholder'}}">{{form.date || '请选择日期'}}</text>
|
<text class="{{form.date ? 'picker-value' : 'picker-placeholder'}}">{{form.date || '请选择日期'}}</text>
|
||||||
@@ -33,42 +49,62 @@
|
|||||||
</view>
|
</view>
|
||||||
</picker>
|
</picker>
|
||||||
</view>
|
</view>
|
||||||
<view class="form-group">
|
<view class="form-group form-group-time">
|
||||||
<text class="form-label">来访时间</text>
|
<text class="form-label">来访时段<text class="required">*</text></text>
|
||||||
<picker class="form-picker-wrap" mode="time" value="{{form.time}}" bindchange="onTimeChange">
|
<view class="form-field-wrap">
|
||||||
<view class="form-picker">
|
<view class="time-range">
|
||||||
<text class="{{form.time ? 'picker-value' : 'picker-placeholder'}}">{{form.time || '请选择时间'}}</text>
|
<picker class="time-picker-wrap" mode="time" value="{{timeStart}}" bindchange="onTimeStartChange">
|
||||||
<text class="picker-arrow">›</text>
|
<view class="time-picker">
|
||||||
|
<text class="{{timeStart ? 'picker-value' : 'picker-placeholder'}}">{{timeStart || '开始时间'}}</text>
|
||||||
</view>
|
</view>
|
||||||
</picker>
|
</picker>
|
||||||
|
<text class="time-range-sep">至</text>
|
||||||
|
<picker class="time-picker-wrap" mode="time" value="{{timeEnd}}" bindchange="onTimeEndChange">
|
||||||
|
<view class="time-picker">
|
||||||
|
<text class="{{timeEnd ? 'picker-value' : 'picker-placeholder'}}">{{timeEnd || '结束时间'}}</text>
|
||||||
|
</view>
|
||||||
|
</picker>
|
||||||
|
</view>
|
||||||
|
<text wx:if="{{fieldErrors.timeRange}}" class="form-error">{{fieldErrors.timeRange}}</text>
|
||||||
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 被访人信息 -->
|
<!-- 被访人信息 -->
|
||||||
<view class="section">
|
<view class="section">
|
||||||
<view class="section-title">被访人信息</view>
|
<view class="section-title">被访人信息</view>
|
||||||
<view class="form-group">
|
<view class="form-group form-group-area">
|
||||||
<text class="form-label">拜访区域</text>
|
<text class="form-label">拜访区域<text class="required">*</text></text>
|
||||||
<picker class="form-picker-wrap" range="{{areas}}" value="{{areaIndex}}" bindchange="onAreaChange">
|
<view class="form-field-wrap">
|
||||||
<view class="form-picker">
|
<view class="multi-select-wrap">
|
||||||
<text class="{{areaIndex >= 0 ? 'picker-value' : 'picker-placeholder'}}">{{areaIndex >= 0 ? areas[areaIndex] : '请选择拜访区域'}}</text>
|
<view class="multi-select-trigger" bindtap="toggleAreaDropdown">
|
||||||
<text class="picker-arrow">›</text>
|
<text class="{{selectedAreasDisplay || 'picker-placeholder'}}">{{selectedAreasDisplay || '请选择拜访区域'}}</text>
|
||||||
|
<text class="picker-arrow {{showAreaDropdown ? 'arrow-up' : ''}}">›</text>
|
||||||
|
</view>
|
||||||
|
<view wx:if="{{showAreaDropdown}}" class="multi-select-dropdown">
|
||||||
|
<view wx:for="{{areaOptions}}" wx:key="*this" class="checkbox-item" bindtap="toggleArea" data-value="{{item}}">
|
||||||
|
<view class="checkbox {{selectedAreas[item] ? 'checkbox-checked' : ''}}">
|
||||||
|
<text wx:if="{{selectedAreas[item]}}" class="checkbox-icon">✓</text>
|
||||||
|
</view>
|
||||||
|
<text class="checkbox-label">{{item}}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<text wx:if="{{fieldErrors.area}}" class="form-error">{{fieldErrors.area}}</text>
|
||||||
</view>
|
</view>
|
||||||
</picker>
|
|
||||||
</view>
|
</view>
|
||||||
<view class="form-group">
|
<view class="form-group">
|
||||||
<text class="form-label">被访人</text>
|
<text class="form-label">被访人</text>
|
||||||
<picker wx:if="{{personNames.length > 0}}" class="form-picker-wrap" range="{{personNames}}" value="{{personIndex}}" bindchange="onPersonChange">
|
<view class="form-field-wrap">
|
||||||
<view class="form-picker">
|
<input class="form-input" placeholder="请输入被访人" value="{{form.hostName}}" bindinput="onHostNameInput" />
|
||||||
<text class="{{personIndex >= 0 ? 'picker-value' : 'picker-placeholder'}}">{{personIndex >= 0 ? personNames[personIndex] : '请选择被访人'}}</text>
|
</view>
|
||||||
<text class="picker-arrow">›</text>
|
</view>
|
||||||
</view>
|
<view class="form-group">
|
||||||
</picker>
|
<text class="form-label">接待人</text>
|
||||||
<view wx:else class="form-picker">
|
<view class="form-field-wrap">
|
||||||
<text class="picker-placeholder">请先选择拜访区域</text>
|
<input class="form-input form-input-disabled" value="金梦婷" disabled />
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 提交按钮 -->
|
<!-- 提交按钮 -->
|
||||||
|
|||||||
@@ -37,6 +37,47 @@ page {
|
|||||||
border-bottom: none;
|
border-bottom: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.form-group-time {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-field-wrap {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-range {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-picker-wrap {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-picker {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 88rpx;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-picker .picker-arrow {
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
font-size: 32rpx;
|
||||||
|
color: #b8c9db;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-range-sep {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #b8c9db;
|
||||||
|
padding: 0 12rpx;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.form-label {
|
.form-label {
|
||||||
width: 160rpx;
|
width: 160rpx;
|
||||||
font-size: 28rpx;
|
font-size: 28rpx;
|
||||||
@@ -44,6 +85,11 @@ page {
|
|||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.required {
|
||||||
|
color: #ff4d4f;
|
||||||
|
margin-left: 4rpx;
|
||||||
|
}
|
||||||
|
|
||||||
.form-input {
|
.form-input {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
font-size: 28rpx;
|
font-size: 28rpx;
|
||||||
@@ -77,6 +123,76 @@ page {
|
|||||||
color: #b8c9db;
|
color: #b8c9db;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.form-input-disabled {
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group-area {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.multi-select-wrap {
|
||||||
|
position: relative;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.multi-select-trigger {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
height: 88rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.multi-select-dropdown {
|
||||||
|
position: absolute;
|
||||||
|
top: 88rpx;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
background: #fff;
|
||||||
|
border: 1rpx solid #e8eef5;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.1);
|
||||||
|
z-index: 100;
|
||||||
|
padding: 8rpx 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 20rpx 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox {
|
||||||
|
width: 40rpx;
|
||||||
|
height: 40rpx;
|
||||||
|
border: 2rpx solid #d0d8e4;
|
||||||
|
border-radius: 8rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-right: 16rpx;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-checked {
|
||||||
|
background: #5b9bd5;
|
||||||
|
border-color: #5b9bd5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-icon {
|
||||||
|
color: #fff;
|
||||||
|
font-size: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-label {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #2c3e50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.arrow-up {
|
||||||
|
transform: rotate(90deg);
|
||||||
|
}
|
||||||
|
|
||||||
.submit-wrap {
|
.submit-wrap {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
@@ -117,3 +233,13 @@ page {
|
|||||||
color: #94aec5;
|
color: #94aec5;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.form-input-error {
|
||||||
|
color: #ff4d4f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-error {
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: #ff4d4f;
|
||||||
|
padding-top: 4rpx;
|
||||||
|
}
|
||||||
|
|||||||
+26
-25
@@ -11,45 +11,33 @@ Page({
|
|||||||
},
|
},
|
||||||
|
|
||||||
onLoad() {
|
onLoad() {
|
||||||
|
// 首页不阻塞,直接渲染;有缓存登录态时立即加载数据
|
||||||
if (app.globalData.isLoggedIn) {
|
if (app.globalData.isLoggedIn) {
|
||||||
this.onLoginReady()
|
this.setData({ isLoggedIn: true })
|
||||||
} else if (app.globalData.loginFailed) {
|
|
||||||
this.onLoginFailed()
|
|
||||||
} else {
|
|
||||||
app.loginReadyCallback = (userInfo) => {
|
|
||||||
if (userInfo) {
|
|
||||||
this.onLoginReady()
|
|
||||||
} else {
|
|
||||||
this.onLoginFailed()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
onShow() {
|
|
||||||
if (app.globalData.isLoggedIn) {
|
|
||||||
this.loadLatestRecord()
|
this.loadLatestRecord()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
onLoginReady() {
|
async onShow() {
|
||||||
|
// 每次显示时等待登录完成,再加载最新数据
|
||||||
|
const userInfo = await app.waitLogin(true)
|
||||||
|
if (userInfo) {
|
||||||
this.setData({ isLoggedIn: true, loginFailed: false })
|
this.setData({ isLoggedIn: true, loginFailed: false })
|
||||||
this.loadLatestRecord()
|
this.loadLatestRecord()
|
||||||
},
|
} else {
|
||||||
|
|
||||||
onLoginFailed() {
|
|
||||||
this.setData({ isLoggedIn: false, loginFailed: true })
|
this.setData({ isLoggedIn: false, loginFailed: true })
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
onRetry() {
|
async onRetry() {
|
||||||
this.setData({ loginFailed: false })
|
this.setData({ loginFailed: false })
|
||||||
app.silentLogin()
|
app.silentLogin()
|
||||||
app.loginReadyCallback = (userInfo) => {
|
const userInfo = await app.waitLogin()
|
||||||
if (userInfo) {
|
if (userInfo) {
|
||||||
this.onLoginReady()
|
this.setData({ isLoggedIn: true, loginFailed: false })
|
||||||
|
this.loadLatestRecord()
|
||||||
} else {
|
} else {
|
||||||
this.onLoginFailed()
|
this.setData({ loginFailed: true })
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -85,5 +73,18 @@ Page({
|
|||||||
|
|
||||||
showQrcode(e) {
|
showQrcode(e) {
|
||||||
this.selectComponent('#qrcodeModal').show(e.currentTarget.dataset.id)
|
this.selectComponent('#qrcodeModal').show(e.currentTarget.dataset.id)
|
||||||
|
},
|
||||||
|
|
||||||
|
onShareAppMessage(res) {
|
||||||
|
return {
|
||||||
|
title: '访客预约',
|
||||||
|
path: '/pages/index/index'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
onShareTimeline() {
|
||||||
|
return {
|
||||||
|
title: '访客预约'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
+5
-12
@@ -1,16 +1,9 @@
|
|||||||
<!--index.wxml-->
|
<!--index.wxml-->
|
||||||
<view class="page">
|
<view class="page">
|
||||||
<!-- loading 遮罩 -->
|
<!-- 登录失败提示条(不遮挡页面) -->
|
||||||
<view class="loading-mask" wx:if="{{!isLoggedIn && !loginFailed}}">
|
<view class="login-fail-bar" wx:if="{{loginFailed && !isLoggedIn}}">
|
||||||
<view class="loading-spinner"></view>
|
<text>网络异常,无法获取身份信息</text>
|
||||||
<text class="loading-text">正在获取身份信息...</text>
|
<text class="retry-link" bindtap="onRetry">重试</text>
|
||||||
</view>
|
|
||||||
|
|
||||||
<!-- 登录失败 -->
|
|
||||||
<view class="loading-mask" wx:if="{{loginFailed}}">
|
|
||||||
<text class="fail-icon">⚠️</text>
|
|
||||||
<text class="fail-text">网络异常,请重试</text>
|
|
||||||
<view class="retry-btn" bindtap="onRetry">重新加载</view>
|
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view class="header">
|
<view class="header">
|
||||||
@@ -26,7 +19,7 @@
|
|||||||
</view>
|
</view>
|
||||||
<view class="action-info">
|
<view class="action-info">
|
||||||
<text class="action-title">访客预约</text>
|
<text class="action-title">访客预约</text>
|
||||||
<text class="action-desc">选择预约时间和预约人,提交预约信息</text>
|
<text class="action-desc">选择来访时间和预约人,提交预约信息</text>
|
||||||
</view>
|
</view>
|
||||||
<text class="action-arrow">›</text>
|
<text class="action-arrow">›</text>
|
||||||
</view>
|
</view>
|
||||||
|
|||||||
+12
-55
@@ -21,67 +21,24 @@ page {
|
|||||||
line-height: 1;
|
line-height: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* loading 遮罩 */
|
/* 登录失败提示条(不遮挡页面) */
|
||||||
.loading-mask {
|
.login-fail-bar {
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
background: #f0f5fa;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: space-between;
|
||||||
z-index: 999;
|
background: #fff3e0;
|
||||||
}
|
color: #e6a23c;
|
||||||
|
font-size: 24rpx;
|
||||||
.loading-spinner {
|
padding: 16rpx 24rpx;
|
||||||
width: 64rpx;
|
border-radius: 12rpx;
|
||||||
height: 64rpx;
|
|
||||||
border: 6rpx solid #dbeafe;
|
|
||||||
border-top: 6rpx solid #5b9bd5;
|
|
||||||
border-radius: 50%;
|
|
||||||
animation: spin 0.8s linear infinite;
|
|
||||||
margin-bottom: 24rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes spin {
|
|
||||||
from { transform: rotate(0deg); }
|
|
||||||
to { transform: rotate(360deg); }
|
|
||||||
}
|
|
||||||
|
|
||||||
.loading-text {
|
|
||||||
font-size: 28rpx;
|
|
||||||
color: #7f8fa6;
|
|
||||||
letter-spacing: 2rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 登录失败 */
|
|
||||||
.fail-icon {
|
|
||||||
font-size: 80rpx;
|
|
||||||
margin-bottom: 20rpx;
|
margin-bottom: 20rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fail-text {
|
.login-fail-bar .retry-link {
|
||||||
font-size: 28rpx;
|
color: #5b9bd5;
|
||||||
color: #7f8fa6;
|
|
||||||
margin-bottom: 32rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.retry-btn {
|
|
||||||
font-size: 28rpx;
|
|
||||||
color: #fff;
|
|
||||||
background: linear-gradient(135deg, #5b9bd5, #4a8bc2);
|
|
||||||
padding: 16rpx 56rpx;
|
|
||||||
border-radius: 40rpx;
|
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
letter-spacing: 2rpx;
|
flex-shrink: 0;
|
||||||
box-shadow: 0 4rpx 16rpx rgba(91, 155, 213, 0.3);
|
margin-left: 20rpx;
|
||||||
}
|
|
||||||
|
|
||||||
.retry-btn:active {
|
|
||||||
opacity: 0.85;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-title {
|
.header-title {
|
||||||
|
|||||||
@@ -7,20 +7,44 @@ Page({
|
|||||||
records: [],
|
records: [],
|
||||||
filteredRecords: [],
|
filteredRecords: [],
|
||||||
currentTab: 'all',
|
currentTab: 'all',
|
||||||
loading: true
|
loading: true,
|
||||||
|
isLoggedIn: false,
|
||||||
|
loginFailed: false
|
||||||
},
|
},
|
||||||
|
|
||||||
onLoad() {
|
onLoad() {
|
||||||
|
this._awaitLoginAndLoad()
|
||||||
|
},
|
||||||
|
|
||||||
|
async _awaitLoginAndLoad() {
|
||||||
|
const userInfo = await app.waitLogin(true)
|
||||||
|
if (userInfo) {
|
||||||
|
this.setData({ isLoggedIn: true, loginFailed: false })
|
||||||
this.loadRecords()
|
this.loadRecords()
|
||||||
|
} else {
|
||||||
|
this.setData({ isLoggedIn: false, loginFailed: true, loading: false })
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
onShow() {
|
onShow() {
|
||||||
// 仅从预约页返回时刷新,避免 onLoad + onShow 双重加载
|
// 已加载过且已登录时刷新(从预约页返回等场景)
|
||||||
if (this.data._loaded) {
|
if (this.data._loaded && app.globalData.isLoggedIn) {
|
||||||
this.loadRecords()
|
this.loadRecords()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async onRetry() {
|
||||||
|
this.setData({ loginFailed: false, loading: true })
|
||||||
|
app.silentLogin()
|
||||||
|
const userInfo = await app.waitLogin()
|
||||||
|
if (userInfo) {
|
||||||
|
this.setData({ isLoggedIn: true, loginFailed: false })
|
||||||
|
this.loadRecords()
|
||||||
|
} else {
|
||||||
|
this.setData({ loginFailed: true })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
async loadRecords() {
|
async loadRecords() {
|
||||||
this.setData({ loading: true })
|
this.setData({ loading: true })
|
||||||
try {
|
try {
|
||||||
@@ -85,5 +109,18 @@ Page({
|
|||||||
|
|
||||||
showQrcode(e) {
|
showQrcode(e) {
|
||||||
this.selectComponent('#qrcodeModal').show(e.currentTarget.dataset.id)
|
this.selectComponent('#qrcodeModal').show(e.currentTarget.dataset.id)
|
||||||
|
},
|
||||||
|
|
||||||
|
onShareAppMessage(res) {
|
||||||
|
return {
|
||||||
|
title: '访客预约',
|
||||||
|
path: '/pages/index/index'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
onShareTimeline() {
|
||||||
|
return {
|
||||||
|
title: '访客预约'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 记录列表 -->
|
<!-- 记录列表 -->
|
||||||
<view class="record-list" wx:if="{{!loading && filteredRecords.length > 0}}">
|
<view class="record-list" wx:if="{{!loading && !loginFailed && filteredRecords.length > 0}}">
|
||||||
<view class="record-card" wx:for="{{filteredRecords}}" wx:key="_id">
|
<view class="record-card" wx:for="{{filteredRecords}}" wx:key="_id">
|
||||||
<view class="record-header">
|
<view class="record-header">
|
||||||
<view wx:if="{{item.status === 'approved'}}" class="qrcode-btn" bindtap="showQrcode" data-id="{{item._id}}">
|
<view wx:if="{{item.status === 'approved'}}" class="qrcode-btn" bindtap="showQrcode" data-id="{{item._id}}">
|
||||||
@@ -45,7 +45,7 @@
|
|||||||
<text class="record-value">{{item.reason}}</text>
|
<text class="record-value">{{item.reason}}</text>
|
||||||
</view>
|
</view>
|
||||||
<view class="record-row">
|
<view class="record-row">
|
||||||
<text class="record-label">预约时间</text>
|
<text class="record-label">来访时间</text>
|
||||||
<text class="record-value">{{item.date}} {{item.time}}</text>
|
<text class="record-value">{{item.date}} {{item.time}}</text>
|
||||||
</view>
|
</view>
|
||||||
<view class="record-row">
|
<view class="record-row">
|
||||||
@@ -70,8 +70,15 @@
|
|||||||
<text class="empty-text">加载中...</text>
|
<text class="empty-text">加载中...</text>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
|
<!-- 登录失败 -->
|
||||||
|
<view class="empty" wx:if="{{loginFailed}}">
|
||||||
|
<text class="empty-icon">⚠️</text>
|
||||||
|
<text class="empty-text">登录失败,请重试</text>
|
||||||
|
<view class="empty-btn" bindtap="onRetry">重新登录</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
<!-- 空状态 -->
|
<!-- 空状态 -->
|
||||||
<view class="empty" wx:if="{{!loading && filteredRecords.length === 0}}">
|
<view class="empty" wx:if="{{!loading && !loginFailed && filteredRecords.length === 0}}">
|
||||||
<text class="empty-icon">📭</text>
|
<text class="empty-icon">📭</text>
|
||||||
<text class="empty-text">暂无预约记录</text>
|
<text class="empty-text">暂无预约记录</text>
|
||||||
<view class="empty-btn" bindtap="goAppointment">去预约</view>
|
<view class="empty-btn" bindtap="goAppointment">去预约</view>
|
||||||
|
|||||||
@@ -94,5 +94,18 @@ Page({
|
|||||||
this.setData({ verifying: false })
|
this.setData({ verifying: false })
|
||||||
wx.showToast({ title: err.message || '核销失败,请稍后重试', icon: 'none' })
|
wx.showToast({ title: err.message || '核销失败,请稍后重试', icon: 'none' })
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
onShareAppMessage(res) {
|
||||||
|
return {
|
||||||
|
title: '访客预约',
|
||||||
|
path: '/pages/index/index'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
onShareTimeline() {
|
||||||
|
return {
|
||||||
|
title: '访客预约'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -36,6 +36,10 @@
|
|||||||
<text class="detail-label">来访事由</text>
|
<text class="detail-label">来访事由</text>
|
||||||
<text class="detail-value">{{record.reason}}</text>
|
<text class="detail-value">{{record.reason}}</text>
|
||||||
</view>
|
</view>
|
||||||
|
<view class="detail-row" wx:if="{{record.plateNumber}}">
|
||||||
|
<text class="detail-label">车牌号</text>
|
||||||
|
<text class="detail-value">{{record.plateNumber}}</text>
|
||||||
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view class="detail-section">
|
<view class="detail-section">
|
||||||
|
|||||||
+1
-1
@@ -36,6 +36,6 @@
|
|||||||
"tabIndent": "auto",
|
"tabIndent": "auto",
|
||||||
"tabSize": 2
|
"tabSize": 2
|
||||||
},
|
},
|
||||||
"appid": "wx50fe0c5c28dd3060",
|
"appid": "wx4286144359eeafe5",
|
||||||
"simulatorPluginLibVersion": {}
|
"simulatorPluginLibVersion": {}
|
||||||
}
|
}
|
||||||
@@ -122,7 +122,9 @@ const appointmentDB = {
|
|||||||
visitDate: data.date,
|
visitDate: data.date,
|
||||||
visitTime: data.time,
|
visitTime: data.time,
|
||||||
hostName: data.hostName,
|
hostName: data.hostName,
|
||||||
|
personId: data.personId,
|
||||||
area: data.area,
|
area: data.area,
|
||||||
|
plateNumber: data.plateNumber,
|
||||||
openid: data.openid
|
openid: data.openid
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -185,6 +187,18 @@ const appointmentDB = {
|
|||||||
return mapApiRecord(data) || null
|
return mapApiRecord(data) || null
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取被访部门/区域列表
|
||||||
|
* @returns {Promise<Array>} 部门列表
|
||||||
|
*/
|
||||||
|
async getDepartmentSelector() {
|
||||||
|
const data = await request({
|
||||||
|
url: BASE_URL + API.DEPARTMENT_SELECTOR,
|
||||||
|
method: 'GET'
|
||||||
|
})
|
||||||
|
return data || []
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取被访人列表
|
* 获取被访人列表
|
||||||
* @param {string} department - 部门/区域编码
|
* @param {string} department - 部门/区域编码
|
||||||
|
|||||||
+8
-6
@@ -2,12 +2,10 @@
|
|||||||
|
|
||||||
// 环境地址配置
|
// 环境地址配置
|
||||||
const ENV_CONFIG = {
|
const ENV_CONFIG = {
|
||||||
// 正式版
|
release: 'https://smartguest.bmser.com:8091',
|
||||||
// release: 'https://xcx.yun.588580.xyz',
|
trial: 'https://smartguest.bmser.com:8091',
|
||||||
trial: 'https://qywx.yun.588580.xyz',
|
// develop: 'https://192.168.123.76:8080'
|
||||||
// 开发版 & 体验版
|
develop: 'https://10.50.13.185:8091'
|
||||||
develop: 'http://172.16.60.235:8080'
|
|
||||||
// develop: 'http://10.50.13.191:8080'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 自动判断当前运行环境
|
// 自动判断当前运行环境
|
||||||
@@ -15,6 +13,9 @@ function getBaseUrl() {
|
|||||||
const accountInfo = wx.getAccountInfoSync()
|
const accountInfo = wx.getAccountInfoSync()
|
||||||
const envVersion = accountInfo.miniProgram.envVersion
|
const envVersion = accountInfo.miniProgram.envVersion
|
||||||
// release = 正式版, develop = 开发版, trial = 体验版
|
// release = 正式版, develop = 开发版, trial = 体验版
|
||||||
|
if (envVersion === 'release') {
|
||||||
|
return ENV_CONFIG.release
|
||||||
|
}
|
||||||
return envVersion === 'trial' ? ENV_CONFIG.trial : ENV_CONFIG.develop
|
return envVersion === 'trial' ? ENV_CONFIG.trial : ENV_CONFIG.develop
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -30,6 +31,7 @@ const API = {
|
|||||||
APPOINTMENT_DETAIL: '/api/wx-mini/appointment/detail',
|
APPOINTMENT_DETAIL: '/api/wx-mini/appointment/detail',
|
||||||
WXACODE: '/api/wx-mini/wxacode',
|
WXACODE: '/api/wx-mini/wxacode',
|
||||||
PERSON_SELECTOR: '/api/wx-mini/appointment/person/selector',
|
PERSON_SELECTOR: '/api/wx-mini/appointment/person/selector',
|
||||||
|
DEPARTMENT_SELECTOR: '/api/wx-mini/appointment/department/selector',
|
||||||
NOTIFY_HOST: '/visitor/notify-host'
|
NOTIFY_HOST: '/visitor/notify-host'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user