补齐平台订阅、外部登录、CRM 同步与用户端页面;Nginx 转发用户端接口,登录页使用 Google 与 Apple 标志。 Co-authored-by: Cursor <cursoragent@cursor.com>
311 行
9.2 KiB
Plaintext
311 行
9.2 KiB
Plaintext
/**
|
|
* OmniCard AI (CRMmp) - 文章动态中心移动端业务逻辑驱动层
|
|
* 严格遵循 uts 强类型契约与 AGENTS.md 规范
|
|
*/
|
|
|
|
import { CardArticle, OwnerPageData, OwnerPageQuery } from '../../../common/types.uts';
|
|
import {
|
|
fetchCardArticlesPage,
|
|
addCardArticle,
|
|
updateCardArticle,
|
|
deleteCardArticle,
|
|
} from '../../../common/api.uts';
|
|
import { getCurrentLocale } from '../../../common/locale.uts';
|
|
|
|
export type ArticlesApi = {
|
|
fetchPage: (query: OwnerPageQuery) => Promise<OwnerPageData<CardArticle>>;
|
|
addArticle: (data: Partial<CardArticle>) => Promise<CardArticle>;
|
|
updateArticle: (data: Partial<CardArticle> & { id: string | number }) => Promise<CardArticle>;
|
|
deleteArticle: (id: string | number) => Promise<boolean>;
|
|
};
|
|
|
|
export const defaultArticlesApi: ArticlesApi = {
|
|
fetchPage: (query: OwnerPageQuery) => fetchCardArticlesPage(query),
|
|
addArticle: (data: Partial<CardArticle>) => addCardArticle(data),
|
|
updateArticle: (data: Partial<CardArticle> & { id: string | number }) => updateCardArticle(data),
|
|
deleteArticle: (id: string | number) => deleteCardArticle(id),
|
|
};
|
|
|
|
export type ArticlesState = {
|
|
cardId: string | number;
|
|
list: CardArticle[];
|
|
loading: boolean;
|
|
empty: boolean;
|
|
errorMessage: string;
|
|
drawerVisible: boolean;
|
|
editingArticle: Partial<CardArticle> | null;
|
|
saving: boolean;
|
|
};
|
|
|
|
export function createArticlesState(cardId: string | number): ArticlesState {
|
|
return {
|
|
cardId,
|
|
list: [],
|
|
loading: false,
|
|
empty: false,
|
|
errorMessage: '',
|
|
drawerVisible: false,
|
|
editingArticle: null,
|
|
saving: false,
|
|
};
|
|
}
|
|
|
|
export function extractArticleSummary(content?: string, maxLen: number = 120): string {
|
|
if (content == null || content.trim() === '') {
|
|
return '';
|
|
}
|
|
const plain = content
|
|
.replace(/<[^>]+>/g, '') // 剥离 HTML
|
|
.replace(/!\[.*?\]\(.*?\)/g, '') // 剥离 Markdown 图片
|
|
.replace(/\[(.*?)\]\(.*?\)/g, '$1') // 提取 Markdown 链接文本
|
|
.replace(/[#*`_~>-]/g, '') // 移除 Markdown 常用标记
|
|
.replace(/\s+/g, ' ') // 压缩多余空格
|
|
.trim();
|
|
|
|
return plain.length > maxLen ? plain.slice(0, maxLen) : plain;
|
|
}
|
|
|
|
export function validateArticleForm(form: Partial<CardArticle>): string | null {
|
|
const title = form.title != null ? form.title.trim() : '';
|
|
if (title === '') {
|
|
return resolveArticlesText('errTitleRequired');
|
|
}
|
|
const content = form.content != null ? form.content.trim() : '';
|
|
if (content === '') {
|
|
return resolveArticlesText('errContentRequired');
|
|
}
|
|
return null;
|
|
}
|
|
|
|
export async function loadArticles(
|
|
state: ArticlesState,
|
|
api: ArticlesApi = defaultArticlesApi
|
|
): Promise<void> {
|
|
state.loading = true;
|
|
state.errorMessage = '';
|
|
try {
|
|
const res = await api.fetchPage({
|
|
page: 1,
|
|
size: 50,
|
|
cardId: state.cardId,
|
|
});
|
|
const items = res.list != null ? res.list : [];
|
|
state.list = items;
|
|
state.empty = items.length === 0;
|
|
} catch (err: any) {
|
|
state.errorMessage = err.message || resolveArticlesText('errLoadFailed');
|
|
} finally {
|
|
state.loading = false;
|
|
}
|
|
}
|
|
|
|
export async function saveArticle(
|
|
state: ArticlesState,
|
|
form: Partial<CardArticle>,
|
|
api: ArticlesApi = defaultArticlesApi
|
|
): Promise<boolean> {
|
|
const err = validateArticleForm(form);
|
|
if (err !== null) {
|
|
state.errorMessage = err;
|
|
return false;
|
|
}
|
|
|
|
state.saving = true;
|
|
state.errorMessage = '';
|
|
try {
|
|
const summary = (form.summary != null && form.summary!.trim() !== '')
|
|
? form.summary!.trim()
|
|
: extractArticleSummary(form.content);
|
|
|
|
const payload: Partial<CardArticle> = {
|
|
...form,
|
|
cardId: state.cardId,
|
|
title: form.title!.trim(),
|
|
summary,
|
|
content: form.content!.trim(),
|
|
isPublished: form.isPublished !== undefined ? form.isPublished : true,
|
|
};
|
|
|
|
if (form.id != null) {
|
|
await api.updateArticle({
|
|
...payload,
|
|
id: form.id,
|
|
});
|
|
} else {
|
|
await api.addArticle(payload);
|
|
}
|
|
|
|
state.drawerVisible = false;
|
|
state.editingArticle = null;
|
|
await loadArticles(state, api);
|
|
return true;
|
|
} catch (err: any) {
|
|
state.errorMessage = err.message || resolveArticlesText('errSaveFailed');
|
|
return false;
|
|
} finally {
|
|
state.saving = false;
|
|
}
|
|
}
|
|
|
|
export async function toggleArticlePublish(
|
|
state: ArticlesState,
|
|
article: CardArticle,
|
|
api: ArticlesApi = defaultArticlesApi
|
|
): Promise<boolean> {
|
|
if (article.id == null) {
|
|
return false;
|
|
}
|
|
state.saving = true;
|
|
state.errorMessage = '';
|
|
try {
|
|
const targetStatus = !article.isPublished;
|
|
await api.updateArticle({
|
|
id: article.id,
|
|
cardId: article.cardId,
|
|
isPublished: targetStatus,
|
|
});
|
|
article.isPublished = targetStatus;
|
|
return true;
|
|
} catch (err: any) {
|
|
state.errorMessage = err.message || resolveArticlesText('errToggleFailed');
|
|
return false;
|
|
} finally {
|
|
state.saving = false;
|
|
}
|
|
}
|
|
|
|
export async function deleteArticle(
|
|
state: ArticlesState,
|
|
id: string | number,
|
|
api: ArticlesApi = defaultArticlesApi
|
|
): Promise<boolean> {
|
|
state.saving = true;
|
|
state.errorMessage = '';
|
|
try {
|
|
const ok = await api.deleteArticle(id);
|
|
if (ok) {
|
|
await loadArticles(state, api);
|
|
return true;
|
|
}
|
|
return false;
|
|
} catch (err: any) {
|
|
state.errorMessage = err.message || resolveArticlesText('errDeleteFailed');
|
|
return false;
|
|
} finally {
|
|
state.saving = false;
|
|
}
|
|
}
|
|
|
|
export type ArticlesCopyKey =
|
|
| 'title'
|
|
| 'subtitle'
|
|
| 'publishBtn'
|
|
| 'editBtn'
|
|
| 'deleteBtn'
|
|
| 'emptyTitle'
|
|
| 'emptyDesc'
|
|
| 'statusPublished'
|
|
| 'statusDraft'
|
|
| 'fieldTitle'
|
|
| 'fieldSummary'
|
|
| 'fieldCover'
|
|
| 'fieldContent'
|
|
| 'fieldPublish'
|
|
| 'publishDesc'
|
|
| 'phTitle'
|
|
| 'phSummary'
|
|
| 'phContent'
|
|
| 'saveBtn'
|
|
| 'cancelBtn'
|
|
| 'saving'
|
|
| 'savedToast'
|
|
| 'statusUpdatedToast'
|
|
| 'deletedToast'
|
|
| 'deleteConfirmTitle'
|
|
| 'deleteConfirmContent'
|
|
| 'deleteConfirmOk'
|
|
| 'errTitleRequired'
|
|
| 'errContentRequired'
|
|
| 'errLoadFailed'
|
|
| 'errSaveFailed'
|
|
| 'errDeleteFailed'
|
|
| 'errToggleFailed';
|
|
|
|
export function resolveArticlesText(key: ArticlesCopyKey): string {
|
|
const locale = getCurrentLocale();
|
|
const zh = locale === 'zh-CN';
|
|
|
|
switch (key) {
|
|
case 'title':
|
|
return zh ? '文章与商务动态' : 'Insights & Articles';
|
|
case 'subtitle':
|
|
return zh ? '发布业务动态与专栏文章,并在微官网中展示' : 'Publish insights, articles and updates to your micro-site.';
|
|
case 'publishBtn':
|
|
return zh ? '发布动态' : 'New Article';
|
|
case 'editBtn':
|
|
return zh ? '编辑' : 'Edit';
|
|
case 'deleteBtn':
|
|
return zh ? '删除' : 'Delete';
|
|
case 'emptyTitle':
|
|
return zh ? '暂无文章动态' : 'No Articles Yet';
|
|
case 'emptyDesc':
|
|
return zh ? '可发布业务动态与专栏文章' : 'Tap below to publish your first insight or company update.';
|
|
case 'statusPublished':
|
|
return zh ? '已发布' : 'Published';
|
|
case 'statusDraft':
|
|
return zh ? '草稿' : 'Draft';
|
|
case 'fieldTitle':
|
|
return zh ? '文章标题' : 'Title';
|
|
case 'fieldSummary':
|
|
return zh ? '摘要导读 (选填)' : 'Summary';
|
|
case 'fieldCover':
|
|
return zh ? '封面图片' : 'Cover Image';
|
|
case 'fieldContent':
|
|
return zh ? '文章正文 (支持富文本与 Markdown)' : 'Content';
|
|
case 'fieldPublish':
|
|
return zh ? '公开发布至微官网' : 'Publish to Micro-site';
|
|
case 'publishDesc':
|
|
return zh
|
|
? '开启后,全球访客可在您的名片微官网中直接浏览'
|
|
: 'When enabled, visitors can view this insight on your micro-site';
|
|
case 'phTitle':
|
|
return zh ? '输入文章或动态标题' : 'Enter article or insight title';
|
|
case 'phSummary':
|
|
return zh ? '选填,不填将从正文自动提取' : 'Optional; auto-extracted from content if empty';
|
|
case 'phContent':
|
|
return zh ? '输入正文,支持段落与 Markdown' : 'Write content; paragraphs and Markdown supported';
|
|
case 'saveBtn':
|
|
return zh ? '确认发布' : 'Publish';
|
|
case 'cancelBtn':
|
|
return zh ? '取消' : 'Cancel';
|
|
case 'saving':
|
|
return zh ? '保存中...' : 'Saving...';
|
|
case 'savedToast':
|
|
return zh ? '动态已保存' : 'Article Saved';
|
|
case 'statusUpdatedToast':
|
|
return zh ? '状态已切换' : 'Status Updated';
|
|
case 'deletedToast':
|
|
return zh ? '已删除' : 'Deleted';
|
|
case 'deleteConfirmTitle':
|
|
return zh ? '确认删除' : 'Confirm Delete';
|
|
case 'deleteConfirmContent':
|
|
return zh ? '确定要删除该文章动态吗?' : 'Are you sure you want to delete this article?';
|
|
case 'deleteConfirmOk':
|
|
return zh ? '删除' : 'Delete';
|
|
case 'errTitleRequired':
|
|
return zh ? '请输入文章或动态标题' : 'Please enter a title';
|
|
case 'errContentRequired':
|
|
return zh ? '请输入文章正文内容' : 'Please enter article content';
|
|
case 'errLoadFailed':
|
|
return zh ? '加载文章列表失败' : 'Failed to load articles';
|
|
case 'errSaveFailed':
|
|
return zh ? '保存文章失败' : 'Failed to save article';
|
|
case 'errDeleteFailed':
|
|
return zh ? '删除文章失败' : 'Failed to delete article';
|
|
case 'errToggleFailed':
|
|
return zh ? '切换发布状态失败' : 'Failed to update publish status';
|
|
default:
|
|
return '';
|
|
}
|
|
}
|