Merge pull request '1.2' (#3) from qjj into main

main
pvtfxms7o 2 months ago
commit 32943d6f8e

@ -1,12 +1,18 @@
<template>
<el-config-provider
namespace="el"
>
<!-- 使用Element Plus的配置提供者组件来包裹应用的主要内容 -->
<el-config-provider namespace="el">
<!-- 路由视图组件它是Vue Router的一部分用于渲染与当前路由匹配的组件 -->
<router-view />
</el-config-provider>
</template>
<!--
下面的样式部分使用SCSS预处理器并且引入了一个全局样式文件
注意这里的注释指出了一个ESLint规则被禁用这可能是为了允许在scoped样式中使用特定的style类型`lang="scss"`
-->
<!-- eslint-disable-next-line vue-scoped-css/enforce-style-type -->
<style lang="scss">
// 使@useSCSSapp.scss
// mixin
@use '@/assets/app.scss';
</style>

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

@ -1,114 +1,110 @@
/* 全局样式 */
/* 设置所有元素及其伪元素的盒模型为包含padding和border */
*,
*:before,
*:after {
box-sizing: border-box;
}
/* 设置body的基础样式 */
body {
font-family: "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "微软雅黑", Arial, sans-serif;
font-size: 14px;
line-height: 1.15;
color: #303133;
background-color: #fff;
font-family: "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", Arial, sans-serif; /* 设置全局字体 */
font-size: 14px; /* 默认字体大小 */
line-height: 1.15; /* 行高 */
color: #303133; /* 文字颜色 */
background-color: #fff; /* 背景颜色 */
}
/* 设置链接的基础样式 */
a {
color: mix(#fff, $--color-primary, 20%);
text-decoration: none;
color: mix(#fff, $--color-primary, 20%); /* 使用Sass混合函数设置链接颜色 */
text-decoration: none; /* 移除默认下划线 */
&:focus,
&:hover {
color: $--color-primary;
text-decoration: underline;
&:hover { /* 当链接被聚焦或悬停时 */
color: $--color-primary; /* 改变链接颜色 */
text-decoration: underline; /* 添加下划线 */
}
}
/* 设置图片的基础样式 */
img {
vertical-align: middle;
max-width: 100%;
vertical-align: middle; /* 图片垂直对齐方式 */
max-width: 100%; /* 确保图片不会超出容器宽度 */
}
.el-cascader-menu {
.el-scrollbar__wrap {
overflow-y: auto !important;
width: 100% !important;
margin: 0 !important;
overflow: auto !important;
}
/* 修改Element UI级联菜单滚动条的行为 */
.el-cascader-menu .el-scrollbar__wrap {
overflow-y: auto !important; /* 确保Y轴可以滚动 */
width: 100% !important; /* 滚动内容占据全部宽度 */
margin: 0 !important; /* 移除默认边距 */
overflow: auto !important; /* 确保溢出内容可见 */
}
/* Utils
------------------------------ */
/* 清除浮动 */
.clearfix:before,
.clearfix:after {
content: " ";
display: table;
}
.clearfix:after {
clear: both;
clear: both; /* 清除浮动影响 */
}
/* Animation
------------------------------ */
/* 定义淡入淡出动画 */
.fade-enter-active,
.fade-leave-active {
transition: opacity .5s;
transition: opacity .5s; /* 动画过渡时间 */
}
.fade-enter,
.fade-leave-to {
opacity: 0;
opacity: 0; /* 初始和结束状态透明度 */
}
/* Reset element-ui
------------------------------ */
.site-wrapper {
.el-pagination {
text-align: right;
}
/* 重置Element UI分页组件的文本对齐 */
.site-wrapper .el-pagination {
text-align: right; /* 分页组件右对齐 */
}
/* Layout
------------------------------ */
/* 布局 */
.site-wrapper {
position: relative;
min-width: 1180px;
position: relative; /* 相对于自身定位 */
min-width: 1180px; /* 最小宽度 */
}
/* Sidebar fold
------------------------------ */
/* 侧边栏折叠时的样式调整 */
.site-sidebar--fold {
.site-navbar__header,
.site-navbar__brand,
.site-sidebar,
.site-sidebar__inner,
.el-menu.site-sidebar__menu {
width: 64px;
width: 64px; /* 折叠后的宽度 */
}
.site-navbar__body,
.site-content__wrapper {
margin-left: 64px;
margin-left: 64px; /* 折叠后内容区的左边距 */
}
.site-navbar__brand {
&-lg {
display: none;
}
&-mini {
display: inline-block;
}
.site-navbar__brand-lg {
display: none; /* 隐藏大尺寸品牌标志 */
}
.site-navbar__brand-mini {
display: inline-block; /* 显示迷你品牌标志 */
}
.site-sidebar,
.site-sidebar__inner {
overflow: initial;
overflow: initial; /* 恢复默认的溢出行为 */
}
.site-sidebar__menu-icon {
margin-right: 0;
font-size: 20px;
margin-right: 0; /* 移除右边距 */
font-size: 20px; /* 字体大小 */
}
.site-content--tabs > .el-tabs > .el-tabs__header {
left: 64px;
left: 64px; /* 标签头部左侧位置 */
}
}
// animation
/* 侧边栏、导航栏等元素在切换时添加动画效果 */
.site-navbar__header,
.site-navbar__brand,
.site-navbar__body,
@ -118,48 +114,46 @@ img {
.site-sidebar__menu-icon,
.site-content__wrapper,
.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 {
position: fixed;
position: fixed; /* 固定定位 */
top: 0;
right: 0;
left: 0;
z-index: 1030;
height: 50px;
box-shadow: 0 2px 4px rgba(0, 0, 0, .08);
background-color: $navbar--background-color;
z-index: 1030; /* 层级 */
height: 50px; /* 高度 */
box-shadow: 0 2px 4px rgba(0, 0, 0, .08); /* 阴影效果 */
background-color: $navbar--background-color; /* 背景颜色 */
&--inverse {
&--inverse { /* 反色主题 */
.site-navbar__body {
background-color: transparent;
background-color: transparent; /* 逆向导航栏背景透明 */
}
.el-menu {
> .el-menu-item,
> .el-submenu > .el-submenu__title {
color: #fff;
color: #fff; /* 白色文字 */
&:focus,
&:hover {
color: #fff;
background-color: mix(#000, $navbar--background-color, 15%);
color: #fff; /* 悬停时保持白色 */
background-color: mix(#000, $navbar--background-color, 15%); /* 混合背景色 */
}
}
> .el-menu-item.is-active,
> .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-submenu__title i,
.el-dropdown {
color: #fff;
color: #fff; /* 白色图标 */
}
}
.el-menu--popup-bottom-start {
background-color: $navbar--background-color;
background-color: $navbar--background-color; /* 下拉菜单背景 */
}
}
@ -168,7 +162,7 @@ img {
float: left;
width: 230px;
height: 50px;
overflow: hidden;
overflow: hidden; /* 导航栏头部 */
}
&__brand {
display: table-cell;
@ -181,7 +175,7 @@ img {
text-align: center;
text-transform: uppercase;
white-space: nowrap;
color: #fff;
color: #fff; /* 品牌名称颜色 */
&-lg,
&-mini {
@ -194,12 +188,12 @@ img {
}
}
&-mini {
display: none;
display: none; /* 默认隐藏迷你品牌 */
}
}
&__switch {
font-size: 18px;
border-bottom: none !important;
border-bottom: none !important; /* 移除底部边框 */
}
&__avatar {
border-bottom: none !important;
@ -220,7 +214,7 @@ img {
position: relative;
margin-left: 230px;
padding-right: 15px;
background-color: #fff;
background-color: #fff; /* 导航栏主体背景 */
}
&__menu {
float: left;
@ -228,7 +222,7 @@ img {
border-bottom: 0;
&--right {
float: right;
float: right; /* 右对齐 */
}
a:focus,
a:hover {
@ -252,9 +246,7 @@ img {
}
}
/* Sidebar
------------------------------ */
/* 侧边栏样式 */
.site-sidebar {
position: fixed;
top: 50px;
@ -266,7 +258,7 @@ img {
&--dark,
&--dark-popper {
background-color: $sidebar--background-color-dark;
background-color: $sidebar--background-color-dark; /* 深色背景 */
.site-sidebar__menu.el-menu,
> .el-menu--popup {
background-color: $sidebar--background-color-dark;
@ -310,9 +302,7 @@ img {
}
}
/* Content
------------------------------ */
/* 内容区样式 */
.site-content {
position: relative;
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
========================================================================== */
/**
* 1. Correct the line height in all browsers.
* 2. Prevent adjustments of font size after orientation changes in
* IE on Windows Phone and in iOS.
* 1.
* 2. Windows Phone IE iOS
*/
html {
html {
line-height: 1.15; /* 1 */
-ms-text-size-adjust: 100%; /* 2 */
-webkit-text-size-adjust: 100%; /* 2 */
@ -17,19 +14,16 @@
/* Sections
========================================================================== */
/**
* Remove the margin in all browsers (opinionated).
* body
*/
body {
margin: 0;
}
/**
* Add the correct display in IE 9-.
* IE 9- HTML5
*/
article,
aside,
footer,
@ -40,10 +34,8 @@ section {
}
/**
* Correct the font size and margin on `h1` elements within `section` and
* `article` contexts in Chrome, Firefox, and Safari.
* Chrome, Firefox, Safari `h1` `section` `article`
*/
h1 {
font-size: 2em;
margin: 0.67em 0;
@ -51,12 +43,9 @@ h1 {
/* Grouping content
========================================================================== */
/**
* Add the correct display in IE 9-.
* 1. Add the correct display in IE.
* 1. IE 9-
*/
figcaption,
figure,
main { /* 1 */
@ -64,18 +53,16 @@ main { /* 1 */
}
/**
* Add the correct margin in IE 8.
* IE 8 figure
*/
figure {
margin: 1em 40px;
}
/**
* 1. Add the correct box sizing in Firefox.
* 2. Show the overflow in Edge and IE.
* 1. Firefox hr
* 2. Edge IE
*/
hr {
box-sizing: content-box; /* 1 */
height: 0; /* 1 */
@ -83,10 +70,9 @@ hr {
}
/**
* 1. Correct the inheritance and scaling of font size in all browsers.
* 2. Correct the odd `em` font sizing in all browsers.
* 1.
* 2. `em`
*/
pre {
font-family: monospace, monospace; /* 1 */
font-size: 1em; /* 2 */
@ -94,22 +80,19 @@ pre {
/* Text-level semantics
========================================================================== */
/**
* 1. Remove the gray background on active links in IE 10.
* 2. Remove gaps in links underline in iOS 8+ and Safari 8+.
* 1. IE 10
* 2. iOS 8+ Safari 8+ 线
*/
a {
background-color: transparent; /* 1 */
-webkit-text-decoration-skip: objects; /* 2 */
}
/**
* 1. Remove the bottom border in Chrome 57- and Firefox 39-.
* 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.
* 1. Chrome 57- Firefox 39- h1
* 2. Chrome, Edge, IE, Opera, Safari
*/
abbr[title] {
border-bottom: none; /* 1 */
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,
strong {
font-weight: inherit;
}
/**
* Add the correct font weight in Chrome, Edge, and Safari.
* Chrome, Edge, Safari
*/
b,
strong {
font-weight: bolder;
}
/**
* 1. Correct the inheritance and scaling of font size in all browsers.
* 2. Correct the odd `em` font sizing in all browsers.
* 1.
* 2. `em`
*/
code,
kbd,
samp {
@ -147,35 +127,30 @@ samp {
}
/**
* Add the correct font style in Android 4.3-.
* Android 4.3-
*/
dfn {
font-style: italic;
}
/**
* Add the correct background and color in IE 9-.
* IE 9-
*/
mark {
background-color: #ff0;
color: #000;
}
/**
* Add the correct font size in all browsers.
*
*/
small {
font-size: 80%;
}
/**
* Prevent `sub` and `sup` elements from affecting the line height in
* all browsers.
* `sub` `sup`
*/
sub,
sup {
font-size: 75%;
@ -194,49 +169,42 @@ sup {
/* Embedded content
========================================================================== */
/**
* Add the correct display in IE 9-.
* IE 9-
*/
audio,
video {
display: inline-block;
}
/**
* Add the correct display in iOS 4-7.
* iOS 4-7
*/
audio:not([controls]) {
display: none;
height: 0;
}
/**
* Remove the border on images inside links in IE 10-.
* IE 10-
*/
img {
border-style: none;
}
/**
* Hide the overflow in IE.
* IE SVG
*/
svg:not(:root) {
overflow: hidden;
}
/* Forms
========================================================================== */
/**
* 1. Change the font styles in all browsers (opinionated).
* 2. Remove the margin in Firefox and Safari.
* 1.
* 2. Firefox Safari
*/
button,
input,
optgroup,
@ -249,31 +217,27 @@ textarea {
}
/**
* Show the overflow in IE.
* 1. Show the overflow in Edge.
* 1. IE
* 2. Edge
*/
button,
input { /* 1 */
overflow: visible;
}
/**
* Remove the inheritance of text transform in Edge, Firefox, and IE.
* 1. Remove the inheritance of text transform in Firefox.
* Edge, Firefox, IE
* 1. Firefox
*/
button,
select { /* 1 */
text-transform: none;
}
/**
* 1. Prevent a WebKit bug where (2) destroys native `audio` and `video`
* controls in Android 4.
* 2. Correct the inability to style clickable types in iOS and Safari.
* 1. WebKit bug Android 4 `audio` `video`
* 2. iOS Safari
*/
button,
html [type="button"], /* 1 */
[type="reset"],
@ -282,9 +246,8 @@ html [type="button"], /* 1 */
}
/**
* Remove the inner border and padding in Firefox.
* Firefox
*/
button::-moz-focus-inner,
[type="button"]::-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,
[type="button"]:-moz-focusring,
[type="reset"]:-moz-focusring,
@ -305,20 +267,17 @@ button:-moz-focusring,
}
/**
* Correct the padding in Firefox.
* Firefox fieldset
*/
fieldset {
padding: 0.35em 0.75em 0.625em;
}
/**
* 1. Correct the text wrapping in Edge and IE.
* 2. Correct the color inheritance from `fieldset` elements in IE.
* 3. Remove the padding so developers are not caught out when they zero out
* `fieldset` elements in all browsers.
* 1. Edge IE
* 2. IE fieldset
* 3. fieldset
*/
legend {
box-sizing: border-box; /* 1 */
color: inherit; /* 2 */
@ -329,28 +288,25 @@ legend {
}
/**
* 1. Add the correct display in IE 9-.
* 2. Add the correct vertical alignment in Chrome, Firefox, and Opera.
* 1. IE 9-
* 2. Chrome, Firefox, Opera
*/
progress {
display: inline-block; /* 1 */
vertical-align: baseline; /* 2 */
}
/**
* Remove the default vertical scrollbar in IE.
* IE textarea
*/
textarea {
overflow: auto;
}
/**
* 1. Add the correct box sizing in IE 10-.
* 2. Remove the padding in IE 10-.
* 1. IE 10- checkbox radio
* 2. IE 10- checkbox radio
*/
[type="checkbox"],
[type="radio"] {
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-outer-spin-button {
height: auto;
}
/**
* 1. Correct the odd appearance in Chrome and Safari.
* 2. Correct the outline style in Safari.
* 1. Chrome Safari
* 2. Safari
*/
[type="search"] {
-webkit-appearance: textfield; /* 1 */
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-decoration {
-webkit-appearance: none;
}
/**
* 1. Correct the inability to style clickable types in iOS and Safari.
* 2. Change font properties to `inherit` in Safari.
* 1. iOS Safari
* 2. Safari
*/
::-webkit-file-upload-button {
-webkit-appearance: button; /* 1 */
font: inherit; /* 2 */
@ -397,51 +349,43 @@ textarea {
/* Interactive
========================================================================== */
/*
* Add the correct display in IE 9-.
* 1. Add the correct display in Edge, IE, and Firefox.
/**
* 1. IE 9- details
* 2. Edge, IE, Firefox menu
*/
details, /* 1 */
menu {
display: block;
}
/*
* Add the correct display in all browsers.
/**
* summary
*/
summary {
display: list-item;
}
/* Scripting
========================================================================== */
/**
* Add the correct display in IE 9-.
* IE 9- canvas
*/
canvas {
display: inline-block;
}
/**
* Add the correct display in IE.
* IE template
*/
template {
display: none;
}
/* Hidden
========================================================================== */
/**
* Add the correct display in IE 10-.
* IE 10- hidden
*/
[hidden] {
display: none;
}
}

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

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

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

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

@ -1,205 +1,229 @@
<template>
<el-dialog
v-model="visible"
title="选择商品"
:modal="false"
:close-on-click-modal="false"
v-model="visible" <!-- 对话框的显示状态 -->
title="选择商品" <!-- 对话框标题 -->
:modal="false" <!-- 是否需要遮罩层 -->
:close-on-click-modal="false" <!-- 点击模态框是否关闭对话框 -->
>
<el-table
ref="prodTableRef"
v-loading="dataListLoading"
:data="dataList"
border
style="width: 100%;"
@selection-change="selectChangeHandle"
>
<el-table-column
v-if="isSingle"
width="50"
header-align="center"
align="center"
>
<template #default="scope">
<div>
<el-radio
v-model="singleSelectProdId"
:label="scope.row.prodId"
@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="产品图片"
<el-table
ref="prodTableRef" <!-- 表格引用 -->
v-loading="dataListLoading" <!-- 加载状态 -->
:data="dataList" <!-- 数据源 -->
border <!-- 显示边框 -->
style="width: 100%;" <!-- 表格宽度 -->
@selection-change="selectChangeHandle" <!-- 多选变化事件处理函数 -->
>
<!-- 单选列 -->
<el-table-column
v-if="isSingle" <!-- 根据props.isSingle决定是否显示 -->
width="50"
header-align="center"
align="center"
>
<template #default="scope">
<div>
<el-radio
v-model="singleSelectProdId" <!-- 绑定单选值 -->
:label="scope.row.prodId" <!-- 单选标签为商品ID -->
@change="getSelectProdRow(scope.row)"<!-- 单选变化事件处理函数 -->
>
<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>
&nbsp;
</el-radio>
</div>
</template>
</el-table-column>
<!-- 多选列 -->
<el-table-column
v-if="!isSingle" <!-- 根据props.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">
<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>
<el-button @click="visible = false">取消</el-button>
<el-button @click="visible = false">取消</el-button> <!-- -->
<el-button
type="primary"
@click="submitProds()"
@click="submitProds()" <!-- 提交选择的商品 -->
>确定</el-button>
</span>
</template>
</template>
</el-dialog>
</template>
<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'])
// eslint-disable-next-line no-unused-vars
//
const props = defineProps({
isSingle: {
default: false,
type: Boolean
default: false, // false
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(() => {
getDataList()
})
/**
* 获取数据列表
* 初始化并打开对话框
* @param selectProdParam - 已选择的商品参数
*/
const init = (selectProdParam) => {
selectProds.value = selectProdParam
visible.value = true
dataListLoading.value = true
if (selectProds.value) {
selectProds.value?.forEach(row => {
dataListSelections.value.push(row)
selectProds.value = selectProdParam || [] //
visible.value = true //
dataListLoading.value = true //
if (selectProds.value.length) {
selectProds.value.forEach(row => {
dataListSelections.value.push(row) //
})
}
getDataList()
getDataList() //
}
defineExpose({ init })
defineExpose({ init }) // init
//
const prodTableRef = ref(null)
/**
* 获取数据列表
*/
const getDataList = () => {
http({
url: http.adornUrl('/prod/prod/page'),
method: 'get',
url: http.adornUrl('/prod/prod/page'), // URL
method: 'get', //
params: http.adornParams(
Object.assign(
{
current: pageIndex.value,
size: pageSize.value
current: pageIndex.value, //
size: pageSize.value //
},
{
prodName: dataForm.prodName
prodName: dataForm.product //
}
)
)
})
.then(({ data }) => {
dataList.value = data.records
totalPage.value = data.total
dataListLoading.value = false
if (selectProds.value) {
nextTick(() => {
selectProds.value?.forEach(row => {
const index = dataList.value?.findIndex((prodItem) => prodItem.prodId === row.prodId)
prodTableRef.value?.toggleRowSelection(dataList.value[index])
})
}).then(({ data }) => {
dataList.value = data.records //
totalPage.value = data.total //
dataListLoading.value = false //
if (selectProds.value.length) {
nextTick(() => {
selectProds.value.forEach(row => {
const index = dataList.value.findIndex(prodItem => prodItem.prodId === row.prodId)
if (index !== -1) {
prodTableRef.value.toggleRowSelection(dataList.value[index], true) //
}
})
}
})
})
}
})
}
/**
* 每页
* @param val
* 每页条数变化事件处理函
* @param val - 新的每页条数
*/
const sizeChangeHandle = (val) => {
pageSize.value = val
pageIndex.value = 1
getDataList()
pageSize.value = val //
pageIndex.value = 1 //
getDataList() //
}
/**
* 当前页
* @param val
* 当前页码变化事件处理函数
* @param val - 新的当前页码
*/
const currentChangeHandle = (val) => {
pageIndex.value = val
getDataList()
pageIndex.value = val //
getDataList() //
}
/**
* 单选商品事件
* @param row
* 单选商品事件处理函数
* @param row - 被选择的商品行数据
*/
const getSelectProdRow = (row) => {
dataListSelections.value = [row]
dataListSelections.value = [row] //
}
/**
* 多选点击事件
* @param selection
* 多选变化事件处理函数
* @param selection - 当前选择的商品列表
*/
const selectChangeHandle = (selection) => {
dataList.value?.forEach((tableItem) => {
const selectedProdIndex = selection.findIndex((selectedProd) => {
if (!selectedProd) {
return false
}
return selectedProd.prodId === tableItem.prodId
})
const dataSelectedProdIndex = dataListSelections.value?.findIndex((dataSelectedProd) => dataSelectedProd.prodId === tableItem.prodId)
dataList.value.forEach(tableItem => {
const selectedProdIndex = selection.findIndex(selectedProd => selectedProd && selectedProd.prodId === tableItem.prodId)
const dataSelectedProdIndex = dataListSelections.value.findIndex(dataSelectedProd => dataSelectedProd.prodId === tableItem.prodId)
if (selectedProdIndex > -1 && dataSelectedProdIndex === -1) {
dataListSelections.value.push(tableItem)
dataListSelections.value.push(tableItem) //
} else if (selectedProdIndex === -1 && dataSelectedProdIndex > -1) {
dataListSelections.value.splice(dataSelectedProdIndex, 1)
dataListSelections.value.splice(dataSelectedProdIndex, 1) //
}
})
}
/**
* 确定事件
* 确定事件处理函数
*/
const submitProds = () => {
if (!dataListSelections.value.length) {
@ -213,14 +237,14 @@ const submitProds = () => {
}
const prods = []
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) {
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)
dataListSelections.value = []
visible.value = false
emit('refreshSelectProds', prods) //
dataListSelections.value = [] //
visible.value = false //
}
</script>

@ -1,58 +1,85 @@
// 定义一个数组来存储所有的回调函数,以便在脚本加载完成后依次调用它们。
let callbacks = []
function loadedTinymce () {
// to fixed https://github.com/PanJiaChen/vue-element-admin/issues/2144
// check is successfully downloaded script
return window.tinymce
/**
* 检查是否已成功加载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 existingScript = document.getElementById(src)
const cb = callback || function () {}
const cb = callback || function () {} // 如果没有提供回调,则创建一个空函数作为默认值
if (!existingScript) {
// 创建新的<script>元素并设置其src属性为传入的src参数。
const script = document.createElement('script')
script.src = src // src url for the third-party library being loaded.
script.id = src
document.body.appendChild(script)
callbacks.push(cb)
script.src = src
script.id = src // 使用src作为ID以避免重复加载同一脚本
document.body.appendChild(script) // 将新创建的<script>元素添加到DOM中
callbacks.push(cb) // 将回调添加到callbacks数组中等待脚本加载完成后再执行
// 根据浏览器支持选择合适的事件监听方式
const onEnd = 'onload' in script ? stdOnEnd : ieOnEnd
onEnd(script)
}
if (existingScript && cb) {
// 如果脚本已经存在于页面中并且提供了回调
if (loadedTinymce()) {
// 如果TinyMCE已加载则立即调用回调
cb(null, existingScript)
} else {
// 否则将回调添加到队列中等待TinyMCE加载完成
callbacks.push(cb)
}
}
function stdOnEnd (script) {
/**
* 处理标准浏览器中的脚本加载结束事件包括成功和失败
*
* @param {HTMLScriptElement} script - 当前的<script>元素
*/
function stdOnEnd(script) {
script.onload = function () {
// this.onload = null here is necessary
// because even IE9 works not like others
// 在IE9及以下版本中this.onerror = this.onload = null 是必要的
this.onerror = this.onload = null
for (const cb of callbacks) {
cb(null, script)
}
callbacks = null
callbacks = null // 清除回调列表以释放内存
}
script.onerror = function () {
document.body.removeChild(script)
document.body.removeChild(script) // 移除加载失败的脚本元素
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 () {
if (this.readyState !== 'complete' && this.readyState !== 'loaded') return
this.onreadystatechange = null
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>
<div class="components-tiny-mce">
<!-- 富文本编辑器容器 -->
<div
class="tinymce-container"
:class="{ 'tox-fullscreen': toxFullscreen }"
:class="{ 'tox-fullscreen': toxFullscreen }" <!-- 根据是否全屏调整样式 -->
>
<textarea
:id="id"
class="tinymce-textarea"
/>
<!-- 增加图片区域 -->
<div
class="add-or-upload"
<!-- 编辑器的textarea元素用于初始化TinyMCE -->
<textarea
:id="id"
class="tinymce-textarea"
/>
<!-- 图片上传区域 -->
<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"
list-type="picture"
: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>
点击上传图片
</el-button>
</el-upload>
</div>
</div>
</div>
</template>
<script setup>
import $cookie from 'vue-cookies'
import plugins from './plugins'
import toolbarPar from './toolbar'
import load from './dynamicLoadScript'
import $cookie from 'vue-cookies' // vue-cookiescookie
import plugins from './plugins' // TinyMCE
import toolbarPar from './toolbar' // TinyMCE
import load from './dynamicLoadScript' //
// 使Authorizationcookie
const uploadHeaders = { Authorization: $cookie.get('Authorization') }
// API
const uploadAction = http.adornUrl('/admin/file/upload/element')
// props
const props = defineProps({
modelValue: {
type: String,
@ -76,44 +83,52 @@ const props = defineProps({
}
})
//
const toxFullscreen = ref(false)
let hasInit = false
let hasChange = false
// modelValue
watch(() => props.modelValue, (val) => {
if (!hasChange && hasInit) {
setContent(val)
}
})
//
const language = computed(() => {
return localStorage.getItem('b2cLang') || 'zh_CN'
})
// TinyMCE
watch(() => language.value, () => {
destroyTinymce()
nextTick(() => initTinymce())
})
//
onMounted(() => {
init()
})
onActivated(() => {
if (window.tinymce) {
initTinymce()
}
})
onDeactivated(() => {
destroyTinymce()
})
onUnmounted(() => {
destroyTinymce()
})
//
const emit = defineEmits(['update:modelValue'])
// TinyMCECDN
const resourceCdn1 = new URL('/static/js/tinymce/js/tinymce/tinymce.min.js', import.meta.url).href
// TinyMCE
const init = () => {
// dynamic load tinymce from cdn
load(resourceCdn1, (err) => {
if (!err) {
initTinymce()
@ -121,95 +136,106 @@ const init = () => {
})
}
// ID
const tinymceId = ref(props.id)
//
const fullscreen = ref(false)
// TinyMCE
const initTinymce = () => {
window.tinymce.init({
language: language.value,
selector: `#${tinymceId.value}`,
height: props.height,
body_class: 'panel-body ',
object_resizing: false,
toolbar: props.toolbar.length > 0 ? props.toolbar : toolbarPar,
menubar: props.menubar,
plugins,
end_container_on_empty_block: true,
powerpaste_word_import: 'clean',
paste_enable_default_filters: false, // word
code_dialog_height: 450,
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}',
advlist_bullet_styles: 'square',
advlist_number_styles: 'default',
imagetools_cors_hosts: ['www.tinymce.com', 'codepen.io'],
default_link_target: '_blank',
link_title: false,
nonbreaking_force_tab: true, // inserting nonbreaking space &nbsp; need Nonbreaking Space Plugin
init_instance_callback: (editor) => {
language: language.value, //
selector: `#${tinymceId.value}`, // DOM
height: props.height, //
body_class: 'panel-body ', // body
object_resizing: false, //
toolbar: props.toolbar.length > 0 ? props.toolbar : toolbarPar, //
menubar: props.menubar, //
plugins, //
end_container_on_empty_block: true, //
powerpaste_word_import: 'clean', // Word
paste_enable_default_filters: false, //
code_dialog_height: 450, //
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}', //
advlist_bullet_styles: 'square', //
advlist_number_styles: 'default', //
imagetools_cors_hosts: ['www.tinymce.com', 'codepen.io'], // CORS
default_link_target: '_blank', //
link_title: false, //
nonbreaking_force_tab: true, // Tab
init_instance_callback: (editor) => { //
if (props.modelValue) {
editor.setContent(props.modelValue)
editor.setContent(props.modelValue) //
}
hasInit = true
editor.on('NodeChange Change KeyUp SetContent', () => {
hasChange = true
emit('update:modelValue', editor.getContent())
emit('update:modelValue', editor.getContent()) //
})
},
setup: (editor) => {
setup: (editor) => { //
editor.on('FullscreenStateChanged', (e) => {
fullscreen.value = e.state
fullscreen.value = e.state //
})
},
convert_urls: false
convert_urls: false // URL
})
}
// TinyMCE
const destroyTinymce = () => {
try {
const tinymce = window.tinymce.get(tinymceId.value)
if (fullscreen.value) {
tinymce.execCommand('mceFullScreen')
tinymce.execCommand('mceFullScreen') // 退
}
if (tinymce) {
tinymce.destroy()
tinymce.destroy() //
}
} catch (e) { }
}
//
const setContent = (value) => {
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
// eslint-disable-next-line no-unused-vars
//
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 isJPG = file.type === 'image/jpeg' || file.type === 'image/png' || file.type === 'image/gif' || file.type === 'image/jpg'
if (!isJPG) {
this.$message.error('上传图片只能是jpeg/jpg/png/gif 格式!')
this.$message.error('上传图片只能是jpeg/jpg/png/gif 格式!') //
}
const isLt2M = file.size / 1024 / 1024 < 2
if (!isLt2M) {
this.$message.error('上传图片大小不能超过 2MB!')
this.$message.error('上传图片大小不能超过 2MB!') //
}
return isLt2M && isJPG
return isLt2M && isJPG //
}
</script>
<!--eslint-disable-next-line vue-scoped-css/enforce-style-type -->
<style lang="scss">
/* 定义组件的样式 */
.components-tiny-mce {
.tox-fullscreen .add-or-upload {
z-index: 9999 !important;
position:fixed !important;
position: fixed !important;
right: 0;
top: 0;
}
.tinymce-container {
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/
// 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']
/**
* 定义TinyMCE编辑器中将要使用的插件
*
* 注意任何想要使用的插件都必须在此处列出
* 有关可用插件的完整列表请参阅官方文档
* - 插件详情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

@ -1,6 +1,47 @@
// Here is a list of the toolbar
// Detail list see https://www.tinymce.com/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 codesample hr numlist link image preview anchor pagebreak insertdatetime media table emoticons forecolor backcolor fullscreen']
/**
* 定义TinyMCE编辑器工具栏中将要显示的按钮列表
*
* 注意工具栏中的每一个条目代表一个功能按钮或命令
* 用户可以通过点击这些按钮来执行相应的操作
* 有关可用工具栏控件的完整列表请参阅官方文档
* - 工具栏控件详情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

File diff suppressed because one or more lines are too long

@ -1,61 +1,63 @@
<template>
<div style="position: relative"
>
<div class="verify-img-out">
<div class="verify-img-panel" :style="{'width': setSize.imgWidth,
'height': setSize.imgHeight,
'background-size' : setSize.imgWidth + ' '+ setSize.imgHeight,
'margin-bottom': vSpace + 'px'}"
>
<div class="verify-refresh" style="z-index:3" @click="refresh" v-show="showRefresh">
<i class="iconfont icon-refresh"></i>
</div>
<img :src="'data:image/png;base64,'+pointBackImgBase"
ref="canvas"
alt="" style="width:100%;height:100%;display:block"
@click="bindingClick?canvasClick($event):undefined">
<div v-for="(tempPoint, index) in tempPoints" :key="index" class="point-area"
:style="{
'background-color':'#1abd6c',
color:'#fff',
'z-index':9999,
width:'20px',
height:'20px',
'text-align':'center',
'line-height':'20px',
'border-radius': '50%',
position:'absolute',
top:parseInt(tempPoint.y-10) + 'px',
left:parseInt(tempPoint.x-10) + 'px'
}">
{{index + 1}}
</div>
</div>
<div style="position: relative">
<!-- 验证码图片容器 -->
<div class="verify-img-out">
<div class="verify-img-panel" :style="{
'width': setSize.imgWidth,
'height': setSize.imgHeight,
'background-size': setSize.imgWidth + ' ' + setSize.imgHeight,
'margin-bottom': vSpace + 'px'
}">
<!-- 刷新按钮 -->
<div class="verify-refresh" style="z-index:3" @click="refresh" v-show="showRefresh">
<i class="iconfont icon-refresh"></i>
</div>
<!-- 'height': this.barSize.height, -->
<div class="verify-bar-area"
:style="{'width': setSize.imgWidth,
'color': this.barAreaColor,
'border-color': this.barAreaBorderColor,
'line-height':this.barSize.height}">
<span class="verify-msg">{{text}}</span>
<!-- 背景图片 -->
<img :src="'data:image/png;base64,'+pointBackImgBase"
ref="canvas"
alt="" style="width:100%; height:100%; display:block"
@click="bindingClick ? canvasClick($event) : undefined">
<!-- 点击位置标记 -->
<div v-for="(tempPoint, index) in tempPoints" :key="index" class="point-area"
:style="{
'background-color': '#1abd6c',
color: '#fff',
'z-index': 9999,
width: '20px',
height: '20px',
'text-align': 'center',
'line-height': '20px',
'border-radius': '50%',
position: 'absolute',
top: parseInt(tempPoint.y - 10) + 'px',
left: parseInt(tempPoint.x - 10) + 'px'
}">
{{ index + 1 }}
</div>
</div>
</div>
<!-- 验证条区域 -->
<div class="verify-bar-area"
:style="{'width': setSize.imgWidth, 'color': barAreaColor, 'border-color': barAreaBorderColor, 'line-height': barSize.height}">
<span class="verify-msg">{{ text }}</span>
</div>
</div>
</template>
<script type="text/babel">
/**
* VerifyPoints
* @description 点选
* */
import { resetSize, _code_chars, _code_color1, _code_color2 } from './../utils/util'
import { aesEncrypt } from './../utils/ase'
import { reqGet, reqCheck } from './../api/index'
import { computed, onMounted, reactive, ref, watch, nextTick, toRefs, watchEffect, getCurrentInstance } from 'vue'
* VerifyPoints 组件
* @description 实现点选验证码功能
*/
import {resetSize, _code_chars, _code_color1, _code_color2} from './../utils/util';
import {aesEncrypt} from './../utils/ase';
import {reqGet, reqCheck} from './../api/index';
import {computed, onMounted, reactive, ref, watch, nextTick, toRefs, watchEffect, getCurrentInstance} from 'vue';
export default {
name: 'VerifyPoints',
props: {
// popfixed
// popfixed
mode: {
type: String,
default: 'fixed'
@ -63,174 +65,224 @@ export default {
captchaType: {
type: String
},
//
//
vSpace: {
type: Number,
default: 5
},
//
imgSize: {
type: Object,
default () {
default() {
return {
width: '310px',
height: '155px'
}
};
}
},
//
barSize: {
type: Object,
default () {
default() {
return {
width: '310px',
height: '40px'
}
};
}
}
},
setup (props, context) {
const { mode, captchaType, vSpace, imgSize, barSize } = toRefs(props)
const { proxy } = getCurrentInstance()
const secretKey = ref('') // ase
const checkNum = ref(3) //
const fontPos = reactive([]) //
const checkPosArr = reactive([]) //
const num = ref(1) //
const pointBackImgBase = ref('') //
const poinTextList = reactive([]) //
const backToken = ref('') // token
setup(props, context) {
const {mode, captchaType, vSpace, imgSize, barSize} = toRefs(props);
const {proxy} = getCurrentInstance();
// AES
const secretKey = ref('');
//
const checkNum = ref(3);
//
const fontPos = reactive([]);
//
const checkPosArr = reactive([]);
//
const num = ref(1);
// base64
const pointBackImgBase = ref('');
//
const poinTextList = reactive([]);
// token
const backToken = ref('');
//
const setSize = reactive({
imgHeight: 0,
imgWidth: 0,
barHeight: 0,
barWidth: 0
})
const tempPoints = reactive([])
const text = ref('')
const barAreaColor = ref(undefined)
const barAreaBorderColor = ref(undefined)
const showRefresh = ref(true)
const bindingClick = ref(true)
});
//
const tempPoints = reactive([]);
//
const text = ref('');
//
const barAreaColor = ref(undefined);
//
const barAreaBorderColor = ref(undefined);
//
const showRefresh = ref(true);
//
const bindingClick = ref(true);
/**
* 初始化组件
*/
const init = () => {
//
fontPos.splice(0, fontPos.length)
checkPosArr.splice(0, checkPosArr.length)
num.value = 1
getPictrue()
//
fontPos.splice(0, fontPos.length);
checkPosArr.splice(0, checkPosArr.length);
num.value = 1;
getPictrue();
nextTick(() => {
const { imgHeight, imgWidth, barHeight, barWidth } = resetSize(proxy)
setSize.imgHeight = imgHeight
setSize.imgWidth = imgWidth
setSize.barHeight = barHeight
setSize.barWidth = barWidth
proxy.$parent.$emit('ready', proxy)
})
}
const size = resetSize(proxy);
setSize.imgHeight = size.imgHeight;
setSize.imgWidth = size.imgWidth;
setSize.barHeight = size.barHeight;
setSize.barWidth = size.barWidth;
proxy.$parent.$emit('ready', proxy);
});
};
onMounted(() => {
//
init()
//
init();
proxy.$el.onselectstart = function () {
return false
}
})
const canvas = ref(null)
return false;
};
});
const canvas = ref(null);
/**
* 处理画布点击事件
* @param {Event} e - 点击事件对象
*/
const canvasClick = (e) => {
checkPosArr.push(getMousePos(canvas, e))
if (num.value == checkNum.value) {
num.value = createPoint(getMousePos(canvas, e))
checkPosArr.push(getMousePos(canvas, e));
if (num.value === checkNum.value) {
num.value = createPoint(getMousePos(canvas, e));
//
const arr = pointTransfrom(checkPosArr, setSize)
checkPosArr.length = 0
checkPosArr.push(...arr)
//
const arr = pointTransfrom(checkPosArr, setSize);
checkPosArr.length = 0;
checkPosArr.push(...arr);
setTimeout(() => {
// var flag = this.comparePos(this.fontPos, this.checkPosArr);
//
const captchaVerification = secretKey.value ? aesEncrypt(backToken.value + '---' + JSON.stringify(checkPosArr), secretKey.value) : backToken.value + '---' + JSON.stringify(checkPosArr)
const captchaVerification = secretKey.value
? aesEncrypt(backToken.value + '---' + JSON.stringify(checkPosArr), secretKey.value)
: backToken.value + '---' + JSON.stringify(checkPosArr);
const data = {
captchaType: captchaType.value,
pointJson: secretKey.value ? aesEncrypt(JSON.stringify(checkPosArr), secretKey.value) : JSON.stringify(checkPosArr),
pointJson: secretKey.value
? aesEncrypt(JSON.stringify(checkPosArr), secretKey.value)
: JSON.stringify(checkPosArr),
token: backToken.value
}
};
reqCheck(data).then(res => {
if (res.repCode == '0000') {
barAreaColor.value = '#4cae4c'
barAreaBorderColor.value = '#5cb85c'
text.value = '验证成功'
bindingClick.value = false
if (mode.value == 'pop') {
if (res.repCode === '0000') {
barAreaColor.value = '#4cae4c';
barAreaBorderColor.value = '#5cb85c';
text.value = '验证成功';
bindingClick.value = false;
if (mode.value === 'pop') {
setTimeout(() => {
proxy.$parent.clickShow = false
refresh()
}, 1500)
proxy.$parent.clickShow = false;
refresh();
}, 1500);
}
proxy.$parent.$emit('success', { captchaVerification })
proxy.$parent.$emit('success', {captchaVerification});
} else {
proxy.$parent.$emit('error', proxy)
barAreaColor.value = '#d9534f'
barAreaBorderColor.value = '#d9534f'
text.value = '验证失败'
setTimeout(() => {
refresh()
}, 700)
proxy.$parent.$emit('error', proxy);
barAreaColor.value = '#d9534f';
barAreaBorderColor.value = '#d9534f';
text.value = '验证失败';
setTimeout(refresh, 700);
}
})
}, 400)
});
}, 400);
} else {
num.value = createPoint(getMousePos(canvas, e));
}
if (num.value < checkNum.value) {
num.value = createPoint(getMousePos(canvas, e))
}
}
//
const getMousePos = function (obj, e) {
const x = e.offsetX
const y = e.offsetY
return { x, y }
}
//
const createPoint = function (pos) {
tempPoints.push(Object.assign({}, pos))
return num.value + 1
}
const refresh = function () {
tempPoints.splice(0, tempPoints.length)
barAreaColor.value = '#000'
barAreaBorderColor.value = '#ddd'
bindingClick.value = true
fontPos.splice(0, fontPos.length)
checkPosArr.splice(0, checkPosArr.length)
num.value = 1
getPictrue()
text.value = '验证失败'
showRefresh.value = true
}
};
//
function getPictrue () {
const data = {
captchaType: captchaType.value
}
/**
* 获取鼠标点击坐标
* @param {HTMLElement} obj - DOM元素
* @param {Event} e - 事件对象
* @returns {Object} 坐标对象
*/
const getMousePos = (obj, e) => ({
x: e.offsetX,
y: e.offsetY
});
/**
* 创建坐标点并添加到临时点列表中
* @param {Object} pos - 坐标对象
* @returns {Number} 更新后的点击计数值
*/
const createPoint = (pos) => {
tempPoints.push(Object.assign({}, pos));
return num.value + 1;
};
/**
* 刷新验证码
*/
const refresh = () => {
tempPoints.splice(0, tempPoints.length);
barAreaColor.value = '#000';
barAreaBorderColor.value = '#ddd';
bindingClick.value = true;
fontPos.splice(0, fontPos.length);
checkPosArr.splice(0, checkPosArr.length);
num.value = 1;
getPictrue();
text.value = '验证失败';
showRefresh.value = true;
};
/**
* 请求背景图片和验证图片
*/
const getPictrue = () => {
const data = {captchaType: captchaType.value};
reqGet(data).then(res => {
if (res.repCode == '0000') {
pointBackImgBase.value = res.repData.originalImageBase64
backToken.value = res.repData.token
secretKey.value = res.repData.secretKey
poinTextList.value = res.repData.wordList
text.value = '请依次点击【' + poinTextList.value.join(',') + '】'
if (res.repCode === '0000') {
pointBackImgBase.value = res.repData.originalImageBase64;
backToken.value = res.repData.token;
secretKey.value = res.repData.secretKey;
poinTextList.value = res.repData.wordList;
text.value = `请依次点击【${poinTextList.value.join(',')}`;
} else {
text.value = res.repMsg
text.value = res.repMsg;
}
})
}
//
const pointTransfrom = function (pointArr, imgSize) {
const newPointArr = pointArr.map(p => {
const x = Math.round(310 * p.x / parseInt(imgSize.imgWidth))
const y = Math.round(155 * p.y / parseInt(imgSize.imgHeight))
return { x, y }
})
return newPointArr
}
});
};
/**
* 将用户点击的坐标转换为原始图片的比例坐标
* @param {Array} pointArr - 用户点击的坐标数组
* @param {Object} imgSize - 图片尺寸信息
* @returns {Array} 转换后的坐标数组
*/
const pointTransfrom = (pointArr, imgSize) => pointArr.map(p => ({
x: Math.round(310 * p.x / parseInt(imgSize.imgWidth)),
y: Math.round(155 * p.y / parseInt(imgSize.imgHeight))
}));
return {
secretKey,
checkNum,
@ -255,7 +307,7 @@ export default {
refresh,
getPictrue,
pointTransfrom
}
};
}
}
};
</script>

@ -1,86 +1,126 @@
<template>
<div style="position: relative;">
<div style="position: relative;">
<!-- 图片验证码部分 -->
<div
v-if="type === '2'"
class="verify-img-out"
:style="{height: (parseInt(setSize.imgHeight) + vSpace) + 'px'}"
>
<div
class="verify-img-panel"
:style="{width: setSize.imgWidth, height: setSize.imgHeight}"
>
<!-- 显示背景图片 -->
<img
:src="'data:image/png;base64,'+backImgBase"
alt=""
style="width:100%;height:100%;display:block"
/>
<!-- 刷新按钮 -->
<div
v-if="type === '2'" class="verify-img-out"
:style="{height: (parseInt(setSize.imgHeight) + vSpace) + 'px'}"
>
<div
class="verify-img-panel" :style="{width: setSize.imgWidth,
height: setSize.imgHeight,}">
<img :src="'data:image/png;base64,'+backImgBase" alt="" style="width:100%;height:100%;display:block"/>
<div v-show="showRefresh" class="verify-refresh" @click="refresh"><i class="iconfont icon-refresh"/>
</div>
<transition name="tips">
<span v-if="tipWords" class="verify-tips" :class="passFlag ?'suc-bg':'err-bg'">{{tipWords}}</span>
</transition>
</div>
v-show="showRefresh"
class="verify-refresh"
@click="refresh"
>
<i class="iconfont icon-refresh"/>
</div>
<!-- 公共部分 -->
<!-- 提示信息 -->
<transition name="tips">
<span
v-if="tipWords"
class="verify-tips"
:class="passFlag ?'suc-bg':'err-bg'"
>{{tipWords}}</span>
</transition>
</div>
</div>
<!-- 公共部分滑动条 -->
<div
class="verify-bar-area"
:style="{width: setSize.imgWidth, height: barSize.height, 'line-height':barSize.height}"
>
<!-- 滑动条上的消息文本 -->
<span class="verify-msg" v-text="text"/>
<!-- 左边进度条 -->
<div
class="verify-left-bar"
:style="{width: (leftBarWidth !== undefined)? leftBarWidth: barSize.height, height: barSize.height, 'border-color': leftBarBorderColor, transition: transitionWidth}"
>
<!-- 完成后的消息文本 -->
<span class="verify-msg" v-text="finishText"/>
<!-- 滑动块 -->
<div
class="verify-bar-area" :style="{width: setSize.imgWidth,
height: barSize.height,
'line-height':barSize.height}">
<span class="verify-msg" v-text="text"/>
<div
class="verify-left-bar"
:style="{width: (leftBarWidth!==undefined)?leftBarWidth: barSize.height, height: barSize.height, 'border-color': leftBarBorderColor, transaction: transitionWidth}">
<span class="verify-msg" v-text="finishText"/>
<div
class="verify-move-block"
:style="{width: barSize.height, height: barSize.height, 'background-color': moveBlockBackgroundColor, left: moveBlockLeft, transition: transitionLeft}"
@touchstart="start"
@mousedown="start">
<i
:class="['verify-icon iconfont', iconClass]"
:style="{color: iconColor}"/>
<div
v-if="type === '2'" class="verify-sub-block"
:style="{'width':Math.floor(parseInt(setSize.imgWidth)*47/310)+ 'px',
'height': setSize.imgHeight,
'top':'-' + (parseInt(setSize.imgHeight) + vSpace) + 'px',
'background-size': setSize.imgWidth + ' ' + setSize.imgHeight,
}">
<img :src="'data:image/png;base64,'+blockBackImgBase" alt="" style="width:100%;height:100%;display:block;-webkit-user-drag:none;"/>
</div>
</div>
</div>
class="verify-move-block"
:style="{width: barSize.height, height: barSize.height, 'background-color': moveBlockBackgroundColor, left: moveBlockLeft, transition: transitionLeft}"
@touchstart="start"
@mousedown="start"
>
<!-- 滑动块内的图标 -->
<i
:class="['verify-icon iconfont', iconClass]"
:style="{color: iconColor}"
/>
<!-- type '2' 时显示的小图块 -->
<div
v-if="type === '2'"
class="verify-sub-block"
:style="{
width: Math.floor(parseInt(setSize.imgWidth)*47/310) + 'px',
height: setSize.imgHeight,
top: '-' + (parseInt(setSize.imgHeight) + vSpace) + 'px',
'background-size': setSize.imgWidth + ' ' + setSize.imgHeight,
}"
>
<img
:src="'data:image/png;base64,'+blockBackImgBase"
alt=""
style="width:100%;height:100%;display:block;-webkit-user-drag:none;"
/>
</div>
</div>
</div>
</div>
</div>
</template>
<script type="text/babel">
/**
* VerifySlide
* @description 滑块
* */
import { aesEncrypt } from './../utils/ase'
import { resetSize } from './../utils/util'
import { reqCheck, reqGet } from './../api/index'
* VerifySlide
* @description 滑块验证组件用于用户交互验证防止机器人操作
*/
import { aesEncrypt } from './../utils/ase' // AES
import { resetSize } from './../utils/util' //
import { reqCheck, reqGet } from './../api/index' // API
import { computed, getCurrentInstance, nextTick, onMounted, reactive, ref, toRefs, watch } from 'vue'
// "captchaType":"blockPuzzle",
export default {
name: 'VerifySlide',
props: {
captchaType: {
captchaType: { //
type: String
},
type: {
type: { // '1'
type: String,
default: '1'
},
// popfixed
mode: {
mode: { //
type: String,
default: 'fixed'
},
vSpace: {
vSpace: { //
type: Number,
default: 5
},
explain: {
explain: { //
type: String,
default: '向右滑动完成验证'
},
imgSize: {
imgSize: { //
type: Object,
default () {
return {
@ -89,7 +129,7 @@ export default {
}
}
},
blockSize: {
blockSize: { //
type: Object,
default () {
return {
@ -98,7 +138,7 @@ export default {
}
}
},
barSize: {
barSize: { //
type: Object,
default () {
return {
@ -111,133 +151,110 @@ export default {
setup (props) {
const { mode, captchaType, type, blockSize, explain } = toRefs(props)
const { proxy } = getCurrentInstance()
const secretKey = ref('') // ase
const passFlag = ref('') //
const backImgBase = ref('') //
const blockBackImgBase = ref('') //
//
const secretKey = ref('') //
const passFlag = ref(false) //
const backImgBase = ref('') // base64
const blockBackImgBase = ref('') // base64
const backToken = ref('') // token
const startMoveTime = ref('') //
const endMovetime = ref('') //
const startMoveTime = ref(0) //
const endMovetime = ref(0) //
const tipsBackColor = ref('') //
const tipWords = ref('')
const text = ref('')
const finishText = ref('')
const tipWords = ref('') //
const text = ref(explain.value) //
const finishText = ref('') //
const setSize = reactive({
imgHeight: 0,
imgWidth: 0,
barHeight: 0,
barWidth: 0
})
const top = ref(0)
const left = ref(0)
const moveBlockLeft = ref(undefined)
const leftBarWidth = ref(undefined)
//
const moveBlockBackgroundColor = ref(undefined)
const leftBarBorderColor = ref('#ddd')
const iconColor = ref(undefined)
const iconClass = ref('icon-right')
const status = ref(false) //
const isEnd = ref(false) //
const showRefresh = ref(true)
const transitionLeft = ref('')
const transitionWidth = ref('')
const startLeft = ref(0)
}) //
const top = ref(0) //
const left = ref(0) //
const moveBlockLeft = ref(undefined) //
const leftBarWidth = ref(undefined) //
//
const moveBlockBackgroundColor = ref(undefined) //
const leftBarBorderColor = ref('#ddd') //
const iconColor = ref(undefined) //
const iconClass = ref('icon-right') //
const status = ref(false) //
const isEnd = ref(false) //
const showRefresh = ref(true) //
const transitionLeft = ref('') //
const transitionWidth = ref('') //
const startLeft = ref(0) // left
const barArea = computed(() => {
return proxy.$el.querySelector('.verify-bar-area')
return proxy.$el.querySelector('.verify-bar-area') // DOM
})
function init () {
text.value = explain.value
//
getPictrue()
// ready
nextTick(() => {
const { imgHeight, imgWidth, barHeight, barWidth } = resetSize(proxy)
setSize.imgHeight = imgHeight
setSize.imgWidth = imgWidth
setSize.barHeight = barHeight
setSize.barWidth = barWidth
const sizeInfo = resetSize(proxy)
Object.assign(setSize, sizeInfo)
proxy.$parent.$emit('ready', proxy)
})
window.removeEventListener('touchmove', (e) => {
move(e)
})
window.removeEventListener('mousemove', (e) => {
move(e)
})
//
window.removeEventListener('touchend', () => {
end()
})
window.removeEventListener('mouseup', () => {
end()
})
window.addEventListener('touchmove', (e) => {
move(e)
})
window.addEventListener('mousemove', (e) => {
move(e)
})
//
window.addEventListener('touchend', () => {
end()
})
window.addEventListener('mouseup', () => {
end()
})
// PC
window.addEventListener('touchmove', move)
window.addEventListener('mousemove', move)
window.addEventListener('touchend', end)
window.addEventListener('mouseup', end)
}
// type
watch(type, () => {
init()
})
//
onMounted(() => {
//
init()
//
proxy.$el.onselectstart = function () {
return false
}
})
//
//
function start (e) {
e = e || window.event
let x
if (!e.touches) { // PC
x = e.clientX
} else { //
x = e.touches[0].pageX
}
startLeft.value = Math.floor(x - barArea.value.getBoundingClientRect().left)
startMoveTime.value = Date.now() //
if (isEnd.value === false) {
text.value = ''
let x = !e.touches ? e.clientX : e.touches[0].pageX //
startLeft.value = Math.floor(x - barArea.value.getBoundingClientRect().left) // left
startMoveTime.value = Date.now() //
if (!isEnd.value) {
text.value = '' //
//
moveBlockBackgroundColor.value = '#337ab7'
leftBarBorderColor.value = '#337AB7'
iconColor.value = '#fff'
e.stopPropagation()
status.value = true
status.value = true //
}
}
//
//
function move (e) {
e = e || window.event
let x
if (status.value && isEnd.value === false) {
if (!e.touches) { // PC
x = e.clientX
} else { //
x = e.touches[0].pageX
}
let x = !e.touches ? e.clientX : e.touches[0].pageX //
if (status.value && !isEnd.value) {
const bar_area_left = barArea.value.getBoundingClientRect().left
let move_block_left = x - bar_area_left // left
if (move_block_left >= barArea.value.offsetWidth - Number.parseInt(Number.parseInt(blockSize.value.width) / 2) - 2) {
move_block_left = barArea.value.offsetWidth - Number.parseInt(Number.parseInt(blockSize.value.width) / 2) - 2
let move_block_left = x - bar_area_left // left
//
if (move_block_left >= barArea.value.offsetWidth - parseInt(blockSize.value.width) / 2 - 2) {
move_block_left = barArea.value.offsetWidth - parseInt(blockSize.value.width) / 2 - 2
}
if (move_block_left <= 0) {
move_block_left = Number.parseInt(Number.parseInt(blockSize.value.width) / 2)
move_block_left = parseInt(blockSize.value.width) / 2
}
// left
//
moveBlockLeft.value = `${move_block_left - startLeft.value}px`
leftBarWidth.value = `${move_block_left - startLeft.value}px`
}
@ -245,99 +262,168 @@ export default {
//
function end () {
//
endMovetime.value = Date.now()
//
// statustrueisEndfalse
if (status.value && isEnd.value === false) {
//
let moveLeftDistance = Number.parseInt((moveBlockLeft.value || '').replace('px', ''))
moveLeftDistance = moveLeftDistance * 310 / Number.parseInt(setSize.imgWidth)
// xy5.0
const data = {
captchaType: captchaType.value,
pointJson: secretKey.value ? aesEncrypt(JSON.stringify({ x: moveLeftDistance, y: 5.0 }), secretKey.value) : JSON.stringify({ x: moveLeftDistance, y: 5.0 }),
token: backToken.value
captchaType: captchaType.value, //
pointJson: secretKey.value
? aesEncrypt(JSON.stringify({ x: moveLeftDistance, y: 5.0 }), secretKey.value) //
: JSON.stringify({ x: moveLeftDistance, y: 5.0 }), //
token: backToken.value //
}
//
reqCheck(data).then(res => {
// '0000'
if (res.data.repCode === '0000') {
// UI
moveBlockBackgroundColor.value = '#5cb85c'
leftBarBorderColor.value = '#5cb85c'
iconColor.value = '#fff'
iconClass.value = 'icon-check'
showRefresh.value = false
isEnd.value = true
//
if (mode.value === 'pop') {
setTimeout(() => {
proxy.$parent.clickShow = false
refresh()
refresh() //
}, 1500)
}
passFlag.value = true
tipWords.value = `${((endMovetime.value - startMoveTime.value) / 1000).toFixed(2)}s验证成功`
const captchaVerification = secretKey.value ? aesEncrypt(`${backToken.value}---${JSON.stringify({ x: moveLeftDistance, y: 5.0 })}`, secretKey.value) : `${backToken.value}---${JSON.stringify({ x: moveLeftDistance, y: 5.0 })}`
passFlag.value = true //
tipWords.value = `${((endMovetime.value - startMoveTime.value) / 1000).toFixed(2)}s验证成功` //
//
const captchaVerification = secretKey.value
? aesEncrypt(`${backToken.value}---${JSON.stringify({ x: moveLeftDistance, y: 5.0 })}`, secretKey.value) //
: `${backToken.value}---${JSON.stringify({ x: moveLeftDistance, y: 5.0 })}` //
//
setTimeout(() => {
tipWords.value = ''
proxy.$parent.closeBox()
proxy.$parent.$emit('success', { captchaVerification })
}, 1000)
} else {
// UI
moveBlockBackgroundColor.value = '#d9534f'
leftBarBorderColor.value = '#d9534f'
iconColor.value = '#fff'
iconClass.value = 'icon-close'
passFlag.value = false
//
setTimeout(() => {
refresh()
}, 1000)
//
proxy.$parent.$emit('error', proxy)
tipWords.value = '验证失败'
//
setTimeout(() => {
tipWords.value = ''
}, 1000)
}
})
status.value = false
status.value = false //
}
}
const refresh = () => {
showRefresh.value = true
finishText.value = ''
// showRefresh true
//
showRefresh.value = true;
transitionLeft.value = 'left .3s'
moveBlockLeft.value = 0
// finishText finishText
//
finishText.value = '';
leftBarWidth.value = undefined
transitionWidth.value = 'width .3s'
// transitionLeft 0.3
// 使
transitionLeft.value = 'left .3s';
leftBarBorderColor.value = '#ddd'
moveBlockBackgroundColor.value = '#fff'
iconColor.value = '#000'
iconClass.value = 'icon-right'
isEnd.value = false
// moveBlockLeft 0
moveBlockLeft.value = 0;
getPictrue()
// leftBarWidth undefined
//
leftBarWidth.value = undefined;
// transitionWidth 0.3
// 使
transitionWidth.value = 'width .3s';
// #ddd
leftBarBorderColor.value = '#ddd';
// #fff
moveBlockBackgroundColor.value = '#fff';
// #000
iconColor.value = '#000';
// 'icon-right'
//
iconClass.value = 'icon-right';
// isEnd false
isEnd.value = false;
// getPictrue
// 使
getPictrue();
// 使 setTimeout 300
// - transitionWidth transitionLeft CSS
// - text explain
//
setTimeout(() => {
transitionWidth.value = ''
transitionLeft.value = ''
text.value = explain.value
}, 300)
}
transitionWidth.value = '';
transitionLeft.value = '';
text.value = explain.value;
}, 300);
};
//
function getPictrue () {
//
const data = {
captchaType: captchaType.value,
clientUid: localStorage.getItem('b2cSlider'),
ts: Date.now() //
}
captchaType: captchaType.value, //
clientUid: localStorage.getItem('b2cSlider'), //
ts: Date.now() //
};
//
reqGet(data).then(res => {
// '0000'
if (res.data.repCode === '0000') {
backImgBase.value = res.data.repData.originalImageBase64
blockBackImgBase.value = res.data.repData.jigsawImageBase64
backToken.value = res.data.repData.token
secretKey.value = res.data.repData.secretKey
// Base64
backImgBase.value = res.data.repData.originalImageBase64; // Base64
blockBackImgBase.value = res.data.repData.jigsawImageBase64; // Base64
//
backToken.value = res.data.repData.token;
secretKey.value = res.data.repData.secretKey;
} else {
tipWords.value = res.data.repMsg
//
tipWords.value = res.data.repMsg; //
}
})
});
}
return {
secretKey, // ase

@ -1,24 +1,38 @@
/**
* 此处可直接引用自己项目封装好的 axios 配合后端联调
* 此处可直接引用自己项目封装好的 axios 配合后端联调
* 使用项目内部封装的 axios 实例可以确保所有请求都遵循统一的配置
* 例如默认的基础URL请求拦截器响应拦截器等从而简化API调用
*/
// 引入组件内部封装的axios实例
import request from './../utils/axios' // 组件内部封装的axios
// import request from "@/api/axios.js" //调用项目封装的axios
// 或者引入项目全局封装的axios实例根据实际路径调整
// import request from "@/api/axios.js" // 调用项目封装的axios
// 获取验证图片 以及token
export function reqGet (data) {
/**
* 获取验证图片及token
*
* @param {Object} data - 请求体数据通常包含生成验证码所需的参数
* @returns {Promise} - 返回一个 Promise 对象解析为服务器响应的数据
*/
export function reqGet(data) {
return request({
url: '/captcha/get',
method: 'post',
data
})
url: '/captcha/get', // API endpoint for getting captcha image and token
method: 'post', // 使用POST方法发送请求
data // 将传入的数据作为请求体发送
});
}
// 滑动或者点选验证
export function reqCheck (data) {
/**
* 滑动或点选验证
*
* @param {Object} data - 请求体数据通常包含验证操作的结果和token
* @returns {Promise} - 返回一个 Promise 对象解析为服务器响应的数据
*/
export function reqCheck(data) {
return request({
url: '/captcha/check',
method: 'post',
data
})
url: '/captcha/check', // API endpoint for checking captcha validation
method: 'post', // 使用POST方法发送请求
data // 将传入的数据作为请求体发送
});
}

@ -1,11 +1,26 @@
import CryptoJS from 'crypto-js'
// 引入 CryptoJS 库,用于提供加密功能
import CryptoJS from 'crypto-js';
/**
* @word 要加密的内容
* @keyWord String 服务器随机返回的关键字
* */
export function aesEncrypt (word, keyWord = 'XwKsGlMcdPMEhR1B') {
const key = CryptoJS.enc.Utf8.parse(keyWord)
const srcs = CryptoJS.enc.Utf8.parse(word)
const encrypted = CryptoJS.AES.encrypt(srcs, key, { mode: CryptoJS.mode.ECB, padding: CryptoJS.pad.Pkcs7 })
return encrypted.toString()
* 使用 AES 算法对给定内容进行加密
*
* @param {string} word - 需要加密的内容
* @param {string} [keyWord='XwKsGlMcdPMEhR1B'] - 用于加密的密钥默认为 'XwKsGlMcdPMEhR1B'
* @returns {string} - 返回加密后的字符串使用 Base64 编码
*/
export function aesEncrypt(word, keyWord = 'XwKsGlMcdPMEhR1B') {
// 将密钥转换为 UTF-8 编码的字节数组
const key = CryptoJS.enc.Utf8.parse(keyWord);
// 将要加密的内容转换为 UTF-8 编码的字节数组
const srcs = CryptoJS.enc.Utf8.parse(word);
// 使用 AES 加密算法,采用 ECB 模式和 PKCS7 填充方式对数据进行加密
const encrypted = CryptoJS.AES.encrypt(srcs, key, {
mode: CryptoJS.mode.ECB, // 设置加密模式为 ECB (Electronic Codebook)
padding: CryptoJS.pad.Pkcs7 // 设置填充方式为 PKCS7
});
// 返回加密后的内容,以 Base64 编码的字符串形式
return encrypted.toString();
}

@ -1,27 +1,65 @@
import axios from 'axios'
// 引入 axios 库,用于发起 HTTP 请求
import axios from 'axios';
axios.defaults.baseURL = import.meta.env.VITE_APP_BASE_API
// 设置默认的基础 URL从环境变量中读取。
// VITE_APP_BASE_API 是在构建时通过 Vite 配置提供的环境变量。
axios.defaults.baseURL = import.meta.env.VITE_APP_BASE_API;
/**
* 创建一个自定义的 axios 实例允许我们设置全局配置
*/
const service = axios.create({
// 设置请求超时时间为 40 秒40000 毫秒)
timeout: 40000,
// 设置默认的请求头信息
headers: {
'X-Requested-With': 'XMLHttpRequest',
'Content-Type': 'application/json; charset=UTF-8'
'X-Requested-With': 'XMLHttpRequest', // 标识这是一个 AJAX 请求
'Content-Type': 'application/json; charset=UTF-8' // 指定发送的数据类型为 JSON
}
})
});
/**
* 请求拦截器在请求发送之前进行处理
* 可以用来添加认证令牌修改请求配置等
*/
service.interceptors.request.use(
config => {
return config
// 在这里可以对 config 进行修改,例如添加 token 或其他头部信息
// 示例config.headers.Authorization = `Bearer ${token}`;
// 返回配置对象或 Promise.resolve(config)
return config;
},
error => {
Promise.reject(error)
// 请求错误处理
// 如果请求出错,在这里可以做些事情,比如日志记录
// 返回错误或 Promise.reject(error)
return Promise.reject(error);
}
)
);
// response interceptor
/**
* 响应拦截器在接收到响应数据之后进行处理
* 可以用来统一处理响应错误解析响应数据等
*/
service.interceptors.response.use(
response => {
return response.data
// 默认情况下只返回响应体中的 data 字段
// 如果需要更多响应信息,可以直接返回整个 response 对象
return response.data;
},
error => {
// 响应错误处理
// 如果服务器返回错误状态码(如 4xx 或 5xx在这里可以进行全局错误处理
// 例如显示错误消息给用户或者跳转到错误页面
// 返回错误或 Promise.reject(error)
return Promise.reject(error);
}
)
export default service
);
// 导出 service 实例,以便其他模块可以引用它来发起 HTTP 请求
export default service;

@ -1,35 +1,68 @@
export function resetSize (vm) {
let img_width, img_height, bar_width, bar_height // 图片的宽度、高度,移动条的宽度、高度
/**
* 重置图片和移动条的尺寸
*
* @param {Object} vm - Vue 组件实例包含 imgSize barSize 属性
* imgSize 包含图片的宽度和高度信息
* barSize 包含移动条的宽度和高度信息
* @returns {Object} 返回一个对象包含重置后的图片和移动条的尺寸信息
*/
export function resetSize(vm) {
// 定义变量来存储图片和移动条的宽度、高度
let img_width, img_height, bar_width, bar_height;
const parentWidth = vm.$el.parentNode.offsetWidth || window.offsetWidth
const parentHeight = vm.$el.parentNode.offsetHeight || window.offsetHeight
if (vm.imgSize.width.indexOf('%') != -1) {
img_width = parseInt(vm.imgSize.width) / 100 * parentWidth + 'px'
// 获取父容器的宽度和高度,默认为窗口的宽度和高度
const parentWidth = vm.$el.parentNode.offsetWidth || window.innerWidth;
const parentHeight = vm.$el.parentNode.offsetHeight || window.innerHeight;
// 计算图片的宽度
if (vm.imgSize.width.indexOf('%') !== -1) {
// 如果宽度是百分比,则根据父容器宽度计算实际像素值
img_width = (parseInt(vm.imgSize.width) / 100 * parentWidth) + 'px';
} else {
img_width = vm.imgSize.width
// 否则直接使用提供的宽度值
img_width = vm.imgSize.width;
}
if (vm.imgSize.height.indexOf('%') != -1) {
img_height = parseInt(vm.imgSize.height) / 100 * parentHeight + 'px'
// 计算图片的高度
if (vm.imgSize.height.indexOf('%') !== -1) {
// 如果高度是百分比,则根据父容器高度计算实际像素值
img_height = (parseInt(vm.imgSize.height) / 100 * parentHeight) + 'px';
} else {
img_height = vm.imgSize.height
// 否则直接使用提供的高度值
img_height = vm.imgSize.height;
}
if (vm.barSize.width.indexOf('%') != -1) {
bar_width = parseInt(vm.barSize.width) / 100 * parentWidth + 'px'
// 计算移动条的宽度
if (vm.barSize.width.indexOf('%') !== -1) {
// 如果宽度是百分比,则根据父容器宽度计算实际像素值
bar_width = (parseInt(vm.barSize.width) / 100 * parentWidth) + 'px';
} else {
bar_width = vm.barSize.width
// 否则直接使用提供的宽度值
bar_width = vm.barSize.width;
}
if (vm.barSize.height.indexOf('%') != -1) {
bar_height = parseInt(vm.barSize.height) / 100 * parentHeight + 'px'
// 计算移动条的高度
if (vm.barSize.height.indexOf('%') !== -1) {
// 如果高度是百分比,则根据父容器高度计算实际像素值
bar_height = (parseInt(vm.barSize.height) / 100 * parentHeight) + 'px';
} else {
bar_height = vm.barSize.height
// 否则直接使用提供的高度值
bar_height = vm.barSize.height;
}
return { imgWidth: img_width, imgHeight: img_height, barWidth: bar_width, barHeight: bar_height }
// 返回包含重置后尺寸的对象
return { imgWidth: img_width, imgHeight: img_height, barWidth: bar_width, barHeight: bar_height };
}
export const _code_chars = [1, 2, 3, 4, 5, 6, 7, 8, 9, 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z']
export const _code_color1 = ['#fffff0', '#f0ffff', '#f0fff0', '#fff0f0']
export const _code_color2 = ['#FF0033', '#006699', '#993366', '#FF9900', '#66CC66', '#FF33CC']
// 验证码字符集包含数字和字母不包括0和O避免混淆
export const _code_chars = [
1, 2, 3, 4, 5, 6, 7, 8, 9,
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'
];
// 验证码背景颜色集合,浅色系
export const _code_color1 = ['#fffff0', '#f0ffff', '#f0fff0', '#fff0f0'];
// 验证码前景颜色集合,多种颜色以增加辨识度
export const _code_color2 = ['#FF0033', '#006699', '#993366', '#FF9900', '#66CC66', '#FF33CC'];

@ -1,70 +1,95 @@
export const tableOption = {
// 搜索菜单所占的栅格数默认为6适用于响应式布局。
searchMenuSpan: 6,
// 是否显示列按钮如排序、筛选等false表示不显示。
columnBtn: false,
// 是否启用表格边框true表示启用。
border: true,
// 是否显示多选框true表示启用允许用户选择一行或多行记录。
selection: true,
// 是否显示行号默认为false即不显示行号。
index: false,
// 行号标题当index为true时生效默认值为'序号'。
indexLabel: '序号',
// 是否启用斑马纹样式true表示启用使表格更易读。
stripe: true,
// 菜单对齐方式,设置为居中对齐。
menuAlign: 'center',
// 菜单宽度默认为350px。
menuWidth: 350,
// 表格内容的对齐方式,设置为居中对齐。
align: 'center',
// 是否显示刷新按钮true表示显示允许用户刷新表格数据。
refreshBtn: true,
// 搜索框大小设置为mini提供紧凑型搜索输入框。
searchSize: 'mini',
// 是否显示添加按钮默认为false即不显示。
addBtn: false,
// 是否显示编辑按钮默认为false即不显示。
editBtn: false,
// 是否显示删除按钮默认为false即不显示。
delBtn: false,
// 是否显示查看按钮默认为false即不显示。
viewBtn: false,
// 配置选项指定下拉框等组件的label和value字段名。
props: {
label: 'label',
value: 'value'
label: 'label', // 下拉框选项显示的文本字段名
value: 'value' // 下拉框选项对应的值字段名
},
column: [{
label: '表单名称',
prop: 'formName',
search: true
}, {
label: '按钮文本',
prop: 'buttonName',
search: true
}, {
label: '提交次数',
prop: 'submitNum',
type: 'select',
dicData: [
{
label: '不做限制',
value: 0
}, {
label: '每个IP限填一次',
value: 1
}
]
}, {
label: '开启验证',
prop: 'needValidation',
type: 'select',
dicData: [
{
label: '不需要',
value: 0
}, {
label: '需要',
value: 1
}
]
}, {
label: '提交权限',
prop: 'submitPerm',
type: 'select',
dicData: [
{
label: '所有人',
value: 0
}, {
label: '仅会员可提交',
value: 1
}
]
}]
// 定义表格中的列,每一列由一个对象表示。
column: [
{
// 列标题
label: '表单名称',
// 对应的数据字段名
prop: 'formName',
// 是否允许在该列上进行搜索过滤
search: true
}, {
label: '按钮文本',
prop: 'buttonName',
search: true
}, {
label: '提交次数',
prop: 'submitNum',
type: 'select', // 列类型,这里是下拉选择框
dicData: [ // 下拉选择框的数据源
{ label: '不做限制', value: 0 },
{ label: '每个IP限填一次', value: 1 }
]
}, {
label: '开启验证',
prop: 'needValidation',
type: 'select',
dicData: [
{ label: '不需要', value: 0 },
{ label: '需要', value: 1 }
]
}, {
label: '提交权限',
prop: 'submitPerm',
type: 'select',
dicData: [
{ label: '所有人', value: 0 },
{ label: '仅会员可提交', value: 1 }
]
}
]
}

@ -1,47 +1,92 @@
export const tableOption = {
// 搜索菜单所占的栅格数默认为6适用于响应式布局。
searchMenuSpan: 6,
// 是否显示列按钮如排序、筛选等false表示不显示。
columnBtn: false,
// 是否启用表格边框true表示启用。
border: true,
// 是否显示多选框true表示启用允许用户选择一行或多行记录。
selection: true,
// 是否显示行号默认为false即不显示行号。
index: false,
// 行号标题当index为true时生效默认值为'序号'。
indexLabel: '序号',
// 是否启用斑马纹样式true表示启用使表格更易读。
stripe: true,
// 菜单对齐方式,设置为居中对齐。
menuAlign: 'center',
// 菜单宽度默认为350px。
menuWidth: 350,
// 表格内容的对齐方式,设置为居中对齐。
align: 'center',
// 是否显示刷新按钮true表示显示允许用户刷新表格数据。
refreshBtn: true,
// 搜索框大小设置为mini提供紧凑型搜索输入框。
searchSize: 'mini',
// 是否显示添加按钮默认为false即不显示。
addBtn: false,
// 是否显示编辑按钮默认为false即不显示。
editBtn: false,
// 是否显示删除按钮默认为false即不显示。
delBtn: false,
// 是否显示查看按钮默认为false即不显示。
viewBtn: false,
// 配置选项指定下拉框等组件的label和value字段名。
props: {
label: 'label',
value: 'value'
label: 'label', // 下拉框选项显示的文本字段名
value: 'value' // 下拉框选项对应的值字段名
},
column: [{
label: '轮播图片',
prop: 'imgUrl',
type: 'upload',
slot: true,
listType: 'picture-img'
}, {
label: '顺序',
prop: 'seq'
}, {
width: 150,
label: '状态',
prop: 'status',
search: true,
type: 'select',
dicData: [
{
label: '禁用',
value: 0
}, {
label: '正常',
value: 1
}
]
}]
// 定义表格中的列,每一列由一个对象表示。
column: [
{
// 列标题
label: '轮播图片',
// 对应的数据字段名
prop: 'imgUrl',
// 列类型,这里是上传组件
type: 'upload',
// 使用插槽自定义列的内容true表示启用
slot: true,
// 上传列表的类型,这里是指定为图片格式
listType: 'picture-img'
}, {
// 列标题
label: '顺序',
// 对应的数据字段名
prop: 'seq'
}, {
// 设置该列的宽度为150px
width: 150,
// 列标题
label: '状态',
// 对应的数据字段名
prop: 'status',
// 是否允许在该列上进行搜索过滤
search: true,
// 列类型,这里是下拉选择框
type: 'select',
// 下拉选择框的数据源
dicData: [
{ label: '禁用', value: 0 },
{ label: '正常', value: 1 }
]
}
]
}

@ -1,52 +1,102 @@
export const tableOption = {
// 搜索菜单所占的栅格数默认为6适用于响应式布局。
searchMenuSpan: 6,
// 是否显示列按钮如排序、筛选等false表示不显示。
columnBtn: false,
// 是否启用表格边框true表示启用。
border: true,
// 是否显示行号默认为false即不显示行号。
index: false,
// 行号标题当index为true时生效默认值为'序号'。
indexLabel: '序号',
// 是否显示多选框true表示启用允许用户选择一行或多行记录。
selection: true,
// 是否启用斑马纹样式true表示启用使表格更易读。
stripe: true,
// 菜单对齐方式,设置为居中对齐。
menuAlign: 'center',
// 菜单宽度默认为350px。
menuWidth: 350,
// 表格内容的对齐方式,设置为居中对齐。
align: 'center',
// 是否显示刷新按钮true表示显示允许用户刷新表格数据。
refreshBtn: true,
// 搜索框大小设置为mini提供紧凑型搜索输入框。
searchSize: 'mini',
// 是否显示添加按钮默认为false即不显示。
addBtn: false,
// 是否显示编辑按钮默认为false即不显示。
editBtn: false,
// 是否显示删除按钮默认为false即不显示。
delBtn: false,
// 是否显示查看按钮默认为false即不显示。
viewBtn: false,
// 配置选项指定下拉框等组件的label和value字段名。
props: {
label: 'label',
value: 'value'
label: 'label', // 下拉框选项显示的文本字段名
value: 'value' // 下拉框选项对应的值字段名
},
column: [{
label: '创建时间',
prop: 'createTime'
},
{
label: '姓名',
prop: 'userName',
search: true
}, {
label: '邮箱',
prop: 'email'
}, {
label: '联系方式',
prop: 'contact'
}, {
label: '审核',
prop: 'status',
search: true,
slot: true,
type: 'select',
dicData: [
{
label: '未审核',
value: 0
}, {
label: '审核通过',
value: 1
}
]
}]
// 定义表格中的列,每一列由一个对象表示。
column: [
{
// 列标题
label: '创建时间',
// 对应的数据字段名
prop: 'createTime'
},
{
// 列标题
label: '姓名',
// 对应的数据字段名
prop: 'userName',
// 是否允许在该列上进行搜索过滤
search: true
},
{
// 列标题
label: '邮箱',
// 对应的数据字段名
prop: 'email'
},
{
// 列标题
label: '联系方式',
// 对应的数据字段名
prop: 'contact'
},
{
// 列标题
label: '审核',
// 对应的数据字段名
prop: 'status',
// 是否允许在该列上进行搜索过滤
search: true,
// 使用插槽自定义列的内容true表示启用
slot: true,
// 列类型,这里是下拉选择框
type: 'select',
// 下拉选择框的数据源
dicData: [
{ label: '未审核', value: 0 },
{ label: '审核通过', value: 1 }
]
}
]
}

@ -1,75 +1,111 @@
export const tableOption = {
// 搜索菜单项所占的栅格数量默认是6总共24
searchMenuSpan: 6,
// 是否显示列设置按钮默认是true这里设为false表示不显示
columnBtn: false,
// 是否显示表格边框默认是false这里设为true表示显示边框
border: true,
// 是否显示索引列默认是false这里设为true表示显示索引列
index: true,
// 索引列的标题,默认是'序号'
indexLabel: '序号',
// 是否启用斑马纹默认是false这里设为true表示启用斑马纹
stripe: true,
// 表格内操作菜单的对齐方式,默认是'right',这里设为'center'表示居中对齐
menuAlign: 'center',
// 表格内容的对齐方式,默认是'left',这里设为'center'表示居中对齐
align: 'center',
// 是否显示添加按钮默认是true这里设为false表示不显示添加按钮
addBtn: false,
// 是否显示编辑按钮默认是true这里设为false表示不显示编辑按钮
editBtn: false,
// 是否显示删除按钮默认是true这里设为false表示不显示删除按钮
delBtn: false,
// 定义表格的列配置
column: [
// 商品名列配置
{
label: '商品名',
prop: 'prodName',
search: true
label: '商品名', // 列标题
prop: 'prodName', // 数据属性名
search: true // 是否在搜索栏中显示此列的筛选条件
},
// 用户昵称列配置
{
label: '用户昵称',
prop: 'nickName',
slot: true
label: '用户昵称', // 列标题
prop: 'nickName', // 数据属性名
slot: true // 是否使用自定义模板渲染这一列
},
// 记录时间列配置
{
label: '记录时间',
prop: 'recTime',
width: '200'
label: '记录时间', // 列标题
prop: 'recTime', // 数据属性名
width: '200' // 列宽
},
// 回复时间列配置
{
label: '回复时间',
slot: true,
prop: 'replyTime',
width: '200',
dicData: [
label: '回复时间', // 列标题
slot: true, // 是否使用自定义模板渲染这一列
prop: 'replyTime', // 数据属性名
width: '200', // 列宽
dicData: [ // 下拉选项数据
{
label: '无',
value: ''
label: '无', // 显示文本
value: '' // 对应值
}
]
},
// 评价得分列配置
{
label: '评价得分',
prop: 'score'
label: '评价得分', // 列标题
prop: 'score' // 数据属性名
},
// 是否匿名列配置
{
label: '是否匿名',
prop: 'isAnonymous',
dicData: [
label: '是否匿名', // 列标题
prop: 'isAnonymous', // 数据属性名
dicData: [ // 下拉选项数据
{
label: '否',
value: 0
label: '否', // 显示文本
value: 0 // 对应值
}, {
label: '是',
value: 1
label: '是', // 显示文本
value: 1 // 对应值
}
]
},
// 审核状态列配置
{
prop: 'status',
label: '审核状态',
search: true,
type: 'select',
dicData: [
prop: 'status', // 数据属性名
label: '审核状态', // 列标题
search: true, // 是否在搜索栏中显示此列的筛选条件
type: 'select', // 输入类型为下拉选择
dicData: [ // 下拉选项数据
{
label: '待审核',
value: 0
label: '待审核', // 显示文本
value: 0 // 对应值
}, {
label: '审核通过',
value: 1
label: '审核通过', // 显示文本
value: 1 // 对应值
}, {
label: '审核未通过',
value: -1
label: '审核未通过', // 显示文本
value: -1 // 对应值
}
]
}

@ -1,59 +1,111 @@
export const tableOption = {
// 搜索菜单项所占的栅格数量默认是6总共24
searchMenuSpan: 6,
// 是否显示列设置按钮默认是true这里设为false表示不显示
columnBtn: false,
// 是否显示表格边框默认是false这里设为true表示显示边框
border: true,
// 是否显示多选框默认是false这里设为true表示启用多选功能
selection: true,
// 是否显示索引列默认是false这里设为false表示不显示索引列
index: false,
// 索引列的标题,默认是'序号'
indexLabel: '序号',
// 是否启用斑马纹默认是false这里设为true表示启用斑马纹
stripe: true,
// 表格内操作菜单的对齐方式,默认是'right',这里设为'center'表示居中对齐
menuAlign: 'center',
// 操作菜单栏的宽度默认值为150px这里设为350px
menuWidth: 350,
// 表格内容的对齐方式,默认是'left',这里设为'center'表示居中对齐
align: 'center',
// 是否显示刷新按钮默认是false这里设为true表示显示刷新按钮
refreshBtn: true,
// 搜索栏的尺寸,默认是'medium',这里设为'mini'表示使用小型搜索栏
searchSize: 'mini',
// 是否显示添加按钮默认是true这里设为false表示不显示添加按钮
addBtn: false,
// 是否显示编辑按钮默认是true这里设为false表示不显示编辑按钮
editBtn: false,
// 是否显示删除按钮默认是true这里设为false表示不显示删除按钮
delBtn: false,
// 是否显示查看按钮默认是true这里设为false表示不显示查看按钮
viewBtn: false,
// 设置下拉选项数据的label和value对应的属性名
props: {
label: 'label',
value: 'value'
label: 'label', // 下拉选项显示的文本属性名
value: 'value' // 下拉选项对应的值属性名
},
column: [{
label: '产品名字',
prop: 'prodName',
search: true
}, {
label: '商品原价',
prop: 'oriPrice'
}, {
label: '商品现价',
prop: 'price'
}, {
label: '商品库存',
prop: 'totalStocks'
}, {
label: '产品图片',
prop: 'pic',
type: 'upload',
width: 150,
listType: 'picture-img'
}, {
width: 150,
label: '状态',
prop: 'status',
search: true,
slot: true,
type: 'select',
dicData: [
{
label: '未上架',
value: 0
}, {
label: '上架',
value: 1
}
]
}]
// 定义表格的列配置
column: [
// 产品名字列配置
{
label: '产品名字', // 列标题
prop: 'prodName', // 数据属性名
search: true // 是否在搜索栏中显示此列的筛选条件
},
// 商品原价列配置
{
label: '商品原价', // 列标题
prop: 'oriPrice' // 数据属性名
},
// 商品现价列配置
{
label: '商品现价', // 列标题
prop: 'price' // 数据属性名
},
// 商品库存列配置
{
label: '商品库存', // 列标题
prop: 'totalStocks' // 数据属性名
},
// 产品图片列配置
{
label: '产品图片', // 列标题
prop: 'pic', // 数据属性名
type: 'upload', // 输入类型为上传文件
width: 150, // 列宽
listType: 'picture-img' // 图片列表类型
},
// 状态列配置
{
width: 150, // 列宽
label: '状态', // 列标题
prop: 'status', // 数据属性名
search: true, // 是否在搜索栏中显示此列的筛选条件
slot: true, // 是否使用自定义模板渲染这一列
type: 'select', // 输入类型为下拉选择
dicData: [ // 下拉选项数据
{
label: '未上架', // 显示文本
value: 0 // 对应值
}, {
label: '上架', // 显示文本
value: 1 // 对应值
}
]
}
]
}

@ -1,46 +1,76 @@
export const tableOption = {
// 搜索菜单项所占的栅格数量默认是6总共24
searchMenuSpan: 6,
// 是否显示列设置按钮默认是true这里设为false表示不显示列设置按钮
columnBtn: false,
// 是否显示表格边框默认是false这里设为true表示显示边框
border: true,
// 是否显示索引列默认是false这里设为true表示显示索引列
index: true,
// 索引列的标题,默认是'序号'
indexLabel: '序号',
// 是否启用斑马纹默认是false这里设为true表示启用斑马纹
stripe: true,
// 表格内操作菜单的对齐方式,默认是'right',这里设为'center'表示居中对齐
menuAlign: 'center',
// 表格内容的对齐方式,默认是'left',这里设为'center'表示居中对齐
align: 'center',
// 是否显示添加按钮默认是true这里设为false表示不显示添加按钮
addBtn: false,
// 是否显示编辑按钮默认是true这里设为false表示不显示编辑按钮
editBtn: false,
// 是否显示删除按钮默认是true这里设为false表示不显示删除按钮
delBtn: false,
// 定义表格的列配置
column: [
// 标签名称列配置
{
label: '标签名称',
prop: 'title',
search: true,
slot: true
label: '标签名称', // 列标题
prop: 'title', // 数据属性名
search: true, // 是否在搜索栏中显示此列的筛选条件
slot: true // 是否使用自定义模板渲染这一列
},
// 状态列配置
{
label: '状态',
prop: 'status',
type: 'select',
slot: true,
search: true,
dicData: [
label: '状态', // 列标题
prop: 'status', // 数据属性名
type: 'select', // 输入类型为下拉选择
slot: true, // 是否使用自定义模板渲染这一列
search: true, // 是否在搜索栏中显示此列的筛选条件
dicData: [ // 下拉选项数据
{
label: '禁用',
value: 0
label: '禁用', // 显示文本
value: 0 // 对应值
}, {
label: '正常',
value: 1
label: '正常', // 显示文本
value: 1 // 对应值
}
]
},
// 默认类型列配置
{
label: '默认类型',
prop: 'isDfault',
slot: true
label: '默认类型', // 列标题
prop: 'isDfault', // 数据属性名
slot: true // 是否使用自定义模板渲染这一列
},
// 排序列配置
{
label: '排序',
prop: 'seq'
label: '排序', // 列标题
prop: 'seq' // 数据属性名
}
]
}

@ -1,30 +1,69 @@
export const tableOption = {
// 搜索菜单项所占的栅格数量默认是6总共24
searchMenuSpan: 6,
// 是否显示列设置按钮默认是true这里设为false表示不显示列设置按钮
columnBtn: false,
// 是否显示表格边框默认是false这里设为true表示显示边框
border: true,
// 是否显示索引列默认是false这里设为true表示显示索引列
index: true,
// 索引列的标题,默认是'序号'
indexLabel: '序号',
// 是否启用斑马纹默认是false这里设为true表示启用斑马纹
stripe: true,
// 表格内操作菜单的对齐方式,默认是'right',这里设为'center'表示居中对齐
menuAlign: 'center',
// 操作菜单栏的宽度默认值为150px这里设为350px
menuWidth: 350,
// 表格内容的对齐方式,默认是'left',这里设为'center'表示居中对齐
align: 'center',
// 是否显示刷新按钮默认是false这里设为true表示显示刷新按钮
refreshBtn: true,
// 搜索栏的尺寸,默认是'medium',这里设为'mini'表示使用小型搜索栏
searchSize: 'mini',
// 是否显示添加按钮默认是true这里设为false表示不显示添加按钮
addBtn: false,
// 是否显示编辑按钮默认是true这里设为false表示不显示编辑按钮
editBtn: false,
// 是否显示查看按钮默认是true这里设为false表示不显示查看按钮
viewBtn: false,
// 是否显示删除按钮默认是true这里设为false表示不显示删除按钮
delBtn: false,
// 设置下拉选项数据的label和value对应的属性名
props: {
label: 'label',
value: 'value'
label: 'label', // 下拉选项显示的文本属性名
value: 'value' // 下拉选项对应的值属性名
},
column: [{
label: '属性名称',
prop: 'propName',
search: true
}, {
label: '属性值',
prop: 'prodPropValues',
slot: true
}]
// 定义表格的列配置
column: [
// 属性名称列配置
{
label: '属性名称', // 列标题
prop: 'propName', // 数据属性名
search: true // 是否在搜索栏中显示此列的筛选条件
},
// 属性值列配置
{
label: '属性值', // 列标题
prop: 'prodPropValues', // 数据属性名
slot: true // 是否使用自定义模板渲染这一列
}
]
}

@ -1,50 +1,84 @@
export const tableOption = {
// 搜索菜单项所占的栅格数量默认是6总共24
searchMenuSpan: 6,
// 是否显示列设置按钮默认是true这里设为false表示不显示列设置按钮
columnBtn: false,
// 是否显示表格边框默认是false这里设为true表示显示边框
border: true,
// 是否显示索引列默认是false这里保持默认值不显示索引列
index: false,
// 是否显示多选框默认是false这里设为true表示启用多选功能
selection: true,
// 索引列的标题,默认是'序号'
indexLabel: '序号',
// 是否启用斑马纹默认是false这里设为true表示启用斑马纹
stripe: true,
// 表格内操作菜单的对齐方式,默认是'right',这里设为'center'表示居中对齐
menuAlign: 'center',
// 表格内容的对齐方式,默认是'left',这里设为'center'表示居中对齐
align: 'center',
// 是否显示添加按钮默认是true这里设为false表示不显示添加按钮
addBtn: false,
// 是否显示编辑按钮默认是true这里设为false表示不显示编辑按钮
editBtn: false,
// 是否显示删除按钮默认是true这里设为false表示不显示删除按钮
delBtn: false,
// 定义表格的列配置
column: [
// 热搜标题列配置
{
label: '热搜标题',
prop: 'title',
search: true
label: '热搜标题', // 列标题
prop: 'title', // 数据属性名
search: true // 是否在搜索栏中显示此列的筛选条件
},
// 热搜内容列配置
{
label: '热搜内容',
prop: 'content',
search: true
label: '热搜内容', // 列标题
prop: 'content', // 数据属性名
search: true // 是否在搜索栏中显示此列的筛选条件
},
// 录入时间列配置
{
label: '录入时间',
prop: 'recDate',
sortable: true
label: '录入时间', // 列标题
prop: 'recDate', // 数据属性名
sortable: true // 是否允许按该列排序
},
// 顺序列配置
{
label: '顺序',
prop: 'seq',
sortable: true
label: '顺序', // 列标题
prop: 'seq', // 数据属性名
sortable: true // 是否允许按该列排序
},
// 启用状态列配置
{
label: '启用状态',
prop: 'status',
type: 'select',
slot: true,
search: true,
dicData: [
label: '启用状态', // 列标题
prop: 'status', // 数据属性名
type: 'select', // 输入类型为下拉选择
slot: true, // 是否使用自定义模板渲染这一列
search: true, // 是否在搜索栏中显示此列的筛选条件
dicData: [ // 下拉选项数据
{
label: '未启用',
value: 0
label: '未启用', // 显示文本
value: 0 // 对应值
}, {
label: '启用',
value: 1
label: '启用', // 显示文本
value: 1 // 对应值
}
]
}

@ -1,50 +1,78 @@
export const tableOption = {
// 搜索菜单项所占的栅格数量默认是6总共24
searchMenuSpan: 6,
// 是否显示列设置按钮默认是true这里设为false表示不显示列设置按钮
columnBtn: false,
// 是否显示表格边框默认是false这里设为true表示显示边框
border: true,
// 是否显示索引列默认是false这里设为true表示显示索引列
index: true,
// 索引列的标题,默认是'序号'
indexLabel: '序号',
// 是否启用斑马纹默认是false这里设为true表示启用斑马纹
stripe: true,
// 表格内操作菜单的对齐方式,默认是'right',这里设为'center'表示居中对齐
menuAlign: 'center',
// 表格内容的对齐方式,默认是'left',这里设为'center'表示居中对齐
align: 'center',
// 是否显示添加按钮默认是true这里设为false表示不显示添加按钮
addBtn: false,
// 是否显示编辑按钮默认是true这里设为false表示不显示编辑按钮
editBtn: false,
// 是否显示删除按钮默认是true这里设为false表示不显示删除按钮
delBtn: false,
// 定义表格的列配置
column: [
// 公告内容列配置
{
label: '公告内容',
prop: 'title',
search: true
label: '公告内容', // 列标题
prop: 'title', // 数据属性名
search: true // 是否在搜索栏中显示此列的筛选条件
},
// 状态列配置
{
label: '状态',
prop: 'status',
search: true,
slot: true,
type: 'select',
dicData: [
label: '状态', // 列标题
prop: 'status', // 数据属性名
search: true, // 是否在搜索栏中显示此列的筛选条件
slot: true, // 是否使用自定义模板渲染这一列
type: 'select', // 输入类型为下拉选择
dicData: [ // 下拉选项数据
{
label: '撤销',
value: 0
label: '撤销', // 显示文本
value: 0 // 对应值
}, {
label: '公布',
value: 1
label: '公布', // 显示文本
value: 1 // 对应值
}
]
},
// 是否置顶列配置
{
label: '是否置顶',
prop: 'isTop',
search: true,
slot: true,
type: 'select',
dicData: [
label: '是否置顶', // 列标题
prop: 'isTop', // 数据属性名
search: true, // 是否在搜索栏中显示此列的筛选条件
slot: true, // 是否使用自定义模板渲染这一列
type: 'select', // 输入类型为下拉选择
dicData: [ // 下拉选项数据
{
label: '否',
value: 0
label: '否', // 显示文本
value: 0 // 对应值
}, {
label: '是',
value: 1
label: '是', // 显示文本
value: 1 // 对应值
}
]
}

@ -1,43 +1,95 @@
export const tableOption = {
// 搜索菜单项所占的栅格数量默认是6总共24
searchMenuSpan: 6,
// 是否显示列设置按钮默认是true这里设为false表示不显示列设置按钮
columnBtn: false,
// 是否显示表格边框默认是false这里设为true表示显示边框
border: true,
// 是否显示索引列默认是false这里保持默认值不显示索引列
index: false,
// 是否显示多选框默认是false这里设为true表示启用多选功能
selection: true,
// 索引列的标题,默认是'序号'
indexLabel: '序号',
// 是否启用斑马纹默认是false这里设为true表示启用斑马纹
stripe: true,
// 表格内操作菜单的对齐方式,默认是'right',这里设为'center'表示居中对齐
menuAlign: 'center',
// 操作菜单栏的宽度默认值为150px这里设为350px
menuWidth: 350,
// 表格内容的对齐方式,默认是'left',这里设为'center'表示居中对齐
align: 'center',
// 是否显示刷新按钮默认是false这里设为true表示显示刷新按钮
refreshBtn: true,
// 搜索栏的尺寸,默认是'medium',这里设为'mini'表示使用小型搜索栏
searchSize: 'mini',
// 是否显示添加按钮默认是true这里设为false表示不显示添加按钮
addBtn: false,
// 是否显示编辑按钮默认是true这里设为false表示不显示编辑按钮
editBtn: false,
// 是否显示查看按钮默认是true这里设为false表示不显示查看按钮
viewBtn: false,
// 是否显示删除按钮默认是true这里设为false表示不显示删除按钮
delBtn: false,
// 设置下拉选项数据的label和value对应的属性名
props: {
label: 'label',
value: 'value'
label: 'label', // 下拉选项显示的文本属性名
value: 'value' // 下拉选项对应的值属性名
},
// 定义表格的列配置
column: [
// 自提点名称列配置
{
label: '自提点名称',
prop: 'addrName',
search: true
}, {
label: '手机号',
prop: 'mobile'
}, {
label: '省份',
prop: 'province'
}, {
label: '城市',
prop: 'city'
}, {
label: '区/县',
prop: 'area'
}, {
label: '地址',
prop: 'addr'
}]
label: '自提点名称', // 列标题
prop: 'addrName', // 数据属性名
search: true // 是否在搜索栏中显示此列的筛选条件
},
// 手机号码列配置
{
label: '手机号', // 列标题
prop: 'mobile' // 数据属性名
},
// 省份列配置
{
label: '省份', // 列标题
prop: 'province' // 数据属性名
},
// 城市列配置
{
label: '城市', // 列标题
prop: 'city' // 数据属性名
},
// 区/县列配置
{
label: '区/县', // 列标题
prop: 'area' // 数据属性名
},
// 地址列配置
{
label: '地址', // 列标题
prop: 'addr' // 数据属性名
}
]
}

@ -1,27 +1,65 @@
export const tableOption = {
// 搜索菜单项所占的栅格数量默认是6总共24
searchMenuSpan: 6,
// 是否显示列设置按钮默认是true这里设为false表示不显示列设置按钮
columnBtn: false,
// 是否显示表格边框默认是false这里设为true表示显示边框
border: true,
// 是否显示索引列默认是false这里保持默认值不显示索引列
index: false,
// 是否显示多选框默认是false这里设为true表示启用多选功能
selection: true,
// 索引列的标题,默认是'序号'
indexLabel: '序号',
// 是否启用斑马纹默认是false这里设为true表示启用斑马纹
stripe: true,
// 表格内操作菜单的对齐方式,默认是'right',这里设为'center'表示居中对齐
menuAlign: 'center',
// 操作菜单栏的宽度默认值为150px这里设为350px
menuWidth: 350,
// 表格内容的对齐方式,默认是'left',这里设为'center'表示居中对齐
align: 'center',
// 是否显示刷新按钮默认是false这里设为true表示显示刷新按钮
refreshBtn: true,
// 搜索栏的尺寸,默认是'medium',这里设为'mini'表示使用小型搜索栏
searchSize: 'mini',
// 是否显示添加按钮默认是true这里设为false表示不显示添加按钮
addBtn: false,
// 是否显示编辑按钮默认是true这里设为false表示不显示编辑按钮
editBtn: false,
delBtn: false,
// 是否显示查看按钮默认是true这里设为false表示不显示查看按钮
viewBtn: false,
// 是否显示删除按钮默认是true这里设为false表示不显示删除按钮
delBtn: false,
// 设置下拉选项数据的label和value对应的属性名
props: {
label: 'label',
value: 'value'
label: 'label', // 下拉选项显示的文本属性名
value: 'value' // 下拉选项对应的值属性名
},
column: [{
label: '模板名称',
prop: 'transName',
search: true
}]
// 定义表格的列配置
column: [
// 模板名称列配置
{
label: '模板名称', // 列标题
prop: 'transName', // 数据属性名
search: true // 是否在搜索栏中显示此列的筛选条件
}
]
}

@ -1,30 +1,58 @@
export const tableOption = {
// 搜索菜单项所占的栅格数量默认是6总共24
searchMenuSpan: 6,
// 是否显示列设置按钮默认是true这里设为false表示不显示列设置按钮
columnBtn: false,
// 是否显示表格边框默认是false这里设为true表示显示边框
border: true,
// 是否显示索引列默认是false这里设为true表示显示索引列
index: true,
// 索引列的标题,默认是'序号'
indexLabel: '序号',
// 是否启用斑马纹默认是false这里设为true表示启用斑马纹
stripe: true,
// 表格内操作菜单的对齐方式,默认是'right',这里设为'center'表示居中对齐
menuAlign: 'center',
// 表格内容的对齐方式,默认是'left',这里设为'center'表示居中对齐
align: 'center',
// 是否显示添加按钮默认是true这里设为false表示不显示添加按钮
addBtn: false,
// 是否显示编辑按钮默认是true这里设为false表示不显示编辑按钮
editBtn: false,
// 定义表格的列配置
column: [
// 区域ID列配置
{
label: '',
prop: 'areaId'
label: '', // 列标题为空,表示该列不会显示标题
prop: 'areaId' // 数据属性名
},
// 区域名称列配置
{
label: '',
prop: 'areaName'
label: '', // 列标题为空,表示该列不会显示标题
prop: 'areaName' // 数据属性名
},
// 父级ID列配置
{
label: '',
prop: 'parentId'
label: '', // 列标题为空,表示该列不会显示标题
prop: 'parentId' // 数据属性名
},
// 层级列配置
{
label: '',
prop: 'level'
label: '', // 列标题为空,表示该列不会显示标题
prop: 'level' // 数据属性名
}
]
}

@ -1,33 +1,77 @@
export const tableOption = {
// 搜索菜单项所占的栅格数量默认是6总共24
searchMenuSpan: 6,
// 是否显示列设置按钮默认是true这里设为false表示不显示列设置按钮
columnBtn: false,
// 是否显示表格边框默认是false这里设为true表示显示边框
border: true,
// 是否显示多选框默认是false这里设为true表示启用多选功能
selection: true,
// 是否显示索引列默认是false这里保持默认值不显示索引列
index: false,
// 索引列的标题,默认是'序号'
indexLabel: '序号',
// 是否启用斑马纹默认是false这里设为true表示启用斑马纹
stripe: true,
// 表格内操作菜单的对齐方式,默认是'right',这里设为'center'表示居中对齐
menuAlign: 'center',
// 操作菜单栏的宽度默认值为150px这里设为350px
menuWidth: 350,
// 表格内容的对齐方式,默认是'left',这里设为'center'表示居中对齐
align: 'center',
// 是否显示刷新按钮默认是false这里设为true表示显示刷新按钮
refreshBtn: true,
// 搜索栏的尺寸,默认是'medium',这里设为'mini'表示使用小型搜索栏
searchSize: 'mini',
// 是否显示添加按钮默认是true这里设为false表示不显示添加按钮
addBtn: false,
// 是否显示编辑按钮默认是true这里设为false表示不显示编辑按钮
editBtn: false,
delBtn: false,
// 是否显示查看按钮默认是true这里设为false表示不显示查看按钮
viewBtn: false,
// 是否显示删除按钮默认是true这里设为false表示不显示删除按钮
delBtn: false,
// 设置下拉选项数据的label和value对应的属性名
props: {
label: 'label',
value: 'value'
label: 'label', // 下拉选项显示的文本属性名
value: 'value' // 下拉选项对应的值属性名
},
column: [{
label: '参数名',
prop: 'paramKey',
search: true
}, {
label: '参数值',
prop: 'paramValue'
}, {
label: '备注',
prop: 'remark'
}]
// 定义表格的列配置
column: [
// 参数名列配置
{
label: '参数名', // 列标题
prop: 'paramKey', // 数据属性名
search: true // 是否在搜索栏中显示此列的筛选条件
},
// 参数值列配置
{
label: '参数值', // 列标题
prop: 'paramValue' // 数据属性名
},
// 备注列配置
{
label: '备注', // 列标题
prop: 'remark' // 数据属性名
}
]
}

@ -1,47 +1,105 @@
export const tableOption = {
// 搜索菜单项所占的栅格数量默认是6总共24
searchMenuSpan: 6,
// 是否显示列设置按钮默认是true这里设为false表示不显示列设置按钮
columnBtn: false,
// 是否显示表格边框默认是false这里设为true表示显示边框
border: true,
menu: false, // 移除操作栏
// 是否显示操作栏默认是true这里设为false表示移除操作栏
menu: false,
// 是否显示多选框默认是false这里设为true表示启用多选功能
selection: true,
// 是否显示索引列默认是false这里保持默认值不显示索引列
index: false,
// 索引列的标题,默认是'序号'
indexLabel: '序号',
// 是否启用斑马纹默认是false这里设为true表示启用斑马纹
stripe: true,
// 表格内操作菜单的对齐方式,默认是'right',这里设为'center'表示居中对齐
menuAlign: 'center',
// 操作菜单栏的宽度默认值为150px这里设为350px
menuWidth: 350,
// 表格内容的对齐方式,默认是'left',这里设为'center'表示居中对齐
align: 'center',
// 是否显示刷新按钮默认是false这里设为true表示显示刷新按钮
refreshBtn: true,
// 搜索栏的尺寸,默认是'medium',这里设为'mini'表示使用小型搜索栏
searchSize: 'mini',
// 是否显示添加按钮默认是true这里设为false表示不显示添加按钮
addBtn: false,
// 是否显示编辑按钮默认是true这里设为false表示不显示编辑按钮
editBtn: false,
delBtn: false,
// 是否显示查看按钮默认是true这里设为false表示不显示查看按钮
viewBtn: false,
// 是否显示删除按钮默认是true这里设为false表示不显示删除按钮
delBtn: false,
// 设置下拉选项数据的label和value对应的属性名
props: {
label: 'label',
value: 'value'
label: 'label', // 下拉选项显示的文本属性名
value: 'value' // 下拉选项对应的值属性名
},
column: [{
label: '用户名',
prop: 'username',
search: true
}, {
label: '用户操作',
prop: 'operation',
search: true
}, {
label: '请求方法',
prop: 'method'
}, {
label: '请求参数',
prop: 'params'
}, {
label: '执行时长(毫秒)',
prop: 'time'
}, {
label: 'IP地址',
prop: 'ip'
}, {
label: '创建时间',
prop: 'createDate'
}]
// 定义表格的列配置
column: [
// 用户名列配置
{
label: '用户名', // 列标题
prop: 'username', // 数据属性名
search: true // 是否在搜索栏中显示此列的筛选条件
},
// 用户操作列配置
{
label: '用户操作', // 列标题
prop: 'operation', // 数据属性名
search: true // 是否在搜索栏中显示此列的筛选条件
},
// 请求方法列配置
{
label: '请求方法', // 列标题
prop: 'method' // 数据属性名
},
// 请求参数列配置
{
label: '请求参数', // 列标题
prop: 'params' // 数据属性名
},
// 执行时长(毫秒)列配置
{
label: '执行时长(毫秒)', // 列标题
prop: 'time' // 数据属性名
},
// IP地址列配置
{
label: 'IP地址', // 列标题
prop: 'ip' // 数据属性名
},
// 创建时间列配置
{
label: '创建时间', // 列标题
prop: 'createDate' // 数据属性名
}
]
}

@ -1,33 +1,77 @@
export const tableOption = {
// 搜索菜单项所占的栅格数量默认是6总共24
searchMenuSpan: 6,
// 是否显示列设置按钮默认是true这里设为false表示不显示列设置按钮
columnBtn: false,
// 是否显示表格边框默认是false这里设为true表示显示边框
border: true,
// 是否显示多选框默认是false这里设为true表示启用多选功能
selection: true,
// 是否显示索引列默认是false这里保持默认值不显示索引列
index: false,
// 索引列的标题,默认是'序号'
indexLabel: '序号',
// 是否启用斑马纹默认是false这里设为true表示启用斑马纹
stripe: true,
// 表格内操作菜单的对齐方式,默认是'right',这里设为'center'表示居中对齐
menuAlign: 'center',
// 操作菜单栏的宽度默认值为150px这里设为350px
menuWidth: 350,
// 表格内容的对齐方式,默认是'left',这里设为'center'表示居中对齐
align: 'center',
// 是否显示刷新按钮默认是false这里设为true表示显示刷新按钮
refreshBtn: true,
// 搜索栏的尺寸,默认是'medium',这里设为'mini'表示使用小型搜索栏
searchSize: 'mini',
// 是否显示添加按钮默认是true这里设为false表示不显示添加按钮
addBtn: false,
// 是否显示编辑按钮默认是true这里设为false表示不显示编辑按钮
editBtn: false,
// 是否显示删除按钮默认是true这里设为false表示不显示删除按钮
delBtn: false,
// 是否显示查看按钮默认是true这里设为false表示不显示查看按钮
viewBtn: false,
// 设置下拉选项数据的label和value对应的属性名
props: {
label: 'label',
value: 'value'
label: 'label', // 下拉选项显示的文本属性名
value: 'value' // 下拉选项对应的值属性名
},
column: [{
label: '角色名称',
prop: 'roleName',
search: true
}, {
label: '备注',
prop: 'remark'
}, {
label: '创建时间',
prop: 'createTime'
}]
// 定义表格的列配置
column: [
// 角色名称列配置
{
label: '角色名称', // 列标题
prop: 'roleName', // 数据属性名
search: true // 是否在搜索栏中显示此列的筛选条件
},
// 备注列配置
{
label: '备注', // 列标题
prop: 'remark' // 数据属性名
},
// 创建时间列配置
{
label: '创建时间', // 列标题
prop: 'createTime' // 数据属性名
}
]
}

@ -1,50 +1,99 @@
export const tableOption = {
// 搜索菜单项所占的栅格数量默认是6总共24
searchMenuSpan: 6,
// 是否显示列设置按钮默认是true这里设为false表示不显示列设置按钮
columnBtn: false,
// 是否显示表格边框默认是false这里设为true表示显示边框
border: true,
// 是否显示多选框默认是false这里设为true表示启用多选功能
selection: true,
// 是否显示索引列默认是false这里保持默认值不显示索引列
index: false,
// 索引列的标题,默认是'序号'
indexLabel: '序号',
// 是否启用斑马纹默认是false这里设为true表示启用斑马纹
stripe: true,
// 表格内操作菜单的对齐方式,默认是'right',这里设为'center'表示居中对齐
menuAlign: 'center',
// 操作菜单栏的宽度默认值为150px这里设为350px
menuWidth: 350,
// 表格内容的对齐方式,默认是'left',这里设为'center'表示居中对齐
align: 'center',
// 是否显示刷新按钮默认是false这里设为true表示显示刷新按钮
refreshBtn: true,
// 搜索栏的尺寸,默认是'medium',这里设为'mini'表示使用小型搜索栏
searchSize: 'mini',
// 是否显示添加按钮默认是true这里设为false表示不显示添加按钮
addBtn: false,
// 是否显示编辑按钮默认是true这里设为false表示不显示编辑按钮
editBtn: false,
// 是否显示删除按钮默认是true这里设为false表示不显示删除按钮
delBtn: false,
// 是否显示查看按钮默认是true这里设为false表示不显示查看按钮
viewBtn: false,
// 设置下拉选项数据的label和value对应的属性名
props: {
label: 'label',
value: 'value'
label: 'label', // 下拉选项显示的文本属性名
value: 'value' // 下拉选项对应的值属性名
},
column: [{
label: '用户名',
prop: 'username',
search: true
}, {
label: '邮箱',
prop: 'email'
}, {
label: '手机号',
prop: 'mobile'
}, {
label: '创建时间',
prop: 'createTime'
}, {
label: '状态',
prop: 'status',
type: 'select',
dicData: [
{
label: '禁用',
value: 0
}, {
label: '正常',
value: 1
}
]
}]
// 定义表格的列配置
column: [
// 用户名列配置
{
label: '用户名', // 列标题
prop: 'username', // 数据属性名
search: true // 是否在搜索栏中显示此列的筛选条件
},
// 邮箱列配置
{
label: '邮箱', // 列标题
prop: 'email' // 数据属性名
},
// 手机号码列配置
{
label: '手机号', // 列标题
prop: 'mobile' // 数据属性名
},
// 创建时间列配置
{
label: '创建时间', // 列标题
prop: 'createTime' // 数据属性名
},
// 状态列配置
{
label: '状态', // 列标题
prop: 'status', // 数据属性名
type: 'select', // 输入类型为下拉选择
dicData: [ // 下拉选项数据
{
label: '禁用', // 显示文本
value: 0 // 对应值
}, {
label: '正常', // 显示文本
value: 1 // 对应值
}
]
}
]
}

@ -1,80 +1,126 @@
export const tableOption = {
// 搜索菜单项所占的栅格数量默认是6总共24
searchMenuSpan: 6,
// 是否显示列设置按钮默认是true这里设为false表示不显示列设置按钮
columnBtn: false,
// 是否显示表格边框默认是false这里设为true表示显示边框
border: true,
// 是否显示索引列默认是false这里设为true表示显示索引列
index: true,
// 索引列的标题,默认是'序号'
indexLabel: '序号',
// 是否启用斑马纹默认是false这里设为true表示启用斑马纹
stripe: true,
// 表格内操作菜单的对齐方式,默认是'right',这里设为'center'表示居中对齐
menuAlign: 'center',
// 表格内容的对齐方式,默认是'left',这里设为'center'表示居中对齐
align: 'center',
// 是否显示添加按钮默认是true这里设为false表示不显示添加按钮
addBtn: false,
// 是否显示编辑按钮默认是true这里设为false表示不显示编辑按钮
editBtn: false,
// 是否显示删除按钮默认是true这里设为false表示不显示删除按钮
delBtn: false,
// 是否显示查看按钮默认是true这里设为false表示不显示查看按钮
viewBtn: false,
// 定义表格的列配置
column: [
// 收货人名称列配置
{
label: '收货人名称',
prop: 'receiver'
label: '收货人名称', // 列标题
prop: 'receiver' // 数据属性名
},
// 省份列配置
{
label: '省',
prop: 'province'
label: '省', // 列标题
prop: 'province' // 数据属性名
},
// 城市列配置
{
label: '城市',
prop: 'city'
label: '城市', // 列标题
prop: 'city' // 数据属性名
},
// 区域列配置
{
label: '区',
prop: 'area'
label: '区', // 列标题
prop: 'area' // 数据属性名
},
// 地址列配置
{
label: '地址',
prop: 'addr'
label: '地址', // 列标题
prop: 'addr' // 数据属性名
},
// 邮编列配置
{
label: '邮编',
prop: 'postCode'
label: '邮编', // 列标题
prop: 'postCode' // 数据属性名
},
// 手机号码列配置
{
label: '手机',
prop: 'mobile'
label: '手机', // 列标题
prop: 'mobile' // 数据属性名
},
// 状态列配置
{
label: '状态',
prop: 'status',
search: true,
type: 'select',
dicData: [
label: '状态', // 列标题
prop: 'status', // 数据属性名
search: true, // 是否在搜索栏中显示此列的筛选条件
type: 'select', // 输入类型为下拉选择
dicData: [ // 下拉选项数据
{
label: '无效',
value: 0
label: '无效', // 显示文本
value: 0 // 对应值
}, {
label: '正常',
value: 1
label: '正常', // 显示文本
value: 1 // 对应值
}
]
},
// 默认地址列配置
{
label: '默认地址',
prop: 'commonAddr',
dicData: [
label: '默认地址', // 列标题
prop: 'commonAddr', // 数据属性名
dicData: [ // 下拉选项数据
{
label: '否',
value: 0
label: '否', // 显示文本
value: 0 // 对应值
}, {
label: '是',
value: 1
label: '是', // 显示文本
value: 1 // 对应值
}
]
},
// 建立时间列配置
{
label: '建立时间',
prop: 'createTime'
label: '建立时间', // 列标题
prop: 'createTime' // 数据属性名
},
// 更新时间列配置
{
label: '更新时间',
prop: 'updateTime'
label: '更新时间', // 列标题
prop: 'updateTime' // 数据属性名
}
]
}

@ -1,53 +1,100 @@
export const tableOption = {
// 搜索菜单项所占的栅格数量默认是6总共24
searchMenuSpan: 6,
// 是否显示列设置按钮默认是true这里设为false表示不显示列设置按钮
columnBtn: false,
// 是否显示表格边框默认是false这里设为true表示显示边框
border: true,
// 注释掉的选择框配置默认是false若启用则表示启用多选功能
// selection: true,
// 是否显示索引列默认是false这里保持默认值不显示索引列
index: false,
// 索引列的标题,默认是'序号'
indexLabel: '序号',
// 是否启用斑马纹默认是false这里设为true表示启用斑马纹
stripe: true,
// 表格内操作菜单的对齐方式,默认是'right',这里设为'center'表示居中对齐
menuAlign: 'center',
// 操作菜单栏的宽度默认值为150px这里设为350px
menuWidth: 350,
// 表格内容的对齐方式,默认是'left',这里设为'center'表示居中对齐
align: 'center',
// 是否显示刷新按钮默认是false这里设为true表示显示刷新按钮
refreshBtn: true,
// 搜索栏的尺寸,默认是'medium',这里设为'mini'表示使用小型搜索栏
searchSize: 'mini',
// 是否显示添加按钮默认是true这里设为false表示不显示添加按钮
addBtn: false,
// 是否显示编辑按钮默认是true这里设为false表示不显示编辑按钮
editBtn: false,
// 是否显示删除按钮默认是true这里设为false表示不显示删除按钮
delBtn: false,
// 是否显示查看按钮默认是true这里设为false表示不显示查看按钮
viewBtn: false,
// 设置下拉选项数据的label和value对应的属性名
props: {
label: 'label',
value: 'value'
label: 'label', // 下拉选项显示的文本属性名
value: 'value' // 下拉选项对应的值属性名
},
column: [{
label: '用户昵称',
prop: 'nickName',
search: true
}, {
label: '用户头像',
prop: 'pic',
type: 'upload',
imgWidth: 150,
listType: 'picture-img',
slot: true
}, {
label: '状态',
prop: 'status',
search: true,
type: 'select',
slot: true,
dicData: [
{
label: '禁用',
value: 0
}, {
label: '正常',
value: 1
}
]
}, {
label: '注册时间',
prop: 'userRegtime',
imgWidth: 150
}]
// 定义表格的列配置
column: [
// 用户昵称列配置
{
label: '用户昵称', // 列标题
prop: 'nickName', // 数据属性名
search: true // 是否在搜索栏中显示此列的筛选条件
},
// 用户头像列配置
{
label: '用户头像', // 列标题
prop: 'pic', // 数据属性名
type: 'upload', // 输入类型为上传
imgWidth: 150, // 图片宽度
listType: 'picture-img', // 图片列表类型
slot: true // 使用插槽自定义内容
},
// 状态列配置
{
label: '状态', // 列标题
prop: 'status', // 数据属性名
search: true, // 是否在搜索栏中显示此列的筛选条件
type: 'select', // 输入类型为下拉选择
slot: true, // 使用插槽自定义内容
dicData: [ // 下拉选项数据
{
label: '禁用', // 显示文本
value: 0 // 对应值
}, {
label: '正常', // 显示文本
value: 1 // 对应值
}
]
},
// 注册时间列配置
{
label: '注册时间', // 列标题
prop: 'userRegtime', // 数据属性名
imgWidth: 150 // 这里可能是误置的属性imgWidth通常用于图片展示对于时间字段可能不需要
}
]
}

@ -1,85 +1,125 @@
<template>
<div>
<!-- 条件渲染如果当前路由配置了 isTab 属性则显示顶部卡片 -->
<el-card
v-if="route.meta.isTab"
class="main-head"
class="main-head"
>
<el-breadcrumb :separator-icon="ArrowRight">
<el-breadcrumb-item
v-for="(item, index) in selectMenu"
:key="index"
class="breadcrumb-item"
>
<span>{{ item }}</span>
</el-breadcrumb-item>
</el-breadcrumb>
<!-- 面包屑导航 -->
<el-breadcrumb :separator-icon="ArrowRight"> <!-- 使用 ArrowRight 图标作为分隔符 -->
<el-breadcrumb-item
v-for="(item, index) in selectMenu"
:key="index"
class="breadcrumb-item"
>
<span>{{ item }}</span> <!-- 显示面包屑项的文本内容 -->
</el-breadcrumb-item>
</el-breadcrumb>
</el-card>
<!-- 主内容区域 -->
<!-- 主内容区容器使用 site-content 类名进行样式定义 -->
<!-- 动态绑定类名如果当前路由配置了 isTab则添加 site-content--tabs -->
<main
class="site-content"
:class="{ 'site-content--tabs': route.meta.isTab }"
:class="{ 'site-content--tabs': route.meta.isTab }"
>
<!-- 主入口标签页 (发布商品) -->
<div
v-if="route.name === 'prod-post-product/postProduct'"
:style="siteContentViewHeight"
>
<keep-alive>
<router-view />
</keep-alive>
</div>
<el-card
v-else-if="homeHidden"
class="card-content-h"
style="border-radius: 0 !important; box-shadow: none"
:body-style="siteContentViewHeight"
>
<router-view />
</el-card>
<div v-else>
<router-view />
</div>
</main>
<!-- 条件渲染根据当前路由名称和 meta 属性显示不同内容 -->
<!-- 发布商品页面 (prod-post-product/postProduct) -->
<!-- 如果当前路由名称为 prod-post-product/postProduct -->
<!-- 动态设置样式确保内容区有足够的高度 -->
<!-- 使用 keep-alive 组件缓存视图提高性能 -->
<!-- 渲染匹配到的子组件 -->
<div
v-if="route.name === 'prod-post-product/postProduct'"
:style="siteContentViewHeight"
>
<keep-alive>
<router-view />
</keep-alive>
</div>
<!-- 其他页面且 homeHidden true -->
<!-- 如果 homeHidden 计算属性为 true -->
<!-- 使用 card-content-h 类名进行样式定义 -->
<!-- 移除边框圆角和阴影效果 -->
<!-- 动态设置内容区高度 -->
<el-card
v-else-if="homeHidden"
class="card-content-h"
style="border-radius: 0 !important; box-shadow: none"
:body-style="siteContentViewHeight"
>
<router-view /> <!-- 渲染匹配到的子组件 -->
</el-card>
<!-- 默认情况首页或其他不需要特殊处理的页面 -->
<div v-else>
<router-view /> <!-- 渲染匹配到的子组件 -->
</div>
</main>
</div>
</template>
<script setup>
import { ArrowRight } from '@element-plus/icons-vue'
const route = useRoute()
import { ArrowRight } from '@element-plus/icons-vue' // Element Plus
import { ref, computed } from 'vue' // Vue API
import { useRoute } from 'vue-router' // vue-router useRoute
import { isURL } from '@/utils/validate' // URL
import { useCommonStore } from '@/store' // Vuex store commonStore
//
const documentClientHeight = ref(document.documentElement.clientHeight)
//
window.addEventListener('resize', () => {
documentClientHeight.value = document.documentElement.clientHeight
})
//
const siteContentViewHeight = computed(() => {
let height = documentClientHeight.value - 50 - 30 - 2
if (route.meta.isTab) {
height -= 40
return isURL(route.meta.iframeUrl) ? { height: height + 'px' } : { minHeight: height + 'px' }
let height = documentClientHeight.value - 50 - 30 - 2 //
if (route.meta.isTab) { // isTab
height -= 40 //
return isURL(route.meta.iframeUrl) // iframe
? { height: height + 'px' } // iframe
: { minHeight: height + 'px' }; //
}
return { minHeight: height + 'px' }
return { minHeight: height + 'px' }; //
})
// Store
const commonStore = useCommonStore()
//
const selectMenu = computed(() => commonStore.selectMenu)
//
const homeHidden = computed(() => route.name !== 'home')
</script>
<style scoped>
/* 主头部样式 */
.main-head {
background: #ffffff;
width: 100%;
height: 40px;
position: fixed;
top: 50px;
z-index: 10;
display: flex;
align-items: center;
border-radius: 0;
box-shadow: none;
border-top: none;
background: #ffffff; /* 白色背景 */
width: 100%; /* 宽度占满整个容器 */
height: 40px; /* 固定高度 */
position: fixed; /* 固定定位 */
top: 50px; /* 距离顶部 50px */
z-index: 10; /* 设置堆叠顺序,确保在其他内容之上 */
display: flex; /* 使用弹性布局 */
align-items: center; /* 垂直居中对齐子元素 */
border-radius: 0; /* 移除边框圆角 */
box-shadow: none; /* 移除阴影效果 */
border-top: none; /* 移除顶部边框 */
}
/* 面包屑最后一项样式 */
.breadcrumb-item:last-child span {
color: #155bd4 !important;
color: #155bd4 !important; /* 设置文本颜色为蓝色,并强制覆盖其他样式 */
}
/* 卡片内容区域最小高度 */
.card-content-h {
min-height: calc(100vh - 50px - 60px - 20px);
min-height: calc(100vh - 50px - 60px - 20px); /* 动态计算最小高度,确保内容区有足够的空间 */
}
</style>

@ -1,121 +1,157 @@
<template>
<!-- 修改密码对话框 -->
<el-dialog
v-model="visible"
:title="'修改密码'"
:append-to-body="true"
:title="'修改密码'"
:append-to-body="true"
>
<el-form
ref="dataFormRef"
:model="dataForm"
:rules="dataRule"
label-width="80px"
@keyup.enter="onSubmit(dataFormRef)"
>
<el-form-item label="账号">
<span>{{ userName }}</span>
</el-form-item>
<el-form-item
label="原密码"
prop="password"
>
<el-input
v-model="dataForm.password"
type="password"
/>
</el-form-item>
<el-form-item
label="新密码"
prop="newPassword"
>
<el-input
v-model="dataForm.newPassword"
type="password"
/>
</el-form-item>
<el-form-item
label="确定密码"
prop="confirmPassword"
>
<el-input
v-model="dataForm.confirmPassword"
type="password"
/>
</el-form-item>
</el-form>
<template #footer>
<!-- 表单容器 -->
<el-form
ref="dataFormRef"
:model="dataForm"
:rules="dataRule"
label-width="80px"
@keyup.enter="onSubmit(dataFormRef)"
>
<!-- 账号信息 -->
<el-form-item label="账号">
<span>{{ userName }}</span> <!-- 显示当前用户名 -->
</el-form-item>
<!-- 原密码输入项 -->
<el-form-item
label="原密码"
prop="password"
>
<el-input
v-model="dataForm.password"
type="password"
/>
</el-form-item>
<!-- 新密码输入项 -->
<el-form-item
label="新密码"
prop="newPassword"
>
<el-input
v-model="dataForm.newPassword"
type="password"
/>
</el-form-item>
<!-- 确认密码输入项 -->
<el-form-item
label="确定密码"
prop="confirmPassword"
>
<el-input
v-model="dataForm.confirmPassword"
type="password"
/>
</el-form-item>
</el-form>
<!-- 对话框底部按钮区域 -->
<template #footer>
<span class="dialog-footer">
<el-button @click="visible = false">取消</el-button>
<el-button @click="visible = false">取消</el-button> <!-- -->
<el-button
type="primary"
@click="onSubmit(dataFormRef)"
>确定</el-button>
</span>
</template>
</template>
</el-dialog>
</template>
<script setup>
import { ElMessage } from 'element-plus'
const visible = ref(false)
const dataForm = reactive({
password: '',
newPassword: '',
confirmPassword: ''
import { ElMessage } from 'element-plus' // Element Plus
import { ref, reactive, computed, nextTick } from 'vue' // Vue API
import { useUserStore } from '@/store' // Vuex store
import { useRouter } from 'vue-router' // vue-router
import http from '@/utils/http' // HTTP
import { encrypt } from '@/utils/encrypt' //
import { clearLoginInfo } from '@/utils/auth' //
//
const visible = ref(false) //
const dataForm = reactive({ //
password: '', //
newPassword: '', //
confirmPassword: '' //
})
const dataFormRef = ref()
const dataFormRef = ref() //
//
const init = () => {
visible.value = true
visible.value = true //
nextTick(() => {
dataFormRef.value?.resetFields()
dataFormRef.value?.resetFields() // DOM
})
}
// eslint-disable-next-line no-unused-vars
//
const validateConfirmPassword = (rule, value, callback) => {
if (dataForm.newPassword !== value) {
callback(new Error('确认密码与新密码不一致'))
callback(new Error('确认密码与新密码不一致')) //
} else {
callback()
callback() //
}
}
//
const dataRule = reactive({
password: [{ required: true, message: '原密码不能为空', trigger: 'blur' }],
newPassword: [{ required: true, message: '新密码不能为空', trigger: 'blur' }],
password: [{ required: true, message: '原密码不能为空', trigger: 'blur' }], //
newPassword: [{ required: true, message: '新密码不能为空', trigger: 'blur' }], //
confirmPassword: [
{ required: true, message: '确认密码不能为空', trigger: 'blur' },
{ validator: validateConfirmPassword, trigger: 'blur' }
{ required: true, message: '确认密码不能为空', trigger: 'blur' }, //
{ validator: validateConfirmPassword, trigger: 'blur' } //
]
})
// Store
const userStore = useUserStore()
//
const userName = computed(() => userStore.name)
//
const router = useRouter()
//
const onSubmit = async formEl => {
if (!formEl) return
await formEl.validate(valid => {
if (valid) {
if (!formEl) return //
await formEl.validate(valid => { //
if (valid) { //
http({
url: http.adornUrl('/sys/user/password'),
method: 'post',
url: http.adornUrl('/sys/user/password'), // API
method: 'post', //
data: http.adornData({
password: encrypt(dataForm.password),
newPassword: encrypt(dataForm.newPassword)
password: encrypt(dataForm.password), //
newPassword: encrypt(dataForm.newPassword) //
})
}).then(() => {
ElMessage({
message: '操作?',
message: '密码修改成功', //
type: 'success',
duration: 1500,
onClose: () => {
visible.value = false
visible.value = false //
nextTick(() => {
clearLoginInfo()
router.replace({ name: 'login' })
clearLoginInfo() //
router.replace({ name: 'login' }) //
})
}
})
}).catch(error => {
ElMessage.error('密码修改失败,请稍后再试') //
})
}
})
}
// init
defineExpose({ init })
</script>

@ -1,172 +1,219 @@
<template>
<div>
<!-- 导航栏容器 -->
<nav class="site-navbar">
<!--左侧-->
<!-- 左侧区域 -->
<div
class="site-navbar-header"
:style="{ 'margin-right': sidebarFold ? 0 : '20px' }"
>
<!-- <img-->
<!-- class="menu-image-logo"-->
<!-- :src="configuration.bsTopBarIcon"-->
<!-- alt="logo"-->
<!-- >-->
<span
v-if="!sidebarFold"
class="site-navbar-lg"
>
mall4j建站后台
</span>
<span
v-else
class="site-navbar-mini"
:style="fontCloseSize"
>
mall4j
</span>
</div>
<!--右侧数据-->
<div class="site-navbar-content">
<div class="navbar-content-left">
<svg-icon
class="left-item"
icon-class="icon-zhedie"
@click="setSidebarFold"
/>
</div>
<div class="navbar-content-right">
<el-dropdown
class="content-right-item"
:show-timeout="0"
placement="bottom"
>
<span class="el-dropdown-link">{{ userName }}</span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item @click="updatePasswordHandle">
修改密码
</el-dropdown-item>
<el-dropdown-item @click="logoutHandle">
退出
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</div>
<!-- 弹窗, 修改密码 -->
<UpdatePassword
v-if="updatePassowrdVisible"
ref="updatePassowrdRef"
<!-- Logo 或标题 -->
<!--
<img
class="menu-image-logo"
:src="configuration.bsTopBarIcon"
alt="logo"
>
-->
<!-- 标题文本根据侧边栏折叠状态显示不同内容 -->
<span
v-if="!sidebarFold"
class="site-navbar-lg"
>
mall4j建站后台
</span>
<span
v-else
class="site-navbar-mini"
:style="fontCloseSize"
>
mall4j
</span>
</div>
<!-- 右侧数据区域 -->
<div class="site-navbar-content">
<!-- 左侧操作项 -->
<div class="navbar-content-left">
<!-- 折叠/展开侧边栏按钮 -->
<svg-icon
class="left-item"
icon-class="icon-zhedie"
@click="setSidebarFold"
/>
</nav>
</div>
<!-- 右侧用户操作项 -->
<div class="navbar-content-right">
<!-- 用户下拉菜单 -->
<el-dropdown
class="content-right-item"
:show-timeout="0"
placement="bottom"
>
<span class="el-dropdown-link">{{ userName }}</span> <!-- 显示当前用户名 -->
<!-- 下拉菜单内容 -->
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item @click="updatePasswordHandle"> <!-- -->
修改密码
</el-dropdown-item>
<el-dropdown-item @click="logoutHandle"> <!-- 退 -->
退出
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</div>
<!-- 修改密码弹窗 -->
<UpdatePassword
v-if="updatePassowrdVisible"
ref="updatePassowrdRef"
/>
</nav>
</div>
</template>
<script setup>
import { ElMessageBox } from 'element-plus'
import UpdatePassword from './main-navbar-update-password.vue'
import { ElMessageBox } from 'element-plus' // Element Plus
import UpdatePassword from './main-navbar-update-password.vue'//
import { computed, reactive, ref, nextTick } from 'vue' // Vue API
import { useRoute, useRouter } from 'vue-router' // vue-router
import { useUserStore, useCommonStore } from '@/store' // Vuex store
import http from '@/utils/http' // HTTP
import { clearLoginInfo } from '@/utils/auth' //
//
const route = useRoute()
//
const router = useRouter()
// Store
const userStore = useUserStore()
//
const userName = computed(() => userStore.name)
//
const fontCloseSize = reactive({
fontSize: '16px'
})
// Store
const commonStore = useCommonStore()
//
const sidebarFold = computed(() => commonStore.sidebarFold)
//
const setSidebarFold = () => {
const len = commonStore.selectMenu.length
const flag = sessionStorage.getItem('isExpand')
if ((route.path === '/home' || len === 1) && flag === '0') {
commonStore.updateSidebarFold(true)
const len = commonStore.selectMenu.length //
const flag = sessionStorage.getItem('isExpand') //
if ((route.path === '/home' || len === 1) && flag === '0') { // '0'
commonStore.updateSidebarFold(true) //
} else {
const foldFlag = sidebarFold.value
commonStore.updateSidebarFold(!foldFlag)
const foldFlag = sidebarFold.value //
commonStore.updateSidebarFold(!foldFlag) //
}
}
// 退
const logoutHandle = () => {
// 使 Element Plus 退
ElMessageBox.confirm('确定进行[退出]操作?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
// POST
http({
url: http.adornUrl('/logOut'),
method: 'post',
data: http.adornData()
url: http.adornUrl('/logOut'), // API
method: 'post', //
data: http.adornData() //
}).then(() => {
clearLoginInfo()
router.push({ name: 'login' })
clearLoginInfo() //
router.push({ name: 'login' }) //
})
})
}
//
const updatePassowrdVisible = ref(false)
//
const updatePassowrdRef = ref(null)
/**
* 修改密码
* 函数处理修改密码操作
*/
const updatePasswordHandle = () => {
updatePassowrdVisible.value = true
updatePassowrdVisible.value = true //
nextTick(() => {
updatePassowrdRef.value?.init()
updatePassowrdRef.value?.init() // DOM
})
}
</script>
<style lang="scss" scoped>
.site-navbar {
display: flex;
align-items: center;
background-color: #ffffff;
color: #333333;
border-bottom: 1px solid #ebedf0;
display: flex; // 使
align-items: center; //
background-color: #ffffff; //
color: #333333; //
border-bottom: 1px solid #ebedf0; //
.site-navbar-header {
display: flex;
align-items: center;
margin-left: 20px;
color: #333;
font-weight: 700;
height: 50px;
line-height: 50px;
display: flex; // 使
align-items: center; //
margin-left: 20px; // 20px
color: #333; //
font-weight: 700; // 700
height: 50px; // 50px
line-height: 50px; // 50px使
.site-navbar-lg {
font-size: 16px;
word-break: break-all;
word-wrap: break-word;
font-size: 16px; // 16px
word-break: break-all; // URL
word-wrap: break-word; // URL
}
.site-navbar-lg,
.site-navbar-mini {
margin: 0 5px;
margin: 0 5px; // 5px
}
}
.site-navbar-content {
flex: 1;
width: 100%;
display: flex;
justify-content: space-between;
padding: 0 20px;
font-size: 18px;
align-items: center;
flex: 1; //
width: 100%; //
display: flex; // 使
justify-content: space-between; //
padding: 0 20px; // 20px
font-size: 18px; // 18px
align-items: center; //
.navbar-content-left {
flex: 1;
flex: 1; //
.left-item {
cursor: pointer;
cursor: pointer; //
}
}
.navbar-content-right {
display: flex;
display: flex; // 使
}
}
}
//
//.menu-image-logo {
// object-fit: contain;
// height: 18px;
// width: 59px;
// margin-right: 10px;
// object-fit: contain; //
// height: 18px; // 18px
// width: 59px; // 59px
// margin-right: 10px; // 10px
//}
</style>

@ -1,125 +1,154 @@
<template>
<div class="menu-mod">
<!-- 如果 expandMenu 包含子菜单项 (list) -->
<div v-if="expandMenu.list">
<!-- 使用 el-sub-menu 创建一个可展开的菜单项 -->
<el-sub-menu
v-if="!item.hidden"
:index="expandMenu.menuId + ''"
:index="expandMenu.menuId + ''"
>
<template #title>
<span
style="font-size: 14px"
:title="expandMenu.name.length > 4 ? expandMenu.name : ''"
>{{ expandMenu.name }}</span>
<template #title>
<!-- 菜单项标题 -->
<span
style="font-size: 14px"
:title="expandMenu.name.length > 4 ? expandMenu.name : ''"
>{{ expandMenu.name }}</span>
</template>
<!-- 使用 el-menu-item-group 包装子菜单项 -->
<el-menu-item-group class="menu-right-el-item">
<template v-for="menu in expandMenu.list"> <!-- -->
<!-- 每个子菜单项 -->
<el-menu-item
v-if="!menu.hidden"
:key="menu.menuId"
style="
font-size: 14px !important; <!-- 设置字体大小为 14px -->
line-height: 40px;
padding-left: 30px !important; <!-- 设置左侧内边距为 30px -->
padding-right: 10px !important; <!-- 设置右侧内边距为 10px -->
"
class="menu-right-el-item is-active item-text"
:class="{ 'menu-active': selectRightId === menu.menuId }"
@click="gotoRouteHandle(menu)"
>
<span :title="menu.name.length > 4 ? menu.name : ''">{{ menu.name }}</span> <!-- 显示子菜单名称 -->
<!-- 如果子菜单还有子菜单则递归渲染 SubMenuItem 组件 -->
<SubMenuItem
v-if="menu.list"
:expand-menu="menu"
/>
</el-menu-item>
</template>
<el-menu-item-group class="menu-right-el-item">
<template v-for="menu in expandMenu.list">
<el-menu-item
v-if="!menu.hidden"
:key="menu.menuId"
style="
font-size: 14px !important;
line-height: 40px;
padding-left: 30px !important;
padding-right: 10px !important;
"
class="menu-right-el-item is-active item-text"
:class="{ 'menu-active': selectRightId === menu.menuId }"
@click="gotoRouteHandle(menu)"
>
<span :title="menu.name.length > 4 ? menu.name : ''">{{ menu.name }}</span>
<SubMenuItem
v-if="menu.list"
:expand-menu="menu"
/>
</el-menu-item>
</template>
</el-menu-item-group>
</el-menu-item-group>
</el-sub-menu>
</div>
<!-- 如果 expandMenu 没有子菜单项 (list) -->
<div v-else>
<!-- 使用 el-menu-item 创建一个不可展开的菜单项 -->
<el-menu-item
v-if="!expandMenu.hidden"
:key="expandMenu.menuId"
style="font-size: 14px !important; padding-left: 15px !important; line-height: 40px"
class="menu-right-el-item is-active item-text"
:class="{ 'menu-active': selectRightId === expandMenu.menuId }"
@click="gotoRouteHandle(expandMenu)"
:key="expandMenu.menuId"
style="font-size: 14px !important; padding-left: 15px !important; line-height: 40px"
class="menu-right-el-item is-active item-text"
:class="{ 'menu-active': selectRightId === expandMenu.menuId }"
@click="gotoRouteHandle(expandMenu)"
>
<span :title="expandMenu.name.length > 4 ? expandMenu.name : ''">{{
expandMenu.name
}}</span>
<span :title="expandMenu.name.length > 4 ? expandMenu.name : ''">{{ expandMenu.name }}</span> <!-- 显示菜单名称 -->
</el-menu-item>
</div>
</div>
</template>
<script setup>
//
import SubMenuItem from './main-sidebar-sub-menu-item.vue'
// (props)
const props = defineProps({
expandMenu: {
type: Object,
default: () => {}
type: Object, //
default: () => ({}) //
},
menuIndex: {
type: String,
default: ''
type: String, //
default: '' //
}
})
// 使 ref props.expandMenu
const item = ref(props.expandMenu)
// Vuex store
const commonStore = useCommonStore()
// ID
const selectRightId = computed(() => commonStore.selectRightId)
const commonStore = useCommonStore()
// Vue Router
const router = useRouter()
//
// routeHandle
watch(
() => router.currentRoute,
() => router.currentRoute.value, //
route => {
routeHandle(route)
routeHandle(route) // routeHandle
}
)
//
//
const routeHandle = route => {
if (route.meta.isTab) {
commonStore.updateSelectRightId(route.meta.menuId || '')
if (route.meta.isTab) { // isTab
commonStore.updateSelectRightId(route.meta.menuId || '') // ID
}
}
// menuId()
// menuId ()
const gotoRouteHandle = menu => {
if (router.currentRoute.value.name === menu.url) {
if (router.currentRoute.value.name === menu.url) { //
return
}
router.push({ name: menu.url })
router.push({ name: menu.url }) //
}
</script>
<style lang="scss" scoped>
.menu-mod {
/* 定义右侧菜单项的基本样式 */
.menu-right-el-item {
/* 当菜单项处于激活状态时的样式 */
&.is-active {
background-color: #ffffff;
color: #333;
background-color: #ffffff; /* 背景颜色为白色 */
color: #333; /* 文本颜色为深灰色 */
}
/* 自定义激活状态下的样式 */
&.menu-active {
background-color: #e7eefb;
color: #155bd4;
background-color: #e7eefb; /* 背景颜色为浅蓝色 */
color: #155bd4; /* 文本颜色为蓝色 */
}
/* 修改 el-menu-item-group 组件中标题的内边距 */
:deep(.el-menu-item-group__title) {
padding: 0;
padding: 0; /* 移除默认内边距 */
}
}
/* 定义子菜单标题的样式 */
div .el-sub-menu__title span {
display: inline-block;
width: 85px;
font-size: 14px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
display: inline-block; /* 设置为行内块元素 */
width: 85px; /* 固定宽度为 85px */
font-size: 14px; /* 字体大小为 14px */
overflow: hidden; /* 隐藏溢出的内容 */
text-overflow: ellipsis; /* 溢出部分用省略号表示 */
white-space: nowrap; /* 强制在同一行显示 */
}
.el-sub-menu .menu-right-el-item.el-menu-item span {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
/* 定义子菜单项中的文本样式 */
.el-sub-menu .menu-right-el-item.el-menu-item span {
overflow: hidden; /* 隐藏溢出的内容 */
text-overflow: ellipsis; /* 溢出部分用省略号表示 */
white-space: nowrap; /* 强制在同一行显示 */
}
}
</style>

@ -1,41 +1,53 @@
<template>
<!-- 使用 el-scrollbar 创建一个具有滚动功能的容器 -->
<el-scrollbar class="menu-right-el">
<!-- 使用 el-menu 组件创建一个垂直菜单 -->
<el-menu
class="el-menu-vertical-demo"
:default-openeds="openeds"
:default-openeds="openeds"
>
<SubMenuItem
v-for="(item, index) in expandMenu"
:key="index"
:expand-menu="item"
/>
<!-- 使用 v-for 指令遍历 expandMenu 数组动态生成子菜单项 -->
<SubMenuItem
v-for="(item, index) in expandMenu"
:key="index"
:expand-menu="item"
/>
</el-menu>
</el-scrollbar>
</template>
<script setup>
//
import SubMenuItem from './main-sidebar-sub-menu-item.vue'
// (props)
defineProps({
expandMenu: {
type: Array,
default: () => []
type: Array, //
default: () => [] //
}
})
// Vuex store
const commonStore = useCommonStore()
const openeds = computed(() => commonStore.menuIds)
// ID
const openeds = computed(() => commonStore.menuIds) // store ID
</script>
<style scoped>
/* 定义右侧菜单容器样式 */
.menu-right-el {
background-color: #fff;
width: 130px !important;
border-top: 1px solid #ebedf0;
height: calc(100vh - 50px);
overflow-y: auto;
background-color: #fff; /* 设置背景颜色为白色 */
width: 130px !important; /* 固定宽度为 130px并使用 !important 确保优先级 */
border-top: 1px solid #ebedf0; /* 添加顶部边框,颜色为浅灰色 */
height: calc(100vh - 50px); /* 高度为视口高度减去 50px通常用于顶部导航栏 */
overflow-y: auto; /* 当内容超出容器高度时启用垂直滚动条 */
}
/* 定义垂直菜单样式的自定义类 */
.el-menu-vertical-demo {
border: none;
height: 100%;
border: none; /* 移除默认的边框 */
height: 100%; /* 设置高度为 100%,确保菜单占满整个容器 */
}
</style>

@ -1,105 +1,124 @@
<template>
<aside class="site-sidebar">
<!-- 侧边栏容器 -->
<div class="menu-mod">
<!-- 左侧菜单滚动容器 -->
<el-scrollbar class="menu-left">
<!-- 首页菜单项 -->
<ul>
<li>
<div
:class="{
'menu-left-active': selectLeftId === '',
'menu-left-item1': 'language' === 'English',
'menu-left-item': 'language' !== 'English'
'menu-left-active': selectLeftId === '', //
'menu-left-item1': 'language' === 'English', // 使
'menu-left-item': 'language' !== 'English' // 使
}"
@click="toHome()"
>
<svg-icon
icon-class="icon-shouye"
style="font-size: 16px; margin-right: 3px !important"
/>
<span style="font-size: 14px">首页</span>
</div>
</li>
</ul>
<ul>
<template
v-for="menu in menuList"
:key="menu.menuId"
>
<li
v-if="!menu.hidden"
class="menu-left-active"
>
<div
v-if="menu.list"
:class="[
<svg-icon
icon-class="icon-shouye"
style="font-size: 16px; margin-right: 3px !important"
/>
<span style="font-size: 14px">首页</span>
</div>
</li>
</ul>
<!-- 动态生成的菜单列表 -->
<ul>
<template
v-for="menu in menuList"
:key="menu.menuId"
>
<li
v-if="!menu.hidden"
:class="{ 'menu-left-active': selectLeftId === menu.menuId }"
>
<!-- 如果菜单有子菜单则显示可展开的菜单项 -->
<div
v-if="menu.list"
:class="[
'menu-left-item',
{'menu-left-active': selectLeftId === menu.menuId}
{'menu-left-active': selectLeftId === menu.menuId} //
]"
@click="expandMenu(menu)"
>
<svg-icon
v-if="menu.icon"
:icon-class="`icon-${menu.icon}`"
style="font-size: 16px; margin-right: 3px !important"
/>
<span
class="item-text"
:title="menu.name.length > 4 ? menu.name : ''"
style="font-size: 14px"
>{{ menu.name }}</span>
</div>
<div
v-else
:class="{
'menu-left-active': selectLeftId === menu.menuId,
'menu-left-item1': '语言' === 'English',
'menu-left-item': '语言' !== 'English'
@click="expandMenu(menu)"
>
<svg-icon
v-if="menu.icon"
:icon-class="`icon-${menu.icon}`"
style="font-size: 16px; margin-right: 3px !important"
/>
<span
class="item-text"
:title="menu.name.length > 4 ? menu.name : ''"
style="font-size: 14px"
>{{ menu.name }}</span> //
</div>
<!-- 如果菜单没有子菜单则直接链接到目标页面 -->
<div
v-else
:class="{
'menu-left-active': selectLeftId === menu.menuId, //
'menu-left-item1': '语言' === 'English', // 使
'menu-left-item': '语言' !== 'English' // 使
}"
@click="gotoRouteHandle(menu)"
>
<svg-icon
v-if="menu.icon"
:icon-class="menu.icon || ''"
style="font-size: 16px; margin-right: 3px !important"
class="site-sidebar__menu-icon"
/>
<span
class="item-text"
:title="menu.name.length > 4 ? menu.name : ''"
style="font-size: 14px"
>{{ menu.name }}</span>
</div>
</li>
</template>
</ul>
</el-scrollbar>
<SubMenu
v-if="!sidebarFold"
:key="selectLeftId"
class="menu-right-con"
:expand-menu="expandMenuList"
@click="gotoRouteHandle(menu)"
>
<svg-icon
v-if="menu.icon"
:icon-class="menu.icon || ''"
style="font-size: 16px; margin-right: 3px !important"
class="site-sidebar__menu-icon"
/>
</div>
</aside>
<span
class="item-text"
:title="menu.name.length > 4 ? menu.name : ''"
style="font-size: 14px"
>{{ menu.name }}</span> //
</div>
</li>
</template>
</ul>
</el-scrollbar>
<!-- 右侧子菜单组件仅当侧边栏未折叠时显示 -->
<SubMenu
v-if="!sidebarFold"
:key="selectLeftId"
class="menu-right-con"
:expand-menu="expandMenuList"
/>
</div>
</aside>
</template>
<script setup>
import SubMenu from './main-sidebar-sub-menu.vue'
const route = useRoute()
const router = useRouter()
const commonStore = useCommonStore()
const dynamicMenuRoutes = ref([])
const expandMenuList = ref([])
import SubMenu from './main-sidebar-sub-menu.vue' //
// Vue Vuex
import { useRoute, useRouter } from 'vue-router'
import { computed, ref, onBeforeMount } from 'vue'
import { useCommonStore } from '@/store'
// Vuex store
const route = useRoute() //
const router = useRouter() //
const commonStore = useCommonStore() //
//
const dynamicMenuRoutes = ref([]) //
const expandMenuList = ref([]) //
//
const sidebarFold = computed(() => commonStore.sidebarFold)
//
const menuList = computed({
get: () => {
return commonStore.menuList
},
set: val => {
commonStore.updateMenuList(val)
}
get: () => commonStore.menuList, // store
set: val => commonStore.updateMenuList(val) // store
})
// ID
const selectLeftId = computed({
get: () => {
handleRightRoute(commonStore.selectLeftId)
@ -107,32 +126,41 @@ const selectLeftId = computed({
}
})
//
onBeforeMount(() => {
// sessionStorage
menuList.value = JSON.parse(sessionStorage.getItem('menuList') || '[]')
dynamicMenuRoutes.value = JSON.parse(sessionStorage.getItem('dynamicMenuRoutes') || '[]')
//
routeHandle(route)
// ID
if (selectLeftId.value) {
handleRightRoute(selectLeftId.value)
}
})
//
const handleRightRoute = selectLeftId => {
menuList.value.forEach(item => {
if (selectLeftId === item.menuId) {
expandMenu(item, true)
expandMenu(item, true) //
}
})
}
//
const toHome = () => {
router.push({ name: 'home' })
expandMenuList.value = []
sessionStorage.setItem('isExpand', '0')
commonStore.updateSidebarFold(true)
commonStore.updateSelectLeftId('')
commonStore.updateSelectRightId('')
router.push({ name: 'home' }) //
expandMenuList.value = [] //
sessionStorage.setItem('isExpand', '0') //
commonStore.updateSidebarFold(true) //
commonStore.updateSelectLeftId('') // ID
commonStore.updateSelectRightId('') // ID
}
//
const routeHandle = route => {
if (route.name === 'home') {
expandMenuList.value = []
@ -142,7 +170,9 @@ const routeHandle = route => {
}
}
//
const gotoRouteHandle = menu => {
//
if (router.history.current.name === menu.url) {
expandMenuList.value = []
commonStore.updateSidebarFold(true)
@ -151,10 +181,13 @@ const gotoRouteHandle = menu => {
commonStore.updateSelectLeftId(menu.menuId || '')
return
}
//
if (menu.name === '消息' || menu.name === 'Message') {
sessionStorage.setItem('isExpand', '0')
window.open(location.href.split('#')[0] + '#/imBox', '_blank', 'noopener,noreferrer')
} else {
//
expandMenuList.value = []
commonStore.updateSidebarFold(true)
sessionStorage.setItem('isExpand', '0')
@ -164,29 +197,36 @@ const gotoRouteHandle = menu => {
}
}
//
const expandMenu = menu => {
expandMenuList.value = menu.list || []
commonStore.updateSidebarFold(menu.list === null)
const id1 = commonStore.selectLeftId
commonStore.updateSelectLeftId(menu.menuId || '')
const id2 = commonStore.selectLeftId
expandMenuList.value = menu.list || [] //
commonStore.updateSidebarFold(menu.list === null) //
const id1 = commonStore.selectLeftId // ID
commonStore.updateSelectLeftId(menu.menuId || '') // ID
const id2 = commonStore.selectLeftId // ID
if (menu.list) {
sessionStorage.setItem('isExpand', '1')
sessionStorage.setItem('isExpand', '1') //
}
// ID
if (id1 !== id2) {
routeJump(menu)
}
}
//
const routeJump = menu => {
const routes = menu.list
for (let i = 0; i < routes.length; i++) {
//
if (!routes[i].hidden && !routes[i].list) {
router.push({ name: routes[i].url })
break
} else if (routes[i].list) {
let flag = false
for (let j = 0; j < routes[i].list.length; j++) {
//
if (!routes[i].list[j].hidden) {
router.push({ name: routes[i].list[j].url })
flag = true
@ -202,94 +242,117 @@ const routeJump = menu => {
</script>
<style scoped>
/* 定义菜单模块的整体布局 */
.menu-mod {
display: flex;
display: flex; /* 使用 Flexbox 布局,使子元素可以灵活排列 */
}
/* 定义右侧内容容器的位置和层级 */
.menu-right-con {
position: absolute;
z-index: 1;
left: 100px;
position: absolute; /* 设置为绝对定位 */
z-index: 1; /* 设置堆叠顺序,确保在其他元素之上 */
left: 100px; /* 左边距为 100px */
}
/* 定义左侧菜单栏样式 */
.menu-left {
background: #222222;
color: #ffffff !important;
width: 100px;
height: calc(100vh - 50px);
overflow-y: auto;
background: #222222; /* 背景颜色为深灰色 */
color: #ffffff !important; /* 文本颜色为白色,并使用 !important 确保优先级 */
width: 100px; /* 固定宽度为 100px */
height: calc(100vh - 50px); /* 高度为视口高度减去 50px通常用于顶部导航栏 */
overflow-y: auto; /* 当内容超出容器高度时启用垂直滚动条 */
}
/* 定义左侧菜单项的基本样式 */
.menu-mod .menu-left-item {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
width: 100%; /* 占满整个父容器宽度 */
height: 100%; /* 占满整个父容器高度 */
display: flex; /* 使用 Flexbox 布局 */
align-items: center; /* 水平居中对齐 */
justify-content: center; /* 垂直居中对齐 */
}
/* 定义左侧菜单项的另一种样式(可能是不同状态或类型的菜单项) */
.menu-mod .menu-left-item1 {
width: 100%;
height: 100%;
display: flex;
align-items: center;
text-align: left;
padding-left: 12px;
width: 100%; /* 占满整个父容器宽度 */
height: 100%; /* 占满整个父容器高度 */
display: flex; /* 使用 Flexbox 布局 */
align-items: center; /* 水平居中对齐 */
text-align: left; /* 文本左对齐 */
padding-left: 12px; /* 左内边距为 12px */
}
/* 定义左侧菜单的无序列表样式 */
.menu-mod .menu-left ul {
list-style-type: none;
margin: 0;
padding: 0;
width: 100px;
text-align: center;
list-style-type: none; /* 移除默认的列表样式 */
margin: 0; /* 移除外边距 */
padding: 0; /* 移除内边距 */
width: 100px; /* 固定宽度为 100px */
text-align: center; /* 文本居中对齐 */
}
/* 定义右侧菜单的无序列表样式 */
.menu-mod .menu-right ul {
list-style-type: none;
margin: 0;
padding: 0;
width: 130px;
text-align: center;
list-style-type: none; /* 移除默认的列表样式 */
margin: 0; /* 移除外边距 */
padding: 0; /* 移除内边距 */
width: 130px; /* 固定宽度为 130px */
text-align: center; /* 文本居中对齐 */
}
/* 定义左侧菜单项的样式 */
.menu-left li {
background: #222222;
color: #fff;
height: 40px;
cursor: pointer;
font-size: 14px;
stroke: #fff !important;
background: #222222; /* 背景颜色为深灰色 */
color: #fff; /* 文本颜色为白色 */
height: 40px; /* 固定高度为 40px */
cursor: pointer; /* 鼠标悬停时显示指针样式 */
font-size: 14px; /* 字体大小为 14px */
stroke: #fff !important; /* SVG 图标颜色为白色,并使用 !important 确保优先级 */
}
/* 定义右侧菜单项的样式 */
.menu-right li {
height: 40px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
font-size: 14px;
color: #333333;
height: 40px; /* 固定高度为 40px */
display: flex; /* 使用 Flexbox 布局 */
align-items: center; /* 水平居中对齐 */
justify-content: center; /* 垂直居中对齐 */
cursor: pointer; /* 鼠标悬停时显示指针样式 */
font-size: 14px; /* 字体大小为 14px */
color: #333333; /* 文本颜色为深灰色 */
}
/* 鼠标移动到选项上修改背景颜色 */
/* 定义左侧菜单项悬停时的样式 */
.menu-left li:hover {
background-color: #ffffff;
color: #155bd4;
stroke: #155bd4 !important;
background-color: #ffffff; /* 背景颜色变为白色 */
color: #155bd4; /* 文本颜色变为蓝色 */
stroke: #155bd4 !important;/* SVG 图标颜色变为蓝色,并使用 !important 确保优先级 */
}
/* 鼠标移动到选项上修改背景颜色 */
/* 定义右侧菜单项悬停时的样式 */
.menu-right li:hover {
background-color: rgba(21, 91, 212, 0.1);
color: #155bd4;
background-color: rgba(21, 91, 212, 0.1); /* 背景颜色变为浅蓝色透明 */
color: #155bd4; /* 文本颜色变为蓝色 */
}
/* 冗余定义,应删除 */
.menu-right li:hover {
background-color: rgba(21, 91, 212, 0.1);
color: #155bd4;
}
/* 定义左侧菜单项激活状态的样式 */
.menu-left-active {
background-color: #ffffff;
color: #155bd4;
stroke: #155bd4 !important;
background-color: #ffffff; /* 背景颜色变为白色 */
color: #155bd4; /* 文本颜色变为蓝色 */
stroke: #155bd4 !important;/* SVG 图标颜色变为蓝色,并使用 !important 确保优先级 */
}
/* 定义菜单项文本的样式 */
.item-text {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
display: inline-block;
max-width: 70px;
overflow: hidden; /* 隐藏溢出的内容 */
text-overflow: ellipsis; /* 溢出部分用省略号表示 */
white-space: nowrap; /* 强制在同一行显示 */
display: inline-block; /* 显示为内联块元素 */
max-width: 70px; /* 最大宽度为 70px */
}
</style>

@ -1,59 +1,84 @@
<template>
<!-- 主应用布局容器 -->
<div
v-loading.fullscreen.lock="loading"
class="site-wrapper"
:class="{ 'site-sidebar--fold': sidebarFold }"
:element-loading-text="'拼命加载中'"
class="site-wrapper"
:class="{ 'site-sidebar--fold': sidebarFold }"
:element-loading-text="'拼命加载中'"
>
<template v-if="!loading">
<MainNavbar />
<MainSidebar />
<div
class="site-content__wrapper"
:style="{ 'min-height': documentClientHeight + 'px' }"
>
<main-content />
</div>
</template>
<!-- 条件渲染只有当 loading false 时才渲染内部组件 -->
<template v-if="!loading">
<!-- 导航栏组件 -->
<MainNavbar /> <!-- 渲染主导航栏组件通常包含导航链接用户信息等 -->
<!-- 侧边栏组件 -->
<MainSidebar /> <!-- 渲染主侧边栏组件通常包含菜单选项或快捷方式 -->
<!-- 内容区容器 -->
<div
class="site-content__wrapper"
:style="{ 'min-height': documentClientHeight + 'px' }"
>
<!-- 主要内容组件 -->
<main-content /> <!-- 渲染主要内容组件这里是页面的主要内容展示区域 -->
</div>
</template>
</div>
</template>
<script setup>
import MainNavbar from './main-navbar.vue'
import MainSidebar from './main-sidebar.vue'
import MainContent from './main-content.vue'
import { ref, computed, onBeforeMount, onMounted } from 'vue'
import http from '@/utils/http' // HTTP
import MainNavbar from './main-navbar.vue' //
import MainSidebar from './main-sidebar.vue' //
import MainContent from './main-content.vue' //
import { useCommonStore, useUserStore } from '@/store' // Vuex store
//
onBeforeMount(() => {
getUserInfo()
getUserInfo() // getUserInfo
})
const commonStore = useCommonStore()
const documentClientHeight = computed(() => commonStore.documentClientHeight)
const userStore = useUserStore()
const sidebarFold = computed(() => commonStore.sidebarFold)
const commonStore = useCommonStore() // Store
const documentClientHeight = computed(() => //
commonStore.documentClientHeight // commonStore
)
const userStore = useUserStore() // Store
const sidebarFold = computed(() => //
commonStore.sidebarFold // commonStore
)
//
onMounted(() => {
resetDocumentClientHeight()
resetDocumentClientHeight() // resetDocumentClientHeight
})
//
const resetDocumentClientHeight = () => {
commonStore.documentClientHeight = document.documentElement.clientHeight
window.onresize = () => {
commonStore.documentClientHeight = document.documentElement.clientHeight
commonStore.documentClientHeight = document.documentElement.clientHeight //
window.onresize = () => { //
commonStore.documentClientHeight = document.documentElement.clientHeight //
}
}
const loading = ref(true)
//
//
const loading = ref(true) // loading true
//
const getUserInfo = () => {
http({
url: http.adornUrl('/sys/user/info'),
method: 'get',
params: http.adornParams()
url: http.adornUrl('/sys/user/info'), // API 使 http.adornUrl URL
method: 'get', // GET
params: http.adornParams() // 使 http.adornParams
}).then(({ data }) => {
loading.value = false
userStore.userId = data.userId
userStore.name = data.username
userStore.mobile = data.mobile
userStore.shopId = data.shopId
userStore.userId = data.userId
}).catch(() => {})
loading.value = false //
userStore.userId = data.userId // ID userStore
userStore.name = data.username // userStore
userStore.mobile = data.mobile // userStore
userStore.shopId = data.shopId // ID userStore
// userStore.userId = data.userId
}).catch(() => {
//
})
}
</script>

@ -5,67 +5,70 @@ import { clearLoginInfo } from '@/utils'
import Layout from '@/layout/main.vue'
import Login from '@/views/common/login/index.vue'
// 全局路由(无需嵌套上左右整体布局)
// 定义全局路由(无需嵌套在左右整体布局中)
const globalRoutes = [
{
path: '/404',
component: () => import('@/views/common/error-page/404.vue'),
name: '404',
meta: { title: '404未找到' }
path: '/404', // 404 页面路径
component: () => import('@/views/common/error-page/404.vue'), // 动态导入 404 页面组件
name: '404', // 路由名称
meta: { title: '404未找到' } // 元信息,用于设置页面标题等
},
{
path: '/login',
component: Login,
name: 'login',
meta: { title: '登录' }
path: '/login', // 登录页面路径
component: Login, // 登录页面组件
name: 'login', // 路由名称
meta: { title: '登录' } // 元信息,用于设置页面标题等
}
]
// 定义主路由(需要嵌套在 Layout 组件中)
export const mainRoutes = {
path: '/',
component: Layout,
name: 'home',
redirect: '/home',
children: [
path: '/', // 根路径
component: Layout, // 布局组件
name: 'home', // 路由名称
redirect: '/home', // 默认重定向到 home 页面
children: [ // 子路由
{
path: 'home',
name: 'home',
component: () => import('@/views/common/home/index.vue')
path: 'home', // 主页路径
name: 'home', // 路由名称
component: () => import('@/views/common/home/index.vue') // 动态导入主页组件
},
{
path: '/prodInfo',
name: 'prodInfo',
component: () => import('@/views/modules/prod/prodInfo/index.vue')
path: '/prodInfo', // 产品信息页面路径
name: 'prodInfo', // 路由名称
component: () => import('@/views/modules/prod/prodInfo/index.vue') // 动态导入产品信息页面组件
}
],
// eslint-disable-next-line no-unused-vars
beforeEnter (to, from, next) {
const authorization = cookie.get('Authorization')
if (!authorization || !/\S/.test(authorization)) {
clearLoginInfo()
next({ name: 'login' })
beforeEnter (to, from, next) { // 路由守卫,检查用户是否已登录
const authorization = cookie.get('Authorization') // 获取存储在 cookie 中的授权信息
if (!authorization || !/\S/.test(authorization)) { // 如果没有授权信息或为空
clearLoginInfo() // 清除登录信息
next({ name: 'login' }) // 重定向到登录页面
}
next()
next() // 否则继续导航
}
}
// 创建 Vue Router 实例
const router = createRouter({
history: createWebHistory(),
scrollBehavior: () => ({ top: 0 }),
history: createWebHistory(), // 使用 HTML5 History 模式
scrollBehavior: () => ({ top: 0 }), // 设置滚动行为,每次导航回到顶部
isAddDynamicMenuRoutes: false, // 是否已经添加动态(菜单)路由
routes: globalRoutes.concat(mainRoutes)
routes: globalRoutes.concat(mainRoutes) // 合并全局路由和主路由
})
// eslint-disable-next-line no-unused-vars
// 全局前置守卫,拦截所有路由导航
router.beforeEach((to, from, next) => {
const commonStore = useCommonStore()
// 添加动态(菜单)路由
// 1. 已经添加 or 全局路由, 直接访问
// 2. 获取菜单列表, 添加并保存本地存储
const commonStore = useCommonStore() // 初始化 Vuex store 实例
// 添加动态(菜单)路由逻辑
if (router.options.isAddDynamicMenuRoutes || fnCurrentRouteType(to, globalRoutes) === 'global') {
const routeList = commonStore.routeList
let navTitles = []
let leftMenuId = ''
// 已经添加动态路由或访问的是全局路由,直接处理导航
const routeList = commonStore.routeList // 获取路由列表
let navTitles = [] // 用于存储导航标题
let leftMenuId = '' // 用于存储左侧菜单 ID
// 查找当前路由对应的菜单项,并构建导航标题链
routeList.forEach(item => {
if (to.meta.menuId === item.menuId) {
navTitles.push(item.name)
@ -83,30 +86,37 @@ router.beforeEach((to, from, next) => {
})
}
})
navTitles = navTitles.reverse()
navTitles = navTitles.reverse() // 反转导航标题链
// 更新 Vuex store 中的状态
if (to.meta.isLeftMenu || to.path === '/home' || leftMenuId) {
if (leftMenuId) {
commonStore.updateSelectLeftId(leftMenuId)
commonStore.updateSelectRightId(to.meta.menuId)
commonStore.updateSelectLeftId(leftMenuId) // 更新选中的左侧菜单 ID
commonStore.updateSelectRightId(to.meta.menuId) // 更新选中的右侧菜单 ID
} else {
commonStore.updateSidebarFold(true)
commonStore.updateSelectLeftId(to.path === '/home' ? '' : to.meta.menuId)
commonStore.updateSidebarFold(true) // 折叠侧边栏
commonStore.updateSelectLeftId(to.path === '/home' ? '' : to.meta.menuId) // 更新选中的左侧菜单 ID
}
}
commonStore.updateSelectMenu(navTitles)
next()
commonStore.updateSelectMenu(navTitles) // 更新选中的菜单标题链
next() // 继续导航
} else {
// 请求菜单列表和权限信息
http({
url: http.adornUrl('/sys/menu/nav'),
method: 'get',
params: http.adornParams()
url: http.adornUrl('/sys/menu/nav'), // 请求 URL
method: 'get', // 请求方法
params: http.adornParams() // 请求参数
}).then(({ data }) => {
sessionStorage.setItem('Authorities', JSON.stringify(data.authorities || '[]'))
fnAddDynamicMenuRoutes(data.menuList)
router.options.isAddDynamicMenuRoutes = true
sessionStorage.setItem('Authorities', JSON.stringify(data.authorities || '[]')) // 存储权限信息到 sessionStorage
fnAddDynamicMenuRoutes(data.menuList) // 添加动态路由
router.options.isAddDynamicMenuRoutes = true // 标记动态路由已添加
// 构建完整的路由列表,并保存到 Vuex store 和 sessionStorage
const rList = []
data.menuList.forEach(item => {
item.isLeftMenu = item.parentId === 0
item.isLeftMenu = item.parentId === 0 // 判断是否为左侧菜单
rList.push({
menuId: item.menuId,
name: item.name,
@ -136,81 +146,92 @@ router.beforeEach((to, from, next) => {
})
}
})
fnAddDynamicMenuRoutes(data.menuList)
sessionStorage.setItem('menuList', JSON.stringify(data.menuList || '[]'))
commonStore.updateRouteList(rList)
commonStore.updateMenuIds(rList)
fnAddDynamicMenuRoutes(data.menuList) // 再次调用以确保所有子菜单被正确添加
sessionStorage.setItem('menuList', JSON.stringify(data.menuList || '[]')) // 存储菜单列表到 sessionStorage
commonStore.updateRouteList(rList) // 更新 Vuex store 中的路由列表
commonStore.updateMenuIds(rList) // 更新 Vuex store 中的菜单 ID 列表
// 重新导航到目标页面
next({ ...to, replace: true })
}).catch(e => {
// eslint-disable-next-line no-console
console.log(`%c${e} 请求菜单列表和权限失败,跳转至登录页!!`, 'color:blue')
router.push({ name: 'login' })
console.log(`%c${e} 请求菜单列表和权限失败,跳转至登录页!!`, 'color:blue') // 打印错误信息
router.push({ name: 'login' }) // 跳转到登录页面
})
}
})
/**
* 判断当前路由类型, global: 全局路由, main: 主入口路由
* @param {*} route 当前路由
* @param globalRoutes 全局路由
* 判断当前路由类型返回 'global' 'main'
* @param {*} route 当前路由对象
* @param globalRoutes 全局路由数组
*/
function fnCurrentRouteType (route, globalRoutes = []) {
let temp = []
let temp = [] // 临时存储子路由
for (let i = 0; i < globalRoutes.length; i++) {
if (route.path === globalRoutes[i].path) {
return 'global'
return 'global' // 如果匹配到全局路由,返回 'global'
} else if (globalRoutes[i].children && globalRoutes[i].children.length >= 1) {
temp = temp.concat(globalRoutes[i].children)
temp = temp.concat(globalRoutes[i].children) // 将子路由添加到临时数组
}
}
return temp.length >= 1 ? fnCurrentRouteType(route, temp) : 'main'
return temp.length >= 1 ? fnCurrentRouteType(route, temp) : 'main' // 递归查找子路由,直到找到匹配项或返回 'main'
}
/**
* 添加动态(菜单)路由
* @param {*} menuList 菜单列表
* @param {*} routes 递归创建的动态(菜单)路由
* @param {*} routes 递归创建的动态(菜单)路由数组
*/
function fnAddDynamicMenuRoutes (menuList = [], routes = []) {
let temp = []
const modules = import.meta.glob('../views/modules/**/index.vue')
let temp = [] // 临时存储子菜单项
const modules = import.meta.glob('../views/modules/**/index.vue') // 动态导入视图模块
// 遍历菜单列表,创建对应的路由对象
for (let i = 0; i < menuList.length; i++) {
if (menuList[i].list && menuList[i].list.length >= 1) {
temp = temp.concat(menuList[i].list)
temp = temp.concat(menuList[i].list) // 如果有子菜单,添加到临时数组
} else if (menuList[i].url && /\S/.test(menuList[i].url)) {
menuList[i].url = menuList[i].url.replace(/^\//, '')
menuList[i].url = menuList[i].url.replace(/^\//, '') // 移除 URL 开头的斜杠
const route = {
path: menuList[i].url,
component: null,
name: menuList[i].url,
path: menuList[i].url, // 路径
component: null, // 组件
name: menuList[i].url, // 名称
meta: {
menuId: menuList[i].menuId,
title: menuList[i].name,
isDynamic: true,
isTab: true,
iframeUrl: ''
menuId: menuList[i].menuId, // 菜单项 ID
title: menuList[i].name, // 标题
isDynamic: true, // 标记为动态路由
isTab: true, // 标记为标签页
iframeUrl: '' // iframe URL默认为空
}
}
// url以http[s]://开头, 通过iframe展示
// 如果 URL 是有效的 HTTP[S] 地址,则通过 iframe 展示
if (isURL(menuList[i].url)) {
route.path = `i-${menuList[i].menuId}`
route.name = `i-${menuList[i].menuId}`
route.meta.iframeUrl = menuList[i].url
route.path = `i-${menuList[i].menuId}` // 修改路径为 iframe 特定格式
route.name = `i-${menuList[i].menuId}` // 修改名称为 iframe 特定格式
route.meta.iframeUrl = menuList[i].url // 设置 iframe URL
} else {
try {
route.component = modules[`../views/modules/${menuList[i].url}/index.vue`] || null
route.component = modules[`../views/modules/${menuList[i].url}/index.vue`] || null // 动态加载组件
} catch (e) {}
}
routes.push(route)
routes.push(route) // 将路由对象添加到 routes 数组
}
}
// 如果还有子菜单项,递归处理
if (temp.length >= 1) {
fnAddDynamicMenuRoutes(temp, routes)
} else {
mainRoutes.name = 'main-dynamic'
mainRoutes.children = routes
router.addRoute(mainRoutes)
mainRoutes.name = 'main-dynamic' // 修改主路由名称为 'main-dynamic'
mainRoutes.children = routes // 将动态路由添加为主路由的子路由
router.addRoute(mainRoutes) // 添加主路由到 Vue Router 实例
}
// 添加一个通配符路由,处理未匹配的路径,重定向到 404 页面
router.addRoute({ path: '/:pathMatch(.*)*', redirect: { name: '404' } })
}
export default router
export default router // 导出配置好的 Vue Router 实例

@ -1,80 +1,116 @@
import { defineStore } from 'pinia'
import router from '@/router'
// 定义名为 'common' 的 Pinia Store
export const useCommonStore = defineStore('common', {
state: () => {
return {
// 页面文档可视高度(随窗口改变大小)
documentClientHeight: 0,
// 侧边栏, 布局皮肤, light(浅色) / dark(黑色)
sidebarLayoutSkin: 'dark',
// 侧边栏, 折叠状态
sidebarFold: true,
// 侧边栏, 菜单
menuList: [],
menuActiveName: '',
// 主入口标签页
mainTabs: [],
mainTabsActiveName: '',
// 当前选择的标签
selectMenu: [],
// 路由列表
routeList: [],
menuIds: [],
selectLeftId: '',
selectRightId: ''
}
},
// 状态 (state) - 存储应用的数据
state: () => ({
// 页面文档可视高度(随窗口改变大小)
documentClientHeight: 0,
// 侧边栏布局皮肤, light(浅色) / dark(黑色)
sidebarLayoutSkin: 'dark',
// 侧边栏折叠状态
sidebarFold: true,
// 侧边栏菜单列表
menuList: [],
// 当前激活的菜单名称
menuActiveName: '',
// 主入口标签页列表
mainTabs: [],
// 当前激活的主入口标签页名称
mainTabsActiveName: '',
// 当前选择的标签链(用于面包屑导航)
selectMenu: [],
// 路由列表
routeList: [],
// 菜单项 ID 列表
menuIds: [],
// 当前选中的左侧菜单 ID
selectLeftId: '',
// 当前选中的右侧菜单 ID
selectRightId: ''
}),
// 动作 (actions) - 包含更改状态的逻辑
actions: {
updateDocumentClientHeight (height) {
this.documentClientHeight = height
// 更新页面文档可视高度
updateDocumentClientHeight(height) {
this.documentClientHeight = height;
},
updateSidebarFold (fold) {
this.sidebarFold = fold
// 更新侧边栏折叠状态
updateSidebarFold(fold) {
this.sidebarFold = fold;
},
updateMenuList (list) {
this.menuList = list
// 更新侧边栏菜单列表
updateMenuList(list) {
this.menuList = list;
},
updateMenuActiveName (name) {
this.menuActiveName = name
// 更新当前激活的菜单名称
updateMenuActiveName(name) {
this.menuActiveName = name;
},
updateMainTabs (tabs) {
this.mainTabs = tabs
// 更新主入口标签页列表
updateMainTabs(tabs) {
this.mainTabs = tabs;
},
updateMainTabsActiveName (name) {
this.mainTabsActiveName = name
// 更新当前激活的主入口标签页名称
updateMainTabsActiveName(name) {
this.mainTabsActiveName = name;
},
updateRouteList (list) {
this.routeList = list
// 更新路由列表
updateRouteList(list) {
this.routeList = list;
},
updateSelectMenu (list) {
this.selectMenu = list
// 更新当前选择的标签链(用于面包屑导航)
updateSelectMenu(list) {
this.selectMenu = list;
},
updateSelectLeftId (id) {
this.selectLeftId = id
// 更新当前选中的左侧菜单 ID
updateSelectLeftId(id) {
this.selectLeftId = id;
},
updateSelectRightId (id) {
this.selectRightId = id
// 更新当前选中的右侧菜单 ID
updateSelectRightId(id) {
this.selectRightId = id;
},
replaceSelectMenu (title) {
this.selectMenu.splice(this.selectMenu.length - 1, 1, title)
// 替换当前选择的标签链的最后一项
replaceSelectMenu(title) {
this.selectMenu.splice(this.selectMenu.length - 1, 1, title);
},
updateMenuIds (list) {
this.menuIds = []
// 更新菜单项 ID 列表,确保所有 ID 是字符串形式
updateMenuIds(list) {
this.menuIds = [];
list.forEach(menu => {
this.menuIds.push(String(menu.menuId + ''))
})
this.menuIds.push(String(menu.menuId + ''));
});
},
removeMainActiveTab () {
this.mainTabs = this.mainTabs.filter(item => item.name !== this.mainTabsActiveName)
// 移除当前激活的主入口标签页,并处理相关逻辑
removeMainActiveTab() {
// 过滤掉当前激活的标签页
this.mainTabs = this.mainTabs.filter(item => item.name !== this.mainTabsActiveName);
if (this.mainTabs.length >= 1) {
// 当前选中tab被删除
// 如果还有其他标签页,则切换到最后一个标签页
router.push({ name: this.mainTabs[this.mainTabs.length - 1].name }, () => {
this.mainTabsActiveName = this.mainTabs[this.mainTabs.length - 1].name
})
this.mainTabsActiveName = this.mainTabs[this.mainTabs.length - 1].name;
});
} else {
this.menuActiveName = ''
router.push({ name: 'home' })
// 如果没有其他标签页,则重置菜单激活名称并跳转到主页
this.menuActiveName = '';
router.push({ name: 'home' });
}
}
}
})
});

@ -1,37 +1,53 @@
import { defineStore } from 'pinia'
// 定义名为 'prod' 的 Pinia Store用于管理商品相关数据和操作
export const scoreProdStore = defineStore('prod', {
state: () => {
return {
id: 0,
skuTags: [],
defalutSku: {
price: 0, // 销售价
oriPrice: 0, // 市场价
stocks: 0, // 库存
properties: '', // 销售属性组合字符串
skuName: '', // sku名称
prodName: '', // 商品名称
weight: 0, // 商品重量
volume: 0, // 商品体积
status: 1 // 0 禁用 1 启用
}
// 状态 (state) - 存储商品相关的数据
state: () => ({
id: 0, // 商品 ID
skuTags: [], // SKU 标签列表,每个标签代表一个销售属性(如颜色、尺寸等)
defalutSku: {
price: 0, // 销售价
oriPrice: 0, // 市场价(原价)
stocks: 0, // 库存数量
properties: '', // 销售属性组合字符串,例如 "红色;M号"
skuName: '', // SKU 名称,具体到某一销售属性组合的商品名称
prodName: '', // 商品名称,通用名称
weight: 0, // 商品重量
volume: 0, // 商品体积
status: 1 // SKU 状态0 表示禁用1 表示启用
}
},
}),
// 动作 (actions) - 包含更改状态的逻辑
actions: {
updateSkuTags (skuTags) {
this.skuTags = skuTags
// 更新 SKU 标签列表
updateSkuTags(skuTags) {
this.skuTags = skuTags;
},
addSkuTag (skuTag) {
this.skuTags.push(skuTag)
// 添加新的 SKU 标签
addSkuTag(skuTag) {
this.skuTags.push(skuTag);
},
removeSkuTag (tagIndex) {
this.skuTags.splice(tagIndex, 1)
// 移除指定索引的 SKU 标签
removeSkuTag(tagIndex) {
this.skuTags.splice(tagIndex, 1);
},
removeSkuTagItem (tagIndex, tagItemIndex) {
this.skuTags[tagIndex].tagItems.splice(tagItemIndex, 1)
// 移除指定 SKU 标签中的某个项
removeSkuTagItem(tagIndex, tagItemIndex) {
if (this.skuTags[tagIndex] && this.skuTags[tagIndex].tagItems) {
this.skuTags[tagIndex].tagItems.splice(tagItemIndex, 1);
}
},
addSkuTagItem ({ tagIndex, tagItem }) {
this.skuTags[tagIndex].tagItems.push(tagItem)
// 向指定 SKU 标签中添加新项
addSkuTagItem({ tagIndex, tagItem }) {
if (this.skuTags[tagIndex] && this.skuTags[tagIndex].tagItems) {
this.skuTags[tagIndex].tagItems.push(tagItem);
}
}
}
})
});

@ -1,29 +1,41 @@
import { defineStore } from 'pinia'
// 定义名为 'user' 的 Pinia Store用于管理用户相关数据和操作
export const useUserStore = defineStore('user', {
state: () => {
return {
id: 0,
name: '',
userId: '',
shopId: '',
mobile: ''
}
},
// 状态 (state) - 存储用户相关的数据
state: () => ({
id: 0, // 用户 ID通常是一个唯一的整数值
name: '', // 用户名称
userId: '', // 用户标识符,可能是用户名或系统内部使用的唯一标识
shopId: '', // 商铺 ID如果用户关联到特定商铺则存储该商铺的 ID
mobile: '' // 用户手机号码
}),
// 动作 (actions) - 包含更改状态的逻辑
actions: {
updateId (id) {
this.id = id
// 更新用户 ID
updateId(id) {
this.id = id;
},
updateName (name) {
this.name = name
// 更新用户名称
updateName(name) {
this.name = name;
},
updateMobile (mobile) {
this.mobile = mobile
// 更新用户手机号码
updateMobile(mobile) {
this.mobile = mobile;
},
updateShopId (shopId) {
this.shopId = shopId
// 更新商铺 ID
updateShopId(shopId) {
this.shopId = shopId;
},
updateUserId (userId) {
this.userId = userId
// 更新用户标识符
updateUserId(userId) {
this.userId = userId;
}
}
})
});

@ -1,8 +1,12 @@
/*
*
*/
*,
*:before,
*:after {
box-sizing: border-box;
box-sizing: border-box; /* 确保所有元素的宽度和高度计算包括 padding 和 border */
}
body {
font-family: "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "微软雅黑", Arial, sans-serif;
font-size: 14px;
@ -10,23 +14,24 @@ body {
color: #303133;
background-color: #fff;
}
a {
color: mix(#fff, $--color-primary, 20%);
color: mix(#fff, $--color-primary, 20%); /* 链接颜色为白色与主色混合后的20% */
text-decoration: none;
&:focus,
&:hover {
color: $--color-primary;
color: $--color-primary; /* 激活或悬停时链接颜色为主色 */
text-decoration: underline;
}
}
img {
vertical-align: middle;
max-width: 100%;
vertical-align: middle; /* 图片垂直居中对齐 */
max-width: 100%; /* 确保图片不会超出其容器 */
}
/* Utils
------------------------------ */
/* 清除浮动 */
.clearfix:before,
.clearfix:after {
content: " ";
@ -36,31 +41,21 @@ img {
clear: both;
}
/* Reset element-ui
------------------------------ */
.site-wrapper {
.el-pagination {
display: flex;
justify-content: flex-end;
}
/* 重置 element-ui 样式 */
.site-wrapper .el-pagination {
display: flex;
justify-content: flex-end; /* 分页组件右对齐 */
}
/* Layout
------------------------------ */
/* 布局 */
.site-wrapper {
position: relative;
min-width: 1180px;
min-width: 1180px; /* 设置最小宽度以确保布局在大屏幕上的稳定性 */
}
/* Sidebar fold
------------------------------ */
/* 侧边栏折叠 */
.site-content--tabs {
padding: 60px 20px 20px 20px !important;
padding: 60px 20px 20px 20px !important; /* 当标签页存在时调整内容区的内边距 */
}
.site-navbar {
color: #333333;
@ -69,37 +64,32 @@ img {
.site-navbar__header,
.site-navbar__brand,
.site-sidebar {
width: 100px;
width: 100px; /* 折叠状态下左侧栏的宽度 */
}
.site-navbar__body,
.site-content__wrapper {
margin-left: 100px;
margin-left: 100px; /* 折叠后内容区向左偏移量 */
border-bottom: 1px solid #EBEDF0;
}
.site-navbar__brand {
&-lg {
display: none;
}
&-mini {
display: inline-block;
}
.site-navbar__brand-lg {
display: none; /* 折叠状态隐藏大尺寸品牌标志 */
}
.site-navbar__brand-mini {
display: inline-block; /* 显示小尺寸品牌标志 */
}
.site-sidebar,
.site-sidebar__inner {
overflow: initial;
overflow: initial; /* 解决滚动条显示问题 */
}
}
// animation
.site-sidebar,
.site-sidebar__menu-icon,
.site-content__wrapper,
.site-content--tabs > .el-tabs .el-tabs__header {
transition: inline-block .3s, left .3s, width .3s, margin-left .3s, font-size .3s;
transition: all .3s ease-in-out; /* 添加过渡效果 */
}
/* Navbar
------------------------------ */
/* 导航栏 */
.site-navbar {
position: fixed;
top: 0;
@ -107,13 +97,10 @@ img {
left: 0;
z-index: 1030;
height: 50px;
// box-shadow: 0 2px 4px rgba(0, 0, 0, .08);
background-color: $navbar--background-color;
&__header {
position: relative;
float: left;
// width: 180px;
height: 50px;
margin-left: 20px;
overflow: hidden;
@ -121,9 +108,7 @@ img {
&__brand {
display: table-cell;
vertical-align: middle;
// width: 230px;
height: 50px;
margin: 0;
line-height: 50px;
font-size: 20px;
text-align: center;
@ -151,28 +136,19 @@ img {
}
&__avatar {
border-bottom: none !important;
* {
vertical-align: inherit;
}
.el-dropdown-link {
> img {
width: 36px;
height: auto;
margin-right: 5px;
border-radius: 100%;
vertical-align: middle;
}
.el-dropdown-link img {
width: 36px;
height: auto;
margin-right: 5px;
border-radius: 100%;
vertical-align: middle;
}
}
&__body {
position: relative;
// margin-left: 230px;
// padding-right: 15px;
background-color: #fff;
}
&__menu {
float: left;
background-color: transparent;
border-bottom: 0;
&--right {
@ -185,9 +161,7 @@ img {
}
}
/* Sidebar
------------------------------ */
/* 侧边栏 */
.site-sidebar {
position: fixed;
top: 50px;
@ -203,14 +177,12 @@ img {
}
&__inner {
position: relative;
z-index: 1;
width: 250px;
height: 100%;
padding-bottom: 15px;
overflow-y: scroll;
}
&__menu.el-menu {
width: 100px;
border-right: 0;
}
&__menu-icon {
@ -222,9 +194,7 @@ img {
}
}
/* Content
------------------------------ */
/* 内容区 */
.site-content {
position: relative;
padding: 15px;
@ -242,38 +212,31 @@ img {
}
}
//
// el-table
/* 表格样式 */
.table-header {
background-color: #f7f8fa !important;
color: #000;
height: 60px;
}
// el-table - /
.table-row {
height: 100px;
}
// el-table -
.table-row-low {
height: 65px;
}
// el-table
.table-cell {
padding: 0;
}
//
.el-table .cell {
line-height: 20px !important;
word-break: break-word !important;
}
// &
.el-table tr > td:first-child > .cell,
.el-table th:first-child > .cell {
padding-left: 20px;
}
//
/* 按钮样式 */
.default-btn {
height: 32px;
line-height: 32px;
@ -334,7 +297,6 @@ img {
cursor: not-allowed;
}
}
//
.default-btn + .default-btn {
margin-left: 10px;
}
@ -342,31 +304,26 @@ img {
margin-left: 20px;
}
//
/* 状态展示 */
.tag-text {
font-size: 14px;
}
// el-dialog
/* 对话框样式 */
.el-dialog__header {
border-bottom: 1px solid #f0f0f0;
margin-right: 0;
}
.el-dialog__footer {
border-top: 1px solid #f0f0f0;
}
//
//
/* 列表布局样式 */
.search-bar {
padding: 25px 20px 0;
margin-bottom: 20px;
background-color: #F7F8FA;
.input-row {
display: block;
// () &
.el-form-item .el-form-item__content .el-select,
.el-form-item .el-form-item__content .el-input {
width: 200px;
@ -382,9 +339,7 @@ img {
}
}
}
//
.main-container {
//
.operation-bar {
position: relative;
display: flex;
@ -403,7 +358,6 @@ img {
font-size: 12px;
}
}
//
.table-con {
margin-top: 20px;
padding-bottom: 30px;
@ -412,7 +366,6 @@ img {
display: flex;
justify-content: center;
}
// +
.table-cell-con {
display: flex;
align-items: center;
@ -430,17 +383,14 @@ img {
margin-left: 8px;
flex: 1;
text-overflow: ellipsis;
-o-text-overflow: ellipsis;
word-break: break-word;
display: -webkit-box;
-webkit-line-clamp: 2;
/* autoprefixer: ignore next */
-webkit-box-orient: vertical;
overflow: hidden;
line-height: 20px;
}
}
//
.table-cell-image {
width: 60px;
height: 60px;
@ -451,14 +401,11 @@ img {
object-fit: contain;
}
}
//
.table-cell-text {
text-overflow: ellipsis;
-o-text-overflow: ellipsis;
word-break: break-word;
display: -webkit-box;
-webkit-line-clamp: 2;
/* autoprefixer: ignore next */
-webkit-box-orient: vertical;
overflow: hidden;
line-height: 20px;
@ -469,7 +416,7 @@ img {
}
}
// el-tabs
/* 标签样式 */
.el-tabs__nav-wrap::after{
height: 1px !important;
}
@ -487,12 +434,11 @@ img {
border-bottom: 2px solid #155BD4;
}
//
/* 新增页面头部标题样式 */
.new-page-title {
width: 100%;
height: 62px;
background: #F7F8FA;
box-sizing: border-box;
padding: 19px 20px;
display: flex;
align-items: center;
@ -514,4 +460,4 @@ img {
}
.time-select-item {
text-align: center;
}
}

@ -4,12 +4,10 @@
========================================================================== */
/**
* 1. Correct the line height in all browsers.
* 2. Prevent adjustments of font size after orientation changes in
* IE on Windows Phone and in iOS.
* 1.
* 2. Windows Phone IE iOS
*/
html {
html {
line-height: 1.15; /* 1 */
-ms-text-size-adjust: 100%; /* 2 */
-webkit-text-size-adjust: 100%; /* 2 */
@ -19,17 +17,15 @@
========================================================================== */
/**
* Remove the margin in all browsers (opinionated).
* body
*/
body {
margin: 0;
}
/**
* Add the correct display in IE 9-.
* IE 9-
*/
article,
aside,
footer,
@ -40,10 +36,8 @@ section {
}
/**
* Correct the font size and margin on `h1` elements within `section` and
* `article` contexts in Chrome, Firefox, and Safari.
* Chrome, Firefox, Safari section article h1
*/
h1 {
font-size: 2em;
margin: 0.67em 0;
@ -53,10 +47,9 @@ h1 {
========================================================================== */
/**
* Add the correct display in IE 9-.
* 1. Add the correct display in IE.
* IE 9-
* 1. IE main
*/
figcaption,
figure,
main { /* 1 */
@ -64,18 +57,16 @@ main { /* 1 */
}
/**
* Add the correct margin in IE 8.
* IE 8
*/
figure {
margin: 1em 40px;
}
/**
* 1. Add the correct box sizing in Firefox.
* 2. Show the overflow in Edge and IE.
* 1. Firefox box-sizing
* 2. EdgeIE
*/
hr {
box-sizing: content-box; /* 1 */
height: 0; /* 1 */
@ -83,10 +74,9 @@ hr {
}
/**
* 1. Correct the inheritance and scaling of font size in all browsers.
* 2. Correct the odd `em` font sizing in all browsers.
* 1.
* 2. em
*/
pre {
font-family: monospace, monospace; /* 1 */
font-size: 1em; /* 2 */
@ -96,47 +86,42 @@ pre {
========================================================================== */
/**
* 1. Remove the gray background on active links in IE 10.
* 2. Remove gaps in links underline in iOS 8+ and Safari 8+.
* 1. IE 10
* 2. iOS 8+ Safari 8+线
*/
a {
background-color: transparent; /* 1 */
}
/**
* 1. Remove the bottom border in Chrome 57- and Firefox 39-.
* 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.
* 1. Chrome 57- Firefox 39-
* 2. Chrome, Edge, IE, Opera, Safari
*/
abbr[title] {
border-bottom: none; /* 1 */
text-decoration: underline; /* 2 */
}
/**
* Prevent the duplicate application of `bolder` by the next rule in Safari 6.
* Safari 6 bolder
*/
b,
strong {
font-weight: inherit;
}
/**
* Add the correct font weight in Chrome, Edge, and Safari.
* Chrome, Edge, Safari
*/
b,
strong {
font-weight: bolder;
}
/**
* 1. Correct the inheritance and scaling of font size in all browsers.
* 2. Correct the odd `em` font sizing in all browsers.
* 1.
* 2. em
*/
code,
kbd,
samp {
@ -145,35 +130,30 @@ samp {
}
/**
* Add the correct font style in Android 4.3-.
* Android 4.3-
*/
dfn {
font-style: italic;
}
/**
* Add the correct background and color in IE 9-.
* IE 9-
*/
mark {
background-color: #ff0;
color: #000;
}
/**
* Add the correct font size in all browsers.
*
*/
small {
font-size: 80%;
}
/**
* Prevent `sub` and `sup` elements from affecting the line height in
* all browsers.
* sub sup
*/
sub,
sup {
font-size: 75%;
@ -194,35 +174,31 @@ sup {
========================================================================== */
/**
* Add the correct display in IE 9-.
* IE 9-
*/
audio,
video {
display: inline-block;
}
/**
* Add the correct display in iOS 4-7.
* iOS 4-7 audio
*/
audio:not([controls]) {
display: none;
height: 0;
}
/**
* Remove the border on images inside links in IE 10-.
* IE 10-
*/
img {
border-style: none;
}
/**
* Hide the overflow in IE.
* IE
*/
svg:not(:root) {
overflow: hidden;
}
@ -231,10 +207,9 @@ svg:not(:root) {
========================================================================== */
/**
* 1. Change the font styles in all browsers (opinionated).
* 2. Remove the margin in Firefox and Safari.
* 1.
* 2. FirefoxSafari
*/
button,
input,
optgroup,
@ -247,42 +222,38 @@ textarea {
}
/**
* Show the overflow in IE.
* 1. Show the overflow in Edge.
* IE
* 1. Edge
*/
button,
input { /* 1 */
overflow: visible;
}
/**
* Remove the inheritance of text transform in Edge, Firefox, and IE.
* 1. Remove the inheritance of text transform in Firefox.
* Edge, Firefox, IE text-transform
* 1. Firefox text-transform
*/
button,
select { /* 1 */
text-transform: none;
}
/**
* 1. Prevent a WebKit bug where (2) destroys native `audio` and `video`
* controls in Android 4.
* 2. Correct the inability to style clickable types in iOS and Safari.
* 1. WebKit bugbug Android 4 native `audio` `video`
* 2. iOSSafari
*/
button,
html [type="button"], /* 1 */
[type="reset"],
[type="reset"],
[type="submit"] {
-webkit-appearance: button; /* 2 */
}
/**
* Remove the inner border and padding in Firefox.
* Firefox
*/
button::-moz-focus-inner,
[type="button"]::-moz-focus-inner,
[type="reset"]::-moz-focus-inner,
@ -292,9 +263,8 @@ button::-moz-focus-inner,
}
/**
* Restore the focus styles unset by the previous rule.
* Firefox
*/
button:-moz-focusring,
[type="button"]:-moz-focusring,
[type="reset"]:-moz-focusring,
@ -303,20 +273,17 @@ button:-moz-focusring,
}
/**
* Correct the padding in Firefox.
* Firefox fieldset
*/
fieldset {
padding: 0.35em 0.75em 0.625em;
}
/**
* 1. Correct the text wrapping in Edge and IE.
* 2. Correct the color inheritance from `fieldset` elements in IE.
* 3. Remove the padding so developers are not caught out when they zero out
* `fieldset` elements in all browsers.
* 1. EdgeIE
* 2. IE fieldset
* 3. fieldset
*/
legend {
box-sizing: border-box; /* 1 */
color: inherit; /* 2 */
@ -327,28 +294,25 @@ legend {
}
/**
* 1. Add the correct display in IE 9-.
* 2. Add the correct vertical alignment in Chrome, Firefox, and Opera.
* 1. IE 9- progress
* 2. Chrome, Firefox, Opera
*/
progress {
display: inline-block; /* 1 */
vertical-align: baseline; /* 2 */
}
/**
* Remove the default vertical scrollbar in IE.
* IE
*/
textarea {
overflow: auto;
}
/**
* 1. Add the correct box sizing in IE 10-.
* 2. Remove the padding in IE 10-.
* 1. IE 10- box-sizing
* 2. IE 10-
*/
[type="checkbox"],
[type="radio"] {
box-sizing: border-box; /* 1 */
@ -356,38 +320,34 @@ textarea {
}
/**
* Correct the cursor style of increment and decrement buttons in Chrome.
* Chrome
*/
[type="number"]::-webkit-inner-spin-button,
[type="number"]::-webkit-outer-spin-button {
height: auto;
}
/**
* 1. Correct the odd appearance in Chrome and Safari.
* 2. Correct the outline style in Safari.
* 1. ChromeSafari
* 2. Safari
*/
[type="search"] {
-webkit-appearance: textfield; /* 1 */
outline-offset: -2px; /* 2 */
}
/**
* Remove the inner padding and cancel buttons in Chrome and Safari on macOS.
* macOSChromeSafari
*/
[type="search"]::-webkit-search-cancel-button,
[type="search"]::-webkit-search-decoration {
-webkit-appearance: none;
}
/**
* 1. Correct the inability to style clickable types in iOS and Safari.
* 2. Change font properties to `inherit` in Safari.
* 1. iOSSafari
* 2. inherit Safari
*/
::-webkit-file-upload-button {
-webkit-appearance: button; /* 1 */
font: inherit; /* 2 */
@ -396,20 +356,18 @@ textarea {
/* Interactive
========================================================================== */
/*
* Add the correct display in IE 9-.
* 1. Add the correct display in Edge, IE, and Firefox.
/**
* IE 9-
* 1. Edge, IE, Firefox
*/
details, /* 1 */
menu {
display: block;
}
/*
* Add the correct display in all browsers.
/**
* summary
*/
summary {
display: list-item;
}
@ -418,17 +376,15 @@ summary {
========================================================================== */
/**
* Add the correct display in IE 9-.
* IE 9- canvas
*/
canvas {
display: inline-block;
}
/**
* Add the correct display in IE.
* IE template
*/
template {
display: none;
}
@ -437,9 +393,8 @@ template {
========================================================================== */
/**
* Add the correct display in IE 10-.
* IE 10- [hidden]
*/
[hidden] {
display: none;
}
}

@ -1,14 +1,36 @@
// css
//
// --color-primary:
$--color-primary: #155BD4;
// Navbar
// Navbar ()
//
$navbar--background-color: $--color-primary;
// Sidebar
// Sidebar ()
//
$sidebar--background-color-dark: #263238;
//
$sidebar--color-text-dark: #8a979e;
// Content
// Content ()
//
$content--background-color: #f1f4f5;
/* 下面是使用上述变量的实际CSS规则示例 */
/* 导航栏 */
.navbar {
background-color: $navbar--background-color; // 使
}
/* 侧边栏 */
.sidebar {
background-color: $sidebar--background-color-dark; // 使
color: $sidebar--color-text-dark; // 使
}
/* 内容区 */
.content {
background-color: $content--background-color; // 使
}

@ -1,7 +1,33 @@
// element-plus
:root {
/*
* CSS
*/
/*
* --el-color-primary:
*
*
*/
--el-color-primary: #155BD4;
/*
* --el-color-primary-light-3:
*
*
*/
--el-color-primary-light-3: #447cdd;
/*
* --el-menu-item-height: 40px
* Element Plus
*
*/
--el-menu-item-height: 40px;
/*
* --el-menu-sub-item-height: 40px
*
*
*/
--el-menu-sub-item-height: 40px;
}

@ -1,8 +1,27 @@
@import "_normalize"; // api: https://github.com/necolas/normalize.css/
// normalize.css
// api: https://github.com/necolas/normalize.css/
// normalize.css CSS
//
@import "_normalize";
//
// _variables 使
// 使便
@import "_variables";
//
// _base resettypographylayout
//
@import "_base";
// Element Plus
// Element Plus
// Element Plus
@import "@/styles/element-variables.scss";
// z-index
// .element-error-message-zindex z-index
// z-index 3000使 !important
.element-error-message-zindex {
z-index:3000 !important;
z-index: 3000 !important;
}

@ -1,14 +1,40 @@
import CryptoJS from 'crypto-js'
// 加密
const keyStr = '-mall4j-password' // 解密用的key
export function encrypt (word) {
const time = Date.now()
const key = CryptoJS.enc.Utf8.parse(keyStr)
const srcs = CryptoJS.enc.Utf8.parse(time + word) // 加密方式: 时间戳 + 密文
// 引入 CryptoJS 库
// CryptoJS 是一个流行的 JavaScript 加密库,提供多种加密算法,
// 包括 AES高级加密标准。它允许开发者在客户端对数据进行加密和解密。
import CryptoJS from 'crypto-js';
// 定义用于加密和解密的密钥
// keyStr 是一个固定的字符串,作为加密和解密时使用的密钥。
// 注意:在实际应用中,应确保密钥的安全性,避免硬编码或泄露。
const keyStr = '-mall4j-password'; // 解密用的key
/**
* 加密函数
* @param {string} word - 需要加密的明文字符串
* @returns {string} - 返回经过 AES 加密后的密文字符串
*/
export function encrypt(word) {
// 获取当前时间戳,用于生成唯一的加密内容前缀
// 这有助于防止相同的明文产生相同的密文,增加了安全性。
const time = Date.now();
// 将密钥字符串转换为字节数组 (WordArray),以便用于加密操作
// 使用 Utf8 编码来解析密钥字符串确保正确处理非ASCII字符。
const key = CryptoJS.enc.Utf8.parse(keyStr);
// 将时间戳与需要加密的文本连接起来,并将其转换为字节数组
// 同样使用 Utf8 编码来解析字符串,以确保正确处理所有字符。
const srcs = CryptoJS.enc.Utf8.parse(time + word); // 加密方式: 时间戳 + 密文
// 使用 AES 算法进行加密
// mode: ECB 表示使用电子密码本模式,这是一种简单的加密模式,但不适合加密大量数据或要求高安全性的场景。
// padding: Pkcs7 表示使用 PKCS#7 填充方案这是AES加密推荐的填充方式。
const encrypted = CryptoJS.AES.encrypt(srcs, key, {
mode: CryptoJS.mode.ECB,
padding: CryptoJS.pad.Pkcs7
})
return encrypted.toString()
});
// 将加密后的结果转换为 Base64 字符串返回
// 这样可以方便地在网络上传输或存储加密数据。
return encrypted.toString();
}

@ -1,19 +1,44 @@
// 防抖 防止表单重复提交
/**
* 防抖函数 (Debounce Function)
* 用于限制某个函数在短时间内频繁触发时只执行一次
* 这对于防止表单重复提交搜索框输入触发等场景非常有用
* 因为它可以确保即使用户快速连续点击按钮或输入内容
* 相关操作也只会被执行一次直到经过一段设定的时间间隔后才会允许再次触发
*
* @param {Function} fn - 要防抖处理的目标函数
* @param {number} t - 可选参数指定等待的时间间隔毫秒默认为1000ms
* @returns {Function} - 返回一个新的函数该函数具有防抖行为
*/
export const Debounce = (fn, t) => {
const delay = t || 1000
let timer
// 设置默认延迟时间为1000毫秒1秒
const delay = t || 1000;
// 定义一个变量来保存定时器ID用于清除之前的定时器
let timer;
// 返回一个新函数,这个函数会替代原始函数被调用
return function () {
const args = arguments
// 保存当前函数调用的所有参数和上下文
const args = arguments;
const context = this;
// 如果之前有未完成的定时器,则清除它
if (timer) {
clearTimeout(timer)
clearTimeout(timer);
}
const callNow = !timer
// 检查是否是首次调用或上一次调用已经超过了设定的时间间隔
const callNow = !timer;
// 设置一个新的定时器在指定的延迟时间后将timer置为空
// 置空是为了下一次调用时能够正确判断是否立即执行目标函数
timer = setTimeout(() => {
timer = null
}, delay)
timer = null;
}, delay);
if (callNow) fn.apply(this, args)
}
}
// 如果是首次调用或者上一次调用已经超过了设定的时间间隔,则立即执行目标函数
if (callNow) {
fn.apply(context, args);
}
};
};

@ -1,190 +1,202 @@
import axios from 'axios'
import qs from 'qs'
import cookie from 'vue-cookies'
import router from '@/router'
import merge from 'lodash/merge'
import { clearLoginInfo } from '@/utils'
import { ElMessage } from 'element-plus'
// 引入必要的库和模块
import axios from 'axios'; // 用于发起HTTP请求
import qs from 'qs'; // 用于序列化查询参数
import cookie from 'vue-cookies'; // 用于管理cookie
import router from '@/router'; // 项目路由实例
import merge from 'lodash/merge'; // 用于合并对象
import { clearLoginInfo } from '@/utils'; // 清除登录信息的工具函数
import { ElMessage } from 'element-plus'; // Element Plus的消息提示组件
// 创建一个自定义的axios实例
const http = axios.create({
timeout: 1000 * 30,
withCredentials: true,
timeout: 1000 * 30, // 设置请求超时时间为30秒
withCredentials: true, // 允许跨域请求时携带凭证如Cookie
headers: {
'Content-Type': 'application/json; charset=utf-8'
'Content-Type': 'application/json; charset=utf-8' // 默认设置请求头为JSON格式
}
})
});
/**
* 请求拦截
* 请求拦截器
* 在每个请求发送之前执行可以在此处添加通用的请求头或处理请求参数等
*/
http.interceptors.request.use(
config => {
config.headers.Authorization = cookie.get('Authorization') // 请求头带上token
// 只针对get方式进行序列化
if (config.method === 'get' || config.method === 'GET') {
config.paramsSerializer = function (params) {
return qs.stringify(params, { arrayFormat: 'repeat' })
}
// 如果存在Authorization token则添加到请求头中
config.headers.Authorization = cookie.get('Authorization');
// 对GET请求进行参数序列化支持数组格式重复参数
if (config.method.toLowerCase() === 'get') {
config.paramsSerializer = params => qs.stringify(params, { arrayFormat: 'repeat' });
}
return config
return config;
},
error => {
return Promise.reject(error)
}
)
error => Promise.reject(error) // 捕获请求错误并返回拒绝的Promise
);
/**
* 响应拦截
* 响应拦截器
* 在接收到响应数据后执行可以根据响应状态码或自定义业务逻辑来处理响应
*/
http.interceptors.response.use(
response => {
// blob 格式处理
// 直接返回blob类型的响应数据
if (response.request.responseType === 'blob') {
return response
}
const res = response.data
// 00000 请求成功
if (res.code === '00000' || res.code === 'A00002') {
return res
}
// A00001 用于直接显示提示用户的错误,内容由输入决定
if (res.code === 'A00001') {
ElMessage({
message: res.msg || res.data || 'Error',
type: 'error',
duration: 1.5 * 1000
})
return Promise.reject(res)
}
// A00004 未授权
if (res.code === 'A00004') {
clearLoginInfo()
router.push({ name: 'login' })
return response;
}
// A00005 服务器异常
if (res.code === 'A00005') {
// eslint-disable-next-line no-console
console.error('============== 请求异常 ==============', '\n', `接口地址: ${response.config.url.replace(import.meta.env.VITE_APP_BASE_API, '')}`, '\n', `异常信息: ${res}`, '\n', '============== 请求异常 end ==========')
ElMessage({
message: '服务器出了点小差,请稍后再试',
type: 'error',
duration: 1.5 * 1000,
customClass: 'element-error-message-zindex'
})
return Promise.reject(res)
const res = response.data;
// 根据不同的响应码执行相应的操作
switch (res.code) {
case '00000': // 成功响应
case 'A00002': // 特定成功的响应码
return res;
case 'A00001': // 用户级错误消息
ElMessage({
message: res.msg || res.data || 'Error',
type: 'error',
duration: 1500
});
return Promise.reject(res);
case 'A00004': // 未授权
clearLoginInfo();
router.push({ name: 'login' });
break;
case 'A00005': // 服务器异常
console.error(`接口地址: ${response.config.url.replace(import.meta.env.VITE_APP_BASE_API, '')}`, '\n', `异常信息: ${res}`);
ElMessage({
message: '服务器出了点小差,请稍后再试',
type: 'error',
duration: 1500,
customClass: 'element-error-message-zindex'
});
return Promise.reject(res);
default:
return res;
}
},
error => {
// eslint-disable-next-line no-console
console.log('========请求失败========', '\n', error.response, '\n', '========请求失败 end========')
switch (error.response.status) {
case 400:
// 处理不同HTTP状态码的错误响应
console.log('请求失败:', error.response);
const status = error.response?.status;
switch (status) {
case 400: // 客户端错误
ElMessage({
message: error.response.data,
type: 'error',
duration: 1500,
customClass: 'element-error-message-zindex'
})
break
case 401:
clearLoginInfo()
router.push({ name: 'login' })
break
case 405:
});
break;
case 401: // 未授权
clearLoginInfo();
router.push({ name: 'login' });
break;
case 405: // 方法不允许
ElMessage({
message: 'http请求方式有误',
type: 'error',
duration: 1500,
customClass: 'element-error-message-zindex'
})
break
case 500:
});
break;
case 500: // 服务器内部错误
ElMessage({
message: '服务器出了点小差,请稍后再试',
type: 'error',
duration: 1500,
customClass: 'element-error-message-zindex'
})
break
case 501:
});
break;
case 501: // 服务器不支持当前请求功能
ElMessage({
message: '服务器不支持当前请求所需要的某个功能',
type: 'error',
duration: 1500,
customClass: 'element-error-message-zindex'
})
break
});
break;
}
return Promise.reject(error)
return Promise.reject(error);
}
)
);
/**
* 请求地址处理
* @param {*} actionName action方法名称
* @param {string} actionName - API方法名称
* @returns {string} - 完整的API请求URL
*/
http.adornUrl = actionName => {
return import.meta.env.VITE_APP_BASE_API + actionName
}
http.adornUrl = actionName => import.meta.env.VITE_APP_BASE_API + actionName;
/**
* im请求地址处理
* @param {*} actionName action方法名称
* IM请求地址处理
* @param {string} actionName - IM API方法名称
* @returns {string} - 完整的IM API请求URL
*/
http.adornImUrl = actionName => {
return import.meta.env.VITE_APP_IM_API + actionName
}
http.adornImUrl = actionName => import.meta.env.VITE_APP_IM_API + actionName;
/**
* im ws 请求地址处理
* @param {*} actionName action方法名称
* IM WebSocket请求地址处理
* @param {string} actionName - WebSocket IM API方法名称
* @returns {string} - 完整的WebSocket IM API请求URL
*/
http.adornWsImUrl = actionName => {
return import.meta.env.VITE_APP_WS_IM_API + actionName
}
http.adornWsImUrl = actionName => import.meta.env.VITE_APP_WS_IM_API + actionName;
/**
* get请求参数处理
* @param {*} params 参数对象
* @param {*} openDefultParams 是否开启默认参数?
* GET请求参数处理
* @param {Object} [params={}] - 参数对象
* @param {boolean} [openDefultParams=true] - 是否开启默认参数
* @returns {Object} - 处理后的参数对象
*/
http.adornParams = (params = {}, openDefultParams = true) => {
const defaults = {
t: Date.now()
}
return openDefultParams ? merge(defaults, params) : params
}
const defaults = { t: Date.now() }; // 添加时间戳以防止缓存
return openDefultParams ? merge(defaults, params) : params;
};
/**
* post请求数据处理
* @param {*} data 数据对象
* @param {*} openDefultdata 是否开启默认数据?
* @param {*} contentType 数据格式
* json: 'application/json; charset=utf-8'
* form: 'application/x-www-form-urlencoded; charset=utf-8'
* POST请求数据处理
* @param {Object} [data={}] - 数据对象
* @param {boolean} [openDefultdata=true] - 是否开启默认数据
* @param {string} [contentType='json'] - 数据格式'json' 'form'
* @returns {string|FormData} - 处理后的数据
*/
http.adornData = (data = {}, openDefultdata = true, contentType = 'json') => {
const defaults = {
t: Date.now()
}
data = openDefultdata ? merge(defaults, data) : data
return contentType === 'json' ? JSON.stringify(data) : qs.stringify(data)
}
const defaults = { t: Date.now() };
data = openDefultdata ? merge(defaults, data) : data;
// 根据contentType决定如何序列化数据
return contentType === 'json' ? JSON.stringify(data) : qs.stringify(data);
};
/**
* 文件上传函数
* @param {string} url - 上传文件的目标URL
* @param {File} file - 要上传的文件
* @returns {Promise} - 返回上传结果的Promise
*/
const uploadFile = function (url, file) {
const config = {
// 添加请求头
headers: {
'Content-Type': 'multipart/form-data',
Authorization: cookie.get('Authorization') // 请求头带上token
Authorization: cookie.get('Authorization') // 添加token到请求头
}
}
const param = new FormData()
// 通过append向form对象添加数据
param.append('file', file)
return axios.post(url, param, config)
}
export default http
export { uploadFile }
};
const param = new FormData();
param.append('file', file); // 向FormData对象添加文件
return axios.post(url, param, config);
};
export default http;
export { uploadFile };

@ -1,109 +1,150 @@
import cookie from 'vue-cookies'
import router from '@/router'
import cookie from 'vue-cookies'; // 用于管理浏览器中的cookie
import router from '@/router'; // 引入Vue Router实例
/**
* 获取uuid
* 获取UUID通用唯一识别码
* @returns {string} - 返回一个随机生成的UUID字符串
*/
export function getUUID () {
export function getUUID() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
return (c === 'x' ? (Math.random() * 16) | 0 : 'r&0x3' | '0x8').toString(16)
})
// 使用位运算生成随机数,并根据'x'或'y'的不同生成不同的值
return (c === 'x' ? (Math.random() * 16) | 0 : ('r&0x3' | '0x8')).toString(16);
});
}
/**
* 是否有权限
* @param {*} key
* 检查用户是否有特定权限
* @param {string} key - 权限标识符
* @returns {boolean} - 如果用户有此权限则返回true否则返回false
*/
export function isAuth (key) {
const authorities = JSON.parse(sessionStorage.getItem('Authorities') || '[]')
export function isAuth(key) {
const authorities = JSON.parse(sessionStorage.getItem('Authorities') || '[]'); // 尝试从sessionStorage中获取用户权限列表
if (authorities.length) {
for (const i in authorities) {
const element = authorities[i]
const element = authorities[i];
if (element === key) {
return true
return true; // 如果找到匹配的权限则返回true
}
}
}
return false
return false; // 如果没有找到匹配的权限则返回false
}
/**
* 清除登录信息
*/
export function clearLoginInfo () {
cookie.remove('Authorization')
router.options.isAddDynamicMenuRoutes = false
export function clearLoginInfo() {
cookie.remove('Authorization'); // 移除存储在cookie中的授权token
router.options.isAddDynamicMenuRoutes = false; // 禁用动态路由添加功能
}
/**
* 树形数据转换
* @param {*} data
* @param {*} id
* @param {*} pid
* 将扁平的数据结构转换为树形结构
* @param {Array} data - 扁平化的数据数组
* @param {string} [id='id'] - 数据项ID字段名默认为'id'
* @param {string} [pid='parentId'] - 数据项父级ID字段名默认为'parentId'
* @returns {Array} - 转换后的树形数据结构
*/
export function treeDataTranslate (data, id = 'id', pid = 'parentId') {
const res = []
const temp = {}
export function treeDataTranslate(data, id = 'id', pid = 'parentId') {
const res = []; // 存储根节点
const temp = {}; // 临时对象,用于快速查找子节点
// 首先将所有元素存入temp对象中以它们的ID作为键
for (let i = 0; i < data.length; i++) {
temp[data[i][id]] = data[i]
temp[data[i][id]] = data[i];
}
// 再次遍历data数组构建树结构
for (let k = 0; k < data.length; k++) {
// 如果当前元素有父级元素且不是自身,则将其作为子元素加入到父元素下
if (temp[data[k][pid]] && data[k][id] !== data[k][pid]) {
if (!temp[data[k][pid]].children) {
temp[data[k][pid]].children = []
temp[data[k][pid]].children = [];
}
if (!temp[data[k][pid]]._level) {
temp[data[k][pid]]._level = 1
temp[data[k][pid]]._level = 1;
}
data[k]._level = temp[data[k][pid]]._level + 1
temp[data[k][pid]].children.push(data[k])
data[k]._level = temp[data[k][pid]]._level + 1;
temp[data[k][pid]].children.push(data[k]);
} else {
res.push(data[k])
// 如果没有父级元素,则认为是根节点,直接加入到结果数组中
res.push(data[k]);
}
}
return res
return res;
}
function idListFromTree (data, val, res = [], id = 'id', children = 'children') {
/**
* 递归函数从树结构中提取指定ID路径
* @param {Array} data - 树形数据结构
* @param {any} val - 查找的目标ID
* @param {Array} [res=[]] - 存储找到的ID路径
* @param {string} [id='id'] - 数据项ID字段名默认为'id'
* @param {string} [children='children'] - 数据项子元素字段名默认为'children'
* @returns {boolean} - 是否找到了目标ID
*/
function idListFromTree(data, val, res = [], id = 'id', children = 'children') {
for (let i = 0; i < data.length; i++) {
const element = data[i]
if (element[children]) {
if (idListFromTree(element[children], val, res, id, children)) {
res.push(element[id])
return true
}
const element = data[i];
// 递归处理子元素
if (element[children] && idListFromTree(element[children], val, res, id, children)) {
res.push(element[id]); // 当找到目标时将当前节点ID加入路径
return true;
}
// 如果当前节点就是目标节点,直接加入路径并返回
if (element[id] === val) {
res.push(element[id])
return true
res.push(element[id]);
return true;
}
}
return false;
}
/**
* 将数组中的parentId列表取出倒序排列
* @param {Array} data - 树形数据结构
* @param {any} val - 查找的目标ID
* @param {string} [id='id'] - 数据项ID字段名默认为'id'
* @param {string} [children='children'] - 数据项子元素字段名默认为'children'
* @returns {Array} - 倒序排列的ID路径
*/
// eslint-disable-next-line no-unused-vars
export function idList (data, val, id = 'id', children = 'children') {
const res = []
idListFromTree(data, val, res, id)
return res
export function idList(data, val, id = 'id', children = 'children') {
const res = [];
idListFromTree(data, val, res, id, children); // 调用递归函数查找ID路径
return res.reverse(); // 返回倒序排列的结果
}
/**
* 文件地址校验
* @param fileUrl 获取到的文件路径
* 文件地址校验与处理
* @param {string|Array} fileUrl - 获取到的文件路径或文件路径数组
* @returns {string|Array} - 处理后的文件路径或路径数组
*/
export function checkFileUrl (fileUrl) {
if (fileUrl === '') return ''
const baseUrl = import.meta.env.VITE_APP_RESOURCES_URL
// 适配el-image 图片组件预览功能
if (fileUrl && typeof fileUrl === 'object') {
// eslint-disable-next-line no-return-assign
return fileUrl.map(el => el = checkFileUrl(el))
} else {
if (fileUrl && fileUrl.indexOf('http') === -1) {
return baseUrl + fileUrl
} else {
return fileUrl
}
export function checkFileUrl(fileUrl) {
if (fileUrl === '') return ''; // 如果输入为空字符串,直接返回空字符串
const baseUrl = import.meta.env.VITE_APP_RESOURCES_URL; // 获取资源基础URL
// 如果fileUrl是一个对象如el-image组件传递的对象则递归处理每个元素
if (fileUrl && typeof fileUrl === 'object' && !Array.isArray(fileUrl)) {
return Object.keys(fileUrl).reduce((acc, key) => {
acc[key] = checkFileUrl(fileUrl[key]);
return acc;
}, {});
}
// 如果fileUrl是数组则映射处理每个元素
if (Array.isArray(fileUrl)) {
return fileUrl.map(el => checkFileUrl(el));
}
// 对于单个文件路径如果它不是绝对路径则拼接基础URL
if (fileUrl && typeof fileUrl === 'string' && fileUrl.indexOf('http') === -1) {
return baseUrl + fileUrl;
}
return fileUrl; // 如果已经是绝对路径或无效路径,则直接返回原值
}

@ -1,102 +1,35 @@
/**
* 单位选项列表
* 此数组包含了多个对象每个对象代表一个可能的商品单位
* 用于在用户界面中提供选择比如商品库存管理订单录入等场景
* 每个对象都有两个属性
* - label: 显示给用户的文本标签
* - value: 实际存储或传递的数据值
*/
export const unitOption = [
{
lable: '件',
value: '件'
},
{
lable: '盒',
value: '盒'
},
{
lable: '箱',
value: '箱'
},
{
lable: '包',
value: '包'
},
{
lable: '瓶',
value: '瓶'
},
{
lable: '只',
value: '只'
},
{
lable: '千克',
value: '千克'
},
{
lable: '克',
value: '克'
},
{
lable: '斤',
value: '斤'
},
{
lable: '两',
value: '两'
},
{
lable: '双',
value: '双'
},
{
lable: '套',
value: '套'
},
{
lable: '对',
value: '对'
},
{
lable: '块',
value: '块'
},
{
lable: '台',
value: '台'
},
{
lable: '本',
value: '本'
},
{
lable: '把',
value: '把'
},
{
lable: '码',
value: '码'
},
{
lable: '捆',
value: '捆'
},
{
lable: '提',
value: '提'
},
{
lable: '杯',
value: '杯'
},
{
lable: '听',
value: '听'
},
{
lable: '条',
value: '条'
},
{
lable: '副',
value: '副'
},
{
lable: '顶',
value: '顶'
}
]
{ label: '件', value: '件' }, // 通用计数单位
{ label: '盒', value: '盒' }, // 适用于装在盒子中的物品
{ label: '箱', value: '箱' }, // 大量物品的包装单位
{ label: '包', value: '包' }, // 适用于包裹或小袋装的物品
{ label: '瓶', value: '瓶' }, // 适用于液体或粉末状物品
{ label: '只', value: '只' }, // 适用于单个独立物品,如手套、袜子等
{ label: '千克', value: '千克' }, // 重量单位1000克
{ label: '克', value: '克' }, // 较小的重量单位
{ label: '斤', value: '斤' }, // 传统中国重量单位等于500克
{ label: '两', value: '两' }, // 传统中国重量单位等于50克
{ label: '双', value: '双' }, // 适用于成对物品,如鞋子、筷子等
{ label: '套', value: '套' }, // 适用于一组或多件相关联的物品
{ label: '对', value: '对' }, // 适用于成对物品,类似“双”
{ label: '块', value: '块' }, // 适用于固体物品,如巧克力、石头等
{ label: '台', value: '台' }, // 适用于大型设备或机器
{ label: '本', value: '本' }, // 适用于书籍或其他装订成册的物品
{ label: '把', value: '把' }, // 适用于手持工具或武器
{ label: '码', value: '码' }, // 长度单位约等于0.9144米
{ label: '捆', value: '捆' }, // 适用于捆绑在一起的物品
{ label: '提', value: '提' }, // 适用于手提袋或携带方便的物品
{ label: '杯', value: '杯' }, // 适用于饮品或小容量容器
{ label: '听', value: '听' }, // 适用于罐装食品或饮料
{ label: '条', value: '条' }, // 适用于细长形物品,如鱼、香烟等
{ label: '副', value: '副' }, // 适用于眼镜、耳环等成对使用的物品
{ label: '顶', value: '顶' } // 适用于帽子、帐篷等覆盖物
];

@ -1,49 +1,69 @@
/**
* 邮箱
* @param {*} s
* 验证邮箱格式是否正确
* @param {string} s - 要验证的字符串
* @returns {boolean} - 如果符合标准邮箱格式则返回true否则返回false
*/
export function isEmail (s) {
return /^([a-zA-Z0-9_-])+@([a-zA-Z0-9_-])+((.[a-zA-Z0-9_-]{2,3}){1,2})$/.test(s)
export function isEmail(s) {
// 正则表达式用于匹配标准的电子邮件地址格式:
// - 用户名部分只能包含字母、数字、下划线或连字符
// - 域名部分同样只允许字母、数字、下划线或连字符
// - 域名后缀长度为2到3个字符并且可以有一个或两个层级例如.com, .co.uk
return /^([a-zA-Z0-9_-])+@([a-zA-Z0-9_-])+((\.[a-zA-Z0-9_-]{2,3}){1,2})$/.test(s);
}
/**
* 手机号码
* @param {*} s
* 验证手机号码格式是否正确
* @param {string} s - 要验证的字符串
* @returns {boolean} - 如果符合中国大陆手机号码格式则返回true否则返回false
*/
export function isMobile (s) {
return /^1[0-9]{10}$/.test(s)
export function isMobile(s) {
// 正则表达式用于匹配中国大陆的手机号码:
// - 以1开头后面跟随10位数字
return /^1[0-9]{10}$/.test(s);
}
/**
* 电话号码
* @param {*} s
* 验证电话号码格式是否正确
* @param {string} s - 要验证的字符串
* @returns {boolean} - 如果符合固定电话号码格式则返回true否则返回false
*/
export function isPhone (s) {
return /^([0-9]{3,4}-)?[0-9]{7,8}$/.test(s)
export function isPhone(s) {
// 正则表达式用于匹配中国大陆的固定电话号码:
// - 可选区号3到4位数字用短横线分隔
// - 主号码由7到8位数字组成
return /^([0-9]{3,4}-)?[0-9]{7,8}$/.test(s);
}
/**
* URL地址
* @param {*} s
* 验证URL地址格式是否正确
* @param {string} s - 要验证的字符串
* @returns {boolean} - 如果符合标准URL格式则返回true否则返回false
*/
export function isURL (s) {
return /^http[s]?:\/\/.*/.test(s)
export function isURL(s) {
// 正则表达式用于匹配标准的URL地址
// - 开头必须是http:// 或 https://
// - 后面跟任意数量的字符
return /^http[s]?:\/\/.*/.test(s);
}
/**
* qq
* @param {*} s
* 验证QQ号码格式是否正确
* @param {string} s - 要验证的字符串
* @returns {boolean} - 如果符合QQ号码格式则返回true否则返回false
*/
export function isQq (s) {
return /[1-9][0-9]{4,14}/.test(s)
export function isQq(s) {
// 正则表达式用于匹配有效的QQ号码
// - QQ号码至少5位最多15位数字
// - 不能以0开头
return /^[1-9][0-9]{4,14}$/.test(s);
}
/**
* 判断是否全为空格 只要有一个其他字符返回false
* @param {String} str
* @returns {Boolean}
* 判断字符串是否全为空格只要有一个非空格字符即返回false
* @param {string} str - 要验证的字符串
* @returns {boolean} - 如果字符串全部为空格则返回true否则返回false
*/
export function validNoEmptySpace (str) {
const reg = /^\s+$/g
return reg.test(str)
export function validNoEmptySpace(str) {
const reg = /^\s+$/g; // 匹配整个字符串全是空白字符(包括空格、制表符等)的正则表达式
return reg.test(str); // 如果字符串完全匹配上述规则则返回true否则返回false
}

@ -1,16 +1,21 @@
<template>
<!-- 400页面模板 -->
<div class="site-wrapper site-page--not-found">
<!-- 内容包裹器用于设置页面整体布局 -->
<div class="site-content__wrapper">
<!-- 页面内容主体 -->
<div class="site-content">
<h2 class="not-found-title">
400
</h2>
<!-- 显示错误代码 -->
<h2 class="not-found-title">400</h2>
<!-- 描述错误信息 -->
<p class="not-found-desc">
抱歉您访问的页面<em>失联</em> ...
</p>
<!-- 返回上一页按钮 -->
<el-button @click="$router.go(-1)">
返回上一页
</el-button>
<!-- 进入首页按钮 -->
<el-button
type="primary"
class="not-found-btn-gohome"
@ -24,48 +29,63 @@
</template>
<script setup>
//
// 使 <script setup> VueAPI
</script>
<style lang="scss" scoped>
.site-wrapper.site-page--not-found {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
overflow: hidden;
.site-content__wrapper {
padding: 0;
margin: 0;
background-color: #fff;
}
.site-content {
position: fixed;
top: 15%;
left: 50%;
z-index: 2;
padding: 30px;
text-align: center;
transform: translate(-50%, 0);
}
.site-wrapper.site-page--not-found {
/* 设置页面占据整个浏览器窗口 */
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
overflow: hidden;
.site-content__wrapper {
/* 移除默认的内边距和外边距,并设置背景颜色 */
padding: 0;
margin: 0;
background-color: #fff;
}
.site-content {
/* 固定定位,使内容居中显示 */
position: fixed;
top: 15%;
left: 50%;
z-index: 2;
padding: 30px;
text-align: center;
transform: translate(-50%, 0);
.not-found-title {
/* 错误代码样式 */
margin: 20px 0 15px;
font-size: 10em;
font-weight: 400;
color: rgb(55, 71, 79);
}
.not-found-desc {
/* 错误描述样式 */
margin: 0 0 30px;
font-size: 26px;
text-transform: uppercase;
color: rgb(118, 131, 143);
> em {
/* 强调文字样式 */
font-style: normal;
color: #ee8145;
}
}
.not-found-btn-gohome {
/* 首页按钮样式,增加左边距以与返回按钮间隔开 */
margin-left: 30px;
}
}
}
</style>

@ -1,114 +1,62 @@
<template>
<div class="mod-home">
<!-- 简介段落 -->
<p>一个基于spring bootspring oauth2.0mybatisredis的轻量级前后端分离拥有完整sku和下单流程的完全开源商城</p>
<p>&nbsp;</p>
<!-- 版权声明 -->
<p>该项目仅供学习参考可供个人学习使用如需商用联系作者进行授权否则必将追究法律责任</p>
<p>&nbsp;</p>
<h2>前言</h2>
<p>
<code>mall4j商城</code>项目致力于为中小企业打造一个完整易于维护的开源电商系统采用现阶段流行技术实现后台管理系统包含商品管理订单管理运费模板规格管理会员管理运营管理内容管理统计报表权限管理设置等模块
<code>mall4j商城</code>项目致力于为中小企业打造一个完整易于维护的开源电商系统采用现阶段流行技术实现后台管理系统包含商品管理订单管理运费模板规格管理会员管理运营管理内容管理统计报表权限管理设置等模块
</p>
<p>&nbsp;</p>
<h2>技术选型</h2>
<figure>
<table
border="1"
cellspacing="0"
cellpadding="5px"
>
<table border="1" cellspacing="0" cellpadding="5px">
<thead>
<tr>
<th>技术</th>
<th>版本</th>
<th>说明</th>
</tr>
<tr>
<th>技术</th>
<th>版本</th>
<th>说明</th>
</tr>
</thead>
<tbody>
<tr>
<td>Spring Boot</td>
<td>3.0.4</td>
<td>MVC核心框架</td>
</tr>
<tr>
<td>MyBatis</td>
<td>3.5.0</td>
<td>ORM框架</td>
</tr>
<tr>
<td>MyBatisPlus</td>
<td>3.5.3.1</td>
<td>基于mybatis使用lambda表达式的</td>
</tr>
<tr>
<td>Swagger-UI</td>
<td>4.0.0</td>
<td>文档生产工具</td>
</tr>
<tr>
<td>redisson</td>
<td>3.19.3</td>
<td>对redis进行封装集成分布式锁等</td>
</tr>
<tr>
<td>hikari</td>
<td>3.2.0</td>
<td>数据库连接池</td>
</tr>
<tr>
<td>log4j2</td>
<td>2.17.2</td>
<td>更快的log日志工具</td>
</tr>
<tr>
<td>lombok</td>
<td>1.18.8</td>
<td>简化对象封装工具</td>
</tr>
<tr>
<td>hutool</td>
<td>5.8.15</td>
<td>更适合国人的java工具集</td>
</tr>
<tr>
<td>xxl-job</td>
<td>2.3.1</td>
<td>定时任务</td>
</tr>
<!-- 列出所用技术及其版本和简要说明 -->
<tr v-for="(tech, index) in technologies" :key="index">
<td>{{ tech.name }}</td>
<td>{{ tech.version }}</td>
<td>{{ tech.description }}</td>
</tr>
</tbody>
</table>
</figure>
<p>&nbsp;</p>
<h2>部署教程</h2>
<p>&nbsp;</p>
<h3>1.开发环境</h3>
<figure>
<table
border="1"
cellspacing="0"
cellpadding="5px"
>
<table border="1" cellspacing="0" cellpadding="5px">
<thead>
<tr>
<th>工具</th>
<th>版本</th>
</tr>
<tr>
<th>工具</th>
<th>版本</th>
</tr>
</thead>
<tbody>
<tr>
<td>jdk</td>
<td>17</td>
</tr>
<tr>
<td>mysql</td>
<td>5.7+</td>
</tr>
<tr>
<td>redis</td>
<td>3.2+</td>
</tr>
<!-- 列出所需的开发环境工具及其版本 -->
<tr v-for="(tool, index) in developmentTools" :key="index">
<td>{{ tool.name }}</td>
<td>{{ tool.version }}</td>
</tr>
</tbody>
</table>
</figure>
<h3>2.启动</h3>
<ul>
<li>推荐使用idea安装lombok插件使用idea导入maven项目</li>
@ -127,8 +75,31 @@
</div>
</template>
<script setup>
//
const technologies = [
{ name: 'Spring Boot', version: '3.0.4', description: 'MVC核心框架' },
{ name: 'MyBatis', version: '3.5.0', description: 'ORM框架' },
{ name: 'MyBatisPlus', version: '3.5.3.1', description: '基于mybatis使用lambda表达式的增强' },
{ name: 'Swagger-UI', version: '4.0.0', description: '文档生产工具' },
{ name: 'redisson', version: '3.19.3', description: '对redis进行封装、集成分布式锁等' },
{ name: 'hikari', version: '3.2.0', description: '数据库连接池' },
{ name: 'log4j2', version: '2.17.2', description: '更快的log日志工具' },
{ name: 'lombok', version: '1.18.8', description: '简化对象封装工具' },
{ name: 'hutool', version: '5.8.15', description: '更适合国人的java工具集' },
{ name: 'xxl-job', version: '2.3.1', description: '定时任务' }
];
//
const developmentTools = [
{ name: 'jdk', version: '17' },
{ name: 'mysql', version: '5.7+' },
{ name: 'redis', version: '3.2+' }
];
</script>
<style lang="scss" scoped>
.mod-home {
line-height: 1.5;
line-height: 1.5; //
}
</style>

@ -1,14 +1,14 @@
<template>
<div class="login">
<!-- 登录框 -->
<div class="login-box">
<!-- 顶部区域包含logo -->
<div class="top">
<div class="logo">
<img
src="~@/assets/img/login-logo.png"
alt=""
>
<img src="~@/assets/img/login-logo.png" alt="Logo">
</div>
</div>
<!-- 中间区域包含登录表单 -->
<div class="mid">
<el-form
ref="dataFormRef"
@ -17,6 +17,7 @@
status-icon
@keyup.enter="dataFormSubmit()"
>
<!-- 用户名输入框 -->
<el-form-item prop="userName">
<el-input
v-model="dataForm.userName"
@ -24,6 +25,7 @@
placeholder="帐号"
/>
</el-form-item>
<!-- 密码输入框 -->
<el-form-item prop="password">
<el-input
v-model="dataForm.password"
@ -32,6 +34,7 @@
placeholder="密码"
/>
</el-form-item>
<!-- 登录按钮 -->
<el-form-item>
<div class="item-btn">
<input
@ -43,10 +46,12 @@
</el-form-item>
</el-form>
</div>
<!-- 底部版权信息 -->
<div class="bottom">
Copyright © 2019 广州市蓝海创新科技有限公司
</div>
</div>
<!-- 验证组件用于显示图形验证码 -->
<Verify
ref="verifyRef"
:captcha-type="'blockPuzzle'"
@ -57,17 +62,23 @@
</template>
<script setup>
import { encrypt } from '@/utils/crypto'
import { getUUID } from '@/utils'
import Verify from '@/components/verifition/Verify.vue'
import cookie from 'vue-cookies'
import { encrypt } from '@/utils/crypto' //
import { getUUID } from '@/utils' //
import Verify from '@/components/verifition/Verify.vue' //
import cookie from 'vue-cookies' // cookies
import { ref, onMounted, onBeforeUnmount } from 'vue'
import { useRouter } from 'vue-router'
import http from '@/api/http'
//
const dataForm = ref({
userName: '',
password: '',
uuid: '',
captcha: ''
})
//
const dataRule = {
userName: [
{ required: true, message: '帐号不能为空', trigger: 'blur' }
@ -80,39 +91,51 @@ const dataRule = {
]
}
//
onBeforeUnmount(() => {
document.removeEventListener('keyup', handerKeyup)
})
//
onMounted(() => {
getCaptcha()
document.addEventListener('keyup', handerKeyup)
})
//
const handerKeyup = (e) => {
const keycode = document.all ? event.keyCode : e.which
const keycode = e.which || e.keyCode
if (keycode === 13) {
this.dataFormSubmit()
dataFormSubmit()
}
}
//
const verifyRef = ref(null)
//
const dataFormRef = ref(null)
let isSubmit = false
/**
* 提交表单
*/
const dataFormSubmit = () => {
dataFormRef.value?.validate((valid) => {
if (valid) {
verifyRef.value?.show()
verifyRef.value?.show() //
}
})
}
//
const router = useRouter()
/**
* 登录逻辑处理
* @param {Object} verifyResult - 验证成功后的结果
*/
const login = (verifyResult) => {
if (isSubmit) {
return
}
if (isSubmit) return
isSubmit = true
http({
url: http.adornUrl('/adminLogin'),
@ -123,8 +146,8 @@ const login = (verifyResult) => {
captchaVerification: verifyResult.captchaVerification
})
}).then(({ data }) => {
cookie.set('Authorization', data.accessToken)
router.replace({ name: 'home' })
cookie.set('Authorization', data.accessToken) // cookie
router.replace({ name: 'home' }) //
}).catch(() => {
isSubmit = false
})
@ -134,9 +157,8 @@ const login = (verifyResult) => {
* 获取验证码
*/
const getCaptcha = () => {
dataForm.value.uuid = getUUID()
dataForm.value.uuid = getUUID() // uuid
}
</script>
<style lang="scss" scoped>
@ -146,30 +168,37 @@ const getCaptcha = () => {
background: url('../../../assets/img/login-bg.png') no-repeat;
background-size: cover;
position: fixed;
.login-box {
position: absolute;
left: 50%;
transform: translateX(-50%);
height: 100%;
padding-top: 10%;
.top {
margin-bottom: 30px;
text-align: center;
.logo {
font-size: 0;
max-width: 50%;
margin: 0 auto;
}
&:deep(.company) {
font-size: 16px;
margin-top: 10px;
}
}
.mid {
font-size: 14px;
.item-btn {
width: 410px;
margin-top: 20px;
input {
border: 0;
width: 100%;
@ -180,6 +209,7 @@ const getCaptcha = () => {
}
}
}
.bottom {
position: absolute;
bottom: 10%;
@ -190,9 +220,11 @@ const getCaptcha = () => {
}
}
}
.info {
width: 410px;
}
:deep(.login-captcha) {
height: 40px;
}

@ -1,5 +1,6 @@
<template>
<div class="mod-index-img">
<!-- 对话框组件用于显示新增或修改轮播图片的表单 -->
<el-dialog
v-model="visible"
:title="!dataForm.imgId ? '新增' : '修改'"
@ -11,12 +12,14 @@
:rules="dataRule"
label-width="100px"
>
<!-- 轮播图片选择项 -->
<el-form-item
label="轮播图片"
prop="imgUrl"
>
<pic-upload v-model="dataForm.imgUrl" />
</el-form-item>
<!-- 顺序输入框 -->
<el-form-item
label="顺序"
prop="seq"
@ -26,32 +29,27 @@
>
<el-input v-model="dataForm.seq" />
</el-form-item>
<!-- 状态选择 -->
<el-form-item
label="状态"
prop="status"
>
<el-radio-group v-model="dataForm.status">
<el-radio :label="0">
禁用
</el-radio>
<el-radio :label="1">
正常
</el-radio>
<el-radio :label="0">禁用</el-radio>
<el-radio :label="1">正常</el-radio>
</el-radio-group>
</el-form-item>
<!-- 类型选择 -->
<el-form-item label="类型">
<el-radio-group
v-model="dataForm.type"
@change="deleteRelation"
>
<el-radio :label="-1">
</el-radio>
<el-radio :label="0">
商品
</el-radio>
<el-radio :label="-1"></el-radio>
<el-radio :label="0">商品</el-radio>
</el-radio-group>
<div v-if="dataForm.relation!=null">
<!-- 显示关联的商品卡片 -->
<div v-if="dataForm.relation != null">
<el-card
:body-style="{ padding: '0px' }"
style="height: 160px;width: 120px"
@ -73,15 +71,12 @@
</div>
</el-card>
</div>
<div v-if="dataForm.relation==null">
<el-button
v-if=" dataForm.type == 0"
@click="addProd"
>
选择商品
</el-button>
<!-- 如果没有关联商品则显示选择商品按钮 -->
<div v-if="dataForm.relation == null && dataForm.type === 0">
<el-button @click="addProd"></el-button>
</div>
</el-form-item>
<!-- 提交按钮 -->
<el-form-item>
<el-button
type="primary"
@ -92,7 +87,7 @@
</el-form-item>
</el-form>
</el-dialog>
<!-- 商品选择弹窗-->
<!-- 商品选择弹窗 -->
<prods-select
v-if="prodsSelectVisible"
ref="prodsSelectRef"
@ -105,7 +100,13 @@
<script setup>
import { ElMessage } from 'element-plus'
import { Debounce } from '@/utils/debounce'
import { ref, reactive, nextTick, defineExpose, defineEmits } from 'vue'
import http from '@/api/http'
//
const emit = defineEmits(['refreshDataList'])
//
const dataForm = ref({
status: 1,
des: '',
@ -115,12 +116,15 @@ const dataForm = ref({
type: -1,
relation: null
})
//
const dataRule = reactive({
imgUrl: [
{ required: true, message: '轮播图片不能为空', trigger: 'blur' }
]
})
//
//
const card = ref({
id: 0,
pic: '',
@ -131,23 +135,32 @@ const card = ref({
activity: []
}
})
//
const page = reactive({
total: 0, //
currentPage: 1, //
pageSize: 10 //
})
//
const prodsSelectVisible = ref(false)
//
const visible = ref(false)
//
const dataFormRef = ref(null)
/**
* 获取分类数据
* @param id
* 初始化数据
* @param id - 要编辑的数据ID若为新增则传入null或不传
*/
const init = (id) => {
visible.value = true
dataForm.value.imgId = id || 0
if (dataForm.value.imgId) {
//
//
http({
url: http.adornUrl(`/admin/indexImg/info/${dataForm.value.imgId}`),
method: 'get'
@ -161,16 +174,19 @@ const init = (id) => {
}
})
} else {
//
nextTick(() => {
dataFormRef.value?.resetFields()
dataForm.value.imgUrl = ''
})
}
}
// init
defineExpose({ init })
/**
* 表单提交
* 表单提交使用防抖功能防止快速多次点击
*/
const onSubmit = Debounce(() => {
dataFormRef.value?.validate((valid) => {
@ -196,16 +212,19 @@ const onSubmit = Debounce(() => {
})
})
})
/**
* 删除关联数据
* 删除关联商品
*/
const deleteRelation = () => {
dataForm.value.relation = null
}
//
const prodsSelectRef = ref(null)
/**
* 打开选择商品
* 打开选择商品窗口
*/
const addProd = () => {
prodsSelectVisible.value = true
@ -213,8 +232,10 @@ const addProd = () => {
prodsSelectRef.value?.init(card.value.realData.prod)
})
}
/**
* 添加指定商品
* 添加指定商品到轮播图关联
* @param prods - 已选中的商品列表
*/
const selectCouponProds = (prods) => {
card.value.realData.prods = prods
@ -231,7 +252,7 @@ const selectCouponProds = (prods) => {
</script>
<style lang="scss" scoped>
//card
//
.card-prod-bottom {
position: relative;
text-align: left;

@ -1,5 +1,6 @@
<template>
<div class="mod-prod">
<!-- Avue-Crud 组件用于创建数据表格 -->
<avue-crud
ref="crudRef"
:page="page"
@ -10,7 +11,9 @@
@selection-change="selectionChange"
@on-load="getDataList"
>
<!-- 自定义左侧菜单按钮 -->
<template #menu-left>
<!-- 新增按钮仅当用户有权限时显示 -->
<el-button
v-if="isAuth('admin:indexImg:save')"
type="primary"
@ -20,6 +23,7 @@
新增
</el-button>
<!-- 批量删除按钮仅当用户有权限且选中至少一项时启用 -->
<el-button
v-if="isAuth('admin:indexImg:delete')"
type="danger"
@ -30,7 +34,9 @@
</el-button>
</template>
<!-- 自定义图片展示列 -->
<template #imgUrl="scope">
<!-- 如果存在图片链接则显示图片否则显示默认图片 -->
<img
v-if="scope.row.imgUrl"
alt=""
@ -46,7 +52,10 @@
height="100"
>
</template>
<!-- 自定义操作菜单 -->
<template #menu="scope">
<!-- 修改按钮仅当用户有权限时显示 -->
<el-button
v-if="isAuth('admin:indexImg:update')"
type="primary"
@ -55,6 +64,7 @@
>
修改
</el-button>
<!-- 删除按钮仅当用户有权限时显示 -->
<el-button
v-if="isAuth('admin:indexImg:delete')"
type="danger"
@ -66,7 +76,7 @@
</template>
</avue-crud>
<!-- 弹窗, 新增 / 修改 -->
<!-- 弹窗组件用于新增或修改轮播图信息 -->
<add-or-update
v-if="addOrUpdateVisible"
ref="addOrUpdateRef"
@ -80,18 +90,33 @@ import { isAuth } from '@/utils'
import { ElMessage, ElMessageBox } from 'element-plus'
import AddOrUpdate from './add-or-update.vue'
import { tableOption } from '@/crud/admin/indexImg.js'
import { ref, reactive, nextTick } from 'vue'
import http from '@/api/http'
//
const dataList = ref([])
//
const dataListLoading = ref(false)
//
const dataListSelections = ref([])
// URL
const resourcesUrl = import.meta.env.VITE_APP_RESOURCES_URL
//
const page = reactive({
total: 0, //
currentPage: 1, //
pageSize: 10 //
pageSize: 10 //
})
/**
* 获取数据列表
* @param {Object} pageParam - 分页参数对象
* @param {Object} params - 查询条件
* @param {Function} done - 完成回调函数
*/
const getDataList = (pageParam, params, done) => {
dataListLoading.value = true
@ -119,11 +144,15 @@ const getDataList = (pageParam, params, done) => {
})
}
//
const addOrUpdateVisible = ref(false)
//
const addOrUpdateRef = ref(null)
/**
* 新增 / 修改
* @param id
* 新增或修改操作
* @param {Number} id - 要编辑的数据ID若为新增则传入null或不传
*/
const onAddOrUpdate = (id) => {
addOrUpdateVisible.value = true
@ -131,14 +160,13 @@ const onAddOrUpdate = (id) => {
addOrUpdateRef.value?.init(id)
})
}
/**
* 删除
* @param id
* 删除操作支持单个删除和批量删除
* @param {Number} id - 单个删除时传入的数据ID
*/
const onDelete = (id) => {
const ids = id ? [id] : dataListSelections.value?.map(item => {
return item.imgId
})
const ids = id ? [id] : dataListSelections.value?.map(item => item.imgId)
ElMessageBox.confirm(`确定进行[${id ? '删除' : '批量删除'}]操作?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
@ -162,14 +190,19 @@ const onDelete = (id) => {
})
})
}
/**
* 条件查询
* 条件查询事件处理
* @param {Object} params - 查询条件
* @param {Function} done - 完成回调函数
*/
const onSearch = (params, done) => {
getDataList(page, params, done)
}
/**
* 多选变化
* 多选变化事件处理
* @param {Array} val - 当前选中的数据项数组
*/
const selectionChange = (val) => {
dataListSelections.value = val

@ -1,5 +1,5 @@
<template>
<!-- 发货信息用于导出代发货订单的excel交给快递公司 -->
<!-- 发货信息对话框用于导出待发货订单的Excel交给快递公司 -->
<el-dialog
v-model="visible"
:modal="false"
@ -7,6 +7,7 @@
:close-on-click-modal="false"
width="38%"
>
<!-- 表单组件包含发件人姓名手机号和地址的输入项 -->
<el-form
ref="dataFormRef"
:model="dataForm"
@ -14,6 +15,7 @@
label-width="120px"
@keyup.enter="onSubmit()"
>
<!-- 发件人姓名输入项 -->
<el-form-item
label="发件人姓名"
prop="consignmentName"
@ -21,9 +23,11 @@
<el-input
v-model="dataForm.consignmentName"
controls-position="right"
label="发件人姓名"
placeholder="请输入发件人姓名"
/>
</el-form-item>
<!-- 发货人手机号输入项 -->
<el-form-item
label="发货人手机号"
prop="consignmentMobile"
@ -31,9 +35,11 @@
<el-input
v-model="dataForm.consignmentMobile"
controls-position="right"
label="发货人手机号"
placeholder="请输入发货人手机号"
/>
</el-form-item>
<!-- 发货地址输入项 -->
<el-form-item
label="发货地址"
prop="consignmentAddr"
@ -41,60 +47,74 @@
<el-input
v-model="dataForm.consignmentAddr"
controls-position="right"
label="发货地址"
placeholder="请输入发货地址"
/>
</el-form-item>
</el-form>
<!-- 对话框底部按钮组 -->
<template #footer>
<span class="dialog-footer">
<el-button
@click="visible = false"
>取消</el-button>
<el-button
type="primary"
@click="onSubmit()"
>确定</el-button>
<!-- 取消按钮关闭对话框 -->
<el-button @click="visible = false">取消</el-button>
<!-- 确定按钮提交表单 -->
<el-button type="primary" @click="onSubmit()"></el-button>
</span>
</template>
</el-dialog>
</template>
<script setup>
import { ref, reactive, nextTick, defineEmits, defineExpose } from 'vue'
//
const emit = defineEmits(['inputCallback'])
//
const visible = ref(false)
//
const dataForm = reactive({
consignmentName: '',
consignmentMobile: '',
consignmentAddr: ''
consignmentName: '', //
consignmentMobile: '', //
consignmentAddr: '' //
})
//
const dataRule = {
consignmentName: [
{ required: true, message: '不能为空', trigger: 'blur' }
{ required: true, message: '发件人姓名不能为空', trigger: 'blur' }
],
consignmentMobile: [
{ required: true, message: '不能为空', trigger: 'blur' }
{ required: true, message: '发货人手机号不能为空', trigger: 'blur' }
],
consignmentAddr: [
{ required: true, message: '不能为空', trigger: 'blur' }
{ required: true, message: '发货地址不能为空', trigger: 'blur' }
]
}
//
const dataFormRef = ref(null)
/**
* 初始化方法打开对话框并重置表单
*/
const init = () => {
visible.value = true
nextTick(() => {
dataFormRef.value?.resetFields()
})
}
defineExpose({ init })
defineExpose({ init }) // init
/**
* 表单提交
* 表单提交方法
*/
const onSubmit = () => {
dataFormRef.value?.validate((valid) => {
if (valid) {
visible.value = false
emit('inputCallback', dataForm)
visible.value = false //
emit('inputCallback', dataForm) //
}
})
}

@ -1,10 +1,12 @@
<template>
<!-- 发货地址选择对话框 -->
<el-dialog
v-model="visible"
:modal="false"
title="选择发货地址"
:close-on-click-modal="false"
>
<!-- 表单组件用于选择快递公司和填写快递单号 -->
<el-form
ref="dataFormRef"
:model="dataForm"
@ -12,11 +14,13 @@
label-width="80px"
@keyup.enter="onSubmit()"
>
<!-- 快递公司选择项 -->
<el-form-item label="快递公司">
<el-select
v-model="dataForm.dvyId"
placeholder="请选择"
>
<!-- 动态生成选项列表 dataForm.dvyNames 中获取 -->
<el-option
v-for="item in dataForm.dvyNames"
:key="item.dvyId"
@ -25,6 +29,8 @@
/>
</el-select>
</el-form-item>
<!-- 快递单号输入项 -->
<el-form-item
label="快递单号"
prop="dvyFlowId"
@ -33,26 +39,37 @@
v-model="dataForm.dvyFlowId"
controls-position="right"
:min="0"
label="快递单号"
placeholder="请输入快递单号"
/>
</el-form-item>
</el-form>
<!-- 对话框底部按钮组 -->
<template #footer>
<span class="dialog-footer">
<!-- 取消按钮关闭对话框 -->
<el-button @click="visible = false">取消</el-button>
<el-button
type="primary"
@click="onSubmit()"
>确定</el-button>
<!-- 确定按钮提交表单 -->
<el-button type="primary" @click="onSubmit()"></el-button>
</span>
</template>
</el-dialog>
</template>
<script setup>
import { ElMessage } from 'element-plus'
import { ref, reactive, defineEmits, defineExpose } from 'vue'
import http from '@/api/http' // HTTP
//
const emit = defineEmits(['refreshDataList'])
// eslint-disable-next-line no-unused-vars
/**
* 自定义验证规则确保快递单号不为空且已正确填写
* @param {Object} rule - 验证规则对象
* @param {String} value - 输入值
* @param {Function} callback - 回调函数用于通知验证结果
*/
const validDvyFlowId = (rule, value, callback) => {
if (!value.trim()) {
callback(new Error('不能为空'))
@ -60,6 +77,8 @@ const validDvyFlowId = (rule, value, callback) => {
callback()
}
}
// 使
const dataRule = {
dvyFlowId: [
{ required: true, message: '不能为空', trigger: 'blur' },
@ -67,19 +86,30 @@ const dataRule = {
]
}
//
const visible = ref(false)
// ID
const dataForm = reactive({
dvyId: '',
dvyFlowId: 0,
dvyNames: [],
orderNumber: 0
dvyId: '', // ID
dvyFlowId: '', //
dvyNames: [], //
orderNumber: '' //
})
/**
* 初始化方法打开对话框并加载快递公司列表
* @param {String} orderNumber - 订单编号
* @param {String} [dvyId] - 默认快递公司ID可选
* @param {String} [dvyFlowId] - 默认快递单号可选
*/
const init = (orderNumber, dvyId, dvyFlowId) => {
visible.value = true
dataForm.orderNumber = orderNumber || ''
dataForm.dvyId = dvyId || ''
dataForm.dvyFlowId = dvyFlowId || ''
// dataForm.dvyNames
http({
url: http.adornUrl('/admin/delivery/list'),
method: 'get',
@ -88,15 +118,18 @@ const init = (orderNumber, dvyId, dvyFlowId) => {
dataForm.dvyNames = data
})
}
defineExpose({ init })
defineExpose({ init }) // init
//
const dataFormRef = ref(null)
/**
* 表单提交
* 表单提交方法验证表单并提交数据
*/
const onSubmit = () => {
dataFormRef.value?.validate((valid) => {
if (valid) {
//
http({
url: http.adornUrl('/order/order/delivery'),
method: 'put',
@ -112,13 +145,12 @@ const onSubmit = () => {
type: 'success',
duration: 1500,
onClose: () => {
visible.value = false
emit('refreshDataList')
visible.value = false //
emit('refreshDataList') //
}
})
})
}
})
}
</script>

@ -1,10 +1,12 @@
<template>
<!-- el-dialog组件用于创建一个对话框用于展示订单相关信息 -->
<el-dialog
v-model="visible"
:title="!dataForm.orderNumber ? '新增' : '查看'"
:title="!dataForm.orderNumber? '新增' : '查看'"
:close-on-click-modal="false"
width="80%"
>
<!-- el-form组件用于创建表单这里用于承载订单信息的展示和相关操作表单 -->
<el-form
ref="dataFormRef"
:model="dataForm"
@ -15,30 +17,36 @@
<div class="content">
<div class="order-number">
<div class="num-cont">
<!-- el-form-item用于在表单中定义一个表单项这里展示订单编号 -->
<el-form-item label="订单编号:">
<span class="text">{{ dataForm.orderNumber }}</span>
</el-form-item>
<el-form-item>
<!-- el-steps组件用于展示步骤条展示订单的不同流程状态 -->
<el-steps
:active="stepsStatus"
align-center
:process-status="dataForm.status == 6 ? 'error':'wait'"
:process-status="dataForm.status == 6? 'error':'wait'"
>
<!-- el-step定义步骤条中的每一个步骤这里展示提交订单步骤及其时间描述 -->
<el-step
title="提交订单"
:description="dataForm.orderTime"
/>
<!-- 展示买家已付款步骤及其时间描述 -->
<el-step
title="买家已付款"
:description="dataForm.payTime"
/>
<!-- 根据订单类型判断是否展示卖家已发货步骤及其时间描述 -->
<el-step
v-if="dataForm.orderType !== 1"
v-if="dataForm.orderType!== 1"
title="卖家已发货"
:description="dataForm.dvyTime"
/>
<!-- 根据订单类型判断是否展示买家已收货步骤及其时间描述 -->
<el-step
v-if="dataForm.orderType !== 1"
v-if="dataForm.orderType!== 1"
title="买家已收货"
:description="dataForm.finallyTime"
/>
@ -49,6 +57,7 @@
<div class="order-state">
<div class="state-cont">
<div class="state-title">
<!-- 展示订单状态的el-form-item通过不同条件判断显示不同状态的标签 -->
<el-form-item label="订单状态:">
<template #default>
<el-tag
@ -58,19 +67,19 @@
待付款
</el-tag>
<el-tag
v-if="dataForm.status === 2 && dataForm.orderType !== 1"
v-if="dataForm.status === 2 && dataForm.orderType!== 1"
type="warning"
>
待发货
</el-tag>
<el-tag
v-if="dataForm.status === 3 && dataForm.orderType !== 1"
v-if="dataForm.status === 3 && dataForm.orderType!== 1"
type="warning"
>
待收货
</el-tag>
<el-tag
v-if="dataForm.status === 4 && dataForm.orderType !== 1"
v-if="dataForm.status === 4 && dataForm.orderType!== 1"
type="warning"
>
待评价
@ -91,8 +100,9 @@
</el-form-item>
<el-form-item>
<el-row>
<!-- 根据订单状态和类型判断是否显示发货按钮点击可触发changeOrder方法 -->
<el-button
v-if="dataForm.status === 2 && dataForm.orderType !== 1"
v-if="dataForm.status === 2 && dataForm.orderType!== 1"
type="primary"
plain
@click="changeOrder(dataForm.orderNumber)"
@ -118,6 +128,7 @@
alt=""
>
<div class="text-width">
<!-- 展示收货人相关信息的el-form-item -->
<el-form-item label="收货人:">
<span>{{ dataForm.userAddrOrder.receiver }}</span>
</el-form-item>
@ -169,6 +180,7 @@
</div>
</div>
<div class="item-list">
<!-- el-table组件用于展示商品列表相关信息如商品名称价格数量总价等 -->
<el-table
:data="dataForm.orderItems"
border
@ -220,6 +232,7 @@
</el-table>
</div>
<div class="item-info">
<!-- 展示商品总价的el-form-item -->
<el-form-item label="商品总价:">
<span class="text">{{ dataForm.total }}</span>
</el-form-item>
@ -240,6 +253,7 @@
<span>订单日志</span>
</div>
<div class="log-cont">
<!-- 根据不同的订单时间相关属性展示对应的订单日志信息 -->
<el-form-item
v-if="dataForm.orderTime"
label-width="10px"
@ -282,7 +296,7 @@
</div>
</el-form>
<!-- 弹窗, 新增 / 修改 -->
<!-- 弹窗组件用于新增或修改相关操作当devyVisible为真时显示可通过@refresh-data-list监听事件刷新数据列表 -->
<devy-add
v-if="devyVisible"
ref="devyAddRef"
@ -293,55 +307,74 @@
<script setup>
import DevyAdd from './order-devy.vue'
import { ref, computed, watch, nextTick } from 'vue'
import http from '@/api/http' // HTTP
import { ElMessage } from 'element-plus'
// URL
const resourcesUrl = import.meta.env.VITE_APP_RESOURCES_URL
//
const dataForm = ref({
orderId: 0,
orderNumber: '',
remarks: '',
total: 0,
actualTotal: 0,
dvyType: '',
status: 1,
addrOrderId: 0,
nickName: '',
orderItems: [],
orderTime: '',
updateTime: '',
payTime: '',
dvyTime: '',
finallyTime: '',
cancelTime: '',
userAddrOrder: {}
orderId: 0, // ID
orderNumber: '', //
remarks: '', //
total: 0, //
actualTotal: 0, //
dvyType: '', //
status: 1, //
addrOrderId: 0, // ID
nickName: '', //
orderItems: [], //
orderTime: '', //
updateTime: '', //
payTime: '', //
dvyTime: '', //
finallyTime: '', //
cancelTime: '', //
userAddrOrder: {} //
})
//
const stepsStatus = computed(() => {
if (dataForm.value.finallyTime) {
return 4
return 4 //
}
if (dataForm.value.dvyTime) {
return 3
return 3 //
}
if (dataForm.value.payTime) {
return 2
return 2 //
}
if (dataForm.value.orderTime) {
return 1
return 1 //
}
return ''
return '' //
})
//
const visible = ref(false)
//
const devyVisible = ref(false)
// `visible`
watch(
() => visible.value,
() => {
if (!visible.value) {
(newValue) => {
if (!newValue) {
devyVisible.value = false
}
}
)
//
const dataFormRef = ref(null)
/**
* 初始化方法打开对话框并加载订单信息
* @param {String|Number} orderNumber - 订单编号
*/
const init = (orderNumber) => {
dataForm.value.orderNumber = orderNumber || 0
visible.value = true
@ -349,34 +382,37 @@ const init = (orderNumber) => {
dataFormRef.value?.resetFields()
})
if (dataForm.value.orderNumber) {
//
//
http({
url: http.adornUrl(`/order/order/orderInfo/${dataForm.value.orderNumber}`),
method: 'get',
params: http.adornParams()
}).then(({ data }) => {
dataForm.value = data
})
.then(({ data }) => {
dataForm.value = data
})
}
}
defineExpose({ init })
defineExpose({ init }) // init
/**
* 获取订单信息的方法
*/
const getDataList = () => {
http({
url: http.adornUrl(`/order/order/orderInfo/${dataForm.value.orderNumber}`),
method: 'get',
params: http.adornParams()
}).then(({ data }) => {
dataForm.value = data
})
.then(({ data }) => {
dataForm.value = data
})
}
//
const devyAddRef = ref(null)
/**
* 发货
* @param orderNumber
* 发货操作打开发货对话框并初始化数据
* @param {String|Number} orderNumber - 订单编号
*/
const changeOrder = (orderNumber) => {
devyVisible.value = true
@ -387,133 +423,172 @@ const changeOrder = (orderNumber) => {
</script>
<style scoped lang="scss">
//
.main {
height: 100%;
width: 100%;
font: 14px Arial, "PingFang SC", "Hiragino Sans GB", STHeiti, "Microsoft YaHei", "WenQuanYi Micro Hei", sans-serif;
color: #495060;
height: 100%; // 100%
width: 100%; // 100%
font: 14px Arial, "PingFang SC", "Hiragino Sans GB", STHeiti, "Microsoft YaHei", "WenQuanYi Micro Hei", sans-serif; //
color: #495060; //
}
//
.content {
margin: 0 20px;
margin: 0 20px; // 20px
//
.order-state {
position: relative;
margin-top: 50px;
border-bottom: 1px solid #e9eaec;
position: relative; //
margin-top: 50px; // 50px
border-bottom: 1px solid #e9eaec; //
}
}
//
.order-number {
.text {
font-size: 14px;
color: #8a8a8a;
font-size: 14px; //
color: #8a8a8a; //
}
}
//
.order-state {
//
.state-title {
width: 100%;
display: flex;
justify-content: space-between;
align-items: center;
width: 100%; // 100%
display: flex; // 使
justify-content: space-between; //
align-items: center; //
}
//
.order-info {
width: 100%;
border-top: 1px solid #e9eaec;
margin: 50px 0;
display: flex;
width: 100%; // 100%
border-top: 1px solid #e9eaec; //
margin: 50px 0; // 50px
display: flex; // 使
}
//
.item-info {
padding-left: 80%;
margin: 25px 0;
padding-left: 80%; // 80%
margin: 25px 0; // 25px
}
}
//
.order-info {
img {
width: 18px !important;
height: 18px !important;
margin-right: 15px;
width: 18px !important; // 18px
height: 18px !important; // 18px
margin-right: 15px; // 15px
}
//
.detail-title {
height: 50px;
line-height: 50px;
display: flex;
align-items: center;
height: 50px; // 50px
line-height: 50px; // 50px
display: flex; // 使
align-items: center; //
}
//
.order-details {
width: 50%;
border-right: 1px solid #e9eaec;
width: 50%; // 50%
border-right: 1px solid #e9eaec; //
}
//
.detail-cont {
position: relative;
border-top: 1px dashed #e9eaec;
margin: 15px 20px 0 0;
position: relative; //
border-top: 1px dashed #e9eaec; // 线
margin: 15px 20px 0 0; //
}
//
.buyers {
width: 50%;
margin-left: 20px;
width: 50%; // 50%
margin-left: 20px; // 20px
}
}
//
.detail-cont {
.detail01 {
display: flex;
height: 100%;
line-height: 25px;
margin-top: 15px;
}
}
.detail01 {
.text-width {
width: 100%;
display: flex; // 使
height: 100%; // 100%
line-height: 25px; // 25px
margin-top: 15px; // 15px
}
}
//
.detail01,
.detail02 {
.text-width {
width: 100%;
width: 100%; // 100%
}
}
//
.buyers {
.buyers-info {
border-top: 1px dashed #e9eaec;
margin-top: 15px;
position: relative;
border-top: 1px dashed #e9eaec; // 线
margin-top: 15px; // 15px
position: relative; //
}
.detail02 {
display: flex;
height: 100%;
line-height: 25px;
margin-top: 15px;
display: flex; // 使
height: 100%; // 100%
line-height: 25px; // 25px
margin-top: 15px; // 15px
}
}
//
.item-info {
span {
margin-bottom: 15px;
line-height: 30px;
margin-bottom: 15px; // 15px
line-height: 30px; // 30px
}
.text {
position: absolute;
right: 0;
position: absolute; //
right: 0; //
}
}
//
.order-log {
.log-title {
height: 50px;
width: 100%;
line-height: 50px;
font-weight: bold;
height: 50px; // 50px
width: 100%; // 100%
line-height: 50px; // 50px
font-weight: bold; //
}
.log-cont {
color: #4395ff;
color: #4395ff; //
}
}
//
.item-list {
.prod-con {
display: flex;
display: flex; // 使
//
.prod-img {
width: 100px;
height: 100px;
margin-right: 8px;
width: 100px; // 100px
height: 100px; // 100px
margin-right: 8px; // 8px
}
}
}
//
:deep(.el-steps--horizontal) {
flex: 25% 1 1;
flex: 25% 1 1; // flex
}
</style>

@ -1,17 +1,14 @@
<template>
<!-- 定义一个订单模块的容器 -->
<div class="mod-order-order">
<el-form
:inline="true"
:model="dataForm"
@keyup.enter="getDataList(page)"
>
<!-- 表单区域用于输入筛选条件 -->
<el-form :inline="true" :model="dataForm" @keyup.enter="getDataList(page)">
<!-- 输入框订单编号 -->
<el-form-item label="订单编号:">
<el-input
v-model="dataForm.orderNumber"
placeholder="订单编号"
clearable
/>
<el-input v-model="dataForm.orderNumber" placeholder="订单编号" clearable />
</el-form-item>
<!-- 时间选择器下单时间范围 -->
<el-form-item label="下单时间:">
<el-date-picker
v-model="dateRange"
@ -22,95 +19,59 @@
end-placeholder="结束日期"
/>
</el-form-item>
<!-- 下拉菜单订单状态 -->
<el-form-item label="订单状态:">
<el-select
v-model="dataForm.status"
clearable
placeholder="请选择订单状态"
>
<el-option
v-for="item in options"
:key="item.value"
:label="item.label"
:value="item.value"
/>
<el-select v-model="dataForm.status" clearable placeholder="请选择订单状态">
<el-option v-for="item in options" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</el-form-item>
<!-- 按钮组查询导出清空 -->
<el-form-item>
<el-button
type="primary"
icon="el-icon-search"
@click="getDataList()"
>
查询
</el-button>
<el-button
v-if="isAuth('order:order:waitingConsignmentExcel')"
type="primary"
@click="showConsignmentInfo()"
>
<el-button type="primary" icon="el-icon-search" @click="getDataList()"></el-button>
<el-button v-if="isAuth('order:order:waitingConsignmentExcel')" type="primary" @click="showConsignmentInfo()">
导出待发货订单
</el-button>
<el-button
v-if="isAuth('order:order:soldExcel')"
type="primary"
@click="getSoldExcel()"
>
<el-button v-if="isAuth('order:order:soldExcel')" type="primary" @click="getSoldExcel()">
导出销售记录
</el-button>
<el-button @click="clearDatas()">
清空
</el-button>
<el-button @click="clearDatas()"></el-button>
</el-form-item>
</el-form>
<!-- 主要内容区 -->
<div class="main">
<div class="content">
<!-- 标题栏显示商品信息头部 -->
<div class="tit">
<el-row style="width:100%">
<el-col :span="10">
<span class="item product">商品</span>
</el-col>
<el-col :span="3">
<span class="item">成交单价/购买数量</span>
</el-col>
<el-col :span="3">
<span class="item">实付金额</span>
</el-col>
<el-col :span="3">
<span class="item">支付方式</span>
</el-col>
<el-col :span="3">
<span class="item">订单状态</span>
</el-col>
<el-col :span="2">
<span class="item">操作</span>
</el-col>
<el-col :span="10"><span class="item product">商品</span></el-col>
<el-col :span="3"><span class="item">成交单价/购买数量</span></el-col>
<el-col :span="3"><span class="item">实付金额</span></el-col>
<el-col :span="3"><span class="item">支付方式</span></el-col>
<el-col :span="3"><span class="item">订单状态</span></el-col>
<el-col :span="2"><span class="item">操作</span></el-col>
</el-row>
</div>
<div
v-for="order in dataList"
:key="order.orderId"
class="prod"
>
<!-- 订单列表 -->
<div v-for="order in dataList" :key="order.orderId" class="prod">
<!-- 订单标题 -->
<div class="prod-tit">
<span>订单编号{{ order.orderNumber }}</span>
<span>下单时间{{ order.createTime }}</span>
</div>
<!-- 订单内容 -->
<div class="prod-cont">
<el-row style="width:100%">
<!-- 商品信息 -->
<el-col :span="12">
<div class="prod-item">
<div
v-for="orderItem in order.orderItems"
:key="orderItem.orderItemId"
class="items name"
>
<div v-for="orderItem in order.orderItems" :key="orderItem.orderItemId" class="items name">
<div class="prod-image">
<img
alt=""
:src="resourcesUrl + orderItem.pic"
style="height:100px;width: 100px;"
>
<img alt="" :src="resourcesUrl + orderItem.pic" style="height:100px;width: 100px;" />
</div>
<div class="prod-name">
<span>{{ orderItem.prodName }}</span>
@ -123,10 +84,9 @@
</div>
</div>
</el-col>
<el-col
:span="3"
style="height: 100%;"
>
<!-- 实付金额 -->
<el-col :span="3" style="height: 100%;">
<div class="item">
<div>
<span class="totalprice">{{ order.actualTotal }}</span>
@ -135,10 +95,9 @@
</div>
</div>
</el-col>
<el-col
:span="3"
style="height: 100%;"
>
<!-- 支付方式 -->
<el-col :span="3" style="height: 100%;">
<div class="item">
<div>
<span v-if="order.payType === 1"></span>
@ -147,47 +106,24 @@
</div>
</div>
</el-col>
<el-col
:span="3"
style="height: 100%;"
>
<!-- 订单状态 -->
<el-col :span="3" style="height: 100%;">
<div class="item">
<span
v-if="order.status === 1"
type="danger"
>待付款</span>
<span
v-else-if="order.status === 2"
type="danger"
>待发货</span>
<span
v-else-if="order.status === 3"
type="danger"
>待收货</span>
<span
v-else-if="order.status === 4"
type="danger"
>待评价</span>
<span
v-else-if="order.status === 5"
type="danger"
>成功</span>
<span
v-else
>失败</span>
<span v-if="order.status === 1" type="danger"></span>
<span v-else-if="order.status === 2" type="danger">待发货</span>
<span v-else-if="order.status === 3" type="danger">待收货</span>
<span v-else-if="order.status === 4" type="danger">待评价</span>
<span v-else-if="order.status === 5" type="danger">成功</span>
<span v-else></span>
</div>
</el-col>
<el-col
:span="3"
style="height: 100%;"
>
<!-- 操作按钮 -->
<el-col :span="3" style="height: 100%;">
<div class="item">
<div class="operate">
<el-button
v-if="isAuth('order:order:update')"
type="text"
@click="onAddOrUpdate(order.orderNumber)"
>
<el-button v-if="isAuth('order:order:update')" type="text" @click="onAddOrUpdate(order.orderNumber)">
查看
</el-button>
</div>
@ -195,6 +131,8 @@
</el-col>
</el-row>
</div>
<!-- 备注信息 -->
<div class="remark">
<div class="buyer-remark">
<span>备注:{{ order.remarks }}</span>
@ -203,15 +141,15 @@
</div>
</div>
</div>
<!-- -->
<div
v-if="!dataList.length"
class="empty-tips"
>
<!-- 当没有数据时显示的提示信息 -->
<div v-if="!dataList.length" class="empty-tips">
暂无数据
</div>
<!-- 分页组件 -->
<el-pagination
:current-page="page.pageIndex"
:current-page="page.currentPage"
:page-sizes="[10, 20, 50, 100]"
:page-size="page.pageSize"
:total="page.total"
@ -219,63 +157,73 @@
@size-change="sizeChangeHandle"
@current-change="currentChangeHandle"
/>
<!-- 弹窗, 新增 / 修改 -->
<add-or-update
v-if="addOrUpdateVisible"
ref="addOrUpdateRef"
@refresh-data-list="getDataList"
/>
<consignment-info
v-if="consignmentInfoVisible"
ref="consignmentInfoRef"
@input-callback="getWaitingConsignmentExcel"
/>
<!-- 弹窗新增或修改订单 -->
<add-or-update v-if="addOrUpdateVisible" ref="addOrUpdateRef" @refresh-data-list="getDataList" />
<!-- 弹窗查看发货信息 -->
<consignment-info v-if="consignmentInfoVisible" ref="consignmentInfoRef" @input-callback="getWaitingConsignmentExcel" />
</div>
</template>
<script setup>
//
import AddOrUpdate from './components/order-info.vue'
import ConsignmentInfo from './components/consignment-info.vue'
//
import { isAuth } from '@/utils'
// URL
const resourcesUrl = import.meta.env.VITE_APP_RESOURCES_URL
//
const dataForm = ref({})
//
const dateRange = ref([])
//
const options = [{
value: 1,
label: '待付款'
},
{
}, {
value: 2,
label: '待发货'
},
{
}, {
value: 3,
label: '待收货'
},
{
}, {
value: 4,
label: '待评价'
},
{
}, {
value: 5,
label: '成功'
},
{
}, {
value: 6,
label: '失败'
}]
//
const dataList = ref([])
//
const page = reactive({
total: 0, //
currentPage: 1, //
pageSize: 10 //
})
//
onMounted(() => {
getDataList(page)
})
/**
* 获取数据列表
* 发起HTTP请求获取订单数据列表并更新页面上的数据显示
* @param pageParam - 分页参数默认使用全局page对象
* @param params - 额外的查询参数
* @param done - 回调函数在获取数据完成后调用
*/
const getDataList = (pageParam, params, done) => {
pageParam = (pageParam === undefined ? page : pageParam)
@ -302,36 +250,42 @@ const getDataList = (pageParam, params, done) => {
if (done) done()
})
}
/**
* 清除数据
* 清除所有表单数据和日期范围选择
*/
const clearDatas = () => {
dataForm.value = {}
dateRange.value = []
}
/**
* 每页数
* @param val
* 当分页组件中每页显示量改变时触发此方法
* @param val - 新的每页显示数量
*/
const sizeChangeHandle = (val) => {
page.pageSize = val
page.currentPage = 1
getDataList(page)
}
/**
* 前页
* @param val
* 分页组件中当前页码改变时触发此方法
* @param val - 新的当前页码
*/
const currentChangeHandle = (val) => {
page.currentPage = val
getDataList(page)
}
// AddOrUpdate
const addOrUpdateRef = ref(null)
// AddOrUpdate
const addOrUpdateVisible = ref(false)
/**
* 新增 / 修改
* @param val
* 打开订单信息编辑或新增弹窗
* @param val - 可选参数当存在时表示编辑指定ID的订单
*/
const onAddOrUpdate = (val) => {
addOrUpdateVisible.value = true
@ -340,14 +294,25 @@ const onAddOrUpdate = (val) => {
})
}
// ConsignmentInfo
const consignmentInfoRef = ref(null)
// ConsignmentInfo
const consignmentInfoVisible = ref(false)
/**
* 显示发货信息弹窗
*/
const showConsignmentInfo = () => {
consignmentInfoVisible.value = true
nextTick(() => {
consignmentInfoRef.value?.init()
})
}
/**
* 下载待发货信息整理Excel文件
* @param consignmentInfo - 发货信息对象
*/
const getWaitingConsignmentExcel = (consignmentInfo) => {
http({
url: http.adornUrl('/order/order/waitingConsignmentExcel'),
@ -356,78 +321,83 @@ const getWaitingConsignmentExcel = (consignmentInfo) => {
consignmentName: consignmentInfo.consignmentName,
consignmentMobile: consignmentInfo.consignmentMobile,
consignmentAddr: consignmentInfo.consignmentAddr,
startTime: dateRange.value === null ? null : dateRange.value[0], //
endTime: dateRange.value === null ? null : dateRange.value[1] //
startTime: dateRange.value === null ? null : dateRange.value[0],
endTime: dateRange.value === null ? null : dateRange.value[1]
}),
responseType: 'blob' //
responseType: 'blob'
})
.then(({ data }) => {
const blob = new Blob([data], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=utf-8' })
const fileName = '待发货信息整理.xls'
const elink = document.createElement('a')
if ('download' in elink) { // IE
elink.download = fileName
elink.style.display = 'none'
elink.href = URL.createObjectURL(blob)
document.body.appendChild(elink)
elink.click()
URL.revokeObjectURL(elink.href) // URL
document.body.removeChild(elink)
} else { // IE10+
navigator.msSaveBlob(blob, fileName)
}
downloadFile(blob, fileName)
})
}
/**
* 下载销售信息整理Excel文件
*/
const getSoldExcel = () => {
http({
url: http.adornUrl('/order/order/soldExcel'),
method: 'get',
params: http.adornParams({
startTime: dateRange.value === null ? null : dateRange.value[0], //
endTime: dateRange.value === null ? null : dateRange.value[1] //
startTime: dateRange.value === null ? null : dateRange.value[0],
endTime: dateRange.value === null ? null : dateRange.value[1]
}),
responseType: 'blob' //
responseType: 'blob'
})
.then(({ data }) => {
const blob = new Blob([data], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=utf-8' })
const fileName = '销售信息整理.xls'
const elink = document.createElement('a')
if ('download' in elink) { // IE
elink.download = fileName
elink.style.display = 'none'
elink.href = URL.createObjectURL(blob)
document.body.appendChild(elink)
elink.click()
URL.revokeObjectURL(elink.href) // URL
document.body.removeChild(elink)
} else { // IE10+
navigator.msSaveBlob(blob, fileName)
}
downloadFile(blob, fileName)
})
}
/**
* 文件下载辅助函数
* @param blob - 文件内容
* @param fileName - 文件名
*/
function downloadFile(blob, fileName) {
const elink = document.createElement('a')
if ('download' in elink) { // IE
elink.download = fileName
elink.style.display = 'none'
elink.href = URL.createObjectURL(blob)
document.body.appendChild(elink)
elink.click()
URL.revokeObjectURL(elink.href)
document.body.removeChild(elink)
} else { // IE10+
navigator.msSaveBlob(blob, fileName)
}
}
</script>
<style lang="scss" scoped>
// 使scoped
.mod-order-order {
// flex45px
.tit {
display: flex;
height: 45px;
align-items: center;
}
.tit {
// 10px10%
.item {
padding: 0 10px;
width: 10%;
text-align: center;
}
// 25%
.product {
width: 25%;
}
}
//
.prod-tit {
padding: 10px;
background: #f8f8f9;
@ -435,11 +405,13 @@ const getSoldExcel = () => {
border-top: 1px solid #dddee1;
border-right: 1px solid #dddee1;
// span15px
span {
margin-right: 15px;
}
}
// flex
.prod-cont {
display: flex;
border-top: 1px solid #dddee1;
@ -447,6 +419,7 @@ const getSoldExcel = () => {
border-left: 1px solid #dddee1;
color: #495060;
// flex
.item {
display: flex;
display: -webkit-flex;
@ -457,33 +430,39 @@ const getSoldExcel = () => {
text-align: center;
height: 100%;
// span
span {
display: block;
}
}
// 使flex
.prod-item {
display: flex;
flex-direction: column;
border-right: 1px solid #dddee1;
}
//
.items.name {
display: flex;
position: relative;
padding: 20px;
border-bottom: 1px solid #dddee1;
//
&:last-child {
border-bottom: none;
}
}
}
// 55%
.prod-name {
width: 55%;
text-align: left;
// 30px
.prod-info {
display: block;
color: #80848f;
@ -491,17 +470,20 @@ const getSoldExcel = () => {
}
}
// 40px
.prod-price {
position: absolute;
right: 40px;
text-align: right;
// span10px
span {
display: block;
margin-bottom: 10px;
}
}
//
.prod-image {
margin-right: 20px;
width: 100px;
@ -513,21 +495,25 @@ const getSoldExcel = () => {
}
}
// span10px
.item {
span {
display: block;
margin-bottom: 10px;
}
//
.operate {
color: #2d8cf0;
}
//
.totalprice {
color: #c00;
}
}
//
.prod {
.remark {
width: 100%;
@ -541,6 +527,7 @@ const getSoldExcel = () => {
}
}
//
.buyer-remark {
padding: 0 20px;
overflow: hidden;
@ -548,6 +535,7 @@ const getSoldExcel = () => {
text-overflow: ellipsis;
}
// 50px
.empty-tips {
display: block;
width: 100%;

@ -1,97 +1,106 @@
<template>
<!-- 使用el-dialog组件创建一个对话框v-model绑定visible控制显示/隐藏 -->
<el-dialog
v-model="visible"
:title="!dataForm.currentId ? '新增' : '修改'"
:close-on-click-modal="false"
:title="!dataForm.currentId ? '新增' : '修改'" <!-- 根据currentId判断是新增还是修改 -->
:close-on-click-modal="false" <!-- 点击模态框区域不关闭对话框 -->
>
<el-form
ref="dataFormRef"
:model="dataForm"
:rules="dataRule"
label-width="80px"
@keyup.enter="onSubmit()"
>
<el-form-item
v-if="dataForm.type !== 2"
label="分类图片"
prop="pic"
>
<pic-upload
v-model="dataForm.pic"
@update:model-value="checkPic"
/>
</el-form-item>
<el-form-item
v-if="dataForm.type !== 2"
label="分类名称"
prop="categoryName"
>
<el-input
v-model="dataForm.categoryName"
controls-position="right"
:min="0"
label="分类名称"
/>
</el-form-item>
<el-form-item label="上级分类">
<el-cascader
v-model="selectedCategory"
expand-trigger="hover"
:options="categoryList"
:props="categoryTreeProps"
change-on-select
:clearable="true"
@change="handleChange"
/>
</el-form-item>
<el-form-item
v-if="dataForm.type !== 2"
label="排序号"
prop="seq"
>
<el-input-number
v-model="dataForm.seq"
controls-position="right"
:min="0"
label="排序号"
/>
</el-form-item>
<el-form-item
label="状态"
prop="status"
>
<el-radio-group v-model="dataForm.status">
<el-radio :label="0">
下线
</el-radio>
<el-radio :label="1">
正常
</el-radio>
</el-radio-group>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="visible = false">
取消
</el-button>
<el-button
type="primary"
@click="onSubmit()"
>
确定
</el-button>
</div>
</template>
<!-- el-form表单ref引用dataFormRef以便在脚本中操作表单 -->
<el-form
ref="dataFormRef"
:model="dataForm"
:rules="dataRule"
label-width="80px"
@keyup.enter="onSubmit()" <!-- 按下Enter键时触发提交 -->
>
<!-- 分类图片表单项仅当type不是2时显示 -->
<el-form-item
v-if="dataForm.type !== 2"
label="分类图片"
prop="pic"
>
<!-- 自定义的图片上传组件 -->
<pic-upload
v-model="dataForm.pic"
@update:model-value="checkPic" <!-- 图片更新后重新校验 -->
/>
</el-form-item>
<!-- 分类名称表单项仅当type不是2时显示 -->
<el-form-item
v-if="dataForm.type !== 2"
label="分类名称"
prop="categoryName"
>
<el-input
v-model="dataForm.categoryName"
controls-position="right"
:min="0"
label="分类名称"
/>
</el-form-item>
<!-- 上级分类选择器 -->
<el-form-item label="上级分类">
<el-cascader
v-model="selectedCategory"
expand-trigger="hover"
:options="categoryList"
:props="categoryTreeProps"
change-on-select
:clearable="true"
@change="handleChange" <!-- 上级分类变化时触发 -->
/>
</el-form-item>
<!-- 排序号表单项仅当type不是2时显示 -->
<el-form-item
v-if="dataForm.type !== 2"
label="排序号"
prop="seq"
>
<el-input-number
v-model="dataForm.seq"
controls-position="right"
:min="0"
label="排序号"
/>
</el-form-item>
<!-- 状态选择项 -->
<el-form-item
label="状态"
prop="status"
>
<el-radio-group v-model="dataForm.status">
<el-radio :label="0">下线</el-radio>
<el-radio :label="1">正常</el-radio>
</el-radio-group>
</el-form-item>
</el-form>
<!-- 对话框底部的操作按钮 -->
<template #footer>
<div class="dialog-footer">
<el-button @click="visible = false">取消</el-button> <!-- -->
<el-button type="primary" @click="onSubmit()"></el-button> <!-- 点击确定提交表单 -->
</div>
</template>
</el-dialog>
</template>
<script setup>
import { ElMessage } from 'element-plus'
import { treeDataTranslate, idList } from '@/utils'
import { Debounce } from '@/utils/debounce'
import {ElMessage} from 'element-plus'
import {treeDataTranslate, idList} from '@/utils'
import {Debounce} from '@/utils/debounce'
// emit
const emit = defineEmits(['refreshDataList'])
//
const visible = ref(false)
// 使reactive
const dataForm = reactive({
categoryId: 0,
currentId: 0,
@ -102,108 +111,109 @@ const dataForm = reactive({
parentId: 0,
pic: ''
})
//
const dataRule = reactive({
categoryName: [
{ required: true, message: '分类名称不能为空', trigger: 'blur' },
{ pattern: /\s\S+|S+\s|\S/, message: '请输入正确的分类名称', trigger: 'blur' }
{required: true, message: '分类名称不能为空', trigger: 'blur'},
{pattern: /\s\S+|S+\s|\S/, message: '请输入正确的分类名称', trigger: 'blur'}
],
pic: [
{ required: true, message: '分类图片不能为空', trigger: 'blur' }
{required: true, message: '分类图片不能为空', trigger: 'blur'}
]
})
//
const categoryList = ref([])
//
const selectedCategory = ref([])
//
const categoryTreeProps = reactive({
value: 'categoryId',
label: 'categoryName',
checkStrictly: true
})
//
const isSubmit = ref(false)
//
const dataFormRef = ref(null)
// id
const init = (id) => {
dataForm.currentId = id || 0
dataForm.categoryId = id || 0
//
http({
url: http.adornUrl('/prod/category/listCategory'),
method: 'get',
params: http.adornParams()
})
.then(({ data }) => {
.then(({data}) => {
categoryList.value = treeDataTranslate(data, 'categoryId', 'parentId')
})
.then(() => {
visible.value = true
nextTick(() => {
dataFormRef.value?.resetFields()
dataFormRef.value?.resetFields() //
selectedCategory.value = []
})
})
.then(() => {
if (dataForm.categoryId) {
//
// ID
http({
url: http.adornUrl(`/prod/category/info/${dataForm.categoryId}`),
method: 'get',
params: http.adornParams()
})
.then(({ data }) => {
dataForm.categoryId = data.categoryId
dataForm.categoryName = data.categoryName
dataForm.seq = data.seq
dataForm.pic = data.pic
dataForm.parentId = data.parentId
dataForm.status = data.status
.then(({data}) => {
Object.assign(dataForm, data)
selectedCategory.value = idList(categoryList.value, data.parentId, 'categoryId', 'children').reverse()
})
}
})
}
defineExpose({ init })
// init
defineExpose({init})
//
const handleChange = (val) => {
dataForm.parentId = val[val.length - 1]
}
//
// 使
const onSubmit = Debounce(() => {
if (selectedCategory.value.length === 1) {
dataForm.grade = 0
}
if (selectedCategory.value.length === 2) {
dataForm.grade = 1
}
if (selectedCategory.value.length === 3) {
dataForm.grade = 2
}
// grade
dataForm.grade = selectedCategory.value.length - 1
//
dataFormRef.value?.validate((valid) => {
if (valid) {
if (isSubmit.value) {
return
}
if (isSubmit.value) return //
isSubmit.value = true
//
http({
url: http.adornUrl('/prod/category'),
method: dataForm.categoryId ? 'put' : 'post',
data: http.adornData({
categoryId: dataForm.categoryId || undefined,
categoryName: dataForm.categoryName,
status: dataForm.status,
seq: dataForm.seq,
grade: dataForm.grade,
parentId: dataForm.parentId,
pic: dataForm.pic
...dataForm
})
})
.then(() => {
ElMessage({
message: '操作成功',
type: 'success',
duration: 1000,
onClose: () => {
isSubmit.value = false
visible.value = false
emit('refreshDataList')
}
})
ElMessage.success('操作成功')
isSubmit.value = false
visible.value = false
emit('refreshDataList') //
})
.catch(() => {
isSubmit.value = false
})
}
})

@ -1,10 +1,10 @@
<template>
<!-- 分类管理模块 -->
<div class="mod-category">
<el-form
:inline="true"
:model="dataForm"
>
<!-- 搜索表单使用inline布局让表单项在同一行显示 -->
<el-form :inline="true" :model="dataForm">
<el-form-item>
<!-- 新增按钮只有拥有相应权限的用户可以点击 -->
<el-button
v-if="isAuth('prod:category:save')"
type="primary"
@ -15,12 +15,10 @@
</el-button>
</el-form-item>
</el-form>
<el-table
:data="dataList"
border
row-key="categoryId"
style="width: 100%;"
>
<!-- 数据表格展示分类列表 -->
<el-table :data="dataList" border row-key="categoryId" style="width: 100%;">
<!-- 分类名称列 -->
<el-table-column
prop="categoryName"
header-align="center"
@ -28,6 +26,8 @@
width="150"
label="分类名称"
/>
<!-- 图片列自定义模板显示图片 -->
<el-table-column
prop="pic"
header-align="center"
@ -35,12 +35,11 @@
label="图片"
>
<template #default="scope">
<img
alt=""
:src="checkFileUrl(scope.row.pic)"
>
<img alt="" :src="checkFileUrl(scope.row.pic)" style="height:80px;width:200px;">
</template>
</el-table-column>
<!-- 状态列根据状态值显示不同颜色的标签 -->
<el-table-column
prop="status"
header-align="center"
@ -48,29 +47,27 @@
label="状态"
>
<template #default="scope">
<el-tag
v-if="scope.row.status === 0"
type="danger"
>
下线
</el-tag>
<el-tag v-else>
正常
</el-tag>
<el-tag v-if="scope.row.status === 0" type="danger">线</el-tag>
<el-tag v-else></el-tag>
</template>
</el-table-column>
<!-- 排序号列 -->
<el-table-column
prop="seq"
header-align="center"
align="center"
label="排序号"
/>
<!-- 操作列提供修改和删除按钮 -->
<el-table-column
header-align="center"
align="center"
label="操作"
>
<template #default="scope">
<!-- 修改按钮只有拥有相应权限的用户可以点击 -->
<el-button
v-if="isAuth('prod:category:update')"
type="primary"
@ -78,6 +75,7 @@
>
修改
</el-button>
<!-- 删除按钮只有拥有相应权限的用户可以点击 -->
<el-button
v-if="isAuth('prod:category:delete')"
type="danger"
@ -88,7 +86,8 @@
</template>
</el-table-column>
</el-table>
<!-- 弹窗, 新增 / 修改 -->
<!-- 弹窗, 新增 / 修改通过addOrUpdateVisible控制显示隐藏 -->
<add-or-update
v-if="addOrUpdateVisible"
ref="addOrUpdateRef"
@ -102,53 +101,64 @@
import { checkFileUrl, isAuth, treeDataTranslate } from '@/utils'
import { ElMessage, ElMessageBox } from 'element-plus'
import AddOrUpdate from './add-or-update.vue'
//
const dataForm = ref({})
//
onMounted(() => {
getDataList()
})
//
const dataList = ref([])
const dataListLoading = ref(false)
/**
* 获取数据列表
* 获取数据列表方法
*/
const getDataList = () => {
dataListLoading.value = true
dataListLoading.value = true // true
http({
url: http.adornUrl('/prod/category/table'),
method: 'get',
params: http.adornParams()
})
.then(({ data }) => {
// dataList
dataList.value = treeDataTranslate(data, 'categoryId', 'parentId')
dataListLoading.value = false
dataListLoading.value = false // false
})
}
//
const addOrUpdateVisible = ref(false)
const addOrUpdateRef = ref(null)
/**
* 新增 / 修改
* @param id
* 新增或修改分类
* @param id - 如果提供id则为修改否则为新增
*/
const onAddOrUpdate = (id) => {
addOrUpdateVisible.value = true
addOrUpdateVisible.value = true //
nextTick(() => {
addOrUpdateRef.value?.init(id)
addOrUpdateRef.value?.init(id) //
})
}
/**
* 删除
* @param id
* 删除分类
* @param id - 要删除的分类ID
*/
const onDelete = (id) => {
//
ElMessageBox.confirm('确定进行删除操作?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
.then(() => {
// HTTP DELETE
http({
url: http.adornUrl(`/prod/category/${id}`),
method: 'delete'
@ -159,20 +169,19 @@ const onDelete = (id) => {
type: 'success',
duration: 1500,
onClose: () => {
getDataList()
getDataList() //
}
})
})
})
}
</script>
<style lang="scss" scoped>
.mod-category {
img {
height: 80px;
width: 200px;
width: 200px; //
}
}
</style>

@ -1,156 +1,130 @@
<template>
<!-- 商品评论管理对话框 -->
<el-dialog
v-model="visible"
:title="!dataForm.prodCommId ? '新增' : '修改'"
:close-on-click-modal="false"
:close-on-click-modal="false"
>
<el-form
ref="dataFormRef"
:model="dataForm"
:rules="dataRule"
label-width="80px"
@keyup.enter="onSubmit()"
>
<div v-if="!isEdit">
<el-form-item
label="评论内容"
prop="userName"
>
<el-input
v-model="dataForm.content"
type="textarea"
:readonly="true"
/>
</el-form-item>
<el-form-item
label="评论图片"
prop="userName"
>
<div v-if="!dataForm.pics?.length">
</div>
<div v-else>
<img
v-for="item in dataForm.pics"
:key="item"
alt=""
max-width="100%"
:src="dialogImageUrl + item"
>
</div>
</el-form-item>
<el-form-item
label="记录时间"
prop="userName"
>
<el-input
v-model="dataForm.recTime"
:readonly="true"
/>
</el-form-item>
<el-form-item
label="回复时间"
prop="userName"
:readonly="true"
>
<el-input
v-model="dataForm.replyTime"
:readonly="true"
/>
</el-form-item>
<el-form-item
label="IP来源"
prop="userName"
>
<el-input
v-model="dataForm.postip"
:readonly="true"
/>
</el-form-item>
<el-form-item
label="得分"
prop="score"
>
<el-input
v-model="dataForm.score"
:readonly="true"
/>
</el-form-item>
<el-form-item
label="是否匿名"
prop="isAnonymous"
<el-form
ref="dataFormRef"
:model="dataForm"
:rules="dataRule"
label-width="80px"
@keyup.enter="onSubmit()"
>
<!-- 如果不是编辑状态则显示评论详情 -->
<div v-if="!isEdit">
<!-- 评论内容表单项只读 -->
<el-form-item label="评论内容" prop="content">
<el-input
v-model="dataForm.content"
type="textarea"
:readonly="true"
/>
</el-form-item>
<!-- 评论图片表单项 -->
<el-form-item label="评论图片" prop="pics">
<div v-if="!dataForm.pics?.length"></div>
<div v-else>
<img
v-for="item in dataForm.pics"
:key="item"
alt=""
max-width="100%"
:src="dialogImageUrl + item"
>
<el-radio-group
v-model="dataForm.isAnonymous"
:disabled="true"
>
<el-radio :label="1">
</el-radio>
<el-radio :label="0">
不是
</el-radio>
</el-radio-group>
</el-form-item>
</div>
</el-form-item>
<el-form-item
label="掌柜回复"
type="textarea"
prop="userName"
<!-- 记录时间表单项只读 -->
<el-form-item label="记录时间" prop="recTime">
<el-input
v-model="dataForm.recTime"
:readonly="true"
/>
</el-form-item>
<!-- 回复时间表单项只读 -->
<el-form-item label="回复时间" prop="replyTime">
<el-input
v-model="dataForm.replyTime"
:readonly="true"
/>
</el-form-item>
<!-- IP来源表单项只读 -->
<el-form-item label="IP来源" prop="postip">
<el-input
v-model="dataForm.postip"
:readonly="true"
/>
</el-form-item>
<!-- 得分表单项只读 -->
<el-form-item label="得分" prop="score">
<el-input
v-model="dataForm.score"
:readonly="true"
/>
</el-form-item>
<!-- 是否匿名表单项只读 -->
<el-form-item label="是否匿名" prop="isAnonymous">
<el-radio-group
v-model="dataForm.isAnonymous"
:disabled="true"
>
<el-input
v-model="dataForm.replyContent"
:readonly="!isEdit"
/>
</el-form-item>
<el-radio :label="1"></el-radio>
<el-radio :label="0">不是</el-radio>
</el-radio-group>
</el-form-item>
</div>
<el-form-item
<!-- 掌柜回复表单项非编辑状态下只读 -->
<el-form-item label="掌柜回复" prop="replyContent">
<el-input
v-model="dataForm.replyContent"
type="textarea"
:readonly="!isEdit"
/>
</el-form-item>
<!-- 审核表单项仅在编辑状态下显示 -->
<el-form-item v-if="isEdit" label="审核" prop="status">
<el-radio-group v-model="dataForm.status">
<el-radio :label="1">审核通过</el-radio>
<el-radio :label="-1">不通过</el-radio>
<el-radio :label="0">等待审核</el-radio>
</el-radio-group>
</el-form-item>
</el-form>
<!-- 对话框底部的操作按钮 -->
<template #footer>
<div class="dialog-footer">
<el-button @click="visible = false">取消</el-button> <!-- -->
<el-button
v-if="isEdit"
label="审核"
prop="status"
>
<el-radio-group
v-model="dataForm.status"
:readonly="true"
>
<el-radio :label="1">
审核通过
</el-radio>
<el-radio :label="-1">
不通过
</el-radio>
<el-radio :label="0">
等待审核
</el-radio>
</el-radio-group>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="visible = false">
取消
</el-button>
<el-button
v-if="isEdit"
type="primary"
@click="onSubmit()"
>
确定
</el-button>
</div>
</template>
type="primary"
@click="onSubmit()"
>确定</el-button> <!-- -->
</div>
</template>
</el-dialog>
</template>
<script setup>
import { ElMessage } from 'element-plus'
const emit = defineEmits(['refreshDataList'])
//
const dataRule = ref({})
//
const isEdit = ref(false)
// 使ref
const dataForm = ref({
prodCommId: null,
prodId: null,
@ -168,52 +142,54 @@ const dataForm = ref({
isAnonymous: null,
status: null
})
// prodCommIdisEditParam
const init = (prodCommId, isEditParam) => {
isEdit.value = isEditParam
isEdit.value = isEditParam //
dataForm.value.prodCommId = prodCommId || 0
visible.value = true
visible.value = true //
nextTick(() => {
dataFormRef.value?.resetFields()
dataFormRef.value?.resetFields() //
if (dataForm.value.prodCommId) {
// ID
http({
url: http.adornUrl('/prod/prodComm/info/' + dataForm.value.prodCommId),
method: 'get',
params: http.adornParams()
})
.then(({ data }) => {
dataForm.value = data
Object.assign(dataForm.value, data) // dataForm
})
}
})
}
defineExpose({ init })
defineExpose({ init }) // init
//
const visible = ref(false)
//
const dataFormRef = ref(null)
/**
* 表单提交
* 表单提交方法
*/
const onSubmit = () => {
dataFormRef.value?.validate((valid) => {
if (valid) {
//
http({
url: http.adornUrl('/prod/prodComm'),
method: dataForm.value.prodCommId ? 'put' : 'post',
data: http.adornData(dataForm.value)
})
.then(() => {
ElMessage({
message: '操作成功',
type: 'success',
duration: 1500,
onClose: () => {
visible.value = false
emit('refreshDataList')
}
})
ElMessage.success('操作成功')
visible.value = false //
emit('refreshDataList') //
})
}
})
}
</script>

@ -1,41 +1,49 @@
<template>
<!-- 商品评论管理模块 -->
<div class="mod-prod-prodComm">
<!-- 使用avue-crud组件构建表格 -->
<avue-crud
ref="crudRef"
:page="page"
:data="dataList"
:table-loading="dataListLoading"
:option="tableOption"
@search-change="onSearch"
@on-load="getDataList"
@refresh-change="refreshChange"
@row-del="rowDel"
:page="page" <!-- 分页配置 -->
:data="dataList" <!-- 表格数据 -->
:table-loading="dataListLoading" <!-- 表格加载状态 -->
:option="tableOption" <!-- 表格选项 -->
@search-change="onSearch" <!-- 搜索条件改变时触发 -->
@on-load="getDataList" <!-- 加载分页数据时触发 -->
@refresh-change="refreshChange" <!-- 刷新表格时触发 -->
@row-del="rowDel" <!-- 删除行时触发 -->
>
<template #nickName="scope">
{{ scope.row.user.nickName }}
</template>
<template #replyTime="scope">
{{ scope.row.replyTime ? scope.row.replyTime : '-' }}
</template>
<!-- 自定义模板显示用户昵称 -->
<template #nickName="scope">
{{ scope.row.user.nickName }}
</template>
<template #menu="scope">
<el-button
type="primary"
icon="el-icon-edit"
@click="onAddOrUpdate(scope.row.prodCommId,true)"
>
编辑
</el-button>
<!-- 自定义模板显示回复时间如果为空则显示'-' -->
<template #replyTime="scope">
{{ scope.row.replyTime ? scope.row.replyTime : '-' }}
</template>
<el-button
type="success"
icon="el-icon-view"
@click="onAddOrUpdate(scope.row.prodCommId,false)"
>
查看
</el-button>
</template>
<!-- 自定义菜单栏提供编辑和查看按钮 -->
<template #menu="scope">
<el-button
type="primary"
icon="el-icon-edit"
@click="onAddOrUpdate(scope.row.prodCommId, true)"
>
编辑
</el-button>
<el-button
type="success"
icon="el-icon-view"
@click="onAddOrUpdate(scope.row.prodCommId, false)"
>
查看
</el-button>
</template>
</avue-crud>
<!-- 弹窗, 新增 / 修改 -->
<add-or-update
v-if="addOrUpdateVisible"
ref="addOrUpdateRef"
@ -46,19 +54,31 @@
<script setup>
import { ElMessage, ElMessageBox } from 'element-plus'
import { tableOption } from '@/crud/prod/prodComm.js'
import { tableOption } from '@/crud/prod/prodComm.js' //
import AddOrUpdate from './add-or-update.vue'
//
const dataList = ref([])
//
const page = reactive({
total: 0, //
currentPage: 1, //
pageSize: 20 //
total: 0, //
currentPage: 1, //
pageSize: 20 //
})
//
const dataListLoading = ref(false)
/**
* 获取数据列表方法
* @param pageParam - 分页参数
* @param params - 搜索参数
* @param done - 完成回调函数
*/
const getDataList = (pageParam, params, done) => {
dataListLoading.value = true
dataListLoading.value = true // true
http({
url: http.adornUrl('/prod/prodComm/page'),
method: 'get',
@ -68,24 +88,33 @@ const getDataList = (pageParam, params, done) => {
}, params))
})
.then(({ data }) => {
dataList.value = data.records
page.total = data.total
dataListLoading.value = false
if (done) done()
dataList.value = data.records // dataList
page.total = data.total //
dataListLoading.value = false // false
if (done) done() //
})
}
//
const addOrUpdateVisible = ref(false)
const addOrUpdateRef = ref(null)
/**
* 新增 / 修改
* 新增或修改操作
* @param id - 商品评论ID新增时为null
* @param isEdit - 是否是编辑模式
*/
const onAddOrUpdate = (id, isEdit) => {
addOrUpdateVisible.value = true
addOrUpdateVisible.value = true //
nextTick(() => {
addOrUpdateRef.value?.init(id, isEdit)
addOrUpdateRef.value?.init(id, isEdit) //
})
}
/**
* 删除行
* @param row - 要删除的行数据
*/
const rowDel = (row) => {
ElMessageBox.confirm('确定进行删除操作?', '提示', {
confirmButtonText: '确定',
@ -94,7 +123,7 @@ const rowDel = (row) => {
})
.then(() => {
http({
url: http.adornUrl('/prod/prodComm/' + row.prodCommId),
url: http.adornUrl(`/prod/prodComm/${row.prodCommId}`),
method: 'delete',
data: http.adornData({})
})
@ -104,19 +133,26 @@ const rowDel = (row) => {
type: 'success',
duration: 1500,
onClose: () => {
getDataList()
getDataList() //
}
})
})
}).catch(() => { })
}).catch(() => { /* 用户取消操作 */ })
}
/**
* 刷新回调
* 刷新回调重新加载数据列表
*/
const refreshChange = () => {
getDataList(page)
getDataList(page) //
}
/**
* 搜索条件改变时重新加载数据列表
* @param params - 搜索参数
* @param done - 完成回调函数
*/
const onSearch = (params, done) => {
getDataList(page, params, done)
getDataList(page, params, done) //
}
</script>

@ -1,9 +1,12 @@
<template>
<!-- 商品运费设置模块 -->
<div class="mod-prod-prod-transport">
<!-- 运费设置表单项 -->
<el-form-item
label="运费设置"
:rules="[{ required: true, message: '运费模板不能为空'}]"
>
<!-- 运费模板选择框 -->
<el-select
v-model="transportId"
placeholder="请选择"
@ -17,6 +20,8 @@
/>
</el-select>
</el-form-item>
<!-- 展示运费详情表格 -->
<el-form-item>
<el-table
v-if="transportInfo.transfees"
@ -28,7 +33,9 @@
width="350"
>
<template #default="scope">
<!-- 如果没有城市列表则显示所有地区 -->
<span v-if="!scope.row.cityList.length"></span>
<!-- 否则显示具体城市 -->
<el-tag
v-for="city in scope.row.cityList"
v-else
@ -38,6 +45,7 @@
</el-tag>
</template>
</el-table-column>
<!-- 动态列标题根据chargeType改变 -->
<el-table-column
prop="firstPiece"
:label="tableTitle[0]"
@ -56,6 +64,8 @@
/>
</el-table>
</el-form-item>
<!-- 展示包邮条件 -->
<el-form-item v-if="transportInfo.hasFreeCondition === 1">
<el-table
:data="transportInfo.transfeeFrees"
@ -97,36 +107,58 @@
</template>
<script setup>
import { computed, ref, watch, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
// propsmodelValue
const props = defineProps({
modelValue: {
default: null,
type: Number
}
})
// emitsmodelValue
const emit = defineEmits(['input', 'update:modelValue'])
// ID
const transportId = ref(null)
//
const tableTitle = computed(() => {
const titles = [['首件(个)', '运费(元)', '续件(个)', '续费(元)'], ['首重(kg)', '运费(元)', '续重(kg)', '续费(元)'], ['首体积(m³)', '运费(元)', '续体积(m³)', '续费(元)']]
if (transportInfo.value.chargeType) {
const titles = [
['首件(个)', '运费(元)', '续件(个)', '续费(元)'],
['首重(kg)', '运费(元)', '续重(kg)', '续费(元)'],
['首体积(m³)', '运费(元)', '续体积(m³)', '续费(元)']
]
if (transportInfo.value.chargeType !== undefined) {
return titles[transportInfo.value.chargeType]
}
return titles[0]
})
// props.modelValue
watch(
() => props.modelValue,
(id) => {
transportId.value = id
})
}
)
//
onMounted(() => {
getTransportList()
})
//
const transportList = ref([{
transportId: null,
transName: ''
}])
/**
* 获取运费模板列表
*/
const getTransportList = () => {
http({
url: http.adornUrl('/shop/transport/list'),
@ -137,12 +169,19 @@ const getTransportList = () => {
transportList.value = data
})
}
//
const transportInfo = ref({
hasFreeCondition: false,
transfeeFrees: [{ freeCityList: [] }]
})
/**
* 当运费模板选择发生变化时触发
* @param id - 选中的运费模板ID
*/
const changeTransport = (id) => {
emit('update:modelValue', id)
emit('update:modelValue', id) // modelValue
if (!id) {
return
}
@ -152,8 +191,7 @@ const changeTransport = (id) => {
params: http.adornParams({})
})
.then(({ data }) => {
transportInfo.value = data
transportInfo.value = data //
})
}
</script>

@ -1,149 +1,170 @@
<template>
<!-- 商品SKU信息管理模块 -->
<div class="mod-prod-sku-table">
<el-form-item>
<!-- SKU信息表格 -->
<el-table
:data="modelValue"
border
style="width: 100%; margin-top: 20px"
:data="modelValue" <!-- 绑定表格数据 -->
border <!-- 显示边框 -->
style="width: 100%; margin-top: 20px"
>
<el-table-column
v-for="(leftTitle, index) in tableLeftTitles"
:key="index"
:label="leftTitle"
>
<template #default="scope">
{{ scope.row.properties.split(';')[index].split(':')[1] }}
</template>
</el-table-column>
<el-table-column
v-if="tableLeftTitles.length"
prop="pic"
label="sku图片"
width="180"
>
<template #default="scope">
<pic-upload v-model="scope.row.pic" />
</template>
</el-table-column>
<el-table-column
v-if="tableLeftTitles.length"
prop="prodName"
label="商品名称"
width="250"
>
<template #default="scope">
<el-input
v-model="scope.row.prodName"
type="textarea"
:disabled="!scope.row.status"
/>
</template>
</el-table-column>
<el-table-column
prop="price"
label="销售价"
width="160"
>
<template #default="scope">
<el-input-number
v-model="scope.row.price"
controls-position="right"
:precision="2"
:max="1000000000"
:min="0.01"
:disabled="!scope.row.status"
/>
</template>
</el-table-column>
<el-table-column
prop="oriPrice"
label="市场价"
width="160"
>
<template #default="scope">
<el-input-number
v-model="scope.row.oriPrice"
controls-position="right"
:precision="2"
:max="1000000000"
:min="0.01"
:disabled="!scope.row.status"
/>
</template>
</el-table-column>
<el-table-column
prop="stocks"
label="库存"
width="160"
>
<template #default="scope">
<el-input-number
v-model="scope.row.stocks"
:min="0"
controls-position="right"
type="number"
:disabled="!scope.row.status"
/>
</template>
</el-table-column>
<el-table-column
prop="weight"
label="商品重量(kg)"
width="210"
>
<template #default="scope">
<el-input-number
v-model="scope.row.weight"
:precision="2"
:min="0"
controls-position="right"
:disabled="!scope.row.status"
/>
</template>
</el-table-column>
<el-table-column
prop="volume"
label="商品体积(m³)"
width="210"
>
<template #default="scope">
<el-input-number
v-model="scope.row.volume"
:precision="2"
:min="0"
controls-position="right"
:disabled="!scope.row.status"
/>
</template>
</el-table-column>
<el-table-column
label="操作"
>
<template #default="scope">
<el-button
v-if="scope.row.status"
type="text"
@click="changeSkuStatus(`${scope.$index}`)"
>
禁用
</el-button>
<el-button
v-else
type="text"
@click="changeSkuStatus(`${scope.$index}`)"
>
启用
</el-button>
</template>
</el-table-column>
<!-- 动态生成左侧标题列 -->
<el-table-column
v-for="(leftTitle, index) in tableLeftTitles" <!-- 循环创建列 -->
:key="index"
:label="leftTitle" <!-- 列标题 -->
>
<template #default="scope"> <!-- 自定义单元格内容 -->
{{ scope.row.properties.split(';')[index].split(':')[1] }} <!-- 根据properties字段显示属性值 -->
</template>
</el-table-column>
<!-- SKU图片列 -->
<el-table-column
v-if="tableLeftTitles.length" <!-- 如果有左侧标题则显示 -->
prop="pic"
label="sku图片"
width="180"
>
<template #default="scope"> <!-- 自定义单元格内容 -->
<pic-upload v-model="scope.row.pic" /> <!-- 使用自定义上传组件 -->
</template>
</el-table-column>
<!-- 商品名称列 -->
<el-table-column
v-if="tableLeftTitles.length" <!-- 如果有左侧标题则显示 -->
prop="prodName"
label="商品名称"
width="250"
>
<template #default="scope"> <!-- 自定义单元格内容 -->
<el-input
v-model="scope.row.prodName" <!-- 绑定输入框值 -->
type="textarea" <!-- 设置为文本区域类型 -->
:disabled="!scope.row.status" <!-- 状态控制是否可编辑 -->
/>
</template>
</el-table-column>
<!-- 销售价列 -->
<el-table-column
prop="price"
label="销售价"
width="160"
>
<template #default="scope"> <!-- 自定义单元格内容 -->
<el-input-number
v-model="scope.row.price" <!-- 绑定输入框值 -->
controls-position="right" <!-- 控制按钮位置 -->
:precision="2" <!-- 小数点后保留位数 -->
:max="1000000000" <!-- 最大值 -->
:min="0.01" <!-- 最小值 -->
:disabled="!scope.row.status" <!-- 状态控制是否可编辑 -->
/>
</template>
</el-table-column>
<!-- 市场价列 -->
<el-table-column
prop="oriPrice"
label="市场价"
width="160"
>
<template #default="scope"> <!-- 自定义单元格内容 -->
<el-input-number
v-model="scope.row.oriPrice" <!-- 绑定输入框值 -->
controls-position="right" <!-- 控制按钮位置 -->
:precision="2" <!-- 小数点后保留位数 -->
:max="1000000000" <!-- 最大值 -->
:min="0.01" <!-- 最小值 -->
:disabled="!scope.row.status" <!-- 状态控制是否可编辑 -->
/>
</template>
</el-table-column>
<!-- 库存列 -->
<el-table-column
prop="stocks"
label="库存"
width="160"
>
<template #default="scope"> <!-- 自定义单元格内容 -->
<el-input-number
v-model="scope.row.stocks" <!-- 绑定输入框值 -->
:min="0" <!-- 最小值 -->
controls-position="right" <!-- 控制按钮位置 -->
type="number" <!-- 类型 -->
:disabled="!scope.row.status" <!-- 状态控制是否可编辑 -->
/>
</template>
</el-table-column>
<!-- 商品重量列 -->
<el-table-column
prop="weight"
label="商品重量(kg)"
width="210"
>
<template #default="scope"> <!-- 自定义单元格内容 -->
<el-input-number
v-model="scope.row.weight" <!-- 绑定输入框值 -->
:precision="2" <!-- 小数点后保留位数 -->
:min="0" <!-- 最小值 -->
controls-position="right" <!-- 控制按钮位置 -->
:disabled="!scope.row.status" <!-- 状态控制是否可编辑 -->
/>
</template>
</el-table-column>
<!-- 商品体积列 -->
<el-table-column
prop="volume"
label="商品体积(m³)"
width="210"
>
<template #default="scope"> <!-- 自定义单元格内容 -->
<el-input-number
v-model="scope.row.volume" <!-- 绑定输入框值 -->
:precision="2" <!-- 小数点后保留位数 -->
:min="0" <!-- 最小值 -->
controls-position="right" <!-- 控制按钮位置 -->
:disabled="!scope.row.status" <!-- 状态控制是否可编辑 -->
/>
</template>
</el-table-column>
<!-- 操作列 -->
<el-table-column
label="操作"
>
<template #default="scope"> <!-- 自定义单元格内容 -->
<el-button
v-if="scope.row.status" <!-- 如果状态为启用则显示禁用按钮 -->
type="text"
@click="changeSkuStatus(`${scope.$index}`)" <!-- 点击事件触发改变状态 -->
>
禁用
</el-button>
<el-button
v-else <!-- 如果状态为禁用则显示启用按钮 -->
type="text"
@click="changeSkuStatus(`${scope.$index}`)" <!-- 点击事件触发改变状态 -->
>
启用
</el-button>
</template>
</el-table-column>
</el-table>
</el-form-item>
</div>
</template>
<script setup>
import { computed, ref, watch, onMounted } from 'vue'
import { scoreProdStore } from '@/stores/prod.js'
// props
const props = defineProps({
modelValue: {
default: () => [],
@ -154,10 +175,17 @@ const props = defineProps({
type: String
}
})
// emits
const emit = defineEmits(['update:modelValue'])
const dbSpecs = ref([]) //
//
const dbSpecs = ref([])
//
let initing = false
//
const tableLeftTitles = computed(() => {
const res = []
for (let i = 0; i < skuTags.value.length; i++) {
@ -166,16 +194,22 @@ const tableLeftTitles = computed(() => {
}
return res
})
//
const prod = scoreProdStore()
// SKU
const skuTags = computed({
get () { return prod.skuTags }
})
// SKU
watch(() => props.prodName,
() => {
skuAddProdName()
})
//
onMounted(() => {
http({
url: http.adornUrl('/prod/spec/list'),
@ -183,48 +217,58 @@ onMounted(() => {
params: http.adornParams()
})
.then(({ data }) => {
dbSpecs.value = data
dbSpecs.value = data //
})
})
//
const init = () => {
initing = true
}
defineExpose({ init })
/**
* 改变SKU状态
* @param tagIndex - 要改变状态的SKU索引
*/
const changeSkuStatus = (tagIndex) => {
// eslint-disable-next-line vue/no-mutating-props
props.modelValue[tagIndex].status = props.modelValue[tagIndex].status ? 0 : 1
props.modelValue[tagIndex].status = props.modelValue[tagIndex].status ? 0 : 1 //
}
/**
* 添加或更新商品名称到每个SKU中
*/
const skuAddProdName = () => {
if (initing) return
if (initing) return //
const skuList = []
for (let i = 0; i < props.modelValue.length; i++) {
const sku = Object.assign({}, props.modelValue[i])
const sku = Object.assign({}, props.modelValue[i]) // SKU
if (!sku.properties) {
return
return // properties
}
sku.skuName = ''
const properties = sku.properties.split(';')
sku.skuName = '' // SKU
const properties = sku.properties.split(';') // properties
for (const propertiesKey in properties) {
sku.skuName += properties[propertiesKey].split(':')[1] + ' '
sku.skuName += properties[propertiesKey].split(':')[1] + ' ' // SKU
}
sku.prodName = props.prodName + ' ' + sku.skuName
skuList.push(sku)
sku.prodName = props.prodName + ' ' + sku.skuName //
skuList.push(sku) // SKU
}
emit('update:modelValue', skuList)
emit('update:modelValue', skuList) // modelValue
}
</script>
<style lang="scss" scoped>
.mod-prod-sku-table{
:deep(.pic-uploader-component .el-upload) {
.mod-prod-sku-table {
/* 图片上传样式 */
:deep(.pic-uploader-component .el-upload) {
border: 1px dashed #d9d9d9;
border-radius: 6px;
cursor: pointer;
position: relative;
overflow: hidden;
.pic-uploader-icon {
font-size: 28px;
color: #8c939d;
@ -233,12 +277,15 @@ const skuAddProdName = () => {
line-height: 120px;
text-align: center;
}
.pic {
width: 120px;
height: 120px;
display: block;
}
}
/* 鼠标悬停时的样式 */
:deep(.pic-uploader-component .el-upload:hover) {
border-color: #409EFF;
}

@ -1,14 +1,18 @@
<template>
<div class="mod-prod-sku-tag">
<!-- 商品规格表单项 -->
<el-form-item label="商品规格">
<!-- 添加规格按钮 -->
<el-button @click="shopTagInput()">
添加规格
</el-button>
<div
v-for="(tag, tagIndex) in skuTags"
:key="tagIndex"
>
<!-- 遍历skuTags数组显示每个规格及其值 -->
<div v-for="(tag, tagIndex) in skuTags" :key="tagIndex">
<!-- 显示规格名称 -->
<span>{{ tag.tagName }}</span>
<!-- 删除规格按钮 -->
<el-button
class="button-new-tag"
type="text"
@ -18,6 +22,8 @@
删除
</el-button>
<br>
<!-- 遍历当前规格下的所有值并以标签形式展示 -->
<el-tag
v-for="(tagItem, tagItemIndex) in tag.tagItems"
:key="tagItem.valueId"
@ -27,6 +33,8 @@
>
{{ tagItem.propValue }}
</el-tag>
<!-- 如果显示输入框则渲染一个用于新增规格值的输入框 -->
<el-input
v-if="tagItemInputs[tagIndex] && tagItemInputs[tagIndex].visible"
:ref="`saveTagInput${tagIndex}`"
@ -35,6 +43,8 @@
@keyup.enter="handleInputConfirm(tagIndex)"
@blur="handleInputConfirm(tagIndex)"
/>
<!-- 否则显示+ 添加按钮以切换到输入框 -->
<el-button
v-else
class="button-new-tag"
@ -44,11 +54,11 @@
</el-button>
</div>
</el-form-item>
<el-form-item
v-show="isShowTagInput"
label="规格名"
>
<!-- 当isShowTagInput为true时显示规格名选择项 -->
<el-form-item v-show="isShowTagInput" label="规格名">
<el-col :span="8">
<!-- 规格名选择器允许创建新选项 -->
<el-select
v-model="addTagInput.propName"
filterable
@ -57,6 +67,7 @@
placeholder="请选择"
@change="handleTagClick"
>
<!-- 遍历未使用的规格作为选择器的选项 -->
<el-option
v-for="item in unUseTags"
:key="item.propId"
@ -66,11 +77,11 @@
</el-select>
</el-col>
</el-form-item>
<el-form-item
v-show="isShowTagInput"
label="规格值"
>
<!-- 当isShowTagInput为true时显示规格值选择项 -->
<el-form-item v-show="isShowTagInput" label="规格值">
<el-col :span="8">
<!-- 规格值选择器支持多选并允许创建新选项 -->
<el-select
v-model="addTagInput.selectValues"
multiple
@ -79,6 +90,7 @@
default-first-option
placeholder="请选择"
>
<!-- 遍历数据库中的规格值作为选择器的选项 -->
<el-option
v-for="item in dbTagValues"
:key="item.valueId"
@ -88,7 +100,10 @@
</el-select>
</el-col>
</el-form-item>
<!-- 操作按钮组当isShowTagInput为true时显示 -->
<el-form-item>
<!-- 确认按钮点击后调用addTag方法 -->
<el-button
v-show="isShowTagInput"
type="primary"
@ -96,6 +111,8 @@
>
确定
</el-button>
<!-- 取消按钮点击后隐藏规格名/值输入框 -->
<el-button
v-show="isShowTagInput"
@click="hideTagInput()"
@ -109,31 +126,54 @@
<script setup>
import { ElMessage } from 'element-plus'
import { scoreProdStore } from '@/stores/prod.js'
import { ref, computed, watch, onMounted, nextTick } from 'vue'
//
const prod = scoreProdStore()
// props
const props = defineProps({
skuList: {
type: Array,
default: () => []
}
})
// emits
const emit = defineEmits(['change'])
// /
const isShowTagInput = ref(false)
//
const addTagInput = ref({
propName: '',
selectValues: []
propName: '', //
selectValues: [] //
})
//
const type = ref(0)
//
const tagItemName = ref('')
//
const tagItemInputs = ref([])
const dbTagValues = ref([]) //
const dbTags = ref([]) //
//
const dbTagValues = ref([])
//
const dbTags = ref([])
//
let tagName = ''
let tagNameIndex = 0
let maxValueId = 0 // id
let maxPropId = 0 // id
let initing = false
// SKU
const skuTags = computed({
get () { return prod.skuTags },
set (val) { prod.updateSkuTags(val) }
@ -154,10 +194,10 @@ const unUseTags = computed(() => {
return res
})
const defalutSku = computed(() => {
return prod.defalutSku
})
// SKU
const defalutSku = computed(() => prod.defalutSku)
// skuTagsSKU
watch(() => skuTags.value,
(val) => {
if (initing) {
@ -165,6 +205,8 @@ watch(() => skuTags.value,
return
}
let skuListArr = []
// typeSKU
if (type.value === 4) {
//
props.skuList?.forEach(sku => {
@ -175,19 +217,19 @@ watch(() => skuTags.value,
})
} else if (type.value === 2) {
//
const properties = tagName + ':' + tagItemName.value
const properties = `${tagName}:${tagItemName.value}`
//
let tempSkuList = []
val?.forEach(tag => {
if (skuListArr.length === 0) {
if (tagName === tag.tagName) {
const sku = Object.assign({}, defalutSku.value)
sku.properties = properties //
sku.properties = properties
skuListArr.push(sku)
} else {
tag.tagItems.forEach(tagItem => {
const sku = Object.assign({}, defalutSku.value)
sku.properties = `${tag.tagName}:${tagItem.propValue}` //
sku.properties = `${tag.tagName}:${tagItem.propValue}`
skuListArr.push(sku)
})
}
@ -225,7 +267,7 @@ watch(() => skuTags.value,
if (skuListArr.length === 0) {
tag.tagItems.forEach(tagItem => {
const sku = Object.assign({}, defalutSku.value)
sku.properties = `${tag.tagName}:${tagItem.propValue}` //
sku.properties = `${tag.tagName}:${tagItem.propValue}`
skuListArr.push(sku)
})
} else {
@ -241,45 +283,52 @@ watch(() => skuTags.value,
}
})
}
// SKUSKU
if (!skuListArr.length) {
skuListArr.push(Object.assign({}, defalutSku.value))
}
// debugger
// SKU
emit('change', skuListArr)
},
{
}, {
deep: true
}
)
})
//
onMounted(() => {
//
http({
url: http.adornUrl('/prod/spec/list'),
method: 'get',
params: http.adornParams()
}).then(({ data }) => {
dbTags.value = data
if (data) {
maxPropId = Math.max.apply(Math, data.map(item => item.propId))
} else {
maxPropId = 0
}
})
.then(({ data }) => {
dbTags.value = data
if (data) {
maxPropId = Math.max.apply(Math, data.map(item => item.propId))
} else {
maxPropId = 0
}
})
// ID
http({
url: http.adornUrl('/prod/spec/listSpecMaxValueId'),
method: 'get',
params: http.adornParams()
}).then(({ data }) => {
if (data) {
maxValueId = data
} else {
maxValueId = 0
}
})
.then(({ data }) => {
if (data) {
maxValueId = data
} else {
maxValueId = 0
}
})
})
/**
* 初始化SKU标签和列表
* @param {Array} skuList - SKU列表
*/
const init = (skuList) => {
if (!skuList || !skuList.length) {
skuTags.value = []
@ -319,12 +368,16 @@ const shopTagInput = () => {
}
/**
* 隐藏规格名规格值输入框
* 隐藏规格名规格值输入框并清除内容
*/
const hideTagInput = () => {
isShowTagInput.value = false
cleanTagInput()
}
/**
* 添加新的规格和规格值
*/
const addTag = () => {
const selectValues = addTagInput.value.selectValues
if (!addTagInput.value.propName) {
@ -397,7 +450,9 @@ const handleTagClick = () => {
}
/**
* 规格名输入框选中规格事件
* 删除规格值
* @param {number} tagIndex - 标签索引
* @param {number} tagItemIndex - 标签项索引
*/
const handleTagClose = (tagIndex, tagItemIndex) => {
tagName = skuTags.value[tagIndex].tagName
@ -412,6 +467,7 @@ const handleTagClose = (tagIndex, tagItemIndex) => {
/**
* 标签输入框确定时调用
* @param {number} tagIndex - 标签索引
*/
const handleInputConfirm = (tagIndex) => {
if (checkTagItem(tagIndex)) {
@ -434,6 +490,7 @@ const handleInputConfirm = (tagIndex) => {
/**
* 显示标签输入框
* @param {number} tagIndex - 标签索引
*/
const showTagInput = (tagIndex) => {
tagItemInputs.value.push({ visible: false, value: '' })
@ -445,6 +502,8 @@ const showTagInput = (tagIndex) => {
/**
* 获取数据集合所有对象中某个属性的最大值
* @param {Array} list - 对象数组
* @returns {number} 最大值
*/
const getMaxValueId = (list) => {
return Math.max.apply(Math, list.map(item => item.valueId))
@ -452,7 +511,7 @@ const getMaxValueId = (list) => {
/**
* 删除 规格
* @param tagIndex
* @param {number} tagIndex - 标签索引
*/
const removeTag = (tagIndex) => {
type.value = 3
@ -461,6 +520,8 @@ const removeTag = (tagIndex) => {
/**
* 新增规格值时判断是否存在同名的规格值
* @param {number} tagIndex - 标签索引
* @returns {boolean} 是否存在重复
*/
const checkTagItem = (tagIndex) => {
const tagItem = tagItemInputs.value[tagIndex].value
@ -476,7 +537,7 @@ const checkTagItem = (tagIndex) => {
})
if (arr.indexOf(tagItem) > -1) {
isSame = true
ElMessage.error('product.specificationValue')
ElMessage.error('product.specificationValue') //
return false
}
})
@ -486,49 +547,67 @@ const checkTagItem = (tagIndex) => {
<style lang="scss" scoped>
.mod-prod-sku-tag {
// el-tag
:deep(.el-tag + .el-tag) {
margin-left: 10px;
margin-left: 10px; //
}
//
.button-new-tag {
margin-left: 10px;
height: 32px;
line-height: 30px;
padding-top: 0;
padding-bottom: 0;
margin-left: 10px; //
height: 32px; //
line-height: 30px; //
padding-top: 0; //
padding-bottom: 0; //
}
//
.input-new-tag {
width: 90px;
margin-left: 10px;
vertical-align: bottom;
width: 90px; //
margin-left: 10px; //
vertical-align: bottom; //
}
}
//
//
:deep(.sku-border) {
border: 1px solid #EBEEF5;
width:70%
border: 1px solid #EBEEF5; //
width: 70%; //
}
:deep(.sku-background){
background-color: #F6f6f6;
margin: 12px 12px;
.el-button{
margin-left: 10px;
span{
color:#000 !important;
// SKU
:deep(.sku-background) {
background-color: #F6f6f6; //
margin: 12px 12px; //
//
.el-button {
margin-left: 10px; //
span {
color: #000 !important; //
}
}
.el-form-item__label{
padding:0 24px 0 0
//
.el-form-item__label {
padding: 0 24px 0 0; //
}
}
:deep(.sku-tag){
margin: 12px 12px;
// SKU
:deep(.sku-tag) {
margin: 12px 12px; //
}
:deep(.tagTree){
margin-left: 18px;
padding-bottom:8px;
//
:deep(.tagTree) {
margin-left: 18px; //
padding-bottom: 8px; //
}
// div
.el-form-item__content div {
width: 100%;
width: 100%; // div100%
}
</style>

@ -1,28 +1,22 @@
<template>
<div class="mod-prod-info">
<el-form
ref="dataFormRef"
:model="dataForm"
label-width="100px"
>
<!-- 使用el-form构建一个表单并设置引用 -->
<el-form ref="dataFormRef" :model="dataForm" label-width="100px">
<!-- 产品图片上传 -->
<el-form-item label="产品图片">
<mul-pic-upload v-model="dataForm.imgs" />
</el-form-item>
<!-- 产品上架/下架状态选择 -->
<el-form-item label="状态">
<el-radio-group v-model="dataForm.status">
<el-radio :label="1">
上架
</el-radio>
<el-radio :label="0">
下架
</el-radio>
<el-radio :label="1">上架</el-radio>
<el-radio :label="0">下架</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item
label="产品分类"
:rules="[{ required: true, message: '请选择产品分类'}]"
prop="categoryId"
>
<!-- 产品分类选择 -->
<el-form-item label="产品分类" prop="categoryId" :rules="[...rules]">
<el-col :span="8">
<el-cascader
v-model="category.selected"
@ -34,97 +28,53 @@
/>
</el-col>
</el-form-item>
<el-form-item
label="产品分组"
:rules="[{ required: true, message: '请选择产品分组'}]"
>
<!-- 产品分组多选 -->
<el-form-item label="产品分组" prop="tagList" :rules="[...rules]">
<el-col :span="8">
<el-select
v-model="dataForm.tagList"
multiple
style="width: 250px"
placeholder="请选择"
>
<el-option
v-for="item in tags"
:key="item.id"
:label="item.title"
:value="item.id"
/>
<el-select v-model="dataForm.tagList" multiple style="width: 250px" placeholder="请选择">
<el-option v-for="item in tags" :key="item.id" :label="item.title" :value="item.id" />
</el-select>
</el-col>
</el-form-item>
<el-form-item
label="产品名称"
prop="prodName"
:rules="[
{ required: true, message: '产品名称不能为空'},
{ pattern: /\s\S+|S+\s|\S/, message: '请输入正确的产品名称', trigger: 'blur' }
]"
>
<!-- 产品名称输入框 -->
<el-form-item label="产品名称" prop="prodName" :rules="[...rules]">
<el-col :span="8">
<el-input
v-model="dataForm.prodName"
placeholder="产品名称"
maxlength="50"
/>
<el-input v-model="dataForm.prodName" placeholder="产品名称" maxlength="50" />
</el-col>
</el-form-item>
<el-form-item
label="产品卖点"
prop="brief"
:rules="[
{ required: false, pattern: /\s\S+|S+\s|\S/, message: '请输入正确的产品卖点', trigger: 'blur' }
]"
>
<!-- 产品卖点文本域 -->
<el-form-item label="产品卖点" prop="brief" :rules="[...rules]">
<el-col :span="8">
<el-input
v-model="dataForm.brief"
type="textarea"
:autosize="{minRows: 2, maxRows: 4}"
placeholder="产品卖点"
/>
<el-input v-model="dataForm.brief" type="textarea" :autosize="{minRows: 2, maxRows: 4}" placeholder="产品卖点" />
</el-col>
</el-form-item>
<!-- 配送方式选择 -->
<el-form-item label="配送方式">
<el-checkbox v-model="dataForm.deliveryMode.hasShopDelivery">
商家配送
</el-checkbox>
<el-checkbox v-model="dataForm.deliveryMode.hasUserPickUp">
用户自提
</el-checkbox>
<el-checkbox v-model="dataForm.deliveryMode.hasShopDelivery"></el-checkbox>
<el-checkbox v-model="dataForm.deliveryMode.hasUserPickUp"></el-checkbox>
</el-form-item>
<prod-transport
v-show="dataForm.deliveryMode.hasShopDelivery"
v-model="dataForm.deliveryTemplateId"
/>
<sku-tag
ref="skuTagRef"
:sku-list="dataForm.skuList"
@change="skuTagChangeSkuHandler"
/>
<sku-table
ref="skuTableRef"
v-model="dataForm.skuList"
:prod-name="dataForm.prodName"
/>
<el-form-item
label="产品详情"
prop="content"
>
<tiny-mce
ref="contentRef"
v-model="dataForm.content"
style="width:1000px"
/>
<!-- 商家配送模板选择仅当选择了商家配送时显示 -->
<prod-transport v-show="dataForm.deliveryMode.hasShopDelivery" v-model="dataForm.deliveryTemplateId" />
<!-- SKU标签组件用于管理商品规格 -->
<sku-tag ref="skuTagRef" :sku-list="dataForm.skuList" @change="skuTagChangeSkuHandler" />
<!-- SKU表格组件展示SKU详细信息 -->
<sku-table ref="skuTableRef" v-model="dataForm.skuList" :prod-name="dataForm.prodName" />
<!-- 产品详情富文本编辑器 -->
<el-form-item label="产品详情" prop="content">
<tiny-mce ref="contentRef" v-model="dataForm.content" style="width:1000px" />
</el-form-item>
<!-- 提交按钮 -->
<el-form-item>
<el-button
type="primary"
@click="onSubmit()"
>
确定
</el-button>
<el-button type="primary" @click="onSubmit()"></el-button>
</el-form-item>
</el-form>
</div>
@ -138,18 +88,20 @@ import SkuTag from './components/sku-tag.vue'
import SkuTable from './components/sku-table.vue'
import { Debounce } from '@/utils/debounce'
//
const emit = defineEmits(['refreshDataList'])
//
//
const category = reactive({
list: [],
selected: [],
list: [], //
selected: [], //
props: {
value: 'categoryId',
label: 'categoryName'
}
})
//
//
const dataForm = ref({
prodName: '',
brief: '',
@ -167,22 +119,28 @@ const dataForm = ref({
},
deliveryTemplateId: null
})
//
const tags = ref([])
// ID
onMounted(() => {
dataForm.value.prodId = useRoute().query.prodId
getDataList()
})
//
const skuTableRef = ref(null)
const skuTagRef = ref(null)
/**
* 获取分类数据
* 获取所有必要的数据列表
*/
const getDataList = () => {
getTagList()
getCategoryList().then(() => {
if (dataForm.value.prodId) {
//
// ID
http({
url: http.adornUrl(`/prod/prod/info/${dataForm.value.prodId}`),
method: 'get',
@ -198,6 +156,7 @@ const getDataList = () => {
})
} else {
nextTick(() => {
//
dataFormRef.value?.resetFields()
skuTagRef.value?.init()
dataForm.value.pic = ''
@ -206,49 +165,48 @@ const getDataList = () => {
}
})
}
/**
* 获取分类信息
* 获取分类信息并转换为树形结构
*/
const getCategoryList = () => {
return http({
url: http.adornUrl('/prod/category/listCategory'),
method: 'get',
params: http.adornParams()
}).then(({ data }) => {
category.list = treeDataTranslate(data, 'categoryId', 'parentId')
})
.then(({ data }) => {
category.list = treeDataTranslate(data, 'categoryId', 'parentId')
})
}
/**
* 选择分类改变事件
* @param val
* 处理分类改变事件更新分类ID
*/
const handleCategoryChange = (val) => {
dataForm.value.categoryId = val[val.length - 1]
}
//
const router = useRouter()
//
const dataFormRef = ref(null)
/**
* 表单提交
* 提交表单添加或更新产品信息
*/
const onSubmit = Debounce(() => {
dataFormRef.value?.validate((valid) => {
if (!valid) {
return
}
if (!dataForm.value.imgs) {
errorMsg('请选择图片上传')
return
}
if (!dataForm.value.deliveryMode) {
errorMsg('请选择配送方式')
return
}
if (dataForm.value.deliveryMode.hasShopDelivery && !dataForm.value.deliveryTemplateId) {
errorMsg('请选择运费模板')
//
if (!dataForm.value.imgs || !dataForm.value.deliveryMode ||
(dataForm.value.deliveryMode.hasShopDelivery && !dataForm.value.deliveryTemplateId)) {
errorMsg('请检查表单内容')
return
}
const param = Object.assign({}, dataForm.value)
//
paramSetPriceAndStocks(param)
@ -257,69 +215,73 @@ const onSubmit = Debounce(() => {
param.deliveryModeVo = dataForm.value.deliveryMode
//
param.pic = dataForm.value.imgs.split(',')[0]
// HTTP
http({
url: http.adornUrl('/prod/prod'),
method: param.prodId ? 'put' : 'post',
data: http.adornData(param)
})
.then(() => {
ElMessage({
message: '操作成功',
type: 'success',
duration: 1500,
onClose: () => {
router.push({
path: '/prod/prodList'
})
emit('refreshDataList')
}
})
}).then(() => {
ElMessage({
message: '操作成功',
type: 'success',
duration: 1500,
onClose: () => {
//
router.push({ path: '/prod/prodList' })
emit('refreshDataList')
}
})
})
})
})
/**
* 设置商品价格和库存总量
*/
const paramSetPriceAndStocks = (param) => {
//
param.totalStocks = 0
//
param.price = 0
//
param.oriPrice = 0
//
for (let i = 0; i < param.skuList.length; i++) {
const element = param.skuList[i]
if (element.status !== 1) {
continue
}
if (param.price === 0) {
param.price = element.price ? Number.parseFloat(element.price) : 0
}
//
if (element.status !== 1) continue
if (param.price === 0) param.price = Number.parseFloat(element.price || 0)
param.price = Math.min(param.price, element.price)
if (param.price === element.price) {
param.oriPrice = element.oriPrice ? Number.parseFloat(element.oriPrice) : 0
}
param.totalStocks += element.stocks ? Number.parseInt(element.stocks) : 0
if (param.price === element.price) param.oriPrice = Number.parseFloat(element.oriPrice || 0)
param.totalStocks += Number.parseInt(element.stocks || 0)
}
// sku使
if (param.skuList.length === 1) {
param.skuList[0].prodName = dataForm.value.prodName
}
}
/**
* 处理SKU变化事件更新SKU名称
*/
const skuTagChangeSkuHandler = (skuList) => {
const prodName = dataForm.value.prodName
skuList.forEach(sku => {
if (sku.properties) {
sku.skuName = ''
const properties = sku.properties.split(';')
for (const propertiesKey in properties) {
sku.skuName += properties[propertiesKey].split(':')[1] + ' '
for (const property of properties) {
sku.skuName += property.split(':')[1] + ' '
}
sku.prodName = prodName + ' ' + sku.skuName
sku.prodName = prodName + ' ' + sku.skuName.trim()
}
})
dataForm.value.skuList = skuList
}
/**
* 显示错误消息
*/
const errorMsg = (message) => {
ElMessage({
message,
@ -336,9 +298,8 @@ const getTagList = () => {
url: http.adornUrl('/prod/prodTag/listTagList'),
method: 'get',
params: http.adornParams()
}).then(({ data }) => {
tags.value = data
})
.then(({ data }) => {
tags.value = data
})
}
</script>

@ -1,5 +1,6 @@
<template>
<div class="mod-prod">
<!-- 使用avue-crud构建一个CRUD表格并设置引用 -->
<avue-crud
ref="crudRef"
:page="page"
@ -11,6 +12,7 @@
@selection-change="selectionChange"
@on-load="getDataList"
>
<!-- 自定义左侧菜单按钮 -->
<template #menu-left>
<el-button
v-if="isAuth('shop:pickAddr:save')"
@ -31,6 +33,7 @@
</el-button>
</template>
<!-- 自定义状态列内容 -->
<template #status="scope">
<el-tag v-if="scope.row.status === 1">
上架
@ -40,6 +43,7 @@
</el-tag>
</template>
<!-- 自定义行操作菜单 -->
<template #menu="scope">
<el-button
v-if="isAuth('prod:prod:update')"
@ -66,19 +70,29 @@
import { isAuth } from '@/utils'
import { ElMessage, ElMessageBox } from 'element-plus'
import { tableOption } from '@/crud/prod/prodList.js'
//
const router = useRouter()
//
const permission = reactive({
delBtn: isAuth('prod:prod:delete')
})
//
const dataList = ref([])
//
const page = reactive({
total: 0, //
currentPage: 1, //
pageSize: 10 //
total: 0, //
currentPage: 1, //
pageSize: 10 //
})
//
const dataListLoading = ref(false)
/**
* 获取数据列表
*/
//
const getDataList = (pageParam, params, done) => {
dataListLoading.value = true
http({
@ -95,23 +109,20 @@ const getDataList = (pageParam, params, done) => {
)
})
.then(({ data }) => {
dataList.value = data.records
for (const key in dataList.value) {
// eslint-disable-next-line no-prototype-builtins
if (dataList.value.hasOwnProperty(key)) {
const element = dataList.value[key]
element.imgs = element.imgs.split(',')[0]
}
}
//
dataList.value = data.records.map(element => ({
...element,
imgs: element.imgs.split(',')[0]
}))
page.total = data.total
dataListLoading.value = false
if (done) done()
})
}
const router = useRouter()
/**
* 新增 / 修改
* @param id
* 新增或编辑商品信息
* @param id - 商品ID如果存在则为编辑否则为新增
*/
const onAddOrUpdate = (id) => {
router.push({
@ -119,19 +130,21 @@ const onAddOrUpdate = (id) => {
query: { prodId: id }
})
}
/**
* 删除和批量删除
* @param id
* 删除或批量删除商品信息
* @param id - 单个商品ID若不存在则执行批量删除
*/
const onDelete = (id) => {
const prodIds = getSeleProdIds()
if (id) {
prodIds.push(id)
}
ElMessageBox.confirm(`确定进行[${id ? '删除' : '批量删除'}]操作?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
type: '警告'
})
.then(() => {
http({
@ -150,32 +163,33 @@ const onDelete = (id) => {
})
})
})
.catch(() => { })
.catch(() => {})
}
/**
* 条件查询
* @param params
* @param done
* 条件查询触发的数据获取
* @param params - 查询参数
* @param done - 完成回调
*/
const onSearch = (params, done) => {
getDataList(page, params, done)
}
//
const dataListSelections = ref([])
/**
* 多选变化
* @param val
* 多选变化事件处理器
* @param val - 选中的行数据
*/
const selectionChange = (val) => {
dataListSelections.value = val
}
/**
* 获取选中的商品Id列表
* 获取选中的商品ID列表
*/
const getSeleProdIds = () => {
return dataListSelections.value?.map(item => {
return item.prodId
})
return dataListSelections.value?.map(item => item.prodId) || []
}
</script>

@ -4,6 +4,7 @@
:title="!dataForm.id ? '新增' : '修改'"
:close-on-click-modal="false"
>
<!-- 表单 -->
<el-form
ref="dataFormRef"
:model="dataForm"
@ -11,50 +12,37 @@
label-width="80px"
@keyup.enter="onSubmit()"
>
<!-- 标签名称 -->
<el-form-item
label="标签名称"
prop="title"
:rules="[
{ required: true, message: '标签名称不能为空', trigger: 'blur' },
{ pattern: /\s\S+|S+\s|\S/, message: '请输入正确的标签名称', trigger: 'blur' }
{ pattern: /\s\S+|\S\s|\S/, message: '请输入正确的标签名称', trigger: 'blur' }
]"
prop="title"
>
<el-input v-model="dataForm.title" />
</el-form-item>
<el-form-item
label="状态"
prop="status"
>
<!-- 状态选择 -->
<el-form-item label="状态" prop="status">
<el-radio-group v-model="dataForm.status">
<el-radio :label="1">
正常
</el-radio>
<el-radio :label="0">
禁用
</el-radio>
<el-radio :label="1">正常</el-radio>
<el-radio :label="0">禁用</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item
label="列表样式"
prop="style"
>
<!-- 列表样式选择 -->
<el-form-item label="列表样式" prop="style">
<el-radio-group v-model="dataForm.style">
<el-radio :label="0">
一列一个
</el-radio>
<el-radio :label="1">
一列两个
</el-radio>
<el-radio :label="2">
一列三个
</el-radio>
<el-radio :label="0">一列一个</el-radio>
<el-radio :label="1">一列两个</el-radio>
<el-radio :label="2">一列三个</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item
label="排序"
prop="seq"
>
<!-- 排序 -->
<el-form-item label="排序" prop="seq">
<el-input-number
v-model="dataForm.seq"
controls-position="right"
@ -63,17 +51,12 @@
/>
</el-form-item>
</el-form>
<!-- 对话框底部按钮 -->
<template #footer>
<div class="dialog-footer">
<el-button @click="visible = false">
取消
</el-button>
<el-button
type="primary"
@click="onSubmit()"
>
确定
</el-button>
<el-button @click="visible = false">取消</el-button>
<el-button type="primary" @click="onSubmit()"></el-button>
</div>
</template>
</el-dialog>
@ -82,55 +65,76 @@
<script setup>
import { ElMessage } from 'element-plus'
import { Debounce } from '@/utils/debounce'
import { ref, nextTick, defineEmits, defineExpose } from 'vue'
//
const emit = defineEmits(['refreshDataList'])
// /
const visible = ref(false)
//
const dataRule = {}
//
const dataForm = ref({
id: null,
title: null,
shopId: null,
status: 1,
status: 1, //
isDefault: null,
prodCount: null,
seq: 0,
style: 0
seq: 0, // 0
style: 0 //
})
// ID
const init = (id) => {
dataForm.value.id = id || 0
visible.value = true
nextTick(() => {
//
dataFormRef.value?.resetFields()
if (dataForm.value.id) {
// ID
http({
url: http.adornUrl('/prod/prodTag/info/' + dataForm.value.id),
method: 'get',
params: http.adornParams()
})
.then(({ data }) => {
dataForm.value = data
Object.assign(dataForm.value, data)
})
}
})
}
// init
defineExpose({ init })
//
const dataFormRef = ref(null)
/**
* 表单提交
* 提交表单
*/
const onSubmit = Debounce(() => {
// 0
if (!dataForm.value.seq) {
dataForm.value.seq = 0
}
//
dataFormRef.value?.validate((valid) => {
if (valid) {
// HTTPID
http({
url: http.adornUrl('/prod/prodTag'),
method: dataForm.value.id ? 'put' : 'post',
data: http.adornData(dataForm.value)
})
.then(() => {
//
ElMessage({
message: '操作成功',
type: 'success',
@ -144,5 +148,4 @@ const onSubmit = Debounce(() => {
}
})
})
</script>

@ -1,5 +1,6 @@
<template>
<div class="mod-prod-prodTag">
<!-- 使用avue-crud构建CRUD表格 -->
<avue-crud
ref="crudRef"
:page="page"
@ -10,6 +11,7 @@
@on-load="getDataList"
@refresh-change="refreshChange"
>
<!-- 自定义左侧菜单按钮 -->
<template #menu-left>
<el-button
v-if="isAuth('prod:prodTag:save')"
@ -20,14 +22,15 @@
新增
</el-button>
</template>
<!-- 自定义标题列内容 -->
<template #title="scope">
{{ scope.row.title || '-' }}
</template>
<!-- 自定义状态列内容 -->
<template #status="scope">
<el-tag
v-if="scope.row.status === 0"
type="danger"
>
<el-tag v-if="scope.row.status === 0" type="danger">
禁用
</el-tag>
<el-tag v-else>
@ -35,6 +38,7 @@
</el-tag>
</template>
<!-- 自定义默认类型列内容 -->
<template #isDfault="scope">
<el-tag v-if="scope.row.isDefault === 0">
自定义类型
@ -44,6 +48,7 @@
</el-tag>
</template>
<!-- 自定义行操作菜单 -->
<template #menu="scope">
<el-button
v-if="isAuth('prod:prodTag:update')"
@ -63,6 +68,8 @@
</el-button>
</template>
</avue-crud>
<!-- 引入用于添加或更新记录的子组件 -->
<add-or-update
v-if="addOrUpdateVisible"
ref="addOrUpdateRef"
@ -76,15 +83,24 @@ import { isAuth } from '@/utils'
import { ElMessage, ElMessageBox } from 'element-plus'
import { tableOption } from '@/crud/prod/prodTag.js'
import AddOrUpdate from './add-or-update.vue'
import { ref, reactive, nextTick } from 'vue'
//
const dataList = ref([])
//
const page = reactive({
total: 0, //
currentPage: 1, //
pageSize: 10 //
total: 0, //
currentPage: 1, //
pageSize: 10 //
})
//
const dataListLoading = ref(false)
/**
* 获取数据列表方法
*/
const getDataList = (pageParam, params, done) => {
dataListLoading.value = true
http({
@ -103,11 +119,13 @@ const getDataList = (pageParam, params, done) => {
})
}
// /
const addOrUpdateVisible = ref(false)
const addOrUpdateRef = ref(null)
/**
* 新增 / 修改
* @param id
* 新增或编辑产品标签
* @param id - 标签ID如果存在则为编辑模式
*/
const onAddOrUpdate = (id) => {
addOrUpdateVisible.value = true
@ -116,6 +134,10 @@ const onAddOrUpdate = (id) => {
})
}
/**
* 删除产品标签
* @param id - 标签ID
*/
const onDelete = (id) => {
ElMessageBox.confirm('确定进行删除操作?', '提示', {
confirmButtonText: '确定',
@ -138,19 +160,22 @@ const onDelete = (id) => {
}
})
})
}).catch(() => { })
}).catch(() => {})
}
/**
* 刷新回调
* 刷新回调用于重新加载数据列表
*/
const refreshChange = () => {
getDataList(page)
}
/**
* 条件查询触发的数据获取
* @param params - 查询参数
* @param done - 完成回调
*/
const onSearch = (params, done) => {
getDataList(page, params, done)
}
</script>
<style lang="scss" scoped>
</style>

@ -4,11 +4,13 @@
:title="!dataList[0].propId ? '新增' : '修改'"
:close-on-click-modal="false"
>
<!-- 属性表格 -->
<el-table
:data="dataList"
border
style="width: 100%;"
>
<!-- 属性名称列 -->
<el-table-column
prop="propName"
header-align="center"
@ -25,6 +27,8 @@
/>
</template>
</el-table-column>
<!-- 属性值列 -->
<el-table-column
prop="prodPropValues"
header-align="center"
@ -47,6 +51,7 @@
@clear="clearProdPropValues"
/>
</el-col>
<!-- 添加新输入框按钮 -->
<el-col :span="4">
<el-button
v-show="scope.row.prodPropValues[scope.row.prodPropValues.length-1].propValue"
@ -59,37 +64,46 @@
</template>
</el-table-column>
</el-table>
<!-- 对话框底部按钮 -->
<template #footer>
<div class="dialog-footer">
<el-button @click="visible = false">
取消
</el-button>
<el-button
type="primary"
@click="onSubmit()"
>
确定
</el-button>
<el-button @click="visible = false">取消</el-button>
<el-button type="primary" @click="onSubmit()"></el-button>
</div>
</template>
</el-dialog>
</template>
<script setup>
import { ElMessage } from 'element-plus'
import { Debounce } from '@/utils/debounce'
import { ref, defineEmits, defineExpose } from 'vue'
//
const emit = defineEmits(['refreshDataList'])
// /
const visible = ref(false)
//
const dataList = ref([{ propId: 0, propName: '', prodPropValues: [{ valueId: 0 }] }])
// 使
const page = {
total: 0, //
currentPage: 1, //
pageSize: 10 //
}
/**
* 初始化方法接受可选的产品属性对象作为参数
* @param val - 产品属性对象如果存在则为编辑模式
*/
const init = (val) => {
if (val) {
dataList.value = [JSON.parse(JSON.stringify(val))]
dataList.value = [JSON.parse(JSON.stringify(val))] //
} else {
dataList.value = [
{ propId: 0, propName: '', prodPropValues: [{ valueId: 0 }] }
@ -103,10 +117,10 @@ defineExpose({ init })
* 表单提交
*/
const onSubmit = Debounce(() => {
//
if (dataList.value[0].prodPropValues) {
const temp = []
for (const key in dataList.value[0].prodPropValues) {
// eslint-disable-next-line no-prototype-builtins
if (dataList.value[0].prodPropValues.hasOwnProperty(key)) {
const element = dataList.value[0].prodPropValues[key]
if (element.propValue) {
@ -116,13 +130,13 @@ const onSubmit = Debounce(() => {
}
dataList.value[0].prodPropValues = temp
}
//
if (!dataList.value[0].propName.trim()) {
dataList.value[0].propName = ''
ElMessage.error('属性名不能为空')
return
}
if (dataList.value[0].prodPropValues.length < 1) {
dataList.value[0].prodPropValues = [{ valueId: 0 }]
ElMessage.error('规格项不能为空')
return
}
@ -138,6 +152,8 @@ const onSubmit = Debounce(() => {
ElMessage.error('属性值长度不能大于20')
return
}
// HTTPID
http({
url: http.adornUrl('/prod/spec'),
method: dataList.value[0].propId ? 'put' : 'post',
@ -160,6 +176,9 @@ const onSubmit = Debounce(() => {
})
})
/**
* 清除属性值时的处理逻辑
*/
const clearProdPropValues = () => {
if (dataList.value[0].prodPropValues.length === 1) {
return
@ -172,10 +191,13 @@ const clearProdPropValues = () => {
}
}
/**
* 添加新的属性值输入框
*/
const addInput = () => {
const temp = dataList.value[0].prodPropValues
if (temp[temp.length - 1].propValue) {
temp.push({})
temp.push({}) //
}
}
</script>

@ -1,5 +1,6 @@
<template>
<div class="mod-prod">
<!-- 使用avue-crud构建CRUD表格 -->
<avue-crud
ref="crudRef"
:page="page"
@ -9,31 +10,31 @@
@search-change="onSearch"
@on-load="getDataList"
>
<!-- 自定义属性值列内容 -->
<template #prodPropValues="scope">
<el-tag
v-for="item in scope.row.prodPropValues"
:key="item.valueId"
>
<el-tag v-for="item in scope.row.prodPropValues" :key="item.valueId">
{{ item.propValue }}
</el-tag>
</template>
<!-- 自定义左侧菜单按钮 -->
<template #menu-left>
<el-button
v-if="isAuth('shop:pickAddr:save')"
type="primary"
icon="el-icon-plus"
@click.stop="onAddOrUpdate()"
>
新增
</el-button>
</template>
<!-- 自定义行操作菜单 -->
<template #menu="scope">
<el-button
v-if="isAuth('prod:spec:update')"
type="primary"
icon="el-icon-edit"
@click.stop="onAddOrUpdate(scope.row)"
>
编辑
@ -43,14 +44,14 @@
v-if="isAuth('prod:spec:delete')"
type="danger"
icon="el-icon-delete"
@click.stop="onDelete(scope.row.propId)"
>
删除
</el-button>
</template>
</avue-crud>
<!-- 弹窗, 新增 / 修改 -->
<!-- 引入用于添加或更新记录的子组件 -->
<add-or-update
v-if="addOrUpdateVisible"
ref="addOrUpdateRef"
@ -64,19 +65,31 @@ import { isAuth } from '@/utils'
import { ElMessage, ElMessageBox } from 'element-plus'
import AddOrUpdate from './add-or-update.vue'
import { tableOption } from '@/crud/prod/spec.js'
import { ref, reactive, nextTick } from 'vue'
//
const permission = ref({
delBtn: isAuth('prod:prod:delete')
delBtn: isAuth('prod:prod:delete') //
})
//
const dataList = ref([])
//
const dataListLoading = ref(false)
//
const dataListSelections = ref([])
//
const page = reactive({
total: 0, //
currentPage: 1, //
pageSize: 10 //
total: 0, //
currentPage: 1, //
pageSize: 10 //
})
/**
* 获取数据列表
* 获取数据列表方法
*/
const getDataList = (pageParam, params, done) => {
dataListLoading.value = true
@ -101,11 +114,13 @@ const getDataList = (pageParam, params, done) => {
})
}
// /
const addOrUpdateVisible = ref(false)
const addOrUpdateRef = ref(null)
/**
* 新增 / 修改
* @param val
* 新增或编辑产品规格
* @param val - 如果存在则为编辑模式否则为新增模式
*/
const onAddOrUpdate = (val) => {
addOrUpdateVisible.value = true
@ -115,13 +130,11 @@ const onAddOrUpdate = (val) => {
}
/**
* 删除
* @param id
* 删除产品规格
* @param id - 规格ID如果不存在则表示批量删除
*/
const onDelete = (id) => {
const ids = id ? [id] : dataListSelections.value?.map(item => {
return item.propId
})
const ids = id ? [id] : dataListSelections.value?.map(item => item.propId)
ElMessageBox.confirm(`确定进行[${id ? '删除' : '批量删除'}]操作?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
@ -144,13 +157,17 @@ const onDelete = (id) => {
})
})
})
.catch(() => { })
.catch(() => {})
}
/**
* 条件查询触发的数据获取
* @param params - 查询参数
* @param done - 完成回调
*/
const onSearch = (params, done) => {
getDataList(page, params, done)
}
</script>
<style scoped lang="scss">

@ -1,10 +1,12 @@
<template>
<div class="mod-hotSearch-add-or-update">
<!-- 对话框 -->
<el-dialog
v-model="visible"
:title="!dataForm.hotSearchId ? '新增' : '修改'"
:close-on-click-modal="false"
>
<!-- 表单 -->
<el-form
ref="dataFormRef"
:model="dataForm"
@ -12,10 +14,8 @@
label-width="80px"
@keyup.enter="onSubmit()"
>
<el-form-item
label="标题"
prop="title"
>
<!-- 标题输入框 -->
<el-form-item label="标题" prop="title">
<el-input
v-model="dataForm.title"
controls-position="right"
@ -26,10 +26,8 @@
/>
</el-form-item>
<el-form-item
label="内容"
prop="content"
>
<!-- 内容文本区域 -->
<el-form-item label="内容" prop="content">
<el-input
v-model="dataForm.content"
controls-position="right"
@ -40,10 +38,9 @@
label="内容"
/>
</el-form-item>
<el-form-item
label="排序号"
prop="seq"
>
<!-- 排序号输入框 -->
<el-form-item label="排序号" prop="seq">
<el-input-number
v-model="dataForm.seq"
controls-position="right"
@ -51,31 +48,21 @@
label="排序号"
/>
</el-form-item>
<el-form-item
label="状态"
prop="status"
>
<!-- 状态选择 -->
<el-form-item label="状态" prop="status">
<el-radio-group v-model="dataForm.status">
<el-radio :label="0">
下线
</el-radio>
<el-radio :label="1">
正常
</el-radio>
<el-radio :label="0">下线</el-radio>
<el-radio :label="1">正常</el-radio>
</el-radio-group>
</el-form-item>
</el-form>
<!-- 对话框底部按钮 -->
<template #footer>
<div class="dialog-footer">
<el-button @click="visible = false">
取消
</el-button>
<el-button
type="primary"
@click="onSubmit()"
>
确定
</el-button>
<el-button @click="visible = false">取消</el-button>
<el-button type="primary" @click="onSubmit()"></el-button>
</div>
</template>
</el-dialog>
@ -85,7 +72,12 @@
<script setup>
import { ElMessage } from 'element-plus'
import { Debounce } from '@/utils/debounce'
import { ref, reactive, nextTick, defineEmits, defineExpose } from 'vue'
//
const emit = defineEmits(['refreshDataList'])
//
const dataForm = ref({
hotSearchId: 0,
title: '',
@ -94,12 +86,18 @@ const dataForm = ref({
seq: 0,
status: 0
})
// 使
const page = reactive({
total: 0, //
currentPage: 1, //
pageSize: 10 //
})
// /
const visible = ref(false)
//
const dataRule = {
title: [
{ required: true, message: '标题不能为空', trigger: 'blur' },
@ -113,7 +111,13 @@ const dataRule = {
]
}
//
const dataFormRef = ref(null)
/**
* 初始化方法接受可选的ID作为参数
* @param id - 热门搜索条目ID如果存在则为编辑模式
*/
const init = (id) => {
dataForm.value.hotSearchId = id || 0
visible.value = true
@ -159,5 +163,4 @@ const onSubmit = Debounce(() => {
}
})
})
</script>

@ -1,5 +1,6 @@
<template>
<div class="mod-hotSearcch">
<!-- 使用avue-crud构建CRUD表格 -->
<avue-crud
ref="crudRef"
:page="page"
@ -11,6 +12,7 @@
@refresh-change="refreshChange"
@selection-change="selectionChange"
>
<!-- 自定义左侧菜单按钮 -->
<template #menu-left>
<el-button
v-if="isAuth('admin:hotSearch:save')"
@ -29,18 +31,13 @@
</el-button>
</template>
<!-- 自定义状态列内容 -->
<template #status="scope">
<el-tag
v-if="scope.row.status === 0"
type="danger"
>
未启用
</el-tag>
<el-tag v-else>
启用
</el-tag>
<el-tag v-if="scope.row.status === 0" type="danger"></el-tag>
<el-tag v-else></el-tag>
</template>
<!-- 自定义行操作菜单 -->
<template #menu="scope">
<el-button
v-if="isAuth('admin:hotSearch:update')"
@ -54,14 +51,14 @@
v-if="isAuth('admin:hotSearch:deconste')"
type="danger"
icon="el-icon-deconste"
@click.stop="onDeconste(scope.row,scope.index)"
@click.stop="onDeconste(scope.row, scope.index)"
>
删除
</el-button>
</template>
</avue-crud>
<!-- 弹窗, 新增 / 修改 -->
<!-- 引入用于添加或更新记录的子组件 -->
<add-or-update
v-if="addOrUpdateVisible"
ref="addOrUpdateRef"
@ -75,15 +72,23 @@ import { isAuth } from '@/utils'
import { ElMessage, ElMessageBox } from 'element-plus'
import { tableOption } from '@/crud/shop/hotSearch.js'
import AddOrUpdate from './add-or-update.vue'
import { ref, reactive, nextTick } from 'vue'
//
const dataList = ref([])
//
const page = reactive({
total: 0, //
currentPage: 1, //
pageSize: 10 //
})
//
const dataListLoading = ref(false)
/**
* 获取数据列表
* 获取数据列表方法
*/
const getDataList = (pageParam, params, done) => {
dataListLoading.value = true
@ -104,20 +109,25 @@ const getDataList = (pageParam, params, done) => {
if (done) done()
})
}
//
const dataListSelections = ref([])
/**
* 多选回调
* @param list
* @param list - 用户选择的数据列表
*/
const selectionChange = (list) => {
dataListSelections.value = list
}
// /
const addOrUpdateVisible = ref(false)
const addOrUpdateRef = ref(null)
/**
* 新增 / 修改
* @param id
* 新增或修改热门搜索条目
* @param id - 如果存在则为编辑模式否则为新增模式
*/
const onAddOrUpdate = (id) => {
addOrUpdateVisible.value = true
@ -127,19 +137,20 @@ const onAddOrUpdate = (id) => {
}
/**
* 点击查询
* 条件查询触发的数据获取
* @param params - 查询参数
* @param done - 完成回调
*/
const onSearch = (params, done) => {
getDataList(page, params, done)
}
/**
* 删除
* 删除单个或多个热门搜索条目
* @param row - 如果传入row则删除单个条目否则批量删除选中项
*/
const onDeconste = (row) => {
const ids = row.hotSearchId ? [row.hotSearchId] : dataListSelections.value?.map(item => {
return item.hotSearchId
})
const ids = row.hotSearchId ? [row.hotSearchId] : dataListSelections.value?.map(item => item.hotSearchId)
ElMessageBox.confirm(`确定进行[${row.hotSearchId ? '删除' : '批量删除'}]操作?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
@ -161,10 +172,13 @@ const onDeconste = (row) => {
}
})
})
}).catch(() => { })
}).catch(() => {})
}
/**
* 刷新页面数据
*/
const refreshChange = () => {
getDataList(page)
}
</script>

@ -4,6 +4,7 @@
:title="!dataForm.id ? '新增' : '修改'"
:close-on-click-modal="false"
>
<!-- 表单 -->
<el-form
ref="dataFormRef"
:model="dataForm"
@ -11,58 +12,38 @@
label-width="80px"
@keyup.enter="onSubmit()"
>
<el-form-item
label="公告标题"
prop="title"
>
<!-- 公告标题 -->
<el-form-item label="公告标题" prop="title">
<el-input v-model="dataForm.title" />
</el-form-item>
<el-form-item
label="状态"
prop="status"
>
<!-- 状态选择 -->
<el-form-item label="状态" prop="status">
<el-radio-group v-model="dataForm.status">
<el-radio :label="1">
公布
</el-radio>
<el-radio :label="0">
撤销
</el-radio>
<el-radio :label="1">公布</el-radio>
<el-radio :label="0">撤销</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item
label="置顶"
prop="isTop"
>
<!-- 置顶选项 -->
<el-form-item label="置顶" prop="isTop">
<el-radio-group v-model="dataForm.isTop">
<el-radio :label="1">
</el-radio>
<el-radio :label="0">
</el-radio>
<el-radio :label="1"></el-radio>
<el-radio :label="0"></el-radio>
</el-radio-group>
</el-form-item>
<el-form-item
label="公告内容"
prop="content"
>
<TinyMce
ref="contentEnRef"
v-model="dataForm.content"
/>
<!-- 公告内容 -->
<el-form-item label="公告内容" prop="content">
<TinyMce ref="contentEnRef" v-model="dataForm.content" />
</el-form-item>
</el-form>
<!-- 对话框底部按钮 -->
<template #footer>
<span class="dialog-footer">
<el-button
@click="visible = false"
>取消</el-button>
<el-button
type="primary"
@click="onSubmit()"
>确定</el-button>
<el-button @click="visible = false">取消</el-button>
<el-button type="primary" @click="onSubmit()"></el-button>
</span>
</template>
</el-dialog>
@ -71,11 +52,20 @@
<script setup>
import { ElMessage } from 'element-plus'
import { Debounce } from '@/utils/debounce'
import { ref, nextTick, defineEmits, defineExpose } from 'vue'
//
const emit = defineEmits(['refreshDataList'])
// /
const visible = ref(false)
// TinyMce
const contentEnRef = ref(null)
// eslint-disable-next-line no-unused-vars
/**
* 自定义验证规则确保公告标题不为空白字符
*/
const validateTitle = (rule, value, callback) => {
if (!value.trim()) {
dataForm.value.title = ''
@ -84,6 +74,8 @@ const validateTitle = (rule, value, callback) => {
callback()
}
}
//
const dataRule = {
title: [
{ required: true, message: '公告标题不能为空', trigger: 'blur' },
@ -91,6 +83,7 @@ const dataRule = {
]
}
//
const dataForm = ref({
title: null,
content: null,
@ -98,7 +91,14 @@ const dataForm = ref({
status: 1,
isTop: 0
})
//
const dataFormRef = ref(null)
/**
* 初始化方法接受可选的ID作为参数
* @param id - 公告条目ID如果存在则为编辑模式
*/
const init = (id) => {
dataForm.value.id = id || 0
visible.value = true
@ -144,5 +144,4 @@ const onSubmit = Debounce(() => {
}
})
})
</script>

@ -1,5 +1,6 @@
<template>
<div class="mod-shop-notice">
<!-- 使用avue-crud构建CRUD表格 -->
<avue-crud
ref="crudRef"
:page="page"
@ -10,25 +11,19 @@
@on-load="getDataList"
@refresh-change="refreshChange"
>
<!-- 自定义状态列内容 -->
<template #status="scope">
<el-tag
v-if="scope.row.status === 0"
type="danger"
>
撤销
</el-tag>
<el-tag v-else>
公布
</el-tag>
<el-tag v-if="scope.row.status === 0" type="danger"></el-tag>
<el-tag v-else></el-tag>
</template>
<!-- 自定义置顶列内容 -->
<template #isTop="scope">
<el-tag v-if="scope.row.isTop === 0">
</el-tag>
<el-tag v-else>
</el-tag>
<el-tag v-if="scope.row.isTop === 0"></el-tag>
<el-tag v-else></el-tag>
</template>
<!-- 自定义左侧菜单按钮 -->
<template #menu-left>
<el-button
v-if="isAuth('shop:notice:save')"
@ -39,6 +34,8 @@
新增
</el-button>
</template>
<!-- 自定义行操作菜单 -->
<template #menu="scope">
<el-button
v-if="isAuth('shop:notice:update')"
@ -58,6 +55,8 @@
</el-button>
</template>
</avue-crud>
<!-- 引入用于添加或更新记录的子组件 -->
<add-or-update
v-if="addOrUpdateVisible"
ref="addOrUpdateRef"
@ -72,16 +71,25 @@ import { isAuth } from '@/utils'
import { ElMessage, ElMessageBox } from 'element-plus'
import { tableOption } from '@/crud/shop/notice'
import AddOrUpdate from './add-or-update.vue'
import { ref, reactive, nextTick } from 'vue'
//
const dataList = ref([])
//
const page = reactive({
total: 0, //
currentPage: 1, //
pageSize: 10 //
})
//
const dataListLoading = ref(false)
// /
const addOrUpdateVisible = ref(false)
//
const getDataList = (pageParam, params, done) => {
dataListLoading.value = true
http({
@ -100,10 +108,12 @@ const getDataList = (pageParam, params, done) => {
})
}
//
const addOrUpdateRef = ref(null)
/**
* 新增 / 修改
* @param id
* @param id - 如果存在则为编辑模式否则为新增模式
*/
const onAddOrUpdate = (id) => {
addOrUpdateVisible.value = true
@ -112,6 +122,10 @@ const onAddOrUpdate = (id) => {
})
}
/**
* 删除公告条目
* @param id - 公告条目的ID
*/
const onDelete = (id) => {
ElMessageBox.confirm('确定进行删除操作?', '提示', {
confirmButtonText: '确定',
@ -134,7 +148,7 @@ const onDelete = (id) => {
}
})
})
}).catch(() => { })
}).catch(() => {})
}
/**
@ -144,10 +158,12 @@ const refreshChange = () => {
getDataList(page)
}
/**
* 条件查询触发的数据获取
* @param params - 查询参数
* @param done - 完成回调
*/
const onSearch = (params, done) => {
getDataList(page, params, done)
}
</script>
<style lang="scss" scoped>
</style>

@ -4,6 +4,7 @@
:title="!dataForm.addrId ? '新增' : '修改'"
:close-on-click-modal="false"
>
<!-- 表单 -->
<el-form
ref="dataFormRef"
:model="dataForm"
@ -11,15 +12,12 @@
label-width="80px"
@keyup.enter="onSubmit()"
>
<el-form-item
label="名称"
prop="addrName"
>
<el-input
v-model="dataForm.addrName"
placeholder="自提点名称"
/>
<!-- 自提点名称 -->
<el-form-item label="名称" prop="addrName">
<el-input v-model="dataForm.addrName" placeholder="自提点名称" />
</el-form-item>
<!-- 省份城市区县选择 -->
<el-form-item label="省份">
<el-col :span="8">
<el-form-item prop="province">
@ -55,10 +53,7 @@
</el-col>
<el-col :span="8">
<el-form-item prop="area">
<el-select
v-model="dataForm.areaId"
placeholder="请选择"
>
<el-select v-model="dataForm.areaId" placeholder="请选择">
<el-option
v-for="area in areaList"
:key="area.areaId"
@ -69,35 +64,23 @@
</el-form-item>
</el-col>
</el-form-item>
<el-form-item
label="地址"
prop="addr"
>
<el-input
v-model="dataForm.addr"
placeholder="地址"
/>
<!-- 地址 -->
<el-form-item label="地址" prop="addr">
<el-input v-model="dataForm.addr" placeholder="地址" />
</el-form-item>
<el-form-item
label="手机号"
prop="mobile"
>
<el-input
v-model="dataForm.mobile"
maxlength="11"
placeholder="手机号"
/>
<!-- 手机号 -->
<el-form-item label="手机号" prop="mobile">
<el-input v-model="dataForm.mobile" maxlength="11" placeholder="手机号" />
</el-form-item>
</el-form>
<!-- 对话框底部按钮 -->
<template #footer>
<span
class="dialog-footer"
>
<span class="dialog-footer">
<el-button @click="visible = false">取消</el-button>
<el-button
type="primary"
@click="onSubmit()"
>确定</el-button>
<el-button type="primary" @click="onSubmit()"></el-button>
</span>
</template>
</el-dialog>
@ -107,9 +90,15 @@
import { ElMessage } from 'element-plus'
import { isMobile } from '@/utils/validate'
import { Debounce } from '@/utils/debounce'
import { ref, reactive, nextTick, defineEmits, defineExpose } from 'vue'
//
const emit = defineEmits(['refreshDataList'])
// /
const visible = ref(false)
// eslint-disable-next-line no-unused-vars
//
const validateMobile = (rule, value, callback) => {
if (!isMobile(value)) {
callback(new Error('手机号格式错误'))
@ -117,6 +106,8 @@ const validateMobile = (rule, value, callback) => {
callback()
}
}
//
const dataRule = {
addrName: [
{ required: true, message: '自提点名称不能为空', trigger: 'blur' },
@ -136,10 +127,20 @@ const dataRule = {
{ validator: validateMobile, trigger: 'blur' }
]
}
//
const provinceList = ref([])
//
const dataFormRef = ref(null)
//
const cityList = ref([])
// /
const areaList = ref([])
//
const dataForm = reactive({
addrId: 0,
addr: '',
@ -152,11 +153,18 @@ const dataForm = reactive({
cityId: null,
provinceId: null
})
//
const page = reactive({
total: 0, //
currentPage: 1, //
pageSize: 10 //
})
/**
* 初始化方法接受可选的ID作为参数
* @param id - 自提点地址条目ID如果存在则为编辑模式
*/
const init = (id) => {
dataForm.addrId = id || 0
visible.value = true
@ -173,9 +181,7 @@ const init = (id) => {
})
if (dataForm.addrId) {
http({
url: http.adornUrl(
`/shop/pickAddr/info/${dataForm.addrId}`
),
url: http.adornUrl(`/shop/pickAddr/info/${dataForm.addrId}`),
method: 'get',
params: http.adornParams()
})
@ -197,6 +203,10 @@ const init = (id) => {
}
defineExpose({ init })
/**
* 根据父级ID获取地区列表
* @param pid - 父级地区ID默认为0表示获取省级列表
*/
const listAreaByParentId = (pid) => {
if (!pid) pid = 0
return http({
@ -207,26 +217,26 @@ const listAreaByParentId = (pid) => {
}
/**
* 选择省
* @param val
* 选择省时触发
* @param val - 省ID
*/
const selectProvince = (val) => {
dataForm.cityId = null
dataForm.city = ''
// select
dataForm.areaId = null
dataForm.area = ''
listAreaByParentId(val).then(({ data }) => {
cityList.value = data
})
}
/**
* 选择市
* @param val
* 选择市时触发
* @param val - 市ID
*/
const selectCity = (val) => {
dataForm.areaId = null
dataForm.area = ''
// select
listAreaByParentId(val).then(({ data }) => {
areaList.value = data
})
@ -236,24 +246,23 @@ const selectCity = (val) => {
* 表单提交
*/
const onSubmit = Debounce(() => {
//
for (let i = 0; i < provinceList.value.length; i++) {
if (provinceList.value[i].areaId === dataForm.provinceId) {
//
dataForm.province = provinceList.value[i].areaName
}
}
for (let i = 0; i < cityList.value.length; i++) {
if (cityList.value[i].areaId === dataForm.cityId) {
//
dataForm.city = cityList.value[i].areaName
}
}
for (let i = 0; i < areaList.value.length; i++) {
if (areaList.value[i].areaId === dataForm.areaId) {
//
dataForm.area = areaList.value[i].areaName
}
}
dataFormRef.value?.validate(valid => {
if (valid) {
http({

@ -1,5 +1,6 @@
<template>
<div class="mod-pickAddr">
<!-- 使用avue-crud构建CRUD表格 -->
<avue-crud
ref="crudRef"
:page="page"
@ -10,6 +11,7 @@
@selection-change="selectionChange"
@on-load="getDataList"
>
<!-- 自定义左侧菜单按钮 -->
<template #menu-left>
<el-button
v-if="isAuth('shop:pickAddr:save')"
@ -30,6 +32,7 @@
</el-button>
</template>
<!-- 自定义行操作菜单 -->
<template #menu="scope">
<el-button
v-if="isAuth('shop:pickAddr:update')"
@ -65,18 +68,28 @@ import { isAuth } from '@/utils'
import { ElMessage, ElMessageBox } from 'element-plus'
import AddOrUpdate from './add-or-update.vue'
import { tableOption } from '@/crud/shop/pickAddr.js'
import { ref, reactive, nextTick } from 'vue'
//
const permission = {
delBtn: isAuth('prod:prod:delete')
}
//
const dataList = ref([])
//
const dataListSelections = ref([])
//
const page = reactive({
total: 0, //
currentPage: 1, //
pageSize: 10 //
})
/**
* 获取数据列表
* 获取数据列表方法
*/
const getDataList = (pageParam, params, done) => {
http({
@ -99,11 +112,13 @@ const getDataList = (pageParam, params, done) => {
})
}
// /
const addOrUpdateVisible = ref(false)
const addOrUpdateRef = ref(null)
/**
* 新增 / 修改
* @param id
* @param id - 如果存在则为编辑模式否则为新增模式
*/
const onAddOrUpdate = (id) => {
addOrUpdateVisible.value = true
@ -113,13 +128,11 @@ const onAddOrUpdate = (id) => {
}
/**
* 删除
* @param id
* 删除单个或多个自提点地址
* @param id - 单个自提点地址ID若未提供则执行批量删除
*/
const onDelete = (id) => {
const ids = id ? [id] : dataListSelections.value?.map(item => {
return item.addrId
})
const ids = id ? [id] : dataListSelections.value?.map(item => item.addrId)
ElMessageBox.confirm(
'确定进行删除操作?', '提示',
{
@ -145,20 +158,21 @@ const onDelete = (id) => {
})
})
})
.catch(() => { })
.catch(() => {})
}
/**
* 条件查询
* @param params
* @param done
* 条件查询触发的数据获取
* @param params - 查询参数
* @param done - 完成回调
*/
const onSearch = (params, done) => {
getDataList(page, params, done)
}
/**
* 多选变化
* @param val
* 多选变化时触发
* @param val - 当前选择的数据项数组
*/
const selectionChange = (val) => {
dataListSelections.value = val

@ -1,326 +1,142 @@
<template>
<!-- 使用Element Plus的el-dialog组件创建一个对话框 -->
<el-dialog
v-model="visible"
:title="!dataForm.transportId ? '新增' : '修改'"
:close-on-click-modal="false"
width="1400px"
class="transport-dialog"
:title="!dataForm.transportId ? '新增' : '修改'"
:close-on-click-modal="false"
width="1400px"
class="transport-dialog"
>
<el-form
ref="dataFormRef"
:model="dataForm"
label-width="80px"
@keyup.enter="onSubmit()"
>
<el-form-item
label="模板名称"
prop="transName"
:rules="[
<!-- 表单元素包含多个表单项和一个表格 -->
<el-form
ref="dataFormRef"
:model="dataForm"
label-width="80px"
@keyup.enter="onSubmit()"
>
<!-- 模板名称输入框 -->
<el-form-item
label="模板名称"
prop="transName"
:rules="[
{ required: true, message: '模板名称不能为空'},
{ pattern: /\s\S+|S+\s|\S/, message: '请输入正确的模板名称', trigger: 'blur' }
]"
>
<el-input
v-model="dataForm.transName"
placeholder="模板名称"
/>
</el-form-item>
<el-form-item
label="模板类型"
prop="isFreeFee"
required="required"
>
<el-radio-group
v-model="dataForm.isFreeFee"
@change="changeFreeFee"
>
<el-radio :label="0">
买家承担运费
</el-radio>
<el-radio :label="1">
卖家包邮
</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item
label="收费方式"
prop="chargeType"
>
<el-radio-group
v-model="dataForm.chargeType"
:disabled="dataForm.isFreeFee == 1"
>
<el-radio :label="0">
按件数
</el-radio>
<el-radio :label="1">
按重量
</el-radio>
<el-radio :label="2">
按体积
</el-radio>
</el-radio-group>
</el-form-item>
<el-table
:data="dataForm.transfees"
border
style="width: 100%;"
class="table-con"
>
<el-table-column
header-align="center"
align="center"
width="450"
label="可配送区域"
>
<template #default="scope">
<span v-if="scope.$index == 0"></span>
<span v-if="(!scope.row.cityList || !scope.row.cityList.length) && scope.$index > 0"></span>
<span v-if="scope.$index > 0">
<el-tag
v-for="city in scope.row.cityList"
:key="city.areaId"
>{{ city.areaName }}</el-tag>
{ pattern: /\s\S+|S+\s|\S/, message: '请输入正确的模板名称', trigger: 'blur' }]"
>
<el-input v-model="dataForm.transName" placeholder="模板名称" />
</el-form-item>
<!-- 模板类型选择器 -->
<el-form-item label="模板类型" prop="isFreeFee" required>
<el-radio-group v-model="dataForm.isFreeFee" @change="changeFreeFee">
<el-radio :label="0">买家承担运费</el-radio>
<el-radio :label="1">卖家包邮</el-radio>
</el-radio-group>
</el-form-item>
<!-- 收费方式选择器 -->
<el-form-item label="收费方式" prop="chargeType">
<el-radio-group v-model="dataForm.chargeType" :disabled="dataForm.isFreeFee == 1">
<el-radio :label="0">按件数</el-radio>
<el-radio :label="1">按重量</el-radio>
<el-radio :label="2">按体积</el-radio>
</el-radio-group>
</el-form-item>
<!-- 运费项表格 -->
<el-table
:data="dataForm.transfees"
border
style="width: 100%;"
class="table-con"
>
<!-- 可配送区域列 -->
<el-table-column header-align="center" align="center" width="450" label="可配送区域">
<template #default="scope">
<span v-if="scope.$index == 0"></span>
<span v-if="(!scope.row.cityList || !scope.row.cityList.length) && scope.$index > 0"></span>
<span v-if="scope.$index > 0">
<el-tag v-for="city in scope.row.cityList" :key="city.areaId">{{ city.areaName }}</el-tag>
</span>
<el-button
v-if="isAuth('shop:transfee:update') && scope.$index > 0"
type="text"
@click="onAddOrUpdate(`${scope.$index}`)"
>
编辑
</el-button>
<el-button
v-if="isAuth('shop:transfee:delete') && scope.$index > 0"
type="text"
@click="onDelete(`${scope.$index}`)"
>
删除
</el-button>
</template>
</el-table-column>
<el-table-column
header-align="center"
align="center"
width="180"
:label="tableTitle[0]"
>
<template #default="scope">
<el-form-item
:prop="`transfees.${scope.$index}.firstPiece`"
label-width="0px"
:rules="[{ required: true, message: `${tableTitle[0]}不能为空`}]"
>
<el-input
v-model="scope.row.firstPiece"
type="number"
:disabled="!scope.row.status && scope.$index === 0"
@change="checkNumber(scope.row, 1)"
/>
</el-form-item>
</template>
</el-table-column>
<el-table-column
header-align="center"
align="center"
:label="tableTitle[1]"
>
<template #default="scope">
<el-form-item
:prop="`transfees.${scope.$index}.firstFee`"
label-width="0px"
:rules="[{ required: true, message: `${tableTitle[1]}不能为空`}]"
>
<el-input
v-model="scope.row.firstFee"
type="number"
:min="0"
:disabled="!scope.row.status && scope.$index === 0"
/>
</el-form-item>
</template>
</el-table-column>
<el-table-column
header-align="center"
align="center"
:label="tableTitle[2]"
>
<template #default="scope">
<el-form-item
:prop="`transfees.${scope.$index}.continuousPiece`"
label-width="0px"
:rules="[{ required: true, message: `${tableTitle[2]}不能为空`}]"
>
<el-input
v-model="scope.row.continuousPiece"
type="number"
:disabled="!scope.row.status && scope.$index === 0"
@change="checkNumber(scope.row, 3)"
/>
</el-form-item>
</template>
</el-table-column>
<el-table-column
header-align="center"
align="center"
:label="tableTitle[3]"
>
<template #default="scope">
<el-form-item
:prop="`transfees.${scope.$index}.continuousFee`"
label-width="0px"
:rules="[{ required: true, message: `${tableTitle[3]}不能为空`}]"
>
<el-input
v-model="scope.row.continuousFee"
type="number"
:min="0"
:disabled="!scope.row.status && scope.$index === 0"
/>
</el-form-item>
</template>
</el-table-column>
</el-table>
<div
v-if="dataForm.isFreeFee == 0"
style="margin-top: 20px"
>
<el-button
type="primary"
icon="el-icon-location-outline"
@click="addTransfee()"
>
点击添加可配送的区域和运费
</el-button>
</div>
<el-checkbox
v-if="!dataForm.isFreeFee"
v-model="dataForm.hasFreeCondition"
style="margin-top:10px;font-size:50px"
>
指定条件包邮
</el-checkbox>
<el-table
v-if="dataForm.hasFreeCondition && !dataForm.isFreeFee"
:data="dataForm.transfeeFrees"
border
style="width: 100%;"
>
<el-table-column
header-align="center"
align="center"
width="350"
label="指定区域"
>
<template #default="scope">
<span v-if="!scope.row.freeCityList || !scope.row.freeCityList.length"></span>
<el-tag
v-for="city in scope.row.freeCityList"
:key="city.areaId"
>
{{ city.areaName }}
</el-tag>
<el-button
v-if="isAuth('shop:transfee:update')"
type="text"
@click="addOrUpdateTransfeeFree(`${scope.$index}`)"
>
编辑
</el-button>
<el-button
v-if="isAuth('shop:transfee:delete')"
type="text"
@click="deleteTransfeeFree(`${scope.$index}`)"
>
删除
</el-button>
</template>
</el-table-column>
<el-table-column
header-align="center"
align="center"
width="600"
label="设置包邮条件"
>
<template #default="scope">
<el-radio-group v-model="scope.row.freeType">
<el-radio :label="0">
满件/重量/体积包邮
</el-radio>
<el-radio :label="1">
满金额包邮
</el-radio>
<el-radio :label="2">
满件/重量/体积且满金额包邮
</el-radio>
</el-radio-group>
</template>
</el-table-column>
<el-table-column
header-align="center"
align="left"
>
<template #default="scope">
<el-form-item
v-if="scope.row.freeType == 1 || scope.row.freeType == 2"
:prop="`transfeeFrees.${scope.$index}.amount`"
label-width="0px"
:rules="[{ required: true, message: `不能为空`}]"
>
<el-input
v-model="scope.row.amount"
style="width:100px"
/>
</el-form-item>
<el-form-item
v-if="scope.row.freeType == 0 || scope.row.freeType == 2"
:prop="`transfeeFrees.${scope.$index}.piece`"
label-width="0px"
:rules="[{ required: true, message: `不能为空`}]"
>
<el-input
v-model="scope.row.piece"
style="width:100px"
/> //
</el-form-item>
</template>
</el-table-column>
</el-table>
<div
v-if="dataForm.isFreeFee == 0"
style="margin-top: 20px"
>
<el-button
type="primary"
icon="el-icon-location-outline"
@click="addTransfeeFree()"
>
点击添加指定包邮条件
</el-button>
</div>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="visible = false">
取消
</el-button>
<el-button
type="primary"
@click="onSubmit()"
>
确定
</el-button>
</div>
<el-button v-if="isAuth('shop:transfee:update') && scope.$index > 0" type="text" @click="onAddOrUpdate(`${scope.$index}`)"></el-button>
<el-button v-if="isAuth('shop:transfee:delete') && scope.$index > 0" type="text" @click="onDelete(`${scope.$index}`)"></el-button>
</template>
<!-- 弹窗, 新增 / 修改 -->
<add-or-update
v-if="addOrUpdateVisible"
ref="addOrUpdateRef"
@refresh-data-list="getDataList"
/>
</el-table-column>
<!-- 动态生成的列根据chargeType显示不同的标题 -->
<el-table-column v-for="(item, index) in tableTitle" :key="index" header-align="center" align="center" :label="item">
<template #default="scope">
<el-form-item :prop="`transfees.${scope.$index}.[${index}]`" label-width="0px" :rules="[{ required: true, message: `${item}不能为空`}]">
<el-input v-model="scope.row[index]" type="number" :min="0" :disabled="!scope.row.status && scope.$index === 0" @change="checkNumber(scope.row, index + 1)" />
</el-form-item>
</template>
</el-table-column>
</el-table>
<!-- 添加新的运费项按钮 -->
<div v-if="dataForm.isFreeFee == 0" style="margin-top: 20px">
<el-button type="primary" icon="el-icon-location-outline" @click="addTransfee()"></el-button>
</div>
<!-- 指定条件包邮复选框 -->
<el-checkbox v-if="!dataForm.isFreeFee" v-model="dataForm.hasFreeCondition" style="margin-top:10px;font-size:50px"></el-checkbox>
<!-- 包邮条件表格 -->
<el-table
v-if="dataForm.hasFreeCondition && !dataForm.isFreeFee"
:data="dataForm.transfeeFrees"
border
style="width: 100%;"
>
<!-- 指定区域列 -->
<el-table-column header-align="center" align="center" width="350" label="指定区域">
<template #default="scope">
<span v-if="!scope.row.freeCityList || !scope.row.freeCityList.length"></span>
<el-tag v-for="city in scope.row.freeCityList" :key="city.areaId">{{ city.areaName }}</el-tag>
<el-button v-if="isAuth('shop:transfee:update')" type="text" @click="addOrUpdateTransfeeFree(`${scope.$index}`)"></el-button>
<el-button v-if="isAuth('shop:transfee:delete')" type="text" @click="deleteTransfeeFree(`${scope.$index}`)"></el-button>
</template>
</el-table-column>
<!-- 设置包邮条件列 -->
<el-table-column header-align="center" align="center" width="600" label="设置包邮条件">
<template #default="scope">
<el-radio-group v-model="scope.row.freeType">
<el-radio :label="0">满件/重量/体积包邮</el-radio>
<el-radio :label="1">满金额包邮</el-radio>
<el-radio :label="2">满件/重量/体积且满金额包邮</el-radio>
</el-radio-group>
</template>
</el-table-column>
<!-- 包邮条件详情列 -->
<el-table-column header-align="center" align="left">
<template #default="scope">
<el-form-item v-if="scope.row.freeType == 1 || scope.row.freeType == 2" :prop="`transfeeFrees.${scope.$index}.amount`" label-width="0px" :rules="[{ required: true, message: `不能为空`}]">
<el-input v-model="scope.row.amount" style="width:100px" /> 元包邮
</el-form-item>
<el-form-item v-if="scope.row.freeType == 0 || scope.row.freeType == 2" :prop="`transfeeFrees.${scope.$index}.piece`" label-width="0px" :rules="[{ required: true, message: `不能为空`}]">
<el-input v-model="scope.row.piece" style="width:100px" /> /重量/体积包邮
</el-form-item>
</template>
</el-table-column>
</el-table>
<!-- 添加新的包邮条件按钮 -->
<div v-if="dataForm.isFreeFee == 0" style="margin-top: 20px">
<el-button type="primary" icon="el-icon-location-outline" @click="addTransfeeFree()"></el-button>
</div>
</el-form>
<!-- 对话框底部按钮 -->
<template #footer>
<div class="dialog-footer">
<el-button @click="visible = false">取消</el-button>
<el-button type="primary" @click="onSubmit()"></el-button>
</div>
</template>
<!-- 子组件用于添加或更新城市选择 -->
<add-or-update v-if="addOrUpdateVisible" ref="addOrUpdateRef" @refresh-data-list="getDataList" />
</el-dialog>
</template>
@ -329,10 +145,18 @@ import { isAuth } from '@/utils'
import { ElMessage } from 'element-plus'
import { Debounce } from '@/utils/debounce'
import AddOrUpdate from './add-or-update.vue'
import { ref, reactive, computed, watch, nextTick } from 'vue'
//
const emit = defineEmits(['refreshDataList'])
//
const hasFreeCondition = ref(0)
//
const visible = ref(false)
//
const dataForm = ref({
hasFreeCondition: false,
transName: '',
@ -343,20 +167,28 @@ const dataForm = ref({
transfees: [{ cityList: [], status: 1 }],
transfeeFrees: [{ freeCityList: [], freeType: 0 }]
})
//
const page = reactive({
total: 0, //
currentPage: 1, //
pageSize: 10 //
})
// chargeType
const tableTitle = computed(() => {
const titles = [['首件(个)', '运费(元)', '续件(个)', '续费(元)'], ['首重(kg)', '运费(元)', '续重(kg)', '续费(元)'], ['首体积(m³)', '运费(元)', '续体积(m³)', '续费(元)']]
const titles = [
['首件(个)', '运费(元)', '续件(个)', '续费(元)'],
['首重(kg)', '运费(元)', '续重(kg)', '续费(元)'],
['首体积(m³)', '运费(元)', '续体积(m³)', '续费(元)']
]
if (dataForm.value.chargeType) {
return titles[dataForm.value.chargeType]
}
return titles[0]
})
//
// visible
watch(
() => visible.value,
(val) => {
@ -366,11 +198,19 @@ watch(
}
)
//
const addOrUpdateVisible = ref(false)
const dataFormRef = ref(null)
/**
* 初始化方法根据ID加载或创建新的运输模板
* @param id - 运输模板ID若不存在则为创建模式
*/
const init = (id) => {
visible.value = true
dataForm.value.transportId = id || 0
// DOM
nextTick(() => {
dataFormRef.value?.resetFields()
dataForm.value = {
@ -384,9 +224,10 @@ const init = (id) => {
transfeeFrees: [{ freeCityList: [], freeType: 0 }]
}
})
// ID
if (dataForm.value.transportId) {
http({
//
url: http.adornUrl(`/shop/transport/info/${dataForm.value.transportId}`),
method: 'get'
})
@ -401,8 +242,16 @@ const init = (id) => {
})
}
}
// init
defineExpose({ init })
/**
* 获取城市列表并填充到对应的运费项或包邮条件中
* @param row - 当前行索引
* @param cityList - 城市列表
* @param type - 类型标识符0为普通运费项1为包邮条件
*/
const getDataList = (row, cityList, type) => {
if (type === 0) {
dataForm.value.transfees[row].cityList = cityList
@ -413,22 +262,25 @@ const getDataList = (row, cityList, type) => {
}
/**
* 添加运费项
* 添加一个新的运费项
*/
const addTransfee = () => {
dataForm.value.transfees.push({ cityList: [], status: 1 })
}
/**
* 删除运费项
* 删除指定的运费项
* @param rowIndex - 要删除的行索引
*/
const onDelete = (rowIndex) => {
dataForm.value.transfees.splice(rowIndex, 1)
}
//
const addOrUpdateRef = ref(null)
/**
* 可配送区域和运费编辑
* 可配送区域和运费编辑当前功能未实现
*/
const onAddOrUpdate = () => {
ElMessage({
@ -448,14 +300,16 @@ const addTransfeeFree = () => {
}
/**
* 删除指定包邮条件
* 删除指定的包邮条件
* @param rowIndex - 要删除的行索引
*/
const deleteTransfeeFree = (rowIndex) => {
dataForm.value.transfeeFrees?.splice(rowIndex, 1)
}
/**
* 指定包邮条件编辑
* 编辑指定的包邮条件
* @param rowIndex - 要编辑的行索引
*/
const addOrUpdateTransfeeFree = (rowIndex) => {
addOrUpdateVisible.value = true
@ -470,8 +324,8 @@ const addOrUpdateTransfeeFree = (rowIndex) => {
}
/**
* 改变模板类型 0 买家承担运费 1 卖家包邮
* @param val
* 改变模板类型0表示买家承担运费1表示卖家包邮
* @param val - 新的选择值
*/
const changeFreeFee = (val) => {
dataForm.value.hasFreeCondition = false
@ -484,7 +338,9 @@ const changeFreeFee = (val) => {
}
/**
* 校验输入的数字
* 校验输入的数字确保其为正整数且不小于零
* @param row - 当前行对象
* @param type - 需要校验的字段类型
*/
const checkNumber = (row, type) => {
if (type === 1) {
@ -497,7 +353,9 @@ const checkNumber = (row, type) => {
}
/**
* 保留整数并小于零的数设为0
* 保留整数并处理小于零的情况
* @param num - 输入的数字
* @returns 处理后的数字
*/
const getNumber = (num) => {
num = Math.round(num)
@ -505,11 +363,12 @@ const getNumber = (num) => {
}
/**
* 表单提交
* 提交表单保存运输模板信息
*/
const onSubmit = Debounce(() => {
dataFormRef.value?.validate((valid) => {
if (valid) {
//
for (let i = 1; i < dataForm.value.transfees.length; i++) {
const transfee = dataForm.value.transfees[i]
if (transfee.cityList.length === 0) {
@ -521,12 +380,18 @@ const onSubmit = Debounce(() => {
return
}
}
//
if (dataForm.value.hasFreeCondition) {
hasFreeCondition.value = 1
} else {
hasFreeCondition.value = 0
}
//
dataForm.value.transfees[0].cityList = []
// HTTP
http({
url: http.adornUrl('/shop/transport'),
method: dataForm.value.transportId ? 'put' : 'post',

@ -1,91 +1,99 @@
<template>
<!-- 使用avue-crud组件创建一个可编辑的数据表格 -->
<div class="mod-transport">
<avue-crud
ref="crudRef"
:page="page"
:data="dataList"
:option="tableOption"
@search-change="onSearch"
@selection-change="selectionChange"
@on-load="getDataList"
:page="page"
:data="dataList"
:option="tableOption"
@search-change="onSearch"
@selection-change="selectionChange"
@on-load="getDataList"
>
<template #prod-prop-values="scope">
<el-tag
v-for="item in scope.row.prodPropValues"
:key="item.valueId"
>
{{ item.propValue }}
</el-tag>
</template>
<template #menu-left>
<el-button
v-if="isAuth('shop:transport:save')"
type="primary"
icon="el-icon-plus"
@click.stop="onAddOrUpdate()"
>
新增
</el-button>
<!-- 自定义列内容产品属性值 -->
<template #prod-prop-values="scope">
<el-tag v-for="item in scope.row.prodPropValues" :key="item.valueId">{{ item.propValue }}</el-tag>
</template>
<el-button
v-if="isAuth('shop:transport:delete')"
type="danger"
:disabled="dataListSelections.length <= 0"
@click="onDelete()"
>
批量删除
</el-button>
</template>
<!-- 左侧菜单按钮 -->
<template #menu-left>
<el-button
v-if="isAuth('shop:transport:save')"
type="primary"
icon="el-icon-plus"
@click.stop="onAddOrUpdate()"
>
新增
</el-button>
<template #menu="scope">
<el-button
v-if="isAuth('shop:transport:update')"
type="primary"
icon="el-icon-edit"
@click.stop="onAddOrUpdate(scope.row.transportId)"
>
修改
</el-button>
<el-button
v-if="isAuth('shop:transport:delete')"
type="danger"
:disabled="dataListSelections.length <= 0"
@click="onDelete()"
>
批量删除
</el-button>
</template>
<el-button
v-if="isAuth('shop:transport:delete')"
type="danger"
icon="el-icon-delete"
@click.stop="onDelete(scope.row.transportId)"
>
删除
</el-button>
</template>
<!-- 行内操作按钮 -->
<template #menu="scope">
<el-button
v-if="isAuth('shop:transport:update')"
type="primary"
icon="el-icon-edit"
@click.stop="onAddOrUpdate(scope.row.transportId)"
>
修改
</el-button>
<el-button
v-if="isAuth('shop:transport:delete')"
type="danger"
icon="el-icon-delete"
@click.stop="onDelete(scope.row.transportId)"
>
删除
</el-button>
</template>
</avue-crud>
<!-- 弹窗, 新增 / 修改 -->
<add-or-update
v-if="addOrUpdateVisible"
ref="addOrUpdateRef"
@refresh-data-list="getDataList"
ref="addOrUpdateRef"
@refresh-data-list="getDataList"
/>
</div>
</template>
<script setup>
import { isAuth } from '@/utils'
import { ElMessage, ElMessageBox } from 'element-plus'
import { tableOption } from '@/crud/shop/transport'
import AddOrUpdate from './add-or-update.vue'
const dataList = ref([])
const dataListSelections = ref([])
import { isAuth } from '@/utils' //
import { ElMessage, ElMessageBox } from 'element-plus' // Element Plus
import { tableOption } from '@/crud/shop/transport' //
import AddOrUpdate from './add-or-update.vue' //
import { ref, nextTick } from 'vue' // VueAPI
import http from '@/api/http' // HTTP
//
const dataList = ref([]) //
const dataListSelections = ref([]) //
const page = ref({
total: 0, //
currentPage: 1, //
pageSize: 10 //
currentPage: 1, //
pageSize: 10 //
})
/**
* 获取数据列表
* @param {Object} pageParam - 分页参数
* @param {Object} params - 查询参数
* @param {Function} done - 加载完成回调函数
*/
const getDataList = (pageParam, params, done) => {
http({
url: http.adornUrl('/shop/transport/page'),
method: 'get',
url: http.adornUrl('/shop/transport/page'), // URL
method: 'get', // HTTP
params: http.adornParams(
Object.assign(
{
@ -94,34 +102,36 @@ const getDataList = (pageParam, params, done) => {
},
params
)
)
) //
})
.then(({ data }) => {
dataList.value = data.records
page.value.total = data.total
if (done) done()
dataList.value = data.records //
page.value.total = data.total //
if (done) done() // done
})
}
//
const addOrUpdateVisible = ref(false)
const addOrUpdateRef = ref(null)
/**
* 新增 / 修改
* @param id
* 新增或修改运输模板
* @param {Number} id - 运输模板ID如果存在则是修改操作
*/
const onAddOrUpdate = (id) => {
addOrUpdateVisible.value = true
addOrUpdateVisible.value = true //
nextTick(() => {
addOrUpdateRef.value?.init(id)
addOrUpdateRef.value?.init(id) // ID
})
}
/**
* 删除
* @param id
* 删除运输模板
* @param {Number} id - 单个运输模板ID如果不存在则进行批量删除
*/
const onDelete = (id) => {
const ids = id ? [id] : dataListSelections.value?.map(item => item.transportId)
const ids = id ? [id] : dataListSelections.value?.map(item => item.transportId) // ID
ElMessageBox.confirm(
`确定进行[${id ? '删除' : '批量删除'}]操作?`,
'提示',
@ -135,7 +145,7 @@ const onDelete = (id) => {
http({
url: http.adornUrl('/shop/transport'),
method: 'delete',
data: http.adornData(ids, false)
data: http.adornData(ids, false) //
})
.then(() => {
ElMessage({
@ -143,7 +153,7 @@ const onDelete = (id) => {
type: 'success',
duration: 1500,
onClose: () => {
getDataList()
getDataList() //
}
})
})
@ -152,16 +162,18 @@ const onDelete = (id) => {
/**
* 条件查询
* @param {Object} params - 查询参数
* @param {Function} done - 查询完成后回调
*/
const onSearch = (params, done) => {
getDataList(page.value, params, done)
getDataList(page.value, params, done) //
}
/**
* 多选变化
* 多选变化时触发
* @param {Array} val - 选中的数据项数组
*/
const selectionChange = (val) => {
dataListSelections.value = val
dataListSelections.value = val //
}
</script>

@ -1,9 +1,11 @@
<template>
<!-- 对话框组件 -->
<el-dialog
v-model="visible"
:title="!dataForm.areaId ? '新增' : '修改'"
:close-on-click-modal="false"
>
<!-- 表单组件 -->
<el-form
ref="dataFormRef"
:model="dataForm"
@ -11,6 +13,7 @@
label-width="100px"
@keyup.enter="onSubmit()"
>
<!-- 地区名称输入项 -->
<el-form-item
label="地区名称"
prop="areaName"
@ -22,6 +25,7 @@
show-word-limit
/>
</el-form-item>
<!-- 上级地区选择项 -->
<el-form-item
label="上级地区"
prop="parentId"
@ -37,6 +41,7 @@
/>
</el-form-item>
</el-form>
<!-- 对话框底部操作按钮 -->
<template #footer>
<div class="dialog-footer">
<el-button
@ -59,7 +64,12 @@
import { ElMessage } from 'element-plus'
import { treeDataTranslate } from '@/utils'
import { Debounce } from '@/utils/debounce'
import { ref, reactive, nextTick, defineEmits, defineExpose } from 'vue'
//
const emit = defineEmits(['refreshDataList'])
//
const dataRule = reactive({
areaName: [
{ required: true, message: '区域名称不能为空', trigger: 'blur' },
@ -67,6 +77,7 @@ const dataRule = reactive({
]
})
//
const visible = ref(false)
const areaList = ref([])
const dataForm = ref({
@ -81,61 +92,76 @@ const categoryTreeProps = reactive({
checkStrictly: true
})
const selectedOptions = ref([])
//
const dataFormRef = ref(null)
// areaId
const init = (areaId) => {
dataForm.value.areaId = areaId || 0
visible.value = true
selectedOptions.value = []
dataForm.value.areaId = areaId || 0 // ID
visible.value = true //
selectedOptions.value = [] //
// 使nextTickDOM
nextTick(() => {
dataFormRef.value?.resetFields()
dataFormRef.value?.resetFields() //
if (dataForm.value.areaId) {
// areaId
http({
url: http.adornUrl('/admin/area/info/' + dataForm.value.areaId),
method: 'get',
params: http.adornParams()
}).then(({ data }) => {
dataForm.value = data //
selectedOptions.value = dataForm.value.parentId //
// categoryTreePropsreactive
categoryTreeProps.areaId = dataForm.value.areaId
categoryTreeProps.areaName = dataForm.value.areaName
})
.then(({ data }) => {
dataForm.value = data
selectedOptions.value = dataForm.value.parentId
categoryTreeProps.areaId = dataForm.value.areaId
categoryTreeProps.areaName = dataForm.value.areaName
})
}
//
http({
url: http.adornUrl('/admin/area/list'),
method: 'get',
params: http.adornParams()
}).then(({ data }) => {
areaList.value = treeDataTranslate(data, 'areaId', 'parentId')
})
.then(({ data }) => {
areaList.value = treeDataTranslate(data, 'areaId', 'parentId')
})
})
}
// init
defineExpose({ init })
//
const page = {
total: 0, //
currentPage: 1, //
pageSize: 20 //
}
/**
* 表单提交
*/
const onSubmit = Debounce(() => {
//
dataFormRef.value?.validate((valid) => {
if (valid) {
// HTTP
http({
url: http.adornUrl('/admin/area'),
method: dataForm.value.areaId ? 'put' : 'post',
method: dataForm.value.areaId ? 'put' : 'post', //
data: http.adornData(dataForm.value)
}).then(() => {
//
ElMessage({
message: '操作成功',
type: 'success',
duration: 1500,
onClose: () => {
visible.value = false
emit('refreshDataList', page)
visible.value = false //
emit('refreshDataList', page) //
}
})
})
@ -143,8 +169,9 @@ const onSubmit = Debounce(() => {
})
})
//
const handleChange = (val) => {
// parentIdID
dataForm.value.parentId = val[val.length - 1]
}
</script>

@ -1,10 +1,13 @@
<template>
<!-- 地区管理模块 -->
<div class="mod-sys-area">
<!-- 地区名称输入框用于过滤树状结构中的节点 -->
<el-input
v-model="areaName"
class="area-search-input"
placeholder="地区关键词"
/>
<!-- 新增按钮点击时触发新增或更新操作 -->
<el-button
type="primary"
class="area-add-btn"
@ -13,17 +16,20 @@
新增
</el-button>
<!-- 树形控件展示地区层级结构 -->
<el-tree
ref="tree2Ref"
:data="areaTreeData"
node-key="areaId"
:filter-node-method="filterNode"
:props="props"
:expand-on-click-node="false"
:data="areaTreeData"
node-key="areaId"
:filter-node-method="filterNode"
:props="props"
:expand-on-click-node="false"
>
<template #default="{ node, data }">
<span class="addr-name">{{ node.label }}</span>
<span>
<!-- 自定义节点内容模板 -->
<template #default="{ node, data }">
<span class="addr-name">{{ node.label }}</span> <!-- 显示地区名称 -->
<span>
<!-- 修改按钮点击时触发修改操作 -->
<el-button
type="text"
icon="el-icon-edit"
@ -31,6 +37,7 @@
>
修改
</el-button>
<!-- 删除按钮点击时触发删除操作 -->
<el-button
type="text"
icon="el-icon-delete"
@ -39,48 +46,59 @@
删除
</el-button>
</span>
</template>
</template>
</el-tree>
<!-- 添加或更新对话框组件 -->
<add-or-update
v-if="addOrUpdateVisible"
ref="addOrUpdateRef"
@refresh-data-list="getDataList"
@close="addOrUpdateVisible=false"
ref="addOrUpdateRef"
@refresh-data-list="getDataList"
@close="addOrUpdateVisible=false"
/>
</div>
</template>
<script setup>
import { ElMessage, ElMessageBox } from 'element-plus'
import AddOrUpdate from './add-or-update.vue'
import { treeDataTranslate } from '@/utils'
import AddOrUpdate from './add-or-update.vue' //
import { treeDataTranslate } from '@/utils' //
import { ref, watch, onMounted, reactive, nextTick } from 'vue'
//
const areaName = ref('')
//
const props = {
id: 'areaId',
label: 'areaName',
children: 'children'
}
const tree2Ref = ref(null)
//
watch(
() => areaName,
() => areaName.value,
(val) => {
tree2Ref.value?.filter(val)
}
)
//
onMounted(() => {
getDataList(page)
})
//
const page = reactive({
total: 0, //
currentPage: 1, //
pageSize: 10 //
total: 0, //
currentPage: 1, //
pageSize: 10 //
})
//
const areaTreeData = ref(null)
//
const getDataList = (pageParam, params) => {
http({
url: http.adornUrl('/admin/area/list'),
@ -90,38 +108,46 @@ const getDataList = (pageParam, params) => {
size: pageParam == null ? page.pageSize : pageParam.pageSize
}, params))
}).then(({ data }) => {
// areaTreeData
areaTreeData.value = treeDataTranslate(data, 'areaId', 'parentId')
})
}
//
const addOrUpdateRef = ref(null)
//
const addOrUpdateVisible = ref(false)
/**
* 新增 / 修改
* @param id
* 触发新增或更新操作
* @param id 可选参数若提供则表示更新指定ID的地区信息
*/
const onAddOrUpdate = (id) => {
addOrUpdateVisible.value = true
nextTick(() => {
addOrUpdateRef.value?.init(id)
addOrUpdateRef.value?.init(id) //
})
}
/**
* 删除
* @param areaId
* 执行删除操作
* @param areaId 要删除的地区ID
*/
const onDelete = (areaId) => {
//
ElMessageBox.confirm('确定进行删除操作?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
// HTTP
http({
url: http.adornUrl('/admin/area/' + areaId),
method: 'delete',
data: http.adornData({})
}).then(() => {
//
ElMessage({
message: '操作成功',
type: 'success',
@ -131,35 +157,37 @@ const onDelete = (areaId) => {
}
})
})
}).catch(() => { })
}).catch(() => { /* 用户取消删除 */ })
}
// eslint-disable-next-line no-unused-vars
// onAddOrUpdate
const update = (node, data) => {
onAddOrUpdate(data.areaId)
}
// eslint-disable-next-line no-unused-vars
// onDelete
const remove = (node, data) => {
onDelete(data.areaId)
}
//
const filterNode = (value, data) => {
if (!value) return true
return data.areaName.indexOf(value) !== -1
if (!value) return true //
return data.areaName.indexOf(value) !== -1 //
}
</script>
<style lang="scss" scoped>
.mod-sys-area {
.area-search-input {
width: 30%;
width: 30%; // 30%
}
.area-add-btn {
float: right;
float: right; //
}
}
/* 使用:deep选择器穿透样式作用于子组件 */
:deep(.el-tree-node) .addr-name {
width: 100%;
width: 100%; //
}
</style>

@ -1,89 +1,111 @@
<template>
<!-- 对话框组件 -->
<el-dialog
v-model="visible"
:title="!dataForm.id ? '新增' : '修改'"
:close-on-click-modal="false"
:title="!dataForm.id ? '新增' : '修改'"
:close-on-click-modal="false"
>
<el-form
ref="dataFormRef"
:model="dataForm"
:rules="dataRule"
label-width="80px"
@keyup.enter="onSubmit()"
>
<el-form-item
label="参数名"
prop="paramKey"
>
<el-input
v-model="dataForm.paramKey"
placeholder="参数名"
/>
</el-form-item>
<el-form-item
label="参数值"
prop="paramValue"
>
<el-input
v-model="dataForm.paramValue"
placeholder="参数值"
/>
</el-form-item>
<el-form-item
label="备注"
prop="remark"
>
<el-input
v-model="dataForm.remark"
placeholder="备注"
/>
</el-form-item>
</el-form>
<template #footer>
<!-- 表单组件 -->
<el-form
ref="dataFormRef"
:model="dataForm"
:rules="dataRule"
label-width="80px"
@keyup.enter="onSubmit()"
>
<!-- 参数名输入项 -->
<el-form-item
label="参数名"
prop="paramKey"
>
<el-input
v-model="dataForm.paramKey"
placeholder="参数名"
/>
</el-form-item>
<!-- 参数值输入项 -->
<el-form-item
label="参数值"
prop="paramValue"
>
<el-input
v-model="dataForm.paramValue"
placeholder="参数值"
/>
</el-form-item>
<!-- 备注输入项 -->
<el-form-item
label="备注"
prop="remark"
>
<el-input
v-model="dataForm.remark"
placeholder="备注"
/>
</el-form-item>
</el-form>
<!-- 对话框底部操作按钮 -->
<template #footer>
<span class="dialog-footer">
<el-button @click="visible = false">取消</el-button>
<el-button
type="primary"
@click="onSubmit()"
>确定</el-button>
<el-button @click="visible = false">取消</el-button> <!-- -->
<el-button type="primary" @click="onSubmit()"></el-button> <!-- 提交表单 -->
</span>
</template>
</template>
</el-dialog>
</template>
<script setup>
import { ElMessage } from 'element-plus'
import { Debounce } from '@/utils/debounce'
import { ref, reactive, nextTick, defineEmits, defineExpose } from 'vue'
//
const emit = defineEmits(['refreshDataList'])
//
const visible = ref(false)
// 使reactive
const dataForm = reactive({
id: 0,
paramKey: '',
paramValue: '',
remark: ''
id: 0, // ID0
paramKey: '', //
paramValue: '', //
remark: '' //
})
//
const dataRule = {
paramKey: [
{ required: true, message: '参数名不能为空', trigger: 'blur' },
{ pattern: /\s\S+|S+\s|\S/, message: '请输入正确的参数名', trigger: 'blur' }
{ required: true, message: '参数名不能为空', trigger: 'blur' }, //
{ pattern: /\s\S+|S+\s|\S/, message: '请输入正确的参数名', trigger: 'blur' } //
],
paramValue: [
{ required: true, message: '参数值不能为空', trigger: 'blur' },
{ pattern: /\s\S+|S+\s|\S/, message: '请输入正确的参数值', trigger: 'blur' }
{ required: true, message: '参数值不能为空', trigger: 'blur' }, //
{ pattern: /\s\S+|S+\s|\S/, message: '请输入正确的参数值', trigger: 'blur' } //
]
}
//
const dataFormRef = ref(null)
// id
const init = (id) => {
dataForm.id = id || 0
visible.value = true
dataForm.id = id || 0 // ID
visible.value = true //
// 使nextTickDOM
nextTick(() => {
dataFormRef.value?.resetFields()
dataFormRef.value?.resetFields() //
if (dataForm.id) {
// id
http({
url: http.adornUrl(`/sys/config/info/${dataForm.id}`),
method: 'get',
params: http.adornParams()
}).then(({ data }) => {
//
dataForm.paramKey = data.paramKey
dataForm.paramValue = data.paramValue
dataForm.remark = data.remark
@ -91,16 +113,21 @@ const init = (id) => {
}
})
}
// init
defineExpose({ init })
/**
* 表单提交
*/
const onSubmit = Debounce(() => {
//
dataFormRef.value?.validate((valid) => {
if (valid) {
// HTTP
http({
url: http.adornUrl('/sys/config'),
method: dataForm.id ? 'put' : 'post',
method: dataForm.id ? 'put' : 'post', //
data: http.adornData({
id: dataForm.id || undefined,
paramKey: dataForm.paramKey,
@ -108,13 +135,14 @@ const onSubmit = Debounce(() => {
remark: dataForm.remark
})
}).then(() => {
//
ElMessage({
message: '操作成功',
type: 'success',
duration: 1500,
onClose: () => {
visible.value = false
emit('refreshDataList')
visible.value = false //
emit('refreshDataList') //
}
})
})

@ -1,135 +1,164 @@
<template>
<!-- 配置管理模块 -->
<div class="mod-config">
<!-- AvueCrud表格组件 -->
<avue-crud
ref="crudRef"
:page="page"
:data="dataList"
:option="tableOption"
@search-change="onSearch"
@selection-change="selectionChange"
@on-load="getDataList"
:page="page"
:data="dataList"
:option="tableOption"
@search-change="onSearch"
@selection-change="selectionChange"
@on-load="getDataList"
>
<template #menu-left>
<el-button
type="primary"
icon="el-icon-plus"
@click.stop="onAddOrUpdate()"
>
新增
</el-button>
<!-- 自定义左侧菜单按钮 -->
<template #menu-left>
<el-button
type="primary"
icon="el-icon-plus"
@click.stop="onAddOrUpdate()"
>
新增
</el-button>
<el-button
type="danger"
:disabled="dataListSelections.length <= 0"
@click="onDelete()"
>
批量删除
</el-button>
</template>
<template #menu="scope">
<el-button
type="primary"
icon="el-icon-edit"
@click.stop="onAddOrUpdate(scope.row.id)"
>
编辑
</el-button>
<el-button
type="danger"
icon="el-icon-delete"
@click.stop="onDelete(scope.row.id)"
>
删除
</el-button>
</template>
<el-button
type="danger"
:disabled="dataListSelections.length <= 0"
@click="onDelete()"
>
批量删除
</el-button>
</template>
<!-- 自定义行内操作按钮 -->
<template #menu="scope">
<el-button
type="primary"
icon="el-icon-edit"
@click.stop="onAddOrUpdate(scope.row.id)"
>
编辑
</el-button>
<el-button
type="danger"
icon="el-icon-delete"
@click.stop="onDelete(scope.row.id)"
>
删除
</el-button>
</template>
</avue-crud>
<!-- 弹窗, 新增 / 修改 -->
<!-- 添加或更新对话框组件 -->
<add-or-update
v-if="addOrUpdateVisible"
ref="addOrUpdateRef"
@refresh-data-list="getDataList"
ref="addOrUpdateRef"
@refresh-data-list="getDataList"
/>
</div>
</template>
<script setup>
import { ElMessage, ElMessageBox } from 'element-plus'
import { tableOption } from '@/crud/sys/config.js'
import { tableOption } from '@/crud/sys/config.js' //
import { ref, reactive, nextTick, defineExpose } from 'vue'
//
const dataList = ref([])
//
const page = reactive({
total: 0, //
currentPage: 1, //
pageSize: 10 //
total: 0, //
currentPage: 1, //
pageSize: 10 //
})
/**
* 获取数据列表
* 获取数据列表方法
* @param pageParam 可选参数用于指定分页信息
* @param params 搜索条件参数
* @param done 分页加载完成回调函数
*/
const getDataList = (pageParam, params, done) => {
http({
url: http.adornUrl('/sys/config/page'),
method: 'get',
url: http.adornUrl('/sys/config/page'), // API
method: 'get', //
params: http.adornParams(
Object.assign(
{
current: pageParam == null ? page.currentPage : pageParam.currentPage,
size: pageParam == null ? page.pageSize : pageParam.pageSize
},
params
params //
)
)
})
.then(({ data }) => {
dataList.value = data.records
page.total = data.total
if (done) done()
dataList.value = data.records //
page.total = data.total //
if (done) done() //
})
}
/**
* 条件查询
* 条件查询方法
* @param params 搜索条件参数
* @param done 分页加载完成回调函数
*/
const onSearch = (params, done) => {
getDataList(page, params, done)
getDataList(page, params, done) //
}
//
const dataListSelections = ref([])
/**
* 多选变化
* 多选变化方法
* @param val 选中的数据项
*/
const selectionChange = (val) => {
dataListSelections.value = val
dataListSelections.value = val //
}
//
const addOrUpdateVisible = ref(false)
//
const addOrUpdate = ref(null)
/**
* 新增 / 修改
* 触发新增或修改操作
* @param id 可选参数若提供则表示更新指定ID的配置信息
*/
const onAddOrUpdate = (id) => {
addOrUpdateVisible.value = true
addOrUpdateVisible.value = true //
nextTick(() => {
addOrUpdate.value?.init(id)
addOrUpdate.value?.init(id) //
})
}
/**
* 删除
* 删除配置信息
* @param id 可选参数若提供则表示删除指定ID的配置信息
*/
const onDelete = (id) => {
const ids = id ? [id] : dataListSelections.value?.map(item => {
return item.id
})
const ids = id ? [id] : dataListSelections.value?.map(item => item.id) // ID
//
ElMessageBox.confirm(`确定对[id=${ids.join(',')}]进行[${id ? '删除' : '批量删除'}]操作?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
.then(() => {
// HTTP
http({
url: http.adornUrl('/sys/config'),
method: 'delete',
data: http.adornData(ids, false)
})
.then(() => {
//
ElMessage({
message: '操作成功',
type: 'success',
@ -139,6 +168,6 @@ const onDelete = (id) => {
}
})
})
}).catch(() => { })
}).catch(() => { /* 用户取消删除 */ })
}
</script>

@ -1,57 +1,70 @@
<template>
<!-- 日志管理模块 -->
<div class="mod-log">
<!-- AvueCrud表格组件用于展示日志数据 -->
<avue-crud
ref="crudRef"
:page="page"
:data="dataList"
:option="tableOption"
@search-change="onSearch"
@on-load="getDataList"
ref="crudRef" <!-- 引用表格实例 -->
:page="page" <!-- 分页信息 -->
:data="dataList" <!-- 表格数据 -->
:option="tableOption" <!-- 表格选项配置 -->
@search-change="onSearch" <!-- 搜索条件变化事件 -->
@on-load="getDataList" <!-- 加载数据事件 -->
/>
</div>
</template>
<script setup>
import { tableOption } from '@/crud/sys/log.js'
import { tableOption } from '@/crud/sys/log.js' //
import { ref, reactive, onMounted } from 'vue'
//
const dataList = ref([])
//
const page = reactive({
total: 0, //
currentPage: 1, //
pageSize: 10 //
total: 0, //
currentPage: 1, //
pageSize: 10 //
})
//
onMounted(() => {
getDataList()
})
/**
* 获取数据列表
* 获取数据列表方法
* @param pageParam 可选参数用于指定分页信息
* @param params 搜索条件参数
* @param done 分页加载完成回调函数
*/
const getDataList = (pageParam, params, done) => {
http({
url: http.adornUrl('/sys/log/page'),
method: 'get',
url: http.adornUrl('/sys/log/page'), // API
method: 'get', //
params: http.adornParams(
Object.assign(
{
current: pageParam == null ? page.currentPage : pageParam.currentPage,
size: pageParam == null ? page.pageSize : pageParam.pageSize
},
params
params //
)
)
})
.then(({ data }) => {
dataList.value = data.records
page.total = data.total
if (done) done()
dataList.value = data.records //
page.total = data.total //
if (done) done() //
})
}
/**
* 条件查询
* 条件查询方法
* @param params 搜索条件参数
* @param done 分页加载完成回调函数
*/
const onSearch = (params, done) => {
getDataList(page, params, done)
getDataList(page, params, done) //
}
</script>

@ -1,162 +1,182 @@
<template>
<!-- 对话框组件 -->
<el-dialog
v-model="visible"
:title="!dataForm.id ? '新增' : '修改'"
:close-on-click-modal="false"
:title="!dataForm.id ? '新增' : '修改'"
:close-on-click-modal="false"
>
<el-form
ref="dataFormRef"
:model="dataForm"
:rules="dataRule"
label-width="80px"
@keyup.enter="onSubmit()"
>
<el-form-item
label="类型"
prop="type"
>
<el-radio-group v-model="dataForm.type">
<el-radio
v-for="(type, index) in dataForm.typeList"
:key="index"
:label="index"
>
{{ type }}
</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item
:label="dataForm.typeList[dataForm.type] + '名称'"
prop="name"
>
<el-input
v-model="dataForm.name"
:placeholder="dataForm.typeList[dataForm.type] + '名称'"
/>
</el-form-item>
<el-form-item label="上级菜单">
<el-cascader
v-model="selectedMenu"
expand-trigger="hover"
:options="menuList"
:props="menuListTreeProps"
change-on-select
:clearable="true"
@change="handleSelectMenuChange"
/>
</el-form-item>
<el-form-item
v-if="dataForm.type === 1"
label="菜单路由"
prop="url"
>
<el-input
v-model="dataForm.url"
placeholder="菜单路由"
/>
</el-form-item>
<el-form-item
v-if="dataForm.type !== 0"
label="授权标识"
prop="perms"
<!-- 表单组件 -->
<el-form
ref="dataFormRef"
:model="dataForm"
:rules="dataRule"
label-width="80px"
@keyup.enter="onSubmit()"
>
<!-- 类型选择项 -->
<el-form-item
label="类型"
prop="type"
>
<el-radio-group v-model="dataForm.type">
<el-radio
v-for="(type, index) in dataForm.typeList"
:key="index"
:label="index"
>
{{ type }}
</el-radio>
</el-radio-group>
</el-form-item>
<!-- 动态标签根据类型改变 -->
<el-form-item
:label="dataForm.typeList[dataForm.type] + '名称'"
prop="name"
>
<el-input
v-model="dataForm.name"
:placeholder="dataForm.typeList[dataForm.type] + '名称'"
/>
</el-form-item>
<!-- 上级菜单选择 -->
<el-form-item label="上级菜单">
<el-cascader
v-model="selectedMenu"
expand-trigger="hover"
:options="menuList"
:props="menuListTreeProps"
change-on-select
:clearable="true"
@change="handleSelectMenuChange"
/>
</el-form-item>
<!-- 菜单路由输入仅当类型为1时显示 -->
<el-form-item
v-if="dataForm.type === 1"
label="菜单路由"
prop="url"
>
<el-input
v-model="dataForm.url"
placeholder="菜单路由"
/>
</el-form-item>
<!-- 授权标识输入除类型0外显示 -->
<el-form-item
v-if="dataForm.type !== 0"
label="授权标识"
prop="perms"
>
<el-input
v-model="dataForm.perms"
placeholder="多个用逗号分隔, 如: user:list,user:create"
/>
</el-form-item>
<!-- 排序号输入除类型2外显示 -->
<el-form-item
v-if="dataForm.type !== 2"
label="排序号"
prop="orderNum"
>
<el-input-number
v-model="dataForm.orderNum"
controls-position="right"
:min="0"
label="排序号"
/>
</el-form-item>
<!-- 菜单图标选择除类型2外显示 -->
<el-form-item
v-if="dataForm.type !== 2"
label="菜单图标"
prop="icon"
>
<el-row>
<el-col :span="22">
<el-input
v-model="dataForm.perms"
placeholder="多个用逗号分隔, 如: user:list,user:create"
ref="iconInputRef"
v-model="dataForm.icon"
:virtual-ref="iconListPopoverRef"
placeholder="菜单图标名称"
clearable
/>
</el-form-item>
<el-form-item
v-if="dataForm.type !== 2"
label="排序号"
prop="orderNum"
>
<el-input-number
v-model="dataForm.orderNum"
controls-position="right"
:min="0"
label="排序号"
/>
</el-form-item>
<el-form-item
v-if="dataForm.type !== 2"
label="菜单图标"
prop="icon"
>
<el-row>
<el-col :span="22">
<el-input
ref="iconInputRef"
v-model="dataForm.icon"
:virtual-ref="iconListPopoverRef"
placeholder="菜单图标名称"
clearable
/>
<el-popover
ref="iconListPopoverRef"
style="width: 390px"
:virtual-ref="iconInputRef"
placement="bottom-start"
trigger="click"
:popper-style="iconPopoverClass"
virtual-triggering
>
<el-button
v-for="(item, index) in iconList"
:key="index"
style="padding: 8px; margin: 8px 0 0 8px"
:class="{ 'is-active': item === dataForm.icon }"
@click="iconActiveHandle(item)"
>
<svg-icon
:icon-class="`${item}`"
/>
</el-button>
</el-popover>
</el-col>
<el-col
:span="2"
class="icon-list__tips"
<el-popover
ref="iconListPopoverRef"
style="width: 390px"
:virtual-ref="iconInputRef"
placement="bottom-start"
trigger="click"
:popper-style="iconPopoverClass"
virtual-triggering
>
<el-button
v-for="(item, index) in iconList"
:key="index"
style="padding: 8px; margin: 8px 0 0 8px"
:class="{ 'is-active': item === dataForm.icon }"
@click="iconActiveHandle(item)"
>
<el-tooltip
placement="top"
effect="light"
>
<template #content>
<div>全站推荐使用SVG Sprite, 详细请参考:icons/index.js 描述</div>
</template>
<i class="el-icon-warning" />
</el-tooltip>
</el-col>
</el-row>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="visible = false">取消</el-button>
<el-button
type="primary"
@click="onSubmit()"
<svg-icon
:icon-class="`${item}`"
/>
</el-button>
</el-popover>
</el-col>
<el-col
:span="2"
class="icon-list__tips"
>
<el-tooltip
placement="top"
effect="light"
>
确定
</el-button>
<template #content>
<div>全站推荐使用SVG Sprite, 详细请参考:icons/index.js 描述</div>
</template>
<i class="el-icon-warning" />
</el-tooltip>
</el-col>
</el-row>
</el-form-item>
</el-form>
<!-- 对话框底部操作按钮 -->
<template #footer>
<span class="dialog-footer">
<el-button @click="visible = false">取消</el-button> <!-- -->
<el-button type="primary" @click="onSubmit()"></el-button> <!-- 提交表单 -->
</span>
</template>
</template>
</el-dialog>
</template>
<script setup>
import { treeDataTranslate, idList } from '@/utils'
import { ElMessage } from 'element-plus'
import { ref, reactive, computed, onMounted, nextTick, defineEmits, defineExpose } from 'vue'
//
const emit = defineEmits(['refreshDataList'])
//
const iconInputRef = ref(null)
const iconListPopoverRef = ref(null)
const iconPopoverClass = computed(() => {
return {
width: '396px'
}
})
//
const iconPopoverClass = computed(() => ({
width: '396px'
}))
//
const visible = ref(false)
// 使reactive
const dataForm = reactive({
id: 0,
type: 1,
@ -169,15 +189,19 @@ const dataForm = reactive({
icon: '',
iconList: []
})
//
const menuList = ref([])
const selectedMenu = ref([])
//
const menuListTreeProps = {
value: 'menuId',
label: 'name',
checkStrictly: true
}
// eslint-disable-next-line no-unused-vars
// URL
const validateUrl = (rule, value, callback) => {
if (dataForm.type === 1 && !/\S/.test(value)) {
callback(new Error('菜单URL不能为空'))
@ -185,6 +209,8 @@ const validateUrl = (rule, value, callback) => {
callback()
}
}
//
const dataRule = ref({
name: [
{ required: true, message: '菜单名称不能为空', trigger: 'blur' },
@ -195,10 +221,11 @@ const dataRule = ref({
]
})
//
onMounted(() => {
onLoadIcons()
})
const iconList = []
/**
* 加载图标
*/
@ -206,35 +233,42 @@ const onLoadIcons = () => {
const icons = import.meta.glob('@/icons/svg/*.svg')
for (const icon in icons) {
const iconName = icon.split('/src/icons/svg/')[1].split('.svg')[0]
iconList.push(iconName)
dataForm.iconList.push(iconName)
}
}
//
const dataFormRef = ref(null)
/**
* 初始化方法接收id作为参数
* @param id 菜单项ID
*/
const init = (id) => {
dataForm.id = id || 0
dataForm.id = id || 0 // ID
http({
url: http.adornUrl('/sys/menu/list'),
method: 'get',
params: http.adornParams()
})
.then(({ data }) => {
menuList.value = treeDataTranslate(data, 'menuId')
menuList.value = treeDataTranslate(data, 'menuId') //
})
.then(() => {
visible.value = true
visible.value = true //
nextTick(() => {
dataFormRef.value?.resetFields()
dataFormRef.value?.resetFields() //
})
})
.then(() => {
if (dataForm.id) {
//
// id
http({
url: http.adornUrl(`/sys/menu/info/${dataForm.id}`),
method: 'get',
params: http.adornParams()
}).then(({ data }) => {
//
dataForm.id = data.menuId
dataForm.type = data.type
dataForm.name = data.name
@ -246,31 +280,39 @@ const init = (id) => {
selectedMenu.value = idList(menuList.value, data.parentId, 'menuId', 'children').reverse()
})
} else {
selectedMenu.value = []
selectedMenu.value = [] //
}
})
}
defineExpose({ init })
/**
* 上级菜单选择变化处理
* @param val 选中的菜单路径
*/
const handleSelectMenuChange = (val) => {
dataForm.parentId = val[val.length - 1]
dataForm.parentId = val[val.length - 1] // parentIdID
}
/**
* 图标选中
* @param iconName
* 图标选中处理
* @param iconName 选中的图标名称
*/
const iconActiveHandle = (iconName) => {
dataForm.icon = iconName
dataForm.icon = iconName //
}
/**
* 表单提交
*/
const onSubmit = Debounce(() => {
//
dataFormRef.value?.validate((valid) => {
if (valid) {
// HTTP
http({
url: http.adornUrl('/sys/menu'),
method: dataForm.id ? 'put' : 'post',
method: dataForm.id ? 'put' : 'post', //
data: http.adornData({
menuId: dataForm.id || undefined,
type: dataForm.type,
@ -283,13 +325,14 @@ const onSubmit = Debounce(() => {
})
})
.then(() => {
//
ElMessage({
message: '操作成功',
type: 'success',
duration: 1500,
onClose: () => {
visible.value = false
emit('refreshDataList')
visible.value = false //
emit('refreshDataList') //
}
})
})

@ -1,194 +1,211 @@
<template>
<!-- 菜单管理模块 -->
<div class="mod-menu">
<!-- 搜索表单 -->
<el-form
:inline="true"
:model="dataForm"
:model="dataForm"
>
<el-form-item>
<el-button
v-if="isAuth('sys:menu:save')"
type="primary"
@click="onAddOrUpdate()"
>
新增
</el-button>
</el-form-item>
<el-form-item>
<!-- 新增按钮只有有权限时显示 -->
<el-button
v-if="isAuth('sys:menu:save')"
type="primary"
@click="onAddOrUpdate()"
>
新增
</el-button>
</el-form-item>
</el-form>
<!-- 数据表格 -->
<el-table
:data="dataList"
border
style="width: 100%;"
row-key="menuId"
border
style="width: 100%;"
row-key="menuId"
>
<el-table-column
prop="name"
header-align="center"
tree-key="menuId"
width="150"
label="名称"
/>
<el-table-column
header-align="center"
align="center"
label="图标"
>
<template #default="scope">
<svg-icon
:icon-class="`icon-${scope.row.icon}`"
/>
</template>
</el-table-column>
<el-table-column
prop="type"
header-align="center"
align="center"
label="类型"
>
<template #default="scope">
<el-tag
v-if="scope.row.type === 0"
>
目录
</el-tag>
<el-tag
v-else-if="scope.row.type === 1"
type="success"
>
菜单
</el-tag>
<el-tag
v-else-if="scope.row.type === 2"
type="info"
>
按钮
</el-tag>
</template>
</el-table-column>
<el-table-column
prop="orderNum"
header-align="center"
align="center"
label="排序号"
<!-- 名称列 -->
<el-table-column
prop="name"
header-align="center"
tree-key="menuId"
width="150"
label="名称"
/>
<!-- 图标列 -->
<el-table-column
header-align="center"
align="center"
label="图标"
>
<template #default="scope"> <!-- 自定义列内容 -->
<svg-icon
:icon-class="`icon-${scope.row.icon}`"
/>
<el-table-column
prop="url"
header-align="center"
align="center"
width="150"
:show-overflow-tooltip="true"
label="菜单URL"
>
<template #default="scope">
{{ scope.row.url || '-' }}
</template>
</el-table-column>
<el-table-column
prop="perms"
header-align="center"
align="center"
width="150"
:show-overflow-tooltip="true"
label="授权标识"
</template>
</el-table-column>
<!-- 类型列 -->
<el-table-column
prop="type"
header-align="center"
align="center"
label="类型"
>
<template #default="scope"> <!-- 自定义列内容 -->
<el-tag v-if="scope.row.type === 0"></el-tag> <!-- 根据类型动态渲染标签 -->
<el-tag v-else-if="scope.row.type === 1" type="success">菜单</el-tag>
<el-tag v-else-if="scope.row.type === 2" type="info">按钮</el-tag>
</template>
</el-table-column>
<!-- 排序号列 -->
<el-table-column
prop="orderNum"
header-align="center"
align="center"
label="排序号"
/>
<!-- 菜单URL列 -->
<el-table-column
prop="url"
header-align="center"
align="center"
width="150"
:show-overflow-tooltip="true"
label="菜单URL"
>
<template #default="scope"> <!-- 自定义列内容 -->
{{ scope.row.url || '-' }} <!-- 如果没有值则显示'-' -->
</template>
</el-table-column>
<!-- 授权标识列 -->
<el-table-column
prop="perms"
header-align="center"
align="center"
width="150"
:show-overflow-tooltip="true"
label="授权标识"
>
<template #default="scope"> <!-- 自定义列内容 -->
{{ scope.row.perms || '-' }} <!-- 如果没有值则显示'-' -->
</template>
</el-table-column>
<!-- 操作列 -->
<el-table-column
fixed="right"
header-align="center"
align="center"
width="150"
label="操作"
>
<template #default="scope"> <!-- 自定义列内容 -->
<!-- 修改按钮只有有权限时显示 -->
<el-button
v-if="isAuth('sys:menu:update')"
type="text"
@click="onAddOrUpdate(scope.row.menuId)"
>
<template #default="scope">
{{ scope.row.perms || '-' }}
</template>
</el-table-column>
<el-table-column
fixed="right"
header-align="center"
align="center"
width="150"
label="操作"
修改
</el-button>
<!-- 删除按钮只有有权限时显示 -->
<el-button
v-if="isAuth('sys:menu:delete')"
type="text"
@click="onDelete(scope.row.menuId)"
>
<template #default="scope">
<el-button
v-if="isAuth('sys:menu:update')"
type="text"
@click="onAddOrUpdate(scope.row.menuId)"
>
修改
</el-button>
<el-button
v-if="isAuth('sys:menu:delete')"
type="text"
@click="onDelete(scope.row.menuId)"
>
删除
</el-button>
</template>
</el-table-column>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 弹窗, 新增 / 修改 -->
<add-or-update
v-if="addOrUpdateVisible"
ref="addOrUpdateRef"
@refresh-data-list="getDataList"
@close="addOrUpdateVisible=false"
ref="addOrUpdateRef"
@refresh-data-list="getDataList"
@close="addOrUpdateVisible=false"
/>
</div>
</template>
<script setup>
import { treeDataTranslate, isAuth } from '@/utils'
import { ElMessage, ElMessageBox } from 'element-plus'
import AddOrUpdate from './add-or-update.vue'
import { ref, onMounted, nextTick } from 'vue'
import { treeDataTranslate, isAuth } from '@/utils' //
import { ElMessage, ElMessageBox } from 'element-plus' //
import AddOrUpdate from './add-or-update.vue' // /
//
const dataForm = ref({})
//
onMounted(() => {
getDataList()
})
//
const dataList = ref([])
/**
* 获取数据列表
* 获取数据列表方法
*/
const getDataList = () => {
http({
url: http.adornUrl('/sys/menu/table'),
method: 'get',
params: http.adornParams()
url: http.adornUrl('/sys/menu/table'), // API
method: 'get', //
params: http.adornParams() //
}).then(({ data }) => {
dataList.value = treeDataTranslate(data, 'menuId')
dataList.value = treeDataTranslate(data, 'menuId') //
})
}
// /
const addOrUpdateRef = ref(null)
//
const addOrUpdateVisible = ref(false)
/**
* 新增 / 修改
* @param id
* 新增 / 修改菜单项
* @param id 菜单项ID可选如果没有传入则表示新增
*/
const onAddOrUpdate = (id) => {
addOrUpdateVisible.value = true
addOrUpdateVisible.value = true //
nextTick(() => {
addOrUpdateRef.value?.init(id)
addOrUpdateRef.value?.init(id) //
})
}
/**
* 删除
* @param id
* 删除菜单项
* @param id 菜单项ID
*/
const onDelete = (id) => {
//
ElMessageBox.confirm(`确定对[id=${id}]进行[删除]操作?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
// HTTP
http({
url: http.adornUrl(`/sys/menu/${id}`),
method: 'delete',
data: http.adornData()
}).then(() => {
//
ElMessage({
message: '操作成功',
type: 'success',
duration: 1500,
onClose: () => {
getDataList()
getDataList() //
}
})
})

@ -1,136 +1,180 @@
<template>
<!-- 角色管理对话框 -->
<el-dialog
v-model="visible"
:title="!dataForm.id ? '新增' : '修改'"
:close-on-click-modal="false"
:title="!dataForm.id ? '新增' : '修改'"
:close-on-click-modal="false"
>
<el-form
ref="dataFormRef"
:model="dataForm"
:rules="dataRule"
label-width="80px"
@keyup.enter="onSubmit()"
>
<el-form-item
label="角色名称"
prop="roleName"
>
<el-input
v-model="dataForm.roleName"
placeholder="角色名称"
/>
</el-form-item>
<el-form-item
label="备注"
prop="remark"
>
<el-input
v-model="dataForm.remark"
placeholder="备注"
/>
</el-form-item>
<el-form-item label="授权">
<el-tree
ref="menuListTreeRef"
:data="menuList"
:props="menuListTreeProps"
node-key="menuId"
show-checkbox
/>
</el-form-item>
</el-form>
<template #footer>
<!-- 表单 -->
<el-form
ref="dataFormRef"
:model="dataForm"
:rules="dataRule"
label-width="80px"
@keyup.enter="onSubmit()"
>
<!-- 角色名称输入框 -->
<el-form-item
label="角色名称"
prop="roleName"
>
<el-input
v-model="dataForm.roleName"
placeholder="角色名称"
/>
</el-form-item>
<!-- 备注输入框 -->
<el-form-item
label="备注"
prop="remark"
>
<el-input
v-model="dataForm.remark"
placeholder="备注"
/>
</el-form-item>
<!-- 授权菜单树 -->
<el-form-item label="授权">
<el-tree
ref="menuListTreeRef"
:data="menuList"
:props="menuListTreeProps"
node-key="menuId"
show-checkbox
/>
</el-form-item>
</el-form>
<!-- 对话框底部按钮组 -->
<template #footer>
<span class="dialog-footer">
<el-button @click="visible = false">取消</el-button>
<el-button
type="primary"
@click="onSubmit()"
>确定</el-button>
<el-button @click="visible = false">取消</el-button> <!-- -->
<el-button type="primary" @click="onSubmit()"></el-button> <!-- 确认按钮 -->
</span>
</template>
</template>
</el-dialog>
</template>
<script setup>
import { ElMessage } from 'element-plus'
import { treeDataTranslate } from '@/utils'
import { Debounce } from '@/utils/debounce'
import { ref, reactive, nextTick } from 'vue'
import { ElMessage } from 'element-plus' //
import { treeDataTranslate } from '@/utils' //
import { Debounce } from '@/utils/debounce' //
//
const emit = defineEmits(['refreshDataList'])
const tempKey = -666666 // key) tree. #
// keytree
const tempKey = -666666
//
const visible = ref(false)
//
const menuList = ref([])
//
const menuListTreeProps = {
label: 'name',
children: 'children'
label: 'name', //
children: 'children' //
}
// 使reactive使
const dataForm = reactive({
id: 0,
roleName: '',
remark: ''
id: 0, // ID0
roleName: '', //
remark: '' //
})
// 使reactive使
const dataRule = reactive({
roleName: [
{ required: true, message: '角色名称不能为空', trigger: 'blur' },
{ pattern: /\s\S+|S+\s|\S/, message: '请输入正确的角色名称', trigger: 'blur' }
roleName: [ //
{ required: true, message: '角色名称不能为空', trigger: 'blur' }, //
{ pattern: /\s\S+|\S+\s|\S/, message: '请输入正确的角色名称', trigger: 'blur' } //
],
remark: [
{ required: false, pattern: /\s\S+|S+\s|\S/, message: '输入格式有误', trigger: 'blur' }
remark: [ //
{ required: false, pattern: /\s\S+|\S+\s|\S/, message: '输入格式有误', trigger: 'blur' } //
]
})
//
const dataFormRef = ref(null)
//
const menuListTreeRef = ref(null)
/**
* 初始化方法接收可选的角色ID参数
* @param id 角色ID可选
*/
const init = (id) => {
dataForm.id = id || 0
dataForm.id = id || 0 // id
//
http({
url: http.adornUrl('/sys/menu/table'),
method: 'get',
params: http.adornParams()
url: http.adornUrl('/sys/menu/table'), // API
method: 'get', //
params: http.adornParams() //
})
.then(({ data }) => {
menuList.value = treeDataTranslate(data, 'menuId', 'parentId')
menuList.value = treeDataTranslate(data, 'menuId', 'parentId') //
})
.then(() => {
visible.value = true
nextTick(() => {
dataFormRef.value?.resetFields()
menuListTreeRef.value?.setCheckedKeys([])
})
visible.value = true //
return nextTick() // DOM
})
.then(() => {
if (dataForm.id) {
dataFormRef.value?.resetFields() //
menuListTreeRef.value?.setCheckedKeys([]) //
})
.then(() => {
if (dataForm.id) { //
http({
url: http.adornUrl(`/sys/role/info/${dataForm.id}`),
method: 'get',
params: http.adornParams()
url: http.adornUrl(`/sys/role/info/${dataForm.id}`), // API
method: 'get', //
params: http.adornParams() //
})
.then(({ data }) => {
dataForm.roleName = data.roleName
dataForm.remark = data.remark
dataForm.roleName = data.roleName //
dataForm.remark = data.remark //
// key
const idx = data.menuIdList.indexOf(tempKey)
if (idx !== -1) {
data.menuIdList.splice(idx, data.menuIdList.length - idx)
}
//
menuListTreeRef.value?.setCheckedKeys(data.menuIdList)
})
}
})
}
// init便
defineExpose({ init })
/**
* 表单提交
* 提交表单方法
*/
const onSubmit = Debounce(() => {
dataFormRef.value?.validate((valid) => {
if (valid) {
const onSubmit = Debounce(() => { // 使
dataFormRef.value?.validate((valid) => { //
if (valid) { //
http({
url: http.adornUrl('/sys/role'),
method: dataForm.id ? 'put' : 'post',
url: http.adornUrl('/sys/role'), // API
method: dataForm.id ? 'put' : 'post', // id
data: http.adornData({
roleId: dataForm.id || undefined,
roleName: dataForm.roleName,
remark: dataForm.remark,
menuIdList: [].concat(menuListTreeRef.value?.getCheckedKeys(), [tempKey], menuListTreeRef.value?.getHalfCheckedKeys())
roleId: dataForm.id || undefined, // ID
roleName: dataForm.roleName, //
remark: dataForm.remark, //
// ID
menuIdList: [].concat(
menuListTreeRef.value?.getCheckedKeys(),
[tempKey],
menuListTreeRef.value?.getHalfCheckedKeys()
)
})
})
.then(() => {
@ -139,13 +183,12 @@ const onSubmit = Debounce(() => {
type: 'success',
duration: 1500,
onClose: () => {
visible.value = false
emit('refreshDataList')
visible.value = false //
emit('refreshDataList') //
}
})
})
}
})
})
</script>

@ -1,153 +1,186 @@
<template>
<!-- 角色管理模块 -->
<div class="mod-role">
<!-- 使用avue-crud创建CRUD表格 -->
<avue-crud
ref="crudRef"
:page="page"
:data="dataList"
:option="tableOption"
@search-change="onSearch"
@selection-change="selectionChange"
@on-load="getDataList"
:page="page"
:data="dataList"
:option="tableOption"
@search-change="onSearch"
@selection-change="selectionChange"
@on-load="getDataList"
>
<template #menu-left>
<el-button
v-if="isAuth('sys:role:save')"
type="primary"
icon="el-icon-plus"
@click.stop="onAddOrUpdate()"
>
新增
</el-button>
<el-button
v-if="isAuth('sys:role:delete')"
type="danger"
:disabled="dataListSelections.length <= 0"
@click="onDelete()"
>
批量删除
</el-button>
</template>
<template #menu="scope">
<el-button
v-if="isAuth('sys:role:update')"
type="primary"
icon="el-icon-edit"
@click.stop="onAddOrUpdate(scope.row.roleId)"
>
编辑
</el-button>
<el-button
v-if="isAuth('sys:role:delete')"
type="danger"
icon="el-icon-delete"
@click.stop="onDelete(scope.row.roleId)"
>
删除
</el-button>
</template>
<!-- 左侧菜单栏 -->
<template #menu-left>
<!-- 新增按钮只有有权限时显示 -->
<el-button
v-if="isAuth('sys:role:save')"
type="primary"
icon="el-icon-plus"
@click.stop="onAddOrUpdate()"
>
新增
</el-button>
<!-- 批量删除按钮只有有权限且选择了项目时显示 -->
<el-button
v-if="isAuth('sys:role:delete')"
type="danger"
:disabled="dataListSelections.length <= 0"
@click="onDelete()"
>
批量删除
</el-button>
</template>
<!-- 行内菜单 -->
<template #menu="scope"> <!-- 自定义行内菜单 -->
<!-- 编辑按钮只有有权限时显示 -->
<el-button
v-if="isAuth('sys:role:update')"
type="primary"
icon="el-icon-edit"
@click.stop="onAddOrUpdate(scope.row.roleId)"
>
编辑
</el-button>
<!-- 删除按钮只有有权限时显示 -->
<el-button
v-if="isAuth('sys:role:delete')"
type="danger"
icon="el-icon-delete"
@click.stop="onDelete(scope.row.roleId)"
>
删除
</el-button>
</template>
</avue-crud>
<!-- 弹窗, 新增 / 修改 -->
<add-or-update
v-if="addOrUpdateVisible"
ref="addOrUpdateRef"
@refresh-data-list="getDataList"
ref="addOrUpdateRef"
@refresh-data-list="getDataList"
/>
</div>
</template>
<script setup>
import { isAuth } from '@/utils'
import { ElMessage, ElMessageBox } from 'element-plus'
import { tableOption } from '@/crud/sys/role.js'
import AddOrUpdate from './add-or-update.vue'
import { ref, reactive } from 'vue'
import { isAuth } from '@/utils' //
import { ElMessage, ElMessageBox } from 'element-plus' //
import { tableOption } from '@/crud/sys/role.js' //
import AddOrUpdate from './add-or-update.vue' // /
//
const dataList = ref([])
//
const dataListSelections = ref([])
// 使reactive使
const page = reactive({
total: 0, //
currentPage: 1, //
pageSize: 10 //
total: 0, //
currentPage: 1, //
pageSize: 10 //
})
/**
* 获取数据列表
* 获取数据列表方法
* @param pageParam 分页参数可选
* @param params 搜索参数可选
* @param done 加载完成回调可选
*/
const getDataList = (pageParam, params, done) => {
http({
url: http.adornUrl('/sys/role/page'),
method: 'get',
url: http.adornUrl('/sys/role/page'), // API
method: 'get', //
params: http.adornParams(
Object.assign(
{
current: pageParam == null ? page.currentPage : pageParam.currentPage,
size: pageParam == null ? page.pageSize : pageParam.pageSize
current: pageParam == null ? page.currentPage : pageParam.currentPage, //
size: pageParam == null ? page.pageSize : pageParam.pageSize //
},
params
params //
)
)
})
.then(({ data }) => {
dataList.value = data.records
page.total = data.total
dataList.value = data.records //
page.total = data.total //
if (done) {
done()
done() //
}
})
}
/**
* 条件查询
* 条件查询方法
* @param params 搜索参数
* @param done 加载完成回调可选
*/
const onSearch = (params, done) => {
getDataList(page, params, done)
}
/**
* 多选变化
* 多选变化方法
* @param val 选择的数据项
*/
const selectionChange = (val) => {
dataListSelections.value = val
dataListSelections.value = val //
}
//
const addOrUpdateVisible = ref(false)
//
const addOrUpdateRef = ref(null)
/**
* 新增 / 修改
* 新增 / 修改角色信息
* @param id 角色ID可选如果没有传入则表示新增
*/
const onAddOrUpdate = (id) => {
addOrUpdateVisible.value = true
addOrUpdateVisible.value = true //
nextTick(() => {
addOrUpdateRef.value?.init(id)
addOrUpdateRef.value?.init(id) //
})
}
/**
* 删除
* 删除角色信息
* @param id 角色ID可选如果传入则为单个删除否则为批量删除
*/
const onDelete = (id) => {
const ids = id ? [id] : dataListSelections.value?.map(item => {
return item.roleId
})
const ids = id ? [id] : dataListSelections.value?.map(item => item.roleId) // ID
//
ElMessageBox.confirm(`确定进行[${id ? '删除' : '批量删除'}]操作?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
.then(() => {
// HTTP
http({
url: http.adornUrl('/sys/role'),
method: 'delete',
data: http.adornData(ids, false)
data: http.adornData(ids, false) // ID
})
.then(() => {
//
ElMessage({
message: '操作成功',
type: 'success',
duration: 1500,
onClose: () => {
getDataList()
getDataList() //
}
})
})
}).catch(() => { })
}).catch(() => { /* 用户取消删除操作 */ })
}
</script>

@ -1,231 +1,264 @@
<template>
<!-- 用户管理对话框 -->
<el-dialog
v-model="visible"
:title="!dataForm.id ? '新增' : '修改'"
:close-on-click-modal="false"
:title="!dataForm.id ? '新增' : '修改'"
:close-on-click-modal="false"
>
<el-form
ref="dataFormRef"
:model="dataForm"
:rules="dataRule"
label-width="80px"
@keyup.enter="onSubmit()"
<!-- 表单 -->
<el-form
ref="dataFormRef"
:model="dataForm"
:rules="dataRule"
label-width="80px"
@keyup.enter="onSubmit()"
>
<!-- 用户名输入框 -->
<el-form-item
label="用户名"
prop="userName"
>
<el-input
v-model="dataForm.userName"
placeholder="登录帐号"
/>
</el-form-item>
<!-- 密码输入框 -->
<el-form-item
label="密码"
prop="password"
:class="{ 'is-required': !dataForm.id }"
>
<el-input
v-model="dataForm.password"
type="password"
placeholder="密码"
/>
</el-form-item>
<!-- 确认密码输入框 -->
<el-form-item
label="确认密码"
prop="comfirmPassword"
:class="{ 'is-required': !dataForm.id }"
>
<el-input
v-model="dataForm.comfirmPassword"
type="password"
placeholder="确认密码"
/>
</el-form-item>
<!-- 邮箱输入框 -->
<el-form-item
label="邮箱"
prop="email"
>
<el-input
v-model="dataForm.email"
placeholder="邮箱"
/>
</el-form-item>
<!-- 手机号输入框 -->
<el-form-item
label="手机号"
prop="mobile"
>
<el-input
v-model="dataForm.mobile"
maxlength="11"
placeholder="手机号"
/>
</el-form-item>
<!-- 角色选择 -->
<el-form-item
label="角色"
prop="roleIdList"
>
<el-checkbox-group v-model="dataForm.roleIdList"> <!-- ID -->
<el-checkbox
v-for="role in roleList"
:key="role.roleId"
:label="role.roleId"
>
<el-form-item
label="用户名"
prop="userName"
>
<el-input
v-model="dataForm.userName"
placeholder="登录帐号"
/>
</el-form-item>
<el-form-item
label="密码"
prop="password"
:class="{ 'is-required': !dataForm.id }"
>
<el-input
v-model="dataForm.password"
type="password"
placeholder="密码"
/>
</el-form-item>
<el-form-item
label="确认密码"
prop="comfirmPassword"
:class="{ 'is-required': !dataForm.id }"
>
<el-input
v-model="dataForm.comfirmPassword"
type="password"
placeholder="确认密码"
/>
</el-form-item>
<el-form-item
label="邮箱"
prop="email"
>
<el-input
v-model="dataForm.email"
placeholder="邮箱"
/>
</el-form-item>
<el-form-item
label="手机号"
prop="mobile"
>
<el-input
v-model="dataForm.mobile"
maxlength="11"
placeholder="手机号"
/>
</el-form-item>
<el-form-item
label="角色"
prop="roleIdList"
>
<el-checkbox-group v-model="dataForm.roleIdList">
<el-checkbox
v-for="role in roleList"
:key="role.roleId"
:label="role.roleId"
>
{{ role.roleName }}
</el-checkbox>
</el-checkbox-group>
</el-form-item>
<el-form-item
label="状态"
prop="status"
>
<el-radio-group v-model="dataForm.status">
<el-radio :label="0">
禁用
</el-radio>
<el-radio :label="1">
正常
</el-radio>
</el-radio-group>
</el-form-item>
</el-form>
<template #footer>
{{ role.roleName }} <!-- 显示角色名称 -->
</el-checkbox>
</el-checkbox-group>
</el-form-item>
<!-- 状态选择 -->
<el-form-item
label="状态"
prop="status"
>
<el-radio-group v-model="dataForm.status"> <!-- -->
<el-radio :label="0">禁用</el-radio> <!-- -->
<el-radio :label="1">正常</el-radio> <!-- -->
</el-radio-group>
</el-form-item>
</el-form>
<!-- 对话框底部按钮组 -->
<template #footer>
<span class="dialog-footer">
<el-button @click="visible = false">取消</el-button>
<el-button
type="primary"
@click="onSubmit()"
>确定</el-button>
<el-button @click="visible = false">取消</el-button> <!-- -->
<el-button type="primary" @click="onSubmit()"></el-button> <!-- 确认按钮 -->
</span>
</template>
</template>
</el-dialog>
</template>
<script setup>
import { ElMessage } from 'element-plus'
import { isEmail, isMobile } from '@/utils/validate'
import { Debounce } from '@/utils/debounce'
import { encrypt } from '@/utils/crypto'
import { ref, reactive, nextTick } from 'vue'
import { ElMessage } from 'element-plus' //
import { isEmail, isMobile } from '@/utils/validate' //
import { Debounce } from '@/utils/debounce' //
import { encrypt } from '@/utils/crypto' //
//
const emit = defineEmits(['refreshDataList'])
//
const visible = ref(false)
// 使reactive使
const dataForm = reactive({
id: 0,
userName: '',
password: '',
comfirmPassword: '',
email: '',
mobile: '',
roleIdList: [],
status: 1
id: 0, // ID0
userName: '', //
password: '', //
comfirmPassword: '', //
email: '', //
mobile: '', //
roleIdList: [], // ID
status: 1 // 1
})
// eslint-disable-next-line no-unused-vars
//
const validatePassword = (rule, value, callback) => {
if (!dataForm.id && !/\S/.test(value)) {
if (!dataForm.id && !/\S/.test(value)) { //
callback(new Error('密码不能为空'))
} else {
callback()
callback() //
}
}
// eslint-disable-next-line no-unused-vars
//
const validateComfirmPassword = (rule, value, callback) => {
if (!dataForm.id && !/\S/.test(value)) {
if (!dataForm.id && !/\S/.test(value)) { //
dataForm.password = ''
callback(new Error('确认密码不能为空'))
} else if (dataForm.password !== value) {
} else if (dataForm.password !== value) { //
callback(new Error('确认密码与密码输入不一致'))
} else {
callback()
callback() //
}
}
// eslint-disable-next-line no-unused-vars
//
const validateEmail = (rule, value, callback) => {
if (!isEmail(value)) {
if (!isEmail(value)) { //
callback(new Error('邮箱格式错误'))
} else {
callback()
callback() //
}
}
// eslint-disable-next-line no-unused-vars
//
const validateMobile = (rule, value, callback) => {
if (!isMobile(value)) {
if (!isMobile(value)) { //
callback(new Error('手机号格式错误'))
} else {
callback()
callback() //
}
}
const dataRule = {
// 使reactive使
const dataRule = reactive({
userName: [
{ required: true, message: '用户名不能为空', trigger: 'blur' },
{ pattern: /\s\S+|S+\s|\S/, message: '请输入正确的用户名', trigger: 'blur' }
{ required: true, message: '用户名不能为空', trigger: 'blur' }, //
{ pattern: /\s\S+|\S+\s|\S/, message: '请输入正确的用户名', trigger: 'blur' } //
],
password: [
{ validator: validatePassword, trigger: 'blur' }
{ validator: validatePassword, trigger: 'blur' } //
],
comfirmPassword: [
{ validator: validateComfirmPassword, trigger: 'blur' }
{ validator: validateComfirmPassword, trigger: 'blur' } //
],
email: [
{ required: true, message: '邮箱不能为空', trigger: 'blur' },
{ validator: validateEmail, trigger: 'blur' }
{ required: true, message: '邮箱不能为空', trigger: 'blur' }, //
{ validator: validateEmail, trigger: 'blur' } //
],
mobile: [
{ required: true, message: '手机号不能为空', trigger: 'blur' },
{ validator: validateMobile, trigger: 'blur' }
{ required: true, message: '手机号不能为空', trigger: 'blur' }, //
{ validator: validateMobile, trigger: 'blur' } //
]
}
})
//
const roleList = ref([])
//
const dataFormRef = ref(null)
/**
* 初始化方法接收可选的用户ID参数
* @param id 用户ID可选
*/
const init = (id) => {
dataForm.id = id || 0
dataForm.id = id || 0 // id
//
http({
url: http.adornUrl('/sys/role/list'),
method: 'get',
params: http.adornParams()
url: http.adornUrl('/sys/role/list'), // API
method: 'get', //
params: http.adornParams() //
}).then(({ data }) => {
roleList.value = data
roleList.value = data //
}).then(() => {
visible.value = true
nextTick(() => {
dataFormRef.value?.resetFields()
})
visible.value = true //
return nextTick() // DOM
}).then(() => {
if (dataForm.id) {
dataFormRef.value?.resetFields() //
}).then(() => {
if (dataForm.id) { //
http({
url: http.adornUrl(`/sys/user/info/${dataForm.id}`),
method: 'get',
params: http.adornParams()
url: http.adornUrl(`/sys/user/info/${dataForm.id}`), // API
method: 'get', //
params: http.adornParams() //
}).then(({ data }) => {
dataForm.userName = data.username
dataForm.email = data.email
dataForm.mobile = data.mobile
dataForm.roleIdList = data.roleIdList
dataForm.status = data.status
dataForm.userName = data.username //
dataForm.email = data.email //
dataForm.mobile = data.mobile //
dataForm.roleIdList = data.roleIdList // ID
dataForm.status = data.status //
})
}
})
}
// init便
defineExpose({ init })
/**
* 表单提交
* 提交表单方法
*/
const onSubmit = Debounce(() => {
dataFormRef.value?.validate((valid) => {
if (valid) {
const onSubmit = Debounce(() => { // 使
dataFormRef.value?.validate((valid) => { //
if (valid) { //
http({
url: http.adornUrl('/sys/user'),
method: dataForm.id ? 'put' : 'post',
url: http.adornUrl('/sys/user'), // API
method: dataForm.id ? 'put' : 'post', // id
data: http.adornData({
userId: dataForm.id || undefined,
username: dataForm.userName,
password: encrypt(dataForm.password),
email: dataForm.email,
mobile: dataForm.mobile,
status: dataForm.status,
roleIdList: dataForm.roleIdList
userId: dataForm.id || undefined, // ID
username: dataForm.userName, //
password: encrypt(dataForm.password), //
email: dataForm.email, //
mobile: dataForm.mobile, //
status: dataForm.status, //
roleIdList: dataForm.roleIdList // ID
})
}).then(() => {
ElMessage({
@ -233,13 +266,12 @@ const onSubmit = Debounce(() => {
type: 'success',
duration: 1500,
onClose: () => {
visible.value = false
emit('refreshDataList')
visible.value = false //
emit('refreshDataList') //
}
})
})
}
})
})
</script>

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save