add ui for resume editor

This commit is contained in:
geekgeekrun
2025-04-10 02:39:34 +08:00
parent 27bc63f63f
commit c40fbde5b2
6 changed files with 567 additions and 2 deletions

View File

@@ -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

View File

@@ -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)
})

View 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!
}

View File

@@ -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">

View 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>

View File

@@ -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'),