feat: improve client order and member workflows

This commit is contained in:
2026-07-30 18:08:40 +08:00
parent 155f06846d
commit 907794f9a7
4 changed files with 112 additions and 32 deletions
+70 -2
View File
@@ -75,6 +75,8 @@ function emptyDraft(): FirstLegOrderDraft {
}
const form = reactive<FirstLegOrderDraft>(emptyDraft())
const serializedDraft = computed(() => JSON.stringify(form))
const editorDirty = computed(() => editorOpen.value && serializedDraft.value !== editorBaseline.value)
const filteredOrders = computed(() => store.orders.filter((order) => {
const keyword = filters.keyword.trim().toLowerCase()
@@ -90,8 +92,10 @@ const filteredOrders = computed(() => store.orders.filter((order) => {
const detailOrder = computed(() => detailId.value ? store.getById(detailId.value) : undefined)
const detailSummary = computed(() => detailOrder.value ? summarizeFirstLegCargo(detailOrder.value.items) : null)
const selectedWarehouse = computed(() => availableWarehouses.value.find((item) => item.id === form.receivingWarehouseId))
const hasActiveFilters = computed(() => Boolean(filters.keyword.trim() || filters.status || filters.transportMode || dateRange.value.length))
const emptyDescription = computed(() => hasActiveFilters.value ? '没有符合筛选条件的头程订单' : '还没有头程订单')
onMounted(async () => {
async function loadPageData() {
try {
await Promise.all([store.load(), firstLegPartySource.load()])
availableShippers.value = firstLegPartySource.listAvailable('SHIPPER')
@@ -99,10 +103,33 @@ onMounted(async () => {
} catch (error) {
ElMessage.error(error instanceof Error ? error.message : '订单与往来单位数据加载失败')
}
}
function handleBeforeUnload(event: BeforeUnloadEvent) {
if (!editorDirty.value) return
event.preventDefault()
event.returnValue = ''
}
onMounted(() => {
window.addEventListener('beforeunload', handleBeforeUnload)
void loadPageData()
})
onBeforeUnmount(() => window.removeEventListener('beforeunload', handleBeforeUnload))
onBeforeRouteLeave(async () => {
if (!editorDirty.value) return true
return confirmDiscardChanges()
})
function markEditorClean() {
editorBaseline.value = serializedDraft.value
}
function replaceForm(draft: FirstLegOrderDraft) {
Object.assign(form, structuredClone(draft))
markEditorClean()
}
function draftFromOrder(order: FirstLegOrder): FirstLegOrderDraft {
@@ -180,6 +207,15 @@ function clearConsignee() {
clearParty('CONSIGNEE')
}
async function loadSelectedWarehouse(id: string) {
if (!id) return
try {
await store.loadWarehouseDetail(id)
} catch (error) {
ElMessage.error(error instanceof Error ? error.message : '仓库地址与收货时间加载失败')
}
}
function openCreate() {
if (availableWarehouses.value.length === 0) {
ElMessage.warning('当前没有可用的平台收货仓,请联系平台维护仓库后再创建订单')
@@ -202,15 +238,47 @@ async function openDetail(order: FirstLegOrder) {
detailId.value = order.id
detailOpen.value = true
detailLoading.value = true
detailError.value = ''
try {
await store.loadDetail(order.id)
} catch (error) {
ElMessage.error(error instanceof Error ? error.message : '订单详情加载失败')
detailError.value = error instanceof Error ? error.message : '订单详情加载失败'
ElMessage.error(detailError.value)
} finally {
detailLoading.value = false
}
}
async function retryDetail() {
const order = detailId.value ? store.getById(detailId.value) : undefined
if (order) await openDetail(order)
}
async function confirmDiscardChanges() {
try {
await ElMessageBox.confirm('当前订单还有未保存的修改。离开后这些内容将丢失。', '放弃未保存的修改?', {
type: 'warning',
confirmButtonText: '放弃修改',
cancelButtonText: '继续编辑',
})
return true
} catch (error) {
if (error === 'cancel' || error === 'close') return false
throw error
}
}
async function requestCloseEditor(done: () => void) {
if (saving.value) return
if (!editorDirty.value || await confirmDiscardChanges()) done()
}
function closeEditor() {
void requestCloseEditor(() => {
editorOpen.value = false
})
}
function addCargoItem() {
form.items.push(createEmptyCargoItem(`new-cargo-${++cargoSequence}`))
}
+7 -6
View File
@@ -134,12 +134,12 @@ function clearFilters() {
<div class="client-card member-table-card">
<el-table v-if="filteredMembers.length" class="desktop-member-table" :data="filteredMembers" row-key="id">
<el-table-column label="成员" min-width="180"><template #default="{ row }"><div class="member-identity"><span>{{ row.name.slice(0, 1) }}</span><div><strong>{{ row.name }}</strong><small>{{ row.account }}</small></div></div></template></el-table-column>
<el-table-column label="联系方式" min-width="190"><template #default="{ row }"><div class="member-contact"><span>{{ row.phone }}</span><small>{{ row.email || '-' }}</small></div></template></el-table-column>
<el-table-column label="角色" min-width="180"><template #default="{ row }"><div class="role-tags"><el-tag v-for="name in store.roleNames(row.roleIds)" :key="name" effect="plain">{{ name }}</el-tag><span v-if="store.roleNames(row.roleIds).length === 0" class="readonly-action">未分配</span></div></template></el-table-column>
<el-table-column prop="lastLoginAt" label="登录记录" min-width="150" />
<el-table-column label="状态" width="100"><template #default="{ row }"><el-tag :type="row.status === 'ACTIVE' ? 'success' : 'info'" effect="light">{{ row.status === 'ACTIVE' ? '正常' : '已停用' }}</el-tag></template></el-table-column>
<el-table-column label="操作" width="250" fixed="right"><template #default="{ row }"><el-button v-if="auth.hasPermission('company:member:write')" link type="primary" @click="openEdit(row as TenantMember)"><Pencil :size="15" />编辑</el-button><el-button v-if="auth.hasPermission('company:member:reset-password')" link type="primary" @click="resetPassword(row as TenantMember)"><KeyRound :size="15" />重置密码</el-button><el-button v-if="auth.hasPermission('company:member:write') && !row.isAdministrator" link :type="row.status === 'ACTIVE' ? 'danger' : 'success'" @click="toggleStatus(row as TenantMember)">{{ row.status === 'ACTIVE' ? '停用' : '启用' }}</el-button><span v-if="!auth.hasPermission('company:member:write') && !auth.hasPermission('company:member:reset-password')" class="readonly-action">仅查看</span></template></el-table-column>
<el-table-column label="成员" min-width="170"><template #default="{ row }"><div class="member-identity"><span>{{ row.name.slice(0, 1) }}</span><div><strong>{{ row.name }}</strong><small>{{ row.account }}</small></div></div></template></el-table-column>
<el-table-column label="联系方式" min-width="200"><template #default="{ row }"><div class="member-contact"><span>{{ row.phone }}</span><small>{{ row.email || '-' }}</small></div></template></el-table-column>
<el-table-column label="角色" min-width="130"><template #default="{ row }"><div class="role-tags"><el-tag v-for="name in store.roleNames(row.roleIds)" :key="name" effect="plain">{{ name }}</el-tag><span v-if="store.roleNames(row.roleIds).length === 0" class="readonly-action">未分配</span></div></template></el-table-column>
<el-table-column prop="lastLoginAt" label="登录记录" min-width="130" />
<el-table-column label="状态" width="90"><template #default="{ row }"><el-tag :type="row.status === 'ACTIVE' ? 'success' : 'info'" effect="light">{{ row.status === 'ACTIVE' ? '正常' : '已停用' }}</el-tag></template></el-table-column>
<el-table-column label="操作" width="230"><template #default="{ row }"><el-button v-if="auth.hasPermission('company:member:write')" link type="primary" @click="openEdit(row as TenantMember)"><Pencil :size="15" />编辑</el-button><el-button v-if="auth.hasPermission('company:member:reset-password')" link type="primary" @click="resetPassword(row as TenantMember)"><KeyRound :size="15" />重置密码</el-button><el-button v-if="auth.hasPermission('company:member:write') && !row.isAdministrator" link :type="row.status === 'ACTIVE' ? 'danger' : 'success'" @click="toggleStatus(row as TenantMember)">{{ row.status === 'ACTIVE' ? '停用' : '启用' }}</el-button><span v-if="!auth.hasPermission('company:member:write') && !auth.hasPermission('company:member:reset-password')" class="readonly-action">仅查看</span></template></el-table-column>
</el-table>
<div v-if="filteredMembers.length" class="mobile-member-list">
@@ -181,6 +181,7 @@ function clearFilters() {
.member-identity { display: flex; align-items: center; gap: 10px; }
.member-identity > span { display: grid; width: 34px; height: 34px; flex: 0 0 34px; place-items: center; border-radius: 50%; background: #e7f3f1; color: var(--primary); font-weight: 750; }
.member-identity > div, .member-contact { display: flex; min-width: 0; flex-direction: column; gap: 3px; }
.member-identity strong, .member-identity small, .member-contact small { overflow-wrap: anywhere; }
.member-identity small, .member-contact small { color: var(--muted); font-size: 10px; }
.role-tags { display: flex; gap: 5px; flex-wrap: wrap; }
.mobile-member-list { display: none; }