| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131 |
- <!-- 采购流程专用表单组件 -->
- <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 = []
- }
- }
- }, { 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 (baseForm.value.contractPurchaseName && baseForm.value.contractPurchaseName.length > 100) {
- return Promise.reject(new Error('采购单名称长度不能超过 100 个字符'))
- }
- if (!materialList.value || materialList.value.length === 0) {
- return Promise.reject(new Error('请至少添加一个物料'))
- }
- // 验证物料数量(发起环节或字段可编辑时)
- if (isSeModel.value && materialList.value.length > 0) {
- for (let i = 0; i < materialList.value.length; i++) {
- const item = materialList.value[i]
- // 验证数量不能为空且必须大于 0
- if (!item.qty || Number(item.qty) <= 0) {
- return Promise.reject(new Error(`请填写第${i + 1}个物料的数量,且必须大于 0`))
- }
- }
- }
- // 验证审批意见字段(如果在 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)) {
- 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 && elem.defaultValue) {
- // 兼容旧数据:如果有 sealImgPath 和 defaultValue,提取 sealId 构建 imgval
- // sealImgPath 格式:/shares/document/seal/593268258724500.png
- const sealIdMatch = elem.sealImgPath.match(/\/seal\/(\d+)\.png$/)
- if (sealIdMatch) {
- const sealId = sealIdMatch[1]
- formElements.push({
- name: fieldName + '_imgval',
- value: `${sealId}_0_0`,
- 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.sealImgPath && elem.defaultValue) {
- // 兼容旧数据:如果有 sealImgPath 和 defaultValue,提取 sealId 构建 imgval
- // sealImgPath 格式:/shares/document/seal/593268258724500.png
- const sealIdMatch = elem.sealImgPath.match(/\/seal\/(\d+)\.png$/)
- if (sealIdMatch) {
- const sealId = sealIdMatch[1]
- formData[fieldName + '_imgval'] = `${sealId}_0_0`
- }
- }
- }
- })
-
- 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>
|