feat: 自定义版本功能更新
Build and Release / Build (push) Has been cancelled

- 新增家庭共享订阅管理
- 新增用户邀请统计
- 新增签名和订阅模式设置表单
- 更新 API 服务层和国际化文件
- UI 组件优化(enhanced-input、pro-table)
This commit is contained in:
2026-03-19 01:56:13 -07:00
parent 31803a1d24
commit 6b92979c7c
82 changed files with 3266 additions and 739 deletions
+11 -5
View File
@@ -15,6 +15,7 @@ import {
DropdownMenuTrigger,
} from "@workspace/ui/components/dropdown-menu";
import { Icon } from "@workspace/ui/composed/icon";
import { shortenDeviceIdentifier } from "@workspace/ui/utils/device";
import { useTranslation } from "react-i18next";
import { useNavs } from "@/layout/navs";
import { useGlobalStore } from "@/stores/global";
@@ -32,6 +33,13 @@ export function UserNav() {
};
if (user) {
const method = user?.auth_methods?.[0];
const isDevice = method?.auth_type === "device";
const rawIdentifier = method?.auth_identifier ?? "";
const displayName = isDevice
? shortenDeviceIdentifier(rawIdentifier)
: rawIdentifier.split("@")[0] || "";
return (
<DropdownMenu modal={false}>
<DropdownMenuTrigger asChild>
@@ -40,16 +48,14 @@ export function UserNav() {
<AvatarImage
alt={user?.avatar ?? ""}
className="object-cover"
src={user?.auth_methods?.[0]?.auth_identifier ?? ""}
src={isDevice ? "" : rawIdentifier}
/>
<AvatarFallback className="bg-linear-to-br from-primary/90 to-primary font-medium text-background">
{user?.auth_methods?.[0]?.auth_identifier
.toUpperCase()
.charAt(0)}
{displayName.toUpperCase().charAt(0)}
</AvatarFallback>
</Avatar>
<span className="max-w-10 truncate text-sm sm:max-w-[100px]">
{user?.auth_methods?.[0]?.auth_identifier.split("@")[0]}
{displayName}
</span>
<Icon
className="size-4 text-muted-foreground"
+3 -3
View File
@@ -26,7 +26,7 @@ export default function Announcement({ type }: { type: "popup" | "pinned" }) {
const result = await queryAnnouncement(
{
page: 1,
size: 10,
size: 200,
pinned: type === "pinned",
popup: type === "popup",
},
@@ -52,7 +52,7 @@ export default function Announcement({ type }: { type: "popup" | "pinned" }) {
<DialogHeader>
<DialogTitle>{data.title}</DialogTitle>
</DialogHeader>
<div className="prose prose-sm max-w-none dark:prose-invert">
<div className="prose prose-sm dark:prose-invert max-w-none">
<Markdown>{data.content}</Markdown>
</div>
</DialogContent>
@@ -69,7 +69,7 @@ export default function Announcement({ type }: { type: "popup" | "pinned" }) {
</h2>
<Card className="p-6">
{data.content ? (
<div className="prose prose-sm max-w-none dark:prose-invert">
<div className="prose prose-sm dark:prose-invert max-w-none">
<Markdown>{data.content}</Markdown>
</div>
) : (
@@ -13,12 +13,12 @@ import {
TabsList,
TabsTrigger,
} from "@workspace/ui/components/tabs";
import { Icon } from "@workspace/ui/composed/icon";
import { Markdown } from "@workspace/ui/composed/markdown";
import {
ProList,
type ProListActions,
} from "@workspace/ui/composed/pro-list/pro-list";
import { Icon } from "@workspace/ui/composed/icon";
import { Markdown } from "@workspace/ui/composed/markdown";
import { queryAnnouncement } from "@workspace/ui/services/user/announcement";
import { formatDate } from "@workspace/ui/utils/formatting";
import { useRef, useState } from "react";
@@ -60,13 +60,13 @@ export default function Announcement() {
<div className="flex items-center gap-2">
<CardTitle className="text-lg">{item.title}</CardTitle>
{item.pinned && (
<span className="flex items-center gap-1 rounded-full bg-primary/10 px-2 py-0.5 text-xs font-medium text-primary">
<span className="flex items-center gap-1 rounded-full bg-primary/10 px-2 py-0.5 font-medium text-primary text-xs">
<Icon className="size-3" icon="uil:pin" />
{t("pinned", "Pinned")}
</span>
)}
{item.popup && (
<span className="flex items-center gap-1 rounded-full bg-orange-500/10 px-2 py-0.5 text-xs font-medium text-orange-500">
<span className="flex items-center gap-1 rounded-full bg-orange-500/10 px-2 py-0.5 font-medium text-orange-500 text-xs">
<Icon className="size-3" icon="uil:popcorn" />
{t("popup", "Popup")}
</span>
@@ -80,7 +80,7 @@ export default function Announcement() {
</div>
</CardHeader>
<CardContent>
<div className="prose prose-sm max-w-none dark:prose-invert">
<div className="prose prose-sm dark:prose-invert max-w-none">
<Markdown>{item.content}</Markdown>
</div>
</CardContent>
@@ -95,9 +95,7 @@ export default function Announcement() {
</h2>
<Tabs defaultValue="all" onValueChange={setActiveTab}>
<TabsList>
<TabsTrigger value="all">
{t("all", "All")}
</TabsTrigger>
<TabsTrigger value="all">{t("all", "All")}</TabsTrigger>
<TabsTrigger value="pinned">
{t("pinnedOnly", "Pinned Only")}
</TabsTrigger>
@@ -110,9 +108,7 @@ export default function Announcement() {
<ProList
action={normalRef}
renderItem={renderAnnouncement}
request={async (pagination) => {
return requestAnnouncements(pagination, {});
}}
request={async (pagination) => requestAnnouncements(pagination, {})}
/>
</TabsContent>
@@ -120,9 +116,9 @@ export default function Announcement() {
<ProList
action={pinnedRef}
renderItem={renderAnnouncement}
request={async (pagination) => {
return requestAnnouncements(pagination, { pinned: true });
}}
request={async (pagination) =>
requestAnnouncements(pagination, { pinned: true })
}
/>
</TabsContent>
@@ -130,9 +126,9 @@ export default function Announcement() {
<ProList
action={normalRef}
renderItem={renderAnnouncement}
request={async (pagination) => {
return requestAnnouncements(pagination, { popup: true });
}}
request={async (pagination) =>
requestAnnouncements(pagination, { popup: true })
}
/>
</TabsContent>
</Tabs>
@@ -25,7 +25,9 @@ export default function RedeemCode({ onSuccess }: RedeemCodeProps) {
const redeemMutation = useMutation({
mutationFn: (code: string) => redeemCode({ code }),
onSuccess: (response) => {
const message = (response.data as { message?: string })?.message || t("redeemSuccess", "兑换成功");
const message =
(response.data as { message?: string })?.message ||
t("redeemSuccess", "兑换成功");
toast.success(message);
setCode("");
onSuccess?.();
@@ -64,10 +66,7 @@ export default function RedeemCode({ onSuccess }: RedeemCodeProps) {
<Input
id="redemption-code"
onChange={(e) => setCode(e.target.value)}
placeholder={t(
"enterRedemptionCode",
"请输入兑换码"
)}
placeholder={t("enterRedemptionCode", "请输入兑换码")}
value={code}
/>
<Button
@@ -65,7 +65,11 @@ export default function Page() {
orderNo: order_no!,
returnUrl: window.location.href,
});
if (data.data?.type === "url" && data.data.checkout_url && !paymentOpened) {
if (
data.data?.type === "url" &&
data.data.checkout_url &&
!paymentOpened
) {
window.open(data.data.checkout_url, "_blank");
setPaymentOpened(true);
}
@@ -31,6 +31,7 @@ import {
updateBindEmail,
updateBindMobile,
} from "@workspace/ui/services/user/user";
import { shortenDeviceIdentifier } from "@workspace/ui/utils/device";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { useTranslation } from "react-i18next";
@@ -287,6 +288,14 @@ export default function ThirdPartyAccounts() {
? currentValue
: method?.auth_identifier || "";
break;
case "device":
displayValue = method?.auth_identifier
? shortenDeviceIdentifier(method.auth_identifier)
: t(
`thirdParty.${account.id}.description`,
account.descriptionDefault
);
break;
default:
displayValue =
method?.auth_identifier ||