yusheng 1 месяц назад
Родитель
Сommit
cdbd9e9018

+ 16 - 7
api/recordRules/index.js

@@ -82,7 +82,7 @@ export async function getRecordRulesPlanDetail(id) {
 
 // /mes/recordrulesplan/revoke post 撤回
 export async function recordrulesplanRevoke(id) {
-  const res = await put(Vue.prototype.apiUrl + `/pcs/recordRulesPlan/revoke`, id);
+  const res = await putJ(Vue.prototype.apiUrl + `/pcs/recordRulesPlan/revoke`, id);
   if (res.code == 0) {
     return res.data;
   }
@@ -100,7 +100,7 @@ export async function producetaskrulerecordQueryRecordWorkOrderPage(params) {
 
 // /mes/recordrulesplan/manualDispatchOrder 手动派单
 export async function recordrulesplanManualDispatchOrder(body) {
-  const res = await put(Vue.prototype.apiUrl + `/pcs/recordRulesPlan/dispatchOrder`, body);
+  const res = await putJ(Vue.prototype.apiUrl + `/pcs/recordRulesPlan/dispatchOrder`, body);
   if (res.code == 0) {
     return res.data;
   }
@@ -133,9 +133,18 @@ export async function getById(id) {
   return Promise.reject(new Error(res.message));
 }
 
-export async function checkReportingWork(id) {
-  const res = await get(
-    Vue.prototype.apiUrl + `/pcs/recordrulesorder/checkReportingWork/${id}`
-  );
-  return res.code;
+export async function batchRevokeOrder(body) {
+  const res = await putJ(Vue.prototype.apiUrl + `/pcs/recordRulesPlan/revokeRepeatedly`, body);
+  if (res.code == 0) {
+    return res.data;
+  }
+  return Promise.reject(new Error(res.message));
 }
+//多次派单
+export async function dispatchRepeatedly(body) {
+  const res = await putJ(Vue.prototype.apiUrl + `/pcs/recordRulesPlan/dispatchRepeatedly`, body);
+  if (res.code == 0) {
+    return res.data;
+  }
+  return Promise.reject(new Error(res.message));
+}

+ 2 - 2
manifest.json

@@ -2,7 +2,7 @@
     "name" : "智慧工厂",
     "appid" : "__UNI__45B3907",
     "description" : "",
-    "versionName" : "V1.0.4.15",
+    "versionName" : "V1.0.4.16",
     "versionCode" : "100",
     "transformPx" : false,
     "h5" : {
@@ -12,7 +12,7 @@
                     // "target" : "http://192.168.1.110:18086/",
                     // "target" : "http://123.249.79.125/api/",
                     // "target" : "http://192.168.1.125:18086/",
-                    "target" : "http://192.168.1.251:18086/",
+                    "target" : "http://192.168.1.102:18086/",
                     // "target" : "http://110.41.182.105/api/",
                     // "target": "http://116.63.185.248:80/api",
                     // "target" : "http://192.168.1.251:18186/",

+ 383 - 0
pages/pcs/components/batchRevokeDialog.vue

@@ -0,0 +1,383 @@
+<template>
+	<u-popup :show="visible" mode="center" :round="20" :closeOnClickOverlay="false" :zIndex="99999"
+		@close="handleCancel" class="batch-revoke-popup">
+		<view class="popup-content">
+			<view class="popup-header">
+				<text class="popup-title">批量撤回工单</text>
+				<view class="close-btn" @click="handleCancel">×</view>
+			</view>
+
+
+
+
+
+			<!-- 工单列表(使用myCard) -->
+			<scroll-view class="popup-body" scroll-y @scrolltolower="loadMore">
+				<view v-for="(item, idx) in workOrderList" :key="item.id">
+					<myCard :item="item" :index="idx + 1" :columns="cardColumns" :title="item.code"
+						:status="getStatusText(item.status)" :showRadio="true" selectionMode="multiple"
+						:checkboxValue="selectedIds" @radioChange="onRadioChange"
+						@checkboxChange="onCheckboxChange" :showDetail="false" />
+				</view>
+				<view v-if="loading && workOrderList.length === 0" class="empty-tip">
+					<u-empty text="加载中..." mode="loading"></u-empty>
+				</view>
+				<view v-if="!loading && workOrderList.length === 0" class="empty-tip">
+					<u-empty text="暂无工单数据"></u-empty>
+				</view>
+				<view class="loadmore-text" v-if="!isEnd && workOrderList.length > 0">加载更多...</view>
+				<view class="loadmore-text" v-if="isEnd && workOrderList.length > 0">没有更多了</view>
+			</scroll-view>
+
+			<view class="popup-footer">
+				<u-button type="default" @click="handleCancel">取消</u-button>
+				<u-button type="primary" :loading="revokeLoading" @click="handleBatchRevoke">
+					批量撤回
+				</u-button>
+			</view>
+		</view>
+		<u-toast ref="uToast"></u-toast>
+	</u-popup>
+</template>
+
+<script>
+	import myCard from './myCard.vue';
+	import {
+		producetaskrulerecordQueryRecordWorkOrderPage,
+		batchRevokeOrder,
+	} from '@/api/recordRules/index';
+
+
+	export default {
+		components: {
+			myCard
+		},
+		props: {
+			pageName: {
+				type: String,
+				default: 'productionRecords',
+			},
+		},
+		data() {
+			return {
+				visible: false,
+				revokeLoading: false,
+				planCode: 0,
+
+
+				selectedIds: [], // 多选选中的id数组
+				workOrderList: [],
+				page: 1,
+				size: 10,
+				isEnd: false,
+				loading: false,
+				searchForm: {
+					keyword: '',
+					productLineId: '',
+					teamId: '',
+					status: '',
+				},
+				dateRange: [],
+				teamList: [],
+				productLineList: [],
+				statusList: [{
+						value: 0,
+						label: '未报工'
+					},
+					{
+						value: 1,
+						label: '执行中'
+					},
+					{
+						value: 2,
+						label: '已执行'
+					},
+				],
+				// myCard的列配置(与原来表格列对应)
+				cardColumns: [
+					[{
+						label: '计划单号',
+						prop: 'planCode',
+						className: 'perce100'
+					}],
+					[{
+						label: '记录规则名称',
+						prop: 'ruleName',
+						className: 'perce100'
+					}],
+					[{
+							label: '场站名称',
+							prop: 'productLineName',
+							className: 'perce50'
+						},
+						{
+							label: '班组',
+							prop: 'teamName',
+							className: 'perce50'
+						},
+					],
+					[{
+						label: '工单生成时间',
+						prop: 'createTime',
+						className: 'perce100'
+					}],
+					[{
+						label: '执行人',
+						prop: 'executeUsers',
+						className: 'perce100',
+						formatter: (row) => row.executeUsers?.map(i => i.userName).join(',') || '',
+					}],
+				],
+				planTypeList: {
+					productionRecords: 2,
+					steamInjectionInspectionRecord: 3,
+					solidWasteRecord: 4,
+					qualityTestRecords: 5,
+					QualityInspection: 7,
+					boilerOperationRecord: 6,
+					HaulSlag: 8,
+					TransportAsh: 9,
+				},
+			};
+		},
+		created() {
+			// this.loadTeamList();
+			// this.loadProductLineList();
+		},
+		methods: {
+			open(planCode) {
+				this.visible = true;
+				this.planCode = planCode;
+				this.selectedIds = [];
+				this.workOrderList = [];
+				this.page = 1;
+				this.isEnd = false;
+				this.searchForm = {
+					keyword: '',
+					productLineId: '',
+					teamId: '',
+					status: ''
+				};
+				this.dateRange = [];
+				this.loadWorkOrders();
+			},
+			setMode(mode) {
+				this.selectedIds = [];
+			},
+
+			onCheckboxChange({
+				checked,
+				item,
+			}) {
+				if (checked) {
+					this.selectedIds.push(item.id)
+				} else {
+					this.selectedIds = this.selectedIds.filter(id => id != item.id)
+				}
+
+
+			},
+
+			async loadWorkOrders() {
+				if (this.loading || this.isEnd) return;
+				this.loading = true;
+				try {
+					const params = {
+						pageNum: this.page,
+						size: this.size,
+						keyword: this.searchForm.keyword || this.planCode,
+						productLineId: this.searchForm.productLineId,
+						teamId: this.searchForm.teamId,
+						status: this.searchForm.status !== '' ? this.searchForm.status : 1,
+						planType: this.planTypeList[this.pageName],
+					};
+					if (this.dateRange && this.dateRange.length === 2) {
+						params.createTimeStart = this.dateRange[0];
+						params.createTimeEnd = this.dateRange[1];
+					}
+					const res = await producetaskrulerecordQueryRecordWorkOrderPage(params);
+					const list = res.list || [];
+					if (this.page === 1) {
+						this.workOrderList = list;
+					} else {
+						this.workOrderList.push(...list);
+					}
+					this.isEnd = list.length < this.size || (this.workOrderList.length >= (res.total || 0));
+					this.page++;
+				} catch (error) {
+					this.$refs.uToast.show({
+						type: 'error',
+						message: '加载工单失败'
+					});
+				} finally {
+					this.loading = false;
+				}
+			},
+			handleSearch() {
+				this.page = 1;
+				this.workOrderList = [];
+				this.isEnd = false;
+				this.loadWorkOrders();
+			},
+			loadMore() {
+				if (!this.isEnd && !this.loading) this.loadWorkOrders();
+			},
+			getStatusText(status) {
+				const map = {
+					0: '未报工',
+					1: '执行中',
+					2: '已执行'
+				};
+				return map[status] || '';
+			},
+			handleCancel() {
+				this.visible = false;
+				this.selectedIds = [];
+			},
+			async handleBatchRevoke() {
+			
+				const ids = this.selectedIds
+				if (ids.length === 0) {
+					this.$refs.uToast.show({
+						type: 'warning',
+						message: '请至少选择一个工单'
+					});
+					return;
+				}
+				this.revokeLoading = true;
+				try {
+					await batchRevokeOrder(ids);
+					this.$refs.uToast.show({
+						type: 'success',
+						message: `成功撤回 ${ids.length} 个工单`
+					});
+					this.visible = false;
+					this.$emit('refresh');
+				} catch (error) {
+					console.log(error)
+					this.$refs.uToast.show({
+						type: 'error',
+						message: '撤回失败,请重试'
+					});
+				} finally {
+					this.revokeLoading = false;
+				}
+			},
+		},
+	};
+</script>
+
+<style lang="scss" scoped>
+	.batch-revoke-popup {
+		.popup-content {
+			width: 90vw;
+			max-width: 700px;
+			height: 85vh;
+			background: #f5f7fb;
+			border-radius: 32rpx;
+			display: flex;
+			flex-direction: column;
+			overflow: hidden;
+		}
+
+		.popup-header {
+			display: flex;
+			justify-content: space-between;
+			align-items: center;
+			padding: 30rpx;
+			background: #fff;
+			border-bottom: 2rpx solid #eef2f6;
+
+			.popup-title {
+				font-size: 36rpx;
+				font-weight: bold;
+			}
+
+			.close-btn {
+				font-size: 60rpx;
+				color: #8e9aae;
+			}
+		}
+
+		.mode-switch {
+			display: flex;
+			background: #fff;
+			padding: 16rpx 30rpx;
+			border-bottom: 2rpx solid #eef2f6;
+
+			.mode-item {
+				flex: 1;
+				text-align: center;
+				font-size: 28rpx;
+				padding: 12rpx 0;
+				border-radius: 48rpx;
+				background: #f5f7fb;
+				color: #8e9aae;
+				margin-right: 20rpx;
+
+				&.active {
+					background: $theme-color;
+					color: #fff;
+				}
+
+				&:last-child {
+					margin-right: 0;
+				}
+			}
+		}
+
+		.search-wrapper {
+			background: #fff;
+			padding: 20rpx 30rpx;
+			border-bottom: 2rpx solid #eef2f6;
+
+			.search-form {
+				display: flex;
+				flex-wrap: wrap;
+				gap: 20rpx;
+
+				>* {
+					flex: 1;
+					min-width: 200rpx;
+				}
+
+				.search_btn {
+					width: 140rpx;
+					background: $theme-color;
+					color: #fff;
+					border-radius: 48rpx;
+				}
+			}
+		}
+
+		.popup-body {
+			flex: 1;
+			overflow-y: auto;
+			padding: 20rpx 24rpx;
+		}
+
+		.empty-tip {
+			margin-top: 100rpx;
+		}
+
+		.loadmore-text {
+			text-align: center;
+			font-size: 26rpx;
+			color: #aaa;
+			padding: 24rpx 0 40rpx;
+		}
+
+		.popup-footer {
+			display: flex;
+			padding: 20rpx 30rpx 30rpx;
+			border-top: 2rpx solid #eef2f6;
+			gap: 20rpx;
+			background: #fff;
+
+			.u-button {
+				flex: 1;
+				border-radius: 48rpx;
+			}
+		}
+	}
+</style>

+ 343 - 332
pages/pcs/components/myCard.vue

@@ -1,367 +1,378 @@
-<!-- myCard.vue(增强版,添加单选功能) -->
 <template>
-	<view class="card_container">
-		<view class="card_box">
-			<!-- 标题区域 -->
-			<view class="header_box" v-if="title">
-				<view class="title_left">
-					<view class="round" v-if="index">{{ index }}</view>
-					<view class="orderId" :style="{ marginLeft: index ? '16rpx' : '' }">{{
-						title
-					}}</view>
-				</view>
-				<!-- 状态标签 -->
-				<view class="status-tag" v-if="status">{{ status }}</view>
-				<!-- 单选按钮 -->
-				<view class="radio-tag" v-if="showRadio" @click.stop="handleRadioClick">
-					<u-radio-group v-model="radioValueModel" placement="row">
-						<u-radio :name="item.id" :iconSize="32" activeColor="#3c9cff"></u-radio>
-					</u-radio-group>
-				</view>
-			</view>
+  <view class="card_container">
+    <view class="card_box">
+      <!-- 标题区域 -->
+      <view class="header_box" v-if="title">
+        <view class="title_left">
+          <view class="round" v-if="index">{{ index }}</view>
+          <view class="orderId" :style="{ marginLeft: index ? '16rpx' : '' }">{{
+            title
+          }}</view>
+        </view>
+        <!-- 状态标签 -->
+        <view class="status-tag" v-if="status">{{ status }}</view>
+        <!-- 单选/多选控件 -->
+        <view class="select-tag" v-if="showRadio" @click.stop>
+          <!-- 单选模式:使用 u-radio-group(每个卡片独立,互斥) -->
+          <u-radio-group
+            v-if="selectionMode === 'single'"
+            v-model="radioValueModel"
+            placement="row"
+            @change="handleRadioChange"
+          >
+            <u-radio
+              :name="item.id"
+              :iconSize="44"
+              activeColor="#3c9cff"
+              :labelSize="0"
+            ></u-radio>
+          </u-radio-group>
+          <!-- 多选模式:使用独立 u-checkbox,由父组件控制选中状态 -->
+          <u-checkbox
+            v-else
+            :name="item.id"
+            :checked="isChecked"
+            :iconSize="44"
+            activeColor="#3c9cff"
+            :labelSize="0"
+            @change="handleCheckboxChange"
+          ></u-checkbox>
+        </view>
+      </view>
 
-			<view class="item_box rx-bc" v-for="(_item, i) in columns" :key="i">
-				<template v-for="val in _item">
-					<view class="perce50" :class="[val.className, { 'full-width': val.isFullWidth }]"
-						:style="val.style" :key="val.prop" v-if="!val.isNone">
-						<!-- 操作行 -->
-						<view class="item_one rx-sc" v-if="val.type == 'action'">
-							<view class="lable">{{ val.label }}</view>
-							<view class="text" style="flex-wrap: wrap">
-								<template v-for="(btn, bI) in btnList">
-									<u-button :plain="true" :hairline="true" size="mini" :type="btn.btnType"
-										v-if="judge(btn)" :text="btn.name" @click="action(btn)" :key="bI">
-									</u-button>
-								</template>
-							</view>
-						</view>
-						<!-- 普通信息行 -->
-						<view class="item_one rx-sc kk" v-else>
-							<view class="lable">{{ val.label }}</view>
-							<view class="text" :class="val.valueClass" v-if="val.formatter">{{
-								val.formatter(item) || ""
-							}}</view>
-							<view class="text" :class="val.valueClass" v-else-if="val.slot">
-								<slot :name="val.slot"></slot>
-							</view>
-							<view class="text" :class="val.valueClass" v-else>{{
-								item[val.prop] || ""
-							}}</view>
-						</view>
-					</view>
-				</template>
-			</view>
+      <!-- 内容区域(与原代码相同,省略...) -->
+      <view class="item_box rx-bc" v-for="(_item, i) in columns" :key="i">
+        <template v-for="val in _item">
+          <view
+            class="perce50"
+            :class="[val.className, { 'full-width': val.isFullWidth }]"
+            :style="val.style"
+            :key="val.prop"
+            v-if="!val.isNone"
+          >
+            <view class="item_one rx-sc" v-if="val.type == 'action'">
+              <view class="lable">{{ val.label }}</view>
+              <view class="text" style="flex-wrap: wrap">
+                <template v-for="(btn, bI) in btnList">
+                  <u-button
+                    :plain="true"
+                    :hairline="true"
+                    size="mini"
+                    :type="btn.btnType"
+                    v-if="judge(btn)"
+                    :text="btn.name"
+                    @click="action(btn)"
+                    :key="bI"
+                  ></u-button>
+                </template>
+              </view>
+            </view>
+            <view class="item_one rx-sc kk" v-else>
+              <view class="lable">{{ val.label }}</view>
+              <view class="text" :class="val.valueClass" v-if="val.formatter">{{
+                val.formatter(item) || ''
+              }}</view>
+              <view class="text" :class="val.valueClass" v-else-if="val.slot">
+                <slot :name="val.slot"></slot>
+              </view>
+              <view class="text" :class="val.valueClass" v-else>{{
+                item[val.prop] || ''
+              }}</view>
+            </view>
+          </view>
+        </template>
+      </view>
 
-			<!-- 查看详情 -->
-			<view class="footer-link" v-if="showDetail" @click="goDetail">
-				<text>查看详情</text>
-				<u-icon name="arrow-right" size="24" color="#999999"></u-icon>
-			</view>
-		</view>
-	</view>
+      <view class="footer-link" v-if="showDetail" @click="goDetail">
+        <text>查看详情</text>
+        <u-icon name="arrow-right" size="24" color="#999999"></u-icon>
+      </view>
+    </view>
+  </view>
 </template>
 
 <script>
-	export default {
-		props: {
-			btnList: {
-				type: Array,
-				default: () => []
-			},
-			item: {
-				type: Object,
-				default: () => ({})
-			},
-			columns: {
-				type: Array,
-				default: () => []
-			},
-			title: {
-				type: String,
-				default: ""
-			},
-			status: {
-				type: String,
-				default: ""
-			},
-			index: {
-				type: [String, Number],
-				default: ""
-			},
-			showDetail: {
-				type: Boolean,
-				default: true
-			},
-			// 新增:是否显示单选按钮
-			showRadio: {
-				type: Boolean,
-				default: false
-			},
-			// 新增:当前选中的值(父组件传入)
-			radioValue: {
-				type: [String, Number],
-				default: null
-			}
-		},
-		computed: {
-			judge() {
-				return (item) => {
-					if (item.judge) {
-						let is = true;
-						item.judge.forEach(({
-							key,
-							value,
-							authorities,
-							fn
-						}) => {
-							if (authorities) {
-								is = this.$isAuthorities(authorities);
-							}
-							if (value && !value.includes(this.item[key])) {
-								is = false;
-							}
-							if (fn) {
-								is = fn(this.item);
-							}
-						});
-						return is;
-					} else {
-						return true;
-					}
-				};
-			},
-			radioValueModel: {
-				get() {
-					return this.radioValue;
-				},
-				set(val) {
-					// 当单选值变化时触发事件
-					if (val === this.item.id) {
-						this.$emit('radioChange', this.item);
-					}
-				}
-			}
-		},
-		methods: {
-			action(item) {
-				if (item.type == 1) {
-					uni.navigateTo({
-						url: item.pageUrl + "?id=" + this.item.id + (item.query || ""),
-					});
-				} else {
-					this.$emit(item.apiName);
-				}
-			},
-			goDetail() {
-				this.$emit("goDetail", this.item);
-			},
-			handleRadioClick() {
-				// 触发选中事件,与radioChange保持一致
-				this.$emit('radioChange', this.item);
-			}
-		}
-	};
+export default {
+  props: {
+    btnList: { type: Array, default: () => [] },
+    item: { type: Object, default: () => ({}) },
+    columns: { type: Array, default: () => [] },
+    title: { type: String, default: '' },
+    status: { type: String, default: '' },
+    index: { type: [String, Number], default: '' },
+    showDetail: { type: Boolean, default: true },
+    showRadio: { type: Boolean, default: false },
+    radioValue: { type: [String, Number], default: null },
+    selectionMode: { type: String, default: 'single' }, // 'single' or 'multiple'
+    checkboxValue: { type: Array, default: () => [] },   // 父组件传入的选中ID数组
+  },
+  computed: {
+    judge() {
+      return (item) => {
+        if (item.judge) {
+          let is = true;
+          item.judge.forEach(({ key, value, authorities, fn }) => {
+            if (authorities) {
+              is = this.$isAuthorities(authorities);
+            }
+            if (value && !value.includes(this.item[key])) {
+              is = false;
+            }
+            if (fn) {
+              is = fn(this.item);
+            }
+          });
+          return is;
+        } else {
+          return true;
+        }
+      };
+    },
+    // 单选模式下的绑定值
+    radioValueModel: {
+      get() {
+        return this.radioValue;
+      },
+      set(val) {
+        if (val === this.item.id) {
+          this.$emit('radioChange', this.item);
+        }
+      },
+    },
+    // 多选模式下的选中状态(根据 checkboxValue 数组判断)
+    isChecked() {
+      return this.checkboxValue.includes(this.item.id);
+    },
+  },
+  methods: {
+    action(item) {
+      if (item.type == 1) {
+        uni.navigateTo({
+          url: item.pageUrl + '?id=' + this.item.id + (item.query || ''),
+        });
+      } else {
+        this.$emit(item.apiName);
+      }
+    },
+    goDetail() {
+      this.$emit('goDetail', this.item);
+    },
+    handleRadioChange(val) {
+      if (val === this.item.id) {
+        this.$emit('radioChange', this.item);
+      }
+    },
+    // 多选:点击 checkbox 时,通知父组件变更
+    handleCheckboxChange(e) {
+      this.$emit('checkboxChange', {
+        checked: e,        // e 为 boolean,表示当前 checkbox 是否被选中
+        item: this.item,
+      });
+    },
+  },
+};
 </script>
-
 <style lang="scss" scoped>
-	.card_container {
-		padding: 16rpx 0;
-	}
+.card_container {
+  padding: 16rpx 0;
+}
 
-	.card_box {
-		background: #ffffff;
-		border-radius: 24rpx;
-		padding: 24rpx;
-		box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.06);
-		position: relative;
+.card_box {
+  background: #ffffff;
+  border-radius: 24rpx;
+  padding: 24rpx;
+  box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.06);
+  position: relative;
 
-		.rx-bc {
-			display: flex;
-			align-items: flex-start;
-			flex-flow: row wrap;
+  .rx-bc {
+    display: flex;
+    align-items: flex-start;
+    flex-flow: row wrap;
 
-			>view {
-				margin-top: 24rpx;
+    > view {
+      margin-top: 24rpx;
 
-				&:first-child,
-				&:nth-child(2) {
-					margin-top: 0;
-				}
-			}
-		}
+      &:first-child,
+      &:nth-child(2) {
+        margin-top: 0;
+      }
+    }
+  }
 
-		.rx-sc {
-			display: flex;
-			align-items: flex-start;
-		}
+  .rx-sc {
+    display: flex;
+    align-items: flex-start;
+  }
 
-		.header_box {
-			display: flex;
-			justify-content: space-between;
-			align-items: center;
-			width: 100%;
-			position: relative;
-			background: linear-gradient(135deg, #eef5ff 0%, #f5f9ff 100%);
-			padding: 20rpx 24rpx;
-			border-radius: 20rpx 20rpx 0 0;
-			margin: -24rpx -24rpx 24rpx -24rpx;
-			width: calc(100% + 48rpx);
-			box-sizing: border-box;
+  .header_box {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    width: 100%;
+    position: relative;
+    background: linear-gradient(135deg, #eef5ff 0%, #f5f9ff 100%);
+    padding: 20rpx 24rpx;
+    border-radius: 20rpx 20rpx 0 0;
+    margin: -24rpx -24rpx 24rpx -24rpx;
+    width: calc(100% + 48rpx);
+    box-sizing: border-box;
 
-			.round {
-				width: 44rpx;
-				height: 44rpx;
-				line-height: 44rpx;
-				border-radius: 50%;
-				background: $theme-color;
-				color: #fff;
-				text-align: center;
-				font-size: 24rpx;
-				font-weight: 600;
-				flex-shrink: 0;
-			}
+    .round {
+      width: 44rpx;
+      height: 44rpx;
+      line-height: 44rpx;
+      border-radius: 50%;
+      background: $theme-color;
+      color: #fff;
+      text-align: center;
+      font-size: 24rpx;
+      font-weight: 600;
+      flex-shrink: 0;
+    }
 
-			.orderId {
-				color: #1f2b3c;
-				font-size: 30rpx;
-				font-weight: 600;
-				white-space: nowrap;
-				overflow: hidden;
-				text-overflow: ellipsis;
-			}
+    .orderId {
+      color: #1f2b3c;
+      font-size: 30rpx;
+      font-weight: 600;
+      white-space: nowrap;
+      overflow: hidden;
+      text-overflow: ellipsis;
+    }
 
-			.title_left {
-				display: flex;
-				align-items: center;
-				flex: 1;
-				padding-right: 120rpx;
-				overflow: hidden;
-			}
+    .title_left {
+      display: flex;
+      align-items: center;
+      flex: 1;
+      padding-right: 120rpx;
+      overflow: hidden;
+    }
 
-			.status-tag {
-				background: #e3f2fd;
-				color: #2196f3;
-				font-size: 24rpx;
-				padding: 6rpx 20rpx;
-				border-radius: 30rpx;
-				font-weight: 500;
-				position: absolute;
-				top: 50%;
-				transform: translateY(-50%);
-				right: 24rpx;
-				z-index: 1;
-			}
+    .status-tag {
+      background: #e3f2fd;
+      color: #2196f3;
+      font-size: 24rpx;
+      padding: 6rpx 20rpx;
+      border-radius: 30rpx;
+      font-weight: 500;
+      position: absolute;
+      top: 50%;
+      transform: translateY(-50%);
+      right: 24rpx;
+      z-index: 1;
+    }
 
-			.radio-tag {
-				position: absolute;
-				top: 50%;
-				transform: translateY(-50%);
-				right: 24rpx;
-				z-index: 2;
-				background: transparent;
+    .select-tag {
+      position: absolute;
+      top: 50%;
+      transform: translateY(-50%);
+      right: 24rpx;
+      z-index: 2;
+      background: transparent;
 
-				/deep/ .u-radio-group {
-					.u-radio {
-						padding: 0;
-						margin-right: 0;
-					}
-				}
-			}
-		}
+      /deep/ .u-radio-group,
+      /deep/ .u-checkbox-group {
+        .u-radio,
+        .u-checkbox {
+          padding: 0;
+          margin-right: 0;
+        }
+      }
+    }
+  }
 
-		.item_box {
-			.text {
-				display: flex;
-				flex: 1;
+  .item_box {
+    .text {
+      display: flex;
+      flex: 1;
 
-				uni-button:after {
-					border: none;
-				}
+      uni-button:after {
+        border: none;
+      }
 
-				uni-button {
-					width: 100rpx;
-					margin-left: 10rpx;
-					margin-right: 0;
-					color: #fff !important;
-					border: none;
-					background: #157a2c;
-					margin-top: 3px;
-				}
-			}
+      uni-button {
+        width: 100rpx;
+        margin-left: 10rpx;
+        margin-right: 0;
+        color: #fff !important;
+        border: none;
+        background: #157a2c;
+        margin-top: 3px;
+      }
+    }
 
-			.kk .text {
-				overflow: hidden;
-				text-overflow: ellipsis;
-				white-space: nowrap;
-				display: block;
-			}
+    .kk .text {
+      overflow: hidden;
+      text-overflow: ellipsis;
+      white-space: nowrap;
+      display: block;
+    }
 
-			.item_one {
-				width: 100%;
-				font-size: 28rpx;
-				line-height: 44rpx;
-				display: flex;
-				align-items: flex-start;
+    .item_one {
+      width: 100%;
+      font-size: 28rpx;
+      line-height: 44rpx;
+      display: flex;
+      align-items: flex-start;
 
-				.lable {
-					color: #8e9aae;
-					flex-shrink: 0;
-					margin-right: 16rpx;
-					font-size: 26rpx;
-				}
+      .lable {
+        color: #8e9aae;
+        flex-shrink: 0;
+        margin-right: 16rpx;
+        font-size: 26rpx;
+      }
 
-				.text {
-					color: #1f2b3c;
-					font-size: 28rpx;
-					flex: 1;
-					white-space: nowrap;
-					overflow: hidden;
-					text-overflow: ellipsis;
-					min-width: 0;
+      .text {
+        color: #1f2b3c;
+        font-size: 28rpx;
+        flex: 1;
+        white-space: nowrap;
+        overflow: hidden;
+        text-overflow: ellipsis;
+        min-width: 0;
 
-					&.highlight {
-						color: #4caf50;
-					}
-				}
-			}
+        &.highlight {
+          color: #4caf50;
+        }
+      }
+    }
 
-			.perce50 {
-				width: 50%;
-				box-sizing: border-box;
-				padding-right: 20rpx;
+    .perce50 {
+      width: 50%;
+      box-sizing: border-box;
+      padding-right: 20rpx;
 
-				&:nth-child(2n) {
-					padding-right: 0;
-					padding-left: 20rpx;
-				}
-			}
+      &:nth-child(2n) {
+        padding-right: 0;
+        padding-left: 20rpx;
+      }
+    }
 
-			.perce100 {
-				width: 100%;
-			}
+    .perce100 {
+      width: 100%;
+    }
 
-			.full-width {
-				width: 100% !important;
-				padding-left: 0 !important;
-				padding-right: 0 !important;
-			}
-		}
+    .full-width {
+      width: 100% !important;
+      padding-left: 0 !important;
+      padding-right: 0 !important;
+    }
+  }
 
-		.footer-link {
-			display: flex;
-			justify-content: center;
-			align-items: center;
-			margin-top: 24rpx;
-			padding-top: 20rpx;
-			border-top: 2rpx solid #f0f2f5;
-			color: #8e9aae;
-			font-size: 28rpx;
-			cursor: pointer;
-			gap: 8rpx;
+  .footer-link {
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    margin-top: 24rpx;
+    padding-top: 20rpx;
+    border-top: 2rpx solid #f0f2f5;
+    color: #8e9aae;
+    font-size: 28rpx;
+    cursor: pointer;
+    gap: 8rpx;
 
-			&:active {
-				opacity: 0.7;
-			}
-		}
-	}
+    &:active {
+      opacity: 0.7;
+    }
+  }
+}
 </style>

+ 23 - 6
pages/pcs/components/workOrderReport.vue

@@ -106,16 +106,19 @@
 
 								</view>
 							</view>
-							<view class="info-item" v-if="pageName == 'QualityInspection'||pageName=='TransportAsh'||pageName=='HaulSlag'">
-								<view class="info-label" style="display: flex;align-items: center;">{{pageName=='QualityInspection'?'车辆照片':'照片'}} <u-button
-										type="primary" style="width: 150rpx;margin-left: 10px;" size="small" text="拍照上传"
+							<view class="info-item"
+								v-if="pageName == 'QualityInspection'||pageName=='TransportAsh'||pageName=='HaulSlag'">
+								<view class="info-label" style="display: flex;align-items: center;">
+									{{pageName=='QualityInspection'?'车辆照片':'照片'}} <u-button type="primary"
+										style="width: 150rpx;margin-left: 10px;" size="small" text="拍照上传"
 										v-if="title != '详情'" @click="chooseImage"></u-button>
 								</view>
 
 							</view>
-					
+
 						</view>
-						<view style="padding-left: 30rpx;" v-if="pageName == 'QualityInspection'||pageName=='TransportAsh'||pageName=='HaulSlag'">
+						<view style="padding-left: 30rpx;"
+							v-if="pageName == 'QualityInspection'||pageName=='TransportAsh'||pageName=='HaulSlag'">
 
 							<AlbumDeletable :urls="getUrl(form.vehiclePhotoUrl)" :singleSize="170" :multipleSize="170"
 								:deletable="title != '详情'" @delete="handleDeleteImage"></AlbumDeletable>
@@ -481,6 +484,20 @@
 			async getOrderDetials(id) {
 				try {
 					const data = await getById(id);
+					let userInfo=uni.getStorageSync("userInfo")
+
+					if (data.type == 1) {
+						//班组报工默认单前登录人
+						data.executeUsers = [{
+							groupId:userInfo.groupId,
+							groupName: userInfo.groupName,
+							teamId: null,
+							teamName: '',
+							userId: userInfo.userId,
+							userName:userInfo.name
+						}];
+					}
+
 					data.detailList = data.detailList.map((i) => {
 						i.toolNames = i.tools.map((j) => j.toolName).join(",");
 						if (i.checkUsers && i.checkUsers.length > 0) {
@@ -609,7 +626,7 @@
 				uni.chooseImage({
 					count: 9, //默认9
 					sizeType: ['original', 'compressed'], //可以指定是原图还是压缩图,默认二者都有
-					sourceType: ['camera','album'], //从相册选择
+					sourceType: ['camera', 'album'], //从相册选择
 					success: function(res) {
 						uni.showLoading({
 							title: '加载中'

+ 378 - 44
pages/pcs/list.vue

@@ -5,12 +5,16 @@
 
 		<view class="top-wrapper">
 			<view class="tab-bar">
-				<view class="tab-item" :class="{ active: activeTab === 'mine' }" @click="switchTab('mine')">我的记录</view>
-				<view class="tab-item" :class="{ active: activeTab === 'all' }" @click="switchTab('all')">全部记录</view>
+				<view class="tab-item" :class="{ active: activeTab === 'mine' }" @click="switchTab('mine')">我的工单</view>
+				<view class="tab-item" :class="{ active: activeTab === 'all' }" @click="switchTab('all')">全部工单</view>
+				<view class="tab-item" :class="{ active: activeTab === 'plan' }" @click="switchTab('plan')"  >计划</view>
 			</view>
 			<uni-section>
-				<uni-easyinput prefixIcon="search" style="width: 460rpx" v-model="keyWord"
-					placeholder="工单单号,计划单号,记录规则名称">
+				<uni-easyinput prefixIcon="search" style="width: 460rpx" v-model="keyWord" :placeholder="
+            activeTab === 'plan'
+              ? '计划单号,计划名称,记录规则名称'
+              : '工单单号,计划单号,记录规则名称'
+          ">
 				</uni-easyinput>
 			</uni-section>
 			<button class="search_btn" @click="doSearch">搜索</button>
@@ -19,10 +23,20 @@
 		<view class="wrapper">
 			<u-list @scrolltolower="scrolltolower" class="listContent">
 				<view v-for="(item, index) in tableList" :key="index" style="position: relative">
-					<myCard :item="item" :index="index + 1" :btnList="activeTab === 'all' ? [] : btnList"
-						:columns="columns" :title="item.code" :status="statusMap[item.status]"
-						@goDetail="goDetail(item, 'view')" @openRedeployOther="openRedeployOther(item)"
-						@edit="goDetail(item, 'edit')">
+					<myCard :item="item" :index="index + 1" :btnList="
+              activeTab === 'all'
+                ? []
+                : activeTab === 'plan'
+                  ? planBtnList
+                  : btnList
+            " :columns="activeTab === 'plan' ? planColumns : columns" :title="item.code || item.planCode" :status="
+              activeTab === 'plan'
+                ? planStatusMap[item.status]
+                : statusMap[item.status]
+            " :showDetail="activeTab !== 'plan'" @goDetail="goDetail(item, 'view')"
+						@openRedeployOther="openRedeployOther(item)" @edit="goDetail(item, 'edit')"
+						@dispatch="handleDispatch(item)" @dispatchAll="handleDispatchAll(item)"
+						@revoke="handleRevoke(item)">
 					</myCard>
 				</view>
 				<view style="width: 100%; height: 40rpx"></view>
@@ -35,6 +49,26 @@
 		<u-toast ref="uToast"></u-toast>
 		<workOrderReport ref="workOrderReportRef" :pageName="pageName" @refresh="successInit"></workOrderReport>
 		<Assign ref="Assign" @refresh="successInit"></Assign>
+		<batchRevokeDialog ref="batchRevokeDialogRef" @refresh="successInit" :pageName="pageName"></batchRevokeDialog>
+
+		<u-popup :show="dispatchAllShow" mode="bottom" :round="20" @close="dispatchAllShow = false">
+			<view class="popup-content">
+				<view class="popup-header">
+					<text class="popup-title">批量派单</text>
+					<view class="close-btn" @click="dispatchAllShow = false">×</view>
+				</view>
+				<view class="popup-body">
+					<view class="form-item">
+						<text class="form-label">派单次数 <text class="required">*</text></text>
+						<uni-easyinput v-model="dispatchQuantity" type="number" placeholder="请输入派单次数" />
+					</view>
+				</view>
+				<view class="popup-footer">
+					<u-button type="default" @click="dispatchAllShow = false">取消</u-button>
+					<u-button type="primary" @click="submitDispatchAll">确定</u-button>
+				</view>
+			</view>
+		</u-popup>
 	</view>
 </template>
 
@@ -43,13 +77,20 @@
 	import myCard from "./components/myCard.vue";
 	import Assign from "./components/Assign.vue";
 	import workOrderReport from "./components/workOrderReport.vue";
+	import batchRevokeDialog from "./components/batchRevokeDialog.vue";
 	import {
-		producetaskrulerecordQueryRecordWorkOrderPage
+		producetaskrulerecordQueryRecordWorkOrderPage,
+		recordrulesplanPage,
+		recordrulesplanRevoke,
+		dispatchRepeatedly,
+		recordrulesplanManualDispatchOrder,
+		getRecordRulesPlanDetail
 	} from "@/api/recordRules/index";
 	export default {
 		components: {
 			myCard,
 			workOrderReport,
+			batchRevokeDialog,
 			Assign,
 		},
 		mixins: [dictMixns],
@@ -60,11 +101,20 @@
 		},
 		data() {
 			return {
+				dispatchAllData:null,
+				dispatchAllShow: false,
+				dispatchQuantity: "",
 				statusMap: {
 					0: "未报工",
 					1: "执行中",
 					2: "已执行",
 				},
+				planStatusMap: {
+					0: "未派单",
+					2: "执行中",
+					3: "已完成",
+					4: "已撤回",
+				},
 				planTypeList: {
 					productionRecords: 2,
 					steamInjectionInspectionRecord: 3,
@@ -73,27 +123,55 @@
 					boilerOperationRecord: 6,
 					QualityInspection: 7,
 					HaulSlag: 8,
-					TransportAsh: 9
+					TransportAsh: 9,
 				},
 				btnList: [{
-					name: "报工",
-					apiName: "edit",
-					btnType: "primary",
+						name: "报工",
+						apiName: "edit",
+						btnType: "primary",
 
-					judge: [{
-						key: "status",
-						value: [1],
-					}, ],
-				}, {
-					name: "转派",
-					apiName: "openRedeployOther",
-					btnType: "primary",
-
-					judge: [{
-						key: "status",
-						value: [1],
-					}, ],
-				}],
+						judge: [{
+							key: "status",
+							value: [1],
+						}, ],
+					},
+					{
+						name: "转派",
+						apiName: "openRedeployOther",
+						btnType: "primary",
+
+						judge: [{
+							key: "status",
+							value: [1],
+						}, ],
+					},
+				],
+				planBtnList: [{
+						name: "派单",
+						apiName: "dispatch",
+						btnType: "primary",
+						judge: [{
+							fn: (row) =>
+								row.dispatchType == 1 || (row.status != 2 && row.status != 3),
+						}, ],
+					},
+					{
+						name: "批量派单",
+						apiName: "dispatchAll",
+						btnType: "primary",
+						judge: [{
+							fn: (row) => row.dispatchType == 1,
+						}, ],
+					},
+					{
+						name: "撤回",
+						apiName: "revoke",
+						btnType: "warning",
+						judge: [{
+							fn: (row) => row.status == 2,
+						}, ],
+					},
+				],
 				columns: [
 					[{
 						label: "计划单号:",
@@ -136,7 +214,10 @@
 						prop: "executeUsers",
 						formatter: (row) => {
 							if ((row.type == 1 && row.status == 2) || row.type != 1) {
-								return row.executeUsers && row.executeUsers.map((i) => i.userName).join(',');
+								return (
+									row.executeUsers &&
+									row.executeUsers.map((i) => i.userName).join(",")
+								);
 							}
 						},
 					}, ],
@@ -144,21 +225,21 @@
 							label: "客户:",
 							prop: "contactName",
 							className: "perce100",
-							isNone: !['productionRecords', 'qualityTestRecords'].includes(
-								this.pageName
-							) || this.pageName == 'boilerOperationRecord'
+							isNone:
+								!["productionRecords", "qualityTestRecords"].includes(
+									this.pageName,
+								) || this.pageName == "boilerOperationRecord",
 						},
 						{
 							label: "供应商:",
 							prop: "supplierName",
 							className: "perce100",
 							isNone: [
-								'productionRecords',
-								'qualityTestRecords',
-								'boilerOperationRecord'
-							].includes(this.pageName)
+								"productionRecords",
+								"qualityTestRecords",
+								"boilerOperationRecord",
+							].includes(this.pageName),
 						},
-
 					],
 					[{
 						label: "操作:",
@@ -167,6 +248,82 @@
 						className: "perce100",
 					}, ],
 				].filter((item) => !item.isNone),
+				planColumns: [
+					[{
+						label: "计划名称:",
+						prop: "name",
+						className: "perce100",
+					}, ],
+					[{
+						label: "记录规则名称:",
+						prop: "ruleName",
+						className: "perce100",
+					}, ],
+					[{
+						label: "场站名称:",
+						prop: "productLineName",
+						className: "perce100",
+					}, ],
+					[{
+						label: "班组:",
+						prop: "teamName",
+						className: "perce100",
+					}, ],
+					[{
+						label: "部门:",
+						prop: "groupNames",
+						className: "perce100",
+						formatter: (row) => [
+							...new Set(
+								(row.executeUsers || [])
+								.map((i) => i.groupName)
+								.filter((i) => i),
+							),
+						].join(","),
+					}, ],
+					[{
+						label: "负责人:",
+						prop: "userNames",
+						className: "perce100",
+						formatter: (row) =>
+							(row.executeUsers || []).map((i) => i.userName).join(","),
+					}, ],
+					[{
+						label: "记录对象:",
+						prop: "recordObject",
+						className: "perce100",
+						formatter: (row) =>
+							row.associatedObject == 1 ? row.workshopName : row.deviceName,
+					}, ],
+					[{
+						label: "派单类型:",
+						prop: "dispatchTypeText",
+						className: "perce100",
+						formatter: (row) => (row.dispatchType ? "多次" : "单次"),
+					}, ],
+					[{
+						label: "状态:",
+						prop: "statusText",
+						className: "perce100",
+						formatter: (row) => this.planStatusMap[row.status] || "",
+					}, ],
+					[{
+						label: "创建人:",
+						prop: "createUserName",
+						className: "perce100",
+					}, ],
+					[{
+						label: "创建时间:",
+						prop: "createTime",
+						className: "perce100",
+					}, ],
+					[{
+						label: "操作:",
+						prop: "action",
+						type: "action",
+						className: "perce100",
+					}, ],
+				],
 
 				tableList: [],
 				page: 1,
@@ -174,7 +331,7 @@
 				isEnd: false,
 				keyWord: "",
 				userInfo: {},
-				activeTab: 'mine',
+				activeTab: "mine",
 			};
 		},
 		computed: {
@@ -186,9 +343,12 @@
 					this.pageName == "solidWasteRecord" ?
 					"固废记录" :
 					this.pageName == "qualityTestRecords" ?
-					"质量检查检测记录" : this.pageName == "HaulSlag" ?
-					"拉渣记录表" : this.pageName == "TransportAsh" ?
-					"拉灰记录表" : this.pageName == "QualityInspection" ?
+					"质量检查检测记录" :
+					this.pageName == "HaulSlag" ?
+					"拉渣记录表" :
+					this.pageName == "TransportAsh" ?
+					"拉灰记录表" :
+					this.pageName == "QualityInspection" ?
 					"来煤质检记录" :
 					"锅炉运行记录";
 			},
@@ -204,7 +364,7 @@
 				this.$refs.workOrderReportRef.open(item, type);
 			},
 			openRedeployOther(row) {
-				this.$refs.Assign.open(row)
+				this.$refs.Assign.open(row);
 			},
 			switchTab(tab) {
 				if (this.activeTab === tab) return;
@@ -214,18 +374,36 @@
 				this.tableList = [];
 				this.getList();
 			},
+
 			successInit() {
 				uni.showLoading({
 					title: "加载中",
 				});
 
+				if (this.activeTab === "plan") {
+					let data = {
+						pageNum: 1,
+						size: this.tableList.length,
+						keyword: this.keyWord,
+						planType: this.planTypeList[this.pageName],
+					};
+					recordrulesplanPage(data)
+						.then((res) => {
+							this.tableList = res.list;
+						})
+						.then(() => {
+							uni.hideLoading();
+						});
+					return;
+				}
+
 				let data = {
 					pageNum: 1,
 					size: this.tableList.length,
 					keyword: this.keyWord,
 					planType: this.planTypeList[this.pageName],
 				};
-				if (this.activeTab === 'mine') {
+				if (this.activeTab === "mine") {
 					data.currentLoginUserId = this.userInfo.userId;
 				}
 
@@ -252,13 +430,36 @@
 					title: "加载中",
 				});
 
+				if (this.activeTab === "plan") {
+					let data = {
+						pageNum: this.page,
+						size: this.size,
+						keyword: this.keyWord,
+						planType: this.planTypeList[this.pageName],
+					};
+					recordrulesplanPage(data)
+						.then((res) => {
+							if (this.page === 1) {
+								this.tableList = res.list;
+							} else {
+								this.tableList.push(...res.list);
+							}
+							this.page += 1;
+							this.isEnd = this.tableList.length >= res.count;
+						})
+						.then(() => {
+							uni.hideLoading();
+						});
+					return;
+				}
+
 				let data = {
 					pageNum: this.page,
 					size: this.size,
 					keyword: this.keyWord,
 					planType: this.planTypeList[this.pageName],
 				};
-				if (this.activeTab === 'mine') {
+				if (this.activeTab === "mine") {
 					data.currentLoginUserId = this.userInfo.userId;
 				}
 
@@ -282,6 +483,74 @@
 				}
 				this.getList();
 			},
+			async handleDispatch(item) {
+				try {
+					const data = await getRecordRulesPlanDetail(item.id)
+					await recordrulesplanManualDispatchOrder(data)
+					this.$refs.uToast.show({
+						type: "success",
+						icon: false,
+						message: `派单成功`,
+					});
+					this.successInit();
+				} catch (error) {
+					this.$refs.uToast.show({
+						type: "error",
+						icon: false,
+						message: `派单失败`,
+					});
+				}
+
+			},
+			async handleDispatchAll(item) {
+				this.dispatchAllData = await getRecordRulesPlanDetail(item.id)
+				this.dispatchQuantity = ""
+				this.dispatchAllShow = true
+			},
+			async submitDispatchAll() {
+				if (!this.dispatchQuantity || Number(this.dispatchQuantity) <= 0) {
+					this.$refs.uToast.show({
+						type: "error",
+						icon: false,
+						message: `请输入派单次数`,
+					});
+					return;
+				}
+				try {
+					const body = {
+						...this.dispatchAllData,
+						dispatchQuantity: Number(this.dispatchQuantity)
+					};
+					await dispatchRepeatedly(body);
+					this.$refs.uToast.show({
+						type: "success",
+						icon: false,
+						message: `批量派单成功`,
+					});
+					this.dispatchAllShow = false;
+					this.successInit();
+				} catch (error) {
+					this.$refs.uToast.show({
+						type: "error",
+						icon: false,
+						message: `批量派单失败`,
+					});
+				}
+			},
+			async revoke(id) {
+				await recordrulesplanRevoke({
+					id
+				});
+				this.successInit();
+			},
+			handleRevoke(item) {
+				if (item.dispatchType == 1) {
+					// 多次派单,打开批量撤回弹窗
+					this.$refs.batchRevokeDialogRef.open(item.code);
+				} else {
+					this.revoke(item.id);
+				}
+			},
 		},
 	};
 </script>
@@ -324,7 +593,7 @@
 				font-weight: bold;
 
 				&::after {
-					content: '';
+					content: "";
 					position: absolute;
 					bottom: 0;
 					left: 50%;
@@ -363,4 +632,69 @@
 			margin-left: 14rpx;
 		}
 	}
+
+	.popup-content {
+		width: 100vw;
+		height: 60vh;
+		background: #fff;
+		border-radius: 20rpx 20rpx 0 0;
+		display: flex;
+		flex-direction: column;
+	}
+
+	.popup-header {
+		display: flex;
+		justify-content: space-between;
+		align-items: center;
+		padding: 30rpx;
+		border-bottom: 1rpx solid #e5e5e5;
+
+		.popup-title {
+			font-size: 36rpx;
+			font-weight: bold;
+			color: #333;
+		}
+
+		.close-btn {
+			width: 60rpx;
+			height: 60rpx;
+			display: flex;
+			align-items: center;
+			justify-content: center;
+			font-size: 60rpx;
+			color: #999;
+			line-height: 1;
+		}
+	}
+
+	.popup-body {
+		flex: 1;
+		padding: 28rpx;
+	}
+
+	.form-item {
+		margin-bottom: 24rpx;
+
+		.form-label {
+			font-size: 28rpx;
+			color: #333;
+			margin-bottom: 16rpx;
+			display: block;
+
+			.required {
+				color: #ff4d4f;
+			}
+		}
+	}
+
+	.popup-footer {
+		display: flex;
+		padding: 20rpx 30rpx;
+		border-top: 1rpx solid #e5e5e5;
+		gap: 20rpx;
+
+		/deep/ .u-button {
+			flex: 1;
+		}
+	}
 </style>