Bläddra i källkod

需求二次整改基本完成

Huanyi 2 veckor sedan
förälder
incheckning
397c5ed3b7

+ 15 - 0
api/drawing/drawingFile.js

@@ -0,0 +1,15 @@
+import request from '@/utils/request';
+
+/**
+ * 根据图纸代码批量查询JPG简图
+ * @param {string} nums 逗号分隔的图纸代码字符串,如 "F001,F002"
+ * @returns {Promise} data 为 { FNum: base64, ... } 的映射
+ * @Author: Trae
+ */
+export function getImagesByNums(nums) {
+  return request({
+    url: '/drawing/file/imageByNums',
+    method: 'GET',
+    params: { nums }
+  });
+}

+ 5 - 4
api/erp/color.js

@@ -1,12 +1,13 @@
 import request from '@/utils/request';
 
 /**
- * 分页获取 ERP 颜色列表
- * @Author: Antigravity
+ * 查询 ERP 颜色列表(按父级ID分级)
+ * parentRowId 为空时查第一级,非空时查下级
+ * @Author: Trae
  */
-export function listPageColor(query) {
+export function listColor(query) {
   return request({
-    url: '/erp/color/page',
+    url: '/erp/color/list',
     method: 'GET',
     params: query
   });

+ 4 - 4
api/erp/colorKind.js

@@ -1,12 +1,12 @@
 import request from '@/utils/request';
 
 /**
- * 分页获取颜色种类列表
- * @Author: Antigravity
+ * 获取颜色种类列表(按父级ID分级查询)
+ * @Author: Trae
  */
-export function listPageColorKind(query) {
+export function listColorKind(query) {
   return request({
-    url: '/erp/colorKind/page',
+    url: '/erp/colorKind/list',
     method: 'GET',
     params: query
   });

+ 5 - 4
api/erp/model.js

@@ -1,12 +1,13 @@
 import request from '@/utils/request';
 
 /**
- * 分页获取型材档案列表
- * @Author: Antigravity
+ * 查询型材档案列表(按父级ID分级)
+ * parentRowId 为空时查第一级,非空时查下级
+ * @Author: Trae
  */
-export function listPageModel(query) {
+export function listModel(query) {
   return request({
-    url: '/erp/model/page',
+    url: '/erp/model/list',
     method: 'GET',
     params: query
   });

+ 13 - 0
api/erp/modelKind.js

@@ -0,0 +1,13 @@
+import request from '@/utils/request';
+
+/**
+ * 获取型号类别列表(按父级ID分级查询)
+ * @Author: Trae
+ */
+export function listModelKind(query) {
+  return request({
+    url: '/erp/modelKind/list',
+    method: 'GET',
+    params: query
+  });
+}

+ 12 - 0
api/erp/orderDetail.js

@@ -23,3 +23,15 @@ export function addOrderDetail(data) {
     data: data
   })
 }
+
+/**
+ * 批量修改订单详情(表面处理 / 包装方式)
+ * @Author: Trae
+ */
+export function batchUpdateOrderDetail(data) {
+  return request({
+    url: '/erp/orderDetail/batchUpdate',
+    method: 'put',
+    data: data
+  })
+}

+ 16 - 0
api/system/complaint.js

@@ -6,3 +6,19 @@ import request from '@/utils/request';
 export function submitComplaint(data) {
 	return request({ url: '/system/complaint', method: 'POST', data });
 }
+
+/**
+ * 查询当前客户的投诉建议列表(小程序端)
+ * @Author: Trae
+ */
+export function getMyComplaintList(params) {
+	return request({ url: '/system/complaint/myList', method: 'GET', data: params });
+}
+
+/**
+ * 查询投诉建议详情
+ * @Author: Trae
+ */
+export function getComplaintDetail(id) {
+	return request({ url: `/system/complaint/detail/${id}`, method: 'GET' });
+}

+ 8 - 0
pages.json

@@ -48,6 +48,14 @@
 			"path": "pages/mine/complaint/index",
 			"style": { "navigationStyle": "custom" }
 		},
+		{
+			"path": "pages/mine/complaint/submit/index",
+			"style": { "navigationStyle": "custom" }
+		},
+		{
+			"path": "pages/mine/complaint/detail/index",
+			"style": { "navigationStyle": "custom" }
+		},
 		{
 			"path": "pages/mine/index",
 			"style": { "navigationStyle": "custom" }

+ 1 - 1
pages/index/index.vue

@@ -1,6 +1,6 @@
 <template>
 	<view class="home-page">
-		<erp-nav-bar title="华型材" :show-back="false" />
+		<erp-nav-bar title="华型材" :show-back="false" />
 		<scroll-view class="main-scroll" scroll-y :show-scrollbar="false" :enhanced="true">
 			<!-- 1. 灵动大屏轮播 (无缝全屏感) -->
 			<view class="hero-section">

+ 268 - 0
pages/mine/complaint/detail/index.vue

@@ -0,0 +1,268 @@
+<template>
+	<view class="complaint-detail-root">
+		<erp-nav-bar title="反馈详情" />
+
+		<scroll-view scroll-y class="scroll-view-container" :show-scrollbar="false">
+			<view class="detail-body" v-if="detail">
+				<view class="section-card">
+					<view class="section-title">反馈信息</view>
+					<view class="info-row">
+						<text class="info-label">反馈类型</text>
+						<text class="info-value type-tag">{{ detail.feedbackTypeLabel }}</text>
+					</view>
+					<view class="info-row">
+						<text class="info-label">反馈时间</text>
+						<text class="info-value">{{ detail.createTime }}</text>
+					</view>
+				</view>
+
+				<view class="section-card">
+					<view class="section-title">反馈内容</view>
+					<view class="content-block">{{ detail.content }}</view>
+				</view>
+
+				<view class="section-card" v-if="detail.imageUrls && detail.imageUrls.length > 0">
+					<view class="section-title">上传图片</view>
+					<view class="upload-grid">
+						<view class="img-item" v-for="(url, index) in detail.imageUrls" :key="index"
+							@click="previewImage(index)">
+							<image :src="url" mode="aspectFill"></image>
+						</view>
+					</view>
+				</view>
+
+				<view class="section-card">
+					<view class="section-title">处理状态</view>
+					<view class="status-block">
+						<view class="status-dot" :class="detail.status === '1' ? 'done' : 'pending'"></view>
+						<text class="status-label">{{ detail.status === '1' ? '已处理' : '待处理' }}</text>
+					</view>
+				</view>
+
+				<view class="section-card" v-if="detail.status === '1'">
+					<view class="section-title">处理结果</view>
+					<view class="content-block">{{ detail.dealResult || '暂无处理结果说明' }}</view>
+				</view>
+
+				<view class="section-card"
+					v-if="detail.status === '1' && detail.dealImageUrls && detail.dealImageUrls.length > 0">
+					<view class="section-title">处理凭证</view>
+					<view class="upload-grid">
+						<view class="img-item" v-for="(url, index) in detail.dealImageUrls" :key="'deal-' + index"
+							@click="previewDealImage(index)">
+							<image :src="url" mode="aspectFill"></image>
+						</view>
+					</view>
+				</view>
+
+				<view class="bottom-placeholder"></view>
+			</view>
+
+			<view class="loading-state" v-if="!detail && loading">
+				<text>加载中...</text>
+			</view>
+		</scroll-view>
+	</view>
+</template>
+
+<script>
+import ErpNavBar from '@/components/erp-nav-bar.vue';
+import { getComplaintDetail } from '@/api/system/complaint.js';
+import { getDictByType } from '@/api/system/dict.js';
+
+export default {
+	components: { ErpNavBar },
+	data() {
+		return {
+			id: '',
+			loading: false,
+			detail: null,
+			typeMap: {}
+		}
+	},
+	onLoad(options) {
+		this.id = options.id || '';
+		this.loadDict().then(() => this.loadDetail());
+	},
+	methods: {
+		goBack() { uni.navigateBack(); },
+		async loadDict() {
+			try {
+				const res = await getDictByType('sys_complaint_type');
+				if (res && res.data) {
+					const map = {};
+					res.data.forEach(d => { map[d.dictValue] = d.dictLabel; });
+					this.typeMap = map;
+				}
+			} catch (e) { /* 忽略字典加载失败 */ }
+		},
+		async loadDetail() {
+			if (!this.id) return;
+			this.loading = true;
+			try {
+				const res = await getComplaintDetail(this.id);
+				const data = res.data || res;
+				if (data) {
+					this.detail = {
+						id: data.id,
+						feedbackType: data.feedbackType,
+						feedbackTypeLabel: this.typeMap[data.feedbackType] || data.feedbackType || '未知类型',
+						content: data.content,
+						status: data.status || '0',
+						createTime: data.createTime,
+						imageUrls: data.imageUrls ? data.imageUrls.split(',').filter(u => u) : [],
+						dealResult: data.dealResult || '',
+						dealImageUrls: data.dealImageUrls ? data.dealImageUrls.split(',').filter(u => u) : []
+					};
+				}
+			} catch (e) {
+				console.error('加载反馈详情失败', e);
+				uni.showToast({ title: e || '加载反馈详情失败', icon: 'none' });
+			} finally {
+				this.loading = false;
+			}
+		},
+		previewImage(index) {
+			uni.previewImage({
+				urls: this.detail.imageUrls,
+				current: index
+			});
+		},
+		previewDealImage(index) {
+			uni.previewImage({
+				urls: this.detail.dealImageUrls,
+				current: index
+			});
+		}
+	}
+}
+</script>
+
+<style scoped>
+.complaint-detail-root {
+	width: 100vw;
+	height: 100vh;
+	background: #f8fafb;
+	display: flex;
+	flex-direction: column;
+	overflow: hidden;
+}
+
+.scroll-view-container {
+	flex: 1;
+	height: 0;
+	width: 100%;
+}
+
+.detail-body {
+	padding: 30rpx;
+}
+
+.section-card {
+	background: #fff;
+	border-radius: 24rpx;
+	padding: 40rpx 30rpx;
+	margin-bottom: 30rpx;
+	box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.02);
+}
+
+.section-title {
+	font-size: 30rpx;
+	font-weight: bold;
+	color: #1a1a1a;
+	margin-bottom: 30rpx;
+	border-left: 8rpx solid #C1001C;
+	padding-left: 20rpx;
+	line-height: 1.2;
+}
+
+.info-row {
+	display: flex;
+	justify-content: space-between;
+	align-items: center;
+	padding: 16rpx 0;
+}
+
+.info-label {
+	font-size: 28rpx;
+	color: #999;
+}
+
+.info-value {
+	font-size: 28rpx;
+	color: #333;
+}
+
+.type-tag {
+	color: #C1001C;
+	font-weight: bold;
+}
+
+.content-block {
+	font-size: 28rpx;
+	color: #333;
+	line-height: 1.8;
+	white-space: pre-wrap;
+	word-break: break-all;
+}
+
+.upload-grid {
+	display: grid;
+	grid-template-columns: repeat(3, 1fr);
+	gap: 20rpx;
+}
+
+.img-item {
+	position: relative;
+	width: 100%;
+	padding-top: 100%;
+	border-radius: 16rpx;
+	overflow: hidden;
+}
+
+.img-item image {
+	position: absolute;
+	top: 0;
+	left: 0;
+	width: 100%;
+	height: 100%;
+}
+
+.status-block {
+	display: flex;
+	align-items: center;
+}
+
+.status-dot {
+	width: 16rpx;
+	height: 16rpx;
+	border-radius: 50%;
+	margin-right: 16rpx;
+}
+
+.status-dot.pending {
+	background: #ff9800;
+}
+
+.status-dot.done {
+	background: #4caf50;
+}
+
+.status-label {
+	font-size: 28rpx;
+	font-weight: bold;
+	color: #333;
+}
+
+.bottom-placeholder {
+	height: 40rpx;
+}
+
+.loading-state {
+	padding-top: 200rpx;
+	display: flex;
+	justify-content: center;
+	font-size: 28rpx;
+	color: #999;
+}
+</style>

+ 168 - 266
pages/mine/complaint/index.vue

@@ -1,160 +1,129 @@
 <template>
-	<view class="complaint-root">
+	<view class="complaint-list-root">
 		<erp-nav-bar title="投诉与建议" />
 
-		<view class="scroll-container">
-			<scroll-view scroll-y class="scroll-content" :show-scrollbar="false">
-				<view class="form-body">
-					<!-- 2. 类型选择 -->
-					<view class="section-card">
-						<view class="section-title">反馈类型</view>
-						<view class="type-grid">
-							<view class="type-item" v-for="item in types" :key="item.value"
-								:class="{ active: formData.type === item.value }" @click="formData.type = item.value">
-								<text>{{ item.label }}</text>
-							</view>
+		<scroll-view scroll-y class="list-scroll-view" :show-scrollbar="false" @scrolltolower="onReachEnd">
+			<view class="list-inner">
+				<view class="complaint-card" v-for="item in displayList" :key="item.id" @click="goDetail(item)">
+					<view class="card-header">
+						<view class="type-badge">{{ item.feedbackTypeLabel }}</view>
+						<view class="status-text" :class="item.status === '1' ? 'done' : 'pending'">
+							{{ item.status === '1' ? '已处理' : '待处理' }}
 						</view>
 					</view>
-
-					<!-- 3. 问题描述 -->
-					<view class="section-card">
-						<view class="section-title">反馈内容</view>
-						<textarea class="content-input" v-model="formData.content" placeholder="请详细描述您遇到的问题或改进建议..."
-							maxlength="500"></textarea>
-						<view class="word-count">{{ formData.content.length }}/500</view>
+					<view class="card-content">
+						<text class="content-text">{{ item.content }}</text>
+					</view>
+					<view class="card-footer">
+						<text class="time-text">{{ item.createTime }}</text>
+						<view class="arrow-icon"></view>
 					</view>
+				</view>
 
-					<!-- 4. 图片上传 -->
-					<view class="section-card">
-						<view class="section-title">上传图片 (最多6张)</view>
-						<view class="upload-grid">
-							<view class="img-item" v-for="(img, index) in formData.images" :key="index">
-								<image :src="img" mode="aspectFill" @click="previewImage(index)"></image>
-								<view class="del-btn" @click.stop="removeImage(index)">
-									<view class="close-icon">×</view>
-								</view>
-							</view>
-							<view class="add-btn" v-if="formData.images.length < 6" @click="chooseImage">
-								<text class="add-icon">+</text>
-								<text class="add-txt">添加图片</text>
-							</view>
-						</view>
+				<view class="list-status-info" v-if="displayList.length > 0">
+					<view class="loading-wrap" v-if="loading">
+						<text>加载中...</text>
+					</view>
+					<view class="nomore-wrap" v-if="noMore">
+						<text class="nomore-text">没有更多了</text>
 					</view>
 				</view>
 
-				<view class="bottom-placeholder"></view>
-			</scroll-view>
-		</view>
+				<view class="empty-state" v-if="displayList.length === 0 && !loading">
+					<image src="https://img.icons8.com/clouds/200/comments.png" mode="aspectFit"></image>
+					<text class="empty-txt">暂无反馈记录</text>
+				</view>
+
+				<view class="safe-bottom"></view>
+			</view>
+		</scroll-view>
 
-		<!-- 5. 底部提交按钮 -->
 		<view class="footer-bar">
-			<button class="submit-btn" :disabled="!isFormValid" @click="handleSubmit">提交反馈</button>
+			<button class="submit-btn" @click="goSubmit">提交反馈</button>
 		</view>
 	</view>
 </template>
 
 <script>
 import ErpNavBar from '@/components/erp-nav-bar.vue';
-import { submitComplaint } from '@/api/system/complaint.js';
+import { getMyComplaintList } from '@/api/system/complaint.js';
 import { getDictByType } from '@/api/system/dict.js';
-import { uploadFile } from '@/api/resource/oss.js';
+
 export default {
 	components: { ErpNavBar },
 	data() {
 		return {
-			types: [],
-			uploading: false,
-			imageOssMap: [],
-			formData: {
-				type: '',
-				content: '',
-				images: []
-			}
-		}
-	},
-	computed: {
-		isFormValid() {
-			return !this.uploading && this.formData.type && this.formData.content && this.formData.content.trim().length >= 1;
+			loading: false,
+			noMore: false,
+			pageNum: 1,
+			displayList: [],
+			typeMap: {}
 		}
 	},
 	onLoad() {
-		this.loadTypes();
+		this.loadDict();
+		this.refresh();
+	},
+	onShow() {
+		if (this.displayList.length > 0) {
+			this.refresh();
+		}
 	},
 	methods: {
-		async loadTypes() {
+		async loadDict() {
 			try {
 				const res = await getDictByType('sys_complaint_type');
 				if (res && res.data) {
-					this.types = res.data.map(d => ({ label: d.dictLabel, value: d.dictValue }));
-					if (this.types.length) this.formData.type = this.types[0].value;
+					const map = {};
+					res.data.forEach(d => { map[d.dictValue] = d.dictLabel; });
+					this.typeMap = map;
 				}
-			} catch (e) {
-				uni.showToast({ title: e || '加载反馈类型失败', icon: 'none' });
-				this.types = [
-					{ label: '系统投诉', value: 'complaint' },
-					{ label: '改进建议', value: 'suggestion' },
-					{ label: '其他反馈', value: 'other' }
-				];
-				this.formData.type = 'complaint';
-			}
+			} catch (e) { /* 忽略字典加载失败 */ }
 		},
 		goBack() { uni.navigateBack(); },
-		chooseImage() {
-			const count = 6 - this.formData.images.length;
-			uni.chooseImage({
-				count,
-				sizeType: ['compressed'],
-				success: async (res) => {
-					const paths = res.tempFilePaths;
-					this.uploading = true;
-					try {
-						for (const path of paths) {
-							const preview = path;
-							const placeholderIndex = this.formData.images.length;
-							this.formData.images.push(preview);
-							this.imageOssMap.push(null);
-							try {
-								const uploadRes = await uploadFile(path);
-								this.formData.images.splice(placeholderIndex, 1, uploadRes.url);
-								this.imageOssMap.splice(placeholderIndex, 1, uploadRes.ossId);
-							} catch (err) {
-								this.formData.images.splice(placeholderIndex, 1);
-								this.imageOssMap.splice(placeholderIndex, 1);
-								uni.showToast({ title: err || '图片上传失败', icon: 'none' });
-							}
-						}
-					} finally {
-						this.uploading = false;
-					}
-				}
-			});
+		goSubmit() {
+			uni.navigateTo({ url: '/pages/mine/complaint/submit/index' });
+		},
+		goDetail(item) {
+			uni.navigateTo({ url: `/pages/mine/complaint/detail/index?id=${item.id}` });
 		},
-		removeImage(index) {
-			this.formData.images.splice(index, 1);
-			this.imageOssMap.splice(index, 1);
+		refresh() {
+			this.displayList = [];
+			this.noMore = false;
+			this.pageNum = 1;
+			this.loadData();
 		},
-		previewImage(index) {
-			uni.previewImage({
-				urls: this.formData.images,
-				current: index
-			});
+		onReachEnd() {
+			if (!this.loading && !this.noMore) this.loadData();
 		},
-		async handleSubmit() {
-			if (!this.isFormValid) return;
+		async loadData() {
+			if (this.loading || this.noMore) return;
+			this.loading = true;
 			try {
-				uni.showLoading({ title: '提交中' });
-				const payload = {
-					feedbackType: this.formData.type,
-					content: this.formData.content,
-					images: this.imageOssMap.filter(id => id !== null).join(',')
+				const params = {
+					pageNum: this.pageNum,
+					pageSize: 10
 				};
-				await submitComplaint(payload);
-				uni.hideLoading();
-				uni.showToast({ title: '反馈成功', icon: 'success' });
-				setTimeout(() => { uni.navigateBack(); }, 1500);
+				const res = await getMyComplaintList(params);
+				const rows = (res && res.rows) ? res.rows : [];
+
+				const formatted = rows.map(item => ({
+					id: item.id,
+					feedbackType: item.feedbackType,
+					feedbackTypeLabel: this.typeMap[item.feedbackType] || item.feedbackType || '未知类型',
+					content: item.content,
+					status: item.status || '0',
+					createTime: item.createTime
+				}));
+
+				this.displayList = [...this.displayList, ...formatted];
+				this.pageNum++;
+				this.noMore = rows.length === 0 || this.displayList.length >= (res.total || 0);
 			} catch (e) {
-				uni.hideLoading();
-				uni.showToast({ title: e || '提交失败', icon: 'none' });
+				console.error('加载反馈列表失败', e);
+				uni.showToast({ title: e || '加载反馈列表失败', icon: 'none' });
+			} finally {
+				this.loading = false;
 			}
 		}
 	}
@@ -162,7 +131,13 @@ export default {
 </script>
 
 <style scoped>
-.complaint-root {
+/deep/ ::-webkit-scrollbar {
+	display: none !important;
+	width: 0 !important;
+	height: 0 !important;
+}
+
+.complaint-list-root {
 	width: 100vw;
 	height: 100vh;
 	background: #f8fafb;
@@ -171,195 +146,127 @@ export default {
 	overflow: hidden;
 }
 
-.custom-navbar {
-	background: #fff;
+.list-scroll-view {
+	flex: 1;
+	height: 0;
 	width: 100%;
-	flex-shrink: 0;
-	border-bottom: 1rpx solid #f0f0f0;
 }
 
-.nav-content {
-	height: 44px;
-	display: flex;
-	align-items: center;
-	justify-content: space-between;
-	padding: 0 30rpx;
-}
-
-.back-area {
-	width: 60rpx;
-	height: 44px;
-	display: flex;
-	align-items: center;
-}
-
-.back-arrow {
-	width: 22rpx;
-	height: 22rpx;
-	border-left: 4rpx solid #333;
-	border-bottom: 4rpx solid #333;
-	transform: rotate(45deg);
-	margin-left: 10rpx;
+.list-inner {
+	padding: 30rpx;
 }
 
-.nav-title {
-	font-size: 34rpx;
-	font-weight: bold;
-	color: #333;
+.complaint-card {
+	background: #fff;
+	border-radius: 16rpx;
+	padding: 30rpx;
+	margin-bottom: 24rpx;
+	box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.03);
 }
 
-.right-placeholder {
-	width: 60rpx;
+.card-header {
+	display: flex;
+	justify-content: space-between;
+	align-items: center;
+	margin-bottom: 20rpx;
 }
 
-.scroll-container {
-	flex: 1;
-	height: 0;
-	width: 100%;
-	position: relative;
+.type-badge {
+	font-size: 26rpx;
+	color: #C1001C;
+	background: rgba(193, 0, 28, 0.06);
+	padding: 6rpx 16rpx;
+	border-radius: 8rpx;
 }
 
-.scroll-content {
-	width: 100%;
-	height: 100%;
+.status-text {
+	font-size: 26rpx;
 }
 
-.form-body {
-	padding: 30rpx;
+.status-text.pending {
+	color: #ff9800;
 }
 
-.section-card {
-	background: #fff;
-	border-radius: 24rpx;
-	padding: 40rpx 30rpx;
-	margin-bottom: 30rpx;
-	box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.02);
+.status-text.done {
+	color: #4caf50;
 }
 
-.section-title {
-	font-size: 30rpx;
-	font-weight: bold;
-	color: #1a1a1a;
-	margin-bottom: 30rpx;
-	border-left: 8rpx solid #C1001C;
-	padding-left: 20rpx;
-	line-height: 1.2;
+.card-content {
+	margin-bottom: 20rpx;
 }
 
-.type-grid {
-	display: flex;
-	gap: 20rpx;
+.content-text {
+	font-size: 28rpx;
+	color: #333;
+	line-height: 1.6;
+	display: -webkit-box;
+	-webkit-line-clamp: 2;
+	-webkit-box-orient: vertical;
+	overflow: hidden;
 }
 
-.type-item {
-	flex: 1;
-	height: 80rpx;
-	background: #f5f6f8;
-	border-radius: 12rpx;
+.card-footer {
 	display: flex;
+	justify-content: space-between;
 	align-items: center;
-	justify-content: center;
-	font-size: 28rpx;
-	color: #666;
-	border: 2rpx solid #f5f6f8;
-	transition: all 0.2s;
 }
 
-.type-item.active {
-	background: rgba(193, 0, 28, 0.05);
-	color: #C1001C;
-	border-color: #C1001C;
-	font-weight: bold;
-}
-
-.content-input {
-	width: 100%;
-	height: 300rpx;
-	background: #f9fafb;
-	border-radius: 16rpx;
-	padding: 24rpx;
-	box-sizing: border-box;
-	font-size: 30rpx;
-	color: #333;
-}
-
-.word-count {
-	text-align: right;
+.time-text {
 	font-size: 24rpx;
-	color: #ccc;
-	margin-top: 12rpx;
+	color: #bbb;
 }
 
-.upload-grid {
-	display: grid;
-	grid-template-columns: repeat(3, 1fr);
-	gap: 20rpx;
+.arrow-icon {
+	width: 14rpx;
+	height: 14rpx;
+	border-right: 3rpx solid #ccc;
+	border-top: 3rpx solid #ccc;
+	transform: rotate(45deg);
+	margin-left: 10rpx;
 }
 
-.img-item {
-	position: relative;
-	width: 100%;
-	padding-top: 100%;
+.list-status-info {
+	padding: 30rpx 0;
+	display: flex;
+	justify-content: center;
 }
 
-.img-item image {
-	position: absolute;
-	top: 0;
-	left: 0;
-	width: 100%;
-	height: 100%;
-	border-radius: 16rpx;
+.loading-wrap {
+	font-size: 26rpx;
+	color: #999;
 }
 
-.del-btn {
-	position: absolute;
-	top: -10rpx;
-	right: -10rpx;
-	width: 40rpx;
-	height: 40rpx;
-	background: rgba(0, 0, 0, 0.5);
-	border-radius: 50%;
+.nomore-wrap {
 	display: flex;
 	align-items: center;
-	justify-content: center;
-	z-index: 10;
 }
 
-.close-icon {
-	color: #fff;
-	font-size: 30rpx;
-	line-height: 1;
+.nomore-text {
+	font-size: 26rpx;
+	color: #ccc;
 }
 
-.add-btn {
-	width: 100%;
-	padding-top: 100%;
-	border: 2rpx dashed #ddd;
-	border-radius: 16rpx;
+.empty-state {
 	display: flex;
 	flex-direction: column;
 	align-items: center;
-	justify-content: center;
-	position: relative;
-	background: #fcfcfc;
+	padding-top: 150rpx;
 }
 
-.add-icon {
-	position: absolute;
-	top: 35%;
-	left: 50%;
-	transform: translateX(-50%);
-	font-size: 60rpx;
-	color: #bbb;
+.empty-state image {
+	width: 200rpx;
+	height: 200rpx;
+	margin-bottom: 30rpx;
 }
 
-.add-txt {
-	position: absolute;
-	bottom: 20%;
-	left: 50%;
-	transform: translateX(-50%);
-	font-size: 22rpx;
+.empty-txt {
+	font-size: 28rpx;
 	color: #999;
+	margin-bottom: 40rpx;
+}
+
+.safe-bottom {
+	height: 40rpx;
 }
 
 .footer-bar {
@@ -383,12 +290,7 @@ export default {
 	border: none;
 }
 
-.submit-btn[disabled] {
-	background: #edb3bb !important;
-	color: rgba(255, 255, 255, 0.6) !important;
-}
-
-.bottom-placeholder {
-	height: 40rpx;
+.submit-btn::after {
+	border: none;
 }
 </style>

+ 349 - 0
pages/mine/complaint/submit/index.vue

@@ -0,0 +1,349 @@
+<template>
+	<view class="complaint-root">
+		<erp-nav-bar title="提交反馈" />
+
+		<view class="scroll-container">
+			<scroll-view scroll-y class="scroll-content" :show-scrollbar="false">
+				<view class="form-body">
+					<view class="section-card">
+						<view class="section-title">反馈类型</view>
+						<view class="type-grid">
+							<view class="type-item" v-for="item in types" :key="item.value"
+								:class="{ active: formData.type === item.value }" @click="formData.type = item.value">
+								<text>{{ item.label }}</text>
+							</view>
+						</view>
+					</view>
+
+					<view class="section-card">
+						<view class="section-title">反馈内容</view>
+						<textarea class="content-input" v-model="formData.content" placeholder="请详细描述您遇到的问题或改进建议..."
+							maxlength="500"></textarea>
+						<view class="word-count">{{ formData.content.length }}/500</view>
+					</view>
+
+					<view class="section-card">
+						<view class="section-title">上传图片 (最多6张)</view>
+						<view class="upload-grid">
+							<view class="img-item" v-for="(img, index) in formData.images" :key="index">
+								<image :src="img" mode="aspectFill" @click="previewImage(index)"></image>
+								<view class="del-btn" @click.stop="removeImage(index)">
+									<view class="close-icon">×</view>
+								</view>
+							</view>
+							<view class="add-btn" v-if="formData.images.length < 6" @click="chooseImage">
+								<text class="add-icon">+</text>
+								<text class="add-txt">添加图片</text>
+							</view>
+						</view>
+					</view>
+				</view>
+
+				<view class="bottom-placeholder"></view>
+			</scroll-view>
+		</view>
+
+		<view class="footer-bar">
+			<button class="submit-btn" :disabled="!isFormValid" @click="handleSubmit">提交反馈</button>
+		</view>
+	</view>
+</template>
+
+<script>
+import ErpNavBar from '@/components/erp-nav-bar.vue';
+import { submitComplaint } from '@/api/system/complaint.js';
+import { getDictByType } from '@/api/system/dict.js';
+import { uploadFile } from '@/api/resource/oss.js';
+export default {
+	components: { ErpNavBar },
+	data() {
+		return {
+			types: [],
+			uploading: false,
+			imageOssMap: [],
+			formData: {
+				type: '',
+				content: '',
+				images: []
+			}
+		}
+	},
+	computed: {
+		isFormValid() {
+			return !this.uploading && this.formData.type && this.formData.content && this.formData.content.trim().length >= 1;
+		}
+	},
+	onLoad() {
+		this.loadTypes();
+	},
+	methods: {
+		async loadTypes() {
+			try {
+				const res = await getDictByType('sys_complaint_type');
+				if (res && res.data) {
+					this.types = res.data.map(d => ({ label: d.dictLabel, value: d.dictValue }));
+					if (this.types.length) this.formData.type = this.types[0].value;
+				}
+			} catch (e) {
+				uni.showToast({ title: e || '加载反馈类型失败', icon: 'none' });
+				this.types = [
+					{ label: '系统投诉', value: 'complaint' },
+					{ label: '改进建议', value: 'suggestion' },
+					{ label: '其他反馈', value: 'other' }
+				];
+				this.formData.type = 'complaint';
+			}
+		},
+		goBack() { uni.navigateBack(); },
+		chooseImage() {
+			const count = 6 - this.formData.images.length;
+			uni.chooseImage({
+				count,
+				sizeType: ['compressed'],
+				success: async (res) => {
+					const paths = res.tempFilePaths;
+					this.uploading = true;
+					try {
+						for (const path of paths) {
+							const preview = path;
+							const placeholderIndex = this.formData.images.length;
+							this.formData.images.push(preview);
+							this.imageOssMap.push(null);
+							try {
+								const uploadRes = await uploadFile(path);
+								this.formData.images.splice(placeholderIndex, 1, uploadRes.url);
+								this.imageOssMap.splice(placeholderIndex, 1, uploadRes.ossId);
+							} catch (err) {
+								this.formData.images.splice(placeholderIndex, 1);
+								this.imageOssMap.splice(placeholderIndex, 1);
+								uni.showToast({ title: err || '图片上传失败', icon: 'none' });
+							}
+						}
+					} finally {
+						this.uploading = false;
+					}
+				}
+			});
+		},
+		removeImage(index) {
+			this.formData.images.splice(index, 1);
+			this.imageOssMap.splice(index, 1);
+		},
+		previewImage(index) {
+			uni.previewImage({
+				urls: this.formData.images,
+				current: index
+			});
+		},
+		async handleSubmit() {
+			if (!this.isFormValid) return;
+			try {
+				uni.showLoading({ title: '提交中' });
+				const payload = {
+					feedbackType: this.formData.type,
+					content: this.formData.content,
+					images: this.imageOssMap.filter(id => id !== null).join(',')
+				};
+				await submitComplaint(payload);
+				uni.hideLoading();
+				uni.showToast({ title: '反馈成功', icon: 'success' });
+				setTimeout(() => { uni.navigateBack(); }, 1500);
+			} catch (e) {
+				uni.hideLoading();
+				uni.showToast({ title: e || '提交失败', icon: 'none' });
+			}
+		}
+	}
+}
+</script>
+
+<style scoped>
+.complaint-root {
+	width: 100vw;
+	height: 100vh;
+	background: #f8fafb;
+	display: flex;
+	flex-direction: column;
+	overflow: hidden;
+}
+
+.scroll-container {
+	flex: 1;
+	height: 0;
+	width: 100%;
+	position: relative;
+}
+
+.scroll-content {
+	width: 100%;
+	height: 100%;
+}
+
+.form-body {
+	padding: 30rpx;
+}
+
+.section-card {
+	background: #fff;
+	border-radius: 24rpx;
+	padding: 40rpx 30rpx;
+	margin-bottom: 30rpx;
+	box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.02);
+}
+
+.section-title {
+	font-size: 30rpx;
+	font-weight: bold;
+	color: #1a1a1a;
+	margin-bottom: 30rpx;
+	border-left: 8rpx solid #C1001C;
+	padding-left: 20rpx;
+	line-height: 1.2;
+}
+
+.type-grid {
+	display: flex;
+	gap: 20rpx;
+}
+
+.type-item {
+	flex: 1;
+	height: 80rpx;
+	background: #f5f6f8;
+	border-radius: 12rpx;
+	display: flex;
+	align-items: center;
+	justify-content: center;
+	font-size: 28rpx;
+	color: #666;
+	border: 2rpx solid #f5f6f8;
+	transition: all 0.2s;
+}
+
+.type-item.active {
+	background: rgba(193, 0, 28, 0.05);
+	color: #C1001C;
+	border-color: #C1001C;
+	font-weight: bold;
+}
+
+.content-input {
+	width: 100%;
+	height: 300rpx;
+	background: #f9fafb;
+	border-radius: 16rpx;
+	padding: 24rpx;
+	box-sizing: border-box;
+	font-size: 30rpx;
+	color: #333;
+}
+
+.word-count {
+	text-align: right;
+	font-size: 24rpx;
+	color: #ccc;
+	margin-top: 12rpx;
+}
+
+.upload-grid {
+	display: grid;
+	grid-template-columns: repeat(3, 1fr);
+	gap: 20rpx;
+}
+
+.img-item {
+	position: relative;
+	width: 100%;
+	padding-top: 100%;
+}
+
+.img-item image {
+	position: absolute;
+	top: 0;
+	left: 0;
+	width: 100%;
+	height: 100%;
+	border-radius: 16rpx;
+}
+
+.del-btn {
+	position: absolute;
+	top: -10rpx;
+	right: -10rpx;
+	width: 40rpx;
+	height: 40rpx;
+	background: rgba(0, 0, 0, 0.5);
+	border-radius: 50%;
+	display: flex;
+	align-items: center;
+	justify-content: center;
+	z-index: 10;
+}
+
+.close-icon {
+	color: #fff;
+	font-size: 30rpx;
+	line-height: 1;
+}
+
+.add-btn {
+	width: 100%;
+	padding-top: 100%;
+	border: 2rpx dashed #ddd;
+	border-radius: 16rpx;
+	display: flex;
+	flex-direction: column;
+	align-items: center;
+	justify-content: center;
+	position: relative;
+	background: #fcfcfc;
+}
+
+.add-icon {
+	position: absolute;
+	top: 35%;
+	left: 50%;
+	transform: translateX(-50%);
+	font-size: 60rpx;
+	color: #bbb;
+}
+
+.add-txt {
+	position: absolute;
+	bottom: 20%;
+	left: 50%;
+	transform: translateX(-50%);
+	font-size: 22rpx;
+	color: #999;
+}
+
+.footer-bar {
+	background: #fff;
+	padding: 30rpx 40rpx calc(30rpx + env(safe-area-inset-bottom));
+	flex-shrink: 0;
+	border-top: 1rpx solid #f0f0f0;
+}
+
+.submit-btn {
+	width: 100%;
+	height: 96rpx;
+	background: #C1001C;
+	color: #fff;
+	border-radius: 48rpx;
+	display: flex;
+	align-items: center;
+	justify-content: center;
+	font-size: 32rpx;
+	font-weight: bold;
+	border: none;
+}
+
+.submit-btn[disabled] {
+	background: #edb3bb !important;
+	color: rgba(255, 255, 255, 0.6) !important;
+}
+
+.bottom-placeholder {
+	height: 40rpx;
+}
+</style>

+ 245 - 139
pages/order/add-model/index.vue

@@ -128,6 +128,7 @@
 					<text class="popup-title">选择产品型号</text>
 					<text class="confirm-text" @click="confirmTypeSelect">确定</text>
 				</view>
+				<!-- 搜索栏 -->
 				<view class="search-bar">
 					<view class="search-input-wrap">
 						<text class="search-icon">🔍</text>
@@ -135,18 +136,23 @@
 							@input="onTypeSearch" />
 					</view>
 				</view>
-				<scroll-view scroll-y class="item-list" @scrolltolower="loadMoreType">
-					<view class="option-item" v-for="(item, index) in typeList" :key="index"
-						:class="{ active: tempSelectedIndex === index }" @click="selectTypeItem(index)">
-						<text>{{ item.num }} - {{ item.name }}</text>
-						<icon type="success_no_circle" size="16" color="#C1001C" v-if="tempSelectedIndex === index">
-						</icon>
+				<!-- 层级面包屑 -->
+				<view class="breadcrumb-bar" v-if="typePickerLevel > 1">
+					<text class="back-btn" @click="goBackModelLevel">← 返回</text>
+					<text class="breadcrumb-path">
+						{{typeBreadcrumbs.map(b => b.label).join(' / ')}}
+					</text>
+				</view>
+				<!-- 列表区 -->
+				<scroll-view scroll-y class="item-list">
+					<view class="option-item kind-item" v-for="(item, index) in modelKindList" :key="'mk'+index"
+						@click="drillDownType(item)">
+						<text class="option-text kind-text">{{ item.num }} - {{ item.name }}</text>
+						<text class="arrow-right">›</text>
 					</view>
-					<view class="loading-tip" v-if="typeLoading && typeHasMore">
-						<text class="loading-text">加载中...</text>
-					</view>
-					<view class="loading-tip" v-if="!typeHasMore && typeList.length > 0">
-						<text class="loading-text">已加载全部</text>
+					<view class="option-item" v-for="(item, index) in modelItemList" :key="'mi'+index"
+						:class="{ active: tempSelectedIndex === index }">
+						<text class="option-text" @click="selectTypeItem(index)">{{ item.num }} - {{ item.name }}</text>
 					</view>
 				</scroll-view>
 			</view>
@@ -160,6 +166,7 @@
 					<text class="popup-title">选择表面处理</text>
 					<text class="confirm-text" @click="confirmSurfaceSelect">确定</text>
 				</view>
+				<!-- 搜索栏 -->
 				<view class="search-bar">
 					<view class="search-input-wrap">
 						<text class="search-icon">🔍</text>
@@ -167,18 +174,22 @@
 							@input="onSurfaceSearch" />
 					</view>
 				</view>
-				<scroll-view scroll-y class="item-list" @scrolltolower="loadMoreSurface">
-					<view class="option-item" v-for="(item, index) in surfaceList" :key="index"
-						:class="{ active: tempSurfaceIndex === index }" @click="selectSurfaceItem(index)">
-						<text>{{ item.num }} - {{ item.name }}</text>
-						<icon type="success_no_circle" size="16" color="#C1001C" v-if="tempSurfaceIndex === index">
-						</icon>
-					</view>
-					<view class="loading-tip" v-if="surfaceLoading && surfaceHasMore">
-						<text class="loading-text">加载中...</text>
+				<!-- 层级面包屑 -->
+				<view class="breadcrumb-bar" v-if="surfacePickerLevel > 1">
+					<text class="back-btn" @click="goBackSurfaceLevel">← 返回</text>
+					<text class="breadcrumb-path">{{surfaceBreadcrumbs.map(b => b.label).join(' / ')}}</text>
+				</view>
+				<!-- 列表区 -->
+				<scroll-view scroll-y class="item-list">
+					<view class="option-item kind-item" v-for="(item, index) in colorKindList" :key="'ck'+index"
+						@click="drillDownSurface(item)">
+						<text class="option-text kind-text">{{ item.num }} - {{ item.name }}</text>
+						<text class="arrow-right">›</text>
 					</view>
-					<view class="loading-tip" v-if="!surfaceHasMore && surfaceList.length > 0">
-						<text class="loading-text">已加载全部</text>
+					<view class="option-item" v-for="(item, index) in colorItemList" :key="'ci'+index"
+						:class="{ active: tempSurfaceIndex === index }">
+						<text class="option-text" @click="selectSurfaceItem(index)">{{ item.num }} - {{ item.name
+						}}</text>
 					</view>
 				</scroll-view>
 			</view>
@@ -221,9 +232,11 @@
 
 <script>
 import ErpNavBar from '@/components/erp-nav-bar.vue';
-import { listPageColor } from '@/api/erp/color.js';
+import { listModel } from '@/api/erp/model.js';
+import { listModelKind } from '@/api/erp/modelKind.js';
+import { listColor } from '@/api/erp/color.js';
+import { listColorKind } from '@/api/erp/colorKind.js';
 import { listPagePack } from '@/api/erp/pack.js';
-import { listPageModel } from '@/api/erp/model.js';
 import { addOrderDetail } from '@/api/erp/orderDetail.js';
 export default {
 	components: { ErpNavBar },
@@ -231,79 +244,28 @@ export default {
 		return {
 			showTypePicker: false, showSurfacePicker: false, showPackagePicker: false,
 			tempSelectedIndex: -1, tempSurfaceIndex: -1, tempPackageIndex: -1,
-			typeList: [],
-			surfaceList: [],
+			modelKindList: [],
+			modelItemList: [],
+			colorKindList: [],
+			colorItemList: [],
 			packageList: [],
-			// 搜索关键词
 			typeSearchKey: '', surfaceSearchKey: '', packageSearchKey: '',
-			// 防抖定时器
-			typeSearchTimer: null, surfaceSearchTimer: null, packageSearchTimer: null,
-			// 分页参数
-			typePageNum: 1, typePageSize: 20, typeHasMore: true, typeLoading: false,
-			surfacePageNum: 1, surfacePageSize: 20, surfaceHasMore: true, surfaceLoading: false,
-			packagePageNum: 1, packagePageSize: 20, packageHasMore: true, packageLoading: false,
+			typeBreadcrumbs: [],
+			typePickerLevel: 1,
+			surfaceBreadcrumbs: [],
+			surfacePickerLevel: 1,
+			packagePageNum: 1, packagePageSize: 10, packageHasMore: true, packageLoading: false,
 			formData: {
 				type: '', modelId: '', name: '', material: '',
 				surfaceName: '', surfaceId: '', packageMethod: '', packageId: '', length: '',
-				wallThickness: '', count: ''
+				wallThickness: '', count: '', meterWeight: ''
 			}
 		}
 	},
 	async onLoad() {
-		await Promise.all([
-			this.loadModels(),
-			this.loadSurfaceKinds(),
-			this.loadPackageMethods()
-		]);
+		await this.loadPackageMethods();
 	},
 	methods: {
-		async loadModels(pageNum = 1, keyword = '') {
-			if (this.typeLoading) return;
-			this.typeLoading = true;
-			try {
-				const res = await listPageModel({
-					pageNum: pageNum,
-					pageSize: this.typePageSize,
-					name: keyword
-				});
-				const rows = res.rows || [];
-				const total = res.total || 0;
-				if (pageNum === 1) {
-					this.typeList = rows;
-				} else {
-					this.typeList = [...this.typeList, ...rows];
-				}
-				this.typeHasMore = this.typeList.length < total;
-			} catch (e) {
-				uni.showToast({ title: e || '加载型号失败', icon: 'none' });
-			} finally {
-				this.typeLoading = false;
-			}
-		},
-		async loadSurfaceKinds(pageNum = 1, keyword = '') {
-			if (this.surfaceLoading) return;
-			this.surfaceLoading = true;
-			try {
-				const res = await listPageColor({
-					pageNum: pageNum,
-					pageSize: this.surfacePageSize,
-					name: keyword
-				});
-				let rows = res.rows || [];
-				const total = res.total || 0;
-				rows = rows.filter(item => item.name);
-				if (pageNum === 1) {
-					this.surfaceList = rows;
-				} else {
-					this.surfaceList = [...this.surfaceList, ...rows];
-				}
-				this.surfaceHasMore = this.surfaceList.length < total;
-			} catch (e) {
-				uni.showToast({ title: e || '加载表面处理失败', icon: 'none' });
-			} finally {
-				this.surfaceLoading = false;
-			}
-		},
 		async loadPackageMethods(pageNum = 1, keyword = '') {
 			if (this.packageLoading) return;
 			this.packageLoading = true;
@@ -327,49 +289,12 @@ export default {
 				this.packageLoading = false;
 			}
 		},
-		loadMoreType() {
-			if (this.typeHasMore && !this.typeLoading) {
-				this.typePageNum++;
-				this.loadModels(this.typePageNum, this.typeSearchKey);
-			}
-		},
-		loadMoreSurface() {
-			if (this.surfaceHasMore && !this.surfaceLoading) {
-				this.surfacePageNum++;
-				this.loadSurfaceKinds(this.surfacePageNum, this.surfaceSearchKey);
-			}
-		},
 		loadMorePackage() {
 			if (this.packageHasMore && !this.packageLoading) {
 				this.packagePageNum++;
 				this.loadPackageMethods(this.packagePageNum, this.packageSearchKey);
 			}
 		},
-		onScrollToLower() {
-			// 主页面滚动触底,可根据需要添加更多逻辑
-		},
-		/**
-		 * 型号搜索(防抖)
-		 * @Author: Antigravity
-		 */
-		onTypeSearch() {
-			if (this.typeSearchTimer) clearTimeout(this.typeSearchTimer);
-			this.typeSearchTimer = setTimeout(() => {
-				this.typePageNum = 1;
-				this.loadModels(1, this.typeSearchKey);
-			}, 300);
-		},
-		/**
-		 * 表面处理搜索(防抖)
-		 * @Author: Antigravity
-		 */
-		onSurfaceSearch() {
-			if (this.surfaceSearchTimer) clearTimeout(this.surfaceSearchTimer);
-			this.surfaceSearchTimer = setTimeout(() => {
-				this.surfacePageNum = 1;
-				this.loadSurfaceKinds(1, this.surfaceSearchKey);
-			}, 300);
-		},
 		/**
 		 * 包装方式搜索(防抖)
 		 * @Author: Antigravity
@@ -381,46 +306,181 @@ export default {
 				this.loadPackageMethods(1, this.packageSearchKey);
 			}, 300);
 		},
-		openTypePicker() {
-			if (this.formData.type) {
-				this.tempSelectedIndex = this.typeList.findIndex(item => item.num === this.formData.type);
-			} else {
-				this.tempSelectedIndex = -1;
+		/**
+		 * 加载型材系列,点击系列后同时加载子系列和具体型号
+		 * @Author: Trae
+		 */
+		async loadTypes(parentRowId) {
+			try {
+				const keyword = (this.typeSearchKey || '').trim();
+				const kindParams = { name: keyword || undefined };
+				if (parentRowId) {
+					kindParams.parentRowId = parentRowId;
+				}
+				const kindRes = await listModelKind(kindParams);
+				this.modelKindList = kindRes.data || [];
+				if (parentRowId) {
+					const modelParams = { name: keyword || undefined, parentRowId };
+					const modelRes = await listModel(modelParams);
+					this.modelItemList = modelRes.data || [];
+				} else {
+					this.modelItemList = [];
+				}
+			} catch (e) {
+				uni.showToast({ title: e || '加载型号失败', icon: 'none' });
 			}
+		},
+		/**
+		 * 型号搜索
+		 * @Author: Trae
+		 */
+		onTypeSearch() {
+			const parentRowId = this.typeBreadcrumbs.length > 0
+				? this.typeBreadcrumbs[this.typeBreadcrumbs.length - 1].rowId : '';
+			this.loadTypes(parentRowId);
+		},
+		/**
+		 * 加载表面处理系列,点击系列后同时加载子系列和具体颜色
+		 * @Author: Trae
+		 */
+		async loadSurfaces(parentRowId) {
+			try {
+				const keyword = (this.surfaceSearchKey || '').trim();
+				const kindParams = { name: keyword || undefined };
+				if (parentRowId) {
+					kindParams.parentRowId = parentRowId;
+				}
+				const kindRes = await listColorKind(kindParams);
+				this.colorKindList = kindRes.data || [];
+				if (parentRowId) {
+					const colorParams = { name: keyword || undefined, parentRowId };
+					const colorRes = await listColor(colorParams);
+					this.colorItemList = colorRes.data || [];
+				} else {
+					this.colorItemList = [];
+				}
+			} catch (e) {
+				uni.showToast({ title: e || '加载表面处理失败', icon: 'none' });
+			}
+		},
+		/**
+		 * 表面处理搜索
+		 * @Author: Trae
+		 */
+		onSurfaceSearch() {
+			const parentRowId = this.surfaceBreadcrumbs.length > 0
+				? this.surfaceBreadcrumbs[this.surfaceBreadcrumbs.length - 1].rowId : '';
+			this.loadSurfaces(parentRowId);
+		},
+		/**
+		 * 打开型号选择器
+		 * @Author: Trae
+		 */
+		openTypePicker() {
+			this.typePickerLevel = 1;
+			this.typeBreadcrumbs = [];
+			this.tempSelectedIndex = -1;
 			this.showTypePicker = true;
+			this.loadTypes('');
+		},
+		/**
+		 * 钻取到子级
+		 * @Author: Trae
+		 */
+		drillDownType(item) {
+			this.typeBreadcrumbs.push({ rowId: item.rowId, label: item.num });
+			this.typePickerLevel++;
+			this.tempSelectedIndex = -1;
+			this.loadTypes(item.rowId);
+		},
+		/**
+		 * 返回上一级
+		 * @Author: Trae
+		 */
+		goBackModelLevel() {
+			this.typeBreadcrumbs.pop();
+			this.typePickerLevel--;
+			this.tempSelectedIndex = -1;
+			const parentRowId = this.typeBreadcrumbs.length > 0
+				? this.typeBreadcrumbs[this.typeBreadcrumbs.length - 1].rowId : '';
+			this.loadTypes(parentRowId);
 		},
 		closeTypePicker() {
 			this.showTypePicker = false;
 			this.typeSearchKey = '';
-			this.typePageNum = 1;
+			this.typePickerLevel = 1;
+			this.typeBreadcrumbs = [];
+			this.tempSelectedIndex = -1;
 		},
 		selectTypeItem(index) { this.tempSelectedIndex = index; },
+		/**
+		 * 确认选择型材
+		 * @Author: Trae
+		 */
 		confirmTypeSelect() {
-			if (this.tempSelectedIndex === -1) return;
-			const item = this.typeList[this.tempSelectedIndex];
+			if (this.tempSelectedIndex === -1) {
+				uni.showToast({ title: '请先选择一个选项', icon: 'none' });
+				return;
+			}
+			const item = this.modelItemList[this.tempSelectedIndex];
 			this.formData.type = item.num;
 			this.formData.modelId = item.rowId;
 			this.formData.name = item.name;
 			this.formData.material = '6063-T5';
+			this.formData.meterWeight = item.meterWt || '';
 			this.closeTypePicker();
 		},
+		/**
+		 * 打开表面处理选择器
+		 * @Author: Trae
+		 */
 		openSurfacePicker() {
-			if (this.formData.surfaceName) {
-				this.tempSurfaceIndex = this.surfaceList.findIndex(item => item.name === this.formData.surfaceName);
-			} else {
-				this.tempSurfaceIndex = -1;
-			}
+			this.surfacePickerLevel = 1;
+			this.surfaceBreadcrumbs = [];
+			this.tempSurfaceIndex = -1;
 			this.showSurfacePicker = true;
+			this.loadSurfaces('');
+		},
+		/**
+		 * 钻取到子级
+		 * @Author: Trae
+		 */
+		drillDownSurface(item) {
+			this.surfaceBreadcrumbs.push({ rowId: item.rowId, label: item.num });
+			this.surfacePickerLevel++;
+			this.tempSurfaceIndex = -1;
+			this.loadSurfaces(item.rowId);
+		},
+		/**
+		 * 返回上一级
+		 * @Author: Trae
+		 */
+		goBackSurfaceLevel() {
+			this.surfaceBreadcrumbs.pop();
+			this.surfacePickerLevel--;
+			this.tempSurfaceIndex = -1;
+			const parentRowId = this.surfaceBreadcrumbs.length > 0
+				? this.surfaceBreadcrumbs[this.surfaceBreadcrumbs.length - 1].rowId : '';
+			this.loadSurfaces(parentRowId);
 		},
 		closeSurfacePicker() {
 			this.showSurfacePicker = false;
 			this.surfaceSearchKey = '';
-			this.surfacePageNum = 1;
+			this.surfacePickerLevel = 1;
+			this.surfaceBreadcrumbs = [];
+			this.tempSurfaceIndex = -1;
 		},
 		selectSurfaceItem(index) { this.tempSurfaceIndex = index; },
+		/**
+		 * 确认选择表面处理
+		 * @Author: Trae
+		 */
 		confirmSurfaceSelect() {
-			if (this.tempSurfaceIndex === -1) return;
-			const item = this.surfaceList[this.tempSurfaceIndex];
+			if (this.tempSurfaceIndex === -1) {
+				uni.showToast({ title: '请先选择一个选项', icon: 'none' });
+				return;
+			}
+			const item = this.colorItemList[this.tempSurfaceIndex];
 			this.formData.surfaceName = item.name;
 			this.formData.surfaceId = item.rowId;
 			this.closeSurfacePicker();
@@ -473,6 +533,7 @@ export default {
 				packName: this.formData.packageMethod,
 				length: parseFloat(this.formData.length || 0).toFixed(4),
 				wallThickness: parseFloat(this.formData.wallThickness || 0).toFixed(4),
+				meterWeight: this.formData.meterWeight ? parseFloat(this.formData.meterWeight || 0).toFixed(4) : null,
 				count: parseInt(this.formData.count || 0)
 			};
 
@@ -791,6 +852,19 @@ export default {
 	font-weight: bold;
 }
 
+.kind-item {
+	background: #fafbfc;
+}
+
+.kind-text {
+	color: #556;
+}
+
+.option-text {
+	flex: 1;
+	padding: 10rpx 0;
+}
+
 .loading-tip {
 	padding: 20rpx 0;
 	text-align: center;
@@ -801,6 +875,38 @@ export default {
 	color: #999;
 }
 
+/* 级联面包屑 */
+.breadcrumb-bar {
+	display: flex;
+	align-items: center;
+	padding: 16rpx 40rpx;
+	background: #fafafa;
+	border-bottom: 1rpx solid #f0f0f0;
+}
+
+.back-btn {
+	font-size: 28rpx;
+	color: #C1001C;
+	padding-right: 20rpx;
+	flex-shrink: 0;
+}
+
+.breadcrumb-path {
+	font-size: 26rpx;
+	color: #666;
+	flex: 1;
+	overflow: hidden;
+	text-overflow: ellipsis;
+	white-space: nowrap;
+}
+
+.arrow-right {
+	font-size: 56rpx;
+	color: #bbb;
+	padding: 10rpx 0 10rpx 20rpx;
+	font-weight: 300;
+}
+
 .bottom-placeholder {
 	height: 60rpx;
 }

+ 451 - 0
pages/order/components/order-confirm-popup.vue

@@ -0,0 +1,451 @@
+<template>
+	<view class="confirm-popup-root" v-if="visible" @click.stop.prevent>
+		<view class="popup-mask" @click="handleClose"></view>
+		<view class="popup-body">
+			<view class="popup-header">
+				<text class="header-title">订单确认</text>
+				<view class="header-actions">
+					<text class="preview-btn" @click="previewFullScreen">全屏查看</text>
+					<text class="close-btn" @click="handleClose">×</text>
+				</view>
+			</view>
+
+			<view class="client-info" v-if="clientName">
+				<text class="client-label">客户:</text>
+				<text class="client-value">{{ clientName }}</text>
+			</view>
+
+			<view class="canvas-wrapper">
+				<canvas canvas-id="orderConfirmPopupCanvas" class="confirm-canvas"
+					:style="{ width: canvasW + 'px', height: canvasH + 'px' }" @click="previewFullScreen">
+				</canvas>
+			</view>
+
+			<view class="summary-row">
+				<text>共 <text class="num-highlight">{{ models.length }}</text> 个型号,合计 <text class="num-highlight">{{
+					totalCount }}</text> 支</text>
+			</view>
+
+			<view class="popup-footer">
+				<button class="cancel-btn" @click="handleClose">取消</button>
+				<button class="confirm-btn" :disabled="submitting" @click="handleConfirm">
+					{{ submitting ? '提交中...' : '确认下单' }}
+				</button>
+			</view>
+		</view>
+	</view>
+</template>
+
+<script>
+/**
+ * 订单确认弹窗 - canvas绘制表格
+ * @Author: Trae
+ */
+import { getImagesByNums } from '../../../api/drawing/drawingFile.js';
+
+export default {
+	props: {
+		visible: { type: Boolean, default: false },
+		models: { type: Array, default: () => [] },
+		clientName: { type: String, default: '' },
+		submitting: { type: Boolean, default: false }
+	},
+	data() {
+		return {
+			canvasW: 345,
+			canvasH: 300,
+			canvasImagePath: '',
+			imagePathMap: {}
+		}
+	},
+	computed: {
+		totalCount() {
+			return this.models.reduce((sum, m) => sum + (parseInt(m.count) || 0), 0);
+		}
+	},
+	watch: {
+		visible(val) {
+			if (val) {
+				this.$nextTick(() => {
+					this.loadImagesAndDraw();
+				});
+			}
+		}
+	},
+	methods: {
+		getCellWidths() {
+			return [32, 56, 56, 42, 42, 42, 36, 56, 56, 46];
+		},
+		getHeaders() {
+			return ['序号', '型材型号', '型材名称', '长度', '米重', '壁厚', '支数', '表面名称', '包装方式', '简图'];
+		},
+		getColumnX(colIndex) {
+			const widths = this.getCellWidths();
+			let x = 0;
+			for (let i = 0; i < colIndex; i++) { x += widths[i]; }
+			return x;
+		},
+		async loadImagesAndDraw() {
+			const models = this.models;
+			const nums = [...new Set(models.map(m => m.modelNum).filter(Boolean))];
+			this.imagePathMap = {};
+			if (nums.length > 0) {
+				try {
+					const res = await getImagesByNums(nums.join(','));
+					const imageMap = res.data || {};
+					await this.convertBase64ToTempPaths(imageMap);
+				} catch (e) {
+					console.error('加载简图失败:', e);
+				}
+			}
+			setTimeout(() => { this.drawTable(); }, 150);
+		},
+		convertBase64ToTempPaths(imageMap) {
+			const fs = uni.getFileSystemManager();
+			const tasks = Object.entries(imageMap).map(([fnum, base64], i) => {
+				return new Promise((resolve) => {
+					const tempPath = `${wx.env.USER_DATA_PATH}/drawing_thumb_${i}_${Date.now()}.jpg`;
+					fs.writeFile({
+						filePath: tempPath,
+						data: base64,
+						encoding: 'base64',
+						success: () => {
+							this.imagePathMap[fnum] = tempPath;
+							resolve();
+						},
+						fail: (err) => {
+							console.error('写入简图文件失败:', fnum, err);
+							resolve();
+						}
+					});
+				});
+			});
+			return Promise.all(tasks);
+		},
+		drawTable() {
+			const models = this.models;
+			if (!models || models.length === 0) return;
+
+			const colWidths = this.getCellWidths();
+			const rowHeight = 32;
+			const headerHeight = 36;
+			const totalWidth = colWidths.reduce((a, b) => a + b, 0);
+			const totalHeight = headerHeight + models.length * rowHeight + 4;
+
+			this.canvasW = totalWidth;
+			this.canvasH = totalHeight;
+
+			const ctx = uni.createCanvasContext('orderConfirmPopupCanvas', this);
+
+			ctx.setFillStyle('#ffffff');
+			ctx.fillRect(0, 0, totalWidth, totalHeight);
+
+			this.drawHeader(ctx, colWidths, headerHeight);
+			this.drawRows(ctx, colWidths, models, headerHeight, rowHeight);
+
+			ctx.draw(false, () => {
+				setTimeout(() => {
+					this.exportCanvasImage(totalWidth, totalHeight);
+				}, 300);
+			});
+		},
+		drawHeader(ctx, colWidths, headerHeight) {
+			let x = 0;
+			ctx.setLineWidth(1);
+			ctx.setStrokeStyle('#666666');
+			const headers = this.getHeaders();
+			headers.forEach((header, i) => {
+				ctx.setFillStyle('#f5f5f5');
+				ctx.fillRect(x, 0, colWidths[i], headerHeight);
+				ctx.setFillStyle('#222222');
+				ctx.setFontSize(12);
+				ctx.setTextAlign('center');
+				ctx.setTextBaseline('middle');
+				const cx = x + colWidths[i] / 2;
+				ctx.fillText(header, cx, headerHeight / 2);
+				ctx.beginPath();
+				ctx.moveTo(x, 0);
+				ctx.lineTo(x, headerHeight);
+				ctx.stroke();
+				x += colWidths[i];
+			});
+			ctx.beginPath();
+			ctx.moveTo(x, 0);
+			ctx.lineTo(x, headerHeight);
+			ctx.stroke();
+			ctx.setLineWidth(1.5);
+			ctx.beginPath();
+			ctx.moveTo(0, 0);
+			ctx.lineTo(x, 0);
+			ctx.stroke();
+			ctx.beginPath();
+			ctx.moveTo(0, headerHeight);
+			ctx.lineTo(x, headerHeight);
+			ctx.stroke();
+		},
+		drawRows(ctx, colWidths, models, headerHeight, rowHeight) {
+			const totalW = colWidths.reduce((a, b) => a + b, 0);
+			models.forEach((item, rowIndex) => {
+				const y = headerHeight + rowIndex * rowHeight;
+				ctx.setFillStyle(rowIndex % 2 === 0 ? '#ffffff' : '#fafafa');
+				ctx.fillRect(0, y, totalW, rowHeight);
+
+				ctx.setFillStyle('#333333');
+				ctx.setFontSize(10);
+				ctx.setTextAlign('center');
+				ctx.setTextBaseline('middle');
+
+				const val0 = '' + (rowIndex + 1);
+				const val1 = this.truncateText(ctx, item.modelNum != null ? '' + item.modelNum : '', colWidths[1] - 6);
+				const val2 = this.truncateText(ctx, item.modelName != null ? '' + item.modelName : '', colWidths[2] - 6);
+				const val3 = this.toDecimalStr(item.length, 3);
+				const val4 = this.toDecimalStr(item.meterWeight, 3);
+				const val5 = this.toDecimalStr(item.wallThickness, 3);
+				const val6 = item.count != null ? '' + item.count : '-';
+				const val7 = this.truncateText(ctx, '' + (item.surfaceName || ''), colWidths[7] - 6);
+				const val8 = this.truncateText(ctx, '' + (item.packName || ''), colWidths[8] - 6);
+
+				const valueTexts = [val0, val1, val2, val3, val4, val5, val6, val7, val8];
+
+				valueTexts.forEach((val, colIndex) => {
+					const cx = this.getColumnX(colIndex) + colWidths[colIndex] / 2;
+					ctx.fillText('' + val, cx, y + rowHeight / 2);
+				});
+
+				const imgPath = this.imagePathMap[item.modelNum];
+				if (imgPath) {
+					const imgColX = this.getColumnX(9);
+					const imgSize = rowHeight - 8;
+					const imgX = imgColX + (colWidths[9] - imgSize) / 2;
+					const imgY = y + 4;
+					ctx.drawImage(imgPath, imgX, imgY, imgSize, imgSize);
+				}
+
+				this.drawRowLines(ctx, colWidths, y, rowHeight);
+			});
+
+			ctx.setStrokeStyle('#666666');
+			ctx.setLineWidth(1.5);
+			ctx.beginPath();
+			ctx.moveTo(0, headerHeight + models.length * rowHeight);
+			ctx.lineTo(totalW, headerHeight + models.length * rowHeight);
+			ctx.stroke();
+		},
+		drawRowLines(ctx, colWidths, y, rowHeight) {
+			ctx.setStrokeStyle('#dddddd');
+			ctx.setLineWidth(0.5);
+			let x = 0;
+			colWidths.forEach(w => {
+				ctx.beginPath();
+				ctx.moveTo(x, y);
+				ctx.lineTo(x, y + rowHeight);
+				ctx.stroke();
+				x += w;
+			});
+		},
+		toDecimalStr(val, digits) {
+			if (val === null || val === undefined || val === '') return '-';
+			const num = parseFloat(String(val));
+			if (isNaN(num)) return '-';
+			return num.toFixed(digits);
+		},
+		truncateText(ctx, text, maxWidth) {
+			if (!text) return '';
+			const metrics = ctx.measureText ? ctx.measureText(text) : null;
+			if (!metrics || metrics.width <= maxWidth) return text;
+			let truncated = text;
+			while (truncated.length > 0) {
+				truncated = truncated.slice(0, -1);
+				const m = ctx.measureText(truncated + '…');
+				if (m.width <= maxWidth) return truncated + '…';
+			}
+			return '';
+		},
+		exportCanvasImage(canvasW, canvasH) {
+			uni.canvasToTempFilePath({
+				canvasId: 'orderConfirmPopupCanvas',
+				destWidth: canvasW * 2,
+				destHeight: canvasH * 2,
+				success: (res) => {
+					this.canvasImagePath = res.tempFilePath;
+				},
+				fail: () => {
+					console.error('canvas导出失败');
+				}
+			}, this);
+		},
+		previewFullScreen() {
+			if (!this.canvasImagePath) {
+				setTimeout(() => {
+					if (this.canvasImagePath) {
+						this.doPreviewImage();
+					} else {
+						uni.showToast({ title: '图片生成中,请稍后', icon: 'none' });
+					}
+				}, 500);
+				return;
+			}
+			this.doPreviewImage();
+		},
+		doPreviewImage() {
+			uni.previewImage({
+				urls: [this.canvasImagePath],
+				current: 0
+			});
+		},
+		handleConfirm() {
+			this.$emit('confirm');
+		},
+		handleClose() {
+			this.$emit('close');
+		}
+	}
+}
+</script>
+
+<style scoped>
+.confirm-popup-root {
+	position: fixed;
+	top: 0;
+	left: 0;
+	width: 100vw;
+	height: 100vh;
+	z-index: 9999;
+	display: flex;
+	align-items: center;
+	justify-content: center;
+}
+
+.popup-mask {
+	position: absolute;
+	top: 0;
+	left: 0;
+	width: 100%;
+	height: 100%;
+	background: rgba(0, 0, 0, 0.5);
+}
+
+.popup-body {
+	position: relative;
+	width: 680rpx;
+	max-height: 90vh;
+	background: #fff;
+	border-radius: 24rpx;
+	display: flex;
+	flex-direction: column;
+	overflow: hidden;
+}
+
+.popup-header {
+	display: flex;
+	justify-content: space-between;
+	align-items: center;
+	padding: 30rpx 30rpx 20rpx;
+	border-bottom: 1rpx solid #f0f0f0;
+}
+
+.header-title {
+	font-size: 34rpx;
+	font-weight: bold;
+	color: #1a1a1a;
+}
+
+.header-actions {
+	display: flex;
+	align-items: center;
+	gap: 24rpx;
+}
+
+.preview-btn {
+	font-size: 26rpx;
+	color: #C1001C;
+}
+
+.close-btn {
+	font-size: 40rpx;
+	color: #999;
+	line-height: 1;
+}
+
+.client-info {
+	padding: 16rpx 30rpx;
+	background: #fdf6f7;
+}
+
+.client-label {
+	font-size: 26rpx;
+	color: #666;
+}
+
+.client-value {
+	font-size: 26rpx;
+	color: #C1001C;
+	font-weight: bold;
+}
+
+.canvas-wrapper {
+	width: 690rpx;
+	max-height: 55vh;
+	overflow: auto;
+	background: #fff;
+	flex-shrink: 1;
+}
+
+.confirm-canvas {
+	display: block;
+	margin: 0 auto;
+}
+
+.summary-row {
+	padding: 20rpx 30rpx;
+	border-top: 1rpx solid #f0f0f0;
+	text-align: center;
+	font-size: 26rpx;
+	color: #666;
+}
+
+.num-highlight {
+	color: #C1001C;
+	font-weight: bold;
+	font-size: 28rpx;
+}
+
+.popup-footer {
+	display: flex;
+	gap: 20rpx;
+	padding: 20rpx 30rpx 30rpx;
+	border-top: 1rpx solid #f0f0f0;
+}
+
+.cancel-btn {
+	flex: 1;
+	height: 80rpx;
+	background: #f5f5f5;
+	color: #666;
+	border-radius: 40rpx;
+	display: flex;
+	align-items: center;
+	justify-content: center;
+	font-size: 28rpx;
+	border: none;
+}
+
+.confirm-btn {
+	flex: 2;
+	height: 80rpx;
+	background: #C1001C;
+	color: #fff;
+	border-radius: 40rpx;
+	display: flex;
+	align-items: center;
+	justify-content: center;
+	font-size: 28rpx;
+	font-weight: bold;
+	border: none;
+}
+
+.confirm-btn[disabled] {
+	background: #edb3bb;
+	color: rgba(255, 255, 255, 0.6);
+}
+</style>

+ 223 - 109
pages/order/edit-model/index.vue

@@ -128,6 +128,7 @@
 					<text class="popup-title">选择产品型号</text>
 					<text class="confirm-text" @click="confirmTypeSelect">确定</text>
 				</view>
+				<!-- 搜索栏 -->
 				<view class="search-bar">
 					<view class="search-input-wrap">
 						<text class="search-icon">🔍</text>
@@ -135,12 +136,23 @@
 							@input="onTypeSearch" />
 					</view>
 				</view>
-				<scroll-view scroll-y class="item-list" @scrolltolower="loadMoreType">
-					<view class="option-item" v-for="(item, index) in typeList" :key="index"
-						:class="{ active: tempSelectedIndex === index }" @click="selectTypeItem(index)">
-						<text>{{ item.num }} - {{ item.name }}</text>
-						<icon type="success_no_circle" size="16" color="#C1001C" v-if="tempSelectedIndex === index">
-						</icon>
+				<!-- 层级面包屑 -->
+				<view class="breadcrumb-bar" v-if="typePickerLevel > 1">
+					<text class="back-btn" @click="goBackModelLevel">← 返回</text>
+					<text class="breadcrumb-path">
+						{{typeBreadcrumbs.map(b => b.label).join(' / ')}}
+					</text>
+				</view>
+				<!-- 列表区 -->
+				<scroll-view scroll-y class="item-list">
+					<view class="option-item kind-item" v-for="(item, index) in modelKindList" :key="'mk'+index"
+						@click="drillDownType(item)">
+						<text class="option-text kind-text">{{ item.num }} - {{ item.name }}</text>
+						<text class="arrow-right">›</text>
+					</view>
+					<view class="option-item" v-for="(item, index) in modelItemList" :key="'mi'+index"
+						:class="{ active: tempSelectedIndex === index }">
+						<text class="option-text" @click="selectTypeItem(index)">{{ item.num }} - {{ item.name }}</text>
 					</view>
 				</scroll-view>
 			</view>
@@ -154,6 +166,7 @@
 					<text class="popup-title">选择表面处理</text>
 					<text class="confirm-text" @click="confirmSurfaceSelect">确定</text>
 				</view>
+				<!-- 搜索栏 -->
 				<view class="search-bar">
 					<view class="search-input-wrap">
 						<text class="search-icon">🔍</text>
@@ -161,12 +174,22 @@
 							@input="onSurfaceSearch" />
 					</view>
 				</view>
-				<scroll-view scroll-y class="item-list" @scrolltolower="loadMoreSurface">
-					<view class="option-item" v-for="(item, index) in surfaceList" :key="index"
-						:class="{ active: tempSurfaceIndex === index }" @click="selectSurfaceItem(index)">
-						<text>{{ item.num }} - {{ item.name }}</text>
-						<icon type="success_no_circle" size="16" color="#C1001C" v-if="tempSurfaceIndex === index">
-						</icon>
+				<!-- 层级面包屑 -->
+				<view class="breadcrumb-bar" v-if="surfacePickerLevel > 1">
+					<text class="back-btn" @click="goBackSurfaceLevel">← 返回</text>
+					<text class="breadcrumb-path">{{surfaceBreadcrumbs.map(b => b.label).join(' / ')}}</text>
+				</view>
+				<!-- 列表区 -->
+				<scroll-view scroll-y class="item-list">
+					<view class="option-item kind-item" v-for="(item, index) in colorKindList" :key="'ck'+index"
+						@click="drillDownSurface(item)">
+						<text class="option-text kind-text">{{ item.num }} - {{ item.name }}</text>
+						<text class="arrow-right">›</text>
+					</view>
+					<view class="option-item" v-for="(item, index) in colorItemList" :key="'ci'+index"
+						:class="{ active: tempSurfaceIndex === index }">
+						<text class="option-text" @click="selectSurfaceItem(index)">{{ item.num }} - {{ item.name
+						}}</text>
 					</view>
 				</scroll-view>
 			</view>
@@ -203,9 +226,11 @@
 
 <script>
 import ErpNavBar from '@/components/erp-nav-bar.vue';
-import { listPageColor } from '@/api/erp/color.js';
+import { listModel } from '@/api/erp/model.js';
+import { listModelKind } from '@/api/erp/modelKind.js';
+import { listColor } from '@/api/erp/color.js';
+import { listColorKind } from '@/api/erp/colorKind.js';
 import { listPagePack } from '@/api/erp/pack.js';
-import { listPageModel } from '@/api/erp/model.js';
 import { addOrderDetail } from '@/api/erp/orderDetail.js';
 export default {
 	components: { ErpNavBar },
@@ -214,21 +239,21 @@ export default {
 			itemIndex: -1,
 			showTypePicker: false, showSurfacePicker: false, showPackagePicker: false,
 			tempSelectedIndex: -1, tempSurfaceIndex: -1, tempPackageIndex: -1,
-			typeList: [],
-			surfaceList: [],
+			modelKindList: [],
+			modelItemList: [],
+			colorKindList: [],
+			colorItemList: [],
 			packageList: [],
-			// 搜索关键词
 			typeSearchKey: '', surfaceSearchKey: '', packageSearchKey: '',
-			// 防抖定时器
-			typeSearchTimer: null, surfaceSearchTimer: null, packageSearchTimer: null,
-			// 分页参数
-			typePageNum: 1, typePageSize: 20, typeHasMore: true, typeLoading: false,
-			surfacePageNum: 1, surfacePageSize: 20, surfaceHasMore: true, surfaceLoading: false,
-			packagePageNum: 1, packagePageSize: 20, packageHasMore: true, packageLoading: false,
+			typeBreadcrumbs: [],
+			typePickerLevel: 1,
+			surfaceBreadcrumbs: [],
+			surfacePickerLevel: 1,
+			packagePageNum: 1, packagePageSize: 10, packageHasMore: true, packageLoading: false,
 			formData: {
 				type: '', modelId: '', name: '', material: '',
 				surfaceName: '', surfaceId: '', packageMethod: '', packageId: '', length: '',
-				wallThickness: '', count: ''
+				wallThickness: '', count: '', meterWeight: ''
 			}
 		}
 	},
@@ -246,57 +271,16 @@ export default {
 				this.formData.packageId = item.packId || '';
 				this.formData.length = item.length || '';
 				this.formData.wallThickness = item.wallThickness || '';
+				this.formData.meterWeight = item.meterWeight || '';
 				this.formData.count = item.count || '';
 				this.itemIndex = parseInt(options.index || -1);
 			} catch (e) {
 				console.error('Data parse error', e);
 			}
 		}
-		await Promise.all([
-			this.loadModels(),
-			this.loadSurfaceKinds(),
-			this.loadPackageMethods()
-		]);
+		await this.loadPackageMethods();
 	},
 	methods: {
-		async loadModels(pageNum = 1, keyword = '') {
-			if (this.typeLoading) return;
-			this.typeLoading = true;
-			try {
-				const res = await listPageModel({ pageNum, pageSize: this.typePageSize, name: keyword });
-				const rows = res.rows || [];
-				const total = res.total || 0;
-				if (pageNum === 1) {
-					this.typeList = rows;
-				} else {
-					this.typeList = [...this.typeList, ...rows];
-				}
-				this.typeHasMore = this.typeList.length < total;
-			} catch (e) {
-				console.error('加载型号失败', e);
-			} finally {
-				this.typeLoading = false;
-			}
-		},
-		async loadSurfaceKinds(pageNum = 1, keyword = '') {
-			if (this.surfaceLoading) return;
-			this.surfaceLoading = true;
-			try {
-				const res = await listPageColor({ pageNum, pageSize: this.surfacePageSize, name: keyword });
-				let rows = (res.rows || []).filter(item => item.name);
-				const total = res.total || 0;
-				if (pageNum === 1) {
-					this.surfaceList = rows;
-				} else {
-					this.surfaceList = [...this.surfaceList, ...rows];
-				}
-				this.surfaceHasMore = this.surfaceList.length < total;
-			} catch (e) {
-				console.error('加载表面处理失败', e);
-			} finally {
-				this.surfaceLoading = false;
-			}
-		},
 		async loadPackageMethods(pageNum = 1, keyword = '') {
 			if (this.packageLoading) return;
 			this.packageLoading = true;
@@ -316,38 +300,12 @@ export default {
 				this.packageLoading = false;
 			}
 		},
-		loadMoreType() {
-			if (this.typeHasMore && !this.typeLoading) {
-				this.typePageNum++;
-				this.loadModels(this.typePageNum, this.typeSearchKey);
-			}
-		},
-		loadMoreSurface() {
-			if (this.surfaceHasMore && !this.surfaceLoading) {
-				this.surfacePageNum++;
-				this.loadSurfaceKinds(this.surfacePageNum, this.surfaceSearchKey);
-			}
-		},
 		loadMorePackage() {
 			if (this.packageHasMore && !this.packageLoading) {
 				this.packagePageNum++;
 				this.loadPackageMethods(this.packagePageNum, this.packageSearchKey);
 			}
 		},
-		onTypeSearch() {
-			if (this.typeSearchTimer) clearTimeout(this.typeSearchTimer);
-			this.typeSearchTimer = setTimeout(() => {
-				this.typePageNum = 1;
-				this.loadModels(1, this.typeSearchKey);
-			}, 300);
-		},
-		onSurfaceSearch() {
-			if (this.surfaceSearchTimer) clearTimeout(this.surfaceSearchTimer);
-			this.surfaceSearchTimer = setTimeout(() => {
-				this.surfacePageNum = 1;
-				this.loadSurfaceKinds(1, this.surfaceSearchKey);
-			}, 300);
-		},
 		onPackageSearch() {
 			if (this.packageSearchTimer) clearTimeout(this.packageSearchTimer);
 			this.packageSearchTimer = setTimeout(() => {
@@ -355,46 +313,157 @@ export default {
 				this.loadPackageMethods(1, this.packageSearchKey);
 			}, 300);
 		},
-		openTypePicker() {
-			if (this.formData.type) {
-				this.tempSelectedIndex = this.typeList.findIndex(item => item.num === this.formData.type);
-			} else {
-				this.tempSelectedIndex = -1;
+		/**
+		 * 加载型材系列,点击系列后同时加载子系列和具体型号
+		 * @Author: Trae
+		 */
+		async loadTypes(parentRowId) {
+			try {
+				const keyword = (this.typeSearchKey || '').trim();
+				const kindParams = { name: keyword || undefined };
+				if (parentRowId) {
+					kindParams.parentRowId = parentRowId;
+				}
+				const kindRes = await listModelKind(kindParams);
+				this.modelKindList = kindRes.data || [];
+				if (parentRowId) {
+					const modelParams = { name: keyword || undefined, parentRowId };
+					const modelRes = await listModel(modelParams);
+					this.modelItemList = modelRes.data || [];
+				} else {
+					this.modelItemList = [];
+				}
+			} catch (e) {
+				uni.showToast({ title: e || '加载型号失败', icon: 'none' });
+			}
+		},
+		onTypeSearch() {
+			const parentRowId = this.typeBreadcrumbs.length > 0
+				? this.typeBreadcrumbs[this.typeBreadcrumbs.length - 1].rowId : '';
+			this.loadTypes(parentRowId);
+		},
+		/**
+		 * 加载表面处理系列,点击系列后同时加载子系列和具体颜色
+		 * @Author: Trae
+		 */
+		async loadSurfaces(parentRowId) {
+			try {
+				const keyword = (this.surfaceSearchKey || '').trim();
+				const kindParams = { name: keyword || undefined };
+				if (parentRowId) {
+					kindParams.parentRowId = parentRowId;
+				}
+				const kindRes = await listColorKind(kindParams);
+				this.colorKindList = kindRes.data || [];
+				if (parentRowId) {
+					const colorParams = { name: keyword || undefined, parentRowId };
+					const colorRes = await listColor(colorParams);
+					this.colorItemList = colorRes.data || [];
+				} else {
+					this.colorItemList = [];
+				}
+			} catch (e) {
+				uni.showToast({ title: e || '加载表面处理失败', icon: 'none' });
 			}
+		},
+		onSurfaceSearch() {
+			const parentRowId = this.surfaceBreadcrumbs.length > 0
+				? this.surfaceBreadcrumbs[this.surfaceBreadcrumbs.length - 1].rowId : '';
+			this.loadSurfaces(parentRowId);
+		},
+		/**
+		 * 打开型号选择器
+		 * @Author: Trae
+		 */
+		openTypePicker() {
+			this.typePickerLevel = 1;
+			this.typeBreadcrumbs = [];
+			this.tempSelectedIndex = -1;
 			this.showTypePicker = true;
+			this.loadTypes('');
+		},
+		/**
+		 * 钻取到子级
+		 * @Author: Trae
+		 */
+		drillDownType(item) {
+			this.typeBreadcrumbs.push({ rowId: item.rowId, label: item.num });
+			this.typePickerLevel++;
+			this.tempSelectedIndex = -1;
+			this.loadTypes(item.rowId);
+		},
+		goBackModelLevel() {
+			this.typeBreadcrumbs.pop();
+			this.typePickerLevel--;
+			this.tempSelectedIndex = -1;
+			const parentRowId = this.typeBreadcrumbs.length > 0
+				? this.typeBreadcrumbs[this.typeBreadcrumbs.length - 1].rowId : '';
+			this.loadTypes(parentRowId);
 		},
 		closeTypePicker() {
 			this.showTypePicker = false;
 			this.typeSearchKey = '';
-			this.typePageNum = 1;
+			this.typePickerLevel = 1;
+			this.typeBreadcrumbs = [];
+			this.tempSelectedIndex = -1;
 		},
 		selectTypeItem(index) { this.tempSelectedIndex = index; },
 		confirmTypeSelect() {
-			if (this.tempSelectedIndex === -1) return;
-			const item = this.typeList[this.tempSelectedIndex];
+			if (this.tempSelectedIndex === -1) {
+				uni.showToast({ title: '请先选择一个选项', icon: 'none' });
+				return;
+			}
+			const item = this.modelItemList[this.tempSelectedIndex];
 			this.formData.type = item.num;
 			this.formData.modelId = item.rowId;
 			this.formData.name = item.name;
 			this.formData.material = '6063-T5';
+			this.formData.meterWeight = item.meterWt || '';
 			this.closeTypePicker();
 		},
+		/**
+		 * 打开表面处理选择器
+		 * @Author: Trae
+		 */
 		openSurfacePicker() {
-			if (this.formData.surfaceName) {
-				this.tempSurfaceIndex = this.surfaceList.findIndex(item => item.name === this.formData.surfaceName);
-			} else {
-				this.tempSurfaceIndex = -1;
-			}
+			this.surfacePickerLevel = 1;
+			this.surfaceBreadcrumbs = [];
+			this.tempSurfaceIndex = -1;
 			this.showSurfacePicker = true;
+			this.loadSurfaces('');
+		},
+		/**
+		 * 钻取到子级
+		 * @Author: Trae
+		 */
+		drillDownSurface(item) {
+			this.surfaceBreadcrumbs.push({ rowId: item.rowId, label: item.num });
+			this.surfacePickerLevel++;
+			this.tempSurfaceIndex = -1;
+			this.loadSurfaces(item.rowId);
+		},
+		goBackSurfaceLevel() {
+			this.surfaceBreadcrumbs.pop();
+			this.surfacePickerLevel--;
+			this.tempSurfaceIndex = -1;
+			const parentRowId = this.surfaceBreadcrumbs.length > 0
+				? this.surfaceBreadcrumbs[this.surfaceBreadcrumbs.length - 1].rowId : '';
+			this.loadSurfaces(parentRowId);
 		},
 		closeSurfacePicker() {
 			this.showSurfacePicker = false;
 			this.surfaceSearchKey = '';
-			this.surfacePageNum = 1;
+			this.surfacePickerLevel = 1;
+			this.surfaceBreadcrumbs = [];
+			this.tempSurfaceIndex = -1;
 		},
 		selectSurfaceItem(index) { this.tempSurfaceIndex = index; },
 		confirmSurfaceSelect() {
-			if (this.tempSurfaceIndex === -1) return;
-			const item = this.surfaceList[this.tempSurfaceIndex];
+			if (this.tempSurfaceIndex === -1) {
+				uni.showToast({ title: '请先选择一个选项', icon: 'none' });
+				return;
+			}
+			const item = this.colorItemList[this.tempSurfaceIndex];
 			this.formData.surfaceName = item.name;
 			this.formData.surfaceId = item.rowId;
 			this.closeSurfacePicker();
@@ -742,6 +811,51 @@ export default {
 	font-weight: bold;
 }
 
+.kind-item {
+	background: #fafbfc;
+}
+
+.kind-text {
+	color: #556;
+}
+
+.option-text {
+	flex: 1;
+	padding: 10rpx 0;
+}
+
+/* 级联面包屑 */
+.breadcrumb-bar {
+	display: flex;
+	align-items: center;
+	padding: 16rpx 40rpx;
+	background: #fafafa;
+	border-bottom: 1rpx solid #f0f0f0;
+}
+
+.back-btn {
+	font-size: 28rpx;
+	color: #C1001C;
+	padding-right: 20rpx;
+	flex-shrink: 0;
+}
+
+.breadcrumb-path {
+	font-size: 26rpx;
+	color: #666;
+	flex: 1;
+	overflow: hidden;
+	text-overflow: ellipsis;
+	white-space: nowrap;
+}
+
+.arrow-right {
+	font-size: 56rpx;
+	color: #bbb;
+	padding: 10rpx 0 10rpx 20rpx;
+	font-weight: 300;
+}
+
 .bottom-placeholder {
 	height: 60rpx;
 }

+ 705 - 36
pages/order/index.vue

@@ -47,26 +47,45 @@
 
 					<!-- 列表头部:仅在有数据时显示 -->
 					<view class="list-header" v-if="selectedModels.length > 0">
-						<text class="header-text">已选型号列表:</text>
+						<view class="header-left">
+							<text class="header-text" v-if="!batchMode">已选型号列表:</text>
+							<text class="header-text" v-else>批量修改({{ selectedIds.length }}/{{ selectedModels.length
+							}})</text>
+							<view class="select-all-toggle" v-if="batchMode" @click="toggleSelectAll">
+								<text class="select-all-icon" :class="{ checked: isAllSelected }">☐</text>
+								<text class="select-all-text">{{ isAllSelected ? '取消全选' : '全选' }}</text>
+							</view>
+						</view>
+						<view class="batch-toggle" :class="{ active: batchMode }" @click="toggleBatchMode">
+							<text>{{ batchMode ? '退出' : '批量修改' }}</text>
+						</view>
 					</view>
 
 					<!-- 1. 列表渲染 (精品化设计) -->
 					<view class="model-item-card" v-for="(item, index) in selectedModels" :key="index"
-						v-if="selectedModels.length > 0" @click="editItem(index, item)">
-						<view class="remove-icon" @click.stop="removeItem(index)">
-							<text class="x-icon">×</text>
+						v-if="selectedModels.length > 0"
+						:class="{ 'batch-mode-card': batchMode, 'batch-selected': batchMode && isSelected(item.rowId) }">
+						<view class="batch-checkbox" v-if="batchMode" @click.stop="toggleSelect(item.rowId)">
+							<view class="checkbox-circle" :class="{ checked: isSelected(item.rowId) }">
+								<text class="check-icon" v-if="isSelected(item.rowId)">✓</text>
+							</view>
 						</view>
-						<view class="card-line">
-							<view class="model-info">
-								<text class="model-value">{{ item.modelNum }}</text>
+						<view class="card-body" @click="batchMode ? toggleSelect(item.rowId) : editItem(index, item)">
+							<view class="remove-icon" @click.stop="removeItem(index)">
+								<text class="x-icon">×</text>
 							</view>
-							<view class="count-tag">
-								支数<text class="count-num">{{ item.count }}</text>
+							<view class="card-line">
+								<view class="model-info">
+									<text class="model-value">{{ item.modelNum }}</text>
+								</view>
+								<view class="count-tag">
+									支数<text class="count-num">{{ item.count }}</text>
+								</view>
+							</view>
+							<view class="card-line secondary">
+								<text class="surface-label">表面名称:</text>
+								<text class="surface-text">{{ item.surfaceName }}</text>
 							</view>
-						</view>
-						<view class="card-line secondary">
-							<text class="surface-label">表面名称:</text>
-							<text class="surface-text">{{ item.surfaceName }}</text>
 						</view>
 					</view>
 
@@ -85,13 +104,25 @@
 				</view>
 			</scroll-view>
 
-			<!-- 悬浮添加按钮 (仅在有数据时显示) -->
-			<view class="floating-add-btn" @click="goToAddModel" v-if="selectedModels.length > 0">
+			<!-- 悬浮添加按钮 (仅在有数据时且非批量模式显示) -->
+			<view class="floating-add-btn" @click="goToAddModel" v-if="selectedModels.length > 0 && !batchMode">
 				<view class="plus-icon"></view>
 			</view>
 
-			<!-- 底部支付/下单汇总栏 (仅在有数据时显示) -->
-			<view class="footer-summary-bar" v-if="selectedModels.length > 0">
+			<!-- 批量操作栏(批量模式下常驻) -->
+			<view class="batch-action-bar" v-if="batchMode">
+				<view class="batch-count" v-if="selectedIds.length > 0">已选 {{ selectedIds.length }} 项</view>
+				<view class="batch-count placeholder" v-else>请勾选型号</view>
+				<view class="batch-btns">
+					<button class="batch-btn surface-btn" :disabled="selectedIds.length === 0"
+						@click="openBatchSurfacePicker">批量修改表面</button>
+					<button class="batch-btn pack-btn" :disabled="selectedIds.length === 0"
+						@click="openBatchPackPicker">批量修改包装</button>
+				</view>
+			</view>
+
+			<!-- 底部支付/下单汇总栏 (仅在有数据时且非批量模式显示) -->
+			<view class="footer-summary-bar" v-if="selectedModels.length > 0 && !batchMode">
 				<view class="summary-info">
 					<text class="count-label">共计:</text>
 					<text class="num-highlight">{{ selectedModels.length }}</text>
@@ -107,17 +138,90 @@
 
 		<!-- 底部菜单栏 -->
 		<erp-tab-bar active="order"></erp-tab-bar>
+
+		<!-- 批量修改 - 表面处理选择弹层 -->
+		<view class="custom-picker-mask" v-if="showBatchSurfacePicker" @click="showBatchSurfacePicker = false"
+			@touchmove.stop.prevent>
+			<view class="picker-popup" @click.stop>
+				<view class="popup-header">
+					<text class="cancel-text" @click="showBatchSurfacePicker = false">取消</text>
+					<text class="popup-title">选择表面处理</text>
+					<text class="confirm-text" @click="confirmBatchSurface">确定</text>
+				</view>
+				<!-- 搜索栏 -->
+				<view class="search-bar">
+					<view class="search-input-wrap">
+						<text class="search-icon">🔍</text>
+						<input class="search-input" type="text" v-model="batchSurfaceSearchKey" placeholder="输入名称检索"
+							@input="onBatchSurfaceSearch" />
+					</view>
+				</view>
+				<!-- 层级面包屑 -->
+				<view class="breadcrumb-bar" v-if="batchSurfaceLevel > 1">
+					<text class="back-btn" @click="goBackBatchSurfaceLevel">← 返回</text>
+					<text class="breadcrumb-path">{{batchSurfaceBreadcrumbs.map(b => b.label).join(' / ')}}</text>
+				</view>
+				<!-- 列表区 -->
+				<scroll-view scroll-y class="item-list">
+					<view class="option-item kind-item" v-for="(item, index) in batchSurfaceKindList" :key="'bsk'+index"
+						@click="drillDownBatchSurface(item)">
+						<text class="option-text kind-text">{{ item.num }} - {{ item.name }}</text>
+						<text class="arrow-right">›</text>
+					</view>
+					<view class="option-item" v-for="item in batchSurfaceItemList" :key="'bsi'+item.rowId"
+						:class="{ active: batchSurfaceId === item.rowId }">
+						<text class="option-text" @click="selectBatchSurfaceItem(item)">{{ item.num }} - {{ item.name
+						}}</text>
+					</view>
+				</scroll-view>
+			</view>
+		</view>
+
+		<!-- 批量修改 - 包装方式选择弹层 -->
+		<view class="custom-picker-mask" v-if="showBatchPackPicker" @click="showBatchPackPicker = false"
+			@touchmove.stop.prevent>
+			<view class="picker-popup" @click.stop>
+				<view class="popup-header">
+					<text class="cancel-text" @click="showBatchPackPicker = false">取消</text>
+					<text class="popup-title">选择包装方式</text>
+					<text class="confirm-text" @click="confirmBatchPack">确定</text>
+				</view>
+				<view class="search-bar">
+					<view class="search-input-wrap">
+						<text class="search-icon">🔍</text>
+						<input class="search-input" type="text" v-model="batchPackSearchKey" placeholder="输入名称检索"
+							@input="onBatchPackSearch" />
+					</view>
+				</view>
+				<scroll-view scroll-y class="item-list" @scrolltolower="loadMorePackBatch">
+					<view class="option-item" v-for="item in batchPackList" :key="item.rowId"
+						:class="{ active: batchPackId === item.rowId }"
+						@click="batchPackId = item.rowId; batchPackName = item.name">
+						<text>{{ item.num }} - {{ item.name }}</text>
+						<icon type="success_no_circle" size="16" color="#C1001C" v-if="batchPackId === item.rowId">
+						</icon>
+					</view>
+				</scroll-view>
+			</view>
+		</view>
+
+		<order-confirm-popup :visible="showConfirmPopup" :models="selectedModels" :clientName="confirmClientName"
+			:submitting="confirmSubmitting" @confirm="handleConfirmOrder" @close="showConfirmPopup = false" />
 	</view>
 </template>
 
 <script>
 import ErpTabBar from '@/components/erp-tab-bar.vue';
 import ErpNavBar from '@/components/erp-nav-bar.vue';
+import OrderConfirmPopup from './components/order-confirm-popup.vue';
 import { getMyInfo } from '@/api/system/customer.js';
-import { listOrderDetail } from '@/api/erp/orderDetail.js';
+import { listOrderDetail, batchUpdateOrderDetail } from '@/api/erp/orderDetail.js';
+import { listColor } from '@/api/erp/color.js';
+import { listColorKind } from '@/api/erp/colorKind.js';
+import { listPagePack } from '@/api/erp/pack.js';
 import { addOrder } from '@/api/erp/order.js';
 export default {
-	components: { ErpNavBar, ErpTabBar },
+	components: { ErpNavBar, ErpTabBar, OrderConfirmPopup },
 	data() {
 		return {
 			isLoggedIn: false,
@@ -125,12 +229,41 @@ export default {
 			myInfo: {},
 			selectedModels: [],
 			selectedClientId: '',
-			selectedClientName: ''
+			selectedClientName: '',
+			// 批量修改相关
+			batchMode: false,
+			selectedIds: [],
+			// 表面处理选择器
+			showBatchSurfacePicker: false,
+			batchSurfaceId: '',
+			batchSurfaceName: '',
+			batchSurfaceSearchKey: '',
+			batchSurfaceBreadcrumbs: [],
+			batchSurfaceLevel: 1,
+			batchSurfaceKindList: [],
+			batchSurfaceItemList: [],
+			// 包装方式选择器
+			showBatchPackPicker: false,
+			batchPackList: [],
+			batchPackPageNum: 1,
+			batchPackHasMore: true,
+			batchPackLoading: false,
+			batchPackId: '',
+			batchPackName: '',
+			batchPackSearchKey: '',
+			batchPackSearchTimer: null,
+			showConfirmPopup: false,
+			confirmClientId: '',
+			confirmClientName: '',
+			confirmSubmitting: false
 		}
 	},
 	computed: {
 		totalCount() {
 			return this.selectedModels.reduce((sum, item) => sum + parseInt(item.count || 0), 0);
+		},
+		isAllSelected() {
+			return this.selectedModels.length > 0 && this.selectedIds.length === this.selectedModels.length;
 		}
 	},
 	onLoad() {
@@ -181,7 +314,6 @@ export default {
 				const res = await getMyInfo();
 				this.isLoggedIn = true;
 				this.myInfo = res.data || {};
-				// 登录成功后加载列表
 				this.loadOrderItems();
 			} catch (e) {
 				uni.showToast({ title: e || '登录状态校验失败', icon: 'none' });
@@ -220,6 +352,240 @@ export default {
 				}
 			});
 		},
+		/**
+		 * 切换全选 / 取消全选
+		 * @Author: Trae
+		 */
+		toggleSelectAll() {
+			if (this.isAllSelected) {
+				this.selectedIds = [];
+			} else {
+				this.selectedIds = this.selectedModels.map(item => item.rowId);
+			}
+		},
+		/**
+		 * 切换批量修改模式
+		 * @Author: Trae
+		 */
+		toggleBatchMode() {
+			this.batchMode = !this.batchMode;
+			if (!this.batchMode) {
+				this.selectedIds = [];
+			}
+		},
+		/**
+		 * 切换选中状态
+		 * @Author: Trae
+		 */
+		toggleSelect(rowId) {
+			const idx = this.selectedIds.indexOf(rowId);
+			if (idx > -1) {
+				this.selectedIds.splice(idx, 1);
+			} else {
+				this.selectedIds.push(rowId);
+			}
+		},
+		/**
+		 * 判断是否已选中
+		 * @Author: Trae
+		 */
+		isSelected(rowId) {
+			return this.selectedIds.indexOf(rowId) > -1;
+		},
+		/**
+		 * 打开批量修改表面处理选择器(二级级联)
+		 * @Author: Trae
+		 */
+		openBatchSurfacePicker() {
+			if (this.selectedIds.length === 0) {
+				uni.showToast({ title: '请先选择型号', icon: 'none' });
+				return;
+			}
+			this.showBatchSurfacePicker = true;
+			this.batchSurfaceId = '';
+			this.batchSurfaceName = '';
+			this.batchSurfaceSearchKey = '';
+			this.batchSurfaceLevel = 1;
+			this.batchSurfaceBreadcrumbs = [];
+			this.loadBatchSurfaceList('');
+		},
+		/**
+		 * 加载表面处理系列,点击系列后同时加载子系列和具体颜色(批量选择器)
+		 * @Author: Trae
+		 */
+		async loadBatchSurfaceList(parentRowId) {
+			try {
+				const keyword = (this.batchSurfaceSearchKey || '').trim();
+				const kindParams = { name: keyword || undefined };
+				if (parentRowId) {
+					kindParams.parentRowId = parentRowId;
+				}
+				const kindRes = await listColorKind(kindParams);
+				this.batchSurfaceKindList = kindRes.data || [];
+				if (parentRowId) {
+					const colorParams = { name: keyword || undefined, parentRowId };
+					const colorRes = await listColor(colorParams);
+					this.batchSurfaceItemList = colorRes.data || [];
+				} else {
+					this.batchSurfaceItemList = [];
+				}
+			} catch (e) {
+				uni.showToast({ title: e || '加载表面处理失败', icon: 'none' });
+			}
+		},
+		/**
+		 * 表面处理搜索
+		 * @Author: Trae
+		 */
+		onBatchSurfaceSearch() {
+			const parentRowId = this.batchSurfaceBreadcrumbs.length > 0
+				? this.batchSurfaceBreadcrumbs[this.batchSurfaceBreadcrumbs.length - 1].rowId : '';
+			this.loadBatchSurfaceList(parentRowId);
+		},
+		selectBatchSurfaceItem(item) {
+			this.batchSurfaceId = item.rowId;
+			this.batchSurfaceName = item.name;
+		},
+		drillDownBatchSurface(item) {
+			this.batchSurfaceBreadcrumbs.push({ rowId: item.rowId, label: item.num });
+			this.batchSurfaceId = '';
+			this.batchSurfaceName = '';
+			this.batchSurfaceLevel++;
+			this.loadBatchSurfaceList(item.rowId);
+		},
+		goBackBatchSurfaceLevel() {
+			this.batchSurfaceBreadcrumbs.pop();
+			this.batchSurfaceLevel--;
+			this.batchSurfaceId = '';
+			this.batchSurfaceName = '';
+			const parentRowId = this.batchSurfaceBreadcrumbs.length > 0
+				? this.batchSurfaceBreadcrumbs[this.batchSurfaceBreadcrumbs.length - 1].rowId : '';
+			this.loadBatchSurfaceList(parentRowId);
+		},
+		/**
+		 * 确认批量修改表面处理
+		 * @Author: Trae
+		 */
+		async confirmBatchSurface() {
+			if (!this.batchSurfaceId) {
+				uni.showToast({ title: '请选择表面处理', icon: 'none' });
+				return;
+			}
+			this.showBatchSurfacePicker = false;
+			uni.showLoading({ title: '修改中...', mask: true });
+			try {
+				await batchUpdateOrderDetail({
+					rowIds: this.selectedIds,
+					surfaceId: this.batchSurfaceId,
+					surfaceName: this.batchSurfaceName
+				});
+				uni.hideLoading();
+				uni.showToast({ title: '修改成功', icon: 'success' });
+				this.selectedIds = [];
+				this.loadOrderItems(false);
+			} catch (e) {
+				uni.hideLoading();
+				uni.showToast({ title: e || '修改失败', icon: 'none' });
+			}
+			this.batchSurfaceLevel = 1;
+			this.batchSurfaceBreadcrumbs = [];
+			this.batchSurfaceId = '';
+			this.batchSurfaceName = '';
+		},
+		/**
+		 * 打开批量修改包装方式选择器
+		 * @Author: Trae
+		 */
+		openBatchPackPicker() {
+			if (this.selectedIds.length === 0) {
+				uni.showToast({ title: '请先选择型号', icon: 'none' });
+				return;
+			}
+			this.showBatchPackPicker = true;
+			this.batchPackId = '';
+			this.batchPackName = '';
+			this.batchPackSearchKey = '';
+			this.batchPackPageNum = 1;
+			if (this.batchPackList.length === 0) {
+				this.loadBatchPackList();
+			}
+		},
+		/**
+		 * 加载包装方式列表(批量选择器)
+		 * @Author: Trae
+		 */
+		async loadBatchPackList(pageNum = 1, keyword = '') {
+			if (this.batchPackLoading) return;
+			this.batchPackLoading = true;
+			try {
+				const res = await listPagePack({
+					pageNum,
+					pageSize: 10,
+					name: keyword || undefined
+				});
+				const rows = res.rows || [];
+				if (pageNum === 1) {
+					this.batchPackList = rows;
+				} else {
+					this.batchPackList = [...this.batchPackList, ...rows];
+				}
+				this.batchPackHasMore = rows.length >= 20;
+			} catch (e) {
+				uni.showToast({ title: e || '加载包装方式失败', icon: 'none' });
+			} finally {
+				this.batchPackLoading = false;
+			}
+		},
+		/**
+		 * 加载更多包装方式列表
+		 * @Author: Trae
+		 */
+		loadMorePackBatch() {
+			if (!this.batchPackHasMore || this.batchPackLoading) return;
+			this.batchPackPageNum++;
+			this.loadBatchPackList(this.batchPackPageNum, this.batchPackSearchKey);
+		},
+		/**
+		 * 包装方式搜索(防抖)
+		 * @Author: Trae
+		 */
+		onBatchPackSearch() {
+			if (this.batchPackSearchTimer) clearTimeout(this.batchPackSearchTimer);
+			this.batchPackSearchTimer = setTimeout(() => {
+				this.batchPackPageNum = 1;
+				this.loadBatchPackList(1, this.batchPackSearchKey);
+			}, 300);
+		},
+		/**
+		 * 确认批量修改包装方式
+		 * @Author: Trae
+		 */
+		async confirmBatchPack() {
+			if (!this.batchPackId) {
+				uni.showToast({ title: '请选择包装方式', icon: 'none' });
+				return;
+			}
+			this.showBatchPackPicker = false;
+			uni.showLoading({ title: '修改中...', mask: true });
+			try {
+				await batchUpdateOrderDetail({
+					rowIds: this.selectedIds,
+					packId: this.batchPackId,
+					packName: this.batchPackName
+				});
+				uni.hideLoading();
+				uni.showToast({ title: '修改成功', icon: 'success' });
+				this.selectedIds = [];
+				this.loadOrderItems(false);
+			} catch (e) {
+				uni.hideLoading();
+				uni.showToast({ title: e || '修改失败', icon: 'none' });
+			}
+		},
+		/**
+		 * 提交最终订单 - 选择客户后弹出确认弹窗
+		 * @Author: Antigravity
+		 */
 		async submitFinalOrder() {
 			if (this.selectedModels.length === 0) return;
 
@@ -229,15 +595,15 @@ export default {
 				return;
 			}
 
-			// 单个授权客户:直接
+			// 单个授权客户:直接弹出
 			if (clientList.length === 1) {
-				this.selectedClientId = clientList[0].rowId;
-				this.selectedClientName = clientList[0].name;
-				this.doSubmitOrder();
+				this.confirmClientId = clientList[0].rowId;
+				this.confirmClientName = clientList[0].name;
+				this.showConfirmPopup = true;
 				return;
 			}
 
-			// 多个授权客户:弹出选择器
+			// 多个授权客户:先选择再弹出
 			const clientNames = clientList.map(c => c.name);
 			const res = await new Promise((resolve) => {
 				uni.showActionSheet({
@@ -248,25 +614,28 @@ export default {
 			});
 			if (res && res.tapIndex !== undefined) {
 				const chosen = clientList[res.tapIndex];
-				this.selectedClientId = chosen.rowId;
-				this.selectedClientName = chosen.name;
-				this.doSubmitOrder();
+				this.confirmClientId = chosen.rowId;
+				this.confirmClientName = chosen.name;
+				this.showConfirmPopup = true;
 			}
 		},
 		/**
-		 * 执行下单请求
-		 * @Author: Antigravity
+		 * 确认弹窗回调 - 正式执行下单
+		 * @Author: Trae
 		 */
-		async doSubmitOrder() {
+		async handleConfirmOrder() {
+			this.confirmSubmitting = true;
 			uni.showLoading({ title: '正在提交订单', mask: true });
 			try {
 				const detailIds = this.selectedModels.map(item => item.rowId);
 				const submitRes = await addOrder({
 					detailIds,
-					clientId: this.selectedClientId,
-					clientName: this.selectedClientName
+					clientId: this.confirmClientId,
+					clientName: this.confirmClientName
 				});
 				uni.hideLoading();
+				this.confirmSubmitting = false;
+				this.showConfirmPopup = false;
 				const orderId = submitRes.data;
 				uni.showToast({ title: '下单成功', icon: 'success' });
 				setTimeout(() => {
@@ -276,6 +645,7 @@ export default {
 				}, 1500);
 			} catch (e) {
 				uni.hideLoading();
+				this.confirmSubmitting = false;
 				uni.showToast({ title: e || '下单失败', icon: 'none' });
 			}
 		}
@@ -391,6 +761,17 @@ export default {
 
 .list-header {
 	padding: 10rpx 0 20rpx;
+	display: flex;
+	align-items: center;
+	justify-content: space-between;
+	flex-wrap: wrap;
+	gap: 10rpx;
+}
+
+.header-left {
+	display: flex;
+	align-items: center;
+	gap: 16rpx;
 }
 
 .header-text {
@@ -412,6 +793,29 @@ export default {
 	border-radius: 4rpx;
 }
 
+.select-all-toggle {
+	display: flex;
+	align-items: center;
+	gap: 6rpx;
+	padding: 6rpx 16rpx;
+	background: #f5f5f5;
+	border-radius: 20rpx;
+}
+
+.select-all-icon {
+	font-size: 28rpx;
+	color: #999;
+}
+
+.select-all-icon.checked {
+	color: #C1001C;
+}
+
+.select-all-text {
+	font-size: 22rpx;
+	color: #666;
+}
+
 /* 型号卡片:大幅升级美化 */
 .model-item-card {
 	background: #fff;
@@ -421,12 +825,57 @@ export default {
 	position: relative;
 	box-shadow: 0 8rpx 30rpx rgba(0, 0, 0, 0.04);
 	border: 1rpx solid rgba(0, 0, 0, 0.02);
+	display: flex;
+	align-items: stretch;
+	transition: border 0.25s, background 0.25s, transform 0.15s;
+}
+
+.model-item-card.batch-mode-card {
+	padding-left: 20rpx;
+	gap: 16rpx;
+}
+
+.batch-checkbox {
+	display: flex;
+	align-items: center;
+	justify-content: center;
+	padding-top: 6rpx;
+	flex-shrink: 0;
+}
+
+.checkbox-circle {
+	width: 44rpx;
+	height: 44rpx;
+	border-radius: 50%;
+	border: 2rpx solid #ddd;
+	display: flex;
+	align-items: center;
+	justify-content: center;
+	background: #fff;
+	transition: all 0.2s;
+}
+
+.checkbox-circle.checked {
+	background: #C1001C;
+	border-color: #C1001C;
+}
+
+.check-icon {
+	font-size: 26rpx;
+	color: #fff;
+	font-weight: bold;
+}
+
+.card-body {
+	flex: 1;
+	min-width: 0;
+	position: relative;
 }
 
 .remove-icon {
 	position: absolute;
-	right: 0;
-	top: 0;
+	right: -36rpx;
+	top: -36rpx;
 	width: 50rpx;
 	height: 50rpx;
 	background: rgba(255, 77, 79, 0.1);
@@ -755,4 +1204,224 @@ export default {
 	font-weight: bold;
 	color: #C1001C;
 }
+
+/* 批量修改按钮 */
+.batch-toggle {
+	flex-shrink: 0;
+	padding: 8rpx 24rpx;
+	background: #C1001C;
+	border-radius: 40rpx;
+	font-size: 24rpx;
+	color: #fff;
+	display: flex;
+	align-items: center;
+}
+
+.batch-selected {
+	border-color: #C1001C !important;
+	background: #FFF1F2 !important;
+}
+
+/* 批量操作栏 */
+.batch-action-bar {
+	position: fixed;
+	bottom: calc(110rpx + env(safe-area-inset-bottom));
+	left: 0;
+	width: 100%;
+	height: 110rpx;
+	background: #fff;
+	border-top: 1rpx solid #f0f0f0;
+	display: flex;
+	align-items: center;
+	justify-content: space-between;
+	padding: 0 30rpx;
+	box-sizing: border-box;
+	z-index: 100;
+	box-shadow: 0 -4rpx 20rpx rgba(0, 0, 0, 0.05);
+}
+
+.batch-count {
+	font-size: 28rpx;
+	color: #333;
+	font-weight: 500;
+}
+
+.batch-count.placeholder {
+	color: #ccc;
+}
+
+.batch-btns {
+	display: flex;
+	gap: 16rpx;
+}
+
+.batch-btn {
+	width: auto;
+	height: 64rpx;
+	padding: 0 28rpx;
+	border-radius: 32rpx;
+	font-size: 26rpx;
+	font-weight: 500;
+	display: flex;
+	align-items: center;
+	border: none;
+	line-height: 64rpx;
+	transition: opacity 0.2s;
+}
+
+.batch-btn[disabled] {
+	opacity: 0.4;
+}
+
+.surface-btn {
+	background: linear-gradient(135deg, #1890FF, #096DD9);
+	color: #fff;
+}
+
+.pack-btn {
+	background: linear-gradient(135deg, #52C41A, #389E0D);
+	color: #fff;
+}
+
+/* 选择器弹层(参照 edit-model 模式) */
+.custom-picker-mask {
+	position: fixed;
+	top: 0;
+	left: 0;
+	right: 0;
+	bottom: 0;
+	background: rgba(0, 0, 0, 0.4);
+	z-index: 1000;
+	display: flex;
+	align-items: flex-end;
+}
+
+.picker-popup {
+	width: 100%;
+	background: #fff;
+	border-radius: 32rpx 32rpx 0 0;
+	padding-bottom: calc(env(safe-area-inset-bottom) + 20rpx);
+	animation: slideUp 0.15s ease-out;
+}
+
+@keyframes slideUp {
+	from {
+		transform: translateY(100%);
+	}
+
+	to {
+		transform: translateY(0);
+	}
+}
+
+.popup-header {
+	display: flex;
+	justify-content: space-between;
+	align-items: center;
+	padding: 30rpx 40rpx;
+	border-bottom: 1rpx solid #f0f0f0;
+}
+
+.popup-title {
+	font-size: 32rpx;
+	font-weight: bold;
+	color: #333;
+}
+
+.cancel-text,
+.confirm-text {
+	font-size: 30rpx;
+	padding: 10rpx;
+}
+
+.confirm-text {
+	color: #C1001C;
+	font-weight: bold;
+}
+
+.item-list {
+	max-height: 50vh;
+	padding: 0 40rpx;
+}
+
+.option-item {
+	height: 110rpx;
+	display: flex;
+	align-items: center;
+	justify-content: space-between;
+	border-bottom: 1rpx solid #f8f8f8;
+	font-size: 32rpx;
+	color: #333;
+}
+
+.option-item.active {
+	color: #C1001C;
+	font-weight: bold;
+}
+
+.kind-item {
+	background: #fafbfc;
+}
+
+.kind-text {
+	color: #556;
+}
+
+.option-text {
+	flex: 1;
+	padding: 10rpx 0;
+}
+
+.arrow-right {
+	font-size: 56rpx;
+	color: #bbb;
+	padding: 10rpx 0 10rpx 20rpx;
+	font-weight: 300;
+}
+
+/* 级联面包屑 */
+.breadcrumb-bar {
+	display: flex;
+	align-items: center;
+	padding: 16rpx 40rpx;
+	background: #fafafa;
+	border-bottom: 1rpx solid #f0f0f0;
+}
+
+.back-btn {
+	color: #C1001C;
+	font-size: 28rpx;
+	margin-right: 16rpx;
+}
+
+.breadcrumb-path {
+	font-size: 28rpx;
+	color: #666;
+}
+
+/* 搜索栏样式 */
+.search-bar {
+	padding: 16rpx 40rpx;
+	border-bottom: 1rpx solid #f0f0f0;
+}
+
+.search-input-wrap {
+	display: flex;
+	align-items: center;
+	background: #f5f6f8;
+	border-radius: 32rpx;
+	padding: 12rpx 24rpx;
+}
+
+.search-icon {
+	font-size: 28rpx;
+	margin-right: 12rpx;
+}
+
+.search-input {
+	flex: 1;
+	font-size: 26rpx;
+	color: #333;
+	height: 56rpx;
+}
 </style>