This commit is contained in:
Leo_Ding 2025-08-07 12:55:52 +08:00
commit eea635e7bd
25 changed files with 949 additions and 836 deletions

View File

@ -1,91 +0,0 @@
<template>
<view class="header">
<view class="header-content">
<view class="left" @click="goBack">
<u-icon name="arrow-left" color="#fff" size="36rpx"></u-icon>
</view>
<view class="center">
<text class="title">{{ title }}</text>
</view>
<view class="right">
<!-- 右侧占位 -->
</view>
</view>
</view>
</template>
<script>
export default {
props: {
title: {
type: String,
default: '标题'
}
},
methods: {
goBack() {
let pages = getCurrentPages(); //
console.log("-----pages",pages)
if (pages.length > 1) {
// 1
uni.navigateBack({
delta: 1
});
} else {
//
uni.showToast({
title: '已经是首页了',
icon: 'none'
});
}
}
}
};
</script>
<style lang="scss" scoped>
.header {
background-color: transparent;
/* 蓝色背景 */
padding: 20rpx 30rpx;
position: fixed;
top: 90rpx;
left: 0;
right: 0;
z-index: 999;
.header-content {
display: flex;
align-items: center;
justify-content: space-between;
.left {
flex: 1;
display: flex;
align-items: center;
.back-icon {
width: 40rpx;
height: 40rpx;
}
}
.center {
flex: 2;
text-align: center;
.title {
color: #2e2e2e;
font-size: 36rpx;
// font-weight: bold;
}
}
.right {
flex: 1;
text-align: right;
}
}
}
</style>

View File

@ -10,6 +10,10 @@
"usingComponents" : true,
"nvueStyleCompiler" : "uni-app",
"compilerVersion" : 3,
"permissions": [
"camera",
"album"
],
"splashscreen" : {
"alwaysShowBeforeRender" : true,
"waiting" : true,
@ -68,7 +72,13 @@
"permission" : {
"scope.userLocation" : {
"desc" : "你的位置信息将用于获取周边服务"
}
},
"scope.album": {
"desc": "需要您的相册权限以便选择图片"
},
"scope.camera": {
"desc": "需要您的相机权限以便拍照"
}
}
},
"mp-alipay" : {

View File

@ -44,7 +44,9 @@
"path": "pages/serviceTickets/index",
"style": {
"navigationBarTitleText": "近山社区",
"navigationStyle": "default",
"navigationStyle": "default",
"enableShareAppMessage": false, //
"enableShareTimeline": false, //
"usingComponents": {}
}
},

View File

@ -34,7 +34,7 @@
<view class="info-item">
<u-icon name="clock" size="26" color="#5b9cf8"></u-icon>
<view class="info-text"><text class="info-label">活动时间</text>{{ activityInfo.openAt }}</view>
<view class="info-text"><text class="info-label">开始时间</text>{{formatTime(activityInfo.startAt,"YYYY-MM-DD HH:mm:ss") }}</view>
</view>
<view class="info-item">

View File

