import tkinter as tk
from tkinter import ttk, filedialog, messagebox, scrolledtext
import pandas as pd
import matplotlib.pyplot as plt
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
import os
from datetime import datetime
class ExcelQuerySystem:
def __init__(self, root):
self.root = root
self.root.title("Excel表格查询系统")
self.root.geometry("1200x800")
self.root.minsize(1000, 600)
# 初始化变量
self.df = None
self.filtered_df = None
self.current_page = 1
self.rows_per_page = 20
self.sort_column = None
self.sort_order = "asc"
# 设置样式
self.style = ttk.Style()
self.style.theme_use("clam")
self.setup_styles()
# 创建主布局
self.create_main_layout()
# 创建菜单
self.create_menus()
# 创建工具栏
self.create_toolbar()
# 创建主内容区
self.create_main_content()
# 初始状态禁用某些控件
self.update_widget_states()
def setup_styles(self):
"""设置界面样式"""
# 主色调:蓝色
primary_color = "#3498db"
secondary_color = "#2980b9"
accent_color = "#f39c12"
# 配置样式
self.style.configure("Main.TFrame", background="white")
self.style.configure("ToolBar.TFrame", background="#f5f5f5")
self.style.configure("SidePanel.TFrame", background="#f8f9fa")
self.style.configure("MainContent.TFrame", background="white")
# 按钮样式
self.style.configure("Primary.TButton",
background=primary_color,
foreground="white",
borderwidth=0,
padding=6)
self.style.map("Primary.TButton",
background=[("active", secondary_color)])
self.style.configure("Accent.TButton",
background=accent_color,
foreground="white",
borderwidth=0,
padding=6)
self.style.map("Accent.TButton",
background=[("active", "#e67e22")])
# 标签样式
self.style.configure("Header.TLabel",
font=("Arial", 14, "bold"),
foreground="#2c3e50",
padding=10)
self.style.configure("Section.TLabel",
font=("Arial", 12, "bold"),
foreground="#34495e",
padding=5)
# 表格样式
self.style.configure("DataTable.Treeview",
font=("Arial", 10),
rowheight=25)
self.style.configure("DataTable.Treeview.Heading",
font=("Arial", 11, "bold"),
background=primary_color,
foreground="white")
# 输入框样式
self.style.configure("Entry.TEntry",
padding=5,
bordercolor="#bdc3c7",
relief="flat")
# 组合框样式
self.style.configure("TCombobox",
padding=5,
bordercolor="#bdc3c7",
relief="flat")
def create_main_layout(self):
"""创建主布局"""
# 主框架
self.main_frame = ttk.Frame(self.root, style="Main.TFrame")
self.main_frame.pack(fill=tk.BOTH, expand=True)
# 工具栏框架
self.toolbar_frame = ttk.Frame(self.main_frame, style="ToolBar.TFrame", height=40)
self.toolbar_frame.pack(fill=tk.X, side=tk.TOP)
# 内容区框架
self.content_frame = ttk.Frame(self.main_frame, style="Main.TFrame")
self.content_frame.pack(fill=tk.BOTH, expand=True)
# 左侧面板
self.side_panel = ttk.Frame(self.content_frame, style="SidePanel.TFrame", width=300)
self.side_panel.pack(fill=tk.Y, side=tk.LEFT, padx=5, pady=5)
# 右侧主内容
self.main_content = ttk.Frame(self.content_frame, style="MainContent.TFrame")
self.main_content.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
def create_menus(self):
"""创建菜单"""
menubar = tk.Menu(self.root)
# 文件菜单
file_menu = tk.Menu(menubar, tearoff=0)
file_menu.add_command(label="导入Excel", command=self.import_excel)
file_menu.add_command(label="导出结果", command=self.export_results)
file_menu.add_separator()
file_menu.add_command(label="退出", command=self.root.quit)
menubar.add_cascade(label="文件", menu=file_menu)
# 帮助菜单
help_menu = tk.Menu(menubar, tearoff=0)
help_menu.add_command(label="使用说明", command=self.show_help)
help_menu.add_command(label="关于", command=self.show_about)
menubar.add_cascade(label="帮助", menu=help_menu)
self.root.config(menu=menubar)
def create_toolbar(self):
"""创建工具栏"""
# 导入按钮
self.import_btn = ttk.Button(self.toolbar_frame, text="导入Excel",
command=self.import_excel, style="Primary.TButton")
self.import_btn.pack(side=tk.LEFT, padx=5, pady=5)
# 导出按钮
self.export_btn = ttk.Button(self.toolbar_frame, text="导出结果",
command=self.export_results, style="Accent.TButton")
self.export_btn.pack(side=tk.LEFT, padx=5, pady=5)
# 统计按钮
self.stats_btn = ttk.Button(self.toolbar_frame, text="数据统计",
command=self.show_statistics, style="Primary.TButton")
self.stats_btn.pack(side=tk.LEFT, padx=5, pady=5)
# 分隔符
separator = ttk.Separator(self.toolbar_frame, orient=tk.VERTICAL)
separator.pack(side=tk.LEFT, fill=tk.Y, padx=5, pady=5)
# 状态标签
self.status_label = ttk.Label(self.toolbar_frame, text="就绪", font=("Arial", 10))
self.status_label.pack(side=tk.RIGHT, padx=10, pady=5)
def create_main_content(self):
"""创建主内容区"""
# 表格数据展示区
self.table_frame = ttk.Frame(self.main_content)
self.table_frame.pack(fill=tk.BOTH, expand=True)
# 表格
self.tree = ttk.Treeview(self.table_frame, style="DataTable.Treeview")
self.tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
# 滚动条
self.scrollbar = ttk.Scrollbar(self.table_frame, orient=tk.VERTICAL, command=self.tree.yview)
self.scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
self.tree.configure(yscrollcommand=self.scrollbar.set)
# 分页控件
self.pagination_frame = ttk.Frame(self.main_content)
self.pagination_frame.pack(fill=tk.X, side=tk.BOTTOM, pady=5)
self.prev_btn = ttk.Button(self.pagination_frame, text="上一页", command=self.prev_page)
self.prev_btn.pack(side=tk.LEFT, padx=5)
self.page_label = ttk.Label(self.pagination_frame, text="第 1 页")
self.page_label.pack(side=tk.LEFT, padx=5)
self.next_btn = ttk.Button(self.pagination_frame, text="下一页", command=self.next_page)
self.next_btn.pack(side=tk.LEFT, padx=5)
# 图表展示区
self.chart_frame = ttk.Frame(self.main_content)
self.chart_frame.pack(fill=tk.BOTH, expand=True, pady=5)
# 初始隐藏图表区
self.chart_frame.pack_forget()
def create_side_panel_content(self):
"""创建侧边面板内容"""
# 清空现有内容
for widget in self.side_panel.winfo_children():
widget.destroy()
# 标题
title_label = ttk.Label(self.side_panel, text="查询条件", style="Section.TLabel")
title_label.pack(fill=tk.X, pady=5)
# 查询条件区域
self.query_frame = ttk.Frame(self.side_panel)
self.query_frame.pack(fill=tk.X, padx=5, pady=5)
# 添加查询条件行
self.add_query_row()
# 添加条件按钮
self.add_condition_btn = ttk.Button(self.side_panel, text="+ 添加条件",
command=self.add_query_row, style="Primary.TButton")
self.add_condition_btn.pack(fill=tk.X, padx=5, pady=2)
# 查询按钮
self.query_btn = ttk.Button(self.side_panel, text="执行查询",
command=self.execute_query, style="Accent.TButton")
self.query_btn.pack(fill=tk.X, padx=5, pady=10)
# 重置按钮
self.reset_btn = ttk.Button(self.side_panel, text="重置查询",
command=self.reset_query, style="Primary.TButton")
self.reset_btn.pack(fill=tk.X, padx=5, pady=2)
# 数据信息区域
info_label = ttk.Label(self.side_panel, text="数据信息", style="Section.TLabel")
info_label.pack(fill=tk.X, padx=5, pady=10)
self.info_text = scrolledtext.ScrolledText(self.side_panel, height=8, wrap=tk.WORD)
self.info_text.pack(fill=tk.BOTH, padx=5, pady=5)
self.info_text.config(state=tk.DISABLED)
def add_query_row(self):
"""添加查询条件行"""
row_frame = ttk.Frame(self.query_frame)
row_frame.pack(fill=tk.X, pady=3)
# 列选择
columns = list(self.df.columns) if self.df is not None else []
column_combo = ttk.Combobox(row_frame, values=columns, state="readonly", width=15)
column_combo.pack(side=tk.LEFT, padx=2)
# 操作符选择
operators = ["包含", "等于", "不等于", "大于", "小于", "大于等于", "小于等于"]
operator_combo = ttk.Combobox(row_frame, values=operators, state="readonly", width=10)
operator_combo.pack(side=tk.LEFT, padx=2)
operator_combo.current(0)
# 值输入
value_entry = ttk.Entry(row_frame)
value_entry.pack(side=tk.LEFT, padx=2, fill=tk.X, expand=True)
# 删除按钮
delete_btn = ttk.Button(row_frame, text="×", command=lambda: row_frame.destroy(),
width=3, style="Accent.TButton")
delete_btn.pack(side=tk.LEFT, padx=2)
def update_widget_states(self):
"""更新控件状态"""
if self.df is None:
# 禁用导出、统计按钮
self.export_btn.config(state=tk.DISABLED)
self.stats_btn.config(state=tk.DISABLED)
self.query_btn.config(state=tk.DISABLED)
self.reset_btn.config(state=tk.DISABLED)
self.add_condition_btn.config(state=tk.DISABLED)
self.prev_btn.config(state=tk.DISABLED)
self.next_btn.config(state=tk.DISABLED)
else:
# 启用导出、统计按钮
self.export_btn.config(state=tk.NORMAL)
self.stats_btn.config(state=tk.NORMAL)
self.query_btn.config(state=tk.NORMAL)
self.reset_btn_btn.config(state=tk.NORMAL)
self.add_condition_btn.config(state=tk.NORMAL)
# 更新根据数据量启用/禁用分页按钮
total_pages = self.get_total_pages()
if total_pages <= 1:
self.prev_btn.config(state=tk.DISABLED)
self.next_btn.config(state=tk.DISABLED)
else:
self.prev_btn.config(state=tk.NORMAL if self.current_page > 1 else tk.DISABLED)
self.next_btn.config(state=tk.NORMAL if self.current_page < total_pages_pages else tk.DISABLED)
def import_excel(self):
"""导入Excel文件"""
file_path = filedialog.askopenfilename(
title="选择Excel文件",
filetypes=[("Excel文件", "*.xlsx *.xls"), ("所有文件", "*.*")]
)
if not file_path:
return
try:
# 读取Excel文件
self.df = pd.read_excel(file_path)
self.filtered_df = self.df.copy()
self.current_page = 1
# 更新状态
self.status_label.config(text=f"已导入: {os.path.basename(file_path)}")
# 显示数据信息
self.update_info_text()
# 创建侧边面板内容
self.create_side_panel_content()
# 显示表格数据
self.display_table_data()
# 更新控件状态
self.update_widget_states()
messagebox.showinfo("导入成功", f"成功导入Excel文件,包含 {len(self.df)} 行数据")
except Exception as e:
messagebox.showerror("导入失败", f"导入Excel文件时出错: {str(e)}")
self.status_label.config(text="导入失败")
def update_info_text(self):
"""更新信息文本框"""
if self.df is None:
return
info = f"数据文件: {self.status_label.cget('text').replace('已导入: ', '')}\n"
info += f"总行数: {len(self.df)}\n"
info += f"总列数: {len(self.df.columns)}\n\n"
info += "列名及数据类型:\n"
for col in self.df.columns:
dtype = str(self.df[col].dtype)
info += f"- {col}: {dtype}\n"
self.info_text.config(state=tk.NORMAL)
self.info_text.delete(1.0, tk.END)
self.info_text.insert(tk.END, info)
self.info_text.config(state=tk.DISABLED)
def display_table_data(self):
"""显示表格数据"""
# 清空现有表格
self.tree.delete(*self.tree.get_children())
# 如果没有数据,显示提示信息
if self.filtered_df is None or len(self.filtered_df) == 0:
self.tree["columns"] = ["提示"]
self.tree.column("#0", width=0, stretch=tk.NO)
self.tree.column("提示", anchor=tk.CENTER, width=800)
self.tree.heading("提示", text="没有符合条件的数据")
self.tree.insert("", tk.END, values=["没有符合条件的数据"])
return
# 获取当前页数据
start_idx = (self.current_page - 1) * self.rows_per_page
end_idx = start_idx + self.rows_per_page
page_data = self.filtered_df.iloc[start_idx:end_idx]
# 设置表格列
columns = list(page_data.columns)
self.tree["columns"] = columns
# 隐藏第一列(默认的#0列)
self.tree.column("#0", width=0, stretch=tk.NO)
# 设置列属性
for col in columns:
self.tree.column(col, anchor=tk.CENTER, width=100, minwidth=80)
self.tree.heading(col, text=col, command=lambda c=col: self.sort_data(c))
# 添加数据行
for _, row in page_data.iterrows():
values = [str(val) if pd.notna(val) else "" for val in row.values]
self.tree.insert("", tk.END, values=values)
# 更新分页信息
total_pages = self.get_total_pages()
self.page_label.config(text=f"第 {self.current_page} 页 / 共 {total_pages} 页")
def get_total_pages(self):
"""计算总页数"""
if self.filtered_df is None or len(self.filtered_df) == 0:
return 1
return (len(self.filtered_df) + self.rows_per_page - 1) // self.rows_per_page
def prev_page(self):
"""上一页"""
if self.current_page > 1:
self.current_page -= 1
self.display_table_data()
self.update_widget_states()
def next_page(self):
"""下一页"""
total_pages = self.get_total_pages()
if self.current_page < total_pages:
self.current_page += 1
self.display_table_data()
self.update_widget_states()
def sort_data(self, column):
"""排序数据"""
if self.filtered_df is None:
return
# 如果点击的是当前排序列,则切换排序顺序
if self.sort_column == column:
self.sort_order = "desc" if self.sort_order == "asc" else "asc"
else:
self.sort_column = column
self.sort_order = "asc"
# 执行排序
self.filtered_df = self.filtered_df.sort_values(
by=column, ascending=(self.sort_order == "asc")
)
# 重置到第一页
self.current_page = 1
# 更新表格显示
self.display_table_data()
def execute_query(self):
"""执行查询"""
if self.df is None:
return
# 获取所有查询条件
conditions = []
valid = True
for row_frame in self.query_frame.winfo_children():
widgets = row_frame.winfo_children()
if len(widgets) < 3:
continue
column_combo = widgets[0]
operator_combo = widgets[1]
value_entry = widgets[2]
column = column_combo.get()
operator = operator_combo.get()
value = value_entry.get().strip()
if not column or not value:
continue
conditions.append((column, operator, value))
# 应用查询条件
try:
self.filtered_df = self.df.copy()
for column, operator, value in conditions:
if column not in self.filtered_df.columns:
continue
# 根据数据类型处理查询值
dtype = self.filtered_df[column].dtype
try:
if "int" in str(dtype):
value = int(value)
elif "float" in str(dtype):
value = float(value)
elif "datetime" in str(dtype):
value = pd.to_datetime(value)
except ValueError:
# 如果无法转换,则作为字符串处理
pass
# 应用过滤条件
if operator == "包含":
if isinstance(value, str):
self.filtered_df = self.filtered_df[
self.filtered_df[column].astype(str).str.contains(value, na=False)
]
elif operator == "等于":
self.filtered_df = self.filtered_df[
self.filtered_df[column] == value
]
elif operator == "不等于":
self.filtered_df = self.filtered_df[
self.filtered_df[column] != value
]
elif operator == "大于":
self.filtered_df = self.filtered_df[
self.filtered_df[column] > value
]
elif operator == "小于":
self.filtered_df = self.filtered_df[
self.filtered_df[column] < value
]
elif operator == "大于等于":
self.filtered_df = self.filtered_df[
self.filtered_df[column] >= value
]
elif operator == "小于等于":
self.filtered_df = self.filtered_df[
self.filtered_df[column] <= value
]
# 重置到第一页
self.current_page = 1
# 更新表格显示
self.display_table_data()
# 更新状态
self.status_label.config(text=f"查询完成,找到 {len(self.filtered_df)} 条结果")
# 更新控件状态
self.update_widget_states()
except Exception as e:
messagebox.showerror("查询失败", f"执行查询时出错: {str(e)}")
def reset_query(self):
"""重置查询"""
if self.df is None:
return
# 重置过滤后的数据
self.filtered_df = self.df.copy()
self.current_page = 1
# 清空查询条件
for widget in self.query_frame.winfo_children():
widget.destroy()
# 添加一行空的查询条件
self.add_query_row()
# 更新表格显示
self.display_table_data()
# 更新状态
self.status_label.config(text="查询已重置")
# 更新控件状态
self.update_widget_states()
def export_results(self):
"""导出查询结果"""
if self.filtered_df is None or len(self.filtered_df) == 0:
messagebox.showwarning("导出警告", "没有可导出的数据")
return
# 选择导出文件路径
file_path = filedialog.asksaveasfilename(
title="导出结果",
defaultextension=".xlsx",
filetypes=[("Excel文件", "*.xlsx"), ("CSV文件", "*.csv"), ("所有文件", "*.*")]
)
if not file_path:
return
try:
# 根据文件扩展名选择导出格式
if file_path.endswith(".csv"):
self.filtered_df.to_csv(file_path, index=False, encoding="utf-8-sig")
else:
self.filtered_df.to_excel(file_path, index=False)
messagebox.showinfo("导出成功", f"成功导出 {len(self.filtered_df)} 行数据到 {file_path}")
self.status_label.config(text=f"已导出: {os.path.basename(file_path)}")
except Exception as e:
messagebox.showerror("导出失败", f"导出数据时出错: {str(e)}")
def show_statistics(self):
"""显示数据统计"""
if self.filtered_df is None or len(self.filtered_df) == 0:
messagebox.showwarning("统计警告", "没有可统计的数据")
return
# 显示图表区域
self.chart_frame.pack(fill=tk.BOTH, expand=True, pady=5)
# 清空现有图表
for widget in self.chart_frame.winfo_children():
widget.destroy()
# 创建统计信息标签
stats_label = ttk.Label(self.chart_frame, text="数据统计与可视化", style="Header.TLabel")
stats_label.pack(fill=tk.X, pady=5)
# 创建统计内容框架
stats_content_frame = ttk.Frame(self.chart_frame)
stats_content_frame.pack(fill=tk.BOTH, expand=True)
# 左侧统计信息
stats_text_frame = ttk.Frame(stats_content_frame, width=300)
stats_text_frame.pack(fill=tk.Y, side=tk.LEFT, padx=5)
# 生成统计信息
stats_text = self.generate_statistics_text()
# 显示统计信息
stats_text_widget = scrolledtext.ScrolledText(stats_text_frame, wrap=tk.WORD)
stats_text_widget.pack(fill=tk.BOTH, expand=True)
stats_text_widget.insert(tk.END, stats_text)
stats_text_widget.config(state=tk.DISABLED)
# 右侧图表
chart_canvas_frame = ttk.Frame(stats_content_frame)
chart_canvas_frame.pack(fill=tk.BOTH, expand=True, padx=5)
# 创建图表
self.create_chart(chart_canvas_frame)
def generate_statistics_text(self):
"""生成统计信息文本"""
if self.filtered_df is None:
return ""
stats_text = f"数据概览:\n"
stats_text += f"- 总记录数: {len(self.filtered_df)}\n"
stats_text += f"- 总列数: {len(self.filtered_df.columns)}\n\n"
# 数值型列的统计信息
numeric_columns = self.filtered_df.select_dtypes(include=["int64", "float64"]).columns
if len(numeric_columns) > 0:
stats_text += "数值型列统计:\n"
for col in numeric_columns:
stats_text += f"\n{col}:\n"
stats_text += f" - 平均值: {self.filtered_df[col].mean():.2f}\n"
stats_text += f" - 中位数: {self.filtered_df[col].median():.2f}\n"
stats_text += f" - 最小值: {self.filtered_df[col].min():.2f}\n"
stats_text += f" - 最大值: {self.filtered_df[col].max():.2f}\n"
stats_text += f" - 标准差: {self.filtered_df[col].std():.2f}\n"
# 字符串列的统计信息
string_columns = self.filtered_df.select_dtypes(include=["object"]).columns
if len(string_columns) > 0:
stats_text += "\n字符串列统计:\n"
for col in string_columns:
non_null_count = self.filtered_df[col].notna().sum()
unique_count = self.filtered_df[col].nunique()
stats_text += f"\n{col}:\n"
stats_text += f" - 非空值数量: {non_null_count}\n"
stats_text += f" - 唯一值数量: {unique_count}\n"
# 如果唯一值数量不多,显示前5个最常见的值
if unique_count > 0 and unique_count <= 20:
top_values = self.filtered_df[col].value_counts().head(5)
stats_text += " - 常见值: "
for val, count in top_values.items():
stats_text += f"{val}({count}), "
stats_text = stats_text.rstrip(", ") + "\n"
# 日期时间列的统计信息
datetime_columns = self.filtered_df.select_dtypes(include=["datetime64"]).columns
if len(datetime_columns) > 0:
stats_text += "\n日期时间列统计:\n"
for col in datetime_columns:
stats_text += f"\n{col}:\n"
stats_text += f" - 最早日期: {self.filtered_df[col].min()}\n"
stats_text += f" - 最晚日期: {self.filtered_df[col].max()}\n"
stats_text += f" - 时间跨度: {(self.filtered_df[col].max() - self.filtered_df[col].min()).days} 天\n"
return stats_text
def create_chart(self, parent_frame):
"""创建数据可视化图表"""
if self.filtered_df is None:
return
# 选择第一个数值型列进行可视化
numeric_columns = self.filtered_df.select_dtypes(include=["int64", "float64"]).columns
if len(numeric_columns) == 0:
# 如果没有数值型列,选择第一个字符串列进行柱状图
string_columns = self.filtered_df.select_dtypes(include=["object"]).columns
if len(string_columns) == 0:
return
col = string_columns[0]
value_counts = self.filtered_df[col].value_counts().head(10)
# 创建柱状图
fig, ax = plt.subplots(figsize=(8, 6))
ax.bar(value_counts.index.astype(str), value_counts.values)
ax.set_title(f"{col} 分布")
ax.set_xlabel(col)
ax.set_ylabel("数量")
plt.xticks(rotation=45, ha="right")
else:
col = numeric_columns[0]
# 创建直方图
fig, ax = plt.subplots(figsize=(8, 6))
ax.hist(self.filtered_df[col].dropna(), bins=20, edgecolor="black")
ax.set_title(f"{col} 分布")
ax.set_xlabel(col)
ax.set_ylabel("频数")
# 调整布局
plt.tight_layout()
# 创建画布并显示
canvas = FigureCanvasTkAgg(fig, master=parent_frame)
canvas.draw()
canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True)
def show_help(self):
"""显示使用说明"""
help_text = """
Excel表格查询系统使用说明
1. 导入Excel文件
- 点击"导入Excel"按钮或通过"文件"菜单选择Excel文件
- 支持.xlsx和.xls格式
2. 查询数据
- 在左侧面板设置查询条件
- 点击"+ 添加条件"可以增加多个查询条件
- 点击"执行查询"按钮应用筛选
- 点击"重置查询"按钮清除所有条件
3. 数据操作
- 点击表头可以按该列排序
- 使用分页控件浏览大量数据
- 点击"导出结果"按钮可以将查询结果导出为Excel或CSV文件
4. 数据统计
- 点击"数据统计"按钮查看数据统计信息和可视化图表
- 统计信息包括基本统计量、分布情况等
- 可视化图表根据数据类型自动生成
5. 其他功能
- 通过"帮助"菜单可以查看使用说明和关于信息
- 通过"文件"菜单可以退出系统
"""
messagebox.showinfo("使用说明", help_text)
def show_about(self):
"""显示关于信息"""
about_text = """
Excel表格查询系统 v1.0
一个简单易用的Excel数据查询和分析工具,
无需编程知识即可快速查询、筛选和分析Excel表格数据。
主要功能:
- Excel文件导入与预览
- 多条件组合查询
- 查询结果导出
- 数据统计与可视化
技术支持:Python + Tkinter + Pandas + Matplotlib
© 2025 Excel表格查询系统
"""
messagebox.showinfo("关于", about_text)
def main():
"""主函数"""
root = tk.Tk()
app = ExcelQuerySystem(root)
root.mainloop()
if __name__ == "__main__":
main()
最新发布