@@ -12,17 +12,17 @@
< div class = "field field-time" >
< select class = "select" v-model = "timeStatMode"><option value="cutoff" > 日期截止统计 < / option > < / select >
< input class = "date-input" type = "date" v-model = "cutoffDateDraft" / >
< button class = "btn btn-sm btn-accent" type = "button" @click ="applyCutoffDate" > 查询 < / button >
< button class = "btn btn-sm btn-accent" type = "button" :disabled = "queryLoading" @click ="applyCutoffDate" > {{ queryLoading ? " 查询中... " : " 查询 " }} < / button >
< div class = "kpi kpi-progress" > 整体完成百分比 : < span class = "kpi-number" > { { formatPercentValue ( overallPercent , 2 ) } } < / span > < / div >
< / div >
< div class = "field field-percent" >
< span class = "field-label" > 进度百分比 < / span >
< label class = "chip chip-status" > < input type = "checkbox" :checked = "percentFilters.all" @change ="togglePercentFilter('all', $event.target.checked)" / > 全部 < / label >
< label class = "chip chip-status chip-gray" > < input type = "checkbox" :checked = "percentFilters.p0" @change ="togglePercentFilter('p0', $event.target.checked)" / > 0 % < / label >
< label class = "chip chip-status chip-red" > < input type = "checkbox" :checked = "percentFilters.p0_50" @change ="togglePercentFilter('p0_50', $event.target.checked)" / > 0 % - 50 % < / label >
< label class = "chip chip-status chip-blue" > < input type = "checkbox" :checked = "percentFilters.p50_100" @change ="togglePercentFilter('p50_100', $event.target.checked)" / > 50 % - 100 % < / label >
< label class = "chip chip-status chip-green" > < input type = "checkbox" :checked = "percentFilters.p100" @change ="togglePercentFilter('p100', $event.target.checked)" / > 100 % < / label >
< label class = "chip chip-status" : class = "{ 'is-on': percentFilters.all }" > < input type = "checkbox" :checked = "percentFilters.all" @change ="togglePercentFilter('all', $event.target.checked)" / > 全部 < / label >
< label class = "chip chip-status chip-gray" : class = "{ 'is-on': percentFilters.p0 }" > < input type = "checkbox" :checked = "percentFilters.p0" @change ="togglePercentFilter('p0', $event.target.checked)" / > 0 % < / label >
< label class = "chip chip-status chip-red" : class = "{ 'is-on': percentFilters.p0_50 }" > < input type = "checkbox" :checked = "percentFilters.p0_50" @change ="togglePercentFilter('p0_50', $event.target.checked)" / > 0 % - 50 % < / label >
< label class = "chip chip-status chip-blue" : class = "{ 'is-on': percentFilters.p50_100 }" > < input type = "checkbox" :checked = "percentFilters.p50_100" @change ="togglePercentFilter('p50_100', $event.target.checked)" / > 50 % - 100 % < / label >
< label class = "chip chip-status chip-green" : class = "{ 'is-on': percentFilters.p100 }" > < input type = "checkbox" :checked = "percentFilters.p100" @change ="togglePercentFilter('p100', $event.target.checked)" / > 100 % < / label >
< / div >
< / div >
< / section >
@@ -205,7 +205,7 @@
< / template >
< script setup >
import { computed , nextTick , onMounted , reactive , ref } from "vue" ;
import { computed , nextTick , onMounted , reactive , ref , watch } from "vue" ;
import { useRouter } from "vue-router" ;
import ModelPlaceholder from "../../components/model-placeholder/index.vue" ;
import { progressApi } from "../../service/api/progress.js" ;
@@ -222,6 +222,7 @@ const bottomCollapsed = ref(false);
const selectedStructureId = ref ( "" ) ;
const expanded = ref ( new Set ( ) ) ;
const loading = ref ( false ) ;
const queryLoading = ref ( false ) ;
const showContextMenu = ref ( false ) ;
const contextMenuX = ref ( 0 ) ;
@@ -321,10 +322,12 @@ const baselineOverallPercent = ref(0);
const cutoffDate = ref ( todayISODate ( ) ) ;
const cutoffDateDraft = ref ( cutoffDate . value ) ;
const selectedPeriod = computed ( ( ) => cutoffDateDraft . value || cutoffDate . value || todayISODate ( ) ) ;
const overallProgressItems = ref ( [ ] ) ;
const percentFilters = reactive ( { all : false , p0 : false , p0 _50 : false , p50 _100 : false , p100 : false } ) ;
const filteredProgressCodeData = ref ( [ ] ) ;
const filteredProgressColorParams = ref ( [ ] ) ;
const codeWbsMappings = ref ( [ ] ) ;
const percentColorMap = {
p0 : "#B0B8C4" ,
p0 _50 : "#D9363E" ,
@@ -364,8 +367,7 @@ const selectedStructureNode = computed(() => {
} ) ;
const overallPercent = computed ( ( ) => {
if ( ! selectedStructureNode . value ) return 0 ;
return 0 ;
return calculateOverallProgressPercent ( overallProgressItems . value ) ;
} ) ;
const progressRows = computed ( ( ) => {
@@ -553,13 +555,29 @@ function normalizeIsLeaf(node) {
}
onMounted ( async ( ) => {
await modelCenterStore . loadModels ( ) . catch ( ( error ) => console . error ( "加载模型列表失败:" , error ) ) ;
await loadWbsTree ( ) ;
window . addEventListener ( "click" , closeContextMenu ) ;
const result = await progressApi . calculateProgress ( selectedPeriod . value ) ;
const result = await progressApi . calculateProgress ( selectedPeriod . value , getActiveBackendModelId ( ) );
console . log ( 888888 , result )
await fetchCompletedProgressResult ( ) . catch ( ( error ) => console . error ( "获取整体进度失败:" , error ) ) ;
// taskId.value = result.taskId
} ) ;
watch (
( ) => modelCenterStore . activeId ,
async ( ) => {
modelRef . value ? . cancelLightModels ? . ( ) ;
filteredProgressCodeData . value = [ ] ;
filteredProgressColorParams . value = [ ] ;
await progressApi . calculateProgress ( selectedPeriod . value , getActiveBackendModelId ( ) ) ;
await fetchCompletedProgressResult ( ) . catch ( ( error ) => console . error ( "刷新整体进度失败:" , error ) ) ;
if ( hasActivePercentFilter ( ) ) {
await refreshFilteredProgressCodeData ( ) ;
}
}
) ;
function todayISODate ( ) {
return new Date ( ) . toISOString ( ) . slice ( 0 , 10 ) ;
}
@@ -591,6 +609,43 @@ function formatPercentValue(value, digits = 2) {
return ` ${ Number ( value ) . toFixed ( digits ) } % ` ;
}
function calculateOverallProgressPercent ( items ) {
if ( ! Array . isArray ( items ) || items . length === 0 ) return 0 ;
let numeratorSum = 0 ;
let denominatorSum = 0 ;
items . forEach ( ( item ) => {
numeratorSum += Number ( item ? . progressNumerator || 0 ) ;
denominatorSum += Number ( item ? . progressDenominator || 0 ) ;
} ) ;
if ( denominatorSum <= 0 ) return 0 ;
return ( numeratorSum / denominatorSum ) * 100 ;
}
function updateOverallProgressItems ( response ) {
overallProgressItems . value = extractProgressItems ( response ) ;
}
function wait ( ms ) {
return new Promise ( ( resolve ) => setTimeout ( resolve , ms ) ) ;
}
async function fetchCompletedProgressResult ( { maxAttempts = 1 , interval = 800 } = { } ) {
let response = null ;
for ( let attempt = 0 ; attempt < maxAttempts ; attempt += 1 ) {
response = await progressApi . getProgress ( selectedPeriod . value , getActiveBackendModelId ( ) ) ;
if ( response ? . status === "COMPLETED" ) {
updateOverallProgressItems ( response ) ;
return response ;
}
if ( response ? . status === "FAILED" ) {
throw new Error ( response ? . error || "进度统计失败" ) ;
}
if ( attempt < maxAttempts - 1 ) await wait ( interval ) ;
}
updateOverallProgressItems ( [ ] ) ;
return response ;
}
function formatMoney ( value ) {
if ( value == null || Number . isNaN ( value ) ) return "--" ;
const abs = Math . abs ( value ) ;
@@ -605,13 +660,45 @@ function formatNumber(value, digits = 0) {
}
function extractProgressItems ( response ) {
if ( Array . isArray ( response ) ) return response ;
if ( Array . isArray ( response ? . current ? . data ) ) return response . current . data ;
if ( Array . isArray ( response ? . data ? . data ) ) return response . data . data ;
if ( Array . isArray ( response ? . data ) ) return response . data ;
const candidates = [
response ,
response ? . data ,
response ? . current ,
response ? . result ,
response ? . data ? . data ,
response ? . data ? . current ,
response ? . data ? . result ,
response ? . current ? . data ,
response ? . result ? . data ,
response ? . data ? . current ? . data ,
response ? . data ? . result ? . data ,
] ;
for ( const candidate of candidates ) {
if ( Array . isArray ( candidate ) ) return candidate ;
}
for ( const candidate of candidates ) {
if ( ! candidate || typeof candidate !== "object" ) continue ;
const nested = Object . values ( candidate ) . find ( ( value ) => Array . isArray ( value ) ) ;
if ( nested ) return nested ;
}
return [ ] ;
}
function resolveProgressRatio ( item ) {
return normalizeProgressRatio (
item ? . progressData ? ?
item ? . progress ? ?
item ? . percent ? ?
item ? . percentage ? ?
item ? . completionRate ? ?
item ? . completePercent ? ?
item ? . rate
) ;
}
function normalizeProgressRatio ( value ) {
const n = Number ( value ) ;
if ( ! Number . isFinite ( n ) ) return 0 ;
@@ -631,16 +718,17 @@ function isProgressMatched(ratio, key) {
function buildModelCodeData ( list ) {
const groupedByUrl = new Map ( ) ;
for ( const item of list ) {
const rawCodeData = String ( item ? . codeData || "" ) . trim ( ) ;
i f ( ! rawCodeData ) continue ;
const params = getParams ( rawCodeData ) ;
const url = String ( params . url || "" ) . trim ( ) ;
const rawId = String ( params . id || "" ) . trim ( ) ;
if ( ! url || ! rawId ) continue ;
const id = Number ( rawId ) ;
const idValue = Number . isFinite ( id ) ? id : rawId ;
if ( ! groupedByUrl . has ( url ) ) groupedByUrl . set ( url , new Set ( ) ) ;
groupedByUrl . get ( url ) . add ( idValue ) ;
const codeItems = Array . isArray ( item ? . codeList ) && item . codeList . length > 0 ? item . codeList : [ item ] ;
for ( const codeItem of codeItems ) {
const params = getParams ( codeItem ? . codeData ? ? codeItem ? . data ? ? codeItem ? . code ? ? item ? . codeData ? ? item ? . data ? ? item ? . code ) ;
const url = String ( params . url || "" ) . trim ( ) ;
const rawId = String ( params . id || "" ) . trim ( ) ;
if ( ! url || ! rawId ) continue ;
const id = Number ( rawId ) ;
const idValue = Number . isFinite ( id ) ? id : rawId ;
if ( ! groupedByUrl . has ( url ) ) groupedByUrl . set ( url , new Set ( ) ) ;
groupedByUrl . get ( url ) . add ( idValue ) ;
}
}
return Array . from ( groupedByUrl . entries ( ) ) . map ( ( [ url , idSet ] ) => ( {
url ,
@@ -648,6 +736,89 @@ function buildModelCodeData(list) {
} ) ) ;
}
async function loadCodeWbsMappings ( ) {
if ( codeWbsMappings . value . length > 0 ) return codeWbsMappings . value ;
try {
const data = await progressApi . getCodeWbsMappings ( ) ;
codeWbsMappings . value = Array . isArray ( data ) ? data : [ ] ;
} catch ( error ) {
console . error ( "获取构件WBS映射失败:" , error ) ;
codeWbsMappings . value = [ ] ;
}
return codeWbsMappings . value ;
}
function collectIdsFromValue ( value ) {
if ( value == null || value === "" ) return [ ] ;
if ( Array . isArray ( value ) ) return value . flatMap ( ( item ) => collectIdsFromValue ( item ) ) ;
return String ( value ) . split ( /[\s,;]+/ ) . map ( ( item ) => item . trim ( ) ) . filter ( Boolean ) ;
}
function collectWbsIds ( item ) {
return new Set ( [
... collectIdsFromValue ( item ? . positionId ) ,
... collectIdsFromValue ( item ? . positionIds ) ,
... collectIdsFromValue ( item ? . partId ) ,
... collectIdsFromValue ( item ? . partIds ) ,
... collectIdsFromValue ( item ? . wbsId ) ,
... collectIdsFromValue ( item ? . wbsIds ) ,
... collectIdsFromValue ( item ? . wbsCode ) ,
... collectIdsFromValue ( item ? . wbsCodes ) ,
... collectIdsFromValue ( item ? . projectWbsId ) ,
... collectIdsFromValue ( item ? . projectWbsIds ) ,
] ) ;
}
function collectCodeIds ( item ) {
const params = getParams ( item ? . codeData ? ? item ? . data ? ? item ? . code ) ;
return new Set ( [
... collectIdsFromValue ( params ? . id ) ,
... collectIdsFromValue ( item ? . codeId ) ,
... collectIdsFromValue ( item ? . codeIds ) ,
... collectIdsFromValue ( item ? . code ) ,
... collectIdsFromValue ( item ? . id ) ,
] ) ;
}
function isSameId ( a , b ) {
return String ( a ) === String ( b ) ;
}
function expandMatchedProgressItems ( matchedItems , mappings ) {
if ( ! Array . isArray ( mappings ) || mappings . length === 0 ) return matchedItems ;
const expanded = [ ... matchedItems ] ;
const seen = new Set ( expanded . map ( ( item ) => String ( item ? . codeData ? ? item ? . data ? ? item ? . code ? ? "" ) ) . filter ( Boolean ) ) ;
matchedItems . forEach ( ( item ) => {
const wbsIds = collectWbsIds ( item ) ;
const codeIds = collectCodeIds ( item ) ;
if ( wbsIds . size === 0 && codeIds . size > 0 ) {
mappings . forEach ( ( mapping ) => {
const mappingCodeIds = collectCodeIds ( mapping ) ;
if ( [ ... codeIds ] . some ( ( id ) => [ ... mappingCodeIds ] . some ( ( mappingId ) => isSameId ( id , mappingId ) ) ) ) {
collectWbsIds ( mapping ) . forEach ( ( id ) => wbsIds . add ( id ) ) ;
}
} ) ;
}
if ( wbsIds . size === 0 ) return ;
mappings . forEach ( ( mapping ) => {
const mappingWbsIds = collectWbsIds ( mapping ) ;
const isSameWbs = [ ... wbsIds ] . some ( ( id ) => [ ... mappingWbsIds ] . some ( ( mappingId ) => isSameId ( id , mappingId ) ) ) ;
if ( ! isSameWbs ) return ;
const key = String ( mapping ? . codeData ? ? mapping ? . data ? ? mapping ? . code ? ? mapping ? . codeId ? ? "" ) ;
if ( key && seen . has ( key ) ) return ;
if ( key ) seen . add ( key ) ;
expanded . push ( mapping ) ;
} ) ;
} ) ;
return expanded ;
}
async function refreshFilteredProgressCodeData ( ) {
// if (!taskId.value) {
// filteredProgressCodeData.value = [];
@@ -657,9 +828,11 @@ async function refreshFilteredProgressCodeData() {
// }
try {
// const response = await progressApi.getProgress(taskId.value);
const response = await progressApi . getProgress ( selectedPeriod . value ) ;
console . log ( 77777 , response )
const response = await progressApi . getProgress ( selectedPeriod . value , getActiveBackendModelId ( ) );
const items = extractProgressItems ( response ) ;
updateOverallProgressItems ( response ) ;
const needsMappingFallback = items . some ( ( item ) => ! Array . isArray ( item ? . codeList ) || item . codeList . length === 0 ) ;
const mappings = needsMappingFallback ? await loadCodeWbsMappings ( ) : [ ] ;
const enabledKeys = [ "p0" , "p0_50" , "p50_100" , "p100" ] . filter ( ( k ) => percentFilters [ k ] ) ;
const activeKeys = percentFilters . all ? [ "p0" , "p0_50" , "p50_100" , "p100" ] : enabledKeys ;
@@ -673,10 +846,11 @@ async function refreshFilteredProgressCodeData() {
const groupedParams = [ ] ;
const allMatchedCodeData = [ ] ;
activeKeys . forEach ( ( key ) => {
const matched = items . filter ( ( item ) => isProgressMatched ( normaliz eProgressRatio( item . progressData ), key ) ) ;
const matched = items . filter ( ( item ) => isProgressMatched ( resolv eProgressRatio( item ) , key ) ) ;
if ( ! matched . length ) return ;
allMatchedCodeData . push ( ... matched . map( ( item ) => item . codeData ) . filter ( Boolean ) ) ;
const modelCodeData = buildModelCodeData ( matched ) ;
const expandedMatched = needsMappingFallback ? expandMatchedProgressItems ( matched , mappings ) : matched ;
allMatchedCodeData . push ( ... expandedMatched . map ( ( item ) => item . codeData ) . filter ( Boolean ) ) ;
const modelCodeData = buildModelCodeData ( expandedMatched ) ;
if ( ! modelCodeData . length ) return ;
groupedParams . push ( {
range : key ,
@@ -700,10 +874,31 @@ async function refreshFilteredProgressCodeData() {
}
}
function applyCutoffDate ( ) {
async function applyCutoffDate ( ) {
if ( queryLoading . value ) return ;
queryLoading . value = true ;
cutoffDate . value = cutoffDateDraft . value || cutoffDate . value || todayISODate ( ) ;
const result = progressApi . calculateProgress ( selectedPeriod . value ) ;
console . log ( 2121 , result )
overallProgressItems . value = [ ] ;
try {
const startResult = await progressApi . calculateProgress ( selectedPeriod . value , getActiveBackendModelId ( ) ) ;
const result = await fetchCompletedProgressResult ( { maxAttempts : startResult ? . status === "COMPLETED" ? 1 : 10 , interval : 800 } ) ;
if ( result ? . status === "COMPLETED" ) {
if ( hasActivePercentFilter ( ) ) {
await refreshFilteredProgressCodeData ( ) ;
}
showToast ( ` 查询成功,已更新 ${ selectedPeriod . value } 的进度统计 ` , "success" ) ;
} else {
modelRef . value ? . cancelLightModels ? . ( ) ;
showToast ( "进度正在统计中,请稍后再次查询" , "success" ) ;
}
} catch ( error ) {
console . error ( "查询进度统计失败:" , error ) ;
overallProgressItems . value = [ ] ;
modelRef . value ? . cancelLightModels ? . ( ) ;
showToast ( error ? . message || "查询失败,请稍后重试" , "error" ) ;
} finally {
queryLoading . value = false ;
}
}
// 计算比例: count / allCount, 并显示百分比
function proportion ( allCount , count ) {
@@ -756,6 +951,10 @@ async function applyAllExclusive(key, checked) {
function togglePercentFilter ( key , checked ) { applyAllExclusive ( key , checked ) . catch ( ( e ) => console . error ( "切换百分比筛选失败:" , e ) ) ; }
function hasActivePercentFilter ( ) {
return percentFilters . all || percentFilters . p0 || percentFilters . p0 _50 || percentFilters . p50 _100 || percentFilters . p100 ;
}
function toggleTreeExpand ( row ) {
if ( row . leaf ) return ;
if ( expanded . value . has ( row . id ) ) {
@@ -809,9 +1008,21 @@ async function onTreeRowClick(row) {
//获取"{url=https://lyz-1259524260.cos.ap-guangzhou.myqcloud.com/iflow/models/417664a3-76c8-4d94-9344-1337246a5d4e, id=353503}" return obj =>{url,id}
function getParams ( str ) {
if ( str && typeof str === "object" ) return str ;
let obj = { } ;
str . replace ( /{([\s\S]+)}/ , '$1' ) . split ( ', ' ) . forEach ( i => {
let [ k , v ] = i . split ( '=' ) ;
const text = String ( str || "" ) . trim ( ) ;
if ( ! text ) return obj ;
try {
const parsed = JSON . parse ( text ) ;
if ( parsed && typeof parsed === "object" ) return parsed ;
} catch ( e ) {
// codeData may be stored as "{url=..., id=...}" instead of JSON.
}
text . replace ( /{([\s\S]+)}/ , '$1' ) . split ( /\s*,\s*/ ) . forEach ( i => {
const index = i . indexOf ( '=' ) ;
if ( index < 0 ) return ;
let k = i . slice ( 0 , index ) ;
let v = i . slice ( index + 1 ) ;
obj [ k ] = v ;
} ) ;
return obj ;
@@ -1197,6 +1408,7 @@ async function loadProgressData(positionId,time) {
. select , . date - input { height : 34 px ; border - radius : 10 px ; border : 1 px solid rgba ( 255 , 255 , 255 , .14 ) ; background : rgba ( 0 , 0 , 0 , .18 ) ; color : rgba ( 255 , 255 , 255 , .9 ) ; padding : 0 10 px ; font - size : 13 px ; font - weight : 700 ; }
. date - input { width : 150 px ; }
. btn { appearance : none ; border : 1 px solid rgba ( 255 , 255 , 255 , .1 ) ; background : rgba ( 255 , 255 , 255 , .08 ) ; color : rgba ( 255 , 255 , 255 , .88 ) ; border - radius : 12 px ; padding : 8 px 10 px ; cursor : pointer ; }
. btn : disabled { cursor : not - allowed ; opacity : .62 ; }
. btn - sm { padding : 6 px 10 px ; border - radius : 999 px ; font - size : 12 px ; font - weight : 800 ; }
. btn - accent { border - color : rgba ( 83 , 214 , 206 , .22 ) ; background : rgba ( 43 , 191 , 178 , .22 ) ; }
@@ -1204,7 +1416,9 @@ async function loadProgressData(positionId,time) {
. kpi - number { font - weight : 900 ; color : rgba ( 83 , 214 , 206 , .95 ) ; margin - left : 4 px ; }
. field - label { font - weight : 900 ; color : rgba ( 244 , 252 , 255 , .92 ) ; font - size : 14 px ; white - space : nowrap ; }
. chip { display : inline - flex ; align - items : center ; gap : 6 px ; padding : 7 px 12 px ; border - radius : 999 px ; border : 1 px solid rgba ( 255 , 255 , 255 , .12 ) ; background : rgba ( 0 , 0 , 0 , .2 ) ; color : rgba ( 255 , 255 , 255 , .88 ) ; font - weight : 800 ; font - size : 13 px ; white - space : nowrap ; }
. chip { display : inline - flex ; align - items : center ; gap : 6 px ; padding : 7 px 12 px ; border - radius : 999 px ; border : 1 px solid rgba ( 255 , 255 , 255 , .12 ) ; background : rgba ( 0 , 0 , 0 , .2 ) ; color : rgba ( 255 , 255 , 255 , .88 ) ; font - weight : 800 ; font - size : 13 px ; white - space : nowrap ; cursor : pointer ; user - select : none ; transition : border - color .18 s ease , background .18 s ease , box - shadow .18 s ease ; }
. chip : hover { border - color : rgba ( 83 , 214 , 206 , .34 ) ; background : rgba ( 83 , 214 , 206 , .12 ) ; }
. chip . is - on { border - color : rgba ( 83 , 214 , 206 , .52 ) ; background : rgba ( 83 , 214 , 206 , .20 ) ; box - shadow : 0 0 0 1 px rgba ( 83 , 214 , 206 , .16 ) inset ; }
. chip input { width : 13 px ; height : 13 px ; margin : 0 ; accent - color : rgba ( 64 , 224 , 208 , .92 ) ; }
. chip - status : : before { content : "" ; width : 10 px ; height : 10 px ; border - radius : 3 px ; border : 1 px solid rgba ( 255 , 255 , 255 , .2 ) ; background : transparent ; }
. chip - status . chip - gray : : before { border - color : rgba ( 110 , 125 , 150 , .45 ) ; background : rgba ( 110 , 125 , 150 , .24 ) ; }