Compare commits
8 Commits
9be53282f4
...
02cae30c4c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
02cae30c4c | ||
|
|
2c3cffdfa5 | ||
|
|
858018e43d | ||
|
|
61c84b0842 | ||
|
|
c156637f03 | ||
|
|
c3d4b7e1e4 | ||
|
|
715d3ff0bb | ||
|
|
62fdd169c5 |
93
README.md
93
README.md
@@ -1,3 +1,94 @@
|
|||||||
|
## DDD领域驱动设计的微服务前端案例
|
||||||
|
|
||||||
|
> 持续开发中,及时拉取代码,文档还不够完善,先凑合看!!目前只是demo,代码还会进行大规模封装细化和改造!
|
||||||
|
|
||||||
|
搭建的一套**基于DDD领域驱动设计的微服务前端**,适用于大型企业,开发大型复杂且拓展灵活的前端应用的架构方案。
|
||||||
|
|
||||||
|
> 微服务架构中,前端用什么语言不受约束,随意就好!
|
||||||
|
|
||||||
|
项目大致技术栈:
|
||||||
|
- [DDD领域驱动设计](https://zh.wikipedia.org/zh-cn/%E9%A0%98%E5%9F%9F%E9%A9%85%E5%8B%95%E8%A8%AD%E8%A8%88)
|
||||||
|
- [@originjs/vite-plugin-federation](https://www.npmjs.com/package/@originjs/vite-plugin-federation)
|
||||||
|
- [Deno](https://deno.com/)
|
||||||
|
- [TypeScript](https://www.typescriptlang.org/)
|
||||||
|
- [GraphQL](https://graphql.org/)
|
||||||
|
- [docker](https://www.docker.com/)
|
||||||
|
- [drone](https://drone.cool/)
|
||||||
|
- [nginx](https://nginx.org/)
|
||||||
|
- [Vue3](https://cn.vuejs.org/)
|
||||||
|
- [Jsx](https://legacy.reactjs.org/docs/introducing-jsx.html)
|
||||||
|
- [Vite](https://cn.vite.dev/)
|
||||||
|
- [axios](https://axios-http.com/)
|
||||||
|
- 等等等....
|
||||||
|
|
||||||
|
## 开发任务
|
||||||
|
|
||||||
|
- [x] 基于DDD理念改造
|
||||||
|
- [ ] 基于 vite-plugin-federation 拆分领域模块为联邦模块,实现微前端
|
||||||
|
- [ ] 领域模块实现 docker自动化构建部署上线
|
||||||
|
- [ ] Deno集成GraphQL
|
||||||
|
|
||||||
|
## 文件夹结构
|
||||||
|
|
||||||
|
**根目录:**
|
||||||
|
|
||||||
|
```txt
|
||||||
|
.
|
||||||
|
├── back-end # 【后端】基于Deno写的后端 测试接口,仅供测试
|
||||||
|
├── doc # 一些markdown笔记
|
||||||
|
├── front-end # 前端 微服务 项目
|
||||||
|
│ ├── app-car # 独立的【购物车】领域模块
|
||||||
|
│ ├── app-product # 独立的【商品】领域模块
|
||||||
|
│ ├── .... # 实际项目还会有更多其他的领域模块
|
||||||
|
│ └── main-app # 主应用(入口应用)
|
||||||
|
└── README.md # 帮助文档
|
||||||
|
```
|
||||||
|
|
||||||
|
**main-app 目录:**
|
||||||
|
|
||||||
|
```txt
|
||||||
|
src/
|
||||||
|
├── domains/ # 【核心】领域层 - 所有业务领域模块
|
||||||
|
│ └── product/ # 订单领域模块(app-product)
|
||||||
|
│ ├── domain/ # 领域模型(充血模型)
|
||||||
|
│ │ ├── entities/ # 实体(如:Product, ProductLineItem)
|
||||||
|
│ │ ├── value-objects/ # 值对象(如:Money, Address)
|
||||||
|
│ │ ├── types/ # 领域内通用类型
|
||||||
|
│ │ ├── enums/ # 领域枚举
|
||||||
|
│ │ └── events/ # 领域事件(如果需要)
|
||||||
|
│ ├── application/ # 应用服务层 - 协调领域对象完成用例
|
||||||
|
│ │ └── services/ # 应用服务(如:ProductApplicationService)
|
||||||
|
│ ├── infrastructure/ # 基础设施层 - 领域层的具体实现
|
||||||
|
│ │ └── api/ # 数据获取实现(如:HttpProductRepository)
|
||||||
|
│ ├── ports/ # 端口(接口/抽象类)- 定义契约
|
||||||
|
│ │ └── repositories/ # 仓储接口(如:ProductRepository)
|
||||||
|
│ └── use-cases/ # (可选)或将用例放在这里
|
||||||
|
├── app/ # 【交付层】应用组装和配置:路由、状态管理、主布局、UI库设置、错误处理、依赖注入容器
|
||||||
|
│ ├── components/ # 通用UI组件(与业务无关,如Button,Modal)
|
||||||
|
│ ├── layouts/ # 布局组件
|
||||||
|
│ ├── di/ # 依赖注入配置
|
||||||
|
│ ├── router/ # Vue Router 配置
|
||||||
|
│ ├── stores/ # Pinia Store(用于UI状态管理,非核心业务状态)
|
||||||
|
│ └── main.ts # 应用入口,依赖注入的组装之地
|
||||||
|
│ └── App.vue # 根组件
|
||||||
|
├── features/ # 【交付层】基于领域的功能模块:关注具体的业务功能实现,它是舞台上的“演员”和“节目”。
|
||||||
|
│ └── product/ # 订单功能模块(app-product)
|
||||||
|
│ ├── components/ # 订单领域专用的UI组件
|
||||||
|
│ ├── views/ # 订单相关的页面级Vue组件
|
||||||
|
│ ├── router/ # 订单相关的页面级Vue组件
|
||||||
|
│ ├── composables/ # 订单相关的Vue组合式函数
|
||||||
|
│ └── index.ts # 订单功能模块的出口
|
||||||
|
├── shared/ # 共享资源
|
||||||
|
│ ├── infra(infrastructure) # 基础设施层
|
||||||
|
│ │ ├── http/
|
||||||
|
│ ├── lib/ # 第三方库的封装/工具函数
|
||||||
|
│ ├── utils/ # 纯工具函数
|
||||||
|
│ └── types/ # 全局通用的TypeScript类型定义
|
||||||
|
│ └── constants/ # 全局常量(如错误码、正则)
|
||||||
|
│ └── exceptions/ # 通用异常(如NotFoundError)
|
||||||
|
└── public/ # 静态资源目录
|
||||||
|
└── index.html # 主HTML文件
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
## 问 & 答
|
## 问 & 答
|
||||||
@@ -11,7 +102,7 @@
|
|||||||
|
|
||||||
|
|
||||||
问:vue3 jsx defineComponent 语法,实际项目中 setup 代码太长,怎么办?
|
问:vue3 jsx defineComponent 语法,实际项目中 setup 代码太长,怎么办?
|
||||||
答:使用组合式函数 (Composables) - 最推荐
|
答:使用组合式函数 (Composables)
|
||||||
|
|
||||||
案例:
|
案例:
|
||||||
|
|
||||||
|
|||||||
13
back-end/.gitignore
vendored
Normal file
13
back-end/.gitignore
vendored
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
# dotenv environment variable files
|
||||||
|
.env
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
.env.local
|
||||||
|
|
||||||
|
# Fresh build directory
|
||||||
|
_fresh/
|
||||||
|
# npm + other dependencies
|
||||||
|
node_modules/
|
||||||
|
vendor/
|
||||||
|
|
||||||
17
back-end/README.md
Normal file
17
back-end/README.md
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
# Fresh project
|
||||||
|
|
||||||
|
Your new Fresh project is ready to go. You can follow the Fresh "Getting
|
||||||
|
Started" guide here: https://fresh.deno.dev/docs/getting-started
|
||||||
|
|
||||||
|
### Usage
|
||||||
|
|
||||||
|
Make sure to install Deno:
|
||||||
|
https://docs.deno.com/runtime/getting_started/installation
|
||||||
|
|
||||||
|
Then start the project in development mode:
|
||||||
|
|
||||||
|
```
|
||||||
|
deno task dev
|
||||||
|
```
|
||||||
|
|
||||||
|
This will watch the project directory and restart as necessary.
|
||||||
144
back-end/assets/styles.css
Normal file
144
back-end/assets/styles.css
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
*,
|
||||||
|
*::before,
|
||||||
|
*::after {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
button, [role="button"] {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
code {
|
||||||
|
font-family:
|
||||||
|
ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono",
|
||||||
|
"Courier New", monospace;
|
||||||
|
font-size: 1em;
|
||||||
|
}
|
||||||
|
img,
|
||||||
|
svg {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
img,
|
||||||
|
video {
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
line-height: 1.5;
|
||||||
|
-webkit-text-size-adjust: 100%;
|
||||||
|
font-family:
|
||||||
|
ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI",
|
||||||
|
Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif,
|
||||||
|
"Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol",
|
||||||
|
"Noto Color Emoji";
|
||||||
|
}
|
||||||
|
.transition-colors {
|
||||||
|
transition-property: background-color, border-color, color, fill, stroke;
|
||||||
|
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
transition-duration: 150ms;
|
||||||
|
}
|
||||||
|
.my-6 {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
}
|
||||||
|
.text-4xl {
|
||||||
|
font-size: 2.25rem;
|
||||||
|
line-height: 2.5rem;
|
||||||
|
}
|
||||||
|
.mx-2 {
|
||||||
|
margin-left: 0.5rem;
|
||||||
|
margin-right: 0.5rem;
|
||||||
|
}
|
||||||
|
.my-4 {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
.mx-auto {
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
|
.px-4 {
|
||||||
|
padding-left: 1rem;
|
||||||
|
padding-right: 1rem;
|
||||||
|
}
|
||||||
|
.py-8 {
|
||||||
|
padding-bottom: 2rem;
|
||||||
|
padding-top: 2rem;
|
||||||
|
}
|
||||||
|
.bg-\[\#86efac\] {
|
||||||
|
background-color: #86efac;
|
||||||
|
}
|
||||||
|
.text-3xl {
|
||||||
|
font-size: 1.875rem;
|
||||||
|
line-height: 2.25rem;
|
||||||
|
}
|
||||||
|
.py-6 {
|
||||||
|
padding-bottom: 1.5rem;
|
||||||
|
padding-top: 1.5rem;
|
||||||
|
}
|
||||||
|
.px-2 {
|
||||||
|
padding-left: 0.5rem;
|
||||||
|
padding-right: 0.5rem;
|
||||||
|
}
|
||||||
|
.py-1 {
|
||||||
|
padding-bottom: 0.25rem;
|
||||||
|
padding-top: 0.25rem;
|
||||||
|
}
|
||||||
|
.border-gray-500 {
|
||||||
|
border-color: #6b7280;
|
||||||
|
}
|
||||||
|
.bg-white {
|
||||||
|
background-color: #fff;
|
||||||
|
}
|
||||||
|
.flex {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
.gap-8 {
|
||||||
|
grid-gap: 2rem;
|
||||||
|
gap: 2rem;
|
||||||
|
}
|
||||||
|
.font-bold {
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
.max-w-screen-md {
|
||||||
|
max-width: 768px;
|
||||||
|
}
|
||||||
|
.flex-col {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.items-center {
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.justify-center {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.border-2 {
|
||||||
|
border-width: 2px;
|
||||||
|
}
|
||||||
|
.rounded-sm {
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
}
|
||||||
|
.hover\:bg-gray-200:hover {
|
||||||
|
background-color: #e5e7eb;
|
||||||
|
}
|
||||||
|
.tabular-nums {
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
|
.min-h-screen {
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fresh-gradient {
|
||||||
|
background-color: rgb(134, 239, 172);
|
||||||
|
background-image: linear-gradient(
|
||||||
|
to right bottom,
|
||||||
|
rgb(219, 234, 254),
|
||||||
|
rgb(187, 247, 208),
|
||||||
|
rgb(254, 249, 195)
|
||||||
|
);
|
||||||
|
}
|
||||||
2
back-end/client.ts
Normal file
2
back-end/client.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
// Import CSS files here for hot module reloading to work.
|
||||||
|
import "./assets/styles.css";
|
||||||
53
back-end/deno.json
Normal file
53
back-end/deno.json
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
{
|
||||||
|
"nodeModulesDir": "auto",
|
||||||
|
"tasks": {
|
||||||
|
"check": "deno fmt --check . && deno lint . && deno check",
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vite build",
|
||||||
|
"start": "deno serve -A _fresh/server.js",
|
||||||
|
"update": "deno run -A -r jsr:@fresh/update ."
|
||||||
|
},
|
||||||
|
"lint": {
|
||||||
|
"rules": {
|
||||||
|
"tags": [
|
||||||
|
"fresh",
|
||||||
|
"recommended"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"exclude": [
|
||||||
|
"**/_fresh/*"
|
||||||
|
],
|
||||||
|
"imports": {
|
||||||
|
"fresh": "jsr:@fresh/core@^2.1.0",
|
||||||
|
"@fresh/plugin-vite": "jsr:@fresh/plugin-vite@^1.0.3",
|
||||||
|
"vite": "npm:vite@^7.1.3"
|
||||||
|
},
|
||||||
|
"compilerOptions": {
|
||||||
|
"lib": [
|
||||||
|
"dom",
|
||||||
|
"dom.asynciterable",
|
||||||
|
"dom.iterable",
|
||||||
|
"deno.ns"
|
||||||
|
],
|
||||||
|
"jsxPrecompileSkipElements": [
|
||||||
|
"a",
|
||||||
|
"img",
|
||||||
|
"source",
|
||||||
|
"body",
|
||||||
|
"html",
|
||||||
|
"head",
|
||||||
|
"title",
|
||||||
|
"meta",
|
||||||
|
"script",
|
||||||
|
"link",
|
||||||
|
"style",
|
||||||
|
"base",
|
||||||
|
"noscript",
|
||||||
|
"template"
|
||||||
|
],
|
||||||
|
"types": [
|
||||||
|
"vite/client"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
1231
back-end/deno.lock
generated
Normal file
1231
back-end/deno.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
82
back-end/main.ts
Normal file
82
back-end/main.ts
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
import { App, staticFiles, cors } from "fresh";
|
||||||
|
import { define, type State } from "./utils.ts";
|
||||||
|
|
||||||
|
export const app = new App<State>();
|
||||||
|
|
||||||
|
app.use(cors({
|
||||||
|
origin: "*",
|
||||||
|
allowHeaders: ["*"],
|
||||||
|
allowMethods: ["POST", "GET", "OPTIONS"],
|
||||||
|
exposeHeaders: ["Content-Length", "X-Kuma-Revision"],
|
||||||
|
maxAge: 600,
|
||||||
|
credentials: true,
|
||||||
|
}))
|
||||||
|
|
||||||
|
app.use(staticFiles());
|
||||||
|
|
||||||
|
// 模拟产品数据
|
||||||
|
const mockProductsList = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
name: "Xiaomi Buds 5 Pro",
|
||||||
|
description: "高音质无线蓝牙耳机,续航可达30小时",
|
||||||
|
price: 1199.19,
|
||||||
|
imageUrl: "https://cdn.cnbj0.fds.api.mi-img.com/b2c-shopapi-pms/pms_1740638541.0557913.png",
|
||||||
|
stock: 2000,
|
||||||
|
categoryId: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
name: "机械键盘MK71pro",
|
||||||
|
description: "青轴机械键盘,全键无冲",
|
||||||
|
price: 899.00,
|
||||||
|
imageUrl: "https://cdn.cnbj0.fds.api.mi-img.com/b2c-shopapi-pms/67D4F43CD4C647AD8F45892D6567E14A.png",
|
||||||
|
stock: 1000,
|
||||||
|
categoryId: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
name: "Xiaomi Watch S4 Sport",
|
||||||
|
description: "多功能智能手表,支持心率监测和GPS",
|
||||||
|
price: 1699.00,
|
||||||
|
imageUrl: "https://cdn.cnbj0.fds.api.mi-img.com/b2c-shopapi-pms/pms_1721233615.78223053.png",
|
||||||
|
stock: 200,
|
||||||
|
categoryId: 1,
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const Res = <T extends Array<Object> | Object>(data: T) => {
|
||||||
|
return JSON.stringify({
|
||||||
|
code: 200,
|
||||||
|
message: "success",
|
||||||
|
result: data
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// http://127.0.0.1:3000/api/productsList
|
||||||
|
app.get("/api/productsList", (ctx: { params: { name: any; }; }) => {
|
||||||
|
return new Response(Res(mockProductsList));
|
||||||
|
});
|
||||||
|
|
||||||
|
// http://127.0.0.1:3000/api/productsInfo?id=3
|
||||||
|
app.get("/api/productsInfo", (req: Request, ctx: { params: { id: number; }; }) => {
|
||||||
|
const id = (new URL(req.url)).searchParams.get('id')
|
||||||
|
|
||||||
|
if (!id) {
|
||||||
|
return new Response(Res("缺少id参数"));
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = mockProductsList.find(product => product.id == Number(id))
|
||||||
|
if (!result) {
|
||||||
|
return new Response(Res("找不到数据"));
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Response(Res(result));
|
||||||
|
});
|
||||||
|
|
||||||
|
const exampleLoggerMiddleware = define.middleware((ctx: { req: { method: any; url: any; }; next: () => any; }) => {
|
||||||
|
console.log(`${ctx.req.method} ${ctx.req.url}`);
|
||||||
|
return ctx.next();
|
||||||
|
});
|
||||||
|
|
||||||
|
app.use(exampleLoggerMiddleware);
|
||||||
BIN
back-end/static/favicon.ico
Normal file
BIN
back-end/static/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 22 KiB |
19
back-end/static/logo.svg
Normal file
19
back-end/static/logo.svg
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<svg width="40" height="40" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path
|
||||||
|
d="M34.092 8.845C38.929 20.652 34.092 27 30 30.5c1 3.5-2.986 4.222-4.5 2.5-4.457 1.537-13.512 1.487-20-5C2 24.5 4.73 16.714 14 11.5c8-4.5 16-7 20.092-2.655Z"
|
||||||
|
fill="#FFDB1E"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M14 11.5c6.848-4.497 15.025-6.38 18.368-3.47C37.5 12.5 21.5 22.612 15.5 25c-6.5 2.587-3 8.5-6.5 8.5-3 0-2.5-4-5.183-7.75C2.232 23.535 6.16 16.648 14 11.5Z"
|
||||||
|
fill="#fff"
|
||||||
|
stroke="#FFDB1E"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M28.535 8.772c4.645 1.25-.365 5.695-4.303 8.536-3.732 2.692-6.606 4.21-7.923 4.83-.366.173-1.617-2.252-1.617-1 0 .417-.7 2.238-.934 2.326-1.365.512-4.223 1.29-5.835 1.29-3.491 0-1.923-4.754 3.014-9.122.892-.789 1.478-.645 2.283-.645-.537-.773-.534-.917.403-1.546C17.79 10.64 23 8.77 25.212 8.42c.366.014.82.35.82.629.41-.14 2.095-.388 2.503-.278Z"
|
||||||
|
fill="#FFE600"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M14.297 16.49c.985-.747 1.644-1.01 2.099-2.526.566.121.841-.08 1.29-.701.324.466 1.657.608 2.453.701-.715.451-1.057.852-1.452 2.106-1.464-.611-3.167-.302-4.39.42Z"
|
||||||
|
fill="#fff"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.1 KiB |
9
back-end/utils.ts
Normal file
9
back-end/utils.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { createDefine } from "fresh";
|
||||||
|
|
||||||
|
// This specifies the type of "ctx.state" which is used to share
|
||||||
|
// data among middlewares, layouts and routes.
|
||||||
|
export interface State {
|
||||||
|
shared: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const define = createDefine<State>();
|
||||||
12
back-end/vite.config.ts
Normal file
12
back-end/vite.config.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { defineConfig } from "vite";
|
||||||
|
import { fresh } from "@fresh/plugin-vite";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
server: {
|
||||||
|
port: 3000, // 设置Vite开发服务器端口
|
||||||
|
strictPort: true, // 严格使用指定端口,如果被占用则报错
|
||||||
|
// 可选:允许跨域请求
|
||||||
|
cors: true,
|
||||||
|
},
|
||||||
|
plugins: [fresh()],
|
||||||
|
});
|
||||||
34
doc/代码存放位置.md
Normal file
34
doc/代码存放位置.md
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
|
||||||
|
### 1. 领域层(Domains)
|
||||||
|
- 实体类:`src/domains/product/domain/entities/Product.ts`
|
||||||
|
- 仓储接口(端口):`src/domains/product/ports/repositories/ProductRepository.ts`
|
||||||
|
|
||||||
|
|
||||||
|
### 2. 基础设施层(Infrastructure)
|
||||||
|
- HTTP仓储实现:`src/domains/product/infrastructure/api/HttpProductRepository.ts`
|
||||||
|
- 通用HTTP客户端:`src/shared/infra/http/httpClient.ts`
|
||||||
|
|
||||||
|
|
||||||
|
### 3. 应用服务层(Application)
|
||||||
|
- 商品应用服务:`src/domains/product/application/services/ProductApplicationService.ts`
|
||||||
|
|
||||||
|
|
||||||
|
### 4. 依赖注入配置
|
||||||
|
- 商品领域DI配置:`src/app/di/productModule.ts`
|
||||||
|
|
||||||
|
|
||||||
|
### 5. 路由配置(交付层)
|
||||||
|
- 商品路由:`src/features/product/router/index.ts`
|
||||||
|
- 主路由配置:`src/app/router/index.ts`
|
||||||
|
|
||||||
|
|
||||||
|
### 6. 功能实现(交付层)
|
||||||
|
- 商品列表页面:`src/features/product/views/ProductList.tsx`
|
||||||
|
- 商品详情页面:`src/features/product/views/ProductDetail.tsx`
|
||||||
|
- 商品卡片组件:`src/features/product/components/ProductCard.tsx`
|
||||||
|
|
||||||
|
|
||||||
|
### 7. 通用UI组件(交付层)
|
||||||
|
- 分页组件:`src/app/components/Pagination.tsx`
|
||||||
|
- 加载组件:`src/app/components/Loading.tsx`(代码中引用)
|
||||||
|
- 按钮组件:`src/app/components/Button.tsx`(代码中引用)
|
||||||
@@ -1,3 +1,16 @@
|
|||||||
|
### 依赖注意
|
||||||
|
|
||||||
|
DDD 中允许 “外层依赖内层”(交付层依赖领域层),但不允许 “内层依赖外层”。
|
||||||
|
- 正确:features(交付层) → domains(领域层)
|
||||||
|
- 错误:domains(领域层) → features(交付层)
|
||||||
|
|
||||||
|
#### 避免类型重复定义
|
||||||
|
如果不在视图中直接使用领域类型,就需要在交付层重新定义一套类似的类型(如 “视图专用商品类型”),这会导致:
|
||||||
|
|
||||||
|
- 代码冗余和不一致风险
|
||||||
|
- 类型转换的额外成本
|
||||||
|
- 业务规则分散(领域层的类型约束可能在视图层被忽略)
|
||||||
|
|
||||||
### 横切关注点
|
### 横切关注点
|
||||||
- 特征:横向,影响多个模块,那些无法放入任何一个业务模块,而是会"横着"贯穿多个甚至所有模块的技术性需求。
|
- 特征:横向,影响多个模块,那些无法放入任何一个业务模块,而是会"横着"贯穿多个甚至所有模块的技术性需求。
|
||||||
- 案例:日志记录、身份认证、授权、事务管理、异常处理、缓存、性能监控
|
- 案例:日志记录、身份认证、授权、事务管理、异常处理、缓存、性能监控
|
||||||
15
doc/目录结构.md
15
doc/目录结构.md
@@ -1,30 +1,33 @@
|
|||||||
```txt
|
```txt
|
||||||
src/
|
src/
|
||||||
├── domains/ # 【核心】领域层 - 所有业务领域模块
|
├── domains/ # 【核心】领域层 - 所有业务领域模块
|
||||||
│ └── order/ # 订单领域模块
|
│ └── product/ # 订单领域模块
|
||||||
│ ├── domain/ # 领域模型(充血模型)
|
│ ├── domain/ # 领域模型(充血模型)
|
||||||
│ │ ├── entities/ # 实体(如:Order, OrderLineItem)
|
│ │ ├── entities/ # 实体(如:Product, ProductLineItem)
|
||||||
│ │ ├── value-objects/ # 值对象(如:Money, Address)
|
│ │ ├── value-objects/ # 值对象(如:Money, Address)
|
||||||
|
│ │ ├── types/ # 领域内通用类型
|
||||||
│ │ ├── enums/ # 领域枚举
|
│ │ ├── enums/ # 领域枚举
|
||||||
│ │ └── events/ # 领域事件(如果需要)
|
│ │ └── events/ # 领域事件(如果需要)
|
||||||
│ ├── application/ # 应用服务层 - 协调领域对象完成用例
|
│ ├── application/ # 应用服务层 - 协调领域对象完成用例
|
||||||
│ │ └── services/ # 应用服务(如:OrderApplicationService)
|
│ │ └── services/ # 应用服务(如:ProductApplicationService)
|
||||||
│ ├── infrastructure/ # 基础设施层 - 领域层的具体实现
|
│ ├── infrastructure/ # 基础设施层 - 领域层的具体实现
|
||||||
│ │ └── api/ # 数据获取实现(如:HttpOrderRepository)
|
│ │ └── api/ # 数据获取实现(如:HttpProductRepository)
|
||||||
│ ├── ports/ # 端口(接口/抽象类)- 定义契约
|
│ ├── ports/ # 端口(接口/抽象类)- 定义契约
|
||||||
│ │ └── repositories/ # 仓储接口(如:OrderRepository)
|
│ │ └── repositories/ # 仓储接口(如:ProductRepository)
|
||||||
│ └── use-cases/ # (可选)或将用例放在这里
|
│ └── use-cases/ # (可选)或将用例放在这里
|
||||||
├── app/ # 【交付层】应用组装和配置:路由、状态管理、主布局、UI库设置、错误处理、依赖注入容器
|
├── app/ # 【交付层】应用组装和配置:路由、状态管理、主布局、UI库设置、错误处理、依赖注入容器
|
||||||
│ ├── components/ # 通用UI组件(与业务无关,如Button,Modal)
|
│ ├── components/ # 通用UI组件(与业务无关,如Button,Modal)
|
||||||
│ ├── layouts/ # 布局组件
|
│ ├── layouts/ # 布局组件
|
||||||
|
│ ├── di/ # 依赖注入配置
|
||||||
│ ├── router/ # Vue Router 配置
|
│ ├── router/ # Vue Router 配置
|
||||||
│ ├── stores/ # Pinia Store(用于UI状态管理,非核心业务状态)
|
│ ├── stores/ # Pinia Store(用于UI状态管理,非核心业务状态)
|
||||||
│ └── main.ts # 应用入口,依赖注入的组装之地
|
│ └── main.ts # 应用入口,依赖注入的组装之地
|
||||||
│ └── App.vue # 根组件
|
│ └── App.vue # 根组件
|
||||||
├── features/ # 【交付层】基于领域的功能模块:关注具体的业务功能实现,它是舞台上的“演员”和“节目”。
|
├── features/ # 【交付层】基于领域的功能模块:关注具体的业务功能实现,它是舞台上的“演员”和“节目”。
|
||||||
│ └── order/ # 订单功能模块
|
│ └── product/ # 订单功能模块
|
||||||
│ ├── components/ # 订单领域专用的UI组件
|
│ ├── components/ # 订单领域专用的UI组件
|
||||||
│ ├── views/ # 订单相关的页面级Vue组件
|
│ ├── views/ # 订单相关的页面级Vue组件
|
||||||
|
│ ├── router/ # 订单相关的页面级Vue组件
|
||||||
│ ├── composables/ # 订单相关的Vue组合式函数
|
│ ├── composables/ # 订单相关的Vue组合式函数
|
||||||
│ └── index.ts # 订单功能模块的出口
|
│ └── index.ts # 订单功能模块的出口
|
||||||
├── shared/ # 共享资源
|
├── shared/ # 共享资源
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 4.2 KiB |
@@ -8,7 +8,8 @@ export default defineComponent({
|
|||||||
<>
|
<>
|
||||||
<div id="nav">
|
<div id="nav">
|
||||||
<RouterLink to="/">首页</RouterLink> |
|
<RouterLink to="/">首页</RouterLink> |
|
||||||
<RouterLink to="/about">我的</RouterLink>
|
<RouterLink to="/about">我的</RouterLink> |
|
||||||
|
<RouterLink to="/Products">商品</RouterLink>
|
||||||
</div>
|
</div>
|
||||||
<RouterView></RouterView>
|
<RouterView></RouterView>
|
||||||
</>
|
</>
|
||||||
|
Before Width: | Height: | Size: 276 B After Width: | Height: | Size: 276 B |
38
front-end/main-app/src/app/di/productModule.ts
Normal file
38
front-end/main-app/src/app/di/productModule.ts
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import { ProductApplicationService } from "../../domains/product/application/services/ProductApplicationService";
|
||||||
|
import { HttpProductRepository } from "../../domains/product/infr/api/HttpProductRepository";
|
||||||
|
import { ProductRepository } from "../../domains/product/ports/repositories/ProductRepository";
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 商品领域的依赖注入配置
|
||||||
|
* 将 接口 与 具体实现 绑定
|
||||||
|
*
|
||||||
|
* 优点:是一个依赖工厂,它隐藏了对象创建的细节,
|
||||||
|
* 让业务代码只需要关心 “做什么”,而不用关心 “依赖从哪里来”
|
||||||
|
* 所有依赖关系都在di目录下集中配置,一目了然,后续维护时能快速找到所有依赖的创建逻辑。
|
||||||
|
*
|
||||||
|
* 松耦合:
|
||||||
|
* 业务代码(ProductApplicationService)只依赖抽象接口(ProductRepository),
|
||||||
|
* 不依赖具体实现。如果未来需要改用其他数据来源(比如 WebSocket),
|
||||||
|
* 只需修改bindProductRepository的返回值,无需改动业务逻辑。
|
||||||
|
*
|
||||||
|
* 易于测试:
|
||||||
|
* 测试时可以临时替换为模拟实现
|
||||||
|
* productModule.bindProductRepository = () => new MockProductRepository();
|
||||||
|
* 这样测试就可以脱离真实 API,使用预设的模拟数据。
|
||||||
|
*/
|
||||||
|
export const productModule = {
|
||||||
|
|
||||||
|
// 绑定 仓储抽象接口 到 HTTP具体实现
|
||||||
|
bindProductRepository: (): ProductRepository => {
|
||||||
|
// return new MockProductRepository 直接从真实接口数据改为测试模拟数据
|
||||||
|
return new HttpProductRepository;
|
||||||
|
},
|
||||||
|
|
||||||
|
// 创建应用服务实例
|
||||||
|
createProductApplicationService: (): ProductApplicationService => {
|
||||||
|
return new ProductApplicationService(
|
||||||
|
productModule.bindProductRepository()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
30
front-end/main-app/src/app/router/index.ts
Normal file
30
front-end/main-app/src/app/router/index.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'
|
||||||
|
import { productRouter } from '@features/product/router'
|
||||||
|
|
||||||
|
const baseRouter: Array<RouteRecordRaw> = [
|
||||||
|
{
|
||||||
|
path: '/',
|
||||||
|
name: 'home',
|
||||||
|
component: () => import('../views/Home'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/about',
|
||||||
|
name: 'about',
|
||||||
|
component: () => import('../views/About'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/:pathMatch(.*)*',
|
||||||
|
name: 'NotFound',
|
||||||
|
component: () => import('../views/NotFound')
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
// 聚合所有功能模块的路由
|
||||||
|
const routes: RouteRecordRaw[] = [...baseRouter, ...productRouter];
|
||||||
|
|
||||||
|
const router = createRouter({
|
||||||
|
history: createWebHistory(import.meta.env.BASE_URL),
|
||||||
|
routes,
|
||||||
|
})
|
||||||
|
|
||||||
|
export default router
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,137 @@
|
|||||||
|
import { ErrorText } from "../enums/ErrorEnums"
|
||||||
|
|
||||||
|
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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
export enum ErrorText {
|
||||||
|
PriceNotNegativein = "商品 价格 不能为负数",
|
||||||
|
StockNotNegativein = "商品 库存 不能为负数",
|
||||||
|
StockIsGreaterThanZero = "减少的库存数量必须大于0",
|
||||||
|
LowStock = "库存不足",
|
||||||
|
IdNotNull = "商品Id不能为空"
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
export interface ProductType {
|
||||||
|
id: number,
|
||||||
|
name: string,
|
||||||
|
description: string,
|
||||||
|
price: number,
|
||||||
|
imageUrl: string,
|
||||||
|
stock: number,
|
||||||
|
categoryId: number,
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import { Product } from "../../domain/entities/Product";
|
||||||
|
import { ProductType } from "../../domain/type/ProductType";
|
||||||
|
import { ProductRepository } from "../../ports/repositories/ProductRepository";
|
||||||
|
import http from "@shared/infra/AxiosInfra";
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTTP商品仓储实现 - 实现商品数据的HTTP访问
|
||||||
|
* 这是基础设施层,负责具体的数据获取实现
|
||||||
|
*/
|
||||||
|
export class HttpProductRepository implements ProductRepository {
|
||||||
|
|
||||||
|
async findAll(): Promise<Array<ProductType>> {
|
||||||
|
const result: Array<ProductType> = await http.get({ url: "/productsList" });
|
||||||
|
const items = result.map((item) => Product.fromDTO(item))
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
|
async findById(id: number): Promise<Product> {
|
||||||
|
const result: ProductType = await http.get({
|
||||||
|
url: "/productsInfo",
|
||||||
|
data: { id }
|
||||||
|
});
|
||||||
|
return Product.fromDTO(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
}
|
||||||
17
front-end/main-app/src/features/product/router/index.ts
Normal file
17
front-end/main-app/src/features/product/router/index.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { RouteRecordRaw } from 'vue-router';
|
||||||
|
|
||||||
|
|
||||||
|
export const productRouter: Array<RouteRecordRaw> = [
|
||||||
|
{
|
||||||
|
path: '/Products',
|
||||||
|
name: 'ProductList',
|
||||||
|
component: () => import('../views/ProductList.tsx'),
|
||||||
|
meta: { title: '商品列表' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/ProductDetail',
|
||||||
|
name: 'ProductDetail',
|
||||||
|
component: () => import('../views/ProductDetail.tsx'),
|
||||||
|
meta: { title: '商品详情' }
|
||||||
|
},
|
||||||
|
]
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
import { productModule } from '@/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({
|
||||||
|
name: "productModule-ProductDetail",
|
||||||
|
setup() {
|
||||||
|
const route = useRoute();
|
||||||
|
const productService = productModule.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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
import { defineComponent, onMounted, reactive } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
import { productModule } from '@/di/productModule';
|
||||||
|
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 = productModule.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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
export default class HttpConfig {
|
export default class HttpConfig {
|
||||||
public static DevBaseUrl: string = "http://127.0.0.1:8080"
|
public static DevBaseUrl: string = "http://127.0.0.1:3000/api"
|
||||||
public static prodBaseUrl: string = "http://127.0.0.1:9090/api"
|
public static prodBaseUrl: string = "http://127.0.0.1:9090/api/"
|
||||||
public static apiList: string = ""
|
public static apiList: string = ""
|
||||||
}
|
}
|
||||||
@@ -12,6 +12,7 @@
|
|||||||
"src/**/__tests__/*"
|
"src/**/__tests__/*"
|
||||||
],
|
],
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
|
"verbatimModuleSyntax": false,
|
||||||
"jsx": "preserve",
|
"jsx": "preserve",
|
||||||
"jsxImportSource": "vue",
|
"jsxImportSource": "vue",
|
||||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||||
@@ -21,6 +22,12 @@
|
|||||||
],
|
],
|
||||||
"@shared/*": [
|
"@shared/*": [
|
||||||
"./src/shared/*"
|
"./src/shared/*"
|
||||||
|
],
|
||||||
|
"@features/*": [
|
||||||
|
"./src/features/*"
|
||||||
|
],
|
||||||
|
"@domains/*": [
|
||||||
|
"./src/domains/*"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -15,7 +15,9 @@ export default defineConfig({
|
|||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
'@': fileURLToPath(new URL('./src/app/', import.meta.url)),
|
'@': fileURLToPath(new URL('./src/app/', import.meta.url)),
|
||||||
'@shared': fileURLToPath(new URL('./src/shared/', 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))
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
import { createRouter, createWebHistory } from 'vue-router'
|
|
||||||
|
|
||||||
const router = createRouter({
|
|
||||||
history: createWebHistory(import.meta.env.BASE_URL),
|
|
||||||
routes: [
|
|
||||||
{
|
|
||||||
path: '/',
|
|
||||||
name: 'home',
|
|
||||||
component: () => import('../views/Home'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/about',
|
|
||||||
name: 'about',
|
|
||||||
component: () => import('../views/About'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/:pathMatch(.*)*',
|
|
||||||
name: 'NotFound',
|
|
||||||
component: () => import('../views/NotFound')
|
|
||||||
}
|
|
||||||
],
|
|
||||||
})
|
|
||||||
|
|
||||||
export default router
|
|
||||||
Reference in New Issue
Block a user