完成 微前端拆分 提交遗漏文件

This commit is contained in:
编码猿
2025-09-18 01:52:30 +08:00
parent ca24ab9922
commit 3d0c69c98e
29 changed files with 4260 additions and 1 deletions

2
.gitignore vendored
View File

@@ -1,6 +1,6 @@
.DS_Store
node_modules
/dist
dist
# local env files

View File

@@ -0,0 +1,16 @@
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="UTF-8">
<link rel="icon" href="/favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vite App</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

3636
front-end/app-product/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,35 @@
{
"name": "app-product",
"version": "0.0.0",
"private": true,
"type": "module",
"engines": {
"node": "^20.19.0 || >=22.12.0"
},
"scripts": {
"dev": "vite --port 5001 --strictPort",
"serve": "vite preview --port 5001 --strictPort",
"build": "vite build",
"all": "npm run build && npm run serve",
"type-check": "vue-tsc --build"
},
"dependencies": {
"axios": "^1.12.2",
"pinia": "^3.0.3",
"vue": "^3.5.18",
"vue-router": "^4.0.6"
},
"devDependencies": {
"@originjs/vite-plugin-federation": "^1.4.1",
"@tsconfig/node22": "^22.0.2",
"@types/node": "^22.16.5",
"@vitejs/plugin-vue": "^6.0.1",
"@vitejs/plugin-vue-jsx": "^5.0.1",
"@vue/tsconfig": "^0.7.0",
"npm-run-all2": "^8.0.4",
"typescript": "~5.8.0",
"vite": "^7.0.6",
"vite-plugin-vue-devtools": "^8.0.0",
"vue-tsc": "^3.0.4"
}
}

View File

@@ -0,0 +1,17 @@
import { defineComponent } from 'vue'
import { RouterLink, RouterView } from "vue-router";
export default defineComponent({
setup() {
return () => (
<>
{/* 仅作占位,主应用加载联邦模块时不会显示此内容 */}
<div class="order-federation-placeholder">
<h2>/</h2>
<p></p>
<p></p>
</div>
</>
)
}
})

View File

@@ -0,0 +1,22 @@
import { ErrorText } from "../../domain/enums/ErrorEnums";
import { ProductRepository } from "../../ports/repositories/ProductRepository";
/**
* 商品应用服务 - 协调领域对象完成业务用例
* 应用服务是领域层的外观,封装了领域逻辑的调用
*/
export class ProductApplicationService {
// 依赖注入仓储接口,而不是具体实现
constructor(private productRepository: ProductRepository) { }
async getProductList() {
return await this.productRepository.findAll();
}
async getProductInfo(id: number) {
if (!id) {
throw new Error(ErrorText.IdNotNull)
}
return await this.productRepository.findById(id);
}
}

View File

