task-263(admin.html观感对齐): 店铺管理页对齐(密码眼睛实时读取/修改时间列/整句必填校验/编辑密码必填语义/删除文案)

This commit is contained in:
2026-09-05 22:04:20 +08:00
parent a7cdb0031a
commit 5e4a2a9d11
2 changed files with 115 additions and 4 deletions
@@ -33,8 +33,35 @@ const form = reactive(emptyShopManageForm())
const credentialVisible = ref(false)
const credential = ref<ShopCredential | null>(null)
/** 密码列“眼睛”实时读取:仅展示已读明文,其余保持掩码;读取中占位。 */
const revealedPasswordCache = ref<Record<number, string>>({})
const revealedRowId = ref<number | null>(null)
const groupOptions = computed(() => groups.value)
function shownPassword(row: ShopSummary): string {
if (revealedRowId.value !== row.id) return row.passwordMasked || '******'
const plain = revealedPasswordCache.value[row.id]
return plain != null && plain !== '' ? plain : '读取中...'
}
async function togglePassword(row: ShopSummary) {
if (revealedRowId.value === row.id) {
revealedRowId.value = null
return
}
revealedRowId.value = row.id
if (revealedPasswordCache.value[row.id] == null) {
try {
const cred = await fetchShopCredential(row.id, row.shopName)
revealedPasswordCache.value[row.id] = cred.password
} catch (error) {
revealedRowId.value = null
ElMessage.error(error instanceof Error ? error.message : '读取密码失败')
}
}
}
async function loadGroups() {
try {
groups.value = await fetchShopManageGroups()
@@ -94,6 +121,16 @@ function openEdit(row: ShopSummary) {
}
async function submit() {
if (
form.groupId == null ||
!(form.shopName || '').trim() ||
!(form.mallName || '').trim() ||
!(form.account || '').trim() ||
!(form.password || '').trim()
) {
ElMessage.warning('请完整填写分组、店铺名、店铺商城名、账号、密码')
return
}
saving.value = true
try {
const createdById = session.user?.id ?? 0
@@ -124,7 +161,7 @@ async function showCredential(row: ShopSummary) {
async function remove(row: ShopSummary) {
try {
await ElMessageBox.confirm(`删除店铺${row.shopName}`, '删除店铺', { type: 'warning' })
await ElMessageBox.confirm(`删除店铺${row.shopName}”吗`, '删除确认', { type: 'warning' })
} catch {
return
}
@@ -188,8 +225,19 @@ onMounted(() => {
<el-table-column prop="mallName" label="商城" min-width="120" />
<el-table-column prop="groupName" label="分组" min-width="120" />
<el-table-column prop="account" label="账号" min-width="140" />
<el-table-column label="密码" width="140">
<template #default="{ row }">{{ (row as ShopSummary).passwordMasked || '—' }}</template>
<el-table-column label="密码" width="170">
<template #default="{ row }">
<span class="pwd-cell">{{ shownPassword(row as ShopSummary) }}</span>
<el-button
link
type="primary"
size="small"
:aria-label="revealedRowId === (row as ShopSummary).id ? '隐藏密码' : '显示密码'"
@click="togglePassword(row as ShopSummary)"
>
{{ revealedRowId === (row as ShopSummary).id ? '隐藏' : '显示' }}
</el-button>
</template>
</el-table-column>
<el-table-column prop="znUsername" label="自动化账号" min-width="140">
<template #default="{ row }">{{ (row as ShopSummary).znUsername || '—' }}</template>
@@ -197,6 +245,9 @@ onMounted(() => {
<el-table-column prop="createdAt" label="创建时间" min-width="170">
<template #default="{ row }">{{ formatDateTime(row.createdAt) }}</template>
</el-table-column>
<el-table-column prop="updatedAt" label="修改时间" min-width="170">
<template #default="{ row }">{{ formatDateTime(row.updatedAt) }}</template>
</el-table-column>
<el-table-column label="操作" width="220" fixed="right">
<template #default="{ row }">
<el-button text type="primary" size="small" @click="openEdit(row as ShopSummary)">编辑</el-button>
@@ -228,7 +279,7 @@ onMounted(() => {
<el-input v-model="form.account" placeholder="登录账号" />
</el-form-item>
<el-form-item label="密码" required>
<el-input v-model="form.password" type="password" show-password :placeholder="editingId == null ? '登录密码' : '留空则不修改?(后端必填,编辑需重填)'" />
<el-input v-model="form.password" type="password" show-password :placeholder="editingId == null ? '登录密码' : '后端必填,编辑请重新填写登录密码'" />
</el-form-item>
<el-form-item label="自动化账号">
<el-input v-model="form.znUsername" placeholder="可选紫鸟自动化账号" />
@@ -267,4 +318,5 @@ onMounted(() => {
.table-footer span { color: var(--el-text-color-secondary); font-size: 12.5px; }
.table-footer b { color: var(--el-text-color-primary); }
.dim { color: var(--el-text-color-secondary); font-size: 12px; }
.pwd-cell { font-family: Consolas, monospace; font-size: 12px; margin-right: 6px; }
</style>
+59
View File
@@ -0,0 +1,59 @@
import test from 'node:test'
import assert from 'node:assert/strict'
import { readSource } from './helpers.ts'
// module 13 task 263:店铺管理页对齐 admin panel-shop-manage(分组/店铺名筛选、密码掩码+眼睛
// 实时读取、修改时间列、必填校验整句、编辑密码必填语义、删除确认、凭证明文弹窗复制)。
test('test_task_263_shop_manage_normal_primary_path', () => {
const page = readSource('src/pages/shop/ShopManagePage.vue')
assert.match(page, /分组/, '需有分组列/分组筛选')
assert.match(page, /店铺名称/, '需有店铺名称筛选')
assert.match(page, /shopName/, '店铺名筛选绑定 shopName')
})
test('test_task_263_shop_manage_normal_variant_input', () => {
// 密码列:掩码默认,提供眼睛实时读取与隐藏。
const page = readSource('src/pages/shop/ShopManagePage.vue')
assert.match(page, /togglePassword/, '需有眼睛切换密码读取')
assert.match(page, /shownPassword/, '密码展示经 shownPassword')
assert.match(page, /读取中\.\.\./, '读取中有占位')
assert.match(page, /\*\*\*\*\*\*/, '掩码默认 ******')
})
test('test_task_263_shop_manage_normal_repeated_operation_is_idempotent', () => {
const page = readSource('src/pages/shop/ShopManagePage.vue')
assert.ok((page.match(/formatDateTime/g) || []).length >= 1, '时间统一格式化')
})
test('test_task_263_shop_manage_boundary_empty_input', () => {
// 必填整句校验(含编辑需重填密码的语义)。
const page = readSource('src/pages/shop/ShopManagePage.vue')
assert.match(page, /请完整填写分组、店铺名、店铺商城名、账号、密码/, '必填校验需整句提示')
assert.match(page, /后端必填,编辑请重新填写登录密码/, '编辑密码占位不再自相矛盾')
})
test('test_task_263_shop_manage_boundary_single_item', () => {
const page = readSource('src/pages/shop/ShopManagePage.vue')
assert.match(page, /updatedAt/, '需含修改时间列')
assert.match(page, /创建时间/, '保留创建时间列')
})
test('test_task_263_shop_manage_boundary_limit_or_missing_field', () => {
// 分组下拉来源 fetchShopManageGroups。
const page = readSource('src/pages/shop/ShopManagePage.vue')
assert.match(page, /fetchShopManageGroups/, '分组下拉走分组接口')
assert.match(page, /credential/, '凭证明文/复制能力在位')
})
test('test_task_263_shop_manage_invalid_input_rejected', () => {
const page = readSource('src/pages/shop/ShopManagePage.vue')
assert.match(page, /确定删除店铺“/, '删除确认需含“确定删除店铺”')
})
test('test_task_263_shop_manage_dependency_failure_returns_actionable_message', () => {
// 密码列含明文时不脱离 fetch credential 适配层(不内联请求)。
const page = readSource('src/pages/shop/ShopManagePage.vue')
assert.match(page, /fetchShopCredential/, '明文经 fetchShopCredential 获取')
assert.equal(/http\.(get|post|put|delete)/.test(page), false, '页面不内联 HTTP 请求')
})