@ -1,282 +1,371 @@
<template>
<view class="container">
<view class="content">
<view class="content_wapper">
<!-- 添加标题 -->
<view class="input-section">
<u-input placeholder="添加标题" v-model="title" border></u-input>
</view>
<!-- 输入求助内容 -->
<view class="textarea-section">
<u--textarea placeholder="请输入求助内容..." v-model="content" border height="300"></u--textarea>
</view>
<!-- 图片上传 -->
<view class="upload-section">
<u-upload
@afterRead="afterRead"
@delete="deletePic"
:fileList="fileList"
:maxCount="9"
:previewFullImage="true"
:accept="'image/*'"
:capture="['album', 'camera']"
width="220"
height="220">
<view class="upload-btn" v-if="fileList.length < 1">
<u-icon name="plus" size="40" color="#666"></u-icon>
</view>
</u-upload>
</view>
<!-- 发布按钮 -->
<view class="publish-section">
<u-button type="primary" text="立即发布" @click="publish"></u-button>
</view>
</view>
</view>
<Footer></Footer>
</view>
</template>
<script>
import Footer from '@/components/footer_common.vue';
import {
get,
post
} from '@/utils/request';
import {
IMAGE_BASE_URL,
BASE_URL
} from '@/utils/config';
export default {
components: {
Footer
},
data() {
return {
title: '',
content: '',
fileList: [],
uploadedImageUrls: []
};
},
methods: {
handleBack() {
uni.navigateBack({
delta: 1 //
});
},
async afterRead(event) {
//
const files = event.file; //
const uploadFiles = Array.isArray(files) ? files : [files];
<template>
<view class="container">
<view class="content">
<view class="content_wapper">
<!-- 添加标题 -->
<view class="input-section">
<u-input placeholder="添加标题" v-model="title" border></u-input>
</view>
<!-- 输入求助内容 -->
<view class="textarea-section">
<u--textarea placeholder="请输入求助内容..." v-model="content" border height="300"></u--textarea>
</view>
<!-- 图片上传 - 修改后的上传组件 -->
<view class="upload-section">
<view class="label">
<u-icon name="photo" color="#3B8CFF" size="38"></u-icon>
上传照片
</view>
<view class="upload-area">
<view class="upload-list">
<view class="upload-item" v-for="(item, index) in fileList" :key="index">
<image :src="item.url" mode="aspectFill" @click="previewImage(index)"></image>
<view class="delete-btn" @click="handleDelete(index)">
<u-icon name="close" color="#fff" size="24"></u-icon>
</view>
</view>
<view class="upload-btn" @click="showUploadAction" v-if="fileList.length < 9">
<u-icon name="plus" size="40" color="#c0c4cc"></u-icon>
</view>
</view>
</view>
<text class="note">照片支持分批上传但最大数量为9</text>
</view>
<!-- 发布按钮 -->
<view class="publish-section">
<u-button type="primary" text="立即发布" @click="publish"></u-button>
</view>
</view>
</view>
<Footer></Footer>
</view>
</template>
<script>
import Footer from '@/components/footer_common.vue';
import {
get,
post
} from '@/utils/request';
import {
IMAGE_BASE_URL,
BASE_URL
} from '@/utils/config';
export default {
components: {
Footer
},
data() {
return {
title: '',
content: '',
fileList: [],
uploadedImageUrls: []
};
},
methods: {
handleBack() {
uni.navigateBack({
delta: 1 //
});
},
//
showUploadAction() {
uni.showActionSheet({
itemList: ['拍照', '从相册选择'],
success: (res) => {
if (res.tapIndex === 0) {
this.chooseMedia('camera');
} else {
this.chooseMedia('album');
}
}
});
},
//
async chooseMedia(sourceType) {
try {
// 1.
const res = await new Promise((resolve, reject) => {
uni.chooseImage({
count: 9 - this.fileList.length,
sourceType: [sourceType === 'camera' ? 'camera' : 'album'],
success: resolve,
fail: reject
});
});
// -
for (const file of uploadFiles) {
// 1
const fileExt = file.url.split('.').pop().toLowerCase();
const allowedExts = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp'];
// 2MIME
const isImage = file.type?.startsWith('image/') ||
allowedExts.includes(fileExt);
if (!isImage) {
uni.showToast({
title: '只能上传图片文件(JPG/PNG等)',
icon: 'none',
duration: 2000
});
return;
}
// 2.
const newFiles = res.tempFilePaths.map(url => ({
url,
status: 'uploading'
}));
this.fileList = [...this.fileList, ...newFiles];
// 3.
for (const file of newFiles) {
await this.uploadFile(file);
}
uni.showLoading({ title: '上传中...' });
try {
//
const uploadPromises = uploadFiles.map(file => this.uploadAvatar(file.url));
const uploadedUrls = await Promise.all(uploadPromises);
// URL
this.uploadedImageUrls = [...this.uploadedImageUrls, ...uploadedUrls];
//
this.fileList = [
...this.fileList,
...uploadFiles.map((file, index) => ({
url: file.url,
status: 'success',
uploadedUrl: uploadedUrls[index]
}))
];
uni.showToast({ title: '上传成功', icon: 'success' });
} catch (error) {
console.error('上传失败:', error);
uni.showToast({
title: '上传失败: ' + error.message,
icon: 'none',
duration: 2000
});
} finally {
uni.hideLoading();
} catch (err) {
console.error('选择图片失败:', err);
uni.showToast({ title: '选择图片失败', icon: 'none' });
}
},
//
async uploadFile(file) {
try {
const res = await new Promise((resolve, reject) => {
uni.uploadFile({
url: `${BASE_URL}/api/v1/upload`,
filePath: file.url,
name: 'file',
header: {
'Authorization': `Bearer ${uni.getStorageSync('token')}`,
'Content-Type': 'multipart/form-data'
},
success: (uploadRes) => {
try {
const data = JSON.parse(uploadRes.data);
if (data.success) {
resolve(data.data); // 使URL
} else {
reject(data.message || '上传失败');
}
} catch (e) {
reject('解析响应失败');
}
},
fail: (err) => reject(err)
});
});
//
const index = this.fileList.findIndex(f => f.url === file.url);
if (index !== -1) {
this.$set(this.fileList, index, {
...file,
status: 'success',
serverUrl: res // URL
});
//
this.uploadedImageUrls.push(res);
}
},
deletePic(event) {
// URL
const deletedFile = this.fileList[event.index];
this.uploadedImageUrls = this.uploadedImageUrls.filter(
url => url !== deletedFile.uploadedUrl
);
//
this.fileList.splice(event.index, 1);
},
uploadAvatar(filePath) {
return new Promise((resolve, reject) => {
uni.uploadFile({
url: `${BASE_URL}/api/v1/upload`,
filePath: filePath,
name: 'file',
header: {
'Authorization': `Bearer ${uni.getStorageSync('token')}`
},
success: (uploadRes) => {
try {
const res = JSON.parse(uploadRes.data);
if (res && res.success) {
resolve(res.data);
} else {
reject(new Error(res.message || '上传失败'));
}
} catch (e) {
reject(new Error('解析响应失败'));
}
},
fail: (err) => {
reject(new Error('上传失败: ' + JSON.stringify(err)));
}
});
});
},
async publish() {
if (!this.title.trim()) {
uni.showToast({
title: '请输入标题',
icon: 'none'
});
return;
}
if (!this.content.trim()) {
uni.showToast({
title: '请输入内容',
icon: 'none'
});
return;
}
uni.showLoading({
title: '发布中...'
});
try {
// URL
const postData = {
title: this.title,
content: this.content,
images: this.uploadedImageUrls.length > 0 ?
this.uploadedImageUrls :
[] //
};
// API
} catch (err) {
console.error('上传失败:', err);
const index = this.fileList.findIndex(f => f.url === file.url);
if (index !== -1) {
this.$set(this.fileList, index, {
...file,
status: 'failed',
error: err.message || err
});
}
uni.showToast({
title: `上传失败: ${err.message || err}`,
icon: 'none'
});
}
},
//
previewImage(index) {
const images = this.fileList.map(item => item.url);
uni.previewImage({
current: index,
urls: images
});
},
//
handleDelete(index) {
const file = this.fileList[index];
if (file.serverUrl) {
const imageIndex = this.uploadedImageUrls.indexOf(file.serverUrl);
if (imageIndex !== -1) this.uploadedImageUrls.splice(imageIndex, 1);
}
this.fileList.splice(index, 1);
},
async publish() {
if (!this.title.trim()) {
uni.showToast({
title: '请输入标题',
icon: 'none'
});
return;
}
if (!this.content.trim()) {
uni.showToast({
title: '请输入内容',
icon: 'none'
});
return;
}
uni.showLoading({
title: '发布中...'
});
try {
// URL
const postData = {
title: this.title,
content: this.content,
images: this.uploadedImageUrls.length > 0 ?
this.uploadedImageUrls :
[] //
};
// API
const res = await post('/api/v1/app_auth/reciprocities', postData);
if (!res || !res.success) {
throw new Error('发布失败');
}
uni.showToast({
title: '发布成功',
icon: 'success'
});
//
setTimeout(() => {
uni.navigateBack();
}, 1500);
} catch (error) {
console.error('发布失败:', error);
uni.showToast({
title: '发布失败',
icon: 'none'
});
} finally {
uni.hideLoading();
}
}
}
};
</script>
<style lang="scss" scoped>
.container {
width: 100%;
height: 100vh;
/* 占满整个视口高度 */
opacity: 1;
background: linear-gradient(0deg, rgba(240, 240, 240, 1) 0%, rgba(255, 250, 250, 0) 100%),
linear-gradient(0deg, rgba(255, 241, 235, 1) 0%, rgba(192, 219, 250, 1) 100%);
position: relative;
.content {
width: 100%;
position: absolute;
margin-top: 40rpx;
border-top-left-radius: 30rpx;
border-top-right-radius: 30rpx;
background-color: rgba(255, 255, 255, 0.65);
padding-bottom: 60rpx;
.content_wapper {
width: 90%;
margin: 0 auto;
}
}
.input-section {
border-bottom: 2rpx solid #C3DCFA;
height: 90rpx;
line-height: 90rpx;
}
.input-section,
.textarea-section,
.upload-section {
margin-bottom: 30rpx;
}
.upload-btn {
width: 220rpx;
height: 220rpx;
display: flex;
align-items: center;
justify-content: center;
background-color: #f0f0f0;
border-radius: 10rpx;
}
.publish-section {
text-align: center;
margin-top: 100rpx;
}
}
}
uni.showToast({
title: '发布成功',
icon: 'success'
});
//
setTimeout(() => {
uni.navigateBack();
}, 1500);
} catch (error) {
console.error('发布失败:', error);
uni.showToast({
title: '发布失败',
icon: 'none'
});
} finally {
uni.hideLoading();
}
}
}
};
</script>
<style lang="scss" scoped>
.container {
width: 100%;
height: 100vh;
/* 占满整个视口高度 */
opacity: 1;
background: linear-gradient(0deg, rgba(240, 240, 240, 1) 0%, rgba(255, 250, 250, 0) 100%),
linear-gradient(0deg, rgba(255, 241, 235, 1) 0%, rgba(192, 219, 250, 1) 100%);
position: relative;
.content {
width: 100%;
position: absolute;
margin-top: 40rpx;
border-top-left-radius: 30rpx;
border-top-right-radius: 30rpx;
background-color: rgba(255, 255, 255, 0.65);
padding-bottom: 60rpx;
.content_wapper {
width: 90%;
margin: 0 auto;
}
}
.input-section {
border-bottom: 2rpx solid #C3DCFA;
height: 90rpx;
line-height: 90rpx;
}
.input-section,
.textarea-section,
.upload-section {
margin-bottom: 30rpx;
}
.upload-section {
.label {
font-size: 32rpx;
color: #333;
font-weight: 500;
margin-bottom: 24rpx;
display: flex;
align-items: center;
.u-icon {
margin-right: 12rpx;
}
}
.upload-area {
margin-top: 16rpx;
.upload-list {
display: flex;
flex-wrap: wrap;
margin: -5rpx;
.upload-item, .upload-btn {
width: 160rpx;
height: 160rpx;
margin: 5rpx;
position: relative;
background: #f8f8f8;
border-radius: 8rpx;
display: flex;
justify-content: center;
align-items: center;
overflow: hidden;
image {
width: 100%;
height: 100%;
}
.delete-btn {
position: absolute;
right: 0;
top: 0;
width: 40rpx;
height: 40rpx;
background: rgba(0, 0, 0, 0.5);
border-radius: 0 0 0 8rpx;
display: flex;
justify-content: center;
align-items: center;
}
}
.upload-btn {
border: 1rpx dashed #c0c4cc;
}
}
}
.note {
font-size: 24rpx;
color: #999;
margin-top: 24rpx;
display: block;
}
}
.publish-section {
text-align: center;
margin-top: 100rpx;
}
}
</style>

View File

@ -47,7 +47,7 @@
</view>
<view class="temperature-range">
<text class="temp-item">最高温 {{weather.highest}}°</text>
<text class="temp-item">{{weather.lowest}}°</text>
<text class="temp-item">{{weather.lowest}}°</text>
</view>
</view>
<view class="weather-footer">

View File

@ -1,437 +1,464 @@
<template>
<view class="work-order-detail">
<!-- 头部用户信息区域 -->
<view class="header">
<image class="avatar" :src="detailObj.customerPortrait" mode="aspectFill"></image>
<text class="username">{{detailObj.customerName}}</text>
</view>
<!-- 图片区域 - 编辑状态下可修改 -->
<view class="image-area">
<image
class="work-order-img"
:src="`${IMAGE_BASE_URL}${detailObj.images && detailObj.images[0] ? detailObj.images[0] : ''}`"
mode="widthFix"
v-if="!isEditing && detailObj.images && detailObj.images.length > 0">
</image>
<view v-else>
<image
:src="isEditing ? workOrderImg : `${IMAGE_BASE_URL}${detailObj.images && detailObj.images[0] ? detailObj.images[0] : ''}`" >
</image>
<view class="upload-btn" @click="uploadImage" v-if="isEditing">
<u-icon name="camera" size="40" color="#fff"></u-icon>
<text class="upload-text">更换图片</text>
</view>
</view>
</view>
<!-- 标题与日期 -->
<view class="title-area">
<text class="title" v-if="!isEditing">{{detailObj.title}}</text>
<input class="title-input" v-model="editLabel" v-else placeholder="请输入标题" />
<text class="date">{{formatTime(detailObj.createdAt,"YYYY-MM-DD")}}</text>
</view>
<!-- 工单详情内容 - 编辑状态下可修改 -->
<view class="content">
<view class="label">
<text>求助内容</text>
<text v-if="!isEditing" style="font-weight: normal;">{{detailObj.content}}</text>
<textarea
class="edit-textarea"
v-model="editTitle"
v-else
placeholder="请输入内容"
auto-height
/>
</view>
</view>
<!-- 操作按钮 -->
<view class="btn-group">
<u-button
@click="handleModify"
:type="isEditing ? 'success' : 'primary'"
class="action-btn"
:loading="isLoading"
>
{{isEditing ? '保存修改' : '修改'}}
</u-button>
<u-button
@click="handleWithdraw"
:type="isEditing ? 'default' : 'primary'"
class="action-btn"
>
{{isEditing ? '取消编辑' : '撤回'}}
</u-button>
</view>
</view>
<view class="work-order-detail">
<!-- 头部用户信息区域 -->
<view class="header">
<image class="avatar" :src="`${IMAGE_BASE_URL}`+detailObj.customerPortrait|| '/static/index/nav.png'" mode="aspectFill"></image>
<text class="username">{{detailObj.customerName || '未知用户'}}</text>
</view>
<!-- 图片区域 - 编辑状态下可修改 -->
<view class="image-area">
<template v-if="!isEditing">
<template v-if="detailObj.images && detailObj.images.length > 0">
<image
class="work-order-img"
:src="`${IMAGE_BASE_URL}${detailObj.images[0]}`"
mode="aspectFit"
/>
</template>
<view class="empty-placeholder" v-else>
<u-icon name="photo" size="48" color="#c0c4cc"></u-icon>
<text class="empty-text">暂无图片</text>
</view>
</template>
<template v-else>
<image
class="work-order-img"
:src="tempImagePath || (detailObj.images && detailObj.images[0] ? `${IMAGE_BASE_URL}${detailObj.images[0]}` : '')"
mode="aspectFit"
v-if="tempImagePath || (detailObj.images && detailObj.images.length > 0)"
/>
<view class="empty-placeholder" v-else>
<u-icon name="photo" size="48" color="#c0c4cc"></u-icon>
<text class="empty-text">暂无图片</text>
</view>
<view class="upload-btn" @click="uploadImage">
<u-icon name="camera" size="40" color="#fff"></u-icon>
<text class="upload-text">{{tempImagePath ? '更换图片' : '上传图片'}}</text>
</view>
</template>
</view>
<!-- 标题与日期 -->
<view class="title-area" :class="{editing: isEditing}">
<text class="title" v-if="!isEditing">{{detailObj.title || '无标题'}}</text>
<input class="title-input" v-model="editLabel" v-else placeholder="请输入标题" />
<text class="date">{{formatTime(detailObj.createdAt,"YYYY-MM-DD") || '未知日期'}}</text>
</view>
<!-- 工单详情内容 - 编辑状态下可修改 -->
<view class="content" :class="{editing: isEditing}">
<view class="label">
<text>求助内容</text>
<text v-if="!isEditing" class="content-text">{{detailObj.content || '无内容描述'}}</text>
<textarea
class="edit-textarea"
v-model="editTitle"
v-else
placeholder="请输入内容"
auto-height
/>
</view>
</view>
<!-- 操作按钮 -->
<view class="btn-group">
<u-button
@click="handleModify"
:type="isEditing ? 'primary' : 'primary'"
class="action-btn"
:loading="isLoading"
:custom-style="btnStyle"
>
{{isEditing ? '保存修改' : '修改'}}
</u-button>
<u-button
@click="handleWithdraw"
:type="isEditing ? 'error' : 'error'"
class="action-btn"
:custom-style="btnStyle"
>
{{isEditing ? '取消编辑' : '撤回'}}
</u-button>
</view>
</view>
</template>
<script>
import { get, put } from '@/utils/request';
import { IMAGE_BASE_URL, BASE_URL } from '@/utils/config';
import { formatTime } from '@/utils/timeFormat';
export default {
data() {
return {
IMAGE_BASE_URL,
BASE_URL,
formatTime,
isEditing: false, //
isLoading: false, //
workOrderImg: '', //
//
editLabel: '',
editAddress: '',
editTitle: '',
detailObj: {
images: [], // images
customerPortrait: '',
customerName: '',
createdAt: '',
label: '',
address: '',
title: '',
id: ''
},
tempImagePath: '' //
};
},
mounted() {
let obj = uni.getStorageSync("Detail") || {};
console.log("===boj",obj)
this.detailObj = {
...obj //
};
// workOrderImg
if (this.detailObj.images && this.detailObj.images.length > 0) {
this.workOrderImg = `${IMAGE_BASE_URL}${this.detailObj.images[0]}`;
}
},
methods: {
//
uploadImage() {
uni.chooseImage({
count: 1,
success: (res) => {
const tempFilePaths = res.tempFilePaths[0];
this.tempImagePath = tempFilePaths;
this.workOrderImg = tempFilePaths;
uni.showToast({
title: '图片已更换',
icon: 'success'
});
},
fail: (err) => {
console.error('选择图片失败:', err);
uni.showToast({
title: '选择图片失败',
icon: 'none'
});
}
});
},
//
async uploadImageToServer(filePath) {
try {
const res = await new Promise((resolve, reject) => {
uni.uploadFile({
url: `${BASE_URL}/api/v1/upload`,
filePath: filePath,
name: 'file',
header: {
'Authorization': `Bearer ${uni.getStorageSync('token')}`
},
success: (uploadRes) => {
try {
const res = JSON.parse(uploadRes.data);
resolve(res);
} catch (e) {
reject(new Error('解析响应失败'));
}
},
fail: (err) => {
reject(new Error('上传失败: ' + JSON.stringify(err)));
}
});
});
if (res && res.success) {
return res.data; //
} else {
throw new Error(res.message || '上传失败');
}
} catch (error) {
console.error('上传图片失败:', error);
uni.showToast({
title: '上传图片失败',
icon: 'none'
});
throw error;
}
},
// /
async handleModify() {
if (!this.isEditing) {
//
this.editLabel = this.detailObj.title || '';
this.editAddress = this.detailObj.address || '';
this.editTitle = this.detailObj.content || '';
if (!this.workOrderImg && this.detailObj.images && this.detailObj.images.length > 0) {
this.workOrderImg = `${IMAGE_BASE_URL}${this.detailObj.images[0]}`;
}
this.isEditing = true;
} else {
//
this.isLoading = true;
try {
let images = [...(this.detailObj.images || [])];
//
if (this.tempImagePath) {
const uploadedPath = await this.uploadImageToServer(this.tempImagePath);
images = [uploadedPath]; //
this.workOrderImg = `${IMAGE_BASE_URL}${uploadedPath}`;
}
const requestData = {
content: this.editTitle,
title: this.editLabel,
images: images,
status: 1
};
const res = await put(`/api/v1/app_auth/reciprocities/${this.detailObj.id}`, requestData);
if (res && res.success) {
//
this.detailObj = {
...this.detailObj,
...requestData
};
uni.showToast({
title: '修改成功',
icon: 'success'
});
this.isEditing = false;
this.tempImagePath = ''; //
// workOrderImg
if (images.length > 0) {
this.workOrderImg = `${IMAGE_BASE_URL}${images[0]}`;
}
} else {
throw new Error(res.message || '修改失败');
}
} catch (error) {
console.error('保存失败:', error);
uni.showToast({
title: error.message || '保存失败,请重试',
icon: 'none'
});
} finally {
this.isLoading = false;
}
}
},
// /
async handleWithdraw() {
if (this.isEditing) {
//
this.isEditing = false;
this.tempImagePath = '';
//
if (this.detailObj.images && this.detailObj.images.length > 0) {
this.workOrderImg = `${IMAGE_BASE_URL}${this.detailObj.images[0]}`;
}
} else {
//
try {
this.isLoading = true;
const res = await put(`/api/v1/app_auth/reciprocities/${this.detailObj.id}`, {status: 98});
if (res && res.success) {
uni.showToast({
title: '撤回成功',
icon: 'success'
});
//
uni.navigateBack();
} else {
throw new Error(res.message || '撤回失败');
}
} catch (error) {
console.error('撤回失败:', error);
uni.showToast({
title: error.message || '撤回失败',
icon: 'none'
});
} finally {
this.isLoading = false;
}
}
}
}
};
import { get, put } from '@/utils/request';
import { IMAGE_BASE_URL, BASE_URL } from '@/utils/config';
import { formatTime } from '@/utils/timeFormat';
export default {
data() {
return {
IMAGE_BASE_URL,
BASE_URL,
formatTime,
isEditing: false, //
isLoading: false, //
tempImagePath: '', //
//
editLabel: '',
editTitle: '',
detailObj: {
images: [], // images
customerPortrait: '',
customerName: '',
createdAt: '',
title: '',
content: '',
id: ''
},
btnStyle: {
width: '45%',
height: '80rpx'
}
};
},
mounted() {
let obj = uni.getStorageSync("Detail") || {};
this.detailObj = {
...obj //
};
},
methods: {
//
uploadImage() {
uni.chooseImage({
count: 1,
success: (res) => {
this.tempImagePath = res.tempFilePaths[0];
uni.showToast({
title: '图片已选择',
icon: 'success'
});
},
fail: (err) => {
console.error('选择图片失败:', err);
uni.showToast({
title: '选择图片失败',
icon: 'none'
});
}
});
},
//
async uploadImageToServer(filePath) {
try {
const res = await new Promise((resolve, reject) => {
uni.uploadFile({
url: `${BASE_URL}/api/v1/upload`,
filePath: filePath,
name: 'file',
header: {
'Authorization': `Bearer ${uni.getStorageSync('token')}`
},
success: (uploadRes) => {
try {
const res = JSON.parse(uploadRes.data);
resolve(res);
} catch (e) {
reject(new Error('解析响应失败'));
}
},
fail: (err) => {
reject(new Error('上传失败: ' + JSON.stringify(err)));
}
});
});
if (res && res.success) {
return res.data;
} else {
throw new Error(res.message || '上传失败');
}
} catch (error) {
console.error('上传图片失败:', error);
uni.showToast({
title: '上传图片失败',
icon: 'none'
});
throw error;
}
},
// /
async handleModify() {
if (!this.isEditing) {
//
this.editLabel = this.detailObj.title || '';
this.editTitle = this.detailObj.content || '';
this.isEditing = true;
} else {
//
this.isLoading = true;
try {
let images = [...(this.detailObj.images || [])];
//
if (this.tempImagePath) {
const uploadedPath = await this.uploadImageToServer(this.tempImagePath);
images = [uploadedPath];
}
const requestData = {
content: this.editTitle,
title: this.editLabel,
images: images,
status: 1
};
const res = await put(`/api/v1/app_auth/reciprocities/${this.detailObj.id}`, requestData);
if (res && res.success) {
this.detailObj = {
...this.detailObj,
...requestData
};
uni.showToast({
title: '修改成功',
icon: 'success'
});
this.isEditing = false;
this.tempImagePath = '';
} else {
throw new Error(res.message || '修改失败');
}
} catch (error) {
console.error('保存失败:', error);
uni.showToast({
title: error.message || '保存失败,请重试',
icon: 'none'
});
} finally {
this.isLoading = false;
}
}
},
// /
async handleWithdraw() {
if (this.isEditing) {
//
this.isEditing = false;
this.tempImagePath = '';
} else {
//
try {
this.isLoading = true;
const res = await put(`/api/v1/app_auth/reciprocities/${this.detailObj.id}`, {status: 98});
if (res && res.success) {
uni.showToast({
title: '撤回成功',
icon: 'success'
});
uni.navigateBack();
} else {
throw new Error(res.message || '撤回失败');
}
} catch (error) {
console.error('撤回失败:', error);
uni.showToast({
title: error.message || '撤回失败',
icon: 'none'
});
} finally {
this.isLoading = false;
}
}
}
}
};
</script>
<style lang="scss" scoped>
/* 原有样式保持不变 */
//
$primary-color: #007AFF;
$success-color: #4cd964;
$warning-color: #FF9500;
$text-color: #333;
$subtext-color: #999;
$border-radius: 10rpx;
$padding-base: 15rpx;
$font-base: 32rpx;
//
$primary-color: #007AFF;
$success-color: #4cd964;
$warning-color: #FF9500;
$error-color: #dd524d;
$text-color: #333;
$subtext-color: #999;
$border-radius: 12rpx;
$padding-base: 20rpx;
$font-base: 32rpx;
$shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.08);
// mixin
@mixin flex-center {
display: flex;
align-items: center;
}
.work-order-detail {
padding: $padding-base;
width: 92%;
margin: 20rpx auto;
border-radius: $border-radius;
box-shadow: $shadow;
background-color: #fff;
.header {
display: flex;
align-items: center;
margin-bottom: $padding-base;
padding-bottom: $padding-base;
border-bottom: 1rpx solid #f5f5f5;
.work-order-detail {
padding: $padding-base;
width: 90%;
margin: 0 auto;
border-radius: 10rpx;
box-shadow: 0rpx 2rpx 10rpx rgba(0, 0, 0, 0.25);
.avatar {
width: 80rpx;
height: 80rpx;
border-radius: 50%;
margin-right: 20rpx;
background-color: #f5f5f5;
}
.header {
@include flex-center;
margin-bottom: $padding-base;
.username {
font-size: $font-base;
font-weight: bold;
color: $text-color;
}
}
.image-area {
position: relative;
margin-bottom: $padding-base;
height: 400rpx;
background-color: #f9f9f9;
border-radius: $border-radius;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
.work-order-img {
width: 100%;
height: 100%;
object-fit: contain;
}
.empty-placeholder {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: #c0c4cc;
width: 100%;
height: 100%;
.empty-text {
font-size: 28rpx;
margin-top: 16rpx;
color: #c0c4cc;
}
}
.upload-btn {
position: absolute;
bottom: 30rpx;
right: 30rpx;
background: rgba(0, 0, 0, 0.6);
padding: 12rpx 24rpx;
border-radius: 40rpx;
display: flex;
align-items: center;
justify-content: center;
.upload-text {
color: #fff;
font-size: 26rpx;
margin-left: 10rpx;
}
}
}
.avatar {
width: 80rpx;
height: 80rpx;
border-radius: 50%;
margin-right: 10rpx;
}
.title-area {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: $padding-base;
padding-bottom: $padding-base;
border-bottom: 1rpx solid #f5f5f5;
&.editing {
border-bottom: 1rpx solid #eaeaea;
}
.username {
font-size: $font-base;
font-weight: bold;
color: $text-color;
}
}
.image-area {
position: relative;
margin-bottom: $padding-base;
min-height: 200rpx;
background-color: #f5f5f5;
display: flex;
align-items: center;
justify-content: center;
}
.title {
font-size: $font-base + 4rpx;
font-weight: bold;
color: $text-color;
flex: 1;
}
.title-input {
font-size: $font-base + 4rpx;
font-weight: bold;
color: $text-color;
flex: 1;
padding: 10rpx;
background: #f8f8f8;
border-radius: 8rpx;
border: 1rpx solid #eaeaea;
}
.work-order-img {
width: 100%;
border-radius: $border-radius;
max-height: 400rpx;
}
.upload-btn {
position: absolute;
bottom: 20rpx;
right: 20rpx;
background: rgba(0, 0, 0, 0.6);
padding: 8rpx 20rpx;
border-radius: 30rpx;
@include flex-center;
.upload-text {
color: #fff;
font-size: 24rpx;
margin-left: 8rpx;
}
}
.date {
font-size: $font-base - 4rpx;
color: $subtext-color;
margin-left: 20rpx;
}
}
.title-area {
@include flex-center;
justify-content: space-between;
margin-bottom: $padding-base;
border-bottom: 2rpx solid $subtext-color;
height: 70rpx;
line-height: 70rpx;
.content {
margin-bottom: $padding-base * 2;
font-size: $font-base;
&.editing {
padding: 10rpx;
background: #fafafa;
border-radius: $border-radius;
}
.title {
font-size: $font-base + 2rpx;
font-weight: bold;
color: $text-color;
max-width: 70%;
}
.title-input {
font-size: $font-base + 2rpx;
font-weight: bold;
color: $text-color;
width: 70%;
padding: 10rpx;
background: #f8f8f8;
border-radius: 8rpx;
}
.label {
display: flex;
flex-direction: column;
text {
font-weight: bold;
margin-bottom: 10rpx;
}
.content-text {
font-weight: normal;
color: #666;
line-height: 1.6;
}
}
.edit-textarea {
width: 100%;
padding: 16rpx;
background: #fff;
border-radius: 8rpx;
border: 1rpx solid #eaeaea;
min-height: 200rpx;
font-size: $font-base;
line-height: 1.5;
}
}
.date {
font-size: $font-base - 4rpx;
color: $subtext-color;
}
}
.content {
margin-bottom: $padding-base;
line-height: 50rpx;
font-size: 24rpx;
.label {
text {
font-weight: bold;
}
margin-bottom: 20rpx;
}
.edit-input {
width: 95%;
padding: 10rpx;
background: #f8f8f8;
border-radius: 8rpx;
margin-top: 5rpx;
}
.edit-textarea {
width: 95%;
padding: 10rpx;
background: #f8f8f8;
border-radius: 8rpx;
margin-top: 5rpx;
min-height: 150rpx;
}
}
.btn-group {
@include flex-center;
justify-content: space-around;
margin-top: 30rpx;
gap:30rpx;
.action-btn {
width: 30%; //
height: 80rpx;
border-radius: 40rpx;
font-size: $font-base - 4rpx;
}
}
}
.btn-group {
display: flex;
justify-content: space-between;
margin-top: 40rpx;
gap: 20rpx;
.action-btn {
flex: 1;
border-radius: 50rpx;
font-size: $font-base;
font-weight: bold;
}
}
}
</style>