@@ -0,0 +1,141 @@
import { ErrorText } from "../enums/ErrorEnums"
/**
* 商品实体 - 包含商品的核心属性和行为
* 在DDD中实体具有唯一标识和生命周期
*/
export class Product {
private readonly _id: number;
private _name: string;
private _description: string;
private _price: number;
private _imageUrl: string;
private _stock: number;
private _categoryId: number;
constructor(
id: number,
name: string,
description: string,
price: number,
imageUrl: string,
stock: number,
categoryId: number,
) {
this._id = id;
this._name = name;
this._description = description;
this._price = price;
this._imageUrl = imageUrl;
this._stock = stock;
this._categoryId = categoryId;
}
// 实体唯一标识
get id(): number {
return this._id;
}
public get name(): string {
return this._name;
}
public set name(value: string) {
this._name = value;
}
public get description(): string {
return this._description;
}
public set description(value: string) {
this._description = value;
}
public get price(): number {
return this._price;
}
public set price(value: number) {
if (value < 0) {
throw new Error(ErrorText.PriceNotNegativein);
}
this._price = value;
}
public get imageUrl(): string {
return this._imageUrl;
}
public set imageUrl(value: string) {
this._imageUrl = value;
}
public get stock(): number {
return this._stock;
}
public set stock(value: number) {
if (value < 0) {
throw new Error(ErrorText.StockNotNegativein)
}
this._stock = value;
}
public get categoryId(): number {
return this._categoryId;
}
public set categoryId(value: number) {
this._categoryId = value;
}
/**
* 检查商品是否有库存
*/
hasStock() {
return this._stock > 0
}
/**
* 减少商品库存
* @param quantity 减少的数量
*/
reduceStock(quantity: number): void {
if (quantity <= 0) {
throw new Error(ErrorText.StockIsGreaterThanZero)
}
if (this._stock < quantity) {
throw new Error(ErrorText.LowStock)
}
this._stock -= quantity;
}
/**
* Class类转化为普通json数据
*/
toDTO(): Record<string, string | number> {
return {
id: this._id,
name: this._name,
description: this._description,
price: this._price,
imageUrl: this._imageUrl,
stock: this._stock,
categoryId: this._categoryId
}
}
/**
* 从json数据创建Product类实例
* 转化为领域实体 Product
*/
static fromDTO(data: Record<string, any>): Product {
return new Product(
data.id,
data.name,
data.description,
data.price,
data.imageUrl,
data.stock,
data.categoryId,
);
}
}

View File

@@ -0,0 +1,7 @@
export enum ErrorText {
PriceNotNegativein = "商品 价格 不能为负数",
StockNotNegativein = "商品 库存 不能为负数",
StockIsGreaterThanZero = "减少的库存数量必须大于0",
LowStock = "库存不足",
IdNotNull = "商品Id不能为空"
}

View File

@@ -0,0 +1 @@
# 临时占位,上传空文件夹,保存文件结构用,后面删除

View File

@@ -0,0 +1,9 @@
export interface ProductType {
id: number,
name: string,
description: string,
price: number,
imageUrl: string,
stock: number,
categoryId: number,
}

View File

@@ -0,0 +1 @@
# 临时占位,上传空文件夹,保存文件结构用,后面删除

View File

@@ -0,0 +1,6 @@
export * from './application/services/ProductApplicationService';
export * from './domain/entities/Product';
export * from './domain/enums/ErrorEnums';
export * from './domain/type/ProductType';
export * from './infr/api/HttpProductRepository';
export * from './ports/repositories/ProductRepository';

View File

@@ -0,0 +1,27 @@
import { Product } from "@domains/product/domain/entities/Product.ts";
import { ProductType } from "@domains/product/domain/type/ProductType.ts";
import { ProductRepository } from "@domains/product/ports/repositories/ProductRepository";
// @ts-ignore
import { http } from "mainApp/shared";
import HttpConfig from "../config/HttpConfig";
/**
* HTTP商品仓储实现 - 实现商品数据的HTTP访问
* 这是基础设施层,负责具体的数据获取实现
*/
export class HttpProductRepository implements ProductRepository {
async findAll(): Promise<Array<ProductType>> {
const result: Array<ProductType> = await http.get({ url: HttpConfig.apiUrlList.productsListUrl });
const items = result.map((item) => Product.fromDTO(item))
return items;
}
async findById(id: number): Promise<Product> {
const result: ProductType = await http.get({
url: HttpConfig.apiUrlList.productsInfoUrl,
data: { id }
});
return Product.fromDTO(result);
}
}

View File

@@ -0,0 +1,11 @@
interface apiUrlListType {
productsListUrl: string
productsInfoUrl: string
}
export default class HttpConfig {
public static apiUrlList: apiUrlListType = {
productsListUrl: '/productsList',
productsInfoUrl: '/productsInfo',
}
}

View File

@@ -0,0 +1,20 @@
import { Product } from "../../domain/entities/Product"
import { ProductType } from "../../domain/type/ProductType"
/**
* 商品仓储接口 - 定义商品数据访问的契约
* 在DDD中端口定义了领域层与外部世界的交互方式
*/
export interface ProductRepository {
/**
* 获取商品列表
*/
findAll(): Promise<Array<ProductType>>;
/**
* 根据ID获取商品详情
* @param id 商品ID
*/
findById(id: number): Promise<Product>
}

