@anker-in/lib 1.0.0-beta.1
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/.gitkeep +0 -0
- package/.turbo/turbo-build.log +27 -0
- package/.turbo/turbo-test.log +14 -0
- package/README.md +162 -0
- package/USAGE_EXAMPLE.md +425 -0
- package/dist/index.d.mts +11 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +45 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +13 -0
- package/dist/index.mjs.map +1 -0
- package/jest.config.ts +12 -0
- package/package.json +31 -0
- package/src/__tests__/math.test.ts +15 -0
- package/src/index.ts +7 -0
- package/src/math.ts +5 -0
- package/tsconfig.json +20 -0
- package/tsup.config.ts +20 -0
package/.gitkeep
ADDED
|
File without changes
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
|
|
2
|
+
> @anker-in/lib@1.0.0 build /Users/anker/Code/headless-ui/packages/lib
|
|
3
|
+
> tsup
|
|
4
|
+
|
|
5
|
+
CLI Building entry: src/index.ts
|
|
6
|
+
CLI Using tsconfig: tsconfig.json
|
|
7
|
+
CLI tsup v8.4.0
|
|
8
|
+
CLI Using tsup config: /Users/anker/Code/headless-ui/packages/lib/tsup.config.ts
|
|
9
|
+
CLI Target: esnext
|
|
10
|
+
CLI Cleaning output folder
|
|
11
|
+
ESM Build start
|
|
12
|
+
CJS Build start
|
|
13
|
+
IIFE Build start
|
|
14
|
+
If you do not supply "output.name", you may not be able to access the exports of an IIFE bundle.
|
|
15
|
+
IIFE dist/index.global.js 198.00 B
|
|
16
|
+
IIFE dist/index.global.js.map 397.00 B
|
|
17
|
+
IIFE ⚡️ Build success in 436ms
|
|
18
|
+
CJS dist/index.js 143.00 B
|
|
19
|
+
CJS dist/index.js.map 389.00 B
|
|
20
|
+
CJS ⚡️ Build success in 438ms
|
|
21
|
+
ESM dist/index.mjs 130.00 B
|
|
22
|
+
ESM dist/index.mjs.map 390.00 B
|
|
23
|
+
ESM ⚡️ Build success in 438ms
|
|
24
|
+
DTS Build start
|
|
25
|
+
DTS ⚡️ Build success in 4317ms
|
|
26
|
+
DTS dist/index.d.mts 161.00 B
|
|
27
|
+
DTS dist/index.d.ts 161.00 B
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
|
|
2
|
+
> @anker-in/lib@1.0.0 test /Users/anker/Code/headless-ui/packages/lib
|
|
3
|
+
> jest --passWithNoTests
|
|
4
|
+
|
|
5
|
+
PASS src/__tests__/math.test.ts
|
|
6
|
+
math functions
|
|
7
|
+
✓ add should correctly add two numbers (1 ms)
|
|
8
|
+
✓ subtract should correctly subtract two numbers
|
|
9
|
+
|
|
10
|
+
Test Suites: 1 passed, 1 total
|
|
11
|
+
Tests: 2 passed, 2 total
|
|
12
|
+
Snapshots: 0 total
|
|
13
|
+
Time: 1.197 s
|
|
14
|
+
Ran all test suites.
|
package/README.md
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
# @anker-in/lib
|
|
2
|
+
|
|
3
|
+
便利导出包,整合所有 Anker headless 功能。用户只需安装和导入这一个包即可使用所有功能。
|
|
4
|
+
|
|
5
|
+
## 安装
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pnpm add @anker-in/lib
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
**就这样!** 所有依赖(`@anker-in/shared`、`@anker-in/cart`、`@anker-in/shopify`)会自动安装。
|
|
12
|
+
|
|
13
|
+
## 快速开始
|
|
14
|
+
|
|
15
|
+
### 1. 设置 HeadlessProvider
|
|
16
|
+
|
|
17
|
+
```tsx
|
|
18
|
+
import { HeadlessProvider } from '@anker-in/lib'
|
|
19
|
+
|
|
20
|
+
const headlessConfig = {
|
|
21
|
+
storefrontToken: 'your-token',
|
|
22
|
+
storeDomain: 'your-store.myshopify.com',
|
|
23
|
+
locale: 'en-US',
|
|
24
|
+
comboMetafieldsNamespace: 'custom',
|
|
25
|
+
cartIdCookieName: 'cart_id',
|
|
26
|
+
brand: 'Anker',
|
|
27
|
+
appName: 'My App'
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function App() {
|
|
31
|
+
return (
|
|
32
|
+
<HeadlessProvider headlessConfig={headlessConfig}>
|
|
33
|
+
<YourApp />
|
|
34
|
+
</HeadlessProvider>
|
|
35
|
+
)
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### 2. 使用所有功能(从一个包导入!)
|
|
40
|
+
|
|
41
|
+
```tsx
|
|
42
|
+
import {
|
|
43
|
+
// Context
|
|
44
|
+
useHeadlessContext,
|
|
45
|
+
|
|
46
|
+
// Shopify 功能
|
|
47
|
+
useProduct,
|
|
48
|
+
createCart,
|
|
49
|
+
|
|
50
|
+
// Cart 功能
|
|
51
|
+
useBuyNow,
|
|
52
|
+
useAddToCart,
|
|
53
|
+
useUpdateCartLines
|
|
54
|
+
} from '@anker-in/lib'
|
|
55
|
+
|
|
56
|
+
function ProductPage() {
|
|
57
|
+
const { storefrontToken, storeDomain } = useHeadlessContext()
|
|
58
|
+
const product = useProduct(productId)
|
|
59
|
+
const { trigger: buyNow } = useBuyNow()
|
|
60
|
+
|
|
61
|
+
const handleBuyNow = () => {
|
|
62
|
+
buyNow({
|
|
63
|
+
lineItems: [
|
|
64
|
+
{
|
|
65
|
+
variant: { id: product.variants[0].id },
|
|
66
|
+
quantity: 1
|
|
67
|
+
}
|
|
68
|
+
]
|
|
69
|
+
})
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
<div>
|
|
74
|
+
<h1>{product?.title}</h1>
|
|
75
|
+
<button onClick={handleBuyNow}>立即购买</button>
|
|
76
|
+
</div>
|
|
77
|
+
)
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## 架构设计
|
|
82
|
+
|
|
83
|
+
为了避免循环依赖,采用了分层架构:
|
|
84
|
+
|
|
85
|
+
```
|
|
86
|
+
┌──────────────┐
|
|
87
|
+
│ shared │ (基础包:Context + Types)
|
|
88
|
+
└──────┬───────┘
|
|
89
|
+
│
|
|
90
|
+
┌───────┼────────┐
|
|
91
|
+
│ │ │
|
|
92
|
+
↓ ↓ ↓
|
|
93
|
+
┌────────┬──────┬─────────┐
|
|
94
|
+
│ lib │ cart │ shopify │
|
|
95
|
+
└────┬───┴──────┴────┬────┘
|
|
96
|
+
│ │
|
|
97
|
+
└───────┬───────┘
|
|
98
|
+
↓
|
|
99
|
+
(lib 重新导出所有)
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### 包职责
|
|
103
|
+
|
|
104
|
+
- **@anker-in/shared**: 基础包,提供 `HeadlessProvider` 和 `HeadlessConfig`
|
|
105
|
+
- **@anker-in/shopify**: Shopify API 集成和 hooks
|
|
106
|
+
- **@anker-in/cart**: 购物车功能和 hooks
|
|
107
|
+
- **@anker-in/lib**: 便利导出包,重新导出所有功能 + 自己的工具函数
|
|
108
|
+
|
|
109
|
+
### 为什么这样设计?
|
|
110
|
+
|
|
111
|
+
**问题:** 如果 `lib` 包含 Context,而 `cart` 使用 `lib` 的 Context,那么 `lib` 就不能导出 `cart`(会形成循环)。
|
|
112
|
+
|
|
113
|
+
**解决:** 将 Context 提取到独立的 `shared` 包,这样:
|
|
114
|
+
- ✅ `cart` 依赖 `shared`(获取 Context)
|
|
115
|
+
- ✅ `lib` 依赖 `shared`(获取 Context)
|
|
116
|
+
- ✅ `lib` 依赖 `cart`(重新导出)
|
|
117
|
+
- ✅ 完全消除循环依赖!
|
|
118
|
+
|
|
119
|
+
## API
|
|
120
|
+
|
|
121
|
+
### HeadlessProvider
|
|
122
|
+
|
|
123
|
+
提供全局配置的 Context Provider。
|
|
124
|
+
|
|
125
|
+
```tsx
|
|
126
|
+
<HeadlessProvider headlessConfig={config}>
|
|
127
|
+
{children}
|
|
128
|
+
</HeadlessProvider>
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### useHeadlessContext
|
|
132
|
+
|
|
133
|
+
获取 Headless 配置的 Hook。
|
|
134
|
+
|
|
135
|
+
```tsx
|
|
136
|
+
const { storefrontToken, storeDomain, locale, ... } = useHeadlessContext()
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### HeadlessConfig
|
|
140
|
+
|
|
141
|
+
```typescript
|
|
142
|
+
type HeadlessConfig = {
|
|
143
|
+
storefrontToken: string // Shopify Storefront API token
|
|
144
|
+
storeDomain: string // 店铺域名
|
|
145
|
+
locale: string // 语言环境
|
|
146
|
+
comboMetafieldsNamespace: string
|
|
147
|
+
cartIdCookieName: string // 购物车 ID 的 cookie 名称
|
|
148
|
+
brand: string // 品牌名称
|
|
149
|
+
appName: string // 应用名称
|
|
150
|
+
}
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
## 导出内容
|
|
154
|
+
|
|
155
|
+
从 `@anker-in/lib` 可以导入:
|
|
156
|
+
|
|
157
|
+
- ✅ **Shared**: `HeadlessProvider`, `useHeadlessContext`, `HeadlessConfig`
|
|
158
|
+
- ✅ **Shopify**: 所有 Shopify 功能和 hooks
|
|
159
|
+
- ✅ **Cart**: 所有购物车功能和 hooks
|
|
160
|
+
- ✅ **Lib 自己的工具**: math 等工具函数
|
|
161
|
+
|
|
162
|
+
**简单来说,你只需要从 `@anker-in/lib` 导入所有内容!**
|
package/USAGE_EXAMPLE.md
ADDED
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
# 使用示例
|
|
2
|
+
|
|
3
|
+
## 完整的电商应用示例
|
|
4
|
+
|
|
5
|
+
以下是一个完整的示例,展示如何使用 `@anker-in/lib` 构建电商应用:
|
|
6
|
+
|
|
7
|
+
### 1. 应用入口(App.tsx)
|
|
8
|
+
|
|
9
|
+
```tsx
|
|
10
|
+
import { HeadlessProvider } from '@anker-in/lib'
|
|
11
|
+
import { ProductList } from './components/ProductList'
|
|
12
|
+
import { Cart } from './components/Cart'
|
|
13
|
+
|
|
14
|
+
const headlessConfig = {
|
|
15
|
+
storefrontToken: process.env.NEXT_PUBLIC_SHOPIFY_TOKEN!,
|
|
16
|
+
storeDomain: process.env.NEXT_PUBLIC_SHOPIFY_DOMAIN!,
|
|
17
|
+
locale: 'zh-CN',
|
|
18
|
+
comboMetafieldsNamespace: 'custom',
|
|
19
|
+
cartIdCookieName: 'anker_cart_id',
|
|
20
|
+
brand: 'Anker',
|
|
21
|
+
appName: 'Anker Store'
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export default function App() {
|
|
25
|
+
return (
|
|
26
|
+
<HeadlessProvider headlessConfig={headlessConfig}>
|
|
27
|
+
<div className="app">
|
|
28
|
+
<header>
|
|
29
|
+
<h1>Anker 商店</h1>
|
|
30
|
+
<Cart />
|
|
31
|
+
</header>
|
|
32
|
+
<main>
|
|
33
|
+
<ProductList />
|
|
34
|
+
</main>
|
|
35
|
+
</div>
|
|
36
|
+
</HeadlessProvider>
|
|
37
|
+
)
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### 2. 产品列表(ProductList.tsx)
|
|
42
|
+
|
|
43
|
+
```tsx
|
|
44
|
+
import { useProducts, useHeadlessContext } from '@anker-in/lib'
|
|
45
|
+
import { ProductCard } from './ProductCard'
|
|
46
|
+
|
|
47
|
+
export function ProductList() {
|
|
48
|
+
const { locale } = useHeadlessContext()
|
|
49
|
+
const { data: products, isLoading } = useProducts({
|
|
50
|
+
first: 12,
|
|
51
|
+
// 其他查询参数...
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
if (isLoading) return <div>加载中...</div>
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<div className="product-grid">
|
|
58
|
+
{products?.map(product => (
|
|
59
|
+
<ProductCard key={product.id} product={product} />
|
|
60
|
+
))}
|
|
61
|
+
</div>
|
|
62
|
+
)
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### 3. 产品卡片(ProductCard.tsx)
|
|
67
|
+
|
|
68
|
+
```tsx
|
|
69
|
+
import {
|
|
70
|
+
useAddToCart,
|
|
71
|
+
useBuyNow,
|
|
72
|
+
type Product
|
|
73
|
+
} from '@anker-in/lib'
|
|
74
|
+
|
|
75
|
+
interface ProductCardProps {
|
|
76
|
+
product: Product
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function ProductCard({ product }: ProductCardProps) {
|
|
80
|
+
const { trigger: addToCart, isMutating: isAdding } = useAddToCart()
|
|
81
|
+
const { trigger: buyNow, isMutating: isBuying } = useBuyNow()
|
|
82
|
+
|
|
83
|
+
const handleAddToCart = async () => {
|
|
84
|
+
try {
|
|
85
|
+
await addToCart({
|
|
86
|
+
lineItems: [{
|
|
87
|
+
variant: { id: product.variants[0].id },
|
|
88
|
+
quantity: 1
|
|
89
|
+
}]
|
|
90
|
+
})
|
|
91
|
+
alert('已添加到购物车!')
|
|
92
|
+
} catch (error) {
|
|
93
|
+
console.error('添加失败:', error)
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const handleBuyNow = async () => {
|
|
98
|
+
await buyNow({
|
|
99
|
+
lineItems: [{
|
|
100
|
+
variant: { id: product.variants[0].id },
|
|
101
|
+
quantity: 1
|
|
102
|
+
}],
|
|
103
|
+
gtmParams: {
|
|
104
|
+
currency: 'USD',
|
|
105
|
+
value: parseFloat(product.variants[0].price.amount)
|
|
106
|
+
}
|
|
107
|
+
})
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return (
|
|
111
|
+
<div className="product-card">
|
|
112
|
+
<img src={product.images[0]?.url} alt={product.title} />
|
|
113
|
+
<h3>{product.title}</h3>
|
|
114
|
+
<p className="price">${product.variants[0].price.amount}</p>
|
|
115
|
+
|
|
116
|
+
<div className="actions">
|
|
117
|
+
<button
|
|
118
|
+
onClick={handleAddToCart}
|
|
119
|
+
disabled={isAdding}
|
|
120
|
+
>
|
|
121
|
+
{isAdding ? '添加中...' : '加入购物车'}
|
|
122
|
+
</button>
|
|
123
|
+
|
|
124
|
+
<button
|
|
125
|
+
onClick={handleBuyNow}
|
|
126
|
+
disabled={isBuying}
|
|
127
|
+
className="primary"
|
|
128
|
+
>
|
|
129
|
+
{isBuying ? '处理中...' : '立即购买'}
|
|
130
|
+
</button>
|
|
131
|
+
</div>
|
|
132
|
+
</div>
|
|
133
|
+
)
|
|
134
|
+
}
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### 4. 购物车(Cart.tsx)
|
|
138
|
+
|
|
139
|
+
```tsx
|
|
140
|
+
import {
|
|
141
|
+
useCart,
|
|
142
|
+
useUpdateCartLines,
|
|
143
|
+
useRemoveCartLines,
|
|
144
|
+
useApplyCartCodes
|
|
145
|
+
} from '@anker-in/lib'
|
|
146
|
+
import { useState } from 'react'
|
|
147
|
+
|
|
148
|
+
export function Cart() {
|
|
149
|
+
const { data: cart } = useCart()
|
|
150
|
+
const { trigger: updateLines } = useUpdateCartLines()
|
|
151
|
+
const { trigger: removeLines } = useRemoveCartLines()
|
|
152
|
+
const { trigger: applyCode } = useApplyCartCodes()
|
|
153
|
+
const [discountCode, setDiscountCode] = useState('')
|
|
154
|
+
|
|
155
|
+
const handleQuantityChange = (lineId: string, quantity: number) => {
|
|
156
|
+
updateLines({
|
|
157
|
+
lines: [{
|
|
158
|
+
id: lineId,
|
|
159
|
+
quantity
|
|
160
|
+
}]
|
|
161
|
+
})
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const handleRemove = (lineId: string) => {
|
|
165
|
+
removeLines({ lineIds: [lineId] })
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const handleApplyDiscount = () => {
|
|
169
|
+
if (discountCode) {
|
|
170
|
+
applyCode({ discountCodes: [discountCode] })
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (!cart) return <div>购物车为空</div>
|
|
175
|
+
|
|
176
|
+
return (
|
|
177
|
+
<div className="cart">
|
|
178
|
+
<h2>购物车 ({cart.lines.length})</h2>
|
|
179
|
+
|
|
180
|
+
{cart.lines.map(line => (
|
|
181
|
+
<div key={line.id} className="cart-item">
|
|
182
|
+
<img src={line.merchandise.image?.url} alt={line.merchandise.title} />
|
|
183
|
+
<div className="info">
|
|
184
|
+
<h4>{line.merchandise.product.title}</h4>
|
|
185
|
+
<p>{line.merchandise.title}</p>
|
|
186
|
+
<p className="price">${line.cost.totalAmount.amount}</p>
|
|
187
|
+
</div>
|
|
188
|
+
|
|
189
|
+
<div className="quantity">
|
|
190
|
+
<button onClick={() => handleQuantityChange(line.id, line.quantity - 1)}>
|
|
191
|
+
-
|
|
192
|
+
</button>
|
|
193
|
+
<span>{line.quantity}</span>
|
|
194
|
+
<button onClick={() => handleQuantityChange(line.id, line.quantity + 1)}>
|
|
195
|
+
+
|
|
196
|
+
</button>
|
|
197
|
+
</div>
|
|
198
|
+
|
|
199
|
+
<button onClick={() => handleRemove(line.id)}>删除</button>
|
|
200
|
+
</div>
|
|
201
|
+
))}
|
|
202
|
+
|
|
203
|
+
<div className="discount">
|
|
204
|
+
<input
|
|
205
|
+
type="text"
|
|
206
|
+
placeholder="优惠码"
|
|
207
|
+
value={discountCode}
|
|
208
|
+
onChange={(e) => setDiscountCode(e.target.value)}
|
|
209
|
+
/>
|
|
210
|
+
<button onClick={handleApplyDiscount}>应用</button>
|
|
211
|
+
</div>
|
|
212
|
+
|
|
213
|
+
<div className="total">
|
|
214
|
+
<h3>总计: ${cart.cost.totalAmount.amount}</h3>
|
|
215
|
+
<a href={cart.checkoutUrl} className="checkout-button">
|
|
216
|
+
去结账
|
|
217
|
+
</a>
|
|
218
|
+
</div>
|
|
219
|
+
</div>
|
|
220
|
+
)
|
|
221
|
+
}
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
### 5. 产品详情页(ProductDetail.tsx)
|
|
225
|
+
|
|
226
|
+
```tsx
|
|
227
|
+
import {
|
|
228
|
+
useProduct,
|
|
229
|
+
useBuyNow,
|
|
230
|
+
useAddToCart,
|
|
231
|
+
useHeadlessContext
|
|
232
|
+
} from '@anker-in/lib'
|
|
233
|
+
import { useState } from 'react'
|
|
234
|
+
|
|
235
|
+
interface ProductDetailProps {
|
|
236
|
+
productId: string
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
export function ProductDetail({ productId }: ProductDetailProps) {
|
|
240
|
+
const { brand } = useHeadlessContext()
|
|
241
|
+
const { data: product, isLoading } = useProduct(productId)
|
|
242
|
+
const [selectedVariant, setSelectedVariant] = useState(0)
|
|
243
|
+
const [quantity, setQuantity] = useState(1)
|
|
244
|
+
|
|
245
|
+
const { trigger: buyNow } = useBuyNow()
|
|
246
|
+
const { trigger: addToCart } = useAddToCart()
|
|
247
|
+
|
|
248
|
+
if (isLoading) return <div>加载中...</div>
|
|
249
|
+
if (!product) return <div>产品未找到</div>
|
|
250
|
+
|
|
251
|
+
const currentVariant = product.variants[selectedVariant]
|
|
252
|
+
|
|
253
|
+
return (
|
|
254
|
+
<div className="product-detail">
|
|
255
|
+
<div className="gallery">
|
|
256
|
+
{product.images.map((image, idx) => (
|
|
257
|
+
<img key={idx} src={image.url} alt={`${product.title} ${idx + 1}`} />
|
|
258
|
+
))}
|
|
259
|
+
</div>
|
|
260
|
+
|
|
261
|
+
<div className="info">
|
|
262
|
+
<span className="brand">{brand}</span>
|
|
263
|
+
<h1>{product.title}</h1>
|
|
264
|
+
<div dangerouslySetInnerHTML={{ __html: product.descriptionHtml }} />
|
|
265
|
+
|
|
266
|
+
<div className="variants">
|
|
267
|
+
<h3>选择规格</h3>
|
|
268
|
+
{product.variants.map((variant, idx) => (
|
|
269
|
+
<button
|
|
270
|
+
key={variant.id}
|
|
271
|
+
onClick={() => setSelectedVariant(idx)}
|
|
272
|
+
className={selectedVariant === idx ? 'active' : ''}
|
|
273
|
+
>
|
|
274
|
+
{variant.title}
|
|
275
|
+
</button>
|
|
276
|
+
))}
|
|
277
|
+
</div>
|
|
278
|
+
|
|
279
|
+
<div className="price">
|
|
280
|
+
<h2>${currentVariant.price.amount}</h2>
|
|
281
|
+
</div>
|
|
282
|
+
|
|
283
|
+
<div className="quantity">
|
|
284
|
+
<label>数量:</label>
|
|
285
|
+
<input
|
|
286
|
+
type="number"
|
|
287
|
+
min="1"
|
|
288
|
+
value={quantity}
|
|
289
|
+
onChange={(e) => setQuantity(parseInt(e.target.value))}
|
|
290
|
+
/>
|
|
291
|
+
</div>
|
|
292
|
+
|
|
293
|
+
<div className="actions">
|
|
294
|
+
<button
|
|
295
|
+
onClick={() => addToCart({
|
|
296
|
+
lineItems: [{
|
|
297
|
+
variant: { id: currentVariant.id },
|
|
298
|
+
quantity
|
|
299
|
+
}]
|
|
300
|
+
})}
|
|
301
|
+
className="add-to-cart"
|
|
302
|
+
>
|
|
303
|
+
加入购物车
|
|
304
|
+
</button>
|
|
305
|
+
|
|
306
|
+
<button
|
|
307
|
+
onClick={() => buyNow({
|
|
308
|
+
lineItems: [{
|
|
309
|
+
variant: { id: currentVariant.id },
|
|
310
|
+
quantity
|
|
311
|
+
}],
|
|
312
|
+
gtmParams: {
|
|
313
|
+
currency: 'USD',
|
|
314
|
+
value: parseFloat(currentVariant.price.amount) * quantity,
|
|
315
|
+
items: [{
|
|
316
|
+
item_id: product.id,
|
|
317
|
+
item_name: product.title,
|
|
318
|
+
price: parseFloat(currentVariant.price.amount),
|
|
319
|
+
quantity
|
|
320
|
+
}]
|
|
321
|
+
}
|
|
322
|
+
})}
|
|
323
|
+
className="buy-now"
|
|
324
|
+
>
|
|
325
|
+
立即购买
|
|
326
|
+
</button>
|
|
327
|
+
</div>
|
|
328
|
+
</div>
|
|
329
|
+
</div>
|
|
330
|
+
)
|
|
331
|
+
}
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
## 高级功能示例
|
|
335
|
+
|
|
336
|
+
### 使用购物车特性 Hooks
|
|
337
|
+
|
|
338
|
+
```tsx
|
|
339
|
+
import {
|
|
340
|
+
useCartAttributes,
|
|
341
|
+
usePriceDiscount,
|
|
342
|
+
useCalcOrderDiscount,
|
|
343
|
+
useCartItemQuantityLimit
|
|
344
|
+
} from '@anker-in/lib'
|
|
345
|
+
|
|
346
|
+
function AdvancedCart() {
|
|
347
|
+
// 购物车自定义属性
|
|
348
|
+
const { attributes, updateAttribute } = useCartAttributes()
|
|
349
|
+
|
|
350
|
+
// 价格折扣
|
|
351
|
+
const { discountedPrice } = usePriceDiscount()
|
|
352
|
+
|
|
353
|
+
// 订单折扣计算
|
|
354
|
+
const { orderDiscount } = useCalcOrderDiscount()
|
|
355
|
+
|
|
356
|
+
// 数量限制
|
|
357
|
+
const { maxQuantity, isLimitReached } = useCartItemQuantityLimit()
|
|
358
|
+
|
|
359
|
+
return (
|
|
360
|
+
<div>
|
|
361
|
+
{/* 使用这些 hooks 的数据 */}
|
|
362
|
+
</div>
|
|
363
|
+
)
|
|
364
|
+
}
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
### 自定义埋点
|
|
368
|
+
|
|
369
|
+
```tsx
|
|
370
|
+
import { useBuyNow } from '@anker-in/lib'
|
|
371
|
+
|
|
372
|
+
function CustomTracking() {
|
|
373
|
+
const { trigger: buyNow } = useBuyNow({
|
|
374
|
+
withTrack: false // 禁用默认埋点
|
|
375
|
+
})
|
|
376
|
+
|
|
377
|
+
const handleBuyNow = async () => {
|
|
378
|
+
// 自定义埋点逻辑
|
|
379
|
+
window.dataLayer?.push({
|
|
380
|
+
event: 'custom_buy_now',
|
|
381
|
+
// ... 自定义数据
|
|
382
|
+
})
|
|
383
|
+
|
|
384
|
+
await buyNow({
|
|
385
|
+
lineItems: [/* ... */],
|
|
386
|
+
fbqTrackConfig: {
|
|
387
|
+
// Facebook Pixel 配置
|
|
388
|
+
content_ids: ['product-123'],
|
|
389
|
+
content_type: 'product',
|
|
390
|
+
value: 99.99,
|
|
391
|
+
currency: 'USD'
|
|
392
|
+
}
|
|
393
|
+
})
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
return <button onClick={handleBuyNow}>购买</button>
|
|
397
|
+
}
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
## TypeScript 支持
|
|
401
|
+
|
|
402
|
+
所有导出的类型都可以直接使用:
|
|
403
|
+
|
|
404
|
+
```tsx
|
|
405
|
+
import type {
|
|
406
|
+
Product,
|
|
407
|
+
Cart,
|
|
408
|
+
CartLine,
|
|
409
|
+
HeadlessConfig,
|
|
410
|
+
BuyNowArgs,
|
|
411
|
+
// ... 更多类型
|
|
412
|
+
} from '@anker-in/lib'
|
|
413
|
+
|
|
414
|
+
const config: HeadlessConfig = {
|
|
415
|
+
storefrontToken: '...',
|
|
416
|
+
storeDomain: '...',
|
|
417
|
+
// ...
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
function handleProduct(product: Product) {
|
|
421
|
+
// TypeScript 完整支持
|
|
422
|
+
}
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export * from '@anker-in/shared';
|
|
2
|
+
export * from '@anker-in/shopify';
|
|
3
|
+
export * from '@anker-in/cart';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 这是一个demo 模版
|
|
7
|
+
*/
|
|
8
|
+
declare const add: (a: number, b: number) => number;
|
|
9
|
+
declare const subtract: (a: number, b: number) => number;
|
|
10
|
+
|
|
11
|
+
export { add, subtract };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export * from '@anker-in/shared';
|
|
2
|
+
export * from '@anker-in/shopify';
|
|
3
|
+
export * from '@anker-in/cart';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 这是一个demo 模版
|
|
7
|
+
*/
|
|
8
|
+
declare const add: (a: number, b: number) => number;
|
|
9
|
+
declare const subtract: (a: number, b: number) => number;
|
|
10
|
+
|
|
11
|
+
export { add, subtract };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __reExport = (target, mod, secondTarget) => (__copyProps(target, mod, "default"), secondTarget && __copyProps(secondTarget, mod, "default"));
|
|
19
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
20
|
+
|
|
21
|
+
// src/index.ts
|
|
22
|
+
var index_exports = {};
|
|
23
|
+
__export(index_exports, {
|
|
24
|
+
add: () => add,
|
|
25
|
+
subtract: () => subtract
|
|
26
|
+
});
|
|
27
|
+
module.exports = __toCommonJS(index_exports);
|
|
28
|
+
|
|
29
|
+
// src/math.ts
|
|
30
|
+
var add = (a, b) => a + b;
|
|
31
|
+
var subtract = (a, b) => a - b;
|
|
32
|
+
|
|
33
|
+
// src/index.ts
|
|
34
|
+
__reExport(index_exports, require("@anker-in/shared"), module.exports);
|
|
35
|
+
__reExport(index_exports, require("@anker-in/shopify"), module.exports);
|
|
36
|
+
__reExport(index_exports, require("@anker-in/cart"), module.exports);
|
|
37
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
38
|
+
0 && (module.exports = {
|
|
39
|
+
add,
|
|
40
|
+
subtract,
|
|
41
|
+
...require("@anker-in/shared"),
|
|
42
|
+
...require("@anker-in/shopify"),
|
|
43
|
+
...require("@anker-in/cart")
|
|
44
|
+
});
|
|
45
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/math.ts"],"sourcesContent":["// 导出本地功能\nexport * from './math'\n\n// 重新导出所有包的内容(作为便利导出)\nexport * from '@anker-in/shared'\nexport * from '@anker-in/shopify'\nexport * from '@anker-in/cart'\n","/**\n * 这是一个demo 模版\n */\nexport const add = (a: number, b: number) => a + b\nexport const subtract = (a: number, b: number) => a - b\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACGO,IAAM,MAAM,CAAC,GAAW,MAAc,IAAI;AAC1C,IAAM,WAAW,CAAC,GAAW,MAAc,IAAI;;;ADAtD,0BAAc,6BAJd;AAKA,0BAAc,8BALd;AAMA,0BAAc,2BANd;","names":[]}
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// src/math.ts
|
|
2
|
+
var add = (a, b) => a + b;
|
|
3
|
+
var subtract = (a, b) => a - b;
|
|
4
|
+
|
|
5
|
+
// src/index.ts
|
|
6
|
+
export * from "@anker-in/shared";
|
|
7
|
+
export * from "@anker-in/shopify";
|
|
8
|
+
export * from "@anker-in/cart";
|
|
9
|
+
export {
|
|
10
|
+
add,
|
|
11
|
+
subtract
|
|
12
|
+
};
|
|
13
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/math.ts","../src/index.ts"],"sourcesContent":["/**\n * 这是一个demo 模版\n */\nexport const add = (a: number, b: number) => a + b\nexport const subtract = (a: number, b: number) => a - b\n","// 导出本地功能\nexport * from './math'\n\n// 重新导出所有包的内容(作为便利导出)\nexport * from '@anker-in/shared'\nexport * from '@anker-in/shopify'\nexport * from '@anker-in/cart'\n"],"mappings":";AAGO,IAAM,MAAM,CAAC,GAAW,MAAc,IAAI;AAC1C,IAAM,WAAW,CAAC,GAAW,MAAc,IAAI;;;ACAtD,cAAc;AACd,cAAc;AACd,cAAc;","names":[]}
|
package/jest.config.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { JestConfigWithTsJest } from 'ts-jest'
|
|
2
|
+
|
|
3
|
+
const config: JestConfigWithTsJest = {
|
|
4
|
+
preset: 'ts-jest',
|
|
5
|
+
testEnvironment: 'jsdom', // 使用 node 环境(如果是浏览器库可改成 'jsdom')
|
|
6
|
+
clearMocks: true, // 每次测试后自动清除 mock
|
|
7
|
+
coverageDirectory: 'coverage', // 输出覆盖率报告
|
|
8
|
+
testMatch: ['**/__tests__/**/*.test.ts'], // 匹配测试文件路径
|
|
9
|
+
moduleFileExtensions: ['ts', 'js', 'json'],
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export default config
|
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@anker-in/lib",
|
|
3
|
+
"version": "1.0.0-beta.1",
|
|
4
|
+
"main": "dist/index.js",
|
|
5
|
+
"types": "dist/index.d.ts",
|
|
6
|
+
"dependencies": {
|
|
7
|
+
"@anker-in/shared": "1.0.0",
|
|
8
|
+
"@anker-in/shopify": "0.0.0-beta.4",
|
|
9
|
+
"@anker-in/cart": "1.0.0"
|
|
10
|
+
},
|
|
11
|
+
"devDependencies": {
|
|
12
|
+
"@types/jest": "^29.5.12",
|
|
13
|
+
"@types/react": "^18.3.3",
|
|
14
|
+
"jest": "^29.0.0",
|
|
15
|
+
"ts-jest": "^29.2.6",
|
|
16
|
+
"tsup": "^8.4.0",
|
|
17
|
+
"typescript": "^5.0.0"
|
|
18
|
+
},
|
|
19
|
+
"peerDependencies": {
|
|
20
|
+
"react": "^18.0.0",
|
|
21
|
+
"react-dom": "^18.0.0"
|
|
22
|
+
},
|
|
23
|
+
"scripts": {
|
|
24
|
+
"dev": "tsup --watch",
|
|
25
|
+
"build": "tsup",
|
|
26
|
+
"test": "jest --passWithNoTests",
|
|
27
|
+
"test:watch": "jest --watch",
|
|
28
|
+
"coverage": "jest --coverage",
|
|
29
|
+
"clean": "rm -rf .turbo node_modules dist"
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { add, subtract } from '../math'
|
|
2
|
+
|
|
3
|
+
describe('math functions', () => {
|
|
4
|
+
test('add should correctly add two numbers', () => {
|
|
5
|
+
expect(add(2, 3)).toBe(5)
|
|
6
|
+
expect(add(-1, -1)).toBe(-2)
|
|
7
|
+
expect(add(0, 5)).toBe(5)
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
test('subtract should correctly subtract two numbers', () => {
|
|
11
|
+
expect(subtract(5, 3)).toBe(2)
|
|
12
|
+
expect(subtract(0, 5)).toBe(-5)
|
|
13
|
+
expect(subtract(-1, -1)).toBe(0)
|
|
14
|
+
})
|
|
15
|
+
})
|
package/src/index.ts
ADDED
package/src/math.ts
ADDED
package/tsconfig.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "../../tsconfig.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"outDir": "dist",
|
|
5
|
+
"target": "esnext",
|
|
6
|
+
"module": "esnext",
|
|
7
|
+
"moduleResolution": "bundler",
|
|
8
|
+
"declaration": true,
|
|
9
|
+
"declarationMap": true,
|
|
10
|
+
"sourceMap": true,
|
|
11
|
+
"strict": true,
|
|
12
|
+
"esModuleInterop": true,
|
|
13
|
+
"lib": ["DOM", "ESNext", "DOM.Iterable"],
|
|
14
|
+
"skipLibCheck": true,
|
|
15
|
+
"jsx": "react-jsx",
|
|
16
|
+
"types": ["jest", "node"]
|
|
17
|
+
},
|
|
18
|
+
"include": ["src"],
|
|
19
|
+
"exclude": ["node_modules", "dist"]
|
|
20
|
+
}
|
package/tsup.config.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { defineConfig } from 'tsup'
|
|
2
|
+
|
|
3
|
+
export default defineConfig({
|
|
4
|
+
entry: ['src/index.ts'],
|
|
5
|
+
format: ['esm', 'cjs'],
|
|
6
|
+
dts: true,
|
|
7
|
+
splitting: false,
|
|
8
|
+
sourcemap: true,
|
|
9
|
+
clean: true,
|
|
10
|
+
external: [
|
|
11
|
+
'react',
|
|
12
|
+
'react-dom',
|
|
13
|
+
'@anker-in/shared',
|
|
14
|
+
'@anker-in/shopify',
|
|
15
|
+
'@anker-in/cart',
|
|
16
|
+
'next/router',
|
|
17
|
+
'@sentry/nextjs',
|
|
18
|
+
'swr',
|
|
19
|
+
],
|
|
20
|
+
})
|