@_tc/template-core 0.1.4 → 0.1.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/.skills/tc-component-usage-skills/SKILL.md +110 -0
- package/.skills/tc-component-usage-skills/reference/component-api.md +774 -0
- package/.skills/tc-component-usage-skills/reference/examples.md +425 -0
- package/.skills/tc-component-usage-skills/reference/patterns.md +357 -0
- package/README.md +12 -5
- package/cjs/bundler/utils.js +1 -1
- package/esm/bundler/utils.js +34 -9
- package/fe/packages/ui/react/components/testPage/index.js +114 -19
- package/package.json +2 -2
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
# Complete Examples
|
|
2
|
+
|
|
3
|
+
Copy-paste starting points for common TC UI scenarios.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Example 1: CRUD List Page
|
|
8
|
+
|
|
9
|
+
```tsx
|
|
10
|
+
import { useState, useEffect, useCallback } from 'react'
|
|
11
|
+
import { DataTable, TableSearch, Button, Modal, SchemaForm, ConfirmDialog, message } from '@tc/ui/react'
|
|
12
|
+
import type { TableColumnDef, TableSearchSchema } from '@tc/ui/react'
|
|
13
|
+
|
|
14
|
+
interface Product {
|
|
15
|
+
id: number
|
|
16
|
+
name: string
|
|
17
|
+
price: number
|
|
18
|
+
status: 'active' | 'inactive'
|
|
19
|
+
createdAt: string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface ProductSearch {
|
|
23
|
+
name?: string
|
|
24
|
+
status?: string
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface ProductForm {
|
|
28
|
+
name: string
|
|
29
|
+
price: number
|
|
30
|
+
status: 'active' | 'inactive'
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function ProductList() {
|
|
34
|
+
const [data, setData] = useState<Product[]>([])
|
|
35
|
+
const [loading, setLoading] = useState(false)
|
|
36
|
+
const [modalOpen, setModalOpen] = useState(false)
|
|
37
|
+
const [editingId, setEditingId] = useState<number | null>(null)
|
|
38
|
+
const [deleteId, setDeleteId] = useState<number | null>(null)
|
|
39
|
+
const [form] = Form.useForm<ProductForm>()
|
|
40
|
+
|
|
41
|
+
const searchSchemas: TableSearchSchema<ProductSearch>[] = [
|
|
42
|
+
{ key: 'name', label: 'Name', type: 'input', isPermanent: true },
|
|
43
|
+
{ key: 'status', label: 'Status', type: 'select',
|
|
44
|
+
fieldProps: { options: [{ label: 'Active', value: 'active' }, { label: 'Inactive', value: 'inactive' }] } },
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
const columns: TableColumnDef<Product>[] = [
|
|
48
|
+
{ key: 'id', title: 'ID', dataIndex: 'id', width: 80 },
|
|
49
|
+
{ key: 'name', title: 'Name', dataIndex: 'name' },
|
|
50
|
+
{ key: 'price', title: 'Price', dataIndex: 'price',
|
|
51
|
+
render: (value) => `¥${value}` },
|
|
52
|
+
{ key: 'status', title: 'Status', dataIndex: 'status',
|
|
53
|
+
render: (value) => (
|
|
54
|
+
<span className={value === 'active' ? 'text-green-600' : 'text-gray-400'}>
|
|
55
|
+
{value}
|
|
56
|
+
</span>
|
|
57
|
+
) },
|
|
58
|
+
{ key: 'createdAt', title: 'Created', dataIndex: 'createdAt' },
|
|
59
|
+
{ key: 'action', title: 'Actions', type: 'action',
|
|
60
|
+
actions: [
|
|
61
|
+
{ key: 'edit', label: 'Edit', onClick: (record) => openEdit(record) },
|
|
62
|
+
{ key: 'delete', label: 'Delete', danger: true, onClick: (record) => setDeleteId(record.id) },
|
|
63
|
+
] },
|
|
64
|
+
]
|
|
65
|
+
|
|
66
|
+
const fetchData = useCallback(async (search?: ProductSearch) => {
|
|
67
|
+
setLoading(true)
|
|
68
|
+
try {
|
|
69
|
+
const res = await api.getProducts(search)
|
|
70
|
+
setData(res)
|
|
71
|
+
} finally {
|
|
72
|
+
setLoading(false)
|
|
73
|
+
}
|
|
74
|
+
}, [])
|
|
75
|
+
|
|
76
|
+
const openCreate = () => {
|
|
77
|
+
setEditingId(null)
|
|
78
|
+
form.resetFields()
|
|
79
|
+
setModalOpen(true)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const openEdit = (record: Product) => {
|
|
83
|
+
setEditingId(record.id)
|
|
84
|
+
form.setFieldsValue(record)
|
|
85
|
+
setModalOpen(true)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const handleSubmit = async (values: ProductForm) => {
|
|
89
|
+
if (editingId !== null) {
|
|
90
|
+
await api.updateProduct(editingId, values)
|
|
91
|
+
message.success('Updated')
|
|
92
|
+
} else {
|
|
93
|
+
await api.createProduct(values)
|
|
94
|
+
message.success('Created')
|
|
95
|
+
}
|
|
96
|
+
setModalOpen(false)
|
|
97
|
+
fetchData()
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const handleDelete = async () => {
|
|
101
|
+
if (deleteId === null) return
|
|
102
|
+
await api.deleteProduct(deleteId)
|
|
103
|
+
message.success('Deleted')
|
|
104
|
+
setDeleteId(null)
|
|
105
|
+
fetchData()
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
useEffect(() => { fetchData() }, [fetchData])
|
|
109
|
+
|
|
110
|
+
return (
|
|
111
|
+
<div>
|
|
112
|
+
<TableSearch<ProductSearch>
|
|
113
|
+
schemas={searchSchemas}
|
|
114
|
+
onSearch={fetchData}
|
|
115
|
+
onReset={() => fetchData()}
|
|
116
|
+
renderActionBtnArea={() => (
|
|
117
|
+
<Button variant="primary" onClick={openCreate}>Create Product</Button>
|
|
118
|
+
)}
|
|
119
|
+
/>
|
|
120
|
+
<DataTable<Product>
|
|
121
|
+
columns={columns}
|
|
122
|
+
data={data}
|
|
123
|
+
rowKey="id"
|
|
124
|
+
loading={loading}
|
|
125
|
+
/>
|
|
126
|
+
|
|
127
|
+
<Modal open={modalOpen} onClose={() => setModalOpen(false)}
|
|
128
|
+
title={editingId !== null ? 'Edit Product' : 'Create Product'}
|
|
129
|
+
footer={
|
|
130
|
+
<div className="flex justify-end gap-2">
|
|
131
|
+
<Button onClick={() => setModalOpen(false)}>Cancel</Button>
|
|
132
|
+
<Button variant="primary" onClick={() => form.submit()}>Save</Button>
|
|
133
|
+
</div>
|
|
134
|
+
}
|
|
135
|
+
>
|
|
136
|
+
<SchemaForm<ProductForm>
|
|
137
|
+
form={form}
|
|
138
|
+
schemas={[
|
|
139
|
+
{ key: 'name', label: 'Name', type: 'input', required: true },
|
|
140
|
+
{ key: 'price', label: 'Price', type: 'inputNumber',
|
|
141
|
+
fieldProps: { min: 0, precision: 2, allowStep: true } },
|
|
142
|
+
{ key: 'status', label: 'Status', type: 'select',
|
|
143
|
+
fieldProps: { options: [{ label: 'Active', value: 'active' }, { label: 'Inactive', value: 'inactive' }] } },
|
|
144
|
+
]}
|
|
145
|
+
footerButtons={false}
|
|
146
|
+
onFinish={handleSubmit}
|
|
147
|
+
/>
|
|
148
|
+
</Modal>
|
|
149
|
+
|
|
150
|
+
<ConfirmDialog
|
|
151
|
+
open={deleteId !== null}
|
|
152
|
+
title="Delete Product"
|
|
153
|
+
content="This action cannot be undone."
|
|
154
|
+
onOk={handleDelete}
|
|
155
|
+
onCancel={() => setDeleteId(null)}
|
|
156
|
+
/>
|
|
157
|
+
</div>
|
|
158
|
+
)
|
|
159
|
+
}
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
---
|
|
163
|
+
|
|
164
|
+
## Example 2: Detail Drawer
|
|
165
|
+
|
|
166
|
+
```tsx
|
|
167
|
+
import { Drawer, Button, Loading, Skeleton } from '@tc/ui/react'
|
|
168
|
+
|
|
169
|
+
export function UserDetailDrawer({ userId, open, onClose }: {
|
|
170
|
+
userId: number | null
|
|
171
|
+
open: boolean
|
|
172
|
+
onClose: () => void
|
|
173
|
+
}) {
|
|
174
|
+
const [user, setUser] = useState<User | null>(null)
|
|
175
|
+
const [loading, setLoading] = useState(false)
|
|
176
|
+
|
|
177
|
+
useEffect(() => {
|
|
178
|
+
if (userId && open) {
|
|
179
|
+
setLoading(true)
|
|
180
|
+
api.getUser(userId).then(setUser).finally(() => setLoading(false))
|
|
181
|
+
}
|
|
182
|
+
}, [userId, open])
|
|
183
|
+
|
|
184
|
+
return (
|
|
185
|
+
<Drawer open={open} onClose={onClose}
|
|
186
|
+
title="User Details" placement="right" width={520}
|
|
187
|
+
>
|
|
188
|
+
{loading ? (
|
|
189
|
+
<Skeleton mode="componentsloading" rows={6} showTitle />
|
|
190
|
+
) : user ? (
|
|
191
|
+
<div className="space-y-4">
|
|
192
|
+
<Field label="Name" value={user.name} />
|
|
193
|
+
<Field label="Email" value={user.email} />
|
|
194
|
+
<Field label="Role" value={user.role} />
|
|
195
|
+
<Field label="Status" value={user.active ? 'Active' : 'Inactive'} />
|
|
196
|
+
</div>
|
|
197
|
+
) : (
|
|
198
|
+
<p className="text-gray-400">No data</p>
|
|
199
|
+
)}
|
|
200
|
+
</Drawer>
|
|
201
|
+
)
|
|
202
|
+
}
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
---
|
|
206
|
+
|
|
207
|
+
## Example 3: App Shell with Sidebar Menu
|
|
208
|
+
|
|
209
|
+
```tsx
|
|
210
|
+
import { Layout, Menu, Breadcrumb } from '@tc/ui/react'
|
|
211
|
+
import { useNavigate, useLocation } from 'react-router-dom'
|
|
212
|
+
|
|
213
|
+
export function AppShell({ children }: { children: ReactNode }) {
|
|
214
|
+
const navigate = useNavigate()
|
|
215
|
+
const { pathname } = useLocation()
|
|
216
|
+
|
|
217
|
+
return (
|
|
218
|
+
<Layout
|
|
219
|
+
sidebar={
|
|
220
|
+
<div className="flex flex-col h-full">
|
|
221
|
+
<div className="h-14 flex items-center px-4 font-bold text-lg">
|
|
222
|
+
My App
|
|
223
|
+
</div>
|
|
224
|
+
<Menu
|
|
225
|
+
mode="left"
|
|
226
|
+
selectedKey={pathname}
|
|
227
|
+
onSelect={(key) => navigate(key)}
|
|
228
|
+
items={[
|
|
229
|
+
{ key: '/dashboard', label: 'Dashboard', icon: <DashboardIcon /> },
|
|
230
|
+
{ key: '/products', label: 'Products', icon: <ProductIcon /> },
|
|
231
|
+
{ key: '/users', label: 'Users', icon: <UserIcon />,
|
|
232
|
+
children: [
|
|
233
|
+
{ key: '/users/list', label: 'All Users' },
|
|
234
|
+
{ key: '/users/roles', label: 'Roles' },
|
|
235
|
+
] },
|
|
236
|
+
{ key: '/settings', label: 'Settings', icon: <SettingsIcon /> },
|
|
237
|
+
]}
|
|
238
|
+
/>
|
|
239
|
+
</div>
|
|
240
|
+
}
|
|
241
|
+
>
|
|
242
|
+
<div className="p-6">
|
|
243
|
+
<SimpleBreadcrumb />
|
|
244
|
+
{children}
|
|
245
|
+
</div>
|
|
246
|
+
</Layout>
|
|
247
|
+
)
|
|
248
|
+
}
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
---
|
|
252
|
+
|
|
253
|
+
## Example 4: Image Upload Form
|
|
254
|
+
|
|
255
|
+
```tsx
|
|
256
|
+
import { SchemaForm, ImageUpload } from '@tc/ui/react'
|
|
257
|
+
|
|
258
|
+
interface BannerForm {
|
|
259
|
+
title: string
|
|
260
|
+
images: string[]
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function BannerEditor() {
|
|
264
|
+
const [form] = Form.useForm<BannerForm>()
|
|
265
|
+
|
|
266
|
+
return (
|
|
267
|
+
<SchemaForm<BannerForm>
|
|
268
|
+
form={form}
|
|
269
|
+
schemas={[
|
|
270
|
+
{ key: 'title', label: 'Title', type: 'input', required: true },
|
|
271
|
+
{ key: 'images', label: 'Banner Images', type: 'input',
|
|
272
|
+
render: ({ value, onChange }) => (
|
|
273
|
+
<ImageUpload
|
|
274
|
+
value={value}
|
|
275
|
+
onChange={onChange}
|
|
276
|
+
maxCount={5}
|
|
277
|
+
accept=".jpg,.png,.webp"
|
|
278
|
+
beforeChange={(file) => {
|
|
279
|
+
if (file.size > 5 * 1024 * 1024) {
|
|
280
|
+
message.warning('Max 5MB per image')
|
|
281
|
+
return false
|
|
282
|
+
}
|
|
283
|
+
}}
|
|
284
|
+
/>
|
|
285
|
+
) },
|
|
286
|
+
]}
|
|
287
|
+
onFinish={handleSubmit}
|
|
288
|
+
/>
|
|
289
|
+
)
|
|
290
|
+
}
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
---
|
|
294
|
+
|
|
295
|
+
## Example 5: TreeSelect for Permissions
|
|
296
|
+
|
|
297
|
+
```tsx
|
|
298
|
+
import { TreeSelect } from '@tc/ui/react'
|
|
299
|
+
|
|
300
|
+
function PermissionEditor() {
|
|
301
|
+
const [checked, setChecked] = useState<(string | number)[]>([])
|
|
302
|
+
const treeData = [
|
|
303
|
+
{ id: '1', name: 'System', children: [
|
|
304
|
+
{ id: '1-1', name: 'User Management' },
|
|
305
|
+
{ id: '1-2', name: 'Role Management' },
|
|
306
|
+
{ id: '1-3', name: 'Settings' },
|
|
307
|
+
] },
|
|
308
|
+
{ id: '2', name: 'Content', children: [
|
|
309
|
+
{ id: '2-1', name: 'Articles' },
|
|
310
|
+
{ id: '2-2', name: 'Comments' },
|
|
311
|
+
] },
|
|
312
|
+
]
|
|
313
|
+
|
|
314
|
+
return (
|
|
315
|
+
<TreeSelect
|
|
316
|
+
data={treeData}
|
|
317
|
+
value={checked}
|
|
318
|
+
onChange={({ checked: newChecked }) => setChecked(newChecked)}
|
|
319
|
+
/>
|
|
320
|
+
)
|
|
321
|
+
}
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
---
|
|
325
|
+
|
|
326
|
+
## Example 6: DataTable with Row Selection (Batch Actions)
|
|
327
|
+
|
|
328
|
+
```tsx
|
|
329
|
+
import { DataTable, Button, message } from '@tc/ui/react'
|
|
330
|
+
|
|
331
|
+
function BatchUserList() {
|
|
332
|
+
const [selectedKeys, setSelectedKeys] = useState<(string | number)[]>([])
|
|
333
|
+
const [users, setUsers] = useState<User[]>([])
|
|
334
|
+
|
|
335
|
+
const handleBatchDelete = async () => {
|
|
336
|
+
await api.batchDeleteUsers(selectedKeys as number[])
|
|
337
|
+
message.success(`Deleted ${selectedKeys.length} users`)
|
|
338
|
+
setSelectedKeys([])
|
|
339
|
+
fetchUsers()
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
return (
|
|
343
|
+
<div>
|
|
344
|
+
{selectedKeys.length > 0 && (
|
|
345
|
+
<div className="mb-4 flex gap-2 items-center">
|
|
346
|
+
<span>{selectedKeys.length} selected</span>
|
|
347
|
+
<Button state="danger" onClick={handleBatchDelete}>
|
|
348
|
+
Batch Delete
|
|
349
|
+
</Button>
|
|
350
|
+
</div>
|
|
351
|
+
)}
|
|
352
|
+
<DataTable<User>
|
|
353
|
+
columns={columns}
|
|
354
|
+
data={users}
|
|
355
|
+
rowKey="id"
|
|
356
|
+
rowSelection={{
|
|
357
|
+
selectedRowKeys: selectedKeys,
|
|
358
|
+
onChange: (keys) => setSelectedKeys(keys),
|
|
359
|
+
}}
|
|
360
|
+
/>
|
|
361
|
+
</div>
|
|
362
|
+
)
|
|
363
|
+
}
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
---
|
|
367
|
+
|
|
368
|
+
## Example 7: DatePicker Usage
|
|
369
|
+
|
|
370
|
+
```tsx
|
|
371
|
+
import { DatePicker } from '@tc/ui/react'
|
|
372
|
+
|
|
373
|
+
// Single date
|
|
374
|
+
<DatePicker
|
|
375
|
+
value={startDate}
|
|
376
|
+
onChange={(v) => setStartDate(v)}
|
|
377
|
+
/>
|
|
378
|
+
|
|
379
|
+
// Range with time
|
|
380
|
+
<DatePicker
|
|
381
|
+
mode="range"
|
|
382
|
+
showTime
|
|
383
|
+
value={dateRange}
|
|
384
|
+
onChange={(v) => setDateRange(v)}
|
|
385
|
+
quickRanges={[
|
|
386
|
+
{ label: 'This Month', range: [monthStart, new Date()] },
|
|
387
|
+
]}
|
|
388
|
+
/>
|
|
389
|
+
|
|
390
|
+
// In SchemaForm
|
|
391
|
+
{ key: 'birthday', label: 'Birthday', type: 'date' }
|
|
392
|
+
{ key: 'period', label: 'Period', type: 'date',
|
|
393
|
+
fieldProps: { mode: 'range', showTime: true } }
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
---
|
|
397
|
+
|
|
398
|
+
## Example 8: Tooltip & Popup
|
|
399
|
+
|
|
400
|
+
```tsx
|
|
401
|
+
import { Tooltip, Popup, Dropdown, Button } from '@tc/ui/react'
|
|
402
|
+
|
|
403
|
+
// Simple tooltip
|
|
404
|
+
<Tooltip content="Click to save changes">
|
|
405
|
+
<Button><SaveIcon /></Button>
|
|
406
|
+
</Tooltip>
|
|
407
|
+
|
|
408
|
+
// Dropdown menu
|
|
409
|
+
<Dropdown items={[
|
|
410
|
+
{ key: 'edit', label: 'Edit', onClick: handleEdit },
|
|
411
|
+
{ key: 'share', label: 'Share', onClick: handleShare },
|
|
412
|
+
{ key: 'delete', label: 'Delete', danger: true, onClick: handleDelete },
|
|
413
|
+
]}>
|
|
414
|
+
<Button>Actions <ChevronDown /></Button>
|
|
415
|
+
</Dropdown>
|
|
416
|
+
|
|
417
|
+
// Custom popup
|
|
418
|
+
<Popup
|
|
419
|
+
content={<UserCard user={user} />}
|
|
420
|
+
trigger="click"
|
|
421
|
+
onVisibleChange={(v) => console.log('popup:', v)}
|
|
422
|
+
>
|
|
423
|
+
<span className="cursor-pointer underline">{user.name}</span>
|
|
424
|
+
</Popup>
|
|
425
|
+
```
|