@5ime/likeme 1.0.4 → 1.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -81,11 +81,23 @@
81
81
  </script>
82
82
  ```
83
83
 
84
- ### 方法二:下载使用
84
+ 也可以用你自己的 Vercel 域名加载静态资源:
85
85
 
86
- 1. 下载 `static/css/likeme.css` 和 `static/js/likeme.js`
87
- 2. 在你的项目中引入这些文件
88
- 3. 按照上面的方式初始化组件
86
+ ```html
87
+ <link rel="stylesheet" href="https://your-domain.vercel.app/static/css/likeme.css" />
88
+ <script src="https://your-domain.vercel.app/static/js/likeme.js"></script>
89
+ ```
90
+
91
+ ### 方法二:npm 安装
92
+
93
+ ```bash
94
+ npm install @5ime/likeme
95
+ ```
96
+
97
+ 然后引入:
98
+
99
+ - `node_modules/@5ime/likeme/static/css/likeme.css`
100
+ - `node_modules/@5ime/likeme/static/js/likeme.js`
89
101
 
90
102
  ### 配置选项
91
103
 
package/package.json CHANGED
@@ -1,24 +1,42 @@
1
- {
2
- "name": "@5ime/likeme",
3
- "version": "1.0.4",
4
- "description": "Do you like me? - 一个可爱的互动小组件",
5
- "main": "index.js",
6
- "scripts": {
7
- "start": "node index.js",
8
- "dev": "node index.js"
9
- },
10
- "author": "iami233",
11
- "license": "MIT",
12
- "dependencies": {
13
- "@vercel/postgres": "^0.10.0",
14
- "express": "^5.1.0"
15
- },
16
- "engines": {
17
- "node": ">=18.0.0"
18
- },
19
- "repository": {
20
- "type": "git",
21
- "url": "https://github.com/5ime/likeMe.git"
22
- },
23
- "homepage": "https://github.com/5ime/likeMe"
24
- }
1
+ {
2
+ "name": "@5ime/likeme",
3
+ "version": "1.0.6",
4
+ "description": "Do you like me? - 一个可爱的互动点赞组件",
5
+ "main": "static/js/likeme.js",
6
+ "unpkg": "static/js/likeme.js",
7
+ "jsdelivr": "static/js/likeme.js",
8
+ "style": "static/css/likeme.css",
9
+ "files": [
10
+ "static",
11
+ "README.md"
12
+ ],
13
+ "scripts": {
14
+ "start": "node index.js",
15
+ "dev": "node index.js",
16
+ "prepare:public": "node scripts/prepare-public.js",
17
+ "prepublishOnly": "node -e \"const fs=require('fs');['static/css/likeme.css','static/js/likeme.js'].forEach(f=>{if(!fs.existsSync(f)){console.error('missing '+f);process.exit(1)}})\""
18
+ },
19
+ "author": "iami233",
20
+ "license": "MIT",
21
+ "keywords": [
22
+ "like",
23
+ "widget",
24
+ "embed",
25
+ "vercel"
26
+ ],
27
+ "dependencies": {
28
+ "@vercel/postgres": "^0.10.0",
29
+ "express": "^5.1.0"
30
+ },
31
+ "engines": {
32
+ "node": ">=18.0.0"
33
+ },
34
+ "repository": {
35
+ "type": "git",
36
+ "url": "https://github.com/5ime/likeMe.git"
37
+ },
38
+ "homepage": "https://github.com/5ime/likeMe",
39
+ "bugs": {
40
+ "url": "https://github.com/5ime/likeMe/issues"
41
+ }
42
+ }
@@ -1 +1 @@
1
- const likeMe=(()=>{let instance;return options=>instance||(instance=new LikeMeSingleton(options))})();class LikeMeSingleton{constructor({el,serverURL,color='#ff9797'}){Object.assign(this,{el,url:serverURL,color});this.isLiking=false;this.init()}async init(){try{await this.renderUI();this.attachEvents();this.updateCount()}catch(error){console.error('LikeMe init error:',error)}}async renderUI(){try{const response=await fetch(this.url);const html=await response.text();const element=document.querySelector(this.el);if(element){const parser=new DOMParser();const doc=parser.parseFromString(html,'text/html');element.innerHTML=doc.body.innerHTML;const card=element.querySelector('.likeCard');if(card){card.style.setProperty('background-color',this.color)}}}catch(error){console.error('Render UI error:',error)}}attachEvents(){const card=document.querySelector(`${this.el} .likeCard`);if(card){card.addEventListener('click',()=>this.handleLike())}}async updateCount(){try{const response=await fetch(`${this.url}/info`);const data=await response.json().catch(()=>null);if(response.ok&&data){const count=data.data?.count||0;const textElement=document.querySelector(`${this.el} .likeCard-text`);if(textElement){textElement.textContent=`❤ ${count}`}}}catch(error){console.error('Update count error:',error)}}setUIState(isLoading,text,isSuccess=false){const card=document.querySelector(`${this.el} .likeCard`);const textElement=document.querySelector(`${this.el} .likeCard-text`);if(card&&textElement){card.classList.remove('loading','success');if(isLoading){card.classList.add('loading')}else if(isSuccess){card.classList.add('success')}textElement.textContent=text}}async handleLike(){if(this.isLiking)return;this.isLiking=true;this.setUIState(true,'❤ 爱意传递中...');try{const response=await fetch(`${this.url}/like`);const payload=await response.json().catch(()=>null);if(!payload){this.setUIState(false,'❤ 网络错误');setTimeout(()=>{this.updateCount()},1500);return}const{code,data={},msg}=payload;const count=data.count||0;if(response.ok&&code==='200'){this.setUIState(false,'❤ 传递成功~',true);setTimeout(()=>{this.setUIState(false,`❤ ${count}`)},500);return}this.setUIState(false,msg||'❤ 传递失败~');setTimeout(()=>{this.updateCount()},1500)}catch(error){console.error('Handle like error:',error);this.setUIState(false,'❤ 网络错误');setTimeout(()=>{this.updateCount()},1500)}finally{this.isLiking=false}}}
1
+ const likeMe=(()=>{let instance;return options=>instance||(instance=new LikeMeSingleton(options))})();class LikeMeSingleton{constructor({el,serverURL,color='#ff9797'}){Object.assign(this,{el,url:serverURL,color});this.isLiking=false;this.init()}async init(){try{await this.renderUI();this.attachEvents();this.updateCount()}catch(error){console.error('LikeMe init error:',error)}}async renderUI(){try{const element=document.querySelector(this.el);if(element){element.innerHTML='\n <div class="container">\n <div class="likeCard">\n <div class="likeCard-text">\n 少女祈祷中...\n </div>\n <h3>Do You Like Me</h3>\n </div>\n </div>\n';const card=element.querySelector('.likeCard');if(card){card.style.setProperty('background-color',this.color)}}}catch(error){console.error('Render UI error:',error)}}attachEvents(){const card=document.querySelector(`${this.el} .likeCard`);if(card){card.addEventListener('click',()=>this.handleLike())}}async updateCount(){try{const response=await fetch(`${this.url}/info`);const data=await response.json().catch(()=>null);if(response.ok&&data){const count=data.data?.count||0;const textElement=document.querySelector(`${this.el} .likeCard-text`);if(textElement){textElement.textContent=`❤ ${count}`}}}catch(error){console.error('Update count error:',error)}}setUIState(isLoading,text,isSuccess=false){const card=document.querySelector(`${this.el} .likeCard`);const textElement=document.querySelector(`${this.el} .likeCard-text`);if(card&&textElement){card.classList.remove('loading','success');if(isLoading){card.classList.add('loading')}else if(isSuccess){card.classList.add('success')}textElement.textContent=text}}async handleLike(){if(this.isLiking)return;this.isLiking=true;this.setUIState(true,'❤ 爱意传递中...');try{const response=await fetch(`${this.url}/like`);const payload=await response.json().catch(()=>null);if(!payload){this.setUIState(false,'❤ 网络错误');setTimeout(()=>{this.updateCount()},1500);return}const{code,data={},msg}=payload;const count=data.count||0;if(response.ok&&code==='200'){this.setUIState(false,'❤ 传递成功~',true);setTimeout(()=>{this.setUIState(false,`❤ ${count}`)},500);return}this.setUIState(false,msg||'❤ 传递失败~');setTimeout(()=>{this.updateCount()},1500)}catch(error){console.error('Handle like error:',error);this.setUIState(false,'❤ 网络错误');setTimeout(()=>{this.updateCount()},1500)}finally{this.isLiking=false}}}
package/.env.example DELETED
@@ -1,7 +0,0 @@
1
- POSTGRES_URL=postgres://...
2
- PORT=3000
3
- ALLOW_ORIGIN=*
4
- TRUST_PROXY=1
5
- INFO_CACHE_SECONDS=5
6
- LIKE_RATE_LIMIT_WINDOW_MS=60000
7
- LIKE_RATE_LIMIT_MAX=20
package/.gitattributes DELETED
@@ -1,20 +0,0 @@
1
- * text=auto
2
-
3
- # Normalize line endings to LF for source/docs to avoid noisy diffs across OSes
4
- *.js text eol=lf
5
- *.cjs text eol=lf
6
- *.mjs text eol=lf
7
- *.ts text eol=lf
8
- *.tsx text eol=lf
9
- *.json text eol=lf
10
- *.md text eol=lf
11
- *.html text eol=lf
12
- *.css text eol=lf
13
- *.svg text eol=lf
14
- *.yml text eol=lf
15
- *.yaml text eol=lf
16
- *.toml text eol=lf
17
-
18
- # Keep Windows scripts with CRLF if you add any in the future
19
- *.bat text eol=crlf
20
- *.cmd text eol=crlf
package/demo/index.html DELETED
@@ -1,43 +0,0 @@
1
- <!DOCTYPE html>
2
- <html lang="zh-CN">
3
- <head>
4
- <meta charset="UTF-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <meta name="description" content="Do You Like Me? - 组件演示页面">
7
- <meta name="keywords" content="点赞,互动,like,web组件,演示">
8
- <title>Do You Like Me? - 演示页面</title>
9
- <link rel="stylesheet" href="/static/css/likeme.css">
10
- <style>
11
- .container {
12
- margin: 0 auto;
13
- max-width: 400px;
14
- width: 100%;
15
- }
16
-
17
- .demo-info {
18
- text-align: center;
19
- margin-bottom: 20px;
20
- padding: 10px;
21
- border-radius: 5px;
22
- }
23
- </style>
24
- </head>
25
- <body>
26
- <div class="container">
27
- <div class="demo-info">
28
- <h2>LikeMe 组件演示</h2>
29
- <p>这是一个可交互的点赞组件演示页面</p>
30
- </div>
31
- <div id="likeme"></div>
32
- </div>
33
-
34
- <script src="/static/js/likeme.js"></script>
35
- <script>
36
- likeMe({
37
- el: '#likeme',
38
- serverURL: `${location.protocol}//${location.host}`,
39
- color: '#ff4e6a'
40
- });
41
- </script>
42
- </body>
43
- </html>
package/index.js DELETED
@@ -1,14 +0,0 @@
1
- const { createApp } = require('./src/app');
2
- const { port, isVercel } = require('./src/config');
3
-
4
- const app = createApp();
5
-
6
- if (isVercel) {
7
- module.exports = app;
8
- } else {
9
- app.listen(port, () => {
10
- console.log(`💖 启动成功!端口:${port}`);
11
- console.log(`✨ 运行环境:${process.env.NODE_ENV || 'development'}`);
12
- console.log(`🌍 访问地址:http://localhost:${port}`);
13
- });
14
- }
package/public/index.html DELETED
@@ -1,35 +0,0 @@
1
- <!DOCTYPE html>
2
- <html lang="zh-CN">
3
- <head>
4
- <meta charset="UTF-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <meta name="description" content="Do You Like Me? - 一个可爱的互动点赞组件">
7
- <title>Do You Like Me?</title>
8
- <link rel="stylesheet" href="/static/css/likeme.css">
9
- <style>
10
- body {
11
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
12
- margin: 0;
13
- min-height: 100vh;
14
- display: flex;
15
- justify-content: center;
16
- align-items: center;
17
- }
18
-
19
- .container {
20
- max-width: 400px;
21
- width: 100%;
22
- }
23
- </style>
24
- </head>
25
- <body>
26
- <div class="container">
27
- <div class="likeCard">
28
- <div class="likeCard-text">
29
- 少女祈祷中...
30
- </div>
31
- <h3>Do You Like Me</h3>
32
- </div>
33
- </div>
34
- </body>
35
- </html>
package/src/app.js DELETED
@@ -1,87 +0,0 @@
1
- const express = require('express');
2
- const path = require('path');
3
-
4
- const {
5
- allowOrigin,
6
- trustProxy,
7
- infoCacheSeconds,
8
- likeRateLimitMax,
9
- likeRateLimitWindowMs,
10
- } = require('./config');
11
- const { getClientIp } = require('./ip');
12
- const { getCurrentCount, likeOncePerDay } = require('./db');
13
- const { ok, fail } = require('./http');
14
- const { createIpRateLimiter } = require('./ratelimit');
15
-
16
- function createApp() {
17
- const app = express();
18
-
19
- // 反代环境下正确识别客户端 IP(Vercel/CF 等)
20
- app.set('trust proxy', trustProxy);
21
-
22
- app.use((req, res, next) => {
23
- res.setHeader('X-Content-Type-Options', 'nosniff');
24
- res.setHeader('X-Frame-Options', 'DENY');
25
- res.setHeader('Referrer-Policy', 'no-referrer');
26
- res.setHeader(
27
- 'Permissions-Policy',
28
- 'geolocation=(), microphone=(), camera=()'
29
- );
30
- next();
31
- });
32
-
33
- app.use((req, res, next) => {
34
- res.header('Access-Control-Allow-Origin', allowOrigin);
35
- res.header('Access-Control-Allow-Headers', 'Content-Type');
36
- res.header('Access-Control-Allow-Methods', 'GET,OPTIONS');
37
- if (req.method === 'OPTIONS') return res.sendStatus(200);
38
- next();
39
- });
40
-
41
- // 健康检查
42
- app.get('/healthz', (req, res) => res.status(200).send('ok'));
43
-
44
- // 静态资源
45
- app.use('/static', express.static(path.join(__dirname, '..', 'static')));
46
- app.use('/demo', express.static(path.join(__dirname, '..', 'demo')));
47
- app.get('/', (req, res) =>
48
- res.sendFile(path.join(__dirname, '..', 'public', 'index.html'))
49
- );
50
-
51
- app.get('/info', async (req, res) => {
52
- try {
53
- // 计数查询短缓存
54
- res.setHeader('Cache-Control', `public, max-age=${infoCacheSeconds}`);
55
- const count = await getCurrentCount();
56
- return ok(res, { count });
57
- } catch (error) {
58
- console.error('Error in /info:', error?.message || 'Unknown error');
59
- return fail(res, '哎呀,数据偷懒跑掉了');
60
- }
61
- });
62
-
63
- // 点赞限流
64
- const likeLimiter = createIpRateLimiter({
65
- windowMs: likeRateLimitWindowMs,
66
- max: likeRateLimitMax,
67
- keyFn: (req) => getClientIp(req),
68
- });
69
-
70
- app.get('/like', likeLimiter, async (req, res) => {
71
- const ip = getClientIp(req);
72
- try {
73
- res.setHeader('Cache-Control', 'no-store');
74
- const result = await likeOncePerDay(ip);
75
- if (!result.ok) return fail(res, '今天的爱意已经收到啦~');
76
- return ok(res, { count: result.count });
77
- } catch (error) {
78
- console.error('Error in /like:', error?.message || 'Unknown error');
79
- return fail(res, '你的爱意传递失败~');
80
- }
81
- });
82
-
83
- return app;
84
- }
85
-
86
- module.exports = { createApp };
87
-
package/src/config.js DELETED
@@ -1,16 +0,0 @@
1
- function env(name, fallback) {
2
- const v = process.env[name];
3
- if (v === undefined || v === null || v === '') return fallback;
4
- return v;
5
- }
6
-
7
- module.exports = {
8
- port: Number(env('PORT', '3000')),
9
- allowOrigin: env('ALLOW_ORIGIN', '*'),
10
- trustProxy: env('TRUST_PROXY', '1'),
11
- infoCacheSeconds: Number(env('INFO_CACHE_SECONDS', '5')),
12
- likeRateLimitWindowMs: Number(env('LIKE_RATE_LIMIT_WINDOW_MS', '60000')),
13
- likeRateLimitMax: Number(env('LIKE_RATE_LIMIT_MAX', '20')),
14
- isVercel: Boolean(process.env.VERCEL),
15
- };
16
-
package/src/db.js DELETED
@@ -1,93 +0,0 @@
1
- const { sql } = require('@vercel/postgres');
2
-
3
- let schemaInitPromise = null;
4
-
5
- async function ensureSchema() {
6
- if (schemaInitPromise) return schemaInitPromise;
7
-
8
- schemaInitPromise = (async () => {
9
- // 总计数表
10
- await sql`
11
- CREATE TABLE IF NOT EXISTS like_count (
12
- id INT PRIMARY KEY DEFAULT 1,
13
- count BIGINT NOT NULL DEFAULT 0
14
- );
15
- `;
16
- await sql`
17
- INSERT INTO like_count (id, count)
18
- VALUES (1, 0)
19
- ON CONFLICT (id) DO NOTHING;
20
- `;
21
-
22
- // 去重表:同一 IP 每天一次(PRIMARY KEY (ip, day))
23
- await sql`
24
- CREATE TABLE IF NOT EXISTS like_user (
25
- ip TEXT NOT NULL,
26
- day DATE NOT NULL DEFAULT CURRENT_DATE,
27
- created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
28
- PRIMARY KEY (ip, day)
29
- );
30
- `;
31
- })();
32
-
33
- return schemaInitPromise;
34
- }
35
-
36
- async function getCurrentCount() {
37
- await ensureSchema();
38
- const { rows } = await sql`SELECT count FROM like_count WHERE id = 1;`;
39
- return Number(rows?.[0]?.count ?? 0);
40
- }
41
-
42
- async function likeOncePerDay(ip) {
43
- await ensureSchema();
44
-
45
- const client = await sql.connect();
46
- try {
47
- await client.sql`BEGIN;`;
48
-
49
- const exists = await client.sql`
50
- SELECT 1
51
- FROM like_user
52
- WHERE ip = ${ip} AND day = CURRENT_DATE
53
- LIMIT 1;
54
- `;
55
-
56
- if (exists.rows.length > 0) {
57
- await client.sql`ROLLBACK;`;
58
- return { ok: false, reason: 'already_liked_today' };
59
- }
60
-
61
- await client.sql`
62
- INSERT INTO like_user (ip, day)
63
- VALUES (${ip}, CURRENT_DATE)
64
- ON CONFLICT (ip, day) DO NOTHING;
65
- `;
66
-
67
- const updated = await client.sql`
68
- UPDATE like_count
69
- SET count = count + 1
70
- WHERE id = 1
71
- RETURNING count;
72
- `;
73
-
74
- await client.sql`COMMIT;`;
75
- const count = Number(updated.rows?.[0]?.count ?? 0);
76
- return { ok: true, count };
77
- } catch (err) {
78
- try {
79
- await client.sql`ROLLBACK;`;
80
- } catch (_) {
81
- }
82
- throw err;
83
- } finally {
84
- client.release();
85
- }
86
- }
87
-
88
- module.exports = {
89
- ensureSchema,
90
- getCurrentCount,
91
- likeOncePerDay,
92
- };
93
-
package/src/http.js DELETED
@@ -1,10 +0,0 @@
1
- function ok(res, data) {
2
- return res.json({ code: '200', msg: 'success', data });
3
- }
4
-
5
- function fail(res, msg) {
6
- return res.json({ code: '201', msg });
7
- }
8
-
9
- module.exports = { ok, fail };
10
-
package/src/ip.js DELETED
@@ -1,23 +0,0 @@
1
- function sanitizeIp(ip) {
2
- if (!ip || typeof ip !== 'string') return 'unknown';
3
- // IPv6 最大长度 45
4
- const cleaned = ip.trim().slice(0, 45);
5
- if (!/^[a-fA-F0-9.:\-]+$/.test(cleaned)) return 'unknown';
6
- return cleaned;
7
- }
8
-
9
- function getClientIp(req) {
10
- const forwarded = req.headers['x-forwarded-for'];
11
- if (forwarded) {
12
- const ip = String(forwarded).split(',')[0].trim();
13
- return sanitizeIp(ip);
14
- }
15
- const ip =
16
- req.socket?.remoteAddress ||
17
- req.connection?.remoteAddress ||
18
- 'unknown';
19
- return sanitizeIp(ip);
20
- }
21
-
22
- module.exports = { getClientIp, sanitizeIp };
23
-
package/src/ratelimit.js DELETED
@@ -1,37 +0,0 @@
1
- function createIpRateLimiter({ windowMs, max, keyFn }) {
2
- // key -> { count, resetAt }
3
- const hits = new Map();
4
-
5
- function cleanup(now) {
6
- for (const [k, v] of hits) {
7
- if (v.resetAt <= now) hits.delete(k);
8
- }
9
- }
10
-
11
- return function rateLimit(req, res, next) {
12
- const now = Date.now();
13
- cleanup(now);
14
-
15
- const key = (keyFn ? keyFn(req) : undefined) || 'unknown';
16
- const existing = hits.get(key);
17
-
18
- if (!existing || existing.resetAt <= now) {
19
- hits.set(key, { count: 1, resetAt: now + windowMs });
20
- return next();
21
- }
22
-
23
- existing.count += 1;
24
- if (existing.count > max) {
25
- res.setHeader(
26
- 'Retry-After',
27
- String(Math.ceil((existing.resetAt - now) / 1000))
28
- );
29
- return res.status(429).json({ code: '201', msg: '请求太频繁啦,稍后再试~' });
30
- }
31
-
32
- return next();
33
- };
34
- }
35
-
36
- module.exports = { createIpRateLimiter };
37
-
package/vercel.json DELETED
@@ -1,14 +0,0 @@
1
- {
2
- "builds": [
3
- {
4
- "src": "index.js",
5
- "use": "@vercel/node"
6
- }
7
- ],
8
- "routes": [
9
- {
10
- "src": "/(.*)",
11
- "dest": "index.js"
12
- }
13
- ]
14
- }