移动端页面缩放
This commit is contained in:
@@ -89,3 +89,32 @@ export interface RegionItem {
|
|||||||
export function fetchRegionTree() {
|
export function fetchRegionTree() {
|
||||||
return request.get<any, ApiResult<RegionItem[]>>('/public/regions/tree')
|
return request.get<any, ApiResult<RegionItem[]>>('/public/regions/tree')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==================== OSS 文件上传相关 ====================
|
||||||
|
|
||||||
|
/** 文件上传路径枚举 */
|
||||||
|
export type OssPathEnum = 'UserImage' | 'ResumeImage' | 'SystemImage' | 'ResumeFile' | 'OtherFile'
|
||||||
|
|
||||||
|
/** 文件上传返回结果 */
|
||||||
|
export interface OssUploadResult {
|
||||||
|
/** 上传地址 */
|
||||||
|
uploadUrl?: string
|
||||||
|
/** 下载地址 */
|
||||||
|
downloadUrl?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 上传文件到OSS
|
||||||
|
* POST /oss/simpleUpload
|
||||||
|
* @param file 要上传的文件
|
||||||
|
* @param pathEnum 路径枚举,默认 ResumeFile
|
||||||
|
* @returns 上传结果(含下载地址)
|
||||||
|
*/
|
||||||
|
export function uploadFileToOss(file: File, pathEnum: OssPathEnum = 'ResumeFile') {
|
||||||
|
const formData = new FormData()
|
||||||
|
formData.append('file', file)
|
||||||
|
return request.post<any, ApiResult<OssUploadResult>>('/oss/simpleUpload', formData, {
|
||||||
|
params: { pathEnum },
|
||||||
|
headers: { 'Content-Type': 'multipart/form-data' },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -1441,6 +1441,12 @@
|
|||||||
margin: 0 0 0.32rem;
|
margin: 0 0 0.32rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
a{
|
||||||
|
text-decoration: none;
|
||||||
|
color: inherit;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
// 链接列表 — ul > li 自然纵排,无需 flex-direction: column
|
// 链接列表 — ul > li 自然纵排,无需 flex-direction: column
|
||||||
ul {
|
ul {
|
||||||
list-style: none;
|
list-style: none;
|
||||||
|
|||||||
@@ -630,6 +630,7 @@
|
|||||||
padding: 0.16rem 0;
|
padding: 0.16rem 0;
|
||||||
font-size: 0.12rem;
|
font-size: 0.12rem;
|
||||||
color: $text-light;
|
color: $text-light;
|
||||||
|
z-index: 10;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 高匹配度特殊样式
|
// 高匹配度特殊样式
|
||||||
|
|||||||
@@ -275,7 +275,7 @@ const store = useStore()
|
|||||||
const tabs = [
|
const tabs = [
|
||||||
{ key: 'account', label: '账号与安全', icon: '👤' },
|
{ key: 'account', label: '账号与安全', icon: '👤' },
|
||||||
{ key: 'member', label: '会员', icon: '🏅' },
|
{ key: 'member', label: '会员', icon: '🏅' },
|
||||||
{ key: 'reminder', label: '目标岗位设置', icon: '🔔' },
|
// { key: 'reminder', label: '目标岗位设置', icon: '🔔' },
|
||||||
]
|
]
|
||||||
|
|
||||||
/** 当前选中的 Tab */
|
/** 当前选中的 Tab */
|
||||||
|
|||||||
@@ -2,9 +2,11 @@ import { createApp } from 'vue'
|
|||||||
import App from './App.vue'
|
import App from './App.vue'
|
||||||
import router from './router'
|
import router from './router'
|
||||||
import store from './stores'
|
import store from './stores'
|
||||||
|
import remAdaptPlugin from './plugins/remAdapt'
|
||||||
import '@/assets/styles/index.scss'
|
import '@/assets/styles/index.scss'
|
||||||
|
|
||||||
createApp(App)
|
createApp(App)
|
||||||
.use(router)
|
.use(router)
|
||||||
.use(store)
|
.use(store)
|
||||||
|
.use(remAdaptPlugin)
|
||||||
.mount('#app')
|
.mount('#app')
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
/**
|
||||||
|
* REM 适配插件
|
||||||
|
* 以 1920px 设计稿为基准,1rem = 100px
|
||||||
|
* 移动端使用 transform scale 缩放,避免浏览器最小字体限制
|
||||||
|
*/
|
||||||
|
import type { Plugin } from 'vue'
|
||||||
|
|
||||||
|
const remAdaptPlugin: Plugin = {
|
||||||
|
install() {
|
||||||
|
const docEl = document.documentElement
|
||||||
|
const designWidth = 1920
|
||||||
|
|
||||||
|
// 检测是否为移动端
|
||||||
|
const isMobile = (): boolean => {
|
||||||
|
return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)
|
||||||
|
|| window.innerWidth < 1050
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重新计算缩放
|
||||||
|
const recalc = () => {
|
||||||
|
const clientWidth = docEl.clientWidth
|
||||||
|
if (!clientWidth) return
|
||||||
|
|
||||||
|
// 始终使用 100px 作为基准
|
||||||
|
docEl.style.fontSize = '100px'
|
||||||
|
|
||||||
|
const body = document.body
|
||||||
|
if (!body) return
|
||||||
|
|
||||||
|
if (isMobile()) {
|
||||||
|
// 移动端:使用 transform scale 缩放整个页面
|
||||||
|
const scale = clientWidth / designWidth
|
||||||
|
body.style.width = designWidth + 'px'
|
||||||
|
body.style.transform = `scale(${scale})`
|
||||||
|
body.style.transformOrigin = 'top left'
|
||||||
|
// 设置 html 高度,让滚动正常工作
|
||||||
|
docEl.style.height = (body.scrollHeight * scale) + 'px'
|
||||||
|
docEl.style.overflow = 'auto'
|
||||||
|
} else {
|
||||||
|
// PC 端:移除缩放
|
||||||
|
body.style.width = ''
|
||||||
|
body.style.transform = ''
|
||||||
|
body.style.transformOrigin = ''
|
||||||
|
docEl.style.height = ''
|
||||||
|
docEl.style.overflow = ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听窗口变化
|
||||||
|
window.addEventListener('resize', recalc, false)
|
||||||
|
window.addEventListener('orientationchange', recalc, false)
|
||||||
|
|
||||||
|
// 初始执行,延迟确保 DOM 加载完成
|
||||||
|
if (document.readyState === 'complete') {
|
||||||
|
recalc()
|
||||||
|
} else {
|
||||||
|
window.addEventListener('load', recalc)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听 DOM 变化,更新高度
|
||||||
|
const observer = new MutationObserver(() => {
|
||||||
|
if (isMobile()) {
|
||||||
|
const scale = docEl.clientWidth / designWidth
|
||||||
|
docEl.style.height = (document.body.scrollHeight * scale) + 'px'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
observer.observe(document.body, { childList: true, subtree: true })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default remAdaptPlugin
|
||||||
@@ -183,3 +183,103 @@ export async function loadResumeTemplateData(resumeId: string): Promise<ResumeTe
|
|||||||
certificates: r.certificates || [],
|
certificates: r.certificates || [],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==================== 生成文件对象(不触发下载,用于上传到服务器) ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成简历PDF的File对象(不触发浏览器下载)
|
||||||
|
* @param element 简历DOM元素
|
||||||
|
* @param fileName 文件名(不含扩展名)
|
||||||
|
* @returns PDF格式的File对象
|
||||||
|
*/
|
||||||
|
export async function generateResumePdfFile(element: HTMLElement, fileName: string): Promise<File> {
|
||||||
|
const options = {
|
||||||
|
margin: [10, 10, 10, 10] as [number, number, number, number],
|
||||||
|
filename: `${fileName}.pdf`,
|
||||||
|
image: { type: 'jpeg' as const, quality: 0.98 },
|
||||||
|
html2canvas: { scale: 2, useCORS: true, logging: false },
|
||||||
|
jsPDF: { unit: 'mm', format: 'a4', orientation: 'portrait' as const },
|
||||||
|
pagebreak: { mode: ['css', 'legacy'] },
|
||||||
|
}
|
||||||
|
// 使用 outputPdf('blob') 获取 Blob,不触发下载
|
||||||
|
const blob: Blob = await html2pdf().set(options).from(element).outputPdf('blob')
|
||||||
|
return new File([blob], `${fileName}.pdf`, { type: 'application/pdf' })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成简历Word的File对象(不触发浏览器下载)
|
||||||
|
* @param element 简历DOM元素
|
||||||
|
* @param fileName 文件名(不含扩展名)
|
||||||
|
* @returns Word格式的File对象
|
||||||
|
*/
|
||||||
|
export function generateResumeWordFile(element: HTMLElement, fileName: string): File {
|
||||||
|
// 复用与 exportResumeWord 相同的 Word HTML 组装逻辑
|
||||||
|
const wordCss = `
|
||||||
|
@page WordSection1 {
|
||||||
|
size: 595.3pt 841.9pt;
|
||||||
|
mso-page-orientation: portrait;
|
||||||
|
mso-header-margin: 0pt;
|
||||||
|
mso-footer-margin: 0pt;
|
||||||
|
mso-paper-source: 0;
|
||||||
|
margin-top: 72pt;
|
||||||
|
margin-right: 54pt;
|
||||||
|
margin-bottom: 72pt;
|
||||||
|
margin-left: 54pt;
|
||||||
|
}
|
||||||
|
div.WordSection1 { page: WordSection1; }
|
||||||
|
body { font-family: 'SimSun', 'Songti SC', serif; color: #000; line-height: 1.6; margin: 0; padding: 0; }
|
||||||
|
.job-resume-template { width: 100%; background: #fff; box-sizing: border-box; }
|
||||||
|
.resume-html { padding: 0; font-family: 'SimSun', 'Songti SC', serif; color: #000; line-height: 1.6; }
|
||||||
|
.resume-html__name { font-size: 24pt; font-weight: 700; margin: 0 0 4.8pt 0; line-height: 1.3; }
|
||||||
|
.resume-html__contact { font-size: 12pt; color: #000; margin-bottom: 3.6pt; line-height: 1.5; }
|
||||||
|
.resume-html__contact-row { display: flex; align-items: center; gap: 0; }
|
||||||
|
.resume-html__separator { margin: 0 4.8pt; color: #777; }
|
||||||
|
.resume-html__section-title { font-size: 17.3pt; font-weight: 700; color: #000; margin-top: 20pt; margin-bottom: 10pt; line-height: 1.3; }
|
||||||
|
.resume-html__divider { height: 1.5px; background: #000; margin-bottom: 9.6pt; }
|
||||||
|
.resume-html__summary { font-size: 12pt; line-height: 1.7; margin-bottom: 4.8pt; }
|
||||||
|
.resume-html__item { margin-bottom: 9.6pt; }
|
||||||
|
.resume-html__item-header { display: flex; align-items: flex-start; justify-content: space-between; margin-bottom: 2pt; }
|
||||||
|
.resume-html__item-main { font-size: 13.2pt; font-weight: 600; color: #000; line-height: 1.4; }
|
||||||
|
.resume-html__item-right { display: flex; align-items: center; gap: 4.8pt; flex-shrink: 0; text-align: right; }
|
||||||
|
.resume-html__item-location { font-size: 12pt; color: #000; }
|
||||||
|
.resume-html__item-date { font-size: 12pt; color: #000; white-space: nowrap; }
|
||||||
|
.resume-html__item-desc { font-size: 12pt; color: #000; line-height: 1.7; margin-top: 2pt; }
|
||||||
|
.resume-html__desc-list { margin: 0; padding-left: 0; list-style: none; mso-list: none; }
|
||||||
|
.resume-html__desc-list li { font-size: 12pt; line-height: 1.7; color: #000; list-style: none; mso-list: none; margin-left: 0; padding-left: 24pt; text-indent: 0; }
|
||||||
|
.resume-html__skills { font-size: 12pt; line-height: 1.7; }
|
||||||
|
.resume-html__skill-row { margin-bottom: 2.4pt; }
|
||||||
|
.resume-html__skill-label { font-weight: 600; }
|
||||||
|
.resume-html__diff-highlight { background-color: #D4EDDA; color: #155724; border-radius: 2px; padding: 0 1px; }
|
||||||
|
`
|
||||||
|
|
||||||
|
const parts: string[] = []
|
||||||
|
parts.push('<html xmlns:o="urn:schemas-microsoft-com:office:office"')
|
||||||
|
parts.push(' xmlns:w="urn:schemas-microsoft-com:office:word"')
|
||||||
|
parts.push(' xmlns="http://www.w3.org/TR/REC-html40">')
|
||||||
|
parts.push('<head>')
|
||||||
|
parts.push('<meta charset="utf-8">')
|
||||||
|
parts.push('<meta name="ProgId" content="Word.Document">')
|
||||||
|
parts.push('<meta name="Generator" content="Microsoft Word 15">')
|
||||||
|
parts.push('<!--[if gte mso 9]><xml>')
|
||||||
|
parts.push('<w:WordDocument>')
|
||||||
|
parts.push('<w:View>Print</w:View>')
|
||||||
|
parts.push('<w:Zoom>100</w:Zoom>')
|
||||||
|
parts.push('</w:WordDocument>')
|
||||||
|
parts.push('</xml><![endif]-->')
|
||||||
|
parts.push('<style>' + wordCss + '</style>')
|
||||||
|
parts.push('</head>')
|
||||||
|
parts.push('<body>')
|
||||||
|
parts.push('<!--[if gte mso 9]><xml>')
|
||||||
|
parts.push('<w:WordDocument>')
|
||||||
|
parts.push('<w:BrowserLevel>MicrosoftInternetExplorer4</w:BrowserLevel>')
|
||||||
|
parts.push('</w:WordDocument>')
|
||||||
|
parts.push('</xml><![endif]-->')
|
||||||
|
parts.push('<div class="WordSection1">' + element.outerHTML + '</div>')
|
||||||
|
parts.push('</body>')
|
||||||
|
parts.push('</html>')
|
||||||
|
const fullHtml = parts.join('\n')
|
||||||
|
|
||||||
|
// 生成File对象,不触发下载
|
||||||
|
const blob = new Blob([fullHtml], { type: 'application/msword' })
|
||||||
|
return new File([blob], `${fileName}.doc`, { type: 'application/msword' })
|
||||||
|
}
|
||||||
|
|||||||
@@ -157,6 +157,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 求职目标设置弹窗(AI 对话 editPreference 工具触发) -->
|
||||||
|
<JobGoalDialog v-model="showJobGoalDialog" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -177,6 +180,7 @@ import type { JobListItem } from '@/api/jobs'
|
|||||||
import { fetchResumeList } from '@/api/resume'
|
import { fetchResumeList } from '@/api/resume'
|
||||||
import { getIntentionCategoryNames, getIntentionRegionNames, getIntentionIndustryNames } from '@/utils/intention'
|
import { getIntentionCategoryNames, getIntentionRegionNames, getIntentionIndustryNames } from '@/utils/intention'
|
||||||
import AiThinkingIndicator from '@/components/tools/AiThinkingIndicator.vue'
|
import AiThinkingIndicator from '@/components/tools/AiThinkingIndicator.vue'
|
||||||
|
import JobGoalDialog from '@/components/JobGoalDialog.vue'
|
||||||
|
|
||||||
/** 页面初始加载状态 */
|
/** 页面初始加载状态 */
|
||||||
const pageLoading = ref(true)
|
const pageLoading = ref(true)
|
||||||
@@ -228,6 +232,9 @@ const isSending = ref(false)
|
|||||||
/** 是否正在投递流程中(防止重复点击开始按钮) */
|
/** 是否正在投递流程中(防止重复点击开始按钮) */
|
||||||
const isApplying = ref(false)
|
const isApplying = ref(false)
|
||||||
|
|
||||||
|
/** 是否显示求职目标设置弹窗(由 AI 对话 editPreference 工具触发) */
|
||||||
|
const showJobGoalDialog = ref(false)
|
||||||
|
|
||||||
/** 是否暂停投递 */
|
/** 是否暂停投递 */
|
||||||
const isPaused = ref(false)
|
const isPaused = ref(false)
|
||||||
|
|
||||||
@@ -1170,6 +1177,25 @@ async function handleSendMessage() {
|
|||||||
} catch {
|
} catch {
|
||||||
console.error('[Agent] 推荐岗位请求失败')
|
console.error('[Agent] 推荐岗位请求失败')
|
||||||
}
|
}
|
||||||
|
} else if (tool === 'editPreference') {
|
||||||
|
/* AI 要求打开偏好设置弹窗 — 先插入 AI 回复消息,再弹出求职目标设置弹窗 */
|
||||||
|
const aiNow = Math.floor(Date.now() / 1000)
|
||||||
|
chatMessages.value.push({
|
||||||
|
id: Date.now() + 1,
|
||||||
|
type: 'assistant',
|
||||||
|
content: aiContent,
|
||||||
|
extra: '',
|
||||||
|
createTime: { seconds: aiNow, nanos: 0 },
|
||||||
|
})
|
||||||
|
try {
|
||||||
|
await addAgentChatMessage({ type: 'assistant', content: aiContent })
|
||||||
|
} catch { /* 静默 */ }
|
||||||
|
|
||||||
|
await nextTick()
|
||||||
|
scrollChatToBottom()
|
||||||
|
|
||||||
|
/* 打开求职目标设置弹窗 */
|
||||||
|
showJobGoalDialog.value = true
|
||||||
} else {
|
} else {
|
||||||
/* 普通 AI 回复 — 直接插入 assistant 消息 */
|
/* 普通 AI 回复 — 直接插入 assistant 消息 */
|
||||||
const aiNow = Math.floor(Date.now() / 1000)
|
const aiNow = Math.floor(Date.now() / 1000)
|
||||||
|
|||||||
+5
-4
@@ -421,10 +421,11 @@
|
|||||||
<div class="home-footer__col">
|
<div class="home-footer__col">
|
||||||
<h5>求职资源</h5>
|
<h5>求职资源</h5>
|
||||||
<ul>
|
<ul>
|
||||||
<li>求职指南</li>
|
<li><a href="https://www.jianshixingqiu.com/tutoring" target="_blank">一站式求职辅导</a></li>
|
||||||
<li>面试技巧</li>
|
<li><a href="https://www.jianshixingqiu.com/internship" target="_blank">名企实习</a></li>
|
||||||
<li>简历模版</li>
|
<li><a href="https://www.jianshixingqiu.com/biunique" target="_blank">名企导师1V1</a></li>
|
||||||
<li>行业分析</li>
|
<li><a href="https://www.jianshixingqiu.com/directTrain" target="_blank">Offer直通车</a></li>
|
||||||
|
<li><a href="https://www.jianshixingqiu.com/mentor" target="_blank">名企导师</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div class="home-footer__col">
|
<div class="home-footer__col">
|
||||||
|
|||||||
+1
-1
@@ -251,7 +251,7 @@
|
|||||||
<div v-else-if="noMore && jobList.length > 0" class="jobs-page__loading-more">没有更多符合的职位了</div>
|
<div v-else-if="noMore && jobList.length > 0" class="jobs-page__loading-more">没有更多符合的职位了</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<AiChat :job-id="currentAskJobId" />
|
<AiChat :jobId="currentAskJobId" />
|
||||||
|
|
||||||
<!-- 职位不感兴趣反馈弹窗 -->
|
<!-- 职位不感兴趣反馈弹窗 -->
|
||||||
<JobDislikeDialog ref="dislikeDialogRef" v-model="showDislikeDialog" :job-id="dislikeJobId" @disliked="removeDislikedJob" />
|
<JobDislikeDialog ref="dislikeDialogRef" v-model="showDislikeDialog" :job-id="dislikeJobId" @disliked="removeDislikedJob" />
|
||||||
|
|||||||
Reference in New Issue
Block a user