主仓只保留代码:移出 contracts/deploy/docs 与模板监听包/原型图,并提交后台工单页改动。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -11,8 +11,8 @@ const props = withDefaults(
|
||||
open: boolean;
|
||||
title: string;
|
||||
subtitle?: string;
|
||||
/** sm=400 md=480 lg=600 xl=680 */
|
||||
size?: "sm" | "md" | "lg" | "xl";
|
||||
/** sm=400 md=480 lg=600 xl=680 xxl=880(工单详情对齐原型 modal-xl) */
|
||||
size?: "sm" | "md" | "lg" | "xl" | "xxl";
|
||||
okText?: string;
|
||||
cancelText?: string;
|
||||
showFooter?: boolean;
|
||||
@@ -34,7 +34,7 @@ const emit = defineEmits<{
|
||||
}>();
|
||||
|
||||
const dialogClass = computed(() => {
|
||||
const map = { sm: "is-sm", md: "is-md", lg: "is-lg", xl: "is-xl" };
|
||||
const map = { sm: "is-sm", md: "is-md", lg: "is-lg", xl: "is-xl", xxl: "is-xxl" };
|
||||
return ["admin-modal-dialog", map[props.size]];
|
||||
});
|
||||
|
||||
@@ -71,7 +71,11 @@ onUnmounted(() => {
|
||||
<div :class="dialogClass" @click.stop>
|
||||
<header class="admin-modal-head">
|
||||
<div class="admin-modal-head-text">
|
||||
<div class="admin-modal-title">{{ title }}</div>
|
||||
<div class="admin-modal-title">
|
||||
<span>{{ title }}</span>
|
||||
<!-- titleExtra:工单号徽章等,不占副标题行 -->
|
||||
<slot name="titleExtra" />
|
||||
</div>
|
||||
<div v-if="subtitle" class="admin-modal-sub">{{ subtitle }}</div>
|
||||
</div>
|
||||
<button type="button" class="btn btn-ghost admin-modal-close" aria-label="关闭" @click="close">
|
||||
|
||||
@@ -826,7 +826,14 @@ body {
|
||||
max-height: min(640px, calc(100vh - 32px)) !important;
|
||||
}
|
||||
|
||||
.admin-modal-dialog.is-xxl {
|
||||
max-height: min(88vh, calc(100vh - 32px)) !important;
|
||||
}
|
||||
|
||||
.admin-modal-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 13px !important;
|
||||
}
|
||||
|
||||
@@ -840,6 +847,12 @@ body {
|
||||
gap: 10px !important;
|
||||
}
|
||||
|
||||
.admin-modal-body:has(.ticket-detail) {
|
||||
display: block;
|
||||
padding: 0 !important;
|
||||
gap: 0 !important;
|
||||
}
|
||||
|
||||
.admin-modal-footer {
|
||||
padding: 10px 16px !important;
|
||||
}
|
||||
|
||||
@@ -1539,6 +1539,9 @@ textarea:focus-visible {
|
||||
.admin-modal-dialog.is-xl {
|
||||
max-width: 680px;
|
||||
}
|
||||
.admin-modal-dialog.is-xxl {
|
||||
max-width: 880px;
|
||||
}
|
||||
|
||||
.admin-modal-head {
|
||||
display: flex;
|
||||
@@ -1552,6 +1555,9 @@ textarea:focus-visible {
|
||||
}
|
||||
|
||||
.admin-modal-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 13px;
|
||||
font-weight: 800;
|
||||
color: #0f172a;
|
||||
@@ -2223,3 +2229,166 @@ textarea:focus-visible {
|
||||
font-weight: 700;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
/* 工单详情:对齐原型 admin-inquiry-tickets 分节网格 */
|
||||
.ticket-detail-no {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
border-radius: 6px;
|
||||
background: #eff6ff;
|
||||
color: #1d4ed8;
|
||||
border: 1px solid #bfdbfe;
|
||||
padding: 1px 8px;
|
||||
font-size: 11px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.ticket-detail {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.admin-modal-body:has(.ticket-detail) {
|
||||
display: block;
|
||||
padding: 0 !important;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.ticket-detail-section {
|
||||
padding: 14px 16px;
|
||||
border-bottom: 1px solid #eef1f5;
|
||||
}
|
||||
|
||||
.ticket-detail-section:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.ticket-detail-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin-bottom: 10px;
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
color: #101828;
|
||||
}
|
||||
|
||||
.ticket-detail-title .proto-icon {
|
||||
color: #155eef;
|
||||
}
|
||||
|
||||
.ticket-detail-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 8px 20px;
|
||||
}
|
||||
|
||||
.ticket-detail-item {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.ticket-detail-item label {
|
||||
flex-shrink: 0;
|
||||
color: #98a2b3;
|
||||
font-weight: 700;
|
||||
white-space: nowrap;
|
||||
min-width: 72px;
|
||||
}
|
||||
|
||||
.ticket-detail-item span {
|
||||
min-width: 0;
|
||||
color: #1d2939;
|
||||
font-weight: 700;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.ticket-detail-demand {
|
||||
margin-top: 10px;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.ticket-detail-note {
|
||||
margin-top: 8px;
|
||||
font-size: 11px;
|
||||
color: #475569;
|
||||
white-space: pre-line;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.ticket-detail-attach {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.ticket-detail-attach th {
|
||||
text-align: left;
|
||||
color: #667085;
|
||||
font-weight: 800;
|
||||
padding: 0 8px 8px 0;
|
||||
}
|
||||
|
||||
.ticket-detail-attach td {
|
||||
padding: 6px 8px 6px 0;
|
||||
color: #334155;
|
||||
font-weight: 700;
|
||||
border-top: 1px solid #eef1f5;
|
||||
}
|
||||
|
||||
.ticket-detail-attach-name {
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.ticket-detail-timeline {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.ticket-detail-tl-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.ticket-detail-tl-item b {
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.ticket-detail-tl-time {
|
||||
flex-shrink: 0;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
color: #94a3b8;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.ticket-detail-empty {
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.ticket-detail-foot {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.ticket-detail-updated {
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
/**
|
||||
* 询价工单列表:对接主账 /inquiry/workOrder。
|
||||
* 六态关单/重试必须调主账;禁止本地改 status 冒充成功。
|
||||
* 详情/关单弹窗必须用 AdminModal:本工程 ant-design-vue 3 的 a-modal 只认 visible,
|
||||
* v-model:open 不会弹出(账号/角色页已统一走 AdminModal)。
|
||||
*/
|
||||
import { onMounted, reactive, ref, watch } from "vue";
|
||||
import { message, Modal } from "ant-design-vue";
|
||||
@@ -12,14 +14,21 @@ import {
|
||||
transitionWorkOrder,
|
||||
} from "/@/views/inquiry/workOrder/workOrder.api";
|
||||
import ProtoIcon from "/@/views/inquiry/components/ProtoIcon.vue";
|
||||
import AdminModal from "/@/views/inquiry/components/AdminModal.vue";
|
||||
|
||||
type TicketQuote = { source: string; price: string; highlight?: boolean };
|
||||
type TicketAttach = { name: string; uploadedAt: string; uploader: string };
|
||||
type TicketTimeline = { status: string; remark: string; time: string };
|
||||
|
||||
type TicketRow = {
|
||||
id: string;
|
||||
initiator: string;
|
||||
demandTitle: string;
|
||||
demandDetail: string;
|
||||
quotes: { source: string; price: string; highlight?: boolean }[];
|
||||
quotes: TicketQuote[];
|
||||
attachments: string[];
|
||||
attachRows: TicketAttach[];
|
||||
timeline: TicketTimeline[];
|
||||
status: string;
|
||||
costPrice: string;
|
||||
salePrice: string;
|
||||
@@ -30,6 +39,14 @@ type TicketRow = {
|
||||
createdAt: string;
|
||||
quoteDate: string;
|
||||
bizType: string;
|
||||
customerName: string;
|
||||
origin: string;
|
||||
destination: string;
|
||||
pieces: string;
|
||||
weightKg: string;
|
||||
volumeCbm: string;
|
||||
goodsName: string;
|
||||
updateTime: string;
|
||||
};
|
||||
|
||||
const rows = ref<TicketRow[]>([]);
|
||||
@@ -55,18 +72,48 @@ function dash(v: any) {
|
||||
return s ? s : "-";
|
||||
}
|
||||
|
||||
function mapTicket(t: any, quotes: any[] = [], attachments: any[] = []): TicketRow {
|
||||
function fmtTime(v: any) {
|
||||
const s = (v ?? "").toString().trim().replace("T", " ");
|
||||
return s ? s.slice(0, 19) : "-";
|
||||
}
|
||||
|
||||
function fmtMeasure(v: any, unit: string) {
|
||||
if (v == null || v === "") return "-";
|
||||
const s = String(v).trim();
|
||||
return s ? `${s} ${unit}` : "-";
|
||||
}
|
||||
|
||||
function mapTicket(t: any, quotes: any[] = [], attachments: any[] = [], timeline: any[] = []): TicketRow {
|
||||
const attachRows: TicketAttach[] = (attachments || [])
|
||||
.map((a) => ({
|
||||
name: a.fileName || a.objectKey || "附件",
|
||||
uploadedAt: fmtTime(a.createTime),
|
||||
uploader: dash(a.createBy || a.uploader || a.uploadedBy),
|
||||
}))
|
||||
.filter((a) => a.name);
|
||||
const origin = dash(t.origin);
|
||||
const destination = dash(t.destination);
|
||||
const route =
|
||||
origin !== "-" || destination !== "-"
|
||||
? `${origin === "-" ? "" : origin} → ${destination === "-" ? "" : destination}`.trim()
|
||||
: "";
|
||||
return {
|
||||
id: t.workOrderNo || t.id || "",
|
||||
initiator: t.salesName || t.salesWecomId || "—",
|
||||
demandTitle: t.demandTitle || `${t.origin || ""} → ${t.destination || ""}` || "—",
|
||||
demandTitle: t.demandTitle || route || "—",
|
||||
demandDetail: t.demandDetail || "",
|
||||
quotes: (quotes || []).map((q) => ({
|
||||
source: q.sourceLabel || q.source || "报价",
|
||||
price: q.priceText || "-",
|
||||
highlight: q.highlighted === 1 || q.highlighted === true,
|
||||
})),
|
||||
attachments: (attachments || []).map((a) => a.fileName || a.objectKey || "附件").filter(Boolean),
|
||||
attachments: attachRows.map((a) => a.name),
|
||||
attachRows,
|
||||
timeline: (timeline || []).map((x) => ({
|
||||
status: dash(x.toStatus || x.status),
|
||||
remark: dash(x.remark || x.action),
|
||||
time: fmtTime(x.createTime),
|
||||
})),
|
||||
status: t.status || "询价中",
|
||||
costPrice: dash(t.costPrice),
|
||||
salePrice: dash(t.salePrice),
|
||||
@@ -74,12 +121,27 @@ function mapTicket(t: any, quotes: any[] = [], attachments: any[] = []): TicketR
|
||||
eventException: dash(t.eventException),
|
||||
systemException: dash(t.systemException),
|
||||
systemExceptionReason: dash(t.systemExceptionReason),
|
||||
createdAt: (t.createTime || "").toString().slice(0, 19),
|
||||
createdAt: fmtTime(t.createTime),
|
||||
quoteDate: dash((t.quoteDate || "").toString().slice(0, 10)),
|
||||
bizType: t.bizTypeLabel || t.businessLine || "",
|
||||
customerName: dash(t.customerName),
|
||||
origin,
|
||||
destination,
|
||||
pieces: t.pieces != null && t.pieces !== "" ? `${t.pieces} 件` : "-",
|
||||
weightKg: fmtMeasure(t.weightKg, "kg"),
|
||||
volumeCbm: fmtMeasure(t.volumeCbm, "CBM"),
|
||||
goodsName: dash(t.goodsName),
|
||||
updateTime: fmtTime(t.updateTime || t.createTime),
|
||||
};
|
||||
}
|
||||
|
||||
function routeText(row: TicketRow) {
|
||||
if (row.origin !== "-" || row.destination !== "-") {
|
||||
return `${row.origin === "-" ? "" : row.origin} → ${row.destination === "-" ? "" : row.destination}`.trim();
|
||||
}
|
||||
return row.demandTitle || "-";
|
||||
}
|
||||
|
||||
async function loadList() {
|
||||
loading.value = true;
|
||||
try {
|
||||
@@ -122,9 +184,9 @@ async function openDetail(row: TicketRow) {
|
||||
detailOpen.value = true;
|
||||
try {
|
||||
const res: any = await getWorkOrderDetail(row.id);
|
||||
const detail = res?.result || res || {};
|
||||
const detail = res?.ticket ? res : res?.result || res || {};
|
||||
const ticket = detail.ticket || detail;
|
||||
current.value = mapTicket(ticket, detail.quotes || [], detail.attachments || []);
|
||||
current.value = mapTicket(ticket, detail.quotes || [], detail.attachments || [], detail.timeline || []);
|
||||
const idx = rows.value.findIndex((r) => r.id === row.id);
|
||||
if (idx >= 0) rows.value[idx] = current.value;
|
||||
} catch (e: any) {
|
||||
@@ -380,20 +442,131 @@ function refresh() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a-modal v-model:open="detailOpen" title="工单详情" width="920px" :footer="null">
|
||||
<template v-if="current">
|
||||
<p><b>工单号:</b>{{ current.id }}</p>
|
||||
<p><b>发起人:</b>{{ current.initiator }}</p>
|
||||
<p><b>需求:</b>{{ current.demandTitle }}</p>
|
||||
<p style="white-space: pre-line">{{ current.demandDetail }}</p>
|
||||
<p><b>状态:</b>{{ current.status }}</p>
|
||||
<p><b>报价日期:</b>{{ current.quoteDate }}</p>
|
||||
<p><b>创建时间:</b>{{ current.createdAt }}</p>
|
||||
<AdminModal
|
||||
v-model:open="detailOpen"
|
||||
title="工单详情"
|
||||
size="xxl"
|
||||
cancel-text="关闭"
|
||||
:show-ok="false"
|
||||
>
|
||||
<template #titleExtra>
|
||||
<span v-if="current" class="ticket-detail-no">{{ current.id }}</span>
|
||||
</template>
|
||||
</a-modal>
|
||||
<div v-if="current" class="ticket-detail">
|
||||
<section class="ticket-detail-section">
|
||||
<div class="ticket-detail-title">
|
||||
<ProtoIcon name="tickets" :size="14" />工单基础信息
|
||||
</div>
|
||||
<div class="ticket-detail-grid">
|
||||
<div class="ticket-detail-item"><label>发起销售</label><span>{{ current.initiator }}</span></div>
|
||||
<div class="ticket-detail-item"><label>业务类型</label><span>{{ dash(current.bizType) }}</span></div>
|
||||
<div class="ticket-detail-item"><label>当前状态</label><span>{{ current.status }}</span></div>
|
||||
<div class="ticket-detail-item"><label>报价日期</label><span>{{ current.quoteDate }}</span></div>
|
||||
<div class="ticket-detail-item"><label>创建时间</label><span>{{ current.createdAt }}</span></div>
|
||||
<div class="ticket-detail-item"><label>事件异常</label><span>{{ current.eventException }}</span></div>
|
||||
<div class="ticket-detail-item"><label>系统异常</label><span>{{ current.systemException }}</span></div>
|
||||
<div class="ticket-detail-item"><label>系统异常原因</label><span>{{ current.systemExceptionReason }}</span></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<a-modal v-model:open="closeOpen" title="关闭工单" ok-text="确认关闭" @ok="confirmClose">
|
||||
<p v-if="current">工单 {{ current.id }}</p>
|
||||
<a-textarea v-model:value="closeReason" :rows="4" placeholder="关闭原因(可选)" />
|
||||
</a-modal>
|
||||
<section class="ticket-detail-section">
|
||||
<div class="ticket-detail-title">
|
||||
<ProtoIcon name="gauge" :size="14" />价格与利润
|
||||
</div>
|
||||
<div class="ticket-detail-grid">
|
||||
<div class="ticket-detail-item"><label>成本价</label><span>{{ current.costPrice }}</span></div>
|
||||
<div class="ticket-detail-item"><label>销售价</label><span>{{ current.salePrice }}</span></div>
|
||||
<div class="ticket-detail-item"><label>利润点</label><span>{{ current.profit }}</span></div>
|
||||
</div>
|
||||
<div v-if="current.quotes.length" class="ticket-detail-demand">
|
||||
<div v-for="q in current.quotes" :key="q.source + q.price" class="quote-row" style="margin-bottom: 6px">
|
||||
<div class="quote-source">{{ q.source }}</div>
|
||||
<div class="quote-price" :style="q.highlight ? { color: '#1d4ed8' } : undefined">{{ q.price }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="ticket-detail-section">
|
||||
<div class="ticket-detail-title">
|
||||
<ProtoIcon name="route" :size="14" />询价需求
|
||||
</div>
|
||||
<div class="ticket-detail-demand">
|
||||
<div style="font-weight: 800; color: #0f172a">
|
||||
{{ dash(current.bizType) }}:{{ routeText(current) }}
|
||||
</div>
|
||||
<div class="ticket-detail-grid" style="margin-top: 8px">
|
||||
<div class="ticket-detail-item"><label>业务类型</label><span>{{ dash(current.bizType) }}</span></div>
|
||||
<div class="ticket-detail-item"><label>起运 / 目的</label><span>{{ routeText(current) }}</span></div>
|
||||
<div class="ticket-detail-item"><label>件数</label><span>{{ current.pieces }}</span></div>
|
||||
<div class="ticket-detail-item"><label>毛重</label><span>{{ current.weightKg }}</span></div>
|
||||
<div class="ticket-detail-item"><label>体积</label><span>{{ current.volumeCbm }}</span></div>
|
||||
<div class="ticket-detail-item"><label>品名</label><span>{{ current.goodsName }}</span></div>
|
||||
<div class="ticket-detail-item"><label>客户</label><span>{{ current.customerName }}</span></div>
|
||||
</div>
|
||||
<div v-if="current.demandDetail" class="ticket-detail-note">{{ current.demandDetail }}</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="ticket-detail-section">
|
||||
<div class="ticket-detail-title">
|
||||
<ProtoIcon name="fileText" :size="14" />附件记录
|
||||
</div>
|
||||
<table v-if="current.attachRows.length" class="ticket-detail-attach">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>附件名称</th>
|
||||
<th>上传时间</th>
|
||||
<th>上传人</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="f in current.attachRows" :key="f.name + f.uploadedAt">
|
||||
<td class="ticket-detail-attach-name">{{ f.name }}</td>
|
||||
<td>{{ f.uploadedAt }}</td>
|
||||
<td>{{ f.uploader }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div v-else class="ticket-detail-empty">暂无附件</div>
|
||||
</section>
|
||||
|
||||
<section class="ticket-detail-section">
|
||||
<div class="ticket-detail-title">
|
||||
<ProtoIcon name="clock" :size="14" />状态流转与备注
|
||||
</div>
|
||||
<div v-if="current.timeline.length" class="ticket-detail-timeline">
|
||||
<div v-for="(item, idx) in current.timeline" :key="item.time + idx" class="ticket-detail-tl-item">
|
||||
<div>
|
||||
<b>{{ item.status }}</b>
|
||||
<span v-if="item.remark !== '-'"> · {{ item.remark }}</span>
|
||||
</div>
|
||||
<span class="ticket-detail-tl-time">{{ item.time }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="ticket-detail-empty">暂无流转记录</div>
|
||||
</section>
|
||||
</div>
|
||||
<template #footer>
|
||||
<div class="ticket-detail-foot">
|
||||
<span class="ticket-detail-updated">最后更新:{{ current?.updateTime || "-" }}</span>
|
||||
<button type="button" class="btn btn-secondary" @click="detailOpen = false">
|
||||
<ProtoIcon name="x" :size="14" />关闭
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</AdminModal>
|
||||
|
||||
<AdminModal
|
||||
v-model:open="closeOpen"
|
||||
title="关闭工单"
|
||||
:subtitle="current ? `工单 ${current.id}` : ''"
|
||||
size="md"
|
||||
ok-text="确认关闭"
|
||||
@ok="confirmClose"
|
||||
>
|
||||
<div class="form-row">
|
||||
<label class="lbl">关闭原因</label>
|
||||
<textarea v-model="closeReason" class="field" rows="4" placeholder="关闭原因(可选)" />
|
||||
</div>
|
||||
</AdminModal>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user