Odoo 17系统水印功能,让数据安全透明!
在 Odoo 开发中,Owl 框架为我们提供了强大的组件化开发能力。今天,我们就来深入解析一个基于 Owl 的水印组件开发案例,看看它是如何实现的。
一、代码解析
(一)组件定义
class Watermark extends Component {
static template = xml`
<div class="watermark_container">
<t t-foreach="watermarks" t-as="wm" t-key="wm.id">
<div class="watermark" t-att-style="wm.style">
<div><t t-esc="state.name"/> </div>
<div><t t-esc="state.currentTime"/></div>
</div>
</t>
</div>
`;
- 这里定义了一个名为
Watermark
的 Owl 组件。 - 使用了
xml
模板语法来构建组件的 HTML 结构。 t-foreach
指令用于循环渲染watermarks
数组中的每个水印对象。t-att-style
动态绑定每个水印的样式,由wm.style
提供。t-esc
用于输出state.name
和state.currentTime
的值,分别显示用户名和当前时间。
(二)组件状态初始化与数据获取
setup() {
this.orm = useService("orm");
this.state = useState({
name: "加载中...",
currentTime: this.getFormattedTime(),
});
this.watermarks = useState(this.generateWatermarks());
setup
方法是组件的初始化方法。- 使用
useService
获取了orm
服务,虽然在这段代码中并未使用到orm
,但可能是为后续扩展预留。 - 使用
useState
初始化了两个状态:state
:包含用户名和当前时间。watermarks
:包含水印的布局信息。
state.name
初始值为 "加载中...",state.currentTime
初始值通过调用this.getFormattedTime()
获取当前时间。
(三)时间更新与窗口大小变化监听
setInterval(() => {
this.state.currentTime = this.getFormattedTime();
}, 60000);
- 使用
setInterval
每分钟更新一次state.currentTime
,调用this.getFormattedTime()
方法获取格式化的时间。
onMounted(() => {
window.addEventListener("resize", () => {
this.watermarks.splice(0, this.watermarks.length, ...this.generateWatermarks());
});
});
- 在组件挂载后,监听窗口大小变化事件。
- 当窗口大小发生变化时,调用
this.generateWatermarks()
方法重新生成水印布局,并更新watermarks
状态。
(四)用户信息获取
onMounted(async () => {
try {
const userId = session.uid;
console.log("当前用户 ID:", userId);
const userName = session.name;
console.log("当前用户名:", userName);
this.state.name = userName;
} catch (error) {
console.error("❌ 获取用户数据失败:", error);
this.state.name = "错误";
}
});
- 在组件挂载后,异步获取当前用户的信息。
- 从
session
中获取用户 ID 和用户名。 - 如果获取成功,将用户名赋值给
state.name
;如果失败,捕获错误并设置state.name
为 "错误"。
(五)时间格式化方法
getFormattedTime() {
const now = new Date();
return `${now.getFullYear()}年${now.getMonth() + 1}月${now.getDate()}日 ${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')}`;
}
- 这是一个辅助方法,用于获取格式化的时间字符串。
- 使用
Date
对象获取当前日期和时间,然后按照 "年月日 时:分" 的格式拼接字符串。 - 使用
padStart
方法确保小时和分钟始终为两位数字。
(六)水印布局生成方法
generateWatermarks() {
const watermarks = [];
const screenWidth = window.innerWidth;
const screenHeight = window.innerHeight;
const spacingX = 200; // 水平间距
const spacingY = 150; // 垂直间距
const cols = Math.ceil(screenWidth / spacingX);
const rows = Math.ceil(screenHeight / spacingY);
for (let row = 0; row < rows; row++) {
for (let col = 0; col < cols; col++) {
const x = col * spacingX + (row % 2) * (spacingX / 2);
const y = row * spacingY;
watermarks.push({
id: `${row}-${col}`,
style: `
position: absolute;
left: ${x}px;
top: ${y}px;
transform: rotate(-30deg);
transform-origin: center;
`,
});
}
}
return watermarks;
}
- 这个方法用于生成水印的布局信息。
- 根据窗口的宽度和高度,以及设定的水平和垂直间距,计算出需要的行列数。
- 使用嵌套循环生成每个水印的位置和样式。
- 水印采用交错排列的方式,通过
(row % 2) * (spacingX / 2)
实现。 - 每个水印的样式包括绝对定位、位置坐标、旋转角度等。
(七)组件挂载到 WebClient
patch(WebClient.prototype, {
async setup() {
await super.setup(...arguments);
const container = document.createElement("div");
container.classList.add("o_watermark_container");
const root = document.querySelector(".o_web_client");
if (root) {
root.appendChild(container);
await mount(Watermark, container, {
props: {},
env: this.env,
});
console.log("✅ 水印已挂载。");
} else {
console.warn("⚠️ 未找到 .o_web_client 元素");
}
},
});
- 使用
patch
方法扩展了WebClient
。 - 在
WebClient
的setup
方法中,创建了一个容器元素,并将其添加到.o_web_client
元素中。 - 使用
mount
方法将Watermark
组件挂载到容器中。
(八)样式添加
const style = document.createElement("style");
style.textContent = `
.o_watermark_container {
position: absolute;
inset: 0;
pointer-events: none;
z-index: 9999;
}
.watermark {
opacity: 0.3;
color: #333;
font-size: 14px;
padding: 5px 10px;
border-radius: 3px;
white-space: nowrap;
}
.watermark > div {
line-height: 1.5;
}
`;
document.head.appendChild(style);
- 动态创建了一个
<style>
标签,并添加到页面的<head>
中。 - 定义了水印容器和水印的样式,包括透明度、颜色、字体大小、内边距、圆角等。
二、可优化项
(一)性能优化
- 减少 DOM 操作:在窗口大小变化时,直接更新水印的位置和样式,而不是重新生成整个水印数组并替换。可以通过直接修改每个水印元素的样式属性来实现,这样可以减少 DOM 操作的开销。
- 优化时间更新逻辑:目前每分钟更新一次时间,但如果用户长时间停留在页面上,可能会导致时间显示不够精确。可以考虑每秒更新一次时间,但为了避免频繁更新 DOM,可以只在时间的分钟部分发生变化时才更新 DOM。
(二)功能增强
- 支持自定义水印内容:目前水印内容固定为用户名和当前时间,可以扩展为支持自定义水印内容,例如通过配置项或从后端获取水印模板。
- 水印样式动态配置:将水印的样式(如字体大小、颜色、透明度、旋转角度等)提取为可配置项,方便用户根据需求进行调整。
(三)完整代码
/** @odoo-module **/
import { Component, useState, onMounted, mount, xml } from "@odoo/owl";
import { patch } from "@web/core/utils/patch";
import { WebClient } from "@web/webclient/webclient";
import { session } from "@web/session";
import { useService } from "@web/core/utils/hooks";
// 水印组件
class Watermark extends Component {
static template = xml`
<div class="watermark_container">
<t t-foreach="watermarks" t-as="wm" t-key="wm.id">
<div class="watermark" t-att-style="wm.style">
<div><t t-esc="state.name"/> </div>
<div><t t-esc="state.currentTime"/></div>
</div>
</t>
</div>
`;
setup() {
this.orm = useService("orm");
this.state = useState({
name: "加载中...",
currentTime: this.getFormattedTime(),
});
this.watermarks = useState(this.generateWatermarks());
// 每分钟更新时间
setInterval(() => {
this.state.currentTime = this.getFormattedTime();
}, 60000);
// 监听窗口大小变化,重新生成水印
onMounted(() => {
window.addEventListener("resize", () => {
this.watermarks.splice(0, this.watermarks.length, ...this.generateWatermarks());
});
});
onMounted(async () => {
try {
// 获取当前用户 ID
const userId = session.uid;
console.log("当前用户 ID:", userId);
// 获取其他用户信息(如用户名)
const userName = session.name;
console.log("当前用户名:", userName);
// 设置用户的名称
this.state.name = userName ;
} catch (error) {
console.error("❌ 获取用户数据失败:", error);
this.state.name = "错误";
}
});
}
// 获取格式化的当前时间(年月日 时:分)
getFormattedTime() {
const now = new Date();
return `${now.getFullYear()}年${now.getMonth() + 1}月${now.getDate()}日 ${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')}`;
}
// 生成水印位置和样式
generateWatermarks() {
const watermarks = [];
const screenWidth = window.innerWidth;
const screenHeight = window.innerHeight;
const spacingX = 200; // 水平间距
const spacingY = 150; // 垂直间距
// 计算需要的行列数
const cols = Math.ceil(screenWidth / spacingX);
const rows = Math.ceil(screenHeight / spacingY);
for (let row = 0; row < rows; row++) {
for (let col = 0; col < cols; col++) {
const x = col * spacingX + (row % 2) * (spacingX / 2); // 交错排列
const y = row * spacingY;
watermarks.push({
id: `${row}-${col}`,
style: `
position: absolute;
left: ${x}px;
top: ${y}px;
transform: rotate(-30deg);
transform-origin: center;
`,
});
}
}
return watermarks;
}
}
// 扩展 WebClient
patch(WebClient.prototype, {
async setup() {
await super.setup(...arguments);
const container = document.createElement("div");
container.classList.add("o_watermark_container");
const root = document.querySelector(".o_web_client");
if (root) {
root.appendChild(container);
await mount(Watermark, container, {
props: {},
env: this.env,
});
console.log("✅ 水印已挂载。");
} else {
console.warn("⚠️ 未找到 .o_web_client 元素");
}
},
});
// 添加样式
const style = document.createElement("style");
style.textContent = `
.o_watermark_container {
position: absolute;
inset: 0;
pointer-events: none;
z-index: 9999;
}
.watermark {
opacity: 0.3;
color: #333;
font-size: 14px;
padding: 5px 10px;
border-radius: 3px;
white-space: nowrap;
}
.watermark > div {
line-height: 1.5;
}
`;
document.head.appendChild(style);
Odoo 17系统水印功能,让数据安全透明!
在 Odoo 开发中,Owl 框架为我们提供了强大的组件化开发能力。今天,我们就来深入解析一个基于 Owl 的水印组件开发案例,看看它是如何实现的。
一、代码解析
(一)组件定义
class Watermark extends Component {
static template = xml`
<div class="watermark_container">
<t t-foreach="watermarks" t-as="wm" t-key="wm.id">
<div class="watermark" t-att-style="wm.style">
<div><t t-esc="state.name"/> </div>
<div><t t-esc="state.currentTime"/></div>
</div>
</t>
</div>
`;
- 这里定义了一个名为
Watermark
的 Owl 组件。 - 使用了
xml
模板语法来构建组件的 HTML 结构。 t-foreach
指令用于循环渲染watermarks
数组中的每个水印对象。t-att-style
动态绑定每个水印的样式,由wm.style
提供。t-esc
用于输出state.name
和state.currentTime
的值,分别显示用户名和当前时间。
(二)组件状态初始化与数据获取
setup() {
this.orm = useService("orm");
this.state = useState({
name: "加载中...",
currentTime: this.getFormattedTime(),
});
this.watermarks = useState(this.generateWatermarks());
setup
方法是组件的初始化方法。- 使用
useService
获取了orm
服务,虽然在这段代码中并未使用到orm
,但可能是为后续扩展预留。 - 使用
useState
初始化了两个状态:state
:包含用户名和当前时间。watermarks
:包含水印的布局信息。
state.name
初始值为 "加载中...",state.currentTime
初始值通过调用this.getFormattedTime()
获取当前时间。
(三)时间更新与窗口大小变化监听
setInterval(() => {
this.state.currentTime = this.getFormattedTime();
}, 60000);
- 使用
setInterval
每分钟更新一次state.currentTime
,调用this.getFormattedTime()
方法获取格式化的时间。
onMounted(() => {
window.addEventListener("resize", () => {
this.watermarks.splice(0, this.watermarks.length, ...this.generateWatermarks());
});
});
- 在组件挂载后,监听窗口大小变化事件。
- 当窗口大小发生变化时,调用
this.generateWatermarks()
方法重新生成水印布局,并更新watermarks
状态。
(四)用户信息获取
onMounted(async () => {
try {
const userId = session.uid;
console.log("当前用户 ID:", userId);
const userName = session.name;
console.log("当前用户名:", userName);
this.state.name = userName;
} catch (error) {
console.error("❌ 获取用户数据失败:", error);
this.state.name = "错误";
}
});
- 在组件挂载后,异步获取当前用户的信息。
- 从
session
中获取用户 ID 和用户名。 - 如果获取成功,将用户名赋值给
state.name
;如果失败,捕获错误并设置state.name
为 "错误"。
(五)时间格式化方法
getFormattedTime() {
const now = new Date();
return `${now.getFullYear()}年${now.getMonth() + 1}月${now.getDate()}日 ${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')}`;
}
- 这是一个辅助方法,用于获取格式化的时间字符串。
- 使用
Date
对象获取当前日期和时间,然后按照 "年月日 时:分" 的格式拼接字符串。 - 使用
padStart
方法确保小时和分钟始终为两位数字。
(六)水印布局生成方法
generateWatermarks() {
const watermarks = [];
const screenWidth = window.innerWidth;
const screenHeight = window.innerHeight;
const spacingX = 200; // 水平间距
const spacingY = 150; // 垂直间距
const cols = Math.ceil(screenWidth / spacingX);
const rows = Math.ceil(screenHeight / spacingY);
for (let row = 0; row < rows; row++) {
for (let col = 0; col < cols; col++) {
const x = col * spacingX + (row % 2) * (spacingX / 2);
const y = row * spacingY;
watermarks.push({
id: `${row}-${col}`,
style: `
position: absolute;
left: ${x}px;
top: ${y}px;
transform: rotate(-30deg);
transform-origin: center;
`,
});
}
}
return watermarks;
}
- 这个方法用于生成水印的布局信息。
- 根据窗口的宽度和高度,以及设定的水平和垂直间距,计算出需要的行列数。
- 使用嵌套循环生成每个水印的位置和样式。
- 水印采用交错排列的方式,通过
(row % 2) * (spacingX / 2)
实现。 - 每个水印的样式包括绝对定位、位置坐标、旋转角度等。
(七)组件挂载到 WebClient
patch(WebClient.prototype, {
async setup() {
await super.setup(...arguments);
const container = document.createElement("div");
container.classList.add("o_watermark_container");
const root = document.querySelector(".o_web_client");
if (root) {
root.appendChild(container);
await mount(Watermark, container, {
props: {},
env: this.env,
});
console.log("✅ 水印已挂载。");
} else {
console.warn("⚠️ 未找到 .o_web_client 元素");
}
},
});
- 使用
patch
方法扩展了WebClient
。 - 在
WebClient
的setup
方法中,创建了一个容器元素,并将其添加到.o_web_client
元素中。 - 使用
mount
方法将Watermark
组件挂载到容器中。
(八)样式添加
const style = document.createElement("style");
style.textContent = `
.o_watermark_container {
position: absolute;
inset: 0;
pointer-events: none;
z-index: 9999;
}
.watermark {
opacity: 0.3;
color: #333;
font-size: 14px;
padding: 5px 10px;
border-radius: 3px;
white-space: nowrap;
}
.watermark > div {
line-height: 1.5;
}
`;
document.head.appendChild(style);
- 动态创建了一个
<style>
标签,并添加到页面的<head>
中。 - 定义了水印容器和水印的样式,包括透明度、颜色、字体大小、内边距、圆角等。
二、可优化项
(一)性能优化
- 减少 DOM 操作:在窗口大小变化时,直接更新水印的位置和样式,而不是重新生成整个水印数组并替换。可以通过直接修改每个水印元素的样式属性来实现,这样可以减少 DOM 操作的开销。
- 优化时间更新逻辑:目前每分钟更新一次时间,但如果用户长时间停留在页面上,可能会导致时间显示不够精确。可以考虑每秒更新一次时间,但为了避免频繁更新 DOM,可以只在时间的分钟部分发生变化时才更新 DOM。
(二)功能增强
- 支持自定义水印内容:目前水印内容固定为用户名和当前时间,可以扩展为支持自定义水印内容,例如通过配置项或从后端获取水印模板。
- 水印样式动态配置:将水印的样式(如字体大小、颜色、透明度、旋转角度等)提取为可配置项,方便用户根据需求进行调整。
(三)完整代码
/** @odoo-module **/
import { Component, useState, onMounted, mount, xml } from "@odoo/owl";
import { patch } from "@web/core/utils/patch";
import { WebClient } from "@web/webclient/webclient";
import { session } from "@web/session";
import { useService } from "@web/core/utils/hooks";
// 水印组件
class Watermark extends Component {
static template = xml`
<div class="watermark_container">
<t t-foreach="watermarks" t-as="wm" t-key="wm.id">
<div class="watermark" t-att-style="wm.style">
<div><t t-esc="state.name"/> </div>
<div><t t-esc="state.currentTime"/></div>
</div>
</t>
</div>
`;
setup() {
this.orm = useService("orm");
this.state = useState({
name: "加载中...",
currentTime: this.getFormattedTime(),
});
this.watermarks = useState(this.generateWatermarks());
// 每分钟更新时间
setInterval(() => {
this.state.currentTime = this.getFormattedTime();
}, 60000);
// 监听窗口大小变化,重新生成水印
onMounted(() => {
window.addEventListener("resize", () => {
this.watermarks.splice(0, this.watermarks.length, ...this.generateWatermarks());
});
});
onMounted(async () => {
try {
// 获取当前用户 ID
const userId = session.uid;
console.log("当前用户 ID:", userId);
// 获取其他用户信息(如用户名)
const userName = session.name;
console.log("当前用户名:", userName);
// 设置用户的名称
this.state.name = userName ;
} catch (error) {
console.error("❌ 获取用户数据失败:", error);
this.state.name = "错误";
}
});
}
// 获取格式化的当前时间(年月日 时:分)
getFormattedTime() {
const now = new Date();
return `${now.getFullYear()}年${now.getMonth() + 1}月${now.getDate()}日 ${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')}`;
}
// 生成水印位置和样式
generateWatermarks() {
const watermarks = [];
const screenWidth = window.innerWidth;
const screenHeight = window.innerHeight;
const spacingX = 200; // 水平间距
const spacingY = 150; // 垂直间距
// 计算需要的行列数
const cols = Math.ceil(screenWidth / spacingX);
const rows = Math.ceil(screenHeight / spacingY);
for (let row = 0; row < rows; row++) {
for (let col = 0; col < cols; col++) {
const x = col * spacingX + (row % 2) * (spacingX / 2); // 交错排列
const y = row * spacingY;
watermarks.push({
id: `${row}-${col}`,
style: `
position: absolute;
left: ${x}px;
top: ${y}px;
transform: rotate(-30deg);
transform-origin: center;
`,
});
}
}
return watermarks;
}
}
// 扩展 WebClient
patch(WebClient.prototype, {
async setup() {
await super.setup(...arguments);
const container = document.createElement("div");
container.classList.add("o_watermark_container");
const root = document.querySelector(".o_web_client");
if (root) {
root.appendChild(container);
await mount(Watermark, container, {
props: {},
env: this.env,
});
console.log("✅ 水印已挂载。");
} else {
console.warn("⚠️ 未找到 .o_web_client 元素");
}
},
});
// 添加样式
const style = document.createElement("style");
style.textContent = `
.o_watermark_container {
position: absolute;
inset: 0;
pointer-events: none;
z-index: 9999;
}
.watermark {
opacity: 0.3;
color: #333;
font-size: 14px;
padding: 5px 10px;
border-radius: 3px;
white-space: nowrap;
}
.watermark > div {
line-height: 1.5;
}
`;
document.head.appendChild(style);
odoo 17系统水印功能,让数据安全透明!