(
38 | url: string,
39 | params?: T,
40 | config?: PureHttpRequestConfig
41 | ): Promise;
42 | get(
43 | url: string,
44 | params?: T,
45 | config?: PureHttpRequestConfig
46 | ): Promise;
47 | }
48 |
--------------------------------------------------------------------------------
/src/plugins/echarts/index.ts:
--------------------------------------------------------------------------------
1 | import type { App } from "vue";
2 | import * as echarts from "echarts/core";
3 | import { CanvasRenderer } from "echarts/renderers";
4 | import { PieChart, BarChart, LineChart } from "echarts/charts";
5 | import {
6 | GridComponent,
7 | TitleComponent,
8 | LegendComponent,
9 | GraphicComponent,
10 | ToolboxComponent,
11 | TooltipComponent,
12 | DataZoomComponent,
13 | VisualMapComponent
14 | } from "echarts/components";
15 |
16 | const { use } = echarts;
17 |
18 | use([
19 | PieChart,
20 | BarChart,
21 | LineChart,
22 | CanvasRenderer,
23 | GridComponent,
24 | TitleComponent,
25 | LegendComponent,
26 | GraphicComponent,
27 | ToolboxComponent,
28 | TooltipComponent,
29 | DataZoomComponent,
30 | VisualMapComponent
31 | ]);
32 |
33 | /**
34 | * @description 按需引入echarts
35 | * @see {@link https://echarts.apache.org/handbook/zh/basics/import#%E6%8C%89%E9%9C%80%E5%BC%95%E5%85%A5-echarts-%E5%9B%BE%E8%A1%A8%E5%92%8C%E7%BB%84%E4%BB%B6}
36 | * @see 温馨提示:必须将 `$echarts` 添加到全局 `globalProperties` ,为了配合 https://pure-admin-utils.netlify.app/hooks/useEcharts/useEcharts.html 使用
37 | */
38 | export function useEcharts(app: App) {
39 | app.config.globalProperties.$echarts = echarts;
40 | }
41 |
42 | export default echarts;
43 |
--------------------------------------------------------------------------------
/src/components/ReIcon/src/iconfont.ts:
--------------------------------------------------------------------------------
1 | import { h, defineComponent } from "vue";
2 |
3 | // 封装iconfont组件,默认`font-class`引用模式,支持`unicode`引用、`font-class`引用、`symbol`引用 (https://www.iconfont.cn/help/detail?spm=a313x.7781069.1998910419.20&helptype=code)
4 | export default defineComponent({
5 | name: "FontIcon",
6 | props: {
7 | icon: {
8 | type: String,
9 | default: ""
10 | }
11 | },
12 | render() {
13 | const attrs = this.$attrs;
14 | if (Object.keys(attrs).includes("uni") || attrs?.iconType === "uni") {
15 | return h(
16 | "i",
17 | {
18 | class: "iconfont",
19 | ...attrs
20 | },
21 | this.icon
22 | );
23 | } else if (
24 | Object.keys(attrs).includes("svg") ||
25 | attrs?.iconType === "svg"
26 | ) {
27 | return h(
28 | "svg",
29 | {
30 | class: "icon-svg",
31 | "aria-hidden": true
32 | },
33 | {
34 | default: () => [
35 | h("use", {
36 | "xlink:href": `#${this.icon}`
37 | })
38 | ]
39 | }
40 | );
41 | } else {
42 | return h("i", {
43 | class: `iconfont ${this.icon}`,
44 | ...attrs
45 | });
46 | }
47 | }
48 | });
49 |
--------------------------------------------------------------------------------
/src/layout/components/search/components/SearchFooter.vue:
--------------------------------------------------------------------------------
1 |
2 |
17 |
18 |
19 |
25 |
26 |
45 |
--------------------------------------------------------------------------------
/src/utils/responsive.ts:
--------------------------------------------------------------------------------
1 | // 响应式storage
2 | import { App } from "vue";
3 | import Storage from "responsive-storage";
4 | import { routerArrays } from "@/layout/types";
5 | import { responsiveStorageNameSpace } from "@/config";
6 |
7 | export const injectResponsiveStorage = (app: App, config: ServerConfigs) => {
8 | const nameSpace = responsiveStorageNameSpace();
9 | const configObj = Object.assign(
10 | {
11 | // layout模式以及主题
12 | layout: Storage.getData("layout", nameSpace) ?? {
13 | layout: config.Layout ?? "vertical",
14 | theme: config.Theme ?? "default",
15 | darkMode: config.DarkMode ?? false,
16 | sidebarStatus: config.SidebarStatus ?? true,
17 | epThemeColor: config.EpThemeColor ?? "#409EFF"
18 | },
19 | configure: Storage.getData("configure", nameSpace) ?? {
20 | grey: config.Grey ?? false,
21 | weak: config.Weak ?? false,
22 | hideTabs: config.HideTabs ?? false,
23 | showLogo: config.ShowLogo ?? true,
24 | showModel: config.ShowModel ?? "smart",
25 | multiTagsCache: config.MultiTagsCache ?? false
26 | }
27 | },
28 | config.MultiTagsCache
29 | ? {
30 | // 默认显示顶级菜单tag
31 | tags: Storage.getData("tags", nameSpace) ?? routerArrays
32 | }
33 | : {}
34 | );
35 |
36 | app.use(Storage, { nameSpace, memory: configObj });
37 | };
38 |
--------------------------------------------------------------------------------
/src/views/sys/menu/utils/hook.tsx:
--------------------------------------------------------------------------------
1 | import { listMenu, delMenu } from "@/api/system";
2 | import { ref, onMounted } from "vue";
3 | import { message } from "@/utils/message";
4 | import { requestHook } from "@/utils/request";
5 |
6 | export function useSysMenuManagement(editRef: any) {
7 | const dataList = ref([]);
8 | const loading = ref(true);
9 |
10 | async function fetchData() {
11 | loading.value = true;
12 | const { data } = await requestHook(listMenu());
13 | dataList.value = data;
14 | loading.value = false;
15 | }
16 |
17 | function handleEdit(row: any) {
18 | editRef.value.fetchData();
19 | if (row.id) {
20 | editRef.value.showEdit(row);
21 | } else {
22 | editRef.value.showEdit();
23 | }
24 | }
25 |
26 | function handleEditChild(row: any) {
27 | if (row.id) {
28 | editRef.value.fetchData();
29 | editRef.value.showEditWithParent(row.id);
30 | }
31 | }
32 |
33 | async function handleDelete(row: any) {
34 | const { code } = await requestHook(delMenu({ id: row.id }));
35 | if (code === 0) {
36 | message("删除成功", { type: "success" });
37 | fetchData();
38 | }
39 | }
40 |
41 | onMounted(() => {
42 | fetchData();
43 | });
44 |
45 | return {
46 | dataList,
47 | loading,
48 | handleEdit,
49 | handleEditChild,
50 | handleDelete,
51 | fetchData
52 | };
53 | }
54 |
--------------------------------------------------------------------------------
/src/views/sys/user/utils/hook.tsx:
--------------------------------------------------------------------------------
1 | import { delUser, listUser } from "@/api/system";
2 | import { message } from "@/utils/message";
3 | import { requestHook } from "@/utils/request";
4 | import { onMounted, ref } from "vue";
5 |
6 | export const useSysUserManagement = (props: any) => {
7 | const dataList = ref([]);
8 | const dataTotal = ref(0);
9 | const loading = ref(true);
10 |
11 | const handleEdit = row => {
12 | if (row.id) {
13 | props.editRef.value.showEdit(row);
14 | } else {
15 | props.editRef.value.showEdit();
16 | }
17 | };
18 |
19 | const handleAssign = row => {
20 | if (row.id) {
21 | props.assignRef.value.showAssign(row);
22 | }
23 | };
24 |
25 | async function handleDelete(row: any) {
26 | const { code } = await requestHook(delUser({ id: row.id }));
27 | if (code === 0) {
28 | message("删除成功", { type: "success" });
29 | fetchData();
30 | }
31 | }
32 |
33 | const fetchData = async () => {
34 | loading.value = true;
35 | const { data } = await requestHook(listUser(props.queryForm));
36 | dataList.value = data?.list;
37 | dataTotal.value = data?.total;
38 | loading.value = false;
39 | };
40 |
41 | onMounted(() => {
42 | fetchData();
43 | });
44 |
45 | return {
46 | dataList,
47 | dataTotal,
48 | loading,
49 | handleAssign,
50 | handleEdit,
51 | handleDelete,
52 | fetchData
53 | };
54 | };
55 |
--------------------------------------------------------------------------------
/src/style/login.css:
--------------------------------------------------------------------------------
1 | .wave {
2 | position: fixed;
3 | height: 100%;
4 | left: 0;
5 | bottom: 0;
6 | z-index: -1;
7 | }
8 |
9 | .login-container {
10 | width: 100vw;
11 | height: 100vh;
12 | display: grid;
13 | grid-template-columns: repeat(2, 1fr);
14 | grid-gap: 18rem;
15 | padding: 0 2rem;
16 | }
17 |
18 | .img {
19 | display: flex;
20 | justify-content: flex-end;
21 | align-items: center;
22 | }
23 |
24 | .img img {
25 | width: 500px;
26 | }
27 |
28 | .login-box {
29 | display: flex;
30 | align-items: center;
31 | text-align: center;
32 | }
33 |
34 | .login-form {
35 | width: 360px;
36 | }
37 |
38 | .avatar {
39 | width: 350px;
40 | height: 80px;
41 | }
42 |
43 | .login-form h2 {
44 | text-transform: uppercase;
45 | margin: 15px 0;
46 | color: #999;
47 | font: bold 200% Consolas, Monaco, monospace;
48 | }
49 |
50 | @media screen and (max-width: 1180px) {
51 | .login-container {
52 | grid-gap: 9rem;
53 | }
54 |
55 | .login-form {
56 | width: 290px;
57 | }
58 |
59 | .login-form h2 {
60 | font-size: 2.4rem;
61 | margin: 8px 0;
62 | }
63 |
64 | .img img {
65 | width: 360px;
66 | }
67 |
68 | .avatar {
69 | width: 280px;
70 | height: 80px;
71 | }
72 | }
73 |
74 | @media screen and (max-width: 968px) {
75 | .wave {
76 | display: none;
77 | }
78 |
79 | .img {
80 | display: none;
81 | }
82 |
83 | .login-container {
84 | grid-template-columns: 1fr;
85 | }
86 |
87 | .login-box {
88 | justify-content: center;
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/src/views/error/404.vue:
--------------------------------------------------------------------------------
1 |
11 |
12 |
13 |
14 |
15 |
16 |
31 | 404
32 |
33 |
48 | 抱歉,你访问的页面不存在
49 |
50 |
66 | 返回首页
67 |
68 |
69 |
70 |
71 |
--------------------------------------------------------------------------------
/src/views/error/500.vue:
--------------------------------------------------------------------------------
1 |
11 |
12 |
13 |
14 |
15 |
16 |
31 | 500
32 |
33 |
48 | 抱歉,服务器出错了
49 |
50 |
66 | 返回首页
67 |
68 |
69 |
70 |
71 |
--------------------------------------------------------------------------------
/src/views/error/403.vue:
--------------------------------------------------------------------------------
1 |
11 |
12 |
13 |
14 |
15 |
16 |
31 | 403
32 |
33 |
48 | 抱歉,你无权访问该页面
49 |
50 |
66 | 返回首页
67 |
68 |
69 |
70 |
71 |
--------------------------------------------------------------------------------
/src/utils/sso.ts:
--------------------------------------------------------------------------------
1 | import { removeToken, setToken, type DataInfo } from "./auth";
2 | import { subBefore, getQueryMap } from "@pureadmin/utils";
3 |
4 | /**
5 | * 简版前端单点登录,根据实际业务自行编写
6 | * 划重点:
7 | * 判断是否为单点登录,不为则直接返回不再进行任何逻辑处理,下面是单点登录后的逻辑处理
8 | * 1.清空本地旧信息;
9 | * 2.获取url中的重要参数信息,然后通过 setToken 保存在本地;
10 | * 3.删除不需要显示在 url 的参数
11 | * 4.使用 window.location.replace 跳转正确页面
12 | */
13 | (function () {
14 | // 获取 url 中的参数
15 | const params = getQueryMap(location.href) as DataInfo;
16 | const must = ["username", "roles", "accessToken"];
17 | const mustLength = must.length;
18 | if (Object.keys(params).length !== mustLength) return;
19 |
20 | // url 参数满足 must 里的全部值,才判定为单点登录,避免非单点登录时刷新页面无限循环
21 | let sso = [];
22 | let start = 0;
23 |
24 | while (start < mustLength) {
25 | if (Object.keys(params).includes(must[start]) && sso.length <= mustLength) {
26 | sso.push(must[start]);
27 | } else {
28 | sso = [];
29 | }
30 | start++;
31 | }
32 |
33 | if (sso.length === mustLength) {
34 | // 判定为单点登录
35 |
36 | // 清空本地旧信息
37 | removeToken();
38 |
39 | // 保存新信息到本地
40 | setToken(params);
41 |
42 | // 删除不需要显示在 url 的参数
43 | delete params["roles"];
44 | delete params["accessToken"];
45 |
46 | const newUrl = `${location.origin}${location.pathname}${subBefore(
47 | location.hash,
48 | "?"
49 | )}?${JSON.stringify(params)
50 | .replace(/["{}]/g, "")
51 | .replace(/:/g, "=")
52 | .replace(/,/g, "&")}`;
53 |
54 | // 替换历史记录项
55 | window.location.replace(newUrl);
56 | } else {
57 | return;
58 | }
59 | })();
60 |
--------------------------------------------------------------------------------
/src/store/modules/epTheme.ts:
--------------------------------------------------------------------------------
1 | import { store } from "@/store";
2 | import { defineStore } from "pinia";
3 | import { storageLocal } from "@pureadmin/utils";
4 | import { getConfig, responsiveStorageNameSpace } from "@/config";
5 |
6 | export const useEpThemeStore = defineStore({
7 | id: "pure-epTheme",
8 | state: () => ({
9 | epThemeColor:
10 | storageLocal().getItem(
11 | `${responsiveStorageNameSpace()}layout`
12 | )?.epThemeColor ?? getConfig().EpThemeColor,
13 | epTheme:
14 | storageLocal().getItem(
15 | `${responsiveStorageNameSpace()}layout`
16 | )?.theme ?? getConfig().Theme
17 | }),
18 | getters: {
19 | getEpThemeColor(state) {
20 | return state.epThemeColor;
21 | },
22 | /** 用于mix导航模式下hamburger-svg的fill属性 */
23 | fill(state) {
24 | if (state.epTheme === "light") {
25 | return "#409eff";
26 | } else if (state.epTheme === "yellow") {
27 | return "#d25f00";
28 | } else {
29 | return "#fff";
30 | }
31 | }
32 | },
33 | actions: {
34 | setEpThemeColor(newColor: string): void {
35 | const layout = storageLocal().getItem(
36 | `${responsiveStorageNameSpace()}layout`
37 | );
38 | this.epTheme = layout?.theme;
39 | this.epThemeColor = newColor;
40 | if (!layout) return;
41 | layout.epThemeColor = newColor;
42 | storageLocal().setItem(`${responsiveStorageNameSpace()}layout`, layout);
43 | }
44 | }
45 | });
46 |
47 | export function useEpThemeStoreHook() {
48 | return useEpThemeStore(store);
49 | }
50 |
--------------------------------------------------------------------------------
/src/layout/frameView.vue:
--------------------------------------------------------------------------------
1 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
69 |
--------------------------------------------------------------------------------
/src/config/index.ts:
--------------------------------------------------------------------------------
1 | import { App } from "vue";
2 | import axios from "axios";
3 |
4 | let config: object = {};
5 | const { VITE_PUBLIC_PATH } = import.meta.env;
6 |
7 | const setConfig = (cfg?: unknown) => {
8 | config = Object.assign(config, cfg);
9 | };
10 |
11 | const getConfig = (key?: string): ServerConfigs => {
12 | if (typeof key === "string") {
13 | const arr = key.split(".");
14 | if (arr && arr.length) {
15 | let data = config;
16 | arr.forEach(v => {
17 | if (data && typeof data[v] !== "undefined") {
18 | data = data[v];
19 | } else {
20 | data = null;
21 | }
22 | });
23 | return data;
24 | }
25 | }
26 | return config;
27 | };
28 |
29 | /** 获取项目动态全局配置 */
30 | export const getServerConfig = async (app: App): Promise => {
31 | app.config.globalProperties.$config = getConfig();
32 | return axios({
33 | method: "get",
34 | url: `${VITE_PUBLIC_PATH}serverConfig.json`
35 | })
36 | .then(({ data: config }) => {
37 | let $config = app.config.globalProperties.$config;
38 | // 自动注入项目配置
39 | if (app && $config && typeof config === "object") {
40 | $config = Object.assign($config, config);
41 | app.config.globalProperties.$config = $config;
42 | // 设置全局配置
43 | setConfig($config);
44 | }
45 | return $config;
46 | })
47 | .catch(() => {
48 | throw "请在public文件夹下添加serverConfig.json配置文件";
49 | });
50 | };
51 |
52 | /** 本地响应式存储的命名空间 */
53 | const responsiveStorageNameSpace = () => getConfig().ResponsiveStorageNameSpace;
54 |
55 | export { getConfig, setConfig, responsiveStorageNameSpace };
56 |
--------------------------------------------------------------------------------
/src/layout/hooks/useLayout.ts:
--------------------------------------------------------------------------------
1 | import { computed } from "vue";
2 | import { routerArrays } from "../types";
3 | import { useGlobal } from "@pureadmin/utils";
4 | import { useMultiTagsStore } from "@/store/modules/multiTags";
5 |
6 | export function useLayout() {
7 | const { $storage, $config } = useGlobal();
8 |
9 | const initStorage = () => {
10 | /** 路由 */
11 | if (
12 | useMultiTagsStore().multiTagsCache &&
13 | (!$storage.tags || $storage.tags.length === 0)
14 | ) {
15 | $storage.tags = routerArrays;
16 | }
17 | /** 导航 */
18 | if (!$storage.layout) {
19 | $storage.layout = {
20 | layout: $config?.Layout ?? "vertical",
21 | theme: $config?.Theme ?? "default",
22 | darkMode: $config?.DarkMode ?? false,
23 | sidebarStatus: $config?.SidebarStatus ?? true,
24 | epThemeColor: $config?.EpThemeColor ?? "#409EFF"
25 | };
26 | }
27 | /** 灰色模式、色弱模式、隐藏标签页 */
28 | if (!$storage.configure) {
29 | $storage.configure = {
30 | grey: $config?.Grey ?? false,
31 | weak: $config?.Weak ?? false,
32 | hideTabs: $config?.HideTabs ?? false,
33 | showLogo: $config?.ShowLogo ?? true,
34 | showModel: $config?.ShowModel ?? "smart",
35 | multiTagsCache: $config?.MultiTagsCache ?? false
36 | };
37 | }
38 | };
39 |
40 | /** 清空缓存后从serverConfig.json读取默认配置并赋值到storage中 */
41 | const layout = computed(() => {
42 | return $storage?.layout.layout;
43 | });
44 |
45 | const layoutTheme = computed(() => {
46 | return $storage.layout;
47 | });
48 |
49 | return {
50 | layout,
51 | layoutTheme,
52 | initStorage
53 | };
54 | }
55 |
--------------------------------------------------------------------------------
/src/views/sys/role/utils/hook.tsx:
--------------------------------------------------------------------------------
1 | import { ref, onMounted } from "vue";
2 | import { delRole, listRole } from "@/api/system";
3 | import { message } from "@/utils/message";
4 | import { requestHook } from "@/utils/request";
5 |
6 | export const useSysRoleManagement = (editRef, assignRef) => {
7 | const dataList = ref([]);
8 | const loading = ref(true);
9 |
10 | const handleEdit = row => {
11 | if (row.id) {
12 | editRef.value.showEdit(row);
13 | } else {
14 | editRef.value.showEdit();
15 | }
16 | };
17 |
18 | const handleAssign = row => {
19 | if (row.id) {
20 | assignRef.value.showAssign(row);
21 | }
22 | };
23 |
24 | async function handleDelete(row: any) {
25 | const { code } = await requestHook(delRole({ id: row.id }));
26 | if (code === 0) {
27 | message("删除成功", { type: "success" });
28 | fetchData();
29 | }
30 | }
31 |
32 | const fetchData = async () => {
33 | loading.value = true;
34 | const { data } = await requestHook(listRole());
35 | dataList.value = data;
36 | loading.value = false;
37 |
38 | // try {
39 | // const { data } = await listRole();
40 | // dataList.value = data;
41 | // } catch (e) {
42 | // if ((e as AxiosError)?.response?.status === 401) {
43 | // message(e.response.data.msg, { type: "error" });
44 | // }
45 | // dataList.value = [];
46 | // console.log(e);
47 | // } finally {
48 | // loading.value = false;
49 | // }
50 | };
51 |
52 | onMounted(() => {
53 | fetchData();
54 | });
55 |
56 | return {
57 | dataList,
58 | loading,
59 | handleEdit,
60 | handleAssign,
61 | handleDelete,
62 | fetchData
63 | };
64 | };
65 |
--------------------------------------------------------------------------------
/src/layout/components/sidebar/leftCollapse.vue:
--------------------------------------------------------------------------------
1 |
41 |
42 |
43 |
44 |
50 |
58 |
59 |
60 |
61 |
62 |
72 |
--------------------------------------------------------------------------------
/types/index.d.ts:
--------------------------------------------------------------------------------
1 | // 此文件跟同级目录的 global.d.ts 文件一样也是全局类型声明,只不过这里存放一些零散的全局类型,无需引入直接在 .vue 、.ts 、.tsx 文件使用即可获得类型提示
2 |
3 | type RefType = T | null;
4 |
5 | type EmitType = (event: string, ...args: any[]) => void;
6 |
7 | type TargetContext = "_self" | "_blank";
8 |
9 | type ComponentRef =
10 | ComponentElRef | null;
11 |
12 | type ElRef = Nullable;
13 |
14 | type ForDataType = {
15 | [P in T]?: ForDataType;
16 | };
17 |
18 | type AnyFunction = (...args: any[]) => T;
19 |
20 | type PropType = VuePropType;
21 |
22 | type Writable = {
23 | -readonly [P in keyof T]: T[P];
24 | };
25 |
26 | type Nullable = T | null;
27 |
28 | type NonNullable = T extends null | undefined ? never : T;
29 |
30 | type Recordable = Record;
31 |
32 | type ReadonlyRecordable = {
33 | readonly [key: string]: T;
34 | };
35 |
36 | type Indexable = {
37 | [key: string]: T;
38 | };
39 |
40 | type DeepPartial = {
41 | [P in keyof T]?: DeepPartial;
42 | };
43 |
44 | type TimeoutHandle = ReturnType;
45 |
46 | type IntervalHandle = ReturnType;
47 |
48 | type Effect = "light" | "dark";
49 |
50 | interface ChangeEvent extends Event {
51 | target: HTMLInputElement;
52 | }
53 |
54 | interface WheelEvent {
55 | path?: EventTarget[];
56 | }
57 |
58 | interface ImportMetaEnv extends ViteEnv {
59 | __: unknown;
60 | }
61 |
62 | interface Fn {
63 | (...arg: T[]): R;
64 | }
65 |
66 | interface PromiseFn {
67 | (...arg: T[]): Promise;
68 | }
69 |
70 | interface ComponentElRef {
71 | $el: T;
72 | }
73 |
74 | function parseInt(s: string | number, radix?: number): number;
75 |
76 | function parseFloat(string: string | number): number;
77 |
--------------------------------------------------------------------------------
/src/main.ts:
--------------------------------------------------------------------------------
1 | import App from "./App.vue";
2 | import router from "./router";
3 | import { setupStore } from "@/store";
4 | import ElementPlus from "element-plus";
5 | import { getServerConfig } from "./config";
6 | import { createApp, Directive } from "vue";
7 | import { MotionPlugin } from "@vueuse/motion";
8 | // import { useEcharts } from "@/plugins/echarts";
9 | import { injectResponsiveStorage } from "@/utils/responsive";
10 |
11 | // import Table from "@pureadmin/table";
12 | // import PureDescriptions from "@pureadmin/descriptions";
13 |
14 | // 引入重置样式
15 | import "./style/reset.scss";
16 | // 导入公共样式
17 | import "./style/index.scss";
18 | // 一定要在main.ts中导入tailwind.css,防止vite每次hmr都会请求src/style/index.scss整体css文件导致热更新慢的问题
19 | import "./style/tailwind.css";
20 | import "element-plus/dist/index.css";
21 | // 导入字体图标
22 | import "./assets/iconfont/iconfont.js";
23 | import "./assets/iconfont/iconfont.css";
24 |
25 | const app = createApp(App);
26 |
27 | // 自定义指令
28 | import * as directives from "@/directives";
29 | Object.keys(directives).forEach(key => {
30 | app.directive(key, (directives as { [key: string]: Directive })[key]);
31 | });
32 |
33 | // 全局注册`@iconify/vue`图标库
34 | import {
35 | IconifyIconOffline,
36 | IconifyIconOnline,
37 | FontIcon
38 | } from "./components/ReIcon";
39 | app.component("IconifyIconOffline", IconifyIconOffline);
40 | app.component("IconifyIconOnline", IconifyIconOnline);
41 | app.component("FontIcon", FontIcon);
42 |
43 | // 全局注册按钮级别权限组件
44 | import { Auth } from "@/components/ReAuth";
45 | app.component("Auth", Auth);
46 |
47 | getServerConfig(app).then(async config => {
48 | app.use(router);
49 | await router.isReady();
50 | injectResponsiveStorage(app, config);
51 | setupStore(app);
52 | app.use(MotionPlugin).use(ElementPlus);
53 | // .use(useEcharts);
54 | // .use(Table);
55 | // .use(PureDescriptions);
56 | app.mount("#app");
57 | });
58 |
--------------------------------------------------------------------------------
/src/layout/components/sidebar/logo.vue:
--------------------------------------------------------------------------------
1 |
12 |
13 |
14 |
38 |
39 |
40 |
73 |
--------------------------------------------------------------------------------
/src/components/ReIcon/src/hooks.ts:
--------------------------------------------------------------------------------
1 | import { iconType } from "./types";
2 | import { h, defineComponent, Component } from "vue";
3 | import { IconifyIconOnline, IconifyIconOffline, FontIcon } from "../index";
4 |
5 | /**
6 | * 支持 `iconfont`、自定义 `svg` 以及 `iconify` 中所有的图标
7 | * @see 点击查看文档图标篇 {@link https://yiming_chang.gitee.io/pure-admin-doc/pages/icon/}
8 | * @param icon 必传 图标
9 | * @param attrs 可选 iconType 属性
10 | * @returns Component
11 | */
12 | export function useRenderIcon(icon: any, attrs?: iconType): Component {
13 | // iconfont
14 | const ifReg = /^IF-/;
15 | // typeof icon === "function" 属于SVG
16 | if (ifReg.test(icon)) {
17 | // iconfont
18 | const name = icon.split(ifReg)[1];
19 | const iconName = name.slice(
20 | 0,
21 | name.indexOf(" ") == -1 ? name.length : name.indexOf(" ")
22 | );
23 | const iconType = name.slice(name.indexOf(" ") + 1, name.length);
24 | return defineComponent({
25 | name: "FontIcon",
26 | render() {
27 | return h(FontIcon, {
28 | icon: iconName,
29 | iconType,
30 | ...attrs
31 | });
32 | }
33 | });
34 | } else if (typeof icon === "function" || typeof icon?.render === "function") {
35 | // svg
36 | return icon;
37 | } else if (typeof icon === "object") {
38 | return defineComponent({
39 | name: "OfflineIcon",
40 | render() {
41 | return h(IconifyIconOffline, {
42 | icon: icon,
43 | ...attrs
44 | });
45 | }
46 | });
47 | } else {
48 | // 通过是否存在 : 符号来判断是在线还是本地图标,存在即是在线图标,反之
49 | return defineComponent({
50 | name: "Icon",
51 | render() {
52 | const IconifyIcon =
53 | icon && icon.includes(":") ? IconifyIconOnline : IconifyIconOffline;
54 | return h(IconifyIcon, {
55 | icon: icon,
56 | ...attrs
57 | });
58 | }
59 | });
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/src/views/sys/user/components/assign.vue:
--------------------------------------------------------------------------------
1 |
55 |
56 |
57 |
64 |
65 |
66 | {{ role.name }}
67 |
68 |
69 |
70 | 取 消
71 | 确 定
72 |
73 |
74 |
75 |
--------------------------------------------------------------------------------
/.drone.yml:
--------------------------------------------------------------------------------
1 | kind: pipeline
2 | name: goAdmin-前端
3 | type: docker
4 |
5 | clone:
6 | depth: 1
7 |
8 | steps:
9 | - name: build-image-and-push
10 | pull: if-not-exists
11 | image: plugins/docker
12 | settings:
13 | storage_driver: vfs
14 | tags:
15 | - latest
16 | - ${DRONE_BUILD_NUMBER}
17 | insecure: true
18 | use_cache: true
19 | registry:
20 | from_secret: harbor_address
21 | repo:
22 | from_secret: harbor_repo
23 | username:
24 | from_secret: harbor_user
25 | password:
26 | from_secret: harbor_pass
27 | context: ./
28 | dockerfile: ./Dockerfile
29 | when:
30 | status:
31 | - success
32 |
33 | - name: send telegram notification
34 | image: appleboy/drone-telegram
35 | pull: if-not-exists
36 | when:
37 | status:
38 | - success
39 | - failure
40 | settings:
41 | token:
42 | from_secret: telegram_token
43 | to:
44 | from_secret: telegram_to
45 | format: markdown
46 | message: >
47 | {{#success build.status}}
48 | ✅ Build #{{build.number}} of `{{repo.name}}` succeeded.
49 | 📝 Commit by {{commit.author}} on `{{commit.branch}}`:
50 | ```
51 | {{commit.message}}
52 | ```
53 | 🌐 {{ build.link }}
54 | {{else}}
55 | ❌ Build #{{build.number}} of `{{repo.name}}` failed.
56 | 📝 Commit by {{commit.author}} on `{{commit.branch}}`:
57 | ```
58 | {{commit.message}}
59 | ```
60 | 🌐 {{ build.link }}
61 | {{/success}}
62 |
63 | - name: deploy
64 | image: alpine
65 | pull: if-not-exists
66 | commands:
67 | - sed -i 's/dl-cdn.alpinelinux.org/mirrors.tuna.tsinghua.edu.cn/g' /etc/apk/repositories
68 | - apk update
69 | - apk --no-cache add curl
70 | # portainer hook
71 | # - curl -XPOST http://192.168.1.9:9000/api/stacks/webhooks/ba7ab0fd-c0b2-47f2-806d-3cfa416cf464
72 |
73 | trigger:
74 | branch:
75 | - main
76 | event:
77 | - push
78 |
--------------------------------------------------------------------------------
/src/layout/types.ts:
--------------------------------------------------------------------------------
1 | import type { IconifyIcon } from "@iconify/vue";
2 | const { VITE_HIDE_HOME } = import.meta.env;
3 |
4 | export const routerArrays: Array =
5 | VITE_HIDE_HOME === "false"
6 | ? [
7 | {
8 | path: "/welcome",
9 | parentPath: "/",
10 | meta: {
11 | title: "首页",
12 | icon: "homeFilled"
13 | }
14 | }
15 | ]
16 | : [];
17 |
18 | export type routeMetaType = {
19 | title?: string;
20 | icon?: string | IconifyIcon;
21 | showLink?: boolean;
22 | savedPosition?: boolean;
23 | auths?: Array;
24 | };
25 |
26 | export type RouteConfigs = {
27 | path?: string;
28 | parentPath?: string;
29 | query?: object;
30 | params?: object;
31 | meta?: routeMetaType;
32 | children?: RouteConfigs[];
33 | name?: string;
34 | };
35 |
36 | export type multiTagsType = {
37 | tags: Array;
38 | };
39 |
40 | export type tagsViewsType = {
41 | icon: string | IconifyIcon;
42 | text: string;
43 | divided: boolean;
44 | disabled: boolean;
45 | show: boolean;
46 | };
47 |
48 | export interface setType {
49 | sidebar: {
50 | opened: boolean;
51 | withoutAnimation: boolean;
52 | isClickCollapse: boolean;
53 | };
54 | device: string;
55 | fixedHeader: boolean;
56 | classes: {
57 | hideSidebar: boolean;
58 | openSidebar: boolean;
59 | withoutAnimation: boolean;
60 | mobile: boolean;
61 | };
62 | hideTabs: boolean;
63 | }
64 |
65 | export type menuType = {
66 | id?: number;
67 | path?: string;
68 | noShowingChildren?: boolean;
69 | children?: menuType[];
70 | value: unknown;
71 | meta?: {
72 | icon?: string;
73 | title?: string;
74 | rank?: number;
75 | showParent?: boolean;
76 | extraIcon?: string;
77 | };
78 | showTooltip?: boolean;
79 | parentId?: number;
80 | pathList?: number[];
81 | redirect?: string;
82 | };
83 |
84 | export type themeColorsType = {
85 | color: string;
86 | themeColor: string;
87 | };
88 |
89 | export interface scrollbarDomType extends HTMLElement {
90 | wrap?: {
91 | offsetWidth: number;
92 | };
93 | }
94 |
--------------------------------------------------------------------------------
/src/layout/components/notice/index.vue:
--------------------------------------------------------------------------------
1 |
13 |
14 |
15 |
16 |
17 |
18 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
78 |
--------------------------------------------------------------------------------
/README.en-US.md:
--------------------------------------------------------------------------------
1 | vue-pure-admin Lite Edition(no i18n version)
2 |
3 | [](LICENSE)
4 |
5 | **English** | [中文](./README.md)
6 |
7 | ## Introduce
8 |
9 | The simplified version is based on the shelf extracted from [vue-pure-admin](https://github.com/pure-admin/vue-pure-admin), which contains main functions and is more suitable for actual project development. The packaged size is introduced globally [element-plus](https://element-plus.org) is still below `2.3MB`, and the full version of the code will be permanently synchronized. After enabling `brotli` compression and `cdn` to replace the local library mode, the package size is less than `350kb`
10 |
11 | ## Supporting Video
12 |
13 | - [Click Watch Tutorial](https://www.bilibili.com/video/BV1kg411v7QT)
14 | - [Click Watch UI Design](https://www.bilibili.com/video/BV17g411T7rq)
15 |
16 | ## Docs
17 |
18 | - [Click me to view the domestic documentation site](https://yiming_chang.gitee.io/pure-admin-doc)
19 | - [Click me to view foreign document site](https://pure-admin.github.io/pure-admin-doc)
20 |
21 | ## Preview
22 |
23 | - [Click me to view the preview station](https://pure-admin-thin.netlify.app/#/login)
24 |
25 | ## Usage
26 |
27 | ### Installation dependencies
28 |
29 | pnpm install
30 |
31 | ### Install a package
32 |
33 | pnpm add packageName
34 |
35 | ### Uninstall a package
36 |
37 | pnpm remove packageName
38 |
39 | I think you should fork the project first to develop, so that you can pull the update synchronously when I update! ! !
40 |
41 | ## Supporting video tutorial
42 |
43 | bilibili: https://www.bilibili.com/video/BV1534y1S7HV/
44 |
45 | ## ⚠️ Attention
46 |
47 | - 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! ! !
48 |
49 | ## License
50 |
51 | 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!
52 |
53 | [MIT © 2020-present, pure-admin](./LICENSE)
54 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # GO-ADMIN-FRONT
2 |
3 | [](LICENSE)
4 |
5 | ## 简介
6 |
7 | 本项目是基于 [pure-admin-thin](https://github.com/pure-admin/pure-admin-thin) 非国际化精简版开发的通用后台基础代码
8 |
9 | 配套的后端代码为 [go-admin-server](https://github.com/anerg2046/go-admin-server)
10 |
11 | ---
12 |
13 | vue-pure-admin精简版(非国际化版本)
14 |
15 | [](LICENSE)
16 |
17 | **中文** | [English](./README.en-US.md)
18 |
19 | ## 介绍
20 |
21 | 精简版是基于 [vue-pure-admin](https://github.com/pure-admin/vue-pure-admin) 提炼出的架子,包含主体功能,更适合实际项目开发,打包后的大小在全局引入 [element-plus](https://element-plus.org) 的情况下仍然低于 `2.3MB`,并且会永久同步完整版的代码。开启 `brotli` 压缩和 `cdn` 替换本地库模式后,打包大小低于 `350kb`
22 |
23 | ## 版本选择
24 |
25 | 当前是非国际化版本哦,如果您需要国际化版本 [请点击](https://github.com/pure-admin/pure-admin-thin/tree/i18n)
26 |
27 | ## 配套视频
28 |
29 | - [点我查看教程](https://www.bilibili.com/video/BV1kg411v7QT)
30 | - [点我查看 UI 设计](https://www.bilibili.com/video/BV17g411T7rq)
31 |
32 | ## 配套文档
33 |
34 | - [点我查看国内文档站](https://yiming_chang.gitee.io/pure-admin-doc)
35 | - [点我查看国外文档站](https://pure-admin.github.io/pure-admin-doc)
36 |
37 | ## 预览
38 |
39 | - [点我查看预览站](https://pure-admin-thin.netlify.app/#/login)
40 |
41 | ## 维护者
42 |
43 | [xiaoxian521](https://github.com/xiaoxian521)
44 |
45 | ## 支持
46 |
47 | 如果你觉得这个项目对您有帮助,可以帮作者买一杯果汁 🍹 表示支持
48 |
49 |
50 |
51 | ## `QQ` 交流群
52 |
53 | [点击去加入](https://yiming_chang.gitee.io/pure-admin-doc/pages/support/#qq-%E4%BA%A4%E6%B5%81%E7%BE%A4)
54 |
55 | ## 用法
56 |
57 | ### 安装依赖
58 |
59 | pnpm install
60 |
61 | ### 安装一个包
62 |
63 | pnpm add 包名
64 |
65 | ### 卸载一个包
66 |
67 | pnpm remove 包名
68 |
69 | 我认为你应该先 `fork` 项目去开发,以便我更新时您可以同步拉取更新!!!
70 |
71 | ## ⚠️ 注意
72 |
73 | - 精简版不接受任何 `issues` 和 `pr`,如果有问题请到完整版 [issues](https://github.com/pure-admin/vue-pure-admin/issues/new/choose) 去提,谢谢!!!
74 |
75 | ## 许可证
76 |
77 | 原则上不收取任何费用及版权,可以放心使用,不过如需二次开源(比如用此平台二次开发并开源)请联系作者获取许可!
78 |
79 | [MIT © 2020-present, pure-admin](./LICENSE)
80 |
--------------------------------------------------------------------------------
/src/store/modules/permission.ts:
--------------------------------------------------------------------------------
1 | import { defineStore } from "pinia";
2 | import { store } from "@/store";
3 | import { cacheType } from "./types";
4 | import { constantMenus } from "@/router";
5 | import { getKeyList } from "@pureadmin/utils";
6 | import { useMultiTagsStoreHook } from "./multiTags";
7 | import { ascending, filterTree, filterNoPermissionTree } from "@/router/utils";
8 |
9 | export const usePermissionStore = defineStore({
10 | id: "pure-permission",
11 | state: () => ({
12 | // 静态路由生成的菜单
13 | constantMenus,
14 | // 整体路由生成的菜单(静态、动态)
15 | wholeMenus: [],
16 | // 缓存页面keepAlive
17 | cachePageList: []
18 | }),
19 | actions: {
20 | /** 组装整体路由生成的菜单 */
21 | handleWholeMenus(routes: any[]) {
22 | this.wholeMenus = filterNoPermissionTree(
23 | filterTree(ascending(this.constantMenus.concat(routes)))
24 | );
25 | },
26 | cacheOperate({ mode, name }: cacheType) {
27 | const delIndex = this.cachePageList.findIndex(v => v === name);
28 | switch (mode) {
29 | case "refresh":
30 | this.cachePageList = this.cachePageList.filter(v => v !== name);
31 | break;
32 | case "add":
33 | this.cachePageList.push(name);
34 | break;
35 | case "delete":
36 | delIndex !== -1 && this.cachePageList.splice(delIndex, 1);
37 | break;
38 | }
39 | /** 监听缓存页面是否存在于标签页,不存在则删除 */
40 | (() => {
41 | let cacheLength = this.cachePageList.length;
42 | const nameList = getKeyList(useMultiTagsStoreHook().multiTags, "name");
43 | while (cacheLength > 0) {
44 | nameList.findIndex(v => v === this.cachePageList[cacheLength - 1]) ===
45 | -1 &&
46 | this.cachePageList.splice(
47 | this.cachePageList.indexOf(this.cachePageList[cacheLength - 1]),
48 | 1
49 | );
50 | cacheLength--;
51 | }
52 | })();
53 | },
54 | /** 清空缓存页面 */
55 | clearAllCachePage() {
56 | this.wholeMenus = [];
57 | this.cachePageList = [];
58 | }
59 | }
60 | });
61 |
62 | export function usePermissionStoreHook() {
63 | return usePermissionStore(store);
64 | }
65 |
--------------------------------------------------------------------------------
/stylelint.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | extends: [
4 | "stylelint-config-standard",
5 | "stylelint-config-html/vue",
6 | "stylelint-config-recess-order"
7 | ],
8 | plugins: ["stylelint-order", "stylelint-prettier", "stylelint-scss"],
9 | overrides: [
10 | {
11 | files: ["**/*.(css|html|vue)"],
12 | customSyntax: "postcss-html"
13 | },
14 | {
15 | files: ["*.scss", "**/*.scss"],
16 | customSyntax: "postcss-scss",
17 | extends: [
18 | "stylelint-config-standard-scss",
19 | "stylelint-config-recommended-vue/scss"
20 | ]
21 | }
22 | ],
23 | rules: {
24 | "selector-class-pattern": null,
25 | "no-descending-specificity": null,
26 | "scss/dollar-variable-pattern": null,
27 | "selector-pseudo-class-no-unknown": [
28 | true,
29 | {
30 | ignorePseudoClasses: ["deep", "global"]
31 | }
32 | ],
33 | "selector-pseudo-element-no-unknown": [
34 | true,
35 | {
36 | ignorePseudoElements: ["v-deep", "v-global", "v-slotted"]
37 | }
38 | ],
39 | "at-rule-no-unknown": [
40 | true,
41 | {
42 | ignoreAtRules: [
43 | "tailwind",
44 | "apply",
45 | "variants",
46 | "responsive",
47 | "screen",
48 | "function",
49 | "if",
50 | "each",
51 | "include",
52 | "mixin",
53 | "use"
54 | ]
55 | }
56 | ],
57 | "rule-empty-line-before": [
58 | "always",
59 | {
60 | ignore: ["after-comment", "first-nested"]
61 | }
62 | ],
63 | "unit-no-unknown": [true, { ignoreUnits: ["rpx"] }],
64 | "order/order": [
65 | [
66 | "dollar-variables",
67 | "custom-properties",
68 | "at-rules",
69 | "declarations",
70 | {
71 | type: "at-rule",
72 | name: "supports"
73 | },
74 | {
75 | type: "at-rule",
76 | name: "media"
77 | },
78 | "rules"
79 | ],
80 | { severity: "warning" }
81 | ]
82 | },
83 | ignoreFiles: ["**/*.js", "**/*.ts", "**/*.jsx", "**/*.tsx"]
84 | };
85 |
--------------------------------------------------------------------------------
/src/api/system.ts:
--------------------------------------------------------------------------------
1 | import { http } from "@/utils/http";
2 | import { baseUrlApi, ApiResult } from "./utils";
3 |
4 | /** 所有菜单列表 */
5 | export const listMenu = () => {
6 | return http.request("get", baseUrlApi("/sys/menus/list"));
7 | };
8 |
9 | /** 添加/修改菜单 */
10 | export const editMenu = (data?: object) => {
11 | return http.request("post", baseUrlApi("/sys/menus/edit"), {
12 | data
13 | });
14 | };
15 |
16 | /** 删除菜单 */
17 | export const delMenu = (data?: object) => {
18 | return http.request("post", baseUrlApi("/sys/menus/del"), {
19 | data
20 | });
21 | };
22 |
23 | /** 所有角色列表 */
24 | export const listRole = () => {
25 | return http.request("get", baseUrlApi("/sys/roles/list"));
26 | };
27 |
28 | /** 添加/修改角色 */
29 | export const editRole = (data?: object) => {
30 | return http.request("post", baseUrlApi("/sys/roles/edit"), {
31 | data
32 | });
33 | };
34 |
35 | /** 删除角色 */
36 | export const delRole = (data?: object) => {
37 | return http.request("post", baseUrlApi("/sys/roles/del"), {
38 | data
39 | });
40 | };
41 |
42 | /** 角色权限 */
43 | export const permissionRole = (params?: object) => {
44 | return http.request("get", baseUrlApi("/sys/roles/permission"), {
45 | params
46 | });
47 | };
48 |
49 | /** 角色指派权限 */
50 | export const assignRole = (data?: object) => {
51 | return http.request("post", baseUrlApi("/sys/roles/assign"), {
52 | data
53 | });
54 | };
55 |
56 | /** 所有用户列表 */
57 | export const listUser = (params?: object) => {
58 | return http.request("get", baseUrlApi("/sys/users/list"), {
59 | params
60 | });
61 | };
62 |
63 | /** 添加/修改用户 */
64 | export const editUser = (data?: object) => {
65 | return http.request("post", baseUrlApi("/sys/users/edit"), {
66 | data
67 | });
68 | };
69 |
70 | /** 删除用户 */
71 | export const delUser = (data?: object) => {
72 | return http.request("post", baseUrlApi("/sys/users/del"), {
73 | data
74 | });
75 | };
76 |
77 | /** 用户分配角色 */
78 | export const assignUser = (data?: object) => {
79 | return http.request("post", baseUrlApi("/sys/users/assign"), {
80 | data
81 | });
82 | };
83 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
11 | pure-admin-thin
12 |
13 |
16 |
17 |
18 |
19 |
85 |
86 |
87 |
88 |
--------------------------------------------------------------------------------
/vite.config.ts:
--------------------------------------------------------------------------------
1 | import dayjs from "dayjs";
2 | import { resolve } from "path";
3 | import pkg from "./package.json";
4 | import { warpperEnv } from "./build";
5 | import { getPluginsList } from "./build/plugins";
6 | import { include, exclude } from "./build/optimize";
7 | import { UserConfigExport, ConfigEnv, loadEnv } from "vite";
8 |
9 | /** 当前执行node命令时文件夹的地址(工作目录) */
10 | const root: string = process.cwd();
11 |
12 | /** 路径查找 */
13 | const pathResolve = (dir: string): string => {
14 | return resolve(__dirname, ".", dir);
15 | };
16 |
17 | /** 设置别名 */
18 | const alias: Record = {
19 | "@": pathResolve("src"),
20 | "@build": pathResolve("build")
21 | };
22 |
23 | const { dependencies, devDependencies, name, version } = pkg;
24 | const __APP_INFO__ = {
25 | pkg: { dependencies, devDependencies, name, version },
26 | lastBuildTime: dayjs(new Date()).format("YYYY-MM-DD HH:mm:ss")
27 | };
28 |
29 | export default ({ command, mode }: ConfigEnv): UserConfigExport => {
30 | const { VITE_CDN, VITE_PORT, VITE_COMPRESSION, VITE_PUBLIC_PATH } =
31 | warpperEnv(loadEnv(mode, root));
32 | return {
33 | base: VITE_PUBLIC_PATH,
34 | root,
35 | resolve: {
36 | alias
37 | },
38 | // 服务端渲染
39 | server: {
40 | // 是否开启 https
41 | https: false,
42 | // 端口号
43 | port: VITE_PORT,
44 | host: "0.0.0.0",
45 | // 本地跨域代理 https://cn.vitejs.dev/config/server-options.html#server-proxy
46 | proxy: {}
47 | },
48 | plugins: getPluginsList(command, VITE_CDN, VITE_COMPRESSION),
49 | // https://cn.vitejs.dev/config/dep-optimization-options.html#dep-optimization-options
50 | optimizeDeps: {
51 | include,
52 | exclude
53 | },
54 | build: {
55 | sourcemap: false,
56 | // 消除打包大小超过500kb警告
57 | chunkSizeWarningLimit: 4000,
58 | rollupOptions: {
59 | input: {
60 | index: pathResolve("index.html")
61 | },
62 | // 静态资源分类打包
63 | output: {
64 | chunkFileNames: "static/js/[name]-[hash].js",
65 | entryFileNames: "static/js/[name]-[hash].js",
66 | assetFileNames: "static/[ext]/[name]-[hash].[ext]"
67 | }
68 | }
69 | },
70 | define: {
71 | __INTLIFY_PROD_DEVTOOLS__: false,
72 | __APP_INFO__: JSON.stringify(__APP_INFO__)
73 | }
74 | };
75 | };
76 |
--------------------------------------------------------------------------------
/src/store/modules/app.ts:
--------------------------------------------------------------------------------
1 | import { store } from "@/store";
2 | import { appType } from "./types";
3 | import { defineStore } from "pinia";
4 | import { getConfig, responsiveStorageNameSpace } from "@/config";
5 | import { deviceDetection, storageLocal } from "@pureadmin/utils";
6 |
7 | export const useAppStore = defineStore({
8 | id: "pure-app",
9 | state: (): appType => ({
10 | sidebar: {
11 | opened:
12 | storageLocal().getItem(
13 | `${responsiveStorageNameSpace()}layout`
14 | )?.sidebarStatus ?? getConfig().SidebarStatus,
15 | withoutAnimation: false,
16 | isClickCollapse: false
17 | },
18 | // 这里的layout用于监听容器拖拉后恢复对应的导航模式
19 | layout:
20 | storageLocal().getItem(
21 | `${responsiveStorageNameSpace()}layout`
22 | )?.layout ?? getConfig().Layout,
23 | device: deviceDetection() ? "mobile" : "desktop"
24 | }),
25 | getters: {
26 | getSidebarStatus(state) {
27 | return state.sidebar.opened;
28 | },
29 | getDevice(state) {
30 | return state.device;
31 | }
32 | },
33 | actions: {
34 | TOGGLE_SIDEBAR(opened?: boolean, resize?: string) {
35 | const layout = storageLocal().getItem(
36 | `${responsiveStorageNameSpace()}layout`
37 | );
38 | if (opened && resize) {
39 | this.sidebar.withoutAnimation = true;
40 | this.sidebar.opened = true;
41 | layout.sidebarStatus = true;
42 | } else if (!opened && resize) {
43 | this.sidebar.withoutAnimation = true;
44 | this.sidebar.opened = false;
45 | layout.sidebarStatus = false;
46 | } else if (!opened && !resize) {
47 | this.sidebar.withoutAnimation = false;
48 | this.sidebar.opened = !this.sidebar.opened;
49 | this.sidebar.isClickCollapse = !this.sidebar.opened;
50 | layout.sidebarStatus = this.sidebar.opened;
51 | }
52 | storageLocal().setItem(`${responsiveStorageNameSpace()}layout`, layout);
53 | },
54 | async toggleSideBar(opened?: boolean, resize?: string) {
55 | await this.TOGGLE_SIDEBAR(opened, resize);
56 | },
57 | toggleDevice(device: string) {
58 | this.device = device;
59 | },
60 | setLayout(layout) {
61 | this.layout = layout;
62 | }
63 | }
64 | });
65 |
66 | export function useAppStoreHook() {
67 | return useAppStore(store);
68 | }
69 |
--------------------------------------------------------------------------------
/src/utils/message.ts:
--------------------------------------------------------------------------------
1 | import { type VNode } from "vue";
2 | import { isFunction } from "@pureadmin/utils";
3 | import { type MessageHandler, ElMessage } from "element-plus";
4 |
5 | type messageStyle = "el" | "antd";
6 | type messageTypes = "info" | "success" | "warning" | "error";
7 |
8 | interface MessageParams {
9 | /** 消息类型,可选 `info` 、`success` 、`warning` 、`error` ,默认 `info` */
10 | type?: messageTypes;
11 | /** 自定义图标,该属性会覆盖 `type` 的图标 */
12 | icon?: any;
13 | /** 是否将 `message` 属性作为 `HTML` 片段处理,默认 `false` */
14 | dangerouslyUseHTMLString?: boolean;
15 | /** 消息风格,可选 `el` 、`antd` ,默认 `antd` */
16 | customClass?: messageStyle;
17 | /** 显示时间,单位为毫秒。设为 `0` 则不会自动关闭,`element-plus` 默认是 `3000` ,平台改成默认 `2000` */
18 | duration?: number;
19 | /** 是否显示关闭按钮,默认值 `false` */
20 | showClose?: boolean;
21 | /** 文字是否居中,默认值 `false` */
22 | center?: boolean;
23 | /** `Message` 距离窗口顶部的偏移量,默认 `20` */
24 | offset?: number;
25 | /** 设置组件的根元素,默认 `document.body` */
26 | appendTo?: string | HTMLElement;
27 | /** 合并内容相同的消息,不支持 `VNode` 类型的消息,默认值 `false` */
28 | grouping?: boolean;
29 | /** 关闭时的回调函数, 参数为被关闭的 `message` 实例 */
30 | onClose?: Function | null;
31 | }
32 |
33 | /** 用法非常简单,参考 src/views/components/message/index.vue 文件 */
34 |
35 | /**
36 | * `Message` 消息提示函数
37 | */
38 | const message = (
39 | message: string | VNode | (() => VNode),
40 | params?: MessageParams
41 | ): MessageHandler => {
42 | if (!params) {
43 | return ElMessage({
44 | message,
45 | customClass: "pure-message"
46 | });
47 | } else {
48 | const {
49 | icon,
50 | type = "info",
51 | dangerouslyUseHTMLString = false,
52 | customClass = "antd",
53 | duration = 2000,
54 | showClose = false,
55 | center = false,
56 | offset = 20,
57 | appendTo = document.body,
58 | grouping = false,
59 | onClose
60 | } = params;
61 |
62 | return ElMessage({
63 | message,
64 | type,
65 | icon,
66 | dangerouslyUseHTMLString,
67 | duration,
68 | showClose,
69 | center,
70 | offset,
71 | appendTo,
72 | grouping,
73 | // 全局搜 pure-message 即可知道该类的样式位置
74 | customClass: customClass === "antd" ? "pure-message" : "",
75 | onClose: () => (isFunction(onClose) ? onClose() : null)
76 | });
77 | }
78 | };
79 |
80 | /**
81 | * 关闭所有 `Message` 消息提示函数
82 | */
83 | const closeAllMessage = (): void => ElMessage.closeAll();
84 |
85 | export { message, closeAllMessage };
86 |
--------------------------------------------------------------------------------
/src/layout/components/search/components/SearchResult.vue:
--------------------------------------------------------------------------------
1 |
58 |
59 |
60 |
61 |
62 |
68 |
69 | {{ item.meta?.title }}
70 |
71 |
72 |
73 |
74 |
75 |
76 |
99 |
--------------------------------------------------------------------------------
/src/store/modules/user.ts:
--------------------------------------------------------------------------------
1 | import { defineStore } from "pinia";
2 | import { store } from "@/store";
3 | import { userType } from "./types";
4 | import { routerArrays } from "@/layout/types";
5 | import { router, resetRouter } from "@/router";
6 | import { storageSession } from "@pureadmin/utils";
7 | import { getLogin, refreshTokenApi } from "@/api/user";
8 | import { LoginResult, RefreshTokenResult } from "@/api/user";
9 | import { useMultiTagsStoreHook } from "@/store/modules/multiTags";
10 | import { type DataInfo, setToken, removeToken, sessionKey } from "@/utils/auth";
11 | import { message } from "@/utils/message";
12 |
13 | export const useUserStore = defineStore({
14 | id: "pure-user",
15 | state: (): userType => ({
16 | // 用户名
17 | username:
18 | storageSession().getItem>(sessionKey)?.username ?? "",
19 | // 页面级别权限
20 | roles: storageSession().getItem>(sessionKey)?.roles ?? []
21 | }),
22 | actions: {
23 | /** 存储用户名 */
24 | SET_USERNAME(username: string) {
25 | this.username = username;
26 | },
27 | /** 存储角色 */
28 | SET_ROLES(roles: Array) {
29 | this.roles = roles;
30 | },
31 | /** 登入 */
32 | async loginByUsername(data) {
33 | return new Promise((resolve, reject) => {
34 | getLogin(data)
35 | .then(data => {
36 | if (data?.code === 0) {
37 | setToken(data.data);
38 | resolve(data);
39 | } else {
40 | throw Error(data.msg);
41 | }
42 | })
43 | .catch(error => {
44 | reject(error);
45 | });
46 | });
47 | },
48 | /** 前端登出(不调用接口) */
49 | logOut() {
50 | this.username = "";
51 | this.roles = [];
52 | removeToken();
53 | useMultiTagsStoreHook().handleTags("equal", [...routerArrays]);
54 | resetRouter();
55 | router.push("/login");
56 | },
57 | /** 刷新`token` */
58 | async handRefreshToken(data) {
59 | return new Promise((resolve, reject) => {
60 | refreshTokenApi(data)
61 | .then(data => {
62 | if (data?.code === 0) {
63 | setToken(data.data);
64 | resolve(data);
65 | } else {
66 | message(data.msg, { type: "error" });
67 | removeToken();
68 | router.push("/login");
69 | }
70 | })
71 | .catch(error => {
72 | reject(error);
73 | });
74 | });
75 | }
76 | }
77 | });
78 |
79 | export function useUserStoreHook() {
80 | return useUserStore(store);
81 | }
82 |
--------------------------------------------------------------------------------
/src/views/sys/role/index.vue:
--------------------------------------------------------------------------------
1 |
15 |
16 |
17 |
18 |
19 |
20 |
21 | 添加
22 |
23 |
24 |
25 |
26 |
27 |
28 |
33 |
34 |
35 |
43 |
51 |
52 |
53 |
54 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
--------------------------------------------------------------------------------
/src/views/sys/user/components/edit.vue:
--------------------------------------------------------------------------------
1 |
56 |
57 |
58 |
65 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
83 |
84 |
85 |
86 | 取 消
87 |
88 | 确定
89 |
90 |
91 |
92 |
93 |
--------------------------------------------------------------------------------
/src/plugins/element-plus/index.ts:
--------------------------------------------------------------------------------
1 | import { App, Component } from "vue";
2 | import {
3 | ElTag,
4 | ElAffix,
5 | ElSkeleton,
6 | ElBreadcrumb,
7 | ElBreadcrumbItem,
8 | ElScrollbar,
9 | ElSubMenu,
10 | ElButton,
11 | ElCol,
12 | ElRow,
13 | ElSpace,
14 | ElDivider,
15 | ElCard,
16 | ElDropdown,
17 | ElDialog,
18 | ElMenu,
19 | ElMenuItem,
20 | ElDropdownItem,
21 | ElDropdownMenu,
22 | ElIcon,
23 | ElInput,
24 | ElForm,
25 | ElFormItem,
26 | ElPopover,
27 | ElPopper,
28 | ElTooltip,
29 | ElDrawer,
30 | ElPagination,
31 | ElAlert,
32 | ElRadio,
33 | ElRadioButton,
34 | ElRadioGroup,
35 | ElDescriptions,
36 | ElDescriptionsItem,
37 | ElBacktop,
38 | ElSwitch,
39 | ElBadge,
40 | ElTabs,
41 | ElTabPane,
42 | ElAvatar,
43 | ElEmpty,
44 | ElCollapse,
45 | ElCollapseItem,
46 | ElTable,
47 | ElTableColumn,
48 | ElLink,
49 | ElColorPicker,
50 | ElSelect,
51 | ElOption,
52 | ElTimeline,
53 | ElTimelineItem,
54 | ElResult,
55 | ElSteps,
56 | ElStep,
57 | ElTree,
58 | ElTreeV2,
59 | ElPopconfirm,
60 | ElCheckbox,
61 | ElCheckboxGroup,
62 | // 指令
63 | ElLoading,
64 | ElInfiniteScroll
65 | } from "element-plus";
66 |
67 | // Directives
68 | const plugins = [ElLoading, ElInfiniteScroll];
69 |
70 | const components = [
71 | ElTag,
72 | ElAffix,
73 | ElSkeleton,
74 | ElBreadcrumb,
75 | ElBreadcrumbItem,
76 | ElScrollbar,
77 | ElSubMenu,
78 | ElButton,
79 | ElCol,
80 | ElRow,
81 | ElSpace,
82 | ElDivider,
83 | ElCard,
84 | ElDropdown,
85 | ElDialog,
86 | ElMenu,
87 | ElMenuItem,
88 | ElDropdownItem,
89 | ElDropdownMenu,
90 | ElIcon,
91 | ElInput,
92 | ElForm,
93 | ElFormItem,
94 | ElPopover,
95 | ElPopper,
96 | ElTooltip,
97 | ElDrawer,
98 | ElPagination,
99 | ElAlert,
100 | ElRadio,
101 | ElRadioButton,
102 | ElRadioGroup,
103 | ElDescriptions,
104 | ElDescriptionsItem,
105 | ElBacktop,
106 | ElSwitch,
107 | ElBadge,
108 | ElTabs,
109 | ElTabPane,
110 | ElAvatar,
111 | ElEmpty,
112 | ElCollapse,
113 | ElCollapseItem,
114 | ElTree,
115 | ElTreeV2,
116 | ElPopconfirm,
117 | ElCheckbox,
118 | ElCheckboxGroup,
119 | ElTable,
120 | ElTableColumn,
121 | ElLink,
122 | ElColorPicker,
123 | ElSelect,
124 | ElOption,
125 | ElTimeline,
126 | ElTimelineItem,
127 | ElResult,
128 | ElSteps,
129 | ElStep
130 | ];
131 |
132 | export function useElementPlus(app: App) {
133 | // 注册组件
134 | components.forEach((component: Component) => {
135 | app.component(component.name, component);
136 | });
137 | // 注册指令
138 | plugins.forEach(plugin => {
139 | app.use(plugin);
140 | });
141 | }
142 |
--------------------------------------------------------------------------------
/src/utils/auth.ts:
--------------------------------------------------------------------------------
1 | import Cookies from "js-cookie";
2 | import { storageSession } from "@pureadmin/utils";
3 | import { useUserStoreHook } from "@/store/modules/user";
4 |
5 | export interface DataInfo {
6 | /** token */
7 | accessToken: string;
8 | /** `accessToken`的过期时间(时间戳) */
9 | expires: T;
10 | /** 用于调用刷新accessToken的接口时所需的token */
11 | refreshToken: string;
12 | /** 用户名 */
13 | username?: string;
14 | /** 当前登陆用户的角色 */
15 | roles?: Array;
16 | }
17 |
18 | export const sessionKey = "user-info";
19 | export const TokenKey = "authorized-token";
20 |
21 | /** 获取`token` */
22 | export function getToken(): DataInfo {
23 | // 此处与`TokenKey`相同,此写法解决初始化时`Cookies`中不存在`TokenKey`报错
24 | return Cookies.get(TokenKey)
25 | ? JSON.parse(Cookies.get(TokenKey))
26 | : storageSession().getItem(sessionKey);
27 | }
28 |
29 | /**
30 | * @description 设置`token`以及一些必要信息并采用无感刷新`token`方案
31 | * 无感刷新:后端返回`accessToken`(访问接口使用的`token`)、`refreshToken`(用于调用刷新`accessToken`的接口时所需的`token`,`refreshToken`的过期时间(比如30天)应大于`accessToken`的过期时间(比如2小时))、`expires`(`accessToken`的过期时间)
32 | * 将`accessToken`、`expires`这两条信息放在key值为authorized-token的cookie里(过期自动销毁)
33 | * 将`username`、`roles`、`refreshToken`、`expires`这四条信息放在key值为`user-info`的sessionStorage里(浏览器关闭自动销毁)
34 | */
35 | export function setToken(data: DataInfo) {
36 | let expires = 0;
37 | const { accessToken, refreshToken } = data;
38 | expires = new Date(data.expires).getTime(); // 如果后端直接设置时间戳,将此处代码改为expires = data.expires,然后把上面的DataInfo改成DataInfo即可
39 | const cookieString = JSON.stringify({ accessToken, expires });
40 |
41 | expires > 0
42 | ? Cookies.set(TokenKey, cookieString, {
43 | expires: (expires - Date.now()) / 86400000
44 | })
45 | : Cookies.set(TokenKey, cookieString);
46 |
47 | function setSessionKey(username: string, roles: Array) {
48 | useUserStoreHook().SET_USERNAME(username);
49 | useUserStoreHook().SET_ROLES(roles);
50 | storageSession().setItem(sessionKey, {
51 | refreshToken,
52 | expires,
53 | username,
54 | roles
55 | });
56 | }
57 |
58 | if (data.username && data.roles) {
59 | const { username, roles } = data;
60 | setSessionKey(username, roles);
61 | } else {
62 | const username =
63 | storageSession().getItem>(sessionKey)?.username ?? "";
64 | const roles =
65 | storageSession().getItem>(sessionKey)?.roles ?? [];
66 | setSessionKey(username, roles);
67 | }
68 | }
69 |
70 | /** 删除`token`以及key值为`user-info`的session信息 */
71 | export function removeToken() {
72 | Cookies.remove(TokenKey);
73 | sessionStorage.clear();
74 | }
75 |
76 | /** 格式化token(jwt格式) */
77 | export const formatToken = (token: string): string => {
78 | return "Bearer " + token;
79 | };
80 |
--------------------------------------------------------------------------------
/src/style/dark.scss:
--------------------------------------------------------------------------------
1 | @use "element-plus/theme-chalk/src/dark/css-vars.scss" as *;
2 |
3 | /* 暗黑模式适配 */
4 | html.dark {
5 | /* 自定义深色背景颜色 */
6 | // --el-bg-color: #020409;
7 | $border-style: #303030;
8 | $color-white: #fff;
9 |
10 | .navbar,
11 | .tags-view,
12 | .contextmenu,
13 | .sidebar-container,
14 | .horizontal-header,
15 | .sidebar-logo-container,
16 | .horizontal-header .el-sub-menu__title,
17 | .horizontal-header .submenu-title-noDropdown {
18 | background: var(--el-bg-color) !important;
19 | }
20 |
21 | .app-main {
22 | background: #020409 !important;
23 | }
24 |
25 | .frame {
26 | filter: invert(0.9) hue-rotate(180deg);
27 | }
28 |
29 | /* 标签页 */
30 | .tags-view {
31 | .arrow-left,
32 | .arrow-right {
33 | border-right: 1px solid $border-style;
34 | box-shadow: none;
35 | }
36 |
37 | .arrow-right {
38 | border-left: 1px solid $border-style;
39 | }
40 | }
41 |
42 | /* 项目配置面板 */
43 | .right-panel-items {
44 | .el-divider__text {
45 | --el-bg-color: var(--el-bg-color);
46 | }
47 |
48 | .el-divider--horizontal {
49 | border-top: none;
50 | }
51 | }
52 |
53 | /* element-plus */
54 | .el-table__cell {
55 | background: var(--el-bg-color);
56 | }
57 |
58 | .el-card {
59 | --el-card-bg-color: var(--el-bg-color);
60 |
61 | // border: none !important;
62 | }
63 |
64 | .el-backtop {
65 | --el-backtop-bg-color: var(--el-color-primary-light-9);
66 | --el-backtop-hover-bg-color: var(--el-color-primary);
67 | }
68 |
69 | .el-dropdown-menu__item:not(.is-disabled):hover {
70 | background: transparent;
71 | }
72 |
73 | /* 全局覆盖element-plus的el-dialog、el-drawer、el-message-box、el-notification组件右上角关闭图标的样式,表现更鲜明 */
74 | .el-icon {
75 | &.el-dialog__close,
76 | &.el-drawer__close,
77 | &.el-message-box__close,
78 | &.el-notification__closeBtn {
79 | &:hover {
80 | color: rgb(255 255 255 / 85%) !important;
81 | background-color: rgb(255 255 255 / 12%);
82 | }
83 | }
84 | }
85 |
86 | /* 克隆并自定义 ElMessage 样式,不会影响 ElMessage 原本样式,在 src/utils/message.ts 中调用自定义样式 ElMessage 方法即可,非暗黑模式在 src/style/element-plus.scss 文件进行了适配 */
87 | .pure-message {
88 | background-color: rgb(36 37 37) !important;
89 | background-image: initial !important;
90 | box-shadow: rgb(13 13 13 / 12%) 0 3px 6px -4px,
91 | rgb(13 13 13 / 8%) 0 6px 16px 0, rgb(13 13 13 / 5%) 0 9px 28px 8px !important;
92 |
93 | & .el-message__content {
94 | color: $color-white !important;
95 | pointer-events: all !important;
96 | background-image: initial !important;
97 | }
98 |
99 | & .el-message__closeBtn {
100 | &:hover {
101 | color: rgb(255 255 255 / 85%);
102 | background-color: rgb(255 255 255 / 12%);
103 | }
104 | }
105 | }
106 | }
107 |
--------------------------------------------------------------------------------
/src/layout/components/sidebar/breadCrumb.vue:
--------------------------------------------------------------------------------
1 |
89 |
90 |
91 |
92 |
93 |
98 |
99 | {{ item.meta.title }}
100 |
101 |
102 |
103 |
104 |
105 |
--------------------------------------------------------------------------------
/src/views/sys/role/components/assign.vue:
--------------------------------------------------------------------------------
1 |
86 |
87 |
88 |
95 |
104 |
105 | 取 消
106 | 确 定
107 |
108 |
109 |
110 |
--------------------------------------------------------------------------------
/src/views/sys/role/components/edit.vue:
--------------------------------------------------------------------------------
1 |
65 |
66 |
67 |
74 |
80 |
81 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 | 取 消
99 |
100 | 确定
101 |
102 |
103 |
104 |
105 |
--------------------------------------------------------------------------------
/src/layout/components/sidebar/horizontal.vue:
--------------------------------------------------------------------------------
1 |
37 |
38 |
39 |
94 |
95 |
96 |
111 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | env: {
4 | node: true
5 | },
6 | globals: {
7 | // Ref sugar (take 2)
8 | $: "readonly",
9 | $$: "readonly",
10 | $ref: "readonly",
11 | $shallowRef: "readonly",
12 | $computed: "readonly",
13 |
14 | // index.d.ts
15 | // global.d.ts
16 | Fn: "readonly",
17 | PromiseFn: "readonly",
18 | RefType: "readonly",
19 | LabelValueOptions: "readonly",
20 | EmitType: "readonly",
21 | TargetContext: "readonly",
22 | ComponentElRef: "readonly",
23 | ComponentRef: "readonly",
24 | ElRef: "readonly",
25 | global: "readonly",
26 | ForDataType: "readonly",
27 | ComponentRoutes: "readonly",
28 |
29 | // script setup
30 | defineProps: "readonly",
31 | defineEmits: "readonly",
32 | defineExpose: "readonly",
33 | withDefaults: "readonly"
34 | },
35 | extends: [
36 | "plugin:vue/vue3-essential",
37 | "eslint:recommended",
38 | "@vue/typescript/recommended",
39 | "@vue/prettier",
40 | "@vue/eslint-config-typescript"
41 | ],
42 | parser: "vue-eslint-parser",
43 | parserOptions: {
44 | parser: "@typescript-eslint/parser",
45 | ecmaVersion: 2020,
46 | sourceType: "module",
47 | jsxPragma: "React",
48 | ecmaFeatures: {
49 | jsx: true
50 | }
51 | },
52 | overrides: [
53 | {
54 | files: ["*.ts", "*.vue"],
55 | rules: {
56 | "no-undef": "off"
57 | }
58 | },
59 | {
60 | files: ["*.vue"],
61 | parser: "vue-eslint-parser",
62 | parserOptions: {
63 | parser: "@typescript-eslint/parser",
64 | extraFileExtensions: [".vue"],
65 | ecmaVersion: "latest",
66 | ecmaFeatures: {
67 | jsx: true
68 | }
69 | },
70 | rules: {
71 | "no-undef": "off"
72 | }
73 | }
74 | ],
75 | rules: {
76 | "vue/no-v-html": "off",
77 | "vue/require-default-prop": "off",
78 | "vue/require-explicit-emits": "off",
79 | "vue/multi-word-component-names": "off",
80 | "@typescript-eslint/no-explicit-any": "off", // any
81 | "no-debugger": "off",
82 | "@typescript-eslint/explicit-module-boundary-types": "off", // setup()
83 | "@typescript-eslint/ban-types": "off",
84 | "@typescript-eslint/ban-ts-comment": "off",
85 | "@typescript-eslint/no-empty-function": "off",
86 | "@typescript-eslint/no-non-null-assertion": "off",
87 | "vue/html-self-closing": [
88 | "error",
89 | {
90 | html: {
91 | void: "always",
92 | normal: "always",
93 | component: "always"
94 | },
95 | svg: "always",
96 | math: "always"
97 | }
98 | ],
99 | "@typescript-eslint/no-unused-vars": [
100 | "error",
101 | {
102 | argsIgnorePattern: "^_",
103 | varsIgnorePattern: "^_"
104 | }
105 | ],
106 | "no-unused-vars": [
107 | "error",
108 | {
109 | argsIgnorePattern: "^_",
110 | varsIgnorePattern: "^_"
111 | }
112 | ],
113 | "prettier/prettier": [
114 | "error",
115 | {
116 | endOfLine: "auto"
117 | }
118 | ]
119 | }
120 | };
121 |
--------------------------------------------------------------------------------
/src/style/element-plus.scss:
--------------------------------------------------------------------------------
1 | .el-breadcrumb__inner,
2 | .el-breadcrumb__inner a {
3 | font-weight: 400 !important;
4 | }
5 |
6 | .el-upload {
7 | input[type="file"] {
8 | display: none !important;
9 | }
10 | }
11 |
12 | .el-upload__input {
13 | display: none;
14 | }
15 |
16 | .upload-container {
17 | .el-upload {
18 | width: 100%;
19 |
20 | .el-upload-dragger {
21 | width: 100%;
22 | height: 200px;
23 | }
24 | }
25 | }
26 |
27 | .el-dropdown-menu {
28 | padding: 0 !important;
29 | }
30 |
31 | .el-range-separator {
32 | box-sizing: content-box;
33 | }
34 |
35 | .is-dark {
36 | z-index: 9999 !important;
37 | }
38 |
39 | /* 重置 el-button 中 icon 的 margin */
40 | .reset-margin [class*="el-icon"] + span {
41 | margin-left: 2px !important;
42 | }
43 |
44 | /* 自定义 popover 的类名 */
45 | .pure-popper {
46 | padding: 0 !important;
47 | }
48 |
49 | /* 自定义 tooltip 的类名 */
50 | .pure-tooltip {
51 | // 右侧操作面板right-panel类名的z-index为40000,tooltip需要大于它才能显示
52 | z-index: 41000 !important;
53 | }
54 |
55 | /* nprogress 适配 element-plus 的主题色 */
56 | #nprogress {
57 | & .bar {
58 | background-color: var(--el-color-primary) !important;
59 | }
60 |
61 | & .peg {
62 | box-shadow: 0 0 10px var(--el-color-primary),
63 | 0 0 5px var(--el-color-primary) !important;
64 | }
65 |
66 | & .spinner-icon {
67 | border-top-color: var(--el-color-primary);
68 | border-left-color: var(--el-color-primary);
69 | }
70 | }
71 |
72 | /* 全局覆盖element-plus的el-dialog、el-drawer、el-message-box、el-notification组件右上角关闭图标的样式,表现更鲜明 */
73 | .el-dialog__headerbtn,
74 | .el-message-box__headerbtn {
75 | &:hover {
76 | .el-dialog__close {
77 | color: var(--el-color-info) !important;
78 | }
79 | }
80 | }
81 |
82 | .el-icon {
83 | &.el-dialog__close,
84 | &.el-drawer__close,
85 | &.el-message-box__close,
86 | &.el-notification__closeBtn {
87 | width: 24px;
88 | height: 24px;
89 | border-radius: 4px;
90 | outline: none;
91 | transition: background-color 0.2s, color 0.2s;
92 |
93 | &:hover {
94 | color: rgb(0 0 0 / 88%) !important;
95 | text-decoration: none;
96 | background-color: rgb(0 0 0 / 6%);
97 | }
98 | }
99 | }
100 |
101 | /* 克隆并自定义 ElMessage 样式,不会影响 ElMessage 原本样式,在 src/utils/message.ts 中调用自定义样式 ElMessage 方法即可,暗黑模式在 src/style/dark.scss 文件进行了适配 */
102 | .pure-message {
103 | padding: 10px 13px !important;
104 | background: #fff !important;
105 | border-width: 0 !important;
106 | box-shadow: 0 3px 6px -4px #0000001f, 0 6px 16px #00000014,
107 | 0 9px 28px 8px #0000000d !important;
108 |
109 | &.el-message.is-closable .el-message__content {
110 | padding-right: 17px !important;
111 | }
112 |
113 | & .el-message__content {
114 | color: #000000d9 !important;
115 | pointer-events: all !important;
116 | background-image: initial !important;
117 | }
118 |
119 | & .el-message__icon {
120 | margin-right: 8px !important;
121 | }
122 |
123 | & .el-message__closeBtn {
124 | right: 9px !important;
125 | border-radius: 4px;
126 | outline: none;
127 | transition: background-color 0.2s, color 0.2s;
128 |
129 | &:hover {
130 | background-color: rgb(0 0 0 / 6%);
131 | }
132 | }
133 | }
134 |
--------------------------------------------------------------------------------
/src/views/sys/user/index.vue:
--------------------------------------------------------------------------------
1 |
31 |
32 |
33 |
34 |
35 |
36 |
37 | 添加
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 | {{ role }}
48 |
49 |
50 |
51 |
52 |
53 | {{ TimeDefault(row.created_at, "YYYY-MM-DD HH:mm:ss") }}
54 |
55 |
56 |
57 |
58 |
65 |
72 |
73 |
74 |
80 |
81 |
82 |
83 |
84 |
85 |
90 |
91 |
92 |
93 |
94 |
95 |
--------------------------------------------------------------------------------
/src/layout/components/sidebar/vertical.vue:
--------------------------------------------------------------------------------
1 |
69 |
70 |
71 |
75 |
76 |
80 | menuSelect(indexPath, routers)"
89 | >
90 |
97 |
98 |
99 |
104 |
105 |
106 |
107 |
112 |
--------------------------------------------------------------------------------
/src/layout/hooks/useDataThemeChange.ts:
--------------------------------------------------------------------------------
1 | import { ref } from "vue";
2 | import { getConfig } from "@/config";
3 | import { useLayout } from "./useLayout";
4 | import { themeColorsType } from "../types";
5 | import { useGlobal } from "@pureadmin/utils";
6 | import { useEpThemeStoreHook } from "@/store/modules/epTheme";
7 | import {
8 | darken,
9 | lighten,
10 | toggleTheme
11 | } from "@pureadmin/theme/dist/browser-utils";
12 |
13 | export function useDataThemeChange() {
14 | const { layoutTheme, layout } = useLayout();
15 | const themeColors = ref>([
16 | /* 道奇蓝(默认) */
17 | { color: "#1b2a47", themeColor: "default" },
18 | /* 亮白色 */
19 | { color: "#ffffff", themeColor: "light" },
20 | /* 猩红色 */
21 | { color: "#f5222d", themeColor: "dusk" },
22 | /* 橙红色 */
23 | { color: "#fa541c", themeColor: "volcano" },
24 | /* 金色 */
25 | { color: "#fadb14", themeColor: "yellow" },
26 | /* 绿宝石 */
27 | { color: "#13c2c2", themeColor: "mingQing" },
28 | /* 酸橙绿 */
29 | { color: "#52c41a", themeColor: "auroraGreen" },
30 | /* 深粉色 */
31 | { color: "#eb2f96", themeColor: "pink" },
32 | /* 深紫罗兰色 */
33 | { color: "#722ed1", themeColor: "saucePurple" }
34 | ]);
35 |
36 | const { $storage } = useGlobal();
37 | const dataTheme = ref($storage?.layout?.darkMode);
38 | const body = document.documentElement as HTMLElement;
39 |
40 | /** 设置导航主题色 */
41 | function setLayoutThemeColor(theme = getConfig().Theme ?? "default") {
42 | layoutTheme.value.theme = theme;
43 | toggleTheme({
44 | scopeName: `layout-theme-${theme}`
45 | });
46 | $storage.layout = {
47 | layout: layout.value,
48 | theme,
49 | darkMode: dataTheme.value,
50 | sidebarStatus: $storage.layout?.sidebarStatus,
51 | epThemeColor: $storage.layout?.epThemeColor
52 | };
53 |
54 | if (theme === "default" || theme === "light") {
55 | setEpThemeColor(getConfig().EpThemeColor);
56 | } else {
57 | const colors = themeColors.value.find(v => v.themeColor === theme);
58 | setEpThemeColor(colors.color);
59 | }
60 | }
61 |
62 | function setPropertyPrimary(mode: string, i: number, color: string) {
63 | document.documentElement.style.setProperty(
64 | `--el-color-primary-${mode}-${i}`,
65 | dataTheme.value ? darken(color, i / 10) : lighten(color, i / 10)
66 | );
67 | }
68 |
69 | /** 设置 `element-plus` 主题色 */
70 | const setEpThemeColor = (color: string) => {
71 | useEpThemeStoreHook().setEpThemeColor(color);
72 | document.documentElement.style.setProperty("--el-color-primary", color);
73 | for (let i = 1; i <= 2; i++) {
74 | setPropertyPrimary("dark", i, color);
75 | }
76 | for (let i = 1; i <= 9; i++) {
77 | setPropertyPrimary("light", i, color);
78 | }
79 | };
80 |
81 | /** 日间、夜间主题切换 */
82 | function dataThemeChange() {
83 | /* 如果当前是light夜间主题,默认切换到default主题 */
84 | if (useEpThemeStoreHook().epTheme === "light" && dataTheme.value) {
85 | setLayoutThemeColor("default");
86 | } else {
87 | setLayoutThemeColor(useEpThemeStoreHook().epTheme);
88 | }
89 |
90 | if (dataTheme.value) {
91 | document.documentElement.classList.add("dark");
92 | } else {
93 | document.documentElement.classList.remove("dark");
94 | }
95 | }
96 |
97 | return {
98 | body,
99 | dataTheme,
100 | layoutTheme,
101 | themeColors,
102 | dataThemeChange,
103 | setEpThemeColor,
104 | setLayoutThemeColor
105 | };
106 | }
107 |
--------------------------------------------------------------------------------
/src/layout/components/navbar.vue:
--------------------------------------------------------------------------------
1 |
23 |
24 |
25 |
28 |
34 |
35 |
39 |
40 |
41 |
42 |
73 |
74 |
75 |
76 |
134 |
--------------------------------------------------------------------------------
/src/components/ReDialog/index.vue:
--------------------------------------------------------------------------------
1 |
69 |
70 |
71 |
81 |
82 |
86 |
89 |
90 |
91 | handleClose(options, index, args)"
95 | />
96 |
97 |
98 |
99 |
100 |
101 |
102 |
113 | {{ btn?.label }}
114 |
115 |
116 |
117 |
118 |
119 |
--------------------------------------------------------------------------------
/src/layout/components/panel/index.vue:
--------------------------------------------------------------------------------
1 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
项目配置
42 |
43 |
50 |
51 |
52 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
68 |
69 |
152 |
--------------------------------------------------------------------------------
/src/layout/components/notice/data.ts:
--------------------------------------------------------------------------------
1 | export interface ListItem {
2 | avatar: string;
3 | title: string;
4 | datetime: string;
5 | type: string;
6 | description: string;
7 | status?: "" | "success" | "warning" | "info" | "danger";
8 | extra?: string;
9 | }
10 |
11 | export interface TabItem {
12 | key: string;
13 | name: string;
14 | list: ListItem[];
15 | }
16 |
17 | export const noticesData: TabItem[] = [
18 | {
19 | key: "1",
20 | name: "通知",
21 | list: [
22 | {
23 | avatar:
24 | "https://gw.alipayobjects.com/zos/rmsportal/ThXAXghbEsBCCSDihZxY.png",
25 | title: "你收到了 12 份新周报",
26 | datetime: "一年前",
27 | description: "",
28 | type: "1"
29 | },
30 | {
31 | avatar:
32 | "https://gw.alipayobjects.com/zos/rmsportal/OKJXDXrmkNshAMvwtvhu.png",
33 | title: "你推荐的 前端高手 已通过第三轮面试",
34 | datetime: "一年前",
35 | description: "",
36 | type: "1"
37 | },
38 | {
39 | avatar:
40 | "https://gw.alipayobjects.com/zos/rmsportal/kISTdvpyTAhtGxpovNWd.png",
41 | title: "这种模板可以区分多种通知类型",
42 | datetime: "一年前",
43 | description: "",
44 | type: "1"
45 | },
46 | {
47 | avatar:
48 | "https://gw.alipayobjects.com/zos/rmsportal/GvqBnKhFgObvnSGkDsje.png",
49 | title:
50 | "展示标题内容超过一行后的处理方式,如果内容超过1行将自动截断并支持tooltip显示完整标题。",
51 | datetime: "一年前",
52 | description: "",
53 | type: "1"
54 | },
55 | {
56 | avatar:
57 | "https://gw.alipayobjects.com/zos/rmsportal/GvqBnKhFgObvnSGkDsje.png",
58 | title: "左侧图标用于区分不同的类型",
59 | datetime: "一年前",
60 | description: "",
61 | type: "1"
62 | },
63 | {
64 | avatar:
65 | "https://gw.alipayobjects.com/zos/rmsportal/GvqBnKhFgObvnSGkDsje.png",
66 | title: "左侧图标用于区分不同的类型",
67 | datetime: "一年前",
68 | description: "",
69 | type: "1"
70 | }
71 | ]
72 | },
73 | {
74 | key: "2",
75 | name: "消息",
76 | list: [
77 | {
78 | avatar:
79 | "https://gw.alipayobjects.com/zos/rmsportal/fcHMVNCjPOsbUGdEduuv.jpeg",
80 | title: "李白 评论了你",
81 | description: "长风破浪会有时,直挂云帆济沧海",
82 | datetime: "一年前",
83 | type: "2"
84 | },
85 | {
86 | avatar:
87 | "https://gw.alipayobjects.com/zos/rmsportal/fcHMVNCjPOsbUGdEduuv.jpeg",
88 | title: "李白 回复了你",
89 | description: "行路难,行路难,多歧路,今安在。",
90 | datetime: "一年前",
91 | type: "2"
92 | },
93 | {
94 | avatar:
95 | "https://gw.alipayobjects.com/zos/rmsportal/fcHMVNCjPOsbUGdEduuv.jpeg",
96 | title: "标题",
97 | description:
98 | "请将鼠标移动到此处,以便测试超长的消息在此处将如何处理。本例中设置的描述最大行数为2,超过2行的描述内容将被省略并且可以通过tooltip查看完整内容",
99 | datetime: "一年前",
100 | type: "2"
101 | }
102 | ]
103 | },
104 | {
105 | key: "3",
106 | name: "代办",
107 | list: [
108 | {
109 | avatar: "",
110 | title: "任务名称",
111 | description: "任务需要在 2022-11-16 20:00 前启动",
112 | datetime: "",
113 | extra: "未开始",
114 | status: "info",
115 | type: "3"
116 | },
117 | {
118 | avatar: "",
119 | title: "第三方紧急代码变更",
120 | description:
121 | "一拳提交于 2022-11-16,需在 2022-11-18 前完成代码变更任务",
122 | datetime: "",
123 | extra: "马上到期",
124 | status: "danger",
125 | type: "3"
126 | },
127 | {
128 | avatar: "",
129 | title: "信息安全考试",
130 | description: "指派小仙于 2022-12-12 前完成更新并发布",
131 | datetime: "",
132 | extra: "已耗时 8 天",
133 | status: "warning",
134 | type: "3"
135 | },
136 | {
137 | avatar: "",
138 | title: "vue-pure-admin 版本发布",
139 | description: "vue-pure-admin 版本发布",
140 | datetime: "",
141 | extra: "进行中",
142 | type: "3"
143 | }
144 | ]
145 | }
146 | ];
147 |
--------------------------------------------------------------------------------
/src/layout/theme/index.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @description ⚠️:此文件仅供主题插件使用,请不要在此文件中导出别的工具函数(仅在页面加载前运行)
3 | */
4 |
5 | import { type multipleScopeVarsOptions } from "@pureadmin/theme";
6 |
7 | /** 预设主题色 */
8 | const themeColors = {
9 | default: {
10 | subMenuActiveText: "#fff",
11 | menuBg: "#001529",
12 | menuHover: "#4091f7",
13 | subMenuBg: "#0f0303",
14 | subMenuActiveBg: "#4091f7",
15 | menuText: "rgb(254 254 254 / 65%)",
16 | sidebarLogo: "#002140",
17 | menuTitleHover: "#fff",
18 | menuActiveBefore: "#4091f7"
19 | },
20 | light: {
21 | subMenuActiveText: "#409eff",
22 | menuBg: "#fff",
23 | menuHover: "#e0ebf6",
24 | subMenuBg: "#fff",
25 | subMenuActiveBg: "#e0ebf6",
26 | menuText: "#7a80b4",
27 | sidebarLogo: "#fff",
28 | menuTitleHover: "#000",
29 | menuActiveBefore: "#4091f7"
30 | },
31 | dusk: {
32 | subMenuActiveText: "#fff",
33 | menuBg: "#2a0608",
34 | menuHover: "#e13c39",
35 | subMenuBg: "#000",
36 | subMenuActiveBg: "#e13c39",
37 | menuText: "rgb(254 254 254 / 65.1%)",
38 | sidebarLogo: "#42090c",
39 | menuTitleHover: "#fff",
40 | menuActiveBefore: "#e13c39"
41 | },
42 | volcano: {
43 | subMenuActiveText: "#fff",
44 | menuBg: "#2b0e05",
45 | menuHover: "#e85f33",
46 | subMenuBg: "#0f0603",
47 | subMenuActiveBg: "#e85f33",
48 | menuText: "rgb(254 254 254 / 65%)",
49 | sidebarLogo: "#441708",
50 | menuTitleHover: "#fff",
51 | menuActiveBefore: "#e85f33"
52 | },
53 | yellow: {
54 | subMenuActiveText: "#d25f00",
55 | menuBg: "#2b2503",
56 | menuHover: "#f6da4d",
57 | subMenuBg: "#0f0603",
58 | subMenuActiveBg: "#f6da4d",
59 | menuText: "rgb(254 254 254 / 65%)",
60 | sidebarLogo: "#443b05",
61 | menuTitleHover: "#fff",
62 | menuActiveBefore: "#f6da4d"
63 | },
64 | mingQing: {
65 | subMenuActiveText: "#fff",
66 | menuBg: "#032121",
67 | menuHover: "#59bfc1",
68 | subMenuBg: "#000",
69 | subMenuActiveBg: "#59bfc1",
70 | menuText: "#7a80b4",
71 | sidebarLogo: "#053434",
72 | menuTitleHover: "#fff",
73 | menuActiveBefore: "#59bfc1"
74 | },
75 | auroraGreen: {
76 | subMenuActiveText: "#fff",
77 | menuBg: "#0b1e15",
78 | menuHover: "#60ac80",
79 | subMenuBg: "#000",
80 | subMenuActiveBg: "#60ac80",
81 | menuText: "#7a80b4",
82 | sidebarLogo: "#112f21",
83 | menuTitleHover: "#fff",
84 | menuActiveBefore: "#60ac80"
85 | },
86 | pink: {
87 | subMenuActiveText: "#fff",
88 | menuBg: "#28081a",
89 | menuHover: "#d84493",
90 | subMenuBg: "#000",
91 | subMenuActiveBg: "#d84493",
92 | menuText: "#7a80b4",
93 | sidebarLogo: "#3f0d29",
94 | menuTitleHover: "#fff",
95 | menuActiveBefore: "#d84493"
96 | },
97 | saucePurple: {
98 | subMenuActiveText: "#fff",
99 | menuBg: "#130824",
100 | menuHover: "#693ac9",
101 | subMenuBg: "#000",
102 | subMenuActiveBg: "#693ac9",
103 | menuText: "#7a80b4",
104 | sidebarLogo: "#1f0c38",
105 | menuTitleHover: "#fff",
106 | menuActiveBefore: "#693ac9"
107 | }
108 | };
109 |
110 | /**
111 | * @description 将预设主题色处理成主题插件所需格式
112 | */
113 | export const genScssMultipleScopeVars = (): multipleScopeVarsOptions[] => {
114 | const result = [] as multipleScopeVarsOptions[];
115 | Object.keys(themeColors).forEach(key => {
116 | result.push({
117 | scopeName: `layout-theme-${key}`,
118 | varsContent: `
119 | $subMenuActiveText: ${themeColors[key].subMenuActiveText} !default;
120 | $menuBg: ${themeColors[key].menuBg} !default;
121 | $menuHover: ${themeColors[key].menuHover} !default;
122 | $subMenuBg: ${themeColors[key].subMenuBg} !default;
123 | $subMenuActiveBg: ${themeColors[key].subMenuActiveBg} !default;
124 | $menuText: ${themeColors[key].menuText} !default;
125 | $sidebarLogo: ${themeColors[key].sidebarLogo} !default;
126 | $menuTitleHover: ${themeColors[key].menuTitleHover} !default;
127 | $menuActiveBefore: ${themeColors[key].menuActiveBefore} !default;
128 | `
129 | } as multipleScopeVarsOptions);
130 | });
131 | return result;
132 | };
133 |
--------------------------------------------------------------------------------
/src/views/sys/menu/index.vue:
--------------------------------------------------------------------------------
1 |
22 |
23 |
24 |
25 |
26 |
27 |
28 | 添加
29 |
30 |
31 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 | 菜单
54 | 操作
55 | 外链
56 |
57 |
58 |
59 |
60 | 是
61 | 否
62 |
63 |
64 |
65 |
66 | 否
67 | 是
68 |
69 |
70 |
71 |
72 | 是
73 | 否
74 |
75 |
76 |
77 |
78 |
85 |
86 |
87 |
94 |
95 |
96 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
--------------------------------------------------------------------------------
/src/layout/components/sidebar/mixNav.vue:
--------------------------------------------------------------------------------
1 |
55 |
56 |
57 |
125 |
126 |
127 |
142 |
--------------------------------------------------------------------------------
/src/style/reset.scss:
--------------------------------------------------------------------------------
1 | *,
2 | ::before,
3 | ::after {
4 | box-sizing: border-box;
5 | border-color: currentColor;
6 | border-style: solid;
7 | border-width: 0;
8 | }
9 |
10 | #app {
11 | width: 100%;
12 | height: 100%;
13 | }
14 |
15 | html {
16 | box-sizing: border-box;
17 | width: 100%;
18 | height: 100%;
19 | line-height: 1.5;
20 | tab-size: 4;
21 | text-size-adjust: 100%;
22 | }
23 |
24 | body {
25 | width: 100%;
26 | height: 100%;
27 | margin: 0;
28 | font-family: "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB",
29 | "Microsoft YaHei", "微软雅黑", Arial, sans-serif;
30 | line-height: inherit;
31 | -moz-osx-font-smoothing: grayscale;
32 | -webkit-font-smoothing: antialiased;
33 | text-rendering: optimizelegibility;
34 | }
35 |
36 | hr {
37 | height: 0;
38 | color: inherit;
39 | border-top-width: 1px;
40 | }
41 |
42 | abbr:where([title]) {
43 | text-decoration: underline dotted;
44 | }
45 |
46 | a {
47 | color: inherit;
48 | text-decoration: inherit;
49 | }
50 |
51 | b,
52 | strong {
53 | font-weight: bolder;
54 | }
55 |
56 | code,
57 | kbd,
58 | samp,
59 | pre {
60 | font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
61 | "Liberation Mono", "Courier New", monospace;
62 | font-size: 1em;
63 | }
64 |
65 | small {
66 | font-size: 80%;
67 | }
68 |
69 | sub,
70 | sup {
71 | position: relative;
72 | font-size: 75%;
73 | line-height: 0;
74 | vertical-align: baseline;
75 | }
76 |
77 | sub {
78 | bottom: -0.25em;
79 | }
80 |
81 | sup {
82 | top: -0.5em;
83 | }
84 |
85 | table {
86 | text-indent: 0;
87 | border-collapse: collapse;
88 | border-color: inherit;
89 | }
90 |
91 | button,
92 | input,
93 | optgroup,
94 | select,
95 | textarea {
96 | padding: 0;
97 | margin: 0;
98 | font-family: inherit;
99 | font-size: 100%;
100 | line-height: inherit;
101 | color: inherit;
102 | }
103 |
104 | button,
105 | select {
106 | text-transform: none;
107 | }
108 |
109 | button,
110 | [type="button"],
111 | [type="reset"],
112 | [type="submit"] {
113 | background-image: none;
114 | }
115 |
116 | :-moz-focusring {
117 | outline: auto;
118 | }
119 |
120 | :-moz-ui-invalid {
121 | box-shadow: none;
122 | }
123 |
124 | progress {
125 | vertical-align: baseline;
126 | }
127 |
128 | ::-webkit-inner-spin-button,
129 | ::-webkit-outer-spin-button {
130 | height: auto;
131 | }
132 |
133 | [type="search"] {
134 | outline-offset: -2px;
135 | }
136 |
137 | ::-webkit-file-upload-button {
138 | font: inherit;
139 | }
140 |
141 | summary {
142 | display: list-item;
143 | }
144 |
145 | blockquote,
146 | dl,
147 | dd,
148 | h1,
149 | h2,
150 | h3,
151 | h4,
152 | h5,
153 | h6,
154 | hr,
155 | figure,
156 | p,
157 | pre {
158 | margin: 0;
159 | }
160 |
161 | fieldset {
162 | padding: 0;
163 | margin: 0;
164 | }
165 |
166 | legend {
167 | padding: 0;
168 | }
169 |
170 | ol,
171 | ul,
172 | menu {
173 | padding: 0;
174 | margin: 0;
175 | list-style: none;
176 | }
177 |
178 | textarea {
179 | resize: vertical;
180 | }
181 |
182 | input::placeholder,
183 | textarea::placeholder {
184 | color: #9ca3af;
185 | opacity: 1;
186 | }
187 |
188 | button,
189 | [role="button"] {
190 | cursor: pointer;
191 | }
192 |
193 | :disabled {
194 | cursor: default;
195 | }
196 |
197 | img,
198 | svg,
199 | video,
200 | canvas,
201 | audio,
202 | iframe,
203 | embed,
204 | object {
205 | display: block;
206 | }
207 |
208 | img,
209 | video {
210 | max-width: 100%;
211 | height: auto;
212 | }
213 |
214 | [hidden] {
215 | display: none;
216 | }
217 |
218 | .dark {
219 | color-scheme: dark;
220 | }
221 |
222 | // label {
223 | // font-weight: 700;
224 | // }
225 |
226 | // html body .el-table th,
227 | // html body[class*=vab-theme-] .el-table th {
228 | // background: #f5f7fa !important
229 | // }
230 |
231 | *,
232 | *::before,
233 | *::after {
234 | box-sizing: inherit;
235 | }
236 |
237 | a:focus,
238 | a:active {
239 | outline: none;
240 | }
241 |
242 | a,
243 | a:focus,
244 | a:hover {
245 | color: inherit;
246 | text-decoration: none;
247 | cursor: pointer;
248 | }
249 |
250 | div:focus {
251 | outline: none;
252 | }
253 |
254 | .clearfix {
255 | &::after {
256 | display: block;
257 | height: 0;
258 | clear: both;
259 | font-size: 0;
260 | visibility: hidden;
261 | content: " ";
262 | }
263 | }
264 |
--------------------------------------------------------------------------------
/src/layout/components/appMain.vue:
--------------------------------------------------------------------------------
1 |
75 |
76 |
77 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
92 |
97 |
98 |
104 |
105 |
106 |
107 |
108 |
112 |
117 |
118 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
149 |
--------------------------------------------------------------------------------
/src/store/modules/multiTags.ts:
--------------------------------------------------------------------------------
1 | import { defineStore } from "pinia";
2 | import { store } from "@/store";
3 | import { routerArrays } from "@/layout/types";
4 | import { multiType, positionType } from "./types";
5 | import { responsiveStorageNameSpace } from "@/config";
6 | import { isEqual, isBoolean, isUrl, storageLocal } from "@pureadmin/utils";
7 |
8 | export const useMultiTagsStore = defineStore({
9 | id: "pure-multiTags",
10 | state: () => ({
11 | // 存储标签页信息(路由信息)
12 | multiTags: storageLocal().getItem(
13 | `${responsiveStorageNameSpace()}configure`
14 | )?.multiTagsCache
15 | ? storageLocal().getItem(
16 | `${responsiveStorageNameSpace()}tags`
17 | )
18 | : [...routerArrays],
19 | multiTagsCache: storageLocal().getItem(
20 | `${responsiveStorageNameSpace()}configure`
21 | )?.multiTagsCache
22 | }),
23 | getters: {
24 | getMultiTagsCache(state) {
25 | return state.multiTagsCache;
26 | }
27 | },
28 | actions: {
29 | multiTagsCacheChange(multiTagsCache: boolean) {
30 | this.multiTagsCache = multiTagsCache;
31 | if (multiTagsCache) {
32 | storageLocal().setItem(
33 | `${responsiveStorageNameSpace()}tags`,
34 | this.multiTags
35 | );
36 | } else {
37 | storageLocal().removeItem(`${responsiveStorageNameSpace()}tags`);
38 | }
39 | },
40 | tagsCache(multiTags) {
41 | this.getMultiTagsCache &&
42 | storageLocal().setItem(
43 | `${responsiveStorageNameSpace()}tags`,
44 | multiTags
45 | );
46 | },
47 | handleTags(
48 | mode: string,
49 | value?: T | multiType,
50 | position?: positionType
51 | ): T {
52 | switch (mode) {
53 | case "equal":
54 | this.multiTags = value;
55 | this.tagsCache(this.multiTags);
56 | break;
57 | case "push":
58 | {
59 | const tagVal = value as multiType;
60 | // 不添加到标签页
61 | if (tagVal?.meta?.hiddenTag) return;
62 | // 如果是外链无需添加信息到标签页
63 | if (isUrl(tagVal?.name)) return;
64 | // 如果title为空拒绝添加空信息到标签页
65 | if (tagVal?.meta?.title.length === 0) return;
66 | // showLink:false 不添加到标签页
67 | if (isBoolean(tagVal?.meta?.showLink) && !tagVal?.meta?.showLink)
68 | return;
69 | const tagPath = tagVal.path;
70 | // 判断tag是否已存在
71 | const tagHasExits = this.multiTags.some(tag => {
72 | return tag.path === tagPath;
73 | });
74 |
75 | // 判断tag中的query键值是否相等
76 | const tagQueryHasExits = this.multiTags.some(tag => {
77 | return isEqual(tag?.query, tagVal?.query);
78 | });
79 |
80 | // 判断tag中的params键值是否相等
81 | const tagParamsHasExits = this.multiTags.some(tag => {
82 | return isEqual(tag?.params, tagVal?.params);
83 | });
84 |
85 | if (tagHasExits && tagQueryHasExits && tagParamsHasExits) return;
86 |
87 | // 动态路由可打开的最大数量
88 | const dynamicLevel = tagVal?.meta?.dynamicLevel ?? -1;
89 | if (dynamicLevel > 0) {
90 | if (
91 | this.multiTags.filter(e => e?.path === tagPath).length >=
92 | dynamicLevel
93 | ) {
94 | // 如果当前已打开的动态路由数大于dynamicLevel,替换第一个动态路由标签
95 | const index = this.multiTags.findIndex(
96 | item => item?.path === tagPath
97 | );
98 | index !== -1 && this.multiTags.splice(index, 1);
99 | }
100 | }
101 | this.multiTags.push(value);
102 | this.tagsCache(this.multiTags);
103 | }
104 | break;
105 | case "splice":
106 | if (!position) {
107 | const index = this.multiTags.findIndex(v => v.path === value);
108 | if (index === -1) return;
109 | this.multiTags.splice(index, 1);
110 | } else {
111 | this.multiTags.splice(position?.startIndex, position?.length);
112 | }
113 | this.tagsCache(this.multiTags);
114 | return this.multiTags;
115 | case "slice":
116 | return this.multiTags.slice(-1);
117 | }
118 | }
119 | }
120 | });
121 |
122 | export function useMultiTagsStoreHook() {
123 | return useMultiTagsStore(store);
124 | }
125 |
--------------------------------------------------------------------------------