index.js 64 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611
  1. import Awesome from '../background/awesome.js'
  2. import MSG_TYPE from '../static/js/common.js';
  3. import Settings from './settings.js';
  4. import Statistics from '../background/statistics.js';
  5. // 工具分类定义
  6. const TOOL_CATEGORIES = [
  7. { key: 'dev', name: '开发工具类', tools: ['json-format', 'json-diff', 'code-beautify', 'code-compress', 'postman', 'websocket', 'regexp','page-timing'] },
  8. { key: 'encode', name: '编解码转换类', tools: ['en-decode', 'trans-radix', 'timestamp', 'trans-color'] },
  9. { key: 'image', name: '图像处理类', tools: ['qr-code', 'image-base64', 'svg-converter', 'chart-maker', 'poster-maker' ,'screenshot', 'color-picker'] },
  10. { key: 'productivity', name: '效率工具类', tools: ['aiagent', 'sticky-notes', 'html2markdown', 'page-monkey'] },
  11. { key: 'calculator', name: '计算工具类', tools: ['crontab', 'loan-rate', 'password'] },
  12. { key: 'other', name: '其他工具', tools: [] }
  13. ];
  14. // Vue实例
  15. new Vue({
  16. el: '#marketContainer',
  17. data: {
  18. manifest: { version: '0.0.0' },
  19. searchKey: '',
  20. currentCategory: '',
  21. sortType: 'default',
  22. viewMode: 'list', // 默认网格视图
  23. categories: TOOL_CATEGORIES,
  24. favorites: new Set(),
  25. recentUsed: [],
  26. loading: true,
  27. originalTools: {}, // 保存原始工具数据
  28. currentView: 'all', // 当前视图类型(all/installed/favorites/recent)
  29. activeTools: {}, // 当前显示的工具列表
  30. installedCount: 0, // 已安装工具数量
  31. // 版本相关
  32. latestVersion: '', // 最新版本号
  33. needUpdate: false, // 是否需要更新
  34. // 设置相关
  35. showSettingsModal: false,
  36. defaultKey: 'Alt+Shift+J', // 默认快捷键
  37. countDown: 0, // 夜间模式倒计时
  38. selectedOpts: [], // 选中的选项(已支持FORBID_STATISTICS)
  39. menuDownloadCrx: false, // 菜单-插件下载
  40. menuFeHelperSeting: false, // 菜单-FeHelper设置
  41. isFirefox: false, // 是否Firefox浏览器
  42. // 打赏相关
  43. showDonateModal: false,
  44. donate: {
  45. text: '感谢你对FeHelper的认可和支持!',
  46. image: './donate.jpeg'
  47. },
  48. // 确认对话框
  49. confirmDialog: {
  50. show: false,
  51. title: '操作确认',
  52. message: '',
  53. callback: null,
  54. data: null
  55. },
  56. // 工具排序相关
  57. sortableTools: [], // 可排序的工具列表
  58. draggedIndex: -1, // 拖拽的工具索引
  59. recentCount: 0,
  60. versionChecked: false,
  61. // 推荐卡片配置,后续可从服务端获取
  62. recommendationCards: [
  63. {
  64. toolKey: 'qr-code',
  65. icon: '📱',
  66. title: '二维码工具',
  67. desc: '快速生成和识别二维码,支持自定义样式',
  68. tag: '必装',
  69. tagClass: 'must-tag',
  70. isAd: false
  71. },
  72. {
  73. toolKey: 'chart-maker',
  74. icon: '📊',
  75. title: '图表制作工具',
  76. desc: '支持多种数据可视化图表,快速生成专业图表',
  77. tag: '最新',
  78. tagClass: 'new-tag',
  79. isAd: false
  80. },
  81. {
  82. toolKey: 'mock-data',
  83. icon: '🎲',
  84. title: '数据Mock工具',
  85. desc: '快速生成各种测试数据,支持快速模板一键生成',
  86. tag: '推荐',
  87. tagClass: 'recommend-tag',
  88. isAd: false
  89. },
  90. {
  91. icon: '🔔',
  92. title: '推广位',
  93. desc: '广告位招租,欢迎流量主联系,开放合作,流量主请到github联系',
  94. tag: '广告',
  95. tagClass: 'ad-tag',
  96. isAd: true,
  97. url: 'https://github.com/zxlie/FeHelper'
  98. }
  99. ],
  100. },
  101. async created() {
  102. // 1. 读取URL中的query参数并赋值给searchKey
  103. try {
  104. const urlParams = new URLSearchParams(window.location.search);
  105. const query = urlParams.get('query');
  106. if (query) {
  107. this.searchKey = query;
  108. }
  109. } catch (e) {
  110. // 忽略异常
  111. }
  112. // 2. 初始化数据
  113. await this.initData();
  114. this.recentCount = (await Statistics.getRecentUsedTools(10)).length;
  115. // 初始化后更新已安装工具数量
  116. this.updateInstalledCount();
  117. // 恢复用户的视图模式设置
  118. this.loadViewMode();
  119. // 加载设置项
  120. this.loadSettings();
  121. // 检查浏览器类型
  122. this.checkBrowserType();
  123. // 检查版本更新
  124. this.checkVersionUpdate();
  125. // 加载远程推荐卡片配置
  126. this.loadRemoteRecommendationCards();
  127. // 检查URL中是否有donate_from参数
  128. this.checkDonateParam();
  129. // 页面加载时自动获取并注入options页面的补丁
  130. this.loadPatchHotfix();
  131. // 埋点:自动触发options
  132. chrome.runtime.sendMessage({
  133. type: 'fh-dynamic-any-thing',
  134. thing: 'statistics-tool-usage',
  135. params: {
  136. tool_name: 'options'
  137. }
  138. });
  139. },
  140. computed: {
  141. filteredTools() {
  142. if (this.loading) {
  143. return [];
  144. }
  145. // 获取当前工具列表
  146. let result = Object.values(this.activeTools).map(tool => ({
  147. ...tool,
  148. favorite: this.favorites.has(tool.key)
  149. }));
  150. // 搜索过滤
  151. if (this.searchKey) {
  152. const key = this.searchKey.toLowerCase();
  153. result = result.filter(tool =>
  154. tool.name.toLowerCase().includes(key) ||
  155. tool.tips.toLowerCase().includes(key)
  156. );
  157. }
  158. // 分类过滤,在所有视图下生效
  159. if (this.currentCategory) {
  160. const category = TOOL_CATEGORIES.find(c => c.key === this.currentCategory);
  161. const categoryTools = category ? category.tools : [];
  162. result = result.filter(tool => categoryTools.includes(tool.key));
  163. }
  164. // 排序
  165. switch (this.sortType) {
  166. case 'newest':
  167. result.sort((a, b) => (b.updateTime || 0) - (a.updateTime || 0));
  168. break;
  169. case 'hot':
  170. result.sort((a, b) => (b.updateTime || 0) - (a.updateTime || 0));
  171. break;
  172. default:
  173. const allTools = TOOL_CATEGORIES.reduce((acc, category) => {
  174. acc.push(...category.tools);
  175. return acc;
  176. }, []);
  177. result.sort((a, b) => {
  178. const indexA = allTools.indexOf(a.key);
  179. const indexB = allTools.indexOf(b.key);
  180. // 如果工具不在任何类别中,放到最后
  181. if (indexA === -1 && indexB === -1) {
  182. return a.key.localeCompare(b.key); // 字母顺序排序
  183. }
  184. if (indexA === -1) return 1;
  185. if (indexB === -1) return -1;
  186. return indexA - indexB;
  187. });
  188. }
  189. return result;
  190. }
  191. },
  192. methods: {
  193. async initData() {
  194. try {
  195. this.loading = true;
  196. // 获取manifest信息
  197. const manifest = await chrome.runtime.getManifest();
  198. this.manifest = manifest;
  199. // 从 Awesome.getAllTools 获取工具列表
  200. const tools = await Awesome.getAllTools();
  201. // 获取收藏数据
  202. const favorites = await this.getFavoritesData();
  203. this.favorites = new Set(favorites);
  204. // 获取最近使用数据
  205. const recentUsed = await this.getRecentUsedData();
  206. this.recentUsed = recentUsed;
  207. this.recentCount = recentUsed.length;
  208. // 获取已安装工具列表
  209. const installedTools = await Awesome.getInstalledTools();
  210. // 处理工具数据
  211. const processedTools = {};
  212. Object.entries(tools).forEach(([key, tool]) => {
  213. // 检查工具是否已安装
  214. const isInstalled = installedTools.hasOwnProperty(key);
  215. // 检查是否有右键菜单
  216. const hasMenu = tool.menu || false;
  217. processedTools[key] = {
  218. ...tool,
  219. key, // 添加key到工具对象中
  220. updateTime: Date.now() - Math.floor(Math.random() * 30) * 24 * 60 * 60 * 1000,
  221. installed: isInstalled, // 使用实时安装状态
  222. inContextMenu: hasMenu, // 使用实时菜单状态
  223. systemInstalled: tool.systemInstalled || false, // 是否系统预装
  224. favorite: this.favorites.has(key)
  225. };
  226. });
  227. this.originalTools = processedTools;
  228. // 初始化activeTools为所有工具
  229. this.activeTools = { ...processedTools };
  230. // 更新"其他工具"类别
  231. this.updateOtherCategory(Object.keys(processedTools));
  232. // 默认选中"全部分类"
  233. this.currentCategory = '';
  234. } catch (error) {
  235. console.error('初始化数据失败:', error);
  236. } finally {
  237. this.loading = false;
  238. }
  239. },
  240. // 更新"其他工具"类别,将未分类的工具添加到此类别
  241. updateOtherCategory(allToolKeys) {
  242. // 获取所有已分类的工具
  243. const categorizedTools = new Set();
  244. TOOL_CATEGORIES.forEach(category => {
  245. if (category.key !== 'other') {
  246. category.tools.forEach(tool => categorizedTools.add(tool));
  247. }
  248. });
  249. // 找出未分类的工具
  250. const uncategorizedTools = allToolKeys.filter(key => !categorizedTools.has(key));
  251. // 更新"其他工具"类别
  252. const otherCategory = TOOL_CATEGORIES.find(category => category.key === 'other');
  253. if (otherCategory) {
  254. otherCategory.tools = uncategorizedTools;
  255. }
  256. },
  257. // 检查版本更新
  258. async checkVersionUpdate() {
  259. try {
  260. // 获取已安装的版本号
  261. const currentVersion = this.manifest.version;
  262. // 尝试从本地存储获取最新版本信息,避免频繁请求
  263. const cachedData = await new Promise(resolve => {
  264. chrome.storage.local.get('fehelper_latest_version_data', data => {
  265. resolve(data.fehelper_latest_version_data || null);
  266. });
  267. });
  268. // 检查是否需要重新获取版本信息:
  269. // 1. 缓存不存在
  270. // 2. 缓存已过期(超过24小时)
  271. // 3. 缓存的当前版本与实际版本不同(说明插件已更新)
  272. const now = Date.now();
  273. const cacheExpired = !cachedData || !cachedData.timestamp || (now - cachedData.timestamp > 24 * 60 * 60 * 1000);
  274. const versionChanged = cachedData && cachedData.currentVersion !== currentVersion;
  275. this.versionChecked = !(cacheExpired || versionChanged);
  276. if (!this.versionChecked) {
  277. try {
  278. // 使用shields.io的JSON API获取最新版本号
  279. const response = await fetch('https://img.shields.io/chrome-web-store/v/pkgccpejnmalmdinmhkkfafefagiiiad.json');
  280. if (!response.ok) {
  281. throw new Error(`HTTP错误:${response.status}`);
  282. }
  283. this.versionChecked = true;
  284. const data = await response.json();
  285. // 提取版本号 - shields.io返回的数据中包含版本信息
  286. let latestVersion = '';
  287. if (data && data.value) {
  288. // 去掉版本号前的'v'字符(如果有)
  289. latestVersion = data.value.replace(/^v/, '');
  290. }
  291. // 比较版本号
  292. const needUpdate = this.compareVersions(currentVersion, latestVersion) < 0;
  293. // 保存到本地存储中
  294. await chrome.storage.local.set({
  295. 'fehelper_latest_version_data': {
  296. timestamp: now,
  297. currentVersion, // 保存当前检查时的版本号
  298. latestVersion,
  299. needUpdate
  300. }
  301. });
  302. this.latestVersion = latestVersion;
  303. this.needUpdate = needUpdate;
  304. } catch (fetchError) {
  305. // 获取失败时不显示更新按钮
  306. this.needUpdate = false;
  307. // 如果是版本变更导致的重新检查,但获取失败,则使用缓存数据
  308. if (versionChanged && cachedData) {
  309. this.latestVersion = cachedData.latestVersion || '';
  310. // 比较新的currentVersion和缓存的latestVersion
  311. this.needUpdate = this.compareVersions(currentVersion, cachedData.latestVersion) < 0;
  312. }
  313. }
  314. } else {
  315. // 使用缓存数据
  316. this.latestVersion = cachedData.latestVersion || '';
  317. this.needUpdate = cachedData.needUpdate || false;
  318. }
  319. } catch (error) {
  320. this.needUpdate = false; // 出错时不显示更新提示
  321. }
  322. },
  323. // 比较版本号:如果v1 < v2返回-1,v1 = v2返回0,v1 > v2返回1
  324. compareVersions(v1, v2) {
  325. // 将版本号拆分为数字数组
  326. const v1Parts = v1.split('.').map(Number);
  327. const v2Parts = v2.split('.').map(Number);
  328. // 计算两个版本号中较长的长度
  329. const maxLength = Math.max(v1Parts.length, v2Parts.length);
  330. // 比较每一部分
  331. for (let i = 0; i < maxLength; i++) {
  332. // 获取当前部分,如果不存在则视为0
  333. const part1 = v1Parts[i] || 0;
  334. const part2 = v2Parts[i] || 0;
  335. // 比较当前部分
  336. if (part1 < part2) return -1;
  337. if (part1 > part2) return 1;
  338. }
  339. // 所有部分都相等
  340. return 0;
  341. },
  342. // 打开Chrome商店页面
  343. openStorePage() {
  344. try {
  345. // 使用Chrome Extension API请求检查更新
  346. // Manifest V3中requestUpdateCheck返回Promise,结果是一个对象而不是数组
  347. chrome.runtime.requestUpdateCheck().then(result => {
  348. // 正确获取status和details,它们是result对象的属性
  349. this.handleUpdateStatus(result.status, result.details);
  350. }).catch(error => {
  351. this.handleUpdateError(error);
  352. });
  353. } catch (error) {
  354. this.handleUpdateError(error);
  355. }
  356. },
  357. // 处理更新状态
  358. handleUpdateStatus(status, details) {
  359. if (status === 'update_available') {
  360. // 显示更新通知
  361. this.showNotification({
  362. title: 'FeHelper 更新',
  363. message: '已发现新版本,正在更新...'
  364. });
  365. // 重新加载扩展以应用更新
  366. setTimeout(() => {
  367. chrome.runtime.reload();
  368. }, 1000);
  369. } else if (status === 'no_update') {
  370. // 如果没有可用更新,但用户点击了更新按钮
  371. this.showNotification({
  372. title: 'FeHelper 更新',
  373. message: '您的FeHelper已经是最新版本。'
  374. });
  375. } else {
  376. // 其他情况,如更新检查失败等
  377. // 备选方案:跳转到官方网站
  378. chrome.tabs.create({
  379. url: 'https://fehelper.com/'
  380. });
  381. this.showNotification({
  382. title: 'FeHelper 更新',
  383. message: '自动更新失败,请访问FeHelper官网手动获取最新版本。'
  384. });
  385. }
  386. },
  387. // 处理更新错误
  388. handleUpdateError(error) {
  389. // 出错时跳转到官方网站
  390. chrome.tabs.create({
  391. url: 'https://fehelper.com/'
  392. });
  393. this.showNotification({
  394. title: 'FeHelper 更新错误',
  395. message: '更新过程中出现错误,请手动检查更新。'
  396. });
  397. },
  398. // 显示通知的统一方法
  399. showNotification(options) {
  400. try {
  401. // 定义通知ID,方便后续关闭
  402. const notificationId = 'fehelper-update-notification';
  403. const simpleNotificationId = 'fehelper-simple-notification';
  404. // 直接尝试创建通知,不检查权限
  405. // Chrome扩展在manifest中已声明notifications权限,应该可以直接使用
  406. const notificationOptions = {
  407. type: 'basic',
  408. iconUrl: chrome.runtime.getURL('static/img/fe-48.png'),
  409. title: options.title || 'FeHelper',
  410. message: options.message || '',
  411. priority: 2,
  412. requireInteraction: false, // 改为false,因为我们会手动关闭
  413. silent: false // 播放音效
  414. };
  415. // 首先尝试直接创建通知
  416. chrome.notifications.create(notificationId, notificationOptions, (createdId) => {
  417. const error = chrome.runtime.lastError;
  418. if (error) {
  419. // 通知创建失败,尝试使用alert作为备选方案
  420. alert(`${options.title}: ${options.message}`);
  421. // 再尝试使用不同的选项创建通知
  422. const simpleOptions = {
  423. type: 'basic',
  424. iconUrl: chrome.runtime.getURL('static/img/fe-48.png'),
  425. title: options.title || 'FeHelper',
  426. message: options.message || ''
  427. };
  428. // 使用简化选项再次尝试
  429. chrome.notifications.create(simpleNotificationId, simpleOptions, (simpleId) => {
  430. if (chrome.runtime.lastError) {
  431. console.error('简化通知创建也失败:', chrome.runtime.lastError);
  432. } else {
  433. // 3秒后自动关闭简化通知
  434. setTimeout(() => {
  435. chrome.notifications.clear(simpleId);
  436. }, 3000);
  437. }
  438. });
  439. } else {
  440. // 3秒后自动关闭通知
  441. setTimeout(() => {
  442. chrome.notifications.clear(createdId);
  443. }, 3000);
  444. }
  445. });
  446. // 同时使用内置UI显示消息
  447. this.showInPageNotification(options);
  448. } catch (error) {
  449. // 降级为alert
  450. alert(`${options.title}: ${options.message}`);
  451. }
  452. },
  453. // 在页面内显示通知消息
  454. showInPageNotification(options) {
  455. try {
  456. // 确保 options 是一个对象
  457. if (!options || typeof options !== 'object') {
  458. options = { message: String(options || '') };
  459. }
  460. // 创建一个通知元素
  461. const notificationEl = document.createElement('div');
  462. notificationEl.className = 'in-page-notification';
  463. const title = (options && options.title) ? String(options.title) : 'FeHelper';
  464. const message = (options && options.message) ? String(options.message) : '';
  465. notificationEl.innerHTML = `
  466. <div class="notification-content">
  467. <div class="notification-title">${title}</div>
  468. <div class="notification-message">${message}</div>
  469. </div>
  470. <button class="notification-close">×</button>
  471. `;
  472. // 添加样式
  473. const style = document.createElement('style');
  474. style.textContent = `
  475. .in-page-notification {
  476. position: fixed;
  477. bottom: 20px;
  478. right: 20px;
  479. background-color: #4285f4;
  480. color: white;
  481. padding: 15px;
  482. border-radius: 8px;
  483. box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
  484. z-index: 9999;
  485. display: flex;
  486. align-items: center;
  487. justify-content: space-between;
  488. min-width: 300px;
  489. animation: slideIn 0.3s ease-out;
  490. }
  491. .notification-content {
  492. flex: 1;
  493. }
  494. .notification-title {
  495. font-weight: bold;
  496. margin-bottom: 5px;
  497. }
  498. .notification-message {
  499. font-size: 14px;
  500. }
  501. .notification-close {
  502. background: none;
  503. border: none;
  504. color: white;
  505. font-size: 20px;
  506. cursor: pointer;
  507. margin-left: 10px;
  508. padding: 0 5px;
  509. }
  510. @keyframes slideIn {
  511. from { transform: translateX(100%); opacity: 0; }
  512. to { transform: translateX(0); opacity: 1; }
  513. }
  514. @keyframes slideOut {
  515. from { transform: translateX(0); opacity: 1; }
  516. to { transform: translateX(100%); opacity: 0; }
  517. }
  518. `;
  519. // 添加到页面
  520. document.head.appendChild(style);
  521. document.body.appendChild(notificationEl);
  522. // 点击关闭按钮移除通知
  523. const closeBtn = notificationEl.querySelector('.notification-close');
  524. if (closeBtn) {
  525. closeBtn.addEventListener('click', () => {
  526. notificationEl.style.animation = 'slideOut 0.3s ease-out forwards';
  527. notificationEl.addEventListener('animationend', () => {
  528. notificationEl.remove();
  529. });
  530. });
  531. }
  532. // 3秒后自动移除(从5秒改为3秒)
  533. setTimeout(() => {
  534. notificationEl.style.animation = 'slideOut 0.3s ease-out forwards';
  535. notificationEl.addEventListener('animationend', () => {
  536. notificationEl.remove();
  537. });
  538. }, 3000);
  539. } catch (error) {
  540. console.error('创建页内通知出错:', error);
  541. }
  542. },
  543. async getFavoritesData() {
  544. return new Promise((resolve) => {
  545. chrome.storage.local.get('favorites', (result) => {
  546. resolve(result.favorites || []);
  547. });
  548. });
  549. },
  550. async getRecentUsedData() {
  551. // 直接从Statistics模块获取最近使用的工具
  552. return await Statistics.getRecentUsedTools(10);
  553. },
  554. async saveFavorites() {
  555. try {
  556. await chrome.storage.local.set({
  557. favorites: Array.from(this.favorites)
  558. });
  559. // 更新工具的收藏状态
  560. Object.keys(this.originalTools).forEach(key => {
  561. this.originalTools[key].favorite = this.favorites.has(key);
  562. });
  563. } catch (error) {
  564. console.error('保存收藏失败:', error);
  565. }
  566. },
  567. handleSearch() {
  568. // 搜索时不重置视图类型,允许在已过滤的结果中搜索
  569. },
  570. handleCategoryChange(category) {
  571. // 切换到全部工具视图
  572. if (this.currentView !== 'all') {
  573. this.currentView = 'all';
  574. this.updateActiveTools('all');
  575. }
  576. this.currentCategory = category;
  577. this.searchKey = '';
  578. // 确保工具显示正确
  579. this.activeTools = { ...this.originalTools };
  580. },
  581. handleSort() {
  582. // 排序逻辑已在computed中实现
  583. },
  584. getCategoryCount(categoryKey) {
  585. const category = TOOL_CATEGORIES.find(c => c.key === categoryKey);
  586. const categoryTools = category ? category.tools : [];
  587. return categoryTools.length;
  588. },
  589. async getInstalledCount() {
  590. try {
  591. // 使用Awesome.getInstalledTools实时获取已安装工具数量
  592. const installedTools = await Awesome.getInstalledTools();
  593. return Object.keys(installedTools).length;
  594. } catch (error) {
  595. // 回退到本地数据
  596. return Object.values(this.originalTools).filter(tool =>
  597. tool.installed || tool.systemInstalled || false
  598. ).length;
  599. }
  600. },
  601. getFavoritesCount() {
  602. return this.favorites.size;
  603. },
  604. getToolCategory(toolKey) {
  605. for (const category of TOOL_CATEGORIES) {
  606. if (category.tools.includes(toolKey)) {
  607. return category.key;
  608. }
  609. }
  610. return 'other';
  611. },
  612. async showMyInstalled() {
  613. this.currentView = 'installed';
  614. this.currentCategory = '';
  615. this.searchKey = '';
  616. await this.updateActiveTools('installed');
  617. // 更新已安装工具数量
  618. await this.updateInstalledCount();
  619. },
  620. showMyFavorites() {
  621. this.currentView = 'favorites';
  622. this.currentCategory = '';
  623. this.searchKey = '';
  624. this.updateActiveTools('favorites');
  625. },
  626. // 重置工具列表到原始状态
  627. resetTools() {
  628. this.currentView = 'all';
  629. },
  630. // 安装工具
  631. async installTool(toolKey) {
  632. try {
  633. // 查找可能存在的按钮元素
  634. const btnElement = document.querySelector(`button[data-tool="${toolKey}"]`);
  635. let elProgress = null;
  636. // 如果是通过按钮点击调用的,获取进度条元素
  637. if (btnElement) {
  638. if (btnElement.getAttribute('data-undergoing') === '1') {
  639. return false;
  640. }
  641. btnElement.setAttribute('data-undergoing', '1');
  642. elProgress = btnElement.querySelector('span.x-progress');
  643. }
  644. // 显示安装进度
  645. let pt = 1;
  646. await Awesome.install(toolKey);
  647. // 只有当进度条元素存在时才更新文本内容
  648. if (elProgress) {
  649. elProgress.textContent = `(${pt}%)`;
  650. let ptInterval = setInterval(() => {
  651. elProgress.textContent = `(${pt}%)`;
  652. pt += Math.floor(Math.random() * 20);
  653. if(pt > 100) {
  654. clearInterval(ptInterval);
  655. elProgress.textContent = ``;
  656. // 在进度条完成后显示安装成功的通知
  657. this.showInPageNotification({
  658. message: `${this.originalTools[toolKey].name} 安装成功!`,
  659. type: 'success',
  660. duration: 3000
  661. });
  662. }
  663. }, 100);
  664. } else {
  665. // 如果没有进度条元素,直接显示通知
  666. this.showInPageNotification({
  667. message: `${this.originalTools[toolKey].name} 安装成功!`,
  668. type: 'success',
  669. duration: 3000
  670. });
  671. }
  672. // 更新原始数据和当前活动数据
  673. this.originalTools[toolKey].installed = true;
  674. if (this.activeTools[toolKey]) {
  675. this.activeTools[toolKey].installed = true;
  676. }
  677. // 更新已安装工具数量
  678. this.updateInstalledCount();
  679. // 如果按钮存在,更新其状态
  680. if (btnElement) {
  681. btnElement.setAttribute('data-undergoing', '0');
  682. }
  683. // 发送消息通知后台更新
  684. chrome.runtime.sendMessage({
  685. type: MSG_TYPE.DYNAMIC_TOOL_INSTALL_OR_OFFLOAD,
  686. toolName: toolKey,
  687. action: 'install',
  688. showTips: true
  689. });
  690. } catch (error) {
  691. // 显示安装失败的通知
  692. this.showInPageNotification({
  693. message: `安装失败:${error.message || '未知错误'}`,
  694. type: 'error',
  695. duration: 5000
  696. });
  697. }
  698. },
  699. // 卸载工具
  700. async uninstallTool(toolKey) {
  701. try {
  702. // 使用自定义确认对话框而非浏览器原生的confirm
  703. this.showConfirm({
  704. title: '卸载确认',
  705. message: `确定要卸载"${this.originalTools[toolKey].name}"工具吗?`,
  706. callback: async (key) => {
  707. try {
  708. // 先调用Awesome.offLoad卸载工具(确保存储数据先被删除)
  709. await Awesome.offLoad(key);
  710. // 再发送消息给background更新browser action
  711. await chrome.runtime.sendMessage({
  712. type: MSG_TYPE.DYNAMIC_TOOL_INSTALL_OR_OFFLOAD,
  713. toolName: key,
  714. action: 'offload',
  715. showTips: true
  716. });
  717. // 更新原始数据和当前活动数据
  718. this.originalTools[key].installed = false;
  719. this.originalTools[key].inContextMenu = false;
  720. if (this.activeTools[key]) {
  721. this.activeTools[key].installed = false;
  722. this.activeTools[key].inContextMenu = false;
  723. }
  724. // 更新已安装工具数量
  725. this.updateInstalledCount();
  726. // 显示卸载成功的通知
  727. this.showInPageNotification({
  728. message: `${this.originalTools[key].name} 已成功卸载!`,
  729. type: 'success',
  730. duration: 3000
  731. });
  732. } catch (error) {
  733. // 显示卸载失败的通知
  734. this.showInPageNotification({
  735. message: `卸载失败:${error.message || '未知错误'}`,
  736. type: 'error',
  737. duration: 5000
  738. });
  739. }
  740. },
  741. data: toolKey
  742. });
  743. } catch (error) {
  744. console.error('准备卸载过程中出错:', error);
  745. }
  746. },
  747. // 切换右键菜单
  748. async toggleContextMenu(toolKey) {
  749. try {
  750. const tool = this.originalTools[toolKey];
  751. const newState = !tool.inContextMenu;
  752. // 更新菜单状态
  753. await Awesome.menuMgr(toolKey, newState ? 'install' : 'offload');
  754. // 更新原始数据和当前活动数据
  755. tool.inContextMenu = newState;
  756. if (this.activeTools[toolKey]) {
  757. this.activeTools[toolKey].inContextMenu = newState;
  758. }
  759. // 发送消息通知后台更新右键菜单
  760. chrome.runtime.sendMessage({
  761. type: MSG_TYPE.DYNAMIC_TOOL_INSTALL_OR_OFFLOAD,
  762. action: `menu-${newState ? 'install' : 'offload'}`,
  763. showTips: false,
  764. menuOnly: true
  765. });
  766. } catch (error) {
  767. console.error('切换右键菜单失败:', error);
  768. }
  769. },
  770. // 切换收藏状态
  771. async toggleFavorite(toolKey) {
  772. try {
  773. if (this.favorites.has(toolKey)) {
  774. this.favorites.delete(toolKey);
  775. // 更新原始数据和当前活动数据
  776. this.originalTools[toolKey].favorite = false;
  777. if (this.activeTools[toolKey]) {
  778. this.activeTools[toolKey].favorite = false;
  779. }
  780. } else {
  781. this.favorites.add(toolKey);
  782. // 更新原始数据和当前活动数据
  783. this.originalTools[toolKey].favorite = true;
  784. if (this.activeTools[toolKey]) {
  785. this.activeTools[toolKey].favorite = true;
  786. }
  787. }
  788. await this.saveFavorites();
  789. // 如果是在收藏视图,需要更新视图
  790. if (this.currentView === 'favorites') {
  791. this.updateActiveTools('favorites');
  792. }
  793. } catch (error) {
  794. console.error('切换收藏状态失败:', error);
  795. }
  796. },
  797. async updateActiveTools(view) {
  798. if (this.loading || Object.keys(this.originalTools).length === 0) {
  799. return;
  800. }
  801. switch (view) {
  802. case 'installed':
  803. // 使用Awesome.getInstalledTools实时获取已安装工具
  804. try {
  805. const installedTools = await Awesome.getInstalledTools();
  806. // 合并installedTools与originalTools的数据
  807. this.activeTools = Object.fromEntries(
  808. Object.entries(this.originalTools).filter(([key]) =>
  809. installedTools.hasOwnProperty(key)
  810. )
  811. );
  812. } catch (error) {
  813. // 回退到本地数据
  814. this.activeTools = Object.fromEntries(
  815. Object.entries(this.originalTools).filter(([_, tool]) =>
  816. tool.installed || tool.systemInstalled || false
  817. )
  818. );
  819. }
  820. break;
  821. case 'favorites':
  822. this.activeTools = Object.fromEntries(
  823. Object.entries(this.originalTools).filter(([key]) => this.favorites.has(key))
  824. );
  825. break;
  826. case 'recent':
  827. // 切换recent时,recentUsed已在showRecentUsed中实时拉取
  828. this.activeTools = Object.fromEntries(
  829. Object.entries(this.originalTools).filter(([key]) => this.recentUsed.includes(key))
  830. );
  831. break;
  832. case 'all':
  833. default:
  834. this.activeTools = { ...this.originalTools };
  835. // 分类过滤在computed属性中处理
  836. break;
  837. }
  838. },
  839. // 新增更新已安装工具数量的方法
  840. async updateInstalledCount() {
  841. this.installedCount = await this.getInstalledCount();
  842. },
  843. // 加载用户保存的视图模式
  844. async loadViewMode() {
  845. try {
  846. const result = await new Promise(resolve => {
  847. chrome.storage.local.get('fehelper_view_mode', result => {
  848. resolve(result.fehelper_view_mode);
  849. });
  850. });
  851. if (result) {
  852. this.viewMode = result;
  853. }
  854. } catch (error) {
  855. console.error('加载视图模式失败:', error);
  856. }
  857. },
  858. // 保存用户的视图模式选择
  859. async saveViewMode(mode) {
  860. try {
  861. this.viewMode = mode;
  862. await chrome.storage.local.set({
  863. 'fehelper_view_mode': mode
  864. });
  865. } catch (error) {
  866. console.error('保存视图模式失败:', error);
  867. }
  868. },
  869. // 加载设置项
  870. async loadSettings() {
  871. try {
  872. Settings.getOptions(async (opts) => {
  873. let selectedOpts = [];
  874. Object.keys(opts).forEach(key => {
  875. if(String(opts[key]) === 'true') {
  876. selectedOpts.push(key);
  877. }
  878. });
  879. this.selectedOpts = selectedOpts;
  880. // 同步localStorage设置,确保与其他工具兼容
  881. localStorage.setItem('AUTO_DARK_MODE', opts.AUTO_DARK_MODE);
  882. localStorage.setItem('ALWAYS_DARK_MODE', opts.ALWAYS_DARK_MODE);
  883. // 应用深色模式设置
  884. this.applyDarkModeSettings(opts);
  885. // 加载右键菜单设置
  886. this.menuDownloadCrx = await Awesome.menuMgr('download-crx', 'get') === '1';
  887. this.menuFeHelperSeting = await Awesome.menuMgr('fehelper-setting', 'get') !== '0';
  888. // 获取快捷键
  889. chrome.commands.getAll((commands) => {
  890. for (let command of commands) {
  891. if (command.name === '_execute_action') {
  892. this.defaultKey = command.shortcut || 'Alt+Shift+J';
  893. break;
  894. }
  895. }
  896. });
  897. });
  898. } catch (error) {
  899. console.error('加载设置项失败:', error);
  900. }
  901. },
  902. // 检查浏览器类型
  903. checkBrowserType() {
  904. try {
  905. this.isFirefox = navigator.userAgent.toLowerCase().indexOf('firefox') > -1;
  906. } catch (error) {
  907. this.isFirefox = false;
  908. }
  909. },
  910. // 应用深色模式设置
  911. applyDarkModeSettings(opts) {
  912. const body = document.body;
  913. const shouldEnableDarkMode = this.shouldEnableDarkMode(opts);
  914. // 同时设置localStorage和chrome.storage.local,确保与其他工具兼容
  915. localStorage.setItem('AUTO_DARK_MODE', opts.AUTO_DARK_MODE);
  916. localStorage.setItem('ALWAYS_DARK_MODE', opts.ALWAYS_DARK_MODE);
  917. if (shouldEnableDarkMode) {
  918. body.classList.add('dark-mode');
  919. // 设置html属性,方便其他工具检测
  920. document.documentElement.setAttribute('data-theme', 'dark');
  921. document.documentElement.setAttribute('dark-mode', 'on');
  922. } else {
  923. body.classList.remove('dark-mode');
  924. document.documentElement.setAttribute('data-theme', 'light');
  925. document.documentElement.setAttribute('dark-mode', 'off');
  926. }
  927. },
  928. // 判断是否应该启用深色模式
  929. shouldEnableDarkMode(opts) {
  930. // 如果始终开启深色模式,直接返回true
  931. if (opts.ALWAYS_DARK_MODE === true || opts.ALWAYS_DARK_MODE === 'true') {
  932. return true;
  933. }
  934. // 如果自动开启深色模式,检查时间
  935. if (opts.AUTO_DARK_MODE === true || opts.AUTO_DARK_MODE === 'true') {
  936. return this.isNightTime();
  937. }
  938. return false;
  939. },
  940. // 检查当前时间是否在夜间时段(19:00-06:00)
  941. isNightTime() {
  942. const now = new Date();
  943. const hour = now.getHours();
  944. // 19:00-23:59 或 00:00-05:59
  945. return hour >= 19 || hour < 6;
  946. },
  947. // 显示设置模态框
  948. async showSettings() {
  949. this.showSettingsModal = true;
  950. // 加载可排序的工具列表
  951. await this.loadSortableTools();
  952. },
  953. // 关闭设置模态框
  954. closeSettings() {
  955. this.showSettingsModal = false;
  956. },
  957. // 显示打赏模态框
  958. openDonateModal() {
  959. this.showDonateModal = true;
  960. },
  961. // 关闭打赏模态框
  962. closeDonateModal() {
  963. this.showDonateModal = false;
  964. },
  965. // 显示确认对话框
  966. showConfirm(options) {
  967. this.confirmDialog = {
  968. show: true,
  969. title: options.title || '操作确认',
  970. message: options.message || '确定要执行此操作吗?',
  971. callback: options.callback || null,
  972. data: options.data || null
  973. };
  974. },
  975. // 确认操作
  976. confirmAction() {
  977. if (this.confirmDialog.callback) {
  978. this.confirmDialog.callback(this.confirmDialog.data);
  979. }
  980. this.confirmDialog.show = false;
  981. },
  982. // 取消确认
  983. cancelConfirm() {
  984. this.confirmDialog.show = false;
  985. },
  986. // 保存设置
  987. async saveSettings() {
  988. try {
  989. // 构建设置对象
  990. let opts = {};
  991. [
  992. 'OPT_ITEM_CONTEXTMENUS',
  993. 'FORBID_OPEN_IN_NEW_TAB',
  994. 'CONTENT_SCRIPT_ALLOW_ALL_FRAMES',
  995. 'JSON_PAGE_FORMAT',
  996. 'AUTO_DARK_MODE',
  997. 'ALWAYS_DARK_MODE',
  998. 'FORBID_STATISTICS' // 新增,确保保存
  999. ].forEach(key => {
  1000. opts[key] = this.selectedOpts.includes(key).toString();
  1001. });
  1002. // 先保存工具排序(如果用户有修改)
  1003. if (this.sortableTools && this.sortableTools.length > 0) {
  1004. try {
  1005. const toolOrder = this.sortableTools.map(tool => tool.key);
  1006. await chrome.storage.local.set({
  1007. tool_custom_order: JSON.stringify(toolOrder)
  1008. });
  1009. } catch (sortError) {
  1010. console.warn('保存工具排序时出现错误:', sortError);
  1011. // 工具排序保存失败不应该阻止设置保存
  1012. }
  1013. }
  1014. // 保存设置 - 直接传递对象,settings.js已增加对对象类型的支持
  1015. Settings.setOptions(opts, async () => {
  1016. try {
  1017. // 处理右键菜单
  1018. const crxAction = this.menuDownloadCrx ? 'install' : 'offload';
  1019. const settingAction = this.menuFeHelperSeting ? 'install' : 'offload';
  1020. await Promise.all([
  1021. Awesome.menuMgr('download-crx', crxAction),
  1022. Awesome.menuMgr('fehelper-setting', settingAction)
  1023. ]);
  1024. // 通知后台更新右键菜单
  1025. chrome.runtime.sendMessage({
  1026. type: MSG_TYPE.DYNAMIC_TOOL_INSTALL_OR_OFFLOAD,
  1027. action: 'menu-change',
  1028. menuOnly: true
  1029. });
  1030. // 应用深色模式设置
  1031. this.applyDarkModeSettings(opts);
  1032. // 关闭弹窗
  1033. this.closeSettings();
  1034. // 显示提示
  1035. this.showNotification({
  1036. title: 'FeHelper 设置',
  1037. message: '设置和工具排序已保存!'
  1038. });
  1039. } catch (innerError) {
  1040. this.showNotification({
  1041. title: 'FeHelper 设置错误',
  1042. message: '保存菜单设置失败: ' + innerError.message
  1043. });
  1044. }
  1045. });
  1046. } catch (error) {
  1047. this.showNotification({
  1048. title: 'FeHelper 设置错误',
  1049. message: '保存设置失败: ' + error.message
  1050. });
  1051. }
  1052. },
  1053. // 设置快捷键
  1054. setShortcuts() {
  1055. chrome.tabs.create({
  1056. url: 'chrome://extensions/shortcuts'
  1057. });
  1058. },
  1059. // 体验夜间模式
  1060. turnLight(event) {
  1061. event.preventDefault();
  1062. // 获取body元素
  1063. const body = document.body;
  1064. // 切换夜间模式
  1065. if (body.classList.contains('dark-mode')) {
  1066. body.classList.remove('dark-mode');
  1067. document.documentElement.setAttribute('data-theme', 'light');
  1068. document.documentElement.setAttribute('dark-mode', 'off');
  1069. } else {
  1070. body.classList.add('dark-mode');
  1071. document.documentElement.setAttribute('data-theme', 'dark');
  1072. document.documentElement.setAttribute('dark-mode', 'on');
  1073. // 设置倒计时
  1074. this.countDown = 10;
  1075. // 启动倒计时
  1076. const timer = setInterval(() => {
  1077. this.countDown--;
  1078. if (this.countDown <= 0) {
  1079. clearInterval(timer);
  1080. body.classList.remove('dark-mode');
  1081. document.documentElement.setAttribute('data-theme', 'light');
  1082. document.documentElement.setAttribute('dark-mode', 'off');
  1083. }
  1084. }, 1000);
  1085. }
  1086. },
  1087. // 检查URL中的donate_from参数并显示打赏弹窗
  1088. checkDonateParam() {
  1089. try {
  1090. const urlParams = new URLSearchParams(window.location.search);
  1091. const donateFrom = urlParams.get('donate_from');
  1092. if (donateFrom) {
  1093. // 记录打赏来源
  1094. chrome.storage.local.set({
  1095. 'fehelper_donate_from': donateFrom,
  1096. 'fehelper_donate_time': Date.now()
  1097. });
  1098. // 等待工具数据加载完成
  1099. this.$nextTick(() => {
  1100. // 在所有工具中查找匹配项
  1101. let matchedTool = null;
  1102. // 首先尝试直接匹配工具key
  1103. if (this.originalTools && this.originalTools[donateFrom]) {
  1104. matchedTool = this.originalTools[donateFrom];
  1105. } else if (this.originalTools) {
  1106. // 如果没有直接匹配,尝试在所有工具中查找部分匹配
  1107. for (const [key, tool] of Object.entries(this.originalTools)) {
  1108. if (key.includes(donateFrom) || donateFrom.includes(key) ||
  1109. (tool.name && tool.name.includes(donateFrom)) ||
  1110. (donateFrom && donateFrom.includes(tool.name))) {
  1111. matchedTool = tool;
  1112. break;
  1113. }
  1114. }
  1115. }
  1116. // 更新打赏文案
  1117. if (matchedTool) {
  1118. this.donate.text = `看起来【${matchedTool.name}】工具帮助到了你,感谢你的认可!`;
  1119. } else {
  1120. // 没有匹配到特定工具,使用通用文案
  1121. this.donate.text = `感谢你对FeHelper的认可和支持!`;
  1122. }
  1123. // 显示打赏弹窗
  1124. this.showDonateModal = true;
  1125. });
  1126. // 埋点:自动触发options
  1127. chrome.runtime.sendMessage({
  1128. type: 'fh-dynamic-any-thing',
  1129. thing: 'statistics-tool-usage',
  1130. params: {
  1131. tool_name: 'donate'
  1132. }
  1133. });
  1134. }
  1135. } catch (error) {
  1136. console.error('处理打赏参数时出错:', error);
  1137. }
  1138. },
  1139. // 补充 getRecentCount,保证模板调用不报错,且数据源唯一
  1140. async getRecentCount() {
  1141. const recent = await Statistics.getRecentUsedTools(10);
  1142. return recent.length;
  1143. },
  1144. async showRecentUsed() {
  1145. this.currentView = 'recent';
  1146. this.currentCategory = '';
  1147. this.searchKey = '';
  1148. // 重新获取最近使用的工具数据
  1149. this.recentUsed = await Statistics.getRecentUsedTools(10);
  1150. this.recentCount = this.recentUsed.length;
  1151. // activeTools会通过currentView的watcher自动更新
  1152. },
  1153. handleRecommendClick(card) {
  1154. if (card.isAd && card.url) {
  1155. window.open(card.url, '_blank');
  1156. } else if (card.toolKey) {
  1157. this.installTool(card.toolKey);
  1158. }
  1159. },
  1160. // 加载远程推荐卡片配置
  1161. async loadRemoteRecommendationCards() {
  1162. try {
  1163. // 通过background代理请求,解决CORS问题
  1164. const result = await new Promise((resolve) => {
  1165. chrome.runtime.sendMessage({
  1166. type: 'fh-dynamic-any-thing',
  1167. thing: 'fetch-hotfix-json',
  1168. }, resolve);
  1169. });
  1170. if (!result || !result.success) {
  1171. throw new Error('获取远程配置失败: ' + (result && result.error ? result.error : '未知错误'));
  1172. }
  1173. // 获取脚本内容
  1174. const scriptContent = result.content;
  1175. // 解析脚本内容,提取GlobalRecommendationCards变量
  1176. let remoteCards = null;
  1177. try {
  1178. remoteCards = JSON.parse(scriptContent);
  1179. } catch (parseError) {
  1180. console.error('解析远程推荐卡片配置失败:', parseError);
  1181. }
  1182. // 如果成功解析到配置,则更新本地配置
  1183. if (remoteCards && Array.isArray(remoteCards) && remoteCards.length > 0) {
  1184. remoteCards.forEach((card, idx) => {
  1185. if (
  1186. card &&
  1187. card.toolKey &&
  1188. this.originalTools &&
  1189. !this.originalTools.hasOwnProperty(card.toolKey)
  1190. ) {
  1191. // toolKey 不存在于本地工具,跳过赋值,保留本地默认
  1192. return;
  1193. }
  1194. // 没有 toolKey 字段,或者 toolKey 存在于本地工具,直接覆盖
  1195. this.recommendationCards[idx] = card;
  1196. });
  1197. this.$forceUpdate();
  1198. }
  1199. } catch (error) {
  1200. console.error('获取远程推荐卡片配置失败:', error);
  1201. }
  1202. },
  1203. // 工具排序相关方法
  1204. async loadSortableTools() {
  1205. try {
  1206. const installedTools = await Awesome.getInstalledTools();
  1207. // 从存储中加载自定义排序
  1208. const customOrder = await chrome.storage.local.get('tool_custom_order');
  1209. const savedOrder = customOrder.tool_custom_order ? JSON.parse(customOrder.tool_custom_order) : null;
  1210. // 转换为可排序的数组格式
  1211. let toolsArray = Object.entries(installedTools).map(([key, tool]) => ({
  1212. key,
  1213. name: tool.name,
  1214. tips: tool.tips,
  1215. icon: tool.icon || (tool.menuConfig && tool.menuConfig[0] ? tool.menuConfig[0].icon : '🔧')
  1216. }));
  1217. // 如果有保存的自定义排序,按照该顺序排列
  1218. if (savedOrder && Array.isArray(savedOrder)) {
  1219. const orderedTools = [];
  1220. const unorderedTools = [...toolsArray];
  1221. // 按照保存的顺序添加工具
  1222. savedOrder.forEach(toolKey => {
  1223. const toolIndex = unorderedTools.findIndex(t => t.key === toolKey);
  1224. if (toolIndex !== -1) {
  1225. orderedTools.push(unorderedTools.splice(toolIndex, 1)[0]);
  1226. }
  1227. });
  1228. // 添加新安装的工具(不在保存的顺序中的)
  1229. orderedTools.push(...unorderedTools);
  1230. toolsArray = orderedTools;
  1231. }
  1232. this.sortableTools = toolsArray;
  1233. } catch (error) {
  1234. console.error('加载可排序工具失败:', error);
  1235. }
  1236. },
  1237. // 拖拽开始
  1238. handleDragStart(event, index) {
  1239. this.draggedIndex = index;
  1240. event.target.classList.add('dragging');
  1241. event.dataTransfer.setData('text/plain', index);
  1242. },
  1243. // 拖拽经过
  1244. handleDragOver(event) {
  1245. event.preventDefault();
  1246. // 移除所有 drag-over 类
  1247. document.querySelectorAll('.sortable-item').forEach(item => {
  1248. item.classList.remove('drag-over');
  1249. });
  1250. // 添加到当前元素
  1251. event.currentTarget.classList.add('drag-over');
  1252. },
  1253. // 放置
  1254. handleDrop(event, dropIndex) {
  1255. event.preventDefault();
  1256. if (this.draggedIndex === -1 || this.draggedIndex === dropIndex) {
  1257. return;
  1258. }
  1259. // 重新排列数组
  1260. const draggedItem = this.sortableTools[this.draggedIndex];
  1261. const newTools = [...this.sortableTools];
  1262. // 移除被拖拽的项目
  1263. newTools.splice(this.draggedIndex, 1);
  1264. // 在新位置插入
  1265. if (dropIndex > this.draggedIndex) {
  1266. newTools.splice(dropIndex - 1, 0, draggedItem);
  1267. } else {
  1268. newTools.splice(dropIndex, 0, draggedItem);
  1269. }
  1270. this.sortableTools = newTools;
  1271. // 清理样式
  1272. this.cleanupDragStyles();
  1273. },
  1274. // 拖拽结束
  1275. handleDragEnd(event) {
  1276. this.cleanupDragStyles();
  1277. this.draggedIndex = -1;
  1278. },
  1279. // 清理拖拽样式
  1280. cleanupDragStyles() {
  1281. document.querySelectorAll('.sortable-item').forEach(item => {
  1282. item.classList.remove('dragging', 'drag-over');
  1283. });
  1284. },
  1285. // 重置工具顺序为默认
  1286. async resetToolOrder() {
  1287. try {
  1288. // 移除保存的自定义排序
  1289. await chrome.storage.local.remove('tool_custom_order');
  1290. // 重新加载工具列表(会使用默认顺序)
  1291. await this.loadSortableTools();
  1292. this.showInPageNotification({
  1293. message: '工具顺序已重置为默认排序',
  1294. type: 'success'
  1295. });
  1296. } catch (error) {
  1297. console.error('重置工具顺序失败:', error);
  1298. this.showInPageNotification({
  1299. message: '重置失败,请重试',
  1300. type: 'error'
  1301. });
  1302. }
  1303. },
  1304. // 保存工具排序
  1305. async saveToolOrder() {
  1306. try {
  1307. const toolOrder = this.sortableTools.map(tool => tool.key);
  1308. await chrome.storage.local.set({
  1309. tool_custom_order: JSON.stringify(toolOrder)
  1310. });
  1311. this.showInPageNotification({
  1312. message: '工具排序已保存!弹窗中的工具将按此顺序显示',
  1313. type: 'success'
  1314. });
  1315. } catch (error) {
  1316. console.error('保存工具排序失败:', error);
  1317. this.showInPageNotification({
  1318. message: '保存失败,请重试',
  1319. type: 'error'
  1320. });
  1321. }
  1322. },
  1323. async autoFixBugs() {
  1324. this.showNotification({
  1325. title: 'FeHelper 一键修复',
  1326. message: '正在拉取修复补丁,请稍候...'
  1327. });
  1328. chrome.runtime.sendMessage({
  1329. type: 'fh-dynamic-any-thing',
  1330. thing: 'fetch-fehelper-patchs'
  1331. }, (resp) => {
  1332. if (chrome.runtime.lastError) {
  1333. this.showNotification({
  1334. title: 'FeHelper 一键修复',
  1335. message: '补丁拉取失败:' + chrome.runtime.lastError.message
  1336. });
  1337. return;
  1338. }
  1339. if (!resp || !resp.success) {
  1340. const errorMsg = resp && resp.error ? resp.error : '未知错误';
  1341. this.showNotification({
  1342. title: 'FeHelper 一键修复',
  1343. message: errorMsg
  1344. });
  1345. return;
  1346. }
  1347. this.showNotification({
  1348. title: 'FeHelper 一键修复',
  1349. message: '当前FeHelper插件中的已知Bug都已修复,你可以去验证了。',
  1350. duration: 5000
  1351. });
  1352. // 当前页面的bug立即更新
  1353. this.loadPatchHotfix();
  1354. });
  1355. },
  1356. loadPatchHotfix() {
  1357. // 页面加载时自动获取并注入options页面的补丁
  1358. chrome.runtime.sendMessage({
  1359. type: 'fh-dynamic-any-thing',
  1360. thing: 'fh-get-tool-patch',
  1361. toolName: 'options'
  1362. }, patch => {
  1363. if (patch) {
  1364. if (patch.css) {
  1365. const style = document.createElement('style');
  1366. style.textContent = patch.css;
  1367. document.head.appendChild(style);
  1368. }
  1369. if (patch.js) {
  1370. try {
  1371. if (window.evalCore && window.evalCore.getEvalInstance) {
  1372. window.evalCore.getEvalInstance(window)(patch.js);
  1373. }
  1374. } catch (e) {
  1375. console.error('options补丁JS执行失败', e);
  1376. }
  1377. }
  1378. }
  1379. });
  1380. }
  1381. },
  1382. watch: {
  1383. // 监听currentView变化
  1384. currentView: {
  1385. immediate: true,
  1386. handler(newView) {
  1387. this.updateActiveTools(newView);
  1388. }
  1389. },
  1390. // 监听currentCategory变化
  1391. currentCategory: {
  1392. handler(newCategory) {
  1393. // 保证在视图模式之外的分类切换也能正确显示
  1394. if (this.currentView === 'all') {
  1395. this.activeTools = { ...this.originalTools };
  1396. }
  1397. // 重置搜索条件
  1398. if (this.searchKey) {
  1399. this.searchKey = '';
  1400. }
  1401. }
  1402. },
  1403. },
  1404. });
  1405. // 添加滚动事件监听
  1406. window.addEventListener('scroll', () => {
  1407. const header = document.querySelector('.market-header');
  1408. const sidebar = document.querySelector('.market-sidebar');
  1409. if (window.scrollY > 10) {
  1410. header.classList.add('scrolled');
  1411. sidebar && sidebar.classList.add('scrolled');
  1412. } else {
  1413. header.classList.remove('scrolled');
  1414. sidebar && sidebar.classList.remove('scrolled');
  1415. }
  1416. });
  1417. // 页面加载后自动采集
  1418. if (window.chrome && chrome.runtime && chrome.runtime.sendMessage) {
  1419. Awesome.collectAndSendClientInfo();
  1420. }