Compare commits
No commits in common. "1fa24f7558ff2e66fed62b9bbbbdd76e8527ec72" and "cd653ce24c4b9bd530d72040921c7a7d2e921f49" have entirely different histories.
1fa24f7558
...
cd653ce24c
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 656 B After Width: | Height: | Size: 656 B |
|
Before Width: | Height: | Size: 962 B After Width: | Height: | Size: 962 B |
@ -194,7 +194,6 @@
|
|||||||
transition: all 0.3s ease-in-out;
|
transition: all 0.3s ease-in-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 与主题模块冲突:强制红色主题会覆盖 Odoo 主题深色模式下的主按钮颜色(@image#1:Clipboard_Screenshot.png)
|
|
||||||
.ks_dn_create_chart .modal-footer .btn-primary {
|
.ks_dn_create_chart .modal-footer .btn-primary {
|
||||||
color: var(--ks-dn-white);
|
color: var(--ks-dn-white);
|
||||||
background: var(--ks-dn-primary-400);
|
background: var(--ks-dn-primary-400);
|
||||||
@ -213,7 +212,6 @@
|
|||||||
.ks_dn_create_chart .modal-footer .btn-secondary:hover {
|
.ks_dn_create_chart .modal-footer .btn-secondary:hover {
|
||||||
background: var(--ks-dn-gray-200);
|
background: var(--ks-dn-gray-200);
|
||||||
}
|
}
|
||||||
*/
|
|
||||||
|
|
||||||
.ks_dn_create_chart .modal:not([data-bs-backdrop="false"]) {
|
.ks_dn_create_chart .modal:not([data-bs-backdrop="false"]) {
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
@ -452,7 +450,6 @@ body:not(.o_touch_device) .o_field_selection:not(:hover):not(:focus-within) sele
|
|||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 与主题模块冲突:强制灰色主题会覆盖 Odoo 主题深色模式下的文件选择按钮颜色(@image#1:Clipboard_Screenshot.png)
|
|
||||||
.chart-form-detail .o_select_file_button.btn.btn-primary {
|
.chart-form-detail .o_select_file_button.btn.btn-primary {
|
||||||
height: 40px;
|
height: 40px;
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -468,7 +465,6 @@ body:not(.o_touch_device) .o_field_selection:not(:hover):not(:focus-within) sele
|
|||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
}
|
}
|
||||||
*/
|
|
||||||
|
|
||||||
.ks-char-preview .ks-chart-inner,
|
.ks-char-preview .ks-chart-inner,
|
||||||
.ks-modal-body-inner {
|
.ks-modal-body-inner {
|
||||||
@ -939,7 +935,6 @@ body:not(.o_touch_device) .o_field_selection:not(:hover):not(:focus-within) sele
|
|||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 与主题模块冲突:强制红色主题会覆盖 Odoo 主题深色模式下的按钮颜色(@image#1:Clipboard_Screenshot.png)
|
|
||||||
.ks_dashboard_header .ks_new_button_div .ks_dashboard_top_menu .ks_date_apply_clear_print .apply-dashboard-date-filter,
|
.ks_dashboard_header .ks_new_button_div .ks_dashboard_top_menu .ks_date_apply_clear_print .apply-dashboard-date-filter,
|
||||||
.ks_dashboard_top_menu .ks_dashboard_edit_mode_settings .ks_dashboard_save_layout,
|
.ks_dashboard_top_menu .ks_dashboard_edit_mode_settings .ks_dashboard_save_layout,
|
||||||
.ks_dashboard_top_menu .ks_dashboard_edit_mode_settings .ks_dashboard_create_new_layout,
|
.ks_dashboard_top_menu .ks_dashboard_edit_mode_settings .ks_dashboard_create_new_layout,
|
||||||
@ -959,7 +954,6 @@ body:not(.o_touch_device) .o_field_selection:not(:hover):not(:focus-within) sele
|
|||||||
border: solid 1px var(--ks-dn-gray-50);
|
border: solid 1px var(--ks-dn-gray-50);
|
||||||
color: var(--ks-dn-black-600);
|
color: var(--ks-dn-black-600);
|
||||||
}
|
}
|
||||||
*/
|
|
||||||
|
|
||||||
.ks_dashboard_top_menu .ks_dashboard_edit_mode_settings .oe_dashboard_links,
|
.ks_dashboard_top_menu .ks_dashboard_edit_mode_settings .oe_dashboard_links,
|
||||||
.ks_dashboard_top_menu .ks_dashboard_edit_mode_settings .ks_dashboard_cancel_layout {
|
.ks_dashboard_top_menu .ks_dashboard_edit_mode_settings .ks_dashboard_cancel_layout {
|
||||||
|
|||||||
@ -18,11 +18,11 @@ export class KSDashboardNinjaOverview extends Component{
|
|||||||
this.actionService = useService("action");
|
this.actionService = useService("action");
|
||||||
this.notification = useService("notification");
|
this.notification = useService("notification");
|
||||||
this.dialogService = useService("dialog");
|
this.dialogService = useService("dialog");
|
||||||
this.overviewTilesNames = [['全部仪表盘', 'one'], ['全部图表', 'two'], ['总地图', 'three'],
|
this.overviewTilesNames = [['All Dashboards', 'one'], ['All Charts', 'two'], ['Total Maps', 'three'],
|
||||||
['收藏的仪表盘', 'four'], ['全部列表', 'five']]
|
['Bookmarked Dashboards', 'four'], ['All Lists', 'five']]
|
||||||
this.state = useState({
|
this.state = useState({
|
||||||
bookmarkedDashboards: false,
|
bookmarkedDashboards: false,
|
||||||
filter: localStorage.getItem('dashboardFilter') || "全部仪表盘",
|
filter: localStorage.getItem('dashboardFilter') || "All Dashboards",
|
||||||
})
|
})
|
||||||
|
|
||||||
onWillStart(async () => {
|
onWillStart(async () => {
|
||||||
@ -96,7 +96,7 @@ export class KSDashboardNinjaOverview extends Component{
|
|||||||
let dashboardId = parseInt(ev.currentTarget.dataset.dashboardId);
|
let dashboardId = parseInt(ev.currentTarget.dataset.dashboardId);
|
||||||
let unBookmarkSvg = this.ks_overview.el.querySelector(`#unBookmark${dashboardId}`);
|
let unBookmarkSvg = this.ks_overview.el.querySelector(`#unBookmark${dashboardId}`);
|
||||||
let bookmarkSvg = this.ks_overview.el.querySelector(`#bookmark${dashboardId}`);
|
let bookmarkSvg = this.ks_overview.el.querySelector(`#bookmark${dashboardId}`);
|
||||||
let bookmarkCountTag = document.getElementById('收藏的仪表盘');
|
let bookmarkCountTag = document.getElementById('Bookmarked Dashboards');
|
||||||
if (unBookmarkSvg && bookmarkSvg){
|
if (unBookmarkSvg && bookmarkSvg){
|
||||||
unBookmarkSvg.classList.toggle('d-none');
|
unBookmarkSvg.classList.toggle('d-none');
|
||||||
bookmarkSvg.classList.toggle('d-none');
|
bookmarkSvg.classList.toggle('d-none');
|
||||||
|
|||||||
@ -269,15 +269,12 @@
|
|||||||
|
|
||||||
.modal-content {
|
.modal-content {
|
||||||
left: auto !important;
|
left: auto !important;
|
||||||
/* 与主题模块冲突:强制浅灰边框会覆盖主题深色模式下的弹窗边框 (@image#1:Clipboard_Screenshot.png) */
|
border: 1px solid $color-E5E7EB !important;
|
||||||
// border: 1px solid $color-E5E7EB !important;
|
background-color: $color-F5F8FB;
|
||||||
/* 与主题模块冲突:强制浅灰背景会覆盖主题深色模式下的弹窗背景 (@image#1:Clipboard_Screenshot.png) */
|
|
||||||
// background-color: $color-F5F8FB;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-header {
|
.modal-header {
|
||||||
/* 与主题模块冲突:强制浅灰底边框会覆盖主题深色模式下的弹窗标题栏边框 (@image#1:Clipboard_Screenshot.png) */
|
border-bottom: 1px solid $color-E5E7EB !important;
|
||||||
// border-bottom: 1px solid $color-E5E7EB !important;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-ai-box {
|
.chat-ai-box {
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
.ks_body_class .encapsulated-form-view {
|
.ks_body_class .encapsulated-form-view {
|
||||||
/* 与主题模块冲突:强制白色背景会覆盖主题深色模式下的弹窗表单区域背景 (@image#1:Clipboard_Screenshot.png) */
|
background-color: $color-white !important;
|
||||||
// background-color: $color-white !important;
|
|
||||||
padding: 24px 40px !important;
|
padding: 24px 40px !important;
|
||||||
|
|
||||||
.o_dropdown_button::after {
|
.o_dropdown_button::after {
|
||||||
@ -32,17 +31,15 @@
|
|||||||
// buttons
|
// buttons
|
||||||
.o_field_domain_dialog_button,
|
.o_field_domain_dialog_button,
|
||||||
.o_form_button_save {
|
.o_form_button_save {
|
||||||
/* 与主题模块冲突:强制红色主题会覆盖 Odoo 主题按钮颜色 (@image#1:Clipboard_Screenshot.png) */
|
background-color: $color-primary-main !important;
|
||||||
// background-color: $color-primary-main !important;
|
border: 1px solid $color-primary-main !important;
|
||||||
// border: 1px solid $color-primary-main !important;
|
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
min-height: 40px;
|
min-height: 40px;
|
||||||
padding: 0 12px;
|
padding: 0 12px;
|
||||||
font-size: $font-16;
|
font-size: $font-16;
|
||||||
font-weight: $f-w-400;
|
font-weight: $f-w-400;
|
||||||
line-height: 19.6px !important;
|
line-height: 19.6px !important;
|
||||||
/* 与主题模块冲突:强制白色文字会覆盖主题深色模式下的按钮文字颜色 (@image#1:Clipboard_Screenshot.png) */
|
color: $color-white;
|
||||||
// color: $color-white;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|
||||||
@ -74,20 +71,17 @@
|
|||||||
font-weight: $f-w-500;
|
font-weight: $f-w-500;
|
||||||
line-height: 19.6px !important;
|
line-height: 19.6px !important;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
/* 与主题模块冲突:强制深色文字会覆盖主题深色模式下的取消按钮文字颜色 (@image#1:Clipboard_Screenshot.png) */
|
color: $color-1E1E1E;
|
||||||
// color: $color-1E1E1E;
|
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
min-height: 35px;
|
min-height: 35px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
padding: 0 20px;
|
padding: 0 20px;
|
||||||
/* 与主题模块冲突:强制白色背景会覆盖主题深色模式下的取消按钮背景 (@image#1:Clipboard_Screenshot.png) */
|
background-color: $color-white;
|
||||||
// background-color: $color-white;
|
|
||||||
min-width: 80px;
|
min-width: 80px;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
/* 与主题模块冲突:强制浅色悬停背景会覆盖主题深色模式下的按钮悬停样式 (@image#1:Clipboard_Screenshot.png) */
|
background-color: $color-secondary-bg;
|
||||||
// background-color: $color-secondary-bg;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&:focus {
|
&:focus {
|
||||||
@ -103,17 +97,15 @@
|
|||||||
|
|
||||||
|
|
||||||
& .btn.btn-primary {
|
& .btn.btn-primary {
|
||||||
/* 与主题模块冲突:强制红色主题会覆盖 Odoo 主题按钮颜色 (@image#1:Clipboard_Screenshot.png) */
|
background-color: $color-primary-main !important;
|
||||||
// background-color: $color-primary-main !important;
|
border: 1px solid $color-primary-main !important;
|
||||||
// border: 1px solid $color-primary-main !important;
|
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
min-height: 40px;
|
min-height: 40px;
|
||||||
padding: 0 12px;
|
padding: 0 12px;
|
||||||
font-size: $font-16;
|
font-size: $font-16;
|
||||||
font-weight: $f-w-400;
|
font-weight: $f-w-400;
|
||||||
line-height: 19.6px;
|
line-height: 19.6px;
|
||||||
/* 与主题模块冲突:强制白色文字会覆盖主题深色模式下的按钮文字颜色 (@image#1:Clipboard_Screenshot.png) */
|
color: $color-white;
|
||||||
// color: $color-white;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
@ -153,8 +145,7 @@
|
|||||||
|
|
||||||
.o_content {
|
.o_content {
|
||||||
border: 1px solid $color-E5E7EB;
|
border: 1px solid $color-E5E7EB;
|
||||||
/* 与主题模块冲突:强制浅灰背景会覆盖主题深色模式下的图表内容区域背景 (@image#1:Clipboard_Screenshot.png) */
|
background-color: $color-F5F8FB !important;
|
||||||
// background-color: $color-F5F8FB !important;
|
|
||||||
border-radius: 20px;
|
border-radius: 20px;
|
||||||
padding: 20px 24px !important;
|
padding: 20px 24px !important;
|
||||||
overflow: visible !important;
|
overflow: visible !important;
|
||||||
|
|||||||
@ -380,40 +380,7 @@ body.ks_body_class {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.ks_body_class .dash-btn-red {
|
.ks_body_class .dash-btn-red,
|
||||||
background-color: $color-primary-main !important;
|
|
||||||
border: 1px solid $color-primary-main !important;
|
|
||||||
border-radius: 10px;
|
|
||||||
min-height: 40px;
|
|
||||||
padding: 0 12px;
|
|
||||||
text-align: center !important;
|
|
||||||
font-size: $font-16;
|
|
||||||
font-weight: $f-w-400;
|
|
||||||
line-height: 19.6px;
|
|
||||||
color: $color-white !important;
|
|
||||||
cursor: pointer;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
width: fit-content;
|
|
||||||
|
|
||||||
&.disabled {
|
|
||||||
opacity: 50%;
|
|
||||||
pointer-events: none;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:focus-visible {
|
|
||||||
outline: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: $btn-hover !important;
|
|
||||||
color: $color-white !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 与主题模块冲突:.btn-primary 硬编码红色主题会覆盖 Odoo 主题的主按钮颜色(@image#1:Clipboard_Screenshot.png)
|
|
||||||
.ks_body_class .btn-primary:not(.btn-link) {
|
.ks_body_class .btn-primary:not(.btn-link) {
|
||||||
background-color: $color-primary-main !important;
|
background-color: $color-primary-main !important;
|
||||||
border: 1px solid $color-primary-main !important;
|
border: 1px solid $color-primary-main !important;
|
||||||
@ -446,31 +413,8 @@ body.ks_body_class {
|
|||||||
color: $color-white !important;
|
color: $color-white !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
*/
|
|
||||||
|
|
||||||
.ks_body_class .dash-default-btn {
|
.ks_body_class .dash-default-btn,
|
||||||
border: 0.81px solid $color-E5E7EB;
|
|
||||||
font-size: $font-16;
|
|
||||||
font-weight: $f-w-500;
|
|
||||||
line-height: 22.4px;
|
|
||||||
text-align: left;
|
|
||||||
color: $color-1E1E1E;
|
|
||||||
border-radius: 8px;
|
|
||||||
min-height: 40px;
|
|
||||||
padding: 0 20px;
|
|
||||||
background-color: $color-white;
|
|
||||||
text-align: center;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: $color-secondary-bg !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:focus {
|
|
||||||
border: 1px solid $color-E7495E !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 与主题模块冲突:.btn-secondary 硬编码白底黑字等样式会覆盖主题深色模式下的按钮样式(@image#1:Clipboard_Screenshot.png)
|
|
||||||
.ks_body_class .btn-secondary {
|
.ks_body_class .btn-secondary {
|
||||||
border: 0.81px solid $color-E5E7EB;
|
border: 0.81px solid $color-E5E7EB;
|
||||||
font-size: $font-16;
|
font-size: $font-16;
|
||||||
@ -492,7 +436,6 @@ body.ks_body_class {
|
|||||||
border: 1px solid $color-E7495E !important;
|
border: 1px solid $color-E7495E !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
*/
|
|
||||||
|
|
||||||
.ks_body_class .dash-link {
|
.ks_body_class .dash-link {
|
||||||
font-size: $font-16;
|
font-size: $font-16;
|
||||||
@ -770,17 +713,15 @@ body.ks_body_class {
|
|||||||
|
|
||||||
.ks_body_class .o_form_button_save,
|
.ks_body_class .o_form_button_save,
|
||||||
.ks_body_class .o_form_button_save_new {
|
.ks_body_class .o_form_button_save_new {
|
||||||
/* 与主题模块冲突:强制红色主题会覆盖 Odoo 主题的保存按钮颜色 (@image#1:Clipboard_Screenshot.png) */
|
background-color: $color-primary-main;
|
||||||
// background-color: $color-primary-main;
|
border: 1px solid $color-primary-main;
|
||||||
// border: 1px solid $color-primary-main;
|
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
min-height: 40px;
|
min-height: 40px;
|
||||||
padding: 0 20px;
|
padding: 0 20px;
|
||||||
font-size: $font-14;
|
font-size: $font-14;
|
||||||
font-weight: $f-w-400;
|
font-weight: $f-w-400;
|
||||||
line-height: 19.6px;
|
line-height: 19.6px;
|
||||||
/* 与主题模块冲突:强制白色文字会覆盖主题深色模式下的保存按钮文字颜色 (@image#1:Clipboard_Screenshot.png) */
|
color: $color-white;
|
||||||
// color: $color-white;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
width: auto !important;
|
width: auto !important;
|
||||||
|
|
||||||
@ -791,9 +732,8 @@ body.ks_body_class {
|
|||||||
}
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
/* 与主题模块冲突:强制红色悬停背景会覆盖主题深色模式下的按钮悬停样式 (@image#1:Clipboard_Screenshot.png) */
|
background-color: $btn-hover;
|
||||||
// background-color: $btn-hover;
|
color: $color-white !important;
|
||||||
// color: $color-white !important;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@include max-575 {
|
@include max-575 {
|
||||||
@ -807,18 +747,15 @@ body.ks_body_class {
|
|||||||
font-weight: $f-w-500;
|
font-weight: $f-w-500;
|
||||||
line-height: 22.4px;
|
line-height: 22.4px;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
/* 与主题模块冲突:强制深色文字会覆盖主题深色模式下的取消按钮文字颜色 (@image#1:Clipboard_Screenshot.png) */
|
color: $color-1E1E1E;
|
||||||
// color: $color-1E1E1E;
|
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
min-height: 40px;
|
min-height: 40px;
|
||||||
padding: 0 20px !important;
|
padding: 0 20px !important;
|
||||||
/* 与主题模块冲突:强制白色背景会覆盖主题深色模式下的取消按钮背景 (@image#1:Clipboard_Screenshot.png) */
|
background-color: $color-white;
|
||||||
// background-color: $color-white;
|
|
||||||
width: auto !important;
|
width: auto !important;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
/* 与主题模块冲突:强制浅色悬停背景会覆盖主题深色模式下的按钮悬停样式 (@image#1:Clipboard_Screenshot.png) */
|
background-color: $color-secondary-bg;
|
||||||
// background-color: $color-secondary-bg;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&:focus {
|
&:focus {
|
||||||
@ -842,15 +779,13 @@ body.ks_body_class {
|
|||||||
|
|
||||||
.modal-header {
|
.modal-header {
|
||||||
padding: 12px 16px;
|
padding: 12px 16px;
|
||||||
/* 与主题模块冲突:强制浅灰背景会覆盖主题深色模式下的弹窗标题栏背景 (@image#1:Clipboard_Screenshot.png) */
|
background-color: $color-bg-main;
|
||||||
// background-color: $color-bg-main;
|
|
||||||
border-radius: 16px 16px 0 0;
|
border-radius: 16px 16px 0 0;
|
||||||
border-bottom: 0;
|
border-bottom: 0;
|
||||||
margin-bottom: 18px;
|
margin-bottom: 18px;
|
||||||
|
|
||||||
@include max-575 {
|
@include max-575 {
|
||||||
/* 与主题模块冲突:强制浅灰背景会覆盖主题深色模式下的弹窗标题栏背景 (@image#1:Clipboard_Screenshot.png) */
|
background-color: $color-bg-main !important;
|
||||||
// background-color: $color-bg-main !important;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-title {
|
.modal-title {
|
||||||
@ -858,12 +793,10 @@ body.ks_body_class {
|
|||||||
font-weight: $f-w-500;
|
font-weight: $f-w-500;
|
||||||
line-height: 22.4px;
|
line-height: 22.4px;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
/* 与主题模块冲突:强制黑色文字会覆盖主题深色模式下的弹窗标题颜色 (@image#1:Clipboard_Screenshot.png) */
|
color: $color-black;
|
||||||
// color: $color-black;
|
|
||||||
|
|
||||||
@include max-575 {
|
@include max-575 {
|
||||||
/* 与主题模块冲突:强制黑色文字会覆盖主题深色模式下的弹窗标题颜色 (@image#1:Clipboard_Screenshot.png) */
|
color: $color-black !important;
|
||||||
// color: $color-black !important;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -882,8 +815,7 @@ body.ks_body_class {
|
|||||||
font-weight: $f-w-500;
|
font-weight: $f-w-500;
|
||||||
line-height: 22.4px;
|
line-height: 22.4px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
/* 与主题模块冲突:强制黑色文字会覆盖主题深色模式下的弹窗正文颜色 (@image#1:Clipboard_Screenshot.png) */
|
color: $color-black;
|
||||||
// color: $color-black;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2637,13 +2569,11 @@ body.ks_body_class {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.ks_body_class .o_form_button_save.bg-violet {
|
.ks_body_class .o_form_button_save.bg-violet {
|
||||||
/* 与主题模块冲突:强制紫色主题会覆盖 Odoo 主题按钮颜色 (@image#1:Clipboard_Screenshot.png) */
|
background-color: $color-6789C6 !important;
|
||||||
// background-color: $color-6789C6 !important;
|
border: 1px solid $color-6789C6 !important;
|
||||||
// border: 1px solid $color-6789C6 !important;
|
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
/* 与主题模块冲突:强制紫色悬停背景会覆盖主题深色模式下的按钮悬停样式 (@image#1:Clipboard_Screenshot.png) */
|
background-color: $color-597ebe !important;
|
||||||
// background-color: $color-597ebe !important;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -14,15 +14,13 @@
|
|||||||
|
|
||||||
.modal-header {
|
.modal-header {
|
||||||
padding: 12px 16px;
|
padding: 12px 16px;
|
||||||
/* 与主题模块冲突:强制浅灰背景会覆盖主题深色模式下的弹窗标题栏背景 (@image#1:Clipboard_Screenshot.png) */
|
background-color: $color-bg-main;
|
||||||
// background-color: $color-bg-main;
|
|
||||||
border-radius: 16px 16px 0 0;
|
border-radius: 16px 16px 0 0;
|
||||||
border-bottom: 0;
|
border-bottom: 0;
|
||||||
justify-content: start !important;
|
justify-content: start !important;
|
||||||
|
|
||||||
@include max-575 {
|
@include max-575 {
|
||||||
/* 与主题模块冲突:强制浅灰背景会覆盖主题深色模式下的弹窗标题栏背景 (@image#1:Clipboard_Screenshot.png) */
|
background-color: $color-bg-main !important;
|
||||||
// background-color: $color-bg-main !important;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-title {
|
.modal-title {
|
||||||
@ -30,12 +28,10 @@
|
|||||||
font-weight: $f-w-500;
|
font-weight: $f-w-500;
|
||||||
line-height: 22.4px;
|
line-height: 22.4px;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
/* 与主题模块冲突:强制黑色文字会覆盖主题深色模式下的弹窗标题颜色 (@image#1:Clipboard_Screenshot.png) */
|
color: $color-black;
|
||||||
// color: $color-black;
|
|
||||||
|
|
||||||
@include max-575 {
|
@include max-575 {
|
||||||
/* 与主题模块冲突:强制黑色文字会覆盖主题深色模式下的弹窗标题颜色 (@image#1:Clipboard_Screenshot.png) */
|
color: $color-black !important;
|
||||||
// color: $color-black !important;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -384,8 +380,7 @@
|
|||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
line-height: 20.99px;
|
line-height: 20.99px;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
/* 与主题模块冲突:强制黑色文字会覆盖主题深色模式下的输入框文字颜色 (@image#1:Clipboard_Screenshot.png) */
|
color: $color-black;
|
||||||
// color: $color-black;
|
|
||||||
padding: 12px 10px;
|
padding: 12px 10px;
|
||||||
border: 0.91px solid $color-D1D5DB;
|
border: 0.91px solid $color-D1D5DB;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
@ -432,10 +427,8 @@
|
|||||||
font-weight: $f-w-400;
|
font-weight: $f-w-400;
|
||||||
line-height: 18.52px;
|
line-height: 18.52px;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
/* 与主题模块冲突:强制黑色文字会覆盖主题深色模式下的输入框文字颜色 (@image#1:Clipboard_Screenshot.png) */
|
color: $color-black;
|
||||||
// color: $color-black;
|
background-color: $color-white !important;
|
||||||
/* 与主题模块冲突:强制白色背景会覆盖主题深色模式下的输入框背景 (@image#1:Clipboard_Screenshot.png) */
|
|
||||||
// background-color: $color-white !important;
|
|
||||||
padding-right: 21px !important;
|
padding-right: 21px !important;
|
||||||
|
|
||||||
|
|
||||||
@ -548,10 +541,8 @@
|
|||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
line-height: 18.52px;
|
line-height: 18.52px;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
/* 与主题模块冲突:强制黑色文字会覆盖主题深色模式下的输入框文字颜色 (@image#1:Clipboard_Screenshot.png) */
|
color: $color-black;
|
||||||
// color: $color-black;
|
background-color: $color-white !important;
|
||||||
/* 与主题模块冲突:强制白色背景会覆盖主题深色模式下的输入框背景 (@image#1:Clipboard_Screenshot.png) */
|
|
||||||
// background-color: $color-white !important;
|
|
||||||
min-height: 45px;
|
min-height: 45px;
|
||||||
|
|
||||||
input {
|
input {
|
||||||
|
|||||||
@ -8,10 +8,8 @@
|
|||||||
line-height: 9.66px !important;
|
line-height: 9.66px !important;
|
||||||
letter-spacing: 0.0015em;
|
letter-spacing: 0.0015em;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
/* 与主题模块冲突:强制浅蓝背景会覆盖主题深色模式下的按钮背景 (@image#1:Clipboard_Screenshot.png) */
|
background-color: $color-D9F1FD !important;
|
||||||
// background-color: $color-D9F1FD !important;
|
color: $color-black !important;
|
||||||
/* 与主题模块冲突:强制黑色文字会覆盖主题深色模式下的按钮文字颜色 (@image#1:Clipboard_Screenshot.png) */
|
|
||||||
// color: $color-black !important;
|
|
||||||
border: 1px solid transparent !important;
|
border: 1px solid transparent !important;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
}
|
}
|
||||||
@ -64,8 +62,7 @@
|
|||||||
font-weight: $f-w-400;
|
font-weight: $f-w-400;
|
||||||
line-height: 18.52px;
|
line-height: 18.52px;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
/* 与主题模块冲突:强制黑色文字会覆盖主题深色模式下的输入框文字颜色 (@image#1:Clipboard_Screenshot.png) */
|
color: $color-black;
|
||||||
// color: $color-black;
|
|
||||||
|
|
||||||
&::placeholder {
|
&::placeholder {
|
||||||
font-size: $font-16;
|
font-size: $font-16;
|
||||||
@ -85,13 +82,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.ks_form_sheet .o_form_sheet{
|
.ks_form_sheet .o_form_sheet{
|
||||||
/* 与主题模块冲突:强制浅灰背景会覆盖主题深色模式下的表单页背景 (@image#1:Clipboard_Screenshot.png) */
|
background: $color-bg-main;
|
||||||
// background: $color-bg-main;
|
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 与主题模块冲突:.btn-primary 硬编码黑色主题会覆盖 Odoo 主题的主按钮颜色(@image#1:Clipboard_Screenshot.png)
|
|
||||||
.upload-file-btn .o_select_file_button.btn-primary{
|
.upload-file-btn .o_select_file_button.btn-primary{
|
||||||
background-color: $color-black !important;
|
background-color: $color-black !important;
|
||||||
border: 1px solid transparent !important;
|
border: 1px solid transparent !important;
|
||||||
@ -104,7 +99,6 @@
|
|||||||
line-height: 21px;
|
line-height: 21px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
*/
|
|
||||||
|
|
||||||
.top-inverse-5{
|
.top-inverse-5{
|
||||||
top: -5px;
|
top: -5px;
|
||||||
|
|||||||
@ -21,12 +21,10 @@
|
|||||||
min-height: 76px;
|
min-height: 76px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 与主题模块冲突:ks_dashboard_ninja 将顶部导航栏设为白色,会覆盖主题深色模式下的背景色,故注释掉 (@image#1:Clipboard_Screenshot.png) */
|
background: white;
|
||||||
// background: white;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
/* 与主题模块冲突:白色底部边框在深色模式下会显示为白线,故注释掉 (@image#1:Clipboard_Screenshot.png) */
|
border-bottom: 1px solid $color-E5E7EB !important;
|
||||||
// border-bottom: 1px solid $color-E5E7EB !important;
|
|
||||||
|
|
||||||
.o_menu_sections {
|
.o_menu_sections {
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
@ -83,8 +81,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.o_menu_brand {
|
.o_menu_brand {
|
||||||
/* 与主题模块冲突:强制黑色文字会覆盖主题深色模式下的导航文字颜色 (@image#1:Clipboard_Screenshot.png) */
|
color: $color-black;
|
||||||
// color: $color-black;
|
|
||||||
font-size: $font-22;
|
font-size: $font-22;
|
||||||
font-weight: $f-w-400;
|
font-weight: $f-w-400;
|
||||||
line-height: 30.8px;
|
line-height: 30.8px;
|
||||||
@ -132,17 +129,15 @@
|
|||||||
font-weight: $f-w-500;
|
font-weight: $f-w-500;
|
||||||
line-height: 32px;
|
line-height: 32px;
|
||||||
min-width: 52px;
|
min-width: 52px;
|
||||||
/* 与主题模块冲突:强制黑字浅蓝背景会覆盖主题深色模式下头像/用户名样式 (@image#1:Clipboard_Screenshot.png) */
|
color: $color-black;
|
||||||
// color: $color-black;
|
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
// background-color: $color-D9F1FD;
|
background-color: $color-D9F1FD;
|
||||||
padding: 7px 11px;
|
padding: 7px 11px;
|
||||||
|
|
||||||
@include max-768 {
|
@include max-768 {
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
border: 0.5px solid var(--Border-Color, #E5E7EB);
|
border: 0.5px solid var(--Border-Color, #E5E7EB);
|
||||||
/* 与主题模块冲突:强制白色背景会覆盖主题深色模式下头像样式 (@image#1:Clipboard_Screenshot.png) */
|
background-color: $color-white;
|
||||||
// background-color: $color-white;
|
|
||||||
font-size: $font-12;
|
font-size: $font-12;
|
||||||
font-weight: $f-w-400;
|
font-weight: $f-w-400;
|
||||||
line-height: 14px;
|
line-height: 14px;
|
||||||
@ -234,14 +229,12 @@
|
|||||||
|
|
||||||
.ks_body_class .o_debug_manager {
|
.ks_body_class .o_debug_manager {
|
||||||
button {
|
button {
|
||||||
/* 与主题模块冲突:强制深色文字会覆盖主题深色模式下的图标/文字颜色 (@image#1:Clipboard_Screenshot.png) */
|
color: #241C1D !important;
|
||||||
// color: #241C1D !important;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.ks_body_class .oe_topbar_name {
|
.ks_body_class .oe_topbar_name {
|
||||||
/* 与主题模块冲突:强制深色文字会覆盖主题深色模式下的顶部用户名颜色 (@image#1:Clipboard_Screenshot.png) */
|
color: #241C1D !important;
|
||||||
// color: #241C1D !important;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.ks_body_class .o_switch_company_menu_dropdown {
|
.ks_body_class .o_switch_company_menu_dropdown {
|
||||||
|
|||||||
@ -6,8 +6,7 @@
|
|||||||
|
|
||||||
.modal-header {
|
.modal-header {
|
||||||
padding: 12px 16px;
|
padding: 12px 16px;
|
||||||
/* 与主题模块冲突:强制浅灰背景会覆盖主题深色模式下的弹窗标题栏背景 (@image#1:Clipboard_Screenshot.png) */
|
background-color: $color-bg-main;
|
||||||
// background-color: $color-bg-main;
|
|
||||||
border-radius: 16px 16px 0 0;
|
border-radius: 16px 16px 0 0;
|
||||||
border-bottom: 0;
|
border-bottom: 0;
|
||||||
|
|
||||||
@ -16,8 +15,7 @@
|
|||||||
font-weight: $f-w-500;
|
font-weight: $f-w-500;
|
||||||
line-height: 22.4px;
|
line-height: 22.4px;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
/* 与主题模块冲突:强制黑色文字会覆盖主题深色模式下的弹窗标题颜色 (@image#1:Clipboard_Screenshot.png) */
|
color: $color-black;
|
||||||
// color: $color-black;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-close {}
|
.btn-close {}
|
||||||
@ -30,16 +28,14 @@
|
|||||||
line-height: 9.66px;
|
line-height: 9.66px;
|
||||||
letter-spacing: 0.0015em;
|
letter-spacing: 0.0015em;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
/* 与主题模块冲突:强制黑色文字会覆盖主题深色模式下的表单标签颜色 (@image#1:Clipboard_Screenshot.png) */
|
color: $color-black;
|
||||||
// color: $color-black;
|
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
|
|
||||||
// &:not(:focus-within) {
|
// &:not(:focus-within) {
|
||||||
// color: $color-danger !important;
|
// color: $color-danger !important;
|
||||||
// }
|
// }
|
||||||
&:focus {
|
&:focus {
|
||||||
/* 与主题模块冲突:强制黑色边框会覆盖主题深色模式下的焦点样式 (@image#1:Clipboard_Screenshot.png) */
|
border: 1px solid $color-black;
|
||||||
// border: 1px solid $color-black;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -83,8 +79,7 @@
|
|||||||
font-weight: $f-w-400;
|
font-weight: $f-w-400;
|
||||||
line-height: 18.52px;
|
line-height: 18.52px;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
/* 与主题模块冲突:强制黑色文字会覆盖主题深色模式下的输入框文字颜色 (@image#1:Clipboard_Screenshot.png) */
|
color: $color-black;
|
||||||
// color: $color-black;
|
|
||||||
|
|
||||||
&::placeholder {
|
&::placeholder {
|
||||||
font-size: $font-16;
|
font-size: $font-16;
|
||||||
|
|||||||
@ -600,7 +600,7 @@
|
|||||||
</a>
|
</a>
|
||||||
|
|
||||||
<ul class="dropdown-menu">
|
<ul class="dropdown-menu">
|
||||||
<li><a class="dropdown-item" href="#" t-on-click="onFilterChange">全部仪表盘</a></li>
|
<li><a class="dropdown-item" href="#" t-on-click="onFilterChange">All Dashboards</a></li>
|
||||||
<li><a class="dropdown-item" href="#" t-on-click="onFilterChange">Bookmarked</a></li>
|
<li><a class="dropdown-item" href="#" t-on-click="onFilterChange">Bookmarked</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
BIN
omux_menu_center/.DS_Store
vendored
Normal file
3
omux_menu_center/__init__.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from . import models
|
||||||
22
omux_menu_center/__manifest__.py
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright 2025 Sveltware Solutions
|
||||||
|
|
||||||
|
{
|
||||||
|
'name': 'Naver center',
|
||||||
|
'category': 'Hidden',
|
||||||
|
'version': '1.0.0',
|
||||||
|
'author': 'PYSSJKJ',
|
||||||
|
'depends': ['ks_dashboard_ninja', 'udoo_om_ux'],
|
||||||
|
'data': [
|
||||||
|
'views/views.xml',
|
||||||
|
],
|
||||||
|
'assets': {
|
||||||
|
'web.assets_backend': [
|
||||||
|
'omux_menu_center/static/src/css/*.scss',
|
||||||
|
'omux_menu_center/static/src/js/*.js'
|
||||||
|
]
|
||||||
|
},
|
||||||
|
'sequence': 0,
|
||||||
|
'application': True,
|
||||||
|
'installable': True
|
||||||
|
}
|
||||||
BIN
omux_menu_center/__pycache__/__init__.cpython-310.pyc
Normal file
BIN
omux_menu_center/__pycache__/__init__.cpython-311.pyc
Normal file
3
omux_menu_center/models/__init__.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from . import model
|
||||||
BIN
omux_menu_center/models/__pycache__/__init__.cpython-310.pyc
Normal file
BIN
omux_menu_center/models/__pycache__/__init__.cpython-311.pyc
Normal file
BIN
omux_menu_center/models/__pycache__/model.cpython-310.pyc
Normal file
BIN
omux_menu_center/models/__pycache__/model.cpython-311.pyc
Normal file
44
omux_menu_center/models/model.py
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from odoo import models, fields, api
|
||||||
|
from odoo.exceptions import UserError, ValidationError
|
||||||
|
|
||||||
|
|
||||||
|
class CustomConfigSettings(models.TransientModel):
|
||||||
|
_inherit = 'res.config.settings'
|
||||||
|
|
||||||
|
status_center = fields.Boolean('顶部导航栏居中', default=True)
|
||||||
|
|
||||||
|
def set_values(self):
|
||||||
|
"""
|
||||||
|
保存设置值到系统参数
|
||||||
|
这个方法在用户点击"保存"按钮时调用
|
||||||
|
"""
|
||||||
|
res = super(CustomConfigSettings, self).set_values()
|
||||||
|
|
||||||
|
self.env['ir.config_parameter'].sudo().set_param(
|
||||||
|
'omux_menu_center.status_center',
|
||||||
|
str(self.status_center) # 转换为字符串存储
|
||||||
|
)
|
||||||
|
|
||||||
|
return res
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def get_values(self):
|
||||||
|
"""
|
||||||
|
从系统参数获取设置值
|
||||||
|
这个方法在打开设置页面时调用
|
||||||
|
"""
|
||||||
|
res = super(CustomConfigSettings, self).get_values()
|
||||||
|
|
||||||
|
status_center_param = self.env['ir.config_parameter'].sudo().get_param(
|
||||||
|
'omux_menu_center.status_center',
|
||||||
|
'True' # 默认值
|
||||||
|
)
|
||||||
|
|
||||||
|
status_center = status_center_param == 'True'
|
||||||
|
|
||||||
|
res.update(
|
||||||
|
status_center=status_center,
|
||||||
|
)
|
||||||
|
return res
|
||||||
0
omux_menu_center/static/src/css/custom.scss
Normal file
198
omux_menu_center/static/src/js/novel.js
Normal file
@ -0,0 +1,198 @@
|
|||||||
|
/** @odoo-module **/
|
||||||
|
|
||||||
|
import { patch } from "@web/core/utils/patch";
|
||||||
|
import { useEffect, onMounted, useState, onWillStart } from "@odoo/owl";
|
||||||
|
import { NavBar } from "@web/webclient/navbar/navbar";
|
||||||
|
import { ActionContainer } from "@web/webclient/actions/action_container";
|
||||||
|
import { useService } from "@web/core/utils/hooks";
|
||||||
|
import { cookie } from "@web/core/browser/cookie";
|
||||||
|
|
||||||
|
export function getCurrentColorScheme() {
|
||||||
|
const COLOR_SCHEME_COOKIE_NAME = 'color_scheme';
|
||||||
|
return cookie.get(COLOR_SCHEME_COOKIE_NAME) || 'light';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function dnNavBarAddClasses(){
|
||||||
|
document.querySelector('body')?.classList.add('ks_body_class');
|
||||||
|
document.querySelector('.o_main_navbar')?.classList.add('navbar', 'navbar-expand-md');
|
||||||
|
document.querySelector('.ks-mobile-burger-menu')?.classList.remove('d-none');
|
||||||
|
document.querySelector('.mobile-burger-menu-default')?.classList.add('d-none');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function themeAddClasses(){
|
||||||
|
const currentScheme = getCurrentColorScheme();
|
||||||
|
if(currentScheme == 'dark'){
|
||||||
|
if (!document.querySelector('body').classList.contains('darkness')){
|
||||||
|
document.querySelector('body')?.classList.add('darkness');
|
||||||
|
const message = document.querySelector('.img-bg.d-none.ks-comments-systray img');
|
||||||
|
if (message) {
|
||||||
|
message.src = '/yuthon_base/static/src/img/message-notif.svg';
|
||||||
|
}
|
||||||
|
const folder = document.querySelector('.img-bg.d-none.ks-mail-systray img');
|
||||||
|
if (folder) {
|
||||||
|
folder.src = '/yuthon_base/static/src/img/note-text.svg';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function dnNavBarRemoveClasses(){
|
||||||
|
document.querySelector('body')?.classList.remove('ks_body_class');
|
||||||
|
document.querySelector('.o_main_navbar')?.classList.remove('navbar', 'navbar-expand-md');
|
||||||
|
document.querySelector('.ks-mobile-burger-menu')?.classList.add('d-none');
|
||||||
|
document.querySelector('.mobile-burger-menu-default')?.classList.remove('d-none');
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
patch(NavBar.prototype, {
|
||||||
|
setup(){
|
||||||
|
super.setup();
|
||||||
|
this.orm = useService('orm')
|
||||||
|
|
||||||
|
this.settings = {
|
||||||
|
'status_center': true
|
||||||
|
}
|
||||||
|
|
||||||
|
onWillStart(async () => {
|
||||||
|
await this.loadConfigSettings();
|
||||||
|
});
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if(this.settings.status_center){
|
||||||
|
this.applyStylesToAllPages();
|
||||||
|
}else{
|
||||||
|
this.applyStylesToAllPages_not();
|
||||||
|
}
|
||||||
|
themeAddClasses()
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if(this.settings.status_center){
|
||||||
|
this.applyStylesToAllPages();
|
||||||
|
}else{
|
||||||
|
this.applyStylesToAllPages_not();
|
||||||
|
}
|
||||||
|
themeAddClasses()
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
async adapt(){
|
||||||
|
const result = await super.adapt();
|
||||||
|
if(this.settings.status_center){
|
||||||
|
this.applyStylesToAllPages();
|
||||||
|
}else{
|
||||||
|
this.applyStylesToAllPages_not();
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
|
||||||
|
onNavBarDropdownItemSelection(menu) {
|
||||||
|
super.onNavBarDropdownItemSelection(menu);
|
||||||
|
if(this.settings.status_center){
|
||||||
|
this.applyStylesToAllPages();
|
||||||
|
}else{
|
||||||
|
this.applyStylesToAllPages_not();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
applyStylesToAllPages() {
|
||||||
|
const body = document.body;
|
||||||
|
if (body && !body.classList.contains('ks_body_class')) {
|
||||||
|
dnNavBarAddClasses();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
applyStylesToAllPages_not(){
|
||||||
|
if(this.currentApp?.xmlid === "ks_dashboard_ninja.board_menu_root" || this.actionService?.currentController?.action.tag === 'ks_dashboard_ninja'){
|
||||||
|
if(!document.querySelector('body').classList.contains('ks_body_class'))
|
||||||
|
dnNavBarAddClasses();
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
if(document.querySelector('body').classList.contains('ks_body_class'))
|
||||||
|
dnNavBarRemoveClasses();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async loadConfigSettings() {
|
||||||
|
try {
|
||||||
|
const settings = await this.orm.call(
|
||||||
|
'res.config.settings',
|
||||||
|
'get_values',
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
this.settings = {
|
||||||
|
'status_center': settings.status_center
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load settings:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
patch(ActionContainer.prototype, {
|
||||||
|
setup(){
|
||||||
|
super.setup();
|
||||||
|
|
||||||
|
this.settings = {
|
||||||
|
'status_center': true
|
||||||
|
}
|
||||||
|
this.orm = this.env.services.orm;
|
||||||
|
|
||||||
|
onWillStart(async () => {
|
||||||
|
await this.loadConfigSettings();
|
||||||
|
});
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if(this.settings.status_center){
|
||||||
|
this.applyStylesToAllPages();
|
||||||
|
}else{
|
||||||
|
this.applyStylesToAllPages_not();
|
||||||
|
}
|
||||||
|
themeAddClasses()
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if(this.settings.status_center){
|
||||||
|
this.applyStylesToAllPages();
|
||||||
|
}else{
|
||||||
|
this.applyStylesToAllPages_not();
|
||||||
|
}
|
||||||
|
themeAddClasses()
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
applyStylesToAllPages() {
|
||||||
|
const body = document.body;
|
||||||
|
if (body && !body.classList.contains('ks_body_class')) {
|
||||||
|
dnNavBarAddClasses();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
applyStylesToAllPages_not(){
|
||||||
|
if(this.currentApp?.xmlid === "ks_dashboard_ninja.board_menu_root" || this.actionService?.currentController?.action.tag === 'ks_dashboard_ninja'){
|
||||||
|
if(!document.querySelector('body').classList.contains('ks_body_class'))
|
||||||
|
dnNavBarAddClasses();
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
if(document.querySelector('body').classList.contains('ks_body_class'))
|
||||||
|
dnNavBarRemoveClasses();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async loadConfigSettings() {
|
||||||
|
try {
|
||||||
|
const settings = await this.orm.call(
|
||||||
|
'res.config.settings',
|
||||||
|
'get_values',
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
this.settings = {
|
||||||
|
'status_center': settings.status_center
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load settings:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
18
omux_menu_center/views/views.xml
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
<odoo>
|
||||||
|
<data>
|
||||||
|
<record id="res_config_settings_view_form" model="ir.ui.view">
|
||||||
|
<field name="name">res.config.settings.view.form.inherit.custom_nav</field>
|
||||||
|
<field name="model">res.config.settings</field>
|
||||||
|
<field name="inherit_id" ref="base_setup.res_config_settings_view_form"/>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<xpath expr="//block[@name='integration']" position="after">
|
||||||
|
<block title="顶部菜单" name="top_naver_center">
|
||||||
|
<setting help="导航栏居中显示" id="cf-custom_nav">
|
||||||
|
<field name="status_center"/>
|
||||||
|
</setting>
|
||||||
|
</block>
|
||||||
|
</xpath>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
</data>
|
||||||
|
</odoo>
|
||||||
@ -1 +0,0 @@
|
|||||||
from . import models
|
|
||||||
@ -1,36 +0,0 @@
|
|||||||
{
|
|
||||||
'name': 'Project Mindmap (project_md)',
|
|
||||||
'summary': '项目管理思维导图视图(D3 树形渲染)',
|
|
||||||
'version': '1.0',
|
|
||||||
'category': 'Project',
|
|
||||||
'depends': ['project', 'web'],
|
|
||||||
'data': [
|
|
||||||
'security/ir.model.access.csv',
|
|
||||||
'views/project_md_views.xml',
|
|
||||||
],
|
|
||||||
'assets': {
|
|
||||||
'web.assets_backend': [
|
|
||||||
'project_md/static/lib/d3/d3.min.js',
|
|
||||||
'project_md/static/src/js/mindmap_arch_parser.js',
|
|
||||||
'project_md/static/src/js/mindmap_controller.js',
|
|
||||||
'project_md/static/src/js/mindmap_renderer.js',
|
|
||||||
'project_md/static/src/js/mindmap_view.js',
|
|
||||||
'project_md/static/src/xml/mindmap_template.xml',
|
|
||||||
'project_md/static/src/scss/mindmap.scss',
|
|
||||||
],
|
|
||||||
# Omux 重定义了 web.assets_web_dark 且不继承 web.assets_backend,
|
|
||||||
# mindmap 视图类型必须在此 bundle 也注册,否则夜色模式下点击导图报错。
|
|
||||||
# 此为功能注册(非主题美化),不可删除。
|
|
||||||
'web.assets_web_dark': [
|
|
||||||
'project_md/static/lib/d3/d3.min.js',
|
|
||||||
'project_md/static/src/js/mindmap_arch_parser.js',
|
|
||||||
'project_md/static/src/js/mindmap_controller.js',
|
|
||||||
'project_md/static/src/js/mindmap_renderer.js',
|
|
||||||
'project_md/static/src/js/mindmap_view.js',
|
|
||||||
'project_md/static/src/xml/mindmap_template.xml',
|
|
||||||
'project_md/static/src/scss/mindmap.scss',
|
|
||||||
],
|
|
||||||
},
|
|
||||||
'installable': True,
|
|
||||||
'application': False,
|
|
||||||
}
|
|
||||||
@ -1,5 +0,0 @@
|
|||||||
from . import mindmap_diagram
|
|
||||||
from . import mindmap_node
|
|
||||||
from . import project_task_extend
|
|
||||||
from . import ir_ui_view_extend
|
|
||||||
from . import project_mindmap_extend
|
|
||||||
@ -1,20 +0,0 @@
|
|||||||
from odoo import fields, models
|
|
||||||
|
|
||||||
|
|
||||||
class IrUiView(models.Model):
|
|
||||||
_inherit = 'ir.ui.view'
|
|
||||||
|
|
||||||
# 注册自定义视图类型 mindmap,使其能被 ir.ui.view 的 type 字段接受
|
|
||||||
type = fields.Selection(
|
|
||||||
selection_add=[('mindmap', 'Mindmap')],
|
|
||||||
string='View Type',
|
|
||||||
)
|
|
||||||
|
|
||||||
def _get_view_info(self):
|
|
||||||
# 关键:只 selection_add 不够。get_view_info 只会把
|
|
||||||
# “既在选择项里、又在 _get_view_info() 字典里”的类型塞进
|
|
||||||
# session.view_info。若不在此登记 mindmap 的元数据,前端会报
|
|
||||||
# “View types not defined mindmap”。
|
|
||||||
res = super()._get_view_info()
|
|
||||||
res['mindmap'] = {'icon': 'fa fa-sitemap', 'multi_record': True}
|
|
||||||
return res
|
|
||||||
@ -1,9 +0,0 @@
|
|||||||
from odoo import models, fields
|
|
||||||
|
|
||||||
|
|
||||||
class MindmapDiagram(models.Model):
|
|
||||||
_name = 'mindmap.diagram'
|
|
||||||
_description = 'Mindmap Diagram'
|
|
||||||
|
|
||||||
name = fields.Char(string='名称', required=True)
|
|
||||||
res_model = fields.Char(string='关联业务模型')
|
|
||||||
@ -1,15 +0,0 @@
|
|||||||
from odoo import models, fields
|
|
||||||
|
|
||||||
|
|
||||||
class MindmapNode(models.Model):
|
|
||||||
_name = 'mindmap.node'
|
|
||||||
_description = 'Mindmap Node'
|
|
||||||
|
|
||||||
diagram_id = fields.Many2one('mindmap.diagram', string='导图', ondelete='cascade')
|
|
||||||
res_model = fields.Char(string='业务模型')
|
|
||||||
res_id = fields.Integer(string='业务记录ID')
|
|
||||||
parent_node_id = fields.Many2one('mindmap.node', string='父节点', ondelete='cascade')
|
|
||||||
x_pos = fields.Float(string='X坐标')
|
|
||||||
y_pos = fields.Float(string='Y坐标')
|
|
||||||
collapsed = fields.Boolean(string='已折叠', default=False)
|
|
||||||
custom_label = fields.Char(string='自定义标签')
|
|
||||||
@ -1,106 +0,0 @@
|
|||||||
from odoo import api, models
|
|
||||||
|
|
||||||
|
|
||||||
class ProjectProject(models.Model):
|
|
||||||
_inherit = 'project.project'
|
|
||||||
|
|
||||||
@api.model
|
|
||||||
def get_mindmap_tree(self, single_id=None):
|
|
||||||
"""返回 项目→阶段→任务 的树形结构(dict),供 mindmap 视图渲染。
|
|
||||||
|
|
||||||
不建中间表:直接利用已有的
|
|
||||||
project.project.type_ids(阶段)/ project.task.project_id / project.task.stage_id。
|
|
||||||
字段是否存在在服务端用 _fields 直接判断,杜绝 JS 侧写死字段导致的
|
|
||||||
Invalid field / 白屏。
|
|
||||||
"""
|
|
||||||
self = self.sudo()
|
|
||||||
|
|
||||||
# 1) 项目(单项目视角则只取当前项目)
|
|
||||||
proj_domain = [('id', '=', single_id)] if single_id else []
|
|
||||||
projects = self.search_read(proj_domain, ['name', 'type_ids'])
|
|
||||||
|
|
||||||
# 2) 阶段:经各项目 type_ids 收集、保序去重
|
|
||||||
stage_ids = []
|
|
||||||
for p in projects:
|
|
||||||
stage_ids += p['type_ids']
|
|
||||||
stage_ids = list(dict.fromkeys(stage_ids))
|
|
||||||
stages = self.env['project.task.type'].search_read(
|
|
||||||
[('id', 'in', stage_ids)], ['name', 'sequence']
|
|
||||||
) if stage_ids else []
|
|
||||||
stage_by_id = {s['id']: s for s in stages}
|
|
||||||
|
|
||||||
# 3) 任务:进度字段在部分版本/客制下不存在,服务端安全探测
|
|
||||||
task_fields = ['name', 'display_name', 'project_id', 'stage_id']
|
|
||||||
tk = self.env['project.task']
|
|
||||||
progress_field = None
|
|
||||||
if 'subtask_completion_percentage' in tk._fields:
|
|
||||||
progress_field = 'subtask_completion_percentage'
|
|
||||||
elif 'progress' in tk._fields:
|
|
||||||
progress_field = 'progress'
|
|
||||||
if progress_field:
|
|
||||||
task_fields.append(progress_field)
|
|
||||||
task_domain = [('project_id', '=', single_id)] if single_id else []
|
|
||||||
tasks = tk.search_read(task_domain, task_fields)
|
|
||||||
|
|
||||||
def _norm(v):
|
|
||||||
if v is None or v is False:
|
|
||||||
return 0
|
|
||||||
try:
|
|
||||||
v = float(v)
|
|
||||||
except (TypeError, ValueError):
|
|
||||||
return 0
|
|
||||||
# 部分字段存 0-1,有的存 0-100;统一归一到 0-100
|
|
||||||
return int(v) if v > 1 else int(v * 100)
|
|
||||||
|
|
||||||
project_nodes = {}
|
|
||||||
for p in projects:
|
|
||||||
project_nodes[p['id']] = {
|
|
||||||
'id': 'P%d' % p['id'],
|
|
||||||
'name': p['name'] or ('项目%s' % p['id']),
|
|
||||||
'kind': 'project',
|
|
||||||
'progress': 0,
|
|
||||||
'children': [],
|
|
||||||
}
|
|
||||||
|
|
||||||
for p in projects:
|
|
||||||
pn = project_nodes[p['id']]
|
|
||||||
proj_stages = [stage_by_id[i] for i in p['type_ids'] if i in stage_by_id]
|
|
||||||
proj_stages.sort(key=lambda s: s.get('sequence') or 0)
|
|
||||||
for st in proj_stages:
|
|
||||||
sn = {
|
|
||||||
'id': 'P%d_S%d' % (p['id'], st['id']),
|
|
||||||
'name': st['name'],
|
|
||||||
'kind': 'stage',
|
|
||||||
'progress': 0,
|
|
||||||
'children': [],
|
|
||||||
}
|
|
||||||
pn['children'].append(sn)
|
|
||||||
st_tasks = [
|
|
||||||
t for t in tasks
|
|
||||||
if t.get('project_id') and t['project_id'][0] == p['id']
|
|
||||||
and t.get('stage_id') and t['stage_id'][0] == st['id']
|
|
||||||
]
|
|
||||||
for t in st_tasks:
|
|
||||||
raw = t.get(progress_field) if progress_field else None
|
|
||||||
sn['children'].append({
|
|
||||||
'id': 'T%d' % t['id'],
|
|
||||||
'name': t.get('display_name') or t['name'],
|
|
||||||
'kind': 'task',
|
|
||||||
'resId': t['id'],
|
|
||||||
'progress': _norm(raw),
|
|
||||||
})
|
|
||||||
sn['progress'] = round(
|
|
||||||
sum(c['progress'] for c in sn['children']) / len(sn['children'])
|
|
||||||
) if sn['children'] else 0
|
|
||||||
pn['progress'] = round(
|
|
||||||
sum(c['progress'] for c in pn['children']) / len(pn['children'])
|
|
||||||
) if pn['children'] else 0
|
|
||||||
|
|
||||||
roots = list(project_nodes.values())
|
|
||||||
if single_id and len(roots) == 1:
|
|
||||||
return roots[0]
|
|
||||||
return {
|
|
||||||
'id': 0,
|
|
||||||
'name': '所有项目' if not single_id else '项目阶段与任务',
|
|
||||||
'children': roots,
|
|
||||||
}
|
|
||||||
@ -1,44 +0,0 @@
|
|||||||
from odoo import models
|
|
||||||
|
|
||||||
|
|
||||||
class ProjectTask(models.Model):
|
|
||||||
_inherit = 'project.task'
|
|
||||||
|
|
||||||
def get_hierarchy(self, project_id=None):
|
|
||||||
"""返回项目下任务的树形结构(嵌套 children),供前端 D3 渲染。
|
|
||||||
|
|
||||||
:param project_id: 项目ID,为空则返回所有任务
|
|
||||||
:return: 含虚拟根的嵌套字典
|
|
||||||
"""
|
|
||||||
domain = []
|
|
||||||
if project_id:
|
|
||||||
domain.append(('project_id', '=', project_id))
|
|
||||||
tasks = self.search(domain)
|
|
||||||
|
|
||||||
by_id = {}
|
|
||||||
for t in tasks:
|
|
||||||
by_id[t.id] = {
|
|
||||||
'id': t.id,
|
|
||||||
'name': t.name or '未命名',
|
|
||||||
'parent_id': t.parent_id.id if t.parent_id else None,
|
|
||||||
'progress': round(t.progress or 0),
|
|
||||||
'user_id': t.user_id.name if t.user_id else False,
|
|
||||||
'stage_id': t.stage_id.name if t.stage_id else False,
|
|
||||||
'date_deadline': t.date_deadline.strftime('%Y-%m-%d') if t.date_deadline else False,
|
|
||||||
'children': [],
|
|
||||||
}
|
|
||||||
|
|
||||||
roots = []
|
|
||||||
for t in tasks:
|
|
||||||
node = by_id[t.id]
|
|
||||||
pid = node['parent_id']
|
|
||||||
if pid and pid in by_id:
|
|
||||||
by_id[pid]['children'].append(node)
|
|
||||||
else:
|
|
||||||
roots.append(node)
|
|
||||||
|
|
||||||
return {
|
|
||||||
'id': 0,
|
|
||||||
'name': '项目任务',
|
|
||||||
'children': roots,
|
|
||||||
}
|
|
||||||
@ -1,3 +0,0 @@
|
|||||||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
|
||||||
access_mindmap_diagram,mindmap.diagram,model_mindmap_diagram,base.group_user,1,1,1,1
|
|
||||||
access_mindmap_node,mindmap.node,model_mindmap_node,base.group_user,1,1,1,1
|
|
||||||
|
2
project_md/static/lib/d3.v7.min.js
vendored
2
project_md/static/lib/d3/d3.min.js
vendored
@ -1,24 +0,0 @@
|
|||||||
/** @odoo-module **/
|
|
||||||
import { visitXML } from "@web/core/utils/xml";
|
|
||||||
|
|
||||||
export class MindmapArchParser {
|
|
||||||
parse(arch, fields = {}, resModel = "") {
|
|
||||||
const archInfo = {
|
|
||||||
fields,
|
|
||||||
resModel,
|
|
||||||
parentField: "parent_id",
|
|
||||||
nameField: "name",
|
|
||||||
};
|
|
||||||
visitXML(arch, (node) => {
|
|
||||||
if (node.tagName === "mindmap") {
|
|
||||||
if (node.hasAttribute("parent_field")) {
|
|
||||||
archInfo.parentField = node.getAttribute("parent_field");
|
|
||||||
}
|
|
||||||
if (node.hasAttribute("name_field")) {
|
|
||||||
archInfo.nameField = node.getAttribute("name_field");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return archInfo;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,67 +0,0 @@
|
|||||||
/** @odoo-module **/
|
|
||||||
import { Component, useState, onWillStart } from "@odoo/owl";
|
|
||||||
import { useService } from "@web/core/utils/hooks";
|
|
||||||
|
|
||||||
export class MindmapController extends Component {
|
|
||||||
setup() {
|
|
||||||
super.setup();
|
|
||||||
this.orm = useService("orm");
|
|
||||||
this.action = useService("action");
|
|
||||||
this.notification = useService("notification");
|
|
||||||
this.state = useState({ treeData: null, loading: true });
|
|
||||||
|
|
||||||
onWillStart(async () => {
|
|
||||||
await this._loadData();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async _loadData() {
|
|
||||||
// 取数 + 建树全部下沉到服务端 project.project.get_mindmap_tree:
|
|
||||||
// 服务端用 _fields 安全探测字段存在性,从根上消灭 Invalid field;
|
|
||||||
// 跨模型关联(project→type_ids→stage→task)也在 Python 侧一次性拼好。
|
|
||||||
const singleProjectId =
|
|
||||||
this.props.resModel === "project.project" && this.props.resId
|
|
||||||
? this.props.resId
|
|
||||||
: null;
|
|
||||||
|
|
||||||
const treeData = await this.orm.call(
|
|
||||||
"project.project",
|
|
||||||
"get_mindmap_tree",
|
|
||||||
[singleProjectId || false]
|
|
||||||
);
|
|
||||||
this.state.treeData = treeData;
|
|
||||||
this.state.loading = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
get rendererProps() {
|
|
||||||
return {
|
|
||||||
treeData: this.state.treeData,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
onNodeClicked(ev) {
|
|
||||||
const node = ev.detail.node;
|
|
||||||
if (!node) return;
|
|
||||||
let resModel;
|
|
||||||
let resId;
|
|
||||||
if (node.kind === "task") {
|
|
||||||
resModel = "project.task";
|
|
||||||
resId = node.resId;
|
|
||||||
} else if (node.kind === "project") {
|
|
||||||
resModel = "project.project";
|
|
||||||
resId = Number(String(node.id).replace(/^P/, ""));
|
|
||||||
} else {
|
|
||||||
return; // 阶段节点是分组,不导航
|
|
||||||
}
|
|
||||||
if (resId) {
|
|
||||||
this.action.doAction({
|
|
||||||
type: "ir.actions.act_window",
|
|
||||||
res_model: resModel,
|
|
||||||
res_id: resId,
|
|
||||||
views: [[false, "form"]],
|
|
||||||
target: "current",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
MindmapController.template = "project_md.MindmapView";
|
|
||||||
@ -1,172 +0,0 @@
|
|||||||
/** @odoo-module **/
|
|
||||||
import { Component, onMounted, onWillUpdateProps } from "@odoo/owl";
|
|
||||||
|
|
||||||
export class MindmapRenderer extends Component {
|
|
||||||
setup() {
|
|
||||||
super.setup();
|
|
||||||
// 真正调用 owl 生命周期钩子,否则 _renderTree 永远不会执行(导图白屏)
|
|
||||||
onMounted(() => this._renderTree());
|
|
||||||
onWillUpdateProps(() => {
|
|
||||||
// 等待 owl 完成 DOM patch 后再重绘 d3,避免直接操作 DOM 与 owl 冲突
|
|
||||||
Promise.resolve().then(() => this._renderTree());
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
get treeData() {
|
|
||||||
return this.props.treeData;
|
|
||||||
}
|
|
||||||
|
|
||||||
_renderTree() {
|
|
||||||
const d3 = window.d3;
|
|
||||||
if (!d3) {
|
|
||||||
console.error("[Mindmap] D3.js not loaded");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const container = this.el?.querySelector?.(".mindmap-renderer-root");
|
|
||||||
if (!container || !this.treeData) return;
|
|
||||||
|
|
||||||
const width = Math.max(container.clientWidth, 900);
|
|
||||||
const height = Math.max(container.clientHeight || 560, 560);
|
|
||||||
|
|
||||||
// 清空后重绘
|
|
||||||
container.innerHTML = "";
|
|
||||||
|
|
||||||
const svg = d3
|
|
||||||
.select(container)
|
|
||||||
.append("svg")
|
|
||||||
.attr("width", width)
|
|
||||||
.attr("height", height)
|
|
||||||
.attr("class", "mindmap-svg");
|
|
||||||
|
|
||||||
const g = svg.append("g").attr("class", "mindmap-zoom-group");
|
|
||||||
|
|
||||||
const zoom = d3
|
|
||||||
.zoom()
|
|
||||||
.scaleExtent([0.15, 4])
|
|
||||||
.on("zoom", (event) => {
|
|
||||||
g.attr("transform", event.transform);
|
|
||||||
});
|
|
||||||
svg.call(zoom);
|
|
||||||
|
|
||||||
const root = d3.hierarchy(this.treeData);
|
|
||||||
root.descendants().forEach((d) => {
|
|
||||||
d.dataId = d.data.id;
|
|
||||||
});
|
|
||||||
|
|
||||||
const treeLayout = d3.tree().nodeSize([50, 220]);
|
|
||||||
treeLayout(root);
|
|
||||||
|
|
||||||
// 竖直居中
|
|
||||||
const x0 = d3.min(root.descendants(), (d) => d.x) || 0;
|
|
||||||
const x1 = d3.max(root.descendants(), (d) => d.x) || 0;
|
|
||||||
const initialTransform = d3.zoomIdentity.translate(
|
|
||||||
80,
|
|
||||||
(height - (x1 + x0)) / 2
|
|
||||||
);
|
|
||||||
svg.call(zoom.transform, initialTransform);
|
|
||||||
|
|
||||||
// 连接线
|
|
||||||
g.selectAll(".mindmap-link")
|
|
||||||
.data(root.links())
|
|
||||||
.enter()
|
|
||||||
.append("path")
|
|
||||||
.attr("class", "mindmap-link")
|
|
||||||
.attr("fill", "none")
|
|
||||||
.attr("stroke-width", 1.5)
|
|
||||||
.attr("opacity", 0.6)
|
|
||||||
.attr("d", (d) => {
|
|
||||||
return `M${d.source.y},${d.source.x}
|
|
||||||
C${(d.source.y + d.target.y) / 2},${d.source.x}
|
|
||||||
${(d.source.y + d.target.y) / 2},${d.target.x}
|
|
||||||
${d.target.y},${d.target.x}`;
|
|
||||||
});
|
|
||||||
|
|
||||||
// 节点
|
|
||||||
const nodes = g
|
|
||||||
.selectAll(".mindmap-node")
|
|
||||||
.data(root.descendants())
|
|
||||||
.enter()
|
|
||||||
.append("g")
|
|
||||||
.attr("class", "mindmap-node")
|
|
||||||
.attr("transform", (d) => `translate(${d.y},${d.x})`)
|
|
||||||
.style("cursor", "pointer");
|
|
||||||
|
|
||||||
nodes
|
|
||||||
.append("rect")
|
|
||||||
.attr("rx", 6)
|
|
||||||
.attr("ry", 6)
|
|
||||||
.attr("width", 160)
|
|
||||||
.attr("height", 36)
|
|
||||||
.attr("x", -80)
|
|
||||||
.attr("y", -18)
|
|
||||||
.attr("fill", (d) => {
|
|
||||||
const k = d.data.kind;
|
|
||||||
if (k === "project") return "#5E35B1";
|
|
||||||
if (k === "stage") return "#1E88E5";
|
|
||||||
return "#43A047";
|
|
||||||
})
|
|
||||||
.attr("stroke", (d) => {
|
|
||||||
const k = d.data.kind;
|
|
||||||
if (k === "project") return "#4527A0";
|
|
||||||
if (k === "stage") return "#1565C0";
|
|
||||||
return "#2E7D32";
|
|
||||||
})
|
|
||||||
.attr("stroke-width", 1.5)
|
|
||||||
.style("filter", "drop-shadow(1px 2px 3px rgba(0,0,0,0.2))")
|
|
||||||
.on("mouseenter", function () {
|
|
||||||
d3.select(this)
|
|
||||||
.transition()
|
|
||||||
.duration(150)
|
|
||||||
.attr("stroke-width", 2.5)
|
|
||||||
.style("filter", "drop-shadow(2px 4px 6px rgba(0,0,0,0.35))");
|
|
||||||
})
|
|
||||||
.on("mouseleave", function () {
|
|
||||||
d3.select(this)
|
|
||||||
.transition()
|
|
||||||
.duration(150)
|
|
||||||
.attr("stroke-width", 1.5)
|
|
||||||
.style("filter", "drop-shadow(1px 2px 3px rgba(0,0,0,0.2))");
|
|
||||||
});
|
|
||||||
|
|
||||||
nodes
|
|
||||||
.append("text")
|
|
||||||
.attr("text-anchor", "middle")
|
|
||||||
.attr("dominant-baseline", "central")
|
|
||||||
.attr("fill", "#fff")
|
|
||||||
.style("font-size", "13px")
|
|
||||||
.style("font-weight", "500")
|
|
||||||
.style("pointer-events", "none")
|
|
||||||
.text((d) => {
|
|
||||||
const name = d.data.name || "";
|
|
||||||
return name.length > 14 ? name.substring(0, 13) + "…" : name;
|
|
||||||
});
|
|
||||||
|
|
||||||
// 进度环(任意带进度的节点:任务/阶段/项目 都显示"进行到哪一步")
|
|
||||||
nodes
|
|
||||||
.filter((d) => d.data.progress && d.data.progress > 0)
|
|
||||||
.append("circle")
|
|
||||||
.attr("cx", 66)
|
|
||||||
.attr("cy", -14)
|
|
||||||
.attr("r", 10)
|
|
||||||
.attr("fill", "none")
|
|
||||||
.attr("stroke", "#FF9800")
|
|
||||||
.attr("stroke-width", 2.5)
|
|
||||||
.each(function (d) {
|
|
||||||
const r = 10;
|
|
||||||
const circ = 2 * Math.PI * r;
|
|
||||||
const p = d.data.progress / 100;
|
|
||||||
d3.select(this)
|
|
||||||
.attr("stroke-dasharray", circ)
|
|
||||||
.attr("stroke-dashoffset", circ * (1 - p));
|
|
||||||
});
|
|
||||||
|
|
||||||
// 点击 → 把整个 node(含 kind/resId)冒泡给 Controller 决定导航
|
|
||||||
// 注意事件名必须与模板 t-on-node-click-ed 一致,否则不触发
|
|
||||||
nodes.on("click", (event, d) => {
|
|
||||||
if (d.data && d.data.kind) {
|
|
||||||
this.trigger("node-click-ed", { node: d.data });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
MindmapRenderer.template = "project_md.MindmapRenderer";
|
|
||||||
@ -1,30 +0,0 @@
|
|||||||
/** @odoo-module **/
|
|
||||||
import { registry } from "@web/core/registry";
|
|
||||||
import { RelationalModel } from "@web/model/relational_model/relational_model";
|
|
||||||
import { MindmapArchParser } from "./mindmap_arch_parser";
|
|
||||||
import { MindmapController } from "./mindmap_controller";
|
|
||||||
import { MindmapRenderer } from "./mindmap_renderer";
|
|
||||||
|
|
||||||
export const mindmapView = {
|
|
||||||
type: "mindmap",
|
|
||||||
display_name: "思维导图",
|
|
||||||
icon: "fa fa-sitemap",
|
|
||||||
multiRecord: true,
|
|
||||||
ArchParser: MindmapArchParser,
|
|
||||||
Controller: MindmapController,
|
|
||||||
Model: RelationalModel,
|
|
||||||
Renderer: MindmapRenderer,
|
|
||||||
|
|
||||||
props: (genericProps, view) => {
|
|
||||||
const { arch, fields, resModel } = genericProps;
|
|
||||||
const archInfo = new view.ArchParser().parse(arch, fields, resModel);
|
|
||||||
return {
|
|
||||||
...genericProps,
|
|
||||||
Model: view.Model,
|
|
||||||
Renderer: view.Renderer,
|
|
||||||
archInfo,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
registry.category("views").add("mindmap", mindmapView);
|
|
||||||
@ -1,55 +0,0 @@
|
|||||||
.o_controller_mindmap {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
height: 100%;
|
|
||||||
padding: 12px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mindmap-renderer-root {
|
|
||||||
flex: 1 1 auto;
|
|
||||||
min-height: 560px;
|
|
||||||
overflow: auto;
|
|
||||||
background: linear-gradient(135deg, #f5f7fa 0%, #e8ecf1 100%);
|
|
||||||
border: 1px solid #d0d7de;
|
|
||||||
border-radius: 10px;
|
|
||||||
box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.06);
|
|
||||||
transition: background 0.3s ease, border-color 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mindmap-svg {
|
|
||||||
display: block;
|
|
||||||
margin: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mindmap-node rect {
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mindmap-node:hover rect {
|
|
||||||
filter: drop-shadow(2px 4px 8px rgba(0, 0, 0, 0.3));
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 连接线颜色交给 CSS 控制,便于夜色模式覆盖(d3 不再写死 stroke) */
|
|
||||||
.mindmap-link {
|
|
||||||
pointer-events: none;
|
|
||||||
stroke: #90a4ae;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mindmap-node text {
|
|
||||||
fill: #ffffff;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 深色模式适配:深色主题下容器与连线都不要纯白/浅灰 */
|
|
||||||
.o_web_client.o_dark_theme .mindmap-renderer-root,
|
|
||||||
.o_web_client.dark .mindmap-renderer-root {
|
|
||||||
background: linear-gradient(135deg, #1e2228 0%, #262a30 100%);
|
|
||||||
border-color: #3a4048;
|
|
||||||
box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.4);
|
|
||||||
}
|
|
||||||
|
|
||||||
.o_web_client.o_dark_theme .mindmap-link,
|
|
||||||
.o_web_client.dark .mindmap-link {
|
|
||||||
stroke: #5b6470;
|
|
||||||
}
|
|
||||||
@ -1,20 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<templates xml:space="preserve">
|
|
||||||
<t t-name="project_md.MindmapView" owl="1">
|
|
||||||
<div class="o_view_controller o_controller_mindmap">
|
|
||||||
<t t-if="state.treeData">
|
|
||||||
<t t-component="props.Renderer"
|
|
||||||
t-props="rendererProps"
|
|
||||||
t-on-node-click-ed="onNodeClicked"/>
|
|
||||||
</t>
|
|
||||||
<t t-else="">
|
|
||||||
<div class="o_mindmap_loading">加载中…</div>
|
|
||||||
</t>
|
|
||||||
</div>
|
|
||||||
</t>
|
|
||||||
|
|
||||||
<t t-name="project_md.MindmapRenderer" owl="1">
|
|
||||||
<div class="mindmap-renderer-root"
|
|
||||||
style="width:100%;position:relative;"/>
|
|
||||||
</t>
|
|
||||||
</templates>
|
|
||||||
@ -1,36 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<odoo>
|
|
||||||
<!-- 任务级 mindmap 视图:保留,用于在单个项目的任务里切换 -->
|
|
||||||
<record id="view_project_task_mindmap" model="ir.ui.view">
|
|
||||||
<field name="name">project.task.mindmap</field>
|
|
||||||
<field name="model">project.task</field>
|
|
||||||
<field name="type">mindmap</field>
|
|
||||||
<field name="arch" type="xml">
|
|
||||||
<mindmap/>
|
|
||||||
</field>
|
|
||||||
</record>
|
|
||||||
|
|
||||||
<!-- 项目级 mindmap 视图:用于"所有项目"动作里直接看所有项目 -->
|
|
||||||
<record id="view_project_project_mindmap" model="ir.ui.view">
|
|
||||||
<field name="name">project.project.mindmap</field>
|
|
||||||
<field name="model">project.project</field>
|
|
||||||
<field name="type">mindmap</field>
|
|
||||||
<field name="arch" type="xml">
|
|
||||||
<mindmap name_field="name"/>
|
|
||||||
</field>
|
|
||||||
</record>
|
|
||||||
|
|
||||||
<!-- 注入到标准"所有项目"动作,使顶部视图切换栏出现思维导图图标 -->
|
|
||||||
<record id="project.open_view_project_all" model="ir.actions.act_window">
|
|
||||||
<field name="view_mode">kanban,list,form,mindmap</field>
|
|
||||||
</record>
|
|
||||||
|
|
||||||
<record id="project.open_view_project_all_group_stage" model="ir.actions.act_window">
|
|
||||||
<field name="view_mode">kanban,list,form,mindmap</field>
|
|
||||||
</record>
|
|
||||||
|
|
||||||
<!-- 注入到标准"所有任务"动作,任务视图也能切换成导图 -->
|
|
||||||
<record id="project.action_view_all_task" model="ir.actions.act_window">
|
|
||||||
<field name="view_mode">kanban,list,form,calendar,pivot,graph,activity,mindmap</field>
|
|
||||||
</record>
|
|
||||||
</odoo>
|
|
||||||