Browse Source

Merge branch 'feature/interaction' of https://git.yuanzoo.cn/weitao/popiart-node-canvas into feature/interaction

feature/interaction
TianYun 1 month ago
parent
commit
81d51882bf
  1. 48
      src/app/api/content/privacy-policy/latest/route.ts
  2. 5
      src/app/api/recommend-banners/route.ts
  3. 2
      src/app/globals.css
  4. 2
      src/app/page.tsx
  5. 12
      src/components/HomeProjectsContent.tsx
  6. 73
      src/components/auth/LoginModal.tsx
  7. 20
      src/i18n/index.tsx
  8. 68
      src/utils/openLatestPrivacyPolicyLink.ts

48
src/app/api/content/privacy-policy/latest/route.ts

@ -0,0 +1,48 @@
import { NextRequest, NextResponse } from "next/server";
import { getRequestToken } from "@/app/api/_auth";
const UPSTREAM_PRIVACY_POLICY_LATEST_PATH = "/api_client/content/privacyPolicy/latest";
function getPopiBaseUrl(request: NextRequest): string {
const baseUrl =
process.env.POPIART_API_BASE_URL ||
process.env.NEXT_PUBLIC_APP_URL ||
request.nextUrl.origin;
return baseUrl.replace(/\/+$/, "");
}
export async function GET(request: NextRequest) {
const key = request.nextUrl.searchParams.get("key")?.trim();
if (!key) {
return NextResponse.json(
{ status: "4000", message: "key is required" },
{ status: 400 },
);
}
const upstreamUrl = new URL(getPopiBaseUrl(request) + UPSTREAM_PRIVACY_POLICY_LATEST_PATH);
upstreamUrl.searchParams.set("key", key);
const headers: Record<string, string> = {
Accept: "application/json",
};
const token = getRequestToken(request);
if (token) {
headers.Authorization = `Bearer ${token}`;
headers.token = token;
}
const upstream = await fetch(upstreamUrl, {
method: "GET",
headers,
cache: "no-store",
});
const body = await upstream.text();
return new NextResponse(body, {
status: upstream.status,
headers: {
"Content-Type": upstream.headers.get("Content-Type") || "application/json",
},
});
}

5
src/app/api/recommend-banners/route.ts

