|
|
@@ -0,0 +1,482 @@
|
|
|
+<template>
|
|
|
+ <view class="appeal-container">
|
|
|
+ <!-- 自定义头部 -->
|
|
|
+ <view class="custom-header">
|
|
|
+ <view class="header-left" @click="navBack">
|
|
|
+ <image class="back-icon" src="/static/icons/chevron_right_dark.svg" style="transform: rotate(180deg);"></image>
|
|
|
+ </view>
|
|
|
+ <text class="header-title">增改服务项</text>
|
|
|
+ <view class="header-right"></view>
|
|
|
+ </view>
|
|
|
+ <view class="header-placeholder"></view>
|
|
|
+
|
|
|
+ <!-- 顶部提示 -->
|
|
|
+ <view class="banner-tip">
|
|
|
+ <view class="tip-content">
|
|
|
+ <text class="tip-title">服务变更申请</text>
|
|
|
+ <text class="tip-desc">如需在服务过程中增加或修改服务内容,请在此提交申请并上传相关凭证。</text>
|
|
|
+ </view>
|
|
|
+ <image class="banner-img" src="/static/icons/service-classification.svg"></image>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <view class="form-wrapper">
|
|
|
+ <!-- 选择服务项 -->
|
|
|
+ <view class="form-section">
|
|
|
+ <view class="section-label">
|
|
|
+ <text class="label-text">变更服务项</text>
|
|
|
+ <text class="required">*</text>
|
|
|
+ </view>
|
|
|
+ <picker @change="onServiceChange" :value="serviceIndex" :range="serviceOptions" range-key="name">
|
|
|
+ <view class="picker-box">
|
|
|
+ <text class="picker-value" :class="{ 'placeholder': serviceIndex === -1 }">
|
|
|
+ {{ serviceIndex === -1 ? '请选择需要变更的服务项' : serviceOptions[serviceIndex].name }}
|
|
|
+ </text>
|
|
|
+ <image class="arrow-icon" src="/static/icons/nav_arrow.svg"></image>
|
|
|
+ </view>
|
|
|
+ </picker>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 上传凭证 -->
|
|
|
+ <view class="form-section">
|
|
|
+ <view class="section-label">
|
|
|
+ <text class="label-text">图片凭证</text>
|
|
|
+ <text class="required">*</text>
|
|
|
+ <text class="label-sub">(最多9张)</text>
|
|
|
+ </view>
|
|
|
+ <view class="upload-grid">
|
|
|
+ <view class="upload-item" v-for="(img, index) in imageList" :key="index" @click="previewImage(index)">
|
|
|
+ <image :src="img" class="uploaded-img" mode="aspectFill"></image>
|
|
|
+ <view class="delete-btn" @click.stop="deleteImage(index)">×</view>
|
|
|
+ </view>
|
|
|
+ <view class="upload-btn" v-if="imageList.length < 9" @click="chooseImage">
|
|
|
+ <text class="plus">+</text>
|
|
|
+ <text class="upload-text">上传凭证</text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 变更说明 -->
|
|
|
+ <view class="form-section">
|
|
|
+ <view class="section-label">
|
|
|
+ <text class="label-text">变更说明</text>
|
|
|
+ <text class="label-sub">(选填)</text>
|
|
|
+ </view>
|
|
|
+ <view class="textarea-box">
|
|
|
+ <textarea
|
|
|
+ class="content-textarea"
|
|
|
+ v-model="description"
|
|
|
+ placeholder="请详细描述具体的变更内容或原因..."
|
|
|
+ maxlength="500"
|
|
|
+ auto-height
|
|
|
+ />
|
|
|
+ <text class="word-count">{{ description.length }}/500</text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 底部确认按钮 -->
|
|
|
+ <view class="bottom-action">
|
|
|
+ <button class="confirm-btn" :class="{ 'disabled': !isReady }" @click="submitAppeal">确认提交</button>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script>
|
|
|
+import { listAllService } from '@/api/service/list'
|
|
|
+import { uploadFile } from '@/api/fulfiller/app'
|
|
|
+import { addSubOrderAppeal } from '@/api/order/subOrderAppeal'
|
|
|
+
|
|
|
+export default {
|
|
|
+ data() {
|
|
|
+ return {
|
|
|
+ orderId: '',
|
|
|
+ serviceOptions: [],
|
|
|
+ serviceIndex: -1,
|
|
|
+ imageList: [],
|
|
|
+ imageOssIds: [],
|
|
|
+ description: ''
|
|
|
+ }
|
|
|
+ },
|
|
|
+ computed: {
|
|
|
+ isReady() {
|
|
|
+ // 必须要选服务且上传了至少一张图片
|
|
|
+ return this.serviceIndex !== -1 && this.imageList.length > 0;
|
|
|
+ }
|
|
|
+ },
|
|
|
+ async onLoad(options) {
|
|
|
+ if (options.id) {
|
|
|
+ this.orderId = options.id;
|
|
|
+ }
|
|
|
+ await this.fetchServices();
|
|
|
+ },
|
|
|
+ methods: {
|
|
|
+ // 补齐一个简单的时间格式化工具
|
|
|
+ formatNow() {
|
|
|
+ const now = new Date();
|
|
|
+ const pad = (n) => String(n).padStart(2, '0');
|
|
|
+ const year = now.getFullYear();
|
|
|
+ const month = pad(now.getMonth() + 1);
|
|
|
+ const day = pad(now.getDate());
|
|
|
+ const hours = pad(now.getHours());
|
|
|
+ const minutes = pad(now.getMinutes());
|
|
|
+ const seconds = pad(now.getSeconds());
|
|
|
+ return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
|
|
|
+ },
|
|
|
+ async fetchServices() {
|
|
|
+ try {
|
|
|
+ const res = await listAllService();
|
|
|
+ this.serviceOptions = res.data || [];
|
|
|
+ } catch (err) {
|
|
|
+ console.error('获取服务项失败:', err);
|
|
|
+ }
|
|
|
+ },
|
|
|
+ navBack() {
|
|
|
+ uni.navigateBack({ delta: 1 });
|
|
|
+ },
|
|
|
+ onServiceChange(e) {
|
|
|
+ this.serviceIndex = e.detail.value;
|
|
|
+ },
|
|
|
+ chooseImage() {
|
|
|
+ uni.chooseImage({
|
|
|
+ count: 9 - this.imageList.length,
|
|
|
+ sizeType: ['compressed'],
|
|
|
+ sourceType: ['album', 'camera'],
|
|
|
+ success: async (res) => {
|
|
|
+ for (const path of res.tempFilePaths) {
|
|
|
+ this.imageList.push(path);
|
|
|
+ try {
|
|
|
+ uni.showLoading({ title: '正在上传...', mask: true });
|
|
|
+ const uploadRes = await uploadFile(path);
|
|
|
+ if (uploadRes && uploadRes.data && uploadRes.data.ossId) {
|
|
|
+ this.imageOssIds.push(uploadRes.data.ossId);
|
|
|
+ }
|
|
|
+ uni.hideLoading();
|
|
|
+ } catch (err) {
|
|
|
+ uni.hideLoading();
|
|
|
+ console.error('上传凭证失败:', err);
|
|
|
+ uni.showToast({ title: '上传失败', icon: 'none' });
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ });
|
|
|
+ },
|
|
|
+ deleteImage(index) {
|
|
|
+ this.imageList.splice(index, 1);
|
|
|
+ this.imageOssIds.splice(index, 1);
|
|
|
+ },
|
|
|
+ previewImage(index) {
|
|
|
+ uni.previewImage({
|
|
|
+ urls: this.imageList,
|
|
|
+ current: index
|
|
|
+ });
|
|
|
+ },
|
|
|
+ async submitAppeal() {
|
|
|
+ // 再次检查校验条件
|
|
|
+ if (!this.isReady) {
|
|
|
+ if (this.serviceIndex === -1) {
|
|
|
+ uni.showToast({ title: '请先选择服务项', icon: 'none' });
|
|
|
+ } else if (this.imageList.length === 0) {
|
|
|
+ uni.showToast({ title: '请上传至少一张凭证', icon: 'none' });
|
|
|
+ }
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ uni.showLoading({ title: '提交中...', mask: true });
|
|
|
+
|
|
|
+ try {
|
|
|
+ const selectedService = this.serviceOptions[this.serviceIndex];
|
|
|
+ const nowStr = this.formatNow();
|
|
|
+
|
|
|
+ // 构造完整 JSON 入参
|
|
|
+ const data = {
|
|
|
+ "id": 0,
|
|
|
+ "orderId": this.orderId, // 保留原始类型(通常为 string 或 number)
|
|
|
+ "service": selectedService.id,
|
|
|
+ "photos": this.imageOssIds.join(','),
|
|
|
+ "fulfillmentCommission": selectedService.fulfillmentCommission || 0,
|
|
|
+ "reason": this.description || '无详细备注',
|
|
|
+ "createDept": 0,
|
|
|
+ "createBy": 0,
|
|
|
+ "createTime": nowStr,
|
|
|
+ "updateBy": 0,
|
|
|
+ "updateTime": nowStr,
|
|
|
+ "params": {}
|
|
|
+ };
|
|
|
+
|
|
|
+ console.log('即将发起 API 请求:', data);
|
|
|
+ const res = await addSubOrderAppeal(data);
|
|
|
+
|
|
|
+ uni.hideLoading();
|
|
|
+ if (res.code === 200 || res.msg === '操作成功') {
|
|
|
+ uni.showToast({ title: '提交成功,请等待后续处理', icon: 'none', duration: 2000 });
|
|
|
+ setTimeout(() => {
|
|
|
+ uni.navigateBack({ delta: 1 });
|
|
|
+ }, 2000);
|
|
|
+ } else {
|
|
|
+ uni.showToast({ title: res.msg || '提交异常,请稍后重试', icon: 'none', duration: 2000 });
|
|
|
+ }
|
|
|
+ } catch (err) {
|
|
|
+ uni.hideLoading();
|
|
|
+ console.error('提交失败详情:', err);
|
|
|
+ uni.showToast({ title: '网络请求失败,请检查网络链接', icon: 'none', duration: 2000 });
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped>
|
|
|
+page {
|
|
|
+ background-color: #F8F9FB;
|
|
|
+}
|
|
|
+
|
|
|
+.appeal-container {
|
|
|
+ min-height: 100vh;
|
|
|
+ padding-bottom: 180rpx;
|
|
|
+}
|
|
|
+
|
|
|
+/* 导航栏样式 */
|
|
|
+.custom-header {
|
|
|
+ position: fixed;
|
|
|
+ top: 0;
|
|
|
+ left: 0;
|
|
|
+ width: 100%;
|
|
|
+ height: 88rpx;
|
|
|
+ padding-top: var(--status-bar-height);
|
|
|
+ background-color: #fff;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: space-between;
|
|
|
+ padding-left: 30rpx;
|
|
|
+ padding-right: 30rpx;
|
|
|
+ box-sizing: content-box;
|
|
|
+ z-index: 100;
|
|
|
+}
|
|
|
+
|
|
|
+.header-placeholder {
|
|
|
+ height: calc(88rpx + var(--status-bar-height));
|
|
|
+}
|
|
|
+
|
|
|
+.back-icon {
|
|
|
+ width: 44rpx;
|
|
|
+ height: 44rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.header-title {
|
|
|
+ font-size: 32rpx;
|
|
|
+ font-weight: bold;
|
|
|
+ color: #333;
|
|
|
+}
|
|
|
+
|
|
|
+.header-right {
|
|
|
+ width: 44rpx;
|
|
|
+}
|
|
|
+
|
|
|
+/* Banner 提示 */
|
|
|
+.banner-tip {
|
|
|
+ background: linear-gradient(135deg, #FF9800 0%, #FF5722 100%);
|
|
|
+ margin: 30rpx;
|
|
|
+ border-radius: 20rpx;
|
|
|
+ padding: 30rpx;
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: center;
|
|
|
+ box-shadow: 0 10rpx 20rpx rgba(255, 87, 34, 0.2);
|
|
|
+}
|
|
|
+
|
|
|
+.tip-content {
|
|
|
+ flex: 1;
|
|
|
+}
|
|
|
+
|
|
|
+.tip-title {
|
|
|
+ color: #fff;
|
|
|
+ font-size: 32rpx;
|
|
|
+ font-weight: bold;
|
|
|
+ display: block;
|
|
|
+ margin-bottom: 10rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.tip-desc {
|
|
|
+ color: rgba(255, 255, 255, 0.9);
|
|
|
+ font-size: 24rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.banner-img {
|
|
|
+ width: 100rpx;
|
|
|
+ height: 100rpx;
|
|
|
+ opacity: 0.8;
|
|
|
+}
|
|
|
+
|
|
|
+/* 表单容器 */
|
|
|
+.form-wrapper {
|
|
|
+ margin: 0 30rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.form-section {
|
|
|
+ background-color: #fff;
|
|
|
+ border-radius: 20rpx;
|
|
|
+ padding: 30rpx;
|
|
|
+ margin-bottom: 24rpx;
|
|
|
+ box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.02);
|
|
|
+}
|
|
|
+
|
|
|
+.section-label {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ margin-bottom: 24rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.label-text {
|
|
|
+ font-size: 30rpx;
|
|
|
+ font-weight: bold;
|
|
|
+ color: #333;
|
|
|
+}
|
|
|
+
|
|
|
+.required {
|
|
|
+ color: #FF5722;
|
|
|
+ margin-left: 6rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.label-sub {
|
|
|
+ font-size: 24rpx;
|
|
|
+ color: #999;
|
|
|
+ margin-left: 10rpx;
|
|
|
+}
|
|
|
+
|
|
|
+/* 下拉选择框 */
|
|
|
+.picker-box {
|
|
|
+ background-color: #F8F9FA;
|
|
|
+ height: 96rpx;
|
|
|
+ padding: 0 30rpx;
|
|
|
+ border-radius: 12rpx;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: space-between;
|
|
|
+ border: 1px solid #EEEEEE;
|
|
|
+}
|
|
|
+
|
|
|
+.picker-value {
|
|
|
+ font-size: 28rpx;
|
|
|
+ color: #333;
|
|
|
+}
|
|
|
+
|
|
|
+.picker-value.placeholder {
|
|
|
+ color: #999;
|
|
|
+}
|
|
|
+
|
|
|
+.arrow-icon {
|
|
|
+ width: 24rpx;
|
|
|
+ height: 24rpx;
|
|
|
+ opacity: 0.3;
|
|
|
+}
|
|
|
+
|
|
|
+/* 图片上传网格 */
|
|
|
+.upload-grid {
|
|
|
+ display: flex;
|
|
|
+ flex-wrap: wrap;
|
|
|
+ gap: 18rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.upload-item, .upload-btn {
|
|
|
+ width: calc((100% - 36rpx) / 3);
|
|
|
+ height: 200rpx;
|
|
|
+ border-radius: 12rpx;
|
|
|
+ overflow: hidden;
|
|
|
+ position: relative;
|
|
|
+}
|
|
|
+
|
|
|
+.uploaded-img {
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+}
|
|
|
+
|
|
|
+.delete-btn {
|
|
|
+ position: absolute;
|
|
|
+ top: 10rpx;
|
|
|
+ right: 10rpx;
|
|
|
+ background-color: rgba(0, 0, 0, 0.5);
|
|
|
+ color: #fff;
|
|
|
+ width: 36rpx;
|
|
|
+ height: 36rpx;
|
|
|
+ border-radius: 50%;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ font-size: 24rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.upload-btn {
|
|
|
+ background-color: #F8F9FA;
|
|
|
+ border: 2rpx dashed #E0E0E0;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+}
|
|
|
+
|
|
|
+.plus {
|
|
|
+ font-size: 60rpx;
|
|
|
+ color: #CCCCCC;
|
|
|
+ line-height: 1;
|
|
|
+}
|
|
|
+
|
|
|
+.upload-text {
|
|
|
+ font-size: 22rpx;
|
|
|
+ color: #999;
|
|
|
+ margin-top: 8rpx;
|
|
|
+}
|
|
|
+
|
|
|
+/* 文本域 */
|
|
|
+.textarea-box {
|
|
|
+ background-color: #F8F9FA;
|
|
|
+ border-radius: 12rpx;
|
|
|
+ padding: 24rpx;
|
|
|
+ border: 1px solid #EEEEEE;
|
|
|
+}
|
|
|
+
|
|
|
+.content-textarea {
|
|
|
+ width: 100%;
|
|
|
+ min-height: 200rpx;
|
|
|
+ font-size: 28rpx;
|
|
|
+ color: #333;
|
|
|
+ line-height: 1.5;
|
|
|
+}
|
|
|
+
|
|
|
+.word-count {
|
|
|
+ text-align: right;
|
|
|
+ display: block;
|
|
|
+ font-size: 22rpx;
|
|
|
+ color: #BBB;
|
|
|
+ margin-top: 10rpx;
|
|
|
+}
|
|
|
+
|
|
|
+/* 底部按钮 */
|
|
|
+.bottom-action {
|
|
|
+ position: fixed;
|
|
|
+ bottom: 0;
|
|
|
+ left: 0;
|
|
|
+ right: 0;
|
|
|
+ background-color: #fff;
|
|
|
+ padding: 30rpx 40rpx;
|
|
|
+ padding-bottom: calc(30rpx + env(safe-area-inset-bottom));
|
|
|
+ box-shadow: 0 -10rpx 30rpx rgba(0, 0, 0, 0.05);
|
|
|
+ z-index: 99;
|
|
|
+}
|
|
|
+
|
|
|
+.confirm-btn {
|
|
|
+ background: linear-gradient(90deg, #FF9800 0%, #FF5722 100%);
|
|
|
+ color: #fff;
|
|
|
+ height: 88rpx;
|
|
|
+ line-height: 88rpx;
|
|
|
+ border-radius: 44rpx;
|
|
|
+ font-size: 32rpx;
|
|
|
+ font-weight: bold;
|
|
|
+ box-shadow: 0 8rpx 20rpx rgba(255, 87, 34, 0.3);
|
|
|
+}
|
|
|
+
|
|
|
+.confirm-btn.disabled {
|
|
|
+ background: #E0E0E0;
|
|
|
+ box-shadow: none;
|
|
|
+ color: #fff;
|
|
|
+}
|
|
|
+</style>
|