import React, { useEffect, useMemo, useState } from 'react'; import { Table, Drawer, Form, Input, Switch, Button, Popconfirm, Card, Tag, Message, Typography, Space, Grid, Radio, } from '@arco-design/web-react'; import { ImageUploader } from '@/components/Upload'; import type { PaginationProps, TableColumnProps } from '@arco-design/web-react'; import axios from 'axios'; import useLocale from '@/utils/useLocale'; import { lightColors } from '@/style/tokens'; import type { CardArticle, ArticleOgMeta } from '@/types/omni'; import locale from './locale'; type TablePaginationConfig = PaginationProps; const DEFAULT_PAGE = 1; const DEFAULT_SIZE = 10; const PAGE_API = '/api/admin/card/article/page'; const ADD_API = '/api/admin/card/article/add'; const UPDATE_API = '/api/admin/card/article/update'; const DELETE_API = '/api/admin/card/article/delete'; interface ArticleFilterForm { keyWord?: string; cardId?: string; isPublished?: boolean; } interface ArticleEditForm { id?: number | string; cardId?: number | string; title?: string; summary?: string; content?: string; coverImage?: string; isPublished?: boolean; } interface CoolPagination { page?: number; size?: number; total?: number; } interface CoolPageData { list?: CardArticle[]; pagination?: CoolPagination; } interface CoolResponse { code: number; message?: string; data?: T; } function resolveErrorMessage(error: unknown, fallback: string): string { if (axios.isAxiosError(error)) { const payload = error.response?.data as { message?: string } | undefined; if (payload && typeof payload.message === 'string' && payload.message) { return payload.message; } if (error.message) { return error.message; } } if (error instanceof Error && error.message) { return error.message; } return fallback; } function extractPlainText(content?: string): string { if (!content) { return ''; } return content .replace(/<[^>]+>/g, '') .replace(/!\[.*?\]\(.*?\)/g, '') .replace(/\[(.*?)\]\(.*?\)/g, '$1') .replace(/[#*`_~>-]/g, '') .replace(/\s+/g, ' ') .trim(); } function extractFirstImage(content?: string): string { if (!content) { return ''; } const htmlImgMatch = content.match(/]+src=["']([^"']+)["']/i); if (htmlImgMatch && htmlImgMatch[1]) { return htmlImgMatch[1].trim(); } const mdImgMatch = content.match(/!\[.*?\]\((https?:\/\/[^\s)]+|\/[^\s)]+)\)/i); if (mdImgMatch && mdImgMatch[1]) { return mdImgMatch[1].trim(); } return ''; } function deriveOgMeta(formData: ArticleEditForm): ArticleOgMeta { const title = (formData.title || '').trim(); let description = (formData.summary || '').trim(); if (!description && formData.content) { description = extractPlainText(formData.content).slice(0, 160); } let image = (formData.coverImage || '').trim(); if (!image && formData.content) { image = extractFirstImage(formData.content); } return { title, description, image }; } export default function CardArticlesPage() { const t = useLocale(locale); const [filterForm] = Form.useForm(); const [drawerForm] = Form.useForm(); const [loading, setLoading] = useState(false); const [saving, setSaving] = useState(false); const [articles, setArticles] = useState([]); const [pagination, setPagination] = useState({ current: DEFAULT_PAGE, pageSize: DEFAULT_SIZE, total: 0, showTotal: true, }); const [drawerVisible, setDrawerVisible] = useState(false); const [currentArticleId, setCurrentArticleId] = useState(null); // 表单联动 OpenGraph 实时预览状态 const [formValues, setFormValues] = useState({ cardId: 1, title: '', summary: '', content: '', coverImage: '', isPublished: false, }); const [ogRatio, setOgRatio] = useState<'landscape' | 'square'>('landscape'); const ogPreview = useMemo(() => deriveOgMeta(formValues), [formValues]); const loadData = async (page = DEFAULT_PAGE, size = DEFAULT_SIZE) => { setLoading(true); try { const filters = filterForm.getFieldsValue(); const payload: Record = { page, size, }; if (filters.keyWord) { payload.keyWord = filters.keyWord.trim(); } if (filters.cardId) { payload.cardId = filters.cardId.trim(); } if (typeof filters.isPublished === 'boolean') { payload.isPublished = filters.isPublished; } const res = await axios.post>(PAGE_API, payload); if (res.data && res.data.code === 1000) { const pageData = res.data.data; const list = Array.isArray(pageData?.list) ? pageData.list : []; setArticles(list); setPagination((prev) => ({ ...prev, current: page, pageSize: size, total: pageData?.pagination?.total ?? list.length, })); } else { Message.error(res.data?.message || t['cardArticles.message.requestFail']); } } catch (err) { Message.error(resolveErrorMessage(err, t['cardArticles.message.requestFail'])); } finally { setLoading(false); } }; useEffect(() => { loadData(); }, []); const handleOpenCreate = () => { setCurrentArticleId(null); const initData: ArticleEditForm = { cardId: 1, title: '', summary: '', content: '', coverImage: '', isPublished: true, }; drawerForm.setFieldsValue(initData); setFormValues(initData); setDrawerVisible(true); }; const handleOpenEdit = (record: CardArticle) => { setCurrentArticleId(record.id ?? null); const editData: ArticleEditForm = { id: record.id, cardId: record.cardId, title: record.title, summary: record.summary, content: record.content, coverImage: record.coverImage, isPublished: Boolean(record.isPublished), }; drawerForm.setFieldsValue(editData); setFormValues(editData); setDrawerVisible(true); }; const handleTogglePublish = async (record: CardArticle, checked: boolean) => { try { const res = await axios.post(UPDATE_API, { id: record.id, isPublished: checked, }); if (res.data && res.data.code === 1000) { Message.success(t['cardArticles.message.statusSuccess']); setArticles((prev) => prev.map((item) => (item.id === record.id ? { ...item, isPublished: checked } : item)) ); } else { Message.error(res.data?.message || t['cardArticles.message.requestFail']); } } catch (err) { Message.error(resolveErrorMessage(err, t['cardArticles.message.requestFail'])); } }; const handleDelete = async (record: CardArticle) => { try { const res = await axios.post(DELETE_API, { ids: [record.id], }); if (res.data && res.data.code === 1000) { Message.success(t['cardArticles.message.deleteSuccess']); loadData(pagination.current, pagination.pageSize); } else { Message.error(res.data?.message || t['cardArticles.message.requestFail']); } } catch (err) { Message.error(resolveErrorMessage(err, t['cardArticles.message.requestFail'])); } }; const handleSaveArticle = async () => { try { const values = await drawerForm.validate(); setSaving(true); const isEdit = Boolean(currentArticleId); const api = isEdit ? UPDATE_API : ADD_API; const payload = { ...values, id: currentArticleId ?? undefined, ogMeta: deriveOgMeta(values), }; const res = await axios.post(api, payload); if (res.data && res.data.code === 1000) { Message.success(t['cardArticles.message.saveSuccess']); setDrawerVisible(false); loadData(pagination.current, pagination.pageSize); } else { Message.error(res.data?.message || t['cardArticles.message.requestFail']); } } catch (err) { Message.error(resolveErrorMessage(err, t['cardArticles.message.requestFail'])); } finally { setSaving(false); } }; const columns: TableColumnProps[] = [ { title: t['cardArticles.field.id'], dataIndex: 'id', width: 80, }, { title: t['cardArticles.field.cardId'], dataIndex: 'cardId', width: 100, render: (val: unknown) => (val !== undefined && val !== null ? String(val) : '-'), }, { title: t['cardArticles.field.title'], dataIndex: 'title', render: (title: string, record: CardArticle) => (
{title} {record.summary && (
{record.summary}
)}
), }, { title: t['cardArticles.field.status'], dataIndex: 'isPublished', width: 130, render: (isPublished: boolean, record: CardArticle) => ( handleTogglePublish(record, checked)} /> {isPublished ? t['cardArticles.status.published'] : t['cardArticles.status.draft']} ), }, { title: t['cardArticles.field.updateTime'], dataIndex: 'updateTime', width: 180, render: (val: unknown) => (typeof val === 'string' ? val : '-'), }, { title: t['cardArticles.field.operations'], width: 140, render: (_: unknown, record: CardArticle) => ( handleDelete(record)} > ), }, ]; return (
{/* 头部导航与操作栏 */}
{t['cardArticles.title']} {t['cardArticles.description']}
{/* 遵循瑞士国际主义风格:整屏唯一主 CTA 按钮 */}
{/* 筛选表单 */}
loadData(1, pagination.pageSize)} > loadData(1, pagination.pageSize)} />
{/* 数据表格 */} rowKey="id" loading={loading} columns={columns} data={articles} pagination={{ ...pagination, onChange: (current, pageSize) => loadData(current, pageSize), }} noDataElement={
{t['cardArticles.empty.text']}
} />
{/* 文章编辑与 OpenGraph 社交预览抽屉 */} setDrawerVisible(false)} footer={
} > {/* 左侧表单编辑区 */}
setFormValues(allValues)} >
{/* 右侧 OpenGraph 社交图文卡实时预览区 */}
{t['cardArticles.ogPreview.title']} {t['cardArticles.ogPreview.desc']} {t['cardArticles.ogPreview.landscape']} {t['cardArticles.ogPreview.square']} {/* 模拟国际社交平台卡片 (LinkedIn / WhatsApp / X Card) */}
{/* 封面图预览 */}
{ogPreview.image ? ( OpenGraph Cover ) : ( OG Image )}
{/* 社交文本预览 */}
{t['cardArticles.ogPreview.domain']}
{ogPreview.title || t['cardArticles.ogPreview.emptyTitle']}
{ogPreview.description || t['cardArticles.ogPreview.emptyDesc']}
); }