Browse Source

251008

master
梁欣 6 days ago
parent
commit
45468fd8e0
  1. 6
      api/common.js
  2. 11
      api/service.js
  3. 4
      config/index.js
  4. 88
      pages.json
  5. 140
      pages/ai/index.vue
  6. 2
      pages/index/index.scss
  7. 45
      pages/index/index.vue
  8. 2
      pages/service/index.scss
  9. 7
      pages/service/index.vue
  10. 412
      servicePages/pages/detail.vue
  11. 464
      servicePages/pages/index.vue
  12. 294
      servicePages/pages/patrol.vue
  13. 37
      userPages/pages/adList.vue

6
api/common.js

@ -35,3 +35,9 @@ export const articleInfoReq = id => get(`/cms/cms-article/${id}`)
export const articleCateReq = params => get('/cms/article-category', {params})
export const openDoorReq = serial => get(`/zhsq/zhsq-access-device/openDoor/${serial}`)
export const addPatrolRecordReq = data => post(`/zhsq/zhsq-service-patrol-point-question-record`, data)
export const getPointInfoReq = id => get(`/zhsq/zhsq-service-point/${id}`)
export const adListReq = () => get(`/cms/cms-ad`)

11
api/service.js

@ -0,0 +1,11 @@
import post, { get } from "@/api/request";
/**
* 服务中心相关API
*/
// 获取服务列表
export const getServiceListReq = params => get('/shop/category', { params })
// 获取服务详情
export const getServiceDetailReq = id => get(`/shop/category/${id}`)

4
config/index.js

@ -1,7 +1,7 @@
const MAIN_API_FN = () => {
if (process.env.NODE_ENV === 'development') {
return 'http://127.0.0.1:9011'
// return 'https://xq-api.ggsxiangan.com'
// return 'http://127.0.0.1:9011'
return 'https://xq-api.ggsxiangan.com'
} else {
return 'https://xq-api.ggsxiangan.com'
}

88
pages.json

@ -151,12 +151,34 @@
"style": {
"navigationBarTitleText": "远程开门(访客)"
}
},
{
"path": "pages/adList",
"style": {
"navigationBarTitleText": "广告列表"
}
}
]
},
{
"root": "servicePages",
"pages": [
{
"path": "pages/index",
"style": {
"navigationBarTitleText": "服务中心",
"navigationBarBackgroundColor": "#87CEEB",
"navigationBarTextStyle": "white"
}
},
{
"path": "pages/detail",
"style": {
"navigationBarTitleText": "服务详情",
"navigationBarBackgroundColor": "#ffffff",
"navigationBarTextStyle": "black"
}
},
{
"path": "pages/pay",
"style": {
@ -193,36 +215,42 @@
"navigationBarTitleText": "常用工具"
}
},
{
"path": "pages/survey",
"style": {
"navigationBarTitleText": "问卷调查"
}
},
{
"path": "pages/surveyInfo",
"style": {
"navigationBarTitleText": "问卷详情"
}
},
{
"path": "pages/invite",
"style": {
"navigationBarTitleText": "访客邀请"
}
},
{
"path": "pages/suggest",
"style": {
"navigationBarTitleText": "投诉建议"
}
},
{
"path": "pages/fix",
"style": {
"navigationBarTitleText": "报修"
}
}
{
"path": "pages/survey",
"style": {
"navigationBarTitleText": "问卷调查"
}
},
{
"path": "pages/surveyInfo",
"style": {
"navigationBarTitleText": "问卷详情"
}
},
{
"path": "pages/invite",
"style": {
"navigationBarTitleText": "访客邀请"
}
},
{
"path": "pages/suggest",
"style": {
"navigationBarTitleText": "投诉建议"
}
},
{
"path": "pages/fix",
"style": {
"navigationBarTitleText": "报修"
}
},
{
"path": "pages/patrol",
"style": {
"navigationBarTitleText": "巡更"
}
}
]
}
],

140
pages/ai/index.vue

