最近在链动小铺后台找货源时,有个感受比较明显:官方的搜索和筛选功能太少了。
原页面能搜商品,也能看到成本价、库存、商家、对接状态这些信息,但真正找货源时,我更关心的是这些问题:
- 哪些商品还有库存
- 我的成本价是不是足够低
- 哪些商品还没有对接
- 某个分类下面有哪些商品
- 能不能一次多看一些结果,而不是一直翻页
这些信息页面上其实都有,只是缺少更顺手的筛选方式。
我没有链动小铺前端和后端的修改权限,所以最后没有走“改页面源码”的路子,而是写了一个油猴脚本,在现有页面上加一个悬浮的增强筛选面板。
这个方案不算正式产品功能,但对自己找货源、比价格、看库存来说,确实能省不少时间。
为什么选油猴脚本
这种需求有几种做法。
如果能改官方系统,最稳的方式肯定是前端加筛选项,后端接口支持条件查询,然后按条件分页返回。
但这次没这个条件。
我能做的是在浏览器里增强当前页面。
Chrome 插件也可以做,但对这个需求来说有点重。油猴脚本更合适:
- 安装简单
- 修改后刷新页面就能验证
- 只针对一个页面生效
- 可以直接复用当前页面的登录态
- 不需要重新打包浏览器插件
所以最后我选择 Tampermonkey。
它的定位很清楚:不是替代官方功能,而是在自己浏览器里加一层辅助工具。
先确认页面到底怎么搜
在写脚本前,我先看了一下页面行为。
链动小铺这个页面路径是:
https://www.ldxp.cn/merchant/my_parent/source_square页面里有两个主要区域:
- 货源广场
- 商品搜索
商品搜索不是一打开就直接显示表格,而是先输入关键词,点击搜索后才会展示商品结果。
搜索时调用的是这个接口:
/merchantApi/MyParent/searchGoodsList请求参数大概是这样:
{
"current": 1,
"pageSize": 50,
"name": "",
"goods_type": "card",
"keywords": "GPT"
}这个接口返回的数据里,已经有脚本需要的大部分字段,比如商品名称、分类、代理价、我的成本价、库存、商家、是否已对接。
所以思路就很明确了:
先用原接口把搜索结果拉到浏览器本地,再在本地做二次筛选。
这里有一个限制要提前说清楚。
这个脚本不是后端筛选。
它只能筛已经拉到本地的数据。如果只拉了 5 页,那筛选范围就是这 5 页。想看更多,就需要把脚本里的“拉取页数”调大。
脚本做了哪些功能
最后做出来的辅助面板是一个悬浮窗。
它不插入原页面内容区,避免把官方页面布局撑乱。
目前主要功能有这些:
- 关键词搜索
- 商品类型筛选:卡密、知识、资源、权益
- 拉取多页结果
- 按我的成本价区间筛选
- 按库存筛选:全部、有库存、缺货
- 按状态筛选:全部、正常、未上架
- 按对接状态筛选:全部、已对接、未对接
- 按分类关键词筛选
- 按商家名称筛选
- 按我的成本价或库存排序
- 每页显示 50、100、200、1000、10000 条
- 首页、上一页、页码跳转、下一页、末页
- 面板支持拖动、调整大小、收起展开
最终效果大概是这样:

