This commit is contained in:
2026-05-10 22:20:54 +08:00
parent d6219f1a0c
commit 192accd796
92 changed files with 7045 additions and 1559 deletions

View File

@@ -154,6 +154,7 @@ export interface AdminUpsertProfileRedeemCodeRequest {
export interface AdminUpsertProfileInviteCodeRequest {
inviteCode: string;
metadata?: Record<string, unknown>;
grantedUserTags: string[];
startsAt?: string | null;
expiresAt?: string | null;
}
@@ -200,6 +201,7 @@ export interface ProfileInviteCodeAdminResponse {
userId: string;
inviteCode: string;
metadata: Record<string, unknown>;
grantedUserTags: string[];
startsAt?: string | null;
expiresAt?: string | null;
status: 'pending' | 'active' | 'expired';

View File

@@ -28,6 +28,7 @@ export function AdminInviteCodePage({
const [inviteCode, setInviteCode] = useState('');
const [startsAt, setStartsAt] = useState('');
const [expiresAt, setExpiresAt] = useState('');
const [grantedTagsText, setGrantedTagsText] = useState('');
const [metadataText, setMetadataText] = useState('{}');
const [errorMessage, setErrorMessage] = useState('');
const [listErrorMessage, setListErrorMessage] = useState('');
@@ -80,6 +81,7 @@ export function AdminInviteCodePage({
const payload: AdminUpsertProfileInviteCodeRequest = {
inviteCode: inviteCode.trim(),
metadata: parseMetadata(metadataText),
grantedUserTags: parseUserTags(grantedTagsText),
startsAt: startsAt ? toIsoDateTime(startsAt) : null,
expiresAt: expiresAt ? toIsoDateTime(expiresAt) : null,
};
@@ -115,6 +117,7 @@ export function AdminInviteCodePage({
setInviteCode(entry.inviteCode);
setStartsAt(toDateTimeLocalValue(entry.startsAt));
setExpiresAt(toDateTimeLocalValue(entry.expiresAt));
setGrantedTagsText(entry.grantedUserTags.join('、'));
setMetadataText(JSON.stringify(entry.metadata, null, 2));
}
@@ -174,6 +177,15 @@ export function AdminInviteCodePage({
</label>
</div>
<label className="admin-field">
<span></span>
<input
autoComplete="off"
value={grantedTagsText}
onChange={(event) => setGrantedTagsText(event.target.value)}
/>
</label>
<label className="admin-field">
<span>Metadata JSON</span>
<textarea
@@ -222,6 +234,7 @@ export function AdminInviteCodePage({
<thead>
<tr>
<th></th>
<th></th>
<th></th>
<th></th>
</tr>
@@ -238,6 +251,9 @@ export function AdminInviteCodePage({
{entry.inviteCode}
</button>
</td>
<td>
<TagList tags={entry.grantedUserTags} />
</td>
<td>
<span className={`admin-status ${inviteValidityClass(entry)}`}>
{inviteValidityLabel(entry)}
@@ -272,6 +288,12 @@ export function AdminInviteCodePage({
<dt></dt>
<dd>{formatValidityWindow(result)}</dd>
</div>
<div>
<dt></dt>
<dd>
<TagList tags={result.grantedUserTags} />
</dd>
</div>
<div>
<dt></dt>
<dd>{result.createdAt}</dd>
@@ -300,6 +322,33 @@ export function AdminInviteCodePage({
);
}
function TagList({tags}: {tags: string[]}) {
if (!tags.length) {
return <span className="admin-muted-text">-</span>;
}
return (
<span className="admin-tag-list">
{tags.map((tag) => (
<span className="admin-tag" key={tag}>
{tag}
</span>
))}
</span>
);
}
function parseUserTags(value: string) {
const tags: string[] = [];
for (const raw of value.split(/[\n,;;、]+/)) {
const tag = raw.trim();
if (tag && !tags.includes(tag)) {
tags.push(tag);
}
}
return tags;
}
function parseMetadata(value: string): Record<string, unknown> {
const trimmed = value.trim();
if (!trimmed) {

View File

@@ -684,6 +684,32 @@ button:disabled {
font-size: 12px;
}
.admin-muted-text {
color: #86939c;
}
.admin-tag-list {
display: flex;
min-width: 0;
flex-wrap: wrap;
gap: 6px;
}
.admin-tag {
display: inline-flex;
max-width: 100%;
align-items: center;
border: 1px solid #cbdfe6;
border-radius: 999px;
background: #eef7f8;
color: #0f5666;
padding: 3px 8px;
font-size: 12px;
font-weight: 750;
line-height: 1.2;
overflow-wrap: anywhere;
}
.admin-table-compact {
min-width: 360px;
}