task-255(admin.html观感对齐): 品牌数据库页对齐(分组筛选/来源标签/自动导入记录分组锁定/删除文案)
This commit is contained in:
@@ -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>
|
||||
@@ -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, /手动新增/, '手动新增文案存在')
|
||||
})
|
||||
Reference in New Issue
Block a user