View File

@@ -0,0 +1 @@
# 临时占位,上传空文件夹,保存文件结构用,后面删除

View File

@@ -0,0 +1 @@
# 临时占位,上传空文件夹,保存文件结构用,后面删除

View File

@@ -0,0 +1 @@
# 临时占位,上传空文件夹,保存文件结构用,后面删除

View File

@@ -0,0 +1,17 @@
import { RouteRecordRaw } from 'vue-router';
export const productRouter: Array<RouteRecordRaw> = [
{
path: '/Products',
name: 'ProductList',
component: () => import('../views/ProductList.jsx'),
meta: { title: '商品列表' }
},
{
path: '/ProductDetail',
name: 'ProductDetail',
component: () => import('../views/ProductDetail.jsx'),
meta: { title: '商品详情' }
},
]

View File

@@ -0,0 +1,61 @@
import { product } from 'mainApp/productService';
import { ProductType } from '@domains/product/domain/type/ProductType';
import { defineComponent, onMounted, reactive } from 'vue'
import { useRoute } from 'vue-router'
interface storeType {
item: ProductType
id: number
}
export default defineComponent({
name: "productModule-ProductDetail",
setup() {
const route = useRoute();
const productService = product.createProductApplicationService();
const state = reactive<storeType>({
item: {} as ProductType,
id: 0
})
onMounted(() => {
if (!route.query.id) {
throw new Error("缺少id参数");
}
state.id = Number(route.query.id)
console.log("id: ", state.id);
loadProductDetail()
})
const loadProductDetail = async () => {
state.item = await productService.getProductInfo(state.id);
console.log("state.item: ", state.item);
}
return () => (
<div class="ProductDetail">
<h1> - </h1>
<hr />
<p>id{state.id}</p>
{state.item.name}
<ul>
{
Object.entries(state.item).map(([key, value]) =>
<li>{key}: {value}</li>
)
}
</ul>
</div>
)
}
})
// export default defineComponent({
// setup() {
// return () => (
// <div>商品详情</div>
// )
// }
// })

View File

@@ -0,0 +1,61 @@
import { defineComponent, onMounted, reactive } from 'vue'
import { useRouter } from 'vue-router';
import { product } from 'mainApp/productService';
import { ProductType } from '@domains/product/domain/type/ProductType';
interface storeType {
list: Array<ProductType>
}
export default defineComponent({
name: "productModule-ProductList",
setup() {
const router = useRouter();
const productService = product.createProductApplicationService();
const state = reactive<storeType>({
list: []
})
onMounted(() => {
loadProductList()
})
const loadProductList = async () => {
state.list = await productService.getProductList();
console.log("state.list: ", state.list);
}
return () => (
<div class="ProductList">
<h1> - </h1>
<hr />
<h2>Deno </h2>
<ul>
{
state.list.map(item =>
<li key={item.id} onClick={() => {
router.push({
name: 'ProductDetail',
query: { id: item.id }
});
}}>
{item.id} | {item.name} | ¥{item.price}
</li>
)
}
</ul>
</div>
)
}
})
// export default defineComponent({
// setup() {
// return () => (
// <div>商品列表</div>
// )
// }
// })

View File

@@ -0,0 +1,11 @@
// import './assets/main.css'
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App'
createApp(App)
.use(createPinia())
.mount('#app')

View File

@@ -0,0 +1,33 @@
{
"extends": "@vue/tsconfig/tsconfig.dom.json",
"include": [
"src/**/*.ts",
"src/**/*.d.ts",
"src/**/*",
"src/**/*.tsx",
"src/**/*.vue"
],
"exclude": [
"src/**/__tests__/*"
],
"compilerOptions": {
"verbatimModuleSyntax": false,
"jsx": "preserve",
"jsxImportSource": "vue",
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"paths": {
"@/*": [
"./src/app/*"
],
"@shared/*": [
"./src/shared/*"
],
"@features/*": [
"./src/features/*"
],
"@domains/*": [
"./src/domains/*"
]
}
}
}

