mirror of
https://github.com/geekgeekrun/geekgeekrun.git
synced 2026-05-16 08:37:34 +08:00
add ui for resume editor
This commit is contained in:
@@ -71,8 +71,12 @@ export const readConfigFile = (fileName) => {
|
||||
)
|
||||
} catch {
|
||||
fs.existsSync(joinedPath) && fs.unlinkSync(joinedPath)
|
||||
ensureConfigFileExist()
|
||||
o = JSON.parse(defaultConfigFileContentMap[fileName])
|
||||
if (defaultConfigFileContentMap[fileName]) {
|
||||
ensureConfigFileExist()
|
||||
o = JSON.parse(defaultConfigFileContentMap[fileName])
|
||||
} else {
|
||||
o = null
|
||||
}
|
||||
}
|
||||
|
||||
return o
|
||||
|
||||
@@ -31,6 +31,7 @@ import { WriteStream } from 'node:fs'
|
||||
// eslint-disable-next-line vue/prefer-import-from-vue
|
||||
import { hasOwn } from '@vue/shared'
|
||||
import { createLlmConfigWindow, llmConfigWindow } from '../../../window/llmConfigWindow'
|
||||
import { createResumeEditorWindow, resumeEditorWindow } from '../../../window/resumeEditorWindow'
|
||||
|
||||
export default function initIpc() {
|
||||
ipcMain.on('open-external-link', (_, link) => {
|
||||
@@ -462,6 +463,39 @@ export default function initIpc() {
|
||||
})
|
||||
ipcMain.on('close-llm-config', () => llmConfigWindow?.close())
|
||||
|
||||
ipcMain.handle('resume-edit', async () => {
|
||||
createResumeEditorWindow({
|
||||
parent: mainWindow!,
|
||||
modal: true,
|
||||
show: true
|
||||
})
|
||||
const defer = Promise.withResolvers()
|
||||
async function saveResumeHandler(_, resumeContent) {
|
||||
await writeConfigFile('resumes.json', [
|
||||
{
|
||||
name: '默认简历',
|
||||
updateTime: Number(new Date()),
|
||||
content: resumeContent
|
||||
}
|
||||
])
|
||||
defer.resolve()
|
||||
resumeEditorWindow?.close()
|
||||
}
|
||||
ipcMain.handle('save-resume-content', saveResumeHandler)
|
||||
resumeEditorWindow?.once('closed', () => {
|
||||
ipcMain.removeHandler('save-resume-content')
|
||||
ipcMain.removeHandler('fetch-resume-content')
|
||||
defer.reject(new Error('cancel'))
|
||||
})
|
||||
|
||||
ipcMain.handle('fetch-resume-content', async () => {
|
||||
const res = (await readConfigFile('resumes.json'))?.[0]
|
||||
return res?.content ?? null
|
||||
})
|
||||
return defer.promise
|
||||
})
|
||||
ipcMain.on('close-resume-editor', () => resumeEditorWindow?.close())
|
||||
|
||||
ipcMain.handle('exit-app-immediately', () => {
|
||||
app.exit(0)
|
||||
})
|
||||
|
||||
45
packages/ui/src/main/window/resumeEditorWindow.ts
Normal file
45
packages/ui/src/main/window/resumeEditorWindow.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { BrowserWindow, ipcMain } from 'electron'
|
||||
import path from 'path'
|
||||
|
||||
export let resumeEditorWindow: BrowserWindow | null = null
|
||||
export function createResumeEditorWindow(
|
||||
opt?: Electron.BrowserWindowConstructorOptions
|
||||
): BrowserWindow {
|
||||
// Create the browser window.
|
||||
if (resumeEditorWindow) {
|
||||
resumeEditorWindow!.show()
|
||||
}
|
||||
resumeEditorWindow = new BrowserWindow({
|
||||
width: 960,
|
||||
height: 720,
|
||||
resizable: true,
|
||||
show: false,
|
||||
autoHideMenuBar: true,
|
||||
// frame: false,
|
||||
webPreferences: {
|
||||
preload: path.join(__dirname, '../preload/index.js'),
|
||||
sandbox: false
|
||||
},
|
||||
...opt
|
||||
})
|
||||
|
||||
resumeEditorWindow.on('ready-to-show', () => {
|
||||
resumeEditorWindow!.show()
|
||||
})
|
||||
|
||||
// HMR for renderer base on electron-vite cli.
|
||||
// Load the remote URL for development or the local html file for production.
|
||||
if (process.env.NODE_ENV === 'development' && process.env['ELECTRON_RENDERER_URL']) {
|
||||
resumeEditorWindow.loadURL(process.env['ELECTRON_RENDERER_URL'] + '#/resumeEditor')
|
||||
} else {
|
||||
resumeEditorWindow.loadURL(
|
||||
'file://' + path.join(__dirname, '../renderer/index.html') + '#/resumeEditor'
|
||||
)
|
||||
}
|
||||
|
||||
resumeEditorWindow!.once('closed', () => {
|
||||
resumeEditorWindow = null
|
||||
})
|
||||
|
||||
return resumeEditorWindow!
|
||||
}
|
||||
@@ -53,6 +53,13 @@
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<div class="flex flex-items-center">
|
||||
<el-button size="small" type="primary" @click="handleClickEditResume">
|
||||
编辑简历
|
||||
</el-button>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item prop="recentMessageQuantityForLlm">
|
||||
<div>
|
||||
携带最近
|
||||
@@ -227,6 +234,14 @@ const handleClickConfigLlm = async () => {
|
||||
console.log(err)
|
||||
}
|
||||
}
|
||||
|
||||
const handleClickEditResume = async () => {
|
||||
try {
|
||||
await electron.ipcRenderer.invoke('resume-edit')
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
460
packages/ui/src/renderer/src/page/ResumeEditor/index.vue
Normal file
460
packages/ui/src/renderer/src/page/ResumeEditor/index.vue
Normal file
@@ -0,0 +1,460 @@
|
||||
<template>
|
||||
<div class="resume-editor-page">
|
||||
<div class="main-wrapper">
|
||||
<main>
|
||||
<div class="mt1em mb1em flex flex-items-center flex-justify-between">
|
||||
<span>简历编辑器</span>
|
||||
</div>
|
||||
<el-form
|
||||
ref="formRef"
|
||||
:model="formContent"
|
||||
:rules="formRules"
|
||||
label-position="top"
|
||||
class="resume-editor-form"
|
||||
>
|
||||
<div
|
||||
:style="{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: '1fr 1fr 1fr',
|
||||
gap: '10px'
|
||||
}"
|
||||
>
|
||||
<el-form-item prop="providerCompleteApiUrl" label="姓名">
|
||||
<el-input v-model="formContent.name" font-size-12px></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item prop="providerCompleteApiUrl" label="性别">
|
||||
<el-input v-model="formContent.gender" font-size-12px></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item prop="providerCompleteApiUrl" label="年龄">
|
||||
<el-input-number
|
||||
v-model="formContent.age"
|
||||
w-full
|
||||
font-size-12px
|
||||
:min="0"
|
||||
:max="200"
|
||||
:precision="0"
|
||||
:step="1"
|
||||
></el-input-number>
|
||||
</el-form-item>
|
||||
<el-form-item prop="providerCompleteApiUrl" label="学历">
|
||||
<el-input v-model="formContent.degree" font-size-12px></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item prop="providerCompleteApiUrl" label="工作年限">
|
||||
<el-input v-model="formContent.workYearDesc" font-size-12px></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item prop="providerCompleteApiUrl" label="期望职位">
|
||||
<el-input v-model="formContent.expectJob" font-size-12px></el-input>
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<el-form-item prop="providerCompleteApiUrl" label="个人优势">
|
||||
<el-input
|
||||
v-model="formContent.userDescription"
|
||||
type="textarea"
|
||||
:autosize="{
|
||||
minRows: 6,
|
||||
maxRows: 8
|
||||
}"
|
||||
font-size-12px
|
||||
></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<div class="el-form-item__label">
|
||||
工作经历
|
||||
<el-button size="small" :icon="Plus" @click="addWorkExp">新增一条</el-button>
|
||||
</div>
|
||||
<div v-for="(exp, index) in formContent.geekWorkExpList" :key="index">
|
||||
<div
|
||||
:style="{
|
||||
display: 'flex',
|
||||
gap: '12px'
|
||||
}"
|
||||
>
|
||||
<div
|
||||
:style="{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: '10px',
|
||||
height: 'fit-content',
|
||||
marginTop: '10px'
|
||||
}"
|
||||
>
|
||||
<el-button
|
||||
:disabled="index <= 0"
|
||||
style="margin: 0"
|
||||
circle
|
||||
size="small"
|
||||
:icon="ArrowUp"
|
||||
@click="moveWorkExpUp(index)"
|
||||
/>
|
||||
<el-button
|
||||
:disabled="index >= formContent.geekWorkExpList.length - 1"
|
||||
style="margin: 0"
|
||||
circle
|
||||
size="small"
|
||||
:icon="ArrowDown"
|
||||
@click="moveWorkExpDown(index)"
|
||||
/>
|
||||
<el-button
|
||||
:disabled="1 >= formContent.geekWorkExpList.length"
|
||||
style="margin: 0"
|
||||
circle
|
||||
size="small"
|
||||
:icon="Delete"
|
||||
@click="removeWorkExp(index)"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
:style="{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: '1fr 1.25fr 1fr',
|
||||
gap: '10px',
|
||||
width: '100%'
|
||||
}"
|
||||
>
|
||||
<el-form-item label="公司名称" style="margin-bottom: 18px">
|
||||
<el-input v-model="exp.company" />
|
||||
</el-form-item>
|
||||
<el-form-item label="任职时间" style="margin-bottom: 18px">
|
||||
<div
|
||||
:style="{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: '1fr 1fr'
|
||||
}"
|
||||
>
|
||||
<el-date-picker
|
||||
v-model="exp.startYearMon"
|
||||
:style="{ '--el-date-editor-width': 'auto' }"
|
||||
type="month"
|
||||
placeholder="开始月份"
|
||||
/>
|
||||
<el-date-picker
|
||||
v-model="exp.endYearMon"
|
||||
:style="{ '--el-date-editor-width': 'auto' }"
|
||||
type="month"
|
||||
placeholder="结束月份"
|
||||
/>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="职务" style="margin-bottom: 18px">
|
||||
<el-input v-model="exp.positionName" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
<el-form-item label="工作业绩">
|
||||
<el-input
|
||||
v-model="exp.performance"
|
||||
type="textarea"
|
||||
:autosize="{
|
||||
minRows: 6,
|
||||
maxRows: 8
|
||||
}"
|
||||
font-size-12px
|
||||
/>
|
||||
</el-form-item>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="index !== formContent.geekWorkExpList.length - 1"
|
||||
class="mt20px mb20px h1px"
|
||||
style="background-color: #dcdcdc"
|
||||
/>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<div class="el-form-item__label">
|
||||
项目经历
|
||||
<el-button size="small" :icon="Plus" @click="addProjExp">新增一条</el-button>
|
||||
</div>
|
||||
<div v-for="(proj, index) in formContent.geekProjExpList" :key="index">
|
||||
<div
|
||||
:style="{
|
||||
display: 'flex',
|
||||
gap: '12px'
|
||||
}"
|
||||
>
|
||||
<div
|
||||
:style="{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: '10px',
|
||||
height: 'fit-content',
|
||||
marginTop: '10px'
|
||||
}"
|
||||
>
|
||||
<el-button
|
||||
:disabled="index <= 0"
|
||||
style="margin: 0"
|
||||
circle
|
||||
size="small"
|
||||
:icon="ArrowUp"
|
||||
@click="moveProjExpUp(index)"
|
||||
/>
|
||||
<el-button
|
||||
:disabled="index >= formContent.geekProjExpList.length - 1"
|
||||
style="margin: 0"
|
||||
circle
|
||||
size="small"
|
||||
:icon="ArrowDown"
|
||||
@click="moveProjExpDown(index)"
|
||||
/>
|
||||
<el-button
|
||||
:disabled="1 >= formContent.geekProjExpList.length"
|
||||
style="margin: 0"
|
||||
circle
|
||||
size="small"
|
||||
:icon="Delete"
|
||||
@click="removeProjExp(index)"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
:style="{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: '1fr 1fr 1.25fr',
|
||||
gap: '10px',
|
||||
width: '100%'
|
||||
}"
|
||||
>
|
||||
<el-form-item label="项目名称" style="margin-bottom: 18px">
|
||||
<el-input v-model="proj.name" />
|
||||
</el-form-item>
|
||||
<el-form-item label="项目角色" style="margin-bottom: 18px">
|
||||
<el-input v-model="proj.roleName" />
|
||||
</el-form-item>
|
||||
<el-form-item label="项目时间" style="margin-bottom: 18px">
|
||||
<div
|
||||
:style="{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: '1fr 1fr'
|
||||
}"
|
||||
>
|
||||
<el-date-picker
|
||||
v-model="proj.startYearMon"
|
||||
:style="{ '--el-date-editor-width': 'auto' }"
|
||||
type="month"
|
||||
placeholder="开始月份"
|
||||
/>
|
||||
<el-date-picker
|
||||
v-model="proj.endYearMon"
|
||||
:style="{ '--el-date-editor-width': 'auto' }"
|
||||
type="month"
|
||||
placeholder="结束月份"
|
||||
/>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</div>
|
||||
<el-form-item label="项目业绩">
|
||||
<el-input
|
||||
v-model="proj.performance"
|
||||
type="textarea"
|
||||
:autosize="{
|
||||
minRows: 6,
|
||||
maxRows: 8
|
||||
}"
|
||||
font-size-12px
|
||||
/>
|
||||
</el-form-item>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="index !== formContent.geekProjExpList.length - 1"
|
||||
class="mt20px mb20px h1px"
|
||||
style="background-color: #dcdcdc"
|
||||
/>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</main>
|
||||
</div>
|
||||
<footer flex pt10px pb10px pr20px flex-justify-between>
|
||||
<div>
|
||||
<!-- <el-button type="text" @click="handleTestAvailability">测试可用性</el-button> -->
|
||||
</div>
|
||||
<div>
|
||||
<el-button @click="handleCancel">取消</el-button>
|
||||
<el-button type="primary" @click="handleSubmit">确定</el-button>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ElForm, ElButton } from 'element-plus'
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { ArrowUp, ArrowDown, Delete, Plus } from '@element-plus/icons-vue'
|
||||
|
||||
interface ResumeContent {
|
||||
name: string
|
||||
gender: string
|
||||
age: string
|
||||
degree: string
|
||||
workYearDesc: string
|
||||
expectJob: string
|
||||
userDescription: string
|
||||
geekWorkExpList: Array<{
|
||||
company: string
|
||||
positionName: string
|
||||
startYearMon: string | null
|
||||
endYearMon: string | null
|
||||
performance: string
|
||||
}>
|
||||
geekProjExpList: Array<{
|
||||
name: string
|
||||
startYearMon: string
|
||||
endYearMon: string
|
||||
roleName: string
|
||||
projectDescription: string
|
||||
performance: string
|
||||
}>
|
||||
}
|
||||
|
||||
const formRef = ref<InstanceType<typeof ElForm>>()
|
||||
|
||||
const getEmptyFormContent = () => {
|
||||
const o: any = {
|
||||
age: '',
|
||||
degree: '',
|
||||
expectJob: '',
|
||||
gender: '',
|
||||
name: '',
|
||||
userDescription: '',
|
||||
workYearDesc: '',
|
||||
geekWorkExpList: [],
|
||||
geekProjExpList: []
|
||||
}
|
||||
o.geekProjExpList = [getNewProjExpItem()]
|
||||
o.geekWorkExpList = [getNewWorkExpItem()]
|
||||
|
||||
return o as ResumeContent
|
||||
}
|
||||
const formContent = ref<ResumeContent>(getEmptyFormContent())
|
||||
|
||||
const formRules = {}
|
||||
|
||||
const handleCancel = () => {
|
||||
electron.ipcRenderer.send('close-resume-editor')
|
||||
}
|
||||
const handleSubmit = async () => {
|
||||
electron.ipcRenderer.invoke('save-resume-content', JSON.parse(JSON.stringify(formContent.value)))
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const savedFileContent = await electron.ipcRenderer.invoke('fetch-resume-content')
|
||||
if (!savedFileContent) {
|
||||
return
|
||||
}
|
||||
for (const k of Object.keys(formContent.value)) {
|
||||
formContent.value[k] = savedFileContent[k]
|
||||
}
|
||||
if (!formContent.value.geekProjExpList?.length) {
|
||||
formContent.value.geekProjExpList = [getNewProjExpItem()]
|
||||
}
|
||||
if (!formContent.value.geekWorkExpList?.length) {
|
||||
formContent.value.geekWorkExpList = [getNewWorkExpItem()]
|
||||
}
|
||||
} catch (err) {
|
||||
formContent.value = getEmptyFormContent()
|
||||
}
|
||||
})
|
||||
|
||||
// function handlePresetClick(selected: (typeof llmPresetList)[number]) {}
|
||||
|
||||
// #region edit work exp list
|
||||
function getNewWorkExpItem() {
|
||||
return {
|
||||
company: '',
|
||||
endYearMon: '',
|
||||
positionName: '',
|
||||
startYearMon: '',
|
||||
performance: ''
|
||||
}
|
||||
}
|
||||
function addWorkExp() {
|
||||
formContent.value.geekWorkExpList.push(getNewWorkExpItem())
|
||||
}
|
||||
function moveWorkExpUp(index) {
|
||||
;[formContent.value.geekWorkExpList[index], formContent.value.geekWorkExpList[index - 1]] = [
|
||||
formContent.value.geekWorkExpList[index - 1],
|
||||
formContent.value.geekWorkExpList[index]
|
||||
]
|
||||
}
|
||||
|
||||
function moveWorkExpDown(index) {
|
||||
;[formContent.value.geekWorkExpList[index], formContent.value.geekWorkExpList[index + 1]] = [
|
||||
formContent.value.geekWorkExpList[index + 1],
|
||||
formContent.value.geekWorkExpList[index]
|
||||
]
|
||||
}
|
||||
|
||||
function removeWorkExp(index) {
|
||||
formContent.value.geekWorkExpList.splice(index, 1)
|
||||
}
|
||||
// #endregion
|
||||
|
||||
// #region edit proj list
|
||||
function getNewProjExpItem() {
|
||||
return {
|
||||
name: '',
|
||||
endYearMon: '',
|
||||
roleName: '',
|
||||
startYearMon: '',
|
||||
performance: '',
|
||||
projectDescription: ''
|
||||
}
|
||||
}
|
||||
function addProjExp() {
|
||||
formContent.value.geekProjExpList.push(getNewProjExpItem())
|
||||
}
|
||||
function moveProjExpUp(index) {
|
||||
;[formContent.value.geekProjExpList[index], formContent.value.geekProjExpList[index - 1]] = [
|
||||
formContent.value.geekProjExpList[index - 1],
|
||||
formContent.value.geekProjExpList[index]
|
||||
]
|
||||
}
|
||||
|
||||
function moveProjExpDown(index) {
|
||||
;[formContent.value.geekProjExpList[index], formContent.value.geekProjExpList[index + 1]] = [
|
||||
formContent.value.geekProjExpList[index + 1],
|
||||
formContent.value.geekProjExpList[index]
|
||||
]
|
||||
}
|
||||
|
||||
function removeProjExp(index) {
|
||||
formContent.value.geekProjExpList.splice(index, 1)
|
||||
}
|
||||
// #endregion
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.resume-editor-page {
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
.main-wrapper {
|
||||
overflow: auto;
|
||||
main {
|
||||
margin: 0 auto;
|
||||
max-width: 768px;
|
||||
}
|
||||
}
|
||||
footer {
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
.resume-editor-form.el-form {
|
||||
.el-form-item__error--inline {
|
||||
margin-left: 0;
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -23,6 +23,13 @@ const routes: Array<RouteRecordRaw> = [
|
||||
title: '大语言模型设置'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/resumeEditor',
|
||||
component: () => import('@renderer/page/ResumeEditor/index.vue'),
|
||||
meta: {
|
||||
title: '简历编辑'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/main-layout',
|
||||
component: () => import('@renderer/page/MainLayout/index.vue'),
|
||||
|
||||
Reference in New Issue
Block a user