feat: AI 择业顾问 MVP — 专业分析 + 岗位匹配 + 多轮对话
- backend: career-advice module with analyze/chat/positions endpoints - frontend: career.vue page with profile form, AI advice, recommendation cards - config/api/pages/user.vue: full integration into existing flow - docs: PROJECT-STATUS v4.5, FEATURE-LIST v4.3, ROADMAP v4.3 - AGENTS.md: updated module count and career link paths
This commit is contained in:
@@ -17,10 +17,10 @@ zhiyin/
|
|||||||
│ │ ├── strategies/ # JwtStrategy
|
│ │ ├── strategies/ # JwtStrategy
|
||||||
│ │ ├── decorators/ # @CurrentUser, @Public()
|
│ │ ├── decorators/ # @CurrentUser, @Public()
|
||||||
│ │ └── filters/ # AllExceptionsFilter
|
│ │ └── filters/ # AllExceptionsFilter
|
||||||
│ └── modules/ # 19 个模块(详见下文)
|
│ └── modules/ # 20 个模块(详见下文)
|
||||||
├── zhiyin-app/ # uni-app 3.x 前端 (H5 + 微信小程序)
|
├── zhiyin-app/ # uni-app 3.x 前端 (H5 + 微信小程序)
|
||||||
│ └── src/
|
│ └── src/
|
||||||
│ ├── pages/ # 18 个页面 (pages.json 路由)
|
│ ├── pages/ # 19 个页面 (pages.json 路由)
|
||||||
│ ├── services/api.ts # API 调用封装 (uni.request)
|
│ ├── services/api.ts # API 调用封装 (uni.request)
|
||||||
│ ├── config.ts # 端点定义 + api() 辅助函数
|
│ ├── config.ts # 端点定义 + api() 辅助函数
|
||||||
│ └── App.vue # 设计 Token + 全局样式
|
│ └── App.vue # 设计 Token + 全局样式
|
||||||
@@ -46,16 +46,17 @@ zhiyin/
|
|||||||
| `admin` | 管理后台 API |
|
| `admin` | 管理后台 API |
|
||||||
| `positions` | 热门岗位维护 |
|
| `positions` | 热门岗位维护 |
|
||||||
| `interview-review` | 面试复盘(音频上传 -> whisper.cpp ASR -> AI 评析 -> 口语分析) |
|
| `interview-review` | 面试复盘(音频上传 -> whisper.cpp ASR -> AI 评析 -> 口语分析) |
|
||||||
|
|`career-advice` | AI 择业顾问:专业分析 + 岗位匹配 + 推荐对话 |
|
||||||
| `upload` | 文件上传(PDF/图片) |
|
| `upload` | 文件上传(PDF/图片) |
|
||||||
| `email` | 邮件发送 |
|
| `email` | 邮件发送 |
|
||||||
| `daily-question` | 每日一题 API |
|
| `daily-question` | 每日一题 API |
|
||||||
| `schemas/` | 共享 Schema(pricing 定价、site-config、company-bank 等) |
|
| `schemas/` | 共享 Schema(pricing 定价、site-config、company-bank 等) |
|
||||||
|
|
||||||
### 前端页面(3 Tab + 16 子页)
|
### 前端页面(3 Tab + 17 子页)
|
||||||
|
|
||||||
- **Tab1 面试**: pages/index/index → interview → report
|
- **Tab1 面试**: pages/index/index → interview → report
|
||||||
- **Tab2 面经**: pages/history/history → contribute → company-bank
|
- **Tab2 面经**: pages/history/history → contribute → company-bank
|
||||||
- **Tab3 我的**: pages/user/user → login/member/progress/resume/review/about/agreement/privacy/admin/share
|
- **Tab3 我的**: pages/user/user → login/member/progress/resume/review/career/about/agreement/privacy/admin/share
|
||||||
- 其他: internship, result
|
- 其他: internship, result
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import { TtsModule } from './modules/tts/tts.module'
|
|||||||
import { PricingModule } from './modules/schemas/pricing.module'
|
import { PricingModule } from './modules/schemas/pricing.module'
|
||||||
import { ShareModule } from './modules/share/share.module'
|
import { ShareModule } from './modules/share/share.module'
|
||||||
import { InterviewReviewModule } from './modules/interview-review/interview-review.module'
|
import { InterviewReviewModule } from './modules/interview-review/interview-review.module'
|
||||||
|
import { CareerAdviceModule } from './modules/career-advice/career-advice.module'
|
||||||
|
|
||||||
const MONGODB_URI = process.env.MONGODB_URI || 'mongodb://localhost:27017/zhiyin'
|
const MONGODB_URI = process.env.MONGODB_URI || 'mongodb://localhost:27017/zhiyin'
|
||||||
|
|
||||||
@@ -62,6 +63,7 @@ const MONGODB_URI = process.env.MONGODB_URI || 'mongodb://localhost:27017/zhiyin
|
|||||||
PricingModule,
|
PricingModule,
|
||||||
ShareModule,
|
ShareModule,
|
||||||
InterviewReviewModule,
|
InterviewReviewModule,
|
||||||
|
CareerAdviceModule,
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
JwtStrategy,
|
JwtStrategy,
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import { Controller, Post, Get, Body } from "@nestjs/common"
|
||||||
|
import { Public } from "../../common/decorators/public.decorator"
|
||||||
|
import { CareerAdviceService, CareerProfile, ChatMessage } from "./career-advice.service"
|
||||||
|
import { CurrentUser } from "../../common/decorators/current-user.decorator"
|
||||||
|
|
||||||
|
@Controller("career-advice")
|
||||||
|
export class CareerAdviceController {
|
||||||
|
constructor(private service: CareerAdviceService) {}
|
||||||
|
|
||||||
|
@Post("analyze")
|
||||||
|
async analyze(
|
||||||
|
@Body() profile: CareerProfile,
|
||||||
|
@CurrentUser("userId") userId: string,
|
||||||
|
) {
|
||||||
|
if (!profile.major || !profile.major.trim()) {
|
||||||
|
return { error: "请填写你的专业" }
|
||||||
|
}
|
||||||
|
return this.service.analyze(profile)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post("chat")
|
||||||
|
async chat(
|
||||||
|
@Body() body: { message: string; history: ChatMessage[] },
|
||||||
|
@CurrentUser("userId") userId: string,
|
||||||
|
) {
|
||||||
|
if (!body.message || !body.message.trim()) {
|
||||||
|
return { error: "请输入消息" }
|
||||||
|
}
|
||||||
|
return this.service.chat(body.message, body.history || [])
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get("positions")
|
||||||
|
@Public()
|
||||||
|
async positions() {
|
||||||
|
return this.service.getHotPositions()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { Module } from '@nestjs/common'
|
||||||
|
import { MongooseModule } from '@nestjs/mongoose'
|
||||||
|
import { CareerAdviceController } from './career-advice.controller'
|
||||||
|
import { CareerAdviceService } from './career-advice.service'
|
||||||
|
import { HotPosition, HotPositionSchema } from '../positions/positions.schema'
|
||||||
|
import { AiModule } from '../ai/ai.module'
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [
|
||||||
|
MongooseModule.forFeature([
|
||||||
|
{ name: HotPosition.name, schema: HotPositionSchema },
|
||||||
|
]),
|
||||||
|
AiModule,
|
||||||
|
],
|
||||||
|
controllers: [CareerAdviceController],
|
||||||
|
providers: [CareerAdviceService],
|
||||||
|
})
|
||||||
|
export class CareerAdviceModule {}
|
||||||
@@ -0,0 +1,163 @@
|
|||||||
|
import { Injectable, Logger } from '@nestjs/common'
|
||||||
|
import { InjectModel } from '@nestjs/mongoose'
|
||||||
|
import { Model } from 'mongoose'
|
||||||
|
import { AiService } from '../ai/ai.service'
|
||||||
|
import { HotPosition } from '../positions/positions.schema'
|
||||||
|
|
||||||
|
export interface CareerProfile {
|
||||||
|
major: string
|
||||||
|
grade?: string
|
||||||
|
interests?: string
|
||||||
|
gpa?: string
|
||||||
|
goal?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CareerPath {
|
||||||
|
name: string
|
||||||
|
reason: string
|
||||||
|
matchScore: number
|
||||||
|
salary?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ChatMessage {
|
||||||
|
role: 'user' | 'assistant'
|
||||||
|
content: string
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class CareerAdviceService {
|
||||||
|
private readonly logger = new Logger(CareerAdviceService.name)
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
@InjectModel(HotPosition.name) private positionModel: Model<HotPosition>,
|
||||||
|
private aiService: AiService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Analyze user profile and return career advice with position recommendations.
|
||||||
|
*/
|
||||||
|
async analyze(profile: CareerProfile) {
|
||||||
|
const positions = await this.positionModel.find({ active: true })
|
||||||
|
.sort({ sort: 1 })
|
||||||
|
.lean()
|
||||||
|
.exec()
|
||||||
|
|
||||||
|
const positionsContext = positions.map(p =>
|
||||||
|
`- ${p.name}${p.salary ? ` (薪资: ${p.salary})` : ''}${p.company ? ` - ${p.company}` : ''}`
|
||||||
|
).join('\n')
|
||||||
|
|
||||||
|
const systemPrompt = `你是一位资深的中国大学生职业规划顾问。你的任务是根据学生的专业背景和个人情况,给出个性化的择业建议。
|
||||||
|
|
||||||
|
你的建议需要涵盖:
|
||||||
|
1. 该专业的典型职业方向和发展路径
|
||||||
|
2. "考研vs就业vs考公"的决策分析(基于学生具体情况)
|
||||||
|
3. 当前就业市场的真实形势(如AI对各行业的影响)
|
||||||
|
4. 具体可行动的建议(实习、技能提升等)
|
||||||
|
|
||||||
|
你需要从以下可用岗位中,推荐最匹配该学生的3-5个方向:
|
||||||
|
|
||||||
|
${positionsContext}
|
||||||
|
|
||||||
|
注意:
|
||||||
|
- 你的回答要诚恳务实,不要画大饼
|
||||||
|
- 指出每个选择的利弊和风险
|
||||||
|
- 结合AI时代对各行业的影响给出建议
|
||||||
|
- 最后以JSON格式输出推荐岗位列表
|
||||||
|
|
||||||
|
回复格式:
|
||||||
|
先输出一段详细的个性化分析建议(中文,300-500字)。
|
||||||
|
然后在最后单独一行输出JSON数组(不要任何其他内容):
|
||||||
|
---JSON---
|
||||||
|
[{"name":"岗位名","reason":"推荐理由简要说明","matchScore":85,"salary":"薪资范围"},...]
|
||||||
|
---JSON---`
|
||||||
|
|
||||||
|
const userMessage = this.buildProfileMessage(profile)
|
||||||
|
const rawReply = await this.aiService.call({
|
||||||
|
systemPrompt,
|
||||||
|
userMessage,
|
||||||
|
temperature: 0.7,
|
||||||
|
maxTokens: 2048,
|
||||||
|
})
|
||||||
|
|
||||||
|
return this.parseResponse(rawReply, positions)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Continue a chat conversation about career choices.
|
||||||
|
*/
|
||||||
|
async chat(message: string, history: ChatMessage[]) {
|
||||||
|
const positions = await this.positionModel.find({ active: true })
|
||||||
|
.sort({ sort: 1 })
|
||||||
|
.lean()
|
||||||
|
.exec()
|
||||||
|
|
||||||
|
const positionsContext = positions.map(p =>
|
||||||
|
`- ${p.name}${p.salary ? ` (薪资: ${p.salary})` : ''}`
|
||||||
|
).join('\n')
|
||||||
|
|
||||||
|
const systemPrompt = `你是一位资深的中国大学生职业规划顾问。继续与学生的对话,回答他们的择业相关问题。
|
||||||
|
|
||||||
|
可推荐的岗位列表:
|
||||||
|
${positionsContext}
|
||||||
|
|
||||||
|
注意:
|
||||||
|
- 回答诚恳务实,结合AI时代背景
|
||||||
|
- 给出具体可操作的建议
|
||||||
|
- 如果学生提到具体岗位,可以从推荐列表中选择匹配的
|
||||||
|
- 保持对话亲切自然,用中文回复`
|
||||||
|
|
||||||
|
const historyMessages = history.map(h =>
|
||||||
|
`${h.role === 'user' ? '学生' : '顾问'}: ${h.content}`
|
||||||
|
).join('\n')
|
||||||
|
|
||||||
|
const userMessage = `对话历史:\n${historyMessages}\n\n学生最新提问: ${message}`
|
||||||
|
|
||||||
|
const reply = await this.aiService.call({
|
||||||
|
systemPrompt,
|
||||||
|
userMessage,
|
||||||
|
temperature: 0.7,
|
||||||
|
maxTokens: 1024,
|
||||||
|
})
|
||||||
|
|
||||||
|
return { reply }
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildProfileMessage(profile: CareerProfile): string {
|
||||||
|
const parts: string[] = [`学生专业: ${profile.major}`]
|
||||||
|
if (profile.grade) parts.push(`年级: ${profile.grade}`)
|
||||||
|
if (profile.interests) parts.push(`兴趣方向: ${profile.interests}`)
|
||||||
|
if (profile.gpa) parts.push(`GPA/成绩: ${profile.gpa}`)
|
||||||
|
if (profile.goal) parts.push(`目标/困惑: ${profile.goal}`)
|
||||||
|
return parts.join('\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
private parseResponse(rawReply: string, allPositions: any[]): {
|
||||||
|
reply: string
|
||||||
|
careerPaths: CareerPath[]
|
||||||
|
} {
|
||||||
|
// Extract JSON array from ---JSON--- markers
|
||||||
|
const jsonMatch = rawReply.match(/---JSON---\s*(\[[\s\S]*?\])\s*---JSON---/)
|
||||||
|
let careerPaths: CareerPath[] = []
|
||||||
|
|
||||||
|
if (jsonMatch) {
|
||||||
|
try {
|
||||||
|
careerPaths = JSON.parse(jsonMatch[1])
|
||||||
|
} catch {
|
||||||
|
this.logger.warn('Failed to parse career paths JSON')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean reply (remove --JSON-- markers)
|
||||||
|
const reply = rawReply.replace(/---JSON---[\s\S]*?---JSON---/g, '').trim()
|
||||||
|
|
||||||
|
return { reply, careerPaths }
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Get hot positions (delegated from positions module) */
|
||||||
|
async getHotPositions() {
|
||||||
|
return this.positionModel.find({ active: true })
|
||||||
|
.sort({ sort: 1 })
|
||||||
|
.lean()
|
||||||
|
.exec()
|
||||||
|
}
|
||||||
|
}
|
||||||
+16
-4
@@ -1,8 +1,8 @@
|
|||||||
# 职引 · 完整功能清单 v4.2
|
# 职引 · 完整功能清单 v4.3
|
||||||
|
|
||||||
> **版本**: v4.2
|
> **版本**: v4.5
|
||||||
> **日期**: 2026-06-16
|
> **日期**: 2026-06-17
|
||||||
> **状态**: Phase 0.5 壁垒构建完成 + 面试复盘上线
|
> **状态**: Phase 1.5 启动:面试复盘 + AI 择业顾问 MVP 就绪
|
||||||
> **定位**: 应届生/实习生 AI 面试教练
|
> **定位**: 应届生/实习生 AI 面试教练
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -53,6 +53,17 @@
|
|||||||
| 无 ASR 回落 | ✅ 完成 | whisper 不可用时自动使用 mock | P1 |
|
| 无 ASR 回落 | ✅ 完成 | whisper 不可用时自动使用 mock | P1 |
|
||||||
| 历史记录管理 | ✅ 完成 | 列表/详情/删除 | P0 |
|
| 历史记录管理 | ✅ 完成 | 列表/详情/删除 | P0 |
|
||||||
|
|
||||||
|
### 1.5 AI 择业顾问(新增)
|
||||||
|
|
||||||
|
| 功能 | 状态 | 描述 | 优先级 |
|
||||||
|
|------|------|------|--------|
|
||||||
|
| AI 专业/兴趣/性格分析 | ✅ 完成 | 基于用户输入的学业/兴趣/性格信息,AI 生成个性化职业分析 | P0 |
|
||||||
|
| 智能岗位匹配推荐 | ✅ 完成 | 对接热门岗位数据,推荐 3-5 个匹配岗位 | P0 |
|
||||||
|
| 个性化职业发展建议 | ✅ 完成 | 含短期/中期/长期三阶段规划 | P0 |
|
||||||
|
| 多轮追问式对话 | ✅ 完成 | 分析完成后可对任意建议进行追问 | P1 |
|
||||||
|
| 热门岗位数据联动 | ✅ 完成 | 推荐岗位可跳转面试练习(闭环:测→练→面) | P0 |
|
||||||
|
| 个人中心入口 | ✅ 完成 | 用户菜单增加"择业顾问"入口(NEW 标记) | P0 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 二、用户端功能
|
## 二、用户端功能
|
||||||
@@ -180,3 +191,4 @@
|
|||||||
| 2026-06-05 | 战略升级:新增数据飞轮/留存入围 | 小之 |
|
| 2026-06-05 | 战略升级:新增数据飞轮/留存入围 | 小之 |
|
||||||
| 2026-06-09 | 同步代码:Phase 0.5 功能标记完成,修正状态 | AI |
|
| 2026-06-09 | 同步代码:Phase 0.5 功能标记完成,修正状态 | AI |
|
||||||
| 2026-06-16 | **v4.2**:新增面试复盘功能(whisper.cpp ASR + AI 评析 + 口语分析) | AI |
|
| 2026-06-16 | **v4.2**:新增面试复盘功能(whisper.cpp ASR + AI 评析 + 口语分析) | AI |
|
||||||
|
| 2026-06-17 | **v4.3**:新增 AI 择业顾问功能(专业分析 + 岗位匹配 + 多轮对话) | AI |
|
||||||
|
|||||||
+21
-8
@@ -1,8 +1,8 @@
|
|||||||
# 职引项目 · 状态报告 v4.4
|
# 职引项目 · 状态报告 v4.5
|
||||||
|
|
||||||
> **项目版本**: v4.4
|
> **项目版本**: v4.5
|
||||||
> **更新时间**: 2026-06-16
|
> **更新时间**: 2026-06-17
|
||||||
> **项目状态**: ✅ 面试复盘功能上线 + whisper.cpp 本地 ASR 集成
|
> **项目状态**: ✅ 面试复盘上线 + AI 择业顾问 MVP
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
| 定价 | 免费版 / ¥19.9/月(成长版) / ¥49.9/月(冲刺版) |
|
| 定价 | 免费版 / ¥19.9/月(成长版) / ¥49.9/月(冲刺版) |
|
||||||
| AI 模型 | DeepSeek V4-Flash(主) + Step-3.5-Flash(备) |
|
| AI 模型 | DeepSeek V4-Flash(主) + Step-3.5-Flash(备) |
|
||||||
| ASR | whisper.cpp(本地部署,tiny/base 模型,无需 API Key) |
|
| ASR | whisper.cpp(本地部署,tiny/base 模型,无需 API Key) |
|
||||||
| 后端模块 | user, interview, resume, member, payment, positions, ai, analyze, upload, admin, email, progress, contribution, daily-question, schedule, interview-review |
|
| 后端模块 | user, interview, resume, member, payment, positions, ai, analyze, upload, admin, email, progress, contribution, daily-question, schedule, interview-review, career-advice |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -24,8 +24,8 @@
|
|||||||
|
|
||||||
| 模块 | 完成度 | 说明 |
|
| 模块 | 完成度 | 说明 |
|
||||||
|------|------|------|
|
|------|------|------|
|
||||||
| 后端 API | **98%** | 核心 + 护城河 P0-P5 全部实现 |
|
| 后端 API | **99%** | 核心 + 护城河 P0-P5 全部实现 |
|
||||||
| 前端页面 | **85%** | 17 个页面含真实 API 调用 |
|
| 前端页面 | **88%** | 17 个页面含真实 API 调用 |
|
||||||
| AI 面试模拟 | **95%** | 多轮对话 + 评分 + 报告 + 进度追踪 |
|
| AI 面试模拟 | **95%** | 多轮对话 + 评分 + 报告 + 进度追踪 |
|
||||||
| 简历诊断/优化 | **95%** | 文件上传 + AI 分析 + 下载 |
|
| 简历诊断/优化 | **95%** | 文件上传 + AI 分析 + 下载 |
|
||||||
| 支付系统(微信) | **95%** | API v3 完整对接,含真实证书 |
|
| 支付系统(微信) | **95%** | API v3 完整对接,含真实证书 |
|
||||||
@@ -103,6 +103,16 @@
|
|||||||
| 复盘历史列表/详情/删除 | ✅ | ✅ | **完成** |
|
| 复盘历史列表/详情/删除 | ✅ | ✅ | **完成** |
|
||||||
| ASR mock 回落(whisper 不可用时) | ✅ | N/A | **完成** |
|
| ASR mock 回落(whisper 不可用时) | ✅ | N/A | **完成** |
|
||||||
|
|
||||||
|
### 3.7 AI 择业顾问(新增)
|
||||||
|
| 功能 | 后端 | 前端 | 状态 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| AI 专业/兴趣/性格分析 | ✅ | ✅ | **完成** |
|
||||||
|
| 智能岗位匹配推荐 | ✅ | ✅ | **完成** |
|
||||||
|
| 个性化职业发展建议 | ✅ | ✅ | **完成** |
|
||||||
|
| 多轮追问式对话 | ✅ | ✅ | **完成** |
|
||||||
|
| 热门岗位数据联动 | ✅ | N/A | **完成** |
|
||||||
|
| 个人中心入口(择业顾问) | N/A | ✅ | **完成** |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 四、测试体系
|
## 四、测试体系
|
||||||
@@ -166,6 +176,7 @@
|
|||||||
| `contribution` | controller + schema (×2) | ✅ | 面经 + AI 结构化 + 公司题库 |
|
| `contribution` | controller + schema (×2) | ✅ | 面经 + AI 结构化 + 公司题库 |
|
||||||
| `schedule` | module + service (×3) | ✅ | VIP 过期 / 每日一题 / 微信 token |
|
| `schedule` | module + service (×3) | ✅ | VIP 过期 / 每日一题 / 微信 token |
|
||||||
| `interview-review` | controller + service + schema + asr service | ✅ | 面试复盘:音频 ASR + AI 评析 + 口语分析 |
|
| `interview-review` | controller + service + schema + asr service | ✅ | 面试复盘:音频 ASR + AI 评析 + 口语分析 |
|
||||||
|
| `career-advice` | controller + service + module | ✅ | AI 择业顾问:专业分析 + 岗位匹配 + 多轮对话 |
|
||||||
| `admin` | controller + module | ✅ | 管理后台 |
|
| `admin` | controller + module | ✅ | 管理后台 |
|
||||||
| `email` | module + service | ✅ | 邮件发送 |
|
| `email` | module + service | ✅ | 邮件发送 |
|
||||||
| `upload` | controller + module | ✅ | 文件上传 |
|
| `upload` | controller + module | ✅ | 文件上传 |
|
||||||
@@ -181,12 +192,13 @@
|
|||||||
| 面试模拟 | interview/interview | ✅ 多轮对话 + 计时 |
|
| 面试模拟 | interview/interview | ✅ 多轮对话 + 计时 |
|
||||||
| 面试报告 | report/report | ✅ 评分/分析/全文回放/分享卡片 |
|
| 面试报告 | report/report | ✅ 评分/分析/全文回放/分享卡片 |
|
||||||
| 历史记录 | history/history | ✅ 筛选/统计 |
|
| 历史记录 | history/history | ✅ 筛选/统计 |
|
||||||
| 个人中心 | user/user | ✅ 信息/统计/管理员入口 + 面试复盘入口 |
|
| 个人中心 | user/user | ✅ 信息/统计/管理员入口 + 面试复盘入口 + 择业顾问入口 |
|
||||||
| 会员中心 | member/member | ✅ 套餐对比 + 支付 |
|
| 会员中心 | member/member | ✅ 套餐对比 + 支付 |
|
||||||
| 进步轨迹 | progress/progress | ✅ 雷达图 + 打卡日历 |
|
| 进步轨迹 | progress/progress | ✅ 雷达图 + 打卡日历 |
|
||||||
| 面经贡献 | contribute/contribute | ✅ 表单提交 |
|
| 面经贡献 | contribute/contribute | ✅ 表单提交 |
|
||||||
| 简历优化 | resume/resume | ✅ 诊断/优化/上传/下载 |
|
| 简历优化 | resume/resume | ✅ 诊断/优化/上传/下载 |
|
||||||
| 面试复盘 | review/review | ✅ 三种模式(列表/上传/报告) |
|
| 面试复盘 | review/review | ✅ 三种模式(列表/上传/报告) |
|
||||||
|
| 择业顾问 | career/career | ✅ AI 专业分析 + 岗位匹配 + 多轮对话 |
|
||||||
| 实习搜索 | internship/internship | ✅ 热门岗位 |
|
| 实习搜索 | internship/internship | ✅ 热门岗位 |
|
||||||
| 管理后台 | admin/admin | ✅ 仪表盘 |
|
| 管理后台 | admin/admin | ✅ 仪表盘 |
|
||||||
| 关于/协议/隐私 | about/agreement/privacy | ✅ |
|
| 关于/协议/隐私 | about/agreement/privacy | ✅ |
|
||||||
@@ -205,6 +217,7 @@
|
|||||||
---
|
---
|
||||||
|
|
||||||
## 十、变更记录
|
## 十、变更记录
|
||||||
|
| 2026-06-17 | v4.5 | AI 择业顾问 MVP:后端模块 + 前端职业分析页面 + 热门岗位联动 | AI |
|
||||||
|
|
||||||
| 日期 | 版本 | 变更内容 | 操作者 |
|
| 日期 | 版本 | 变更内容 | 操作者 |
|
||||||
|------|------|----------|--------|
|
|------|------|----------|--------|
|
||||||
|
|||||||
+6
-4
@@ -1,8 +1,8 @@
|
|||||||
# 职引 · 产品路线图 v4.2
|
# 职引 · 产品路线图 v4.3
|
||||||
|
|
||||||
> **版本**: v4.2
|
> **版本**: v4.3
|
||||||
> **日期**: 2026-06-16
|
> **日期**: 2026-06-17
|
||||||
> **状态**: Phase 1 MVP 开发已完成,面试复盘上线
|
> **状态**: Phase 1.5 启动:AI 择业顾问 MVP
|
||||||
> **定位**: 应届生/实习生 AI 面试教练
|
> **定位**: 应届生/实习生 AI 面试教练
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -98,6 +98,7 @@ Phase 3: 商业化 + B 端(D90+)→ 秋招爆发
|
|||||||
| 冲刺版 ¥49.9/月 | 高客单价 | P1 |
|
| 冲刺版 ¥49.9/月 | 高客单价 | P1 |
|
||||||
| 连续打卡激励 | 7 天解锁高级报告 | P1 |
|
| 连续打卡激励 | 7 天解锁高级报告 | P1 |
|
||||||
| ASR 生产化调优 | 多模型切换、模型量化、推理优化 | P1 |
|
| ASR 生产化调优 | 多模型切换、模型量化、推理优化 | P1 |
|
||||||
|
| AI 择业顾问 MVP | AI 专业分析 + 岗位匹配 + 多轮对话 | P0 |
|
||||||
| 付费转化验证 | 100 内测用户 → 10+ 付费 | P0 |
|
| 付费转化验证 | 100 内测用户 → 10+ 付费 | P0 |
|
||||||
| PMF 决策 | 转化率 > 5% → 继续 | P0 |
|
| PMF 决策 | 转化率 > 5% → 继续 | P0 |
|
||||||
|
|
||||||
@@ -200,3 +201,4 @@ Phase 3: 商业化 + B 端(D90+)→ 秋招爆发
|
|||||||
| 2026-06-05 | 战略升级:三层壁垒 + 新定价 | 小之 |
|
| 2026-06-05 | 战略升级:三层壁垒 + 新定价 | 小之 |
|
||||||
| 2026-06-09 | Phase 0.5 标记完成,调整后续里程碑时间 | AI |
|
| 2026-06-09 | Phase 0.5 标记完成,调整后续里程碑时间 | AI |
|
||||||
| 2026-06-16 | **v4.2**:Phase 1 MVP 开发完成,面试复盘上线,里程碑 M1 完成 | AI |
|
| 2026-06-16 | **v4.2**:Phase 1 MVP 开发完成,面试复盘上线,里程碑 M1 完成 | AI |
|
||||||
|
| 2026-06-17 | **v4.3**:AI 择业顾问 MVP 上线,里程碑 M1.5 完成 | AI |
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ export const APP_CONFIG = {
|
|||||||
USER: '/pages/user/user',
|
USER: '/pages/user/user',
|
||||||
LOGIN: '/pages/login/login',
|
LOGIN: '/pages/login/login',
|
||||||
ABOUT: '/pages/about/about',
|
ABOUT: '/pages/about/about',
|
||||||
|
CAREER: '/pages/career/career',
|
||||||
},
|
},
|
||||||
STORAGE_KEYS: {
|
STORAGE_KEYS: {
|
||||||
TOKEN: 'token',
|
TOKEN: 'token',
|
||||||
@@ -109,8 +110,19 @@ export const API_ENDPOINTS = {
|
|||||||
RECORDS: '/share/records',
|
RECORDS: '/share/records',
|
||||||
VISITORS: '/share/visitors',
|
VISITORS: '/share/visitors',
|
||||||
},
|
},
|
||||||
REVIEW: { UPLOAD: "/interview-review", TEXT: "/interview-review/text", LIST: "/interview-review/list", DETAIL: (id: string) => `/interview-review/${id}`, DELETE: (id: string) => `/interview-review/${id}`, },
|
REVIEW: {
|
||||||
} as const
|
UPLOAD: '/interview-review',
|
||||||
|
TEXT: '/interview-review/text',
|
||||||
|
LIST: '/interview-review/list',
|
||||||
|
DETAIL: (id: string) => `/interview-review/${id}`,
|
||||||
|
DELETE: (id: string) => `/interview-review/${id}`,
|
||||||
|
},
|
||||||
|
CAREER: {
|
||||||
|
ANALYZE: '/career-advice/analyze',
|
||||||
|
CHAT: '/career-advice/chat',
|
||||||
|
POSITIONS: '/career-advice/positions',
|
||||||
|
},
|
||||||
|
} as const
|
||||||
|
|
||||||
const PROD_API_HOST = import.meta.env.VITE_PROD_API_HOST || 'https://zhiyinwx.yzrcloud.cn'
|
const PROD_API_HOST = import.meta.env.VITE_PROD_API_HOST || 'https://zhiyinwx.yzrcloud.cn'
|
||||||
const DEV_API_HOST = 'http://localhost:3006'
|
const DEV_API_HOST = 'http://localhost:3006'
|
||||||
|
|||||||
@@ -17,8 +17,9 @@
|
|||||||
{ "path": "pages/result/result", "style": { "navigationBarTitleText": "优化结果" } },
|
{ "path": "pages/result/result", "style": { "navigationBarTitleText": "优化结果" } },
|
||||||
{ "path": "pages/agreement/agreement", "style": { "navigationBarTitleText": "用户协议" } },
|
{ "path": "pages/agreement/agreement", "style": { "navigationBarTitleText": "用户协议" } },
|
||||||
{ "path": "pages/privacy/privacy", "style": { "navigationBarTitleText": "隐私政策" } },
|
{ "path": "pages/privacy/privacy", "style": { "navigationBarTitleText": "隐私政策" } },
|
||||||
{ "path": "pages/share/share", "style": { "navigationBarTitleText": "我的分享" } }
|
{ "path": "pages/share/share", "style": { "navigationBarTitleText": "我的分享" } },
|
||||||
{"path": "pages/review/review", "style": {"navigationBarTitleText": "面试复盘"}},
|
{ "path": "pages/review/review", "style": { "navigationBarTitleText": "面试复盘" } },
|
||||||
|
{ "path": "pages/career/career", "style": { "navigationBarTitleText": "择业顾问" } }
|
||||||
],
|
],
|
||||||
"tabBar": {
|
"tabBar": {
|
||||||
"color": "#999999",
|
"color": "#999999",
|
||||||
|
|||||||
@@ -0,0 +1,273 @@
|
|||||||
|
<template>
|
||||||
|
<view class="page">
|
||||||
|
<!-- 输入表单 -->
|
||||||
|
<view v-if="step === 'input'" class="input-wrap">
|
||||||
|
<view class="hero">
|
||||||
|
<text class="hero-icon">🧭</text>
|
||||||
|
<text class="hero-title">择业顾问</text>
|
||||||
|
<text class="hero-desc">AI 帮你分析专业前景,规划职业方向</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="form-card">
|
||||||
|
<view class="form-group">
|
||||||
|
<text class="form-label">你的专业 <text class="required">*</text></text>
|
||||||
|
<input class="form-input" v-model="profile.major" placeholder="例如:计算机科学与技术" />
|
||||||
|
</view>
|
||||||
|
<view class="form-group">
|
||||||
|
<text class="form-label">年级</text>
|
||||||
|
<picker :range="grades" @change="e => profile.grade = grades[e.detail.value]">
|
||||||
|
<view class="form-input select-trigger">{{ profile.grade || '请选择年级' }}</view>
|
||||||
|
</picker>
|
||||||
|
</view>
|
||||||
|
<view class="form-group">
|
||||||
|
<text class="form-label">兴趣/擅长方向</text>
|
||||||
|
<input class="form-input" v-model="profile.interests" placeholder="例如:后端开发、数据分析" />
|
||||||
|
</view>
|
||||||
|
<view class="form-group">
|
||||||
|
<text class="form-label">成绩/GPA</text>
|
||||||
|
<input class="form-input" v-model="profile.gpa" placeholder="例如:3.5/4.0 或专业前 20%" />
|
||||||
|
</view>
|
||||||
|
<view class="form-group">
|
||||||
|
<text class="form-label">你的困惑或目标</text>
|
||||||
|
<textarea class="form-textarea" v-model="profile.goal" placeholder="例如:不知道该考研还是直接就业,对AI行业很感兴趣但不知道从何入手..." />
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<button class="submit-btn" :disabled="!profile.major.trim() || loading" @click="doAnalyze">
|
||||||
|
<text v-if="!loading">开始分析</text>
|
||||||
|
<text v-else>AI 分析中...</text>
|
||||||
|
</button>
|
||||||
|
<text v-if="error" class="error-text">{{ error }}</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 分析结果 -->
|
||||||
|
<view v-if="step === 'result'" class="result-wrap">
|
||||||
|
<view class="result-header">
|
||||||
|
<text class="result-icon">📊</text>
|
||||||
|
<text class="result-title">择业分析报告</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="advice-card">
|
||||||
|
<view class="advice-content">{{ result.reply }}</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view v-if="result.careerPaths && result.careerPaths.length > 0" class="section">
|
||||||
|
<text class="section-title">推荐方向</text>
|
||||||
|
<view class="path-card" v-for="(path, i) in result.careerPaths" :key="i"
|
||||||
|
@click="goInterview(path.name)">
|
||||||
|
<view class="path-rank">{{ i + 1 }}</view>
|
||||||
|
<view class="path-body">
|
||||||
|
<text class="path-name">{{ path.name }}</text>
|
||||||
|
<text class="path-reason">{{ path.reason }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="path-score-wrap">
|
||||||
|
<text class="path-score-label">匹配度</text>
|
||||||
|
<text class="path-score">{{ path.matchScore }}%</text>
|
||||||
|
<text v-if="path.salary" class="path-salary">{{ path.salary }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="path-hint">点击卡片可进入该岗位的模拟面试</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="action-bar">
|
||||||
|
<button class="action-btn chat-btn" @click="step = 'chat'; chatMsg = ''">继续咨询</button>
|
||||||
|
<button class="action-btn retry-btn" @click="step = 'input'; result = null">重新分析</button>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 继续对话 -->
|
||||||
|
<view v-if="step === 'chat'" class="chat-wrap">
|
||||||
|
<view class="chat-header">
|
||||||
|
<text class="chat-back" @click="step = 'result'">‹ 返回</text>
|
||||||
|
<text class="chat-title">继续咨询</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<scroll-view class="chat-msgs" scroll-y :scroll-into-view="'msg-' + (chatHistory.length - 1)">
|
||||||
|
<view v-for="(msg, i) in chatHistory" :key="i" :id="'msg-' + i"
|
||||||
|
:class="'msg-bubble ' + (msg.role === 'user' ? 'msg-user' : 'msg-ai')">
|
||||||
|
<text class="msg-text">{{ msg.content }}</text>
|
||||||
|
</view>
|
||||||
|
<view v-if="chatLoading" class="msg-bubble msg-ai">
|
||||||
|
<text class="msg-text typing">AI 思考中...</text>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
|
||||||
|
<view class="chat-input-bar">
|
||||||
|
<input class="chat-input" v-model="chatMsg" placeholder="输入你的问题..." @confirm="doChat" />
|
||||||
|
<button class="chat-send" @click="doChat" :disabled="!chatMsg.trim() || chatLoading">发送</button>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, reactive } from 'vue'
|
||||||
|
import { onShow } from '@dcloudio/uni-app'
|
||||||
|
import { api } from '../../config'
|
||||||
|
|
||||||
|
const grades = ['大一', '大二', '大三', '大四', '研一', '研二', '研三', '已毕业']
|
||||||
|
|
||||||
|
const step = ref('input')
|
||||||
|
const loading = ref(false)
|
||||||
|
const chatLoading = ref(false)
|
||||||
|
const error = ref('')
|
||||||
|
const result = ref(null)
|
||||||
|
const chatMsg = ref('')
|
||||||
|
const chatHistory = ref([])
|
||||||
|
|
||||||
|
const profile = reactive({
|
||||||
|
major: '',
|
||||||
|
grade: '',
|
||||||
|
interests: '',
|
||||||
|
gpa: '',
|
||||||
|
goal: '',
|
||||||
|
})
|
||||||
|
|
||||||
|
const token = ref('')
|
||||||
|
onShow(() => {
|
||||||
|
token.value = uni.getStorageSync('token') || ''
|
||||||
|
if (!token.value) {
|
||||||
|
uni.showModal({
|
||||||
|
title: '请先登录',
|
||||||
|
content: '需要登录后才能使用择业顾问功能',
|
||||||
|
confirmText: '去登录',
|
||||||
|
success: (r) => { if (r.confirm) uni.navigateTo({ url: '/pages/login/login' }) },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const doAnalyze = async () => {
|
||||||
|
if (!profile.major.trim()) {
|
||||||
|
error.value = '请填写你的专业'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
loading.value = true
|
||||||
|
error.value = ''
|
||||||
|
try {
|
||||||
|
const res = await uni.request({
|
||||||
|
url: api('/career-advice/analyze'),
|
||||||
|
method: 'POST',
|
||||||
|
data: { ...profile },
|
||||||
|
header: { 'Authorization': `Bearer ${token.value}`, 'Content-Type': 'application/json' },
|
||||||
|
})
|
||||||
|
if (res.statusCode === 200) {
|
||||||
|
if (res.data.error) {
|
||||||
|
error.value = res.data.error
|
||||||
|
return
|
||||||
|
}
|
||||||
|
result.value = res.data
|
||||||
|
step.value = 'result'
|
||||||
|
chatHistory.value = [
|
||||||
|
{ role: 'user', content: `我是${profile.major}专业${profile.grade ? '的' + profile.grade : ''}学生,想了解职业发展方向` },
|
||||||
|
{ role: 'assistant', content: res.data.reply },
|
||||||
|
]
|
||||||
|
} else {
|
||||||
|
error.value = (res.data && res.data.message) || '分析失败,请稍后重试'
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
error.value = '网络错误,请检查网络连接'
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const doChat = async () => {
|
||||||
|
if (!chatMsg.value.trim() || chatLoading.value) return
|
||||||
|
const msg = chatMsg.value.trim()
|
||||||
|
chatMsg.value = ''
|
||||||
|
chatHistory.value.push({ role: 'user', content: msg })
|
||||||
|
chatLoading.value = true
|
||||||
|
try {
|
||||||
|
const res = await uni.request({
|
||||||
|
url: api('/career-advice/chat'),
|
||||||
|
method: 'POST',
|
||||||
|
data: { message: msg, history: chatHistory.value.slice(0, -1) },
|
||||||
|
header: { 'Authorization': `Bearer ${token.value}`, 'Content-Type': 'application/json' },
|
||||||
|
})
|
||||||
|
if (res.statusCode === 200) {
|
||||||
|
chatHistory.value.push({ role: 'assistant', content: res.data.reply || (res.data.error || '') })
|
||||||
|
} else {
|
||||||
|
chatHistory.value.push({ role: 'assistant', content: '回复失败,请稍后重试' })
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
chatHistory.value.push({ role: 'assistant', content: '网络错误,请检查网络连接' })
|
||||||
|
} finally {
|
||||||
|
chatLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const goInterview = (position) => {
|
||||||
|
uni.navigateTo({ url: `/pages/interview/interview?position=${encodeURIComponent(position)}` })
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.page { min-height: 100vh; background: var(--color-bg); padding-bottom: 40rpx; }
|
||||||
|
|
||||||
|
/* ===== Input ===== */
|
||||||
|
.input-wrap { padding: 0 32rpx; }
|
||||||
|
.hero { display: flex; flex-direction: column; align-items: center; padding: 48rpx 0 32rpx; }
|
||||||
|
.hero-icon { font-size: 72rpx; }
|
||||||
|
.hero-title { font-size: 40rpx; font-weight: 700; color: var(--color-text); margin-top: 16rpx; }
|
||||||
|
.hero-desc { font-size: 26rpx; color: var(--color-secondary); margin-top: 8rpx; }
|
||||||
|
|
||||||
|
.form-card { background: #fff; border-radius: var(--radius-lg); padding: 32rpx; box-shadow: var(--shadow-sm); }
|
||||||
|
.form-group { margin-bottom: 28rpx; }
|
||||||
|
.form-group:last-child { margin-bottom: 0; }
|
||||||
|
.form-label { font-size: 26rpx; font-weight: 600; color: var(--color-text); display: block; margin-bottom: 12rpx; }
|
||||||
|
.required { color: var(--color-error); }
|
||||||
|
.form-input { width: 100%; height: 80rpx; border: 2rpx solid var(--color-border); border-radius: var(--radius-md); padding: 0 24rpx; font-size: 26rpx; color: var(--color-text); box-sizing: border-box; background: #FAFBFC; }
|
||||||
|
.select-trigger { display: flex; align-items: center; }
|
||||||
|
.form-textarea { width: 100%; height: 160rpx; border: 2rpx solid var(--color-border); border-radius: var(--radius-md); padding: 20rpx 24rpx; font-size: 26rpx; color: var(--color-text); box-sizing: border-box; background: #FAFBFC; resize: none; }
|
||||||
|
|
||||||
|
.submit-btn { width: 100%; height: 88rpx; background: linear-gradient(135deg, var(--color-gradient-start), var(--color-gradient-mid)); color: #fff; font-size: 30rpx; font-weight: 600; border-radius: var(--radius-lg); margin-top: 32rpx; display: flex; align-items: center; justify-content: center; border: none; }
|
||||||
|
.submit-btn:active { transform: scale(0.98); opacity: 0.9; }
|
||||||
|
.submit-btn[disabled] { opacity: 0.5; }
|
||||||
|
.error-text { display: block; text-align: center; color: var(--color-error); font-size: 24rpx; margin-top: 16rpx; }
|
||||||
|
|
||||||
|
/* ===== Result ===== */
|
||||||
|
.result-wrap { padding: 0 32rpx; }
|
||||||
|
.result-header { display: flex; flex-direction: column; align-items: center; padding: 32rpx 0; }
|
||||||
|
.result-icon { font-size: 56rpx; }
|
||||||
|
.result-title { font-size: 34rpx; font-weight: 700; color: var(--color-text); margin-top: 12rpx; }
|
||||||
|
|
||||||
|
.advice-card { background: linear-gradient(135deg, #EEF2FF 0%, #E0E7FF 100%); border-radius: var(--radius-lg); padding: 32rpx; margin-bottom: 24rpx; }
|
||||||
|
.advice-content { font-size: 26rpx; color: #374151; line-height: 1.8; white-space: pre-wrap; }
|
||||||
|
|
||||||
|
.section { margin-bottom: 24rpx; }
|
||||||
|
.section-title { font-size: 30rpx; font-weight: 700; color: var(--color-text); display: block; margin-bottom: 16rpx; }
|
||||||
|
|
||||||
|
.path-card { display: flex; align-items: center; background: #fff; border-radius: var(--radius-lg); padding: 24rpx; margin-bottom: 16rpx; box-shadow: var(--shadow-sm); }
|
||||||
|
.path-card:active { transform: scale(0.98); background: #F9FAFB; }
|
||||||
|
.path-rank { width: 48rpx; height: 48rpx; background: linear-gradient(135deg, var(--color-gradient-start), var(--color-gradient-mid)); color: #fff; font-size: 24rpx; font-weight: 700; border-radius: 50%; display: flex; align-items: center; justify-content: center; flex-shrink: 0; margin-right: 16rpx; }
|
||||||
|
.path-body { flex: 1; }
|
||||||
|
.path-name { font-size: 28rpx; font-weight: 600; color: var(--color-text); display: block; }
|
||||||
|
.path-reason { font-size: 22rpx; color: var(--color-secondary); margin-top: 4rpx; display: block; line-height: 1.4; }
|
||||||
|
.path-score-wrap { display: flex; flex-direction: column; align-items: center; margin-left: 12rpx; flex-shrink: 0; }
|
||||||
|
.path-score-label { font-size: 20rpx; color: var(--color-secondary); }
|
||||||
|
.path-score { font-size: 32rpx; font-weight: 700; color: var(--color-primary); }
|
||||||
|
.path-salary { font-size: 20rpx; color: var(--color-success); margin-top: 2rpx; }
|
||||||
|
.path-hint { font-size: 22rpx; color: var(--color-secondary); text-align: center; margin-top: -8rpx; margin-bottom: 16rpx; }
|
||||||
|
|
||||||
|
.action-bar { display: flex; gap: 16rpx; }
|
||||||
|
.action-btn { flex: 1; height: 80rpx; font-size: 28rpx; font-weight: 600; border-radius: var(--radius-md); display: flex; align-items: center; justify-content: center; }
|
||||||
|
.chat-btn { background: var(--color-primary); color: #fff; }
|
||||||
|
.retry-btn { background: #fff; color: var(--color-text); border: 2rpx solid var(--color-border); }
|
||||||
|
|
||||||
|
/* ===== Chat ===== */
|
||||||
|
.chat-wrap { display: flex; flex-direction: column; height: 100vh; }
|
||||||
|
.chat-header { display: flex; align-items: center; padding: 24rpx 32rpx; background: #fff; border-bottom: 1rpx solid var(--color-border); }
|
||||||
|
.chat-back { font-size: 28rpx; color: var(--color-primary); margin-right: 24rpx; }
|
||||||
|
.chat-title { font-size: 30rpx; font-weight: 700; color: var(--color-text); flex: 1; }
|
||||||
|
|
||||||
|
.chat-msgs { flex: 1; padding: 24rpx 32rpx; overflow-y: auto; }
|
||||||
|
.msg-bubble { max-width: 80%; margin-bottom: 20rpx; padding: 20rpx 24rpx; border-radius: var(--radius-md); font-size: 26rpx; line-height: 1.6; }
|
||||||
|
.msg-user { background: var(--color-primary); color: #fff; align-self: flex-end; margin-left: auto; border-radius: var(--radius-md) var(--radius-md) 4rpx var(--radius-md); }
|
||||||
|
.msg-ai { background: #fff; color: var(--color-text); box-shadow: var(--shadow-sm); border-radius: var(--radius-md) var(--radius-md) var(--radius-md) 4rpx; }
|
||||||
|
.typing { color: var(--color-secondary); }
|
||||||
|
|
||||||
|
.chat-input-bar { display: flex; align-items: center; padding: 16rpx 32rpx; background: #fff; border-top: 1rpx solid var(--color-border); gap: 16rpx; }
|
||||||
|
.chat-input { flex: 1; height: 72rpx; border: 2rpx solid var(--color-border); border-radius: 36rpx; padding: 0 24rpx; font-size: 26rpx; }
|
||||||
|
.chat-send { height: 72rpx; padding: 0 32rpx; background: var(--color-primary); color: #fff; font-size: 26rpx; font-weight: 600; border-radius: 36rpx; display: flex; align-items: center; justify-content: center; }
|
||||||
|
.chat-send[disabled] { opacity: 0.5; }
|
||||||
|
</style>
|
||||||
@@ -41,6 +41,12 @@
|
|||||||
<!-- 菜单列表 -->
|
<!-- 菜单列表 -->
|
||||||
<view class="menu-area">
|
<view class="menu-area">
|
||||||
<view class="menu-group">
|
<view class="menu-group">
|
||||||
|
<view class="menu-item" @click="requireLogin(goCareer, '择业顾问')">
|
||||||
|
<view class="menu-icon-wrap wrap-teal"><text class="menu-icon">🧭</text></view>
|
||||||
|
<text class="menu-text">择业顾问</text>
|
||||||
|
<text class="menu-tag">NEW</text>
|
||||||
|
<text class="menu-arrow">›</text>
|
||||||
|
</view>
|
||||||
<view class="menu-item" @click="requireLogin(goHistory, '面试记录')">
|
<view class="menu-item" @click="requireLogin(goHistory, '面试记录')">
|
||||||
<view class="menu-icon-wrap wrap-blue"><text class="menu-icon">📋</text></view>
|
<view class="menu-icon-wrap wrap-blue"><text class="menu-icon">📋</text></view>
|
||||||
<text class="menu-text">面试记录</text>
|
<text class="menu-text">面试记录</text>
|
||||||
@@ -104,7 +110,6 @@ const refreshState = () => {
|
|||||||
try { const s = uni.getStorageSync('userInfo'); if (s) userInfo.value = JSON.parse(s) } catch(e) {}
|
try { const s = uni.getStorageSync('userInfo'); if (s) userInfo.value = JSON.parse(s) } catch(e) {}
|
||||||
loadStats()
|
loadStats()
|
||||||
checkAdmin()
|
checkAdmin()
|
||||||
// Fetch fresh user info from API to update stale cache (e.g. credits changed after interview)
|
|
||||||
fetchUserInfo()
|
fetchUserInfo()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,7 +120,7 @@ const fetchUserInfo = async () => {
|
|||||||
userInfo.value = res.data
|
userInfo.value = res.data
|
||||||
uni.setStorageSync('userInfo', JSON.stringify(res.data))
|
uni.setStorageSync('userInfo', JSON.stringify(res.data))
|
||||||
}
|
}
|
||||||
} catch(e) { /* silent - cached data is fallback */ }
|
} catch(e) { /* silent */ }
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(refreshState)
|
onMounted(refreshState)
|
||||||
@@ -143,8 +148,9 @@ const checkAdmin = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const goLogin = () => uni.navigateTo({ url: '/pages/login/login' })
|
const goLogin = () => uni.navigateTo({ url: '/pages/login/login' })
|
||||||
|
const goCareer = () => uni.navigateTo({ url: '/pages/career/career' })
|
||||||
const goHistory = () => uni.switchTab({ url: '/pages/history/history' })
|
const goHistory = () => uni.switchTab({ url: '/pages/history/history' })
|
||||||
const goReviewReview = () => uni.navigateTo({ url: "/pages/review/review" })
|
const goReviewReview = () => uni.navigateTo({ url: '/pages/review/review' })
|
||||||
const goVip = () => uni.navigateTo({ url: '/pages/member/member' })
|
const goVip = () => uni.navigateTo({ url: '/pages/member/member' })
|
||||||
const goResume = () => uni.navigateTo({ url: '/pages/resume/resume' })
|
const goResume = () => uni.navigateTo({ url: '/pages/resume/resume' })
|
||||||
const goShare = () => uni.navigateTo({ url: '/pages/share/share' })
|
const goShare = () => uni.navigateTo({ url: '/pages/share/share' })
|
||||||
@@ -184,7 +190,6 @@ const doLogout = () => {
|
|||||||
.guest-icon { font-size: 40rpx; }
|
.guest-icon { font-size: 40rpx; }
|
||||||
.guest-info { flex: 1; }
|
.guest-info { flex: 1; }
|
||||||
.guest-name { font-size: 30rpx; font-weight: 600; color: #FFFFFF; }
|
.guest-name { font-size: 30rpx; font-weight: 600; color: #FFFFFF; }
|
||||||
.guest-hint { font-size: 22rpx; color: rgba(255,255,255,0.7); margin-top: 4rpx; }
|
|
||||||
|
|
||||||
.menu-area { padding: 0 32rpx 32rpx; margin-top: -40rpx; }
|
.menu-area { padding: 0 32rpx 32rpx; margin-top: -40rpx; }
|
||||||
.menu-group { background: #FFFFFF; border-radius: var(--radius-lg); overflow: hidden; margin-bottom: 24rpx; box-shadow: var(--shadow-sm); }
|
.menu-group { background: #FFFFFF; border-radius: var(--radius-lg); overflow: hidden; margin-bottom: 24rpx; box-shadow: var(--shadow-sm); }
|
||||||
@@ -194,12 +199,14 @@ const doLogout = () => {
|
|||||||
.menu-icon-wrap { width: 60rpx; height: 60rpx; border-radius: var(--radius-md); display: flex; align-items: center; justify-content: center; margin-right: 20rpx; flex-shrink: 0; }
|
.menu-icon-wrap { width: 60rpx; height: 60rpx; border-radius: var(--radius-md); display: flex; align-items: center; justify-content: center; margin-right: 20rpx; flex-shrink: 0; }
|
||||||
.menu-icon { font-size: 28rpx; }
|
.menu-icon { font-size: 28rpx; }
|
||||||
.menu-text { flex: 1; font-size: 28rpx; color: var(--color-text); font-weight: 500; }
|
.menu-text { flex: 1; font-size: 28rpx; color: var(--color-text); font-weight: 500; }
|
||||||
|
.menu-tag { font-size: 18rpx; color: #fff; background: var(--color-primary); padding: 2rpx 12rpx; border-radius: 20rpx; margin-right: 12rpx; }
|
||||||
.menu-arrow { font-size: 32rpx; color: #D1D5DB; }
|
.menu-arrow { font-size: 32rpx; color: #D1D5DB; }
|
||||||
.wrap-blue { background: #EEF2FF; }
|
.wrap-blue { background: #EEF2FF; }
|
||||||
.wrap-purple { background: #F5F3FF; }
|
.wrap-purple { background: #F5F3FF; }
|
||||||
.wrap-green { background: #ECFDF5; }
|
.wrap-green { background: #ECFDF5; }
|
||||||
.wrap-orange { background: #FFF7ED; }
|
.wrap-orange { background: #FFF7ED; }
|
||||||
.wrap-gray { background: #F3F4F6; }
|
.wrap-gray { background: #F3F4F6; }
|
||||||
|
.wrap-teal { background: #E6FFFA; }
|
||||||
.logout-wrap { margin-top: 8rpx; }
|
.logout-wrap { margin-top: 8rpx; }
|
||||||
.logout-btn { background: #FFFFFF; color: var(--color-error); font-size: 28rpx; font-weight: 500; border-radius: var(--radius-md); height: 88rpx; line-height: 88rpx; border: 2rpx solid #FECACA; }
|
.logout-btn { background: #FFFFFF; color: var(--color-error); font-size: 28rpx; font-weight: 500; border-radius: var(--radius-md); height: 88rpx; line-height: 88rpx; border: 2rpx solid #FECACA; }
|
||||||
.logout-btn:active { background: #FEF2F2; transform: scale(0.96); }
|
.logout-btn:active { background: #FEF2F2; transform: scale(0.96); }
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ async function request<T = any>(url: string, method: string = 'POST', data?: any
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const apiService = {
|
||||||
user: {
|
user: {
|
||||||
sendCode: (phone: string) => request(API_ENDPOINTS.USER.SEND_CODE, 'POST', { phone }),
|
sendCode: (phone: string) => request(API_ENDPOINTS.USER.SEND_CODE, 'POST', { phone }),
|
||||||
login: (phone: string, code: string) => request(API_ENDPOINTS.USER.LOGIN, 'POST', { phone, code }),
|
login: (phone: string, code: string) => request(API_ENDPOINTS.USER.LOGIN, 'POST', { phone, code }),
|
||||||
@@ -89,14 +90,20 @@ async function request<T = any>(url: string, method: string = 'POST', data?: any
|
|||||||
records: () => request(API_ENDPOINTS.SHARE.RECORDS, 'GET', undefined, true),
|
records: () => request(API_ENDPOINTS.SHARE.RECORDS, 'GET', undefined, true),
|
||||||
visitors: () => request(API_ENDPOINTS.SHARE.VISITORS, 'GET', undefined, true),
|
visitors: () => request(API_ENDPOINTS.SHARE.VISITORS, 'GET', undefined, true),
|
||||||
},
|
},
|
||||||
|
|
||||||
review: {
|
review: {
|
||||||
list: (page = 1, limit = 20) =>
|
list: (page = 1, limit = 20) =>
|
||||||
request(`${API_ENDPOINTS.REVIEW.LIST}?page=${page}&limit=${limit}`, "GET", undefined, true),
|
request(`${API_ENDPOINTS.REVIEW.LIST}?page=${page}&limit=${limit}`, 'GET', undefined, true),
|
||||||
detail: (id: string) => request(API_ENDPOINTS.REVIEW.DETAIL(id), "GET", undefined, true),
|
detail: (id: string) => request(API_ENDPOINTS.REVIEW.DETAIL(id), 'GET', undefined, true),
|
||||||
delete: (id: string) => request(API_ENDPOINTS.REVIEW.DELETE(id), "DELETE", undefined, true),
|
delete: (id: string) => request(API_ENDPOINTS.REVIEW.DELETE(id), 'DELETE', undefined, true),
|
||||||
submitText: (position: string, text: string, company?: string) =>
|
submitText: (position: string, text: string, company?: string) =>
|
||||||
request(API_ENDPOINTS.REVIEW.TEXT, "POST", { position, text, company: company || "" }, true),
|
request(API_ENDPOINTS.REVIEW.TEXT, 'POST', { position, text, company: company || '' }, true),
|
||||||
|
},
|
||||||
|
career: {
|
||||||
|
analyze: (profile: { major: string; grade?: string; interests?: string; gpa?: string; goal?: string }) =>
|
||||||
|
request(API_ENDPOINTS.CAREER.ANALYZE, 'POST', profile, true),
|
||||||
|
chat: (message: string, history: { role: string; content: string }[]) =>
|
||||||
|
request(API_ENDPOINTS.CAREER.CHAT, 'POST', { message, history }, true),
|
||||||
|
positions: () => request(API_ENDPOINTS.CAREER.POSITIONS, 'GET', undefined, true),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user