@@ -0,0 +1,394 @@
< template >
< div v-can = "permissionCodes.point.ledgerRead" class="point-ledger-detail" >
< customer -profile -card
v-if = "customer"
class = "customer-profile"
:customer = "customer"
@click ="toCustomerDetail"
/ >
< template v-if = "ledger" >
< t -cell -group title = "积分流水" theme = "card" >
< t-cell title = "流水类型" >
< template # note >
< t-tag
:theme = "getDeltaTheme(ledger.pointDelta)"
:content = "getChangeTypeText(ledger.changeType)"
/ >
< / template >
< / t-cell >
< t-cell title = "积分变化" :note = "getPointDeltaText(ledger.pointDelta)" / >
< t-cell title = "变化前余额" :note = "getPointText(ledger.balanceBefore)" / >
< t-cell title = "变化后余额" :note = "getPointText(ledger.balanceAfter)" / >
< t-cell title = "发生时间" >
< template # note >
< pass-time-label :timestamp = "ledger.occurredAt" underline / >
< / template >
< / t-cell >
< t-cell title = "流水备注" :note = "Text.display(ledger.remark)" / >
< / t-cell-group >
< t-cell-group title = "触发事件" theme = "card" >
< template v-if = "triggerEvent" >
< t -cell title = "事件名称" : note = "triggerEvent.name || '未命名事件'" / >
< t-cell title = "事件类型" :note = "getEventValueTypeText(triggerEvent)" / >
< t-cell title = "事件说明" :note = "Text.display(triggerEvent.description)" / >
< / template >
< div v-else class = "empty" v-text = "getEventEmptyText()" / >
< / t-cell-group >
< t-cell-group title = "事件记录" theme = "card" >
< template v-if = "eventRecord" >
< t -cell title = "登记事件" : note = "eventRecord.event?.name || triggerEvent?.name || '未知事件'" / >
< t-cell title = "登记时间" >
< template # note >
< pass-time-label :timestamp = "eventRecord.registeredAt" underline / >
< / template >
< / t-cell >
< t-cell v-if = "hasEventNumberValue" title="事件数值" :note="getEventNumberValueText()" / >
< t-cell v-if = "hasSubmittedPointValue" title="提交积分值" :note="getPointText(eventRecord.pointValue)" / >
< t-cell v-if = "hasEarnedPointValue" title="记录获得积分" :note="getPointText(eventRecord.earnedPointValue)" / >
< t-cell title = "登记人" : note = "Text.display(eventRecord.operator?.nick || eventRecord.operator?.name)" / >
< t-cell title = "记录备注" :note = "Text.display(eventRecord.remark)" / >
< / template >
< div v-else class = "empty" v-text = "getRecordEmptyText()" / >
< / t-cell-group >
< t-cell-group title = "命中规则" theme = "card" >
< template v-if = "hitRule" >
< t -cell title = "规则名称" : note = "hitRule.name || '未命名规则'" / >
< t-cell title = "触发类型" :note = "getTriggerTypeText(hitRule.triggerType)" / >
< t-cell title = "积分模式" :note = "getPointModeText(hitRule.pointMode)" / >
< t-cell title = "规则积分" :note = "getRulePointText(hitRule)" / >
< t-cell title = "匹配条件" :note = "getRuleConditionText(hitRule)" / >
< t-cell title = "规则状态" :note = "getRuleStatusText(hitRule.status)" / >
< t-cell title = "规则优先级" :note = "getPointText(hitRule.priority)" / >
< t-cell title = "命中后停止匹配" : note = "hitRule.exclusive ? '是' : '否'" / >
< t-cell title = "规则备注" :note = "Text.display(hitRule.remark)" / >
< / template >
< div v-else class = "empty" v-text = "getRuleEmptyText()" / >
< / t-cell-group >
< / template >
< t-empty v-else : description = "isLoading ? '正在加载积分流水' : '积分流水快照不存在,请从积分流水列表进入'" / >
< / div >
< / template >
< script setup lang = "ts" >
import { PassTimeLabel , Text , useUserStore } from "timi-web" ;
import CustomerAPI from "@/api/CustomerAPI" ;
import EventAPI from "@/api/EventAPI" ;
import EventRecordAPI from "@/api/EventRecordAPI" ;
import PointAPI from "@/api/PointAPI" ;
import CustomerProfileCard from "@/components/CustomerProfileCard.vue" ;
import type { Customer } from "@/types/Customer" ;
import type { EventItem } from "@/types/EventItem" ;
import type { EventRecord } from "@/types/EventRecord" ;
import type { PointLedger , PointRule , PointRuleMode , PointRuleStatus , PointRuleTriggerType } from "@/types/Point" ;
import { formatEventNumberValue , getEventValueUnitText } from "@/utils/eventValue" ;
import { permissionCodes } from "@/utils/permission" ;
type SourcePayloadItem = {
key : string ;
value : string ;
} ;
defineOptions ( {
name : "PointLedgerDetail"
} ) ;
const route = useRoute ( ) ;
const router = useRouter ( ) ;
const userStore = useUserStore ( ) ;
// ---------- 权限 ----------
const canReadCustomer = computed ( ( ) => userStore . hasPermissions ( permissionCodes . customer . read ) ) ;
const canReadEvent = computed ( ( ) => userStore . hasPermissions ( permissionCodes . event . read ) ) ;
const canReadEventRecord = computed ( ( ) => userStore . hasPermissions ( permissionCodes . eventRecord . read ) ) ;
const canReadRule = computed ( ( ) => userStore . hasPermissions ( permissionCodes . pointRule . read ) ) ;
// ---------- 流水与关联数据 ----------
const ledger = ref < PointLedger > ( ) ;
const customer = ref < Customer > ( ) ;
const triggerEvent = ref < EventItem > ( ) ;
const sourceEvent = ref < EventItem > ( ) ;
const eventRecord = ref < EventRecord > ( ) ;
const hitRule = ref < PointRule > ( ) ;
const isLoading = ref ( false ) ;
const hasEventNumberValue = computed ( ( ) => eventRecord . value ? . numberValue !== undefined && eventRecord . value ? . numberValue !== null ) ;
const hasSubmittedPointValue = computed ( ( ) => eventRecord . value ? . pointValue !== undefined && eventRecord . value ? . pointValue !== null ) ;
const hasEarnedPointValue = computed ( ( ) => eventRecord . value ? . earnedPointValue !== undefined && eventRecord . value ? . earnedPointValue !== null ) ;
const sourcePayloadList = computed < SourcePayloadItem [ ] > ( ( ) => {
const payload = ledger . value ? . sourcePayload ;
if ( ! payload ) {
return [ ] ;
}
return Object . entries ( payload ) . map ( ( [ key , value ] ) => ( {
key ,
value : formatSourceValue ( value )
} ) ) ;
} ) ;
function getQueryText ( key : string ) : string {
const value = route . query [ key ] ;
return Array . isArray ( value ) ? String ( value [ 0 ] || "" ) : String ( value || "" ) ;
}
function parseLedgerSnapshot ( ) : PointLedger | undefined {
const snapshotText = getQueryText ( "snapshot" ) ;
if ( ! snapshotText ) {
return undefined ;
}
try {
const snapshot = JSON . parse ( snapshotText ) as PointLedger ;
if ( ! snapshot || typeof snapshot !== "object" ) {
return undefined ;
}
return {
... snapshot ,
id : String ( route . params . id || snapshot . id || "" )
} ;
} catch ( error ) {
console . warn ( "积分流水快照解析失败" , error ) ;
return undefined ;
}
}
async function loadOptional < T > ( loader : ( ) => Promise < T > , message : string ) : Promise < T | undefined > {
try {
return await loader ( ) ;
} catch ( error ) {
console . warn ( message , error ) ;
return undefined ;
}
}
async function loadPageData ( ) : Promise < void > {
ledger . value = parseLedgerSnapshot ( ) ;
customer . value = undefined ;
triggerEvent . value = undefined ;
sourceEvent . value = undefined ;
eventRecord . value = undefined ;
hitRule . value = undefined ;
if ( ! ledger . value ) {
return ;
}
isLoading . value = true ;
try {
if ( ledger . value . eventRecordId && canReadEventRecord . value ) {
eventRecord . value = await loadOptional ( ( ) => EventRecordAPI . detail ( ledger . value ! . eventRecordId ! ) , "积分流水关联事件记录加载失败" ) ;
}
const eventId = eventRecord . value ? . eventId || ledger . value . eventId ;
const eventResult = eventRecord . value ? . event
? Promise . resolve ( eventRecord . value . event )
: eventId && canReadEvent . value
? loadOptional ( ( ) => EventAPI . detail ( eventId ) , "积分流水触发事件加载失败" )
: Promise . resolve ( undefined ) ;
const ruleResult = ledger . value . ruleId && canReadRule . value
? loadOptional ( ( ) => PointAPI . ruleDetail ( ledger . value ! . ruleId ! ) , "积分流水命中规则加载失败" )
: Promise . resolve ( undefined ) ;
const customerResult = ! eventRecord . value ? . customer && ledger . value . customerId && canReadCustomer . value
? loadOptional ( ( ) => CustomerAPI . detail ( ledger . value ! . customerId ! ) , "积分流水客户加载失败" )
: Promise . resolve ( undefined ) ;
[ triggerEvent . value , hitRule . value , customer . value ] = await Promise . all ( [ eventResult , ruleResult , customerResult ] ) ;
if ( eventRecord . value ? . customer ) {
customer . value = eventRecord . value . customer ;
}
const sourceEventId = hitRule . value ? . conditionJson ? . sourceEventId ;
if ( sourceEventId && sourceEventId !== eventId && canReadEvent . value ) {
sourceEvent . value = await loadOptional ( ( ) => EventAPI . detail ( sourceEventId ) , "积分规则来源事件加载失败" ) ;
}
} finally {
isLoading . value = false ;
}
}
// ---------- 展示 ----------
function getChangeTypeText ( changeType ? : PointLedger [ "changeType" ] ) : string {
return ( {
EARN : "获得积分" ,
REVOKE : "撤销积分" ,
ADJUST : "人工调整"
} as Record < string , string > ) [ changeType || "" ] || "未知流水" ;
}
function getDeltaTheme ( pointDelta ? : number ) : "success" | "danger" {
return pointDelta !== undefined && 0 < pointDelta ? "success" : "danger" ;
}
function getPointDeltaText ( pointDelta ? : number ) : string {
if ( pointDelta === undefined || pointDelta === null ) {
return "-" ;
}
return ` ${ 0 < pointDelta ? "+" : "" } ${ pointDelta } 分 ` ;
}
function getPointText ( value ? : number | null ) : string {
return value === undefined || value === null ? "-" : ` ${ value } 分 ` ;
}
function getEventValueTypeText ( event : EventItem ) : string {
if ( event . valueType === "NUMBER" ) {
return ` 数值事件( ${ getEventValueUnitText ( event . valueUnit ) } ) ` ;
}
return "普通事件" ;
}
function getEventNumberValueText ( ) : string {
return formatEventNumberValue ( eventRecord . value ? . event || triggerEvent . value , eventRecord . value ? . numberValue ) ;
}
function getTriggerTypeText ( triggerType ? : PointRuleTriggerType ) : string {
return ( {
EVERY _TIME : "每次登记" ,
FIRST _TIME : "首次登记" ,
AFTER _COUNT _EVERY _TIME : "达到阈值后每次登记" ,
SOURCE _VALUE : "登记时自定义积分" ,
CUSTOMER _EVENT _VALUE _TIER : "按来源事件累计分档"
} as Record < string , string > ) [ triggerType || "" ] || "未知触发类型" ;
}
function getPointModeText ( pointMode ? : PointRuleMode ) : string {
return ( {
FIXED : "固定积分" ,
SOURCE _VALUE : "使用登记时填写的积分" ,
TIER : "分档积分"
} as Record < string , string > ) [ pointMode || "" ] || "未知积分模式" ;
}
function getRulePointText ( rule : PointRule ) : string {
if ( rule . pointMode === "TIER" ) {
return rule . conditionJson ? . tiers ? . length ? ` ${ rule . conditionJson . tiers . length } 档 ` : "分档积分" ;
}
return getPointText ( rule . pointValue ) ;
}
function getRuleConditionText ( rule : PointRule ) : string {
const condition = rule . conditionJson ;
if ( ! condition ) {
return "无额外条件" ;
}
const textList : string [ ] = [ ] ;
if ( condition . min !== undefined || condition . max !== undefined ) {
textList . push ( ` 数值范围 ${ condition . min ? ? "不限" } ~ ${ condition . max ? ? "不限" } ` ) ;
}
if ( condition . threshold !== undefined ) {
textList . push ( ` 累计达到 ${ condition . threshold } 次 ` ) ;
}
if ( condition . includeCurrent !== undefined ) {
textList . push ( condition . includeCurrent ? "包含本次登记" : "不含本次登记" ) ;
}
if ( condition . window ) {
textList . push ( ` 累计窗口 ${ condition . window . amount } ${ condition . window . unit === "YEAR" ? "年" : condition . window . unit } ` ) ;
}
if ( condition . sourceEventId ) {
textList . push ( ` 来源事件: ${ sourceEvent . value ? . name || sourceEvent . value ? . id || condition . sourceEventId } ` ) ;
}
if ( condition . tiers ? . length ) {
textList . push ( condition . tiers . map ( tier => ` ${ tier . min } ~ ${ tier . max ? ? "以上" } : ${ tier . pointValue } 分 ` ) . join ( ", " ) ) ;
}
return textList . join ( "; " ) || "无额外条件" ;
}
function getRuleStatusText ( status ? : PointRuleStatus ) : string {
return ( {
DRAFT : "草稿" ,
ACTIVE : "启用" ,
INACTIVE : "停用" ,
DELETED : "已删除"
} as Record < string , string > ) [ status || "" ] || "未知状态" ;
}
function getEventEmptyText ( ) : string {
if ( ledger . value ? . changeType === "ADJUST" ) {
return "该流水为人工调整,未关联触发事件" ;
}
return canReadEvent . value ? "未找到触发事件" : "暂无触发事件查看权限" ;
}
function getRecordEmptyText ( ) : string {
if ( ledger . value ? . changeType === "ADJUST" ) {
return "该流水为人工调整,未关联事件记录" ;
}
return canReadEventRecord . value ? "未找到事件记录" : "暂无事件记录查看权限" ;
}
function getRuleEmptyText ( ) : string {
if ( ledger . value ? . changeType === "ADJUST" ) {
return "该流水为人工调整,未命中积分规则" ;
}
return canReadRule . value ? "未找到命中规则" : "暂无命中规则查看权限" ;
}
function formatSourceValue ( value : unknown ) : string {
if ( value === null || value === undefined ) {
return "-" ;
}
if ( typeof value === "string" ) {
return value || "-" ;
}
const text = JSON . stringify ( value ) ;
return text === undefined ? String ( value ) : text ;
}
// ---------- 导航 ----------
async function toCustomerDetail ( ) : Promise < void > {
if ( customer . value ? . id ) {
await router . push ( ` /management/customers/ ${ customer . value . id } ` ) ;
}
}
watch ( ( ) => [ route . params . id , route . query . snapshot ] , async ( ) => {
await loadPageData ( ) ;
} , { immediate : true } ) ;
< / script >
< style scoped lang = "less" >
. point - ledger - detail {
gap : 1 rem ;
min - height : 100 vh ;
padding : calc ( var ( -- app - nav - height ) + 1 rem ) 0 1 rem ;
display : flex ;
flex - direction : column ;
. customer - profile ,
. payload - panel {
margin : 0 1 rem ;
}
. empty {
padding : 1.5 rem 1 rem ;
color : # 52607 a ;
font - size : .875 rem ;
text - align : center ;
}
. payload - panel {
. title {
margin : 0 0 .5 rem ;
font - size : 1 rem ;
}
}
}
: global ( html . theme - dark ) {
. point - ledger - detail {
. empty {
color : # 94 a3b8 ;
}
. payload - panel . title {
color : # e2e8f0 ;
}
}
}
< / style >