feat(web-admin): split recharge summary by fuel/ETC and fix pnpm workspace

Show fuel-only cards on fuel routes and ETC-only cards on ETC routes with a 5-column layout, and declare packages in pnpm-workspace.yaml so pnpm 9+ can build.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
wuzilongandCursor committed 2026-11-12 20:39:48 +08:00
1 parent 7ebbaa2784
commit a4d213231c
7 files changed
+132 -26

No files matched your search

@@ -13,7 +13,7 @@ Web【车辆管理】下现有【ETC/油卡充值管理】(路由前缀 `/vehi
| 产品结构 | 并列【ETC充值管理】与【油卡充值管理】(原名改名) |
| ETC 子菜单 | 申请、审批、发放管理、审批人设置 |
| 油卡侧 | 界面保持现状;列表等不再展示 `rechargeType=etc` |
| ETC 顶部统计 | 不展示(`PlRechargeSummary` 不对 ETC 路由出现) |
| ETC 顶部统计 | ETC 分组展示 `PlRechargeSummary`(仅 ETC 信息);油卡分组仅展示加油卡信息(验收调整) |
| 前端实现 | 复制现有 4 个页面目录再改,便于后续各自演进 |
| 后端实现 | 不拆服务/表;列表与写入靠类型过滤/约束;小改即可 |
| 审批流程 / 审批人 | 保持现状,仍一套 `vehicle_recharge`;两边菜单都进同一配置(方案 A) |
+4
View File
@@ -1,3 +1,7 @@
# 单包工程也须声明 packages,否则 pnpm 9+ 报 “packages field missing or empty”
packages:
- '.'
allowBuilds:
'@parcel/watcher': true
'@tailwindcss/oxide': true
@@ -9,15 +9,15 @@
<article class="fuel-plate-card recharge-card recharge-notice">
<div class="recharge-notice-head"> <div class="plate">消息通知</div><em>最新</em> </div>
<div class="recharge-notice-list">
<div v-for="notice in summary.notices" :key="notice.id" class="recharge-notice-item">
<div v-for="notice in displayedNotices" :key="notice.id" class="recharge-notice-item">
<time :datetime="notice.occurredAt">{{ notice.occurredAt.slice(0, 10) }}</time>
<span
>{{ notice.actorName }}{{ notice.roleName ? `(${notice.roleName})` : ''
}}{{ notice.action }}{{ notice.licensePlate
}}{{ notice.rechargeType === 'etc' ? 'ETC' : '加油卡' }}充值;</span
}}{{ noticeTypeLabel(notice.rechargeType) }}充值;</span
>
</div>
<div v-if="!summary.notices.length" class="recharge-notice-item muted">暂无消息</div>
<div v-if="!displayedNotices.length" class="recharge-notice-item muted">暂无消息</div>
</div>
</article>
<article
@@ -45,7 +45,7 @@
>{{ statusLabels[vehicle.vehicleStatus] || vehicle.vehicleStatus || '—' }}</el-tag
>
</div>
<div class="recharge-split">
<div class="recharge-split" :class="{ 'is-single': kinds.length === 1 }">
<div v-for="kind in kinds" :key="kind.value" class="recharge-pane">
<div class="recharge-pane-h">
<h4>{{ kind.label }}</h4>
@@ -94,10 +94,17 @@
} from '@/api/pl/recharge-summary'
import { pickBxData } from '@/utils/http/bx'
import { formatPlVehiclePlateDisplay } from '@/utils/pl-display'
import { rechargeComparison, rechargeSummaryQuery } from '@/utils/pl-recharge-summary'
import {
rechargeComparison,
rechargeSummaryQuery,
type RechargeSummaryMode
} from '@/utils/pl-recharge-summary'
import mittBus from '@/utils/sys/mittBus'
const props = defineProps<{ applyPath: string }>()
const props = withDefaults(
defineProps<{ applyPath: string; mode?: RechargeSummaryMode }>(),
{ mode: 'fuel' }
)
const router = useRouter()
const route = useRoute()
const summary = ref<RechargeSummary | null>(null)
@@ -107,12 +114,22 @@
vehicle.vehicleType === 'self' && !['油桶', '叉车'].includes(vehicle.licensePlate.trim())
)
)
const displayedNotices = computed(() => {
const notices = summary.value?.notices || []
if (props.mode === 'etc') return notices.filter((n) => n.rechargeType === 'etc')
return notices.filter((n) => n.rechargeType !== 'etc')
})
const loading = ref(false)
const error = ref(false)
const kinds = [
const kindDefs = [
{ value: 'etc', label: 'ETC' },
{ value: 'fuel', label: '加油卡' }
] as const
const kinds = computed(() =>
kindDefs.filter((kind) =>
props.mode === 'etc' ? kind.value === 'etc' : kind.value === 'fuel'
)
)
const statusLabels: Record<string, string> = {
available: '可用',
maintaining: '维修中',
@@ -122,6 +139,11 @@
Number(value).toLocaleString('zh-CN', { maximumFractionDigits: 0 })
let requestVersion = 0
function noticeTypeLabel(rechargeType: string) {
if (rechargeType === 'etc') return 'ETC'
return '加油卡'
}
function metrics(amounts: RechargeAmounts) {
return [
{
@@ -186,11 +186,13 @@
}
.recharge-grid {
grid-template-columns: repeat(3, 1fr);
/* 单栏(油卡/ETC 分开展示):固定一行 5 张 */
grid-template-columns: repeat(5, 1fr);
}
.recharge-card {
cursor: default;
padding: 12px 14px;
}
.recharge-split {
@@ -199,6 +201,10 @@
gap: 10px;
}
.recharge-split.is-single {
grid-template-columns: 1fr;
}
.recharge-pane {
min-width: 0;
padding: 10px 12px;
@@ -14,7 +14,11 @@
</div>
</div>
<PlRechargeSummary v-if="!isFullPage && showRechargeSummary" :apply-path="rechargeApplyPath" />
<PlRechargeSummary
v-if="!isFullPage && showRechargeSummary && rechargeSummaryMode"
:apply-path="rechargeApplyPath"
:mode="rechargeSummaryMode"
/>
<ArtGroupLeafTabs v-if="!isFullPage && showGroupLeafTabs" />
<!-- 勿强制 minHeight:calc(100vh-…),否则门户布局会叠出多余纵向滚动条 -->
@@ -71,7 +75,11 @@
import { layoutOutletCacheKey, resolveLeafGroup } from '@/utils/router/leaf-group'
import ArtGroupLeafTabs from '@/components/core/layouts/art-group-leaf-tabs/index.vue'
import PlRechargeSummary from '@/components/business/pl-recharge-summary/index.vue'
import { isRechargeSummaryRoute } from '@/utils/pl-recharge-summary'
import {
isRechargeSummaryRoute,
rechargeSummaryModeFromPath,
type RechargeSummaryMode
} from '@/utils/pl-recharge-summary'
defineOptions({ name: 'ArtPageContent' })
@@ -93,12 +101,29 @@
const showGroupLeafTabs = computed(() => (leafGroup.value?.leaves.length || 0) > 1)
const leafGroupId = computed(() => leafGroup.value?.groupId || '')
const rechargeApplyPath = computed(() =>
leafGroup.value?.leaves.find((leaf) => leaf.meta.menuCode === 'pl:vehicle-recharge-apply:list')?.path || ''
const rechargeSummaryMode = computed<RechargeSummaryMode | null>(() => {
const fromRoute = rechargeSummaryModeFromPath(route.path)
if (fromRoute) return fromRoute
for (const leaf of leafGroup.value?.leaves || []) {
const mode = rechargeSummaryModeFromPath(leaf.path)
if (mode) return mode
}
return null
})
const rechargeApplyMenuCode = computed(() =>
rechargeSummaryMode.value === 'etc'
? 'pl:etc-vehicle-recharge-apply:list'
: 'pl:vehicle-recharge-apply:list'
)
const showRechargeSummary = computed(() =>
isRechargeSummaryRoute(route.path) ||
!!leafGroup.value?.leaves.some((leaf) => isRechargeSummaryRoute(leaf.path))
const rechargeApplyPath = computed(
() =>
leafGroup.value?.leaves.find((leaf) => leaf.meta.menuCode === rechargeApplyMenuCode.value)
?.path || ''
)
const showRechargeSummary = computed(
() =>
isRechargeSummaryRoute(route.path) ||
!!leafGroup.value?.leaves.some((leaf) => isRechargeSummaryRoute(leaf.path))
)
const useGroupOutletCache = computed(
@@ -3,7 +3,10 @@ import { describe, it } from 'node:test'
import {
rechargeComparison,
rechargeSummaryQuery,
isRechargeSummaryRoute
isRechargeSummaryRoute,
isFuelRechargeSummaryRoute,
isEtcRechargeSummaryRoute,
rechargeSummaryModeFromPath
} from './pl-recharge-summary'
describe('充值统计卡片', () => {
@@ -30,32 +33,60 @@ describe('充值统计卡片', () => {
})
})
it('只对油卡充值业务子页显示,ETC 与其它车辆页不显示', () => {
it('油卡分组子页识别为 fuel 模式', () => {
assert.equal(
isRechargeSummaryRoute('/vehicle-mgmt/fuel-recharge/vehicle-recharge-apply'),
isFuelRechargeSummaryRoute('/vehicle-mgmt/fuel-recharge/vehicle-recharge-apply'),
true
)
assert.equal(
isRechargeSummaryRoute('/vehicle-mgmt/fuel-recharge/vehicle-recharge-record'),
isFuelRechargeSummaryRoute('/vehicle-mgmt/fuel-recharge/vehicle-recharge-record'),
true
)
assert.equal(
isRechargeSummaryRoute('/vehicle-mgmt/fuel-recharge/vehicle-recharge-approval'),
isFuelRechargeSummaryRoute('/vehicle-mgmt/fuel-recharge/vehicle-recharge-approval'),
true
)
assert.equal(
isRechargeSummaryRoute('/vehicle-mgmt/fuel-recharge/vehicle-recharge-workflow'),
isFuelRechargeSummaryRoute('/vehicle-mgmt/fuel-recharge/vehicle-recharge-workflow'),
true
)
assert.equal(rechargeSummaryModeFromPath('/vehicle-mgmt/fuel-recharge/vehicle-recharge-apply'), 'fuel')
})
it('ETC 分组子页识别为 etc 模式并展示顶部统计', () => {
assert.equal(
isEtcRechargeSummaryRoute('/vehicle-mgmt/etc-recharge/etc-vehicle-recharge-apply'),
true
)
assert.equal(
isEtcRechargeSummaryRoute('/vehicle-mgmt/etc-recharge/etc-vehicle-recharge-approval'),
true
)
assert.equal(
isEtcRechargeSummaryRoute('/vehicle-mgmt/etc-recharge/etc-vehicle-recharge-record'),
true
)
assert.equal(
isEtcRechargeSummaryRoute('/vehicle-mgmt/etc-recharge/etc-vehicle-recharge-workflow'),
true
)
assert.equal(
isRechargeSummaryRoute('/vehicle-mgmt/etc-recharge/etc-vehicle-recharge-apply'),
false
true
)
assert.equal(
rechargeSummaryModeFromPath('/vehicle-mgmt/etc-recharge/etc-vehicle-recharge-apply'),
'etc'
)
})
it('旧路径与其它车辆页不显示充值统计', () => {
assert.equal(
isRechargeSummaryRoute('/vehicle-mgmt/etc-recharge/vehicle-recharge-apply'),
false
)
assert.equal(isRechargeSummaryRoute('/vehicle-mgmt/vehicle-fuel-card'), false)
assert.equal(isRechargeSummaryRoute('/vehicle-mgmt/vehicle'), false)
assert.equal(rechargeSummaryModeFromPath('/vehicle-mgmt/vehicle'), null)
})
})
+21 -3
View File
@@ -13,12 +13,30 @@ export function rechargeComparison(
}
}
export type RechargeSummaryMode = 'fuel' | 'etc'
export function rechargeSummaryQuery(plate: string, kind: 'etc' | 'fuel') {
return { rechargePlate: plate, rechargeKind: kind }
}
/** 油卡充值分组下的申请/审批/发放/审批人设置 */
export function isFuelRechargeSummaryRoute(path: string) {
return /\/fuel-recharge\/vehicle-recharge-(apply|approval|record|workflow)\/?$/.test(path)
}
/** ETC 充值分组下的申请/审批/发放/审批人设置 */
export function isEtcRechargeSummaryRoute(path: string) {
return /\/etc-recharge\/etc-vehicle-recharge-(apply|approval|record|workflow)\/?$/.test(path)
}
/** 是否展示顶部充值统计(油卡或 ETC 业务子页) */
export function isRechargeSummaryRoute(path: string) {
return /\/fuel-recharge\/vehicle-recharge-(apply|approval|record|workflow)\/?$/.test(
path
)
return isFuelRechargeSummaryRoute(path) || isEtcRechargeSummaryRoute(path)
}
/** 当前路由对应的统计模式;非充值业务页返回 null */
export function rechargeSummaryModeFromPath(path: string): RechargeSummaryMode | null {
if (isEtcRechargeSummaryRoute(path)) return 'etc'
if (isFuelRechargeSummaryRoute(path)) return 'fuel'
return null
}