feat:迁移
This commit is contained in:
1 parent
838f05b3c0
commit
36a8665bae
44 files changed
+8043
-29
No files matched your search
@@ -1,21 +0,0 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
.DS_Store
|
||||
dist
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.idea
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
+114
@@ -9,6 +9,13 @@
|
||||
}
|
||||
},
|
||||
"pages": [
|
||||
{
|
||||
"path": "pages/root/dashboard",
|
||||
"style": {
|
||||
"navigationBarTitleText": "浦江县机关事务智慧后勤一体化平台",
|
||||
"disableScroll": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/tabbar/canteen",
|
||||
"style": {
|
||||
@@ -230,6 +237,113 @@
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"root": "pages/integrated",
|
||||
"pages": [
|
||||
{
|
||||
"path": "lostAndFound/index",
|
||||
"style": {
|
||||
"navigationBarTitleText": "失物招领"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "lostAndFound/detail",
|
||||
"style": {
|
||||
"navigationBarTitleText": "失物详情"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "meeting/index",
|
||||
"style": {
|
||||
"navigationBarTitleText": "会议预定"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "feedback/index",
|
||||
"style": {
|
||||
"navigationBarTitleText": "意见反馈"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "feedback/detail",
|
||||
"style": {
|
||||
"navigationBarTitleText": "反馈详情"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "repair/index",
|
||||
"style": {
|
||||
"navigationBarTitleText": "维修上报"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "repair/detail",
|
||||
"style": {
|
||||
"navigationBarTitleText": "维修详情"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "notice/index",
|
||||
"style": {
|
||||
"navigationBarTitleText": "通知公告"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "notice/detail",
|
||||
"style": {
|
||||
"navigationBarTitleText": "公告详情"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "sysbanner/detail",
|
||||
"style": {
|
||||
"navigationBarTitleText": "轮播公告详情"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "visitor/invitation",
|
||||
"style": {
|
||||
"navigationBarTitleText": "访客邀约"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "visitor/invitation-record",
|
||||
"style": {
|
||||
"navigationBarTitleText": "邀约记录"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "visitor/detail",
|
||||
"style": {
|
||||
"navigationBarTitleText": "邀约详情"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "meeting/detail",
|
||||
"style": {
|
||||
"navigationBarTitleText": "预约会议"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "meeting/my-reservations",
|
||||
"style": {
|
||||
"navigationBarTitleText": "我的预约"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "meeting/booking-detail",
|
||||
"style": {
|
||||
"navigationBarTitleText": "预约详情"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "todo/index",
|
||||
"style": {
|
||||
"navigationBarTitleText": "工作待办"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"globalStyle": {
|
||||
|
||||
@@ -0,0 +1,304 @@
|
||||
<template>
|
||||
<view class="feedback-detail">
|
||||
<view v-if="!detail.title" class="loading-tip">加载中...</view>
|
||||
<view v-else>
|
||||
<view class="detail-header">
|
||||
<view class="detail-title">{{ detail.title || '无标题' }}</view>
|
||||
<view v-if="detail.type" class="detail-type">
|
||||
反馈类型:{{ typeOptionsLoaded ? getTypeLabel(detail.type) : '加载中...' }}
|
||||
</view>
|
||||
</view>
|
||||
<view class="detail-content">
|
||||
<view class="content-text">{{ detail.description || '无描述' }}</view>
|
||||
<view v-if="imageList.length" class="image-list">
|
||||
<image
|
||||
v-for="(img, idx) in imageList"
|
||||
:key="idx"
|
||||
:src="img.startsWith('http') || img.startsWith('https') ? img : filePath(img)"
|
||||
class="feedback-img"
|
||||
mode="aspectFill"
|
||||
@error="handleImageError(img)"
|
||||
@load="handleImageLoad(img)"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
<view class="detail-footer">
|
||||
<text class="create-time">提交时间:{{ formatTime(detail.createTime) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
import feedbackApi from '@/sheep/api/feedback'
|
||||
import { filePath } from '@/sheep/utils/file'
|
||||
|
||||
const detail = ref<any>({})
|
||||
const typeOptions = ref<{ value: string; label: string }[]>([])
|
||||
const typeOptionsLoaded = ref(false)
|
||||
|
||||
async function loadTypeOptions() {
|
||||
try {
|
||||
const res: any = await feedbackApi.getFeedbackType()
|
||||
if (res.code === 0 && res.data && Array.isArray(res.data)) {
|
||||
typeOptions.value = res.data.map(item => ({
|
||||
value: item.id,
|
||||
label: item.label
|
||||
}))
|
||||
typeOptionsLoaded.value = true
|
||||
} else {
|
||||
typeOptionsLoaded.value = true
|
||||
}
|
||||
} catch (e) {
|
||||
typeOptionsLoaded.value = true
|
||||
}
|
||||
}
|
||||
|
||||
function getTypeLabel(type) {
|
||||
if (!typeOptions.value.length || !type) {
|
||||
return type || ''
|
||||
}
|
||||
const option = typeOptions.value.find(item => String(item.value) === String(type))
|
||||
return option ? option.label : type
|
||||
}
|
||||
|
||||
function getTypeClass(type: string) {
|
||||
const classMap: Record<string, string> = {
|
||||
'投诉': 'type-complain',
|
||||
'建议': 'type-suggest',
|
||||
'bug报告': 'type-bug',
|
||||
'功能需求': 'type-feature',
|
||||
'其他': 'type-other'
|
||||
}
|
||||
return classMap[type] || 'type-other'
|
||||
}
|
||||
|
||||
function getStatusText(status: string) {
|
||||
const statusMap: Record<string, string> = {
|
||||
'1': '待处理',
|
||||
'2': '处理中',
|
||||
'3': '已处理',
|
||||
'4': '已关闭'
|
||||
}
|
||||
return statusMap[status] || '未知'
|
||||
}
|
||||
|
||||
function getStatusClass(status: string) {
|
||||
const classMap: Record<string, string> = {
|
||||
'1': 'status-pending',
|
||||
'2': 'status-processing',
|
||||
'3': 'status-completed',
|
||||
'4': 'status-closed'
|
||||
}
|
||||
return classMap[status] || 'status-pending'
|
||||
}
|
||||
|
||||
function formatTime(time: string) {
|
||||
if (!time) return ''
|
||||
return new Date(time).toLocaleString()
|
||||
}
|
||||
|
||||
function handleImageError(img: string) {
|
||||
// 图片加载失败处理
|
||||
}
|
||||
|
||||
function handleImageLoad(img: string) {
|
||||
// 图片加载成功处理
|
||||
}
|
||||
|
||||
// 计算图片数组
|
||||
const imageList = computed(() => {
|
||||
if (!detail.value.picUrl) return []
|
||||
return detail.value.picUrl.split(',').filter(Boolean)
|
||||
})
|
||||
|
||||
onLoad(async (options: any) => {
|
||||
const id = options.id
|
||||
if (!id) {
|
||||
uni.showToast({ title: '缺少反馈ID', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
uni.showLoading({ title: '加载中...' })
|
||||
|
||||
// 先获取反馈详情
|
||||
const response: any = await feedbackApi.getFeedbackDetail(id)
|
||||
|
||||
if (response.code === 0) {
|
||||
// 处理不同的数据结构
|
||||
if (Array.isArray(response.data) && response.data.length > 0) {
|
||||
detail.value = response.data[0]
|
||||
} else if (response.data && typeof response.data === 'object') {
|
||||
detail.value = response.data
|
||||
} else {
|
||||
uni.showToast({ title: '数据格式错误', icon: 'none' })
|
||||
return
|
||||
}
|
||||
} else {
|
||||
uni.showToast({ title: response.msg || '获取详情失败', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
// 再获取类型选项
|
||||
await loadTypeOptions()
|
||||
|
||||
uni.hideLoading()
|
||||
} catch (error) {
|
||||
uni.hideLoading()
|
||||
uni.showToast({ title: '获取详情失败', icon: 'none' })
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.feedback-detail {
|
||||
padding: 32rpx;
|
||||
background: #f5f5f5;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.loading-tip {
|
||||
text-align: center;
|
||||
padding: 60rpx 0;
|
||||
color: #999;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.detail-header {
|
||||
background: #fff;
|
||||
border-radius: 16rpx;
|
||||
padding: 32rpx;
|
||||
margin-bottom: 24rpx;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
.type-tag {
|
||||
padding: 12rpx 24rpx;
|
||||
border-radius: 24rpx;
|
||||
font-size: 26rpx;
|
||||
|
||||
&.type-suggestion {
|
||||
background: #e3f2fd;
|
||||
color: #1976d2;
|
||||
}
|
||||
|
||||
&.type-problem {
|
||||
background: #ffebee;
|
||||
color: #d32f2f;
|
||||
}
|
||||
|
||||
&.type-ui {
|
||||
background: #f3e5f5;
|
||||
color: #7b1fa2;
|
||||
}
|
||||
|
||||
&.type-other {
|
||||
background: #f5f5f5;
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
|
||||
.status-tag {
|
||||
padding: 12rpx 24rpx;
|
||||
border-radius: 24rpx;
|
||||
font-size: 26rpx;
|
||||
|
||||
&.status-pending {
|
||||
background: #fff3e0;
|
||||
color: #f57c00;
|
||||
}
|
||||
|
||||
&.status-processing {
|
||||
background: #e8f5e8;
|
||||
color: #388e3c;
|
||||
}
|
||||
|
||||
&.status-completed {
|
||||
background: #e3f2fd;
|
||||
color: #1976d2;
|
||||
}
|
||||
|
||||
&.status-closed {
|
||||
background: #f5f5f5;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.detail-content {
|
||||
background: #fff;
|
||||
border-radius: 16rpx;
|
||||
padding: 32rpx;
|
||||
margin-bottom: 24rpx;
|
||||
|
||||
.content-text {
|
||||
font-size: 30rpx;
|
||||
color: #333;
|
||||
line-height: 1.6;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.contact-info {
|
||||
font-size: 26rpx;
|
||||
color: #666;
|
||||
padding-top: 24rpx;
|
||||
border-top: 1rpx solid #f0f0f0;
|
||||
}
|
||||
}
|
||||
|
||||
.reply-section {
|
||||
background: #fff;
|
||||
border-radius: 16rpx;
|
||||
padding: 32rpx;
|
||||
margin-bottom: 24rpx;
|
||||
|
||||
.reply-title {
|
||||
font-size: 30rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.reply-content {
|
||||
font-size: 28rpx;
|
||||
color: #666;
|
||||
line-height: 1.6;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.reply-time {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
|
||||
.detail-footer {
|
||||
background: #fff;
|
||||
border-radius: 16rpx;
|
||||
padding: 32rpx;
|
||||
|
||||
.create-time {
|
||||
font-size: 26rpx;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
.image-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 16rpx;
|
||||
margin-top: 16rpx;
|
||||
}
|
||||
.feedback-img {
|
||||
width: 180rpx;
|
||||
height: 180rpx;
|
||||
border-radius: 8rpx;
|
||||
background: #f7f8fa;
|
||||
object-fit: cover;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,588 @@
|
||||
<template>
|
||||
<view class="feedback-container">
|
||||
<view class="feedback-tabs">
|
||||
<view :class="['tab', { active: activeTab === 0 }]" @click="activeTab = 0">提交反馈</view>
|
||||
<view :class="['tab', { active: activeTab === 1 }]" @click="activeTab = 1">我的反馈</view>
|
||||
</view>
|
||||
<view v-if="activeTab === 0">
|
||||
<!-- 反馈表单 -->
|
||||
<view class="feedback-form">
|
||||
<view class="form-title">提交反馈</view>
|
||||
<view class="form-item">
|
||||
<view class="label">标题</view>
|
||||
<input v-model="title" placeholder="请输入反馈标题" class="title-input" />
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<view class="label">反馈类型</view>
|
||||
<picker @change="onTypeChange" :value="typeIndex" :range="typeOptions" range-key="label">
|
||||
<view class="picker">
|
||||
{{ typeOptionsLoaded ? (typeOptions[typeIndex]?.label || '请选择类型') : '加载中...' }}
|
||||
</view>
|
||||
</picker>
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<view class="label">反馈描述</view>
|
||||
<textarea v-model="description" placeholder="请详细描述您的问题或建议..." class="content-textarea"
|
||||
maxlength="500" />
|
||||
<view class="char-count">{{ description.length }}/500</view>
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<view class="label">联系方式</view>
|
||||
<input v-model="contact" placeholder="手机号或邮箱(选填)" class="contact-input" />
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<view class="label">图片上传</view>
|
||||
<tm-upload-photo v-model="uploadList" :maxCount="6" url="/api/admin/sys-file/upload"
|
||||
:header="uploadHeader" :show-upload-list="true" :width="180" :height="180" imgKey="url" />
|
||||
</view>
|
||||
<button @click="submitFeedback" class="submit-btn" :disabled="!canSubmit">
|
||||
提交反馈
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
<view v-else>
|
||||
<!-- 我的反馈列表 -->
|
||||
<view class="feedback-list">
|
||||
<view class="list-title">我的反馈</view>
|
||||
<view v-if="error" class="error-tip">
|
||||
加载失败,请 <text class="retry-btn" @click="refreshList">重试</text>
|
||||
</view>
|
||||
<view v-else-if="feedbackList.length === 0" class="empty-tip">
|
||||
暂无反馈记录
|
||||
</view>
|
||||
<view v-else>
|
||||
<view v-for="item in feedbackList" :key="item.id" class="feedback-item" @click="viewDetail(item)">
|
||||
<view class="item-header">
|
||||
<view class="type-tag" :class="getTypeClass(item.type)">
|
||||
{{ typeOptionsLoaded ? getTypeLabel(item.type) : '加载中...' }}
|
||||
</view>
|
||||
<view class="status-tag" :class="getStatusClass(item.status)">{{ getStatusText(item.status)
|
||||
}}</view>
|
||||
</view>
|
||||
<view class="item-content">
|
||||
<view class="item-title">{{ item.title }}</view>
|
||||
</view>
|
||||
<view class="item-footer">
|
||||
<text class="time">{{ formatTime(item.createTime) }}</text>
|
||||
<text v-if="item.reply" class="reply-count">已回复</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, computed, watch, nextTick } from 'vue'
|
||||
import feedbackApi from '@/sheep/api/feedback'
|
||||
import { onReachBottom, onPullDownRefresh } from '@dcloudio/uni-app'
|
||||
import { filePath } from '@/sheep/utils/file'
|
||||
import { useDefaultHeader } from '@/sheep/hooks/use-token'
|
||||
|
||||
const typeOptions = ref<{ value: string; label: string }[]>([])
|
||||
const typeOptionsLoaded = ref(false)
|
||||
const typeIndex = ref(0)
|
||||
const title = ref('')
|
||||
const description = ref('')
|
||||
const contact = ref('')
|
||||
const feedbackList = ref<any[]>([])
|
||||
const activeTab = ref(0)
|
||||
const page = ref(1)
|
||||
const pageSize = 5
|
||||
const hasMore = ref(true)
|
||||
const loading = ref(false)
|
||||
const error = ref(false)
|
||||
const uploadList = ref<any[]>([])
|
||||
const uploadHeader = ref({})
|
||||
|
||||
const canSubmit = computed(() => {
|
||||
return title.value.trim() && description.value.trim()
|
||||
})
|
||||
|
||||
async function loadTypeOptions() {
|
||||
try {
|
||||
const res: any = await feedbackApi.getFeedbackType()
|
||||
if (res.code === 0 && res.data && Array.isArray(res.data)) {
|
||||
typeOptions.value = res.data.map(item => ({
|
||||
value: item.id,
|
||||
label: item.label
|
||||
}))
|
||||
typeOptionsLoaded.value = true
|
||||
} else {
|
||||
uni.showToast({ title: res.msg || '获取反馈类型失败', icon: 'none' })
|
||||
typeOptionsLoaded.value = true
|
||||
}
|
||||
} catch (e) {
|
||||
uni.showToast({ title: '获取反馈类型失败', icon: 'none' })
|
||||
typeOptionsLoaded.value = true
|
||||
}
|
||||
}
|
||||
|
||||
function onTypeChange(e: any) {
|
||||
typeIndex.value = e.detail.value
|
||||
}
|
||||
|
||||
function getTypeLabel(type: string) {
|
||||
if (!typeOptions.value.length || !type) {
|
||||
return type || ''
|
||||
}
|
||||
const option = typeOptions.value.find(item => String(item.value) === String(type))
|
||||
return option ? option.label : type
|
||||
}
|
||||
|
||||
function getTypeClass(type: string) {
|
||||
const classMap: Record<string, string> = {
|
||||
'投诉': 'type-complain',
|
||||
'建议': 'type-suggest',
|
||||
'bug报告': 'type-bug',
|
||||
'功能需求': 'type-feature',
|
||||
'其他': 'type-other'
|
||||
}
|
||||
return classMap[type] || 'type-other'
|
||||
}
|
||||
|
||||
function getStatusText(status: string | number) {
|
||||
if (status === 1 || status === '1') return '已处理'
|
||||
// 只要不是1都显示未处理
|
||||
return '未处理'
|
||||
}
|
||||
|
||||
function getStatusClass(status: string | number) {
|
||||
if (status === 1 || status === '1') return 'status-completed'
|
||||
// 只要不是1都用未处理样式
|
||||
return 'status-pending'
|
||||
}
|
||||
|
||||
function formatTime(time: string) {
|
||||
if (!time) return ''
|
||||
return new Date(time).toLocaleDateString()
|
||||
}
|
||||
|
||||
async function submitFeedback() {
|
||||
if (!canSubmit.value) {
|
||||
uni.showToast({ title: '请填写完整信息', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
uni.showLoading({ title: '提交中...' })
|
||||
let picUrlArr: any[] = []
|
||||
uploadList.value.forEach(item => {
|
||||
if (item.response) {
|
||||
try {
|
||||
const res = JSON.parse(item.response)
|
||||
if (res.code === 0 && res.data && res.data.url) {
|
||||
picUrlArr.push(res.data.url)
|
||||
}
|
||||
} catch { }
|
||||
}
|
||||
})
|
||||
const data = {
|
||||
title: title.value.trim(),
|
||||
type: typeOptions.value[typeIndex.value].value,
|
||||
description: description.value.trim(),
|
||||
contact: contact.value.trim(),
|
||||
picUrl: picUrlArr.join(',')
|
||||
}
|
||||
const response: any = await feedbackApi.submitFeedback(data)
|
||||
uni.hideLoading()
|
||||
if (response.code === 0) {
|
||||
uni.showToast({ title: '提交成功', icon: 'success' })
|
||||
// 清空表单
|
||||
title.value = ''
|
||||
description.value = ''
|
||||
contact.value = ''
|
||||
typeIndex.value = 0
|
||||
// 使用 nextTick 确保在下一个 DOM 更新周期清空上传列表
|
||||
await nextTick()
|
||||
uploadList.value = []
|
||||
// 刷新列表
|
||||
loadFeedbackList()
|
||||
} 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/feedback/detail?id=${item.id}`
|
||||
})
|
||||
}
|
||||
|
||||
async function loadFeedbackList(isLoadMore = false, autoFill = false) {
|
||||
if (loading.value || (!hasMore.value && isLoadMore)) return
|
||||
loading.value = true
|
||||
error.value = false
|
||||
try {
|
||||
const response: any = await feedbackApi.getFeedbackList({
|
||||
current: page.value,
|
||||
size: pageSize
|
||||
})
|
||||
if (response.code === 0 && response.data) {
|
||||
const records = response.data.records || []
|
||||
if (isLoadMore) {
|
||||
feedbackList.value = feedbackList.value.concat(records)
|
||||
} else {
|
||||
feedbackList.value = records
|
||||
}
|
||||
hasMore.value = records.length === pageSize
|
||||
if (hasMore.value) page.value += 1
|
||||
} else {
|
||||
error.value = true
|
||||
hasMore.value = false
|
||||
uni.showToast({ title: response.msg || '加载失败', icon: 'none' })
|
||||
}
|
||||
} catch (e) {
|
||||
error.value = true
|
||||
hasMore.value = false
|
||||
uni.showToast({ title: '加载失败', icon: 'none' })
|
||||
} finally {
|
||||
loading.value = false
|
||||
// H5端自动补齐:只有 autoFill=true 时才自动补齐
|
||||
if (autoFill) {
|
||||
await nextTick()
|
||||
if (hasMore.value) {
|
||||
const listDom = document.querySelector('.feedback-list')
|
||||
if (listDom && listDom.scrollHeight <= window.innerHeight) {
|
||||
await loadFeedbackList(true, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function refreshList() {
|
||||
page.value = 1
|
||||
hasMore.value = true
|
||||
loadFeedbackList(false, true)
|
||||
}
|
||||
|
||||
// 监听页面触底事件,下拉加载更多
|
||||
onReachBottom(() => {
|
||||
if (activeTab.value === 1) {
|
||||
// 确保类型选项已加载完成后再加载更多
|
||||
if (typeOptionsLoaded.value) {
|
||||
loadFeedbackList(true, false)
|
||||
} else {
|
||||
loadTypeOptions().then(() => {
|
||||
loadFeedbackList(true, false)
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 切换tab时重置分页并刷新
|
||||
watch(activeTab, (val) => {
|
||||
if (val === 1) {
|
||||
// 确保类型选项已加载完成后再刷新列表
|
||||
if (typeOptionsLoaded.value) {
|
||||
refreshList()
|
||||
} else {
|
||||
loadTypeOptions().then(() => {
|
||||
refreshList()
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
onPullDownRefresh(() => {
|
||||
if (activeTab.value === 1) {
|
||||
page.value = 1
|
||||
hasMore.value = true
|
||||
// 确保类型选项已加载完成后再刷新列表
|
||||
if (typeOptionsLoaded.value) {
|
||||
loadFeedbackList(false, true).then(() => {
|
||||
uni.stopPullDownRefresh()
|
||||
})
|
||||
} else {
|
||||
loadTypeOptions().then(() => {
|
||||
loadFeedbackList(false, true).then(() => {
|
||||
uni.stopPullDownRefresh()
|
||||
})
|
||||
})
|
||||
}
|
||||
} else {
|
||||
uni.stopPullDownRefresh()
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
uploadHeader.value = useDefaultHeader()
|
||||
loadTypeOptions().then(() => {
|
||||
if (activeTab.value === 1) {
|
||||
refreshList()
|
||||
}
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.feedback-container {
|
||||
padding: 24rpx;
|
||||
background: #f5f5f5;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.feedback-tabs {
|
||||
display: flex;
|
||||
background: #fff;
|
||||
border-radius: 16rpx 16rpx 0 0;
|
||||
overflow: hidden;
|
||||
margin-bottom: 24rpx;
|
||||
|
||||
.tab {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
padding: 28rpx 0;
|
||||
font-size: 30rpx;
|
||||
color: #888;
|
||||
background: #f7f8fa;
|
||||
|
||||
&.active {
|
||||
color: #1777FF;
|
||||
background: #fff;
|
||||
font-weight: bold;
|
||||
border-bottom: 4rpx solid #1777FF;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.feedback-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;
|
||||
}
|
||||
|
||||
.title-input {
|
||||
height: 80rpx;
|
||||
padding: 0 24rpx;
|
||||
background: #f7f8fa;
|
||||
border-radius: 8rpx;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.picker {
|
||||
height: 80rpx;
|
||||
line-height: 80rpx;
|
||||
padding: 0 24rpx;
|
||||
background: #f7f8fa;
|
||||
border-radius: 8rpx;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.content-textarea {
|
||||
width: 100%;
|
||||
height: 200rpx;
|
||||
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;
|
||||
}
|
||||
|
||||
.contact-input {
|
||||
height: 80rpx;
|
||||
padding: 0 24rpx;
|
||||
background: #f7f8fa;
|
||||
border-radius: 8rpx;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.submit-btn {
|
||||
width: 100%;
|
||||
height: 88rpx;
|
||||
background: #007aff;
|
||||
color: #fff;
|
||||
border-radius: 8rpx;
|
||||
font-size: 32rpx;
|
||||
|
||||
&:disabled {
|
||||
background: #ccc;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.feedback-list {
|
||||
background: #fff;
|
||||
border-radius: 16rpx;
|
||||
padding: 32rpx;
|
||||
|
||||
.list-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.error-tip {
|
||||
text-align: center;
|
||||
color: #f56c6c;
|
||||
font-size: 28rpx;
|
||||
padding: 60rpx 0;
|
||||
}
|
||||
|
||||
.retry-btn {
|
||||
color: #007aff;
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.empty-tip {
|
||||
text-align: center;
|
||||
color: #999;
|
||||
font-size: 28rpx;
|
||||
padding: 60rpx 0;
|
||||
}
|
||||
|
||||
.feedback-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;
|
||||
|
||||
.type-tag {
|
||||
padding: 8rpx 16rpx;
|
||||
border-radius: 20rpx;
|
||||
font-size: 24rpx;
|
||||
|
||||
&.type-complain {
|
||||
background: #ffebee;
|
||||
color: #d32f2f;
|
||||
}
|
||||
|
||||
&.type-suggest {
|
||||
background: #e3f2fd;
|
||||
color: #1976d2;
|
||||
}
|
||||
|
||||
&.type-bug {
|
||||
background: #f3e5f5;
|
||||
color: #7b1fa2;
|
||||
}
|
||||
|
||||
&.type-feature {
|
||||
background: #e8f5e8;
|
||||
color: #388e3c;
|
||||
}
|
||||
|
||||
&.type-other {
|
||||
background: #f5f5f5;
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
|
||||
.status-tag {
|
||||
padding: 8rpx 16rpx;
|
||||
border-radius: 20rpx;
|
||||
font-size: 24rpx;
|
||||
|
||||
&.status-pending {
|
||||
background: #fff3e0;
|
||||
color: #f57c00;
|
||||
}
|
||||
|
||||
&.status-processing {
|
||||
background: #e8f5e8;
|
||||
color: #388e3c;
|
||||
}
|
||||
|
||||
&.status-completed {
|
||||
background: #e3f2fd;
|
||||
color: #1976d2;
|
||||
}
|
||||
|
||||
&.status-closed {
|
||||
background: #f5f5f5;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.item-content {
|
||||
.item-title {
|
||||
font-size: 28rpx;
|
||||
color: #222;
|
||||
font-weight: bold;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.item-desc {
|
||||
font-size: 26rpx;
|
||||
color: #333;
|
||||
margin-bottom: 8rpx;
|
||||
line-height: 1.5;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.image-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 16rpx;
|
||||
margin-top: 8rpx;
|
||||
}
|
||||
|
||||
.item-img {
|
||||
width: 120rpx;
|
||||
height: 120rpx;
|
||||
border-radius: 8rpx;
|
||||
background: #f7f8fa;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
|
||||
.item-footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
.time {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.reply-count {
|
||||
font-size: 24rpx;
|
||||
color: #007aff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,270 @@
|
||||
<template>
|
||||
<view class="detail-wrapper">
|
||||
<view v-if="loading" class="loading">加载中...</view>
|
||||
|
||||
<view v-else-if="detail">
|
||||
<view class="detail-card">
|
||||
<view class="detail-title">{{ detail.title || '失物招领详情' }}</view>
|
||||
<view class="detail-meta">
|
||||
<text class="meta-item">时间:{{ formatDate(detail.dateFound) }}</text>
|
||||
<text class="meta-item">地点:{{ detail.location || '暂无' }}</text>
|
||||
</view>
|
||||
<view class="detail-status" :class="statusClass">{{ statusText }}</view>
|
||||
</view>
|
||||
|
||||
<view v-if="imageList.length" class="image-list">
|
||||
<image v-for="(src, index) in imageList" :key="index" class="detail-image" :src="src" mode="aspectFill"
|
||||
@tap="previewImage(index)" />
|
||||
</view>
|
||||
|
||||
<view class="detail-section">
|
||||
<view class="section-title">物品描述</view>
|
||||
<rich-text v-if="descriptionNodes" :nodes="descriptionNodes"></rich-text>
|
||||
<view v-else class="section-content">暂无描述信息</view>
|
||||
</view>
|
||||
|
||||
<view v-if="contactInfo" class="detail-section">
|
||||
<view class="section-title">联系方式</view>
|
||||
<view class="section-content">{{ contactInfo }}</view>
|
||||
</view>
|
||||
|
||||
<view v-if="detail.remark" class="detail-section">
|
||||
<view class="section-title">备注</view>
|
||||
<view class="section-content">{{ detail.remark }}</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-else class="empty-state">暂无详情数据</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref } from 'vue'
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
import dayjs from 'dayjs'
|
||||
import lostAndFoundApi from '@/sheep/api/lost-and-found'
|
||||
import { filePath } from '@/sheep/utils/file'
|
||||
|
||||
interface LostAndFoundDetail {
|
||||
id: string
|
||||
title?: string
|
||||
picUrl?: string | string[] | null
|
||||
description?: string
|
||||
dateFound?: string
|
||||
location?: string
|
||||
status?: string | number
|
||||
contactPerson?: string
|
||||
contactPhone?: string
|
||||
phone?: string
|
||||
userName?: string
|
||||
remark?: string
|
||||
}
|
||||
|
||||
const detail = ref<LostAndFoundDetail | null>(null)
|
||||
const loading = ref(false)
|
||||
|
||||
const toAbsoluteUrl = (url?: string | null) => {
|
||||
if (!url) return ''
|
||||
if (/^https?:\/\//i.test(url)) return url
|
||||
return filePath(url)
|
||||
}
|
||||
|
||||
const statusText = computed(() => {
|
||||
const status = detail.value?.status?.toString()
|
||||
if (status === '1') return '已认领'
|
||||
if (status === '2') return '已撤销'
|
||||
return '待认领'
|
||||
})
|
||||
|
||||
const statusClass = computed(() => {
|
||||
const status = detail.value?.status?.toString()
|
||||
if (status === '1') return 'status-claimed'
|
||||
if (status === '2') return 'status-cancelled'
|
||||
return 'status-pending'
|
||||
})
|
||||
|
||||
const imageList = computed(() => {
|
||||
const imgs = detail.value?.picUrl
|
||||
if (!imgs) return [] as string[]
|
||||
const rawList = Array.isArray(imgs)
|
||||
? imgs
|
||||
: imgs
|
||||
.split(',')
|
||||
.map((item) => item.trim())
|
||||
|
||||
return rawList
|
||||
.map((item) => toAbsoluteUrl(item))
|
||||
.filter((item): item is string => Boolean(item))
|
||||
})
|
||||
|
||||
const descriptionNodes = computed(() => {
|
||||
const desc = detail.value?.description
|
||||
if (!desc) return ''
|
||||
return desc.replace(/<img\s+[^>]*src=["']([^"']+)["']/gi, (match, src) => {
|
||||
const fullSrc = toAbsoluteUrl(src)
|
||||
if (!fullSrc) return match
|
||||
return match.replace(src, fullSrc)
|
||||
})
|
||||
})
|
||||
|
||||
const contactInfo = computed(() => {
|
||||
const person = detail.value?.contactPerson || detail.value?.userName
|
||||
const phone = detail.value?.contactPhone || detail.value?.phone
|
||||
if (person && phone) return `${person} ${phone}`
|
||||
return person || phone || ''
|
||||
})
|
||||
|
||||
const formatDate = (date?: string) => {
|
||||
if (!date) return '未知'
|
||||
return dayjs(date).format('YYYY-MM-DD HH:mm')
|
||||
}
|
||||
|
||||
const previewImage = (index: number) => {
|
||||
if (!imageList.value.length) return
|
||||
uni.previewImage({
|
||||
current: index,
|
||||
urls: imageList.value,
|
||||
})
|
||||
}
|
||||
|
||||
const fetchDetail = async (id: string) => {
|
||||
try {
|
||||
loading.value = true
|
||||
const response: any = await lostAndFoundApi.getDetail(id)
|
||||
if (response?.code === 0 && response.data) {
|
||||
detail.value = {
|
||||
...detail.value,
|
||||
...response.data,
|
||||
status: response.data.status ?? detail.value?.status,
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch lost-and-found detail:', error)
|
||||
if (!detail.value) {
|
||||
uni.showToast({ title: '获取详情失败', icon: 'none' })
|
||||
}
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onLoad((options: Record<string, any>) => {
|
||||
try {
|
||||
const eventChannel = getOpenerEventChannel?.()
|
||||
eventChannel?.on('initDetail', (data: LostAndFoundDetail) => {
|
||||
detail.value = data
|
||||
})
|
||||
} catch (error) {
|
||||
// H5 环境可能不存在 getOpenerEventChannel
|
||||
console.warn('initDetail event registration failed:', error)
|
||||
}
|
||||
|
||||
const id = options?.id
|
||||
if (id) {
|
||||
fetchDetail(id)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.detail-wrapper {
|
||||
min-height: 100vh;
|
||||
background-color: #f5f5f5;
|
||||
padding: 24rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.loading,
|
||||
.empty-state {
|
||||
padding: 120rpx 0;
|
||||
text-align: center;
|
||||
color: #999;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.detail-card {
|
||||
background-color: #fff;
|
||||
border-radius: 16rpx;
|
||||
padding: 32rpx;
|
||||
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.04);
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.detail-title {
|
||||
font-size: 36rpx;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.detail-meta {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8rpx;
|
||||
font-size: 26rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.meta-item {
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.detail-status {
|
||||
margin-top: 16rpx;
|
||||
display: inline-block;
|
||||
padding: 8rpx 16rpx;
|
||||
border-radius: 999rpx;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.status-pending {
|
||||
background-color: #e6f7ff;
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
.status-claimed {
|
||||
background-color: #f6ffed;
|
||||
color: #52c41a;
|
||||
}
|
||||
|
||||
.status-cancelled {
|
||||
background-color: #fff1f0;
|
||||
color: #ff4d4f;
|
||||
}
|
||||
|
||||
.image-list {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 16rpx;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.detail-image {
|
||||
width: 100%;
|
||||
height: 200rpx;
|
||||
border-radius: 12rpx;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.detail-section {
|
||||
background-color: #fff;
|
||||
border-radius: 16rpx;
|
||||
padding: 32rpx;
|
||||
margin-bottom: 24rpx;
|
||||
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.section-content {
|
||||
font-size: 28rpx;
|
||||
color: #555;
|
||||
line-height: 1.6;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,234 @@
|
||||
<template>
|
||||
<view class="container">
|
||||
<view class="custom-nav">
|
||||
<view class="nav-back" @tap="goBack">
|
||||
<text class="back-arrow">←</text>
|
||||
</view>
|
||||
<view class="nav-title">失物招领</view>
|
||||
</view>
|
||||
<scroll-view scroll-y class="scroll-view" @scrolltolower="onLoadMore" refresher-enabled
|
||||
:refresher-triggered="refreshing" @refresherrefresh="onRefresh">
|
||||
<view v-if="list.length > 0">
|
||||
<view v-for="item in list" :key="item.id" class="item-card" @tap="viewDetail(item)">
|
||||
<view class="item-title">{{ item.title }}</view>
|
||||
<view class="item-description" v-html="item.description"></view>
|
||||
<view class="item-info">
|
||||
<text>地点:{{ item.location }}</text>
|
||||
<text>时间:{{ formatDate(item.dateFound) }}</text>
|
||||
</view>
|
||||
<view :class="['status-tag', item.status === '0' ? 'status-pending' : 'status-claimed']">
|
||||
{{ item.status === '0' ? '待认领' : '已认领' }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view v-else class="empty-box">
|
||||
<text class="empty-text">暂无失物招领信息</text>
|
||||
</view>
|
||||
<view v-if="loading" class="loading">加载中...</view>
|
||||
<view v-if="!hasMore && list.length > 0" class="no-more">没有更多了</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import dayjs from 'dayjs'
|
||||
import lostAndFoundApi from '@/sheep/api/lost-and-found'
|
||||
|
||||
interface LostAndFoundItem {
|
||||
id: string
|
||||
title: string
|
||||
picUrl: string | null
|
||||
description: string
|
||||
dateFound: string
|
||||
location: string
|
||||
status: string
|
||||
}
|
||||
|
||||
interface ApiResponse {
|
||||
code: number
|
||||
data: {
|
||||
records: LostAndFoundItem[]
|
||||
pages: number
|
||||
}
|
||||
}
|
||||
|
||||
const list = ref<LostAndFoundItem[]>([])
|
||||
const current = ref(1)
|
||||
const hasMore = ref(true)
|
||||
const loading = ref(false)
|
||||
const refreshing = ref(false)
|
||||
|
||||
const fetchData = async (page: number = 1) => {
|
||||
try {
|
||||
loading.value = true
|
||||
const response = await lostAndFoundApi.getList({
|
||||
current: page,
|
||||
size: 10,
|
||||
}) as unknown as ApiResponse
|
||||
|
||||
if (response.code === 0 && response.data) {
|
||||
if (page === 1) {
|
||||
list.value = response.data.records
|
||||
} else {
|
||||
list.value = [...list.value, ...response.data.records]
|
||||
}
|
||||
hasMore.value = page < response.data.pages
|
||||
current.value = page
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch lost and found items:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
refreshing.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const onLoadMore = () => {
|
||||
if (!loading.value && hasMore.value) {
|
||||
fetchData(current.value + 1)
|
||||
}
|
||||
}
|
||||
|
||||
const onRefresh = async () => {
|
||||
refreshing.value = true
|
||||
await fetchData(1)
|
||||
}
|
||||
|
||||
const viewDetail = (item: LostAndFoundItem) => {
|
||||
if (!item?.id) return
|
||||
uni.navigateTo({
|
||||
url: `/pages/integrated/lostAndFound/detail?id=${item.id}`,
|
||||
success: (res) => {
|
||||
res.eventChannel.emit('initDetail', item)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const formatDate = (date: string) => {
|
||||
return dayjs(date).format('YYYY-MM-DD')
|
||||
}
|
||||
|
||||
const goBack = () => {
|
||||
uni.navigateBack()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.container {
|
||||
min-height: 100vh;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.custom-nav {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 88rpx;
|
||||
background-color: #1777FF;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 30rpx;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.nav-back {
|
||||
width: 60rpx;
|
||||
height: 60rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.back-arrow {
|
||||
color: #fff;
|
||||
font-size: 40rpx;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.nav-title {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
color: #fff;
|
||||
font-size: 32rpx;
|
||||
font-weight: 500;
|
||||
margin-right: 60rpx; // 为了保持标题居中,抵消返回按钮的宽度
|
||||
}
|
||||
|
||||
.scroll-view {
|
||||
height: calc(100vh - 88rpx);
|
||||
margin-top: 88rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.item-card {
|
||||
margin: 12px;
|
||||
padding: 16px;
|
||||
background-color: #fff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.item-title {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.item-description {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.item-info {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
color: #999;
|
||||
font-size: 12px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.status-tag {
|
||||
display: inline-block;
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.status-pending {
|
||||
background-color: #e6f7ff;
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
.status-claimed {
|
||||
background-color: #f6ffed;
|
||||
color: #52c41a;
|
||||
}
|
||||
|
||||
.empty-box {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 200px;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
color: #999;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.loading,
|
||||
.no-more {
|
||||
text-align: center;
|
||||
padding: 12px;
|
||||
color: #999;
|
||||
font-size: 12px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,260 @@
|
||||
<template>
|
||||
<view class="booking-detail">
|
||||
<!-- 会议室信息卡片 -->
|
||||
<view class="room-card">
|
||||
<view class="room-header">
|
||||
<view class="room-name">{{ detail.roomName }}</view>
|
||||
<view class="status-badge" :class="getStatusClass(detail.checkStatus)">
|
||||
{{ detail.checkStatusText }}
|
||||
</view>
|
||||
</view>
|
||||
<view class="room-meta">
|
||||
<view class="meta-item">
|
||||
<text class="icon">👥</text>
|
||||
<text>{{ detail.roomCapacity }}人</text>
|
||||
</view>
|
||||
<view class="meta-item">
|
||||
<text class="icon">📍</text>
|
||||
<text>{{ detail.roomLocation || '未设置' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="detail.roomEquipment" class="room-equip">
|
||||
<text class="icon">🔧</text>
|
||||
<text>{{ detail.roomEquipment }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 预约信息卡片 -->
|
||||
<view class="info-card">
|
||||
<view class="card-title">
|
||||
<text class="title-icon">📅</text>
|
||||
<text>预约信息</text>
|
||||
</view>
|
||||
<view class="info-item">
|
||||
<text class="label">预约人</text>
|
||||
<text class="value">{{ detail.applicantName }}</text>
|
||||
</view>
|
||||
<view class="info-item">
|
||||
<text class="label">预约日期</text>
|
||||
<text class="value">{{ detail.bookingDate }}</text>
|
||||
</view>
|
||||
<view class="info-item">
|
||||
<text class="label">会议时间</text>
|
||||
<text class="value primary">{{ detail.startTime }} ~ {{ detail.endTime }}</text>
|
||||
</view>
|
||||
<view class="info-item">
|
||||
<text class="label">会议时长</text>
|
||||
<text class="value">{{ detail.duration }}分钟</text>
|
||||
</view>
|
||||
<view class="info-item" v-if="detail.checkReason">
|
||||
<text class="label">审核意见</text>
|
||||
<text class="value">{{ detail.checkReason }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 会议配置卡片 -->
|
||||
<view class="info-card">
|
||||
<view class="card-title">
|
||||
<text class="title-icon">⚙️</text>
|
||||
<text>会议配置</text>
|
||||
</view>
|
||||
<view class="info-item">
|
||||
<text class="label">会务服务</text>
|
||||
<text class="value">{{ detail.needServiceText }}</text>
|
||||
</view>
|
||||
<view class="info-item" v-if="detail.deskLayoutText">
|
||||
<text class="label">桌子摆放</text>
|
||||
<text class="value">{{ detail.deskLayoutText }}</text>
|
||||
</view>
|
||||
<view class="info-item" v-if="detail.description">
|
||||
<text class="label">备注说明</text>
|
||||
<text class="value remark">{{ detail.description }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
import meetingRoomApi from '@/sheep/api/meeting-room'
|
||||
|
||||
const detail = ref<any>({})
|
||||
|
||||
// 获取状态样式类
|
||||
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'
|
||||
}
|
||||
|
||||
onLoad(async (options: any) => {
|
||||
const id = options.id
|
||||
if (!id) return
|
||||
const { code, data } = await meetingRoomApi.getBookingDetail(id)
|
||||
if (code === 0 && data) {
|
||||
detail.value = data
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.booking-detail {
|
||||
min-height: 100vh;
|
||||
background: #f5f6fa;
|
||||
padding: 24rpx;
|
||||
padding-bottom: 60rpx;
|
||||
}
|
||||
|
||||
// 会议室信息卡片
|
||||
.room-card {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
border-radius: 20rpx;
|
||||
padding: 32rpx;
|
||||
margin-bottom: 24rpx;
|
||||
box-shadow: 0 8rpx 24rpx rgba(102, 126, 234, 0.25);
|
||||
|
||||
.room-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20rpx;
|
||||
|
||||
.room-name {
|
||||
font-size: 40rpx;
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
padding: 8rpx 20rpx;
|
||||
border-radius: 20rpx;
|
||||
font-size: 24rpx;
|
||||
font-weight: 500;
|
||||
|
||||
&.status-pending {
|
||||
background: rgba(255, 193, 7, 0.9);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
&.status-approved {
|
||||
background: rgba(76, 175, 80, 0.9);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
&.status-rejected {
|
||||
background: rgba(244, 67, 54, 0.9);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
&.status-cancelled {
|
||||
background: rgba(158, 158, 158, 0.9);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
&.status-completed {
|
||||
background: rgba(33, 150, 243, 0.9);
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.room-meta {
|
||||
display: flex;
|
||||
gap: 32rpx;
|
||||
margin-bottom: 16rpx;
|
||||
|
||||
.meta-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 26rpx;
|
||||
color: rgba(255, 255, 255, 0.95);
|
||||
|
||||
.icon {
|
||||
margin-right: 8rpx;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.room-equip {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 24rpx;
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
|
||||
.icon {
|
||||
margin-right: 8rpx;
|
||||
font-size: 26rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 信息卡片
|
||||
.info-card {
|
||||
background: #fff;
|
||||
border-radius: 20rpx;
|
||||
padding: 32rpx;
|
||||
margin-bottom: 24rpx;
|
||||
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.06);
|
||||
|
||||
.card-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-bottom: 24rpx;
|
||||
padding-bottom: 16rpx;
|
||||
border-bottom: 2rpx solid #f0f0f0;
|
||||
|
||||
.title-icon {
|
||||
margin-right: 12rpx;
|
||||
font-size: 36rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.info-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
padding: 16rpx 0;
|
||||
border-bottom: 1rpx solid #f5f5f5;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.label {
|
||||
font-size: 28rpx;
|
||||
color: #666;
|
||||
min-width: 160rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.value {
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
text-align: right;
|
||||
flex: 1;
|
||||
|
||||
&.primary {
|
||||
color: #1777FF;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
&.remark {
|
||||
text-align: left;
|
||||
line-height: 1.6;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,848 @@
|
||||
<template>
|
||||
<view class="container">
|
||||
<!-- 顶部自定义导航栏 -->
|
||||
<view class="custom-nav">
|
||||
<view class="nav-back" @tap="goBack">
|
||||
<text class="back-arrow">←</text>
|
||||
</view>
|
||||
<view class="nav-title">预约会议</view>
|
||||
</view>
|
||||
|
||||
<!-- 会议室信息 -->
|
||||
<view class="room-info-box">
|
||||
<image :src="roomImage" class="room-img" mode="aspectFill" />
|
||||
<view class="room-info-main">
|
||||
<view class="room-title">{{ room.name }} <text class="room-capacity">{{ room.capacity }}人</text></view>
|
||||
<view class="room-address"><text>📍</text> {{ room.address }}</view>
|
||||
<view class="room-tags">
|
||||
<text v-for="tag in room.tags" :key="tag" class="room-tag">{{ tag }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 日期切换 -->
|
||||
<view class="date-tabs">
|
||||
<view v-for="(d, i) in dateTabs" :key="i" :class="['date-tab', i === activeDate ? 'active' : '']"
|
||||
@tap="handleDateChange(i)">{{ d }}</view>
|
||||
</view>
|
||||
|
||||
<!-- 时间选择列表 -->
|
||||
<view class="time-list-container">
|
||||
<scroll-view class="time-list" scroll-y>
|
||||
<view class="time-list-content">
|
||||
<view v-for="(slot, index) in timeSlots" :key="index" class="time-item"
|
||||
:class="[slot.status, selectedTime.includes(index) ? 'selected' : '']" @tap="selectTime(index)">
|
||||
<view class="time-range">{{ slot.startTime }} - {{ slot.endTime }}</view>
|
||||
<view class="time-status">
|
||||
<text v-if="slot.status === 'booked'">🚫 已预约</text>
|
||||
<text v-else-if="slot.status === 'disabled'">🚫 不可预约</text>
|
||||
<text v-else>✓ 可预约</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
|
||||
<!-- 预约信息 -->
|
||||
<view class="reserve-info">
|
||||
<view>会议时间:{{ selectedTimeText }}</view>
|
||||
<view>时长:{{ selectedDuration }}分钟</view>
|
||||
<view>发起人:{{ currentUser.name }}</view>
|
||||
</view>
|
||||
|
||||
<!-- 预约按钮 -->
|
||||
<view class="reserve-btn" @tap="showReserveModal">发起会议</view>
|
||||
|
||||
<!-- 预约弹窗 -->
|
||||
<view v-if="showModal" class="modal-overlay" @tap="hideModal">
|
||||
<view class="modal-content" @tap.stop>
|
||||
<view class="modal-header">
|
||||
<text class="modal-title">会议预约</text>
|
||||
<text class="modal-close" @tap="hideModal">×</text>
|
||||
</view>
|
||||
|
||||
<scroll-view class="modal-body" scroll-y>
|
||||
<!-- 会务服务选择 -->
|
||||
<view class="form-item">
|
||||
<text class="form-label">是否需要会务服务</text>
|
||||
<view class="service-options">
|
||||
<view v-for="option in serviceOptions" :key="option.value"
|
||||
:class="['service-option', needService === option.value ? 'active' : '']"
|
||||
@tap="selectService(option.value)">
|
||||
<text class="option-text">{{ option.label }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 桌子摆放形态选择 -->
|
||||
<view class="form-item">
|
||||
<text class="form-label">桌子摆放形态</text>
|
||||
<view class="layout-options">
|
||||
<view v-for="layout in deskLayouts" :key="layout.id"
|
||||
:class="['layout-option', selectedLayout === layout.id ? 'active' : '']" @tap="selectLayout(layout.id)">
|
||||
<text class="layout-name">{{ layout.layoutName }}</text>
|
||||
<text class="layout-desc">{{ layout.description }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<view class="modal-footer">
|
||||
<view class="modal-btn cancel-btn" @tap="hideModal" :class="{ disabled: isBooking }">取消</view>
|
||||
<view class="modal-btn confirm-btn" @tap="confirmReserve" :class="{ disabled: isBooking }">
|
||||
{{ isBooking ? '预约中...' : '确认预约' }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import meetingRoomApi from '@/sheep/api/meeting-room'
|
||||
import { filePath } from '@/sheep/utils/file'
|
||||
|
||||
// 定义类型
|
||||
interface TimeSlotItem {
|
||||
startTime: string
|
||||
endTime: string
|
||||
status: 'free' | 'booked' | 'disabled'
|
||||
bookedBy: {
|
||||
userId: string
|
||||
userName: string
|
||||
} | null
|
||||
}
|
||||
|
||||
interface ApiResponse<T> {
|
||||
code: number
|
||||
msg: string | null
|
||||
data: T
|
||||
ok: boolean
|
||||
}
|
||||
|
||||
// 会议室ID
|
||||
const roomId = ref('')
|
||||
|
||||
// 获取页面参数
|
||||
const props = defineProps<{
|
||||
roomId?: string
|
||||
roomInfo?: any
|
||||
}>()
|
||||
|
||||
// 使用传入的会议室信息,如果没有则使用默认值
|
||||
const room = ref({
|
||||
name: '一号会议室',
|
||||
capacity: 20,
|
||||
address: '公司总部南水吧台旁',
|
||||
tags: ['电视', '白板', '投影仪'],
|
||||
img: '/static/meeting/room1.jpg'
|
||||
})
|
||||
|
||||
const roomImage = computed(() => {
|
||||
const raw = room.value.img || ''
|
||||
const fallback = '/static/meeting/default.jpg'
|
||||
if (!raw) return fallback
|
||||
if (raw.startsWith('/static/') || raw.startsWith('data:')) return raw
|
||||
const src = filePath(raw)
|
||||
return src || raw || fallback
|
||||
})
|
||||
|
||||
// 当前用户信息
|
||||
const currentUser = ref({
|
||||
name: '张三',
|
||||
id: '1'
|
||||
})
|
||||
|
||||
// 日期选项
|
||||
const dateTabs = ['今天', '明天', '后天', '大后天']
|
||||
const activeDate = ref(0)
|
||||
|
||||
// 时间段数据
|
||||
const timeSlots = ref<TimeSlotItem[]>([])
|
||||
|
||||
// 选中的时间
|
||||
const selectedTime = ref<number[]>([])
|
||||
const selectedTimeText = computed(() => {
|
||||
if (selectedTime.value.length === 0) return '未选择'
|
||||
const start = timeSlots.value[selectedTime.value[0]]
|
||||
const end = timeSlots.value[selectedTime.value[selectedTime.value.length - 1]]
|
||||
return `${start.startTime} - ${end.endTime}`
|
||||
})
|
||||
const selectedDuration = computed(() => selectedTime.value.length * 30)
|
||||
|
||||
// 弹窗相关数据
|
||||
const showModal = ref(false)
|
||||
const needService = ref('0') // 默认不需要会务服务
|
||||
const selectedLayout = ref('') // 选中的桌子摆放形态ID
|
||||
const deskLayouts = ref<any[]>([]) // 桌子摆放形态列表
|
||||
const isBooking = ref(false) // 预约中状态
|
||||
|
||||
// 会务服务选项
|
||||
const serviceOptions = [
|
||||
{ value: '0', label: '不需要' },
|
||||
{ value: '1', label: '需要' }
|
||||
]
|
||||
|
||||
// 获取日期字符串
|
||||
const getDateString = (index: number) => {
|
||||
const date = new Date()
|
||||
date.setDate(date.getDate() + index)
|
||||
return date.toISOString().split('T')[0]
|
||||
}
|
||||
|
||||
// 处理日期切换
|
||||
const handleDateChange = async (index: number) => {
|
||||
activeDate.value = index
|
||||
const date = getDateString(index)
|
||||
await fetchTimeSlots(date)
|
||||
}
|
||||
|
||||
// 判断时间段是否已过期
|
||||
const isTimeSlotExpired = (slot: TimeSlotItem, date: string): boolean => {
|
||||
try {
|
||||
const now = new Date()
|
||||
const slotDateTime = new Date(`${date} ${slot.startTime}`)
|
||||
return slotDateTime < now
|
||||
} catch (error) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// 获取时间段数据
|
||||
const fetchTimeSlots = async (date: string) => {
|
||||
if (!roomId.value) {
|
||||
uni.showToast({
|
||||
title: '会议室ID不存在',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await meetingRoomApi.getTimeSlots(roomId.value, date)
|
||||
const response = res as unknown as ApiResponse<TimeSlotItem[]>
|
||||
|
||||
if (response.code === 0 && response.data) {
|
||||
// 处理时间段数据,将已过期的时间段标记为不可用
|
||||
timeSlots.value = response.data.map(slot => {
|
||||
if (isTimeSlotExpired(slot, date) && slot.status === 'free') {
|
||||
return { ...slot, status: 'disabled' as 'disabled' }
|
||||
}
|
||||
return slot
|
||||
})
|
||||
} else {
|
||||
uni.showToast({
|
||||
title: response.msg || '获取时间段失败',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
uni.showToast({
|
||||
title: '获取时间段失败',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 获取桌子摆放形态数据
|
||||
const fetchDeskLayouts = async () => {
|
||||
try {
|
||||
const res = await meetingRoomApi.getMeetingDeskLayout('')
|
||||
const response = res as unknown as ApiResponse<any[]>
|
||||
if (response.code === 0 && response.data) {
|
||||
deskLayouts.value = response.data
|
||||
}
|
||||
} catch (error) {
|
||||
// 获取桌子摆放形态失败
|
||||
}
|
||||
}
|
||||
|
||||
// 选择时间
|
||||
function selectTime(index: number) {
|
||||
const slot = timeSlots.value[index]
|
||||
if (slot.status === 'free') {
|
||||
// 检查是否已经选中
|
||||
const currentIndex = selectedTime.value.indexOf(index)
|
||||
if (currentIndex === -1) {
|
||||
// 检查是否连续
|
||||
if (selectedTime.value.length === 0) {
|
||||
selectedTime.value.push(index)
|
||||
} else {
|
||||
const last = selectedTime.value[selectedTime.value.length - 1]
|
||||
if (last === index - 1) {
|
||||
selectedTime.value.push(index)
|
||||
} else {
|
||||
uni.showToast({
|
||||
title: '请选择连续的时间段',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 取消选中
|
||||
selectedTime.value.splice(currentIndex, 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 显示预约弹窗
|
||||
function showReserveModal() {
|
||||
if (selectedTime.value.length === 0) {
|
||||
uni.showToast({ title: '请选择时间', icon: 'none' })
|
||||
return
|
||||
}
|
||||
showModal.value = true
|
||||
}
|
||||
|
||||
// 隐藏弹窗
|
||||
function hideModal() {
|
||||
showModal.value = false
|
||||
}
|
||||
|
||||
// 选择会务服务
|
||||
function selectService(value: string) {
|
||||
needService.value = value
|
||||
}
|
||||
|
||||
// 选择桌子摆放形态
|
||||
function selectLayout(layoutId: string) {
|
||||
selectedLayout.value = layoutId
|
||||
}
|
||||
|
||||
// 获取选中的桌子摆放形态名称
|
||||
const getSelectedLayoutName = () => {
|
||||
if (!selectedLayout.value) {
|
||||
return '未选择'
|
||||
}
|
||||
|
||||
const selectedLayoutItem = deskLayouts.value.find(layout => layout.id === selectedLayout.value)
|
||||
|
||||
if (selectedLayoutItem) {
|
||||
return selectedLayoutItem.layoutName
|
||||
} else {
|
||||
return '未选择'
|
||||
}
|
||||
}
|
||||
|
||||
// 确认预约
|
||||
async function confirmReserve() {
|
||||
if (!roomId.value) {
|
||||
uni.showToast({ title: '会议室ID不存在', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
// 防止重复点击
|
||||
if (isBooking.value) {
|
||||
return
|
||||
}
|
||||
|
||||
// 设置预约中状态
|
||||
isBooking.value = true
|
||||
|
||||
// 显示loading
|
||||
uni.showLoading({
|
||||
title: '预约中...',
|
||||
mask: true
|
||||
})
|
||||
|
||||
try {
|
||||
const date = getDateString(activeDate.value)
|
||||
const startSlot = timeSlots.value[selectedTime.value[0]]
|
||||
const endSlot = timeSlots.value[selectedTime.value[selectedTime.value.length - 1]]
|
||||
|
||||
const deskLayoutName = getSelectedLayoutName()
|
||||
|
||||
const res = await meetingRoomApi.bookMeetingRoom(roomId.value, {
|
||||
date,
|
||||
startTime: startSlot.startTime,
|
||||
endTime: endSlot.endTime,
|
||||
title: room.value.name || '会议',
|
||||
description: '',
|
||||
attendees: [],
|
||||
need_service: needService.value,
|
||||
deskLayout: deskLayoutName
|
||||
})
|
||||
|
||||
// 处理响应数据
|
||||
const responseData = res as any
|
||||
|
||||
// 检查业务错误(HTTP 200 但 code !== 0)
|
||||
if (responseData && responseData.code !== undefined) {
|
||||
if (responseData.code === 0) {
|
||||
// 成功:隐藏loading,显示成功提示
|
||||
uni.hideLoading()
|
||||
isBooking.value = false
|
||||
uni.showToast({ title: '预约成功', icon: 'success' })
|
||||
hideModal()
|
||||
setTimeout(() => {
|
||||
uni.navigateBack()
|
||||
}, 1500)
|
||||
} else {
|
||||
// 失败:隐藏loading,关闭弹窗,显示错误信息
|
||||
uni.hideLoading()
|
||||
isBooking.value = false
|
||||
|
||||
// 先关闭弹窗,再显示错误信息
|
||||
hideModal()
|
||||
|
||||
// 延迟显示错误信息,确保弹窗完全关闭
|
||||
setTimeout(() => {
|
||||
uni.showToast({
|
||||
title: responseData.msg || '预约失败',
|
||||
icon: 'none',
|
||||
duration: 3000
|
||||
})
|
||||
}, 300)
|
||||
}
|
||||
} else {
|
||||
// 响应格式异常:隐藏loading,显示错误信息
|
||||
uni.hideLoading()
|
||||
isBooking.value = false
|
||||
uni.showToast({
|
||||
title: '预约失败,响应格式异常',
|
||||
icon: 'none',
|
||||
duration: 3000
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
// 确保在异常情况下也隐藏loading
|
||||
uni.hideLoading()
|
||||
isBooking.value = false
|
||||
|
||||
// 处理HTTP错误(如500错误)
|
||||
const errorData = error as any
|
||||
if (errorData?.data?.msg) {
|
||||
uni.showToast({
|
||||
title: errorData.data.msg,
|
||||
icon: 'none',
|
||||
duration: 3000
|
||||
})
|
||||
} else {
|
||||
uni.showToast({
|
||||
title: '预约失败,请重试',
|
||||
icon: 'none',
|
||||
duration: 3000
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化数据
|
||||
onMounted(() => {
|
||||
// 从props获取roomId
|
||||
if (props.roomId) {
|
||||
roomId.value = props.roomId
|
||||
}
|
||||
|
||||
// 接收列表页传递的会议室信息
|
||||
const currentPage = getCurrentPages().pop()
|
||||
const eventChannel = currentPage ? (currentPage as any).getOpenerEventChannel() : undefined
|
||||
if (eventChannel) {
|
||||
eventChannel.on('acceptRoomInfo', (data: any) => {
|
||||
// 字段映射
|
||||
room.value.name = data.roomName || '未知会议室'
|
||||
room.value.capacity = data.capacity || 0
|
||||
room.value.address = data.location || '未知地址'
|
||||
room.value.img = data.picUrl || '/static/meeting/default.jpg'
|
||||
room.value.tags = data.equipmentNames ? data.equipmentNames.map((eq: { name: string, quantity?: number }) => eq.quantity ? `${eq.name} x${eq.quantity}` : eq.name) : []
|
||||
})
|
||||
}
|
||||
|
||||
// 获取当天的时间段
|
||||
const today = getDateString(0)
|
||||
fetchTimeSlots(today)
|
||||
|
||||
// 获取桌子摆放形态数据
|
||||
fetchDeskLayouts()
|
||||
|
||||
// 从 localStorage 获取用户信息
|
||||
try {
|
||||
const userInfoString = localStorage.getItem('user_info')
|
||||
if (userInfoString) {
|
||||
const userInfoData = JSON.parse(userInfoString)
|
||||
if (userInfoData && userInfoData.data && userInfoData.data.sysUser && userInfoData.data.sysUser.username) {
|
||||
currentUser.value.name = userInfoData.data.sysUser.username
|
||||
if (userInfoData.data.sysUser.userId) {
|
||||
currentUser.value.id = userInfoData.data.sysUser.userId.toString()
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// 解析用户信息失败
|
||||
}
|
||||
})
|
||||
|
||||
const goBack = () => {
|
||||
uni.navigateBack()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.container {
|
||||
min-height: 100vh;
|
||||
background: #f5f6fa;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
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;
|
||||
}
|
||||
|
||||
.nav-back {
|
||||
width: 60rpx;
|
||||
height: 60rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.back-arrow {
|
||||
color: #333;
|
||||
font-size: 40rpx;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.nav-title {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
color: #222;
|
||||
font-size: 32rpx;
|
||||
font-weight: 500;
|
||||
margin-right: 60rpx;
|
||||
}
|
||||
|
||||
.room-info-box {
|
||||
margin-top: 100rpx;
|
||||
display: flex;
|
||||
background: #fff;
|
||||
border-radius: 16rpx;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
||||
padding: 24rpx;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.room-img {
|
||||
width: 160rpx;
|
||||
height: 100rpx;
|
||||
border-radius: 8rpx;
|
||||
object-fit: cover;
|
||||
background: #eee;
|
||||
margin-right: 24rpx;
|
||||
}
|
||||
|
||||
.room-info-main {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.room-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #222;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.room-capacity {
|
||||
font-size: 24rpx;
|
||||
color: #888;
|
||||
margin-left: 8rpx;
|
||||
}
|
||||
|
||||
.room-address {
|
||||
font-size: 24rpx;
|
||||
color: #888;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.room-tags {
|
||||
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;
|
||||
}
|
||||
|
||||
.date-tabs {
|
||||
display: flex;
|
||||
margin: 24rpx 24rpx 0 24rpx;
|
||||
background: #fff;
|
||||
border-radius: 12rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.date-tab {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
padding: 18rpx 0;
|
||||
color: #888;
|
||||
font-size: 28rpx;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.date-tab.active {
|
||||
color: #1777FF;
|
||||
background: #e6f0ff;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.time-list-container {
|
||||
margin: 24rpx;
|
||||
background: #fff;
|
||||
border-radius: 12rpx;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.time-list {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.time-list-content {
|
||||
padding: 16rpx;
|
||||
}
|
||||
|
||||
.time-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 24rpx;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
background: #f8f9fc;
|
||||
margin-bottom: 16rpx;
|
||||
border-radius: 8rpx;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.time-item:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.time-item.free {
|
||||
background: #e6f0ff;
|
||||
color: #1777FF;
|
||||
}
|
||||
|
||||
.time-item.booked {
|
||||
background: #fff3e0;
|
||||
color: #ff9800;
|
||||
}
|
||||
|
||||
.time-item.disabled {
|
||||
background: #f5f5f5;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.time-item.selected {
|
||||
border: 2rpx solid #1777FF;
|
||||
}
|
||||
|
||||
.time-range {
|
||||
font-size: 28rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.time-status {
|
||||
font-size: 24rpx;
|
||||
padding: 4rpx 16rpx;
|
||||
border-radius: 4rpx;
|
||||
background: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
.reserve-info {
|
||||
margin: 0 24rpx;
|
||||
background: #fff;
|
||||
border-radius: 12rpx;
|
||||
padding: 16rpx;
|
||||
color: #333;
|
||||
font-size: 26rpx;
|
||||
}
|
||||
|
||||
.reserve-btn {
|
||||
margin: 24rpx;
|
||||
background: linear-gradient(90deg, #0094ff, #06be36);
|
||||
color: #fff;
|
||||
font-size: 30rpx;
|
||||
text-align: center;
|
||||
border-radius: 40rpx;
|
||||
padding: 24rpx 0;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* 弹窗样式 */
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
width: 90%;
|
||||
max-width: 600rpx;
|
||||
background: #fff;
|
||||
border-radius: 16rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 32rpx;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #222;
|
||||
}
|
||||
|
||||
.modal-close {
|
||||
font-size: 40rpx;
|
||||
color: #999;
|
||||
width: 60rpx;
|
||||
height: 60rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
max-height: 800rpx;
|
||||
padding: 32rpx;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.form-item {
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
display: block;
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
margin-bottom: 16rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.service-options {
|
||||
display: flex;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.service-option {
|
||||
flex: 1;
|
||||
padding: 20rpx;
|
||||
border: 2rpx solid #e0e0e0;
|
||||
border-radius: 8rpx;
|
||||
text-align: center;
|
||||
background: #fff;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.service-option.active {
|
||||
border-color: #1777FF;
|
||||
background: #e6f0ff;
|
||||
color: #1777FF;
|
||||
}
|
||||
|
||||
.option-text {
|
||||
font-size: 26rpx;
|
||||
}
|
||||
|
||||
.layout-options {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12rpx;
|
||||
}
|
||||
|
||||
.layout-option {
|
||||
padding: 20rpx;
|
||||
border: 2rpx solid #e0e0e0;
|
||||
border-radius: 8rpx;
|
||||
background: #fff;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.layout-option.active {
|
||||
border-color: #1777FF;
|
||||
background: #e6f0ff;
|
||||
}
|
||||
|
||||
.layout-name {
|
||||
display: block;
|
||||
font-size: 26rpx;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
margin-bottom: 4rpx;
|
||||
}
|
||||
|
||||
.layout-option.active .layout-name {
|
||||
color: #1777FF;
|
||||
}
|
||||
|
||||
.layout-desc {
|
||||
display: block;
|
||||
font-size: 22rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.layout-option.active .layout-desc {
|
||||
color: #1777FF;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
display: flex;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.modal-btn {
|
||||
flex: 1;
|
||||
padding: 24rpx;
|
||||
text-align: center;
|
||||
font-size: 28rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.cancel-btn {
|
||||
color: #666;
|
||||
border-right: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.confirm-btn {
|
||||
color: #1777FF;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.modal-btn.disabled {
|
||||
opacity: 0.5;
|
||||
pointer-events: none;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,335 @@
|
||||
<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="date-bar">
|
||||
<text class="date-text">{{ todayText }}</text>
|
||||
<text class="date-arrow">▼</text>
|
||||
</view>
|
||||
|
||||
<!-- 加载状态 -->
|
||||
<view v-if="loading" class="loading-container">
|
||||
<text class="loading-text">加载中...</text>
|
||||
</view>
|
||||
|
||||
<!-- 错误状态 -->
|
||||
<view v-else-if="error" class="error-container">
|
||||
<text class="error-text">{{ error }}</text>
|
||||
<view class="retry-btn" @tap="fetchMeetingRooms">重试</view>
|
||||
</view>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<view v-else-if="meetingRooms.length === 0" class="empty-container">
|
||||
<text class="empty-text">暂无会议室</text>
|
||||
</view>
|
||||
|
||||
<!-- 会议室列表 -->
|
||||
<view v-else>
|
||||
<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="getRoomImage(room.picUrl)" class="room-img" mode="aspectFill" />
|
||||
</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>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, onMounted } from "vue"
|
||||
import meetingRoomApi from "@/sheep/api/meeting-room"
|
||||
import { filePath } from "@/sheep/utils/file"
|
||||
|
||||
const goBack = () => {
|
||||
uni.navigateBack()
|
||||
}
|
||||
|
||||
const todayText = ref("")
|
||||
const meetingRooms = ref<any[]>([])
|
||||
const loading = ref(false)
|
||||
const error = ref("")
|
||||
|
||||
onMounted(() => {
|
||||
todayText.value = getTodayText()
|
||||
fetchMeetingRooms()
|
||||
})
|
||||
|
||||
function getTodayText() {
|
||||
const d = new Date()
|
||||
const week = ["日", "一", "二", "三", "四", "五", "六"]
|
||||
return `${d.getMonth() + 1}月${d.getDate()}日 周${week[d.getDay()]}`
|
||||
}
|
||||
|
||||
async function fetchMeetingRooms() {
|
||||
loading.value = true
|
||||
error.value = ""
|
||||
try {
|
||||
const res: any = await meetingRoomApi.getMeetingRoomList({ current: 1, size: 10 })
|
||||
if (res.code === 0) {
|
||||
meetingRooms.value = res.data.records || []
|
||||
} else {
|
||||
error.value = res.msg || '获取会议室列表失败'
|
||||
}
|
||||
} catch (err) {
|
||||
error.value = '网络请求失败'
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function goDetail(room) {
|
||||
uni.navigateTo({
|
||||
url: `/pages/integrated/meeting/detail?roomId=${room.id}`,
|
||||
success: function (res) {
|
||||
// 通过 eventChannel 传递 room 对象
|
||||
res.eventChannel.emit("acceptRoomInfo", room)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
function getRoomImage(picUrl: string) {
|
||||
const src = filePath(picUrl)
|
||||
return src || "/static/icon/service_4.png"
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
/* 移除外部CSS引用,使用本地图标或内联样式 */
|
||||
.container {
|
||||
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;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.nav-back .iconfont {
|
||||
font-size: 36rpx;
|
||||
margin-right: 8rpx;
|
||||
}
|
||||
|
||||
.nav-title {
|
||||
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;
|
||||
}
|
||||
|
||||
.date-text {
|
||||
color: #222;
|
||||
font-size: 28rpx;
|
||||
margin-right: 8rpx;
|
||||
}
|
||||
|
||||
.date-arrow {
|
||||
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;
|
||||
}
|
||||
|
||||
.room-info {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.room-main {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.room-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #222;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.room-capacity {
|
||||
font-size: 24rpx;
|
||||
color: #888;
|
||||
margin-left: 8rpx;
|
||||
}
|
||||
|
||||
.room-address {
|
||||
font-size: 24rpx;
|
||||
color: #888;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.room-tags {
|
||||
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;
|
||||
}
|
||||
|
||||
.room-img {
|
||||
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;
|
||||
}
|
||||
|
||||
.timeline-slot {
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
margin-right: 2rpx;
|
||||
border-radius: 4rpx;
|
||||
background: #e6eaf2;
|
||||
}
|
||||
|
||||
.timeline-slot.busy {
|
||||
background: #ffb74d;
|
||||
}
|
||||
|
||||
.timeline-slot.free {
|
||||
background: #4d8fff;
|
||||
}
|
||||
|
||||
.timeline-labels {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
margin-top: 8rpx;
|
||||
}
|
||||
|
||||
.timeline-label {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
color: #bbb;
|
||||
font-size: 20rpx;
|
||||
}
|
||||
|
||||
/* 加载状态样式 */
|
||||
.loading-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 80rpx 0;
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
color: #666;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
/* 错误状态样式 */
|
||||
.error-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 80rpx 32rpx;
|
||||
}
|
||||
|
||||
.error-text {
|
||||
color: #f44336;
|
||||
font-size: 28rpx;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.retry-btn {
|
||||
background: #1976d2;
|
||||
color: #fff;
|
||||
padding: 16rpx 32rpx;
|
||||
border-radius: 8rpx;
|
||||
font-size: 26rpx;
|
||||
}
|
||||
|
||||
/* 空状态样式 */
|
||||
.empty-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 80rpx 0;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
color: #999;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,351 @@
|
||||
<template>
|
||||
<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>
|
||||
|
||||
<!-- 预约卡片列表 -->
|
||||
<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>
|
||||
<text v-if="isEnded(item)" class="tag ended">已结束</text>
|
||||
<text v-else-if="isExpired(item)" class="tag expired">已过期</text>
|
||||
</view>
|
||||
<view class="card-info">
|
||||
<view>预约日期:{{ item.bookingDate }}</view>
|
||||
<view>时间:{{ item.startTime }} ~ {{ item.endTime }}</view>
|
||||
<view>申请人:{{ item.applicantName }}</view>
|
||||
<view>创建时间:{{ item.createTime }}</view>
|
||||
</view>
|
||||
<view class="card-footer">
|
||||
<view class="card-btn" @tap="viewDetail(item)">查看详情</view>
|
||||
<view v-if="Number(item.checkStatus) === 1" class="card-btn cancel" @tap="cancelBooking(item)">取消预约
|
||||
</view>
|
||||
<view v-if="Number(item.checkStatus) === 2 && !isEnded(item)" class="card-btn end"
|
||||
@tap="endMeeting(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>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from "vue"
|
||||
import meetingRoomApi from "@/sheep/api/meeting-room"
|
||||
|
||||
const tabs = ["全部", "审核中", "审核成功", "已拒绝", "已取消"]
|
||||
const activeTab = ref(0)
|
||||
const reservationList = ref<any[]>([])
|
||||
const loading = ref(false)
|
||||
const total = ref(0)
|
||||
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" },
|
||||
}
|
||||
|
||||
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
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchReservations()
|
||||
})
|
||||
|
||||
function onTabChange(idx: number) {
|
||||
activeTab.value = idx
|
||||
current.value = 1
|
||||
fetchReservations()
|
||||
}
|
||||
|
||||
function cancelBooking(item: any) {
|
||||
uni.showModal({
|
||||
title: "提示",
|
||||
content: "确定要取消该预约吗?",
|
||||
success: async (res) => {
|
||||
if (res.confirm) {
|
||||
try {
|
||||
uni.showLoading({ title: "处理中..." })
|
||||
const response: any = await meetingRoomApi.cancelBooking(item.id)
|
||||
uni.hideLoading()
|
||||
|
||||
if (response.code === 0) {
|
||||
uni.showToast({ title: "预约已取消", icon: "success" })
|
||||
// 刷新列表
|
||||
fetchReservations()
|
||||
} else {
|
||||
uni.showToast({
|
||||
title: response.msg || "操作失败",
|
||||
icon: "none"
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
uni.hideLoading()
|
||||
uni.showToast({ title: "操作失败,请重试", icon: "none" })
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// 是否已结束
|
||||
function isEnded(item: any) {
|
||||
return Number(item.isConfirmEnd) === 1
|
||||
}
|
||||
|
||||
// 是否已过期(当前日期大于预约日期,且未结束)
|
||||
function isExpired(item: any) {
|
||||
try {
|
||||
if (!item.bookingDate) return false
|
||||
const todayStr = new Date().toISOString().split('T')[0]
|
||||
return todayStr > item.bookingDate && !isEnded(item)
|
||||
} catch (e) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
function endMeeting(item: any) {
|
||||
uni.showModal({
|
||||
title: "提示",
|
||||
content: "确定要结束该会议吗?",
|
||||
success: async (res) => {
|
||||
if (res.confirm) {
|
||||
try {
|
||||
uni.showLoading({ title: "处理中..." })
|
||||
const response: any = await meetingRoomApi.confirmEndMeeting(item.id)
|
||||
uni.hideLoading()
|
||||
|
||||
if (response.code === 0) {
|
||||
uni.showToast({ title: "会议已结束", icon: "success" })
|
||||
// 刷新列表
|
||||
fetchReservations()
|
||||
} 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/meeting/booking-detail?id=" + item.id,
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.container {
|
||||
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;
|
||||
}
|
||||
|
||||
.tabs {
|
||||
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;
|
||||
}
|
||||
|
||||
.tab.active {
|
||||
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;
|
||||
}
|
||||
|
||||
.card {
|
||||
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;
|
||||
}
|
||||
|
||||
.card-icon {
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
margin-right: 12rpx;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.card-status {
|
||||
font-size: 26rpx;
|
||||
margin-left: 12rpx;
|
||||
}
|
||||
|
||||
.card-status.done {
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.card-status.pending {
|
||||
color: #1777ff;
|
||||
}
|
||||
|
||||
.card-status.cancel {
|
||||
color: #ff4d4f;
|
||||
}
|
||||
|
||||
.tag {
|
||||
margin-left: 8rpx;
|
||||
font-size: 22rpx;
|
||||
padding: 2rpx 10rpx;
|
||||
border-radius: 6rpx;
|
||||
}
|
||||
|
||||
.tag.ended {
|
||||
background: #f5f5f5;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.tag.expired {
|
||||
background: #fff1f0;
|
||||
color: #ff4d4f;
|
||||
}
|
||||
|
||||
.card-info {
|
||||
font-size: 26rpx;
|
||||
color: #666;
|
||||
margin-bottom: 12rpx;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.card-footer {
|
||||
display: flex;
|
||||
gap: 24rpx;
|
||||
}
|
||||
|
||||
.card-btn {
|
||||
background: #f2f6ff;
|
||||
color: #1777ff;
|
||||
border-radius: 8rpx;
|
||||
padding: 8rpx 24rpx;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.card-btn.cancel {
|
||||
background: #ffeaea;
|
||||
color: #ff4d4f;
|
||||
}
|
||||
|
||||
.card-btn.end {
|
||||
background: #e6f7ff;
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
.empty {
|
||||
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;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.empty-tip {
|
||||
font-size: 24rpx;
|
||||
color: #aaa;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,236 @@
|
||||
<template>
|
||||
<crud-layout title="通知公告" bg="/static/page/blue-bg.png">
|
||||
<view class="notice-detail">
|
||||
<view class="detail-header">
|
||||
<view class="title">
|
||||
<text v-if="detail.isTop === '1'" class="top-tag">置顶</text>
|
||||
{{ detail.title }}
|
||||
</view>
|
||||
<view class="meta">
|
||||
<text v-if="false" class="type-tag" :class="getTypeClass(detail.type)">{{ getTypeLabel(detail.type)
|
||||
}}</text>
|
||||
<text class="time">{{ formatTime(detail.createTime) }}</text>
|
||||
<text class="view-count">浏览 {{ detail.viewCount }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="detail-content">
|
||||
<rich-text :nodes="nodes || ''"></rich-text>
|
||||
</view>
|
||||
|
||||
<view v-if="detail.attachments && detail.attachments.length > 0" class="attachments">
|
||||
<view class="attachment-title">附件</view>
|
||||
<view v-for="(file, index) in detail.attachments" :key="index" class="attachment-item"
|
||||
@click="downloadFile(file)">
|
||||
<text class="file-name">{{ file.name }}</text>
|
||||
<text class="file-size">{{ formatFileSize(file.size) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<tm-button v-if="actDetail" class="px-2 my-4" block @click="goGetCoupon">去 领 券</tm-button>
|
||||
</view>
|
||||
</crud-layout>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from "vue"
|
||||
import { onLoad } from "@dcloudio/uni-app"
|
||||
import { actCancel, actComApi, noticeApi } from "@/sheep/api"
|
||||
|
||||
const noticeId = ref("")
|
||||
const detail = ref<any>({})
|
||||
const actDetail = ref<any>({
|
||||
actId: "",
|
||||
actType: "",
|
||||
detail: {
|
||||
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
|
||||
// 获取活动详情
|
||||
const getActDetail = async () => {
|
||||
if (!noticeId.value) return
|
||||
try {
|
||||
const { data = {} }: any = await actComApi.getActByNotifyId({ notifyId: noticeId.value })
|
||||
actDetail.value = data
|
||||
} catch (error) {
|
||||
console.log("|获取活动详情|,以外的错误error:", error);
|
||||
}
|
||||
}
|
||||
|
||||
const goGetCoupon = () => {
|
||||
uni.navigateTo({ url: `/pages/merchant/merchant-activity?actId=${actDetail.value.actId}&actType=${actDetail.value.actType}` })
|
||||
}
|
||||
|
||||
const nodes = computed(() => {
|
||||
return detail.value.description?.replaceAll(`<img src="/api`, `<img src="https://stgl.pj.gov.cn/api`)
|
||||
})
|
||||
|
||||
function getTypeLabel(type: string) {
|
||||
const typeMap: Record<string, string> = {
|
||||
"1": "重要",
|
||||
"2": "一般",
|
||||
"3": "活动",
|
||||
}
|
||||
return typeMap[type] || "一般"
|
||||
}
|
||||
|
||||
function getTypeClass(type: string) {
|
||||
const classMap: Record<string, string> = {
|
||||
"1": "type-important",
|
||||
"2": "type-general",
|
||||
"3": "type-activity",
|
||||
}
|
||||
return classMap[type] || "type-general"
|
||||
}
|
||||
|
||||
function formatTime(time: string) {
|
||||
if (!time) return ""
|
||||
return new Date(time).toLocaleString()
|
||||
}
|
||||
|
||||
function formatFileSize(size: number) {
|
||||
if (size < 1024) {
|
||||
return size + "B"
|
||||
} else if (size < 1024 * 1024) {
|
||||
return (size / 1024).toFixed(1) + "KB"
|
||||
} else {
|
||||
return (size / (1024 * 1024)).toFixed(1) + "MB"
|
||||
}
|
||||
}
|
||||
|
||||
function downloadFile(file: any) {
|
||||
uni.downloadFile({
|
||||
url: file.url,
|
||||
success: (res) => {
|
||||
if (res.statusCode === 200) {
|
||||
uni.openDocument({
|
||||
filePath: res.tempFilePath,
|
||||
success: () => {
|
||||
console.log("打开文档成功")
|
||||
},
|
||||
fail: () => {
|
||||
uni.showToast({ title: "无法打开此文件", icon: "none" })
|
||||
},
|
||||
})
|
||||
}
|
||||
},
|
||||
fail: () => {
|
||||
uni.showToast({ title: "下载失败", icon: "none" })
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
onLoad(async (options: any) => {
|
||||
const id = options.id
|
||||
if (!id) return
|
||||
noticeId.value = id
|
||||
getActDetail()
|
||||
try {
|
||||
// 增加浏览量
|
||||
await noticeApi.addNoticeView(id)
|
||||
// 获取详情
|
||||
const response: any = await noticeApi.getNoticeDetail(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>
|
||||
.notice-detail {
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.detail-header {
|
||||
background: #fff;
|
||||
border-radius: 16rpx;
|
||||
padding: 32rpx;
|
||||
margin-bottom: 24rpx;
|
||||
|
||||
.title {
|
||||
font-size: 36rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
line-height: 1.4;
|
||||
margin-bottom: 8rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.top-tag {
|
||||
background: #ffebee;
|
||||
color: #d32f2f;
|
||||
font-size: 22rpx;
|
||||
border-radius: 8rpx;
|
||||
padding: 12rpx 12rpx;
|
||||
margin-right: 12rpx;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.meta {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
color: #999;
|
||||
font-size: 24rpx;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.view-count {
|
||||
margin-left: 24rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.detail-content {
|
||||
background: #fff;
|
||||
border-radius: 16rpx;
|
||||
padding: 32rpx;
|
||||
margin-bottom: 24rpx;
|
||||
font-size: 30rpx;
|
||||
color: #333;
|
||||
line-height: 1.8;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.attachments {
|
||||
background: #fff;
|
||||
border-radius: 16rpx;
|
||||
padding: 32rpx;
|
||||
|
||||
.attachment-title {
|
||||
font-size: 30rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.attachment-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16rpx 0;
|
||||
border-bottom: 1rpx solid #f0f0f0;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.file-name {
|
||||
font-size: 28rpx;
|
||||
color: #007aff;
|
||||
flex: 1;
|
||||
margin-right: 16rpx;
|
||||
}
|
||||
|
||||
.file-size {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,229 @@
|
||||
<template>
|
||||
<crud-layout title="通知公告" bg="/static/page/blue-bg.png">
|
||||
<view class="notice-container">
|
||||
<view class="notice-list">
|
||||
<view v-if="noticeList.length === 0" class="empty-tip"> 暂无公告 </view>
|
||||
<view v-else>
|
||||
<view v-for="item in noticeList" :key="item.id" class="notice-item">
|
||||
<view class="item-main">
|
||||
<view class="icon-area">
|
||||
<view class="icon-bell">
|
||||
<view class="red-dot"></view>
|
||||
<text class="iconfont">
|
||||
<svg t="1755142010765" class="icon" viewBox="0 0 1024 1024" version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg" p-id="12392" width="24" height="24">
|
||||
<path
|
||||
d="M512 960c49.6 0 88-38.4 88-88H424c0 49.6 38.4 88 88 88z m288.8-286.4V444.8c0-137.6-97.6-252.8-224.8-283.2v-28.8c0-32-17.6-60.8-48-67.2-44-10.4-80 23.2-80 66.4v30.4C320.8 192 223.2 307.2 223.2 444.8v228.8L136 763.2v44.8h752v-44.8l-87.2-89.6z"
|
||||
p-id="12393" data-spm-anchor-id="a313x.search_index.0.i6.61dc3a812gyLD2"
|
||||
class="selected" fill="#3976DA"></path>
|
||||
</svg>
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="content-area">
|
||||
<view class="title">{{ item.title }}</view>
|
||||
<view class="summary">{{ item.summary || item.content }}</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="item-footer">
|
||||
<view class="detail-btn" @click="viewDetail(item)">查看详情</view>
|
||||
<view class="date">{{ formatDate(item.createTime) }}</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="hasMore" class="load-more" @click="loadMore">
|
||||
<text>加载更多</text>
|
||||
</view>
|
||||
</view>
|
||||
</crud-layout>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from "vue"
|
||||
import { noticeApi } from "@/sheep/api"
|
||||
|
||||
const noticeList = ref<any[]>([])
|
||||
const currentPage = ref(1)
|
||||
const hasMore = ref(true)
|
||||
|
||||
function formatDate(time: string) {
|
||||
if (!time) return ""
|
||||
const d = new Date(time)
|
||||
const y = d.getFullYear()
|
||||
const m = (d.getMonth() + 1).toString().padStart(2, "0")
|
||||
const day = d.getDate().toString().padStart(2, "0")
|
||||
return `${y}.${m}.${day}`
|
||||
}
|
||||
|
||||
function viewDetail(item: any) {
|
||||
uni.navigateTo({
|
||||
url: `/pages/integrated/notice/detail?id=${item.id}`,
|
||||
})
|
||||
}
|
||||
|
||||
async function loadNoticeList(reset = false) {
|
||||
try {
|
||||
const params = {
|
||||
current: currentPage.value,
|
||||
size: 10,
|
||||
}
|
||||
const response: any = await noticeApi.getNoticePage(params)
|
||||
if (response.code === 0 && response.data) {
|
||||
const records = response.data.records || []
|
||||
if (reset) {
|
||||
noticeList.value = records
|
||||
} else {
|
||||
noticeList.value = [...noticeList.value, ...records]
|
||||
}
|
||||
hasMore.value = records.length === 10
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("加载公告列表失败:", error)
|
||||
}
|
||||
}
|
||||
|
||||
function loadMore() {
|
||||
if (!hasMore.value) return
|
||||
currentPage.value++
|
||||
loadNoticeList()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadNoticeList(true)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.notice-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 88rpx;
|
||||
background: #fff;
|
||||
padding-left: 24rpx;
|
||||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.04);
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.back-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 30rpx;
|
||||
color: #1976d2;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.back-btn .iconfont {
|
||||
font-size: 36rpx;
|
||||
margin-right: 8rpx;
|
||||
}
|
||||
|
||||
.notice-container {
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.notice-list {}
|
||||
|
||||
.notice-item {
|
||||
background: #fff;
|
||||
border-radius: 16rpx;
|
||||
margin-bottom: 32rpx;
|
||||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.04);
|
||||
padding: 32rpx 32rpx 24rpx 32rpx;
|
||||
}
|
||||
|
||||
.item-main {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.icon-area {
|
||||
margin-right: 24rpx;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.icon-bell {
|
||||
width: 48rpx;
|
||||
height: 48rpx;
|
||||
background: #e3f2fd;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.icon-bell .iconfont {
|
||||
font-size: 32rpx;
|
||||
color: #1976d2;
|
||||
}
|
||||
|
||||
.red-dot {
|
||||
width: 14rpx;
|
||||
height: 14rpx;
|
||||
background: #f44336;
|
||||
border-radius: 50%;
|
||||
position: absolute;
|
||||
top: 4rpx;
|
||||
right: 4rpx;
|
||||
border: 2rpx solid #fff;
|
||||
}
|
||||
|
||||
.content-area {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 30rpx;
|
||||
font-weight: bold;
|
||||
color: #222;
|
||||
margin-bottom: 8rpx;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.summary {
|
||||
font-size: 26rpx;
|
||||
color: #888;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.item-footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
border-top: 1rpx solid #f0f0f0;
|
||||
margin-top: 18rpx;
|
||||
padding-top: 12rpx;
|
||||
}
|
||||
|
||||
.detail-btn {
|
||||
color: #1976d2;
|
||||
font-size: 26rpx;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.date {
|
||||
color: #999;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.empty-tip {
|
||||
text-align: center;
|
||||
color: #999;
|
||||
font-size: 28rpx;
|
||||
padding: 60rpx 0;
|
||||
}
|
||||
|
||||
.load-more {
|
||||
text-align: center;
|
||||
color: #007aff;
|
||||
font-size: 28rpx;
|
||||
padding: 32rpx 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,440 @@
|
||||
<template>
|
||||
<view class="repair-detail">
|
||||
<view class="detail-header">
|
||||
<view class="equipment-info">
|
||||
<text class="equipment-name">{{ detail.title || detail.equipmentName || '无标题' }}</text>
|
||||
<text class="location">{{ detail.location || '无位置' }}</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">
|
||||
{{ typeOptionsLoaded ? getTypeLabel(detail.type || detail.maintainType) : '加载中...' }}
|
||||
</text>
|
||||
</view>
|
||||
<view class="info-row">
|
||||
<text class="label">紧急程度:</text>
|
||||
<text class="value">{{ getUrgencyLabel(detail.urgency) }}</text>
|
||||
</view>
|
||||
<view class="info-row">
|
||||
<text class="label">联系电话:</text>
|
||||
<text class="value">{{ detail.phone }}</text>
|
||||
</view>
|
||||
<view class="description-section">
|
||||
<text class="label">故障描述:</text>
|
||||
<text class="description">{{ detail.description }}</text>
|
||||
</view>
|
||||
<view v-if="imageList.length" class="image-list">
|
||||
<image
|
||||
v-for="(img, idx) in imageList"
|
||||
:key="idx"
|
||||
:src="filePath(img)"
|
||||
class="repair-img"
|
||||
mode="aspectFill"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="detail.repairResult" class="result-section">
|
||||
<view class="result-title">维修结果</view>
|
||||
<view class="result-content">{{ detail.repairResult }}</view>
|
||||
<view class="result-time">{{ formatTime(detail.repairTime) }}</view>
|
||||
</view>
|
||||
|
||||
<view class="detail-footer">
|
||||
<text class="create-time">申请时间:{{ formatTime(detail.createTime) }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 回复内容 -->
|
||||
<view v-if="hasHandleInfo" class="reply-section">
|
||||
<view class="reply-title">回复内容</view>
|
||||
<view v-if="detail.handleRemark" class="reply-content">
|
||||
<text class="reply-text">{{ detail.handleRemark }}</text>
|
||||
</view>
|
||||
<view v-if="handleImageList.length" class="handle-image-list">
|
||||
<image
|
||||
v-for="(img, idx) in handleImageList"
|
||||
:key="idx"
|
||||
:src="filePath(img)"
|
||||
class="handle-img"
|
||||
mode="aspectFill"
|
||||
@click="previewHandleImage(idx)"
|
||||
/>
|
||||
</view>
|
||||
<view class="reply-footer">
|
||||
<text class="handle-by" v-if="detail.handleBy">处理人:{{ detail.handleBy }}</text>
|
||||
<text class="handle-time" v-if="detail.handleTime">{{ formatTime(detail.handleTime) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
import maintenanceApi from '@/sheep/api/maintenance'
|
||||
import { filePath } from "@/sheep/utils/file"
|
||||
|
||||
const detail = ref<any>({})
|
||||
const typeOptionsLoaded = ref(false)
|
||||
|
||||
const typeOptions = ref<any[]>([])
|
||||
|
||||
const urgencyOptions = [
|
||||
{ value: '1', label: '高' },
|
||||
{ value: '2', label: '中' },
|
||||
{ value: '3', label: '低' }
|
||||
]
|
||||
|
||||
// 加载维修类型选项
|
||||
async function loadTypeOptions() {
|
||||
try {
|
||||
const response: any = await maintenanceApi.getFeedbackType()
|
||||
if (response.code === 0 && response.data) {
|
||||
// 处理API返回的数据格式
|
||||
if (Array.isArray(response.data)) {
|
||||
typeOptions.value = response.data.map((item: any) => ({
|
||||
value: String(item.value || item.id),
|
||||
label: item.label || item.name || item.text
|
||||
}))
|
||||
} else {
|
||||
// 如果返回的是对象,尝试提取选项
|
||||
typeOptions.value = Object.entries(response.data).map(([key, value]: [string, any]) => ({
|
||||
value: key,
|
||||
label: value
|
||||
}))
|
||||
}
|
||||
typeOptionsLoaded.value = true
|
||||
} else {
|
||||
// 使用默认选项作为后备
|
||||
typeOptions.value = [
|
||||
{ value: '1', label: '电气设备' },
|
||||
{ value: '2', label: '网络设备' },
|
||||
{ value: '3', label: '办公设备' },
|
||||
{ value: '4', label: '空调设备' },
|
||||
{ value: '5', label: '其他' }
|
||||
]
|
||||
typeOptionsLoaded.value = true
|
||||
}
|
||||
} catch (error) {
|
||||
// 使用默认选项作为后备
|
||||
typeOptions.value = [
|
||||
{ value: '1', label: '电气设备' },
|
||||
{ value: '2', label: '网络设备' },
|
||||
{ value: '3', label: '办公设备' },
|
||||
{ value: '4', label: '空调设备' },
|
||||
{ value: '5', label: '其他' }
|
||||
]
|
||||
typeOptionsLoaded.value = true
|
||||
}
|
||||
}
|
||||
|
||||
function getTypeLabel(type: string) {
|
||||
const option = typeOptions.value.find(item => item.value === String(type))
|
||||
return option ? option.label : '其他'
|
||||
}
|
||||
|
||||
function getUrgencyLabel(urgency: string) {
|
||||
const option = urgencyOptions.find(item => item.value === String(urgency))
|
||||
return option ? option.label : '--'
|
||||
}
|
||||
|
||||
function getStatusText(status: string) {
|
||||
const statusMap: Record<string, string> = {
|
||||
'1': '已处理',
|
||||
'2': '处理中',
|
||||
'3': '已完成',
|
||||
'4': '已取消'
|
||||
}
|
||||
return statusMap[status] || '未知'
|
||||
}
|
||||
|
||||
function getStatusClass(status: string) {
|
||||
const classMap: Record<string, string> = {
|
||||
'1': 'status-completed',
|
||||
'2': 'status-processing',
|
||||
'3': 'status-completed',
|
||||
'4': 'status-cancelled'
|
||||
}
|
||||
return classMap[status] || 'status-pending'
|
||||
}
|
||||
|
||||
function formatTime(time: string) {
|
||||
if (!time) return ''
|
||||
return new Date(time).toLocaleString()
|
||||
}
|
||||
|
||||
// 计算图片数组
|
||||
const imageList = computed(() => {
|
||||
if (!detail.value.picUrl) return []
|
||||
return detail.value.picUrl.split(',').filter(Boolean)
|
||||
})
|
||||
|
||||
// 计算处理图片数组
|
||||
const handleImageList = computed(() => {
|
||||
if (!detail.value.handlePicUrl) return []
|
||||
const picUrl = String(detail.value.handlePicUrl).trim()
|
||||
if (!picUrl) return []
|
||||
return picUrl.split(',').map(item => item.trim()).filter(Boolean)
|
||||
})
|
||||
|
||||
// 判断是否有回复内容
|
||||
const hasHandleInfo = computed(() => {
|
||||
return detail.value.handleRemark || detail.value.handlePicUrl || detail.value.handleTime || detail.value.handleBy
|
||||
})
|
||||
|
||||
// 预览处理图片
|
||||
function previewHandleImage(index: number) {
|
||||
if (handleImageList.value.length === 0) return
|
||||
const urls = handleImageList.value.map(img => filePath(img))
|
||||
uni.previewImage({
|
||||
current: index,
|
||||
urls: urls
|
||||
})
|
||||
}
|
||||
|
||||
onLoad(async (options: any) => {
|
||||
const id = options.id
|
||||
if (!id) {
|
||||
uni.showToast({ title: '缺少维修ID', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
uni.showLoading({ title: '加载中...' })
|
||||
|
||||
// 先加载维修类型选项
|
||||
await loadTypeOptions()
|
||||
|
||||
// 再加载维修详情
|
||||
const response: any = await maintenanceApi.getRepairDetail(id)
|
||||
|
||||
if (response.code === 0 && response.data) {
|
||||
detail.value = response.data
|
||||
} else {
|
||||
uni.showToast({ title: response.msg || '获取详情失败', icon: 'none' })
|
||||
}
|
||||
|
||||
uni.hideLoading()
|
||||
} catch (error) {
|
||||
uni.hideLoading()
|
||||
uni.showToast({ title: '获取详情失败', icon: 'none' })
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.repair-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;
|
||||
|
||||
.equipment-info {
|
||||
.equipment-name {
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-right: 16rpx;
|
||||
}
|
||||
|
||||
.location {
|
||||
font-size: 26rpx;
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
|
||||
.status-tag {
|
||||
padding: 12rpx 24rpx;
|
||||
border-radius: 24rpx;
|
||||
font-size: 26rpx;
|
||||
|
||||
&.status-pending {
|
||||
background: #fff3e0;
|
||||
color: #f57c00;
|
||||
}
|
||||
|
||||
&.status-processing {
|
||||
background: #e8f5e8;
|
||||
color: #388e3c;
|
||||
}
|
||||
|
||||
&.status-completed {
|
||||
background: #e3f2fd;
|
||||
color: #1976d2;
|
||||
}
|
||||
|
||||
&.status-cancelled {
|
||||
background: #f5f5f5;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.detail-content {
|
||||
background: #fff;
|
||||
border-radius: 16rpx;
|
||||
padding: 32rpx;
|
||||
margin-bottom: 24rpx;
|
||||
|
||||
.info-row {
|
||||
display: flex;
|
||||
margin-bottom: 16rpx;
|
||||
|
||||
.label {
|
||||
font-size: 28rpx;
|
||||
color: #666;
|
||||
width: 160rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.value {
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.description-section {
|
||||
margin-top: 24rpx;
|
||||
padding-top: 24rpx;
|
||||
border-top: 1rpx solid #f0f0f0;
|
||||
|
||||
.label {
|
||||
font-size: 28rpx;
|
||||
color: #666;
|
||||
display: block;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.description {
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
line-height: 1.6;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.result-section {
|
||||
background: #fff;
|
||||
border-radius: 16rpx;
|
||||
padding: 32rpx;
|
||||
margin-bottom: 24rpx;
|
||||
|
||||
.result-title {
|
||||
font-size: 30rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.result-content {
|
||||
font-size: 28rpx;
|
||||
color: #666;
|
||||
line-height: 1.6;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.result-time {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
|
||||
.reply-section {
|
||||
background: #fff;
|
||||
border-radius: 16rpx;
|
||||
padding: 32rpx;
|
||||
margin-bottom: 24rpx;
|
||||
|
||||
.reply-title {
|
||||
font-size: 30rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-bottom: 16rpx;
|
||||
padding-bottom: 16rpx;
|
||||
border-bottom: 1rpx solid #f0f0f0;
|
||||
}
|
||||
|
||||
.reply-content {
|
||||
margin-bottom: 16rpx;
|
||||
|
||||
.reply-text {
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
line-height: 1.6;
|
||||
}
|
||||
}
|
||||
|
||||
.handle-image-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 16rpx;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.handle-img {
|
||||
width: 180rpx;
|
||||
height: 180rpx;
|
||||
border-radius: 8rpx;
|
||||
background: #f7f8fa;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.reply-footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding-top: 16rpx;
|
||||
border-top: 1rpx solid #f0f0f0;
|
||||
|
||||
.handle-by {
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.handle-time {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.detail-footer {
|
||||
background: #fff;
|
||||
border-radius: 16rpx;
|
||||
padding: 32rpx;
|
||||
|
||||
.create-time {
|
||||
font-size: 26rpx;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
.image-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 16rpx;
|
||||
margin-top: 16rpx;
|
||||
}
|
||||
.repair-img {
|
||||
width: 180rpx;
|
||||
height: 180rpx;
|
||||
border-radius: 8rpx;
|
||||
background: #f7f8fa;
|
||||
object-fit: cover;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,625 @@
|
||||
<template>
|
||||
<view class="repair-container">
|
||||
<view class="repair-tabs">
|
||||
<view :class="['tab', {active: activeTab === 0} ]" @click="activeTab = 0">维修申请</view>
|
||||
<view :class="['tab', {active: activeTab === 1} ]" @click="activeTab = 1">维修记录</view>
|
||||
</view>
|
||||
<view v-if="activeTab === 0">
|
||||
<!-- 维修申请表单 -->
|
||||
<view class="repair-form">
|
||||
<view class="form-title">维修申请</view>
|
||||
<view class="form-item">
|
||||
<view class="label">维修类型</view>
|
||||
<picker @change="onTypeChange" :value="typeIndex" :range="typeOptions" range-key="label">
|
||||
<view class="picker" :class="{ 'placeholder': typeIndex === -1 }">
|
||||
{{ typeIndex === -1 ? '请选择维修类型' : typeOptions[typeIndex].label }}
|
||||
</view>
|
||||
</picker>
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<view class="label">设备名称</view>
|
||||
<input v-model="title" placeholder="请输入设备名称" class="equipment-input" />
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<view class="label">设备位置</view>
|
||||
<input v-model="location" placeholder="请输入设备具体位置" class="location-input" />
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<view class="label">发现日期</view>
|
||||
<picker mode="date" :value="dateFound" @change="onDateChange">
|
||||
<view class="picker">{{ dateFound || '请选择日期' }}</view>
|
||||
</picker>
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<view class="label">图片</view>
|
||||
<tm-upload-photo
|
||||
v-model="uploadFileUrl"
|
||||
:url="'/api/admin/sys-file/upload'"
|
||||
:maxCount="5"
|
||||
:maxFileSize="10 * 1024 * 1024"
|
||||
:beforeComplete="beforeComplete"
|
||||
:header="header"
|
||||
/>
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<view class="label">故障描述</view>
|
||||
<textarea
|
||||
v-model="description"
|
||||
placeholder="请详细描述设备故障情况..."
|
||||
class="description-textarea"
|
||||
maxlength="300"
|
||||
/>
|
||||
<view class="char-count">{{ description.length }}/300</view>
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<view class="label">紧急程度</view>
|
||||
<picker @change="onUrgencyChange" :value="urgencyIndex" :range="urgencyOptions" range-key="label">
|
||||
<view class="picker">{{ urgencyOptions[urgencyIndex].label }}</view>
|
||||
</picker>
|
||||
</view>
|
||||
<button @click="submitRepair" class="submit-btn" :disabled="!canSubmit">
|
||||
提交申请
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
<view v-else>
|
||||
<!-- 维修记录列表 -->
|
||||
<view class="repair-list">
|
||||
<view class="list-title">维修记录</view>
|
||||
<view v-if="repairList.length === 0" class="empty-tip">
|
||||
暂无维修记录
|
||||
</view>
|
||||
<view v-else>
|
||||
<view
|
||||
v-for="item in repairList"
|
||||
:key="item.id"
|
||||
class="repair-item"
|
||||
@click="viewDetail(item)"
|
||||
>
|
||||
<view class="item-header">
|
||||
<view class="type-tag" :class="getTypeClass(item.type)">
|
||||
{{ typeOptionsLoaded ? getTypeLabel(item.type) : '加载中...' }}
|
||||
</view>
|
||||
<view class="status-tag" :class="getStatusClass(item.status)">{{ getStatusText(item.status) }}</view>
|
||||
</view>
|
||||
<view class="item-content">
|
||||
<view class="equipment-info">
|
||||
<text class="equipment-name">{{ item.title || item.equipmentName || '无标题' }}</text>
|
||||
<text class="location">{{ item.location }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="item-footer">
|
||||
<text class="time">{{ formatTime(item.createTime) }}</text>
|
||||
<text v-if="item.urgency === '1'" class="urgency-tag">紧急</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, watch, nextTick } from 'vue'
|
||||
import { onReachBottom, onPullDownRefresh } from '@dcloudio/uni-app'
|
||||
import maintenanceApi from '@/sheep/api/maintenance'
|
||||
import { hooks } from '@@/index'
|
||||
import { filePath } from '@/sheep/utils/file'
|
||||
|
||||
const typeOptions = ref<any[]>([])
|
||||
const typeOptionsLoaded = ref(false)
|
||||
|
||||
const urgencyOptions = [
|
||||
{ value: 3, label: '高' },
|
||||
{ value: 2, label: '中' },
|
||||
{ value: 1, label: '低' }
|
||||
]
|
||||
|
||||
const typeIndex = ref(-1)
|
||||
const urgencyIndex = ref(0)
|
||||
const title = ref('')
|
||||
const location = ref('')
|
||||
const description = ref('')
|
||||
const dateFound = ref('')
|
||||
const uploadFileUrl = ref<any[]>([])
|
||||
const repairList = ref<any[]>([])
|
||||
const activeTab = ref(0)
|
||||
const page = ref(1)
|
||||
const pageSize = 5
|
||||
const hasMore = ref(true)
|
||||
const loading = ref(false)
|
||||
const repairForm = ref<any>(null)
|
||||
const header = ref({})
|
||||
|
||||
const canSubmit = computed(() => {
|
||||
return title.value.trim() && location.value.trim() && description.value.trim() && dateFound.value && typeIndex.value !== -1
|
||||
})
|
||||
|
||||
function onTypeChange(e: any) {
|
||||
typeIndex.value = e.detail.value
|
||||
}
|
||||
|
||||
function onUrgencyChange(e: any) {
|
||||
urgencyIndex.value = e.detail.value
|
||||
}
|
||||
|
||||
function onDateChange(e: any) {
|
||||
dateFound.value = e.detail.value
|
||||
}
|
||||
|
||||
function beforeComplete(uploadFile: any) {
|
||||
const res = JSON.parse(uploadFile.data)
|
||||
return res.code == 0
|
||||
}
|
||||
|
||||
// 加载维修类型选项
|
||||
async function loadTypeOptions() {
|
||||
try {
|
||||
const response: any = await maintenanceApi.getFeedbackType()
|
||||
if (response.code === 0 && response.data) {
|
||||
// 处理API返回的数据格式
|
||||
if (Array.isArray(response.data)) {
|
||||
typeOptions.value = response.data.map((item: any) => ({
|
||||
value: item.value || item.id,
|
||||
label: item.label || item.name || item.text
|
||||
}))
|
||||
} else {
|
||||
// 如果返回的是对象,尝试提取选项
|
||||
typeOptions.value = Object.entries(response.data).map(([key, value]: [string, any]) => ({
|
||||
value: key,
|
||||
label: value
|
||||
}))
|
||||
}
|
||||
typeOptionsLoaded.value = true
|
||||
} else {
|
||||
// 使用默认选项作为后备
|
||||
typeOptions.value = [
|
||||
{ value: 1, label: '电气设备' },
|
||||
{ value: 2, label: '网络设备' },
|
||||
{ value: 3, label: '办公设备' },
|
||||
{ value: 4, label: '空调设备' },
|
||||
{ value: 5, label: '其他' }
|
||||
]
|
||||
typeOptionsLoaded.value = true
|
||||
}
|
||||
} catch (error) {
|
||||
// 使用默认选项作为后备
|
||||
typeOptions.value = [
|
||||
{ value: 1, label: '电气设备' },
|
||||
{ value: 2, label: '网络设备' },
|
||||
{ value: 3, label: '办公设备' },
|
||||
{ value: 4, label: '空调设备' },
|
||||
{ value: 5, label: '其他' }
|
||||
]
|
||||
typeOptionsLoaded.value = true
|
||||
}
|
||||
}
|
||||
|
||||
function getTypeLabel(type: number) {
|
||||
const option = typeOptions.value.find(item => item.value === type)
|
||||
return option ? option.label : '其他'
|
||||
}
|
||||
|
||||
function getTypeClass(type: number) {
|
||||
const classMap: Record<number, string> = {
|
||||
1: 'type-electrical',
|
||||
2: 'type-network',
|
||||
3: 'type-office',
|
||||
4: 'type-ac',
|
||||
5: 'type-other'
|
||||
}
|
||||
return classMap[type] || 'type-other'
|
||||
}
|
||||
|
||||
function getStatusText(status: string | number) {
|
||||
if (status === 1 || status === '1') return '已处理'
|
||||
// 只要不是1都显示未处理
|
||||
return '未处理'
|
||||
}
|
||||
|
||||
function getStatusClass(status: string | number) {
|
||||
if (status === 1 || status === '1') return 'status-completed'
|
||||
// 只要不是1都用未处理样式
|
||||
return 'status-pending'
|
||||
}
|
||||
|
||||
function formatTime(time: string) {
|
||||
if (!time) return ''
|
||||
return new Date(time).toLocaleDateString()
|
||||
}
|
||||
|
||||
async function submitRepair() {
|
||||
if (!canSubmit.value) {
|
||||
uni.showToast({ title: '请填写完整信息', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
// 验证维修类型是否已选择
|
||||
if (typeIndex.value === -1 || !typeOptions.value[typeIndex.value]) {
|
||||
uni.showToast({ title: '请选择维修类型', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
// 收集所有上传成功图片的 url
|
||||
let picUrl: string[] = []
|
||||
if (uploadFileUrl.value.length > 0) {
|
||||
uploadFileUrl.value.forEach(item => {
|
||||
if (item.response) {
|
||||
try {
|
||||
const res = JSON.parse(item.response)
|
||||
if (res.code === 0 && res.data && res.data.url) {
|
||||
picUrl.push(res.data.url)
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
try {
|
||||
uni.showLoading({ title: '提交中...' })
|
||||
const data = {
|
||||
title: title.value.trim(),
|
||||
picUrl: picUrl.join(','),
|
||||
description: description.value.trim(),
|
||||
dateFound: dateFound.value,
|
||||
location: location.value.trim(),
|
||||
urgency: String(urgencyOptions[urgencyIndex.value].value),
|
||||
maintainType: typeOptions.value[typeIndex.value].value
|
||||
}
|
||||
const response: any = await maintenanceApi.submitRepair(data)
|
||||
uni.hideLoading()
|
||||
if (response.code === 0) {
|
||||
uni.showToast({ title: '提交成功', icon: 'success' })
|
||||
// 清空表单
|
||||
title.value = ''
|
||||
location.value = ''
|
||||
description.value = ''
|
||||
dateFound.value = ''
|
||||
typeIndex.value = -1
|
||||
urgencyIndex.value = 0
|
||||
// 使用 nextTick 确保在下一个 DOM 更新周期清空上传列表
|
||||
await nextTick()
|
||||
uploadFileUrl.value = []
|
||||
// 刷新列表
|
||||
if (activeTab.value === 1) {
|
||||
loadRepairList()
|
||||
}
|
||||
} 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/repair/detail?id=${item.id}`
|
||||
})
|
||||
}
|
||||
|
||||
async function loadRepairList(isLoadMore = false, autoFill = false) {
|
||||
if (loading.value || (!hasMore.value && isLoadMore)) return
|
||||
loading.value = true
|
||||
let requestSuccess = false
|
||||
try {
|
||||
const response: any = await maintenanceApi.getRepairList({
|
||||
current: page.value,
|
||||
size: pageSize
|
||||
})
|
||||
if (response.code === 0 && response.data) {
|
||||
const records = response.data.records || []
|
||||
if (isLoadMore) {
|
||||
repairList.value = repairList.value.concat(records)
|
||||
} else {
|
||||
repairList.value = records
|
||||
}
|
||||
hasMore.value = records.length === pageSize
|
||||
if (hasMore.value) page.value += 1
|
||||
requestSuccess = true
|
||||
}
|
||||
} catch (e) {
|
||||
hasMore.value = false // 网络/接口异常,阻止递归
|
||||
} finally {
|
||||
loading.value = false
|
||||
// 只有请求成功时才自动补齐
|
||||
if (autoFill && requestSuccess) {
|
||||
await nextTick()
|
||||
if (hasMore.value) {
|
||||
const listDom = document.querySelector('.repair-list')
|
||||
if (listDom && listDom.scrollHeight <= window.innerHeight) {
|
||||
await loadRepairList(true, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function refreshRepairList() {
|
||||
page.value = 1
|
||||
hasMore.value = true
|
||||
loadRepairList(false, true)
|
||||
}
|
||||
|
||||
onReachBottom(() => {
|
||||
if (activeTab.value === 1) {
|
||||
loadRepairList(true, false)
|
||||
}
|
||||
})
|
||||
|
||||
onPullDownRefresh(() => {
|
||||
if (activeTab.value === 1) {
|
||||
page.value = 1
|
||||
hasMore.value = true
|
||||
loadRepairList(false, true).then(() => {
|
||||
uni.stopPullDownRefresh()
|
||||
})
|
||||
} else {
|
||||
uni.stopPullDownRefresh()
|
||||
}
|
||||
})
|
||||
|
||||
watch(activeTab, (val) => {
|
||||
if (val === 1) {
|
||||
refreshRepairList()
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
header.value = hooks.useDefaultHeader()
|
||||
loadTypeOptions()
|
||||
if (activeTab.value === 1) {
|
||||
refreshRepairList()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.repair-container {
|
||||
padding: 24rpx;
|
||||
background: #f5f5f5;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.repair-tabs {
|
||||
display: flex;
|
||||
background: #fff;
|
||||
border-radius: 16rpx 16rpx 0 0;
|
||||
overflow: hidden;
|
||||
margin-bottom: 24rpx;
|
||||
.tab {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
padding: 28rpx 0;
|
||||
font-size: 30rpx;
|
||||
color: #888;
|
||||
background: #f7f8fa;
|
||||
&.active {
|
||||
color: #1777FF;
|
||||
background: #fff;
|
||||
font-weight: bold;
|
||||
border-bottom: 4rpx solid #1777FF;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.repair-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;
|
||||
|
||||
&.placeholder {
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
.location-input,
|
||||
.equipment-input,
|
||||
.phone-input {
|
||||
height: 80rpx;
|
||||
padding: 0 24rpx;
|
||||
background: #f7f8fa;
|
||||
border-radius: 8rpx;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.description-textarea {
|
||||
width: 100%;
|
||||
height: 160rpx;
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.repair-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;
|
||||
}
|
||||
|
||||
.repair-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;
|
||||
|
||||
.type-tag {
|
||||
padding: 8rpx 16rpx;
|
||||
border-radius: 20rpx;
|
||||
font-size: 24rpx;
|
||||
|
||||
&.type-electrical {
|
||||
background: #fff3e0;
|
||||
color: #f57c00;
|
||||
}
|
||||
|
||||
&.type-network {
|
||||
background: #e3f2fd;
|
||||
color: #1976d2;
|
||||
}
|
||||
|
||||
&.type-office {
|
||||
background: #f3e5f5;
|
||||
color: #7b1fa2;
|
||||
}
|
||||
|
||||
&.type-ac {
|
||||
background: #e8f5e8;
|
||||
color: #388e3c;
|
||||
}
|
||||
|
||||
&.type-other {
|
||||
background: #f5f5f5;
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
|
||||
.status-tag {
|
||||
padding: 8rpx 16rpx;
|
||||
border-radius: 20rpx;
|
||||
font-size: 24rpx;
|
||||
|
||||
&.status-pending {
|
||||
background: #fff3e0;
|
||||
color: #f57c00;
|
||||
}
|
||||
|
||||
&.status-processing {
|
||||
background: #e8f5e8;
|
||||
color: #388e3c;
|
||||
}
|
||||
|
||||
&.status-completed {
|
||||
background: #e3f2fd;
|
||||
color: #1976d2;
|
||||
}
|
||||
|
||||
&.status-cancelled {
|
||||
background: #f5f5f5;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.item-content {
|
||||
margin-bottom: 16rpx;
|
||||
|
||||
.equipment-info {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 8rpx;
|
||||
|
||||
.equipment-name {
|
||||
font-size: 28rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.location {
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
|
||||
.description {
|
||||
font-size: 26rpx;
|
||||
color: #666;
|
||||
line-height: 1.5;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.item-footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
.time {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.urgency-tag {
|
||||
font-size: 24rpx;
|
||||
color: #d32f2f;
|
||||
background: #ffebee;
|
||||
padding: 4rpx 12rpx;
|
||||
border-radius: 12rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,110 @@
|
||||
<template>
|
||||
<view class="banner-detail">
|
||||
<view class="detail-header">
|
||||
<view class="title">{{ detail.title || "加载中..." }}</view>
|
||||
</view>
|
||||
|
||||
<view v-if="loading" class="loading-tip">加载中...</view>
|
||||
<view v-else-if="error" class="error-tip">{{ error }}</view>
|
||||
<view v-else class="detail-content">
|
||||
<rich-text :nodes="richTextNodes || ''" />
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from "vue"
|
||||
import { onLoad } from "@dcloudio/uni-app"
|
||||
import sysBannerApi from "@/sheep/api/sys-banner"
|
||||
import { filePath } from "@/sheep/utils/file"
|
||||
|
||||
const detail = ref<{ title?: string; richTextContent?: string }>({})
|
||||
const loading = ref(false)
|
||||
const error = ref("")
|
||||
|
||||
const toAbsoluteUrl = (url?: string | null) => {
|
||||
if (!url) return ""
|
||||
if (/^https?:\/\//i.test(url)) return url
|
||||
return filePath(url)
|
||||
}
|
||||
|
||||
const richTextNodes = computed(() => {
|
||||
const html = detail.value.richTextContent
|
||||
if (!html) return ""
|
||||
return html.replace(/<img\s+[^>]*src=["']([^"']+)["']/gi, (match, src) => {
|
||||
const fullSrc = toAbsoluteUrl(src)
|
||||
if (!fullSrc) return match
|
||||
return match.replace(src, fullSrc)
|
||||
})
|
||||
})
|
||||
|
||||
onLoad(async (options: any) => {
|
||||
const id = options.id
|
||||
if (!id) {
|
||||
error.value = "缺少轮播图 ID"
|
||||
return
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
error.value = ""
|
||||
try {
|
||||
const response: any = await sysBannerApi.getDetail(id)
|
||||
if (response.code === 0 && response.data) {
|
||||
detail.value = {
|
||||
title: response.data.title,
|
||||
richTextContent: response.data.richTextContent,
|
||||
}
|
||||
} else {
|
||||
error.value = response.msg || "轮播图不存在"
|
||||
}
|
||||
} catch (e) {
|
||||
error.value = "获取详情失败"
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.banner-detail {
|
||||
padding: 32rpx;
|
||||
background: #f5f5f5;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.detail-header {
|
||||
background: #fff;
|
||||
border-radius: 16rpx;
|
||||
padding: 32rpx;
|
||||
margin-bottom: 24rpx;
|
||||
|
||||
.title {
|
||||
font-size: 36rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
line-height: 1.4;
|
||||
}
|
||||
}
|
||||
|
||||
.loading-tip,
|
||||
.error-tip {
|
||||
text-align: center;
|
||||
padding: 80rpx 32rpx;
|
||||
color: #999;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.error-tip {
|
||||
color: #f44336;
|
||||
}
|
||||
|
||||
.detail-content {
|
||||
background: #fff;
|
||||
border-radius: 16rpx;
|
||||
padding: 32rpx;
|
||||
font-size: 30rpx;
|
||||
color: #333;
|
||||
line-height: 1.8;
|
||||
overflow: auto;
|
||||
}
|
||||
</style>
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,133 @@
|
||||
<template>
|
||||
<crud-layout title="详情" mainIsScroll bg="/static/page/blue-bg.png">
|
||||
<view v-if="detail.id">
|
||||
<view class="h-20rpx" />
|
||||
<tm-alert
|
||||
v-if="alertListType2[detail.checkStatus] && detail.type == '2'"
|
||||
:showClose="false"
|
||||
skin="normal"
|
||||
:color="alertListType2[detail.checkStatus].color"
|
||||
>
|
||||
{{ alertListType2[detail.checkStatus].text }}
|
||||
</tm-alert>
|
||||
<tm-alert
|
||||
v-if="alertListType1[detail.checkStatus] && detail.type == '1'"
|
||||
:showClose="false"
|
||||
skin="normal"
|
||||
:color="alertListType1[detail.checkStatus].color"
|
||||
>
|
||||
{{ alertListType1[detail.checkStatus].text }}
|
||||
</tm-alert>
|
||||
<tm-sheet :margin="[0, 20]" class="second-text-30">
|
||||
<tm-text class="prefix_title" font-size="32">访客信息</tm-text>
|
||||
<view class="flex mt-1">
|
||||
<text class="w-220rpx mr-6">访客姓名</text>
|
||||
<text>{{ detail.name }}</text>
|
||||
</view>
|
||||
<view class="flex mt-1">
|
||||
<text class="w-220rpx mr-6">访客手机号</text>
|
||||
<text>{{ detail.mobile }}</text>
|
||||
</view>
|
||||
<view class="flex mt-1">
|
||||
<text class="w-220rpx mr-6">访客公司名称</text>
|
||||
<text>{{ detail.companyName }}</text>
|
||||
</view>
|
||||
<view class="flex mt-1">
|
||||
<text class="w-220rpx mr-6">来访原因</text>
|
||||
<text>{{ visitorReasonList.find((item) => item.id == detail.reasonValue)?.title || "" }}</text>
|
||||
</view>
|
||||
<view class="flex mt-1">
|
||||
<text class="w-220rpx mr-6">接待人</text>
|
||||
<text>{{ detail.userName }}</text>
|
||||
</view>
|
||||
<view class="flex mt-1">
|
||||
<text class="w-220rpx mr-6">来访人数</text>
|
||||
<text>{{ detail.peopleNumber }}</text>
|
||||
</view>
|
||||
<view class="flex mt-1">
|
||||
<text class="w-220rpx mr-6">是否可随行人员</text>
|
||||
<text>{{ detail.isOtherPeople == "1" ? "是" : "否" }}</text>
|
||||
</view>
|
||||
<view class="flex mt-1">
|
||||
<text class="w-220rpx mr-6">是否可驾驶车辆</text>
|
||||
<text>{{ detail.isDrive == "1" ? "是" : "否" }}</text>
|
||||
</view>
|
||||
</tm-sheet>
|
||||
|
||||
<tm-sheet :margin="[0, 20]" class="second-text-30">
|
||||
<tm-text class="prefix_title" font-size="32">时间</tm-text>
|
||||
<view class="flex mt-1">
|
||||
<text class="w-220rpx mr-6">预计来访时间</text>
|
||||
<text>{{ detail.vendDate }}</text>
|
||||
</view>
|
||||
<view class="flex mt-1">
|
||||
<text class="w-220rpx mr-6">预计离开时间</text>
|
||||
<text>{{ detail.vstartDate }}</text>
|
||||
</view>
|
||||
</tm-sheet>
|
||||
|
||||
<tm-sheet :margin="[0, 20]" class="second-text-30">
|
||||
<tm-text class="prefix_title" font-size="32">其他</tm-text>
|
||||
<view class="flex mt-1">
|
||||
<text class="w-220rpx mr-6">创建方式</text>
|
||||
<text>{{ detail.type == "1" ? "主动申请" : detail.type == "2" ? "邀约" : "后台创建" }}</text>
|
||||
</view>
|
||||
<view class="flex mt-1">
|
||||
<text class="w-220rpx mr-6">提交时间</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 { invitationApi } from "@/sheep/api"
|
||||
|
||||
const alertListType2 = {
|
||||
1: { color: "#2db7f5", text: "访客还未接受邀约" },
|
||||
2: { color: "#52c41a", text: "访客待到访" },
|
||||
3: { color: "#f5a623", text: "访客拒绝了邀约" },
|
||||
5: { color: "#f5222d", text: "您取消了邀约" },
|
||||
}
|
||||
const alertListType1 = {
|
||||
1: { color: "#2db7f5", text: "您还未接受访客申请" },
|
||||
2: { color: "#52c41a", text: "访客待到访" },
|
||||
3: { color: "#f5a623", text: "您拒绝了访客申请" },
|
||||
5: { color: "#f5222d", text: "访客申请者取消了申请" },
|
||||
}
|
||||
|
||||
const detail = ref<any>({})
|
||||
const visitorReasonList = ref<{ title: string; id: string }[]>([])
|
||||
|
||||
onLoad(async (options: any) => {
|
||||
const id = options.id
|
||||
|
||||
try {
|
||||
uni.showLoading({ title: "加载中..." })
|
||||
const { code, data, msg } = await invitationApi.getInvitationDetail(id)
|
||||
if (code != 0) return uni.showToast({ title: msg || "获取详情失败", icon: "none" })
|
||||
detail.value = data
|
||||
} catch (error: any) {
|
||||
uni.showToast({ title: error.message, icon: "none" })
|
||||
} finally {
|
||||
uni.hideLoading()
|
||||
}
|
||||
|
||||
await 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>
|
||||
@@ -0,0 +1,204 @@
|
||||
<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: '2' },
|
||||
{ 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',
|
||||
marginTop: '10rpx',
|
||||
borderRadius: '20rpx',
|
||||
border: '1px solid #007AFF',
|
||||
}"
|
||||
:inactive-style="{
|
||||
fontSize: '30rpx',
|
||||
padding: '12rpx 64rpx',
|
||||
marginTop: '10rpx',
|
||||
border: '1px solid transparent',
|
||||
}"
|
||||
:badge-style="{
|
||||
// whiteSpace: 'nowrap',
|
||||
// position: 'relative',
|
||||
// top: '-14rpx',
|
||||
// left: '-72rpx',
|
||||
}"
|
||||
@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-22 rounded-1 bg-#2db7f5">待接受</view>
|
||||
<view v-if="v.checkStatus == '2'" class="p-1 c-white! sub-text-22 rounded-1 bg-#52c41a">待到访</view>
|
||||
<view v-if="v.checkStatus == '3'" class="p-1 c-white! sub-text-22 rounded-1 bg-#f5a623">已拒绝</view>
|
||||
<view v-if="v.checkStatus == '5'" class="p-1 c-white! sub-text-22 rounded-1 bg-#f5222d">已取消</view>
|
||||
<view class="sub-text-26">{{ v.vstartDate + " ~ " + v.vendDate }}</view>
|
||||
</view>
|
||||
<view class="m-2 h-1rpx bg-#A0A0A044"></view>
|
||||
<view class="sub-text-26 whitespace-nowrap ml-4">
|
||||
<view class="flex">
|
||||
<text class="w-160rpx">访客姓名</text>
|
||||
<text>{{ v.name || "---" }}</text>
|
||||
</view>
|
||||
<view class="flex">
|
||||
<text class="w-160rpx">访客手机</text>
|
||||
<text>{{ v.mobile || "---" }}</text>
|
||||
</view>
|
||||
<view class="flex">
|
||||
<text class="w-160rpx">邀请人</text>
|
||||
<text>{{ v.userName || "---" }}</text>
|
||||
</view>
|
||||
<view class="flex">
|
||||
<text class="w-160rpx">拜访原因</text>
|
||||
<text>{{ visitorReasonList.find((item) => item.id == v.reasonValue)?.title || "---" }}</text>
|
||||
</view>
|
||||
<view class="flex justify-end gap-1 mt-2">
|
||||
<template v-if="v.type == '1'">
|
||||
<tm-modal v-if="v.checkStatus == '1'" title="确定接受访客申请吗?" height="auto" @confirm="visitorCheck(v.id, '2')">
|
||||
<template #trigger><tm-button class="px-1" size="xs" color="success">接受申请</tm-button></template>
|
||||
</tm-modal>
|
||||
<tm-modal v-if="v.checkStatus == '1'" title="确定拒绝访客申请吗?" height="auto" @confirm="visitorCheck(v.id, '3')">
|
||||
<template #trigger><tm-button class="px-1" size="xs" color="danger">拒绝申请</tm-button></template>
|
||||
</tm-modal>
|
||||
<tm-button class="px-1" size="xs" @click.stop="goDetail(v)">查看详情</tm-button>
|
||||
</template>
|
||||
<template v-if="v.type == '2'">
|
||||
<tm-button class="px-1 mr-2" size="xs" @click.stop="goDetail(v)">查看详情</tm-button>
|
||||
<tm-modal
|
||||
v-if="v.checkStatus != '5' && v.checkStatus != '3'"
|
||||
title="确定取消邀约吗?"
|
||||
height="auto"
|
||||
@confirm="visitorCheck(v.id, '5')"
|
||||
>
|
||||
<template #trigger><tm-button class="px-1" size="xs" color="danger">取消邀约</tm-button></template>
|
||||
</tm-modal>
|
||||
</template>
|
||||
</view>
|
||||
</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 } 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 visitorReasonList = ref<{ title: string; id: string }[]>([])
|
||||
const state = reactive<any>({
|
||||
params: {
|
||||
checkStatus: tabs[0].value,
|
||||
type: 2, // type为1:访客端主动申请 type为2:邀约端邀约 type为3:后台创建
|
||||
},
|
||||
})
|
||||
const pageRef = ref<any>(null)
|
||||
|
||||
onShow(async () => {
|
||||
const isLogin = await hooks.useCheckLogin()
|
||||
if (!isLogin) return
|
||||
getVisitorReasonList()
|
||||
})
|
||||
|
||||
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 = (index: number, value: number) => {
|
||||
state.params.type = value
|
||||
pageRef.value.reload(true)
|
||||
}
|
||||
|
||||
// 获取拜访原因列表
|
||||
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 = []
|
||||
}
|
||||
|
||||
// 接受申请、拒绝申请、取消邀约(1:审核中 2:接受申请 3:拒绝申请 5:取消邀约)
|
||||
const visitorCheck = async (id: string, checkStatus: "1" | "2" | "3" | "5") => {
|
||||
if (!id) return uni.showToast({ title: "id不存在", icon: "error" })
|
||||
try {
|
||||
uni.showLoading({ title: "加载中...", mask: true })
|
||||
const { code, msg } = await invitationApi.updateCheckStatus({ id, checkStatus })
|
||||
if (code != 0) return uni.showToast({ title: msg || "操作失败", icon: "error" })
|
||||
pageRef.value.reload(false)
|
||||
uni.showToast({ title: "操作成功", icon: "success" })
|
||||
} finally {
|
||||
uni.hideLoading()
|
||||
}
|
||||
}
|
||||
|
||||
// 查看详情
|
||||
const goDetail = (v: any) => {
|
||||
if (!v.id || !v.token) return uni.showToast({ title: "id不存在", icon: "none" })
|
||||
router.jump("/pages/integrated/visitor/detail", { id: v.id, token: v.token })
|
||||
}
|
||||
|
||||
const goInvitation = () => {
|
||||
uni.redirectTo({ url: "/pages/integrated/visitor/invitation" })
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,192 @@
|
||||
<template>
|
||||
<crud-layout mainIsScroll bg="/static/page/blue-bg.png">
|
||||
<view class="h-20rpx" />
|
||||
<tm-sheet :margin="[0]">
|
||||
<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="mobile" label="访客手机号">
|
||||
<tm-input color="transparent" v-model.trim="form.mobile" 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="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 class="mt-4" form-type="submit" block>提 交</tm-button>
|
||||
</tm-form>
|
||||
</tm-sheet>
|
||||
|
||||
<tm-drawer
|
||||
title="请选择时间"
|
||||
:showClose="true"
|
||||
:showFooter="false"
|
||||
size="auto"
|
||||
v-model:show="showDatePicker"
|
||||
:duration="100"
|
||||
zIndex="100"
|
||||
>
|
||||
<time-range-picker @confirm="onDatePickerConfirm" @cancel="showDatePicker = false" />
|
||||
</tm-drawer>
|
||||
|
||||
<tm-modal
|
||||
v-model:show="confirmModalShow"
|
||||
:overlayClick="false"
|
||||
title="确定提交吗?"
|
||||
position="bottom"
|
||||
height="auto"
|
||||
:showCancel="true"
|
||||
cancelText="取消"
|
||||
confirmText="确定"
|
||||
@confirm="confirmSubmit"
|
||||
@cancel="confirmModalShow = false"
|
||||
>
|
||||
<view class="text-center">
|
||||
<tm-text font-size="30" color="#999999">提交后系统将会向手机号</tm-text>
|
||||
<tm-text font-size="30" color="#0779FD">{{ form.mobile || "" }}</tm-text>
|
||||
<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/time-range-picker.vue"
|
||||
|
||||
interface FormData {
|
||||
mobile: string
|
||||
vStartDate: string
|
||||
vEndDate: string
|
||||
isOtherPeople: boolean | string
|
||||
isDrive: boolean | string
|
||||
}
|
||||
|
||||
const form = reactive<FormData>({
|
||||
mobile: "",
|
||||
vStartDate: "",
|
||||
vEndDate: "",
|
||||
isOtherPeople: true,
|
||||
isDrive: true,
|
||||
})
|
||||
const showDatePicker = ref(false)
|
||||
const curDateType = ref("")
|
||||
const confirmModalShow = ref(false)
|
||||
|
||||
const rules: Record<string, TM.FORM_RULE[]> = {
|
||||
mobile: [{ required: true, message: "请输入正确的手机号", rule: /^(?:(?:\+|00)86)?1\d{10}$/ }],
|
||||
vStartDate: [{ required: true, message: "请选择拜访开始时间" }],
|
||||
vEndDate: [{ required: true, message: "请选择拜访离开时间" }],
|
||||
}
|
||||
|
||||
// 开始日期选择
|
||||
const startDatePicker = (dateType: string) => {
|
||||
showDatePicker.value = true
|
||||
curDateType.value = dateType
|
||||
}
|
||||
|
||||
// 离开日期选择
|
||||
const endDatePicker = (dateType: string) => {
|
||||
showDatePicker.value = true
|
||||
curDateType.value = dateType
|
||||
}
|
||||
|
||||
// 日期选择器确认
|
||||
const onDatePickerConfirm = (dateValue: any, timeValue: string) => {
|
||||
form[curDateType.value] = `${dateValue.fullDate} ${timeValue}`
|
||||
showDatePicker.value = false
|
||||
}
|
||||
|
||||
// 表单提交
|
||||
const onsubmit = (evt: TM.FORM_SUBMIT_RESULT) => {
|
||||
// console.log("校验结果:", evt)
|
||||
if (!evt.isPass) return
|
||||
confirmModalShow.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 {
|
||||
uni.showLoading({ title: "提交中...", mask: true })
|
||||
const { code, msg } = await invitationApi.saveInvitation(form)
|
||||
if (code != 0) return uni.showToast({ title: msg || "提交失败", icon: "error" })
|
||||
uni.showToast({ title: "提交成功", icon: "success" })
|
||||
resetForm()
|
||||
confirmModalShow.value = false
|
||||
uni.navigateTo({ url: "/pages/integrated/visitor/invitation-record" })
|
||||
} catch (error: any) {
|
||||
uni.showToast({ title: error.message || "提交失败", icon: "none" })
|
||||
} finally {
|
||||
uni.hideLoading()
|
||||
}
|
||||
}
|
||||
|
||||
// 重置表单
|
||||
const resetForm = () => {
|
||||
Object.assign(form, {
|
||||
mobile: "",
|
||||
vstartDate: "",
|
||||
vendDate: "",
|
||||
isOtherPeople: true,
|
||||
isDrive: true,
|
||||
})
|
||||
}
|
||||
|
||||
const goInvitationRecord = () => {
|
||||
uni.redirectTo({ url: "/pages/integrated/visitor/invitation-record" })
|
||||
}
|
||||
</script>
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<crud-layout mainIsScroll bg="/static/page/blue-bg.png">
|
||||
<crud-layout title="活动详情" mainIsScroll bg="/static/page/blue-bg.png">
|
||||
<view v-if="state.actId" class="px-2 py-4">
|
||||
<view v-if="state.actType == 'CANCEL_ACT'"
|
||||
class="card rounded-lb-0! rounded-rb-0! w-full h-400rpx relative activity-card overflow-hidden">
|
||||
@@ -71,8 +71,9 @@
|
||||
</template>
|
||||
</view>
|
||||
<template #main-bottom>
|
||||
<view class="text-center">
|
||||
<tm-button class="px-32 m-2" @click="doQrcode">我的礼券</tm-button>
|
||||
<view class="w-full flex-col-center">
|
||||
<tm-button class="px-32 m-2" width="90%" @click="doQrcode">我的礼券</tm-button>
|
||||
<view class="h-10rpx"></view>
|
||||
</view>
|
||||
</template>
|
||||
</crud-layout>
|
||||
|
||||
@@ -141,7 +141,7 @@ const genVerificationCode = async (params: Record<string, any>) => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<crud-layout mainIsScroll bg="/static/page/blue-bg.png">
|
||||
<crud-layout title="我参加的活动" mainIsScroll bg="/static/page/blue-bg.png">
|
||||
<z-paging ref="pageRef" height="100%" :fixed="false" v-model="dynamicPageHook.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"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<view class="w-full h-full">
|
||||
<view title="支付成功" class="w-full h-full">
|
||||
<view class="flex-col-center p-30rpx">
|
||||
<view class="flex-col-center m-y-50rpx">
|
||||
<svg t="1742978804614" class="icon" viewBox="0 0 1024 1024" version="1.1"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<crud-layout bg="/static/page/blue-bg.png">
|
||||
<crud-layout title="支付" bg="/static/page/blue-bg.png">
|
||||
<view class="h-screen flex flex-col">
|
||||
<view class="mx-auto text-34 py-2 h-80rpx">{{ scanPayState.mchName }}</view>
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<crud-layout mainIsScroll bg="/static/page/blue-bg.png">
|
||||
<crud-layout title="转账" mainIsScroll bg="/static/page/blue-bg.png">
|
||||
<view class="h-20rpx"></view>
|
||||
<view class="p-4 card flex items-center justify-between">
|
||||
<view class="flex items-center">
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
<script setup lang="ts">
|
||||
import { reactive, computed } from "vue"
|
||||
import { onHide, onShow } from "@dcloudio/uni-app"
|
||||
|
||||
import { router } from "@@/index"
|
||||
import { noticeApi } from "@@/api/index"
|
||||
|
||||
import Empty from "@@/components/empty/empty.vue"
|
||||
|
||||
const state = reactive<any>({
|
||||
swiperList: [
|
||||
{
|
||||
image: "/static/swiper/swiper-1.png"
|
||||
}
|
||||
],
|
||||
noticeList: [
|
||||
|
||||
],
|
||||
appList: []
|
||||
})
|
||||
const groupSize = 3
|
||||
const shouldScroll = computed(() => state.noticeList.length > groupSize)
|
||||
|
||||
const groupedNoticeList = computed(() => {
|
||||
const base = state.noticeList
|
||||
if (!base || base.length === 0) return []
|
||||
if (base.length <= groupSize) return base
|
||||
const groups: any[] = []
|
||||
for (let i = 0; i < base.length; i += groupSize) {
|
||||
const group = base.slice(i, i + groupSize)
|
||||
if (group.length < groupSize) {
|
||||
group.push(...base.slice(0, groupSize - group.length))
|
||||
}
|
||||
groups.push(group)
|
||||
}
|
||||
return groups
|
||||
})
|
||||
|
||||
onShow(() => {
|
||||
getNoticeList()
|
||||
})
|
||||
|
||||
onHide(() => {
|
||||
})
|
||||
|
||||
// 获取通知公告列表
|
||||
const getNoticeList = async () => {
|
||||
try {
|
||||
const { data, code }: any = await noticeApi.getNoticePage({
|
||||
current: 1,
|
||||
size: 9
|
||||
})
|
||||
if (code != 0) {
|
||||
throw new Error()
|
||||
}
|
||||
state.noticeList = data?.records || []
|
||||
} catch (error) {
|
||||
console.log("获取通知公告列表意外的错误error:", error);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<crud-layout title="智慧后勤系统" bg="/static/page/blue-bg.png">
|
||||
<view class="h-20rpx" />
|
||||
<swiper class="w-full h-280rpx rounded-16rpx overflow-hidden" circular :indicator-dots="true"
|
||||
indicator-active-color="#0779FD" indicator-color="#ffffff" :autoplay="true">
|
||||
<swiper-item v-for="(v, i) in state?.swiperList" :key="i">
|
||||
<image class="w-full h-full" :src="v.image" mode="aspectFill" />
|
||||
</swiper-item>
|
||||
</swiper>
|
||||
<view class="h-20rpx" />
|
||||
<view class="relative shadow-main min-h-350rpx rounded-16rpx bg-white">
|
||||
<view
|
||||
class="container-title font-size-28rpx absolute top-0 left-0 color-white w-200rpx h-50rpx line-height-50rpx text-center rounded-lt-16rpx bg-gradient-to-r from-[#22b0ff] to-[#557bfa]">
|
||||
通知公告
|
||||
</view>
|
||||
<view
|
||||
class="absolute color-blue right-20rpx font-size-24rpx top-8rpx rounded-full p-x-12rpx p-y-6rpx bg-#f2f6ff"
|
||||
@tap="() => router.jump('/pages/integrated/notice/index')">
|
||||
查看更多
|
||||
</view>
|
||||
<view class="w-full h-50rpx" />
|
||||
<view class="h-300rpx overflow-hidden" v-if="state?.noticeList?.length > 0">
|
||||
<swiper class="w-full h-full" :vertical="true" :circular="shouldScroll" :autoplay="shouldScroll"
|
||||
:interval="2500" :duration="500" :indicator-dots="false" :disable-touch="true">
|
||||
<swiper-item v-for="(group, i) in groupedNoticeList" :key="i">
|
||||
<view class="w-full">
|
||||
<view
|
||||
class="h-100rpx flex justify-between items-center p-x-16rpx p-y-20rpx border-b-1 border-color-#f0f0f0"
|
||||
v-for="(v, idx) in group" :key="idx">
|
||||
<view class="flex-1 sub-text-26 font-normal!">{{ v.title }}</view>
|
||||
<view class="w-250rpx second-text-24">{{ v.createTime }}</view>
|
||||
</view>
|
||||
</view>
|
||||
</swiper-item>
|
||||
</swiper>
|
||||
</view>
|
||||
<view v-else>
|
||||
<empty text="暂无公告" />
|
||||
</view>
|
||||
</view>
|
||||
<view class="h-20rpx" />
|
||||
<view class="relative shadow-main min-h-360rpx rounded-16rpx bg-white">
|
||||
<view
|
||||
class="container-title font-size-28rpx absolute top-0 left-0 color-white w-200rpx h-50rpx line-height-50rpx text-center rounded-lt-16rpx bg-gradient-to-r from-[#22b0ff] to-[#557bfa]">
|
||||
常用功能
|
||||
</view>
|
||||
</view>
|
||||
</crud-layout>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.container-title::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: -40rpx;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-style: solid;
|
||||
border-width: 50rpx 40rpx 0 0;
|
||||
border-color: #557bfa transparent transparent transparent;
|
||||
}
|
||||
</style>
|
||||
@@ -139,7 +139,7 @@ const doTransferMoney = (v: any) => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<crud-layout bg="/static/page/blue-bg.png">
|
||||
<crud-layout title="我的钱包" bg="/static/page/blue-bg.png">
|
||||
<z-paging ref="pageRef" height="100%" :fixed="false" v-model="pageHook.list" :default-page-size="5"
|
||||
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">
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import request from "@/sheep/utils/request"
|
||||
|
||||
export default {
|
||||
/**
|
||||
* 提交意见反馈
|
||||
*/
|
||||
submitFeedback: (data: {
|
||||
title: string;
|
||||
description: string;
|
||||
picUrl?: string;
|
||||
type: string;
|
||||
contact?: string;
|
||||
}) =>
|
||||
request({
|
||||
url: '/integrated/feedback/app/save',
|
||||
method: 'POST',
|
||||
data,
|
||||
}),
|
||||
|
||||
/**
|
||||
* 获取我的反馈列表
|
||||
*/
|
||||
getFeedbackList: (params: { current: number; size: number }) =>
|
||||
request({
|
||||
url: '/integrated/feedback/app/page',
|
||||
method: 'GET',
|
||||
params,
|
||||
}),
|
||||
|
||||
/**
|
||||
* 获取反馈详情
|
||||
*/
|
||||
getFeedbackDetail: (id: string) =>
|
||||
request({
|
||||
url: '/integrated/feedback/app/getDetails',
|
||||
method: 'GET',
|
||||
params: { id },
|
||||
}),
|
||||
|
||||
/**
|
||||
* 获取反馈类型字典
|
||||
*/
|
||||
getFeedbackType: () =>
|
||||
request({
|
||||
url: '/integrated/feedback/app/getFeedbackType',
|
||||
method: 'GET',
|
||||
}),
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -7,14 +7,23 @@ export { default as applyApi } from "./apply"
|
||||
export { default as adApi } from "./ad"
|
||||
export { default as boxesApi } from "./boxes"
|
||||
export { default as faceApi } from "./face"
|
||||
export { default as feedbackApi } from "./feedback"
|
||||
export { default as foodApi } from "./food"
|
||||
export { default as integratedApi } from "./integrated"
|
||||
export { default as invitationApi } from "./invitation"
|
||||
export { default as lostAndFoundApi } from "./lost-and-found"
|
||||
export { default as maintenanceApi } from "./maintenance"
|
||||
export { default as meetingRoomApi } from "./meeting-room"
|
||||
export { default as merchantApi } from "./merchant"
|
||||
export { default as miniPlatformApi } from "./mini-platform"
|
||||
export { default as noticeApi } from "./notice"
|
||||
export { default as orderApi } from "./order"
|
||||
export { default as payApi } from "./pay"
|
||||
export { default as productApi } from "./product"
|
||||
export { default as refundApi } from "./refund"
|
||||
export { default as shopApi } from "./shop"
|
||||
export { default as sysBannerApi } from "./sys-banner"
|
||||
export { default as taskApi } from "./task"
|
||||
export { default as totalApi } from "./total"
|
||||
export { default as userCarApi } from "./user-car"
|
||||
export { default as userApi } from "./user"
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import request from '@/sheep/utils/request'
|
||||
|
||||
export default {
|
||||
// 工作待办列表
|
||||
getTodoList: (params = {}) =>
|
||||
request({
|
||||
url: '/integrated/task/process-instance/queryMineTask',
|
||||
method: 'GET',
|
||||
params,
|
||||
}),
|
||||
// 我的已办列表
|
||||
getDoneList: (params = {}) =>
|
||||
request({
|
||||
url: '/integrated/task/process-instance/queryMineEndTask',
|
||||
method: 'GET',
|
||||
params,
|
||||
}),
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import request from "@/sheep/utils/request"
|
||||
|
||||
export default {
|
||||
// 获取拜访原因字典
|
||||
getDictListByType: (type: number | string): Promise<any> =>
|
||||
request({
|
||||
url: `/integrated/visitors/getDictListByType?type=${type}`,
|
||||
method: "GET",
|
||||
}),
|
||||
|
||||
// 保存访客邀约
|
||||
saveInvitation: (data: any): Promise<any> =>
|
||||
request({
|
||||
url: `/integrated/visitors/app/saveYY`,
|
||||
method: "POST",
|
||||
data,
|
||||
}),
|
||||
|
||||
// 获取访客邀约记录列表
|
||||
// type为1:主动申请 type为2:邀约 type为3:后台创建
|
||||
getInvitationRecord: (params: { current: number; size: number; checkStatus?: string; type?: number }): Promise<any> =>
|
||||
request({
|
||||
url: `/integrated/visitors/app/yy/page`,
|
||||
method: "GET",
|
||||
params,
|
||||
}),
|
||||
|
||||
// 更新审核状态通过 2:通过 3:拒绝
|
||||
updateCheckStatus: (params: { id: string; checkStatus: number | string }): Promise<any> =>
|
||||
request({
|
||||
url: `/integrated/visitors/app/updateCheckStatus`,
|
||||
method: "GET",
|
||||
params,
|
||||
}),
|
||||
|
||||
// 查看邀约详情
|
||||
getInvitationDetail: (id: string): Promise<any> =>
|
||||
request({
|
||||
url: `/integrated/visitors/${id}`,
|
||||
method: "GET",
|
||||
}),
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import request from "@/sheep/utils/request"
|
||||
|
||||
interface LostAndFoundItem {
|
||||
id: string
|
||||
title: string
|
||||
picUrl: string | null
|
||||
description: string
|
||||
dateFound: string
|
||||
location: string
|
||||
status: string
|
||||
}
|
||||
|
||||
interface PageData {
|
||||
records: LostAndFoundItem[]
|
||||
total: number
|
||||
size: number
|
||||
current: number
|
||||
pages: number
|
||||
}
|
||||
|
||||
interface ApiResponse {
|
||||
code: number
|
||||
msg: string | null
|
||||
data: PageData
|
||||
ok: boolean
|
||||
}
|
||||
|
||||
export default {
|
||||
// 获取失物招领列表
|
||||
getList: (params: { current: number; size: number }) =>
|
||||
request({
|
||||
url: "integrated/lostAndFound/app/page",
|
||||
method: "GET",
|
||||
params,
|
||||
}),
|
||||
|
||||
// 获取失物招领详情
|
||||
getDetail: (id: string) =>
|
||||
request({
|
||||
url: "integrated/lostAndFound/app/getDetails",
|
||||
method: "GET",
|
||||
params: { id },
|
||||
}),
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import request from "@/sheep/utils/request"
|
||||
|
||||
export default {
|
||||
/**
|
||||
* 提交维修申请
|
||||
*/
|
||||
submitRepair: (data: {
|
||||
title: string;
|
||||
picUrl?: string;
|
||||
description: string;
|
||||
dateFound: string;
|
||||
location: string;
|
||||
urgency: string;
|
||||
maintainType: number;
|
||||
}) =>
|
||||
request({
|
||||
url: '/integrated/maintenanceOrder/app/apply',
|
||||
method: 'POST',
|
||||
data,
|
||||
}),
|
||||
|
||||
/**
|
||||
* 获取我的维修记录列表
|
||||
*/
|
||||
getRepairList: (params: { current: number; size: number }) =>
|
||||
request({
|
||||
url: '/integrated/maintenanceOrder/app/my',
|
||||
method: 'GET',
|
||||
params,
|
||||
}),
|
||||
|
||||
/**
|
||||
* 获取维修详情
|
||||
*/
|
||||
getRepairDetail: (id: string | number) =>
|
||||
request({
|
||||
url: `/integrated/maintenanceOrder/app/my/${id}`,
|
||||
method: 'GET',
|
||||
}),
|
||||
|
||||
/**
|
||||
* 获取维修类型选项
|
||||
*/
|
||||
getFeedbackType: () =>
|
||||
request({
|
||||
url: '/integrated/maintenanceOrder/app/getFeedbackType',
|
||||
method: 'GET',
|
||||
}),
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
import request from "@/sheep/utils/request"
|
||||
|
||||
export default {
|
||||
/**
|
||||
* 会议室分页列表
|
||||
*/
|
||||
getMeetingRoomList: (params: { current: number; size: number }) =>
|
||||
request({
|
||||
url: "/integrated/meetingRoom/app/page",
|
||||
method: "GET",
|
||||
params,
|
||||
}),
|
||||
|
||||
/**
|
||||
* 获取会议室时间段
|
||||
*/
|
||||
getTimeSlots: (roomId: string, date: string) =>
|
||||
request({
|
||||
url: `/integrated/meetingRoom/${roomId}/time-slots`,
|
||||
method: "GET",
|
||||
params: { date },
|
||||
}),
|
||||
|
||||
/**
|
||||
* 预约会议室
|
||||
*/
|
||||
bookMeetingRoom: (roomId: string, data: {
|
||||
date: string;
|
||||
startTime: string;
|
||||
endTime: string;
|
||||
title: string;
|
||||
description: string;
|
||||
attendees: string[];
|
||||
need_service?: string;
|
||||
deskLayout?: string;
|
||||
}) =>
|
||||
request({
|
||||
url: `/integrated/meetingRoom/${roomId}/book`,
|
||||
method: "POST",
|
||||
data,
|
||||
}),
|
||||
|
||||
/**
|
||||
* 获取我的预约列表
|
||||
*/
|
||||
getMyBookings: (params: { current: number; size: number; checkStatus?: number }) =>
|
||||
request({
|
||||
url: '/integrated/meetingRoom/app/my-bookings',
|
||||
method: 'GET',
|
||||
params,
|
||||
}),
|
||||
|
||||
/**
|
||||
* 获取预约详情
|
||||
*/
|
||||
getBookingDetail: (id: string) =>
|
||||
request({
|
||||
url: `/integrated/meetingRoom/app/booking-detail/${id}`,
|
||||
method: 'GET',
|
||||
}),
|
||||
|
||||
|
||||
/**
|
||||
* 获取桌子拜访形态
|
||||
*/
|
||||
getMeetingDeskLayout: (id: string) =>
|
||||
request({
|
||||
url: `/integrated/meetingDeskLayout/app/list`,
|
||||
method: 'GET',
|
||||
}),
|
||||
|
||||
/**
|
||||
* 确认结束会议
|
||||
*/
|
||||
confirmEndMeeting: (bookingId: string) =>
|
||||
request({
|
||||
url: `/integrated/meetingBooking/app/confirm-end-meeting/${bookingId}`,
|
||||
method: 'POST',
|
||||
}),
|
||||
|
||||
/**
|
||||
* 取消预约
|
||||
*/
|
||||
cancelBooking: (bookingId: string) =>
|
||||
request({
|
||||
url: `/integrated/meetingRoom/app/cancel-booking/${bookingId}`,
|
||||
method: 'POST',
|
||||
}),
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import request from "@/sheep/utils/request"
|
||||
|
||||
export default {
|
||||
// 公告分页列表
|
||||
getNoticePage: (params: { current: number; size: number }) =>
|
||||
request({
|
||||
url: "/integrated/notice/app/page",
|
||||
method: "GET",
|
||||
params,
|
||||
}),
|
||||
// 公告详情
|
||||
getNoticeDetail: (id: string) =>
|
||||
request({
|
||||
url: `/integrated/notice/app/${id}`,
|
||||
method: "GET",
|
||||
}),
|
||||
// 增加浏览量
|
||||
addNoticeView: (id: string) =>
|
||||
request({
|
||||
url: `/integrated/notice/app/view/${id}`,
|
||||
method: "POST",
|
||||
}),
|
||||
|
||||
// ========== 通知公告相关接口 ==========
|
||||
/**
|
||||
* 获取公告列表
|
||||
*/
|
||||
getNoticeList: (params: { current: number; size: number; type?: string }) =>
|
||||
request({
|
||||
url: "/integrated/notice/list",
|
||||
method: "GET",
|
||||
params,
|
||||
}),
|
||||
|
||||
/**
|
||||
* 获取公告详情
|
||||
*/
|
||||
getNoticeDetailById: (id: string) =>
|
||||
request({
|
||||
url: `/integrated/notice/detail/${id}`,
|
||||
method: "GET",
|
||||
}),
|
||||
|
||||
/**
|
||||
* 标记公告已读
|
||||
*/
|
||||
markNoticeRead: (id: string) =>
|
||||
request({
|
||||
url: `/integrated/notice/read/${id}`,
|
||||
method: "POST",
|
||||
}),
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import request from "@/sheep/utils/request"
|
||||
|
||||
export default {
|
||||
/**
|
||||
* 根据轮播图 id 查询标题和富文本详情
|
||||
*/
|
||||
getDetail: (id: string) =>
|
||||
request({
|
||||
url: `/integrated/sysBanner/detail/${id}`,
|
||||
method: "GET",
|
||||
}),
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
import request from "@/sheep/utils/request"
|
||||
|
||||
export default {
|
||||
/**
|
||||
* 查询任务详情
|
||||
*/
|
||||
queryTask: (taskId: string, view: boolean = false) =>
|
||||
request({
|
||||
url: "/task/task/queryTask",
|
||||
method: "GET",
|
||||
params: { taskId, view },
|
||||
}),
|
||||
|
||||
/**
|
||||
* 格式化开始节点显示
|
||||
*/
|
||||
formatStartNodeShow: (data: {
|
||||
flowId: string;
|
||||
processInstanceId: string;
|
||||
paramMap: any;
|
||||
taskId: string;
|
||||
}) =>
|
||||
request({
|
||||
url: "/task/process-instance/formatStartNodeShow",
|
||||
method: "POST",
|
||||
data,
|
||||
}),
|
||||
|
||||
/**
|
||||
* 完成任务(审批)
|
||||
*/
|
||||
completeTask: (data: {
|
||||
taskId: string;
|
||||
paramMap: any;
|
||||
taskLocalParamMap: {
|
||||
approveDesc: string;
|
||||
};
|
||||
}) =>
|
||||
request({
|
||||
url: "/task/task/completeTask",
|
||||
method: "POST",
|
||||
data,
|
||||
}),
|
||||
|
||||
/**
|
||||
* 转办任务
|
||||
*/
|
||||
delegateTask: (data: {
|
||||
taskId: string;
|
||||
assignee: string;
|
||||
comment?: string;
|
||||
}) =>
|
||||
request({
|
||||
url: "/task/task/delegate",
|
||||
method: "POST",
|
||||
data,
|
||||
}),
|
||||
|
||||
/**
|
||||
* 获取用户选择列表
|
||||
*/
|
||||
getUserSelectionList: () =>
|
||||
request({
|
||||
url: "/task/task/getUserSelectionList",
|
||||
method: "GET",
|
||||
}),
|
||||
|
||||
/**
|
||||
* 获取部门组织架构和用户列表
|
||||
*/
|
||||
getDeptOrgUsers: (parentDeptId: number = 0) =>
|
||||
request({
|
||||
url: "/admin/dept/org",
|
||||
method: "GET",
|
||||
params: {
|
||||
type: "user",
|
||||
parentDeptId,
|
||||
},
|
||||
}),
|
||||
|
||||
/**
|
||||
* 设置转办人员
|
||||
*/
|
||||
setAssignee: (data: {
|
||||
paramMap: any;
|
||||
taskId: string;
|
||||
targetUserIdList: string[];
|
||||
taskLocalParamMap: {
|
||||
approveDesc: string;
|
||||
};
|
||||
}) =>
|
||||
request({
|
||||
url: "/task/task/setAssignee",
|
||||
method: "POST",
|
||||
data,
|
||||
}),
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive } from "vue"
|
||||
|
||||
type Props = {
|
||||
text?: string
|
||||
}
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
text: "暂无内容"
|
||||
})
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="size-full flex-col-center">
|
||||
<image class="w-400rpx h-240rpx" src="/static/empty/empty-1.png" mode="aspectFill" />
|
||||
<view class="sub-text-24 m-t-20rpx">{{ props.text }}</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
@@ -0,0 +1,141 @@
|
||||
<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">
|
||||
<tm-button class="day-time-button" size="m" :color="selectedTimePointIndex == item.index ? '' : 'info'"
|
||||
:disabled="item.disabled" v-for="item in availableTimePoints" :key="item.index"
|
||||
@click="selectTimePoint(item.time, item.index)">
|
||||
{{ item.time.slice(0, 5) }}
|
||||
</tm-button>
|
||||
</view>
|
||||
<view class="px-20rpx py-2 flex justify-between">
|
||||
<tm-button skin="thin" block @click="emit('cancel')">取消</tm-button>
|
||||
|
||||
<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
|
||||
}
|
||||
|
||||
// 判断时间是否已过期
|
||||
const isTimePast = (dateStr: string, timeStr: string): boolean => {
|
||||
try {
|
||||
const now = new Date()
|
||||
const selectedDateTime = new Date(`${dateStr} ${timeStr}`)
|
||||
return selectedDateTime < now
|
||||
} catch (error) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// 生成时间点列表(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 availableTimePoints = computed(() => {
|
||||
return dayTimePointList.value.map((time, index) => ({
|
||||
time,
|
||||
index,
|
||||
disabled: isTimePast(selectedDateValue.value.fullDate, time)
|
||||
}))
|
||||
})
|
||||
|
||||
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) => {
|
||||
// 检查是否已过期
|
||||
if (isTimePast(selectedDateValue.value.fullDate, value)) {
|
||||
uni.showToast({ title: '不能选择过去的时间', icon: 'none' })
|
||||
return
|
||||
}
|
||||
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 16rpx;
|
||||
background-color: #eee;
|
||||
border-radius: 12rpx;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
transition: all 0.1s linear;
|
||||
}
|
||||
|
||||
.date-time-range .active {
|
||||
background: #0779fd;
|
||||
border-color: #4988f2;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.day-time-point-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10rpx;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.day-time-button {
|
||||
flex: 1 0 calc(100vw / 4);
|
||||
}
|
||||
</style>
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 37 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 5.7 KiB |
Binary file not shown.
|
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 |
Binary file not shown.
|
After Width: | Height: | Size: 46 KiB |
Reference in new issue
Block a user