feat(后台分组): 分组筛选与分组列仅超管可见,非超管一律隐藏

管理员/普通用户数据已由后端裁剪到本人可访问分组,分组维度对其无意义;
账号归属分组是超管职责,故只有超管需要按分组筛选与区分。

- 隐藏分组筛选+分组列:品牌数据库、查询ASIN、最低价ASIN、去重总数据、
  店铺管理、用户密钥
- 仅隐藏分组展示:店铺数据记录(筛选框+卡片行)、图片视频任务(所属分组行)、
  撞款检测(明细抽屉分组列+卡片分组标签)
- 弹窗分组选择保留展示但锁定:非超管仅1个可访问分组时自动选中并置灰
  (查询/最低价ASIN 另联动加载该分组店铺),多分组不锁以免限制用户
- 空数据行 colspan 随列显隐动态化
- 新增 tests/align-group-visibility.test.ts 回归守卫(5 条)
This commit is contained in:
2026-09-13 14:00:19 +08:00
parent 0323f0bb7b
commit 3f6ad0c6ad
10 changed files with 191 additions and 52 deletions
@@ -24,6 +24,11 @@ import { importOutcomeText, importTaskFinished, isAllowedImportFile } from './de
import type { DedupeImportProgress } from './dedupe-import-model.ts'
import { toExportUrl } from './dedupe-total-export.ts'
import OldPagination from '@/components/OldPagination.vue'
import { useAdminSessionStore } from '@/stores/admin-session'
const session = useAdminSessionStore()
/** 仅超管需要分组维度:非超管数据由后端裁剪到本人分组,分组筛选/列一律不展示。 */
const isSuperAdmin = computed(() => session.isSuperAdmin)
const loading = ref(false)
const rows = ref<DedupeTotalItem[]>([])
@@ -32,6 +37,11 @@ const page = ref(1)
const pageSize = ref(10)
const filter = reactive(createDedupeTotalFilterState())
const groups = ref<DedupeGroupOption[]>([])
/** 非超管仅有一个可访问分组时,编辑/导入弹窗分组选择锁定为该组(保留展示、不可改)。 */
const lockedGroupId = computed<number | null>(() => {
if (isSuperAdmin.value) return null
return groups.value.length === 1 ? groups.value[0].id : null
})
const jumpPage = ref('')
const totalPages = computed(() => Math.max(1, Math.ceil(total.value / pageSize.value)))
@@ -115,7 +125,7 @@ function changeSize(size: number) {
function openEdit(row: DedupeTotalItem): void {
editTarget.value = row
editValue.value = row.dataValue
editGroupId.value = row.groupId
editGroupId.value = row.groupId ?? lockedGroupId.value
editVisible.value = true
}
@@ -157,7 +167,7 @@ async function removeRow(row: DedupeTotalItem): Promise<void> {
}
function resetImport(): void {
importGroupId.value = null
importGroupId.value = lockedGroupId.value
importFile.value = null
importProgress.value = ''
importRunning.value = false
@@ -306,7 +316,7 @@ onMounted(() => {
<label>用户名模糊搜索</label>
<input v-model="filter.username" type="text" placeholder="输入用户名" @keyup.enter="apply" />
</div>
<div class="form-group">
<div v-if="isSuperAdmin" class="form-group">
<label>分组</label>
<el-select v-model="filter.groupId" class="admin-filter" filterable clearable placeholder="全部分组">
<el-option v-for="group in groups" :key="group.id" :label="group.name" :value="group.id" />
@@ -348,7 +358,7 @@ onMounted(() => {
<th>ASIN</th>
<th>国家</th>
<th>用户名</th>
<th>分组</th>
<th v-if="isSuperAdmin">分组</th>
<th>创建时间</th>
<th>操作</th>
</tr>
@@ -360,7 +370,7 @@ onMounted(() => {
<td class="mono-cell">{{ row.dataValue }}</td>
<td>{{ asinCountryLabel(row.country || '') }}</td>
<td>{{ row.username || '-' }}</td>
<td>{{ row.groupName || '未分组' }}</td>
<td v-if="isSuperAdmin">{{ row.groupName || '未分组' }}</td>
<td>{{ formatDateTime(row.createdAt) }}</td>
<td class="ops-cell">
<button class="btn btn-sm" type="button" @click="openEdit(row)">编辑</button>
@@ -369,10 +379,10 @@ onMounted(() => {
</tr>
</template>
<tr v-else-if="loading">
<td colspan="7" class="empty-tip">加载中...</td>
<td :colspan="isSuperAdmin ? 7 : 6" class="empty-tip">加载中...</td>
</tr>
<tr v-else>
<td colspan="7" class="empty-tip">暂无总数据</td>
<td :colspan="isSuperAdmin ? 7 : 6" class="empty-tip">暂无总数据</td>
</tr>
</tbody>
</table>
@@ -390,7 +400,7 @@ onMounted(() => {
</div>
<div class="form-group">
<label>分组</label>
<select v-model="editGroupId">
<select v-model="editGroupId" :disabled="lockedGroupId != null">
<option disabled :value="null">请选择分组</option>
<option v-for="group in groups" :key="group.id" :value="group.id">{{ group.name }}</option>
</select>
@@ -411,7 +421,7 @@ onMounted(() => {
</div>
<div class="form-group">
<label>分组</label>
<select v-model="importGroupId" :disabled="importRunning">
<select v-model="importGroupId" :disabled="importRunning || lockedGroupId != null">
<option disabled :value="null">请选择分组</option>
<option v-for="group in groups" :key="group.id" :value="group.id">{{ group.name }}</option>
</select>