Quellcode durchsuchen

采购相关代码

ouyj vor 2 Monaten
Ursprung
Commit
407f19f1da

+ 102 - 0
api/purchase.js

@@ -0,0 +1,102 @@
+import request from '@/utils/request'
+
+const preUrl = '/clientServices.do?iscrypt=1'
+
+/**
+ * 获取采购流程初始化数据
+ */
+export function getPurchaseInitData(useId) {
+	return request({
+		url: preUrl,
+		method: 'post',
+		data: {
+			serviceId: 'miniapp_purchaseNoContract',
+			params: {
+				task: 'getInitData',
+				useId: useId
+			}
+		}
+	})
+}
+
+/**
+ * 发起采购流程(包含表单、附件)
+ * @param {String} useId - 用户 ID
+ * @param {Object} formData - 表单数据(包含 baseForm 和 detailList)
+ * @param {Object} processInfo - 流程信息(包含 modelId, tmodelId, formId, fileIds 等)
+ */
+export function startPurchaseProcess(useId, formData, processInfo) {
+	return request({
+		url: preUrl,
+		method: 'post',
+		data: {
+			serviceId: 'miniapp_purchaseNoContract',
+			params: {
+				task: 'startContractPurchase',
+				useId: useId,
+				formData: formData,
+				flowInfo: processInfo // 流程信息(包含 modelId, tmodelId, formId, fileIds 等)
+			}
+		}
+	})
+}
+
+/**
+ * 选择物料列表(分页)
+ * @param {String} useId - 用户 ID
+ * @param {number} page - 页码(从 1 开始)
+ * @param {number} pageSize - 每页条数
+ * @param {string} itemName - 物料名称(可选)
+ */
+export function getMaterialList(useId, page = 1, pageSize = 20, itemName = '') {
+	return request({
+		url: preUrl,
+		method: 'post',
+		data: {
+			serviceId: 'miniapp_purchaseNoContract',
+			params: {
+				task: 'selectMaterial',
+				useId: useId,
+				page: page,
+				pageSize: pageSize,
+				itemName: itemName
+			}
+		}
+	})
+}
+
+// 获取采购单数据(用于审批页面展示)
+// 根据 formInsId 获取采购单的详细信息
+export function getPurchaseFormData(useId, formInsId) {
+	return request({
+		url: preUrl,
+		method: 'post',
+		isSession: true,
+		data: {
+			serviceId: 'miniapp_purchaseNoContract',
+			params: {
+				task: 'getPurchaseFormData',
+				useId,
+				formInsId
+			}
+		}
+	})
+}
+
+// 获取采购单数据(用于查看页面)
+// 根据流程实例 ID(insId)获取采购单的详细信息
+export function getPurchaseDataByInsId(useId, insId) {
+	return request({
+		url: preUrl,
+		method: 'post',
+		isSession: true,
+		data: {
+			serviceId: 'miniapp_purchaseNoContract',
+			params: {
+				task: 'getPurchaseDataByInsId',
+				useId,
+				insId
+			}
+		}
+	})
+}

+ 69 - 0
components/processForms/default-form.vue

@@ -0,0 +1,69 @@
+<!-- 默认通用表单组件 -->
+<template>
+	<view class="default-form-component">
+		<uni-card title="表单信息" spacing="0">
+			<view v-for="(item, index) in formElements" :key="index" class="form-item">
+				<text class="form-label">{{ item.label }}:</text>
+				<text class="form-value">{{ item.value || '-' }}</text>
+			</view>
+		</uni-card>
+	</view>
+</template>
+
+<script setup lang="ts">
+import { ref, watch } from 'vue'
+
+const props = defineProps({
+	formData: {
+		type: Object,
+		default: () => ({})
+	},
+	formElements: {
+		type: Array,
+		default: () => []
+	},
+	repeatingForm: {
+		type: Object,
+		default: () => ({ elementItem: [], elements: [] })
+	}
+})
+
+const emits = defineEmits(['update'])
+
+const formElements = ref<any[]>([])
+
+// 监听表单数据变化
+watch(() => props.formData, (newVal) => {
+	if (newVal && newVal.formElements) {
+		formElements.value = newVal.formElements
+		emits('update', newVal)
+	}
+}, { immediate: true, deep: true })
+
+// 暴露验证方法给父组件
+defineExpose({
+	validate: async () => {
+		return Promise.resolve()
+	}
+})
+</script>
+
+<style lang="scss" scoped>
+.default-form-component {
+	.form-item {
+		display: flex;
+		margin-bottom: 10px;
+		
+		.form-label {
+			color: #666;
+			width: 100px;
+			flex-shrink: 0;
+		}
+		
+		.form-value {
+			color: #333;
+			flex: 1;
+		}
+	}
+}
+</style>

+ 1113 - 0
components/processForms/purchase-form.vue

