terry-staging #1

Merged
terry merged 8 commits from terry-staging into main 2026-05-26 06:53:11 +00:00
13 changed files with 53 additions and 50 deletions
Showing only changes of commit f482a2ec38 - Show all commits

View File

@@ -15,7 +15,7 @@ status: draft
- API base前端通过 `VITE_API_URL` 指向后端;本地可同源 `/api`
- 上传文件可通过 `/uploads/...` 或完整 URL 返回;前端会用 `assetUrl()` 处理相对路径。
- 所有时间字段使用 ISO 8601 字符串,例如 `2026-05-24T14:42:00.000Z`
- 语言字段:`zh` / `en` / `ja` / `ko` / `vi` / `id` / `ms`;默认语言为 `en`。中文只有简体 `zh`,没有繁体中文。
- 语言字段:`zh-CN` / `en` / `ja` / `ko` / `vi` / `id` / `ms`;默认语言为 `en`。中文只有简体 `zh-CN`,没有繁体中文。
- 错误格式:非 2xx + text/plain 或 JSON 均可;前端会显示错误文本。
- Admin 接口需要 `Authorization: Bearer <token>`
@@ -57,7 +57,7 @@ type Post = {
id: string;
categoryId: number;
categorySlug: string;
language: "zh" | "en" | "ja" | "ko" | "vi" | "id" | "ms";
language: "zh-CN" | "en" | "ja" | "ko" | "vi" | "id" | "ms";
text?: string;
attachments: Attachment[];
isRecommended: boolean;
@@ -449,7 +449,7 @@ Request:
```ts
type UpsertPostPayload = {
categoryId: number;
language: "zh" | "en" | "ja" | "ko" | "vi" | "id" | "ms";
language: "zh-CN" | "en" | "ja" | "ko" | "vi" | "id" | "ms";
text?: string;
attachments: Attachment[];
isPublic: boolean;

View File

@@ -33,7 +33,7 @@ type Post = {
id: string;
categoryId: number;
categorySlug: string;
language: string; // "zh" | "en" | "ja" | "ko" | "vi" | "id" | "ms"
language: string; // "zh-CN" | "en" | "ja" | "ko" | "vi" | "id" | "ms"
text?: string; // 可选,纯文本/图说;前端做 https → 链接自动识别
attachments: Attachment[]; // 0~Ntext-only post 时为 []
isRecommended: boolean;
@@ -72,7 +72,7 @@ Query 参数:
| `lang` | 是 | UI 语言;后端可据此选择不同语言版本的 `text` |
| `category` | 否 | category slug不传 = 全部分类 |
| `type` | 否 | `all` / `image` / `video` / `pdf` / `ppt` / `text` / `link` / `archive`;语义见 §3 |
| `language` | 否 | 资源语言:`zh` / `en` / `ja` / `ko` / `vi` / `id` / `ms` |
| `language` | 否 | 资源语言:`zh-CN` / `en` / `ja` / `ko` / `vi` / `id` / `ms` |
| `cursor` | 否 | 上一次返回的 `nextCursor`;不传 = 第一页 |
| `limit` | 否 | 默认 20最大 50 |

View File

@@ -68,7 +68,7 @@ src/
App.tsx # public app + optional admin routes
AppAdminOnly.tsx # admin-only app entry
api.ts # fetch helpers and shared API types
i18n.tsx # zh / en / ja / ko / vi / id / ms dictionary
i18n.tsx # zh-CN / en / ja / ko / vi / id / ms dictionary
adminPaths.ts # admin UI prefix logic
adminRouteTree.tsx # admin routes
components/ # reusable public components

View File

@@ -3,5 +3,5 @@ import { tLang } from "../i18n";
/** Admin area always uses Chinese, independent of site language. */
export function useAdminT() {
return useCallback((key: string) => tLang("zh", key), []);
return useCallback((key: string) => tLang("zh-CN", key), []);
}

View File

@@ -26,7 +26,7 @@ export function MessageStream({ scope }: MessageStreamProps) {
const { items, isLoading, error, hasMore, loadMore, reset } =
usePostStream(params);
const groups = useGroupedByDay(items, lang);
const retryLabel = lang === "zh" ? "重试" : "Retry";
const retryLabel = lang === "zh-CN" ? "重试" : "Retry";
const sentinelRef = useRef<HTMLDivElement>(null);
const hasMoreRef = useRef(hasMore);

View File

@@ -8,7 +8,7 @@ function makePost(id: string, isoDate: string): Post {
id,
categoryId: 1,
categorySlug: "x",
language: "zh",
language: "zh-CN",
attachments: [],
isRecommended: false,
publishedAt: isoDate,
@@ -25,7 +25,7 @@ describe("useGroupedByDay", () => {
makePost("c", "2026-02-28T01:00:00.000Z"),
makePost("d", "2026-05-16T12:00:00.000Z"),
];
const { result } = renderHook(() => useGroupedByDay(posts, "zh"));
const { result } = renderHook(() => useGroupedByDay(posts, "zh-CN"));
expect(result.current.length).toBeGreaterThanOrEqual(2);
const allIds = result.current.flatMap((g) => g.items.map((p) => p.id));
expect(allIds).toEqual(["a", "b", "c", "d"]);
@@ -47,7 +47,7 @@ describe("useGroupedByDay", () => {
});
it("returns empty array for empty input", () => {
const { result } = renderHook(() => useGroupedByDay([], "zh"));
const { result } = renderHook(() => useGroupedByDay([], "zh-CN"));
expect(result.current).toEqual([]);
});
});

View File

@@ -35,10 +35,10 @@ function dayLabel(iso: string, lang: string): string {
a.getMonth() === b.getMonth() &&
a.getDate() === b.getDate();
if (isSameDay(d, today)) {
return lang === "zh" ? "今天" : "Today";
return lang === "zh-CN" ? "今天" : "Today";
}
if (isSameDay(d, yesterday)) {
return lang === "zh" ? "昨天" : "Yesterday";
return lang === "zh-CN" ? "昨天" : "Yesterday";
}
return new Intl.DateTimeFormat(localeFor(lang), {
month: "long",

View File

@@ -6,7 +6,7 @@ import React, {
useState,
} from "react";
export type Lang = "zh" | "en" | "ja" | "ko" | "vi" | "id" | "ms";
export type Lang = "zh-CN" | "en" | "ja" | "ko" | "vi" | "id" | "ms";
type Dict = Record<string, string>;
@@ -82,7 +82,7 @@ const zhDict: Dict = {
walletMissingProjectId:
"请配置 VITE_WALLETCONNECT_PROJECT_IDReown Cloud否则无法使用 WalletConnect/扫码。",
walletSetupNeeded: "钱包扫码未启用(请在服务器配置环境变量)",
lang_zh: "中文",
lang_zh_CN: "中文",
lang_en: "English",
lang_ja: "日本語",
lang_ko: "한국어",
@@ -208,7 +208,7 @@ const enDict: Dict = {
walletMissingProjectId:
"Set VITE_WALLETCONNECT_PROJECT_ID (free on Reown Cloud). Required for WalletConnect / QR login.",
walletSetupNeeded: "Wallet QR login disabled (set env on server)",
lang_zh: "Chinese",
lang_zh_CN: "Chinese",
lang_en: "English",
lang_ja: "Japanese",
lang_ko: "Korean",
@@ -262,8 +262,8 @@ const enDict: Dict = {
};
const languageNames: Record<Lang, Dict> = {
zh: {
lang_zh: "中文",
"zh-CN": {
lang_zh_CN: "中文",
lang_en: "English",
lang_ja: "日本語",
lang_ko: "한국어",
@@ -272,7 +272,7 @@ const languageNames: Record<Lang, Dict> = {
lang_ms: "Bahasa Melayu",
},
en: {
lang_zh: "Chinese",
lang_zh_CN: "Chinese",
lang_en: "English",
lang_ja: "Japanese",
lang_ko: "Korean",
@@ -281,7 +281,7 @@ const languageNames: Record<Lang, Dict> = {
lang_ms: "Malay",
},
ja: {
lang_zh: "中国語",
lang_zh_CN: "中国語",
lang_en: "英語",
lang_ja: "日本語",
lang_ko: "韓国語",
@@ -290,7 +290,7 @@ const languageNames: Record<Lang, Dict> = {
lang_ms: "マレー語",
},
ko: {
lang_zh: "중국어",
lang_zh_CN: "중국어",
lang_en: "영어",
lang_ja: "일본어",
lang_ko: "한국어",
@@ -299,7 +299,7 @@ const languageNames: Record<Lang, Dict> = {
lang_ms: "말레이어",
},
vi: {
lang_zh: "Tiếng Trung",
lang_zh_CN: "Tiếng Trung",
lang_en: "Tiếng Anh",
lang_ja: "Tiếng Nhật",
lang_ko: "Tiếng Hàn",
@@ -308,7 +308,7 @@ const languageNames: Record<Lang, Dict> = {
lang_ms: "Tiếng Mã Lai",
},
id: {
lang_zh: "Bahasa Tionghoa",
lang_zh_CN: "Bahasa Tionghoa",
lang_en: "Bahasa Inggris",
lang_ja: "Bahasa Jepang",
lang_ko: "Bahasa Korea",
@@ -317,7 +317,7 @@ const languageNames: Record<Lang, Dict> = {
lang_ms: "Bahasa Melayu",
},
ms: {
lang_zh: "Bahasa Cina",
lang_zh_CN: "Bahasa Cina",
lang_en: "Bahasa Inggeris",
lang_ja: "Bahasa Jepun",
lang_ko: "Bahasa Korea",
@@ -328,7 +328,7 @@ const languageNames: Record<Lang, Dict> = {
};
const dict: Record<Lang, Dict> = {
zh: { ...zhDict, ...languageNames.zh },
"zh-CN": { ...zhDict, ...languageNames["zh-CN"] },
en: { ...enDict, ...languageNames.en },
ja: { ...enDict, ...languageNames.ja },
ko: { ...enDict, ...languageNames.ko },
@@ -351,9 +351,9 @@ const LANG_KEY = "ark_lang";
export function I18nProvider({ children }: { children: React.ReactNode }) {
const [lang, setLangState] = useState<Lang>(() => {
const s = localStorage.getItem(LANG_KEY);
if (s === "zh-CN" || s === "zh-TW") return "zh";
if (s === "zh" || s === "zh-TW") return "zh-CN";
if (
s === "zh" ||
s === "zh-CN" ||
s === "en" ||
s === "ja" ||
s === "ko" ||
@@ -383,5 +383,5 @@ export function useI18n() {
}
export function langQuery(lang: Lang) {
return lang === "zh" ? "zh-CN" : lang;
return lang;
}

View File

@@ -1,7 +1,7 @@
import type { Lang } from "./i18n";
export const LANG_OPTIONS: { code: Lang; label: string }[] = [
{ code: "zh", label: "中文" },
{ code: "zh-CN", label: "中文" },
{ code: "en", label: "English" },
{ code: "ja", label: "日本語" },
{ code: "ko", label: "한국어" },
@@ -12,6 +12,7 @@ export const LANG_OPTIONS: { code: Lang; label: string }[] = [
export function languageLabel(t: (key: string) => string, code: string) {
if (!code) return t("filterLanguageAll");
const label = t(`lang_${code}`);
return label === `lang_${code}` ? code : label;
const key = `lang_${code.replace("-", "_")}`;
const label = t(key);
return label === key ? code : label;
}

View File

@@ -31,7 +31,7 @@ export const MOCK_POSTS: Post[] = [
id: "p-001",
categoryId: 1,
categorySlug: "project",
language: "zh",
language: "zh-CN",
attachments: [
{
id: "a-001",
@@ -55,7 +55,7 @@ export const MOCK_POSTS: Post[] = [
id: "p-002",
categoryId: 1,
categorySlug: "project",
language: "zh",
language: "zh-CN",
attachments: [
{
id: "a-002",
@@ -79,7 +79,7 @@ export const MOCK_POSTS: Post[] = [
id: "p-013",
categoryId: 1,
categorySlug: "project",
language: "zh",
language: "zh-CN",
attachments: [
{
id: "a-013",
@@ -100,7 +100,7 @@ export const MOCK_POSTS: Post[] = [
id: "p-003",
categoryId: 2,
categorySlug: "guide",
language: "zh",
language: "zh-CN",
attachments: [
{
id: "a-003",
@@ -121,7 +121,7 @@ export const MOCK_POSTS: Post[] = [
id: "p-004",
categoryId: 2,
categorySlug: "guide",
language: "zh",
language: "zh-CN",
attachments: [
{
id: "a-004",
@@ -142,7 +142,7 @@ export const MOCK_POSTS: Post[] = [
id: "p-005",
categoryId: 3,
categorySlug: "data",
language: "zh",
language: "zh-CN",
text:
"📊 ARK DeFAI 各大平台现已上线 🔥\n\n" +
"🔷 市场数据平台\n" +
@@ -163,7 +163,7 @@ export const MOCK_POSTS: Post[] = [
id: "p-006",
categoryId: 3,
categorySlug: "data",
language: "zh",
language: "zh-CN",
text:
"📌 收取协议固定 2.5% 手续费。\n\n" +
"🔷 贡献值合约\n0x7736b5B84cADDB7661D250D10e60E31F3C905c99\n📌 用于新贡献值机制的 USDT 购买与资金流向管理(通缩销毁 / 储备 RBS",
@@ -178,7 +178,7 @@ export const MOCK_POSTS: Post[] = [
id: "p-007",
categoryId: 4,
categorySlug: "videos",
language: "zh",
language: "zh-CN",
text: "ARK 山东·东营社区 招商复盘·势位重塑\n🔥 ARK DeFai 相位偏移锁死增值弧度。质能裂变诱发认知风暴,海岱动能正于中原合围!🚀",
attachments: [
{
@@ -204,7 +204,7 @@ export const MOCK_POSTS: Post[] = [
id: "p-008",
categoryId: 5,
categorySlug: "poster",
language: "zh",
language: "zh-CN",
attachments: [
{
id: "a-008",
@@ -228,7 +228,7 @@ export const MOCK_POSTS: Post[] = [
id: "p-009",
categoryId: 5,
categorySlug: "poster",
language: "zh",
language: "zh-CN",
attachments: [
{
id: "a-009",
@@ -252,7 +252,7 @@ export const MOCK_POSTS: Post[] = [
id: "p-010",
categoryId: 6,
categorySlug: "meeting",
language: "zh",
language: "zh-CN",
text: "📌 ARK DeFAI 方舟晨间时刻\n\n🧠 会议主题:市场概况交流 & 市场问题讨论。\n🕙 会议时间3月1日10:00\n🎬 直播腾讯会议链接https://meeting.tencent.com/l/G718S4Sedm38",
attachments: [
{
@@ -277,7 +277,7 @@ export const MOCK_POSTS: Post[] = [
id: "p-011",
categoryId: 5,
categorySlug: "poster",
language: "zh",
language: "zh-CN",
attachments: [
{
id: "a-011a",
@@ -323,7 +323,7 @@ export const MOCK_POSTS: Post[] = [
id: "p-012",
categoryId: 5,
categorySlug: "poster",
language: "zh",
language: "zh-CN",
attachments: Array.from({ length: 7 }).map((_, i) => ({
id: `a-012-${i}`,
kind: "image" as const,

View File

@@ -37,7 +37,7 @@ export function AdminResourceForm() {
const [title, setTitle] = useState("");
const [description, setDescription] = useState("");
const [rtype, setRtype] = useState<string>("image");
const [language, setLanguage] = useState("zh");
const [language, setLanguage] = useState("zh-CN");
const [categoryId, setCategoryId] = useState(1);
const [coverImage, setCoverImage] = useState("");
const [fileUrl, setFileUrl] = useState("");
@@ -66,7 +66,7 @@ export function AdminResourceForm() {
setTitle(r.title || "");
setDescription(r.description || "");
setRtype(r.type || "image");
setLanguage(r.language || "zh");
setLanguage(r.language || "zh-CN");
setCategoryId(r.categoryId || 1);
setCoverImage(r.coverImage || "");
setFileUrl(r.fileUrl || "");
@@ -183,7 +183,7 @@ export function AdminResourceForm() {
value={language}
onChange={(e) => setLanguage(e.target.value)}
>
<option value="zh">{t("lang_zh")}</option>
<option value="zh-CN">{t("lang_zh_CN")}</option>
<option value="en">{t("lang_en")}</option>
<option value="ja">{t("lang_ja")}</option>
<option value="ko">{t("lang_ko")}</option>

View File

@@ -12,7 +12,7 @@ const t = (key: string) =>
type_image: "图片",
type_video: "视频",
type_music: "音乐",
lang_zh: "中文",
lang_zh_CN: "中文",
lang_en: "English",
lang_ja: "日本語",
})[key] ?? key;

View File

@@ -34,8 +34,10 @@ export function resourceLanguageLabel(
): string {
const lc = langCode.trim().toLowerCase();
const normalized =
lc === "zh-cn" || lc === "zh-tw" || lc === "zh-hans" ? "zh" : lc;
const key = `lang_${normalized}`;
lc === "zh" || lc === "zh-cn" || lc === "zh-tw" || lc === "zh-hans"
? "zh-CN"
: lc;
const key = `lang_${normalized.replace("-", "_")}`;
const label = t(key);
return label !== key ? label : langCode.trim();
}