task-255(admin.html观感对齐): 品牌数据库页对齐(分组筛选/来源标签/自动导入记录分组锁定/删除文案)

This commit is contained in:
2026-09-05 21:27:26 +08:00
parent 88f2ec8bb0
commit 178ff96be4
2 changed files with 277 additions and 0 deletions
@@ -0,0 +1,221 @@
<script setup lang="ts">
/** 不符合 ASIN(品牌数据库):分页 + 手动新增/编辑/删除。 */
import { onMounted, reactive, ref } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { createInvalidAsin, deleteInvalidAsin, fetchInvalidAsinPage, updateInvalidAsin } from '../invalidasin/invalid-asin-api.ts'
import type { InvalidAsinItem } from '../invalidasin/invalid-asin-model.ts'
import { invalidAsinDeleteText, recordSourceLabel } from '../invalidasin/invalid-asin-model.ts'
import { createEmptyInvalidAsinForm, invalidAsinFormFromItem, validateInvalidAsinForm } from '../invalidasin/invalid-asin-form-model.ts'
import { fetchShopManageGroups } from '../shop/shop-manage-api.ts'
import type { ShopGroupOption } from '../shop/shop-dto.ts'
const loading = ref(false)
const rows = ref<InvalidAsinItem[]>([])
const total = ref(0)
const page = ref(1)
const pageSize = 15
const groups = ref<ShopGroupOption[]>([])
const filter = reactive({ dataValue: '', brand: '', groupId: null as number | null })
const dialogVisible = ref(false)
const editingId = ref<number | null>(null)
/** 当前编辑行来源:自动导入(AUTO)记录不可改分组。 */
const editingSource = ref('')
const saving = ref(false)
const form = reactive(createEmptyInvalidAsinForm())
async function loadGroups() {
try {
groups.value = await fetchShopManageGroups()
} catch {
groups.value = []
}
}
async function load() {
loading.value = true
try {
const result = await fetchInvalidAsinPage({
page: page.value,
pageSize,
dataValue: filter.dataValue.trim() || undefined,
brand: filter.brand.trim() || undefined,
groupId: filter.groupId,
})
rows.value = result.items
total.value = result.total
page.value = result.page
} catch (error) {
ElMessage.error(error instanceof Error ? error.message : '数据加载失败')
} finally {
loading.value = false
}
}
function apply() {
page.value = 1
load()
}
function reset() {
filter.dataValue = ''
filter.brand = ''
filter.groupId = null
page.value = 1
load()
}
function openCreate() {
editingId.value = null
editingSource.value = ''
Object.assign(form, createEmptyInvalidAsinForm())
dialogVisible.value = true
}
function openEdit(row: InvalidAsinItem) {
editingId.value = row.id
editingSource.value = row.recordSource
Object.assign(form, invalidAsinFormFromItem(row))
dialogVisible.value = true
}
async function submit() {
const { valid, errors } = validateInvalidAsinForm({ ...form }, { requireGroup: editingId.value == null })
if (!valid) {
ElMessage.warning(Object.values(errors)[0] || '表单校验未通过')
return
}
saving.value = true
try {
if (editingId.value == null) {
await createInvalidAsin({ ...form })
ElMessage.success('已新增')
} else {
const includeGroup = editingSource.value.toUpperCase() !== 'AUTO'
await updateInvalidAsin(editingId.value, { ...form }, includeGroup)
ElMessage.success('已保存')
}
dialogVisible.value = false
load()
} catch (error) {
ElMessage.error(error instanceof Error ? error.message : '保存失败')
} finally {
saving.value = false
}
}
async function remove(row: InvalidAsinItem) {
try {
await ElMessageBox.confirm(invalidAsinDeleteText(row), '删除', { type: 'warning' })
} catch {
return
}
try {
await deleteInvalidAsin(row.id)
ElMessage.success('已删除')
load()
} catch (error) {
ElMessage.error(error instanceof Error ? error.message : '删除失败')
}
}
onMounted(() => {
loadGroups()
load()
})
</script>
<template>
<div class="page-stack">
<div class="page-heading">
<div>
<h2>不符合 ASIN 数据</h2>
<p>管理命中"不符合 ASIN"的品牌数据库记录</p>
</div>
<el-button type="primary" @click="openCreate">新增</el-button>
</div>
<el-card shadow="never" style="margin-bottom: 14px">
<div class="filter-grid">
<div class="f-item">
<label>ASIN</label>
<el-input v-model="filter.dataValue" placeholder="精确/模糊搜索ASIN" clearable @keyup.enter="apply" />
</div>
<div class="f-item">
<label>品牌</label>
<el-input v-model="filter.brand" placeholder="品牌模糊" clearable @keyup.enter="apply" />
</div>
<div class="f-item">
<label>分组</label>
<el-select v-model="filter.groupId" placeholder="全部分组" clearable>
<el-option v-for="group in groups" :key="group.id" :label="group.groupName" :value="group.id" />
</el-select>
</div>
<div class="f-item btn-row">
<el-button type="primary" @click="apply">查询</el-button>
<el-button @click="reset">重置</el-button>
</div>
</div>
</el-card>
<el-card shadow="never">
<el-table v-loading="loading" :data="rows" stripe border>
<el-table-column prop="dataValue" label="ASIN" min-width="160" />
<el-table-column prop="brand" label="品牌" min-width="140">
<template #default="{ row }">{{ (row as InvalidAsinItem).brand || '—' }}</template>
</el-table-column>
<el-table-column prop="groupName" label="分组" min-width="130" />
<el-table-column label="来源" min-width="110">
<template #default="{ row }">
<el-tag size="small" effect="light" :type="(row as InvalidAsinItem).recordSource !== 'MANUAL' ? 'info' : undefined">
{{ recordSourceLabel((row as InvalidAsinItem).recordSource) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="createdAt" label="创建时间" min-width="170" />
<el-table-column label="操作" width="150" fixed="right">
<template #default="{ row }">
<el-button text type="primary" size="small" @click="openEdit(row as InvalidAsinItem)">编辑</el-button>
<el-button text type="danger" size="small" @click="remove(row as InvalidAsinItem)">删除</el-button>
</template>
</el-table-column>
</el-table>
<div class="table-footer">
<span> <b>{{ total.toLocaleString() }}</b> </span>
<el-pagination background layout="prev, pager, next, jumper" :total="total" :page-size="pageSize" :current-page="page" @current-change="(p: number) => { page = p; load() }" />
</div>
</el-card>
<el-dialog v-model="dialogVisible" :title="editingId == null ? '新增不符合ASIN' : '编辑不符合ASIN'" width="520px">
<el-form label-width="110px">
<el-form-item label="ASIN" required>
<el-input v-model="form.dataValue" placeholder="不符合的 ASIN" />
</el-form-item>
<el-form-item label="品牌">
<el-input v-model="form.brand" placeholder="可选" />
</el-form-item>
<el-form-item v-if="editingId == null || editingSource.toUpperCase() !== 'AUTO'" label="分组" required>
<el-select v-model="form.groupId" placeholder="选择分组" filterable>
<el-option v-for="group in groups" :key="group.id" :label="group.groupName" :value="group.id" />
</el-select>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" :loading="saving" @click="submit">保存</el-button>
</template>
</el-dialog>
</div>
</template>
<style scoped>
.page-heading { display: flex; justify-content: space-between; align-items: center; }
.filter-grid { display: flex; flex-wrap: wrap; gap: 12px 18px; }
.f-item { display: flex; flex-direction: column; gap: 6px; width: 200px; }
.f-item label { color: var(--el-text-color-secondary); font-size: 12px; }
.f-item.btn-row { flex-direction: row; align-items: flex-end; gap: 8px; width: auto; }
.table-footer { display: flex; justify-content: space-between; align-items: center; padding-top: 14px; }
.table-footer span { color: var(--el-text-color-secondary); font-size: 12.5px; }
.table-footer b { color: var(--el-text-color-primary); }
</style>
+56
View File
@@ -0,0 +1,56 @@
import test from 'node:test'
import assert from 'node:assert/strict'
import { readSource } from './helpers.ts'
// module 13 task 255:品牌数据库(不符合ASIN)页对齐 admin panel-invalid-asin-data ——
// 分组筛选、来源列用 recordSourceLabel、自动导入记录编辑隐藏分组、删除文案用模型辅助。
test('test_task_255_invalidasin_normal_primary_path', () => {
const page = readSource('src/pages/asin/AsinInvalidPage.vue')
assert.match(page, /分组/, '筛选需含分组下拉')
assert.match(page, /recordSourceLabel/, '来源列需用 recordSourceLabel 文案')
assert.match(page, /el-tag/, '来源宜用 tag 形态')
})
test('test_task_255_invalidasin_normal_variant_input', () => {
const page = readSource('src/pages/asin/AsinInvalidPage.vue')
assert.match(page, /invalidAsinDeleteText/, '删除需用模型删除文案')
assert.match(page, /includeGroup/, '更新提交需按来源决定是否迁组')
assert.match(page, /MANUAL/, '需判断来源是否为手动新增')
})
test('test_task_255_invalidasin_normal_repeated_operation_is_idempotent', () => {
const page = readSource('src/pages/asin/AsinInvalidPage.vue')
assert.ok(page.includes('分组'))
assert.ok(page.includes('分组'))
})
test('test_task_255_invalidasin_boundary_empty_input', () => {
const page = readSource('src/pages/asin/AsinInvalidPage.vue')
assert.match(page, /filter\.brand|品牌/, '需保留品牌筛选')
assert.match(page, /filter\.dataValue|ASIN/, '需保留 ASIN 筛选')
assert.match(page, /requireGroup/, '新建需校验分组必选')
})
test('test_task_255_invalidasin_boundary_single_item', () => {
// 边界:自动导入(非 MANUAL)记录编辑时不应展示分组字段(admin 语义)。
const page = readSource('src/pages/asin/AsinInvalidPage.vue')
assert.match(page, /editingSource|recordSource/, '需记录当前编辑行来源以控制分组显隐')
})
test('test_task_255_invalidasin_boundary_limit_or_missing_field', () => {
const api = readSource('src/pages/invalidasin/invalid-asin-api.ts')
assert.match(api, /groupId/, '查询适配需支持分组筛选参数')
})
test('test_task_255_invalidasin_invalid_input_rejected', () => {
const page = readSource('src/pages/asin/AsinInvalidPage.vue')
assert.equal(page.includes('MANUAL') && page.includes('recordSourceLabel'), true, '来源展示必须统一走映射')
})
test('test_task_255_invalidasin_dependency_failure_returns_actionable_message', () => {
// 依赖失败:模型已提供记录来源与删除文案纯函数,页面不得自造不一致文案。
const model = readSource('src/pages/invalidasin/invalid-asin-model.ts')
assert.match(model, /recordSourceLabel/, '模型需提供来源标签函数')
assert.match(model, /手动新增/, '手动新增文案存在')
})