For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: In web-admin Console (/dashboard/console) add a top region tab bar and make all displayed statistics obey super_admin (all regions) vs admin (only permission scope, with “all” meaning sum of permitted regions). “Latest orders” are merged across regions and sorted by createAt descending.
Architecture: Add region tab UI + tab state in web-admin/src/views/dashboard/console/index.vue. Refactor data loading into “scoped” loaders that either:
super_admin/all, oradmin/all.Tech Stack: Vue3 <script setup lang="ts">, Pinia useUserStore, Element Plus el-button, existing REST APIs in web-admin/src/api/business.ts, region list from web-admin/src/api/admin.ts (fetchActiveRegions).
Files:
web-admin/src/views/dashboard/console/index.vueInsert this block immediately after the existing page-head section (right after the closing </div> of .page-head and before the alert bar):
<div class="region-tabs region-tab-strip">
<div class="tab-button-group region-tab-group">
<el-button
class="region-tab-btn"
:type="activeRegionTabKey === 'all' ? 'primary' : 'default'"
@click="selectRegionTab('all')"
>
全部区域
</el-button>
<el-button
v-for="r in allowedRegionList"
:key="r.id"
class="region-tab-btn"
:type="activeRegionTabKey === r.id ? 'primary' : 'default'"
@click="selectRegionTab(r.id)"
>
{{ r.name }}
</el-button>
</div>
</div>
In <style lang="scss" scoped>, add under .console-container { ... }:
.tab-button-group {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin: 0 0 12px;
}
.region-tab-btn {
min-width: 96px;
}
In <script setup lang="ts">, add these imports:
import { fetchActiveRegions } from '@/api/admin'
import { useUserStore } from '@/store/modules/user'
Then add the following state after existing dashboard ref:
const userStore = useUserStore()
const regionList = ref<any[]>([])
const activeRegionTabKey = ref<string>('all')
const userRole = computed(() => userStore.getUserInfo?.role)
const allowedRegionIds = computed<string[]>(() => {
const info: any = userStore.getUserInfo
if (info?.role === 'super_admin') return []
const ids = Array.isArray(info?.regionIds) ? info.regionIds : info?.regionId ? [info.regionId] : []
return ids.map((x: any) => String(x))
})
const allowedRegionList = computed(() => {
if (userRole.value === 'super_admin') return regionList.value
const allow = new Set(allowedRegionIds.value)
return regionList.value.filter((r: any) => allow.has(String(r.id)))
})
Add these functions near existing loadDashboard/loadOrderTrend functions:
const selectRegionTab = (key: string) => {
if (activeRegionTabKey.value === key) return
activeRegionTabKey.value = key
void loadConsoleData()
}
const loadRegions = async () => {
const res = await fetchActiveRegions()
regionList.value = Array.isArray(res) ? res : []
}
onMounted entryReplace the current onMounted(() => { ... load... }) with:
onMounted(() => {
void loadRegions().then(() => loadConsoleData())
})
Files:
web-admin/src/views/dashboard/console/index.vueAdd the following helper functions in <script setup>:
const isSuperAdmin = computed(() => userRole.value === 'super_admin')
const getSingleRegionId = () => {
return activeRegionTabKey.value !== 'all' ? String(activeRegionTabKey.value) : undefined
}
const getMultiRegionIds = () => {
return allowedRegionIds.value
}
Create a top-level loader:
const loadConsoleData = async () => {
loading.value = true
try {
await Promise.all([
loadDashboardScoped(),
loadOrderTrendScoped(),
loadTodayRegionStatsScoped(),
loadTodayClientStatsScoped()
])
} finally {
loading.value = false
}
}
Replace existing loadDashboard() with loadDashboardScoped():
const loadDashboardScoped = async () => {
const singleRegionId = getSingleRegionId()
// super_admin + all -> keep existing unscoped behavior
if (isSuperAdmin.value && !singleRegionId) {
dashboard.value = await fetchWorkbenchDashboard()
return
}
// single region
if (singleRegionId) {
dashboard.value = await fetchWorkbenchDashboard({ regionId: singleRegionId })
return
}
// admin + all -> aggregate over permission regions
const regionIds = getMultiRegionIds()
const results = await Promise.allSettled(regionIds.map((id) => fetchWorkbenchDashboard({ regionId: id })))
const ok = results
.filter((r): r is PromiseFulfilledResult<WorkbenchDashboardData> => r.status === 'fulfilled')
.map((r) => r.value)
const first = ok[0]
const sum = (key: keyof WorkbenchDashboardData) =>
ok.reduce((acc, d) => acc + Number((d as any)?.[key] ?? 0), 0)
const mergedRecentOrders = ok.flatMap((d) => (Array.isArray(d.recentOrders) ? d.recentOrders : []))
mergedRecentOrders.sort((a: any, b: any) => new Date(b.createAt || 0).getTime() - new Date(a.createAt || 0).getTime())
const snapMap = new Map<string, { type: string; title: string; count: number; items: string[] }>()
ok.forEach((d) => {
;(d.todoSnapshots || []).forEach((s) => {
const k = String(s.type || s.title || '')
if (!k) return
const prev = snapMap.get(k)
if (!prev) {
snapMap.set(k, { type: s.type, title: s.title, count: Number(s.count ?? 0), items: Array.isArray(s.items) ? [...s.items] : [] })
} else {
prev.count += Number(s.count ?? 0)
const merged = [...prev.items, ...(Array.isArray(s.items) ? s.items : [])]
prev.items = Array.from(new Set(merged))
}
})
})
dashboard.value = {
todayOrderCount: sum('todayOrderCount'),
todayOrderAmount: sum('todayOrderAmount'),
monthOrderCount: sum('monthOrderCount'),
monthOrderAmount: sum('monthOrderAmount'),
pendingDeliveryCount: sum('pendingDeliveryCount'),
completedOrderCount: sum('completedOrderCount'),
activePriceListCount: sum('activePriceListCount'),
customerCount: sum('customerCount'),
supplierCount: sum('supplierCount'),
deliveryWarningCount: sum('deliveryWarningCount'),
todayOrderCountHint: first?.todayOrderCountHint,
todayOrderAmountHint: first?.todayOrderAmountHint,
recentOrders: mergedRecentOrders,
todoSnapshots: Array.from(snapMap.values())
} as WorkbenchDashboardData
}
Replace existing loadOrderTrend() with loadOrderTrendScoped():
const loadOrderTrendScoped = async () => {
trendLoading.value = true
try {
const singleRegionId = getSingleRegionId()
// super_admin + all -> unscoped
if (isSuperAdmin.value && !singleRegionId) {
const list = await fetchOrderTrend({ days: TREND_DAYS })
orderTrend.value = Array.isArray(list) ? list : []
return
}
// single region
const regionIds = singleRegionId ? [singleRegionId] : getMultiRegionIds()
const results = await Promise.allSettled(regionIds.map((id) => fetchOrderTrend({ regionId: id, days: TREND_DAYS })))
const ok = results
.filter((r): r is PromiseFulfilledResult<OrderTrendDayItem[]> => r.status === 'fulfilled')
.map((r) => r.value)
const base = ok.find((x) => Array.isArray(x) && x.length > 0) || []
const baseDates = base.map((x) => x.date)
const m = new Map<string, OrderTrendDayItem>()
ok.forEach((list) => {
;(list || []).forEach((row) => {
const k = String(row.date)
const prev = m.get(k)
if (!prev) {
m.set(k, { date: row.date, orderCount: Number(row.orderCount ?? 0), orderAmount: Number(row.orderAmount ?? 0) })
} else {
prev.orderCount += Number(row.orderCount ?? 0)
prev.orderAmount += Number(row.orderAmount ?? 0)
}
})
})
orderTrend.value = baseDates.map((d) => m.get(d)).filter(Boolean)
} catch {
orderTrend.value = []
} finally {
trendLoading.value = false
}
}
Replace existing loadTodayRegionStats() with loadTodayRegionStatsScoped():
const loadTodayRegionStatsScoped = async () => {
regionTodayLoading.value = true
try {
const singleRegionId = getSingleRegionId()
// super_admin + all -> unscoped
if (isSuperAdmin.value && !singleRegionId) {
const list = await fetchTodayOrdersByRegion()
regionTodayList.value = Array.isArray(list) ? list : []
return
}
const regionIds = singleRegionId ? [singleRegionId] : getMultiRegionIds()
const results = await Promise.allSettled(regionIds.map((id) => fetchTodayOrdersByRegion({ regionId: id })))
const ok = results
.filter((r): r is PromiseFulfilledResult<any[]> => r.status === 'fulfilled')
.map((r) => r.value)
const flat = ok.flat()
const m = new Map<string, TodayRegionOrderStatItem>()
flat.forEach((it: any) => {
const rid = String(it.regionId)
const prev = m.get(rid)
if (!prev) {
m.set(rid, {
regionId: rid,
regionName: it.regionName || '',
orderCount: Number(it.orderCount ?? 0),
orderAmount: Number(it.orderAmount ?? 0)
})
} else {
prev.orderCount += Number(it.orderCount ?? 0)
prev.orderAmount += Number(it.orderAmount ?? 0)
}
})
const orderIds = singleRegionId ? [singleRegionId] : getMultiRegionIds()
regionTodayList.value = orderIds.map((id) => m.get(String(id))).filter(Boolean)
} catch {
regionTodayList.value = []
} finally {
regionTodayLoading.value = false
}
}
Replace existing loadTodayClientStats() with loadTodayClientStatsScoped():
const loadTodayClientStatsScoped = async () => {
clientTodayLoading.value = true
try {
const singleRegionId = getSingleRegionId()
// super_admin + all -> unscoped
if (isSuperAdmin.value && !singleRegionId) {
const list = await fetchTodayOrdersByClient({ limit: 30 })
clientTodayList.value = Array.isArray(list) ? list : []
return
}
const regionIds = singleRegionId ? [singleRegionId] : getMultiRegionIds()
const limit = 30
const results = await Promise.allSettled(regionIds.map((id) => fetchTodayOrdersByClient({ regionId: id, limit })))
const ok = results
.filter((r): r is PromiseFulfilledResult<any[]> => r.status === 'fulfilled')
.map((r) => r.value)
const flat = ok.flat()
const m = new Map<string, TodayClientOrderStatItem>()
flat.forEach((it: any) => {
const cid = String(it.clientId)
const prev = m.get(cid)
if (!prev) {
m.set(cid, {
clientId: cid,
clientName: it.clientName || '',
orderAmount: Number(it.orderAmount ?? 0),
productQuantity: Number(it.productQuantity ?? 0)
})
} else {
prev.orderAmount += Number(it.orderAmount ?? 0)
prev.productQuantity += Number(it.productQuantity ?? 0)
}
})
const merged = Array.from(m.values())
merged.sort((a, b) => (Number(b.orderAmount) || 0) - (Number(a.orderAmount) || 0))
clientTodayList.value = merged
} catch {
clientTodayList.value = []
} finally {
clientTodayLoading.value = false
}
}
Files:
web-admin/src/views/dashboard/console/index.vueVerify that the computed orderList remains:
const orderList = computed(() => dashboard.value?.recentOrders ?? [])
And ensure recentOrders is populated by loadDashboardScoped() aggregation, sorted by createAt descending:
mergedRecentOrders.sort((a: any, b: any) => new Date(b.createAt || 0).getTime() - new Date(a.createAt || 0).getTime())
No additional slicing or “top N” rule is applied beyond merging and sorting.
Files: none
http://localhost:3006/#/dashboard/console全部区域 + 每个启用区域全部区域,所有统计回到全量数据全部区域全部区域:所有统计应等于“各权限区数据求和/合并”(最新订单按 createAt 倒序合并)