This commit is contained in:
dzc committed 2025-08-11 16:05:28 +08:00
commit cc329e243d
30 files changed
+1878 -1607

No files matched your search

+3 -27
View File
@@ -85,35 +85,11 @@ const tabbar: TabbarItem[] = [
auth: ["merchant"],
},
{
pagePath: "pages/tabbar/invitation",
text: "访客邀约",
iconPath: "/static/tabbar/invitation.png",
selectedIconPath: "/static/tabbar/invitation-select.png",
launchParam: ["invitation"],
auth: [],
},
{
pagePath: "pages/tabbar/invitationRecord",
text: "邀约记录",
iconPath: "/static/tabbar/invitation.png",
selectedIconPath: "/static/tabbar/invitation-select.png",
launchParam: ["invitation"],
auth: [],
},
{
pagePath: "pages/tabbar/message",
text: "通知公告",
iconPath: "/static/tabbar/message.png",
selectedIconPath: "/static/tabbar/message-select.png",
launchParam: ["message"],
auth: [],
},
{
pagePath: "pages/integrated/index",
pagePath: "pages/tabbar/integrated",
text: "综合服务",
iconPath: "/static/tabbar/integrated.png",
selectedIconPath: "/static/tabbar/integrated-select.png",
launchParam: [],
launchParam: ["integrated"],
auth: [],
},
{
@@ -121,7 +97,7 @@ const tabbar: TabbarItem[] = [
text: "我的",
iconPath: "/static/tabbar/my.png",
selectedIconPath: "/static/tabbar/my-select.png",
launchParam: ["canteen", "selfMerchant", "onlineShop", "coopMerchant", "invitation"],
launchParam: ["canteen", "selfMerchant", "onlineShop", "coopMerchant", "integrated"],
auth: ["user", "merchant", "doorAdmin"],
},
]
+14 -57
View File
@@ -73,23 +73,9 @@
}
},
{
"path": "pages/tabbar/invitation",
"path": "pages/tabbar/integrated",
"style": {
"navigationBarTitleText": "访客",
"disableScroll": true
}
},
{
"path": "pages/tabbar/invitationRecord",
"style": {
"navigationBarTitleText": "访客记录",
"disableScroll": true
}
},
{
"path": "pages/tabbar/message",
"style": {
"navigationBarTitleText": "通知公告",
"navigationBarTitleText": "综合服务",
"disableScroll": true
}
},
@@ -414,24 +400,13 @@
}
]
},
{
"root": "pages/invitation",
"pages": [
{
"path": "detail",
"style": {
"navigationBarTitleText": "访客详情"
}
}
]
},
{
"root": "pages/thirdApp",
"pages": [
{
"path": "thirdAppList",
"path": "happyMall",
"style": {
"navigationBarTitleText": "第三方应用"
"navigationBarTitleText": "美好商城"
}
}
]
@@ -439,12 +414,6 @@
{
"root": "pages/integrated",
"pages": [
{
"path": "index",
"style": {
"navigationBarTitleText": "综合服务"
}
},
{
"path": "lostAndFound/index",
"style": {
@@ -494,15 +463,21 @@
}
},
{
"path": "visitor/index",
"path": "visitor/invitation",
"style": {
"navigationBarTitleText": "访客管理"
"navigationBarTitleText": "访客邀约"
}
},
{
"path": "visitor/invitationRecord",
"style": {
"navigationBarTitleText": "邀约记录"
}
},
{
"path": "visitor/detail",
"style": {
"navigationBarTitleText": "访客详情"
"navigationBarTitleText": "邀约详情"
}
},
{
@@ -596,25 +571,7 @@
"selectedIconPath": ""
},
{
"pagePath": "pages/tabbar/invitation",
"text": "",
"iconPath": "",
"selectedIconPath": ""
},
{
"pagePath": "pages/tabbar/invitationRecord",
"text": "",
"iconPath": "",
"selectedIconPath": ""
},
{
"pagePath": "pages/tabbar/message",
"text": "",
"iconPath": "",
"selectedIconPath": ""
},
{
"pagePath": "pages/integrated/index",
"pagePath": "pages/tabbar/integrated",
"text": "",
"iconPath": "",
"selectedIconPath": ""
-139
View File
@@ -1,139 +0,0 @@
<template>
<div class="integrated-title-card">
<div class="integrated-title">综合服务</div>
</div>
<div class="service-container-card">
<div class="service-container">
<div class="service-grid">
<div
v-for="item in services"
:key="item.name"
class="service-item"
@click="goPage(item)"
>
<image :src="item.icon" class="service-icon" />
<div class="service-label">{{ item.label }}</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
const services = [
{
name: 'todo',
label: '工作待办',
icon: '/src/static/icon/todo.png',
url: '/pages/integrated/todo/index'
},
{
name: 'meeting-booking',
label: '会议预定',
icon: '/src/static/icon/meeting-booking.png',
url: '/pages/integrated/meeting/index'
},
{
name: 'lost-and-found',
label: '失物招领',
icon: '/src/static/icon/lost-and-found.png',
url: '/pages/integrated/lostAndFound/index'
},
{
name: 'feedback',
label: '意见反馈',
icon: '/src/static/icon/feedback.png',
url: '/pages/integrated/feedback/index'
},
{
name: 'repair-report',
label: '维修上报',
icon: '/src/static/icon/repair-report.png',
url: '/pages/integrated/repair/index'
},
{
name: 'notice',
label: '通知公告',
icon: '/src/static/icon/notice.png',
url: '/pages/integrated/notice/index'
},
{
name: 'visitor',
label: '访客管理',
icon: '/src/static/icon/visitor.png',
url: '/pages/integrated/visitor/index'
}
]
function goPage(item) {
if (item.url) {
uni.navigateTo({ url: item.url })
}
}
</script>
<style scoped lang="less">
.integrated-title-card {
background: #fff;
border-radius: 20rpx;
box-shadow: 0 4rpx 16rpx rgba(0,0,0,0.06);
margin: 32rpx 24rpx 0 24rpx;
padding: 0 0 8rpx 0;
}
.integrated-title {
font-size: 38rpx;
font-weight: bold;
color: #222;
text-align: center;
padding: 36rpx 0 24rpx 0;
letter-spacing: 2rpx;
}
.service-container-card {
background: #fff;
border-radius: 24rpx;
box-shadow: 0 4rpx 16rpx rgba(0,0,0,0.06);
margin: 24rpx;
padding-bottom: 32rpx;
}
.service-container {
padding: 32rpx 0 0 0;
background: transparent;
border-radius: 16rpx;
}
.service-grid {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
padding: 0 24rpx;
}
.service-item {
width: 25%;
margin-bottom: 40rpx;
display: flex;
flex-direction: column;
align-items: center;
transition: transform 0.2s;
}
.service-item:hover {
transform: translateY(-6rpx) scale(1.06);
box-shadow: 0 2rpx 12rpx rgba(25,118,210,0.08);
}
.service-icon {
width: 92rpx;
height: 92rpx;
margin-bottom: 14rpx;
border-radius: 16rpx;
background: #f5f7fa;
box-shadow: 0 2rpx 8rpx rgba(25,118,210,0.04);
transition: box-shadow 0.2s;
}
.service-label {
font-size: 28rpx;
color: #333;
text-align: center;
letter-spacing: 1rpx;
}
body {
background: #f5f6fa;
}
</style>
+162 -166
View File
@@ -1,238 +1,234 @@
<template>
<view class="container">
<!-- 顶部自定义导航栏 -->
<view class="custom-nav">
<view class="nav-back" @tap="goBack">
<text class="iconfont icon-fanhui"></text> 返回
</view>
<view class="nav-title">预定会议室</view>
</view>
<view class="container">
<!-- 顶部自定义导航栏 -->
<view class="custom-nav">
<view class="nav-back" @tap="goBack"> <text class="iconfont icon-fanhui"></text> 返回 </view>
<view class="nav-title">预定会议室</view>
</view>
<!-- 日期选择栏(简化版) -->
<view class="date-bar">
<text class="date-text">{{ todayText }}</text>
<text class="date-arrow">▼</text>
</view>
<!-- 日期选择栏(简化版) -->
<view class="date-bar">
<text class="date-text">{{ todayText }}</text>
<text class="date-arrow">▼</text>
</view>
<!-- 会议室列表 -->
<view v-for="room in meetingRooms" :key="room.id" class="room-card" @tap="goDetail(room)">
<view class="room-info">
<view class="room-main">
<view class="room-title">
{{ room.roomName }} <text class="room-capacity">{{ room.capacity }}人</text>
</view>
<view class="room-address">
<text class="iconfont">📍</text> {{ room.location || '未知地址' }}
</view>
<view class="room-tags">
<text v-for="eq in room.equipmentNames" :key="eq.name" class="room-tag">
{{ eq.name }}<span v-if="eq.quantity"> x{{ eq.quantity }}</span>
</text>
</view>
</view>
<image :src="room.picUrl || '/static/meeting/default.jpg'" class="room-img" mode="aspectFill"></image>
</view>
<!-- 时间条 -->
<view class="timeline-bar">
<view v-for="(slot, idx) in room.timeline" :key="idx" :class="['timeline-slot', slot]" />
</view>
<view class="timeline-labels">
<text v-for="h in 17" :key="h" class="timeline-label">{{ h + 7 }}</text>
</view>
</view>
</view>
<MeetingTabbar activeTab="home" />
<!-- 会议室列表 -->
<view v-for="room in meetingRooms" :key="room.id" class="room-card" @tap="goDetail(room)">
<view class="room-info">
<view class="room-main">
<view class="room-title">
{{ room.roomName }} <text class="room-capacity">{{ room.capacity }}人</text>
</view>
<view class="room-address"> <text class="iconfont">📍</text> {{ room.location || "未知地址" }} </view>
<view class="room-tags">
<text v-for="eq in room.equipmentNames" :key="eq.name" class="room-tag">
{{ eq.name }}<span v-if="eq.quantity"> x{{ eq.quantity }}</span>
</text>
</view>
</view>
<image :src="room.picUrl || '/static/meeting/default.jpg'" class="room-img" mode="aspectFill"></image>
</view>
<!-- 时间条 -->
<view class="timeline-bar">
<view v-for="(slot, idx) in room.timeline" :key="idx" :class="['timeline-slot', slot]" />
</view>
<view class="timeline-labels">
<text v-for="h in 17" :key="h" class="timeline-label">{{ h + 7 }}</text>
</view>
</view>
</view>
<MeetingTabbar activeTab="home" />
</template>
<script lang="ts" setup>
import { ref, onMounted } from 'vue'
import meetingRoomApi from '@/sheep/api/meetingRoom'
import MeetingTabbar from '@/components/MeetingTabbar.vue'
import { ref, onMounted } from "vue"
import meetingRoomApi from "@/sheep/api/meetingRoom"
import MeetingTabbar from "@/sheep/components/MeetingTabbar.vue"
const goBack = () => {
uni.navigateBack()
uni.navigateBack()
}
const todayText = ref('')
const todayText = ref("")
const meetingRooms = ref<any[]>([])
onMounted(() => {
todayText.value = getTodayText()
fetchMeetingRooms()
todayText.value = getTodayText()
fetchMeetingRooms()
})
function getTodayText() {
const d = new Date()
const week = ['日','一','二','三','四','五','六']
return `${d.getMonth()+1}月${d.getDate()}日 周${week[d.getDay()]}`
const d = new Date()
const week = ["日", "一", "二", "三", "四", "五", "六"]
return `${d.getMonth() + 1}月${d.getDate()}日 周${week[d.getDay()]}`
}
function fetchMeetingRooms() {
meetingRoomApi.getMeetingRoomList({ current: 1, size: 10 }).then((res: any) => {
if (res.code === 0) {
meetingRooms.value = res.data.records
}
})
meetingRoomApi.getMeetingRoomList({ current: 1, size: 10 }).then((res: any) => {
if (res.code === 0) {
meetingRooms.value = res.data.records
}
})
}
function goDetail(room) {
console.log('index.vue: Navigating to detail page with room:', room)
console.log('index.vue: Room ID being passed:', room.id)
uni.navigateTo({
url: `/pages/integrated/meeting/detail?roomId=${room.id}`,
success: function(res) {
// 通过 eventChannel 传递 room 对象
res.eventChannel.emit('acceptRoomInfo', room)
console.log('index.vue: Room object emitted via eventChannel:', room)
}
})
console.log("index.vue: Navigating to detail page with room:", room)
console.log("index.vue: Room ID being passed:", room.id)
uni.navigateTo({
url: `/pages/integrated/meeting/detail?roomId=${room.id}`,
success: function (res) {
// 通过 eventChannel 传递 room 对象
res.eventChannel.emit("acceptRoomInfo", room)
console.log("index.vue: Room object emitted via eventChannel:", room)
},
})
}
</script>
<style lang="less" scoped>
@import url('//at.alicdn.com/t/font_1234567_lingdang.css'); /* 替换为你的iconfont地址 */
@import url("//at.alicdn.com/t/font_1234567_lingdang.css"); /* 替换为你的iconfont地址 */
.container {
min-height: 100vh;
background: #f5f6fa;
padding-bottom: 32rpx;
min-height: 100vh;
background: #f5f6fa;
padding-bottom: 32rpx;
}
.custom-nav {
position: fixed;
top: 0;
left: 0;
right: 0;
height: 88rpx;
background: #fff;
display: flex;
align-items: center;
border-bottom: 1px solid #f0f0f0;
z-index: 100;
position: fixed;
top: 0;
left: 0;
right: 0;
height: 88rpx;
background: #fff;
display: flex;
align-items: center;
border-bottom: 1px solid #f0f0f0;
z-index: 100;
}
.nav-back {
padding-left: 24rpx;
padding-top: 12rpx;
width: auto;
height: 60rpx;
display: flex;
align-items: center;
justify-content: flex-start;
font-size: 30rpx;
color: #1976d2;
cursor: pointer;
padding-left: 24rpx;
padding-top: 12rpx;
width: auto;
height: 60rpx;
display: flex;
align-items: center;
justify-content: flex-start;
font-size: 30rpx;
color: #1976d2;
cursor: pointer;
}
.nav-back .iconfont {
font-size: 36rpx;
margin-right: 8rpx;
font-size: 36rpx;
margin-right: 8rpx;
}
.nav-title {
flex: 1;
text-align: center;
color: #222;
font-size: 32rpx;
font-weight: 500;
margin-right: 60rpx;
flex: 1;
text-align: center;
color: #222;
font-size: 32rpx;
font-weight: 500;
margin-right: 60rpx;
}
.date-bar {
margin-top: 88rpx;
display: flex;
align-items: center;
justify-content: flex-end;
padding: 0 32rpx;
height: 60rpx;
background: #fff;
border-bottom: 1px solid #f0f0f0;
margin-top: 88rpx;
display: flex;
align-items: center;
justify-content: flex-end;
padding: 0 32rpx;
height: 60rpx;
background: #fff;
border-bottom: 1px solid #f0f0f0;
}
.date-text {
color: #222;
font-size: 28rpx;
margin-right: 8rpx;
color: #222;
font-size: 28rpx;
margin-right: 8rpx;
}
.date-arrow {
color: #bbb;
font-size: 24rpx;
color: #bbb;
font-size: 24rpx;
}
.room-card {
margin: 24rpx 24rpx 0 24rpx;
background: #fff;
border-radius: 16rpx;
box-shadow: 0 2px 8px rgba(0,0,0,0.04);
padding: 24rpx;
margin: 24rpx 24rpx 0 24rpx;
background: #fff;
border-radius: 16rpx;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
padding: 24rpx;
}
.room-info {
display: flex;
flex-direction: row;
align-items: flex-start;
display: flex;
flex-direction: row;
align-items: flex-start;
}
.room-main {
flex: 1;
flex: 1;
}
.room-title {
font-size: 32rpx;
font-weight: 600;
color: #222;
margin-bottom: 8rpx;
font-size: 32rpx;
font-weight: 600;
color: #222;
margin-bottom: 8rpx;
}
.room-capacity {
font-size: 24rpx;
color: #888;
margin-left: 8rpx;
font-size: 24rpx;
color: #888;
margin-left: 8rpx;
}
.room-address {
font-size: 24rpx;
color: #888;
margin-bottom: 8rpx;
font-size: 24rpx;
color: #888;
margin-bottom: 8rpx;
}
.room-tags {
margin-bottom: 8rpx;
margin-bottom: 8rpx;
}
.room-tag {
display: inline-block;
background: #f2f6ff;
color: #1777FF;
font-size: 22rpx;
border-radius: 8rpx;
padding: 4rpx 16rpx;
margin-right: 12rpx;
margin-bottom: 4rpx;
display: inline-block;
background: #f2f6ff;
color: #1777ff;
font-size: 22rpx;
border-radius: 8rpx;
padding: 4rpx 16rpx;
margin-right: 12rpx;
margin-bottom: 4rpx;
}
.room-img {
width: 160rpx;
height: 100rpx;
border-radius: 8rpx;
margin-left: 24rpx;
object-fit: cover;
background: #eee;
width: 160rpx;
height: 100rpx;
border-radius: 8rpx;
margin-left: 24rpx;
object-fit: cover;
background: #eee;
}
.timeline-bar {
display: flex;
flex-direction: row;
align-items: center;
margin: 16rpx 0 0 0;
height: 16rpx;
display: flex;
flex-direction: row;
align-items: center;
margin: 16rpx 0 0 0;
height: 16rpx;
}
.timeline-slot {
flex: 1;
height: 100%;
margin-right: 2rpx;
border-radius: 4rpx;
background: #e6eaf2;
flex: 1;
height: 100%;
margin-right: 2rpx;
border-radius: 4rpx;
background: #e6eaf2;
}
.timeline-slot.busy {
background: #ffb74d;
background: #ffb74d;
}
.timeline-slot.free {
background: #4d8fff;
background: #4d8fff;
}
.timeline-labels {
display: flex;
flex-direction: row;
align-items: center;
margin-top: 8rpx;
display: flex;
flex-direction: row;
align-items: center;
margin-top: 8rpx;
}
.timeline-label {
flex: 1;
text-align: center;
color: #bbb;
font-size: 20rpx;
flex: 1;
text-align: center;
color: #bbb;
font-size: 20rpx;
}
</style>
</style>
+161 -163
View File
@@ -1,58 +1,51 @@
<template>
<view class="container">
<!-- 顶部自定义导航栏 -->
<view class="custom-nav">
<view class="nav-title">我的预约</view>
</view>
<view class="container">
<!-- 顶部自定义导航栏 -->
<view class="custom-nav">
<view class="nav-title">我的预约</view>
</view>
<!-- 顶部tab -->
<view class="tabs">
<view
v-for="(tab, idx) in tabs"
:key="tab"
:class="['tab', idx === activeTab ? 'active' : '']"
@tap="onTabChange(idx)"
>{{ tab }}</view>
</view>
<!-- 顶部tab -->
<view class="tabs">
<view v-for="(tab, idx) in tabs" :key="tab" :class="['tab', idx === activeTab ? 'active' : '']" @tap="onTabChange(idx)">{{
tab
}}</view>
</view>
<!-- 预约卡片列表 -->
<view v-if="reservationList.length">
<view v-for="item in reservationList" :key="item.id" class="card">
<view class="card-header">
<image src="/static/icons/meeting.png" class="card-icon" />
<text class="card-title">{{ item.title }}</text>
<text class="card-status" :class="statusMap[item.checkStatus]?.class">{{ statusMap[item.checkStatus]?.text }}</text>
</view>
<view class="card-info">
<view>预约日期:{{ item.bookingDate }}</view>
<view>时间:{{ item.startTime }} ~ {{ item.endTime }}</view>
<view>申请人:{{ item.createBy }}</view>
</view>
<view class="card-footer">
<view class="card-btn" @tap="viewDetail(item)">查看详情</view>
<view
v-if="item.checkStatus === 1 || item.checkStatus === 2"
class="card-btn cancel"
@tap="cancelBooking(item)"
>取消预约</view>
</view>
</view>
</view>
<view v-else class="empty">
<image src="/static/icons/empty.png" class="empty-img" />
<view class="empty-text">暂无预约</view>
<view class="empty-tip">您还没有预约会议,快去预约吧!</view>
</view>
<MeetingTabbar activeTab="mine" />
</view>
<!-- 预约卡片列表 -->
<view v-if="reservationList.length">
<view v-for="item in reservationList" :key="item.id" class="card">
<view class="card-header">
<image src="/static/icons/meeting.png" class="card-icon" />
<text class="card-title">{{ item.title }}</text>
<text class="card-status" :class="statusMap[item.checkStatus]?.class">{{ statusMap[item.checkStatus]?.text }}</text>
</view>
<view class="card-info">
<view>预约日期:{{ item.bookingDate }}</view>
<view>时间:{{ item.startTime }} ~ {{ item.endTime }}</view>
<view>申请人:{{ item.createBy }}</view>
</view>
<view class="card-footer">
<view class="card-btn" @tap="viewDetail(item)">查看详情</view>
<view v-if="item.checkStatus === 1 || item.checkStatus === 2" class="card-btn cancel" @tap="cancelBooking(item)">取消预约</view>
</view>
</view>
</view>
<view v-else class="empty">
<image src="/static/icons/empty.png" class="empty-img" />
<view class="empty-text">暂无预约</view>
<view class="empty-tip">您还没有预约会议,快去预约吧!</view>
</view>
<MeetingTabbar activeTab="mine" />
</view>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import meetingRoomApi from '@/sheep/api/meetingRoom'
import MeetingTabbar from '@/components/MeetingTabbar.vue'
import { ref, onMounted } from "vue"
import meetingRoomApi from "@/sheep/api/meetingRoom"
import MeetingTabbar from "@/sheep/components/MeetingTabbar.vue"
const tabs = ['全部', '审核中', '审核成功', '已拒绝', '已取消']
const tabs = ["全部", "审核中", "审核成功", "已拒绝", "已取消"]
const activeTab = ref(0)
const reservationList = ref<any[]>([])
const loading = ref(false)
@@ -61,181 +54,186 @@ const current = ref(1)
const size = ref(10)
const statusMap = {
1: { text: '审核中', class: 'pending' },
2: { text: '审核成功', class: 'done' },
3: { text: '已拒绝', class: 'cancel' },
4: { text: '已取消', class: 'cancel' }
1: { text: "审核中", class: "pending" },
2: { text: "审核成功", class: "done" },
3: { text: "已拒绝", class: "cancel" },
4: { text: "已取消", class: "cancel" },
}
function fetchReservations() {
loading.value = true
let checkStatus = activeTab.value === 0 ? undefined : activeTab.value // 0为全部
meetingRoomApi.getMyBookings({
current: current.value,
size: size.value,
...(checkStatus ? { checkStatus } : {})
}).then((res: any) => {
loading.value = false
if (res.code === 0 && res.data && res.data.records) {
reservationList.value = res.data.records
total.value = res.data.total
}
}).catch(() => {
loading.value = false
})
loading.value = true
let checkStatus = activeTab.value === 0 ? undefined : activeTab.value // 0为全部
meetingRoomApi
.getMyBookings({
current: current.value,
size: size.value,
...(checkStatus ? { checkStatus } : {}),
})
.then((res: any) => {
loading.value = false
if (res.code === 0 && res.data && res.data.records) {
reservationList.value = res.data.records
total.value = res.data.total
}
})
.catch(() => {
loading.value = false
})
}
onMounted(() => {
fetchReservations()
fetchReservations()
})
function onTabChange(idx: number) {
activeTab.value = idx
current.value = 1
fetchReservations()
activeTab.value = idx
current.value = 1
fetchReservations()
}
function cancelBooking(item: any) {
uni.showModal({
title: '提示',
content: '确定要取消该预约吗?',
success: (res) => {
if (res.confirm) {
// TODO: 调用取消预约接口
uni.showToast({ title: '已取消(待接入接口)', icon: 'none' })
}
}
})
uni.showModal({
title: "提示",
content: "确定要取消该预约吗?",
success: (res) => {
if (res.confirm) {
// TODO: 调用取消预约接口
uni.showToast({ title: "已取消(待接入接口)", icon: "none" })
}
},
})
}
function viewDetail(item: any) {
uni.navigateTo({
url: '/pages/integrated/meeting/booking-detail?id=' + item.id
})
uni.navigateTo({
url: "/pages/integrated/meeting/booking-detail?id=" + item.id,
})
}
</script>
<style lang="less" scoped>
.container {
min-height: 100vh;
background: #f5f6fa;
padding-bottom: 120rpx;
min-height: 100vh;
background: #f5f6fa;
padding-bottom: 120rpx;
}
.custom-nav {
height: 88rpx;
background: #fff;
display: flex;
align-items: center;
justify-content: center;
font-size: 32rpx;
font-weight: 600;
border-bottom: 1px solid #f0f0f0;
height: 88rpx;
background: #fff;
display: flex;
align-items: center;
justify-content: center;
font-size: 32rpx;
font-weight: 600;
border-bottom: 1px solid #f0f0f0;
}
.tabs {
display: flex;
background: #fff;
border-bottom: 1px solid #f0f0f0;
display: flex;
background: #fff;
border-bottom: 1px solid #f0f0f0;
}
.tab {
flex: 1;
text-align: center;
padding: 24rpx 0;
font-size: 28rpx;
color: #888;
position: relative;
flex: 1;
text-align: center;
padding: 24rpx 0;
font-size: 28rpx;
color: #888;
position: relative;
}
.tab.active {
color: #1777FF;
font-weight: 600;
color: #1777ff;
font-weight: 600;
}
.tab.active::after {
content: '';
display: block;
width: 60rpx;
height: 6rpx;
background: #1777FF;
border-radius: 3rpx;
margin: 0 auto;
position: absolute;
left: 0; right: 0; bottom: 0;
content: "";
display: block;
width: 60rpx;
height: 6rpx;
background: #1777ff;
border-radius: 3rpx;
margin: 0 auto;
position: absolute;
left: 0;
right: 0;
bottom: 0;
}
.card {
margin: 24rpx;
background: #fff;
border-radius: 16rpx;
box-shadow: 0 2px 8px rgba(0,0,0,0.04);
padding: 24rpx;
margin-bottom: 16rpx;
margin: 24rpx;
background: #fff;
border-radius: 16rpx;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
padding: 24rpx;
margin-bottom: 16rpx;
}
.card-header {
display: flex;
align-items: center;
margin-bottom: 12rpx;
display: flex;
align-items: center;
margin-bottom: 12rpx;
}
.card-icon {
width: 40rpx;
height: 40rpx;
margin-right: 12rpx;
width: 40rpx;
height: 40rpx;
margin-right: 12rpx;
}
.card-title {
font-size: 30rpx;
font-weight: 600;
flex: 1;
font-size: 30rpx;
font-weight: 600;
flex: 1;
}
.card-status {
font-size: 26rpx;
margin-left: 12rpx;
font-size: 26rpx;
margin-left: 12rpx;
}
.card-status.done {
color: #888;
color: #888;
}
.card-status.pending {
color: #1777FF;
color: #1777ff;
}
.card-status.cancel {
color: #ff4d4f;
color: #ff4d4f;
}
.card-info {
font-size: 26rpx;
color: #666;
margin-bottom: 12rpx;
line-height: 1.7;
font-size: 26rpx;
color: #666;
margin-bottom: 12rpx;
line-height: 1.7;
}
.card-footer {
display: flex;
gap: 24rpx;
display: flex;
gap: 24rpx;
}
.card-btn {
background: #f2f6ff;
color: #1777FF;
border-radius: 8rpx;
padding: 8rpx 24rpx;
font-size: 24rpx;
background: #f2f6ff;
color: #1777ff;
border-radius: 8rpx;
padding: 8rpx 24rpx;
font-size: 24rpx;
}
.card-btn.cancel {
background: #ffeaea;
color: #ff4d4f;
background: #ffeaea;
color: #ff4d4f;
}
.empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
margin-top: 120rpx;
color: #bbb;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
margin-top: 120rpx;
color: #bbb;
}
.empty-img {
width: 180rpx;
height: 180rpx;
margin-bottom: 24rpx;
width: 180rpx;
height: 180rpx;
margin-bottom: 24rpx;
}
.empty-text {
font-size: 30rpx;
font-weight: 600;
margin-bottom: 8rpx;
font-size: 30rpx;
font-weight: 600;
margin-bottom: 8rpx;
}
.empty-tip {
font-size: 24rpx;
color: #aaa;
font-size: 24rpx;
color: #aaa;
}
</style>
</style>
+95 -226
View File
@@ -1,236 +1,105 @@
<template>
<view class="visitor-detail">
<view class="detail-header">
<view class="visitor-info">
<text class="visitor-name">{{ detail.visitorName }}</text>
<text class="visitor-phone">{{ detail.visitorPhone }}</text>
</view>
<view class="status-tag" :class="getStatusClass(detail.status)">{{ getStatusText(detail.status) }}</view>
</view>
<view class="detail-content">
<view class="info-row">
<text class="label">来访目的:</text>
<text class="value">{{ getPurposeLabel(detail.purpose) }}</text>
</view>
<view class="info-row">
<text class="label">被访人:</text>
<text class="value">{{ detail.hostName }}</text>
</view>
<view class="info-row">
<text class="label">来访时间:</text>
<text class="value">{{ formatVisitTime(detail.visitDate, detail.visitTime) }}</text>
</view>
<view v-if="detail.remark" class="info-row">
<text class="label">备注说明:</text>
<text class="value">{{ detail.remark }}</text>
</view>
</view>
<view v-if="detail.rejectReason" class="reject-section">
<view class="reject-title">拒绝原因</view>
<view class="reject-content">{{ detail.rejectReason }}</view>
</view>
<view class="detail-footer">
<text class="apply-time">申请时间:{{ formatTime(detail.createTime) }}</text>
</view>
</view>
<crud-layout title="详情" mainIsScroll bg="/static/page/blue-bg.png">
<view v-if="detail.id">
<tm-sheet :margin="[0, 20]" class="second-text-32">
<tm-text font-size="32">访客信息</tm-text>
<view class="flex justify-between mt-1">
<text>访客姓名:</text>
<text>{{ detail.name }}</text>
</view>
<view class="flex justify-between mt-1">
<text>访客手机号:</text>
<text>{{ detail.mobile }}</text>
</view>
<view class="flex justify-between mt-1">
<text>来访公司名称:</text>
<text>{{ detail.companyName }}</text>
</view>
<view class="flex justify-between mt-1">
<text>来访原因:</text>
<text>{{ visitorReasonList.find((item) => item.id == detail.reasonValue)?.title || "" }}</text>
</view>
<view class="flex justify-between mt-1">
<text>接待人:</text>
<text>{{ detail.userName }}</text>
</view>
<view class="flex justify-between mt-1">
<text>来访人数:</text>
<text>{{ detail.peopleNumber }}</text>
</view>
<view class="flex justify-between mt-1">
<text>是否可随行人员:</text>
<text>{{ detail.isOtherPeople == "1" ? "是" : "否" }}</text>
</view>
<view class="flex justify-between mt-1">
<text>是否可驾驶车辆:</text>
<text>{{ detail.isDrive == "1" ? "是" : "否" }}</text>
</view>
</tm-sheet>
<tm-sheet :margin="[0, 20]" class="second-text-32">
<tm-text font-size="32">时间</tm-text>
<view class="flex justify-between mt-1">
<text>预计来访时间:</text>
<text>{{ detail.vendDate }}</text>
</view>
<view class="flex justify-between mt-1">
<text>预计离开时间:</text>
<text>{{ detail.vstartDate }}</text>
</view>
</tm-sheet>
<tm-sheet :margin="[0, 20]" class="second-text-32">
<tm-text font-size="32">其他</tm-text>
<view class="flex justify-between mt-1">
<text>创建方式:</text>
<text>{{ detail.type == "1" ? "主动申请" : detail.type == "2" ? "邀约" : "后台创建" }}</text>
</view>
<view class="flex justify-between mt-1">
<text>提交时间:</text>
<text>{{ detail.createTime }}</text>
</view>
</tm-sheet>
</view>
<view class="h-40rpx" />
</crud-layout>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import meetingRoomApi from '@/sheep/api/meetingRoom'
import { onMounted, ref } from "vue"
import { onLoad } from "@dcloudio/uni-app"
import { invitationApi } from "@/sheep/api"
const detail = ref<any>({})
const purposeOptions = [
{ value: '1', label: '商务洽谈' },
{ value: '2', label: '技术交流' },
{ value: '3', label: '面试' },
{ value: '4', label: '参观访问' },
{ value: '5', label: '其他' }
]
const timeOptions = [
{ value: '1', label: '上午 (9:00-12:00)' },
{ value: '2', label: '下午 (14:00-18:00)' },
{ value: '3', label: '全天 (9:00-18:00)' }
]
function getPurposeLabel(purpose: string) {
const option = purposeOptions.find(item => item.value === purpose)
return option ? option.label : '其他'
}
function getStatusText(status: string) {
const statusMap: Record<string, string> = {
'1': '待审核',
'2': '已通过',
'3': '已拒绝',
'4': '已取消',
'5': '已完成'
}
return statusMap[status] || '未知'
}
function getStatusClass(status: string) {
const classMap: Record<string, string> = {
'1': 'status-pending',
'2': 'status-approved',
'3': 'status-rejected',
'4': 'status-cancelled',
'5': 'status-completed'
}
return classMap[status] || 'status-pending'
}
function formatVisitTime(date: string, time: string) {
const timeLabel = timeOptions.find(item => item.value === time)?.label || ''
return `${date} ${timeLabel}`
}
function formatTime(time: string) {
if (!time) return ''
return new Date(time).toLocaleString()
}
const visitorReasonList = ref<{ title: string; id: string }[]>([])
onLoad(async (options: any) => {
const id = options.id
if (!id) return
try {
const response: any = await meetingRoomApi.getVisitorDetail(id)
if (response.code === 0 && response.data) {
detail.value = response.data
}
} catch (error) {
console.error('获取访客详情失败:', error)
uni.showToast({ title: '获取详情失败', icon: 'none' })
}
const id = options.id
try {
const { code, data, msg } = await invitationApi.getInvitationDetail(id)
if (code == 0) {
detail.value = data
return
}
uni.showToast({ title: msg || "获取详情失败", icon: "none" })
} catch (error: any) {
uni.showToast({ title: error.message, icon: "none" })
}
})
onMounted(() => {
getVisitorReasonList()
})
// 获取拜访原因列表
const getVisitorReasonList = async () => {
const { code, data } = await invitationApi.getDictListByType("1")
if (code == 0 && data) {
visitorReasonList.value = data.map((item: any) => ({ title: item.label, id: item.value }))
return
}
visitorReasonList.value = []
}
</script>
<style lang="scss" scoped>
.visitor-detail {
padding: 32rpx;
background: #f5f5f5;
min-height: 100vh;
}
.detail-header {
background: #fff;
border-radius: 16rpx;
padding: 32rpx;
margin-bottom: 24rpx;
display: flex;
justify-content: space-between;
align-items: center;
.visitor-info {
.visitor-name {
font-size: 32rpx;
font-weight: bold;
color: #333;
margin-right: 16rpx;
}
.visitor-phone {
font-size: 26rpx;
color: #666;
}
}
.status-tag {
padding: 12rpx 24rpx;
border-radius: 24rpx;
font-size: 26rpx;
&.status-pending {
background: #fff3e0;
color: #f57c00;
}
&.status-approved {
background: #e8f5e8;
color: #388e3c;
}
&.status-rejected {
background: #ffebee;
color: #d32f2f;
}
&.status-cancelled {
background: #f5f5f5;
color: #999;
}
&.status-completed {
background: #e3f2fd;
color: #1976d2;
}
}
}
.detail-content {
background: #fff;
border-radius: 16rpx;
padding: 32rpx;
margin-bottom: 24rpx;
.info-row {
display: flex;
margin-bottom: 16rpx;
&:last-child {
margin-bottom: 0;
}
.label {
font-size: 28rpx;
color: #666;
width: 160rpx;
flex-shrink: 0;
}
.value {
font-size: 28rpx;
color: #333;
flex: 1;
}
}
}
.reject-section {
background: #fff;
border-radius: 16rpx;
padding: 32rpx;
margin-bottom: 24rpx;
.reject-title {
font-size: 30rpx;
font-weight: bold;
color: #d32f2f;
margin-bottom: 16rpx;
}
.reject-content {
font-size: 28rpx;
color: #666;
line-height: 1.6;
}
}
.detail-footer {
background: #fff;
border-radius: 16rpx;
padding: 32rpx;
.apply-time {
font-size: 26rpx;
color: #999;
}
}
</style>
-438
View File
@@ -1,438 +0,0 @@
<template>
<view class="visitor-container">
<!-- 访客申请表单 -->
<view class="visitor-form">
<view class="form-title">访客申请</view>
<view class="form-item">
<view class="label">访客姓名</view>
<input v-model="visitorName" placeholder="请输入访客姓名" class="name-input" />
</view>
<view class="form-item">
<view class="label">访客手机号</view>
<input v-model="visitorPhone" placeholder="请输入访客手机号" class="phone-input" />
</view>
<view class="form-item">
<view class="label">来访目的</view>
<picker @change="onPurposeChange" :value="purposeIndex" :range="purposeOptions" range-key="label">
<view class="picker">{{ purposeOptions[purposeIndex].label }}</view>
</picker>
</view>
<view class="form-item">
<view class="label">来访时间</view>
<picker mode="date" @change="onDateChange" :value="visitDate">
<view class="picker">{{ visitDate || '请选择来访日期' }}</view>
</picker>
</view>
<view class="form-item">
<view class="label">来访时段</view>
<picker @change="onTimeChange" :value="timeIndex" :range="timeOptions" range-key="label">
<view class="picker">{{ timeOptions[timeIndex].label }}</view>
</picker>
</view>
<view class="form-item">
<view class="label">被访人</view>
<input v-model="hostName" placeholder="请输入被访人姓名" class="host-input" />
</view>
<view class="form-item">
<view class="label">备注说明</view>
<textarea
v-model="remark"
placeholder="请输入备注说明(选填)"
class="remark-textarea"
maxlength="200"
/>
<view class="char-count">{{ remark.length }}/200</view>
</view>
<button @click="submitVisitor" class="submit-btn" :disabled="!canSubmit">
提交申请
</button>
</view>
<!-- 访客记录列表 -->
<view class="visitor-list">
<view class="list-title">访客记录</view>
<view v-if="visitorList.length === 0" class="empty-tip">
暂无访客记录
</view>
<view v-else>
<view
v-for="item in visitorList"
:key="item.id"
class="visitor-item"
@click="viewDetail(item)"
>
<view class="item-header">
<view class="visitor-info">
<text class="visitor-name">{{ item.visitorName }}</text>
<text class="visitor-phone">{{ item.visitorPhone }}</text>
</view>
<view class="status-tag" :class="getStatusClass(item.status)">{{ getStatusText(item.status) }}</view>
</view>
<view class="item-content">
<view class="purpose">来访目的:{{ getPurposeLabel(item.purpose) }}</view>
<view class="host">被访人:{{ item.hostName }}</view>
<view class="time">来访时间:{{ formatVisitTime(item.visitDate, item.visitTime) }}</view>
</view>
<view class="item-footer">
<text class="apply-time">申请时间:{{ formatTime(item.createTime) }}</text>
<text v-if="item.remark" class="has-remark">有备注</text>
</view>
</view>
</view>
</view>
</view>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import meetingRoomApi from '@/sheep/api/meetingRoom'
const purposeOptions = [
{ value: '1', label: '商务洽谈' },
{ value: '2', label: '技术交流' },
{ value: '3', label: '面试' },
{ value: '4', label: '参观访问' },
{ value: '5', label: '其他' }
]
const timeOptions = [
{ value: '1', label: '上午 (9:00-12:00)' },
{ value: '2', label: '下午 (14:00-18:00)' },
{ value: '3', label: '全天 (9:00-18:00)' }
]
const visitorName = ref('')
const visitorPhone = ref('')
const purposeIndex = ref(0)
const visitDate = ref('')
const timeIndex = ref(0)
const hostName = ref('')
const remark = ref('')
const visitorList = ref<any[]>([])
const canSubmit = computed(() => {
return visitorName.value.trim() &&
visitorPhone.value.trim() &&
visitDate.value &&
hostName.value.trim()
})
function onPurposeChange(e: any) {
purposeIndex.value = e.detail.value
}
function onDateChange(e: any) {
visitDate.value = e.detail.value
}
function onTimeChange(e: any) {
timeIndex.value = e.detail.value
}
function getPurposeLabel(purpose: string) {
const option = purposeOptions.find(item => item.value === purpose)
return option ? option.label : '其他'
}
function getStatusText(status: string) {
const statusMap: Record<string, string> = {
'1': '待审核',
'2': '已通过',
'3': '已拒绝',
'4': '已取消',
'5': '已完成'
}
return statusMap[status] || '未知'
}
function getStatusClass(status: string) {
const classMap: Record<string, string> = {
'1': 'status-pending',
'2': 'status-approved',
'3': 'status-rejected',
'4': 'status-cancelled',
'5': 'status-completed'
}
return classMap[status] || 'status-pending'
}
function formatVisitTime(date: string, time: string) {
const timeLabel = timeOptions.find(item => item.value === time)?.label || ''
return `${date} ${timeLabel}`
}
function formatTime(time: string) {
if (!time) return ''
return new Date(time).toLocaleDateString()
}
async function submitVisitor() {
if (!canSubmit.value) {
uni.showToast({ title: '请填写完整信息', icon: 'none' })
return
}
// 验证手机号格式
const phoneRegex = /^1[3-9]\d{9}$/
if (!phoneRegex.test(visitorPhone.value)) {
uni.showToast({ title: '请输入正确的手机号', icon: 'none' })
return
}
try {
uni.showLoading({ title: '提交中...' })
const data = {
visitorName: visitorName.value.trim(),
visitorPhone: visitorPhone.value.trim(),
purpose: purposeOptions[purposeIndex.value].value,
visitDate: visitDate.value,
visitTime: timeOptions[timeIndex.value].value,
hostName: hostName.value.trim(),
remark: remark.value.trim()
}
const response: any = await meetingRoomApi.submitVisitor(data)
uni.hideLoading()
if (response.code === 0) {
uni.showToast({ title: '提交成功', icon: 'success' })
// 清空表单
visitorName.value = ''
visitorPhone.value = ''
purposeIndex.value = 0
visitDate.value = ''
timeIndex.value = 0
hostName.value = ''
remark.value = ''
// 刷新列表
loadVisitorList()
} else {
uni.showToast({ title: response.msg || '提交失败', icon: 'none' })
}
} catch (error) {
uni.hideLoading()
uni.showToast({ title: '提交失败', icon: 'none' })
}
}
function viewDetail(item: any) {
uni.navigateTo({
url: `/pages/integrated/visitor/detail?id=${item.id}`
})
}
async function loadVisitorList() {
try {
const response: any = await meetingRoomApi.getVisitorList({
current: 1,
size: 20
})
if (response.code === 0 && response.data) {
visitorList.value = response.data.records || []
}
} catch (error) {
console.error('加载访客记录失败:', error)
}
}
onMounted(() => {
loadVisitorList()
})
</script>
<style lang="scss" scoped>
.visitor-container {
padding: 24rpx;
background: #f5f5f5;
min-height: 100vh;
}
.visitor-form {
background: #fff;
border-radius: 16rpx;
padding: 32rpx;
margin-bottom: 24rpx;
.form-title {
font-size: 32rpx;
font-weight: bold;
margin-bottom: 24rpx;
}
.form-item {
margin-bottom: 24rpx;
.label {
font-size: 28rpx;
color: #333;
margin-bottom: 12rpx;
}
.picker {
height: 80rpx;
line-height: 80rpx;
padding: 0 24rpx;
background: #f7f8fa;
border-radius: 8rpx;
font-size: 28rpx;
}
.name-input,
.phone-input,
.host-input {
height: 80rpx;
padding: 0 24rpx;
background: #f7f8fa;
border-radius: 8rpx;
font-size: 28rpx;
}
.remark-textarea {
width: 100%;
height: 120rpx;
padding: 24rpx;
background: #f7f8fa;
border-radius: 8rpx;
font-size: 28rpx;
box-sizing: border-box;
}
.char-count {
text-align: right;
font-size: 24rpx;
color: #999;
margin-top: 8rpx;
}
}
.submit-btn {
width: 100%;
height: 88rpx;
background: #007aff;
color: #fff;
border-radius: 8rpx;
font-size: 32rpx;
&:disabled {
background: #ccc;
}
}
}
.visitor-list {
background: #fff;
border-radius: 16rpx;
padding: 32rpx;
.list-title {
font-size: 32rpx;
font-weight: bold;
margin-bottom: 24rpx;
}
.empty-tip {
text-align: center;
color: #999;
font-size: 28rpx;
padding: 60rpx 0;
}
.visitor-item {
border-bottom: 1rpx solid #f0f0f0;
padding: 24rpx 0;
&:last-child {
border-bottom: none;
}
.item-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16rpx;
.visitor-info {
.visitor-name {
font-size: 30rpx;
font-weight: bold;
color: #333;
margin-right: 16rpx;
}
.visitor-phone {
font-size: 26rpx;
color: #666;
}
}
.status-tag {
padding: 8rpx 16rpx;
border-radius: 20rpx;
font-size: 24rpx;
&.status-pending {
background: #fff3e0;
color: #f57c00;
}
&.status-approved {
background: #e8f5e8;
color: #388e3c;
}
&.status-rejected {
background: #ffebee;
color: #d32f2f;
}
&.status-cancelled {
background: #f5f5f5;
color: #999;
}
&.status-completed {
background: #e3f2fd;
color: #1976d2;
}
}
}
.item-content {
margin-bottom: 16rpx;
.purpose,
.host,
.time {
font-size: 26rpx;
color: #666;
margin-bottom: 8rpx;
line-height: 1.4;
}
}
.item-footer {
display: flex;
justify-content: space-between;
align-items: center;
.apply-time {
font-size: 24rpx;
color: #999;
}
.has-remark {
font-size: 24rpx;
color: #007aff;
background: #e3f2fd;
padding: 4rpx 12rpx;
border-radius: 12rpx;
}
}
}
}
</style>
+245
View File
@@ -0,0 +1,245 @@
<template>
<crud-layout mainIsScroll bg="/static/page/blue-bg.png">
<view class="h-20rpx" />
<tm-sheet margin="[0]">
<view class="mx-auto mb-1 text-center text-lg">访客邀约</view>
<tm-alert :border="['2']" :borderColor="['#89c0ff']" :darkBorderColor="['#3b5066']">访客邀约请填写以下信息</tm-alert>
<tm-form @submit="onsubmit" v-model="form" :rules="rules" direction="vertical">
<!-- <tm-form-item name="name" label="访客姓名">
<tm-input color="transparent" v-model.trim="form.name" placeholder="请输入真实姓名" />
</tm-form-item> -->
<tm-form-item name="mobile" label="访客手机号">
<tm-input color="transparent" v-model.trim="form.mobile" placeholder="请输入访客手机号" />
</tm-form-item>
<!-- <tm-form-item name="companyName" label="来访公司名称">
<tm-input color="transparent" v-model.trim="form.companyName" placeholder="来访公司名称" />
</tm-form-item> -->
<tm-form-item name="vStartDate" label="拜访开始时间 >" @click="startDatePicker('vStartDate')">
<template #label>
<span>拜访开始时间 </span>
<tm-icon name="arrow-right-s-line" />
</template>
<tm-input
:modelValue="form.vStartDate"
placeholder="请选择拜访开始时间"
style="pointer-events: none"
disabled
color="transparent"
/>
</tm-form-item>
<tm-form-item name="vEndDate" label="拜访离开时间 >" @click="endDatePicker('vEndDate')">
<template #label>
<span>拜访离开时间 </span>
<tm-icon name="arrow-right-s-line" />
</template>
<tm-input
:modelValue="form.vEndDate"
placeholder="请选择拜访离开时间"
style="pointer-events: none"
disabled
color="transparent"
/>
</tm-form-item>
<!-- <tm-form-item name="deptId" label="拜访部门">
<tm-drawer :show-close="true" :show-footer="true" size="auto" title="请选择拜访部门">
<template #trigger>
<tm-input :modelValue="form.deptId" placeholder="请选择拜访部门" style="pointer-events: none" disabled color="transparent">
<template v-slot:rightIcon>
<tm-icon name="arrow-right-s-line"></tm-icon>
</template>
</tm-input>
</template>
<tm-text color="#999999">精致美观的对话框,可全局配置风格</tm-text>
</tm-drawer>
</tm-form-item> -->
<!-- <tm-form-item name="reasonValue" label="拜访原因">
<tm-picker @confirm="reasonPickerConfirm" :list="visitorReasonList">
<tm-input :modelValue="form.reasonText" placeholder="请选择拜访原因" style="pointer-events: none" disabled color="transparent">
<template #rightIcon>
<tm-icon name="arrow-right-s-line"></tm-icon>
</template>
</tm-input>
</tm-picker>
</tm-form-item> -->
<tm-form-item name="isOtherPeople" label="是否可随行人员" direction="horizontal" labelWidth="500">
<tm-switch v-model="form.isOtherPeople" />
</tm-form-item>
<tm-form-item name="isDrive" label="是否可驾驶车辆" direction="horizontal" labelWidth="500">
<tm-switch v-model="form.isDrive" />
</tm-form-item>
<tm-button form-type="submit" block>提交</tm-button>
</tm-form>
</tm-sheet>
<tm-drawer
title="请选择时间"
:showClose="true"
:showFooter="false"
size="auto"
placement="bottom"
v-model:show="showDatePicker"
:duration="100"
zIndex="100"
>
<time-range-picker @confirm="onDatePickerConfirm" @cancel="showDatePicker = false" />
</tm-drawer>
<tm-modal
v-model:show="modalShow"
:overlayClick="false"
title="提示"
position="bottom"
height="auto"
:showCancel="true"
cancelText="取消"
confirmText="确定"
@confirm="confirmSubmit"
@cancel="modalShow = false"
>
<view class="text-center"><tm-text font-size="30" color="#999999">确认提交吗?</tm-text></view>
</tm-modal>
<template #main-bottom>
<view class="h-100rpx flex justify-around items-center">
<view class="flex-col justify-center items-center">
<image src="@/static/page/invitation-select.png" class="w-50rpx h-50rpx" />
<view class="text-center text-24 c-#1977FD">访客邀约</view>
</view>
<view class="flex-col justify-center items-center" @click="goInvitationRecord">
<image src="@/static/page/order.png" class="w-50rpx h-50rpx" />
<view class="text-center text-24 c-#999999">邀约记录</view>
</view>
</view>
</template>
</crud-layout>
</template>
<script setup lang="ts">
import { reactive, ref } from "vue"
import { invitationApi } from "@/sheep/api/index"
import TimeRangePicker from "@/sheep/components/TimePointPicker.vue"
const form = reactive<any>({
// name: "",
mobile: "",
// companyName: "",
vStartDate: "",
vEndDate: "",
// reasonValue: "",
// reasonText: "",
isOtherPeople: true,
isDrive: true,
})
const showDatePicker = ref(false)
const curSelectedInputName = ref("")
const modalShow = ref(false)
// const visitorReasonList = ref<{ title: string; id: string }[]>([])
// onLoad(() => {
// getVisitorReasonList()
// })
// 获取拜访原因列表
// const getVisitorReasonList = async () => {
// const { code, data } = await invitationApi.getDictListByType("1")
// if (code == 0 && data) {
// visitorReasonList.value = data.map((item: any) => ({ title: item.label, id: item.value }))
// return
// }
// visitorReasonList.value = []
// }
// 来访原因选择器确认
// const reasonPickerConfirm = (ids: string) => {
// const reason = visitorReasonList.value.find((item) => item.id === ids[0])
// form.reasonText = reason?.title || ""
// form.reasonValue = reason?.id || ""
// }
const rules: Record<string, TM.FORM_RULE[]> = {
// name: [{ required: true, message: "姓名2到5个字符", rule: /^.{2,5}$/ }],
mobile: [{ required: true, message: "请输入正确的手机号", rule: /^(?:(?:\+|00)86)?1\d{10}$/ }],
// companyName: [{ required: true, message: "请输入来访公司名称" }],
vStartDate: [{ required: true, message: "请选择拜访开始时间" }],
vEndDate: [{ required: true, message: "请选择拜访离开时间" }],
// reasonValue: [{ required: true, message: "请选择拜访原因" }],
}
// 开始日期选择
const startDatePicker = (key: string) => {
showDatePicker.value = true
curSelectedInputName.value = key
}
// 离开日期选择
const endDatePicker = (key: string) => {
showDatePicker.value = true
curSelectedInputName.value = key
}
// 日期选择器确认
const onDatePickerConfirm = (dateValue: any, timeValue: string) => {
form[curSelectedInputName.value] = `${dateValue.fullDate} ${timeValue}`
showDatePicker.value = false
}
// 表单提交
const onsubmit = (evt: TM.FORM_SUBMIT_RESULT) => {
// console.log("校验结果:", evt)
if (!evt.isPass) return
modalShow.value = true
}
// 确认提交
const confirmSubmit = async () => {
if (form.isOtherPeople) {
form.isOtherPeople = "1"
} else {
form.isOtherPeople = "0"
}
if (form.isDrive) {
form.isDrive = "1"
} else {
form.isDrive = "0"
}
form.mobile = form.mobile.trim().replace(/\s/g, "")
try {
const { code, data, msg } = await invitationApi.saveInvitation(form)
if (code == 0) {
uni.showToast({ title: "提交成功", icon: "success" })
resetForm()
modalShow.value = false
uni.navigateTo({ url: "/pages/integrated/visitor/invitationRecord" })
return
}
uni.showToast({ title: msg || "提交失败", icon: "none" })
} catch (error: any) {
uni.showToast({ title: error.message || "提交失败", icon: "none" })
}
}
// 重置表单
const resetForm = () => {
Object.assign(form, {
// name: "",
mobile: "",
// companyName: "",
vStartDate: "",
vEndDate: "",
// reasonValue: "",
// reasonText: "",
isOtherPeople: true,
isDrive: true,
})
}
const goInvitationRecord = () => {
uni.redirectTo({ url: "/pages/integrated/visitor/invitationRecord" })
}
</script>
@@ -0,0 +1,192 @@
<template>
<crud-layout bg="/static/page/blue-bg.png">
<z-paging
ref="pageRef"
height="100%"
:fixed="false"
v-model="pageHook.list"
:default-page-size="10"
safe-area-inset-bottom
:loading-more-enabled="true"
:show-scrollbar="false"
@query="query"
:auto-clean-list-when-reload="false"
:auto-scroll-to-top-when-reload="false"
@scroll="pageHook.scroll"
>
<template #top>
<!-- <view class="bg-white rounded-20rpx my-20rpx font-bold py-1">
<z-tabs
:list="[
{ name: '访客申请', value: 0 },
{ name: '我的邀约', value: 1 },
// { name: '访客申请', value: 0, badge: { count: 10 } },
// { name: '我的邀约', value: 1, badge: { count: 6 } },
]"
bar-width="0"
:active-style="{
fontSize: '30rpx',
backgroundColor: '#ECF4F9',
padding: '12rpx 64rpx',
borderRadius: '20rpx',
border: '1px solid #007AFF',
}"
:inactive-style="{
fontSize: '30rpx',
padding: '12rpx 64rpx',
border: '1px solid transparent',
}"
:badge-style="{
whiteSpace: 'nowrap',
position: 'relative',
top: '-14rpx',
left: '-72rpx',
}"
@change="onTabChange"
/>
</view> -->
<view class="h-20rpx" />
<view class="bg-white rounded-16rpx px-20rpx">
<z-tabs :list="tabs" @change="doTab" />
</view>
<view class="h-20rpx" />
</template>
<view class="flex-col justify-center card p-3 m-b-20rpx" v-for="(v, i) in pageHook.list" :key="i">
<view class="flex justify-between items-center">
<view v-if="v.checkStatus == '1'" class="p-1 c-white! sub-text-26 rounded-1 bg-#f5a623">待接受</view>
<view v-if="v.checkStatus == '2'" class="p-1 c-white! sub-text-26 rounded-1 bg-#2db7f5">待到访</view>
<view v-if="v.checkStatus == '3'" class="p-1 c-white! sub-text-26 rounded-1 bg-#52c41a">已拒绝</view>
<view v-if="v.checkStatus == '5'" class="p-1 c-white! sub-text-26 rounded-1 bg-#f5222d">已取消</view>
<view class="sub-text-26">{{ v.vstartDate + " ~ " + v.vendDate }}</view>
</view>
<view class="flex-col my-1 px-4 py-2 bg-#ECF4F9 rounded-4">
<view class="flex justify-start sub-text-30 whitespace-nowrap">
<view>{{ v.name }}</view>
<view class="second-text-28 ml-2">{{ v.mobile }}</view>
</view>
<view class="flex justify-center sub-text-28 font-italic whitespace-nowrap my-2">
<span v-if="v.reasonValue">拜访原因:{{ visitorReasonList.find((item) => item.id == v.reasonValue)?.title || "" }}</span>
</view>
<!-- <view class="flex justify-end second-text-28 whitespace-nowrap">
<span v-if="state.params.type == 1">接待人:{{ v.userName }}</span>
</view> -->
</view>
<view class="flex justify-end items-center gap-2">
<!-- <view v-if="v.checkStatus == 1" class="flex items-center gap-2">
<tm-modal title="确定审核通过吗?" height="auto" @confirm="visitorCheck(v.id, 2)">
<template v-slot:trigger><tm-button class="px-1" size="s">审核通过</tm-button></template>
</tm-modal>
<tm-modal title="确定审核拒绝吗?" height="auto" @confirm="visitorCheck(v.id, 3)">
<template v-slot:trigger><tm-button class="px-1" size="s" color="danger">审核拒绝</tm-button></template>
</tm-modal>
</view> -->
<tm-button v-if="v.checkStatus == '2'" class="px-1" size="s" color="success" @click.stop="goDetail(v)">查看详情</tm-button>
</view>
</view>
</z-paging>
<template #main-bottom>
<view class="h-100rpx flex justify-around items-center">
<view class="flex-col justify-center items-center" @click="goInvitation">
<image src="@/static/page/invitation.png" class="w-50rpx h-50rpx" />
<view class="text-center text-24 c-#999999">访客邀约</view>
</view>
<view class="flex-col justify-center items-center">
<image src="@/static/page/order-select.png" class="w-50rpx h-50rpx" />
<view class="text-center text-24 c-#1977FD">邀约记录</view>
</view>
</view>
</template>
</crud-layout>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from "vue"
import { onShow } from "@dcloudio/uni-app"
import { hooks, router } from "@@/index"
import zTabs from "@@/components/other/z-tabs/z-tabs.vue"
import { invitationApi } from "@/sheep/api"
const tabs = [
{ value: "", name: "全部", disabled: false },
{ value: "1", name: "待接受", disabled: false },
{ value: "2", name: "待到访", disabled: false },
{ value: "3", name: "已拒绝", disabled: false },
{ value: "5", name: "已取消", disabled: false },
]
const state = reactive<any>({
params: {
checkStatus: tabs[0].value,
type: 2, // type为1:主动申请 type为2:邀约 type为3:后台创建
},
})
const pageRef = ref<any>(null)
const visitorReasonList = ref<{ title: string; id: string }[]>([])
onShow(async () => {
const isLogin = await hooks.useCheckLogin()
if (!isLogin) return
if (pageRef.value) {
pageRef.value.reload(false)
}
})
onMounted(() => {
getVisitorReasonList()
})
// 获取拜访原因列表
const getVisitorReasonList = async () => {
const { code, data } = await invitationApi.getDictListByType("1")
if (code == 0 && data) {
visitorReasonList.value = data.map((item: any) => ({ title: item.label, id: item.value }))
return
}
visitorReasonList.value = []
}
const doTab = (e: any) => {
const target = tabs[e]
state.params.checkStatus = target.value
pageRef.value.reload(true)
}
const pageHook = hooks.usePage(pageRef, invitationApi.getInvitationRecord, null, "data.records")
const query = (page: number, pagesize: number) => {
const isLogin = hooks.useStaticCheckLogin()
if (!isLogin) return
pageHook.startPage(page, pagesize, false, state.params)
}
// const onTabChange = (e: number) => {
// state.params.type = e + 1
// pageRef.value.reload(false)
// }
// 审核通过/拒绝
// const visitorCheck = async (id: string, checkStatus: number | string) => {
// try {
// const { code, msg } = await invitationApi.updateCheckStatus({ id, checkStatus })
// if (code == 0) {
// uni.showToast({ title: "操作成功", icon: "success" })
// pageRef.value.reload(false)
// return
// }
// uni.showToast({ title: msg, icon: "error" })
// } catch (error: any) {
// uni.showToast({ title: error.message, icon: "error" })
// }
// }
// 查看详情
const goDetail = (v: any) => {
router.jump("/pages/integrated/visitor/detail", { id: v.id, token: v.token })
}
const goInvitation = () => {
uni.redirectTo({ url: "/pages/integrated/visitor/invitation" })
}
</script>
+236
View File
@@ -0,0 +1,236 @@
<template>
<view class="visitor-detail">
<view class="detail-header">
<view class="visitor-info">
<text class="visitor-name">{{ detail.visitorName }}</text>
<text class="visitor-phone">{{ detail.visitorPhone }}</text>
</view>
<view class="status-tag" :class="getStatusClass(detail.status)">{{ getStatusText(detail.status) }}</view>
</view>
<view class="detail-content">
<view class="info-row">
<text class="label">来访目的:</text>
<text class="value">{{ getPurposeLabel(detail.purpose) }}</text>
</view>
<view class="info-row">
<text class="label">被访人:</text>
<text class="value">{{ detail.hostName }}</text>
</view>
<view class="info-row">
<text class="label">来访时间:</text>
<text class="value">{{ formatVisitTime(detail.visitDate, detail.visitTime) }}</text>
</view>
<view v-if="detail.remark" class="info-row">
<text class="label">备注说明:</text>
<text class="value">{{ detail.remark }}</text>
</view>
</view>
<view v-if="detail.rejectReason" class="reject-section">
<view class="reject-title">拒绝原因</view>
<view class="reject-content">{{ detail.rejectReason }}</view>
</view>
<view class="detail-footer">
<text class="apply-time">申请时间:{{ formatTime(detail.createTime) }}</text>
</view>
</view>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import meetingRoomApi from '@/sheep/api/meetingRoom'
const detail = ref<any>({})
const purposeOptions = [
{ value: '1', label: '商务洽谈' },
{ value: '2', label: '技术交流' },
{ value: '3', label: '面试' },
{ value: '4', label: '参观访问' },
{ value: '5', label: '其他' }
]
const timeOptions = [
{ value: '1', label: '上午 (9:00-12:00)' },
{ value: '2', label: '下午 (14:00-18:00)' },
{ value: '3', label: '全天 (9:00-18:00)' }
]
function getPurposeLabel(purpose: string) {
const option = purposeOptions.find(item => item.value === purpose)
return option ? option.label : '其他'
}
function getStatusText(status: string) {
const statusMap: Record<string, string> = {
'1': '待审核',
'2': '已通过',
'3': '已拒绝',
'4': '已取消',
'5': '已完成'
}
return statusMap[status] || '未知'
}
function getStatusClass(status: string) {
const classMap: Record<string, string> = {
'1': 'status-pending',
'2': 'status-approved',
'3': 'status-rejected',
'4': 'status-cancelled',
'5': 'status-completed'
}
return classMap[status] || 'status-pending'
}
function formatVisitTime(date: string, time: string) {
const timeLabel = timeOptions.find(item => item.value === time)?.label || ''
return `${date} ${timeLabel}`
}
function formatTime(time: string) {
if (!time) return ''
return new Date(time).toLocaleString()
}
onLoad(async (options: any) => {
const id = options.id
if (!id) return
try {
const response: any = await meetingRoomApi.getVisitorDetail(id)
if (response.code === 0 && response.data) {
detail.value = response.data
}
} catch (error) {
console.error('获取访客详情失败:', error)
uni.showToast({ title: '获取详情失败', icon: 'none' })
}
})
</script>
<style lang="scss" scoped>
.visitor-detail {
padding: 32rpx;
background: #f5f5f5;
min-height: 100vh;
}
.detail-header {
background: #fff;
border-radius: 16rpx;
padding: 32rpx;
margin-bottom: 24rpx;
display: flex;
justify-content: space-between;
align-items: center;
.visitor-info {
.visitor-name {
font-size: 32rpx;
font-weight: bold;
color: #333;
margin-right: 16rpx;
}
.visitor-phone {
font-size: 26rpx;
color: #666;
}
}
.status-tag {
padding: 12rpx 24rpx;
border-radius: 24rpx;
font-size: 26rpx;
&.status-pending {
background: #fff3e0;
color: #f57c00;
}
&.status-approved {
background: #e8f5e8;
color: #388e3c;
}
&.status-rejected {
background: #ffebee;
color: #d32f2f;
}
&.status-cancelled {
background: #f5f5f5;
color: #999;
}
&.status-completed {
background: #e3f2fd;
color: #1976d2;
}
}
}
.detail-content {
background: #fff;
border-radius: 16rpx;
padding: 32rpx;
margin-bottom: 24rpx;
.info-row {
display: flex;
margin-bottom: 16rpx;
&:last-child {
margin-bottom: 0;
}
.label {
font-size: 28rpx;
color: #666;
width: 160rpx;
flex-shrink: 0;
}
.value {
font-size: 28rpx;
color: #333;
flex: 1;
}
}
}
.reject-section {
background: #fff;
border-radius: 16rpx;
padding: 32rpx;
margin-bottom: 24rpx;
.reject-title {
font-size: 30rpx;
font-weight: bold;
color: #d32f2f;
margin-bottom: 16rpx;
}
.reject-content {
font-size: 28rpx;
color: #666;
line-height: 1.6;
}
}
.detail-footer {
background: #fff;
border-radius: 16rpx;
padding: 32rpx;
.apply-time {
font-size: 26rpx;
color: #999;
}
}
</style>
+421
View File
@@ -0,0 +1,421 @@
<template>
<view class="visitor-container">
<!-- 访客申请表单 -->
<view class="visitor-form">
<view class="form-title">访客申请</view>
<view class="form-item">
<view class="label">访客姓名</view>
<input v-model="visitorName" placeholder="请输入访客姓名" class="name-input" />
</view>
<view class="form-item">
<view class="label">访客手机号</view>
<input v-model="visitorPhone" placeholder="请输入访客手机号" class="phone-input" />
</view>
<view class="form-item">
<view class="label">来访目的</view>
<picker @change="onPurposeChange" :value="purposeIndex" :range="purposeOptions" range-key="label">
<view class="picker">{{ purposeOptions[purposeIndex].label }}</view>
</picker>
</view>
<view class="form-item">
<view class="label">来访时间</view>
<picker mode="date" @change="onDateChange" :value="visitDate">
<view class="picker">{{ visitDate || "请选择来访日期" }}</view>
</picker>
</view>
<view class="form-item">
<view class="label">来访时段</view>
<picker @change="onTimeChange" :value="timeIndex" :range="timeOptions" range-key="label">
<view class="picker">{{ timeOptions[timeIndex].label }}</view>
</picker>
</view>
<view class="form-item">
<view class="label">被访人</view>
<input v-model="hostName" placeholder="请输入被访人姓名" class="host-input" />
</view>
<view class="form-item">
<view class="label">备注说明</view>
<textarea v-model="remark" placeholder="请输入备注说明(选填)" class="remark-textarea" maxlength="200" />
<view class="char-count">{{ remark.length }}/200</view>
</view>
<button @click="submitVisitor" class="submit-btn" :disabled="!canSubmit">提交申请</button>
</view>
<!-- 访客记录列表 -->
<view class="visitor-list">
<view class="list-title">访客记录</view>
<view v-if="visitorList.length === 0" class="empty-tip"> 暂无访客记录 </view>
<view v-else>
<view v-for="item in visitorList" :key="item.id" class="visitor-item" @click="viewDetail(item)">
<view class="item-header">
<view class="visitor-info">
<text class="visitor-name">{{ item.visitorName }}</text>
<text class="visitor-phone">{{ item.visitorPhone }}</text>
</view>
<view class="status-tag" :class="getStatusClass(item.status)">{{ getStatusText(item.status) }}</view>
</view>
<view class="item-content">
<view class="purpose">来访目的:{{ getPurposeLabel(item.purpose) }}</view>
<view class="host">被访人:{{ item.hostName }}</view>
<view class="time">来访时间:{{ formatVisitTime(item.visitDate, item.visitTime) }}</view>
</view>
<view class="item-footer">
<text class="apply-time">申请时间:{{ formatTime(item.createTime) }}</text>
<text v-if="item.remark" class="has-remark">有备注</text>
</view>
</view>
</view>
</view>
</view>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from "vue"
import meetingRoomApi from "@/sheep/api/meetingRoom"
const purposeOptions = [
{ value: "1", label: "商务洽谈" },
{ value: "2", label: "技术交流" },
{ value: "3", label: "面试" },
{ value: "4", label: "参观访问" },
{ value: "5", label: "其他" },
]
const timeOptions = [
{ value: "1", label: "上午 (9:00-12:00)" },
{ value: "2", label: "下午 (14:00-18:00)" },
{ value: "3", label: "全天 (9:00-18:00)" },
]
const visitorName = ref("")
const visitorPhone = ref("")
const purposeIndex = ref(0)
const visitDate = ref("")
const timeIndex = ref(0)
const hostName = ref("")
const remark = ref("")
const visitorList = ref<any[]>([])
const canSubmit = computed(() => {
return visitorName.value.trim() && visitorPhone.value.trim() && visitDate.value && hostName.value.trim()
})
function onPurposeChange(e: any) {
purposeIndex.value = e.detail.value
}
function onDateChange(e: any) {
visitDate.value = e.detail.value
}
function onTimeChange(e: any) {
timeIndex.value = e.detail.value
}
function getPurposeLabel(purpose: string) {
const option = purposeOptions.find((item) => item.value === purpose)
return option ? option.label : "其他"
}
function getStatusText(status: string) {
const statusMap: Record<string, string> = {
"1": "待审核",
"2": "已通过",
"3": "已拒绝",
"4": "已取消",
"5": "已完成",
}
return statusMap[status] || "未知"
}
function getStatusClass(status: string) {
const classMap: Record<string, string> = {
"1": "status-pending",
"2": "status-approved",
"3": "status-rejected",
"4": "status-cancelled",
"5": "status-completed",
}
return classMap[status] || "status-pending"
}
function formatVisitTime(date: string, time: string) {
const timeLabel = timeOptions.find((item) => item.value === time)?.label || ""
return `${date} ${timeLabel}`
}
function formatTime(time: string) {
if (!time) return ""
return new Date(time).toLocaleDateString()
}
async function submitVisitor() {
if (!canSubmit.value) {
uni.showToast({ title: "请填写完整信息", icon: "none" })
return
}
// 验证手机号格式
const phoneRegex = /^1[3-9]\d{9}$/
if (!phoneRegex.test(visitorPhone.value)) {
uni.showToast({ title: "请输入正确的手机号", icon: "none" })
return
}
try {
uni.showLoading({ title: "提交中..." })
const data = {
visitorName: visitorName.value.trim(),
visitorPhone: visitorPhone.value.trim(),
purpose: purposeOptions[purposeIndex.value].value,
visitDate: visitDate.value,
visitTime: timeOptions[timeIndex.value].value,
hostName: hostName.value.trim(),
remark: remark.value.trim(),
}
const response: any = await meetingRoomApi.submitVisitor(data)
uni.hideLoading()
if (response.code === 0) {
uni.showToast({ title: "提交成功", icon: "success" })
// 清空表单
visitorName.value = ""
visitorPhone.value = ""
purposeIndex.value = 0
visitDate.value = ""
timeIndex.value = 0
hostName.value = ""
remark.value = ""
// 刷新列表
loadVisitorList()
} else {
uni.showToast({ title: response.msg || "提交失败", icon: "none" })
}
} catch (error) {
uni.hideLoading()
uni.showToast({ title: "提交失败", icon: "none" })
}
}
function viewDetail(item: any) {
uni.navigateTo({
url: `/pages/integrated/visitor/detail?id=${item.id}`,
})
}
async function loadVisitorList() {
try {
const response: any = await meetingRoomApi.getVisitorList({
current: 1,
size: 20,
})
if (response.code === 0 && response.data) {
visitorList.value = response.data.records || []
}
} catch (error) {
console.error("加载访客记录失败:", error)
}
}
onMounted(() => {
loadVisitorList()
})
</script>
<style lang="scss" scoped>
.visitor-container {
padding: 24rpx;
background: #f5f5f5;
min-height: 100vh;
}
.visitor-form {
background: #fff;
border-radius: 16rpx;
padding: 32rpx;
margin-bottom: 24rpx;
.form-title {
font-size: 32rpx;
font-weight: bold;
margin-bottom: 24rpx;
}
.form-item {
margin-bottom: 24rpx;
.label {
font-size: 28rpx;
color: #333;
margin-bottom: 12rpx;
}
.picker {
height: 80rpx;
line-height: 80rpx;
padding: 0 24rpx;
background: #f7f8fa;
border-radius: 8rpx;
font-size: 28rpx;
}
.name-input,
.phone-input,
.host-input {
height: 80rpx;
padding: 0 24rpx;
background: #f7f8fa;
border-radius: 8rpx;
font-size: 28rpx;
}
.remark-textarea {
width: 100%;
height: 120rpx;
padding: 24rpx;
background: #f7f8fa;
border-radius: 8rpx;
font-size: 28rpx;
box-sizing: border-box;
}
.char-count {
text-align: right;
font-size: 24rpx;
color: #999;
margin-top: 8rpx;
}
}
.submit-btn {
width: 100%;
height: 88rpx;
background: #007aff;
color: #fff;
border-radius: 8rpx;
font-size: 32rpx;
&:disabled {
background: #ccc;
}
}
}
.visitor-list {
background: #fff;
border-radius: 16rpx;
padding: 32rpx;
.list-title {
font-size: 32rpx;
font-weight: bold;
margin-bottom: 24rpx;
}
.empty-tip {
text-align: center;
color: #999;
font-size: 28rpx;
padding: 60rpx 0;
}
.visitor-item {
border-bottom: 1rpx solid #f0f0f0;
padding: 24rpx 0;
&:last-child {
border-bottom: none;
}
.item-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16rpx;
.visitor-info {
.visitor-name {
font-size: 30rpx;
font-weight: bold;
color: #333;
margin-right: 16rpx;
}
.visitor-phone {
font-size: 26rpx;
color: #666;
}
}
.status-tag {
padding: 8rpx 16rpx;
border-radius: 20rpx;
font-size: 24rpx;
&.status-pending {
background: #fff3e0;
color: #f57c00;
}
&.status-approved {
background: #e8f5e8;
color: #388e3c;
}
&.status-rejected {
background: #ffebee;
color: #d32f2f;
}
&.status-cancelled {
background: #f5f5f5;
color: #999;
}
&.status-completed {
background: #e3f2fd;
color: #1976d2;
}
}
}
.item-content {
margin-bottom: 16rpx;
.purpose,
.host,
.time {
font-size: 26rpx;
color: #666;
margin-bottom: 8rpx;
line-height: 1.4;
}
}
.item-footer {
display: flex;
justify-content: space-between;
align-items: center;
.apply-time {
font-size: 24rpx;
color: #999;
}
.has-remark {
font-size: 24rpx;
color: #007aff;
background: #e3f2fd;
padding: 4rpx 12rpx;
border-radius: 12rpx;
}
}
}
}
</style>
-3
View File
@@ -1,3 +0,0 @@
<template></template>
<script setup lang="ts"></script>
+10 -14
View File
@@ -40,19 +40,7 @@ const moduleList = [
launchParam: "flowStatistics",
},
{
pagePath: "/pages/tabbar/invitation",
text: "访客",
iconPath: "/static/tabbar/invitation-select.png",
launchParam: "invitation",
},
{
pagePath: "/pages/tabbar/message",
text: "通知公告",
iconPath: "/static/tabbar/message-select.png",
launchParam: "message",
},
{
pagePath: "/pages/integrated/index",
pagePath: "/pages/tabbar/integrated",
text: "综合服务",
iconPath: "/static/tabbar/integrated-select.png",
launchParam: "integrated",
@@ -87,6 +75,10 @@ const jumpByParam = async () => {
uni.setStorageSync("launchParam", launchParam)
uni.switchTab({ url: "/pages/tabbar/coopMerchant" })
break
case "integrated": // 综合服务
uni.setStorageSync("launchParam", launchParam)
uni.switchTab({ url: "/pages/tabbar/integrated" })
break
default:
uni.hideTabBar()
break
@@ -99,7 +91,7 @@ const jumpByParam = async () => {
const userRole = uni.getStorageSync("userRole") // 获取用户角色
if (!launchParam) return uni.hideTabBar()
if (launchParam == "thirdApp") return uni.reLaunch({ url: "/pages/thirdApp/thirdAppList" })
if (launchParam == "thirdApp") return uni.reLaunch({ url: "/pages/thirdApp/happyMall" })
switch (userRole) {
case "merchant":
@@ -117,6 +109,10 @@ const jumpByParam = async () => {
uni.setStorageSync("launchParam", launchParam)
return uni.reLaunch({ url: "/pages/tabbar/coopMerchant" })
}
if (launchParam === "integrated") {
uni.setStorageSync("launchParam", launchParam)
return uni.reLaunch({ url: "/pages/tabbar/integrated" })
}
case "doorAdmin":
return uni.switchTab({ url: "/pages/tabbar/cabinet" })
default:
+143
View File
@@ -0,0 +1,143 @@
<template>
<crud-layout bg="/static/page/blue-bg.png">
<div class="integrated-title-card">
<div class="integrated-title">综合服务</div>
</div>
<div class="service-container-card">
<div class="service-container">
<div class="service-grid">
<div v-for="item in services" :key="item.name" class="service-item" @click="goPage(item)">
<image :src="item.icon" class="service-icon" />
<div class="service-label">{{ item.label }}</div>
</div>
</div>
</div>
</div>
</crud-layout>
</template>
<script setup lang="ts">
import { onLoad } from "@dcloudio/uni-app"
const services = [
{
name: "todo",
label: "工作待办",
icon: "/src/static/icon/todo.png",
url: "/pages/integrated/todo/index",
},
{
name: "meeting-booking",
label: "会议预定",
icon: "/src/static/icon/meeting-booking.png",
url: "/pages/integrated/meeting/index",
},
{
name: "lost-and-found",
label: "失物招领",
icon: "/src/static/icon/lost-and-found.png",
url: "/pages/integrated/lostAndFound/index",
},
{
name: "feedback",
label: "意见反馈",
icon: "/src/static/icon/feedback.png",
url: "/pages/integrated/feedback/index",
},
{
name: "repair-report",
label: "维修上报",
icon: "/src/static/icon/repair-report.png",
url: "/pages/integrated/repair/index",
},
{
name: "notice",
label: "通知公告",
icon: "/src/static/icon/notice.png",
url: "/pages/integrated/notice/index",
},
{
name: "visitor",
label: "访客邀约",
icon: "/src/static/icon/visitor.png",
url: "/pages/integrated/visitor/invitation",
},
]
onLoad(() => {
const app = getApp()
app._initTabbar()
})
function goPage(item) {
if (item.url) {
uni.navigateTo({ url: item.url })
}
}
</script>
<style scoped lang="less">
.integrated-title-card {
background: #fff;
border-radius: 20rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.06);
margin: 32rpx 24rpx 0 24rpx;
padding: 0 0 8rpx 0;
}
.integrated-title {
font-size: 38rpx;
font-weight: bold;
color: #222;
text-align: center;
padding: 36rpx 0 24rpx 0;
letter-spacing: 2rpx;
}
.service-container-card {
background: #fff;
border-radius: 24rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.06);
margin: 24rpx;
padding-bottom: 32rpx;
}
.service-container {
padding: 32rpx 0 0 0;
background: transparent;
border-radius: 16rpx;
}
.service-grid {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
padding: 0 24rpx;
}
.service-item {
width: 25%;
margin-bottom: 40rpx;
display: flex;
flex-direction: column;
align-items: center;
transition: transform 0.2s;
}
.service-item:hover {
transform: translateY(-6rpx) scale(1.06);
box-shadow: 0 2rpx 12rpx rgba(25, 118, 210, 0.08);
}
.service-icon {
width: 92rpx;
height: 92rpx;
margin-bottom: 14rpx;
border-radius: 16rpx;
background: #f5f7fa;
box-shadow: 0 2rpx 8rpx rgba(25, 118, 210, 0.04);
transition: box-shadow 0.2s;
}
.service-label {
font-size: 28rpx;
color: #333;
text-align: center;
letter-spacing: 1rpx;
}
body {
background: #f5f6fa;
}
</style>
-158
View File
@@ -1,158 +0,0 @@
<template>
<crud-layout mainIsScroll bg="/static/page/blue-bg.png">
<view class="h-20rpx" />
<tm-sheet margin="[0]">
<view class="mx-auto mb-1 text-center text-lg">访客邀约</view>
<tm-alert :border="['2']" :borderColor="['#89c0ff']" :darkBorderColor="['#3b5066']">访客邀约请填写以下信息</tm-alert>
<tm-form @submit="onsubmit" v-model="form" :rules="rules" direction="vertical">
<tm-form-item name="name" label="访客姓名">
<tm-input color="transparent" v-model.trim="form.name" placeholder="请输入真实姓名" />
</tm-form-item>
<tm-form-item name="mobile" label="访客手机号">
<tm-input color="transparent" v-model.trim="form.mobile" placeholder="请输入访客手机号" />
</tm-form-item>
<tm-form-item name="companyName" label="来访公司名称">
<tm-input color="transparent" v-model.trim="form.companyName" placeholder="来访公司名称" />
</tm-form-item>
<tm-form-item name="vStartDate" label="拜访开始时间">
<tm-picker-date
v-model="form.vStartDate"
v-model:modelStr="_timestr"
type="minute"
start="2020-01-01 00:00"
end="2080-01-01 23:59"
>
<tm-input
:modelValue="form.vStartDate"
placeholder="请选择拜访开始时间"
style="pointer-events: none"
disabled
color="transparent"
>
<template v-slot:rightIcon>
<tm-icon name="arrow-right-s-line"></tm-icon>
</template>
</tm-input>
</tm-picker-date>
</tm-form-item>
<tm-form-item name="vEndDate" label="拜访离开时间">
<tm-picker-date v-model="form.vEndDate" v-model:modelStr="_timestr" type="minute" start="2020-01-01 00:00" end="2080-01-01 23:59">
<tm-input
:modelValue="form.vEndDate"
placeholder="请选择拜访离开时间"
style="pointer-events: none"
disabled
color="transparent"
>
<template v-slot:rightIcon>
<tm-icon name="arrow-right-s-line"></tm-icon>
</template>
</tm-input>
</tm-picker-date>
</tm-form-item>
<tm-form-item name="deptId" label="拜访部门">
<tm-drawer :show-close="true" :show-footer="true" size="auto" title="请选择拜访部门">
<template #trigger>
<tm-input :modelValue="form.deptId" placeholder="请选择拜访部门" style="pointer-events: none" disabled color="transparent">
<template v-slot:rightIcon>
<tm-icon name="arrow-right-s-line"></tm-icon>
</template>
</tm-input>
</template>
<tm-text color="#999999">精致美观的对话框,可全局配置风格</tm-text>
</tm-drawer>
</tm-form-item>
<tm-form-item name="reasonValue" label="拜访原因">
<tm-picker v-model="form.reasonValue" v-model:modelStr="_pingtaistr" rangText="reasonValue" :list="visitorReasonList">
<tm-input :modelValue="_pingtaistr" placeholder="请选择拜访原因" style="pointer-events: none" disabled color="transparent">
<template #rightIcon>
<tm-icon name="arrow-right-s-line"></tm-icon>
</template>
</tm-input>
</tm-picker>
</tm-form-item>
<tm-form-item name="isOtherPeople" label="是否可随行人员" direction="horizontal" labelWidth="500">
<tm-switch v-model="form.isOtherPeople" />
</tm-form-item>
<tm-form-item name="isDrive" label="是否可驾驶车辆" direction="horizontal" labelWidth="500">
<tm-switch v-model="form.isDrive" />
</tm-form-item>
<tm-button form-type="submit" block>提交</tm-button>
</tm-form>
</tm-sheet>
</crud-layout>
</template>
<script setup lang="ts">
import { onLoad } from "@dcloudio/uni-app"
import { reactive, ref } from "vue"
import { invitationApi } from "@/sheep/api/index"
const form = reactive({
name: "",
mobile: "",
companyName: "",
vStartDate: "",
vEndDate: "",
deptId: "",
reasonValue: "",
isOtherPeople: true,
isDrive: true,
})
const _timestr = ref("2025-01-01")
const _pingtaistr = ref("")
const visitorReasonList = ref([])
onLoad(() => {
const app = getApp()
app._initTabbar()
getVisitorReasonList()
})
// 获取拜访原因列表
const getVisitorReasonList = async () => {
const { code, data } = await invitationApi.getDictListByType("1")
if (code == 0 && data) {
visitorReasonList.value = data
return
}
visitorReasonList.value = []
}
const rules: Record<string, TM.FORM_RULE[]> = {
name: [{ required: true, message: "姓名2到5个字符", rule: /^.{2,5}$/ }],
mobile: [{ required: true, message: "请输入正确的手机号", rule: /^(?:(?:\+|00)86)?1\d{10}$/ }],
companyName: [{ required: true, message: "请输入来访公司名称" }],
vStartDate: [{ required: true, message: "请选择拜访开始时间" }],
vEndDate: [{ required: true, message: "请选择拜访离开时间" }],
deptId: [{ required: true, message: "请选择拜访部门" }],
reasonValue: [{ required: true, message: "请选择拜访原因" }],
}
// 表单提交
const onsubmit = (evt: TM.FORM_SUBMIT_RESULT) => {
console.log("校验结果:", evt)
if (!evt.isPass) return
uni.showModal({
title: "提示",
content: "确认提交吗?",
confirmColor: "#0094ff",
success: async () => {
try {
const { code, data, msg } = await invitationApi.saveInvitation(form)
if (code == 0) {
uni.showToast({ title: "提交成功", icon: "success" })
uni.switchTab({ url: "/pages/tabbar/invitationRecord" })
return
}
uni.showToast({ title: msg || "提交失败", icon: "none" })
} catch (error: any) {
uni.showToast({ title: error.message || "提交失败", icon: "none" })
}
},
})
}
</script>
-137
View File
@@ -1,137 +0,0 @@
<template>
<crud-layout bg="/static/page/blue-bg.png">
<z-paging
ref="pageRef"
height="100%"
:fixed="false"
v-model="pageHook.list"
:default-page-size="10"
safe-area-inset-bottom
:loading-more-enabled="true"
:show-scrollbar="false"
@query="query"
:auto-clean-list-when-reload="false"
:auto-scroll-to-top-when-reload="false"
@scroll="pageHook.scroll"
>
<template #top>
<view class="bg-white rounded-20rpx my-20rpx font-bold">
<z-tabs
:list="[
{ name: '审核申请', value: 0 },
{ name: '我的邀约', value: 1 },
]"
bar-width="150"
:active-style="{
fontSize: '30rpx',
}"
:inactive-style="{
fontSize: '30rpx',
}"
@change="onTabChange"
/>
</view>
<view class="bg-white rounded-16rpx px-20rpx">
<z-tabs :list="tabs" @change="doTab" />
</view>
<view class="h-20rpx" />
</template>
<view class="flex-col justify-center card p-3 m-b-20rpx" v-for="(v, i) in pageHook.list" :key="i">
<view class="flex justify-between items-center">
<view v-if="v.checkStatus == '1'" class="p-1 c-white! sub-text-26 rounded-1 bg-#f5a623">待审核</view>
<view v-if="v.checkStatus == '2'" class="p-1 c-white! sub-text-26 rounded-1 bg-#2db7f5">待到访</view>
<view v-if="v.checkStatus == '3'" class="p-1 c-white! sub-text-26 rounded-1 bg-#52c41a">已拒绝</view>
<view v-if="v.checkStatus == '5'" class="p-1 c-white! sub-text-26 rounded-1 bg-#f5222d">已取消</view>
<view class="sub-text-26">{{ v.vstartDate + " ~ " + v.vendDate }}</view>
</view>
<view class="flex justify-center items-center my-2">
<view class="sub-text-32 mr-4 flex-col-center font-italic bg-#ECF4F9 p-4 rounded-lt-12 rounded-rb-12">
<view>{{ v.name }}</view>
<view>{{ v.mobile }}</view>
</view>
<view class="second-text-28 bg-#ECF4F9 p-4 rounded-lb-12 rounded-rt-12">接待人:{{ v.userName }}</view>
</view>
<view v-if="v.checkStatus == 1" class="flex justify-end items-center gap-2">
<tm-modal title="确定审核通过吗?" height="auto" @confirm="visitorCheck(v.id, 2)">
<template v-slot:trigger><tm-button class="px-1" size="s">审核通过</tm-button></template>
</tm-modal>
<tm-modal title="确定审核拒绝吗?" height="auto" @confirm="visitorCheck(v.id, 3)">
<template v-slot:trigger><tm-button class="px-1" size="s" color="danger">审核拒绝</tm-button></template>
</tm-modal>
</view>
</view>
</z-paging>
</crud-layout>
</template>
<script setup lang="ts">
import { ref, reactive } from "vue"
import { onLoad, onShow } from "@dcloudio/uni-app"
import { hooks } from "@@/index"
import zTabs from "@@/components/other/z-tabs/z-tabs.vue"
import { invitationApi } from "@/sheep/api"
const tabs = [
{ value: "", name: "全部", disabled: false },
{ value: "1", name: "待审核", disabled: false },
{ value: "2", name: "待到访", disabled: false },
{ value: "3", name: "已拒绝", disabled: false },
{ value: "5", name: "已取消", disabled: false },
]
const state = reactive<any>({
params: {
checkStatus: tabs[0].value,
type: 1, // type为1:主动申请 type为2:邀约 type为3:后台创建
},
})
const pageRef = ref<any>(null)
onLoad(() => {
const app = getApp()
app._initTabbar()
})
onShow(async () => {
const isLogin = await hooks.useCheckLogin()
if (!isLogin) return
if (pageRef.value) {
pageRef.value.reload(false)
}
})
const doTab = (e: any) => {
const target = tabs[e]
state.params.checkStatus = target.value
pageRef.value.reload(true)
}
const pageHook = hooks.usePage(pageRef, invitationApi.getInvitationRecord, null, "data.records")
const query = (page: number, pagesize: number) => {
const isLogin = hooks.useStaticCheckLogin()
if (!isLogin) return
pageHook.startPage(page, pagesize, false, state.params)
}
const onTabChange = (e: number) => {
state.params.type = e + 1
pageRef.value.reload(false)
}
// 审核通过/拒绝
const visitorCheck = async (id: string, checkStatus: number | string) => {
try {
const { code, msg } = await invitationApi.updateCheckStatus({ id, checkStatus })
if (code == 0) {
uni.showToast({ title: "操作成功", icon: "success" })
pageRef.value.reload(false)
return
}
uni.showToast({ title: msg, icon: "error" })
} catch (error: any) {
uni.showToast({ title: error.message, icon: "error" })
}
}
</script>
+1 -2
View File
@@ -405,10 +405,9 @@ const initMenuConfig = () => {
{ text: "dev-外来人员就餐", icon: "/static/icon/service_1.png", router: "/pages/apply/foreign-list" },
{ text: "dev-跨单位就餐", icon: "/static/icon/service_1.png", router: "/pages/apply/stride-list" },
{ text: "dev-包厢预定", icon: "/static/icon/service_4.png", router: "/pages/boxes/apply-list" },
{ text: "dev-第三方应用", icon: "/static/icon/login.png", router: "/pages/thirdApp/thirdAppList" },
{ text: "dev-第三方应用", icon: "/static/icon/login.png", router: "/pages/thirdApp/happyMall" },
{ text: "dev-登录", icon: "/static/icon/login.png", router: "/pages/user/login" },
{ text: "dev-角色选择", icon: "/static/icon/login.png", router: "/pages/user/userRole" },
{ text: "dev-意见反馈", icon: "/static/icon/service_5.png", router: "/pages/integrated/feedback/index" },
)
}
}
File renamed without changes.
File renamed without changes.
@@ -1,6 +1,6 @@
<template>
<crud-layout bg="/static/page/blue-bg.png">
<z-paging
<crud-layout mainIsScroll bg="/static/page/blue-bg.png">
<!-- <z-paging
ref="pageRef"
height="100%"
:fixed="false"
@@ -13,8 +13,8 @@
:auto-clean-list-when-reload="false"
:auto-scroll-to-top-when-reload="false"
@scroll="pageHook.scroll"
>
<view class="h-20rpx" />
> -->
<!-- <view class="h-20rpx" />
<view class="flex-row card p-20rpx m-b-20rpx" v-for="(v, i) in pageHook.list" :key="i" @tap="toDetail(v)">
<image class="w-180rpx h-180rpx rounded-12rpx my-auto" :src="filePath(v.logoPic)" mode="aspectFill" />
<view class="flex-1 flex-col m-l-20rpx">
@@ -38,59 +38,71 @@
</view>
</view>
</view>
</view>
</view> -->
<tm-modal
v-model:show="state.show"
:overlayClick="false"
title="注意"
position="bottom"
height="auto"
:showCancel="true"
cancelText="取消"
confirmText="继续访问"
@confirm="thirdAppJump()"
@cancel="state.show = false"
>
<tm-text font-size="30" color="#999999">
您即将离开智慧后勤应用并跳转到第三方应用,请注意个人隐私和账号财产安全。您通过第三方服务页面主动提供给第三方商户的个人信息,相关信息保护责任由第三方商户承担。
</tm-text>
</tm-modal>
</z-paging>
<view class="w-full text-center mt-20">
<image src="@/static/page/happyMall.png" class="rounded-full w-1/3" mode="widthFix" />
<view>美好商城</view>
<view class="h-100rpx" />
<tm-button @click="show = true">继续跳转</tm-button>
</view>
<tm-modal
v-model:show="show"
:overlayClick="false"
title="注意"
position="bottom"
height="auto"
:showCancel="true"
cancelText="取消"
confirmText="继续访问"
@confirm="thirdAppJump()"
@cancel="show = false"
>
<tm-text font-size="30" color="#999999">
您即将离开智慧后勤应用并跳转到第三方应用,请注意个人隐私和账号财产安全。您通过第三方服务页面主动提供给第三方商户的个人信息,相关信息保护责任由第三方商户承担。
</tm-text>
</tm-modal>
<!-- </z-paging> -->
</crud-layout>
</template>
<script setup lang="ts">
import { ref, reactive } from "vue"
import { onShow } from "@dcloudio/uni-app"
import { ref } from "vue"
import { onLoad } from "@dcloudio/uni-app"
import { hooks } from "@@/index"
import { merchantApi } from "@@/api"
import { filePath } from "@@/utils/file"
// import { filePath } from "@@/utils/file"
const params = reactive<any>({
type: "4",
})
// const params = reactive<any>({
// type: "4",
// })
const state = reactive({
show: false,
})
const show = ref(false)
onShow(() => {
if (pageRef.value) pageRef.value.reload(false)
})
const pageRef = ref<any>(null)
const pageHook = hooks.usePage(pageRef, merchantApi.page, null, "data.records")
const query = (page: number, pagesize: number) => {
pageHook.startPage(page, pagesize, false, params)
}
const toDetail = async (v: any) => {
onLoad(async () => {
const isLogin = await hooks.useCheckLogin()
if (isLogin) {
state.show = true
show.value = true
}
}
})
// onShow(() => {
// if (pageRef.value) pageRef.value.reload(false)
// })
// const pageRef = ref<any>(null)
// const pageHook = hooks.usePage(pageRef, merchantApi.page, null, "data.records")
// const query = (page: number, pagesize: number) => {
// pageHook.startPage(page, pagesize, false, params)
// }
// const toDetail = async (v: any) => {
// const isLogin = await hooks.useCheckLogin()
// if (isLogin) {
// state.show = true
// }
// }
// 确认或拒绝协议
const thirdAppJump = async () => {
@@ -98,7 +110,7 @@ const thirdAppJump = async () => {
uni.showLoading({ title: "跳转中", mask: true })
const res: any = await merchantApi.lbLogin()
if (res.code == 0) {
state.show = false
show.value = false
if (!res.data) return uni.showToast({ title: "url错误", icon: "none" })
window.location.href = res.data
}
+8 -1
View File
@@ -4,7 +4,7 @@ export default {
// 获取拜访原因字典
getDictListByType: (type: number | string): Promise<any> =>
request({
url: `/integrated/app/visitor/getDictListByType?type=${type}`,
url: `/integrated/visitors/getDictListByType?type=${type}`,
method: "GET",
}),
@@ -32,4 +32,11 @@ export default {
method: "GET",
params,
}),
// 查看邀约详情
getInvitationDetail: (id: string): Promise<any> =>
request({
url: `/integrated/visitors/${id}`,
method: "GET",
}),
}
File renamed without changes.
+130
View File
@@ -0,0 +1,130 @@
<template>
<view class="date-time-range">
<view
class="date-range-item"
:class="[selectedDateIndex == index ? 'active' : '']"
v-for="(item, index) in getLast7DaysWeek()"
:key="index"
@click="selectDate(item, index)"
>
<view>{{ item.week }}</view>
<view>{{ item.date }}</view>
</view>
</view>
<view class="day-time-point-list">
<button
:class="selectedTimePointIndex == index ? 'active' : ''"
v-for="(item, index) in dayTimePointList"
:key="index"
@click="selectTimePoint(item, index)"
>
{{ item.slice(0, 5) }}
</button>
</view>
<view class="px-20rpx py-2 flex justify-between">
<tm-button skin="thin" block @click="emit('cancel')">取消</tm-button>
&nbsp;&nbsp;
<tm-button block @click="onConfirm">确定</tm-button>
</view>
</template>
<script setup lang="ts">
import { ref, computed } from "vue"
const emit = defineEmits(["confirm", "cancel"])
// 未来一周的日期
const getLast7DaysWeek = (): { week: string; date: string; fullDate: string }[] => {
const weekMap: string[] = ["周日", "周一", "周二", "周三", "周四", "周五", "周六"]
const result: { week: string; date: string; fullDate: string }[] = []
const today = new Date()
for (let i = 0; i < 7; i++) {
const d = new Date(today)
d.setDate(today.getDate() + i)
const week = weekMap[d.getDay()]
const month = (d.getMonth() + 1).toString().padStart(2, "0")
const day = d.getDate().toString().padStart(2, "0")
const fullDate = `${d.getFullYear()}-${month}-${day}`
result.push({
week,
date: `${month}-${day}`,
fullDate,
})
}
return result
}
// 生成时间点列表(8:00-18:00,每半小时一个时间点)
const dayTimePointList = computed(() => {
const times: string[] = []
for (let i = 8; i < 18; i++) {
times.push(`${i.toString().padStart(2, "0")}:00:00`)
times.push(`${i.toString().padStart(2, "0")}:30:00`)
}
times.push("18:00:00") // 添加18:00作为结束时间
return times
})
const selectedDateIndex = ref(0)
const selectedDateValue = ref<{ week: string; date: string; fullDate: string }>(getLast7DaysWeek()[0])
const selectedTimePointIndex = ref(0)
const selectedTimePointValue = ref(dayTimePointList.value[0])
// 选择周
const selectDate = (value: { week: string; date: string; fullDate: string }, index: number) => {
selectedDateValue.value = value
selectedDateIndex.value = index
}
// 选择时间点
const selectTimePoint = (value: string, index: number) => {
selectedTimePointValue.value = value
selectedTimePointIndex.value = index
}
// 确认
function onConfirm() {
emit("confirm", selectedDateValue.value, selectedTimePointValue.value)
}
</script>
<style scoped>
.date-time-range {
display: flex;
justify-content: space-between;
margin-bottom: 30rpx;
font-size: 24rpx;
}
.date-time-range .date-range-item {
padding: 10rpx;
background-color: #eee;
border-radius: 12rpx;
margin: 0 10rpx;
text-align: center;
white-space: nowrap;
}
.date-time-range .active {
background: #0779fd;
border-color: #4988f2;
color: white;
}
.day-time-point-list {
display: flex;
flex-wrap: wrap;
gap: 20rpx;
margin-bottom: 20rpx;
}
.day-time-point-list button {
flex: 1 0 90rpx;
padding: 6rpx 0;
border: none;
background: #eeeeee;
border-radius: 12rpx;
font-size: 26rpx;
cursor: pointer;
}
.day-time-point-list .active {
background-color: #0779fd;
color: white;
}
</style>
-31
View File
@@ -1,31 +0,0 @@
<template>
<view class="flex-row h-100rpx bg-#f6f8fa px-10rpx py-10rpx rounded-16rpx">
<view
class="w-50% flex-row-center color-#818898 font-size-28rpx! rounded-16rpx"
:style="{ color: index == cur ? '#fff' : '#818898', backgroundColor: index == cur ? '#00a624' : '#f6f8fa' }"
v-for="(item, index) in tablist"
@click="changeTab(index)"
>
{{ item.name }}
</view>
</view>
</template>
<script setup lang="ts">
import { reactive, ref } from "vue"
import { onLoad, onShow } from "@dcloudio/uni-app"
let cur = ref(0)
let tablist = ref([
{
name: "我的订单",
},
{
name: "历史订单",
},
])
onLoad(() => {})
const changeTab = (index: number) => {
cur.value = index
}
</script>
Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB

File renamed without changes.

Before

Width:  |  Height:  |  Size: 5.7 KiB

After

Width:  |  Height:  |  Size: 5.7 KiB

File renamed without changes.

Before

Width:  |  Height:  |  Size: 5.5 KiB

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 699 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 677 B