@ -9,7 +9,6 @@ function getPopiBaseUrl() {
export async function GET(request: NextRequest) {
const baseUrl = getPopiBaseUrl();
console.log("request", request);
if (!baseUrl) {
return NextResponse.json(
{ status: "5000", message: "POPIART_API_BASE_URL is not configured" },
@ -20,8 +19,8 @@ export async function GET(request: NextRequest) {
console.log("token", token);
const upstreamUrl = new URL(UPSTREAM_RECOMMEND_BANNER_LIST_PATH, baseUrl);
upstreamUrl.searchParams.set(
"categoryCode",
request.nextUrl.searchParams.get("categoryCode") || "",
"origin",
request.nextUrl.searchParams.get("origin") || "",
);
const response = await fetch(upstreamUrl, {

2
src/app/globals.css

@ -34,6 +34,8 @@ body {
overscroll-behavior-x: none;
/* Prevent touch gestures from triggering navigation */
touch-action: pan-y pinch-zoom;
overflow-y: initial !important;
width: 100% !important;
}
/* React Flow customizations */

2
src/app/page.tsx

@ -57,7 +57,7 @@ function isVideoUrl(url: string) {
async function getHeroCards(): Promise<HeroCardData[]> {
try {
const params = new URLSearchParams({ categoryCode: "" });
const params = new URLSearchParams({ origin: "canvas" });
const response = await authFetch(`/api/recommend-banners?${params.toString()}`, {
method: "GET",
});

12
src/components/HomeProjectsContent.tsx

@ -32,7 +32,7 @@ import {
updateCanvasWorkflow,
uploadCanvasWorkflowCover,
} from "@/lib/canvasWorkflowApi";
import { Form, Input, Modal, message } from "antd";
import { Form, Input, Modal, Tooltip, message } from "antd";
type Translate = (key: TranslationKey, values?: Record<string, string | number>) => string;
type ProjectData = {
@ -225,9 +225,11 @@ function ShowCard({ id, title, subtitle, image, avatar, vip, data, onOpen }: Sho
</span>
) : null} */}
</div>
<p className="mt-2 truncate text-[13px] leading-tight text-[#666]">
{subtitle || '-'}
</p>
<Tooltip title={subtitle || '-'} placement="top">
<p className="mt-2 truncate text-[13px] leading-tight text-[#666]">
{subtitle || '-'}
</p>
</Tooltip>
</div>
</article>
);
@ -1496,7 +1498,7 @@ function ShowsSection() {
const workflowId = (result.data as any)?.id;
router.push(workflowId ? `/canvas?workflowId=${workflowId}` : "/canvas");
} catch (error) {
window.alert(error instanceof Error ? error.message : t("home.createFailed"));
message.error(error instanceof Error ? error.message : t("home.createFailed"));
setIsCreating(false);
}
}, [isCreating, router, t]);

73
src/components/auth/LoginModal.tsx

@ -5,6 +5,11 @@ import { Alert, Button, Checkbox, Input, Modal, Segmented } from "antd";
import { useI18n } from "@/i18n";
import { useLoginModalStore } from "@/store/loginModalStore";
import { useUserStore } from "@/store/userStore";
import {
openLatestPrivacyPolicyLink,
PRIVACY_POLICY_KEY_PRIVACY_POLICY,
PRIVACY_POLICY_KEY_USER_TERMS,
} from "@/utils/openLatestPrivacyPolicyLink";
type LoginMode = "code" | "wechat" | "password";
type CodePurpose = "login" | "wechat-bind";
@ -30,6 +35,12 @@ const getTokenFromResponse = (payload: ApiResponse): string | null => {
return null;
};
function handleAgreementLinkClick(key: string): void {
void openLatestPrivacyPolicyLink(key).catch((error) => {
console.error("Failed to open agreement link:", error);
});
}
async function readJsonResponse(response: Response, fallbackMessage: string): Promise<ApiResponse> {
const payload = (await response.json()) as ApiResponse;
if (!response.ok || payload.status !== "0000") {
@ -505,24 +516,22 @@ export function LoginModal() {
{t("auth.wechatReloadQr")}
</Button>
<p className="text-center text-xs leading-5 text-neutral-500">
<a
href="https://www.popi.art/content/userTerms"
target="_blank"
rel="noreferrer"
className="text-blue-500 hover:underline"
{t("auth.agreementPrefix")}
<button
type="button"
onClick={() => handleAgreementLinkClick(PRIVACY_POLICY_KEY_USER_TERMS)}
className="appearance-none border-0 bg-transparent p-0 text-blue-500 hover:underline"
>
</a>
<a
href="https://www.popi.art/content/privacyPolicy"
target="_blank"
rel="noreferrer"
className="text-blue-500 hover:underline"
{t("auth.userAgreement")}
</button>
{t("common.and")}
<button
type="button"
onClick={() => handleAgreementLinkClick(PRIVACY_POLICY_KEY_PRIVACY_POLICY)}
className="appearance-none border-0 bg-transparent p-0 text-blue-500 hover:underline"
>
</a>
{t("auth.privacyPolicy")}
</button>
</p>
</div>
)
@ -590,25 +599,23 @@ export function LoginModal() {
onChange={(event) => setAgreementChecked(event.target.checked)}
className="text-xs leading-5 text-neutral-500"
>
<a
href="https://www.popi.art/content/userTerms"
target="_blank"
rel="noreferrer"
className="text-blue-500 hover:underline"
{t("auth.agreementPrefix")}
<button
type="button"
onClick={() => handleAgreementLinkClick(PRIVACY_POLICY_KEY_USER_TERMS)}
className="appearance-none border-0 bg-transparent p-0 text-blue-500 hover:underline"
>
</a>
<a
href="https://www.popi.art/content/privacyPolicy"
target="_blank"
rel="noreferrer"
className="text-blue-500 hover:underline"
{t("auth.userAgreement")}
</button>
{t("common.and")}
<button
type="button"
onClick={() => handleAgreementLinkClick(PRIVACY_POLICY_KEY_PRIVACY_POLICY)}
className="appearance-none border-0 bg-transparent p-0 text-blue-500 hover:underline"
>
</a>
{t("auth.privacyPolicy")}
</button>
{t("auth.autoRegisterSuffix")}
</Checkbox>
</>
)}

20
src/i18n/index.tsx

@ -23,6 +23,7 @@ const en = {
"common.back": "Back",
"common.next": "Next",
"common.cancel": "Cancel",
"common.and": " and ",
"common.close": "Close",
"common.save": "Save",
"common.create": "Create",
@ -181,6 +182,10 @@ const en = {
"auth.wechatMissingBindToken": "Missing WeChat phone binding token",
"auth.wechatPollFailed": "Failed to check WeChat login status. Reload the QR code.",
"auth.bindPhoneFailed": "Failed to bind phone",
"auth.agreementPrefix": "By logging in, you agree to",
"auth.userAgreement": "User Agreement",
"auth.privacyPolicy": "Privacy Policy",
"auth.autoRegisterSuffix": ", and unregistered phone numbers will be registered automatically",
"quickstart.description": "A node based workflow editor for generative AI pipelines. Connect nodes to build pipelines that transform and generate images, video, audio and 3D assets.",
"quickstart.docs": "Docs",
"quickstart.discord": "Discord",
@ -880,6 +885,10 @@ const zhCNAuth = {
"auth.wechatMissingBindToken": "微信登录缺少绑定凭证",
"auth.wechatPollFailed": "微信登录状态检查失败,请重新加载二维码",
"auth.bindPhoneFailed": "手机号绑定失败",
"auth.agreementPrefix": "登录即代表同意",
"auth.userAgreement": "《用户协议》",
"auth.privacyPolicy": "《隐私政策》",
"auth.autoRegisterSuffix": ",未注册手机号将自动注册",
} satisfies Pick<Dictionary, AuthTranslationKey>;
const zhTWAuth = {
@ -923,6 +932,10 @@ const zhTWAuth = {
"auth.wechatMissingBindToken": "微信登入缺少綁定憑證",
"auth.wechatPollFailed": "微信登入狀態檢查失敗,請重新載入 QR Code",
"auth.bindPhoneFailed": "手機號碼綁定失敗",
"auth.agreementPrefix": "登入即代表同意",
"auth.userAgreement": "《使用者協議》",
"auth.privacyPolicy": "《隱私政策》",
"auth.autoRegisterSuffix": ",未註冊手機號碼將自動註冊",
} satisfies Pick<Dictionary, AuthTranslationKey>;
const jaAuth = {
@ -966,11 +979,16 @@ const jaAuth = {
"auth.wechatMissingBindToken": "WeChat ログインの紐付け情報が不足しています",
"auth.wechatPollFailed": "WeChat ログイン状態の確認に失敗しました。QR コードを再読み込みしてください。",
"auth.bindPhoneFailed": "電話番号の紐付けに失敗しました",
"auth.agreementPrefix": "ログインすると、以下に同意したものとみなされます",
"auth.userAgreement": "ユーザー契約",
"auth.privacyPolicy": "プライバシーポリシー",
"auth.autoRegisterSuffix": "。未登録の電話番号は自動的に登録されます",
} satisfies Pick<Dictionary, AuthTranslationKey>;
const zhCN: Dictionary = {
...en,
...zhCNAuth,
"common.and": "和",
"language.label": "语言",
"home.closeBanner": "关闭公告",
"home.messages": "消息",
@ -1753,6 +1771,7 @@ Object.assign(zhCN, {
const zhTW: Dictionary = {
...zhCN,
...zhTWAuth,
"common.and": "和",
"home.closeBanner": "關閉公告",
"home.messages": "消息",
"home.scanContactService": "掃碼聯繫客服",
@ -2378,6 +2397,7 @@ const zhTW: Dictionary = {
const ja: Dictionary = {
...en,
...jaAuth,
"common.and": "、",
"home.closeBanner": "お知らせを閉じる",
"home.messages": "メッセージ",
"home.scanContactService": "スキャンしてサポートに連絡",

68
src/utils/openLatestPrivacyPolicyLink.ts

@ -0,0 +1,68 @@
/** 内容协议 key:使用说明 */
export const PRIVACY_POLICY_KEY_USAGE_TUTORIAL = "web.usageTutorial";
/** 内容协议 key:AI 功能使用须知 */
export const PRIVACY_POLICY_KEY_AI_USE_NOTICE = "web.aiUseNotice";
/** 内容协议 key:用户协议 */
export const PRIVACY_POLICY_KEY_USER_TERMS = "web.userTerms";
/** 内容协议 key:隐私政策 */
export const PRIVACY_POLICY_KEY_PRIVACY_POLICY = "web.privacyPolicy";
/** 内容协议 key:“Popi.art”付费服务协议 */
export const PRIVACY_POLICY_KEY_PAYMENT_TERMS = "web.paymentTerms";
/** 内容协议 key:Popi 存证协议 */
export const PRIVACY_POLICY_KEY_EVIDENCE_PRESERVATION = "web.popiartEvidencePreservationAgreement";
/** 内容协议 key:Popi Future Creator 计划用户服务协议 */
export const PRIVACY_POLICY_KEY_FUTURE_CREATOR = "web.futureCreatorUserServiceAgreement";
export interface GetLatestPrivacyPolicyParams {
key: string;
}
export async function getLatestPrivacyPolicy({ key }: GetLatestPrivacyPolicyParams): Promise<unknown> {
const params = new URLSearchParams({ key });
const response = await fetch(`/api/content/privacy-policy/latest?${params.toString()}`, {
method: "GET",
});
const payload = (await response.json().catch(() => null)) as unknown;
if (!response.ok) {
const message =
typeof payload === "object" &&
payload !== null &&
"message" in payload &&
typeof payload.message === "string"
? payload.message
: "Failed to fetch latest privacy policy.";
throw new Error(message);
}
return payload;
}
interface PrivacyPolicyItem {
id: number;
title: string;
key: string;
type: number;
content: string;
status: number;
}
interface PrivacyPolicyResponse {
data?: PrivacyPolicyItem;
message?: string;
status?: string;
}
/** 按 key 拉取最新协议/说明,type 为 2 时在新标签页打开 content 链接 */
export async function openLatestPrivacyPolicyLink(key: string): Promise<void> {
const res = (await getLatestPrivacyPolicy({ key })) as PrivacyPolicyResponse;
if (res.status && res.status !== "0000") {
throw new Error(res.message || "Failed to fetch latest privacy policy.");
}
const item = res.data;
if (!item?.content || item.type !== 2) {
throw new Error(res.message || "Latest privacy policy link is unavailable.");
}
window.open(item.content, "_blank", "noopener,noreferrer");
}
Loading…
Cancel
Save