chore: sync release updates
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { request, uploadFile } from "../utils/request";
|
||||
import { request, uploadDirectFile, uploadFile } from "../utils/request";
|
||||
import type { AdminSessionInfo } from "../utils/auth";
|
||||
|
||||
export interface AdminLoginResponse {
|
||||
@@ -15,6 +15,56 @@ export interface AdminFileAsset {
|
||||
mime_type?: string;
|
||||
}
|
||||
|
||||
export interface AdminDirectUploadMeta {
|
||||
original_name?: string;
|
||||
file_size?: number;
|
||||
mime_type?: string;
|
||||
}
|
||||
|
||||
export type AdminUploadScene = "appraisal_evidence" | "zhongjian_report" | "warehouse_inbound_evidence" | "warehouse_return_packing";
|
||||
|
||||
export interface AdminDirectUploadPolicy {
|
||||
enabled: boolean;
|
||||
upload_url?: string;
|
||||
form_data?: Record<string, string | number>;
|
||||
asset?: AdminFileAsset;
|
||||
max_size?: number;
|
||||
max_size_text?: string;
|
||||
expires_at?: string;
|
||||
}
|
||||
|
||||
function filenameFromPath(filePath: string) {
|
||||
return filePath.split(/[\\/]/).pop() || `upload-${Date.now()}`;
|
||||
}
|
||||
|
||||
async function uploadManagedAdminFile(
|
||||
filePath: string,
|
||||
scene: AdminUploadScene,
|
||||
fallbackUrl: string,
|
||||
meta: AdminDirectUploadMeta = {},
|
||||
fallbackFormData: Record<string, string | number> = {},
|
||||
) {
|
||||
const policy = await request<AdminDirectUploadPolicy>("/api/admin/file-upload/direct-policy", {
|
||||
method: "POST",
|
||||
data: {
|
||||
upload_scene: scene,
|
||||
original_name: meta.original_name || filenameFromPath(filePath),
|
||||
file_size: meta.file_size || 0,
|
||||
mime_type: meta.mime_type || "",
|
||||
},
|
||||
});
|
||||
const formData = { ...fallbackFormData, upload_scene: scene };
|
||||
if (!policy.enabled) {
|
||||
return uploadFile<AdminFileAsset>(fallbackUrl, filePath, formData);
|
||||
}
|
||||
if (!policy.upload_url || !policy.form_data || !policy.asset) {
|
||||
throw new Error("OSS 上传签名无效,请稍后重试");
|
||||
}
|
||||
|
||||
await uploadDirectFile(policy.upload_url, filePath, policy.form_data);
|
||||
return policy.asset;
|
||||
}
|
||||
|
||||
export interface PaginatedList<T> {
|
||||
list: T[];
|
||||
total?: number;
|
||||
@@ -445,8 +495,13 @@ export const adminApi = {
|
||||
data,
|
||||
});
|
||||
},
|
||||
uploadWarehouseInboundEvidenceFile(filePath: string) {
|
||||
return uploadFile<AdminFileAsset>("/api/admin/warehouse-workbench/inbound/evidence/upload", filePath);
|
||||
uploadWarehouseInboundEvidenceFile(filePath: string, meta: AdminDirectUploadMeta = {}) {
|
||||
return uploadManagedAdminFile(
|
||||
filePath,
|
||||
"warehouse_inbound_evidence",
|
||||
"/api/admin/warehouse-workbench/inbound/evidence/upload",
|
||||
meta,
|
||||
);
|
||||
},
|
||||
lookupZhongjianWarehouseTransfer(internalTagNo: string) {
|
||||
return request<AdminWarehouseWorkbenchContext>("/api/admin/warehouse-workbench/zhongjian/lookup", {
|
||||
@@ -488,8 +543,13 @@ export const adminApi = {
|
||||
data: { internal_tag_no: internalTagNo },
|
||||
});
|
||||
},
|
||||
uploadWarehouseReturnPackingFile(filePath: string) {
|
||||
return uploadFile<AdminFileAsset>("/api/admin/warehouse-workbench/return/packing/upload", filePath);
|
||||
uploadWarehouseReturnPackingFile(filePath: string, meta: AdminDirectUploadMeta = {}) {
|
||||
return uploadManagedAdminFile(
|
||||
filePath,
|
||||
"warehouse_return_packing",
|
||||
"/api/admin/warehouse-workbench/return/packing/upload",
|
||||
meta,
|
||||
);
|
||||
},
|
||||
shipWarehouseReturn(data: { internal_tag_no: string; express_company: string; tracking_no: string; packing_attachments?: AdminFileAsset[] }) {
|
||||
return request<AdminWarehouseWorkbenchContext>("/api/admin/warehouse-workbench/return/ship", {
|
||||
@@ -530,8 +590,14 @@ export const adminApi = {
|
||||
data,
|
||||
});
|
||||
},
|
||||
uploadAppraisalEvidenceFile(filePath: string, taskId?: number) {
|
||||
return uploadFile<AdminFileAsset>("/api/admin/appraisal-task/evidence/upload", filePath, taskId ? { task_id: taskId } : {});
|
||||
uploadAppraisalEvidenceFile(
|
||||
filePath: string,
|
||||
taskId?: number,
|
||||
meta: AdminDirectUploadMeta = {},
|
||||
scene: AdminUploadScene = "appraisal_evidence",
|
||||
) {
|
||||
const formData: Record<string, string | number> = taskId ? { task_id: taskId } : {};
|
||||
return uploadManagedAdminFile(filePath, scene, "/api/admin/appraisal-task/evidence/upload", meta, formData);
|
||||
},
|
||||
deleteAppraisalEvidenceFile(fileUrl: string, taskId?: number) {
|
||||
return request<{ file_url: string }>("/api/admin/appraisal-task/evidence/delete", {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { computed, ref } from "vue";
|
||||
import { onLoad, onShow } from "@dcloudio/uni-app";
|
||||
import { adminApi, type AdminFileAsset, type AdminOrderDetail } from "../../api/admin";
|
||||
import { showErrorToast } from "../../utils/feedback";
|
||||
import { showErrorToast, showInfoToast } from "../../utils/feedback";
|
||||
|
||||
const loading = ref(false);
|
||||
const pageReady = ref(false);
|
||||
@@ -40,6 +40,16 @@ function openReportDetail() {
|
||||
uni.navigateTo({ url: `/pages/report/detail?id=${reportId}` });
|
||||
}
|
||||
|
||||
function copyOrderNo() {
|
||||
const orderNo = detail.value?.order_info.order_no || "";
|
||||
if (!orderNo) return;
|
||||
uni.setClipboardData({
|
||||
data: orderNo,
|
||||
success: () => showInfoToast("订单号已复制"),
|
||||
fail: () => showInfoToast("复制失败,请重试"),
|
||||
});
|
||||
}
|
||||
|
||||
function formatMoney(value?: number) {
|
||||
const amount = Number(value || 0);
|
||||
return `¥${amount.toFixed(2)}`;
|
||||
@@ -105,7 +115,15 @@ onShow(() => {
|
||||
<template v-else-if="detail">
|
||||
<view class="hero">
|
||||
<view class="eyebrow">订单详情</view>
|
||||
<view class="title">{{ pageTitle }}</view>
|
||||
<view class="order-title-row">
|
||||
<view class="title order-title">{{ pageTitle }}</view>
|
||||
<button class="copy-order-button" aria-label="复制订单号" hover-class="copy-order-button--active" @click.stop="copyOrderNo">
|
||||
<view class="copy-order-button__icon">
|
||||
<view class="copy-order-button__sheet copy-order-button__sheet--back"></view>
|
||||
<view class="copy-order-button__sheet copy-order-button__sheet--front"></view>
|
||||
</view>
|
||||
</button>
|
||||
</view>
|
||||
<view class="subtitle">{{ detail.order_info.appraisal_no }}</view>
|
||||
</view>
|
||||
|
||||
@@ -261,6 +279,65 @@ onShow(() => {
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.order-title-row {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 14rpx;
|
||||
margin-top: 18rpx;
|
||||
}
|
||||
|
||||
.order-title {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
margin-top: 0;
|
||||
overflow-wrap: anywhere;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.copy-order-button {
|
||||
position: relative;
|
||||
flex: 0 0 auto;
|
||||
width: 60rpx;
|
||||
height: 60rpx;
|
||||
margin-top: 2rpx;
|
||||
border: 1px solid var(--work-border);
|
||||
border-radius: var(--work-radius-sm);
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.copy-order-button--active {
|
||||
background: var(--work-card-muted);
|
||||
}
|
||||
|
||||
.copy-order-button__icon {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
width: 36rpx;
|
||||
height: 40rpx;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
.copy-order-button__sheet {
|
||||
position: absolute;
|
||||
width: 26rpx;
|
||||
height: 30rpx;
|
||||
border: 3rpx solid var(--work-text);
|
||||
border-radius: 5rpx;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.copy-order-button__sheet--back {
|
||||
left: 1rpx;
|
||||
top: 1rpx;
|
||||
border-color: var(--work-text-soft);
|
||||
}
|
||||
|
||||
.copy-order-button__sheet--front {
|
||||
right: 1rpx;
|
||||
bottom: 1rpx;
|
||||
}
|
||||
|
||||
.timeline {
|
||||
display: grid;
|
||||
gap: 16rpx;
|
||||
|
||||
@@ -66,6 +66,11 @@ const resultMetaItems = computed(() => {
|
||||
items.push({ label: "估值", value: [range, valuationDesc].filter(Boolean).join(";") || "-" });
|
||||
}
|
||||
|
||||
const externalRemark = textValue(result.external_remark);
|
||||
if (externalRemark) {
|
||||
items.push({ label: "备注", value: externalRemark });
|
||||
}
|
||||
|
||||
return items;
|
||||
});
|
||||
const evidenceAttachments = computed(() => detail.value?.evidence_attachments || []);
|
||||
|
||||
@@ -120,11 +120,18 @@ async function choosePackingVideo() {
|
||||
try {
|
||||
const result = await uni.chooseVideo({
|
||||
sourceType: ["album", "camera"],
|
||||
compressed: true,
|
||||
maxDuration: 600,
|
||||
});
|
||||
const filePath = result.tempFilePath;
|
||||
if (!filePath) return;
|
||||
uploading.value = true;
|
||||
const asset = await adminApi.uploadWarehouseReturnPackingFile(filePath);
|
||||
const tempFile = result.tempFile as File | undefined;
|
||||
const asset = await adminApi.uploadWarehouseReturnPackingFile(filePath, {
|
||||
original_name: tempFile?.name,
|
||||
file_size: Number(result.size || tempFile?.size || 0),
|
||||
mime_type: tempFile?.type || "video/mp4",
|
||||
});
|
||||
packingAttachments.value.push(asset);
|
||||
showInfoToast("视频上传成功");
|
||||
} catch (error) {
|
||||
|
||||
@@ -250,11 +250,18 @@ async function chooseInboundVideo() {
|
||||
try {
|
||||
const result = await uni.chooseVideo({
|
||||
sourceType: ["album", "camera"],
|
||||
compressed: true,
|
||||
maxDuration: 600,
|
||||
});
|
||||
const filePath = result.tempFilePath;
|
||||
if (!filePath) return;
|
||||
uploadingInbound.value = true;
|
||||
const asset = await adminApi.uploadWarehouseInboundEvidenceFile(filePath);
|
||||
const tempFile = result.tempFile as File | undefined;
|
||||
const asset = await adminApi.uploadWarehouseInboundEvidenceFile(filePath, {
|
||||
original_name: tempFile?.name,
|
||||
file_size: Number(result.size || tempFile?.size || 0),
|
||||
mime_type: tempFile?.type || "video/mp4",
|
||||
});
|
||||
inboundAttachments.value.push(asset);
|
||||
showInfoToast("视频上传成功");
|
||||
} catch (error) {
|
||||
|
||||
@@ -238,13 +238,30 @@ function updateTemplatePoint(index: number, key: "point_value" | "point_remark",
|
||||
current[key] = value;
|
||||
}
|
||||
|
||||
function inputEventValue(event: unknown) {
|
||||
if (typeof event === "string" || typeof event === "number") {
|
||||
return String(event);
|
||||
}
|
||||
|
||||
const inputEvent = event as {
|
||||
detail?: { value?: unknown };
|
||||
target?: { value?: unknown };
|
||||
};
|
||||
if (inputEvent?.detail && "value" in inputEvent.detail) {
|
||||
return String(inputEvent.detail.value ?? "");
|
||||
}
|
||||
if (inputEvent?.target && "value" in inputEvent.target) {
|
||||
return String(inputEvent.target.value ?? "");
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
function updateTemplatePointFromInput(
|
||||
index: number,
|
||||
key: "point_value" | "point_remark",
|
||||
event: Event,
|
||||
event: unknown,
|
||||
) {
|
||||
const target = event.target as HTMLInputElement | HTMLTextAreaElement | null;
|
||||
updateTemplatePoint(index, key, target?.value || "");
|
||||
updateTemplatePoint(index, key, inputEventValue(event));
|
||||
}
|
||||
|
||||
function templateKeyPointsPayload() {
|
||||
@@ -256,6 +273,15 @@ function templateKeyPointsPayload() {
|
||||
})) || [];
|
||||
}
|
||||
|
||||
function validateRequiredTemplatePoints() {
|
||||
const missing = detail.value?.appraisal_template?.key_points?.find((item) => item.is_required && !String(item.point_value || "").trim());
|
||||
if (!missing) {
|
||||
return true;
|
||||
}
|
||||
showInfoToast(`请填写鉴定模板项:${missing.point_name}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
function returnToWorkOrders(message: string) {
|
||||
showInfoToast(message);
|
||||
setTimeout(() => {
|
||||
@@ -375,11 +401,18 @@ async function chooseEvidenceVideo() {
|
||||
try {
|
||||
const result = await uni.chooseVideo({
|
||||
sourceType: ["album", "camera"],
|
||||
compressed: true,
|
||||
maxDuration: 600,
|
||||
});
|
||||
const filePath = result.tempFilePath;
|
||||
if (!filePath) return;
|
||||
uploading.value = true;
|
||||
const asset = await adminApi.uploadAppraisalEvidenceFile(filePath, detail.value.task_info.id);
|
||||
const tempFile = result.tempFile as File | undefined;
|
||||
const asset = await adminApi.uploadAppraisalEvidenceFile(filePath, detail.value.task_info.id, {
|
||||
original_name: tempFile?.name,
|
||||
file_size: Number(result.size || tempFile?.size || 0),
|
||||
mime_type: tempFile?.type || "video/mp4",
|
||||
});
|
||||
evidenceFiles.value.push(asset);
|
||||
showInfoToast("视频上传成功");
|
||||
} catch (error) {
|
||||
@@ -403,7 +436,7 @@ async function chooseZhongjianImage() {
|
||||
if (!result.tempFilePaths?.length) return;
|
||||
uploading.value = true;
|
||||
for (const filePath of result.tempFilePaths) {
|
||||
const asset = await adminApi.uploadAppraisalEvidenceFile(filePath, detail.value.task_info.id);
|
||||
const asset = await adminApi.uploadAppraisalEvidenceFile(filePath, detail.value.task_info.id, {}, "zhongjian_report");
|
||||
zhongjianFiles.value.push(asset);
|
||||
}
|
||||
showInfoToast("图片上传成功");
|
||||
@@ -422,11 +455,23 @@ async function chooseZhongjianVideo() {
|
||||
try {
|
||||
const result = await uni.chooseVideo({
|
||||
sourceType: ["album", "camera"],
|
||||
compressed: true,
|
||||
maxDuration: 600,
|
||||
});
|
||||
const filePath = result.tempFilePath;
|
||||
if (!filePath) return;
|
||||
uploading.value = true;
|
||||
const asset = await adminApi.uploadAppraisalEvidenceFile(filePath, detail.value.task_info.id);
|
||||
const tempFile = result.tempFile as File | undefined;
|
||||
const asset = await adminApi.uploadAppraisalEvidenceFile(
|
||||
filePath,
|
||||
detail.value.task_info.id,
|
||||
{
|
||||
original_name: tempFile?.name,
|
||||
file_size: Number(result.size || tempFile?.size || 0),
|
||||
mime_type: tempFile?.type || "video/mp4",
|
||||
},
|
||||
"zhongjian_report",
|
||||
);
|
||||
zhongjianFiles.value.push(asset);
|
||||
showInfoToast("视频上传成功");
|
||||
} catch (error) {
|
||||
@@ -452,6 +497,13 @@ async function submitResult(action: "save" | "submit") {
|
||||
showInfoToast("请先填写鉴定结论");
|
||||
return;
|
||||
}
|
||||
if (action === "submit" && !productName.value.trim() && !categoryName.value.trim() && !brandName.value.trim()) {
|
||||
showInfoToast("请先完善物品信息");
|
||||
return;
|
||||
}
|
||||
if (action === "submit" && !validateRequiredTemplatePoints()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const qrInput = action === "submit" ? await confirmAndScanMaterialTag() : "";
|
||||
if (action === "submit" && !qrInput) {
|
||||
@@ -487,12 +539,12 @@ async function submitResult(action: "save" | "submit") {
|
||||
action,
|
||||
product_info: {
|
||||
category_id: detail.value!.product_info.category_id,
|
||||
product_name: detail.value!.product_info.product_name,
|
||||
category_name: detail.value!.product_info.category_name,
|
||||
brand_name: detail.value!.product_info.brand_name,
|
||||
color: detail.value!.product_info.color,
|
||||
size_spec: detail.value!.product_info.size_spec,
|
||||
serial_no: detail.value!.product_info.serial_no,
|
||||
product_name: productName.value.trim(),
|
||||
category_name: categoryName.value.trim(),
|
||||
brand_name: brandName.value.trim(),
|
||||
color: color.value.trim(),
|
||||
size_spec: sizeSpec.value.trim(),
|
||||
serial_no: serialNo.value.trim(),
|
||||
},
|
||||
result_text: resultText.value.trim(),
|
||||
result_desc: resultDesc.value.trim(),
|
||||
@@ -573,6 +625,9 @@ async function submitZhongjianReport() {
|
||||
showInfoToast("请先完善物品信息");
|
||||
return;
|
||||
}
|
||||
if (!validateRequiredTemplatePoints()) {
|
||||
return;
|
||||
}
|
||||
if (!zhongjianFiles.value.length) {
|
||||
showInfoToast("请至少上传 1 个中检报告文件");
|
||||
return;
|
||||
@@ -691,6 +746,16 @@ onShow(() => {
|
||||
<view v-if="activeSection === 'result' && !isZhongjian" class="card">
|
||||
<view class="card-title">鉴定结论</view>
|
||||
<view class="stack" style="margin-top: 18rpx">
|
||||
<view class="card-desc">报告展示信息</view>
|
||||
<input v-model="productName" class="field" :disabled="isTaskReadonly" placeholder="产品名称" />
|
||||
<input v-model="categoryName" class="field" :disabled="isTaskReadonly" placeholder="品类" />
|
||||
<input v-model="brandName" class="field" :disabled="isTaskReadonly" placeholder="品牌" />
|
||||
<view class="meta-grid">
|
||||
<input v-model="color" class="field" :disabled="isTaskReadonly" placeholder="颜色" />
|
||||
<input v-model="sizeSpec" class="field" :disabled="isTaskReadonly" placeholder="规格 / 尺寸" />
|
||||
</view>
|
||||
<input v-model="serialNo" class="field" :disabled="isTaskReadonly" placeholder="序列号 / 编码" />
|
||||
<view class="card-desc">鉴定结果</view>
|
||||
<input v-model="resultText" class="field" :disabled="isTaskReadonly" placeholder="结论,例如:正品 / 存疑" />
|
||||
<textarea v-model="resultDesc" class="textarea" :disabled="isTaskReadonly" placeholder="结论说明" />
|
||||
<template v-if="showConditionFields">
|
||||
|
||||
@@ -3,6 +3,9 @@ import { resolveApiBaseUrl } from "./env";
|
||||
|
||||
const BASE_URL = resolveApiBaseUrl().replace(/\/$/, "");
|
||||
const REQUEST_TIMEOUT_MS = 15000;
|
||||
const UPLOAD_TIMEOUT_MS = 300000;
|
||||
export const UPLOAD_FILE_SIZE_LIMIT_BYTES = 50 * 1024 * 1024;
|
||||
export const UPLOAD_FILE_SIZE_LIMIT_TEXT = "50MB";
|
||||
|
||||
type RequestMethod = "GET" | "POST" | "PUT" | "DELETE" | "OPTIONS" | "HEAD";
|
||||
|
||||
@@ -31,6 +34,10 @@ function buildNetworkError(error: UniApp.GeneralCallbackResult, fallback = "网
|
||||
return new Error("连接服务器超时,请检查网络,或联系管理员确认 API 服务是否正常");
|
||||
}
|
||||
|
||||
if (normalized.includes("413") || normalized.includes("too large") || errMsg.includes("过大")) {
|
||||
return new Error(`上传文件过大,请压缩到 ${UPLOAD_FILE_SIZE_LIMIT_TEXT} 以内后重试`);
|
||||
}
|
||||
|
||||
if (normalized.includes("abort")) {
|
||||
return new Error("请求已取消");
|
||||
}
|
||||
@@ -90,6 +97,9 @@ export function parseUploadResponse<T>(
|
||||
try {
|
||||
payload = JSON.parse(response.data) as ApiResponse<T>;
|
||||
} catch {
|
||||
if (response.statusCode === 413) {
|
||||
throw new Error(`上传文件过大,请压缩到 ${UPLOAD_FILE_SIZE_LIMIT_TEXT} 以内后重试`);
|
||||
}
|
||||
if (response.statusCode >= 500) {
|
||||
throw new Error("服务器上传处理异常,请稍后重试");
|
||||
}
|
||||
@@ -117,6 +127,7 @@ export function uploadFile<T>(url: string, filePath: string, formData: Record<st
|
||||
name: "file",
|
||||
header: buildAuthHeaders(),
|
||||
formData,
|
||||
timeout: UPLOAD_TIMEOUT_MS,
|
||||
success: (response) => {
|
||||
try {
|
||||
resolve(parseUploadResponse<T>(response));
|
||||
@@ -128,3 +139,27 @@ export function uploadFile<T>(url: string, filePath: string, formData: Record<st
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function uploadDirectFile(uploadUrl: string, filePath: string, formData: Record<string, string | number>) {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
uni.uploadFile({
|
||||
url: uploadUrl,
|
||||
filePath,
|
||||
name: "file",
|
||||
formData,
|
||||
timeout: UPLOAD_TIMEOUT_MS,
|
||||
success: (response) => {
|
||||
if (response.statusCode >= 200 && response.statusCode < 300) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
if (response.statusCode === 413) {
|
||||
reject(new Error("上传文件过大,请压缩后重试"));
|
||||
return;
|
||||
}
|
||||
reject(new Error("OSS 上传失败,请稍后重试"));
|
||||
},
|
||||
fail: (error) => reject(buildNetworkError(error, "OSS 上传失败,请稍后重试")),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user