初始提交:实习工作日志系统(设计方案 + Vue3 应用 + Electron 打包)

This commit is contained in:
root
2026-10-15 23:55:27 +08:00
commit cfb984627d
50 changed files with 9223 additions and 0 deletions
@@ -0,0 +1,155 @@
<script setup lang="ts">
import { nextTick, onUnmounted, ref, watch } from 'vue'
import Icon from './Icon.vue'
const props = defineProps<{
open: boolean
title: string
initial: { system: string; account: string; password: string; remark: string }
}>()
const emit = defineEmits<{
(e: 'cancel'): void
(e: 'save', payload: { system: string; account: string; password: string; remark: string }): void
}>()
const fSystem = ref('')
const fAccount = ref('')
const fPassword = ref('')
const fRemark = ref('')
const fError = ref('')
const systemInput = ref<HTMLInputElement | null>(null)
function onKey(e: KeyboardEvent) {
if (!props.open) return
if (e.key === 'Escape') emit('cancel')
}
watch(() => props.open, async v => {
if (v) {
fSystem.value = props.initial.system
fAccount.value = props.initial.account
fPassword.value = props.initial.password
fRemark.value = props.initial.remark
fError.value = ''
window.addEventListener('keydown', onKey)
await nextTick()
systemInput.value?.focus()
} else {
window.removeEventListener('keydown', onKey)
}
})
onUnmounted(() => window.removeEventListener('keydown', onKey))
// 生成 15 位随机密码:大小写字母与数字为主体,末尾追加一个符号
function genPassword() {
const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz23456789'
const symbols = '!@#$%^&*'
const arr = crypto.getRandomValues(new Uint8Array(14))
let s = ''
for (let i = 0; i < arr.length; i++) s += chars[arr[i] % chars.length]
s += symbols[crypto.getRandomValues(new Uint8Array(1))[0] % symbols.length]
fPassword.value = s
fError.value = ''
}
function save() {
if (!fSystem.value.trim() || !fAccount.value.trim() || !fPassword.value) {
fError.value = '系统名称、账号、密码为必填项'
return
}
emit('save', {
system: fSystem.value.trim(),
account: fAccount.value.trim(),
password: fPassword.value,
remark: fRemark.value.trim()
})
}
</script>
<template>
<teleport to="body">
<transition name="modal">
<div v-if="open" class="mask" @click.self="emit('cancel')">
<div class="dialog" role="dialog" :aria-label="title">
<div class="dialog-head">
<span class="dialog-title">{{ title }}</span>
<button class="icon-btn" title="关闭" @click="emit('cancel')">
<Icon name="close" :size="16" />
</button>
</div>
<div class="dialog-body">
<div class="form-grid">
<div class="field span2">
<label>系统名称(必填)</label>
<input ref="systemInput" class="input" v-model="fSystem" placeholder="如:企业邮箱 / OA / VPN" />
</div>
<div class="field span2">
<label>账号(必填)</label>
<input class="input" v-model="fAccount" placeholder="用户名、工号或邮箱" />
</div>
<div class="field span2">
<label>密码(必填)</label>
<div class="pwd-row">
<input class="input" v-model="fPassword" placeholder="输入或点击生成" />
<button class="btn btn-sm" @click="genPassword">生成</button>
</div>
</div>
<div class="field span2">
<label>备注</label>
<textarea class="textarea" v-model="fRemark" placeholder="用途、到期时间、负责人等(选填)"></textarea>
</div>
</div>
<div v-if="fError" class="form-error">{{ fError }}</div>
</div>
<div class="dialog-btns">
<button class="btn btn-sm" @click="emit('cancel')">取消</button>
<button class="btn btn-primary btn-sm" @click="save">保存</button>
</div>
</div>
</div>
</transition>
</teleport>
</template>
<style scoped>
.mask {
position: fixed;
inset: 0;
background: rgba(29, 33, 41, 0.4);
display: flex;
align-items: center;
justify-content: center;
z-index: 90;
}
.dialog {
width: 480px;
max-width: calc(100vw - 32px);
background: var(--bg-page);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-float);
}
.dialog-head {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 20px 12px;
border-bottom: 1px solid var(--border);
}
.dialog-title { font-size: 14px; font-weight: 600; color: var(--text-1); }
.dialog-body { padding: 16px 20px; }
.dialog-btns {
display: flex;
justify-content: flex-end;
gap: 8px;
padding: 12px 20px 16px;
border-top: 1px solid var(--border);
}
.form-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px 16px; }
.field.span2 { grid-column: span 2; }
.field label { display: block; font-size: 12px; color: var(--text-2); margin-bottom: 4px; }
.pwd-row { display: flex; gap: 8px; }
.pwd-row .input { flex: 1; }
.form-error { color: var(--danger); font-size: 12px; margin-top: 8px; }
.modal-enter-active, .modal-leave-active { transition: opacity 0.15s ease; }
.modal-enter-from, .modal-leave-to { opacity: 0; }
</style>