This commit is contained in:
ctw committed 2025-10-31 16:37:07 +08:00
commit f2d6f0e6ce
6 files changed
+307 -5

No files matched your search

+4 -4
View File
@@ -20,15 +20,15 @@ VITE_OAUTH2_APP_CLIENT='mini:mini'
# 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.174:9999 # 邓-本地
# VITE_APP_DEV_SERVICE = http://127.0.0.1:9999 # 邓-本地
VITE_APP_DEV_SERVICE = http://123.157.250.162:8089/api # 测试线上
VITE_APP_DEV_SERVICE = http://127.0.0.1:9999 # 邓-本地
#VITE_APP_DEV_SERVICE = http://123.157.250.162: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.174:9999 # 邓-本地
# VITE_IMAGE_DEV_PATH = http://127.0.0.1:9999
VITE_IMAGE_DEV_PATH = http://123.157.250.162:8089/api # 测试线上
VITE_IMAGE_DEV_PATH = http://127.0.0.1:9999
#VITE_IMAGE_DEV_PATH = http://123.157.250.162:8089/api # 测试线上
# --------------------------- 生产环境 api地址 ----------------------------------
VITE_APP_PROD_SERVICE = https://stgl.pj.gov.cn/api # 域名线上
+6
View File
@@ -507,6 +507,12 @@
"navigationBarTitleText": "失物招领"
}
},
{
"path": "lostAndFound/detail",
"style": {
"navigationBarTitleText": "失物详情"
}
},
{
"path": "meeting/index",
"style": {
@@ -0,0 +1,277 @@
<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/lostAndFound'
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="less" 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>
@@ -19,6 +19,7 @@
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>
@@ -105,6 +106,16 @@ const onRefresh = async () => {
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')
}
+1 -1
View File
@@ -36,7 +36,7 @@
v-model="uploadFileUrl"
:url="'/api/admin/sys-file/upload'"
:maxCount="5"
:maxFileSize="1024 * 1024"
:maxFileSize="10 * 1024 * 1024"
:beforeComplete="beforeComplete"
:header="header"
/>
+8
View File
@@ -33,4 +33,12 @@ export default {
method: "GET",
params,
}),
// 获取失物招领详情
getDetail: (id: string) =>
request({
url: "integrated/lostAndFound/app/getDetails",
method: "GET",
params: { id },
}),
}