Files
order_site/apps/frontend/src/views/admin/AdminInventoryView.vue
T
2026-04-09 15:51:24 +08:00

328 lines
8.9 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import AdminPaginationBar from '@/components/admin/AdminPaginationBar.vue'
import AdminStatusTag from '@/components/admin/AdminStatusTag.vue'
import { createAdminInventoryItem, fetchAdminInventoryItems, importAdminInventoryItems, invalidateAdminInventoryItem, releaseAdminInventoryItem } from '@/services/admin'
import type { AdminInventoryItemListItem, AdminPagination } from '@/types/admin'
import { hasAdminRole } from '@/utils/admin-auth'
import { adminInventoryStatusOptions } from '@/utils/admin-options'
const loading = ref(true)
const importing = ref(false)
const creating = ref(false)
const actionLoadingId = ref<number | null>(null)
const errorMessage = ref('')
const skuCode = ref('')
const status = ref('')
const batchNo = ref('')
const bulkText = ref('')
const singleDisplayValue = ref('')
const items = ref<AdminInventoryItemListItem[]>([])
const pagination = ref<AdminPagination>({
page: 1,
pageSize: 20,
total: 0,
})
async function loadInventoryItems(page = pagination.value.page) {
loading.value = true
errorMessage.value = ''
try {
const response = await fetchAdminInventoryItems({
page,
pageSize: pagination.value.pageSize,
skuCode: skuCode.value.trim(),
status: status.value.trim(),
batchNo: batchNo.value.trim(),
})
items.value = response.data.items
pagination.value = response.data.pagination
} catch (error) {
errorMessage.value = error instanceof Error ? error.message : '读取库存列表失败'
} finally {
loading.value = false
}
}
async function submitCreate() {
if (!skuCode.value.trim() || !singleDisplayValue.value.trim()) {
ElMessage.error('请填写 SKU 和库存凭据')
return
}
creating.value = true
try {
await createAdminInventoryItem({
skuCode: skuCode.value.trim(),
batchNo: batchNo.value.trim(),
displayValue: singleDisplayValue.value.trim(),
})
ElMessage.success('库存项已新增')
singleDisplayValue.value = ''
await loadInventoryItems()
} catch (error) {
ElMessage.error(error instanceof Error ? error.message : '新增库存项失败')
} finally {
creating.value = false
}
}
async function submitImport() {
const codes = bulkText.value
.split('\n')
.map((line) => line.trim())
.filter(Boolean)
if (!skuCode.value.trim() || codes.length === 0) {
ElMessage.error('请填写 SKU 并输入至少一条库存凭据')
return
}
importing.value = true
try {
const response = await importAdminInventoryItems({
skuCode: skuCode.value.trim(),
batchNo: batchNo.value.trim(),
codes,
})
ElMessage.success(`导入完成,新增 ${response.data.created} 条`)
bulkText.value = ''
await loadInventoryItems()
} catch (error) {
ElMessage.error(error instanceof Error ? error.message : '导入失败')
} finally {
importing.value = false
}
}
async function runRowAction(inventoryItemId: number, action: () => Promise<unknown>, successMessage: string, confirmText: string) {
try {
await ElMessageBox.confirm(confirmText, '确认操作', {
type: 'warning',
confirmButtonText: '继续执行',
cancelButtonText: '取消',
})
} catch {
return
}
actionLoadingId.value = inventoryItemId
try {
await action()
ElMessage.success(successMessage)
await loadInventoryItems()
} catch (error) {
ElMessage.error(error instanceof Error ? error.message : '操作失败')
} finally {
actionLoadingId.value = null
}
}
onMounted(loadInventoryItems)
</script>
<template>
<section class="admin-panel">
<header class="panel-header">
<div>
<h1>库存管理</h1>
<p>查看库存项、快速新增凭据,并处理预占或失效库存。</p>
</div>
</header>
<section class="import-card">
<div class="form-row">
<input v-model="skuCode" class="text-input" placeholder="SKU,例如 dnf-cdk-a" />
<select v-model="status" class="text-input select-input">
<option v-for="option in adminInventoryStatusOptions" :key="option.value" :value="option.value">
{{ option.label }}
</option>
</select>
<input v-model="batchNo" class="text-input" placeholder="批次号筛选/导入批次号" />
<el-button round @click="() => loadInventoryItems()">查询列表</el-button>
</div>
<div class="form-row form-row-top">
<input v-model="singleDisplayValue" class="text-input" placeholder="单条新增库存凭据" />
<el-button :loading="creating" round type="success" @click="submitCreate">单条新增</el-button>
<el-button :loading="importing" round type="primary" @click="submitImport">批量导入</el-button>
</div>
<textarea
v-model="bulkText"
class="text-area"
placeholder="每行一个库存凭据"
/>
</section>
<p v-if="errorMessage" class="error-copy">{{ errorMessage }}</p>
<div v-if="loading" class="empty-block">库存列表加载中</div>
<div v-else class="table-card">
<table class="data-table">
<thead>
<tr>
<th>ID</th>
<th>SKU</th>
<th>批次</th>
<th>状态/绑定</th>
<th>订单号</th>
<th>预占任务</th>
<th>失效原因</th>
<th>凭据内容</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr v-for="item in items" :key="item.inventoryItemId">
<td>{{ item.inventoryItemId }}</td>
<td>{{ item.skuCode }}</td>
<td>{{ item.batchNo || '-' }}</td>
<td>
<div class="status-stack">
<AdminStatusTag :status="item.status" />
<AdminStatusTag :status="item.systemBindingStatus" />
<AdminStatusTag :status="item.userBindingStatus" />
</div>
</td>
<td>{{ item.platformOrderId || '-' }}</td>
<td>{{ item.reservedByTaskNo || item.reservedByTaskId || '-' }}</td>
<td>{{ item.invalidReason || '-' }}</td>
<td>{{ item.displayValue }}</td>
<td>
<div class="action-stack">
<el-button
v-if="hasAdminRole('admin') && item.status === 'reserved'"
:loading="actionLoadingId === item.inventoryItemId"
link
@click="runRowAction(item.inventoryItemId, () => releaseAdminInventoryItem(item.inventoryItemId), '库存项已释放', `确认释放库存凭据 ${item.displayValue} 吗?`)"
>
释放
</el-button>
<el-button
v-if="hasAdminRole('admin') && item.status === 'available'"
:loading="actionLoadingId === item.inventoryItemId"
link
type="danger"
@click="runRowAction(item.inventoryItemId, () => invalidateAdminInventoryItem(item.inventoryItemId, { reason: '后台手动作废' }), '库存项已作废', `确认作废库存凭据 ${item.displayValue} 吗?作废后不会再参与分配。`)"
>
作废
</el-button>
</div>
</td>
</tr>
</tbody>
</table>
<AdminPaginationBar
:page="pagination.page"
:page-size="pagination.pageSize"
:total="pagination.total"
:loading="loading"
@change="loadInventoryItems"
/>
</div>
</section>
</template>
<style scoped>
.admin-panel {
display: grid;
gap: 16px;
}
.panel-header h1 {
margin: 0;
color: #1d3555;
}
.panel-header p {
margin: 8px 0 0;
color: #64748b;
}
.import-card,
.table-card,
.empty-block,
.error-copy {
padding: 18px;
border-radius: 20px;
background: rgba(255, 255, 255, 0.94);
border: 1px solid rgba(86, 108, 138, 0.1);
}
.error-copy {
color: #b42318;
}
.form-row {
display: flex;
gap: 12px;
align-items: center;
}
.status-stack {
display: flex;
flex-wrap: wrap;
gap: 6px;
align-items: center;
}
.form-row-top {
margin-top: 12px;
}
.text-input,
.text-area {
width: 100%;
min-height: 42px;
padding: 0 14px;
border-radius: 14px;
border: 1px solid rgba(86, 108, 138, 0.18);
background: rgba(255, 255, 255, 0.94);
}
.select-input {
appearance: none;
}
.text-area {
min-height: 180px;
padding: 14px;
margin-top: 14px;
resize: vertical;
font-family: var(--font-mono);
}
.data-table {
width: 100%;
border-collapse: collapse;
}
.data-table th,
.data-table td {
padding: 12px 10px;
text-align: left;
border-bottom: 1px solid rgba(86, 108, 138, 0.08);
color: #334155;
vertical-align: top;
}
.data-table th {
color: #64748b;
font-size: 13px;
}
.action-stack {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
</style>