Merge remote-tracking branch 'refs/remotes/ruoyi/master'

# Conflicts:
#	README.md
#	pom.xml
#	ruoyi-ui/package.json
#	ruoyi-ui/src/views/index.vue
#	ruoyi-ui/vue.config.js
This commit is contained in:
Tony
2025-04-17 06:32:26 +08:00
76 changed files with 1328 additions and 1123 deletions

View File

@@ -1,7 +1,7 @@
<template>
<el-breadcrumb class="app-breadcrumb" separator="/">
<transition-group name="breadcrumb">
<el-breadcrumb-item v-for="(item,index) in levelList" :key="item.path">
<el-breadcrumb-item v-for="(item, index) in levelList" :key="item.path">
<span v-if="item.redirect === 'noRedirect' || index == levelList.length - 1" class="no-redirect">{{ item.meta.title }}</span>
<a v-else @click.prevent="handleLink(item)">{{ item.meta.title }}</a>
</el-breadcrumb-item>
@@ -31,15 +31,45 @@ export default {
methods: {
getBreadcrumb() {
// only show routes with meta.title
let matched = this.$route.matched.filter(item => item.meta && item.meta.title)
const first = matched[0]
if (!this.isDashboard(first)) {
matched = [{ path: '/index', meta: { title: '首页' }}].concat(matched)
let matched = []
const router = this.$route
const pathNum = this.findPathNum(router.path)
// multi-level menu
if (pathNum > 2) {
const reg = /\/\w+/gi
const pathList = router.path.match(reg).map((item, index) => {
if (index !== 0) item = item.slice(1)
return item
})
this.getMatched(pathList, this.$store.getters.defaultRoutes, matched)
} else {
matched = router.matched.filter(item => item.meta && item.meta.title)
}
// 判断是否为首页
if (!this.isDashboard(matched[0])) {
matched = [{ path: "/index", meta: { title: "首页" } }].concat(matched)
}
this.levelList = matched.filter(item => item.meta && item.meta.title && item.meta.breadcrumb !== false)
},
findPathNum(str, char = "/") {
let index = str.indexOf(char)
let num = 0
while (index !== -1) {
num++
index = str.indexOf(char, index + 1)
}
return num
},
getMatched(pathList, routeList, matched) {
let data = routeList.find(item => item.path == pathList[0] || (item.name += '').toLowerCase() == pathList[0])
if (data) {
matched.push(data)
if (data.children && pathList.length) {
pathList.shift()
this.getMatched(pathList, data.children, matched)
}
}
},
isDashboard(route) {
const name = route && route.name
if (!name) {
@@ -65,7 +95,6 @@ export default {
font-size: 14px;
line-height: 50px;
margin-left: 8px;
.no-redirect {
color: #97a8be;
cursor: text;

View File

@@ -108,7 +108,7 @@ export default {
if (val !== this.currentValue) {
this.currentValue = val === null ? "" : val;
if (this.Quill) {
this.Quill.pasteHTML(this.currentValue);
this.Quill.clipboard.dangerouslyPasteHTML(this.currentValue);
}
}
},
@@ -136,7 +136,7 @@ export default {
}
});
}
this.Quill.pasteHTML(this.currentValue);
this.Quill.clipboard.dangerouslyPasteHTML(this.currentValue);
this.Quill.on("text-change", (delta, oldDelta, source) => {
const html = this.$refs.editor.children[0].innerHTML;
const text = this.Quill.getText();

View File

@@ -13,6 +13,7 @@
:headers="headers"
class="upload-file-uploader"
ref="fileUpload"
v-if="!disabled"
>
<!-- 上传按钮 -->
<el-button size="mini" type="primary">选取文件</el-button>
@@ -32,7 +33,7 @@
<span class="el-icon-document"> {{ getFileName(file.name) }} </span>
</el-link>
<div class="ele-upload-list__item-content-action">
<el-link :underline="false" @click="handleDelete(index)" type="danger">删除</el-link>
<el-link :underline="false" @click="handleDelete(index)" type="danger" v-if="!disabled">删除</el-link>
</div>
</li>
</transition-group>
@@ -50,22 +51,27 @@ export default {
// 数量限制
limit: {
type: Number,
default: 5,
default: 5
},
// 大小限制(MB)
fileSize: {
type: Number,
default: 5,
default: 5
},
// 文件类型, 例如['png', 'jpg', 'jpeg']
fileType: {
type: Array,
default: () => ["doc", "xls", "ppt", "txt", "pdf"],
default: () => ["doc", "docx", "xls", "xlsx", "ppt", "pptx", "txt", "pdf"]
},
// 是否显示提示
isShowTip: {
type: Boolean,
default: true
},
// 禁用组件(仅查看文件)
disabled: {
type: Boolean,
default: false
}
},
data() {
@@ -119,10 +125,15 @@ export default {
const fileExt = fileName[fileName.length - 1];
const isTypeOk = this.fileType.indexOf(fileExt) >= 0;
if (!isTypeOk) {
this.$modal.msgError(`文件格式不正确, 请上传${this.fileType.join("/")}格式文件!`);
this.$modal.msgError(`文件格式不正确请上传${this.fileType.join("/")}格式文件!`);
return false;
}
}
// 校检文件名是否包含特殊字符
if (file.name.includes(',')) {
this.$modal.msgError('文件名不正确,不能包含英文逗号!');
return false;
}
// 校检文件大小
if (this.fileSize) {
const isLt = file.size / 1024 / 1024 < this.fileSize;

View File

@@ -22,6 +22,7 @@
// make search results more in line with expectations
import Fuse from 'fuse.js/dist/fuse.min.js'
import path from 'path'
import { isHttp } from '@/utils/validate'
export default {
name: 'HeaderSearch',
@@ -36,7 +37,7 @@ export default {
},
computed: {
routes() {
return this.$store.getters.permission_routes
return this.$store.getters.defaultRoutes
}
},
watch: {
@@ -72,7 +73,7 @@ export default {
change(val) {
const path = val.path;
const query = val.query;
if(this.ishttp(val.path)) {
if(isHttp(val.path)) {
// http(s):// 路径新窗口打开
const pindex = path.indexOf("http");
window.open(path.substr(pindex, path.length), "_blank");
@@ -115,7 +116,7 @@ export default {
if (router.hidden) { continue }
const data = {
path: !this.ishttp(router.path) ? path.resolve(basePath, router.path) : router.path,
path: !isHttp(router.path) ? path.resolve(basePath, router.path) : router.path,
title: [...prefixTitle]
}
@@ -149,9 +150,6 @@ export default {
} else {
this.options = []
}
},
ishttp(url) {
return url.indexOf('http://') !== -1 || url.indexOf('https://') !== -1
}
}
}

View File

@@ -44,6 +44,7 @@
<script>
import { getToken } from "@/utils/auth";
import { isExternal } from "@/utils/validate";
export default {
props: {
@@ -93,7 +94,7 @@ export default {
// 然后将数组转为对象数组
this.fileList = list.map(item => {
if (typeof item === "string") {
if (item.indexOf(this.baseUrl) === -1) {
if (item.indexOf(this.baseUrl) === -1 && !isExternal(item)) {
item = { name: this.baseUrl + item, url: this.baseUrl + item };
} else {
item = { name: item, url: item };
@@ -135,7 +136,11 @@ export default {
}
if (!isImg) {
this.$modal.msgError(`文件格式不正确, 请上传${this.fileType.join("/")}图片格式文件!`);
this.$modal.msgError(`文件格式不正确请上传${this.fileType.join("/")}图片格式文件!`);
return false;
}
if (file.name.includes(',')) {
this.$modal.msgError('文件名不正确,不能包含英文逗号!');
return false;
}
if (this.fileSize) {

View File

@@ -106,7 +106,6 @@ export default {
<style scoped>
.pagination-container {
background: #fff;
padding: 32px 16px;
}
.pagination-container.hidden {
display: none;

View File

@@ -1,173 +1,170 @@
<template>
<el-color-picker
v-model="theme"
:predefine="['#409EFF', '#1890ff', '#304156','#212121','#11a983', '#13c2c2', '#6959CD', '#f5222d', ]"
class="theme-picker"
popper-class="theme-picker-dropdown"
/>
</template>
<script>
const version = require('element-ui/package.json').version // element-ui version from node_modules
const ORIGINAL_THEME = '#409EFF' // default color
export default {
data() {
return {
chalk: '', // content of theme-chalk css
theme: ''
}
},
computed: {
defaultTheme() {
return this.$store.state.settings.theme
}
},
watch: {
defaultTheme: {
handler: function(val, oldVal) {
this.theme = val
},
immediate: true
},
async theme(val) {
await this.setTheme(val)
}
},
created() {
if(this.defaultTheme !== ORIGINAL_THEME) {
this.setTheme(this.defaultTheme)
}
},
methods: {
async setTheme(val) {
const oldVal = this.chalk ? this.theme : ORIGINAL_THEME
if (typeof val !== 'string') return
const themeCluster = this.getThemeCluster(val.replace('#', ''))
const originalCluster = this.getThemeCluster(oldVal.replace('#', ''))
const getHandler = (variable, id) => {
return () => {
const originalCluster = this.getThemeCluster(ORIGINAL_THEME.replace('#', ''))
const newStyle = this.updateStyle(this[variable], originalCluster, themeCluster)
let styleTag = document.getElementById(id)
if (!styleTag) {
styleTag = document.createElement('style')
styleTag.setAttribute('id', id)
document.head.appendChild(styleTag)
}
styleTag.innerText = newStyle
}
}
if (!this.chalk) {
const url = `https://unpkg.com/element-ui@${version}/lib/theme-chalk/index.css`
await this.getCSSString(url, 'chalk')
}
const chalkHandler = getHandler('chalk', 'chalk-style')
chalkHandler()
const styles = [].slice.call(document.querySelectorAll('style'))
.filter(style => {
const text = style.innerText
return new RegExp(oldVal, 'i').test(text) && !/Chalk Variables/.test(text)
})
styles.forEach(style => {
const { innerText } = style
if (typeof innerText !== 'string') return
style.innerText = this.updateStyle(innerText, originalCluster, themeCluster)
})
this.$emit('change', val)
},
updateStyle(style, oldCluster, newCluster) {
let newStyle = style
oldCluster.forEach((color, index) => {
newStyle = newStyle.replace(new RegExp(color, 'ig'), newCluster[index])
})
return newStyle
},
getCSSString(url, variable) {
return new Promise(resolve => {
const xhr = new XMLHttpRequest()
xhr.onreadystatechange = () => {
if (xhr.readyState === 4 && xhr.status === 200) {
this[variable] = xhr.responseText.replace(/@font-face{[^}]+}/, '')
resolve()
}
}
xhr.open('GET', url)
xhr.send()
})
},
getThemeCluster(theme) {
const tintColor = (color, tint) => {
let red = parseInt(color.slice(0, 2), 16)
let green = parseInt(color.slice(2, 4), 16)
let blue = parseInt(color.slice(4, 6), 16)
if (tint === 0) { // when primary color is in its rgb space
return [red, green, blue].join(',')
} else {
red += Math.round(tint * (255 - red))
green += Math.round(tint * (255 - green))
blue += Math.round(tint * (255 - blue))
red = red.toString(16)
green = green.toString(16)
blue = blue.toString(16)
return `#${red}${green}${blue}`
}
}
const shadeColor = (color, shade) => {
let red = parseInt(color.slice(0, 2), 16)
let green = parseInt(color.slice(2, 4), 16)
let blue = parseInt(color.slice(4, 6), 16)
red = Math.round((1 - shade) * red)
green = Math.round((1 - shade) * green)
blue = Math.round((1 - shade) * blue)
red = red.toString(16)
green = green.toString(16)
blue = blue.toString(16)
return `#${red}${green}${blue}`
}
const clusters = [theme]
for (let i = 0; i <= 9; i++) {
clusters.push(tintColor(theme, Number((i / 10).toFixed(2))))
}
clusters.push(shadeColor(theme, 0.1))
return clusters
}
}
}
</script>
<style>
.theme-message,
.theme-picker-dropdown {
z-index: 99999 !important;
}
.theme-picker .el-color-picker__trigger {
height: 26px !important;
width: 26px !important;
padding: 2px;
}
.theme-picker-dropdown .el-color-dropdown__link-btn {
display: none;
}
</style>
<template>
<el-color-picker
v-model="theme"
:predefine="['#409EFF', '#1890ff', '#304156','#212121','#11a983', '#13c2c2', '#6959CD', '#f5222d', ]"
class="theme-picker"
popper-class="theme-picker-dropdown"
/>
</template>
<script>
const ORIGINAL_THEME = '#409EFF' // default color
export default {
data() {
return {
chalk: '', // content of theme-chalk css
theme: ''
}
},
computed: {
defaultTheme() {
return this.$store.state.settings.theme
}
},
watch: {
defaultTheme: {
handler: function(val, oldVal) {
this.theme = val
},
immediate: true
},
async theme(val) {
await this.setTheme(val)
}
},
created() {
if(this.defaultTheme !== ORIGINAL_THEME) {
this.setTheme(this.defaultTheme)
}
},
methods: {
async setTheme(val) {
const oldVal = this.chalk ? this.theme : ORIGINAL_THEME
if (typeof val !== 'string') return
const themeCluster = this.getThemeCluster(val.replace('#', ''))
const originalCluster = this.getThemeCluster(oldVal.replace('#', ''))
const getHandler = (variable, id) => {
return () => {
const originalCluster = this.getThemeCluster(ORIGINAL_THEME.replace('#', ''))
const newStyle = this.updateStyle(this[variable], originalCluster, themeCluster)
let styleTag = document.getElementById(id)
if (!styleTag) {
styleTag = document.createElement('style')
styleTag.setAttribute('id', id)
document.head.appendChild(styleTag)
}
styleTag.innerText = newStyle
}
}
if (!this.chalk) {
const url = `/styles/theme-chalk/index.css`
await this.getCSSString(url, 'chalk')
}
const chalkHandler = getHandler('chalk', 'chalk-style')
chalkHandler()
const styles = [].slice.call(document.querySelectorAll('style'))
.filter(style => {
const text = style.innerText
return new RegExp(oldVal, 'i').test(text) && !/Chalk Variables/.test(text)
})
styles.forEach(style => {
const { innerText } = style
if (typeof innerText !== 'string') return
style.innerText = this.updateStyle(innerText, originalCluster, themeCluster)
})
this.$emit('change', val)
},
updateStyle(style, oldCluster, newCluster) {
let newStyle = style
oldCluster.forEach((color, index) => {
newStyle = newStyle.replace(new RegExp(color, 'ig'), newCluster[index])
})
return newStyle
},
getCSSString(url, variable) {
return new Promise(resolve => {
const xhr = new XMLHttpRequest()
xhr.onreadystatechange = () => {
if (xhr.readyState === 4 && xhr.status === 200) {
this[variable] = xhr.responseText.replace(/@font-face{[^}]+}/, '')
resolve()
}
}
xhr.open('GET', url)
xhr.send()
})
},
getThemeCluster(theme) {
const tintColor = (color, tint) => {
let red = parseInt(color.slice(0, 2), 16)
let green = parseInt(color.slice(2, 4), 16)
let blue = parseInt(color.slice(4, 6), 16)
if (tint === 0) { // when primary color is in its rgb space
return [red, green, blue].join(',')
} else {
red += Math.round(tint * (255 - red))
green += Math.round(tint * (255 - green))
blue += Math.round(tint * (255 - blue))
red = red.toString(16)
green = green.toString(16)
blue = blue.toString(16)
return `#${red}${green}${blue}`
}
}
const shadeColor = (color, shade) => {
let red = parseInt(color.slice(0, 2), 16)
let green = parseInt(color.slice(2, 4), 16)
let blue = parseInt(color.slice(4, 6), 16)
red = Math.round((1 - shade) * red)
green = Math.round((1 - shade) * green)
blue = Math.round((1 - shade) * blue)
red = red.toString(16)
green = green.toString(16)
blue = blue.toString(16)
return `#${red}${green}${blue}`
}
const clusters = [theme]
for (let i = 0; i <= 9; i++) {
clusters.push(tintColor(theme, Number((i / 10).toFixed(2))))
}
clusters.push(shadeColor(theme, 0.1))
return clusters
}
}
}
</script>
<style>
.theme-message,
.theme-picker-dropdown {
z-index: 99999 !important;
}
.theme-picker .el-color-picker__trigger {
height: 26px !important;
width: 26px !important;
padding: 2px;
}
.theme-picker-dropdown .el-color-dropdown__link-btn {
display: none;
}
</style>

View File

@@ -14,7 +14,7 @@
</template>
<!-- 顶部菜单超出数量折叠 -->
<el-submenu :style="{'--theme': theme}" index="more" v-if="topMenus.length > visibleNumber">
<el-submenu :style="{'--theme': theme}" index="more" :key="visibleNumber" v-if="topMenus.length > visibleNumber">
<template slot="title">更多菜单</template>
<template v-for="(item, index) in topMenus">
<el-menu-item
@@ -33,6 +33,7 @@
<script>
import { constantRoutes } from "@/router";
import { isHttp } from "@/utils/validate";
// 隐藏侧边栏路由
const hideList = ['/index', '/user/profile'];
@@ -78,7 +79,7 @@ export default {
if(router.path === "/") {
router.children[item].path = "/" + router.children[item].path;
} else {
if(!this.ishttp(router.children[item].path)) {
if(!isHttp(router.children[item].path)) {
router.children[item].path = router.path + "/" + router.children[item].path;
}
}
@@ -95,8 +96,8 @@ export default {
let activePath = path;
if (path !== undefined && path.lastIndexOf("/") > 0 && hideList.indexOf(path) === -1) {
const tmpPath = path.substring(1, path.length);
activePath = "/" + tmpPath.substring(0, tmpPath.indexOf("/"));
if (!this.$route.meta.link) {
activePath = "/" + tmpPath.substring(0, tmpPath.indexOf("/"));
this.$store.dispatch('app/toggleSideBarHide', false);
}
} else if(!this.$route.children) {
@@ -126,7 +127,7 @@ export default {
handleSelect(key, keyPath) {
this.currentIndex = key;
const route = this.routers.find(item => item.path === key);
if (this.ishttp(key)) {
if (isHttp(key)) {
// http(s):// 路径新窗口打开
window.open(key, "_blank");
} else if (!route || !route.children) {
@@ -160,9 +161,6 @@ export default {
} else {
this.$store.dispatch('app/toggleSideBarHide', true);
}
},
ishttp(url) {
return url.indexOf('http://') !== -1 || url.indexOf('https://') !== -1
}
},
};