feat: impl accounts and channels

This commit is contained in:
2026-09-06 18:17:42 +08:00
parent aa9999a940
commit 1ccee1414c
70 changed files with 9879 additions and 31 deletions
+444
View File
@@ -0,0 +1,444 @@
---
name: drizzle
description: 'LobeHub Drizzle ORM schema and query style. Use for pgTable schemas, indexes, joins, inferred types, db.select/db.query, schema fields, foreign keys, junction tables, or postgres query patterns.'
user-invocable: false
---
# Drizzle ORM Schema Style Guide
> **Adding a Model or Repository?** Ship a sibling test in the same PR — every new
> file under `packages/database/src/models/**` or `src/repositories/**` needs a
> matching `__tests__/<name>.test.ts`. See the **testing** skill
> (`.agents/skills/testing/references/db-model-test.md`) for the `getTestDB()`
> integration pattern, user-isolation tests, the BM25 `describe.skipIf(!isServerDB)`
> guard, and schema gotchas. CI's coverage patch gate won't reliably catch a brand-new
> untested file, so this is on you.
## Configuration
- Config: `drizzle.config.ts`
- Schemas: `packages/database/src/schemas/`
- Migrations: `packages/database/migrations/`
- Dialect: `postgresql` with `strict: true`
## Helper Functions
Location: `packages/database/src/schemas/_helpers.ts`
- `timestamptz(name)`: Timestamp with timezone
- `createdAt()`, `updatedAt()`, `accessedAt()`: Standard timestamp columns
- `timestamps`: Object with all three for easy spread
## Naming Conventions
- **Tables**: Plural snake\_case (`users`, `session_groups`)
- **Columns**: snake\_case (`user_id`, `created_at`)
- **New tables**: Check nearby existing tables before naming a new one. Preserve
the established noun family and suffix. For example, if the user-scoped table
is `user_xxx_logs`, the workspace-scoped counterpart should be
`workspace_xxx_logs`, not `workspace_xxx_records` or another new synonym.
```typescript
// ✅ Good: follows the existing user/workspace table family.
export const userSignupLogs = pgTable('user_signup_logs', { ... });
export const workspaceSignupLogs = pgTable('workspace_signup_logs', { ... });
// ❌ Bad: introduces a new suffix for the same concept.
export const workspaceSignupRecords = pgTable('workspace_signup_records', { ... });
```
## Column Definitions
### Primary Keys
Do not use auto-incrementing primary keys (`serial`, `bigserial`, generated
identity columns). They create sequence-state problems during cross-database
migrations, restores, and data copy jobs. Prefer text IDs from application
generators (`idGenerator`, `createNanoId`) or `uuid` for internal tables.
Keep `$defaultFn(...)` when a table normally owns ID generation. Callers can
still pass an explicit `id`; the default only runs when the insert omits it. Do
not remove the default just because one flow needs to supply a request-scoped ID.
```typescript
// ✅ Good: app-generated text ID; explicit inserts can still override it.
id: text('id')
.primaryKey()
.$defaultFn(() => idGenerator('agents'))
.notNull(),
// ❌ Bad: sequence state is fragile across DB migrations and restores.
id: serial('id').primaryKey(),
```
ID prefixes make entity types distinguishable. For internal tables, use `uuid`.
Do not use composite primary keys on new tables. Give every table a single-column
surrogate PK and carry business uniqueness in a `uniqueIndex` instead. PK columns
cannot be nullable, so when the uniqueness scope later grows by a nullable
dimension the composite PK must be torn down and rebuilt — exactly what happened
when `ai_providers` / `ai_models` were workspace-scoped (migration 0110 replaced
their composite PKs with a surrogate `_id` plus partial unique indexes). A unique
index still works as the arbiter for `onConflictDoUpdate` upserts.
```typescript
// ✅ Good: surrogate PK; uniqueness scope can evolve without a PK rebuild.
export const workspaceUserSettings = pgTable(
'workspace_user_settings',
{
id: uuid('id').defaultRandom().notNull().primaryKey(),
workspaceId: text('workspace_id').references(() => workspaces.id, { onDelete: 'cascade' }).notNull(),
userId: text('user_id').references(() => users.id, { onDelete: 'cascade' }).notNull(),
...timestamps,
},
(t) => [uniqueIndex('workspace_user_settings_workspace_id_user_id_unique').on(t.workspaceId, t.userId)],
);
// ❌ Bad: locked to exactly these columns; adding a nullable scope column
// (workspaceId, deviceId, …) later forces a full PK rebuild migration.
(t) => [primaryKey({ columns: [t.workspaceId, t.userId] })],
```
Existing composite PKs are legacy — leave them alone unless they block a scope
change, then migrate them the 0110 way.
### Foreign Keys
```typescript
userId: text('user_id')
.references(() => users.id, { onDelete: 'cascade' })
.notNull(),
```
### Timestamps
```typescript
...timestamps, // Spread from _helpers.ts
```
### Optional and Undefined Values
Do not introduce artificial sentinel strings for missing values, such as
`unknown`, unless the domain already has that explicit state and existing code
uses it consistently. Prefer nullable columns, optional TypeScript fields, or a
separate concrete status enum when the value is genuinely absent.
```typescript
// ✅ Good: absent until the final stage writes a real decision.
export type UserSignupLogFinalDecision = 'allow' | 'block' | 'error';
finalDecision: varchar('final_decision', { length: 32 }).$type<UserSignupLogFinalDecision>(),
// ❌ Bad: invents a new state that callers now need to handle everywhere.
export type UserSignupLogFinalDecision = 'allow' | 'block' | 'error' | 'unknown';
finalDecision: varchar('final_decision', { length: 32 })
.$type<UserSignupLogFinalDecision>()
.notNull()
.default('unknown');
```
### Database Enums
Default to **not** using PostgreSQL/Drizzle `pgEnum`. Database enums are
expensive to evolve safely: adding members needs migrations, removing or
renaming members is awkward, and deployment order becomes more fragile.
For product/business states, use `text()` or `varchar()` with a TypeScript value
type via `$type<...>()`. Keep those TS-only value types in the domain/shared type
module, then import them into the schema. For cloud DB schemas, that usually
means `cloudDB/types.ts`.
Do not copy existing DB enums as a pattern. Treat them as legacy or explicitly
reviewed exceptions. If a new `pgEnum` seems necessary, stop and justify why the
value set is effectively immutable and why the migration cost is acceptable.
### Field Descriptions
For columns whose meaning is not obvious from the name alone, add JSDoc on the
schema field. Include a concrete example when it clarifies the stored value or
the lifecycle moment that writes it. This is especially important for external
IDs, lifecycle statuses, denormalized snapshots, JSONB signals, and fields whose
name could mean either a request ID or a persisted row ID.
```typescript
// ✅ Good: explain the table's business object first, then only document
// non-obvious lifecycle or risk-control fields.
/**
* User signup logs - one row per signup flow, collecting stage-level
* risk-control decisions before and after the auth provider creates a user.
*/
export const userSignupLogs = pgTable('user_signup_logs', {
/** Final signup outcome reason, for example user_created, llm_block, or guard_error */
finalReason: text('final_reason'),
/** Aggregated risk level derived from stage decisions, for example block -> high */
riskLevel: varchar('risk_level', { length: 16 }).$type<UserSignupLogRiskLevel>(),
/** Ordered stage-level decisions and metadata grouped by signup review stage */
stageResults: jsonb('stage_results').$type<UserSignupLogStageResults>(),
});
// ❌ Bad: comments restate obvious column names without adding domain meaning.
/** User email */
email: text('email'),
```
### JSONB Types
Avoid `Record<string, unknown>` or similarly loose JSONB types for schema
columns. Define a concrete interface that describes the expected JSON shape, even
when most properties are optional. This keeps callers, migrations, and review
queries aligned on the same data contract.
```typescript
interface UserSignupLogMetadata {
payloadPath?: string;
requestPath?: string;
}
metadata: jsonb('metadata').$type<UserSignupLogMetadata>(),
```
```typescript
// ❌ Bad: hides the contract and makes downstream access untyped.
metadata: jsonb('metadata').$type<Record<string, unknown>>(),
```
A loosely-typed JSONB column is often a symptom of a deeper problem: the column
was reserved speculatively ("for future extension") and nothing actually writes
it. Don't add `metadata` / `extra` JSONB columns for hypothetical future needs —
a column earns its place only when a concrete writer ships alongside it. When
review finds such a column, the fix is to **delete the column**, not to invent
an interface for data that doesn't exist; add a properly-typed column once the
real requirement arrives.
### Indexes
```typescript
// Return array (object style deprecated)
(t) => [uniqueIndex('client_id_user_id_unique').on(t.clientId, t.userId)],
```
## Type Inference
```typescript
export const insertAgentSchema = createInsertSchema(agents);
export type NewAgent = typeof agents.$inferInsert;
export type AgentItem = typeof agents.$inferSelect;
```
## Example Pattern
```typescript
export const agents = pgTable(
'agents',
{
id: text('id')
.primaryKey()
.$defaultFn(() => idGenerator('agents'))
.notNull(),
slug: varchar('slug', { length: 100 })
.$defaultFn(() => randomSlug(4))
.unique(),
userId: text('user_id')
.references(() => users.id, { onDelete: 'cascade' })
.notNull(),
clientId: text('client_id'),
chatConfig: jsonb('chat_config').$type<LobeAgentChatConfig>(),
...timestamps,
},
(t) => [uniqueIndex('client_id_user_id_unique').on(t.clientId, t.userId)],
);
```
## Common Patterns
### Junction Tables (Many-to-Many)
The surrogate-PK rule above applies to junction tables too — pair uniqueness
goes in a `uniqueIndex`, not a composite PK (many existing junction tables
still use composite PKs; that is legacy, not the template):
```typescript
export const agentsKnowledgeBases = pgTable(
'agents_knowledge_bases',
{
id: uuid('id').defaultRandom().notNull().primaryKey(),
agentId: text('agent_id')
.references(() => agents.id, { onDelete: 'cascade' })
.notNull(),
knowledgeBaseId: text('knowledge_base_id')
.references(() => knowledgeBases.id, { onDelete: 'cascade' })
.notNull(),
userId: text('user_id')
.references(() => users.id, { onDelete: 'cascade' })
.notNull(),
enabled: boolean('enabled').default(true),
...timestamps,
},
(t) => [
uniqueIndex('agents_knowledge_bases_agent_id_knowledge_base_id_unique').on(
t.agentId,
t.knowledgeBaseId,
),
],
);
```
## Query Style
**Always use `db.select()` builder API. Never use `db.query.*` relational API** (`findMany`, `findFirst`, `with:`).
The relational API generates complex lateral joins with `json_build_array` that are fragile and hard to debug.
### Select Single Row
```typescript
// ✅ Good
const [result] = await this.db.select().from(agents).where(eq(agents.id, id)).limit(1);
return result;
// ❌ Bad: relational API
return this.db.query.agents.findFirst({
where: eq(agents.id, id),
});
```
### Select with JOIN
```typescript
// ✅ Good: explicit select + leftJoin
const rows = await this.db
.select({
runId: agentEvalRunTopics.runId,
score: agentEvalRunTopics.score,
testCase: agentEvalTestCases,
topic: topics,
})
.from(agentEvalRunTopics)
.leftJoin(agentEvalTestCases, eq(agentEvalRunTopics.testCaseId, agentEvalTestCases.id))
.leftJoin(topics, eq(agentEvalRunTopics.topicId, topics.id))
.where(eq(agentEvalRunTopics.runId, runId))
.orderBy(asc(agentEvalRunTopics.createdAt));
// ❌ Bad: relational API with `with:`
return this.db.query.agentEvalRunTopics.findMany({
where: eq(agentEvalRunTopics.runId, runId),
with: { testCase: true, topic: true },
});
```
### Select with Aggregation
```typescript
// ✅ Good: select + leftJoin + groupBy
const rows = await this.db
.select({
id: agentEvalDatasets.id,
name: agentEvalDatasets.name,
testCaseCount: count(agentEvalTestCases.id).as('testCaseCount'),
})
.from(agentEvalDatasets)
.leftJoin(agentEvalTestCases, eq(agentEvalDatasets.id, agentEvalTestCases.datasetId))
.groupBy(agentEvalDatasets.id);
```
### Raw SQL and Advanced Queries
Prefer Drizzle builders whenever the query reads clearly with `select`,
`insert().select()`, `update().from()`, joins, CTEs, and `groupBy` — this keeps
table/column references tied to schema, so changes surface as TypeScript errors.
Within a builder, expression-level `sql<T>` is fine for features lacking a helper
(JSON path, casts, aggregates, `CASE`, `NOW()`). Row locks are clauses, not
expressions — use `.for('update')`, never raw `FOR UPDATE`.
Use `COALESCE` only when null-handling is part of required DB semantics (nullable
JSONB append/merge, "keep first non-null"). Don't scatter
`COALESCE(excluded.col, current.col)` across ordinary upsert scalars just to avoid
an update object — build `set` from defined values only, and hide any remaining
SQL behind named helpers (`appendJsonbArray`, `mergeJsonbObject`, `keepFirstValue`)
so the method reads as business intent, not SQL plumbing.
```typescript
// ✅ Scalars included only when present; SQL hidden behind a named helper.
const updateValues = compactUndefined({
email: record.email ?? undefined,
ip: record.ip ?? undefined,
});
await db.insert(userSignupLogs).values(values).onConflictDoUpdate({
set: { ...updateValues, stageResults: appendStageResult(stage, result), updatedAt: now },
target: userSignupLogs.id,
});
// ❌ Every scalar becomes SQL plumbing.
set: {
email: sql`COALESCE(excluded.email, ${userSignupLogs.email})`,
ip: sql`COALESCE(excluded.ip, ${userSignupLogs.ip})`,
}
```
When refactoring raw SQL:
- Preserve query shape on latency-sensitive paths. If raw SQL is one roundtrip,
don't split it into multiple depth-based queries just to drop `execute`.
- Use `$with(...)` + `insert().select()` / `update().from()` for multi-step
single-roundtrip writes Drizzle can express.
- Don't rely on `execute<MyRow>(sql...)` for safety — it types rows but doesn't keep
selected columns in sync with schema changes.
- If only a PostgreSQL feature Drizzle can't express works, keep the raw SQL and
tighten it: schema refs in interpolations, explicit user scope, a narrow row
interface, and regression tests.
Recursive CTEs are the canonical "keep raw" case — there's no clean `WITH RECURSIVE`
builder, and a rewrite would add depth-based roundtrips:
```typescript
interface TaskTreeRow {
id: string;
parent_task_id: string | null;
}
// execute<T> acceptable: no clean WITH RECURSIVE builder. Keep schema refs in the
// interpolations and scope every leg to the user.
const { rows } = await db.execute<TaskTreeRow>(sql`
WITH RECURSIVE task_tree AS (
SELECT ${tasks.id}, ${tasks.parentTaskId}
FROM ${tasks}
WHERE ${tasks.id} = ${rootTaskId} AND ${tasks.createdByUserId} = ${userId}
UNION ALL
SELECT ${tasks.id}, ${tasks.parentTaskId}
FROM ${tasks}
JOIN task_tree ON ${tasks.parentTaskId} = task_tree.id
WHERE ${tasks.createdByUserId} = ${userId}
)
SELECT * FROM task_tree
`);
```
### One-to-Many (Separate Queries)
When you need a parent record with its children, use two queries instead of relational `with:`:
```typescript
// ✅ Good: two simple queries
const [dataset] = await this.db
.select()
.from(agentEvalDatasets)
.where(eq(agentEvalDatasets.id, id))
.limit(1);
if (!dataset) return undefined;
const testCases = await this.db
.select()
.from(agentEvalTestCases)
.where(eq(agentEvalTestCases.datasetId, id))
.orderBy(asc(agentEvalTestCases.sortOrder));
return { ...dataset, testCases };
```
## Database Migrations
See the `db-migrations` skill for the detailed migration guide.
+34
View File
@@ -3,3 +3,37 @@
This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` before writing any code. Heed deprecation notices.
<!-- END:nextjs-agent-rules -->
# Fluxent Web
## Commands
- Use `pnpm`. The available checks are `pnpm lint`, `pnpm typecheck`, and `pnpm build`; run `pnpm typecheck && pnpm build` after application changes. There is no committed test suite or CI workflow.
- Add UI primitives only through `pnpm dlx shadcn@latest add <component> --yes`. Before using a shadcn component, run `pnpm dlx shadcn@latest docs <component>` and read the referenced docs.
- Formatting is Prettier with Tailwind sorting (`pnpm format`). It uses double quotes, no semicolons, 2 spaces, LF, and 80-column wrapping.
## App And Auth
- This is a single Next.js App Router application; routes live under `app/`. Authenticated pages compose `SidebarProvider`, `AppSidebar`, and `SidebarInset` themselves.
- Next.js 16 uses `proxy.ts`, not `middleware.ts`. Keep `proxy.ts` edge-safe: it imports only `lib/auth/config.ts`, never database or password-hashing code.
- Login, registration, and `/api/auth` are public. All other routes are protected by `authConfig`; server pages and actions must still validate the session themselves.
- `lib/auth/index.ts` owns Node-side Auth.js providers and database work. OIDC is configuration-driven and disabled unless `AUTH_OIDC_ENABLED=true` with a complete issuer/client configuration.
## Database
- PostgreSQL access is Drizzle + `postgres`; the schema source of truth is `lib/db/schema.ts`. Drizzle Kit loads `DATABASE_URL` from `.env.local` and uses `drizzle.config.ts`.
- Use `pnpm drizzle-kit push --force` only when a schema change is intended; it can apply destructive changes. `pnpm tsx lib/db/reset.ts` drops `transactions`, `channels`, `accounts`, `user_accounts`, and `users` and must never be run casually.
- Business tables are user-scoped. Every read, update, or delete in `lib/actions/` must obtain `auth().user.id` and constrain the query with that `user_id`; validate that referenced account/channel IDs belong to the same user.
- Keep soft deletion (`deleted_at`) semantics in reads and mutations. Account and channel management actions already follow this model.
## UI And Copy
- The project uses shadcn `base-nova` on `@base-ui/react`, Lucide icons, Tailwind v4, and semantic CSS variables from `app/globals.css`.
- Prefer stock shadcn composition and variants over custom styling. Use `className` only for necessary layout, responsiveness, truncation, or stable dimensions; do not override component padding, margins, colors, typography, or default Dialog/Footer behavior without a verified need.
- Forms use `FieldGroup`, `Field`, and `FieldLabel`; use `FieldSet`/`FieldLegend` only when the grouping adds user-facing meaning. Put `SelectItem` inside `SelectGroup`.
- Errors and callouts use `Alert`; destructive errors use `Alert variant="destructive"` with `AlertTitle` and `AlertDescription`, never a hand-styled `div`.
- Follow the default Dialog composition: `DialogHeader`, form/content, then `DialogFooter` as a direct `DialogContent` child. Do not add `p-0`, custom negative margins, fixed heights, sticky footers, or isolated scroll containers unless the task explicitly requires them.
- Use `Button`, `Badge`, `Empty`, `Separator`, `Tooltip`, and other installed primitives instead of recreating them. Use `data-icon="inline-start"` or `data-icon="inline-end"` for icons in buttons.
- Use `gap-*`, never `space-x-*` or `space-y-*`; use semantic tokens instead of raw color palettes or manual `dark:` overrides.
- Product-facing UI copy is Chinese only. Do not add English parentheticals to menus, labels, options, or headings; retain user-entered business values such as currency codes and card brands verbatim.
- Sidebar active state must derive from `usePathname()` with exact matching for `/` and route-boundary matching for child paths. Do not add navigation links until their route exists.
+316
View File
@@ -0,0 +1,316 @@
"use client"
import * as React from "react"
import { CircleAlertIcon, Loader2Icon } from "lucide-react"
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"
import { Button } from "@/components/ui/button"
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog"
import { Field, FieldGroup, FieldLabel } from "@/components/ui/field"
import { Input } from "@/components/ui/input"
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select"
import {
createAccountAction,
updateAccountAction,
type AccountInput,
type AccountWithChannels,
} from "@/lib/actions/account"
type AccountType = "BANK" | "E_WALLET" | "CASH"
const accountLabels = {
BANK: "银行账户",
E_WALLET: "电子钱包",
CASH: "现金",
} as const
const balanceLabels = {
ASSET: "资产类",
LIABILITY: "负债类",
EQUITY: "权益类",
} as const
export function AccountDialog({
open,
onOpenChange,
account,
onSuccess,
}: {
open: boolean
onOpenChange: (open: boolean) => void
account?: AccountWithChannels | null
onSuccess: () => void
}) {
const editing = Boolean(account)
const [accountType, setAccountType] = React.useState<AccountType>(
account?.accountType || "BANK"
)
const [balanceType, setBalanceType] = React.useState<
"ASSET" | "LIABILITY" | "EQUITY"
>(account?.balanceType || "ASSET")
const [name, setName] = React.useState(account?.name || "")
const [primaryCurrency, setPrimaryCurrency] = React.useState(
account?.primaryCurrency || ""
)
const [supportedCurrencies, setSupportedCurrencies] = React.useState(
account?.supportedCurrencies?.join(", ") || ""
)
const [remark, setRemark] = React.useState(account?.remark || "")
const [issuerName, setIssuerName] = React.useState(account?.issuerName || "")
const [accountNumber, setAccountNumber] = React.useState(
account?.accountNumber || ""
)
const [platform, setPlatform] = React.useState(account?.platform || "")
const [accountId, setAccountId] = React.useState(account?.accountId || "")
const [location, setLocation] = React.useState(account?.location || "")
const [isPending, setIsPending] = React.useState(false)
const [errorMessage, setErrorMessage] = React.useState<string | null>(null)
const submit = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault()
setErrorMessage(null)
if (!name.trim()) {
setErrorMessage("请输入账户名称")
return
}
const currencies = supportedCurrencies
.split(/[,\s]+/)
.map((item) => item.trim().toUpperCase())
.filter(Boolean)
const payload: AccountInput = {
name: name.trim(),
accountType,
balanceType,
primaryCurrency: primaryCurrency.trim().toUpperCase() || null,
supportedCurrencies: currencies.length ? currencies : null,
remark: remark.trim() || null,
issuerName: accountType === "BANK" ? issuerName.trim() || null : null,
accountNumber:
accountType === "BANK" ? accountNumber.trim() || null : null,
platform: accountType === "E_WALLET" ? platform.trim() || null : null,
accountId: accountType === "E_WALLET" ? accountId.trim() || null : null,
location: accountType === "CASH" ? location.trim() || null : null,
}
setIsPending(true)
try {
const result =
editing && account
? await updateAccountAction(account.id, payload)
: await createAccountAction(payload)
if (!result.success) {
setErrorMessage(result.error || "保存失败,请检查输入")
return
}
onOpenChange(false)
onSuccess()
} catch {
setErrorMessage("网络异常,提交失败")
} finally {
setIsPending(false)
}
}
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-lg">
<DialogHeader>
<DialogTitle>{editing ? "编辑资金账户" : "新建资金账户"}</DialogTitle>
<DialogDescription>
</DialogDescription>
</DialogHeader>
<form id="account-form" onSubmit={submit}>
<FieldGroup>
{errorMessage && (
<Alert variant="destructive">
<CircleAlertIcon />
<AlertTitle></AlertTitle>
<AlertDescription>{errorMessage}</AlertDescription>
</Alert>
)}
<FieldGroup className="grid gap-3 sm:grid-cols-2">
<Field>
<FieldLabel htmlFor="accountType"></FieldLabel>
<Select
value={accountType}
onValueChange={(value) =>
value && setAccountType(value as AccountType)
}
>
<SelectTrigger id="accountType">
<SelectValue>{accountLabels[accountType]}</SelectValue>
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem value="BANK"></SelectItem>
<SelectItem value="E_WALLET"></SelectItem>
<SelectItem value="CASH"></SelectItem>
</SelectGroup>
</SelectContent>
</Select>
</Field>
<Field>
<FieldLabel htmlFor="balanceType"></FieldLabel>
<Select
value={balanceType}
onValueChange={(value) =>
value && setBalanceType(value as typeof balanceType)
}
>
<SelectTrigger id="balanceType">
<SelectValue>{balanceLabels[balanceType]}</SelectValue>
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem value="ASSET"></SelectItem>
<SelectItem value="LIABILITY"></SelectItem>
<SelectItem value="EQUITY"></SelectItem>
</SelectGroup>
</SelectContent>
</Select>
</Field>
</FieldGroup>
<FieldGroup>
<Field>
<FieldLabel htmlFor="accountName"></FieldLabel>
<Input
id="accountName"
value={name}
onChange={(event) => setName(event.target.value)}
placeholder="例如:招商银行个人消费卡账户"
disabled={isPending}
required
/>
</Field>
<FieldGroup className="grid gap-3 sm:grid-cols-2">
<Field>
<FieldLabel htmlFor="primaryCurrency"></FieldLabel>
<Input
id="primaryCurrency"
value={primaryCurrency}
onChange={(event) => setPrimaryCurrency(event.target.value)}
placeholder="例如:HKD"
disabled={isPending}
/>
</Field>
<Field>
<FieldLabel htmlFor="supportedCurrencies">
</FieldLabel>
<Input
id="supportedCurrencies"
value={supportedCurrencies}
onChange={(event) =>
setSupportedCurrencies(event.target.value)
}
placeholder="用逗号分隔"
disabled={isPending}
/>
</Field>
</FieldGroup>
</FieldGroup>
<FieldGroup className="grid gap-3 sm:grid-cols-2">
{accountType === "BANK" && (
<>
<Field>
<FieldLabel htmlFor="issuerName"></FieldLabel>
<Input
id="issuerName"
value={issuerName}
onChange={(event) => setIssuerName(event.target.value)}
placeholder="例如:汇丰银行"
disabled={isPending}
/>
</Field>
<Field>
<FieldLabel htmlFor="accountNumber">
</FieldLabel>
<Input
id="accountNumber"
value={accountNumber}
onChange={(event) => setAccountNumber(event.target.value)}
placeholder="可填写完整账号或尾号"
disabled={isPending}
/>
</Field>
</>
)}
{accountType === "E_WALLET" && (
<>
<Field>
<FieldLabel htmlFor="platform"></FieldLabel>
<Input
id="platform"
value={platform}
onChange={(event) => setPlatform(event.target.value)}
placeholder="例如:支付宝"
disabled={isPending}
/>
</Field>
<Field>
<FieldLabel htmlFor="accountId"></FieldLabel>
<Input
id="accountId"
value={accountId}
onChange={(event) => setAccountId(event.target.value)}
placeholder="手机号、邮箱或会员号"
disabled={isPending}
/>
</Field>
</>
)}
{accountType === "CASH" && (
<Field>
<FieldLabel htmlFor="location"></FieldLabel>
<Input
id="location"
value={location}
onChange={(event) => setLocation(event.target.value)}
placeholder="例如:随身钱包"
disabled={isPending}
/>
</Field>
)}
</FieldGroup>
<Field>
<FieldLabel htmlFor="remark"></FieldLabel>
<Input
id="remark"
value={remark}
onChange={(event) => setRemark(event.target.value)}
placeholder="可选"
disabled={isPending}
/>
</Field>
</FieldGroup>
</form>
<DialogFooter>
<Button
type="button"
variant="outline"
onClick={() => onOpenChange(false)}
disabled={isPending}
>
</Button>
<Button form="account-form" type="submit" disabled={isPending}>
{isPending && (
<Loader2Icon data-icon="inline-start" className="animate-spin" />
)}
{editing ? "保存修改" : "创建账户"}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)
}
+470
View File
@@ -0,0 +1,470 @@
"use client"
import * as React from "react"
import { useRouter } from "next/navigation"
import {
BanknoteIcon,
CreditCardIcon,
LandmarkIcon,
Layers2Icon,
Loader2Icon,
MoreHorizontalIcon,
PowerIcon,
PowerOffIcon,
PencilIcon,
PlusIcon,
SearchIcon,
Trash2Icon,
WalletCardsIcon,
} from "lucide-react"
import {
deleteAccountAction,
toggleAccountActiveAction,
type AccountWithChannels,
} from "@/lib/actions/account"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import { Input } from "@/components/ui/input"
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select"
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip"
import { AccountDialog } from "./account-dialog"
const typeLabels = {
BANK: "银行账户",
E_WALLET: "电子钱包",
CASH: "现金",
} as const
const balanceLabels = {
ASSET: "资产类",
LIABILITY: "负债类",
EQUITY: "权益类",
} as const
const typeIcon = (type: AccountWithChannels["accountType"]) =>
type === "BANK" ? (
<LandmarkIcon className="size-4" />
) : type === "E_WALLET" ? (
<WalletCardsIcon className="size-4" />
) : (
<BanknoteIcon className="size-4" />
)
export function AccountsView({
initialAccounts,
}: {
initialAccounts: AccountWithChannels[]
}) {
const router = useRouter()
const [accountsList, setAccountsList] = React.useState(initialAccounts)
const [searchQuery, setSearchQuery] = React.useState("")
const [typeFilter, setTypeFilter] = React.useState("ALL")
const [balanceFilter, setBalanceFilter] = React.useState("ALL")
const [dialogOpen, setDialogOpen] = React.useState(false)
const [editingAccount, setEditingAccount] =
React.useState<AccountWithChannels | null>(null)
const [deletingAccount, setDeletingAccount] =
React.useState<AccountWithChannels | null>(null)
const [isDeleting, setIsDeleting] = React.useState(false)
const [togglingId, setTogglingId] = React.useState<string | null>(null)
const filteredAccounts = React.useMemo(() => {
const query = searchQuery.trim().toLowerCase()
return accountsList.filter((account) => {
const text = [
account.name,
account.issuerName,
account.platform,
account.accountNumber,
account.accountId,
account.primaryCurrency,
account.remark,
]
.filter(Boolean)
.join(" ")
.toLowerCase()
return (
(!query || text.includes(query)) &&
(typeFilter === "ALL" || account.accountType === typeFilter) &&
(balanceFilter === "ALL" || account.balanceType === balanceFilter)
)
})
}, [accountsList, balanceFilter, searchQuery, typeFilter])
const groups = (
Object.keys(balanceLabels) as Array<keyof typeof balanceLabels>
)
.map((balanceType) => ({
balanceType,
accounts: filteredAccounts.filter(
(account) => account.balanceType === balanceType
),
}))
.filter((group) => group.accounts.length)
const clearFilters = () => {
setSearchQuery("")
setTypeFilter("ALL")
setBalanceFilter("ALL")
}
const hasFilters =
Boolean(searchQuery) || typeFilter !== "ALL" || balanceFilter !== "ALL"
const openCreate = () => {
setEditingAccount(null)
setDialogOpen(true)
}
const toggleActive = async (
account: AccountWithChannels,
checked: boolean
) => {
setTogglingId(account.id)
setAccountsList((items) =>
items.map((item) =>
item.id === account.id ? { ...item, isActive: checked } : item
)
)
try {
const result = await toggleAccountActiveAction(account.id, checked)
if (!result.success)
setAccountsList((items) =>
items.map((item) =>
item.id === account.id ? { ...item, isActive: !checked } : item
)
)
else router.refresh()
} catch {
setAccountsList((items) =>
items.map((item) =>
item.id === account.id ? { ...item, isActive: !checked } : item
)
)
} finally {
setTogglingId(null)
}
}
const confirmDelete = async () => {
if (!deletingAccount) return
setIsDeleting(true)
try {
const result = await deleteAccountAction(deletingAccount.id)
if (result.success) {
setAccountsList((items) =>
items.filter((item) => item.id !== deletingAccount.id)
)
setDeletingAccount(null)
router.refresh()
}
} finally {
setIsDeleting(false)
}
}
return (
<TooltipProvider>
<main className="flex min-w-0 flex-1 flex-col gap-4 p-4 md:p-6 lg:p-8">
<div className="flex flex-col gap-4 border-b border-border/70 pb-5 sm:flex-row sm:items-end sm:justify-between">
<div>
<p className="mb-1 text-xs font-medium text-muted-foreground">
</p>
<h1 className="font-heading text-2xl font-semibold tracking-tight">
</h1>
<p className="mt-1 text-sm text-muted-foreground">
</p>
</div>
<Button onClick={openCreate} className="w-full sm:w-auto">
<PlusIcon data-icon="inline-start" />
</Button>
</div>
<div className="flex flex-col gap-2 md:flex-row md:items-center">
<div className="relative min-w-0 flex-1 md:max-w-md">
<SearchIcon className="pointer-events-none absolute top-1/2 left-2.5 size-4 -translate-y-1/2 text-muted-foreground" />
<Input
value={searchQuery}
onChange={(event) => setSearchQuery(event.target.value)}
placeholder="搜索账户、机构、账号或币种"
className="pl-8"
/>
</div>
<div className="grid grid-cols-2 gap-2 sm:flex">
<Select
value={typeFilter}
onValueChange={(value) => setTypeFilter(value ?? "ALL")}
>
<SelectTrigger className="w-full sm:w-32">
<SelectValue>
{typeFilter === "ALL"
? "全部类型"
: typeLabels[typeFilter as keyof typeof typeLabels]}
</SelectValue>
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem value="ALL"></SelectItem>
<SelectItem value="BANK"></SelectItem>
<SelectItem value="E_WALLET"></SelectItem>
<SelectItem value="CASH"></SelectItem>
</SelectGroup>
</SelectContent>
</Select>
<Select
value={balanceFilter}
onValueChange={(value) => setBalanceFilter(value ?? "ALL")}
>
<SelectTrigger className="w-full sm:w-32">
<SelectValue>
{balanceFilter === "ALL"
? "全部分类"
: balanceLabels[
balanceFilter as keyof typeof balanceLabels
]}
</SelectValue>
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem value="ALL"></SelectItem>
<SelectItem value="ASSET"></SelectItem>
<SelectItem value="LIABILITY"></SelectItem>
<SelectItem value="EQUITY"></SelectItem>
</SelectGroup>
</SelectContent>
</Select>
</div>
{hasFilters && (
<Button variant="ghost" size="sm" onClick={clearFilters}>
</Button>
)}
</div>
{groups.length ? (
<div className="flex flex-col gap-6">
{groups.map(({ balanceType, accounts }) => (
<section
key={balanceType}
aria-labelledby={`account-group-${balanceType}`}
>
<div className="mb-2 flex items-center gap-2">
<h2
id={`account-group-${balanceType}`}
className="text-sm font-semibold"
>
{balanceLabels[balanceType]}
</h2>
<span className="text-xs text-muted-foreground">
{accounts.length}
</span>
</div>
<div className="overflow-hidden rounded-lg border border-border/70 bg-card">
<div className="hidden grid-cols-[minmax(220px,1.6fr)_110px_110px_64px] gap-3 border-b bg-muted/30 px-4 py-2.5 text-[11px] font-medium text-muted-foreground md:grid">
<span></span>
<span></span>
<span></span>
<span />
</div>
{accounts.map((account) => {
const actionMenu = (
<DropdownMenu>
<Tooltip>
<TooltipTrigger
render={
<DropdownMenuTrigger
render={
<Button
variant="ghost"
size="icon-sm"
aria-label="打开账户操作"
/>
}
/>
}
>
<MoreHorizontalIcon />
</TooltipTrigger>
<TooltipContent></TooltipContent>
</Tooltip>
<DropdownMenuContent align="end">
<DropdownMenuItem
disabled={togglingId === account.id}
onClick={() =>
toggleActive(account, !account.isActive)
}
>
{account.isActive ? (
<PowerOffIcon data-icon="inline-start" />
) : (
<PowerIcon data-icon="inline-start" />
)}
{account.isActive ? "停用账户" : "启用账户"}
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => {
setEditingAccount(account)
setDialogOpen(true)
}}
>
<PencilIcon data-icon="inline-start" />
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
variant="destructive"
onClick={() => setDeletingAccount(account)}
>
<Trash2Icon data-icon="inline-start" />
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)
return (
<div
key={account.id}
className={`grid gap-3 border-b border-border/60 px-4 py-3 last:border-b-0 md:grid-cols-[minmax(220px,1.6fr)_110px_110px_64px] md:items-center ${!account.isActive ? "opacity-60" : ""}`}
>
<div className="flex min-w-0 items-center justify-between gap-3">
<div className="flex min-w-0 items-center gap-3">
<span className="flex size-8 shrink-0 items-center justify-center rounded-md bg-muted text-muted-foreground">
{typeIcon(account.accountType)}
</span>
<div className="min-w-0">
<div className="flex min-w-0 items-center gap-2">
<p className="truncate text-sm font-medium">
{account.name}
</p>
{!account.isActive && (
<Badge variant="secondary"></Badge>
)}
</div>
<p className="truncate text-xs text-muted-foreground">
{account.issuerName ||
account.platform ||
account.location ||
account.remark ||
""}
</p>
</div>
</div>
<div className="shrink-0 md:hidden">{actionMenu}</div>
</div>
<div className="flex justify-between text-xs md:block">
<span className="text-muted-foreground md:hidden">
</span>
{typeLabels[account.accountType]}
</div>
<div className="flex justify-between text-xs md:block">
<span className="text-muted-foreground md:hidden">
</span>
<span className="inline-flex items-center gap-1">
<CreditCardIcon className="size-3.5 text-muted-foreground" />
{account.channelCount}
</span>
</div>
<div className="hidden md:flex md:justify-end">
{actionMenu}
</div>
</div>
)
})}
</div>
</section>
))}
</div>
) : (
<div className="flex min-h-56 flex-col items-center justify-center rounded-lg border border-dashed border-border px-6 py-10 text-center">
<Layers2Icon className="size-6 text-muted-foreground" />
<h2 className="mt-3 text-sm font-semibold">
{hasFilters ? "没有符合条件的账户" : "还没有资金账户"}
</h2>
<p className="mt-1 text-xs text-muted-foreground">
{hasFilters
? "调整关键词或筛选条件后再试。"
: "创建账户后即可关联支付渠道。"}
</p>
<div className="mt-4 flex gap-2">
{hasFilters && (
<Button variant="outline" size="sm" onClick={clearFilters}>
</Button>
)}
<Button size="sm" onClick={openCreate}>
<PlusIcon data-icon="inline-start" />
</Button>
</div>
</div>
)}
<AccountDialog
key={`${dialogOpen}-${editingAccount?.id ?? "new"}`}
open={dialogOpen}
onOpenChange={setDialogOpen}
account={editingAccount}
onSuccess={() => router.refresh()}
/>
<Dialog
open={Boolean(deletingAccount)}
onOpenChange={(open) => !open && setDeletingAccount(null)}
>
<DialogContent className="sm:max-w-sm">
<DialogHeader>
<DialogTitle></DialogTitle>
<DialogDescription>
{deletingAccount?.name}
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button
variant="outline"
onClick={() => setDeletingAccount(null)}
disabled={isDeleting}
>
</Button>
<Button
variant="destructive"
onClick={confirmDelete}
disabled={isDeleting}
>
{isDeleting && (
<Loader2Icon
data-icon="inline-start"
className="animate-spin"
/>
)}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</main>
</TooltipProvider>
)
}
+44
View File
@@ -0,0 +1,44 @@
import { redirect } from "next/navigation"
import { auth } from "@/lib/auth"
import { getAccountsAction } from "@/lib/actions/account"
import { AppSidebar } from "@/components/app-sidebar"
import { AppHeader } from "@/components/app-header"
import { SidebarInset, SidebarProvider } from "@/components/ui/sidebar"
import { AccountsView } from "./accounts-view"
export default async function AccountsPage() {
const session = await auth()
if (!session?.user) {
redirect("/login")
}
const user = session.user
const userName = user.name || "Fluxent 用户"
const userEmail = user.email || ""
const userAvatar = user.image || null
const accountsRes = await getAccountsAction()
const initialAccounts =
accountsRes.success && accountsRes.data ? accountsRes.data : []
return (
<SidebarProvider>
<AppSidebar
user={{
name: userName,
email: userEmail,
avatar: userAvatar,
}}
/>
<SidebarInset>
<AppHeader title="资金账户" />
<AccountsView
key={JSON.stringify(initialAccounts)}
initialAccounts={initialAccounts}
/>
</SidebarInset>
</SidebarProvider>
)
}
+3
View File
@@ -0,0 +1,3 @@
import { handlers } from "@/lib/auth";
export const { GET, POST } = handlers;
+486
View File
@@ -0,0 +1,486 @@
"use client"
import * as React from "react"
import Image from "next/image"
import { CircleAlertIcon, Loader2Icon } from "lucide-react"
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog"
import { Button } from "@/components/ui/button"
import { Checkbox } from "@/components/ui/checkbox"
import { Input } from "@/components/ui/input"
import {
Field,
FieldGroup,
FieldLabel,
FieldLegend,
FieldSet,
} from "@/components/ui/field"
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select"
import {
Combobox,
ComboboxContent,
ComboboxEmpty,
ComboboxInput,
ComboboxItem,
ComboboxList,
} from "@/components/ui/combobox"
import {
CARD_BRANDS,
CARD_BRAND_LABELS,
detectCardBrand,
getCardBrandLogoUrl,
normalizeCardBrand,
type CardBrand,
} from "@/lib/payment/card-brand"
import {
createChannelAction,
updateChannelAction,
type ChannelInput,
type ChannelWithAccountNames,
} from "@/lib/actions/channel"
import { type AccountWithChannels } from "@/lib/actions/account"
type ChannelType = "PAYMENT_CARD" | "E_WALLET" | "CASH" | "TRANSFER"
const channelLabels = {
PAYMENT_CARD: "支付卡",
E_WALLET: "电子钱包",
CASH: "现金",
TRANSFER: "转账",
} as const
export function ChannelDialog({
open,
onOpenChange,
channel,
accounts,
onSuccess,
}: {
open: boolean
onOpenChange: (open: boolean) => void
channel?: ChannelWithAccountNames | null
accounts: AccountWithChannels[]
onSuccess: () => void
}) {
const editing = Boolean(channel)
const [channelType, setChannelType] = React.useState<ChannelType>(
channel?.channelType || "PAYMENT_CARD"
)
const [selectedAccounts, setSelectedAccounts] = React.useState<string[]>(
channel
? Array.isArray(channel.refAccounts)
? channel.refAccounts
: []
: accounts.length
? [accounts[0].id]
: []
)
const [desc, setDesc] = React.useState(channel?.desc || "")
const [region, setRegion] = React.useState(channel?.region || "")
const [issuerName, setIssuerName] = React.useState(channel?.issuerName || "")
const [cardType, setCardType] = React.useState<"CREDIT" | "DEBIT" | "NONE">(
channel?.cardType || "CREDIT"
)
const [cardBrand, setCardBrand] = React.useState(channel?.cardBrand || "")
const [cardNumberFull, setCardNumberFull] = React.useState(
channel?.cardNumberFull || ""
)
const [cardNumberSuffix, setCardNumberSuffix] = React.useState(
channel?.cardNumberFull ? "" : channel?.cardNumberSuffix || ""
)
const [platform, setPlatform] = React.useState(channel?.platform || "")
const [platformAccountId, setPlatformAccountId] = React.useState(
channel?.platformAccountId || ""
)
const [subChannel, setSubChannel] = React.useState(channel?.subChannel || "")
const [subChannelType, setSubChannelType] = React.useState<
"CREDIT" | "DEBIT" | "NONE"
>(channel?.subChannelType || "DEBIT")
const [isPending, setIsPending] = React.useState(false)
const [errorMessage, setErrorMessage] = React.useState<string | null>(null)
const cardBrandRequest = React.useRef(0)
const submit = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault()
setErrorMessage(null)
if (!selectedAccounts.length) {
setErrorMessage("请至少关联一个资金账户")
return
}
const full = cardNumberFull.trim()
const payload: ChannelInput = {
channelType,
refAccounts: selectedAccounts,
desc: desc.trim() || null,
region: region.trim().toUpperCase() || null,
issuerName:
channelType === "PAYMENT_CARD" ? issuerName.trim() || null : null,
cardType:
channelType === "PAYMENT_CARD" && cardType !== "NONE" ? cardType : null,
cardNumberFull: channelType === "PAYMENT_CARD" ? full || null : null,
cardNumberSuffix:
channelType === "PAYMENT_CARD" && !full
? cardNumberSuffix.trim() || null
: null,
cardBrand:
channelType === "PAYMENT_CARD" ? normalizeCardBrand(cardBrand) : null,
platform: channelType === "E_WALLET" ? platform.trim() || null : null,
platformAccountId:
channelType === "E_WALLET" ? platformAccountId.trim() || null : null,
subChannel: channelType === "E_WALLET" ? subChannel.trim() || null : null,
subChannelType:
channelType === "E_WALLET" && subChannelType !== "NONE"
? subChannelType
: null,
}
setIsPending(true)
try {
const result =
editing && channel
? await updateChannelAction(channel.id, payload)
: await createChannelAction(payload)
if (!result.success) {
setErrorMessage(result.error || "保存失败,请检查输入")
return
}
onOpenChange(false)
onSuccess()
} catch {
setErrorMessage("网络异常,提交失败")
} finally {
setIsPending(false)
}
}
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-xl">
<DialogHeader>
<DialogTitle>{editing ? "编辑支付渠道" : "新建支付渠道"}</DialogTitle>
<DialogDescription className="text-xs">
</DialogDescription>
</DialogHeader>
<form id="channel-form" onSubmit={submit}>
<FieldGroup>
{errorMessage && (
<Alert variant="destructive">
<CircleAlertIcon />
<AlertTitle></AlertTitle>
<AlertDescription>{errorMessage}</AlertDescription>
</Alert>
)}
<FieldGroup>
<Field>
<FieldLabel htmlFor="desc"></FieldLabel>
<Input
id="desc"
value={desc}
onChange={(event) => setDesc(event.target.value)}
placeholder="例如:日常主用渠道"
disabled={isPending}
/>
</Field>
</FieldGroup>
<FieldGroup>
<Field>
<FieldLabel htmlFor="channelType"></FieldLabel>
<Select
value={channelType}
onValueChange={(value) =>
value && setChannelType(value as ChannelType)
}
>
<SelectTrigger id="channelType">
<SelectValue>{channelLabels[channelType]}</SelectValue>
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem value="PAYMENT_CARD"></SelectItem>
<SelectItem value="E_WALLET"></SelectItem>
<SelectItem value="CASH"></SelectItem>
<SelectItem value="TRANSFER"></SelectItem>
</SelectGroup>
</SelectContent>
</Select>
</Field>
</FieldGroup>
<FieldSet>
<div className="flex items-center justify-between">
<FieldLegend variant="label"></FieldLegend>
<span className="text-xs text-muted-foreground">
{selectedAccounts.length}
</span>
</div>
<div className="flex flex-col gap-1.5">
{accounts.length ? (
accounts.map((account) => (
<label
key={account.id}
className="flex cursor-pointer items-center gap-2 rounded-md border border-border/70 px-3 py-2 text-xs hover:bg-muted/50"
>
<Checkbox
checked={selectedAccounts.includes(account.id)}
onCheckedChange={(checked) =>
setSelectedAccounts((items) =>
checked
? [...items, account.id]
: items.filter((id) => id !== account.id)
)
}
disabled={isPending}
/>
<span className="min-w-0 flex-1 truncate">
{account.name}
</span>
<span className="font-mono text-muted-foreground">
{account.primaryCurrency || "通用"}
</span>
</label>
))
) : (
<p className="border border-dashed border-border p-3 text-center text-xs text-muted-foreground">
</p>
)}
</div>
</FieldSet>
<FieldGroup>
{channelType === "PAYMENT_CARD" && (
<div className="grid gap-3 sm:grid-cols-2">
<Field>
<FieldLabel htmlFor="issuerName"></FieldLabel>
<Input
id="issuerName"
value={issuerName}
onChange={(event) => setIssuerName(event.target.value)}
placeholder="例如:汇丰银行"
disabled={isPending}
/>
</Field>
<Field>
<FieldLabel htmlFor="cardBrand"></FieldLabel>
<Combobox
items={CARD_BRANDS}
value={normalizeCardBrand(cardBrand)}
onValueChange={(value) => {
cardBrandRequest.current += 1
setCardBrand((value as CardBrand | null) || "")
}}
autoHighlight
>
<ComboboxInput
id="cardBrand"
placeholder="选择卡组织"
disabled={isPending}
/>
<ComboboxContent>
<ComboboxEmpty></ComboboxEmpty>
<ComboboxList>
{(brand: CardBrand) => (
<ComboboxItem key={brand} value={brand}>
<Image
src={getCardBrandLogoUrl(brand) || ""}
alt=""
width={20}
height={20}
className="size-5 object-contain"
/>
{CARD_BRAND_LABELS[brand]}
</ComboboxItem>
)}
</ComboboxList>
</ComboboxContent>
</Combobox>
</Field>
<Field>
<FieldLabel htmlFor="cardType"></FieldLabel>
<Select
value={cardType}
onValueChange={(value) =>
value && setCardType(value as typeof cardType)
}
>
<SelectTrigger id="cardType">
<SelectValue>
{cardType === "CREDIT"
? "信用卡"
: cardType === "DEBIT"
? "借记卡"
: "未指定"}
</SelectValue>
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem value="CREDIT"></SelectItem>
<SelectItem value="DEBIT"></SelectItem>
<SelectItem value="NONE"></SelectItem>
</SelectGroup>
</SelectContent>
</Select>
</Field>
<Field>
<FieldLabel htmlFor="region"></FieldLabel>
<Input
id="region"
value={region}
onChange={(event) => setRegion(event.target.value)}
placeholder="例如:HK"
disabled={isPending}
/>
</Field>
{!cardNumberSuffix && (
<Field>
<FieldLabel htmlFor="cardNumberFull"></FieldLabel>
<Input
id="cardNumberFull"
value={cardNumberFull}
className="font-mono"
onChange={(event) => {
const value = event.target.value
const request = ++cardBrandRequest.current
setCardNumberFull(value)
if (value) setCardNumberSuffix("")
if (!value) return
void detectCardBrand(value).then((brand) => {
if (request === cardBrandRequest.current && brand)
setCardBrand(brand)
})
}}
placeholder="可留空"
disabled={isPending}
/>
</Field>
)}
{!cardNumberFull && (
<Field>
<FieldLabel htmlFor="cardNumberSuffix">
</FieldLabel>
<Input
id="cardNumberSuffix"
value={cardNumberSuffix}
className="font-mono"
onChange={(event) => {
const value = event.target.value
setCardNumberSuffix(value)
if (value) setCardNumberFull("")
}}
placeholder="例如:8888"
disabled={isPending}
/>
</Field>
)}
</div>
)}
{channelType === "E_WALLET" && (
<div className="grid gap-3 sm:grid-cols-2">
<Field>
<FieldLabel htmlFor="platform"></FieldLabel>
<Input
id="platform"
value={platform}
onChange={(event) => setPlatform(event.target.value)}
placeholder="例如:支付宝"
disabled={isPending}
/>
</Field>
<Field>
<FieldLabel htmlFor="platformAccountId">
</FieldLabel>
<Input
id="platformAccountId"
value={platformAccountId}
onChange={(event) =>
setPlatformAccountId(event.target.value)
}
placeholder="手机号或邮箱"
disabled={isPending}
/>
</Field>
<Field>
<FieldLabel htmlFor="subChannel"></FieldLabel>
<Input
id="subChannel"
value={subChannel}
onChange={(event) => setSubChannel(event.target.value)}
placeholder="例如:余额"
disabled={isPending}
/>
</Field>
<Field>
<FieldLabel htmlFor="subChannelType"></FieldLabel>
<Select
value={subChannelType}
onValueChange={(value) =>
value &&
setSubChannelType(value as typeof subChannelType)
}
>
<SelectTrigger id="subChannelType">
<SelectValue>
{subChannelType === "CREDIT"
? "信用消费"
: subChannelType === "DEBIT"
? "借记储值"
: "未指定"}
</SelectValue>
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem value="DEBIT"></SelectItem>
<SelectItem value="CREDIT"></SelectItem>
<SelectItem value="NONE"></SelectItem>
</SelectGroup>
</SelectContent>
</Select>
</Field>
<Field>
<FieldLabel htmlFor="walletRegion"></FieldLabel>
<Input
id="walletRegion"
value={region}
onChange={(event) => setRegion(event.target.value)}
placeholder="例如:HK"
disabled={isPending}
/>
</Field>
</div>
)}
{(channelType === "CASH" || channelType === "TRANSFER") && (
<p className="text-xs text-muted-foreground">
</p>
)}
</FieldGroup>
</FieldGroup>
</form>
<DialogFooter>
<Button
type="button"
variant="outline"
onClick={() => onOpenChange(false)}
disabled={isPending}
>
</Button>
<Button form="channel-form" type="submit" disabled={isPending}>
{isPending && (
<Loader2Icon data-icon="inline-start" className="animate-spin" />
)}
{editing ? "保存修改" : "创建渠道"}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)
}
+571
View File
@@ -0,0 +1,571 @@
"use client"
import * as React from "react"
import Image from "next/image"
import { useRouter } from "next/navigation"
import {
ArrowRightLeftIcon,
BanknoteIcon,
CreditCardIcon,
CopyIcon,
EyeIcon,
EyeOffIcon,
Loader2Icon,
MoreHorizontalIcon,
PencilIcon,
PowerIcon,
PowerOffIcon,
PlusIcon,
SearchIcon,
Trash2Icon,
WalletCardsIcon,
} from "lucide-react"
import {
type ChannelWithAccountNames,
deleteChannelAction,
toggleChannelActiveAction,
} from "@/lib/actions/channel"
import { type AccountWithChannels } from "@/lib/actions/account"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import { Input } from "@/components/ui/input"
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select"
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip"
import { ChannelDialog } from "./channel-dialog"
import {
CARD_BRAND_LABELS,
getCardBrandLogoUrl,
normalizeCardBrand,
} from "@/lib/payment/card-brand"
interface ChannelsViewProps {
initialChannels: ChannelWithAccountNames[]
initialAccounts: AccountWithChannels[]
}
const channelTypeLabels = {
PAYMENT_CARD: "支付卡",
E_WALLET: "电子钱包",
CASH: "现金",
TRANSFER: "转账",
} as const
const channelIcon = (type: ChannelWithAccountNames["channelType"]) =>
type === "PAYMENT_CARD" ? (
<CreditCardIcon className="size-4" />
) : type === "E_WALLET" ? (
<WalletCardsIcon className="size-4" />
) : type === "CASH" ? (
<BanknoteIcon className="size-4" />
) : (
<ArrowRightLeftIcon className="size-4" />
)
function paymentCardIcon(channel: ChannelWithAccountNames) {
const brand = normalizeCardBrand(channel.cardBrand)
const logoUrl = getCardBrandLogoUrl(channel.cardBrand)
return logoUrl && brand ? (
<Image
src={logoUrl}
alt={CARD_BRAND_LABELS[brand]}
width={24}
height={24}
className="size-6 object-contain"
/>
) : (
channelIcon(channel.channelType)
)
}
function channelName(channel: ChannelWithAccountNames) {
if (channel.channelType === "PAYMENT_CARD")
return channel.issuerName || "支付卡"
if (channel.channelType === "E_WALLET") return channel.platform || "电子钱包"
return channel.channelType === "CASH" ? "现金渠道" : "转账渠道"
}
function formatCardNumber(cardNumber: string) {
return cardNumber
.replace(/\D/g, "")
.replace(/(.{4})/g, "$1 ")
.trim()
}
export function ChannelsView({
initialChannels,
initialAccounts,
}: ChannelsViewProps) {
const router = useRouter()
const [channelsList, setChannelsList] = React.useState(initialChannels)
const [searchQuery, setSearchQuery] = React.useState("")
const [typeFilter, setTypeFilter] = React.useState("ALL")
const [dialogOpen, setDialogOpen] = React.useState(false)
const [editingChannel, setEditingChannel] =
React.useState<ChannelWithAccountNames | null>(null)
const [deletingChannel, setDeletingChannel] =
React.useState<ChannelWithAccountNames | null>(null)
const [isDeleting, setIsDeleting] = React.useState(false)
const [togglingId, setTogglingId] = React.useState<string | null>(null)
const [visibleCardIds, setVisibleCardIds] = React.useState<Set<string>>(
() => new Set()
)
const [copyStatus, setCopyStatus] = React.useState<{
channelId: string
message: string
} | null>(null)
const filteredChannels = React.useMemo(() => {
const query = searchQuery.trim().toLowerCase()
return channelsList.filter((item) => {
const searchable = [
channelName(item),
item.issuerName,
item.cardBrand,
item.cardNumberSuffix,
item.platform,
item.platformAccountId,
item.region,
item.subChannel,
item.desc,
...(item.linkedAccounts || []).map((account) => account.name),
]
.filter(Boolean)
.join(" ")
.toLowerCase()
return (
(!query || searchable.includes(query)) &&
(typeFilter === "ALL" || item.channelType === typeFilter)
)
})
}, [channelsList, searchQuery, typeFilter])
const openCreate = () => {
setEditingChannel(null)
setDialogOpen(true)
}
const confirmDelete = async () => {
if (!deletingChannel) return
setIsDeleting(true)
try {
const result = await deleteChannelAction(deletingChannel.id)
if (result.success) {
setChannelsList((items) =>
items.filter((item) => item.id !== deletingChannel.id)
)
setDeletingChannel(null)
router.refresh()
}
} finally {
setIsDeleting(false)
}
}
const toggleActive = async (
channel: ChannelWithAccountNames,
isActive: boolean
) => {
setTogglingId(channel.id)
setChannelsList((items) =>
items.map((item) =>
item.id === channel.id ? { ...item, isActive } : item
)
)
try {
const result = await toggleChannelActiveAction(channel.id, isActive)
if (!result.success)
setChannelsList((items) =>
items.map((item) =>
item.id === channel.id ? { ...item, isActive: !isActive } : item
)
)
else router.refresh()
} catch {
setChannelsList((items) =>
items.map((item) =>
item.id === channel.id ? { ...item, isActive: !isActive } : item
)
)
} finally {
setTogglingId(null)
}
}
const hasFilters = Boolean(searchQuery) || typeFilter !== "ALL"
const clearFilters = () => {
setSearchQuery("")
setTypeFilter("ALL")
}
const toggleCardVisibility = (channelId: string) => {
setVisibleCardIds((ids) => {
const nextIds = new Set(ids)
if (nextIds.has(channelId)) nextIds.delete(channelId)
else nextIds.add(channelId)
return nextIds
})
}
const copyCardNumber = async (channel: ChannelWithAccountNames) => {
if (!channel.cardNumberFull) return
try {
await navigator.clipboard.writeText(channel.cardNumberFull)
setCopyStatus({ channelId: channel.id, message: "已复制" })
} catch {
setCopyStatus({ channelId: channel.id, message: "复制失败" })
}
}
return (
<TooltipProvider>
<main className="flex min-w-0 flex-1 flex-col gap-4 p-4 md:p-6 lg:p-8">
<div className="flex flex-col gap-4 border-b border-border/70 pb-5 sm:flex-row sm:items-end sm:justify-between">
<div>
<h1 className="font-heading text-2xl font-semibold tracking-tight">
</h1>
<p className="mt-1 text-sm text-muted-foreground">
</p>
</div>
<Button onClick={openCreate} className="w-full sm:w-auto">
<PlusIcon data-icon="inline-start" />
</Button>
</div>
<div className="flex flex-col gap-2 md:flex-row md:items-center">
<div className="relative min-w-0 flex-1 md:max-w-md">
<SearchIcon className="pointer-events-none absolute top-1/2 left-2.5 size-4 -translate-y-1/2 text-muted-foreground" />
<Input
value={searchQuery}
onChange={(event) => setSearchQuery(event.target.value)}
placeholder="搜索渠道、平台、尾号或账户"
className="pl-8"
/>
</div>
<Select
value={typeFilter}
onValueChange={(value) => setTypeFilter(value ?? "ALL")}
>
<SelectTrigger className="w-full md:w-36">
<SelectValue>
{typeFilter === "ALL"
? "全部类型"
: channelTypeLabels[
typeFilter as keyof typeof channelTypeLabels
]}
</SelectValue>
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem value="ALL"></SelectItem>
<SelectItem value="PAYMENT_CARD"></SelectItem>
<SelectItem value="E_WALLET"></SelectItem>
<SelectItem value="CASH"></SelectItem>
<SelectItem value="TRANSFER"></SelectItem>
</SelectGroup>
</SelectContent>
</Select>
{hasFilters && (
<Button variant="ghost" size="sm" onClick={clearFilters}>
</Button>
)}
</div>
{filteredChannels.length > 0 ? (
<div className="overflow-hidden rounded-lg border border-border/70 bg-card">
<div className="hidden grid-cols-[minmax(220px,1.5fr)_minmax(180px,1fr)_110px_minmax(180px,1fr)_64px] gap-3 border-b bg-muted/30 px-4 py-2.5 text-[11px] font-medium text-muted-foreground md:grid">
<span></span>
<span></span>
<span></span>
<span></span>
<span />
</div>
{filteredChannels.map((channel) => {
const actionMenu = (
<DropdownMenu>
<Tooltip>
<TooltipTrigger
render={
<DropdownMenuTrigger
render={
<Button
variant="ghost"
size="icon-sm"
aria-label="打开渠道操作"
/>
}
/>
}
>
<MoreHorizontalIcon />
</TooltipTrigger>
<TooltipContent></TooltipContent>
</Tooltip>
<DropdownMenuContent align="end">
<DropdownMenuItem
disabled={togglingId === channel.id}
onClick={() => toggleActive(channel, !channel.isActive)}
>
{channel.isActive ? (
<PowerOffIcon data-icon="inline-start" />
) : (
<PowerIcon data-icon="inline-start" />
)}
{channel.isActive ? "停用渠道" : "启用渠道"}
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => {
setEditingChannel(channel)
setDialogOpen(true)
}}
>
<PencilIcon data-icon="inline-start" />
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
variant="destructive"
onClick={() => setDeletingChannel(channel)}
>
<Trash2Icon data-icon="inline-start" />
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)
return (
<div
key={channel.id}
className={`grid gap-3 border-b border-border/60 px-4 py-3 last:border-b-0 md:grid-cols-[minmax(220px,1.5fr)_minmax(180px,1fr)_110px_minmax(180px,1fr)_64px] md:items-center ${!channel.isActive ? "opacity-60" : ""}`}
>
<div className="flex min-w-0 items-center justify-between gap-3">
<div className="flex min-w-0 items-center gap-3">
<span className="flex size-8 shrink-0 items-center justify-center rounded-md bg-muted text-muted-foreground">
{channel.channelType === "PAYMENT_CARD"
? paymentCardIcon(channel)
: channelIcon(channel.channelType)}
</span>
<div className="min-w-0">
<div className="flex min-w-0 items-center gap-2">
<p className="truncate text-sm font-medium">
{channel.desc ||
channel.platformAccountId ||
channel.subChannel ||
""}
</p>
{!channel.isActive && (
<Badge variant="secondary"></Badge>
)}
</div>
<div className="flex min-w-0 items-center gap-2 text-xs">
<p className="min-w-0 truncate text-muted-foreground">
{channelName(channel)}
</p>
</div>
</div>
</div>
<div className="shrink-0 md:hidden">{actionMenu}</div>
</div>
<div className="flex min-w-0 items-center justify-between gap-2 text-xs md:block">
<span className="text-muted-foreground md:hidden">
</span>
<span className="min-w-0 truncate">
{channel.channelType === "PAYMENT_CARD" &&
(channel.cardNumberSuffix || channel.cardNumberFull) ? (
<span className="flex min-w-0 items-center gap-1.5 text-sm">
<span className="font-mono whitespace-nowrap text-muted-foreground">
{visibleCardIds.has(channel.id) &&
channel.cardNumberFull
? formatCardNumber(channel.cardNumberFull)
: `•••• ${
channel.cardNumberSuffix ||
formatCardNumber(
channel.cardNumberFull || ""
).slice(-4)
}`}
</span>
{channel.cardNumberFull && (
<>
<Tooltip>
<TooltipTrigger
render={
<Button
type="button"
variant="ghost"
size="icon-xs"
aria-label={
visibleCardIds.has(channel.id)
? "隐藏完整卡号"
: "显示完整卡号"
}
onClick={() =>
toggleCardVisibility(channel.id)
}
/>
}
>
{visibleCardIds.has(channel.id) ? (
<EyeOffIcon />
) : (
<EyeIcon />
)}
</TooltipTrigger>
<TooltipContent>
{visibleCardIds.has(channel.id)
? "隐藏完整卡号"
: "显示完整卡号"}
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger
render={
<Button
type="button"
variant="ghost"
size="icon-xs"
aria-label="复制卡号"
onClick={() => copyCardNumber(channel)}
/>
}
>
<CopyIcon />
</TooltipTrigger>
<TooltipContent></TooltipContent>
</Tooltip>
{copyStatus?.channelId === channel.id && (
<span className="text-[11px] whitespace-nowrap text-muted-foreground">
{copyStatus.message}
</span>
)}
</>
)}
</span>
) : channel.channelType === "E_WALLET" ? (
channel.platformAccountId || ""
) : (
""
)}
</span>
</div>
<div className="flex items-center justify-between text-xs md:block">
<span className="text-muted-foreground md:hidden"></span>
<span>{channelTypeLabels[channel.channelType]}</span>
</div>
<div className="flex min-w-0 items-center justify-between gap-2 text-xs md:block">
<span className="text-muted-foreground md:hidden">
</span>
<span className="truncate">
{channel.linkedAccounts?.length ? (
channel.linkedAccounts
.map((account) => account.name)
.join("、")
) : (
<span className="text-destructive"></span>
)}
</span>
</div>
<div className="hidden md:flex md:justify-end">
{actionMenu}
</div>
</div>
)
})}
</div>
) : (
<div className="flex min-h-56 flex-col items-center justify-center rounded-lg border border-dashed border-border px-6 py-10 text-center">
<CreditCardIcon className="size-6 text-muted-foreground" />
<h2 className="mt-3 text-sm font-semibold">
{hasFilters ? "没有符合条件的渠道" : "还没有支付渠道"}
</h2>
<p className="mt-1 text-xs text-muted-foreground">
{hasFilters
? "调整关键词或筛选条件后再试。"
: "添加渠道后即可在交易中选择对应的支付工具。"}
</p>
<div className="mt-4 flex gap-2">
{hasFilters && (
<Button variant="outline" size="sm" onClick={clearFilters}>
</Button>
)}
<Button size="sm" onClick={openCreate}>
<PlusIcon data-icon="inline-start" />
</Button>
</div>
</div>
)}
<ChannelDialog
key={`${dialogOpen}-${editingChannel?.id ?? "new"}`}
open={dialogOpen}
onOpenChange={setDialogOpen}
channel={editingChannel}
accounts={initialAccounts}
onSuccess={() => router.refresh()}
/>
<Dialog
open={Boolean(deletingChannel)}
onOpenChange={(open) => !open && setDeletingChannel(null)}
>
<DialogContent className="sm:max-w-sm">
<DialogHeader>
<DialogTitle></DialogTitle>
<DialogDescription>
{deletingChannel ? channelName(deletingChannel) : ""}
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button
variant="outline"
onClick={() => setDeletingChannel(null)}
disabled={isDeleting}
>
</Button>
<Button
variant="destructive"
onClick={confirmDelete}
disabled={isDeleting}
>
{isDeleting && (
<Loader2Icon
data-icon="inline-start"
className="animate-spin"
/>
)}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</main>
</TooltipProvider>
)
}
+52
View File
@@ -0,0 +1,52 @@
import { redirect } from "next/navigation"
import { auth } from "@/lib/auth"
import { getChannelsAction } from "@/lib/actions/channel"
import { getAccountsAction } from "@/lib/actions/account"
import { AppSidebar } from "@/components/app-sidebar"
import { AppHeader } from "@/components/app-header"
import { SidebarInset, SidebarProvider } from "@/components/ui/sidebar"
import { ChannelsView } from "./channels-view"
export default async function ChannelsPage() {
const session = await auth()
if (!session?.user) {
redirect("/login")
}
const user = session.user
const userName = user.name || "Fluxent 用户"
const userEmail = user.email || ""
const userAvatar = user.image || null
const [channelsRes, accountsRes] = await Promise.all([
getChannelsAction(),
getAccountsAction(),
])
const initialChannels =
channelsRes.success && channelsRes.data ? channelsRes.data : []
const initialAccounts =
accountsRes.success && accountsRes.data ? accountsRes.data : []
return (
<SidebarProvider>
<AppSidebar
user={{
name: userName,
email: userEmail,
avatar: userAvatar,
}}
/>
<SidebarInset>
<AppHeader title="支付渠道" />
<ChannelsView
key={JSON.stringify([initialChannels, initialAccounts])}
initialChannels={initialChannels}
initialAccounts={initialAccounts}
/>
</SidebarInset>
</SidebarProvider>
)
}
+7 -3
View File
@@ -1,3 +1,4 @@
@import url("https://fonts.googleapis.com/css2?family=Geist+Mono:wght@100..900&family=Inter:wght@100..900&display=swap");
@import "tailwindcss";
@import "tw-animate-css";
@import "shadcn/tailwind.css";
@@ -5,8 +6,9 @@
@custom-variant dark (&:is(.dark *));
@theme inline {
--font-heading: var(--font-sans);
--font-sans: var(--font-sans);
--font-heading: var(--font-app-sans);
--font-sans: var(--font-app-sans);
--font-mono: var(--font-app-mono);
--color-sidebar-ring: var(--sidebar-ring);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
@@ -48,6 +50,8 @@
}
:root {
--font-app-sans: "Inter", "Noto Sans SC", Arial, sans-serif;
--font-app-mono: "Geist Mono", "SFMono-Regular", Consolas, monospace;
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--card: oklch(1 0 0);
@@ -129,4 +133,4 @@
html {
@apply font-sans;
}
}
}
+5 -12
View File
@@ -1,15 +1,6 @@
import { Geist, Geist_Mono, Inter } from "next/font/google"
import "./globals.css"
import { ThemeProvider } from "@/components/theme-provider"
import { cn } from "@/lib/utils";
const inter = Inter({subsets:['latin'],variable:'--font-sans'})
const fontMono = Geist_Mono({
subsets: ["latin"],
variable: "--font-mono",
})
import { TooltipProvider } from "@/components/ui/tooltip"
export default function RootLayout({
children,
@@ -20,10 +11,12 @@ export default function RootLayout({
<html
lang="en"
suppressHydrationWarning
className={cn("antialiased", fontMono.variable, "font-sans", inter.variable)}
className="antialiased"
>
<body>
<ThemeProvider>{children}</ThemeProvider>
<ThemeProvider>
<TooltipProvider>{children}</TooltipProvider>
</ThemeProvider>
</body>
</html>
)
+396
View File
@@ -0,0 +1,396 @@
"use client";
import * as React from "react";
import Link from "next/link";
import { useRouter, useSearchParams } from "next/navigation";
import { signIn } from "next-auth/react";
import {
ArrowRightIcon,
CheckCircle2Icon,
CoinsIcon,
Globe2Icon,
KeyRoundIcon,
Loader2Icon,
LockIcon,
MailIcon,
PieChartIcon,
TrendingUpIcon,
WalletIcon,
} from "lucide-react";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Checkbox } from "@/components/ui/checkbox";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Separator } from "@/components/ui/separator";
import { FluxentLogo } from "@/components/fluxent-logo";
import { ThemeToggle } from "@/components/theme-toggle";
interface LoginFormProps {
oidcEnabled: boolean;
oidcName: string;
}
export function LoginForm({ oidcEnabled, oidcName }: LoginFormProps) {
const router = useRouter();
const searchParams = useSearchParams();
const callbackUrl = searchParams.get("callbackUrl") || "/";
const authError = searchParams.get("error");
const registered = searchParams.get("registered");
const [email, setEmail] = React.useState("");
const [password, setPassword] = React.useState("");
const [rememberMe, setRememberMe] = React.useState(false);
const [isPending, setIsPending] = React.useState(false);
const [isOidcPending, setIsOidcPending] = React.useState(false);
const [errorMessage, setErrorMessage] = React.useState<string | null>(() => {
if (authError === "CredentialsSignin") {
return "邮箱或密码错误,请核对后重试";
}
if (authError) {
return "认证过程中遇到问题,请重新登录";
}
return null;
});
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
setErrorMessage(null);
if (!email.trim()) {
setErrorMessage("请输入登录邮箱");
return;
}
if (!password) {
setErrorMessage("请输入登录密码");
return;
}
try {
setIsPending(true);
const res = await signIn("credentials", {
email: email.trim(),
password,
redirect: false,
callbackUrl,
});
if (res?.error) {
if (res.error === "CredentialsSignin" || res.code === "credentials") {
setErrorMessage("账号或密码不正确,请重新检查");
} else {
setErrorMessage("登录失败,请稍后重试");
}
return;
}
router.push(callbackUrl);
router.refresh();
} catch {
setErrorMessage("网络异常,无法连接到认证服务器");
} finally {
setIsPending(false);
}
};
const handleOidcLogin = async () => {
try {
setIsOidcPending(true);
await signIn("oidc", { callbackUrl });
} catch {
setIsOidcPending(false);
setErrorMessage("SSO 单点登录发起失败,请稍后重试");
}
};
return (
<div className="relative flex min-h-svh flex-col justify-between overflow-hidden bg-background">
{/* Decorative ambient background meshes */}
<div className="pointer-events-none absolute -top-40 -right-40 size-[32rem] rounded-full bg-primary/5 blur-3xl" />
<div className="pointer-events-none absolute top-1/2 -left-48 size-[34rem] rounded-full bg-primary/5 blur-3xl" />
<div className="pointer-events-none absolute inset-0 bg-[radial-gradient(ellipse_80%_80%_at_50%_-20%,rgba(120,119,198,0.08),rgba(255,255,255,0))]" />
{/* Top navigation / branding header */}
<header className="relative z-10 flex h-16 w-full items-center justify-between px-6 md:px-12">
<Link
href="/"
className="group flex items-center gap-2.5 outline-none focus-visible:ring-2 focus-visible:ring-ring"
>
<FluxentLogo size={32} />
<div className="flex flex-col">
<span className="font-heading text-base font-semibold tracking-tight">
Fluxent
</span>
<span className="text-[10px] tracking-wider text-muted-foreground uppercase">
Financial OS
</span>
</div>
</Link>
<div className="flex items-center gap-2">
<ThemeToggle />
</div>
</header>
{/* Main content grid */}
<main className="relative z-10 flex flex-1 items-center justify-center px-4 py-8 sm:px-6">
<div className="grid w-full max-w-4xl gap-8 lg:grid-cols-[1.1fr_1fr] lg:items-center">
{/* Left Hero feature showcase (visible on large screen) */}
<div className="hidden flex-col gap-8 pr-4 lg:flex">
<div className="flex flex-col gap-3">
<div className="inline-flex w-fit items-center gap-2 rounded-full border border-border/80 bg-muted/60 px-3 py-1 text-xs text-muted-foreground backdrop-blur-xs">
<span className="inline-block size-1.5 rounded-full bg-primary animate-pulse" />
</div>
<h1 className="font-heading text-3xl font-bold tracking-tight text-foreground sm:text-4xl">
<br />
</h1>
<p className="text-muted-foreground text-sm leading-relaxed">
Fluxent
</p>
</div>
{/* Visual value propositions */}
<div className="grid grid-cols-2 gap-3">
<div className="flex flex-col gap-1.5 rounded-xl border border-border/60 bg-card/50 p-3.5 backdrop-blur-xs transition-colors hover:border-border">
<div className="flex size-8 items-center justify-center rounded-lg bg-primary/10 text-primary">
<CoinsIcon className="size-4" />
</div>
<h3 className="text-xs font-semibold text-foreground">
</h3>
<p className="text-[11px] text-muted-foreground leading-normal">
</p>
</div>
<div className="flex flex-col gap-1.5 rounded-xl border border-border/60 bg-card/50 p-3.5 backdrop-blur-xs transition-colors hover:border-border">
<div className="flex size-8 items-center justify-center rounded-lg bg-primary/10 text-primary">
<WalletIcon className="size-4" />
</div>
<h3 className="text-xs font-semibold text-foreground">
</h3>
<p className="text-[11px] text-muted-foreground leading-normal">
</p>
</div>
<div className="flex flex-col gap-1.5 rounded-xl border border-border/60 bg-card/50 p-3.5 backdrop-blur-xs transition-colors hover:border-border">
<div className="flex size-8 items-center justify-center rounded-lg bg-primary/10 text-primary">
<TrendingUpIcon className="size-4" />
</div>
<h3 className="text-xs font-semibold text-foreground">
</h3>
<p className="text-[11px] text-muted-foreground leading-normal">
</p>
</div>
<div className="flex flex-col gap-1.5 rounded-xl border border-border/60 bg-card/50 p-3.5 backdrop-blur-xs transition-colors hover:border-border">
<div className="flex size-8 items-center justify-center rounded-lg bg-primary/10 text-primary">
<PieChartIcon className="size-4" />
</div>
<h3 className="text-xs font-semibold text-foreground">
</h3>
<p className="text-[11px] text-muted-foreground leading-normal">
</p>
</div>
</div>
</div>
{/* Right Login Card */}
<div className="w-full">
<Card className="border-border/80 shadow-lg shadow-black/5 dark:shadow-black/25">
<CardHeader className="gap-1.5 pb-4">
<div className="flex items-center justify-between">
<CardTitle className="text-xl font-bold tracking-tight">
Fluxent
</CardTitle>
<div className="flex size-7 items-center justify-center rounded-lg bg-primary/5 text-primary lg:hidden">
<FluxentLogo size={24} />
</div>
</div>
<CardDescription className="text-xs">
使
</CardDescription>
</CardHeader>
<CardContent className="flex flex-col gap-4">
{registered === "1" && (
<Alert className="border-border bg-muted/40">
<CheckCircle2Icon className="text-foreground" />
<AlertTitle></AlertTitle>
<AlertDescription className="text-xs">
Fluxent 使
</AlertDescription>
</Alert>
)}
{errorMessage && (
<Alert variant="destructive">
<AlertDescription className="text-xs">
{errorMessage}
</AlertDescription>
</Alert>
)}
{/* OIDC Single Sign On Button (if enabled) */}
{oidcEnabled && (
<div className="flex flex-col gap-3">
<Button
type="button"
variant="outline"
size="lg"
onClick={handleOidcLogin}
disabled={isOidcPending || isPending}
className="w-full justify-center text-xs font-medium"
>
{isOidcPending ? (
<Loader2Icon
data-icon="inline-start"
className="animate-spin"
/>
) : (
<Globe2Icon data-icon="inline-start" />
)}
{oidcName}
</Button>
<div className="relative flex items-center justify-center">
<Separator className="w-full" />
<span className="absolute bg-card px-2 text-[11px] uppercase tracking-wider text-muted-foreground">
使
</span>
</div>
</div>
)}
{/* Main Email & Password Form */}
<form onSubmit={handleSubmit} className="flex flex-col gap-3.5">
<div className="flex flex-col gap-1.5">
<Label htmlFor="email" className="text-xs font-medium">
</Label>
<div className="relative flex items-center">
<Input
id="email"
name="email"
type="email"
autoComplete="email"
placeholder="name@company.com"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
disabled={isPending}
className="pl-8"
/>
<MailIcon className="pointer-events-none absolute left-2.5 size-3.5 text-muted-foreground" />
</div>
</div>
<div className="flex flex-col gap-1.5">
<div className="flex items-center justify-between">
<Label htmlFor="password" className="text-xs font-medium">
</Label>
</div>
<div className="relative flex items-center">
<Input
id="password"
name="password"
type="password"
autoComplete="current-password"
placeholder="••••••••"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
disabled={isPending}
className="pl-8"
/>
<LockIcon className="pointer-events-none absolute left-2.5 size-3.5 text-muted-foreground" />
</div>
</div>
<div className="flex items-center justify-between pt-0.5">
<div className="flex items-center gap-2">
<Checkbox
id="rememberMe"
checked={rememberMe}
onCheckedChange={(checked) =>
setRememberMe(Boolean(checked))
}
/>
<label
htmlFor="rememberMe"
className="text-xs text-muted-foreground cursor-pointer select-none"
>
</label>
</div>
</div>
<Button
type="submit"
size="lg"
disabled={isPending}
className="mt-1 w-full justify-center text-xs font-medium"
>
{isPending ? (
<Loader2Icon
data-icon="inline-start"
className="animate-spin"
/>
) : (
<KeyRoundIcon data-icon="inline-start" />
)}
{isPending ? "正在登录..." : "登录进入系统"}
</Button>
</form>
</CardContent>
<CardFooter className="justify-center border-t border-border/50 py-3 text-xs text-muted-foreground">
<p>
Fluxent {" "}
<Link
href="/register"
className="font-medium text-foreground underline underline-offset-4 hover:text-primary"
>
<ArrowRightIcon
data-icon="inline-end"
className="inline size-3 ml-0.5"
/>
</Link>
</p>
</CardFooter>
</Card>
<p className="mt-4 text-center text-[11px] text-muted-foreground">
Fluxent
</p>
</div>
</div>
</main>
{/* Footer */}
<footer className="relative z-10 flex h-12 items-center justify-center border-t border-border/40 px-6 text-center text-[11px] text-muted-foreground">
<span>© {new Date().getFullYear()} Fluxent Financial. </span>
</footer>
</div>
);
}
+24
View File
@@ -0,0 +1,24 @@
import { Suspense } from "react";
import { LoginForm } from "./login-form";
export const metadata = {
title: "登录 - Fluxent",
description: "Fluxent 多币种资产与全场景记账系统",
};
export default function LoginPage() {
const oidcEnabled = process.env.AUTH_OIDC_ENABLED === "true";
const oidcName = process.env.AUTH_OIDC_NAME || "统一身份认证 (SSO)";
return (
<Suspense
fallback={
<div className="flex min-h-svh items-center justify-center bg-background">
<div className="size-6 animate-spin rounded-full border-2 border-primary border-t-transparent" />
</div>
}
>
<LoginForm oidcEnabled={oidcEnabled} oidcName={oidcName} />
</Suspense>
);
}
+209 -15
View File
@@ -1,19 +1,213 @@
import { Button } from "@/components/ui/button"
import { redirect } from "next/navigation";
import Link from "next/link";
import {
ArrowRightIcon,
CreditCardIcon,
DollarSignIcon,
LayersIcon,
PlusIcon,
SlidersHorizontalIcon,
TrendingUpIcon,
WalletIcon,
} from "lucide-react";
import { auth } from "@/lib/auth";
import { AppSidebar } from "@/components/app-sidebar";
import { AppHeader } from "@/components/app-header";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import {
SidebarInset,
SidebarProvider,
} from "@/components/ui/sidebar";
export default async function HomePage() {
const session = await auth();
// 若用户未登录,由于 proxy 会拦截并重定向,作为服务端页面增加双重保障
if (!session?.user) {
redirect("/login");
}
const user = session.user;
const userName = user.name || "Fluxent 用户";
const userEmail = user.email || "";
const userAvatar = user.image || null;
export default function Page() {
return (
<div className="flex min-h-svh p-6">
<div className="flex max-w-md min-w-0 flex-col gap-4 text-sm leading-loose">
<div>
<h1 className="font-medium">Project ready!</h1>
<p>You may now add components and start building.</p>
<p>We&apos;ve already added the button component for you.</p>
<Button className="mt-2">Button</Button>
<SidebarProvider>
<AppSidebar
user={{
name: userName,
email: userEmail,
avatar: userAvatar,
}}
/>
<SidebarInset>
<AppHeader title="控制台" />
{/* Inset Main Dashboard Content */}
<div className="flex flex-1 flex-col gap-6 p-4 md:p-6 lg:p-8">
{/* Welcome greeting banner */}
<div className="relative overflow-hidden rounded-2xl border border-border/80 bg-gradient-to-br from-card via-card to-muted/40 p-6 md:p-8">
<div className="relative z-10 flex flex-col gap-3 md:max-w-2xl">
<h1 className="font-heading text-2xl font-bold tracking-tight text-foreground md:text-3xl">
{userName}
</h1>
<p className="text-sm text-muted-foreground leading-relaxed">
Fluxent
</p>
<div className="mt-2 flex flex-wrap items-center gap-3">
<Button size="sm" className="text-xs">
<PlusIcon data-icon="inline-start" />
</Button>
<Button
variant="outline"
size="sm"
className="text-xs"
render={<Link href="/accounts" />}
nativeButton={false}
>
<LayersIcon data-icon="inline-start" />
</Button>
</div>
</div>
{/* Subtle background decoration */}
<div className="pointer-events-none absolute -right-12 -bottom-12 size-64 rounded-full bg-primary/5 blur-2xl" />
</div>
{/* Quick Metrics & Highlights Cards (3 columns on desktop) */}
<div className="grid gap-4 md:grid-cols-3">
<Card className="border-border/70 bg-card/60 backdrop-blur-xs">
<CardHeader className="flex flex-row items-center justify-between pb-2">
<CardTitle className="text-xs font-medium text-muted-foreground">
(CNY)
</CardTitle>
<DollarSignIcon className="size-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold tracking-tight">¥ 0.00</div>
<p className="mt-1 text-[11px] text-muted-foreground">
</p>
</CardContent>
</Card>
<Card className="border-border/70 bg-card/60 backdrop-blur-xs">
<CardHeader className="flex flex-row items-center justify-between pb-2">
<CardTitle className="text-xs font-medium text-muted-foreground">
</CardTitle>
<WalletIcon className="size-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold tracking-tight">0</div>
<p className="mt-1 text-[11px] text-muted-foreground">
</p>
</CardContent>
</Card>
<Card className="border-border/70 bg-card/60 backdrop-blur-xs">
<CardHeader className="flex flex-row items-center justify-between pb-2">
<CardTitle className="text-xs font-medium text-muted-foreground">
</CardTitle>
<TrendingUpIcon className="size-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold tracking-tight">0</div>
<p className="mt-1 text-[11px] text-muted-foreground">
</p>
</CardContent>
</Card>
</div>
{/* Next Steps / Feature Guide (3 columns) */}
<div className="flex flex-col gap-4">
<h2 className="font-heading text-base font-semibold tracking-tight text-foreground">
</h2>
<div className="grid gap-4 md:grid-cols-3">
<Link
href="/accounts"
className="group flex flex-col justify-between rounded-xl border border-border/80 bg-card p-5 transition-all hover:border-foreground/30 hover:shadow-sm"
>
<div className="flex flex-col gap-2">
<div className="flex size-9 items-center justify-center rounded-lg bg-primary/10 text-primary">
<CreditCardIcon className="size-4.5" />
</div>
<h3 className="font-heading text-sm font-semibold text-foreground">
1.
</h3>
<p className="text-xs text-muted-foreground leading-relaxed">
</p>
</div>
<div className="mt-4 flex items-center gap-1 text-xs font-medium text-primary">
<span></span>
<ArrowRightIcon
data-icon="inline-end"
className="size-3.5 transition-transform group-hover:translate-x-0.5"
/>
</div>
</Link>
<div className="group flex flex-col justify-between rounded-xl border border-border/80 bg-card p-5 transition-all hover:border-foreground/30 hover:shadow-sm">
<div className="flex flex-col gap-2">
<div className="flex size-9 items-center justify-center rounded-lg bg-primary/10 text-primary">
<LayersIcon className="size-4.5" />
</div>
<h3 className="font-heading text-sm font-semibold text-foreground">
2.
</h3>
<p className="text-xs text-muted-foreground leading-relaxed">
</p>
</div>
<div className="mt-4 flex items-center gap-1 text-xs font-medium text-primary">
<span></span>
<ArrowRightIcon
data-icon="inline-end"
className="size-3.5 transition-transform group-hover:translate-x-0.5"
/>
</div>
</div>
<div className="group flex flex-col justify-between rounded-xl border border-border/80 bg-card p-5 transition-all hover:border-foreground/30 hover:shadow-sm">
<div className="flex flex-col gap-2">
<div className="flex size-9 items-center justify-center rounded-lg bg-primary/10 text-primary">
<SlidersHorizontalIcon className="size-4.5" />
</div>
<h3 className="font-heading text-sm font-semibold text-foreground">
3.
</h3>
<p className="text-xs text-muted-foreground leading-relaxed">
</p>
</div>
<div className="mt-4 flex items-center gap-1 text-xs font-medium text-primary">
<span></span>
<ArrowRightIcon
data-icon="inline-end"
className="size-3.5 transition-transform group-hover:translate-x-0.5"
/>
</div>
</div>
</div>
</div>
</div>
<div className="font-mono text-xs text-muted-foreground">
(Press <kbd>d</kbd> to toggle dark mode)
</div>
</div>
</div>
)
</SidebarInset>
</SidebarProvider>
);
}
+21
View File
@@ -0,0 +1,21 @@
import { Suspense } from "react";
import { RegisterForm } from "./register-form";
export const metadata = {
title: "注册新账号 - Fluxent",
description: "创建您的 Fluxent 多币种资产与记账账户",
};
export default function RegisterPage() {
return (
<Suspense
fallback={
<div className="flex min-h-svh items-center justify-center bg-background">
<div className="size-6 animate-spin rounded-full border-2 border-primary border-t-transparent" />
</div>
}
>
<RegisterForm />
</Suspense>
);
}
+319
View File
@@ -0,0 +1,319 @@
"use client";
import * as React from "react";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { useActionState } from "react";
import {
ArrowLeftIcon,
CheckCircle2Icon,
KeyRoundIcon,
Loader2Icon,
LockIcon,
MailIcon,
SparklesIcon,
UserIcon,
} from "lucide-react";
import { registerAction, type RegisterState } from "@/lib/actions/auth";
import { Alert, AlertDescription } from "@/components/ui/alert";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { FluxentLogo } from "@/components/fluxent-logo";
import { ThemeToggle } from "@/components/theme-toggle";
const initialState: RegisterState = {};
export function RegisterForm() {
const router = useRouter();
const [state, formAction, isPending] = useActionState(
registerAction,
initialState
);
const [password, setPassword] = React.useState("");
const [confirmPassword, setConfirmPassword] = React.useState("");
// 当注册成功时,优雅倒计时或自动跳转
React.useEffect(() => {
if (state?.success) {
const timer = setTimeout(() => {
router.push("/login?registered=1");
}, 1500);
return () => clearTimeout(timer);
}
}, [state?.success, router]);
// 密码复杂度提示计算
const hasMinLen = password.length >= 8;
const passwordsMatch = Boolean(
password && confirmPassword && password === confirmPassword
);
return (
<div className="relative flex min-h-svh flex-col justify-between overflow-hidden bg-background">
{/* Decorative ambient gradients */}
<div className="pointer-events-none absolute -top-40 -left-40 size-[32rem] rounded-full bg-primary/5 blur-3xl" />
<div className="pointer-events-none absolute -bottom-40 -right-40 size-[34rem] rounded-full bg-primary/5 blur-3xl" />
<div className="pointer-events-none absolute inset-0 bg-[radial-gradient(ellipse_80%_80%_at_50%_-20%,rgba(120,119,198,0.08),rgba(255,255,255,0))]" />
{/* Header */}
<header className="relative z-10 flex h-16 w-full items-center justify-between px-6 md:px-12">
<Link
href="/"
className="group flex items-center gap-2.5 outline-none focus-visible:ring-2 focus-visible:ring-ring"
>
<FluxentLogo size={32} />
<div className="flex flex-col">
<span className="font-heading text-base font-semibold tracking-tight">
Fluxent
</span>
<span className="text-[10px] tracking-wider text-muted-foreground uppercase">
Financial OS
</span>
</div>
</Link>
<div className="flex items-center gap-2">
<ThemeToggle />
</div>
</header>
{/* Main Card */}
<main className="relative z-10 flex flex-1 items-center justify-center px-4 py-8 sm:px-6">
<div className="w-full max-w-md">
<Card className="border-border/80 shadow-lg shadow-black/5 dark:shadow-black/25">
<CardHeader className="gap-1.5 pb-4">
<div className="flex items-center justify-between">
<CardTitle className="text-xl font-bold tracking-tight">
Fluxent
</CardTitle>
<div className="flex size-7 items-center justify-center rounded-lg bg-primary/10 text-primary">
<SparklesIcon className="size-4" />
</div>
</div>
<CardDescription className="text-xs">
</CardDescription>
</CardHeader>
<CardContent className="flex flex-col gap-4">
{/* 注册成功反馈 */}
{state?.success ? (
<div className="flex flex-col items-center justify-center gap-3 py-6 text-center">
<div className="flex size-12 items-center justify-center rounded-full bg-primary/10 text-primary">
<CheckCircle2Icon className="size-6" />
</div>
<div className="flex flex-col gap-1">
<h3 className="text-base font-semibold text-foreground">
</h3>
<p className="text-xs text-muted-foreground">
...
</p>
</div>
<Button
variant="outline"
size="sm"
className="mt-2 text-xs"
onClick={() => router.push("/login?registered=1")}
>
</Button>
</div>
) : (
<>
{/* 全局错误提示 */}
{state?.error && (
<Alert variant="destructive">
<AlertDescription className="text-xs">
{state.error}
</AlertDescription>
</Alert>
)}
<form action={formAction} className="flex flex-col gap-3.5">
{/* 姓名 / 昵称 */}
<div className="flex flex-col gap-1.5">
<Label htmlFor="name" className="text-xs font-medium">
/
</Label>
<div className="relative flex items-center">
<Input
id="name"
name="name"
type="text"
autoComplete="name"
placeholder="例如:Alex Chen"
required
disabled={isPending}
className="pl-8"
/>
<UserIcon className="pointer-events-none absolute left-2.5 size-3.5 text-muted-foreground" />
</div>
{state?.fieldErrors?.name && (
<p className="text-[11px] text-destructive">
{state.fieldErrors.name[0]}
</p>
)}
</div>
{/* 邮箱 */}
<div className="flex flex-col gap-1.5">
<Label htmlFor="email" className="text-xs font-medium">
</Label>
<div className="relative flex items-center">
<Input
id="email"
name="email"
type="email"
autoComplete="email"
placeholder="alex@example.com"
required
disabled={isPending}
className="pl-8"
/>
<MailIcon className="pointer-events-none absolute left-2.5 size-3.5 text-muted-foreground" />
</div>
{state?.fieldErrors?.email && (
<p className="text-[11px] text-destructive">
{state.fieldErrors.email[0]}
</p>
)}
</div>
{/* 密码 */}
<div className="flex flex-col gap-1.5">
<Label htmlFor="password" className="text-xs font-medium">
</Label>
<div className="relative flex items-center">
<Input
id="password"
name="password"
type="password"
autoComplete="new-password"
placeholder="至少 8 个字符"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
disabled={isPending}
className="pl-8"
/>
<LockIcon className="pointer-events-none absolute left-2.5 size-3.5 text-muted-foreground" />
</div>
{state?.fieldErrors?.password && (
<p className="text-[11px] text-destructive">
{state.fieldErrors.password[0]}
</p>
)}
</div>
{/* 确认密码 */}
<div className="flex flex-col gap-1.5">
<Label
htmlFor="confirmPassword"
className="text-xs font-medium"
>
</Label>
<div className="relative flex items-center">
<Input
id="confirmPassword"
name="confirmPassword"
type="password"
autoComplete="new-password"
placeholder="重复输入上方设置的密码"
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
required
disabled={isPending}
className="pl-8"
/>
<LockIcon className="pointer-events-none absolute left-2.5 size-3.5 text-muted-foreground" />
</div>
{state?.fieldErrors?.confirmPassword && (
<p className="text-[11px] text-destructive">
{state.fieldErrors.confirmPassword[0]}
</p>
)}
</div>
{/* 密码强度/匹配指引 */}
<div className="flex flex-col gap-1 rounded-lg border border-border/40 bg-muted/30 p-2.5 text-[11px] text-muted-foreground">
<div className="flex items-center gap-1.5">
<span
className={`size-1.5 rounded-full ${
hasMinLen ? "bg-primary" : "bg-muted-foreground/40"
}`}
/>
<span> 8 </span>
</div>
<div className="flex items-center gap-1.5">
<span
className={`size-1.5 rounded-full ${
passwordsMatch
? "bg-primary"
: "bg-muted-foreground/40"
}`}
/>
<span></span>
</div>
</div>
<Button
type="submit"
size="lg"
disabled={isPending}
className="mt-1 w-full justify-center text-xs font-medium"
>
{isPending ? (
<Loader2Icon
data-icon="inline-start"
className="animate-spin"
/>
) : (
<KeyRoundIcon data-icon="inline-start" />
)}
{isPending ? "正在建立账户..." : "注册并立即开始"}
</Button>
</form>
</>
)}
</CardContent>
<CardFooter className="justify-center border-t border-border/50 py-3 text-xs text-muted-foreground">
<p>
Fluxent {" "}
<Link
href="/login"
className="font-medium text-foreground underline underline-offset-4 hover:text-primary"
>
<ArrowLeftIcon
data-icon="inline-start"
className="inline size-3 mr-0.5"
/>
</Link>
</p>
</CardFooter>
</Card>
</div>
</main>
{/* Footer */}
<footer className="relative z-10 flex h-12 items-center justify-center border-t border-border/40 px-6 text-center text-[11px] text-muted-foreground">
<span>© {new Date().getFullYear()} Fluxent Financial. </span>
</footer>
</div>
);
}
+42
View File
@@ -0,0 +1,42 @@
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator,
} from "@/components/ui/breadcrumb"
import { Separator } from "@/components/ui/separator"
import {
SidebarTrigger,
} from "@/components/ui/sidebar"
import { ThemeToggle } from "@/components/theme-toggle"
export function AppHeader({ title }: { title: string }) {
const isHome = title === "控制台"
return (
<header className="flex h-16 shrink-0 items-center justify-between gap-2 border-b px-4 transition-[width,height] ease-linear group-has-data-[collapsible=icon]/sidebar-wrapper:h-12">
<div className="flex min-w-0 h-5 items-center gap-2">
<SidebarTrigger className="-ml-1" />
<Separator orientation="vertical" className="mr-2" />
<Breadcrumb>
<BreadcrumbList>
{!isHome && (
<>
<BreadcrumbItem className="hidden md:block">
<BreadcrumbLink href="/"></BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator className="hidden md:block" />
</>
)}
<BreadcrumbItem>
<BreadcrumbPage>{title}</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
</div>
<ThemeToggle />
</header>
)
}
+93
View File
@@ -0,0 +1,93 @@
"use client";
import * as React from "react";
import Link from "next/link";
import {
CreditCardIcon,
LayoutDashboardIcon,
WalletCardsIcon,
} from "lucide-react";
import { FluxentLogo } from "@/components/fluxent-logo";
import { NavMain, type NavMainItem } from "@/components/nav-main";
import { NavUser, type NavUserData } from "@/components/nav-user";
import {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarHeader,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
useSidebar,
} from "@/components/ui/sidebar";
const navMainItems: NavMainItem[] = [
{
title: "控制台",
url: "/",
icon: LayoutDashboardIcon,
},
{
title: "资金账户",
url: "/accounts",
icon: WalletCardsIcon,
},
{
title: "支付渠道",
url: "/channels",
icon: CreditCardIcon,
},
];
export function AppSidebar({
user,
...props
}: {
user: NavUserData;
} & React.ComponentProps<typeof Sidebar>) {
const { isMobile, setOpenMobile } = useSidebar();
const handleLogoNavigation = () => {
if (isMobile) {
setOpenMobile(false);
}
};
return (
<Sidebar variant="inset" {...props}>
{/* Sidebar Header: Fluxent Logo & Brand */}
<SidebarHeader className="p-2.5">
<SidebarMenu>
<SidebarMenuItem>
<SidebarMenuButton
size="lg"
render={<Link href="/" onClick={handleLogoNavigation} />}
className="gap-2.5 hover:bg-sidebar-accent"
>
<FluxentLogo size={32} />
<div className="grid flex-1 text-left leading-tight">
<span className="truncate font-heading text-sm font-bold tracking-tight text-sidebar-foreground">
Fluxent
</span>
<span className="truncate text-[10px] tracking-wider text-muted-foreground uppercase">
</span>
</div>
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
</SidebarHeader>
{/* Sidebar Content: Main navigation & secondary links */}
<SidebarContent>
<NavMain items={navMainItems} />
</SidebarContent>
{/* Sidebar Footer: User profile card with dropdown */}
<SidebarFooter>
<NavUser user={user} />
</SidebarFooter>
</Sidebar>
);
}
+48
View File
@@ -0,0 +1,48 @@
import React from "react";
import { cn } from "@/lib/utils";
export function FluxentLogo({
className,
size = 36,
}: {
className?: string;
size?: number;
}) {
return (
<div
className={cn(
"relative flex items-center justify-center rounded-xl bg-primary text-primary-foreground shadow-sm transition-transform duration-300 group-hover:scale-105",
className
)}
style={{ width: size, height: size }}
>
<svg
width={Math.round(size * 0.64)}
height={Math.round(size * 0.64)}
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className="text-primary-foreground"
>
{/* Modern streamlined geometric flow icon for Fluxent multi-currency assets */}
<path
d="M4 6.5C4 5.11929 5.11929 4 6.5 4H14.5C15.8807 4 17 5.11929 17 6.5C17 7.88071 15.8807 9 14.5 9H9.5C8.11929 9 7 10.1193 7 11.5V11.5C7 12.8807 8.11929 14 9.5 14H19.5"
stroke="currentColor"
strokeWidth="2.2"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M7 11.5H16.5C17.8807 11.5 19 12.6193 19 14V14C19 15.3807 17.8807 16.5 16.5 16.5H6.5C5.11929 16.5 4 17.6193 4 19V19C4 19.5523 4.44772 20 5 20H15"
stroke="currentColor"
strokeWidth="2.2"
strokeLinecap="round"
strokeLinejoin="round"
strokeOpacity="0.75"
/>
<circle cx="19.5" cy="14" r="1.5" fill="currentColor" />
<circle cx="15" cy="20" r="1.5" fill="currentColor" fillOpacity="0.8" />
</svg>
</div>
);
}
+66
View File
@@ -0,0 +1,66 @@
"use client";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { type LucideIcon } from "lucide-react";
import {
SidebarGroup,
SidebarGroupLabel,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
useSidebar,
} from "@/components/ui/sidebar";
export interface NavMainItem {
title: string;
url: string;
icon: LucideIcon;
badge?: string;
}
export function NavMain({ items }: { items: NavMainItem[] }) {
const pathname = usePathname();
const { isMobile, setOpenMobile } = useSidebar();
const handleNavigation = () => {
if (isMobile) {
setOpenMobile(false);
}
};
return (
<SidebarGroup>
<SidebarGroupLabel className="text-[11px] font-medium tracking-wider text-sidebar-foreground/60 uppercase">
</SidebarGroupLabel>
<SidebarMenu>
{items.map((item) => {
const Icon = item.icon;
return (
<SidebarMenuItem key={item.title}>
<SidebarMenuButton
isActive={
item.url === "/"
? pathname === "/"
: pathname === item.url || pathname.startsWith(`${item.url}/`)
}
tooltip={item.title}
render={<Link href={item.url} onClick={handleNavigation} />}
>
<Icon data-icon="inline-start" />
<span className="font-medium">{item.title}</span>
{item.badge && (
<span className="ml-auto rounded-full bg-primary/10 px-1.5 py-0.5 text-[10px] font-medium text-primary">
{item.badge}
</span>
)}
</SidebarMenuButton>
</SidebarMenuItem>
);
})}
</SidebarMenu>
</SidebarGroup>
);
}
+65
View File
@@ -0,0 +1,65 @@
"use client";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { type LucideIcon } from "lucide-react";
import {
SidebarGroup,
SidebarGroupContent,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
useSidebar,
} from "@/components/ui/sidebar";
export interface NavSecondaryItem {
title: string;
url: string;
icon: LucideIcon;
badge?: React.ReactNode;
}
export function NavSecondary({
items,
className,
...props
}: {
items: NavSecondaryItem[];
} & React.ComponentProps<typeof SidebarGroup>) {
const pathname = usePathname();
const { isMobile, setOpenMobile } = useSidebar();
const handleNavigation = () => {
if (isMobile) {
setOpenMobile(false);
}
};
return (
<SidebarGroup className={className} {...props}>
<SidebarGroupContent>
<SidebarMenu>
{items.map((item) => {
const Icon = item.icon;
return (
<SidebarMenuItem key={item.title}>
<SidebarMenuButton
size="sm"
isActive={
pathname === item.url || pathname.startsWith(`${item.url}/`)
}
tooltip={item.title}
render={<Link href={item.url} onClick={handleNavigation} />}
>
<Icon data-icon="inline-start" />
<span>{item.title}</span>
</SidebarMenuButton>
</SidebarMenuItem>
);
})}
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
);
}
+144
View File
@@ -0,0 +1,144 @@
"use client";
import {
ChevronsUpDownIcon,
LogOutIcon,
Settings2Icon,
SparklesIcon,
UserCogIcon,
} from "lucide-react";
import { signOut } from "next-auth/react";
import {
Avatar,
AvatarFallback,
AvatarImage,
} from "@/components/ui/avatar";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import {
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
useSidebar,
} from "@/components/ui/sidebar";
export interface NavUserData {
name: string;
email: string;
avatar?: string | null;
}
export function NavUser({ user }: { user: NavUserData }) {
const { isMobile } = useSidebar();
const initials = (user.name || user.email || "U")
.slice(0, 2)
.toUpperCase();
const handleSignOut = () => {
signOut({ callbackUrl: "/login" });
};
return (
<SidebarMenu>
<SidebarMenuItem>
<DropdownMenu>
<DropdownMenuTrigger
render={
<SidebarMenuButton
size="lg"
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
/>
}
>
<Avatar size="sm" className="rounded-lg">
{user.avatar ? (
<AvatarImage src={user.avatar} alt={user.name} />
) : null}
<AvatarFallback className="rounded-lg font-medium text-xs">
{initials}
</AvatarFallback>
</Avatar>
<div className="grid flex-1 text-left text-xs leading-tight">
<span className="truncate font-semibold text-sidebar-foreground">
{user.name}
</span>
<span className="truncate text-[11px] text-muted-foreground">
{user.email}
</span>
</div>
<ChevronsUpDownIcon className="ml-auto size-4 text-muted-foreground" />
</DropdownMenuTrigger>
<DropdownMenuContent
className="w-(--anchor-width) min-w-56 rounded-lg"
side={isMobile ? "bottom" : "right"}
align="end"
sideOffset={4}
>
<DropdownMenuLabel className="p-0 font-normal">
<div className="flex items-center gap-2.5 px-1 py-1.5 text-left text-xs">
<Avatar size="sm" className="rounded-lg">
{user.avatar ? (
<AvatarImage src={user.avatar} alt={user.name} />
) : null}
<AvatarFallback className="rounded-lg font-medium text-xs">
{initials}
</AvatarFallback>
</Avatar>
<div className="grid flex-1 text-left text-xs leading-tight">
<span className="truncate font-semibold text-foreground">
{user.name}
</span>
<span className="truncate text-[11px] text-muted-foreground">
{user.email}
</span>
</div>
</div>
</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem>
<SparklesIcon data-icon="inline-start" />
<span></span>
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem>
<UserCogIcon data-icon="inline-start" />
<span></span>
</DropdownMenuItem>
<DropdownMenuItem>
<Settings2Icon data-icon="inline-start" />
<span></span>
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuItem
variant="destructive"
onClick={handleSignOut}
className="cursor-pointer"
>
<LogOutIcon data-icon="inline-start" />
<span>退</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</SidebarMenuItem>
</SidebarMenu>
);
}
+48
View File
@@ -0,0 +1,48 @@
"use client";
import * as React from "react";
import { useTheme } from "next-themes";
import { MoonIcon, SunIcon } from "lucide-react";
import { Button } from "@/components/ui/button";
export function ThemeToggle() {
const { resolvedTheme, setTheme } = useTheme();
const mounted = React.useSyncExternalStore(
() => () => {},
() => true,
() => false
);
if (!mounted) {
return (
<Button
variant="ghost"
size="icon-sm"
aria-label="Toggle theme"
className="text-muted-foreground"
>
<span className="size-4" />
</Button>
);
}
const isDark = resolvedTheme === "dark";
return (
<Button
variant="ghost"
size="icon-sm"
onClick={() => setTheme(isDark ? "light" : "dark")}
aria-label="Toggle color theme"
title={isDark ? "切换为浅色模式" : "切换为深色模式"}
className="text-muted-foreground hover:text-foreground transition-colors"
>
{isDark ? (
<SunIcon data-icon="inline-start" />
) : (
<MoonIcon data-icon="inline-start" />
)}
</Button>
);
}
+75
View File
@@ -0,0 +1,75 @@
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "cn"
const alertVariants = cva(
"group/alert relative grid w-full gap-0.5 rounded-lg border px-2.5 py-2 text-left text-sm has-data-[slot=alert-action]:relative has-data-[slot=alert-action]:pr-18 has-[>svg]:grid-cols-[auto_1fr] has-[>svg]:gap-x-2 *:[svg]:row-span-2 *:[svg]:translate-y-0.5 *:[svg]:text-current *:[svg:not([class*='size-'])]:size-4",
{
variants: {
variant: {
default: "bg-card text-card-foreground",
destructive:
"bg-card text-destructive *:data-[slot=alert-description]:text-destructive/90 *:[svg]:text-current",
},
},
defaultVariants: {
variant: "default",
},
}
)
function Alert({
className,
variant,
...props
}: React.ComponentProps<"div"> & VariantProps<typeof alertVariants>) {
return (
<div
data-slot="alert"
role="alert"
className={cn(alertVariants({ variant }), className)}
{...props}
/>
)
}
function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="alert-title"
className={cn(
"font-medium group-has-[>svg]/alert:col-start-2 [&_a]:underline [&_a]:underline-offset-3 [&_a]:hover:text-foreground",
className
)}
{...props}
/>
)
}
function AlertDescription({
className,
...props
}: React.ComponentProps<"div">) {
return (
<div
data-slot="alert-description"
className={cn(
"text-sm text-balance text-muted-foreground md:text-pretty [&_a]:underline [&_a]:underline-offset-3 [&_a]:hover:text-foreground [&_p:not(:last-child)]:mb-4",
className
)}
{...props}
/>
)
}
function AlertAction({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="alert-action"
className={cn("absolute top-2 right-2", className)}
{...props}
/>
)
}
export { Alert, AlertTitle, AlertDescription, AlertAction }
+108
View File
@@ -0,0 +1,108 @@
"use client"
import * as React from "react"
import { Avatar as AvatarPrimitive } from "@base-ui/react/avatar"
import { cn } from "cn"
function Avatar({
className,
size = "default",
...props
}: AvatarPrimitive.Root.Props & {
size?: "default" | "sm" | "lg"
}) {
return (
<AvatarPrimitive.Root
data-slot="avatar"
data-size={size}
className={cn(
"group/avatar relative flex size-8 shrink-0 rounded-full select-none after:absolute after:inset-0 after:rounded-full after:border after:border-border after:mix-blend-darken data-[size=lg]:size-10 data-[size=sm]:size-6 dark:after:mix-blend-lighten",
className
)}
{...props}
/>
)
}
function AvatarImage({ className, ...props }: AvatarPrimitive.Image.Props) {
return (
<AvatarPrimitive.Image
data-slot="avatar-image"
className={cn(
"aspect-square size-full rounded-full object-cover",
className
)}
{...props}
/>
)
}
function AvatarFallback({
className,
...props
}: AvatarPrimitive.Fallback.Props) {
return (
<AvatarPrimitive.Fallback
data-slot="avatar-fallback"
className={cn(
"flex size-full items-center justify-center rounded-full bg-muted text-sm text-muted-foreground group-data-[size=sm]/avatar:text-xs",
className
)}
{...props}
/>
)
}
function AvatarBadge({ className, ...props }: React.ComponentProps<"span">) {
return (
<span
data-slot="avatar-badge"
className={cn(
"absolute right-0 bottom-0 z-10 inline-flex items-center justify-center rounded-full bg-primary text-primary-foreground bg-blend-color ring-2 ring-background select-none",
"group-data-[size=sm]/avatar:size-2 group-data-[size=sm]/avatar:[&>svg]:hidden",
"group-data-[size=default]/avatar:size-2.5 group-data-[size=default]/avatar:[&>svg]:size-2",
"group-data-[size=lg]/avatar:size-3 group-data-[size=lg]/avatar:[&>svg]:size-2",
className
)}
{...props}
/>
)
}
function AvatarGroup({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="avatar-group"
className={cn(
"group/avatar-group flex -space-x-2 *:data-[slot=avatar]:ring-2 *:data-[slot=avatar]:ring-background",
className
)}
{...props}
/>
)
}
function AvatarGroupCount({
className,
...props
}: React.ComponentProps<"div">) {
return (
<div
data-slot="avatar-group-count"
className={cn(
"relative flex size-8 shrink-0 items-center justify-center rounded-full bg-muted text-sm text-muted-foreground ring-2 ring-background group-has-data-[size=lg]/avatar-group:size-10 group-has-data-[size=sm]/avatar-group:size-6 [&>svg]:size-4 group-has-data-[size=lg]/avatar-group:[&>svg]:size-5 group-has-data-[size=sm]/avatar-group:[&>svg]:size-3",
className
)}
{...props}
/>
)
}
export {
Avatar,
AvatarImage,
AvatarFallback,
AvatarGroup,
AvatarGroupCount,
AvatarBadge,
}
+51
View File
@@ -0,0 +1,51 @@
import { mergeProps } from "@base-ui/react/merge-props"
import { useRender } from "@base-ui/react/use-render"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "cn"
const badgeVariants = cva(
"group/badge inline-flex h-5 w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-4xl border border-transparent px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-all focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3!",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80",
secondary:
"bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80",
destructive:
"bg-destructive/10 text-destructive focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:focus-visible:ring-destructive/40 [a]:hover:bg-destructive/20",
outline:
"border-border text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground",
ghost:
"hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50",
link: "text-primary underline-offset-4 hover:underline",
},
},
defaultVariants: {
variant: "default",
},
}
)
function Badge({
className,
variant = "default",
render,
...props
}: useRender.ComponentProps<"span"> & VariantProps<typeof badgeVariants>) {
return useRender({
defaultTagName: "span",
props: mergeProps<"span">(
{
className: cn(badgeVariants({ variant }), className),
},
props
),
render,
state: {
slot: "badge",
variant,
},
})
}
export { Badge, badgeVariants }
+124
View File
@@ -0,0 +1,124 @@
import * as React from "react"
import { mergeProps } from "@base-ui/react/merge-props"
import { useRender } from "@base-ui/react/use-render"
import { cn } from "cn"
import { ChevronRightIcon, MoreHorizontalIcon } from "lucide-react"
function Breadcrumb({ className, ...props }: React.ComponentProps<"nav">) {
return (
<nav
aria-label="breadcrumb"
data-slot="breadcrumb"
className={cn(className)}
{...props}
/>
)
}
function BreadcrumbList({ className, ...props }: React.ComponentProps<"ol">) {
return (
<ol
data-slot="breadcrumb-list"
className={cn(
"flex flex-wrap items-center gap-1.5 text-sm wrap-break-word text-muted-foreground",
className
)}
{...props}
/>
)
}
function BreadcrumbItem({ className, ...props }: React.ComponentProps<"li">) {
return (
<li
data-slot="breadcrumb-item"
className={cn("inline-flex items-center gap-1", className)}
{...props}
/>
)
}
function BreadcrumbLink({
className,
render,
...props
}: useRender.ComponentProps<"a">) {
return useRender({
defaultTagName: "a",
props: mergeProps<"a">(
{
className: cn("transition-colors hover:text-foreground", className),
},
props
),
render,
state: {
slot: "breadcrumb-link",
},
})
}
function BreadcrumbPage({ className, ...props }: React.ComponentProps<"span">) {
return (
<span
data-slot="breadcrumb-page"
role="link"
aria-disabled="true"
aria-current="page"
className={cn("font-normal text-foreground", className)}
{...props}
/>
)
}
function BreadcrumbSeparator({
children,
className,
...props
}: React.ComponentProps<"li">) {
return (
<li
data-slot="breadcrumb-separator"
role="presentation"
aria-hidden="true"
className={cn("[&>svg]:size-3.5", className)}
{...props}
>
{children ?? (
<ChevronRightIcon />
)}
</li>
)
}
function BreadcrumbEllipsis({
className,
...props
}: React.ComponentProps<"span">) {
return (
<span
data-slot="breadcrumb-ellipsis"
role="presentation"
aria-hidden="true"
className={cn(
"flex size-5 items-center justify-center [&>svg]:size-4",
className
)}
{...props}
>
<MoreHorizontalIcon
/>
<span className="sr-only">More</span>
</span>
)
}
export {
Breadcrumb,
BreadcrumbList,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbPage,
BreadcrumbSeparator,
BreadcrumbEllipsis,
}
+102
View File
@@ -0,0 +1,102 @@
import * as React from "react"
import { cn } from "cn"
function Card({
className,
size = "default",
...props
}: React.ComponentProps<"div"> & { size?: "default" | "sm" }) {
return (
<div
data-slot="card"
data-size={size}
className={cn(
"group/card flex flex-col gap-(--card-spacing) overflow-hidden rounded-xl bg-card py-(--card-spacing) text-sm text-card-foreground ring-1 ring-foreground/10 [--card-spacing:--spacing(4)] has-data-[slot=card-footer]:pb-0 has-[>img:first-child]:pt-0 data-[size=sm]:[--card-spacing:--spacing(3)] data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl",
className
)}
{...props}
/>
)
}
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-header"
className={cn(
"group/card-header @container/card-header grid auto-rows-min items-start gap-1 rounded-t-xl px-(--card-spacing) has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto] [.border-b]:pb-(--card-spacing)",
className
)}
{...props}
/>
)
}
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-title"
className={cn(
"font-heading text-base leading-snug font-medium group-data-[size=sm]/card:text-sm",
className
)}
{...props}
/>
)
}
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-description"
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
)
}
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-action"
className={cn(
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
className
)}
{...props}
/>
)
}
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-content"
className={cn("px-(--card-spacing)", className)}
{...props}
/>
)
}
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-footer"
className={cn(
"flex items-center rounded-b-xl border-t bg-muted/50 p-(--card-spacing)",
className
)}
{...props}
/>
)
}
export {
Card,
CardHeader,
CardFooter,
CardTitle,
CardAction,
CardDescription,
CardContent,
}
+28
View File
@@ -0,0 +1,28 @@
"use client";
import * as React from "react";
import { Checkbox as CheckboxPrimitive } from "@base-ui/react/checkbox";
import { CheckIcon } from "lucide-react";
import { cn } from "@/lib/utils";
function Checkbox({
className,
...props
}: React.ComponentProps<typeof CheckboxPrimitive.Root>) {
return (
<CheckboxPrimitive.Root
data-slot="checkbox"
className={cn(
"peer size-4 shrink-0 rounded-[4px] border border-input bg-transparent transition-all outline-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 data-[checked]:bg-primary data-[checked]:border-primary data-[checked]:text-primary-foreground",
className
)}
{...props}
>
<CheckboxPrimitive.Indicator className="flex items-center justify-center text-current">
<CheckIcon data-icon="inline-start" className="size-3.5 stroke-[2.5]" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
);
}
export { Checkbox };
+21
View File
@@ -0,0 +1,21 @@
"use client"
import { Collapsible as CollapsiblePrimitive } from "@base-ui/react/collapsible"
function Collapsible({ ...props }: CollapsiblePrimitive.Root.Props) {
return <CollapsiblePrimitive.Root data-slot="collapsible" {...props} />
}
function CollapsibleTrigger({ ...props }: CollapsiblePrimitive.Trigger.Props) {
return (
<CollapsiblePrimitive.Trigger data-slot="collapsible-trigger" {...props} />
)
}
function CollapsibleContent({ ...props }: CollapsiblePrimitive.Panel.Props) {
return (
<CollapsiblePrimitive.Panel data-slot="collapsible-content" {...props} />
)
}
export { Collapsible, CollapsibleTrigger, CollapsibleContent }
+297
View File
@@ -0,0 +1,297 @@
"use client"
import * as React from "react"
import { Combobox as ComboboxPrimitive } from "@base-ui/react"
import { cn } from "cn"
import { Button } from "@/components/ui/button"
import {
InputGroup,
InputGroupAddon,
InputGroupButton,
InputGroupInput,
} from "@/components/ui/input-group"
import { ChevronDownIcon, XIcon, CheckIcon } from "lucide-react"
const Combobox = ComboboxPrimitive.Root
function ComboboxValue({ ...props }: ComboboxPrimitive.Value.Props) {
return <ComboboxPrimitive.Value data-slot="combobox-value" {...props} />
}
function ComboboxTrigger({
className,
children,
...props
}: ComboboxPrimitive.Trigger.Props) {
return (
<ComboboxPrimitive.Trigger
data-slot="combobox-trigger"
className={cn("[&_svg:not([class*='size-'])]:size-4", className)}
{...props}
>
{children}
<ChevronDownIcon className="pointer-events-none size-4 text-muted-foreground" />
</ComboboxPrimitive.Trigger>
)
}
function ComboboxClear({ className, ...props }: ComboboxPrimitive.Clear.Props) {
return (
<ComboboxPrimitive.Clear
data-slot="combobox-clear"
render={<InputGroupButton variant="ghost" size="icon-xs" />}
className={cn(className)}
{...props}
>
<XIcon className="pointer-events-none" />
</ComboboxPrimitive.Clear>
)
}
function ComboboxInput({
className,
children,
disabled = false,
showTrigger = true,
showClear = false,
...props
}: ComboboxPrimitive.Input.Props & {
showTrigger?: boolean
showClear?: boolean
}) {
return (
<InputGroup className={cn("w-auto", className)}>
<ComboboxPrimitive.Input
render={<InputGroupInput disabled={disabled} />}
{...props}
/>
<InputGroupAddon align="inline-end">
{showTrigger && (
<InputGroupButton
size="icon-xs"
variant="ghost"
render={<ComboboxTrigger />}
data-slot="input-group-button"
className="group-has-data-[slot=combobox-clear]/input-group:hidden data-pressed:bg-transparent"
disabled={disabled}
/>
)}
{showClear && <ComboboxClear disabled={disabled} />}
</InputGroupAddon>
{children}
</InputGroup>
)
}
function ComboboxContent({
className,
side = "bottom",
sideOffset = 6,
align = "start",
alignOffset = 0,
anchor,
...props
}: ComboboxPrimitive.Popup.Props &
Pick<
ComboboxPrimitive.Positioner.Props,
"side" | "align" | "sideOffset" | "alignOffset" | "anchor"
>) {
return (
<ComboboxPrimitive.Portal>
<ComboboxPrimitive.Positioner
side={side}
sideOffset={sideOffset}
align={align}
alignOffset={alignOffset}
anchor={anchor}
className="isolate z-50"
>
<ComboboxPrimitive.Popup
data-slot="combobox-content"
data-chips={!!anchor}
className={cn("group/combobox-content relative max-h-(--available-height) w-(--anchor-width) max-w-(--available-width) min-w-[calc(var(--anchor-width)+--spacing(7))] origin-(--transform-origin) overflow-hidden rounded-lg bg-popover text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 data-[chips=true]:min-w-(--anchor-width) data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 *:data-[slot=input-group]:m-1 *:data-[slot=input-group]:mb-0 *:data-[slot=input-group]:h-8 *:data-[slot=input-group]:border-input/30 *:data-[slot=input-group]:bg-input/30 *:data-[slot=input-group]:shadow-none data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95", className )}
{...props}
/>
</ComboboxPrimitive.Positioner>
</ComboboxPrimitive.Portal>
)
}
function ComboboxList({ className, ...props }: ComboboxPrimitive.List.Props) {
return (
<ComboboxPrimitive.List
data-slot="combobox-list"
className={cn(
"no-scrollbar max-h-[min(calc(--spacing(72)---spacing(9)),calc(var(--available-height)---spacing(9)))] scroll-py-1 overflow-y-auto overscroll-contain p-1 data-empty:p-0",
className
)}
{...props}
/>
)
}
function ComboboxItem({
className,
children,
...props
}: ComboboxPrimitive.Item.Props) {
return (
<ComboboxPrimitive.Item
data-slot="combobox-item"
className={cn(
"relative flex w-full cursor-default items-center gap-2 rounded-md py-1 pr-8 pl-1.5 text-sm outline-hidden select-none data-highlighted:bg-accent data-highlighted:text-accent-foreground not-data-[variant=destructive]:data-highlighted:**:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
>
{children}
<ComboboxPrimitive.ItemIndicator
render={
<span className="pointer-events-none absolute right-2 flex size-4 items-center justify-center" />
}
>
<CheckIcon className="pointer-events-none" />
</ComboboxPrimitive.ItemIndicator>
</ComboboxPrimitive.Item>
)
}
function ComboboxGroup({ className, ...props }: ComboboxPrimitive.Group.Props) {
return (
<ComboboxPrimitive.Group
data-slot="combobox-group"
className={cn(className)}
{...props}
/>
)
}
function ComboboxLabel({
className,
...props
}: ComboboxPrimitive.GroupLabel.Props) {
return (
<ComboboxPrimitive.GroupLabel
data-slot="combobox-label"
className={cn("px-2 py-1.5 text-xs text-muted-foreground", className)}
{...props}
/>
)
}
function ComboboxCollection({ ...props }: ComboboxPrimitive.Collection.Props) {
return (
<ComboboxPrimitive.Collection data-slot="combobox-collection" {...props} />
)
}
function ComboboxEmpty({ className, ...props }: ComboboxPrimitive.Empty.Props) {
return (
<ComboboxPrimitive.Empty
data-slot="combobox-empty"
className={cn(
"hidden w-full justify-center py-2 text-center text-sm text-muted-foreground group-data-empty/combobox-content:flex",
className
)}
{...props}
/>
)
}
function ComboboxSeparator({
className,
...props
}: ComboboxPrimitive.Separator.Props) {
return (
<ComboboxPrimitive.Separator
data-slot="combobox-separator"
className={cn("-mx-1 my-1 h-px bg-border", className)}
{...props}
/>
)
}
function ComboboxChips({
className,
...props
}: React.ComponentPropsWithRef<typeof ComboboxPrimitive.Chips> &
ComboboxPrimitive.Chips.Props) {
return (
<ComboboxPrimitive.Chips
data-slot="combobox-chips"
className={cn(
"flex min-h-8 flex-wrap items-center gap-1 rounded-lg border border-input bg-transparent bg-clip-padding px-2.5 py-1 text-sm transition-colors focus-within:border-ring focus-within:ring-3 focus-within:ring-ring/50 has-aria-invalid:border-destructive has-aria-invalid:ring-3 has-aria-invalid:ring-destructive/20 has-data-[slot=combobox-chip]:px-1 dark:bg-input/30 dark:has-aria-invalid:border-destructive/50 dark:has-aria-invalid:ring-destructive/40",
className
)}
{...props}
/>
)
}
function ComboboxChip({
className,
children,
showRemove = true,
...props
}: ComboboxPrimitive.Chip.Props & {
showRemove?: boolean
}) {
return (
<ComboboxPrimitive.Chip
data-slot="combobox-chip"
className={cn(
"flex h-[calc(--spacing(5.25))] w-fit items-center justify-center gap-1 rounded-sm bg-muted px-1.5 text-xs font-medium whitespace-nowrap text-foreground has-disabled:pointer-events-none has-disabled:cursor-not-allowed has-disabled:opacity-50 has-data-[slot=combobox-chip-remove]:pr-0",
className
)}
{...props}
>
{children}
{showRemove && (
<ComboboxPrimitive.ChipRemove
render={<Button variant="ghost" size="icon-xs" />}
className="-ml-1 opacity-50 hover:opacity-100"
data-slot="combobox-chip-remove"
>
<XIcon className="pointer-events-none" />
</ComboboxPrimitive.ChipRemove>
)}
</ComboboxPrimitive.Chip>
)
}
function ComboboxChipsInput({
className,
...props
}: ComboboxPrimitive.Input.Props) {
return (
<ComboboxPrimitive.Input
data-slot="combobox-chip-input"
className={cn("min-w-16 flex-1 outline-none", className)}
{...props}
/>
)
}
function useComboboxAnchor() {
return React.useRef<HTMLDivElement | null>(null)
}
export {
Combobox,
ComboboxInput,
ComboboxContent,
ComboboxList,
ComboboxItem,
ComboboxGroup,
ComboboxLabel,
ComboboxCollection,
ComboboxEmpty,
ComboboxSeparator,
ComboboxChips,
ComboboxChip,
ComboboxChipsInput,
ComboboxTrigger,
ComboboxValue,
useComboboxAnchor,
}
+160
View File
@@ -0,0 +1,160 @@
"use client"
import * as React from "react"
import { Dialog as DialogPrimitive } from "@base-ui/react/dialog"
import { cn } from "cn"
import { Button } from "@/components/ui/button"
import { XIcon } from "lucide-react"
function Dialog({ ...props }: DialogPrimitive.Root.Props) {
return <DialogPrimitive.Root data-slot="dialog" {...props} />
}
function DialogTrigger({ ...props }: DialogPrimitive.Trigger.Props) {
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />
}
function DialogPortal({ ...props }: DialogPrimitive.Portal.Props) {
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />
}
function DialogClose({ ...props }: DialogPrimitive.Close.Props) {
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />
}
function DialogOverlay({
className,
...props
}: DialogPrimitive.Backdrop.Props) {
return (
<DialogPrimitive.Backdrop
data-slot="dialog-overlay"
className={cn(
"fixed inset-0 isolate z-50 bg-black/10 duration-100 supports-backdrop-filter:backdrop-blur-xs data-open:animate-in data-open:fade-in-0 data-closed:animate-out data-closed:fade-out-0",
className
)}
{...props}
/>
)
}
function DialogContent({
className,
children,
showCloseButton = true,
...props
}: DialogPrimitive.Popup.Props & {
showCloseButton?: boolean
}) {
return (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Popup
data-slot="dialog-content"
className={cn(
"fixed top-1/2 left-1/2 z-50 grid w-full max-w-[calc(100%-2rem)] -translate-x-1/2 -translate-y-1/2 gap-4 rounded-xl bg-popover p-4 text-sm text-popover-foreground ring-1 ring-foreground/10 duration-100 outline-none sm:max-w-sm data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
className
)}
{...props}
>
{children}
{showCloseButton && (
<DialogPrimitive.Close
data-slot="dialog-close"
render={
<Button
variant="ghost"
className="absolute top-2 right-2"
size="icon-sm"
/>
}
>
<XIcon
/>
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
)}
</DialogPrimitive.Popup>
</DialogPortal>
)
}
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="dialog-header"
className={cn("flex flex-col gap-2", className)}
{...props}
/>
)
}
function DialogFooter({
className,
showCloseButton = false,
children,
...props
}: React.ComponentProps<"div"> & {
showCloseButton?: boolean
}) {
return (
<div
data-slot="dialog-footer"
className={cn(
"-mx-4 -mb-4 flex flex-col-reverse gap-2 rounded-b-xl border-t bg-muted/50 p-4 sm:flex-row sm:justify-end",
className
)}
{...props}
>
{children}
{showCloseButton && (
<DialogPrimitive.Close render={<Button variant="outline" />}>
Close
</DialogPrimitive.Close>
)}
</div>
)
}
function DialogTitle({ className, ...props }: DialogPrimitive.Title.Props) {
return (
<DialogPrimitive.Title
data-slot="dialog-title"
className={cn(
"font-heading text-base leading-none font-medium",
className
)}
{...props}
/>
)
}
function DialogDescription({
className,
...props
}: DialogPrimitive.Description.Props) {
return (
<DialogPrimitive.Description
data-slot="dialog-description"
className={cn(
"text-sm text-muted-foreground *:[a]:underline *:[a]:underline-offset-3 *:[a]:hover:text-foreground",
className
)}
{...props}
/>
)
}
export {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogOverlay,
DialogPortal,
DialogTitle,
DialogTrigger,
}
+267
View File
@@ -0,0 +1,267 @@
"use client"
import * as React from "react"
import { Menu as MenuPrimitive } from "@base-ui/react/menu"
import { cn } from "cn"
import { ChevronRightIcon, CheckIcon } from "lucide-react"
function DropdownMenu({ ...props }: MenuPrimitive.Root.Props) {
return <MenuPrimitive.Root data-slot="dropdown-menu" {...props} />
}
function DropdownMenuPortal({ ...props }: MenuPrimitive.Portal.Props) {
return <MenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />
}
function DropdownMenuTrigger({ ...props }: MenuPrimitive.Trigger.Props) {
return <MenuPrimitive.Trigger data-slot="dropdown-menu-trigger" {...props} />
}
function DropdownMenuContent({
align = "start",
alignOffset = 0,
side = "bottom",
sideOffset = 4,
className,
...props
}: MenuPrimitive.Popup.Props &
Pick<
MenuPrimitive.Positioner.Props,
"align" | "alignOffset" | "side" | "sideOffset"
>) {
return (
<MenuPrimitive.Portal>
<MenuPrimitive.Positioner
className="isolate z-50 outline-none"
align={align}
alignOffset={alignOffset}
side={side}
sideOffset={sideOffset}
>
<MenuPrimitive.Popup
data-slot="dropdown-menu-content"
className={cn("z-50 max-h-(--available-height) w-(--anchor-width) min-w-32 origin-(--transform-origin) overflow-x-hidden overflow-y-auto rounded-lg bg-popover p-1 text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 outline-none data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:overflow-hidden data-closed:fade-out-0 data-closed:zoom-out-95", className )}
{...props}
/>
</MenuPrimitive.Positioner>
</MenuPrimitive.Portal>
)
}
function DropdownMenuGroup({ ...props }: MenuPrimitive.Group.Props) {
return <MenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />
}
function DropdownMenuLabel({
className,
inset,
...props
}: MenuPrimitive.GroupLabel.Props & {
inset?: boolean
}) {
return (
<MenuPrimitive.GroupLabel
data-slot="dropdown-menu-label"
data-inset={inset}
className={cn(
"px-1.5 py-1 text-xs font-medium text-muted-foreground data-inset:pl-7",
className
)}
{...props}
/>
)
}
function DropdownMenuItem({
className,
inset,
variant = "default",
...props
}: MenuPrimitive.Item.Props & {
inset?: boolean
variant?: "default" | "destructive"
}) {
return (
<MenuPrimitive.Item
data-slot="dropdown-menu-item"
data-inset={inset}
data-variant={variant}
className={cn(
"group/dropdown-menu-item relative flex cursor-default items-center gap-1.5 rounded-md px-1.5 py-1 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-inset:pl-7 data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 data-[variant=destructive]:focus:text-destructive dark:data-[variant=destructive]:focus:bg-destructive/20 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 data-[variant=destructive]:*:[svg]:text-destructive",
className
)}
{...props}
/>
)
}
function DropdownMenuSub({ ...props }: MenuPrimitive.SubmenuRoot.Props) {
return <MenuPrimitive.SubmenuRoot data-slot="dropdown-menu-sub" {...props} />
}
function DropdownMenuSubTrigger({
className,
inset,
children,
...props
}: MenuPrimitive.SubmenuTrigger.Props & {
inset?: boolean
}) {
return (
<MenuPrimitive.SubmenuTrigger
data-slot="dropdown-menu-sub-trigger"
data-inset={inset}
className={cn(
"flex cursor-default items-center gap-1.5 rounded-md px-1.5 py-1 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-inset:pl-7 data-popup-open:bg-accent data-popup-open:text-accent-foreground data-open:bg-accent data-open:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
>
{children}
<ChevronRightIcon className="ml-auto" />
</MenuPrimitive.SubmenuTrigger>
)
}
function DropdownMenuSubContent({
align = "start",
alignOffset = -3,
side = "right",
sideOffset = 0,
className,
...props
}: React.ComponentProps<typeof DropdownMenuContent>) {
return (
<DropdownMenuContent
data-slot="dropdown-menu-sub-content"
className={cn("w-auto min-w-[96px] rounded-lg bg-popover p-1 text-popover-foreground shadow-lg ring-1 ring-foreground/10 duration-100 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95", className )}
align={align}
alignOffset={alignOffset}
side={side}
sideOffset={sideOffset}
{...props}
/>
)
}
function DropdownMenuCheckboxItem({
className,
children,
checked,
inset,
...props
}: MenuPrimitive.CheckboxItem.Props & {
inset?: boolean
}) {
return (
<MenuPrimitive.CheckboxItem
data-slot="dropdown-menu-checkbox-item"
data-inset={inset}
className={cn(
"relative flex cursor-default items-center gap-1.5 rounded-md py-1 pr-8 pl-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground data-inset:pl-7 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
checked={checked}
{...props}
>
<span
className="pointer-events-none absolute right-2 flex items-center justify-center"
data-slot="dropdown-menu-checkbox-item-indicator"
>
<MenuPrimitive.CheckboxItemIndicator>
<CheckIcon
/>
</MenuPrimitive.CheckboxItemIndicator>
</span>
{children}
</MenuPrimitive.CheckboxItem>
)
}
function DropdownMenuRadioGroup({ ...props }: MenuPrimitive.RadioGroup.Props) {
return (
<MenuPrimitive.RadioGroup
data-slot="dropdown-menu-radio-group"
{...props}
/>
)
}
function DropdownMenuRadioItem({
className,
children,
inset,
...props
}: MenuPrimitive.RadioItem.Props & {
inset?: boolean
}) {
return (
<MenuPrimitive.RadioItem
data-slot="dropdown-menu-radio-item"
data-inset={inset}
className={cn(
"relative flex cursor-default items-center gap-1.5 rounded-md py-1 pr-8 pl-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground data-inset:pl-7 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
>
<span
className="pointer-events-none absolute right-2 flex items-center justify-center"
data-slot="dropdown-menu-radio-item-indicator"
>
<MenuPrimitive.RadioItemIndicator>
<CheckIcon
/>
</MenuPrimitive.RadioItemIndicator>
</span>
{children}
</MenuPrimitive.RadioItem>
)
}
function DropdownMenuSeparator({
className,
...props
}: MenuPrimitive.Separator.Props) {
return (
<MenuPrimitive.Separator
data-slot="dropdown-menu-separator"
className={cn("-mx-1 my-1 h-px bg-border", className)}
{...props}
/>
)
}
function DropdownMenuShortcut({
className,
...props
}: React.ComponentProps<"span">) {
return (
<span
data-slot="dropdown-menu-shortcut"
className={cn(
"ml-auto text-xs tracking-widest text-muted-foreground group-focus/dropdown-menu-item:text-accent-foreground",
className
)}
{...props}
/>
)
}
export {
DropdownMenu,
DropdownMenuPortal,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuLabel,
DropdownMenuItem,
DropdownMenuCheckboxItem,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuSub,
DropdownMenuSubTrigger,
DropdownMenuSubContent,
}
+238
View File
@@ -0,0 +1,238 @@
"use client"
import { useMemo } from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "cn"
import { Label } from "@/components/ui/label"
import { Separator } from "@/components/ui/separator"
function FieldSet({ className, ...props }: React.ComponentProps<"fieldset">) {
return (
<fieldset
data-slot="field-set"
className={cn(
"flex flex-col gap-4 has-[>[data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3",
className
)}
{...props}
/>
)
}
function FieldLegend({
className,
variant = "legend",
...props
}: React.ComponentProps<"legend"> & { variant?: "legend" | "label" }) {
return (
<legend
data-slot="field-legend"
data-variant={variant}
className={cn(
"mb-1.5 font-medium data-[variant=label]:text-sm data-[variant=legend]:text-base",
className
)}
{...props}
/>
)
}
function FieldGroup({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="field-group"
className={cn(
"group/field-group @container/field-group flex w-full flex-col gap-5 data-[slot=checkbox-group]:gap-3 *:data-[slot=field-group]:gap-4",
className
)}
{...props}
/>
)
}
const fieldVariants = cva(
"group/field flex w-full gap-2 data-[invalid=true]:text-destructive",
{
variants: {
orientation: {
vertical: "flex-col *:w-full [&>.sr-only]:w-auto",
horizontal:
"flex-row items-center has-[>[data-slot=field-content]]:items-start *:data-[slot=field-label]:flex-auto has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px",
responsive:
"flex-col *:w-full @md/field-group:flex-row @md/field-group:items-center @md/field-group:*:w-auto @md/field-group:has-[>[data-slot=field-content]]:items-start @md/field-group:*:data-[slot=field-label]:flex-auto [&>.sr-only]:w-auto @md/field-group:has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px",
},
},
defaultVariants: {
orientation: "vertical",
},
}
)
function Field({
className,
orientation = "vertical",
...props
}: React.ComponentProps<"div"> & VariantProps<typeof fieldVariants>) {
return (
<div
role="group"
data-slot="field"
data-orientation={orientation}
className={cn(fieldVariants({ orientation }), className)}
{...props}
/>
)
}
function FieldContent({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="field-content"
className={cn(
"group/field-content flex flex-1 flex-col gap-0.5 leading-snug",
className
)}
{...props}
/>
)
}
function FieldLabel({
className,
...props
}: React.ComponentProps<typeof Label>) {
return (
<Label
data-slot="field-label"
className={cn(
"group/field-label peer/field-label flex w-fit gap-2 leading-snug group-data-[disabled=true]/field:opacity-50 has-data-checked:border-primary/30 has-data-checked:bg-primary/5 has-[>[data-slot=field]]:rounded-lg has-[>[data-slot=field]]:border has-[>[data-slot=field]]:not-has-[:disabled,[data-disabled]]:hover:bg-muted/50 has-[>[data-slot=field]]:has-[:focus-visible]:border-ring has-[>[data-slot=field]]:has-[:focus-visible]:ring-3 has-[>[data-slot=field]]:has-[:focus-visible]:ring-ring/50 *:data-[slot=field]:p-2.5 dark:has-data-checked:border-primary/20 dark:has-data-checked:bg-primary/10",
"has-[>[data-slot=field]]:w-full has-[>[data-slot=field]]:flex-col",
className
)}
{...props}
/>
)
}
function FieldTitle({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="field-label"
className={cn(
"flex w-fit items-center gap-2 text-sm font-medium group-data-[disabled=true]/field:opacity-50",
className
)}
{...props}
/>
)
}
function FieldDescription({ className, ...props }: React.ComponentProps<"p">) {
return (
<p
data-slot="field-description"
className={cn(
"text-left text-sm leading-normal font-normal text-muted-foreground group-has-data-horizontal/field:text-balance [[data-variant=legend]+&]:-mt-1.5",
"last:mt-0 nth-last-2:-mt-1",
"[&>a]:underline [&>a]:underline-offset-4 [&>a:hover]:text-primary",
className
)}
{...props}
/>
)
}
function FieldSeparator({
children,
className,
...props
}: React.ComponentProps<"div"> & {
children?: React.ReactNode
}) {
return (
<div
data-slot="field-separator"
data-content={!!children}
className={cn(
"relative -my-2 h-5 text-sm group-data-[variant=outline]/field-group:-mb-2",
className
)}
{...props}
>
<Separator className="absolute inset-0 top-1/2" />
{children && (
<span
className="relative mx-auto block w-fit bg-background px-2 text-muted-foreground"
data-slot="field-separator-content"
>
{children}
</span>
)}
</div>
)
}
function FieldError({
className,
children,
errors,
...props
}: React.ComponentProps<"div"> & {
errors?: Array<{ message?: string } | undefined>
}) {
const content = useMemo(() => {
if (children) {
return children
}
if (!errors?.length) {
return null
}
const uniqueErrors = [
...new Map(errors.map((error) => [error?.message, error])).values(),
]
if (uniqueErrors?.length == 1) {
return uniqueErrors[0]?.message
}
return (
<ul className="ml-4 flex list-disc flex-col gap-1">
{uniqueErrors.map(
(error, index) =>
error?.message && <li key={index}>{error.message}</li>
)}
</ul>
)
}, [children, errors])
if (!content) {
return null
}
return (
<div
role="alert"
data-slot="field-error"
className={cn("text-sm font-normal text-destructive", className)}
{...props}
>
{content}
</div>
)
}
export {
Field,
FieldLabel,
FieldDescription,
FieldError,
FieldGroup,
FieldLegend,
FieldSeparator,
FieldSet,
FieldContent,
FieldTitle,
}
+158
View File
@@ -0,0 +1,158 @@
"use client"
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "cn"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Textarea } from "@/components/ui/textarea"
function InputGroup({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="input-group"
role="group"
className={cn(
"group/input-group relative flex h-8 w-full min-w-0 items-center rounded-lg border border-input transition-colors outline-none in-data-[slot=combobox-content]:focus-within:border-inherit in-data-[slot=combobox-content]:focus-within:ring-0 has-disabled:bg-input/50 has-disabled:opacity-50 has-[[data-slot=input-group-control]:focus-visible]:border-ring has-[[data-slot=input-group-control]:focus-visible]:ring-3 has-[[data-slot=input-group-control]:focus-visible]:ring-ring/50 has-[[data-slot][aria-invalid=true]]:border-destructive has-[[data-slot][aria-invalid=true]]:ring-3 has-[[data-slot][aria-invalid=true]]:ring-destructive/20 has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>textarea]:h-auto dark:bg-input/30 dark:has-disabled:bg-input/80 dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40 has-[>[data-align=block-end]]:[&>input]:pt-3 has-[>[data-align=block-start]]:[&>input]:pb-3 has-[>[data-align=inline-end]]:[&>input]:pr-1.5 has-[>[data-align=inline-start]]:[&>input]:pl-1.5",
className
)}
{...props}
/>
)
}
const inputGroupAddonVariants = cva(
"flex h-auto cursor-text items-center justify-center gap-2 py-1.5 text-sm font-medium text-muted-foreground select-none group-data-[disabled=true]/input-group:opacity-50 [&>kbd]:rounded-[calc(var(--radius)-5px)] [&>svg:not([class*='size-'])]:size-4",
{
variants: {
align: {
"inline-start":
"order-first pl-2 has-[>button]:ml-[-0.3rem] has-[>kbd]:ml-[-0.15rem]",
"inline-end":
"order-last pr-2 has-[>button]:mr-[-0.3rem] has-[>kbd]:mr-[-0.15rem]",
"block-start":
"order-first w-full justify-start px-2.5 pt-2 group-has-[>input]/input-group:pt-2 [.border-b]:pb-2",
"block-end":
"order-last w-full justify-start px-2.5 pb-2 group-has-[>input]/input-group:pb-2 [.border-t]:pt-2",
},
},
defaultVariants: {
align: "inline-start",
},
}
)
function InputGroupAddon({
className,
align = "inline-start",
...props
}: React.ComponentProps<"div"> & VariantProps<typeof inputGroupAddonVariants>) {
return (
<div
role="group"
data-slot="input-group-addon"
data-align={align}
className={cn(inputGroupAddonVariants({ align }), className)}
onClick={(e) => {
if ((e.target as HTMLElement).closest("button")) {
return
}
e.currentTarget.parentElement?.querySelector("input")?.focus()
}}
{...props}
/>
)
}
const inputGroupButtonVariants = cva(
"flex items-center gap-2 text-sm shadow-none",
{
variants: {
size: {
xs: "h-6 gap-1 rounded-[calc(var(--radius)-3px)] px-1.5 [&>svg:not([class*='size-'])]:size-3.5",
sm: "",
"icon-xs":
"size-6 rounded-[calc(var(--radius)-3px)] p-0 has-[>svg]:p-0",
"icon-sm": "size-8 p-0 has-[>svg]:p-0",
},
},
defaultVariants: {
size: "xs",
},
}
)
function InputGroupButton({
className,
type = "button",
variant = "ghost",
size = "xs",
...props
}: Omit<React.ComponentProps<typeof Button>, "size" | "type"> &
VariantProps<typeof inputGroupButtonVariants> & {
type?: "button" | "submit" | "reset"
}) {
return (
<Button
type={type}
data-size={size}
variant={variant}
className={cn(inputGroupButtonVariants({ size }), className)}
{...props}
/>
)
}
function InputGroupText({ className, ...props }: React.ComponentProps<"span">) {
return (
<span
className={cn(
"flex items-center gap-2 text-sm text-muted-foreground [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
/>
)
}
function InputGroupInput({
className,
...props
}: React.ComponentProps<"input">) {
return (
<Input
data-slot="input-group-control"
className={cn(
"flex-1 rounded-none border-0 bg-transparent shadow-none ring-0 focus-visible:ring-0 disabled:bg-transparent aria-invalid:ring-0 dark:bg-transparent dark:disabled:bg-transparent",
className
)}
{...props}
/>
)
}
function InputGroupTextarea({
className,
...props
}: React.ComponentProps<"textarea">) {
return (
<Textarea
data-slot="input-group-control"
className={cn(
"flex-1 resize-none rounded-none border-0 bg-transparent py-2 shadow-none ring-0 focus-visible:ring-0 disabled:bg-transparent aria-invalid:ring-0 dark:bg-transparent dark:disabled:bg-transparent",
className
)}
{...props}
/>
)
}
export {
InputGroup,
InputGroupAddon,
InputGroupButton,
InputGroupText,
InputGroupInput,
InputGroupTextarea,
}
+19
View File
@@ -0,0 +1,19 @@
import * as React from "react"
import { Input as InputPrimitive } from "@base-ui/react/input"
import { cn } from "cn"
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
return (
<InputPrimitive
type={type}
data-slot="input"
className={cn(
"h-8 w-full min-w-0 rounded-lg border border-input bg-transparent px-2.5 py-1 text-base transition-colors outline-none file:inline-flex file:h-6 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40",
className
)}
{...props}
/>
)
}
export { Input }
+19
View File
@@ -0,0 +1,19 @@
"use client"
import * as React from "react"
import { cn } from "cn"
function Label({ className, ...props }: React.ComponentProps<"label">) {
return (
<label
data-slot="label"
className={cn(
"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
className
)}
{...props}
/>
)
}
export { Label }
+200
View File
@@ -0,0 +1,200 @@
"use client"
import * as React from "react"
import { Select as SelectPrimitive } from "@base-ui/react/select"
import { cn } from "cn"
import { ChevronDownIcon, CheckIcon, ChevronUpIcon } from "lucide-react"
const Select = SelectPrimitive.Root
function SelectGroup({ className, ...props }: SelectPrimitive.Group.Props) {
return (
<SelectPrimitive.Group
data-slot="select-group"
className={cn("scroll-my-1 p-1", className)}
{...props}
/>
)
}
function SelectValue({ className, ...props }: SelectPrimitive.Value.Props) {
return (
<SelectPrimitive.Value
data-slot="select-value"
className={cn("flex flex-1 text-left", className)}
{...props}
/>
)
}
function SelectTrigger({
className,
size = "default",
children,
...props
}: SelectPrimitive.Trigger.Props & {
size?: "sm" | "default"
}) {
return (
<SelectPrimitive.Trigger
data-slot="select-trigger"
data-size={size}
className={cn(
"flex w-fit items-center justify-between gap-1.5 rounded-lg border border-input bg-transparent py-2 pr-2 pl-2.5 text-sm whitespace-nowrap transition-colors outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 data-placeholder:text-muted-foreground data-[size=default]:h-8 data-[size=sm]:h-7 data-[size=sm]:rounded-[min(var(--radius-md),10px)] *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-1.5 dark:bg-input/30 dark:hover:bg-input/50 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
>
{children}
<SelectPrimitive.Icon
render={
<ChevronDownIcon className="pointer-events-none size-4 text-muted-foreground" />
}
/>
</SelectPrimitive.Trigger>
)
}
function SelectContent({
className,
children,
side = "bottom",
sideOffset = 4,
align = "center",
alignOffset = 0,
alignItemWithTrigger = true,
...props
}: SelectPrimitive.Popup.Props &
Pick<
SelectPrimitive.Positioner.Props,
"align" | "alignOffset" | "side" | "sideOffset" | "alignItemWithTrigger"
>) {
return (
<SelectPrimitive.Portal>
<SelectPrimitive.Positioner
side={side}
sideOffset={sideOffset}
align={align}
alignOffset={alignOffset}
alignItemWithTrigger={alignItemWithTrigger}
className="isolate z-50"
>
<SelectPrimitive.Popup
data-slot="select-content"
data-align-trigger={alignItemWithTrigger}
className={cn("relative isolate z-50 max-h-(--available-height) w-(--anchor-width) min-w-36 origin-(--transform-origin) overflow-x-hidden overflow-y-auto rounded-lg bg-popover text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 data-[align-trigger=true]:animate-none data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95", className )}
{...props}
>
<SelectScrollUpButton />
<SelectPrimitive.List>{children}</SelectPrimitive.List>
<SelectScrollDownButton />
</SelectPrimitive.Popup>
</SelectPrimitive.Positioner>
</SelectPrimitive.Portal>
)
}
function SelectLabel({
className,
...props
}: SelectPrimitive.GroupLabel.Props) {
return (
<SelectPrimitive.GroupLabel
data-slot="select-label"
className={cn("px-1.5 py-1 text-xs text-muted-foreground", className)}
{...props}
/>
)
}
function SelectItem({
className,
children,
...props
}: SelectPrimitive.Item.Props) {
return (
<SelectPrimitive.Item
data-slot="select-item"
className={cn(
"relative flex w-full cursor-default items-center gap-1.5 rounded-md py-1 pr-8 pl-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
className
)}
{...props}
>
<SelectPrimitive.ItemText className="flex flex-1 shrink-0 gap-2 whitespace-nowrap">
{children}
</SelectPrimitive.ItemText>
<SelectPrimitive.ItemIndicator
render={
<span className="pointer-events-none absolute right-2 flex size-4 items-center justify-center" />
}
>
<CheckIcon className="pointer-events-none" />
</SelectPrimitive.ItemIndicator>
</SelectPrimitive.Item>
)
}
function SelectSeparator({
className,
...props
}: SelectPrimitive.Separator.Props) {
return (
<SelectPrimitive.Separator
data-slot="select-separator"
className={cn("pointer-events-none -mx-1 my-1 h-px bg-border", className)}
{...props}
/>
)
}
function SelectScrollUpButton({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.ScrollUpArrow>) {
return (
<SelectPrimitive.ScrollUpArrow
data-slot="select-scroll-up-button"
className={cn(
"top-0 z-10 flex w-full cursor-default items-center justify-center bg-popover py-1 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
>
<ChevronUpIcon
/>
</SelectPrimitive.ScrollUpArrow>
)
}
function SelectScrollDownButton({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.ScrollDownArrow>) {
return (
<SelectPrimitive.ScrollDownArrow
data-slot="select-scroll-down-button"
className={cn(
"bottom-0 z-10 flex w-full cursor-default items-center justify-center bg-popover py-1 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
>
<ChevronDownIcon
/>
</SelectPrimitive.ScrollDownArrow>
)
}
export {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectScrollDownButton,
SelectScrollUpButton,
SelectSeparator,
SelectTrigger,
SelectValue,
}
+24
View File
@@ -0,0 +1,24 @@
"use client"
import { Separator as SeparatorPrimitive } from "@base-ui/react/separator"
import { cn } from "cn"
function Separator({
className,
orientation = "horizontal",
...props
}: SeparatorPrimitive.Props) {
return (
<SeparatorPrimitive
data-slot="separator"
orientation={orientation}
className={cn(
"shrink-0 bg-border data-horizontal:h-px data-horizontal:w-full data-vertical:w-px data-vertical:self-stretch",
className
)}
{...props}
/>
)
}
export { Separator }
+138
View File
@@ -0,0 +1,138 @@
"use client"
import * as React from "react"
import { Dialog as SheetPrimitive } from "@base-ui/react/dialog"
import { cn } from "cn"
import { Button } from "@/components/ui/button"
import { XIcon } from "lucide-react"
function Sheet({ ...props }: SheetPrimitive.Root.Props) {
return <SheetPrimitive.Root data-slot="sheet" {...props} />
}
function SheetTrigger({ ...props }: SheetPrimitive.Trigger.Props) {
return <SheetPrimitive.Trigger data-slot="sheet-trigger" {...props} />
}
function SheetClose({ ...props }: SheetPrimitive.Close.Props) {
return <SheetPrimitive.Close data-slot="sheet-close" {...props} />
}
function SheetPortal({ ...props }: SheetPrimitive.Portal.Props) {
return <SheetPrimitive.Portal data-slot="sheet-portal" {...props} />
}
function SheetOverlay({ className, ...props }: SheetPrimitive.Backdrop.Props) {
return (
<SheetPrimitive.Backdrop
data-slot="sheet-overlay"
className={cn(
"fixed inset-0 z-50 bg-black/10 transition-opacity duration-150 data-ending-style:opacity-0 data-starting-style:opacity-0 supports-backdrop-filter:backdrop-blur-xs",
className
)}
{...props}
/>
)
}
function SheetContent({
className,
children,
side = "right",
showCloseButton = true,
...props
}: SheetPrimitive.Popup.Props & {
side?: "top" | "right" | "bottom" | "left"
showCloseButton?: boolean
}) {
return (
<SheetPortal>
<SheetOverlay />
<SheetPrimitive.Popup
data-slot="sheet-content"
data-side={side}
className={cn(
"fixed z-50 flex flex-col gap-4 bg-popover bg-clip-padding text-sm text-popover-foreground shadow-lg transition duration-200 ease-in-out data-ending-style:opacity-0 data-starting-style:opacity-0 data-[side=bottom]:inset-x-0 data-[side=bottom]:bottom-0 data-[side=bottom]:h-auto data-[side=bottom]:border-t data-[side=bottom]:data-ending-style:translate-y-[2.5rem] data-[side=bottom]:data-starting-style:translate-y-[2.5rem] data-[side=left]:inset-y-0 data-[side=left]:left-0 data-[side=left]:h-full data-[side=left]:w-3/4 data-[side=left]:border-r data-[side=left]:data-ending-style:translate-x-[-2.5rem] data-[side=left]:data-starting-style:translate-x-[-2.5rem] data-[side=right]:inset-y-0 data-[side=right]:right-0 data-[side=right]:h-full data-[side=right]:w-3/4 data-[side=right]:border-l data-[side=right]:data-ending-style:translate-x-[2.5rem] data-[side=right]:data-starting-style:translate-x-[2.5rem] data-[side=top]:inset-x-0 data-[side=top]:top-0 data-[side=top]:h-auto data-[side=top]:border-b data-[side=top]:data-ending-style:translate-y-[-2.5rem] data-[side=top]:data-starting-style:translate-y-[-2.5rem] data-[side=left]:sm:max-w-sm data-[side=right]:sm:max-w-sm",
className
)}
{...props}
>
{children}
{showCloseButton && (
<SheetPrimitive.Close
data-slot="sheet-close"
render={
<Button
variant="ghost"
className="absolute top-3 right-3"
size="icon-sm"
/>
}
>
<XIcon
/>
<span className="sr-only">Close</span>
</SheetPrimitive.Close>
)}
</SheetPrimitive.Popup>
</SheetPortal>
)
}
function SheetHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="sheet-header"
className={cn("flex flex-col gap-0.5 p-4", className)}
{...props}
/>
)
}
function SheetFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="sheet-footer"
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
{...props}
/>
)
}
function SheetTitle({ className, ...props }: SheetPrimitive.Title.Props) {
return (
<SheetPrimitive.Title
data-slot="sheet-title"
className={cn(
"font-heading text-base font-medium text-foreground",
className
)}
{...props}
/>
)
}
function SheetDescription({
className,
...props
}: SheetPrimitive.Description.Props) {
return (
<SheetPrimitive.Description
data-slot="sheet-description"
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
)
}
export {
Sheet,
SheetTrigger,
SheetClose,
SheetContent,
SheetHeader,
SheetFooter,
SheetTitle,
SheetDescription,
}
+723
View File
@@ -0,0 +1,723 @@
"use client"
import * as React from "react"
import { mergeProps } from "@base-ui/react/merge-props"
import { useRender } from "@base-ui/react/use-render"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "cn"
import { useIsMobile } from "@/hooks/use-mobile"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Separator } from "@/components/ui/separator"
import {
Sheet,
SheetContent,
SheetDescription,
SheetHeader,
SheetTitle,
} from "@/components/ui/sheet"
import { Skeleton } from "@/components/ui/skeleton"
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip"
import { PanelLeftIcon } from "lucide-react"
const SIDEBAR_COOKIE_NAME = "sidebar_state"
const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7
const SIDEBAR_WIDTH = "16rem"
const SIDEBAR_WIDTH_MOBILE = "18rem"
const SIDEBAR_WIDTH_ICON = "3rem"
const SIDEBAR_KEYBOARD_SHORTCUT = "b"
type SidebarContextProps = {
state: "expanded" | "collapsed"
open: boolean
setOpen: (open: boolean) => void
openMobile: boolean
setOpenMobile: (open: boolean) => void
isMobile: boolean
toggleSidebar: () => void
}
const SidebarContext = React.createContext<SidebarContextProps | null>(null)
function useSidebar() {
const context = React.useContext(SidebarContext)
if (!context) {
throw new Error("useSidebar must be used within a SidebarProvider.")
}
return context
}
function SidebarProvider({
defaultOpen = true,
open: openProp,
onOpenChange: setOpenProp,
className,
style,
children,
...props
}: React.ComponentProps<"div"> & {
defaultOpen?: boolean
open?: boolean
onOpenChange?: (open: boolean) => void
}) {
const isMobile = useIsMobile()
const [openMobile, setOpenMobile] = React.useState(false)
// This is the internal state of the sidebar.
// We use openProp and setOpenProp for control from outside the component.
const [_open, _setOpen] = React.useState(defaultOpen)
const open = openProp ?? _open
const setOpen = React.useCallback(
(value: boolean | ((value: boolean) => boolean)) => {
const openState = typeof value === "function" ? value(open) : value
if (setOpenProp) {
setOpenProp(openState)
} else {
_setOpen(openState)
}
// This sets the cookie to keep the sidebar state.
document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`
},
[setOpenProp, open]
)
// Helper to toggle the sidebar.
const toggleSidebar = React.useCallback(() => {
return isMobile ? setOpenMobile((open) => !open) : setOpen((open) => !open)
}, [isMobile, setOpen, setOpenMobile])
// Adds a keyboard shortcut to toggle the sidebar.
React.useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (
event.key === SIDEBAR_KEYBOARD_SHORTCUT &&
(event.metaKey || event.ctrlKey)
) {
event.preventDefault()
toggleSidebar()
}
}
window.addEventListener("keydown", handleKeyDown)
return () => window.removeEventListener("keydown", handleKeyDown)
}, [toggleSidebar])
// We add a state so that we can do data-state="expanded" or "collapsed".
// This makes it easier to style the sidebar with Tailwind classes.
const state = open ? "expanded" : "collapsed"
const contextValue = React.useMemo<SidebarContextProps>(
() => ({
state,
open,
setOpen,
isMobile,
openMobile,
setOpenMobile,
toggleSidebar,
}),
[state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar]
)
return (
<SidebarContext.Provider value={contextValue}>
<div
data-slot="sidebar-wrapper"
style={
{
"--sidebar-width": SIDEBAR_WIDTH,
"--sidebar-width-icon": SIDEBAR_WIDTH_ICON,
...style,
} as React.CSSProperties
}
className={cn(
"group/sidebar-wrapper flex min-h-svh w-full has-data-[variant=inset]:bg-sidebar",
className
)}
{...props}
>
{children}
</div>
</SidebarContext.Provider>
)
}
function Sidebar({
side = "left",
variant = "sidebar",
collapsible = "offcanvas",
className,
children,
dir,
...props
}: React.ComponentProps<"div"> & {
side?: "left" | "right"
variant?: "sidebar" | "floating" | "inset"
collapsible?: "offcanvas" | "icon" | "none"
}) {
const { isMobile, state, openMobile, setOpenMobile } = useSidebar()
if (collapsible === "none") {
return (
<div
data-slot="sidebar"
className={cn(
"flex h-full w-(--sidebar-width) flex-col bg-sidebar text-sidebar-foreground",
className
)}
{...props}
>
{children}
</div>
)
}
if (isMobile) {
return (
<Sheet open={openMobile} onOpenChange={setOpenMobile} {...props}>
<SheetContent
dir={dir}
data-sidebar="sidebar"
data-slot="sidebar"
data-mobile="true"
className="w-(--sidebar-width) bg-sidebar p-0 text-sidebar-foreground [&>button]:hidden"
style={
{
"--sidebar-width": SIDEBAR_WIDTH_MOBILE,
} as React.CSSProperties
}
side={side}
>
<SheetHeader className="sr-only">
<SheetTitle>Sidebar</SheetTitle>
<SheetDescription>Displays the mobile sidebar.</SheetDescription>
</SheetHeader>
<div className="flex h-full w-full flex-col">{children}</div>
</SheetContent>
</Sheet>
)
}
return (
<div
className="group peer hidden text-sidebar-foreground md:block"
data-state={state}
data-collapsible={state === "collapsed" ? collapsible : ""}
data-variant={variant}
data-side={side}
data-slot="sidebar"
>
{/* This is what handles the sidebar gap on desktop */}
<div
data-slot="sidebar-gap"
className={cn(
"relative w-(--sidebar-width) bg-transparent transition-[width] duration-200 ease-linear",
"group-data-[collapsible=offcanvas]:w-0",
"group-data-[side=right]:rotate-180",
variant === "floating" || variant === "inset"
? "group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4)))]"
: "group-data-[collapsible=icon]:w-(--sidebar-width-icon)"
)}
/>
<div
data-slot="sidebar-container"
data-side={side}
className={cn(
"fixed inset-y-0 z-10 hidden h-svh w-(--sidebar-width) transition-[left,right,width] duration-200 ease-linear data-[side=left]:left-0 data-[side=left]:group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)] data-[side=right]:right-0 data-[side=right]:group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)] md:flex",
// Adjust the padding for floating and inset variants.
variant === "floating" || variant === "inset"
? "p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4))+2px)]"
: "group-data-[collapsible=icon]:w-(--sidebar-width-icon) group-data-[side=left]:border-r group-data-[side=right]:border-l",
className
)}
{...props}
>
<div
data-sidebar="sidebar"
data-slot="sidebar-inner"
className="flex size-full flex-col bg-sidebar group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:shadow-sm group-data-[variant=floating]:ring-1 group-data-[variant=floating]:ring-sidebar-border"
>
{children}
</div>
</div>
</div>
)
}
function SidebarTrigger({
className,
onClick,
...props
}: React.ComponentProps<typeof Button>) {
const { toggleSidebar } = useSidebar()
return (
<Button
data-sidebar="trigger"
data-slot="sidebar-trigger"
variant="ghost"
size="icon-sm"
className={cn(className)}
onClick={(event) => {
onClick?.(event)
toggleSidebar()
}}
{...props}
>
<PanelLeftIcon />
<span className="sr-only">Toggle Sidebar</span>
</Button>
)
}
function SidebarRail({ className, ...props }: React.ComponentProps<"button">) {
const { toggleSidebar } = useSidebar()
return (
<button
data-sidebar="rail"
data-slot="sidebar-rail"
aria-label="Toggle Sidebar"
tabIndex={-1}
onClick={toggleSidebar}
title="Toggle Sidebar"
className={cn(
"absolute inset-y-0 z-20 hidden w-4 transition-all ease-linear group-data-[side=left]:-right-4 group-data-[side=right]:left-0 after:absolute after:inset-y-0 after:start-1/2 after:w-[2px] hover:after:bg-sidebar-border sm:flex ltr:-translate-x-1/2 rtl:-translate-x-1/2",
"in-data-[side=left]:cursor-w-resize in-data-[side=right]:cursor-e-resize",
"[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize",
"group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full hover:group-data-[collapsible=offcanvas]:bg-sidebar",
"[[data-side=left][data-collapsible=offcanvas]_&]:-right-2",
"[[data-side=right][data-collapsible=offcanvas]_&]:-left-2",
className
)}
{...props}
/>
)
}
function SidebarInset({ className, ...props }: React.ComponentProps<"main">) {
return (
<main
data-slot="sidebar-inset"
className={cn(
"relative flex w-full flex-1 flex-col bg-background md:peer-data-[variant=inset]:m-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow-sm md:peer-data-[variant=inset]:peer-data-[state=collapsed]:ml-2",
className
)}
{...props}
/>
)
}
function SidebarInput({
className,
...props
}: React.ComponentProps<typeof Input>) {
return (
<Input
data-slot="sidebar-input"
data-sidebar="input"
className={cn("h-8 w-full bg-background shadow-none", className)}
{...props}
/>
)
}
function SidebarHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="sidebar-header"
data-sidebar="header"
className={cn("flex flex-col gap-2 p-2", className)}
{...props}
/>
)
}
function SidebarFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="sidebar-footer"
data-sidebar="footer"
className={cn("flex flex-col gap-2 p-2", className)}
{...props}
/>
)
}
function SidebarSeparator({
className,
...props
}: React.ComponentProps<typeof Separator>) {
return (
<Separator
data-slot="sidebar-separator"
data-sidebar="separator"
className={cn("mx-2 w-auto bg-sidebar-border", className)}
{...props}
/>
)
}
function SidebarContent({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="sidebar-content"
data-sidebar="content"
className={cn(
"no-scrollbar flex min-h-0 flex-1 flex-col gap-0 overflow-auto group-data-[collapsible=icon]:overflow-hidden",
className
)}
{...props}
/>
)
}
function SidebarGroup({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="sidebar-group"
data-sidebar="group"
className={cn("relative flex w-full min-w-0 flex-col p-2", className)}
{...props}
/>
)
}
function SidebarGroupLabel({
className,
render,
...props
}: useRender.ComponentProps<"div"> & React.ComponentProps<"div">) {
return useRender({
defaultTagName: "div",
props: mergeProps<"div">(
{
className: cn(
"flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium text-sidebar-foreground/70 ring-sidebar-ring outline-hidden transition-[margin,opacity] duration-200 ease-linear group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0 focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
className
),
},
props
),
render,
state: {
slot: "sidebar-group-label",
sidebar: "group-label",
},
})
}
function SidebarGroupAction({
className,
render,
...props
}: useRender.ComponentProps<"button"> & React.ComponentProps<"button">) {
return useRender({
defaultTagName: "button",
props: mergeProps<"button">(
{
className: cn(
"absolute top-3.5 right-3 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground ring-sidebar-ring outline-hidden transition-transform group-data-[collapsible=icon]:hidden after:absolute after:-inset-2 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 md:after:hidden [&>svg]:size-4 [&>svg]:shrink-0",
className
),
},
props
),
render,
state: {
slot: "sidebar-group-action",
sidebar: "group-action",
},
})
}
function SidebarGroupContent({
className,
...props
}: React.ComponentProps<"div">) {
return (
<div
data-slot="sidebar-group-content"
data-sidebar="group-content"
className={cn("w-full text-sm", className)}
{...props}
/>
)
}
function SidebarMenu({ className, ...props }: React.ComponentProps<"ul">) {
return (
<ul
data-slot="sidebar-menu"
data-sidebar="menu"
className={cn("flex w-full min-w-0 flex-col gap-0", className)}
{...props}
/>
)
}
function SidebarMenuItem({ className, ...props }: React.ComponentProps<"li">) {
return (
<li
data-slot="sidebar-menu-item"
data-sidebar="menu-item"
className={cn("group/menu-item relative", className)}
{...props}
/>
)
}
const sidebarMenuButtonVariants = cva(
"peer/menu-button group/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm ring-sidebar-ring outline-hidden transition-[width,height,padding] group-has-data-[sidebar=menu-action]/menu-item:pr-8 group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-open:hover:bg-sidebar-accent data-open:hover:text-sidebar-accent-foreground data-active:bg-sidebar-accent data-active:font-medium data-active:text-sidebar-accent-foreground [&_svg]:size-4 [&_svg]:shrink-0 [&>span:last-child]:truncate",
{
variants: {
variant: {
default: "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
outline:
"bg-background shadow-[0_0_0_1px_var(--sidebar-border)] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_var(--sidebar-accent)]",
},
size: {
default: "h-8 text-sm",
sm: "h-7 text-xs",
lg: "h-12 text-sm group-data-[collapsible=icon]:p-0!",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
function SidebarMenuButton({
render,
isActive = false,
variant = "default",
size = "default",
tooltip,
className,
...props
}: useRender.ComponentProps<"button"> &
React.ComponentProps<"button"> & {
isActive?: boolean
tooltip?: string | React.ComponentProps<typeof TooltipContent>
} & VariantProps<typeof sidebarMenuButtonVariants>) {
const { isMobile, state } = useSidebar()
const comp = useRender({
defaultTagName: "button",
props: mergeProps<"button">(
{
className: cn(sidebarMenuButtonVariants({ variant, size }), className),
},
props
),
render: !tooltip ? render : <TooltipTrigger render={render} />,
state: {
slot: "sidebar-menu-button",
sidebar: "menu-button",
size,
active: isActive,
},
})
if (!tooltip) {
return comp
}
if (typeof tooltip === "string") {
tooltip = {
children: tooltip,
}
}
return (
<Tooltip>
{comp}
<TooltipContent
side="right"
align="center"
hidden={state !== "collapsed" || isMobile}
{...tooltip}
/>
</Tooltip>
)
}
function SidebarMenuAction({
className,
render,
showOnHover = false,
...props
}: useRender.ComponentProps<"button"> &
React.ComponentProps<"button"> & {
showOnHover?: boolean
}) {
return useRender({
defaultTagName: "button",
props: mergeProps<"button">(
{
className: cn(
"absolute top-1.5 right-1 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground ring-sidebar-ring outline-hidden transition-transform group-data-[collapsible=icon]:hidden peer-hover/menu-button:text-sidebar-accent-foreground peer-data-[size=default]/menu-button:top-1.5 peer-data-[size=lg]/menu-button:top-2.5 peer-data-[size=sm]/menu-button:top-1 after:absolute after:-inset-2 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 md:after:hidden [&>svg]:size-4 [&>svg]:shrink-0",
showOnHover &&
"group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 peer-data-active/menu-button:text-sidebar-accent-foreground aria-expanded:opacity-100 md:opacity-0",
className
),
},
props
),
render,
state: {
slot: "sidebar-menu-action",
sidebar: "menu-action",
},
})
}
function SidebarMenuBadge({
className,
...props
}: React.ComponentProps<"div">) {
return (
<div
data-slot="sidebar-menu-badge"
data-sidebar="menu-badge"
className={cn(
"pointer-events-none absolute right-1 flex h-5 min-w-5 items-center justify-center rounded-md px-1 text-xs font-medium text-sidebar-foreground tabular-nums select-none group-data-[collapsible=icon]:hidden peer-hover/menu-button:text-sidebar-accent-foreground peer-data-[size=default]/menu-button:top-1.5 peer-data-[size=lg]/menu-button:top-2.5 peer-data-[size=sm]/menu-button:top-1 peer-data-active/menu-button:text-sidebar-accent-foreground",
className
)}
{...props}
/>
)
}
function SidebarMenuSkeleton({
className,
showIcon = false,
...props
}: React.ComponentProps<"div"> & {
showIcon?: boolean
}) {
// Random width between 50 to 90%.
const [width] = React.useState(() => {
return `${Math.floor(Math.random() * 40) + 50}%`
})
return (
<div
data-slot="sidebar-menu-skeleton"
data-sidebar="menu-skeleton"
className={cn("flex h-8 items-center gap-2 rounded-md px-2", className)}
{...props}
>
{showIcon && (
<Skeleton
className="size-4 rounded-md"
data-sidebar="menu-skeleton-icon"
/>
)}
<Skeleton
className="h-4 max-w-(--skeleton-width) flex-1"
data-sidebar="menu-skeleton-text"
style={
{
"--skeleton-width": width,
} as React.CSSProperties
}
/>
</div>
)
}
function SidebarMenuSub({ className, ...props }: React.ComponentProps<"ul">) {
return (
<ul
data-slot="sidebar-menu-sub"
data-sidebar="menu-sub"
className={cn(
"mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-l border-sidebar-border px-2.5 py-0.5 group-data-[collapsible=icon]:hidden",
className
)}
{...props}
/>
)
}
function SidebarMenuSubItem({
className,
...props
}: React.ComponentProps<"li">) {
return (
<li
data-slot="sidebar-menu-sub-item"
data-sidebar="menu-sub-item"
className={cn("group/menu-sub-item relative", className)}
{...props}
/>
)
}
function SidebarMenuSubButton({
render,
size = "md",
isActive = false,
className,
...props
}: useRender.ComponentProps<"a"> &
React.ComponentProps<"a"> & {
size?: "sm" | "md"
isActive?: boolean
}) {
return useRender({
defaultTagName: "a",
props: mergeProps<"a">(
{
className: cn(
"flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 text-sidebar-foreground ring-sidebar-ring outline-hidden group-data-[collapsible=icon]:hidden hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[size=md]:text-sm data-[size=sm]:text-xs data-active:bg-sidebar-accent data-active:text-sidebar-accent-foreground [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0 [&>svg]:text-sidebar-accent-foreground",
className
),
},
props
),
render,
state: {
slot: "sidebar-menu-sub-button",
sidebar: "menu-sub-button",
size,
active: isActive,
},
})
}
export {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarGroup,
SidebarGroupAction,
SidebarGroupContent,
SidebarGroupLabel,
SidebarHeader,
SidebarInput,
SidebarInset,
SidebarMenu,
SidebarMenuAction,
SidebarMenuBadge,
SidebarMenuButton,
SidebarMenuItem,
SidebarMenuSkeleton,
SidebarMenuSub,
SidebarMenuSubButton,
SidebarMenuSubItem,
SidebarProvider,
SidebarRail,
SidebarSeparator,
SidebarTrigger,
useSidebar,
}
+13
View File
@@ -0,0 +1,13 @@
import { cn } from "cn"
function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="skeleton"
className={cn("animate-pulse rounded-md bg-muted", className)}
{...props}
/>
)
}
export { Skeleton }
+31
View File
@@ -0,0 +1,31 @@
"use client"
import { Switch as SwitchPrimitive } from "@base-ui/react/switch"
import { cn } from "cn"
function Switch({
className,
size = "default",
...props
}: SwitchPrimitive.Root.Props & {
size?: "sm" | "default"
}) {
return (
<SwitchPrimitive.Root
data-slot="switch"
data-size={size}
className={cn(
"peer group/switch relative inline-flex shrink-0 items-center rounded-full border border-transparent transition-all outline-none group-has-[:focus-visible]/field-label:border-transparent group-has-[:focus-visible]/field-label:ring-0 after:absolute after:-inset-x-3 after:-inset-y-2 focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 data-[size=default]:h-[18.4px] data-[size=default]:w-[32px] data-[size=sm]:h-[14px] data-[size=sm]:w-[24px] dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 data-checked:bg-primary data-unchecked:bg-input dark:data-unchecked:bg-input/80 data-disabled:cursor-not-allowed data-disabled:opacity-50",
className
)}
{...props}
>
<SwitchPrimitive.Thumb
data-slot="switch-thumb"
className="pointer-events-none block rounded-full bg-background ring-0 transition-transform group-data-[size=default]/switch:size-4 group-data-[size=sm]/switch:size-3 group-data-[size=default]/switch:data-checked:translate-x-[calc(100%-2px)] group-data-[size=sm]/switch:data-checked:translate-x-[calc(100%-2px)] dark:data-checked:bg-primary-foreground group-data-[size=default]/switch:data-unchecked:translate-x-0 group-data-[size=sm]/switch:data-unchecked:translate-x-0 dark:data-unchecked:bg-foreground"
/>
</SwitchPrimitive.Root>
)
}
export { Switch }
+17
View File
@@ -0,0 +1,17 @@
import * as React from "react"
import { cn } from "cn"
function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
return (
<textarea
data-slot="textarea"
className={cn(
"flex field-sizing-content min-h-16 w-full rounded-lg border border-input bg-transparent px-2.5 py-2 text-base transition-colors outline-none placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40",
className
)}
{...props}
/>
)
}
export { Textarea }
+65
View File
@@ -0,0 +1,65 @@
"use client"
import { Tooltip as TooltipPrimitive } from "@base-ui/react/tooltip"
import { cn } from "cn"
function TooltipProvider({
delay = 0,
...props
}: TooltipPrimitive.Provider.Props) {
return (
<TooltipPrimitive.Provider
data-slot="tooltip-provider"
delay={delay}
{...props}
/>
)
}
function Tooltip({ ...props }: TooltipPrimitive.Root.Props) {
return <TooltipPrimitive.Root data-slot="tooltip" {...props} />
}
function TooltipTrigger({ ...props }: TooltipPrimitive.Trigger.Props) {
return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />
}
function TooltipContent({
className,
side = "top",
sideOffset = 4,
align = "center",
alignOffset = 0,
children,
...props
}: TooltipPrimitive.Popup.Props &
Pick<
TooltipPrimitive.Positioner.Props,
"align" | "alignOffset" | "side" | "sideOffset"
>) {
return (
<TooltipPrimitive.Portal>
<TooltipPrimitive.Positioner
align={align}
alignOffset={alignOffset}
side={side}
sideOffset={sideOffset}
className="isolate z-50"
>
<TooltipPrimitive.Popup
data-slot="tooltip-content"
className={cn(
"z-50 inline-flex w-fit max-w-xs origin-(--transform-origin) items-center gap-1.5 rounded-md bg-foreground px-3 py-1.5 text-xs text-background has-data-[slot=kbd]:pr-1.5 data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 **:data-[slot=kbd]:relative **:data-[slot=kbd]:isolate **:data-[slot=kbd]:z-50 **:data-[slot=kbd]:rounded-sm data-[state=delayed-open]:animate-in data-[state=delayed-open]:fade-in-0 data-[state=delayed-open]:zoom-in-95 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
className
)}
{...props}
>
{children}
<TooltipPrimitive.Arrow className="z-50 size-2.5 translate-y-[calc(-50%-2px)] rotate-45 rounded-[2px] bg-foreground fill-foreground data-[side=bottom]:top-1 data-[side=inline-end]:top-1/2! data-[side=inline-end]:-left-1 data-[side=inline-end]:-translate-y-1/2 data-[side=inline-start]:top-1/2! data-[side=inline-start]:-right-1 data-[side=inline-start]:-translate-y-1/2 data-[side=left]:top-1/2! data-[side=left]:-right-1 data-[side=left]:-translate-y-1/2 data-[side=right]:top-1/2! data-[side=right]:-left-1 data-[side=right]:-translate-y-1/2 data-[side=top]:-bottom-2.5" />
</TooltipPrimitive.Popup>
</TooltipPrimitive.Positioner>
</TooltipPrimitive.Portal>
)
}
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
+15
View File
@@ -0,0 +1,15 @@
import { defineConfig } from "drizzle-kit";
import * as dotenv from "dotenv";
dotenv.config({ path: ".env.local" });
export default defineConfig({
schema: "./lib/db/schema.ts",
out: "./drizzle",
dialect: "postgresql",
dbCredentials: {
url: process.env.DATABASE_URL!,
},
strict: true,
verbose: true,
});
+15
View File
@@ -0,0 +1,15 @@
import * as React from "react";
const MOBILE_BREAKPOINT = 768;
export function useIsMobile() {
return React.useSyncExternalStore(
(callback) => {
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);
mql.addEventListener("change", callback);
return () => mql.removeEventListener("change", callback);
},
() => window.innerWidth < MOBILE_BREAKPOINT,
() => false
);
}
+217
View File
@@ -0,0 +1,217 @@
"use server";
import { z } from "zod";
import { and, desc, eq, isNull } from "drizzle-orm";
import { revalidatePath } from "next/cache";
import { auth } from "@/lib/auth";
import { db } from "@/lib/db";
import { accounts, channels, type Account } from "@/lib/db/schema";
async function requireUser() {
const session = await auth();
if (!session?.user?.id) {
throw new Error("请先登录");
}
return session.user.id;
}
const accountSchema = z.object({
name: z.string().min(1, "请输入账户名称").max(100),
accountType: z.enum(["BANK", "E_WALLET", "CASH"] as const),
balanceType: z.enum(["ASSET", "LIABILITY", "EQUITY"] as const),
primaryCurrency: z.string().trim().max(10).optional().nullable(),
supportedCurrencies: z.array(z.string().trim()).optional().nullable(),
remark: z.string().max(500).optional().nullable(),
ext: z.string().optional().nullable(),
// 银行账户字段
issuerName: z.string().max(100).optional().nullable(),
accountNumber: z.string().max(100).optional().nullable(),
// 电子钱包字段
platform: z.string().max(64).optional().nullable(),
accountId: z.string().max(100).optional().nullable(),
// 现金账户字段
location: z.string().max(150).optional().nullable(),
});
export type AccountInput = z.infer<typeof accountSchema>;
export type AccountWithChannels = Account & {
channelCount: number;
};
export async function getAccountsAction(): Promise<{
success: boolean;
data?: AccountWithChannels[];
error?: string;
}> {
try {
const userId = await requireUser();
const userAccounts = await db
.select()
.from(accounts)
.where(and(eq(accounts.userId, userId), isNull(accounts.deletedAt)))
.orderBy(desc(accounts.createdAt));
const userChannels = await db
.select({ id: channels.id, refAccounts: channels.refAccounts })
.from(channels)
.where(and(eq(channels.userId, userId), isNull(channels.deletedAt)));
const result: AccountWithChannels[] = userAccounts.map((acc) => {
const count = userChannels.filter((ch) =>
Array.isArray(ch.refAccounts) && ch.refAccounts.includes(acc.id)
).length;
return {
...acc,
channelCount: count,
};
});
return { success: true, data: result };
} catch (err) {
console.error("getAccountsAction error:", err);
return { success: false, error: err instanceof Error ? err.message : "获取账户失败" };
}
}
export async function createAccountAction(
data: AccountInput
): Promise<{ success: boolean; data?: Account; error?: string }> {
try {
const userId = await requireUser();
const parsed = accountSchema.safeParse(data);
if (!parsed.success) {
return { success: false, error: parsed.error.issues[0]?.message || "参数错误" };
}
const val = parsed.data;
const [newAccount] = await db
.insert(accounts)
.values({
userId,
name: val.name.trim(),
accountType: val.accountType,
balanceType: val.balanceType,
primaryCurrency: val.primaryCurrency ? val.primaryCurrency.toUpperCase() : null,
supportedCurrencies: val.supportedCurrencies && val.supportedCurrencies.length > 0 ? val.supportedCurrencies : null,
remark: val.remark?.trim() || null,
ext: val.ext?.trim() || null,
issuerName: val.issuerName?.trim() || null,
accountNumber: val.accountNumber?.trim() || null,
platform: val.platform?.trim() || null,
accountId: val.accountId?.trim() || null,
location: val.location?.trim() || null,
isActive: true,
})
.returning();
revalidatePath("/accounts");
revalidatePath("/channels");
revalidatePath("/");
return { success: true, data: newAccount };
} catch (err) {
console.error("createAccountAction error:", err);
return { success: false, error: err instanceof Error ? err.message : "创建账户失败" };
}
}
export async function updateAccountAction(
id: string,
data: AccountInput
): Promise<{ success: boolean; data?: Account; error?: string }> {
try {
const userId = await requireUser();
const parsed = accountSchema.safeParse(data);
if (!parsed.success) {
return { success: false, error: parsed.error.issues[0]?.message || "参数错误" };
}
const val = parsed.data;
const [updated] = await db
.update(accounts)
.set({
name: val.name.trim(),
accountType: val.accountType,
balanceType: val.balanceType,
primaryCurrency: val.primaryCurrency ? val.primaryCurrency.toUpperCase() : null,
supportedCurrencies: val.supportedCurrencies && val.supportedCurrencies.length > 0 ? val.supportedCurrencies : null,
remark: val.remark?.trim() || null,
ext: val.ext?.trim() || null,
issuerName: val.issuerName?.trim() || null,
accountNumber: val.accountNumber?.trim() || null,
platform: val.platform?.trim() || null,
accountId: val.accountId?.trim() || null,
location: val.location?.trim() || null,
updatedAt: new Date(),
})
.where(and(eq(accounts.id, id), eq(accounts.userId, userId), isNull(accounts.deletedAt)))
.returning();
if (!updated) {
return { success: false, error: "未找到该账户或无权修改" };
}
revalidatePath("/accounts");
revalidatePath("/channels");
revalidatePath("/");
return { success: true, data: updated };
} catch (err) {
console.error("updateAccountAction error:", err);
return { success: false, error: err instanceof Error ? err.message : "更新账户失败" };
}
}
export async function toggleAccountActiveAction(
id: string,
isActive: boolean
): Promise<{ success: boolean; error?: string }> {
try {
const userId = await requireUser();
await db
.update(accounts)
.set({
isActive,
updatedAt: new Date(),
})
.where(and(eq(accounts.id, id), eq(accounts.userId, userId), isNull(accounts.deletedAt)));
revalidatePath("/accounts");
return { success: true };
} catch (err) {
console.error("toggleAccountActiveAction error:", err);
return { success: false, error: err instanceof Error ? err.message : "状态切换失败" };
}
}
export async function deleteAccountAction(
id: string
): Promise<{ success: boolean; error?: string }> {
try {
const userId = await requireUser();
// 软删除
const [deleted] = await db
.update(accounts)
.set({
deletedAt: new Date(),
updatedAt: new Date(),
})
.where(and(eq(accounts.id, id), eq(accounts.userId, userId), isNull(accounts.deletedAt)))
.returning();
if (!deleted) {
return { success: false, error: "账户不存在或已删除" };
}
revalidatePath("/accounts");
revalidatePath("/channels");
revalidatePath("/");
return { success: true };
} catch (err) {
console.error("deleteAccountAction error:", err);
return { success: false, error: err instanceof Error ? err.message : "删除账户失败" };
}
}
+85
View File
@@ -0,0 +1,85 @@
"use server";
import { z } from "zod";
import { eq } from "drizzle-orm";
import { db } from "@/lib/db";
import { users } from "@/lib/db/schema";
import { hashPassword } from "@/lib/auth/password";
const registerSchema = z
.object({
name: z.string().min(1, "请输入姓名/昵称").max(100),
email: z.string().email("请输入有效的邮箱地址").max(255),
password: z.string().min(8, "密码长度至少需要 8 个字符"),
confirmPassword: z.string().min(1, "请确认密码"),
})
.refine((data: { password: string; confirmPassword: string }) => data.password === data.confirmPassword, {
message: "两次输入的密码不一致",
path: ["confirmPassword"],
});
export type RegisterState = {
success?: boolean;
error?: string;
fieldErrors?: Record<string, string[]>;
};
export async function registerAction(
prevState: RegisterState | null,
formData: FormData
): Promise<RegisterState> {
const rawData = {
name: formData.get("name"),
email: formData.get("email"),
password: formData.get("password"),
confirmPassword: formData.get("confirmPassword"),
};
const parsed = registerSchema.safeParse(rawData);
if (!parsed.success) {
return {
success: false,
fieldErrors: parsed.error.flatten().fieldErrors,
};
}
const { name, email, password } = parsed.data;
const normalizedEmail = email.toLowerCase().trim();
try {
// 检查邮箱是否已被注册
const [existing] = await db
.select()
.from(users)
.where(eq(users.email, normalizedEmail))
.limit(1);
if (existing) {
return {
success: false,
error: "该邮箱已被注册,请直接登录",
};
}
// 使用 Argon2 哈希密码
const passwordHash = await hashPassword(password);
// 插入新用户
await db.insert(users).values({
name: name.trim(),
email: normalizedEmail,
passwordHash,
isActive: true,
});
return {
success: true,
};
} catch (err) {
console.error("Registration error:", err);
return {
success: false,
error: "注册失败,请稍后重试",
};
}
}
+278
View File
@@ -0,0 +1,278 @@
"use server";
import { z } from "zod";
import { and, desc, eq, inArray, isNull } from "drizzle-orm";
import { revalidatePath } from "next/cache";
import { auth } from "@/lib/auth";
import { db } from "@/lib/db";
import {
accounts,
channels,
type Channel,
} from "@/lib/db/schema";
async function requireUser() {
const session = await auth();
if (!session?.user?.id) {
throw new Error("请先登录");
}
return session.user.id;
}
const channelSchema = z.object({
channelType: z.enum(["PAYMENT_CARD", "E_WALLET", "CASH", "TRANSFER"] as const),
refAccounts: z.array(z.string().uuid("无效的账户标识")).min(1, "请至少关联一个资金账户"),
desc: z.string().max(500).optional().nullable(),
ext: z.string().optional().nullable(),
// 支付卡特定字段
region: z.string().max(10).optional().nullable(),
issuerName: z.string().max(100).optional().nullable(),
cardType: z.enum(["CREDIT", "DEBIT"] as const).optional().nullable(),
cardNumberFull: z.string().max(100).optional().nullable(),
cardNumberSuffix: z.string().max(10).optional().nullable(),
cardBrand: z.string().max(32).optional().nullable(),
// 电子钱包特定字段
platform: z.string().max(64).optional().nullable(),
platformAccountId: z.string().max(100).optional().nullable(),
subChannel: z.string().max(64).optional().nullable(),
subChannelType: z.enum(["CREDIT", "DEBIT"] as const).optional().nullable(),
});
export type ChannelInput = z.infer<typeof channelSchema>;
export type ChannelWithAccountNames = Channel & {
linkedAccounts: { id: string; name: string }[];
};
export async function getChannelsAction(): Promise<{
success: boolean;
data?: ChannelWithAccountNames[];
error?: string;
}> {
try {
const userId = await requireUser();
const userChannels = await db
.select()
.from(channels)
.where(and(eq(channels.userId, userId), isNull(channels.deletedAt)))
.orderBy(desc(channels.createdAt));
const userAccounts = await db
.select({ id: accounts.id, name: accounts.name })
.from(accounts)
.where(and(eq(accounts.userId, userId), isNull(accounts.deletedAt)));
const accountMap = new Map(userAccounts.map((a) => [a.id, a.name]));
const result: ChannelWithAccountNames[] = userChannels.map((ch) => {
const linked: { id: string; name: string }[] = [];
if (Array.isArray(ch.refAccounts)) {
for (const accId of ch.refAccounts) {
const name = accountMap.get(accId);
if (name) {
linked.push({ id: accId, name });
}
}
}
return {
...ch,
linkedAccounts: linked,
};
});
return { success: true, data: result };
} catch (err) {
console.error("getChannelsAction error:", err);
return { success: false, error: err instanceof Error ? err.message : "获取渠道失败" };
}
}
export async function createChannelAction(
data: ChannelInput
): Promise<{ success: boolean; data?: Channel; error?: string }> {
try {
const userId = await requireUser();
const parsed = channelSchema.safeParse(data);
if (!parsed.success) {
return { success: false, error: parsed.error.issues[0]?.message || "参数错误" };
}
const val = parsed.data;
// 校验关联的所有账户必须属于当前登录用户
const userOwnedAccounts = await db
.select({ id: accounts.id })
.from(accounts)
.where(
and(
eq(accounts.userId, userId),
inArray(accounts.id, val.refAccounts),
isNull(accounts.deletedAt)
)
);
if (userOwnedAccounts.length !== val.refAccounts.length) {
return { success: false, error: "关联的部分账户不存在或已被删除" };
}
// 自动提取或补全卡号后4位
let suffix = val.cardNumberSuffix?.trim() || null;
if (!suffix && val.cardNumberFull && val.cardNumberFull.length >= 4) {
suffix = val.cardNumberFull.slice(-4);
}
const [newChannel] = await db
.insert(channels)
.values({
userId,
channelType: val.channelType,
refAccounts: val.refAccounts,
isActive: true,
desc: val.desc?.trim() || null,
ext: val.ext?.trim() || null,
region: val.region ? val.region.toUpperCase().trim() : null,
issuerName: val.issuerName?.trim() || null,
cardType: val.cardType || null,
cardNumberFull: val.cardNumberFull?.trim() || null,
cardNumberSuffix: suffix,
cardBrand: val.cardBrand?.trim() || null,
platform: val.platform?.trim() || null,
platformAccountId: val.platformAccountId?.trim() || null,
subChannel: val.subChannel?.trim() || null,
subChannelType: val.subChannelType || null,
})
.returning();
revalidatePath("/channels");
revalidatePath("/accounts");
revalidatePath("/");
return { success: true, data: newChannel };
} catch (err) {
console.error("createChannelAction error:", err);
return { success: false, error: err instanceof Error ? err.message : "创建渠道失败" };
}
}
export async function updateChannelAction(
id: string,
data: ChannelInput
): Promise<{ success: boolean; data?: Channel; error?: string }> {
try {
const userId = await requireUser();
const parsed = channelSchema.safeParse(data);
if (!parsed.success) {
return { success: false, error: parsed.error.issues[0]?.message || "参数错误" };
}
const val = parsed.data;
const userOwnedAccounts = await db
.select({ id: accounts.id })
.from(accounts)
.where(
and(
eq(accounts.userId, userId),
inArray(accounts.id, val.refAccounts),
isNull(accounts.deletedAt)
)
);
if (userOwnedAccounts.length !== val.refAccounts.length) {
return { success: false, error: "关联的部分账户不存在或已被删除" };
}
let suffix = val.cardNumberSuffix?.trim() || null;
if (!suffix && val.cardNumberFull && val.cardNumberFull.length >= 4) {
suffix = val.cardNumberFull.slice(-4);
}
const [updated] = await db
.update(channels)
.set({
channelType: val.channelType,
refAccounts: val.refAccounts,
desc: val.desc?.trim() || null,
ext: val.ext?.trim() || null,
region: val.region ? val.region.toUpperCase().trim() : null,
issuerName: val.issuerName?.trim() || null,
cardType: val.cardType || null,
cardNumberFull: val.cardNumberFull?.trim() || null,
cardNumberSuffix: suffix,
cardBrand: val.cardBrand?.trim() || null,
platform: val.platform?.trim() || null,
platformAccountId: val.platformAccountId?.trim() || null,
subChannel: val.subChannel?.trim() || null,
subChannelType: val.subChannelType || null,
updatedAt: new Date(),
})
.where(and(eq(channels.id, id), eq(channels.userId, userId), isNull(channels.deletedAt)))
.returning();
if (!updated) {
return { success: false, error: "渠道不存在或无权修改" };
}
revalidatePath("/channels");
revalidatePath("/accounts");
revalidatePath("/");
return { success: true, data: updated };
} catch (err) {
console.error("updateChannelAction error:", err);
return { success: false, error: err instanceof Error ? err.message : "更新渠道失败" };
}
}
export async function toggleChannelActiveAction(
id: string,
isActive: boolean
): Promise<{ success: boolean; error?: string }> {
try {
const userId = await requireUser();
await db
.update(channels)
.set({
isActive,
updatedAt: new Date(),
})
.where(and(eq(channels.id, id), eq(channels.userId, userId), isNull(channels.deletedAt)));
revalidatePath("/channels");
revalidatePath("/accounts");
revalidatePath("/");
return { success: true };
} catch (err) {
console.error("toggleChannelActiveAction error:", err);
return { success: false, error: err instanceof Error ? err.message : "状态切换失败" };
}
}
export async function deleteChannelAction(
id: string
): Promise<{ success: boolean; error?: string }> {
try {
const userId = await requireUser();
const [deleted] = await db
.update(channels)
.set({
deletedAt: new Date(),
updatedAt: new Date(),
})
.where(and(eq(channels.id, id), eq(channels.userId, userId), isNull(channels.deletedAt)))
.returning();
if (!deleted) {
return { success: false, error: "渠道不存在或已删除" };
}
revalidatePath("/channels");
revalidatePath("/accounts");
revalidatePath("/");
return { success: true };
} catch (err) {
console.error("deleteChannelAction error:", err);
return { success: false, error: err instanceof Error ? err.message : "删除渠道失败" };
}
}
+43
View File
@@ -0,0 +1,43 @@
import type { NextAuthConfig } from "next-auth";
export const authConfig = {
pages: {
signIn: "/login",
error: "/login",
},
callbacks: {
authorized({ auth, request: { nextUrl } }) {
const isLoggedIn = !!auth?.user;
const pathname = nextUrl.pathname;
const publicPaths = ["/login", "/register", "/api/auth"];
const isPublic = publicPaths.some(
(path) => pathname === path || pathname.startsWith("/api/auth/")
);
// 已登录用户在登录/注册页时重定向到首页
if (isLoggedIn && (pathname === "/login" || pathname === "/register")) {
return Response.redirect(new URL("/", nextUrl));
}
// 访问受保护页面必须已登录
if (!isLoggedIn && !isPublic) {
return false;
}
return true;
},
jwt({ token, user }) {
if (user?.id) {
token.id = user.id;
}
return token;
},
session({ session, token }) {
if (session.user && token.id) {
session.user.id = token.id as string;
}
return session;
},
},
providers: [],
} satisfies NextAuthConfig;
+147
View File
@@ -0,0 +1,147 @@
import NextAuth from "next-auth";
import Credentials from "next-auth/providers/credentials";
import { eq } from "drizzle-orm";
import { db } from "@/lib/db";
import { users, userAccounts } from "@/lib/db/schema";
import { verifyPassword } from "./password";
import { authConfig } from "./config";
import type { Provider } from "next-auth/providers";
// 动态构建 Providers 列表
const providers: Provider[] = [
Credentials({
name: "Credentials",
credentials: {
email: { label: "Email", type: "email" },
password: { label: "Password", type: "password" },
},
async authorize(credentials) {
if (!credentials?.email || !credentials?.password) {
return null;
}
const email = String(credentials.email).toLowerCase().trim();
const password = String(credentials.password);
const [user] = await db
.select()
.from(users)
.where(eq(users.email, email))
.limit(1);
if (!user || !user.passwordHash || !user.isActive) {
return null;
}
const isValid = await verifyPassword(password, user.passwordHash);
if (!isValid) {
return null;
}
return {
id: user.id,
name: user.name,
email: user.email,
image: user.avatar,
};
},
}),
];
// 如果配置了 OIDC,且 AUTH_OIDC_ENABLED 为 true,则动态注册通用 OIDC 提供商
const isOidcEnabled =
process.env.AUTH_OIDC_ENABLED === "true" &&
Boolean(process.env.AUTH_OIDC_ISSUER) &&
Boolean(process.env.AUTH_OIDC_CLIENT_ID);
if (isOidcEnabled) {
providers.push({
id: "oidc",
name: process.env.AUTH_OIDC_NAME || "SSO 统一身份认证",
// Laysense currently returns nonce: "" even when no nonce was requested,
// causing Auth.js to reject the ID Token. Keep the standard OIDC flow here
// and leave it disabled in .env.local until the identity provider is fixed.
type: "oidc",
issuer: process.env.AUTH_OIDC_ISSUER!.replace(/\/$/, ""),
clientId: process.env.AUTH_OIDC_CLIENT_ID,
clientSecret: process.env.AUTH_OIDC_CLIENT_SECRET,
// Laysense requires an OAuth state parameter to correlate the callback.
checks: ["state"],
authorization: {
params: {
scope: process.env.AUTH_OIDC_SCOPES || "openid profile email",
},
},
});
}
export const { handlers, signIn, signOut, auth } = NextAuth({
...authConfig,
providers,
session: {
strategy: "jwt",
maxAge: 30 * 24 * 60 * 60, // 30 天
},
callbacks: {
...authConfig.callbacks,
async signIn({ user, account }) {
if (!account || account.type === "credentials") {
return true;
}
// 处理 OIDC / OAuth 登录与本地用户的关联或新建
const email = user.email?.toLowerCase().trim();
if (!email) {
return false;
}
// 1. 查询用户是否已存在
let [existingUser] = await db
.select()
.from(users)
.where(eq(users.email, email))
.limit(1);
if (!existingUser) {
// 创建新用户
const [newUser] = await db
.insert(users)
.values({
name: user.name || email.split("@")[0],
email: email,
avatar: user.image || null,
isActive: true,
})
.returning();
existingUser = newUser;
}
user.id = existingUser.id;
// 2. 查询是否已记录该 provider 的账户绑定
const [existingAccount] = await db
.select()
.from(userAccounts)
.where(
eq(userAccounts.providerAccountId, account.providerAccountId)
)
.limit(1);
if (!existingAccount) {
await db.insert(userAccounts).values({
userId: existingUser.id,
provider: account.provider,
providerAccountId: account.providerAccountId,
refreshToken: account.refresh_token,
accessToken: account.access_token,
expiresAt: account.expires_at ? new Date(account.expires_at * 1000) : null,
tokenType: account.token_type,
scope: account.scope,
idToken: account.id_token,
});
}
return true;
},
},
});
+21
View File
@@ -0,0 +1,21 @@
import { hash, verify } from "@node-rs/argon2";
// 遵循 OWASP 密码哈希安全推荐配置
const ARGON2_OPTIONS = {
memoryCost: 19456, // 19 MiB
timeCost: 2,
outputLen: 32,
parallelism: 1,
};
export async function hashPassword(password: string): Promise<string> {
return await hash(password, ARGON2_OPTIONS);
}
export async function verifyPassword(password: string, passwordHash: string): Promise<boolean> {
try {
return await verify(passwordHash, password);
} catch {
return false;
}
}
+15
View File
@@ -0,0 +1,15 @@
import { drizzle } from "drizzle-orm/postgres-js";
import postgres from "postgres";
import * as schema from "./schema";
const connectionString = process.env.DATABASE_URL || "postgresql://postgres:Aa110011@localhost:5432/fluxent";
// 在 Next.js 开发环境下避免热重载创建重复连接池
const globalForDb = globalThis as unknown as {
conn: postgres.Sql | undefined;
};
const client = globalForDb.conn ?? postgres(connectionString, { max: 10 });
if (process.env.NODE_ENV !== "production") globalForDb.conn = client;
export const db = drizzle(client, { schema });
+52
View File
@@ -0,0 +1,52 @@
import * as dotenv from "dotenv"
import postgres from "postgres"
import { normalizeCardBrand } from "../payment/card-brand"
dotenv.config({ path: ".env.local" })
const sql = postgres(process.env.DATABASE_URL!)
async function main() {
const rows = await sql<{ id: string; card_brand: string | null }[]>`
SELECT id, card_brand
FROM channels
WHERE card_brand IS NOT NULL
`
const unsupported = rows.filter((row) => !normalizeCardBrand(row.card_brand))
if (unsupported.length > 0) {
throw new Error(
`Unsupported card brands: ${unsupported.map((row) => row.card_brand).join(", ")}`
)
}
let updated = 0
for (const row of rows) {
const cardBrand = normalizeCardBrand(row.card_brand)
if (cardBrand && cardBrand !== row.card_brand) {
await sql`
UPDATE channels
SET card_brand = ${cardBrand}, updated_at = CURRENT_TIMESTAMP
WHERE id = ${row.id}
`
updated += 1
}
}
const brands = await sql<{ card_brand: string }[]>`
SELECT DISTINCT card_brand
FROM channels
WHERE card_brand IS NOT NULL
ORDER BY card_brand
`
console.log(`Normalized ${updated} channel card brands.`)
console.log("Stored card brands:", brands.map((row) => row.card_brand))
}
main()
.catch((error) => {
console.error(error)
process.exitCode = 1
})
.finally(() => sql.end())
+22
View File
@@ -0,0 +1,22 @@
import postgres from "postgres";
import * as dotenv from "dotenv";
dotenv.config({ path: ".env.local" });
const sql = postgres(process.env.DATABASE_URL!);
async function resetAndInit() {
console.log("Dropping old tables...");
await sql`DROP TABLE IF EXISTS transactions CASCADE`;
await sql`DROP TABLE IF EXISTS channels CASCADE`;
await sql`DROP TABLE IF EXISTS accounts CASCADE`;
await sql`DROP TABLE IF EXISTS user_accounts CASCADE`;
await sql`DROP TABLE IF EXISTS users CASCADE`;
console.log("Old tables dropped.");
await sql.end();
}
resetAndInit().catch((err) => {
console.error(err);
process.exit(1);
});
+244
View File
@@ -0,0 +1,244 @@
import {
pgTable,
text,
varchar,
uuid,
boolean,
timestamp,
jsonb,
uniqueIndex,
index,
} from "drizzle-orm/pg-core";
import { sql } from "drizzle-orm";
// ---------------------------------------------------------------------------
// 基础时间戳辅助
// ---------------------------------------------------------------------------
export const timestamps = {
createdAt: timestamp("created_at", { withTimezone: true })
.default(sql`CURRENT_TIMESTAMP`)
.notNull(),
updatedAt: timestamp("updated_at", { withTimezone: true })
.default(sql`CURRENT_TIMESTAMP`)
.notNull(),
deletedAt: timestamp("deleted_at", { withTimezone: true }),
};
// ---------------------------------------------------------------------------
// 1. 用户与认证表
// ---------------------------------------------------------------------------
/**
* OIDC
*/
export const users = pgTable(
"users",
{
id: uuid("id").defaultRandom().primaryKey(),
name: varchar("name", { length: 100 }).notNull(),
email: varchar("email", { length: 255 }).notNull(),
passwordHash: text("password_hash"), // OIDC 注册的用户可无密码
avatar: text("avatar"),
isActive: boolean("is_active").default(true).notNull(),
...timestamps,
},
(t) => [
uniqueIndex("users_email_unique").on(t.email),
]
);
export type User = typeof users.$inferSelect;
export type NewUser = typeof users.$inferInsert;
/**
* ( OIDC / OAuth )
*/
export const userAccounts = pgTable(
"user_accounts",
{
id: uuid("id").defaultRandom().primaryKey(),
userId: uuid("user_id")
.references(() => users.id, { onDelete: "cascade" })
.notNull(),
provider: varchar("provider", { length: 64 }).notNull(), // 如 'oidc'
providerAccountId: varchar("provider_account_id", { length: 255 }).notNull(),
refreshToken: text("refresh_token"),
accessToken: text("access_token"),
expiresAt: timestamp("expires_at", { withTimezone: true }),
tokenType: varchar("token_type", { length: 64 }),
scope: text("scope"),
idToken: text("id_token"),
...timestamps,
},
(t) => [
uniqueIndex("user_accounts_provider_account_id_unique").on(
t.provider,
t.providerAccountId
),
index("user_accounts_user_id_idx").on(t.userId),
]
);
// ---------------------------------------------------------------------------
// 2. 资金账户表 (Accounts)
// ---------------------------------------------------------------------------
export type AccountType = "BANK" | "E_WALLET" | "CASH";
export type BalanceType = "ASSET" | "LIABILITY" | "EQUITY";
export const accounts = pgTable(
"accounts",
{
id: uuid("id").defaultRandom().primaryKey(),
userId: uuid("user_id")
.references(() => users.id, { onDelete: "cascade" })
.notNull(),
name: varchar("name", { length: 150 }).notNull(),
accountType: varchar("account_type", { length: 32 }).$type<AccountType>().notNull(),
balanceType: varchar("balance_type", { length: 32 })
.$type<BalanceType>()
.default("ASSET")
.notNull(),
primaryCurrency: varchar("primary_currency", { length: 10 }),
supportedCurrencies: jsonb("supported_currencies").$type<string[] | null>(),
isActive: boolean("is_active").default(true).notNull(),
remark: text("remark"),
ext: text("ext"),
// 银行账户特定字段
issuerName: varchar("issuer_name", { length: 100 }),
accountNumber: varchar("account_number", { length: 100 }),
subAccounts: jsonb("sub_accounts").$type<string[]>(),
// 电子钱包特定字段
platform: varchar("platform", { length: 64 }),
accountId: varchar("account_id", { length: 100 }),
// 现金账户特定字段
location: varchar("location", { length: 150 }),
...timestamps,
},
(t) => [
index("accounts_user_id_idx").on(t.userId),
]
);
export type Account = typeof accounts.$inferSelect;
export type NewAccount = typeof accounts.$inferInsert;
// ---------------------------------------------------------------------------
// 3. 渠道表 (Channels)
// ---------------------------------------------------------------------------
export type ChannelType = "PAYMENT_CARD" | "E_WALLET" | "CASH" | "TRANSFER";
export type PaymentInstrumentType = "CREDIT" | "DEBIT";
export const channels = pgTable(
"channels",
{
id: uuid("id").defaultRandom().primaryKey(),
userId: uuid("user_id")
.references(() => users.id, { onDelete: "cascade" })
.notNull(),
channelType: varchar("channel_type", { length: 32 }).$type<ChannelType>().notNull(),
refAccounts: jsonb("ref_accounts").$type<string[]>().notNull(), // 关联的账户 UUID 列表
isActive: boolean("is_active").default(true).notNull(),
desc: text("desc"),
ext: text("ext"),
// 支付卡渠道字段
region: varchar("region", { length: 10 }), // 发卡地如 HK, CN
issuerName: varchar("issuer_name", { length: 100 }),
cardType: varchar("card_type", { length: 32 }).$type<PaymentInstrumentType>(),
cardNumberFull: varchar("card_number_full", { length: 100 }),
cardNumberSuffix: varchar("card_number_suffix", { length: 10 }),
cardBrand: varchar("card_brand", { length: 32 }),
// 电子钱包渠道字段
platform: varchar("platform", { length: 64 }),
platformAccountId: varchar("platform_account_id", { length: 100 }),
subChannel: varchar("sub_channel", { length: 64 }),
subChannelType: varchar("sub_channel_type", { length: 32 }).$type<PaymentInstrumentType>(),
...timestamps,
},
(t) => [
index("channels_user_id_idx").on(t.userId),
]
);
export type Channel = typeof channels.$inferSelect;
export type NewChannel = typeof channels.$inferInsert;
// ---------------------------------------------------------------------------
// 4. 交易记账表 (Transactions)
// ---------------------------------------------------------------------------
export type TransactionStatus = "PENDING" | "COMPLETED" | "FAILED" | "REFUNDED";
export type DcFlag = "DEBIT" | "CREDIT";
export interface FxRateItem {
fromCcy: string;
toCcy: string;
rate: string;
}
export const transactions = pgTable(
"transactions",
{
id: uuid("id").defaultRandom().primaryKey(),
userId: uuid("user_id")
.references(() => users.id, { onDelete: "cascade" })
.notNull(),
version: varchar("version", { length: 16 }).default("0").notNull(),
refTransactions: jsonb("ref_transactions").$type<string[]>(), // 关联交易 UUID 列表
txnDate: timestamp("txn_date", { withTimezone: true }).notNull(),
clearingDate: timestamp("clearing_date", { withTimezone: true }),
postingDate: timestamp("posting_date", { withTimezone: true }),
// 金额与币种
txnAmt: varchar("txn_amt", { length: 32 }).notNull(),
txnCcy: varchar("txn_ccy", { length: 10 }).notNull(),
postingAmt: varchar("posting_amt", { length: 32 }),
postingCcy: varchar("posting_ccy", { length: 10 }),
commAmt: varchar("comm_amt", { length: 32 }),
commCcy: varchar("comm_ccy", { length: 10 }),
surchargeAmt: varchar("surcharge_amt", { length: 32 }),
surchargeCcy: varchar("surcharge_ccy", { length: 10 }),
discAmt: varchar("disc_amt", { length: 32 }),
discCcy: varchar("disc_ccy", { length: 10 }),
fxRates: jsonb("fx_rates").$type<FxRateItem[]>(),
dcFlag: varchar("dc_flag", { length: 16 }).$type<DcFlag>().notNull(),
refChannels: jsonb("ref_channels").$type<string[]>().notNull(), // 渠道 UUID 列表
cp: varchar("cp", { length: 255 }), // 对手方
acqInst: varchar("acq_inst", { length: 150 }),
clearingNetwork: varchar("clearing_network", { length: 100 }),
txnSts: varchar("txn_sts", { length: 32 })
.$type<TransactionStatus>()
.default("COMPLETED")
.notNull(),
description: text("description"),
memo: text("memo"),
ext: text("ext"),
rawDescription: text("raw_description"),
rawData: jsonb("raw_data"),
// 场景类型
txnScene: varchar("txn_scene", { length: 64 }).notNull(), // MISC_IN, PAYMENT, ECOM_PAYMENT, POS_PAYMENT, ATM, TRANSFER 等
merchantName: varchar("merchant_name", { length: 255 }),
orderId: varchar("order_id", { length: 150 }),
geo: varchar("geo", { length: 64 }),
...timestamps,
},
(t) => [
index("transactions_user_id_idx").on(t.userId),
index("transactions_txn_date_idx").on(t.txnDate),
]
);
export type Transaction = typeof transactions.$inferSelect;
export type NewTransaction = typeof transactions.$inferInsert;
+105
View File
@@ -0,0 +1,105 @@
export const CARD_BRANDS = [
"visa",
"mastercard",
"unionpay",
"amex",
"diners",
"discover",
"jcb",
] as const
export type CardBrand = (typeof CARD_BRANDS)[number]
export const CARD_BRAND_LABELS: Record<CardBrand, string> = {
visa: "Visa",
mastercard: "Mastercard",
unionpay: "UnionPay",
amex: "American Express",
diners: "Diners Club",
discover: "Discover",
jcb: "JCB",
}
export const CARD_BRAND_LOGOS: Record<CardBrand, string> = {
visa: "/payment-logos/visa.svg",
mastercard: "/payment-logos/mastercard.svg",
unionpay: "/payment-logos/unionpay.svg",
amex: "/payment-logos/amex.svg",
diners: "/payment-logos/diners.svg",
discover: "/payment-logos/discover.svg",
jcb: "/payment-logos/jcb.svg",
}
const CARD_BRAND_ALIASES: Record<string, CardBrand> = {
visa: "visa",
mastercard: "mastercard",
unionpay: "unionpay",
amex: "amex",
americanexpress: "amex",
diners: "diners",
dinersclub: "diners",
discover: "discover",
jcb: "jcb",
}
export function normalizeCardBrand(
input: string | null | undefined
): CardBrand | null {
if (typeof input !== "string") {
return null
}
const key = input
.trim()
.toLowerCase()
.replace(/[\s_-]+/g, "")
return CARD_BRAND_ALIASES[key] ?? null
}
export function getCardBrandLogoUrl(
input: string | null | undefined
): string | null {
const brand = normalizeCardBrand(input)
return brand ? CARD_BRAND_LOGOS[brand] : null
}
type CardNumberResult = {
card: { type?: string } | null
isPotentiallyValid: boolean
}
type CardValidatorApi = {
number?: (value: string) => CardNumberResult
}
type CardValidatorModule = CardValidatorApi & {
default?: CardValidatorApi
}
export async function detectCardBrand(
cardNumber: string
): Promise<CardBrand | null> {
const digits = cardNumber.replace(/\D/g, "")
if (!digits) {
return null
}
try {
const loadedValidator =
(await import("card-validator")) as unknown as CardValidatorModule
const validator = loadedValidator.default ?? loadedValidator
if (typeof validator.number !== "function") {
return null
}
const result = validator.number(digits)
if (!result.card || !result.isPotentiallyValid) {
return null
}
return result.card.type ? normalizeCardBrand(result.card.type) : null
} catch {
return null
}
}
+12 -1
View File
@@ -13,26 +13,37 @@
},
"dependencies": {
"@base-ui/react": "^1.7.0",
"@hookform/resolvers": "^5.9.1",
"@node-rs/argon2": "^2.2.0",
"card-validator": "^10.0.4",
"class-variance-authority": "^0.7.1",
"cn": "^0.2.4",
"dotenv": "^17.4.2",
"drizzle-orm": "^0.45.2",
"lucide-react": "^1.39.0",
"next": "16.2.6",
"next-auth": "5.0.0-beta.32",
"next-themes": "^0.4.6",
"postgres": "^3.4.9",
"react": "19.2.4",
"react-dom": "19.2.4",
"react-hook-form": "^7.87.0",
"shadcn": "^4.20.1",
"tw-animate-css": "^1.4.0"
"tw-animate-css": "^1.4.0",
"zod": "^4.5.4"
},
"devDependencies": {
"@tailwindcss/postcss": "^4",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"drizzle-kit": "^0.31.10",
"eslint": "^9",
"eslint-config-next": "16.2.6",
"prettier": "^3.8.3",
"prettier-plugin-tailwindcss": "^0.8.0",
"tailwindcss": "^4",
"tsx": "^4.23.13",
"typescript": "^5"
}
}
+1288
View File
File diff suppressed because it is too large Load Diff
+1
View File
@@ -4,5 +4,6 @@ allowBuilds:
sharp: true
unrs-resolver: true
msw: false
esbuild: true
registry: https://registry.npmjs.org/
+12
View File
@@ -0,0 +1,12 @@
import NextAuth from "next-auth";
import { authConfig } from "@/lib/auth/config";
const { auth } = NextAuth(authConfig);
export const proxy = auth;
export default auth;
export const config = {
matcher: ["/((?!api|_next/static|_next/image|favicon.ico).*)"],
};
File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 9.1 KiB

+54
View File
@@ -0,0 +1,54 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="120px" height="90px" viewBox="0 0 120 90" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>creditcard_diners</title>
<g id="LOGO-+-SDK-+-payment-icon" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="payment" transform="translate(-918.000000, -155.000000)">
<g id="creditcard_diners" transform="translate(918.000000, 155.000000)">
<g id="payment-4:3bg" transform="translate(-20.000000, -15.000000)"></g>
<g id="Diners-Club" transform="translate(10.000000, 9.000000)" fill-rule="nonzero">
<path d="M99.429228,65.6661692 C99.4096822,67.4096715 98.6925551,69.0739908 97.4357384,70.2926895 C96.1789217,71.5113881 94.4854632,72.1845437 92.7282162,72.1644049 L7.26807754,72.1644049 C5.51083053,72.1845437 3.81737197,71.5113881 2.56055529,70.2926895 C1.3037386,69.0739908 0.586611461,67.4096715 0.567065713,65.6661692 L0.567065713,7.0721847 C0.586611461,5.32868243 1.3037386,3.66436311 2.56055529,2.44566448 C3.81737197,1.22696585 5.51083053,0.553810257 7.26807754,0.573949046 L92.7282162,0.573949046 C94.4854632,0.553810257 96.1789217,1.22696585 97.4357384,2.44566448 C98.6925551,3.66436311 99.4096822,5.32868243 99.429228,7.0721847 L99.429228,65.6661692 Z" id="路径" fill="#FFFFFF"></path>
<path d="M1.14154405,7.0721847 L1.14154405,65.6661692 C1.18832343,68.9807444 3.927181,71.6340446 7.26807754,71.6016981 L92.7282162,71.6016981 C96.0719925,71.6381262 98.8153848,68.9836446 98.8621623,65.6661692 L98.8621623,7.0721847 C98.817412,3.7538451 96.0728124,1.09819537 92.7282162,1.13661338 L7.26807754,1.13661338 C3.9234813,1.09819537 1.17888167,3.7538451 1.13413143,7.0721847 M7.26807754,72.7269253 C3.29714027,72.7653705 0.044794507,69.605956 0,65.6661692 L0,7.0721847 C0.0176043709,5.17785613 0.793499194,3.36817562 2.1567636,2.04179537 C3.520028,0.715415118 5.3588211,-0.0188499324 7.26807754,0.000367955376 L92.7282162,0.000367955376 C96.69977,-0.0380577188 99.9531776,3.12077745 100,7.06115282 L100,65.6661692 C99.9531776,69.6065446 96.69977,72.7653798 92.7282162,72.7269253 L7.26807754,72.7269253 Z" id="形状" fill="#0E72B2"></path>
<path d="M11.4191468,52.266114 C11.4191468,53.6414216 12.4124384,53.8032224 13.2908343,53.8032224 C17.1824617,53.8032224 18.457433,50.8944838 18.457433,48.2321238 C18.457433,44.8931418 16.2966532,42.484515 12.823839,42.484515 C12.0825766,42.484515 11.7453022,42.5359971 11.4191468,42.5580609 L11.4191468,52.266114 Z M9.65864868,43.8929182 C9.65864868,42.2896185 8.80990327,42.3925827 8.00192728,42.3778735 L8.00192728,41.9218892 C8.70612653,41.9549848 9.42144472,41.9549848 10.125644,41.9549848 C10.8298432,41.9549848 11.90838,41.9218892 13.2389459,41.9218892 C17.8977799,41.9218892 20.4366035,45.0071379 20.4366035,48.1732871 C20.4366035,49.9420649 19.3951299,54.387912 13.0239798,54.387912 C12.1048145,54.387912 11.2597754,54.3511391 10.4295615,54.3511391 C9.59934769,54.3511391 8.82472851,54.3511391 8.01675253,54.387912 L8.01675253,53.9098639 C9.09528928,53.8032224 9.62158556,53.7664495 9.67347393,52.5529429 L9.65864868,43.8929182 Z" id="形状" fill="#231815"></path>
<path d="M22.9939587,43.4663522 C22.5067871,43.4663522 22.1118565,43.0745137 22.1118565,42.5911565 C22.1118565,42.1077993 22.5067871,41.7159608 22.9939587,41.7159608 C23.4739841,41.7158557 23.8659764,42.0966082 23.8760693,42.57277 C23.8781572,43.059994 23.484965,43.4583033 22.9939587,43.4663522 M21.0666766,53.9098639 L21.4076572,53.9098639 C21.9117157,53.9098639 22.2712279,53.9098639 22.2712279,53.3214971 L22.2712279,48.4821797 C22.2712279,47.6989163 22.0043734,47.5885975 21.3372373,47.2319001 L21.3372373,46.9487485 C22.1822764,46.6950153 23.1903932,46.3603817 23.2608132,46.3052223 C23.3595337,46.2467505 23.4720089,46.2150475 23.5869686,46.21329 C23.6759201,46.21329 23.7129832,46.3236088 23.7129832,46.4670232 L23.7129832,53.3214971 C23.7129832,53.9098639 24.1095586,53.9098639 24.613617,53.9098639 L24.9175346,53.9098639 L24.9175346,54.3768801 C24.3059931,54.3768801 23.6759201,54.3401072 23.0273155,54.3401072 C22.3787109,54.3401072 21.7338127,54.3401072 21.0666766,54.3768801 L21.0666766,53.9098639 Z" id="形状" fill="#231815"></path>
<path d="M26.6261443,48.5888212 C26.6261443,47.9305857 26.4297098,47.7503984 25.5846707,47.4120874 L25.5846707,47.0700992 C26.3912859,46.8249197 27.1843031,46.5376917 27.9604166,46.2096127 C28.012305,46.2096127 28.0641933,46.2463856 28.0641933,46.3898 L28.0641933,47.5518246 C29.094548,46.816366 29.9766502,46.2096127 31.1812016,46.2096127 C32.7082021,46.2096127 33.2493236,47.3128005 33.2493236,48.7101718 L33.2493236,53.3214971 C33.2493236,53.9098639 33.645899,53.9098639 34.1499574,53.9098639 L34.4724065,53.9098639 L34.4724065,54.3768801 C33.8423335,54.3768801 33.2122605,54.3401072 32.5673622,54.3401072 C31.922464,54.3401072 31.2701531,54.3401072 30.6215485,54.3768801 L30.6215485,53.9098639 L30.9477039,53.9098639 C31.4517624,53.9098639 31.8075683,53.9098639 31.8075683,53.3214971 L31.8075683,48.6991399 C31.8075683,47.6805298 31.1812016,47.180418 30.1545532,47.180418 C29.5800749,47.180418 28.6720285,47.6437569 28.0641933,48.0372272 L28.0641933,53.3214971 C28.0641933,53.9098639 28.464475,53.9098639 28.9685334,53.9098639 L29.2909825,53.9098639 L29.2909825,54.3768801 C28.6609095,54.3768801 28.0308365,54.3401072 27.3822319,54.3401072 C26.7336274,54.3401072 26.0887291,54.3401072 25.4401245,54.3768801 L25.4401245,53.9098639 L25.76628,53.9098639 C26.2666321,53.9098639 26.6261443,53.9098639 26.6261443,53.3214971 L26.6261443,48.5888212 Z" id="路径" fill="#231815"></path>
<path d="M39.0274638,48.87565 C39.4759275,48.87565 39.5278159,48.6439806 39.5278159,48.4270203 C39.5278159,47.5187289 38.9718691,46.7869477 37.9637523,46.7869477 C36.8518587,46.7869477 36.1105963,47.5885975 35.8956303,48.87565 L39.0274638,48.87565 Z M35.8437419,49.4493077 C35.8022944,49.79252 35.8022944,50.1394147 35.8437419,50.482627 C35.9475186,52.177859 37.0482932,53.571553 38.4863422,53.571553 C39.4759275,53.571553 40.2505467,53.0346682 40.9176828,52.3764328 L41.1660057,52.6264887 C40.3394982,53.7296766 39.3128498,54.6453225 37.830325,54.6453225 C34.9690523,54.6453225 34.394574,51.8910302 34.394574,50.7510694 C34.394574,47.2502866 36.7703199,46.21329 38.0267596,46.21329 C39.4870464,46.21329 41.0548164,47.1252586 41.0696416,49.0190644 C41.0770366,49.1255797 41.0770366,49.2324736 41.0696416,49.3389889 L40.9065639,49.4493077 L35.8437419,49.4493077 Z" id="形状" fill="#231815"></path>
<path d="M41.4921612,53.9098639 L41.977688,53.9098639 C42.4817464,53.9098639 42.8412587,53.9098639 42.8412587,53.3214971 L42.8412587,48.3056696 C42.8412587,47.7503984 42.1741225,47.6437569 41.9035618,47.5003425 L41.9035618,47.2319001 C43.2193025,46.6803062 43.938327,46.21329 44.1014047,46.21329 C44.2644824,46.21329 44.2607761,46.2684494 44.2607761,46.4486367 L44.2607761,48.0556137 L44.3015455,48.0556137 C44.7500093,47.3606053 45.5060969,46.2169673 46.6031652,46.2169673 C46.8661989,46.2060397 47.1226432,46.3003396 47.3149649,46.4787109 C47.5072865,46.6570822 47.6193836,46.904591 47.6261073,47.1657088 C47.6392051,47.39804 47.5573139,47.6257942 47.3989622,47.7974407 C47.2406104,47.9690872 47.0191814,48.0701191 46.7847745,48.0776775 C46.2251214,48.0776775 46.2251214,47.6474342 45.5950484,47.6474342 C44.8501648,47.7473898 44.2908591,48.3726592 44.2793077,49.1183513 L44.2793077,53.3214971 C44.2793077,53.9098639 44.6499388,53.9098639 45.1428783,53.9098639 L46.1509951,53.9098639 L46.1509951,54.3768801 C45.1614099,54.3548164 44.4090286,54.3401072 43.6344094,54.3401072 C42.8597902,54.3401072 42.1518847,54.3401072 41.4921612,54.3768801 L41.4921612,53.9098639 Z" id="路径" fill="#231815"></path>
<path d="M48.4081391,51.8910302 C48.6416367,53.0714412 49.3606612,54.0716648 50.6764019,54.0716648 C51.7364071,54.0716648 52.1329825,53.4281386 52.1329825,52.8029988 C52.1329825,50.69591 48.2117045,51.3762092 48.2117045,48.5005661 C48.2117045,47.5003425 49.0196805,46.2169673 50.998851,46.2169673 C51.7127757,46.2338009 52.4135528,46.4110811 53.0484415,46.7354656 L53.1781624,48.574112 L52.7630555,48.574112 C52.5814462,47.4488604 51.9550795,46.8053341 50.8024165,46.8053341 C50.0796857,46.8053341 49.3977243,47.2171909 49.3977243,47.9820678 C49.3977243,50.0744475 53.5710315,49.4309212 53.5710315,52.2330184 C53.5710315,53.4097521 52.6185093,54.663709 50.4762611,54.663709 C49.7057585,54.6401219 48.952655,54.430601 48.2821245,54.0532784 L48.0856899,52.0013489 L48.4081391,51.8910302 Z" id="路径" fill="#231815"></path>
<path d="M69.8306215,45.1431977 L69.3821578,45.1431977 C69.0411771,43.0581727 67.5475334,42.2013635 65.5350061,42.2013635 C63.4631778,42.2013635 60.4573589,43.576671 60.4573589,47.8607172 C60.4573589,51.4681415 63.0517772,54.0569557 65.8203921,54.0569557 C67.6303291,54.0569706 69.1681874,52.743767 69.4377525,50.9680296 L69.8528594,51.0746711 L69.4377525,53.6892264 C68.6964901,54.1525653 66.6468997,54.6379679 65.4571736,54.6379679 C61.2505096,54.6379679 58.5856714,51.938835 58.5856714,47.9232312 C58.5856714,44.2459383 61.8805826,41.6350604 65.4089915,41.6350604 C66.8655721,41.6350604 68.2665579,42.1020765 69.6527186,42.5838019 L69.8306215,45.1431977 Z" id="路径" fill="#231815"></path>
<path d="M70.4829324,53.9098639 L70.8239131,53.9098639 C71.3279715,53.9098639 71.6874838,53.9098639 71.6874838,53.3214971 L71.6874838,43.3928063 C71.6874838,42.2307818 71.416923,42.1976862 70.7312553,41.9991123 L70.7312553,41.7122835 C71.3709395,41.5117168 71.9911545,41.2544967 72.5844113,40.9437293 C72.7080785,40.8652075 72.8397668,40.7998788 72.9772803,40.7488328 C73.0884697,40.7488328 73.1218265,40.8554743 73.1218265,40.9988887 L73.1218265,53.3214971 C73.1218265,53.9098639 73.5184018,53.9098639 74.0224602,53.9098639 L74.3263778,53.9098639 L74.3263778,54.3768801 C73.7185427,54.3768801 73.0884697,54.3401072 72.4398651,54.3401072 C71.7912605,54.3401072 71.1463623,54.3401072 70.4792261,54.3768801 L70.4829324,53.9098639 Z" id="路径" fill="#231815"></path>
<path d="M82.0355065,53.3913656 C82.0355065,53.7149674 82.231941,53.7333539 82.5358586,53.7333539 C82.754531,53.7333539 83.0213854,53.7149674 83.2585894,53.7149674 L83.2585894,54.0826967 C82.3809283,54.2027258 81.5136562,54.388344 80.6641711,54.6379679 L80.5715133,54.5828085 L80.5715133,53.1339551 C79.4929765,54.0091508 78.666469,54.6379679 77.3840851,54.6379679 C76.4130314,54.6379679 75.4086209,54.0091508 75.4086209,52.5124927 L75.4086209,47.9489722 C75.4086209,47.481956 75.3344946,47.0370036 74.3300841,46.9487485 L74.3300841,46.6067603 C74.9786887,46.6067603 76.4130314,46.4854096 76.6502354,46.4854096 C76.8874393,46.4854096 76.8503762,46.6067603 76.8503762,47.0002306 L76.8503762,51.6078786 C76.8503762,52.1447634 76.8503762,53.6781945 78.4144398,53.6781945 C79.0259812,53.6781945 79.8339572,53.2148556 80.5900448,52.5750066 L80.5900448,47.8055578 C80.5900448,47.4378285 79.7264742,47.2502866 79.0815759,47.0700992 L79.0815759,46.7501747 C80.6975279,46.6398559 81.7056447,46.5001188 81.887254,46.5001188 C82.0688633,46.5001188 82.0318002,46.6214695 82.0318002,46.8200433 L82.0355065,53.3913656 Z" id="路径" fill="#231815"></path>
<path d="M85.6158037,52.3580463 C85.6683878,53.3479803 86.4707357,54.1360854 87.4689596,54.1783063 C89.3925355,54.1783063 90.2042178,52.302887 90.2042178,50.7142964 C90.2042178,48.787395 88.721693,47.180418 87.3244135,47.180418 C86.6572773,47.180418 86.1013306,47.606984 85.6158037,48.0188408 L85.6158037,52.3580463 Z M85.6158037,47.5003425 C86.3348282,46.8935892 87.3058819,46.21329 88.2954672,46.21329 C90.3858271,46.21329 91.6311478,48.0188408 91.6311478,49.9641287 C91.6311478,52.302887 89.9040065,54.6453225 87.3318261,54.6453225 C86.4582493,54.6448183 85.598518,54.428725 84.8300656,54.0165054 L84.3037693,54.4246849 L83.9331381,54.2297884 C84.0976742,53.177302 84.1819231,52.1139968 84.1851673,51.0489301 L84.1851673,43.3928063 C84.1851673,42.2307818 83.9146066,42.1976862 83.2289389,41.9991123 L83.2289389,41.7122835 C83.8687617,41.5120926 84.4890102,41.2548586 85.0820948,40.9437293 C85.2068941,40.8649019 85.3398441,40.7995637 85.4786702,40.7488328 C85.5861532,40.7488328 85.6232163,40.8554743 85.6232163,40.9988887 L85.6158037,47.5003425 Z" id="形状" fill="#231815"></path>
<path d="M7.98339572,64.7578779 L8.12052926,64.7578779 C8.47262889,64.7578779 8.84326007,64.7100731 8.84326007,64.2026067 L8.84326007,59.1132333 C8.84326007,58.6057669 8.47262889,58.5579621 8.12052926,58.5579621 L7.98339572,58.5579621 L7.98339572,58.267456 C8.35402691,58.267456 8.95074312,58.3042289 9.43256366,58.3042289 C9.9143842,58.3042289 10.5073941,58.267456 10.9706831,58.267456 L10.9706831,58.5579621 L10.8298432,58.5579621 C10.4814499,58.5579621 10.0885809,58.6057669 10.0885809,59.1132333 L10.0885809,64.2026067 C10.0885809,64.7100731 10.459212,64.7578779 10.8298432,64.7578779 L10.9743894,64.7578779 L10.9743894,65.048384 C10.5036878,65.048384 9.9143842,65.0116111 9.42515103,65.0116111 C8.93591787,65.0116111 8.36885216,65.048384 7.98710203,65.048384 L7.98339572,64.7578779 Z" id="路径" fill="#231815"></path>
<path d="M10.9262073,65.0079338 L10.9262073,64.7983281 L10.8335495,64.7983281 C10.4629183,64.7983281 10.066343,64.7394914 10.0626367,64.2026067 L10.0626367,59.1132333 C10.0626367,58.5763486 10.4814499,58.5175119 10.8335495,58.5175119 L10.9262073,58.5175119 L10.9262073,58.3079062 C10.4740373,58.3079062 9.90697157,58.3483564 9.44368259,58.3483564 C8.98039361,58.3483564 8.41703421,58.3115835 8.03528409,58.3079062 L8.03528409,58.5175119 L8.1316482,58.5175119 C8.48004151,58.5175119 8.89514844,58.5763486 8.89514844,59.1132333 L8.89514844,64.2026067 C8.89514844,64.7394914 8.48004151,64.7983281 8.1316482,64.7983281 L8.00933991,64.7983281 L8.00933991,65.0079338 C8.37997109,65.0079338 8.94333049,64.9674836 9.40661947,64.9674836 C9.86990845,64.9674836 10.4517994,65.0042565 10.9113821,65.0079338 M10.9558578,65.0888343 C10.4814499,65.0888343 9.89585264,65.0520613 9.40661947,65.0520613 C8.91738631,65.0520613 8.35402691,65.0888343 7.96857048,65.0888343 L7.92409473,65.0888343 L7.92409473,64.721105 L8.10570401,64.721105 C8.4763352,64.721105 8.78395908,64.684332 8.78766539,64.2099613 L8.78766539,59.1132333 C8.78766539,58.6388626 8.46150995,58.6057669 8.10570401,58.6020896 L7.92409473,58.6020896 L7.92409473,58.2343604 L7.96857048,58.2343604 C8.33920166,58.2343604 8.93962418,58.2748106 9.41773841,58.2748106 C9.89585264,58.2748106 10.4925688,58.2343604 10.9558578,58.2343604 L10.9966273,58.2343604 L10.9966273,58.6020896 L10.8187243,58.6020896 C10.4480931,58.6020896 10.1367629,58.6388626 10.1367629,59.1132333 L10.1367629,64.2026067 C10.1367629,64.6769775 10.4629183,64.7063958 10.8187243,64.7137504 L10.9966273,64.7137504 L10.9966273,65.0814797 L10.9558578,65.0888343 Z" id="形状" fill="#231815"></path>
<path d="M17.6976391,63.1325144 L17.7161706,63.114128 L17.7161706,59.473608 C17.7513788,59.2341447 17.6756751,58.991716 17.5101776,58.8139499 C17.3446802,58.6361838 17.1071024,58.5421066 16.8637189,58.5579621 L16.6487528,58.5579621 L16.6487528,58.267456 C17.1083355,58.267456 17.5605055,58.3042289 18.0200882,58.3042289 C18.4203699,58.3042289 18.8243579,58.267456 19.2246396,58.267456 L19.2246396,58.5579621 L19.0763871,58.5579621 C18.6649865,58.5579621 18.2054038,58.6351853 18.2054038,59.7935325 L18.2054038,64.206284 C18.1988407,64.5344878 18.2174258,64.8627119 18.2609985,65.1881212 L17.8903673,65.1881212 L12.8497832,59.6096679 L12.8497832,63.6179171 C12.8497832,64.4636945 13.0128609,64.7542006 13.7689485,64.7542006 L13.9394389,64.7542006 L13.9394389,65.0447067 C13.5169193,65.0447067 13.0981061,65.0079338 12.6755865,65.0079338 C12.253067,65.0079338 11.7860717,65.0447067 11.3450206,65.0447067 L11.3450206,64.7542006 L11.4821541,64.7542006 C12.1567029,64.7542006 12.3642563,64.294539 12.3642563,63.5223075 L12.3642563,59.4331578 C12.3642751,59.1991174 12.2698148,58.9748196 12.1019759,58.8103678 C11.934137,58.6459159 11.7069038,58.555012 11.4710352,58.5579621 L11.3450206,58.5579621 L11.3450206,58.267456 C11.7156518,58.267456 12.0862829,58.3042289 12.4569141,58.3042289 C12.7534191,58.3042289 13.0388051,58.267456 13.3316037,58.267456 L17.6976391,63.1325144 Z" id="路径" fill="#231815"></path>
<path d="M17.8903673,65.1513482 L18.2128164,65.1513482 C18.1756819,64.8401955 18.1595852,64.526925 18.1646344,64.2136386 L18.1646344,59.8008871 C18.1646344,58.6351853 18.6538675,58.5248665 19.0763871,58.5211892 L19.1801638,58.5211892 L19.1801638,58.3152608 C18.8095326,58.3152608 18.409251,58.355711 18.0200882,58.355711 C17.5716245,58.355711 17.1342797,58.3189381 16.6932286,58.3152608 L16.6932286,58.5211892 L16.8674252,58.5211892 C17.1211201,58.5094027 17.3670893,58.6094934 17.5393838,58.7946252 C17.7116782,58.979757 17.792671,59.2309896 17.7606464,59.4809626 L17.7606464,63.1582555 L17.7421148,63.176642 L17.708758,63.2097376 L13.3538416,58.3226154 C13.061043,58.3226154 12.7793633,58.3630656 12.479152,58.3630656 C12.1085208,58.3630656 11.7378896,58.3262927 11.3969089,58.3226154 L11.3969089,58.5285438 L11.4821541,58.5285438 C11.7291797,58.526583 11.9667672,58.6225737 12.1421408,58.7951932 C12.3175144,58.9678127 12.4161526,59.2027677 12.4161447,59.447867 L12.4161447,63.540694 C12.4161447,64.3166028 12.1974723,64.8130373 11.493273,64.8167146 L11.3969089,64.8167146 L11.3969089,65.0263203 C11.8231348,65.0263203 12.2604796,64.9858701 12.6867055,64.9858701 C13.1129313,64.9858701 13.5020941,65.022643 13.9060821,65.0263203 L13.9060821,64.8203919 L13.7837738,64.8203919 C13.0128609,64.8203919 12.8201327,64.4820809 12.8201327,63.6399809 L12.8201327,59.5067037 L17.8903673,65.1513482 Z M18.2609985,65.235926 L17.8607168,65.235926 L12.8905526,59.7420504 L12.8905526,63.6179171 C12.8905526,64.4600172 13.0313925,64.7027185 13.7689485,64.721105 L13.976502,64.721105 L13.976502,65.0888343 L13.9394389,65.0888343 C13.513213,65.0888343 13.0943998,65.0520613 12.6755865,65.0520613 C12.2567733,65.0520613 11.7860717,65.0888343 11.3450206,65.0888343 L11.3005448,65.0888343 L11.3005448,64.721105 L11.4821541,64.721105 C12.1270524,64.721105 12.312368,64.3055709 12.3197806,63.5333394 L12.3197806,59.4331578 C12.3208135,59.2105107 12.2313613,58.9968467 12.0716392,58.8404512 C11.9119171,58.6840557 11.6954062,58.5981291 11.4710352,58.6020896 L11.3005448,58.6020896 L11.3005448,58.2343604 L11.3450206,58.2343604 C11.7156518,58.2343604 12.0862829,58.2748106 12.4569141,58.2748106 C12.7497128,58.2748106 13.0313925,58.2343604 13.3612542,58.2490695 L17.6642823,63.0552913 L17.6642823,59.4846399 C17.6642823,58.7124084 17.1416923,58.6167988 16.8563063,58.6131215 L16.5968645,58.6131215 L16.5968645,58.2453922 L16.6413402,58.2453922 C17.1046292,58.2453922 17.5530929,58.2858425 18.0126756,58.2858425 C18.409251,58.2858425 18.8095326,58.2453922 19.2172269,58.2453922 L19.2579964,58.2453922 L19.2579964,58.6131215 L19.0689745,58.6131215 C18.6649865,58.6131215 18.2498795,58.657249 18.2387606,59.8045644 L18.2387606,64.2173158 C18.2332529,64.5431728 18.2530787,64.8689661 18.2980616,65.1917985 L18.2980616,65.2396033 L18.2609985,65.235926 Z" id="形状" fill="#231815"></path>
<path d="M20.6886327,58.7528586 C19.9473704,58.7528586 19.9251325,58.9293687 19.7805863,59.6354089 L19.4989066,59.6354089 C19.5396761,59.3632893 19.5878581,59.0911696 19.6175086,58.8116953 C19.6579297,58.5377392 19.6777514,58.2611789 19.6768096,57.9843044 L19.9103073,57.9843044 C19.9918461,58.2784879 20.2364627,58.267456 20.4996108,58.267456 L25.5513139,58.267456 C25.8181683,58.267456 26.0627849,58.267456 26.0813165,57.965918 L26.3148141,58.0063682 C26.277751,58.267456 26.2369816,58.5285438 26.2073311,58.7933089 C26.1776806,59.058074 26.170268,59.3154845 26.170268,59.572895 L25.8774693,59.6832137 C25.8552315,59.3154845 25.8070494,58.7491814 25.136207,58.7491814 L23.5313739,58.7491814 L23.5313739,63.9157778 C23.5313739,64.6512364 23.8760609,64.7542006 24.3430562,64.7542006 L24.5320781,64.7542006 L24.5320781,65.0447067 C24.1614469,65.0447067 23.460954,65.0079338 22.9346577,65.0079338 C22.3453541,65.0079338 21.6596864,65.0447067 21.2779363,65.0447067 L21.2779363,64.7542006 L21.4669582,64.7542006 C22.0043734,64.7542006 22.2786405,64.7063958 22.2786405,63.9378416 L22.2786405,58.7528586 L20.6886327,58.7528586 Z" id="路径" fill="#231815"></path>
<path d="M24.5506097,65.0888343 C24.1799785,65.0888343 23.4794856,65.0520613 22.9531893,65.0520613 C22.367592,65.0520613 21.6819243,65.0888343 21.2964679,65.0888343 L21.2556985,65.0888343 L21.2556985,64.721105 L21.4854898,64.721105 C22.022905,64.721105 22.2267522,64.7027185 22.2526963,63.9488735 L22.2526963,58.8006635 L20.6886327,58.8006635 L20.6886327,58.7160857 L22.3379415,58.7160857 L22.3379415,63.9488735 C22.3379415,64.7284596 22.0191987,64.8056827 21.4854898,64.80936 L21.3520626,64.80936 L21.3520626,65.0116111 C21.7226937,65.0116111 22.4009488,64.9748382 22.9680145,64.9748382 C23.4794856,64.9748382 24.1317964,65.0116111 24.5209592,65.0116111 L24.5209592,64.80936 L24.376413,64.80936 C23.9020051,64.80936 23.5239613,64.6916866 23.5239613,63.9268097 L23.5239613,58.7087311 L25.1695638,58.7087311 C25.8292873,58.7087311 25.9108261,59.2640024 25.9367703,59.6280543 L26.1480301,59.5508312 C26.1480301,59.297098 26.1480301,59.0433648 26.1480301,58.7859543 C26.1480301,58.5285438 26.2110374,58.2895197 26.2481005,58.0504957 L26.099848,58.0247547 C26.0553723,58.3079062 25.7773989,58.3226154 25.5327823,58.3189381 L20.4366035,58.3189381 C20.2031059,58.3189381 19.954783,58.3189381 19.8658315,58.0394638 L19.7027538,58.0394638 C19.7030749,58.3027542 19.6844964,58.565733 19.6471591,58.8264045 C19.6175086,59.0948469 19.5730329,59.3485801 19.5359698,59.6059906 L19.7472295,59.6059906 C19.8732441,58.9293687 19.9584893,58.7087311 20.6886327,58.719763 L20.6886327,58.8043407 C19.9473704,58.8043407 19.9807272,58.9404006 19.8213558,59.6574727 L19.8213558,59.6905683 L19.4507246,59.6905683 L19.4507246,59.6390862 C19.4877877,59.3706438 19.5396761,59.0948469 19.5693266,58.8153726 C19.6096263,58.5438714 19.6294483,58.2697648 19.6286276,57.9953363 L19.6286276,57.9548861 L19.9362514,57.9548861 L19.9362514,57.9843044 C20.002965,58.2306831 20.1845743,58.2306831 20.4366035,58.2343604 L25.5364886,58.2343604 C25.8070494,58.2343604 26.0071902,58.2343604 26.0257218,57.9732726 L26.0257218,57.929145 L26.0664912,57.929145 L26.3444646,57.9732726 L26.3444646,58.0137228 C26.3074015,58.2748106 26.2666321,58.5358984 26.2406879,58.7969862 C26.2147437,59.058074 26.2221563,59.3191617 26.2221563,59.5802495 L26.2221563,59.6096679 L26.1925058,59.6096679 L25.8441125,59.7383731 L25.8441125,59.6795364 C25.8181683,59.3118072 25.7773989,58.7859543 25.1584448,58.7859543 L23.5980875,58.7859543 L23.5980875,63.9121005 C23.5980875,64.6475591 23.8982988,64.6990412 24.3652941,64.7063958 L24.5950854,64.7063958 L24.5950854,65.0741251 L24.5506097,65.0888343 Z" id="路径" fill="#231815"></path>
<path d="M26.6113191,64.7578779 L26.7521589,64.7578779 C27.1227901,64.7578779 27.4934213,64.7100731 27.4934213,64.2026067 L27.4934213,59.1132333 C27.4934213,58.6057669 27.1227901,58.5579621 26.7521589,58.5579621 L26.6113191,58.5579621 L26.6113191,58.267456 C27.2080353,58.267456 28.2309774,58.3042289 29.0500723,58.3042289 C29.8691672,58.3042289 30.9032282,58.267456 31.5555391,58.267456 C31.5555391,58.6829901 31.5555391,59.3265163 31.577777,59.7383731 L31.281272,59.8155963 C31.2367963,59.1831019 31.1181943,58.6793128 30.0878396,58.6793128 L28.7313295,58.6793128 L28.7313295,61.2239995 L29.8765798,61.2239995 C30.4658834,61.2239995 30.5956043,60.8967204 30.651199,60.3708675 L30.9439976,60.3708675 C30.9439976,60.7385968 30.9143471,61.1283898 30.9143471,61.507151 C30.9143471,61.8859122 30.9143471,62.2426096 30.9439976,62.6103389 L30.651199,62.6691755 C30.5956043,62.0881633 30.5659538,61.7094021 29.8876988,61.7094021 L28.7165042,61.7094021 L28.7165042,63.9856464 C28.7165042,64.6181408 29.2798636,64.6181408 29.9062303,64.6181408 C31.0848375,64.6181408 31.6000148,64.5372403 31.8965198,63.4340525 L32.1707869,63.5002437 C32.0410659,64.0150647 31.9261703,64.5298857 31.8372188,65.0447067 C31.2108521,65.0447067 30.0841333,65.0079338 29.2057374,65.0079338 C28.3273415,65.0079338 27.1561469,65.0447067 26.6113191,65.0447067 L26.6113191,64.7578779 Z" id="路径" fill="#231815"></path>
<path d="M31.8149809,65.0042565 C31.9002261,64.5151766 32.0114154,64.0224193 32.1337237,63.5333394 L31.9409955,63.4855346 C31.6444906,64.5887224 31.0774249,64.6696229 29.9210556,64.6622683 C29.3058078,64.6622683 28.6868537,64.6622683 28.6868537,63.9893237 L28.6868537,61.6836611 L29.902524,61.6836611 C30.5881917,61.6836611 30.6437864,62.084486 30.7030874,62.6397572 L30.9143471,62.5956297 C30.8958156,62.2279004 30.888403,61.8601711 30.888403,61.5218602 C30.888403,61.1835492 30.888403,60.7864016 30.9143471,60.4186723 L30.7030874,60.4186723 C30.651199,60.9261387 30.4881213,61.2791588 29.8914051,61.2754816 L28.6868537,61.2754816 L28.6868537,58.6535717 L30.0878396,58.6535717 C31.1070753,58.6535717 31.2775657,59.1683927 31.3220414,59.7788234 L31.5333012,59.7199867 C31.5147697,59.447867 31.5036507,59.0948469 31.5036507,58.7712451 C31.5036507,58.6057669 31.5036507,58.4476433 31.5036507,58.3079062 C30.8365146,58.3079062 29.843223,58.3483564 29.0389533,58.3483564 C28.2346837,58.3483564 27.2450984,58.3115835 26.6446759,58.3079062 L26.6446759,58.5138346 L26.74104,58.5138346 C27.0894333,58.5138346 27.5045402,58.5763486 27.5082465,59.1132333 L27.5082465,64.2026067 C27.5082465,64.7394914 27.0894333,64.7983281 26.74104,64.8020054 L26.6446759,64.8020054 L26.6446759,65.0042565 C27.2154479,65.0042565 28.345873,64.9674836 29.2057374,64.9674836 C30.0656017,64.9674836 31.173789,65.0042565 31.8001557,65.0042565 M31.8372188,65.0888343 C31.2071458,65.0888343 30.080427,65.048384 29.2020311,65.048384 C28.3236352,65.048384 27.1561469,65.0888343 26.6076128,65.0888343 L26.5668433,65.0888343 L26.5668433,64.721105 L26.7484526,64.721105 C27.1190838,64.721105 27.4230014,64.684332 27.4267077,64.2099613 L27.4267077,59.1132333 C27.4267077,58.6388626 27.1005522,58.6057669 26.7484526,58.6020896 L26.5668433,58.6020896 L26.5668433,58.2343604 L26.6076128,58.2343604 C27.2080353,58.2343604 28.227271,58.2748106 29.046366,58.2748106 C29.8654609,58.2748106 30.8995219,58.2343604 31.5518328,58.2343604 L31.5963085,58.2343604 L31.5963085,58.2821652 C31.5963085,58.4255796 31.5963085,58.5984123 31.5963085,58.782277 C31.5963085,59.1169106 31.5963085,59.4846399 31.625959,59.7604369 L31.625959,59.7972098 L31.5926022,59.7972098 L31.2516215,59.8891421 L31.2516215,59.83766 C31.1923205,59.208843 31.1070753,58.7344722 30.0989585,58.7344722 L28.7646863,58.7344722 L28.7646863,61.1982584 L29.8765798,61.1982584 C30.4436455,61.1982584 30.5437159,60.904075 30.6178422,60.3818994 L30.6178422,60.3414492 L30.9884734,60.3414492 L30.9884734,60.3855767 C30.9884734,60.753306 30.9588229,61.143099 30.9588229,61.5218602 C30.9588229,61.9006213 30.9588229,62.2573187 30.9884734,62.625048 L30.9884734,62.6581437 L30.9514103,62.6581437 L30.6104296,62.7280122 L30.6104296,62.6802074 C30.5437159,62.0881633 30.5363033,61.7645615 29.8691672,61.7572069 L28.7646863,61.7572069 L28.7646863,63.9635826 C28.7646863,64.5519495 29.2798636,64.5482722 29.9173492,64.5519495 C31.0996627,64.5519495 31.5703643,64.4894355 31.8668693,63.4009568 L31.8668693,63.3605066 L31.9076387,63.3605066 L32.2226752,63.441407 L32.2226752,63.47818 C32.0929543,63.993001 31.9780586,64.507822 31.8891071,65.022643 L31.8891071,65.0557386 L31.8372188,65.0888343 Z" id="形状" fill="#231815"></path>
<path d="M34.7577925,61.5476012 L35.2359067,61.5476012 C36.2180794,61.5476012 36.7443757,61.1798719 36.7443757,60.0325566 C36.7724105,59.6485676 36.6305753,59.2714921 36.3558388,58.9996128 C36.0811022,58.7277335 35.7006844,58.5879875 35.3137393,58.6167988 C35.1277498,58.6186477 34.9420641,58.632158 34.7577925,58.657249 L34.7577925,61.5476012 Z M33.5050591,59.297098 C33.5050591,58.5873805 33.1121901,58.5616394 32.8082725,58.5616394 L32.6340758,58.5616394 L32.6340758,58.2711333 C32.945406,58.2711333 33.5532412,58.3079062 34.1499574,58.3079062 C34.7466736,58.3079062 35.2099626,58.2711333 35.7288462,58.2711333 C36.9593418,58.2711333 38.0601164,58.6020896 38.0601164,59.9810745 C38.0601164,60.8562702 37.4708128,61.3894776 36.6961936,61.6910156 L38.3714466,64.1768656 C38.5547184,64.5305667 38.9194682,64.7553368 39.3202624,64.7615552 L39.3202624,65.0520613 C38.9978133,65.0520613 38.6827768,65.0152884 38.3640339,65.0152884 C38.0452911,65.0152884 37.7450799,65.0520613 37.4411623,65.0520613 C36.6906388,64.0497992 36.0095815,62.9981256 35.4026908,61.9042986 L34.7577925,61.9042986 L34.7577925,63.9856464 C34.7577925,64.721105 35.1098921,64.7652325 35.5583559,64.7652325 L35.7362588,64.7652325 L35.7362588,65.0557386 C35.1766058,65.0557386 34.6243653,65.0189657 34.0535933,65.0189657 C33.5828917,65.0189657 33.123309,65.0557386 32.6340758,65.0557386 L32.6340758,64.7652325 L32.8082725,64.7652325 C33.1789037,64.7652325 33.5050591,64.5997543 33.5050591,64.2393796 L33.5050591,59.297098 Z" id="形状" fill="#231815"></path>
<path d="M35.2359067,61.507151 C36.2069605,61.507151 36.6961936,61.1651628 36.7184315,60.0362339 C36.7464394,59.662676 36.6090275,59.2955467 36.3420621,59.0306716 C36.0750967,58.7657965 35.7050703,58.6294604 35.3285645,58.657249 C35.1574519,58.6585147 34.9866024,58.6707982 34.8170935,58.694022 L34.8170935,61.507151 L35.2359067,61.507151 Z M34.7577925,61.5917287 L34.7170231,61.5917287 L34.7170231,58.6167988 L34.7540862,58.6167988 C34.9397184,58.5933471 35.1266113,58.581067 35.3137393,58.5800259 C35.7154553,58.5499606 36.1104058,58.6952485 36.395269,58.9778813 C36.6801321,59.2605141 36.8265666,59.6523723 36.796264,60.050943 C36.796264,61.2129676 36.2329046,61.6064379 35.2433194,61.6064379 L34.7577925,61.5917287 Z M34.7577925,61.8564938 L35.4360476,61.8564938 L35.4360476,61.8785576 C36.0382643,62.9611869 36.7066479,64.0062629 37.437456,65.0079338 C37.7376672,65.0079338 38.0489974,64.9674836 38.3566213,64.9674836 C38.6642452,64.9674836 38.9718691,65.0005792 39.2757867,65.0042565 L39.2757867,64.7909735 C38.876958,64.7744573 38.5176571,64.5470911 38.3343835,64.1952521 L36.62948,61.6689519 L36.6776621,61.6468881 C37.4448686,61.3490274 38.0119343,60.8305291 38.0119343,59.9773972 C38.0119343,58.6278307 36.9556354,58.3152608 35.7251399,58.3079062 C35.2099626,58.3079062 34.739261,58.3483564 34.1462511,58.3483564 C33.5532412,58.3483564 32.9898818,58.3115835 32.6637263,58.3079062 L32.6637263,58.5138346 L32.7971536,58.5138346 C33.1010711,58.5138346 33.5384159,58.5653167 33.5384159,59.297098 L33.5384159,64.232025 C33.5384159,64.6218181 33.1677847,64.7983281 32.7971536,64.8020054 L32.6637263,64.8020054 L32.6637263,65.0042565 C33.1344279,65.0042565 33.5828917,64.9674836 34.0424743,64.9674836 C34.5873022,64.9674836 35.1543679,65.0042565 35.6843705,65.0042565 L35.6843705,64.8020054 L35.5472369,64.8020054 C35.1024795,64.8020054 34.7059042,64.746846 34.7059042,63.9782918 L34.7059042,61.8564938 L34.7577925,61.8564938 Z M39.3202624,65.0888343 C38.994107,65.0888343 38.6827768,65.0520613 38.3603276,65.0520613 C38.0378785,65.0520613 37.7487862,65.0888343 37.4078055,65.0741251 C36.6600445,64.0786685 35.9826409,63.0330433 35.3804529,61.9447489 L34.8022683,61.9447489 L34.8022683,63.9856464 C34.8022683,64.721105 35.1061858,64.721105 35.5435306,64.721105 L35.7659093,64.721105 L35.7659093,65.0888343 L35.7214336,65.0888343 C35.1617805,65.0888343 34.5910085,65.0520613 34.038768,65.0520613 C33.5717727,65.0520613 33.1084837,65.0888343 32.6192506,65.0888343 L32.5747748,65.0888343 L32.5747748,64.721105 L32.7934472,64.721105 C33.1455469,64.721105 33.4457581,64.5666587 33.4457581,64.2393796 L33.4457581,59.297098 C33.4457581,58.6057669 33.0973648,58.6094442 32.7934472,58.6020896 L32.5747748,58.6020896 L32.5747748,58.2343604 L32.6192506,58.2343604 C32.9342871,58.2343604 33.5384159,58.2748106 34.1351321,58.2748106 C34.7318483,58.2748106 35.191431,58.2343604 35.714021,58.2343604 C36.9482228,58.2343604 38.0823542,58.5763486 38.0860606,59.988429 C38.0860606,60.8636247 37.5004633,61.4115414 36.748082,61.7167567 L38.3899781,64.1621565 C38.5727597,64.5076427 38.934411,64.7232181 39.327675,64.721105 L39.3610318,64.721105 L39.3610318,65.0888343 L39.3202624,65.0888343 Z" id="形状" fill="#231815"></path>
<path d="M45.9693859,63.1325144 L45.9693859,63.114128 L45.9693859,59.473608 C46.0044219,59.2348202 45.9291825,58.993077 45.7645474,58.8154649 C45.5999123,58.6378528 45.3633982,58.5432692 45.1206405,58.5579621 L44.9056744,58.5579621 L44.9056744,58.267456 C45.365257,58.267456 45.8137208,58.3042289 46.2770097,58.3042289 C46.6809977,58.3042289 47.0775731,58.267456 47.4815611,58.267456 L47.4815611,58.5579621 L47.3333086,58.5579621 C46.921908,58.5579621 46.4623253,58.6351853 46.4623253,59.7935325 L46.4623253,64.206284 C46.4569331,64.5345823 46.4767574,64.8628112 46.5216263,65.1881212 L46.1509951,65.1881212 L41.1067047,59.6096679 L41.1067047,63.6179171 C41.1067047,64.4636945 41.2734888,64.7542006 42.0258701,64.7542006 L42.1926541,64.7542006 L42.1926541,65.0447067 C41.7738409,65.0447067 41.3513213,65.0079338 40.9325081,65.0079338 C40.5136948,65.0079338 40.0392869,65.0447067 39.5982358,65.0447067 L39.5982358,64.7542006 L39.7353693,64.7542006 C40.4099181,64.7542006 40.6174716,64.294539 40.6174716,63.5223075 L40.6174716,59.4331578 C40.6184843,59.1994558 40.5248526,58.9751419 40.3575992,58.810581 C40.1903459,58.64602 39.9634863,58.5550017 39.7279567,58.5579621 L39.5982358,58.5579621 L39.5982358,58.267456 C39.968867,58.267456 40.3394982,58.3042289 40.7101294,58.3042289 C41.002928,58.3042289 41.288314,58.267456 41.5811126,58.267456 L45.9693859,63.1325144 Z" id="路径" fill="#231815"></path>
<path d="M46.1658204,65.1513482 L46.4845632,65.1513482 C46.4460903,64.8402867 46.428755,64.5270093 46.4326748,64.2136386 L46.4326748,59.8008871 C46.4326748,58.6351853 46.9293206,58.5248665 47.3481339,58.5211892 L47.4519106,58.5211892 L47.4519106,58.3152608 C47.0812794,58.3152608 46.6809977,58.355711 46.291835,58.355711 C45.8433713,58.355711 45.4060265,58.3189381 44.961269,58.3152608 L44.961269,58.5211892 L45.1354657,58.5211892 C45.3891606,58.5094027 45.6351298,58.6094934 45.8074242,58.7946252 C45.9797186,58.979757 46.0607114,59.2309896 46.0286869,59.4809626 L46.0286869,63.1582555 L46.0101553,63.176642 L45.9805048,63.2097376 L41.6218821,58.3226154 C41.3327897,58.3226154 41.0474037,58.3630656 40.7508988,58.3630656 C40.3802676,58.3630656 40.0096364,58.3262927 39.6649494,58.3226154 L39.6649494,58.5285438 L39.7539009,58.5285438 C40.0005873,58.5265758 40.237799,58.622682 40.4125861,58.7954096 C40.5873731,58.9681373 40.6851839,59.2031062 40.6841852,59.447867 L40.6841852,63.5370167 C40.6841852,64.3166028 40.4618065,64.8130373 39.7613135,64.8167146 L39.6649494,64.8167146 L39.6649494,65.0263203 C40.0911753,65.0263203 40.5248138,64.9858701 40.9547459,64.9858701 C41.3846781,64.9858701 41.7738409,65.022643 42.1778288,65.0263203 L42.1778288,64.8203919 L42.0518142,64.8203919 C41.2846077,64.8203919 41.0918795,64.4857582 41.0881732,63.6363036 L41.0881732,59.510381 L46.1658204,65.1513482 Z M46.5364516,65.235926 L46.1324636,65.235926 L41.1622994,59.7420504 L41.1622994,63.6179171 C41.1622994,64.4673718 41.3031392,64.721105 42.0406953,64.721105 L42.2482488,64.721105 L42.2482488,65.0888343 L42.2074793,65.0888343 C41.7849598,65.0888343 41.3624402,65.048384 40.943627,65.048384 C40.5248138,65.048384 40.0541122,65.0888343 39.613061,65.0888343 L39.5685853,65.0888343 L39.5685853,64.721105 L39.7501946,64.721105 C40.3950928,64.721105 40.5804084,64.3055709 40.5878211,63.5296621 L40.5878211,59.4331578 C40.5888354,59.2111584 40.4998898,58.9980674 40.3409767,58.8417815 C40.1820636,58.6854957 39.9665156,58.5991273 39.742782,58.6020896 L39.5685853,58.6020896 L39.5685853,58.2343604 L39.613061,58.2343604 C39.9836922,58.2343604 40.3543234,58.2748106 40.7249546,58.2748106 C41.0140469,58.2748106 41.2957266,58.2343604 41.6255884,58.2490695 L45.9286164,63.0552913 L45.9286164,59.4846399 C45.9286164,58.7124084 45.4060265,58.6167988 45.1206405,58.6131215 L44.8611986,58.6131215 L44.8611986,58.2453922 L44.9056744,58.2453922 C45.3689633,58.2453922 45.8174271,58.2858425 46.2770097,58.2858425 C46.6772914,58.2858425 47.0775731,58.2453922 47.4815611,58.2453922 L47.5223305,58.2453922 L47.5223305,58.6131215 L47.3333086,58.6131215 C46.9293206,58.6131215 46.5142137,58.657249 46.5030948,59.8045644 L46.5030948,64.2173158 C46.4975871,64.5431728 46.5174129,64.8689661 46.5623958,65.1917985 L46.5623958,65.2396033 L46.5364516,65.235926 Z" id="形状" fill="#231815"></path>
<path d="M50.5022053,59.3559347 L50.4799674,59.3559347 L49.571921,62.1065497 L51.3991327,62.1065497 L50.5022053,59.3559347 Z M49.0715689,63.6583673 C48.9729541,63.9028568 48.8996854,64.1566731 48.8528965,64.4158897 C48.8528965,64.7100731 49.2680034,64.7578779 49.5941589,64.7578779 L49.7016419,64.7578779 L49.7016419,65.048384 C49.3087728,65.0263203 48.9084912,65.0116111 48.5156221,65.0116111 C48.122753,65.0116111 47.8114229,65.0116111 47.4593232,65.048384 L47.4593232,64.7578779 L47.5186242,64.7578779 C47.9116305,64.757198 48.2571968,64.4996782 48.3673696,64.1253835 L49.9351395,59.6648273 C50.0914238,59.2746651 50.2153783,58.8725073 50.3057707,58.4623525 C50.6154107,58.3518873 50.9133498,58.2114743 51.1952856,58.0431411 C51.2222248,58.0380689 51.2498852,58.0380689 51.2768244,58.0431411 C51.3031061,58.0378634 51.329875,58.0496676 51.343538,58.0725595 C51.3731885,58.1497826 51.402839,58.2380376 51.4324895,58.3152608 L53.2263445,63.3788931 C53.3449464,63.717204 53.4598421,64.0775787 53.5969756,64.3680849 C53.7003408,64.6278537 53.9656628,64.7873047 54.2455802,64.7578779 L54.3048812,64.7578779 L54.3048812,65.048384 C53.8638301,65.0263203 53.422779,65.0116111 52.9557837,65.0116111 C52.4887884,65.0116111 51.98473,65.0116111 51.473259,65.048384 L51.473259,64.7578779 L51.580742,64.7578779 C51.8031207,64.7578779 52.1885771,64.7174277 52.1885771,64.4747263 C52.1499445,64.2352092 52.0840448,64.0008148 51.9921426,63.7760407 L51.6215114,62.6471118 L49.3977243,62.6471118 L49.0715689,63.6583673 Z" id="形状" fill="#231815"></path>
<path d="M49.6275157,62.0624222 L51.343538,62.0624222 L50.4910863,59.4588989 L49.6275157,62.0624222 Z M49.5200326,62.147 L50.4540232,59.3118072 L50.5096179,59.3118072 L50.5096179,59.3559347 L50.5096179,59.3559347 L50.5096179,59.3118072 L50.5392684,59.3118072 L51.4695526,62.147 L49.5200326,62.147 Z M51.521441,65.0042565 C52.0032616,65.0042565 52.4776695,64.9674836 52.9483711,64.9674836 C53.4190727,64.9674836 53.826767,64.9858701 54.2529928,65.0042565 L54.2529928,64.7983281 L54.2529928,64.7983281 C53.9562766,64.8284921 53.6757482,64.658484 53.5673252,64.382794 C53.4413106,64.0922879 53.3190023,63.7319132 53.196694,63.3899249 L51.3731885,58.3079062 L51.2879434,58.0762368 C51.2879434,58.0762368 51.2879434,58.0762368 51.2879434,58.0762368 L51.2694118,58.0762368 C51.2509641,58.0737631 51.2322648,58.0737631 51.2138171,58.0762368 C50.9402302,58.246073 50.6447551,58.3784287 50.3354212,58.4697071 C50.243789,58.8831352 50.1198713,59.2888621 49.96479,59.6832137 L48.4081391,64.1400927 C48.2914762,64.5308801 47.9294095,64.7988072 47.5186242,64.7983281 L47.5186242,64.7983281 L47.5186242,65.0042565 C47.8558986,65.0042565 48.193173,64.9674836 48.5341537,64.9674836 C48.8751344,64.9674836 49.3013602,64.9858701 49.6756977,65.0042565 L49.6756977,64.7983281 L49.6126904,64.7983281 C49.2902413,64.7983281 48.8454839,64.7615552 48.8343649,64.4305988 C48.8797555,64.1674845 48.9530536,63.9098682 49.0530373,63.6620446 L49.0938068,63.6620446 L49.0530373,63.6620446 L49.3754865,62.6213707 L51.6585746,62.6213707 L52.0477373,63.7760407 C52.1449271,64.0043679 52.2121898,64.2441234 52.2478781,64.4894355 C52.2478781,64.7872962 51.8216523,64.80936 51.5992736,64.8130373 L51.5362663,64.8130373 L51.521441,65.0042565 Z M54.2937623,65.0888343 C53.8564175,65.0704478 53.4116601,65.048384 52.9483711,65.048384 C52.4850821,65.048384 51.9773174,65.0704478 51.4658463,65.0888343 L51.4176643,65.0888343 L51.4176643,64.721105 L51.5696231,64.721105 C51.7957081,64.721105 52.1329825,64.6696229 52.1329825,64.4820809 C52.0957364,64.2462443 52.031062,64.0154863 51.9402543,63.7944272 L51.5696231,62.6912393 L49.4125496,62.6912393 L49.108632,63.6730765 C49.0124333,63.9108061 48.9392136,64.1570652 48.8899596,64.4085351 C48.8899596,64.6475591 49.2605908,64.7063958 49.5867462,64.7063958 L49.7349987,64.7063958 L49.7349987,65.0741251 L49.690523,65.0741251 C49.3013602,65.0557386 48.9010785,65.0336749 48.5082095,65.0336749 C48.1153404,65.0336749 47.8040102,65.0557386 47.4556169,65.0741251 L47.4074349,65.0741251 L47.4074349,64.7063958 L47.5112116,64.7063958 C47.8854114,64.7050382 48.2141508,64.459666 48.3191876,64.1033198 L49.8906638,59.6501181 C50.043083,59.248136 50.1730019,58.8381059 50.2798265,58.4219023 C50.5888349,58.3099362 50.8866322,58.1695901 51.1693414,58.0026909 C51.1988848,57.990606 51.2301457,57.9831621 51.2619992,57.980598 C51.3059586,57.9797135 51.3475496,58.0003462 51.3731885,58.0357865 C51.3991327,58.116687 51.4324895,58.204942 51.46214,58.2821652 L53.2634076,63.3641839 C53.3783033,63.7024948 53.4969052,64.0628695 53.6340388,64.3496984 C53.7275522,64.5961769 53.9782059,64.7478179 54.2418739,64.7174277 L54.338238,64.7174277 L54.338238,65.085157 L54.2937623,65.0888343 Z" id="形状" fill="#231815"></path>
<path d="M54.7088692,58.7528586 C53.9676068,58.7528586 53.945369,58.9293687 53.7971165,59.6354089 L53.5043179,59.6354089 C53.541381,59.3632893 53.5932693,59.0911696 53.6229198,58.8116953 C53.6602528,58.5374595 53.6788268,58.2610303 53.6785145,57.9843044 L53.9157185,57.9843044 C53.993551,58.2784879 54.2381676,58.267456 54.5013157,58.267456 L59.5567251,58.267456 C59.8198732,58.267456 60.0644898,58.267456 60.0830214,57.965918 L60.316519,58.0063682 C60.2794559,58.267456 60.2423928,58.5285438 60.2127423,58.7933089 C60.1830918,59.058074 60.1905044,59.3154845 60.1905044,59.5765722 L59.8977058,59.686891 C59.8977058,59.3191617 59.8309922,58.7528586 59.174975,58.7528586 L57.5664356,58.7528586 L57.5664356,63.9194551 C57.5664356,64.6549137 57.9111226,64.7578779 58.3818242,64.7578779 L58.5671398,64.7578779 L58.5671398,65.048384 C58.1965087,65.048384 57.499722,65.0116111 56.9697194,65.0116111 C56.4397168,65.0116111 55.6984545,65.048384 55.3167043,65.048384 L55.3167043,64.7578779 L55.5020199,64.7578779 C56.0431415,64.7578779 56.3137022,64.7100731 56.3137022,63.9415189 L56.3137022,58.7528586 L54.7088692,58.7528586 Z" id="路径" fill="#231815"></path>
<path d="M58.5782588,65.0888343 C58.2076276,65.0888343 57.5071347,65.0520613 56.9808384,65.0520613 C56.3952411,65.0520613 55.7095734,65.0888343 55.3278233,65.0888343 L55.2833475,65.0888343 L55.2833475,64.721105 L55.5131389,64.721105 C56.0542604,64.721105 56.2544012,64.7027185 56.2840517,63.9488735 L56.2840517,58.8006635 L54.7088692,58.8006635 L54.7088692,58.7160857 L56.358178,58.7160857 L56.358178,63.9488735 C56.358178,64.7284596 56.0394352,64.8056827 55.5020199,64.8056827 L55.3574738,64.8056827 L55.3574738,65.0152884 C55.7466365,65.0152884 56.4026537,64.9748382 56.9697194,64.9748382 C57.5367851,64.9748382 58.1372077,65.0116111 58.5226641,65.0152884 L58.5226641,64.8056827 L58.3781179,64.8056827 C57.9074163,64.8056827 57.5256662,64.6916866 57.5256662,63.9268097 L57.5256662,58.7087311 L59.174975,58.7087311 C59.8309922,58.7087311 59.9162374,59.2640024 59.9347689,59.6280543 L60.149735,59.5508312 C60.149735,59.297098 60.149735,59.0433648 60.1682666,58.7859543 C60.1867981,58.5285438 60.2349802,58.2895197 60.268337,58.0504957 L60.1200845,58.0247547 C60.0756088,58.3079062 59.8013417,58.3262927 59.5567251,58.3189381 L54.4642526,58.3189381 C54.2344613,58.3189381 53.9824321,58.3189381 53.8934806,58.0357865 L53.7341092,58.0357865 C53.7333539,58.3003911 53.7135364,58.5646037 53.6748082,58.8264045 C53.648864,59.0948469 53.600682,59.3522574 53.5636188,59.6059906 L53.7748786,59.6059906 C53.9008932,58.9293687 53.9861384,58.7087311 54.7199881,58.719763 L54.7199881,58.8043407 C53.9787258,58.8043407 54.0120826,58.9404006 53.8490049,59.6574727 L53.8490049,59.6905683 L53.4783737,59.6905683 L53.4783737,59.6427635 C53.5191431,59.3706438 53.5673252,59.0948469 53.5969756,58.8153726 C53.6372754,58.5438714 53.6570974,58.2697648 53.6562766,57.9953363 L53.6562766,57.9548861 L53.9676068,57.9548861 L53.9676068,57.9843044 C54.0343204,58.2306831 54.2122234,58.2306831 54.4642526,58.2343604 L59.567844,58.2343604 C59.8384048,58.2343604 60.0348393,58.2343604 60.0533709,57.9732726 L60.0533709,57.929145 L60.1015529,57.929145 L60.3795263,57.9732726 L60.3795263,58.0137228 C60.3387569,58.2748106 60.3016938,58.5358984 60.2720433,58.7969862 C60.2423928,59.058074 60.2535117,59.3191617 60.2535117,59.5802495 L60.2535117,59.6096679 L60.2238612,59.6096679 L59.8754679,59.7383731 L59.8754679,59.6795364 C59.8495237,59.3118072 59.8124606,58.7859543 59.1935065,58.7859543 L57.6294429,58.7859543 L57.6294429,63.9121005 C57.6294429,64.6475591 57.9333605,64.6990412 58.3966495,64.7063958 L58.6264408,64.7063958 L58.6264408,65.0741251 L58.5782588,65.0888343 Z" id="路径" fill="#231815"></path>
<path d="M60.6908565,64.7578779 L60.8279901,64.7578779 C61.1800897,64.7578779 61.5507209,64.7100731 61.5507209,64.2026067 L61.5507209,59.1132333 C61.5507209,58.6057669 61.1800897,58.5579621 60.8279901,58.5579621 L60.6908565,58.5579621 L60.6908565,58.267456 C61.0614877,58.267456 61.6582039,58.3042289 62.1400245,58.3042289 C62.621845,58.3042289 63.2185612,58.267456 63.6781439,58.267456 L63.6781439,58.5579621 L63.5410103,58.5579621 C63.1889107,58.5579621 62.799748,58.6057669 62.799748,59.1132333 L62.799748,64.2026067 C62.799748,64.7100731 63.1703792,64.7578779 63.5410103,64.7578779 L63.6781439,64.7578779 L63.6781439,65.048384 C63.2074423,65.048384 62.6181387,65.0116111 62.1326118,65.0116111 C61.647085,65.0116111 61.0726066,65.048384 60.6908565,65.048384 L60.6908565,64.7578779 Z" id="路径" fill="#231815"></path>
<path d="M63.6336681,65.0042565 L63.6336681,64.7983281 L63.5410103,64.7983281 C63.1889107,64.7983281 62.7738038,64.7394914 62.7738038,64.2026067 L62.7738038,59.1132333 C62.7738038,58.5763486 63.1889107,58.5175119 63.5410103,58.5138346 L63.6336681,58.5138346 L63.6336681,58.3079062 C63.1814981,58.3079062 62.6144324,58.3483564 62.1511434,58.3483564 C61.6878544,58.3483564 61.124495,58.3115835 60.7464512,58.3079062 L60.7464512,58.5138346 L60.839109,58.5138346 C61.1875023,58.5138346 61.6063156,58.5763486 61.6063156,59.1132333 L61.6063156,64.2026067 C61.6063156,64.7394914 61.1875023,64.7983281 60.839109,64.7983281 L60.7464512,64.7983281 L60.7464512,65.0042565 C61.1170824,65.0042565 61.6767355,64.9674836 62.1437308,64.9674836 C62.6107261,64.9674836 63.1814981,65.0042565 63.6447871,65.0042565 M63.6892628,65.0888343 C63.2148549,65.0888343 62.6292576,65.048384 62.1437308,65.048384 C61.6582039,65.048384 61.0837256,65.0888343 60.7019755,65.0888343 L60.6389682,65.0888343 L60.6389682,64.721105 L60.8168711,64.721105 C61.1689708,64.721105 61.4951262,64.684332 61.4988325,64.2099613 L61.4988325,59.1132333 C61.4988325,58.6388626 61.1689708,58.6057669 60.8168711,58.6020896 L60.6389682,58.6020896 L60.6389682,58.2343604 L60.6797376,58.2343604 C61.0503688,58.2343604 61.6507913,58.2748106 62.1289055,58.2748106 C62.6070198,58.2748106 63.203736,58.2343604 63.6670249,58.2343604 L63.7077944,58.2343604 L63.7077944,58.6020896 L63.5298914,58.6020896 C63.1592602,58.6020896 62.8516363,58.6388626 62.84793,59.1132333 L62.84793,64.2026067 C62.84793,64.6769775 63.1703792,64.7100731 63.5298914,64.7137504 L63.7077944,64.7137504 L63.7077944,65.0814797 L63.6892628,65.0888343 Z" id="形状" fill="#231815"></path>
<path d="M67.7291427,64.7652325 C69.6304807,64.7652325 69.9529298,63.1030961 69.9529298,61.6873384 C69.9529298,60.2715806 69.182017,58.5469302 67.5549461,58.5469302 C65.84263,58.5469302 65.331159,60.0656522 65.331159,61.3637366 C65.331159,63.1030961 66.1354286,64.7652325 67.7180238,64.7652325 M67.5697713,58.1203643 C69.6601312,58.1203643 71.3242652,59.4037395 71.3242652,61.4740554 C71.3692349,62.466313 70.9947389,63.4321616 70.2910898,64.138679 C69.5874408,64.8451964 68.6185066,65.2282545 67.6179534,65.1954757 C65.5424187,65.1954757 63.9561173,63.7944272 63.9561173,61.7057248 C63.9420116,60.7512464 64.3176914,59.8317041 64.9976596,59.1563672 C65.6776277,58.4810303 66.6040405,58.1073485 67.566065,58.1203643" id="形状" fill="#231815"></path>
<path d="M65.2978022,61.3637366 C65.2978022,60.0582976 65.8166858,58.50648 67.5623587,58.50648 C69.2264927,58.50648 70.0011119,60.2605487 70.0048479,61.6873384 C70.0085245,63.114128 69.6675438,64.8056827 67.7291427,64.8056827 L67.7291427,64.721105 C69.5822987,64.721105 69.9158667,63.0994188 69.919573,61.6836611 C69.9232793,60.2679033 69.1560728,58.5910578 67.5623587,58.5873805 C65.8833994,58.5873805 65.3867536,60.0582976 65.3830473,61.3600593 C65.3830473,63.0920642 66.1799044,64.7174277 67.7291427,64.721105 L67.7291427,64.8020054 C66.1057781,64.8020054 65.3015085,63.1104507 65.2978022,61.3600593 M63.9264668,61.7020475 C63.9144051,60.7368038 64.2955492,59.8075951 64.9835186,59.1250122 C65.671488,58.4424294 66.6080294,58.0642695 67.5808903,58.0762368 L67.5808903,58.1608145 C66.6300228,58.146791 65.7139529,58.5153792 65.0415138,59.1825534 C64.3690747,59.8497276 63.9975778,60.758625 64.0117119,61.7020475 C64.0117119,63.7686861 65.5757755,65.1476709 67.6327786,65.1476709 C68.6209098,65.1794344 69.5775628,64.8005725 70.2720551,64.1024421 C70.9665474,63.4043118 71.335849,62.4502731 71.2909084,61.4703781 C71.2909084,59.4258032 69.6527186,58.1608145 67.5845966,58.1608145 L67.5845966,58.0762368 C69.6897817,58.0762368 71.3761536,59.3743211 71.3798599,61.4703781 C71.4224874,62.4720904 71.0434494,63.4462956 70.3334898,64.1597631 C69.6235302,64.8732305 68.6465648,65.2617286 67.6364849,65.2322487 C65.5424187,65.2322487 63.9301731,63.8128136 63.9301731,61.7020475" id="形状" fill="#231815"></path>
<path d="M77.9326193,63.1325144 L77.9326193,63.114128 L77.9326193,59.473608 C77.9678274,59.2341447 77.8921237,58.991716 77.7266262,58.8139499 C77.5611288,58.6361838 77.323551,58.5421066 77.0801675,58.5579621 L76.8652014,58.5579621 L76.8652014,58.267456 C77.3247841,58.267456 77.7769542,58.3042289 78.2365368,58.3042289 C78.6961195,58.3042289 79.0408065,58.267456 79.4410882,58.267456 L79.4410882,58.5579621 L79.2928357,58.5579621 C78.8851414,58.5579621 78.4218524,58.6351853 78.4218524,59.7935325 L78.4218524,64.206284 C78.4164602,64.5345823 78.4362845,64.8628112 78.4811534,65.1881212 L78.1105222,65.1881212 L73.0662318,59.6096679 L73.0662318,63.6179171 C73.0662318,64.4636945 73.2330158,64.7542006 73.9853971,64.7542006 L74.1521812,64.7542006 L74.1521812,65.0447067 C73.7333679,65.0447067 73.3108484,65.0079338 72.8920351,65.0079338 C72.4732219,65.0079338 71.998814,65.0447067 71.5577629,65.0447067 L71.5577629,64.7542006 L71.6948964,64.7542006 C72.3731515,64.7542006 72.5769986,64.294539 72.5769986,63.5223075 L72.5769986,59.4331578 C72.5780289,59.1988085 72.4838906,58.9739226 72.3158286,58.8092522 C72.1477666,58.6445817 71.9199447,58.5540046 71.6837775,58.5579621 L71.5577629,58.5579621 L71.5577629,58.267456 C71.9283941,58.267456 72.2990252,58.3042289 72.6696564,58.3042289 C72.9624551,58.3042289 73.2441348,58.267456 73.5406397,58.267456 L77.9326193,63.1325144 Z" id="路径" fill="#231815"></path>
<path d="M78.1290538,65.1513482 L78.4477966,65.1513482 C78.410662,64.8401955 78.3945654,64.526925 78.3996145,64.2136386 L78.3996145,59.8008871 C78.3996145,58.631508 78.892554,58.5248665 79.3113673,58.5211892 L79.4188503,58.5211892 L79.4188503,58.3152608 C79.0296876,58.3152608 78.6442311,58.355711 78.2550684,58.355711 C77.8066046,58.355711 77.3692598,58.3189381 76.9282087,58.3152608 L76.9282087,58.5211892 L77.0986991,58.5211892 C77.3523939,58.5094027 77.5983632,58.6094934 77.7706576,58.7946252 C77.942952,58.979757 78.0239448,59.2309896 77.9919202,59.4809626 L77.9919202,63.1582555 L77.9733887,63.176642 L77.9437382,63.2097376 L73.5739965,58.3079062 C73.2849042,58.3079062 72.9995182,58.3483564 72.7030132,58.3483564 C72.332382,58.3483564 71.9617509,58.3115835 71.6170639,58.3079062 L71.6170639,58.5138346 L71.702309,58.5138346 C71.9493346,58.5118738 72.1869221,58.6078645 72.3622957,58.780484 C72.5376693,58.9531036 72.6363075,59.1880585 72.6362996,59.4331578 L72.6362996,63.5259848 C72.6362996,64.3018936 72.4176272,64.7983281 71.713428,64.8020054 L71.6170639,64.8020054 L71.6170639,65.0116111 C72.0432897,65.0116111 72.4806345,64.9711609 72.9105667,64.9711609 C73.3404989,64.9711609 73.722249,65.0079338 74.1299433,65.0116111 L74.1299433,64.8056827 L74.0039287,64.8056827 C73.2330158,64.8056827 73.0402876,64.4710491 73.0402876,63.6252717 L73.0402876,59.5067037 L78.1290538,65.1513482 Z M78.499685,65.235926 L78.095697,65.235926 L73.1255328,59.7420504 L73.1255328,63.6179171 C73.1255328,64.4636945 73.2663726,64.6990412 74.0039287,64.721105 L74.2151885,64.721105 L74.2151885,65.0888343 L74.1707127,65.0888343 C73.7481932,65.0888343 73.3293799,65.0520613 72.9105667,65.0520613 C72.4917535,65.0520613 72.0210519,65.0888343 71.5762944,65.0888343 L71.535525,65.0888343 L71.535525,64.721105 L71.713428,64.721105 C72.3583262,64.721105 72.5436418,64.3055709 72.5547608,63.5333394 L72.5547608,59.4331578 C72.554796,59.2101725 72.4645141,58.996527 72.3042075,58.8402416 C72.1439009,58.6839563 71.9270188,58.598143 71.702309,58.6020896 L71.535525,58.6020896 L71.535525,58.2343604 L71.5762944,58.2343604 C71.9469256,58.2343604 72.3175568,58.2748106 72.688188,58.2748106 C72.9772803,58.2748106 73.25896,58.2343604 73.5925281,58.2490695 L77.8955561,63.0552913 L77.8955561,59.4846399 C77.8955561,58.7124084 77.3692598,58.6167988 77.0838738,58.6131215 L76.824432,58.6131215 L76.824432,58.2453922 L76.8689077,58.2453922 C77.3321967,58.2453922 77.7806605,58.2858425 78.2402431,58.2858425 C78.6405248,58.2858425 79.0371002,58.2453922 79.4447945,58.2453922 L79.4855639,58.2453922 L79.4855639,58.6131215 L79.296542,58.6131215 C78.892554,58.6131215 78.4774471,58.657249 78.4700345,59.8045644 L78.4700345,64.2173158 C78.4633575,64.5430772 78.4819439,64.8688655 78.5256291,65.1917985 L78.5256291,65.2396033 L78.499685,65.235926 Z" id="形状" fill="#231815"></path>
<path d="M82.4654386,59.3559347 L82.4469071,59.3559347 L81.5314481,62.1065497 L83.3660724,62.1065497 L82.4654386,59.3559347 Z M81.0236833,63.6583673 C80.9276259,63.9034231 80.8556367,64.1571072 80.8087172,64.4158897 C80.8087172,64.7100731 81.2201179,64.7578779 81.5499796,64.7578779 L81.6574627,64.7578779 L81.6574627,65.048384 C81.2645936,65.0263203 80.8643119,65.0116111 80.4714429,65.0116111 C80.0785738,65.0116111 79.7672436,65.0116111 79.415144,65.048384 L79.415144,64.7578779 L79.4707387,64.7578779 C79.8651174,64.7588326 80.2126332,64.500986 80.3231904,64.1253835 L81.8946666,59.6648273 C82.0509509,59.2746651 82.1749054,58.8725073 82.2652978,58.4623525 C82.5760936,58.3518244 82.8752564,58.2114198 83.158519,58.0431411 C83.1841706,58.0376746 83.2106998,58.0376746 83.2363515,58.0431411 C83.266002,58.0431411 83.2845336,58.0431411 83.3030651,58.0725595 L83.3920166,58.3152608 L85.1932842,63.3972795 C85.3081798,63.7355905 85.4267818,64.0959652 85.5639153,64.3864713 C85.6725381,64.6398492 85.9370558,64.7913184 86.2125199,64.7578779 L86.2718209,64.7578779 L86.2718209,65.048384 C85.8307698,65.0263203 85.3897187,65.0116111 84.9190171,65.0116111 C84.4483155,65.0116111 83.9516697,65.0116111 83.4364923,65.048384 L83.4364923,64.7578779 L83.5439754,64.7578779 C83.7700604,64.7578779 84.1518105,64.7174277 84.1518105,64.4747263 C84.1150489,64.2348069 84.049089,64.0001981 83.955376,63.7760407 L83.5847448,62.6471118 L81.3609577,62.6471118 L81.0236833,63.6583673 Z" id="形状" fill="#231815"></path>
<path d="M81.590749,62.0624222 L83.3067714,62.0624222 L82.4543197,59.4588989 L81.590749,62.0624222 Z M81.4721471,62.147 L82.4135503,59.3118072 L82.4654386,59.3118072 L82.4654386,59.3559347 L82.4654386,59.3559347 L82.4654386,59.3118072 L82.5062081,59.3118072 L83.432786,62.147 L81.4721471,62.147 Z M83.4735555,65.0042565 C83.9590823,65.0042565 84.4371965,64.9674836 84.9004855,64.9674836 C85.3637745,64.9674836 85.7825877,64.9858701 86.2125199,65.0042565 L86.2125199,64.7983281 L86.2125199,64.7983281 C85.9158037,64.8284921 85.6352753,64.658484 85.5268522,64.382794 C85.3971313,64.0922879 85.2822356,63.7319132 85.156221,63.3899249 L83.3549535,58.3079062 C83.325303,58.2306831 83.2956525,58.1461053 83.2734146,58.0762368 C83.2734146,58.0762368 83.2734146,58.0762368 83.2734146,58.0762368 L83.2474704,58.0762368 C83.229089,58.0729269 83.2102572,58.0729269 83.1918758,58.0762368 C82.9177252,58.2407107 82.6238697,58.3704279 82.3171862,58.4623525 C82.225554,58.8757806 82.1016362,59.2815075 81.946555,59.6758592 L80.3787851,64.1364154 C80.260674,64.527775 79.8973146,64.795543 79.4855639,64.7946508 L79.4855639,64.7946508 L79.4855639,65.0005792 C79.819132,65.0005792 80.1564064,64.9638063 80.4973871,64.9638063 C80.8383677,64.9638063 81.2645936,64.9821928 81.6389311,65.0005792 L81.6389311,64.7946508 L81.5759238,64.7946508 C81.257181,64.7946508 80.8087172,64.7578779 80.7975983,64.4269215 C80.84476,64.1642075 80.9180077,63.9067685 81.0162707,63.6583673 L81.0570401,63.6583673 L81.0162707,63.6583673 L81.3387198,62.6176934 L83.6181016,62.6176934 L84.0109707,63.7723634 C84.1056318,64.0012882 84.1716036,64.2408792 84.2074052,64.4857582 C84.2074052,64.7836189 83.7811793,64.8056827 83.5588006,64.80936 L83.492087,64.80936 L83.4735555,65.0042565 Z M86.249583,65.0888343 C85.8085319,65.0888343 85.3674808,65.0520613 84.9004855,65.0520613 C84.4334902,65.0520613 83.9331381,65.0741251 83.4364923,65.0888343 L83.3920166,65.0888343 L83.3920166,64.721105 L83.5402691,64.721105 C83.7663541,64.721105 84.1036285,64.6696229 84.1073348,64.4820809 C84.0663779,64.2466547 84.0005363,64.0161647 83.9109003,63.7944272 L83.5402691,62.6912393 L81.3943145,62.6912393 L81.0941033,63.6730765 C80.9991685,63.9109332 80.9271907,64.1571893 80.8791372,64.4085351 C80.8791372,64.6475591 81.2497684,64.7063958 81.5722175,64.7063958 L81.72047,64.7063958 L81.72047,65.0741251 L81.6759942,65.0741251 C81.305363,65.0741251 80.8828435,65.0373522 80.4936807,65.0373522 C80.104518,65.0373522 79.7931878,65.0594159 79.4373819,65.0741251 L79.3966124,65.0741251 L79.3966124,64.7063958 L79.4929765,64.7063958 C79.8695121,64.7070785 80.2014887,64.4615427 80.3083651,64.1033198 L81.8761351,59.6427635 C82.0206328,59.2398726 82.144337,58.8299354 82.2467662,58.4145477 C82.556627,58.3017934 82.8555833,58.1614856 83.1399874,57.9953363 C83.1696663,57.9842705 83.2009596,57.9780609 83.2326452,57.9769498 C83.276303,57.9726894 83.3184047,57.9942956 83.3401282,58.0321092 C83.3697787,58.1093324 83.3957229,58.1975874 83.4290797,58.2748106 L85.2303473,63.3568293 C85.3489493,63.6951403 85.4638449,64.055515 85.6009785,64.3460211 C85.6944919,64.5924996 85.9451456,64.7441406 86.2088136,64.7137504 L86.308884,64.7137504 L86.308884,65.0814797 L86.249583,65.0888343 Z" id="形状" fill="#231815"></path>
<path d="M88.721693,64.0959652 C88.721693,64.4857582 88.9922538,64.5997543 89.3072903,64.6402045 C89.7375957,64.6784648 90.1704878,64.6784648 90.6007932,64.6402045 C90.9713667,64.5955062 91.3087666,64.4062951 91.53849,64.1143516 C91.6798165,63.9006784 91.7826515,63.6642677 91.8424076,63.415666 L92.1426189,63.415666 C92.0351358,63.9672599 91.8980023,64.5188539 91.7719877,65.0667705 C90.9121233,65.0667705 90.0485527,65.0299976 89.1775694,65.0299976 C88.3065861,65.0299976 87.4541344,65.0667705 86.5831511,65.0667705 L86.5831511,64.7762644 L86.7202846,64.7762644 C87.0909158,64.7762644 87.461547,64.7284596 87.461547,64.1253835 L87.461547,59.1132333 C87.461547,58.6057669 87.0909158,58.5579621 86.7202846,58.5579621 L86.5831511,58.5579621 L86.5831511,58.267456 C87.1020348,58.267456 87.6135058,58.3042289 88.1323895,58.3042289 C88.6512731,58.3042289 89.1182684,58.267456 89.6149142,58.267456 L89.6149142,58.5579621 L89.3702976,58.5579621 C88.9996664,58.5579621 88.7068678,58.5579621 88.7068678,59.083815 L88.721693,64.0959652 Z" id="路径" fill="#231815"></path>
<path d="M91.727512,65.0042565 C91.8424076,64.4784036 91.9721285,63.9635826 92.0759053,63.4377298 L91.8757644,63.4377298 C91.813996,63.6810736 91.711245,63.9123183 91.5718469,64.1217062 C91.3342373,64.4218448 90.9863592,64.6162421 90.6044995,64.6622683 C90.3450576,64.6880093 90.0930284,64.7027185 89.8632371,64.7027185 C89.6334458,64.7027185 89.4926059,64.7027185 89.3109966,64.6806547 C88.9922538,64.6475591 88.6846299,64.5114993 88.6846299,64.0959652 L88.6846299,59.083815 C88.6846299,58.543253 89.0293169,58.5138346 89.3962418,58.5138346 L89.5963826,58.5138346 L89.5963826,58.3079062 C89.1145621,58.3079062 88.6401542,58.3483564 88.150921,58.3483564 C87.6616879,58.3483564 87.1502168,58.3115835 86.6498647,58.3079062 L86.6498647,58.5138346 L86.7425225,58.5138346 C87.0909158,58.5138346 87.5171417,58.5763486 87.5171417,59.1132333 L87.5171417,64.1069971 C87.5171417,64.7358141 87.0946221,64.7983281 86.7425225,64.7983281 L86.6498647,64.7983281 L86.6498647,65.0042565 C87.4949038,65.0042565 88.3399429,64.9674836 89.1923946,64.9674836 C90.0448464,64.9674836 90.8935918,65.0042565 91.7386309,65.0042565 M91.775694,65.0888343 C90.9121233,65.0888343 90.0485527,65.0520613 89.1812757,65.0520613 C88.3139987,65.0520613 87.4578407,65.0888343 86.5868574,65.0888343 L86.546088,65.0888343 L86.546088,64.721105 L86.723991,64.721105 C87.0946221,64.721105 87.413365,64.6880093 87.4170713,64.1143516 L87.4170713,59.1132333 C87.4170713,58.6388626 87.0797969,58.6057669 86.723991,58.6020896 L86.546088,58.6020896 L86.546088,58.2343604 L86.5831511,58.2343604 C87.1020348,58.2343604 87.6135058,58.2748106 88.1286831,58.2748106 C88.6438605,58.2748106 89.1145621,58.2343604 89.6112079,58.2343604 L89.6519773,58.2343604 L89.6519773,58.6020896 L89.3665913,58.6020896 C88.9959601,58.6020896 88.7513435,58.6020896 88.7439309,59.083815 L88.7439309,64.0959652 C88.7439309,64.4636945 88.9848412,64.5556268 89.2924651,64.5997543 C89.4629554,64.5997543 89.6445647,64.6181408 89.8335866,64.6181408 C90.081145,64.6184549 90.3285569,64.6061811 90.574849,64.5813678 C90.9343791,64.5382931 91.2621407,64.3558664 91.4866017,64.0739014 C91.6245491,63.8633019 91.7248543,63.6306744 91.7831066,63.3862477 L91.7831066,63.353152 L92.1537378,63.353152 L92.1537378,63.4046341 C92.0425485,63.9599053 91.9091212,64.507822 91.7831066,65.0557386 L91.7831066,65.0888343 L91.775694,65.0888343 Z" id="形状" fill="#231815"></path>
<path d="M91.8424076,59.0176237 L91.9647159,59.0176237 C92.0981431,59.0176237 92.1574441,58.9183368 92.1574441,58.7565359 C92.1574441,58.5947351 92.0647863,58.5358984 91.9573033,58.5358984 L91.8424076,58.5358984 L91.8424076,59.0176237 Z M91.4940143,59.5545085 L91.4940143,59.5067037 C91.6200289,59.4883172 91.6459731,59.5067037 91.6459731,59.4147714 L91.6459731,58.657249 C91.6459731,58.5506075 91.6459731,58.5138346 91.4977206,58.5211892 L91.4977206,58.4697071 L92.0277232,58.4697071 C92.2093325,58.4697071 92.3761165,58.5579621 92.3761165,58.7418268 C92.374098,58.8877348 92.2739067,59.0142527 92.1314999,59.0507194 L92.3019903,59.2860661 C92.3640869,59.3782064 92.4403772,59.4601038 92.5280753,59.5287674 L92.5280753,59.572895 L92.3279345,59.572895 C92.2315704,59.572895 92.1463252,59.3743211 91.9573033,59.1058788 L91.8424076,59.1058788 L91.8424076,59.4441897 C91.8424076,59.510381 91.8646455,59.5067037 91.9943664,59.5250901 L91.9943664,59.572895 L91.4940143,59.5545085 Z M92.0054853,59.8450146 C92.3464885,59.8539973 92.6589178,59.6569082 92.7953896,59.3467211 C92.9318613,59.036534 92.8651101,58.6752206 92.6266248,58.4332248 C92.3881394,58.191229 92.0255665,58.1168987 91.7099441,58.2452982 C91.3943217,58.3736976 91.1887075,58.679174 91.1900877,59.0176237 C91.1879966,59.4682953 91.5513248,59.8369716 92.0054853,59.8450146 M92.0054853,58.0063682 C92.5740512,58.0063718 93.0351534,58.4633204 93.0358334,59.0274343 C93.0365119,59.5915481 92.5765116,60.0495885 92.0079473,60.0509431 C91.4393831,60.0522917 90.9771758,59.5964435 90.9751306,59.0323329 C90.9700793,58.7593648 91.0768893,58.4960478 91.2711132,58.3026514 C91.4653371,58.109255 91.7303468,58.0023366 92.0054853,58.0063682" id="形状" fill="#231815"></path>
<path d="M30.8513394,23.9693454 C30.8533867,14.8835207 38.2783246,7.51946082 47.4358494,7.52081461 C56.5933741,7.52216841 64.0160999,14.8884232 64.0154178,23.9742482 C64.0147357,33.0600732 56.5909039,40.425231 47.433379,40.425231 C43.0349066,40.425231 38.8166382,38.6913736 35.7067961,35.6051907 C32.596954,32.5190078 30.8503567,28.3333795 30.8513394,23.9693454 L30.8513394,23.9693454 Z" id="路径" fill="#FFFFFF"></path>
<path d="M47.4556169,40.7159274 C37.9526333,40.7598646 30.1323153,33.1221274 30.1323153,23.8222537 C30.1323153,13.6618935 37.9526333,6.63090956 47.4556169,6.63458541 L51.9031911,6.63458541 C61.2949854,6.63458541 69.8639784,13.6582162 69.8639784,23.8222537 C69.8639784,33.1184501 61.2949854,40.7159274 51.9031911,40.7159274 L47.4556169,40.7159274 Z M47.4963863,8.04298611 C38.812672,8.04908174 31.7775334,15.0373677 31.7816241,23.653093 C31.7857183,32.2688184 38.8274927,39.2505221 47.511209,39.2484934 C56.1949252,39.2464638 63.2333865,32.26147 63.2333865,23.6457436 C63.2333865,19.505724 61.5750321,15.5354298 58.6234521,12.6090286 C55.671872,9.68262732 51.6690755,8.04006283 47.4963863,8.04298611 Z M43.927208,32.8720715 L43.927208,14.4194158 C40.0756054,15.8841933 37.5327489,19.5531866 37.5327489,23.6457436 C37.5327489,27.7383007 40.0756054,31.407294 43.927208,32.8720715 L43.927208,32.8720715 Z M57.4626589,23.6457436 C57.4579163,19.5518878 54.9138052,15.8832595 51.0618583,14.4157386 L51.0618583,32.8757487 C54.9131238,31.40737 57.4568652,27.7392747 57.4626589,23.6457436 Z" id="形状" fill="#0E72B2"></path>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 65 KiB

+354
View File
@@ -0,0 +1,354 @@
<svg width="360" height="270" viewBox="0 0 360 270" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M34.2733 49.5165V220.485C34.2733 224.083 37.2177 227.028 40.8163 227.028H222.319C273.125 227.028 314.346 185.839 314.346 135.001C314.346 84.1943 273.157 42.9735 222.319 42.9735H40.8163C37.2177 42.9735 34.2733 45.9179 34.2733 49.5165Z" fill="white"/>
<path d="M56.4218 114.294H44.8408V154.795H56.3891C62.5068 154.795 66.956 153.356 70.8491 150.117C75.4619 146.289 78.21 140.531 78.21 134.577C78.21 122.636 69.2788 114.294 56.4545 114.294H56.4218ZM65.6474 144.719C63.1611 146.943 59.955 147.925 54.8515 147.925H52.725V121.131H54.8515C59.955 121.131 63.063 122.047 65.6474 124.403C68.3955 126.824 70.0312 130.619 70.0312 134.479C70.0312 138.339 68.3955 142.265 65.6474 144.686V144.719Z" fill="#261F20"/>
<path d="M89.6569 114.294H81.7726V154.795H89.6569V114.294Z" fill="#261F20"/>
<path d="M108.993 129.83C104.249 128.063 102.875 126.918 102.875 124.726C102.875 122.174 105.361 120.244 108.764 120.244C111.119 120.244 113.082 121.226 115.143 123.516L119.265 118.118C115.863 115.141 111.806 113.636 107.357 113.636C100.192 113.636 94.7289 118.609 94.7289 125.25C94.7289 130.844 97.2807 133.69 104.707 136.34C107.782 137.42 109.385 138.172 110.17 138.63C111.741 139.644 112.526 141.116 112.526 142.818C112.526 146.089 109.909 148.543 106.375 148.543C102.613 148.543 99.5708 146.645 97.7387 143.145L92.6352 148.052C96.2665 153.384 100.65 155.773 106.67 155.773C114.881 155.773 120.639 150.309 120.639 142.49C120.639 136.046 117.989 133.134 108.993 129.862V129.83Z" fill="#261F20"/>
<path d="M123.127 134.572C123.127 146.48 132.483 155.706 144.489 155.706C147.892 155.706 150.803 155.052 154.402 153.35V144.059C151.229 147.233 148.448 148.509 144.849 148.509C136.899 148.509 131.24 142.751 131.24 134.539C131.24 126.753 137.063 120.636 144.489 120.636C148.252 120.636 151.098 121.977 154.402 125.183V115.892C150.934 114.125 148.088 113.406 144.686 113.406C132.712 113.406 123.127 122.827 123.127 134.605V134.572Z" fill="#261F20"/>
<path d="M217.05 141.513L206.254 114.294H197.617L214.793 155.842H219.045L236.548 114.294H227.977L217.05 141.513Z" fill="#261F20"/>
<path d="M240.121 154.795H262.53V147.958H248.038V137.031H262.007V130.161H248.038V121.164H262.53V114.294H240.121V154.795Z" fill="#261F20"/>
<path d="M293.803 126.267C293.803 118.677 288.569 114.326 279.474 114.326H267.762V154.827H275.646V138.567H276.693L287.62 154.827H297.336L284.578 137.75C290.532 136.539 293.803 132.482 293.803 126.267ZM277.969 132.94H275.646V120.672H278.067C282.975 120.672 285.657 122.733 285.657 126.692C285.657 130.65 282.975 132.94 277.936 132.94H277.969Z" fill="#261F20"/>
<path d="M300.114 116.416C300.114 115.696 299.623 115.303 298.772 115.303H297.627V118.869H298.478V117.495L299.492 118.869H300.539L299.361 117.397C299.852 117.266 300.146 116.906 300.146 116.416H300.114ZM298.609 116.906H298.478V115.958H298.641C299.067 115.958 299.296 116.121 299.296 116.416C299.296 116.71 299.067 116.906 298.641 116.906H298.609Z" fill="#261F20"/>
<path d="M298.908 113.994C297.174 113.994 295.8 115.368 295.8 117.101C295.8 118.835 297.206 120.209 298.908 120.209C300.609 120.209 301.983 118.803 301.983 117.101C301.983 115.4 300.609 113.994 298.908 113.994ZM298.908 119.653C297.534 119.653 296.421 118.508 296.421 117.101C296.421 115.695 297.534 114.55 298.908 114.55C300.282 114.55 301.361 115.727 301.361 117.101C301.361 118.475 300.249 119.653 298.908 119.653Z" fill="#261F20"/>
<path d="M199.876 134.508C199.876 146.351 190.291 155.969 178.415 155.969C166.54 155.969 156.954 146.384 156.954 134.508C156.954 122.633 166.54 113.047 178.415 113.047C190.291 113.047 199.876 122.633 199.876 134.508Z" fill="url(#paint0_linear_2812_87301)"/>
<path style="mix-blend-mode:multiply" opacity="0.65" d="M199.876 134.508C199.876 146.351 190.291 155.969 178.415 155.969C166.54 155.969 156.954 146.384 156.954 134.508C156.954 122.633 166.54 113.047 178.415 113.047C190.291 113.047 199.876 122.633 199.876 134.508Z" fill="url(#paint1_linear_2812_87301)"/>
<g style="mix-blend-mode:multiply" opacity="0.5">
<mask id="mask0_2812_87301" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="156" y="113" width="44" height="43">
<path d="M199.878 134.507C199.878 146.35 190.26 155.968 178.417 155.968C166.574 155.968 156.956 146.383 156.956 134.507C156.956 122.632 166.541 113.046 178.417 113.046C190.292 113.046 199.878 122.632 199.878 134.507Z" fill="white"/>
</mask>
<g mask="url(#mask0_2812_87301)">
<path d="M178.511 112.753C190.583 112.753 200.398 122.535 200.398 134.606C200.398 146.678 190.583 156.493 178.511 156.493C166.439 156.493 156.625 146.711 156.625 134.606C156.625 122.535 166.407 112.72 178.511 112.72V112.753Z" fill="#020000"/>
<path d="M178.549 112.784C190.621 112.784 200.436 122.533 200.436 134.605C200.436 146.677 190.621 156.524 178.549 156.524C166.478 156.524 156.663 146.742 156.663 134.67C156.663 122.599 166.478 112.784 178.549 112.784Z" fill="#030100"/>
<path d="M178.542 112.816C190.614 112.816 200.461 122.565 200.461 134.637C200.461 146.708 190.647 156.556 178.542 156.556C166.438 156.556 156.656 146.774 156.689 134.702C156.689 122.63 166.503 112.816 178.542 112.849V112.816Z" fill="#050100"/>
<path d="M178.58 112.848C190.652 112.848 200.499 122.564 200.499 134.636C200.499 146.708 190.652 156.555 178.58 156.555C166.509 156.555 156.694 146.773 156.727 134.701C156.727 122.629 166.541 112.815 178.58 112.848Z" fill="#070200"/>
<path d="M178.607 112.886C190.679 112.886 200.526 122.57 200.526 134.642C200.526 146.714 190.679 156.594 178.607 156.594C166.535 156.594 156.72 146.812 156.753 134.74C156.753 122.668 166.568 112.854 178.607 112.886Z" fill="#080200"/>
<path d="M178.645 112.918C190.717 112.918 200.564 122.602 200.564 134.674C200.564 146.745 190.717 156.625 178.645 156.625C166.573 156.625 156.759 146.844 156.791 134.772C156.791 122.7 166.606 112.885 178.645 112.918Z" fill="#0A0300"/>
<path d="M178.645 112.95C190.717 112.95 200.597 122.601 200.597 134.673C200.597 146.745 190.717 156.657 178.645 156.657C166.573 156.657 156.759 146.875 156.791 134.804C156.791 122.732 166.606 112.917 178.645 112.95Z" fill="#0C0300"/>
<path d="M178.67 112.983C190.742 112.983 200.622 122.634 200.622 134.673C200.622 146.712 190.742 156.657 178.67 156.657C166.599 156.657 156.784 146.875 156.817 134.804C156.817 122.732 166.631 112.917 178.67 112.95V112.983Z" fill="#0E0400"/>
<path d="M178.709 113.014C190.78 113.047 200.66 122.632 200.66 134.704C200.66 146.776 190.748 156.721 178.709 156.721C166.669 156.721 156.822 146.939 156.855 134.868C156.855 122.796 166.669 112.981 178.709 113.014Z" fill="#0F0400"/>
<path d="M178.709 113.079C190.78 113.111 200.66 122.664 200.66 134.736C200.66 146.808 190.748 156.753 178.709 156.753C166.669 156.753 156.822 146.971 156.855 134.899C156.888 122.828 166.669 113.013 178.709 113.046V113.079Z" fill="#110500"/>
<path d="M178.74 113.11C190.811 113.143 200.724 122.695 200.724 134.734C200.724 146.773 190.811 156.784 178.74 156.784C166.668 156.784 156.853 147.002 156.919 134.963C156.952 122.892 166.733 113.077 178.74 113.143V113.11Z" fill="#130500"/>
<path d="M178.779 113.148C190.818 113.181 200.763 122.701 200.763 134.773C200.763 146.844 190.818 156.822 178.779 156.822C166.74 156.822 156.892 147.041 156.958 135.002C156.991 122.93 166.772 113.115 178.779 113.181V113.148Z" fill="#140600"/>
<path d="M178.804 113.18C190.843 113.213 200.789 122.7 200.789 134.772C200.789 146.844 190.843 156.822 178.804 156.822C166.765 156.822 156.918 147.04 156.983 135.001C157.016 122.929 166.831 113.115 178.804 113.18Z" fill="#160600"/>
<path d="M178.804 113.212C190.843 113.278 200.821 122.732 200.821 134.771C200.821 146.811 190.876 156.854 178.804 156.854C166.732 156.854 156.95 147.072 156.983 135.033C157.016 122.961 166.83 113.147 178.804 113.212Z" fill="#180600"/>
<path d="M178.842 113.244C190.881 113.31 200.859 122.732 200.859 134.803C200.859 146.875 190.881 156.886 178.842 156.886C166.803 156.886 156.989 147.104 157.021 135.065C157.087 122.993 166.868 113.179 178.842 113.244Z" fill="#190700"/>
<path d="M178.868 113.276C190.907 113.342 200.885 122.763 200.885 134.802C200.885 146.842 190.907 156.918 178.868 156.918C166.829 156.918 157.014 147.136 157.047 135.097C157.112 123.025 166.894 113.211 178.868 113.276Z" fill="#1B0700"/>
<path d="M178.868 113.309C190.907 113.374 200.885 122.763 200.885 134.802C200.885 146.842 190.907 156.918 178.868 156.918C166.829 156.918 157.014 147.169 157.047 135.097C157.112 123.025 166.894 113.211 178.868 113.276V113.309Z" fill="#1D0800"/>
<path d="M178.906 113.34C190.945 113.406 200.956 122.762 200.956 134.834C200.956 146.906 190.945 156.982 178.906 156.982C166.867 156.982 157.052 147.233 157.085 135.161C157.15 123.057 166.932 113.275 178.906 113.34Z" fill="#1F0800"/>
<path d="M178.938 113.41C190.977 113.476 200.988 122.832 200.988 134.871C200.988 146.91 190.977 157.019 178.938 157.019C166.899 157.019 157.084 147.27 157.15 135.231C157.215 123.126 166.997 113.345 178.938 113.443V113.41Z" fill="#200900"/>
<path d="M178.963 113.442C191.002 113.507 201.013 122.831 201.013 134.87C201.013 146.909 191.002 157.051 178.963 157.051C166.924 157.051 157.11 147.302 157.175 135.263C157.241 123.158 167.022 113.377 178.963 113.475V113.442Z" fill="#220900"/>
<path d="M178.963 113.475C191.002 113.54 201.013 122.864 201.013 134.87C201.013 146.877 190.97 157.051 178.963 157.051C166.957 157.051 157.11 147.302 157.175 135.263C157.241 123.158 167.022 113.376 178.963 113.475Z" fill="#240A00"/>
<path d="M179.001 113.507C191.04 113.605 201.084 122.863 201.084 134.902C201.084 146.941 191.04 157.083 179.001 157.083C166.962 157.083 157.148 147.334 157.213 135.295C157.279 123.19 167.06 113.409 179.001 113.507Z" fill="#250A00"/>
<path d="M179.04 113.539C191.079 113.637 201.122 122.862 201.122 134.902C201.122 146.941 191.079 157.115 179.04 157.115C167 157.115 157.186 147.366 157.251 135.327C157.317 123.222 167.131 113.441 179.04 113.539Z" fill="#270B00"/>
<path d="M179.04 113.571C191.079 113.669 201.122 122.895 201.122 134.901C201.122 146.907 191.046 157.115 179.04 157.115C167.033 157.115 157.186 147.365 157.251 135.326C157.317 123.222 167.131 113.473 179.04 113.538V113.571Z" fill="#290B00"/>
<path d="M179.065 113.602C191.104 113.7 201.18 122.893 201.18 134.932C201.18 146.971 191.104 157.179 179.065 157.179C167.026 157.179 157.212 147.429 157.277 135.39C157.375 123.286 167.157 113.537 179.065 113.602Z" fill="#2A0C00"/>
<path d="M179.103 113.635C191.142 113.733 201.218 122.893 201.218 134.932C201.218 146.971 191.142 157.179 179.103 157.179C167.064 157.179 157.25 147.429 157.315 135.39C157.413 123.286 167.195 113.537 179.103 113.602V113.635Z" fill="#2C0C00"/>
<path d="M179.129 113.666C191.135 113.764 201.244 122.924 201.244 134.931C201.244 146.937 191.135 157.21 179.129 157.21C167.122 157.21 157.275 147.46 157.341 135.454C157.439 123.35 167.22 113.601 179.129 113.666Z" fill="#2E0D00"/>
<path d="M179.135 113.704C191.142 113.802 201.251 122.93 201.251 134.969C201.251 147.008 191.142 157.248 179.135 157.248C167.129 157.248 157.282 147.499 157.38 135.492C157.478 123.388 167.26 113.639 179.135 113.737V113.704Z" fill="#300D00"/>
<path d="M179.161 113.768C191.167 113.866 201.309 122.993 201.309 135C201.309 147.006 191.2 157.311 179.161 157.311C167.122 157.311 157.307 147.562 157.405 135.556C157.504 123.451 167.285 113.702 179.161 113.801V113.768Z" fill="#310D00"/>
<path d="M179.199 113.8C191.205 113.898 201.347 122.993 201.347 135C201.347 147.006 191.205 157.311 179.199 157.311C167.193 157.311 157.345 147.562 157.444 135.556C157.542 123.451 167.324 113.702 179.199 113.8Z" fill="#330E00"/>
<path d="M179.199 113.833C191.205 113.964 201.347 122.993 201.347 135.032C201.347 147.071 191.205 157.343 179.199 157.343C167.193 157.343 157.345 147.594 157.444 135.588C157.542 123.484 167.324 113.734 179.199 113.833Z" fill="#350E00"/>
<path d="M179.237 113.865C191.244 113.995 201.418 123.025 201.418 135.031C201.418 147.037 191.276 157.375 179.237 157.375C167.198 157.375 157.384 147.626 157.482 135.62C157.58 123.515 167.362 113.766 179.237 113.865Z" fill="#360F00"/>
<path d="M179.263 113.897C191.269 114.028 201.443 123.025 201.443 135.031C201.443 147.037 191.269 157.375 179.263 157.375C167.256 157.375 157.409 147.626 157.507 135.62C157.605 123.515 167.387 113.766 179.263 113.865V113.897Z" fill="#380F00"/>
<path d="M179.301 113.928C191.307 114.059 201.481 123.056 201.481 135.062C201.481 147.069 191.307 157.439 179.301 157.439C167.294 157.439 157.447 147.69 157.545 135.684C157.644 123.579 167.458 113.83 179.301 113.928Z" fill="#3A1000"/>
<path d="M179.301 113.961C191.307 114.092 201.481 123.056 201.481 135.062C201.481 147.069 191.307 157.439 179.301 157.439C167.294 157.439 157.447 147.69 157.545 135.684C157.676 123.579 167.458 113.83 179.301 113.928V113.961Z" fill="#3B1000"/>
<path d="M179.333 113.998C191.34 114.129 201.547 123.06 201.547 135.066C201.547 147.073 191.34 157.476 179.333 157.476C167.327 157.476 157.48 147.727 157.61 135.753C157.741 123.649 167.523 113.9 179.333 114.031V113.998Z" fill="#3D1100"/>
<path d="M179.359 114.03C191.365 114.161 201.572 123.092 201.572 135.098C201.572 147.105 191.365 157.508 179.359 157.508C167.352 157.508 157.505 147.792 157.636 135.785C157.767 123.681 167.549 113.932 179.359 114.063V114.03Z" fill="#3F1100"/>
<path d="M179.359 114.063C191.365 114.194 201.572 123.093 201.572 135.099C201.572 147.105 191.365 157.509 179.359 157.509C167.352 157.509 157.505 147.792 157.636 135.786C157.767 123.681 167.549 113.932 179.359 114.063Z" fill="#411200"/>
<path d="M179.397 114.127C191.403 114.258 201.61 123.124 201.61 135.13C201.61 147.136 191.37 157.573 179.397 157.573C167.423 157.573 157.543 147.856 157.674 135.85C157.805 123.745 167.587 113.996 179.397 114.127Z" fill="#421200"/>
<path d="M179.422 114.159C191.429 114.322 201.668 123.156 201.668 135.162C201.668 147.168 191.429 157.604 179.422 157.604C167.416 157.604 157.569 147.888 157.7 135.882C157.83 123.777 167.612 114.028 179.422 114.159Z" fill="#441300"/>
<path d="M179.46 114.191C191.467 114.354 201.707 123.155 201.707 135.161C201.707 147.167 191.467 157.636 179.46 157.636C167.454 157.636 157.607 147.92 157.738 135.913C157.869 123.809 167.65 114.06 179.46 114.191Z" fill="#461300"/>
<path d="M179.46 114.223C191.434 114.387 201.707 123.187 201.707 135.161C201.707 147.135 191.434 157.636 179.46 157.636C167.487 157.636 157.607 147.92 157.738 135.913C157.869 123.809 167.65 114.06 179.46 114.191V114.223Z" fill="#471300"/>
<path d="M179.487 114.255C191.46 114.418 201.766 123.186 201.766 135.192C201.766 147.199 191.493 157.7 179.487 157.7C167.48 157.7 157.633 147.984 157.764 135.978C157.895 123.873 167.677 114.124 179.487 114.255Z" fill="#491400"/>
<path d="M179.525 114.292C191.499 114.455 201.804 123.19 201.804 135.197C201.804 147.203 191.531 157.704 179.525 157.704C167.519 157.704 157.671 147.988 157.802 136.014C157.966 123.91 167.715 114.161 179.525 114.324V114.292Z" fill="#4B1400"/>
<path d="M179.531 114.324C191.505 114.488 201.81 123.222 201.81 135.196C201.81 147.17 191.505 157.737 179.531 157.737C167.557 157.737 157.678 148.02 157.841 136.047C158.005 123.942 167.786 114.193 179.531 114.357V114.324Z" fill="#4C1500"/>
<path d="M179.557 114.358C191.53 114.521 201.835 123.223 201.835 135.197C201.835 147.171 191.53 157.738 179.557 157.738C167.583 157.738 157.703 148.021 157.867 136.048C158.03 123.943 167.812 114.194 179.557 114.358Z" fill="#4E1500"/>
<path d="M179.595 114.39C191.568 114.553 201.906 123.255 201.906 135.229C201.906 147.203 191.601 157.77 179.595 157.77C167.588 157.77 157.741 148.053 157.905 136.08C158.068 123.975 167.85 114.226 179.595 114.39Z" fill="#501600"/>
<path d="M179.62 114.454C191.594 114.617 201.932 123.287 201.932 135.26C201.932 147.234 191.594 157.834 179.62 157.834C167.647 157.834 157.767 148.117 157.93 136.144C158.094 124.039 167.876 114.29 179.62 114.454Z" fill="#521600"/>
<path d="M179.62 114.486C191.594 114.683 201.932 123.287 201.932 135.26C201.932 147.234 191.594 157.834 179.62 157.834C167.647 157.834 157.767 148.117 157.93 136.144C158.094 124.039 167.876 114.29 179.62 114.454V114.486Z" fill="#531700"/>
<path d="M179.659 114.517C191.633 114.714 201.971 123.318 201.971 135.291C201.971 147.265 191.633 157.897 179.659 157.897C167.686 157.897 157.806 148.181 157.969 136.207C158.133 124.103 167.915 114.354 179.659 114.517Z" fill="#551700"/>
<path d="M179.685 114.55C191.658 114.746 202.029 123.318 202.029 135.291C202.029 147.265 191.658 157.897 179.685 157.897C167.711 157.897 157.831 148.181 157.995 136.207C158.158 124.103 167.94 114.354 179.685 114.517V114.55Z" fill="#571800"/>
<path d="M179.685 114.582C191.658 114.779 202.029 123.317 202.029 135.291C202.029 147.264 191.658 157.929 179.685 157.929C167.711 157.929 157.831 148.213 157.995 136.239C158.158 124.135 167.94 114.386 179.685 114.55V114.582Z" fill="#581800"/>
<path d="M179.716 114.618C191.69 114.814 202.061 123.352 202.061 135.326C202.061 147.3 191.69 157.965 179.716 157.965C167.743 157.965 157.863 148.248 158.059 136.308C158.223 124.203 168.004 114.454 179.716 114.65V114.618Z" fill="#5A1900"/>
<path d="M179.755 114.65C191.728 114.846 202.132 123.352 202.132 135.326C202.132 147.299 191.728 157.997 179.755 157.997C167.781 157.997 157.901 148.281 158.097 136.34C158.294 124.235 168.043 114.486 179.755 114.683V114.65Z" fill="#5C1900"/>
<path d="M179.793 114.685C191.766 114.881 202.17 123.387 202.17 135.328C202.17 147.269 191.766 157.999 179.793 157.999C167.819 157.999 157.939 148.283 158.136 136.342C158.332 124.237 168.114 114.488 179.793 114.685Z" fill="#5E1A00"/>
<path d="M179.793 114.716C191.766 114.913 202.17 123.386 202.17 135.359C202.17 147.333 191.766 158.031 179.793 158.031C167.819 158.031 157.939 148.347 158.136 136.374C158.332 124.269 168.114 114.52 179.793 114.716Z" fill="#5F1A00"/>
<path d="M179.818 114.749C191.759 114.945 202.195 123.385 202.195 135.359C202.195 147.333 191.759 158.063 179.818 158.063C167.877 158.063 157.965 148.379 158.161 136.406C158.357 124.301 168.139 114.552 179.818 114.749Z" fill="#611A00"/>
<path d="M179.857 114.813C191.798 115.042 202.267 123.45 202.267 135.391C202.267 147.332 191.831 158.095 179.857 158.095C167.884 158.095 158.004 148.411 158.2 136.438C158.396 124.333 168.178 114.584 179.857 114.78V114.813Z" fill="#631B00"/>
<path d="M179.857 114.844C191.798 115.073 202.267 123.448 202.267 135.422C202.267 147.396 191.798 158.159 179.857 158.159C167.916 158.159 158.004 148.475 158.2 136.502C158.396 124.397 168.178 114.648 179.857 114.844Z" fill="#641B00"/>
<path d="M179.883 114.877C191.824 115.106 202.292 123.481 202.292 135.422C202.292 147.363 191.824 158.159 179.883 158.159C167.942 158.159 158.029 148.475 158.225 136.502C158.422 124.397 168.203 114.648 179.883 114.844V114.877Z" fill="#661C00"/>
<path d="M179.915 114.911C191.856 115.141 202.324 123.483 202.324 135.424C202.324 147.365 191.856 158.193 179.915 158.193C167.974 158.193 158.061 148.51 158.29 136.569C158.486 124.464 168.268 114.715 179.915 114.944V114.911Z" fill="#681C00"/>
<path d="M179.953 114.944C191.894 115.173 202.395 123.482 202.395 135.456C202.395 147.43 191.894 158.226 179.953 158.226C168.012 158.226 158.099 148.542 158.328 136.601C158.525 124.497 168.306 114.748 179.953 114.977V114.944Z" fill="#691D00"/>
<path d="M179.953 114.976C191.894 115.205 202.395 123.514 202.395 135.455C202.395 147.396 191.894 158.258 179.953 158.258C168.012 158.258 158.099 148.574 158.328 136.633C158.525 124.528 168.306 114.779 179.953 115.008V114.976Z" fill="#6B1D00"/>
<path d="M179.978 115.011C191.919 115.24 202.421 123.517 202.421 135.458C202.421 147.399 191.919 158.26 179.978 158.26C168.037 158.26 158.125 148.577 158.354 136.636C158.583 124.531 168.332 114.782 179.978 115.011Z" fill="#6D1E00"/>
<path d="M180.017 115.044C191.958 115.273 202.493 123.517 202.493 135.49C202.493 147.464 191.958 158.293 180.017 158.293C168.076 158.293 158.164 148.609 158.393 136.668C158.622 124.564 168.371 114.815 180.017 115.044Z" fill="#6F1E00"/>
<path d="M180.017 115.075C191.958 115.337 202.493 123.549 202.493 135.489C202.493 147.43 191.958 158.324 180.017 158.324C168.076 158.324 158.164 148.641 158.393 136.7C158.622 124.595 168.404 114.846 180.017 115.075Z" fill="#701F00"/>
<path d="M180.054 115.108C191.995 115.37 202.53 123.549 202.53 135.489C202.53 147.43 191.995 158.324 180.054 158.324C168.114 158.324 158.234 148.641 158.43 136.7C158.659 124.595 168.441 114.846 180.054 115.075V115.108Z" fill="#721F00"/>
<path d="M180.08 115.171C192.021 115.433 202.555 123.612 202.555 135.553C202.555 147.494 191.988 158.42 180.08 158.42C168.172 158.42 158.259 148.737 158.455 136.796C158.684 124.691 168.466 114.942 180.08 115.171Z" fill="#742000"/>
<path d="M180.118 115.204C192.059 115.466 202.626 123.612 202.626 135.553C202.626 147.494 192.059 158.421 180.118 158.421C168.177 158.421 158.297 148.737 158.494 136.796C158.723 124.692 168.504 114.943 180.118 115.204Z" fill="#752000"/>
<path d="M180.112 115.237C192.053 115.499 202.62 123.612 202.62 135.553C202.62 147.494 192.053 158.454 180.112 158.454C168.171 158.454 158.291 148.77 158.52 136.862C158.749 124.757 168.531 115.008 180.112 115.27V115.237Z" fill="#772000"/>
<path d="M180.15 115.275C192.091 115.536 202.658 123.65 202.658 135.558C202.658 147.466 192.058 158.458 180.15 158.458C168.242 158.458 158.329 148.775 158.558 136.867C158.787 124.762 168.569 115.013 180.15 115.275Z" fill="#792100"/>
<path d="M180.176 115.307C192.084 115.568 202.716 123.649 202.716 135.59C202.716 147.531 192.117 158.49 180.176 158.49C168.235 158.49 158.355 148.807 158.584 136.898C158.813 124.794 168.594 115.045 180.176 115.307Z" fill="#7A2100"/>
<path d="M180.176 115.338C192.084 115.6 202.716 123.681 202.716 135.589C202.716 147.497 192.117 158.522 180.176 158.522C168.235 158.522 158.355 148.838 158.584 136.93C158.845 124.826 168.594 115.077 180.176 115.338Z" fill="#7C2200"/>
<path d="M180.215 115.371C192.123 115.633 202.755 123.681 202.755 135.589C202.755 147.497 192.123 158.522 180.215 158.522C168.306 158.522 158.394 148.838 158.623 136.93C158.884 124.826 168.633 115.077 180.215 115.338V115.371Z" fill="#7E2200"/>
<path d="M180.24 115.402C192.148 115.697 202.781 123.679 202.781 135.62C202.781 147.561 192.148 158.586 180.24 158.586C168.332 158.586 158.419 148.935 158.648 136.994C158.91 124.89 168.659 115.141 180.24 115.402Z" fill="#802300"/>
<path d="M180.278 115.435C192.186 115.73 202.851 123.712 202.851 135.62C202.851 147.529 192.219 158.586 180.278 158.586C168.337 158.586 158.457 148.935 158.686 136.994C158.948 124.89 168.697 115.141 180.278 115.402V115.435Z" fill="#812300"/>
<path d="M180.278 115.499C192.186 115.794 202.851 123.743 202.851 135.652C202.851 147.56 192.186 158.65 180.278 158.65C168.37 158.65 158.457 148.999 158.686 137.058C158.948 124.954 168.73 115.205 180.278 115.466V115.499Z" fill="#832400"/>
<path d="M180.31 115.531C192.219 115.826 202.884 123.776 202.884 135.684C202.884 147.592 192.219 158.682 180.31 158.682C168.402 158.682 158.489 149.031 158.751 137.123C159.013 125.019 168.795 115.27 180.31 115.564V115.531Z" fill="#852400"/>
<path d="M180.348 115.564C192.257 115.858 202.922 123.775 202.922 135.683C202.922 147.591 192.257 158.715 180.348 158.715C168.44 158.715 158.528 149.064 158.789 137.155C159.051 125.051 168.833 115.302 180.348 115.596V115.564Z" fill="#862500"/>
<path d="M180.348 115.602C192.257 115.896 202.954 123.781 202.954 135.689C202.954 147.597 192.257 158.72 180.348 158.72C168.44 158.72 158.528 149.069 158.789 137.161C159.051 125.056 168.833 115.307 180.348 115.602Z" fill="#882500"/>
<path d="M180.375 115.634C192.283 115.928 202.981 123.812 202.981 135.721C202.981 147.629 192.283 158.752 180.375 158.752C168.467 158.752 158.554 149.101 158.816 137.193C159.077 125.088 168.859 115.339 180.375 115.634Z" fill="#8A2600"/>
<path d="M180.413 115.666C192.321 115.96 203.019 123.812 203.019 135.72C203.019 147.628 192.321 158.784 180.413 158.784C168.505 158.784 158.592 149.133 158.854 137.225C159.116 125.121 168.897 115.371 180.413 115.666Z" fill="#8B2600"/>
<path d="M180.438 115.699C192.347 115.993 203.077 123.812 203.077 135.72C203.077 147.628 192.347 158.784 180.438 158.784C168.53 158.784 158.618 149.133 158.879 137.225C159.174 125.121 168.923 115.371 180.438 115.666V115.699Z" fill="#8D2600"/>
<path d="M180.438 115.73C192.347 116.057 203.077 123.843 203.077 135.751C203.077 147.659 192.347 158.848 180.438 158.848C168.53 158.848 158.618 149.197 158.879 137.289C159.174 125.184 168.923 115.435 180.438 115.73Z" fill="#8F2700"/>
<path d="M180.477 115.762C192.385 116.09 203.115 123.843 203.115 135.751C203.115 147.659 192.385 158.848 180.477 158.848C168.568 158.848 158.656 149.197 158.917 137.289C159.212 125.184 168.961 115.435 180.477 115.73V115.762Z" fill="#912700"/>
<path d="M180.509 115.793C192.384 116.121 203.148 123.874 203.148 135.75C203.148 147.625 192.384 158.879 180.509 158.879C168.633 158.879 158.688 149.228 158.982 137.353C159.277 125.248 169.026 115.499 180.509 115.826V115.793Z" fill="#922800"/>
<path d="M180.509 115.864C192.384 116.191 203.18 123.912 203.18 135.82C203.18 147.728 192.417 158.949 180.509 158.949C168.601 158.949 158.688 149.299 158.982 137.423C159.277 125.318 169.059 115.569 180.509 115.897V115.864Z" fill="#942800"/>
<path d="M180.535 115.896C192.411 116.223 203.207 123.911 203.207 135.819C203.207 147.727 192.443 158.981 180.535 158.981C168.627 158.981 158.714 149.33 159.009 137.455C159.303 125.35 169.085 115.601 180.535 115.928V115.896Z" fill="#962900"/>
<path d="M180.573 115.929C192.449 116.256 203.245 123.944 203.245 135.82C203.245 147.695 192.449 158.982 180.573 158.982C168.698 158.982 158.752 149.331 159.047 137.456C159.341 125.351 169.123 115.602 180.573 115.929Z" fill="#972900"/>
<path d="M180.611 115.961C192.487 116.288 203.283 123.943 203.283 135.852C203.283 147.76 192.487 159.014 180.611 159.014C168.736 159.014 158.791 149.363 159.085 137.487C159.379 125.383 169.161 115.634 180.611 115.961Z" fill="#992A00"/>
<path d="M180.599 115.993C192.474 116.32 203.303 123.976 203.303 135.851C203.303 147.727 192.507 159.046 180.599 159.046C168.69 159.046 158.778 149.395 159.072 137.52C159.367 125.415 169.149 115.666 180.599 115.993Z" fill="#9B2A00"/>
<path d="M180.637 116.026C192.512 116.353 203.341 123.976 203.341 135.851C203.341 147.727 192.512 159.046 180.637 159.046C168.761 159.046 158.816 149.395 159.111 137.52C159.405 125.415 169.187 115.666 180.637 115.993V116.026Z" fill="#9C2B00"/>
<path d="M180.675 116.057C192.551 116.417 203.379 123.974 203.379 135.882C203.379 147.79 192.551 159.11 180.675 159.11C168.8 159.11 158.854 149.459 159.149 137.583C159.476 125.479 169.225 115.73 180.675 116.057Z" fill="#9E2B00"/>
<path d="M180.675 116.09C192.551 116.449 203.412 124.007 203.412 135.882C203.412 147.758 192.583 159.11 180.675 159.11C168.767 159.11 158.854 149.459 159.149 137.583C159.476 125.479 169.225 115.73 180.675 116.057V116.09Z" fill="#A02C00"/>
<path d="M180.708 116.119C192.584 116.479 203.445 124.004 203.445 135.879C203.445 147.755 192.584 159.139 180.708 159.139C168.833 159.139 158.887 149.521 159.215 137.646C159.542 125.541 169.291 115.792 180.708 116.152V116.119Z" fill="#A22C00"/>
<path d="M180.734 116.154C192.609 116.514 203.471 124.006 203.471 135.882C203.471 147.757 192.609 159.142 180.734 159.142C168.858 159.142 158.913 149.524 159.24 137.648C159.567 125.544 169.316 115.795 180.734 116.154Z" fill="#A32D00"/>
<path d="M180.772 116.221C192.647 116.581 203.509 124.073 203.509 135.949C203.509 147.824 192.647 159.242 180.772 159.242C168.896 159.242 158.951 149.623 159.278 137.748C159.605 125.643 169.354 115.894 180.772 116.254V116.221Z" fill="#A52D00"/>
<path d="M180.772 116.25C192.647 116.61 203.542 124.069 203.542 135.945C203.542 147.82 192.647 159.238 180.772 159.238C168.896 159.238 158.951 149.619 159.278 137.744C159.605 125.639 169.387 115.89 180.772 116.25Z" fill="#A72D00"/>
<path d="M180.797 116.283C192.673 116.643 203.567 124.102 203.567 135.945C203.567 147.787 192.673 159.238 180.797 159.238C168.922 159.238 158.977 149.619 159.304 137.744C159.631 125.639 169.413 115.89 180.797 116.25V116.283Z" fill="#A82E00"/>
<path d="M180.836 116.321C192.711 116.681 203.605 124.107 203.605 135.982C203.605 147.858 192.711 159.308 180.836 159.308C168.96 159.308 159.015 149.69 159.342 137.814C159.669 125.71 169.451 115.961 180.836 116.321Z" fill="#AA2E00"/>
<path d="M180.836 116.353C192.678 116.713 203.605 124.107 203.605 135.982C203.605 147.858 192.678 159.308 180.836 159.308C168.993 159.308 159.015 149.69 159.342 137.814C159.669 125.71 169.451 115.961 180.836 116.321V116.353Z" fill="#AC2F00"/>
<path d="M180.861 116.385C192.704 116.778 203.663 124.139 203.663 135.981C203.663 147.824 192.737 159.34 180.861 159.34C168.986 159.34 159.04 149.722 159.367 137.846C159.694 125.742 169.476 115.993 180.861 116.353V116.385Z" fill="#AD2F00"/>
<path d="M180.894 116.42C192.737 116.812 203.697 124.14 203.697 136.016C203.697 147.891 192.77 159.374 180.894 159.374C169.019 159.374 159.074 149.756 159.433 137.913C159.793 125.809 169.542 116.06 180.894 116.452V116.42Z" fill="#AF3000"/>
<path d="M180.933 116.452C192.775 116.844 203.735 124.172 203.735 136.015C203.735 147.858 192.775 159.406 180.933 159.406C169.09 159.406 159.112 149.788 159.472 137.945C159.831 125.841 169.58 116.092 180.933 116.484V116.452Z" fill="#B13000"/>
<path d="M180.933 116.482C192.775 116.874 203.768 124.17 203.768 136.013C203.768 147.855 192.808 159.404 180.933 159.404C169.057 159.404 159.112 149.786 159.472 137.943C159.831 125.838 169.581 116.089 180.933 116.482Z" fill="#B33100"/>
<path d="M180.971 116.546C192.814 116.939 203.806 124.201 203.806 136.077C203.806 147.952 192.846 159.468 180.971 159.468C169.095 159.468 159.15 149.85 159.51 138.007C159.87 125.903 169.619 116.154 180.971 116.546Z" fill="#B43100"/>
<path d="M180.996 116.578C192.839 116.97 203.831 124.233 203.831 136.076C203.831 147.919 192.839 159.5 180.996 159.5C169.153 159.5 159.175 149.882 159.535 138.039C159.895 125.934 169.644 116.185 180.996 116.578Z" fill="#B63200"/>
<path d="M180.996 116.611C192.839 117.003 203.831 124.233 203.831 136.076C203.831 147.919 192.839 159.5 180.996 159.5C169.153 159.5 159.175 149.882 159.535 138.039C159.895 125.934 169.644 116.185 180.996 116.578V116.611Z" fill="#B83200"/>
<path d="M181.034 116.648C192.877 117.041 203.902 124.238 203.902 136.114C203.902 147.989 192.877 159.57 181.034 159.57C169.192 159.57 159.214 149.952 159.573 138.109C159.933 125.972 169.715 116.256 181.034 116.648Z" fill="#B93300"/>
<path d="M203.928 136.114C203.928 147.957 192.903 159.57 181.06 159.57C169.217 159.57 159.239 149.952 159.599 138.109C159.959 125.972 169.74 116.256 181.06 116.648C192.903 117.074 203.928 124.238 203.928 136.081V136.114Z" fill="#BB3300"/>
<path opacity="0.99" d="M181.06 116.711C192.903 117.103 203.928 124.3 203.928 136.176C203.928 148.051 192.935 159.633 181.06 159.633C169.184 159.633 159.239 150.014 159.599 138.172C159.959 126.067 169.74 116.351 181.06 116.711Z" fill="#BC3502"/>
<path opacity="0.98" d="M181.097 116.743C192.94 117.136 203.932 124.366 203.932 136.209C203.932 148.051 192.94 159.633 181.097 159.633C169.254 159.633 159.309 150.014 159.636 138.172C159.996 126.067 169.745 116.351 181.097 116.711V116.743Z" fill="#BC3705"/>
<path opacity="0.97" d="M181.097 116.742C192.94 117.135 203.932 124.398 203.932 136.241C203.932 148.083 192.94 159.664 181.097 159.664C169.254 159.664 159.309 150.046 159.636 138.203C159.996 126.099 169.745 116.383 181.097 116.742Z" fill="#BD3907"/>
<path opacity="0.96" d="M181.123 116.775C192.966 117.168 203.925 124.43 203.925 136.273C203.925 148.116 192.966 159.664 181.123 159.664C169.28 159.664 159.335 150.046 159.662 138.203C160.022 126.099 169.771 116.383 181.123 116.742V116.775Z" fill="#BD3A09"/>
<path opacity="0.95" d="M181.161 116.807C193.004 117.2 203.963 124.495 203.963 136.338C203.963 148.181 193.004 159.696 181.161 159.696C169.318 159.696 159.373 150.078 159.7 138.235C160.06 126.131 169.809 116.414 181.161 116.774V116.807Z" fill="#BE3C0C"/>
<path opacity="0.95" d="M181.161 116.839C193.004 117.232 203.963 124.527 203.963 136.37C203.963 148.213 193.036 159.728 181.161 159.728C169.285 159.728 159.373 150.11 159.7 138.267C160.06 126.163 169.809 116.447 181.161 116.806V116.839Z" fill="#BF3E0E"/>
<path opacity="0.94" d="M181.199 116.877C193.042 117.27 203.969 124.598 203.969 136.441C203.969 148.284 193.042 159.767 181.199 159.767C169.356 159.767 159.411 150.149 159.738 138.306C160.098 126.201 169.847 116.485 181.199 116.845V116.877Z" fill="#BF4010"/>
<path opacity="0.93" d="M181.199 116.909C193.042 117.302 203.969 124.63 203.969 136.473C203.969 148.316 193.042 159.798 181.199 159.798C169.356 159.798 159.411 150.18 159.738 138.337C160.065 126.233 169.814 116.517 181.199 116.877V116.909Z" fill="#C04213"/>
<path opacity="0.92" d="M181.225 116.909C193.067 117.269 203.961 124.663 203.961 136.505C203.961 148.348 193.067 159.798 181.225 159.798C169.382 159.798 159.436 150.18 159.764 138.337C160.091 126.233 169.84 116.517 181.225 116.877V116.909Z" fill="#C14415"/>
<path opacity="0.91" d="M181.225 116.939C193.067 117.299 203.961 124.692 203.961 136.568C203.961 148.443 193.067 159.861 181.225 159.861C169.382 159.861 159.436 150.243 159.764 138.4C160.091 126.295 169.84 116.612 181.225 116.939Z" fill="#C14617"/>
<path opacity="0.9" d="M181.263 116.972C193.106 117.331 204 124.758 204 136.6C204 148.443 193.106 159.861 181.263 159.861C169.42 159.861 159.475 150.243 159.802 138.4C160.129 126.295 169.878 116.612 181.263 116.939V116.972Z" fill="#C24719"/>
<path opacity="0.89" d="M181.288 117.002C193.13 117.362 203.992 124.821 203.992 136.664C203.992 148.507 193.13 159.924 181.288 159.924C169.445 159.924 159.532 150.306 159.827 138.463C160.154 126.359 169.903 116.675 181.288 117.002Z" fill="#C2491C"/>
<path opacity="0.88" d="M181.288 117.035C193.13 117.395 203.992 124.854 203.992 136.697C203.992 148.54 193.13 159.924 181.288 159.924C169.445 159.924 159.532 150.306 159.827 138.463C160.154 126.359 169.903 116.675 181.288 117.002V117.035Z" fill="#C34B1E"/>
<path opacity="0.87" d="M181.326 117.074C193.169 117.434 203.997 124.925 203.997 136.768C203.997 148.611 193.169 159.963 181.326 159.963C169.483 159.963 159.57 150.345 159.865 138.502C160.192 126.43 169.941 116.714 181.326 117.041V117.074Z" fill="#C44D20"/>
<path opacity="0.86" d="M181.326 117.106C193.169 117.466 203.997 124.957 203.997 136.8C203.997 148.643 193.169 159.995 181.326 159.995C169.483 159.995 159.57 150.377 159.865 138.534C160.192 126.462 169.908 116.746 181.326 117.073V117.106Z" fill="#C44F23"/>
<path opacity="0.85" d="M181.351 117.106C193.194 117.466 204.023 124.99 204.023 136.833C204.023 148.676 193.194 159.995 181.351 159.995C169.508 159.995 159.596 150.377 159.89 138.534C160.217 126.462 169.934 116.746 181.351 117.073V117.106Z" fill="#C55125"/>
<path opacity="0.85" d="M181.351 117.137C193.194 117.497 203.99 125.022 203.99 136.865C203.99 148.707 193.194 160.027 181.351 160.027C169.508 160.027 159.596 150.408 159.89 138.566C160.217 126.494 169.934 116.778 181.351 117.105V117.137Z" fill="#C65327"/>
<path opacity="0.84" d="M181.389 117.17C193.232 117.497 204.028 125.087 204.028 136.929C204.028 148.772 193.232 160.059 181.389 160.059C169.547 160.059 159.634 150.441 159.928 138.598C160.223 126.526 169.972 116.81 181.389 117.137V117.17Z" fill="#C6542A"/>
<path opacity="0.83" d="M181.389 117.199C193.232 117.526 203.995 125.116 203.995 136.992C203.995 148.867 193.232 160.121 181.389 160.121C169.547 160.121 159.634 150.503 159.928 138.66C160.223 126.588 169.972 116.905 181.389 117.199Z" fill="#C7562C"/>
<path opacity="0.82" d="M181.428 117.232C193.27 117.559 204.034 125.181 204.034 137.024C204.034 148.867 193.27 160.121 181.428 160.121C169.585 160.121 159.672 150.503 159.967 138.66C160.261 126.588 170.01 116.905 181.428 117.199V117.232Z" fill="#C7582E"/>
<path opacity="0.81" d="M181.453 117.27C193.296 117.598 204.059 125.253 204.059 137.096C204.059 148.938 193.296 160.16 181.453 160.16C169.61 160.16 159.698 150.542 159.992 138.699C160.286 126.627 170.036 116.943 181.453 117.238V117.27Z" fill="#C85A31"/>
<path opacity="0.8" d="M181.452 117.27C193.294 117.598 204.025 125.253 204.025 137.096C204.025 148.938 193.294 160.16 181.452 160.16C169.609 160.16 159.729 150.542 159.991 138.699C160.285 126.627 170.001 116.943 181.452 117.238V117.27Z" fill="#C95C33"/>
<path opacity="0.79" d="M181.49 117.302C193.333 117.629 204.063 125.317 204.063 137.16C204.063 149.003 193.333 160.192 181.49 160.192C169.647 160.192 159.767 150.573 160.029 138.731C160.323 126.659 170.04 116.975 181.49 117.27V117.302Z" fill="#C95E35"/>
<path opacity="0.78" d="M181.49 117.334C193.333 117.661 204.031 125.349 204.031 137.192C204.031 149.035 193.333 160.223 181.49 160.223C169.647 160.223 159.767 150.605 160.029 138.762C160.323 126.691 170.04 117.007 181.49 117.301V117.334Z" fill="#CA6038"/>
<path opacity="0.77" d="M181.516 117.366C193.359 117.693 204.057 125.414 204.057 137.257C204.057 149.1 193.359 160.256 181.516 160.256C169.673 160.256 159.794 150.637 160.055 138.795C160.35 126.723 170.066 117.039 181.516 117.334V117.366Z" fill="#CA613A"/>
<path opacity="0.76" d="M181.516 117.397C193.359 117.724 204.057 125.445 204.057 137.321C204.057 149.196 193.359 160.319 181.516 160.319C169.673 160.319 159.794 150.701 160.055 138.858C160.35 126.786 170.066 117.103 181.516 117.397Z" fill="#CB633C"/>
<path opacity="0.75" d="M181.554 117.43C193.397 117.724 204.062 125.51 204.062 137.353C204.062 149.196 193.397 160.319 181.554 160.319C169.712 160.319 159.832 150.701 160.093 138.858C160.388 126.819 170.104 117.103 181.554 117.397V117.43Z" fill="#CC653F"/>
<path opacity="0.75" d="M181.554 117.429C193.397 117.724 204.062 125.543 204.062 137.386C204.062 149.228 193.397 160.351 181.554 160.351C169.712 160.351 159.832 150.733 160.093 138.89C160.355 126.851 170.104 117.135 181.554 117.429Z" fill="#CC6741"/>
<path opacity="0.74" d="M181.593 117.461C193.435 117.755 204.1 125.574 204.1 137.417C204.1 149.26 193.468 160.35 181.593 160.35C169.717 160.35 159.87 150.732 160.132 138.889C160.393 126.85 170.11 117.166 181.593 117.428V117.461Z" fill="#CD6943"/>
<path opacity="0.73" d="M181.618 117.499C193.461 117.793 204.093 125.645 204.093 137.488C204.093 149.331 193.461 160.388 181.618 160.388C169.775 160.388 159.895 150.77 160.157 138.927C160.419 126.888 170.135 117.205 181.618 117.466V117.499Z" fill="#CE6B46"/>
<path opacity="0.72" d="M181.618 117.531C193.461 117.825 204.093 125.677 204.093 137.52C204.093 149.362 193.461 160.42 181.618 160.42C169.775 160.42 159.895 150.802 160.157 138.959C160.419 126.92 170.135 117.236 181.618 117.498V117.531Z" fill="#CE6C48"/>
<path opacity="0.71" d="M181.655 117.563C193.498 117.857 204.097 125.742 204.097 137.585C204.097 149.427 193.498 160.452 181.655 160.452C169.812 160.452 159.965 150.834 160.194 138.991C160.456 126.952 170.172 117.269 181.655 117.53V117.563Z" fill="#CF6E4A"/>
<path opacity="0.7" d="M181.655 117.595C193.498 117.889 204.097 125.774 204.097 137.616C204.097 149.459 193.498 160.484 181.655 160.484C169.812 160.484 159.965 150.866 160.194 139.023C160.456 126.984 170.172 117.3 181.655 117.562V117.595Z" fill="#CF704C"/>
<path opacity="0.69" d="M181.68 117.595C193.523 117.889 204.123 125.806 204.123 137.649C204.123 149.492 193.556 160.484 181.68 160.484C169.805 160.484 159.99 150.866 160.22 139.023C160.481 126.984 170.198 117.3 181.68 117.562V117.595Z" fill="#D0724F"/>
<path opacity="0.68" d="M181.68 117.626C193.523 117.921 204.09 125.838 204.09 137.713C204.09 149.589 193.523 160.548 181.68 160.548C169.838 160.548 159.99 150.93 160.219 139.087C160.481 127.048 170.198 117.365 181.68 117.626Z" fill="#D17451"/>
<path opacity="0.67" d="M181.72 117.659C193.562 117.921 204.129 125.903 204.129 137.746C204.129 149.589 193.562 160.548 181.72 160.548C169.877 160.548 160.03 150.93 160.259 139.087C160.52 127.048 170.204 117.365 181.72 117.626V117.659Z" fill="#D17653"/>
<path opacity="0.66" d="M181.72 117.69C193.562 117.952 204.097 125.967 204.097 137.81C204.097 149.652 193.562 160.612 181.72 160.612C169.877 160.612 160.03 150.994 160.259 139.151C160.52 127.112 170.204 117.428 181.72 117.69Z" fill="#D27856"/>
<path opacity="0.65" d="M181.745 117.728C193.588 117.989 204.122 126.005 204.122 137.847C204.122 149.69 193.588 160.617 181.745 160.617C169.902 160.617 160.055 150.999 160.284 139.156C160.513 127.117 170.229 117.466 181.745 117.695V117.728Z" fill="#D27958"/>
<path opacity="0.65" d="M181.783 117.76C193.626 118.022 204.16 126.07 204.16 137.912C204.16 149.755 193.659 160.649 181.783 160.649C169.908 160.649 160.093 151.031 160.322 139.188C160.551 127.149 170.267 117.498 181.783 117.727V117.76Z" fill="#D37B5A"/>
<path opacity="0.64" d="M181.783 117.76C193.626 118.022 204.127 126.07 204.127 137.912C204.127 149.755 193.626 160.649 181.783 160.649C169.94 160.649 160.093 151.031 160.322 139.188C160.551 127.182 170.267 117.498 181.783 117.727V117.76Z" fill="#D47D5D"/>
<path opacity="0.63" d="M181.821 117.792C193.664 118.054 204.166 126.134 204.166 137.977C204.166 149.82 193.664 160.681 181.821 160.681C169.979 160.681 160.131 151.063 160.36 139.22C160.589 127.214 170.306 117.53 181.821 117.759V117.792Z" fill="#D47F5F"/>
<path opacity="0.62" d="M181.82 117.824C193.663 118.085 204.132 126.166 204.132 138.009C204.132 149.852 193.663 160.713 181.82 160.713C169.978 160.713 160.163 151.095 160.359 139.252C160.588 127.246 170.272 117.562 181.82 117.791V117.824Z" fill="#D58161"/>
<path opacity="0.61" d="M181.846 117.855C193.689 118.117 204.157 126.23 204.157 138.073C204.157 149.916 193.689 160.745 181.846 160.745C170.003 160.745 160.189 151.126 160.385 139.284C160.614 127.277 170.297 117.594 181.846 117.823V117.855Z" fill="#D68364"/>
<path opacity="0.6" d="M181.846 117.887C193.689 118.149 204.157 126.295 204.157 138.137C204.157 149.98 193.689 160.809 181.846 160.809C170.003 160.809 160.189 151.191 160.385 139.348C160.614 127.341 170.297 117.658 181.846 117.887Z" fill="#D68566"/>
<path opacity="0.59" d="M181.884 117.92C193.727 118.149 204.163 126.327 204.163 138.17C204.163 150.013 193.727 160.809 181.884 160.809C170.041 160.809 160.227 151.191 160.423 139.348C160.652 127.341 170.336 117.658 181.884 117.887V117.92Z" fill="#D78668"/>
<path opacity="0.58" d="M181.91 117.951C193.753 118.18 204.189 126.392 204.189 138.234C204.189 150.077 193.753 160.84 181.91 160.84C170.067 160.84 160.253 151.222 160.449 139.38C160.678 127.373 170.362 117.69 181.91 117.919V117.951Z" fill="#D7886B"/>
<path opacity="0.57" d="M181.91 117.957C193.753 118.186 204.156 126.397 204.156 138.24C204.156 150.083 193.753 160.846 181.91 160.846C170.067 160.846 160.253 151.228 160.449 139.385C160.646 127.378 170.362 117.728 181.91 117.924V117.957Z" fill="#D88A6D"/>
<path opacity="0.56" d="M181.948 117.989C193.791 118.218 204.195 126.462 204.195 138.305C204.195 150.148 193.791 160.878 181.948 160.878C170.106 160.878 160.291 151.26 160.487 139.417C160.684 127.411 170.4 117.76 181.948 117.956V117.989Z" fill="#D98C6F"/>
<path opacity="0.55" d="M181.948 118.021C193.791 118.25 204.195 126.494 204.195 138.337C204.195 150.179 193.791 160.91 181.948 160.91C170.106 160.91 160.291 151.292 160.487 139.449C160.684 127.443 170.367 117.792 181.948 117.988V118.021Z" fill="#D98E72"/>
<path opacity="0.55" d="M181.974 118.053C193.817 118.282 204.187 126.558 204.187 138.401C204.187 150.244 193.817 160.942 181.974 160.942C170.131 160.942 160.317 151.324 160.513 139.481C160.709 127.474 170.393 117.823 181.974 118.02V118.053Z" fill="#DA9074"/>
<path opacity="0.54" d="M181.974 118.084C193.817 118.313 204.187 126.59 204.187 138.465C204.187 150.341 193.817 161.006 181.974 161.006C170.131 161.006 160.317 151.388 160.513 139.545C160.709 127.538 170.393 117.887 181.974 118.084Z" fill="#DB9276"/>
<path opacity="0.53" d="M182.011 118.116C193.854 118.345 204.192 126.655 204.192 138.498C204.192 150.341 193.854 161.006 182.011 161.006C170.168 161.006 160.387 151.388 160.55 139.545C160.746 127.538 170.43 117.887 182.011 118.084V118.116Z" fill="#DB9379"/>
<path opacity="0.52" d="M182.011 118.116C193.854 118.312 204.192 126.687 204.192 138.53C204.192 150.372 193.854 161.038 182.011 161.038C170.168 161.038 160.387 151.419 160.55 139.577C160.746 127.603 170.43 117.919 182.011 118.116Z" fill="#DC957B"/>
<path opacity="0.51" d="M182.049 118.148C193.892 118.345 204.23 126.72 204.23 138.562C204.23 150.405 193.892 161.038 182.049 161.038C170.207 161.038 160.425 151.419 160.588 139.577C160.785 127.603 170.468 117.919 182.049 118.116V118.148Z" fill="#DC977D"/>
<path opacity="0.5" d="M182.075 118.181C193.918 118.377 204.223 126.785 204.223 138.627C204.223 150.47 193.918 161.07 182.075 161.07C170.232 161.07 160.45 151.452 160.614 139.609C160.81 127.635 170.494 117.952 182.075 118.148V118.181Z" fill="#DD9980"/>
<path opacity="0.49" d="M182.075 118.218C193.918 118.414 204.223 126.822 204.223 138.665C204.223 150.507 193.918 161.107 182.075 161.107C170.232 161.107 160.45 151.489 160.614 139.646C160.81 127.672 170.461 118.022 182.075 118.185V118.218Z" fill="#DE9B82"/>
<path opacity="0.48" d="M182.114 118.25C193.957 118.446 204.229 126.886 204.229 138.729C204.229 150.572 193.957 161.139 182.114 161.139C170.271 161.139 160.489 151.521 160.653 139.678C160.816 127.704 170.5 118.053 182.114 118.217V118.25Z" fill="#DE9D84"/>
<path opacity="0.47" d="M182.114 118.282C193.957 118.478 204.229 126.919 204.229 138.762C204.229 150.604 193.957 161.171 182.114 161.171C170.271 161.171 160.489 151.553 160.653 139.71C160.816 127.737 170.5 118.086 182.114 118.249V118.282Z" fill="#DF9F86"/>
<path opacity="0.46" d="M182.139 118.282C193.982 118.478 204.255 126.951 204.255 138.794C204.255 150.637 193.982 161.171 182.139 161.171C170.297 161.171 160.515 151.553 160.678 139.71C160.842 127.737 170.526 118.086 182.139 118.249V118.282Z" fill="#DFA089"/>
<path opacity="0.45" d="M182.139 118.313C193.982 118.509 204.222 126.982 204.222 138.858C204.222 150.733 193.982 161.235 182.139 161.235C170.297 161.235 160.515 151.617 160.678 139.774C160.842 127.8 170.526 118.149 182.139 118.313Z" fill="#E0A28B"/>
<path opacity="0.45" d="M182.177 118.346C194.02 118.542 204.26 127.048 204.26 138.891C204.26 150.733 194.02 161.235 182.177 161.235C170.335 161.235 160.553 151.617 160.716 139.774C160.88 127.8 170.564 118.149 182.177 118.313V118.346Z" fill="#E1A48D"/>
<path opacity="0.44" d="M182.177 118.377C194.02 118.541 204.259 127.112 204.259 138.955C204.259 150.797 194.052 161.299 182.177 161.299C170.301 161.299 160.585 151.681 160.716 139.838C160.879 127.864 170.563 118.213 182.177 118.377Z" fill="#E1A690"/>
<path opacity="0.43" d="M182.202 118.41C194.045 118.573 204.252 127.145 204.252 138.987C204.252 150.83 194.045 161.299 182.202 161.299C170.359 161.299 160.61 151.681 160.741 139.838C160.905 127.864 170.556 118.213 182.202 118.377V118.41Z" fill="#E2A892"/>
<path opacity="0.42" d="M182.24 118.448C194.083 118.611 204.29 127.215 204.29 139.058C204.29 150.901 194.083 161.337 182.24 161.337C170.398 161.337 160.649 151.719 160.779 139.876C160.943 127.902 170.594 118.252 182.24 118.415V118.448Z" fill="#E3AA94"/>
<path opacity="0.41" d="M182.24 118.447C194.083 118.611 204.257 127.215 204.257 139.057C204.257 150.9 194.083 161.336 182.24 161.336C170.398 161.336 160.649 151.718 160.779 139.875C160.943 127.902 170.594 118.284 182.24 118.414V118.447Z" fill="#E3AC97"/>
<path opacity="0.4" d="M182.279 118.479C194.121 118.643 204.296 127.28 204.296 139.122C204.296 150.965 194.121 161.369 182.279 161.369C170.436 161.369 160.687 151.75 160.818 139.908C160.981 127.967 170.632 118.316 182.279 118.447V118.479Z" fill="#E4AD99"/>
<path opacity="0.39" d="M182.279 118.511C194.121 118.675 204.296 127.312 204.296 139.154C204.296 150.997 194.154 161.4 182.279 161.4C170.403 161.4 160.687 151.782 160.818 139.939C160.948 127.999 170.632 118.348 182.279 118.478V118.511Z" fill="#E4AF9B"/>
<path opacity="0.38" d="M182.304 118.543C194.147 118.707 204.288 127.376 204.288 139.219C204.288 151.062 194.147 161.432 182.304 161.432C170.461 161.432 160.712 151.814 160.843 139.971C160.974 128.03 170.658 118.379 182.304 118.51V118.543Z" fill="#E5B19E"/>
<path opacity="0.37" d="M182.304 118.574C194.147 118.738 204.288 127.44 204.288 139.283C204.288 151.126 194.147 161.496 182.304 161.496C170.461 161.496 160.712 151.878 160.843 140.035C160.974 128.094 170.658 118.443 182.304 118.574Z" fill="#E6B3A0"/>
<path opacity="0.36" d="M182.343 118.607C194.186 118.771 204.295 127.473 204.295 139.316C204.295 151.158 194.186 161.496 182.343 161.496C170.5 161.496 160.751 151.878 160.882 140.035C161.013 128.094 170.664 118.443 182.343 118.574V118.607Z" fill="#E6B5A2"/>
<path opacity="0.35" d="M182.369 118.639C194.211 118.77 204.32 127.537 204.32 139.38C204.32 151.223 194.211 161.528 182.369 161.528C170.526 161.528 160.777 151.91 160.908 140.067C161.038 128.126 170.689 118.475 182.369 118.606V118.639Z" fill="#E7B7A5"/>
<path opacity="0.35" d="M182.368 118.639C194.211 118.77 204.32 127.537 204.32 139.38C204.32 151.223 194.243 161.528 182.368 161.528C170.492 161.528 160.809 151.91 160.907 140.067C161.038 128.126 170.689 118.475 182.368 118.606V118.639Z" fill="#E8B9A7"/>
<path opacity="0.34" d="M182.406 118.677C194.249 118.808 204.325 127.608 204.325 139.451C204.325 151.294 194.249 161.566 182.406 161.566C170.563 161.566 160.847 151.948 160.945 140.105C161.076 128.164 170.727 118.513 182.406 118.644V118.677Z" fill="#E8BAA9"/>
<path opacity="0.33" d="M182.406 118.709C194.249 118.84 204.325 127.64 204.325 139.483C204.325 151.326 194.249 161.599 182.406 161.599C170.563 161.599 160.847 151.98 160.945 140.138C161.076 128.197 170.727 118.546 182.406 118.677V118.709Z" fill="#E9BCAC"/>
<path opacity="0.32" d="M182.432 118.74C194.274 118.871 204.318 127.704 204.318 139.547C204.318 151.39 194.274 161.63 182.432 161.63C170.589 161.63 160.872 152.012 160.971 140.169C161.101 128.228 170.752 118.61 182.432 118.708V118.74Z" fill="#E9BEAE"/>
<path opacity="0.31" d="M182.432 118.772C194.274 118.903 204.318 127.736 204.318 139.611C204.318 151.487 194.274 161.694 182.432 161.694C170.589 161.694 160.872 152.076 160.971 140.233C161.069 128.292 170.752 118.674 182.432 118.772Z" fill="#EAC0B0"/>
<path opacity="0.3" d="M182.47 118.805C194.313 118.935 204.356 127.801 204.356 139.644C204.356 151.487 194.345 161.694 182.47 161.694C170.594 161.694 160.911 152.076 161.009 140.233C161.107 128.292 170.758 118.674 182.47 118.772V118.805Z" fill="#EBC2B3"/>
<path opacity="0.29" d="M182.47 118.804C194.313 118.934 204.323 127.833 204.323 139.676C204.323 151.518 194.313 161.726 182.47 161.726C170.627 161.726 160.911 152.107 161.009 140.265C161.107 128.324 170.758 118.705 182.47 118.804Z" fill="#EBC4B5"/>
<path opacity="0.28" d="M182.508 118.836C194.351 118.967 204.361 127.866 204.361 139.708C204.361 151.551 194.351 161.726 182.508 161.726C170.665 161.726 160.949 152.107 161.047 140.265C161.145 128.356 170.796 118.705 182.508 118.804V118.836Z" fill="#ECC6B7"/>
<path opacity="0.27" d="M182.534 118.868C194.377 118.966 204.355 127.93 204.355 139.773C204.355 151.616 194.377 161.757 182.534 161.757C170.691 161.757 160.975 152.139 161.073 140.296C161.171 128.388 170.822 118.737 182.534 118.835V118.868Z" fill="#ECC7B9"/>
<path opacity="0.26" d="M182.534 118.901C194.377 118.999 204.355 127.963 204.355 139.805C204.355 151.648 194.377 161.79 182.534 161.79C170.691 161.79 160.975 152.172 161.073 140.329C161.171 128.421 170.822 118.77 182.534 118.868V118.901Z" fill="#EDC9BC"/>
<path opacity="0.25" d="M182.572 118.939C194.415 119.037 204.393 128.033 204.393 139.876C204.393 151.719 194.415 161.828 182.572 161.828C170.729 161.828 161.046 152.21 161.111 140.367C161.209 128.459 170.86 118.808 182.572 118.906V118.939Z" fill="#EECBBE"/>
<path opacity="0.25" d="M182.572 118.97C194.415 119.068 204.36 128.065 204.36 139.908C204.36 151.751 194.415 161.86 182.572 161.86C170.729 161.86 161.046 152.241 161.111 140.399C161.209 128.49 170.827 118.839 182.572 118.938V118.97Z" fill="#EECDC0"/>
<path opacity="0.24" d="M182.597 118.97C194.44 119.068 204.386 128.097 204.386 139.94C204.386 151.783 194.44 161.859 182.597 161.859C170.755 161.859 161.071 152.241 161.136 140.398C161.235 128.49 170.853 118.872 182.597 118.937V118.97Z" fill="#EFCFC3"/>
<path opacity="0.23" d="M182.597 119.001C194.44 119.099 204.353 128.161 204.353 140.004C204.353 151.847 194.44 161.923 182.597 161.923C170.755 161.923 161.071 152.305 161.136 140.462C161.235 128.554 170.853 118.936 182.597 119.001Z" fill="#F0D1C5"/>
<path opacity="0.22" d="M182.636 119.034C194.478 119.132 204.391 128.194 204.391 140.037C204.391 151.88 194.478 161.923 182.636 161.923C170.793 161.923 161.109 152.305 161.175 140.462C161.24 128.554 170.891 118.936 182.636 119.001V119.034Z" fill="#F0D2C7"/>
<path opacity="0.21" d="M182.636 119.065C194.478 119.164 204.391 128.258 204.391 140.101C204.391 151.944 194.478 161.987 182.636 161.987C170.793 161.987 161.109 152.369 161.175 140.526C161.24 128.618 170.891 119 182.636 119.065Z" fill="#F1D4CA"/>
<path opacity="0.2" d="M182.661 119.098C194.504 119.196 204.384 128.291 204.384 140.134C204.384 151.977 194.504 161.987 182.661 161.987C170.818 161.987 161.135 152.369 161.2 140.526C161.266 128.618 170.916 119 182.661 119.065V119.098Z" fill="#F1D6CC"/>
<path opacity="0.19" d="M182.699 119.13C194.542 119.195 204.422 128.356 204.422 140.198C204.422 152.041 194.542 162.019 182.699 162.019C170.856 162.019 161.173 152.401 161.238 140.558C161.304 128.65 170.955 119.032 182.699 119.097V119.13Z" fill="#F2D8CE"/>
<path opacity="0.18" d="M182.699 119.13C194.542 119.195 204.389 128.356 204.389 140.198C204.389 152.041 194.542 162.019 182.699 162.019C170.856 162.019 161.173 152.401 161.238 140.558C161.304 128.65 170.922 119.032 182.699 119.097V119.13Z" fill="#F3DAD1"/>
<path opacity="0.17" d="M182.726 119.162C194.568 119.227 204.416 128.42 204.416 140.263C204.416 152.106 194.568 162.051 182.726 162.051C170.883 162.051 161.199 152.433 161.265 140.59C161.33 128.682 170.948 119.064 182.726 119.129V119.162Z" fill="#F3DCD3"/>
<path opacity="0.16" d="M182.738 119.199C194.581 119.265 204.428 128.458 204.428 140.333C204.428 152.209 194.581 162.121 182.738 162.121C170.895 162.121 161.244 152.503 161.277 140.66C161.342 128.785 170.961 119.134 182.738 119.199Z" fill="#F4DED5"/>
<path opacity="0.15" d="M182.764 119.232C194.606 119.297 204.421 128.523 204.421 140.366C204.421 152.209 194.606 162.121 182.764 162.121C170.921 162.121 161.27 152.503 161.303 140.66C161.368 128.785 170.986 119.166 182.764 119.199V119.232Z" fill="#F4DFD8"/>
<path opacity="0.15" d="M182.764 119.263C194.606 119.329 204.421 128.587 204.421 140.43C204.421 152.273 194.606 162.185 182.764 162.185C170.921 162.185 161.27 152.567 161.303 140.724C161.368 128.849 170.986 119.231 182.764 119.263Z" fill="#F5E1DA"/>
<path opacity="0.14" d="M182.802 119.296C194.644 119.361 204.459 128.62 204.459 140.463C204.459 152.305 194.677 162.185 182.802 162.185C170.926 162.185 161.308 152.567 161.341 140.724C161.406 128.849 171.024 119.231 182.802 119.263V119.296Z" fill="#F6E3DC"/>
<path opacity="0.13" d="M182.802 119.296C194.644 119.361 204.426 128.652 204.426 140.495C204.426 152.338 194.644 162.185 182.802 162.185C170.959 162.185 161.308 152.567 161.341 140.724C161.373 128.849 171.024 119.231 182.802 119.263V119.296Z" fill="#F6E5DF"/>
<path opacity="0.12" d="M182.827 119.328C194.67 119.361 204.452 128.684 204.452 140.527C204.452 152.37 194.67 162.217 182.827 162.217C170.984 162.217 161.333 152.599 161.366 140.756C161.399 128.881 171.017 119.262 182.827 119.295V119.328Z" fill="#F7E7E1"/>
<path opacity="0.11" d="M182.865 119.36C194.708 119.392 204.457 128.749 204.457 140.592C204.457 152.434 194.708 162.249 182.865 162.249C171.023 162.249 161.372 152.631 161.404 140.788C161.437 128.912 171.055 119.294 182.865 119.327V119.36Z" fill="#F8E9E3"/>
<path opacity="0.1" d="M182.865 119.391C194.708 119.424 204.457 128.781 204.457 140.623C204.457 152.466 194.708 162.281 182.865 162.281C171.023 162.281 161.372 152.662 161.404 140.82C161.437 128.944 171.055 119.326 182.865 119.359V119.391Z" fill="#F8EBE6"/>
<path opacity="0.09" d="M182.891 119.424C194.734 119.456 204.483 128.846 204.483 140.688C204.483 152.531 194.766 162.313 182.891 162.313C171.015 162.313 161.397 152.695 161.43 140.852C161.463 128.976 171.081 119.358 182.891 119.391V119.424Z" fill="#F9ECE8"/>
<path opacity="0.08" d="M182.891 119.461C194.734 119.494 204.45 128.883 204.45 140.758C204.45 152.634 194.734 162.383 182.891 162.383C171.048 162.383 161.43 152.765 161.43 140.922C161.43 129.046 171.081 119.428 182.891 119.461Z" fill="#F9EEEA"/>
<path opacity="0.07" d="M182.93 119.494C194.773 119.494 204.489 128.948 204.489 140.791C204.489 152.634 194.773 162.383 182.93 162.383C171.087 162.383 161.469 152.765 161.469 140.922C161.469 129.046 171.12 119.461 182.93 119.461V119.494Z" fill="#FAF0EC"/>
<path opacity="0.06" d="M182.93 119.493C194.773 119.493 204.456 128.98 204.456 140.823C204.456 152.666 194.773 162.415 182.93 162.415C171.087 162.415 161.469 152.797 161.469 140.954C161.469 129.079 171.12 119.493 182.93 119.493Z" fill="#FBF2EF"/>
<path opacity="0.05" d="M182.955 119.526C194.798 119.526 204.482 129.013 204.482 140.856C204.482 152.699 194.798 162.415 182.955 162.415C171.112 162.415 161.494 152.797 161.494 140.954C161.494 129.079 171.112 119.493 182.955 119.493V119.526Z" fill="#FBF4F1"/>
<path opacity="0.05" d="M182.993 119.558C194.836 119.558 204.52 129.078 204.52 140.92C204.52 152.763 194.869 162.447 182.993 162.447C171.118 162.447 161.532 152.829 161.532 140.986C161.532 129.143 171.151 119.525 182.993 119.525V119.558Z" fill="#FCF6F3"/>
<path opacity="0.04" d="M182.993 119.589C194.836 119.589 204.487 129.11 204.487 140.952C204.487 152.795 194.836 162.479 182.993 162.479C171.151 162.479 161.532 152.861 161.532 141.018C161.532 129.175 171.151 119.557 182.993 119.557V119.589Z" fill="#FDF8F6"/>
<path opacity="0.03" d="M183.032 119.622C194.874 119.622 204.525 129.174 204.525 141.017C204.525 152.86 194.874 162.511 183.032 162.511C171.189 162.511 161.571 152.893 161.571 141.05C161.571 129.207 171.189 119.589 183.032 119.589V119.622Z" fill="#FDF9F8"/>
<path opacity="0.02" d="M183.032 119.654C194.874 119.654 204.493 129.206 204.493 141.049C204.493 152.892 194.874 162.543 183.032 162.543C171.189 162.543 161.571 152.925 161.571 141.082C161.571 129.239 171.189 119.621 183.032 119.621V119.654Z" fill="#FEFBFA"/>
</g>
</g>
<mask id="mask1_2812_87301" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="31" y="39" width="298" height="192">
<path d="M319.218 39.7015H40.8145C35.3838 39.7015 31 44.0853 31 49.516V220.484C31 225.915 35.3838 230.299 40.8145 230.299H319.186C324.616 230.299 329 225.915 329 220.484V49.516C329 44.0853 324.616 39.7015 319.186 39.7015H319.218ZM314.344 135C314.344 185.806 273.156 227.027 222.317 227.027H40.8145C37.2158 227.027 34.2715 224.083 34.2715 220.484V49.516C34.2715 45.9174 37.2158 42.973 40.8145 42.973H222.317C273.123 42.973 314.344 84.1611 314.344 135Z" fill="white"/>
</mask>
<g mask="url(#mask1_2812_87301)">
<mask id="mask2_2812_87301" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="3" y="-42" width="354" height="354">
<path d="M180.151 311.79C277.79 311.79 356.942 232.637 356.942 134.998C356.942 37.3591 277.79 -41.7931 180.151 -41.7931C82.5116 -41.7931 3.35944 37.3591 3.35944 134.998C3.35944 232.637 82.5116 311.79 180.151 311.79Z" fill="white"/>
</mask>
<g mask="url(#mask2_2812_87301)">
<path d="M244.829 -106.488L-61.3546 70.2867L115.42 376.47L421.604 199.695L244.829 -106.488Z" fill="#E04E15"/>
<path d="M248.509 -100.111L-57.674 76.6637L119.101 382.847L425.284 206.072L248.509 -100.111Z" fill="#E04F15"/>
<path d="M252.19 -93.7535L-53.9935 83.0215L122.781 389.205L428.965 212.43L252.19 -93.7535Z" fill="#E15015"/>
<path d="M255.844 -87.3765L-50.3392 89.3984L126.436 395.582L432.619 218.807L255.844 -87.3765Z" fill="#E15115"/>
<path d="M259.563 -81.0128L-46.6205 95.7622L130.154 401.945L436.338 225.17L259.563 -81.0128Z" fill="#E25215"/>
<path d="M263.205 -74.6423L-42.9781 102.133L133.797 408.316L439.98 231.541L263.205 -74.6423Z" fill="#E25316"/>
<path d="M266.911 -68.2465L-39.2722 108.528L137.503 414.712L443.686 237.937L266.911 -68.2465Z" fill="#E35416"/>
<path d="M270.592 -61.876L-35.5916 114.899L141.183 421.082L447.367 244.307L270.592 -61.876Z" fill="#E35516"/>
<path d="M274.272 -55.4798L-31.9111 121.295L144.864 427.478L451.047 250.703L274.272 -55.4798Z" fill="#E35616"/>
<path d="M277.94 -49.1347L-28.2433 127.64L148.532 433.823L454.715 257.049L277.94 -49.1347Z" fill="#E45716"/>
<path d="M281.646 -42.7769L-24.5373 133.998L152.238 440.181L458.421 263.406L281.646 -42.7769Z" fill="#E45716"/>
<path d="M285.287 -36.3685L-20.8958 140.406L155.879 446.59L462.062 269.815L285.287 -36.3685Z" fill="#E55816"/>
<path d="M288.993 -30.0106L-17.1898 146.764L159.585 452.948L465.768 276.173L288.993 -30.0106Z" fill="#E55916"/>
<path d="M292.674 -23.6341L-13.5092 153.141L163.266 459.324L469.449 282.549L292.674 -23.6341Z" fill="#E55A16"/>
<path d="M296.355 -17.2445L-9.82867 159.531L166.946 465.714L473.13 288.939L296.355 -17.2445Z" fill="#E65B16"/>
<path d="M300.035 -10.8675L-6.14813 165.907L170.627 472.091L476.81 295.316L300.035 -10.8675Z" fill="#E65C17"/>
<path d="M303.728 -4.53511L-2.4549 172.24L174.32 478.423L480.503 301.648L303.728 -4.53511Z" fill="#E75D17"/>
<path d="M307.37 1.87337L1.18665 178.648L177.962 484.832L484.145 308.057L307.37 1.87337Z" fill="#E75E17"/>
<path d="M311.089 8.23125L4.90533 185.006L181.68 491.189L487.864 314.414L311.089 8.23125Z" fill="#E85F17"/>
<path d="M314.769 14.6081L8.58588 191.383L185.361 497.566L491.544 320.791L314.769 14.6081Z" fill="#E86017"/>
<path d="M316.188 17.0549L10.0045 193.83L186.779 500.013L492.963 323.238L316.188 17.0549Z" fill="#E86117"/>
<path d="M317.606 19.5539L11.4231 196.329L188.198 502.512L494.381 325.737L317.606 19.5539Z" fill="#E96217"/>
<path d="M319.025 22.0073L12.8417 198.782L189.617 504.965L495.8 328.191L319.025 22.0073Z" fill="#E96317"/>
<path d="M320.431 24.4736L14.2476 201.249L191.023 507.432L497.206 330.657L320.431 24.4736Z" fill="#E96418"/>
<path d="M321.875 26.953L15.6916 203.728L192.467 509.911L498.65 333.136L321.875 26.953Z" fill="#E96518"/>
<path d="M323.306 29.3936L17.1229 206.169L193.898 512.352L500.081 335.577L323.306 29.3936Z" fill="#EA6618"/>
<path d="M324.725 31.8407L18.5415 208.616L195.316 514.799L501.5 338.024L324.725 31.8407Z" fill="#EA6718"/>
<path d="M326.13 34.3137L19.9465 211.089L196.721 517.272L502.905 340.497L326.13 34.3137Z" fill="#EA6818"/>
<path d="M327.574 36.7928L21.3906 213.568L198.166 519.751L504.349 342.976L327.574 36.7928Z" fill="#EA6918"/>
<path d="M328.967 39.2591L22.7837 216.034L199.559 522.217L505.742 345.442L328.967 39.2591Z" fill="#EB6A18"/>
<path d="M330.398 41.7131L24.215 218.488L200.99 524.671L507.173 347.896L330.398 41.7131Z" fill="#EB6B19"/>
<path d="M331.804 44.1792L25.6209 220.954L202.396 527.137L508.579 350.362L331.804 44.1792Z" fill="#EB6C19"/>
<path d="M333.274 46.6264L27.0904 223.401L203.865 529.585L510.049 352.81L333.274 46.6264Z" fill="#EB6D19"/>
<path d="M334.667 49.0992L28.4835 225.874L205.258 532.057L511.442 355.282L334.667 49.0992Z" fill="#EC6E19"/>
<path d="M336.111 51.5781L29.9276 228.353L206.703 534.536L512.886 357.761L336.111 51.5781Z" fill="#EC6F19"/>
<path d="M337.504 54.0446L31.3207 230.82L208.096 537.003L514.279 360.228L337.504 54.0446Z" fill="#EC7019"/>
<path d="M338.935 56.4982L32.7521 233.273L209.527 539.456L515.71 362.681L338.935 56.4982Z" fill="#EC7119"/>
<path d="M340.354 58.9646L34.1707 235.74L210.946 541.923L517.129 365.148L340.354 58.9646Z" fill="#ED7219"/>
<path d="M341.811 61.412L35.6274 238.187L212.402 544.37L518.586 367.595L341.811 61.412Z" fill="#ED731A"/>
<path d="M343.203 63.8847L37.0197 240.66L213.795 546.843L519.978 370.068L343.203 63.8847Z" fill="#ED741A"/>
<path d="M344.622 66.3766L38.4384 243.152L215.213 549.335L521.397 372.56L344.622 66.3766Z" fill="#ED751A"/>
<path d="M346.053 68.8303L39.8696 245.605L216.645 551.788L522.828 375.013L346.053 68.8303Z" fill="#EE761A"/>
<path d="M347.446 71.2968L41.2628 248.072L218.038 554.255L524.221 377.48L347.446 71.2968Z" fill="#EE771A"/>
<path d="M348.89 73.7502L42.7068 250.525L219.482 556.708L525.665 379.933L348.89 73.7502Z" fill="#EE781A"/>
<path d="M350.321 76.2166L44.1382 252.992L220.913 559.175L527.096 382.4L350.321 76.2166Z" fill="#EF791A"/>
<path d="M351.753 78.6704L45.5695 255.445L222.344 561.628L528.528 384.854L351.753 78.6704Z" fill="#EF7A1B"/>
<path d="M353.159 81.1622L46.9753 257.937L223.75 564.12L529.934 387.345L353.159 81.1622Z" fill="#EF7B1B"/>
<path d="M354.59 83.6157L48.4067 260.391L225.182 566.574L531.365 389.799L354.59 83.6157Z" fill="#EF7C1B"/>
<path d="M355.982 86.0824L49.799 262.857L226.574 569.041L532.757 392.266L355.982 86.0824Z" fill="#F07D1B"/>
<path d="M357.426 88.5297L51.243 265.305L228.018 571.488L534.201 394.713L357.426 88.5297Z" fill="#F07E1B"/>
<path d="M358.858 91.0022L52.6743 267.777L229.449 573.96L535.632 397.185L358.858 91.0022Z" fill="#F07F1B"/>
<path d="M360.289 93.456L54.1057 270.231L230.881 576.414L537.064 399.639L360.289 93.456Z" fill="#F0801B"/>
<path d="M361.695 95.9478L55.5115 272.723L232.286 578.906L538.47 402.131L361.695 95.9478Z" fill="#F1801C"/>
<path d="M363.126 98.4017L56.9429 275.177L233.718 581.36L539.901 404.585L363.126 98.4017Z" fill="#F1811C"/>
<path d="M364.519 100.868L58.336 277.643L235.111 583.826L541.294 407.051L364.519 100.868Z" fill="#F1821C"/>
<path d="M365.963 103.315L59.78 280.09L236.555 586.273L542.738 409.498L365.963 103.315Z" fill="#F1831C"/>
<path d="M367.395 105.788L61.2114 282.563L237.986 588.746L544.17 411.971L367.395 105.788Z" fill="#F2841C"/>
<path d="M368.826 108.242L62.6427 285.017L239.418 591.2L545.601 414.425L368.826 108.242Z" fill="#F2851C"/>
<path d="M370.244 110.733L64.0613 287.508L240.836 593.691L547.019 416.916L370.244 110.733Z" fill="#F2861C"/>
<path d="M371.662 113.187L65.479 289.962L242.254 596.145L548.437 419.37L371.662 113.187Z" fill="#F2871D"/>
<path d="M373.068 115.653L66.8849 292.428L243.66 598.611L549.843 421.836L373.068 115.653Z" fill="#F3881D"/>
<path d="M374.487 118.075L68.3035 294.85L245.078 601.033L551.262 424.258L374.487 118.075Z" fill="#F3891D"/>
<path d="M375.943 120.573L69.7603 297.348L246.535 603.532L552.718 426.757L375.943 120.573Z" fill="#F38A1D"/>
<path d="M377.362 123.021L71.1788 299.796L247.954 605.979L554.137 429.204L377.362 123.021Z" fill="#F48B1D"/>
<path d="M378.768 125.494L72.5847 302.269L249.36 608.452L555.543 431.677L378.768 125.494Z" fill="#F48C1D"/>
<path d="M380.212 127.973L74.0288 304.748L250.804 610.931L556.987 434.156L380.212 127.973Z" fill="#F48D1D"/>
<path d="M381.605 130.439L75.4219 307.214L252.197 613.397L558.38 436.622L381.605 130.439Z" fill="#F48E1E"/>
<path d="M383.037 132.861L76.8533 309.635L253.628 615.819L559.811 439.044L383.037 132.861Z" fill="#F58F1E"/>
<path d="M384.48 135.359L78.2973 312.134L255.072 618.317L561.255 441.542L384.48 135.359Z" fill="#F5901E"/>
<path d="M385.899 137.806L79.7159 314.581L256.491 620.764L562.674 443.989L385.899 137.806Z" fill="#F5911E"/>
<path d="M387.304 140.279L81.1209 317.054L257.896 623.237L564.079 446.462L387.304 140.279Z" fill="#F5921E"/>
<path d="M388.748 142.758L82.5649 319.533L259.34 625.716L565.523 448.941L388.748 142.758Z" fill="#F6931E"/>
<path d="M390.141 145.225L83.9581 322L260.733 628.183L566.916 451.408L390.141 145.225Z" fill="#F6941E"/>
<path d="M391.573 147.646L85.3894 324.421L262.164 630.605L568.348 453.83L391.573 147.646Z" fill="#F6951E"/>
<path d="M393.017 150.145L86.8335 326.92L263.608 633.103L569.792 456.328L393.017 150.145Z" fill="#F6961F"/>
<path d="M394.448 152.592L88.2648 329.367L265.04 635.55L571.223 458.775L394.448 152.592Z" fill="#F7971F"/>
<path d="M395.841 155.065L89.658 331.84L266.433 638.023L572.616 461.248L395.841 155.065Z" fill="#F7981F"/>
<path d="M397.285 157.544L91.102 334.319L267.877 640.502L574.06 463.727L397.285 157.544Z" fill="#F7991F"/>
<path d="M398.678 160.01L92.4951 336.785L269.27 642.968L575.453 466.193L398.678 160.01Z" fill="#F79A1F"/>
<path d="M400.11 162.432L93.9265 339.207L270.701 645.39L576.885 468.615L400.11 162.432Z" fill="#F89B1F"/>
<path d="M401.554 164.93L95.3705 341.705L272.145 647.888L578.329 471.113L401.554 164.93Z" fill="#F89C1F"/>
<path d="M402.985 167.377L96.8018 344.152L273.577 650.335L579.76 473.561L402.985 167.377Z" fill="#F89D20"/>
<path d="M404.377 169.85L98.1941 346.625L274.969 652.808L581.152 476.033L404.377 169.85Z" fill="#F89E20"/>
<path d="M405.796 172.342L99.6127 349.117L276.388 655.3L582.571 478.525L405.796 172.342Z" fill="#F99F20"/>
<path d="M407.227 174.796L101.044 351.571L277.819 657.754L584.002 480.979L407.227 174.796Z" fill="#F9A020"/>
</g>
</g>
<defs>
<linearGradient id="paint0_linear_2812_87301" x1="189.407" y1="151.684" x2="174.555" y2="128.456" gradientUnits="userSpaceOnUse">
<stop stop-color="#FF9200"/>
<stop offset="0.21" stop-color="#FF8D00"/>
<stop offset="0.45" stop-color="#FF7F00"/>
<stop offset="0.62" stop-color="#FF7100"/>
<stop offset="0.82" stop-color="#FF5E00"/>
<stop offset="1" stop-color="#FF5200"/>
</linearGradient>
<linearGradient id="paint1_linear_2812_87301" x1="187.052" y1="151.389" x2="165.362" y2="108.958" gradientUnits="userSpaceOnUse">
<stop stop-color="#FF7100" stop-opacity="0"/>
<stop offset="0.03" stop-color="#FF6D00" stop-opacity="0.03"/>
<stop offset="0.21" stop-color="#FF5900" stop-opacity="0.25"/>
<stop offset="0.39" stop-color="#FF4800" stop-opacity="0.42"/>
<stop offset="0.56" stop-color="#FF3B00" stop-opacity="0.56"/>
<stop offset="0.72" stop-color="#FF3200" stop-opacity="0.66"/>
<stop offset="0.86" stop-color="#FF2C00" stop-opacity="0.72"/>
<stop offset="0.98" stop-color="#FF2B00" stop-opacity="0.74"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 68 KiB

+54
View File
@@ -0,0 +1,54 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="120px" height="90px" viewBox="0 0 120 90" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>creditcard_jcb</title>
<defs>
<linearGradient x1="-57.5270968%" y1="50.1241953%" x2="232.39121%" y2="50.1241953%" id="linearGradient-1">
<stop stop-color="#007940" offset="0%"></stop>
<stop stop-color="#00873F" offset="22.85%"></stop>
<stop stop-color="#40A737" offset="74.33%"></stop>
<stop stop-color="#5CB531" offset="100%"></stop>
</linearGradient>
<linearGradient x1="0.182516704%" y1="49.95997%" x2="100.273441%" y2="49.95997%" id="linearGradient-2">
<stop stop-color="#007940" offset="0%"></stop>
<stop stop-color="#00873F" offset="22.85%"></stop>
<stop stop-color="#40A737" offset="74.33%"></stop>
<stop stop-color="#5CB531" offset="100%"></stop>
</linearGradient>
<linearGradient x1="-62.8015845%" y1="49.8578253%" x2="253.671294%" y2="49.8578253%" id="linearGradient-3">
<stop stop-color="#007940" offset="0%"></stop>
<stop stop-color="#00873F" offset="22.85%"></stop>
<stop stop-color="#40A737" offset="74.33%"></stop>
<stop stop-color="#5CB531" offset="100%"></stop>
</linearGradient>
<linearGradient x1="0.175556793%" y1="50.0058048%" x2="101.808162%" y2="50.0058048%" id="linearGradient-4">
<stop stop-color="#1F286F" offset="0%"></stop>
<stop stop-color="#004E94" offset="47.51%"></stop>
<stop stop-color="#0066B1" offset="82.61%"></stop>
<stop stop-color="#006FBC" offset="100%"></stop>
</linearGradient>
<linearGradient x1="-0.575855512%" y1="49.9142191%" x2="98.13299%" y2="49.9142191%" id="linearGradient-5">
<stop stop-color="#6C2C2F" offset="0%"></stop>
<stop stop-color="#882730" offset="17.35%"></stop>
<stop stop-color="#BE1833" offset="57.31%"></stop>
<stop stop-color="#DC0436" offset="85.85%"></stop>
<stop stop-color="#E60039" offset="100%"></stop>
</linearGradient>
</defs>
<g id="LOGO-+-SDK-+-payment-icon" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="payment" transform="translate(-218.000000, -1355.000000)">
<g id="creditcard_jcb" transform="translate(218.000000, 1355.000000)">
<g id="payment-4:3bg" transform="translate(-20.000000, -15.000000)"></g>
<g id="JCB" transform="translate(12.000000, 8.000000)" fill-rule="nonzero">
<path d="M96,59.0825396 C96,67.3047619 89.3105129,74 81.0953545,74 L0,74 L0,14.9174604 C0,6.6952381 6.68948652,0 14.9046454,0 L96,0 L96,59.0825396 Z" id="path6325" fill="#FFFFFF"></path>
<g id="g6327" transform="translate(64.251969, 5.402412)">
<path d="M5.29133856,38.6852504 L11.4645669,38.6852504 C11.6409448,38.6852504 12.0524934,38.6269017 12.2288714,38.6269017 C13.4047243,38.3935065 14.4041995,37.3432282 14.4041995,35.8845083 C14.4041995,34.4841373 13.4047243,33.433859 12.2288714,33.142115 C12.0524934,33.0837662 11.6997375,33.0837662 11.4645669,33.0837662 L5.29133856,33.0837662 L5.29133856,38.6852504 Z" id="path6338" fill="url(#linearGradient-1)"></path>
<path d="M10.7590551,0 C4.87979001,0 0.0587926507,4.72625232 0.0587926507,10.6194805 L0.0587926507,21.6474026 L15.1685039,21.6474026 C15.5212598,21.6474026 15.9328084,21.6474026 16.2267716,21.7057514 C19.6367453,21.8807978 22.1648293,23.6312616 22.1648293,26.6653989 C22.1648293,29.0576994 20.4598425,31.0999072 17.2850393,31.5083488 L17.2850393,31.6250464 C20.7538057,31.8584415 23.399475,33.7839517 23.399475,36.7597402 C23.399475,39.9689239 20.4598425,42.0694805 16.5795275,42.0694805 L0,42.0694805 L0,63.6585343 L15.6976378,63.6585343 C21.5769028,63.6585343 26.3979001,58.932282 26.3979001,53.0390538 L26.3979001,0 L10.7590551,0 Z" id="path6349" fill="url(#linearGradient-2)"></path>
<path d="M13.6398949,27.3655844 C13.6398949,25.9652134 12.6404199,25.0316327 11.4645669,24.8565863 C11.3469816,24.8565863 11.0530183,24.7982374 10.8766404,24.7982374 L5.29133856,24.7982374 L5.29133856,29.9329313 L10.8766404,29.9329313 C11.0530183,29.9329313 11.4057742,29.9329313 11.4645669,29.8745825 C12.6404199,29.6995362 13.6398949,28.7659555 13.6398949,27.3655844 L13.6398949,27.3655844 Z" id="path6360" fill="url(#linearGradient-3)"></path>
</g>
<path d="M15.3184504,4.53061224 C9.42609119,4.53061224 4.59435664,9.30022471 4.59435664,15.2475192 L4.59435664,41.6864819 C7.59945981,43.1585845 10.7224102,44.1007302 13.8453606,44.1007302 C17.5575469,44.1007302 19.560949,41.8631343 19.560949,38.8011608 L19.560949,26.3177308 L28.7530293,26.3177308 L28.7530293,38.7422767 C28.7530293,43.5707733 25.7479262,47.5160082 15.5541447,47.5160082 C9.3671676,47.5160082 4.53543305,46.1616738 4.53543305,46.1616738 L4.53543305,68.7142857 L20.2680321,68.7142857 C26.1603913,68.7142857 30.9921259,63.9446733 30.9921259,57.9973787 L30.9921259,4.53061224 L15.3184504,4.53061224 Z" id="path6371" fill="url(#linearGradient-4)"></path>
<path d="M45.4957471,4.53061224 C39.603388,4.53061224 34.7716534,9.29148916 34.7716534,15.2278912 L34.7716534,29.2166405 C37.4821386,26.9243664 42.196026,25.45496 49.7971694,25.8076176 C53.8628972,25.9839464 58.223243,27.1006952 58.223243,27.1006952 L58.223243,31.626467 C56.0430701,30.5097182 53.4504321,29.5105218 50.0917873,29.2754167 C44.3172753,28.8639829 40.8407834,31.6852434 40.8407834,36.622449 C40.8407834,41.6184309 44.3172753,44.4396913 50.0917873,43.9694812 C53.4504321,43.7343762 56.0430701,42.6764036 58.223243,41.6184309 L58.223243,46.1442027 C58.223243,46.1442027 53.9218208,47.2609516 49.7971694,47.4372804 C42.196026,47.789938 37.4821386,46.3205315 34.7716534,44.0282575 L34.7716534,68.7142857 L50.5042525,68.7142857 C56.3966117,68.7142857 61.2283462,63.9534088 61.2283462,58.0170068 L61.2283462,4.53061224 L45.4957471,4.53061224 Z" id="path6384" fill="url(#linearGradient-5)"></path>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 6.2 KiB

+17
View File
@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="120px" height="90px" viewBox="0 0 120 90" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>creditcard_mastercard</title>
<g id="LOGO-+-SDK-+-payment-icon" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="payment" transform="translate(-568.000000, -555.000000)">
<g id="creditcard_mastercard" transform="translate(568.000000, 555.000000)">
<g id="payment-4:3bg" transform="translate(-20.000000, -15.000000)"></g>
<g id="mastercard" transform="translate(10.000000, 14.000000)" fill-rule="nonzero">
<rect id="矩形" fill="#FF5F00" x="36" y="6" width="27" height="48"></rect>
<path d="M38.079102,30.4667146 C38.0896581,21.1038083 42.4529576,12.2644901 49.9142381,6.49070014 C37.2388758,-3.31863405 19.0605509,-1.85791103 8.15664308,9.84614025 C-2.74726476,21.5501915 -2.71504155,39.5671555 8.23066331,51.2330718 C19.1763682,62.8989881 37.3598045,64.2962475 50,54.4427291 C42.4419072,48.7382455 38.0289928,39.8627314 38.079102,30.4667146 Z" id="_Path_" fill="#EB001B"></path>
<path d="M99,30.5187373 C98.9868408,42.186337 92.2316764,52.8252119 81.5968292,57.9274198 C70.9619821,63.0296277 58.3092912,61.7019268 49,54.5068953 C62.3867519,44.0916089 64.7287373,24.9618636 54.2405498,11.7011717 C52.7151077,9.77221931 50.9550508,8.03565794 49,6.5305793 C55.4562023,1.5201139 63.6673558,-0.750529912 71.8223982,0.219463943 C79.9774405,1.1894578 87.4064196,5.3204025 92.4707904,11.7011717 C96.6951227,17.0893902 98.9911406,23.7066486 99,30.5187373 L99,30.5187373 Z" id="路径" fill="#F79E1B"></path>
<path d="M95.7,48.98 L95.7,48.14 L96.2,48.14 L96.2,48 L95,48 L95,48.14 L95.5,48.14 L95.5,48.98 L95.7,48.98 Z M98,48.98 L98,48 L97.7,48 L97.3,48.7 L96.9,48 L96.5,48 L96.5,48.98 L96.7,48.98 L96.7,48.21 L97.1,48.84 L97.4,48.84 L97.8,48.21 L97.8,48.98 L98,48.98 Z" id="形状" fill="#F79E1B"></path>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 9.4 KiB

+20
View File
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="120px" height="90px" viewBox="0 0 120 90" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>creditcard_visa</title>
<defs>
<linearGradient x1="0%" y1="49.9996731%" x2="100.00004%" y2="49.9996731%" id="linearGradient-1">
<stop stop-color="#231F5D" offset="0%"></stop>
<stop stop-color="#024DA1" offset="100%"></stop>
</linearGradient>
</defs>
<g id="LOGO-+-SDK-+-payment-icon" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="payment" transform="translate(-568.000000, -355.000000)">
<g id="creditcard_visa" transform="translate(568.000000, 355.000000)">
<g id="payment-4:3bg" transform="translate(-20.000000, -15.000000)"></g>
<g id="VISA" transform="translate(6.000000, 27.000000)" fill="url(#linearGradient-1)" fill-rule="nonzero">
<path d="M70.4643465,0 C61.9102892,0.0469984375 55.9007835,4.63505547 55.8522771,11.1885867 L55.8522771,11.1885867 C55.7954063,16.064918 60.1853418,18.7819188 63.4934755,20.402468 L63.4934755,20.402468 C66.8915137,22.0616211 68.0318559,23.1261844 68.0193147,24.6091016 L68.0193147,24.6091016 C67.9933892,26.881307 65.3066522,27.8825094 62.7955531,27.9215344 L62.7955531,27.9215344 C58.4081247,27.9903477 55.861898,26.7319281 53.8321041,25.783182 L53.8321041,25.783182 L52.2526812,33.2011148 C54.290002,34.1402086 58.0539343,34.9588805 61.9567016,35 L61.9567016,35 C71.1254718,35 77.1211745,30.4568414 77.1500259,23.4169742 L77.1500259,23.4169742 C77.1884982,14.4838148 64.8395578,13.9907688 64.9240292,9.99898047 L64.9240292,9.99898047 C64.9520486,8.78545938 66.1032624,7.494725 68.6256576,7.16364688 L68.6256576,7.16364688 C69.8747271,7.00083672 73.3233655,6.87411484 77.2290588,8.67845781 L77.2290588,8.67845781 L78.7624824,1.50012187 C76.6875324,0.744816406 74.0283961,0.0180414062 70.7231939,0 L70.7231939,0 L70.4643465,0 Z M93.506692,0.61893125 C91.8306753,0.61893125 90.4181014,1.59621328 89.7920982,3.10136641 L89.7920982,3.10136641 L76.6963102,34.4708656 L85.8592231,34.4708656 L87.6795135,29.4128406 L98.8738898,29.4128406 L99.9293473,34.4708656 L108,34.4708656 L100.954697,0.61893125 L93.506692,0.61893125 Z M94.7854506,9.76525703 L97.4291143,22.4757805 L90.1910314,22.4757805 L94.7854506,9.76525703 Z M44.7498992,0.61893125 L37.5306392,34.4708656 L46.2582349,34.4708656 L53.474569,0.61893125 L44.7498992,0.61893125 Z M31.838108,0.61893125 L22.7550655,23.6586766 L19.080619,4.06941719 C18.6503216,1.88196641 16.9471231,0.61893125 15.0557455,0.61893125 L15.0557455,0.61893125 L0.20574,0.61893125 L0,1.60040781 C3.04761122,2.26591641 6.51172224,3.33802656 8.60925857,4.48105547 L8.60925857,4.48105547 C9.89386898,5.18097344 10.2601873,5.79151016 10.682539,7.45486328 L10.682539,7.45486328 L17.6408633,34.4708656 L26.8652424,34.4708656 L41.0043655,0.61893125 L31.838108,0.61893125 Z"></path>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.0 KiB

+6
View File
@@ -1,6 +1,12 @@
{
"version": 1,
"skills": {
"drizzle": {
"source": "lobehub/lobehub",
"sourceType": "github",
"skillPath": ".agents/skills/drizzle/SKILL.md",
"computedHash": "c826b51d435fb0448f10309f00979b9be62ea380505166234b816fd617cd1a93"
},
"shadcn": {
"source": "shadcn/ui",
"sourceType": "github",