feat(payment): 兑换码 - 增加可配置联动小铺入口 (#668)
Co-authored-by: linxi <linxi@enjoysoftware.cn>
This commit is contained in:
@@ -10,6 +10,7 @@ import { closePaymentOrder, createPaymentOrder, getPaymentOrder, listPaymentProv
|
||||
import { getWallet, redeemCredits, type CreditLedgerEntry, type WalletSummary } from "@/services/api/wallet";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { openWorkspaceWallet, WORKSPACE_WALLET_OPEN_EVENT, type WorkspaceWalletOpenDetail } from "@/lib/workspace-wallet";
|
||||
import { useAppearanceStore } from "@/stores/use-appearance-store";
|
||||
import { useUserStore } from "@/stores/use-user-store";
|
||||
|
||||
type WalletModalTab = "topup" | "redeem" | "history";
|
||||
@@ -64,6 +65,7 @@ export function WorkspaceWalletHost() {
|
||||
|
||||
export function WorkspaceWalletModal({ open, onClose, pendingPaymentOrderId, paymentInvalid }: { open: boolean; onClose: () => void; pendingPaymentOrderId?: string; paymentInvalid?: boolean }) {
|
||||
const { message } = App.useApp();
|
||||
const redeemPurchaseUrl = useAppearanceStore((state) => state.appearance.redeemPurchaseUrl);
|
||||
const [tab, setTab] = useState<WalletModalTab>("topup");
|
||||
const [wallet, setWallet] = useState<WalletSummary | null>(null);
|
||||
const [walletLoading, setWalletLoading] = useState(false);
|
||||
@@ -378,7 +380,14 @@ export function WorkspaceWalletModal({ open, onClose, pendingPaymentOrderId, pay
|
||||
<div className="workspace-wallet-section-heading">
|
||||
<div>
|
||||
<h3>兑换码</h3>
|
||||
<p>输入兑换码,将积分存入当前账户。</p>
|
||||
<p>
|
||||
购买后请复制卡密,返回此处兑换。
|
||||
{redeemPurchaseUrl ? (
|
||||
<a className="workspace-wallet-redeem-link" href={redeemPurchaseUrl} target="_blank" rel="noopener noreferrer">
|
||||
获取兑换码
|
||||
</a>
|
||||
) : null}
|
||||
</p>
|
||||
</div>
|
||||
<TicketCheck />
|
||||
</div>
|
||||
|
||||
@@ -45,6 +45,7 @@ export default function AppearanceSettingsPage() {
|
||||
const [footerCopyright, setFooterCopyright] = useState("");
|
||||
const [icpFilingEnabled, setIcpFilingEnabled] = useState(false);
|
||||
const [icpFilingNumber, setIcpFilingNumber] = useState("");
|
||||
const [redeemPurchaseUrl, setRedeemPurchaseUrl] = useState("");
|
||||
const [files, setFiles] = useState<DraftFiles>(EMPTY_FILES);
|
||||
const [resets, setResets] = useState<ResetState>(EMPTY_RESETS);
|
||||
const [loading, setLoading] = useState(true);
|
||||
@@ -79,6 +80,7 @@ export default function AppearanceSettingsPage() {
|
||||
normalizeSingleLine(footerCopyright) !== setting?.footerCopyright ||
|
||||
icpFilingEnabled !== setting?.icpFilingEnabled ||
|
||||
normalizeSingleLine(icpFilingNumber) !== setting?.icpFilingNumber ||
|
||||
normalizeSingleLine(redeemPurchaseUrl) !== setting?.redeemPurchaseUrl ||
|
||||
Object.values(files).some(Boolean) ||
|
||||
Object.values(resets).some(Boolean));
|
||||
const blocker = useBlocker(dirty && !saving && !restoring);
|
||||
@@ -103,6 +105,7 @@ export default function AppearanceSettingsPage() {
|
||||
setFooterCopyright(value.footerCopyright);
|
||||
setIcpFilingEnabled(value.icpFilingEnabled);
|
||||
setIcpFilingNumber(value.icpFilingNumber);
|
||||
setRedeemPurchaseUrl(value.redeemPurchaseUrl);
|
||||
setFiles(EMPTY_FILES);
|
||||
setResets(EMPTY_RESETS);
|
||||
setLoadError("");
|
||||
@@ -214,6 +217,7 @@ export default function AppearanceSettingsPage() {
|
||||
setFooterCopyright(setting.footerCopyright);
|
||||
setIcpFilingEnabled(setting.icpFilingEnabled);
|
||||
setIcpFilingNumber(setting.icpFilingNumber);
|
||||
setRedeemPurchaseUrl(setting.redeemPurchaseUrl);
|
||||
setFiles(EMPTY_FILES);
|
||||
setResets(EMPTY_RESETS);
|
||||
Object.values(inputRefs).forEach((ref) => {
|
||||
@@ -289,6 +293,7 @@ export default function AppearanceSettingsPage() {
|
||||
const nextSeoKeywords = normalizeSingleLine(seoKeywords);
|
||||
const nextFooterCopyright = normalizeSingleLine(footerCopyright);
|
||||
const nextIcpFilingNumber = normalizeSingleLine(icpFilingNumber);
|
||||
const nextRedeemPurchaseUrl = normalizeSingleLine(redeemPurchaseUrl);
|
||||
if (!nextBrandName || Array.from(nextBrandName).length > 40) {
|
||||
setActiveTab("brand");
|
||||
message.error("品牌名称必须为 1 到 40 个字符");
|
||||
@@ -327,6 +332,16 @@ export default function AppearanceSettingsPage() {
|
||||
message.error("显示备案号前请先填写备案号");
|
||||
return;
|
||||
}
|
||||
if (nextRedeemPurchaseUrl) {
|
||||
try {
|
||||
const parsed = new URL(nextRedeemPurchaseUrl);
|
||||
if (parsed.protocol !== "https:" || !parsed.host) throw new Error("invalid");
|
||||
} catch {
|
||||
setActiveTab("footer");
|
||||
message.error("兑换码购买链接必须是有效的 HTTPS 地址");
|
||||
return;
|
||||
}
|
||||
}
|
||||
const skinError = validateSkinDrafts(skinThemes, skinId);
|
||||
if (skinError) {
|
||||
setActiveTab("skins");
|
||||
@@ -370,6 +385,7 @@ export default function AppearanceSettingsPage() {
|
||||
footerCopyright: nextFooterCopyright,
|
||||
icpFilingEnabled,
|
||||
icpFilingNumber: nextIcpFilingNumber,
|
||||
redeemPurchaseUrl: nextRedeemPurchaseUrl,
|
||||
});
|
||||
applySetting(updated);
|
||||
commitPublicAppearance(updated.public);
|
||||
@@ -732,6 +748,9 @@ export default function AppearanceSettingsPage() {
|
||||
<Form.Item label="备案号" extra="请填写真实备案号,例如“蜀ICP备XXXXXXXX号”;系统不会替你申请或核验备案。">
|
||||
<Input value={icpFilingNumber} maxLength={64} showCount placeholder="例如:蜀ICP备XXXXXXXX号" onChange={(event) => setIcpFilingNumber(event.target.value)} />
|
||||
</Form.Item>
|
||||
<Form.Item label="兑换码购买链接" extra="填写 HTTPS 购买页面地址;留空则不显示“获取兑换码”入口。">
|
||||
<Input value={redeemPurchaseUrl} maxLength={500} showCount placeholder="例如:https://wzyp.cn/shop/JPDK4SRD" onChange={(event) => setRedeemPurchaseUrl(event.target.value)} />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
<div className="admin-appearance-logo-frame-option">
|
||||
<div className="admin-appearance-logo-frame-copy">
|
||||
|
||||
@@ -26,6 +26,7 @@ export type PublicAppearance = {
|
||||
footerCopyright: string;
|
||||
icpFilingEnabled: boolean;
|
||||
icpFilingNumber: string;
|
||||
redeemPurchaseUrl: string;
|
||||
logoConfigured: boolean;
|
||||
darkLogoConfigured: boolean;
|
||||
authVideoConfigured: boolean;
|
||||
@@ -57,6 +58,7 @@ export type AdminAppearance = {
|
||||
footerCopyright: string;
|
||||
icpFilingEnabled: boolean;
|
||||
icpFilingNumber: string;
|
||||
redeemPurchaseUrl: string;
|
||||
public: PublicAppearance;
|
||||
configured: boolean;
|
||||
updatedBy?: string;
|
||||
@@ -107,6 +109,7 @@ export async function updateAdminAppearance(
|
||||
| "footerCopyright"
|
||||
| "icpFilingEnabled"
|
||||
| "icpFilingNumber"
|
||||
| "redeemPurchaseUrl"
|
||||
>,
|
||||
) {
|
||||
const result = await http.patch<{ setting: AdminAppearance }>("/admin/settings/appearance", input);
|
||||
|
||||
@@ -27,6 +27,7 @@ export const DEFAULT_PUBLIC_APPEARANCE: PublicAppearance = {
|
||||
footerCopyright: `© ${new Date().getFullYear()} 影策. All rights reserved.`,
|
||||
icpFilingEnabled: false,
|
||||
icpFilingNumber: "",
|
||||
redeemPurchaseUrl: "",
|
||||
logoConfigured: false,
|
||||
darkLogoConfigured: false,
|
||||
authVideoConfigured: false,
|
||||
@@ -61,6 +62,7 @@ export function normalizePublicAppearance(value?: Partial<PublicAppearance> | nu
|
||||
const seoKeywords = normalizeAppearanceCopy(value?.seoKeywords, "", true);
|
||||
const footerCopyright = normalizeAppearanceCopy(value?.footerCopyright, `© ${new Date().getFullYear()} ${resolvedBrandName}. All rights reserved.`);
|
||||
const icpFilingNumber = normalizeAppearanceCopy(value?.icpFilingNumber, "", true);
|
||||
const redeemPurchaseUrl = typeof value?.redeemPurchaseUrl === "string" ? safeAppearanceURL(value.redeemPurchaseUrl, "") : "";
|
||||
return {
|
||||
...DEFAULT_PUBLIC_APPEARANCE,
|
||||
...value,
|
||||
@@ -85,6 +87,7 @@ export function normalizePublicAppearance(value?: Partial<PublicAppearance> | nu
|
||||
footerCopyright,
|
||||
icpFilingEnabled: Boolean(value?.icpFilingEnabled && icpFilingNumber),
|
||||
icpFilingNumber,
|
||||
redeemPurchaseUrl,
|
||||
logoConfigured: Boolean(value?.logoConfigured),
|
||||
darkLogoConfigured: Boolean(value?.darkLogoConfigured),
|
||||
authVideoConfigured: customVideo,
|
||||
|
||||
@@ -17034,6 +17034,31 @@ html:not(.dark) .creation-home .creation-user-message-copy-wrap { box-shadow: 0
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.workspace-wallet-redeem-link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
margin-left: 8px;
|
||||
border-radius: 6px;
|
||||
background: var(--user-accent-soft);
|
||||
padding: 3px 9px;
|
||||
color: var(--user-accent) !important;
|
||||
font-size: 13px;
|
||||
font-weight: 650;
|
||||
line-height: 1.5;
|
||||
text-decoration: none;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.workspace-wallet-redeem-link:hover {
|
||||
background: color-mix(in srgb, var(--user-accent) 16%, transparent);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.workspace-wallet-redeem-link:focus-visible {
|
||||
outline: 2px solid var(--user-accent);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.workspace-wallet-section-heading > svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
|
||||
Reference in New Issue
Block a user