简历上传,pdf简历下载
This commit is contained in:
Vendored
+1
@@ -6,6 +6,7 @@
|
||||
// biome-ignore lint: disable
|
||||
export {}
|
||||
declare global {
|
||||
const ElLoading: typeof import('element-plus/es').ElLoading
|
||||
const ElMessage: typeof import('element-plus/es').ElMessage
|
||||
const ElMessageBox: typeof import('element-plus/es').ElMessageBox
|
||||
}
|
||||
|
||||
Vendored
+3
@@ -42,4 +42,7 @@ declare module 'vue' {
|
||||
SettingsDialog: typeof import('./src/components/SettingsDialog.vue')['default']
|
||||
SideNav: typeof import('./src/components/SideNav.vue')['default']
|
||||
}
|
||||
export interface GlobalDirectives {
|
||||
vLoading: typeof import('element-plus/es')['ElLoadingDirective']
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
"@element-plus/icons-vue": "^2.3.2",
|
||||
"axios": "^1.13.6",
|
||||
"element-plus": "^2.13.3",
|
||||
"html2pdf.js": "^0.14.0",
|
||||
"sass": "^1.97.3",
|
||||
"vue": "^3.5.25",
|
||||
"vue-router": "^4.6.4",
|
||||
|
||||
Generated
+179
@@ -17,6 +17,9 @@ importers:
|
||||
element-plus:
|
||||
specifier: ^2.13.3
|
||||
version: 2.13.3(vue@3.5.29(typescript@5.9.3))
|
||||
html2pdf.js:
|
||||
specifier: ^0.14.0
|
||||
version: 0.14.0
|
||||
sass:
|
||||
specifier: ^1.97.3
|
||||
version: 1.97.3
|
||||
@@ -83,6 +86,10 @@ packages:
|
||||
engines: {node: '>=6.0.0'}
|
||||
hasBin: true
|
||||
|
||||
'@babel/runtime@7.29.2':
|
||||
resolution: {integrity: sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@babel/types@7.29.0':
|
||||
resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
@@ -532,6 +539,15 @@ packages:
|
||||
'@types/node@24.11.0':
|
||||
resolution: {integrity: sha512-fPxQqz4VTgPI/IQ+lj9r0h+fDR66bzoeMGHp8ASee+32OSGIkeASsoZuJixsQoVef1QJbeubcPBxKk22QVoWdw==}
|
||||
|
||||
'@types/pako@2.0.4':
|
||||
resolution: {integrity: sha512-VWDCbrLeVXJM9fihYodcLiIv0ku+AlOa/TQ1SvYOaBuyrSKgEcro95LJyIsJ4vSo6BXIxOKxiJAat04CmST9Fw==}
|
||||
|
||||
'@types/raf@3.4.3':
|
||||
resolution: {integrity: sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw==}
|
||||
|
||||
'@types/trusted-types@2.0.7':
|
||||
resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==}
|
||||
|
||||
'@types/web-bluetooth@0.0.20':
|
||||
resolution: {integrity: sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==}
|
||||
|
||||
@@ -717,6 +733,10 @@ packages:
|
||||
bare-url@2.3.2:
|
||||
resolution: {integrity: sha512-ZMq4gd9ngV5aTMa5p9+UfY0b3skwhHELaDkhEHetMdX0LRkW9kzaym4oo/Eh+Ghm0CCDuMTsRIGM/ytUc1ZYmw==}
|
||||
|
||||
base64-arraybuffer@1.0.2:
|
||||
resolution: {integrity: sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==}
|
||||
engines: {node: '>= 0.6.0'}
|
||||
|
||||
basic-ftp@5.2.0:
|
||||
resolution: {integrity: sha512-VoMINM2rqJwJgfdHq6RiUudKt2BV+FY5ZFezP/ypmwayk68+NzzAQy4XXLlqsGD4MCzq3DrmNFD/uUmBJuGoXw==}
|
||||
engines: {node: '>=10.0.0'}
|
||||
@@ -748,6 +768,10 @@ packages:
|
||||
resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
canvg@3.0.11:
|
||||
resolution: {integrity: sha512-5ON+q7jCTgMp9cjpu4Jo6XbvfYwSB2Ow3kzHKfIyJfaCAOHLbdKPQqGKgfED/R5B+3TFFfe8pegYA+b423SRyA==}
|
||||
engines: {node: '>=10.0.0'}
|
||||
|
||||
chokidar@4.0.3:
|
||||
resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==}
|
||||
engines: {node: '>= 14.16.0'}
|
||||
@@ -797,6 +821,9 @@ packages:
|
||||
resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
||||
core-js@3.49.0:
|
||||
resolution: {integrity: sha512-es1U2+YTtzpwkxVLwAFdSpaIMyQaq0PBgm3YD1W3Qpsn1NAmO3KSgZfu+oGSWVu6NvLHoHCV/aYcsE5wiB7ALg==}
|
||||
|
||||
cosmiconfig@9.0.1:
|
||||
resolution: {integrity: sha512-hr4ihw+DBqcvrsEDioRO31Z17x71pUYoNe/4h6Z0wB72p7MU7/9gH8Q3s12NFhHPfYBBOV3qyfUxmr/Yn3shnQ==}
|
||||
engines: {node: '>=14'}
|
||||
@@ -806,6 +833,9 @@ packages:
|
||||
typescript:
|
||||
optional: true
|
||||
|
||||
css-line-break@2.1.0:
|
||||
resolution: {integrity: sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==}
|
||||
|
||||
csstype@3.2.3:
|
||||
resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==}
|
||||
|
||||
@@ -856,6 +886,9 @@ packages:
|
||||
devtools-protocol@0.0.1566079:
|
||||
resolution: {integrity: sha512-MJfAEA1UfVhSs7fbSQOG4czavUp1ajfg6prlAN0+cmfa2zNjaIbvq8VneP7do1WAQQIvgNJWSMeP6UyI90gIlQ==}
|
||||
|
||||
dompurify@3.3.3:
|
||||
resolution: {integrity: sha512-Oj6pzI2+RqBfFG+qOaOLbFXLQ90ARpcGG6UePL82bJLtdsa6CYJD7nmiU8MW9nQNOtCHV3lZ/Bzq1X0QYbBZCA==}
|
||||
|
||||
dunder-proto@1.0.1:
|
||||
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -973,6 +1006,9 @@ packages:
|
||||
fast-fifo@1.3.2:
|
||||
resolution: {integrity: sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==}
|
||||
|
||||
fast-png@6.4.0:
|
||||
resolution: {integrity: sha512-kAqZq1TlgBjZcLr5mcN6NP5Rv4V2f22z00c3g8vRrwkcqjerx7BEhPbOnWCPqaHUl2XWQBJQvOT/FQhdMT7X/Q==}
|
||||
|
||||
fast-uri@3.1.0:
|
||||
resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==}
|
||||
|
||||
@@ -988,6 +1024,9 @@ packages:
|
||||
picomatch:
|
||||
optional: true
|
||||
|
||||
fflate@0.8.2:
|
||||
resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==}
|
||||
|
||||
fill-range@7.1.1:
|
||||
resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -1061,6 +1100,13 @@ packages:
|
||||
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
html2canvas@1.4.1:
|
||||
resolution: {integrity: sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==}
|
||||
engines: {node: '>=8.0.0'}
|
||||
|
||||
html2pdf.js@0.14.0:
|
||||
resolution: {integrity: sha512-yvNJgE/8yru2UeGflkPdjW8YEY+nDH5X7/2WG4uiuSCwYiCp8PZ8EKNiTAa6HxJ1NjC51fZSIEq6xld5CADKBQ==}
|
||||
|
||||
http-errors@2.0.1:
|
||||
resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==}
|
||||
engines: {node: '>= 0.8'}
|
||||
@@ -1100,6 +1146,9 @@ packages:
|
||||
inherits@2.0.4:
|
||||
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
|
||||
|
||||
iobuffer@5.4.0:
|
||||
resolution: {integrity: sha512-DRebOWuqDvxunfkNJAlc3IzWIPD5xVxwUNbHr7xKB8E6aLJxIPfNX3CoMJghcFjpv6RWQsrcJbghtEwSPoJqMA==}
|
||||
|
||||
ip-address@10.1.0:
|
||||
resolution: {integrity: sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==}
|
||||
engines: {node: '>= 12'}
|
||||
@@ -1147,6 +1196,9 @@ packages:
|
||||
json-schema-traverse@1.0.0:
|
||||
resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==}
|
||||
|
||||
jspdf@4.2.1:
|
||||
resolution: {integrity: sha512-YyAXyvnmjTbR4bHQRLzex3CuINCDlQnBqoSYyjJwTP2x9jDLuKDzy7aKUl0hgx3uhcl7xzg32agn5vlie6HIlQ==}
|
||||
|
||||
lines-and-columns@1.2.4:
|
||||
resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==}
|
||||
|
||||
@@ -1265,6 +1317,9 @@ packages:
|
||||
resolution: {integrity: sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==}
|
||||
engines: {node: '>= 14'}
|
||||
|
||||
pako@2.1.0:
|
||||
resolution: {integrity: sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==}
|
||||
|
||||
parent-module@1.0.1:
|
||||
resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
|
||||
engines: {node: '>=6'}
|
||||
@@ -1289,6 +1344,9 @@ packages:
|
||||
pend@1.2.0:
|
||||
resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==}
|
||||
|
||||
performance-now@2.1.0:
|
||||
resolution: {integrity: sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==}
|
||||
|
||||
picocolors@1.1.1:
|
||||
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
|
||||
|
||||
@@ -1351,6 +1409,9 @@ packages:
|
||||
quansync@0.2.11:
|
||||
resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==}
|
||||
|
||||
raf@3.4.1:
|
||||
resolution: {integrity: sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==}
|
||||
|
||||
range-parser@1.2.1:
|
||||
resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==}
|
||||
engines: {node: '>= 0.6'}
|
||||
@@ -1367,6 +1428,9 @@ packages:
|
||||
resolution: {integrity: sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==}
|
||||
engines: {node: '>= 20.19.0'}
|
||||
|
||||
regenerator-runtime@0.13.11:
|
||||
resolution: {integrity: sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==}
|
||||
|
||||
require-directory@2.1.1:
|
||||
resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@@ -1382,6 +1446,10 @@ packages:
|
||||
resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
|
||||
engines: {node: '>=4'}
|
||||
|
||||
rgbcolor@1.0.1:
|
||||
resolution: {integrity: sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw==}
|
||||
engines: {node: '>= 0.8.15'}
|
||||
|
||||
rollup@4.59.0:
|
||||
resolution: {integrity: sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==}
|
||||
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
|
||||
@@ -1461,6 +1529,10 @@ packages:
|
||||
resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
stackblur-canvas@2.7.0:
|
||||
resolution: {integrity: sha512-yf7OENo23AGJhBriGx0QivY5JP6Y1HbrrDI6WLt6C5auYZXlQrheoY8hD4ibekFKz1HOfE48Ww8kMWMnJD/zcQ==}
|
||||
engines: {node: '>=0.1.14'}
|
||||
|
||||
statuses@2.0.2:
|
||||
resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==}
|
||||
engines: {node: '>= 0.8'}
|
||||
@@ -1483,6 +1555,10 @@ packages:
|
||||
strip-literal@3.1.0:
|
||||
resolution: {integrity: sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==}
|
||||
|
||||
svg-pathdata@6.0.3:
|
||||
resolution: {integrity: sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw==}
|
||||
engines: {node: '>=12.0.0'}
|
||||
|
||||
tar-fs@3.1.1:
|
||||
resolution: {integrity: sha512-LZA0oaPOc2fVo82Txf3gw+AkEd38szODlptMYejQUhndHMLQ9M059uXR+AfS7DNo0NpINvSqDsvyaCrBVkptWg==}
|
||||
|
||||
@@ -1495,6 +1571,9 @@ packages:
|
||||
text-decoder@1.2.7:
|
||||
resolution: {integrity: sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==}
|
||||
|
||||
text-segmentation@1.0.3:
|
||||
resolution: {integrity: sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==}
|
||||
|
||||
tinyglobby@0.2.15:
|
||||
resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==}
|
||||
engines: {node: '>=12.0.0'}
|
||||
@@ -1574,6 +1653,9 @@ packages:
|
||||
resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==}
|
||||
engines: {node: '>= 0.4.0'}
|
||||
|
||||
utrie@1.0.2:
|
||||
resolution: {integrity: sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==}
|
||||
|
||||
vary@1.1.2:
|
||||
resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==}
|
||||
engines: {node: '>= 0.8'}
|
||||
@@ -1715,6 +1797,8 @@ snapshots:
|
||||
dependencies:
|
||||
'@babel/types': 7.29.0
|
||||
|
||||
'@babel/runtime@7.29.2': {}
|
||||
|
||||
'@babel/types@7.29.0':
|
||||
dependencies:
|
||||
'@babel/helper-string-parser': 7.27.1
|
||||
@@ -2045,6 +2129,14 @@ snapshots:
|
||||
dependencies:
|
||||
undici-types: 7.16.0
|
||||
|
||||
'@types/pako@2.0.4': {}
|
||||
|
||||
'@types/raf@3.4.3':
|
||||
optional: true
|
||||
|
||||
'@types/trusted-types@2.0.7':
|
||||
optional: true
|
||||
|
||||
'@types/web-bluetooth@0.0.20': {}
|
||||
|
||||
'@types/yauzl@2.10.3':
|
||||
@@ -2250,6 +2342,8 @@ snapshots:
|
||||
dependencies:
|
||||
bare-path: 3.0.0
|
||||
|
||||
base64-arraybuffer@1.0.2: {}
|
||||
|
||||
basic-ftp@5.2.0: {}
|
||||
|
||||
body-parser@1.20.4:
|
||||
@@ -2290,6 +2384,18 @@ snapshots:
|
||||
|
||||
callsites@3.1.0: {}
|
||||
|
||||
canvg@3.0.11:
|
||||
dependencies:
|
||||
'@babel/runtime': 7.29.2
|
||||
'@types/raf': 3.4.3
|
||||
core-js: 3.49.0
|
||||
raf: 3.4.1
|
||||
regenerator-runtime: 0.13.11
|
||||
rgbcolor: 1.0.1
|
||||
stackblur-canvas: 2.7.0
|
||||
svg-pathdata: 6.0.3
|
||||
optional: true
|
||||
|
||||
chokidar@4.0.3:
|
||||
dependencies:
|
||||
readdirp: 4.1.2
|
||||
@@ -2334,6 +2440,9 @@ snapshots:
|
||||
|
||||
cookie@0.7.2: {}
|
||||
|
||||
core-js@3.49.0:
|
||||
optional: true
|
||||
|
||||
cosmiconfig@9.0.1(typescript@5.9.3):
|
||||
dependencies:
|
||||
env-paths: 2.2.1
|
||||
@@ -2343,6 +2452,10 @@ snapshots:
|
||||
optionalDependencies:
|
||||
typescript: 5.9.3
|
||||
|
||||
css-line-break@2.1.0:
|
||||
dependencies:
|
||||
utrie: 1.0.2
|
||||
|
||||
csstype@3.2.3: {}
|
||||
|
||||
data-uri-to-buffer@6.0.2: {}
|
||||
@@ -2374,6 +2487,10 @@ snapshots:
|
||||
|
||||
devtools-protocol@0.0.1566079: {}
|
||||
|
||||
dompurify@3.3.3:
|
||||
optionalDependencies:
|
||||
'@types/trusted-types': 2.0.7
|
||||
|
||||
dunder-proto@1.0.1:
|
||||
dependencies:
|
||||
call-bind-apply-helpers: 1.0.2
|
||||
@@ -2551,6 +2668,12 @@ snapshots:
|
||||
|
||||
fast-fifo@1.3.2: {}
|
||||
|
||||
fast-png@6.4.0:
|
||||
dependencies:
|
||||
'@types/pako': 2.0.4
|
||||
iobuffer: 5.4.0
|
||||
pako: 2.1.0
|
||||
|
||||
fast-uri@3.1.0: {}
|
||||
|
||||
fd-slicer@1.1.0:
|
||||
@@ -2561,6 +2684,8 @@ snapshots:
|
||||
optionalDependencies:
|
||||
picomatch: 4.0.3
|
||||
|
||||
fflate@0.8.2: {}
|
||||
|
||||
fill-range@7.1.1:
|
||||
dependencies:
|
||||
to-regex-range: 5.0.1
|
||||
@@ -2641,6 +2766,17 @@ snapshots:
|
||||
dependencies:
|
||||
function-bind: 1.1.2
|
||||
|
||||
html2canvas@1.4.1:
|
||||
dependencies:
|
||||
css-line-break: 2.1.0
|
||||
text-segmentation: 1.0.3
|
||||
|
||||
html2pdf.js@0.14.0:
|
||||
dependencies:
|
||||
dompurify: 3.3.3
|
||||
html2canvas: 1.4.1
|
||||
jspdf: 4.2.1
|
||||
|
||||
http-errors@2.0.1:
|
||||
dependencies:
|
||||
depd: 2.0.0
|
||||
@@ -2696,6 +2832,8 @@ snapshots:
|
||||
|
||||
inherits@2.0.4: {}
|
||||
|
||||
iobuffer@5.4.0: {}
|
||||
|
||||
ip-address@10.1.0: {}
|
||||
|
||||
ipaddr.js@1.9.1: {}
|
||||
@@ -2730,6 +2868,17 @@ snapshots:
|
||||
|
||||
json-schema-traverse@1.0.0: {}
|
||||
|
||||
jspdf@4.2.1:
|
||||
dependencies:
|
||||
'@babel/runtime': 7.29.2
|
||||
fast-png: 6.4.0
|
||||
fflate: 0.8.2
|
||||
optionalDependencies:
|
||||
canvg: 3.0.11
|
||||
core-js: 3.49.0
|
||||
dompurify: 3.3.3
|
||||
html2canvas: 1.4.1
|
||||
|
||||
lines-and-columns@1.2.4: {}
|
||||
|
||||
local-pkg@1.1.2:
|
||||
@@ -2834,6 +2983,8 @@ snapshots:
|
||||
degenerator: 5.0.1
|
||||
netmask: 2.0.2
|
||||
|
||||
pako@2.1.0: {}
|
||||
|
||||
parent-module@1.0.1:
|
||||
dependencies:
|
||||
callsites: 3.1.0
|
||||
@@ -2855,6 +3006,9 @@ snapshots:
|
||||
|
||||
pend@1.2.0: {}
|
||||
|
||||
performance-now@2.1.0:
|
||||
optional: true
|
||||
|
||||
picocolors@1.1.1: {}
|
||||
|
||||
picomatch@2.3.1:
|
||||
@@ -2956,6 +3110,11 @@ snapshots:
|
||||
|
||||
quansync@0.2.11: {}
|
||||
|
||||
raf@3.4.1:
|
||||
dependencies:
|
||||
performance-now: 2.1.0
|
||||
optional: true
|
||||
|
||||
range-parser@1.2.1: {}
|
||||
|
||||
raw-body@2.5.3:
|
||||
@@ -2969,6 +3128,9 @@ snapshots:
|
||||
|
||||
readdirp@5.0.0: {}
|
||||
|
||||
regenerator-runtime@0.13.11:
|
||||
optional: true
|
||||
|
||||
require-directory@2.1.1: {}
|
||||
|
||||
require-from-string@2.0.2: {}
|
||||
@@ -2978,6 +3140,9 @@ snapshots:
|
||||
|
||||
resolve-from@4.0.0: {}
|
||||
|
||||
rgbcolor@1.0.1:
|
||||
optional: true
|
||||
|
||||
rollup@4.59.0:
|
||||
dependencies:
|
||||
'@types/estree': 1.0.8
|
||||
@@ -3116,6 +3281,9 @@ snapshots:
|
||||
source-map@0.6.1:
|
||||
optional: true
|
||||
|
||||
stackblur-canvas@2.7.0:
|
||||
optional: true
|
||||
|
||||
statuses@2.0.2: {}
|
||||
|
||||
stoppable@1.1.0: {}
|
||||
@@ -3143,6 +3311,9 @@ snapshots:
|
||||
dependencies:
|
||||
js-tokens: 9.0.1
|
||||
|
||||
svg-pathdata@6.0.3:
|
||||
optional: true
|
||||
|
||||
tar-fs@3.1.1:
|
||||
dependencies:
|
||||
pump: 3.0.4
|
||||
@@ -3179,6 +3350,10 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- react-native-b4a
|
||||
|
||||
text-segmentation@1.0.3:
|
||||
dependencies:
|
||||
utrie: 1.0.2
|
||||
|
||||
tinyglobby@0.2.15:
|
||||
dependencies:
|
||||
fdir: 6.5.0(picomatch@4.0.3)
|
||||
@@ -3265,6 +3440,10 @@ snapshots:
|
||||
|
||||
utils-merge@1.0.1: {}
|
||||
|
||||
utrie@1.0.2:
|
||||
dependencies:
|
||||
base64-arraybuffer: 1.0.2
|
||||
|
||||
vary@1.1.2: {}
|
||||
|
||||
vite@7.3.1(@types/node@24.11.0)(sass@1.97.3):
|
||||
|
||||
@@ -0,0 +1,372 @@
|
||||
import request from '@/utils/request'
|
||||
import type { ApiResult } from '@/api/auth'
|
||||
|
||||
// ==================== 简历列表相关 ====================
|
||||
|
||||
/** 时间戳结构(Instant) */
|
||||
export interface InstantTime {
|
||||
/** 秒级时间戳 */
|
||||
seconds?: number
|
||||
/** 纳秒部分 */
|
||||
nanos?: number
|
||||
}
|
||||
|
||||
/** 简历列表项 */
|
||||
export interface ResumeListItem {
|
||||
/** 简历 ID(字符串,避免大整数精度丢失) */
|
||||
id?: string
|
||||
/** 简历名称 */
|
||||
resumeName?: string
|
||||
/** 目标岗位 */
|
||||
targetPosition?: string
|
||||
/** 是否默认简历 0=否 1=是 */
|
||||
isDefault?: number
|
||||
/** 简历修改时间 */
|
||||
updateTime?: InstantTime
|
||||
/** 简历创建时间 */
|
||||
createTime?: InstantTime
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取简历列表
|
||||
* GET /resume/list
|
||||
*/
|
||||
export function fetchResumeList() {
|
||||
return request.get<any, ApiResult<ResumeListItem[]>>('/resume/list')
|
||||
}
|
||||
|
||||
// ==================== 描述段落(通用) ====================
|
||||
|
||||
/** 描述段落 */
|
||||
export interface DescriptionParagraph {
|
||||
/** 段落标识,前端生成的短ID */
|
||||
id?: string
|
||||
/** 段落文本内容 */
|
||||
text?: string
|
||||
}
|
||||
|
||||
// ==================== 简历主表 ====================
|
||||
|
||||
/** 简历主表数据 */
|
||||
export interface ResumeMainData {
|
||||
/** 简历 ID(字符串,避免大整数精度丢失) */
|
||||
id?: string
|
||||
/** 简历名称 */
|
||||
resumeName?: string
|
||||
/** 目标岗位 */
|
||||
targetPosition?: string
|
||||
/** 是否默认简历 0=否 1=是 */
|
||||
isDefault?: number
|
||||
/** 头像URL */
|
||||
avatarUrl?: string
|
||||
/** 真实姓名 */
|
||||
name?: string
|
||||
/** 邮箱 */
|
||||
email?: string
|
||||
/** 手机号码 */
|
||||
mobileNumber?: string
|
||||
/** 所在城市 */
|
||||
city?: string
|
||||
/** 微信号 */
|
||||
wechatNumber?: string
|
||||
/** 作品集链接 */
|
||||
portfolioUrl?: string
|
||||
/** 技能标签列表 */
|
||||
skills?: string[]
|
||||
/** 证书标签列表 */
|
||||
certificates?: string[]
|
||||
/** 个人概述 */
|
||||
summary?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询简历主表
|
||||
* GET /resume?resumeId=xxx
|
||||
*/
|
||||
export function fetchResumeMain(resumeId: string) {
|
||||
return request.get<any, ApiResult<ResumeMainData>>('/resume', {
|
||||
params: { resumeId },
|
||||
})
|
||||
}
|
||||
|
||||
// ==================== 教育经历 ====================
|
||||
|
||||
/** 教育经历项 */
|
||||
export interface ResumeEducation {
|
||||
/** ID(字符串,避免大整数精度丢失) */
|
||||
id?: string
|
||||
/** 学校名称 */
|
||||
school?: string
|
||||
/** 专业 */
|
||||
major?: string
|
||||
/** 学历:大专/本科/硕士/博士 */
|
||||
degree?: string
|
||||
/** 学习形式:全日制/非全日制 */
|
||||
studyType?: string
|
||||
/** 开始时间,格式:2023.09 */
|
||||
startDate?: string
|
||||
/** 结束时间,格式:2024.06 */
|
||||
endDate?: string
|
||||
/** 描述段落 */
|
||||
description?: DescriptionParagraph[]
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询简历的教育经历列表
|
||||
* GET /resume/education?resumeId=xxx
|
||||
*/
|
||||
export function fetchResumeEducation(resumeId: string) {
|
||||
return request.get<any, ApiResult<ResumeEducation[]>>('/resume/education', {
|
||||
params: { resumeId },
|
||||
})
|
||||
}
|
||||
|
||||
// ==================== 工作经历 ====================
|
||||
|
||||
/** 工作经历项 */
|
||||
export interface ResumeWork {
|
||||
/** ID(字符串,避免大整数精度丢失) */
|
||||
id?: string
|
||||
/** 公司名称 */
|
||||
companyName?: string
|
||||
/** 职位 */
|
||||
position?: string
|
||||
/** 开始时间 */
|
||||
startDate?: string
|
||||
/** 结束时间 */
|
||||
endDate?: string
|
||||
/** 描述段落 */
|
||||
description?: DescriptionParagraph[]
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询简历的工作经历列表
|
||||
* GET /resume/work?resumeId=xxx
|
||||
*/
|
||||
export function fetchResumeWork(resumeId: string) {
|
||||
return request.get<any, ApiResult<ResumeWork[]>>('/resume/work', {
|
||||
params: { resumeId },
|
||||
})
|
||||
}
|
||||
|
||||
// ==================== 实习经历 ====================
|
||||
|
||||
/** 实习经历项 */
|
||||
export interface ResumeInternship {
|
||||
/** ID(字符串,避免大整数精度丢失) */
|
||||
id?: string
|
||||
/** 公司名称 */
|
||||
companyName?: string
|
||||
/** 职位 */
|
||||
position?: string
|
||||
/** 开始时间 */
|
||||
startDate?: string
|
||||
/** 结束时间 */
|
||||
endDate?: string
|
||||
/** 描述段落 */
|
||||
description?: DescriptionParagraph[]
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询简历的实习经历列表
|
||||
* GET /resume/internship?resumeId=xxx
|
||||
*/
|
||||
export function fetchResumeInternship(resumeId: string) {
|
||||
return request.get<any, ApiResult<ResumeInternship[]>>('/resume/internship', {
|
||||
params: { resumeId },
|
||||
})
|
||||
}
|
||||
|
||||
// ==================== 项目经历 ====================
|
||||
|
||||
/** 项目经历项 */
|
||||
export interface ResumeProject {
|
||||
/** ID(字符串,避免大整数精度丢失) */
|
||||
id?: string
|
||||
/** 所属公司 */
|
||||
companyName?: string
|
||||
/** 项目名称 */
|
||||
projectName?: string
|
||||
/** 担任角色 */
|
||||
role?: string
|
||||
/** 开始时间 */
|
||||
startDate?: string
|
||||
/** 结束时间 */
|
||||
endDate?: string
|
||||
/** 描述段落 */
|
||||
description?: DescriptionParagraph[]
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询简历的项目经历列表
|
||||
* GET /resume/project?resumeId=xxx
|
||||
*/
|
||||
export function fetchResumeProject(resumeId: string) {
|
||||
return request.get<any, ApiResult<ResumeProject[]>>('/resume/project', {
|
||||
params: { resumeId },
|
||||
})
|
||||
}
|
||||
|
||||
// ==================== 竞赛经历 ====================
|
||||
|
||||
/** 竞赛经历项 */
|
||||
export interface ResumeCompetition {
|
||||
/** ID(字符串,避免大整数精度丢失) */
|
||||
id?: string
|
||||
/** 竞赛名称 */
|
||||
competitionName?: string
|
||||
/** 获奖情况 */
|
||||
award?: string
|
||||
/** 获奖时间,格式:2023.07 */
|
||||
awardDate?: string
|
||||
/** 描述段落 */
|
||||
description?: DescriptionParagraph[]
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询简历的竞赛经历列表
|
||||
* GET /resume/competition?resumeId=xxx
|
||||
*/
|
||||
export function fetchResumeCompetition(resumeId: string) {
|
||||
return request.get<any, ApiResult<ResumeCompetition[]>>('/resume/competition', {
|
||||
params: { resumeId },
|
||||
})
|
||||
}
|
||||
|
||||
// ==================== 简历保存相关 ====================
|
||||
|
||||
/** 保存简历主表参数 */
|
||||
export interface SaveResumeMainParams {
|
||||
/** 简历 ID */
|
||||
resumeId: string
|
||||
/** 简历名称 */
|
||||
resumeName?: string
|
||||
/** 目标岗位 */
|
||||
targetPosition?: string
|
||||
/** 真实姓名 */
|
||||
name?: string
|
||||
/** 邮箱 */
|
||||
email?: string
|
||||
/** 手机号码 */
|
||||
mobileNumber?: string
|
||||
/** 所在城市 */
|
||||
city?: string
|
||||
/** 微信号 */
|
||||
wechatNumber?: string
|
||||
/** 作品集链接 */
|
||||
portfolioUrl?: string
|
||||
/** 技能标签列表 */
|
||||
skills?: string[]
|
||||
/** 证书标签列表 */
|
||||
certificates?: string[]
|
||||
/** 个人概述 */
|
||||
summary?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存/更新简历主表信息
|
||||
* POST /resume
|
||||
*/
|
||||
export function saveResumeMain(data: SaveResumeMainParams) {
|
||||
return request.post<any, ApiResult>('/resume', data)
|
||||
}
|
||||
|
||||
/** 保存简历教育经历参数(单条) */
|
||||
export interface SaveResumeEducationItem {
|
||||
/** 学校名称 */
|
||||
school?: string
|
||||
/** 专业 */
|
||||
major?: string
|
||||
/** 学历 */
|
||||
degree?: string
|
||||
/** 学习形式 */
|
||||
studyType?: string
|
||||
/** 开始时间 */
|
||||
startDate?: string
|
||||
/** 结束时间 */
|
||||
endDate?: string
|
||||
/** 描述段落 */
|
||||
description?: DescriptionParagraph[]
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存简历教育经历(全量覆盖)
|
||||
* POST /resume/education
|
||||
*/
|
||||
export function saveResumeEducation(resumeId: string, data: SaveResumeEducationItem[]) {
|
||||
return request.post<any, ApiResult>('/resume/education', { resumeId, items: data })
|
||||
}
|
||||
|
||||
/** 保存简历工作经历参数(单条) */
|
||||
export interface SaveResumeWorkItem {
|
||||
/** 公司名称 */
|
||||
companyName?: string
|
||||
/** 职位 */
|
||||
position?: string
|
||||
/** 开始时间 */
|
||||
startDate?: string
|
||||
/** 结束时间 */
|
||||
endDate?: string
|
||||
/** 描述段落 */
|
||||
description?: DescriptionParagraph[]
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存简历工作经历(全量覆盖)
|
||||
* POST /resume/work
|
||||
*/
|
||||
export function saveResumeWork(resumeId: string, data: SaveResumeWorkItem[]) {
|
||||
return request.post<any, ApiResult>('/resume/work', { resumeId, items: data })
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存简历实习经历(全量覆盖)
|
||||
* POST /resume/internship
|
||||
*/
|
||||
export function saveResumeInternship(resumeId: string, data: SaveResumeWorkItem[]) {
|
||||
return request.post<any, ApiResult>('/resume/internship', { resumeId, items: data })
|
||||
}
|
||||
|
||||
/** 保存简历项目经历参数(单条) */
|
||||
export interface SaveResumeProjectItem {
|
||||
/** 项目名称 */
|
||||
projectName?: string
|
||||
/** 所属公司 */
|
||||
companyName?: string
|
||||
/** 担任角色 */
|
||||
role?: string
|
||||
/** 开始时间 */
|
||||
startDate?: string
|
||||
/** 结束时间 */
|
||||
endDate?: string
|
||||
/** 描述段落 */
|
||||
description?: DescriptionParagraph[]
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存简历项目经历(全量覆盖)
|
||||
* POST /resume/project
|
||||
*/
|
||||
export function saveResumeProject(resumeId: string, data: SaveResumeProjectItem[]) {
|
||||
return request.post<any, ApiResult>('/resume/project', { resumeId, items: data })
|
||||
}
|
||||
|
||||
/** 保存简历竞赛经历参数(单条) */
|
||||
export interface SaveResumeCompetitionItem {
|
||||
/** 竞赛名称 */
|
||||
competitionName?: string
|
||||
/** 获奖情况 */
|
||||
award?: string
|
||||
/** 获奖时间 */
|
||||
awardDate?: string
|
||||
/** 描述段落 */
|
||||
description?: DescriptionParagraph[]
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存简历竞赛经历(全量覆盖)
|
||||
* POST /resume/competition
|
||||
*/
|
||||
export function saveResumeCompetition(resumeId: string, data: SaveResumeCompetitionItem[]) {
|
||||
return request.post<any, ApiResult>('/resume/competition', { resumeId, items: data })
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 37 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 7.3 KiB |
@@ -11,6 +11,7 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 0.14rem;
|
||||
|
||||
// 遮罩层
|
||||
&__overlay {
|
||||
|
||||
@@ -38,6 +38,13 @@
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
// Logo 图片
|
||||
&__logo-img {
|
||||
width: 1.32rem;
|
||||
height: 0.34rem;
|
||||
display: block;
|
||||
}
|
||||
|
||||
&__menu {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -55,3 +55,10 @@
|
||||
font-size: 14px !important;
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== Element Plus Loading 品牌色覆盖 ====================
|
||||
.el-loading-spinner {
|
||||
.circular .path {
|
||||
stroke: #4FC2C9 !important;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,15 +20,17 @@
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 50;
|
||||
background: rgba(255, 255, 255, 0.7);
|
||||
backdrop-filter: blur(100px);
|
||||
border-bottom: 1px solid rgba(243, 244, 246, 0.5);
|
||||
|
||||
// 导航内容容器 — 居中 12rem 宽
|
||||
&__inner {
|
||||
width: 12rem;
|
||||
margin: 0 auto;
|
||||
height: 0.8rem;
|
||||
height: 0.68rem;
|
||||
padding: 0.2rem;
|
||||
box-sizing: border-box;
|
||||
border-radius: 40px;
|
||||
background: #FFFFFF;
|
||||
border: 0.01rem solid #FFFFFF;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
@@ -49,11 +51,18 @@
|
||||
letter-spacing: -0.01rem;
|
||||
}
|
||||
|
||||
// Logo 图片
|
||||
&__logo-img {
|
||||
width: 1.32rem;
|
||||
height: 0.34rem;
|
||||
display: block;
|
||||
}
|
||||
|
||||
// 导航右侧 CTA 按钮
|
||||
&__btn {
|
||||
padding: 0.1rem 0.32rem;
|
||||
border-radius: 9999px;
|
||||
background: $accent;
|
||||
background: #111;
|
||||
color: #fff;
|
||||
font-size: 0.14rem;
|
||||
line-height: 0.2rem;
|
||||
@@ -61,7 +70,7 @@
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
&:hover { background: $accent-hover; }
|
||||
&:hover { }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,9 +79,41 @@
|
||||
background: #fff;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
font-size: 0.14rem;
|
||||
background: radial-gradient(49.26% 24.58% at 46% 0%, #CEF0F2 0%, #FFFFFF 100%);
|
||||
// 背景色块 — 顶部
|
||||
&__orb {
|
||||
position: absolute;
|
||||
border-radius: 3.5rem;
|
||||
filter: blur(1.2rem);
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
background: rgba(82, 202, 209, 0.3);
|
||||
|
||||
&--top {
|
||||
left: 40%;
|
||||
transform: translateX(-50%);
|
||||
top: -1.2rem;
|
||||
width: 8rem;
|
||||
height: 2.2rem;
|
||||
background: rgba(82, 202, 209, 0.2);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
// 背景色块 — 底部
|
||||
&--bottom {
|
||||
left: 40%;
|
||||
transform: translateX(-50%);
|
||||
bottom: -2.4rem;
|
||||
width: 4rem;
|
||||
height: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
// Hero 内容容器 — 左右两栏布局
|
||||
&__inner {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
width: 12rem;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
@@ -251,6 +292,9 @@
|
||||
&__inner {
|
||||
width: 12rem;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
// 标题区
|
||||
@@ -282,7 +326,6 @@
|
||||
|
||||
// 单个统计卡片
|
||||
.stat-card {
|
||||
flex: 1;
|
||||
background: #fff;
|
||||
border-radius: 0.48rem;
|
||||
padding: 0.5rem;
|
||||
@@ -311,6 +354,7 @@
|
||||
.home-jobs-showcase {
|
||||
padding: 1rem 0;
|
||||
background: rgba(82, 202, 209, 0.05);
|
||||
background: linear-gradient(to top, rgba(82, 202, 209, 0.05), rgba(82, 202, 209, 0.0001));
|
||||
|
||||
// 内容容器
|
||||
&__inner {
|
||||
@@ -388,6 +432,7 @@
|
||||
border-radius: 0.12rem;
|
||||
padding: 0.2rem 0.24rem;
|
||||
min-width: 2.8rem;
|
||||
text-align: left;
|
||||
|
||||
// 公司名 + 时间
|
||||
&__company {
|
||||
@@ -451,7 +496,7 @@
|
||||
gap: 0.12rem;
|
||||
padding: 0.2rem 0.4rem;
|
||||
border-radius: 9999px;
|
||||
background: $btn-dark;
|
||||
background: #111111;
|
||||
color: #fff;
|
||||
font-size: 0.18rem;
|
||||
line-height: 0.28rem;
|
||||
@@ -459,8 +504,14 @@
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
&:hover { background: $btn-dark-hover; }
|
||||
svg { flex-shrink: 0; }
|
||||
&:hover {
|
||||
//background: $btn-dark-hover;
|
||||
//background: $btn-dark;
|
||||
}
|
||||
svg {
|
||||
flex-shrink: 0;
|
||||
color: $btn-dark;
|
||||
}
|
||||
}
|
||||
|
||||
// 视觉展示区(右侧/左侧)
|
||||
@@ -470,7 +521,7 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(82, 202, 209, 0.05);
|
||||
background: linear-gradient(157.19deg, #E8F8F9 0%, #FBFDF7 100%);
|
||||
border-radius: 0.48rem;
|
||||
padding: 0.48rem;
|
||||
min-height: 3.87rem;
|
||||
@@ -584,7 +635,7 @@
|
||||
padding: 0.32rem;
|
||||
width: 100%;
|
||||
max-width: 5.6rem;
|
||||
transform: rotate(-2deg);
|
||||
//transform: rotate(-2deg);
|
||||
|
||||
// 顶部状态栏
|
||||
&__header {
|
||||
@@ -659,7 +710,7 @@
|
||||
&__title {
|
||||
font-size: 0.14rem;
|
||||
line-height: 0.2rem;
|
||||
font-weight: 600;
|
||||
//font-weight: 600;
|
||||
color: #111;
|
||||
margin: 0;
|
||||
}
|
||||
@@ -686,6 +737,13 @@
|
||||
justify-content: center;
|
||||
|
||||
// 简历文档
|
||||
&__doc--border {
|
||||
background: #F9FDFD;
|
||||
padding: 0.2rem;
|
||||
border-radius: 0.25rem;
|
||||
border: 0.62px solid #FFFFFF;
|
||||
box-shadow: 0 0.15rem 0.31rem -0.074rem rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
&__doc {
|
||||
width: 2rem;
|
||||
aspect-ratio: 3/4;
|
||||
@@ -693,17 +751,18 @@
|
||||
border: 1.2px dashed #e5e7eb;
|
||||
border-radius: 0.16rem;
|
||||
padding: 0.16rem;
|
||||
|
||||
}
|
||||
|
||||
// ATS 优化徽章
|
||||
&__badge {
|
||||
position: absolute;
|
||||
bottom: -0.1rem;
|
||||
left: -0.1rem;
|
||||
background: $btn-dark;
|
||||
left: -0.12rem;
|
||||
background: #111111;
|
||||
color: #fff;
|
||||
padding: 0.12rem 0.2rem;
|
||||
border-radius: 0.24rem;
|
||||
padding: 0.16rem 0.2rem;
|
||||
border-radius: 0.15rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.08rem;
|
||||
@@ -712,7 +771,6 @@
|
||||
span {
|
||||
font-size: 0.09rem;
|
||||
line-height: 0.09rem;
|
||||
font-weight: 900;
|
||||
letter-spacing: 0.01rem;
|
||||
}
|
||||
}
|
||||
@@ -774,6 +832,7 @@
|
||||
backdrop-filter: blur(56px);
|
||||
border-radius: 0.32rem;
|
||||
padding: 0.2rem;
|
||||
font-size: 0.14rem;
|
||||
|
||||
// 奇数列偏移效果
|
||||
&--offset {
|
||||
@@ -890,10 +949,44 @@
|
||||
|
||||
// ==================== 用户评价区 ====================
|
||||
.home-testimonials {
|
||||
padding: 1.2rem 0;
|
||||
background: rgba(82, 202, 209, 0.03);
|
||||
position: relative;
|
||||
|
||||
// 底层背景色块 — 左上角
|
||||
&__orb {
|
||||
position: absolute;
|
||||
border-radius: 3.5rem;
|
||||
filter: blur(1.2rem);
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
|
||||
&--left {
|
||||
left: -0.11rem;
|
||||
top: 0.6rem;
|
||||
width: 5.95rem;
|
||||
height: 5.95rem;
|
||||
background: #52CAD1;
|
||||
}
|
||||
|
||||
// 底层背景色块 — 右下角
|
||||
&--right {
|
||||
right: -0.5rem;
|
||||
bottom: 0.5rem;
|
||||
width: 6rem;
|
||||
height: 6rem;
|
||||
background: #6DE5B6;
|
||||
}
|
||||
}
|
||||
|
||||
// 内容层白色容器 — 半透明白底透出色块
|
||||
&__container {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
border-radius: 1rem;
|
||||
backdrop-filter: blur(100px);
|
||||
overflow: hidden;
|
||||
padding: 0.8rem 1.2rem 0.6rem;
|
||||
box-shadow: inset 0 0 0.9rem rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
// 标题区
|
||||
&__header {
|
||||
@@ -905,6 +998,7 @@
|
||||
line-height: 0.48rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: -0.012rem;
|
||||
color: #111;
|
||||
}
|
||||
|
||||
p {
|
||||
@@ -913,56 +1007,84 @@
|
||||
color: #9ca3af;
|
||||
letter-spacing: 0.016rem;
|
||||
text-transform: uppercase;
|
||||
margin-top: 0.12rem;
|
||||
margin-top: 0.24rem;
|
||||
}
|
||||
}
|
||||
|
||||
// 创始人引言卡片
|
||||
// 评价卡片横向排列
|
||||
&__cards {
|
||||
display: flex;
|
||||
gap: 0.24rem;
|
||||
justify-content: center;
|
||||
margin-bottom: 0.48rem;
|
||||
}
|
||||
|
||||
// 创始人引言深色卡片
|
||||
&__founder {
|
||||
width: 12rem;
|
||||
margin: 0 auto 0.8rem;
|
||||
background: $btn-dark;
|
||||
color: #fff;
|
||||
background: #1a1a2e;
|
||||
border-radius: 0.48rem;
|
||||
padding: 0.48rem;
|
||||
padding: 0.48rem 0.6rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
// 创始人装饰圆环 SVG
|
||||
&__founder-decor {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 4.55rem;
|
||||
height: 4.55rem;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
// 创始人内容区
|
||||
&__founder-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.48rem;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
z-index: 1;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
// 创始人头像
|
||||
&-img {
|
||||
&__founder-img {
|
||||
width: 1.28rem;
|
||||
height: 1.28rem;
|
||||
border-radius: 0.24rem;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 0.6rem;
|
||||
line-height: 1;
|
||||
object-fit: cover;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
// 引言文字
|
||||
// 创始人文字区
|
||||
&__founder-text {
|
||||
flex: 1;
|
||||
|
||||
blockquote {
|
||||
font-size: 0.24rem;
|
||||
line-height: 0.39rem;
|
||||
color: #fff;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
// 署名
|
||||
cite {
|
||||
display: block;
|
||||
margin-top: 0.24rem;
|
||||
font-style: normal;
|
||||
line-height: 0.28rem;
|
||||
|
||||
.cite-name {
|
||||
font-size: 0.2rem;
|
||||
color: $accent;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.cite-role {
|
||||
font-size: 0.14rem;
|
||||
color: #999;
|
||||
@@ -971,21 +1093,35 @@
|
||||
}
|
||||
}
|
||||
|
||||
// 评价卡片横向滚动容器
|
||||
&__scroll {
|
||||
// 分页小圆点
|
||||
&__dots {
|
||||
display: flex;
|
||||
gap: 0.24rem;
|
||||
padding: 0 1.2rem;
|
||||
overflow-x: auto;
|
||||
&::-webkit-scrollbar { display: none; }
|
||||
justify-content: center;
|
||||
gap: 0.1rem;
|
||||
margin-top: 0.4rem;
|
||||
}
|
||||
|
||||
&__dot {
|
||||
width: 0.12rem;
|
||||
height: 0.12rem;
|
||||
border-radius: 50%;
|
||||
background: #111;
|
||||
opacity: 0.3;
|
||||
cursor: pointer;
|
||||
transition: opacity 0.3s;
|
||||
|
||||
&--active {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 单个评价卡片
|
||||
.testimonial-card {
|
||||
flex-shrink: 0;
|
||||
min-width: 3.71rem;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
max-width: 3.71rem;
|
||||
background: #fff;
|
||||
box-shadow: 0 20px 50px rgba(0, 0, 0, 0.05);
|
||||
border-radius: 0.4rem;
|
||||
padding: 0.3rem;
|
||||
@@ -995,7 +1131,7 @@
|
||||
&__quote {
|
||||
position: absolute;
|
||||
right: 0.3rem;
|
||||
top: 0.3rem;
|
||||
bottom: 0.3rem;
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
@@ -1020,9 +1156,15 @@
|
||||
width: 0.48rem;
|
||||
height: 0.48rem;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, $accent, #a7f3d0);
|
||||
object-fit: cover;
|
||||
border: 2px solid #fff;
|
||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
|
||||
box-shadow: 0 2px 4px -2px rgba(0, 0, 0, 0.1), 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
// 作者信息容器
|
||||
&__info {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
// 作者姓名
|
||||
@@ -1217,15 +1359,18 @@
|
||||
// ==================== 底部 CTA 行动号召 ====================
|
||||
.home-cta {
|
||||
padding: 0 1.2rem 1.2rem;
|
||||
width: 12rem;
|
||||
margin: 0 auto;
|
||||
|
||||
// CTA 内容区
|
||||
&__inner {
|
||||
background: $accent;
|
||||
background: linear-gradient(223.56deg, #52CAD1 0%, #53D9C8 100%);
|
||||
border-radius: 0.6rem;
|
||||
padding: 1.28rem 0;
|
||||
padding: 1.0rem 0;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
line-height: 1;
|
||||
|
||||
h2 {
|
||||
font-size: 0.52rem;
|
||||
@@ -1239,10 +1384,10 @@
|
||||
|
||||
// CTA 按钮
|
||||
&__btn {
|
||||
margin-top: 0.4rem;
|
||||
margin-top: 0.2rem;
|
||||
padding: 0.24rem 0.64rem;
|
||||
border-radius: 9999px;
|
||||
background: $btn-dark;
|
||||
background: #111;
|
||||
color: #fff;
|
||||
font-size: 0.2rem;
|
||||
line-height: 0.28rem;
|
||||
@@ -1253,15 +1398,16 @@
|
||||
z-index: 1;
|
||||
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
|
||||
transition: background 0.2s;
|
||||
&:hover { background: $btn-dark-hover; }
|
||||
&:hover { background: #111; }
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 页脚 ====================
|
||||
.home-footer {
|
||||
background: #fff;
|
||||
background: #111;
|
||||
border-top: 1px solid #f3f4f6;
|
||||
padding: 0.8rem 0 0.4rem;
|
||||
line-height: 1;
|
||||
|
||||
// 页脚内容 — 四列横向排列
|
||||
&__inner {
|
||||
@@ -1282,7 +1428,7 @@
|
||||
font-size: 0.16rem;
|
||||
line-height: 0.24rem;
|
||||
font-weight: 500;
|
||||
color: #111;
|
||||
color: #fff;
|
||||
margin: 0 0 0.32rem;
|
||||
}
|
||||
|
||||
@@ -1309,9 +1455,17 @@
|
||||
// Logo
|
||||
&__logo {
|
||||
margin-bottom: 0.16rem;
|
||||
font-size: 0.14rem;
|
||||
}
|
||||
|
||||
// Logo 文字
|
||||
// Logo 图片
|
||||
&__logo-img {
|
||||
width: 1.43rem;
|
||||
height: 0.355rem;
|
||||
display: block;
|
||||
}
|
||||
|
||||
// Logo 文字(保留备用)
|
||||
&__logo-text {
|
||||
font-size: 0.22rem;
|
||||
line-height: 0.34rem;
|
||||
@@ -1331,7 +1485,6 @@
|
||||
width: 12rem;
|
||||
margin: 0.6rem auto 0;
|
||||
text-align: center;
|
||||
border-top: 1px solid #f3f4f6;
|
||||
padding-top: 0.4rem;
|
||||
|
||||
p {
|
||||
|
||||
@@ -615,6 +615,14 @@
|
||||
}
|
||||
}
|
||||
|
||||
// 暂无数据提示
|
||||
&__empty {
|
||||
text-align: center;
|
||||
padding: 1rem 0;
|
||||
font-size: 0.14rem;
|
||||
color: $text-light;
|
||||
}
|
||||
|
||||
// 加载更多提示
|
||||
&__loading-more {
|
||||
text-align: center;
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
background: $bg-main;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
font-size: 0.14rem;
|
||||
}
|
||||
|
||||
&__page-title {
|
||||
@@ -130,10 +131,11 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: $bg-white;
|
||||
color: $text-dark;
|
||||
font-size: 0.18rem;
|
||||
font-weight: 700;
|
||||
flex-shrink: 0;
|
||||
background: $bg-main;
|
||||
}
|
||||
|
||||
&__score-badge {
|
||||
@@ -317,12 +319,37 @@
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
// ---- 编辑按钮(与 ProfilePageContent 一致) ----
|
||||
&__edit-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
color: $text-light;
|
||||
cursor: pointer;
|
||||
padding: 0.04rem;
|
||||
border-radius: 0.04rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
transition: all 0.2s;
|
||||
flex-shrink: 0;
|
||||
|
||||
&:hover {
|
||||
color: $accent;
|
||||
background: $theme-color;
|
||||
}
|
||||
}
|
||||
|
||||
&__edit-icon {
|
||||
width: 0.15rem;
|
||||
height: 0.15rem;
|
||||
}
|
||||
|
||||
// ---- 教育背景 ----
|
||||
&__edu-item {
|
||||
padding: 0.1rem 0;
|
||||
|
||||
margin-top: 0.16rem;
|
||||
padding-left: 0.1rem;
|
||||
border-left: 2px solid $border-color;
|
||||
&:not(:last-child) {
|
||||
border-bottom: 1px solid $border-color;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -339,12 +366,15 @@
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
// ---- 工作经验 ----
|
||||
// ---- 工作经历 ----
|
||||
&__exp-item {
|
||||
padding: 0.1rem 0;
|
||||
margin-top: 0.16rem;
|
||||
margin-left: 0.1rem;
|
||||
padding-left: 0.1rem;
|
||||
border-left: 2px solid $border-color;
|
||||
|
||||
&:not(:last-child) {
|
||||
border-bottom: 1px solid $border-color;
|
||||
//border-bottom: 1px solid $border-color;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -390,4 +420,47 @@
|
||||
background: $theme-color;
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 卡片底部问题操作区 ----
|
||||
&__card-issue {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 0.1rem;
|
||||
margin-top: 0.18rem;
|
||||
padding-top: 0.14rem;
|
||||
}
|
||||
|
||||
// 问题类型按钮组(三选一)
|
||||
&__issue-type-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.06rem;
|
||||
}
|
||||
|
||||
// 问题类型按钮
|
||||
&__issue-type-btn {
|
||||
font-size: 0.12rem;
|
||||
border-radius: 0.16rem;
|
||||
padding: 0.05rem 0.14rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
white-space: nowrap;
|
||||
border: 1px solid $border-color;
|
||||
background: $bg-main;
|
||||
color: $text-light;
|
||||
|
||||
&:hover {
|
||||
border-color: $accent;
|
||||
color: $accent;
|
||||
background: $theme-color;
|
||||
}
|
||||
|
||||
// 选中态
|
||||
&--active {
|
||||
background: $theme-color;
|
||||
color: $accent;
|
||||
border-color: $accent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -188,3 +188,16 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 上传简历加载提示 ====================
|
||||
.resume-upload-loading {
|
||||
.el-loading-spinner {
|
||||
.circular .path {
|
||||
stroke: $accent;
|
||||
}
|
||||
|
||||
.el-loading-text {
|
||||
color: $accent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -304,6 +304,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, nextTick } from 'vue'
|
||||
import html2pdf from 'html2pdf.js'
|
||||
import JobResumeTemplate from '@/components/JobResumeTemplate.vue'
|
||||
import type { ResumeTemplateData } from '@/components/JobResumeTemplate.vue'
|
||||
import { fetchProfile, fetchEducation, fetchWork, fetchInternship, fetchProject, fetchCompetition } from '@/api/profile'
|
||||
@@ -588,10 +589,82 @@ function toggleDownloadMenu() {
|
||||
}
|
||||
|
||||
/** 处理下载(PDF/Word) */
|
||||
function handleDownload(type: 'pdf' | 'word') {
|
||||
async function handleDownload(type: 'pdf' | 'word') {
|
||||
showDownloadMenu.value = false
|
||||
// TODO: 实现简历HTML转PDF/Word下载
|
||||
console.log(`[下载简历] 格式: ${type}`)
|
||||
|
||||
if (type === 'pdf') {
|
||||
// 通过 JobResumeTemplate 组件暴露的 resumeRef 获取简历DOM
|
||||
const element = resumeTemplateRef.value?.resumeRef
|
||||
if (!element) {
|
||||
console.error('[下载简历] 无法获取简历模板DOM')
|
||||
return
|
||||
}
|
||||
|
||||
// html2pdf 配置选项
|
||||
const options = {
|
||||
margin: [10, 10, 10, 10] as [number, number, number, number],
|
||||
filename: `${resumeTemplateData.value.name || '简历'}_定制简历.pdf`,
|
||||
image: { type: 'jpeg', quality: 0.98 },
|
||||
html2canvas: { scale: 2, useCORS: true, logging: false },
|
||||
jsPDF: { unit: 'mm', format: 'a4', orientation: 'portrait' as const },
|
||||
}
|
||||
|
||||
try {
|
||||
await html2pdf().set(options).from(element).save()
|
||||
} catch (err) {
|
||||
console.error('[下载简历] PDF生成失败', err)
|
||||
}
|
||||
} else {
|
||||
// 将简历HTML转为Word文档并下载(使用HTML格式的.doc文件,Word可正常打开)
|
||||
const element = resumeTemplateRef.value?.resumeRef
|
||||
if (!element) {
|
||||
console.error('[下载简历] 无法获取简历模板DOM')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
// 获取页面样式表内容
|
||||
const styleSheets = Array.from(document.styleSheets)
|
||||
let cssText = ''
|
||||
styleSheets.forEach((sheet) => {
|
||||
try {
|
||||
Array.from(sheet.cssRules).forEach((rule) => {
|
||||
cssText += rule.cssText + '\n'
|
||||
})
|
||||
} catch {
|
||||
// 跨域样式表无法读取,跳过
|
||||
}
|
||||
})
|
||||
|
||||
// 组装完整HTML文档(Word可识别的HTML格式)
|
||||
const fullHtml = `
|
||||
<html xmlns:o="urn:schemas-microsoft-com:office:office"
|
||||
xmlns:w="urn:schemas-microsoft-com:office:word"
|
||||
xmlns="http://www.w3.org/TR/REC-html40">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="ProgId" content="Word.Document">
|
||||
<meta name="Generator" content="Microsoft Word 15">
|
||||
<style>${cssText}</style>
|
||||
</head>
|
||||
<body>${element.outerHTML}</body>
|
||||
</html>
|
||||
`
|
||||
|
||||
// 生成Blob并触发下载
|
||||
const blob = new Blob([fullHtml], { type: 'application/msword' })
|
||||
const fileName = `${resumeTemplateData.value.name || '简历'}_定制简历.doc`
|
||||
const link = document.createElement('a')
|
||||
link.href = URL.createObjectURL(blob)
|
||||
link.download = fileName
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
URL.revokeObjectURL(link.href)
|
||||
} catch (err) {
|
||||
console.error('[下载简历] Word生成失败', err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 立即去投递 */
|
||||
|
||||
@@ -109,11 +109,9 @@
|
||||
<span class="profile-drawer__required">*</span>学历类型
|
||||
</label>
|
||||
<div class="profile-drawer__select-wrap">
|
||||
<select class="profile-drawer__input" v-model.number="edu.studyType">
|
||||
<option :value="0">全日制</option>
|
||||
<option :value="1">非全日制</option>
|
||||
<select class="profile-drawer__input" v-model="edu.studyType">
|
||||
<option v-for="opt in studyTypeOptions" :key="opt.value" :value="opt.value">{{ opt.label }}</option>
|
||||
</select>
|
||||
<!-- <svg viewBox="0 0 16 16" fill="none" class="profile-drawer__select-arrow"><path d="M4 6l4 4 4-4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg> -->
|
||||
</div>
|
||||
</div>
|
||||
<div class="profile-drawer__field profile-drawer__field--half">
|
||||
@@ -121,13 +119,9 @@
|
||||
<span class="profile-drawer__required">*</span>学历
|
||||
</label>
|
||||
<div class="profile-drawer__select-wrap">
|
||||
<select class="profile-drawer__input" v-model.number="edu.degree">
|
||||
<option :value="1">大专</option>
|
||||
<option :value="2">本科</option>
|
||||
<option :value="3">硕士</option>
|
||||
<option :value="4">博士</option>
|
||||
<select class="profile-drawer__input" v-model="edu.degree">
|
||||
<option v-for="opt in degreeOptions" :key="opt.value" :value="opt.value">{{ opt.label }}</option>
|
||||
</select>
|
||||
<!-- <svg viewBox="0 0 16 16" fill="none" class="profile-drawer__select-arrow"><path d="M4 6l4 4 4-4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg> -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -742,6 +736,19 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- ========== 个人概述模块(仅简历使用) ========== -->
|
||||
<template v-else-if="module === 'summary'">
|
||||
<div class="profile-drawer__field">
|
||||
<label class="profile-drawer__label">个人概述</label>
|
||||
<textarea
|
||||
class="profile-drawer__textarea"
|
||||
placeholder="请输入个人概述,简要介绍自己的优势和职业目标"
|
||||
v-model="summaryText"
|
||||
rows="8"
|
||||
></textarea>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- ========== 其他模块占位 ========== -->
|
||||
<template v-else>
|
||||
<div class="profile-drawer__empty">
|
||||
@@ -770,10 +777,10 @@ interface EducationItem {
|
||||
school: string
|
||||
/** 专业 */
|
||||
major: string
|
||||
/** 学历类型(0=全日制 1=非全日制) */
|
||||
studyType: number
|
||||
/** 学历(1=大专 2=本科 3=硕士 4=博士) */
|
||||
degree: number
|
||||
/** 学历类型(数字模式:0=全日制 1=非全日制;文本模式:中文字符串) */
|
||||
studyType: number | string
|
||||
/** 学历(数字模式:1=大专 2=本科 3=硕士 4=博士;文本模式:中文字符串) */
|
||||
degree: number | string
|
||||
/** 入学时间(日期选择器使用字符串,格式:YYYY-MM) */
|
||||
startDate: string
|
||||
/** 毕业时间(日期选择器使用字符串,格式:YYYY-MM) */
|
||||
@@ -854,8 +861,22 @@ const props = defineProps<{
|
||||
module: string
|
||||
/** 初始数据 — 不同模块传入不同 JSON 结构 */
|
||||
initialData?: Record<string, any>
|
||||
/** 教育经历是否使用中文文本作为选项值(默认 false 使用数字编码,true 则输出中文字符串) */
|
||||
useEducationTextValue?: boolean
|
||||
}>()
|
||||
|
||||
/** 学历类型选项映射 — 根据 useEducationTextValue 决定 value 类型 */
|
||||
const studyTypeOptions = computed(() => props.useEducationTextValue
|
||||
? [{ value: '全日制', label: '全日制' }, { value: '非全日制', label: '非全日制' }]
|
||||
: [{ value: 0, label: '全日制' }, { value: 1, label: '非全日制' }]
|
||||
)
|
||||
|
||||
/** 学历选项映射 — 根据 useEducationTextValue 决定 value 类型 */
|
||||
const degreeOptions = computed(() => props.useEducationTextValue
|
||||
? [{ value: '大专', label: '大专' }, { value: '本科', label: '本科' }, { value: '硕士', label: '硕士' }, { value: '博士', label: '博士' }]
|
||||
: [{ value: 1, label: '大专' }, { value: 2, label: '本科' }, { value: 3, label: '硕士' }, { value: 4, label: '博士' }]
|
||||
)
|
||||
|
||||
/** 组件 Emits */
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', value: boolean): void
|
||||
@@ -868,6 +889,7 @@ const store = useStore()
|
||||
/** 模块名称映射表 — 模块标识 → 中文标题 */
|
||||
const moduleTitleMap: Record<string, string> = {
|
||||
info: '基本信息',
|
||||
summary: '个人概述',
|
||||
education: '教育经历',
|
||||
work: '工作经历',
|
||||
internship: '实习经历',
|
||||
@@ -943,8 +965,8 @@ const educationList = ref<EducationItem[]>([])
|
||||
const createEmptyEducation = (): EducationItem => ({
|
||||
school: '',
|
||||
major: '',
|
||||
studyType: 0,
|
||||
degree: 2,
|
||||
studyType: props.useEducationTextValue ? '全日制' : 0,
|
||||
degree: props.useEducationTextValue ? '本科' : 2,
|
||||
startDate: '',
|
||||
endDate: '',
|
||||
description: [{ id: generateId(), text: '' }],
|
||||
@@ -1099,6 +1121,9 @@ const skillsList = ref<string[]>([])
|
||||
/** 作品集链接 — 作品集模块使用 */
|
||||
const portfolioUrl = ref('')
|
||||
|
||||
/** 个人概述文本 — 个人概述模块使用 */
|
||||
const summaryText = ref('')
|
||||
|
||||
/** 新技能输入框的值 */
|
||||
const newSkillInput = ref('')
|
||||
|
||||
@@ -1205,6 +1230,9 @@ watch(() => props.modelValue, (visible) => {
|
||||
} else if (props.module === 'portfolio') {
|
||||
// 作品集:用初始数据填充链接
|
||||
portfolioUrl.value = props.initialData?.portfolioUrl || ''
|
||||
} else if (props.module === 'summary') {
|
||||
// 个人概述:用初始数据填充文本
|
||||
summaryText.value = props.initialData?.summary || ''
|
||||
} else if (props.module === 'skills') {
|
||||
// 技能:用初始数据填充列表,若无则创建空数组
|
||||
skillsList.value = props.initialData?.skills ? [...props.initialData.skills] : []
|
||||
@@ -1256,6 +1284,8 @@ const handleSave = () => {
|
||||
})) })
|
||||
} else if (props.module === 'portfolio') {
|
||||
emit('save', { portfolioUrl: portfolioUrl.value })
|
||||
} else if (props.module === 'summary') {
|
||||
emit('save', { summary: summaryText.value })
|
||||
} else if (props.module === 'skills') {
|
||||
emit('save', { skills: [...skillsList.value] })
|
||||
} else if (props.module === 'certificate') {
|
||||
|
||||
@@ -2,10 +2,9 @@
|
||||
<div class="side-nav">
|
||||
<!-- 顶部 Logo -->
|
||||
<div class="side-nav__header">
|
||||
<div class="side-nav__avatar">
|
||||
<span class="side-nav__avatar-icon">👤</span>
|
||||
</div>
|
||||
<span class="side-nav__logo-text">Offer派</span>
|
||||
|
||||
<!-- 侧边栏Logo图片 -->
|
||||
<img src="@/assets/images/logo.png" alt="Offer派" class="side-nav__logo-img" />
|
||||
</div>
|
||||
|
||||
<!-- 主导航 -->
|
||||
|
||||
+9
-2
@@ -264,15 +264,22 @@ export default createStore<RootState>({
|
||||
},
|
||||
|
||||
/**
|
||||
* 保存求职意向到后端,成功后同步更新 store
|
||||
* 保存求职意向:已登录时调接口保存并更新 store,未登录时仅更新 store
|
||||
* @param data 求职意向数据
|
||||
*/
|
||||
async saveJobIntention({ commit }, data: JobIntention) {
|
||||
async saveJobIntention({ commit, state }, data: JobIntention) {
|
||||
if (state.isAuthenticated) {
|
||||
// 已登录 — 调接口持久化,成功后同步 store
|
||||
const res = await saveJobIntention(data)
|
||||
if (res.code === '0') {
|
||||
commit('SET_JOB_INTENTION', data)
|
||||
}
|
||||
return res
|
||||
} else {
|
||||
// 未登录 — 仅本地更新 store,不请求接口
|
||||
commit('SET_JOB_INTENTION', data)
|
||||
return { code: '0', message: 'ok' }
|
||||
}
|
||||
},
|
||||
},
|
||||
modules: {},
|
||||
|
||||
Vendored
+22
@@ -0,0 +1,22 @@
|
||||
/** html2pdf.js 类型声明 */
|
||||
declare module 'html2pdf.js' {
|
||||
interface Html2PdfOptions {
|
||||
margin?: number | number[]
|
||||
filename?: string
|
||||
image?: { type?: string; quality?: number }
|
||||
html2canvas?: Record<string, unknown>
|
||||
jsPDF?: { unit?: string; format?: string; orientation?: string }
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
interface Html2PdfInstance {
|
||||
set(options: Html2PdfOptions): Html2PdfInstance
|
||||
from(element: HTMLElement | string): Html2PdfInstance
|
||||
save(): Promise<void>
|
||||
toPdf(): Html2PdfInstance
|
||||
output(type: string): Promise<unknown>
|
||||
}
|
||||
|
||||
function html2pdf(): Html2PdfInstance
|
||||
export default html2pdf
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import axios from 'axios'
|
||||
|
||||
/** 默认 Token */
|
||||
const DEFAULT_TOKEN = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VySWQiOjIwMzUyNTM4OTg5MTk4NzA0NjUsInV1SWQiOiI2MmQ5MDE2NTcyNzY0ZmNjODNjZTIyYjRjODA5ZmU5MiJ9.eE-Q5rio5J5kxkS-XPYdmk-1Tgvg6kj6NGoKWMFNU14'
|
||||
|
||||
/**
|
||||
* AI 端专用 axios 实例
|
||||
* 基础地址指向 AI 服务,超时时间较长(大模型响应慢)
|
||||
*/
|
||||
const aiService = axios.create({
|
||||
baseURL: '/ai-api',
|
||||
timeout: 300000, // 5 分钟,大模型处理可能较慢
|
||||
})
|
||||
|
||||
/**
|
||||
* 请求拦截器 — 每次请求都带上 Token header
|
||||
* 浏览器安全策略不允许 JS 直接设置 Cookie header,改用 Token header 传递
|
||||
*/
|
||||
aiService.interceptors.request.use((config) => {
|
||||
config.headers['Token'] = DEFAULT_TOKEN
|
||||
return config
|
||||
})
|
||||
|
||||
/** AI 端通用响应结构 */
|
||||
export interface AiResult<T = any> {
|
||||
code: number
|
||||
msg: string
|
||||
data: T
|
||||
timestamp: string
|
||||
uuid: string
|
||||
}
|
||||
|
||||
/** 上传简历返回数据 */
|
||||
export interface UploadResumeData {
|
||||
resumeId: number
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传简历文件
|
||||
* POST /resume/upload
|
||||
* @param file 简历文件(pdf / word)
|
||||
*/
|
||||
export function uploadResume(file: File) {
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
|
||||
return aiService.post<AiResult<UploadResumeData>>('/resume/upload', formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
}).then(res => res.data)
|
||||
}
|
||||
|
||||
export default aiService
|
||||
+200
-55
@@ -1,17 +1,27 @@
|
||||
<template>
|
||||
<div class="home-page">
|
||||
<!-- SEO: 语义化 header -->
|
||||
<header class="home-nav">
|
||||
<!-- Hero 主视觉区(包含顶部导航) -->
|
||||
<section class="home-hero">
|
||||
<!-- 底层背景色块 -->
|
||||
<div class="home-hero__orb home-hero__orb--top"></div>
|
||||
<div class="home-hero__orb home-hero__orb--bottom"></div>
|
||||
<!-- 顶部导航栏 -->
|
||||
<header class="home-nav mt32">
|
||||
<div class="home-nav__inner">
|
||||
<div class="home-nav__logo">
|
||||
<span class="home-nav__logo-text">Offer派</span>
|
||||
<!-- 导航栏Logo图片 -->
|
||||
<img src="@/assets/images/logo.png" alt="Offer派" class="home-nav__logo-img" />
|
||||
</div>
|
||||
<button class="home-nav__btn" @click="router.push('/jobs')">免费领取 offer</button>
|
||||
<div class="dflex-end aliite-c">
|
||||
<!-- 未登录时显示登陆和立即加入按钮 -->
|
||||
<div v-if="!store.state.isAuthenticated" class="fs14 color-1 mr40 fw600 cursor-po" @click="handleLoginClick">登陆</div>
|
||||
<button v-if="!store.state.isAuthenticated" class="home-nav__btn" @click="router.push('/jobs')">立即加入</button>
|
||||
<!-- 已登录时显示进入平台按钮 -->
|
||||
<button v-else class="home-nav__btn" @click="router.push('/jobs')">进入平台</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Hero Section -->
|
||||
<section class="home-hero">
|
||||
<div class="home-hero__inner">
|
||||
<div class="home-hero__left">
|
||||
<h1 class="home-hero__title">Offer派<br/>收offer就是快!</h1>
|
||||
@@ -84,8 +94,8 @@
|
||||
<div class="showcase-stat__label">岗位总数</div>
|
||||
</div>
|
||||
<div class="showcase-stat">
|
||||
<div class="showcase-stat__num"><span class="accent">3120</span>今日新增</div>
|
||||
<div class="showcase-stat__label">今日新增</div>
|
||||
<div class="showcase-stat__num"><span class="accent">3120</span>个岗位</div>
|
||||
<div class="showcase-stat__label">今日更新</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="home-jobs-showcase__scroll">
|
||||
@@ -134,6 +144,14 @@
|
||||
<!-- Feature 2: 一键自动网申 -->
|
||||
<section class="home-feature home-feature--reverse">
|
||||
<div class="home-feature__inner">
|
||||
<div class="home-feature__text">
|
||||
<h2>校招一键<br/>自动网申</h2>
|
||||
<p>每日向数百岗位一键投递,覆盖各大企业网申系统。告别重复填写,节省 80% 的宝贵时间。</p>
|
||||
<button class="home-feature__btn" @click="router.push('/jobs')">
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><path d="M3 10l2-8h10l2 8-7 7-7-7z" stroke="currentColor" stroke-width="2" stroke-linejoin="round"/></svg>
|
||||
<span>开启自动网申</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="home-feature__visual home-feature__visual--apply">
|
||||
<div class="feature-apply-card">
|
||||
<div class="feature-apply-card__header">
|
||||
@@ -162,14 +180,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="home-feature__text">
|
||||
<h2>一键<br/>自动网申</h2>
|
||||
<p>每日向数百岗位一键投递,覆盖各大企业网申系统。告别重复填写,节省 80% 的宝贵时间。</p>
|
||||
<button class="home-feature__btn" @click="router.push('/jobs')">
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><path d="M3 10l2-8h10l2 8-7 7-7-7z" stroke="currentColor" stroke-width="2" stroke-linejoin="round"/></svg>
|
||||
<span>开启自动网申</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -178,7 +189,7 @@
|
||||
<div class="home-feature__inner">
|
||||
<div class="home-feature__text">
|
||||
<h2>岗位定制简历</h2>
|
||||
<p>6秒内生成针对特定岗位优化的专业简历,通过ATS系统,突出你的核心优势。</p>
|
||||
<p>10秒内生成针对特定岗位优化的专业简历,通过ATS系统,突出你的核心优势。。</p>
|
||||
<button class="home-feature__btn" @click="router.push('/jobs')">
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><path d="M4 2h8l4 4v12H4V2z" stroke="currentColor" stroke-width="2" stroke-linejoin="round"/><path d="M12 2v4h4" stroke="currentColor" stroke-width="2"/><path d="M4 11h8" stroke="currentColor" stroke-width="2"/></svg>
|
||||
<span>优化我的简历</span>
|
||||
@@ -186,6 +197,7 @@
|
||||
</div>
|
||||
<div class="home-feature__visual home-feature__visual--resume">
|
||||
<div class="feature-resume-card">
|
||||
<div class="feature-resume-card__doc--border">
|
||||
<div class="feature-resume-card__doc">
|
||||
<div class="doc-header">
|
||||
<div class="doc-avatar"></div>
|
||||
@@ -200,6 +212,8 @@
|
||||
<div class="doc-bar doc-bar--3"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="feature-resume-card__badge">
|
||||
<svg width="15" height="15" viewBox="0 0 15 15" fill="none"><path d="M3 2h7l3 3v8H3V2z" stroke="#4FC2C9" stroke-width="1.2"/><path d="M6 7h3" stroke="#4FC2C9" stroke-width="1.2"/></svg>
|
||||
<span>ATS OPTIMIZED</span>
|
||||
@@ -212,6 +226,14 @@
|
||||
<!-- Feature 4: 内推人脉 -->
|
||||
<section class="home-feature home-feature--reverse">
|
||||
<div class="home-feature__inner">
|
||||
<div class="home-feature__text">
|
||||
<h2>名企内推<br/>人脉直通</h2>
|
||||
<p>实时获取名企最新内推信息,自动填写网申内推码,简历更快到达HR。</p>
|
||||
<button class="home-feature__btn" @click="router.push('/jobs')">
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><path d="M3 3h10l4 4v10H3V3z" stroke="currentColor" stroke-width="2" stroke-linejoin="round"/><path d="M6 12l2 2 4-4" stroke="currentColor" stroke-width="2"/></svg>
|
||||
<span>立即投递</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="home-feature__visual home-feature__visual--referral">
|
||||
<div class="referral-grid">
|
||||
<div class="referral-card" v-for="(ref, i) in referralCards" :key="i" :class="{ 'referral-card--offset': i % 2 === 1 }">
|
||||
@@ -222,14 +244,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="home-feature__text">
|
||||
<h2>内推<br/>人脉直通</h2>
|
||||
<p>实时获取名企最新内推信息,自动填写网申内推码,简历更快到达HR。</p>
|
||||
<button class="home-feature__btn" @click="router.push('/jobs')">
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><path d="M3 3h10l4 4v10H3V3z" stroke="currentColor" stroke-width="2" stroke-linejoin="round"/><path d="M6 12l2 2 4-4" stroke="currentColor" stroke-width="2"/></svg>
|
||||
<span>立即投递</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -260,50 +275,63 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Testimonials Section -->
|
||||
<!-- 用户评价区 Testimonials Section -->
|
||||
<section class="home-testimonials">
|
||||
<!-- 底层背景色块 -->
|
||||
<div class="home-testimonials__orb home-testimonials__orb--left"></div>
|
||||
<div class="home-testimonials__orb home-testimonials__orb--right"></div>
|
||||
<!-- 内容层白色容器 -->
|
||||
<div class="home-testimonials__container">
|
||||
<!-- 标题区 -->
|
||||
<div class="home-testimonials__header">
|
||||
<h2>万千毕业生的信赖之选</h2>
|
||||
<p>REAL VOICES FROM THE COMMUNITY</p>
|
||||
</div>
|
||||
<div class="home-testimonials__founder">
|
||||
<div class="home-testimonials__founder-img">👩💼</div>
|
||||
<blockquote>"很多大学生还在用传统方式找工作,海投、反复改简历,效率很低。Offer派利用AI技术让整个校招流程更丝滑,节省了80%的繁琐过程。"</blockquote>
|
||||
<cite><span class="cite-name">创始人</span> <span class="cite-role">Offer派</span></cite>
|
||||
</div>
|
||||
<div class="home-testimonials__scroll">
|
||||
<!-- 评价卡片横向排列 -->
|
||||
<div class="home-testimonials__cards">
|
||||
<div class="testimonial-card" v-for="(t, i) in testimonials" :key="i">
|
||||
<svg class="testimonial-card__quote" width="48" height="48" viewBox="0 0 48 48" fill="none"><path d="M6 6h14v36" stroke="rgba(82,202,209,0.2)" stroke-width="2"/><path d="M28 6h14v36" stroke="rgba(82,202,209,0.2)" stroke-width="2"/></svg>
|
||||
<!-- 引号装饰 SVG -->
|
||||
<svg class="testimonial-card__quote" width="48" height="48" viewBox="0 0 48 48" fill="none">
|
||||
<path d="M6 6h14v36" stroke="rgba(82,202,209,0.2)" stroke-width="2"/>
|
||||
<path d="M28 6h14v36" stroke="rgba(82,202,209,0.2)" stroke-width="2"/>
|
||||
</svg>
|
||||
<p class="testimonial-card__text">"{{ t.text }}"</p>
|
||||
<div class="testimonial-card__author">
|
||||
<div class="testimonial-card__avatar"></div>
|
||||
<div>
|
||||
<img class="testimonial-card__avatar" :src="avatarImg" alt="用户头像" />
|
||||
<div class="testimonial-card__info">
|
||||
<p class="testimonial-card__name">{{ t.name }}</p>
|
||||
<p class="testimonial-card__school">{{ t.school }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Job Search Section -->
|
||||
<section class="home-job-search">
|
||||
<div class="home-job-search__inner">
|
||||
<h3>一键找到最适合你的工作</h3>
|
||||
<div class="home-job-search__filters">
|
||||
<div class="filter-select">
|
||||
<span>行业</span>
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none"><path d="M6 9l6 6 6-6" stroke="currentColor" stroke-width="2"/></svg>
|
||||
<!-- 创始人引言卡片(可切换) -->
|
||||
<div class="home-testimonials__founder">
|
||||
<!-- 装饰圆环 SVG -->
|
||||
<svg class="home-testimonials__founder-decor" width="455" height="455" viewBox="0 0 455 455" fill="none">
|
||||
<circle cx="227.5" cy="227.5" r="189.5" stroke="#fff" stroke-width="20" opacity="0.08"/>
|
||||
<circle cx="227.5" cy="227.5" r="80" stroke="#fff" stroke-width="20" opacity="0.08"/>
|
||||
</svg>
|
||||
<div class="home-testimonials__founder-content">
|
||||
<img class="home-testimonials__founder-img" :src="avatarImg" alt="创始人头像" />
|
||||
<div class="home-testimonials__founder-text">
|
||||
<blockquote>"{{ founderQuotes[founderIndex].text }}"</blockquote>
|
||||
<cite>
|
||||
<span class="cite-name">{{ founderQuotes[founderIndex].name }}</span>
|
||||
<span class="cite-role">{{ founderQuotes[founderIndex].role }}</span>
|
||||
</cite>
|
||||
</div>
|
||||
<div class="filter-select">
|
||||
<span>地区</span>
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none"><path d="M6 9l6 6 6-6" stroke="currentColor" stroke-width="2"/></svg>
|
||||
</div>
|
||||
<div class="filter-select">
|
||||
<span>岗位</span>
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none"><path d="M6 9l6 6 6-6" stroke="currentColor" stroke-width="2"/></svg>
|
||||
</div>
|
||||
<button class="filter-btn" @click="router.push('/jobs')">搜索职位</button>
|
||||
<!-- 分页小圆点 -->
|
||||
<div class="home-testimonials__dots">
|
||||
<span
|
||||
v-for="(_, i) in founderQuotes"
|
||||
:key="i"
|
||||
class="home-testimonials__dot"
|
||||
:class="{ 'home-testimonials__dot--active': founderIndex === i }"
|
||||
@click="founderIndex = i"
|
||||
></span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -348,12 +376,52 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Job Search Section -->
|
||||
<section class="home-job-search">
|
||||
<div class="home-job-search__inner">
|
||||
<h3>一键找到最适合你的工作</h3>
|
||||
<div class="home-job-search__filters">
|
||||
<!-- 行业选择器 -->
|
||||
<IndustrySelector
|
||||
:industryIds="homeIndustryIds"
|
||||
:level="2"
|
||||
:maxSelect="3"
|
||||
:allowParentSelect="false"
|
||||
:triggerStyle="filterTriggerStyle"
|
||||
:displayStyle="filterDisplayStyle"
|
||||
@update:industryIds="onHomeIndustryChange"
|
||||
/>
|
||||
<!-- 地区选择器 -->
|
||||
<RegionSelector
|
||||
:regionCodes="homeRegionCodes"
|
||||
:level="2"
|
||||
:maxSelect="3"
|
||||
:triggerStyle="filterTriggerStyle"
|
||||
:displayStyle="filterDisplayStyle"
|
||||
@update:regionCodes="onHomeRegionChange"
|
||||
/>
|
||||
<!-- 岗位选择器 -->
|
||||
<JobCategorySelector
|
||||
:categoryIds="homeCategoryIds"
|
||||
:level="3"
|
||||
:maxSelect="3"
|
||||
:allowParentSelect="false"
|
||||
:triggerStyle="filterTriggerStyle"
|
||||
:displayStyle="filterDisplayStyle"
|
||||
@update:categoryIds="onHomeCategoryChange"
|
||||
/>
|
||||
<button class="filter-btn" @click="goSearchJobs">搜索职位</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Footer -->
|
||||
<footer class="home-footer">
|
||||
<div class="home-footer__inner">
|
||||
<div class="home-footer__col">
|
||||
<div class="home-footer__logo">
|
||||
<span class="home-footer__logo-text">Offer派</span>
|
||||
<!-- 网站Logo图片 -->
|
||||
<img src="@/assets/images/logo.png" alt="Offer派" class="home-footer__logo-img" />
|
||||
</div>
|
||||
<p class="home-footer__slogan">大学生AI求职平台</p>
|
||||
</div>
|
||||
@@ -391,9 +459,13 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { ref, onMounted, computed } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useStore } from 'vuex'
|
||||
import avatarImg from '@/assets/images/home/avatar-temporary.png'
|
||||
import IndustrySelector from '@/components/tools/IndustrySelector.vue'
|
||||
import RegionSelector from '@/components/tools/RegionSelector.vue'
|
||||
import JobCategorySelector from '@/components/tools/JobCategorySelector.vue'
|
||||
|
||||
/** 路由实例 */
|
||||
const router = useRouter()
|
||||
@@ -403,6 +475,15 @@ const store = useStore()
|
||||
/** 当前展开的 FAQ 索引,-1 表示全部收起 */
|
||||
const faqOpen = ref(-1)
|
||||
|
||||
/** 创始人引言当前索引 — 点击小圆点可切换 */
|
||||
const founderIndex = ref(0)
|
||||
|
||||
/** 创始人引言数据 — 支持多条切换 */
|
||||
const founderQuotes = [
|
||||
{ text: '很多大学生还在用传统方式找工作,海投、反复改简历,效率很低。Offer派利用AI技术让整个校招流程更丝滑,节省了80%的繁琐过程。', name: 'Stella', role: 'Offer派创始人' },
|
||||
{ text: '我们的目标是让每一位大学生都能高效地找到心仪的工作,AI技术正在改变校招的游戏规则。', name: 'Stella', role: 'Offer派创始人' },
|
||||
]
|
||||
|
||||
/** 岗位滚动展示数据 — 模拟最新发布的校招岗位 */
|
||||
const tickerJobs = [
|
||||
{ company: '字节跳动', time: '15分钟前', title: '前端开发工程师' },
|
||||
@@ -436,10 +517,74 @@ const faqs = [
|
||||
{ q: '支持哪些企业?', a: '目前已覆盖互联网、金融、制造、快消等行业的数千家企业,包括字节跳动、腾讯、阿里巴巴、华为等头部企业。' },
|
||||
]
|
||||
|
||||
// ==================== 底部筛选区:行业/地区/岗位选择器 ====================
|
||||
|
||||
/** 首页选中的行业 ID 数组 — 读写 store.jobIntention */
|
||||
const homeIndustryIds = computed<number[]>(() => store.state.jobIntention.industryIds || [])
|
||||
|
||||
/** 首页选中的地区编码数组 — 读写 store.jobIntention */
|
||||
const homeRegionCodes = computed<string[]>(() => store.state.jobIntention.regionCodes || [])
|
||||
|
||||
/** 首页选中的岗位 ID 数组 — 读写 store.jobIntention */
|
||||
const homeCategoryIds = computed<number[]>(() => store.state.jobIntention.categoryIds || [])
|
||||
|
||||
/** 选择器触发按钮样式 — 匹配首页 .filter-select 的外观 */
|
||||
const filterTriggerStyle = {
|
||||
width: '2.35rem',
|
||||
height: '0.48rem',
|
||||
'border-radius': '0.24rem',
|
||||
border: '1px solid #e5e7eb',
|
||||
padding: '0 0.2rem',
|
||||
background: '#fff',
|
||||
'justify-content': 'space-between',
|
||||
}
|
||||
|
||||
/** 选择器显示文字样式 — 匹配首页 .filter-select 内文字 */
|
||||
const filterDisplayStyle = {
|
||||
'font-size': '0.16rem',
|
||||
'line-height': '0.24rem',
|
||||
color: 'rgba(0, 0, 0, 0.45)',
|
||||
'max-width': '1.8rem',
|
||||
}
|
||||
|
||||
/** 行业选择变更回调 — 保存到 store */
|
||||
function onHomeIndustryChange(ids: number[]) {
|
||||
store.dispatch('saveJobIntention', {
|
||||
...store.state.jobIntention,
|
||||
industryIds: ids,
|
||||
})
|
||||
}
|
||||
|
||||
/** 地区选择变更回调 — 保存到 store */
|
||||
function onHomeRegionChange(codes: string[]) {
|
||||
store.dispatch('saveJobIntention', {
|
||||
...store.state.jobIntention,
|
||||
regionCodes: codes,
|
||||
})
|
||||
}
|
||||
|
||||
/** 岗位选择变更回调 — 保存到 store */
|
||||
function onHomeCategoryChange(ids: number[]) {
|
||||
store.dispatch('saveJobIntention', {
|
||||
...store.state.jobIntention,
|
||||
categoryIds: ids,
|
||||
})
|
||||
}
|
||||
|
||||
/** 点击搜索职位 — 跳转到 Jobs 页面 */
|
||||
function goSearchJobs() {
|
||||
router.push('/jobs')
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// 加载公共工具数据(行业分类、岗位分类、地区分类等)
|
||||
store.dispatch('loadCommonData')
|
||||
// 触发预渲染事件 — 配合 vite.config.ts 中 PrerenderPlugin 的 renderAfterDocumentEvent 设置
|
||||
document.dispatchEvent(new Event('prerender-trigger'))
|
||||
})
|
||||
|
||||
/** 点击登陆按钮 — 打开登录弹窗,登录成功后跳转到 jobs 页面 */
|
||||
function handleLoginClick() {
|
||||
store.dispatch('openLogin', '/jobs')
|
||||
}
|
||||
</script>
|
||||
|
||||
+3
-1
@@ -100,7 +100,7 @@
|
||||
</div>
|
||||
|
||||
<!-- 职位列表 -->
|
||||
<div ref="jobListRef" class="jobs-page__list pr5" :style="restoring ? { visibility: 'hidden' } : {}" @scroll="onListScroll">
|
||||
<div ref="jobListRef" v-loading="loading" class="jobs-page__list pr5" :style="restoring ? { visibility: 'hidden' } : {}" @scroll="onListScroll">
|
||||
<div
|
||||
v-for="(job, index) in jobList"
|
||||
:key="index"
|
||||
@@ -232,6 +232,8 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 暂无数据提示 -->
|
||||
<div v-if="!loading && jobList.length === 0" class="jobs-page__empty">暂无数据</div>
|
||||
<!-- 加载更多提示 -->
|
||||
<div v-if="loadingMore" class="jobs-page__loading-more">加载中...</div>
|
||||
<div v-else-if="noMore && jobList.length > 0" class="jobs-page__loading-more">没有更多了</div>
|
||||
|
||||
+104
-37
@@ -77,15 +77,22 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import SideNav from '@/components/SideNav.vue'
|
||||
import { uploadResume } from '@/utils/aiRequest'
|
||||
import { fetchResumeList, type ResumeListItem } from '@/api/resume'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
// ==================== 头像颜色池 ====================
|
||||
|
||||
/** 头像背景色列表,按索引循环取色 */
|
||||
const avatarColors = ['#1A1A2E', '#4FC2C9', '#F2994A', '#6C5CE7', '#E17055', '#00B894', '#FDCB6E']
|
||||
|
||||
// ==================== 类型定义 ====================
|
||||
|
||||
/** 简历列表项类型 */
|
||||
/** 简历列表项(前端展示用) */
|
||||
interface ResumeItem {
|
||||
id: string
|
||||
name: string
|
||||
@@ -97,41 +104,68 @@ interface ResumeItem {
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
// ==================== 数据(模拟数据,后续对接接口) ====================
|
||||
// ==================== 工具方法 ====================
|
||||
|
||||
/**
|
||||
* 将 Instant 时间戳转为友好的相对时间文案
|
||||
* @param instant 后端返回的 Instant 对象
|
||||
*/
|
||||
function formatTime(instant?: { seconds?: number; nanos?: number }): string {
|
||||
if (!instant?.seconds) return '-'
|
||||
const date = new Date(instant.seconds * 1000)
|
||||
const now = Date.now()
|
||||
const diff = now - date.getTime()
|
||||
const minutes = Math.floor(diff / 60000)
|
||||
if (minutes < 1) return '刚刚'
|
||||
if (minutes < 60) return `${minutes}分钟前`
|
||||
const hours = Math.floor(minutes / 60)
|
||||
if (hours < 24) return `${hours}小时前`
|
||||
const days = Math.floor(hours / 24)
|
||||
if (days < 30) return `${days}天前`
|
||||
const months = Math.floor(days / 30)
|
||||
if (months < 12) return `${months}个月前`
|
||||
return `${Math.floor(months / 12)}年前`
|
||||
}
|
||||
|
||||
/**
|
||||
* 将后端简历数据转为前端展示结构
|
||||
*/
|
||||
function mapResumeItem(item: ResumeListItem, index: number): ResumeItem {
|
||||
const name = item.resumeName || '未命名简历'
|
||||
return {
|
||||
id: String(item.id ?? ''),
|
||||
name,
|
||||
avatarLetter: name.charAt(0) || '?',
|
||||
avatarColor: avatarColors[index % avatarColors.length],
|
||||
isDefault: item.isDefault === 1,
|
||||
targetJob: item.targetPosition || '',
|
||||
updatedAt: formatTime(item.updateTime),
|
||||
createdAt: formatTime(item.createTime),
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 数据 ====================
|
||||
|
||||
/** 简历列表 */
|
||||
const resumeList = ref<ResumeItem[]>([
|
||||
{
|
||||
id: '1',
|
||||
name: '李华_产品经理',
|
||||
avatarLetter: 'D',
|
||||
avatarColor: '#1A1A2E',
|
||||
isDefault: true,
|
||||
targetJob: '产品经理',
|
||||
updatedAt: '1个月前',
|
||||
createdAt: '1个月前',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: '李华_产品运营',
|
||||
avatarLetter: 'C',
|
||||
avatarColor: '#4FC2C9',
|
||||
isDefault: false,
|
||||
targetJob: '产品运营',
|
||||
updatedAt: '1个月前',
|
||||
createdAt: '1个月前',
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
name: '李华_产品经理',
|
||||
avatarLetter: '?',
|
||||
avatarColor: '#BFBFBF',
|
||||
isDefault: false,
|
||||
targetJob: '',
|
||||
updatedAt: '1个月前',
|
||||
createdAt: '1个月前',
|
||||
},
|
||||
])
|
||||
const resumeList = ref<ResumeItem[]>([])
|
||||
|
||||
/** 加载简历列表 */
|
||||
async function loadResumeList() {
|
||||
try {
|
||||
const res = await fetchResumeList()
|
||||
if (res.code === '0' && res.data) {
|
||||
resumeList.value = res.data.map(mapResumeItem)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('获取简历列表失败', e)
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 生命周期 ====================
|
||||
|
||||
onMounted(() => {
|
||||
loadResumeList()
|
||||
})
|
||||
|
||||
// ==================== 页面状态 ====================
|
||||
|
||||
@@ -154,9 +188,42 @@ function handleAction(action: string, id: string) {
|
||||
console.log(action, id)
|
||||
}
|
||||
|
||||
/** 上传简历 */
|
||||
/** 上传简历 — 弹出文件选择,选择后调用 AI 接口上传 */
|
||||
function handleUpload() {
|
||||
console.log('上传简历')
|
||||
const input = document.createElement('input')
|
||||
input.type = 'file'
|
||||
// 限定 pdf 和 word 格式
|
||||
input.accept = '.pdf,.doc,.docx,application/pdf,application/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document'
|
||||
|
||||
input.onchange = async () => {
|
||||
const file = input.files?.[0]
|
||||
if (!file) return
|
||||
|
||||
// 全屏加载提示,AI 接口响应较慢
|
||||
const loading = ElLoading.service({
|
||||
lock: true,
|
||||
text: '简历解析中,请耐心等待…',
|
||||
background: 'rgba(0, 0, 0, 0.5)',
|
||||
customClass: 'resume-upload-loading',
|
||||
})
|
||||
|
||||
try {
|
||||
const res = await uploadResume(file)
|
||||
if (res.code === 0) {
|
||||
// 上传成功,刷新列表并跳转详情页
|
||||
loadResumeList()
|
||||
goDetail(String(res.data.resumeId))
|
||||
} else {
|
||||
ElMessage.error(res.msg || '上传失败')
|
||||
}
|
||||
} catch {
|
||||
ElMessage.error('上传失败,请稍后重试')
|
||||
} finally {
|
||||
loading.close()
|
||||
}
|
||||
}
|
||||
|
||||
input.click()
|
||||
}
|
||||
|
||||
/** 跳转到简历详情页 */
|
||||
|
||||
+633
-80
@@ -2,6 +2,15 @@
|
||||
<div class="resume-detail dflex">
|
||||
<SideNav />
|
||||
<div class="resume-detail__content">
|
||||
<!-- 个人资料编辑抽屉 -->
|
||||
<ProfileEditDrawer
|
||||
v-model="showEditDrawer"
|
||||
:module="editModule"
|
||||
:initial-data="editInitialData"
|
||||
:use-education-text-value="true"
|
||||
@save="handleSaveEdit"
|
||||
/>
|
||||
|
||||
<!-- 顶部标题 -->
|
||||
<h2 class="resume-detail__page-title">我的简历</h2>
|
||||
|
||||
@@ -13,7 +22,7 @@
|
||||
<path d="M12 4L4 12M4 4l8 8" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
|
||||
</svg>
|
||||
</button>
|
||||
<span class="resume-detail__tab-name">{{ resume.name }}</span>
|
||||
<span class="resume-detail__tab-name">{{ resumeMain.resumeName || '未命名简历' }}</span>
|
||||
</div>
|
||||
<div class="resume-detail__toolbar-right">
|
||||
<button class="resume-detail__tool-btn" @click="handleFeedback">问题反馈</button>
|
||||
@@ -41,23 +50,23 @@
|
||||
<!-- 简历评分区域 -->
|
||||
<div class="resume-detail__score-bar">
|
||||
<div class="resume-detail__score-left">
|
||||
<span class="resume-detail__score-avatar" :style="{ background: resume.avatarColor }">
|
||||
{{ resume.avatarLetter }}
|
||||
<span class="resume-detail__score-avatar">
|
||||
{{ getAvatarLetter() }}
|
||||
</span>
|
||||
<span class="resume-detail__score-badge">良好</span>
|
||||
<button class="resume-detail__score-link" @click="handleViewReport">查看评估报告 ></button>
|
||||
</div>
|
||||
<div class="resume-detail__score-right">
|
||||
<div class="resume-detail__score-item">
|
||||
<span class="resume-detail__score-num">{{ resume.urgentCount }}</span>
|
||||
<span class="resume-detail__score-num">0</span>
|
||||
<span class="resume-detail__score-label">紧急修复项</span>
|
||||
</div>
|
||||
<div class="resume-detail__score-item">
|
||||
<span class="resume-detail__score-num">{{ resume.severeCount }}</span>
|
||||
<span class="resume-detail__score-num">0</span>
|
||||
<span class="resume-detail__score-label">严重问题</span>
|
||||
</div>
|
||||
<div class="resume-detail__score-item">
|
||||
<span class="resume-detail__score-num">{{ resume.optionalCount }}</span>
|
||||
<span class="resume-detail__score-num">0</span>
|
||||
<span class="resume-detail__score-label">可选修复项</span>
|
||||
</div>
|
||||
<button class="resume-detail__diagnose-btn" @click="handleDiagnose">重新诊断</button>
|
||||
@@ -70,72 +79,327 @@
|
||||
<div class="resume-detail__card">
|
||||
<div class="resume-detail__card-header">
|
||||
<div>
|
||||
<h3 class="resume-detail__user-name">{{ resume.realName }}</h3>
|
||||
<p class="resume-detail__user-title">{{ resume.jobTitle }}</p>
|
||||
</div>
|
||||
<div class="resume-detail__card-actions">
|
||||
<button class="resume-detail__card-btn resume-detail__card-btn--outline" @click="handleUrgentFix">紧急修复项</button>
|
||||
<button class="resume-detail__card-btn resume-detail__card-btn--dark" @click="handlePolish">修复</button>
|
||||
<h3 class="resume-detail__user-name">{{ resumeMain.name || '未填写姓名' }}</h3>
|
||||
<p class="resume-detail__user-title">{{ resumeMain.targetPosition || '' }}</p>
|
||||
</div>
|
||||
<!-- 编辑按钮 -->
|
||||
<button class="resume-detail__edit-btn" @click="openEditDrawer('info')" aria-label="编辑个人信息">
|
||||
<svg viewBox="0 0 16 16" fill="none" class="resume-detail__edit-icon"><path d="M11.5 2.5l2 2L5 13H3v-2l8.5-8.5z" stroke="currentColor" stroke-width="1.2" stroke-linejoin="round"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="resume-detail__contact">
|
||||
<span class="resume-detail__contact-item">
|
||||
<!-- 邮箱 -->
|
||||
<span v-if="resumeMain.email" class="resume-detail__contact-item">
|
||||
<svg viewBox="0 0 16 16" fill="none" class="resume-detail__contact-icon">
|
||||
<rect x="2" y="3" width="12" height="10" rx="1.5" stroke="currentColor" stroke-width="1.2"/>
|
||||
<path d="M2 5.5l6 4 6-4" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"/>
|
||||
</svg>
|
||||
{{ resume.email }}
|
||||
{{ resumeMain.email }}
|
||||
</span>
|
||||
<span class="resume-detail__contact-item">
|
||||
<!-- 手机号 -->
|
||||
<span v-if="resumeMain.mobileNumber" class="resume-detail__contact-item">
|
||||
<svg viewBox="0 0 16 16" fill="none" class="resume-detail__contact-icon">
|
||||
<path d="M4 2h2.5l1.5 3-1.5 1.5a8 8 0 003 3L11 8l3 1.5V12a2 2 0 01-2 2C6.5 14 2 9.5 2 4a2 2 0 012-2z" stroke="currentColor" stroke-width="1.2" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
{{ resume.phone }}
|
||||
{{ resumeMain.mobileNumber }}
|
||||
</span>
|
||||
<span class="resume-detail__contact-item">
|
||||
<!-- 所在城市 -->
|
||||
<span v-if="resumeMain.city" class="resume-detail__contact-item">
|
||||
<svg viewBox="0 0 16 16" fill="none" class="resume-detail__contact-icon">
|
||||
<circle cx="8" cy="6.5" r="2.5" stroke="currentColor" stroke-width="1.2"/>
|
||||
<path d="M8 14s-5-4-5-7.5a5 5 0 0110 0C13 10 8 14 8 14z" stroke="currentColor" stroke-width="1.2"/>
|
||||
</svg>
|
||||
{{ resume.location }}
|
||||
{{ resumeMain.city }}
|
||||
</span>
|
||||
<!-- 微信号 -->
|
||||
<span v-if="resumeMain.wechatNumber" class="resume-detail__contact-item">
|
||||
<svg viewBox="0 0 16 16" fill="none" class="resume-detail__contact-icon">
|
||||
<path d="M2 8a6 6 0 1112 0 6 6 0 01-12 0z" stroke="currentColor" stroke-width="1.2"/>
|
||||
<path d="M5.5 7.5c.5-1 1.5-1.5 2.5-1.5s2 .5 2.5 1.5M6 10s.8 1 2 1 2-1 2-1" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"/>
|
||||
</svg>
|
||||
{{ resumeMain.wechatNumber }}
|
||||
</span>
|
||||
</div>
|
||||
<!-- 问题操作区 -->
|
||||
<div v-if="cardIssueConfig.personalInfo.show" class="resume-detail__card-issue">
|
||||
<div class="resume-detail__issue-type-group">
|
||||
<button
|
||||
v-for="t in getCardIssueTypes('personalInfo')" :key="t.value"
|
||||
class="resume-detail__issue-type-btn"
|
||||
:class="{ 'resume-detail__issue-type-btn--active': cardIssueConfig.personalInfo.activeType === t.value }"
|
||||
@click="cardIssueConfig.personalInfo.activeType = t.value"
|
||||
>{{ t.label }}</button>
|
||||
</div>
|
||||
<button class="resume-detail__card-btn resume-detail__card-btn--dark" @click="handleFixIssue('personalInfo')">修复</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 个人概述 -->
|
||||
<div v-if="resumeMain.summary" class="resume-detail__card">
|
||||
<div class="resume-detail__section-header">
|
||||
<h3 class="resume-detail__section-title">个人概述</h3>
|
||||
<!-- 编辑按钮 -->
|
||||
<button class="resume-detail__edit-btn" @click="openEditDrawer('summary')" aria-label="编辑个人概述">
|
||||
<svg viewBox="0 0 16 16" fill="none" class="resume-detail__edit-icon"><path d="M11.5 2.5l2 2L5 13H3v-2l8.5-8.5z" stroke="currentColor" stroke-width="1.2" stroke-linejoin="round"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
<p class="resume-detail__summary-text">{{ resumeMain.summary }}</p>
|
||||
<!-- 问题操作区 -->
|
||||
<div v-if="cardIssueConfig.summary.show" class="resume-detail__card-issue">
|
||||
<div class="resume-detail__issue-type-group">
|
||||
<button
|
||||
v-for="t in getCardIssueTypes('summary')" :key="t.value"
|
||||
class="resume-detail__issue-type-btn"
|
||||
:class="{ 'resume-detail__issue-type-btn--active': cardIssueConfig.summary.activeType === t.value }"
|
||||
@click="cardIssueConfig.summary.activeType = t.value"
|
||||
>{{ t.label }}</button>
|
||||
</div>
|
||||
<button class="resume-detail__card-btn resume-detail__card-btn--dark" @click="handleFixIssue('summary')">修复</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 作品集链接 -->
|
||||
<div v-if="resumeMain.portfolioUrl" class="resume-detail__card">
|
||||
<div class="resume-detail__section-header">
|
||||
<h3 class="resume-detail__section-title">作品集</h3>
|
||||
<!-- 编辑按钮 -->
|
||||
<button class="resume-detail__edit-btn" @click="openEditDrawer('portfolio')" aria-label="编辑作品集">
|
||||
<svg viewBox="0 0 16 16" fill="none" class="resume-detail__edit-icon"><path d="M11.5 2.5l2 2L5 13H3v-2l8.5-8.5z" stroke="currentColor" stroke-width="1.2" stroke-linejoin="round"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
<a :href="resumeMain.portfolioUrl" target="_blank" rel="noopener noreferrer" class="resume-detail__portfolio-link">
|
||||
{{ resumeMain.portfolioUrl }}
|
||||
</a>
|
||||
<!-- 问题操作区 -->
|
||||
<div v-if="cardIssueConfig.portfolio.show" class="resume-detail__card-issue">
|
||||
<div class="resume-detail__issue-type-group">
|
||||
<button
|
||||
v-for="t in getCardIssueTypes('portfolio')" :key="t.value"
|
||||
class="resume-detail__issue-type-btn"
|
||||
:class="{ 'resume-detail__issue-type-btn--active': cardIssueConfig.portfolio.activeType === t.value }"
|
||||
@click="cardIssueConfig.portfolio.activeType = t.value"
|
||||
>{{ t.label }}</button>
|
||||
</div>
|
||||
<button class="resume-detail__card-btn resume-detail__card-btn--dark" @click="handleFixIssue('portfolio')">修复</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 教育背景 -->
|
||||
<div class="resume-detail__card">
|
||||
<div v-if="educationList.length" class="resume-detail__card">
|
||||
<div class="resume-detail__section-header">
|
||||
<h3 class="resume-detail__section-title">教育背景</h3>
|
||||
<div class="resume-detail__card-actions">
|
||||
<button class="resume-detail__card-btn resume-detail__card-btn--outline">紧急修复项</button>
|
||||
<button class="resume-detail__card-btn resume-detail__card-btn--dark">修复</button>
|
||||
<!-- 编辑按钮 -->
|
||||
<button class="resume-detail__edit-btn" @click="openEditDrawer('education')" aria-label="编辑教育背景">
|
||||
<svg viewBox="0 0 16 16" fill="none" class="resume-detail__edit-icon"><path d="M11.5 2.5l2 2L5 13H3v-2l8.5-8.5z" stroke="currentColor" stroke-width="1.2" stroke-linejoin="round"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
<div v-for="edu in educationList" :key="edu.id" class="resume-detail__edu-item">
|
||||
<div class="resume-detail__edu-degree">{{ edu.degree }} · {{ edu.major }}</div>
|
||||
<div class="resume-detail__edu-meta">{{ edu.school }} · {{ edu.studyType }} · {{ edu.startDate }} - {{ edu.endDate }}</div>
|
||||
<div v-if="edu.description?.length" class="resume-detail__desc-list">
|
||||
<p v-for="p in edu.description" :key="p.id" class="resume-detail__desc-item">{{ p.text }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-for="(edu, i) in resume.education" :key="i" class="resume-detail__edu-item">
|
||||
<div class="resume-detail__edu-degree">{{ edu.degree }}</div>
|
||||
<div class="resume-detail__edu-meta">{{ edu.school }} · {{ edu.period }}</div>
|
||||
<div v-if="edu.gpa" class="resume-detail__edu-meta">GPA: {{ edu.gpa }}</div>
|
||||
<!-- 问题操作区 -->
|
||||
<div v-if="cardIssueConfig.education.show" class="resume-detail__card-issue">
|
||||
<div class="resume-detail__issue-type-group">
|
||||
<button
|
||||
v-for="t in getCardIssueTypes('education')" :key="t.value"
|
||||
class="resume-detail__issue-type-btn"
|
||||
:class="{ 'resume-detail__issue-type-btn--active': cardIssueConfig.education.activeType === t.value }"
|
||||
@click="cardIssueConfig.education.activeType = t.value"
|
||||
>{{ t.label }}</button>
|
||||
</div>
|
||||
<button class="resume-detail__card-btn resume-detail__card-btn--dark" @click="handleFixIssue('education')">修复</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 工作经验 -->
|
||||
<div class="resume-detail__card">
|
||||
<h3 class="resume-detail__section-title">工作经验</h3>
|
||||
<div v-for="(exp, i) in resume.experience" :key="i" class="resume-detail__exp-item">
|
||||
<div class="resume-detail__exp-title">{{ exp.title }}</div>
|
||||
<div class="resume-detail__exp-meta">{{ exp.company }} · {{ exp.period }}</div>
|
||||
<p class="resume-detail__exp-desc">{{ exp.description }}</p>
|
||||
<!-- 工作经历 -->
|
||||
<div v-if="workList.length" class="resume-detail__card">
|
||||
<div class="resume-detail__section-header">
|
||||
<h3 class="resume-detail__section-title">工作经历</h3>
|
||||
<!-- 编辑按钮 -->
|
||||
<button class="resume-detail__edit-btn" @click="openEditDrawer('work')" aria-label="编辑工作经历">
|
||||
<svg viewBox="0 0 16 16" fill="none" class="resume-detail__edit-icon"><path d="M11.5 2.5l2 2L5 13H3v-2l8.5-8.5z" stroke="currentColor" stroke-width="1.2" stroke-linejoin="round"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
<div v-for="exp in workList" :key="exp.id" class="resume-detail__exp-item">
|
||||
<div class="resume-detail__exp-title dflex">
|
||||
<span>{{ exp.companyName }}</span>
|
||||
<span v-if="exp.startDate||exp.endDate" class="color-5 fw400 fs12 pt5">{{ exp.startDate }} <span v-if="exp.startDate&&exp.endDate"> - </span> {{ exp.endDate }}</span>
|
||||
</div>
|
||||
<div class="resume-detail__exp-meta">{{ exp.position }} </div>
|
||||
<ul v-if="exp.description?.length" class="resume-detail__desc-list pl16">
|
||||
<li v-for="p in exp.description" :key="p.id" class="resume-detail__desc-item fs12 color-5">{{ p.text }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
<!-- 问题操作区 -->
|
||||
<div v-if="cardIssueConfig.work.show" class="resume-detail__card-issue">
|
||||
<div class="resume-detail__issue-type-group">
|
||||
<button
|
||||
v-for="t in getCardIssueTypes('work')" :key="t.value"
|
||||
class="resume-detail__issue-type-btn"
|
||||
:class="{ 'resume-detail__issue-type-btn--active': cardIssueConfig.work.activeType === t.value }"
|
||||
@click="cardIssueConfig.work.activeType = t.value"
|
||||
>{{ t.label }}</button>
|
||||
</div>
|
||||
<button class="resume-detail__card-btn resume-detail__card-btn--dark" @click="handleFixIssue('work')">修复</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 实习经历 -->
|
||||
<div v-if="internshipList.length" class="resume-detail__card">
|
||||
<div class="resume-detail__section-header">
|
||||
<h3 class="resume-detail__section-title">实习经历</h3>
|
||||
<!-- 编辑按钮 -->
|
||||
<button class="resume-detail__edit-btn" @click="openEditDrawer('internship')" aria-label="编辑实习经历">
|
||||
<svg viewBox="0 0 16 16" fill="none" class="resume-detail__edit-icon"><path d="M11.5 2.5l2 2L5 13H3v-2l8.5-8.5z" stroke="currentColor" stroke-width="1.2" stroke-linejoin="round"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
<div v-for="intern in internshipList" :key="intern.id" class="resume-detail__exp-item">
|
||||
<div class="resume-detail__exp-title dflex">
|
||||
<span>{{ intern.position }}</span>
|
||||
<span v-if="intern.startDate||intern.endDate" class="color-5 fw400 fs12 pt5">{{ intern.startDate }} <span v-if="intern.startDate&&intern.endDate"> - </span> {{ intern.endDate }}</span>
|
||||
</div>
|
||||
<div class="resume-detail__exp-meta">{{ intern.companyName }}</div>
|
||||
<div v-if="intern.description?.length" class="resume-detail__desc-list">
|
||||
<p v-for="p in intern.description" :key="p.id" class="resume-detail__desc-item">{{ p.text }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 问题操作区 -->
|
||||
<div v-if="cardIssueConfig.internship.show" class="resume-detail__card-issue">
|
||||
<div class="resume-detail__issue-type-group">
|
||||
<button
|
||||
v-for="t in getCardIssueTypes('internship')" :key="t.value"
|
||||
class="resume-detail__issue-type-btn"
|
||||
:class="{ 'resume-detail__issue-type-btn--active': cardIssueConfig.internship.activeType === t.value }"
|
||||
@click="cardIssueConfig.internship.activeType = t.value"
|
||||
>{{ t.label }}</button>
|
||||
</div>
|
||||
<button class="resume-detail__card-btn resume-detail__card-btn--dark" @click="handleFixIssue('internship')">修复</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 项目经历 -->
|
||||
<div v-if="projectList.length" class="resume-detail__card">
|
||||
<div class="resume-detail__section-header">
|
||||
<h3 class="resume-detail__section-title">项目经历</h3>
|
||||
<!-- 编辑按钮 -->
|
||||
<button class="resume-detail__edit-btn" @click="openEditDrawer('project')" aria-label="编辑项目经历">
|
||||
<svg viewBox="0 0 16 16" fill="none" class="resume-detail__edit-icon"><path d="M11.5 2.5l2 2L5 13H3v-2l8.5-8.5z" stroke="currentColor" stroke-width="1.2" stroke-linejoin="round"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
<div v-for="proj in projectList" :key="proj.id" class="resume-detail__exp-item">
|
||||
<div class="resume-detail__exp-title dflex">
|
||||
<span>{{ proj.projectName }}</span>
|
||||
<span v-if="proj.startDate||proj.endDate" class="color-5 fw400 fs12 pt5">{{ proj.startDate }} <span v-if="proj.startDate&&proj.endDate"> - </span> {{ proj.endDate }}</span>
|
||||
</div>
|
||||
<div class="resume-detail__exp-meta">{{ proj.companyName }} · {{ proj.role }}</div>
|
||||
<div v-if="proj.description?.length" class="resume-detail__desc-list">
|
||||
<p v-for="p in proj.description" :key="p.id" class="resume-detail__desc-item">{{ p.text }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 问题操作区 -->
|
||||
<div v-if="cardIssueConfig.project.show" class="resume-detail__card-issue">
|
||||
<div class="resume-detail__issue-type-group">
|
||||
<button
|
||||
v-for="t in getCardIssueTypes('project')" :key="t.value"
|
||||
class="resume-detail__issue-type-btn"
|
||||
:class="{ 'resume-detail__issue-type-btn--active': cardIssueConfig.project.activeType === t.value }"
|
||||
@click="cardIssueConfig.project.activeType = t.value"
|
||||
>{{ t.label }}</button>
|
||||
</div>
|
||||
<button class="resume-detail__card-btn resume-detail__card-btn--dark" @click="handleFixIssue('project')">修复</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 竞赛经历 -->
|
||||
<div v-if="competitionList.length" class="resume-detail__card">
|
||||
<div class="resume-detail__section-header">
|
||||
<h3 class="resume-detail__section-title">竞赛经历</h3>
|
||||
<!-- 编辑按钮 -->
|
||||
<button class="resume-detail__edit-btn" @click="openEditDrawer('competition')" aria-label="编辑竞赛经历">
|
||||
<svg viewBox="0 0 16 16" fill="none" class="resume-detail__edit-icon"><path d="M11.5 2.5l2 2L5 13H3v-2l8.5-8.5z" stroke="currentColor" stroke-width="1.2" stroke-linejoin="round"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
<div v-for="comp in competitionList" :key="comp.id" class="resume-detail__exp-item">
|
||||
<div class="resume-detail__exp-title dflex">
|
||||
<span>{{ comp.competitionName }}</span>
|
||||
<span v-if="comp.awardDate" class="color-5 fw400 fs12 pt5">{{ comp.awardDate }}</span>
|
||||
</div>
|
||||
<div class="resume-detail__exp-meta">{{ comp.award }}</div>
|
||||
<div v-if="comp.description?.length" class="resume-detail__desc-list">
|
||||
<p v-for="p in comp.description" :key="p.id" class="resume-detail__desc-item">{{ p.text }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 问题操作区 -->
|
||||
<div v-if="cardIssueConfig.competition.show" class="resume-detail__card-issue">
|
||||
<div class="resume-detail__issue-type-group">
|
||||
<button
|
||||
v-for="t in getCardIssueTypes('competition')" :key="t.value"
|
||||
class="resume-detail__issue-type-btn"
|
||||
:class="{ 'resume-detail__issue-type-btn--active': cardIssueConfig.competition.activeType === t.value }"
|
||||
@click="cardIssueConfig.competition.activeType = t.value"
|
||||
>{{ t.label }}</button>
|
||||
</div>
|
||||
<button class="resume-detail__card-btn resume-detail__card-btn--dark" @click="handleFixIssue('competition')">修复</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 技能 -->
|
||||
<div class="resume-detail__card">
|
||||
<div v-if="resumeMain.skills?.length" class="resume-detail__card">
|
||||
<div class="resume-detail__section-header">
|
||||
<h3 class="resume-detail__section-title">技能</h3>
|
||||
<!-- 编辑按钮 -->
|
||||
<button class="resume-detail__edit-btn" @click="openEditDrawer('skills')" aria-label="编辑技能">
|
||||
<svg viewBox="0 0 16 16" fill="none" class="resume-detail__edit-icon"><path d="M11.5 2.5l2 2L5 13H3v-2l8.5-8.5z" stroke="currentColor" stroke-width="1.2" stroke-linejoin="round"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="resume-detail__skills">
|
||||
<span v-for="(skill, i) in resume.skills" :key="i" class="resume-detail__skill-tag">
|
||||
<span v-for="(skill, i) in resumeMain.skills" :key="i" class="resume-detail__skill-tag">
|
||||
{{ skill }}
|
||||
</span>
|
||||
</div>
|
||||
<!-- 问题操作区 -->
|
||||
<div v-if="cardIssueConfig.skills.show" class="resume-detail__card-issue">
|
||||
<div class="resume-detail__issue-type-group">
|
||||
<button
|
||||
v-for="t in getCardIssueTypes('skills')" :key="t.value"
|
||||
class="resume-detail__issue-type-btn"
|
||||
:class="{ 'resume-detail__issue-type-btn--active': cardIssueConfig.skills.activeType === t.value }"
|
||||
@click="cardIssueConfig.skills.activeType = t.value"
|
||||
>{{ t.label }}</button>
|
||||
</div>
|
||||
<button class="resume-detail__card-btn resume-detail__card-btn--dark" @click="handleFixIssue('skills')">修复</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 证书 -->
|
||||
<div v-if="resumeMain.certificates?.length" class="resume-detail__card">
|
||||
<div class="resume-detail__section-header">
|
||||
<h3 class="resume-detail__section-title">证书</h3>
|
||||
<!-- 编辑按钮 -->
|
||||
<button class="resume-detail__edit-btn" @click="openEditDrawer('certificate')" aria-label="编辑证书">
|
||||
<svg viewBox="0 0 16 16" fill="none" class="resume-detail__edit-icon"><path d="M11.5 2.5l2 2L5 13H3v-2l8.5-8.5z" stroke="currentColor" stroke-width="1.2" stroke-linejoin="round"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="resume-detail__skills">
|
||||
<span v-for="(cert, i) in resumeMain.certificates" :key="i" class="resume-detail__skill-tag">
|
||||
{{ cert }}
|
||||
</span>
|
||||
</div>
|
||||
<!-- 问题操作区 -->
|
||||
<div v-if="cardIssueConfig.certificates.show" class="resume-detail__card-issue">
|
||||
<div class="resume-detail__issue-type-group">
|
||||
<button
|
||||
v-for="t in getCardIssueTypes('certificates')" :key="t.value"
|
||||
class="resume-detail__issue-type-btn"
|
||||
:class="{ 'resume-detail__issue-type-btn--active': cardIssueConfig.certificates.activeType === t.value }"
|
||||
@click="cardIssueConfig.certificates.activeType = t.value"
|
||||
>{{ t.label }}</button>
|
||||
</div>
|
||||
<button class="resume-detail__card-btn resume-detail__card-btn--dark" @click="handleFixIssue('certificates')">修复</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -143,60 +407,149 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import SideNav from '@/components/SideNav.vue'
|
||||
import ProfileEditDrawer from '@/components/ProfileEditDrawer.vue'
|
||||
import {
|
||||
fetchResumeMain,
|
||||
fetchResumeEducation,
|
||||
fetchResumeWork,
|
||||
fetchResumeInternship,
|
||||
fetchResumeProject,
|
||||
fetchResumeCompetition,
|
||||
saveResumeMain,
|
||||
saveResumeEducation,
|
||||
saveResumeWork,
|
||||
saveResumeInternship,
|
||||
saveResumeProject,
|
||||
saveResumeCompetition,
|
||||
type ResumeMainData,
|
||||
type ResumeEducation,
|
||||
type ResumeWork,
|
||||
type ResumeInternship,
|
||||
type ResumeProject,
|
||||
type ResumeCompetition,
|
||||
} from '@/api/resume'
|
||||
|
||||
// ==================== 问题类型相关 ====================
|
||||
|
||||
/** 问题类型枚举值 */
|
||||
type IssueType = 'urgent' | 'optimize' | 'expression'
|
||||
|
||||
/** 问题类型选项列表 */
|
||||
const issueTypes: { label: string; value: IssueType }[] = [
|
||||
{ label: '紧急修复项', value: 'urgent' },
|
||||
{ label: '重点优化项', value: 'optimize' },
|
||||
{ label: '表达提升', value: 'expression' },
|
||||
]
|
||||
|
||||
/** 单个卡片的问题配置 */
|
||||
interface CardIssueItem {
|
||||
/** 是否显示问题操作区 */
|
||||
show: boolean
|
||||
/** 当前选中的问题类型 */
|
||||
activeType: IssueType
|
||||
/** 该模块拥有的问题类型列表(只显示这些) */
|
||||
types: IssueType[]
|
||||
}
|
||||
|
||||
/** 所有卡片模块的 key */
|
||||
type CardKey = 'personalInfo' | 'summary' | 'portfolio' | 'education' | 'work' | 'internship' | 'project' | 'competition' | 'skills' | 'certificates'
|
||||
|
||||
/** 各模块问题操作区的显示与选中状态配置,每个模块只包含自己实际存在的问题类型 */
|
||||
const cardIssueConfig = reactive<Record<CardKey, CardIssueItem>>({
|
||||
personalInfo: { show: true, activeType: 'urgent', types: ['urgent'] },
|
||||
summary: { show: true, activeType: 'expression', types: ['expression'] },
|
||||
portfolio: { show: true, activeType: 'optimize', types: ['optimize'] },
|
||||
education: { show: true, activeType: 'urgent', types: ['urgent'] },
|
||||
work: { show: true, activeType: 'optimize', types: ['optimize'] },
|
||||
internship: { show: true, activeType: 'optimize', types: ['optimize'] },
|
||||
project: { show: true, activeType: 'expression', types: ['expression'] },
|
||||
competition: { show: true, activeType: 'urgent', types: ['urgent'] },
|
||||
skills: { show: true, activeType: 'optimize', types: ['optimize'] },
|
||||
certificates: { show: true, activeType: 'urgent', types: ['urgent'] },
|
||||
})
|
||||
|
||||
/** 根据模块 key 获取该模块可用的问题类型选项 */
|
||||
function getCardIssueTypes(cardKey: CardKey) {
|
||||
return issueTypes.filter(t => cardIssueConfig[cardKey].types.includes(t.value))
|
||||
}
|
||||
|
||||
// ==================== 路由相关 ====================
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
|
||||
/** 当前简历 ID,从路由参数中获取 */
|
||||
/** 当前简历 ID,从路由参数中获取(保持字符串,避免大整数精度丢失) */
|
||||
const resumeId = route.params.id as string
|
||||
|
||||
// ==================== 简历数据(模拟数据,后续对接接口) ====================
|
||||
// ==================== 数据 ====================
|
||||
|
||||
const resume = ref({
|
||||
id: resumeId,
|
||||
name: '李华_产品经理',
|
||||
avatarLetter: 'B',
|
||||
avatarColor: '#1A1A2E',
|
||||
realName: '李华',
|
||||
jobTitle: '数据产品经理',
|
||||
email: '[email]',
|
||||
phone: '+1 (555) 123-4567',
|
||||
location: '北京',
|
||||
/** 紧急修复项数量 */
|
||||
urgentCount: 8,
|
||||
/** 严重问题数量 */
|
||||
severeCount: 0,
|
||||
/** 可选修复项数量 */
|
||||
optionalCount: 1,
|
||||
/** 教育背景 */
|
||||
education: [
|
||||
{ degree: '计算机科学学士', school: '斯坦福大学', period: '2018 - 2022', gpa: '3.8/4.0' },
|
||||
{ degree: '高中毕业', school: '加州理工学院附属中学', period: '2014 - 2018', gpa: '' },
|
||||
],
|
||||
/** 工作经验 */
|
||||
experience: [
|
||||
{
|
||||
title: '高级软件工程师',
|
||||
company: 'Google',
|
||||
period: '2022 - 至今',
|
||||
description: '负责开发和维护大规模分布式系统,优化系统性能和可靠性。',
|
||||
},
|
||||
{
|
||||
title: '软件工程师实习生',
|
||||
company: 'Microsoft',
|
||||
period: '2021 - 2022',
|
||||
description: '参与云服务平台的开发,协助团队完成多个核心功能模块。',
|
||||
},
|
||||
],
|
||||
/** 技能标签 */
|
||||
skills: ['JavaScript', 'Python', 'React', 'Node.js', 'SQL', 'AWS', 'Docker', 'Kubernetes'],
|
||||
/** 简历主表数据 */
|
||||
const resumeMain = ref<ResumeMainData>({})
|
||||
|
||||
/** 教育经历列表 */
|
||||
const educationList = ref<ResumeEducation[]>([])
|
||||
|
||||
/** 工作经历列表 */
|
||||
const workList = ref<ResumeWork[]>([])
|
||||
|
||||
/** 实习经历列表 */
|
||||
const internshipList = ref<ResumeInternship[]>([])
|
||||
|
||||
/** 项目经历列表 */
|
||||
const projectList = ref<ResumeProject[]>([])
|
||||
|
||||
/** 竞赛经历列表 */
|
||||
const competitionList = ref<ResumeCompetition[]>([])
|
||||
|
||||
// ==================== 数据加载 ====================
|
||||
|
||||
/** 加载所有简历详情数据 */
|
||||
async function loadResumeDetail() {
|
||||
const [mainRes, eduRes, workRes, internRes, projRes, compRes] = await Promise.all([
|
||||
fetchResumeMain(resumeId),
|
||||
fetchResumeEducation(resumeId),
|
||||
fetchResumeWork(resumeId),
|
||||
fetchResumeInternship(resumeId),
|
||||
fetchResumeProject(resumeId),
|
||||
fetchResumeCompetition(resumeId),
|
||||
])
|
||||
|
||||
if (mainRes.code === '0' && mainRes.data) {
|
||||
resumeMain.value = mainRes.data
|
||||
}
|
||||
if (eduRes.code === '0' && eduRes.data) {
|
||||
educationList.value = eduRes.data
|
||||
}
|
||||
if (workRes.code === '0' && workRes.data) {
|
||||
workList.value = workRes.data
|
||||
}
|
||||
if (internRes.code === '0' && internRes.data) {
|
||||
internshipList.value = internRes.data
|
||||
}
|
||||
if (projRes.code === '0' && projRes.data) {
|
||||
projectList.value = projRes.data
|
||||
}
|
||||
if (compRes.code === '0' && compRes.data) {
|
||||
competitionList.value = compRes.data
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 生命周期 ====================
|
||||
|
||||
onMounted(() => {
|
||||
loadResumeDetail()
|
||||
})
|
||||
|
||||
// ==================== 计算属性 ====================
|
||||
|
||||
/** 头像首字母 */
|
||||
function getAvatarLetter(): string {
|
||||
return resumeMain.value.name?.charAt(0) || resumeMain.value.resumeName?.charAt(0) || '?'
|
||||
}
|
||||
|
||||
// ==================== 事件处理 ====================
|
||||
|
||||
/** 返回简历列表页 */
|
||||
@@ -222,9 +575,209 @@ function handleViewReport() { console.log('查看评估报告') }
|
||||
/** 重新诊断简历 */
|
||||
function handleDiagnose() { console.log('重新诊断') }
|
||||
|
||||
/** 紧急修复项 */
|
||||
function handleUrgentFix() { console.log('紧急修复项') }
|
||||
/** 修复指定模块的问题,传入模块 key */
|
||||
function handleFixIssue(cardKey: CardKey) {
|
||||
const config = cardIssueConfig[cardKey]
|
||||
console.log(`修复模块: ${cardKey}, 问题类型: ${config.activeType}`)
|
||||
}
|
||||
|
||||
/** 修复简历问题 */
|
||||
function handlePolish() { console.log('修复') }
|
||||
// ==================== 编辑抽屉相关 ====================
|
||||
|
||||
/** 编辑抽屉的显示状态 */
|
||||
const showEditDrawer = ref(false)
|
||||
|
||||
/** 当前编辑的模块名称 */
|
||||
const editModule = ref('info')
|
||||
|
||||
/** 当前编辑模块的初始数据 */
|
||||
const editInitialData = ref<Record<string, any>>({})
|
||||
|
||||
/** 打开编辑抽屉 — 根据模块名设置初始数据 */
|
||||
function openEditDrawer(section: string) {
|
||||
editModule.value = section
|
||||
|
||||
if (section === 'info') {
|
||||
editInitialData.value = {
|
||||
name: resumeMain.value.name || '',
|
||||
email: resumeMain.value.email || '',
|
||||
phone: resumeMain.value.mobileNumber || '',
|
||||
location: '',
|
||||
wechat: resumeMain.value.wechatNumber || '',
|
||||
}
|
||||
} else if (section === 'summary') {
|
||||
editInitialData.value = {
|
||||
summary: resumeMain.value.summary || '',
|
||||
}
|
||||
} else if (section === 'education') {
|
||||
editInitialData.value = {
|
||||
education: educationList.value.map(edu => ({
|
||||
school: edu.school || '',
|
||||
major: edu.major || '',
|
||||
studyType: edu.studyType || '全日制',
|
||||
degree: edu.degree || '本科',
|
||||
startDate: edu.startDate || '',
|
||||
endDate: edu.endDate || '',
|
||||
description: (edu.description || []).map(d => ({ ...d })),
|
||||
})),
|
||||
}
|
||||
} else if (section === 'work') {
|
||||
editInitialData.value = {
|
||||
works: workList.value.map(w => ({
|
||||
companyName: w.companyName || '',
|
||||
position: w.position || '',
|
||||
startDate: w.startDate || '',
|
||||
endDate: w.endDate || '',
|
||||
description: (w.description || []).map(d => ({ ...d })),
|
||||
})),
|
||||
}
|
||||
} else if (section === 'internship') {
|
||||
editInitialData.value = {
|
||||
internships: internshipList.value.map(i => ({
|
||||
companyName: i.companyName || '',
|
||||
position: i.position || '',
|
||||
startDate: i.startDate || '',
|
||||
endDate: i.endDate || '',
|
||||
description: (i.description || []).map(d => ({ ...d })),
|
||||
})),
|
||||
}
|
||||
} else if (section === 'project') {
|
||||
editInitialData.value = {
|
||||
projects: projectList.value.map(p => ({
|
||||
projectName: p.projectName || '',
|
||||
companyName: p.companyName || '',
|
||||
role: p.role || '',
|
||||
startDate: p.startDate || '',
|
||||
endDate: p.endDate || '',
|
||||
description: (p.description || []).map(d => ({ ...d })),
|
||||
})),
|
||||
}
|
||||
} else if (section === 'competition') {
|
||||
editInitialData.value = {
|
||||
competitions: competitionList.value.map(c => ({
|
||||
competitionName: c.competitionName || '',
|
||||
award: c.award || '',
|
||||
awardDate: c.awardDate || '',
|
||||
description: (c.description || []).map(d => ({ ...d })),
|
||||
})),
|
||||
}
|
||||
} else if (section === 'portfolio') {
|
||||
editInitialData.value = {
|
||||
portfolioUrl: resumeMain.value.portfolioUrl || '',
|
||||
}
|
||||
} else if (section === 'skills') {
|
||||
editInitialData.value = {
|
||||
skills: [...(resumeMain.value.skills || [])],
|
||||
}
|
||||
} else if (section === 'certificate') {
|
||||
editInitialData.value = {
|
||||
certificates: [...(resumeMain.value.certificates || [])],
|
||||
}
|
||||
}
|
||||
|
||||
showEditDrawer.value = true
|
||||
}
|
||||
|
||||
/** 保存编辑数据 — 将抽屉返回的数据调用对应接口持久化,并更新本地数据 */
|
||||
async function handleSaveEdit(data: Record<string, any>) {
|
||||
try {
|
||||
if (editModule.value === 'info') {
|
||||
// 保存基本信息到简历主表
|
||||
await saveResumeMain({
|
||||
resumeId,
|
||||
name: data.name,
|
||||
email: data.email,
|
||||
mobileNumber: data.phone,
|
||||
city: data.locationName || data.location || '',
|
||||
wechatNumber: data.wechat,
|
||||
})
|
||||
resumeMain.value.name = data.name
|
||||
resumeMain.value.email = data.email
|
||||
resumeMain.value.mobileNumber = data.phone
|
||||
resumeMain.value.city = data.locationName || data.location || ''
|
||||
resumeMain.value.wechatNumber = data.wechat
|
||||
} else if (editModule.value === 'summary') {
|
||||
// 保存个人概述到简历主表
|
||||
await saveResumeMain({
|
||||
resumeId,
|
||||
summary: data.summary,
|
||||
})
|
||||
resumeMain.value.summary = data.summary
|
||||
} else if (editModule.value === 'education') {
|
||||
// 保存教育经历
|
||||
await saveResumeEducation(resumeId, data.education.map((edu: any) => ({
|
||||
school: edu.school,
|
||||
major: edu.major,
|
||||
degree: edu.degree,
|
||||
studyType: edu.studyType,
|
||||
startDate: edu.startDate,
|
||||
endDate: edu.endDate,
|
||||
description: edu.description,
|
||||
})))
|
||||
educationList.value = data.education.map((edu: any) => ({ ...edu, description: edu.description.map((d: any) => ({ ...d })) }))
|
||||
} else if (editModule.value === 'work') {
|
||||
// 保存工作经历
|
||||
await saveResumeWork(resumeId, data.works.map((w: any) => ({
|
||||
companyName: w.companyName,
|
||||
position: w.position,
|
||||
startDate: w.startDate,
|
||||
endDate: w.endDate,
|
||||
description: w.description,
|
||||
})))
|
||||
workList.value = data.works.map((w: any) => ({ ...w, description: w.description.map((d: any) => ({ ...d })) }))
|
||||
} else if (editModule.value === 'internship') {
|
||||
// 保存实习经历
|
||||
await saveResumeInternship(resumeId, data.internships.map((i: any) => ({
|
||||
companyName: i.companyName,
|
||||
position: i.position,
|
||||
startDate: i.startDate,
|
||||
endDate: i.endDate,
|
||||
description: i.description,
|
||||
})))
|
||||
internshipList.value = data.internships.map((i: any) => ({ ...i, description: i.description.map((d: any) => ({ ...d })) }))
|
||||
} else if (editModule.value === 'project') {
|
||||
// 保存项目经历
|
||||
await saveResumeProject(resumeId, data.projects.map((p: any) => ({
|
||||
projectName: p.projectName,
|
||||
companyName: p.companyName,
|
||||
role: p.role,
|
||||
startDate: p.startDate,
|
||||
endDate: p.endDate,
|
||||
description: p.description,
|
||||
})))
|
||||
projectList.value = data.projects.map((p: any) => ({ ...p, description: p.description.map((d: any) => ({ ...d })) }))
|
||||
} else if (editModule.value === 'competition') {
|
||||
// 保存竞赛经历
|
||||
await saveResumeCompetition(resumeId, data.competitions.map((c: any) => ({
|
||||
competitionName: c.competitionName,
|
||||
award: c.award,
|
||||
awardDate: c.awardDate,
|
||||
description: c.description,
|
||||
})))
|
||||
competitionList.value = data.competitions.map((c: any) => ({ ...c, description: c.description.map((d: any) => ({ ...d })) }))
|
||||
} else if (editModule.value === 'portfolio') {
|
||||
// 保存作品集链接到简历主表
|
||||
await saveResumeMain({
|
||||
resumeId,
|
||||
portfolioUrl: data.portfolioUrl,
|
||||
})
|
||||
resumeMain.value.portfolioUrl = data.portfolioUrl
|
||||
} else if (editModule.value === 'skills') {
|
||||
// 保存技能到简历主表
|
||||
await saveResumeMain({
|
||||
resumeId,
|
||||
skills: [...data.skills],
|
||||
})
|
||||
resumeMain.value.skills = [...data.skills]
|
||||
} else if (editModule.value === 'certificate') {
|
||||
// 保存证书到简历主表
|
||||
await saveResumeMain({
|
||||
resumeId,
|
||||
certificates: [...data.certificates],
|
||||
})
|
||||
resumeMain.value.certificates = [...data.certificates]
|
||||
}
|
||||
} catch {
|
||||
console.error('[ResumeDetail] 保存失败')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -40,6 +40,11 @@ export default defineConfig({
|
||||
target: 'http://127.0.0.1:8080',
|
||||
changeOrigin: true,
|
||||
},
|
||||
'/ai-api': {
|
||||
target: 'http://192.168.31.133:8000',
|
||||
changeOrigin: true,
|
||||
rewrite: (path) => path.replace(/^\/ai-api/, ''),
|
||||
},
|
||||
},
|
||||
},
|
||||
css: {
|
||||
|
||||
Reference in New Issue
Block a user