wking-jie 8 months ago
commit c88e214145

@ -1,26 +1,31 @@
// //
.card-prod-info-btn{ .card-prod-info-btn {
font-size: 14px; font-size: 14px; // 14
.prod-name{
white-space: nowrap; .prod-name { //
text-overflow: ellipsis; white-space: nowrap; //
overflow: hidden; text-overflow: ellipsis; // ...
padding: 0 8px; overflow: hidden; // text-overflow使
padding: 0 8px; // 8使
} }
.del-btn{
line-height: 20px; .del-btn { //
text-align: right; line-height: 20px; // 20
padding-right: 8px; text-align: right; // 使
user-select: none; padding-right: 8px; // 8
span{ user-select: none; //
color: #155bd4;
cursor: pointer; span { // span
&:hover{ color: #155bd4; //
opacity: 0.8; cursor: pointer; //
&:hover { //
opacity: 0.8; // 80%
} }
&.disabled{
opacity: 0.6; &.disabled { // .disabled
cursor: not-allowed; opacity: 0.6; // 60%
cursor: not-allowed; //
} }
} }
} }

@ -1,114 +1,110 @@
/* 全局样式 */
/* 设置所有元素及其伪元素的盒模型为包含padding和border */
*, *,
*:before, *:before,
*:after { *:after {
box-sizing: border-box; box-sizing: border-box;
} }
/* 设置body的基础样式 */
body { body {
font-family: "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "微软雅黑", Arial, sans-serif; font-family: "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", Arial, sans-serif; /* 设置全局字体 */
font-size: 14px; font-size: 14px; /* 默认字体大小 */
line-height: 1.15; line-height: 1.15; /* 行高 */
color: #303133; color: #303133; /* 文字颜色 */
background-color: #fff; background-color: #fff; /* 背景颜色 */
} }
/* 设置链接的基础样式 */
a { a {
color: mix(#fff, $--color-primary, 20%); color: mix(#fff, $--color-primary, 20%); /* 使用Sass混合函数设置链接颜色 */
text-decoration: none; text-decoration: none; /* 移除默认下划线 */
&:focus, &:focus,
&:hover { &:hover { /* 当链接被聚焦或悬停时 */
color: $--color-primary; color: $--color-primary; /* 改变链接颜色 */
text-decoration: underline; text-decoration: underline; /* 添加下划线 */
} }
} }
/* 设置图片的基础样式 */
img { img {
vertical-align: middle; vertical-align: middle; /* 图片垂直对齐方式 */
max-width: 100%; max-width: 100%; /* 确保图片不会超出容器宽度 */
} }
.el-cascader-menu { /* 修改Element UI级联菜单滚动条的行为 */
.el-scrollbar__wrap { .el-cascader-menu .el-scrollbar__wrap {
overflow-y: auto !important; overflow-y: auto !important; /* 确保Y轴可以滚动 */
width: 100% !important; width: 100% !important; /* 滚动内容占据全部宽度 */
margin: 0 !important; margin: 0 !important; /* 移除默认边距 */
overflow: auto !important; overflow: auto !important; /* 确保溢出内容可见 */
}
} }
/* Utils /* 清除浮动 */
------------------------------ */
.clearfix:before, .clearfix:before,
.clearfix:after { .clearfix:after {
content: " "; content: " ";
display: table; display: table;
} }
.clearfix:after { .clearfix:after {
clear: both; clear: both; /* 清除浮动影响 */
} }
/* 定义淡入淡出动画 */
/* Animation
------------------------------ */
.fade-enter-active, .fade-enter-active,
.fade-leave-active { .fade-leave-active {
transition: opacity .5s; transition: opacity .5s; /* 动画过渡时间 */
} }
.fade-enter, .fade-enter,
.fade-leave-to { .fade-leave-to {
opacity: 0; opacity: 0; /* 初始和结束状态透明度 */
} }
/* 重置Element UI分页组件的文本对齐 */
/* Reset element-ui .site-wrapper .el-pagination {
------------------------------ */ text-align: right; /* 分页组件右对齐 */
.site-wrapper {
.el-pagination {
text-align: right;
}
} }
/* 布局 */
/* Layout
------------------------------ */
.site-wrapper { .site-wrapper {
position: relative; position: relative; /* 相对于自身定位 */
min-width: 1180px; min-width: 1180px; /* 最小宽度 */
} }
/* 侧边栏折叠时的样式调整 */
/* Sidebar fold
------------------------------ */
.site-sidebar--fold { .site-sidebar--fold {
.site-navbar__header, .site-navbar__header,
.site-navbar__brand, .site-navbar__brand,
.site-sidebar, .site-sidebar,
.site-sidebar__inner, .site-sidebar__inner,
.el-menu.site-sidebar__menu { .el-menu.site-sidebar__menu {
width: 64px; width: 64px; /* 折叠后的宽度 */
} }
.site-navbar__body, .site-navbar__body,
.site-content__wrapper { .site-content__wrapper {
margin-left: 64px; margin-left: 64px; /* 折叠后内容区的左边距 */
} }
.site-navbar__brand { .site-navbar__brand-lg {
&-lg { display: none; /* 隐藏大尺寸品牌标志 */
display: none; }
} .site-navbar__brand-mini {
&-mini { display: inline-block; /* 显示迷你品牌标志 */
display: inline-block;
}
} }
.site-sidebar, .site-sidebar,
.site-sidebar__inner { .site-sidebar__inner {
overflow: initial; overflow: initial; /* 恢复默认的溢出行为 */
} }
.site-sidebar__menu-icon { .site-sidebar__menu-icon {
margin-right: 0; margin-right: 0; /* 移除右边距 */
font-size: 20px; font-size: 20px; /* 字体大小 */
} }
.site-content--tabs > .el-tabs > .el-tabs__header { .site-content--tabs > .el-tabs > .el-tabs__header {
left: 64px; left: 64px; /* 标签头部左侧位置 */
} }
} }
// animation
/* 侧边栏、导航栏等元素在切换时添加动画效果 */
.site-navbar__header, .site-navbar__header,
.site-navbar__brand, .site-navbar__brand,
.site-navbar__body, .site-navbar__body,
@ -118,48 +114,46 @@ img {
.site-sidebar__menu-icon, .site-sidebar__menu-icon,
.site-content__wrapper, .site-content__wrapper,
.site-content--tabs > .el-tabs .el-tabs__header { .site-content--tabs > .el-tabs .el-tabs__header {
transition: inline-block .3s, left .3s, width .3s, margin-left .3s, font-size .3s; transition: inline-block .3s, left .3s, width .3s, margin-left .3s, font-size .3s; /* 动画过渡 */
} }
/* 导航栏样式 */
/* Navbar
------------------------------ */
.site-navbar { .site-navbar {
position: fixed; position: fixed; /* 固定定位 */
top: 0; top: 0;
right: 0; right: 0;
left: 0; left: 0;
z-index: 1030; z-index: 1030; /* 层级 */
height: 50px; height: 50px; /* 高度 */
box-shadow: 0 2px 4px rgba(0, 0, 0, .08); box-shadow: 0 2px 4px rgba(0, 0, 0, .08); /* 阴影效果 */
background-color: $navbar--background-color; background-color: $navbar--background-color; /* 背景颜色 */
&--inverse { &--inverse { /* 反色主题 */
.site-navbar__body { .site-navbar__body {
background-color: transparent; background-color: transparent; /* 逆向导航栏背景透明 */
} }
.el-menu { .el-menu {
> .el-menu-item, > .el-menu-item,
> .el-submenu > .el-submenu__title { > .el-submenu > .el-submenu__title {
color: #fff; color: #fff; /* 白色文字 */
&:focus, &:focus,
&:hover { &:hover {
color: #fff; color: #fff; /* 悬停时保持白色 */
background-color: mix(#000, $navbar--background-color, 15%); background-color: mix(#000, $navbar--background-color, 15%); /* 混合背景色 */
} }
} }
> .el-menu-item.is-active, > .el-menu-item.is-active,
> .el-submenu.is-active > .el-submenu__title { > .el-submenu.is-active > .el-submenu__title {
border-bottom-color: mix(#fff, $navbar--background-color, 85%); border-bottom-color: mix(#fff, $navbar--background-color, 85%); /* 活动项下划线颜色 */
} }
.el-menu-item i, .el-menu-item i,
.el-submenu__title i, .el-submenu__title i,
.el-dropdown { .el-dropdown {
color: #fff; color: #fff; /* 白色图标 */
} }
} }
.el-menu--popup-bottom-start { .el-menu--popup-bottom-start {
background-color: $navbar--background-color; background-color: $navbar--background-color; /* 下拉菜单背景 */
} }
} }
@ -168,7 +162,7 @@ img {
float: left; float: left;
width: 230px; width: 230px;
height: 50px; height: 50px;
overflow: hidden; overflow: hidden; /* 导航栏头部 */
} }
&__brand { &__brand {
display: table-cell; display: table-cell;
@ -181,7 +175,7 @@ img {
text-align: center; text-align: center;
text-transform: uppercase; text-transform: uppercase;
white-space: nowrap; white-space: nowrap;
color: #fff; color: #fff; /* 品牌名称颜色 */
&-lg, &-lg,
&-mini { &-mini {
@ -194,12 +188,12 @@ img {
} }
} }
&-mini { &-mini {
display: none; display: none; /* 默认隐藏迷你品牌 */
} }
} }
&__switch { &__switch {
font-size: 18px; font-size: 18px;
border-bottom: none !important; border-bottom: none !important; /* 移除底部边框 */
} }
&__avatar { &__avatar {
border-bottom: none !important; border-bottom: none !important;
@ -220,7 +214,7 @@ img {
position: relative; position: relative;
margin-left: 230px; margin-left: 230px;
padding-right: 15px; padding-right: 15px;
background-color: #fff; background-color: #fff; /* 导航栏主体背景 */
} }
&__menu { &__menu {
float: left; float: left;
@ -228,7 +222,7 @@ img {
border-bottom: 0; border-bottom: 0;
&--right { &--right {
float: right; float: right; /* 右对齐 */
} }
a:focus, a:focus,
a:hover { a:hover {
@ -252,9 +246,7 @@ img {
} }
} }
/* 侧边栏样式 */
/* Sidebar
------------------------------ */
.site-sidebar { .site-sidebar {
position: fixed; position: fixed;
top: 50px; top: 50px;
@ -266,7 +258,7 @@ img {
&--dark, &--dark,
&--dark-popper { &--dark-popper {
background-color: $sidebar--background-color-dark; background-color: $sidebar--background-color-dark; /* 深色背景 */
.site-sidebar__menu.el-menu, .site-sidebar__menu.el-menu,
> .el-menu--popup { > .el-menu--popup {
background-color: $sidebar--background-color-dark; background-color: $sidebar--background-color-dark;
@ -310,9 +302,7 @@ img {
} }
} }
/* 内容区样式 */
/* Content
------------------------------ */
.site-content { .site-content {
position: relative; position: relative;
padding: 15px; padding: 15px;
@ -365,6 +355,7 @@ img {
} }
} }
.element-error-message-zindex{ /* 提升错误信息的层级 */
z-index:3000 !important; .element-error-message-zindex {
z-index: 3000 !important; /* 确保错误信息显示在其他元素之上 */
} }

@ -2,14 +2,11 @@
/* Document /* Document
========================================================================== */ ========================================================================== */
/** /**
* 1. Correct the line height in all browsers. * 1.
* 2. Prevent adjustments of font size after orientation changes in * 2. Windows Phone IE iOS
* IE on Windows Phone and in iOS.
*/ */
html {
html {
line-height: 1.15; /* 1 */ line-height: 1.15; /* 1 */
-ms-text-size-adjust: 100%; /* 2 */ -ms-text-size-adjust: 100%; /* 2 */
-webkit-text-size-adjust: 100%; /* 2 */ -webkit-text-size-adjust: 100%; /* 2 */
@ -17,19 +14,16 @@
/* Sections /* Sections
========================================================================== */ ========================================================================== */
/** /**
* Remove the margin in all browsers (opinionated). * body
*/ */
body { body {
margin: 0; margin: 0;
} }
/** /**
* Add the correct display in IE 9-. * IE 9- HTML5
*/ */
article, article,
aside, aside,
footer, footer,
@ -40,10 +34,8 @@ section {
} }
/** /**
* Correct the font size and margin on `h1` elements within `section` and * Chrome, Firefox, Safari `h1` `section` `article`
* `article` contexts in Chrome, Firefox, and Safari.
*/ */
h1 { h1 {
font-size: 2em; font-size: 2em;
margin: 0.67em 0; margin: 0.67em 0;
@ -51,12 +43,9 @@ h1 {
/* Grouping content /* Grouping content
========================================================================== */ ========================================================================== */
/** /**
* Add the correct display in IE 9-. * 1. IE 9-
* 1. Add the correct display in IE.
*/ */
figcaption, figcaption,
figure, figure,
main { /* 1 */ main { /* 1 */
@ -64,18 +53,16 @@ main { /* 1 */
} }
/** /**
* Add the correct margin in IE 8. * IE 8 figure
*/ */
figure { figure {
margin: 1em 40px; margin: 1em 40px;
} }
/** /**
* 1. Add the correct box sizing in Firefox. * 1. Firefox hr
* 2. Show the overflow in Edge and IE. * 2. Edge IE
*/ */
hr { hr {
box-sizing: content-box; /* 1 */ box-sizing: content-box; /* 1 */
height: 0; /* 1 */ height: 0; /* 1 */
@ -83,10 +70,9 @@ hr {
} }
/** /**
* 1. Correct the inheritance and scaling of font size in all browsers. * 1.
* 2. Correct the odd `em` font sizing in all browsers. * 2. `em`
*/ */
pre { pre {
font-family: monospace, monospace; /* 1 */ font-family: monospace, monospace; /* 1 */
font-size: 1em; /* 2 */ font-size: 1em; /* 2 */
@ -94,22 +80,19 @@ pre {
/* Text-level semantics /* Text-level semantics
========================================================================== */ ========================================================================== */
/** /**
* 1. Remove the gray background on active links in IE 10. * 1. IE 10
* 2. Remove gaps in links underline in iOS 8+ and Safari 8+. * 2. iOS 8+ Safari 8+ 线
*/ */
a { a {
background-color: transparent; /* 1 */ background-color: transparent; /* 1 */
-webkit-text-decoration-skip: objects; /* 2 */ -webkit-text-decoration-skip: objects; /* 2 */
} }
/** /**
* 1. Remove the bottom border in Chrome 57- and Firefox 39-. * 1. Chrome 57- Firefox 39- h1
* 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari. * 2. Chrome, Edge, IE, Opera, Safari
*/ */
abbr[title] { abbr[title] {
border-bottom: none; /* 1 */ border-bottom: none; /* 1 */
text-decoration: underline; /* 2 */ text-decoration: underline; /* 2 */
@ -117,28 +100,25 @@ abbr[title] {
} }
/** /**
* Prevent the duplicate application of `bolder` by the next rule in Safari 6. * Safari 6 `bolder`
*/ */
b, b,
strong { strong {
font-weight: inherit; font-weight: inherit;
} }
/** /**
* Add the correct font weight in Chrome, Edge, and Safari. * Chrome, Edge, Safari
*/ */
b, b,
strong { strong {
font-weight: bolder; font-weight: bolder;
} }
/** /**
* 1. Correct the inheritance and scaling of font size in all browsers. * 1.
* 2. Correct the odd `em` font sizing in all browsers. * 2. `em`
*/ */
code, code,
kbd, kbd,
samp { samp {
@ -147,35 +127,30 @@ samp {
} }
/** /**
* Add the correct font style in Android 4.3-. * Android 4.3-
*/ */
dfn { dfn {
font-style: italic; font-style: italic;
} }
/** /**
* Add the correct background and color in IE 9-. * IE 9-
*/ */
mark { mark {
background-color: #ff0; background-color: #ff0;
color: #000; color: #000;
} }
/** /**
* Add the correct font size in all browsers. *
*/ */
small { small {
font-size: 80%; font-size: 80%;
} }
/** /**
* Prevent `sub` and `sup` elements from affecting the line height in * `sub` `sup`
* all browsers.
*/ */
sub, sub,
sup { sup {
font-size: 75%; font-size: 75%;
@ -194,49 +169,42 @@ sup {
/* Embedded content /* Embedded content
========================================================================== */ ========================================================================== */
/** /**
* Add the correct display in IE 9-. * IE 9-
*/ */
audio, audio,
video { video {
display: inline-block; display: inline-block;
} }
/** /**
* Add the correct display in iOS 4-7. * iOS 4-7
*/ */
audio:not([controls]) { audio:not([controls]) {
display: none; display: none;
height: 0; height: 0;
} }
/** /**
* Remove the border on images inside links in IE 10-. * IE 10-
*/ */
img { img {
border-style: none; border-style: none;
} }
/** /**
* Hide the overflow in IE. * IE SVG
*/ */
svg:not(:root) { svg:not(:root) {
overflow: hidden; overflow: hidden;
} }
/* Forms /* Forms
========================================================================== */ ========================================================================== */
/** /**
* 1. Change the font styles in all browsers (opinionated). * 1.
* 2. Remove the margin in Firefox and Safari. * 2. Firefox Safari
*/ */
button, button,
input, input,
optgroup, optgroup,
@ -249,31 +217,27 @@ textarea {
} }
/** /**
* Show the overflow in IE. * 1. IE
* 1. Show the overflow in Edge. * 2. Edge
*/ */
button, button,
input { /* 1 */ input { /* 1 */
overflow: visible; overflow: visible;
} }
/** /**
* Remove the inheritance of text transform in Edge, Firefox, and IE. * Edge, Firefox, IE
* 1. Remove the inheritance of text transform in Firefox. * 1. Firefox
*/ */
button, button,
select { /* 1 */ select { /* 1 */
text-transform: none; text-transform: none;
} }
/** /**
* 1. Prevent a WebKit bug where (2) destroys native `audio` and `video` * 1. WebKit bug Android 4 `audio` `video`
* controls in Android 4. * 2. iOS Safari
* 2. Correct the inability to style clickable types in iOS and Safari.
*/ */
button, button,
html [type="button"], /* 1 */ html [type="button"], /* 1 */
[type="reset"], [type="reset"],
@ -282,9 +246,8 @@ html [type="button"], /* 1 */
} }
/** /**
* Remove the inner border and padding in Firefox. * Firefox
*/ */
button::-moz-focus-inner, button::-moz-focus-inner,
[type="button"]::-moz-focus-inner, [type="button"]::-moz-focus-inner,
[type="reset"]::-moz-focus-inner, [type="reset"]::-moz-focus-inner,
@ -294,9 +257,8 @@ button::-moz-focus-inner,
} }
/** /**
* Restore the focus styles unset by the previous rule. * Firefox
*/ */
button:-moz-focusring, button:-moz-focusring,
[type="button"]:-moz-focusring, [type="button"]:-moz-focusring,
[type="reset"]:-moz-focusring, [type="reset"]:-moz-focusring,
@ -305,20 +267,17 @@ button:-moz-focusring,
} }
/** /**
* Correct the padding in Firefox. * Firefox fieldset
*/ */
fieldset { fieldset {
padding: 0.35em 0.75em 0.625em; padding: 0.35em 0.75em 0.625em;
} }
/** /**
* 1. Correct the text wrapping in Edge and IE. * 1. Edge IE
* 2. Correct the color inheritance from `fieldset` elements in IE. * 2. IE fieldset
* 3. Remove the padding so developers are not caught out when they zero out * 3. fieldset
* `fieldset` elements in all browsers.
*/ */
legend { legend {
box-sizing: border-box; /* 1 */ box-sizing: border-box; /* 1 */
color: inherit; /* 2 */ color: inherit; /* 2 */
@ -329,28 +288,25 @@ legend {
} }
/** /**
* 1. Add the correct display in IE 9-. * 1. IE 9-
* 2. Add the correct vertical alignment in Chrome, Firefox, and Opera. * 2. Chrome, Firefox, Opera
*/ */
progress { progress {
display: inline-block; /* 1 */ display: inline-block; /* 1 */
vertical-align: baseline; /* 2 */ vertical-align: baseline; /* 2 */
} }
/** /**
* Remove the default vertical scrollbar in IE. * IE textarea
*/ */
textarea { textarea {
overflow: auto; overflow: auto;
} }
/** /**
* 1. Add the correct box sizing in IE 10-. * 1. IE 10- checkbox radio
* 2. Remove the padding in IE 10-. * 2. IE 10- checkbox radio
*/ */
[type="checkbox"], [type="checkbox"],
[type="radio"] { [type="radio"] {
box-sizing: border-box; /* 1 */ box-sizing: border-box; /* 1 */
@ -358,38 +314,34 @@ textarea {
} }
/** /**
* Correct the cursor style of increment and decrement buttons in Chrome. * Chrome
*/ */
[type="number"]::-webkit-inner-spin-button, [type="number"]::-webkit-inner-spin-button,
[type="number"]::-webkit-outer-spin-button { [type="number"]::-webkit-outer-spin-button {
height: auto; height: auto;
} }
/** /**
* 1. Correct the odd appearance in Chrome and Safari. * 1. Chrome Safari
* 2. Correct the outline style in Safari. * 2. Safari
*/ */
[type="search"] { [type="search"] {
-webkit-appearance: textfield; /* 1 */ -webkit-appearance: textfield; /* 1 */
outline-offset: -2px; /* 2 */ outline-offset: -2px; /* 2 */
} }
/** /**
* Remove the inner padding and cancel buttons in Chrome and Safari on macOS. * macOS Chrome Safari
*/ */
[type="search"]::-webkit-search-cancel-button, [type="search"]::-webkit-search-cancel-button,
[type="search"]::-webkit-search-decoration { [type="search"]::-webkit-search-decoration {
-webkit-appearance: none; -webkit-appearance: none;
} }
/** /**
* 1. Correct the inability to style clickable types in iOS and Safari. * 1. iOS Safari
* 2. Change font properties to `inherit` in Safari. * 2. Safari
*/ */
::-webkit-file-upload-button { ::-webkit-file-upload-button {
-webkit-appearance: button; /* 1 */ -webkit-appearance: button; /* 1 */
font: inherit; /* 2 */ font: inherit; /* 2 */
@ -397,51 +349,43 @@ textarea {
/* Interactive /* Interactive
========================================================================== */ ========================================================================== */
/**
/* * 1. IE 9- details
* Add the correct display in IE 9-. * 2. Edge, IE, Firefox menu
* 1. Add the correct display in Edge, IE, and Firefox.
*/ */
details, /* 1 */ details, /* 1 */
menu { menu {
display: block; display: block;
} }
/* /**
* Add the correct display in all browsers. * summary
*/ */
summary { summary {
display: list-item; display: list-item;
} }
/* Scripting /* Scripting
========================================================================== */ ========================================================================== */
/** /**
* Add the correct display in IE 9-. * IE 9- canvas
*/ */
canvas { canvas {
display: inline-block; display: inline-block;
} }
/** /**
* Add the correct display in IE. * IE template
*/ */
template { template {
display: none; display: none;
} }
/* Hidden /* Hidden
========================================================================== */ ========================================================================== */
/** /**
* Add the correct display in IE 10-. * IE 10- hidden
*/ */
[hidden] { [hidden] {
display: none; display: none;
} }

@ -1,13 +1,13 @@
// //
// tips: , [$--color-primary][/src/element-ui-theme/index.js][import './element-[#17B3A3]/index.css'] // tips: , [$--color-primary][/src/element-ui-theme/index.js][import './element-#17B3A3/index.css']
$--color-primary: #02A1E9; $--color-primary: #02A1E9; // #02A1E9Element UI
// Navbar // Navbar ()
$navbar--background-color: $--color-primary; $navbar--background-color: $--color-primary; // 使使
// Sidebar // Sidebar ()
$sidebar--background-color-dark: #263238; $sidebar--background-color-dark: #263238; // #263238使
$sidebar--color-text-dark: #8a979e; $sidebar--color-text-dark: #8a979e; // #8a979e
// Content // Content ()
$content--background-color: #f1f4f5; $content--background-color: #f1f4f5; // #f1f4f5

@ -1,3 +1,9 @@
@import "normalize"; // api: https://github.com/necolas/normalize.css/ // Normalize.css
@import "variables"; // // api: https://github.com/necolas/normalize.css/
@import "base"; @import "normalize"; // Normalize.css CSS使Normalize.css
//
@import "variables"; // 使Sass便
//
@import "base"; //

@ -1,60 +1,75 @@
<template> <template>
<div> <div>
<!-- 图片上传组件 -->
<el-upload <el-upload
:action="uploadAction" :action="uploadAction" <!-- 设置上传的服务器地址 -->
:headers="uploadHeaders" :headers="uploadHeaders" <!-- 设置上传请求头包含认证信息 -->
list-type="picture-card" list-type="picture-card" <!-- 使用卡片列表显示上传的图片 -->
:on-preview="handlePictureCardPreview" :on-preview="handlePictureCardPreview" <!-- 点击预览图标时触发的事件 -->
:on-remove="handleRemove" :on-remove="handleRemove" <!-- 点击移除图标时触发的事件 -->
:on-success="handleUploadSuccess" :on-success="handleUploadSuccess" <!-- 文件上传成功后的回调 -->
:file-list="imageList" :file-list="imageList" <!-- 显示的文件列表 -->
:before-upload="beforeAvatarUpload" :before-upload="beforeAvatarUpload" <!-- 上传前的钩子函数用于限制文件类型和大小 -->
> >
<el-icon><Plus /></el-icon> <el-icon><Plus /></el-icon> <!-- -->
</el-upload> </el-upload>
<el-dialog v-model="dialogVisible">
<!-- 预览对话框 -->
<el-dialog v-model="dialogVisible"> <!-- dialogVisible -->
<img <img
width="100%" width="100%" <!-- 图片宽度为100%适应对话框大小 -->
:src="dialogImageUrl" :src="dialogImageUrl" <!-- 动态绑定预览图片的URL -->
alt="" alt=""
> >
</el-dialog> </el-dialog>
</div> </div>
</template> </template>
<script setup> <script setup>
import $cookie from 'vue-cookies' import $cookie from 'vue-cookies' // vue-cookiestoken
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus' // Element Plus
import { computed, ref } from 'vue' // VueAPI
// cookieAuthorization
const uploadHeaders = { Authorization: $cookie.get('Authorization') } const uploadHeaders = { Authorization: $cookie.get('Authorization') }
//
const uploadAction = http.adornUrl('/admin/file/upload/element') const uploadAction = http.adornUrl('/admin/file/upload/element')
//
const props = defineProps({ const props = defineProps({
modelValue: { modelValue: {
default: '', default: '', //
type: String type: String //
} }
}) })
// emit
const emit = defineEmits(['update:modelValue']) const emit = defineEmits(['update:modelValue'])
// URL
const resourcesUrl = import.meta.env.VITE_APP_RESOURCES_URL
// URL
const dialogImageUrl = ref('') const dialogImageUrl = ref('')
const dialogVisible = ref(false) const dialogVisible = ref(false)
const resourcesUrl = import.meta.env.VITE_APP_RESOURCES_URL
// modelValue
const imageList = computed(() => { const imageList = computed(() => {
const res = [] const res = []
if (props.modelValue) { if (props.modelValue) {
const imageArray = props.modelValue?.split(',') const imageArray = props.modelValue?.split(',') // modelValue
for (let i = 0; i < imageArray.length; i++) { for (let i = 0; i < imageArray.length; i++) {
res.push({ url: resourcesUrl + imageArray[i], response: imageArray[i] }) res.push({ url: resourcesUrl + imageArray[i], response: imageArray[i] })
} }
} }
emit('update:modelValue', props.modelValue) emit('update:modelValue', props.modelValue) // modelValue
return res return res
}) })
/** /**
* 图片上传 * 图片上传成功的处理函数
*/ */
// eslint-disable-next-line no-unused-vars
const handleUploadSuccess = (response, file, fileList) => { const handleUploadSuccess = (response, file, fileList) => {
const pics = fileList.map(file => { const pics = fileList.map(file => {
if (typeof file.response === 'string') { if (typeof file.response === 'string') {
@ -62,25 +77,29 @@ const handleUploadSuccess = (response, file, fileList) => {
} }
return file.response.data return file.response.data
}).join(',') }).join(',')
emit('update:modelValue', pics) emit('update:modelValue', pics) // modelValue
} }
/** /**
* 限制图片上传大小 * 上传前的钩子函数用于限制图片上传的格式和大小
*/ */
const beforeAvatarUpload = (file) => { const beforeAvatarUpload = (file) => {
const isJPG = file.type === 'image/jpeg' || file.type === 'image/png' || file.type === 'image/gif' || file.type === 'image/jpg' const isJPG = file.type === 'image/jpeg' || file.type === 'image/png' || file.type === 'image/gif' || file.type === 'image/jpg'
if (!isJPG) { if (!isJPG) {
ElMessage.error('上传图片只能是jpeg/jpg/png/gif 格式!') ElMessage.error('上传图片只能是jpeg/jpg/png/gif 格式!') //
return false
} }
const isLt2M = file.size / 1024 / 1024 < 2 const isLt2M = file.size / 1024 / 1024 < 2
if (!isLt2M) { if (!isLt2M) {
ElMessage.error('上传图片大小不能超过 2MB!') ElMessage.error('上传图片大小不能超过 2MB!') //
return false
} }
return isLt2M && isJPG return isLt2M && isJPG
} }
// eslint-disable-next-line no-unused-vars /**
* 处理移除图片的操作
*/
const handleRemove = (file, fileList) => { const handleRemove = (file, fileList) => {
const pics = fileList.map(file => { const pics = fileList.map(file => {
if (typeof file.response === 'string') { if (typeof file.response === 'string') {
@ -88,12 +107,14 @@ const handleRemove = (file, fileList) => {
} }
return file.response.data return file.response.data
}).join(',') }).join(',')
emit('update:modelValue', pics) emit('update:modelValue', pics) // modelValue
} }
/**
* 处理图片预览操作
*/
const handlePictureCardPreview = (file) => { const handlePictureCardPreview = (file) => {
dialogImageUrl.value = file.url dialogImageUrl.value = file.url // URL
dialogVisible.value = true dialogVisible.value = true //
} }
</script> </script>

@ -1,96 +1,109 @@
<template> <template>
<div> <div>
<!-- 图片上传组件 -->
<el-upload <el-upload
class="pic-uploader-component" class="pic-uploader-component" <!-- 自定义类名用于样式控制 -->
:action="uploadAction" :action="uploadAction" <!-- 设置上传的服务器地址 -->
:headers="uploadHeaders" :headers="uploadHeaders" <!-- 设置上传请求头包含认证信息 -->
accept=".png,.jpg,.jpeg,.gif" accept=".png,.jpg,.jpeg,.gif" <!-- 限制可接受的文件类型 -->
:show-file-list="false" :show-file-list="false" <!-- 不显示上传文件列表 -->
:on-success="handleUploadSuccess" :on-success="handleUploadSuccess" <!-- 文件上传成功后的回调 -->
:before-upload="beforeAvatarUpload" :before-upload="beforeAvatarUpload" <!-- 上传前的钩子函数用于限制文件类型和大小 -->
> >
<img <!-- 如果有图片则显示图片否则显示加号图标 -->
v-if="modelValue" <img
alt="" v-if="modelValue"
:src="checkFileUrl(modelValue)" alt=""
class="pic" :src="checkFileUrl(modelValue)" <!-- 动态绑定图片源 -->
> class="pic" <!-- 自定义类名用于样式控制 -->
<el-icon >
v-else <el-icon
color="#8c939d" v-else <!-- 如果没有图片则显示加号图标 -->
size="28" color="#8c939d"
> size="28"
<Plus /> >
</el-icon> <Plus /> <!-- 加号图标 -->
</el-icon>
</el-upload> </el-upload>
</div> </div>
</template> </template>
<script setup> <script setup>
import { checkFileUrl } from '@/utils' import { checkFileUrl } from '@/utils' // URL
import $cookie from 'vue-cookies' import $cookie from 'vue-cookies' // vue-cookiestoken
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus' // Element Plus
import { defineEmits, defineProps } from 'vue' // VueAPI
// cookieAuthorization
const uploadHeaders = { Authorization: $cookie.get('Authorization') } const uploadHeaders = { Authorization: $cookie.get('Authorization') }
//
const uploadAction = http.adornUrl('/admin/file/upload/element') const uploadAction = http.adornUrl('/admin/file/upload/element')
// emit
const emit = defineEmits(['update:modelValue']) const emit = defineEmits(['update:modelValue'])
// eslint-disable-next-line no-unused-vars
//
const props = defineProps({ const props = defineProps({
modelValue: { modelValue: {
default: '', default: '', //
type: String type: String //
} }
}) })
/** /**
* 图片上传 * 图片上传成功的处理函数
*/ */
// eslint-disable-next-line no-unused-vars
const handleUploadSuccess = (response, file) => { const handleUploadSuccess = (response, file) => {
emit('update:modelValue', file.response.data) emit('update:modelValue', file.response.data) // modelValue
} }
/** /**
* 限制图片上传大小 * 上传前的钩子函数用于限制图片上传的格式和大小
*/ */
const beforeAvatarUpload = (file) => { const beforeAvatarUpload = (file) => {
const isLt2M = file.size / 1024 / 1024 < 2 const isLt2M = file.size / 1024 / 1024 < 2 // 2MB
const isJPG = file.type === 'image/jpeg' || file.type === 'image/png' || file.type === 'image/gif' || file.type === 'image/jpg' const isJPG = file.type === 'image/jpeg' || file.type === 'image/png' || file.type === 'image/gif' || file.type === 'image/jpg'
if (!isJPG) { if (!isJPG) {
ElMessage.error('上传图片只能是jpeg/jpg/png/gif 格式!') ElMessage.error('上传图片只能是jpeg/jpg/png/gif 格式!') //
return false
} }
if (!isLt2M) { if (!isLt2M) {
ElMessage.error('上传图片大小不能超过 2MB!') ElMessage.error('上传图片大小不能超过 2MB!') //
return false
} }
return isLt2M && isJPG return isLt2M && isJPG //
} }
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
/* 使用SCSS编写样式并且只应用于当前组件 */
.pic-uploader-component :deep(.el-upload) { .pic-uploader-component :deep(.el-upload) {
border: 1px dashed #d9d9d9; border: 1px dashed #d9d9d9; /* 边框样式 */
border-radius: 6px; border-radius: 6px; /* 圆角边框 */
cursor: pointer; cursor: pointer; /* 鼠标悬停时指针样式 */
position: relative; position: relative; /* 相对定位 */
overflow: hidden; overflow: hidden; /* 超出内容隐藏 */
.pic-uploader-icon { .pic-uploader-icon {
font-size: 28px; font-size: 28px; /* 字体大小 */
color: #8c939d; color: #8c939d; /* 字体颜色 */
width: 178px; width: 178px;
height: 178px; height: 178px;
line-height: 178px; line-height: 178px; /* 行高 */
text-align: center; text-align: center; /* 文本居中 */
} }
.pic { .pic {
width: 178px; width: 178px; /* 图片宽度 */
height: 178px; height: 178px; /* 图片高度 */
display: block; display: block; /* 块级元素 */
} }
} }
.pic-uploader-component :deep(.el-upload:hover) { .pic-uploader-component :deep(.el-upload:hover) {
border-color: #409EFF; border-color: #409EFF; /* 悬停时边框颜色 */
} }
:deep(.el-upload) { :deep(.el-upload) {
width: 148px; width: 148px; /* 元素宽度 */
height: 148px; height: 148px; /* 元素高度 */
} }
</style> </style>

@ -1,205 +1,229 @@
<template> <template>
<el-dialog <el-dialog
v-model="visible" v-model="visible" <!-- 对话框的显示状态 -->
title="选择商品" title="选择商品" <!-- 对话框标题 -->
:modal="false" :modal="false" <!-- 是否需要遮罩层 -->
:close-on-click-modal="false" :close-on-click-modal="false" <!-- 点击模态框是否关闭对话框 -->
> >
<el-table <el-table
ref="prodTableRef" ref="prodTableRef" <!-- 表格引用 -->
v-loading="dataListLoading" v-loading="dataListLoading" <!-- 加载状态 -->
:data="dataList" :data="dataList" <!-- 数据源 -->
border border <!-- 显示边框 -->
style="width: 100%;" style="width: 100%;" <!-- 表格宽度 -->
@selection-change="selectChangeHandle" @selection-change="selectChangeHandle" <!-- 多选变化事件处理函数 -->
> >
<el-table-column <!-- 单选列 -->
v-if="isSingle" <el-table-column
width="50" v-if="isSingle" <!-- 根据props.isSingle决定是否显示 -->
header-align="center" width="50"
align="center" header-align="center"
> align="center"
<template #default="scope"> >
<div> <template #default="scope">
<el-radio <div>
v-model="singleSelectProdId" <el-radio
:label="scope.row.prodId" v-model="singleSelectProdId" <!-- 绑定单选值 -->
@change="getSelectProdRow(scope.row)" :label="scope.row.prodId" <!-- 单选标签为商品ID -->
> @change="getSelectProdRow(scope.row)"<!-- 单选变化事件处理函数 -->
&nbsp;
</el-radio>
</div>
</template>
</el-table-column>
<el-table-column
v-if="!isSingle"
type="selection"
header-align="center"
align="center"
width="50"
/>
<el-table-column
prop="prodName"
header-align="center"
align="center"
label="产品名称"
/>
<el-table-column
align="center"
width="140"
label="产品图片"
> >
<template #default="scope"> &nbsp;
<img </el-radio>
alt="" </div>
:src="scope.row.pic" </template>
width="100" </el-table-column>
height="100"
> <!-- 多选列 -->
</template> <el-table-column
</el-table-column> v-if="!isSingle" <!-- 根据props.isSingle决定是否显示 -->
</el-table> type="selection" <!-- 多选类型 -->
<el-pagination header-align="center"
:current-page="pageIndex" align="center"
:page-sizes="[10, 20, 50, 100]" width="50"
:page-size="pageSize" />
:total="totalPage"
layout="total, sizes, prev, pager, next, jumper" <!-- 产品名称列 -->
@size-change="sizeChangeHandle" <el-table-column
@current-change="currentChangeHandle" prop="prodName" <!-- 数据字段 -->
/> header-align="center"
<template #footer> align="center"
label="产品名称" <!-- 列标题 -->
/>
<!-- 产品图片列 -->
<el-table-column
align="center"
width="140"
label="产品图片" <!-- 列标题 -->
>
<template #default="scope">
<img
alt=""
:src="scope.row.pic" <!-- 动态绑定图片源 -->
width="100"
height="100"
>
</template>
</el-table-column>
</el-table>
<!-- 分页组件 -->
<el-pagination
:current-page="pageIndex" <!-- 当前页码 -->
:page-sizes="[10, 20, 50, 100]" <!-- 可选每页条数 -->
:page-size="pageSize" <!-- 默认每页条数 -->
:total="totalPage" <!-- 总记录数 -->
layout="total, sizes, prev, pager, next, jumper" <!-- 分页布局 -->
@size-change="sizeChangeHandle" <!-- 每页条数变化事件处理函数 -->
@current-change="currentChangeHandle" <!-- 当前页码变化事件处理函数 -->
/>
<!-- 对话框底部操作按钮 -->
<template #footer>
<span> <span>
<el-button @click="visible = false">取消</el-button> <el-button @click="visible = false">取消</el-button> <!-- -->
<el-button <el-button
type="primary" type="primary"
@click="submitProds()" @click="submitProds()" <!-- 提交选择的商品 -->
>确定</el-button> >确定</el-button>
</span> </span>
</template> </template>
</el-dialog> </el-dialog>
</template> </template>
<script setup> <script setup>
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus' // Element Plus
import { defineEmits, defineProps, ref, reactive, onMounted, nextTick } from 'vue'
import http from '@/utils/http'
// emit
const emit = defineEmits(['refreshSelectProds']) const emit = defineEmits(['refreshSelectProds'])
// eslint-disable-next-line no-unused-vars
//
const props = defineProps({ const props = defineProps({
isSingle: { isSingle: {
default: false, default: false, // false
type: Boolean type: Boolean //
} }
}) })
const visible = ref(false)
const dataForm = reactive({
product: ''
})
const singleSelectProdId = ref(0)
const selectProds = ref([])
const dataList = ref([])
const pageIndex = ref(1)
const pageSize = ref(10)
const totalPage = ref(0)
const dataListLoading = ref(false)
const dataListSelections = ref([])
//
const visible = ref(false) // /
const dataForm = reactive({ product: '' }) //
const singleSelectProdId = ref(0) // ID
const selectProds = ref([]) //
const dataList = ref([]) //
const pageIndex = ref(1) //
const pageSize = ref(10) //
const totalPage = ref(0) //
const dataListLoading = ref(false) //
const dataListSelections = ref([]) //
//
onMounted(() => { onMounted(() => {
getDataList() getDataList()
}) })
/** /**
* 获取数据列表 * 初始化并打开对话框
* @param selectProdParam - 已选择的商品参数
*/ */
const init = (selectProdParam) => { const init = (selectProdParam) => {
selectProds.value = selectProdParam selectProds.value = selectProdParam || [] //
visible.value = true visible.value = true //
dataListLoading.value = true dataListLoading.value = true //
if (selectProds.value) { if (selectProds.value.length) {
selectProds.value?.forEach(row => { selectProds.value.forEach(row => {
dataListSelections.value.push(row) dataListSelections.value.push(row) //
}) })
} }
getDataList() getDataList() //
} }
defineExpose({ init }) defineExpose({ init }) // init
//
const prodTableRef = ref(null) const prodTableRef = ref(null)
/**
* 获取数据列表
*/
const getDataList = () => { const getDataList = () => {
http({ http({
url: http.adornUrl('/prod/prod/page'), url: http.adornUrl('/prod/prod/page'), // URL
method: 'get', method: 'get', //
params: http.adornParams( params: http.adornParams(
Object.assign( Object.assign(
{ {
current: pageIndex.value, current: pageIndex.value, //
size: pageSize.value size: pageSize.value //
}, },
{ {
prodName: dataForm.prodName prodName: dataForm.product //
} }
) )
) )
}) }).then(({ data }) => {
.then(({ data }) => { dataList.value = data.records //
dataList.value = data.records totalPage.value = data.total //
totalPage.value = data.total dataListLoading.value = false //
dataListLoading.value = false if (selectProds.value.length) {
if (selectProds.value) { nextTick(() => {
nextTick(() => { selectProds.value.forEach(row => {
selectProds.value?.forEach(row => { const index = dataList.value.findIndex(prodItem => prodItem.prodId === row.prodId)
const index = dataList.value?.findIndex((prodItem) => prodItem.prodId === row.prodId) if (index !== -1) {
prodTableRef.value?.toggleRowSelection(dataList.value[index]) prodTableRef.value.toggleRowSelection(dataList.value[index], true) //
}) }
}) })
} })
}) }
})
} }
/** /**
* 每页 * 每页条数变化事件处理函
* @param val * @param val - 新的每页条数
*/ */
const sizeChangeHandle = (val) => { const sizeChangeHandle = (val) => {
pageSize.value = val pageSize.value = val //
pageIndex.value = 1 pageIndex.value = 1 //
getDataList() getDataList() //
} }
/** /**
* 当前页 * 当前页码变化事件处理函数
* @param val * @param val - 新的当前页码
*/ */
const currentChangeHandle = (val) => { const currentChangeHandle = (val) => {
pageIndex.value = val pageIndex.value = val //
getDataList() getDataList() //
} }
/** /**
* 单选商品事件 * 单选商品事件处理函数
* @param row * @param row - 被选择的商品行数据
*/ */
const getSelectProdRow = (row) => { const getSelectProdRow = (row) => {
dataListSelections.value = [row] dataListSelections.value = [row] //
} }
/** /**
* 多选点击事件 * 多选变化事件处理函数
* @param selection * @param selection - 当前选择的商品列表
*/ */
const selectChangeHandle = (selection) => { const selectChangeHandle = (selection) => {
dataList.value?.forEach((tableItem) => { dataList.value.forEach(tableItem => {
const selectedProdIndex = selection.findIndex((selectedProd) => { const selectedProdIndex = selection.findIndex(selectedProd => selectedProd && selectedProd.prodId === tableItem.prodId)
if (!selectedProd) { const dataSelectedProdIndex = dataListSelections.value.findIndex(dataSelectedProd => dataSelectedProd.prodId === tableItem.prodId)
return false
}
return selectedProd.prodId === tableItem.prodId
})
const dataSelectedProdIndex = dataListSelections.value?.findIndex((dataSelectedProd) => dataSelectedProd.prodId === tableItem.prodId)
if (selectedProdIndex > -1 && dataSelectedProdIndex === -1) { if (selectedProdIndex > -1 && dataSelectedProdIndex === -1) {
dataListSelections.value.push(tableItem) dataListSelections.value.push(tableItem) //
} else if (selectedProdIndex === -1 && dataSelectedProdIndex > -1) { } else if (selectedProdIndex === -1 && dataSelectedProdIndex > -1) {
dataListSelections.value.splice(dataSelectedProdIndex, 1) dataListSelections.value.splice(dataSelectedProdIndex, 1) //
} }
}) })
} }
/** /**
* 确定事件 * 确定事件处理函数
*/ */
const submitProds = () => { const submitProds = () => {
if (!dataListSelections.value.length) { if (!dataListSelections.value.length) {
@ -213,14 +237,14 @@ const submitProds = () => {
} }
const prods = [] const prods = []
dataListSelections.value.forEach(item => { dataListSelections.value.forEach(item => {
const prodIndex = prods.findIndex((prod) => prod.prodId === item.prodId) const prodIndex = prods.findIndex(prod => prod.prodId === item.prodId)
if (prodIndex === -1) { if (prodIndex === -1) {
prods.push({ prodId: item.prodId, prodName: item.prodName, pic: item.pic }) prods.push({ prodId: item.prodId, prodName: item.prodName, pic: item.pic }) //
} }
}) })
emit('refreshSelectProds', prods) emit('refreshSelectProds', prods) //
dataListSelections.value = [] dataListSelections.value = [] //
visible.value = false visible.value = false //
} }
</script> </script>

@ -1,58 +1,85 @@
// 定义一个数组来存储所有的回调函数,以便在脚本加载完成后依次调用它们。
let callbacks = [] let callbacks = []
function loadedTinymce () { /**
// to fixed https://github.com/PanJiaChen/vue-element-admin/issues/2144 * 检查是否已成功加载TinyMCE编辑器
// check is successfully downloaded script *
return window.tinymce * @returns {boolean} 如果window对象存在tinymce属性则返回true表示TinyMCE已加载否则返回false
*/
function loadedTinymce() {
// 解决 https://github.com/PanJiaChen/vue-element-admin/issues/2144 的问题
// 检查是否成功下载了脚本
return window.tinymce !== undefined
} }
/**
* 动态加载指定URL的脚本文件并在加载完成或发生错误时调用提供的回调函数
*
* @param {string} src - 脚本文件的URL
* @param {Function} [callback] - 可选参数当脚本加载完成或出错时将被调用的回调函数
*/
const dynamicLoadScript = (src, callback) => { const dynamicLoadScript = (src, callback) => {
const existingScript = document.getElementById(src) const existingScript = document.getElementById(src)
const cb = callback || function () {} const cb = callback || function () {} // 如果没有提供回调,则创建一个空函数作为默认值
if (!existingScript) { if (!existingScript) {
// 创建新的<script>元素并设置其src属性为传入的src参数。
const script = document.createElement('script') const script = document.createElement('script')
script.src = src // src url for the third-party library being loaded. script.src = src
script.id = src script.id = src // 使用src作为ID以避免重复加载同一脚本
document.body.appendChild(script) document.body.appendChild(script) // 将新创建的<script>元素添加到DOM中
callbacks.push(cb) callbacks.push(cb) // 将回调添加到callbacks数组中等待脚本加载完成后再执行
// 根据浏览器支持选择合适的事件监听方式
const onEnd = 'onload' in script ? stdOnEnd : ieOnEnd const onEnd = 'onload' in script ? stdOnEnd : ieOnEnd
onEnd(script) onEnd(script)
} }
if (existingScript && cb) { if (existingScript && cb) {
// 如果脚本已经存在于页面中并且提供了回调
if (loadedTinymce()) { if (loadedTinymce()) {
// 如果TinyMCE已加载则立即调用回调
cb(null, existingScript) cb(null, existingScript)
} else { } else {
// 否则将回调添加到队列中等待TinyMCE加载完成
callbacks.push(cb) callbacks.push(cb)
} }
} }
function stdOnEnd (script) { /**
* 处理标准浏览器中的脚本加载结束事件包括成功和失败
*
* @param {HTMLScriptElement} script - 当前的<script>元素
*/
function stdOnEnd(script) {
script.onload = function () { script.onload = function () {
// this.onload = null here is necessary // 在IE9及以下版本中this.onerror = this.onload = null 是必要的
// because even IE9 works not like others
this.onerror = this.onload = null this.onerror = this.onload = null
for (const cb of callbacks) { for (const cb of callbacks) {
cb(null, script) cb(null, script)
} }
callbacks = null callbacks = null // 清除回调列表以释放内存
} }
script.onerror = function () { script.onerror = function () {
document.body.removeChild(script) document.body.removeChild(script) // 移除加载失败的脚本元素
this.onerror = this.onload = null this.onerror = this.onload = null
cb(new Error('Failed to load ' + src), script) cb(new Error('Failed to load ' + src), script) // 执行回调并传递错误信息
} }
} }
function ieOnEnd (script) { /**
* 处理IE8等不支持onload事件的旧版浏览器中的脚本加载结束事件
*
* @param {HTMLScriptElement} script - 当前的<script>元素
*/
function ieOnEnd(script) {
script.onreadystatechange = function () { script.onreadystatechange = function () {
if (this.readyState !== 'complete' && this.readyState !== 'loaded') return if (this.readyState !== 'complete' && this.readyState !== 'loaded') return
this.onreadystatechange = null this.onreadystatechange = null
for (const cb of callbacks) { for (const cb of callbacks) {
cb(null, script) // there is no way to catch loading errors in IE8 cb(null, script) // IE8中无法捕获加载错误
} }
callbacks = null callbacks = null // 清除回调列表以释放内存
} }
} }
} }

@ -1,47 +1,54 @@
<template> <template>
<div class="components-tiny-mce"> <div class="components-tiny-mce">
<!-- 富文本编辑器容器 -->
<div <div
class="tinymce-container" class="tinymce-container"
:class="{ 'tox-fullscreen': toxFullscreen }" :class="{ 'tox-fullscreen': toxFullscreen }" <!-- 根据是否全屏调整样式 -->
> >
<textarea <!-- 编辑器的textarea元素用于初始化TinyMCE -->
:id="id" <textarea
class="tinymce-textarea" :id="id"
/> class="tinymce-textarea"
<!-- 增加图片区域 --> />
<div <!-- 图片上传区域 -->
class="add-or-upload" <div
class="add-or-upload"
>
<!-- 使用Element Plus的el-upload组件实现图片上传 -->
<el-upload
class="upload-demo"
list-type="picture" <!-- 列表类型为图片 -->
:action="uploadAction" <!-- 图片上传的目标URL -->
:headers="uploadHeaders" <!-- 自定义请求头例如包含授权信息 -->
:on-success="imageSuccessCBK" <!-- 图片上传成功后的回调函数 -->
:show-file-list="false" <!-- 是否显示已上传文件列表 -->
:before-upload="beforeAvatarUpload" <!-- 文件上传前的钩子函数 -->
>
<!-- 上传按钮 -->
<el-button
size="small"
type="primary"
> >
<el-upload 点击上传图片
class="upload-demo" </el-button>
list-type="picture" </el-upload>
:action="uploadAction"
:headers="uploadHeaders"
:on-success="imageSuccessCBK"
:show-file-list="false"
:before-upload="beforeAvatarUpload"
>
<el-button
size="small"
type="primary"
>
点击上传图片
</el-button>
</el-upload>
</div>
</div> </div>
</div> </div>
</div>
</template> </template>
<script setup> <script setup>
import $cookie from 'vue-cookies' import $cookie from 'vue-cookies' // vue-cookiescookie
import plugins from './plugins' import plugins from './plugins' // TinyMCE
import toolbarPar from './toolbar' import toolbarPar from './toolbar' // TinyMCE
import load from './dynamicLoadScript' import load from './dynamicLoadScript' //
// 使Authorizationcookie
const uploadHeaders = { Authorization: $cookie.get('Authorization') } const uploadHeaders = { Authorization: $cookie.get('Authorization') }
// API
const uploadAction = http.adornUrl('/admin/file/upload/element') const uploadAction = http.adornUrl('/admin/file/upload/element')
// props
const props = defineProps({ const props = defineProps({
modelValue: { modelValue: {
type: String, type: String,
@ -76,44 +83,52 @@ const props = defineProps({
} }
}) })
//
const toxFullscreen = ref(false) const toxFullscreen = ref(false)
let hasInit = false let hasInit = false
let hasChange = false let hasChange = false
// modelValue
watch(() => props.modelValue, (val) => { watch(() => props.modelValue, (val) => {
if (!hasChange && hasInit) { if (!hasChange && hasInit) {
setContent(val) setContent(val)
} }
}) })
//
const language = computed(() => { const language = computed(() => {
return localStorage.getItem('b2cLang') || 'zh_CN' return localStorage.getItem('b2cLang') || 'zh_CN'
}) })
// TinyMCE
watch(() => language.value, () => { watch(() => language.value, () => {
destroyTinymce() destroyTinymce()
nextTick(() => initTinymce()) nextTick(() => initTinymce())
}) })
//
onMounted(() => { onMounted(() => {
init() init()
}) })
onActivated(() => { onActivated(() => {
if (window.tinymce) { if (window.tinymce) {
initTinymce() initTinymce()
} }
}) })
onDeactivated(() => { onDeactivated(() => {
destroyTinymce() destroyTinymce()
}) })
onUnmounted(() => { onUnmounted(() => {
destroyTinymce() destroyTinymce()
}) })
//
const emit = defineEmits(['update:modelValue']) const emit = defineEmits(['update:modelValue'])
// TinyMCECDN
const resourceCdn1 = new URL('/static/js/tinymce/js/tinymce/tinymce.min.js', import.meta.url).href const resourceCdn1 = new URL('/static/js/tinymce/js/tinymce/tinymce.min.js', import.meta.url).href
// TinyMCE
const init = () => { const init = () => {
// dynamic load tinymce from cdn
load(resourceCdn1, (err) => { load(resourceCdn1, (err) => {
if (!err) { if (!err) {
initTinymce() initTinymce()
@ -121,95 +136,106 @@ const init = () => {
}) })
} }
// ID
const tinymceId = ref(props.id) const tinymceId = ref(props.id)
//
const fullscreen = ref(false) const fullscreen = ref(false)
// TinyMCE
const initTinymce = () => { const initTinymce = () => {
window.tinymce.init({ window.tinymce.init({
language: language.value, language: language.value, //
selector: `#${tinymceId.value}`, selector: `#${tinymceId.value}`, // DOM
height: props.height, height: props.height, //
body_class: 'panel-body ', body_class: 'panel-body ', // body
object_resizing: false, object_resizing: false, //
toolbar: props.toolbar.length > 0 ? props.toolbar : toolbarPar, toolbar: props.toolbar.length > 0 ? props.toolbar : toolbarPar, //
menubar: props.menubar, menubar: props.menubar, //
plugins, plugins, //
end_container_on_empty_block: true, end_container_on_empty_block: true, //
powerpaste_word_import: 'clean', powerpaste_word_import: 'clean', // Word
paste_enable_default_filters: false, // word paste_enable_default_filters: false, //
code_dialog_height: 450, code_dialog_height: 450, //
code_dialog_width: 1000, code_dialog_width: 1000, //
content_style: 'body {-webkit-user-modify: read-write;overflow-wrap: break-word;-webkit-line-break: after-white-space;}img {max-width: 100%;vertical-align:initial}', content_style: 'body {-webkit-user-modify: read-write;overflow-wrap: break-word;-webkit-line-break: after-white-space;}img {max-width: 100%;vertical-align:initial}', //
advlist_bullet_styles: 'square', advlist_bullet_styles: 'square', //
advlist_number_styles: 'default', advlist_number_styles: 'default', //
imagetools_cors_hosts: ['www.tinymce.com', 'codepen.io'], imagetools_cors_hosts: ['www.tinymce.com', 'codepen.io'], // CORS
default_link_target: '_blank', default_link_target: '_blank', //
link_title: false, link_title: false, //
nonbreaking_force_tab: true, // inserting nonbreaking space &nbsp; need Nonbreaking Space Plugin nonbreaking_force_tab: true, // Tab
init_instance_callback: (editor) => { init_instance_callback: (editor) => { //
if (props.modelValue) { if (props.modelValue) {
editor.setContent(props.modelValue) editor.setContent(props.modelValue) //
} }
hasInit = true hasInit = true
editor.on('NodeChange Change KeyUp SetContent', () => { editor.on('NodeChange Change KeyUp SetContent', () => {
hasChange = true hasChange = true
emit('update:modelValue', editor.getContent()) emit('update:modelValue', editor.getContent()) //
}) })
}, },
setup: (editor) => { setup: (editor) => { //
editor.on('FullscreenStateChanged', (e) => { editor.on('FullscreenStateChanged', (e) => {
fullscreen.value = e.state fullscreen.value = e.state //
}) })
}, },
convert_urls: false convert_urls: false // URL
}) })
} }
// TinyMCE
const destroyTinymce = () => { const destroyTinymce = () => {
try { try {
const tinymce = window.tinymce.get(tinymceId.value) const tinymce = window.tinymce.get(tinymceId.value)
if (fullscreen.value) { if (fullscreen.value) {
tinymce.execCommand('mceFullScreen') tinymce.execCommand('mceFullScreen') // 退
} }
if (tinymce) { if (tinymce) {
tinymce.destroy() tinymce.destroy() //
} }
} catch (e) { } } catch (e) { }
} }
//
const setContent = (value) => { const setContent = (value) => {
if (window.tinymce) { if (window.tinymce) {
window.tinymce.get(tinymceId.value).setContent(value || '') window.tinymce.get(tinymceId.value).setContent(value || '') //
} }
} }
// URL
const resourcesUrl = import.meta.env.VITE_APP_RESOURCES_URL const resourcesUrl = import.meta.env.VITE_APP_RESOURCES_URL
// eslint-disable-next-line no-unused-vars
//
const imageSuccessCBK = (response, file, fileList) => { const imageSuccessCBK = (response, file, fileList) => {
window.tinymce.get(props.id).insertContent(`<img alt="" src="${resourcesUrl + file.response.data}" >`) window.tinymce.get(props.id).insertContent(`<img alt="" src="${resourcesUrl + file.response.data}" >`) //
} }
/** /**
* 限制图片上传大小 * 限制图片上传大小及格式
*/ */
const beforeAvatarUpload = (file) => { const beforeAvatarUpload = (file) => {
const isJPG = file.type === 'image/jpeg' || file.type === 'image/png' || file.type === 'image/gif' || file.type === 'image/jpg' const isJPG = file.type === 'image/jpeg' || file.type === 'image/png' || file.type === 'image/gif' || file.type === 'image/jpg'
if (!isJPG) { if (!isJPG) {
this.$message.error('上传图片只能是jpeg/jpg/png/gif 格式!') this.$message.error('上传图片只能是jpeg/jpg/png/gif 格式!') //
} }
const isLt2M = file.size / 1024 / 1024 < 2 const isLt2M = file.size / 1024 / 1024 < 2
if (!isLt2M) { if (!isLt2M) {
this.$message.error('上传图片大小不能超过 2MB!') this.$message.error('上传图片大小不能超过 2MB!') //
} }
return isLt2M && isJPG return isLt2M && isJPG //
} }
</script> </script>
<!--eslint-disable-next-line vue-scoped-css/enforce-style-type -->
<style lang="scss"> <style lang="scss">
/* 定义组件的样式 */
.components-tiny-mce { .components-tiny-mce {
.tox-fullscreen .add-or-upload { .tox-fullscreen .add-or-upload {
z-index: 9999 !important; z-index: 9999 !important;
position:fixed !important; position: fixed !important;
right: 0; right: 0;
top: 0; top: 0;
} }
.tinymce-container { .tinymce-container {
position: relative; position: relative;

@ -1,7 +1,33 @@
// Any plugins you want to use has to be imported /**
// Detail plugins list see https://www.tinymce.com/docs/plugins/ * 定义TinyMCE编辑器中将要使用的插件
// Custom builds see https://www.tinymce.com/download/custom-builds/ *
* 注意任何想要使用的插件都必须在此处列出
const plugins = ['paste preview anchor autolink codesample emoticons image link lists media searchreplace table visualblocks wordcount pagebreak insertdatetime fullscreen code'] * 有关可用插件的完整列表请参阅官方文档
* - 插件详情https://www.tiny.cloud/docs/plugins/
* - 自定义构建https://www.tiny.cloud/docs/download/custom-builds/
*
* 每个插件名称代表一个功能模块它们可以单独或组合使用来增强编辑器的功能
*/
const plugins = [
'paste', // 允许用户从其他应用程序如Word粘贴内容并控制粘贴行为。例如可以选择保留或清理格式。
'preview', // 提供预览功能,允许用户在不离开编辑器的情况下查看最终渲染的内容,确保内容看起来是预期的样子。
'anchor', // 添加创建和编辑锚点的功能,用于在同一页面内创建可点击的链接,直接跳转到特定位置。
'autolink', // 自动检测文本中的URL并将其转换为超链接无需手动添加<a>标签。
'codesample', // 支持插入代码示例,包括语法高亮,用户可以选择不同的编程语言以应用相应的样式。
'emoticons', // 提供插入表情符号的功能,增加内容的情感表达。
'image', // 允许插入和编辑图片,支持上传、调整大小、对齐等操作。
'link', // 提供插入和编辑超链接的功能,用户可以轻松地向文本添加链接或修改现有链接。
'lists', // 支持有序(编号)和无序(项目符号)列表的创建和编辑。
'media', // 允许插入和编辑多媒体元素,如嵌入视频或音频文件。
'searchreplace', // 提供查找和替换文本的功能,方便用户快速修改文档内容。
'table', // 提供创建和编辑表格的功能,支持行、列的操作以及样式调整。
'visualblocks', // 显示HTML元素的边界框帮助用户可视化编辑器中的块级元素结构。
'wordcount', // 实时显示当前编辑器中字符数和字数统计,适用于需要遵循长度限制的内容。
'pagebreak', // 插入分页符,特别适用于打印长文档时,可以在适当的位置进行分页。
'insertdatetime',// 插入当前日期和时间,便于记录内容创建或更新的时间戳。
'fullscreen', // 允许切换到全屏模式进行编辑,提供更加沉浸式的编辑体验。
'code' // 显示和编辑原始HTML源代码适合高级用户或需要对生成的HTML有更多的控制权。
]
// 导出插件列表以便在初始化TinyMCE编辑器时引用。
export default plugins export default plugins

@ -1,6 +1,47 @@
// Here is a list of the toolbar /**
// Detail list see https://www.tinymce.com/docs/advanced/editor-control-identifiers/#toolbarcontrols * 定义TinyMCE编辑器工具栏中将要显示的按钮列表
*
const toolbar = ['searchreplace bold italic underline strikethrough alignleft aligncenter alignright outdent indent blockquote undo redo removeformat subscript superscript code codesample hr numlist link image preview anchor pagebreak insertdatetime media table emoticons forecolor backcolor fullscreen'] * 注意工具栏中的每一个条目代表一个功能按钮或命令
* 用户可以通过点击这些按钮来执行相应的操作
* 有关可用工具栏控件的完整列表请参阅官方文档
* - 工具栏控件详情https://www.tiny.cloud/docs/advanced/editor-control-identifiers/#toolbarcontrols
*
* 每个工具栏控件名称代表一个特定的功能模块它们可以单独或组合使用来增强编辑器的功能
*/
const toolbar = [
'searchreplace', // 提供查找和替换文本的功能,方便用户快速修改文档内容。
'bold', // 加粗选中的文本。
'italic', // 斜体选中的文本。
'underline', // 下划线选中的文本。
'strikethrough', // 删除线选中的文本。
'alignleft', // 将选中的文本左对齐。
'aligncenter', // 将选中的文本居中对齐。
'alignright', // 将选中的文本右对齐。
'outdent', // 减少选中文本的缩进级别。
'indent', // 增加选中文本的缩进级别。
'blockquote', // 创建或移除引用块(通常用于引用其他来源的内容)。
'undo', // 撤销上一步操作。
'redo', // 重做已撤销的操作。
'removeformat', // 移除选中文本的所有格式。
'subscript', // 设置选中的文本为下标。
'superscript', // 设置选中的文本为上标。
'code', // 切换到代码视图允许直接编辑HTML源代码。
'codesample', // 插入代码示例,支持多种编程语言的语法高亮。
'hr', // 插入水平线,用于分隔内容。
'numlist', // 插入有序列表(编号列表)。
'link', // 插入或编辑超链接。
'image', // 插入或编辑图片。
'preview', // 预览编辑器内容,确保最终效果符合预期。
'anchor', // 插入锚点,用于在同一页面内创建可点击的链接。
'pagebreak', // 插入分页符,特别适用于打印长文档时进行分页。
'insertdatetime', // 插入当前日期和时间。
'media', // 插入多媒体元素,如视频或音频文件。
'table', // 插入或编辑表格。
'emoticons', // 插入表情符号,增加内容的情感表达。
'forecolor', // 更改选中文本的颜色。
'backcolor', // 更改选中文本背景颜色。
'fullscreen' // 切换到全屏模式进行编辑,提供更加沉浸式的编辑体验。
]
// 导出工具栏配置以便在初始化TinyMCE编辑器时引用。
export default toolbar export default toolbar

@ -1,91 +1,92 @@
import { defineConfig, loadEnv } from 'vite' import { defineConfig, loadEnv } from 'vite'; // 导入 Vite 的定义配置函数和环境变量加载函数
import vue from '@vitejs/plugin-vue' import vue from '@vitejs/plugin-vue'; // 导入 Vue 插件,使 Vite 支持 .vue 文件
import path from 'path' import path from 'path'; // Node.js 标准库,用于处理和转换文件路径
import AutoImport from 'unplugin-auto-import/vite' import AutoImport from 'unplugin-auto-import/vite'; // 自动导入 API 插件
import Components from 'unplugin-vue-components/vite' import Components from 'unplugin-vue-components/vite'; // 自动注册组件插件
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers' import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'; // 组件解析器,用于自动导入 Element Plus 组件
import { createSvgIconsPlugin } from 'vite-plugin-svg-icons' import { createSvgIconsPlugin } from 'vite-plugin-svg-icons'; // SVG 图标插件,将 SVG 文件转化为 Vue 组件
import viteCompression from 'vite-plugin-compression' import viteCompression from 'vite-plugin-compression'; // 压缩构建输出文件的插件
// eslint // ESLint 插件,用于在开发期间进行代码检查
import eslintPlugin from 'vite-plugin-eslint' import eslintPlugin from 'vite-plugin-eslint';
// https://vitejs.dev/config/ export default defineConfig(({ command }) => ({
export default defineConfig(({ command })=> { plugins: [
return { vue(), // 启用 Vue 插件
plugins: [
vue(), // 创建 SVG 图标插件实例,配置图标目录和生成的 symbol ID 模板
createSvgIconsPlugin({ createSvgIconsPlugin({
iconDirs: [path.resolve(process.cwd(), 'src/icons/svg')], iconDirs: [path.resolve(process.cwd(), 'src/icons/svg')],
symbolId: 'icon-[dir]-[name]' symbolId: 'icon-[dir]-[name]'
}), }),
// 自动引入内容
AutoImport({ // 配置 AutoImport 插件以自动导入指定模块中的内容
imports: [ AutoImport({
'vue', imports: ['vue', 'vue-router'], // 指定要自动导入的模块
'vue-router' dirs: ['src/hooks/**', 'src/stores/**', 'src/utils/**'], // 包含自定义钩子、状态管理、工具函数的目录
], resolvers: command === 'build' ? [ElementPlusResolver()] : [], // 构建时使用 ElementPlusResolver 解析器
dirs: [ dts: 'src/auto-import/imports.d.ts', // 自动生成类型声明文件的位置
'src/hooks/**', eslintrc: {
'src/stores/**', enabled: false // 禁用对 .eslintrc 文件的自动更新
'src/utils/**'
],
resolvers: command === 'build' ? [ElementPlusResolver()] : [],
dts: 'src/auto-import/imports.d.ts',
eslintrc: {
enabled: false
}
}),
// 自动引入组件
Components({
dirs: [
'src/components'
],
resolvers: command === 'build' ? [ElementPlusResolver()] : [],
dts: 'src/auto-import/components.d.ts'
}),
// 对大于 1k 的文件进行压缩
viteCompression({
threshold: 1000,
})
].concat(
// eslint
command !== 'build' ? [eslintPlugin({ include: ['src/**/*.js', 'src/**/*.vue', 'src/*.js', 'src/*.vue'] })] : []
),
server: {
host: true,
port: 9527,
open: true
},
resolve: {
alias: {
'@': path.resolve(__dirname, 'src'),
'vue-i18n': 'vue-i18n/dist/vue-i18n.cjs.js'
} }
}, }),
build: {
base: './', // 自动注册来自特定目录的 Vue 组件,并可选地使用解析器来处理第三方库中的组件
rollupOptions: { Components({
// 静态资源分类打包 dirs: ['src/components'], // 包含组件的目录
output: { resolvers: command === 'build' ? [ElementPlusResolver()] : [], // 构建时使用 ElementPlusResolver 解析器
chunkFileNames: 'static/js/[name]-[hash].js', dts: 'src/auto-import/components.d.ts' // 自动生成类型声明文件的位置
entryFileNames: 'static/js/[name]-[hash].js', }),
assetFileNames: 'static/[ext]/[name]-[hash].[ext]',
// 静态资源分拆打包 // 使用 ESLint 插件检查 JavaScript 和 Vue 文件
manualChunks (id) { eslintPlugin({
if (id.includes('node_modules')) { include: ['src/**/*.js', 'src/**/*.vue', 'src/*.js', 'src/*.vue'] // 指定要检查的文件模式
if (id.toString().indexOf('.pnpm/') !== -1) { }),
return id.toString().split('.pnpm/')[1].split('/')[0].toString();
} else if (id.toString().indexOf('node_modules/') !== -1) { // 对超过 1KB 的文件启用 gzip 或 brotli 压缩
return id.toString().split('node_modules/')[1].split('/')[0].toString(); viteCompression({
} threshold: 1000,
})
],
// 开发服务器配置
server: {
host: true, // 允许外部访问
port: 9527, // 设置服务端口
open: true // 在启动时自动打开浏览器
},
// 路径别名配置
resolve: {
alias: {
'@': path.resolve(__dirname, 'src'), // 将 @ 替换为 src 目录的绝对路径
'vue-i18n': 'vue-i18n/dist/vue-i18n.cjs.js' // 强制使用兼容性更好的 vue-i18n 版本
}
},
// 构建选项
build: {
base: './', // 构建后的资源基础路径
rollupOptions: {
output: {
chunkFileNames: 'static/js/[name]-[hash].js', // 动态导入的代码分割块命名规则
entryFileNames: 'static/js/[name]-[hash].js', // 入口文件命名规则
assetFileNames: 'static/[ext]/[name]-[hash].[ext]', // 静态资源(如图片、字体等)命名规则
// 手动拆分 chunks通常用于优化包大小和缓存策略
manualChunks(id) {
if (id.includes('node_modules')) {
if (id.toString().indexOf('.pnpm/') !== -1) {
return id.toString().split('.pnpm/')[1].split('/')[0].toString();
} else if (id.toString().indexOf('node_modules/') !== -1) {
return id.toString().split('node_modules/')[1].split('/')[0].toString();
} }
} }
} }
}, }
sourcemap: false, },
target: 'es2015', sourcemap: false, // 是否生成 source map 文件
reportCompressedSize: false target: 'es2015', // 设置打包后代码的目标浏览器或 Node.js 版本
} reportCompressedSize: false // 不报告压缩后的包大小
} }
}) }));

Loading…
Cancel
Save