youhua
This commit is contained in:
516
.agents/skills/clean-code/SKILL.md
Normal file
516
.agents/skills/clean-code/SKILL.md
Normal file
@@ -0,0 +1,516 @@
|
|||||||
|
---
|
||||||
|
name: clean-code
|
||||||
|
description: Clean Code 代码质量审查和重构指南。用于代码审查、重构、提升代码质量、减少技术债务。触发词:代码审查、clean code、重构、代码质量、技术债务、代码规范。
|
||||||
|
---
|
||||||
|
|
||||||
|
# Clean Code 技能
|
||||||
|
|
||||||
|
帮助编写高质量、可维护、可读性强的代码。
|
||||||
|
|
||||||
|
## 核心原则
|
||||||
|
|
||||||
|
### 1. 有意义的命名
|
||||||
|
```typescript
|
||||||
|
// ❌ 坏
|
||||||
|
const d = new Date();
|
||||||
|
const ymd = date.split('-');
|
||||||
|
|
||||||
|
// ✅ 好
|
||||||
|
const currentDate = new Date();
|
||||||
|
const [year, month, day] = date.split('-');
|
||||||
|
```
|
||||||
|
|
||||||
|
**规则:**
|
||||||
|
- 使用描述性名称,避免缩写
|
||||||
|
- 名称应该表达意图
|
||||||
|
- 避免误导性名称
|
||||||
|
- 做有意义的区分(不是 a1, a2)
|
||||||
|
- 使用可搜索的名称
|
||||||
|
- 类名用名词,方法名用动词
|
||||||
|
|
||||||
|
### 2. 函数
|
||||||
|
```typescript
|
||||||
|
// ❌ 坏 - 做太多事
|
||||||
|
function processUser(user: User) {
|
||||||
|
// 验证
|
||||||
|
if (!user.email) throw new Error('Email required');
|
||||||
|
if (!user.name) throw new Error('Name required');
|
||||||
|
|
||||||
|
// 保存
|
||||||
|
database.save(user);
|
||||||
|
|
||||||
|
// 发送邮件
|
||||||
|
emailService.send(user.email, 'Welcome!');
|
||||||
|
|
||||||
|
// 记录日志
|
||||||
|
logger.log(`User ${user.name} created`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ 好 - 单一职责
|
||||||
|
function validateUser(user: User): void {
|
||||||
|
if (!user.email) throw new Error('Email required');
|
||||||
|
if (!user.name) throw new Error('Name required');
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveUser(user: User): void {
|
||||||
|
database.save(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendWelcomeEmail(user: User): void {
|
||||||
|
emailService.send(user.email, 'Welcome!');
|
||||||
|
}
|
||||||
|
|
||||||
|
function logUserCreation(user: User): void {
|
||||||
|
logger.log(`User ${user.name} created`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function processUser(user: User) {
|
||||||
|
validateUser(user);
|
||||||
|
saveUser(user);
|
||||||
|
sendWelcomeEmail(user);
|
||||||
|
logUserCreation(user);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**规则:**
|
||||||
|
- 函数应该小(<20行)
|
||||||
|
- 只做一件事
|
||||||
|
- 每个函数一个抽象层级
|
||||||
|
- 使用描述性名称
|
||||||
|
- 函数参数越少越好(理想0-3个)
|
||||||
|
- 避免副作用
|
||||||
|
- 分隔指令与询问
|
||||||
|
|
||||||
|
### 3. 注释
|
||||||
|
```typescript
|
||||||
|
// ❌ 坏 - 多余的注释
|
||||||
|
// 检查用户是否已激活
|
||||||
|
if (user.isActive) { ... }
|
||||||
|
|
||||||
|
// ✅ 好 - 代码自解释
|
||||||
|
if (user.isActive) { ... }
|
||||||
|
|
||||||
|
// ✅ 好 - 解释为什么
|
||||||
|
// 使用 setTimeout 而不是 setInterval 避免重叠执行
|
||||||
|
setTimeout(processQueue, 1000);
|
||||||
|
```
|
||||||
|
|
||||||
|
**规则:**
|
||||||
|
- 好的代码是自解释的
|
||||||
|
- 注释不能弥补糟糕的代码
|
||||||
|
- 用代码表达意图
|
||||||
|
- 好的注释:法律信息、解释意图、警示、TODO
|
||||||
|
- 坏的注释:喃喃自语、多余的、误导的、注释掉的代码
|
||||||
|
|
||||||
|
### 4. 格式
|
||||||
|
```typescript
|
||||||
|
// ❌ 坏
|
||||||
|
export class User{constructor(private name:string,private age:number){}
|
||||||
|
getName(){return this.name;}}
|
||||||
|
|
||||||
|
// ✅ 好
|
||||||
|
export class User {
|
||||||
|
constructor(
|
||||||
|
private name: string,
|
||||||
|
private age: number
|
||||||
|
) {}
|
||||||
|
|
||||||
|
getName(): string {
|
||||||
|
return this.name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**规则:**
|
||||||
|
- 垂直格式:概念之间用空行分隔
|
||||||
|
- 水平格式:行宽<120字符
|
||||||
|
- 缩进:统一使用2或4空格
|
||||||
|
- 团队规则:遵循项目既定风格
|
||||||
|
|
||||||
|
### 5. 对象和数据结构
|
||||||
|
```typescript
|
||||||
|
// ❌ 坏 - 暴露内部结构
|
||||||
|
class User {
|
||||||
|
public name: string;
|
||||||
|
public age: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ 好 - 隐藏实现
|
||||||
|
class User {
|
||||||
|
private _name: string;
|
||||||
|
private _age: number;
|
||||||
|
|
||||||
|
get name(): string {
|
||||||
|
return this._name;
|
||||||
|
}
|
||||||
|
|
||||||
|
set age(value: number) {
|
||||||
|
if (value < 0) throw new Error('Invalid age');
|
||||||
|
this._age = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**规则:**
|
||||||
|
- 数据抽象:隐藏实现
|
||||||
|
- 数据、对象的反对称性
|
||||||
|
- 得墨忒耳定律:模块不应知道它操作对象的内部细节
|
||||||
|
|
||||||
|
### 6. 错误处理
|
||||||
|
```typescript
|
||||||
|
// ❌ 坏 - 返回 null
|
||||||
|
function getUser(id: string): User | null {
|
||||||
|
return database.find(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ 好 - 抛出异常
|
||||||
|
function getUser(id: string): User {
|
||||||
|
const user = database.find(id);
|
||||||
|
if (!user) throw new UserNotFoundError(id);
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ 好 - 使用 Special Case 模式
|
||||||
|
class NullUser implements User {
|
||||||
|
name = 'Guest';
|
||||||
|
age = 0;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**规则:**
|
||||||
|
- 使用异常而非返回码
|
||||||
|
- 先写 Try-Catch-Finally
|
||||||
|
- 给出异常的上下文
|
||||||
|
- 别返回 null 值
|
||||||
|
- 别传递 null 值
|
||||||
|
|
||||||
|
### 7. 边界
|
||||||
|
```typescript
|
||||||
|
// ❌ 坏 - 直接依赖第三方类
|
||||||
|
import { Map } from 'third-party-lib';
|
||||||
|
|
||||||
|
class UserCollection {
|
||||||
|
private users: Map<string, User>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ 好 - 使用适配器模式
|
||||||
|
interface UserMap {
|
||||||
|
get(key: string): User;
|
||||||
|
set(key: string, user: User): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
class ThirdPartyUserMap implements UserMap {
|
||||||
|
private map: Map<string, User>;
|
||||||
|
|
||||||
|
get(key: string): User {
|
||||||
|
return this.map.get(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
set(key: string, user: User): void {
|
||||||
|
this.map.set(key, user);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**规则:**
|
||||||
|
- 隐藏第三方代码
|
||||||
|
- 使用适配器模式
|
||||||
|
- 边界处的代码需要清晰的分割和测试
|
||||||
|
|
||||||
|
### 8. 单元测试
|
||||||
|
```typescript
|
||||||
|
// ❌ 坏 - 不清晰的测试
|
||||||
|
test('user', () => {
|
||||||
|
const u = new User('John', 25);
|
||||||
|
expect(u.getName()).toBe('John');
|
||||||
|
expect(u.getAge()).toBe(25);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ✅ 好 - BUILD-OPERATE-CHECK 模式
|
||||||
|
test('shouldReturnUserName', () => {
|
||||||
|
// BUILD
|
||||||
|
const user = new User('John', 25);
|
||||||
|
|
||||||
|
// OPERATE
|
||||||
|
const name = user.getName();
|
||||||
|
|
||||||
|
// CHECK
|
||||||
|
expect(name).toBe('John');
|
||||||
|
});
|
||||||
|
|
||||||
|
// ✅ 好 - Given-When-Then 模式
|
||||||
|
test('shouldReturnUserName', () => {
|
||||||
|
// Given
|
||||||
|
const user = new User('John', 25);
|
||||||
|
|
||||||
|
// When
|
||||||
|
const name = user.getName();
|
||||||
|
|
||||||
|
// Then
|
||||||
|
expect(name).toBe('John');
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
**规则:**
|
||||||
|
- F.I.R.S.T 原则:
|
||||||
|
- Fast(快速)
|
||||||
|
- Independent(独立)
|
||||||
|
- Repeatable(可重复)
|
||||||
|
- Self-Validating(自验证)
|
||||||
|
- Timely(及时)
|
||||||
|
- 每个测试一个断言
|
||||||
|
- 单一概念
|
||||||
|
- 测试代码和生产代码一样重要
|
||||||
|
|
||||||
|
### 9. 类
|
||||||
|
```typescript
|
||||||
|
// ❌ 坏 - 大类
|
||||||
|
class UserManager {
|
||||||
|
createUser() {}
|
||||||
|
deleteUser() {}
|
||||||
|
updateUser() {}
|
||||||
|
sendEmail() {}
|
||||||
|
validateEmail() {}
|
||||||
|
generateReport() {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ 好 - 单一职责
|
||||||
|
class UserService {
|
||||||
|
create(user: User) {}
|
||||||
|
delete(id: string) {}
|
||||||
|
update(user: User) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
class EmailService {
|
||||||
|
send(email: string, content: string) {}
|
||||||
|
validate(email: string): boolean {}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ReportService {
|
||||||
|
generate(userId: string): Report {}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**规则:**
|
||||||
|
- 类应该小
|
||||||
|
- 单一职责原则(SRP)
|
||||||
|
- 内聚性:方法和数据互相依赖
|
||||||
|
- 保持内聚性就会得到许多短小的类
|
||||||
|
|
||||||
|
### 10. 系统
|
||||||
|
```typescript
|
||||||
|
// ❌ 坏 - 硬编码依赖
|
||||||
|
class UserService {
|
||||||
|
private db = new Database(); // 硬编码
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ 好 - 依赖注入
|
||||||
|
class UserService {
|
||||||
|
constructor(private db: Database) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ 好 - 工厂模式
|
||||||
|
class ServiceFactory {
|
||||||
|
static createUserService(): UserService {
|
||||||
|
return new UserService(new Database());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**规则:**
|
||||||
|
- 分离构造和使用
|
||||||
|
- 依赖注入
|
||||||
|
- 扩充:AOP(面向切面编程)
|
||||||
|
- 测试驱动系统架构
|
||||||
|
|
||||||
|
## 代码审查清单
|
||||||
|
|
||||||
|
### 命名
|
||||||
|
- [ ] 变量名是否描述了其用途?
|
||||||
|
- [ ] 函数名是否描述了其行为?
|
||||||
|
- [ ] 类名是否描述了其职责?
|
||||||
|
- [ ] 名称是否一致?
|
||||||
|
|
||||||
|
### 函数
|
||||||
|
- [ ] 函数是否小于20行?
|
||||||
|
- [ ] 函数是否只做一件事?
|
||||||
|
- [ ] 函数参数是否<=3个?
|
||||||
|
- [ ] 函数是否有副作用?
|
||||||
|
- [ ] 函数名是否描述性?
|
||||||
|
|
||||||
|
### 结构
|
||||||
|
- [ ] 代码是否有清晰的层次结构?
|
||||||
|
- [ ] 类是否遵循单一职责原则?
|
||||||
|
- [ ] 是否有重复代码?
|
||||||
|
- [ ] 依赖是否清晰?
|
||||||
|
|
||||||
|
### 测试
|
||||||
|
- [ ] 是否有足够的测试覆盖?
|
||||||
|
- [ ] 测试是否快速?
|
||||||
|
- [ ] 测试是否独立?
|
||||||
|
- [ ] 测试是否清晰?
|
||||||
|
|
||||||
|
### 错误处理
|
||||||
|
- [ ] 是否处理了所有可能的错误?
|
||||||
|
- [ ] 错误信息是否清晰?
|
||||||
|
- [ ] 是否避免了返回 null?
|
||||||
|
|
||||||
|
## 重构技巧
|
||||||
|
|
||||||
|
### 提取方法
|
||||||
|
```typescript
|
||||||
|
// Before
|
||||||
|
function printOwing(invoice: Invoice) {
|
||||||
|
let outstanding = 0;
|
||||||
|
|
||||||
|
// 打印横幅
|
||||||
|
console.log('***********************');
|
||||||
|
console.log('*** Customer Owes ***');
|
||||||
|
console.log('***********************');
|
||||||
|
|
||||||
|
// 计算未付款
|
||||||
|
for (const order of invoice.orders) {
|
||||||
|
outstanding += order.amount;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 打印详情
|
||||||
|
console.log(`name: ${invoice.customer}`);
|
||||||
|
console.log(`amount: ${outstanding}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// After
|
||||||
|
function printOwing(invoice: Invoice) {
|
||||||
|
printBanner();
|
||||||
|
const outstanding = calculateOutstanding(invoice);
|
||||||
|
printDetails(invoice, outstanding);
|
||||||
|
}
|
||||||
|
|
||||||
|
function printBanner() {
|
||||||
|
console.log('***********************');
|
||||||
|
console.log('*** Customer Owes ***');
|
||||||
|
console.log('***********************');
|
||||||
|
}
|
||||||
|
|
||||||
|
function calculateOutstanding(invoice: Invoice): number {
|
||||||
|
return invoice.orders.reduce((sum, order) => sum + order.amount, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
function printDetails(invoice: Invoice, outstanding: number) {
|
||||||
|
console.log(`name: ${invoice.customer}`);
|
||||||
|
console.log(`amount: ${outstanding}`);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 内联方法
|
||||||
|
```typescript
|
||||||
|
// Before
|
||||||
|
function getRating(driver: Driver): number {
|
||||||
|
return moreThanFiveLateDeliveries(driver) ? 2 : 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
function moreThanFiveLateDeliveries(driver: Driver): boolean {
|
||||||
|
return driver.numberOfLateDeliveries > 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
// After
|
||||||
|
function getRating(driver: Driver): number {
|
||||||
|
return driver.numberOfLateDeliveries > 5 ? 2 : 1;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 提取变量
|
||||||
|
```typescript
|
||||||
|
// Before
|
||||||
|
if (platform.toUpperCase().indexOf('MAC') > -1 &&
|
||||||
|
browser.toUpperCase().indexOf('IE') > -1 &&
|
||||||
|
wasInitialized() && resize > 0) {
|
||||||
|
// do something
|
||||||
|
}
|
||||||
|
|
||||||
|
// After
|
||||||
|
const isMacOs = platform.toUpperCase().indexOf('MAC') > -1;
|
||||||
|
const isIE = browser.toUpperCase().indexOf('IE') > -1;
|
||||||
|
const wasResized = resize > 0;
|
||||||
|
|
||||||
|
if (isMacOs && isIE && wasInitialized() && wasResized) {
|
||||||
|
// do something
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 分解条件
|
||||||
|
```typescript
|
||||||
|
// Before
|
||||||
|
if (date.before(SUMMER_START) || date.after(SUMMER_END)) {
|
||||||
|
charge = quantity * winterRate + winterServiceCharge;
|
||||||
|
} else {
|
||||||
|
charge = quantity * summerRate;
|
||||||
|
}
|
||||||
|
|
||||||
|
// After
|
||||||
|
if (isSummer(date)) {
|
||||||
|
charge = summerCharge(quantity);
|
||||||
|
} else {
|
||||||
|
charge = winterCharge(quantity);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isSummer(date: Date): boolean {
|
||||||
|
return !date.before(SUMMER_START) && !date.after(SUMMER_END);
|
||||||
|
}
|
||||||
|
|
||||||
|
function summerCharge(quantity: number): number {
|
||||||
|
return quantity * summerRate;
|
||||||
|
}
|
||||||
|
|
||||||
|
function winterCharge(quantity: number): number {
|
||||||
|
return quantity * winterRate + winterServiceCharge;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 以多态取代条件
|
||||||
|
```typescript
|
||||||
|
// Before
|
||||||
|
function calculatePay(employee: Employee): number {
|
||||||
|
switch (employee.type) {
|
||||||
|
case 'ENGINEER':
|
||||||
|
return employee.monthlySalary;
|
||||||
|
case 'SALESMAN':
|
||||||
|
return employee.monthlySalary + employee.commission;
|
||||||
|
case 'MANAGER':
|
||||||
|
return employee.monthlySalary + employee.bonus;
|
||||||
|
default:
|
||||||
|
throw new Error('Invalid employee type');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// After
|
||||||
|
abstract class Employee {
|
||||||
|
abstract calculatePay(): number;
|
||||||
|
}
|
||||||
|
|
||||||
|
class Engineer extends Employee {
|
||||||
|
calculatePay(): number {
|
||||||
|
return this.monthlySalary;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Salesman extends Employee {
|
||||||
|
calculatePay(): number {
|
||||||
|
return this.monthlySalary + this.commission;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Manager extends Employee {
|
||||||
|
calculatePay(): number {
|
||||||
|
return this.monthlySalary + this.bonus;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 使用方法
|
||||||
|
|
||||||
|
1. **代码审查时**:参考清单逐项检查
|
||||||
|
2. **重构时**:应用重构技巧
|
||||||
|
3. **新功能开发时**:遵循核心原则
|
||||||
|
4. **代码坏味道识别**:参考常见问题
|
||||||
|
|
||||||
|
告诉我需要审查的代码,我会帮你识别问题并提供改进建议!
|
||||||
160
.agents/skills/ralph-loop/SKILL.md
Normal file
160
.agents/skills/ralph-loop/SKILL.md
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
---
|
||||||
|
name: ralph-loop
|
||||||
|
description: Ralph Loop 自动化编码循环 - 让 AI 代理持续工作直到完成任务。支持 PLANNING(规划)和 BUILDING(构建)两种模式,使用 exec + process 工具监控进度。
|
||||||
|
allowed-tools: Read, Write, Edit, Exec, Process
|
||||||
|
---
|
||||||
|
|
||||||
|
# Ralph Loop (Agent Mode)
|
||||||
|
|
||||||
|
自动化 AI 编码代理循环工作流。
|
||||||
|
|
||||||
|
## 工作流程
|
||||||
|
|
||||||
|
```
|
||||||
|
需求定义 → PLANNING Loop → BUILDING Loop → 完成
|
||||||
|
```
|
||||||
|
|
||||||
|
1. **PLANNING Loop** - 创建/更新 IMPLEMENTATION_PLAN.md(不实现)
|
||||||
|
2. **BUILDING Loop** - 实现任务、运行测试、更新计划、提交
|
||||||
|
|
||||||
|
## 支持的 CLI
|
||||||
|
|
||||||
|
| CLI | 命令模式 | TTY 需求 |
|
||||||
|
|-----|---------|---------|
|
||||||
|
| OpenCode | `opencode run --model <MODEL> "$(cat PROMPT.md)"` | ✅ |
|
||||||
|
| Codex | `codex exec --full-auto "$(cat PROMPT.md)"` | ✅ |
|
||||||
|
| Claude Code | `claude --dangerously-skip-permissions "$(cat PROMPT.md)"` | ✅ |
|
||||||
|
| Pi | `pi --provider <PROVIDER> -p "$(cat PROMPT.md)"` | ✅ |
|
||||||
|
| Goose | `goose run "$(cat PROMPT.md)"` | ✅ |
|
||||||
|
|
||||||
|
## 使用方法
|
||||||
|
|
||||||
|
### 1. 准备项目文件
|
||||||
|
|
||||||
|
创建 `PROMPT.md`:
|
||||||
|
```markdown
|
||||||
|
# 目标
|
||||||
|
<你的任务描述>
|
||||||
|
|
||||||
|
## 参考文件
|
||||||
|
- specs/*.md
|
||||||
|
- IMPLEMENTATION_PLAN.md
|
||||||
|
```
|
||||||
|
|
||||||
|
创建 `AGENTS.md`:
|
||||||
|
```markdown
|
||||||
|
# 项目说明
|
||||||
|
|
||||||
|
## 测试命令
|
||||||
|
npm test
|
||||||
|
|
||||||
|
## 构建命令
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 启动循环
|
||||||
|
|
||||||
|
**PLANNING 模式**:
|
||||||
|
```bash
|
||||||
|
# 我会执行
|
||||||
|
exec(
|
||||||
|
command: 'opencode run --model claude-opus-4 "$(cat PROMPT.md)"',
|
||||||
|
workdir: "/path/to/project",
|
||||||
|
background: true,
|
||||||
|
pty: true,
|
||||||
|
timeout: 3600
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
**BUILDING 模式**:
|
||||||
|
```bash
|
||||||
|
# 同上,但 PROMPT.md 内容不同
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 监控进度
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 轮询状态
|
||||||
|
process(action: "poll", sessionId: "xxx")
|
||||||
|
|
||||||
|
# 查看日志
|
||||||
|
process(action: "log", sessionId: "xxx", offset: -30)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 检测完成
|
||||||
|
|
||||||
|
检查 `IMPLEMENTATION_PLAN.md`:
|
||||||
|
- `STATUS: PLANNING_COMPLETE` - 规划完成
|
||||||
|
- `STATUS: COMPLETE` - 构建完成
|
||||||
|
|
||||||
|
## 提示词模板
|
||||||
|
|
||||||
|
### PLANNING 模式
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
You are running a Ralph PLANNING loop for this goal: <goal>.
|
||||||
|
|
||||||
|
Read specs/* and the current codebase. Only update IMPLEMENTATION_PLAN.md.
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
- Do not implement
|
||||||
|
- Do not commit
|
||||||
|
- Create a prioritized task list
|
||||||
|
- Write down questions if unclear
|
||||||
|
|
||||||
|
Completion:
|
||||||
|
When plan is ready, add: STATUS: PLANNING_COMPLETE
|
||||||
|
```
|
||||||
|
|
||||||
|
### BUILDING 模式
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
You are running a Ralph BUILDING loop for this goal: <goal>.
|
||||||
|
|
||||||
|
Context: specs/*, IMPLEMENTATION_PLAN.md, AGENTS.md
|
||||||
|
|
||||||
|
Tasks:
|
||||||
|
1) Pick the most important task
|
||||||
|
2) Investigate code
|
||||||
|
3) Implement
|
||||||
|
4) Run backpressure commands from AGENTS.md
|
||||||
|
5) Update IMPLEMENTATION_PLAN.md
|
||||||
|
6) Update AGENTS.md with learnings
|
||||||
|
7) Commit with clear message
|
||||||
|
|
||||||
|
Completion:
|
||||||
|
When all done, add: STATUS: COMPLETE
|
||||||
|
```
|
||||||
|
|
||||||
|
## 安全注意事项
|
||||||
|
|
||||||
|
- 使用沙盒环境
|
||||||
|
- 设置合理超时
|
||||||
|
- 重要项目先备份
|
||||||
|
- 监控进度,不要过早终止
|
||||||
|
|
||||||
|
## 故障排除
|
||||||
|
|
||||||
|
| 问题 | 解决方案 |
|
||||||
|
|------|---------|
|
||||||
|
| CLI 卡住 | 确保 pty: true |
|
||||||
|
| 无法启动 | 检查 CLI 路径和 git 仓库 |
|
||||||
|
| 未检测到完成 | 验证 IMPLEMENTATION_PLAN.md 中的格式 |
|
||||||
|
| 超时 | 增加 timeout 参数 |
|
||||||
|
|
||||||
|
## 示例
|
||||||
|
|
||||||
|
### 完整工作流
|
||||||
|
|
||||||
|
1. 用户: "帮我实现用户认证模块"
|
||||||
|
2. 我创建 specs/auth.md, PROMPT.md, AGENTS.md
|
||||||
|
3. 我启动 PLANNING 循环
|
||||||
|
4. AI 生成 IMPLEMENTATION_PLAN.md
|
||||||
|
5. 我启动 BUILDING 循环
|
||||||
|
6. AI 逐个实现任务
|
||||||
|
7. 检测到 STATUS: COMPLETE
|
||||||
|
8. 完成!
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT
|
||||||
74
flutter_monisuo/lib/core/network/api_response.dart
Normal file
74
flutter_monisuo/lib/core/network/api_response.dart
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
/// API 响应状态码
|
||||||
|
class ResponseCode {
|
||||||
|
static const String success = '0000';
|
||||||
|
static const String unauthorized = '0002';
|
||||||
|
}
|
||||||
|
|
||||||
|
/// API 响应模型
|
||||||
|
class ApiResponse<T> {
|
||||||
|
final bool success;
|
||||||
|
final String? message;
|
||||||
|
final T? data;
|
||||||
|
final String? code;
|
||||||
|
|
||||||
|
ApiResponse({
|
||||||
|
required this.success,
|
||||||
|
this.message,
|
||||||
|
this.data,
|
||||||
|
this.code,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory ApiResponse.success(T data, [String? message]) {
|
||||||
|
return ApiResponse(
|
||||||
|
success: true,
|
||||||
|
data: data,
|
||||||
|
message: message,
|
||||||
|
code: ResponseCode.success,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
factory ApiResponse.fail(String message, [String? code]) {
|
||||||
|
return ApiResponse(
|
||||||
|
success: false,
|
||||||
|
message: message,
|
||||||
|
code: code,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
factory ApiResponse.unauthorized(String message) {
|
||||||
|
return ApiResponse(
|
||||||
|
success: false,
|
||||||
|
message: message,
|
||||||
|
code: ResponseCode.unauthorized,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
factory ApiResponse.fromJson(
|
||||||
|
Map<String, dynamic> json,
|
||||||
|
T Function(dynamic)? fromJsonT,
|
||||||
|
) {
|
||||||
|
final code = json['code'] as String? ?? '';
|
||||||
|
final msg = json['msg'] as String? ?? '';
|
||||||
|
|
||||||
|
return switch (code) {
|
||||||
|
ResponseCode.success => _parseSuccess(json, msg, fromJsonT),
|
||||||
|
ResponseCode.unauthorized => ApiResponse.unauthorized(msg),
|
||||||
|
_ => ApiResponse.fail(msg, code),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static ApiResponse<T> _parseSuccess<T>(
|
||||||
|
Map<String, dynamic> json,
|
||||||
|
String msg,
|
||||||
|
T Function(dynamic)? fromJsonT,
|
||||||
|
) {
|
||||||
|
final data = json['data'];
|
||||||
|
if (fromJsonT != null && data != null) {
|
||||||
|
return ApiResponse.success(fromJsonT(data), msg);
|
||||||
|
}
|
||||||
|
return ApiResponse.success(data as T, msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool get isSuccess => success;
|
||||||
|
bool get isUnauthorized => code == ResponseCode.unauthorized;
|
||||||
|
}
|
||||||
@@ -1,65 +1,13 @@
|
|||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
import '../storage/local_storage.dart';
|
import '../storage/local_storage.dart';
|
||||||
import 'api_exception.dart';
|
import 'api_exception.dart';
|
||||||
|
import 'api_response.dart';
|
||||||
|
|
||||||
/// API 响应模型
|
/// 网络配置常量
|
||||||
class ApiResponse<T> {
|
class NetworkConfig {
|
||||||
final bool success;
|
static const String baseUrl = 'http://localhost:5010';
|
||||||
final String? message;
|
static const Duration connectTimeout = Duration(seconds: 30);
|
||||||
final T? data;
|
static const Duration receiveTimeout = Duration(seconds: 30);
|
||||||
final String? code;
|
|
||||||
|
|
||||||
ApiResponse({
|
|
||||||
required this.success,
|
|
||||||
this.message,
|
|
||||||
this.data,
|
|
||||||
this.code,
|
|
||||||
});
|
|
||||||
|
|
||||||
factory ApiResponse.success(T data, [String? message]) {
|
|
||||||
return ApiResponse(
|
|
||||||
success: true,
|
|
||||||
data: data,
|
|
||||||
message: message,
|
|
||||||
code: '0000',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
factory ApiResponse.fail(String message, [String? code]) {
|
|
||||||
return ApiResponse(
|
|
||||||
success: false,
|
|
||||||
message: message,
|
|
||||||
code: code,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
factory ApiResponse.unauthorized(String message) {
|
|
||||||
return ApiResponse(
|
|
||||||
success: false,
|
|
||||||
message: message,
|
|
||||||
code: '0002',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
factory ApiResponse.fromJson(
|
|
||||||
Map<String, dynamic> json,
|
|
||||||
T Function(dynamic)? fromJsonT,
|
|
||||||
) {
|
|
||||||
final code = json['code'] as String? ?? '';
|
|
||||||
final msg = json['msg'] as String? ?? '';
|
|
||||||
|
|
||||||
if (code == '0000') {
|
|
||||||
final data = json['data'];
|
|
||||||
if (fromJsonT != null && data != null) {
|
|
||||||
return ApiResponse.success(fromJsonT(data), msg);
|
|
||||||
}
|
|
||||||
return ApiResponse.success(data as T, msg);
|
|
||||||
} else if (code == '0002') {
|
|
||||||
return ApiResponse.unauthorized(msg);
|
|
||||||
} else {
|
|
||||||
return ApiResponse.fail(msg, code);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Dio 网络客户端
|
/// Dio 网络客户端
|
||||||
@@ -67,23 +15,30 @@ class DioClient {
|
|||||||
late final Dio _dio;
|
late final Dio _dio;
|
||||||
|
|
||||||
DioClient() {
|
DioClient() {
|
||||||
_dio = Dio(BaseOptions(
|
_dio = _createDio();
|
||||||
baseUrl: 'http://localhost:5010',
|
_setupInterceptors();
|
||||||
connectTimeout: const Duration(seconds: 30),
|
}
|
||||||
receiveTimeout: const Duration(seconds: 30),
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
));
|
|
||||||
|
|
||||||
_dio.interceptors.add(_AuthInterceptor());
|
Dio _createDio() {
|
||||||
_dio.interceptors.add(LogInterceptor(
|
return Dio(BaseOptions(
|
||||||
requestHeader: false,
|
baseUrl: NetworkConfig.baseUrl,
|
||||||
responseHeader: false,
|
connectTimeout: NetworkConfig.connectTimeout,
|
||||||
error: true,
|
receiveTimeout: NetworkConfig.receiveTimeout,
|
||||||
|
headers: {'Content-Type': 'application/json'},
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _setupInterceptors() {
|
||||||
|
_dio.interceptors.addAll([
|
||||||
|
_AuthInterceptor(),
|
||||||
|
LogInterceptor(
|
||||||
|
requestHeader: false,
|
||||||
|
responseHeader: false,
|
||||||
|
error: true,
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
/// GET 请求
|
/// GET 请求
|
||||||
Future<ApiResponse<T>> get<T>(
|
Future<ApiResponse<T>> get<T>(
|
||||||
String path, {
|
String path, {
|
||||||
@@ -112,7 +67,6 @@ class DioClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 处理响应
|
|
||||||
ApiResponse<T> _handleResponse<T>(
|
ApiResponse<T> _handleResponse<T>(
|
||||||
Response response,
|
Response response,
|
||||||
T Function(dynamic)? fromJson,
|
T Function(dynamic)? fromJson,
|
||||||
@@ -124,13 +78,39 @@ class DioClient {
|
|||||||
return ApiResponse.fail('响应数据格式错误');
|
return ApiResponse.fail('响应数据格式错误');
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 处理错误
|
|
||||||
ApiResponse<T> _handleError<T>(DioException e) {
|
ApiResponse<T> _handleError<T>(DioException e) {
|
||||||
if (e.response?.statusCode == 401) {
|
if (_isUnauthorized(e)) {
|
||||||
LocalStorage.clearUserData();
|
_clearUserData();
|
||||||
return ApiResponse.unauthorized('登录已过期,请重新登录');
|
return ApiResponse.unauthorized('登录已过期,请重新登录');
|
||||||
}
|
}
|
||||||
return ApiResponse.fail(e.message ?? '网络请求失败');
|
|
||||||
|
final message = _getErrorMessage(e);
|
||||||
|
return ApiResponse.fail(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _isUnauthorized(DioException e) {
|
||||||
|
return e.response?.statusCode == 401;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _clearUserData() {
|
||||||
|
LocalStorage.clearUserData();
|
||||||
|
}
|
||||||
|
|
||||||
|
String _getErrorMessage(DioException e) {
|
||||||
|
switch (e.type) {
|
||||||
|
case DioExceptionType.connectionTimeout:
|
||||||
|
return '连接超时,请检查网络';
|
||||||
|
case DioExceptionType.sendTimeout:
|
||||||
|
return '发送超时,请重试';
|
||||||
|
case DioExceptionType.receiveTimeout:
|
||||||
|
return '响应超时,请重试';
|
||||||
|
case DioExceptionType.connectionError:
|
||||||
|
return '网络连接失败';
|
||||||
|
case DioExceptionType.badResponse:
|
||||||
|
return '服务器错误 (${e.response?.statusCode})';
|
||||||
|
default:
|
||||||
|
return e.message ?? '网络请求失败';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -139,7 +119,7 @@ class _AuthInterceptor extends Interceptor {
|
|||||||
@override
|
@override
|
||||||
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
|
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
|
||||||
final token = LocalStorage.getToken();
|
final token = LocalStorage.getToken();
|
||||||
if (token != null && token.isNotEmpty) {
|
if (token?.isNotEmpty == true) {
|
||||||
options.headers['Authorization'] = 'Bearer $token';
|
options.headers['Authorization'] = 'Bearer $token';
|
||||||
}
|
}
|
||||||
super.onRequest(options, handler);
|
super.onRequest(options, handler);
|
||||||
|
|||||||
@@ -19,12 +19,9 @@ import 'ui/pages/main/main_page.dart';
|
|||||||
|
|
||||||
void main() async {
|
void main() async {
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
// 禁用 Provider 类型检查
|
|
||||||
Provider.debugCheckInvalidValueType = null;
|
Provider.debugCheckInvalidValueType = null;
|
||||||
|
|
||||||
// 初始化本地存储
|
await SharedPreferences.getInstance();
|
||||||
final prefs = await SharedPreferences.getInstance();
|
|
||||||
await LocalStorage.init();
|
await LocalStorage.init();
|
||||||
|
|
||||||
runApp(const MyApp());
|
runApp(const MyApp());
|
||||||
@@ -36,84 +33,117 @@ class MyApp extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return MultiProvider(
|
return MultiProvider(
|
||||||
providers: [
|
providers: _buildProviders(),
|
||||||
// 服务
|
child: AuthNavigator(
|
||||||
Provider<DioClient>(create: (_) => DioClient()),
|
child: ShadApp.custom(
|
||||||
ProxyProvider<DioClient, UserService>(
|
themeMode: ThemeMode.dark,
|
||||||
create: (_) => UserService(DioClient()),
|
darkTheme: ShadThemeData(
|
||||||
update: (_, client, previous) => previous ?? UserService(client),
|
brightness: Brightness.dark,
|
||||||
|
colorScheme: const ShadSlateColorScheme.dark(),
|
||||||
|
),
|
||||||
|
appBuilder: _buildMaterialApp,
|
||||||
),
|
),
|
||||||
ProxyProvider<DioClient, MarketService>(
|
|
||||||
create: (_) => MarketService(DioClient()),
|
|
||||||
update: (_, client, previous) => previous ?? MarketService(client),
|
|
||||||
),
|
|
||||||
ProxyProvider<DioClient, TradeService>(
|
|
||||||
create: (_) => TradeService(DioClient()),
|
|
||||||
update: (_, client, previous) => previous ?? TradeService(client),
|
|
||||||
),
|
|
||||||
ProxyProvider<DioClient, AssetService>(
|
|
||||||
create: (_) => AssetService(DioClient()),
|
|
||||||
update: (_, client, previous) => previous ?? AssetService(client),
|
|
||||||
),
|
|
||||||
ProxyProvider<DioClient, FundService>(
|
|
||||||
create: (_) => FundService(DioClient()),
|
|
||||||
update: (_, client, previous) => previous ?? FundService(client),
|
|
||||||
),
|
|
||||||
// 状态管理
|
|
||||||
ProxyProvider2<UserService, DioClient, AuthProvider>(
|
|
||||||
create: (_) => AuthProvider(UserService(DioClient())),
|
|
||||||
update: (_, userService, __, previous) =>
|
|
||||||
previous ?? AuthProvider(userService),
|
|
||||||
),
|
|
||||||
ProxyProvider<MarketService, MarketProvider>(
|
|
||||||
create: (_) => MarketProvider(MarketService(DioClient())),
|
|
||||||
update: (_, service, previous) =>
|
|
||||||
previous ?? MarketProvider(service),
|
|
||||||
),
|
|
||||||
ProxyProvider2<AssetService, FundService, AssetProvider>(
|
|
||||||
create: (_) =>
|
|
||||||
AssetProvider(AssetService(DioClient()), FundService(DioClient())),
|
|
||||||
update: (_, assetService, fundService, previous) =>
|
|
||||||
previous ?? AssetProvider(assetService, fundService),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
child: ShadApp.custom(
|
|
||||||
themeMode: ThemeMode.dark,
|
|
||||||
darkTheme: ShadThemeData(
|
|
||||||
brightness: Brightness.dark,
|
|
||||||
colorScheme: const ShadSlateColorScheme.dark(),
|
|
||||||
),
|
|
||||||
appBuilder: (context) {
|
|
||||||
return MaterialApp(
|
|
||||||
debugShowCheckedModeBanner: false,
|
|
||||||
theme: Theme.of(context),
|
|
||||||
localizationsDelegates: const [
|
|
||||||
GlobalShadLocalizations.delegate,
|
|
||||||
GlobalMaterialLocalizations.delegate,
|
|
||||||
GlobalCupertinoLocalizations.delegate,
|
|
||||||
GlobalWidgetsLocalizations.delegate,
|
|
||||||
],
|
|
||||||
builder: (context, child) {
|
|
||||||
return ShadAppBuilder(child: child!);
|
|
||||||
},
|
|
||||||
home: Consumer<AuthProvider>(
|
|
||||||
builder: (context, auth, _) {
|
|
||||||
if (auth.isLoading) {
|
|
||||||
return const Scaffold(
|
|
||||||
body: Center(
|
|
||||||
child: CircularProgressIndicator(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (auth.isLoggedIn) {
|
|
||||||
return const MainPage();
|
|
||||||
}
|
|
||||||
return const LoginPage();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
List<SingleChildWidget> _buildProviders() {
|
||||||
|
final dioClient = DioClient();
|
||||||
|
|
||||||
|
return [
|
||||||
|
// Services
|
||||||
|
Provider<DioClient>.value(value: dioClient),
|
||||||
|
Provider<UserService>(create: (_) => UserService(dioClient)),
|
||||||
|
Provider<MarketService>(create: (_) => MarketService(dioClient)),
|
||||||
|
Provider<TradeService>(create: (_) => TradeService(dioClient)),
|
||||||
|
Provider<AssetService>(create: (_) => AssetService(dioClient)),
|
||||||
|
Provider<FundService>(create: (_) => FundService(dioClient)),
|
||||||
|
// State Management
|
||||||
|
ChangeNotifierProvider<AuthProvider>(
|
||||||
|
create: (ctx) => AuthProvider(ctx.read<UserService>()),
|
||||||
|
),
|
||||||
|
ChangeNotifierProvider<MarketProvider>(
|
||||||
|
create: (ctx) => MarketProvider(ctx.read<MarketService>()),
|
||||||
|
),
|
||||||
|
ChangeNotifierProvider<AssetProvider>(
|
||||||
|
create: (ctx) => AssetProvider(
|
||||||
|
ctx.read<AssetService>(),
|
||||||
|
ctx.read<FundService>(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildMaterialApp(BuildContext context) {
|
||||||
|
return MaterialApp(
|
||||||
|
debugShowCheckedModeBanner: false,
|
||||||
|
theme: Theme.of(context),
|
||||||
|
localizationsDelegates: const [
|
||||||
|
GlobalShadLocalizations.delegate,
|
||||||
|
GlobalMaterialLocalizations.delegate,
|
||||||
|
GlobalCupertinoLocalizations.delegate,
|
||||||
|
GlobalWidgetsLocalizations.delegate,
|
||||||
|
],
|
||||||
|
builder: (context, child) => ShadAppBuilder(child: child!),
|
||||||
|
home: _buildHome(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildHome() {
|
||||||
|
return Consumer<AuthProvider>(
|
||||||
|
builder: (context, auth, _) {
|
||||||
|
if (auth.isLoading) {
|
||||||
|
return const Scaffold(
|
||||||
|
body: Center(child: CircularProgressIndicator()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return auth.isLoggedIn ? const MainPage() : const LoginPage();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 认证路由守卫 - 监听认证状态并自动导航
|
||||||
|
class AuthNavigator extends StatefulWidget {
|
||||||
|
final Widget child;
|
||||||
|
|
||||||
|
const AuthNavigator({super.key, required this.child});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<AuthNavigator> createState() => _AuthNavigatorState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AuthNavigatorState extends State<AuthNavigator> {
|
||||||
|
bool? _wasLoggedIn;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didChangeDependencies() {
|
||||||
|
super.didChangeDependencies();
|
||||||
|
final isLoggedIn = context.watch<AuthProvider>().isLoggedIn;
|
||||||
|
|
||||||
|
if (_wasLoggedIn == null) {
|
||||||
|
_wasLoggedIn = isLoggedIn;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_wasLoggedIn != isLoggedIn) {
|
||||||
|
_wasLoggedIn = isLoggedIn;
|
||||||
|
_navigateToAuthPage(isLoggedIn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _navigateToAuthPage(bool isLoggedIn) {
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
if (!mounted) return;
|
||||||
|
Navigator.of(context).pushAndRemoveUntil(
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (_) => isLoggedIn ? const MainPage() : const LoginPage(),
|
||||||
|
),
|
||||||
|
(route) => false,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) => widget.child;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import 'dart:convert';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import '../core/network/dio_client.dart';
|
import '../core/network/dio_client.dart';
|
||||||
import '../core/storage/local_storage.dart';
|
import '../core/storage/local_storage.dart';
|
||||||
@@ -15,7 +14,7 @@ class AuthProvider extends ChangeNotifier {
|
|||||||
String? _token;
|
String? _token;
|
||||||
|
|
||||||
AuthProvider(this._userService) {
|
AuthProvider(this._userService) {
|
||||||
_checkAuth();
|
_initAuth();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Getters
|
// Getters
|
||||||
@@ -24,100 +23,81 @@ class AuthProvider extends ChangeNotifier {
|
|||||||
bool get isLoading => _isLoading;
|
bool get isLoading => _isLoading;
|
||||||
String? get token => _token;
|
String? get token => _token;
|
||||||
|
|
||||||
/// 检查登录状态
|
/// 初始化认证状态
|
||||||
Future<void> _checkAuth() async {
|
Future<void> _initAuth() async {
|
||||||
_isLoading = true;
|
|
||||||
notifyListeners();
|
|
||||||
|
|
||||||
_token = LocalStorage.getToken();
|
_token = LocalStorage.getToken();
|
||||||
_isLoggedIn = _token != null && _token!.isNotEmpty;
|
_isLoggedIn = _token?.isNotEmpty == true;
|
||||||
|
|
||||||
if (_isLoggedIn) {
|
if (_isLoggedIn) {
|
||||||
final userJson = LocalStorage.getUserInfo();
|
_user = _loadUserFromStorage();
|
||||||
if (userJson != null) {
|
|
||||||
_user = User.fromJson(userJson);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_isLoading = false;
|
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
User? _loadUserFromStorage() {
|
||||||
|
final userJson = LocalStorage.getUserInfo();
|
||||||
|
return userJson != null ? User.fromJson(userJson) : null;
|
||||||
|
}
|
||||||
|
|
||||||
/// 登录
|
/// 登录
|
||||||
Future<ApiResponse<User>> login(String username, String password) async {
|
Future<ApiResponse<User>> login(String username, String password) {
|
||||||
_isLoading = true;
|
return _authenticate(() => _userService.login(username, password));
|
||||||
notifyListeners();
|
|
||||||
|
|
||||||
try {
|
|
||||||
final response = await _userService.login(username, password);
|
|
||||||
|
|
||||||
if (response.success && response.data != null) {
|
|
||||||
_token = response.data!['token'] as String?;
|
|
||||||
final userJson = response.data!['user'] as Map<String, dynamic>? ??
|
|
||||||
response.data!['userInfo'] as Map<String, dynamic>?;
|
|
||||||
|
|
||||||
if (_token != null) {
|
|
||||||
await LocalStorage.saveToken(_token!);
|
|
||||||
}
|
|
||||||
if (userJson != null) {
|
|
||||||
await LocalStorage.saveUserInfo(userJson);
|
|
||||||
_user = User.fromJson(userJson);
|
|
||||||
}
|
|
||||||
|
|
||||||
_isLoggedIn = true;
|
|
||||||
notifyListeners();
|
|
||||||
return ApiResponse.success(_user!, response.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
_isLoading = false;
|
|
||||||
notifyListeners();
|
|
||||||
return ApiResponse.fail(response.message ?? '登录失败');
|
|
||||||
} catch (e) {
|
|
||||||
_isLoading = false;
|
|
||||||
notifyListeners();
|
|
||||||
return ApiResponse.fail('登录失败: $e');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 注册
|
/// 注册
|
||||||
Future<ApiResponse<User>> register(String username, String password) async {
|
Future<ApiResponse<User>> register(String username, String password) {
|
||||||
_isLoading = true;
|
return _authenticate(() => _userService.register(username, password));
|
||||||
notifyListeners();
|
}
|
||||||
|
|
||||||
|
/// 统一认证处理
|
||||||
|
Future<ApiResponse<User>> _authenticate(
|
||||||
|
Future<ApiResponse<Map<String, dynamic>>> Function() action,
|
||||||
|
) async {
|
||||||
|
_setLoading(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final response = await _userService.register(username, password);
|
final response = await action();
|
||||||
|
|
||||||
if (response.success && response.data != null) {
|
if (!response.success || response.data == null) {
|
||||||
_token = response.data!['token'] as String?;
|
return ApiResponse.fail(response.message ?? '操作失败');
|
||||||
final userJson = response.data!['userInfo'] as Map<String, dynamic>?;
|
|
||||||
|
|
||||||
if (_token != null) {
|
|
||||||
await LocalStorage.saveToken(_token!);
|
|
||||||
}
|
|
||||||
if (userJson != null) {
|
|
||||||
await LocalStorage.saveUserInfo(userJson);
|
|
||||||
_user = User.fromJson(userJson);
|
|
||||||
}
|
|
||||||
|
|
||||||
_isLoggedIn = true;
|
|
||||||
notifyListeners();
|
|
||||||
return ApiResponse.success(_user!, response.message);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_isLoading = false;
|
return _handleAuthSuccess(response.data!, response.message);
|
||||||
notifyListeners();
|
|
||||||
return ApiResponse.fail(response.message ?? '注册失败');
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
_isLoading = false;
|
return ApiResponse.fail('操作失败: $e');
|
||||||
notifyListeners();
|
} finally {
|
||||||
return ApiResponse.fail('注册失败: $e');
|
_setLoading(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 处理认证成功
|
||||||
|
ApiResponse<User> _handleAuthSuccess(
|
||||||
|
Map<String, dynamic> data,
|
||||||
|
String? message,
|
||||||
|
) {
|
||||||
|
_token = data['token'] as String?;
|
||||||
|
final userJson = data['user'] as Map<String, dynamic>? ??
|
||||||
|
data['userInfo'] as Map<String, dynamic>?;
|
||||||
|
|
||||||
|
if (_token != null) {
|
||||||
|
LocalStorage.saveToken(_token!);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (userJson != null) {
|
||||||
|
LocalStorage.saveUserInfo(userJson);
|
||||||
|
_user = User.fromJson(userJson);
|
||||||
|
}
|
||||||
|
|
||||||
|
_isLoggedIn = true;
|
||||||
|
|
||||||
|
return _user != null
|
||||||
|
? ApiResponse.success(_user!, message)
|
||||||
|
: ApiResponse.fail('用户信息获取失败');
|
||||||
|
}
|
||||||
|
|
||||||
/// 退出登录
|
/// 退出登录
|
||||||
Future<void> logout() async {
|
Future<void> logout() async {
|
||||||
_isLoading = true;
|
_setLoading(true);
|
||||||
notifyListeners();
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await _userService.logout();
|
await _userService.logout();
|
||||||
@@ -125,12 +105,15 @@ class AuthProvider extends ChangeNotifier {
|
|||||||
// 忽略退出登录的接口错误
|
// 忽略退出登录的接口错误
|
||||||
}
|
}
|
||||||
|
|
||||||
await LocalStorage.clearUserData();
|
_clearAuthState();
|
||||||
|
_setLoading(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _clearAuthState() {
|
||||||
|
LocalStorage.clearUserData();
|
||||||
_user = null;
|
_user = null;
|
||||||
_token = null;
|
_token = null;
|
||||||
_isLoggedIn = false;
|
_isLoggedIn = false;
|
||||||
_isLoading = false;
|
|
||||||
notifyListeners();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 刷新用户信息
|
/// 刷新用户信息
|
||||||
@@ -148,4 +131,9 @@ class AuthProvider extends ChangeNotifier {
|
|||||||
// 忽略错误
|
// 忽略错误
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _setLoading(bool value) {
|
||||||
|
_isLoading = value;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import 'package:shadcn_ui/shadcn_ui.dart';
|
|||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
import '../../../providers/auth_provider.dart';
|
import '../../../providers/auth_provider.dart';
|
||||||
|
import '../main/main_page.dart';
|
||||||
import 'register_page.dart';
|
import 'register_page.dart';
|
||||||
|
|
||||||
class LoginPage extends StatefulWidget {
|
class LoginPage extends StatefulWidget {
|
||||||
@@ -15,6 +16,10 @@ class LoginPage extends StatefulWidget {
|
|||||||
class _LoginPageState extends State<LoginPage> {
|
class _LoginPageState extends State<LoginPage> {
|
||||||
final formKey = GlobalKey<ShadFormState>();
|
final formKey = GlobalKey<ShadFormState>();
|
||||||
|
|
||||||
|
static const _maxFormWidth = 400.0;
|
||||||
|
static const _logoSize = 64.0;
|
||||||
|
static const _loadingIndicatorSize = 16.0;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final theme = ShadTheme.of(context);
|
final theme = ShadTheme.of(context);
|
||||||
@@ -22,7 +27,7 @@ class _LoginPageState extends State<LoginPage> {
|
|||||||
return Scaffold(
|
return Scaffold(
|
||||||
body: Center(
|
body: Center(
|
||||||
child: ConstrainedBox(
|
child: ConstrainedBox(
|
||||||
constraints: const BoxConstraints(maxWidth: 400),
|
constraints: const BoxConstraints(maxWidth: _maxFormWidth),
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(24),
|
padding: const EdgeInsets.all(24),
|
||||||
child: ShadForm(
|
child: ShadForm(
|
||||||
@@ -31,135 +36,15 @@ class _LoginPageState extends State<LoginPage> {
|
|||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
// Logo 和标题
|
_buildHeader(theme),
|
||||||
Icon(
|
|
||||||
LucideIcons.trendingUp,
|
|
||||||
size: 64,
|
|
||||||
color: theme.colorScheme.primary,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 24),
|
|
||||||
Text(
|
|
||||||
'模拟所',
|
|
||||||
style: theme.textTheme.h1,
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
Text(
|
|
||||||
'虚拟货币模拟交易平台',
|
|
||||||
style: theme.textTheme.muted,
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 48),
|
const SizedBox(height: 48),
|
||||||
|
_buildUsernameField(),
|
||||||
// 用户名输入
|
|
||||||
ShadInputFormField(
|
|
||||||
id: 'username',
|
|
||||||
label: const Text('用户名'),
|
|
||||||
placeholder: const Text('请输入用户名'),
|
|
||||||
leading: const Icon(LucideIcons.user),
|
|
||||||
validator: (value) {
|
|
||||||
if (value == null || value.isEmpty) {
|
|
||||||
return '请输入用户名';
|
|
||||||
}
|
|
||||||
if (value.length < 3) {
|
|
||||||
return '用户名至少 3 个字符';
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
_buildPasswordField(),
|
||||||
// 密码输入
|
|
||||||
ShadInputFormField(
|
|
||||||
id: 'password',
|
|
||||||
label: const Text('密码'),
|
|
||||||
placeholder: const Text('请输入密码'),
|
|
||||||
obscureText: true,
|
|
||||||
leading: const Icon(LucideIcons.lock),
|
|
||||||
validator: (value) {
|
|
||||||
if (value == null || value.isEmpty) {
|
|
||||||
return '请输入密码';
|
|
||||||
}
|
|
||||||
if (value.length < 6) {
|
|
||||||
return '密码至少 6 个字符';
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
),
|
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
|
_buildLoginButton(),
|
||||||
// 登录按钮
|
|
||||||
Consumer<AuthProvider>(
|
|
||||||
builder: (context, auth, _) {
|
|
||||||
return ShadButton(
|
|
||||||
onPressed: auth.isLoading
|
|
||||||
? null
|
|
||||||
: () async {
|
|
||||||
if (formKey.currentState!.saveAndValidate()) {
|
|
||||||
final values = formKey.currentState!.value;
|
|
||||||
final response = await auth.login(
|
|
||||||
values['username'],
|
|
||||||
values['password'],
|
|
||||||
);
|
|
||||||
|
|
||||||
// 登录成功后,Provider 会自动更新状态
|
|
||||||
// MaterialApp 的 Consumer 会自动切换到 MainPage
|
|
||||||
if (!response.success && mounted) {
|
|
||||||
// 只在失败时显示错误
|
|
||||||
showShadDialog(
|
|
||||||
context: context,
|
|
||||||
builder: (context) => ShadDialog.alert(
|
|
||||||
title: const Text('登录失败'),
|
|
||||||
description: Text(
|
|
||||||
response.message ?? '用户名或密码错误',
|
|
||||||
),
|
|
||||||
actions: [
|
|
||||||
ShadButton(
|
|
||||||
child: const Text('确定'),
|
|
||||||
onPressed: () =>
|
|
||||||
Navigator.of(context).pop(),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
child: auth.isLoading
|
|
||||||
? const SizedBox.square(
|
|
||||||
dimension: 16,
|
|
||||||
child: CircularProgressIndicator(
|
|
||||||
strokeWidth: 2,
|
|
||||||
color: Colors.white,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
: const Text('登录'),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
_buildRegisterLink(theme),
|
||||||
// 注册链接
|
|
||||||
Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
'还没有账号?',
|
|
||||||
style: theme.textTheme.muted,
|
|
||||||
),
|
|
||||||
ShadButton.link(
|
|
||||||
onPressed: () {
|
|
||||||
Navigator.push(
|
|
||||||
context,
|
|
||||||
MaterialPageRoute(
|
|
||||||
builder: (context) => RegisterPage(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
child: const Text('立即注册'),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -168,4 +53,154 @@ class _LoginPageState extends State<LoginPage> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildHeader(ShadThemeData theme) {
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
LucideIcons.trendingUp,
|
||||||
|
size: _logoSize,
|
||||||
|
color: theme.colorScheme.primary,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
Text(
|
||||||
|
'模拟所',
|
||||||
|
style: theme.textTheme.h1,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
'虚拟货币模拟交易平台',
|
||||||
|
style: theme.textTheme.muted,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildUsernameField() {
|
||||||
|
return ShadInputFormField(
|
||||||
|
id: 'username',
|
||||||
|
label: const Text('用户名'),
|
||||||
|
placeholder: const Text('请输入用户名'),
|
||||||
|
leading: const Icon(LucideIcons.user),
|
||||||
|
validator: _validateUsername,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildPasswordField() {
|
||||||
|
return ShadInputFormField(
|
||||||
|
id: 'password',
|
||||||
|
label: const Text('密码'),
|
||||||
|
placeholder: const Text('请输入密码'),
|
||||||
|
obscureText: true,
|
||||||
|
leading: const Icon(LucideIcons.lock),
|
||||||
|
validator: _validatePassword,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildLoginButton() {
|
||||||
|
return Consumer<AuthProvider>(
|
||||||
|
builder: (context, auth, _) {
|
||||||
|
return ShadButton(
|
||||||
|
onPressed: auth.isLoading ? null : () => _handleLogin(auth),
|
||||||
|
child: auth.isLoading
|
||||||
|
? const SizedBox.square(
|
||||||
|
dimension: _loadingIndicatorSize,
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
strokeWidth: 2,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: const Text('登录'),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildRegisterLink(ShadThemeData theme) {
|
||||||
|
return Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'还没有账号?',
|
||||||
|
style: theme.textTheme.muted,
|
||||||
|
),
|
||||||
|
ShadButton.link(
|
||||||
|
onPressed: _navigateToRegister,
|
||||||
|
child: const Text('立即注册'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validators
|
||||||
|
String? _validateUsername(String? value) {
|
||||||
|
if (value == null || value.isEmpty) {
|
||||||
|
return '请输入用户名';
|
||||||
|
}
|
||||||
|
if (value.length < 3) {
|
||||||
|
return '用户名至少 3 个字符';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
String? _validatePassword(String? value) {
|
||||||
|
if (value == null || value.isEmpty) {
|
||||||
|
return '请输入密码';
|
||||||
|
}
|
||||||
|
if (value.length < 6) {
|
||||||
|
return '密码至少 6 个字符';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Actions
|
||||||
|
Future<void> _handleLogin(AuthProvider auth) async {
|
||||||
|
if (!formKey.currentState!.saveAndValidate()) return;
|
||||||
|
|
||||||
|
final values = formKey.currentState!.value;
|
||||||
|
final response = await auth.login(
|
||||||
|
values['username'],
|
||||||
|
values['password'],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!mounted) return;
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
_navigateToMainPage();
|
||||||
|
} else {
|
||||||
|
_showErrorDialog(response.message ?? '用户名或密码错误');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _navigateToMainPage() {
|
||||||
|
Navigator.of(context).pushAndRemoveUntil(
|
||||||
|
MaterialPageRoute(builder: (_) => const MainPage()),
|
||||||
|
(route) => false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _navigateToRegister() {
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(builder: (context) => const RegisterPage()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showErrorDialog(String message) {
|
||||||
|
showShadDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => ShadDialog.alert(
|
||||||
|
title: const Text('登录失败'),
|
||||||
|
description: Text(message),
|
||||||
|
actions: [
|
||||||
|
ShadButton(
|
||||||
|
child: const Text('确定'),
|
||||||
|
onPressed: () => Navigator.of(context).pop(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:shadcn_ui/shadcn_ui.dart';
|
import 'package:shadcn_ui/shadcn_ui.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import '../../../providers/auth_provider.dart';
|
import '../../../providers/auth_provider.dart';
|
||||||
|
import '../auth/login_page.dart';
|
||||||
|
|
||||||
/// 我的页面 - 使用 shadcn_ui 现代化设计
|
/// 我的页面 - 使用 shadcn_ui 现代化设计
|
||||||
class MinePage extends StatefulWidget {
|
class MinePage extends StatefulWidget {
|
||||||
@@ -275,8 +276,14 @@ class _MinePageState extends State<MinePage> with AutomaticKeepAliveClientMixin
|
|||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
await auth.logout();
|
await auth.logout();
|
||||||
|
// 登出成功,直接导航到登录页
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
Navigator.pushReplacementNamed(context, '/login');
|
Navigator.of(context).pushAndRemoveUntil(
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (_) => const LoginPage(),
|
||||||
|
),
|
||||||
|
(route) => false,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,13 +1,17 @@
|
|||||||
package com.it.rattan.config;
|
package com.it.rattan.config;
|
||||||
|
|
||||||
|
import org.springframework.boot.web.servlet.FilterRegistrationBean;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
import org.springframework.web.cors.CorsConfiguration;
|
import org.springframework.core.Ordered;
|
||||||
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
|
|
||||||
import org.springframework.web.filter.CorsFilter;
|
|
||||||
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
|
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
|
||||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||||
|
|
||||||
|
import javax.servlet.*;
|
||||||
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
import javax.servlet.http.HttpServletResponse;
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
public class WebConfig implements WebMvcConfigurer {
|
public class WebConfig implements WebMvcConfigurer {
|
||||||
|
|
||||||
@@ -17,24 +21,46 @@ public class WebConfig implements WebMvcConfigurer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 跨域配置
|
* 跨域过滤器 - 支持凭证,最高优先级
|
||||||
*/
|
*/
|
||||||
@Bean
|
@Bean
|
||||||
public CorsFilter corsFilter() {
|
public FilterRegistrationBean<Filter> corsFilterRegistration() {
|
||||||
CorsConfiguration config = new CorsConfiguration();
|
FilterRegistrationBean<Filter> registration = new FilterRegistrationBean<>();
|
||||||
// 允许所有来源
|
registration.setFilter(new CorsFilter());
|
||||||
config.addAllowedOrigin("*");
|
registration.addUrlPatterns("/*");
|
||||||
// 允许所有请求头
|
registration.setName("corsFilter");
|
||||||
config.addAllowedHeader("*");
|
registration.setOrder(Ordered.HIGHEST_PRECEDENCE);
|
||||||
// 允许所有请求方法
|
return registration;
|
||||||
config.addAllowedMethod("*");
|
}
|
||||||
// 允许携带凭证
|
|
||||||
config.setAllowCredentials(true);
|
|
||||||
// 预检请求缓存时间
|
|
||||||
config.setMaxAge(3600L);
|
|
||||||
|
|
||||||
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
|
/**
|
||||||
source.registerCorsConfiguration("/**", config);
|
* CORS 过滤器实现
|
||||||
return new CorsFilter(source);
|
*/
|
||||||
|
private static class CorsFilter implements Filter {
|
||||||
|
@Override
|
||||||
|
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
|
||||||
|
throws IOException, ServletException {
|
||||||
|
HttpServletResponse response = (HttpServletResponse) res;
|
||||||
|
HttpServletRequest request = (HttpServletRequest) req;
|
||||||
|
|
||||||
|
String origin = request.getHeader("Origin");
|
||||||
|
if (origin != null) {
|
||||||
|
response.setHeader("Access-Control-Allow-Origin", origin);
|
||||||
|
} else {
|
||||||
|
response.setHeader("Access-Control-Allow-Origin", "*");
|
||||||
|
}
|
||||||
|
response.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
|
||||||
|
response.setHeader("Access-Control-Allow-Headers", "*");
|
||||||
|
response.setHeader("Access-Control-Allow-Credentials", "true");
|
||||||
|
response.setHeader("Access-Control-Expose-Headers", "Authorization");
|
||||||
|
response.setHeader("Access-Control-Max-Age", "3600");
|
||||||
|
|
||||||
|
if ("OPTIONS".equalsIgnoreCase(request.getMethod())) {
|
||||||
|
response.setStatus(HttpServletResponse.SC_OK);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
chain.doFilter(req, res);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user