日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 前端技术 > vue >内容正文

vue

vue 改变domclass_基于 vue 开发甘特图组件的心路历程(兼设计分享)

發布時間:2025/4/5 vue 31 豆豆
生活随笔 收集整理的這篇文章主要介紹了 vue 改变domclass_基于 vue 开发甘特图组件的心路历程(兼设计分享) 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

語雀原文 有更好的排版體驗~

這篇文章主要講述筆者開發 v-gantt 甘特圖組件的經過。

起源

公司項目有個甘特圖的需求。

筆者考察了世面上 常見的甘特圖組件 后,本著 我上我也行 的心態,以及考慮之前拓展其他開源組件時的痛苦體驗,決定自研。一番設計后,就開始動工了。

設計

布局拆解

首先對目標界面進行拆解。可以看到甘特圖組件是由 左側可折疊的樹 & 右側條狀圖 兩部分組成的。

滾動方面, 頭部日期 header 在橫向滾動中應當跟隨條狀圖內容;在縱向滾動中保持固定。 左側樹 組件則相反。考慮到拆解組件時已經將左右側分離了。所以縱向滾動行為會通過 同步兩邊縱向滾動容器的 scrollTop 這一 js 技巧來實現。

繼續拆解右側條狀圖組件。可以觀察到其由三部分組成:

  • 日期列表。展示整個甘特圖數據的跨越日期范圍。此外還負責展示工作日、節假日信息,以及其他的一些高亮信息
  • 工具欄。放置一些改變視圖的操作按鈕
  • 條狀圖容器。布局條狀圖節點
  • 繼續拆解,我們會發現實質上甘特圖有三種節點:

    • 群組節點。包含自身進度狀態和若干子節點的節點
    • 葉子節點。沒有子節點的節點
    • 里程碑節點。特殊的葉子節點。持續時間僅為一天。只包含 完成 & 未完成 兩種狀態

    其中群組節點內又是一個布局容器。所以實際實現會有一個遞歸的處理。

    組件設計

    拆解完布局,也就得到了具體的 vue 組件設計~

    基本就是 布局拆解 的代碼實現。

    數據結構

    定義好組件后,我們需要設計組件所需要的數據結構。這里我們結合 typescript 定義來設計數據分層。

    組件接受的 data 屬性類型

    // 首先是傳入的總 data,本質是節點的數組 type GanttPropData = GanttPropNode[]// 節點共有三種類型 type GanttPropNode = GanttPropItem | GanttPropGroup | GanttPropMilestone// 葉子節點類型 interface GanttPropItem {id: string // 唯一標識name: string // 名稱startDate: string // 起始時間endDate: string // 結束時間progress: number // 0 - 100 }// 群組節點類型。基本與葉子節點相同,多一個 children 屬性又是一個節點的數組 // 區別是:起始時間和進度都是可選的。這是因為甘特圖內會忽略這些數據,并完全基于其子節點數據生成 interface GanttPropGroup {id: stringname: stringstartDate?: stringendDate?: stringprogress?: numberchildren: GanttPropData }// 甘特圖節點類型 interface GanttPropMilestone {id: stringname: stringdate: string // 單一日期done: boolean // 完成狀態 }

    這就是甘特圖組件所接收的數據類型。但這里仍缺失群組節點的 持續時間 & 進度狀態 數據。所以內部要進行一層轉換。

    完整數據

    // 基本數據結構仍相同 type GanttData = GanttNode[] type GanttNode = GanttItem | GanttGroup | GanttMilestone// 群組節點所有可選項都是必填 interface GanttGroup {id: stringname: stringstartDate: stringendDate: stringprogress: numberchildren: GanttData }// 其他節點類型完全一致 type GanttItem = GanttPropItem type GanttMilestone = GanttPropMilestone

    可以看到關鍵轉換基本發生在群組節點的數據類型上。下面是核心的 transform & transformGroup 算法。

    function transform(data: GanttPropData): GanttData {return data.map((d) => {if (isGroup(d)) {return transformGroup(d)} else if (isMilestone(d)) {return transformMilestone(d)} else {return transformItem(d)}}) }function transformGroup(g: GanttPropGroup): GanttGroup {return {id: g.id,name: g.name,children: transform(g.children),// 起始時間 = 子節點中最早的起始時間get startDate() {return this.children.reduce((result, c) => {const startDate = isMilestone(c) ? c.date : c.startDatereturn !result || dayjs(startDate).isBefore(result) ? startDate : result}, '')},// 結束時間 = 子節點中最晚的結束時間get endDate() {return this.children.reduce((result, c) => {const endDate = isMilestone(c) ? c.date : c.endDatereturn !result || dayjs(endDate).isAfter(result) ? endDate : result}, '')},// 進度 = 子節點進度的加權平均值(權重 = 持續時間)get progress() {let finished = 0let total = 0this.children.forEach((c) => {if (isMilestone(c)) returnconst duration = dayjs.$duration(c.startDate, c.endDate)finished += duration * c.progresstotal += duration})return finished / total},} }

    數據準備好了,但要給最終的組件使用還是需要進行數據轉換。這里需要產出至少兩份數據:

  • 日期列數據,就是橫跨的日期范圍,并據此去拿工作日數據
  • 布局數據
  • 感興趣的同學可以去看看源碼。這里簡要講講布局數據的產出

    布局數據

    布局數據,具體來說指的就是一個條狀圖究的 長度 、 高度 以及 左上角在布局容器中的定位 。

    我們仍是結合 typescript 定義來分析。

    // 布局數據基礎 interface BaseLayoutItem {x: number // 距離容器左邊距(列數)y: number // 距離容器上邊距(行數)w: number // 橫跨的列數h: number // 縱跨的行數 }type GanttLayoutData = GanttLayoutNode[] type GanttLayoutNode =| GanttLayoutLeaf| GanttLayoutGroup| GanttLayoutMilestone interface GanttLayoutLeaf extends BaseLayoutItem {progress: number } interface GanttLayoutGroup extends GanttLayoutLeaf {children: GanttLayoutData } interface GanttLayoutMilestone extends BaseLayoutItem {done: boolean }

    整個布局數據的結構也是很類似的。其中 progress、done 屬性會作為節點的狀態來展示

    小結

    至此,只要將節點的定位、狀態信息渲染出來,整個甘特圖的展示功能就算完成了。接下來就可以進行更復雜的交互功能設計。

    功能設計

    組件間數據共享 & 跨組件通信

    這是所有功能實現的基礎。

    有些數據,我們是希望其在每個組件都能訪問。可行的方案如下:

    • provide/inject,但在 typescript 環境下缺乏類型提示
    • props & events,但是組件可能嵌套層級較深,比如為了傳遞一個拖拽事件、一個 rowH 屬性,prop 定義和 emit 代碼會遍布每一個組件。

    為此,筆者結合這兩種做法,實現了下述方案:定義一個會傳遍全局的屬性(bus),bus 中會存放全局通用的屬性,同時包含一個簡單的事件發布監聽器。

    interface Bus {rowH: number // 行高和列寬是每一個布局節點都需要的colW: numberee: EventEmitter }interface EventEmitter {on(event: GanttEvent, handler: Function) {}emit(event: GanttEvent, ...args: any) {} }

    有些同學可能會建議直接傳遞某個 vue 實例到各個組件,作為全局環境。比如就是根實例。但筆者傾向于定義好清晰的全局數據接口,而不是將根組件完全暴露給所有子組件。這樣就不像“公交車”,而是“垃圾場”了。

    有了方便的跨組件通信機制,就可以方便的實現各種功能了。

    樹/群組節點折疊實現

    節點折疊,也就是指通過 el-tree 提供的折疊樹節點功能,聯動實現群組節點折疊的效果。那么關鍵點就是:

    • 將 el-tree 提供的節點折疊信息,通知到甘特圖節點這邊
    • 當外部數據發生變更時,已折疊的節點保持它的狀態

    兩點結合起來,就是要求:

  • 需要存放折疊好的節點信息,且全局共享
  • 該信息需要獨立存放而不是依附著 GanttData ,否則數據一刷新(每次 GanttPropData 更新都會生成全新的 GanttData )折疊信息就丟失了
  • 所以,數據定義如下:

    /*** key 是節點 id;value 表示是否被折疊*/ interface CollapsedMap {[id: string]: boolean }// 存放在公交車 bus 上 interface Bus {collapsedMap: CollapsedMap }

    具體步驟是:

  • 監聽 el-tree 的折疊事件(node-click),記錄該節點是否被折疊
  • 在 <gantt-group> 組件監聽自身的折疊情況 this.bus.collapsedMap[this.data.id] ,決定自身高度以及是否顯示子節點們
  • 每當 GanttData 更新時,對 collapsedMap實現的是增量更新
  • // src/index.vue { watch: { // 監聽 GanttPropData 的變化 data: { handler(v) { this.collapsedMap = { // 就是提取所有節點的 key,一個類似 flatmap 的過程,初始 val 都是 false ...getCollapsedMap(v), // 優先保留折疊狀態 ...this.collapsedMap, } }, immediate: true, }, }, }

    拖拽實現

    這是一個比較重頭的功能。

    首先是需求定義:

  • 甘特圖節點可以整體被鼠標拖拽;葉子節點還可以拖拽其右側實現長度更改功能
  • 拖拽時,應限定方向在水平方向
  • 拖拽時,條狀圖應當有相應移位和形變(感覺跟手)
  • 結束拖拽時,甘特圖保持狀態,并通知外界
  • 因為通知外界只需要用我們之前定義的 bus.ee 事件監聽發布器即可實現,所以我們要解決的是:

  • 監聽拖拽事件發生
  • 監聽時,改變節點的布局信息
  • 可選的拖拽實現如下:

  • drag & drop event。但不支持限定方向;不支持自定義 cursor 樣式
  • mouse event 系列。支持細粒度地模擬拖拽,沒有 drag & drop 的限制
  • 第三方拖拽庫。只能說一方面坑的很;另一方面,當你真正理解所需要的功能時,代碼實現并不多
  • 在數據結構定義的章節筆者提過,節點的布局信息是由 GanttLayoutData 決定的。實際并不準確:真實的定位樣式,是需要額外結合 拖拽信息(dragData & resizeData)實現。也就是:

    • 節點的左邊距 x = layoutData.x + dragData.offsetX
    • 節點的長度 w = layoutData.w + resizeData.offsetX

    其中 dragData & resizeData 的實現是:

  • 在節點的 mousedown 事件初始化(offsetX = 0)
  • 在節點的 mousemove 事件變更(offsetX += event.movementX)
  • 在節點的 mousedown 事件,如果發生了實質變更,則通過 bus.ee.emit 發送變更
  • 更具體的信息,可以參考 src/components/gantt-node.vue 源碼

    快速定位功能

    簡單來說,就是點擊 今天 按鈕,今天列跳轉到視窗內;點擊 樹節點 ,對應的條狀圖節點出現在視窗內。

    第一反應是利用瀏覽器內置的 scrollToView api 實現。事實上跳轉到今天列功能就是這樣實現的。

    但后一種聯動要復雜一些。條狀圖節點是通過 transform: translate 絕對定位的節點,再加上先前在 布局拆解 階段提及的兩種滾動行為實現,導致 scrollToView 不能滿足我們的需求。

    那么剩下的方案,就是手動控制 橫向滾動容器的 scrollLeft 和 縱向滾動容器的 scrollTop 。

    具體操作是:

  • 監聽樹節點的 click 事件,通過 bus.ee.emit 發送 ScrollToNodeEvent ,攜帶節點 id
  • 所有 <gantt-node> 組件都會監聽該事件。當發現事件 id 與自身 id 匹配時,通過 bus.ee.emit 發送 ScrollToEvent ,攜帶自身在 頂級 <gantt-layout> 容器中的位置。注意這里需要遞歸往上累積計算
  • <gantt-chart> 組件(掌控著兩個滾動容器的位置)監聽 ScrollToEvent ,調整兩個滾動容器的 scrollTop & scrollLeft 值
  • 哇,三言兩語就說完了呢~ 實際開發的時候踩了不少坑 (特別是折磨在 drag & drop api 上)

    具體實現

    這一部分,感興趣的朋友就可以 參考源碼 啦~ 也可以直接通過社交平臺(github)的 issue & pr 功能與筆者交流。

    結語

    這次歷時三周的開發,讓筆者體會到設計先行對代碼質量的提升。核心體驗就是開發是遵照著目標前進的,可以專心于邏輯的同時保持組件設計分工的穩定。

    另外,架構是且應當是不斷演進的。不同的功能復雜度適合不同的架構復雜度。事實上,就跨組件通信機制而言組件內部就重構了一兩次。但筆者認為這些勞動都是值得的。只有抱著 架構應當持續演進 的心態,就能保持好隨時 微重構 的心態,保持對添加新功能的信息~ 。最后再借《反脆弱》一書的核心論點論證:一個貌似穩定的系統其實是畏懼變化、僵死且具有脆弱性的系統;一個貌似反復變更、時不時破壞重建的系統,則是能夠不斷演進,持續變強。

    感謝閱讀!

    總結

    以上是生活随笔為你收集整理的vue 改变domclass_基于 vue 开发甘特图组件的心路历程(兼设计分享)的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。