| | |
| | | <template> |
| | | <view class="account-detail"> |
| | | <view class="shipment-page"> |
| | | <PageHeader title="发货" |
| | | @back="goBack" /> |
| | | <!-- 表单区域 --> |
| | | <u-form ref="formRef" |
| | | @submit="submitForm" |
| | | :rules="rules" |
| | | :model="form" |
| | | label-width="140rpx"> |
| | | <u-form-item prop="typeValue" |
| | | label="发货类型" |
| | | required> |
| | | <u-input v-model="typeValue" |
| | | readonly |
| | | placeholder="请选择发货方式" |
| | | @click="showPicker = true" /> |
| | | <template #right> |
| | | <up-icon name="arrow-right" |
| | | @click="showPicker = true"></up-icon> |
| | | </template> |
| | | </u-form-item> |
| | | </u-form> |
| | | <!-- 选择器弹窗 --> |
| | | <up-action-sheet :show="showPicker" |
| | | :actions="productOptions" |
| | | title="发货方式" |
| | | @select="onConfirm" |
| | | @close="showPicker = false" /> |
| | | <!-- 审核流程区域 --> |
| | | <view class="approval-process"> |
| | | <view class="approval-header"> |
| | | <text class="approval-title">审核流程</text> |
| | | <text class="approval-desc">每个步骤只能选择一个审批人</text> |
| | | </view> |
| | | <view class="approval-steps"> |
| | | <view v-for="(step, stepIndex) in approverNodes" |
| | | :key="stepIndex" |
| | | class="approval-step"> |
| | | <view class="step-dot"></view> |
| | | <view class="step-title"> |
| | | <text>审批人</text> |
| | | <view class="form-container"> |
| | | <up-form ref="formRef" |
| | | :model="form" |
| | | :rules="rules" |
| | | label-width="100" |
| | | input-align="right" |
| | | error-message-align="right"> |
| | | <!-- 基本信息 --> |
| | | <u-cell-group title="基本信息" |
| | | class="form-section"> |
| | | <up-form-item label="发货方式" |
| | | prop="type" |
| | | required> |
| | | <up-input v-model="form.type" |
| | | readonly |
| | | placeholder="请选择发货方式" |
| | | @click="showTypePicker = true" /> |
| | | <template #right> |
| | | <up-icon name="arrow-right" |
| | | @click="showTypePicker = true"></up-icon> |
| | | </template> |
| | | </up-form-item> |
| | | <block v-if="form.type === '货车'"> |
| | | <up-form-item label="发货车牌" |
| | | prop="shippingCarNumber" |
| | | required> |
| | | <up-input v-model="form.shippingCarNumber" |
| | | placeholder="请输入发货车牌号" |
| | | clearable /> |
| | | </up-form-item> |
| | | </block> |
| | | <block v-if="form.type === '快递'"> |
| | | <up-form-item label="快递公司" |
| | | prop="expressCompany" |
| | | required> |
| | | <up-input v-model="form.expressCompany" |
| | | placeholder="请输入快递公司" |
| | | clearable /> |
| | | </up-form-item> |
| | | <up-form-item label="快递单号" |
| | | prop="expressNumber" |
| | | required> |
| | | <up-input v-model="form.expressNumber" |
| | | placeholder="请输入快递单号" |
| | | clearable /> |
| | | </up-form-item> |
| | | </block> |
| | | </u-cell-group> |
| | | <!-- 批次选择 --> |
| | | <u-cell-group title="批次选择" |
| | | class="form-section"> |
| | | <view class="section-header-info"> |
| | | <text class="subtitle">待发货数量: {{ goOutData.noQuantity || 0 }}</text> |
| | | </view> |
| | | <view class="approver-container"> |
| | | <view v-if="step.nickName" |
| | | class="approver-item"> |
| | | <view class="approver-avatar"> |
| | | <text class="avatar-text">{{ step.nickName.charAt(0) }}</text> |
| | | <view class="status-dot"></view> |
| | | <view v-if="batchList.length === 0" |
| | | class="empty-text"> |
| | | <text>暂无可用库存批次</text> |
| | | </view> |
| | | <view v-else |
| | | class="batch-list"> |
| | | <view v-for="(item, index) in batchList" |
| | | :key="index" |
| | | class="batch-card"> |
| | | <view class="batch-header"> |
| | | <text class="batch-no">批号: {{ item.batchNo }}</text> |
| | | <text class="batch-qty">库存: {{ getAvailableQty(item) }}</text> |
| | | </view> |
| | | <view class="approver-info"> |
| | | <text class="approver-name">{{ step.nickName }}</text> |
| | | <up-divider></up-divider> |
| | | <view class="batch-body"> |
| | | <up-form-item label="发货数量"> |
| | | <up-input v-model="item.deliveryQuantity" |
| | | type="digit" |
| | | placeholder="0.00" |
| | | input-align="right" |
| | | @blur="onBatchQtyChange(item)" /> |
| | | </up-form-item> |
| | | </view> |
| | | <view class="delete-approver-btn" |
| | | @click="removeApprover(stepIndex)">×</view> |
| | | </view> |
| | | <view v-else |
| | | class="add-approver-btn" |
| | | @click="addApprover(stepIndex)"> |
| | | <view class="add-circle">+</view> |
| | | <text class="add-label">选择审批人</text> |
| | | </view> |
| | | </view> |
| | | <view class="step-line" |
| | | v-if="stepIndex < approverNodes.length - 1"></view> |
| | | <view class="delete-step-btn" |
| | | v-if="approverNodes.length > 1" |
| | | @click="removeApprovalStep(stepIndex)">删除节点</view> |
| | | </view> |
| | | </view> |
| | | <view class="add-step-btn"> |
| | | <u-button icon="plus" |
| | | plain |
| | | type="primary" |
| | | style="width: 100%" |
| | | @click="addApprovalStep">新增节点</u-button> |
| | | </view> |
| | | </u-cell-group> |
| | | <!-- 发货图片 --> |
| | | <u-cell-group title="发货图片" |
| | | class="form-section"> |
| | | <view class="upload-container"> |
| | | <up-upload :fileList="fileList" |
| | | @afterRead="afterRead" |
| | | @delete="deleteFile" |
| | | multiple |
| | | :maxCount="9" |
| | | width="160rpx" |
| | | height="160rpx" /> |
| | | </view> |
| | | </u-cell-group> |
| | | </up-form> |
| | | </view> |
| | | <!-- 底部按钮 --> |
| | | <view class="footer-btns"> |
| | | <u-button class="cancel-btn" |
| | | @click="goBack">取消</u-button> |
| | | <u-button class="save-btn" |
| | | @click="submitForm">发货</u-button> |
| | | </view> |
| | | <FooterButtons confirmText="确认发货" |
| | | @cancel="goBack" |
| | | @confirm="submitForm" /> |
| | | <!-- 发货方式选择器 --> |
| | | <up-action-sheet :show="showTypePicker" |
| | | :actions="typeActions" |
| | | title="发货方式" |
| | | @select="onTypeSelect" |
| | | @close="showTypePicker = false" /> |
| | | </view> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { ref, onMounted, onUnmounted, reactive, toRefs } from "vue"; |
| | | import config from "@/config"; |
| | | import { ref, onMounted, reactive } from "vue"; |
| | | import PageHeader from "@/components/PageHeader.vue"; |
| | | import FooterButtons from "@/components/FooterButtons.vue"; |
| | | import { addShippingInfo } from "@/api/salesManagement/salesLedger"; |
| | | const showToast = message => { |
| | | uni.showToast({ |
| | | title: message, |
| | | icon: "none", |
| | | }); |
| | | }; |
| | | import { userListNoPageByTenantId } from "@/api/system/user"; |
| | | import { getStockInventoryByModelId } from "@/api/inventoryManagement/stockInventory"; |
| | | import { getToken } from "@/utils/auth"; |
| | | |
| | | const data = reactive({ |
| | | form: { |
| | | approveTime: "", |
| | | approveId: "", |
| | | approveUser: "", |
| | | approveUserName: "", |
| | | approveDeptName: "", |
| | | approveDeptId: "", |
| | | approveReason: "", |
| | | checkResult: "", |
| | | tempFileIds: [], |
| | | approverList: [], // 新增字段,存储所有节点的审批人id |
| | | startDate: "", |
| | | endDate: "", |
| | | location: "", |
| | | price: "", |
| | | }, |
| | | rules: { |
| | | typeValue: [{ required: false, message: "请选择", trigger: "change" }], |
| | | }, |
| | | }); |
| | | const { form, rules } = toRefs(data); |
| | | const showPicker = ref(false); |
| | | const productOptions = ref([ |
| | | { |
| | | value: "货车", |
| | | name: "货车", |
| | | }, |
| | | { |
| | | value: "快递", |
| | | name: "快递", |
| | | }, |
| | | ]); |
| | | const operationType = ref(""); |
| | | const currentApproveStatus = ref(""); |
| | | const approverNodes = ref([]); |
| | | const userList = ref([]); |
| | | const formRef = ref(null); |
| | | const approveType = ref(0); |
| | | const goOutData = ref({}); |
| | | const batchList = ref([]); |
| | | const fileList = ref([]); |
| | | const showTypePicker = ref(false); |
| | | const typeActions = [ |
| | | { name: "货车", value: "货车" }, |
| | | { name: "快递", value: "快递" }, |
| | | ]; |
| | | |
| | | const form = reactive({ |
| | | type: "货车", |
| | | shippingCarNumber: "", |
| | | expressCompany: "", |
| | | expressNumber: "", |
| | | }); |
| | | |
| | | const rules = { |
| | | type: [{ required: true, message: "请选择发货方式", trigger: "change" }], |
| | | shippingCarNumber: [ |
| | | { |
| | | required: true, |
| | | validator: (rule, value, callback) => { |
| | | if (form.type === "货车" && !value) { |
| | | return false; |
| | | } |
| | | return true; |
| | | }, |
| | | message: "请输入车牌号", |
| | | trigger: "blur", |
| | | }, |
| | | ], |
| | | expressCompany: [ |
| | | { |
| | | required: true, |
| | | validator: (rule, value, callback) => { |
| | | if (form.type === "快递" && !value) { |
| | | return false; |
| | | } |
| | | return true; |
| | | }, |
| | | message: "请输入快递公司", |
| | | trigger: "blur", |
| | | }, |
| | | ], |
| | | expressNumber: [ |
| | | { |
| | | required: true, |
| | | validator: (rule, value, callback) => { |
| | | if (form.type === "快递" && !value) { |
| | | return false; |
| | | } |
| | | return true; |
| | | }, |
| | | message: "请输入快递单号", |
| | | trigger: "blur", |
| | | }, |
| | | ], |
| | | }; |
| | | |
| | | const formRef = ref(null); |
| | | |
| | | onMounted(async () => { |
| | | try { |
| | | userListNoPageByTenantId().then(res => { |
| | | userList.value = res.data; |
| | | }); |
| | | // 从本地存储获取发货详情 |
| | | goOutData.value = JSON.parse(uni.getStorageSync("goOutData")); |
| | | console.log(goOutData.value, "goOutData.value"); |
| | | |
| | | // 初始化审批流程节点,默认一个节点 |
| | | approverNodes.value = [{ id: 1, userId: null }]; |
| | | |
| | | // 监听联系人选择事件 |
| | | uni.$on("selectContact", handleSelectContact); |
| | | } catch (error) { |
| | | console.error("获取失败:", error); |
| | | const storedData = uni.getStorageSync("goOutData"); |
| | | goOutData.value = JSON.parse(storedData || "{}"); |
| | | if (goOutData.value.productModelId) { |
| | | loadBatches(goOutData.value.productModelId); |
| | | } |
| | | }); |
| | | |
| | | onUnmounted(() => { |
| | | // 移除事件监听 |
| | | uni.$off("selectContact", handleSelectContact); |
| | | }); |
| | | const typeValue = ref("货车"); |
| | | const onConfirm = item => { |
| | | // 设置选中的部门 |
| | | typeValue.value = item.name; |
| | | showPicker.value = false; |
| | | const loadBatches = async modelId => { |
| | | if (!modelId) return; |
| | | try { |
| | | const res = await getStockInventoryByModelId(modelId); |
| | | const rawList = Array.isArray(res?.data) |
| | | ? res.data |
| | | : res?.data?.records || res?.data?.rows || res || []; |
| | | const seenIds = new Set(); |
| | | batchList.value = rawList |
| | | .filter(item => { |
| | | if (!item?.id || !item?.batchNo || seenIds.has(item.id)) { |
| | | return false; |
| | | } |
| | | seenIds.add(item.id); |
| | | return true; |
| | | }) |
| | | .map(item => ({ |
| | | ...item, |
| | | deliveryQuantity: "", |
| | | })); |
| | | } catch (e) { |
| | | console.error("加载批次失败", e); |
| | | } |
| | | }; |
| | | |
| | | const goBack = () => { |
| | | // 清除本地存储的数据 |
| | | uni.removeStorageSync("operationType"); |
| | | uni.removeStorageSync("invoiceLedgerEditRow"); |
| | | uni.removeStorageSync("approveType"); |
| | | uni.navigateBack(); |
| | | const getAvailableQty = item => { |
| | | const quantity = |
| | | item?.qualitity ?? |
| | | item?.quantity ?? |
| | | item?.unLockedQuantity ?? |
| | | item?.qualifiedUnLockedQuantity ?? |
| | | item?.qualifiedQuantity ?? |
| | | item?.stockQuantity; |
| | | return quantity ?? 0; |
| | | }; |
| | | |
| | | const submitForm = () => { |
| | | // 检查每个审批步骤是否都有审批人 |
| | | const hasEmptyStep = approverNodes.value.some(step => !step.nickName); |
| | | if (hasEmptyStep) { |
| | | showToast("请为每个审批步骤选择审批人"); |
| | | const onBatchQtyChange = item => { |
| | | const val = parseFloat(item.deliveryQuantity); |
| | | if (isNaN(val) || val <= 0) { |
| | | item.deliveryQuantity = ""; |
| | | return; |
| | | } |
| | | formRef.value |
| | | .validate() |
| | | .then(valid => { |
| | | if (valid) { |
| | | // 表单校验通过,可以提交数据 |
| | | // 收集所有节点的审批人id |
| | | console.log("approverNodes---", approverNodes.value); |
| | | const approveUserIds = approverNodes.value |
| | | .map(node => node.userId) |
| | | .join(","); |
| | | const params = { |
| | | salesLedgerId: goOutData.value.salesLedgerId, |
| | | salesLedgerProductId: goOutData.value.id, |
| | | type: typeValue.value, |
| | | approveUserIds, |
| | | }; |
| | | console.log(params, "params"); |
| | | |
| | | addShippingInfo(params).then(res => { |
| | | showToast("发货成功"); |
| | | setTimeout(() => { |
| | | goBack(); |
| | | }, 500); |
| | | }); |
| | | } |
| | | }) |
| | | .catch(error => { |
| | | console.error("表单校验失败:", error); |
| | | // 尝试获取具体的错误字段 |
| | | if (error && error.errors) { |
| | | const firstError = error.errors[0]; |
| | | if (firstError) { |
| | | uni.showToast({ |
| | | title: firstError.message || "表单校验失败,请检查必填项", |
| | | icon: "none", |
| | | }); |
| | | return; |
| | | const available = getAvailableQty(item); |
| | | if (val > available) { |
| | | uni.showToast({ title: "不能超过库存数量", icon: "none" }); |
| | | item.deliveryQuantity = available.toString(); |
| | | } |
| | | |
| | | const totalToShip = Number(goOutData.value.noQuantity || 0); |
| | | const otherBatchesTotal = batchList.value.reduce((sum, b) => { |
| | | if (b.id === item.id) return sum; |
| | | return sum + Number(b.deliveryQuantity || 0); |
| | | }, 0); |
| | | |
| | | if (val + otherBatchesTotal > totalToShip) { |
| | | uni.showToast({ title: "总数不能超过待发货数量", icon: "none" }); |
| | | item.deliveryQuantity = (totalToShip - otherBatchesTotal).toString(); |
| | | } |
| | | }; |
| | | |
| | | const onTypeSelect = item => { |
| | | form.type = item.name; |
| | | showTypePicker.value = false; |
| | | }; |
| | | |
| | | const afterRead = async event => { |
| | | const { file } = event; |
| | | const lists = [].concat(file); |
| | | const token = getToken(); |
| | | |
| | | for (let i = 0; i < lists.length; i++) { |
| | | const item = lists[i]; |
| | | const uploadIndex = fileList.value.length; |
| | | fileList.value.push({ |
| | | ...item, |
| | | status: "uploading", |
| | | message: "上传中", |
| | | }); |
| | | |
| | | uni.uploadFile({ |
| | | url: config.baseUrl + "/common/upload", |
| | | filePath: item.url, |
| | | name: "files", |
| | | header: { |
| | | Authorization: "Bearer " + token, |
| | | }, |
| | | success: res => { |
| | | try { |
| | | const data = JSON.parse(res.data); |
| | | if (data.code === 200) { |
| | | const fileData = Array.isArray(data.data) |
| | | ? data.data[0] |
| | | : data.data || data; |
| | | fileList.value[uploadIndex].status = "success"; |
| | | fileList.value[uploadIndex].message = ""; |
| | | fileList.value[uploadIndex].url = fileData.url; |
| | | fileList.value[uploadIndex].storageBlobDTO = fileData; |
| | | } else { |
| | | fileList.value[uploadIndex].status = "failed"; |
| | | fileList.value[uploadIndex].message = data.msg || "上传失败"; |
| | | } |
| | | } catch (e) { |
| | | fileList.value[uploadIndex].status = "failed"; |
| | | fileList.value[uploadIndex].message = "解析失败"; |
| | | } |
| | | } |
| | | // 显示通用错误信息 |
| | | uni.showToast({ |
| | | title: "表单校验失败,请检查必填项", |
| | | icon: "none", |
| | | }); |
| | | }, |
| | | fail: () => { |
| | | fileList.value[uploadIndex].status = "failed"; |
| | | fileList.value[uploadIndex].message = "网络异常"; |
| | | }, |
| | | }); |
| | | } |
| | | }; |
| | | |
| | | // 处理联系人选择结果 |
| | | const handleSelectContact = data => { |
| | | const { stepIndex, contact } = data; |
| | | // 将选中的联系人设置为对应审批步骤的审批人 |
| | | approverNodes.value[stepIndex].userId = contact.userId; |
| | | approverNodes.value[stepIndex].nickName = contact.nickName; |
| | | const deleteFile = event => { |
| | | fileList.value.splice(event.index, 1); |
| | | }; |
| | | |
| | | const addApprover = stepIndex => { |
| | | // 跳转到联系人选择页面 |
| | | uni.setStorageSync("stepIndex", stepIndex); |
| | | uni.navigateTo({ |
| | | url: "/pages/cooperativeOffice/collaborativeApproval/contactSelect", |
| | | }); |
| | | }; |
| | | const goBack = () => uni.navigateBack(); |
| | | |
| | | const addApprovalStep = () => { |
| | | // 添加新的审批步骤 |
| | | approverNodes.value.push({ userId: null, nickName: null }); |
| | | }; |
| | | const submitForm = async () => { |
| | | const valid = await formRef.value.validate().catch(() => false); |
| | | if (!valid) return; |
| | | |
| | | const removeApprover = stepIndex => { |
| | | // 移除审批人 |
| | | approverNodes.value[stepIndex].userId = null; |
| | | approverNodes.value[stepIndex].nickName = null; |
| | | }; |
| | | const selectedBatches = batchList.value.filter( |
| | | b => parseFloat(b.deliveryQuantity) > 0 |
| | | ); |
| | | if (selectedBatches.length === 0) { |
| | | uni.showToast({ title: "请至少填写一个批次的发货数量", icon: "none" }); |
| | | return; |
| | | } |
| | | |
| | | const removeApprovalStep = stepIndex => { |
| | | // 确保至少保留一个审批步骤 |
| | | if (approverNodes.value.length > 1) { |
| | | approverNodes.value.splice(stepIndex, 1); |
| | | } else { |
| | | uni.showToast({ |
| | | title: "至少需要一个审批步骤", |
| | | icon: "none", |
| | | }); |
| | | // Check if any file is still uploading |
| | | if (fileList.value.some(f => f.status === "uploading")) { |
| | | uni.showToast({ title: "请等待图片上传完成", icon: "none" }); |
| | | return; |
| | | } |
| | | |
| | | const payload = { |
| | | salesLedgerId: goOutData.value.salesLedgerId, |
| | | salesLedgerProductId: goOutData.value.id, |
| | | type: form.type, |
| | | shippingCarNumber: form.type === "货车" ? form.shippingCarNumber : "", |
| | | expressCompany: form.type === "快递" ? form.expressCompany : "", |
| | | expressNumber: form.type === "快递" ? form.expressNumber : "", |
| | | storageBlobDTOs: fileList.value |
| | | .filter(f => f.status === "success") |
| | | .map(f => f.storageBlobDTO), |
| | | batchNo: selectedBatches.map(b => b.id), |
| | | batchNoDetailList: selectedBatches.map(b => ({ |
| | | stockInventoryId: b.id, |
| | | batchNo: b.batchNo, |
| | | quantity: parseFloat(b.deliveryQuantity), |
| | | productModelId: goOutData.value.productModelId, |
| | | })), |
| | | }; |
| | | |
| | | try { |
| | | uni.showLoading({ title: "提交中..." }); |
| | | const res = await addShippingInfo(payload); |
| | | uni.hideLoading(); |
| | | uni.showToast({ title: "发货成功" }); |
| | | setTimeout(() => goBack(), 500); |
| | | } catch (e) { |
| | | uni.hideLoading(); |
| | | uni.showToast({ title: "发货失败", icon: "none" }); |
| | | } |
| | | }; |
| | | </script> |
| | | |
| | | <style scoped lang="scss"> |
| | | @import "@/static/scss/form-common.scss"; |
| | | |
| | | .approval-process { |
| | | background: #fff; |
| | | margin: 16px; |
| | | border-radius: 16px; |
| | | padding: 16px; |
| | | box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04); |
| | | .shipment-page { |
| | | min-height: 100vh; |
| | | background: #f8f9fa; |
| | | padding-bottom: 100px; |
| | | } |
| | | |
| | | .approval-header { |
| | | margin-bottom: 16px; |
| | | .form-container { |
| | | padding: 12px 12px 0; |
| | | } |
| | | |
| | | .approval-title { |
| | | font-size: 16px; |
| | | font-weight: 600; |
| | | color: #333; |
| | | display: block; |
| | | margin-bottom: 4px; |
| | | } |
| | | |
| | | .approval-desc { |
| | | font-size: 12px; |
| | | color: #999; |
| | | } |
| | | |
| | | /* 样式增强为“简洁小圆圈风格” */ |
| | | .approval-steps { |
| | | padding-left: 22px; |
| | | position: relative; |
| | | |
| | | &::before { |
| | | content: ""; |
| | | position: absolute; |
| | | left: 11px; |
| | | top: 40px; |
| | | bottom: 40px; |
| | | width: 2px; |
| | | background: linear-gradient( |
| | | to bottom, |
| | | #e6f7ff 0%, |
| | | #bae7ff 50%, |
| | | #91d5ff 100% |
| | | ); |
| | | border-radius: 1px; |
| | | } |
| | | } |
| | | |
| | | .approval-step { |
| | | position: relative; |
| | | margin-bottom: 24px; |
| | | |
| | | &::before { |
| | | content: ""; |
| | | position: absolute; |
| | | left: -18px; |
| | | top: 14px; // 从 8px 调整为 14px,与文字中心对齐 |
| | | width: 12px; |
| | | height: 12px; |
| | | background: #fff; |
| | | border: 3px solid #006cfb; |
| | | border-radius: 50%; |
| | | z-index: 2; |
| | | box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); |
| | | } |
| | | } |
| | | |
| | | .step-title { |
| | | top: 12px; |
| | | .form-section { |
| | | margin-bottom: 12px; |
| | | position: relative; |
| | | margin-left: 6px; |
| | | } |
| | | |
| | | .step-title text { |
| | | font-size: 14px; |
| | | color: #666; |
| | | background: #f0f0f0; |
| | | padding: 4px 12px; |
| | | border-radius: 12px; |
| | | position: relative; |
| | | line-height: 1.4; // 确保文字行高一致 |
| | | overflow: hidden; |
| | | box-shadow: 0 2px 10px rgba(15, 35, 95, 0.05); |
| | | } |
| | | |
| | | .approver-item { |
| | | .section-header-info { |
| | | padding: 10px 18px; |
| | | background: #f8fbff; |
| | | display: flex; |
| | | align-items: center; |
| | | background: linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%); |
| | | border-radius: 16px; |
| | | padding: 16px; |
| | | justify-content: flex-end; |
| | | |
| | | .subtitle { |
| | | font-size: 13px; |
| | | color: #7a8599; |
| | | } |
| | | } |
| | | |
| | | .batch-list { |
| | | padding: 12px; |
| | | display: flex; |
| | | flex-direction: column; |
| | | gap: 12px; |
| | | position: relative; |
| | | border: 1px solid #e6f7ff; |
| | | box-shadow: 0 4px 12px rgba(0, 108, 251, 0.08); |
| | | transition: all 0.3s ease; |
| | | } |
| | | |
| | | .approver-avatar { |
| | | width: 48px; |
| | | height: 48px; |
| | | background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); |
| | | border-radius: 50%; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | position: relative; |
| | | box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3); |
| | | } |
| | | |
| | | .avatar-text { |
| | | color: #fff; |
| | | font-size: 18px; |
| | | font-weight: 600; |
| | | text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); |
| | | } |
| | | |
| | | .approver-info { |
| | | flex: 1; |
| | | position: relative; |
| | | } |
| | | |
| | | .approver-name { |
| | | display: block; |
| | | font-size: 16px; |
| | | color: #333; |
| | | font-weight: 500; |
| | | position: relative; |
| | | } |
| | | |
| | | .approver-dept { |
| | | font-size: 12px; |
| | | color: #999; |
| | | background: rgba(0, 108, 251, 0.05); |
| | | padding: 2px 8px; |
| | | border-radius: 8px; |
| | | display: inline-block; |
| | | position: relative; |
| | | |
| | | &::before { |
| | | content: ""; |
| | | position: absolute; |
| | | left: 4px; |
| | | top: 50%; |
| | | transform: translateY(-50%); |
| | | width: 2px; |
| | | height: 2px; |
| | | background: #006cfb; |
| | | border-radius: 50%; |
| | | } |
| | | } |
| | | |
| | | .delete-approver-btn { |
| | | font-size: 16px; |
| | | color: #ff4d4f; |
| | | background: linear-gradient( |
| | | 135deg, |
| | | rgba(255, 77, 79, 0.1) 0%, |
| | | rgba(255, 77, 79, 0.05) 100% |
| | | ); |
| | | width: 28px; |
| | | height: 28px; |
| | | border-radius: 50%; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | transition: all 0.3s ease; |
| | | position: relative; |
| | | } |
| | | |
| | | .add-approver-btn { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | background: linear-gradient(135deg, #f0f8ff 0%, #e6f7ff 100%); |
| | | border: 2px dashed #006cfb; |
| | | border-radius: 16px; |
| | | padding: 20px; |
| | | color: #006cfb; |
| | | font-size: 14px; |
| | | position: relative; |
| | | transition: all 0.3s ease; |
| | | |
| | | &::before { |
| | | content: ""; |
| | | position: absolute; |
| | | left: 50%; |
| | | top: 50%; |
| | | transform: translate(-50%, -50%); |
| | | width: 32px; |
| | | height: 32px; |
| | | border: 2px solid #006cfb; |
| | | border-radius: 50%; |
| | | opacity: 0; |
| | | transition: all 0.3s ease; |
| | | } |
| | | } |
| | | |
| | | .delete-step-btn { |
| | | color: #ff4d4f; |
| | | font-size: 12px; |
| | | background: linear-gradient( |
| | | 135deg, |
| | | rgba(255, 77, 79, 0.1) 0%, |
| | | rgba(255, 77, 79, 0.05) 100% |
| | | ); |
| | | padding: 6px 12px; |
| | | border-radius: 12px; |
| | | display: inline-block; |
| | | position: relative; |
| | | transition: all 0.3s ease; |
| | | |
| | | &::before { |
| | | content: ""; |
| | | position: absolute; |
| | | left: 6px; |
| | | top: 50%; |
| | | transform: translateY(-50%); |
| | | width: 4px; |
| | | height: 4px; |
| | | background: #ff4d4f; |
| | | border-radius: 50%; |
| | | } |
| | | } |
| | | |
| | | .step-line { |
| | | display: none; // 隐藏原来的线条,使用伪元素代替 |
| | | } |
| | | |
| | | .add-step-btn { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | } |
| | | .footer-btns { |
| | | position: fixed; |
| | | left: 0; |
| | | right: 0; |
| | | bottom: 0; |
| | | .batch-card { |
| | | background: #fff; |
| | | border-radius: 12px; |
| | | padding: 0 12px 12px; |
| | | box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); |
| | | border: 1px solid #f0f3f7; |
| | | } |
| | | |
| | | .batch-header { |
| | | padding: 12px 0; |
| | | display: flex; |
| | | justify-content: space-around; |
| | | justify-content: space-between; |
| | | align-items: center; |
| | | padding: 0.75rem 0; |
| | | box-shadow: 0 -0.125rem 0.5rem rgba(0, 0, 0, 0.05); |
| | | z-index: 1000; |
| | | } |
| | | |
| | | .cancel-btn { |
| | | font-weight: 400; |
| | | font-size: 1rem; |
| | | color: #ffffff; |
| | | width: 6.375rem; |
| | | background: #c7c9cc; |
| | | box-shadow: 0 0.25rem 0.625rem 0 rgba(3, 88, 185, 0.2); |
| | | border-radius: 2.5rem 2.5rem 2.5rem 2.5rem; |
| | | } |
| | | |
| | | .save-btn { |
| | | font-weight: 400; |
| | | font-size: 1rem; |
| | | color: #ffffff; |
| | | width: 14rem; |
| | | background: linear-gradient(140deg, #00baff 0%, #006cfb 100%); |
| | | box-shadow: 0 0.25rem 0.625rem 0 rgba(3, 88, 185, 0.2); |
| | | border-radius: 2.5rem 2.5rem 2.5rem 2.5rem; |
| | | } |
| | | |
| | | // 动画定义 |
| | | @keyframes pulse { |
| | | 0% { |
| | | transform: scale(1); |
| | | opacity: 1; |
| | | .batch-no { |
| | | font-size: 14px; |
| | | font-weight: 600; |
| | | color: #22324d; |
| | | } |
| | | 50% { |
| | | transform: scale(1.2); |
| | | opacity: 0.7; |
| | | } |
| | | 100% { |
| | | transform: scale(1); |
| | | opacity: 1; |
| | | |
| | | .batch-qty { |
| | | font-size: 13px; |
| | | color: #2979ff; |
| | | background: #eef6ff; |
| | | padding: 2px 8px; |
| | | border-radius: 4px; |
| | | } |
| | | } |
| | | |
| | | @keyframes rotate { |
| | | 0% { |
| | | transform: rotate(0deg); |
| | | } |
| | | 100% { |
| | | transform: rotate(360deg); |
| | | } |
| | | } |
| | | |
| | | @keyframes ripple { |
| | | 0% { |
| | | transform: translate(-50%, -50%) scale(0.8); |
| | | opacity: 1; |
| | | } |
| | | 100% { |
| | | transform: translate(-50%, -50%) scale(1.6); |
| | | opacity: 0; |
| | | } |
| | | } |
| | | |
| | | /* 如果已有 .step-line,这里更精准定位到左侧与小圆点对齐 */ |
| | | .step-line { |
| | | position: absolute; |
| | | left: 4px; |
| | | top: 48px; |
| | | width: 2px; |
| | | height: calc(100% - 48px); |
| | | background: #e5e7eb; |
| | | } |
| | | |
| | | .approver-container { |
| | | display: flex; |
| | | align-items: center; |
| | | background: linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%); |
| | | border-radius: 16px; |
| | | gap: 12px; |
| | | padding: 10px 0; |
| | | background: transparent; |
| | | border: none; |
| | | box-shadow: none; |
| | | } |
| | | |
| | | .approver-item { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 12px; |
| | | padding: 8px 10px; |
| | | background: transparent; |
| | | border: none; |
| | | box-shadow: none; |
| | | border-radius: 0; |
| | | } |
| | | |
| | | .approver-avatar { |
| | | position: relative; |
| | | width: 40px; |
| | | height: 40px; |
| | | border-radius: 50%; |
| | | background: #f3f4f6; |
| | | border: 2px solid #e5e7eb; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | animation: none; /* 禁用旋转等动画,回归简洁 */ |
| | | } |
| | | |
| | | .avatar-text { |
| | | font-size: 14px; |
| | | color: #374151; |
| | | font-weight: 600; |
| | | } |
| | | |
| | | .add-approver-btn { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 8px; |
| | | background: transparent; |
| | | border: none; |
| | | box-shadow: none; |
| | | padding: 0; |
| | | } |
| | | |
| | | .add-approver-btn .add-circle { |
| | | width: 40px; |
| | | height: 40px; |
| | | border: 2px dashed #a0aec0; |
| | | border-radius: 50%; |
| | | color: #6b7280; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | font-size: 22px; |
| | | line-height: 1; |
| | | } |
| | | |
| | | .add-approver-btn .add-label { |
| | | color: #3b82f6; |
| | | .empty-text { |
| | | padding: 30px 12px; |
| | | text-align: center; |
| | | color: #999; |
| | | font-size: 14px; |
| | | } |
| | | </style> |
| | | |
| | | .upload-container { |
| | | padding: 12px 18px; |
| | | } |
| | | |
| | | :deep(.u-cell-group__title) { |
| | | padding: 14px 18px 10px !important; |
| | | font-size: 15px !important; |
| | | font-weight: 600 !important; |
| | | color: #22324d !important; |
| | | background: #f8fbff !important; |
| | | } |
| | | |
| | | :deep(.u-form-item) { |
| | | padding: 0 18px !important; |
| | | } |
| | | </style> |