@@ -0,0 +1,701 @@
< template >
< div class = "overview-page" >
<!-- 顶部统计卡片 -- >
< div class = "stat-row" >
< div class = "stat-card stat-blue" >
< div class = "stat-num" > { { stats . total || 0 } } < / div >
< div class = "stat-label" > 项目总数 < / div >
< / div >
< div class = "stat-card stat-green" >
< div class = "stat-num" > { { stats . completed || 0 } } < / div >
< div class = "stat-label" > 完成 < / div >
< / div >
< div class = "stat-card stat-orange" >
< div class = "stat-num" > { { stats . undone || 0 } } < / div >
< div class = "stat-label" > 未完成 < / div >
< / div >
< div class = "stat-card stat-red" >
< div class = "stat-num" > { { stats . overdue || 0 } } < / div >
< div class = "stat-label" > 逾期 < / div >
< / div >
< / div >
<!-- 项目列表 -- >
< el-card class = "project-card" shadow = "never" >
< div slot = "header" class = "card-header" >
< div class = "title" > < i class = "el-icon-tickets" / > 项目列表 < / div >
< / div >
< div class = "filter-row" >
< el-input
v-model = "query.projectName"
placeholder = "搜索项目编号、名称..."
clearable
size = "small"
class = "filter-search"
@keyup.enter.native ="handleQuery"
@clear ="handleQuery"
/ >
< el-select
v-model = "query.projectStatus"
placeholder = "全部状态"
clearable
size = "small"
class = "filter-status"
@change ="handleQuery"
>
< el-option
v-for = "d in dict.type.sys_project_status"
:key = "d.value"
:label = "d.label"
:value = "d.value"
/ >
< / el-select >
< el-button size = "small" type = "primary" icon = "el-icon-search" @click ="handleQuery" > 搜索 < / el -button >
< / div >
< el-table
v-loading = "loading"
:data = "projectList"
size = "small"
stripe
highlight -current -row
:row-class-name = "rowClassName"
@ row -click = " onRowClick "
>
< el-table-column label = "编号" prop = "projectNum" min -width = " 120 " >
< template slot -scope = " scope " >
< span class = "link-cell" > { { scope . row . projectNum || '—' } } < / span >
< / template >
< / el-table-column >
< el-table-column label = "名称" prop = "projectName" min -width = " 220 " show -overflow -tooltip / >
< el-table-column label = "阶段" min -width = " 100 " align = "center" >
< template slot -scope = " scope " >
< el-tag size = "mini" type = "info" effect = "plain" >
{ { currentStageOf ( scope . row ) || '—' } }
< / el-tag >
< / template >
< / el-table-column >
< el-table-column label = "进度" width = "170" align = "center" >
< template slot -scope = " scope " >
< el-progress
:percentage = "progressOf(scope.row)"
:stroke-width = "10"
:color = "progressColor(scope.row)"
: format = "p => p + '%'"
/ >
< / template >
< / el-table-column >
< el-table-column label = "负责人" prop = "functionary" width = "110" align = "center" >
< template slot -scope = " scope " >
< span > { { scope . row . functionary || '—' } } < / span >
< / template >
< / el-table-column >
< el-table-column label = "更新" prop = "updateTime" width = "120" align = "center" >
< template slot -scope = " scope " >
< span > { { parseTime ( scope . row . updateTime || scope . row . createTime , '{y}-{m}-{d}' ) } } < / span >
< / template >
< / el-table-column >
< el-table-column label = "状态" prop = "projectStatus" width = "100" align = "center" >
< template slot -scope = " scope " >
< dict-tag :options = "dict.type.sys_project_status" :value = "scope.row.projectStatus" / >
< / template >
< / el-table-column >
< / el-table >
< pagination
v-show = "total > 0"
:total = "total"
:page.sync = "query.pageNum"
:limit.sync = "query.pageSize"
@pagination ="getList"
/ >
< / el-card >
<!-- 项目详情面板 -- >
< el-card v-if = "activeProject" class="detail-card" shadow="never" >
< div slot = "header" class = "card-header" >
< div class = "title" >
< span class = "proj-name" > { { activeProject . projectName } } < / span >
< span class = "proj-num" > · { { activeProject . projectNum } } < / span >
< span class = "proj-tail" > — 详情 < / span >
< / div >
< el-button type = "text" size = "small" @click ="closeDetail" > 关闭 < / el -button >
< / div >
< el-tabs v-if = "tabNames.length" v-model="activeTab" v-loading="detailLoading" >
< el -tab -pane
v-for = "t in tabNames"
:key = "t.key"
:name = "t.key"
:label = "t.label"
>
<!-- 项目文件 tab -- >
< template v-if = "t.key === '__files__'" >
< div v-if = "allFiles.length === 0" class="empty-block" > 暂无文件 < / div >
< div v-for = "(f, idx) in allFiles" :key="idx" class="file-row" >
< i class = "el-icon-document file-icon" / >
< a v-if = "f.url" :href="f.url" target="_blank" class="file-name" > {{ f.name }} < / a >
< span v-else class = "file-name no-url" > { { f . name } } < / span >
< el-tag size = "mini" :type = "f.sourceType" effect = "plain" class = "file-source" >
{ { f . sourceLabel } }
< / el-tag >
< span v-if = "f.detail" class="file-detail" > {{ f.detail }} < / span >
< / div >
< / template >
< ! - - 其他任务 tab - - >
< template v-else-if = "t.key === '__other__'" >
< div v-if = "otherTasks.length === 0" class="empty-block" > 暂无未关联进度的任务 < / div >
< div v-for = "tk in otherTasks" :key="tk.taskId" class="task-card" >
< div class = "task-line-1" >
< span class = "task-title" > { { tk . taskTitle || '无标题任务' } } < / span >
< el-tag size = "mini" :type = "taskStateType(tk.state)" effect = "plain" >
{ { taskStateLabel ( tk . state ) } }
< / el-tag >
< el-tag v-if = "tk.taskGrade" size="mini" type="warning" effect="plain" > {{ priorityLabel ( tk.taskGrade ) }} < / el -tag >
< / div >
< div class = "task-line-2" >
< span v-if = "tk.workerNickName"><i class="el-icon-user-solid" / > 执行 : { { tk . workerNickName } } < / span >
< span v-if = "tk.createUserNickName"><i class="el-icon-edit-outline" / > 创建 : { { tk . createUserNickName } } < / span >
< span v-if = "tk.beginTime || tk.finishTime" >
< i class = "el-icon-date" / >
{ { formatRange ( '' , tk . beginTime , tk . finishTime ) } }
< / span >
< / div >
< div v-if = "tk.content" class="task-content" > {{ tk.content }} < / div >
< div v-if = "taskFiles(tk).length" class="task-files" >
< a v-for = "(f, i) in taskFiles(tk)" :key="i"
:href = "f.url" target = "_blank" class = "task-file-chip" >
< i class = "el-icon-paperclip" / > { { f . name } }
< / a >
< / div >
< / div >
< / template >
<!-- 分类 tab : 列出步骤 + 步骤下的任务 -- >
< template v-else >
< div v-if = "!stepsByTab[t.key] || stepsByTab[t.key].length === 0" class="empty-block" >
此分类暂无进度记录
< / div >
< div
v-for = "step in stepsByTab[t.key] || []"
:key = "step.trackId"
class = "step-block"
>
< div class = "step-head" >
< span class = "dot" :class = "stepDotClass(step.status)" / >
< span class = "step-name" > { { step . secondLevelNode || step . stepName || '—' } } < / span >
< el-tag size = "mini" :type = "stepTagType(step.status)" effect = "plain" class = "step-status" >
{ { stepStatusLabel ( step . status ) } }
< / el-tag >
< / div >
< div class = "step-meta" >
< span v-if = "step.tabNode" class="meta-chip"><i class="el-icon-collection-tag" / > { { step . tabNode } } < / span >
< span v-if = "step.nodeHeader || step.header" class="meta-chip"><i class="el-icon-user" / > { { step . nodeHeader || step . header } } < / span >
< span v-if = "step.planStart || step.planEnd" class="meta-chip" >
< i class = "el-icon-date" / >
{ { formatRange ( '计划' , step . planStart , step . planEnd ) } }
< / span >
< span v-if = "step.actualStart || step.actualEnd" class="meta-chip" >
< i class = "el-icon-check" / >
{ { formatRange ( '实际' , step . actualStart , step . actualEnd ) } }
< / span >
< span v-if = "step.supplierName" class="meta-chip"><i class="el-icon-office-building" / > { { step . supplierName } } < / span >
< / div >
< div v-if = "step.specification" class="step-spec" > {{ step.specification }} < / div >
< div v-if = "stepFiles(step).length" class="step-files" >
< a v-for = "(f, i) in stepFiles(step)" :key="i"
:href = "f.url" target = "_blank" class = "task-file-chip" >
< i class = "el-icon-paperclip" / > { { f . name } }
< / a >
< / div >
< div v-if = "(tasksByTrack[step.trackId] || []).length" class="task-list" >
< div v-for = "tk in tasksByTrack[step.trackId]" :key="tk.taskId" class="task-card" >
< div class = "task-line-1" >
< span class = "task-title" > { { tk . taskTitle || '无标题任务' } } < / span >
< el-tag size = "mini" :type = "taskStateType(tk.state)" effect = "plain" >
{ { taskStateLabel ( tk . state ) } }
< / el-tag >
< el-tag v-if = "tk.taskGrade" size="mini" type="warning" effect="plain" > {{ priorityLabel ( tk.taskGrade ) }} < / el -tag >
< / div >
< div class = "task-line-2" >
< span v-if = "tk.workerNickName"><i class="el-icon-user-solid" / > 执行 : { { tk . workerNickName } } < / span >
< span v-if = "tk.createUserNickName"><i class="el-icon-edit-outline" / > 创建 : { { tk . createUserNickName } } < / span >
< span v-if = "tk.beginTime || tk.finishTime" >
< i class = "el-icon-date" / >
{ { parseTime ( tk . beginTime , '{y}-{m}-{d}' ) || '?' } } ~ { { parseTime ( tk . finishTime , '{y}-{m}-{d}' ) || '?' } }
< / span >
< / div >
< div v-if = "tk.content" class="task-content" > {{ tk.content }} < / div >
< div v-if = "taskFiles(tk).length" class="task-files" >
< a v-for = "(f, i) in taskFiles(tk)" :key="i"
:href = "f.url" target = "_blank" class = "task-file-chip" >
< i class = "el-icon-paperclip" / > { { f . name } }
< / a >
< / div >
< / div >
< / div >
< / div >
< / template >
< / el-tab-pane >
< / el-tabs >
< div v-else-if = "!detailLoading" class="empty-block" > 此项目暂无进度数据 < / div >
< / el -card >
< / div >
< / template >
< script >
import { listOverviewProject , getProjectDashboard , getProjectOverviewStats } from '@/api/oa/project'
import { listByIds as listOssByIds } from '@/api/system/oss'
export default {
name : 'ProjectOverview' ,
dicts : [ 'sys_project_status' ] ,
data ( ) {
return {
loading : false ,
detailLoading : false ,
projectList : [ ] ,
total : 0 ,
query : {
pageNum : 1 ,
pageSize : 10 ,
projectName : undefined ,
projectStatus : undefined
} ,
stats : { total : 0 , completed : 0 , undone : 0 , overdue : 0 } ,
activeProject : null ,
activeTab : '' ,
dashboard : null ,
ossMap : { }
}
} ,
computed : {
/** 当前项目所有 step( 顺序保留) */
allSteps ( ) {
const list = ( this . dashboard && this . dashboard . steps ) || [ ]
return list . slice ( ) . sort ( ( a , b ) => ( a . sortNum || 0 ) - ( b . sortNum || 0 ) )
} ,
allTasks ( ) {
return ( this . dashboard && this . dashboard . tasks ) || [ ]
} ,
/** step 按 firstLevelNode 分组(这才是用户口中的"分类") */
stepsByTab ( ) {
const map = { }
for ( const s of this . allSteps ) {
const k = this . normalizeCategory ( s . firstLevelNode ) || '未分类'
if ( ! map [ k ] ) map [ k ] = [ ]
map [ k ] . push ( s )
}
return map
} ,
/** task 按 trackId 分组(仅有 trackId 的) */
tasksByTrack ( ) {
const map = { }
for ( const t of this . allTasks ) {
if ( t . trackId ) {
if ( ! map [ t . trackId ] ) map [ t . trackId ] = [ ]
map [ t . trackId ] . push ( t )
}
}
return map
} ,
/** 没有 trackId 的任务,归到「其他任务」 */
otherTasks ( ) {
return this . allTasks . filter ( t => ! t . trackId )
} ,
/** 动态 tab: 分类 + 其他任务 + 项目文件 */
tabNames ( ) {
const keys = Object . keys ( this . stepsByTab )
const tabs = keys . map ( k => ( { key : k , label : k } ) )
tabs . push ( { key : '__other__' , label : ` 其他任务 ( ${ this . otherTasks . length } ) ` } )
tabs . push ( { key : '__files__' , label : ` 项目文件 ( ${ this . allFiles . length } ) ` } )
return tabs
} ,
/** 汇总所有文件,每条附带来源 */
allFiles ( ) {
const out = [ ]
const push = ( raw , sourceType , sourceLabel , detail ) => {
for ( const f of this . parseFiles ( raw ) ) {
out . push ( { ... this . resolveFile ( f ) , sourceType , sourceLabel , detail } )
}
}
const proj = this . dashboard && this . dashboard . project
if ( proj ) {
push ( proj . accessory , 'success' , '项目附件' , proj . projectName )
push ( proj . closureFiles , 'success' , '项目结项' , proj . projectName )
}
for ( const s of this . allSteps ) {
const stepLabel = s . secondLevelNode || s . stepName || '步骤'
const stepCat = s . firstLevelNode ? this . normalizeCategory ( s . firstLevelNode ) : ''
const detail = stepCat ? ` ${ stepCat } / ${ stepLabel } ` : stepLabel
push ( s . accessory , 'primary' , '步骤附件' , detail )
push ( s . relatedDocs , 'primary' , '相关资料' , detail )
push ( s . relatedImages , 'primary' , '相关图片' , detail )
push ( s . requirementFile , 'primary' , '需求文件' , detail )
push ( s . other , 'primary' , '其他' , detail )
}
for ( const t of this . allTasks ) {
const detail = t . taskTitle || '任务'
push ( t . accessory , 'warning' , '任务附件' , detail )
push ( t . files , 'warning' , '任务文件' , detail )
}
return out
}
} ,
created ( ) {
this . getList ( )
this . refreshStats ( )
} ,
methods : {
handleQuery ( ) {
this . query . pageNum = 1
this . getList ( )
this . refreshStats ( )
} ,
refreshStats ( ) {
getProjectOverviewStats ( {
projectName : this . query . projectName ,
projectStatus : this . query . projectStatus
} ) . then ( res => {
if ( res && res . data ) this . stats = res . data
} )
} ,
getList ( ) {
this . loading = true
listOverviewProject ( this . query ) . then ( res => {
this . projectList = res . rows || [ ]
this . total = res . total || 0
} ) . finally ( ( ) => { this . loading = false } )
} ,
onRowClick ( row ) {
if ( ! row || ! row . projectId ) return
this . activeProject = row
this . loadDashboard ( row . projectId )
} ,
closeDetail ( ) {
this . activeProject = null
this . dashboard = null
this . activeTab = ''
} ,
loadDashboard ( projectId ) {
this . detailLoading = true
this . dashboard = null
this . activeTab = ''
this . ossMap = { }
getProjectDashboard ( projectId ) . then ( res => {
this . dashboard = res . data || null
// 默认选中第一个 tab
const t = this . tabNames [ 0 ]
this . activeTab = t ? t . key : ''
// 异步拉所有 ossId 对应的真实文件名 / URL
this . loadOssInfo ( )
} ) . finally ( ( ) => { this . detailLoading = false } )
} ,
/** 扫描 dashboard 里所有附件字段,集中调一次 listByIds 拉文件名/URL */
loadOssInfo ( ) {
if ( ! this . dashboard ) return
const ids = new Set ( )
const collect = raw => this . parseFiles ( raw ) . forEach ( f => { if ( f . ossId ) ids . add ( f . ossId ) } )
const proj = this . dashboard . project
if ( proj ) { collect ( proj . accessory ) ; collect ( proj . closureFiles ) }
for ( const s of ( this . dashboard . steps || [ ] ) ) {
collect ( s . accessory ) ; collect ( s . relatedDocs ) ; collect ( s . relatedImages )
collect ( s . requirementFile ) ; collect ( s . other )
}
for ( const t of ( this . dashboard . tasks || [ ] ) ) {
collect ( t . accessory ) ; collect ( t . files )
}
if ( ids . size === 0 ) return
listOssByIds ( Array . from ( ids ) . join ( ',' ) ) . then ( res => {
if ( ! res || ! Array . isArray ( res . data ) ) return
const m = { }
for ( const f of res . data ) m [ String ( f . ossId ) ] = f
this . ossMap = m
} ) . catch ( err => { console . warn ( '加载文件信息失败' , err ) } )
} ,
/** 把解析出的文件对象补全名字/URL( 若仅有 ossId 则查 ossMap) */
resolveFile ( f ) {
const meta = f . ossId ? this . ossMap [ String ( f . ossId ) ] : null
const name = f . name && f . name !== f . ossId
? f . name
: ( meta && ( meta . originalName || meta . fileName ) ) || ( f . ossId ? '加载中…' : ( f . name || '附件' ) )
const url = f . url || ( meta && meta . url ) || ''
return { ... f , name , url }
} ,
/** 去掉 "一、" "二、" "1." "(一)" 等中/西文序号前缀 */
normalizeCategory ( raw ) {
if ( ! raw ) return ''
// 先去首尾空白
let s = String ( raw ) . trim ( )
// 匹配前缀:中文数字+、 / 阿拉伯数字+. / 括号包裹的序号 / 罗马数字
s = s . replace ( /^[( (]?\s*[一二三四五六七八九十百千万0-90 -9 IVXLCDM]+\s*[)) ]?\s*[、.. ::·\-—]?\s*/u , '' )
return s . trim ( ) || raw
} ,
rowClassName ( { row } ) {
return this . activeProject && row . projectId === this . activeProject . projectId
? 'row-active' : ''
} ,
/** 当前阶段:取第一个未完成步骤的 tabNode/firstLevelNode 作为"阶段" */
currentStageOf ( row ) {
// 没拉过 dashboard 的项目直接拿不到,先用 projectType 或空
return row . projectType || ''
} ,
progressOf ( row ) {
// 简易:状态 1=100、0=可粗略按 currentStep / step 总数
if ( row . projectStatus === '1' ) return 100
// 暂时按是否到期粗算
if ( ! row . beginTime || ! row . finishTime ) return 30
const start = new Date ( row . beginTime ) . getTime ( )
const end = new Date ( row . finishTime ) . getTime ( )
const now = Date . now ( )
if ( now <= start ) return 5
if ( now >= end ) return 95
return Math . max ( 5 , Math . min ( 95 , Math . round ( ( now - start ) / ( end - start ) * 100 ) ) )
} ,
progressColor ( row ) {
if ( row . projectStatus === '1' ) return '#67c23a'
if ( row . finishTime && new Date ( row . finishTime ) . getTime ( ) < Date . now ( ) ) return '#f56c6c'
return '#409eff'
} ,
stepDotClass ( status ) {
const s = Number ( status )
if ( s === 2 ) return 'dot-done'
if ( s === 1 ) return 'dot-doing'
if ( s === 3 ) return 'dot-pause'
return 'dot-todo'
} ,
stepTagType ( status ) {
const s = Number ( status )
if ( s === 2 ) return 'success'
if ( s === 1 ) return 'primary'
if ( s === 3 ) return 'warning'
return 'info'
} ,
stepStatusLabel ( status ) {
const s = Number ( status )
if ( s === 2 ) return '已完成'
if ( s === 1 ) return '进行中'
if ( s === 3 ) return '暂停'
return '未开始'
} ,
taskStateLabel ( state ) {
const s = Number ( state )
if ( s === 2 ) return '已完成'
if ( s === 1 ) return '进行中'
if ( s === 3 ) return '已延期'
return '待开始'
} ,
taskStateType ( state ) {
const s = Number ( state )
if ( s === 2 ) return 'success'
if ( s === 1 ) return 'primary'
if ( s === 3 ) return 'danger'
return 'info'
} ,
/** 时间区间格式化:缺一边时显示"开始"/"完成",都缺则空 */
formatRange ( prefix , start , end ) {
const s = start ? this . parseTime ( start , '{y}-{m}-{d}' ) : ''
const e = end ? this . parseTime ( end , '{y}-{m}-{d}' ) : ''
const pfx = prefix ? prefix + ' ' : ''
if ( s && e ) return ` ${ pfx } ${ s } ~ ${ e } `
if ( s && ! e ) return ` ${ pfx } 开始 ${ s } `
if ( ! s && e ) return ` ${ pfx } 完成 ${ e } `
return ''
} ,
priorityLabel ( g ) {
const map = { 1 : '低' , 2 : '中' , 3 : '高' , 4 : '紧急' }
return map [ String ( g ) ] || g
} ,
/** 解析后端附件字段
* 兼容 3 种存储格式:
* - `ossId|name|url,,ossId|name|url`
* - 逗号分隔的纯 ossId( 数字串)
* - 逗号分隔的 url
*/
parseFiles ( raw ) {
if ( ! raw ) return [ ]
const str = String ( raw ) . trim ( )
if ( ! str ) return [ ]
// 形式 1: ossId|name|url,,...
if ( str . includes ( '|' ) || str . includes ( ',,' ) ) {
return str . split ( ',,' ) . map ( s => {
const [ ossId , name , url ] = s . split ( '|' )
return { ossId , name : name || '' , url : url || '' }
} ) . filter ( f => f . ossId || f . url || f . name )
}
// 形式 2/3: 逗号分隔
return str . split ( ',' ) . map ( u => u . trim ( ) ) . filter ( Boolean ) . map ( u => {
if ( /^\d{6,}$/ . test ( u ) ) {
// 纯数字串:是 ossId
return { ossId : u , name : '' , url : '' }
}
return { name : u . split ( '/' ) . pop ( ) || '附件' , url : u }
} )
} ,
taskFiles ( task ) {
return this . parseFiles ( task . accessory ) . concat ( this . parseFiles ( task . files ) ) . map ( this . resolveFile )
} ,
stepFiles ( step ) {
return [ ] . concat (
this . parseFiles ( step . accessory ) ,
this . parseFiles ( step . relatedDocs ) ,
this . parseFiles ( step . relatedImages ) ,
this . parseFiles ( step . requirementFile ) ,
this . parseFiles ( step . other )
) . map ( this . resolveFile )
}
}
}
< / script >
< style scoped lang = "scss" >
. overview - page {
padding : 8 px ;
}
. stat - row {
display : flex ;
gap : 8 px ;
margin - bottom : 8 px ;
}
. stat - card {
flex : 1 ;
background : # fff ;
border - radius : 4 px ;
padding : 8 px 12 px ;
position : relative ;
box - shadow : 0 1 px 2 px rgba ( 0 , 0 , 0 , 0.04 ) ;
border - left : 3 px solid # 409 eff ;
. stat - num { font - size : 18 px ; font - weight : 600 ; line - height : 1.2 ; color : # 303133 ; }
. stat - label { margin - top : 2 px ; color : # 909399 ; font - size : 12 px ; }
& . stat - blue { border - left - color : # 409 eff ; }
& . stat - green { border - left - color : # 67 c23a ; }
& . stat - orange { border - left - color : # e6a23c ; }
& . stat - red { border - left - color : # f56c6c ; }
}
. project - card , . detail - card {
margin - bottom : 8 px ;
: : v - deep . el - card _ _header { padding : 8 px 12 px ; }
: : v - deep . el - card _ _body { padding : 10 px 12 px ; }
}
. card - header {
display : flex ;
justify - content : space - between ;
align - items : center ;
. title { font - weight : 600 ; color : # 303133 ; }
. proj - num { color : # 909399 ; font - weight : normal ; }
. proj - tail { color : # 909399 ; font - weight : normal ; }
}
. filter - row {
display : flex ;
gap : 8 px ;
margin - bottom : 10 px ;
. filter - search { flex : 1 ; max - width : 360 px ; }
. filter - status { width : 140 px ; }
}
. link - cell { color : # 409 eff ; }
: : v - deep . row - active td { background : # ecf5ff ! important ; }
. empty - block {
text - align : center ;
color : # c0c4cc ;
padding : 30 px 0 ;
}
. step - block {
position : relative ;
padding : 6 px 8 px 6 px 18 px ;
border - left : 2 px solid # ebeef5 ;
margin - left : 6 px ;
margin - bottom : 4 px ;
}
. step - head {
display : flex ;
align - items : center ;
gap : 8 px ;
. dot {
width : 10 px ; height : 10 px ; border - radius : 50 % ;
background : # c0c4cc ;
margin - left : - 23 px ;
border : 2 px solid # fff ;
box - shadow : 0 0 0 1 px # dcdfe6 ;
& . dot - done { background : # 67 c23a ; }
& . dot - doing { background : # 409 eff ; }
& . dot - pause { background : # e6a23c ; }
& . dot - todo { background : # c0c4cc ; }
}
. step - time { color : # 909399 ; font - size : 12 px ; }
. step - name { font - weight : 500 ; color : # 303133 ; }
. step - status { margin - left : auto ; }
}
. step - sub { color : # 909399 ; font - size : 12 px ; margin - top : 2 px ; padding - left : 0 ; }
. step - spec { color : # 606266 ; font - size : 13 px ; margin - top : 4 px ; }
. step - meta {
display : flex ; flex - wrap : wrap ; gap : 6 px ;
margin - top : 4 px ;
. meta - chip {
font - size : 12 px ; color : # 606266 ;
padding : 1 px 6 px ;
background : # f4f4f5 ;
border - radius : 3 px ;
i { margin - right : 3 px ; color : # 909399 ; }
}
}
. step - files { margin - top : 4 px ; }
. task - list { margin - top : 6 px ; padding - left : 4 px ; }
. task - card {
background : # f7faff ;
border - left : 3 px solid # 409 eff ;
border - radius : 0 4 px 4 px 0 ;
padding : 6 px 10 px ;
margin : 6 px 0 ;
font - size : 13 px ;
}
. task - line - 1 {
display : flex ; align - items : center ; gap : 6 px ;
. task - title { font - weight : 500 ; color : # 303133 ; flex : 1 ; }
}
. task - line - 2 {
display : flex ; flex - wrap : wrap ; gap : 12 px ;
margin - top : 3 px ;
color : # 909399 ; font - size : 12 px ;
i { margin - right : 2 px ; }
}
. task - content {
margin - top : 4 px ;
color : # 606266 ; font - size : 12 px ;
white - space : pre - wrap ;
max - height : 60 px ; overflow : hidden ;
}
. task - files , . step - files {
display : flex ; flex - wrap : wrap ; gap : 6 px ;
margin - top : 4 px ;
}
. task - file - chip {
display : inline - flex ; align - items : center ; gap : 2 px ;
padding : 1 px 6 px ;
background : # ecf5ff ;
color : # 409 eff ;
border - radius : 3 px ;
font - size : 12 px ;
text - decoration : none ;
& : hover { background : # d9ecff ; }
}
. file - row {
display : flex ; align - items : center ; gap : 8 px ;
padding : 6 px 8 px ;
border - bottom : 1 px dashed # ebeef5 ;
font - size : 13 px ;
& : hover { background : # fafafa ; }
. file - icon { color : # 909399 ; }
. file - name { color : # 409 eff ; flex : 1 ; text - decoration : none ;
& : hover { text - decoration : underline ; }
& . no - url { color : # 606266 ; }
}
. file - source { margin - left : auto ; }
. file - detail { color : # 909399 ; font - size : 12 px ; min - width : 0 ; max - width : 240 px ;
overflow : hidden ; text - overflow : ellipsis ; white - space : nowrap ; }
}
< / style >