@ -66,6 +66,7 @@
</view>
</view>
</view>
<Login ref="Login" @done="getUserData"/>
</view>
</template>
@ -79,10 +80,12 @@ import {dictDataReq} from "@/api/common";
import {userRoomListReq} from "@/api/room";
import {listBillReq} from "@/api/bill";
import BillItem from "@/pages/ai/components/BillItem.vue";
import Login from "@/components/Login.vue";
import {userInfoReq} from "@/api/user";
export default {
name: "index",
components: {BillItem, HeaderItem},
components: {Login, BillItem, HeaderItem},
data() {
return {
list: [],
@ -90,7 +93,9 @@ export default {
content: '',
taskId: null,
roomList: null,
userData: null,
answering: false,
isLogin: false,
billList: [],
}
},
@ -102,6 +107,12 @@ export default {
const {data} = await dictDataReq({dictId: '165'})
this.questionList = data
},
async getUserData() {
const {data} = await userInfoReq()
this.userData = data
this.isLogin = true
this.ws()
},
async getWelcomeWord() {
const {data} = await dictDataReq({dictId: '1435'})
this.list.push({
@ -114,6 +125,7 @@ export default {
console.log(this.list)
},
async send(content = null) {
if (!this.isLogin) return this.$refs.Login.open()
const contentItem = content || this.content
// console.log(contentItem, this.content)
if (!contentItem.trim()) return this.$toast('请输入提问内容')
@ -136,7 +148,7 @@ export default {
this.taskId = null
sendChatReq({
query: contentItem,
user: getUserInfo().uid
user: this.userData.userId,
}).then(() => {
this.list[this.list.length - 1].done = true
this.answering = false
@ -158,7 +170,9 @@ export default {
taskId: this.taskId
})
this.answering = false
uni.closeSocket()
this.list.splice(this.list.length - 1, 1)
// uni.closeSocket()
},
async getRoomList() {
const {data} = await userRoomListReq()
@ -204,70 +218,78 @@ export default {
console.error('获取账单数据失败:', error);
}
},
ws() {
uni.connectSocket({
url: `${WS_API}/${this.userData.userId}`
})
uni.onSocketMessage(async res => {
// console.log(res.data)
if (res.data.includes('连接成功')) return
const aiData = JSON.parse(res.data)
this.list[this.list.length - 1].loading = false
if (aiData.answer !== '__END__' && !this.list[this.list.length - 1].isMine) {
this.list[this.list.length - 1].type = 'text'
this.list[this.list.length - 1].content += aiData.answer
} else {
this.list[this.list.length - 1].done = true
const lastContent = this.list[this.list.length - 1].content
console.log('check url', lastContent, lastContent.includes(`<openUrl url="/pages/user/property-bill">`))
if (lastContent.includes(`pages/user/property-bill`)) {
await this.getRoomList()
this.list.push({
content: '',
isMine: false,
loading: false,
done: true,
type: 'bill'
})
} else if (lastContent.includes(`servicePages/pages/suggest`) || lastContent.includes(`servicePages/pages/suuggest`)) {
this.list.push({
content: '点击跳转',
isMine: false,
loading: false,
done: true,
type: 'link',
url: '/servicePages/pages/suggest'
})
} else if (lastContent.includes(`servicePages/pages/survey`)) {
this.list.push({
content: '点击跳转',
isMine: false,
loading: false,
done: true,
type: 'link',
url: '/servicePages/pages/survey'
})
} else if (lastContent.includes(`servicePages/pages/fix`)) {
this.list.push({
content: '点击跳转',
isMine: false,
loading: false,
done: true,
type: 'link',
url: '/servicePages/pages/fix'
})
}
}
this.taskId = aiData.taskId
this.scrollToBottom()
// console.log(this.list)
})
}
},
onLoad() {
uni.connectSocket({
url: `${WS_API}/aiUser`
})
uni.onSocketMessage(async res => {
if (res.data === '连接成功') return
const aiData = JSON.parse(res.data)
this.list[this.list.length - 1].loading = false
if (aiData.answer !== '__END__') {
this.list[this.list.length - 1].type = 'text'
this.list[this.list.length - 1].content += aiData.answer
} else {
const lastContent = this.list[this.list.length - 1].content
console.log('check url', lastContent, lastContent.includes(`<openUrl url="/pages/user/property-bill">`))
if (lastContent.includes(`<openUrl url="/pages/user/property-bill">`)) {
await this.getRoomList()
this.list.push({
content: '',
isMine: false,
loading: false,
done: true,
type: 'bill'
})
} else if (lastContent.includes(`servicePages/pages/suggest`) || lastContent.includes(`<openUrl url="/servicePages/pages/suuggest">`)) {
this.list.push({
content: '点击跳转',
isMine: false,
loading: false,
done: true,
type: 'link',
url: '/servicePages/pages/suggest'
})
} else if (lastContent.includes(`servicePages/pages/survey`)) {
this.list.push({
content: '点击跳转',
isMine: false,
loading: false,
done: true,
type: 'link',
url: '/servicePages/pages/survey'
})
} else if (lastContent.includes(`<openUrl url="/servicePages/pages/fix">`)) {
this.list.push({
content: '点击跳转',
isMine: false,
loading: false,
done: true,
type: 'link',
url: '/servicePages/pages/fix'
})
}
}
this.taskId = aiData.taskId
this.scrollToBottom()
console.log(this.list)
})
this.getWelcomeWord()
},
onShow() {
if (getUserInfo().token) this.getUserData()
},
onUnload() {
uni.closeSocket()
this.stopAI()
},
onHide() {
uni.closeSocket()
this.stopAI()
},
}

2
pages/index/index.scss

@ -106,7 +106,7 @@
margin: 30rpx 0;
border-radius: 20rpx;
overflow: hidden;
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.08);
//box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.08);
.ad-img {
width: 100%;

45
pages/index/index.vue

@ -8,15 +8,15 @@
</view>
</view>
<u-gap :height="(navBarPaddingTop + navBarHeight) + 'px'"></u-gap>
<!-- <view class="pos-r">-->
<image src="https://img.ggsxiangan.com/top-banner.png" mode="widthFix" class="w-100p"/>
<!-- &lt;!&ndash; 搜索栏 &ndash;&gt;-->
<!-- <view class="search-bar">-->
<!-- <view class="m-20 w-100p">-->
<!-- <u-search @click="handleSearchClick" :showAction="false" placeholder="搜索关键字" disabled/>-->
<!-- </view>-->
<!-- </view>-->
<!-- </view>-->
<!-- <view class="pos-r">-->
<image src="https://img.ggsxiangan.com/top-banner.png" mode="widthFix" class="w-100p"/>
<!-- &lt;!&ndash; 搜索栏 &ndash;&gt;-->
<!-- <view class="search-bar">-->
<!-- <view class="m-20 w-100p">-->
<!-- <u-search @click="handleSearchClick" :showAction="false" placeholder="搜索关键字" disabled/>-->
<!-- </view>-->
<!-- </view>-->
<!-- </view>-->
<!-- 主内容区域 -->
<u-gap height="200rpx"></u-gap>
<view class="main-content">
@ -43,7 +43,7 @@
style="width: 80rpx; height: 80rpx"/>
<text class="text-24 mt-15">远程开门</text>
</view>
<view class="icon-item" @click="navToWithLoginAuth('/pages/user/property-bill')">
<view class="icon-item" @click="$jump('/servicePages/pages/pay')">
<image src="https://oss.wsdns.cn/20250626/21ba1bb210f945ffa7466c93ded9073b.png"
style="width: 80rpx; height: 80rpx"/>
<text class="text-24 mt-15">生活缴费</text>
@ -51,7 +51,18 @@
</view>
<!-- 广告图 -->
<view class="ad-section">
<image src="https://oss.wsdns.cn/20250626/5cf1908340ca4904bacc31ed1ad76558.png" class="ad-img"/>
<swiper class="swiper-container" indicator-dots indicator-color="#999" indicator-active-color="#333" autoplay
interval="3000" duration="500" style="height: 400rpx">
<swiper-item v-for="(item, index) in adList" :key="index">
<video :src="item.imageList[0].url" v-if="item.type === 3" :poster="item.cover"
style="width: 100%; height: 400rpx" show-play-btn show-fullscreen-btn/>
<image :src="item.imageList[0].url" v-else-if="item.type === 2" class="swiper-img"/>
</swiper-item>
</swiper>
<view class="flex justify-end items-center text-24 mb-15 text-gray" @click="$jump('/userPages/pages/adList')">
查看更多
</view>
<!-- <image src="https://oss.wsdns.cn/20250626/5cf1908340ca4904bacc31ed1ad76558.png" class="ad-img"/>-->
</view>
<!-- 轮播图 -->
<swiper class="swiper-container" indicator-dots indicator-color="#999" indicator-active-color="#333" autoplay
@ -99,7 +110,7 @@
<image style="width: 160rpx; height: 160rpx;"
src="https://img.ggsxiangan.com/ai.png"/>
</view>
<custom-tabbar :current="0"/>
<custom-tabbar :current="0"/>
<Login ref="Login" @done="getUserData"/>
</view>
</template>
@ -109,7 +120,7 @@ import Login from "@/components/Login.vue";
import {userInfoReq} from "@/api/user";
import {getUserInfo} from "@/util/user";
import {userRoomListReq} from "@/api/room";
import {dictDataInfoReq, openDoorReq, pageArticleReq} from "@/api/common";
import {adListReq, dictDataInfoReq, openDoorReq, pageArticleReq} from "@/api/common";
import dayjs from "dayjs";
import CustomTabbar from "@/components/customTabbar.vue";
@ -126,6 +137,7 @@ export default {
villageList: [],
currentVillage: null,
articleList: [],
adList: [],
isAuditing: true
}
},
@ -138,6 +150,10 @@ export default {
})
this.articleList = data.list
},
async getAdList() {
const {data} = await adListReq()
this.adList = data
},
async getRoomList() {
this.villageList = []
const {data} = await userRoomListReq()
@ -197,7 +213,7 @@ export default {
async checkAudit() {
const {data} = await dictDataInfoReq(3678)
this.isAuditing = data.dictDataCode === '1'
}
},
},
onLoad() {
const systemInfo = uni.getSystemInfoSync()
@ -210,6 +226,7 @@ export default {
onShow() {
if (getUserInfo().token) this.getUserData()
this.checkAudit()
this.getAdList()
},
onShareAppMessage() {
let path = `/pages/index/index`

2
pages/service/index.scss

@ -34,7 +34,7 @@
display: flex;
flex-direction: column;
align-items: center;
padding: 20rpx;
padding: 10rpx;
border-radius: 16rpx;
transition: all 0.3s ease;
cursor: pointer;

7
pages/service/index.vue

@ -5,7 +5,7 @@
<view class="section-header">
<text class="section-title">物业管家</text>
</view>
<view class="service-grid">
<view class="service-grid" style="grid-template-columns: repeat(5, 1fr);">
<view
v-for="(service, index) in propertyServices"
:key="index"
@ -56,8 +56,9 @@ export default {
propertyServices: [
{id: 1, name: '物业缴费', icon: '/static/物业费.png', path: '/pages/user/property-bill'},
{id: 2, name: '远程开门', icon: '/static/一键开门.png', path: '/userPages/pages/remote-door'},
{id: 3, name: '报事报修', icon: '/static/报修管理备份2x.png', path: '/servicePages/pages/fix'},
{id: 4, name: '投诉建议', icon: '/static/物业费.png', path: '/servicePages/pages/suggest'}
{id: 3, name: '业主服务', icon: 'https://img.ggsxiangan.com/业主关怀.png', path: '/servicePages/pages/index'},
{id: 4, name: '报事报修', icon: '/static/报修管理备份2x.png', path: '/servicePages/pages/fix'},
{id: 5, name: '投诉建议', icon: '/static/物业费.png', path: '/servicePages/pages/suggest'}
],
lifeServices: [
{name: '生活缴费', icon: '/static/20-缴费记录.png', path: '/servicePages/pages/pay'},

412
servicePages/pages/detail.vue

@ -0,0 +1,412 @@
<template>
<view class="service-detail-page">
<!-- 顶部轮播图 -->
<view class="swiper-section">
<swiper class="swiper" :indicator-dots="true" :autoplay="true" :interval="3000" :duration="500" circular>
<swiper-item v-for="(image, index) in swiperImages" :key="index">
<image class="swiper-image" :src="image" mode="aspectFill"></image>
</swiper-item>
</swiper>
</view>
<!-- 服务信息 -->
<view class="service-info-section">
<view class="service-title">{{ serviceInfo.name || serviceInfo.title }}</view>
<view class="service-subtitle" v-if="serviceInfo.comments">{{ serviceInfo.comments }}</view>
<!-- 价格信息 -->
<view class="price-section">
<view class="price-box">
<text class="price-symbol">¥</text>
<text class="price-value">{{ serviceInfo.amount || '0' }}</text>
<text class="price-unit"></text>
</view>
<view class="original-price" v-if="serviceInfo.originAmount">
<text class="original-price-text">原价: ¥{{ serviceInfo.originAmount }}</text>
</view>
</view>
</view>
<!-- 服务详情 -->
<view class="detail-section">
<view class="section-title">
<view class="title-line"></view>
<text class="title-text">服务详情</text>
<view class="title-line"></view>
</view>
<view class="detail-content">
<u-parse v-if="serviceInfo.content" :content="serviceInfo.content"/>
<view v-else class="default-content">
<view class="content-item">
<text class="item-label">服务名称</text>
<text class="item-value">{{ serviceInfo.title }}</text>
</view>
<view class="content-item" v-if="serviceInfo.comments">
<text class="item-label">服务说明</text>
<text class="item-value">{{ serviceInfo.comments }}</text>
</view>
<view class="content-item">
<text class="item-label">服务价格</text>
<text class="item-value price-text">¥{{ serviceInfo.amount || '0' }}</text>
</view>
<view class="content-item" v-if="serviceInfo.description">
<text class="item-label">详细描述</text>
<text class="item-value">{{ serviceInfo.description }}</text>
</view>
</view>
</view>
</view>
<!-- 服务须知 -->
<!-- <view class="notice-section">-->
<!-- <view class="section-title">-->
<!-- <view class="title-line"></view>-->
<!-- <text class="title-text">服务须知</text>-->
<!-- <view class="title-line"></view>-->
<!-- </view>-->
<!-- <view class="notice-content">-->
<!-- <view class="notice-item" v-for="(notice, index) in noticeList" :key="index">-->
<!-- <text class="notice-dot"></text>-->
<!-- <text class="notice-text">{{ notice }}</text>-->
<!-- </view>-->
<!-- </view>-->
<!-- </view>-->
<!-- 底部操作栏 -->
<view class="bottom-bar">
<button class="share-btn" open-type="contact">
<u-icon name="chat" size="40rpx" color="#666" label="客服" label-pos="bottom"/>
</button>
<view class="action-buttons">
<button class="order-btn" @click="handleOrder">立即预约</button>
</view>
</view>
<u-popup :show="showPopup" close-on-click-overlay round="10" mode="bottom">
<view class="p-30">
<view class="flex justify-between items-center">
<view class="flex justify-start items-start">
<u-image :src="serviceInfo.image" radius="10" width="100rpx" height="100rpx"/>
<view class="flex flex-col justify-start items-start text-25 ml-15">
<text class="font-bold">{{ serviceInfo.title }}</text>
<view class="flex justify-start items-center mt-20">
</view>
</view>
</view>
</view>
</view>
</u-popup>
</view>
</template>
<script>
import { getServiceDetailReq } from "@/api/service";
import { addViewNumReq } from "@/api/common";
export default {
name: "ServiceDetail",
data() {
return {
serviceId: null,
serviceInfo: {},
isCollected: false,
showPopup: false,
serviceTags: ['专业服务', '快速响应', '品质保证'],
noticeList: [],
specList: [],
spec: [],
storeList: []
}
},
computed: {
swiperImages() {
// 使使
if (this.serviceInfo.images && this.serviceInfo.images.length > 0) {
return this.serviceInfo.images;
} else if (this.serviceInfo.image) {
return [this.serviceInfo.image];
}
//
return [];
}
},
methods: {
async getServiceDetail() {
try {
uni.showLoading({ title: '加载中...' });
const { data } = await getServiceDetailReq(this.serviceId);
this.serviceInfo = data;
this.specList = JSON.parse(data.spec);
this.storeList = JSON.parse(data.serviceStoreList);
this.spec = this.specList[0];
} catch (error) {
console.error('获取服务详情失败:', error);
uni.showToast({
title: '加载失败',
icon: 'none'
});
} finally {
uni.hideLoading();
}
},
handleOrder() {
// TODO:
uni.showToast({
title: '预约功能开发中',
icon: 'none'
});
}
},
onLoad(options) {
if (options.id) {
this.serviceId = options.id;
this.getServiceDetail();
} else {
uni.showToast({
title: '参数错误',
icon: 'none'
});
setTimeout(() => {
uni.navigateBack();
}, 1500);
}
},
}
</script>
<style lang="scss" scoped>
.service-detail-page {
min-height: 100vh;
background-color: #f5f5f5;
padding-bottom: 120rpx;
}
.swiper-section {
width: 100%;
height: 500rpx;
background-color: #fff;
.swiper {
width: 100%;
height: 100%;
.swiper-image {
width: 100%;
height: 100%;
}
}
}
.service-info-section {
background-color: #fff;
padding: 30rpx;
margin-bottom: 20rpx;
.service-title {
font-size: 36rpx;
font-weight: bold;
color: #333;
margin-bottom: 16rpx;
}
.service-subtitle {
font-size: 28rpx;
color: #666;
margin-bottom: 24rpx;
line-height: 1.6;
}
.price-section {
display: flex;
align-items: center;
margin-bottom: 24rpx;
.price-box {
display: flex;
align-items: baseline;
margin-right: 20rpx;
.price-symbol {
font-size: 28rpx;
color: #FF6B6B;
font-weight: bold;
}
.price-value {
font-size: 48rpx;
color: #FF6B6B;
font-weight: bold;
margin: 0 4rpx;
}
.price-unit {
font-size: 24rpx;
color: #FF6B6B;
}
}
.original-price {
.original-price-text {
font-size: 24rpx;
color: #999;
text-decoration: line-through;
}
}
}
.tags-section {
display: flex;
flex-wrap: wrap;
gap: 16rpx;
.tag-item {
padding: 8rpx 20rpx;
background: linear-gradient(135deg, #FFE5E5 0%, #FFF0F0 100%);
border-radius: 30rpx;
font-size: 24rpx;
color: #FF6B6B;
}
}
}
.detail-section,
.notice-section {
background-color: #fff;
padding: 30rpx;
margin-bottom: 20rpx;
}
.section-title {
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 30rpx;
.title-line {
flex: 1;
height: 2rpx;
background: linear-gradient(90deg, transparent 0%, #ddd 50%, transparent 100%);
}
.title-text {
font-size: 32rpx;
font-weight: bold;
color: #333;
padding: 0 30rpx;
}
}
.detail-content {
.default-content {
.content-item {
margin-bottom: 24rpx;
line-height: 1.8;
.item-label {
font-size: 28rpx;
color: #666;
font-weight: 500;
}
.item-value {
font-size: 28rpx;
color: #333;
&.price-text {
color: #FF6B6B;
font-weight: bold;
}
}
}
}
}
.notice-content {
.notice-item {
display: flex;
margin-bottom: 20rpx;
.notice-dot {
font-size: 28rpx;
color: #FF6B6B;
margin-right: 12rpx;
}
.notice-text {
flex: 1;
font-size: 26rpx;
color: #666;
line-height: 1.8;
}
}
}
.bottom-bar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background-color: #fff;
padding: 20rpx 30rpx;
box-shadow: 0 -4rpx 20rpx rgba(0, 0, 0, 0.08);
display: flex;
align-items: center;
justify-content: space-between;
z-index: 999;
.contact-btn {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 0 20rpx;
.btn-text {
font-size: 22rpx;
color: #666;
margin-top: 8rpx;
}
}
.action-buttons {
flex: 1;
display: flex;
gap: 20rpx;
margin-left: 20rpx;
.collect-btn {
flex: 1;
height: 80rpx;
background-color: #FFF5E6;
border: 2rpx solid #FFD700;
border-radius: 40rpx;
display: flex;
align-items: center;
justify-content: center;
gap: 8rpx;
.btn-text {
font-size: 28rpx;
color: #666;
}
}
.order-btn {
flex: 2;
height: 80rpx;
background: linear-gradient(135deg, #FF6B6B 0%, #FF8E53 100%);
border-radius: 40rpx;
color: #fff;
font-size: 32rpx;
font-weight: bold;
border: none;
display: flex;
align-items: center;
justify-content: center;
}
}
}
</style>

464
servicePages/pages/index.vue

@ -0,0 +1,464 @@
<template>
<view class="service-center-page">
<!-- 顶部区域 -->
<view class="header-section">
<!-- 位置和搜索 -->
<view class="top-bar">
<u-search v-model="keyword" shape="round"/>
</view>
<!-- 标题和城市背景 -->
</view>
<u-image src="https://img.ggsxiangan.com/v2_sxaf4p.png" width="100%"/>
<!-- 服务图标网格 -->
<view class="service-grid-section">
<view class="service-grid">
<view
class="service-item"
v-for="(item, index) in serviceList.filter(i => i.parentId === 0)"
:key="index"
@click="handleServiceClick(item)"
>
<view class="service-icon-box">
<u-icon :name="item.image" width="80rpx" height="80rpx"/>
</view>
<text class="service-name">{{ item.title }}</text>
</view>
</view>
</view>
<!-- 分类标签 -->
<view class="category-tabs">
<view
class="tab-item"
:class="{ active: currentTab === index }"
v-for="(tab, index) in tabs"
:key="index"
@click="changeTab(index)"
>
<text class="tab-text">{{ tab.title }}</text>
<view class="tab-underline" v-if="currentTab === index"></view>
</view>
</view>
<!-- 商品列表 -->
<view class="goods-list">
<view
class="goods-item"
v-for="(item, index) in goodsList"
:key="index"
@click="handleGoodsClick(item)"
>
<image class="goods-image" :src="item.image" mode="aspectFill"></image>
<view class="goods-info">
<text class="goods-name">{{ item.name }}</text>
<text class="goods-desc">{{ item.comments }}</text>
<view class="goods-price-box">
<text class="price-symbol">¥</text>
<text class="price-value">{{ item.amount }}</text>
<text class="price-original" v-if="item.originAmount">¥{{ item.originAmount }}</text>
</view>
</view>
</view>
</view>
</view>
</template>
<script>
import {getServiceListReq} from "@/api/service";
export default {
name: "ServiceIndex",
data() {
return {
currentTab: 0,
keyword: '',
tabs: [],
serviceList: [],
goodsList: [],
}
},
methods: {
changeTab(index) {
this.currentTab = index
},
handleServiceClick(item) {
if (item.path) {
uni.navigateTo({url: item.path})
} else {
uni.showToast({
title: '功能开发中',
icon: 'none'
})
}
},
handleGoodsClick(item) {
//
uni.navigateTo({
url: `/servicePages/pages/detail?id=${item.categoryId}`
})
},
async getCateList(){
const {data} = await getServiceListReq()
this.serviceList = data
this.tabs = data.filter(i => i.parentId === 0)
this.goodsList = data.filter(i => i.parentId === this.tabs[0].categoryId)
}
},
onLoad() {
//
this.getCateList()
}
}
</script>
<style lang="scss" scoped>
.service-center-page {
min-height: 100vh;
background-color: #f5f5f5;
}
.header-section {
background: linear-gradient(180deg, #87CEEB 0%, #B0E0E6 100%);
padding-bottom: 20rpx;
position: relative;
overflow: hidden;
}
.top-bar {
display: flex;
align-items: center;
padding: 20rpx 30rpx;
padding-top: 40rpx;
.location {
display: flex;
align-items: center;
margin-right: 20rpx;
.location-text {
color: #fff;
font-size: 28rpx;
margin-left: 8rpx;
}
}
.search-box {
flex: 1;
background-color: rgba(255, 255, 255, 0.9);
border-radius: 40rpx;
padding: 12rpx 24rpx;
display: flex;
align-items: center;
margin: 0 20rpx;
.search-text {
color: #999;
font-size: 26rpx;
margin-left: 12rpx;
}
}
.menu-icon, .scan-icon {
margin-left: 20rpx;
}
}
.title-section {
padding: 40rpx 30rpx 20rpx;
position: relative;
.title-text {
font-size: 60rpx;
font-weight: bold;
color: #2E8B57;
text-align: center;
letter-spacing: 4rpx;
text-shadow: 2rpx 2rpx 4rpx rgba(0, 0, 0, 0.1);
}
.subtitle-text {
font-size: 36rpx;
color: #4682B4;
text-align: center;
margin-top: 10rpx;
letter-spacing: 2rpx;
}
.city-bg {
width: 100%;
height: 200rpx;
margin-top: 30rpx;
position: relative;
display: flex;
align-items: flex-end;
justify-content: space-around;
padding: 0 20rpx;
.building {
background: linear-gradient(180deg, rgba(255, 255, 255, 0.3) 0%, rgba(255, 255, 255, 0.5) 100%);
border-radius: 8rpx 8rpx 0 0;
position: relative;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
&::before {
content: '';
position: absolute;
top: 10rpx;
left: 50%;
transform: translateX(-50%);
width: 60%;
height: 20rpx;
background: rgba(255, 255, 255, 0.4);
border-radius: 4rpx;
}
}
.building-1 {
width: 60rpx;
height: 120rpx;
background: linear-gradient(180deg, #FF6B6B 0%, #FF8E53 100%);
}
.building-2 {
width: 50rpx;
height: 90rpx;
background: linear-gradient(180deg, #4ECDC4 0%, #44A08D 100%);
}
.building-3 {
width: 70rpx;
height: 140rpx;
background: linear-gradient(180deg, #FFD93D 0%, #FFA500 100%);
}
.building-4 {
width: 55rpx;
height: 100rpx;
background: linear-gradient(180deg, #6C5CE7 0%, #A29BFE 100%);
}
.building-5 {
width: 65rpx;
height: 130rpx;
background: linear-gradient(180deg, #00B894 0%, #55EFC4 100%);
}
.building-6 {
width: 50rpx;
height: 85rpx;
background: linear-gradient(180deg, #FD79A8 0%, #FDCB6E 100%);
}
.building-7 {
width: 60rpx;
height: 110rpx;
background: linear-gradient(180deg, #74B9FF 0%, #0984E3 100%);
}
.building-8 {
width: 55rpx;
height: 95rpx;
background: linear-gradient(180deg, #A29BFE 0%, #6C5CE7 100%);
}
.building-9 {
width: 70rpx;
height: 125rpx;
background: linear-gradient(180deg, #FFEAA7 0%, #FDCB6E 100%);
}
.building-10 {
width: 50rpx;
height: 80rpx;
background: linear-gradient(180deg, #55EFC4 0%, #00B894 100%);
}
.decoration {
position: absolute;
font-size: 40rpx;
animation: float 3s ease-in-out infinite;
}
.tree-1 {
left: 40rpx;
bottom: 0;
}
.tree-2 {
right: 40rpx;
bottom: 0;
}
.balloon {
top: 20rpx;
right: 80rpx;
animation: float 2s ease-in-out infinite;
}
.leaf-1 {
top: 40rpx;
left: 100rpx;
font-size: 30rpx;
animation: float 2.5s ease-in-out infinite;
}
.leaf-2 {
top: 60rpx;
right: 150rpx;
font-size: 25rpx;
animation: float 3.5s ease-in-out infinite;
}
}
}
@keyframes float {
0%, 100% {
transform: translateY(0);
}
50% {
transform: translateY(-10rpx);
}
}
.service-grid-section {
background-color: #fff;
margin: -20rpx 30rpx 20rpx;
border-radius: 20rpx;
padding: 40rpx 20rpx;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.08);
}
.service-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 30rpx 20rpx;
.service-item {
display: flex;
flex-direction: column;
align-items: center;
.service-icon-box {
width: 100rpx;
height: 100rpx;
border-radius: 20rpx;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 16rpx;
.service-emoji {
font-size: 48rpx;
}
}
.service-name {
font-size: 24rpx;
color: #333;
text-align: center;
}
}
}
.category-tabs {
display: flex;
background-color: #fff;
padding: 0 30rpx;
margin-bottom: 20rpx;
.tab-item {
flex: 1;
padding: 30rpx 0;
display: flex;
flex-direction: column;
align-items: center;
position: relative;
.tab-text {
font-size: 28rpx;
color: #666;
}
.tab-underline {
width: 60rpx;
height: 6rpx;
background: linear-gradient(90deg, #FF6B6B 0%, #FF8E53 100%);
border-radius: 3rpx;
margin-top: 12rpx;
}
&.active .tab-text {
color: #FF6B6B;
font-weight: bold;
}
}
}
.goods-list {
padding: 0 30rpx 30rpx;
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 20rpx;
.goods-item {
background-color: #fff;
border-radius: 16rpx;
overflow: hidden;
.goods-image {
width: 100%;
height: 320rpx;
}
.goods-info {
padding: 20rpx;
.goods-name {
font-size: 28rpx;
color: #333;
font-weight: 500;
display: block;
margin-bottom: 8rpx;
}
.goods-desc {
font-size: 24rpx;
color: #999;
display: block;
margin-bottom: 16rpx;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.goods-price-box {
display: flex;
align-items: baseline;
.price-symbol {
font-size: 24rpx;
color: #FF6B6B;
font-weight: bold;
}
.price-value {
font-size: 36rpx;
color: #FF6B6B;
font-weight: bold;
margin-left: 4rpx;
}
.price-original {
font-size: 24rpx;
color: #999;
text-decoration: line-through;
margin-left: 12rpx;
}
}
}
}
}
</style>

294
servicePages/pages/patrol.vue

@ -0,0 +1,294 @@
<template>
<view class="container">
<view class="form-container">
<view class="form-item">
<text class="label">巡更名称</text>
<text class="value">{{ patrolName }}</text>
</view>
<view class="form-item-column">
<text class="label">上报描述</text>
<textarea
class="textarea"
v-model="questionDescription"
placeholder="请输入上报描述"
placeholder-class="placeholder"
/>
</view>
<view class="form-item-column">
<text class="label">异常图片</text>
<view class="image-uploader">
<view class="image-list">
<view class="image-item" v-if="questionImage">
<image :src="questionImage" mode="aspectFill" @click="previewImageQuest"></image>
<view class="delete-icon" @click="deleteImageQuest">×</view>
</view>
<view class="add-btn" @click="chooseImageQuest">
<text>+</text>
</view>
</view>
</view>
</view>
<view class="form-item-column">
<text class="label">处理描述</text>
<textarea
class="textarea"
v-model="answerDescription"
placeholder="请输入处理描述"
placeholder-class="placeholder"
/>
</view>
<view class="form-item-column">
<text class="label">处理图片</text>
<view class="image-uploader">
<view class="image-list">
<view class="image-item" v-if="answerImage">
<image :src="answerImage" mode="aspectFill" @click="previewImage"></image>
<view class="delete-icon" @click="deleteImage">×</view>
</view>
<view class="add-btn" @click="chooseImage">
<text>+</text>
</view>
</view>
</view>
</view>
</view>
<view class="button-container">
<button class="submit-button" @click="submit">提交打卡</button>
</view>
</view>
</template>
<script>
import {addPatrolRecordReq, getPointInfoReq} from "@/api/common";
import {chooseImg} from "@/util";
export default {
data() {
return {
patrolName: '',
questionDescription: '',
answerDescription: '',
imageList: [],
filedId: [],
pointId: null,
answerImage: null,
questionImage: null,
}
},
methods: {
async chooseImage() {
uni.showLoading({
title: '上传中...',
})
const res = await chooseImg(1)
this.answerImage = res[0].path
uni.hideLoading()
},
previewImage() {
uni.previewImage({
urls: [this.answerImage],
current: 0
});
},
deleteImage() {
this.answerImage = null;
},
async chooseImageQuest() {
uni.showLoading({
title: '上传中...',
})
const res = await chooseImg(1)
this.questionImage = res[0].path
uni.hideLoading()
},
previewImageQuest() {
uni.previewImage({
urls: [this.questionImage],
current: 0
});
},
deleteImageQuest() {
this.questionImage = null;
},
async submit() {
if (!this.questionDescription.trim()) {
uni.showToast({
title: '请输入上报描述',
icon: 'none'
});
return;
}
if (!this.answerDescription.trim()) {
uni.showToast({
title: '请输入处理描述',
icon: 'none'
});
return;
}
const formData = {
patrolName: this.patrolName,
questionDescription: this.questionDescription,
answerDescription: this.answerDescription,
answerImage: this.answerImage,
questionImage: this.questionImage,
patrolRouteId: this.pointData.patrolRoute.id,
pointId: this.pointId,
};
console.log('提交数据:', formData);
await addPatrolRecordReq(formData)
//
uni.showToast({
title: '提交成功',
icon: 'success'
});
setTimeout(() => {
uni.switchTab({
url: '/pages/index/index'
});
}, 1500);
},
async getPointInfoReq(){
const {data} = await getPointInfoReq(this.pointId)
this.pointData = data
this.patrolName = `${data.patrolRoute.routeName}-${data.pointName}`
}
},
onLoad(e){
this.pointId = e.scene;
this.getPointInfoReq()
},
}
</script>
<style lang="scss">
.container {
padding: 20rpx;
background-color: #f7f7f7;
min-height: 100vh;
}
.form-container {
background-color: #fff;
border-radius: 16rpx;
padding: 0 30rpx;
}
.form-item, .form-item-column {
padding: 30rpx 0;
border-bottom: 1px solid #f0f0f0;
font-size: 28rpx;
&:last-child {
border-bottom: none;
}
}
.form-item {
display: flex;
justify-content: space-between;
align-items: center;
}
.form-item-column {
display: flex;
flex-direction: column;
.label {
margin-bottom: 20rpx;
}
}
.label {
color: #333;
font-weight: 500;
}
.value {
color: #333;
}
.textarea {
width: 100%;
height: 200rpx;
font-size: 28rpx;
border: 1px solid #e0e0e0;
border-radius: 8rpx;
padding: 20rpx;
box-sizing: border-box;
}
.placeholder {
color: #999;
}
.image-uploader {
.image-list {
display: flex;
flex-wrap: wrap;
gap: 20rpx;
}
.image-item {
position: relative;
width: 150rpx;
height: 150rpx;
image {
width: 100%;
height: 100%;
border-radius: 10rpx;
}
.delete-icon {
position: absolute;
top: -10rpx;
right: -10rpx;
width: 30rpx;
height: 30rpx;
background: #ff4757;
color: white;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 20rpx;
font-weight: bold;
}
}
.add-btn {
width: 150rpx;
height: 150rpx;
border: 2rpx dashed #ccc;
border-radius: 10rpx;
display: flex;
justify-content: center;
align-items: center;
font-size: 60rpx;
color: #ccc;
background-color: #fafafa;
}
}
.button-container {
margin-top: 40rpx;
}
.submit-button {
background-color: #007aff;
color: #fff;
border-radius: 50rpx;
font-size: 32rpx;
padding: 20rpx 0;
}
</style>

37
userPages/pages/adList.vue

@ -0,0 +1,37 @@
<template>
<view class="min-height bg-white">
<view class="card flex flex-col justify-start items-start p-20 border-bottom" v-for="(item, index) in adList" :key="index">
<view class="text-24 my-20 flex justify-between items-center">
<text>{{ item.name }}</text>
<text class="text-gray">{{ dayjs(item.createTime).format('YYYY-MM-DD') }}</text>
</view>
<video :src="item.imageList[0].url" v-if="item.type === 3" :poster="item.cover"
style="width: 100%; height: 400rpx" show-play-btn show-fullscreen-btn/>
</view>
</view>
</template>
<script>
import {adListReq} from "@/api/common";
import dayjs from "dayjs";
export default {
name: "adList",
data() {
return {
adList: [],
}
},
methods: {
dayjs,
async getAdList() {
const {data} = await adListReq()
this.adList = data.filter(item => item.type === 3)
},
},
onLoad(){
this.getAdList()
}
}
</script>
Loading…
Cancel
Save