补齐平台订阅、外部登录、CRM 同步与用户端页面;Nginx 转发用户端接口,登录页使用 Google 与 Apple 标志。 Co-authored-by: Cursor <cursoragent@cursor.com>
260 行
7.4 KiB
Plaintext
260 行
7.4 KiB
Plaintext
/**
|
|
* OmniCard AI (CRMmp) - 产品相册与标杆案例移动端业务逻辑驱动层
|
|
* 严格遵循 uts 强类型契约与 AGENTS.md 规范
|
|
*/
|
|
|
|
import { CardCase, OwnerPageData, OwnerPageQuery } from '../../../common/types.uts';
|
|
import {
|
|
fetchCardCasesPage,
|
|
addCardCase,
|
|
updateCardCase,
|
|
deleteCardCase,
|
|
} from '../../../common/api.uts';
|
|
import { getCurrentLocale } from '../../../common/locale.uts';
|
|
|
|
export type CasesApi = {
|
|
fetchPage: (query: OwnerPageQuery) => Promise<OwnerPageData<CardCase>>;
|
|
addCase: (data: Partial<CardCase>) => Promise<CardCase>;
|
|
updateCase: (data: Partial<CardCase> & { id: string | number }) => Promise<CardCase>;
|
|
deleteCase: (id: string | number) => Promise<boolean>;
|
|
};
|
|
|
|
export const defaultCasesApi: CasesApi = {
|
|
fetchPage: (query: OwnerPageQuery) => fetchCardCasesPage(query),
|
|
addCase: (data: Partial<CardCase>) => addCardCase(data),
|
|
updateCase: (data: Partial<CardCase> & { id: string | number }) => updateCardCase(data),
|
|
deleteCase: (id: string | number) => deleteCardCase(id),
|
|
};
|
|
|
|
export type CasesState = {
|
|
cardId: string | number;
|
|
list: CardCase[];
|
|
loading: boolean;
|
|
empty: boolean;
|
|
errorMessage: string;
|
|
drawerVisible: boolean;
|
|
editingCase: Partial<CardCase> | null;
|
|
saving: boolean;
|
|
};
|
|
|
|
export function createCasesState(cardId: string | number): CasesState {
|
|
return {
|
|
cardId,
|
|
list: [],
|
|
loading: false,
|
|
empty: false,
|
|
errorMessage: '',
|
|
drawerVisible: false,
|
|
editingCase: null,
|
|
saving: false,
|
|
};
|
|
}
|
|
|
|
export function validateCaseForm(form: Partial<CardCase>): string | null {
|
|
const title = form.title != null ? form.title.trim() : '';
|
|
if (title === '') {
|
|
return resolveCasesText('errTitleRequired');
|
|
}
|
|
if (title.length > 150) {
|
|
return resolveCasesText('errTitleTooLong');
|
|
}
|
|
return null;
|
|
}
|
|
|
|
export async function loadCases(
|
|
state: CasesState,
|
|
api: CasesApi = defaultCasesApi
|
|
): 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 || resolveCasesText('errLoadFailed');
|
|
} finally {
|
|
state.loading = false;
|
|
}
|
|
}
|
|
|
|
export async function saveCase(
|
|
state: CasesState,
|
|
form: Partial<CardCase>,
|
|
api: CasesApi = defaultCasesApi
|
|
): Promise<boolean> {
|
|
const err = validateCaseForm(form);
|
|
if (err !== null) {
|
|
state.errorMessage = err;
|
|
return false;
|
|
}
|
|
|
|
state.saving = true;
|
|
state.errorMessage = '';
|
|
try {
|
|
const payload: Partial<CardCase> = {
|
|
...form,
|
|
cardId: state.cardId,
|
|
title: form.title!.trim(),
|
|
};
|
|
|
|
if (form.id != null) {
|
|
await api.updateCase({
|
|
...payload,
|
|
id: form.id,
|
|
});
|
|
} else {
|
|
await api.addCase(payload);
|
|
}
|
|
|
|
state.drawerVisible = false;
|
|
state.editingCase = null;
|
|
await loadCases(state, api);
|
|
return true;
|
|
} catch (err: any) {
|
|
state.errorMessage = err.message || resolveCasesText('errSaveFailed');
|
|
return false;
|
|
} finally {
|
|
state.saving = false;
|
|
}
|
|
}
|
|
|
|
export async function deleteCase(
|
|
state: CasesState,
|
|
id: string | number,
|
|
api: CasesApi = defaultCasesApi
|
|
): Promise<boolean> {
|
|
state.saving = true;
|
|
state.errorMessage = '';
|
|
try {
|
|
const ok = await api.deleteCase(id);
|
|
if (ok) {
|
|
await loadCases(state, api);
|
|
return true;
|
|
}
|
|
return false;
|
|
} catch (err: any) {
|
|
state.errorMessage = err.message || resolveCasesText('errDeleteFailed');
|
|
return false;
|
|
} finally {
|
|
state.saving = false;
|
|
}
|
|
}
|
|
|
|
export type CasesCopyKey =
|
|
| 'title'
|
|
| 'subtitle'
|
|
| 'addBtn'
|
|
| 'editBtn'
|
|
| 'deleteBtn'
|
|
| 'emptyTitle'
|
|
| 'emptyDesc'
|
|
| 'fieldTitle'
|
|
| 'fieldClient'
|
|
| 'fieldDuration'
|
|
| 'fieldMetrics'
|
|
| 'fieldDesc'
|
|
| 'fieldCover'
|
|
| 'saveBtn'
|
|
| 'cancelBtn'
|
|
| 'phTitle'
|
|
| 'phClient'
|
|
| 'phDuration'
|
|
| 'phMetrics'
|
|
| 'phDesc'
|
|
| 'clientPrefix'
|
|
| 'coverPlaceholder'
|
|
| 'saving'
|
|
| 'savedToast'
|
|
| 'deletedToast'
|
|
| 'deleteConfirmTitle'
|
|
| 'deleteConfirmContent'
|
|
| 'deleteConfirmOk'
|
|
| 'errTitleRequired'
|
|
| 'errTitleTooLong'
|
|
| 'errLoadFailed'
|
|
| 'errSaveFailed'
|
|
| 'errDeleteFailed';
|
|
|
|
export function resolveCasesText(key: CasesCopyKey): string {
|
|
const locale = getCurrentLocale();
|
|
const zh = locale === 'zh-CN';
|
|
|
|
switch (key) {
|
|
case 'title':
|
|
// 兼容历史标题匹配: 产品相册与标杆案例,规范展示为产品与案例
|
|
return zh ? '产品与案例' : 'Portfolio & Case Studies';
|
|
case 'subtitle':
|
|
return zh ? '维护产品案例,并在微官网中展示' : 'Showcase your products and case studies on your micro-site.';
|
|
case 'addBtn':
|
|
return zh ? '新增案例' : 'Add Case Study';
|
|
case 'editBtn':
|
|
return zh ? '编辑' : 'Edit';
|
|
case 'deleteBtn':
|
|
return zh ? '删除' : 'Delete';
|
|
case 'emptyTitle':
|
|
return zh ? '暂无产品案例' : 'No Cases Yet';
|
|
case 'emptyDesc':
|
|
return zh ? '可添加产品相册与案例展示' : 'Tap below to add your first product showcase or case study.';
|
|
case 'fieldTitle':
|
|
return zh ? '案例/产品名称' : 'Title';
|
|
case 'fieldClient':
|
|
return zh ? '合作客商/品牌 (选填)' : 'Client / Brand';
|
|
case 'fieldDuration':
|
|
return zh ? '项目周期 (选填)' : 'Timeline';
|
|
case 'fieldMetrics':
|
|
return zh ? '成效标签' : 'Key Metrics Tag';
|
|
case 'fieldDesc':
|
|
return zh ? '方案与成果描述' : 'Overview';
|
|
case 'fieldCover':
|
|
return zh ? '产品封面与相册图片' : 'Cover & Photos';
|
|
case 'saveBtn':
|
|
return zh ? '保存案例' : 'Save Case';
|
|
case 'cancelBtn':
|
|
return zh ? '取消' : 'Cancel';
|
|
case 'phTitle':
|
|
return zh ? '输入案例或产品名称' : 'Enter case or product title';
|
|
case 'phClient':
|
|
return zh ? '选填合作客商或品牌' : 'Optional client or brand';
|
|
case 'phDuration':
|
|
return zh ? '例如项目起止时间' : 'e.g. project timeline';
|
|
case 'phMetrics':
|
|
return zh ? '选填成效标签' : 'Optional metrics tag';
|
|
case 'phDesc':
|
|
return zh ? '简述方案背景、成效或客户反馈' : 'Brief overview, outcomes, or feedback';
|
|
case 'clientPrefix':
|
|
return zh ? '合作客户: ' : 'Client: ';
|
|
case 'coverPlaceholder':
|
|
return zh ? '产品展示' : 'Showcase';
|
|
case 'saving':
|
|
return zh ? '保存中...' : 'Saving...';
|
|
case 'savedToast':
|
|
return zh ? '案例已保存' : 'Case Saved';
|
|
case 'deletedToast':
|
|
return zh ? '已删除' : 'Deleted';
|
|
case 'deleteConfirmTitle':
|
|
return zh ? '确认删除' : 'Confirm Delete';
|
|
case 'deleteConfirmContent':
|
|
return zh ? '确定要删除该案例吗?此操作无法撤销。' : 'Are you sure you want to delete this case?';
|
|
case 'deleteConfirmOk':
|
|
return zh ? '删除' : 'Delete';
|
|
case 'errTitleRequired':
|
|
return zh ? '请输入案例或产品标题' : 'Please enter a title';
|
|
case 'errTitleTooLong':
|
|
return zh ? '标题长度不能超过 150 个字符' : 'Title must be 150 characters or fewer';
|
|
case 'errLoadFailed':
|
|
return zh ? '加载案例列表失败' : 'Failed to load cases';
|
|
case 'errSaveFailed':
|
|
return zh ? '保存案例失败' : 'Failed to save case';
|
|
case 'errDeleteFailed':
|
|
return zh ? '删除案例失败' : 'Failed to delete case';
|
|
default:
|
|
return '';
|
|
}
|
|
}
|