refactor(app): 移除页面附件上传功能
| | |
| | | <script setup lang="ts"> |
| | | import { ref } from "vue"; |
| | | import { chooseAftersalesFiles, uploadAftersalesFile, type AftersalesAttachment } from "../../api/aftersales"; |
| | | const props = withDefaults(defineProps<{ modelValue?: number[]; files?: AftersalesAttachment[]; token?: string | null; readonly?: boolean }>(), { modelValue: () => [], files: () => [], token: null, readonly: false }); |
| | | const emit = defineEmits<{ "update:modelValue": [value: number[]]; "update:files": [value: AftersalesAttachment[]] }>(); |
| | | const uploading = ref(false); |
| | | async function addFiles() { |
| | | if (!props.token || uploading.value || props.readonly) return; |
| | | const left = 10 - props.modelValue.length; |
| | | if (left <= 0) return uni.showToast({ title: "最多上传 10 个附件", icon: "none" }); |
| | | try { |
| | | const picked = await chooseAftersalesFiles(left); uploading.value = true; |
| | | const ids = [...props.modelValue], files = [...props.files]; |
| | | for (const file of picked) { const saved = await uploadAftersalesFile(props.token, file.path); ids.push(saved.id); files.push({ ...saved, name: saved.name || file.name }); } |
| | | emit("update:modelValue", ids); emit("update:files", files); |
| | | } catch (error) { if (!String(error).includes("cancel")) uni.showToast({ title: "附件上传失败", icon: "none" }); } |
| | | finally { uploading.value = false; } |
| | | } |
| | | function remove(index: number) { emit("update:modelValue", props.modelValue.filter((_, i) => i !== index)); emit("update:files", props.files.filter((_, i) => i !== index)); } |
| | | import type { AftersalesAttachment } from "../../api/aftersales"; |
| | | withDefaults(defineProps<{ modelValue?: number[]; files?: AftersalesAttachment[]; token?: string | null; readonly?: boolean }>(), { modelValue: () => [], files: () => [], token: null, readonly: false }); |
| | | defineEmits<{ "update:modelValue": [value: number[]]; "update:files": [value: AftersalesAttachment[]] }>(); |
| | | function preview(file: AftersalesAttachment) { |
| | | if (!file.url) return uni.showToast({ title: "附件暂无可访问地址", icon: "none" }); |
| | | const url = file.url; |
| | |
| | | // #endif |
| | | } |
| | | </script> |
| | | <template><view class="attachments"><view class="attachment-head"><text>附件</text><button v-if="!readonly" :loading="uploading" @click="addFiles">添加附件</button></view><view v-for="(file,index) in files" :key="file.id" class="file"><text class="file-name" @click="preview(file)">{{ file.name || `附件 ${index+1}` }}</text><text v-if="!readonly" @click="remove(index)">删除</text></view><text v-if="!files.length" class="empty">暂无附件</text></view></template> |
| | | <template><view v-if="files.length" class="attachments"><view class="attachment-head"><text>附件</text></view><view v-for="(file,index) in files" :key="file.id" class="file"><text class="file-name" @click="preview(file)">{{ file.name || `附件 ${index+1}` }}</text></view></view></template> |
| | | <style scoped lang="scss">.attachments{padding:20rpx 24rpx}.attachment-head,.file{display:flex;align-items:center;justify-content:space-between;gap:20rpx}.attachment-head{color:#33455d;font-size:27rpx}.attachment-head button{width:150rpx;height:56rpx;margin:0;color:var(--color-primary, #1769d8);font-size:23rpx;line-height:54rpx;background:#f3f7fd;border:1rpx solid #b9d1f2}.attachment-head button::after{border:0}.file{padding:14rpx 0;color:#526780;font-size:24rpx}.file-name{min-width:0;flex:1;overflow:hidden;color:var(--color-primary, #1769d8);text-decoration:underline;text-overflow:ellipsis;white-space:nowrap}.file text:last-child{flex:none;color:#cf4d4d}.empty{display:block;margin-top:14rpx;color:#9aa7b8;font-size:24rpx}</style> |
| | |
| | | return ""; |
| | | } |
| | | |
| | | /** 上传后的文件值(数组,元素为 { name, url } 或 url 字符串) */ |
| | | const uploading = computed(() => false); |
| | | |
| | | /** 历史文件值仅用于只读展示,App 端不提供附件上传。 */ |
| | | function fileList(field: FormField) { |
| | | const value = getValue(field); |
| | | if (!Array.isArray(value)) return []; |
| | | return value.map(item => |
| | | typeof item === "string" ? { name: item, url: item } : item, |
| | | ); |
| | | } |
| | | |
| | | async function removeFile(field: FormField, index: number) { |
| | | const list = fileList(field); |
| | | const next = list.filter((_, i) => i !== index); |
| | | setValue( |
| | | field.field, |
| | | next.map(item => item.value ?? item.url ?? item), |
| | | ); |
| | | } |
| | | |
| | |
| | | <view v-else-if="field.kind === 'image' || field.kind === 'file'" class="upload"> |
| | | <view v-for="(file, index) in fileList(field)" :key="index" class="upload-item"> |
| | | <text class="upload-name">{{ file.name || `文件${index + 1}` }}</text> |
| | | <text class="upload-remove" @click="removeFile(field, index)">删除</text> |
| | | </view> |
| | | <text class="upload-tip">{{ field.placeholder }}</text> |
| | | </view> |
| | | |
| | | <!-- 兜底:不支持的组件按文本录入,保证值能填上 --> |
| | |
| | | import { getSimpleDictDataList, type DictData } from "../../../api/crm"; |
| | | import { |
| | | approveTask, |
| | | chooseBpmFile, |
| | | copyTask, |
| | | delegateTask, |
| | | getApprovalDetail, |
| | |
| | | signCreateTask, |
| | | signDeleteTask, |
| | | transferTask, |
| | | uploadBpmFile, |
| | | type BpmApprovalDetail, |
| | | type BpmApprovalNode, |
| | | type BpmDept, |
| | |
| | | }); |
| | | } |
| | | |
| | | /* ---------- 附件上传 ---------- */ |
| | | |
| | | const uploading = ref(false); |
| | | |
| | | async function chooseAttachment() { |
| | | if (!state.accessToken || uploading.value) return; |
| | | let files: Array<{ path: string; name?: string }> = []; |
| | | try { |
| | | files = await chooseBpmFile(9); |
| | | } catch (error) { |
| | | const msg = error instanceof Error ? error.message : ""; |
| | | if (msg && !/cancel/i.test(msg)) toast(msg); |
| | | return; |
| | | } |
| | | if (!files.length) return; |
| | | uploading.value = true; |
| | | uni.showLoading({ title: "上传中…", mask: true }); |
| | | try { |
| | | for (const file of files) { |
| | | const saved = await uploadBpmFile(state.accessToken, file.path); |
| | | if (saved.url) operationAttachments.value = [...operationAttachments.value, saved.url]; |
| | | } |
| | | toast("上传成功"); |
| | | } catch (error) { |
| | | toast(error instanceof Error ? error.message : "上传失败"); |
| | | } finally { |
| | | uploading.value = false; |
| | | uni.hideLoading(); |
| | | } |
| | | } |
| | | |
| | | const operationAttachments = ref<string[]>([]); |
| | | |
| | | function removeAttachment(index: number) { |
| | | operationAttachments.value = operationAttachments.value.filter((_, i) => i !== index); |
| | | } |
| | | |
| | | onLoad(async query => { |
| | | processInstanceId.value = query?.id ? String(query.id) : ""; |
| | | taskId.value = query?.taskId ? String(query.taskId) : ""; |
| | |
| | | class="modal-textarea" |
| | | placeholder="请输入审批意见" |
| | | /> |
| | | </view> |
| | | |
| | | <!-- 附件 --> |
| | | <view class="modal-field"> |
| | | <text class="modal-label">附件</text> |
| | | <view class="attach-list"> |
| | | <view v-for="(url, index) in operationAttachments" :key="url" class="attach-item"> |
| | | <text class="attach-name">附件 {{ index + 1 }}</text> |
| | | <text class="attach-remove" @click="removeAttachment(index)">删除</text> |
| | | </view> |
| | | </view> |
| | | <button class="attach-btn" :loading="uploading" @click="chooseAttachment">添加附件</button> |
| | | </view> |
| | | |
| | | <view class="modal-actions"> |
| | |
| | | }, |
| | | }); |
| | | } |
| | | async function chooseFollowFile() { |
| | | if (!state.accessToken || uploading.value) return; |
| | | uni.chooseFile({ |
| | | count: 10 - followForm.blobIds.length, |
| | | success: async result => { |
| | | uploading.value = true; |
| | | try { |
| | | for (const file of result.tempFiles || []) { |
| | | if (!file.path) continue; |
| | | const attachment = await uploadCrmFile(state.accessToken!, file.path); |
| | | followForm.blobIds.push(attachment.id); |
| | | followForm.attachments.push(attachment); |
| | | } |
| | | } catch (error) { |
| | | uni.showToast({ |
| | | title: error instanceof Error ? error.message : "附件上传失败", |
| | | icon: "none", |
| | | }); |
| | | } finally { |
| | | uploading.value = false; |
| | | } |
| | | }, |
| | | }); |
| | | } |
| | | function removeImage(index: number) { |
| | | followForm.picUrls.splice(index, 1); |
| | | } |
| | | function removeFile(index: number) { |
| | | followForm.blobIds.splice(index, 1); |
| | | followForm.attachments.splice(index, 1); |
| | | } |
| | | async function saveFollow() { |
| | | if ( |
| | |
| | | @click="uni.previewImage({urls:followForm.picUrls,current:url})" /><text @click="removeImage(index)">×</text> |
| | | </view> |
| | | </view> |
| | | <view class="upload-head"><text>附件</text><text @click="chooseFollowFile">上传附件</text></view> |
| | | <view v-for="(item,index) in followForm.attachments" |
| | | :key="item.id" |
| | | class="file-item"><text>{{item.name || `附件 #${item.id}`}}</text><text @click="removeFile(index)">删除</text></view><button class="primary" |
| | | <button class="primary" |
| | | :loading="saving || uploading" |
| | | @click="saveFollow">保存跟进</button> |
| | | </view> |
| | |
| | | }, |
| | | }); |
| | | } |
| | | async function chooseFollowFile() { |
| | | if (!state.accessToken || uploading.value) return; |
| | | uni.chooseFile({ |
| | | count: 10 - followForm.blobIds.length, |
| | | success: async result => { |
| | | uploading.value = true; |
| | | try { |
| | | for (const file of result.tempFiles || []) { |
| | | if (!file.path) continue; |
| | | const attachment = await uploadCrmFile(state.accessToken!, file.path); |
| | | followForm.blobIds.push(attachment.id); |
| | | followForm.attachments.push(attachment); |
| | | } |
| | | } catch (error) { |
| | | uni.showToast({ |
| | | title: error instanceof Error ? error.message : "附件上传失败", |
| | | icon: "none", |
| | | }); |
| | | } finally { |
| | | uploading.value = false; |
| | | } |
| | | }, |
| | | }); |
| | | } |
| | | function removeImage(index: number) { |
| | | followForm.picUrls.splice(index, 1); |
| | | } |
| | | function removeFile(index: number) { |
| | | followForm.blobIds.splice(index, 1); |
| | | followForm.attachments.splice(index, 1); |
| | | } |
| | | async function saveFollow() { |
| | | if ( |
| | |
| | | @click="uni.previewImage({urls:followForm.picUrls,current:url})" /><text @click="removeImage(index)">×</text> |
| | | </view> |
| | | </view> |
| | | <view class="upload-head"><text>附件</text><text @click="chooseFollowFile">上传附件</text></view> |
| | | <view v-for="(item,index) in followForm.attachments" |
| | | :key="item.id" |
| | | class="file-item"><text>{{item.name || `附件 #${item.id}`}}</text><text @click="removeFile(index)">删除</text></view><button class="primary" |
| | | <button class="primary" |
| | | :loading="saving || uploading" |
| | | @click="saveFollow">保存跟进</button> |
| | | </view> |
| | |
| | | }, |
| | | }); |
| | | } |
| | | async function chooseFile() { |
| | | if (!state.accessToken || uploading.value) return; |
| | | uni.chooseFile({ |
| | | count: 10 - follow.blobIds.length, |
| | | success: async result => { |
| | | uploading.value = true; |
| | | try { |
| | | for (const file of result.tempFiles || []) { |
| | | if (!file.path) continue; |
| | | const attachment = await uploadCrmFile(state.accessToken!, file.path); |
| | | follow.blobIds.push(attachment.id); |
| | | follow.attachments.push(attachment); |
| | | } |
| | | } catch (error) { |
| | | uni.showToast({ |
| | | title: error instanceof Error ? error.message : "附件上传失败", |
| | | icon: "none", |
| | | }); |
| | | } finally { |
| | | uploading.value = false; |
| | | } |
| | | }, |
| | | }); |
| | | } |
| | | function removeImage(index: number) { |
| | | follow.picUrls.splice(index, 1); |
| | | } |
| | | function removeFile(index: number) { |
| | | follow.blobIds.splice(index, 1); |
| | | follow.attachments.splice(index, 1); |
| | | } |
| | | async function saveFollow() { |
| | | if ( |
| | |
| | | @click="uni.previewImage({urls:follow.picUrls,current:url})" /><text @click="removeImage(index)">×</text> |
| | | </view> |
| | | </view> |
| | | <view class="upload-head"><text>附件</text><text @click="chooseFile">上传附件</text></view> |
| | | <view v-for="(file,index) in follow.attachments" |
| | | :key="file.id" |
| | | class="file"><text>{{file.name||`附件 #${file.id}`}}</text><text @click="removeFile(index)">删除</text></view><button class="primary" |
| | | <button class="primary" |
| | | :loading="saving||uploading" |
| | | @click="saveFollow">保存跟进</button> |
| | | </view> |
| | |
| | | }, |
| | | }); |
| | | } |
| | | async function chooseFile() { |
| | | if (!state.accessToken || uploading.value) return; |
| | | uni.chooseFile({ |
| | | count: 10 - follow.blobIds.length, |
| | | success: async result => { |
| | | uploading.value = true; |
| | | try { |
| | | for (const file of result.tempFiles || []) { |
| | | if (!file.path) continue; |
| | | const attachment = await uploadCrmFile(state.accessToken!, file.path); |
| | | follow.blobIds.push(attachment.id); |
| | | follow.attachments.push(attachment); |
| | | } |
| | | } catch (error) { |
| | | uni.showToast({ |
| | | title: error instanceof Error ? error.message : "附件上传失败", |
| | | icon: "none", |
| | | }); |
| | | } finally { |
| | | uploading.value = false; |
| | | } |
| | | }, |
| | | }); |
| | | } |
| | | function removeImage(index: number) { |
| | | follow.picUrls.splice(index, 1); |
| | | } |
| | | function removeFile(index: number) { |
| | | follow.blobIds.splice(index, 1); |
| | | follow.attachments.splice(index, 1); |
| | | } |
| | | async function saveFollow() { |
| | | if ( |
| | |
| | | @click="uni.previewImage({urls:follow.picUrls,current:url})" /><text @click="removeImage(index)">×</text> |
| | | </view> |
| | | </view> |
| | | <view class="upload-head"><text>附件</text><text @click="chooseFile">上传附件</text></view> |
| | | <view v-for="(file,index) in follow.attachments" |
| | | :key="file.id" |
| | | class="file"><text>{{file.name||`附件 #${file.id}`}}</text><text @click="removeFile(index)">删除</text></view><button class="primary" |
| | | <button class="primary" |
| | | :loading="saving||uploading" |
| | | @click="saveFollow">保存跟进</button> |
| | | </view> |
| | |
| | | <script setup lang="ts"> |
| | | import { computed, reactive, ref } from 'vue'; |
| | | import { onLoad } from '@dcloudio/uni-app'; |
| | | import { createInvoice, getContractPage, getCrmUserList, getCustomerSimpleList, getInvoice, getSimpleDictDataList, updateInvoice, uploadCrmFile, type CrmContract, type CrmCustomerSimple, type CrmInvoice, type DictData, type StorageFile } from '../../../api/crm'; |
| | | import { createInvoice, getContractPage, getCrmUserList, getCustomerSimpleList, getInvoice, getSimpleDictDataList, updateInvoice, type CrmContract, type CrmCustomerSimple, type CrmInvoice, type DictData } from '../../../api/crm'; |
| | | import type { UserSimple } from '../../../api/purchase'; import { useAuth } from '../../../stores/auth'; |
| | | const { state } = useAuth(); const id=ref<number>(),saving=ref(false),uploading=ref(false),customers=ref<CrmCustomerSimple[]>([]),contracts=ref<CrmContract[]>([]),users=ref<UserSimple[]>([]),dicts=ref<DictData[]>([]),attachments=ref<StorageFile[]>([]); |
| | | const { state } = useAuth(); const id=ref<number>(),saving=ref(false),customers=ref<CrmCustomerSimple[]>([]),contracts=ref<CrmContract[]>([]),users=ref<UserSimple[]>([]),dicts=ref<DictData[]>([]); |
| | | const form=reactive<CrmInvoice>({customerId:undefined,contractId:undefined,ownerUserId:undefined,invoiceType:undefined,invoiceNo:'',invoiceTitle:'',price:undefined,invoiceTime:'',remark:'',blobIds:[]}); |
| | | const invoiceTypes=computed(()=>dicts.value.filter(x=>x.dictType==='crm_invoice_type'&&x.status!==1)); const label=(items:any[],value?:number,key='name')=>items.find(x=>x.id===value)?.[key]||'请选择'; |
| | | function selectCustomer(index:number){const x=customers.value[index];form.customerId=x.id;form.customerName=x.name;form.contractId=undefined;form.contract=undefined;} function selectContract(index:number){const x=contracts.value[index];form.contractId=x.id;form.contract={id:x.id,no:x.no,name:x.name};form.customerId=x.customerId;form.customerName=x.customerName;} |
| | | async function chooseFile(){if(!state.accessToken||uploading.value)return; uni.chooseFile({count:10-(form.blobIds?.length||0),success:async result=>{uploading.value=true;try{for(const file of result.tempFiles||[]){if(!file.path)continue;const saved=await uploadCrmFile(state.accessToken!,file.path);form.blobIds?.push(saved.id);attachments.value.push(saved);}}catch(error){uni.showToast({title:error instanceof Error?error.message:'附件上传失败',icon:'none'});}finally{uploading.value=false;}}});} |
| | | async function save(){if(!state.accessToken||saving.value)return;if(!form.customerId||!form.contractId||!form.ownerUserId||form.invoiceType===undefined||!Number(form.price)||!form.invoiceTime)return uni.showToast({title:'请填写客户、合同、负责人、发票类型、金额和日期',icon:'none'});saving.value=true;try{const data={...form,price:Number(form.price),invoiceTime:`${form.invoiceTime} 00:00:00`};if(id.value)await updateInvoice(state.accessToken,{...data,id:id.value});else await createInvoice(state.accessToken,data);uni.showToast({title:'保存成功',icon:'success'});setTimeout(()=>uni.navigateBack(),500);}catch(error){uni.showToast({title:error instanceof Error?error.message:'保存失败',icon:'none'});}finally{saving.value=false;}} |
| | | onLoad(async query=>{id.value=query.id?Number(query.id):undefined;if(!state.accessToken)return;try{const [customerData,contractData,userData,dictData]=await Promise.all([getCustomerSimpleList(state.accessToken),getContractPage(state.accessToken,{pageNo:1,pageSize:100}),getCrmUserList(state.accessToken),getSimpleDictDataList(state.accessToken)]);customers.value=customerData;contracts.value=contractData.list;users.value=userData;dicts.value=dictData;if(id.value){const data=await getInvoice(state.accessToken,id.value);Object.assign(form,data,{invoiceTime:data.invoiceTime?.slice(0,10)||'',blobIds:data.attachmentList?.map(x=>x.id)||data.blobIds||[]});attachments.value=data.attachmentList||[];}else{form.ownerUserId=state.user?.id;form.ownerUserName=state.user?.nickname;}}catch(error){uni.showToast({title:error instanceof Error?error.message:'数据加载失败',icon:'none'});}}); |
| | | onLoad(async query=>{id.value=query.id?Number(query.id):undefined;if(!state.accessToken)return;try{const [customerData,contractData,userData,dictData]=await Promise.all([getCustomerSimpleList(state.accessToken),getContractPage(state.accessToken,{pageNo:1,pageSize:100}),getCrmUserList(state.accessToken),getSimpleDictDataList(state.accessToken)]);customers.value=customerData;contracts.value=contractData.list;users.value=userData;dicts.value=dictData;if(id.value){const data=await getInvoice(state.accessToken,id.value);Object.assign(form,data,{invoiceTime:data.invoiceTime?.slice(0,10)||'',blobIds:data.attachmentList?.map(x=>x.id)||data.blobIds||[]});}else{form.ownerUserId=state.user?.id;form.ownerUserName=state.user?.nickname;}}catch(error){uni.showToast({title:error instanceof Error?error.message:'数据加载失败',icon:'none'});}}); |
| | | </script> |
| | | <template><view class="page"><view class="card"><view class="section">开票信息</view><view class="field"><text>客户 <text class="required">*</text></text><picker :range="customers" range-key="name" @change="selectCustomer(Number($event.detail.value))"><view class="picker">{{form.customerName||label(customers,form.customerId)}}<text>›</text></view></picker></view><view class="field"><text>合同 <text class="required">*</text></text><picker :range="contracts" range-key="name" @change="selectContract(Number($event.detail.value))"><view class="picker">{{form.contract?.name||form.contract?.no||label(contracts,form.contractId)}}<text>›</text></view></picker></view><view class="field"><text>负责人 <text class="required">*</text></text><picker :disabled="Boolean(id)" :range="users" range-key="nickname" @change="form.ownerUserId=users[Number($event.detail.value)].id;form.ownerUserName=users[Number($event.detail.value)].nickname"><view class="picker">{{form.ownerUserName||label(users,form.ownerUserId,'nickname')}}<text>›</text></view></picker></view><view class="field"><text>发票类型 <text class="required">*</text></text><picker :range="invoiceTypes" range-key="label" @change="form.invoiceType=Number(invoiceTypes[Number($event.detail.value)].value)"><view class="picker">{{invoiceTypes.find(x=>Number(x.value)===form.invoiceType)?.label||'请选择发票类型'}}<text>›</text></view></picker></view><view class="field"><text>发票号码</text><input v-model="form.invoiceNo" placeholder="请输入发票号码"/></view><view class="field"><text>发票抬头</text><input v-model="form.invoiceTitle" placeholder="请输入发票抬头"/></view><view class="field"><text>开票金额 <text class="required">*</text></text><input :value="form.price===undefined?'':String(form.price)" type="digit" placeholder="请输入开票金额" @input="form.price=Number($event.detail.value||0)"/></view><view class="field"><text>开票日期 <text class="required">*</text></text><picker mode="date" :value="form.invoiceTime" @change="form.invoiceTime=$event.detail.value"><view class="picker">{{form.invoiceTime||'请选择日期'}}<text>›</text></view></picker></view><view class="field column"><text>发票附件</text><button class="upload" :loading="uploading" @click="chooseFile">添加附件</button><text v-for="(file,index) in attachments" :key="file.id" class="file">{{file.name||`附件 ${index+1}`}} <text @click="form.blobIds?.splice(index,1);attachments.splice(index,1)">删除</text></text></view><view class="field column"><text>备注</text><textarea v-model="form.remark" placeholder="请输入备注"></textarea></view></view><button class="save" :loading="saving" :disabled="saving" @click="save">保存</button></view></template> |
| | | <template><view class="page"><view class="card"><view class="section">开票信息</view><view class="field"><text>客户 <text class="required">*</text></text><picker :range="customers" range-key="name" @change="selectCustomer(Number($event.detail.value))"><view class="picker">{{form.customerName||label(customers,form.customerId)}}<text>›</text></view></picker></view><view class="field"><text>合同 <text class="required">*</text></text><picker :range="contracts" range-key="name" @change="selectContract(Number($event.detail.value))"><view class="picker">{{form.contract?.name||form.contract?.no||label(contracts,form.contractId)}}<text>›</text></view></picker></view><view class="field"><text>负责人 <text class="required">*</text></text><picker :disabled="Boolean(id)" :range="users" range-key="nickname" @change="form.ownerUserId=users[Number($event.detail.value)].id;form.ownerUserName=users[Number($event.detail.value)].nickname"><view class="picker">{{form.ownerUserName||label(users,form.ownerUserId,'nickname')}}<text>›</text></view></picker></view><view class="field"><text>发票类型 <text class="required">*</text></text><picker :range="invoiceTypes" range-key="label" @change="form.invoiceType=Number(invoiceTypes[Number($event.detail.value)].value)"><view class="picker">{{invoiceTypes.find(x=>Number(x.value)===form.invoiceType)?.label||'请选择发票类型'}}<text>›</text></view></picker></view><view class="field"><text>发票号码</text><input v-model="form.invoiceNo" placeholder="请输入发票号码"/></view><view class="field"><text>发票抬头</text><input v-model="form.invoiceTitle" placeholder="请输入发票抬头"/></view><view class="field"><text>开票金额 <text class="required">*</text></text><input :value="form.price===undefined?'':String(form.price)" type="digit" placeholder="请输入开票金额" @input="form.price=Number($event.detail.value||0)"/></view><view class="field"><text>开票日期 <text class="required">*</text></text><picker mode="date" :value="form.invoiceTime" @change="form.invoiceTime=$event.detail.value"><view class="picker">{{form.invoiceTime||'请选择日期'}}<text>›</text></view></picker></view><view class="field column"><text>备注</text><textarea v-model="form.remark" placeholder="请输入备注"></textarea></view></view><button class="save" :loading="saving" :disabled="saving" @click="save">保存</button></view></template> |
| | | <style lang="scss" scoped>.page{min-height:100vh;padding:24rpx 30rpx 70rpx;background:#f6f7f9}.card{overflow:hidden;background:#fff;border-radius:16rpx}.section{padding:22rpx 24rpx 12rpx;color:var(--color-primary, #1769d8);font-size:25rpx;font-weight:600;background:#f8fbff}.field{display:flex;align-items:center;min-height:96rpx;padding:0 24rpx;gap:20rpx;border-bottom:1rpx solid #eef1f5}.field>text:first-child{width:190rpx;color:#33455d;font-size:27rpx}.required{color:#e34b4b}.field input,.field picker,.picker{flex:1;color:#1e3049;font-size:27rpx}.picker{display:flex;justify-content:space-between;color:#65758a}.picker text{color:#8092a7;font-size:36rpx}.column{align-items:flex-start;flex-direction:column;padding:20rpx 24rpx}.column>text:first-child{width:auto}.column textarea{width:100%;height:130rpx;font-size:27rpx}.upload{margin:0;color:var(--color-primary, #1769d8);font-size:24rpx;background:var(--color-primary-soft, #f5f8ff);border:1rpx solid var(--color-primary-border, #8ab3ee)}.file{width:100%;color:#65758a!important;font-size:24rpx!important}.file text{float:right;color:#c65050}.save{margin-top:32rpx;color:#fff;background:var(--color-primary, #1769d8);border-radius:12rpx;font-size:30rpx}.save::after,.upload::after{border:0}</style> |
| | |
| | | const returnTypeLabel = () => returnTypes.value.find(item => Number(item.value) === form.returnType)?.label || "请选择回款方式"; |
| | | async function loadRelated() { if (!state.accessToken || !form.contractId) { invoices.value = []; plans.value = []; return; } const [invoiceData, planData] = await Promise.all([getInvoiceSimpleList(state.accessToken, form.contractId), form.customerId ? getReceivablePlanSimpleList(state.accessToken, form.customerId, form.contractId) : Promise.resolve([])]); invoices.value = invoiceData; plans.value = planData; } |
| | | function selectCustomer(index: number) { const customer = customers.value[index]; form.customerId = customer.id; form.customerName = customer.name; form.contractId = undefined; form.contractNo = undefined; form.invoiceId = undefined; form.planId = undefined; form.returnTime = ""; form.returnType = undefined; form.price = undefined; invoices.value = []; plans.value = []; } |
| | | async function selectContract(index: number) { const contract = availableContracts.value[index]; form.contractId = contract.id; form.contractNo = contract.no || contract.name; form.customerId = contract.customerId; form.customerName = contract.customerName; form.invoiceId = undefined; form.planId = undefined; form.returnTime = ""; form.returnType = undefined; form.price = undefined; await loadRelated(); uni.showToast({ title: "请先完成开票并上传发票附件,再进行回款操作", icon: "none", duration: 2400 }); } |
| | | async function selectContract(index: number) { const contract = availableContracts.value[index]; form.contractId = contract.id; form.contractNo = contract.no || contract.name; form.customerId = contract.customerId; form.customerName = contract.customerName; form.invoiceId = undefined; form.planId = undefined; form.returnTime = ""; form.returnType = undefined; form.price = undefined; await loadRelated(); uni.showToast({ title: "请先完成开票,再进行回款操作", icon: "none", duration: 2400 }); } |
| | | function selectPlan(index: number) { const plan = plans.value[index]; form.planId = plan.id; form.returnTime = plan.returnTime?.slice(0, 10) || ""; form.returnType = plan.returnType; form.price = plan.price; } |
| | | async function save() { if (!state.accessToken || saving.value || !editable.value) return; if (!form.customerId || !form.contractId || !form.invoiceId || !form.ownerUserId || !form.returnTime || form.returnType === undefined || !Number(form.price)) return uni.showToast({ title: "请填写客户、合同、关联发票、负责人、回款日期、方式和金额", icon: "none" }); saving.value = true; try { const data = { ...form, returnTime: `${form.returnTime} 00:00:00`, price: Number(form.price) }; if (id.value) await updateReceivable(state.accessToken, { ...data, id: id.value }); else await createReceivable(state.accessToken, data); uni.showToast({ title: "保存成功", icon: "success" }); setTimeout(() => uni.navigateBack(), 500); } catch (error) { uni.showToast({ title: error instanceof Error ? error.message : "保存失败", icon: "none" }); } finally { saving.value = false; } } |
| | | onLoad(async query => { id.value = query.id ? Number(query.id) : undefined; const contractId = query.contractId ? Number(query.contractId) : undefined; const customerId = query.customerId ? Number(query.customerId) : undefined; const planId = query.planId ? Number(query.planId) : undefined; if (!state.accessToken) return; try { const [customerData, contractData, userData, dictData] = await Promise.all([getCustomerSimpleList(state.accessToken), getContractPage(state.accessToken, { pageNo: 1, pageSize: 100 }), getCrmUserList(state.accessToken), getSimpleDictDataList(state.accessToken)]); customers.value = customerData; contracts.value = contractData.list; users.value = userData; dicts.value = dictData; if (id.value) { const data = await getReceivable(state.accessToken, id.value); Object.assign(form, data, { returnTime: data.returnTime?.slice(0, 10) || "" }); await loadRelated(); } else { form.customerId = customerId; form.customerName = customers.value.find(item => item.id === customerId)?.name; form.ownerUserId = state.user?.id; form.ownerUserName = state.user?.nickname; if (contractId) { const contract = contracts.value.find(item => item.id === contractId) || await getContract(state.accessToken, contractId); if (!contracts.value.some(item => item.id === contract.id)) contracts.value.push(contract); form.contractId = contract.id; form.contractNo = contract.no || contract.name; form.customerId = contract.customerId; form.customerName = contract.customerName; await loadRelated(); } if (planId && form.customerId && form.contractId) { const plan = plans.value.find(item => item.id === planId); if (plan) selectPlan(plans.value.indexOf(plan)); } } } catch (error) { uni.showToast({ title: error instanceof Error ? error.message : "数据加载失败", icon: "none" }); } }); |
| | |
| | | <script setup lang="ts"> |
| | | import { ref } from "vue"; |
| | | import { chooseHrmFiles, uploadHrmFile, type HrmAttachment } from "../../api/hrm"; |
| | | |
| | | const props = withDefaults(defineProps<{ |
| | | import type { HrmAttachment } from "../../api/hrm"; |
| | | withDefaults(defineProps<{ |
| | | modelValue?: number[]; |
| | | files?: HrmAttachment[]; |
| | | token?: string | null; |
| | | readonly?: boolean; |
| | | }>(), { modelValue: () => [], files: () => [], token: null, readonly: false }); |
| | | const emit = defineEmits<{ |
| | | defineEmits<{ |
| | | "update:modelValue": [value: number[]]; |
| | | "update:files": [value: HrmAttachment[]]; |
| | | }>(); |
| | | const uploading = ref(false); |
| | | |
| | | async function addFiles() { |
| | | if (!props.token || uploading.value || props.readonly) return; |
| | | const remaining = 10 - props.modelValue.length; |
| | | if (remaining <= 0) return uni.showToast({ title: "最多上传 10 个附件", icon: "none" }); |
| | | try { |
| | | const picked = await chooseHrmFiles(remaining); |
| | | if (!picked.length) return; |
| | | uploading.value = true; |
| | | const ids = [...props.modelValue]; |
| | | const files = [...props.files]; |
| | | for (const item of picked) { |
| | | const uploaded = await uploadHrmFile(props.token, item.path); |
| | | ids.push(uploaded.id); |
| | | files.push({ ...uploaded, name: uploaded.name || item.name }); |
| | | } |
| | | emit("update:modelValue", ids); |
| | | emit("update:files", files); |
| | | } catch (error) { |
| | | const message = error instanceof Error ? error.message : "附件上传失败"; |
| | | if (!String(message).includes("cancel")) uni.showToast({ title: message, icon: "none" }); |
| | | } finally { |
| | | uploading.value = false; |
| | | } |
| | | } |
| | | |
| | | function remove(index: number) { |
| | | emit("update:modelValue", props.modelValue.filter((_, itemIndex) => itemIndex !== index)); |
| | | emit("update:files", props.files.filter((_, itemIndex) => itemIndex !== index)); |
| | | } |
| | | </script> |
| | | |
| | | <template> |
| | | <view class="attachment-field"> |
| | | <view v-if="files.length" class="attachment-field"> |
| | | <view class="attachment-head"> |
| | | <text>附件</text> |
| | | <button v-if="!readonly" :loading="uploading" :disabled="uploading||modelValue.length>=10" @click="addFiles">添加附件</button> |
| | | |
| | | </view> |
| | | <view v-if="files.length" class="attachment-list"> |
| | | <view v-for="(file,index) in files" :key="file.id" class="attachment-item"> |
| | | <text>{{ file.name || `附件 ${index + 1}` }}</text> |
| | | <text v-if="!readonly" class="remove" @click="remove(index)">删除</text> |
| | | |
| | | </view> |
| | | </view> |
| | | <text v-else class="empty">暂无附件</text> |
| | | |
| | | </view> |
| | | </template> |
| | | |
| | |
| | | import { getSimpleDictDataList, type DictData } from "../../../../api/crm"; |
| | | import { |
| | | archivePdProject, |
| | | choosePdFile, |
| | | createPdDocument, |
| | | deletePdDocument, |
| | | getPdDocumentAuditPage, |
| | |
| | | submitPdProjectApproval, |
| | | updatePdDocument, |
| | | updatePdDocumentAudit, |
| | | uploadPdFile, |
| | | uploadPdProject, |
| | | type PdApproveProcess, |
| | | type PdDocument, |
| | | type PdDocumentAudit, |
| | | type PdProject, |
| | | type PickedFile, |
| | | } from "../../../../api/product-design"; |
| | | import { useAuth } from "../../../../stores/auth"; |
| | | import PickerSheet from "../../process-design/components/PickerSheet.vue"; |
| | |
| | | |
| | | function onDocTypePicked(keys: Array<number | string>) { |
| | | docForm.value.documentType = keys[0] === undefined || keys[0] === "" ? undefined : String(keys[0]); |
| | | } |
| | | |
| | | /** 选择文件并上传:走 /system/storage-blob/upload,取回 id 作为 blobIds(三端兼容) */ |
| | | async function chooseFile() { |
| | | if (!state.accessToken) return; |
| | | let files: PickedFile[] = []; |
| | | try { |
| | | files = await choosePdFile(9); |
| | | } catch (error) { |
| | | // 用户主动取消不提示;真失败才提示 |
| | | const msg = error instanceof Error ? error.message : ""; |
| | | if (msg && !/cancel/i.test(msg)) toast(msg); |
| | | return; |
| | | } |
| | | if (!files.length) return; |
| | | uni.showLoading({ title: "上传中…", mask: true }); |
| | | try { |
| | | for (const file of files) { |
| | | const attachment = await uploadPdFile(state.accessToken, file.path); |
| | | if (attachment.id) { |
| | | docForm.value.blobIds = [...(docForm.value.blobIds ?? []), attachment.id]; |
| | | docAttachments.value = [ |
| | | ...docAttachments.value, |
| | | { id: attachment.id, name: attachment.name || file.name, url: attachment.url }, |
| | | ]; |
| | | } |
| | | } |
| | | toast("上传成功"); |
| | | } catch (error) { |
| | | toast(error instanceof Error ? error.message : "上传失败"); |
| | | } finally { |
| | | uni.hideLoading(); |
| | | } |
| | | } |
| | | |
| | | function removeAttachment(index: number) { |
| | | const target = docAttachments.value[index]; |
| | | docAttachments.value = docAttachments.value.filter((_, i) => i !== index); |
| | | if (target?.id) { |
| | | docForm.value.blobIds = (docForm.value.blobIds ?? []).filter(id => id !== target.id); |
| | | } |
| | | } |
| | | |
| | | async function submitDoc() { |
| | |
| | | <view class="field"> |
| | | <text class="label required">版本</text> |
| | | <input v-model="docForm.documentVersion" placeholder="请输入版本" /> |
| | | </view> |
| | | <view class="field column"> |
| | | <text class="label">附件</text> |
| | | <view class="files"> |
| | | <view v-for="(file, index) in docAttachments" :key="file.id ?? index" class="file"> |
| | | <text class="file-name">{{ file.name || `附件${index + 1}` }}</text> |
| | | <text class="file-del" @click="removeAttachment(index)">移除</text> |
| | | </view> |
| | | <text v-if="!docAttachments.length" class="file-empty">暂未上传附件</text> |
| | | </view> |
| | | <button class="upload" @click="chooseFile">选择文件上传</button> |
| | | </view> |
| | | <view class="field column"> |
| | | <text class="label">备注</text> |
| | |
| | | import { getMaterialPage, type MaterialItem } from "../../../api/basic-data"; |
| | | import { |
| | | getSupplierSimpleList, |
| | | uploadCertificateFile, |
| | | type StorageAttachment, |
| | | type SupplierItem, |
| | | } from "../../../api/supplier"; |
| | | import { useAuth } from "../../../stores/auth"; |
| | |
| | | ); |
| | | form.totalPrice = total; |
| | | form.paymentPrice = total - (form.discountPrice || 0); |
| | | } |
| | | function chooseAttachment() { |
| | | if (!state.accessToken) return; |
| | | uni.chooseFile({ |
| | | count: 1, |
| | | success: async result => { |
| | | const file = result.tempFiles?.[0]; |
| | | if (!file?.path) return; |
| | | try { |
| | | const attachment = await uploadCertificateFile( |
| | | state.accessToken!, |
| | | file.path |
| | | ); |
| | | form.blobIds = [...(form.blobIds || []), attachment.id]; |
| | | form.attachmentList = [...(form.attachmentList || []), attachment]; |
| | | uni.showToast({ title: "上传成功", icon: "success" }); |
| | | } catch (error) { |
| | | uni.showToast({ title: message(error), icon: "none" }); |
| | | } |
| | | }, |
| | | }); |
| | | } |
| | | function removeAttachment(index: number) { |
| | | form.blobIds?.splice(index, 1); |
| | | form.attachmentList?.splice(index, 1); |
| | | } |
| | | function previewAttachment(attachment: StorageAttachment) { |
| | | if (attachment.url) uni.previewImage({ urls: [attachment.url] }); |
| | | else uni.showToast({ title: "附件地址不可用", icon: "none" }); |
| | | } |
| | | async function load() { |
| | | if (!state.accessToken) return; |
| | |
| | | class="summary"><text>合计</text> |
| | | <view><text>数量:{{ (form.items || []).reduce((sum, item) => sum + (item.count || 0), 0).toFixed(3) }}</text><text>金额:¥ {{ (form.items || []).reduce((sum, item) => sum + (kind === 'request' ? requestItemAmount(item) : orderItemAmounts(item).totalAmount), 0).toFixed(2) }}</text></view> |
| | | </view> |
| | | </view> |
| | | <view class="card attachments"> |
| | | <view class="section"><text>附件</text><button @click="chooseAttachment">上传附件</button></view> |
| | | <view v-if="form.attachmentList?.length"> |
| | | <view v-for="(attachment, index) in form.attachmentList" |
| | | :key="attachment.id" |
| | | class="attachment"><text @click="previewAttachment(attachment)">{{ attachment.name || `附件 #${attachment.id}` }}</text><text class="danger" |
| | | @click="removeAttachment(index)">删除</text></view> |
| | | </view><text v-else |
| | | class="empty">暂无附件</text> |
| | | </view> |
| | | <button class="save" |
| | | :loading="saving" |
| | |
| | | getCustomerSimpleList, |
| | | getMaterialSimpleList, |
| | | } from "../../../api/crm"; |
| | | import type { StorageFile } from "../../../api/crm"; |
| | | import { uploadCertificateFile } from "../../../api/supplier"; |
| | | import { useAuth } from "../../../stores/auth"; |
| | | import { materialOptionText } from "../../../utils/material"; |
| | | |
| | |
| | | form.items?.splice(index, 1); |
| | | recalc(); |
| | | } |
| | | function chooseAttachment() { |
| | | if (!state.accessToken) return; |
| | | uni.chooseFile({ |
| | | count: 1, |
| | | success: async result => { |
| | | const file = result.tempFiles?.[0]; |
| | | if (!file?.path) return; |
| | | try { |
| | | const attachment = await uploadCertificateFile(state.accessToken!, file.path); |
| | | form.blobIds = [...(form.blobIds || []), attachment.id]; |
| | | form.attachmentList = [...(form.attachmentList || []), attachment as StorageFile]; |
| | | uni.showToast({ title: "上传成功", icon: "success" }); |
| | | } catch (e) { |
| | | uni.showToast({ title: e instanceof Error ? e.message : "上传失败", icon: "none" }); |
| | | } |
| | | }, |
| | | }); |
| | | } |
| | | function removeAttachment(index: number) { |
| | | form.blobIds?.splice(index, 1); |
| | | form.attachmentList?.splice(index, 1); |
| | | } |
| | | async function save() { |
| | | if (!state.accessToken || saving.value) return; |
| | | if (!editable.value) |
| | |
| | | @change="selectUser"><view class="picker">{{ userLabel() }}<text>›</text></view></picker></view> |
| | | <view class="field area"><text>备注</text><textarea v-model="form.remark" |
| | | placeholder="请输入备注" ></textarea></view> |
| | | <view class="field column"><text>附件</text><view class="attachments"><button @click="chooseAttachment">上传附件</button><text v-for="(file, index) in form.attachmentList" |
| | | :key="file.id" |
| | | @click="removeAttachment(index)">{{ file.name || `附件 #${file.id}` }} ×</text></view></view> |
| | | </view> |
| | | <view class="card"> |
| | | <view class="sectionHead"><text>销售产品清单</text><button @click="addItem">添加销售产品</button></view> |
| | |
| | | getTenderProject, |
| | | updateTenderProject, |
| | | updateTenderProjectInfo, |
| | | uploadTenderFile, |
| | | type StorageAttachment, |
| | | type TenderProject, |
| | | } from "../../../api/srm"; |
| | | import { useAuth } from "../../../stores/auth"; |
| | |
| | | } |
| | | const partialEditable = () => |
| | | Boolean(id.value && [1, 2].includes(form.tenderStatus || 0)); |
| | | async function chooseAttachment() { |
| | | if (!state.accessToken) return; |
| | | uni.chooseFile({ |
| | | count: 1, |
| | | extension: ["jpg", "jpeg", "png", "pdf", "doc", "docx"], |
| | | success: async result => { |
| | | const file = result.tempFiles?.[0]; |
| | | if (!file?.path) return; |
| | | try { |
| | | const attachment = await uploadTenderFile( |
| | | state.accessToken!, |
| | | file.path |
| | | ); |
| | | form.blobIds = [...(form.blobIds || []), attachment.id]; |
| | | form.attachmentList = [...(form.attachmentList || []), attachment]; |
| | | uni.showToast({ title: "上传成功", icon: "success" }); |
| | | } catch (error) { |
| | | uni.showToast({ |
| | | title: error instanceof Error ? error.message : "上传失败", |
| | | icon: "none", |
| | | }); |
| | | } |
| | | }, |
| | | }); |
| | | } |
| | | function removeAttachment(index: number) { |
| | | form.blobIds?.splice(index, 1); |
| | | form.attachmentList?.splice(index, 1); |
| | | } |
| | | function previewAttachment(item: StorageAttachment) { |
| | | const url = item.previewURL || item.url; |
| | | if (url) uni.previewImage({ urls: [url] }); |
| | | else uni.showToast({ title: "附件地址不可用", icon: "none" }); |
| | | } |
| | | async function save() { |
| | | if (!form.tenderNo.trim() || !form.tenderName.trim()) |
| | | return uni.showToast({ title: "请填写招标编号和名称", icon: "none" }); |
| | |
| | | </template> |
| | | <view class="field column"><text>资质要求</text><textarea v-model="form.qualificationRequirements" |
| | | placeholder="请输入资质要求" ></textarea></view> |
| | | <view class="attachments"> |
| | | <view class="attachmentHead"><text>资质要求附件</text><text @click="chooseAttachment">上传附件</text></view> |
| | | <view v-for="(item,index) in form.attachmentList" |
| | | :key="item.id" |
| | | class="attachment"><text @click="previewAttachment(item)">{{item.name||`附件${index+1}`}}</text><text @click="removeAttachment(index)">删除</text></view> |
| | | </view> |
| | | </view> |
| | | <button class="save" |
| | | :loading="saving" |
| | |
| | | reviewQuality, |
| | | submitApply, |
| | | updateApply, |
| | | uploadCertificateFile, |
| | | type StorageAttachment, |
| | | type SupplierApplyItem, |
| | | type SupplierItem, |
| | | } from "../../../api/supplier"; |
| | |
| | | address: supplier.address || "", |
| | | taxNo: supplier.taxNo || "", |
| | | }); |
| | | } |
| | | async function chooseAttachment() { |
| | | if (!state.accessToken) return; |
| | | uni.chooseFile({ |
| | | count: 1, |
| | | extension: ["jpg", "jpeg", "png", "pdf", "doc", "docx"], |
| | | success: async result => { |
| | | const file = result.tempFiles?.[0]; |
| | | if (!file?.path) return; |
| | | try { |
| | | const attachment = await uploadCertificateFile( |
| | | state.accessToken!, |
| | | file.path |
| | | ); |
| | | form.blobIds = [...(form.blobIds || []), attachment.id]; |
| | | form.attachmentList = [...(form.attachmentList || []), attachment]; |
| | | uni.showToast({ title: "上传成功", icon: "success" }); |
| | | } catch (e) { |
| | | uni.showToast({ |
| | | title: e instanceof Error ? e.message : "上传失败", |
| | | icon: "none", |
| | | }); |
| | | } |
| | | }, |
| | | }); |
| | | } |
| | | function removeAttachment(index: number) { |
| | | form.blobIds?.splice(index, 1); |
| | | form.attachmentList?.splice(index, 1); |
| | | } |
| | | function previewAttachment(item: StorageAttachment) { |
| | | const url = item.previewURL || item.url; |
| | | if (url) uni.previewImage({ urls: [url] }); |
| | | else uni.showToast({ title: "附件地址不可用", icon: "none" }); |
| | | } |
| | | async function save() { |
| | | if (!form.supplierId && !form.supplierName.trim()) |
| | |
| | | placeholder="税号" /></view> |
| | | <view class="area"><text>备注</text><textarea v-model="form.remark" |
| | | placeholder="备注" ></textarea></view> |
| | | <view class="attachments"> |
| | | <view class="attachmentHead"><text>准入资料</text><text @click="chooseAttachment">上传附件</text></view> |
| | | <view v-for="(item,index) in form.attachmentList" |
| | | :key="item.id" |
| | | class="attachment"><text @click="previewAttachment(item)">{{item.name||`附件${index+1}`}}</text><text @click="removeAttachment(index)">删除</text></view> |
| | | </view><button class="save" |
| | | <button class="save" |
| | | @click="save">保存草稿</button> |
| | | </scroll-view></view> |
| | | <view v-if="reviewOpen" |
| | |
| | | getExpiringCertificates, |
| | | getSupplierSimpleList, |
| | | updateCertificate, |
| | | uploadCertificateFile, |
| | | type SupplierCertificateItem, |
| | | type SupplierItem, |
| | | type StorageAttachment, |
| | | } from "../../../api/supplier"; |
| | | import { useAuth } from "../../../stores/auth"; |
| | | import { statusToneClass } from "../../../utils/status"; |
| | |
| | | }); |
| | | } |
| | | } |
| | | function chooseAttachment() { |
| | | if (!state.accessToken) return; |
| | | uni.chooseFile({ |
| | | count: 1, |
| | | success: async result => { |
| | | const file = result.tempFiles?.[0]; |
| | | if (!file?.path) return; |
| | | try { |
| | | const attachment = await uploadCertificateFile( |
| | | state.accessToken!, |
| | | file.path |
| | | ); |
| | | form.blobIds = [...(form.blobIds || []), attachment.id]; |
| | | form.attachmentList = [...(form.attachmentList || []), attachment]; |
| | | uni.showToast({ title: "上传成功", icon: "success" }); |
| | | } catch (e) { |
| | | uni.showToast({ |
| | | title: e instanceof Error ? e.message : "上传失败", |
| | | icon: "none", |
| | | }); |
| | | } |
| | | }, |
| | | }); |
| | | } |
| | | function removeAttachment(index: number) { |
| | | form.blobIds?.splice(index, 1); |
| | | form.attachmentList?.splice(index, 1); |
| | | } |
| | | function previewAttachment(item: StorageAttachment) { |
| | | if (item.url) uni.previewImage({ urls: [item.url] }); |
| | | else uni.showToast({ title: "附件地址不可用", icon: "none" }); |
| | | } |
| | | onShow(() => load(true)); |
| | | onPullDownRefresh(async () => { |
| | | await load(true); |
| | |
| | | </view> |
| | | <view class="area"><text>备注</text><textarea v-model="form.remark" |
| | | placeholder="备注" ></textarea></view> |
| | | <view class="attachments"> |
| | | <view class="attachmentHead"><text>证书附件</text><text @click="chooseAttachment">上传附件</text></view> |
| | | <view v-for="(item,index) in form.attachmentList" |
| | | :key="item.id" |
| | | class="attachment"><text @click="previewAttachment(item)">{{item.name||`附件${index+1}`}}</text><text @click="removeAttachment(index)">删除</text></view> |
| | | </view><button class="save" |
| | | <button class="save" |
| | | @click="save">保存</button> |
| | | </scroll-view></view> |
| | | </view> |