综合服务:会议,失物招领页面

This commit is contained in:
zjf committed 2025-05-22 14:50:23 +08:00
1 parent 199c72740d
commit 021c622bc7
10 files changed
+21866 -8

No files matched your search

+4 -2
View File
@@ -19,13 +19,15 @@ VITE_OAUTH2_APP_CLIENT='mini:mini'
# --------------------------- 开发环境 api地址 ----------------------------------
# VITE_APP_DEV_SERVICE = https://stgl.pj.gov.cn/api # 域名线上
#VITE_APP_DEV_SERVICE = http://59.202.170.152:81/api # 信创线上
VITE_APP_DEV_SERVICE = http://192.168.101.125:9999 # 邓-本地
#VITE_APP_DEV_SERVICE = http://192.168.101.125:9999 # 邓-本地
VITE_APP_DEV_SERVICE = http://127.0.0.1:9999 # 邓-本地
# VITE_APP_DEV_SERVICE = http://123.156.230.15:8089/api # 测试线上
# --------------------------- 开发环境 图片访问基础地址 ---------------------------
# VITE_IMAGE_DEV_PATH = https://stgl.pj.gov.cn/api # 域名线上
#VITE_IMAGE_DEV_PATH = http://59.202.170.152:81/api # 信创线上
VITE_IMAGE_DEV_PATH = http://192.168.101.125:9999 # 邓-本地
; VITE_IMAGE_DEV_PATH = http://192.168.101.125:9999 # 邓-本地
VITE_IMAGE_DEV_PATH = http://127.0.0.1:9999
# VITE_IMAGE_DEV_PATH = http://123.156.230.15:8089/api # 测试线上
# --------------------------- 生产环境 api地址 ----------------------------------
+16
View File
@@ -0,0 +1,16 @@
{ // launch.json 配置了启动调试时相关设置,configurations下节点名称可为 app-plus/h5/mp-weixin/mp-baidu/mp-alipay/mp-qq/mp-toutiao/mp-360/
// launchtype项可配置值为local或remote, local代表前端连本地云函数,remote代表前端连云端云函数
"version": "0.0",
"configurations": [{
"default" :
{
"launchtype" : "local"
},
"mp-weixin" :
{
"launchtype" : "local"
},
"type" : "uniCloud"
}
]
}
+20954
View File
File diff suppressed because it is too large. Load diff
+25 -6
View File
@@ -433,11 +433,29 @@
"navigationBarTitleText": "综合服务"
}
},
{
"path": "lostAndFound/index",
"style": {
"navigationBarTitleText": "失物招领"
}
},
{
"path": "meeting/index",
"style": {
"navigationBarTitleText": "会议预定"
}
},
{
"path": "feedback/index",
"style": {
"navigationBarTitleText": "意见反馈"
}
},
{
"path": "meeting/detail",
"style": {
"navigationBarTitleText": "预约会议"
}
}
]
}
@@ -510,13 +528,14 @@
"text": "",
"iconPath": "",
"selectedIconPath": ""
},
{
"pagePath": "pages/integrated/index",
"text": "综合服务",
"iconPath": "static/tabbar/integrated.png",
"selectedIconPath": "static/tabbar/integrated-select.png"
}
// ,
// {
// "pagePath": "pages/integrated/index",
// "text": "综合服务",
// "iconPath": "static/tabbar/integrated.png",
// "selectedIconPath": "static/tabbar/integrated-select.png"
// }
]
}
}
+69
View File
@@ -0,0 +1,69 @@
<template>
<div class="service-container">
<div class="service-grid">
<div
v-for="item in services"
:key="item.name"
class="service-item"
@click="goPage(item)"
>
<image :src="item.icon" class="service-icon" />
<div class="service-label">{{ item.label }}</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
const services = [
{
name: 'water-query',
label: '会议预定',
icon: '/static/service/water-query.png',
url: '/pages/integrated/meeting/index'
},
{
name: 'lost-and-found',
label: '失物招领',
icon: '/static/service/lost-and-found.png',
url: '/pages/integrated/lostAndFound/index'
}
]
function goPage(item) {
if (item.url) {
uni.navigateTo({ url: item.url })
}
}
</script>
<style scoped lang="less">
.service-container {
padding: 24rpx 0;
background: #fff;
border-radius: 16rpx;
}
.service-grid {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
padding: 0 24rpx;
}
.service-item {
width: 25%;
margin-bottom: 32rpx;
display: flex;
flex-direction: column;
align-items: center;
}
.service-icon {
width: 88rpx;
height: 88rpx;
margin-bottom: 12rpx;
}
.service-label {
font-size: 26rpx;
color: #333;
text-align: center;
}
</style>
+233
View File
@@ -0,0 +1,233 @@
<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"
>
<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/lostAndFound'
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 formatDate = (date: string) => {
return dayjs(date).format('YYYY-MM-DD')
}
const goBack = () => {
uni.navigateBack()
}
onMounted(() => {
fetchData()
})
</script>
<style lang="less" 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>
+292
View File
@@ -0,0 +1,292 @@
<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="room.img" class="room-img" mode="aspectFill"></image>
<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="activeDate = i">{{ d }}</view>
</view>
<!-- 时间选择表格 -->
<view class="time-table">
<view class="time-row" v-for="(row, rowIdx) in timeTable" :key="rowIdx">
<view class="time-cell" v-for="(cell, colIdx) in row" :key="colIdx"
:class="cell.status"
@tap="selectTime(rowIdx, colIdx)"
>
<text v-if="cell.status === 'booked'">{{ cell.user }}</text>
<text v-else-if="cell.status === 'disabled'">🚫</text>
</view>
</view>
</view>
<view class="time-labels">
<text v-for="h in 10" :key="h" class="time-label">{{ 8 + h }}:00</text>
</view>
<!-- 预约信息 -->
<view class="reserve-info">
<view>会议时间:{{ selectedTimeText }}</view>
<view>时长:{{ selectedDuration }}分钟</view>
<view>发起人:张三</view>
</view>
<!-- 预约按钮 -->
<view class="reserve-btn" @tap="reserveMeeting">发起会议</view>
</view>
</template>
<script lang="ts" setup>
import { ref, computed } from 'vue'
const goBack = () => {
uni.navigateBack()
}
// 假数据,可替换为实际接口
const room = ref({
name: '一号会议室',
capacity: 20,
address: '公司总部南水吧台旁',
tags: ['电视', '白板', '投影仪'],
img: '/static/meeting/room1.jpg',
})
const dateTabs = ['今天', '明天', '后天', '选择日期']
const activeDate = ref(0)
// 时间表格(4行10列,每格15分钟)
const timeTable = ref([
[
{ status: 'disabled' }, { status: 'disabled' }, { status: 'booked', user: '张三' }, { status: 'booked', user: '张三' }, { status: 'free' }, { status: 'free' }, { status: 'free' }, { status: 'free' }, { status: 'free' }, { status: 'free' }
],
[
{ status: 'free' }, { status: 'free' }, { status: 'free' }, { status: 'free' }, { status: 'booked', user: '张三' }, { status: 'booked', user: '张三' }, { status: 'free' }, { status: 'free' }, { status: 'free' }, { status: 'free' }
],
[
{ status: 'free' }, { status: 'free' }, { status: 'free' }, { status: 'free' }, { status: 'free' }, { status: 'free' }, { status: 'free' }, { status: 'free' }, { status: 'free' }, { status: 'free' }
],
[
{ status: 'free' }, { status: 'free' }, { status: 'free' }, { status: 'free' }, { status: 'free' }, { status: 'free' }, { status: 'free' }, { status: 'free' }, { status: 'free' }, { status: 'free' }
]
])
const selectedTime = ref<{row: number, col: number} | null>(null)
const selectedTimeText = computed(() => {
if (!selectedTime.value) return '未选择'
const start = 8 + selectedTime.value.col * 0.5
const end = start + 0.5
return `${Math.floor(start)}:${start % 1 === 0 ? '00' : '30'}~${Math.floor(end)}:${end % 1 === 0 ? '00' : '30'}`
})
const selectedDuration = computed(() => 30)
function selectTime(row: number, col: number) {
const cell = timeTable.value[row][col]
if (cell.status === 'free') {
selectedTime.value = { row, col }
}
}
function reserveMeeting() {
if (!selectedTime.value) {
uni.showToast({ title: '请选择时间', icon: 'none' })
return
}
uni.showToast({ title: '预约成功', icon: 'success' })
}
</script>
<style lang="less" scoped>
.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 {
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-table {
margin: 24rpx;
background: #fff;
border-radius: 12rpx;
padding: 16rpx;
}
.time-row {
display: flex;
margin-bottom: 8rpx;
}
.time-cell {
flex: 1;
height: 48rpx;
margin-right: 4rpx;
border-radius: 6rpx;
background: #e6eaf2;
display: flex;
align-items: center;
justify-content: center;
color: #bbb;
font-size: 22rpx;
}
.time-cell:last-child {
margin-right: 0;
}
.time-cell.free {
background: #4d8fff;
color: #fff;
}
.time-cell.booked {
background: #ffb74d;
color: #fff;
}
.time-cell.disabled {
background: #f0f0f0;
color: #ccc;
}
.time-cell.selected {
border: 2rpx solid #1777FF;
}
.time-labels {
display: flex;
margin: 0 24rpx 16rpx 24rpx;
}
.time-label {
flex: 1;
text-align: center;
color: #bbb;
font-size: 20rpx;
}
.reserve-info {
margin: 0 24rpx 16rpx 24rpx;
background: #fff;
border-radius: 12rpx;
padding: 16rpx;
color: #333;
font-size: 26rpx;
}
.reserve-btn {
margin: 0 24rpx;
background: linear-gradient(90deg, #0094ff, #06be36);
color: #fff;
font-size: 30rpx;
text-align: center;
border-radius: 40rpx;
padding: 24rpx 0;
font-weight: 600;
margin-top: 16rpx;
}
</style>
+224
View File
@@ -0,0 +1,224 @@
<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="date-bar">
<text class="date-text">{{ todayText }}</text>
<text class="date-arrow">▼</text>
</view>
<!-- 会议室列表 -->
<view v-for="room in meetingRooms" :key="room.id" class="room-card" @tap="goDetail(room)">
<view class="room-info">
<view class="room-main">
<view class="room-title">
{{ room.roomName }} <text class="room-capacity">{{ room.capacity }}人</text>
</view>
<view class="room-address">
<text class="iconfont">📍</text> {{ room.location || '未知地址' }}
</view>
<view class="room-tags">
<text v-for="eq in room.equipmentNames" :key="eq.name" class="room-tag">
{{ eq.name }}<span v-if="eq.quantity"> x{{ eq.quantity }}</span>
</text>
</view>
</view>
<image :src="room.picUrl || '/static/meeting/default.jpg'" class="room-img" mode="aspectFill"></image>
</view>
<!-- 时间条 -->
<view class="timeline-bar">
<view v-for="(slot, idx) in room.timeline" :key="idx" :class="['timeline-slot', slot]" />
</view>
<view class="timeline-labels">
<text v-for="h in 17" :key="h" class="timeline-label">{{ h + 7 }}</text>
</view>
</view>
</view>
</template>
<script lang="ts" setup>
import { ref, onMounted } from 'vue'
import meetingRoomApi from '@/sheep/api/meetingRoom'
const goBack = () => {
uni.navigateBack()
}
const todayText = ref('')
const meetingRooms = ref<any[]>([])
onMounted(() => {
todayText.value = getTodayText()
fetchMeetingRooms()
})
function getTodayText() {
const d = new Date()
const week = ['日','一','二','三','四','五','六']
return `${d.getMonth()+1}月${d.getDate()}日 周${week[d.getDay()]}`
}
function fetchMeetingRooms() {
meetingRoomApi.getMeetingRoomList({ current: 1, size: 10 }).then((res: any) => {
if (res.code === 0) {
meetingRooms.value = res.data.records
}
})
}
function goDetail(room) {
uni.navigateTo({
url: `/pages/integrated/meeting/detail?roomId=${room.id}`
})
}
</script>
<style lang="less" scoped>
.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 {
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;
}
.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;
}
</style>
+36
View File
@@ -0,0 +1,36 @@
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,
}),
}
+13
View File
@@ -0,0 +1,13 @@
import request from "@/sheep/utils/request"
export default {
/**
* 会议室分页列表
*/
getMeetingRoomList: (params: { current: number; size: number }) =>
request({
url: "/integrated/meetingRoom/app/page",
method: "GET",
params,
}),
}