OWL公告教学模块提交
This commit is contained in:
@@ -1,3 +1,2 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from . import models
|
||||
from . import controllers
|
||||
from . import controllers
|
||||
@@ -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,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 +0,0 @@
|
||||
from . import hr_employee
|
||||
@@ -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] # 返回字典
|
||||
+9
File diff suppressed because one or more lines are too long
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
@@ -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.
Reference in New Issue
Block a user