Compare commits

...

10 Commits

Author SHA1 Message Date
kailong321200875 0aca9430c2 fix: #580 #573 #572 #564 2025-01-09 10:20:32 +08:00
Archer 9c91d8d68e
Merge pull request #571 from Chengyu531/master
fix: 当前页不为1时,修改页数后会导致多次调用getList方法问题
2024-12-11 09:32:31 +08:00
chengyu 28ac2dd7b6 fix: 当前页不为1时,修改页数后会导致多次调用getList方法问题 2024-12-10 21:06:22 +08:00
Archer 72d6fd5a4e
Merge pull request #570 from lt5227/master
feat: 新增支持右键自定义菜单进行节点编辑的树形组件
2024-12-09 15:59:55 +08:00
lt5227 0735371c63 feat: 新增支持右键自定义菜单进行节点编辑的树形组件. #569 2024-12-09 14:39:48 +08:00
lt5227 5a00171c9a feat: 新增支持右键自定义菜单进行节点编辑的树形组件. #569 2024-12-09 14:36:16 +08:00
Archer 9af5c99c2c
Merge pull request #546 from DavidQinqianWu/fix-error-message-on-no-prototype-builtins
fix: fix error message about "no-prototype-builtins" after run project
2024-10-15 11:01:38 +08:00
DavidQinqianWu 64ec1206e8 fix: fix error message about "no-prototype-builtins" after run project 2024-10-14 19:14:23 +08:00
Archer ed301a5b9d
Merge pull request #537 from sixiTr/fixs-initModel
fix: initModel判断schema对应的field是否存在,兼容null与0等场景
2024-09-23 09:41:59 +08:00
sixiTr f236109945 fix: initModel判断schema对应的field是否存在,兼容null与0等场景 2024-09-18 16:45:50 +08:00
21 changed files with 12384 additions and 74 deletions

1
.gitignore vendored
View File

@ -4,7 +4,6 @@ dist
dist-ssr
*.local
/dist*
*-lock.*
pnpm-debug
stats.html
.idea

View File

