-------------------------------------------------------------------------------- -- Nanami-UI: Social UI (SocialUI.lua) -- Replaces FriendsFrame with modern rounded UI -- Tabs: Friends/Ignore, Who, Guild, Raid -------------------------------------------------------------------------------- SFrames = SFrames or {} SFrames.SocialUI = {} local SUI = SFrames.SocialUI SFramesDB = SFramesDB or {} -------------------------------------------------------------------------------- -- Theme (Pink Cat-Paw) -------------------------------------------------------------------------------- local T = SFrames.Theme:Extend({ onlineText = { 0.30, 1.0, 0.30 }, offlineText = { 0.50, 0.45, 0.48 }, }) local CLASS_COLORS = { ["WARRIOR"] = { 0.78, 0.61, 0.43 }, ["MAGE"] = { 0.41, 0.80, 0.94 }, ["ROGUE"] = { 1.00, 0.96, 0.41 }, ["DRUID"] = { 1.00, 0.49, 0.04 }, ["HUNTER"] = { 0.67, 0.83, 0.45 }, ["SHAMAN"] = { 0.14, 0.35, 1.00 }, ["PRIEST"] = { 1.00, 1.00, 1.00 }, ["WARLOCK"] = { 0.58, 0.51, 0.79 }, ["PALADIN"] = { 0.96, 0.55, 0.73 }, } -- Reverse lookup: localized class name -> English key local CLASS_NAME_TO_EN = {} local function BuildClassReverseLookup() -- Try WoW global tables first if LOCALIZED_CLASS_NAMES_MALE then for en, loc in pairs(LOCALIZED_CLASS_NAMES_MALE) do CLASS_NAME_TO_EN[loc] = en end end if LOCALIZED_CLASS_NAMES_FEMALE then for en, loc in pairs(LOCALIZED_CLASS_NAMES_FEMALE) do CLASS_NAME_TO_EN[loc] = en end end -- Hardcoded Chinese fallback local zhMap = { ["战士"] = "WARRIOR", ["法师"] = "MAGE", ["盗贼"] = "ROGUE", ["德鲁伊"] = "DRUID", ["猎人"] = "HUNTER", ["萨满祭司"] = "SHAMAN", ["牧师"] = "PRIEST", ["术士"] = "WARLOCK", ["圣骑士"] = "PALADIN", } for loc, en in pairs(zhMap) do if not CLASS_NAME_TO_EN[loc] then CLASS_NAME_TO_EN[loc] = en end end -- Also add English names themselves for en, _ in pairs(CLASS_COLORS) do CLASS_NAME_TO_EN[en] = en CLASS_NAME_TO_EN[string.lower(en)] = en end end -------------------------------------------------------------------------------- -- Chinese -> English search-term translation for SendWho -------------------------------------------------------------------------------- local WHO_ZH_TO_EN = { -- Classes ["战士"] = "Warrior", ["法师"] = "Mage", ["盗贼"] = "Rogue", ["德鲁伊"] = "Druid", ["猎人"] = "Hunter", ["萨满祭司"] = "Shaman", ["萨满"] = "Shaman", ["牧师"] = "Priest", ["术士"] = "Warlock", ["圣骑士"] = "Paladin", -- Races ["人类"] = "Human", ["矮人"] = "Dwarf", ["暗夜精灵"] = "Night Elf", ["侏儒"] = "Gnome", ["兽人"] = "Orc", ["巨魔"] = "Troll", ["亡灵"] = "Undead", ["牛头人"] = "Tauren", ["高等精灵"] = "High Elf", ["哥布林"] = "Goblin", -- Zones (Alliance) ["暴风城"] = "Stormwind", ["铁炉堡"] = "Ironforge", ["达纳苏斯"] = "Darnassus", ["艾尔文森林"] = "Elwynn Forest", ["西部荒野"] = "Westfall", ["丹莫罗"] = "Dun Morogh", ["洛克莫丹"] = "Loch Modan", ["湿地"] = "Wetlands", ["赤脊山"] = "Redridge Mountains", ["暮色森林"] = "Duskwood", ["荆棘谷"] = "Stranglethorn Vale", ["泰达希尔"] = "Teldrassil", ["黑海岸"] = "Darkshore", ["灰谷"] = "Ashenvale", ["石爪山脉"] = "Stonetalon Mountains", -- Zones (Horde) ["奥格瑞玛"] = "Orgrimmar", ["雷霆崖"] = "Thunder Bluff", ["幽暗城"] = "Undercity", ["杜隆塔尔"] = "Durotar", ["莫高雷"] = "Mulgore", ["贫瘠之地"] = "The Barrens", ["银松森林"] = "Silverpine Forest", ["提瑞斯法林地"] = "Tirisfal Glades", ["希尔斯布莱德丘陵"] = "Hillsbrad Foothills", -- Zones (Contested / High-level) ["塔纳利斯"] = "Tanaris", ["菲拉斯"] = "Feralas", ["凄凉之地"] = "Desolace", ["尘泥沼泽"] = "Dustwallow Marsh", ["千针石林"] = "Thousand Needles", ["辛特兰"] = "The Hinterlands", ["阿拉希高地"] = "Arathi Highlands", ["荒芜之地"] = "Badlands", ["灼热峡谷"] = "Searing Gorge", ["燃烧平原"] = "Burning Steppes", ["西瘟疫之地"] = "Western Plaguelands", ["东瘟疫之地"] = "Eastern Plaguelands", ["费伍德森林"] = "Felwood", ["冬泉谷"] = "Winterspring", ["安戈洛环形山"] = "Un'Goro Crater", ["希利苏斯"] = "Silithus", ["艾萨拉"] = "Azshara", ["诅咒之地"] = "Blasted Lands", ["逆风小径"] = "Deadwind Pass", ["悲伤沼泽"] = "Swamp of Sorrows", -- Dungeons / Raids ["熔火之心"] = "Molten Core", ["黑翼之巢"] = "Blackwing Lair", ["奥妮克希亚的巢穴"] = "Onyxia's Lair", ["祖尔格拉布"] = "Zul'Gurub", ["安其拉"] = "Ahn'Qiraj", ["纳克萨玛斯"] = "Naxxramas", ["黑石深渊"] = "Blackrock Depths", ["黑石塔"] = "Blackrock Spire", ["斯坦索姆"] = "Stratholme", ["通灵学院"] = "Scholomance", ["厄运之槌"] = "Dire Maul", ["玛拉顿"] = "Maraudon", ["祖尔法拉克"] = "Zul'Farrak", } local function TranslateWhoQuery(text) if not text then return "" end for zh, en in pairs(WHO_ZH_TO_EN) do text = string.gsub(text, zh, en) end return text end local CLASS_ICON_PATH = "Interface\\AddOns\\Nanami-UI\\img\\UI-Classes-Circles" local CLASS_ICON_TCOORDS = { ["WARRIOR"] = { 0, 0.25, 0, 0.25 }, ["MAGE"] = { 0.25, 0.49609375, 0, 0.25 }, ["ROGUE"] = { 0.49609375, 0.7421875, 0, 0.25 }, ["DRUID"] = { 0.7421875, 0.98828125, 0, 0.25 }, ["HUNTER"] = { 0, 0.25, 0.25, 0.5 }, ["SHAMAN"] = { 0.25, 0.49609375, 0.25, 0.5 }, ["PRIEST"] = { 0.49609375, 0.7421875, 0.25, 0.5 }, ["WARLOCK"] = { 0.7421875, 0.98828125, 0.25, 0.5 }, ["PALADIN"] = { 0, 0.25, 0.5, 0.75 }, } -------------------------------------------------------------------------------- -- Layout -------------------------------------------------------------------------------- local FRAME_W = 380 local FRAME_H = 455 local HEADER_H = 30 local TAB_BAR_H = 26 local SIDE_PAD = 10 local CONTENT_W = FRAME_W - SIDE_PAD * 2 local ROW_H = 22 local BOTTOM_H = 30 local SCROLL_STEP = 22 -------------------------------------------------------------------------------- -- State -------------------------------------------------------------------------------- local MainFrame = nil local mainTabs = {} local pages = {} local currentMainTab = 1 local initialized = false local friendRows = {} local ignoreRows = {} local whoRows = {} local guildRows = {} local raidSlots = {} local selectedFriend = nil local selectedWho = nil local selectedGuild = nil local friendSubTab = "friends" local guildViewMode = "player" local guildHideOffline = false local guildSortField = "name" local guildSortAsc = true local friendSearchText = "" local guildSearchText = "" local origShowFriendsAPI = nil local widgetId = 0 local function NextName(p) widgetId = widgetId + 1 return "SFramesSocial" .. (p or "") .. tostring(widgetId) end -------------------------------------------------------------------------------- -- Helpers -------------------------------------------------------------------------------- local function GetFont() if SFrames and SFrames.GetFont then return SFrames:GetFont() end return "Fonts\\ARIALN.TTF" end local function SetRoundBackdrop(frame, bgColor, borderColor) frame:SetBackdrop({ bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", tile = true, tileSize = 16, edgeSize = 14, insets = { left = 3, right = 3, top = 3, bottom = 3 }, }) local bg = bgColor or T.panelBg local bd = borderColor or T.panelBorder frame:SetBackdropColor(bg[1], bg[2], bg[3], bg[4] or 1) frame:SetBackdropBorderColor(bd[1], bd[2], bd[3], bd[4] or 1) end local function SetPixelBackdrop(frame, bgColor, borderColor) frame:SetBackdrop({ bgFile = "Interface\\Buttons\\WHITE8X8", edgeFile = "Interface\\Buttons\\WHITE8X8", tile = false, tileSize = 0, edgeSize = 1, insets = { left = 1, right = 1, top = 1, bottom = 1 }, }) local bg = bgColor or T.slotBg local bd = borderColor or T.slotBorder frame:SetBackdropColor(bg[1], bg[2], bg[3], bg[4] or 1) frame:SetBackdropBorderColor(bd[1], bd[2], bd[3], bd[4] or 1) end local function SetRowNormal(frame) SetPixelBackdrop(frame, T.rowNormal, T.rowNormalBd) end local function AddSelHighlight(row) local sb = row:CreateTexture(nil, "ARTWORK") sb:SetTexture("Interface\\Buttons\\WHITE8X8") sb:SetAllPoints(row) sb:SetVertexColor(T.slotSelected[1], T.slotSelected[2], T.slotSelected[3], 0.35) sb:Hide(); row._selBg = sb local sg = row:CreateTexture(nil, "ARTWORK") sg:SetTexture("Interface\\Buttons\\WHITE8X8") sg:SetWidth(4) sg:SetPoint("TOPLEFT", row, "TOPLEFT", 0, 0) sg:SetPoint("BOTTOMLEFT", row, "BOTTOMLEFT", 0, 0) sg:SetVertexColor(1, 0.65, 0.85, 1) sg:Hide(); row._selGlow = sg local st = row:CreateTexture(nil, "OVERLAY") st:SetTexture("Interface\\Buttons\\WHITE8X8") st:SetHeight(1) st:SetPoint("TOPLEFT", row, "TOPLEFT", 0, 0) st:SetPoint("TOPRIGHT", row, "TOPRIGHT", 0, 0) st:SetVertexColor(T.slotSelected[1], T.slotSelected[2], T.slotSelected[3], 0.8) st:Hide(); row._selTop = st local sbo = row:CreateTexture(nil, "OVERLAY") sbo:SetTexture("Interface\\Buttons\\WHITE8X8") sbo:SetHeight(1) sbo:SetPoint("BOTTOMLEFT", row, "BOTTOMLEFT", 0, 0) sbo:SetPoint("BOTTOMRIGHT", row, "BOTTOMRIGHT", 0, 0) sbo:SetVertexColor(T.slotSelected[1], T.slotSelected[2], T.slotSelected[3], 0.8) sbo:Hide(); row._selBot = sbo end local function ShowSelHighlight(row) if row._selBg then row._selBg:Show() end if row._selGlow then row._selGlow:Show() end if row._selTop then row._selTop:Show() end if row._selBot then row._selBot:Show() end end local function HideSelHighlight(row) if row._selBg then row._selBg:Hide() end if row._selGlow then row._selGlow:Hide() end if row._selTop then row._selTop:Hide() end if row._selBot then row._selBot:Hide() end end local function CreateShadow(parent) local s = CreateFrame("Frame", nil, parent) s:SetPoint("TOPLEFT", parent, "TOPLEFT", -4, 4) s:SetPoint("BOTTOMRIGHT", parent, "BOTTOMRIGHT", 4, -4) s:SetFrameLevel(math.max(parent:GetFrameLevel() - 1, 0)) s:SetBackdrop({ bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", tile = true, tileSize = 16, edgeSize = 16, insets = { left = 4, right = 4, top = 4, bottom = 4 }, }) s:SetBackdropColor(0, 0, 0, 0.6) s:SetBackdropBorderColor(0, 0, 0, 0.45) return s end local function MakeFS(parent, size, justifyH, color) local fs = parent:CreateFontString(nil, "OVERLAY") fs:SetFont(GetFont(), size or 11, "OUTLINE") fs:SetJustifyH(justifyH or "LEFT") local c = color or T.nameText fs:SetTextColor(c[1], c[2], c[3]) return fs end local function MakeSep(parent, y) local sep = parent:CreateTexture(nil, "ARTWORK") sep:SetTexture("Interface\\Buttons\\WHITE8X8") sep:SetVertexColor(T.sepColor[1], T.sepColor[2], T.sepColor[3], T.sepColor[4]) sep:SetHeight(1) sep:SetPoint("TOPLEFT", parent, "TOPLEFT", 0, y) sep:SetPoint("TOPRIGHT", parent, "TOPRIGHT", 0, y) return sep end local function MakeButton(parent, text, w, h) local btn = CreateFrame("Button", NextName("Btn"), parent) btn:SetWidth(w or 80) btn:SetHeight(h or 22) SetRoundBackdrop(btn, T.btnBg, T.btnBorder) local fs = MakeFS(btn, 11, "CENTER", T.btnText) fs:SetPoint("CENTER", 0, 0) fs:SetText(text or "") btn.text = fs btn:SetScript("OnEnter", function() SetRoundBackdrop(this, T.btnHoverBg, T.tabActiveBorder) this.text:SetTextColor(T.btnActiveText[1], T.btnActiveText[2], T.btnActiveText[3]) end) btn:SetScript("OnLeave", function() SetRoundBackdrop(this, T.btnBg, T.btnBorder) this.text:SetTextColor(T.btnText[1], T.btnText[2], T.btnText[3]) end) return btn end local function MakeEditBox(parent, w, h) local box = CreateFrame("EditBox", NextName("Edit"), parent) box:SetWidth(w or 200) box:SetHeight(h or 22) box:SetFont(GetFont(), 11, "OUTLINE") box:SetTextColor(T.nameText[1], T.nameText[2], T.nameText[3]) box:SetAutoFocus(false) box:SetMaxLetters(256) SetPixelBackdrop(box, T.inputBg, T.inputBorder) box:SetTextInsets(6, 6, 0, 0) box:SetScript("OnEscapePressed", function() this:ClearFocus() end) return box end local customPopup = nil local function ShowCustomPopup(title, hasInput, onAccept, onCancel) if not customPopup then local f = CreateFrame("Frame", "SFramesSocialPopup", UIParent) f:SetWidth(280) f:SetHeight(120) f:SetPoint("CENTER", UIParent, "CENTER", 0, 80) f:SetFrameStrata("DIALOG") f:SetFrameLevel(100) SetRoundBackdrop(f, T.panelBg, T.panelBorder) CreateShadow(f) f:EnableMouse(true) f:SetMovable(true) f:RegisterForDrag("LeftButton") f:SetScript("OnDragStart", function() this:StartMoving() end) f:SetScript("OnDragStop", function() this:StopMovingOrSizing() end) local titleFS = MakeFS(f, 12, "CENTER", T.headerText) titleFS:SetPoint("TOP", f, "TOP", 0, -10) f.titleFS = titleFS local editBox = CreateFrame("EditBox", "SFramesSocialPopupEdit", f) editBox:SetWidth(230) editBox:SetHeight(24) editBox:SetPoint("TOP", titleFS, "BOTTOM", 0, -10) editBox:SetFont(GetFont(), 11, "OUTLINE") editBox:SetTextColor(T.nameText[1], T.nameText[2], T.nameText[3]) editBox:SetAutoFocus(false) editBox:SetMaxLetters(64) SetRoundBackdrop(editBox, T.inputBg, T.inputBorder) editBox:SetTextInsets(8, 8, 4, 4) editBox:SetScript("OnEscapePressed", function() this:ClearFocus() f:Hide() end) f.editBox = editBox local acceptBtn = MakeButton(f, "确定", 100, 24) acceptBtn:SetPoint("BOTTOMRIGHT", f, "BOTTOM", -4, 10) f.acceptBtn = acceptBtn local cancelBtn = MakeButton(f, "取消", 100, 24) cancelBtn:SetPoint("BOTTOMLEFT", f, "BOTTOM", 4, 10) cancelBtn:SetScript("OnClick", function() f:Hide() end) f.cancelBtn = cancelBtn f:Hide() customPopup = f end customPopup.titleFS:SetText(title or "") customPopup.editBox:SetText("") if hasInput then customPopup.editBox:Show() customPopup:SetHeight(120) else customPopup.editBox:Hide() customPopup:SetHeight(80) end customPopup.acceptBtn:SetScript("OnClick", function() local text = customPopup.editBox:GetText() customPopup:Hide() if onAccept then onAccept(text) end end) customPopup.editBox:SetScript("OnEnterPressed", function() local text = customPopup.editBox:GetText() customPopup:Hide() if onAccept then onAccept(text) end end) customPopup:Show() if hasInput then customPopup.editBox:SetFocus() end end local function GetClassEN(localizedName) if not localizedName then return nil end return CLASS_NAME_TO_EN[localizedName] or CLASS_NAME_TO_EN[string.upper(localizedName)] end local function GetClassColor(classEN) if not classEN then return T.nameText end local c = CLASS_COLORS[string.upper(classEN)] if c then return c end return T.nameText end local function CreateClassIcon(parent, size) local sz = size or 16 local icon = parent:CreateTexture(nil, "OVERLAY") icon:SetTexture(CLASS_ICON_PATH) icon:SetWidth(sz) icon:SetHeight(sz) return icon end local function SetClassIcon(icon, classEN) if not classEN then icon:Hide() return end local upper = string.upper(classEN) local coords = CLASS_ICON_TCOORDS[upper] if coords then icon:SetTexCoord(coords[1], coords[2], coords[3], coords[4]) icon:SetVertexColor(1, 1, 1) icon:Show() else icon:Hide() end end -------------------------------------------------------------------------------- -- Scroll Frame Helper -------------------------------------------------------------------------------- local function CreateScrollArea(parent, w, h) local container = CreateFrame("Frame", nil, parent) container:SetWidth(w) container:SetHeight(h) local scrollFrame = CreateFrame("ScrollFrame", NextName("Scroll"), container) scrollFrame:SetWidth(w) scrollFrame:SetHeight(h) scrollFrame:SetAllPoints(container) local child = CreateFrame("Frame", nil, scrollFrame) child:SetWidth(w) child:SetHeight(1) scrollFrame:SetScrollChild(child) local offset = 0 local maxOffset = 0 container.SetContentHeight = function(self, ch) child:SetHeight(ch) maxOffset = math.max(0, ch - h) if offset > maxOffset then offset = maxOffset scrollFrame:SetVerticalScroll(offset) end end container.Reset = function(self) offset = 0 scrollFrame:SetVerticalScroll(0) end local function DoScroll(delta) offset = offset - delta * SCROLL_STEP if offset < 0 then offset = 0 end if offset > maxOffset then offset = maxOffset end scrollFrame:SetVerticalScroll(offset) end scrollFrame:EnableMouseWheel(true) scrollFrame:SetScript("OnMouseWheel", function() DoScroll(arg1 or 0) end) container:EnableMouseWheel(true) container:SetScript("OnMouseWheel", function() DoScroll(arg1 or 0) end) container.child = child container.scrollFrame = scrollFrame return container end local function WhoDebug(msg) if DEFAULT_CHAT_FRAME then DEFAULT_CHAT_FRAME:AddMessage("|cff00ffcc[Who调试]|r " .. msg) end end -------------------------------------------------------------------------------- -- Hide Blizzard FriendsFrame -------------------------------------------------------------------------------- local origFriendsFrameShow local function HideBlizzardFriends() if not FriendsFrame then return end origFriendsFrameShow = FriendsFrame.Show FriendsFrame:Hide() FriendsFrame:SetAlpha(0) FriendsFrame:EnableMouse(false) FriendsFrame:ClearAllPoints() FriendsFrame:SetPoint("TOPLEFT", UIParent, "BOTTOMRIGHT", 2000, 2000) FriendsFrame:UnregisterAllEvents() FriendsFrame.Show = function() end if UIPanelWindows then UIPanelWindows["FriendsFrame"] = nil end for i = table.getn(UISpecialFrames), 1, -1 do if UISpecialFrames[i] == "FriendsFrame" then table.remove(UISpecialFrames, i) end end if SetWhoToUI then local origSetWhoToUI = SetWhoToUI SetWhoToUI = function(flag) if flag ~= 1 then WhoDebug("拦截 SetWhoToUI(" .. tostring(flag) .. ") -> 强制为1") end origSetWhoToUI(1) end end end -------------------------------------------------------------------------------- -- Who query helper -------------------------------------------------------------------------------- local whoQueryPending = false local whoTimeoutFrame = nil local function DoSendWho(query) if whoQueryPending and whoTimeoutFrame then WhoDebug("取消上次挂起的查询") whoQueryPending = false whoTimeoutFrame:SetScript("OnUpdate", nil) end WhoDebug("发送查询: \"" .. (query or "") .. "\"") if SetWhoToUI then SetWhoToUI(1) end whoQueryPending = true SendWho(query or "") WhoDebug("SendWho() 已调用, 等待 WHO_LIST_UPDATE...") if not whoTimeoutFrame then whoTimeoutFrame = CreateFrame("Frame", nil, UIParent) end whoTimeoutFrame.elapsed = 0 whoTimeoutFrame:SetScript("OnUpdate", function() this.elapsed = (this.elapsed or 0) + (arg1 or 0.016) if not whoQueryPending then this:SetScript("OnUpdate", nil) return end if this.elapsed >= 6 then local n = GetNumWhoResults() WhoDebug("超时! 6秒未收到事件, 强制刷新, 当前结果=" .. tostring(n)) whoQueryPending = false this:SetScript("OnUpdate", nil) SUI:UpdateWhoList() end end) end -------------------------------------------------------------------------------- -- Tab 1: Friends / Ignore -------------------------------------------------------------------------------- local function BuildFriendsPage(page) local subTabFrame = CreateFrame("Frame", nil, page) subTabFrame:SetHeight(16) subTabFrame:SetPoint("TOPLEFT", page, "TOPLEFT", 0, 0) subTabFrame:SetPoint("TOPRIGHT", page, "TOPRIGHT", 0, 0) local friendsSubBtn = CreateFrame("Button", NextName("SubTab"), subTabFrame) friendsSubBtn:SetWidth(50) friendsSubBtn:SetHeight(16) friendsSubBtn:SetPoint("LEFT", subTabFrame, "LEFT", 0, 0) local fsBtnText = MakeFS(friendsSubBtn, 10, "CENTER", T.tabActiveText) fsBtnText:SetPoint("CENTER", 0, 0) fsBtnText:SetText("好友") friendsSubBtn.text = fsBtnText friendsSubBtn:SetScript("OnEnter", function() if friendSubTab ~= "friends" then this.text:SetTextColor(1, 1, 1) end end) friendsSubBtn:SetScript("OnLeave", function() if friendSubTab ~= "friends" then this.text:SetTextColor(T.dimText[1], T.dimText[2], T.dimText[3]) end end) page.friendsSubBtn = friendsSubBtn local ignoreSubBtn = CreateFrame("Button", NextName("SubTab"), subTabFrame) ignoreSubBtn:SetWidth(50) ignoreSubBtn:SetHeight(16) ignoreSubBtn:SetPoint("LEFT", friendsSubBtn, "RIGHT", 6, 0) local igBtnText = MakeFS(ignoreSubBtn, 10, "CENTER", T.tabText) igBtnText:SetPoint("CENTER", 0, 0) igBtnText:SetText("屏蔽") ignoreSubBtn.text = igBtnText ignoreSubBtn:SetScript("OnEnter", function() if friendSubTab ~= "ignore" then this.text:SetTextColor(1, 1, 1) end end) ignoreSubBtn:SetScript("OnLeave", function() if friendSubTab ~= "ignore" then this.text:SetTextColor(T.dimText[1], T.dimText[2], T.dimText[3]) end end) page.ignoreSubBtn = ignoreSubBtn -- Underline indicator for active sub-tab local subIndicator = friendsSubBtn:CreateTexture(nil, "OVERLAY") subIndicator:SetHeight(2) subIndicator:SetPoint("BOTTOMLEFT", friendsSubBtn, "BOTTOMLEFT", 4, 0) subIndicator:SetPoint("BOTTOMRIGHT", friendsSubBtn, "BOTTOMRIGHT", -4, 0) subIndicator:SetTexture(1, 1, 1, 1) subIndicator:SetVertexColor(T.tabActiveBorder[1], T.tabActiveBorder[2], T.tabActiveBorder[3], 0.8) page.subIndicator = subIndicator -- Friend search bar local friendSearchBar = CreateFrame("Frame", nil, page) friendSearchBar:SetHeight(20) friendSearchBar:SetPoint("TOPLEFT", page, "TOPLEFT", 0, -18) friendSearchBar:SetPoint("TOPRIGHT", page, "TOPRIGHT", 0, -18) page.friendSearchBar = friendSearchBar local searchLabel = MakeFS(friendSearchBar, 9, "LEFT", T.dimText) searchLabel:SetPoint("LEFT", friendSearchBar, "LEFT", 2, 0) searchLabel:SetText("搜索:") local friendSearchBox = MakeEditBox(friendSearchBar, CONTENT_W - 36, 18) friendSearchBox:SetPoint("LEFT", searchLabel, "RIGHT", 4, 0) friendSearchBox:SetScript("OnTextChanged", function() friendSearchText = this:GetText() or "" SUI:UpdateFriendsList() end) friendSearchBox:SetScript("OnEnterPressed", function() this:ClearFocus() end) friendSearchBox:SetScript("OnEscapePressed", function() this:SetText("") this:ClearFocus() end) page.friendSearchBox = friendSearchBox -- Friends list local friendsArea = CreateFrame("Frame", nil, page) friendsArea:SetPoint("TOPLEFT", page, "TOPLEFT", 0, -40) friendsArea:SetPoint("BOTTOMRIGHT", page, "BOTTOMRIGHT", 0, BOTTOM_H) page.friendsArea = friendsArea local fScroll = CreateScrollArea(friendsArea, CONTENT_W, FRAME_H - HEADER_H - TAB_BAR_H - 40 - BOTTOM_H - 16) fScroll:SetPoint("TOPLEFT", friendsArea, "TOPLEFT", 0, 0) page.fScroll = fScroll local MAX_FRIEND_ROWS = 50 for idx = 1, MAX_FRIEND_ROWS do local rowIdx = idx local row = CreateFrame("Button", NextName("FR"), fScroll.child) row:SetWidth(CONTENT_W - 4) row:SetHeight(ROW_H) row:SetPoint("TOPLEFT", fScroll.child, "TOPLEFT", 2, -((rowIdx - 1) * ROW_H)) row.rowIndex = rowIdx SetRowNormal(row) local classIcon = CreateClassIcon(row, 16) classIcon:SetPoint("LEFT", row, "LEFT", 4, 0) row.classIcon = classIcon local nameFS = MakeFS(row, 11, "LEFT", T.nameText) nameFS:SetPoint("LEFT", classIcon, "RIGHT", 4, 0) nameFS:SetWidth(120) row.nameFS = nameFS local infoFS = MakeFS(row, 9, "LEFT", T.dimText) infoFS:SetPoint("LEFT", nameFS, "RIGHT", 4, 0) infoFS:SetPoint("RIGHT", row, "RIGHT", -4, 0) row.infoFS = infoFS AddSelHighlight(row) row:RegisterForClicks("LeftButtonUp", "RightButtonUp") row:SetScript("OnEnter", function() SetPixelBackdrop(this, T.slotHover, T.slotSelected) end) row:SetScript("OnLeave", function() if selectedFriend and selectedFriend == this.friendDataIndex then SetPixelBackdrop(this, T.tabActiveBg, T.tabActiveBorder) else SetRowNormal(this) end end) row:SetScript("OnClick", function() selectedFriend = this.friendDataIndex if arg1 == "RightButton" and this.friendDataIndex then SUI:ShowFriendRowMenu(this) return end SUI:UpdateFriendsList() end) row:Hide() friendRows[idx] = row end -- Ignore list local ignoreArea = CreateFrame("Frame", nil, page) ignoreArea:SetPoint("TOPLEFT", page, "TOPLEFT", 0, -20) ignoreArea:SetPoint("BOTTOMRIGHT", page, "BOTTOMRIGHT", 0, BOTTOM_H) ignoreArea:Hide() page.ignoreArea = ignoreArea local iScroll = CreateScrollArea(ignoreArea, CONTENT_W, FRAME_H - HEADER_H - TAB_BAR_H - 20 - BOTTOM_H - 16) iScroll:SetPoint("TOPLEFT", ignoreArea, "TOPLEFT", 0, 0) page.iScroll = iScroll local MAX_IGNORE_ROWS = 50 for idx = 1, MAX_IGNORE_ROWS do local rowIdx = idx local row = CreateFrame("Button", NextName("IR"), iScroll.child) row:SetWidth(CONTENT_W - 4) row:SetHeight(20) row:SetPoint("TOPLEFT", iScroll.child, "TOPLEFT", 2, -((rowIdx - 1) * 20)) row.rowIndex = rowIdx SetRowNormal(row) local nameFS = MakeFS(row, 11, "LEFT", T.nameText) nameFS:SetPoint("LEFT", row, "LEFT", 8, 0) row.nameFS = nameFS row:SetScript("OnEnter", function() SetPixelBackdrop(this, T.slotHover, T.slotSelected) end) row:SetScript("OnLeave", function() SetRowNormal(this) end) row:Hide() ignoreRows[idx] = row end -- Bottom buttons local btnBar = CreateFrame("Frame", nil, page) btnBar:SetHeight(BOTTOM_H) btnBar:SetPoint("BOTTOMLEFT", page, "BOTTOMLEFT", 0, 0) btnBar:SetPoint("BOTTOMRIGHT", page, "BOTTOMRIGHT", 0, 0) page.btnBar = btnBar local addBtn = MakeButton(btnBar, "添加好友", 80, 22) addBtn:SetPoint("BOTTOMLEFT", btnBar, "BOTTOMLEFT", 0, 2) addBtn:SetScript("OnClick", function() if friendSubTab == "friends" then ShowCustomPopup("添加好友", true, function(name) if name and name ~= "" then AddFriend(name) end end) else ShowCustomPopup("添加屏蔽", true, function(name) if name and name ~= "" then AddIgnore(name) end end) end end) local removeBtn = MakeButton(btnBar, "删除好友", 80, 22) removeBtn:SetPoint("LEFT", addBtn, "RIGHT", 4, 0) removeBtn:SetScript("OnClick", function() if selectedFriend then RemoveFriend(selectedFriend) end end) local msgBtn = MakeButton(btnBar, "发送信息", 80, 22) msgBtn:SetPoint("LEFT", removeBtn, "RIGHT", 4, 0) msgBtn:SetScript("OnClick", function() if selectedFriend then local name = GetFriendInfo(selectedFriend) if name then ChatFrame_SendTell(name) end end end) local inviteBtn = MakeButton(btnBar, "组队邀请", 80, 22) inviteBtn:SetPoint("LEFT", msgBtn, "RIGHT", 4, 0) inviteBtn:SetScript("OnClick", function() if selectedFriend then local name = GetFriendInfo(selectedFriend) if name then InviteByName(name) end end end) page.addBtn = addBtn page.removeBtn = removeBtn page.msgBtn = msgBtn page.inviteBtn = inviteBtn friendsSubBtn:SetScript("OnClick", function() friendSubTab = "friends" SUI:UpdateFriendsPage() end) ignoreSubBtn:SetScript("OnClick", function() friendSubTab = "ignore" SUI:UpdateFriendsPage() end) end function SUI:UpdateFriendsPage() local page = pages[1] if not page then return end if friendSubTab == "friends" then page.friendsArea:Show() page.friendSearchBar:Show() page.ignoreArea:Hide() page.friendsSubBtn.text:SetTextColor(T.tabActiveText[1], T.tabActiveText[2], T.tabActiveText[3]) page.ignoreSubBtn.text:SetTextColor(T.dimText[1], T.dimText[2], T.dimText[3]) page.subIndicator:ClearAllPoints() page.subIndicator:SetPoint("BOTTOMLEFT", page.friendsSubBtn, "BOTTOMLEFT", 4, 0) page.subIndicator:SetPoint("BOTTOMRIGHT", page.friendsSubBtn, "BOTTOMRIGHT", -4, 0) page.subIndicator:Show() page.addBtn.text:SetText("添加好友") page.removeBtn.text:SetText("删除好友") self:UpdateFriendsList() else page.friendsArea:Hide() page.friendSearchBar:Hide() page.ignoreArea:Show() page.ignoreSubBtn.text:SetTextColor(T.tabActiveText[1], T.tabActiveText[2], T.tabActiveText[3]) page.friendsSubBtn.text:SetTextColor(T.dimText[1], T.dimText[2], T.dimText[3]) page.subIndicator:ClearAllPoints() page.subIndicator:SetPoint("BOTTOMLEFT", page.ignoreSubBtn, "BOTTOMLEFT", 4, 0) page.subIndicator:SetPoint("BOTTOMRIGHT", page.ignoreSubBtn, "BOTTOMRIGHT", -4, 0) page.subIndicator:Show() page.addBtn.text:SetText("添加屏蔽") page.removeBtn.text:SetText("取消屏蔽") self:UpdateIgnoreList() end end function SUI:UpdateFriendsList() local numFriends = GetNumFriends() local totalH = 0 local rowIdx = 0 local searchLower = string.lower(friendSearchText or "") local hasSearch = searchLower ~= "" for i = 1, numFriends do local name, level, class, area, connected, status = GetFriendInfo(i) if name then local match = true if hasSearch then match = string.find(string.lower(name), searchLower, 1, true) if not match and class then match = string.find(string.lower(class), searchLower, 1, true) end if not match and area then match = string.find(string.lower(area), searchLower, 1, true) end end if match then rowIdx = rowIdx + 1 if rowIdx > 50 then break end local row = friendRows[rowIdx] if row then row.nameFS:SetText(name) row.friendDataIndex = i local classEN = GetClassEN(class) if connected then local cc = classEN and GetClassColor(classEN) or T.onlineText row.nameFS:SetTextColor(cc[1], cc[2], cc[3]) local info = "" if level and level > 0 then info = "等级" .. level end if class then info = info .. " " .. class end if area then info = info .. " - " .. area end row.infoFS:SetText(info) row.infoFS:SetTextColor(T.bodyText[1], T.bodyText[2], T.bodyText[3]) else row.nameFS:SetTextColor(T.offlineText[1], T.offlineText[2], T.offlineText[3]) row.infoFS:SetText("离线") row.infoFS:SetTextColor(T.offlineText[1], T.offlineText[2], T.offlineText[3]) end SetClassIcon(row.classIcon, classEN) if selectedFriend == i then SetPixelBackdrop(row, T.tabActiveBg, T.tabActiveBorder) ShowSelHighlight(row) row.nameFS:SetTextColor(1, 1, 1) else SetRowNormal(row) HideSelHighlight(row) end row:Show() totalH = totalH + ROW_H end end end end for i = rowIdx + 1, 50 do if friendRows[i] then friendRows[i]:Hide() end end if pages[1] and pages[1].fScroll then pages[1].fScroll:SetContentHeight(totalH + 4) end end function SUI:UpdateIgnoreList() local numIgnores = GetNumIgnores() local totalH = 0 for i = 1, 50 do local row = ignoreRows[i] if not row then break end if i <= numIgnores then local name = GetIgnoreName(i) if name then row.nameFS:SetText(name) row:Show() totalH = totalH + 20 else row:Hide() end else row:Hide() end end if pages[1] and pages[1].iScroll then pages[1].iScroll:SetContentHeight(totalH + 4) end end function SUI:ShowFriendRowMenu(row) local di = row.friendDataIndex if not di then return end local name, level, class, area, connected, status = GetFriendInfo(di) if not name then return end if not SUI.friendDropDown then SUI.friendDropDown = CreateFrame("Frame", "SFramesSocialFriendDD", UIParent, "UIDropDownMenuTemplate") SUI.friendDropDown.displayMode = "MENU" SUI.friendDropDown.initialize = function() local fd = SUI.friendDropDown local fName = fd.friendName local fOnline = fd.friendOnline if not fName then return end local info = {} info.text = fName info.isTitle = 1 info.notCheckable = 1 UIDropDownMenu_AddButton(info) if fOnline then info = {} info.text = "密语" info.notCheckable = 1 info.func = function() ChatFrame_SendTell(fName) end UIDropDownMenu_AddButton(info) info = {} info.text = "邀请组队" info.notCheckable = 1 info.func = function() InviteByName(fName) end UIDropDownMenu_AddButton(info) info = {} info.text = "观察" info.notCheckable = 1 info.func = function() if TargetByName then TargetByName(fName) end if UnitExists("target") and UnitName("target") == fName then InspectUnit("target") end end UIDropDownMenu_AddButton(info) end info = {} info.text = "删除好友" info.notCheckable = 1 info.func = function() local idx = fd.friendIdx if idx then RemoveFriend(idx) end end UIDropDownMenu_AddButton(info) info = {} info.text = "取消" info.notCheckable = 1 UIDropDownMenu_AddButton(info) end end SUI.friendDropDown.friendName = name SUI.friendDropDown.friendOnline = connected SUI.friendDropDown.friendIdx = di ToggleDropDownMenu(1, nil, SUI.friendDropDown, "cursor") end -------------------------------------------------------------------------------- -- Tab 2: Who -------------------------------------------------------------------------------- local function BuildWhoPage(page) local searchBar = CreateFrame("Frame", nil, page) searchBar:SetHeight(26) searchBar:SetPoint("TOPLEFT", page, "TOPLEFT", 0, 0) searchBar:SetPoint("TOPRIGHT", page, "TOPRIGHT", 0, 0) local editBox = MakeEditBox(searchBar, CONTENT_W - 110, 22) editBox:SetPoint("LEFT", searchBar, "LEFT", 0, 0) local placeholder = editBox:CreateFontString(nil, "ARTWORK") placeholder:SetFont(GetFont(), 10, "OUTLINE") placeholder:SetPoint("LEFT", editBox, "LEFT", 6, 0) placeholder:SetTextColor(T.dimText[1], T.dimText[2], T.dimText[3], 0.6) placeholder:SetText("名称/等级/职业/种族/区域") editBox.placeholder = placeholder editBox:SetScript("OnTextChanged", function() local text = this:GetText() if text and text ~= "" then this.placeholder:Hide() else this.placeholder:Show() end end) editBox:SetScript("OnEditFocusGained", function() if this:GetText() == "" then this.placeholder:Show() end end) editBox:SetScript("OnEditFocusLost", function() if this:GetText() == "" then this.placeholder:Show() end end) editBox:SetScript("OnEnterPressed", function() local text = this:GetText() or "" SUI:ClearWhoList() DoSendWho(text) this:ClearFocus() end) page.editBox = editBox local clearBtn = MakeButton(searchBar, "X", 28, 22) clearBtn:SetPoint("LEFT", editBox, "RIGHT", 2, 0) clearBtn:SetScript("OnClick", function() if page.editBox then page.editBox:SetText("") page.editBox:SetFocus() end end) local searchBtn = MakeButton(searchBar, "搜索", 64, 22) searchBtn:SetPoint("LEFT", clearBtn, "RIGHT", 2, 0) searchBtn:SetScript("OnClick", function() local text = page.editBox:GetText() or "" SUI:ClearWhoList() DoSendWho(text) end) -- Column headers local colFrame = CreateFrame("Frame", nil, page) colFrame:SetHeight(18) colFrame:SetPoint("TOPLEFT", page, "TOPLEFT", 0, -28) colFrame:SetPoint("TOPRIGHT", page, "TOPRIGHT", 0, -28) local cols = { { "名称", 0.30 }, { "等级", 0.12 }, { "职业", 0.18 }, { "区域", 0.40 } } local cx = 0 for _, col in ipairs(cols) do local fs = MakeFS(colFrame, 10, "LEFT", T.colHeader) fs:SetPoint("TOPLEFT", colFrame, "TOPLEFT", cx, 0) fs:SetWidth(CONTENT_W * col[2]) fs:SetText(col[1]) cx = cx + CONTENT_W * col[2] end MakeSep(page, -46) -- Results scroll (leave 16px above btnBar for totalFS) local listArea = CreateFrame("Frame", nil, page) listArea:SetPoint("TOPLEFT", page, "TOPLEFT", 0, -48) listArea:SetPoint("BOTTOMRIGHT", page, "BOTTOMRIGHT", 0, BOTTOM_H + 18) local scrollH = FRAME_H - HEADER_H - TAB_BAR_H - 48 - BOTTOM_H - 18 local wScroll = CreateScrollArea(listArea, CONTENT_W, scrollH) wScroll:SetPoint("TOPLEFT", listArea, "TOPLEFT", 0, 0) page.wScroll = wScroll local MAX_WHO_ROWS = 50 for idx = 1, MAX_WHO_ROWS do local rowIdx = idx local row = CreateFrame("Button", NextName("WR"), wScroll.child) row:SetWidth(CONTENT_W - 4) row:SetHeight(20) row:SetPoint("TOPLEFT", wScroll.child, "TOPLEFT", 2, -((rowIdx - 1) * 20)) row.rowIndex = rowIdx SetRowNormal(row) local nameFS = MakeFS(row, 10, "LEFT", T.nameText) nameFS:SetPoint("LEFT", row, "LEFT", 4, 0) nameFS:SetWidth(CONTENT_W * 0.30 - 8) row.nameFS = nameFS local lvlFS = MakeFS(row, 10, "LEFT", T.bodyText) lvlFS:SetPoint("LEFT", row, "LEFT", CONTENT_W * 0.30, 0) lvlFS:SetWidth(CONTENT_W * 0.12) row.lvlFS = lvlFS local classFS = MakeFS(row, 10, "LEFT", T.bodyText) classFS:SetPoint("LEFT", row, "LEFT", CONTENT_W * 0.42, 0) classFS:SetWidth(CONTENT_W * 0.18) row.classFS = classFS local zoneFS = MakeFS(row, 10, "LEFT", T.dimText) zoneFS:SetPoint("LEFT", row, "LEFT", CONTENT_W * 0.60, 0) zoneFS:SetPoint("RIGHT", row, "RIGHT", -4, 0) row.zoneFS = zoneFS AddSelHighlight(row) row:SetScript("OnEnter", function() SetPixelBackdrop(this, T.slotHover, T.slotSelected) end) row:SetScript("OnLeave", function() if selectedWho == this.rowIndex then SetPixelBackdrop(this, T.tabActiveBg, T.tabActiveBorder) else SetRowNormal(this) end end) row:SetScript("OnClick", function() selectedWho = this.rowIndex SUI:UpdateWhoList() end) row:Hide() whoRows[idx] = row end -- Totals local totalFS = MakeFS(page, 10, "LEFT", T.dimText) totalFS:SetPoint("BOTTOMLEFT", page, "BOTTOMLEFT", 0, BOTTOM_H + 2) page.totalFS = totalFS -- Bottom buttons local btnBar = CreateFrame("Frame", nil, page) btnBar:SetHeight(BOTTOM_H) btnBar:SetPoint("BOTTOMLEFT", page, "BOTTOMLEFT", 0, 0) btnBar:SetPoint("BOTTOMRIGHT", page, "BOTTOMRIGHT", 0, 0) local invBtn = MakeButton(btnBar, "组队邀请", 80, 22) invBtn:SetPoint("BOTTOMLEFT", btnBar, "BOTTOMLEFT", 0, 2) invBtn:SetScript("OnClick", function() if selectedWho then local name = GetWhoInfo(selectedWho) if name then InviteByName(name) end end end) local addFriendBtn = MakeButton(btnBar, "添加好友", 80, 22) addFriendBtn:SetPoint("LEFT", invBtn, "RIGHT", 4, 0) addFriendBtn:SetScript("OnClick", function() if selectedWho then local name = GetWhoInfo(selectedWho) if name then AddFriend(name) end end end) local whisperBtn = MakeButton(btnBar, "密语", 80, 22) whisperBtn:SetPoint("LEFT", addFriendBtn, "RIGHT", 4, 0) whisperBtn:SetScript("OnClick", function() if selectedWho then local name = GetWhoInfo(selectedWho) if name then ChatFrame_SendTell(name) end end end) end function SUI:ClearWhoList() selectedWho = nil for i = 1, 50 do local row = whoRows[i] if not row then break end row.nameFS:SetText("") row.lvlFS:SetText("") row.classFS:SetText("") row.zoneFS:SetText("") SetRowNormal(row) HideSelHighlight(row) row:Hide() end if pages[2] and pages[2].wScroll then pages[2].wScroll:SetContentHeight(4) end if pages[2] and pages[2].totalFS then pages[2].totalFS:SetText("搜索中...") end end function SUI:UpdateWhoList() local numWho, totalCount = GetNumWhoResults() local totalH = 0 for i = 1, 50 do local row = whoRows[i] if not row then break end if i <= numWho then local name, guild, level, race, class, zone, classEN = GetWhoInfo(i) if name then if not classEN or classEN == "" then classEN = GetClassEN(class) end local cc = GetClassColor(classEN) row.nameFS:SetText(name) row.nameFS:SetTextColor(cc[1], cc[2], cc[3]) row.lvlFS:SetText(level or "") row.classFS:SetText(class or "") row.classFS:SetTextColor(cc[1], cc[2], cc[3]) row.zoneFS:SetText(zone or "") if selectedWho == i then SetPixelBackdrop(row, T.tabActiveBg, T.tabActiveBorder) ShowSelHighlight(row) row.nameFS:SetTextColor(1, 1, 1) else SetRowNormal(row) HideSelHighlight(row) end row:Show() totalH = totalH + 20 else row:Hide() end else row:Hide() end end if pages[2] and pages[2].wScroll then pages[2].wScroll:SetContentHeight(totalH + 4) end if pages[2] and pages[2].totalFS then pages[2].totalFS:SetText((numWho or 0) .. " 个结果 (共 " .. (totalCount or 0) .. " 人)") end end -------------------------------------------------------------------------------- -- Tab 3: Guild -------------------------------------------------------------------------------- local motdPopup = nil local function ShowMotdPopup(motdStr) if not motdPopup then local f = CreateFrame("Frame", "SFramesSocialMotdPopup", UIParent) f:SetWidth(360) f:SetHeight(220) f:SetPoint("CENTER", UIParent, "CENTER", 0, 60) f:SetFrameStrata("DIALOG") f:SetFrameLevel(120) SetRoundBackdrop(f, T.panelBg, T.panelBorder) CreateShadow(f) f:EnableMouse(true) f:SetMovable(true) f:RegisterForDrag("LeftButton") f:SetScript("OnDragStart", function() this:StartMoving() end) f:SetScript("OnDragStop", function() this:StopMovingOrSizing() end) local titleFS = MakeFS(f, 12, "CENTER", T.gold) titleFS:SetPoint("TOP", f, "TOP", 0, -10) titleFS:SetText("公会公告") f.titleFS = titleFS MakeSep(f, -26) local scrollFrame = CreateFrame("ScrollFrame", NextName("MotdScroll"), f) scrollFrame:SetPoint("TOPLEFT", f, "TOPLEFT", 12, -30) scrollFrame:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", -12, 40) local child = CreateFrame("Frame", nil, scrollFrame) child:SetWidth(336 - 24) child:SetHeight(1) scrollFrame:SetScrollChild(child) local bodyFS = child:CreateFontString(nil, "OVERLAY") bodyFS:SetFont(GetFont(), 11, "OUTLINE") bodyFS:SetJustifyH("LEFT") bodyFS:SetTextColor(T.bodyText[1], T.bodyText[2], T.bodyText[3]) bodyFS:SetPoint("TOPLEFT", child, "TOPLEFT", 0, 0) bodyFS:SetWidth(336 - 24) f.bodyFS = bodyFS local scrollOffset = 0 local scrollMax = 0 f.UpdateScroll = function() local textH = bodyFS:GetHeight() or 0 local viewH = scrollFrame:GetHeight() or 1 scrollMax = math.max(0, textH - viewH) if scrollOffset > scrollMax then scrollOffset = scrollMax end scrollFrame:SetVerticalScroll(scrollOffset) child:SetHeight(math.max(textH, viewH)) end scrollFrame:EnableMouseWheel(true) scrollFrame:SetScript("OnMouseWheel", function() scrollOffset = scrollOffset - (arg1 or 0) * 18 if scrollOffset < 0 then scrollOffset = 0 end if scrollOffset > scrollMax then scrollOffset = scrollMax end scrollFrame:SetVerticalScroll(scrollOffset) end) local closeBtn = MakeButton(f, "关闭", 100, 24) closeBtn:SetPoint("BOTTOM", f, "BOTTOM", 0, 10) closeBtn:SetScript("OnClick", function() f:Hide() end) f:Hide() motdPopup = f end motdPopup.bodyFS:SetText(motdStr or "无公告") motdPopup:Show() motdPopup.UpdateScroll() end local function BuildGuildPage(page) local motdFrame = CreateFrame("Button", NextName("MotdBtn"), page) motdFrame:SetHeight(36) motdFrame:SetPoint("TOPLEFT", page, "TOPLEFT", 0, 0) motdFrame:SetPoint("TOPRIGHT", page, "TOPRIGHT", 0, 0) SetPixelBackdrop(motdFrame, { 0.08, 0.04, 0.06, 0.8 }, T.slotBorder) local motdLabel = MakeFS(motdFrame, 9, "LEFT", T.dimText) motdLabel:SetPoint("TOPLEFT", motdFrame, "TOPLEFT", 6, -2) motdLabel:SetText("公会公告: (点击查看完整)") local motdText = MakeFS(motdFrame, 10, "LEFT", T.bodyText) motdText:SetPoint("TOPLEFT", motdLabel, "BOTTOMLEFT", 0, -2) motdText:SetPoint("RIGHT", motdFrame, "RIGHT", -6, 0) page.motdText = motdText motdFrame:SetScript("OnClick", function() local motd = GetGuildRosterMOTD and GetGuildRosterMOTD() or "" ShowMotdPopup(motd ~= "" and motd or "无公告") end) motdFrame:SetScript("OnEnter", function() SetPixelBackdrop(this, { 0.12, 0.06, 0.09, 0.9 }, T.tabActiveBorder) end) motdFrame:SetScript("OnLeave", function() SetPixelBackdrop(this, { 0.08, 0.04, 0.06, 0.8 }, T.slotBorder) end) -- Toolbar row: search + filter buttons local toolBar = CreateFrame("Frame", nil, page) toolBar:SetHeight(18) toolBar:SetPoint("TOPLEFT", page, "TOPLEFT", 0, -38) toolBar:SetPoint("TOPRIGHT", page, "TOPRIGHT", 0, -38) local gSearchLabel = MakeFS(toolBar, 9, "LEFT", T.dimText) gSearchLabel:SetPoint("LEFT", toolBar, "LEFT", 0, 0) gSearchLabel:SetText("搜索:") local gSearchBox = MakeEditBox(toolBar, 110, 16) gSearchBox:SetPoint("LEFT", gSearchLabel, "RIGHT", 2, 0) gSearchBox:SetFont(GetFont(), 9, "OUTLINE") gSearchBox:SetScript("OnTextChanged", function() guildSearchText = this:GetText() or "" SUI:UpdateGuildList() end) gSearchBox:SetScript("OnEnterPressed", function() this:ClearFocus() end) gSearchBox:SetScript("OnEscapePressed", function() this:SetText("") this:ClearFocus() end) page.gSearchBox = gSearchBox local offlineToggle = MakeButton(toolBar, guildHideOffline and "显示离线" or "隐藏离线", 56, 16) offlineToggle:SetPoint("TOPRIGHT", toolBar, "TOPRIGHT", 0, 0) offlineToggle.text:SetFont(GetFont(), 9, "OUTLINE") offlineToggle:SetScript("OnClick", function() guildHideOffline = not guildHideOffline SFramesDB.guildHideOffline = guildHideOffline if guildHideOffline then this.text:SetText("显示离线") else this.text:SetText("隐藏离线") end SUI:UpdateGuildList() end) page.offlineToggle = offlineToggle local viewToggle = MakeButton(toolBar, "切换视图", 56, 16) viewToggle:SetPoint("RIGHT", offlineToggle, "LEFT", -4, 0) viewToggle.text:SetFont(GetFont(), 9, "OUTLINE") viewToggle:SetScript("OnClick", function() if guildViewMode == "player" then guildViewMode = "guild" else guildViewMode = "player" end SUI:UpdateGuildList() end) -- Column headers local colFrame = CreateFrame("Frame", nil, page) colFrame:SetHeight(18) colFrame:SetPoint("TOPLEFT", page, "TOPLEFT", 0, -56) colFrame:SetPoint("TOPRIGHT", page, "TOPRIGHT", 0, -56) page.guildColFrame = colFrame page.guildColBtns = {} MakeSep(page, -74) local listArea = CreateFrame("Frame", nil, page) listArea:SetPoint("TOPLEFT", page, "TOPLEFT", 0, -76) listArea:SetPoint("BOTTOMRIGHT", page, "BOTTOMRIGHT", 0, BOTTOM_H + 14) local gScroll = CreateScrollArea(listArea, CONTENT_W, FRAME_H - HEADER_H - TAB_BAR_H - 76 - BOTTOM_H - 14 - 16) gScroll:SetPoint("TOPLEFT", listArea, "TOPLEFT", 0, 0) page.gScroll = gScroll local MAX_GUILD_ROWS = 100 for idx = 1, MAX_GUILD_ROWS do local rowIdx = idx local row = CreateFrame("Button", NextName("GR"), gScroll.child) row:SetWidth(CONTENT_W - 4) row:SetHeight(18) row:SetPoint("TOPLEFT", gScroll.child, "TOPLEFT", 2, -((rowIdx - 1) * 18)) row.rowIndex = rowIdx SetRowNormal(row) local classIcon = CreateClassIcon(row, 14) classIcon:SetPoint("LEFT", row, "LEFT", 2, 0) row.classIcon = classIcon local nameFS = MakeFS(row, 10, "LEFT", T.nameText) nameFS:SetPoint("LEFT", classIcon, "RIGHT", 3, 0) nameFS:SetWidth(CONTENT_W * 0.24 - 20) row.nameFS = nameFS local lvlFS = MakeFS(row, 10, "CENTER", T.bodyText) lvlFS:SetWidth(30) row.lvlFS = lvlFS local col3FS = MakeFS(row, 10, "LEFT", T.dimText) col3FS:SetWidth(100) row.col3FS = col3FS local col4FS = MakeFS(row, 9, "LEFT", T.dimText) row.col4FS = col4FS local col5FS = MakeFS(row, 9, "LEFT", T.dimText) row.col5FS = col5FS AddSelHighlight(row) row:RegisterForClicks("LeftButtonUp", "RightButtonUp") row:SetScript("OnEnter", function() SetPixelBackdrop(this, T.slotHover, T.slotSelected) end) row:SetScript("OnLeave", function() if selectedGuild and selectedGuild == this.guildDataIndex then SetPixelBackdrop(this, T.tabActiveBg, T.tabActiveBorder) else SetRowNormal(this) end end) row:SetScript("OnClick", function() selectedGuild = this.guildDataIndex if arg1 == "RightButton" and this.guildDataIndex then SUI:ShowGuildRowMenu(this) else SUI:UpdateGuildList() end end) row:Hide() guildRows[idx] = row end local totalFS = MakeFS(page, 9, "LEFT", T.dimText) totalFS:SetPoint("BOTTOMLEFT", page, "BOTTOMLEFT", 2, BOTTOM_H + 2) page.guildTotalFS = totalFS local btnBar = CreateFrame("Frame", nil, page) btnBar:SetHeight(BOTTOM_H) btnBar:SetPoint("BOTTOMLEFT", page, "BOTTOMLEFT", 0, 0) btnBar:SetPoint("BOTTOMRIGHT", page, "BOTTOMRIGHT", 0, 0) local invBtn = MakeButton(btnBar, "组队邀请", 80, 22) invBtn:SetPoint("BOTTOMLEFT", btnBar, "BOTTOMLEFT", 0, 2) invBtn:SetScript("OnClick", function() if selectedGuild then local name, rank, rankIndex, level, class, zone, note, officerNote, online = GetGuildRosterInfo(selectedGuild) if name and online then InviteByName(name) end end end) local msgBtn = MakeButton(btnBar, "发送信息", 80, 22) msgBtn:SetPoint("LEFT", invBtn, "RIGHT", 4, 0) msgBtn:SetScript("OnClick", function() if selectedGuild then local name = GetGuildRosterInfo(selectedGuild) if name then ChatFrame_SendTell(name) end end end) local gInviteBtn = MakeButton(btnBar, "邀请入会", 80, 22) gInviteBtn:SetPoint("LEFT", msgBtn, "RIGHT", 4, 0) gInviteBtn:SetScript("OnClick", function() ShowCustomPopup("邀请玩家加入公会", true, function(name) if name and name ~= "" then if GuildInvite then GuildInvite(name) elseif GuildInviteByName then GuildInviteByName(name) else SendChatMessage(".ginvite " .. name, "GUILD") end end end) end) page.gInviteBtn = gInviteBtn local gLeaveBtn = MakeButton(btnBar, "退出公会", 80, 22) gLeaveBtn:SetPoint("LEFT", gInviteBtn, "RIGHT", 4, 0) gLeaveBtn:SetScript("OnClick", function() StaticPopup_Show("CONFIRM_GUILD_LEAVE", GetGuildInfo("player")) end) page.gLeaveBtn = gLeaveBtn end function SUI:ShowGuildRowMenu(row) local di = row.guildDataIndex if not di then return end local name, rank, rankIndex, level, class, zone, note, officerNote, online = GetGuildRosterInfo(di) if not name then return end local isSelf = (name == UnitName("player")) if not SUI.guildDropDown then SUI.guildDropDown = CreateFrame("Frame", "SFramesSocialGuildDD", UIParent, "UIDropDownMenuTemplate") SUI.guildDropDown.displayMode = "MENU" SUI.guildDropDown.initialize = function() local gd = SUI.guildDropDown local gName = gd.memberName local gOnline = gd.memberOnline local gSelf = gd.memberIsSelf if not gName then return end local info = {} info.text = gName info.isTitle = 1 info.notCheckable = 1 UIDropDownMenu_AddButton(info) if not gSelf then if gOnline then info = {} info.text = "密语" info.notCheckable = 1 info.func = function() ChatFrame_SendTell(gName) end UIDropDownMenu_AddButton(info) info = {} info.text = "邀请组队" info.notCheckable = 1 info.func = function() InviteByName(gName) end UIDropDownMenu_AddButton(info) info = {} info.text = "观察" info.notCheckable = 1 info.func = function() if TargetByName then TargetByName(gName) end if UnitExists("target") and UnitName("target") == gName then InspectUnit("target") end end UIDropDownMenu_AddButton(info) end if CanGuildRemove and CanGuildRemove() then info = {} info.text = "移出公会" info.notCheckable = 1 info.func = function() GuildUninvite(gName) end UIDropDownMenu_AddButton(info) end if CanGuildPromote and CanGuildPromote() then info = {} info.text = "晋升" info.notCheckable = 1 info.func = function() GuildPromote(gName) end UIDropDownMenu_AddButton(info) end if CanGuildDemote and CanGuildDemote() then info = {} info.text = "降级" info.notCheckable = 1 info.func = function() GuildDemote(gName) end UIDropDownMenu_AddButton(info) end else info = {} info.text = "退出公会" info.notCheckable = 1 info.func = function() StaticPopup_Show("CONFIRM_GUILD_LEAVE", GetGuildInfo("player")) end UIDropDownMenu_AddButton(info) end info = {} info.text = "取消" info.notCheckable = 1 UIDropDownMenu_AddButton(info) end end SUI.guildDropDown.memberName = name SUI.guildDropDown.memberOnline = online SUI.guildDropDown.memberIsSelf = isSelf ToggleDropDownMenu(1, nil, SUI.guildDropDown, "cursor") end local GUILD_COL_SORT_MAP = { ["名称"] = "name", ["等级"] = "level", ["职业"] = "class", ["职位"] = "rank", ["区域"] = "zone", ["备注"] = "note", ["状态"] = "status", } local function RebuildGuildColHeaders() local colFrame = pages[3] and pages[3].guildColFrame if not colFrame then return end local old = pages[3].guildColBtns if old then for _, btn in ipairs(old) do btn:Hide() end end pages[3].guildColBtns = {} local headers if guildViewMode == "player" then headers = { { "名称", 0, 0.24 }, { "等级", 0.24, 0.08 }, { "职业", 0.32, 0.14 }, { "职位", 0.46, 0.18 }, { "区域", 0.64, 0.36 } } else headers = { { "名称", 0, 0.30 }, { "职位", 0.30, 0.20 }, { "备注", 0.50, 0.30 }, { "状态", 0.80, 0.20 } } end for _, h in ipairs(headers) do local sortKey = GUILD_COL_SORT_MAP[h[1]] local btn = CreateFrame("Button", nil, colFrame) btn:SetHeight(18) btn:SetPoint("TOPLEFT", colFrame, "TOPLEFT", CONTENT_W * h[2], 0) btn:SetWidth(CONTENT_W * h[3]) local fs = MakeFS(btn, 10, "LEFT", T.colHeader) fs:SetPoint("LEFT", btn, "LEFT", 0, 0) local arrow = "" if sortKey and guildSortField == sortKey then arrow = guildSortAsc and " ▲" or " ▼" end fs:SetText(h[1] .. arrow) btn.headerFS = fs if sortKey then btn.sortKey = sortKey btn:SetScript("OnClick", function() if guildSortField == this.sortKey then guildSortAsc = not guildSortAsc else guildSortField = this.sortKey guildSortAsc = true end SUI:UpdateGuildList() end) btn:SetScript("OnEnter", function() this.headerFS:SetTextColor(1, 1, 1) end) btn:SetScript("OnLeave", function() this.headerFS:SetTextColor(T.colHeader[1], T.colHeader[2], T.colHeader[3]) end) end table.insert(pages[3].guildColBtns, btn) end end function SUI:UpdateGuildList() if not IsInGuild() then for i = 1, 100 do if guildRows[i] then guildRows[i]:Hide() end end if pages[3] and pages[3].motdText then pages[3].motdText:SetText("你没有加入公会") end if pages[3] and pages[3].gInviteBtn then pages[3].gInviteBtn:Hide() end if pages[3] and pages[3].gLeaveBtn then pages[3].gLeaveBtn:Hide() end if pages[3] and pages[3].guildTotalFS then pages[3].guildTotalFS:SetText("") end return end if SetGuildRosterShowOffline then SetGuildRosterShowOffline(not guildHideOffline) end GuildRoster() local motd = GetGuildRosterMOTD and GetGuildRosterMOTD() or "" if pages[3] and pages[3].motdText then pages[3].motdText:SetText(motd ~= "" and motd or "无公告") end RebuildGuildColHeaders() local numTotal, numOnlineRet = GetNumGuildMembers() numTotal = numTotal or 0 local totalOnline = 0 local members = {} local searchLower = string.lower(guildSearchText or "") local hasSearch = searchLower ~= "" for i = 1, numTotal do local name, rank, rankIndex, level, class, zone, note, officerNote, online, status, classEN = GetGuildRosterInfo(i) if name then if online then totalOnline = totalOnline + 1 end local match = true if hasSearch then match = string.find(string.lower(name), searchLower, 1, true) if not match and class then match = string.find(string.lower(class), searchLower, 1, true) end if not match and rank then match = string.find(string.lower(rank), searchLower, 1, true) end if not match and zone then match = string.find(string.lower(zone), searchLower, 1, true) end end if match then if not classEN or classEN == "" then classEN = GetClassEN(class) end table.insert(members, { idx = i, name = name, rank = rank, rankIndex = rankIndex or 99, level = level or 0, class = class or "", classEN = classEN, zone = zone or "", note = note or "", officerNote = officerNote or "", online = online, status = status, }) end end end local sf = guildSortField local asc = guildSortAsc table.sort(members, function(a, b) local va, vb if sf == "name" then va, vb = (a.name or ""), (b.name or "") elseif sf == "level" then va, vb = (a.level or 0), (b.level or 0) elseif sf == "class" then va, vb = (a.class or ""), (b.class or "") elseif sf == "rank" then va, vb = (a.rankIndex or 99), (b.rankIndex or 99) elseif sf == "zone" then va, vb = (a.zone or ""), (b.zone or "") elseif sf == "note" then va, vb = (a.note or ""), (b.note or "") elseif sf == "status" then local sa = a.online and 0 or 1 local sb = b.online and 0 or 1 va, vb = sa, sb else va, vb = (a.name or ""), (b.name or "") end if va == vb then return (a.name or "") < (b.name or "") end if asc then return va < vb else return va > vb end end) local totalH = 0 local rowIdx = 0 for mi = 1, table.getn(members) do local m = members[mi] rowIdx = rowIdx + 1 if rowIdx > 100 then break end local row = guildRows[rowIdx] if row then local cc = GetClassColor(m.classEN) SetClassIcon(row.classIcon, m.classEN) row.nameFS:SetText(m.name) row.guildDataIndex = m.idx if m.online then row.nameFS:SetTextColor(cc[1], cc[2], cc[3]) else row.nameFS:SetTextColor(T.offlineText[1], T.offlineText[2], T.offlineText[3]) end if guildViewMode == "player" then row.lvlFS:SetText(m.level > 0 and m.level or "") row.lvlFS:ClearAllPoints() row.lvlFS:SetPoint("LEFT", row, "LEFT", CONTENT_W * 0.24, 0) row.col3FS:SetText(m.class) row.col3FS:ClearAllPoints() row.col3FS:SetPoint("LEFT", row, "LEFT", CONTENT_W * 0.32, 0) row.col3FS:SetWidth(CONTENT_W * 0.14) row.col3FS:SetTextColor(cc[1], cc[2], cc[3]) row.col4FS:SetText(m.rank or "") row.col4FS:ClearAllPoints() row.col4FS:SetPoint("LEFT", row, "LEFT", CONTENT_W * 0.46, 0) row.col4FS:SetWidth(CONTENT_W * 0.18) row.col4FS:SetTextColor(T.bodyText[1], T.bodyText[2], T.bodyText[3]) row.col5FS:SetText(m.online and m.zone or "离线") row.col5FS:ClearAllPoints() row.col5FS:SetPoint("LEFT", row, "LEFT", CONTENT_W * 0.64, 0) row.col5FS:SetPoint("RIGHT", row, "RIGHT", -4, 0) if m.online then row.col5FS:SetTextColor(T.dimText[1], T.dimText[2], T.dimText[3]) else row.col5FS:SetTextColor(T.offlineText[1], T.offlineText[2], T.offlineText[3]) end row.col5FS:Show() else row.lvlFS:SetText("") row.col3FS:SetText(m.rank or "") row.col3FS:ClearAllPoints() row.col3FS:SetPoint("LEFT", row, "LEFT", CONTENT_W * 0.30, 0) row.col3FS:SetWidth(CONTENT_W * 0.20) row.col3FS:SetTextColor(T.bodyText[1], T.bodyText[2], T.bodyText[3]) row.col4FS:SetText(m.note) row.col4FS:ClearAllPoints() row.col4FS:SetPoint("LEFT", row, "LEFT", CONTENT_W * 0.50, 0) row.col4FS:SetWidth(CONTENT_W * 0.30) row.col4FS:SetTextColor(T.dimText[1], T.dimText[2], T.dimText[3]) row.col5FS:SetText(m.online and "在线" or "离线") row.col5FS:ClearAllPoints() row.col5FS:SetPoint("LEFT", row, "LEFT", CONTENT_W * 0.80, 0) row.col5FS:SetPoint("RIGHT", row, "RIGHT", -4, 0) if m.online then row.col5FS:SetTextColor(T.onlineText[1], T.onlineText[2], T.onlineText[3]) else row.col5FS:SetTextColor(T.offlineText[1], T.offlineText[2], T.offlineText[3]) end row.col5FS:Show() end if selectedGuild == m.idx then SetPixelBackdrop(row, T.tabActiveBg, T.tabActiveBorder) ShowSelHighlight(row) row.nameFS:SetTextColor(1, 1, 1) else SetRowNormal(row) HideSelHighlight(row) end row:Show() totalH = totalH + 18 end end for i = rowIdx + 1, 100 do if guildRows[i] then guildRows[i]:Hide() end end if pages[3] and pages[3].gScroll then pages[3].gScroll:SetContentHeight(totalH + 4) end if pages[3] and pages[3].guildTotalFS then local shownCount = table.getn(members) if guildHideOffline then pages[3].guildTotalFS:SetText(totalOnline .. " 人在线 (共 " .. numTotal .. " 名成员)") else pages[3].guildTotalFS:SetText("共 " .. numTotal .. " 名成员, " .. totalOnline .. " 人在线") end end -- Show/hide guild invite button based on permission if pages[3] and pages[3].gInviteBtn then local canInvite = CanGuildInvite and CanGuildInvite() if canInvite then pages[3].gInviteBtn:Show() else pages[3].gInviteBtn:Hide() end end end -------------------------------------------------------------------------------- -- Tab 4: Raid -------------------------------------------------------------------------------- local RAID_COLS = 5 local RAID_GROUPS = 8 local RAID_SLOT_W = 68 local RAID_SLOT_H = 24 local RAID_GROUP_PAD = 3 local RAID_GROUP_LABEL_H = 14 local RAID_ICON_SIZE = 14 local dragSlot = nil local dragHighlight = nil local function BuildRaidPage(page) local infoFS = MakeFS(page, 10, "LEFT", T.dimText) infoFS:SetPoint("TOPLEFT", page, "TOPLEFT", 0, 0) page.raidInfoFS = infoFS local rScroll = CreateScrollArea(page, CONTENT_W, FRAME_H - HEADER_H - TAB_BAR_H - 20 - BOTTOM_H - 16) rScroll:SetPoint("TOPLEFT", page, "TOPLEFT", 0, -18) page.rScroll = rScroll local groupH = RAID_GROUP_LABEL_H + RAID_SLOT_H for gIdx = 1, RAID_GROUPS do local g = gIdx local groupFrame = CreateFrame("Frame", nil, rScroll.child) groupFrame:SetWidth(CONTENT_W) groupFrame:SetHeight(groupH) local gy = -((g - 1) * (groupH + RAID_GROUP_PAD)) groupFrame:SetPoint("TOPLEFT", rScroll.child, "TOPLEFT", 0, gy) SetRoundBackdrop(groupFrame, T.raidGroup, T.raidGroupBorder) groupFrame.groupIndex = g groupFrame:EnableMouse(true) groupFrame:SetScript("OnMouseUp", function() if not dragSlot then return end if dragHighlight then dragHighlight:Hide() dragHighlight:SetScript("OnUpdate", nil) end local tg = this.groupIndex if tg and dragSlot.raidIndex and tg ~= dragSlot.group then SetRaidSubgroup(dragSlot.raidIndex, tg) end dragSlot = nil end) local groupLabel = MakeFS(groupFrame, 9, "LEFT", T.gold) groupLabel:SetPoint("TOPLEFT", groupFrame, "TOPLEFT", 4, -1) groupLabel:SetText("小组 " .. g) for sIdx = 1, RAID_COLS do local s = sIdx local slot = CreateFrame("Button", NextName("RS"), groupFrame) slot:SetWidth(RAID_SLOT_W) slot:SetHeight(RAID_SLOT_H) local sx = 2 + (s - 1) * (RAID_SLOT_W + 2) slot:SetPoint("BOTTOMLEFT", groupFrame, "BOTTOMLEFT", sx, 1) SetPixelBackdrop(slot, T.raidSlotEmpty, T.slotBorder) slot.group = g slot.slotIndex = s local classIcon = CreateClassIcon(slot, RAID_ICON_SIZE) classIcon:SetPoint("LEFT", slot, "LEFT", 2, 0) slot.classIcon = classIcon local nameFS = MakeFS(slot, 8, "LEFT", T.nameText) nameFS:SetPoint("LEFT", classIcon, "RIGHT", 2, 0) nameFS:SetPoint("RIGHT", slot, "RIGHT", -2, 0) slot.nameFS = nameFS slot.infoFS = nil slot:RegisterForClicks("LeftButtonUp", "RightButtonUp") slot:RegisterForDrag("LeftButton") slot:SetScript("OnClick", function() if not this.raidIndex then return end if arg1 == "RightButton" then SUI:ShowRaidSlotMenu(this) end end) slot:SetScript("OnDragStart", function() if not this.raidIndex then return end if not (IsRaidLeader() or IsRaidOfficer()) then return end dragSlot = this if not dragHighlight then dragHighlight = CreateFrame("Frame", "SFramesRaidDragHL", UIParent) dragHighlight:SetWidth(RAID_SLOT_W) dragHighlight:SetHeight(RAID_SLOT_H) dragHighlight:SetFrameStrata("TOOLTIP") SetPixelBackdrop(dragHighlight, T.tabActiveBg, T.tabActiveBorder) local dfs = MakeFS(dragHighlight, 8, "CENTER", T.tabActiveText) dfs:SetPoint("CENTER", 0, 0) dragHighlight.text = dfs end dragHighlight.text:SetText(this.playerName or "") local cc = GetClassColor(this.playerClass) dragHighlight.text:SetTextColor(cc[1], cc[2], cc[3]) dragHighlight:Show() dragHighlight:SetScript("OnUpdate", function() local cx, cy = GetCursorPosition() local s = UIParent:GetEffectiveScale() dragHighlight:ClearAllPoints() dragHighlight:SetPoint("CENTER", UIParent, "BOTTOMLEFT", cx / s, cy / s) end) end) slot:SetScript("OnDragStop", function() if not dragSlot then return end if dragHighlight then dragHighlight:Hide() dragHighlight:SetScript("OnUpdate", nil) end local cx, cy = GetCursorPosition() local es = UIParent:GetEffectiveScale() cx = cx / es cy = cy / es local targetSlot = nil local targetGroup = nil for tg = 1, RAID_GROUPS do for ts = 1, RAID_COLS do local tSlot = raidSlots[tg] and raidSlots[tg][ts] if tSlot and tSlot:IsVisible() then local left = tSlot:GetLeft() local right = tSlot:GetRight() local top = tSlot:GetTop() local bottom = tSlot:GetBottom() if left and cx >= left and cx <= right and cy >= bottom and cy <= top then targetSlot = tSlot targetGroup = tg break end end end if targetGroup then break end end if not targetGroup then for tg = 1, RAID_GROUPS do local gf = raidSlots[tg] and raidSlots[tg][1] and raidSlots[tg][1]:GetParent() if gf and gf:IsVisible() then local left = gf:GetLeft() local right = gf:GetRight() local top = gf:GetTop() local bottom = gf:GetBottom() if left and cx >= left and cx <= right and cy >= bottom and cy <= top then targetGroup = tg break end end end end if targetGroup and dragSlot.raidIndex and targetGroup ~= dragSlot.group then if targetSlot and targetSlot.raidIndex and SwapRaidMembers then SwapRaidMembers(dragSlot.raidIndex, targetSlot.raidIndex) else SetRaidSubgroup(dragSlot.raidIndex, targetGroup) end end dragSlot = nil end) slot:SetScript("OnEnter", function() if dragSlot and dragSlot ~= this then SetPixelBackdrop(this, { 0.30, 0.18, 0.26, 0.9 }, T.tabActiveBorder) else SetPixelBackdrop(this, T.slotHover, T.slotSelected) end if this.playerName and not dragSlot then GameTooltip:SetOwner(this, "ANCHOR_RIGHT") GameTooltip:AddLine(this.playerName, 1, 1, 1) if this.playerClass then local cc = GetClassColor(this.playerClass) GameTooltip:AddLine(this.playerClass, cc[1], cc[2], cc[3]) end if this.playerLevel then GameTooltip:AddLine("等级 " .. this.playerLevel, T.bodyText[1], T.bodyText[2], T.bodyText[3]) end if IsRaidLeader() or IsRaidOfficer() then GameTooltip:AddLine("左键拖拽: 移动到其他小组", T.dimText[1], T.dimText[2], T.dimText[3]) end GameTooltip:AddLine("右键: 团队操作菜单", T.dimText[1], T.dimText[2], T.dimText[3]) GameTooltip:Show() end end) slot:SetScript("OnLeave", function() if this.raidIndex then SetPixelBackdrop(this, T.slotBg, T.slotBorder) else SetPixelBackdrop(this, T.raidSlotEmpty, T.slotBorder) end GameTooltip:Hide() end) if not raidSlots[g] then raidSlots[g] = {} end raidSlots[g][s] = slot end end rScroll:SetContentHeight(RAID_GROUPS * (groupH + RAID_GROUP_PAD)) local btnBar = CreateFrame("Frame", nil, page) btnBar:SetHeight(BOTTOM_H) btnBar:SetPoint("BOTTOMLEFT", page, "BOTTOMLEFT", 0, 0) btnBar:SetPoint("BOTTOMRIGHT", page, "BOTTOMRIGHT", 0, 0) local convertBtn = MakeButton(btnBar, "转换团队", 80, 22) convertBtn:SetPoint("BOTTOMLEFT", btnBar, "BOTTOMLEFT", 0, 2) convertBtn:SetScript("OnClick", function() if GetNumRaidMembers() == 0 and GetNumPartyMembers() > 0 then ConvertToRaid() end end) local leaveBtn = MakeButton(btnBar, "离开团队", 80, 22) leaveBtn:SetPoint("LEFT", convertBtn, "RIGHT", 4, 0) leaveBtn:SetScript("OnClick", function() if GetNumRaidMembers() > 0 then LeaveParty() end end) local readyBtn = MakeButton(btnBar, "就位确认", 80, 22) readyBtn:SetPoint("LEFT", leaveBtn, "RIGHT", 4, 0) readyBtn:SetScript("OnClick", function() if DoReadyCheck then DoReadyCheck() end end) end function SUI:GetRaidUnitByName(targetName) for i = 1, 40 do local u = "raid" .. i if UnitExists(u) and UnitName(u) == targetName then return u end end return nil end function SUI:ShowRaidSlotMenu(slot) if not slot.raidIndex then return end local pName = slot.playerName if not pName then return end if not SUI.raidDropDown then SUI.raidDropDown = CreateFrame("Frame", "SFramesSocialRaidDD", UIParent, "UIDropDownMenuTemplate") SUI.raidDropDown.displayMode = "MENU" SUI.raidDropDown.initialize = function() local rd = SUI.raidDropDown local name = rd.slotName local rIdx = rd.slotRaidIndex local grp = rd.slotGroup local rnk = rd.slotRank local isSelf = rd.slotIsSelf if not name or not rIdx then return end local isLeader = IsRaidLeader() local isOfficer = IsRaidOfficer() local info info = {} info.text = name info.isTitle = 1 info.notCheckable = 1 UIDropDownMenu_AddButton(info) if not isSelf then info = {} info.text = "密语" info.notCheckable = 1 info.func = function() ChatFrame_SendTell(name) end UIDropDownMenu_AddButton(info) info = {} info.text = "观察" info.notCheckable = 1 info.func = function() local u = SUI:GetRaidUnitByName(name) if u then InspectUnit(u) end end UIDropDownMenu_AddButton(info) end if isLeader or isOfficer then for gIdx = 1, RAID_GROUPS do if gIdx ~= grp then info = {} info.text = "移至小组 " .. gIdx info.notCheckable = 1 local tg = gIdx info.func = function() SetRaidSubgroup(rIdx, tg) end UIDropDownMenu_AddButton(info) end end end if isLeader and not isSelf then if rnk and rnk < 1 then info = {} info.text = "提升为助理" info.notCheckable = 1 info.func = function() PromoteToAssistant(name) end UIDropDownMenu_AddButton(info) elseif rnk and rnk == 1 then info = {} info.text = "取消助理" info.notCheckable = 1 info.func = function() DemoteAssistant(name) end UIDropDownMenu_AddButton(info) end info = {} info.text = "转让队长" info.notCheckable = 1 info.func = function() StaticPopupDialogs["SFRAMES_PROMOTE_LEADER"] = { text = "确定要将队长转让给 " .. name .. " 吗?", button1 = "确定", button2 = "取消", OnAccept = function() PromoteToLeader(name) end, timeout = 0, whileDead = true, hideOnEscape = true, } StaticPopup_Show("SFRAMES_PROMOTE_LEADER") end UIDropDownMenu_AddButton(info) end if (isLeader or isOfficer) and not isSelf then info = {} info.text = "移出团队" info.notCheckable = 1 info.func = function() UninviteByName(name) end UIDropDownMenu_AddButton(info) end info = {} info.text = "取消" info.notCheckable = 1 UIDropDownMenu_AddButton(info) end end SUI.raidDropDown.slotName = pName SUI.raidDropDown.slotRaidIndex = slot.raidIndex SUI.raidDropDown.slotGroup = slot.group SUI.raidDropDown.slotRank = slot.playerRank SUI.raidDropDown.slotIsSelf = (pName == UnitName("player")) ToggleDropDownMenu(1, nil, SUI.raidDropDown, "cursor") end local function CancelRaidDrag() if dragSlot then dragSlot = nil if dragHighlight then dragHighlight:Hide() dragHighlight:SetScript("OnUpdate", nil) end end end function SUI:UpdateRaidPage() local page = pages[4] if not page then return end local numRaid = GetNumRaidMembers() -- Clear all slots for g = 1, RAID_GROUPS do for s = 1, RAID_COLS do local slot = raidSlots[g] and raidSlots[g][s] if slot then slot.raidIndex = nil slot.playerName = nil slot.playerClass = nil slot.playerRank = nil slot.playerLevel = nil slot.nameFS:SetText("") slot.classIcon:Hide() SetPixelBackdrop(slot, T.raidSlotEmpty, T.slotBorder) end end end if numRaid == 0 then page.raidInfoFS:SetText("你不在团队中") return end page.raidInfoFS:SetText("团队成员: " .. numRaid .. "/40") local groupCount = {} for g = 1, RAID_GROUPS do groupCount[g] = 0 end for i = 1, numRaid do local name, rank, subgroup, level, class, fileName, zone, online, isDead, role, isML = GetRaidRosterInfo(i) if name and subgroup then local g = subgroup groupCount[g] = (groupCount[g] or 0) + 1 local s = groupCount[g] if s <= RAID_COLS then local slot = raidSlots[g] and raidSlots[g][s] if slot then slot.raidIndex = i slot.playerName = name slot.playerClass = fileName slot.playerRank = rank slot.playerLevel = level local shortName = name if string.len(name) > 7 then shortName = string.sub(name, 1, 6) .. ".." end local prefix = "" if rank and rank == 2 then prefix = "*" elseif rank and rank == 1 then prefix = "+" end slot.nameFS:SetText(prefix .. shortName) local cc = GetClassColor(fileName) slot.nameFS:SetTextColor(cc[1], cc[2], cc[3]) SetClassIcon(slot.classIcon, fileName) if online then SetPixelBackdrop(slot, T.slotBg, T.slotBorder) if isDead then slot.nameFS:SetTextColor(0.5, 0.2, 0.2) end else SetPixelBackdrop(slot, { 0.06, 0.03, 0.05, 0.7 }, { 0.20, 0.15, 0.18, 0.6 }) slot.nameFS:SetTextColor(T.offlineText[1], T.offlineText[2], T.offlineText[3]) end end end end end end -------------------------------------------------------------------------------- -- Main Tab Switching -------------------------------------------------------------------------------- local TAB_NAMES = { "好友", "查询", "工会", "团队" } local function ShowPage(tabIdx) currentMainTab = tabIdx for i = 1, 4 do local p = pages[i] if p then if i == tabIdx then p:Show() else p:Hide() end end local btn = mainTabs[i] if btn then if i == tabIdx then SetRoundBackdrop(btn, T.tabActiveBg, T.tabActiveBorder) btn.text:SetTextColor(T.tabActiveText[1], T.tabActiveText[2], T.tabActiveText[3]) else SetRoundBackdrop(btn, T.tabBg, T.tabBorder) btn.text:SetTextColor(T.tabText[1], T.tabText[2], T.tabText[3]) end end end if tabIdx == 1 then if origShowFriendsAPI then origShowFriendsAPI() end SUI:UpdateFriendsPage() elseif tabIdx == 2 then if SetWhoToUI then SetWhoToUI(1) end SUI:UpdateWhoList() elseif tabIdx == 3 then SUI:UpdateGuildList() elseif tabIdx == 4 then SUI:UpdateRaidPage() end if MainFrame and MainFrame.titleFS then local titles = { "好友名单", "查询玩家", "公会", "团队" } if tabIdx == 3 and IsInGuild() then local guildName = GetGuildInfo("player") if guildName and guildName ~= "" then MainFrame.titleFS:SetText("< " .. guildName .. " >") else MainFrame.titleFS:SetText(titles[tabIdx]) end else MainFrame.titleFS:SetText(titles[tabIdx] or "社交") end end end -------------------------------------------------------------------------------- -- Build Main Frame -------------------------------------------------------------------------------- local function BuildMainFrame() if MainFrame then return end local f = CreateFrame("Frame", "SFramesSocialUI", UIParent) f:SetWidth(FRAME_W) f:SetHeight(FRAME_H) f:SetPoint("CENTER", UIParent, "CENTER", 0, 0) f:SetFrameStrata("HIGH") f:SetFrameLevel(10) SetRoundBackdrop(f) CreateShadow(f) f:EnableMouse(true) f:SetMovable(true) f:RegisterForDrag("LeftButton") f:SetScript("OnDragStart", function() this:StartMoving() end) f:SetScript("OnDragStop", function() this:StopMovingOrSizing() end) f:SetScript("OnHide", function() CancelRaidDrag() end) -- Header local header = CreateFrame("Frame", nil, f) header:SetHeight(HEADER_H) header:SetPoint("TOPLEFT", f, "TOPLEFT", 0, 0) header:SetPoint("TOPRIGHT", f, "TOPRIGHT", 0, 0) local titleIco = SFrames:CreateIcon(header, "friends", 14) titleIco:SetDrawLayer("OVERLAY") titleIco:SetPoint("CENTER", header, "CENTER", -40, 0) titleIco:SetVertexColor(T.gold[1], T.gold[2], T.gold[3]) local title = MakeFS(header, 13, "CENTER", T.gold) title:SetPoint("LEFT", titleIco, "RIGHT", 4, 0) title:SetText("好友名单") f.titleFS = title local closeBtn = CreateFrame("Button", NextName("Close"), header) closeBtn:SetWidth(20) closeBtn:SetHeight(20) closeBtn:SetPoint("RIGHT", header, "RIGHT", -6, 0) closeBtn:SetScript("OnClick", function() f:Hide() end) local closeIco = SFrames:CreateIcon(closeBtn, "close", 12) closeIco:SetDrawLayer("OVERLAY") closeIco:SetPoint("CENTER", closeBtn, "CENTER", 0, 0) closeIco:SetVertexColor(1, 0.7, 0.7) closeBtn:SetScript("OnEnter", function() closeIco:SetVertexColor(1, 0.4, 0.5) end) closeBtn:SetScript("OnLeave", function() closeIco:SetVertexColor(1, 0.7, 0.7) end) -- Main tabs (use this.tabIndex to avoid closure issues) local tabContainer = CreateFrame("Frame", nil, f) tabContainer:SetHeight(TAB_BAR_H) tabContainer:SetPoint("TOPLEFT", f, "TOPLEFT", SIDE_PAD, -(HEADER_H + 2)) tabContainer:SetPoint("TOPRIGHT", f, "TOPRIGHT", -SIDE_PAD, -(HEADER_H + 2)) local tabW = math.floor((CONTENT_W - 6) / 4) for idx = 1, 4 do local tabIdx = idx local btn = CreateFrame("Button", NextName("MTab"), tabContainer) btn:SetWidth(tabW) btn:SetHeight(TAB_BAR_H - 2) btn.tabIndex = tabIdx if tabIdx == 1 then btn:SetPoint("LEFT", tabContainer, "LEFT", 0, 0) else btn:SetPoint("LEFT", mainTabs[tabIdx - 1], "RIGHT", 2, 0) end SetRoundBackdrop(btn, T.tabBg, T.tabBorder) local text = MakeFS(btn, 10, "CENTER", T.tabText) text:SetPoint("CENTER", 0, 0) text:SetText(TAB_NAMES[tabIdx]) btn.text = text btn:SetScript("OnClick", function() ShowPage(this.tabIndex) end) mainTabs[tabIdx] = btn end -- Content pages local contentTop = -(HEADER_H + TAB_BAR_H + 6) for i = 1, 4 do local page = CreateFrame("Frame", nil, f) page:SetPoint("TOPLEFT", f, "TOPLEFT", SIDE_PAD, contentTop) page:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", -SIDE_PAD, SIDE_PAD) page:Hide() pages[i] = page end BuildFriendsPage(pages[1]) BuildWhoPage(pages[2]) BuildGuildPage(pages[3]) BuildRaidPage(pages[4]) table.insert(UISpecialFrames, "SFramesSocialUI") local scale = SFramesDB.socialScale or 1 if scale ~= 1 then f:SetScale(scale) end MainFrame = f f:Hide() end -------------------------------------------------------------------------------- -- Public API -------------------------------------------------------------------------------- function SUI:Toggle(tabIdx) if not initialized then return end if not MainFrame then BuildMainFrame() end if MainFrame:IsShown() then if tabIdx and tabIdx ~= currentMainTab then ShowPage(tabIdx) else MainFrame:Hide() end else MainFrame:Show() ShowPage(tabIdx or 1) end end function SUI:Show(tabIdx) if not initialized then return end if not MainFrame then BuildMainFrame() end MainFrame:Show() ShowPage(tabIdx or 1) end function SUI:Hide() if MainFrame then MainFrame:Hide() end end function SUI:IsShown() return MainFrame and MainFrame:IsShown() end -------------------------------------------------------------------------------- -- Initialize -------------------------------------------------------------------------------- function SUI:Initialize() if initialized then return end initialized = true guildHideOffline = SFramesDB.guildHideOffline or false BuildClassReverseLookup() HideBlizzardFriends() BuildMainFrame() local ef = CreateFrame("Frame", nil, UIParent) ef:RegisterEvent("FRIENDLIST_UPDATE") ef:RegisterEvent("IGNORELIST_UPDATE") ef:RegisterEvent("WHO_LIST_UPDATE") ef:RegisterEvent("GUILD_ROSTER_UPDATE") ef:RegisterEvent("RAID_ROSTER_UPDATE") ef:RegisterEvent("PARTY_MEMBERS_CHANGED") ef:SetScript("OnEvent", function() if event == "WHO_LIST_UPDATE" then local n, t = GetNumWhoResults() WhoDebug("收到 WHO_LIST_UPDATE! 结果=" .. tostring(n) .. " 总计=" .. tostring(t) .. " pending=" .. tostring(whoQueryPending)) local wasPending = whoQueryPending whoQueryPending = false if SetWhoToUI then SetWhoToUI(1) end if FriendsFrame and FriendsFrame:IsShown() then FriendsFrame:Hide() end if MainFrame and MainFrame:IsShown() and currentMainTab == 2 then WhoDebug("更新列表显示") SUI:UpdateWhoList() else WhoDebug("自动打开查询页显示结果") if not MainFrame then BuildMainFrame() end if MainFrame then MainFrame:Show() ShowPage(2) SUI:UpdateWhoList() end end return end if not MainFrame or not MainFrame:IsShown() then return end if event == "FRIENDLIST_UPDATE" or event == "IGNORELIST_UPDATE" then if currentMainTab == 1 then SUI:UpdateFriendsPage() end elseif event == "GUILD_ROSTER_UPDATE" then if currentMainTab == 3 then SUI:UpdateGuildList() end elseif event == "RAID_ROSTER_UPDATE" or event == "PARTY_MEMBERS_CHANGED" then if currentMainTab == 4 then SUI:UpdateRaidPage() end end end) end -------------------------------------------------------------------------------- -- Hook ToggleFriendsFrame -------------------------------------------------------------------------------- local origToggleFriendsFrame = ToggleFriendsFrame ToggleFriendsFrame = function(tab) if SFramesDB and SFramesDB.enableSocial == false then if origToggleFriendsFrame then if origFriendsFrameShow and FriendsFrame then FriendsFrame.Show = origFriendsFrameShow end origToggleFriendsFrame(tab) end return end SUI:Toggle(tab or 1) end if ShowFriends then origShowFriendsAPI = ShowFriends local origShowFriends = ShowFriends ShowFriends = function() if SFramesDB and SFramesDB.enableSocial == false then if origShowFriends then origShowFriends() end return end SUI:Show(1) end end -------------------------------------------------------------------------------- -- Hook SetItemRef: shift-click player name -> WHO query in our panel -------------------------------------------------------------------------------- do local origSetItemRef_SUI = SetItemRef SetItemRef = function(link, text, button) if link and IsShiftKeyDown and IsShiftKeyDown() then local playerName = nil if string.sub(link, 1, 7) == "player:" then playerName = string.sub(link, 8) local colonPos = string.find(playerName, ":") if colonPos then playerName = string.sub(playerName, 1, colonPos - 1) end end if playerName and playerName ~= "" then if SFramesDB and SFramesDB.enableSocial ~= false and initialized then WhoDebug("Shift点击玩家: " .. playerName .. ", 发起WHO查询") local query = "n-\"" .. playerName .. "\"" if not MainFrame then BuildMainFrame() end if MainFrame then MainFrame:Show() ShowPage(2) end if pages[2] and pages[2].editBox then pages[2].editBox:SetText(playerName) if pages[2].editBox.placeholder then pages[2].editBox.placeholder:Hide() end end SUI:ClearWhoList() DoSendWho(query) return end end end if origSetItemRef_SUI then origSetItemRef_SUI(link, text, button) end end end -------------------------------------------------------------------------------- -- Bootstrap -------------------------------------------------------------------------------- local bootstrap = CreateFrame("Frame", nil, UIParent) bootstrap:RegisterEvent("PLAYER_LOGIN") bootstrap:SetScript("OnEvent", function() if event == "PLAYER_LOGIN" then if SFramesDB.enableSocial == nil then SFramesDB.enableSocial = true end if SFramesDB.enableSocial ~= false then SUI:Initialize() end end end)