feat(projects): 增加意见反馈
This commit is contained in:
34
src/service/api/feedback-normalize.ts
Normal file
34
src/service/api/feedback-normalize.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
/** 后端统计原始返回(type/status 可能为 number 或 string,count 为整数) */
|
||||
export type FeedbackStatResponse = {
|
||||
total: number;
|
||||
typeCounts?: Array<{ type: string | number; count: number }> | null;
|
||||
statusCounts?: Array<{ status: string | number; count: number }> | null;
|
||||
};
|
||||
|
||||
/** 后端统计原始返回 → 业务层(type/status 一律 String 化为 Record 键,count 缺省兜底 0;data 为空全兜底 0) */
|
||||
export function normalizeFeedbackStat(data: FeedbackStatResponse | null | undefined): Api.Feedback.FeedbackStat {
|
||||
const typeCounts: Record<string, number> = {};
|
||||
(data?.typeCounts ?? []).forEach(item => {
|
||||
typeCounts[String(item.type)] = item.count ?? 0;
|
||||
});
|
||||
|
||||
const statusCounts: Record<string, number> = {};
|
||||
(data?.statusCounts ?? []).forEach(item => {
|
||||
statusCounts[String(item.status)] = item.count ?? 0;
|
||||
});
|
||||
|
||||
const total = data?.total ?? 0;
|
||||
|
||||
// 契约约定 total == sum(typeCounts) == sum(statusCounts);dev 下做一致性自检,便于尽早发现后端统计口径错位
|
||||
if (import.meta.env.DEV) {
|
||||
const typeSum = Object.values(typeCounts).reduce((sum, count) => sum + count, 0);
|
||||
const statusSum = Object.values(statusCounts).reduce((sum, count) => sum + count, 0);
|
||||
if (total !== typeSum || total !== statusSum) {
|
||||
console.warn(
|
||||
`[feedback-stat] total=${total} 与分类合计=${typeSum}、状态合计=${statusSum} 不一致,后端统计口径可能有误`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return { total, typeCounts, statusCounts };
|
||||
}
|
||||
185
src/service/api/feedback.ts
Normal file
185
src/service/api/feedback.ts
Normal file
@@ -0,0 +1,185 @@
|
||||
import { SYSTEM_SERVICE_PREFIX } from '@/constants/service';
|
||||
import { request } from '../request';
|
||||
import { type ServiceRequestResult, mapServiceResult, normalizeStringId, safeJsonRequestConfig } from './shared';
|
||||
import { type FeedbackStatResponse, normalizeFeedbackStat } from './feedback-normalize';
|
||||
|
||||
const FEEDBACK_PREFIX = `${SYSTEM_SERVICE_PREFIX}/feedback`;
|
||||
|
||||
type FeedbackResponse = Omit<Api.Feedback.FeedbackItem, 'id' | 'creator' | 'attachments' | 'contentPreview'> & {
|
||||
id: string | number;
|
||||
creator: string | number;
|
||||
/** 后端原始:附件的 JSON 数组字符串(前端约定存完整附件对象,兼容历史纯 URL 数组) */
|
||||
attachmentUrls?: string | null;
|
||||
};
|
||||
|
||||
type FeedbackPageResponse = {
|
||||
total: number;
|
||||
list: FeedbackResponse[];
|
||||
};
|
||||
|
||||
/** 从代理 URL 末段拆出文件名(兜底历史纯 URL 附件的展示名) */
|
||||
function deriveAttachmentName(url: string): string {
|
||||
try {
|
||||
const path = decodeURI(url.split('?')[0]);
|
||||
const idx = path.lastIndexOf('/');
|
||||
return idx >= 0 ? path.slice(idx + 1) : path;
|
||||
} catch {
|
||||
return url;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 单个附件归一化为业务层 AttachmentItem(ID 铁律:fileId/configId 一律 String)。
|
||||
* - 对象形态(新版存储):按字段对齐;
|
||||
* - 字符串形态(历史仅存 URL):补出 url + 派生 name,fileId 缺省空串(不可下载/清理,仅展示)。
|
||||
*/
|
||||
function normalizeAttachment(raw: unknown): Api.Project.AttachmentItem | null {
|
||||
if (typeof raw === 'string') {
|
||||
return { fileId: '', url: raw, name: deriveAttachmentName(raw) };
|
||||
}
|
||||
if (raw && typeof raw === 'object') {
|
||||
const item = raw as Record<string, unknown>;
|
||||
const url = typeof item.url === 'string' ? item.url : '';
|
||||
return {
|
||||
fileId: item.fileId === null || item.fileId === undefined ? '' : String(item.fileId),
|
||||
url,
|
||||
name: typeof item.name === 'string' && item.name ? item.name : deriveAttachmentName(url),
|
||||
size: typeof item.size === 'number' ? item.size : undefined,
|
||||
contentType: typeof item.contentType === 'string' ? item.contentType : undefined,
|
||||
configId: item.configId === null || item.configId === undefined ? undefined : String(item.configId),
|
||||
path: typeof item.path === 'string' ? item.path : undefined
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** 把后端 attachmentUrls(JSON 字符串) 安全解析成附件对象数组;空 / 异常兜底为 [] */
|
||||
function parseAttachments(raw?: string | null): Api.Project.AttachmentItem[] {
|
||||
if (!raw) {
|
||||
return [];
|
||||
}
|
||||
try {
|
||||
const parsed = JSON.parse(raw);
|
||||
if (!Array.isArray(parsed)) {
|
||||
return [];
|
||||
}
|
||||
return parsed.map(normalizeAttachment).filter((item): item is Api.Project.AttachmentItem => item !== null);
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/** 列表「内容」列去富文本标签,取纯文本预览(content 为 HTML,预算一次免每次渲染重扫) */
|
||||
function stripHtml(html: string | null | undefined): string {
|
||||
if (!html) {
|
||||
return '';
|
||||
}
|
||||
return html
|
||||
.replace(/<[^>]+>/g, '')
|
||||
.replace(/ /g, ' ')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim();
|
||||
}
|
||||
|
||||
/** 后端记录 → 业务层口径(ID 铁律 String 兜底 + 附件 parse + 内容纯文本预览预算) */
|
||||
function normalizeFeedback(data: FeedbackResponse): Api.Feedback.FeedbackItem {
|
||||
const { attachmentUrls, ...rest } = data;
|
||||
return {
|
||||
...rest,
|
||||
id: normalizeStringId(data.id),
|
||||
creator: normalizeStringId(data.creator),
|
||||
attachments: parseAttachments(attachmentUrls),
|
||||
contentPreview: stripHtml(data.content)
|
||||
};
|
||||
}
|
||||
|
||||
/** 附件对象数组 → attachmentUrls JSON 字符串(无附件传空串) */
|
||||
function serializeAttachments(attachments: Api.Project.AttachmentItem[]): string {
|
||||
return attachments.length ? JSON.stringify(attachments) : '';
|
||||
}
|
||||
|
||||
/** 分页(全量,不按提交人过滤;默认 createTime 倒序由后端保证) */
|
||||
export async function fetchGetFeedbackPage(params: Api.Feedback.FeedbackSearchParams) {
|
||||
const result = await request<FeedbackPageResponse>({
|
||||
...safeJsonRequestConfig,
|
||||
url: `${FEEDBACK_PREFIX}/page`,
|
||||
method: 'get',
|
||||
params
|
||||
});
|
||||
|
||||
return mapServiceResult(result as ServiceRequestResult<FeedbackPageResponse>, data => ({
|
||||
total: data.total,
|
||||
list: data.list.map(normalizeFeedback)
|
||||
}));
|
||||
}
|
||||
|
||||
/** 统计聚合(左侧分面面板;全量口径,无业务筛选入参) */
|
||||
export async function fetchGetFeedbackStat() {
|
||||
const result = await request<FeedbackStatResponse>({
|
||||
...safeJsonRequestConfig,
|
||||
url: `${FEEDBACK_PREFIX}/stat`,
|
||||
method: 'get'
|
||||
});
|
||||
|
||||
return mapServiceResult(result as ServiceRequestResult<FeedbackStatResponse>, normalizeFeedbackStat);
|
||||
}
|
||||
|
||||
/** 详情 */
|
||||
export async function fetchGetFeedback(id: string) {
|
||||
const result = await request<FeedbackResponse>({
|
||||
...safeJsonRequestConfig,
|
||||
url: `${FEEDBACK_PREFIX}/get`,
|
||||
method: 'get',
|
||||
params: { id }
|
||||
});
|
||||
|
||||
return mapServiceResult(result as ServiceRequestResult<FeedbackResponse>, normalizeFeedback);
|
||||
}
|
||||
|
||||
/** 提交反馈(attachments 对象数组在此 stringify 成 attachmentUrls;无附件传空串;返回新建 id,ID 铁律 String 化) */
|
||||
export async function fetchCreateFeedback(params: Api.Feedback.FeedbackSubmitParams) {
|
||||
const { attachments, ...rest } = params;
|
||||
const result = await request<string | number>({
|
||||
...safeJsonRequestConfig,
|
||||
url: `${FEEDBACK_PREFIX}/create`,
|
||||
method: 'post',
|
||||
data: {
|
||||
...rest,
|
||||
attachmentUrls: serializeAttachments(attachments)
|
||||
}
|
||||
});
|
||||
|
||||
return mapServiceResult(result as ServiceRequestResult<string | number>, normalizeStringId);
|
||||
}
|
||||
|
||||
/** 更新反馈(编辑态;attachments 对象数组在此 stringify 成 attachmentUrls;无附件传空串) */
|
||||
export function fetchUpdateFeedback(params: Api.Feedback.FeedbackUpdateParams) {
|
||||
const { attachments, ...rest } = params;
|
||||
return request<boolean>({
|
||||
...safeJsonRequestConfig,
|
||||
url: `${FEEDBACK_PREFIX}/update`,
|
||||
method: 'put',
|
||||
data: {
|
||||
...rest,
|
||||
attachmentUrls: serializeAttachments(attachments)
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** 修改处理状态(仅超管;状态可任意改,不校验流转) */
|
||||
export function fetchUpdateFeedbackStatus(id: string, status: number) {
|
||||
return request<boolean>({
|
||||
url: `${FEEDBACK_PREFIX}/${id}/status`,
|
||||
method: 'put',
|
||||
data: { status }
|
||||
});
|
||||
}
|
||||
|
||||
/** 删除反馈(软删除;后端按归属二次校验) */
|
||||
export function fetchDeleteFeedback(id: string) {
|
||||
return request<boolean>({
|
||||
url: `${FEEDBACK_PREFIX}/delete`,
|
||||
method: 'delete',
|
||||
params: { id }
|
||||
});
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
export * from './auth';
|
||||
export * from './dict';
|
||||
export * from './feedback';
|
||||
export * from './file';
|
||||
export * from './infra';
|
||||
export * from './notice';
|
||||
|
||||
Reference in New Issue
Block a user