commit 9de2bdee2e6e4f10090a96c4f3e4106eb7a8be12 Author: chenglijuan Date: Sat Apr 18 22:15:11 2026 +0800 Initial Commit diff --git a/.cloudbase/container/debug.json b/.cloudbase/container/debug.json new file mode 100644 index 0000000..0d44458 --- /dev/null +++ b/.cloudbase/container/debug.json @@ -0,0 +1 @@ +{"containers":[],"config":{}} \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..14ea590 --- /dev/null +++ b/.gitignore @@ -0,0 +1,14 @@ +# Windows +[Dd]esktop.ini +Thumbs.db +$RECYCLE.BIN/ + +# macOS +.DS_Store +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes + +# Node.js +node_modules/ diff --git a/app.js b/app.js new file mode 100644 index 0000000..0ffe35d --- /dev/null +++ b/app.js @@ -0,0 +1,45 @@ +// app.js +App({ + onLaunch() { + if (!wx.cloud) { + console.error('请使用 2.2.3 或以上的基础库以使用云能力') + } else { + wx.cloud.init({ + traceUser: true + }) + } + + // 自动静默登录:调用云函数获取 openid + this.silentLogin() + }, + + silentLogin() { + wx.cloud.callFunction({ + name: 'login', + data: {}, + success: (res) => { + const openid = res.result.openid + const userInfo = res.result.userInfo || {} + userInfo.openid = openid + + this.globalData.userInfo = userInfo + this.globalData.isLoggedIn = true + wx.setStorageSync('userInfo', userInfo) + + // 通知当前页面刷新 + if (this.loginReadyCallback) { + this.loginReadyCallback(userInfo) + } + }, + fail: (err) => { + console.error('静默登录失败', err) + this.globalData.isLoggedIn = false + } + }) + }, + + globalData: { + userInfo: null, + isLoggedIn: false + } +}) diff --git a/app.json b/app.json new file mode 100644 index 0000000..8d646d3 --- /dev/null +++ b/app.json @@ -0,0 +1,16 @@ +{ + "pages": [ + "pages/index/index", + "pages/appointment/appointment", + "pages/records/records" + ], + "window": { + "navigationBarTextStyle": "white", + "navigationBarTitleText": "访客预约", + "navigationBarBackgroundColor": "#1890ff" + }, + "style": "v2", + "componentFramework": "glass-easel", + "sitemapLocation": "sitemap.json", + "lazyCodeLoading": "requiredComponents" +} diff --git a/app.wxss b/app.wxss new file mode 100644 index 0000000..b5f04c8 --- /dev/null +++ b/app.wxss @@ -0,0 +1,17 @@ +/**app.wxss**/ +page { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; + font-size: 28rpx; + color: #333; + background-color: #f5f7fa; +} + +.container { + height: 100%; + display: flex; + flex-direction: column; + align-items: center; + justify-content: space-between; + padding: 200rpx 0; + box-sizing: border-box; +} diff --git a/cloudfunctions/login/index.js b/cloudfunctions/login/index.js new file mode 100644 index 0000000..13c64ab --- /dev/null +++ b/cloudfunctions/login/index.js @@ -0,0 +1,54 @@ +// 云函数入口文件 +const cloud = require('wx-server-sdk') + +cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }) + +const db = cloud.database() + +// 云函数入口函数 +exports.main = async (event, context) => { + const wxContext = cloud.getWXContext() + const openid = wxContext.OPENID + + // 查找是否已存在用户 + const userRes = await db.collection('users').where({ _openid: openid }).get() + + let user + if (userRes.data.length > 0) { + // 已存在,更新登录时间(如果传了头像昵称也一并更新) + user = userRes.data[0] + const updateData = { + lastLoginTime: db.serverDate() + } + if (event.avatarUrl) updateData.avatarUrl = event.avatarUrl + if (event.nickName) updateData.nickName = event.nickName + await db.collection('users').doc(user._id).update({ data: updateData }) + // 合并最新数据返回 + if (event.avatarUrl) user.avatarUrl = event.avatarUrl + if (event.nickName) user.nickName = event.nickName + } else { + // 新用户,创建记录 + const addRes = await db.collection('users').add({ + data: { + _openid: openid, + avatarUrl: event.avatarUrl || '', + nickName: event.nickName || '', + createTime: db.serverDate(), + lastLoginTime: db.serverDate() + } + }) + user = { + _id: addRes._id, + _openid: openid, + avatarUrl: event.avatarUrl || '', + nickName: event.nickName || '' + } + } + + return { + openid: openid, + appid: wxContext.APPID, + unionid: wxContext.UNIONID, + userInfo: user + } +} diff --git a/cloudfunctions/login/package.json b/cloudfunctions/login/package.json new file mode 100644 index 0000000..e610802 --- /dev/null +++ b/cloudfunctions/login/package.json @@ -0,0 +1,9 @@ +{ + "name": "login", + "version": "1.0.0", + "description": "登录云函数", + "main": "index.js", + "dependencies": { + "wx-server-sdk": "~2.6.3" + } +} diff --git a/pages/appointment/appointment.js b/pages/appointment/appointment.js new file mode 100644 index 0000000..86ed315 --- /dev/null +++ b/pages/appointment/appointment.js @@ -0,0 +1,135 @@ +// appointment.js +const { formatDate } = require('../../utils/util') +const { appointmentDB } = require('../../utils/cloud') +const app = getApp() + +Page({ + data: { + form: { + name: '', + phone: '', + company: '', + reason: '', + date: '', + time: '', + hostName: '', + area: '' + }, + areas: ['A区-生产车间', 'B区-办公楼', 'C区-仓储区', 'D区-研发中心', 'E区-综合区'], + areaIndex: -1, + today: '', + submitting: false + }, + + onLoad() { + if (!app.globalData.isLoggedIn) { + wx.showToast({ title: '请先登录', icon: 'none' }) + setTimeout(() => { + wx.navigateBack() + }, 1500) + return + } + const today = formatDate(new Date()) + this.setData({ today }) + }, + + onNameInput(e) { + this.setData({ 'form.name': e.detail.value }) + }, + onPhoneInput(e) { + this.setData({ 'form.phone': e.detail.value }) + }, + onCompanyInput(e) { + this.setData({ 'form.company': e.detail.value }) + }, + onReasonInput(e) { + this.setData({ 'form.reason': e.detail.value }) + }, + onHostNameInput(e) { + this.setData({ 'form.hostName': e.detail.value }) + }, + + onAreaChange(e) { + const index = Number(e.detail.value) + this.setData({ + areaIndex: index, + 'form.area': this.data.areas[index] + }) + }, + + onDateChange(e) { + this.setData({ 'form.date': e.detail.value }) + }, + + onTimeChange(e) { + this.setData({ 'form.time': e.detail.value }) + }, + + validateForm() { + const { name, phone, company, reason, date, time, hostName, area } = this.data.form + if (!name.trim()) { + wx.showToast({ title: '请输入访客姓名', icon: 'none' }) + return false + } + if (!phone.trim() || phone.length !== 11) { + wx.showToast({ title: '请输入正确的手机号', icon: 'none' }) + return false + } + if (!company.trim()) { + wx.showToast({ title: '请输入所属公司', icon: 'none' }) + return false + } + if (!reason.trim()) { + wx.showToast({ title: '请输入来访事由', icon: 'none' }) + return false + } + if (!date) { + wx.showToast({ title: '请选择来访日期', icon: 'none' }) + return false + } + if (!time) { + wx.showToast({ title: '请选择来访时间', icon: 'none' }) + return false + } + if (!hostName.trim()) { + wx.showToast({ title: '请输入被访人姓名', icon: 'none' }) + return false + } + if (!area) { + wx.showToast({ title: '请选择拜访区域', icon: 'none' }) + return false + } + return true + }, + + async onSubmit() { + if (!this.validateForm()) return + if (this.data.submitting) return + + this.setData({ submitting: true }) + + try { + // 写入云数据库 + await appointmentDB.create(this.data.form) + + this.setData({ submitting: false }) + + wx.showModal({ + title: '提交成功', + content: '您的预约已提交,请等待审核', + showCancel: false, + confirmText: '查看记录', + confirmColor: '#1890ff', + success: () => { + wx.redirectTo({ + url: '/pages/records/records' + }) + } + }) + } catch (err) { + this.setData({ submitting: false }) + console.error('提交预约失败', err) + wx.showToast({ title: '提交失败,请重试', icon: 'none' }) + } + } +}) diff --git a/pages/appointment/appointment.json b/pages/appointment/appointment.json new file mode 100644 index 0000000..c69f2e7 --- /dev/null +++ b/pages/appointment/appointment.json @@ -0,0 +1,4 @@ +{ + "usingComponents": {}, + "navigationBarTitleText": "访客预约" +} diff --git a/pages/appointment/appointment.wxml b/pages/appointment/appointment.wxml new file mode 100644 index 0000000..69dcda8 --- /dev/null +++ b/pages/appointment/appointment.wxml @@ -0,0 +1,71 @@ + + + + + 预约人信息 + + 姓名 + + + + 手机号 + + + + 公司 + + + + 来访事由 + + + + + + + 预约时间 + + 来访日期 + + + {{form.date || '请选择日期'}} + + + + + + 来访时间 + + + {{form.time || '请选择时间'}} + + + + + + + + + 被访人信息 + + 被访人 + + + + 拜访区域 + + + {{areaIndex >= 0 ? areas[areaIndex] : '请选择拜访区域'}} + + + + + + + + + + + diff --git a/pages/appointment/appointment.wxss b/pages/appointment/appointment.wxss new file mode 100644 index 0000000..587cc2f --- /dev/null +++ b/pages/appointment/appointment.wxss @@ -0,0 +1,114 @@ +/**appointment.wxss**/ +page { + background-color: #f5f7fa; + min-height: 100vh; +} + +.page { + padding: 24rpx 32rpx 160rpx; +} + +.section { + background: #fff; + border-radius: 20rpx; + padding: 32rpx; + margin-bottom: 24rpx; + box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04); +} + +.section-title { + font-size: 30rpx; + font-weight: 600; + color: #1a1a1a; + margin-bottom: 24rpx; + padding-left: 16rpx; + border-left: 6rpx solid #1890ff; +} + +.form-group { + display: flex; + align-items: center; + min-height: 88rpx; + border-bottom: 1rpx solid #f0f0f0; +} + +.form-group:last-child { + border-bottom: none; +} + +.form-label { + width: 160rpx; + font-size: 28rpx; + color: #333; + flex-shrink: 0; +} + +.form-input { + flex: 1; + font-size: 28rpx; + color: #1a1a1a; + height: 88rpx; +} + +.form-picker-wrap { + flex: 1; +} + +.form-picker { + display: flex; + align-items: center; + justify-content: space-between; + height: 88rpx; +} + +.picker-value { + font-size: 28rpx; + color: #1a1a1a; +} + +.picker-placeholder { + font-size: 28rpx; + color: #ccc; +} + +.picker-arrow { + font-size: 32rpx; + color: #ccc; +} + +.submit-wrap { + position: fixed; + bottom: 0; + left: 0; + right: 0; + padding: 20rpx 32rpx; + padding-bottom: calc(20rpx + env(safe-area-inset-bottom)); + background: #fff; + box-shadow: 0 -2rpx 12rpx rgba(0, 0, 0, 0.06); +} + +.submit-btn { + width: 100%; + height: 88rpx; + display: flex; + align-items: center; + justify-content: center; + background: #1890ff; + color: #fff; + font-size: 32rpx; + font-weight: 600; + border-radius: 44rpx; + border: none; + padding: 0; + margin: 0; + line-height: 1; +} + +.submit-btn::after { + border: none; +} + +.submit-btn[disabled] { + background: #b3d9ff; + color: #fff; +} diff --git a/pages/index/index.js b/pages/index/index.js new file mode 100644 index 0000000..5dae5dc --- /dev/null +++ b/pages/index/index.js @@ -0,0 +1,75 @@ +// index.js +const { appointmentDB } = require('../../utils/cloud') +const app = getApp() + +Page({ + data: { + isLoggedIn: false, + latestRecord: null + }, + + onLoad() { + if (app.globalData.isLoggedIn) { + this.onLoginReady() + } else { + app.loginReadyCallback = () => { + this.onLoginReady() + } + } + }, + + onShow() { + if (app.globalData.isLoggedIn) { + this.loadLatestRecord() + } + }, + + onLoginReady() { + this.setData({ isLoggedIn: true }) + this.loadLatestRecord() + }, + + async loadLatestRecord() { + if (!this.data.isLoggedIn) { + this.setData({ latestRecord: null }) + return + } + try { + const openid = app.globalData.userInfo.openid + const record = await appointmentDB.getLatest(openid) + if (record) { + this.setData({ latestRecord: this.formatRecord(record) }) + } else { + this.setData({ latestRecord: null }) + } + } catch (err) { + console.error('加载最新预约失败', err) + this.setData({ latestRecord: null }) + } + }, + + formatRecord(record) { + const date = record.createTime + let createTimeStr = '' + if (date) { + if (typeof date === 'object' && date.$date) { + createTimeStr = new Date(date.$date).toLocaleString('zh-CN') + } else { + createTimeStr = new Date(date).toLocaleString('zh-CN') + } + } + return { ...record, createTime: createTimeStr } + }, + + goAppointment() { + wx.navigateTo({ + url: '/pages/appointment/appointment' + }) + }, + + goRecords() { + wx.navigateTo({ + url: '/pages/records/records' + }) + } +}) diff --git a/pages/index/index.json b/pages/index/index.json new file mode 100644 index 0000000..e02c929 --- /dev/null +++ b/pages/index/index.json @@ -0,0 +1,4 @@ +{ + "usingComponents": {}, + "navigationBarTitleText": "访客预约系统" +} diff --git a/pages/index/index.wxml b/pages/index/index.wxml new file mode 100644 index 0000000..e5dd5d7 --- /dev/null +++ b/pages/index/index.wxml @@ -0,0 +1,71 @@ + + + + + + 正在获取身份信息... + + + + 🏢 + 访客预约系统 + 协能工厂区域访问管理 + + + + + + 📅 + + + 访客预约 + 选择预约时间和预约人,提交预约信息 + + + + + + + 📋 + + + 预约记录 + 查看预约记录、进度及取消预约 + + + + + + + + 最新预约 + {{latestRecord.statusText}} + + + + 访客 + {{latestRecord.name}} + + + 时间 + {{latestRecord.date}} {{latestRecord.time}} + + + 区域 + {{latestRecord.area}} + + + 被访人 + {{latestRecord.hostName}} + + + + 查看全部记录 › + + + + + + 协能工厂 · 访客管理 + + diff --git a/pages/index/index.wxss b/pages/index/index.wxss new file mode 100644 index 0000000..d78fba4 --- /dev/null +++ b/pages/index/index.wxss @@ -0,0 +1,228 @@ +/**index.wxss**/ +page { + background-color: #f5f7fa; + min-height: 100vh; +} + +.page { + padding: 40rpx 32rpx; +} + +.header { + display: flex; + flex-direction: column; + align-items: center; + padding: 60rpx 0 40rpx; +} + +.header-icon { + font-size: 100rpx; + margin-bottom: 24rpx; + line-height: 1; +} + +/* loading 遮罩 */ +.loading-mask { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: #f5f7fa; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + z-index: 999; +} + +.loading-spinner { + width: 64rpx; + height: 64rpx; + border: 6rpx solid #e0e0e0; + border-top: 6rpx solid #1890ff; + 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: #999; +} + +.header-title { + font-size: 44rpx; + font-weight: 700; + color: #1a1a1a; + margin-bottom: 12rpx; +} + +.header-subtitle { + font-size: 28rpx; + color: #999; +} + +/* 功能卡片 */ +.action-list { + margin-top: 24rpx; +} + +.action-card { + display: flex; + align-items: center; + background: #fff; + border-radius: 20rpx; + padding: 36rpx 32rpx; + margin-bottom: 24rpx; + box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.06); +} + +.action-card:active { + background: #f0f0f0; +} + +.action-icon-wrap { + width: 88rpx; + height: 88rpx; + border-radius: 20rpx; + display: flex; + align-items: center; + justify-content: center; + margin-right: 28rpx; + flex-shrink: 0; +} + +.action-icon-blue { + background: rgba(24, 144, 255, 0.1); +} + +.action-icon-green { + background: rgba(82, 196, 26, 0.1); +} + +.action-icon-text { + font-size: 44rpx; +} + +.action-info { + flex: 1; + display: flex; + flex-direction: column; +} + +.action-title { + font-size: 32rpx; + font-weight: 600; + color: #1a1a1a; + margin-bottom: 8rpx; +} + +.action-desc { + font-size: 24rpx; + color: #999; +} + +.action-arrow { + font-size: 40rpx; + color: #ccc; + margin-left: 16rpx; +} + +/* 最新预约卡片 */ +.latest-card { + background: #fff; + border-radius: 20rpx; + padding: 28rpx 32rpx; + margin-top: 8rpx; + box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.06); +} + +.latest-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 20rpx; + padding-bottom: 16rpx; + border-bottom: 1rpx solid #f0f0f0; +} + +.latest-title { + font-size: 28rpx; + font-weight: 600; + color: #1a1a1a; +} + +.status-tag { + font-size: 22rpx; + padding: 4rpx 16rpx; + border-radius: 16rpx; + font-weight: 500; +} + +.status-pending { + background: rgba(250, 173, 20, 0.1); + color: #faad14; +} + +.status-approved { + background: rgba(82, 196, 26, 0.1); + color: #52c41a; +} + +.status-rejected { + background: rgba(255, 77, 79, 0.1); + color: #ff4d4f; +} + +.status-cancelled { + background: rgba(0, 0, 0, 0.04); + color: #999; +} + +.latest-body { + margin-bottom: 8rpx; +} + +.latest-row { + display: flex; + justify-content: space-between; + align-items: center; + padding: 8rpx 0; +} + +.latest-label { + font-size: 24rpx; + color: #999; +} + +.latest-value { + font-size: 24rpx; + color: #333; +} + +.latest-footer { + padding-top: 16rpx; + border-top: 1rpx solid #f0f0f0; + text-align: center; +} + +.latest-link { + font-size: 24rpx; + color: #1890ff; +} + +.footer { + margin-top: 120rpx; + text-align: center; +} + +.footer-text { + font-size: 24rpx; + color: #ccc; +} diff --git a/pages/records/records.js b/pages/records/records.js new file mode 100644 index 0000000..c586669 --- /dev/null +++ b/pages/records/records.js @@ -0,0 +1,105 @@ +// records.js +const { appointmentDB } = require('../../utils/cloud') +const app = getApp() + +Page({ + data: { + records: [], + filteredRecords: [], + currentTab: 'all', + loading: true + }, + + onLoad() { + if (!app.globalData.isLoggedIn) { + wx.showToast({ title: '请先登录', icon: 'none' }) + setTimeout(() => { + wx.navigateBack() + }, 1500) + return + } + this.loadRecords() + }, + + onShow() { + if (app.globalData.isLoggedIn) { + this.loadRecords() + } + }, + + async loadRecords() { + this.setData({ loading: true }) + try { + const openid = app.globalData.userInfo.openid + const records = await appointmentDB.getList(openid) + + // 格式化时间 + const formatted = records.map(item => { + const date = item.createTime + let createTimeStr = '' + if (date) { + if (typeof date === 'object' && date.$date) { + createTimeStr = new Date(date.$date).toLocaleString('zh-CN') + } else { + createTimeStr = new Date(date).toLocaleString('zh-CN') + } + } + return { ...item, createTime: createTimeStr } + }) + + this.setData({ records: formatted, loading: false }) + this.filterRecords() + } catch (err) { + console.error('加载预约记录失败', err) + this.setData({ records: [], loading: false }) + this.filterRecords() + } + }, + + switchTab(e) { + const tab = e.currentTarget.dataset.tab + this.setData({ currentTab: tab }) + this.filterRecords() + }, + + filterRecords() { + const { records, currentTab } = this.data + let filtered = records + if (currentTab !== 'all') { + filtered = records.filter(item => item.status === currentTab) + } + this.setData({ filteredRecords: filtered }) + }, + + onCancel(e) { + const id = e.currentTarget.dataset.id + wx.showModal({ + title: '确认取消', + content: '确定要取消该预约吗?', + confirmColor: '#ff4d4f', + success: async (res) => { + if (res.confirm) { + try { + const openid = app.globalData.userInfo.openid + const success = await appointmentDB.cancel(id, openid) + if (success) { + wx.showToast({ title: '已取消预约', icon: 'success' }) + this.loadRecords() + } else { + wx.showToast({ title: '取消失败,无权限或状态不允许', icon: 'none' }) + } + } catch (err) { + console.error('取消预约失败', err) + wx.showToast({ title: '操作失败', icon: 'none' }) + } + } + } + }) + }, + + goAppointment() { + wx.navigateTo({ + url: '/pages/appointment/appointment' + }) + } +}) diff --git a/pages/records/records.json b/pages/records/records.json new file mode 100644 index 0000000..7bc86a0 --- /dev/null +++ b/pages/records/records.json @@ -0,0 +1,4 @@ +{ + "usingComponents": {}, + "navigationBarTitleText": "预约记录" +} diff --git a/pages/records/records.wxml b/pages/records/records.wxml new file mode 100644 index 0000000..6cc673b --- /dev/null +++ b/pages/records/records.wxml @@ -0,0 +1,74 @@ + + + + + + 全部 + + + 待审核 + + + 已通过 + + + 已拒绝 + + + + + + + + {{item.id}} + + {{item.statusText}} + + + + + + 访客姓名 + {{item.name}} + + + 手机号码 + {{item.phone}} + + + 来访事由 + {{item.reason}} + + + 预约时间 + {{item.date}} {{item.time}} + + + 拜访区域 + {{item.area}} + + + 被访人 + {{item.hostName}} + + + + + 取消预约 + + + + + + + + 加载中... + + + + + 📭 + 暂无预约记录 + 去预约 + + diff --git a/pages/records/records.wxss b/pages/records/records.wxss new file mode 100644 index 0000000..3e6afc7 --- /dev/null +++ b/pages/records/records.wxss @@ -0,0 +1,174 @@ +/**records.wxss**/ +page { + background-color: #f5f7fa; + min-height: 100vh; +} + +.page { + padding-bottom: calc(32rpx + env(safe-area-inset-bottom)); +} + +/* 筛选标签 */ +.tabs { + display: flex; + background: #fff; + padding: 16rpx 32rpx; + box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.04); + position: sticky; + top: 0; + z-index: 10; +} + +.tab { + flex: 1; + text-align: center; + font-size: 28rpx; + color: #666; + padding: 16rpx 0; + position: relative; +} + +.tab-active { + color: #1890ff; + font-weight: 600; +} + +.tab-active::after { + content: ''; + position: absolute; + bottom: 0; + left: 50%; + transform: translateX(-50%); + width: 48rpx; + height: 6rpx; + background: #1890ff; + border-radius: 3rpx; +} + +/* 记录列表 */ +.record-list { + padding: 24rpx 32rpx; +} + +.record-card { + background: #fff; + border-radius: 20rpx; + padding: 32rpx; + margin-bottom: 24rpx; + box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04); +} + +.record-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 24rpx; + padding-bottom: 20rpx; + border-bottom: 1rpx solid #f0f0f0; +} + +.record-id { + font-size: 24rpx; + color: #999; +} + +.status-tag { + font-size: 24rpx; + padding: 6rpx 20rpx; + border-radius: 20rpx; + font-weight: 500; +} + +.status-pending { + background: rgba(250, 173, 20, 0.1); + color: #faad14; +} + +.status-approved { + background: rgba(82, 196, 26, 0.1); + color: #52c41a; +} + +.status-rejected { + background: rgba(255, 77, 79, 0.1); + color: #ff4d4f; +} + +.status-cancelled { + background: rgba(0, 0, 0, 0.04); + color: #999; +} + +.record-body { + margin-bottom: 8rpx; +} + +.record-row { + display: flex; + justify-content: space-between; + align-items: center; + padding: 12rpx 0; +} + +.record-label { + font-size: 26rpx; + color: #999; + flex-shrink: 0; +} + +.record-value { + font-size: 26rpx; + color: #333; + text-align: right; + margin-left: 32rpx; +} + +.record-footer { + padding-top: 20rpx; + border-top: 1rpx solid #f0f0f0; + display: flex; + justify-content: flex-end; +} + +.cancel-btn { + font-size: 26rpx; + color: #ff4d4f; + padding: 12rpx 32rpx; + border: 1rpx solid #ff4d4f; + border-radius: 32rpx; +} + +.cancel-btn:active { + background: #fff1f0; +} + +/* 空状态 */ +.empty { + display: flex; + flex-direction: column; + align-items: center; + padding-top: 200rpx; +} + +.empty-icon { + font-size: 120rpx; + margin-bottom: 32rpx; +} + +.empty-text { + font-size: 28rpx; + color: #999; + margin-bottom: 48rpx; +} + +.empty-btn { + font-size: 28rpx; + color: #fff; + background: #1890ff; + padding: 20rpx 64rpx; + border-radius: 40rpx; +} + +.empty-btn:active { + background: #096dd9; +} diff --git a/project.config.json b/project.config.json new file mode 100644 index 0000000..5209e1e --- /dev/null +++ b/project.config.json @@ -0,0 +1,42 @@ +{ + "compileType": "miniprogram", + "libVersion": "trial", + "packOptions": { + "ignore": [], + "include": [] + }, + "setting": { + "coverView": true, + "es6": true, + "postcss": true, + "minified": true, + "enhance": true, + "showShadowRootInWxmlPanel": true, + "packNpmRelationList": [], + "babelSetting": { + "ignore": [], + "disablePlugins": [], + "outputPath": "" + }, + "compileWorklet": false, + "uglifyFileName": false, + "uploadWithSourceMap": true, + "packNpmManually": false, + "minifyWXSS": true, + "minifyWXML": true, + "localPlugins": false, + "condition": false, + "swc": false, + "disableSWC": true, + "disableUseStrict": false, + "useCompilerPlugins": false + }, + "condition": {}, + "editorSetting": { + "tabIndent": "auto", + "tabSize": 2 + }, + "appid": "wx50fe0c5c28dd3060", + "cloudfunctionRoot": "cloudfunctions/", + "simulatorPluginLibVersion": {} +} \ No newline at end of file diff --git a/project.private.config.json b/project.private.config.json new file mode 100644 index 0000000..3698571 --- /dev/null +++ b/project.private.config.json @@ -0,0 +1,22 @@ +{ + "description": "项目私有配置文件。此文件中的内容将覆盖 project.config.json 中的相同字段。项目的改动优先同步到此文件中。详见文档:https://developers.weixin.qq.com/miniprogram/dev/devtools/projectconfig.html", + "projectname": "p1", + "setting": { + "compileHotReLoad": true, + "urlCheck": true, + "coverView": true, + "lazyloadPlaceholderEnable": false, + "skylineRenderEnable": false, + "preloadBackgroundData": false, + "autoAudits": false, + "useApiHook": true, + "showShadowRootInWxmlPanel": true, + "useStaticServer": false, + "useLanDebug": false, + "showES6CompileOption": false, + "bigPackageSizeSupport": false, + "checkInvalidKey": true, + "ignoreDevUnusedFiles": true + }, + "libVersion": "3.15.2" +} \ No newline at end of file diff --git a/sitemap.json b/sitemap.json new file mode 100644 index 0000000..ca02add --- /dev/null +++ b/sitemap.json @@ -0,0 +1,7 @@ +{ + "desc": "关于本文件的更多信息,请参考文档 https://developers.weixin.qq.com/miniprogram/dev/framework/sitemap.html", + "rules": [{ + "action": "allow", + "page": "*" + }] +} \ No newline at end of file diff --git a/todo.md b/todo.md new file mode 100644 index 0000000..0f643bd --- /dev/null +++ b/todo.md @@ -0,0 +1,14 @@ +# 项目目标 +开发微信小程序,实现一个简单的访客预约系统,用于协能工厂区域的访问管理。 +预约功能页面可以选择预约时间和预约人,并提交预约信息。 +预约记录页面可以查看预约记录,和预约进度结果和取消预约。 +目前不需要进行登录和注册功能,可以直接使用微信小程序的登录和注册功能。 +# 系统功能需求 +### 主要页面 +1. 访客预约操作页面,小程序的首页可以选择跳转预约功能页面和选择跳转进入预约记录页面。 +2. 预约功能页面,选择预约时间和预约人,并提交预约信息。 + +3. 预约记录页面,可以查看预约记录,和预约进度结果和取消预约。 + +### 编码原则 +目前不需要后端信息,使用硬编码模拟数据 \ No newline at end of file diff --git a/utils/cloud.js b/utils/cloud.js new file mode 100644 index 0000000..b385c3e --- /dev/null +++ b/utils/cloud.js @@ -0,0 +1,138 @@ +// 云数据库操作工具库 + +const db = wx.cloud.database() +const _ = db.command + +/** + * 用户相关操作 + */ +const userDB = { + /** + * 通过 openId 查找用户,不存在则创建 + * @param {string} openid + * @param {object} userInfo - { avatarUrl, nickName } + * @returns {Promise} 用户记录 + */ + async loginOrCreate(openid, userInfo) { + const res = await db.collection('users').where({ _openid: openid }).get() + if (res.data.length > 0) { + // 已存在,更新头像昵称 + const existing = res.data[0] + await db.collection('users').doc(existing._id).update({ + data: { + avatarUrl: userInfo.avatarUrl, + nickName: userInfo.nickName, + lastLoginTime: db.serverDate() + } + }) + return { ...existing, avatarUrl: userInfo.avatarUrl, nickName: userInfo.nickName } + } else { + // 不存在,新建 + const addRes = await db.collection('users').add({ + data: { + _openid: openid, + avatarUrl: userInfo.avatarUrl, + nickName: userInfo.nickName, + createTime: db.serverDate(), + lastLoginTime: db.serverDate() + } + }) + return { + _id: addRes._id, + _openid: openid, + avatarUrl: userInfo.avatarUrl, + nickName: userInfo.nickName + } + } + }, + + /** + * 根据 openId 获取用户信息 + * @param {string} openid + * @returns {Promise} + */ + async getByOpenId(openid) { + const res = await db.collection('users').where({ _openid: openid }).get() + return res.data.length > 0 ? res.data[0] : null + } +} + +/** + * 预约相关操作 + */ +const appointmentDB = { + /** + * 创建预约 + * @param {object} data - 预约表单数据 + * @returns {Promise} 新记录 _id + */ + async create(data) { + const res = await db.collection('appointments').add({ + data: { + ...data, + status: 'pending', + statusText: '待审核', + createTime: db.serverDate() + } + }) + return res._id + }, + + /** + * 获取当前用户的预约列表(按创建时间倒序) + * @param {string} openid + * @returns {Promise} + */ + async getList(openid) { + const res = await db.collection('appointments') + .where({ _openid: openid }) + .orderBy('createTime', 'desc') + .get() + return res.data + }, + + /** + * 获取当前用户最新一条预约 + * @param {string} openid + * @returns {Promise} + */ + async getLatest(openid) { + const res = await db.collection('appointments') + .where({ _openid: openid }) + .orderBy('createTime', 'desc') + .limit(1) + .get() + return res.data.length > 0 ? res.data[0] : null + }, + + /** + * 取消预约 + * @param {string} id - 预约记录 _id + * @param {string} openid - 当前用户 openid,用于权限校验 + * @returns {Promise} 是否成功 + */ + async cancel(id, openid) { + // 先校验该预约属于当前用户 + const res = await db.collection('appointments').doc(id).get() + if (res.data._openid !== openid) { + return false + } + if (res.data.status !== 'pending') { + return false + } + await db.collection('appointments').doc(id).update({ + data: { + status: 'cancelled', + statusText: '已取消' + } + }) + return true + } +} + +module.exports = { + db, + _, + userDB, + appointmentDB +} diff --git a/utils/util.js b/utils/util.js new file mode 100644 index 0000000..624788c --- /dev/null +++ b/utils/util.js @@ -0,0 +1,27 @@ +const formatTime = date => { + const year = date.getFullYear() + const month = date.getMonth() + 1 + const day = date.getDate() + const hour = date.getHours() + const minute = date.getMinutes() + const second = date.getSeconds() + + return `${[year, month, day].map(formatNumber).join('/')} ${[hour, minute, second].map(formatNumber).join(':')}` +} + +const formatDate = date => { + const year = date.getFullYear() + const month = date.getMonth() + 1 + const day = date.getDate() + return `${year}-${formatNumber(month)}-${formatNumber(day)}` +} + +const formatNumber = n => { + n = n.toString() + return n[1] ? n : `0${n}` +} + +module.exports = { + formatTime, + formatDate +}