@@ -0,0 +1,1113 @@
+<!-- 采购流程专用表单组件 -->
+<template>
+	<view class="purchase-form-component">
+		<!-- 基本信息 -->
+		<uni-card>
+        <uni-section titleFontSize="1.3rem" title="基本信息" type="line"></uni-section>
+        <uni-forms ref="baseFormRef" :modelValue="baseForm" :rules="baseFormRules" label-position="left" :label-width="100" :border="true">
+          <uni-forms-item name="contractPurchaseNumber" label="采购单号">
+            <uni-easyinput v-model="baseForm.contractPurchaseNumber" disabled placeholder="自动生成"></uni-easyinput>
+          </uni-forms-item>
+          <uni-forms-item name="contractPurchaseName" label="采购单名称" required>
+            <uni-easyinput v-model="baseForm.contractPurchaseName"
+                           :disabled="!isInitiateOrFieldEditable('contract_purchase_name')"
+                           :placeholder="'请输入采购单名称'"
+                           :clearable="false"></uni-easyinput>
+          </uni-forms-item>
+          <uni-forms-item name="applyDate" label="申请日期">
+            <uni-easyinput v-model="baseForm.applyDate" type="date" disabled />
+          </uni-forms-item>
+          <uni-forms-item name="department" label="申请部门">
+            <uni-easyinput v-model="baseForm.department" disabled></uni-easyinput>
+          </uni-forms-item>
+
+        <!-- 部门意见 -->
+        <uni-forms-item label="部门意见" name="departmentalOpinion">
+          <view class="element_value_container">
+            <view v-if="isApprovalFieldEditable(departmentalOpinionElem)" class="element_value">
+              <!-- 审批签字板 --> 
+                <view v-if="departmentalOpinionElem && departmentalOpinionElem.defaultValue == ''">
+                  <uni-row>
+                    <uni-col :span="24">
+                      <button type="primary" @click="handleApprovalSignature('departmental_opinion')">手动签名</button>
+                    </uni-col>
+                    <uni-col :span="24">
+                      <button style="margin-top: 5px;" type="primary" @click="handleAutoSeal('departmental_opinion')">一键签名</button>
+                    </uni-col>
+                  </uni-row>
+                </view>
+                <view v-else-if="departmentalOpinionElem && departmentalOpinionElem.defaultValue" class="signature_img">
+                  <img style="width: 100%;" mode="widthFix" @click="handleApprovalSignature('departmental_opinion')"
+                       :src="config.baseUrlPre + departmentalOpinionElem.sealImgPath"
+                       :alt="departmentalOpinionElem.elementName + '签名'" />
+                </view>
+            </view>
+            <view v-else-if="departmentalOpinionElem && typeof departmentalOpinionElem.sealImgPath === 'string' && departmentalOpinionElem.sealImgPath.startsWith('/shares')" class="signature_img">
+              <img style="width: 100%;" mode="widthFix"
+                   :src="config.baseUrlPre + departmentalOpinionElem.sealImgPath" />
+            </view> 
+          </view>
+        </uni-forms-item>
+
+        <!-- 分管副总意见 -->
+        <uni-forms-item label="分管副总" name="dgmOpinion">
+          <view class="element_value_container">
+            <view v-if="isApprovalFieldEditable(deputyGeneralManagerOpinionElem)" class="element_value">
+              <!-- 审批签字板 --> 
+                <view v-if="deputyGeneralManagerOpinionElem && deputyGeneralManagerOpinionElem.defaultValue == ''">
+                  <uni-row>
+                    <uni-col :span="24">
+                      <button type="primary" @click="handleApprovalSignature('deputy_general_manager_opinion')">手动签名</button>
+                    </uni-col>
+                    <uni-col :span="24">
+                      <button style="margin-top: 5px;" type="primary" @click="handleAutoSeal('deputy_general_manager_opinion')">一键签名</button>
+                    </uni-col>
+                  </uni-row>
+                </view>
+                <view v-else-if="deputyGeneralManagerOpinionElem && deputyGeneralManagerOpinionElem.defaultValue" class="signature_img">
+                  <img style="width: 100%;" mode="widthFix" @click="handleApprovalSignature('deputy_general_manager_opinion')"
+                       :src="config.baseUrlPre + deputyGeneralManagerOpinionElem.sealImgPath"
+                       :alt="deputyGeneralManagerOpinionElem.elementName + '签名'" />
+                </view>
+            </view>
+            <view v-else-if="deputyGeneralManagerOpinionElem && typeof deputyGeneralManagerOpinionElem.sealImgPath === 'string' && deputyGeneralManagerOpinionElem.sealImgPath.startsWith('/shares')" class="signature_img">
+              <img style="width: 100%;" mode="widthFix"
+                   :src="config.baseUrlPre + deputyGeneralManagerOpinionElem.sealImgPath" />
+            </view> 
+          </view>
+        </uni-forms-item>
+
+        <!-- 审核副总意见 -->
+        <uni-forms-item  label="分管副总" name="auditDgmOpinion">
+          <view class="element_value_container">
+            <view v-if="isApprovalFieldEditable(auditDeputyGeneralManagerOpinionElem)" class="element_value">
+              <!-- 审批签字板 --> 
+                <view v-if="auditDeputyGeneralManagerOpinionElem && auditDeputyGeneralManagerOpinionElem.defaultValue == ''">
+                  <uni-row>
+                    <uni-col :span="24">
+                      <button type="primary" @click="handleApprovalSignature('audit_deputy_general_manager_opinion')">手动签名</button>
+                    </uni-col>
+                    <uni-col :span="24">
+                      <button style="margin-top: 5px;" type="primary" @click="handleAutoSeal('audit_deputy_general_manager_opinion')">一键签名</button>
+                    </uni-col>
+                  </uni-row>
+                </view>
+                <view v-else-if="auditDeputyGeneralManagerOpinionElem && auditDeputyGeneralManagerOpinionElem.defaultValue" class="signature_img">
+                  <img style="width: 100%;" mode="widthFix" @click="handleApprovalSignature('audit_deputy_general_manager_opinion')"
+                       :src="config.baseUrlPre + auditDeputyGeneralManagerOpinionElem.sealImgPath"
+                       :alt="auditDeputyGeneralManagerOpinionElem.elementName + '签名'" />
+                </view>
+            </view>
+            <view v-else-if="auditDeputyGeneralManagerOpinionElem && typeof auditDeputyGeneralManagerOpinionElem.sealImgPath === 'string' && auditDeputyGeneralManagerOpinionElem.sealImgPath.startsWith('/shares')" class="signature_img">
+              <img style="width: 100%;" mode="widthFix"
+                   :src="config.baseUrlPre + auditDeputyGeneralManagerOpinionElem.sealImgPath" />
+            </view> 
+          </view>
+        </uni-forms-item>
+
+        <!-- 总经理意见 -->
+        <uni-forms-item  label="总经理" name="gmOpinion">
+          <view class="element_value_container">
+            <view v-if="isApprovalFieldEditable(generalManagerOpinionElem)" class="element_value">
+              <!-- 审批签字板 --> 
+                <view v-if="generalManagerOpinionElem && generalManagerOpinionElem.defaultValue == ''">
+                  <uni-row>
+                    <uni-col :span="24">
+                      <button type="primary" @click="handleApprovalSignature('general_manager_opinion')">手动签名</button>
+                    </uni-col>
+                    <uni-col :span="24">
+                      <button style="margin-top: 5px;" type="primary" @click="handleAutoSeal('general_manager_opinion')">一键签名</button>
+                    </uni-col>
+                  </uni-row>
+                </view>
+                <view v-else-if="generalManagerOpinionElem && generalManagerOpinionElem.defaultValue" class="signature_img">
+                  <img style="width: 100%;" mode="widthFix" @click="handleApprovalSignature('general_manager_opinion')"
+                       :src="config.baseUrlPre + generalManagerOpinionElem.sealImgPath"
+                       :alt="generalManagerOpinionElem.elementName + '签名'" />
+                </view>
+            </view>
+            <view v-else-if="generalManagerOpinionElem && typeof generalManagerOpinionElem.sealImgPath === 'string' && generalManagerOpinionElem.sealImgPath.startsWith('/shares')" class="signature_img">
+              <img style="width: 100%;" mode="widthFix"
+                   :src="config.baseUrlPre + generalManagerOpinionElem.sealImgPath" />
+            </view>
+          </view>
+        </uni-forms-item>
+			</uni-forms>
+		</uni-card>
+				
+
+
+		<!-- 物料明细 -->
+		<uni-card>
+      <uni-section titleFontSize="1.3rem" title="物料明细" type="line"></uni-section>
+			<!-- 添加物料按钮:发起环节或字段可编辑时显示 -->
+			<view v-if="isSeModel" class="material-actions">
+				<button type="primary" size="mini" @click="openMaterialSelector" plain>添加物料</button>
+			</view>
+
+      <!-- 物料列表 - 卡片式展示 -->
+      <view v-if="materialList.length > 0" class="material-list">
+        <view v-for="(item, index) in materialList" :key="'material-' + item.materialCode + '-' + index" class="material-card">
+          <view class="material-header" @click="toggleExpand(index)">
+            <view class="material-main-info">
+              <text class="material-name">{{ item.materialName }}</text>
+              <text class="material-code">{{ item.materialCode }}</text>
+            </view>
+            <view class="material-expand">
+              <uni-icons :type="item.expanded ? 'up' : 'down'" size="16" color="#999"></uni-icons>
+            </view>
+          </view>
+
+          <!-- 展开的详细信息 -->
+          <view v-show="item.expanded" class="material-detail">
+            <view class="detail-row">
+              <text class="detail-label">规格型号:</text>
+              <text class="detail-value">{{ item.materialModel }}</text>
+              <text class="detail-label" style="margin-left: 15px;">单位:</text>
+              <text class="detail-value">{{ item.measureName }}</text>
+            </view>
+            <view class="detail-row delete-row">
+              <text class="detail-label">数量:</text>
+              <uni-easyinput
+                  v-model="item.qty"
+                  type="digit"
+                  v-if="isSeModel"
+                  placeholder="请输入数量"
+                  style="width: 100px; display: inline-block; margin-right: 15px;"
+              />
+              <text v-if="!isSeModel" class="detail-value">{{ item.qty }}</text>
+              <button v-if="isSeModel" type="warn" size="mini" @click="removeMaterial(index)">删除</button>
+            </view>
+          </view>
+        </view>
+      </view>
+
+      <view v-else-if="materialList.length === 0" class="empty-materials">
+        <text>暂无物料</text>
+      </view>
+    </uni-card>
+
+		<!-- 选择器弹出层 -->
+		<uni-popup ref="selectorPopup" type="center">
+			<view class="selector-popup">
+				<view class="popup-header">
+					<text class="popup-title">选择物料</text>
+					<uni-icons type="closeempty" size="20" @click="closePopup"></uni-icons>
+				</view>
+				
+				<!-- 搜索框 -->
+				<view class="search-bar">
+					<uni-easyinput 
+						v-model="searchKeyword" 
+						placeholder="输入物料名称搜索"
+						clearable
+						@confirm="handleSearch"
+					/>
+					<button type="primary" size="mini" @click="handleSearch">搜索</button>
+				</view>
+				
+				<scroll-view 
+					scroll-y 
+					class="popup-content"
+					refresher-enabled
+					:refresher-triggered="isRefreshing"
+					@refresh="onRefresh"
+					@scrolltolower="loadMore"
+				>
+					<view v-for="(item, index) in selectorList" :key="index" 
+					      class="selector-item" 
+					      @click="selectItem(item)">
+						<view class="selector-item-content">
+							<view class="item-info">
+								<text class="item-name">{{ item.materialName || item.itemName }}</text>
+								<text class="item-code">{{ item.materialCode || item.itemCode }}</text>
+								<text class="item-spec">{{ item.materialModel || item.specification || '' }}</text>
+								<text class="item-unit">{{ item.measureName || item.unit || '' }}</text>
+							</view>
+							<text v-if="isSelected(item)" class="selected-tag">已选择</text>
+						</view>
+					</view>
+					
+					<!-- 加载状态 -->
+					<view v-if="isLoading && selectorList.length > 0" class="loading-text">
+						<uni-load-more status="loading" />
+					</view>
+					
+					<!-- 没有更多数据 -->
+					<view v-else-if="!hasMore && selectorList.length > 0" class="no-more-text">
+						<text>没有更多了</text>
+					</view>
+					
+					<!-- 空数据提示 -->
+					<view v-if="selectorList.length === 0 && !isLoading" class="empty-data">
+						<text>{{ searchKeyword ? '暂无相关数据' : '暂无数据' }}</text>
+					</view>
+				</scroll-view>
+			</view>
+		</uni-popup>
+		
+		<!-- 签名板弹出层 -->
+		<uni-popup ref="signaturePopup" @maskClick="closeSignature">
+			<view class="signature_container" :class="{ 'signature_container_landscape': isLandscape }">
+				<view class="signature_content">
+					<l-signature ref="signatureRef" v-if="signaturePopupShow" :landscape="isLandscape" :penSize="8"
+						:minLineWidth="4" :maxLineWidth="12" :openSmooth="true" :preferToDataURL="true"
+						backgroundColor="#ffffff" penColor="black"></l-signature>
+				</view>
+				<view class="signature_button_container">
+					<uni-row :gutter="10">
+						<uni-col :span="6">
+							<button type="warn" @click="onclickSignatureButton('undo')">撤销</button>
+						</uni-col>
+						<uni-col :span="6">
+							<button type="warn" @click="onclickSignatureButton('clear')">清空</button>
+						</uni-col>
+						<uni-col :span="6">
+							<button type="primary" @click="onclickSignatureButton('save')">保存</button>
+						</uni-col>
+						<uni-col :span="6">
+							<button @click="onclickSignatureButton('landscape')">全屏</button>
+						</uni-col>
+					</uni-row>
+				</view>
+			</view>
+		</uni-popup>
+	</view>
+</template>
+
+<script setup lang="ts">
+import { ref, watch, computed, nextTick } from 'vue'
+import { getMaterialList } from '@/api/purchase.js'
+import { useUserStore } from '@/store/user.js'
+import $modal from '@/plugins/modal.js'
+import config from '@/config.js'
+import { uploadSignatureBoardImg, getSeal } from '@/api/process.js'
+
+const userStore = useUserStore()
+
+const props = defineProps({
+	formData: {
+		type: Object,
+		default: () => ({})
+	},
+	formElements: {
+		type: Array,
+		default: () => []
+	},
+	repeatingForm: {
+		type: Object,
+		default: () => ({ elementItem: [], elements: [] })
+	},
+	// 是否为发起环节(seModel == '1')
+	isInitiate: {
+		type: Boolean,
+		default: false
+	},
+	// 当前环节可编辑的字段列表
+	editableFields: {
+		type: Array,
+		default: () => []
+	},
+	// 当前环节的审批意见配置
+	currentTacheOpinion: {
+		type: Object,
+		default: null
+	}
+})
+
+const emits = defineEmits(['update', 'signature-change'])
+
+// 表单数据
+const baseForm = ref<any>({
+	contractPurchaseNumber: '',
+	contractPurchaseName: '',
+	applyDate: '',
+	department: ''
+})
+
+const baseFormRules = ref({
+	contractPurchaseName: [
+		{
+			required: true,
+			errorMessage: '请输入采购单名称'
+		}
+	]
+})
+
+const materialList = ref<any[]>([])
+const selectorList = ref<any[]>([])
+const searchKeyword = ref('')
+const isLoading = ref(false)
+const hasMore = ref(true)
+const currentPage = ref(1)
+const pageSize = ref(20)
+const isRefreshing = ref(false)
+const selectorPopup = ref(null)
+const signaturePopup = ref(null)
+const signatureRef = ref(null)
+const signatureImg = ref('')
+const currentApprovalText = ref('')
+const approvals = ref<any[]>([]) // 历史审批意见列表
+const signaturePopupShow = ref(false)
+const isLandscape = ref(false)
+
+// 获取指定的审批意见元素
+const getApprovalElement = (fieldName: string) => {
+	if (!props.formElements || !Array.isArray(props.formElements)) {
+		return null
+	}
+	// 从 formElements 中找到对应的字段
+	const elem = props.formElements.find(e => e.tableField === fieldName || e.elementName.includes(fieldName.replace(/_/g, '')))
+	return elem || null
+}
+
+// 获取部门意见元素(计算属性,避免重复调用)
+const departmentalOpinionElem = computed(() => getApprovalElement('departmental_opinion'))
+// 获取副总经理意见元素
+const deputyGeneralManagerOpinionElem = computed(() => getApprovalElement('deputy_general_manager_opinion'))
+// 获取审计副总经理意见元素
+const auditDeputyGeneralManagerOpinionElem = computed(() => getApprovalElement('audit_deputy_general_manager_opinion'))
+// 获取总经理意见元素
+const generalManagerOpinionElem = computed(() => getApprovalElement('general_manager_opinion'))
+
+// 是否发起环节
+const isSeModel = computed(() => props.isInitiate)
+
+// 计算各部门意见是否可编辑
+const isApprovalFieldEditable = (elem: any) => {
+	if (!elem) return false
+	return  props.editableFields.includes(elem.tableField)
+}
+
+// 处理审批意见签名
+let lastFormInsId = ''
+
+// 计算字段是否可编辑 (只需要该字段在 table_fields 中即可)
+const getFieldEditable = (fieldName: string) => {
+	// 如果没有配置 editableFields,则都不可编辑
+	if (!props.editableFields || props.editableFields.length === 0) {
+		return false
+	}
+	
+	// 检查该字段是否在可编辑字段列表中 (table_fields 包含的字段才可以编辑)
+	return props.editableFields.includes(fieldName)
+}
+
+// 计算是否为发起环节或字段可编辑 (满足任一条件即可)
+const isInitiateOrFieldEditable = (fieldName: string) => {
+	return props.isInitiate || getFieldEditable(fieldName)
+}
+
+// 监听表单数据变化
+watch(() => props.formData, (newVal) => {
+	if (!newVal || Object.keys(newVal).length === 0) {
+		return
+	}
+	// 使用 formInsId 判断是否是新的数据(formInsId 肯定存在)
+	const currentFormInsId = newVal.lFormInsId || newVal.universalid || ''
+
+	// 只有当 formInsId 变化时,才认为是新的数据需要加载
+	if (currentFormInsId && currentFormInsId !== lastFormInsId) {
+		lastFormInsId = currentFormInsId
+		
+		// 填充基本信息
+		baseForm.value = {
+			contractPurchaseNumber: newVal.contractPurchaseNumber || '',
+			contractPurchaseName: newVal.contractPurchaseName || '',
+			applyDate: newVal.applyDate || '',
+			department: newVal.department || ''
+		}
+		
+		// 从 details 或 detailList 中获取物料列表 (后端返回的是 details)
+		const rawList = newVal.details || newVal.detailList || []
+				
+		// 加载物料数据 (即使为空也加载,因为可能是新增场景)
+		if (Array.isArray(rawList)) {
+			materialList.value = rawList.map((item: any) => ({
+				materialCode: item.materialCode || item.itemCode,
+				materialName: item.materialName || item.itemName,
+				materialModel: item.materialModel || item.specification,
+				measureName: item.measureName || item.unit,
+				itemTypeName: item.itemTypeName || '',
+				qty: item.qty || 0,
+				expanded: true // 默认展开
+			}))
+		} else {
+			materialList.value = []
+		}
+				
+		// 处理 formElements,提取可编辑字段配置
+		if (props.formElements && props.formElements.length > 0) {
+			console.log('采购表单组件:处理 formElements 配置', props.formElements)
+		}
+	}
+}, { immediate: true, deep: true })
+
+// 切换展开/收起状态
+function toggleExpand(index: number) {
+	if (materialList.value[index]) {
+		materialList.value[index].expanded = !materialList.value[index].expanded
+	}
+}
+
+// 打开物料选择器
+async function openMaterialSelector() {
+	currentPage.value = 1
+	hasMore.value = true
+	selectorList.value = []
+	searchKeyword.value = ''
+	await loadMaterialList(1)
+	selectorPopup.value.open()
+}
+
+// 关闭弹出层
+function closePopup() {
+	selectorPopup.value.close()
+}
+
+// 加载物料列表
+function loadMaterialList(page: number = 1) {
+	isLoading.value = true
+	getMaterialList(userStore.user.useId, page, pageSize.value, searchKeyword.value)
+		.then(({ returnParams }) => {
+			const list = returnParams.list || []
+			const total = returnParams.total || 0
+			
+			if (page === 1) {
+				selectorList.value = list
+			} else {
+				selectorList.value = [...selectorList.value, ...list]
+			}
+			hasMore.value = selectorList.value.length < total
+			currentPage.value = page
+		})
+		.catch(err => {
+			console.error('加载物料失败:', err)
+			$modal.msgError('加载物料失败')
+		})
+		.finally(() => {
+			isLoading.value = false
+			isRefreshing.value = false
+		})
+}
+
+// 搜索
+function handleSearch() {
+	loadMaterialList(1)
+}
+
+// 刷新
+function onRefresh() {
+	isRefreshing.value = true
+	loadMaterialList(1)
+}
+
+// 加载更多
+function loadMore() {
+	if (!isLoading.value && hasMore.value) {
+		loadMaterialList(currentPage.value + 1)
+	}
+}
+
+// 选择物料
+async function selectItem(item: any) {
+	// 检查是否已选择
+	const exists = materialList.value.find(m => 
+		m.materialCode === item.materialCode || 
+		m.materialCode === item.itemCode
+	)
+	if (!exists) {
+		// 使用完整的字段映射(参考 start.vue)
+		const newItem = {
+			materialCode: item.materialCode || item.itemCode,
+			materialName: item.materialName || item.itemName,
+			materialModel: item.materialModel || item.specification,
+			measureName: item.measureName || item.unit,
+			itemTypeName: item.itemTypeName || '',
+			qty: 0,
+			expanded: true
+		}
+		materialList.value = [...materialList.value, newItem]
+		await nextTick()
+		emitUpdate()
+	}
+	closePopup()
+}
+
+// 判断是否已选择
+function isSelected(item: any) {
+	return materialList.value.some(m => m.materialCode === item.materialCode)
+}
+
+// 删除物料
+async function removeMaterial(index: number) {
+	uni.showModal({
+		title: '',
+		content: '确认删除该物料?',
+		success: async (res) => {
+			if (res.confirm) {
+				// 使用新数组替换旧数组,确保触发响应式更新
+				materialList.value = materialList.value.filter((_, i) => i !== index)
+				await nextTick()
+				emitUpdate()
+			}
+		}
+	})
+}
+
+// 发送更新事件
+function emitUpdate() {
+	emits('update', {
+		...baseForm.value,
+		detailList: materialList.value
+	})
+}
+
+// 处理审批意见签名
+const currentApprovalFieldName = ref('')
+// 存储每个签名字段的印章信息
+const approvalSealInfo = ref<Record<string, any>>({})
+
+function handleApprovalSignature(fieldName: string) {
+	currentApprovalFieldName.value = fieldName
+	signaturePopupShow.value = false
+	nextTick(() => {
+		signaturePopupShow.value = true
+	})
+	signaturePopup.value.open()
+}
+
+// 处理一键签名
+function handleAutoSeal(fieldName: string) {
+	const elem = getApprovalElement(fieldName)
+	if (elem) {
+		getSeal(userStore.user.useId).then(({ returnParams }) => {
+			const elem = getApprovalElement(fieldName)
+			elem.defaultValue = returnParams.sealFileId.universalid
+			elem.sealImgPath = returnParams.sealFileId.path
+			// 保存印章信息(用于后端处理)
+			// 一键签名时,sealFileId.universalid 就是印章模板 ID
+			approvalSealInfo.value[fieldName] = {
+				sealInsId: returnParams.sealFileId.universalid,  // 签名实例 ID(这里与印章模板 ID 相同)
+				sealFileId: returnParams.sealFileId.universalid,  // 印章模板 ID(用于 imgval)
+				left: 0,  // 默认左边距为 0
+				top: 0    // 默认上边距为 0
+			}
+		}).catch(err => {
+			$modal.msgError('获取签名失败:' + err)
+		})
+	}
+}
+
+// 初始化签字板
+function initSignature() {
+	signaturePopupShow.value = false
+	setTimeout(() => {
+		signaturePopupShow.value = true
+	}, 100)
+}
+
+// 点击签字板按钮
+function onclickSignatureButton(event: string) {
+	switch (event) {
+		case 'undo':
+			signatureRef.value.undo()
+			break
+		case 'clear':
+			signatureRef.value.clear()
+			break
+		case 'save':
+			signatureRef.value.canvasToTempFilePath({
+				success: (res: any) => {
+					if (res.isEmpty) {
+						$modal.msgError('签名不能为空!')
+						return
+					}
+					// 判断上传文件是否是 base64
+					if (res.tempFilePath.substring(0, 'data:image/png;base64,'.length) == 'data:image/png;base64,') {
+						const _fileData = res.tempFilePath
+						uploadSignatureBoardImg(userStore.user.useId, _fileData, getApprovalElement(currentApprovalFieldName.value).tableField)
+							.then(({ returnParams }) => {
+								const elem = getApprovalElement(currentApprovalFieldName.value)
+								elem.defaultValue = returnParams.sealInsID
+								elem.sealImgPath = returnParams.path
+								// 保存印章信息(用于后端处理)
+								// sealInsID 是手写签名的实例 ID
+								approvalSealInfo.value[currentApprovalFieldName.value] = {
+									sealInsId: returnParams.sealInsID,      // 签名实例 ID
+									sealFileId: returnParams.sealInsID,     // 手写签名图片 ID
+									left: 0,  // 默认左边距为 0
+									top: 0    // 默认上边距为 0
+								}
+								currentApprovalFieldName.value = ''
+								signaturePopupShow.value = false
+								signaturePopup.value.close()
+							})
+					} else {
+						// 转 base64
+						uni.getFileSystemManager().readFile({
+							filePath: res.tempFilePath,
+							encoding: 'base64',
+							success: (fileData: any) => {
+								const _fileData = 'data:image/png;base64,' + fileData.data
+								uploadSignatureBoardImg(userStore.user.useId, _fileData, getApprovalElement(currentApprovalFieldName.value).tableField)
+									.then(({ returnParams }) => {
+										const elem = getApprovalElement(currentApprovalFieldName.value)
+										elem.defaultValue = returnParams.sealInsID
+										elem.sealImgPath = returnParams.path
+										// 保存印章信息(用于后端处理)
+										// sealInsID 是手写签名的实例 ID
+										approvalSealInfo.value[currentApprovalFieldName.value] = {
+											sealInsId: returnParams.sealInsID,      // 签名实例 ID
+											sealFileId: returnParams.sealInsID,     // 手写签名图片 ID
+											left: 0,  // 默认左边距为 0
+											top: 0    // 默认上边距为 0
+										}
+										currentApprovalFieldName.value = ''
+										signaturePopupShow.value = false
+										signaturePopup.value.close()
+									})
+							}
+						})
+					}
+				}
+			})
+			break
+		case 'landscape':
+			isLandscape.value = !isLandscape.value
+			initSignature()
+			break
+	}
+}
+
+// 清空签名
+function clearSignature() {
+	onclickSignatureButton('clear')
+}
+
+// 保存签名
+function saveSignature() {
+	onclickSignatureButton('save')
+}
+
+// 关闭签名
+function closeSignature() {
+	signaturePopupShow.value = false
+	signaturePopup.value.close()
+}
+
+// 暴露验证方法给父组件
+defineExpose({
+	validate: async () => {
+		// 采购表单验证
+		if (!baseForm.value.contractPurchaseName) {
+			return Promise.reject(new Error('请输入采购单名称'))
+		}
+		if (!materialList.value || materialList.value.length === 0) {
+			return Promise.reject(new Error('请至少添加一个物料'))
+		}
+		// 验证审批意见字段(如果在 table_fields 中)
+		const approvalFields = [
+			{ fieldName: 'departmental_opinion', msg: '部门意见' },
+			{ fieldName: 'deputy_general_manager_opinion', msg: '副总经理意见' },
+			{ fieldName: 'audit_deputy_general_manager_opinion', msg: '审核副总意见' },
+			{ fieldName: 'general_manager_opinion', msg: '总经理意见' }
+		]
+			
+		for (const field of approvalFields) {
+			const elem = getApprovalElement(field.fieldName)
+			if (elem) {
+				// 检查该字段是否在当前环节的可编辑字段列表中
+				// editableFields 包含了当前环节可编辑的所有字段(即 table_fields)
+				if (props.editableFields && props.editableFields.includes(field.fieldName)) {
+					console.log('【验证】检查字段:', field.fieldName, ', defaultValue:', elem.defaultValue)
+					if (!elem.defaultValue || elem.defaultValue === '') {
+						return Promise.reject(new Error(field.msg + '不能为空!'))
+					}
+				}
+			}
+		}
+			
+		return Promise.resolve()
+	},
+	// 获取表单数据(返回 formElements 格式)
+	getFormElements: () => {
+		const formElements: any[] = []
+			
+		// 添加基本信息字段
+		Object.keys(baseForm.value).forEach(key => {
+			const value = baseForm.value[key]
+			if (value !== undefined && value !== null) {
+				formElements.push({
+					name: key,
+					value: String(value),
+					type: '0' // 普通文本类型
+				})
+			}
+		})
+			
+		// 添加审批意见字段
+		const approvalFields = ['departmental_opinion', 'deputy_general_manager_opinion', 'audit_deputy_general_manager_opinion', 'general_manager_opinion']
+		approvalFields.forEach(fieldName => {
+			const elem = getApprovalElement(fieldName)
+			if (elem) {
+				formElements.push({
+					name: fieldName,
+					value: elem.defaultValue || '',
+					type: elem.type || '0'
+				})
+				// 添加签名图片的 imgval 值(用于后端处理)
+				// 格式:sealFileId_left_top(必须使用印章模板 ID,而不是签名实例 ID)
+				if (approvalSealInfo.value[fieldName]) {
+					const sealInfo = approvalSealInfo.value[fieldName]
+					formElements.push({
+						name: fieldName + '_imgval',
+						value: `${sealInfo.sealFileId}_${sealInfo.left}_${sealInfo.top}`,
+						type: '0'
+					})
+				} else if (elem.sealImgPath) {
+					// 兼容旧数据(如果没有印章信息,仅传递图片路径)
+					formElements.push({
+						name: fieldName + '_imgval',
+						value: elem.sealImgPath,
+						type: '0'
+					})
+				}
+			}
+		})
+			
+		// 添加物料明细(JSON 字符串格式)
+		if (materialList.value && materialList.value.length > 0) {
+			formElements.push({
+				name: 'detailList',
+				value: JSON.stringify(materialList.value),
+				type: '0'
+			})
+		}
+			
+		return formElements
+	},
+	// 获取表单数据(兼容旧版本,返回键值对格式)
+	getFormData: () => {
+		const formData: any = {
+			...baseForm.value,
+			detailList: materialList.value || []
+		}
+			
+		// 添加审批意见字段数据
+		const approvalFields = ['departmental_opinion', 'deputy_general_manager_opinion', 'audit_deputy_general_manager_opinion', 'general_manager_opinion']
+		approvalFields.forEach(fieldName => {
+			const elem = getApprovalElement(fieldName)
+			if (elem) {
+				formData[fieldName] = elem.defaultValue || ''
+				// 添加签名图片的 imgval 值(用于后端处理)
+				// 格式:sealFileId_left_top(必须使用印章模板 ID,而不是签名实例 ID)
+				if (approvalSealInfo.value[fieldName]) {
+					const sealInfo = approvalSealInfo.value[fieldName]
+					formData[fieldName + '_imgval'] = `${sealInfo.sealFileId}_${sealInfo.left}_${sealInfo.top}`
+				} else if (elem && elem.sealImgPath) {
+					// 兼容旧数据(如果没有印章信息,仅传递图片路径)
+					formData[fieldName + '_imgval'] = elem.sealImgPath
+				}
+			}
+		})
+			
+		return formData
+	}
+})
+</script>
+
+<style lang="scss" scoped>
+/*.purchase-form-component {*/
+	// 基本信息中的禁用字段样式优化
+	::v-deep .uni-forms {
+		.uni-forms-item__content {
+			.uni-easyinput__content-input {
+				font-size: calc(14px + 1.2*(1rem - 16px)) !important;
+				font-weight: 500;
+				color: #333;
+			}
+			
+			.uni-date {
+				.uni-icons {
+					font-size: calc(22px + 1.2*(1rem - 16px)) !important;
+					font-weight: 500;
+				}
+				
+				.uni-date__x-input {
+					height: auto;
+					font-size: calc(14px + 1.2*(1rem - 16px)) !important;
+					font-weight: 500;
+					color: #333;
+				}
+			}
+		}
+	}
+	
+	// 审批意见字段样式(与 index.vue 保持一致)
+	.element_value_container {
+		.signature_img {
+			width: 180px;
+		}
+	}
+	
+	// 签名弹窗样式(与 index.vue 保持一致)
+	.signature_container {
+		background-color: #f5f5f5;
+		height: 40vh;
+		width: 90vw;
+
+		.signature_content {
+			height: 80%;
+			width: 100%;
+		}
+
+		.signature_button_container {
+			height: 20%;
+			width: 100%;
+
+			button {
+				height: 100%;
+			}
+		}
+	}
+
+	.signature_container_landscape {
+		height: 100vh;
+		width: 100vw;
+
+		.signature_content {
+			height: 85%;
+			width: 100%;
+		}
+
+		.signature_button_container {
+			margin-top: 10%;
+			height: 15%;
+			width: 100%;
+
+			button {
+				height: 100%;
+				width: 100%;
+				transform: rotate(90deg);
+			}
+		}
+	}
+	
+	.material-actions {
+		display: flex;
+		gap: 10px;
+		margin-bottom: 15px;
+		
+		button {
+			flex: 1;
+		}
+	}
+
+	// 物料列表 - 卡片式展示
+	.material-list {
+		display: flex;
+		flex-direction: column;
+		gap: 10px;
+	}
+
+	.material-card {
+		border: 1px solid #e5e5e5;
+		border-radius: 6px;
+		overflow: hidden;
+		background-color: #fff;
+		margin-bottom: 8px;
+		
+		.material-header {
+			display: flex;
+			justify-content: space-between;
+			align-items: center;
+			padding: 10px 12px;
+			background-color: #f8f9fa;
+			cursor: pointer;
+			
+			.material-main-info {
+				display: flex;
+				align-items: center;
+				gap: 8px;
+				flex: 1;
+				
+				.material-name {
+					font-size: 14px;
+					font-weight: bold;
+					color: #333;
+				}
+				
+				.material-code {
+					font-size: 12px;
+					color: #999;
+					white-space: nowrap;
+				}
+			}
+			
+			.material-expand {
+				display: flex;
+				align-items: center;
+				margin-left: 10px;
+			}
+		}
+		
+		.material-detail {
+			padding: 10px 12px;
+			border-top: 1px solid #e5e5e5;
+			
+			.detail-row {
+				display: flex;
+				align-items: center;
+				flex-wrap: wrap;
+				gap: 8px;
+				
+				.detail-label {
+					font-size: 13px;
+					color: #666;
+					flex-shrink: 0;
+				}
+				
+				.detail-value {
+					flex: 0 0 auto;
+					font-size: 13px;
+					color: #333;
+				}
+			}
+			
+			.delete-row {
+				margin-top: 10px;
+				text-align: center;
+				padding-top: 8px;
+				border-top: 1px dashed #e5e5e5;
+			}
+		}
+	}
+
+	.empty-materials {
+		text-align: center;
+		padding: 30px;
+		color: #909399;
+		font-size: 14px;
+	}
+	
+	// 弹窗样式
+	.selector-popup {
+		width: 90%;
+		max-width: 500px;
+		max-height: 70vh;
+		background-color: #fff;
+		border-radius: 12px;
+		overflow: hidden;
+		box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
+		
+		/* 确保弹窗居中 */
+		position: relative;
+		margin: 0 auto;
+		
+		.popup-header {
+			display: flex;
+			justify-content: space-between;
+			align-items: center;
+			padding: 15px;
+			border-bottom: 1px solid #e5e5e5;
+			
+			.popup-title {
+				font-size: 16px;
+				font-weight: bold;
+			}
+		}
+		
+		.search-bar {
+			display: flex;
+			gap: 10px;
+			padding: 10px 15px;
+			align-items: center;
+		}
+		
+		.popup-content {
+			max-height: 50vh;
+			
+			.selector-item {
+				padding: 10px 12px;
+				border-bottom: 1px solid #f0f0f0;
+				
+				&:active {
+					background-color: #f5f5f5;
+				}
+				
+				.selector-item-content {
+					display: flex;
+					justify-content: space-between;
+					align-items: center;
+					gap: 8px;
+					
+					.item-info {
+						flex: 1;
+						display: flex;
+						flex-wrap: wrap;
+						align-items: center;
+						gap: 4px;
+						
+						.item-name {
+							font-size: 15px;
+							font-weight: bold;
+							color: #333;
+							flex-basis: 100%;
+							margin-bottom: 4px;
+						}
+						
+						.item-code,
+						.item-spec,
+						.item-unit {
+							font-size: 13px;
+							color: #666;
+							white-space: nowrap;
+							font-weight: 500;
+						}
+						
+						.item-code::after,
+						.item-spec::after,
+						.item-unit::after {
+							content: ' | ';
+							margin: 0 4px;
+							color: #ddd;
+						}
+						
+						.item-unit:last-child::after {
+							content: '';
+						}
+					}
+					
+					.selected-tag {
+						color: #409eff;
+						font-size: 12px;
+						flex-shrink: 0;
+						white-space: nowrap;
+					}
+				}
+			}
+			
+			.loading-text {
+				text-align: center;
+				padding: 12px;
+				color: #909399;
+				font-size: 13px;
+			}
+			
+			.no-more-text {
+				text-align: center;
+				padding: 12px;
+				color: #909399;
+				font-size: 13px;
+			}
+			
+			.empty-data {
+				text-align: center;
+				padding: 30px;
+				color: #909399;
+			}
+		}
+	}
+/*}*/
+</style>