@ -34,6 +34,8 @@ export default tseslint.config({
'prettier/prettier': 'error',
'no-useless-escape': 0,
'no-undef': 0,
'@typescript-eslint/no-unused-expressions': 0,
'@typescript-eslint/no-unsafe-function-type': 0,
'vue/no-setup-props-destructure': 0,
'vue/script-setup-uses-vars': 1,
'vue/no-reserved-component-names': 0,
@ -52,7 +54,6 @@ export default tseslint.config({
'@typescript-eslint/no-unused-vars': 0,
'no-unused-vars': 0,
'space-before-function-paren': 0,
'vue/attributes-order': 0,
'vue/one-component-per-file': 0,
'vue/html-closing-bracket-newline': 0,

View File

@ -348,6 +348,14 @@ const adminList = [
meta: {
title: 'router.iAgree'
}
},
{
path: 'tree',
component: 'views/Components/Tree',
name: 'Tree',
meta: {
title: 'router.tree'
}
}
]
},

View File

@ -29,89 +29,89 @@
},
"dependencies": {
"@iconify/iconify": "^3.1.1",
"@iconify/vue": "^4.1.2",
"@vueuse/core": "^10.11.0",
"@iconify/vue": "^4.3.0",
"@vueuse/core": "^12.3.0",
"@wangeditor/editor": "^5.1.23",
"@wangeditor/editor-for-vue": "^5.1.10",
"@zxcvbn-ts/core": "^3.0.4",
"animate.css": "^4.1.1",
"axios": "^1.7.2",
"axios": "^1.7.9",
"cropperjs": "^1.6.2",
"dayjs": "^1.11.11",
"dayjs": "^1.11.13",
"driver.js": "^1.3.1",
"echarts": "^5.5.1",
"echarts": "^5.6.0",
"echarts-wordcloud": "^2.1.0",
"element-plus": "2.7.7",
"element-plus": "2.9.2",
"lodash-es": "^4.17.21",
"mitt": "^3.0.1",
"monaco-editor": "^0.50.0",
"monaco-editor": "^0.52.2",
"nprogress": "^0.2.0",
"pinia": "^2.1.7",
"pinia-plugin-persistedstate": "^3.2.1",
"qrcode": "^1.5.3",
"qs": "^6.12.3",
"url": "^0.11.3",
"vue": "3.4.32",
"vue-draggable-plus": "^0.5.2",
"vue-i18n": "9.13.1",
"pinia": "^2.3.0",
"pinia-plugin-persistedstate": "^4.2.0",
"qrcode": "^1.5.4",
"qs": "^6.13.1",
"url": "^0.11.4",
"vue": "3.5.13",
"vue-draggable-plus": "^0.6.0",
"vue-i18n": "11.0.1",
"vue-json-pretty": "^2.4.0",
"vue-router": "^4.4.0",
"vue-router": "^4.5.0",
"vue-types": "^5.1.3",
"xgplayer": "^3.0.18"
"xgplayer": "^3.0.20"
},
"devDependencies": {
"@commitlint/cli": "^19.3.0",
"@commitlint/config-conventional": "^19.2.2",
"@iconify/json": "^2.2.229",
"@intlify/unplugin-vue-i18n": "^4.0.0",
"@commitlint/cli": "^19.6.1",
"@commitlint/config-conventional": "^19.6.0",
"@iconify/json": "^2.2.293",
"@intlify/unplugin-vue-i18n": "^6.0.3",
"@types/fs-extra": "^11.0.4",
"@types/inquirer": "^9.0.7",
"@types/lodash-es": "^4.17.12",
"@types/mockjs": "^1.0.10",
"@types/node": "^20.14.11",
"@types/node": "^22.10.5",
"@types/nprogress": "^0.2.3",
"@types/qrcode": "^1.5.5",
"@types/qs": "^6.9.15",
"@types/qs": "^6.9.17",
"@types/sortablejs": "^1.15.8",
"@typescript-eslint/eslint-plugin": "^7.16.1",
"@typescript-eslint/parser": "^7.16.1",
"@unocss/transformer-variant-group": "^0.61.5",
"@vitejs/plugin-legacy": "^5.4.1",
"@vitejs/plugin-vue": "^5.0.5",
"@vitejs/plugin-vue-jsx": "^4.0.0",
"autoprefixer": "^10.4.19",
"chalk": "^5.3.0",
"consola": "^3.2.3",
"eslint": "^9.7.0",
"@typescript-eslint/eslint-plugin": "^8.19.1",
"@typescript-eslint/parser": "^8.19.1",
"@unocss/transformer-variant-group": "^0.65.4",
"@vitejs/plugin-legacy": "^6.0.0",
"@vitejs/plugin-vue": "^5.2.1",
"@vitejs/plugin-vue-jsx": "^4.1.1",
"autoprefixer": "^10.4.20",
"chalk": "^5.4.1",
"consola": "^3.3.3",
"eslint": "^9.17.0",
"eslint-config-prettier": "^9.1.0",
"eslint-define-config": "^2.1.0",
"eslint-plugin-prettier": "^5.2.1",
"eslint-plugin-vue": "^9.27.0",
"esno": "^4.7.0",
"eslint-plugin-vue": "^9.32.0",
"esno": "^4.8.0",
"fs-extra": "^11.2.0",
"husky": "^9.1.0",
"inquirer": "^10.0.3",
"less": "^4.2.0",
"lint-staged": "^15.2.7",
"husky": "^9.1.7",
"inquirer": "^12.3.0",
"less": "^4.2.1",
"lint-staged": "^15.3.0",
"mockjs": "^1.1.0",
"plop": "^4.0.1",
"postcss": "^8.4.39",
"postcss": "^8.4.49",
"postcss-html": "^1.7.0",
"postcss-less": "^6.0.0",
"prettier": "^3.3.3",
"prettier": "^3.4.2",
"rimraf": "^6.0.1",
"rollup": "^4.18.1",
"rollup-plugin-visualizer": "^5.12.0",
"stylelint": "^16.7.0",
"rollup": "^4.30.1",
"rollup-plugin-visualizer": "^5.14.0",
"stylelint": "^16.12.0",
"stylelint-config-html": "^1.1.0",
"stylelint-config-recommended": "^14.0.1",
"stylelint-config-standard": "^36.0.1",
"stylelint-order": "^6.0.4",
"terser": "^5.31.3",
"typescript": "5.5.3",
"typescript-eslint": "^7.16.1",
"unocss": "^0.61.5",
"vite": "5.3.4",
"terser": "^5.37.0",
"typescript": "5.7.3",
"typescript-eslint": "^8.19.1",
"unocss": "^0.65.4",
"vite": "6.0.7",
"vite-plugin-ejs": "^1.7.0",
"vite-plugin-eslint": "^1.8.1",
"vite-plugin-mock": "2.9.6",
@ -120,9 +120,9 @@
"vite-plugin-style-import": "2.0.0",
"vite-plugin-svg-icons": "^2.0.1",
"vite-plugin-url-copy": "^1.1.4",
"vue-tsc": "^2.0.26"
"vue-tsc": "^2.2.0"
},
"packageManager": "pnpm@8.1.0",
"packageManager": "pnpm@9.15.3",
"engines": {
"node": ">=18.0.0",
"pnpm": ">=8.1.0"

11876
pnpm-lock.yaml Normal file

File diff suppressed because it is too large Load Diff

View File

@ -54,11 +54,11 @@ const props = defineProps({
default: false
},
loadingIcon: {
type: [String, Object] as PropType<String | Component>,
type: [String, Object] as PropType<string | Component>,
default: undefined
},
icon: {
type: [String, Object] as PropType<String | Component>,
type: [String, Object] as PropType<string | Component>,
default: undefined
},
autofocus: {
@ -82,7 +82,7 @@ const props = defineProps({
default: false
},
tag: {
type: [String, Object] as PropType<String | Component>,
type: [String, Object] as PropType<string | Component>,
default: 'button'
}
})

View File

@ -12,7 +12,7 @@ const props = withDefaults(
language?: string
themeSelector?: boolean
theme?: string
editorOption?: Object
editorOption?: object
modelValue: string
}>(),
{

View File

@ -161,7 +161,7 @@ export const initModel = (schema: FormSchema[], formModel: Recordable) => {
// 如果 schema 对应的 field 不存在,则删除 model 中的对应的 field
for (let i = 0; i < schema.length; i++) {
const key = schema[i].field
if (!get(model, key) && get(model, key) !== 0) {
if (!Object.prototype.hasOwnProperty.call(model, key)) {
delete model[key]
}
}

View File

@ -138,7 +138,7 @@ export interface SelectComponentProps extends Omit<Partial<ISelectProps>, 'optio
children?: string
}
on?: {
change?: (value: string | number | boolean | Object) => void
change?: (value: string | number | boolean | object) => void
visibleChange?: (visible: boolean) => void
removeTag?: (tag: any) => void
clear?: () => void
@ -186,7 +186,7 @@ export interface SelectV2ComponentProps {
placement?: AutocompleteProps['placement']
collapseTagsTooltip?: boolean
on?: {
change?: (value: string | number | boolean | Object) => void
change?: (value: string | number | boolean | object) => void
visibleChange?: (visible: boolean) => void
removeTag?: (tag: any) => void
clear?: () => void
@ -556,7 +556,7 @@ export interface TreeSelectComponentProps
allowDrag?: (...args: any[]) => boolean
allowDrop?: (...args: any[]) => boolean
on?: {
change?: (value: string | number | boolean | Object) => void
change?: (value: string | number | boolean | object) => void
visibleChange?: (visible: boolean) => void
removeTag?: (tag: any) => void
clear?: () => void

View File

@ -32,7 +32,7 @@ const setSystemTheme = (color: string) => {
setCssVar('--el-color-primary', color)
appStore.setTheme({ elColorPrimary: color })
const leftMenuBgColor = useCssVar('--left-menu-bg-color', document.documentElement)
setMenuTheme(trim(unref(leftMenuBgColor)))
setMenuTheme(trim(unref(leftMenuBgColor) as string))
}
//

View File

@ -0,0 +1,3 @@
import Tree from './src/Tree.vue'
export { Tree }

View File

@ -0,0 +1,147 @@
<script lang="tsx" setup>
import { defineProps, defineEmits, ref, CSSProperties } from 'vue'
import { ElTree } from 'element-plus'
interface TreeProps {
data: any[]
treeProps?: Record<string, any>
width?: string
height?: string
}
const props = defineProps<TreeProps>()
const emit = defineEmits<{
(e: 'node-click', nodeData: any): void
(e: 'node-expand', nodeData: any): void
(e: 'node-collapse', nodeData: any): void
}>()
const treeContainer = ref<any>(null)
const showTreeMenu = ref(false)
const contextNode = ref<any>(null)
const menuStyle = ref<any>({})
const defaultWidth = '300px'
const defaultHeight = '400px'
//
const closeTreeMenu = () => {
showTreeMenu.value = false
document.removeEventListener('click', closeTreeMenu)
document.removeEventListener('contextmenu', closeTreeMenu)
}
//
const openTreeMenu = (event: MouseEvent, data: any, _node: any, _target: HTMLElement) => {
contextNode.value = data
if (!treeContainer.value) return
const containerRect = treeContainer.value.getBoundingClientRect()
const nodeRect = (event.target as HTMLElement).getBoundingClientRect()
//
const top = nodeRect.top - containerRect.top + treeContainer.value.scrollTop
const left = nodeRect.left - containerRect.left + treeContainer.value.scrollLeft
menuStyle.value = {
position: 'absolute',
top: `${top + 20}px`,
left: `${left + 20}px`
}
showTreeMenu.value = true
//
document.addEventListener('click', closeTreeMenu)
document.addEventListener('contextmenu', closeTreeMenu)
}
//
const handleNodeClick = (data: any) => {
emit('node-click', data)
closeTreeMenu()
}
//
const handleNodeExpand = (data: any) => {
emit('node-expand', data)
closeTreeMenu()
}
//
const handleNodeCollapse = (data: any) => {
emit('node-collapse', data)
closeTreeMenu()
}
//
const containerStyle: CSSProperties = {
position: 'relative',
overflow: 'auto',
width: props.width ?? defaultWidth,
height: props.height ?? defaultHeight
}
</script>
<template>
<div class="tree-container" ref="treeContainer" :style="containerStyle">
<ElTree
v-bind="treeProps"
:data="data"
@node-click="handleNodeClick"
@node-expand="handleNodeExpand"
@node-collapse="handleNodeCollapse"
@node-contextmenu="openTreeMenu"
>
<template #default="{ node }">
<!-- 如果使用者提供了 render-node slot则渲染使用者的内容 -->
<template v-if="$slots['render-node']">
<slot name="render-node" :node="node"></slot>
</template>
<!-- 否则使用默认节点显示比如使用 node.label -->
<template v-else>
<span>{{ node.label }}</span>
</template>
</template>
</ElTree>
<div class="treeMenu" v-show="showTreeMenu" :style="menuStyle">
<!-- 用户通过 context-menu slot 来自定义菜单内容 -->
<slot name="context-menu" :node="contextNode" :data="contextNode">
<!-- 如果用户不提供 context-menu slot可给一个默认内容 -->
<div style="padding: 8px">No menu defined</div>
</slot>
</div>
<slot></slot>
</div>
</template>
<style scoped lang="less">
.treeMenu {
position: absolute;
padding: 5px;
font-size: 14px;
color: #606266;
background-color: rgb(255 255 255 / 90%);
border: 1px solid #dcdcdc;
border-radius: 5px;
box-shadow: 0 4px 10px rgb(0 0 0 / 40%);
/* 移除 overflow: hidden; 或尝试不使用负的 top 值 */
/* overflow: hidden; */
&::after {
position: absolute;
/* 将箭头向上移动到菜单外部 */
top: -6px;
left: 50%;
border-right: 6px solid transparent;
border-bottom: 6px solid rgb(206 194 194);
/* 创建一个向上的箭头 */
border-left: 6px solid transparent;
content: '';
transform: translateX(-50%);
}
}
</style>

View File

@ -21,7 +21,7 @@ export const NO_REDIRECT_WHITE_LIST = ['/login']
/**
*
*/
export const NO_RESET_WHITE_LIST = ['Redirect', 'Login', 'NoFind', 'Root']
export const NO_RESET_WHITE_LIST = ['Redirect', 'RedirectWrap', 'Login', 'NoFind', 'Root']
/**
*

View File

@ -13,7 +13,7 @@ export const useNProgress = () => {
await nextTick()
const bar = document.getElementById('nprogress')?.getElementsByClassName('bar')[0] as ElRef
if (bar) {
bar.style.background = unref(primaryColor.value)
bar.style.background = unref(primaryColor.value) as string
}
}

View File

@ -25,22 +25,24 @@ export const useTable = (config: UseTableConfig) => {
const pageSize = ref(10)
const total = ref(0)
const dataList = ref<any[]>([])
let isPageSizeChange = false
watch(
() => currentPage.value,
() => {
methods.getList()
if (!isPageSizeChange) methods.getList()
isPageSizeChange = false
}
)
watch(
() => pageSize.value,
() => {
// 当前页不为1时修改页数后会导致多次调用getList方法
if (unref(currentPage) === 1) {
methods.getList()
} else {
currentPage.value = 1
isPageSizeChange = true
methods.getList()
}
}

View File

@ -190,7 +190,8 @@ export default {
personalCenter: 'Personal center',
personal: 'Personal',
avatars: 'Avatars',
iAgree: 'I agree'
iAgree: 'I agree',
tree: 'Tree'
},
permission: {
hasPermission: 'Please set the operation permission value'
@ -393,6 +394,11 @@ export default {
logoStyle: 'Logo style',
size: 'size config'
},
treeDemo: {
treeTitle: 'Tree control (right-click node to customize menu options)',
message:
'The tree component is based on the secondary packaging of the tree component of ElementPlus'
},
highlightDemo: {
highlight: 'Highlight',
message: 'The best time to plant a tree is ten years ago, followed by now.',

View File

@ -186,7 +186,8 @@ export default {
personalCenter: '个人中心',
personal: '个人',
avatars: '头像列表',
iAgree: '我同意'
iAgree: '我同意',
tree: 'Tree 树形控件'
},
permission: {
hasPermission: '请设置操作权限值'
@ -385,6 +386,10 @@ export default {
logoStyle: 'logo样式',
size: '大小配置'
},
treeDemo: {
treeTitle: '树形控件(节点右键可自定义菜单选项)',
message: '基于 ElementPlus 的 Tree 组件二次封装'
},
highlightDemo: {
highlight: '高亮',
message: '种一棵树最好的时间是十年前,其次就是现在。',

View File

@ -20,7 +20,7 @@ export const constantRouterMap: AppRouteRecordRaw[] = [
{
path: '/redirect',
component: Layout,
name: 'Redirect',
name: 'RedirectWrap',
children: [
{
path: '/redirect/:path(.*)',

View File

@ -287,11 +287,11 @@ export const useAppStore = defineStore('app', {
// 左侧菜单选中背景颜色
leftMenuBgActiveColor: isDarkColor
? 'var(--el-color-primary)'
: hexToRGB(unref(primaryColor), 0.1),
: hexToRGB(unref(primaryColor) as string, 0.1),
// 左侧菜单收起选中背景颜色
leftMenuCollapseBgActiveColor: isDarkColor
? 'var(--el-color-primary)'
: hexToRGB(unref(primaryColor), 0.1),
: hexToRGB(unref(primaryColor) as string, 0.1),
// 左侧菜单字体颜色
leftMenuTextColor: isDarkColor ? '#bfcbd9' : '#333',
// 左侧菜单选中字体颜色

View File

@ -77,9 +77,20 @@ export const usePermissionStore = defineStore('permission', {
this.menuTabRouters = routers
}
},
persist: {
paths: ['routers', 'addRouters', 'menuTabRouters']
}
persist: [
{
pick: ['routers'],
storage: localStorage
},
{
pick: ['addRouters'],
storage: localStorage
},
{
pick: ['menuTabRouters'],
storage: localStorage
}
]
})
export const usePermissionStoreWithOut = () => {

View File

@ -0,0 +1,252 @@
<script setup lang="tsx">
import { Icon } from '@/components/Icon'
import { Tree } from '@/components/Tree'
import { ContentWrap } from '@/components/ContentWrap'
import { useI18n } from 'vue-i18n'
import { ElMessage, ElMessageBox } from 'element-plus'
import { ref } from 'vue'
const { t } = useI18n()
const treeData = ref([
{
id: 1,
name: '北京',
children: [
{
id: 5,
name: '朝阳',
children: [
{
id: 17,
name: '双塔',
children: []
},
{
id: 18,
name: '龙城',
children: []
}
]
},
{
id: 6,
name: '丰台',
children: [
{
id: 19,
name: '新村',
children: []
},
{
id: 20,
name: '大红门',
children: []
},
{
id: 21,
name: '长辛店',
children: [
{
id: 22,
name: '东山坡',
children: []
},
{
id: 23,
name: '北关',
children: []
},
{
id: 24,
name: '光明里',
children: []
},
{
id: 25,
name: '赵辛店',
children: []
},
{
id: 26,
name: '西峰寺',
children: []
}
]
}
]
},
{
id: 7,
name: '海淀',
children: []
},
{
id: 8,
name: '房山',
children: []
},
{
id: 10,
name: '顺义',
children: []
}
]
},
{
id: 2,
name: '上海',
children: [
{
id: 11,
name: '黄埔',
children: []
},
{
id: 12,
name: '徐汇',
children: []
}
]
},
{
id: 3,
name: '广州',
children: [
{
id: 13,
name: '荔湾',
children: []
},
{
id: 14,
name: '白云',
children: []
},
{
id: 15,
name: '越秀',
children: []
},
{
id: 16,
name: '南沙',
children: []
}
]
}
])
const handleNodeClick = (data: any) => {
console.log('Node clicked:', data)
}
const addOrg = (node: any) => {
ElMessageBox.prompt('请输入分组名称', '添加子分组', {
confirmButtonText: '确定',
cancelButtonText: '取消',
inputPattern: /\S/,
inputErrorMessage: '分组名称不能为空'
}).then(({ value }) => {
node.children.push({
id: node.children.length + 1,
name: value,
children: []
})
ElMessage.success('添加成功')
})
}
const editOrg = (node: any) => {
ElMessageBox.prompt('请输入新的分组名称', '修改分组名称', {
confirmButtonText: '确定',
cancelButtonText: '取消',
inputValue: node.name,
inputPattern: /\S/,
inputErrorMessage: '分组名称不能为空'
}).then(({ value }) => {
node.name = value
ElMessage.success('修改成功')
})
}
const deleteOrg = (node: any) => {
ElMessageBox.confirm(`删除 [${node.name}] 分组、下级子分组 <br>是否继续?`, '提示', {
dangerouslyUseHTMLString: true,
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
center: true
}).then(() => {
const id = node.id
// treeData
const deleteNode = (data: any) => {
for (let i = 0; i < data.length; i++) {
if (data[i].id === id) {
data.splice(i, 1)
return
}
if (data[i].children) {
deleteNode(data[i].children)
}
}
}
deleteNode(treeData.value)
ElMessage.success('删除成功')
})
}
</script>
<template>
<ContentWrap :title="t('treeDemo.treeTitle')" :message="t('qrcodeDemo.qrcodeDes')">
<Tree
:data="treeData"
:tree-props="{
highlightCurrent: true,
nodeKey: 'id',
props: {
children: 'children',
label: 'name'
}
}"
width="300px"
height="400px"
@node-click="handleNodeClick"
>
<!-- 自定义右键菜单 -->
<template #context-menu="{ node }">
<div class="menuItem" @click="addOrg(node)">
<Icon icon="ep:plus" style="color: #1e9fff" />
<span>添加子分组</span>
</div>
<div class="menuItem" @click="editOrg(node)">
<Icon icon="ep:edit-pen" style="color: #1e9fff" />
修改分组名称
</div>
<div class="menuItem" @click="deleteOrg(node)">
<Icon icon="ep:delete" style="color: #1e9fff" />
删除分组及子分组
</div>
</template>
<!-- 自定义节点显示 -->
<!-- <template #render-node="{ node }">
<span v-if="node.isLeaf">[FILE] {{ node.label }}</span>
<span v-else>[FOLDER] {{ node.label }}</span>
</template> -->
</Tree>
</ContentWrap>
</template>
<style lang="less" scoped>
.menuItem {
display: flex;
padding: 2px 10px;
text-align: left;
box-sizing: border-box;
align-items: center; /* 垂直居中 */
gap: 5px; /* 图标和文字之间的间距,可根据需要调整 */
}
.menuItem:hover {
cursor: pointer;
background-color: #eee;
}
</style>