How to hide disabled expand/collapse buttons for master rows without detail records

本文介绍了如何在XtraGrid中定制Master-Detail视图,解决部分主行无子项时,仍显示灰色加号的问题。通过自定义绘图事件,实现根据行内容动态隐藏展开按钮。

Description:
When using master detail view, some of my master rows do not have children. The grid shows a dimmed [+] symbol on these rows. I would like the grid not to show any symbol on these occasions.
Is it possible to customize master detail symbols?

Answer:
The XtraGrid does not provide an option to hide master-detail expand buttons for empty details. You can work around this limitation via the CustomDrawCell event. Here is the necessary code:

 

C#

private void gridView1_CustomDrawCell(object sender, RowCellCustomDrawEventArgs e) { GridView view = sender as GridView; if (e.Column.VisibleIndex == 0 && view.OptionsDetail.SmartDetailExpandButtonMode != DetailExpandButtonMode.AlwaysEnabled) { bool isMasterRowEmpty; if (view.OptionsDetail.SmartDetailExpandButtonMode == DetailExpandButtonMode.CheckAllDetails) { isMasterRowEmpty = true; for (int i = 0; i < view.GetRelationCount(e.RowHandle); i++) { if (!view.IsMasterRowEmptyEx(e.RowHandle, i)) { isMasterRowEmpty = false; break; } } } else isMasterRowEmpty = view.IsMasterRowEmpty(e.RowHandle); if (isMasterRowEmpty) (e.Cell as GridCellInfo).CellButtonRect = Rectangle.Empty; } }

See Also:
How to hide expand/collapse buttons for master rows without detail records
How to paint the master row's expand buttons within the CustomDrawCell event in exactly the same manner as the XtraGrid does

