Files
order_site/apps/frontend/src/views/admin/AdminCdksView.vue
T
2026-04-08 16:30:42 +08:00

328 lines
8.5 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 { createAdminCdk, fetchAdminCdks, importAdminCdks, invalidateAdminCdk, releaseAdminInventoryCdk } from '@/services/admin'
import type { AdminCdkListItem, AdminPagination } from '@/types/admin'
import { hasAdminRole } from '@/utils/admin-auth'
import { adminCdkStatusOptions } 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 singleCdkCode = ref('')
const items = ref<AdminCdkListItem[]>([])
const pagination = ref<AdminPagination>({
page: 1,
pageSize: 20,
total: 0,
})
async function loadCdks(page = pagination.value.page) {
loading.value = true
errorMessage.value = ''
try {
const response = await fetchAdminCdks({
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 : '读取 CDK 列表失败'
} finally {
loading.value = false
}
}
async function submitCreate() {
if (!skuCode.value.trim() || !singleCdkCode.value.trim()) {
ElMessage.error('请填写 SKU 和单条 CDK')
return
}
creating.value = true
try {
await createAdminCdk({
skuCode: skuCode.value.trim(),
batchNo: batchNo.value.trim(),
cdkCode: singleCdkCode.value.trim(),
})
ElMessage.success('CDK 已新增')
singleCdkCode.value = ''
await loadCdks()
} catch (error) {
ElMessage.error(error instanceof Error ? error.message : '新增 CDK 失败')
} 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 并输入至少一条 CDK')
return
}
importing.value = true
try {
const response = await importAdminCdks({
skuCode: skuCode.value.trim(),
batchNo: batchNo.value.trim(),
codes,
})
ElMessage.success(`导入完成,新增 ${response.data.created} 条`)
bulkText.value = ''
await loadCdks()
} catch (error) {
ElMessage.error(error instanceof Error ? error.message : '导入失败')
} finally {
importing.value = false
}
}
async function runRowAction(cdkId: number, action: () => Promise<unknown>, successMessage: string, confirmText: string) {
try {
await ElMessageBox.confirm(confirmText, '确认操作', {
type: 'warning',
confirmButtonText: '继续执行',
cancelButtonText: '取消',
})
} catch {
return
}
actionLoadingId.value = cdkId
try {
await action()
ElMessage.success(successMessage)
await loadCdks()
} catch (error) {
ElMessage.error(error instanceof Error ? error.message : '操作失败')
} finally {
actionLoadingId.value = null
}
}
onMounted(loadCdks)
</script>
<template>
<section class="admin-panel">
<header class="panel-header">
<div>
<h1>CDK 管理</h1>
<p>查看库存、快速新增 CDK,并处理预占或失效库存。</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 adminCdkStatusOptions" :key="option.value" :value="option.value">
{{ option.label }}
</option>
</select>
<input v-model="batchNo" class="text-input" placeholder="批次号筛选/导入批次号" />
<el-button round @click="() => loadCdks()">查询列表</el-button>
</div>
<div class="form-row form-row-top">
<input v-model="singleCdkCode" class="text-input" placeholder="单条新增 CDK" />
<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="每行一个 CDK"
/>
</section>
<p v-if="errorMessage" class="error-copy">{{ errorMessage }}</p>
<div v-if="loading" class="empty-block">CDK 列表加载中</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>CDK</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr v-for="item in items" :key="item.cdkId">
<td>{{ item.cdkId }}</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.cdkCode }}</td>
<td>
<div class="action-stack">
<el-button
v-if="hasAdminRole('admin') && item.status === 'reserved'"
:loading="actionLoadingId === item.cdkId"
link
@click="runRowAction(item.cdkId, () => releaseAdminInventoryCdk(item.cdkId), 'CDK 已释放', `确认释放 CDK ${item.cdkCode} 吗?`)"
>
释放
</el-button>
<el-button
v-if="hasAdminRole('admin') && item.status === 'available'"
:loading="actionLoadingId === item.cdkId"
link
type="danger"
@click="runRowAction(item.cdkId, () => invalidateAdminCdk(item.cdkId, { reason: '后台手动作废' }), 'CDK 已作废', `确认作废 CDK ${item.cdkCode} 吗?作废后不会再参与分配。`)"
>
作废
</el-button>
</div>
</td>
</tr>
</tbody>
</table>
<AdminPaginationBar
:page="pagination.page"
:page-size="pagination.pageSize"
:total="pagination.total"
:loading="loading"
@change="loadCdks"
/>
</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>