release: update 4.3.0

This commit is contained in:
xiaoxian521 2023-06-05 15:44:49 +08:00
parent 4e95672cb4
commit d9ab1b1198
38 changed files with 2065 additions and 1236 deletions

21
.dockerignore Normal file
View File

@ -0,0 +1,21 @@
node_modules
.DS_Store
dist
dist-ssr
*.local
.eslintcache
report.html
yarn.lock
npm-debug.log*
.pnpm-error.log*
.pnpm-debug.log
tests/**/coverage/
# Editor directories and files
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
tsconfig.tsbuildinfo

View File

@ -1,6 +1,6 @@
# 预发布也需要生产环境的行为
# https://cn.vitejs.dev/guide/env-and-mode.html#modes
NODE_ENV=production
# NODE_ENV = development
VITE_PUBLIC_PATH = /

1
.gitignore vendored
View File

@ -4,6 +4,7 @@ dist
dist-ssr
*.local
.eslintcache
.stylelintcache
report.html
yarn.lock

View File

@ -1,19 +1,19 @@
{
"Vue3.0快速生成模板": {
"scope": "vue",
"prefix": "Vue3.0",
"body": [
"<template>",
"\t<div>\n",
"\t</div>",
"\t<div>test</div>",
"</template>\n",
"<script lang='ts'>",
"export default {",
"\tsetup(){",
"\t\treturn{\n\n\t\t}",
"\t},",
"\tsetup() {",
"\t\treturn {}",
"\t}",
"}",
"</script>\n",
"<style scoped>\n",
"<style lang='scss' scoped>\n",
"</style>",
"$2"
],

View File

@ -1,14 +1,14 @@
{
"Vue3.2+快速生成模板": {
"scope": "vue",
"prefix": "Vue3.2+",
"body": [
"<script setup lang='ts'>",
"</script>\n",
"<template>",
"\t<div>\n",
"\t</div>",
"\t<div>test</div>",
"</template>\n",
"<style scoped>\n",
"<style lang='scss' scoped>\n",
"</style>",
"$2"
],

20
.vscode/vue3.3.code-snippets vendored Normal file
View File

@ -0,0 +1,20 @@
{
"Vue3.3+defineOptions快速生成模板": {
"scope": "vue",
"prefix": "Vue3.3+",
"body": [
"<script setup lang='ts'>",
"defineOptions({",
"\tname: ''",
"})",
"</script>\n",
"<template>",
"\t<div>test</div>",
"</template>\n",
"<style lang='scss' scoped>\n",
"</style>",
"$2"
],
"description": "Vue3.3+defineOptions快速生成模板"
}
}

20
Dockerfile Normal file
View File

@ -0,0 +1,20 @@
FROM node:16-alpine as build-stage
WORKDIR /app
RUN corepack enable
RUN corepack prepare pnpm@7.32.1 --activate
RUN npm config set registry https://registry.npmmirror.com
COPY .npmrc package.json pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile
COPY . .
RUN pnpm build
FROM nginx:stable-alpine as production-stage
COPY --from=build-stage /app/dist /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

View File

@ -15,39 +15,22 @@ The simplified version is based on the shelf extracted from [vue-pure-admin](htt
## Docs
- [Click me to view the domestic documentation site](https://yiming_chang.gitee.io/pure-admin-doc)
- [Click me to view foreign document site](https://pure-admin.github.io/pure-admin-doc)
- [documentation site](https://yiming_chang.gitee.io/pure-admin-doc)
## Preview
- [Click me to view the preview station](https://pure-admin-thin.netlify.app/#/login)
## Usage
## Maintainer
### Installation dependencies
pnpm install
### Install a package
pnpm add packageName
### Uninstall a package
pnpm remove packageName
I think you should fork the project first to develop, so that you can pull the update synchronously when I update! ! !
## Supporting video tutorial
bilibili: https://www.bilibili.com/video/BV1534y1S7HV/
[xiaoxian521](https://github.com/xiaoxian521)
## ⚠️ Attention
- The Lite version does not accept any issues and prs. If you have any questions, please go to the full version https://github.com/pure-admin/vue-pure-admin/issues/new/choose to mention, thank you! ! !
- The Lite version does not accept any issues and prs. If you have any questions, please go to the full version [issues](https://github.com/pure-admin/vue-pure-admin/issues/new/choose) to mention, thank you!
## License
In principle, no fees and copyrights are charged, and you can use it with confidence, but if you need secondary open source, please contact the author for permission!
In principle, no fees and copyrights are charged, and it is commercially available, but if you need secondary open source (such as using this platform for secondary development and open source, the front-end code must be open source and free), please contact the author for permission! (Free, just take a record)
[MIT © 2020-present, pure-admin](./LICENSE)

View File

@ -10,58 +10,31 @@
## 版本选择
当前是非国际化版本,如果您需要国际化版本 [请点击](https://github.com/pure-admin/pure-admin-thin/tree/i18n)
当前是非国际化版本,如果您需要国际化版本 [请点击](https://github.com/pure-admin/pure-admin-thin/tree/i18n)
## 配套视频
- [点我查看教程](https://www.bilibili.com/video/BV1kg411v7QT)
- [点我查看 UI 设计](https://www.bilibili.com/video/BV17g411T7rq)
## 配套文档
## 配套保姆级文档
- [点我查看国内文档站](https://yiming_chang.gitee.io/pure-admin-doc)
- [点我查看国外文档站](https://pure-admin.github.io/pure-admin-doc)
- [查看文档](https://yiming_chang.gitee.io/pure-admin-doc)
## 预览
- [点我查看预览](https://pure-admin-thin.netlify.app/#/login)
- [查看预览](https://pure-admin-thin.netlify.app/#/login)
## 维护者
[xiaoxian521](https://github.com/xiaoxian521)
## 支持
如果你觉得这个项目对您有帮助,可以帮作者买一杯果汁 🍹 表示支持
<img src="https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/f69bf13c5b854ed5b699807cafa0e3ce~tplv-k3u1fbpfcp-zoom-in-crop-mark:1304:0:0:0.awebp?" width="150px" height="150px" />
## `QQ` 交流群
[点击去加入](https://yiming_chang.gitee.io/pure-admin-doc/pages/support/#qq-%E4%BA%A4%E6%B5%81%E7%BE%A4)
## 用法
### 安装依赖
pnpm install
### 安装一个包
pnpm add 包名
### 卸载一个包
pnpm remove 包名
我认为你应该先 `fork` 项目去开发,以便我更新时您可以同步拉取更新!!!
## ⚠️ 注意
- 精简版不接受任何 `issues``pr`,如果有问题请到完整版 [issues](https://github.com/pure-admin/vue-pure-admin/issues/new/choose) 去提,谢谢!
- 精简版不接受任何 `issues``pr`,如果有问题请到完整版 [issues](https://github.com/pure-admin/vue-pure-admin/issues/new/choose) 去提,谢谢!
## 许可证
原则上不收取任何费用及版权,可以放心使用,不过如需二次开源(比如用此平台二次开发并开源)请联系作者获取许可!
原则上不收取任何费用及版权,可商用,不过如需二次开源(比如用此平台二次开发并开源,要求前端代码必须开源免费)请联系作者获取许可!(免费,走个记录而已)
[MIT © 2020-present, pure-admin](./LICENSE)

View File

@ -1,6 +1,6 @@
{
"name": "pure-admin-thin",
"version": "4.1.0",
"version": "4.3.0",
"private": true,
"scripts": {
"dev": "NODE_OPTIONS=--max-old-space-size=4096 vite",
@ -13,10 +13,10 @@
"typecheck": "tsc --noEmit && vue-tsc --noEmit --skipLibCheck",
"svgo": "svgo -f src/assets/svg -o src/assets/svg",
"cloc": "NODE_OPTIONS=--max-old-space-size=4096 cloc . --exclude-dir=node_modules --exclude-lang=YAML",
"clean:cache": "rm -rf node_modules && rm -rf .eslintcache && pnpm install",
"clean:cache": "rimraf node_modules && rimraf .eslintcache && pnpm install",
"lint:eslint": "eslint --cache --max-warnings 0 \"{src,mock,build}/**/*.{vue,js,ts,tsx}\" --fix",
"lint:prettier": "prettier --write \"src/**/*.{js,ts,json,tsx,css,scss,vue,html,md}\"",
"lint:stylelint": "stylelint --cache --fix \"**/*.{html,vue,css,scss}\" --cache --cache-location node_modules/.cache/stylelint/",
"lint:stylelint": "stylelint \"**/*.{html,vue,css,scss}\" --fix --cache --cache-location node_modules/.cache/stylelint/",
"lint:lint-staged": "lint-staged -c ./.husky/lintstagedrc.js",
"lint:pretty": "pretty-quick --staged",
"lint": "pnpm lint:eslint && pnpm lint:prettier && pnpm lint:stylelint",
@ -30,69 +30,69 @@
],
"dependencies": {
"@pureadmin/descriptions": "^1.1.1",
"@pureadmin/table": "^2.1.0",
"@pureadmin/utils": "^1.8.9",
"@pureadmin/table": "^2.2.0",
"@pureadmin/utils": "^1.9.3",
"@vueuse/core": "^10.1.2",
"@vueuse/motion": "2.0.0-beta.12",
"@vueuse/motion": "^2.0.0",
"animate.css": "^4.1.1",
"axios": "^1.4.0",
"dayjs": "^1.11.7",
"dayjs": "^1.11.8",
"echarts": "^5.4.2",
"element-plus": "^2.3.4",
"element-plus": "^2.3.6",
"element-resize-detector": "^1.2.4",
"js-cookie": "^3.0.5",
"mitt": "^3.0.0",
"mockjs": "^1.1.0",
"nprogress": "^0.2.0",
"path": "^0.12.7",
"pinia": "^2.0.36",
"qs": "^6.11.1",
"pinia": "^2.1.3",
"qs": "^6.11.2",
"responsive-storage": "^2.2.0",
"sortablejs": "^1.15.0",
"vue": "^3.3.1",
"vue-router": "^4.1.6",
"vue-types": "^5.0.2"
"vue": "^3.3.4",
"vue-router": "^4.2.2",
"vue-types": "^5.0.3"
},
"devDependencies": {
"@commitlint/cli": "^17.6.3",
"@commitlint/config-conventional": "^17.6.3",
"@commitlint/cli": "^17.6.5",
"@commitlint/config-conventional": "^17.6.5",
"@iconify-icons/ep": "^1.2.11",
"@iconify-icons/ri": "^1.2.7",
"@iconify-icons/ri": "^1.2.8",
"@iconify/vue": "^4.1.1",
"@pureadmin/theme": "^3.0.0",
"@types/element-resize-detector": "1.1.3",
"@types/js-cookie": "^3.0.3",
"@types/mockjs": "^1.0.7",
"@types/node": "^18.15.12",
"@types/node": "^20.2.5",
"@types/nprogress": "0.2.0",
"@types/qs": "^6.9.7",
"@types/sortablejs": "^1.15.1",
"@typescript-eslint/eslint-plugin": "^5.59.5",
"@typescript-eslint/parser": "^5.59.5",
"@vitejs/plugin-vue": "^4.2.2",
"@typescript-eslint/eslint-plugin": "^5.59.8",
"@typescript-eslint/parser": "^5.59.8",
"@vitejs/plugin-vue": "^4.2.3",
"@vitejs/plugin-vue-jsx": "^3.0.1",
"@vue/eslint-config-prettier": "^7.1.0",
"@vue/eslint-config-typescript": "^11.0.3",
"autoprefixer": "^10.4.14",
"cloc": "^2.11.0",
"cssnano": "^6.0.1",
"eslint": "^8.40.0",
"eslint": "^8.42.0",
"eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-vue": "^9.12.0",
"eslint-plugin-vue": "^9.14.1",
"husky": "^8.0.3",
"lint-staged": "^13.2.2",
"picocolors": "^1.0.0",
"postcss": "^8.4.23",
"postcss": "^8.4.24",
"postcss-html": "^1.5.0",
"postcss-import": "^15.1.0",
"postcss-scss": "^4.0.6",
"prettier": "^2.8.7",
"pretty-quick": "3.1.1",
"rimraf": "^5.0.0",
"prettier": "^2.8.8",
"pretty-quick": "^3.1.3",
"rimraf": "^5.0.1",
"rollup-plugin-visualizer": "^5.9.0",
"sass": "^1.62.1",
"sass-loader": "^13.2.2",
"stylelint": "^15.6.1",
"sass-loader": "^13.3.1",
"stylelint": "^15.6.3",
"stylelint-config-html": "^1.1.0",
"stylelint-config-recess-order": "^4.0.0",
"stylelint-config-recommended": "^12.0.0",
@ -105,16 +105,16 @@
"stylelint-scss": "^5.0.0",
"svgo": "^3.0.2",
"tailwindcss": "^3.3.2",
"terser": "^5.17.1",
"typescript": "^5.0.4",
"vite": "^4.3.5",
"terser": "^5.17.7",
"typescript": "5.0.4",
"vite": "^4.3.9",
"vite-plugin-cdn-import": "^0.3.5",
"vite-plugin-compression": "^0.5.1",
"vite-plugin-mock": "^2.9.6",
"vite-plugin-remove-console": "^2.1.1",
"vite-svg-loader": "^4.0.0",
"vue-eslint-parser": "^9.2.1",
"vue-tsc": "^1.6.4"
"vue-eslint-parser": "^9.3.0",
"vue-tsc": "^1.6.5"
},
"pnpm": {
"peerDependencyRules": {
@ -126,6 +126,7 @@
},
"allowedDeprecatedVersions": {
"sourcemap-codec": "*",
"w3c-hr-time": "*",
"stable": "*"
}
},

2660
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,5 @@
{
"Version": "4.1.0",
"Version": "4.3.0",
"Title": "PureAdmin",
"FixedHeader": true,
"HiddenSideBar": false,

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

BIN
src/assets/user.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

View File

@ -12,6 +12,7 @@ import type {
const dialogStore = ref<Array<DialogOptions>>([]);
/** 打开弹框 */
const addDialog = (options: DialogOptions) => {
const open = () =>
dialogStore.value.push(Object.assign(options, { visible: true }));
@ -24,16 +25,40 @@ const addDialog = (options: DialogOptions) => {
}
};
/** 关闭弹框 */
const closeDialog = (options: DialogOptions, index: number, args?: any) => {
dialogStore.value.splice(index, 1);
options.closeCallBack && options.closeCallBack({ options, index, args });
};
/**
* @description
* @param value
* @param key `title`
* @param index `0``index`
*/
const updateDialog = (value: any, key = "title", index = 0) => {
dialogStore.value[index][key] = value;
};
/** 关闭所有弹框 */
const closeAllDialog = () => {
dialogStore.value = [];
};
/** 使`addDialog`
* https://github.com/pure-admin/vue-pure-admin/blob/main/src/App.vue#L4
* https://github.com/pure-admin/vue-pure-admin/blob/main/src/App.vue#L13
* https://github.com/pure-admin/vue-pure-admin/blob/main/src/App.vue#L18
*/
const ReDialog = withInstall(reDialog);
export type { EventType, ArgsType, DialogProps, ButtonProps, DialogOptions };
export { ReDialog, dialogStore, addDialog, closeDialog, closeAllDialog };
export {
ReDialog,
dialogStore,
addDialog,
closeDialog,
updateDialog,
closeAllDialog
};

View File

@ -1,13 +1,17 @@
<script setup lang="ts">
import { computed } from "vue";
import { isFunction } from "@pureadmin/utils";
import {
type DialogOptions,
type ButtonProps,
type EventType,
closeDialog,
dialogStore,
closeDialog
type EventType,
type ButtonProps,
type DialogOptions
} from "./index";
import { ref, computed } from "vue";
import { isFunction } from "@pureadmin/utils";
import Fullscreen from "@iconify-icons/ri/fullscreen-fill";
import ExitFullscreen from "@iconify-icons/ri/fullscreen-exit-fill";
const fullscreen = ref(false);
const footerButtons = computed(() => {
return (options: DialogOptions) => {
@ -47,11 +51,22 @@ const footerButtons = computed(() => {
};
});
const fullscreenClass = computed(() => {
return [
"el-icon",
"el-dialog__close",
"-translate-x-2",
"cursor-pointer",
"hover:!text-[red]"
];
});
function eventsCallBack(
event: EventType,
options: DialogOptions,
index: number
) {
fullscreen.value = options?.fullscreen ?? false;
if (options?.[event] && isFunction(options?.[event])) {
return options?.[event]({ options, index });
}
@ -69,25 +84,49 @@ function handleClose(
<template>
<el-dialog
class="pure-dialog"
v-for="(options, index) in dialogStore"
:key="index"
v-bind="options"
v-model="options.visible"
@opened="eventsCallBack('open', options, index)"
:fullscreen="fullscreen ? true : options?.fullscreen ? true : false"
@close="handleClose(options, index)"
@opened="eventsCallBack('open', options, index)"
@openAutoFocus="eventsCallBack('openAutoFocus', options, index)"
@closeAutoFocus="eventsCallBack('closeAutoFocus', options, index)"
>
<!-- header -->
<template
v-if="options?.headerRenderer"
v-if="options?.fullscreenIcon || options?.headerRenderer"
#header="{ close, titleId, titleClass }"
>
<div
v-if="options?.fullscreenIcon"
class="flex items-center justify-between"
>
<span :id="titleId" :class="titleClass">{{ options?.title }}</span>
<i
v-if="!options?.fullscreen"
:class="fullscreenClass"
@click="fullscreen = !fullscreen"
>
<IconifyIconOffline
class="pure-dialog-svg"
:icon="
options?.fullscreen
? ExitFullscreen
: fullscreen
? ExitFullscreen
: Fullscreen
"
/>
</i>
</div>
<component
v-else
:is="options?.headerRenderer({ close, titleId, titleClass })"
/>
</template>
<!-- default -->
<component
v-bind="options?.props"
:is="options.contentRenderer({ options, index })"

View File

@ -15,8 +15,10 @@ type DialogProps = {
title?: string;
/** `Dialog` 的宽度,默认 `50%` */
width?: string | number;
/** 是否为全屏 `Dialog`,默认 `false` */
/** 是否为全屏 `Dialog`(会一直处于全屏状态,除非弹框关闭),默认 `false``fullscreen` 和 `fullscreenIcon` 都传时只有 `fullscreen` 会生效 */
fullscreen?: boolean;
/** 是否显示全屏操作图标,默认 `false``fullscreen` 和 `fullscreenIcon` 都传时只有 `fullscreen` 会生效 */
fullscreenIcon?: boolean;
/** `Dialog CSS` 中的 `margin-top` 值,默认 `15vh` */
top?: string;
/** 是否需要遮罩层,默认 `true` */

View File

@ -200,9 +200,13 @@ export default defineComponent({
return () => (
<>
<div {...attrs} class="w-[99/100] mt-6 p-2 bg-bg_color">
<div {...attrs} class="w-[99/100] mt-2 px-2 pb-2 bg-bg_color">
<div class="flex justify-between w-full h-[60px] p-4">
<p class="font-bold truncate">{props.title}</p>
{slots?.title ? (
slots.title()
) : (
<p class="font-bold truncate">{props.title}</p>
)}
<div class="flex items-center justify-around">
{slots?.buttons ? (
<div class="flex mr-4">{slots.buttons()}</div>
@ -245,6 +249,7 @@ export default defineComponent({
<el-popover
v-slots={reference}
placement="bottom-start"
popper-style={{ padding: 0 }}
width="160"
trigger="click"

View File

@ -15,6 +15,7 @@ const {
onPanel,
pureApp,
username,
userAvatar,
avatarsStyle,
toggleSideBar
} = useNav();
@ -46,10 +47,7 @@ const {
<!-- 退出登录 -->
<el-dropdown trigger="click">
<span class="el-dropdown-link navbar-bg-hover select-none">
<img
src="https://avatars.githubusercontent.com/u/44761321?v=4"
:style="avatarsStyle"
/>
<img :src="userAvatar" :style="avatarsStyle" />
<p v-if="username" class="dark:text-white">{{ username }}</p>
</span>
<template #dropdown>

View File

@ -46,8 +46,9 @@ notices.value.map(v => (noticesNum.value += v.list.length));
display: flex;
align-items: center;
justify-content: center;
width: 60px;
width: 40px;
height: 48px;
margin-right: 10px;
cursor: pointer;
.header-notice-icon {

View File

@ -1,3 +1,17 @@
<script setup lang="ts">
import ArrowUpLine from "@iconify-icons/ri/arrow-up-line";
import ArrowDownLine from "@iconify-icons/ri/arrow-down-line";
import { useNav } from "@/layout/hooks/useNav";
import mdiKeyboardEsc from "@/assets/svg/keyboard_esc.svg?component";
import enterOutlined from "@/assets/svg/enter_outlined.svg?component";
const props = withDefaults(defineProps<{ total: number }>(), {
total: 0
});
const { device } = useNav();
</script>
<template>
<div class="search-footer text-[#333] dark:text-white">
<span class="search-footer-item">
@ -13,16 +27,15 @@
<mdiKeyboardEsc class="icon" />
关闭
</span>
<p
v-if="device !== 'mobile' && props.total > 0"
class="search-footer-total"
>
{{ props.total }}
</p>
</div>
</template>
<script setup lang="ts">
import ArrowUpLine from "@iconify-icons/ri/arrow-up-line";
import ArrowDownLine from "@iconify-icons/ri/arrow-down-line";
import mdiKeyboardEsc from "@/assets/svg/keyboard_esc.svg?component";
import enterOutlined from "@/assets/svg/enter_outlined.svg?component";
</script>
<style lang="scss" scoped>
.search-footer {
display: flex;
@ -40,5 +53,10 @@ import enterOutlined from "@/assets/svg/enter_outlined.svg?component";
box-shadow: inset 0 -2px #cdcde6, inset 0 0 1px 1px #fff,
0 1px 2px 1px #1e235a66;
}
.search-footer-total {
position: absolute;
right: 20px;
}
}
</style>

View File

@ -7,7 +7,7 @@ import { useNav } from "@/layout/hooks/useNav";
import { ref, computed, shallowRef } from "vue";
import { useDebounceFn, onKeyStroke } from "@vueuse/core";
import { usePermissionStoreHook } from "@/store/modules/permission";
import Search from "@iconify-icons/ep/search";
import Search from "@iconify-icons/ri/search-line";
interface Props {
/** 弹窗显隐 */
@ -24,6 +24,8 @@ const props = withDefaults(defineProps<Props>(), {});
const router = useRouter();
const keyword = ref("");
const scrollbarRef = ref();
const resultRef = ref();
const activePath = ref("");
const inputRef = ref<HTMLInputElement | null>(null);
const resultOptions = shallowRef([]);
@ -82,6 +84,11 @@ function handleClose() {
}, 200);
}
function scrollTo(index) {
const scrollTop = resultRef.value.handleScroll(index);
scrollbarRef.value.setScrollTop(scrollTop);
}
/** key up */
function handleUp() {
const { length } = resultOptions.value;
@ -91,8 +98,10 @@ function handleUp() {
);
if (index === 0) {
activePath.value = resultOptions.value[length - 1].path;
scrollTo(resultOptions.value.length - 1);
} else {
activePath.value = resultOptions.value[index - 1].path;
scrollTo(index - 1);
}
}
@ -108,6 +117,7 @@ function handleDown() {
} else {
activePath.value = resultOptions.value[index + 1].path;
}
scrollTo(index + 1);
}
/** key enter */
@ -126,41 +136,55 @@ onKeyStroke("ArrowDown", handleDown);
<template>
<el-dialog
top="5vh"
class="pure-search-dialog"
v-model="show"
:width="device === 'mobile' ? '80vw' : '50vw'"
:show-close="false"
:width="device === 'mobile' ? '80vw' : '40vw'"
:before-close="handleClose"
:style="{
borderRadius: '6px'
}"
@opened="inputRef.focus()"
@closed="inputRef.blur()"
>
<el-input
ref="inputRef"
size="large"
v-model="keyword"
clearable
placeholder="请输入关键词搜索"
placeholder="搜索菜单"
@input="handleSearch"
>
<template #prefix>
<span class="el-input__icon">
<IconifyIconOffline :icon="Search" />
</span>
<IconifyIconOffline
:icon="Search"
class="text-primary w-[24px] h-[24px]"
/>
</template>
</el-input>
<div class="search-result-container">
<el-empty v-if="resultOptions.length === 0" description="暂无搜索结果" />
<SearchResult
v-else
v-model:value="activePath"
:options="resultOptions"
@click="handleEnter"
/>
<el-scrollbar ref="scrollbarRef" max-height="calc(90vh - 140px)">
<el-empty
v-if="resultOptions.length === 0"
description="暂无搜索结果"
/>
<SearchResult
v-else
ref="resultRef"
v-model:value="activePath"
:options="resultOptions"
@click="handleEnter"
/>
</el-scrollbar>
</div>
<template #footer>
<SearchFooter />
<SearchFooter :total="resultOptions.length" />
</template>
</el-dialog>
</template>
<style lang="scss" scoped>
.search-result-container {
margin-top: 20px;
margin-top: 12px;
}
</style>

View File

@ -1,7 +1,8 @@
<script setup lang="ts">
import { computed } from "vue";
import { useResizeObserver } from "@vueuse/core";
import { useEpThemeStoreHook } from "@/store/modules/epTheme";
import { useRenderIcon } from "@/components/ReIcon/src/hooks";
import { ref, computed, getCurrentInstance, onMounted } from "vue";
import enterOutlined from "@/assets/svg/enter_outlined.svg?component";
import Bookmark2Line from "@iconify-icons/ri/bookmark-2-line";
@ -23,8 +24,11 @@ interface Emits {
(e: "enter"): void;
}
const resultRef = ref();
const innerHeight = ref();
const props = withDefaults(defineProps<Props>(), {});
const emit = defineEmits<Emits>();
const instance = getCurrentInstance()!;
const itemStyle = computed(() => {
return item => {
@ -54,22 +58,46 @@ async function handleMouse(item) {
function handleTo() {
emit("enter");
}
function resizeResult() {
// el-scrollbar max-height="calc(90vh - 140px)"
innerHeight.value = window.innerHeight - window.innerHeight / 10 - 140;
}
useResizeObserver(resultRef, () => {
resizeResult();
});
function handleScroll(index: number) {
const curInstance = instance?.proxy?.$refs[`resultItemRef${index}`];
if (!curInstance) return 0;
const curRef = curInstance[0] as ElRef;
const scrollTop = curRef.offsetTop + 128; // 128 result-item56px+56px=112pxmargin8px+8px=16px
return scrollTop > innerHeight.value ? scrollTop - innerHeight.value : 0;
}
onMounted(() => {
resizeResult();
});
defineExpose({ handleScroll });
</script>
<template>
<div class="result">
<template v-for="item in options" :key="item.path">
<div
class="result-item dark:bg-[#1d1d1d]"
:style="itemStyle(item)"
@click="handleTo"
@mouseenter="handleMouse(item)"
>
<component :is="useRenderIcon(item.meta?.icon ?? Bookmark2Line)" />
<span class="result-item-title">{{ item.meta?.title }}</span>
<enterOutlined />
</div>
</template>
<div ref="resultRef" class="result">
<div
v-for="(item, index) in options"
:key="item.path"
:ref="'resultItemRef' + index"
class="result-item dark:bg-[#1d1d1d]"
:style="itemStyle(item)"
@click="handleTo"
@mouseenter="handleMouse(item)"
>
<component :is="useRenderIcon(item.meta?.icon ?? Bookmark2Line)" />
<span class="result-item-title">{{ item.meta?.title }}</span>
<enterOutlined />
</div>
</div>
</template>

View File

@ -218,7 +218,6 @@ watch($storage, ({ layout }) => {
});
onBeforeMount(() => {
dataThemeChange();
/* 初始化项目配置 */
nextTick(() => {
settings.greyVal &&

View File

@ -19,6 +19,7 @@ const {
onPanel,
menuSelect,
username,
userAvatar,
avatarsStyle
} = useNav();
@ -66,10 +67,7 @@ watch(
<!-- 退出登录 -->
<el-dropdown trigger="click">
<span class="el-dropdown-link navbar-bg-hover">
<img
src="https://avatars.githubusercontent.com/u/44761321?v=4"
:style="avatarsStyle"
/>
<img :src="userAvatar" :style="avatarsStyle" />
<p v-if="username" class="dark:text-white">{{ username }}</p>
</span>
<template #dropdown>

View File

@ -7,7 +7,6 @@ const props = defineProps({
});
const { title } = useNav();
const topPath = getTopMenu().path;
</script>
<template>
@ -18,7 +17,7 @@ const topPath = getTopMenu().path;
key="props.collapse"
:title="title"
class="sidebar-logo-link"
:to="topPath"
:to="getTopMenu()?.path ?? '/'"
>
<img src="/logo.svg" alt="logo" />
<span class="sidebar-title">{{ title }}</span>
@ -28,7 +27,7 @@ const topPath = getTopMenu().path;
key="expand"
:title="title"
class="sidebar-logo-link"
:to="topPath"
:to="getTopMenu()?.path ?? '/'"
>
<img src="/logo.svg" alt="logo" />
<span class="sidebar-title">{{ title }}</span>

View File

@ -22,6 +22,7 @@ const {
menuSelect,
resolvePath,
username,
userAvatar,
getDivStyle,
avatarsStyle
} = useNav();
@ -97,10 +98,7 @@ watch(
<!-- 退出登录 -->
<el-dropdown trigger="click">
<span class="el-dropdown-link navbar-bg-hover select-none">
<img
src="https://avatars.githubusercontent.com/u/44761321?v=4"
:style="avatarsStyle"
/>
<img :src="userAvatar" :style="avatarsStyle" />
<p v-if="username" class="dark:text-white">{{ username }}</p>
</span>
<template #dropdown>

View File

@ -48,7 +48,7 @@ const tabDom = ref();
const containerDom = ref();
const scrollbarDom = ref();
const isShowArrow = ref(false);
const topPath = getTopMenu().path;
const topPath = getTopMenu()?.path;
const { VITE_HIDE_HOME } = import.meta.env;
const { isFullscreen, toggle } = useFullscreen();

View File

@ -2,6 +2,7 @@ import { storeToRefs } from "pinia";
import { getConfig } from "@/config";
import { emitter } from "@/utils/mitt";
import { routeMetaType } from "../types";
import userAvatar from "@/assets/user.jpg";
import { getTopMenu } from "@/router/utils";
import { useGlobal } from "@pureadmin/utils";
import { computed, CSSProperties } from "vue";
@ -70,7 +71,7 @@ export function useNav() {
}
function backTopMenu() {
router.push(getTopMenu().path);
router.push(getTopMenu()?.path);
}
function onPanel() {
@ -150,6 +151,7 @@ export function useNav() {
isCollapse,
pureApp,
username,
userAvatar,
avatarsStyle,
tooltipEffect
};

View File

@ -8,7 +8,15 @@ import { useLayout } from "./hooks/useLayout";
import { useAppStoreHook } from "@/store/modules/app";
import { useSettingStoreHook } from "@/store/modules/settings";
import { deviceDetection, useDark, useGlobal } from "@pureadmin/utils";
import { h, reactive, computed, onMounted, defineComponent } from "vue";
import { useDataThemeChange } from "@/layout/hooks/useDataThemeChange";
import {
h,
reactive,
computed,
onMounted,
onBeforeMount,
defineComponent
} from "vue";
import navbar from "./components/navbar.vue";
import tag from "./components/tag/index.vue";
@ -88,11 +96,12 @@ emitter.on("resize", ({ detail }) => {
toggle("desktop", false);
isAutoCloseSidebar = false;
}
} else if (width > 990) {
if (!set.sidebar.isClickCollapse) {
toggle("desktop", true);
isAutoCloseSidebar = true;
}
} else if (width > 990 && !set.sidebar.isClickCollapse) {
toggle("desktop", true);
isAutoCloseSidebar = true;
} else {
toggle("desktop", false);
isAutoCloseSidebar = false;
}
});
@ -102,6 +111,10 @@ onMounted(() => {
}
});
onBeforeMount(() => {
useDataThemeChange().dataThemeChange();
});
const layoutHeader = defineComponent({
render() {
return h(

View File

@ -22,7 +22,7 @@ import {
formatFlatteningRoutes
} from "./utils";
import { buildHierarchyTree } from "@/utils/tree";
import { isUrl, openLink, storageSession } from "@pureadmin/utils";
import { isUrl, openLink, storageSession, isAllEmpty } from "@pureadmin/utils";
import remainingRouter from "./modules/remaining";
@ -46,13 +46,13 @@ Object.keys(modules).forEach(key => {
/** 导出处理后的静态路由(三级及以上的路由全部拍成二级) */
export const constantRoutes: Array<RouteRecordRaw> = formatTwoStageRoutes(
formatFlatteningRoutes(buildHierarchyTree(ascending(routes)))
formatFlatteningRoutes(buildHierarchyTree(ascending(routes.flat(Infinity))))
);
/** 用于渲染菜单,保持原始层级 */
export const constantMenus: Array<RouteComponent> = ascending(routes).concat(
...remainingRouter
);
export const constantMenus: Array<RouteComponent> = ascending(
routes.flat(Infinity)
).concat(...remainingRouter);
/** 不参与菜单的路由 */
export const remainingPaths = Object.keys(remainingRouter).map(v => {
@ -86,7 +86,9 @@ export function resetRouter() {
if (name && router.hasRoute(name) && meta?.backstage) {
router.removeRoute(name);
router.options.routes = formatTwoStageRoutes(
formatFlatteningRoutes(buildHierarchyTree(ascending(routes)))
formatFlatteningRoutes(
buildHierarchyTree(ascending(routes.flat(Infinity)))
)
);
}
});
@ -154,11 +156,22 @@ router.beforeEach((to: toRouteType, _from, next) => {
getTopMenu(true);
// query、params模式路由传参数的标签页不在此处处理
if (route && route.meta?.title) {
useMultiTagsStoreHook().handleTags("push", {
path: route.path,
name: route.name,
meta: route.meta
});
if (isAllEmpty(route.parentId) && route.meta?.backstage) {
// 此处为动态顶级路由(目录)
const { path, name, meta } = route.children[0];
useMultiTagsStoreHook().handleTags("push", {
path,
name,
meta
});
} else {
const { path, name, meta } = route;
useMultiTagsStoreHook().handleTags("push", {
path,
name,
meta
});
}
}
}
router.push(to.fullPath);

View File

@ -79,6 +79,10 @@ html.dark {
&:hover {
color: rgb(255 255 255 / 85%) !important;
background-color: rgb(255 255 255 / 12%);
.pure-dialog-svg {
color: rgb(255 255 255 / 85%) !important;
}
}
}
}
@ -103,4 +107,35 @@ html.dark {
}
}
}
/* 自定义菜单搜索样式 */
.pure-search-dialog {
.el-dialog__footer {
box-shadow: 0 -1px 0 0 #555a64, 0 -3px 6px 0 rgb(69 98 155 / 12%);
}
.search-footer {
.search-footer-item {
color: rgb(235 235 235 / 60%);
.icon {
box-shadow: none;
}
}
}
}
/* ReSegmented 组件 */
.pure-segmented {
color: rgb(255 255 255 / 65%);
background-color: #000;
.pure-segmented-item-selected {
background-color: #1f1f1f;
}
.pure-segmented-item-disabled {
color: rgb(255 255 255 / 25%);
}
}
}

View File

@ -69,6 +69,19 @@
}
}
.pure-dialog {
.pure-dialog-svg {
color: var(--el-color-info);
}
.el-dialog__headerbtn {
top: 20px;
right: 14px;
width: 24px;
height: 24px;
}
}
/* 全局覆盖element-plus的el-dialog、el-drawer、el-message-box、el-notification组件右上角关闭图标的样式表现更鲜明 */
.el-dialog__headerbtn,
.el-message-box__headerbtn {
@ -94,6 +107,10 @@
color: rgb(0 0 0 / 88%) !important;
text-decoration: none;
background-color: rgb(0 0 0 / 6%);
.pure-dialog-svg {
color: rgb(0 0 0 / 88%) !important;
}
}
}
}
@ -131,3 +148,24 @@
}
}
}
/* 自定义菜单搜索样式 */
.pure-search-dialog {
.el-dialog__header {
display: none;
}
.el-dialog__body {
padding-top: 12px;
padding-bottom: 0;
}
.el-input__inner {
font-size: 1.2em;
}
.el-dialog__footer {
padding-bottom: 10px;
box-shadow: 0 -1px 0 0 #e0e3e8, 0 -3px 6px 0 rgb(69 98 155 / 12%);
}
}

View File

@ -63,7 +63,7 @@ class PureHttp {
async (config: PureHttpRequestConfig): Promise<any> => {
// 开启进度条动画
NProgress.start();
// 优先判断post/get等方法是否传入回掉,否则执行初始化设置等回掉
// 优先判断post/get等方法是否传入回调,否则执行初始化设置等回调
if (typeof config.beforeRequestCallback === "function") {
config.beforeRequestCallback(config);
return config;
@ -123,7 +123,7 @@ class PureHttp {
const $config = response.config;
// 关闭进度条动画
NProgress.done();
// 优先判断post/get等方法是否传入回掉,否则执行初始化设置等回掉
// 优先判断post/get等方法是否传入回调,否则执行初始化设置等回调
if (typeof $config.beforeResponseCallback === "function") {
$config.beforeResponseCallback(response);
return response.data;
@ -159,7 +159,7 @@ class PureHttp {
...axiosConfig
} as PureHttpRequestConfig;
// 单独处理自定义请求/响应回
// 单独处理自定义请求/响应回
return new Promise((resolve, reject) => {
PureHttp.axiosInstance
.request(config)

View File

@ -28,8 +28,7 @@
"element-plus/global",
"@pureadmin/table/volar",
"@pureadmin/descriptions/volar"
],
"typeRoots": ["./types", "./node_modules/@types/"]
]
},
"include": [
"mock/*.ts",

4
types/index.d.ts vendored
View File

@ -41,6 +41,10 @@ type DeepPartial<T> = {
[P in keyof T]?: DeepPartial<T[P]>;
};
type Without<T, U> = { [P in Exclude<keyof T, keyof U>]?: never };
type Exclusive<T, U> = (Without<T, U> & U) | (Without<U, T> & T);
type TimeoutHandle = ReturnType<typeof setTimeout>;
type IntervalHandle = ReturnType<typeof setInterval>;