当前位置: 首页 > news >正文

Python编写的yaml编辑器

效果预览

图片

图片

代码

代码采用AI辅助编写

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
树形结构 YAML 编辑器 (修复版)
支持可视化编辑 YAML 文件的层级结构
"""import tkinter as tk
from tkinter import ttk, filedialog, messagebox
import yaml
import json
import os
from typing import Any, Dict, List, Optionalclass TreeYAMLEditor:"""基于树形结构的 YAML 编辑器"""def __init__(self, root: tk.Tk):self.root = rootself.root.title("YAML Editor")self.is_modified: bool = Falseself.yaml_data: Any = {}          # ✅ 修复:初始化数据容器self.current_file: Optional[str] = None  # ✅ 修复:初始化文件路径self.tree_items: Dict[str, tuple] = {}  # item_id -> (key, value, path)self._create_toolbar()self._create_main_area()self._create_statusbar()self.root.protocol("WM_DELETE_WINDOW", self._on_close)def _create_toolbar(self):toolbar = ttk.Frame(self.root)toolbar.pack(side=tk.TOP, fill=tk.X, padx=5, pady=5)buttons_info = [("新建", self.new_file), ("打开", self.open_file), ("保存", self.save_file),("separator", None), ("刷新", self.reload_file), ("separator", None),("添加", self.add_node), ("编辑值", self.edit_value), ("删除", self.delete_node),("separator", None), ("JSON", self.show_json_preview), ("校验", self.validate_yaml),]for text, cmd in buttons_info:if text == "separator":ttk.Separator(toolbar, orient=tk.VERTICAL).pack(side=tk.LEFT, padx=8)else:btn = ttk.Button(toolbar, text=text, command=cmd)btn.pack(side=tk.LEFT, padx=2)def _create_main_area(self):paned = ttk.PanedWindow(self.root, orient=tk.HORIZONTAL)paned.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)left_frame = ttk.LabelFrame(paned, text="YAML 结构树", padding=5)paned.add(left_frame, weight=2)tree_toolbar = ttk.Frame(left_frame)tree_toolbar.pack(fill=tk.X, pady=(0, 5))ttk.Button(tree_toolbar, text="展开", command=self.expand_all).pack(side=tk.LEFT, padx=2)ttk.Button(tree_toolbar, text="折叠", command=self.collapse_all).pack(side=tk.LEFT, padx=2)self.search_var = tk.StringVar()ttk.Entry(tree_toolbar, textvariable=self.search_var, width=12).pack(side=tk.RIGHT, padx=5)ttk.Button(tree_toolbar, text="搜索", command=self.search_node).pack(side=tk.RIGHT, padx=2)tree_frame = ttk.Frame(left_frame)tree_frame.pack(fill=tk.BOTH, expand=True)vsb = ttk.Scrollbar(tree_frame, orient=tk.VERTICAL)hsb = ttk.Scrollbar(tree_frame, orient=tk.HORIZONTAL)columns = ("value", "type")self.tree = ttk.Treeview(tree_frame, columns=columns, yscrollcommand=vsb.set, xscrollcommand=hsb.set, selectmode="extended", show=["tree"])self.tree.column("#0", width=300, minwidth=200)self.tree.column("value", width=200, minwidth=100)self.tree.column("type", width=100, minwidth=80)self.tree.heading("#0", text="键")self.tree.heading("value", text="值")self.tree.heading("type", text="类型")vsb.config(command=self.tree.yview)hsb.config(command=self.tree.xview)self.tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)vsb.pack(side=tk.RIGHT, fill=tk.Y)hsb.pack(side=tk.BOTTOM, fill=tk.X)self.tree.bind("<Double-Button-1>", lambda e: self.edit_value())self.tree.bind("<Return>", lambda e: self.edit_value())self.tree.bind("<F2>", lambda e: self.edit_value())self.tree.bind("<Delete>", lambda e: self.delete_node())right_frame = ttk.LabelFrame(paned, text="节点编辑", padding=10)paned.add(right_frame, weight=1)ttk.Label(right_frame, text="当前路径:", font=("Arial", 10, "bold")).pack(anchor=tk.W)self.path_label = ttk.Label(right_frame, text="/", background="#e8f4e8", relief=tk.SUNKEN, font=("Consolas", 10), anchor=tk.W, padding=5)self.path_label.pack(fill=tk.X, pady=(0, 10))ttk.Label(right_frame, text="节点信息:", font=("Arial", 10, "bold")).pack(anchor=tk.W)info_frame = ttk.Frame(right_frame)info_frame.pack(fill=tk.X, pady=(0, 10))ttk.Label(info_frame, text="键名:").grid(row=0, column=0, sticky=tk.W, padx=5)self.key_var = tk.StringVar()ttk.Entry(info_frame, textvariable=self.key_var, width=20).grid(row=0, column=1, sticky=tk.EW, padx=5)ttk.Label(info_frame, text="类型:").grid(row=1, column=0, sticky=tk.W, padx=5, pady=5)self.type_label = ttk.Label(info_frame, text="无", background="#f0f0f0", padding=3)self.type_label.grid(row=1, column=1, sticky=tk.W, padx=5, pady=5)info_frame.columnconfigure(1, weight=1)ttk.Label(right_frame, text="值:", font=("Arial", 10, "bold")).pack(anchor=tk.W)type_frame = ttk.Frame(right_frame)type_frame.pack(fill=tk.X, pady=(0, 5))self.value_type_var = tk.StringVar(value="str")for val_type, label in [("str", "文本"), ("int", "整数"), ("float", "小数"), ("bool", "布尔"), ("null", "空值")]:ttk.Radiobutton(type_frame, text=label, variable=self.value_type_var, value=val_type).pack(side=tk.LEFT, padx=2)self.value_text = tk.Text(right_frame, font=("Consolas", 11), wrap=tk.WORD, height=8,background="#ffffff", foreground="#333333", insertbackground="#0066cc", relief=tk.GROOVE, borderwidth=1)self.value_text.pack(fill=tk.BOTH, expand=True, pady=(0, 5))btn_frame = ttk.Frame(right_frame)btn_frame.pack(fill=tk.X, pady=(10, 0))ttk.Button(btn_frame, text="应用", command=self.apply_changes).pack(side=tk.LEFT, padx=2)ttk.Button(btn_frame, text="重置", command=self.reset_value).pack(side=tk.LEFT, padx=2)ttk.Button(btn_frame, text="添加", command=self.add_node).pack(side=tk.RIGHT, padx=2)preview_frame = ttk.LabelFrame(right_frame, text="JSON 预览", padding=5)preview_frame.pack(fill=tk.BOTH, expand=True, pady=(15, 0))self.json_text = tk.Text(preview_frame, font=("Consolas", 9), wrap=tk.WORD, height=6, background="#f8f8f8", state=tk.DISABLED)self.json_text.pack(fill=tk.BOTH, expand=True)self.tree.bind("<<TreeviewSelect>>", self._on_tree_select)def _create_statusbar(self):status_frame = ttk.Frame(self.root)status_frame.pack(side=tk.BOTTOM, fill=tk.X)self.status_label = ttk.Label(status_frame, text="就绪", relief=tk.SUNKEN, anchor=tk.W)self.status_label.pack(side=tk.LEFT, fill=tk.X, expand=True)self.info_label = ttk.Label(status_frame, text="", relief=tk.SUNKEN, anchor=tk.E)self.info_label.pack(side=tk.RIGHT)def load_yaml_to_tree(self, data):self._clear_tree()if data is None:self.yaml_data = {}returnself.yaml_data = dataif isinstance(data, (dict, list)):self._build_tree(data, parent="")else:self.tree.insert("", tk.END, text="  root", values=[str(data), self._get_type_name(data)])self.tree_items[""] = ("root", data, "")def _clear_tree(self):for item in self.tree.get_children():self.tree.delete(item)self.tree_items.clear()# ✅ 修复:移除错误的 key 参数,统一使用 current_path 追踪完整路径def _build_tree(self, data, parent="", current_path=""):if isinstance(data, dict):for k, v in data.items():new_path = f"{current_path}/{k}" if current_path else kif isinstance(v, dict):display_value, node_type = "{...}", "dict"elif isinstance(v, list):display_value, node_type = f"[{len(v)}]", "list"else:display_value = str(v) if v is not None else "null"node_type = self._get_type_name(v)item_id = self.tree.insert(parent, tk.END, text=f"  {k}", values=[display_value, node_type])self.tree_items[item_id] = (k, v, new_path)if isinstance(v, (dict, list)):self._build_tree(v, item_id, new_path)elif isinstance(data, list):for i, v in enumerate(data):new_path = f"{current_path}[{i}]" if current_path else f"[{i}]"if isinstance(v, dict):display_value, node_type = "{...}", "dict"elif isinstance(v, list):display_value, node_type = f"[{len(v)}]", "list"else:display_value = str(v) if v is not None else "null"node_type = self._get_type_name(v)item_id = self.tree.insert(parent, tk.END, text=f"  [{i}]", values=[display_value, node_type])self.tree_items[item_id] = (f"[{i}]", v, new_path)if isinstance(v, (dict, list)):self._build_tree(v, item_id, new_path)def _get_type_name(self, value: Any) -> str:if value is None: return "null"if isinstance(value, bool): return "bool"if isinstance(value, int): return "int"if isinstance(value, float): return "float"if isinstance(value, str): return "str"return type(value).__name__def _on_tree_select(self, event=None):selection = self.tree.selection()if not selection: returnitem = selection[0]if item not in self.tree_items: returnkey, value, path = self.tree_items[item]self.path_label.config(text=f"/{path}" if path else "/")self.key_var.set(str(key))type_name = self._get_type_name(value)self.type_label.config(text=type_name)# ✅ 修复:同步类型单选框状态(仅标量类型有效)if type_name in ("str", "int", "float", "bool", "null"):self.value_type_var.set(type_name)if isinstance(value, (dict, list)):self.value_text.config(state=tk.NORMAL)self.value_text.delete("1.0", tk.END)self.value_text.insert("1.0", json.dumps(value, ensure_ascii=False, indent=2))self.value_text.config(state=tk.DISABLED)else:self.value_text.config(state=tk.NORMAL)self.value_text.delete("1.0", tk.END)self.value_text.insert("1.0", str(value) if value is not None else "")self._update_json_preview()def edit_value(self):selection = self.tree.selection()if not selection:messagebox.showwarning("提示", "请先选择要编辑的节点")returnitem = selection[0]if item in self.tree_items:_, value, _ = self.tree_items[item]if isinstance(value, (dict, list)):self._edit_complex_value(item)else:self.value_text.config(state=tk.NORMAL)self.value_text.focus()def _edit_complex_value(self, item: str):key, value, path = self.tree_items[item]dialog = tk.Toplevel(self.root)dialog.title(f"编辑: {key}")dialog.geometry("500x400")dialog.transient(self.root)dialog.grab_set()ttk.Label(dialog, text=f"路径: /{path}", font=("Arial", 10)).pack(pady=5)text = tk.Text(dialog, font=("Consolas", 11), wrap=tk.WORD)text.pack(fill=tk.BOTH, expand=True, padx=10, pady=5)text.insert("1.0", json.dumps(value, ensure_ascii=False, indent=2))def save():try:raw = text.get("1.0", tk.END).strip()# ✅ 修复:安全解析 JSON,空内容时根据原类型提供默认值new_value = json.loads(raw) if raw else ({} if isinstance(value, dict) else [])self._update_yaml_data(path, new_value)self.load_yaml_to_tree(self.yaml_data)self._set_modified()dialog.destroy()except json.JSONDecodeError as e:messagebox.showerror("错误", f"JSON 格式错误:\n{e}")btn_frame = ttk.Frame(dialog)btn_frame.pack(pady=10)ttk.Button(btn_frame, text="保存", command=save).pack(side=tk.LEFT, padx=5)ttk.Button(btn_frame, text="取消", command=dialog.destroy).pack(side=tk.LEFT, padx=5)def apply_changes(self):selection = self.tree.selection()if not selection:messagebox.showwarning("提示", "请先选择节点")return Falseitem = selection[0]if item not in self.tree_items: return Falsekey, old_value, path = self.tree_items[item]if isinstance(old_value, (dict, list)):self._edit_complex_value(item)return Truevalue_type = self.value_type_var.get()text_content = self.value_text.get("1.0", tk.END).strip()try:if value_type == "null": new_value = Noneelif value_type == "str": new_value = text_contentelif value_type == "int": new_value = int(text_content)elif value_type == "float": new_value = float(text_content)elif value_type == "bool": new_value = text_content.lower() in ("true", "1", "yes")else: new_value = text_contentself._update_yaml_data(path, new_value)self.load_yaml_to_tree(self.yaml_data)self._set_modified()self.status_label.config(text=f"已更新: {key} = {new_value}")return Trueexcept ValueError as e:messagebox.showerror("错误", f"值转换失败:\n{e}")return Falsedef reset_value(self):self._on_tree_select()def _update_yaml_data(self, path: str, new_value: Any):parts = self._parse_path(path)if not parts:self.yaml_data = new_value  # ✅ 修复:空路径表示根节点returndata = self.yaml_datafor part in parts[:-1]:try:data = data[part]except (KeyError, IndexError, TypeError) as e:raise RuntimeError(f"路径导航失败: {path} -> {part}") from elast_part = parts[-1]try:data[last_part] = new_valueexcept (KeyError, IndexError, TypeError) as e:raise RuntimeError(f"更新值失败: {path} -> {last_part}") from edef _parse_path(self, path: str) -> List:if not path: return []parts = []current = ""in_bracket = Falsefor char in path:if char == "/" and not in_bracket:if current: parts.append(current)current = ""elif char == "[":if current: parts.append(current)current = ""in_bracket = Trueelif char == "]":if current: parts.append(int(current))current = ""in_bracket = Falseelse:current += charif current: parts.append(current)return partsdef add_node(self):selection = self.tree.selection()if selection:parent_item = selection[0]if parent_item in self.tree_items:_, parent_value, parent_path = self.tree_items[parent_item]if not isinstance(parent_value, (dict, list)):messagebox.showwarning("提示", "只能在字典或列表下添加")returnelse:parent_item = ""parent_path = ""dialog = tk.Toplevel(self.root)dialog.title("添加节点")dialog.geometry("350x220")dialog.transient(self.root)dialog.grab_set()ttk.Label(dialog, text="添加新节点", font=("Arial", 12, "bold")).pack(pady=10)input_frame = ttk.Frame(dialog)input_frame.pack(pady=10, padx=20, fill=tk.X)ttk.Label(input_frame, text="键名:").grid(row=0, column=0, sticky=tk.W, pady=5)key_var = tk.StringVar()ttk.Entry(input_frame, textvariable=key_var, width=25).grid(row=0, column=1, pady=5, padx=5)ttk.Label(input_frame, text="类型:").grid(row=1, column=0, sticky=tk.W, pady=5)type_var = tk.StringVar(value="str")ttk.Combobox(input_frame, textvariable=type_var, values=["str", "int", "float", "bool", "list", "dict"], width=10, state="readonly").grid(row=1, column=1, sticky=tk.W, pady=5, padx=5)ttk.Label(input_frame, text="值:").grid(row=2, column=0, sticky=tk.W, pady=5)value_var = tk.StringVar()ttk.Entry(input_frame, textvariable=value_var, width=25).grid(row=2, column=1, pady=5, padx=5)def do_add():key = key_var.get().strip()value_type = type_var.get()val_text = value_var.get()if not key:messagebox.showwarning("提示", "键名不能为空")returntry:if value_type == "str": value = val_textelif value_type == "int": value = int(val_text) if val_text else 0elif value_type == "float": value = float(val_text) if val_text else 0.0elif value_type == "bool": value = val_text.lower() in ("true", "1", "yes")elif value_type == "list": value = []elif value_type == "dict": value = {}else: value = val_textexcept ValueError:messagebox.showerror("错误", "值格式不正确")returnif parent_item and self.yaml_data:parent_parts = self._parse_path(parent_path)target = self.yaml_datafor p in parent_parts:target = target[p]if isinstance(target, dict):target[key] = valueelif isinstance(target, list):target.append(value)else:if not isinstance(self.yaml_data, dict):self.yaml_data = {}self.yaml_data[key] = valueself.load_yaml_to_tree(self.yaml_data)self._set_modified()dialog.destroy()self.status_label.config(text=f"已添加: {key}")btn_frame = ttk.Frame(dialog)btn_frame.pack(pady=15)ttk.Button(btn_frame, text="添加", command=do_add).pack(side=tk.LEFT, padx=5)ttk.Button(btn_frame, text="取消", command=dialog.destroy).pack(side=tk.LEFT, padx=5)def delete_node(self):selection = self.tree.selection()if not selection:messagebox.showwarning("提示", "请选择要删除的节点")returnitem = selection[0]if item not in self.tree_items: returnkey, _, path = self.tree_items[item]if not messagebox.askyesno("确认", f"确定删除 '{key}' 吗?"): returntry:parts = self._parse_path(path)if not parts:self.yaml_data = {}  # ✅ 修复:安全处理根节点删除else:data = self.yaml_datafor part in parts[:-1]:data = data[part]last_part = parts[-1]del data[last_part]self.load_yaml_to_tree(self.yaml_data)self._set_modified()self.status_label.config(text=f"已删除: {key}")except Exception as e:messagebox.showerror("错误", f"删除失败:\n{e}")def new_file(self):if self.is_modified:if not messagebox.askyesno("确认", "当前文件未保存,是否继续?"): returnself.yaml_data = {}self.current_file = Noneself.is_modified = Falseself.load_yaml_to_tree(self.yaml_data)self._update_title()self._update_json_preview()self.status_label.config(text="已创建新文件")def open_file(self):if self.is_modified:if not messagebox.askyesno("确认", "当前文件未保存,是否继续?"): returnfile_path = filedialog.askopenfilename(title="打开 YAML 文件", filetypes=[("YAML文件", "*.yaml *.yml"), ("所有文件", "*.*")])if file_path:try:with open(file_path, "r", encoding="utf-8") as f:self.yaml_data = yaml.safe_load(f.read()) or {}self.current_file = file_pathself.is_modified = Falseself.load_yaml_to_tree(self.yaml_data)self._update_title()self._update_json_preview()self.status_label.config(text=f"已打开: {file_path}")except Exception as e:messagebox.showerror("错误", f"无法打开文件:\n{e}")def save_file(self):if self.current_file:return self._do_save(self.current_file)return self.save_file_as()def save_file_as(self):file_path = filedialog.asksaveasfilename(title="保存 YAML 文件", defaultextension=".yaml", filetypes=[("YAML文件", "*.yaml"), ("YML文件", "*.yml")])if file_path:return self._do_save(file_path)return Falsedef _do_save(self, file_path: str) -> bool:try:with open(file_path, "w", encoding="utf-8") as f:yaml.dump(self.yaml_data, f, allow_unicode=True, default_flow_style=False, sort_keys=False, indent=2)self.current_file = file_pathself.is_modified = Falseself._update_title()self.status_label.config(text=f"已保存: {file_path}")return Trueexcept Exception as e:messagebox.showerror("错误", f"保存失败:\n{e}")return Falsedef reload_file(self):if self.current_file:try:with open(self.current_file, "r", encoding="utf-8") as f:self.yaml_data = yaml.safe_load(f.read()) or {}self.load_yaml_to_tree(self.yaml_data)self._update_json_preview()self.status_label.config(text="已刷新")except Exception as e:messagebox.showerror("错误", f"刷新失败:\n{e}")else:self.load_yaml_to_tree(self.yaml_data)def expand_all(self):for item in self.tree.get_children(""): self._expand_recursive(item)def _expand_recursive(self, item: str):self.tree.item(item, open=True)for child in self.tree.get_children(item): self._expand_recursive(child)def collapse_all(self):for item in self.tree.get_children(""): self._collapse_recursive(item)def _collapse_recursive(self, item: str):self.tree.item(item, open=False)for child in self.tree.get_children(item): self._collapse_recursive(child)def search_node(self):keyword = self.search_var.get().strip().lower()if not keyword: returnself.collapse_all()found = Falsefor item in self.tree.get_children(""):if self._search_recursive(item, keyword): found = Trueif not found: messagebox.showinfo("搜索", f"未找到: '{keyword}'")def _search_recursive(self, item: str, keyword: str) -> bool:found = Falsetext = self.tree.item(item, "text").strip().lower()values = self.tree.item(item, "values")display = values[0].lower() if values else ""if keyword in text or keyword in display:self.tree.see(item)self.tree.selection_set(item)self.tree.focus(item)self.tree.item(item, open=True)found = Truefor child in self.tree.get_children(item):if self._search_recursive(child, keyword): found = Truereturn founddef validate_yaml(self):try:yaml.dump(self.yaml_data, allow_unicode=True)messagebox.showinfo("校验", "YAML 格式正确!")self.status_label.config(text="校验通过")except Exception as e:messagebox.showerror("校验失败", f"格式错误:\n{e}")def show_json_preview(self): self._update_json_preview()def _update_json_preview(self):self.json_text.config(state=tk.NORMAL)self.json_text.delete("1.0", tk.END)try:if self.yaml_data is not None:self.json_text.insert("1.0", json.dumps(self.yaml_data, ensure_ascii=False, indent=2))else:self.json_text.insert("1.0", "null")except Exception as e:  # ✅ 修复:捕获具体异常self.json_text.insert("1.0", f"无法转换: {e}")self.json_text.config(state=tk.DISABLED)def _set_modified(self):self.is_modified = Trueself._update_title()def _update_title(self):filename = os.path.basename(self.current_file) if self.current_file else "未命名"modified = " *" if self.is_modified else ""self.root.title(f"YAML 树形编辑器 - {filename}{modified}")def _on_close(self):if self.is_modified:if not messagebox.askyesno("确认", "当前文件未保存,是否退出?"): returnself.root.destroy()def main():root = tk.Tk()style = ttk.Style()try:style.theme_use("clam")except tk.TclError:passapp = TreeYAMLEditor(root)root.mainloop()if __name__ == "__main__":main()
http://www.jsqmd.com/news/885765/

相关文章:

  • Godot RTS开发核心四支柱:帧同步、指令缓冲、状态机与空间索引
  • 长期使用Taotoken服务稳定性和路由可靠性的主观评价
  • Vue2和Vue3响应式数据对比
  • 1985-2025年 专利质押数据 xlsx
  • 基于SOM-RMO与RBFN-Tabu Search的恶意URL实时检测模型解析
  • 从浪潮到戴尔:不同品牌服务器IPMI配置的‘坑’与避坑指南(附ipmitool通用命令)
  • 长春全屋定制源头工厂选哪家 - 资讯快报
  • 从泛函分析到AutoDML:Neyman正交性与稳健统计推断的统一框架
  • 终极指南:如何用开源工具OmenSuperHub彻底释放惠普OMEN游戏本性能
  • 1寸证件照怎么制作?2026一寸照尺寸要求+免费制作教程 - 科技大爆炸
  • Midjourney云雾质感跃迁实战手册(从灰蒙蒙到电影级氛围光雾):含12组经DxO Lab实测验证的--stylize与--chaos黄金配比表
  • 通过用量看板清晰掌握网站AI功能月度资源消耗
  • JMeter HTTP接口测试全链路实战:从协议合规到业务归因
  • 2026 上海市嘉定区十大装修公司推荐榜单:真实数据核验,装修避坑指南 - 元点智创
  • 2026年成人纸尿裤经济型选购指南:高性价比产品分析与场景适配建议 - 万事通达
  • **BGE(智源)** 与 **M3E(MokaAI)** 讲清楚:定位、版本、参数、用法、RAG 选型建议,直接可用。
  • 湖北省荆门CPPMSCMP官网报考入口,官方授权双证报考中心 - 众智商学院课程中心
  • 2026年AI编程终极对决:Claude Code vs Codex,谁才是你的最佳AI同事?
  • 基于机器学习与信息论的加密系统安全实证评估方法
  • 车载露营居家随身 WiFi 哪个好用?2026实用机型功能对比 - 资讯快报
  • 模型反演攻击:TinyML场景下的隐私泄露与轻量化防御实践
  • 微信抢红包神器:Android自动抢红包插件深度体验指南
  • 告别图像异常!深入解析NVP6158 DVP接口的BT1120模式与时钟配置(以RK平台为例)
  • 湖北省恩施CPPMSCMP官网报考入口,官方授权双证报考中心 - 众智商学院课程中心
  • Beyond Compare 5密钥生成技术深度解析:从RSA加密到实战激活的全链路揭秘
  • 【Claude测试效能跃迁计划】:为什么92%的团队在v3.5升级后端到端测试失效?3步重建可信性
  • AI写作辅助平台8款AI论文平台榜单,毕业答辩稳了!
  • 从画原理图到后仿真:手把手带你用Cadence Virtuoso完成一个完整的反相器设计流程
  • 随身 wifi 性价比高的推荐,2026多场景使用便携上网设备深度测评 - 资讯快报
  • 2026年建材围挡厂家口碑推荐榜:施工围挡、钢结构围挡、市政围挡、工地围挡、彩钢围挡、地铁围挡、工程围挡、建筑围挡、地产围挡、临时围挡厂家选择指南 - 海棠依旧大