View File

@ -3,7 +3,7 @@
<!-- 主帖内容 -->
<view class="post-container">
<view class="post-header">
<image class="avatar" :src="postData.customerPortrait" mode="aspectFill"></image>
<image class="avatar" :src="`${IMAGE_BASE_URL}`+postData.customerPortrait" mode="aspectFill"></image>
<view class="post-info">
<text class="username">{{ postData.customerName }}</text>
<text class="time">{{ formatTime(postData.pushAt) }}</text>
@ -32,7 +32,8 @@
<view v-for="comment in flatComments" :key="comment.id" class="comment-item"
:class="{'is-reply': comment.toUserName}">
<view class="comment-header">
<image class="avatar" :src="comment.pusherPortrait" mode="aspectFill"></image>
<image class="avatar"
:src="comment.pusherPortrait === '/static/imgs/index/nav.png' ? comment.pusherPortrait : `${IMAGE_BASE_URL}${comment.pusherPortrait}`" mode="aspectFill"></image>
<view class="comment-user-info">
<text class="username">{{ comment.pusherName }}</text>
<view class="meta-info">

View File

@ -41,7 +41,7 @@
</view>
<view class="item_right">
<view class="right_nav">
<image :src="item.customerPortrait ?item.customerPortrait :`/static/imgs/index/nav.png`" class="nav"></image>
<image :src="item.customerPortrait ?`${IMAGE_BASE_URL}`+item.customerPortrait :`/static/imgs/index/nav.png`" class="nav"></image>
<image src="/static/imgs/index/nav_bg.png" class="bg"></image>
</view>
<view class="right_name">
@ -317,7 +317,7 @@
.bg {
position: absolute;
z-index: 1;
top: 20rpx;
top: 0rpx;
left: -118rpx;
width: 260rpx;
height: 260rpx;

View File

@ -37,9 +37,6 @@
工单内容
</view>
<textarea v-model="workOrderContent" placeholder="请输入工单详细内容..." class="textarea" />
<view class="voice-icon">
<u-icon name="mic" color="#409EFF" size="36"></u-icon>
</view>
</view>
<!-- 联系人信息 -->
@ -56,7 +53,13 @@
<u-icon name="phone" color="#3B8CFF" size="38"></u-icon>
电话
</view>
<input v-model="customerPhone" placeholder="请输入联系人电话" class="contact-input" type="number" />
<input
v-model="customerPhone"
placeholder="请输入联系人电话"
class="contact-input"
type="number"
@blur="validatePhone"
/>
</view>
</view>
@ -80,14 +83,14 @@
</view>
</view>
</view>
<text class="note">照片可识别工单内容支持分批上传但最大数量为8</text>
<text class="note">照片支持分批上传但最大数量为8</text>
</view>
<!-- 工单地点 -->
<view class="form-item">
<view class="label">
<u-icon name="map" color="#3B8CFF" size="38"></u-icon>
工作地点
维修地点
</view>
<textarea v-model="workOrderLocation" placeholder="请输入详细工作地点..." class="textarea" />
</view>
@ -151,6 +154,19 @@ export default {
this.getOrderTypeList();
},
methods: {
//
validatePhone() {
if (!this.customerPhone) return true;
const phoneReg = /^1[3-9]\d{9}$/;
if (!phoneReg.test(this.customerPhone)) {
uni.showToast({
title: '请输入正确的手机号码',
icon: 'none'
});
return false;
}
return true;
},
//
showUploadAction() {
uni.showActionSheet({
@ -350,8 +366,11 @@ export default {
this.fileList.splice(index, 1);
},
// ...
async handleSubmit() {
//
if (!this.validatePhone()) {
return;
}
if (!this.workOrderContent) {
uni.showToast({
title: '请填写工单内容',

Binary file not shown.

Before

Width:  |  Height:  |  Size: 628 B

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 429 B

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 616 B

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 930 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@ -1,13 +1,17 @@
import { BASE_URL } from "./config";
// 获取本地Token的同步方法
// 获取本地 Token 的同步方法
const getToken = () => {
try {
return uni.getStorageSync('token') || null
return uni.getStorageSync('token') || null;
} catch (e) {
console.error('获取本地Token失败:', e)
return null
console.error('获取本地Token失败:', e);
return null;
}
}
};
// 是否正在跳转登录页,防止重复弹窗
let isRedirectingToLogin = false;
// 统一请求方法
export const request = (options) => {
@ -15,54 +19,106 @@ export const request = (options) => {
// 合并请求头
const header = {
'Content-Type': 'application/json',
...(options.header || {}), // 允许覆盖默认header
Authorization: getToken() ? `Bearer ${getToken()}` : undefined
}
...(options.header || {}), // 允许覆盖默认 header
Authorization: getToken() ? `Bearer ${getToken()}` : undefined,
};
// 过滤掉undefined的header项
// 过滤掉 undefined header
const filteredHeader = Object.fromEntries(
Object.entries(header).filter(([_, v]) => v !== undefined)
)
);
uni.request({
url: `${BASE_URL}${options.url}`,
method: options.method || 'GET',
data: options.data || {},
header: filteredHeader,
timeout: options.timeout || 10000, // 建议设置超时时间
success: (res) => {
// 处理401未授权
if (res.statusCode === 401) {
uni.removeStorageSync('token')
// uni.showToast({ title: '请前往"个人中心页"登录', icon: 'none' })
// uni.navigateTo({ url: '/pages/mine/index' })
return reject(new Error('登录状态已过期'))
const { statusCode } = res;
// 处理 401 未授权(登录过期/未登录)
if (statusCode === 401) {
// 防止多次弹窗和重复跳转
if (!isRedirectingToLogin) {
isRedirectingToLogin = true;
uni.removeStorageSync('token');
// 友好提示 + 延迟跳转
uni.showToast({
title: '暂未登录,请登录后使用',
icon: 'none',
duration: 1500,
});
setTimeout(() => {
isRedirectingToLogin = false;
uni.navigateTo({
url: '/pages/mine/index',
fail: () => {
uni.reLaunch({ url: '/pages/mine/index' });
}
});
}, 1500);
}
return reject(new Error('未登录或登录已过期'));
}
// 处理其他错误状态码
if (res.statusCode < 200 || res.statusCode >= 300) {
const errorMsg = res.data?.message || `请求失败 (${res.statusCode})`
uni.showToast({ title: errorMsg, icon: 'none' })
return reject(new Error(errorMsg))
// 处理其他非 2xx 状态码
if (statusCode < 200 || statusCode >= 300) {
const errorMsg = res.data?.message || `请求失败 (HTTP ${statusCode})`;
uni.showToast({ title: errorMsg, icon: 'none', duration: 2000 });
return reject(new Error(errorMsg));
}
resolve(res.data)
const responseData = res.data;
if (responseData && typeof responseData === 'object' && 'code' in responseData) {
if (responseData.code !== 0) {
const bizMsg = responseData.message || '请求异常';
uni.showToast({ title: bizMsg, icon: 'none' });
return reject(new Error(bizMsg));
}
// 成功:返回 data 字段
resolve(responseData.data);
} else {
// 兼容没有统一响应格式的接口
resolve(responseData);
}
},
fail: (err) => {
const errorMsg = err.errMsg.includes('timeout')
? '网络请求超时'
: '网络连接失败'
uni.showToast({ title: errorMsg, icon: 'none' })
reject(new Error(errorMsg))
}
})
})
}
const errMsg = err.errMsg || '';
let errorMsg = '网络连接失败';
// 封装常用方法
export const get = (url, data, header) => request({ url, method: 'GET', data, header })
if (errMsg.includes('timeout')) {
errorMsg = '网络请求超时,请检查网络';
} else if (errMsg.includes('Network Error')) {
errorMsg = '网络异常,请检查网络设置';
}
export const post = (url, data, header) => request({ url, method: 'POST', data, header })
uni.showToast({
title: errorMsg,
icon: 'none',
duration: 2000,
});
export const put = (url, data, header) => request({ url, method: 'PUT', data, header })
reject(new Error(errorMsg));
},
});
});
};
export const del = (url, data, header) => request({ url, method: 'DELETE', data, header })
// 封装常用方法(支持传入额外配置,比如 timeout、skipAuth 等)
export const get = (url, data, header, options = {}) =>
request({ url, method: 'GET', data, header, ...options });
export const post = (url, data, header, options = {}) =>
request({ url, method: 'POST', data, header, ...options });
export const put = (url, data, header, options = {}) =>
request({ url, method: 'PUT', data, header, ...options });
export const del = (url, data, header, options = {}) =>
request({ url, method: 'DELETE', data, header, ...options });