View File

@@ -0,0 +1,11 @@
{
"files": [],
"references": [
{
"path": "./tsconfig.node.json"
},
{
"path": "./tsconfig.app.json"
}
]
}

View File

@@ -0,0 +1,19 @@
{
"extends": "@tsconfig/node22/tsconfig.json",
"include": [
"vite.config.*",
"vitest.config.*",
"cypress.config.*",
"nightwatch.conf.*",
"playwright.config.*",
"eslint.config.*"
],
"compilerOptions": {
"noEmit": true,
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"module": "ESNext",
"moduleResolution": "Bundler",
"types": ["node"]
}
}

View File

@@ -0,0 +1,50 @@
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import vueJsx from '@vitejs/plugin-vue-jsx'
import vueDevTools from 'vite-plugin-vue-devtools'
import federation from '@originjs/vite-plugin-federation';
import {fileURLToPath, URL} from "node:url";
export default defineConfig({
plugins: [
federation({
name: 'productModule', // 联邦模块名称(主应用通过此名称引用)
filename: 'remoteEntry.js',
// 关键:声明远程模块(主模块)
remotes: {
mainApp: "http://localhost:5000/assets/remoteEntry.js"
},
exposes: { // 暴露给主应用的资源(按功能分组)
'./ProductDomain': './src/domains/product/index.ts', // 领域层(实体、服务、接口等)
'./ProductRouter': './src/features/product/router/index.ts' // 商品路由
},
shared: {
vue: { generate:false, requiredVersion: '^3.5.18' },
axios: { generate:false, requiredVersion: '^1.12.2' },
'vue-router': { generate:false, requiredVersion: '^4.0.6' },
pinia: { generate:false, requiredVersion: '^3.0.3' }
}
}),
vue(),
vueJsx(),
vueDevTools()
],
build: {
target: 'esnext',
cssCodeSplit: true,
rollupOptions: {
output: {
format: 'es',
minifyInternalExports: false
}
}
},
resolve: {
alias: {
'@': fileURLToPath(new URL('./src/', import.meta.url)),
'@shared': fileURLToPath(new URL('./src/shared/', import.meta.url)),
'@features': fileURLToPath(new URL('./src/features/', import.meta.url)),
'@domains': fileURLToPath(new URL('./src/domains/', import.meta.url))
},
},
});

View File

@@ -0,0 +1,3 @@
export * from "./config/HttpConfig"
export * from "./config/ThemeConfig"
export { default as http } from "./infra/AxiosInfra"

View File

@@ -0,0 +1,41 @@
// 声明领域层类型
declare module 'productModule/ProductDomain' {
import type { DefineComponent } from 'vue';
// 商品实体类型(与联邦模块一致)
export class Product {
id: number;
name: string;
description: string;
price: number;
imageUrl: string;
stock: number;
categoryId: number;
}
// 应用服务类型
export class ProductApplicationService {
constructor(repo: ProductRepository);
findAll(): Promise<Array<any>>; // TODO any 需要修改
findById(id: number): Promise<Product>
}
export class HttpProductRepository implements ProductRepository {
findAll(): Promise<Array<any>>; // TODO any 需要修改
findById(id: number): Promise<Product>
}
export class ProductRepository {
findAll(): Promise<Array<any>>; // TODO any 需要修改
findById(id: number): Promise<Product>
}
}
// 声明视图类型(同理补充其他模块)
// declare module 'productModule/ProductViews' {
// import type { DefineComponent } from 'vue';
// export const ProductList: DefineComponent; // 商品列表页
// export const ProductDetail: DefineComponent; // 商品详情页
// }
declare module 'productModule/ProductRouter' {
export const productRouter: RouteRecordRaw[];
}