feat:迁移

This commit is contained in:
dzc committed 2026-03-12 14:43:35 +08:00
1 parent 838f05b3c0
commit 36a8665bae
44 files changed
+8043 -29

No files matched your search

-21
View File
@@ -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
View File
@@ -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": {
+304
View File
@@ -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>
+588
View File
@@ -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>
+234
View File
@@ -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>
+848
View File
@@ -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>
+335
View File
@@ -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>
+236
View File
@@ -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>
+229
View File
@@ -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>
+440
View File
@@ -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>
+625
View File
@@ -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>
+110
View File
@@ -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
+133
View File
@@ -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>
+192
View File
@@ -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>
+4 -3
View File
@@ -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>
+1 -1
View File
@@ -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 -1
View File
@@ -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 -1
View File
@@ -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 -1
View File
@@ -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">
+125
View File
@@ -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>
+1 -1
View File
@@ -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">
+51
View File
@@ -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',
}),
}
+9
View File
@@ -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"
+18
View File
@@ -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,
}),
}
+42
View File
@@ -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",
}),
}
+44
View File
@@ -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 },
}),
}
+52
View File
@@ -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',
}),
}
+92
View File
@@ -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',
}),
}
+52
View File
@@ -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",
}),
}
+12
View File
@@ -0,0 +1,12 @@
import request from "@/sheep/utils/request"
export default {
/**
* 根据轮播图 id 查询标题和富文本详情
*/
getDetail: (id: string) =>
request({
url: `/integrated/sysBanner/detail/${id}`,
method: "GET",
}),
}
+97
View File
@@ -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,
}),
}
+21
View File
@@ -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>
+141
View File
@@ -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>
&nbsp;&nbsp;
<tm-button block @click="onConfirm">确定</tm-button>
</view>
</template>
<script setup lang="ts">
import { ref, computed } from "vue"
const emit = defineEmits(["confirm", "cancel"])
// 未来一周的日期
const getLast7DaysWeek = (): { week: string; date: string; fullDate: string }[] => {
const weekMap: string[] = ["周日", "周一", "周二", "周三", "周四", "周五", "周六"]
const result: { week: string; date: string; fullDate: string }[] = []
const today = new Date()
for (let i = 0; i < 7; i++) {
const d = new Date(today)
d.setDate(today.getDate() + i)
const week = weekMap[d.getDay()]
const month = (d.getMonth() + 1).toString().padStart(2, "0")
const day = d.getDate().toString().padStart(2, "0")
const fullDate = `${d.getFullYear()}-${month}-${day}`
result.push({
week,
date: `${month}-${day}`,
fullDate,
})
}
return result
}
// 判断时间是否已过期
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