UIPosts.java v2.0
多端自定义GUI、精美按钮和完善边界提示,提供权限、统计和交互引导的多分页帖子系统
package org.abc.untitled1;
import org.bukkit.Bukkit;
import org.bukkit.ChatColor;
import org.bukkit.Material;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.command.TabCompleter;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.inventory.InventoryClickEvent;
import org.bukkit.event.player.PlayerInteractEvent;
import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.BookMeta;
import org.bukkit.inventory.meta.ItemMeta;
import org.bukkit.metadata.FixedMetadataValue;
import org.bukkit.plugin.java.JavaPlugin;
import org.bukkit.scheduler.BukkitRunnable;
import java.io.File;
import java.sql.*;
import java.text.SimpleDateFormat;
import java.util.*;
import java.net.HttpURLConnection;
import java.net.URL;
import java.io.InputStreamReader;
import java.io.BufferedReader;
/**
* @pluginName UIPosts
* @author ScriptIrc Engine
* @version 2.0
* @description 多端自定义GUI、精美按钮和完善边界提示,提供权限、统计和交互引导的多分页帖子系统
* [command]uiposts|打开UIPosts多界面系统
* [Permission]uiposts.post|允许发帖
* [Permission]uiposts.reply|允许留言
* [Permission]uiposts.admin|管理及调试命令
*/
public class UIPosts extends JavaPlugin implements Listener, TabCompleter {
private static final String DB_FILE = "uiposts.db";
private static final String TIME_API = "http://worldtimeapi.org/api/timezone/Asia/Shanghai";
private static final int POST_LIMIT = 100, REPLY_LIMIT = 50;
private static final ChatColor PRIMARY = ChatColor.AQUA, SECONDARY = ChatColor.YELLOW, SUCCESS = ChatColor.GREEN, ERROR = ChatColor.RED;
private Connection connection;
private Map<String, Object> guiConfig;
private final Map<UUID, Boolean> awaitingPostInput = new HashMap<>();
private final Map<UUID, Integer> awaitingReply = new HashMap<>();
private final Map<UUID, Long> lastPostTime = new HashMap<>();
@Override
public void onEnable() {
Bukkit.getPluginManager().registerEvents(this, this);
if (getCommand("uiposts") != null) {
getCommand("uiposts").setTabCompleter(this);
}
initDatabase();
loadConfig();
getLogger().info("UIPosts v2.0 启动,丰富UI+安全交互!");
}
@Override
public void onDisable() {
try { if (connection != null && !connection.isClosed()) connection.close(); } catch (Exception ignored) {}
getLogger().info("UIPosts 插件关闭.");
}
// ======= 配置、数据库部分 =======
private void loadConfig() {
guiConfig = new HashMap<>();
Map<String, Object> chestGUI = new HashMap<>();
chestGUI.put("title", PRIMARY+"§l帖子广场 §f[Chest]{sep}{page}/{pages}");
chestGUI.put("size", 27);
chestGUI.put("post_slot", Arrays.asList(10, 11, 12, 13, 14));
chestGUI.put("next_slot", 25);
chestGUI.put("prev_slot", 19);
chestGUI.put("home_slot", 22);
chestGUI.put("stat_slot", 15);
chestGUI.put("theme", ChatColor.AQUA.toString()); // 主题色
chestGUI.put("post_button", SUCCESS+"§l我要发帖");
chestGUI.put("jump_book", SECONDARY+"§l[切换到书本模式]");
chestGUI.put("refresh", PRIMARY+"§l刷新/回首页");
guiConfig.put("chest", chestGUI);
Map<String, Object> bookGUI = new HashMap<>();
bookGUI.put("title", PRIMARY+"§l帖子广场 §f[书本]{sep}{page}页");
bookGUI.put("lines_per_page", 7);
guiConfig.put("book", bookGUI);
}
private void initDatabase() {
try {
File dbFile = new File(getDataFolder(), DB_FILE);
if (!getDataFolder().exists()) getDataFolder().mkdirs();
boolean init = false;
if (!dbFile.exists()) {
dbFile.createNewFile();
init = true;
}
connection = DriverManager.getConnection("jdbc:sqlite:" + dbFile.getPath());
if (init) {
Statement st = connection.createStatement();
st.executeUpdate("CREATE TABLE IF NOT EXISTS posts (id INTEGER PRIMARY KEY AUTOINCREMENT, player VARCHAR, content TEXT, time VARCHAR)");
st.executeUpdate("CREATE TABLE IF NOT EXISTS replies (id INTEGER PRIMARY KEY AUTOINCREMENT, post_id INTEGER, player VARCHAR, content TEXT, time VARCHAR)");
st.close();
}
} catch (Exception e) { e.printStackTrace(); }
}
// ============ 命令与补全 ============
@Override
public boolean onCommand(CommandSender sender, Command cmd, String label, String[] args) {
if (!(sender instanceof Player)) { sender.sendMessage(ERROR+"仅玩家可使用此命令."); return true; }
Player p = (Player) sender;
String type = (args.length == 0) ? "chest" : args[0].toLowerCase();
if (type.equals("chest")) {
openChestGUI(p, 1);
} else if (type.equals("book")) {
openBookGUI(p, 1);
} else {
p.sendMessage(SECONDARY+"[用法] "+PRIMARY+"/uiposts chest"+SECONDARY+" 或 "+PRIMARY+"/uiposts book");
}
return true;
}
@Override
public List<String> onTabComplete(CommandSender s, Command c, String l, String[] a) {
if (a.length == 1) return Arrays.asList("chest", "book");
return null;
}
// ======= 核心UI与事件 =======
private void openChestGUI(Player p, int page) {
Map<String, Object> cfg = (Map<String, Object>) guiConfig.get("chest");
int size = (int) cfg.get("size");
String title = ((String) cfg.get("title")).replace("{sep}", ChatColor.GRAY+" | ");
List<Integer> postSlots = (List<Integer>) cfg.get("post_slot");
int totalPages = getTotalPages(postSlots.size());
page = Math.max(1, Math.min(page, totalPages));
Inventory inv = Bukkit.createInventory(null, size, parse(title, p, page, totalPages));
List<Post> posts = getPostsByPage(page, postSlots.size());
int i = 0;
for (; i < posts.size() && i < postSlots.size(); i++) {
Post post = posts.get(i);
ItemStack item = new ItemStack(Material.WRITABLE_BOOK);
ItemMeta im = item.getItemMeta();
List<String> lore = new ArrayList<>();
im.setDisplayName(PRIMARY+"【"+post.player+"】"+ChatColor.GRAY+" - "+ChatColor.WHITE+post.time);
lore.add(SECONDARY+wrapLines(post.content,25));
int replyCount = getReplies(post.id).size();
lore.add((replyCount>0? ChatColor.LIGHT_PURPLE+"回复 "+replyCount+"条": ChatColor.GRAY+"暂无留言"));
lore.add(ChatColor.GRAY+"点击查看/留言");
im.setLore(lore);
item.setItemMeta(im);
inv.setItem(postSlots.get(i), item);
}
// 发帖按钮
ItemStack postBtn = new ItemStack(Material.ANVIL);
ItemMeta pm = postBtn.getItemMeta();
pm.setDisplayName((String) cfg.get("post_button"));
pm.setLore(Arrays.asList(
ChatColor.GRAY+"你可以每隔5秒发一次帖",
ChatColor.YELLOW+"长度上限 "+POST_LIMIT+" 字节"));
postBtn.setItemMeta(pm);
inv.setItem(4, postBtn);
// 书本切换
ItemStack jumpBtn = new ItemStack(Material.BOOK);
ItemMeta jm = jumpBtn.getItemMeta();
jm.setDisplayName((String) cfg.get("jump_book"));
jm.setLore(Arrays.asList(ChatColor.GRAY+"以书本分布分页浏览"));
jumpBtn.setItemMeta(jm);
inv.setItem((int) cfg.get("home_slot"), jumpBtn);
// 刷新按钮
ItemStack refreshBtn = new ItemStack(Material.SUNFLOWER);
ItemMeta ref = refreshBtn.getItemMeta();
ref.setDisplayName(cfg.get("refresh").toString());
ref.setLore(Arrays.asList(ChatColor.GRAY+"点击返回首页,刷新帖子"));
refreshBtn.setItemMeta(ref);
inv.setItem(16, refreshBtn);
// 统计按钮
ItemStack statBtn = new ItemStack(Material.PAPER);
ItemMeta stat = statBtn.getItemMeta();
int mycnt=countPlayerPosts(p.getName());
int totalcnt=countPosts();
stat.setDisplayName(ChatColor.BOLD+"帖子统计信息");
stat.setLore(Arrays.asList(ChatColor.GRAY+"总帖数: "+ChatColor.GREEN+totalcnt,ChatColor.GRAY+"你发帖: "+ChatColor.YELLOW+mycnt));
statBtn.setItemMeta(stat);
inv.setItem((int)cfg.get("stat_slot"),statBtn);
// 下一页
ItemStack nextBtn = new ItemStack(Material.ARROW);
ItemMeta nm = nextBtn.getItemMeta();
nm.setDisplayName(ChatColor.YELLOW+"§l下一页 »");
nm.setLore(Arrays.asList(ChatColor.GRAY+"第 "+(page+1)+" 页"+(page>=totalPages?ChatColor.DARK_GRAY+"(无更多)":"")));
nextBtn.setItemMeta(nm);
if (page<totalPages) inv.setItem((int) cfg.get("next_slot"), nextBtn);
// 上一页
ItemStack prevBtn = new ItemStack(Material.ARROW);
ItemMeta pmv = prevBtn.getItemMeta();
pmv.setDisplayName(ChatColor.YELLOW+"« 上一页");
pmv.setLore(Arrays.asList(ChatColor.GRAY+"第 "+(page-1)+" 页"+(page<=1?ChatColor.DARK_GRAY+"(最前)":"")));
prevBtn.setItemMeta(pmv);
if (page>1) inv.setItem((int) cfg.get("prev_slot"), prevBtn);
p.openInventory(inv);
p.setMetadata("uiposts_gui_page", new FixedMetadataValue(this, page));
p.setMetadata("uiposts_gui_type", new FixedMetadataValue(this, "chest"));
}
private void openBookGUI(Player p, int page) {
Map<String, Object> cfg = (Map<String, Object>) guiConfig.get("book");
int lines = (int) cfg.get("lines_per_page");
String title = ((String) cfg.get("title")).replace("{sep}", ChatColor.GRAY+" | ");
List<Post> posts = getPostsByPage(page, lines);
ItemStack book = new ItemStack(Material.WRITTEN_BOOK);
BookMeta meta = (BookMeta) book.getItemMeta();
List<String> pages = new ArrayList<>();
StringBuilder sb = new StringBuilder();
for (int i = 0; i < posts.size(); i++) {
Post post = posts.get(i);
sb.append(PRIMARY+"【"+post.player+"】 "+ChatColor.GRAY+post.time+"\n");
sb.append(ChatColor.RESET).append(wrapLines(post.content,25)).append("\n");
List<Reply> replies = getReplies(post.id);
for (Reply r : replies) {
sb.append(ChatColor.GRAY+"- " + r.player + ": "+wrapLines(r.content,20)).append("\n");
}
sb.append("\n");
if ((i+1)%lines==0 || i==posts.size()-1) {
sb.append(ChatColor.DARK_GRAY+"[第"+(page)+"页] /uiposts chest");
pages.add(sb.toString());
sb.setLength(0);
}
}
if (pages.isEmpty()) pages.add(ChatColor.GRAY+"暂无帖子, "+SUCCESS+"去发第一条吧!");
meta.setAuthor("UIPosts");
meta.setTitle(parse(title,p,page,getTotalPages(lines)));
meta.setPages(pages);
book.setItemMeta(meta);
int oldSlot = p.getInventory().getHeldItemSlot();
p.getInventory().setItem(oldSlot, book);
new BukkitRunnable(){public void run(){p.openBook(book);}}.runTaskLater(this,2L);
p.setMetadata("uiposts_gui_page", new FixedMetadataValue(this, page));
p.setMetadata("uiposts_gui_type", new FixedMetadataValue(this, "book"));
}
@EventHandler
public void onInvClick(InventoryClickEvent e) {
Player p = (Player) e.getWhoClicked();
if (!p.hasMetadata("uiposts_gui_type") || !p.hasMetadata("uiposts_gui_page")) return;
String title = e.getView().getTitle();
if (!title.contains("帖子广场")) return;
e.setCancelled(true);
int page = p.getMetadata("uiposts_gui_page").get(0).asInt();
String type = p.getMetadata("uiposts_gui_type").get(0).asString();
ItemStack current = e.getCurrentItem();
if (current == null || !current.hasItemMeta()) return;
String name = ChatColor.stripColor(current.getItemMeta().getDisplayName());
if (name.contains("我要发帖")) {
if (!p.hasPermission("uiposts.post")) {p.sendMessage(ERROR+"无权发帖: 需要权限 uiposts.post");return;}
long now=System.currentTimeMillis();
if(lastPostTime.containsKey(p.getUniqueId())&&(now-lastPostTime.get(p.getUniqueId())<5000)){
p.sendMessage(ERROR+"冷却中!每5秒可发一次~"); return;
}
p.closeInventory(); askForPostInput(p); return;
}
if (name.contains("切换到书本模式")) { p.closeInventory(); openBookGUI(p, 1); return; }
if (name.contains("刷新")||name.contains("首页")) { p.closeInventory(); openChestGUI(p,1); return; }
if (name.contains("下一页")) { openChestGUI(p, page+1); return; }
if (name.contains("上一页")) { openChestGUI(p,Math.max(1,page-1)); return; }
if (current.getType()==Material.WRITABLE_BOOK) {
List<Integer> postSlots = (List<Integer>) ((Map<String, Object>) guiConfig.get("chest")).get("post_slot");
int idx = postSlots.indexOf(e.getSlot());
List<Post> posts = getPostsByPage(page, postSlots.size());
if (idx<0 || idx>=posts.size()) return;
showBookForPost(p, posts.get(idx));
}
}
@EventHandler
public void onPlayerInteract(PlayerInteractEvent e) {
Player p = e.getPlayer();
if (p.hasMetadata("uiposts_gui_type") && "book".equals(p.getMetadata("uiposts_gui_type").get(0).asString())) {
openChestGUI(p, 1);
}
}
// ===== 发帖/留言输入控制 =====
private void askForPostInput(Player p) {
p.sendMessage(SECONDARY+"请输入帖子内容,限"+POST_LIMIT+"字内,输入'取消'退出:");
awaitingPostInput.put(p.getUniqueId(), true);
}
private void askForReplyInput(Player p, int postId) {
p.sendMessage(SECONDARY+"请输入你的留言(限"+REPLY_LIMIT+"字),输入'取消'退出:");
awaitingReply.put(p.getUniqueId(), postId);
}
@EventHandler
public void onPlayerChat(org.bukkit.event.player.AsyncPlayerChatEvent e) {
UUID uid = e.getPlayer().getUniqueId();
Player player = e.getPlayer();
if (awaitingPostInput.containsKey(uid)) {
e.setCancelled(true);
String msg = e.getMessage();
if ("取消".equals(msg)) { awaitingPostInput.remove(uid); player.sendMessage(ERROR+"已取消发帖"); return; }
if (msg.trim().length()==0) {player.sendMessage(ERROR+"内容不能为空");return;}
if (msg.length()>POST_LIMIT) {
player.sendMessage(ERROR+"内容超出最大长度,将自动截断。"+SECONDARY+msg.substring(0,POST_LIMIT));
msg=msg.substring(0,POST_LIMIT);
}
savePost(player, msg);
awaitingPostInput.remove(uid);
lastPostTime.put(uid,System.currentTimeMillis());
player.sendMessage(SUCCESS+"发帖成功!");
openChestGUI(player, 1);
return;
}
if (awaitingReply.containsKey(uid)) {
e.setCancelled(true);
String msg = e.getMessage();
if ("取消".equals(msg)) { awaitingReply.remove(uid); player.sendMessage(ERROR+"已取消留言"); return; }
if (!player.hasPermission("uiposts.reply")) { player.sendMessage(ERROR+"无权留言:uiposts.reply");return; }
if (msg.trim().length()==0) {player.sendMessage(ERROR+"留言不能为空");return;}
if (msg.length()>REPLY_LIMIT) { player.sendMessage(ERROR+"内容超出限制,已截断。"); msg=msg.substring(0,REPLY_LIMIT); }
saveReply(player, awaitingReply.get(uid), msg);
awaitingReply.remove(uid);
player.sendMessage(SUCCESS+"留言成功!");
openChestGUI(player, 1);
return;
}
}
// ===== 数据库与数据结构 =====
private void savePost(Player p, String content) {
String time = getInternetTime();
try {
PreparedStatement ps = connection.prepareStatement("INSERT INTO posts(player,content,time) VALUES(?, ?, ?)");
ps.setString(1, p.getName());
ps.setString(2, content);
ps.setString(3, time);
ps.executeUpdate();
ps.close();
} catch (SQLException e) {}
}
private void saveReply(Player p, int postId, String content) {
String time = getInternetTime();
try {
PreparedStatement ps = connection.prepareStatement("INSERT INTO replies(post_id,player,content,time) VALUES(?, ?, ?, ?)");
ps.setInt(1, postId);
ps.setString(2, p.getName());
ps.setString(3, content);
ps.setString(4, time);
ps.executeUpdate();
ps.close();
} catch (SQLException e) {}
}
private List<Post> getPostsByPage(int page, int size) {
List<Post> res = new ArrayList<>();
try {
PreparedStatement ps = connection.prepareStatement("SELECT id,player,content,time FROM posts ORDER BY id DESC LIMIT ? OFFSET ?");
ps.setInt(1, size);
ps.setInt(2, (page - 1) * size);
ResultSet rs = ps.executeQuery();
while (rs.next()) {
res.add(new Post(
rs.getInt("id"),
rs.getString("player"),
rs.getString("content"),
rs.getString("time")
));
}
rs.close();ps.close();
} catch (SQLException e) {}
return res;
}
private int getTotalPages(int pageSize) {
try {
PreparedStatement ps = connection.prepareStatement("SELECT COUNT(*) FROM posts");
ResultSet rs = ps.executeQuery();
int count = rs.next() ? rs.getInt(1) : 0;
rs.close();ps.close();
return Math.max(1, (count + pageSize - 1) / pageSize);
} catch (SQLException e) {}
return 1;
}
private int countPosts(){
int count=0;
try{
PreparedStatement ps=connection.prepareStatement("SELECT count(*) FROM posts");
ResultSet rs=ps.executeQuery();
count=rs.next()?rs.getInt(1):0;
rs.close();ps.close();
}catch(Exception e){}
return count;
}
private int countPlayerPosts(String name){
int count=0;
try{
PreparedStatement ps=connection.prepareStatement("SELECT count(*) FROM posts WHERE player=?");
ps.setString(1, name);
ResultSet rs=ps.executeQuery();
count=rs.next()?rs.getInt(1):0;
rs.close();ps.close();
}catch(Exception e){}
return count;
}
private List<Reply> getReplies(int postId) {
List<Reply> res = new ArrayList<>();
try {
PreparedStatement ps = connection.prepareStatement("SELECT player,content,time FROM replies WHERE post_id=? ORDER BY id ASC");
ps.setInt(1, postId);
ResultSet rs = ps.executeQuery();
while (rs.next()) {
res.add(new Reply(
rs.getString("player"), rs.getString("content"), rs.getString("time")
));
}
rs.close();ps.close();
} catch (SQLException e) {}
return res;
}
private void showBookForPost(Player p, Post post) {
ItemStack book = new ItemStack(Material.WRITTEN_BOOK); BookMeta meta = (BookMeta) book.getItemMeta(); StringBuilder sb = new StringBuilder();
sb.append(PRIMARY+"【"+post.player+"】 "+ChatColor.GRAY+post.time+"\n");
sb.append(ChatColor.RESET).append(wrapLines(post.content,25)).append("\n");
List<Reply> replies = getReplies(post.id);
for (Reply r : replies) sb.append(ChatColor.GRAY+"- "+r.player+": "+wrapLines(r.content,20)+"\n");
sb.append("\n"+SECONDARY+"输入留言内容(聊天发送),'取消'退出");
meta.setAuthor("UIPosts");
meta.setTitle("回复帖子");
meta.setPages(Collections.singletonList(sb.toString()));
book.setItemMeta(meta);
p.getInventory().setItem(p.getInventory().getHeldItemSlot(), book);
new BukkitRunnable(){public void run(){p.openBook(book);}}.runTaskLater(this,2L);
askForReplyInput(p, post.id);
}
// -------实用方法---------
private String parse(String raw, Player p, int page, int pages) {
return raw.replace("{player}", p.getName()).replace("{page}", String.valueOf(page)).replace("{pages}", String.valueOf(pages));
}
private String getInternetTime() {
try {
URL url = new URL(TIME_API);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setConnectTimeout(1500);
conn.setReadTimeout(2000);
conn.setRequestMethod("GET");
BufferedReader in = new BufferedReader(new InputStreamReader(conn.getInputStream()));
StringBuilder json = new StringBuilder(); String line; while ((line = in.readLine()) != null) json.append(line); in.close();
String txt = json.toString(); int idx = txt.indexOf("\"datetime\":\""); if (idx != -1) { int start = idx + 12; int end = txt.indexOf("\"", start); String val = txt.substring(start, end); if (val.contains("T")) val = val.replace("T", " "); int dot = val.indexOf('.'); if (dot > 0) val = val.substring(0, dot); int plus = val.indexOf('+'); if (plus > 0) val = val.substring(0, plus); return val.trim(); }
} catch (Exception e) {}
return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new java.util.Date());
}
private static String wrapLines(String str,int lineLen){if(str==null)return"";StringBuilder sb=new StringBuilder();int i=0;while(i<str.length()){int e=Math.min(i+lineLen,str.length());sb.append(str.substring(i,e));if(e<str.length())sb.append("\n");i=e;}return sb.toString();}
// ===== 数据结构=======
private static class Post {
int id; String player; String content; String time;
Post(int id, String player, String content, String time) { this.id = id; this.player = player; this.content = content; this.time = time; }
}
private static class Reply { String player; String content; String time; Reply(String pl, String c, String t) { player = pl; content = c; time = t; } }
}