从主项目中删除 app-product,抽离为独立仓库存储!!

This commit is contained in:
编码猿
2025-10-07 04:37:32 +08:00
parent cb8f7a4682
commit 2653800c3f
41 changed files with 6 additions and 1113 deletions

View File

@@ -13,7 +13,6 @@
![架构图](./doc/jgt.png)
## CI/CD 自动化集群部署
![cicd](./doc/newci.png)
@@ -247,11 +246,13 @@ export default class Header extends TSX<Props>()(Vue) implements Props {
- 2: 在1台Linux服务器上部署公司自己的 Docker Registry 镜像存储库
- 3: 准本 N台运行 前端各模块 的Linux服务器并提前组成N个Docker Swarm 集群,以供前端各模块部署使用
- 4: 准本 >=3台运行 Nacos服务中心 的Linux服务器并提前组成Docker Swarm 集群,提前运行好,确保服务可用
- [Nacos 部署文档](./docker/nacos-cluster/README.md)
- [Nacos 部署文档](./docker/nacos-cluster/README.md)
- 5: 准本 一台 运行 traefik 网关的Linux服务器提前运行好确保服务可用
- [Traefik 部署文档](./docker/traefik/README.md)
- [Traefik 部署文档](./docker/traefik/README.md)
**前端工作:**
详细请看 [app-product的README.md](./front-end/app-product/README.md) 内的帮助文档!!!
详细请看 各前端模块 存储库内的 帮助文档!!!
- [商品模块 app-product](https://gitee.com/bmycode/app-product)

View File

@@ -1,14 +0,0 @@
node_modules
dist
.git
.DS_Store
*.log
docker/ # 排除 docker 配置文件夹本身
.editorconfig
.eslintrc.js
.prettierrc
.vscode
.idea
*.swp
*.swo

View File

@@ -1,26 +0,0 @@
# 请求协议 http or https建议线上配置证书后使用https
VITE_PREFIX=http://
# 内网 中的 Nacos 配置中心 ip:端口 | 域名
#VITE_NACOS_ADDRESS=cluster.nacos.com
VITE_NACOS_ADDRESS=192.168.139.84:8848
# 注册到 Nacos 的服务名称(微前端模块名称
VITE_SERVER_NAME=app-product
# 注册到 Nacos 的端口,也是前端运行的端口
VITE_SERVER_PORT=1301
#
# 说明:项目要调用哪些 微前端 模块
# 格式VITE_MODEL_XXXX_NAME=微前端模块名称(模块 的 开发者提供)
# 作用通过名称从Nacos配置中心发现模块地址和端口给 federation 实时加载
#
# shared 共享资源模块
VITE_MODEL_SHARED_NAME=app-shared

View File

@@ -1,23 +0,0 @@
# 请求协议 http or https建议线上配置证书后使用https
VITE_PREFIX=http://
# 内网 中的 Nacos 配置中心 ip:端口 | 域名
#VITE_NACOS_ADDRESS=cluster.nacos.com
VITE_NACOS_ADDRESS=192.168.139.84:8848
# 注册到 Nacos 的服务名称(微前端模块名称
VITE_SERVER_NAME=app-product
# 注册到 Nacos 的端口,也是前端运行的端口
VITE_SERVER_PORT=1301
#
# 说明:项目要调用哪些 微前端 模块
# 格式VITE_MODEL_XXXX_NAME=微前端模块名称(模块 的 开发者提供)
# 作用通过名称从Nacos配置中心发现模块地址和端口给 federation 实时加载
#
# shared 共享资源模块
VITE_MODEL_SHARED_NAME=app-shared

View File

@@ -1,75 +0,0 @@
name: build and push
# 当代码推送到 main 分支时触发
on:
push:
branches: [ main ]
jobs:
build-and-push:
runs-on: ubuntu-latest
steps:
- name: 拉取最新代码
uses: actions/checkout@v4
- name: 设置 Docker Buildx
uses: docker/setup-buildx-action@v3
- name: 登录到自己的 Docker 仓库
uses: docker/login-action@v3
with:
registry: registry.bmycode.help
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: 构建并推送镜像
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: |
registry.bmycode.help/app-product:latest
registry.bmycode.help/app-product:${{ github.sha }}
- name: 替换镜像标签为 github.sha
run: |
sed -i "s|{{IMAGE_TAG}}|${{ github.sha }}|g" app-product.yml
- name: 上传 app-product.yml 到前端集群管理机
uses: appleboy/scp-action@v1
with:
host: ${{ vars.SSH_MANAGE }}
username: ${{ secrets.USERNAME }}
password: ${{ secrets.PASSWORD }}
port: ${{ secrets.PORT }}
source: "app-product.yml"
target: "/home/bmy/"
- name: 登录前端集群,拉镜像
uses: appleboy/ssh-action@v1
with:
# 统一登录三台前端集群的linux统一拉取刚推到仓库的最新镜像
# 解决 Swarm 无法控制 工作机拉取镜像的问题!
host: ${{ vars.SSH_HOST_LIST }}
sync: true
username: ${{ secrets.USERNAME }}
password: ${{ secrets.PASSWORD }}
port: ${{ secrets.PORT }}
script: |
# 拉取刚打包的最新镜像
docker pull registry.bmycode.help/app-product:${{ github.sha }}
- name: 管理节点部署到 Swarm
uses: appleboy/ssh-action@v1
with:
host: ${{ vars.SSH_MANAGE }}
sync: true
username: ${{ secrets.USERNAME }}
password: ${{ secrets.PASSWORD }}
port: ${{ secrets.PORT }}
script: |
# 切换到管理员,要不然没权限在服务器执行 docker 命令,部署到 Swarm 集群
sudo docker stack deploy -c /home/bmy/app-product.yml app-product

View File

@@ -1,22 +0,0 @@
FROM node:24-alpine AS builder
WORKDIR /app
COPY package.json ./
# 直接使用 npm install 并解决依赖问题
RUN npm install --no-audit --no-fund --legacy-peer-deps
# 拷贝源码并构建
COPY . .
# 【前端修改】如果要以子路径 /app-product/ 部署,请用 base 指定前缀,同 .env中VITE_SERVER_NAME
RUN npx vite build --base=/app-product/
FROM nginx:alpine
# 构建产物放到 /usr/share/nginx/html/app-product
# nginx.conf 也需要修改
COPY --from=builder /app/dist /usr/share/nginx/html/app-product
# 覆盖 Nginx 配置,支持 SPA 子路径
COPY docker/nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

View File

@@ -1,156 +0,0 @@
## app-product 商品模块(微前端模块)
从项目中拆分出来,用于演示的微前端模块,主要实现 商品相关功能。本案例演示中提供:
- 调用 微前端 app-shared 模块
- 调接口,获取商品首页的数据和展示
- 调接口,点击商品列表进入商品详情页
## 运行
下面文档分为**开发**和**上线**两部分进行说明!**默认前端已经在电脑上安装过 Docker 环境!!**
> 想要【开发阶段运行】,必须 先参考下面 【上线】-【任务1】配置好两个前端自己的环境变量文件很简单
### 开发
```bash
# 安装依赖
npm install
# 开发阶段 运行 项目
# PS注意此命令运行的项目【无法】被其他 微前端模块 使用,仅用于本地独立开发,独立测试
npm run dev
# 启动 实时打包 & 实时预览
# PS注意此命令运行的项目【可以】被其他 微前端模块 调用,可用于本地开发和微前端联合开发
npm run dep
```
### 上线
代码 到 仓库的 **main 分支** 后会触发 全程自动化实现自动打包前端代码构建镜像自动部署到公司内网的Linux服务器集群中
提交到其他分支不触发自动化操作!具体什么规则,以 技术负责人 说的为准!请遵守规则!
**根目录核心文件说明**
- `app-product.yml` - Dcoker Swarm 集群部署配置【不准动】
- `docker-compose.yml` - 开发阶段容器编排【可选,不要就删除】
- `Dockerfile` - 打包构建前端代码为镜像【根据需求改动】
- `nginx.conf` - 镜像内前端被访问的Nginx配置【根据需求改动】
- `act-runner.yml` - gitea action 的 runner【根据需求改动】
前端工作:
前期大致工作内容如下,改好后,以后就不用再动了,很简单:
- 任务1: 编辑 前端 .env.development 和 .env.production 环境变量(只需修改一次)
- 任务2: 确认或编辑 Dockerfilenginx.conf
- 任务3: 编辑 act-runner.yml
---
详细展开说明:
**任务1**
主要修改两个环境变量文件 `.env.development`,`.env.production` 中的:
```bash
# 找技术负责人要
# 情况1: 【内网模块】填写 Nacos 内网 ip 或 域名 即可
# 情况2: 【外网模块】填写 traefik 公网 ip 或 域名 即可
# .env.development, .env.production 中 可以不一样!
VITE_NACOS_ADDRESS=????
# 这个参数很重要,前端可以自定义填写,填写后不允许再改,填完给 技术负责人 即可
# 微前端应用的名称,遵守 app-xxxxx
# .env.development, .env.production 中 必须一样!
VITE_SERVER_NAME=app-???
#【ps】其他参数看环境变量内说明
```
---
**任务2**
主要修改两个 `Dockerfile``nginx.conf`,改起来也很简单:
Dockerfile 修改教程:
```bash
# 将 VITE_SERVER_NAME 替换为 上一步设置的 VITE_SERVER_NAME 的 数值 即可
RUN npx vite build --base=/VITE_SERVER_NAME/
# 将 VITE_SERVER_NAME 替换为 上一步设置的 VITE_SERVER_NAME 的 数值 即可
COPY --from=builder /app/dist /usr/share/nginx/html/VITE_SERVER_NAME
```
nginx.conf 修改教程:
```bash
# 看仔细了,有三处 VITE_SERVER_NAME 需要替换成 值!!
location /VITE_SERVER_NAME/ {
alias /usr/share/nginx/html/VITE_SERVER_NAME/;
try_files $uri $uri/ /VITE_SERVER_NAME/index.html;
add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range';
add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range';
}
```
---
**任务3**
act-runner.yml 是 Gitea Action 的核心Gitea会将Job委托给Runner来运行**所以我们前端本地必须要先运行 act-runner.yml 文件**,这样在我们提交代码的时候 Gitea Action 的自动构建才会运行!!
但是,运行 act-runner.yml 之前,需要先对 act-runner.yml 文件进行简单的配置:
```bash
# 核心 需要改动的配置
# 可以直接让 仓库 的管理员 给你把下面参数之间配置好,当然你也可以自己配
# GITEA_INSTANCE_URL=这个前端项目模块的 git仓库地址
# 格式:协议://域名 即可例如https://git.bmycode.help 或 https://gitee.com 等
# 项目仓库首页-设置-Actions-Runner 右上角可复制令牌
# GITEA_RUNNER_REGISTRATION_TOKEN=注册令牌,来源三选一:实例,组织,全存储库
environment:
- GITEA_INSTANCE_URL=https://git.bmycode.help
- GITEA_RUNNER_REGISTRATION_TOKEN=WFiO5JDwz7CHnk7fMGJYMcSITU81CLXuI7Ofg10e
```
改好后,在**项目文件夹根目录**打开终端,通过下面命令运行 act-runner.yml 文件:
```bash
docker-compose -f act-runner.yml up -d
# 运行后,如果 在 docker desktop 的容器中找到 名字叫gitea-act-runner 的容器,查看日志:
# 有下面这行,则表示 runner 成功启动docker desktop 可以关闭了或者放后台,不用管了!
# level=debug msg="Successfully pinged the Gitea instance server"
# 如果出问题了,找 技术负责人!
```
---
**到这一步,上线 配置已经搞完,很简单吧!剩下的,你提交代码到仓库就行,项目会自动构建,自动打包发布到公司内网集群中部署和运行,不用你管了!!**

View File

@@ -1,17 +0,0 @@
version: '3.8'
services:
gitea-actions-runner:
image: gitea/act_runner:latest
container_name: gitea-act-runner
restart: unless-stopped
volumes:
- runner-data:/data
- /var/run/docker.sock:/var/run/docker.sock
environment:
- GITEA_INSTANCE_URL=https://git.bmycode.help
- GITEA_RUNNER_REGISTRATION_TOKEN=WFiO5JDwz7CHnk7fMGJYMcSITU81CLXuI7Ofg10e
- GITEA_RUNNER_LABELS=ubuntu-latest:docker://docker.gitea.com/runner-images:ubuntu-latest
volumes:
runner-data:

View File

@@ -1,39 +0,0 @@
version: "3.8" # Docker Compose 文件版本3.8 支持 Swarm 模式的所有功能
services:
app-product:
image: registry.bmycode.help/app-product:{{IMAGE_TAG}}
ports:
- target: 80 # 容器内部端口
published: 1301 # 宿主机对外暴露端口
protocol: tcp # 协议类型
mode: ingress # 模式:使用路由网格
deploy:
replicas: 3 # 副本数量,在集群中运行 3 个容器实例
placement:
max_replicas_per_node: 1 # 每个 Swarm 节点最多运行 1 个副本,确保高可用性
restart_policy:
condition: any # 重启条件:任何情况下容器退出都重启
delay: 5s # 重启延迟 5 秒
max_attempts: 3 # 最大重启尝试次数
update_config: # 滚动更新配置
parallelism: 1 # 并行更新数量:一次只更新 1 个容器
delay: 10s # 更新间隔:每个容器更新后等待 10 秒再更新下一个
failure_action: rollback # 更新失败时自动回滚到上一版本
order: start-first # 更新顺序:先启动新容器,确认健康后再停止旧容器
rollback_config: # 回滚配置
parallelism: 1 # 回滚时并行数量
delay: 5s # 回滚延迟
resources: # 资源限制
limits: # 资源上限
cpus: "0.50" # 最多使用 0.5 个 CPU 核心
memory: "256M" # 最多使用 256MB 内存
reservations: # 资源预留
cpus: "0.25" # 预留 0.25 个 CPU 核心
memory: "128M" # 预留 128MB 内存
networks:
- frontend # 连接到 frontend 网络
networks:
frontend: # 定义 frontend 网络
driver: overlay # 使用 overlay 网络驱动,支持跨主机通信

View File

@@ -1,13 +0,0 @@
version: "3.8"
services:
app-product:
build:
context: ..
dockerfile: docker/Dockerfile
image: registry.bmycode.help/app-product:latest
container_name: app-product
ports:
- "1301:80"
restart: unless-stopped

View File

@@ -1,16 +0,0 @@
<!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>

View File

@@ -1,14 +0,0 @@
server {
listen 80;
server_name *.ddd.com;
location /app-product/ {
alias /usr/share/nginx/html/app-product/;
try_files $uri $uri/ /app-product/index.html;
add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range';
add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range';
}
}

View File

@@ -1,39 +0,0 @@
{
"name": "app-product",
"version": "0.0.0",
"private": true,
"type": "module",
"engines": {
"node": "^20.19.0 || >=22.12.0"
},
"scripts": {
"dev": "vite",
"dep": "concurrently \"vite build --watch\" \"sleep 5 && npm run preview\"",
"preview": "vite preview --port 1301 --strictPort",
"build": "vite build",
"type-check": "vue-tsc --build"
},
"dependencies": {
"less": "^4.4.1",
"pinia": "^3.0.3",
"vite-plugin-nacos": "^1.1.3",
"vue": "^3.5.22",
"vue-router": "^4.5.1"
},
"devDependencies": {
"@element-plus/icons-vue": "^2.3.2",
"@tsconfig/node22": "^22.0.2",
"@types/node": "^24.6.1",
"@vitejs/plugin-vue": "^6.0.1",
"@vitejs/plugin-vue-jsx": "^5.1.1",
"@vue/tsconfig": "^0.8.1",
"element-plus": "^2.11.4",
"npm-run-all2": "^8.0.4",
"rollup-plugin-visualizer": "^6.0.3",
"typescript": "~5.9.3",
"vite": "^7.1.7",
"vite-federation": "^3.4.4",
"vite-plugin-vue-devtools": "^8.0.2",
"vue-tsc": "^3.1.0"
}
}

View File

@@ -1,27 +0,0 @@
import { defineComponent, onMounted } from 'vue'
export default defineComponent({
setup() {
onMounted(() => {
console.log("import.meta.env: ", import.meta.env)
})
return () => (
<>
<div class="order-federation-placeholder">
<h2>-</h2>
<p>
</p>
<h2></h2>
<ul>
<li>1</li>
<li>2</li>
</ul>
<p></p>
</div>
</>
)
}
})

View File

@@ -1,22 +0,0 @@
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

@@ -1,38 +0,0 @@
// 从商品联邦模块导入领域层资源
import {ProductRepository} from "@domains/product/ports/repositories/ProductRepository";
import {HttpProductRepository} from "@domains/product/infr/api/HttpProductRepository";
import {ProductApplicationService} from "@domains/product/application/services/ProductApplicationService";
/**
* 商品领域的依赖注入配置
* 将 接口 与 具体实现 绑定
*
* 优点:是一个依赖工厂,它隐藏了对象创建的细节,
* 让业务代码只需要关心 “做什么”,而不用关心 “依赖从哪里来”
* 所有依赖关系都在di目录下集中配置一目了然后续维护时能快速找到所有依赖的创建逻辑。
*
* 松耦合:
* 业务代码ProductApplicationService只依赖抽象接口ProductRepository
* 不依赖具体实现。如果未来需要改用其他数据来源(比如 WebSocket
* 只需修改bindProductRepository的返回值无需改动业务逻辑。
*
* 易于测试:
* 测试时可以临时替换为模拟实现
* productModule.bindProductRepository = () => new MockProductRepository();
* 这样测试就可以脱离真实 API使用预设的模拟数据。
*/
export const product = {
// 绑定 仓储抽象接口 到 HTTP具体实现
bindProductRepository: (): ProductRepository => {
// return new MockProductRepository 直接从真实接口数据改为测试模拟数据
return new HttpProductRepository;
},
// 创建应用服务实例
createProductApplicationService: (): ProductApplicationService => {
return new ProductApplicationService(
product.bindProductRepository()
);
}
}

View File

@@ -1,141 +0,0 @@
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

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +0,0 @@
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

@@ -1,27 +0,0 @@
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";
import { http } from "shared/infra";
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

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

View File

@@ -1,20 +0,0 @@
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

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

View File

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

View File

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

View File

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

View File

@@ -1,5 +0,0 @@
.ProductDetail {
.title {
color: blue;
}
}

View File

@@ -1,62 +0,0 @@
import styles from './ProductDetail.module.less'
import { product } from '@domains/product/di/productModule';
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({
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={ styles.ProductDetail }>
<h1 class={ styles.title }> - </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

@@ -1,5 +0,0 @@
.ProductList {
.test {
color: var(--theme-color);
}
}

View File

@@ -1,79 +0,0 @@
import styles from './ProductList.module.less'
import { defineComponent, onMounted, reactive } from 'vue'
import { useRouter } from 'vue-router';
import { product } from '@domains/product/di/productModule';
import { ProductType } from '@domains/product/domain/type/ProductType';
import { ElCard, ElIcon, Icons } from "shared/element-plus";
interface storeType {
list: Array<ProductType>
}
export default defineComponent({
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);
}
const ElCardHeader = () => {
return (
<div class="card-header">
<ElIcon size={ 15 }>
<Icons.Position></Icons.Position>
</ElIcon>
<span>Card </span>
</div>
)
}
const ElCardFooter = () => {
return <div>Footer </div>
}
return () => (
<div class={ styles.ProductList }>
<h1 class={ styles.test }> - 3</h1>
<hr/>
<h2>Deno </h2>
<ElCard>
{ {
header: () => ElCardHeader(),
footer: () => ElCardFooter(),
default: () =>
state.list.map(item =>
<p key={ item.id } onClick={ () => {
router.push({ name: 'ProductDetail', query: { id: item.id } });
} }>
{ item.id } | { item.name } | ¥{ item.price }
</p>
)
} }
</ElCard>
</div>
)
}
})
// export default defineComponent({
// setup() {
// return () => (
// <div>商品列表</div>
// )
// }
// })

View File

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

View File

@@ -1,4 +0,0 @@
declare module '*.module.less' {
const classes: { [key: string]: string }
export default classes
}

View File

@@ -1,29 +0,0 @@
/// <reference types="vite/client" />
declare module 'shared/infra' {
export const http: any;
}
declare module 'shared/utils' {
export const UrlCheckUtils: any;
}
declare module 'shared/element-plus' {
import * as ElementPlus from 'element-plus';
import * as ElementPlusIcons from '@element-plus/icons-vue';
import type { App } from 'vue';
export * from 'element-plus';
export import Icons = ElementPlusIcons;
export function init(app: App): void;
const defaultExport: typeof ElementPlus & {
Icons: typeof ElementPlusIcons;
init: typeof init;
};
export default defaultExport;
}
declare module 'shared/normalize' {
export const init: () => void;
}

View File

@@ -1,33 +0,0 @@
{
"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

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

View File

@@ -1,19 +0,0 @@
{
"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

@@ -1,66 +0,0 @@
import { defineConfig, loadEnv } 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 'vite-federation';
import { fileURLToPath, URL } from "node:url";
import { visualizer } from 'rollup-plugin-visualizer';
import { reg, init } from 'vite-plugin-nacos';
export default defineConfig(async ({ mode }) => {
const env = loadEnv(mode, process.cwd());
const { VITE_MODEL_SHARED_URL } = await init(env)
// console.log("VITE_MODEL_SHARED_URL: ", VITE_MODEL_SHARED_URL);
return {
plugins: [
reg(),
federation({
name: env.VITE_SERVER_NAME,
remotes: {
shared: `${VITE_MODEL_SHARED_URL}/${env.VITE_MODEL_SHARED_NAME}/assets/remoteEntry.js`
},
exposes: { // 暴露给主应用的资源(按功能分组)
'./ProductDomain': './src/domains/product/index.ts', // 领域层(实体、服务、接口等)
'./ProductRouter': './src/features/product/router/index.ts' // 商品路由
},
shared: {
vue: { import: false, generate: false, requiredVersion: '^3.5.18' },
'vue-router': { import: false, generate: false, requiredVersion: '^4.0.6' },
pinia: { import: false, generate: false, requiredVersion: '^3.0.3' },
}
}),
vue(),
vueJsx(),
vueDevTools(),
visualizer({
title: "商品模块依赖分析",
filename: 'dist/stats.html', // 分析文件输出路径
open: false, // 构建完成后自动打开
gzipSize: true, // 显示gzip后的大小
brotliSize: true, // 显示brotli后的大小
})
],
build: {
target: 'esnext',
cssCodeSplit: true,
rollupOptions: {
// 关键配置:告诉 Rollup 不要处理模块联邦的导入
external: ['shared/normalize'],
output: {
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))
},
},
}
});