first commit
This commit is contained in:
23
.gitignore
vendored
Normal file
23
.gitignore
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
.DS_Store
|
||||
node_modules
|
||||
/dist
|
||||
|
||||
|
||||
# local env files
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# Log files
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
|
||||
# Editor directories and files
|
||||
.idea
|
||||
.vscode
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
84
README.md
Normal file
84
README.md
Normal file
@@ -0,0 +1,84 @@
|
||||
|
||||
|
||||
## 问 & 答
|
||||
|
||||
问: 为什么vue项目要抛弃SFC写法而尝试jsx/tsx方式来编码?
|
||||
答:https://juejin.cn/post/6911175470255964174
|
||||
|
||||
|
||||
问:不会 vue3 jsx 语法,怎么办?
|
||||
答:https://juejin.cn/post/7141674726434439176
|
||||
|
||||
|
||||
问:vue3 jsx defineComponent 语法,实际项目中 setup 代码太长,怎么办?
|
||||
答:使用组合式函数 (Composables) - 最推荐
|
||||
|
||||
案例:
|
||||
|
||||
```jsx
|
||||
// composables/useUserData.js
|
||||
import { ref, onMounted } from 'vue'
|
||||
import api from '@/api'
|
||||
|
||||
export function useUserData(userId) {
|
||||
const user = ref(null)
|
||||
const loading = ref(false)
|
||||
const error = ref(null)
|
||||
|
||||
const fetchUser = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
user.value = await api.getUser(userId)
|
||||
} catch (err) {
|
||||
error.value = err.message
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(fetchUser)
|
||||
|
||||
return {
|
||||
user,
|
||||
loading,
|
||||
error,
|
||||
refetch: fetchUser
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
主组件:
|
||||
|
||||
```jsx
|
||||
// UserProfile.jsx
|
||||
import { defineComponent } from 'vue'
|
||||
import { useUserData } from '@/composables/useUserData'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'UserProfile',
|
||||
|
||||
props: {
|
||||
userId: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
|
||||
setup(props) {
|
||||
const { user, loading, error, refetch } = useUserData(props.userId)
|
||||
|
||||
return () => (
|
||||
<div>
|
||||
{loading.value && <div>Loading...</div>}
|
||||
{error.value && <div>Error: {error.value}</div>}
|
||||
{user.value && (
|
||||
<div>
|
||||
<h2>{user.value.name}</h2>
|
||||
<p>{user.value.email}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
})
|
||||
```
|
||||
98
doc/杂项.md
Normal file
98
doc/杂项.md
Normal file
@@ -0,0 +1,98 @@
|
||||
### 横切关注点
|
||||
- 特征:横向,影响多个模块,那些无法放入任何一个业务模块,而是会"横着"贯穿多个甚至所有模块的技术性需求。
|
||||
- 案例:日志记录、身份认证、授权、事务管理、异常处理、缓存、性能监控
|
||||
|
||||
## 业务关注点
|
||||
- 特征:通常局限于一个业务模块内
|
||||
- 案例:计算订单总额、验证用户邮箱格式、处理库存扣减
|
||||
|
||||
|
||||
### 对象职责的设计理念对比:
|
||||
|
||||
#### 贫血模型
|
||||
|
||||
- 特征:对象仅仅是数据的容器(一堆属性的集合),没有任何业务逻辑。所有操作这些数据的逻辑都放在外部的“服务”或“工具”类中。
|
||||
- 比喻:像一个没有灵魂的空壳,或者一个数据结构。它只负责装数据,至于数据怎么用、怎么变,它不管。
|
||||
|
||||
```typescript
|
||||
// 1. 贫血的Order实体:只有数据,没有行为
|
||||
class Order {
|
||||
public id: string;
|
||||
public items: OrderItem[];
|
||||
public total: number; // 总额需要外部来计算和设置
|
||||
public status: string;
|
||||
}
|
||||
|
||||
// 2. 所有业务逻辑都在外部的“服务”里
|
||||
class OrderService {
|
||||
calculateTotal(order: Order): void {
|
||||
let total = 0;
|
||||
for (const item of order.items) {
|
||||
total += item.price * item.quantity;
|
||||
}
|
||||
order.total = total; // 从外部修改对象的数据
|
||||
}
|
||||
|
||||
markAsPaid(order: Order): void {
|
||||
if (order.total <= 0) {
|
||||
throw new Error('订单金额无效');
|
||||
}
|
||||
order.status = 'paid'; // 从外部修改对象的状态
|
||||
}
|
||||
}
|
||||
|
||||
// 使用方式
|
||||
const order = new Order();
|
||||
order.items = [...];
|
||||
const orderService = new OrderService();
|
||||
orderService.calculateTotal(order); // 服务来算总额
|
||||
orderService.markAsPaid(order); // 服务来改状态
|
||||
```
|
||||
|
||||
#### 充血模型
|
||||
|
||||
- 特征:对象不仅包含数据,还包含与这些数据紧密相关的业务逻辑。它是有行为和责任的。
|
||||
- 比喻:像一个有智慧的专家。它不仅拥有数据,还知道如何操作和处理自己的数据。
|
||||
|
||||
```typescript
|
||||
// 充血的Order实体:数据 + 行为
|
||||
class Order {
|
||||
public id: string;
|
||||
private _items: OrderItem[];
|
||||
private _total: number; // 总额是内部计算的结果
|
||||
public status: string;
|
||||
|
||||
constructor(items: OrderItem[]) {
|
||||
this._items = items;
|
||||
this._total = this.calculateTotal(); // 构造时自己计算总额
|
||||
this.status = 'created';
|
||||
}
|
||||
|
||||
// 业务逻辑封装在实体内部
|
||||
private calculateTotal(): number {
|
||||
return this._items.reduce((sum, item) => sum + (item.price * item.quantity), 0);
|
||||
}
|
||||
|
||||
// 一个公开的业务方法
|
||||
public markAsPaid(): void {
|
||||
// 它自己 knows 自己的业务规则
|
||||
if (this._total <= 0) {
|
||||
throw new Error('订单金额无效,无法支付');
|
||||
}
|
||||
this.status = 'paid';
|
||||
}
|
||||
|
||||
// 提供访问内部数据的方法(如果需要)
|
||||
get total(): number {
|
||||
return this._total;
|
||||
}
|
||||
|
||||
get items(): ReadonlyArray<OrderItem> {
|
||||
return [...this._items]; // 返回副本,保护内部数据
|
||||
}
|
||||
}
|
||||
|
||||
// 使用方式:对象是聪明的,告诉它做什么就行,而不是一步步操作它
|
||||
const order = new Order([...]);
|
||||
order.markAsPaid(); // 对象自己处理状态变更
|
||||
```
|
||||
40
doc/目录结构.md
Normal file
40
doc/目录结构.md
Normal file
@@ -0,0 +1,40 @@
|
||||
```txt
|
||||
src/
|
||||
├── domains/ # 【核心】领域层 - 所有业务领域模块
|
||||
│ └── order/ # 订单领域模块
|
||||
│ ├── domain/ # 领域模型(充血模型)
|
||||
│ │ ├── entities/ # 实体(如:Order, OrderLineItem)
|
||||
│ │ ├── value-objects/ # 值对象(如:Money, Address)
|
||||
│ │ ├── enums/ # 领域枚举
|
||||
│ │ └── events/ # 领域事件(如果需要)
|
||||
│ ├── application/ # 应用服务层 - 协调领域对象完成用例
|
||||
│ │ └── services/ # 应用服务(如:OrderApplicationService)
|
||||
│ ├── infrastructure/ # 基础设施层 - 领域层的具体实现
|
||||
│ │ └── api/ # 数据获取实现(如:HttpOrderRepository)
|
||||
│ ├── ports/ # 端口(接口/抽象类)- 定义契约
|
||||
│ │ └── repositories/ # 仓储接口(如:OrderRepository)
|
||||
│ └── use-cases/ # (可选)或将用例放在这里
|
||||
├── app/ # 【交付层】应用组装和配置:路由、状态管理、主布局、UI库设置、错误处理、依赖注入容器
|
||||
│ ├── components/ # 通用UI组件(与业务无关,如Button,Modal)
|
||||
│ ├── layouts/ # 布局组件
|
||||
│ ├── router/ # Vue Router 配置
|
||||
│ ├── stores/ # Pinia Store(用于UI状态管理,非核心业务状态)
|
||||
│ └── main.ts # 应用入口,依赖注入的组装之地
|
||||
│ └── App.vue # 根组件
|
||||
├── features/ # 【交付层】基于领域的功能模块:关注具体的业务功能实现,它是舞台上的“演员”和“节目”。
|
||||
│ └── order/ # 订单功能模块
|
||||
│ ├── components/ # 订单领域专用的UI组件
|
||||
│ ├── views/ # 订单相关的页面级Vue组件
|
||||
│ ├── composables/ # 订单相关的Vue组合式函数
|
||||
│ └── index.ts # 订单功能模块的出口
|
||||
├── shared/ # 共享资源
|
||||
│ ├── infra(infrastructure) # 基础设施层
|
||||
│ │ ├── http/
|
||||
│ ├── lib/ # 第三方库的封装/工具函数
|
||||
│ ├── utils/ # 纯工具函数
|
||||
│ └── types/ # 全局通用的TypeScript类型定义
|
||||
│ └── constants/ # 全局常量(如错误码、正则)
|
||||
│ └── exceptions/ # 通用异常(如NotFoundError)
|
||||
└── public/ # 静态资源目录
|
||||
└── index.html # 主HTML文件
|
||||
```
|
||||
186
doc/领域驱动设计.md
Normal file
186
doc/领域驱动设计.md
Normal file
@@ -0,0 +1,186 @@
|
||||
# 领域驱动设计
|
||||
|
||||
领域驱动设计(Domain-Driven Design,DDD)是由埃里克・埃文斯(Eric Evans)提出的一种软件开发方法论,核心是通过深入理解业务领域,将业务逻辑与技术实现紧密结合,从而构建出灵活、可维护且贴合业务的复杂系统。它尤其适合业务逻辑复杂、需求多变的大型应用(如金融、电商、企业 ERP 等)。
|
||||
|
||||
### **一、DDD 的核心思想**
|
||||
|
||||
DDD 的核心是 “以领域为中心”,强调开发团队与业务专家深度协作,共同挖掘业务本质(领域知识),并将这些知识转化为软件设计。其目标是:
|
||||
|
||||
|
||||
* 解决复杂业务逻辑的建模问题;
|
||||
|
||||
* 让软件设计直接反映业务规则,降低技术与业务的鸿沟;
|
||||
|
||||
* 提高系统的可扩展性和可维护性。
|
||||
|
||||
### **二、DDD 的核心概念**
|
||||
|
||||
DDD 通过一系列概念将领域知识结构化,核心概念包括:
|
||||
|
||||
#### 1. **领域(Domain)**
|
||||
|
||||
业务所涉及的范围和知识集合,是问题的核心。例如,电商领域包括商品、订单、支付、物流等子领域。
|
||||
|
||||
#### 2. **子领域(Subdomain)**
|
||||
|
||||
将复杂领域拆分为更小的、可管理的子领域,分为三类:
|
||||
|
||||
|
||||
|
||||
* **核心子领域**:业务的核心竞争力(如电商的订单履约),需重点投入设计;
|
||||
|
||||
* **支撑子领域**:支持核心业务但非核心(如用户评价),可适度设计;
|
||||
|
||||
* **通用子领域**:多个子领域共享的功能(如日志、权限),可使用通用组件或第三方工具。
|
||||
|
||||
#### 3. **限界上下文(Bounded Context)**
|
||||
|
||||
|
||||
|
||||
* **定义**:领域模型的边界,在边界内,领域术语、规则、对象的含义是统一的(即 “通用语言”),边界外可能有不同的模型。
|
||||
|
||||
* **作用**:解决领域模型的歧义性(例如,“订单” 在电商订单系统和财务系统中可能有不同含义),是微服务拆分的重要依据(通常一个限界上下文对应一个微服务)。
|
||||
|
||||
#### 4. **通用语言(Ubiquitous Language)**
|
||||
|
||||
|
||||
|
||||
* 开发团队与业务专家共同定义的语言,包含术语、规则、流程,需贯穿需求分析、设计、编码、测试的全流程。
|
||||
|
||||
* 例如,在电商领域,“下单”“库存锁定”“支付回调” 等术语需在团队中达成共识,避免歧义。
|
||||
|
||||
#### 5. **领域模型(Domain Model)**
|
||||
|
||||
对领域知识的抽象表示,是 DDD 的核心。领域模型通过 “实体”“值对象”“聚合” 等元素构成。
|
||||
|
||||
#### 6. **实体(Entity)**
|
||||
|
||||
|
||||
|
||||
* 具有唯一标识(ID),且其生命周期中标识不变的对象,状态可变化。
|
||||
|
||||
* 例如:“用户”(ID 唯一,姓名、地址可变)、“订单”(订单号唯一,状态可从 “待支付” 变为 “已发货”)。
|
||||
|
||||
#### 7. **值对象(Value Object)**
|
||||
|
||||
|
||||
|
||||
* 无唯一标识,仅通过属性值来定义,不可变(一旦创建,属性不可修改)。
|
||||
|
||||
* 用于描述实体的属性或状态。例如:“地址”(由省、市、街道等属性组成,无独立 ID)、“金额”(由数值和货币单位组成)。
|
||||
|
||||
#### 8. **聚合(Aggregate)**
|
||||
|
||||
|
||||
|
||||
* 一组紧密关联的实体和值对象的集合,作为数据修改和事务的最小单元(“聚合根” 是聚合的入口)。
|
||||
|
||||
* 目的:保证领域模型的一致性,避免复杂的跨对象事务。
|
||||
|
||||
* **聚合根(Aggregate Root)**:聚合中唯一对外暴露的实体,外部只能通过聚合根访问聚合内的其他对象。例如,“订单” 是聚合根,包含 “订单项”(实体)和 “收货地址”(值对象),外部需通过订单 ID 操作订单项。
|
||||
|
||||
#### 9. **领域服务(Domain Service)**
|
||||
|
||||
|
||||
|
||||
* 当业务逻辑不属于某个实体或值对象时,将其封装为领域服务(无状态,仅处理领域逻辑)。
|
||||
|
||||
* 例如:“订单结算” 涉及订单、支付、库存多个聚合,需通过领域服务协调。
|
||||
|
||||
#### 10. **领域事件(Domain Event)**
|
||||
|
||||
|
||||
|
||||
* 领域中发生的重要事件(如 “订单支付成功”“库存不足”),用于捕获领域中的状态变化,实现跨聚合 / 限界上下文的通信。
|
||||
|
||||
* 例如:订单支付成功后,发布 “OrderPaidEvent”,库存系统监听事件并扣减库存,物流系统监听并创建物流单。
|
||||
|
||||
#### 11. **仓储(Repository)**
|
||||
|
||||
|
||||
|
||||
* 封装对聚合的持久化操作(如保存、查询),隔离领域模型与数据存储细节(如数据库、缓存)。
|
||||
|
||||
* 仓储仅针对聚合根设计,例如 “OrderRepository” 负责订单聚合的持久化。
|
||||
|
||||
#### 12. **工厂(Factory)**
|
||||
|
||||
|
||||
|
||||
* 用于创建复杂的领域对象(如聚合),封装创建逻辑,避免领域模型被创建逻辑污染。
|
||||
|
||||
### **三、DDD 的分层架构**
|
||||
|
||||
为了隔离关注点,DDD 通常采用分层架构(与传统三层架构不同,更强调领域层的核心地位):
|
||||
|
||||
|
||||
|
||||
1. **用户界面层(UI Layer)**
|
||||
|
||||
* 负责与用户交互(如 API 接口、网页、APP),处理输入输出,不包含业务逻辑。
|
||||
|
||||
1. **应用层(Application Layer)**
|
||||
|
||||
* 协调领域层完成业务用例(如 “下单流程” 需调用订单聚合、支付服务、库存服务),无业务规则,仅负责流程编排。
|
||||
|
||||
* 依赖领域层,不依赖基础设施层(通过依赖注入反转)。
|
||||
|
||||
1. **领域层(Domain Layer)**
|
||||
|
||||
* 核心层,包含领域模型(实体、值对象、聚合、领域服务、领域事件等),封装业务规则和逻辑。
|
||||
|
||||
* 不依赖其他层(基础设施层的依赖通过接口反转)。
|
||||
|
||||
1. **基础设施层(Infrastructure Layer)**
|
||||
|
||||
* 提供技术支持(如数据库持久化、消息队列、缓存、第三方服务集成)。
|
||||
|
||||
* 实现领域层定义的接口(如 Repository 的数据库实现),支撑应用层和领域层的运行。
|
||||
|
||||
### **四、DDD 的实践流程**
|
||||
|
||||
|
||||
|
||||
1. **事件风暴(Event Storming)**:业务专家与开发团队通过头脑风暴,梳理领域中的事件、命令、实体、聚合等,划分限界上下文,形成通用语言。
|
||||
|
||||
2. **领域建模**:在限界上下文内,设计实体、值对象、聚合等领域模型,明确聚合根和领域服务。
|
||||
|
||||
3. **分层实现**:按分层架构编码,领域层优先实现,基础设施层提供技术支撑。
|
||||
|
||||
4. **持续迭代**:通过反馈优化模型,确保领域模型与业务同步演进。
|
||||
|
||||
### **五、DDD 与微服务的关系**
|
||||
|
||||
DDD 是微服务拆分的重要指导思想:
|
||||
|
||||
|
||||
|
||||
* 限界上下文是微服务的天然边界(一个限界上下文通常对应一个微服务),确保服务内模型一致,服务间通过接口或事件通信。
|
||||
|
||||
* 微服务的独立部署、扩展特性,与 DDD 隔离领域复杂度的目标一致。
|
||||
|
||||
### **六、DDD 的优势与挑战**
|
||||
|
||||
|
||||
|
||||
* **优势**:
|
||||
|
||||
|
||||
* 模型贴近业务,易理解和维护;
|
||||
|
||||
* 隔离复杂业务,提高系统扩展性;
|
||||
|
||||
* 减少技术与业务的沟通成本。
|
||||
|
||||
* **挑战**:
|
||||
|
||||
|
||||
* 学习成本高(概念多且抽象);
|
||||
|
||||
* 适合复杂业务,简单系统可能过度设计;
|
||||
|
||||
* 依赖团队与业务专家的深度协作,对团队能力要求高。
|
||||
|
||||
### **总结**
|
||||
|
||||
DDD 不是一套固定的工具或框架,而是一种 “以业务为中心” 的思维方式。它通过领域建模将复杂业务拆解为可管理的部分,帮助团队构建出既满足业务需求又具备技术灵活性的系统。对于业务复杂、长期演进的系统,DDD 是值得投入的设计方法。
|
||||
16
index.html
Normal file
16
index.html
Normal 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/app/main.ts"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
2903
package-lock.json
generated
Normal file
2903
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
35
package.json
Normal file
35
package.json
Normal file
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"name": "vue-project",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "run-p type-check \"build-only {@}\" --",
|
||||
"preview": "vite preview",
|
||||
"build-only": "vite build",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
BIN
public/favicon.ico
Normal file
BIN
public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
17
src/app/App.tsx
Normal file
17
src/app/App.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import { defineComponent } from 'vue'
|
||||
import { RouterLink, RouterView } from "vue-router";
|
||||
|
||||
export default defineComponent({
|
||||
name: 'App',
|
||||
setup() {
|
||||
return () => (
|
||||
<>
|
||||
<div id="nav">
|
||||
<RouterLink to="/">首页</RouterLink> |
|
||||
<RouterLink to="/about">我的</RouterLink>
|
||||
</div>
|
||||
<RouterView></RouterView>
|
||||
</>
|
||||
)
|
||||
}
|
||||
})
|
||||
86
src/app/assets/base.css
Normal file
86
src/app/assets/base.css
Normal file
@@ -0,0 +1,86 @@
|
||||
/* color palette from <https://github.com/vuejs/theme> */
|
||||
:root {
|
||||
--vt-c-white: #ffffff;
|
||||
--vt-c-white-soft: #f8f8f8;
|
||||
--vt-c-white-mute: #f2f2f2;
|
||||
|
||||
--vt-c-black: #181818;
|
||||
--vt-c-black-soft: #222222;
|
||||
--vt-c-black-mute: #282828;
|
||||
|
||||
--vt-c-indigo: #2c3e50;
|
||||
|
||||
--vt-c-divider-light-1: rgba(60, 60, 60, 0.29);
|
||||
--vt-c-divider-light-2: rgba(60, 60, 60, 0.12);
|
||||
--vt-c-divider-dark-1: rgba(84, 84, 84, 0.65);
|
||||
--vt-c-divider-dark-2: rgba(84, 84, 84, 0.48);
|
||||
|
||||
--vt-c-text-light-1: var(--vt-c-indigo);
|
||||
--vt-c-text-light-2: rgba(60, 60, 60, 0.66);
|
||||
--vt-c-text-dark-1: var(--vt-c-white);
|
||||
--vt-c-text-dark-2: rgba(235, 235, 235, 0.64);
|
||||
}
|
||||
|
||||
/* semantic color variables for this project */
|
||||
:root {
|
||||
--color-background: var(--vt-c-white);
|
||||
--color-background-soft: var(--vt-c-white-soft);
|
||||
--color-background-mute: var(--vt-c-white-mute);
|
||||
|
||||
--color-border: var(--vt-c-divider-light-2);
|
||||
--color-border-hover: var(--vt-c-divider-light-1);
|
||||
|
||||
--color-heading: var(--vt-c-text-light-1);
|
||||
--color-text: var(--vt-c-text-light-1);
|
||||
|
||||
--section-gap: 160px;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--color-background: var(--vt-c-black);
|
||||
--color-background-soft: var(--vt-c-black-soft);
|
||||
--color-background-mute: var(--vt-c-black-mute);
|
||||
|
||||
--color-border: var(--vt-c-divider-dark-2);
|
||||
--color-border-hover: var(--vt-c-divider-dark-1);
|
||||
|
||||
--color-heading: var(--vt-c-text-dark-1);
|
||||
--color-text: var(--vt-c-text-dark-2);
|
||||
}
|
||||
}
|
||||
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
body {
|
||||
min-height: 100vh;
|
||||
color: var(--color-text);
|
||||
background: var(--color-background);
|
||||
transition:
|
||||
color 0.5s,
|
||||
background-color 0.5s;
|
||||
line-height: 1.6;
|
||||
font-family:
|
||||
Inter,
|
||||
-apple-system,
|
||||
BlinkMacSystemFont,
|
||||
'Segoe UI',
|
||||
Roboto,
|
||||
Oxygen,
|
||||
Ubuntu,
|
||||
Cantarell,
|
||||
'Fira Sans',
|
||||
'Droid Sans',
|
||||
'Helvetica Neue',
|
||||
sans-serif;
|
||||
font-size: 15px;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
1
src/app/assets/logo.svg
Normal file
1
src/app/assets/logo.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 261.76 226.69"><path d="M161.096.001l-30.225 52.351L100.647.001H-.005l130.877 226.688L261.749.001z" fill="#41b883"/><path d="M161.096.001l-30.225 52.351L100.647.001H52.346l78.526 136.01L209.398.001z" fill="#34495e"/></svg>
|
||||
|
After Width: | Height: | Size: 276 B |
35
src/app/assets/main.css
Normal file
35
src/app/assets/main.css
Normal file
@@ -0,0 +1,35 @@
|
||||
@import './base.css';
|
||||
|
||||
#app {
|
||||
max-width: 1280px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
a,
|
||||
.green {
|
||||
text-decoration: none;
|
||||
color: hsla(160, 100%, 37%, 1);
|
||||
transition: 0.4s;
|
||||
padding: 3px;
|
||||
}
|
||||
|
||||
@media (hover: hover) {
|
||||
a:hover {
|
||||
background-color: hsla(160, 100%, 37%, 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
body {
|
||||
display: flex;
|
||||
place-items: center;
|
||||
}
|
||||
|
||||
#app {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
padding: 0 2rem;
|
||||
}
|
||||
}
|
||||
54
src/app/components/Header.tsx
Normal file
54
src/app/components/Header.tsx
Normal file
@@ -0,0 +1,54 @@
|
||||
import { defineComponent, defineEmits, onMounted} from 'vue'
|
||||
import type { JSX } from "vue/jsx-runtime";
|
||||
|
||||
export default defineComponent({
|
||||
name: "Header",
|
||||
props: {
|
||||
text: {
|
||||
type: Array<{ id: number, title: string }>,
|
||||
default: () => []
|
||||
}
|
||||
},
|
||||
emits: {
|
||||
sendMessage: (message: string) => typeof message === 'string'
|
||||
},
|
||||
|
||||
setup(props, { attrs, slots, emit, expose }) {
|
||||
|
||||
onMounted(() => {
|
||||
console.log("onMounted")
|
||||
})
|
||||
|
||||
const handleClick = () => {
|
||||
console.log("handleClick")
|
||||
emit('sendMessage', '子给父的数据')
|
||||
}
|
||||
|
||||
const button = (): JSX.Element => {
|
||||
return <button onClick={handleClick}>父向子emit点我</button>
|
||||
}
|
||||
|
||||
return () => (
|
||||
<div>
|
||||
<h1>头部组件</h1>
|
||||
|
||||
{ button() }
|
||||
|
||||
{ slots.default?.() }
|
||||
|
||||
{ slots.header?.() }
|
||||
|
||||
<div class="ScopedSlots">
|
||||
<p>作用于插槽:</p>
|
||||
<ul>
|
||||
{
|
||||
props.text.map(item => {
|
||||
return slots.scoped?.(item)
|
||||
})
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
})
|
||||
13
src/app/main.ts
Normal file
13
src/app/main.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
// import './assets/main.css'
|
||||
|
||||
import { createApp } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
|
||||
import App from './App'
|
||||
import router from './router'
|
||||
|
||||
createApp(App)
|
||||
.use(createPinia())
|
||||
.use(router)
|
||||
.mount('#app')
|
||||
|
||||
24
src/app/router/index.ts
Normal file
24
src/app/router/index.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
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
|
||||
12
src/app/stores/counter.ts
Normal file
12
src/app/stores/counter.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { ref, computed } from 'vue'
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
export const useCounterStore = defineStore('counter', () => {
|
||||
const count = ref(0)
|
||||
const doubleCount = computed(() => count.value * 2)
|
||||
function increment() {
|
||||
count.value++
|
||||
}
|
||||
|
||||
return { count, doubleCount, increment }
|
||||
})
|
||||
12
src/app/views/About.tsx
Normal file
12
src/app/views/About.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
import { defineComponent } from 'vue'
|
||||
|
||||
export default defineComponent({
|
||||
name: "About",
|
||||
setup() {
|
||||
return () => (
|
||||
<div class="about">
|
||||
<h1>我的</h1>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
})
|
||||
60
src/app/views/Home.tsx
Normal file
60
src/app/views/Home.tsx
Normal file
@@ -0,0 +1,60 @@
|
||||
import { defineComponent, reactive, onMounted } from 'vue'
|
||||
import Header from '@/components/Header'
|
||||
import { HttpConfig } from '@shared/config/index'
|
||||
import { UrlCheckUtils } from '@shared/utils'
|
||||
import { useCounterStore } from '@/stores/counter'
|
||||
import { storeToRefs } from 'pinia'
|
||||
|
||||
interface stateType {
|
||||
list: ListItemType[]
|
||||
}
|
||||
|
||||
interface ListItemType {
|
||||
id: number
|
||||
title: string
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
name: "Home",
|
||||
setup() {
|
||||
|
||||
const counterStore = useCounterStore()
|
||||
const { increment } = counterStore
|
||||
const { count, doubleCount } = storeToRefs(counterStore)
|
||||
|
||||
const state = reactive<stateType>({
|
||||
list: [
|
||||
{ id: 1, title: '新闻1' },
|
||||
{ id: 2, title: '新闻2' },
|
||||
{ id: 3, title: '新闻3' },
|
||||
]
|
||||
})
|
||||
|
||||
|
||||
|
||||
onMounted(() => {
|
||||
// console.log("onMounted: ", HttpConfig.DevBaseUrl);
|
||||
console.log("check: ", UrlCheckUtils.check())
|
||||
})
|
||||
|
||||
// 定义处理子组件事件的函数
|
||||
const handleChildEvent = (message: string) => {
|
||||
console.log('收到子组件事件:', message)
|
||||
}
|
||||
|
||||
|
||||
return () => (
|
||||
<div class="home">
|
||||
<Header text={state.list} onSendMessage={handleChildEvent}>
|
||||
{{
|
||||
scoped: (item: ListItemType) => <li key={item.id}>{item.title}</li>,
|
||||
header: () => <div class="header">父到子 具名 插槽</div>,
|
||||
default: () => <div class="default">父到子 default 插槽</div>
|
||||
}}
|
||||
</Header>
|
||||
<h1>首页</h1>
|
||||
<div class="count" onClick={increment}>点我+1:{count.value}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
})
|
||||
11
src/app/views/NotFound.tsx
Normal file
11
src/app/views/NotFound.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import { defineComponent } from 'vue'
|
||||
|
||||
export default defineComponent({
|
||||
setup() {
|
||||
return () => (
|
||||
<div class="404">
|
||||
<h1>404</h1>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
})
|
||||
0
src/features/car/index.ts
Normal file
0
src/features/car/index.ts
Normal file
0
src/features/order/index.ts
Normal file
0
src/features/order/index.ts
Normal file
5
src/shared/config/HttpConfig.ts
Normal file
5
src/shared/config/HttpConfig.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export default class HttpConfig {
|
||||
public static DevBaseUrl: string = "http://127.0.0.1:8080"
|
||||
public static prodBaseUrl: string = "http://127.0.0.1:9090/api"
|
||||
public static apiList: string = ""
|
||||
}
|
||||
4
src/shared/config/ThemeConfig.ts
Normal file
4
src/shared/config/ThemeConfig.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export default class ThemeConfig {
|
||||
public static primaryColor: string = '#1890ff'; // 主色调
|
||||
public static layoutType: 'side' | 'top' = 'side'; // 侧边栏布局
|
||||
}
|
||||
4
src/shared/config/index.ts
Normal file
4
src/shared/config/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import HttpConfig from './HttpConfig';
|
||||
import ThemeConfig from './ThemeConfig';
|
||||
|
||||
export { HttpConfig, ThemeConfig };
|
||||
93
src/shared/infra/AxiosInfra.ts
Normal file
93
src/shared/infra/AxiosInfra.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
import axios, { type AxiosInstance, AxiosError, type AxiosRequestConfig, type InternalAxiosRequestConfig } from "axios";
|
||||
import { UrlCheckUtils } from '@shared/utils/index'
|
||||
|
||||
|
||||
class AxiosInfra {
|
||||
public instance: AxiosInstance;
|
||||
|
||||
public constructor() {
|
||||
this.instance = axios.create({
|
||||
baseURL: UrlCheckUtils.check(),
|
||||
timeout: 10000,
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
});
|
||||
this.ResponseInterceptor();
|
||||
this.RequestInterceptor();
|
||||
}
|
||||
|
||||
public async get(params: AxiosRequestConfig): Promise<any> {
|
||||
return await this.instance.get(<string>params.url, {
|
||||
params: params.data,
|
||||
headers: Object.assign({ Authorization: localStorage.getItem("token") }, params.headers)
|
||||
});
|
||||
}
|
||||
|
||||
public async post(params: AxiosRequestConfig): Promise<any> {
|
||||
return await this.instance.post(<string>params.url, params.data, {
|
||||
headers: Object.assign({ Authorization: localStorage.getItem("token") }, params.headers)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Put 请求
|
||||
* 请求参数请参考接口:RequestParams
|
||||
* @param params
|
||||
*/
|
||||
public async put(params: AxiosRequestConfig): Promise<any> {
|
||||
return await this.instance.put(<string>params.url, params.data)
|
||||
}
|
||||
|
||||
/**
|
||||
* delete 请求
|
||||
* 请求参数请参考接口:RequestParams
|
||||
* @param params
|
||||
*/
|
||||
public async delete(params: AxiosRequestConfig): Promise<any> {
|
||||
return await this.instance.delete(<string>params.url, { params: params.data })
|
||||
}
|
||||
|
||||
/**
|
||||
* 响应拦截器
|
||||
* @constructor
|
||||
*/
|
||||
public async ResponseInterceptor(): Promise<any> {
|
||||
this.instance.interceptors.response.use(
|
||||
(response: any) => {
|
||||
return response.data.result;
|
||||
},
|
||||
(error: AxiosError) => {
|
||||
console.log(`
|
||||
❌ 请求错误 ❌
|
||||
1、基地址: ${error.config?.baseURL}
|
||||
2、短地址: ${error.config?.url}
|
||||
3、请求 方式: ${error.config?.method}
|
||||
4、请求 参数: ${JSON.stringify(error.config?.params)}
|
||||
5、请求 头 : ${JSON.stringify(error.config?.headers)}
|
||||
|
||||
------------------------------------------------------------
|
||||
|
||||
6、错误信息为: ${error.message}
|
||||
7、错误代码为: ${error.stack}
|
||||
`);
|
||||
return Promise.reject(error)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加请求拦截器
|
||||
* @constructor
|
||||
*/
|
||||
public async RequestInterceptor(): Promise<any> {
|
||||
this.instance.interceptors.request.use((config: any) => {
|
||||
return config
|
||||
}, function (error: AxiosError) {
|
||||
return Promise.reject(error)
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
export default new AxiosInfra();
|
||||
1
src/shared/types/env.d.ts
vendored
Normal file
1
src/shared/types/env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
4
src/shared/types/shim.d.ts
vendored
Normal file
4
src/shared/types/shim.d.ts
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
declare module '*.vue' {
|
||||
import Vue from 'vue';
|
||||
export default Vue;
|
||||
}
|
||||
12
src/shared/utils/UrlCheckUtils.ts
Normal file
12
src/shared/utils/UrlCheckUtils.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { HttpConfig } from '@shared/config/index'
|
||||
|
||||
export default class UrlCheckUtils {
|
||||
|
||||
public static check() {
|
||||
if (import.meta.env.MODE === 'production') {
|
||||
return HttpConfig.prodBaseUrl;
|
||||
} else {
|
||||
return HttpConfig.DevBaseUrl;
|
||||
}
|
||||
}
|
||||
}
|
||||
3
src/shared/utils/index.ts
Normal file
3
src/shared/utils/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import UrlCheckUtils from './UrlCheckUtils';
|
||||
|
||||
export { UrlCheckUtils };
|
||||
27
tsconfig.app.json
Normal file
27
tsconfig.app.json
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"extends": "@vue/tsconfig/tsconfig.dom.json",
|
||||
"include": [
|
||||
"src/shared/types/*.d.ts",
|
||||
"src/**/*.ts",
|
||||
"src/**/*.d.ts",
|
||||
"src/**/*",
|
||||
"src/**/*.tsx",
|
||||
"src/**/*.vue"
|
||||
],
|
||||
"exclude": [
|
||||
"src/**/__tests__/*"
|
||||
],
|
||||
"compilerOptions": {
|
||||
"jsx": "preserve",
|
||||
"jsxImportSource": "vue",
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"./src/app/*"
|
||||
],
|
||||
"@shared/*": [
|
||||
"./src/shared/*"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
11
tsconfig.json
Normal file
11
tsconfig.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.node.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.app.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
19
tsconfig.node.json
Normal file
19
tsconfig.node.json
Normal 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"]
|
||||
}
|
||||
}
|
||||
21
vite.config.ts
Normal file
21
vite.config.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { fileURLToPath, URL } from 'node:url'
|
||||
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import vueJsx from '@vitejs/plugin-vue-jsx'
|
||||
import vueDevTools from 'vite-plugin-vue-devtools'
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
vue(),
|
||||
vueJsx(),
|
||||
vueDevTools(),
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': fileURLToPath(new URL('./src/app/', import.meta.url)),
|
||||
'@shared': fileURLToPath(new URL('./src/shared/', import.meta.url))
|
||||
},
|
||||
},
|
||||
})
|
||||
Reference in New Issue
Block a user