-------------------------------------------------------------------------------- -- Nanami-UI: Merchant UI (Merchant.lua) -- Replaces MerchantFrame with Nanami-UI styled interface -- Tabs: Merchant / Buyback, 4x5 grid, batch buy, repair -------------------------------------------------------------------------------- SFrames = SFrames or {} SFrames.Merchant = {} local MUI = SFrames.Merchant SFramesDB = SFramesDB or {} -------------------------------------------------------------------------------- -- Theme (Pink Cat-Paw) -------------------------------------------------------------------------------- local T = SFrames.Theme:Extend({ moneyGold = { 1, 0.84, 0.0 }, moneySilver = { 0.78, 0.78, 0.78 }, moneyCopper = { 0.71, 0.43, 0.18 }, cantAfford = { 0.80, 0.20, 0.20 }, unusable = { 1.00, 0.25, 0.25 }, }) local QUALITY_COLORS = { [0] = { 0.62, 0.62, 0.62 }, [1] = { 1, 1, 1 }, [2] = { 0.12, 1, 0 }, [3] = { 0.0, 0.44, 0.87 }, [4] = { 0.64, 0.21, 0.93 }, [5] = { 1, 0.5, 0 }, } -------------------------------------------------------------------------------- -- Layout -------------------------------------------------------------------------------- local NUM_COLS = 4 local NUM_ROWS = 5 local ITEMS_PER_PAGE = NUM_COLS * NUM_ROWS local FRAME_W = 620 local HEADER_H = 34 local SIDE_PAD = 14 local ITEM_W = 138 local ITEM_H = 42 local ITEM_GAP_X = 6 local ITEM_GAP_Y = 4 local ICON_SIZE = 34 local BOTTOM_H = 50 local TAB_AREA_H = 28 local FRAME_H = HEADER_H + 6 + TAB_AREA_H + (ITEM_H + ITEM_GAP_Y) * NUM_ROWS + 6 + BOTTOM_H -------------------------------------------------------------------------------- -- State -------------------------------------------------------------------------------- local MainFrame = nil local ItemButtons = {} local CurrentPage = 1 local CurrentTab = 1 local BuyPopup = nil local ScanTip = nil -------------------------------------------------------------------------------- -- Helpers -------------------------------------------------------------------------------- local function GetFont() if SFrames and SFrames.GetFont then return SFrames:GetFont() end return "Fonts\\ARIALN.TTF" end local function SetRoundBackdrop(frame) 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 }, }) frame:SetBackdropColor(T.panelBg[1], T.panelBg[2], T.panelBg[3], T.panelBg[4]) frame:SetBackdropBorderColor(T.panelBorder[1], T.panelBorder[2], T.panelBorder[3], T.panelBorder[4]) 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: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.45) s:SetBackdropBorderColor(0, 0, 0, 0.6) s:SetFrameLevel(math.max(0, parent:GetFrameLevel() - 1)) return s end local function FormatMoney(copper) if not copper or copper <= 0 then return 0, 0, 0 end local g = math.floor(copper / 10000) local s = math.floor(math.mod(copper, 10000) / 100) local c = math.mod(copper, 100) return g, s, c end local function GetBuybackQualityColor(index) if not ScanTip then ScanTip = CreateFrame("GameTooltip", "SFramesMerchantScanTip", UIParent, "GameTooltipTemplate") ScanTip:SetOwner(UIParent, "ANCHOR_NONE") end ScanTip:ClearLines() ScanTip:SetBuybackItem(index) local textLine = _G["SFramesMerchantScanTipTextLeft1"] if textLine then local r, g, b = textLine:GetTextColor() if r and g and b then return r, g, b end end return nil end -------------------------------------------------------------------------------- -- Item Stack Helpers -------------------------------------------------------------------------------- local function GetMaxStack(link) if not link then return 1 end local _, _, _, _, _, _, ms = GetItemInfo(link) if ms and ms > 0 then return ms end return 1 end local function GetItemCountInBags(link) if not link then return 0 end local _, _, searchID = string.find(link, "item:(%d+)") if not searchID then return 0 end local total = 0 for bag = 0, 4 do local slots = GetContainerNumSlots(bag) for slot = 1, slots do local bagLink = GetContainerItemLink(bag, slot) if bagLink then local _, _, bagID = string.find(bagLink, "item:(%d+)") if bagID == searchID then local _, count = GetContainerItemInfo(bag, slot) total = total + (count or 1) end end end end return total end -------------------------------------------------------------------------------- -- Rounded Action Button Factory (for popups) -------------------------------------------------------------------------------- local function CreateRoundActionBtn(parent, text, w) local btn = CreateFrame("Button", nil, parent) btn:SetWidth(w or 100) btn:SetHeight(28) btn: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 }, }) btn:SetBackdropColor(T.btnBg[1], T.btnBg[2], T.btnBg[3], T.btnBg[4]) btn:SetBackdropBorderColor(T.btnBorder[1], T.btnBorder[2], T.btnBorder[3], T.btnBorder[4]) local fs = btn:CreateFontString(nil, "OVERLAY") fs:SetFont(GetFont(), 11, "OUTLINE") fs:SetPoint("CENTER", 0, 0) fs:SetText(text) fs:SetTextColor(T.btnText[1], T.btnText[2], T.btnText[3]) btn.label = fs btn.disabled = false function btn:SetDisabled(flag) self.disabled = flag if flag then self.label:SetTextColor(T.btnDisabledText[1], T.btnDisabledText[2], T.btnDisabledText[3]) self:SetBackdropColor(T.btnBg[1], T.btnBg[2], T.btnBg[3], 0.5) self:SetBackdropBorderColor(T.slotBorder[1], T.slotBorder[2], T.slotBorder[3], 0.5) else self.label:SetTextColor(T.btnText[1], T.btnText[2], T.btnText[3]) self:SetBackdropColor(T.btnBg[1], T.btnBg[2], T.btnBg[3], T.btnBg[4]) self:SetBackdropBorderColor(T.btnBorder[1], T.btnBorder[2], T.btnBorder[3], T.btnBorder[4]) end end btn:SetScript("OnEnter", function() if not this.disabled then this:SetBackdropColor(T.btnHoverBg[1], T.btnHoverBg[2], T.btnHoverBg[3], T.btnHoverBg[4]) this:SetBackdropBorderColor(T.btnHoverBd[1], T.btnHoverBd[2], T.btnHoverBd[3], T.btnHoverBd[4]) this.label:SetTextColor(T.btnActiveText[1], T.btnActiveText[2], T.btnActiveText[3]) end end) btn:SetScript("OnLeave", function() if not this.disabled then this:SetBackdropColor(T.btnBg[1], T.btnBg[2], T.btnBg[3], T.btnBg[4]) this:SetBackdropBorderColor(T.btnBorder[1], T.btnBorder[2], T.btnBorder[3], T.btnBorder[4]) this.label:SetTextColor(T.btnText[1], T.btnText[2], T.btnText[3]) end end) btn:SetScript("OnMouseDown", function() if not this.disabled then this:SetBackdropColor(T.btnDownBg[1], T.btnDownBg[2], T.btnDownBg[3], T.btnDownBg[4]) end end) btn:SetScript("OnMouseUp", function() if not this.disabled then this:SetBackdropColor(T.btnHoverBg[1], T.btnHoverBg[2], T.btnHoverBg[3], T.btnHoverBg[4]) end end) return btn end -------------------------------------------------------------------------------- -- Action Button Factory (matching TrainerUI / QuestUI) -------------------------------------------------------------------------------- local function CreateActionBtn(parent, text, w) local btn = CreateFrame("Button", nil, parent) btn:SetWidth(w or 100) btn:SetHeight(28) btn:SetBackdrop({ bgFile = "Interface\\Buttons\\WHITE8X8", edgeFile = "Interface\\Buttons\\WHITE8X8", tile = false, tileSize = 0, edgeSize = 1, insets = { left = 1, right = 1, top = 1, bottom = 1 }, }) btn:SetBackdropColor(T.btnBg[1], T.btnBg[2], T.btnBg[3], T.btnBg[4]) btn:SetBackdropBorderColor(T.btnBorder[1], T.btnBorder[2], T.btnBorder[3], T.btnBorder[4]) local fs = btn:CreateFontString(nil, "OVERLAY") fs:SetFont(GetFont(), 12, "OUTLINE") fs:SetPoint("CENTER", 0, 0) fs:SetText(text) fs:SetTextColor(T.btnText[1], T.btnText[2], T.btnText[3]) btn.label = fs btn.disabled = false function btn:SetDisabled(flag) self.disabled = flag if flag then self.label:SetTextColor(T.btnDisabledText[1], T.btnDisabledText[2], T.btnDisabledText[3]) self:SetBackdropColor(T.btnBg[1], T.btnBg[2], T.btnBg[3], 0.5) self:SetBackdropBorderColor(0.2, 0.15, 0.18, 0.5) else self.label:SetTextColor(T.btnText[1], T.btnText[2], T.btnText[3]) self:SetBackdropColor(T.btnBg[1], T.btnBg[2], T.btnBg[3], T.btnBg[4]) self:SetBackdropBorderColor(T.btnBorder[1], T.btnBorder[2], T.btnBorder[3], T.btnBorder[4]) end end btn:SetScript("OnEnter", function() if not this.disabled then this:SetBackdropColor(T.btnHoverBg[1], T.btnHoverBg[2], T.btnHoverBg[3], T.btnHoverBg[4]) this:SetBackdropBorderColor(T.btnHoverBd[1], T.btnHoverBd[2], T.btnHoverBd[3], T.btnHoverBd[4]) this.label:SetTextColor(T.btnActiveText[1], T.btnActiveText[2], T.btnActiveText[3]) end end) btn:SetScript("OnLeave", function() if not this.disabled then this:SetBackdropColor(T.btnBg[1], T.btnBg[2], T.btnBg[3], T.btnBg[4]) this:SetBackdropBorderColor(T.btnBorder[1], T.btnBorder[2], T.btnBorder[3], T.btnBorder[4]) this.label:SetTextColor(T.btnText[1], T.btnText[2], T.btnText[3]) end end) btn:SetScript("OnMouseDown", function() if not this.disabled then this:SetBackdropColor(T.btnDownBg[1], T.btnDownBg[2], T.btnDownBg[3], T.btnDownBg[4]) end end) btn:SetScript("OnMouseUp", function() if not this.disabled then this:SetBackdropColor(T.btnHoverBg[1], T.btnHoverBg[2], T.btnHoverBg[3], T.btnHoverBg[4]) end end) return btn end -------------------------------------------------------------------------------- -- Tab Button Factory -------------------------------------------------------------------------------- local function CreateTabBtn(parent, text, w) local btn = CreateFrame("Button", nil, parent) btn:SetWidth(w or 70) btn:SetHeight(22) btn:SetBackdrop({ bgFile = "Interface\\Buttons\\WHITE8X8", edgeFile = "Interface\\Buttons\\WHITE8X8", tile = false, tileSize = 0, edgeSize = 1, insets = { left = 1, right = 1, top = 1, bottom = 1 }, }) btn:SetBackdropColor(T.tabBg[1], T.tabBg[2], T.tabBg[3], T.tabBg[4]) btn:SetBackdropBorderColor(T.tabBorder[1], T.tabBorder[2], T.tabBorder[3], 0.5) local fs = btn:CreateFontString(nil, "OVERLAY") fs:SetFont(GetFont(), 12, "OUTLINE") fs:SetPoint("CENTER", 0, 0) fs:SetText(text) fs:SetTextColor(T.tabText[1], T.tabText[2], T.tabText[3]) btn.label = fs btn:SetScript("OnEnter", function() if not this.active then this:SetBackdropColor(T.btnHoverBg[1], T.btnHoverBg[2], T.btnHoverBg[3], T.btnHoverBg[4]) this:SetBackdropBorderColor(T.btnHoverBd[1], T.btnHoverBd[2], T.btnHoverBd[3], T.btnHoverBd[4]) end end) btn:SetScript("OnLeave", function() if this.active then this:SetBackdropColor(T.tabActiveBg[1], T.tabActiveBg[2], T.tabActiveBg[3], T.tabActiveBg[4]) this:SetBackdropBorderColor(T.tabActiveBorder[1], T.tabActiveBorder[2], T.tabActiveBorder[3], T.tabActiveBorder[4]) else this:SetBackdropColor(T.tabBg[1], T.tabBg[2], T.tabBg[3], T.tabBg[4]) this:SetBackdropBorderColor(T.tabBorder[1], T.tabBorder[2], T.tabBorder[3], 0.5) end end) function btn:SetActive(flag) self.active = flag if flag then self:SetBackdropColor(T.tabActiveBg[1], T.tabActiveBg[2], T.tabActiveBg[3], T.tabActiveBg[4]) self:SetBackdropBorderColor(T.tabActiveBorder[1], T.tabActiveBorder[2], T.tabActiveBorder[3], T.tabActiveBorder[4]) self.label:SetTextColor(T.tabActiveText[1], T.tabActiveText[2], T.tabActiveText[3]) else self:SetBackdropColor(T.tabBg[1], T.tabBg[2], T.tabBg[3], T.tabBg[4]) self:SetBackdropBorderColor(T.tabBorder[1], T.tabBorder[2], T.tabBorder[3], 0.5) self.label:SetTextColor(T.tabText[1], T.tabText[2], T.tabText[3]) end end return btn end -------------------------------------------------------------------------------- -- Buy Popup (Nanami themed, rounded corners, simple quantity input) -------------------------------------------------------------------------------- local function EnsureBuyPopup() if BuyPopup then return end BuyPopup = CreateFrame("Frame", "SFramesMerchantBuyPopup", UIParent) BuyPopup:SetWidth(240) BuyPopup:SetHeight(110) BuyPopup:SetPoint("CENTER", UIParent, "CENTER", 0, 60) BuyPopup:SetFrameStrata("DIALOG") SetRoundBackdrop(BuyPopup) CreateShadow(BuyPopup) BuyPopup:Hide() local font = GetFont() local titleFS = BuyPopup:CreateFontString(nil, "OVERLAY") titleFS:SetFont(font, 13, "OUTLINE") titleFS:SetPoint("TOP", BuyPopup, "TOP", 0, -12) titleFS:SetTextColor(T.gold[1], T.gold[2], T.gold[3]) titleFS:SetText("批量购买") BuyPopup.titleFS = titleFS local customLabel = BuyPopup:CreateFontString(nil, "OVERLAY") customLabel:SetFont(font, 11, "OUTLINE") customLabel:SetPoint("TOP", titleFS, "BOTTOM", -18, -10) customLabel:SetTextColor(T.dimText[1], T.dimText[2], T.dimText[3]) customLabel:SetText("购买数量:") local editbox = CreateFrame("EditBox", "SFramesMerchantBuyEditBox", BuyPopup) editbox:SetWidth(60) editbox:SetHeight(22) editbox:SetPoint("LEFT", customLabel, "RIGHT", 6, 0) editbox:SetBackdrop({ bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", tile = true, tileSize = 16, edgeSize = 12, insets = { left = 2, right = 2, top = 2, bottom = 2 }, }) editbox:SetBackdropColor(T.inputBg[1], T.inputBg[2], T.inputBg[3], T.inputBg[4] or 0.95) editbox:SetBackdropBorderColor(T.slotBorder[1], T.slotBorder[2], T.slotBorder[3], 0.8) editbox:SetFont(font, 12, "OUTLINE") editbox:SetTextColor(T.nameText[1], T.nameText[2], T.nameText[3]) editbox:SetJustifyH("CENTER") editbox:SetNumeric(true) editbox:SetAutoFocus(false) editbox:SetTextInsets(4, 4, 2, 2) local confirm = CreateRoundActionBtn(BuyPopup, "确定", 75) confirm:SetPoint("BOTTOMLEFT", BuyPopup, "BOTTOMLEFT", 28, 12) local cancel = CreateRoundActionBtn(BuyPopup, "取消", 75) cancel:SetPoint("BOTTOMRIGHT", BuyPopup, "BOTTOMRIGHT", -28, 12) BuyPopup.index = 1 BuyPopup.maxPurchase = 100 BuyPopup.itemLink = nil local function DoBuy() local qty = tonumber(editbox:GetText()) or 0 if qty > 0 then if qty > BuyPopup.maxPurchase then qty = BuyPopup.maxPurchase end MUI:BuyMultiple(BuyPopup.index, qty) end BuyPopup:Hide() end confirm:SetScript("OnClick", DoBuy) cancel:SetScript("OnClick", function() BuyPopup:Hide() end) editbox:SetScript("OnEnterPressed", DoBuy) editbox:SetScript("OnEscapePressed", function() BuyPopup:Hide() end) BuyPopup.ShowPopup = function(index, maxAfford, itemLink) BuyPopup.index = index BuyPopup.maxPurchase = math.min(9999, maxAfford) BuyPopup.itemLink = itemLink editbox:SetText("1") BuyPopup.titleFS:SetText("批量购买") BuyPopup:Show() editbox:SetFocus() editbox:HighlightText() end end -------------------------------------------------------------------------------- -- Batch Buy Logic -------------------------------------------------------------------------------- function MUI:BuyMultiple(index, totalAmount) if totalAmount <= 0 then return end if CurrentTab == 2 then BuybackItem(index) return end local name, _, price, batchQty, numAvailable = GetMerchantItemInfo(index) local batchSize = (batchQty and batchQty > 0) and batchQty or 1 local numPurchases = math.ceil(totalAmount / batchSize) if numAvailable > -1 and numPurchases > numAvailable then numPurchases = numAvailable end while numPurchases > 0 do local batch = numPurchases if batch > 255 then batch = 255 end BuyMerchantItem(index, batch) numPurchases = numPurchases - batch end end -------------------------------------------------------------------------------- -- Merchant Item Button Factory -------------------------------------------------------------------------------- local function CreateMerchantButton(parent, id) local btn = CreateFrame("Button", "SFramesMerchantItem" .. id, parent) btn:SetWidth(ITEM_W) btn:SetHeight(ITEM_H) local iconFrame = CreateFrame("Button", btn:GetName() .. "Icon", btn) iconFrame:SetWidth(ICON_SIZE) iconFrame:SetHeight(ICON_SIZE) iconFrame:SetPoint("LEFT", btn, "LEFT", 2, 0) iconFrame:RegisterForClicks("LeftButtonUp", "RightButtonUp") iconFrame:SetBackdrop({ bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", tile = true, tileSize = 16, edgeSize = 12, insets = { left = 2, right = 2, top = 2, bottom = 2 }, }) iconFrame:SetBackdropColor(T.slotBg[1], T.slotBg[2], T.slotBg[3], T.slotBg[4]) iconFrame:SetBackdropBorderColor(T.slotBorder[1], T.slotBorder[2], T.slotBorder[3], T.slotBorder[4]) btn.iconFrame = iconFrame local qualGlow = iconFrame:CreateTexture(nil, "OVERLAY") qualGlow:SetTexture("Interface\\Buttons\\UI-ActionButton-Border") qualGlow:SetBlendMode("ADD") qualGlow:SetAlpha(0.7) qualGlow:SetWidth(ICON_SIZE * 1.8) qualGlow:SetHeight(ICON_SIZE * 1.8) qualGlow:SetPoint("CENTER", iconFrame, "CENTER", 0, 0) qualGlow:Hide() btn.qualGlow = qualGlow function btn:SetQualityBorder(r, g, b) self.qualGlow:SetVertexColor(r, g, b) self.qualGlow:Show() self.iconFrame:SetBackdropBorderColor(r, g, b, 1) end function btn:ResetBorder() self.qualGlow:Hide() self.iconFrame:SetBackdropBorderColor(T.slotBorder[1], T.slotBorder[2], T.slotBorder[3], T.slotBorder[4]) end local icon = iconFrame:CreateTexture(nil, "ARTWORK") icon:SetTexCoord(0.08, 0.92, 0.08, 0.92) icon:SetPoint("TOPLEFT", iconFrame, "TOPLEFT", 3, -3) icon:SetPoint("BOTTOMRIGHT", iconFrame, "BOTTOMRIGHT", -3, 3) btn.icon = icon local font = GetFont() local nameFS = btn:CreateFontString(nil, "OVERLAY") nameFS:SetFont(font, 11, "OUTLINE") nameFS:SetPoint("TOPLEFT", iconFrame, "TOPRIGHT", 5, -1) nameFS:SetPoint("RIGHT", btn, "RIGHT", -2, 0) nameFS:SetJustifyH("LEFT") nameFS:SetTextColor(T.nameText[1], T.nameText[2], T.nameText[3]) btn.nameFS = nameFS btn.gTxt = btn:CreateFontString(nil, "OVERLAY") btn.gTxt:SetFont(font, 10, "OUTLINE") btn.gTex = btn:CreateTexture(nil, "ARTWORK") btn.gTex:SetTexture("Interface\\MoneyFrame\\UI-MoneyIcons") btn.gTex:SetTexCoord(0, 0.25, 0, 1) btn.gTex:SetWidth(11); btn.gTex:SetHeight(11) btn.sTxt = btn:CreateFontString(nil, "OVERLAY") btn.sTxt:SetFont(font, 10, "OUTLINE") btn.sTex = btn:CreateTexture(nil, "ARTWORK") btn.sTex:SetTexture("Interface\\MoneyFrame\\UI-MoneyIcons") btn.sTex:SetTexCoord(0.25, 0.5, 0, 1) btn.sTex:SetWidth(11); btn.sTex:SetHeight(11) btn.cTxt = btn:CreateFontString(nil, "OVERLAY") btn.cTxt:SetFont(font, 10, "OUTLINE") btn.cTex = btn:CreateTexture(nil, "ARTWORK") btn.cTex:SetTexture("Interface\\MoneyFrame\\UI-MoneyIcons") btn.cTex:SetTexCoord(0.5, 0.75, 0, 1) btn.cTex:SetWidth(11); btn.cTex:SetHeight(11) local countFS = iconFrame:CreateFontString(nil, "OVERLAY") countFS:SetFont("Fonts\\ARIALN.TTF", 11, "OUTLINE") countFS:SetPoint("BOTTOMRIGHT", iconFrame, "BOTTOMRIGHT", -2, 2) countFS:SetJustifyH("RIGHT") btn.countFS = countFS local highlight = btn:CreateTexture(nil, "HIGHLIGHT") highlight:SetTexture("Interface\\QuestFrame\\UI-QuestTitleHighlight") highlight:SetBlendMode("ADD") highlight:SetAllPoints(btn) highlight:SetAlpha(0.15) local function OnEnter() if not btn.itemIndex then return end btn.iconFrame:SetBackdropBorderColor(T.slotHover[1], T.slotHover[2], T.slotHover[3], T.slotHover[4]) GameTooltip:SetOwner(this, "ANCHOR_RIGHT") if CurrentTab == 1 then GameTooltip:SetMerchantItem(btn.itemIndex) ShowMerchantSellCursor(btn.itemIndex) local _, _, price = GetMerchantItemInfo(btn.itemIndex) if price and price > 0 then GameTooltip:AddLine(" ") GameTooltip:AddLine(">> 本店售价:", 1, 0.8, 0) SetTooltipMoney(GameTooltip, price) end else GameTooltip:SetBuybackItem(btn.itemIndex) local _, _, price = GetBuybackItemInfo(btn.itemIndex) if price and price > 0 then GameTooltip:AddLine(" ") GameTooltip:AddLine(">> 购回需花费:", 1, 0.5, 0) SetTooltipMoney(GameTooltip, price) end end local ttLink = (CurrentTab == 1) and GetMerchantItemLink(btn.itemIndex) or nil if not ttLink and CurrentTab == 2 and GetBuybackItemLink then ttLink = GetBuybackItemLink(btn.itemIndex) end if ttLink then local ms = GetMaxStack(ttLink) if ms and ms > 1 then GameTooltip:AddLine(" ") GameTooltip:AddLine("最大堆叠: " .. ms, 0.5, 0.8, 1) end end if IsControlKeyDown() then ShowInspectCursor() end GameTooltip:Show() end local function OnLeave() GameTooltip:Hide() ResetCursor() if btn._qualR then btn.iconFrame:SetBackdropBorderColor(btn._qualR, btn._qualG, btn._qualB, 1) else btn.iconFrame:SetBackdropBorderColor(T.slotBorder[1], T.slotBorder[2], T.slotBorder[3], T.slotBorder[4]) end end iconFrame:SetScript("OnEnter", OnEnter) iconFrame:SetScript("OnLeave", OnLeave) iconFrame:SetScript("OnUpdate", function() if GameTooltip:IsOwned(this) then if IsControlKeyDown() then if not this.controlDownLast then this.controlDownLast = true ShowInspectCursor() end else if this.controlDownLast then this.controlDownLast = false ResetCursor() end end end end) iconFrame:SetScript("OnClick", function() if not btn.itemIndex then return end if IsControlKeyDown() and arg1 == "LeftButton" then local link = GetMerchantItemLink(btn.itemIndex) if link and DressUpItemLink then DressUpItemLink(link) return end end if IsShiftKeyDown() then if ChatFrameEditBox and ChatFrameEditBox:IsVisible() then local link = nil if CurrentTab == 1 then link = GetMerchantItemLink(btn.itemIndex) end if link then ChatFrameEditBox:Insert(link) end else if CurrentTab == 1 then local name, _, price, quantity, numAvailable = GetMerchantItemInfo(btn.itemIndex) if not name then return end local popupLink = GetMerchantItemLink(btn.itemIndex) local batchSize = (quantity and quantity > 0) and quantity or 1 local maxCanAfford = 9999 if price and price > 0 then maxCanAfford = math.floor(GetMoney() / price) * batchSize end if numAvailable > -1 and numAvailable * batchSize < maxCanAfford then maxCanAfford = numAvailable * batchSize end if maxCanAfford < 1 then return end EnsureBuyPopup() BuyPopup.ShowPopup(btn.itemIndex, maxCanAfford, popupLink) else BuybackItem(btn.itemIndex) end end elseif arg1 == "RightButton" then if CurrentTab == 1 then BuyMerchantItem(btn.itemIndex, 1) else BuybackItem(btn.itemIndex) end end end) return btn end -------------------------------------------------------------------------------- -- Money Layout Helper -------------------------------------------------------------------------------- local function LayoutMoney(btn, copper, canBuy) btn.gTxt:Hide(); btn.gTex:Hide() btn.sTxt:Hide(); btn.sTex:Hide() btn.cTxt:Hide(); btn.cTex:Hide() local r, g, b if canBuy then r, g, b = T.bodyText[1], T.bodyText[2], T.bodyText[3] else r, g, b = T.cantAfford[1], T.cantAfford[2], T.cantAfford[3] end if copper == 0 then btn.cTxt:SetText("0") btn.cTxt:SetTextColor(r, g, b) btn.cTxt:ClearAllPoints() btn.cTxt:SetPoint("BOTTOMLEFT", btn.iconFrame, "BOTTOMRIGHT", 5, 2) btn.cTxt:Show() btn.cTex:ClearAllPoints() btn.cTex:SetPoint("LEFT", btn.cTxt, "RIGHT", 1, 0) btn.cTex:Show() return end local vG, vS, vC = FormatMoney(copper) local anchor = nil local function AttachPair(txt, tex, val, coinR, coinG, coinB) txt:SetText(val) txt:SetTextColor(canBuy and coinR or r, canBuy and coinG or g, canBuy and coinB or b) txt:ClearAllPoints() if not anchor then txt:SetPoint("BOTTOMLEFT", btn.iconFrame, "BOTTOMRIGHT", 5, 2) else txt:SetPoint("LEFT", anchor, "RIGHT", 4, 0) end txt:Show() tex:ClearAllPoints() tex:SetPoint("LEFT", txt, "RIGHT", 1, 0) tex:Show() anchor = tex end if vG > 0 then AttachPair(btn.gTxt, btn.gTex, vG, T.moneyGold[1], T.moneyGold[2], T.moneyGold[3]) end if vS > 0 then AttachPair(btn.sTxt, btn.sTex, vS, T.moneySilver[1], T.moneySilver[2], T.moneySilver[3]) end if vC > 0 then AttachPair(btn.cTxt, btn.cTex, vC, T.moneyCopper[1], T.moneyCopper[2], T.moneyCopper[3]) end end -------------------------------------------------------------------------------- -- Update -------------------------------------------------------------------------------- function MUI:Update() if not MainFrame or not MainFrame:IsVisible() then return end local numItems = (CurrentTab == 1) and GetMerchantNumItems() or GetNumBuybackItems() local name = UnitName("NPC") or "商人" if CurrentTab == 2 then name = name .. " - 购回" end MainFrame.npcNameFS:SetText(name) local totalPages = math.max(1, math.ceil(numItems / ITEMS_PER_PAGE)) if CurrentPage > totalPages then CurrentPage = totalPages end if CurrentPage < 1 then CurrentPage = 1 end MainFrame.pageText:SetText(string.format("第 %d / %d 页", CurrentPage, totalPages)) MainFrame.prevBtn:SetDisabled(CurrentPage <= 1) MainFrame.nextBtn:SetDisabled(CurrentPage >= totalPages) MainFrame.tabMerchant:SetActive(CurrentTab == 1) MainFrame.tabBuyback:SetActive(CurrentTab == 2) for i = 1, ITEMS_PER_PAGE do local btn = ItemButtons[i] local itemIndex = (CurrentPage - 1) * ITEMS_PER_PAGE + i if itemIndex <= numItems then local itemName, itemTexture, itemPrice, itemQuantity, numAvailable, isUsable local itemLink if CurrentTab == 1 then itemName, itemTexture, itemPrice, itemQuantity, numAvailable, isUsable = GetMerchantItemInfo(itemIndex) itemLink = GetMerchantItemLink(itemIndex) else itemName, itemTexture, itemPrice, itemQuantity, numAvailable, isUsable = GetBuybackItemInfo(itemIndex) if GetBuybackItemLink then itemLink = GetBuybackItemLink(itemIndex) end end btn.itemIndex = itemIndex btn.icon:SetTexture(itemTexture) btn.nameFS:SetText(itemName) btn._qualR = nil; btn._qualG = nil; btn._qualB = nil if CurrentTab == 2 and isUsable == nil then isUsable = true end local canAfford = (GetMoney() >= itemPrice) local isRequirementBlocked = (CurrentTab == 1 and not isUsable) local canBuy = (not isRequirementBlocked) and canAfford if canBuy then btn.icon:SetVertexColor(1, 1, 1) btn.nameFS:SetAlpha(1) else btn.icon:SetVertexColor(0.5, 0.2, 0.2) btn.nameFS:SetAlpha(0.6) end LayoutMoney(btn, itemPrice, canBuy) if CurrentTab == 1 and numAvailable and numAvailable > -1 then btn.countFS:SetText(numAvailable) if numAvailable == 0 then btn.countFS:SetTextColor(T.dimText[1], T.dimText[2], T.dimText[3]) else btn.countFS:SetTextColor(T.nameText[1], T.nameText[2], T.nameText[3]) end elseif itemQuantity and itemQuantity > 1 then btn.countFS:SetText(itemQuantity) btn.countFS:SetTextColor(T.nameText[1], T.nameText[2], T.nameText[3]) else btn.countFS:SetText("") end local qr, qg, qb local quality if itemLink then local _, _, q = GetItemInfo(itemLink) quality = q if not quality then local _, _, itemID = string.find(itemLink, "item:(%d+)") if itemID then _, _, quality = GetItemInfo("item:" .. itemID) end end if quality then qr, qg, qb = GetItemQualityColor(quality) else local _, _, hexColor = string.find(itemLink, "|c(%x%x%x%x%x%x%x%x)|") if hexColor then qr = tonumber(string.sub(hexColor, 3, 4), 16) / 255 qg = tonumber(string.sub(hexColor, 5, 6), 16) / 255 qb = tonumber(string.sub(hexColor, 7, 8), 16) / 255 end end elseif CurrentTab == 2 and itemName then qr, qg, qb = GetBuybackQualityColor(itemIndex) end if isRequirementBlocked then -- Level / weapon skill / class requirements not met: always mark red. btn.nameFS:SetTextColor(T.unusable[1], T.unusable[2], T.unusable[3]) btn.nameFS:SetAlpha(1) btn.icon:SetVertexColor(0.75, 0.25, 0.25) btn:SetQualityBorder(T.unusable[1], T.unusable[2], T.unusable[3]) btn._qualR = T.unusable[1]; btn._qualG = T.unusable[2]; btn._qualB = T.unusable[3] elseif qr then btn.nameFS:SetTextColor(qr, qg, qb) btn._qualR = qr; btn._qualG = qg; btn._qualB = qb if quality and quality > 1 then btn:SetQualityBorder(qr, qg, qb) else local isWhite = (qr > 0.95 and qg > 0.95 and qb > 0.95) local isGrey = (qr < 0.7 and qg < 0.7 and qb < 0.7) if not isWhite and not isGrey then btn:SetQualityBorder(qr, qg, qb) else btn:ResetBorder() btn._qualR = nil; btn._qualG = nil; btn._qualB = nil end end else btn.nameFS:SetTextColor(T.nameText[1], T.nameText[2], T.nameText[3]) btn:ResetBorder() end if CurrentTab == 1 and numAvailable and numAvailable == 0 then btn.icon:SetVertexColor(0.4, 0.4, 0.4) btn.nameFS:SetTextColor(T.dimText[1], T.dimText[2], T.dimText[3]) btn.nameFS:SetAlpha(0.5) btn:ResetBorder() btn._qualR = nil; btn._qualG = nil; btn._qualB = nil LayoutMoney(btn, itemPrice, false) end btn:Show() else btn.itemIndex = nil btn._qualR = nil; btn._qualG = nil; btn._qualB = nil btn:ResetBorder() btn:Hide() end end if CanMerchantRepair() then MainFrame.repairAllBtn:Show() MainFrame.repairItemBtn:Show() local repairCost, canRepair = GetRepairAllCost() if canRepair and repairCost > 0 and (GetMoney() >= repairCost) then MainFrame.repairAllBtn.canClick = true if MainFrame.repairAllBtn.iconTex then MainFrame.repairAllBtn.iconTex:SetVertexColor(1, 1, 1) end else MainFrame.repairAllBtn.canClick = false if MainFrame.repairAllBtn.iconTex then MainFrame.repairAllBtn.iconTex:SetVertexColor(0.5, 0.5, 0.5) end end else MainFrame.repairAllBtn:Hide() MainFrame.repairItemBtn:Hide() end end -------------------------------------------------------------------------------- -- Initialize -------------------------------------------------------------------------------- function MUI:Initialize() if MainFrame then return end MainFrame = CreateFrame("Frame", "SFramesMerchantFrame", UIParent) MainFrame:SetWidth(FRAME_W) MainFrame:SetHeight(FRAME_H) MainFrame:SetPoint("TOPLEFT", UIParent, "TOPLEFT", 12, -104) MainFrame:SetFrameStrata("HIGH") MainFrame:SetToplevel(true) MainFrame:EnableMouse(true) MainFrame:SetMovable(true) MainFrame:RegisterForDrag("LeftButton") MainFrame:SetScript("OnDragStart", function() this:StartMoving() end) MainFrame:SetScript("OnDragStop", function() this:StopMovingOrSizing() end) SetRoundBackdrop(MainFrame) CreateShadow(MainFrame) -- Header local header = CreateFrame("Frame", nil, MainFrame) header:SetPoint("TOPLEFT", MainFrame, "TOPLEFT", 0, 0) header:SetPoint("TOPRIGHT", MainFrame, "TOPRIGHT", 0, 0) header:SetHeight(HEADER_H) header:SetBackdrop({ bgFile = "Interface\\Buttons\\WHITE8X8" }) header:SetBackdropColor(T.headerBg[1], T.headerBg[2], T.headerBg[3], T.headerBg[4]) local titleIco = SFrames:CreateIcon(header, "merchant", 16) titleIco:SetDrawLayer("OVERLAY") titleIco:SetPoint("LEFT", header, "LEFT", SIDE_PAD, 0) titleIco:SetVertexColor(T.gold[1], T.gold[2], T.gold[3]) local npcNameFS = header:CreateFontString(nil, "OVERLAY") npcNameFS:SetFont(GetFont(), 14, "OUTLINE") npcNameFS:SetPoint("LEFT", titleIco, "RIGHT", 5, 0) npcNameFS:SetPoint("RIGHT", header, "RIGHT", -30, 0) npcNameFS:SetJustifyH("LEFT") npcNameFS:SetTextColor(T.gold[1], T.gold[2], T.gold[3]) MainFrame.npcNameFS = npcNameFS local closeBtn = CreateFrame("Button", nil, header) closeBtn:SetWidth(20); closeBtn:SetHeight(20) closeBtn:SetPoint("RIGHT", header, "RIGHT", -8, 0) local closeTex = closeBtn:CreateTexture(nil, "ARTWORK") closeTex:SetTexture("Interface\\AddOns\\Nanami-UI\\img\\icon") closeTex:SetTexCoord(0.25, 0.375, 0, 0.125) closeTex:SetAllPoints() closeTex:SetVertexColor(T.dimText[1], T.dimText[2], T.dimText[3]) closeBtn:SetScript("OnClick", function() MainFrame:Hide() end) closeBtn:SetScript("OnEnter", function() closeTex:SetVertexColor(1, 0.6, 0.7) end) closeBtn:SetScript("OnLeave", function() closeTex:SetVertexColor(T.dimText[1], T.dimText[2], T.dimText[3]) end) local headerSep = MainFrame:CreateTexture(nil, "ARTWORK") headerSep:SetTexture("Interface\\Buttons\\WHITE8X8") headerSep:SetHeight(1) headerSep:SetPoint("TOPLEFT", MainFrame, "TOPLEFT", 6, -HEADER_H) headerSep:SetPoint("TOPRIGHT", MainFrame, "TOPRIGHT", -6, -HEADER_H) headerSep:SetVertexColor(T.divider[1], T.divider[2], T.divider[3], T.divider[4]) -- Tabs local tabMerchant = CreateTabBtn(MainFrame, "商人", 60) tabMerchant:SetPoint("TOPLEFT", MainFrame, "TOPLEFT", SIDE_PAD, -(HEADER_H + 6)) tabMerchant:SetScript("OnClick", function() CurrentTab = 1 CurrentPage = 1 MUI:Update() end) MainFrame.tabMerchant = tabMerchant local tabBuyback = CreateTabBtn(MainFrame, "购回", 60) tabBuyback:SetPoint("LEFT", tabMerchant, "RIGHT", 4, 0) tabBuyback:SetScript("OnClick", function() CurrentTab = 2 CurrentPage = 1 MUI:Update() end) MainFrame.tabBuyback = tabBuyback -- Item Grid local gridTop = HEADER_H + 6 + 22 + 6 for i = 1, ITEMS_PER_PAGE do local btn = CreateMerchantButton(MainFrame, i) local row = math.floor((i - 1) / NUM_COLS) local col = math.mod((i - 1), NUM_COLS) btn:SetPoint("TOPLEFT", MainFrame, "TOPLEFT", SIDE_PAD + col * (ITEM_W + ITEM_GAP_X), -(gridTop + row * (ITEM_H + ITEM_GAP_Y))) ItemButtons[i] = btn end -- Bottom bar separator local bottomSep = MainFrame:CreateTexture(nil, "ARTWORK") bottomSep:SetTexture("Interface\\Buttons\\WHITE8X8") bottomSep:SetHeight(1) bottomSep:SetPoint("BOTTOMLEFT", MainFrame, "BOTTOMLEFT", 6, BOTTOM_H) bottomSep:SetPoint("BOTTOMRIGHT", MainFrame, "BOTTOMRIGHT", -6, BOTTOM_H) bottomSep:SetVertexColor(T.divider[1], T.divider[2], T.divider[3], T.divider[4]) -- Page Navigation local prevBtn = CreateActionBtn(MainFrame, "<", 28) prevBtn:SetHeight(22) prevBtn:SetPoint("BOTTOMLEFT", MainFrame, "BOTTOMLEFT", SIDE_PAD, 14) prevBtn:SetScript("OnClick", function() CurrentPage = CurrentPage - 1 MUI:Update() end) MainFrame.prevBtn = prevBtn local nextBtn = CreateActionBtn(MainFrame, ">", 28) nextBtn:SetHeight(22) nextBtn:SetPoint("LEFT", prevBtn, "RIGHT", 4, 0) nextBtn:SetScript("OnClick", function() CurrentPage = CurrentPage + 1 MUI:Update() end) MainFrame.nextBtn = nextBtn local pageText = MainFrame:CreateFontString(nil, "OVERLAY") pageText:SetFont(GetFont(), 11, "OUTLINE") pageText:SetPoint("LEFT", nextBtn, "RIGHT", 8, 0) pageText:SetTextColor(T.dimText[1], T.dimText[2], T.dimText[3]) MainFrame.pageText = pageText -- Repair Buttons local repairAllBtn = CreateFrame("Button", nil, MainFrame) repairAllBtn:SetWidth(32); repairAllBtn:SetHeight(32) repairAllBtn:SetPoint("BOTTOMRIGHT", MainFrame, "BOTTOMRIGHT", -(SIDE_PAD), 10) repairAllBtn:SetBackdrop({ bgFile = "Interface\\Buttons\\WHITE8X8", edgeFile = "Interface\\Buttons\\WHITE8X8", tile = false, tileSize = 0, edgeSize = 1, insets = { left = 1, right = 1, top = 1, bottom = 1 }, }) repairAllBtn:SetBackdropColor(T.btnBg[1], T.btnBg[2], T.btnBg[3], T.btnBg[4]) repairAllBtn:SetBackdropBorderColor(T.btnBorder[1], T.btnBorder[2], T.btnBorder[3], 0.6) local repairAllIcon = repairAllBtn:CreateTexture(nil, "ARTWORK") repairAllIcon:SetTexCoord(0.08, 0.92, 0.08, 0.92) repairAllIcon:SetPoint("TOPLEFT", 3, -3) repairAllIcon:SetPoint("BOTTOMRIGHT", -3, 3) repairAllIcon:SetTexture("Interface\\Icons\\Trade_BlackSmithing") repairAllBtn.iconTex = repairAllIcon repairAllBtn:SetScript("OnClick", function() if this.canClick then RepairAllItems() end end) repairAllBtn:SetScript("OnEnter", function() this:SetBackdropColor(T.btnHoverBg[1], T.btnHoverBg[2], T.btnHoverBg[3], T.btnHoverBg[4]) this:SetBackdropBorderColor(T.btnHoverBd[1], T.btnHoverBd[2], T.btnHoverBd[3], T.btnHoverBd[4]) GameTooltip:SetOwner(this, "ANCHOR_RIGHT") GameTooltip:SetText("修理所有物品", 1, 1, 1) local repairCost, canRepair = GetRepairAllCost() if canRepair and repairCost > 0 then SetTooltipMoney(GameTooltip, repairCost) end GameTooltip:Show() end) repairAllBtn:SetScript("OnLeave", function() this:SetBackdropColor(T.btnBg[1], T.btnBg[2], T.btnBg[3], T.btnBg[4]) this:SetBackdropBorderColor(T.btnBorder[1], T.btnBorder[2], T.btnBorder[3], 0.6) GameTooltip:Hide() end) MainFrame.repairAllBtn = repairAllBtn local repairItemBtn = CreateFrame("Button", nil, MainFrame) repairItemBtn:SetWidth(32); repairItemBtn:SetHeight(32) repairItemBtn:SetPoint("RIGHT", repairAllBtn, "LEFT", -6, 0) repairItemBtn:SetBackdrop({ bgFile = "Interface\\Buttons\\WHITE8X8", edgeFile = "Interface\\Buttons\\WHITE8X8", tile = false, tileSize = 0, edgeSize = 1, insets = { left = 1, right = 1, top = 1, bottom = 1 }, }) repairItemBtn:SetBackdropColor(T.btnBg[1], T.btnBg[2], T.btnBg[3], T.btnBg[4]) repairItemBtn:SetBackdropBorderColor(T.btnBorder[1], T.btnBorder[2], T.btnBorder[3], 0.6) local repairItemIcon = repairItemBtn:CreateTexture(nil, "ARTWORK") repairItemIcon:SetTexCoord(0.08, 0.92, 0.08, 0.92) repairItemIcon:SetPoint("TOPLEFT", 3, -3) repairItemIcon:SetPoint("BOTTOMRIGHT", -3, 3) repairItemIcon:SetTexture("Interface\\Icons\\INV_Hammer_20") repairItemBtn.iconTex = repairItemIcon repairItemBtn:SetScript("OnClick", function() if InRepairMode() then HideRepairCursor() else ShowRepairCursor() end end) repairItemBtn:SetScript("OnEnter", function() this:SetBackdropColor(T.btnHoverBg[1], T.btnHoverBg[2], T.btnHoverBg[3], T.btnHoverBg[4]) this:SetBackdropBorderColor(T.btnHoverBd[1], T.btnHoverBd[2], T.btnHoverBd[3], T.btnHoverBd[4]) GameTooltip:SetOwner(this, "ANCHOR_RIGHT") GameTooltip:SetText("修理一件物品", 1, 1, 1) GameTooltip:Show() end) repairItemBtn:SetScript("OnLeave", function() this:SetBackdropColor(T.btnBg[1], T.btnBg[2], T.btnBg[3], T.btnBg[4]) this:SetBackdropBorderColor(T.btnBorder[1], T.btnBorder[2], T.btnBorder[3], 0.6) GameTooltip:Hide() end) MainFrame.repairItemBtn = repairItemBtn -- Events MainFrame:EnableMouseWheel(true) MainFrame:SetScript("OnMouseWheel", function() local numItems = (CurrentTab == 1) and GetMerchantNumItems() or GetNumBuybackItems() local totalPages = math.max(1, math.ceil(numItems / ITEMS_PER_PAGE)) if arg1 > 0 then if CurrentPage > 1 then CurrentPage = CurrentPage - 1 MUI:Update() end elseif arg1 < 0 then if CurrentPage < totalPages then CurrentPage = CurrentPage + 1 MUI:Update() end end end) MainFrame:SetScript("OnHide", function() pcall(CloseMerchant) end) MainFrame:SetScript("OnEvent", function() if event == "MERCHANT_SHOW" then if SFramesDB and SFramesDB.enableMerchant == false then return end CurrentTab = 1 CurrentPage = 1 MainFrame:Show() MUI:Update() elseif event == "MERCHANT_UPDATE" then if MainFrame:IsVisible() then MUI:Update() end elseif event == "MERCHANT_CLOSED" then MainFrame:Hide() end end) MainFrame:RegisterEvent("MERCHANT_SHOW") MainFrame:RegisterEvent("MERCHANT_UPDATE") MainFrame:RegisterEvent("MERCHANT_CLOSED") MainFrame:Hide() if MerchantFrame then MerchantFrame:UnregisterEvent("MERCHANT_SHOW") end tinsert(UISpecialFrames, "SFramesMerchantFrame") end -------------------------------------------------------------------------------- -- Bootstrap -------------------------------------------------------------------------------- local f = CreateFrame("Frame") f:RegisterEvent("PLAYER_LOGIN") f:SetScript("OnEvent", function() if event == "PLAYER_LOGIN" then if SFramesDB and SFramesDB.enableMerchant == nil then SFramesDB.enableMerchant = true end if SFramesDB and SFramesDB.enableMerchant ~= false then MUI:Initialize() end end end)