81 Commits
Author SHA1 Message Date
xuxin 1e103627f8 加一个超级管理员登陆页面 2026-08-05 14:32:33 +08:00
xuxin 8b9ebd6ddc 删掉之前测试时用过的一个无效旧token 2026-07-29 15:18:24 +08:00
xuxin 5abd6d27d6 联系二维码图片存oss图片链接缓存oss里的 2026-07-27 14:31:02 +08:00
xuxin bb7a8c55b2 4个mvp补丁 2026-07-24 18:15:31 +08:00
xuxin a0f01dd697 插件上传谷歌商店验证文件 2026-07-22 10:01:10 +08:00
xuxin 58a027296a 隐私协议单独加个页面 2026-07-21 11:29:26 +08:00
xuxin 010a80be81 隐私协议单独加个页面 2026-07-21 11:20:01 +08:00
xuxin 6c03793daf 去掉offer派插件test和www不同环境域名的通信隔离 2026-07-17 20:19:59 +08:00
xuxin 33ecccdcb0 bug处理加文字样式微调 2026-07-17 20:00:24 +08:00
xuxin d429bae98a 备案号 2026-07-17 16:49:46 +08:00
xuxin 0b9f2e7333 备案号 2026-07-17 16:48:39 +08:00
xuxin db46766d48 个人资料教育经历学历类型保存bug,和岗位为列表页查询条件添加公司类型分类 2026-07-17 16:28:50 +08:00
xuxin 27d2592d06 bug修改 2026-07-17 15:12:41 +08:00
xuxin c81c9b79dd 没有个人资料时检测跳转登陆页,登陆页改造,添加新的业务配置文件来控制功能开关 2026-07-17 12:01:34 +08:00
xuxin 5f4aab8d04 bug处理 2026-07-16 18:36:40 +08:00
xuxin 15971bffd9 友盟跟踪记录 2026-07-16 16:30:07 +08:00
xuxin bf84da7783 bug处理,插件版本同步 2026-07-15 16:36:20 +08:00
xuxin 0911760e27 3个需求处理和插件通信组件引入(和插件共用的一个ts) 2026-07-15 09:49:21 +08:00
xuxin 66026f211c bug处理 2026-07-14 11:44:09 +08:00
xuxin 7abe158dbb bug处理 2026-07-13 23:01:49 +08:00
xuxin aed2535ab2 登陆和首页微调 2026-07-13 20:56:06 +08:00
xuxin 5f723e5573 ai助手投递偏好设置修复 2026-07-10 16:57:43 +08:00
xuxin 8cd21e7b98 ai助手投递偏好设置修复 2026-07-10 16:29:31 +08:00
xuxin e8e2e09e62 样式小bug修改-01 2026-07-10 11:38:56 +08:00
xuxin 4be35ced80 ai助手本地不缓存没有完成的投递记录 2026-07-10 10:37:35 +08:00
xuxin 0e5c7d14d0 AI投递助手样式优化 2026-07-10 10:01:07 +08:00
xuxin cf268520e1 AI投递助手 2026-07-10 02:08:41 +08:00
xuxin cd64ded6ae AI润色用新加载特效 2026-07-09 16:26:29 +08:00
xuxin ec6ba7ee35 简历详情样式微调 2026-07-09 16:04:25 +08:00
xuxin 72339f332d 简历详情样式微调 2026-07-09 15:27:31 +08:00
xuxin b25f2dde3a 左侧菜单微调 2026-07-09 11:45:36 +08:00
xuxin 38e3957da2 导师搜索标签 2026-07-09 10:47:14 +08:00
xuxin ed959e6e6d 一键修复样式微调 2026-07-08 23:20:22 +08:00
xuxin f0b29d9abb 导师列表导师详情右侧广告,去投递弹窗等 2026-07-08 22:48:58 +08:00
xuxin c94a96d14e 个人资料,简历 2026-07-08 18:58:27 +08:00
xuxin 851310a901 左侧菜单,邀请,设置,和会员相关 2026-07-07 23:40:20 +08:00
xuxin b740e0cd39 AI定制专属简历和简历预览调整 2026-07-07 11:34:14 +08:00
xuxin 9e791345f2 移动端二维码图片保存 2026-07-06 21:44:32 +08:00
xuxin 73f5bde68f 移动端首页 2026-07-06 21:34:40 +08:00
xuxin 78bf7fbebc 生成定制简历部分默认优化选项全选 2026-07-06 18:58:16 +08:00
xuxin 6024ea27e9 职位页微调 2026-07-03 23:00:37 +08:00
xuxin f90c8be2a9 导师相关接口换offer派接口,职位列表和职位详情,Nova聊天侧边栏UI重写 2026-07-03 19:55:53 +08:00
xuxin a1a27073e4 导师列表和导师详情 2026-07-03 11:55:44 +08:00
xuxin 51be3e5585 登陆也左侧调整 2026-07-01 15:17:19 +08:00
xuxin 8773c65b28 全项目加载动效重做和简历经历区域的时间和编辑按钮同位置显示切换 2026-06-30 17:09:09 +08:00
xuxin 0aebde7bd3 登陆页左侧新广告特效 2026-06-28 19:23:54 +08:00
xuxin 0888d9fa31 登陆页左侧新广告特效 2026-06-28 14:53:37 +08:00
zk 4eb730208e 添加测试环境部署配置 2026-06-25 16:10:32 +08:00
xuxin 8143a89dab 新版交互调整 2026-06-24 19:03:41 +08:00
xuxin 065553f53e 登陆时没有简历先上传一份简历 2026-06-09 14:39:59 +08:00
xuxin 1c3ea6aa3c 登陆页操作流程调整 2026-06-05 17:56:44 +08:00
xuxin c17f75c707 意向字段全部改成使用招聘分类 2026-06-05 17:27:07 +08:00
xuxin 8d6282c3e6 验证码用户手动输入期间延长倒计时,防止用户输入一半之前60秒结束终止了输验证码步骤 2026-06-05 09:54:27 +08:00
xuxin 403e5e9fc9 401状态拦截 2026-06-04 17:47:28 +08:00
xuxin 540d61b6a0 新增登陆页面,去掉之前的窗口登陆 2026-06-04 17:41:07 +08:00
xuxin 2844d99ae1 招聘分类定义修改 2026-06-04 14:54:55 +08:00
xuxin 0783ecb570 添加招聘分类筛选功能 2026-06-04 14:43:33 +08:00
xuxin ad9c448fc7 首页UI微调 2026-06-04 11:12:34 +08:00
xuxin 5ef8b3a9df 首页UI微调 2026-06-03 17:28:09 +08:00
xuxin 0fcc19f3f8 首页UI微调 2026-06-03 12:12:59 +08:00
xuxin e6ae57e5fd 首页和页面缩放调整 2026-06-03 11:03:13 +08:00
xuxin 557a6f30f2 sass警告 2026-06-02 14:21:13 +08:00
xuxin dde72be9de 职位列表去投递定时提醒去AI助手弹窗 2026-06-01 15:46:30 +08:00
xuxin ddb67cfb6f AI助手聊天时候的历史消息组装去掉列表和投递进度等非对话聊天内容 2026-05-29 17:22:22 +08:00
xuxin 59ac8ab783 定制简历浏览器记录缓存加IndexDB存储工具封装,修复同一个页面的不同协议切换问题 2026-05-29 15:45:16 +08:00
xuxin 0d1d080cc1 消息通知左侧列表的时间 2026-05-28 21:26:01 +08:00
xuxin a1633978c5 首页和设置会员页,购买页的协议 2026-05-28 21:22:30 +08:00
xuxin 0e6fde08c7 页面右侧聊天助手的ai输出结果加上md文档格式显示 2026-05-28 17:41:23 +08:00
xuxin 96071d0105 定制简历AI编辑撤回修改按钮展示逻辑,和消息通知显示时间 2026-05-28 16:28:18 +08:00
xuxin e34bb2a21e 聊天助手ai回答内容换行符转换 2026-05-28 15:00:30 +08:00
xuxin 3dbeddcb8f 求职助手配置 2026-05-28 11:23:48 +08:00
xuxin f52a7c56f7 AI助手引导步骤返回上一步和正式使用AI助手时的再次设置功能,和投递过程的其他地方按钮点击保护 2026-05-28 11:09:38 +08:00
zk a9638fc7ec 跟新 nginx 流量切换方式 2026-05-28 09:46:03 +08:00
zk 91992a2443 修改 nginx 文件修改方式,不在删除,仅仅修改 2026-05-27 18:37:06 +08:00
zk b666f0047d 修改流浪切换方式 2026-05-27 16:29:37 +08:00
zk 42768cc4c5 删除预渲染相关文件 2026-05-27 16:05:45 +08:00
zk 833533f780 重构部署方案 远程发版 2026-05-27 15:44:26 +08:00
xuxin 8b6d424b2f 带文件拖动上传的新版简历上传功能,独立文件操作组件,简历列表页和个人资料页有调用 2026-05-26 19:20:34 +08:00
xuxin 4b16341f92 Merge branch 'xuxin_2026-05-26' 2026-05-26 19:19:47 +08:00
zk 12baa93e58 Merge remote-tracking branch 'origin/master' 2026-05-26 11:54:46 +08:00
zk a3740688ab 修改 jenkins 代码仓库地址账号 2026-05-26 11:54:37 +08:00
235 changed files with 28960 additions and 10927 deletions
+3
View File
@@ -1,2 +1,5 @@
# 开发环境 — 请求走 vite proxy
VITE_API_BASE_URL=/api
# 见识星球接口 — 开发环境走 vite proxy 中转解决跨域
VITE_JIANSHI_BASE_URL=/jianshi-api
+3
View File
@@ -1,2 +1,5 @@
# 生产环境 — 同域路径代理,不需要完整域名
VITE_API_BASE_URL=/api
# 见识星球接口 — 生产环境直接请求真实域名
VITE_JIANSHI_BASE_URL=https://www.jianshixingqiu.com/proxy-api
+6 -3
View File
@@ -16,10 +16,13 @@ inclusion: always
## 编码规范
- 颜色用尽量用variables.scss里的统一颜色变量,特别是背景,按钮,文字的颜色
- 除了Home.vue首页外,其他页面我上传了截图要写页面或组件的,需要参考图片里文字和布局结构,在保证美观前提上自由发挥
- 全局用1rem=100px的格式并注意对某些特殊元素组件的line-height行高影响,纵布局如非必要不用flex-direction: column布局
- 除了Home.vue首页外,其他页面我上传了截图要写页面或组件的,需要参考图片里文字和布局结构,在保证美观前提上自由发挥(如果我粘贴了图片内容里的完整样式过来,那就要参考我给的样式结合图片里的结构和样式代码保证还原度)
- 全局用1rem=100px的格式并注意对某些特殊元素组件的line-height行高影响,纵布局能用div自动排列加上下margin的就用div排列,不要用display:flex加flex-direction: column布局,除非是像这个盒子里有个子元素需要固定在底部这样需要明确高度的情况就用flex-direction: column,否则没有高度要求内部子元素只需要正常从上到下排列纵布局只用div
- 如果是建一个组件,这个组件看我说是用在views里哪个页面的,比如用在Profile.vue里的组件,组件名字最前面要加Profile,而且整个组件的命名不能过度简化,要容易看懂组件的用途;如果检测到某种名字开头的组件数量比如Profile开头的超过15个,就在components里新建个类似profile这样的页面名字的文件夹,把这类命名的组件都移到文件夹里并查找更新组件所有被引用地方的文件地址
## 注意事项
- 页面结构和ts的常量变量和方法都要加中文注释
- 页面结构和ts的常量变量和方法都要加中文注释kiro编程工具沟通要回复中文
- 新建 SCSS 文件如果使用了 variables.scss 中的变量(如 `$bg-white``$accent` 等),必须在文件顶部加 `@use '../variables' as *;`,否则通过 `@use` 方式引入 index.scss 时变量作用域不会穿透,会报 `Undefined variable` 错误
- 因为项目用了 rem(1rem=100px)适配方案,所有 Vue 页面和组件文件的最外层盒子都要加 `font-size: 0.14rem`,避免页面样式中受浏览器默认 rem 行高影响导致文字和布局异常
- 需要占满视口高度的元素(页面主容器、侧边栏、聊天面板等)禁止直接写 `height: 100vh`,必须使用 `height: var(--app-height, 100vh)`。原因:项目在小屏下使用 `transform: scale` 缩放,`100vh` 是缩放后的视口高度而非设计稿高度,会导致底部留白。`--app-height``src/plugins/remAdapt.ts` 动态注入,值为 `视口高度 / scale`PC 端等于 `100vh`
-2
View File
@@ -1,5 +1,3 @@
# syntax=docker/dockerfile:1
# ==================== 第一阶段:构建 ====================
FROM node:22-alpine AS builder
WORKDIR /build
Vendored
+90 -96
View File
@@ -1,15 +1,8 @@
/**
* OfferPie Web 前端蓝绿部署流水线
*
* 工作目录说明:
* - Jenkins 会自动为每个项目创建独立的工作空间:/var/jenkins_home/workspace/<项目名>/
* - docker-compose.yml、Dockerfile、nginx.conf 都在项目根目录,直接执行即可
* - nginx.conf 首次部署时 docker cp 进 nginx 容器
*
* 内层 Nginx 职责:
* - 蓝绿切换(前端静态文件 serve)
* - /api/ 代理到 Java 后端(10202
* - /ai-api/ 代理到 Python AI10502),rewrite 去掉前缀
* 架构:Jenkins 本地编译 → scp 镜像到目标机 → SSH 远程蓝绿切换
* 目标机目录:/opt/offerpie/web/
*/
pipeline {
agent any
@@ -19,14 +12,28 @@ pipeline {
}
environment {
CONTAINER_PREFIX = 'offerpie-web' // 容器名前缀,拼接 -blue / -green / -nginx
HEALTH_URL = 'http://localhost:80/' // 前端静态页面健康检查
// 目标服务器配置
DEPLOY_HOST = '8.148.237.97'
DEPLOY_PORT = '22'
DEPLOY_USER = 'root'
DEPLOY_PASS = 'Mzpy520@126.com'
// 项目配置
IMAGE_NAME = 'offerpie-web'
IMAGE_TAG = 'latest'
CONTAINER_PREFIX = 'offerpie-web'
REMOTE_DIR = '/opt/offerpie/web'
HEALTH_URL = 'http://localhost:80/'
// SSH 命令前缀
SSH_CMD = "sshpass -p '${DEPLOY_PASS}' ssh -o StrictHostKeyChecking=no -p ${DEPLOY_PORT} ${DEPLOY_USER}@${DEPLOY_HOST}"
SCP_CMD = "sshpass -p '${DEPLOY_PASS}' scp -o StrictHostKeyChecking=no -P ${DEPLOY_PORT}"
}
stages {
stage('开始提示') {
stage('环境检查') {
steps {
echo "OfferPie Web 前端开始构建"
sh 'sshpass -V'
}
}
@@ -34,23 +41,41 @@ pipeline {
steps {
echo "拉取 ${params.BRANCH} 分支代码"
git branch: "${params.BRANCH}",
credentialsId: 'ef5fffc1-9b35-403d-9ca6-e1b73eb0e45a',
url: 'https://codeup.aliyun.com/5f0ed3b9769820a3e817dee2/offerpie/offerpie_web.git'
credentialsId: 'gitea-fab089c1-b55d-4b58-9fad',
url: 'http://git.jianshixingqiu.com/offerpai/offerpai_web.git'
}
}
stage('本地编译') {
steps {
echo "开始构建镜像"
sh "docker build -t ${IMAGE_NAME}:${IMAGE_TAG} ."
echo "导出镜像"
sh "docker save -o ${IMAGE_NAME}.tar ${IMAGE_NAME}:${IMAGE_TAG}"
}
}
stage('文件传输') {
steps {
echo "传输文件到目标服务器"
sh "${SSH_CMD} 'mkdir -p ${REMOTE_DIR}'"
sh "${SCP_CMD} ${IMAGE_NAME}.tar ${DEPLOY_USER}@${DEPLOY_HOST}:${REMOTE_DIR}/"
sh "${SCP_CMD} proxy_nginx.conf ${DEPLOY_USER}@${DEPLOY_HOST}:${REMOTE_DIR}/proxy_nginx.conf"
sh "${SCP_CMD} docker-compose.yml ${DEPLOY_USER}@${DEPLOY_HOST}:${REMOTE_DIR}/docker-compose.yml"
}
}
stage('加载镜像') {
steps {
sh "${SSH_CMD} 'docker load < ${REMOTE_DIR}/${IMAGE_NAME}.tar'"
}
}
/**
* 检测部署目标
* - 检查 blue 和 green 容器的运行状态
* - blue 在运行 → 部署 greengreen 在运行 → 部署 blue
* - 都不在或都在 → 默认部署 blue
*/
stage('检测部署目标') {
steps {
echo "检查当前容器状态"
script {
def blueRunning = sh(script: "docker ps -q -f name=${CONTAINER_PREFIX}-blue", returnStdout: true).trim()
def greenRunning = sh(script: "docker ps -q -f name=${CONTAINER_PREFIX}-green", returnStdout: true).trim()
def blueRunning = sh(script: "${SSH_CMD} 'docker ps -q -f name=${CONTAINER_PREFIX}-blue'", returnStdout: true).trim()
def greenRunning = sh(script: "${SSH_CMD} 'docker ps -q -f name=${CONTAINER_PREFIX}-green'", returnStdout: true).trim()
env.DEPLOY_TARGET = ''
@@ -71,70 +96,42 @@ pipeline {
}
}
/**
* 构建新版本
* - 先清理目标颜色的旧容器(如果残留)
* - docker-compose build 构建新镜像(含 pnpm build
*/
stage('构建新版本') {
steps {
script {
def existingContainer = sh(script: "docker ps -aq -f name=${CONTAINER_PREFIX}-${env.DEPLOY_TARGET}", returnStdout: true).trim()
if (existingContainer) {
echo "清理已存在的容器: ${CONTAINER_PREFIX}-${env.DEPLOY_TARGET}"
sh "docker rm -f ${CONTAINER_PREFIX}-${env.DEPLOY_TARGET}"
}
sh "docker-compose build ${env.DEPLOY_TARGET}"
}
}
}
/**
* 检查 Nginx
* - 检查 nginx 容器是否存在
* - 不存在则启动并复制配置文件(首次部署)
* - 存在则确保容器运行中
*/
stage('检查Nginx') {
steps {
script {
def nginxExists = sh(script: "docker ps -aq -f name=${CONTAINER_PREFIX}-nginx", returnStdout: true).trim()
if (!nginxExists) {
echo "首次部署,初始化 Nginx 容器"
sh "docker-compose up -d nginx"
sh "sleep 3"
// 复制配置文件到容器内
sh "docker cp proxy_nginx.conf ${CONTAINER_PREFIX}-nginx:/etc/nginx/nginx.conf"
sh "docker exec ${CONTAINER_PREFIX}-nginx nginx -s reload"
} else {
def nginxRunning = sh(script: "docker ps -q -f name=${CONTAINER_PREFIX}-nginx", returnStdout: true).trim()
if (!nginxRunning) {
echo "Nginx 容器已停止,重新启动"
sh "docker start ${CONTAINER_PREFIX}-nginx"
sh "sleep 2"
}
echo "Nginx 容器已存在且运行中"
}
}
}
}
stage('启动新版本') {
steps {
script {
sh "docker-compose up -d ${env.DEPLOY_TARGET}"
// 等待 Nginx 容器启动
def existingContainer = sh(script: "${SSH_CMD} 'docker ps -aq -f name=${CONTAINER_PREFIX}-${env.DEPLOY_TARGET}'", returnStdout: true).trim()
if (existingContainer) {
sh "${SSH_CMD} 'docker rm -f ${CONTAINER_PREFIX}-${env.DEPLOY_TARGET}'"
}
sh "${SSH_CMD} 'cd ${REMOTE_DIR} && docker compose up -d ${env.DEPLOY_TARGET}'"
sh 'sleep 5'
}
}
}
/**
* 健康检查
* - 进入容器检测静态页面是否正常返回
* - 最多重试 3 次,每次间隔 5 秒
* - 全部失败则终止部署流程
*/
stage('检查Nginx') {
steps {
script {
def nginxExists = sh(script: "${SSH_CMD} 'docker ps -aq -f name=${CONTAINER_PREFIX}-nginx'", returnStdout: true).trim()
if (!nginxExists) {
echo "首次部署,初始化 Nginx 容器"
sh "${SSH_CMD} 'cd ${REMOTE_DIR} && docker compose up -d nginx'"
sh 'sleep 3'
} else {
def nginxRunning = sh(script: "${SSH_CMD} 'docker ps -q -f name=${CONTAINER_PREFIX}-nginx'", returnStdout: true).trim()
if (!nginxRunning) {
echo "Nginx 容器已停止,重新启动"
sh "${SSH_CMD} 'docker start ${CONTAINER_PREFIX}-nginx'"
sh 'sleep 2'
}
echo "Nginx 容器已存在且运行中"
}
// docker cp proxy_nginx.conf 进容器
sh "${SSH_CMD} 'docker cp ${REMOTE_DIR}/proxy_nginx.conf ${CONTAINER_PREFIX}-nginx:/etc/nginx/nginx.conf'"
}
}
}
stage('健康检查') {
steps {
script {
@@ -144,7 +141,7 @@ pipeline {
while (retryCount < maxRetries && !healthy) {
try {
sh "docker exec ${CONTAINER_PREFIX}-${env.DEPLOY_TARGET} curl -f ${HEALTH_URL}"
sh "${SSH_CMD} 'docker exec ${CONTAINER_PREFIX}-${env.DEPLOY_TARGET} curl -f ${HEALTH_URL}'"
healthy = true
} catch (Exception e) {
retryCount++
@@ -163,38 +160,34 @@ pipeline {
}
}
/**
* 切换流量
* - 修改 nginx 配置中的 proxy_pass 指向新版本
* - nginx 不挂载宿主机文件,直接 sed -i 修改容器内配置
* - reload 使新配置生效,实现零停机切换
*/
stage('切换流量') {
steps {
script {
sh "docker exec ${CONTAINER_PREFIX}-nginx sed -i 's/proxy_pass http:\\/\\/\\(blue\\|green\\);/proxy_pass http:\\/\\/${env.DEPLOY_TARGET};/' /etc/nginx/nginx.conf"
sh "docker exec ${CONTAINER_PREFIX}-nginx nginx -s reload"
sh "${SSH_CMD} 'docker exec ${CONTAINER_PREFIX}-nginx sed -i \"s/proxy_pass http:\\/\\/\\(blue\\|green\\):80;/proxy_pass http:\\/\\/${env.DEPLOY_TARGET}:80;/\" /etc/nginx/nginx.conf'"
sh "${SSH_CMD} 'docker exec ${CONTAINER_PREFIX}-nginx nginx -s reload'"
echo "✅ 流量已切换到 ${env.DEPLOY_TARGET}"
}
}
}
/**
* 停止并删除旧版本
* - 不能用 docker-compose down,会删除所有服务(包括 nginx)
* - 用 docker rm -f 只删除指定的旧颜色容器
*/
stage('删除旧版本') {
steps {
script {
def otherExists = sh(script: "docker ps -aq -f name=${CONTAINER_PREFIX}-${env.OTHER_TARGET}", returnStdout: true).trim()
def otherExists = sh(script: "${SSH_CMD} 'docker ps -aq -f name=${CONTAINER_PREFIX}-${env.OTHER_TARGET}'", returnStdout: true).trim()
if (otherExists) {
sh "docker rm -f ${CONTAINER_PREFIX}-${env.OTHER_TARGET}"
sh "${SSH_CMD} 'docker rm -f ${CONTAINER_PREFIX}-${env.OTHER_TARGET}'"
echo "旧版本 ${env.OTHER_TARGET} 已删除"
}
}
}
}
stage('清理') {
steps {
sh "${SSH_CMD} 'rm -f ${REMOTE_DIR}/${IMAGE_NAME}.tar'"
sh "rm -f ${IMAGE_NAME}.tar"
}
}
}
post {
@@ -203,6 +196,7 @@ pipeline {
}
failure {
echo '❌ 部署失败,请检查日志'
sh "rm -f ${IMAGE_NAME}.tar || true"
}
}
}
+212
View File
@@ -0,0 +1,212 @@
/**
* OfferPie Web 前端蓝绿部署流水线(测试环境)
*
* 架构:Jenkins 本地编译 → scp 镜像到目标机 → SSH 远程蓝绿切换
* 目标机目录:/opt/offerpie/web/
*
* 与生产差异:
* - 部署目标为测试服务器
* - 默认分支 test
* - 镜像 tag = test(与生产 latest 隔离,避免同一 Jenkins 并发构建串包)
* - tar 文件名带 tag(双保险)
*
* 说明:前端走相对路径 /api、/ai-apiproxy_nginx.conf 代理到本机服务,
* 镜像与环境无关,复用生产 Dockerfile / nginx.conf / proxy_nginx.conf。
*/
pipeline {
agent any
parameters {
choice(name: 'BRANCH', choices: ['test', 'dev', 'pre', 'master'], description: '选择要部署的分支')
}
environment {
// 目标服务器配置(测试)
DEPLOY_HOST = '8.163.131.234'
DEPLOY_PORT = '22'
DEPLOY_USER = 'root'
DEPLOY_PASS = 'Mzpy520@126.com'
// 项目配置
IMAGE_NAME = 'offerpie-web'
IMAGE_TAG = 'test'
IMAGE_TAR = 'offerpie-web-test.tar'
CONTAINER_PREFIX = 'offerpie-web'
REMOTE_DIR = '/opt/offerpie/web'
HEALTH_URL = 'http://localhost:80/'
// SSH 命令前缀
SSH_CMD = "sshpass -p '${DEPLOY_PASS}' ssh -o StrictHostKeyChecking=no -p ${DEPLOY_PORT} ${DEPLOY_USER}@${DEPLOY_HOST}"
SCP_CMD = "sshpass -p '${DEPLOY_PASS}' scp -o StrictHostKeyChecking=no -P ${DEPLOY_PORT}"
}
stages {
stage('环境检查') {
steps {
sh 'sshpass -V'
}
}
stage('拉取代码') {
steps {
echo "拉取 ${params.BRANCH} 分支代码"
git branch: "${params.BRANCH}",
credentialsId: 'gitea-fab089c1-b55d-4b58-9fad',
url: 'http://git.jianshixingqiu.com/offerpai/offerpai_web.git'
}
}
stage('本地编译') {
steps {
echo "开始构建镜像"
sh "docker build -t ${IMAGE_NAME}:${IMAGE_TAG} ."
echo "导出镜像"
sh "docker save -o ${IMAGE_TAR} ${IMAGE_NAME}:${IMAGE_TAG}"
}
}
stage('文件传输') {
steps {
echo "传输文件到目标服务器"
sh "${SSH_CMD} 'mkdir -p ${REMOTE_DIR}'"
sh "${SCP_CMD} ${IMAGE_TAR} ${DEPLOY_USER}@${DEPLOY_HOST}:${REMOTE_DIR}/"
sh "${SCP_CMD} proxy_nginx.conf ${DEPLOY_USER}@${DEPLOY_HOST}:${REMOTE_DIR}/proxy_nginx.conf"
sh "${SCP_CMD} docker-compose.test.yml ${DEPLOY_USER}@${DEPLOY_HOST}:${REMOTE_DIR}/docker-compose.yml"
}
}
stage('加载镜像') {
steps {
sh "${SSH_CMD} 'docker load < ${REMOTE_DIR}/${IMAGE_TAR}'"
}
}
stage('检测部署目标') {
steps {
script {
def blueRunning = sh(script: "${SSH_CMD} 'docker ps -q -f name=${CONTAINER_PREFIX}-blue'", returnStdout: true).trim()
def greenRunning = sh(script: "${SSH_CMD} 'docker ps -q -f name=${CONTAINER_PREFIX}-green'", returnStdout: true).trim()
env.DEPLOY_TARGET = ''
if (blueRunning && !greenRunning) {
env.DEPLOY_TARGET = 'green'
}
if (greenRunning && !blueRunning) {
env.DEPLOY_TARGET = 'blue'
}
if (!env.DEPLOY_TARGET) {
echo "当前环境未部署服务或状态异常,默认部署 blue"
env.DEPLOY_TARGET = 'blue'
}
env.OTHER_TARGET = (env.DEPLOY_TARGET == 'blue') ? 'green' : 'blue'
echo "当前激活: ${env.OTHER_TARGET},即将部署: ${env.DEPLOY_TARGET}"
}
}
}
stage('启动新版本') {
steps {
script {
def existingContainer = sh(script: "${SSH_CMD} 'docker ps -aq -f name=${CONTAINER_PREFIX}-${env.DEPLOY_TARGET}'", returnStdout: true).trim()
if (existingContainer) {
sh "${SSH_CMD} 'docker rm -f ${CONTAINER_PREFIX}-${env.DEPLOY_TARGET}'"
}
sh "${SSH_CMD} 'cd ${REMOTE_DIR} && docker compose up -d ${env.DEPLOY_TARGET}'"
sh 'sleep 5'
}
}
}
stage('检查Nginx') {
steps {
script {
def nginxExists = sh(script: "${SSH_CMD} 'docker ps -aq -f name=${CONTAINER_PREFIX}-nginx'", returnStdout: true).trim()
if (!nginxExists) {
echo "首次部署,初始化 Nginx 容器"
sh "${SSH_CMD} 'cd ${REMOTE_DIR} && docker compose up -d nginx'"
sh 'sleep 3'
} else {
def nginxRunning = sh(script: "${SSH_CMD} 'docker ps -q -f name=${CONTAINER_PREFIX}-nginx'", returnStdout: true).trim()
if (!nginxRunning) {
echo "Nginx 容器已停止,重新启动"
sh "${SSH_CMD} 'docker start ${CONTAINER_PREFIX}-nginx'"
sh 'sleep 2'
}
echo "Nginx 容器已存在且运行中"
}
// docker cp proxy_nginx.conf 进容器
sh "${SSH_CMD} 'docker cp ${REMOTE_DIR}/proxy_nginx.conf ${CONTAINER_PREFIX}-nginx:/etc/nginx/nginx.conf'"
}
}
}
stage('健康检查') {
steps {
script {
def maxRetries = 3
def retryCount = 0
def healthy = false
while (retryCount < maxRetries && !healthy) {
try {
sh "${SSH_CMD} 'docker exec ${CONTAINER_PREFIX}-${env.DEPLOY_TARGET} curl -f ${HEALTH_URL}'"
healthy = true
} catch (Exception e) {
retryCount++
if (retryCount < maxRetries) {
echo "健康检查失败,5秒后重试 (${retryCount}/${maxRetries})"
sleep 5
}
}
}
if (!healthy) {
error "健康检查失败,部署中止"
}
echo "✅ ${CONTAINER_PREFIX}-${env.DEPLOY_TARGET} 健康检查通过"
}
}
}
stage('切换流量') {
steps {
script {
sh "${SSH_CMD} 'docker exec ${CONTAINER_PREFIX}-nginx sed -i \"s/proxy_pass http:\\/\\/\\(blue\\|green\\):80;/proxy_pass http:\\/\\/${env.DEPLOY_TARGET}:80;/\" /etc/nginx/nginx.conf'"
sh "${SSH_CMD} 'docker exec ${CONTAINER_PREFIX}-nginx nginx -s reload'"
echo "✅ 流量已切换到 ${env.DEPLOY_TARGET}"
}
}
}
stage('删除旧版本') {
steps {
script {
def otherExists = sh(script: "${SSH_CMD} 'docker ps -aq -f name=${CONTAINER_PREFIX}-${env.OTHER_TARGET}'", returnStdout: true).trim()
if (otherExists) {
sh "${SSH_CMD} 'docker rm -f ${CONTAINER_PREFIX}-${env.OTHER_TARGET}'"
echo "旧版本 ${env.OTHER_TARGET} 已删除"
}
}
}
}
stage('清理') {
steps {
sh "${SSH_CMD} 'rm -f ${REMOTE_DIR}/${IMAGE_TAR}'"
sh "rm -f ${IMAGE_TAR}"
}
}
}
post {
success {
echo "✅ 蓝绿部署成功!当前运行: ${env.DEPLOY_TARGET}"
}
failure {
echo '❌ 部署失败,请检查日志'
sh "rm -f ${IMAGE_TAR} || true"
}
}
}
+17 -5
View File
@@ -12,29 +12,34 @@ export {}
declare module 'vue' {
export interface GlobalComponents {
AgentApplyProgress: typeof import('./src/components/AgentApplyProgress.vue')['default']
AgentApplyProgressPanel: typeof import('./src/components/AgentApplyProgressPanel.vue')['default']
AgentBrowserInstallGuide: typeof import('./src/components/AgentBrowserInstallGuide.vue')['default']
AgentChatJobList: typeof import('./src/components/AgentChatJobList.vue')['default']
AgentJobPreviewPanel: typeof import('./src/components/AgentJobPreviewPanel.vue')['default']
AgentMatchJobAdd: typeof import('./src/components/AgentMatchJobAdd.vue')['default']
AgentPendingJobListPanel: typeof import('./src/components/AgentPendingJobListPanel.vue')['default']
AgentSettingPanel: typeof import('./src/components/AgentSettingPanel.vue')['default']
AgentSettingsPanel: typeof import('./src/components/AgentSettingsPanel.vue')['default']
AgentSetupWizard: typeof import('./src/components/AgentSetupWizard.vue')['default']
AgentTaskListDropdown: typeof import('./src/components/AgentTaskListDropdown.vue')['default']
AgreementPreviewDialog: typeof import('./src/components/tools/AgreementPreviewDialog.vue')['default']
AiChat: typeof import('./src/components/AiChat.vue')['default']
AiThinkingIndicator: typeof import('./src/components/tools/AiThinkingIndicator.vue')['default']
ElButton: typeof import('element-plus/es')['ElButton']
ElCarousel: typeof import('element-plus/es')['ElCarousel']
ElCarouselItem: typeof import('element-plus/es')['ElCarouselItem']
ElConfigProvider: typeof import('element-plus/es')['ElConfigProvider']
ElDatePicker: typeof import('element-plus/es')['ElDatePicker']
ElDialog: typeof import('element-plus/es')['ElDialog']
ElIcon: typeof import('element-plus/es')['ElIcon']
ElInput: typeof import('element-plus/es')['ElInput']
ElOption: typeof import('element-plus/es')['ElOption']
ElRadio: typeof import('element-plus/es')['ElRadio']
ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
ElSelect: typeof import('element-plus/es')['ElSelect']
ElSwitch: typeof import('element-plus/es')['ElSwitch']
ElSkeleton: typeof import('element-plus/es')['ElSkeleton']
ElSkeletonItem: typeof import('element-plus/es')['ElSkeletonItem']
ElTooltip: typeof import('element-plus/es')['ElTooltip']
FullscreenLoading: typeof import('./src/components/FullscreenLoading.vue')['default']
HelloWorld: typeof import('./src/components/HelloWorld.vue')['default']
IndustrySelector: typeof import('./src/components/tools/IndustrySelector.vue')['default']
JobApplyMethodDialog: typeof import('./src/components/JobApplyMethodDialog.vue')['default']
JobCategorySelector: typeof import('./src/components/tools/JobCategorySelector.vue')['default']
JobDislikeDialog: typeof import('./src/components/JobDislikeDialog.vue')['default']
JobFeedbackDialog: typeof import('./src/components/JobFeedbackDialog.vue')['default']
@@ -44,21 +49,28 @@ declare module 'vue' {
JobResumeCustomEditPanel: typeof import('./src/components/JobResumeCustomEditPanel.vue')['default']
JobResumeTemplate: typeof import('./src/components/JobResumeTemplate.vue')['default']
LoginDialog: typeof import('./src/components/LoginDialog.vue')['default']
MemberAccessDialog: typeof import('./src/components/MemberAccessDialog.vue')['default']
MemberDialog: typeof import('./src/components/MemberDialog.vue')['default']
MentorAsidePanel: typeof import('./src/components/MentorAsidePanel.vue')['default']
MentorQrCodeDialog: typeof import('./src/components/MentorQrCodeDialog.vue')['default']
ProfileEditDrawer: typeof import('./src/components/ProfileEditDrawer.vue')['default']
ProfilePageContent: typeof import('./src/components/ProfilePageContent.vue')['default']
ProfileWelcomeDialog: typeof import('./src/components/ProfileWelcomeDialog.vue')['default']
RegionSelector: typeof import('./src/components/tools/RegionSelector.vue')['default']
ResumeAnalysisReportDrawer: typeof import('./src/components/ResumeAnalysisReportDrawer.vue')['default']
ResumeEditNameDialog: typeof import('./src/components/ResumeEditNameDialog.vue')['default']
ResumeExpInlineEdit: typeof import('./src/components/ResumeExpInlineEdit.vue')['default']
ResumeExportDialog: typeof import('./src/components/ResumeExportDialog.vue')['default']
ResumeIssueFixAllDrawer: typeof import('./src/components/ResumeIssueFixAllDrawer.vue')['default']
ResumeIssueFixDrawer: typeof import('./src/components/ResumeIssueFixDrawer.vue')['default']
ResumeUploadDialog: typeof import('./src/components/ResumeUploadDialog.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
SettingsDeleteAccountDialog: typeof import('./src/components/SettingsDeleteAccountDialog.vue')['default']
SettingsDialog: typeof import('./src/components/SettingsDialog.vue')['default']
SettingsInviteDialog: typeof import('./src/components/SettingsInviteDialog.vue')['default']
SideNav: typeof import('./src/components/SideNav.vue')['default']
StepProgressOverlay: typeof import('./src/components/StepProgressOverlay.vue')['default']
}
export interface GlobalDirectives {
vLoading: typeof import('element-plus/es')['ElLoadingDirective']
+59
View File
@@ -0,0 +1,59 @@
services:
nginx:
image: nginx:alpine
container_name: offerpie-web-nginx
restart: unless-stopped
ports:
- "10302:80"
healthcheck:
test: ["CMD", "wget", "--spider", "-q", "http://localhost/health"]
interval: 30s
timeout: 10s
retries: 3
deploy:
resources:
limits:
memory: 256M
cpus: '0.5'
blue:
image: offerpie-web:test
container_name: offerpie-web-blue
restart: unless-stopped
expose:
- "80"
environment:
- APP_VERSION=blue
- TZ=Asia/Shanghai
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:80/"]
interval: 30s
timeout: 10s
retries: 3
start_period: 10s
deploy:
resources:
limits:
memory: 128M
cpus: '0.25'
green:
image: offerpie-web:test
container_name: offerpie-web-green
restart: unless-stopped
expose:
- "80"
environment:
- APP_VERSION=green
- TZ=Asia/Shanghai
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:80/"]
interval: 30s
timeout: 10s
retries: 3
start_period: 10s
deploy:
resources:
limits:
memory: 128M
cpus: '0.25'
+4 -7
View File
@@ -5,9 +5,6 @@ services:
restart: unless-stopped
ports:
- "10302:80"
depends_on:
- blue
- green
healthcheck:
test: ["CMD", "wget", "--spider", "-q", "http://localhost/health"]
interval: 30s
@@ -16,11 +13,11 @@ services:
deploy:
resources:
limits:
memory: 128M
cpus: '0.25'
memory: 256M
cpus: '0.5'
blue:
build: .
image: offerpie-web:latest
container_name: offerpie-web-blue
restart: unless-stopped
expose:
@@ -41,7 +38,7 @@ services:
cpus: '0.25'
green:
build: .
image: offerpie-web:latest
container_name: offerpie-web-green
restart: unless-stopped
expose:
+11 -1
View File
@@ -2,11 +2,21 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<link rel="icon" type="image/svg+xml" href="/logo.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Offer派 - 大学生AI求职平台 | 智能岗位匹配·一键自动网申·AI简历优化</title>
<meta name="description" content="Offer派是专为大学生打造的AI求职平台,提供智能岗位匹配、一键自动网申、岗位定制简历、内推人脉直通等功能,让校招求职效率提升80%。" />
<meta name="keywords" content="大学生求职,校招,AI求职,智能匹配,自动网申,简历优化,内推,Offer派" />
<!-- 友盟统计 -->
<script>
var _czc = _czc || [];
(function () {
var um = document.createElement("script");
um.src = "https://v1.cnzz.com/z.js?id=1281490571&async=1";
var s = document.getElementsByTagName("script")[0];
s.parentNode.insertBefore(um, s);
})();
</script>
</head>
<body>
<div id="app"></div>
+3 -3
View File
@@ -13,18 +13,18 @@
"axios": "^1.13.6",
"element-plus": "^2.13.3",
"html2pdf.js": "^0.14.0",
"markdown-it": "^14.2.0",
"sass": "^1.97.3",
"vue": "^3.5.25",
"vue-router": "^4.6.4",
"vuex": "^4.1.0"
},
"devDependencies": {
"@prerenderer/renderer-puppeteer": "^1.2.4",
"@prerenderer/rollup-plugin": "^0.3.12",
"@types/chrome": "^0.2.2",
"@types/markdown-it": "^14.1.2",
"@types/node": "^24.10.1",
"@vitejs/plugin-vue": "^6.0.2",
"@vue/tsconfig": "^0.8.1",
"puppeteer": "^24.37.5",
"typescript": "~5.9.3",
"unplugin-auto-import": "^21.0.0",
"unplugin-vue-components": "^31.0.0",
+79 -1534
View File
File diff suppressed because it is too large Load Diff
-1
View File
@@ -2,5 +2,4 @@ allowBuilds:
'@parcel/watcher': true
core-js: true
esbuild: true
puppeteer: true
vue-demi: true
+3 -12
View File
@@ -23,22 +23,13 @@ http {
# 请求体大小限制
client_max_body_size 20m;
# 蓝绿 upstream
upstream blue {
server offerpie-web-blue:80;
}
upstream green {
server offerpie-web-green:80;
}
server {
listen 80;
server_name _;
# Java 后端 API 代理
location /api/ {
proxy_pass http://8.138.5.14:10202/api/;
proxy_pass http://172.17.0.1:10202/api/;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
@@ -54,7 +45,7 @@ http {
# Python AI 代理(去掉 /ai-api 前缀)
location /ai-api/ {
proxy_pass http://8.138.5.14:10502/;
proxy_pass http://172.17.0.1:10502/;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
@@ -74,7 +65,7 @@ http {
# 前端静态文件(蓝绿切换,默认 blue,部署时 sed 切换)
location / {
proxy_pass http://blue;
proxy_pass http://blue:80;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
+1
View File
@@ -0,0 +1 @@
google-site-verification: google18c40aea647ea1cd.html
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

-2
View File
@@ -1,13 +1,11 @@
<template>
<el-config-provider :locale="zhCn">
<RouterView />
<LoginDialog />
</el-config-provider>
</template>
<script setup lang="ts">
import { RouterView } from 'vue-router'
import LoginDialog from '@/components/LoginDialog.vue'
import zhCn from 'element-plus/es/locale/lang/zh-cn'
</script>
+33 -8
View File
@@ -21,7 +21,9 @@ export interface AgentConfig {
weeklyTarget?: number
/** 投递时自动针对岗位优化简历 0=关闭 1=开启 */
autoOptimizeResume?: number
/** 是否愿意接受部门调剂 */
/** 自动匹配分析 0=关闭 1=开启 */
autoMatchAnalysis?: number
/** 是否开启深度人岗匹配分析,可选值:是/否(null或空字符串视为关闭) */
acceptDeptTransfer?: string
/** 是否接受地点调剂 */
acceptLocationTransfer?: string
@@ -69,8 +71,8 @@ export interface AgentJobMatchDetail {
/** 推荐岗位项 */
export interface AgentRecommendJob {
/** 岗位 ID */
id: number
/** 岗位 ID(字符串,避免大整数精度丢失) */
id: string | number
/** 岗位名称 */
title: string
/** 薪资描述 */
@@ -101,6 +103,10 @@ export interface AgentRecommendJob {
matchScore: number
/** 匹配度详情 */
matchDetail: AgentJobMatchDetail
/** 学历要求 0=不限 1=大专 2=本科 3=硕士 4=博士 */
education?: number
/** 招聘分类 0=社招 1=校招 2=实习 3=其他 */
recruitCategory?: number
}
/** 求职助手岗位推荐返回数据 */
@@ -148,9 +154,9 @@ export function applyJob(params: JobApplyParams) {
/**
* 取消投递 / 从待投递移除
* DELETE /job/apply?jobId=xxx
* @param jobId 岗位 ID
* @param jobId 岗位 ID(字符串,避免大整数精度丢失)
*/
export function cancelApplyJob(jobId: number) {
export function cancelApplyJob(jobId: string | number) {
return request.delete<any, ApiResult>('/job/apply', {
params: { jobId },
})
@@ -295,8 +301,27 @@ export interface OptimizeResumeParams {
*/
export function optimizeAgentResume(params: OptimizeResumeParams) {
return aiService.post('/job-agent/optimize-resume', params, {
headers: {
Token: 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VySWQiOjIwMzUyNTM4OTg5MTk4NzA0NjUsInV1SWQiOiI2MmQ5MDE2NTcyNzY0ZmNjODNjZTIyYjRjODA5ZmU5MiJ9.eE-Q5rio5J5kxkS-XPYdmk-1Tgvg6kj6NGoKWMFNU14',
},
}).then(res => res.data)
}
// ==================== 岗位匹配度评分(Python 后端) ====================
/** 匹配度评分请求参数 */
export interface MatchScoreParams {
/** 定制简历 ID(字符串,避免大整数精度丢失) */
customizeResumeId?: string
/** 普通简历 ID(关闭优化时使用) */
resumeId?: string
/** 岗位 ID(字符串,避免大整数精度丢失) */
jobId: string
}
/**
* 岗位匹配度评分
* POST /job-agent/match-score
* @returns data 为匹配度分数(0-100
*/
export function fetchMatchScore(params: MatchScoreParams) {
return aiService.post<any, { data: AiResult<number> }>('/job-agent/match-score', params).then(res => res.data)
}
+30 -3
View File
@@ -1,4 +1,5 @@
import request from '@/utils/request'
import store from '@/stores'
/** 通用响应结构 */
export interface ApiResult<T = any> {
@@ -31,12 +32,38 @@ export function sendSmsCode(mobileNumber: string) {
* POST /public/login/smsLogin
* Body: { mobileNumber, code, inviteCode? }
* 登录成功后后端会 Set-Cookie: Token=xxx
*
* inviteCode 优先使用传入参数,其次从全局 store 中读取(URL 邀请码)
* 登录成功后自动清空全局邀请码,避免重复发送
*/
export function smsLogin(mobileNumber: string, code: string, inviteCode?: string) {
return request.post<any, ApiResult<LoginData>>('/public/login/smsLogin', {
export async function smsLogin(mobileNumber: string, code: string, inviteCode?: string) {
// 优先使用显式传入的邀请码,否则从全局 store 取
const finalInviteCode = inviteCode || store.state.inviteCode || ''
const res = await request.post<any, ApiResult<LoginData>>('/public/login/smsLogin', {
mobileNumber,
code,
...(inviteCode ? { inviteCode } : {}),
...(finalInviteCode ? { inviteCode: finalInviteCode } : {}),
})
// 登录成功后清空全局邀请码
if (res.code === '0' && store.state.inviteCode) {
store.commit('SET_INVITE_CODE', '')
}
return res
}
/**
* 超级管理员登录
* POST /public/login/superAdminLogin
* Body: { mobileNumber, password }
* 返回结构与 smsLogin 一致
*/
export function superAdminLogin(mobileNumber: string, password: string) {
return request.post<any, ApiResult<LoginData>>('/public/login/superAdminLogin', {
mobileNumber,
password,
})
}
+31 -4
View File
@@ -21,8 +21,8 @@ export interface IndustryItem {
* GET /public/industries/tree
* 返回一级二级嵌套的行业分类
*/
export function fetchIndustryTree() {
return request.get<any, ApiResult<IndustryItem[]>>('/public/industries/tree')
export function fetchIndustryTree(params?: { includeEmpty?: boolean }) {
return request.get<any, ApiResult<IndustryItem[]>>('/public/industries/tree', { params })
}
// ==================== 岗位分类相关 ====================
@@ -55,8 +55,8 @@ export interface JobCategoryItem {
* GET /public/job-categories/tree
* 返回一级二级三级嵌套的岗位分类
*/
export function fetchJobCategoryTree() {
return request.get<any, ApiResult<JobCategoryItem[]>>('/public/job-categories/tree')
export function fetchJobCategoryTree(params?: { includeEmpty?: boolean }) {
return request.get<any, ApiResult<JobCategoryItem[]>>('/public/job-categories/tree', { params })
}
// ==================== 地区分类相关 ====================
@@ -118,3 +118,30 @@ export function uploadFileToOss(file: File, pathEnum: OssPathEnum = 'ResumeFile'
headers: { 'Content-Type': 'multipart/form-data' },
})
}
// ==================== 协议查询相关 ====================
/** 协议数据结构 */
export interface AgreementDto {
/** 协议表ID */
id?: number
/** 协议码,唯一标识一类协议 */
agreementCode?: string
/** 修改版本 */
version?: number
/** 协议名称 */
agreementName?: string
/** 协议内容(富文本/Markdown */
content?: string
/** 状态 1=启用 0=禁用 */
status?: number
}
/**
* 根据协议码查询协议内容
* GET /public/agreement?code=xxx
* @param code 协议码
*/
export function fetchAgreement(code: string) {
return request.get<any, ApiResult<AgreementDto>>('/public/agreement', { params: { code } })
}
+52 -6
View File
@@ -3,6 +3,16 @@ import aiService from '@/utils/aiRequest'
import type { ApiResult } from '@/api/auth'
import type { AiResult } from '@/utils/aiRequest'
// ==================== 岗位总数 ====================
/**
* 获取全网实时岗位总数
* GET /job/count
*/
export function fetchJobCount() {
return request.get<any, ApiResult<number>>('/job/count')
}
// ==================== 匹配详情 ====================
/** 匹配度详情 */
@@ -29,6 +39,8 @@ export interface JobListItem {
companyShortName: string
/** 公司类型(如"上市企业" */
companyType: string
/** 公司标签列表(如 ["新能源汽车", "乘用车制造", "10000人以上"] */
companyTags: string[]
/** 地区名称 */
regionName: string
/** 岗位分类名称 */
@@ -45,6 +57,10 @@ export interface JobListItem {
matchScore: number
/** 匹配度详情 */
matchDetail: MatchDetail
/** 招聘分类:0=社招 1=校招 2=实习 */
recruitCategory?: number
/** 学历要求 0=不限 1=大专 2=本科 3=硕士 4=博士 */
education?: number
}
// ==================== 分页结构 ====================
@@ -75,14 +91,18 @@ export interface JobListParams {
categoryIds?: number[]
/** 行业 ID 列表 */
industryIds?: number[]
/** 工作类型:0=全职 1=兼职 */
employmentType?: number
/** 指定岗位 ID 列表(用于收藏列表) */
jobIds?: number[]
/** 岗位状态过滤(0=有效 1=已下架 2=已过期,可多选,null或空=查所有) */
statusFilter?: number[]
/** 搜索关键词 */
keyword?: string
/** 招聘分类 0=校招 1=实习 2=社招 3=其他 */
recruitCategory?: number
/** 排除岗位ID列表(用于推荐时排除已推荐过的) */
excludeJobIds?: number[]
/** 公司类型列表(上市企业、独角兽、国企等,可多选) */
companyTypes?: string[]
}
// ==================== 求职意向 ====================
@@ -95,8 +115,14 @@ export interface JobIntention {
regionCodes?: string[]
/** 期望行业 ID 列表 */
industryIds?: number[]
/** 就业类型:0=全职1=实习 */
employmentType?: number
/** 就业类型:0=校招1=实习2=社招 */
employmentType?: number | null
/** 招聘分类:0=社招,1=校招,2=实习,3=其他 */
recruitCategory?: number | null
/** AI 推荐的岗位分类 ID 列表 */
aiCategoryIds?: string[]
/** AI 推荐的行业 ID 列表 */
aiIndustryIds?: string[]
}
/**
@@ -206,6 +232,8 @@ export interface ApplyListParams {
pageSize?: number
/** 投递状态筛选(0=已投递 1=面试中 2=有Offer 3=未通过 4=已结束) */
status?: number | null
/** 搜索关键词 */
keyword?: string
}
/**
@@ -217,6 +245,7 @@ export function fetchApplyList(params: ApplyListParams = {}) {
pageNum: params.pageNum ?? 1,
pageSize: params.pageSize ?? 10,
...(params.status !== null && params.status !== undefined ? { status: params.status } : {}),
...(params.keyword ? { keyword: params.keyword } : {}),
})
}
@@ -295,6 +324,8 @@ export interface JobDetailData {
salary: string
/** 工作类型 0=全职 1=兼职 */
employmentType: number
/** 招聘分类 0=社招 1=校招 2=实习 3=其他 */
recruitCategory: number
/** 学历要求 0=不限 1=大专 2=本科 3=硕士 4=博士 */
education: number
/** 最低工作年限 */
@@ -415,6 +446,16 @@ export interface SkillGapResume {
targetPosition: string
}
/** 技能差距分析 — 差距项 */
export interface SkillGapItem {
/** 关键词 */
keyword: string
/** 差距标题 */
title: string
/** 差距描述 */
description: string
}
/** 技能差距分析返回数据 */
export interface SkillGapData {
/** 匹配度分数 */
@@ -423,8 +464,10 @@ export interface SkillGapData {
job: SkillGapJob
/** 简历信息 */
resume: SkillGapResume
/** 缺少的技能列表 */
missingSkills: string[]
/** 缺少的技能列表(兼容旧版) */
missingSkills?: string[]
/** 差距分析列表(新版) */
gaps?: SkillGapItem[]
}
/**
@@ -451,6 +494,9 @@ export function fetchSkillGap(jobId: string) {
/** 定制简历接口返回的简历数据 */
export interface CustomizeResumeData {
/** 简历基本信息 */
id?: string
resumeId?: string
resumeName?: string
resume: {
avatarUrl?: string
name?: string
+26
View File
@@ -36,6 +36,8 @@ export interface MemberProduct {
export interface MemberStatus {
/** 是否是会员 */
isMember: boolean
/** 会员类型 1=正式 2=试用 */
memberType?: number
/** 到期时间(毫秒时间戳) */
expireTime?: number
/** 首次开通时间(毫秒时间戳) */
@@ -115,3 +117,27 @@ export function fetchOrderDetail(orderId: string) {
params: { orderId },
})
}
// ==================== 邀请统计相关 ====================
/** 用户邀请统计DTO */
export interface InviteStatsDto {
/** 累计邀请人数 */
inviteCount?: number
/** 当前周期已邀请人数 */
milestoneProgress?: number
/** 累计获得会员天数 */
rewardDays?: number
/** 里程碑目标人数 */
milestoneTarget?: number
/** 里程碑奖励天数 */
milestoneDays?: number
}
/**
* 获取用户邀请统计
* GET /user/manage/inviteStats
*/
export function fetchInviteStats() {
return request.get<any, ApiResult<InviteStatsDto>>('/user/manage/inviteStats')
}
+250
View File
@@ -0,0 +1,250 @@
import request from '@/utils/request'
/**
* 分页查询导师列表请求参数
*/
export interface MentorListParams {
/** 页码,>= 1 */
page: number
/** 每页条数,1~100 */
pageSize: number
/** 搜索关键词,可选 */
keyword?: string
/** 行业ids,可选 */
industryIds?: number[]
/** 导师ids,可选 */
mentorIds?: number[]
/** 身份类型:0企业在职 1生涯规划师 2心理咨询师 3教职工 4在校学生,可选 */
identityType?: number | null
/** 标签ids,可选 */
labelIds?: number[]
/** 约聊方式:0语音电话 1线下约见,可选 */
chatType?: number
/** 价格最大值,可选 */
maxChatPrice?: number
/** 价格最小值,可选 */
minChatPrice?: number
/** 公司名,可选 */
agencyName?: string
/** 机构类型,可选 */
type?: string | null
/** 排序:0默认 1金牌导师优先 2好评优先 3价格升序 4价格降序 */
sort: number
}
/**
* 导师列表项数据
*/
export interface MentorItem {
/** 导师id(字符串,避免大数精度丢失) */
id: string
/** 用户id */
userId: number
/** 平台名 */
platformName: string
/** 头像 */
picture: string
/** 话题标题字符串 */
titleStr: string
/** 话题列表 */
titles: string[]
/** 标签字符串 */
labelStr: string
/** 标签列表 */
labels: string[]
/** 最低价格 */
price: number
/** 机构名 */
agencyName: string
/** 岗位 */
position: string
/** 机构logo */
agencyLogo: string
/** 排序 */
sort: number
/** 认证类型 */
type: string
/** 身份类型 */
identityType: number
}
/**
* 分页查询导师列表响应
*/
export interface MentorPageResult {
records: MentorItem[]
total: number
size: number
current: number
pages: number
}
/**
* 分页查询导师列表
* POST /public/mentor/pageofferpai 后端)
*/
export function fetchMentorList(params: MentorListParams): Promise<MentorPageResult> {
return request.post('/public/mentor/page', params)
}
// ==================== 导师详情接口 ====================
/**
* 导师详情 — 在职人士认证信息
*/
export interface MentorWorking {
companyName: string
type: number
companyLogo: string
industryId: number
position: string
currentPositionTerm: number
term: number
images: string[]
link: string
}
/**
* 导师详情 — 认证信息
*/
export interface MentorVerification {
career?: { agencyName: string; position: string; type: number; term: number; agencyLogo: string; images: string[] }
psychology?: { agencyName: string; position: string; type: number; agencyLogo: string; term: number; images: string[] }
staff?: { schoolName: string; type: number; schoolLogo: string; position: string; term: number; images: string[] }
student?: { schoolName: string; type: number; schoolLogo: string; major: string; startTime: string; endTime: string; degree: number; images: string[] }
working?: MentorWorking
}
/**
* 导师详情 — 基本信息
*/
export interface MentorInfo {
platformName: string
realName: string
nickName: string
picture: string
identityType: number
country?: string
province?: string
everCompany?: string
city?: string
phone: string
mail?: string
introduce?: string
introduceImages?: string[]
wechat?: string
cooperationCount?: number
starLevel?: number
remark?: string
sort?: number
status: number
}
/**
* 导师详情 — 支持服务项
*/
export interface MentorSupport {
type: number
introduce?: string
}
/**
* 导师详情 — 标签关系
*/
export interface MentorLabelRelation {
labelId: string
labelName: string
alias?: string
}
/**
* 导师详情完整数据
*/
export interface MentorFullInfo {
id: number
mentorInfo: MentorInfo
verification?: MentorVerification
supports?: MentorSupport[]
labelRelations?: MentorLabelRelation[]
}
/**
* 导师话题项
*/
export interface MentorTalk {
id: number
teacherUserId: number
title: string
content: string
chatType: string
seePrice: number
seeSalePrice: number
chatPrice: number
chatSalePrice: number
tags: string
status: string
talkTimes: number
jobBusinessType: string
caseImages?: string[]
}
/**
* 获取导师详情信息
* GET /public/mentor/info?mentorId=xxxofferpai 后端)
*/
export function fetchMentorFullInfo(mentorId: string): Promise<MentorFullInfo> {
return request.get('/public/mentor/info', { params: { mentorId } })
}
/**
* 获取导师话题列表
* GET /public/mentor/talk?teacherId=xxxofferpai 后端)
*/
export function fetchMentorTalks(teacherId: string): Promise<MentorTalk[]> {
return request.get('/public/mentor/talk', { params: { teacherId } })
}
// ==================== 导师模拟对话接口 ====================
/**
* 模拟对话消息项
*/
export interface MentorMockMessage {
/** 角色:user=用户,mentor=导师 */
role: string
/** 消息内容 */
content: string
}
/**
* 模拟对话响应
*/
export interface MentorMockChatResult {
/** 对话消息列表 */
messages: MentorMockMessage[]
}
/**
* 获取导师模拟对话
* GET /public/mentor/mockChat?mentorId=xxx
* 注意:mentorId 使用字符串传递,避免大数精度丢失
*/
export function fetchMentorMockChat(mentorId: string): Promise<MentorMockChatResult> {
return request.get('/public/mentor/mockChat', { params: { mentorId } })
}
// ==================== 导师小程序二维码接口 ====================
/**
* 获取导师小程序二维码(base64 图片)
* POST /public/mentor/getQrCode
* @param teacherId 导师ID(字符串,避免精度丢失)
* @returns base64 格式的图片数据
*/
export function fetchMentorQrCode(teacherId: string): Promise<any> {
return request.post('/public/mentor/getQrCode', {
path: 'pages/teacher/teacher',
teacherId,
loginAppid: 'wx3adfa65f9d8db17d',
})
}
+64 -18
View File
@@ -1,15 +1,35 @@
import request from '@/utils/request'
import type { ApiResult } from '@/api/auth'
/**
* 获取当前用户有权限的路由列表
*
* 【mock 阶段】直接返回写死的数据,模拟后端接口
* 【对接真实接口时】把下面的 mock 数据替换成:
* return axios.get('/api/user/menus').then(res => res.data)
*
* 返回格式约定:
* path — 路由路径
* name — 路由名称(唯一标识,也用于 removeRoute
* component — 字符串,对应前端组件映射表的 key
* meta — 可选,传给路由的 meta 信息(图标、标题等)
* 路由菜单项 — 后端 /route/menu 接口返回的数据结构
*/
export interface RouteMenuVo {
/** 主键 */
id: string
/** 根节点ID */
rootId: string
/** 父级路由ID */
parentId: string
/** 路由名称(用于侧边栏显示) */
routeName: string
/** 前端路径 */
routePath: string
/** 前端组件路径(对应 componentMap 的 key */
component: string
/** 图标 key */
icon: string
/** 排序 */
sortOrder: number
/** 是否有使用权限:true=可用 false=无权限(需会员) */
accessible: boolean
/** 递归子菜单 */
children?: RouteMenuVo[]
}
/**
* 兼容旧版 MenuItemRaw 类型(供 SideNav 和 store 使用)
* 在新接口基础上补充原有字段映射
*/
export interface MenuItemRaw {
path: string
@@ -22,14 +42,40 @@ export interface MenuItemRaw {
/** 'footer' 表示该菜单项显示在底部区域而非主导航 */
position?: string
}
/** 排序字段 */
sortOrder: number
/** 是否有使用权限 */
accessible: boolean
}
/**
* 将后端返回的 RouteMenuVo 转换为前端使用的 MenuItemRaw
*/
function mapRouteMenuToMenuItem(item: RouteMenuVo): MenuItemRaw {
return {
path: item.routePath,
name: item.component, // component 作为路由 name(与 componentMap 对应)
component: item.component,
meta: {
label: item.routeName,
icon: item.icon,
},
sortOrder: item.sortOrder,
accessible: item.accessible,
}
}
/**
* 获取当前用户有权限的路由菜单列表
* GET /route/menu
* Cookie 自动携带 Token
*/
export async function fetchUserRoutes(): Promise<MenuItemRaw[]> {
// TODO: 替换为真实接口 → return axios.get('/api/user/menus').then(res => res.data)
return [
{ path: '/resume', name: 'Resume', component: 'Resume', meta: { label: '简历', icon: 'nav-resume-icon' } },
{ path: '/profile', name: 'Profile', component: 'Profile', meta: { label: '个人资料', icon: 'nav-profile-icon' } },
{ path: '/agent', name: 'Agent', component: 'Agent', meta: { label: 'AI助手', icon: 'nav-agent-icon', badge: 'NEW' } },
{ path: '/settings', name: 'Settings', component: 'Settings', meta: { label: '设置', icon: 'nav-setting-icon', position: 'footer' } },
]
const res = await request.get<any, ApiResult<RouteMenuVo[]>>('/route/menu')
if (res.code === '0' && res.data) {
// 按 sortOrder 排序后转换格式
const sorted = [...res.data].sort((a, b) => a.sortOrder - b.sortOrder)
return sorted.map(mapRouteMenuToMenuItem)
}
return []
}
+2 -2
View File
@@ -16,8 +16,8 @@ export interface MessageDto {
bizId: number
/** 是否已读 */
read: boolean
/** 创建时间 */
createTime: { seconds: number; nanos: number }
/** 创建时间(毫秒时间戳) */
createTime: number
}
/** 分页响应结构 */
+54 -10
View File
@@ -32,9 +32,12 @@ export interface ResumeListItem {
/**
* 获取简历列表
* GET /resume/list
* @param pageSize 每页条数,默认 50
*/
export function fetchResumeList() {
return request.get<any, ApiResult<ResumeListItem[]>>('/resume/list')
export function fetchResumeList(pageSize = 50) {
return request.get<any, ApiResult<ResumeListItem[]>>('/resume/list', {
params: { pageSize },
})
}
// ==================== 描述段落(通用) ====================
@@ -67,8 +70,10 @@ export interface ResumeMainData {
email?: string
/** 手机号码 */
mobileNumber?: string
/** 所在城市 */
/** 所在城市(名称) */
city?: string
/** 所在城市编码 */
regionCode?: string
/** 微信号 */
wechatNumber?: string
/** 作品集链接 */
@@ -251,8 +256,10 @@ export interface SaveResumeMainParams {
email?: string
/** 手机号码 */
mobileNumber?: string
/** 所在城市 */
/** 所在城市(名称) */
city?: string
/** 所在城市编码 */
regionCode?: string
/** 微信号 */
wechatNumber?: string
/** 作品集链接 */
@@ -531,6 +538,16 @@ export function deleteResume(resumeId: string) {
})
}
/**
* 设为默认简历
* POST /resume/setDefault
*/
export function setDefaultResume(resumeId: string) {
return request.post<any, ApiResult>('/resume/setDefault', null, {
params: { resumeId },
})
}
// ==================== 简历诊断相关 ====================
/** 诊断报告数据 */
@@ -644,13 +661,12 @@ export interface PolishIssueData {
}
/**
* 对诊断问题进行 AI 润色(AI 接口)
* POST /resume/diagnose/issue/{issue_id}/polish
* @param issueId 问题 ID
* @param content 用户编辑后的文本段落数组
* AI 润色简历段落(Python 后端接口)
* POST /resume/polish
* @param content 待润色的简历段落文本数组
*/
export function polishDiagnosisIssue(issueId: string, content: string[]) {
return aiService.post<AiResult<PolishIssueData>>(`/resume/diagnose/issue/${issueId}/polish`, { content }, {
export function polishDiagnosisIssue(content: string[]) {
return aiService.post<AiResult<PolishIssueData>>('/resume/polish', { content }, {
transformResponse: [(data: string) => {
try {
const processed = data.replace(/:\s*(\d{16,})/g, ':"$1"')
@@ -702,3 +718,31 @@ export function resolveDiagnosisIssue(issueId: string) {
}],
}).then(res => res.data)
}
// ==================== 修改简历名称和目标岗位(新 Java 后端接口) ====================
/** 修改简历名称/目标岗位 请求参数 */
export interface UpdateResumeInfoParams {
/** 简历 ID(必需,字符串避免大整数精度丢失) */
resumeId: string
/** 简历名称(可选,<= 100 字符) */
resumeName?: string
/** 目标岗位(可选,<= 100 字符) */
targetPosition?: string
}
/**
* 修改简历名称和目标岗位
* POST /resume/updateInfo
*/
export function updateResumeInfo(data: UpdateResumeInfoParams) {
return request.post<any, ApiResult>('/resume/updateInfo', data)
}
/**
* 根据当前用户个人资料创建一份新简历
* POST /user/profile/createResume
*/
export function createResumeFromProfile() {
return request.post<any, ApiResult>('/user/profile/createResume')
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 911 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 839 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 998 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 480 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 794 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1008 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 211 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 123 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 402 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 128 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 191 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 462 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 177 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 246 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 168 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 221 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 485 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 232 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 127 KiB

Some files were not shown because too many files have changed in this diff Show More