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, Optional

class TreeYAMLEditor:
    """基于树形结构的 YAML 编辑器"""
    
    def __init__(self, root: tk.Tk):
        self.root = root
        self.root.title("YAML Editor")
        self.is_modified: bool = False
        self.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 = {}
            return
        self.yaml_data = data
        
        if 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 k
                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"  {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: return
        item = selection[0]
        if item not in self.tree_items: return
            
        key, 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("提示", "请先选择要编辑的节点")
            return
        item = 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 False
        item = selection[0]
        if item not in self.tree_items: return False
            
        key, old_value, path = self.tree_items[item]
        if isinstance(old_value, (dict, list)):
            self._edit_complex_value(item)
            return True
            
        value_type = self.value_type_var.get()
        text_content = self.value_text.get("1.0", tk.END).strip()
        
        try:
            if value_type == "null": new_value = None
            elif value_type == "str": new_value = text_content
            elif 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_content
                
            self._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 True
        except ValueError as e:
            messagebox.showerror("错误", f"值转换失败:\n{e}")
            return False
            
    def 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  # ✅ 修复:空路径表示根节点
            return

        data = self.yaml_data
        for part in parts[:-1]:
            try:
                data = data[part]
            except (KeyError, IndexError, TypeError) as e:
                raise RuntimeError(f"路径导航失败: {path} -> {part}") from e

        last_part = parts[-1]
        try:
            data[last_part] = new_value
        except (KeyError, IndexError, TypeError) as e:
            raise RuntimeError(f"更新值失败: {path} -> {last_part}") from e
            
    def _parse_path(self, path: str) -> List:
        if not path: return []
        parts = []
        current = ""
        in_bracket = False
        for 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 = True
            elif char == "]":
                if current: parts.append(int(current))
                current = ""
                in_bracket = False
            else:
                current += char
        if current: parts.append(current)
        return parts
        
    def 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("提示", "只能在字典或列表下添加")
                    return
        else:
            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("提示", "键名不能为空")
                return
                
            try:
                if value_type == "str": value = val_text
                elif value_type == "int": value = int(val_text) if val_text else 0
                elif value_type == "float": value = float(val_text) if val_text else 0.0
                elif value_type == "bool": value = val_text.lower() in ("true", "1", "yes")
                elif value_type == "list": value = []
                elif value_type == "dict": value = {}
                else: value = val_text
            except ValueError:
                messagebox.showerror("错误", "值格式不正确")
                return
                
            if parent_item and self.yaml_data:
                parent_parts = self._parse_path(parent_path)
                target = self.yaml_data
                for p in parent_parts:
                    target = target[p]
                if isinstance(target, dict):
                    target[key] = value
                elif isinstance(target, list):
                    target.append(value)
            else:
                if not isinstance(self.yaml_data, dict):
                    self.yaml_data = {}
                self.yaml_data[key] = value
                
            self.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("提示", "请选择要删除的节点")
            return
        item = selection[0]
        if item not in self.tree_items: return
        key, _, path = self.tree_items[item]
        
        if not messagebox.askyesno("确认", f"确定删除 '{key}' 吗?"): return
            
        try:
            parts = self._parse_path(path)
            if not parts:
                self.yaml_data = {}  # ✅ 修复:安全处理根节点删除
            else:
                data = self.yaml_data
                for 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("确认", "当前文件未保存,是否继续?"): return
        self.yaml_data = {}
        self.current_file = None
        self.is_modified = False
        self.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("确认", "当前文件未保存,是否继续?"): return
        file_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_path
                self.is_modified = False
                self.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 False
        
    def _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_path
            self.is_modified = False
            self._update_title()
            self.status_label.config(text=f"已保存: {file_path}")
            return True
        except Exception as e:
            messagebox.showerror("错误", f"保存失败:\n{e}")
            return False
            
    def 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: return
        self.collapse_all()
        found = False
        for item in self.tree.get_children(""):
            if self._search_recursive(item, keyword): found = True
        if not found: messagebox.showinfo("搜索", f"未找到: '{keyword}'")
            
    def _search_recursive(self, item: str, keyword: str) -> bool:
        found = False
        text = 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 = True
        for child in self.tree.get_children(item):
            if self._search_recursive(child, keyword): found = True
        return found
        
    def 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 = True
        self._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("确认", "当前文件未保存,是否退出?"): return
        self.root.destroy()
 
 
def main():
    root = tk.Tk()
    style = ttk.Style()
    try:
        style.theme_use("clam")
    except tk.TclError:
        pass
    app = TreeYAMLEditor(root)
    root.mainloop()
 
if __name__ == "__main__":
    main()
posted @ 2026-05-25 20:24  乌合之众  阅读(11)  评论(0)    收藏  举报
clear