merge: 合入AppData产品树展示主源

This commit is contained in:
Script Generator
2026-07-03 16:51:16 +08:00
3 changed files with 110 additions and 43 deletions

View File

@@ -3,6 +3,17 @@ import { PrismaService } from '../../prisma/prisma.service';
import { CreateProductDto } from './dto/create-product.dto'; import { CreateProductDto } from './dto/create-product.dto';
import { UpdateProductDto } from './dto/update-product.dto'; import { UpdateProductDto } from './dto/update-product.dto';
type ProductOverviewItem = {
id: string;
name: string;
description?: string;
createdAt?: string;
updatedAt?: string;
projects?: { id: string; name: string; description?: string; createdAt?: string }[];
versions?: { id: string; name: string; releaseDate?: string | null; createdAt?: string }[];
_count?: { requirements?: number; projects?: number; versions?: number };
};
@Injectable() @Injectable()
export class ProductService { export class ProductService {
constructor(private prisma: PrismaService) {} constructor(private prisma: PrismaService) {}
@@ -11,7 +22,16 @@ export class ProductService {
return this.prisma.product.create({ data: dto }); return this.prisma.product.create({ data: dto });
} }
findAll() { async findAll() {
const overview = await this.getAppDataOverview();
if (overview) {
return overview.map((product) => {
const normalized = this.normalizeOverviewProduct(product);
const { projects: _projects, versions: _versions, ...rest } = normalized;
return rest;
});
}
return this.prisma.product.findMany({ return this.prisma.product.findMany({
orderBy: { createdAt: 'desc' }, orderBy: { createdAt: 'desc' },
include: { include: {
@@ -20,7 +40,12 @@ export class ProductService {
}); });
} }
findAllWithChildren() { async findAllWithChildren() {
const overview = await this.getAppDataOverview();
if (overview) {
return overview.map((product) => this.normalizeOverviewProduct(product));
}
return this.prisma.product.findMany({ return this.prisma.product.findMany({
orderBy: { createdAt: 'desc' }, orderBy: { createdAt: 'desc' },
include: { include: {
@@ -38,6 +63,16 @@ export class ProductService {
} }
async findOne(id: string) { async findOne(id: string) {
const overview = await this.getAppDataOverview();
if (overview) {
const product = overview.find((item) => item.id === id);
if (!product) throw new NotFoundException('产品不存在');
return {
...this.normalizeOverviewProduct(product),
requirements: [],
};
}
const product = await this.prisma.product.findUnique({ const product = await this.prisma.product.findUnique({
where: { id }, where: { id },
include: { include: {
@@ -64,4 +99,28 @@ export class ProductService {
const exists = await this.prisma.product.findUnique({ where: { id } }); const exists = await this.prisma.product.findUnique({ where: { id } });
if (!exists) throw new NotFoundException('产品不存在'); if (!exists) throw new NotFoundException('产品不存在');
} }
private async getAppDataOverview(): Promise<ProductOverviewItem[] | null> {
const row = await this.prisma.appData.findUnique({
where: { key: 'products-overview' },
});
if (!Array.isArray(row?.value)) return null;
return row.value as unknown as ProductOverviewItem[];
}
private normalizeOverviewProduct(product: ProductOverviewItem) {
const projects = product.projects ?? [];
const versions = product.versions ?? [];
return {
...product,
description: product.description ?? '',
projects,
versions,
_count: {
requirements: product._count?.requirements ?? 0,
projects: projects.length,
versions: versions.length,
},
};
}
} }

View File

@@ -64,14 +64,14 @@ interface ProductState {
createProduct: (data: { name: string; description?: string }) => Promise<void>; createProduct: (data: { name: string; description?: string }) => Promise<void>;
updateProduct: (id: string, data: { name?: string; description?: string }) => Promise<void>; updateProduct: (id: string, data: { name?: string; description?: string }) => Promise<void>;
deleteProduct: (id: string) => Promise<void>; deleteProduct: (id: string) => Promise<void>;
reorderProducts: (ids: string[]) => void; reorderProducts: (ids: string[]) => Promise<void>;
migrateAndDeleteProduct: (sourceId: string, targetId: string) => void; migrateAndDeleteProduct: (sourceId: string, targetId: string) => Promise<void>;
createProject: (productId: string, data: { name: string; description?: string }) => void; createProject: (productId: string, data: { name: string; description?: string }) => Promise<void>;
updateProject: (productId: string, projectId: string, data: { name?: string; description?: string }) => void; updateProject: (productId: string, projectId: string, data: { name?: string; description?: string }) => Promise<void>;
createVersion: (productId: string, data: { name: string; status: string }) => void; createVersion: (productId: string, data: { name: string; status: string }) => Promise<void>;
updateVersion: (productId: string, versionId: string, data: Partial<VersionItem>) => void; updateVersion: (productId: string, versionId: string, data: Partial<VersionItem>) => Promise<void>;
deleteVersion: (productId: string, versionId: string) => void; deleteVersion: (productId: string, versionId: string) => Promise<void>;
deleteProject: (productId: string, projectId: string) => void; deleteProject: (productId: string, projectId: string) => Promise<void>;
} }
export const useProductStore = create<ProductState>((set, get) => ({ export const useProductStore = create<ProductState>((set, get) => ({
@@ -85,7 +85,10 @@ export const useProductStore = create<ProductState>((set, get) => ({
fetchProducts: async () => { fetchProducts: async () => {
set({ loading: true, error: null }); set({ loading: true, error: null });
try { try {
const products = await api.get<ProductWithCount[]>('/products'); const overview = await loadStoredOverview();
const products = overview
? overview.map(({ projects: _projects, versions: _versions, ...product }) => product)
: await api.get<ProductWithCount[]>('/products');
set({ products, loading: false }); set({ products, loading: false });
} catch { } catch {
set({ products: [], error: null, loading: false }); set({ products: [], error: null, loading: false });
@@ -117,6 +120,12 @@ export const useProductStore = create<ProductState>((set, get) => ({
fetchProduct: async (id) => { fetchProduct: async (id) => {
set({ loading: true, error: null }); set({ loading: true, error: null });
try { try {
const overview = await loadStoredOverview();
const found = overview?.find((p) => p.id === id);
if (found) {
set({ currentProduct: found as any, loading: false });
return;
}
const product = await api.get<Product>(`/products/${id}`); const product = await api.get<Product>(`/products/${id}`);
set({ currentProduct: product, loading: false }); set({ currentProduct: product, loading: false });
} catch { } catch {
@@ -130,7 +139,6 @@ export const useProductStore = create<ProductState>((set, get) => ({
}, },
createProduct: async (data) => { createProduct: async (data) => {
// 直接本地新增mock 模式核心,保留远端兼容但不依赖)
const newProduct: ProductOverview = { const newProduct: ProductOverview = {
id: `local-${Date.now()}`, id: `local-${Date.now()}`,
name: data.name || '', name: data.name || '',
@@ -143,43 +151,42 @@ export const useProductStore = create<ProductState>((set, get) => ({
}; };
const updated = [newProduct, ...get().overview]; const updated = [newProduct, ...get().overview];
set({ overview: updated, overviewLoaded: true }); set({ overview: updated, overviewLoaded: true });
saveStoredOverview(updated); await saveStoredOverview(updated);
// 异步尝试同步远端(失败也无所谓,本地已更新)
try { await api.post('/products', data); } catch {}
}, },
updateProduct: async (id, data) => { updateProduct: async (id, data) => {
try {
await api.patch(`/products/${id}`, data);
} catch {}
const updated = get().overview.map((p) => const updated = get().overview.map((p) =>
p.id === id ? { ...p, ...data } : p, p.id === id ? { ...p, ...data, updatedAt: new Date().toISOString() } : p,
); );
set({ overview: updated, overviewLoaded: true }); const currentProduct = get().currentProduct;
saveStoredOverview(updated); set({
overview: updated,
overviewLoaded: true,
currentProduct:
currentProduct?.id === id ? ({ ...currentProduct, ...data } as any) : currentProduct,
});
await saveStoredOverview(updated);
}, },
deleteProduct: async (id) => { deleteProduct: async (id) => {
try {
await api.delete(`/products/${id}`);
} catch {}
const updated = get().overview.filter((p) => p.id !== id); const updated = get().overview.filter((p) => p.id !== id);
set({ set({
products: get().products.filter((p) => p.id !== id), products: get().products.filter((p) => p.id !== id),
overview: updated, overview: updated,
overviewLoaded: true, overviewLoaded: true,
currentProduct: get().currentProduct?.id === id ? null : get().currentProduct,
}); });
saveStoredOverview(updated); await saveStoredOverview(updated);
}, },
reorderProducts: (ids) => { reorderProducts: async (ids) => {
const map = new Map(get().overview.map((p) => [p.id, p])); const map = new Map(get().overview.map((p) => [p.id, p]));
const reordered = ids.map((id) => map.get(id)).filter(Boolean) as ProductOverview[]; const reordered = ids.map((id) => map.get(id)).filter(Boolean) as ProductOverview[];
set({ overview: reordered, overviewLoaded: true }); set({ overview: reordered, overviewLoaded: true });
saveStoredOverview(reordered); await saveStoredOverview(reordered);
}, },
migrateAndDeleteProduct: (sourceId, targetId) => { migrateAndDeleteProduct: async (sourceId, targetId) => {
const source = get().overview.find((p) => p.id === sourceId); const source = get().overview.find((p) => p.id === sourceId);
const target = get().overview.find((p) => p.id === targetId); const target = get().overview.find((p) => p.id === targetId);
if (!source || !target) return; if (!source || !target) return;
@@ -199,10 +206,10 @@ export const useProductStore = create<ProductState>((set, get) => ({
.filter((p) => p.id !== sourceId) .filter((p) => p.id !== sourceId)
.map((p) => (p.id === targetId ? migratedTarget : p)); .map((p) => (p.id === targetId ? migratedTarget : p));
set({ overview: updated, overviewLoaded: true }); set({ overview: updated, overviewLoaded: true });
saveStoredOverview(updated); await saveStoredOverview(updated);
}, },
createProject: (productId, data) => { createProject: async (productId, data) => {
const newProject = { const newProject = {
id: `proj-${Date.now()}`, id: `proj-${Date.now()}`,
name: data.name || '', name: data.name || '',
@@ -218,10 +225,10 @@ export const useProductStore = create<ProductState>((set, get) => ({
}; };
}); });
set({ overview: updated, overviewLoaded: true }); set({ overview: updated, overviewLoaded: true });
saveStoredOverview(updated); await saveStoredOverview(updated);
}, },
updateProject: (productId, projectId, data) => { updateProject: async (productId, projectId, data) => {
const updated = get().overview.map((p) => { const updated = get().overview.map((p) => {
if (p.id !== productId) return p; if (p.id !== productId) return p;
const currentProject = p.projects.find((proj) => proj.id === projectId); const currentProject = p.projects.find((proj) => proj.id === projectId);
@@ -255,10 +262,10 @@ export const useProductStore = create<ProductState>((set, get) => ({
}; };
}); });
set({ overview: updated, overviewLoaded: true }); set({ overview: updated, overviewLoaded: true });
saveStoredOverview(updated); await saveStoredOverview(updated);
}, },
createVersion: (productId, data) => { createVersion: async (productId, data) => {
const newVersion: VersionItem = { const newVersion: VersionItem = {
id: `ver-${Date.now()}`, id: `ver-${Date.now()}`,
name: data.name, name: data.name,
@@ -276,10 +283,10 @@ export const useProductStore = create<ProductState>((set, get) => ({
}; };
}); });
set({ overview: updated, overviewLoaded: true }); set({ overview: updated, overviewLoaded: true });
saveStoredOverview(updated); await saveStoredOverview(updated);
}, },
updateVersion: (productId, versionId, data) => { updateVersion: async (productId, versionId, data) => {
const updated = get().overview.map((p) => { const updated = get().overview.map((p) => {
if (p.id !== productId) return p; if (p.id !== productId) return p;
return { return {
@@ -290,10 +297,10 @@ export const useProductStore = create<ProductState>((set, get) => ({
}; };
}); });
set({ overview: updated, overviewLoaded: true }); set({ overview: updated, overviewLoaded: true });
saveStoredOverview(updated); await saveStoredOverview(updated);
}, },
deleteVersion: (productId, versionId) => { deleteVersion: async (productId, versionId) => {
const updated = get().overview.map((p) => { const updated = get().overview.map((p) => {
if (p.id !== productId) return p; if (p.id !== productId) return p;
return { return {
@@ -303,10 +310,10 @@ export const useProductStore = create<ProductState>((set, get) => ({
}; };
}); });
set({ overview: updated, overviewLoaded: true }); set({ overview: updated, overviewLoaded: true });
saveStoredOverview(updated); await saveStoredOverview(updated);
}, },
deleteProject: (productId, projectId) => { deleteProject: async (productId, projectId) => {
const updated = get().overview.map((p) => { const updated = get().overview.map((p) => {
if (p.id !== productId) return p; if (p.id !== productId) return p;
return { return {
@@ -316,12 +323,12 @@ export const useProductStore = create<ProductState>((set, get) => ({
}; };
}); });
set({ overview: updated, overviewLoaded: true }); set({ overview: updated, overviewLoaded: true });
saveStoredOverview(updated); await saveStoredOverview(updated);
}, },
})); }));
function saveStoredOverview(data: ProductOverview[]) { async function saveStoredOverview(data: ProductOverview[]) {
saveServerData('products-overview', data).catch(() => {}); await saveServerData('products-overview', data).catch(() => {});
} }
async function loadStoredOverview(): Promise<ProductOverview[] | null> { async function loadStoredOverview(): Promise<ProductOverview[] | null> {

View File

@@ -2,6 +2,7 @@
"$schema": "https://turbo.build/schema.json", "$schema": "https://turbo.build/schema.json",
"tasks": { "tasks": {
"dev": { "dev": {
"dependsOn": ["^build"],
"cache": false, "cache": false,
"persistent": true "persistent": true
}, },