This commit is contained in:
weiwei committed 2025-04-25 16:41:57 +08:00
1 parent 618ac4cdfb
commit 933a5abd3a
3 files changed
+372 -436

No files matched your search

+2 -2
View File
@@ -751,7 +751,7 @@ onMounted(async () => {
.content-wrapper {
min-height: 100%;
padding: 0 0 calc(240rpx + env(safe-area-inset-bottom));
padding: 0 0 150rpx;
background: #fff;
}
@@ -887,7 +887,7 @@ onMounted(async () => {
position: fixed;
left: 30rpx;
right: 30rpx;
bottom: calc(120rpx + env(safe-area-inset-bottom));
bottom: 30rpx;
height: 88rpx;
background: #1989fa;
border-radius: 44rpx;
+366 -429
View File
@@ -1,105 +1,76 @@
<template>
<view class="page-container">
<!-- 顶部状态切换 -->
<view class="status-tabs">
<view
v-for="(tab, index) in tabs"
:key="index"
:class="['tab-item', { active: currentTab === tab.value }]"
@tap="handleTabChange(tab.value)"
>
{{ tab.label }}
</view>
</view>
<view class="page-container">
<!-- 顶部状态切换 -->
<view class="status-tabs">
<view
v-for="(tab, index) in tabs"
:key="index"
:class="['tab-item', { active: currentTab === tab.value }]"
@tap="handleTabChange(tab.value)"
>
{{ tab.label }}
</view>
</view>
<view class="visitor-list">
<view
v-for="(item, index) in visitorList"
:key="item.id"
class="visitor-item"
@tap="goToDetail(item)"
>
<view class="visitor-header">
<text class="visit-time">{{ formatDateTime(item.vStartDate) }}</text>
<text :class="['status-tag', getStatusClass(item.checkStatus)]">{{ getStatusText(item.checkStatus) }}</text>
</view>
<view class="visitor-content">
<view class="info-row">
<text class="label">访客姓名:</text>
<text class="value">{{ item.name }}</text>
</view>
<view class="info-row">
<text class="label">手机号码:</text>
<text class="value">{{ item.mobile }}</text>
</view>
<view class="info-row">
<text class="label">拜访部门:</text>
<text class="value">{{ item.deptName }}</text>
</view>
<view class="info-row" v-if="item.companyName">
<text class="label">来访公司:</text>
<text class="value">{{ item.companyName }}</text>
</view>
<view class="info-row">
<text class="label">拜访时间:</text>
<text class="value">{{ formatDateTime(item.vstartDate) }} - {{ formatDateTime(item.vendDate) }}</text>
</view>
<view class="info-row">
<text class="label">拜访原因:</text>
<text class="value">{{ getReasonText(item.reasonValue) }}</text>
</view>
</view>
<view class="visitor-footer">
<template v-if="item.checkStatus === '1'">
<view class="action-btn reject-btn" @tap="handleReject(item)">拒绝</view>
<view class="action-btn accept-btn" @tap="handleAccept(item)">接受</view>
</template>
<view v-else-if="canCancel(item.checkStatus)" class="action-btn cancel-btn" @tap="handleCancel(item)">取消邀约</view>
</view>
</view>
</view>
<view class="visitor-list">
<view v-for="(item, index) in visitorList" :key="item.id" class="visitor-item" @tap="goToDetail(item)">
<view class="visitor-header">
<text class="visit-time">{{ formatDateTime(item.vStartDate) }}</text>
<text :class="['status-tag', getStatusClass(item.checkStatus)]">{{ getStatusText(item.checkStatus) }}</text>
</view>
<view class="visitor-content">
<view class="info-row">
<text class="label">访客姓名:</text>
<text class="value">{{ item.name }}</text>
</view>
<view class="info-row">
<text class="label">手机号码:</text>
<text class="value">{{ item.mobile }}</text>
</view>
<view class="info-row">
<text class="label">拜访部门:</text>
<text class="value">{{ item.deptName }}</text>
</view>
<view class="info-row" v-if="item.companyName">
<text class="label">来访公司:</text>
<text class="value">{{ item.companyName }}</text>
</view>
<view class="info-row">
<text class="label">拜访时间:</text>
<text class="value">{{ formatDateTime(item.vstartDate) }} - {{ formatDateTime(item.vendDate) }}</text>
</view>
<view class="info-row">
<text class="label">拜访原因:</text>
<text class="value">{{ getReasonText(item.reasonValue) }}</text>
</view>
</view>
<view class="visitor-footer">
<template v-if="item.checkStatus === '1'">
<view class="action-btn reject-btn" @tap="handleReject(item)">拒绝</view>
<view class="action-btn accept-btn" @tap="handleAccept(item)">接受</view>
</template>
<view v-else-if="canCancel(item.checkStatus)" class="action-btn cancel-btn" @tap="handleCancel(item)">取消邀约</view>
</view>
</view>
</view>
<!-- 下拉刷新和上拉加载的提示 -->
<view class="loading-more" v-if="loading">
<text>加载中...</text>
</view>
<view class="no-more" v-if="!hasMore && visitorList.length > 0">
<text>没有更多了</text>
</view>
<view class="empty" v-if="!loading && visitorList.length === 0">
<text>暂无数据</text>
</view>
<!-- 底部固定导航 -->
<view class="bottom-nav">
<view class="nav-item">
<image src="/static/images/visitor/visitor-apply.png" mode="aspectFit" class="nav-icon"></image>
<text class="nav-text active">访客申请</text>
</view>
<view class="nav-item">
<image src="/static/images/visitor/visitor-record.png" mode="aspectFit" class="nav-icon"></image>
<text class="nav-text">申请记录</text>
</view>
<view class="nav-item">
<image src="/static/images/visitor/visitor-invite.png" mode="aspectFit" class="nav-icon"></image>
<text class="nav-text">主动邀约</text>
</view>
<view class="nav-item">
<image src="/static/images/visitor/visitor-invited.png" mode="aspectFit" class="nav-icon"></image>
<text class="nav-text">邀约记录</text>
</view>
<view class="nav-item">
<image src="/static/images/visitor/visitor-my.png" mode="aspectFit" class="nav-icon"></image>
<text class="nav-text">我的</text>
</view>
</view>
</view>
<!-- 下拉刷新和上拉加载的提示 -->
<view class="loading-more" v-if="loading">
<text>加载中...</text>
</view>
<view class="no-more" v-if="!hasMore && visitorList.length > 0">
<text>没有更多了</text>
</view>
<view class="empty" v-if="!loading && visitorList.length === 0">
<text>暂无数据</text>
</view>
</view>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import visitorApi from '@/sheep/api/integrated/visitors'
import dayjs from 'dayjs'
import { ref, onMounted } from "vue"
import visitorApi from "@/sheep/api/integrated/visitors"
import dayjs from "dayjs"
// 定义响应式变量
const visitorList = ref<any[]>([])
@@ -107,439 +78,405 @@ const loading = ref(false)
const hasMore = ref(true)
const current = ref(1)
const size = ref(10)
const currentTab = ref('')
const currentTab = ref("")
const reasonDict = ref<any[]>([])
const tabs = [
{ label: '全部', value: '' },
{ label: '待审核', value: '1' },
{ label: '待到访', value: '2' },
{ label: '已取消', value: '5' }
{ label: "全部", value: "" },
{ label: "待审核", value: "1" },
{ label: "待到访", value: "2" },
{ label: "已取消", value: "5" },
]
// 获取拜访原因字典
const getReasonDict = async () => {
try {
const { code, data } = await visitorApi.getDictListByType(1)
if (code === 0 && data) {
reasonDict.value = data
}
} catch (error) {
console.error('获取拜访原因字典失败:', error)
}
try {
const { code, data } = await visitorApi.getDictListByType(1)
if (code === 0 && data) {
reasonDict.value = data
}
} catch (error) {
console.error("获取拜访原因字典失败:", error)
}
}
// 获取拜访原因文本
const getReasonText = (value: string) => {
const reason = reasonDict.value.find(item => item.value === value)
return reason ? reason.label : value
const reason = reasonDict.value.find((item) => item.value === value)
return reason ? reason.label : value
}
// 格式化日期时间
const formatDateTime = (dateStr: string) => {
if (!dateStr) return ''
return dayjs(dateStr).format('YYYY/MM/DD HH:mm')
if (!dateStr) return ""
return dayjs(dateStr).format("YYYY/MM/DD HH:mm")
}
// 获取状态文本
const getStatusText = (status: string) => {
const statusMap: { [key: string]: string } = {
'1': '待审核',
'2': '待到访',
'5': '已取消',
'0': '已到访'
}
return statusMap[status] || ''
const statusMap: { [key: string]: string } = {
"1": "待审核",
"2": "待到访",
"5": "已取消",
"0": "已到访",
}
return statusMap[status] || ""
}
// 获取状态样式类
const getStatusClass = (status: string) => {
const statusClassMap: { [key: string]: string } = {
'1': 'status-pending',
'2': 'status-waiting',
'5': 'status-cancelled',
'0': 'status-visited'
}
return statusClassMap[status] || ''
const statusClassMap: { [key: string]: string } = {
"1": "status-pending",
"2": "status-waiting",
"5": "status-cancelled",
"0": "status-visited",
}
return statusClassMap[status] || ""
}
// 判断是否可以取消
const canCancel = (status: string) => {
return ['1', '2'].includes(status)
return ["1", "2"].includes(status)
}
// 处理标签切换
const handleTabChange = (value: string) => {
if (currentTab.value === value) return
currentTab.value = value
getVisitorList(true)
if (currentTab.value === value) return
currentTab.value = value
getVisitorList(true)
}
// 获取访客列表
const getVisitorList = async (isRefresh = false) => {
if (isRefresh) {
current.value = 1
hasMore.value = true
visitorList.value = []
}
if (isRefresh) {
current.value = 1
hasMore.value = true
visitorList.value = []
}
if (!hasMore.value || loading.value) return
if (!hasMore.value || loading.value) return
loading.value = true
try {
// 确保先获取拜访原因字典
if (reasonDict.value.length === 0) {
await getReasonDict()
}
loading.value = true
try {
// 确保先获取拜访原因字典
if (reasonDict.value.length === 0) {
await getReasonDict()
}
const { code, data } = await visitorApi.getVisitorYYList({
current: current.value,
size: size.value,
checkStatus: currentTab.value || undefined
})
const { code, data } = await visitorApi.getVisitorYYList({
current: current.value,
size: size.value,
checkStatus: currentTab.value || undefined,
})
if (code === 0 && data) {
if (isRefresh) {
visitorList.value = data.records || []
} else {
visitorList.value = [...visitorList.value, ...(data.records || [])]
}
if (code === 0 && data) {
if (isRefresh) {
visitorList.value = data.records || []
} else {
visitorList.value = [...visitorList.value, ...(data.records || [])]
}
hasMore.value = data.records.length === size.value
if (hasMore.value) {
current.value++
}
}
} catch (error) {
console.error('获取访客列表失败:', error)
} finally {
loading.value = false
}
hasMore.value = data.records.length === size.value
if (hasMore.value) {
current.value++
}
}
} catch (error) {
console.error("获取访客列表失败:", error)
} finally {
loading.value = false
}
}
// 处理取消邀约
const handleCancel = async (item: any) => {
uni.showModal({
title: '提示',
content: '确定要取消该邀约吗?',
success: async (res) => {
if (res.confirm) {
try {
const { code, msg } = await visitorApi.cancelVisitor(item.id)
if (code === 0) {
uni.showToast({
title: '取消成功',
icon: 'success'
})
// 刷新列表
getVisitorList(true)
} else {
uni.showToast({
title: msg || '取消失败',
icon: 'none'
})
}
} catch (error) {
console.error('取消邀约失败:', error)
uni.showToast({
title: '取消失败',
icon: 'none'
})
}
}
}
})
uni.showModal({
title: "提示",
content: "确定要取消该邀约吗?",
success: async (res) => {
if (res.confirm) {
try {
const { code, msg } = await visitorApi.cancelVisitor(item.id)
if (code === 0) {
uni.showToast({
title: "取消成功",
icon: "success",
})
// 刷新列表
getVisitorList(true)
} else {
uni.showToast({
title: msg || "取消失败",
icon: "none",
})
}
} catch (error) {
console.error("取消邀约失败:", error)
uni.showToast({
title: "取消失败",
icon: "none",
})
}
}
},
})
}
// 处理接受邀约
const handleAccept = async (item: any) => {
uni.showModal({
title: '提示',
content: '确定接受该邀约吗?',
success: async (res) => {
if (res.confirm) {
try {
const { code, msg } = await visitorApi.updateCheckStatus({
id: item.id,
checkStatus: '2'
})
if (code === 0) {
uni.showToast({
title: '已接受',
icon: 'success'
})
// 刷新列表
getVisitorList(true)
} else {
uni.showToast({
title: msg || '操作失败',
icon: 'none'
})
}
} catch (error) {
console.error('接受邀约失败:', error)
uni.showToast({
title: '操作失败',
icon: 'none'
})
}
}
}
})
uni.showModal({
title: "提示",
content: "确定接受该邀约吗?",
success: async (res) => {
if (res.confirm) {
try {
const { code, msg } = await visitorApi.updateCheckStatus({
id: item.id,
checkStatus: "2",
})
if (code === 0) {
uni.showToast({
title: "已接受",
icon: "success",
})
// 刷新列表
getVisitorList(true)
} else {
uni.showToast({
title: msg || "操作失败",
icon: "none",
})
}
} catch (error) {
console.error("接受邀约失败:", error)
uni.showToast({
title: "操作失败",
icon: "none",
})
}
}
},
})
}
// 处理拒绝邀约
const handleReject = async (item: any) => {
uni.showModal({
title: '提示',
content: '确定拒绝该邀约吗?',
success: async (res) => {
if (res.confirm) {
try {
const { code, msg } = await visitorApi.updateCheckStatus({
id: item.id,
checkStatus: '3'
})
if (code === 0) {
uni.showToast({
title: '已拒绝',
icon: 'success'
})
// 刷新列表
getVisitorList(true)
} else {
uni.showToast({
title: msg || '操作失败',
icon: 'none'
})
}
} catch (error) {
console.error('拒绝邀约失败:', error)
uni.showToast({
title: '操作失败',
icon: 'none'
})
}
}
}
})
uni.showModal({
title: "提示",
content: "确定拒绝该邀约吗?",
success: async (res) => {
if (res.confirm) {
try {
const { code, msg } = await visitorApi.updateCheckStatus({
id: item.id,
checkStatus: "3",
})
if (code === 0) {
uni.showToast({
title: "已拒绝",
icon: "success",
})
// 刷新列表
getVisitorList(true)
} else {
uni.showToast({
title: msg || "操作失败",
icon: "none",
})
}
} catch (error) {
console.error("拒绝邀约失败:", error)
uni.showToast({
title: "操作失败",
icon: "none",
})
}
}
},
})
}
// 跳转到详情页
const goToDetail = (item: any) => {
uni.navigateTo({
url: `/pages/integrated/visitor/visitorDetail?token=${item.token}`
})
uni.navigateTo({
url: `/pages/integrated/visitor/visitorDetail?token=${item.token}`,
})
}
// 页面生命周期钩子
onMounted(() => {
getReasonDict()
getVisitorList(true)
getReasonDict()
getVisitorList(true)
})
// 页面配置
defineOptions({
navigationBarTitleText: '我的邀约',
enablePullDownRefresh: true,
backgroundColor: '#f5f5f5'
navigationBarTitleText: "我的邀约",
enablePullDownRefresh: true,
backgroundColor: "#f5f5f5",
})
// 下拉刷新
defineExpose({
onPullDownRefresh() {
getVisitorList(true).then(() => {
uni.stopPullDownRefresh()
})
},
onReachBottom() {
getVisitorList()
}
onPullDownRefresh() {
getVisitorList(true).then(() => {
uni.stopPullDownRefresh()
})
},
onReachBottom() {
getVisitorList()
},
})
</script>
<style lang="scss" scoped>
.page-container {
min-height: 100vh;
background-color: #f5f5f5;
padding-bottom: 120rpx; // 为底部导航留出空间
min-height: 100vh;
background-color: #f5f5f5;
padding-bottom: 120rpx; // 为底部导航留出空间
}
.status-tabs {
display: flex;
background-color: #fff;
padding: 20rpx 30rpx;
margin-bottom: 20rpx;
display: flex;
background-color: #fff;
padding: 20rpx 30rpx;
margin-bottom: 20rpx;
.tab-item {
flex: 1;
text-align: center;
font-size: 28rpx;
color: #666;
position: relative;
padding: 20rpx 0;
.tab-item {
flex: 1;
text-align: center;
font-size: 28rpx;
color: #666;
position: relative;
padding: 20rpx 0;
&.active {
color: #1777FF;
font-weight: 500;
&.active {
color: #1777ff;
font-weight: 500;
&::after {
content: '';
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 40rpx;
height: 4rpx;
background-color: #1777FF;
border-radius: 2rpx;
}
}
}
&::after {
content: "";
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 40rpx;
height: 4rpx;
background-color: #1777ff;
border-radius: 2rpx;
}
}
}
}
.visitor-list {
padding: 0 20rpx;
padding-bottom: 20rpx; // 为底部添加一些间距
padding: 0 20rpx;
padding-bottom: 20rpx; // 为底部添加一些间距
.visitor-item {
background-color: #fff;
border-radius: 12rpx;
margin-bottom: 20rpx;
padding: 24rpx;
.visitor-item {
background-color: #fff;
border-radius: 12rpx;
margin-bottom: 20rpx;
padding: 24rpx;
.visitor-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20rpx;
.visitor-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20rpx;
.visit-time {
font-size: 28rpx;
color: #333;
}
.visit-time {
font-size: 28rpx;
color: #333;
}
.status-tag {
padding: 4rpx 16rpx;
border-radius: 4rpx;
font-size: 24rpx;
.status-tag {
padding: 4rpx 16rpx;
border-radius: 4rpx;
font-size: 24rpx;
&.status-pending {
background-color: #fff7e6;
color: #ffa940;
}
&.status-pending {
background-color: #fff7e6;
color: #ffa940;
}
&.status-waiting {
background-color: #e6f7ff;
color: #1890ff;
}
&.status-waiting {
background-color: #e6f7ff;
color: #1890ff;
}
&.status-visited {
background-color: #f6ffed;
color: #52c41a;
}
&.status-visited {
background-color: #f6ffed;
color: #52c41a;
}
&.status-cancelled {
background-color: #f5f5f5;
color: #999;
}
}
}
&.status-cancelled {
background-color: #f5f5f5;
color: #999;
}
}
}
.visitor-content {
.info-row {
display: flex;
margin-bottom: 16rpx;
.visitor-content {
.info-row {
display: flex;
margin-bottom: 16rpx;
.label {
width: 160rpx;
color: #666;
font-size: 28rpx;
}
.label {
width: 160rpx;
color: #666;
font-size: 28rpx;
}
.value {
flex: 1;
color: #333;
font-size: 28rpx;
}
}
}
.value {
flex: 1;
color: #333;
font-size: 28rpx;
}
}
}
.visitor-footer {
margin-top: 20rpx;
display: flex;
justify-content: flex-end;
gap: 20rpx;
.visitor-footer {
margin-top: 20rpx;
display: flex;
justify-content: flex-end;
gap: 20rpx;
.action-btn {
padding: 12rpx 24rpx;
border: 1rpx solid #ddd;
border-radius: 4rpx;
font-size: 26rpx;
.action-btn {
padding: 12rpx 24rpx;
border: 1rpx solid #ddd;
border-radius: 4rpx;
font-size: 26rpx;
&:active {
opacity: 0.8;
}
}
&:active {
opacity: 0.8;
}
}
.cancel-btn {
color: #666;
}
.cancel-btn {
color: #666;
}
.reject-btn {
color: #ff4d4f;
border-color: #ff4d4f;
}
.reject-btn {
color: #ff4d4f;
border-color: #ff4d4f;
}
.accept-btn {
color: #fff;
background-color: #1777FF;
border-color: #1777FF;
}
}
}
.accept-btn {
color: #fff;
background-color: #1777ff;
border-color: #1777ff;
}
}
}
}
.loading-more, .no-more, .empty {
text-align: center;
padding: 30rpx;
color: #999;
font-size: 26rpx;
}
.bottom-nav {
position: fixed;
bottom: 0;
left: 0;
right: 0;
height: 100rpx;
background-color: #fff;
display: flex;
justify-content: space-around;
align-items: center;
border-top: 1rpx solid #eee;
padding-bottom: env(safe-area-inset-bottom); // 适配iPhone底部安全区
.nav-item {
display: flex;
flex-direction: column;
align-items: center;
padding: 10rpx 0;
.nav-icon {
width: 44rpx;
height: 44rpx;
margin-bottom: 4rpx;
}
.nav-text {
font-size: 20rpx;
color: #999;
&.active {
color: #1777FF;
}
}
}
.loading-more,
.no-more,
.empty {
text-align: center;
padding: 30rpx;
color: #999;
font-size: 26rpx;
}
</style>
+4 -5
View File
@@ -1,7 +1,6 @@
<template>
<crud-layout bg="/static/page/blue-bg.png">
<view class="h-20rpx" />
<view class="flex-row items-center">
<view class="bg-#F8F8F8 w-full h-screen overflow-hidden">
<view class="card p-30rpx mx-4 flex items-center">
<image v-if="userInfo?.avatar" class="w-100rpx h-100rpx rounded-full" :src="userInfo.avatarUrl" mode="aspectFill" />
<image v-else class="w-100rpx h-100rpx rounded-full" src="/static/icon/default-avatar.png" mode="aspectFill" />
<view class="m-l-30rpx" v-if="token">
@@ -15,13 +14,13 @@
</view>
</view>
<view class="mt-4 rounded-20rpx shadow-main p-30rpx bg-white!">
<view class="card p-30rpx m-4 active:bg-#F8F8F8!">
<view class="flex items-center justify-between" @tap="onTabMenu">
<text class="sub-text-36 m-l-20rpx">核销</text>
<view class="iconfont icon-youjiantou" />
</view>
</view>
</crud-layout>
</view>
</template>
<script setup lang="ts">