触发条件接入 AI 助手(选模型、发送 skill、一键填入);预报时机并入下单并支持公式验证;下单失败可继续修改或存草稿;补充订单附件、H5 下单与渠道买价相关优化。 Co-authored-by: Cursor <cursoragent@cursor.com>
343 lines
8.4 KiB
Vue
343 lines
8.4 KiB
Vue
<template>
|
|
<div class="order-attachments-tab">
|
|
<a-spin :loading="loading" class="attachments-spin">
|
|
<div
|
|
v-for="section in visibleSections"
|
|
:key="section.type"
|
|
class="attach-section"
|
|
>
|
|
<div class="attach-section-header">
|
|
<div class="attach-section-title-wrap">
|
|
<span class="attach-section-title">{{ $t(section.labelKey) }}</span>
|
|
<span class="attach-section-count">
|
|
{{ section.files.length }}/{{ section.maxCount }}
|
|
</span>
|
|
</div>
|
|
<a-upload
|
|
v-if="canUpload"
|
|
:accept="section.accept"
|
|
:multiple="section.maxCount > 1"
|
|
:show-file-list="false"
|
|
:disabled="uploadingType === section.type || section.files.length >= section.maxCount"
|
|
:custom-request="(opt) => handleUpload(section.type, opt)"
|
|
>
|
|
<a-button
|
|
type="outline"
|
|
size="small"
|
|
:loading="uploadingType === section.type"
|
|
:disabled="section.files.length >= section.maxCount"
|
|
>
|
|
{{ $t('order.myOrders.drawer.attachments.upload') }}
|
|
</a-button>
|
|
</a-upload>
|
|
</div>
|
|
|
|
<div v-if="section.files.length" class="attach-file-list">
|
|
<div
|
|
v-for="file in section.files"
|
|
:key="file.id"
|
|
class="attach-file-item"
|
|
>
|
|
<a
|
|
class="attach-file-name"
|
|
:href="resolveUrl(file.fileUrl)"
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
>
|
|
{{ file.fileName || file.fileUrl }}
|
|
</a>
|
|
<div class="attach-file-meta">
|
|
<span>{{ formatTime(file.createTime) }}</span>
|
|
<span v-if="file.source">{{ $t(`order.myOrders.drawer.attachments.source.${file.source}`, file.source) }}</span>
|
|
</div>
|
|
<div class="attach-file-actions">
|
|
<a-button
|
|
type="text"
|
|
size="mini"
|
|
@click="openFile(file.fileUrl)"
|
|
>
|
|
{{ $t('order.myOrders.drawer.attachments.preview') }}
|
|
</a-button>
|
|
<a-button
|
|
v-if="canUpload"
|
|
type="text"
|
|
size="mini"
|
|
status="danger"
|
|
:loading="deletingId === file.id"
|
|
@click="handleDelete(file.id)"
|
|
>
|
|
{{ $t('order.myOrders.drawer.attachments.delete') }}
|
|
</a-button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<a-empty
|
|
v-else
|
|
:description="$t('order.myOrders.drawer.attachments.empty')"
|
|
:image-style="{ height: '48px' }"
|
|
class="attach-empty"
|
|
/>
|
|
</div>
|
|
</a-spin>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { computed, ref, watch } from 'vue';
|
|
import { useI18n } from 'vue-i18n';
|
|
import { Message, Modal } from '@arco-design/web-vue';
|
|
import {
|
|
deleteOrderFile,
|
|
getOrderFiles,
|
|
uploadOrderFiles,
|
|
type OrderFileItem,
|
|
} from '@/api/order';
|
|
import { resolveOmsAssetUrl } from '@/api/oms/common';
|
|
|
|
const FILE_TYPE_DEFS = [
|
|
{
|
|
type: 'tradeProof',
|
|
labelKey: 'order.myOrders.drawer.attachments.types.tradeProof',
|
|
maxCount: 20,
|
|
accept: '.jpg,.jpeg,.png,.webp,.gif,.bmp,.pdf',
|
|
},
|
|
{
|
|
type: 'systemLabel',
|
|
labelKey: 'order.myOrders.drawer.attachments.types.systemLabel',
|
|
maxCount: 1,
|
|
accept: '.pdf,.jpg,.jpeg,.png',
|
|
},
|
|
{
|
|
type: 'downstreamLabel',
|
|
labelKey: 'order.myOrders.drawer.attachments.types.downstreamLabel',
|
|
maxCount: 20,
|
|
accept: '.pdf,.jpg,.jpeg,.png',
|
|
},
|
|
{
|
|
type: 'lastMileLabel',
|
|
labelKey: 'order.myOrders.drawer.attachments.types.lastMileLabel',
|
|
maxCount: 20,
|
|
accept: '.pdf,.jpg,.jpeg,.png',
|
|
},
|
|
{
|
|
type: 'infraredImage',
|
|
labelKey: 'order.myOrders.drawer.attachments.types.infraredImage',
|
|
maxCount: 20,
|
|
accept: '.jpg,.jpeg,.png,.webp,.gif,.bmp,.pdf',
|
|
},
|
|
] as const;
|
|
|
|
const props = withDefaults(
|
|
defineProps<{
|
|
orderId?: number | null;
|
|
active?: boolean;
|
|
/** 客户门户:只读系统面单 */
|
|
readOnly?: boolean;
|
|
customerView?: boolean;
|
|
}>(),
|
|
{
|
|
orderId: null,
|
|
active: false,
|
|
readOnly: false,
|
|
customerView: false,
|
|
}
|
|
);
|
|
|
|
const { t } = useI18n();
|
|
const loading = ref(false);
|
|
const uploadingType = ref<string | null>(null);
|
|
const deletingId = ref<number | null>(null);
|
|
const groups = ref<Record<string, OrderFileItem[]>>({});
|
|
|
|
const canUpload = computed(() => !props.readOnly && !props.customerView);
|
|
|
|
const visibleSections = computed(() => {
|
|
const defs = props.customerView
|
|
? FILE_TYPE_DEFS.filter((d) => d.type === 'systemLabel')
|
|
: FILE_TYPE_DEFS;
|
|
return defs.map((def) => ({
|
|
...def,
|
|
files: groups.value[def.type] ?? [],
|
|
}));
|
|
});
|
|
|
|
function resolveUrl(path?: string) {
|
|
return resolveOmsAssetUrl(path);
|
|
}
|
|
|
|
function openFile(path?: string) {
|
|
const url = resolveUrl(path);
|
|
if (!url) return;
|
|
window.open(url, '_blank', 'noopener,noreferrer');
|
|
}
|
|
|
|
function formatTime(value?: string) {
|
|
if (!value) return '';
|
|
return String(value).replace('T', ' ').slice(0, 19);
|
|
}
|
|
|
|
async function loadFiles() {
|
|
if (!props.orderId) {
|
|
groups.value = {};
|
|
return;
|
|
}
|
|
loading.value = true;
|
|
try {
|
|
const data = await getOrderFiles(props.orderId);
|
|
groups.value = data.groups ?? {};
|
|
} catch (e: any) {
|
|
groups.value = {};
|
|
Message.error(e?.message || t('order.myOrders.drawer.attachments.loadFailed'));
|
|
} finally {
|
|
loading.value = false;
|
|
}
|
|
}
|
|
|
|
async function handleUpload(fileType: string, options: any) {
|
|
const file = options?.fileItem?.file ?? options?.file;
|
|
if (!file || !props.orderId) {
|
|
options?.onError?.(new Error('no file'));
|
|
return;
|
|
}
|
|
uploadingType.value = fileType;
|
|
try {
|
|
await uploadOrderFiles(props.orderId, fileType, [file as File]);
|
|
options?.onSuccess?.();
|
|
Message.success(t('order.myOrders.drawer.attachments.uploadSuccess'));
|
|
await loadFiles();
|
|
} catch (e: any) {
|
|
options?.onError?.(e);
|
|
Message.error(e?.message || t('order.myOrders.drawer.attachments.uploadFailed'));
|
|
} finally {
|
|
uploadingType.value = null;
|
|
}
|
|
}
|
|
|
|
function handleDelete(id: number) {
|
|
Modal.confirm({
|
|
title: t('order.myOrders.drawer.attachments.deleteConfirmTitle'),
|
|
content: t('order.myOrders.drawer.attachments.deleteConfirm'),
|
|
onOk: async () => {
|
|
deletingId.value = id;
|
|
try {
|
|
await deleteOrderFile(id);
|
|
Message.success(t('order.myOrders.drawer.attachments.deleteSuccess'));
|
|
await loadFiles();
|
|
} catch (e: any) {
|
|
Message.error(e?.message || t('order.myOrders.drawer.attachments.deleteFailed'));
|
|
} finally {
|
|
deletingId.value = null;
|
|
}
|
|
},
|
|
});
|
|
}
|
|
|
|
watch(
|
|
() => [props.active, props.orderId] as const,
|
|
([active, orderId]) => {
|
|
if (active && orderId) {
|
|
void loadFiles();
|
|
}
|
|
},
|
|
{ immediate: true }
|
|
);
|
|
</script>
|
|
|
|
<style scoped lang="less">
|
|
.order-attachments-tab {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 16px;
|
|
min-height: 200px;
|
|
}
|
|
|
|
.attachments-spin {
|
|
width: 100%;
|
|
}
|
|
|
|
.attach-section {
|
|
background: #fff;
|
|
border: 1px solid var(--color-border-2);
|
|
border-radius: 4px;
|
|
padding: 12px 16px;
|
|
}
|
|
|
|
.attach-section-header {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 12px;
|
|
margin-bottom: 12px;
|
|
}
|
|
|
|
.attach-section-title-wrap {
|
|
display: flex;
|
|
align-items: baseline;
|
|
gap: 8px;
|
|
}
|
|
|
|
.attach-section-title {
|
|
font-size: 14px;
|
|
font-weight: 600;
|
|
color: var(--color-text-1);
|
|
}
|
|
|
|
.attach-section-count {
|
|
font-size: 12px;
|
|
color: var(--color-text-3);
|
|
}
|
|
|
|
.attach-file-list {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 8px;
|
|
}
|
|
|
|
.attach-file-item {
|
|
display: grid;
|
|
grid-template-columns: minmax(0, 1fr) auto auto;
|
|
gap: 8px 12px;
|
|
align-items: center;
|
|
padding: 8px 10px;
|
|
background: #f7f8fa;
|
|
border-radius: 4px;
|
|
}
|
|
|
|
.attach-file-name {
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
color: var(--app-primary-color);
|
|
font-size: 13px;
|
|
}
|
|
|
|
.attach-file-meta {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 2px;
|
|
font-size: 12px;
|
|
color: var(--color-text-3);
|
|
text-align: right;
|
|
}
|
|
|
|
.attach-file-actions {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 4px;
|
|
}
|
|
|
|
.attach-empty {
|
|
margin: 8px 0;
|
|
}
|
|
|
|
@media (max-width: 720px) {
|
|
.attach-file-item {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
|
|
.attach-file-meta {
|
|
text-align: left;
|
|
}
|
|
}
|
|
</style>
|