<template>
|
<view class="account-detail">
|
<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-item v-if="typeValue === '货车'"
|
prop="shippingCarNumber"
|
label="车牌号"
|
required>
|
<u-input v-model="form.shippingCarNumber"
|
placeholder="请输入车牌号"
|
clearable />
|
</u-form-item>
|
<u-form-item v-if="typeValue === '快递'"
|
prop="expressNumber"
|
label="快递单号"
|
required>
|
<u-input v-model="form.expressNumber"
|
placeholder="请输入快递单号"
|
clearable />
|
</u-form-item>
|
</u-form>
|
<!-- 发货图片:相册或相机、/file/upload、预览与列表样式 -->
|
<view class="ship-images-card">
|
<view class="ship-images-header">
|
<text class="ship-images-title">发货图片</text>
|
<text class="ship-images-hint">最多 {{ uploadConfig.limit }} 张</text>
|
</view>
|
<view class="simple-upload-area">
|
<view class="upload-buttons">
|
<u-button type="primary"
|
@click="chooseShipImage"
|
:loading="shipUploading"
|
:disabled="shipFiles.length >= uploadConfig.limit"
|
:customStyle="{ width: '100%' }">
|
<u-icon name="camera"
|
size="18"
|
color="#fff"
|
style="margin-right: 5px;"></u-icon>
|
{{ shipUploading ? '上传中...' : '添加图片' }}
|
</u-button>
|
</view>
|
<view v-if="shipUploading"
|
class="upload-progress">
|
<u-line-progress :percentage="shipUploadProgress"
|
:showText="true"
|
activeColor="#409eff"></u-line-progress>
|
</view>
|
<view v-if="shipFiles.length > 0"
|
class="file-list">
|
<view v-for="(file, index) in shipFiles"
|
:key="file.uid || index"
|
class="file-item">
|
<view class="file-preview-container"
|
@click="previewShipImage(file)">
|
<image :src="getFileAccessUrl(file)"
|
class="file-preview"
|
mode="aspectFill" />
|
<view class="delete-btn"
|
@click.stop="removeShipFile(index)">
|
<u-icon name="close"
|
size="12"
|
color="#fff"></u-icon>
|
</view>
|
</view>
|
<view class="file-info">
|
<text class="file-name">{{ file.bucketFilename || file.name || '图片' }}</text>
|
<text class="file-size">{{ formatFileSize(file.size) }}</text>
|
</view>
|
</view>
|
</view>
|
<view v-else
|
class="empty-state">
|
<text>请从相册选择或拍照上传发货图片</text>
|
</view>
|
</view>
|
</view>
|
<!-- 选择器弹窗 -->
|
<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>
|
<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>
|
<view class="approver-info">
|
<text class="approver-name">{{ step.nickName }}</text>
|
</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>
|
</view>
|
<!-- 底部按钮 -->
|
<view class="footer-btns">
|
<u-button class="cancel-btn"
|
@click="goBack">取消</u-button>
|
<u-button class="save-btn"
|
@click="submitForm">发货</u-button>
|
</view>
|
</view>
|
</template>
|
|
<script setup>
|
import { ref, computed, onMounted, onUnmounted, reactive, toRefs } from "vue";
|
import PageHeader from "@/components/PageHeader.vue";
|
import { addShippingInfo } from "@/api/salesManagement/salesLedger";
|
import config from "@/config";
|
import { getToken } from "@/utils/auth";
|
const showToast = message => {
|
uni.showToast({
|
title: message,
|
icon: "none",
|
});
|
};
|
import { userListNoPageByTenantId } from "@/api/system/user";
|
|
const typeValue = ref("货车");
|
|
const data = reactive({
|
form: {
|
approveTime: "",
|
approveId: "",
|
approveUser: "",
|
approveUserName: "",
|
approveDeptName: "",
|
approveDeptId: "",
|
approveReason: "",
|
checkResult: "",
|
tempFileIds: [],
|
approverList: [], // 新增字段,存储所有节点的审批人id
|
startDate: "",
|
endDate: "",
|
location: "",
|
price: "",
|
shippingCarNumber: "",
|
expressNumber: "",
|
},
|
rules: {
|
typeValue: [{ required: false, message: "请选择", trigger: "change" }],
|
shippingCarNumber: [
|
{
|
validator: (rule, value, callback) => {
|
if (typeValue.value !== "货车") {
|
callback();
|
return;
|
}
|
if (!value || !String(value).trim()) {
|
callback(new Error("请输入车牌号"));
|
return;
|
}
|
callback();
|
},
|
trigger: ["blur", "change"],
|
},
|
],
|
expressNumber: [
|
{
|
validator: (rule, value, callback) => {
|
if (typeValue.value !== "快递") {
|
callback();
|
return;
|
}
|
if (!value || !String(value).trim()) {
|
callback(new Error("请输入快递单号"));
|
return;
|
}
|
callback();
|
},
|
trigger: ["blur", "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({});
|
|
// 与设备巡检 inspectionUpload/index.vue 中 uploadConfig 一致
|
const uploadConfig = {
|
action: "/file/upload",
|
limit: 10,
|
fileSize: 50,
|
fileType: ["jpg", "jpeg", "png", "gif", "webp"],
|
};
|
|
/** 与设备巡检一致:生产前 type=10 传给 /file/upload 的 formData.type */
|
const shipUploadFormType = 10;
|
|
const uploadFileUrl = computed(() => (config.baseUrl || "").replace(/\/$/, "") + uploadConfig.action);
|
|
const shipFiles = ref([]);
|
const shipUploading = ref(false);
|
const shipUploadProgress = ref(0);
|
|
const isImageFile = file => {
|
if (file?.contentType && String(file.contentType).startsWith("image/")) return true;
|
if (file?.type === "image" || file?.mediaType === "image") return true;
|
const name = file?.bucketFilename || file?.originalFilename || file?.name || "";
|
const ext = name.split(".").pop()?.toLowerCase();
|
return ["jpg", "jpeg", "png", "gif", "webp"].includes(ext);
|
};
|
|
const filePreviewBase = config.fileUrl;
|
|
const normalizeFileUrl = (rawUrl = "") => {
|
let fileUrl = rawUrl || "";
|
if (typeof fileUrl === "string") {
|
fileUrl = fileUrl.trim().replace(/^['"]|['"]$/g, "");
|
}
|
const javaApi = filePreviewBase;
|
const localPrefixes = ["wxfile://", "file://", "content://", "blob:", "data:"];
|
|
if (localPrefixes.some(prefix => fileUrl.startsWith(prefix))) {
|
return fileUrl;
|
}
|
|
if (fileUrl && fileUrl.indexOf("\\") > -1) {
|
const lowerPath = fileUrl.toLowerCase();
|
const uploadPathIndex = lowerPath.indexOf("uploadpath");
|
const prodIndex = lowerPath.indexOf("\\prod\\");
|
|
if (uploadPathIndex > -1) {
|
fileUrl = fileUrl.substring(uploadPathIndex).replace(/\\/g, "/");
|
} else if (prodIndex > -1) {
|
fileUrl = fileUrl
|
.substring(prodIndex + "\\prod\\".length)
|
.replace(/\\/g, "/");
|
} else {
|
fileUrl = fileUrl.replace(/\\/g, "/");
|
}
|
}
|
const normalizedLower = String(fileUrl).toLowerCase();
|
const fileProdIdx = normalizedLower.indexOf("/file/prod/");
|
if (fileProdIdx > -1) {
|
fileUrl = `/profile/prod/${fileUrl.substring(fileProdIdx + "/file/prod/".length)}`;
|
}
|
const fileTempIdx = normalizedLower.indexOf("/file/temp/");
|
if (fileTempIdx > -1) {
|
fileUrl = `/profile/temp/${fileUrl.substring(fileTempIdx + "/file/temp/".length)}`;
|
}
|
|
if (/^\/?uploadPath/i.test(fileUrl)) {
|
fileUrl = fileUrl.replace(/^\/?uploadPath/i, "/profile");
|
}
|
|
if (fileUrl && !fileUrl.startsWith("http")) {
|
if (!fileUrl.startsWith("/")) fileUrl = "/" + fileUrl;
|
fileUrl = javaApi + fileUrl;
|
}
|
|
return fileUrl;
|
};
|
|
const getFileAccessUrl = (file = {}) => {
|
if (file?.link) {
|
if (String(file.link).startsWith("http")) return file.link;
|
return normalizeFileUrl(file.link);
|
}
|
const remoteUrl = normalizeFileUrl(
|
file?.url || file?.downloadUrl || file?.tempPath || ""
|
);
|
if (remoteUrl) return remoteUrl;
|
if (file?._localPreviewUrl) return file._localPreviewUrl;
|
return normalizeFileUrl(file?.tempFilePath || file?.path || "");
|
};
|
|
const formatFileSize = size => {
|
if (!size) return "";
|
if (size < 1024) return size + "B";
|
if (size < 1024 * 1024) return (size / 1024).toFixed(1) + "KB";
|
return (size / (1024 * 1024)).toFixed(1) + "MB";
|
};
|
|
const handleShipUploadError = (message = "上传文件失败") => {
|
shipUploading.value = false;
|
shipUploadProgress.value = 0;
|
uni.showToast({ title: message, icon: "error" });
|
};
|
|
const handleShipUploadSuccess = (res, file) => {
|
const uploadedFile = res.data;
|
if (!uploadedFile) {
|
handleShipUploadError("上传响应数据格式错误");
|
return;
|
}
|
const fileData = {
|
...file,
|
id: uploadedFile.id,
|
tempId: uploadedFile.tempId ?? uploadedFile.tempFileId ?? uploadedFile.id,
|
url:
|
uploadedFile.url ||
|
uploadedFile.downloadUrl ||
|
uploadedFile.tempPath ||
|
file.tempFilePath ||
|
file.path,
|
tempPath: uploadedFile.tempPath || file.tempPath || "",
|
_localPreviewUrl: file.tempFilePath || file.path || "",
|
bucketFilename:
|
uploadedFile.bucketFilename ||
|
uploadedFile.originalFilename ||
|
uploadedFile.originalName ||
|
file.name,
|
downloadUrl: uploadedFile.downloadUrl || uploadedFile.url,
|
size: uploadedFile.size || uploadedFile.byteSize || file.size,
|
createTime: uploadedFile.createTime || new Date().getTime(),
|
mediaType: file.type || uploadedFile.mediaType,
|
};
|
shipFiles.value.push(fileData);
|
uni.showToast({ title: "上传成功", icon: "success" });
|
};
|
|
const uploadShipWithUniUploadFile = (file, filePath, token) => {
|
if (!filePath) {
|
handleShipUploadError("文件路径不存在");
|
return;
|
}
|
shipUploading.value = true;
|
shipUploadProgress.value = 0;
|
|
const uploadTask = uni.uploadFile({
|
url: uploadFileUrl.value,
|
filePath,
|
name: "file",
|
formData: {
|
type: shipUploadFormType,
|
},
|
header: {
|
Authorization: `Bearer ${token}`,
|
},
|
success: res => {
|
try {
|
if (res.statusCode === 200) {
|
const response = JSON.parse(res.data || "{}");
|
if (response.code === 200) {
|
handleShipUploadSuccess(response, file);
|
} else {
|
handleShipUploadError(response.msg || "服务器返回错误");
|
}
|
} else {
|
handleShipUploadError(`服务器错误,状态码: ${res.statusCode}`);
|
}
|
} catch (e) {
|
console.error("解析响应失败:", e);
|
handleShipUploadError("响应数据解析失败");
|
}
|
},
|
fail: err => {
|
let errorMessage = "上传失败";
|
if (err.errMsg) {
|
if (err.errMsg.includes("statusCode: null")) {
|
errorMessage = "网络连接失败,请检查网络设置";
|
} else if (err.errMsg.includes("timeout")) {
|
errorMessage = "上传超时,请重试";
|
} else if (err.errMsg.includes("fail")) {
|
errorMessage = "上传失败,请检查网络连接";
|
} else {
|
errorMessage = err.errMsg;
|
}
|
}
|
handleShipUploadError(errorMessage);
|
},
|
complete: () => {
|
shipUploading.value = false;
|
shipUploadProgress.value = 0;
|
},
|
});
|
|
if (uploadTask && uploadTask.onProgressUpdate) {
|
uploadTask.onProgressUpdate(r => {
|
shipUploadProgress.value = r.progress;
|
});
|
}
|
};
|
|
const uploadShipFile = file => {
|
const token = getToken();
|
if (!token) {
|
handleShipUploadError("用户未登录");
|
return;
|
}
|
uploadShipWithUniUploadFile(file, file.tempFilePath || file.path || "", token);
|
};
|
|
const handleBeforeShipUpload = async file => {
|
if (uploadConfig.fileType?.length) {
|
const fileName = file.name || "";
|
const fileExtension = fileName ? fileName.split(".").pop().toLowerCase() : "";
|
const expectedTypes = ["jpg", "jpeg", "png", "gif", "webp"];
|
if (fileExtension && expectedTypes.length > 0) {
|
const isAllowed = expectedTypes.some(
|
t => uploadConfig.fileType.includes(t) && t === fileExtension
|
);
|
if (!isAllowed) {
|
uni.showToast({
|
title: `文件格式不支持,请选择 ${expectedTypes.join("/")} 格式的图片`,
|
icon: "none",
|
});
|
return false;
|
}
|
}
|
}
|
uploadShipFile(file);
|
return true;
|
};
|
|
const chooseShipImage = () => {
|
if (shipFiles.value.length >= uploadConfig.limit) {
|
uni.showToast({
|
title: `最多只能上传${uploadConfig.limit}张图片`,
|
icon: "none",
|
});
|
return;
|
}
|
|
const remaining = uploadConfig.limit - shipFiles.value.length;
|
|
if (typeof uni.chooseMedia === "function") {
|
uni.chooseMedia({
|
count: Math.min(remaining, 1),
|
mediaType: ["image"],
|
sizeType: ["compressed", "original"],
|
sourceType: ["album", "camera"],
|
success: res => {
|
try {
|
const files = res?.tempFiles || [];
|
if (!files.length) throw new Error("未获取到文件");
|
files.forEach((tf, idx) => {
|
const filePath = tf.tempFilePath || tf.path || "";
|
const file = {
|
tempFilePath: filePath,
|
path: filePath,
|
type: "image",
|
name: `image_${Date.now()}_${idx}.jpg`,
|
size: tf.size || 0,
|
createTime: Date.now(),
|
uid: Date.now() + Math.random() + idx,
|
};
|
handleBeforeShipUpload(file);
|
});
|
} catch (e) {
|
console.error("处理图片选择结果失败:", e);
|
uni.showToast({ title: "处理文件失败", icon: "error" });
|
}
|
},
|
fail: () => uni.showToast({ title: "选择图片失败", icon: "error" }),
|
});
|
return;
|
}
|
|
uni.chooseImage({
|
count: 1,
|
sizeType: ["compressed", "original"],
|
sourceType: ["album", "camera"],
|
success: res => {
|
const tempFilePath = res?.tempFilePaths?.[0];
|
const tempFile = res?.tempFiles?.[0] || {};
|
if (!tempFilePath) return;
|
handleBeforeShipUpload({
|
tempFilePath,
|
path: tempFilePath,
|
type: "image",
|
name: `photo_${Date.now()}.jpg`,
|
size: tempFile.size || 0,
|
createTime: Date.now(),
|
uid: Date.now() + Math.random(),
|
});
|
},
|
fail: () => uni.showToast({ title: "选择图片失败", icon: "none" }),
|
});
|
};
|
|
const previewShipImage = file => {
|
if (!file || !isImageFile(file)) return;
|
const imageUrls = shipFiles.value
|
.filter(f => isImageFile(f))
|
.map(f => getFileAccessUrl(f))
|
.filter(Boolean);
|
const current = getFileAccessUrl(file);
|
if (!imageUrls.length || !current) return;
|
uni.previewImage({ urls: imageUrls, current });
|
};
|
|
const removeShipFile = index => {
|
uni.showModal({
|
title: "确认删除",
|
content: "确定要删除这个文件吗?",
|
success: res => {
|
if (res.confirm) {
|
shipFiles.value.splice(index, 1);
|
uni.showToast({ title: "删除成功", icon: "success" });
|
}
|
},
|
});
|
};
|
|
const getShipTempFileIds = () =>
|
shipFiles.value
|
.map(it => it.tempId ?? it.tempFileId ?? it.id)
|
.filter(v => v !== undefined && v !== null && v !== "");
|
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);
|
}
|
});
|
|
onUnmounted(() => {
|
// 移除事件监听
|
uni.$off("selectContact", handleSelectContact);
|
});
|
const onConfirm = item => {
|
typeValue.value = item.name;
|
showPicker.value = false;
|
if (item.name === "货车") {
|
form.value.expressNumber = "";
|
} else {
|
form.value.shippingCarNumber = "";
|
}
|
};
|
|
const goBack = () => {
|
// 清除本地存储的数据
|
uni.removeStorageSync("operationType");
|
uni.removeStorageSync("invoiceLedgerEditRow");
|
uni.removeStorageSync("approveType");
|
uni.navigateBack();
|
};
|
|
const submitForm = () => {
|
// 检查每个审批步骤是否都有审批人
|
const hasEmptyStep = approverNodes.value.some(step => !step.nickName);
|
if (hasEmptyStep) {
|
showToast("请为每个审批步骤选择审批人");
|
return;
|
}
|
if (shipUploading.value) {
|
showToast("图片正在上传,请稍候");
|
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,
|
shippingCarNumber:
|
typeValue.value === "货车" ? String(form.value.shippingCarNumber || "").trim() : "",
|
expressNumber:
|
typeValue.value === "快递" ? String(form.value.expressNumber || "").trim() : "",
|
tempFileIds: getShipTempFileIds(),
|
};
|
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;
|
}
|
}
|
// 显示通用错误信息
|
uni.showToast({
|
title: "表单校验失败,请检查必填项",
|
icon: "none",
|
});
|
});
|
};
|
|
// 处理联系人选择结果
|
const handleSelectContact = data => {
|
if (data?.source === "scanShip") return;
|
const { stepIndex, contact } = data;
|
// 将选中的联系人设置为对应审批步骤的审批人
|
approverNodes.value[stepIndex].userId = contact.userId;
|
approverNodes.value[stepIndex].nickName = contact.nickName;
|
};
|
|
const addApprover = stepIndex => {
|
// 跳转到联系人选择页面
|
uni.setStorageSync("stepIndex", stepIndex);
|
uni.navigateTo({
|
url: "/pages/cooperativeOffice/collaborativeApproval/contactSelect?approveType=7",
|
});
|
};
|
|
const addApprovalStep = () => {
|
// 添加新的审批步骤
|
approverNodes.value.push({ userId: null, nickName: null });
|
};
|
|
const removeApprover = stepIndex => {
|
// 移除审批人
|
approverNodes.value[stepIndex].userId = null;
|
approverNodes.value[stepIndex].nickName = null;
|
};
|
|
const removeApprovalStep = stepIndex => {
|
// 确保至少保留一个审批步骤
|
if (approverNodes.value.length > 1) {
|
approverNodes.value.splice(stepIndex, 1);
|
} else {
|
uni.showToast({
|
title: "至少需要一个审批步骤",
|
icon: "none",
|
});
|
}
|
};
|
</script>
|
|
<style scoped lang="scss">
|
@import "@/static/scss/form-common.scss";
|
|
.ship-images-card {
|
background: #fff;
|
margin: 16px;
|
border-radius: 16px;
|
padding: 16px;
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
}
|
|
.ship-images-header {
|
margin-bottom: 12px;
|
}
|
|
.ship-images-title {
|
font-size: 16px;
|
font-weight: 600;
|
color: #333;
|
display: block;
|
margin-bottom: 4px;
|
}
|
|
.ship-images-hint {
|
font-size: 12px;
|
color: #999;
|
}
|
|
/* 以下与 inspectionUpload/index.vue 简化上传区、文件列表、视频弹窗一致 */
|
.simple-upload-area {
|
padding: 0;
|
}
|
|
.upload-buttons {
|
display: flex;
|
gap: 10px;
|
margin-bottom: 15px;
|
}
|
|
.upload-progress {
|
margin-bottom: 12px;
|
}
|
|
.file-list {
|
margin-top: 15px;
|
display: flex;
|
flex-wrap: wrap;
|
gap: 12px;
|
}
|
|
.file-item {
|
display: flex;
|
flex-direction: column;
|
align-items: center;
|
background: #fff;
|
border-radius: 12px;
|
padding: 8px;
|
border: 1px solid #e9ecef;
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
width: calc(50% - 6px);
|
min-width: 120px;
|
}
|
|
.file-preview-container {
|
position: relative;
|
margin-bottom: 8px;
|
}
|
|
.file-preview {
|
width: 80px;
|
height: 80px;
|
border-radius: 8px;
|
object-fit: cover;
|
border: 2px solid #f0f0f0;
|
}
|
|
.delete-btn {
|
position: absolute;
|
top: -6px;
|
right: -6px;
|
width: 20px;
|
height: 20px;
|
background: #ff4757;
|
border-radius: 50%;
|
display: flex;
|
align-items: center;
|
justify-content: center;
|
box-shadow: 0 2px 4px rgba(255, 71, 87, 0.3);
|
}
|
|
.file-info {
|
text-align: center;
|
width: 100%;
|
}
|
|
.file-name {
|
font-size: 12px;
|
color: #333;
|
font-weight: 500;
|
display: block;
|
white-space: nowrap;
|
overflow: hidden;
|
text-overflow: ellipsis;
|
max-width: 100px;
|
}
|
|
.file-size {
|
font-size: 10px;
|
color: #999;
|
margin-top: 2px;
|
display: block;
|
}
|
|
.empty-state {
|
text-align: center;
|
padding: 24px 16px;
|
color: #999;
|
font-size: 14px;
|
background: #f8f9fa;
|
border-radius: 8px;
|
border: 2px dashed #ddd;
|
}
|
|
.approval-process {
|
background: #fff;
|
margin: 16px;
|
border-radius: 16px;
|
padding: 16px;
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
}
|
|
.approval-header {
|
margin-bottom: 16px;
|
}
|
|
.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;
|
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; // 确保文字行高一致
|
}
|
|
.approver-item {
|
display: flex;
|
align-items: center;
|
background: linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%);
|
border-radius: 16px;
|
padding: 16px;
|
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;
|
background: #fff;
|
display: flex;
|
justify-content: space-around;
|
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;
|
}
|
50% {
|
transform: scale(1.2);
|
opacity: 0.7;
|
}
|
100% {
|
transform: scale(1);
|
opacity: 1;
|
}
|
}
|
|
@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;
|
font-size: 14px;
|
}
|
</style>
|