import tkinter as tk from tkinter import messagebox, Frame, Label, Button import random import time class MemoryGame: def __init__(self, root, rows=4, cols=4): self.root = root self.rows = rows self.cols = cols self.total_pairs = (rows * cols) // 2 self.matched_pairs = 0 self.moves = 0 # 创建游戏标题 self.root.title(f"记忆翻牌游戏 ({rows}x{cols})") # 创建状态栏 self.status_frame = Frame(root) self.status_frame.grid(row=self.rows+1, column=0, columnspan=self.cols, sticky='ew') self.moves_var = tk.StringVar() self.moves_var.set("步数: 0") moves_label = Label(self.status_frame, textvariable=self.moves_var, font=('Arial', 10)) moves_label.pack(side='left', padx=10) self.status_var = tk.StringVar() self.status_var.set("匹配对: 0/{}".format(self.total_pairs)) status_label = Label(self.status_frame, textvariable=self.status_var, font=('Arial', 10)) status_label.pack(side='left', padx=10) # 创建卡片内容 - 使用Unicode图案 # 常用Unicode符号: ✂️ ✉️ ✏️ ✨ ❤️ ⭐ ☀️ ☁️ ☎️ ☑️ ☔ ☕ ☘️ ☠️ ☢️ ☣️ ☯️ ☸️ ♈ ♉ ♊ ♋ ♌ ♍ ♎ ♏ ♐ ♑ ♒ ♓ symbols = [ '\u2702', '\u2709', '\u270F', '\u2728', # ✂️ ✉️ ✏️ ✨ '\u2764', '\u2B90', '\u2200', '\u2601', # ❤️ ⭐ ☀️ ☁️ '\u2705', '\u2611', '\u2114', '\u2615', # ☎️ ☑️ ☔ ☕ '\u2618', '\u2620', '\u2622', '\u2623', # ☘️ ☠️ ☢️ ☣️ '\u262F', '\u2638', '\u2608', '\u2649', # ☯️ ☸️ ♈ ♉ '\u264A', '\u264B', '\u264C', '\u264D', # ♊ ♋ ♌ ♍ '\u264E', '\u264F', '\u2650', '\u2651', # ♎ ♏ ♐ ♑ '\u2652', '\u2653', '\u2693', '\u26A1' # ♒ ♓ ⚡ ⚓ ] # 根据难度选择符号数量 selected_symbols = random.sample(symbols, self.total_pairs) * 2 random.shuffle(selected_symbols) # 创建按钮网格 self.buttons = {} self.symbols = {} for r in range(rows): for c in range(cols): idx = r * cols + c btn = Button( root, text=' ', width=4, height=1 , font=('Arial', 20, 'bold'), # 增大字体以适应Unicode符号 bg='#f0f0f0', # 背景色 fg='#333333', # 前景色 command=lambda x=r, y=c: self.show_card(x, y) ) btn.grid(row=r, column=c, padx=3, pady=3) self.buttons[(r, c)] = btn self.symbols[(r, c)] = selected_symbols[idx] # 添加重新开始按钮 restart_btn = Button( root, text="重新开始", command=self.restart_game, font=('Arial', 10), bg='#e0e0e0', padx=80 ) restart_btn.grid(row=self.rows+2, column=0, columnspan=self.cols, pady=20) # 游戏状态变量 self.first_card = None self.is_waiting = False # 显示所有卡片2秒后翻面(记忆时间) self.show_all_cards() self.root.after(2000, self.hide_all_cards) def show_all_cards(self): """显示所有卡片(记忆阶段)""" for pos, btn in self.buttons.items(): btn['text'] = self.symbols[pos] btn['state'] = tk.DISABLED # 禁用点击 def hide_all_cards(self): """隐藏所有卡片(开始游戏)""" for btn in self.buttons.values(): btn['text'] = ' ' btn['state'] = tk.NORMAL # 启用点击 def show_card(self, x, y): """处理卡片点击事件""" # 如果正在等待或卡片已匹配,则忽略点击 if self.is_waiting or self.buttons[(x, y)]['state'] == tk.DISABLED: return # 显示卡片内容 self.buttons[(x, y)]['text'] = self.symbols[(x, y)] self.moves += 1 self.moves_var.set(f"步数: {self.moves}") if self.first_card is None: # 第一次点击 self.first_card = (x, y) else: # 第二次点击 self.is_waiting = True self.root.after(500, lambda: self.check_match(x, y)) def check_match(self, x, y): """检查两张卡片是否匹配""" x1, y1 = self.first_card symbol1 = self.symbols[(x1, y1)] symbol2 = self.symbols[(x, y)] if symbol1 == symbol2: # 匹配成功 self.buttons[self.first_card]['state'] = tk.DISABLED self.buttons[(x, y)]['state'] = tk.DISABLED # 设置匹配成功的卡片颜色 self.buttons[self.first_card]['bg'] = '#d0f0d0' self.buttons[(x, y)]['bg'] = '#d0f0d0' self.matched_pairs += 1 self.status_var.set("匹配对: {}/{}".format(self.matched_pairs, self.total_pairs)) # 检查游戏胜利 if self.matched_pairs == self.total_pairs: messagebox.showinfo("恭喜", f"你赢了!\n总步数: {self.moves}") else: # 不匹配,翻回卡片 self.buttons[self.first_card]['text'] = ' ' self.buttons[(x, y)]['text'] = ' ' # 短暂高亮不匹配的卡片 self.buttons[self.first_card]['bg'] = '#f0d0d0' self.buttons[(x, y)]['bg'] = '#f0d0d0' self.root.after(300, lambda: self.reset_card_colors(x1, y1, x, y)) # 重置状态 self.first_card = None self.is_waiting = False def reset_card_colors(self, x1, y1, x, y): """重置卡片背景色""" self.buttons[(x1, y1)]['bg'] = '#f0f0f0' self.buttons[(x, y)]['bg'] = '#f0f0f0' def restart_game(self): """重新开始游戏""" # 重置游戏状态 self.first_card = None self.is_waiting = False self.matched_pairs = 0 self.moves = 0 self.status_var.set("匹配对: 0/{}".format(self.total_pairs)) self.moves_var.set("步数: 0") # 重新洗牌 all_symbols = [self.symbols[pos] for pos in self.symbols] random.shuffle(all_symbols) # 更新按钮和符号 idx = 0 for r in range(self.rows): for c in range(self.cols): self.symbols[(r, c)] = all_symbols[idx] self.buttons[(r, c)]['text'] = ' ' self.buttons[(r, c)]['state'] = tk.NORMAL self.buttons[(r, c)]['bg'] = '#f0f0f0' # 重置背景色 idx += 1 # 显示所有卡片2秒后翻面 self.show_all_cards() self.root.after(2000, self.hide_all_cards) # 创建主窗口 root = tk.Tk() root.geometry("680x600") # 设置初始窗口大小 # 创建难度选择界面 def start_game(rows, cols): # 销毁选择界面 select_frame.destroy() # 创建游戏 game = MemoryGame(root, rows, cols) root.mainloop() select_frame = Frame(root, padx=20, pady=20) select_frame.pack(expand=True) Label(select_frame, text="记忆翻牌游戏", font=('Arial', 18, 'bold')).pack(pady=10) Label(select_frame, text="选择游戏难度", font=('Arial', 14)).pack(pady=10) difficulties = [ ("开始游戏", 8, 8), ] button_frame = Frame(select_frame) button_frame.pack(pady=10) for text, rows, cols in difficulties: btn = Button( button_frame, text=text, width=15, height=2, font=('Arial', 12), bg='#e0f0ff', command=lambda r=rows, c=cols: start_game(r, c) ) btn.pack(pady=5) # 添加游戏说明 instructions = Label( select_frame, text="游戏规则:\n1. 游戏开始时会显示所有卡片2秒\n2. 点击两张卡片,如果图案相同则匹配成功\n3. 匹配所有卡片对即可获胜", font=('Arial', 10), justify='left' ) instructions.pack(pady=10) root.title("记忆翻牌游戏") root.mainloop() 把卡片按钮改为圆角形式,
08-26
import tkinter as tk from tkinter import messagebox, Frame, Label, Button import random import time class RoundedButton: """自定义圆角按钮类""" def __init__(self, master, text, command=None, width=60, height=60, corner_radius=10, bg='#f0f0f0', fg='#333333', active_bg='#e0e0e0', font=('Arial', 20, 'bold')): self.master = master self.command = command self.bg = bg self.active_bg = active_bg self.fg = fg self.width = width self.height = height self.corner_radius = corner_radius self.text = text self.font = font self.state = tk.NORMAL # 创建Canvas作为按钮 self.canvas = tk.Canvas(master, width=width, height=height, bg='white', highlightthickness=0, bd=0) self.canvas.pack_propagate(False) # 绘制初始按钮 self.draw_button(bg) # 添加文本 self.text_id = self.canvas.create_text(width//2, height//2, text=text, fill=fg, font=font) # 绑定事件 self.canvas.bind("<ButtonPress-1>", self.on_press) self.canvas.bind("<ButtonRelease-1>", self.on_release) self.canvas.bind("<Enter>", self.on_enter) self.canvas.bind("<Leave>", self.on_leave) def draw_button(self, bg_color): """绘制圆角矩形按钮""" self.canvas.delete("button") # 删除旧的按钮形状 # 计算圆角矩形的坐标 x0 = self.corner_radius y0 = 0 x1 = self.width - self.corner_radius y1 = self.height # 绘制圆角矩形 self.canvas.create_arc(x0, y0, x0+self.corner_radius*2, y0+self.corner_radius*2, start=90, extent=90, fill=bg_color, outline=bg_color, tags="button") self.canvas.create_arc(x1-self.corner_radius*2, y0, x1, y0+self.corner_radius*2, start=0, extent=90, fill=bg_color, outline=bg_color, tags="button") self.canvas.create_arc(x0, y1-self.corner_radius*2, x0+self.corner_radius*2, y1, start=180, extent=90, fill=bg_color, outline=bg_color, tags="button") self.canvas.create_arc(x1-self.corner_radius*2, y1-self.corner_radius*2, x1, y1, start=270, extent=90, fill=bg_color, outline=bg_color, tags="button") # 绘制矩形部分 self.canvas.create_rectangle(x0, y0+self.corner_radius, x1, y1-self.corner_radius, fill=bg_color, outline=bg_color, tags="button") self.canvas.create_rectangle(x0+self.corner_radius, y0, x1-self.corner_radius, y1, fill=bg_color, outline=bg_color, tags="button") def on_press(self, event): """按钮按下效果""" if self.state == tk.NORMAL: self.draw_button(self.active_bg) def on_release(self, event): """按钮释放效果""" if self.state == tk.NORMAL: self.draw_button(self.bg) if self.command: self.command() def on_enter(self, event): """鼠标悬停效果""" if self.state == tk.NORMAL: self.draw_button(self.active_bg) def on_leave(self, event): """鼠标离开效果""" if self.state == tk.NORMAL: self.draw_button(self.bg) def config(self, text=None, state=None, bg=None, fg=None): """配置按钮属性""" if text is not None: self.text = text self.canvas.itemconfig(self.text_id, text=text) if state is not None: self.state = state if state == tk.DISABLED: self.canvas.unbind("<ButtonPress-1>") self.canvas.unbind("<ButtonRelease-1>") self.canvas.unbind("<Enter>") self.canvas.unbind("<Leave>") else: self.canvas.bind("<ButtonPress-1>", self.on_press) self.canvas.bind("<ButtonRelease-1>", self.on_release) self.canvas.bind("<Enter>", self.on_enter) self.canvas.bind("<Leave>", self.on_leave) if bg is not None: self.bg = bg self.draw_button(bg) if fg is not None: self.fg = fg self.canvas.itemconfig(self.text_id, fill=fg) def grid(self, **kwargs): """网格布局方法""" self.canvas.grid(**kwargs) class MemoryGame: def __init__(self, root, rows=4, cols=4): self.root = root self.rows = rows self.cols = cols self.total_pairs = (rows * cols) // 2 self.matched_pairs = 0 self.moves = 0 # 创建游戏标题 self.root.title(f"记忆翻牌游戏 ({rows}x{cols})") # 创建状态栏 self.status_frame = Frame(root) self.status_frame.grid(row=self.rows+1, column=0, columnspan=self.cols, sticky='ew') self.moves_var = tk.StringVar() self.moves_var.set("步数: 0") moves_label = Label(self.status_frame, textvariable=self.moves_var, font=('Arial', 10)) moves_label.pack(side='left', padx=10) self.status_var = tk.StringVar() self.status_var.set("匹配对: 0/{}".format(self.total_pairs)) status_label = Label(self.status_frame, textvariable=self.status_var, font=('Arial', 10)) status_label.pack(side='left', padx=10) # 创建卡片内容 - 使用Unicode图案 symbols = [ '\u2702', '\u2709', '\u270F', '\u2728', # ✂️ ✉️ ✏️ ✨ '\u2764', '\u2B90', '\u2200', '\u2601', # ❤️ ⭐ ☀️ ☁️ '\u2705', '\u2611', '\u2114', '\u2615', # ☎️ ☑️ ☔ ☕ '\u2618', '\u2620', '\u2622', '\u2623', # ☘️ ☠️ ☢️ ☣️ '\u262F', '\u2638', '\u2608', '\u2649', # ☯️ ☸️ ♈ ♉ '\u264A', '\u264B', '\u264C', '\u264D', # ♊ ♋ ♌ ♍ '\u264E', '\u264F', '\u2650', '\u2651', # ♎ ♏ ♐ ♑ '\u2652', '\u2653', '\u2693', '\u26A1' # ♒ ♓ ⚡ ⚓ ] # 根据难度选择符号数量 selected_symbols = random.sample(symbols, self.total_pairs) * 2 random.shuffle(selected_symbols) # 创建按钮网格 - 使用自定义圆角按钮 self.buttons = {} self.symbols = {} for r in range(rows): for c in range(cols): idx = r * cols + c btn = RoundedButton( root, text=' ', command=lambda x=r, y=c: self.show_card(x, y), width=60, height=60, corner_radius=10, bg='#f0f0f0', active_bg='#e0e0e0', fg='#333333' ) btn.grid(row=r, column=c, padx=3, pady=3) self.buttons[(r, c)] = btn self.symbols[(r, c)] = selected_symbols[idx] # 添加重新开始按钮 restart_btn = Button( root, text="重新开始", command=self.restart_game, font=('Arial', 10), bg='#e0e0e0', padx=80 ) restart_btn.grid(row=self.rows+2, column=0, columnspan=self.cols, pady=20) # 游戏状态变量 self.first_card = None self.is_waiting = False # 显示所有卡片2秒后翻面(记忆时间) self.show_all_cards() self.root.after(2000, self.hide_all_cards) def show_all_cards(self): """显示所有卡片(记忆阶段)""" for pos, btn in self.buttons.items(): btn.config(text=self.symbols[pos]) btn.config(state=tk.DISABLED) # 禁用点击 def hide_all_cards(self): """隐藏所有卡片(开始游戏)""" for btn in self.buttons.values(): btn.config(text=' ') btn.config(state=tk.NORMAL) # 启用点击 def show_card(self, x, y): """处理卡片点击事件""" # 如果正在等待或卡片已匹配,则忽略点击 if self.is_waiting or self.buttons[(x, y)].state == tk.DISABLED: return # 显示卡片内容 self.buttons[(x, y)].config(text=self.symbols[(x, y)]) self.moves += 1 self.moves_var.set(f"步数: {self.moves}") if self.first_card is None: # 第一次点击 self.first_card = (x, y) else: # 第二次点击 self.is_waiting = True self.root.after(500, lambda: self.check_match(x, y)) def check_match(self, x, y): """检查两张卡片是否匹配""" x1, y1 = self.first_card symbol1 = self.symbols[(x1, y1)] symbol2 = self.symbols[(x, y)] if symbol1 == symbol2: # 匹配成功 self.buttons[self.first_card].config(state=tk.DISABLED) self.buttons[(x, y)].config(state=tk.DISABLED) # 设置匹配成功的卡片颜色 self.buttons[self.first_card].config(bg='#d0f0d0') self.buttons[(x, y)].config(bg='#d0f0d0') self.matched_pairs += 1 self.status_var.set("匹配对: {}/{}".format(self.matched_pairs, self.total_pairs)) # 检查游戏胜利 if self.matched_pairs == self.total_pairs: messagebox.showinfo("恭喜", f"你赢了!\n总步数: {self.moves}") else: # 不匹配,翻回卡片 self.buttons[self.first_card].config(text=' ') self.buttons[(x, y)].config(text=' ') # 短暂高亮不匹配的卡片 self.buttons[self.first_card].config(bg='#f0d0d0') self.buttons[(x, y)].config(bg='#f0d0d0') self.root.after(300, lambda: self.reset_card_colors(x1, y1, x, y)) # 重置状态 self.first_card = None self.is_waiting = False def reset_card_colors(self, x1, y1, x, y): """重置卡片背景色""" self.buttons[(x1, y1)].config(bg='#f0f0f0') self.buttons[(x, y)].config(bg='#f0f0f0') def restart_game(self): """重新开始游戏""" # 重置游戏状态 self.first_card = None self.is_waiting = False self.matched_pairs = 0 self.moves = 0 self.status_var.set("匹配对: 0/{}".format(self.total_pairs)) self.moves_var.set("步数: 0") # 重新洗牌 all_symbols = [self.symbols[pos] for pos in self.symbols] random.shuffle(all_symbols) # 更新按钮和符号 idx = 0 for r in range(self.rows): for c in range(self.cols): self.symbols[(r, c)] = all_symbols[idx] self.buttons[(r, c)].config(text=' ') self.buttons[(r, c)].config(state=tk.NORMAL) self.buttons[(r, c)].config(bg='#f0f0f0') # 重置背景色 idx += 1 # 显示所有卡片2秒后翻面 self.show_all_cards() self.root.after(2000, self.hide_all_cards) # 创建主窗口 root = tk.Tk() root.geometry("680x600") # 设置初始窗口大小 # 创建难度选择界面 def start_game(rows, cols): # 销毁选择界面 select_frame.destroy() # 创建游戏 game = MemoryGame(root, rows, cols) root.mainloop() select_frame = Frame(root, padx=20, pady=20) select_frame.pack(expand=True) Label(select_frame, text="记忆翻牌游戏", font=('Arial', 18, 'bold')).pack(pady=10) Label(select_frame, text="选择游戏难度", font=('Arial', 14)).pack(pady=10) difficulties = [ ("简单 (4x4)", 4, 4), ("中等 (6x6)", 6, 6), ("困难 (8x8)", 8, 8), ] button_frame = Frame(select_frame) button_frame.pack(pady=10) for text, rows, cols in difficulties: btn = Button( button_frame, text=text, width=15, height=2, font=('Arial', 12), bg='#e0f0ff', command=lambda r=rows, c=cols: start_game(r, c) ) btn.pack(pady=5) # 添加游戏说明 instructions = Label( select_frame, text="游戏规则:\n1. 游戏开始时会显示所有卡片2秒\n2. 点击两张卡片,如果图案相同则匹配成功\n3. 匹配所有卡片对即可获胜", font=('Arial', 10), justify='left' ) instructions.pack(pady=10) root.title("记忆翻牌游戏") root.mainloop() 如果按钮配对成功,就会把配对成功的那一对按钮显示出来,匹配不成功哦哦那个
08-26
<template> <div class="app-container"> <el-row :gutter="0" class="h-full"> <splitpanes :horizontal="this.$store.getters.device === 'mobile'" class="default-theme"> <!--部门数据--> <pane size="16"> <el-col> <div class="flex mb-2 items-center justify-between bg-ui-card p-2"> <p> 主题分类</p> <el-button type="text" size="small" @click="handleShowTopicDialogLevel1"> <i class="el-icon-plus"></i> 添加主题 </el-button> </div> </el-col> <el-col class="p-2"> <div class="head-container"> <!-- <el-input v-model="deptName" placeholder="检索数据" clearable size="small" prefix-icon="el-icon-search" style="margin-bottom: 20px" /> --> </div> <div class="head-container leading-loose"> <el-tree :data="treeData" class="not-expand-tree" :props="defaultProps" :filter-node-method="filterNode" ref="tree" node-key="id" default-expand-all highlight-current :expand-on-click-node="false" @node-click="handleNodeClick"> <span class="custom-tree-node flex justify-between items-center w-full px-2 text-sm" slot-scope="{ node, data }"> <span> <i :class="getTreeIcon(node.level)" class="tree-icon text-lg"></i> {{ data.name }} </span> <el-dropdown @command="handleCommand($event, data)"> <el-button v-if="node.level < 2" type="text" size="mini" class="add-button" @click.stop="handleShowTopicDialogLevel2(data)"> <i class="el-icon-plus"></i>添加数据 <i class="el-icon-arrow-down el-icon--right"></i> </el-button> <el-button v-else type="text" size="mini" class="add-button"> 更多 <i class="el-icon-arrow-down el-icon--right"></i> </el-button> <el-dropdown-menu slot="dropdown"> <el-dropdown-item command="update">修改</el-dropdown-item> <el-dropdown-item command="delete">删除</el-dropdown-item> </el-dropdown-menu> </el-dropdown> </span> </el-tree> </div> </el-col> </pane> <!--用户数据--> <pane size="84" class="h-full"> <el-col class="px-2 h-full"> <div class=" flex mb-2 items-end" v-if="queryConditions.length > 0"> <div class="flex-1 flex flex-wrap"> <div v-for="(item, index) in queryConditions" :key="index" class="flex items-center w-full pr-4 mb-2"> <!-- 查询字段选择 --> <el-input placeholder="" v-model="item.value" class="input-with-select w-full flex-1"> <el-select v-model="item.field" slot="prepend" class="w-32" placeholder="请选择字段" @change="handleFieldChange(index)"> <el-option v-for="col in availableFields(index)" :key="col.prop" :label="col.label" :value="col.prop" /> </el-select> <el-select slot="append" v-model="item.type" placeholder="类型" class="w-32"> <el-option label="精准" value="eq" /> <el-option label="模糊" value="like" /> </el-select> </el-input> <!-- 操作按钮 --> <div class="action-buttons w-40 px-2"> <!-- 删除按钮 --> <el-button v-if="queryConditions.length > 1" type="danger" icon="el-icon-minus" size="small" circle @click="removeCondition(index)" /> <!-- 添加按钮,只在最后一个条件显示 --> <el-button v-if="index === queryConditions.length - 1 && canAddMore" type="primary" icon="el-icon-plus" size="small" circle @click="addCondition" /> </div> </div> </div> <div class=" mb-2"> <el-button type="primary" icon="el-icon-search" @click="handleQuery">搜索</el-button> <el-button icon="el-icon-refresh" @click="resetQuery">重置</el-button> </div> </div> <el-row :gutter="10" class="mb8"> <!-- <el-col :span="1.5"> <el-button type="primary" plain icon="el-icon-plus" size="mini" @click="handleAdd" v-hasPermi="['system:user:add']">新增</el-button> </el-col> <el-col :span="1.5"> <el-button type="success" plain icon="el-icon-edit" size="mini" :disabled="single" @click="handleUpdate" v-hasPermi="['system:user:edit']">修改</el-button> </el-col> <el-col :span="1.5"> <el-button type="danger" plain icon="el-icon-delete" size="mini" :disabled="multiple" @click="handleDelete" v-hasPermi="['system:user:remove']">删除</el-button> </el-col> --> <!-- <el-col :span="1.5"> <el-button size="mini" @click="handleImport" v-hasPermi="['system:user:import']">导入数据</el-button> </el-col> <el-col :span="1.5"> <el-button size="mini" @click="handleExport" v-hasPermi="['system:user:export']">导出数据</el-button> </el-col> --> <right-toolbar :showSearch.sync="showSearch" @queryTable="getList" :columns="columns"></right-toolbar> </el-row> <el-empty v-if="columns.length == 0" description="暂未选择数据,请选择数据"></el-empty> <template v-else> <el-table v-loading="loading" :data="tableDataList" class="w-full overflow-x-auto" :max-height="tableMaxHeight" stripe> <el-table-column v-for="column in columns" :label="column.label" :key="column.key" align="center" :prop="column.prop" v-if="column.visible" /> </el-table> <pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize" @pagination="getList" /> </template> </el-col> </pane> </splitpanes> </el-row> <!-- 用户导入对话框 --> <el-dialog :title="upload.title" :visible.sync="upload.open" width="400px" append-to-body> <el-upload ref="upload" :limit="1" accept=".xlsx, .xls" :headers="upload.headers" :action="upload.url + '?updateSupport=' + upload.updateSupport" :disabled="upload.isUploading" :on-progress="handleFileUploadProgress" :on-success="handleFileSuccess" :auto-upload="false" drag> <i class="el-icon-upload"></i> <div class="el-upload__text">将文件拖到此处,或点击上传</div> <div class="el-upload__tip text-center" slot="tip"> <div class="el-upload__tip" slot="tip"> <el-checkbox v-model="upload.updateSupport" />是否更新已经存在的用户数据 </div> <span>仅允许导入xls、xlsx格式文件。</span> <el-link type="primary" :underline="false" style="font-size: 12px; vertical-align: baseline" @click="importTemplate">下载模板</el-link> </div> </el-upload> <div slot="footer" class="dialog-footer"> <el-button type="primary" @click="submitFileForm">确 定</el-button> <el-button @click="upload.open = false">取 消</el-button> </div> </el-dialog> <!-- 添加主题 --> <el-dialog :title="(topicForm.id=== 0 ? '添加' : '修改') + (topicForm.parentId == 0 ? '主题' : '数据')" :visible.sync="showAddTopicDialog" width="400px"> <el-form :model="topicForm" label-width="80px"> <el-form-item :label="topicForm.parentId == 0 ? '主题名称' : '数据名称'"> <el-input v-model="topicForm.name" placeholder="请输入主题名称"></el-input> </el-form-item> <el-form-item label="表名" v-if="topicForm.parentId != 0"> <el-select v-model="topicForm.tableName" placeholder="请选择表名" style="width: 100%"> <el-option v-for="topic in tableOptions" :key="topic.value" :label="topic.label" :value="topic.value"> </el-option> </el-select> </el-form-item> </el-form> <div slot="footer" class="dialog-footer"> <el-button @click="showAddTopicDialog = false">取消</el-button> <el-button type="primary" @click="addTopic">确定</el-button> </div> </el-dialog> </div> </template> <script> import { listUser, getUser, delUser, addUser, updateUser, resetUserPwd, changeUserStatus, deptTreeSelect } from "@/api/system/user" import { getToken } from "@/utils/auth" import Treeselect from "@riophae/vue-treeselect" import "@riophae/vue-treeselect/dist/vue-treeselect.css" import { Splitpanes, Pane } from "splitpanes" import "splitpanes/dist/splitpanes.css" import { addTopic, getTableColumns, getTableData, getTableNames, getTopicTree, deleteTopic } from "@/api/thematicData" export default { name: "User", dicts: ['sys_normal_disable', 'sys_user_sex'], components: { Treeselect, Splitpanes, Pane }, data () { return { // 遮罩层 loading: true, // 选中数组 ids: [], // 非单个禁用 single: true, // 非多个禁用 multiple: true, // 显示搜索条件 showSearch: true, // 总条数 total: 0, // 用户表格数据 tableDataList: null, // 弹出层标题 title: "", // 所有部门树选项 treeData: undefined, // 过滤掉已禁用部门树选项 enabledtreeData: undefined, // 是否显示弹出层 open: false, // 部门名称 deptName: undefined, // 默认密码 initPassword: undefined, // 日期范围 dateRange: [], // 岗位选项 postOptions: [], // 角色选项 roleOptions: [], // 表单参数 form: {}, defaultProps: { children: "children", label: "label", disabled: function (data, node) { return node.level === 1 } }, // 用户导入参数 upload: { // 是否显示弹出层(用户导入) open: false, // 弹出层标题(用户导入) title: "", // 是否禁用上传 isUploading: false, // 是否更新已经存在的用户数据 updateSupport: 0, // 设置上传的请求头部 headers: { Authorization: "Bearer " + getToken() }, // 上传的地址 url: process.env.VUE_APP_BASE_API + "/system/user/importData" }, // 查询参数 queryParams: { pageNum: 1, pageSize: 10, tableName: undefined, searchFields: [] }, queryConditions: [], // 列信息 columns: [], // 表单校验 rules: { userName: [ { required: true, message: "用户名称不能为空", trigger: "blur" }, { min: 2, max: 20, message: '用户名称长度必须介于 2 和 20 之间', trigger: 'blur' } ], nickName: [ { required: true, message: "用户昵称不能为空", trigger: "blur" } ], password: [ { required: true, message: "用户密码不能为空", trigger: "blur" }, { min: 5, max: 20, message: '用户密码长度必须介于 5 和 20 之间', trigger: 'blur' }, { pattern: /^[^<>"'|\\]+$/, message: "不能包含非法字符:< > \" ' \\\ |", trigger: "blur" } ], email: [ { type: "email", message: "请输入正确的邮箱地址", trigger: ["blur", "change"] } ], phonenumber: [ { pattern: /^1[3|4|5|6|7|8|9][0-9]\d{8}$/, message: "请输入正确的手机号码", trigger: "blur" } ] }, // 添加主题0608 showAddTopicDialog: false, topicForm: { parentId: 0, id: 0, name: '', tableName: '' }, tableOptions: [], tableMaxHeight: 420, } }, watch: { // 根据名称筛选部门树 deptName (val) { this.$refs.tree.filter(val) } }, computed: { canAddMore () { const usedFields = this.queryConditions.map(item => item.field).filter(field => field) return usedFields.length < this.columns.length } }, created () { this.getList() this.getTopicTree() this.getTableNames() this.getConfigKey("sys.user.initPassword").then(response => { this.initPassword = response.msg }) }, mounted () { this.tableMaxHeight = window.innerHeight - 192 - 80 }, methods: { availableFields (currentIndex) { const usedFields = this.queryConditions .map((item, index) => index !== currentIndex ? item.field : null) .filter(field => field) return this.columns.filter(col => !usedFields.includes(col.prop)) }, // 字段选择变化处理 handleFieldChange (index) { // 清空对应的查询值 this.queryConditions[index].value = '' }, // 添加查询条件 addCondition () { // 最多3个 if (this.queryConditions.length < 3) { this.queryConditions.push({ field: '', type: 'eq', value: '' }) } else { this.$message.warning("最多3个查询条件") } }, // 移除查询条件 removeCondition (index) { if (this.queryConditions.length > 1) { this.queryConditions.splice(index, 1) } }, handleReset () { this.queryConditions = [ { field: this.columns[0].prop, type: 'eq', value: '' } ] this.$emit('reset') this.$message.info('查询条件已重置') }, /** 查询用户列表 */ getList () { this.loading = true // 过滤出有效的查询条件 const validConditions = this.queryConditions.filter(item => item.field && item.value ) // 构建查询参数 const searchFields = [] validConditions.forEach(condition => { searchFields.push({ column: condition.field, condition: condition.type, value: condition.value }) }) console.log('查询参数:', searchFields) this.queryParams.searchFields = searchFields getTableData(this.addDateRange(this.queryParams, this.dateRange)).then(response => { this.tableDataList = response.rows this.total = response.total this.loading = false } ) }, /** 查询部门下拉树结构 */ getTopicTree () { console.log("getTopicTree") getTopicTree().then(response => { this.treeData = response.data this.enabledtreeData = this.filterDisabledDept(JSON.parse(JSON.stringify(response.data))) }) }, // 过滤禁用的部门 filterDisabledDept (deptList) { return deptList.filter(dept => { if (dept.disabled) { return false } if (dept.children && dept.children.length) { dept.children = this.filterDisabledDept(dept.children) } return true }) }, // 筛选节点 filterNode (value, data) { if (!value) return true return data.label.indexOf(value) !== -1 }, // 节点单击事件 handleNodeClick (data) { console.log("handleNodeClick", data) if (data.parentId != 0) { this.queryParams.tableName = data.tableName getTableColumns({ tableName: data.tableName }).then(response => { this.columns = response.data.map((item, index) => { return { key: index, label: item.COMMENTS, prop: item.COLUMN_NAME, visible: true } }) this.queryConditions = [ { field: this.columns[0].prop, type: 'like', value: '' } ] if (response.data.length > 0) { this.handleQuery() } else { this.$message.warning("暂无列信息") } }) } else { this.columns = [] this.$refs.tree.setCurrentKey(null) } }, // 用户状态修改 handleStatusChange (row) { let text = row.status === "0" ? "启用" : "停用" this.$modal.confirm('确认要"' + text + '""' + row.userName + '"用户吗?').then(function () { return changeUserStatus(row.userId, row.status) }).then(() => { this.$modal.msgSuccess(text + "成功") }).catch(function () { row.status = row.status === "0" ? "1" : "0" }) }, // 取消按钮 cancel () { this.open = false this.reset() }, // 表单重置 reset () { this.form = { userId: undefined, tableName: undefined, userName: undefined, nickName: undefined, password: undefined, phonenumber: undefined, email: undefined, sex: undefined, status: "0", remark: undefined, postIds: [], roleIds: [] } this.resetForm(this.form) }, /** 搜索按钮操作 */ handleQuery () { this.queryParams.pageNum = 1 this.getList() }, /** 重置按钮操作 */ resetQuery () { this.dateRange = [] this.queryConditions = [ { field: this.columns[0].prop, type: 'like', value: '' } ] // this.queryParams.tableName = undefined // this.$refs.tree.setCurrentKey(null) this.handleQuery() }, // 多选框选中数据 handleSelectionChange (selection) { this.ids = selection.map(item => item.userId) this.single = selection.length != 1 this.multiple = !selection.length }, // 更多操作触发 handleCommand (command, row) { switch (command) { case "update": this.topicForm.parentId = row.parentId this.topicForm.tableName = row.tableName this.topicForm.name = row.name this.topicForm.id = row.id this.showAddTopicDialog = true break case "delete": this.$modal.confirm('是否确认删除?').then(function () { return deleteTopic({ ids: [row.id] }) }).then(() => { this.$modal.msgSuccess("删除成功") this.getTopicTree() }).catch(() => { }) break default: break } }, /** 修改按钮操作 */ handleUpdate (row) { this.reset() const userId = row.userId || this.ids getUser(userId).then(response => { this.form = response.data this.postOptions = response.posts this.roleOptions = response.roles this.$set(this.form, "postIds", response.postIds) this.$set(this.form, "roleIds", response.roleIds) this.open = true this.title = "修改用户" this.form.password = "" }) }, /** 重置密码按钮操作 */ handleResetPwd (row) { this.$prompt('请输入"' + row.userName + '"的新密码', "提示", { confirmButtonText: "确定", cancelButtonText: "取消", closeOnClickModal: false, inputPattern: /^.{5,20}$/, inputErrorMessage: "用户密码长度必须介于 5 和 20 之间", inputValidator: (value) => { if (/<|>|"|'|\||\\/.test(value)) { return "不能包含非法字符:< > \" ' \\\ |" } }, }).then(({ value }) => { resetUserPwd(row.userId, value).then(response => { this.$modal.msgSuccess("修改成功,新密码是:" + value) }) }).catch(() => { }) }, /** 分配角色操作 */ handleAuthRole: function (row) { const userId = row.userId this.$router.push("/system/user-auth/role/" + userId) }, /** 提交按钮 */ submitForm: function () { this.$refs["form"].validate(valid => { if (valid) { if (this.form.userId != undefined) { updateUser(this.form).then(response => { this.$modal.msgSuccess("修改成功") this.open = false this.getList() }) } else { addUser(this.form).then(response => { this.$modal.msgSuccess("新增成功") this.open = false this.getList() }) } } }) }, /** 删除按钮操作 */ handleDelete (row) { const userIds = row.userId || this.ids this.$modal.confirm('是否确认删除?').then(function () { return delUser(userIds) }).then(() => { this.getList() this.$modal.msgSuccess("删除成功") }).catch(() => { }) }, /** 导出按钮操作 */ handleExport () { this.download('system/user/export', { ...this.queryParams }, `user_${new Date().getTime()}.xlsx`) }, /** 导入按钮操作 */ handleImport () { this.upload.title = "用户导入" this.upload.open = true }, /** 下载模板操作 */ importTemplate () { this.download('system/user/importTemplate', { }, `user_template_${new Date().getTime()}.xlsx`) }, // 文件上传中处理 handleFileUploadProgress (event, file, fileList) { this.upload.isUploading = true }, // 文件上传成功处理 handleFileSuccess (response, file, fileList) { this.upload.open = false this.upload.isUploading = false this.$refs.upload.clearFiles() this.$alert("<div style='overflow: auto;overflow-x: hidden;max-height: 70vh;padding: 10px 20px 0;'>" + response.msg + "</div>", "导入结果", { dangerouslyUseHTMLString: true }) this.getList() }, // 提交上传文件 submitFileForm () { this.$refs.upload.submit() }, // 添加主题0608 getTreeIcon (level) { switch (level) { case 1: return 'assets-icon-folder w-5 h-5 inline-block' default: return ' w-1 h-1 inline-block' } }, addData (data) { this.topicForm.parentId = data.id this.showAddTopicDialog = true }, handleShowTopicDialogLevel1 () { this.topicForm.parentId = 0 this.topicForm.tableName = '' this.topicForm.name = '' this.showAddTopicDialog = true }, handleShowTopicDialogLevel2 (data) { this.topicForm.parentId = data.id this.topicForm.tableName = '' this.topicForm.id = 0 this.topicForm.name = '' this.showAddTopicDialog = true }, addTopic () { const params = { ...this.topicForm } if (this.topicForm.id === 0) { delete params.id } addTopic(params).then(response => { }).then(response => { this.showAddTopicDialog = false this.$message.success(this.topicForm.id === 0 ? '添加成功' : '修改成功') this.getTopicTree() }) }, getTableNames () { getTableNames().then(response => { this.tableOptions = response.data.map(item => { return { label: item.COMMENTS, value: item.TABLENAME } }) }) } } } </script> <style lang="scss" scoped> .app-container { min-height: calc(100vh - 84px); } </style> 在 <div class=" mb-2"> <el-button type="primary" icon="el-icon-search" @click="handleQuery">搜索</el-button> <el-button icon="el-icon-refresh" @click="resetQuery">重置</el-button> </div>中加一个按钮,为更新,点击后调用8个接口,进度条随着返回变化
10-11
<template> <div class="container"> <TablePane ref="tablePane" :buttons="this.operations" :search-fields="searchFields" :query-params.sync="this.queryParams" :get-list="getListFun" :table-data="tableData" v-if="showGantt" > </TablePane> <div v-if="!showGantt"> <div class="search-wrapper"> <el-form class="search-from" :inline="true" label-width="70px" @submit.prevent="initGantt"> <el-form-item label="名称"> <el-input v-model="queryParams.name" placeholder="请输入名称" clearable/> </el-form-item> <el-form-item label="责任人"> <el-input v-model="queryParams.respPerson" placeholder="请输入责任人" clearable/> </el-form-item> <el-form-item> <el-button type="primary" @click="initGantt"> <i class="el-icon-search"></i> 搜索 </el-button> <el-button @click="resetQuery"> <i class="el-icon-refresh"></i> 重置 </el-button> <el-button type="primary" style="margin-left: 10px;" @click="retractGantt" > {{ '收起甘特图' }} </el-button> </el-form-item> </el-form> </div> <div ref="gantt" class="gantt-container"></div> </div> <!-- 添加或修改治理计划对话框 --> <Dialog title="查看治理计划" :visible.sync="open" width="850px" height="600px" append-to-body> <el-form ref="form" :model="form" label-width="100px" disabled> <el-row> <el-col :span="12"> <el-form-item label="编号" prop="code"> <el-input v-model="form.code" placeholder="请输入编号"/> </el-form-item> </el-col> <el-col :span="12"> <el-form-item label="名称" prop="text"> <el-input v-model="form.text" placeholder="请输入名称"/> </el-form-item> </el-col> </el-row> <el-row> <el-col :span="12"> <el-form-item label="上级计划" prop="parent"> <treeselect v-model="form.parent" :options="planOptions" :normalizer="normalizer" placeholder="选择上级计划" disabled /> </el-form-item> </el-col> <el-col :span="12"> <el-form-item label="完成度(%)" prop="progress"> <el-input-number v-model="form.progress" placeholder="请输入完成百分比"/> </el-form-item> </el-col> </el-row> <el-row> <el-col :span="12"> <el-form-item label="责任人" prop="respPerson"> <span slot="label"> <span class="content-font">责任人</span> </span> <!-- 用户向导 --> <p style="margin-top:0px; float: left;font-size: 12px;"> {{ form.respPerson }} <el-button size="mini" type="primary" icon="el-icon-user" class="btn-wizard-trigger" style="margin-left: 10px" >选择用户 </el-button> </p> </el-form-item> </el-col> <el-col :span="12"> <el-form-item label="责任部门" prop="respDept"> <el-input v-model="form.respDept" placeholder="请输入责任部门"/> </el-form-item> </el-col> </el-row> <el-row> <el-col :span="12"> <el-form-item label="计划开始日期" prop="planStartDate"> <el-date-picker clearable v-model="form.planStartDate" type="date" value-format="yyyy-MM-dd" placeholder="请选择计划开始日期"> </el-date-picker> </el-form-item> </el-col> <el-col :span="12"> <el-form-item label="实际开始日期" prop="realStartDate"> <el-date-picker clearable v-model="form.realStartDate" type="date" value-format="yyyy-MM-dd" placeholder="请选择实际开始日期"> </el-date-picker> </el-form-item> </el-col> </el-row> <el-row> <el-col :span="12"> <el-form-item label="计划结束时间" prop="planEndDate"> <el-date-picker clearable v-model="form.planEndDate" type="date" value-format="yyyy-MM-dd" placeholder="请选择计划结束时间"> </el-date-picker> </el-form-item> </el-col> <el-col :span="12"> <el-form-item label="实际结束日期" prop="realEndDate"> <el-date-picker clearable v-model="form.realEndDate" type="date" value-format="yyyy-MM-dd" placeholder="请选择实际结束日期"> </el-date-picker> </el-form-item> </el-col> </el-row> <el-row> <el-col :span="12"> <el-form-item label="计划工期(天)" prop="planDuration"> <el-input v-model="form.planDuration" placeholder="请选择计划日期"/> </el-form-item> </el-col> <el-col :span="12"> <el-form-item label="实际工期(天)" prop="realDuration"> <el-input v-model="form.realDuration" placeholder="请选择实际日期"/> </el-form-item> </el-col> </el-row> <el-row> <el-col :span="24"> <el-form-item label="备注" prop="remarks"> <el-input v-model="form.remarks" type="textarea" placeholder="请输入备注"/> </el-form-item> </el-col> </el-row> <el-row> <el-col :span="24"> <el-form-item label="反馈内容" prop="feedback"> <el-input v-model="form.feedback" type="textarea" placeholder="请输入内容"/> </el-form-item> </el-col> </el-row> </el-form> <div slot="buttons" class="dialog-footer"> <el-button @click="open = false">关 闭</el-button> </div> </Dialog> </div> </template> <script> import TablePane from '@/components/TablePane' import Dialog from '@/components/Dialog' import Treeselect from '@riophae/vue-treeselect' import '@riophae/vue-treeselect/dist/vue-treeselect.css' import {gantt} from "dhtmlx-gantt"; import "dhtmlx-gantt/codebase/dhtmlxgantt.css"; import {listPlan} from "@/api/dw/plan/planview"; export default { components: {TablePane, Dialog, Treeselect}, name: "gantt", data() { return { getListFun: listPlan, searchFields: [ {'field': 'name', 'label': '名称', 'type': 'input'}, {'field': 'respPerson', 'label': '责任人', 'type': 'input'}, ], operations: [ { 'label': '展开甘特图', 'click': this.expandGantt, 'hasPermi': ['dw:planview:list'] }], tableData: { 'idField': 'uid', //加载数据后进行转换用于特殊处理 'loadDataHook': this.loadDataHook, 'attrs': { 'row-key': 'uid', '@row-click': this.rowClick, 'tree-props': { children: 'children', hasChildren: 'hasChildren' }, 'default-expand-all': true }, 'column': [ {'label': '编号', 'prop': 'code', 'attrs': { 'sortable': false,'align': 'left' } }, {'label': '状态', 'prop': 'status1', 'attrs': { 'sortable': false } ,'width': 80,'columnHook': this.light}, {'label': '名称', 'prop': 'name', 'attrs': { 'sortable': false } }, {'label': '完成百分比', 'prop': 'schedule', 'attrs': { 'sortable': false } }, {'label': '责任人', 'prop': 'respPerson', 'attrs': { 'sortable': false } }, {'label': '责任部门', 'prop': 'respDept' , 'attrs': { 'sortable': false }}, {'label': '计划开始日期', 'prop': 'planStartDate' ,'type':"dateTime" , 'format': (date) => { return this.parseTime(date, '{y}-{m}-{d}')}}, {'label': '计划结束时间', 'prop': 'planEndDate' ,'type':"dateTime" , 'format': (date) => { return this.parseTime(date, '{y}-{m}-{d}')}}, {'label': '实际开始日期', 'prop': 'realStartDate' ,'type':"dateTime" , 'format': (date) => { return this.parseTime(date, '{y}-{m}-{d}')}}, {'label': '实际结束日期', 'prop': 'realEndDate' ,'type':"dateTime" , 'format': (date) => { return this.parseTime(date, '{y}-{m}-{d}')}}, {'label': '计划工期', 'prop': 'planDuration' }, {'label': '实际工期', 'prop': 'realDuration' }, {'label': '备注', 'prop': 'remarks' }, { 'width': 100, 'label': '操作', 'type': 'button', 'attrs': { 'sortable': false, 'fixed': 'right' }, 'buttons': [ { 'icon': 'el-icon-view', 'label': '查看', 'hasPermi': ['dw:planview:query'], 'click': this.handleView }, ] } ] }, /***************************************************甘特图 start****************************************************************/ tasks: { data: [], }, queryParams: { name: null, respPerson: null }, showGantt: true, // 状态控制甘特图显示 planOptions: [], open: false, // 控制详情弹窗显示 form: {} // 当前查看的任务 /***************************************************甘特图 end****************************************************************/ }; }, // 把携带的参数放到queryParams查询参数里 created() { // console.log(this.$route.params.id); }, methods: { /** 查询治理计划列表 */ getList() { this.$refs.tablePane.loadData() }, loadDataHook(response) { response.data = this.handleTree(response.data, 'uid','parentUid') return response }, light(column, row){ const currentDate = this.getCurrentDate(); if( row.realStartDate != null){//有实际开始时间 // 状态灯:绿色:已完成; // 红色:计划开始时间小于当前日期并且没有实际开始日期; // 黄色:有实际开始日期,但是没有实际结束日期,并且计划结束日期小宇当前日期。 // 其他状态都没有颜色(数据体系里面的计划状态灯昨天没写,也按这个规则) var res = this.compareDate(new Date(row.planEndDate),new Date(currentDate)) if( row.realEndDate == null){ if(res== -1){ column.class='circle-light-yellow' } }else{ column.class='circle-light-green' } }else{//没用实际开始时间 红色:没有实际开始,但是当前日期大于计划开始日期; if(row.planStartDate != null){//有计划开始时间 var res = this.compareDate(new Date(row.planStartDate),new Date(currentDate)) if(res == -1){ column.class='circle-light-red' } } } }, getCurrentDate(){ const dateObj = new Date(); const year = dateObj.getFullYear(); // 获取当前年份 const month = ("0" + (dateObj.getMonth() + 1)).slice(-2); // 获取当前月份,其中需要将月份加1,因为月份是从0开始计数的 const day = ("0" + dateObj.getDate()).slice(-2); // 获取当前日期 const formattedDate = `${year}-${month}-${day}`; // 格式化日期 return formattedDate; // 输出当前时间的年月日 }, compareDate(date1,date2){//date1 > date2 返回1;date1 < date2 返回-1 相等返回0 if (date1.getTime() > date2.getTime()) { return 1; } else if (date1.getTime() < date2.getTime()) { return -1; } else { return 0; } }, // 上级节点 getTreeselect() { listPlan().then(response => { const data = {uid: 0, name: '顶级节点', children: []}; data.children = this.handleTree(response.data, 'uid', 'parentUid') this.planOptions.push(data) }) }, normalizer(node) { if (node.children && !node.children.length) { delete node.children } return { id: node.uid, label: node.name, children: node.children } }, rowClick(row, column, event) { // 行点击事件 }, /***************************************************甘特图 start****************************************************************/ // 查看任务详情 handleView(taskId) { // 根据任务ID查找任务详情 const task = this.tasks.data.find(item => item.id == taskId); if (task) { this.getTreeselect(); this.form = task; this.open = true; } }, expandGantt() { this.showGantt = false this.$nextTick(() => { this.initGantt(); // 确保DOM更新后再初始化甘特图 }); }, retractGantt() { this.showGantt = true; this.destroyGantt(); // 添加销毁甘特图方法 }, //开始时间-结束时间参数 DateDifference: function (strDateStart, strDateEnd) { var begintime_ms = Date.parse(new Date(strDateStart.replace(/-/g, "/"))); //begintime 为开始时间 var endtime_ms = Date.parse(new Date(strDateEnd.replace(/-/g, "/"))); // endtime 为结束时间 var date3 = endtime_ms - begintime_ms; //时间差的毫秒数 var days = Math.floor(date3 / (24 * 3600 * 1000)); return days; }, // 重置查询 resetQuery() { this.queryParams = { name: null, respPerson: null }; this.initGantt(); }, initGantt: function () { this.destroyGantt(); // 先销毁旧实例 // 确保容器存在 if (!this.$refs.gantt) return; // 设置容器尺寸 this.$refs.gantt.style.height = `${window.innerHeight - 150}px`; // 在初始化前配置所有设置 //自适应甘特图的尺寸大小, 使得在不出现滚动条的情况下, 显示全部任务 gantt.config.autosize = true; //只读模式 gantt.config.readonly = true; //是否显示左侧树表格 gantt.config.show_grid = true; //时间轴配置 gantt.config.show_task_cells = true; //当task的长度改变时,自动调整图表坐标轴区间用于适配task的长度 gantt.config.fit_tasks = true; gantt.config.min_column_width = 50; gantt.config.auto_types = true; gantt.config.xml_date = "%Y-%m-%d"; gantt.config.scale_unit = "month"; gantt.config.step = 1; gantt.config.date_scale = "%Y年%M"; gantt.config.start_on_monday = true; gantt.config.scale_height = 160; gantt.config.autoscroll = true; gantt.config.calendar_property = "start_date"; gantt.config.calendar_property = "end_date"; gantt.config.readonly = true; //时间刻度配置 var weekScaleTemplate = function (date) { var dateToStr = gantt.date.date_to_str("%m %d"); var endDate = gantt.date.add( gantt.date.add(date, 1, "week"), -1, "day" ); var weekNum = gantt.date.date_to_str("第 %W 周"); return weekNum(date); }; gantt.config.subscales = [ { unit: "week", step: 1, template: weekScaleTemplate, }, { unit: "day", step: 1, format: "%d", }, ]; //表格列设置 gantt.config.columns = [ { name: "code", label: "编号", tree: true, width: "160", onrender: function (task, node) { node.setAttribute( "class", "gantt_cell gantt_last_cell gantt_cell_tree " + task.status ); }, }, { name: "status", label: "状态", align: "center", width: "80", template: function (task) { // 自定义状态列显示为状态灯 return `<div class="status-light" style="background-color: ${task.color}"></div>`; } }, {name: "text", label: "名称", align: "center", width: "180", hide: true}, {name: "progress", label: "完成度(%)", align: "center", width: "90", hide: true}, {name: "respPerson", label: "责任人", align: "center", width: "120", hide: true}, {name: "respDept", label: "责任部门", align: "center", width: "140", hide: true}, {name: "planStartDate", label: "计划开始日期", align: "center", width: "130", hide: true}, {name: "planEndDate", label: "计划结束时间", align: "center", width: "130", hide: true}, {name: "realStartDate", label: "实际开始日期", align: "center", width: "130", hide: true}, {name: "realEndDate", label: "实际结束日期", align: "center", width: "130", hide: true}, {name: "planDuration", label: "计划工期", align: "center", width: "90", hide: true}, {name: "realDuration", label: "实际工期", align: "center", width: "90", hide: true}, {name: "remarks", label: "备注", align: "center", width: "220", hide: true}, ]; // 初始化 gantt.init(this.$refs.gantt); gantt.plugins({ tooltip: true, }); //设置鼠标放置显示事件 gantt.attachEvent("onGanttReady", function() { var tooltips = gantt.ext.tooltips; gantt.templates.tooltip_text = function(start, end, task) { return "编号:" + task.code + "<br/>" + "名称:" + task.text + "<br/>" + "计划开始:" + gantt.templates.tooltip_date_format(start) + "<br/>" + "工期:" + task.duration }; }); //设置任务条进度内容 gantt.templates.progress_text = function (start, end, task) { return ( "<div style='text-align:left;color:#fff;padding-left:20px'>" + task.progress + "% </div>" ); }; //任务条显示内容 gantt.templates.task_text = function (start, end, task) { return ( "<div style='text-align:center;color:#fff'>" + task.text + "(" + task.duration + "天)" + "</div>" ); }; //任务条上的文字大小 以及取消border自带样式 gantt.templates.task_class = function (start, end, item) { return item.$level == 0 ? "firstLevelTask" : "secondLevelTask"; }; // 设置布局 gantt.config.layout = { css: "gantt_container", cols: [ { width: "800", // 左侧表格固定宽度 min_width: 300, rows: [ { view: "grid", scrollX: "gridScroll", scrollable: true, scrollY: "scrollVer", }, { view: "scrollbar", id: "gridScroll", group: "horizontal", }, ], }, { resizer: true, width: 1, }, { rows: [ { view: "timeline", scrollX: "scrollHor", scrollY: "scrollVer", }, { view: "scrollbar", id: "scrollHor", group: "horizontal", }, ] } ], }; gantt.i18n.setLocale("cn"); listPlan(this.queryParams).then((res) => { this.tasks.data = res.data.map((item) => { let statusColor; //存在status字段 说明非一级菜单,判断阶段的具体类型 设置不同颜色 if (item.status == '1') { //冒烟 statusColor = "#84bd54" } else if (item.status == '2') { //单元 statusColor = "#fcca02" } else if (item.status == '3') { //回归 statusColor = "#dc1626" } else { statusColor = "#999999" } return { id: item.uid, parent: item.parent, text: item.name, start_date: item.planStartDate, duration: item.planDuration, open: true, //默认打开, toolTipsTxt: item.name, progress: item.schedule, status: item.status, code: item.code, respPerson: item.respPerson, respDept: item.respDept, planStartDate: item.planStartDate, planEndDate: item.planEndDate, realStartDate: item.realStartDate, realEndDate: item.realEndDate, planDuration: item.planDuration, realDuration: item.realDuration, remarks: item.remarks, feedback: item.feedback, color: statusColor, } }); // 数据解析 gantt.parse(this.tasks); }); // 添加双击行事件监听器 gantt.attachEvent("onTaskDblClick", function(id, e) { // 调用查看详情方法 window.vueInstance.handleView(id); return true; }); }, destroyGantt() { // 推荐方式:检查容器是否有内容 if (this.$refs.gantt && this.$refs.gantt.children.length > 0) { gantt.destroy(); // 安全销毁 } this.$refs.gantt.innerHTML = ""; // 清空容器 }, }, mounted() { window.vueInstance = this; if (!this.showGantt) { this.$nextTick(this.initGantt); } else { this.getList(); } }, }; </script> <style lang="scss" scoped> .firstLevelTask { border: none; .gantt_task_content { font-size: 13px; } } .secondLevelTask { border: none; } .thirdLevelTask { border: 2px solid #da645d; color: #da645d; background: #da645d; } .milestone-default { border: none; background: rgba(0, 0, 0, 0.45); } .milestone-unfinished { border: none; background: #5692f0; } .milestone-finished { border: none; background: #84bd54; } .milestone-canceled { border: none; background: #da645d; } html, body { margin: 0; padding: 0; height: 100%; overflow: hidden; } .container { height: 100%; width: 100%; position: relative; padding: 10px; .gantt_grid_head_cell { padding-left: 20px; text-align: left !important; font-size: 14px; color: #333; } .left-container { height: 100%; } .green, .yellow, .pink, .popular { .gantt_tree_icon.gantt_file { background: none; position: relative; &::before { content: ""; width: 10px; height: 10px; border-radius: 50%; position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%); } } } .green { .gantt_tree_icon.gantt_file { &::before { background: #84bd54; } } } .yellow { .gantt_tree_icon.gantt_file { &::before { background: #fcca02; } } } .pink { .gantt_tree_icon.gantt_file { &::before { background: #da645d; } } } .popular { .gantt_tree_icon.gantt_file { &::before { background: #d1a6ff; } } } } .left-container { height: 100%; } .gantt_task_content { text-align: left; padding-left: 10px; } .gantt-container { height: calc(100vh - 150px) !important; width: 100%; } // 状态灯样式 ::v-deep .gantt_grid_data .gantt_cell div.status-light { width: 12px; height: 12px; border-radius: 50%; display: inline-block; margin: 0 auto; } // 表格表头居中样式 ::v-deep .gantt_grid_head_cell { text-align: center !important; } .search-wrapper { text-align: left; padding: 10px 0; } .search-from { .el-form-item--mini.el-form-item, .el-form-item--small.el-form-item { margin-bottom: 3px; } } </style> <style lang="less"> .circle-light-yellow > span::before { content: ''; display: inline-block; width: 16px; height: 16px; background-color: #e6a23c; border-radius: 50%; margin-right: 10px; } .circle-light-green > span::before { content: ""; display: inline-block; width: 16px; height: 16px; background-color: #67c23a; border-radius: 50%; margin-right: 10px; } .circle-light-red > span::before { content: ""; display: inline-block; width: 16px; height: 16px; background-color: #f56c6c; border-radius: 50%; margin-right: 10px; } </style> 第一次展开甘特图时表格铺满了,请修复
08-08
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值