{CODE} : new tabs

This commit is contained in:
raman 2025-09-16 11:17:28 +05:30
parent f578cc6cf4
commit 9286d099ff
13 changed files with 2578 additions and 0 deletions

View File

View File

@ -0,0 +1,26 @@
# -*- coding: utf-8 -*-
{
'name': "Multi Tabs",
'summary': "Multi Tabs for Odoo 18",
'description': """
Multi Tabs
""",
"author": "1311793927@qq.com",
'support': '1311793927qq.com',
'images': ['static/description/main_banner.png'],
'category': 'General',
'version': '0.1',
"license": "LGPL-3",
'depends': ['base','web'],
"installable": True,
"auto_install": False,
"assets": {
"web.assets_backend": [
"tabbar/static/src/**/*",
],
},
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 786 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

View File

@ -0,0 +1,10 @@
<section class="oe_container oe_slogan">
<span>
The basic functions have been implemented, and further optimization will be carried out later</span>
<br />
<br />
<img src="1.png" style="width:950px">
</section>

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,128 @@
import { ActionContainer } from '@web/webclient/actions/action_container';
import { patch } from '@web/core/utils/patch';
import { AklMultiTab } from './components/multi_tab/akl_multi_tab';
import { xml, useState } from '@odoo/owl';
import { browser } from '@web/core/browser/browser';
import { useService } from '@web/core/utils/hooks';
import {
router as _router,
} from '@web/core/browser/router';
patch(ActionContainer.prototype, {
setup() {
super.setup();
this.action_infos = [];
this.controllerStacks = {};
// this.action_service = useService('action');
this.env.bus.addEventListener(
'ACTION_MANAGER:UPDATE',
({ detail: info }) => {
debugger
this.action_infos = this.get_controllers(info);
this.controllerStacks = info.controllerStacks;
this.render();
}
);
},
get_controllers(info) {
const action_infos = [];
const entries = Object.entries(info.controllerStacks);
entries.forEach(([key, stack]) => {
const lastController = stack[stack.length - 1];
const action_info = {
key: key,
__info__: lastController,
Component: lastController.__info__.Component,
active: false,
componentProps: lastController.__info__.componentProps || {},
}
if (lastController.count == info.count) {
action_info.active = true;
}
action_infos.push(action_info);
})
return action_infos;
},
_on_close_action(action_info) {
this.action_infos = this.action_infos.filter((info) => {
return info.key !== action_info.key;
});
if (this.action_infos.length > 0) {
delete this.controllerStacks[action_info.key];
this.action_infos[this.action_infos.length - 1].active = true; // Set last
this.render();
}
},
_on_active_action(action_info) {
debugger
this.action_infos.forEach((info) => {
info.active = info.key === action_info.key;
});
const url = _router.stateToUrl(action_info.__info__.state)
browser.history.pushState({}, "", url);
this.render();
},
_close_other_action() {
this.action_infos = this.action_infos.filter((info) => {
if (info.active == false) {
delete this.controllerStacks[info.key];
}
return info.active == true
});
this.render();
},
_close_current_action() {
debugger
this.action_infos = this.action_infos.filter((info) => {
if (info.active == true) {
delete this.controllerStacks[info.key];
}
return info.active == false
});
this.action_infos[this.action_infos.length - 1].active = true;
this.render();
},
_on_close_all_action() {
debugger
this.action_infos.forEach((info) => {
delete this.controllerStacks[info.key];
});
this.action_infos = {}
window.location.href = "/";
}
});
ActionContainer.components = {
...ActionContainer.components,
AklMultiTab,
};
ActionContainer.template = xml`
<t t-name="web.ActionContainer">
<t t-set="action_infos" t-value="action_infos" />
<div class="o_action_manager d-flex flex-colum">
<AklMultiTab
action_infos="action_infos"
active_action="(action_info) => this._on_active_action(action_info)"
close_action="(action_info) => this._on_close_action(action_info)"
close_current_action="() => this._close_current_action()"
close_other_action="() => this._close_other_action()"
close_all_action="() => this._on_close_all_action()"
/>
<div t-foreach="action_infos" t-as="action_info" t-if="action_info" t-key="action_info.key" class="akl_controller_container d-flex flex-column" t-att-class="action_info.active ? '' : 'd-none'" >
<t t-component="action_info.Component" className="'o_action'" t-props="action_info.componentProps" />
</div>
</div>
</t>
`;

View File

@ -0,0 +1,29 @@
.akl_controller_container {
overflow-y: hidden;
flex: 1 1 auto;
.o_view_controller {
display: flex;
height: 100%;
overflow: hidden;
flex-direction: column;
.o_content {
flex: 1 1 auto;
overflow-y: auto;
}
}
.o_action {
display: flex;
flex-direction: column;
height: 100%;
overflow: hidden;
background-color: white;
.o_content {
flex: 1 1 auto;
overflow-y: auto;
background-color: white;
}
}
}

View File

@ -0,0 +1,31 @@
import { Component, useRef } from '@odoo/owl';
import { Dropdown } from '@web/core/dropdown/dropdown';
import { DropdownItem } from '@web/core/dropdown/dropdown_item';
import { DropdownGroup } from '@web/core/dropdown/dropdown_group';
export class AklMultiTab extends Component {
static template = 'akl_multi_tab.tab';
static components = { Dropdown, DropdownItem, DropdownGroup };
static props = ['*'];
setup() {
super.setup();
this.tabContainerRef = [];
}
rollPage() { }
_close_all_action() { this.props.close_all_action(); }
_close_current_action() {
this.props.close_current_action();
}
_close_other_action() {
this.props.close_other_action();
}
_on_click_tab_close(info) {
this.props.close_action(info);
}
_on_click_tab_item(info) {
this.props.active_action(info);
}
_on_multi_tab_next() { }
_on_multi_tab_prev() { }
get action_infos() { }
get current_action_info() { }
}

View File

@ -0,0 +1,295 @@
.akl_multi_tab_container {
line-height: 40px;
background-color: #fff;
border-bottom: 1px solid #dee2e6;
box-sizing: border-box;
z-index: 3;
.akl_multi_tab {
position: relative;
padding: 0 80px 0 40px;
height: 40px;
display: flex;
flex-direction: row;
box-sizing: border-box;
.akl_tab_scroll {
width: 100%;
height: 100%;
overflow: hidden;
.akl_page_items {
position: relative;
left: 0px;
height: 100%;
flex: 1 1 auto;
background: rgb(255, 255, 255);
overflow: visible;
white-space: nowrap;
padding: 0px;
li {
display: inline-block;
*display: inline;
*zoom: 1;
vertical-align: middle;
font-size: 14px;
transition: all 0.2s;
-webkit-transition: all 0.2s;
min-width: 65px;
padding: 0 15px;
text-align: center;
cursor: pointer;
line-height: 40px;
max-width: 160px;
text-overflow: ellipsis;
padding-right: 40px;
overflow: hidden;
border-right: 1px solid #f6f6f6;
vertical-align: top;
position: relative;
white-space: nowrap;
padding: 0px 30px 0px 10px;
user-select: none;
&::after {
content: '';
position: absolute;
top: 0;
left: 0;
width: 0;
height: 2px;
border-radius: 0;
background-color: #292b34;
transition: all 0.3s;
-webkit-transition: all 0.3s;
}
&.akl_multi_tab_active {
&::after {
width: 100%;
}
}
&:hover {
background-color: #f6f6f6;
&::after {
width: 100%;
}
}
.akl_tab_close {
position: absolute;
right: 8px;
top: 50%;
margin: -8px 0 0 0;
width: 16px;
height: 16px;
line-height: 16px;
border-radius: 50%;
font-size: 12px;
&:first {
display: none;
}
&:hover {
background-color: #ff5722;
color: #fff;
}
svg {
margin-top: -2px;
}
}
&.active {
background-color: $o-brand-primary;
color:white;
}
}
}
}
.akl_tab_control {
width: 40px;
height: 100%;
text-align: center;
cursor: pointer;
transition: all 0.3s;
-webkit-transition: all 0.3s;
box-sizing: border-box;
border-left: 1px solid #f6f6f6;
&:hover {
background-color: #f6f6f6;
}
}
.akl_icon_prev {
border-right: 1px solid #f6f6f6;
color: #666;
line-height: 40px;
margin: 0;
padding: 0;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
-webkit-font-smoothing: antialiased;
-webkit-transition: all 0.3s;
font-size: 16px;
font-style: normal;
position: absolute;
top: 0;
left: 0;
width: 40px;
height: 100%;
text-align: center;
cursor: pointer;
box-sizing: border-box;
border-left: none;
border-right: 1px solid #f6f6f6;
}
.akl_icon_next {
border-right: 1px solid #f6f6f6;
color: #666;
line-height: 40px;
margin: 0;
padding: 0;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
font-family: layui-icon !important;
font-size: 16px;
font-style: normal;
-webkit-font-smoothing: antialiased;
position: absolute;
top: 0;
width: 40px;
height: 100%;
text-align: center;
cursor: pointer;
-webkit-transition: all 0.3s;
box-sizing: border-box;
border-left: 1px solid #f6f6f6;
right: 40px;
}
.akl_icon_down {
//right: 1px;
color: #666;
line-height: 40px;
margin: 0;
padding: 0;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
-webkit-transition: all 0.3s;
font-family: layui-icon !important;
font-size: 16px;
font-style: normal;
-webkit-font-smoothing: antialiased;
position: absolute;
top: 0;
width: 40px;
height: 100%;
text-align: center;
cursor: pointer;
box-sizing: border-box;
border-left: 1px solid #f6f6f6;
right: 0;
.dropdown-toggle {
width: 100%;
height: 100%;
background: none;
border: none;
}
.dropdown-menu {
.dropdown-item {
line-height: 30px;
}
}
}
.akl_multi_tab_active {
background-color: #f6f6f6;
&::after {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 2px;
border-radius: 0;
background-color: #292b34;
transition: all 0.3s;
-webkit-transition: all 0.3s;
}
}
}
.dropdown-menu {
z-index: 1000;
}
}
.o_action_manager {
display: flex;
flex-direction: column;
.akl_tab_page_container {
display: flex;
flex-direction: column;
overflow: hidden;
flex: 1 1 auto;
// > div {
// flex: 1 1 auto;
// overflow: auto;
// display: flex;
// flex-direction: column;
// }
.o_view_controller {
display: flex;
flex: 1 1 auto;
flex-direction: column;
overflow: hidden;
height: 100%;
.o_content {
overflow: auto;
}
}
}
}
.o_home_menu_background {
.akl_multi_tab_container {
display: none !important;
}
}
[name='product_template_id'] {
div,
span {
width: 100% !important;
display: block !important;
}
span {
white-space: pre-wrap !important;
word-break: break-all !important;
text-overflow: initial !important;
}
}
td[name='product_id'] {
white-space: pre-wrap !important;
word-break: break-all !important;
text-overflow: initial !important;
}

View File

@ -0,0 +1,99 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates id="template"
xml:space="preserve">
<t t-name="akl_multi_tab.tab"
owl="1">
<div class="akl_multi_tab_container">
<div class="akl_multi_tab">
<div class="akl_tab_control akl_icon_prev"
t-on-click.stop="_on_multi_tab_prev">
<svg viewBox="0 0 1024 1024"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20">
<path d="M269.25 512l271.53-271.53a32 32 0 0 0-45.25-45.25L201.37 489.38a32 32 0 0 0 0 45.25l294.16 294.16a32 32 0 0 0 45.25-45.25z"
fill="#adb5bd"/>
<path d="M551.1 512l271.53-271.53a32 32 0 0 0-45.26-45.25L483.22 489.38a32 32 0 0 0 0 45.25l294.15 294.16a32 32 0 1 0 45.26-45.25z"
fill="#adb5bd"/>
</svg>
</div>
<div class="akl_tab_control akl_icon_next"
t-on-click.stop="_on_multi_tab_next">
<svg viewBox="0 0 1024 1024"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20">
<path d="M483.22 240.47L754.73 512 483.22 783.53a32 32 0 0 0 45.25 45.25l294.15-294.15a32 32 0 0 0 0-45.26L528.47 195.22a32 32 0 0 0-45.25 45.25z"
fill="#adb5bd"/>
<path d="M540.78 534.63a32 32 0 0 0 0-45.26L246.63 195.22a32 32 0 0 0-45.25 45.25L472.89 512 201.38 783.53a32 32 0 0 0 45.25 45.25z"
fill="#adb5bd"/>
</svg>
</div>
<div class='akl_tab_control akl_icon_down'>
<DropdownGroup >
<Dropdown >
<svg viewBox="0 0 1024 1024"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20">
<path fill="#adb5bd"
d="M830.24 340.688l11.328 11.312a16 16 0 0 1 0 22.624L530.448 685.76a16 16 0 0 1-22.64 0L196.688 374.624a16 16 0 0 1 0-22.624l11.312-11.312a16 16 0 0 1 22.624 0l288.496 288.496 288.512-288.496a16 16 0 0 1 22.624 0z"/>
</svg>
<t t-set-slot="content">
<DropdownItem class="'akl_close_cur_tab'"
onSelected="() => this._close_current_action()">
Close Current Tab
</DropdownItem>
<DropdownItem class="'akl_close_other_tabs'"
onSelected="() => this._close_other_action()">
Close Other Tabs
</DropdownItem>
<DropdownItem class="'akl_close_all_tabs'"
onSelected="() => this._close_all_action()">
Close All Tabs
</DropdownItem>
</t>
</Dropdown>
</DropdownGroup>
</div>
<div class="akl_tab_scroll">
<ul class="akl_page_items tab_container"
t-ref="tab_container">
<li t-foreach="props.action_infos"
t-as="action_info"
t-if="action_info.key"
t-key="action_info.key"
t-att-class="{'active': action_info.active, 'akl_page_tab_item': true}"
t-on-click.stop="() => this._on_click_tab_item(action_info)">
<span>
<t t-esc="action_info.key" />
</span>
<span class="akl_tab_close"
t-on-click.stop="() => this._on_click_tab_close(action_info)">
<svg viewBox="0 0 1024 1024"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
width="14"
height="14">
<path d="M556.8 512l265.6-265.6c12.8-12.8 12.8-32 0-44.8s-32-12.8-44.8 0L512 467.2 246.4 201.6c-12.8-12.8-32-12.8-44.8 0s-12.8 32 0 44.8l265.6 265.6-265.6 265.6c-12.8 12.8-12.8 32 0 44.8 6.4 6.4 12.8 9.6 22.4 9.6s16-3.2 22.4-9.6l265.6-265.6 265.6 265.6c6.4 6.4 16 9.6 22.4 9.6s16-3.2 22.4-9.6c12.8-12.8 12.8-32 0-44.8L556.8 512z"
fill="#bfbfbf"/>
</svg>
</span>
</li>
</ul>
</div>
</div>
</div>
</t>
</templates>

View File

@ -0,0 +1,18 @@
.tabbar {
height: 30px;
margin-left: 0px;
width: 100%;
display: flex;
.tabbar_left {
display: flex;
overflow: hidden;
flex: 1 1 0%;
height: 100%;
}
.tabbar_right {
display: flex;
height: 100%;
align-items: center;
justify-content: center;
}
}