OWL公告教学模块提交

This commit is contained in:
夜未央
2025-09-16 22:51:16 +08:00
parent d8f122c01b
commit ebf16f83e6
13 changed files with 520 additions and 94 deletions
+1 -2
View File
@@ -1,3 +1,2 @@
# -*- coding: utf-8 -*-
from . import models
from . import controllers
from . import controllers
+5 -1
View File
@@ -7,7 +7,7 @@
"website": "#",
"category": "Yuthon/Yuthon Web Notice",
"version": "17.0.0.1",
"depends": ["base", "web","hr"],
"depends": ["base", "web"],
"data": [
"views/templates.xml",
],
@@ -26,6 +26,10 @@
"web/static/src/core/browser/browser.js",
"web/static/src/core/registry.js",
"web/static/src/core/assets.js",
# my files
"yuthon_web_notice/static/css/all.min.css",
"yuthon_web_notice/static/css/index.css",
"yuthon_web_notice/static/webfonts/fa-solid-900.woff2",
"yuthon_web_notice/static/src/**/*",
],
},
+1 -1
View File
@@ -1,3 +1,3 @@
# -*- coding: utf-8 -*-
from . import controllers
from . import noticeController
@@ -1,10 +0,0 @@
from odoo import http
from odoo.http import request, route
class OwlPlayground(http.Controller):
@http.route(['/web_notice'], type='http', auth='public')
def show_playground(self):
"""
Renders the owl playground page
"""
return request.render('yuthon_web_notice.Announcement')
@@ -0,0 +1,19 @@
from odoo import http
from odoo.http import request, route
class NoticeController(http.Controller):
@http.route(["/web_notice"], type="http", auth="public")
def show_playground(self):
"""
Renders the owl playground page
"""
return request.render("yuthon_web_notice.Announcement")
@http.route("/web_notice/employee/<int:employee_id>", type="json", auth="public")
def get_employee(self, employee_id):
employee = request.env["hr.employee"].sudo().browse(employee_id)
if not employee.exists():
return {}
fields = employee.fields_get().keys()
return employee.read(fields)[0]
-1
View File
@@ -1 +0,0 @@
from . import hr_employee
-16
View File
@@ -1,16 +0,0 @@
# -*- coding: utf-8 -*-
from odoo import models, fields
class HrEmployee(models.Model):
_inherit = "hr.employee"
def get_full_employee_info(self, employee_id):
"""返回员工完整信息(所有字段)"""
employee = self.browse(employee_id)
if not employee.exists():
return {}
# 读取所有字段
fields = employee.fields_get().keys() # 获取所有字段名
return employee.sudo().read(fields)[0] # 返回字典
File diff suppressed because one or more lines are too long
+271
View File
@@ -0,0 +1,271 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: 'Segoe UI', 'Microsoft YaHei', sans-serif;
}
body {
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
color: #333;
line-height: 1.6;
padding: 20px;
min-height: 100vh;
}
.container {
max-width: 1000px;
margin: 0 auto;
}
header {
text-align: center;
margin-bottom: 30px;
padding: 20px;
}
h1 {
color: #2c3e50;
font-size: 2.5rem;
margin-bottom: 10px;
position: relative;
display: inline-block;
}
h1:after {
content: '';
position: absolute;
bottom: -10px;
left: 50%;
transform: translateX(-50%);
width: 80px;
height: 4px;
background: #e67e22;
border-radius: 2px;
}
.employee-card {
background: white;
border-radius: 15px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
overflow: hidden;
margin-bottom: 30px;
}
.profile-header {
background: linear-gradient(to right, #3498db, #2c3e50);
color: white;
padding: 30px;
display: flex;
align-items: center;
position: relative;
}
.avatar {
width: 150px;
height: 150px;
border-radius: 50%;
border: 5px solid rgba(255, 255, 255, 0.3);
background-color: #ecf0f1;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
margin-right: 30px;
flex-shrink: 0;
}
.avatar i {
font-size: 70px;
color: #7f8c8d;
}
.basic-info h2 {
font-size: 2rem;
margin-bottom: 5px;
}
.position {
font-size: 1.2rem;
opacity: 0.9;
margin-bottom: 15px;
}
.contact-info {
display: flex;
flex-wrap: wrap;
gap: 15px;
margin-top: 10px;
}
.contact-item {
display: flex;
align-items: center;
font-size: 0.95rem;
}
.contact-item i {
margin-right: 8px;
color: #f1c40f;
}
.profile-content {
padding: 30px;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 30px;
}
@media (max-width: 768px) {
.profile-content {
grid-template-columns: 1fr;
}
.profile-header {
flex-direction: column;
text-align: center;
}
.avatar {
margin-right: 0;
margin-bottom: 20px;
}
}
.section {
margin-bottom: 25px;
}
.section-title {
font-size: 1.4rem;
color: #2c3e50;
border-bottom: 2px solid #e67e22;
padding-bottom: 8px;
margin-bottom: 15px;
display: flex;
align-items: center;
}
.section-title i {
margin-right: 10px;
color: #e67e22;
}
.info-grid {
display: grid;
grid-template-columns: 100px 1fr;
gap: 12px;
}
.info-label {
font-weight: 600;
color: #7f8c8d;
}
.skills-container {
display: grid;
gap: 12px;
}
.skill-item {
display: flex;
align-items: center;
}
.skill-name {
width: 120px;
font-weight: 500;
}
.skill-bar {
flex-grow: 1;
height: 10px;
background: #ecf0f1;
border-radius: 5px;
overflow: hidden;
}
.skill-level {
height: 100%;
background: #3498db;
border-radius: 5px;
}
.project-item {
margin-bottom: 15px;
padding-bottom: 15px;
border-bottom: 1px solid #eee;
}
.project-item:last-child {
border-bottom: none;
}
.project-title {
font-weight: 600;
color: #2c3e50;
margin-bottom: 5px;
}
.project-period {
color: #7f8c8d;
font-size: 0.9rem;
margin-bottom: 8px;
}
.project-description {
font-size: 0.95rem;
}
.badge {
display: inline-block;
background: #e74c3c;
color: white;
padding: 3px 8px;
border-radius: 4px;
font-size: 0.8rem;
margin-left: 8px;
}
footer {
text-align: center;
padding: 20px;
color: #7f8c8d;
font-size: 0.9rem;
}
/* 无数据提示样式 */
.no-data {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
padding: 50px 20px;
margin: 40px auto;
max-width: 600px;
border: 1px solid #f5c6cb;
border-radius: 12px;
background: #fff8f8;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
color: #721c24;
}
.no-data i {
font-size: 56px;
color: #e74c3c;
margin-bottom: 16px;
}
.no-data h2 {
margin: 0;
font-size: 22px;
font-weight: bold;
color: #c0392b;
}
.no-data p {
margin-top: 10px;
font-size: 15px;
color: #a94442;
}
+10 -16
View File
@@ -1,30 +1,24 @@
/** @odoo-module **/
import { Component, useState, onWillUnmount, } from "@odoo/owl";
import { Component, useState, onWillStart } from "@odoo/owl";
import { useService } from "@web/core/utils/hooks";
export class Announcement extends Component {
static template = "yuthon_web_notice.Announcement";
static props = ["*"];
setup() {
console.log(useService);
debugger;
this.rpc = useService("rpc");
this.employee = useState({});
this.id = this.props.id || "";
onWillUnmount(async () => {
if (this.id) {
await this.loadEmployee(this.id);
this.state = useState({
employeeId: this.props.id || null,
employee: {},
});
this.rpc = useService('rpc');
onWillStart(async () => {
if (this.state.employeeId) {
await this.loadEmployee(this.state.employeeId);
}
});
}
async loadEmployee(employeeId) {
try {
this.employee = await this.rpc({
model: "hr.employee",
method: "get_full_employee_info",
args: [parseInt(employeeId)],
});
this.state.employee = await this.rpc('/web_notice/employee/' + employeeId, {});;
} catch (err) {
console.error("获取员工信息失败:", err);
}
+193 -1
View File
@@ -1,6 +1,198 @@
<?xml version="1.0" encoding="UTF-8" ?>
<templates xml:space="preserve">
<t t-name="yuthon_web_notice.Announcement">
<h1>Hello OWL Web</h1>
<div class="container">
<t t-set="employee" t-value="state.employee"/>
<t t-if="(!employee || Object.keys(employee).length === 0) || !state.employeeId">
<div class="no-data">
<i class="fas fa-exclamation-triangle"></i>
<h2>未找到信息</h2>
<p>请检查您的链接是否正确,或稍后再试。</p>
</div>
</t>
<t t-else="">
<header>
<h1>
<i class="fas fa-users"></i> 员工信息中心
</h1>
<p>企业人力资源信息管理系统</p>
</header>
<div class="employee-card">
<div class="profile-header">
<div class="avatar">
<t t-if="state.employee.image_1920">
<img t-att-src="'data:image/png;base64,' + employee.image_1920"
style="max-width:150px; border-radius:8px;" />
</t>
</div>
<div class="basic-info">
<h2>
<t t-esc='employee.name'/>
</h2>
<div class="position">
<t t-esc="employee?.job_id[1]"/>
</div>
<div class="department">
<t t-esc="employee?.department_id[1]"/>
</div>
<div class="contact-info">
<div class="contact-item">
<i class="fas fa-envelope"></i>
<t t-esc="employee?.work_email"/>
</div>
<div class="contact-item">
<i class="fas fa-phone"></i>
<t t-esc="employee?.work_phone"/>
</div>
<div class="contact-item">
<i class="fas fa-map-marker-alt"></i>
<t t-esc="employee?.private_street"/>
</div>
</div>
</div>
</div>
<div class="profile-content">
<div class="left-column">
<div class="section">
<h3 class="section-title">
<i class="fas fa-info-circle"></i> 基本信息
</h3>
<div class="info-grid">
<div class="info-label">员工ID:</div>
<div>
<t t-esc="employee?.permit_no"/>
</div>
<div class="info-label">性别:</div>
<div>
<t t-esc="employee?.gender"/>
</div>
<div class="info-label">出生日期:</div>
<div>
<t t-esc="employee?.birthday"/>
</div>
<div class="info-label">入职日期:</div>
<div>2018年6月10日</div>
<div class="info-label">工作年限:</div>
<div>5年3个月</div>
<div class="info-label">政治面貌:</div>
<div>保密</div>
</div>
</div>
<div class="section">
<h3 class="section-title">
<i class="fas fa-graduation-cap"></i> 教育背景
</h3>
<div class="info-grid">
<div class="info-label">2010-2014:</div>
<div>
<strong>北京大学</strong>
<br/>
计算机科学与技术 · 本科
</div>
<div class="info-label">2014-2017:</div>
<div>
<strong>清华大学</strong>
<br/>
软件工程 · 硕士
</div>
</div>
</div>
<div class="section">
<h3 class="section-title">
<i class="fas fa-certificate"></i> 证书资质
</h3>
<ul style="padding-left: 20px;">
<li>前端开发高级工程师认证</li>
<li>项目管理专业人士(PMP)</li>
<li>AWS 解决方案架构师</li>
<li>英语六级证书(CET-6)</li>
</ul>
</div>
</div>
<div class="right-column">
<div class="section">
<h3 class="section-title">
<i class="fas fa-star"></i> 专业技能
</h3>
<div class="skills-container">
<div class="skill-item">
<div class="skill-name">HTML/CSS</div>
<div class="skill-bar">
<div class="skill-level" style="width: 95%"></div>
</div>
</div>
<div class="skill-item">
<div class="skill-name">JavaScript</div>
<div class="skill-bar">
<div class="skill-level" style="width: 90%"></div>
</div>
</div>
<div class="skill-item">
<div class="skill-name">React</div>
<div class="skill-bar">
<div class="skill-level" style="width: 88%"></div>
</div>
</div>
<div class="skill-item">
<div class="skill-name">Vue.js</div>
<div class="skill-bar">
<div class="skill-level" style="width: 85%"></div>
</div>
</div>
<div class="skill-item">
<div class="skill-name">Node.js</div>
<div class="skill-bar">
<div class="skill-level" style="width: 80%"></div>
</div>
</div>
</div>
</div>
<div class="section">
<h3 class="section-title">
<i class="fas fa-briefcase"></i> 项目经验
</h3>
<div class="project-item">
<div class="project-title">企业ERP系统重构
<span class="badge">进行中</span>
</div>
<div class="project-period">2022.03 - 至今</div>
<div class="project-description">
负责前端架构设计和核心模块开发,使用React+TypeScript技术栈,优化了系统性能40%
</div>
</div>
<div class="project-item">
<div class="project-title">移动端电商平台</div>
<div class="project-period">2021.05 - 2022.02</div>
<div class="project-description">
主导开发了公司旗舰电商平台的移动端应用,采用Vue.js+Vant框架,日活用户超过10万
</div>
</div>
<div class="project-item">
<div class="project-title">数据可视化平台</div>
<div class="project-period">2020.08 - 2021.04</div>
<div class="project-description">
基于ECharts开发的企业级数据可视化系统,支持实时数据展示和大屏展示
</div>
</div>
</div>
<div class="section">
<h3 class="section-title">
<i class="fas fa-heart"></i> 兴趣爱好
</h3>
<div>
<span class="badge">摄影</span>
<span class="badge">徒步旅行</span>
<span class="badge">开源贡献</span>
<span class="badge">技术博客写作</span>
</div>
</div>
</div>
</div>
</div>
<footer>
<p>© 2025 企业人力资源管理系统 | 最后更新: 2025-09-15</p>
</footer>
</t>
</div>
</t>
</templates>
+11 -46
View File
@@ -1,51 +1,16 @@
/** @odoo-module **/
import { browser } from "@web/core/browser/browser";
import { makeEnv } from "@web/env"; // 引入环境构建工具
import { mount, whenReady } from "@odoo/owl";
import { whenReady } from "@odoo/owl";
import { mountComponent } from "@web/env";
import { Announcement } from "./announcement";
import { templates } from "@web/core/assets";
const config = {
dev: true,
name: "Web Notice",
props: {
id: new URLSearchParams(window.location.search).get("id"),
}
};
// 当document.body准备好时,挂载 Announcement 组件
whenReady(() => {
const env = makeEnv(); // 创建标准 Odoo 环境
mount(Announcement, document.body, {
templates,
dev: true,
name: "Web Notice",
env,
props: {
id: new URLSearchParams(window.location.search).get("id"),
},
});
});
/**
* 错误日志捕获函数
* 遍历 error 对象的 cause 属性,输出错误堆栈信息(包含链式错误原因)
* @param {Event} ev 浏览器事件对象
*/
function logError(ev) {
ev.preventDefault();
let error = ev?.error || ev.reason;
if (error.seen) {
// 如果错误已经标记过 seen,就不再重复打印
// 场景:mount 失败时,Owl 会抛出错误,如果不处理就会打印两次
return;
}
error.seen = true;
let errorMessage = error.stack;
while (error.cause) {
errorMessage += "\nCaused by: "
errorMessage += error.cause.stack;
error = error.cause;
}
console.error(errorMessage);
}
// 监听全局错误事件(比如未捕获的异常)
browser.addEventListener("error", (ev) => { logError(ev) });
// 监听未捕获的 Promise 错误(unhandledrejection)
browser.addEventListener("unhandledrejection", (ev) => { logError(ev) });
mountComponent(Announcement, document.body, config);
});
Binary file not shown.