关键代码
油猴脚本头部先限制只在链动小铺对应页面生效。
// ==UserScript==
// @name 链动小铺货源商品增强筛选
// @namespace https://www.ldxp.cn/
// @version 0.4.1
// @description 在货源商品搜索页增加价格区间、库存、分类、对接状态等本地筛选,并支持一次拉取多页结果。
// @match https://www.ldxp.cn/merchant/my_parent/source_square*
// @grant none
// @run-at document-idle
// ==/UserScript==这里我用了 @grant none。
原因是这个脚本只请求当前站点自己的接口,不需要跨域,也不需要额外的 GM API。
读取当前登录态
原页面请求接口时会带 Merchant-Token。
脚本里没有把 token 写死,而是从当前页面的 localStorage 里读。
function getToken() {
try {
const raw = localStorage.getItem("auth-token");
if (!raw) return "";
const parsed = JSON.parse(raw);
return parsed && parsed.value ? parsed.value : "";
} catch (_) {
return "";
}
}然后用 fetch 调接口。
async function postJson(url, body) {
const token = getToken();
const response = await fetch(url, {
method: "POST",
credentials: "include",
headers: {
"Content-Type": "application/json",
Accept: "application/json, text/plain, */*",
...(token ? { "Merchant-Token": token } : {}),
},
body: JSON.stringify(body),
});
if (!response.ok) {
throw new Error(`请求失败:HTTP ${response.status}`);
}
const data = await response.json();
if (data.code !== 1) {
throw new Error(data.msg || "接口返回失败");
}
return data.data || {};
}这个地方一定不要为了省事,把自己的 cookie、token、session 直接写进脚本。
这些都是登录凭证,发文章或者发截图前也要检查一遍,避免泄露。
拉取多页结果
官方页面是分页搜索。
脚本为了能筛更多数据,会按设置的页数循环请求。
async function search() {
readControls();
if (!filters.keywords) {
setStatus("请输入关键词后再搜索。", true);
return;
}
state.loading = true;
state.raw = [];
state.filtered = [];
renderResults();
try {
let total = 0;
const pageSize = 50;
const maxPages = filters.maxPages;
for (let current = 1; current <= maxPages; current += 1) {
setStatus(`正在拉取第 ${current} / ${maxPages} 页...`, false);
const data = await postJson("/merchantApi/MyParent/searchGoodsList", {
current,
pageSize,
name: "",
goods_type: filters.goodsType,
keywords: filters.keywords,
});
const list = Array.isArray(data.list) ? data.list : [];
total = Number(data.total) || total;
state.raw.push(...list);
if (list.length < pageSize || state.raw.length >= total) break;
}
state.loading = false;
applyFilters();
setStatus(`已拉取 ${state.raw.length} 条,筛选后 ${state.filtered.length} 条。`, false);
} catch (error) {
state.loading = false;
setStatus(error.message || String(error), true);
renderResults();
}
}这里有两个参数容易混。
一个是拉取页数,决定从接口拿多少数据。
一个是每页显示,决定增强表格里一次展示多少条。
比如接口每页拉 50 条,拉取页数设置成 20,理论上最多会拉 1000 条。每页显示设置成 200,就会在脚本自己的表格里分 5 页展示。
成本价和我的成本价
这个地方我一开始处理得不够好。
为了做价格筛选,我最开始只显示了一个最低价。
但原页面的成本价其实是分代理等级展示的:
普通代理 125元
金牌代理 115元
核心代理 105元这个信息不能丢。
所以后面改成两列:
- 成本价:按普通代理、金牌代理、核心代理分行显示
- 我的成本价:显示当前账号实际成本价
function getAgentPrices(item) {
return [
["普通代理", item.agent_price1],
["金牌代理", item.agent_price2],
["核心代理", item.agent_price3],
]
.map(([label, value]) => [label, toNumber(value)])
.filter(([, value]) => value !== null && value >= 0);
}
function getMyCostPrice(item) {
const cost = toNumber(item.cost_price);
if (cost !== null) return cost;
const limited = toNumber(item.agent_price_limit);
if (limited !== null) return limited;
const prices = getAgentPrices(item).map(([, value]) => value);
return prices.length ? Math.min(...prices) : null;
}价格区间筛选按「我的成本价」来做。
因为找货源时,我真正关心的是自己当前账号拿到的成本,而不是其他代理等级的价格。
本地筛选逻辑
筛选逻辑放在浏览器本地执行。
function itemMatches(item) {
const minPrice = toNumber(filters.minPrice);
const maxPrice = toNumber(filters.maxPrice);
const price = getMyCostPrice(item);
const stock = getStock(item);
const category = getCategory(item).toLowerCase();
const merchant = getMerchant(item).toLowerCase();
if (minPrice !== null && (price === null || price < minPrice)) return false;
if (maxPrice !== null && (price === null || price > maxPrice)) return false;
if (filters.stock === "in" && !(stock === null || stock > 0)) return false;
if (filters.stock === "out" && stock !== 0) return false;
if (filters.status === "normal" && item.status !== 1) return false;
if (filters.status === "off" && item.status === 1) return false;
if (filters.connect === "yes" && !item.child) return false;
if (filters.connect === "no" && item.child) return false;
if (filters.category && !category.includes(filters.category.toLowerCase())) return false;
if (filters.merchant && !merchant.includes(filters.merchant.toLowerCase())) return false;
return true;
}这段代码没有什么高级技巧,但比较实用。
它解决的是一个很具体的问题:官方页面信息有了,但筛选入口不够。
收起功能踩的一个小坑
悬浮面板支持拖动和调整大小。
这里有个小坑:如果用户手动调整过面板大小,浏览器可能会给面板留下内联宽高。
这时如果只用 CSS class 做收起,可能会出现内容隐藏了,但白色外框还是原来那么大。
所以收起时我改成用 JS 显式保存和恢复尺寸。
function toggleCollapsed(panel, button) {
const collapsed = panel.classList.toggle("is-collapsed");
if (collapsed) {
const rect = panel.getBoundingClientRect();
panel.dataset.expandedWidth = panel.style.width || `${rect.width}px`;
panel.dataset.expandedHeight = panel.style.height || `${rect.height}px`;
panel.style.width = "300px";
panel.style.height = "";
panel.style.maxHeight = "56px";
panel.style.resize = "none";
panel.style.overflow = "hidden";
button.textContent = "展开";
return;
}
panel.style.width = panel.dataset.expandedWidth || "";
panel.style.height = panel.dataset.expandedHeight || "";
panel.style.maxHeight = "";
panel.style.resize = "";
panel.style.overflow = "";
button.textContent = "收起";
}这种外挂式页面增强,很多问题不是业务逻辑,而是和原页面布局、浏览器默认行为打交道。
使用方式
先安装 Tampermonkey。
然后新建脚本,把完整脚本内容粘进去保存。
刷新链动小铺货源页面:
https://www.ldxp.cn/merchant/my_parent/source_square页面右上角会出现「货源商品增强筛选」悬浮面板。
我的使用习惯是:
- 输入关键词,比如 GPT
- 选择商品类型,比如卡密
- 拉取页数先设置 10 或 20
- 点击增强搜索
- 按我的成本价、库存、对接状态继续筛
- 如果结果不够,再把拉取页数调大
评论0
暂时没有评论