+ 15 - 0
pages.json

@@ -186,6 +186,21 @@
 			{
 				"navigationBarTitleText" : "头像修改"
 			}
+		},
+		{
+			"path" : "pages/work/purchase/start",
+			"style" : 
+			{
+				"navigationBarTitleText" : "采购申请",
+				"enablePullDownRefresh" : false
+			}
+		},
+		{
+			"path": "pages/process/common/detail",
+			"style": {
+				"navigationBarTitleText": "流程审批",
+				"enablePullDownRefresh": false
+			}
 		}
 	],
 	"tabBar": {

+ 802 - 0
pages/process/common/detail.vue

@@ -0,0 +1,802 @@
+<template>
+	<page-meta root-font-size="system" />
+	<view class="process_detail_container">
+		<!-- 动态表单组件区域(包含表单、重复表、签名等所有表单相关内容) -->
+		<view class="dynamic-form-section">
+			<!-- 采购流程表单 -->
+			<purchase-form 
+				v-if="formComponentName === 'purchase-form'"
+				ref="dynamicFormRef" 
+				:formData="{...formData, ...purchaseData}" 
+				:formElements="formElements"
+				:repeatingForm="repeatingForm"
+				:is-initiate="flowInfo.seModel == '1'"
+				:editable-fields="editableFields"
+				:current-tache-opinion="currentTacheOpinion"
+				@update="handleFormUpdate"
+				@signature-change="handleSignatureChange" />
+			
+			<!-- 默认表单 -->
+			<default-form 
+				v-else
+				ref="dynamicFormRef" 
+				:formData="formData" 
+				:formElements="formElements"
+				:repeatingForm="repeatingForm"
+				@update="handleFormUpdate" />
+		</view>
+		
+		<!-- 已有附件列表 -->
+		<view v-for="(item, index) in fileList" :key="index">
+			<uni-card v-if="item.files && item.files.length > 0">
+				<uni-section titleFontSize="1.3rem" title="附件" type="line"></uni-section>
+				<attachment-list 
+					@clickDelete="deleteFile" 
+					:canEdit="flowInfo.seModel == '1'"
+					:attachments="item.files">
+				</attachment-list>
+			</uni-card>
+		</view>
+		
+		<!-- 上传附件(审批时) -->
+		<view v-if="processInfo.tinsId || flowInfo.seModel == '1'" class="file_picker_container">
+			<uni-card title="上传附件" :extra="`${subFileList.length}/50`" spacing="0">
+				<uni-file-picker 
+					ref="filePicker" 
+					v-model="subFileList" 
+					:auto-upload="true" 
+					mode="list" 
+					:limit="50"
+					file-mediatype="all" 
+					@select="handleFileSelect" 
+					@progress="handleFileProgress"
+					@success="handleFileSuccess" 
+					@fail="handleFileFail" 
+					@delete="handleFileDelete" />
+			</uni-card>
+		</view>
+		
+		<!-- 流转过程 -->
+		<uni-card>
+			<view class="flow_step_container">
+				<uni-section titleFontSize="1.3rem" title="流转过程" type="line"></uni-section>
+				<up-steps :current="stepActive" activeColor="#18bc37" inactiveColor="#2979ff" direction="column">
+					<view v-for="(step, index) in options" :key="index">
+						<up-steps-item 
+							:contentClass="'redcontent'" 
+							v-if="step.state == 3"
+							:title="step.title + ' 退回'" 
+							:desc="step.desc" 
+							error></up-steps-item>
+						<up-steps-item 
+							:contentClass="'redcontent'" 
+							v-else-if="step.state == 0"
+							:title="step.title + ' 撤销'" 
+							:desc="step.desc" 
+							error></up-steps-item>
+						<up-steps-item 
+							v-else-if="index == stepActive" 
+							:title="step.title" 
+							:desc="step.desc">
+							<template #icon>
+								<view class="active_step_circle">
+									<text class="active_step_text">{{ index + 1 }}</text>
+								</view>
+							</template>
+						</up-steps-item>
+						<up-steps-item 
+							v-else 
+							:title="step.title" 
+							:desc="step.desc"></up-steps-item>
+					</view>
+				</up-steps>
+			</view>
+		</uni-card>
+		
+		<!-- 环节备注 -->
+		<view v-if="processInfo.tinsId">
+			<uni-card>
+				<uni-section titleFontSize="1.3rem" title="环节备注" type="line"></uni-section>
+				<view class="remark_content">
+					<uni-easyinput 
+						type="textarea" 
+						autoHeight 
+						v-model="remark" 
+						placeholder="请输入" />
+				</view>
+			</uni-card>
+		</view>
+		
+		<!-- 通过按钮 -->
+		<view v-if="processInfo.tinsId" class="approve_button">
+			<uni-card spacing="0" padding="0">
+				<button 
+					:disabled="!button_state" 
+					:loading="!button_state" 
+					type="primary"
+					@click="handleSubmitProcess('1')">
+					{{ flowInfo.seModel == '0' ? '通过' : '提交' }}
+				</button>
+			</uni-card>
+		</view>
+		
+		<!-- 退回按钮组 -->
+		<view v-if="processInfo.tinsId && flowInfo.seModel == '0'">
+			<view class="reject_button">
+				<uni-card spacing="0" padding="0" :is-shadow="false" :border="false">
+					<uni-row>
+						<uni-col :span="11">
+							<button 
+								:disabled="!button_state" 
+								:loading="!button_state" 
+								type="warn"
+								@click="handleSubmitProcess('0')">
+								退回上一级
+							</button>
+						</uni-col>
+						<uni-col :span="11" :offset="2">
+							<button 
+								:disabled="!button_state" 
+								:loading="!button_state" 
+								type="warn"
+								@click="handleSubmitProcess('2')">
+								退回发起人
+							</button>
+						</uni-col>
+					</uni-row>
+				</uni-card>
+			</view>
+		</view>
+	</view>
+</template>
+
+<script setup lang="ts">
+import { computed, onMounted, reactive, ref, nextTick } from 'vue'
+import { onLoad, onShow } from '@dcloudio/uni-app'
+import attachmentList from '@/components/ygoa/attachmentList.vue'
+import purchaseForm from '@/components/processForms/purchase-form.vue'
+import defaultForm from '@/components/processForms/default-form.vue'
+import config from '@/config.js'
+import $modal from '@/plugins/modal.js'
+import $tab from '@/plugins/tab.js'
+import { 
+	getProcessFlowInfo, 
+	getProcessFormInfoInFlow, 
+	getProcessFormInfo, // 新增:没有 tinsId 时使用
+	getProcessFlow, 
+	uploadFile, 
+	commonProcessApproval,
+} from '@/api/process.js'
+import { useUserStore } from '@/store/user.js'
+import { keepSession } from '@/api/login.js'
+import { getPurchaseFormData, getPurchaseDataByInsId   } from '@/api/purchase.js'
+
+const userStore = useUserStore()
+
+// 页面参数
+const processInfo = reactive({
+	insId: '',
+	insName: '',
+	control: '1',
+	username: '',
+	reqOffice: 0,  // 是否需要附件:0-不需要,1-需要
+	reqRemark: 0,  // 是否需要备注:0-不需要,1-需要
+	tinsId: undefined as any,
+	modelId: ''
+})
+
+onLoad((options) => {
+	processInfo.insId = options.insId || ''
+	processInfo.insName = options.insName || ''
+	processInfo.control = options.control || '1'
+	processInfo.modelId = options.modelId || ''
+	if (options.tinsId) {
+		processInfo.tinsId = options.tinsId
+	}
+	
+	// 设置导航栏标题
+	uni.setNavigationBarTitle({
+		title: processInfo.insName
+	});
+})
+
+// 动态表单组件名称
+const formComponentName = computed(() => {
+	// 根据 modelId 映射到对应的组件
+	const componentMap: Record<string, string> = {
+		'200001': 'purchase-form', // 采购流程表单组件
+		// 后续添加更多映射
+	}
+	return componentMap[processInfo.modelId] || 'default-form'
+})
+
+// 表单数据(传递给自定义组件)
+const formData = ref<any>({}) // BPM 流程表单数据
+const purchaseData = ref<any>({}) // 采购单业务数据
+const formElements = ref<any[]>([])
+const repeatingForm = ref<any>({ elementItem: [], elements: [] })
+const fileList = ref<any[]>([]) // 已有附件列表
+const dynamicFormRef = ref(null)
+
+// 采购流程专用字段
+const editableFields = ref<string[]>([]) // 当前环节可编辑的字段列表
+const currentTacheOpinion = ref<any>(null) // 当前环节的审批意见配置
+const signatureData = ref<string>('') // 签名图片 base64
+
+// 上传附件相关
+const subFileList = ref<any[]>([]) // 新上传的附件
+const filePicker = ref(null)
+
+// 流转过程相关
+const flowInfo = ref<any>({
+	seModel: '0' // 0-审批模式,1-办理模式
+})
+const stepActive = ref(0)
+const options = ref<any[]>([])
+
+// 环节备注
+const remark = ref('')
+
+// 按钮状态
+const button_state = ref(true)
+
+// 获取流程详情
+async function loadProcessDetail() {
+	try {
+		// 判断是否有 tinsId,选择不同的接口
+		if (processInfo.tinsId) {
+			// ✅ 有待办环节:调用 getProcessFormInfoInFlow(审批场景)
+			const formRes = await getProcessFormInfoInFlow(userStore.user.useId, {
+				tinsId: processInfo.tinsId,
+				insId: processInfo.insId,
+				control: processInfo.control
+			})
+				
+			if (formRes.returnCode === '0' || formRes.returnCode === '1') {
+				const formInfo = formRes.returnParams
+								
+				formElements.value = formInfo.formElements || []
+				repeatingForm.value = formInfo.repeatingForm || { elementItem: [], elements: [] }
+				formData.value = formInfo
+							
+				// 处理附件列表 - 从后端接口获取(如果有)
+				if (formInfo.fileList) {
+					fileList.value = formInfo.fileList
+				}
+							
+				// 从 formElements 中提取可编辑字段列表 (canEdit == '1')
+				 
+        editableFields.value = formElements.value
+          .filter(elem => elem.canEdit == '1')
+          .map(elem => elem.tableField)
+				 
+			}
+				
+			// 获取流转信息(必须在 loadPurchaseFormData 之前调用,因为需要 flowInfo 数据)
+			await loadFlowSteps()
+			await loadFlowInfo()
+		} else {
+			const formRes = await getProcessFormInfo(userStore.user.useId, processInfo.insId)
+				
+			if (formRes.returnCode === '0' || formRes.returnCode === '1') {
+				const formInfo = formRes.returnParams
+				formElements.value = formInfo.formElements || []
+				repeatingForm.value = formInfo.repeatingForm || { elementItem: [], elements: [] }
+				formData.value = formInfo
+					
+				// 处理附件列表
+				if (formInfo.fileList) {
+					fileList.value = formInfo.fileList
+				}
+			 
+        editableFields.value = []
+					 
+			}
+				
+			// 获取流转信息(不需要 tinsId 的版本)
+			await loadFlowSteps()
+		}
+			
+		// 如果是采购流程,额外加载采购单数据(需要在 flowInfo 加载完成后才能获取 table_fields)
+		if (processInfo.modelId === '200001') {
+			await loadPurchaseFormData()
+		}
+	} catch (error) {
+		console.error('加载流程详情失败:', error)
+		$modal.msgError('加载失败')
+	}
+}
+
+// 加载流转步骤
+async function loadFlowSteps() {
+	try {
+		const flowRes = await getProcessFlow(userStore.user.useId, {
+			insId: processInfo.insId,
+			control: processInfo.control
+		})
+		
+		if (flowRes.returnCode === '1' || flowRes.returnCode === '0') {
+			const flowList = flowRes.returnParams.list || []
+			
+			options.value = flowList.map((item: any, index: number) => {
+				const { tmodelName, name, createdate, finishdate, remark, state, task, groupName, positionName } = item
+				if (state == 1) {
+					stepActive.value = index
+				}
+				const title = tmodelName + (name == '' ? '' : ' ( ' + name + ' ' + groupName + '-' + positionName + ' )')
+				let desc = '创建时间:' + createdate
+					+ (finishdate == '' ? '\n' : '\n办理时间:' + finishdate)
+					+ (remark == '' ? '\n' : '\n环节意见:' + remark)
+				if (task && task.length > 0) {
+					desc += '\n抄送信息'
+					task.forEach((taskitem: any) => {
+						desc += `\n抄送对象:(${taskitem.username})抄送时间:${taskitem.createdate}`
+					})
+				}
+				return {
+					title,
+					desc,
+					state
+				}
+			})
+			if (stepActive.value === -1) {
+				stepActive.value = flowList.length
+			}
+		}
+	} catch (error) {
+		console.error('加载流转信息失败:', error)
+	}
+}
+
+// 加载采购单数据(用于审批页面展示)
+async function loadPurchaseFormData() {
+	try {
+		// 优先使用 flowInfo 中的 formInsId
+		let formInsId = flowInfo.value.formInsId || ''
+		
+		if (!formInsId) {
+			// 如果 flowInfo 中没有,尝试从 formData 中获取
+			formInsId = formData.value.formInsId || formData.value.universalid || ''
+		}
+		
+		if (formInsId) {
+			// ✅ 有 formInsId:调用 getPurchaseFormData
+			const res = await getPurchaseFormData(userStore.user.useId, formInsId)
+			
+			if (res.returnCode === '1') {
+				const purchaseDataRes = res.returnParams
+				
+				// 将采购单数据单独存储
+				purchaseData.value = purchaseDataRes
+				
+				// 保存当前环节的审批意见配置
+				currentTacheOpinion.value = flowInfo.value.tmodel
+			}
+		} else if (processInfo.insId && !processInfo.tinsId) {
+			const res = await getPurchaseDataByInsId(userStore.user.useId, processInfo.insId)
+			
+			if (res.returnCode === '1') {
+				const purchaseDataRes = res.returnParams
+				
+				// 将采购单数据单独存储
+				purchaseData.value = purchaseDataRes
+				
+				// 注意:查看场景没有 table_fields,所以不设置 editableFields
+				editableFields.value = [] // 查看场景不可编辑
+				currentTacheOpinion.value = null
+			}
+		} else {
+			console.warn('缺少 formInsId 和 insId,无法加载采购单数据')
+		}
+	} catch (error) {
+		console.error('加载采购单数据失败:', error)
+	}
+}
+
+// 获取流程控制信息
+async function loadFlowInfo() {
+	try {
+		const flowInfoRes = await getProcessFlowInfo(userStore.user.useId, {
+			tinsId: processInfo.tinsId,
+			control: processInfo.control
+		})
+		
+		if (flowInfoRes.returnCode === '1' || flowInfoRes.returnCode === '0') {
+			const params = flowInfoRes.returnParams
+			
+			if (params.flow && params.flow.length > 0) {
+				flowInfo.value = params.flow[0]
+				
+				// 设置附件列表(从流转信息中获取)
+				if (params.flow[0].files && params.flow[0].files.length > 0) {
+					const newFileList = [{ files: params.flow[0].files }]
+					
+					// 使用 splice 清空再 push,确保响应式更新
+					fileList.value.splice(0, fileList.value.length)
+					newFileList.forEach(item => fileList.value.push(item))
+				}
+				
+				// 添加下一环节到流转步骤
+				if (params.nextTmodels && params.nextTmodels.length > 0) {
+					options.value.push({ 
+						title: params.nextTmodels[0].nextTmodelName,
+						desc: '',
+						state: -1
+					})
+				}
+				
+				// 保存其他必要信息
+				processInfo.reqOffice = params.tmodel?.reqOffice || 0
+				processInfo.reqRemark = params.tmodel?.reqRemark || 0
+			}
+		}
+	} catch (error) {
+		console.error('加载流程控制信息失败:', error)
+	}
+}
+
+// 表单更新回调
+function handleFormUpdate(data: any) {
+	// 只更新 BPM 流程表单数据,不影响采购单数据
+	formData.value = data
+}
+
+// 处理签名变化
+function handleSignatureChange(signatureBase64: string) {
+	signatureData.value = signatureBase64
+}
+
+// 文件上传相关
+async function handleFileSelect(files: any) {
+	files.tempFiles.forEach((file: any) => {
+		const data = {
+			name: file.name,
+			filePath: file.path,
+		}
+		uploadFile(data)
+			.then(res => {
+				file.seq = res.returnParams
+				subFileList.value.push({ seq: res.returnParams, path: file.path })
+				$modal.msgSuccess('文件' + data.name + '上传成功')
+			})
+			.catch(err => {
+				$modal.msgError('文件' + data.name + '上传失败,请删除重新上传')
+			})
+	})
+}
+
+function handleFileProgress(file: any, progress: any) {}
+
+function handleFileSuccess(file: any, res: any) {}
+
+function handleFileFail(file: any, err: any) {}
+
+function handleFileDelete(file: any) {
+	const index = subFileList.value.findIndex(({ path }) => path == file.tempFilePath)
+	if (index !== -1) {
+		subFileList.value.splice(index, 1)
+	}
+}
+
+// 删除已有附件
+function deleteFile(file: any) {
+	// TODO: 调用删除附件接口
+	$modal.msgSuccess('删除成功')
+}
+
+// 提交审批
+function handleSubmitProcess(result) {
+	console.log('=== 点击审批按钮 ===')
+	console.log('result:', result)
+	
+	let content = '确认退回'
+	if (result == "1") {
+		content = '确认通过'
+	}
+	console.log('确认内容:', content)
+	
+	$modal.confirm(content).then(() => {
+		console.log('=== 用户点击确认 ===')
+		button_state.value = false
+		console.log('准备调用 submitProcess, result:', result)
+		if (result == "1") {
+			submitProcess(result)
+		} else {
+			submitProcess(result)
+		}
+	}).catch((err) => {
+		console.log('用户取消操作', err)
+	})
+}
+
+function submitProcess(result) {
+	console.log('=== 进入 submitProcess ===')
+	console.log('result:', result)
+	console.log('modelId:', processInfo.modelId)
+	console.log('dynamicFormRef:', dynamicFormRef.value)
+	
+	let flow = Object.assign({}, flowInfo.value)
+	flow['staffId'] = userStore.user.useId
+	flow['gxId'] = userStore.user.gxId
+	flow['groupId'] = flowInfo.value.groupid
+	if (result == "1") {
+		const seqs = subFileList.value.map(({ seq }) => seq)
+		if (flowInfo.value.seModel == '0' && processInfo.reqOffice == 1 && seqs.length == 0) {
+			button_state.value = true
+			$modal.msgError('请上传附件')
+			return
+		} else {
+			flow['fileIds'] = seqs
+		}
+		if (processInfo.reqRemark == 1 && remark.value == '') {
+			button_state.value = true
+			$modal.msgError('请填写备注')
+			return
+		}
+	} else {
+		flow.fileIds = []
+		flow.files = []
+	}
+	flow['remark'] = remark.value
+	// result: 1 通过 2 退回发起人 0 退回上一级
+	flow['result'] = result
+	flow['nextTmodelId'] = 'undefined'
+	
+	// 构建 form 参数
+	const form = {
+		formId: flowInfo.value.formId || '',
+		formInsId: purchaseData.value.lFormInsId || purchaseData.value.universalid || formData.value.formInsId || formData.value.universalid || '',
+		formElements: formElements.value
+	}
+	
+	console.log('构建的 form 对象:', form)
+	console.log('是否为采购流程:', processInfo.modelId === '200001')
+	
+	// 如果是采购流程,直接传递 purchaseFormData
+	if (processInfo.modelId === '200001' && dynamicFormRef.value) {
+		console.log('进入采购流程验证分支')
+			
+		// 【关键修复】只有“通过”操作才需要验证表单,退回不需要
+		if (result == "1") {
+			// 调用采购表单组件的 validate 方法进行验证
+			dynamicFormRef.value.validate()
+				.then(() => {
+					console.log('表单验证通过')
+					console.log('formData.value:', formData.value)
+					console.log('formData.value.table_fields:', formData.value.table_fields)
+					console.log('formElements.value:', formElements.value)
+						
+					// 【关键修复】从 formElements 中提取可编辑字段的 tableField
+					// table_fields 应该只包含当前环节可编辑的字段 (canEdit == '1')
+					const tableFieldsList = formElements.value
+						.filter(elem => elem.canEdit == '1')  // ✅ 只提取可编辑字段
+						.map(elem => elem.tableField)
+						.filter(f => f)  // 过滤掉空值
+					const tableFieldsStr = ',' + tableFieldsList.join(',') + ','
+					console.log('从 formElements 提取的可编辑 table_fields:', tableFieldsStr)
+					console.log('可编辑字段列表:', tableFieldsList)
+						
+					// 验证通过,获取采购表单数据(包含审批意见和印章)
+					const purchaseFormData = dynamicFormRef.value.getFormData()
+									
+					// 【关键修复】添加 table_fields 字段,后端需要它来判断哪些审批意见字段需要处理
+					purchaseFormData.table_fields = tableFieldsStr
+									
+					// 直接将 purchaseFormData 转换为 formElements 格式
+					const purchaseFormElements = []
+					Object.keys(purchaseFormData).forEach(key => {
+						// 跳过不需要提交的字段
+						if (key !== 'details' && key !== 'detailList') {
+							const value = purchaseFormData[key]
+							purchaseFormElements.push({
+								name: key,
+								value: String(value),
+								type: '0'
+							})
+						}
+					})
+						
+					// 物料明细需要转成 JSON 字符串
+					if (purchaseFormData.detailList && Array.isArray(purchaseFormData.detailList)) {
+						purchaseFormElements.push({
+							name: 'detailList',
+							value: JSON.stringify(purchaseFormData.detailList),
+							type: '0'
+						})
+					}
+						
+					// 合并到 formElements 中
+					form.formElements = [...formElements.value, ...purchaseFormElements]
+						
+					// 提交审批
+					console.log('调用 submitApproval (采购流程)')
+					submitApproval(flow, form)
+				})
+				.catch(err => {
+					console.error('表单验证失败:', err)
+					button_state.value = true
+					$modal.msgError(err.message || '表单验证失败')
+				})
+		} else {
+			// 退回操作,不验证表单,直接提交
+			console.log('退回操作,不验证表单,直接提交')
+			submitApproval(flow, form)
+		}
+	} else {
+		console.log('进入非采购流程分支')
+		// 非采购流程,直接提交
+		submitApproval(flow, form)
+	}
+}
+
+// 执行审批提交
+function submitApproval(flow, form) {
+	console.log('=== 开始调用审批接口 ===')
+	console.log('flow 参数:', JSON.stringify(flow, null, 2))
+	console.log('form 参数:', JSON.stringify(form, null, 2))
+	console.log('control:', processInfo.control)
+	
+	// 调用通用审批接口
+	commonProcessApproval(flow, form, processInfo.control).then(({ returnCode, returnMsg, returnParams }) => {
+		console.log('=== 接口响应 ===')
+		console.log('returnCode:', returnCode)
+		console.log('returnMsg:', returnMsg)
+		console.log('returnParams:', returnParams)
+		
+		if (returnMsg.includes('提交失败')) {
+			// 启用提交按钮
+			button_state.value = true
+			$modal.msgError(returnMsg)
+		} else {
+			$modal.msgSuccess(returnMsg)
+			// 通知列表刷新数据
+			uni.$emit('ReloadProcessData');
+			setTimeout(() => {
+				$tab.navigateBack();
+			}, 1000)
+		}
+	}).catch(error => {
+		console.error('=== 接口调用失败 ===')
+		console.error('错误信息:', error)
+		button_state.value = true
+		$modal.msgError('网络请求失败:' + (error.message || '未知错误'))
+	})
+}
+
+// 保持会话
+onShow(() => {
+	keepSession().catch(err => {
+		console.error('保持会话失败:', err)
+	})
+})
+
+onMounted(() => {
+	loadProcessDetail()
+})
+</script>
+
+<style lang="scss" scoped>
+.process_detail_container {
+	padding-bottom: 80px;
+}
+
+.dynamic-form-section {
+	margin-top: 15px;
+}
+
+// 流转过程样式
+.flow_step_container {
+	min-height: 100px;
+	
+	::v-deep .u-steps {
+		.u-steps-item {
+			padding-bottom: 11px;
+			
+			.redcontent {
+				.u-text__value--content {
+					color: #ff4500;
+				}
+			}
+			
+			.active_step_circle {
+				width: 20px;
+				height: 20px;
+				box-sizing: border-box;
+				flex-shrink: 0;
+				border-radius: 100px;
+				border-width: 1px;
+				border-color: #A78BFA;
+				background-color: #A78BFA;
+				border-style: solid;
+				display: flex;
+				flex-direction: row;
+				align-items: center;
+				justify-content: center;
+				transition: background-color .3s;
+				
+				.active_step_text {
+					color: #fff;
+					font-size: 0.6875rem;
+					display: flex;
+					flex-direction: row;
+					align-items: center;
+					justify-content: center;
+					text-align: center;
+					line-height: 0.6875rem;
+				}
+			}
+		}
+	}
+}
+
+// 环节备注样式
+.remark_container {
+	.remark_content {
+		margin-top: 10px;
+	}
+}
+
+// 审批按钮样式
+.approve_button {
+	position: sticky;
+	z-index: 10;
+	width: 100%;
+	bottom: 10px;
+	margin-top: 15px;
+	
+	button {
+		background-color: #007aff !important;
+		color: #fff !important;
+	}
+}
+
+// 退回按钮样式
+.reject_button {
+	margin-top: 10px;
+	
+	button {
+		font-size: calc(18px + .5*(1rem - 16px));
+	}
+}
+
+// 附件上传容器
+.file_picker_container {
+	margin-top: 15px;
+	
+	::v-deep .uni-card {
+		.uni-card__header-content-title {
+			font-size: calc(15px + .5*(1rem - 16px));
+			font-weight: 700;
+		}
+		
+		.uni-card__header-extra-text {
+			font-size: calc(15px + .5*(1rem - 16px));
+		}
+	}
+}
+
+// 基本信息中的禁用字段样式优化
+::v-deep .uni-forms {
+  .uni-forms-item__content {
+    .uni-easyinput__content-input {
+      font-size: calc(14px + 1.2*(1rem - 16px)) !important;
+      font-weight: 500;
+      color: #333;
+    }
+
+    .uni-date {
+      .uni-icons {
+        font-size: calc(22px + 1.2*(1rem - 16px)) !important;
+        font-weight: 500;
+      }
+
+      .uni-date__x-input {
+        height: auto;
+        font-size: calc(14px + 1.2*(1rem - 16px)) !important;
+        font-weight: 500;
+        color: #333;
+      }
+    }
+  }
+}
+</style>

+ 820 - 0
pages/work/purchase/start.vue

@@ -0,0 +1,820 @@
+<template>
+	<view class="purchase-container">
+		<!-- 基本信息 -->
+		<uni-card title="基本信息" spacing="0" >
+			<uni-forms ref="baseFormRef" :modelValue="baseForm" :rules="baseFormRules" label-position="left" :label-width="100" :border="true">
+				<uni-forms-item name="contractPurchaseNumber" label="采购单号">
+					<uni-easyinput v-model="baseForm.contractPurchaseNumber" disabled placeholder="自动生成"></uni-easyinput>
+				</uni-forms-item>
+				<uni-forms-item name="contractPurchaseName" label="采购单名称" required>
+					<uni-easyinput v-model="baseForm.contractPurchaseName" placeholder="请输入采购单名称"></uni-easyinput>
+				</uni-forms-item>
+				<uni-forms-item name="applyDate" label="申请日期">
+					<uni-easyinput v-model="baseForm.applyDate" type="date" disabled />
+				</uni-forms-item>
+				<uni-forms-item name="department" label="申请部门">
+					<uni-easyinput v-model="baseForm.department" disabled></uni-easyinput>
+				</uni-forms-item>
+			</uni-forms>
+		</uni-card>
+
+		<!-- 物料明细 -->
+		<uni-card title="物品信息" spacing="0">
+			<view class="material-actions">
+				<button type="primary" size="mini" @click="openMaterialSelector" plain>添加物料</button>
+			</view>
+			
+			<!-- 物料列表 - 卡片式展示 -->
+			<view v-if="materialList.length > 0" class="material-list">
+				<view v-for="(item, index) in materialList" :key="index" class="material-card">
+					<view class="material-header" @click="toggleExpand(index)">
+						<view class="material-main-info">
+							<text class="material-name">{{ item.materialName }}</text>
+							<text class="material-code">{{ item.materialCode }}</text>
+						</view>
+						<view class="material-expand">
+							<uni-icons :type="item.expanded ? 'up' : 'down'" size="16" color="#999"></uni-icons>
+						</view>
+					</view>
+					
+					<!-- 展开的详细信息 -->
+					<view v-show="item.expanded" class="material-detail">
+						<view class="detail-row">
+							<text class="detail-label">规格型号:</text>
+							<text class="detail-value">{{ item.materialModel }}</text>
+							<text class="detail-label" style="margin-left: 15px;">单位:</text>
+							<text class="detail-value">{{ item.measureName }}</text>
+						</view>
+						<view class="detail-row delete-row">
+							<text class="detail-label">数量:</text>
+							<uni-easyinput 
+								v-model="item.qty" 
+								type="digit" 
+								placeholder="请输入数量"
+								style="width: 100px; display: inline-block; margin-right: 15px;"
+							/>
+							<button type="warn" size="mini" @click="removeMaterial(index)">删除</button>
+						</view>
+					</view>
+				</view>
+			</view>
+			
+			<view v-else class="empty-materials">
+				<text>暂无物料,请点击上方按钮添加</text>
+			</view>
+		</uni-card>
+
+		<!-- 上传附件 -->
+		<uni-card title="上传附件" :extra="`${fileList.length}/50`" spacing="0">
+			<uni-file-picker 
+				ref="filePicker" 
+				v-model="fileList" 
+				:auto-upload="true" 
+				mode="list" 
+				:limit="50"
+				:limit-length="50" 
+				file-mediatype="all"
+				@select="handleFileSelect" 
+				@progress="handleFileProgress" 
+				@success="handleFileSuccess" 
+				@fail="handleFileFail" 
+				@delete="handleFileDelete" 
+			/>
+		</uni-card>
+
+		<!-- 提交按钮 -->
+		<view class="submit-btn-wrapper">
+			<button type="primary" :loading="isSubmitting" :disabled="isSubmitting" @click="submitForm">提 交</button>
+		</view>
+
+		<!-- 选择器弹出层 -->
+		<uni-popup ref="selectorPopup" type="center">
+			<view class="selector-popup">
+				<view class="popup-header">
+					<text class="popup-title">{{ popupTitle }}</text>
+					<uni-icons type="closeempty" size="20" @click="closePopup"></uni-icons>
+				</view>
+				
+				<!-- 搜索框 -->
+				<view v-if="selectorType === 'material'" class="search-bar">
+					<uni-easyinput 
+						v-model="searchKeyword" 
+						placeholder="输入物料名称搜索"
+						clearable
+						@confirm="handleSearch"
+					/>
+					<button type="primary" size="mini" @click="handleSearch">搜索</button>
+				</view>
+				
+				<scroll-view 
+					scroll-y 
+					class="popup-content"
+					refresher-enabled
+					:refresher-triggered="isRefreshing"
+					@refresherrefresh="onRefresh"
+					@scrolltolower="loadMore"
+				>
+					<view v-for="(item, index) in selectorList" :key="index" 
+					      class="selector-item" 
+					      @click="selectItem(item)">
+						<view class="selector-item-content">
+							<view class="item-info">
+								<text class="item-name">{{ item.materialName || item.itemName }}</text>
+								<text class="item-code">{{ item.materialCode || item.itemCode }}</text>
+								<text class="item-spec">{{ item.materialModel || item.specification || '' }}</text>
+								<text class="item-unit">{{ item.measureName || item.unit || '' }}</text>
+								<text v-if="item.itemTypeName" class="item-type">{{ item.itemTypeName }}</text>
+							</view>
+							<text v-if="isSelected(item)" class="selected-tag">已选择</text>
+						</view>
+					</view>
+					
+					<!-- 加载状态 -->
+					<view v-if="isLoading && selectorList.length > 0" class="loading-text">
+						<uni-load-more status="loading" />
+					</view>
+					
+					<!-- 没有更多数据 -->
+					<view v-else-if="!hasMore && selectorList.length > 0" class="no-more-text">
+						<text>没有更多了</text>
+					</view>
+					
+					<!-- 空数据提示 -->
+					<view v-if="selectorList.length === 0 && !isLoading" class="empty-data">
+						<text>{{ searchKeyword ? '暂无相关数据' : '暂无数据' }}</text>
+					</view>
+				</scroll-view>
+			</view>
+		</uni-popup>
+	</view>
+</template>
+
+<script setup lang="ts">
+import { onMounted, reactive, ref, computed } from 'vue'
+import { onLoad } from '@dcloudio/uni-app'
+import { useUserStore } from '@/store/user.js'
+import $modal from '@/plugins/modal.js'
+import $tab from '@/plugins/tab.js'
+import { getPurchaseInitData, startPurchaseProcess, getMaterialList } from '@/api/purchase.js'
+import { uploadFile, getProcessInfo } from '@/api/work.js'
+import config from '@/config.js'
+
+const userStore = useUserStore()
+
+// 流程信息(参考通用流程)
+let processInfo = reactive({
+	modelName: '采购申请',
+	reqOffice: 0,
+	control: '',
+	formId: '',
+	modelId: '',
+	tmodelId: '',
+	isMoreIns: '',
+	pathJudgeType: '',
+	form: []
+})
+
+// 接收页面参数
+onLoad((options) => {
+	const { modelName, modelId, control } = options
+	if (modelName) processInfo.modelName = modelName
+	if (modelId) processInfo.modelId = modelId
+	if (control) processInfo.control = control
+})
+
+// 初始化流程信息(获取 formId, tmodelId 等)
+onMounted(() => {
+	initProcessInfo().then(() => {
+		// 初始化基本信息(流水号、部门等)
+		initBaseForm()
+	})
+})
+
+function initProcessInfo() {
+	return new Promise<void>((resolve, reject) => {
+		// 如果没有传入 modelId,使用默认值或提示错误
+		if (!processInfo.modelId) {
+			$modal.msgError('缺少流程模型')
+			reject(new Error('缺少流程模型 ID'))
+			return
+		}
+		
+		getProcessInfo(processInfo)
+			.then(({ returnParams }) => {
+				const { formId, tmodelId, isMoreIns, pathJudgeType, reqOffice } = returnParams.flow[0]
+				processInfo.formId = formId
+				processInfo.tmodelId = tmodelId
+				processInfo.isMoreIns = isMoreIns
+				processInfo.pathJudgeType = pathJudgeType
+				processInfo.reqOffice = reqOffice || 0 // 是否需要附件(0-不需要,1-需要)
+				resolve()
+			})
+			.catch(err => {
+				console.error('获取流程信息失败:', err)
+				reject(err)
+			})
+	})
+}
+
+function initBaseForm() {
+	// 获取初始化数据(流水号、部门信息等)
+	getPurchaseInitData(userStore.user.useId).then(res => {
+		if (res.returnCode === '1') {
+			const params = res.returnParams
+			baseForm.contractPurchaseNumber = params.contractPurchaseNumber || ''
+			baseForm.initiator = params.initiator || userStore.user.name
+			baseForm.department = params.department || ''
+			baseForm.depid = params.depid || null
+		}
+	}).catch(err => {
+		console.error('获取初始化数据失败:', err)
+	})
+}
+const baseForm = reactive({
+	contractPurchaseNumber: '',
+	contractPurchaseName: '',
+	// iOS 兼容的日期格式:使用 "/" 而不是 "-"
+	applyDate: '',
+	initiator: '',
+	department: '',
+  depid: null
+})
+
+const materialList = ref<any[]>([])
+const isSubmitting = ref(false)
+
+// 文件上传相关
+const fileList = ref<any[]>([])
+const fileSeqs = ref<any[]>([])
+
+// 表单校验规则(参考通用流程)
+const baseFormRef = ref(null)
+const baseFormRules = computed(() => {
+	return {
+		contractPurchaseName: {
+			rules: [{ required: true, message: '请输入采购单名称' }],
+			label: '采购单名称'
+		}
+	}
+})
+
+// 选择器相关
+const selectorPopup = ref(null)
+const selectorList = ref<any[]>([])
+const selectorType = ref('') // 'material'
+
+// 分页和搜索相关
+const currentPage = ref(1)
+const pageSize = 20
+const hasMore = ref(true)
+const isLoading = ref(false)
+const isRefreshing = ref(false)
+const searchKeyword = ref('')
+
+const popupTitle = computed(() => {
+	return selectorType.value === 'material' ? '选择物料' : '选择'
+})
+
+// 切换展开/收起状态
+function toggleExpand(index: number) {
+	if (materialList.value[index]) {
+		materialList.value[index].expanded = !materialList.value[index].expanded
+	}
+}
+
+onLoad(() => {
+	initData()
+})
+
+async function initData() {
+	try {
+		const res = await getPurchaseInitData(userStore.user.useId)
+		if (res.returnCode === '1') {
+			const data = res.returnParams
+			baseForm.contractPurchaseNumber = data.contractPurchaseNumber
+			baseForm.initiator = data.initiator
+			baseForm.department = data.department
+			baseForm.applyDate = data.applyDate
+      baseForm.depid = data.depid
+		}
+	} catch (error) {
+		console.error('初始化数据失败', error)
+		$modal.msgError('加载数据失败')
+	}
+}
+
+// 打开物料选择器
+async function openMaterialSelector() {
+	selectorType.value = 'material'
+	currentPage.value = 1
+	hasMore.value = true
+	selectorList.value = []
+	await loadMaterials(1, false)
+	openPopup()
+}
+
+// 加载物料列表(分页)
+async function loadMaterials(page: number, append: boolean = false) {
+	if (isLoading.value) return
+	
+	isLoading.value = true
+	if (page === 1) {
+		isRefreshing.value = true
+	}
+	
+	try {
+		const res = await getMaterialList(
+			userStore.user.useId,
+			page,
+			pageSize,
+			searchKeyword.value
+		)
+		
+		if (res.returnCode === '1') {
+			const result = res.returnParams
+			const newList = result.list || []
+			
+			if (append) {
+				// 追加模式(加载更多)
+				selectorList.value = [...selectorList.value, ...newList]
+			} else {
+				// 覆盖模式(刷新/搜索)
+				selectorList.value = newList
+			}
+			
+			// 判断是否还有更多数据
+			hasMore.value = selectorList.value.length < result.total
+		} else {
+			$modal.msgError('加载物料失败')
+		}
+	} catch (error) {
+		console.error('加载物料失败', error)
+		$modal.msgError('加载失败')
+	} finally {
+		isLoading.value = false
+		isRefreshing.value = false
+	}
+}
+
+// 加载更多
+function loadMore() {
+	if (!hasMore.value || isLoading.value) return
+	currentPage.value++
+	loadMaterials(currentPage.value, true)
+}
+
+// 下拉刷新
+function onRefresh() {
+	currentPage.value = 1
+	loadMaterials(1, false)
+}
+
+// 搜索
+function handleSearch() {
+	currentPage.value = 1
+	loadMaterials(1, false)
+}
+
+// 判断物料是否已选中
+function isSelected(item: any): boolean {
+	const code = item.materialCode || item.itemCode
+	return materialList.value.some(m => 
+		(m.materialCode || '') === code
+	)
+}
+
+function openPopup() {
+	;(selectorPopup.value as any).open()
+}
+
+function closePopup() {
+	;(selectorPopup.value as any).close()
+}
+
+function selectItem(item: any) {
+	// 只处理物料选择
+	addMaterial(item)
+	closePopup()
+}
+
+function addMaterial(item: any) {
+	// 检查是否已存在
+	const exists = materialList.value.some(m => m.materialCode === item.materialCode || m.materialCode === item.itemCode)
+	if (exists) {
+		$modal.msgWarn('该物料已存在')
+		return
+	}
+	
+	materialList.value.push({
+		materialCode: item.materialCode || item.itemCode,
+		materialName: item.materialName || item.itemName,
+		materialModel: item.materialModel || item.specification,
+		measureName: item.measureName || item.unit,
+		itemTypeName: item.itemTypeName || '', // 物料类别
+		qty: item.qty || 0,
+		expanded: true // 默认展开
+	})
+}
+async function handleFileSelect(files: any) {
+	files.tempFiles.forEach(async (file: any) => {
+		const data = {
+			name: file.name,
+			filePath: file.path,
+		}
+		try {
+			const res = await uploadFile(data)
+			file.seq = res.returnParams
+			fileSeqs.value.push({ 'seq': res.returnParams, 'path': file.path })
+			fileList.value.push(file)
+			$modal.msgSuccess('文件' + data.name + '上传成功')
+		} catch (err) {
+			$modal.msgError('文件' + data.name + '上传失败,请删除重新上传')
+			console.error('文件上传失败:', err)
+		}
+	})
+}
+
+function handleFileProgress(file: any, progress: any) {
+	console.log('handleFileProgress', file, progress)
+}
+
+function handleFileSuccess(file: any, res: any) {
+	console.log('handleFileSuccess', file, res)
+}
+
+function handleFileFail(file: any, err: any) {
+	console.log('handleFileFail', file, err)
+}
+
+function handleFileDelete(file: any) {
+	const index = fileSeqs.value.findIndex(({ path }) => path === file.tempFilePath)
+	if (index !== -1) {
+		fileSeqs.value.splice(index, 1)
+	}
+}
+
+function removeMaterial(index: number) {
+	$modal.confirm('', '确认删除该物料?')
+		.then(() => {
+			materialList.value.splice(index, 1)
+		})
+		.catch(() => {})
+}
+
+async function submitForm() {
+	// 先进行表单校验(使用 uni-forms 的 validate 方法)
+	if (!baseFormRef.value) {
+		$modal.msgError('表单未初始化')
+		return
+	}
+	
+	try {
+		await baseFormRef.value.validate()
+	} catch (error) {
+		console.error('表单校验失败', error)
+		$modal.msgError('请填写必填项')
+		return
+	}
+	
+	// 校验物料列表和数量
+	if (materialList.value.length === 0) {
+		$modal.msgError('请添加物料')
+		return
+	}
+	
+	// 检查每个物料的数量是否填写
+	for (let i = 0; i < materialList.value.length; i++) {
+		const item = materialList.value[i]
+		if (!item.qty || Number(item.qty) <= 0) {
+			uni.showToast({
+				title: `请填写第${i + 1}个物料的采购数量`,
+				icon: 'none',
+				duration: 2000
+			})
+			return
+		}
+	}
+
+	isSubmitting.value = true
+	
+	try {
+		// 构建表单数据(包含流程信息)
+		const formData = {
+			...baseForm,
+			detailList: materialList.value.map(item => ({
+				materialCode: item.materialCode,
+				materialName: item.materialName,
+				materialModel: item.materialModel,
+				measureName: item.measureName,
+				qty: item.qty,
+				price: item.price,
+				cess: item.cess
+			}))
+		}
+
+		// 准备提交流程参数(参考通用流程 bpm_20150325001FlowStart)
+		const processInfoData = {
+			staffId: userStore.user.useId,
+			staffName: userStore.user.name,
+			gxId: userStore.user.gxId,
+			groupId: userStore.user.groupid,
+			insName: userStore.user.name + '的'+ processInfo.modelName,
+			modelId: processInfo.modelId, // 从页面参数或初始化获取
+			tmodelId: processInfo.tmodelId, // 从 getProcessInfo 获取
+			formId: processInfo.formId, // 从 getProcessInfo 获取
+			// 附件 ID:参考 work.js,为空时设置为空字符串
+			fileIds: fileSeqs.value.length === 0 ? '' : fileSeqs.value.map(f => f.seq)
+		}
+
+		// 检查是否需要附件
+		if (processInfo.reqOffice == 1 && fileSeqs.value.length === 0) {
+			$modal.msgError('该流程需要上传附件')
+			isSubmitting.value = false
+			return
+		}
+
+		// 一次性提交表单和流程数据(包含附件)
+		const res = await startPurchaseProcess(userStore.user.useId, formData, processInfoData)
+		
+		if (res.returnCode !== '1') {
+			$modal.msgError(res.returnMsg || '提交失败')
+			isSubmitting.value = false
+			return
+		}
+
+		$modal.msgSuccess('提交成功')
+		setTimeout(() => {
+			$tab.navigateBack()
+		}, 1000)
+	} catch (error) {
+		console.error('提交失败', error)
+		$modal.msgError('提交失败,请重试')
+	} finally {
+		isSubmitting.value = false
+	}
+}
+</script>
+
+<style lang="scss" scoped>
+.purchase-container {
+	padding-bottom: 80px;
+}
+
+.material-actions {
+	display: flex;
+	gap: 10px;
+	margin-bottom: 15px;
+	
+	button {
+		flex: 1;
+	}
+}
+
+// 基本信息中的禁用字段样式优化(参考 edit/index.vue)
+::v-deep .uni-forms {
+	.uni-forms-item__content {
+		.uni-easyinput__content-input {
+			font-size: calc(14px + 1.2*(1rem - 16px)) !important;
+			font-weight: 500;
+			color: #333;
+		}
+		
+		.uni-date {
+			.uni-icons {
+				font-size: calc(22px + 1.2*(1rem - 16px)) !important;
+				font-weight: 500;
+			}
+			
+			.uni-date__x-input {
+				height: auto;
+				font-size: calc(14px + 1.2*(1rem - 16px)) !important;
+				font-weight: 500;
+				color: #333;
+			}
+		}
+	}
+}
+
+// 物料列表 - 卡片式展示
+.material-list {
+	.display: flex;
+	flex-direction: column;
+	gap: 10px;
+}
+
+.material-card {
+	border: 1px solid #e5e5e5;
+	border-radius: 6px;
+	overflow: hidden;
+	background-color: #fff;
+	margin-bottom: 8px;
+		
+	.material-header {
+		display: flex;
+		justify-content: space-between;
+		align-items: center;
+		padding: 10px 12px;
+		background-color: #f8f9fa;
+		cursor: pointer;
+			
+		.material-main-info {
+			display: flex;
+			align-items: center;
+			gap: 8px;
+			flex: 1;
+				
+			.material-name {
+				font-size: 14px;
+				font-weight: bold;
+				color: #333;
+			}
+				
+			.material-code {
+				font-size: 12px;
+				color: #999;
+				white-space: nowrap;
+			}
+		}
+			
+		.material-expand {
+			display: flex;
+			align-items: center;
+			margin-left: 10px;
+		}
+	}
+		
+	.material-detail {
+		padding: 10px 12px;
+		border-top: 1px solid #e5e5e5;
+			
+		.detail-row {
+			display: flex;
+			align-items: center;
+			flex-wrap: wrap;
+			gap: 8px;
+				
+			.detail-label {
+				font-size: 13px;
+				color: #666;
+				flex-shrink: 0;
+			}
+				
+			.detail-value {
+				flex: 0 0 auto;
+				font-size: 13px;
+				color: #333;
+			}
+		}
+			
+		.delete-row {
+			margin-top: 10px;
+			text-align: center;
+			padding-top: 8px;
+			border-top: 1px dashed #e5e5e5;
+		}
+	}
+}
+
+.empty-materials {
+	text-align: center;
+	padding: 30px;
+	color: #909399;
+	font-size: 14px;
+}
+
+.submit-btn-wrapper {
+	position: fixed;
+	bottom: 0;
+	left: 0;
+	right: 0;
+	padding: 10px 15px;
+	background-color: #fff;
+	box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.05);
+	z-index: 100;
+	
+	button {
+		background-color: #007aff !important;
+		color: #fff !important;
+	}
+}
+
+.selector-popup {
+	width: 90%;
+	max-width: 500px;
+	max-height: 70vh;
+	background-color: #fff;
+	border-radius: 12px;
+	overflow: hidden;
+	box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
+	
+	/* 确保弹窗居中 */
+	position: relative;
+	margin: 0 auto;
+	
+	.popup-header {
+		display: flex;
+		justify-content: space-between;
+		align-items: center;
+		padding: 15px;
+		border-bottom: 1px solid #e5e5e5;
+		
+		.popup-title {
+			font-size: 16px;
+			font-weight: bold;
+		}
+	}
+	
+	.search-bar {
+		display: flex;
+		gap: 10px;
+		padding: 10px 15px;
+		align-items: center;
+	}
+	
+	.popup-content {
+		max-height: 50vh;
+		
+		.selector-item {
+			padding: 10px 12px;
+			border-bottom: 1px solid #f0f0f0;
+			
+			&:active {
+				background-color: #f5f5f5;
+			}
+			
+			.selector-item-content {
+				display: flex;
+				justify-content: space-between;
+				align-items: center;
+				gap: 8px;
+				
+				.item-info {
+					flex: 1;
+					display: flex;
+					flex-wrap: wrap;
+					align-items: center;
+					gap: 4px;
+					
+					.item-name {
+						font-size: 15px;
+						font-weight: bold;
+						color: #333;
+						flex-basis: 100%;
+						margin-bottom: 4px;
+					}
+					
+					.item-code,
+					.item-spec,
+					.item-unit,
+					.item-type {
+						font-size: 13px;
+						color: #666;
+						white-space: nowrap;
+						font-weight: 500;
+					}
+					
+					.item-code::after,
+					.item-spec::after,
+					.item-unit::after {
+						content: ' | ';
+						margin: 0 4px;
+						color: #ddd;
+					}
+					
+					.item-type:last-child::after,
+					.item-unit:last-child::after {
+						content: '';
+					}
+				}
+				
+				.selected-tag {
+					color: #409eff;
+					font-size: 12px;
+					flex-shrink: 0;
+					white-space: nowrap;
+				}
+			}
+		}
+		
+		.loading-text {
+			text-align: center;
+			padding: 12px;
+			color: #909399;
+			font-size: 13px;
+		}
+		
+		.no-more-text {
+			text-align: center;
+			padding: 12px;
+			color: #909399;
+			font-size: 13px;
+		}
+		
+		.empty-data {
+			text-align: center;
+			padding: 30px;
+			color: #909399;
+		}
+	}
+}
+</style>