feat: 报修 建议提交、列表查询;

property-only-app
wx-jincw 2 months ago
parent 00a856d00e
commit ce6759d27a

@ -19,6 +19,32 @@ export function listComplaintSuggestion(params) {
}
// 报修记录 - 列表
export function listMaintenanceOrder(params) {
return request.get(
'autogencode/pmmaintenanceorder/list',
params,
{ useAdminUrl: true }
);
}
// 报修记录 - 新增(用于业主发起报修,后端可据此生成工单)
export function createMaintenanceOrder(data) {
return request.post(
'autogencode/pmmaintenanceorder/save',
data,
{ useAdminUrl: true }
);
}
// 报修记录 - 详情
export function listMaintenanceOrderDetail(id) {
return request.get(
'autogencode/pmmaintenanceorder/info/' + id,
{ useAdminUrl: true }
);
}
// 报修派单记录 - 列表
export function listMaintenanceDispatch(params) {
return request.get(
'autogencode/pmmaintenancedispatch/list',
@ -27,7 +53,7 @@ export function listMaintenanceDispatch(params) {
);
}
// 报修记录 - 新增(用于业主发起报修,后端可据此生成工单)
// 报修派单记录 - 新增
export function createMaintenanceDispatch(data) {
return request.post(
'autogencode/pmmaintenancedispatch/save',

@ -95,13 +95,13 @@
</navigator>
<navigator class='item' url='/pages/supply_chain/complaint/index' hover-class='none'>
<view class='pictrue'>
<image src="/static/images/wg/wg_get.png"></image>
<image src="/static/images/wg/wg_jy.png"></image>
</view>
<view class="menu-txt">投诉与建议</view>
</navigator>
<navigator class='item' url='/pages/supply_chain/repair/index' hover-class='none'>
<view class='pictrue'>
<image src="/static/images/wg/wg_get.png"></image>
<image src="/static/images/wg/wg_wx.png"></image>
</view>
<view class="menu-txt">报修服务</view>
</navigator>

@ -35,21 +35,35 @@
<view class="type-tags">
<view
class="type-tag"
:class="form.csType === '投诉' ? 'checked' : ''"
@click="form.csType = '投诉'"
:class="form.submitChannel === '0' ? 'checked' : ''"
@click="form.submitChannel = '0'"
>
投诉
</view>
<view
class="type-tag"
:class="form.csType === '建议' ? 'checked' : ''"
@click="form.csType = '建议'"
:class="form.submitChannel === '1' ? 'checked' : ''"
@click="form.submitChannel = '1'"
>
建议
</view>
</view>
</view>
<view class="form-item">
<text class="label">问题类型</text>
<picker
@change="handleTypeChange"
:value="typeIndex"
:range="dict.get('cs_type')"
:range-key="'dictLabel'"
>
<view class="picker">
<text>{{ selectedType || '请选择问题类型' }}</text>
</view>
</picker>
</view>
<view class="form-item">
<text class="label">联系方式</text>
<input
@ -106,7 +120,7 @@
>
<view class="card-header">
<view class="left">
<text class="type">{{ item.csType || '—' }}</text>
<text class="type">{{ item.submitChannel === '0' ? '投诉' : '建议' }}</text>
<text
class="status"
:class="statusClass(item.status)"
@ -174,15 +188,18 @@ import {
} from '@/api/property.js';
export default {
dicts: ['cs_type'],
data() {
return {
activeTab: 'form',
form: {
csType: '投诉',
csType: '',
submitChannel: '0',
csContent: '',
remark: '',
phone: ''
phone: '',
},
typeIndex: 0,
records: [],
page: 1,
limit: 10,
@ -190,6 +207,15 @@ export default {
finished: false
};
},
computed: {
selectedType() {
const types = this.dict.get('cs_type');
if (types && types.length > this.typeIndex) {
return types[this.typeIndex].dictLabel;
}
return '';
}
},
onShow() {
if (this.activeTab === 'list') {
this.refreshList();
@ -205,6 +231,14 @@ export default {
this.refreshList();
}
},
handleTypeChange(e) {
const index = e.detail.value;
this.typeIndex = index;
const types = this.dict.get('cs_type');
if (types && types.length > index) {
this.form.csType = types[index].dictValue;
}
},
async submitComplaint() {
if (!this.form.csContent.trim()) {
uni.showToast({
@ -216,10 +250,10 @@ export default {
const payload = {
csType: this.form.csType,
submitChannel: this.form.submitChannel,
csContent: this.form.csContent.trim(),
remark: this.form.remark.trim(),
phone: this.form.phone.trim(),
submitChannel: 'APP'
};
try {
@ -264,10 +298,11 @@ export default {
try {
const params = {
page: this.page,
limit: this.limit
limit: this.limit,
};
const res = await listComplaintSuggestion(params);
const list = (res && (res.list || res.data || res.rows)) || [];
const data = res?.data;
const list = data?.list || [];
if (this.page === 1) {
this.records = list;
} else {
@ -286,15 +321,18 @@ export default {
}
},
statusText(status) {
if (!status) return '待处理';
if (status === '1' || status === '已处理') return '已处理';
if (status === '0' || status === '待处理') return '待处理';
if (!status || status === '0') return '待处理';
if (status === '1') return '未处理';
if (status === '2') return '处理中';
if (status === '3') return '已处理';
return status;
},
statusClass(status) {
const text = this.statusText(status);
if (text === '已处理') return 'status-done';
if (text === '待处理') return 'status-pending';
if (text === '未处理') return 'status-pending';
if (text === '处理中') return 'status-processing';
return '';
}
}
@ -439,6 +477,19 @@ export default {
}
}
}
.picker {
width: 100%;
height: 72rpx;
border-radius: 10rpx;
border: 1rpx solid #e5e5e5;
padding: 0 20rpx;
display: flex;
align-items: center;
font-size: 24rpx;
color: #333;
box-sizing: border-box;
}
}
.remark-item {
@ -506,6 +557,11 @@ export default {
background-color: #F6FFED;
color: #52C41A;
}
.status-processing {
background-color: #FFF7E6;
color: #FA8C16;
}
}
.time {

@ -41,6 +41,24 @@
</view>
</view>
<view class="form-item">
<text class="label">故障类型</text>
<picker
@change="handleFaultTypeChange"
:value="faultTypeIndex"
:range="dict.get('fault_type')"
:range-key="'dictLabel'"
>
<view class="picker-row">
<text class="value" v-if="selectedFaultType">
{{ selectedFaultType }}
</text>
<text class="placeholder" v-else></text>
<text class="iconfont icon-xiangyou"></text>
</view>
</picker>
</view>
<view class="form-item">
<text class="label">联系方式</text>
<input
@ -65,14 +83,24 @@
</view>
<view class="form-item">
<text class="label">期望上门时间选填</text>
<input
class="input"
type="text"
v-model="form.expectedTime"
placeholder="例如:工作日白天 / 周末上午"
maxlength="50"
/>
<text class="label">上传图片选填</text>
<view class="upload-section">
<view class="upload-list">
<view
class="upload-item"
v-for="(image, index) in images"
:key="index"
>
<image :src="HTTP_ADMIN_URL + '/' + image.url" mode="aspectFill"></image>
<text class="delete-btn" @click="deleteImage(index)">×</text>
</view>
<view class="upload-btn" @click="chooseImage" v-if="images.length < 9">
<text class="iconfont icon-tianjia"></text>
<text>添加图片</text>
</view>
</view>
<text class="hint">最多上传9张图片</text>
</view>
</view>
</view>
@ -113,6 +141,12 @@
</view>
<view class="card-body">
<view class="row" v-if="item.faultType">
<text class="label">故障类型</text>
<text class="value">
{{ getFaultTypeLabel(item.faultType) }}
</text>
</view>
<view class="row" v-if="item.assigneContext">
<text class="label">报修内容</text>
<text class="value">
@ -185,12 +219,10 @@
@click="chooseHouse(item)"
>
<text class="name">
{{ item.houseName || item.houseNo || '房屋' }}
{{ (item.unitNo ? item.unitNo + '单元' : '') + (item.floorNo ? ' ' + item.floorNo + '层' : '') + (item.houseNo ? ' ' + item.houseNo + '室' : '') || item.houseName || '房屋' }}
</text>
<text class="desc">
{{ item.buildingId ? '楼栋 ' + item.buildingId : '' }}
{{ item.unitNo ? ' 单元 ' + item.unitNo : '' }}
{{ item.floorNo ? ' ' + item.floorNo + '层' : '' }}
</text>
</view>
<view
@ -213,21 +245,24 @@
</template>
<script>
import {
createMaintenanceDispatch,
listMaintenanceDispatch,
listHouses
} from '@/api/property.js';
import { createMaintenanceOrder, listMaintenanceOrder, listMaintenanceOrderDetail, listHouses } from '@/api/property.js';
import request from '@/utils/request.js';
import { HTTP_ADMIN_URL } from '@/config/app';
export default {
dicts: ['fault_type'],
data() {
return {
HTTP_ADMIN_URL,
activeTab: 'form',
form: {
phone: '',
handleContent: '',
expectedTime: ''
expectedTime: '',
faultType: ''
},
faultTypeIndex: 0,
images: [],
selectedHouseId: null,
selectedHouseName: '',
records: [],
@ -240,6 +275,15 @@ export default {
houseLoading: false
};
},
computed: {
selectedFaultType() {
const types = this.dict.get('fault_type');
if (types && types.length > this.faultTypeIndex) {
return types[this.faultTypeIndex].dictLabel;
}
return '';
}
},
onShow() {
if (this.activeTab === 'list') {
this.refreshList();
@ -249,6 +293,14 @@ export default {
goBack() {
uni.navigateBack();
},
handleFaultTypeChange(e) {
const index = e.detail.value;
this.faultTypeIndex = index;
const types = this.dict.get('fault_type');
if (types && types.length > index) {
this.form.faultType = types[index].dictValue;
}
},
switchToList() {
this.activeTab = 'list';
if (!this.records.length) {
@ -265,7 +317,7 @@ export default {
this.houseLoading = true;
try {
const res = await listHouses({});
this.houseList = (res && (res.list || res.data || res.rows)) || [];
this.houseList = res?.data?.list || [];
} catch (e) {
uni.showToast({
title: typeof e === 'string' ? e : '获取房屋失败',
@ -277,9 +329,72 @@ export default {
},
chooseHouse(item) {
this.selectedHouseId = item.id;
this.selectedHouseName = item.houseName || item.houseNo || '';
//
const unitPart = item.unitNo ? item.unitNo + '单元' : '';
const floorPart = item.floorNo ? item.floorNo + '层' : '';
const housePart = item.houseNo ? item.houseNo + '室' : '';
this.selectedHouseName = [unitPart, floorPart, housePart].filter(Boolean).join(' ');
this.housePopupVisible = false;
},
chooseImage() {
// API
// #ifndef MP-WEIXIN
// H5使chooseImage
uni.chooseImage({
count: 9 - this.images.length,
sizeType: ['compressed'],
sourceType: ['album', 'camera'],
success: (res) => {
this.uploadImages(res.tempFilePaths);
},
fail: (err) => {
console.error('选择图片失败:', err);
}
});
// #endif
// #ifdef MP-WEIXIN
// 使chooseMedia
uni.chooseMedia({
count: 9 - this.images.length,
mediaType: ['image'],
sourceType: ['album', 'camera'],
sizeType: ['compressed'],
success: (res) => {
const tempFilePaths = res.tempFiles.map(file => file.tempFilePath);
this.uploadImages(tempFilePaths);
},
fail: (err) => {
console.error('选择图片失败:', err);
}
});
// #endif
},
async uploadImages(filePaths) {
for (let i = 0; i < filePaths.length; i++) {
try {
uni.showLoading({ title: '上传中...', mask: true });
const result = await request.uploadFile(filePaths[i], 'multipart', {}, {
params: {
model: 'app-repair',
pid: 10
}
});
this.images.push(result);
} catch (e) {
console.error('上传图片失败:', e);
uni.showToast({
title: '上传失败:' + (typeof e === 'string' ? e : '未知错误'),
icon: 'none'
});
} finally {
uni.hideLoading();
}
}
},
deleteImage(index) {
this.images.splice(index, 1);
},
async submitRepair() {
if (!this.selectedHouseId) {
uni.showToast({
@ -288,6 +403,13 @@ export default {
});
return;
}
if (!this.form.faultType) {
uni.showToast({
title: '请选择故障类型',
icon: 'none'
});
return;
}
if (!this.form.handleContent.trim()) {
uni.showToast({
title: '请填写报修内容',
@ -296,25 +418,53 @@ export default {
return;
}
//
const imagefiles = this.images.map(file => {
let attDir = file.url;
// /file/public/
if (attDir.startsWith('file/public/')) {
attDir = attDir.replace('file/public/', '')
} else if (attDir.startsWith('file/')) {
attDir = attDir.replace('file/', '')
}
return {
attId: file.id + '',
name: file.fileName,
attDir,
attSize: file.fileSize,
attType: file.type, //
fileName: file.fileName,
filePath: attDir,
url: attDir,
originalFileName: file.fileName,
};
});
const payload = {
phone: this.form.phone.trim(),
assigneContext: this.form.handleContent.trim(),
remark: this.form.expectedTime.trim(),
status: '待派单',
houseName: this.selectedHouseName
status: this.form.status || '0',
houseName: this.selectedHouseName,
files: imagefiles,
faultType: this.form.faultType
};
try {
uni.showLoading({ title: '提交中...', mask: true });
await createMaintenanceDispatch(payload);
await createMaintenanceOrder(payload);
uni.hideLoading();
uni.showToast({
title: '报修已提交',
icon: 'success'
});
this.form.phone = '';
this.form.handleContent = '';
this.form.expectedTime = '';
this.form.faultType = '';
this.faultTypeIndex = 0;
this.images = [];
this.page = 1;
this.records = [];
@ -348,8 +498,8 @@ export default {
page: this.page,
limit: this.limit
};
const res = await listMaintenanceDispatch(params);
const list = (res && (res.list || res.data || res.rows)) || [];
const res = await listMaintenanceOrder(params);
const list = res?.data?.list || [];
if (this.page === 1) {
this.records = list;
} else {
@ -368,18 +518,26 @@ export default {
}
},
statusText(status) {
if (!status) return '待处理';
if (status === '已完成' || status === '完成') return '已完成';
if (status === '待派单' || status === '待处理') return '待处理';
if (status === '处理中') return '处理中';
if (status === 0 || status === '0') return '待处理';
if (status === 1 || status === '1') return '处理中';
if (status === 2 || status === '2') return '已处理';
if (status === 99 || status === '99') return '已办结';
return status;
},
statusClass(status) {
const text = this.statusText(status);
if (text === '已完成') return 'status-done';
if (text === '已处理' || text === '已办结') return 'status-done';
if (text === '处理中') return 'status-doing';
if (text === '待处理') return 'status-pending';
return '';
},
getFaultTypeLabel(value) {
const types = this.dict.get('fault_type');
if (types) {
const type = types.find(t => t.dictValue === value);
return type ? type.dictLabel : value;
}
return value;
}
}
};
@ -527,6 +685,74 @@ export default {
color: #ccc;
}
}
.upload-section {
.upload-list {
display: flex;
flex-wrap: wrap;
gap: 20rpx;
.upload-item {
width: 180rpx;
height: 180rpx;
position: relative;
border-radius: 10rpx;
overflow: hidden;
border: 1rpx solid #e5e5e5;
image {
width: 100%;
height: 100%;
}
.delete-btn {
position: absolute;
top: 10rpx;
right: 10rpx;
width: 36rpx;
height: 36rpx;
background-color: rgba(0, 0, 0, 0.6);
color: #fff;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 28rpx;
line-height: 1;
}
}
.upload-btn {
width: 180rpx;
height: 180rpx;
border: 1rpx dashed #d9d9d9;
border-radius: 10rpx;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background-color: #fafafa;
.iconfont {
font-size: 40rpx;
color: #bfbfbf;
margin-bottom: 10rpx;
}
text {
font-size: 24rpx;
color: #999;
}
}
}
.hint {
display: block;
margin-top: 16rpx;
font-size: 20rpx;
color: #999;
}
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

@ -36,11 +36,16 @@ function baseRequest(url, method, data, {
if (store.state.app.token) header[TOKENNAME] = store.state.app.token;
return new Promise((reslove, reject) => {
const apiPrefix = useAdminUrl ? '/api/' : '/api/front/'
// 当使用admin URL时统一添加uid参数
let requestData = data || {};
if (useAdminUrl && store.state.app.uid) {
requestData.uid = store.state.app.uid;
}
uni.request({
url: Url + apiPrefix + url,
method: method || 'GET',
header: header,
data: data || {},
data: requestData,
success: (res) => {
if (noVerify)
reslove(res.data, res);
@ -65,6 +70,66 @@ const request = {};
request[method] = (api, data, opt, params) => baseRequest(api, method, data, opt || {}, params)
});
/**
* 文件上传
* @param {string} filePath 文件路径
* @param {string} name 文件名称
* @param {object} formData 其他表单数据
* @param {object} options 选项
* @returns {Promise}
*/
request.uploadFile = (filePath, name = 'file', formData = {}, options = {}) => {
return new Promise((resolve, reject) => {
const Url = HTTP_ADMIN_URL;
const apiPrefix = '/api/';
let uploadUrl = Url + apiPrefix + 'admin/upload/file';
let queryObj = {
uid: store.state.app.uid,
...(options.params || {})
}
if (queryObj) {
uploadUrl += '?' + new URLSearchParams(queryObj).toString();
}
// 添加token
const header = {};
if (store.state.app.token) {
header[TOKENNAME] = store.state.app.token;
}
const uploadTask = uni.uploadFile({
url: uploadUrl,
filePath: filePath,
name: name,
header: header,
formData: formData,
success: (res) => {
if (res.statusCode === 200) {
try {
const data = JSON.parse(res.data);
if (data.code === 200) {
resolve(data.data);
} else {
reject(data.message || '上传失败');
}
} catch (e) {
reject('上传失败,返回数据格式错误');
}
} else {
reject('上传失败HTTP状态码' + res.statusCode);
}
},
fail: (err) => {
reject('上传失败:' + (err.errMsg || '未知错误'));
}
});
// 支持进度回调
if (options.onProgressUpdate) {
uploadTask.onProgressUpdate(options.onProgressUpdate);
}
});
};
export default request;

Loading…
Cancel
Save