├── src ├── types │ ├── shims-vue.d.ts │ ├── env.d.ts │ └── global.d.ts ├── assets │ ├── logo.png │ ├── images │ │ ├── 401.gif │ │ ├── 404.png │ │ ├── login-bg.jpg │ │ ├── 404_cloud.png │ │ └── login-bg-dark.jpg │ └── icons │ │ ├── size.svg │ │ ├── chart.svg │ │ ├── fullscreen-exit.svg │ │ ├── fullscreen.svg │ │ ├── setting.svg │ │ ├── close_all.svg │ │ ├── close_other.svg │ │ ├── guide.svg │ │ ├── drag.svg │ │ ├── rabbitmq.svg │ │ ├── close.svg │ │ ├── language.svg │ │ ├── refresh.svg │ │ ├── shrink.svg │ │ ├── ip.svg │ │ ├── close_left.svg │ │ ├── theme.svg │ │ ├── close_right.svg │ │ ├── nested.svg │ │ ├── monitor.svg │ │ ├── document.svg │ │ ├── eye.svg │ │ ├── user.svg │ │ ├── uv.svg │ │ ├── dict_item.svg │ │ ├── link.svg │ │ ├── advert.svg │ │ ├── download.svg │ │ ├── visit.svg │ │ ├── skill.svg │ │ ├── perm.svg │ │ ├── multi_level.svg │ │ ├── eye-open.svg │ │ ├── menu.svg │ │ ├── system.svg │ │ ├── role.svg │ │ ├── message.svg │ │ ├── client.svg │ │ ├── publish.svg │ │ ├── cart.svg │ │ ├── todolist.svg │ │ ├── bug.svg │ │ ├── github.svg │ │ ├── project.svg │ │ ├── rate.svg │ │ ├── tree.svg │ │ ├── exit-fullscreen.svg │ │ ├── password.svg │ │ ├── goods-list.svg │ │ ├── homepage.svg │ │ ├── peoples.svg │ │ ├── coupon.svg │ │ ├── dict.svg │ │ ├── api.svg │ │ ├── cascader.svg │ │ ├── dashboard.svg │ │ ├── security.svg │ │ ├── captcha.svg │ │ └── redis.svg ├── plugins │ ├── index.ts │ ├── i18n.ts │ ├── icons.ts │ └── permission.ts ├── api │ ├── file │ │ ├── types.ts │ │ └── index.ts │ ├── pms │ │ ├── brand │ │ │ ├── types.ts │ │ │ └── index.ts │ │ ├── attribute │ │ │ └── index.ts │ │ ├── goods │ │ │ ├── types.ts │ │ │ └── index.ts │ │ └── category │ │ │ └── index.ts │ ├── sms │ │ ├── advert │ │ │ ├── types.ts │ │ │ └── index.ts │ │ └── coupon │ │ │ ├── index.ts │ │ │ └── types.ts │ ├── oms │ │ └── order │ │ │ ├── index.ts │ │ │ └── types.ts │ ├── auth │ │ ├── types.ts │ │ └── index.ts │ ├── system │ │ ├── dept │ │ │ ├── types.ts │ │ │ └── index.ts │ │ ├── role │ │ │ ├── types.ts │ │ │ └── index.ts │ │ ├── menu │ │ │ ├── index.ts │ │ │ └── types.ts │ │ ├── user │ │ │ └── types.ts │ │ └── dict │ │ │ └── types.ts │ └── ums │ │ └── member │ │ ├── index.ts │ │ └── types.ts ├── views │ ├── demo │ │ ├── multi-level │ │ │ ├── children │ │ │ │ ├── children │ │ │ │ │ ├── level3-1.vue │ │ │ │ │ └── level3-2.vue │ │ │ │ └── level2.vue │ │ │ └── level1.vue │ │ ├── icon-selector.vue │ │ ├── wang-editor.vue │ │ ├── api-doc.vue │ │ └── upload.vue │ ├── redirect │ │ └── index.vue │ ├── system │ │ └── user │ │ │ └── components │ │ │ └── dept-tree.vue │ ├── pms │ │ ├── category │ │ │ └── index.vue │ │ └── goods │ │ │ └── detail.vue │ ├── dashboard │ │ └── components │ │ │ ├── PieChart.vue │ │ │ ├── RadarChart.vue │ │ │ └── FunnelChart.vue │ └── error-page │ │ └── 401.vue ├── enums │ ├── LanguageEnum.ts │ ├── DeviceEnum.ts │ ├── SidebarStatusEnum.ts │ ├── ThemeEnum.ts │ ├── LayoutEnum.ts │ ├── SizeEnum.ts │ └── MenuTypeEnum.ts ├── directive │ ├── index.ts │ └── permission │ │ └── index.ts ├── utils │ ├── nprogress.ts │ ├── i18n.ts │ ├── index.ts │ ├── request.ts │ └── filter.ts ├── layout │ └── components │ │ ├── NavBar │ │ ├── index.vue │ │ └── components │ │ │ └── NavbarLeft.vue │ │ ├── Settings │ │ └── components │ │ │ ├── ThemeColorPicker.vue │ │ │ └── LayoutSelect.vue │ │ ├── Sidebar │ │ ├── components │ │ │ ├── SidebarMenuItemTitle.vue │ │ │ ├── SidebarLogo.vue │ │ │ ├── SidebarMenu.vue │ │ │ └── SidebarMixTopMenu.vue │ │ └── index.vue │ │ └── AppMain │ │ └── index.vue ├── lang │ ├── package │ │ ├── zh-cn.ts │ │ └── en.ts │ └── index.ts ├── store │ ├── index.ts │ └── modules │ │ ├── app.ts │ │ └── user.ts ├── styles │ ├── variables.module.scss │ ├── index.scss │ ├── variables.scss │ └── reset.scss ├── settings.ts ├── main.ts ├── components │ ├── Hamburger │ │ └── index.vue │ ├── AppLink │ │ └── index.vue │ ├── SizeSelect │ │ └── index.vue │ ├── SvgIcon │ │ └── index.vue │ ├── LangSelect │ │ └── index.vue │ ├── Pagination │ │ └── index.vue │ ├── Upload │ │ └── SingleUpload.vue │ ├── WangEditor │ │ └── index.vue │ ├── Dictionary │ │ └── index.vue │ ├── GithubCorner │ │ └── index.vue │ └── Breadcrumb │ │ └── index.vue ├── App.vue └── router │ └── index.ts ├── public └── favicon.ico ├── .husky ├── pre-commit └── commit-msg ├── .env.production ├── .prettierignore ├── .stylelintignore ├── .eslintignore ├── .gitignore ├── .vscode ├── extensions.json └── settings.json ├── .env.development ├── .editorconfig ├── tsconfig.json ├── LICENSE ├── uno.config.ts ├── .stylelintrc.cjs ├── .prettierrc.cjs ├── index.html └── CHANGELOG.md /src/types/shims-vue.d.ts: -------------------------------------------------------------------------------- 1 | declare module "xlsx/xlsx.mjs"; 2 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/youlaitech/mall-admin/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/youlaitech/mall-admin/HEAD/src/assets/logo.png -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npm run lint:lint-staged 5 | -------------------------------------------------------------------------------- /src/assets/images/401.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/youlaitech/mall-admin/HEAD/src/assets/images/401.gif -------------------------------------------------------------------------------- /src/assets/images/404.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/youlaitech/mall-admin/HEAD/src/assets/images/404.png -------------------------------------------------------------------------------- /.env.production: -------------------------------------------------------------------------------- 1 | ## 生产环境 2 | NODE_ENV='production' 3 | 4 | # 代理前缀 5 | VITE_APP_BASE_API = '/prod-api' 6 | 7 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx --no-install commitlint --edit $1 5 | -------------------------------------------------------------------------------- /src/plugins/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./icons"; 2 | export * from "./i18n"; 3 | export * from "./permission"; 4 | -------------------------------------------------------------------------------- /src/assets/images/login-bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/youlaitech/mall-admin/HEAD/src/assets/images/login-bg.jpg -------------------------------------------------------------------------------- /src/assets/images/404_cloud.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/youlaitech/mall-admin/HEAD/src/assets/images/404_cloud.png -------------------------------------------------------------------------------- /src/assets/images/login-bg-dark.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/youlaitech/mall-admin/HEAD/src/assets/images/login-bg-dark.jpg -------------------------------------------------------------------------------- /src/api/file/types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 文件API类型声明 3 | */ 4 | export interface FileInfo { 5 | name: string; 6 | url: string; 7 | } 8 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | public 4 | .husky 5 | .vscode 6 | .idea 7 | *.sh 8 | *.md 9 | 10 | src/assets 11 | stats.html 12 | -------------------------------------------------------------------------------- /.stylelintignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | public 4 | .husky 5 | .vscode 6 | .idea 7 | *.sh 8 | *.md 9 | 10 | src/assets 11 | stats.html 12 | -------------------------------------------------------------------------------- /src/assets/icons/size.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/plugins/i18n.ts: -------------------------------------------------------------------------------- 1 | // 国际化 2 | import i18n from "@/lang/index"; 3 | import type { App } from "vue"; 4 | 5 | export function setupI18n(app: App) { 6 | app.use(i18n); 7 | } 8 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | public 4 | .husky 5 | .vscode 6 | .idea 7 | *.sh 8 | *.md 9 | 10 | src/assets 11 | 12 | .eslintrc.cjs 13 | .prettierrc.cjs 14 | .stylelintrc.cjs 15 | -------------------------------------------------------------------------------- /src/views/demo/multi-level/children/children/level3-1.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /src/assets/icons/chart.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/views/demo/multi-level/children/children/level3-2.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /src/assets/icons/fullscreen-exit.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/fullscreen.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/enums/LanguageEnum.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 语言枚举 3 | */ 4 | export const enum LanguageEnum { 5 | /** 6 | * 中文 7 | */ 8 | ZH_CN = "zh-cn", 9 | 10 | /** 11 | * 英文 12 | */ 13 | EN = "en", 14 | } 15 | -------------------------------------------------------------------------------- /src/enums/DeviceEnum.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 设备枚举 3 | */ 4 | export const enum DeviceEnum { 5 | /** 6 | * 宽屏设备 7 | */ 8 | DESKTOP = "desktop", 9 | 10 | /** 11 | * 窄屏设备 12 | */ 13 | MOBILE = "mobile", 14 | } 15 | -------------------------------------------------------------------------------- /src/views/demo/multi-level/children/level2.vue: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /src/enums/SidebarStatusEnum.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 侧边栏状态枚举 3 | */ 4 | export const enum SidebarStatusEnum { 5 | /** 6 | * 展开 7 | */ 8 | OPENED = "opened", 9 | 10 | /** 11 | * 关闭 12 | */ 13 | CLOSED = "closed", 14 | } 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | dist 4 | dist-ssr 5 | *.local 6 | .history 7 | 8 | # Editor directories and files 9 | .idea 10 | *.suo 11 | *.ntvs* 12 | *.njsproj 13 | *.sln 14 | *.local 15 | 16 | package-lock.json 17 | pnpm-lock.yaml 18 | stats.html 19 | -------------------------------------------------------------------------------- /src/directive/index.ts: -------------------------------------------------------------------------------- 1 | import type { App } from "vue"; 2 | 3 | import { hasPerm } from "./permission"; 4 | 5 | // 全局注册 directive 6 | export function setupDirective(app: App) { 7 | // 使 v-hasPerm 在所有组件中都可用 8 | app.directive("hasPerm", hasPerm); 9 | } 10 | -------------------------------------------------------------------------------- /src/enums/ThemeEnum.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 主题枚举 3 | */ 4 | export enum ThemeEnum { 5 | /** 6 | * 明亮主题 7 | */ 8 | LIGHT = "light", 9 | /** 10 | * 暗黑主题 11 | */ 12 | DARK = "dark", 13 | 14 | /** 15 | * 系统自动 16 | */ 17 | AUTO = "auto", 18 | } 19 | -------------------------------------------------------------------------------- /src/enums/LayoutEnum.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 菜单布局枚举 3 | */ 4 | export enum LayoutEnum { 5 | /** 6 | * 左侧菜单布局 7 | */ 8 | LEFT = "left", 9 | /** 10 | * 顶部菜单布局 11 | */ 12 | TOP = "top", 13 | 14 | /** 15 | * 混合菜单布局 16 | */ 17 | MIX = "mix", 18 | } 19 | -------------------------------------------------------------------------------- /src/enums/SizeEnum.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 布局大小枚举 3 | */ 4 | export const enum SizeEnum { 5 | /** 6 | * 默认 7 | */ 8 | DEFAULT = "default", 9 | 10 | /** 11 | * 大型 12 | */ 13 | LARGE = "large", 14 | 15 | /** 16 | * 小型 17 | */ 18 | SMALL = "small", 19 | } 20 | -------------------------------------------------------------------------------- /src/assets/icons/setting.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/close_all.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/close_other.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/plugins/icons.ts: -------------------------------------------------------------------------------- 1 | import type { App } from "vue"; 2 | import * as ElementPlusIconsVue from "@element-plus/icons-vue"; 3 | 4 | // 注册所有图标 5 | export function setupElIcons(app: App) { 6 | for (const [key, component] of Object.entries(ElementPlusIconsVue)) { 7 | app.component(key, component); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "vue.volar", 4 | "vue.vscode-typescript-vue-plugin", 5 | "antfu.unocss", 6 | "lokalise.i18n-ally", 7 | "dbaeumer.vscode-eslint", 8 | "esbenp.prettier-vscode", 9 | "stylelint.vscode-stylelint", 10 | "editorconfig.editorconfig" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /src/assets/icons/guide.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/enums/MenuTypeEnum.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 菜单类型枚举 3 | */ 4 | export enum MenuTypeEnum { 5 | /** 6 | * 目录 7 | */ 8 | CATALOG = "CATALOG", 9 | /** 10 | * 菜单 11 | */ 12 | MENU = "MENU", 13 | 14 | /** 15 | * 按钮 16 | */ 17 | BUTTON = "BUTTON", 18 | /** 19 | * 外链 20 | */ 21 | EXTLINK = "EXTLINK", 22 | } 23 | -------------------------------------------------------------------------------- /.env.development: -------------------------------------------------------------------------------- 1 | ## 开发环境 2 | NODE_ENV='development' 3 | 4 | # 应用端口 5 | VITE_APP_PORT = 9527 6 | 7 | # 代理前缀 8 | VITE_APP_BASE_API = '/dev-api' 9 | 10 | 11 | # 开发接口地址 12 | VITE_APP_API_URL = http://localhost:9999 13 | 14 | # 线上接口地址(暂不可用) 15 | # VITE_APP_API_URL = https://api.youlai.tech 16 | 17 | # 是否启用 Mock 服务 18 | VITE_MOCK_DEV_SERVER = false 19 | -------------------------------------------------------------------------------- /src/assets/icons/drag.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/views/redirect/index.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 16 | -------------------------------------------------------------------------------- /src/assets/icons/rabbitmq.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/utils/nprogress.ts: -------------------------------------------------------------------------------- 1 | import NProgress from "nprogress"; 2 | import "nprogress/nprogress.css"; 3 | 4 | // 进度条 5 | NProgress.configure({ 6 | // 动画方式 7 | easing: "ease", 8 | // 递增进度条的速度 9 | speed: 500, 10 | // 是否显示加载ico 11 | showSpinner: false, 12 | // 自动递增间隔 13 | trickleSpeed: 200, 14 | // 初始化时的最小百分比 15 | minimum: 0.3, 16 | }); 17 | 18 | export default NProgress; 19 | -------------------------------------------------------------------------------- /src/assets/icons/close.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | # 表示所有文件适用 5 | [*] 6 | charset = utf-8 # 设置文件字符集为 utf-8 7 | end_of_line = lf # 控制换行类型(lf | cr | crlf) 8 | indent_style = space # 缩进风格(tab | space) 9 | indent_size = 2 # 缩进大小 10 | insert_final_newline = true # 始终在文件末尾插入一个新行 11 | 12 | # 表示仅 md 文件适用以下规则 13 | [*.md] 14 | max_line_length = off # 关闭最大行长度限制 15 | trim_trailing_whitespace = false # 关闭末尾空格修剪 16 | -------------------------------------------------------------------------------- /src/layout/components/NavBar/index.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 19 | -------------------------------------------------------------------------------- /src/utils/i18n.ts: -------------------------------------------------------------------------------- 1 | // translate router.meta.title, be used in breadcrumb sidebar tagsview 2 | import i18n from "@/lang/index"; 3 | 4 | export function translateRouteTitle(title: any) { 5 | // 判断是否存在国际化配置,如果没有原生返回 6 | const hasKey = i18n.global.te("route." + title); 7 | if (hasKey) { 8 | const translatedTitle = i18n.global.t("route." + title); 9 | return translatedTitle; 10 | } 11 | return title; 12 | } 13 | -------------------------------------------------------------------------------- /src/lang/package/zh-cn.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | // 路由国际化 3 | route: { 4 | dashboard: "首页", 5 | document: "项目文档", 6 | }, 7 | // 登录页面国际化 8 | login: { 9 | username: "用户名", 10 | password: "密码", 11 | login: "登 录", 12 | captchaCode: "验证码", 13 | }, 14 | // 导航栏国际化 15 | navbar: { 16 | dashboard: "首页", 17 | logout: "注销", 18 | document: "项目文档", 19 | gitee: "码云", 20 | }, 21 | }; 22 | -------------------------------------------------------------------------------- /src/store/index.ts: -------------------------------------------------------------------------------- 1 | import type { App } from "vue"; 2 | import { createPinia } from "pinia"; 3 | 4 | const store = createPinia(); 5 | 6 | // 全局注册 store 7 | export function setupStore(app: App) { 8 | app.use(store); 9 | } 10 | 11 | export * from "./modules/app"; 12 | export * from "./modules/permission"; 13 | export * from "./modules/settings"; 14 | export * from "./modules/tagsView"; 15 | export * from "./modules/user"; 16 | export { store }; 17 | -------------------------------------------------------------------------------- /src/views/demo/multi-level/level1.vue: -------------------------------------------------------------------------------- 1 | 16 | -------------------------------------------------------------------------------- /src/lang/package/en.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | // 路由国际化 3 | route: { 4 | dashboard: "Dashboard", 5 | document: "Document", 6 | }, 7 | // 登录页面国际化 8 | login: { 9 | username: "Username", 10 | password: "Password", 11 | login: "Login", 12 | captchaCode: "Verify Code", 13 | }, 14 | // 导航栏国际化 15 | navbar: { 16 | dashboard: "Dashboard", 17 | logout: "Logout", 18 | document: "Document", 19 | gitee: "Gitee", 20 | }, 21 | }; 22 | -------------------------------------------------------------------------------- /src/layout/components/NavBar/components/NavbarLeft.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 20 | -------------------------------------------------------------------------------- /src/views/demo/icon-selector.vue: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 18 | -------------------------------------------------------------------------------- /src/styles/variables.module.scss: -------------------------------------------------------------------------------- 1 | /* stylelint-disable property-no-unknown */ 2 | :export { 3 | sidebar-width: $sidebar-width; 4 | navbar-height: $navbar-height; 5 | menu-background: $menu-background; 6 | menu-text: $menu-text; 7 | menu-active-text: $menu-active-text; 8 | menu-hover: $menu-hover; 9 | sub-menu-background: $sub-menu-background; 10 | sub-menu-active-text: $sub-menu-active-text; 11 | sub-menu-hover: $sub-menu-hover; 12 | } 13 | /* stylelint-enable property-no-unknown */ 14 | -------------------------------------------------------------------------------- /src/assets/icons/language.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/api/pms/brand/types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 品牌查询参数类型声明 3 | */ 4 | export interface BrandQuery extends PageQuery { 5 | name?: string; 6 | } 7 | 8 | /** 9 | * 品牌分页列表项声明 10 | */ 11 | export interface Brand { 12 | id: string; 13 | name: string; 14 | logoUrl: string; 15 | sort: number; 16 | } 17 | 18 | /** 19 | * 品牌分页项类型声明 20 | */ 21 | export type BrandPageResult = PageResult; 22 | 23 | /** 24 | * 品牌表单类型声明 25 | */ 26 | export interface BrandForm { 27 | id: number | undefined; 28 | name: string; 29 | logoUrl: string; 30 | sort: number; 31 | } 32 | -------------------------------------------------------------------------------- /src/assets/icons/refresh.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/lang/index.ts: -------------------------------------------------------------------------------- 1 | import { createI18n } from "vue-i18n"; 2 | import { useAppStoreHook } from "@/store/modules/app"; 3 | // 本地语言包 4 | import enLocale from "./package/en"; 5 | import zhCnLocale from "./package/zh-cn"; 6 | 7 | const appStore = useAppStoreHook(); 8 | 9 | const messages = { 10 | "zh-cn": { 11 | ...zhCnLocale, 12 | }, 13 | en: { 14 | ...enLocale, 15 | }, 16 | }; 17 | 18 | const i18n = createI18n({ 19 | legacy: false, 20 | locale: appStore.language, 21 | messages: messages, 22 | globalInjection: true, 23 | }); 24 | 25 | export default i18n; 26 | -------------------------------------------------------------------------------- /src/api/pms/attribute/index.ts: -------------------------------------------------------------------------------- 1 | import request from "@/utils/request"; 2 | 3 | /** 4 | * 获取商品属性列表 5 | * 6 | * @param params 7 | */ 8 | export function getAttributeList(params: object) { 9 | return request({ 10 | url: "/mall-pms/api/v1/attributes", 11 | method: "get", 12 | params: params, 13 | }); 14 | } 15 | 16 | /** 17 | * 批量修改商品属性 18 | * 19 | * @param data 20 | */ 21 | export function saveAttributeBatch(data: object) { 22 | return request({ 23 | url: "/mall-pms/api/v1/attributes/batch", 24 | method: "post", 25 | data: data, 26 | }); 27 | } 28 | -------------------------------------------------------------------------------- /src/views/demo/wang-editor.vue: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 19 | -------------------------------------------------------------------------------- /src/assets/icons/shrink.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/views/demo/api-doc.vue: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 28 | -------------------------------------------------------------------------------- /src/assets/icons/ip.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/assets/icons/close_left.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/styles/index.scss: -------------------------------------------------------------------------------- 1 | @use "./reset"; 2 | 3 | .app-container { 4 | padding: 10px; 5 | } 6 | 7 | .search-container { 8 | padding: 18px 0 0 10px; 9 | margin-bottom: 10px; 10 | background-color: var(--el-bg-color-overlay); 11 | border: 1px solid var(--el-border-color-light); 12 | border-radius: 4px; 13 | box-shadow: var(--el-box-shadow-light); 14 | } 15 | 16 | .table-container > .el-card__header { 17 | padding: calc(var(--el-card-padding) - 8px) var(--el-card-padding); 18 | } 19 | 20 | .link-type, 21 | .link-type:focus { 22 | color: #337ab7; 23 | cursor: pointer; 24 | 25 | &:hover { 26 | color: rgb(32 160 255); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/api/sms/advert/types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 广告查询参数类型 3 | */ 4 | export interface AdvertQuery extends PageQuery { 5 | keywords: string; 6 | } 7 | 8 | /** 9 | * 广告分页列表项 10 | */ 11 | export interface Advert { 12 | id: string; 13 | name: string; 14 | logoUrl: string; 15 | sort: number; 16 | } 17 | 18 | /** 19 | * 广告分页项类型 20 | */ 21 | export type AdvertPageResult = PageResult; 22 | 23 | /** 24 | * 广告表单类型 25 | */ 26 | export interface AdvertForm { 27 | id?: number; 28 | title: string; 29 | picUrl: string; 30 | beginTime: string; 31 | endTime: string; 32 | status: number; 33 | sort: number; 34 | url: string; 35 | remark: string; 36 | } 37 | -------------------------------------------------------------------------------- /src/assets/icons/theme.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/close_right.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/api/oms/order/index.ts: -------------------------------------------------------------------------------- 1 | import request from "@/utils/request"; 2 | import { AxiosPromise } from "axios"; 3 | import { OrderPageResult, OrderQuery } from "./types"; 4 | 5 | /** 6 | * 获取订单分页列表 7 | * 8 | * @param queryParams 9 | */ 10 | export function getOrderPage( 11 | queryParams: OrderQuery 12 | ): AxiosPromise { 13 | return request({ 14 | url: "/mall-oms/api/v1/orders", 15 | method: "get", 16 | params: queryParams, 17 | }); 18 | } 19 | 20 | /** 21 | * 获取订单详情 22 | * 23 | * @param orderId 24 | */ 25 | export function getOrderDetail(orderId: number) { 26 | return request({ 27 | url: "/mall-oms/api/v1/orders/" + orderId, 28 | method: "get", 29 | }); 30 | } 31 | -------------------------------------------------------------------------------- /src/settings.ts: -------------------------------------------------------------------------------- 1 | import { SizeEnum } from "./enums/SizeEnum"; 2 | import { LayoutEnum } from "./enums/LayoutEnum"; 3 | import { ThemeEnum } from "./enums/ThemeEnum"; 4 | import { LanguageEnum } from "./enums/LanguageEnum"; 5 | 6 | const { pkg } = __APP_INFO__; 7 | 8 | const defaultSettings: AppSettings = { 9 | title: pkg.name, 10 | version: pkg.version, 11 | showSettings: true, 12 | tagsView: true, 13 | fixedHeader: true, 14 | sidebarLogo: true, 15 | layout: LayoutEnum.LEFT, 16 | theme: ThemeEnum.LIGHT, 17 | size: SizeEnum.DEFAULT, 18 | language: LanguageEnum.ZH_CN, 19 | themeColor: "#409EFF", 20 | watermarkEnabled: false, 21 | watermarkContent: pkg.name, 22 | }; 23 | 24 | export default defaultSettings; 25 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from "vue"; 2 | import App from "./App.vue"; 3 | import router from "@/router"; 4 | import { setupStore } from "@/store"; 5 | import { setupDirective } from "@/directive"; 6 | import { setupElIcons, setupI18n, setupPermission } from "@/plugins"; 7 | 8 | // 本地SVG图标 9 | import "virtual:svg-icons-register"; 10 | 11 | // 样式 12 | import "element-plus/theme-chalk/dark/css-vars.css"; 13 | import "@/styles/index.scss"; 14 | import "uno.css"; 15 | import "animate.css"; 16 | 17 | const app = createApp(App); 18 | // 全局注册 自定义指令(directive) 19 | setupDirective(app); 20 | // 全局注册 状态管理(store) 21 | setupStore(app); 22 | // 全局注册Element-plus图标 23 | setupElIcons(app); 24 | // 国际化 25 | setupI18n(app); 26 | // 注册动态路由 27 | setupPermission(); 28 | app.use(router).mount("#app"); 29 | -------------------------------------------------------------------------------- /src/assets/icons/nested.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/api/file/index.ts: -------------------------------------------------------------------------------- 1 | import request from "@/utils/request"; 2 | import { AxiosPromise } from "axios"; 3 | import { FileInfo } from "./types"; 4 | 5 | /** 6 | * 上传文件 7 | * 8 | * @param file 9 | */ 10 | export function uploadFileApi(file: File): AxiosPromise { 11 | const formData = new FormData(); 12 | formData.append("file", file); 13 | return request({ 14 | url: "/youlai-system/api/v1/files", 15 | method: "post", 16 | data: formData, 17 | headers: { 18 | "Content-Type": "multipart/form-data", 19 | }, 20 | }); 21 | } 22 | 23 | /** 24 | * 删除文件 25 | * 26 | * @param filePath 文件完整路径 27 | */ 28 | export function deleteFileApi(filePath?: string) { 29 | return request({ 30 | url: "/youlai-system/api/v1/files", 31 | method: "delete", 32 | params: { filePath: filePath }, 33 | }); 34 | } 35 | -------------------------------------------------------------------------------- /src/components/Hamburger/index.vue: -------------------------------------------------------------------------------- 1 | 13 | 28 | 29 | 40 | -------------------------------------------------------------------------------- /src/components/AppLink/index.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 41 | -------------------------------------------------------------------------------- /src/assets/icons/monitor.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/document.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/assets/icons/eye.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/layout/components/Settings/components/ThemeColorPicker.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 36 | 37 | 42 | -------------------------------------------------------------------------------- /src/assets/icons/user.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/api/auth/types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 登录请求参数 3 | */ 4 | export interface LoginData { 5 | /** 6 | * 用户名 7 | */ 8 | username: string; 9 | /** 10 | * 密码 11 | */ 12 | password: string; 13 | /** 14 | * 授权类型 15 | */ 16 | grant_type?: string; 17 | /** 18 | * 验证码Code 19 | */ 20 | captchaCode?: string; 21 | /** 22 | * 验证码唯一标识(UUID) 23 | */ 24 | captchaId?: string; 25 | } 26 | 27 | /** 28 | * 登录响应 29 | */ 30 | export interface LoginResult { 31 | /** 32 | * 访问token 33 | */ 34 | access_token?: string; 35 | /** 36 | * 过期时间(单位:毫秒) 37 | */ 38 | expires?: number; 39 | /** 40 | * 刷新token 41 | */ 42 | refresh_token?: string; 43 | /** 44 | * token 类型 45 | */ 46 | token_type?: string; 47 | } 48 | 49 | /** 50 | * 验证码响应 51 | */ 52 | export interface CaptchaResult { 53 | /** 54 | * 验证码缓存key 55 | */ 56 | captchaId: string; 57 | /** 58 | * 验证码图片Base64字符串 59 | */ 60 | captchaBase64: string; 61 | } 62 | -------------------------------------------------------------------------------- /src/assets/icons/uv.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/dict_item.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 12 | 13 | -------------------------------------------------------------------------------- /src/types/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | declare module "*.vue" { 4 | import { DefineComponent } from "vue"; 5 | // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types 6 | const component: DefineComponent<{}, {}, any>; 7 | export default component; 8 | } 9 | 10 | interface ImportMetaEnv { 11 | /** 应用端口 */ 12 | VITE_APP_PORT: number; 13 | /** API 基础路径(代理前缀) */ 14 | VITE_APP_BASE_API: string; 15 | /** API 地址 */ 16 | VITE_APP_API_URL: string; 17 | /** 是否开启 Mock 服务 */ 18 | } 19 | 20 | interface ImportMeta { 21 | readonly env: ImportMetaEnv; 22 | } 23 | 24 | /** 25 | * 平台的名称、版本、运行所需的`node`版本、依赖、构建时间的类型提示 26 | */ 27 | declare const __APP_INFO__: { 28 | pkg: { 29 | name: string; 30 | version: string; 31 | engines: { 32 | node: string; 33 | }; 34 | dependencies: Record; 35 | devDependencies: Record; 36 | }; 37 | buildTimestamp: number; 38 | }; 39 | -------------------------------------------------------------------------------- /src/assets/icons/link.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/assets/icons/advert.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/SizeSelect/index.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 36 | -------------------------------------------------------------------------------- /src/assets/icons/download.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/layout/components/Sidebar/components/SidebarMenuItemTitle.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 24 | 25 | 43 | -------------------------------------------------------------------------------- /src/api/system/dept/types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 部门查询参数 3 | */ 4 | export interface DeptQuery { 5 | keywords?: string; 6 | status?: number; 7 | } 8 | 9 | /** 10 | * 部门类型 11 | */ 12 | export interface DeptVO { 13 | /** 14 | * 子部门 15 | */ 16 | children?: DeptVO[]; 17 | /** 18 | * 创建时间 19 | */ 20 | createTime?: Date; 21 | /** 22 | * 部门ID 23 | */ 24 | id?: number; 25 | /** 26 | * 部门名称 27 | */ 28 | name?: string; 29 | /** 30 | * 父部门ID 31 | */ 32 | parentId?: number; 33 | /** 34 | * 排序 35 | */ 36 | sort?: number; 37 | /** 38 | * 状态(1:启用;0:禁用) 39 | */ 40 | status?: number; 41 | /** 42 | * 修改时间 43 | */ 44 | updateTime?: Date; 45 | } 46 | 47 | /** 48 | * 部门表单类型 49 | */ 50 | export interface DeptForm { 51 | /** 52 | * 部门ID(新增不填) 53 | */ 54 | id?: number; 55 | /** 56 | * 部门名称 57 | */ 58 | name?: string; 59 | /** 60 | * 父部门ID 61 | */ 62 | parentId: number; 63 | /** 64 | * 排序 65 | */ 66 | sort?: number; 67 | /** 68 | * 状态(1:启用;0:禁用) 69 | */ 70 | status?: number; 71 | } 72 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "useDefineForClassFields": true, 5 | "module": "esnext", 6 | "moduleResolution": "node", 7 | "strict": true, 8 | "noLib": false, 9 | "sourceMap": true, 10 | "resolveJsonModule": true, 11 | "esModuleInterop": true, 12 | "lib": ["esnext", "dom"], 13 | "baseUrl": ".", 14 | "allowJs": true, 15 | "paths": { 16 | "@/*": ["src/*"] 17 | }, 18 | "types": ["vite/client", "unplugin-icons/types/vue", "element-plus/global"], 19 | "skipLibCheck": true /* Skip type checking all .d.ts files. */, 20 | "allowSyntheticDefaultImports": true /* 允许默认导入 */, 21 | "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, 22 | 23 | "jsx": "preserve", 24 | "jsxFactory": "h", 25 | "jsxFragmentFactory": "Fragment" 26 | }, 27 | "include": [ 28 | "src/**/*.ts", 29 | "src/**/*.vue", 30 | "src/types/**/*.d.ts", 31 | "mock/**/*.ts", 32 | "vite.config.ts" 33 | ], 34 | "exclude": ["node_modules", "dist", "**/*.js"] 35 | } 36 | -------------------------------------------------------------------------------- /src/assets/icons/visit.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/components/SvgIcon/index.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 34 | 35 | 46 | -------------------------------------------------------------------------------- /src/components/LangSelect/index.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021-present 有来开源组织 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/views/demo/upload.vue: -------------------------------------------------------------------------------- 1 | 2 | 15 | 35 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 37 | -------------------------------------------------------------------------------- /src/api/system/role/types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 角色查询参数 3 | */ 4 | export interface RoleQuery extends PageQuery { 5 | keywords?: string; 6 | } 7 | 8 | /** 9 | * 角色分页对象 10 | */ 11 | export interface RolePageVO { 12 | /** 13 | * 角色编码 14 | */ 15 | code?: string; 16 | 17 | /** 18 | * 角色ID 19 | */ 20 | id?: number; 21 | /** 22 | * 角色名称 23 | */ 24 | name?: string; 25 | /** 26 | * 排序 27 | */ 28 | sort?: number; 29 | /** 30 | * 角色状态 31 | */ 32 | status?: number; 33 | /** 34 | * 创建时间 35 | */ 36 | createTime?: Date; 37 | /** 38 | * 修改时间 39 | */ 40 | updateTime?: Date; 41 | } 42 | 43 | /** 44 | * 角色分页 45 | */ 46 | export type RolePageResult = PageResult; 47 | 48 | /** 49 | * 角色表单对象 50 | */ 51 | export interface RoleForm { 52 | /** 53 | * 角色ID 54 | */ 55 | id?: number; 56 | 57 | /** 58 | * 角色编码 59 | */ 60 | code: string; 61 | /** 62 | * 数据权限 63 | */ 64 | dataScope?: number; 65 | 66 | /** 67 | * 角色名称 68 | */ 69 | name: string; 70 | /** 71 | * 排序 72 | */ 73 | sort?: number; 74 | /** 75 | * 角色状态(1-正常;0-停用) 76 | */ 77 | status?: number; 78 | } 79 | -------------------------------------------------------------------------------- /src/assets/icons/skill.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/api/ums/member/index.ts: -------------------------------------------------------------------------------- 1 | import request from "@/utils/request"; 2 | import { AxiosPromise } from "axios"; 3 | import { MemberQuery, MemberPageResult } from "./types"; 4 | 5 | /** 6 | * 获取会员分页列表 7 | * 8 | * @param queryParams 9 | */ 10 | export function getMemberPage( 11 | queryParams: MemberQuery 12 | ): AxiosPromise { 13 | return request({ 14 | url: "/mall-ums/api/v1/members", 15 | method: "get", 16 | params: queryParams, 17 | }); 18 | } 19 | 20 | /** 21 | * 获取会员详情 22 | * 23 | * @param id 24 | */ 25 | export function getMemberDetail(id: number) { 26 | return request({ 27 | url: "/mall-ums/api/v1/members/" + id, 28 | method: "get", 29 | }); 30 | } 31 | 32 | /** 33 | * 添加会员 34 | * 35 | * @param data 36 | */ 37 | export function addMember(data: object) { 38 | return request({ 39 | url: "/mall-ums/api/v1/members", 40 | method: "post", 41 | data: data, 42 | }); 43 | } 44 | 45 | /** 46 | * 添加会员 47 | * 48 | * @param id 49 | * @param data 50 | */ 51 | export function updateMember(id: number, data: object) { 52 | return request({ 53 | url: "/mall-ums/api/v1/members/" + id, 54 | method: "put", 55 | data: data, 56 | }); 57 | } 58 | -------------------------------------------------------------------------------- /uno.config.ts: -------------------------------------------------------------------------------- 1 | // uno.config.ts 2 | import { 3 | defineConfig, 4 | presetAttributify, 5 | presetIcons, 6 | presetTypography, 7 | presetUno, 8 | presetWebFonts, 9 | transformerDirectives, 10 | transformerVariantGroup, 11 | } from "unocss"; 12 | 13 | export default defineConfig({ 14 | shortcuts: { 15 | "flex-center": "flex justify-center items-center", 16 | "flex-x-center": "flex justify-center", 17 | "flex-y-center": "flex items-center", 18 | "wh-full": "w-full h-full", 19 | "flex-x-between": "flex items-center justify-between", 20 | "flex-x-end": "flex items-center justify-end", 21 | "absolute-lt": "absolute left-0 top-0", 22 | "absolute-rt": "absolute right-0 top-0 ", 23 | "fixed-lt": "fixed left-0 top-0", 24 | }, 25 | theme: { 26 | colors: { 27 | primary: "var(--el-color-primary)", 28 | primary_dark: "var(--el-color-primary-light-5)", 29 | }, 30 | }, 31 | presets: [ 32 | presetUno(), 33 | presetAttributify(), 34 | presetIcons(), 35 | presetTypography(), 36 | presetWebFonts({ 37 | fonts: { 38 | // ... 39 | }, 40 | }), 41 | ], 42 | transformers: [transformerDirectives(), transformerVariantGroup()], 43 | }); 44 | -------------------------------------------------------------------------------- /src/layout/components/Sidebar/index.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 30 | 31 | 38 | -------------------------------------------------------------------------------- /src/assets/icons/perm.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/assets/icons/multi_level.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/assets/icons/eye-open.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/api/ums/member/types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 会员查询参数类型声明 3 | */ 4 | export interface MemberQuery extends PageQuery { 5 | nickName?: string; 6 | } 7 | 8 | /** 9 | * 会员分页列表项声明 10 | */ 11 | export interface Member { 12 | id: string; 13 | gender: number; 14 | nickName: string; 15 | mobile: string; 16 | birthday?: any; 17 | avatarUrl: string; 18 | openid: string; 19 | sessionKey?: any; 20 | city: string; 21 | country: string; 22 | language: string; 23 | province: string; 24 | status: number; 25 | balance: string; 26 | deleted: number; 27 | point: number; 28 | addressList: Address[]; 29 | } 30 | 31 | export interface Address { 32 | id: string; 33 | memberId: string; 34 | consigneeName: string; 35 | consigneeMobile: string; 36 | province: string; 37 | city: string; 38 | area: string; 39 | detailAddress: string; 40 | zipCode?: any; 41 | defaulted: number; 42 | } 43 | 44 | /** 45 | * 会员分页项类型声明 46 | */ 47 | export type MemberPageResult = PageResult; 48 | 49 | /** 50 | * 会员表单类型声明 51 | */ 52 | export interface MemberForm { 53 | id: number | undefined; 54 | title: string; 55 | picUrl: string; 56 | beginTime: string; 57 | endTime: string; 58 | status: number; 59 | sort: number; 60 | url: string; 61 | remark: string; 62 | } 63 | -------------------------------------------------------------------------------- /src/assets/icons/menu.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/api/pms/goods/types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 商品查询参数类型声明 3 | */ 4 | export interface GoodsQuery extends PageQuery { 5 | name?: string; 6 | categoryId?: number; 7 | } 8 | 9 | /** 10 | * 商品列表项类型声明 11 | */ 12 | export interface Goods { 13 | id: string; 14 | name: string; 15 | categoryId?: any; 16 | brandId?: any; 17 | originPrice: string; 18 | price: string; 19 | sales: number; 20 | picUrl?: any; 21 | album?: any; 22 | unit?: any; 23 | description: string; 24 | detail: string; 25 | status?: any; 26 | categoryName: string; 27 | brandName: string; 28 | skuList: Sku[]; 29 | } 30 | 31 | /** 32 | * 商品规格项类型声明 33 | */ 34 | export interface Sku { 35 | id: string; 36 | skuSn?: any; 37 | name: string; 38 | spuId?: any; 39 | specIds: string; 40 | price: string; 41 | stock: number; 42 | lockedstock?: any; 43 | picUrl?: any; 44 | } 45 | 46 | /** 47 | * 商品分页项类型声明 48 | */ 49 | export type GoodsPageResult = PageResult; 50 | 51 | /** 52 | * 商品表单数据类型声明 53 | */ 54 | export interface GoodsDetail { 55 | id?: string; 56 | name?: string; 57 | categoryId?: string; 58 | brandId?: string; 59 | originPrice?: number; 60 | price?: number; 61 | picUrl?: string; 62 | album: string[]; 63 | description?: string; 64 | detail?: string; 65 | attrList: any[]; 66 | specList: any[]; 67 | skuList: any[]; 68 | } 69 | -------------------------------------------------------------------------------- /src/api/auth/index.ts: -------------------------------------------------------------------------------- 1 | import request from "@/utils/request"; 2 | import { AxiosPromise } from "axios"; 3 | import { CaptchaResult, LoginData, LoginResult } from "./types"; 4 | 5 | /** 6 | * 登录API 7 | * 8 | * @param data {LoginData} 9 | * @returns 10 | */ 11 | export function loginApi(data: LoginData): AxiosPromise { 12 | const formData = new FormData(); 13 | formData.append("username", data.username); 14 | formData.append("password", data.password); 15 | formData.append("captchaId", data.captchaId as string); 16 | formData.append("captchaCode", data.captchaCode as string); 17 | formData.append("grant_type", "password"); 18 | return request({ 19 | url: "/youlai-auth/oauth2/token", 20 | method: "post", 21 | data: formData, 22 | headers: { 23 | "Content-Type": "multipart/form-data", 24 | Authorization: "Basic bWFsbC1hZG1pbjoxMjM0NTY=", // 客户端信息Base64明文:mall-admin:123456 25 | }, 26 | }); 27 | } 28 | 29 | /** 30 | * 获取验证码 31 | */ 32 | export function getCaptchaApi(): AxiosPromise { 33 | return request({ 34 | url: "/youlai-auth/api/v1/auth/captcha", 35 | method: "get", 36 | }); 37 | } 38 | 39 | /** 40 | * 注销API 41 | */ 42 | export function logoutApi() { 43 | return request({ 44 | url: "/youlai-system/api/v1/users/logout", 45 | method: "delete", 46 | }); 47 | } 48 | -------------------------------------------------------------------------------- /src/assets/icons/system.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Check if an element has a class 3 | * @param {HTMLElement} ele 4 | * @param {string} cls 5 | * @returns {boolean} 6 | */ 7 | export function hasClass(ele: HTMLElement, cls: string) { 8 | return !!ele.className.match(new RegExp("(\\s|^)" + cls + "(\\s|$)")); 9 | } 10 | 11 | /** 12 | * Add class to element 13 | * @param {HTMLElement} ele 14 | * @param {string} cls 15 | */ 16 | export function addClass(ele: HTMLElement, cls: string) { 17 | if (!hasClass(ele, cls)) ele.className += " " + cls; 18 | } 19 | 20 | /** 21 | * Remove class from element 22 | * @param {HTMLElement} ele 23 | * @param {string} cls 24 | */ 25 | export function removeClass(ele: HTMLElement, cls: string) { 26 | if (hasClass(ele, cls)) { 27 | const reg = new RegExp("(\\s|^)" + cls + "(\\s|$)"); 28 | ele.className = ele.className.replace(reg, " "); 29 | } 30 | } 31 | 32 | /** 33 | * 判断是否为外部链接 34 | * 35 | * @param {string} path 36 | * @returns {Boolean} 37 | */ 38 | export function isExternal(path: string) { 39 | const isExternal = /^(https?:|http?:|mailto:|tel:)/.test(path); 40 | return isExternal; 41 | } 42 | 43 | /** 44 | * 设置Style属性 45 | * 46 | * @param propName 47 | * @param value 48 | */ 49 | export function setStyleProperty(propName: string, value: string) { 50 | document.documentElement.style.setProperty(propName, value); 51 | } 52 | -------------------------------------------------------------------------------- /.stylelintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // 继承推荐规范配置 3 | extends: [ 4 | "stylelint-config-standard", 5 | "stylelint-config-recommended-scss", 6 | "stylelint-config-recommended-vue/scss", 7 | "stylelint-config-html/vue", 8 | "stylelint-config-recess-order", 9 | ], 10 | // 指定不同文件对应的解析器 11 | overrides: [ 12 | { 13 | files: ["**/*.{vue,html}"], 14 | customSyntax: "postcss-html", 15 | }, 16 | { 17 | files: ["**/*.{css,scss}"], 18 | customSyntax: "postcss-scss", 19 | }, 20 | ], 21 | // 自定义规则 22 | rules: { 23 | "import-notation": "string", // 指定导入CSS文件的方式("string"|"url") 24 | "selector-class-pattern": null, // 选择器类名命名规则 25 | "custom-property-pattern": null, // 自定义属性命名规则 26 | "keyframes-name-pattern": null, // 动画帧节点样式命名规则 27 | "no-descending-specificity": null, // 允许无降序特异性 28 | "no-empty-source": null, // 允许空样式 29 | // 允许 global 、export 、deep伪类 30 | "selector-pseudo-class-no-unknown": [ 31 | true, 32 | { 33 | ignorePseudoClasses: ["global", "export", "deep"], 34 | }, 35 | ], 36 | // 允许未知属性 37 | "property-no-unknown": [ 38 | true, 39 | { 40 | ignoreProperties: [], 41 | }, 42 | ], 43 | // 允许未知规则 44 | "at-rule-no-unknown": [ 45 | true, 46 | { 47 | ignoreAtRules: ["apply", "use"], 48 | }, 49 | ], 50 | }, 51 | }; 52 | -------------------------------------------------------------------------------- /src/styles/variables.scss: -------------------------------------------------------------------------------- 1 | /** 全局SCSS变量 */ 2 | 3 | :root { 4 | --menu-background: #304156; 5 | --menu-text: #bfcbd9; 6 | --menu-active-text: #409eff; 7 | --menu-hover: #263445; 8 | --sub-menu-background: #1f2d3d; 9 | --sub-menu-active-text: #f4f4f5; 10 | --sub-menu-hover: #001528; 11 | --sidebar-logo-background: #2d3748; 12 | } 13 | 14 | /** 暗黑主题 */ 15 | html.dark { 16 | --menu-background: var(--el-bg-color-overlay); 17 | --menu-text: #fff; 18 | --menu-active-text: var(--el-menu-active-color); 19 | --menu-hover: rgb(0 0 0 / 20%); 20 | --sub-menu-background: var(--el-menu-bg-color); 21 | --sub-menu-active-text: var(--el-menu-active-color); 22 | --sub-menu-hover: rgb(0 0 0 / 20%); 23 | --sidebar-logo-background: rgb(0 0 0 / 20%); 24 | } 25 | 26 | $menu-background: var(--menu-background); // 菜单背景色 27 | $menu-text: var(--menu-text); // 菜单文字颜色 28 | $menu-active-text: var(--menu-active-text); // 菜单激活文字颜色 29 | $menu-hover: var(--menu-hover); // 菜单悬停背景色 30 | $sub-menu-background: var(--sub-menu-background); // 子菜单背景色 31 | $sub-menu-active-text: var(--sub-menu-active-text); // 子菜单激活文字颜色 32 | $sub-menu-hover: var(--sub-menu-hover); // 子菜单悬停背景色 33 | $sidebar-logo-background: var(--sidebar-logo-background); // 侧边栏 Logo 背景色 34 | 35 | $sidebar-width: 210px; // 侧边栏宽度 36 | $sidebar-width-collapsed: 54px; // 侧边栏收缩宽度 37 | $navbar-height: 50px; // 导航栏高度 38 | $tags-view-height: 34px; // TagsView 高度 39 | -------------------------------------------------------------------------------- /src/styles/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 | a { 37 | color: inherit; 38 | text-decoration: inherit; 39 | } 40 | 41 | img, 42 | svg { 43 | display: inline-block; 44 | } 45 | 46 | svg { 47 | // 因icon大小被设置为和字体大小一致,而span等标签的下边缘会和字体的基线对齐,故需设置一个往下的偏移比例,来纠正视觉上的未对齐效果 48 | vertical-align: -0.15em; 49 | } 50 | 51 | ul, 52 | li { 53 | padding: 0; 54 | margin: 0; 55 | list-style: none; 56 | } 57 | 58 | *, 59 | *::before, 60 | *::after { 61 | box-sizing: inherit; 62 | } 63 | 64 | a, 65 | a:focus, 66 | a:hover { 67 | color: inherit; 68 | text-decoration: none; 69 | cursor: pointer; 70 | } 71 | 72 | a:focus, 73 | a:active, 74 | div:focus { 75 | outline: none; 76 | } 77 | -------------------------------------------------------------------------------- /src/api/pms/goods/index.ts: -------------------------------------------------------------------------------- 1 | import { GoodsDetail, GoodsPageResult, GoodsQuery } from "./types"; 2 | import request from "@/utils/request"; 3 | import { AxiosPromise } from "axios"; 4 | 5 | /** 6 | * 获取商品分页列表 7 | * 8 | * @param queryParams 9 | */ 10 | export function getSpuPage( 11 | queryParams: GoodsQuery 12 | ): AxiosPromise { 13 | return request({ 14 | url: "/mall-pms/api/v1/spu/page", 15 | method: "get", 16 | params: queryParams, 17 | }); 18 | } 19 | 20 | /** 21 | * 获取商品详情 22 | * 23 | * @param id 24 | */ 25 | export function getSpuDetail(id: string): AxiosPromise { 26 | return request({ 27 | url: "/mall-pms/api/v1/spu/" + id + "/detail", 28 | method: "get", 29 | }); 30 | } 31 | 32 | /** 33 | * 添加商品 34 | * 35 | * @param data 36 | */ 37 | export function addSpu(data: object) { 38 | return request({ 39 | url: "/mall-pms/api/v1/spu", 40 | method: "post", 41 | data: data, 42 | }); 43 | } 44 | 45 | /** 46 | * 修改商品 47 | * 48 | * @param id 49 | * @param data 50 | */ 51 | export function updateSpu(id: number, data: object) { 52 | return request({ 53 | url: "/mall-pms/api/v1/spu/" + id, 54 | method: "put", 55 | data: data, 56 | }); 57 | } 58 | 59 | /** 60 | * 删除商品 61 | * 62 | * @param ids 63 | */ 64 | export function deleteSpu(ids: string) { 65 | return request({ 66 | url: "/mall-pms/api/v1/spu/" + ids, 67 | method: "delete", 68 | }); 69 | } 70 | -------------------------------------------------------------------------------- /src/assets/icons/role.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/api/sms/advert/index.ts: -------------------------------------------------------------------------------- 1 | import { AdvertForm, AdvertPageResult, AdvertQuery } from "./types"; 2 | import request from "@/utils/request"; 3 | import { AxiosPromise } from "axios"; 4 | 5 | /** 6 | * 获取广告分页列表 7 | * 8 | * @param queryParams 9 | */ 10 | export function getAdvertPage( 11 | queryParams: AdvertQuery 12 | ): AxiosPromise { 13 | return request({ 14 | url: "/mall-sms/api/v1/adverts/page", 15 | method: "get", 16 | params: queryParams, 17 | }); 18 | } 19 | 20 | /** 21 | * 获取广告详情 22 | * 23 | * @param id 24 | */ 25 | export function getAdvertForm(id: number): AxiosPromise { 26 | return request({ 27 | url: "/mall-sms/api/v1/adverts/" + id, 28 | method: "get", 29 | }); 30 | } 31 | 32 | /** 33 | * 添加广告 34 | * 35 | * @param data 36 | */ 37 | export function addAdvert(data: AdvertForm) { 38 | return request({ 39 | url: "/mall-sms/api/v1/adverts", 40 | method: "post", 41 | data: data, 42 | }); 43 | } 44 | 45 | /** 46 | * 修改广告 47 | * 48 | * @param id 49 | * @param data 50 | */ 51 | export function updateAdvert(id: number, data: AdvertForm) { 52 | return request({ 53 | url: "/mall-sms/api/v1/adverts/" + id, 54 | method: "put", 55 | data: data, 56 | }); 57 | } 58 | 59 | /** 60 | * 删除广告 61 | * 62 | * @param ids 63 | */ 64 | export function deleteAdverts(ids: string) { 65 | return request({ 66 | url: "/mall-sms/api/v1/adverts/" + ids, 67 | method: "delete", 68 | }); 69 | } 70 | -------------------------------------------------------------------------------- /src/api/sms/coupon/index.ts: -------------------------------------------------------------------------------- 1 | import { CouponQuery, CouponPageResult, CouponForm } from "./types"; 2 | import request from "@/utils/request"; 3 | import { AxiosPromise } from "axios"; 4 | 5 | /** 6 | * 获取优惠券分页列表 7 | * 8 | * @param queryParams 9 | */ 10 | export function getCouponPage( 11 | queryParams: CouponQuery 12 | ): AxiosPromise { 13 | return request({ 14 | url: "/mall-sms/api/v1/coupons/page", 15 | method: "get", 16 | params: queryParams, 17 | }); 18 | } 19 | 20 | /** 21 | * 获取优惠券表单数据 22 | * 23 | * @param id 24 | */ 25 | export function getCouponForm(id: number): AxiosPromise { 26 | return request({ 27 | url: "/mall-sms/api/v1/coupons/" + id + "/form_data", 28 | method: "get", 29 | }); 30 | } 31 | 32 | /** 33 | * 添加优惠券 34 | * 35 | * @param data 36 | */ 37 | export function addCoupon(data: CouponForm) { 38 | return request({ 39 | url: "/mall-sms/api/v1/coupons", 40 | method: "post", 41 | data: data, 42 | }); 43 | } 44 | 45 | /** 46 | * 修改优惠券 47 | * 48 | * @param id 49 | * @param data 50 | */ 51 | export function updateCoupon(id: number, data: CouponForm) { 52 | return request({ 53 | url: "/mall-sms/api/v1/coupons/" + id, 54 | method: "put", 55 | data: data, 56 | }); 57 | } 58 | 59 | /** 60 | * 删除优惠券 61 | * 62 | * @param ids 63 | */ 64 | export function deleteCoupons(ids: string) { 65 | return request({ 66 | url: "/mall-sms/api/v1/coupons/" + ids, 67 | method: "delete", 68 | }); 69 | } 70 | -------------------------------------------------------------------------------- /src/assets/icons/message.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/assets/icons/client.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/publish.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/cart.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/assets/icons/todolist.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /.prettierrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // (x)=>{},单个参数箭头函数是否显示小括号。(always:始终显示;avoid:省略括号。默认:always) 3 | arrowParens: "always", 4 | // 开始标签的右尖括号是否跟随在最后一行属性末尾,默认false 5 | bracketSameLine: false, 6 | // 对象字面量的括号之间打印空格 (true - Example: { foo: bar } ; false - Example: {foo:bar}) 7 | bracketSpacing: true, 8 | // 是否格式化一些文件中被嵌入的代码片段的风格(auto|off;默认auto) 9 | embeddedLanguageFormatting: "auto", 10 | // 指定 HTML 文件的空格敏感度 (css|strict|ignore;默认css) 11 | htmlWhitespaceSensitivity: "css", 12 | // 当文件已经被 Prettier 格式化之后,是否会在文件顶部插入一个特殊的 @format 标记,默认false 13 | insertPragma: false, 14 | // 在 JSX 中使用单引号替代双引号,默认false 15 | jsxSingleQuote: false, 16 | // 每行最多字符数量,超出换行(默认80) 17 | printWidth: 80, 18 | // 超出打印宽度 (always | never | preserve ) 19 | proseWrap: "preserve", 20 | // 对象属性是否使用引号(as-needed | consistent | preserve;默认as-needed:对象的属性需要加引号才添加;) 21 | quoteProps: "as-needed", 22 | // 是否只格式化在文件顶部包含特定注释(@prettier| @format)的文件,默认false 23 | requirePragma: false, 24 | // 结尾添加分号 25 | semi: true, 26 | // 使用单引号 (true:单引号;false:双引号) 27 | singleQuote: false, 28 | // 缩进空格数,默认2个空格 29 | tabWidth: 2, 30 | // 元素末尾是否加逗号,默认es5: ES5中的 objects, arrays 等会添加逗号,TypeScript 中的 type 后不加逗号 31 | trailingComma: "es5", 32 | // 指定缩进方式,空格或tab,默认false,即使用空格 33 | useTabs: false, 34 | // vue 文件中是否缩进 -------------------------------------------------------------------------------- /src/assets/icons/project.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/api/system/dept/index.ts: -------------------------------------------------------------------------------- 1 | import request from "@/utils/request"; 2 | import { AxiosPromise } from "axios"; 3 | import { DeptForm, DeptQuery, DeptVO } from "./types"; 4 | 5 | /** 6 | * 部门树形表格 7 | * 8 | * @param queryParams 9 | */ 10 | export function listDepts(queryParams?: DeptQuery): AxiosPromise { 11 | return request({ 12 | url: "/youlai-system/api/v1/dept", 13 | method: "get", 14 | params: queryParams, 15 | }); 16 | } 17 | 18 | /** 19 | * 部门下拉列表 20 | */ 21 | export function getDeptOptions(): AxiosPromise { 22 | return request({ 23 | url: "/youlai-system/api/v1/dept/options", 24 | method: "get", 25 | }); 26 | } 27 | 28 | /** 29 | * 获取部门详情 30 | * 31 | * @param id 32 | */ 33 | export function getDeptForm(id: number): AxiosPromise { 34 | return request({ 35 | url: "/youlai-system/api/v1/dept/" + id + "/form", 36 | method: "get", 37 | }); 38 | } 39 | 40 | /** 41 | * 新增部门 42 | * 43 | * @param data 44 | */ 45 | export function addDept(data: DeptForm) { 46 | return request({ 47 | url: "/youlai-system/api/v1/dept", 48 | method: "post", 49 | data: data, 50 | }); 51 | } 52 | 53 | /** 54 | * 修改部门 55 | * 56 | * @param id 57 | * @param data 58 | */ 59 | export function updateDept(id: number, data: DeptForm) { 60 | return request({ 61 | url: "/youlai-system/api/v1/dept/" + id, 62 | method: "put", 63 | data: data, 64 | }); 65 | } 66 | 67 | /** 68 | * 删除部门 69 | * 70 | * @param ids 71 | */ 72 | export function deleteDept(ids: string) { 73 | return request({ 74 | url: "/youlai-system/api/v1/dept/" + ids, 75 | method: "delete", 76 | }); 77 | } 78 | -------------------------------------------------------------------------------- /src/assets/icons/rate.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/layout/components/Sidebar/components/SidebarLogo.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 31 | 32 | 65 | -------------------------------------------------------------------------------- /src/assets/icons/tree.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/api/pms/brand/index.ts: -------------------------------------------------------------------------------- 1 | import { BrandForm, Brand, BrandPageResult, BrandQuery } from "./types"; 2 | import request from "@/utils/request"; 3 | import { AxiosPromise } from "axios"; 4 | 5 | /** 6 | * 获取品牌分页列表 7 | * 8 | * @param queryParams 9 | */ 10 | export function getBrandPage( 11 | queryParams: BrandQuery 12 | ): AxiosPromise { 13 | return request({ 14 | url: "/mall-pms/api/v1/brands/page", 15 | method: "get", 16 | params: queryParams, 17 | }); 18 | } 19 | 20 | /** 21 | * 获取品牌列表 22 | * 23 | * @param queryParams 24 | */ 25 | export function getBrandList(queryParams?: BrandQuery): AxiosPromise { 26 | return request({ 27 | url: "/mall-pms/api/v1/brands", 28 | method: "get", 29 | params: queryParams, 30 | }); 31 | } 32 | 33 | /** 34 | * 获取品牌详情 35 | * 36 | * @param id 37 | */ 38 | export function getBrandFormDetail(id: number): AxiosPromise { 39 | return request({ 40 | url: "/mall-pms/api/v1/brands/" + id, 41 | method: "get", 42 | }); 43 | } 44 | 45 | /** 46 | * 添加品牌 47 | * 48 | * @param data 49 | */ 50 | export function addBrand(data: BrandForm) { 51 | return request({ 52 | url: "/mall-pms/api/v1/brands", 53 | method: "post", 54 | data: data, 55 | }); 56 | } 57 | 58 | /** 59 | * 修改品牌 60 | * 61 | * @param id 62 | * @param data 63 | */ 64 | export function updateBrand(id: number, data: BrandForm) { 65 | return request({ 66 | url: "/mall-pms/api/v1/brands/" + id, 67 | method: "put", 68 | data: data, 69 | }); 70 | } 71 | 72 | /** 73 | * 删除品牌 74 | * 75 | * @param ids 76 | */ 77 | export function deleteBrands(ids: string) { 78 | return request({ 79 | url: "/mall-pms/api/v1/brands/" + ids, 80 | method: "delete", 81 | }); 82 | } 83 | -------------------------------------------------------------------------------- /src/assets/icons/exit-fullscreen.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/views/system/user/components/dept-tree.vue: -------------------------------------------------------------------------------- 1 | 2 | 22 | 23 | 70 | -------------------------------------------------------------------------------- /src/types/global.d.ts: -------------------------------------------------------------------------------- 1 | declare global { 2 | /** 3 | * 分页查询参数 4 | */ 5 | interface PageQuery { 6 | pageNum: number; 7 | pageSize: number; 8 | } 9 | 10 | /** 11 | * 分页响应对象 12 | */ 13 | interface PageResult { 14 | /** 数据列表 */ 15 | list: T; 16 | /** 总数 */ 17 | total: number; 18 | } 19 | 20 | /** 21 | * 页签对象 22 | */ 23 | interface TagView { 24 | /** 页签名称 */ 25 | name: string; 26 | /** 页签标题 */ 27 | title: string; 28 | /** 页签路由路径 */ 29 | path: string; 30 | /** 页签路由完整路径 */ 31 | fullPath: string; 32 | /** 页签图标 */ 33 | icon?: string; 34 | /** 是否固定页签 */ 35 | affix?: boolean; 36 | /** 是否开启缓存 */ 37 | keepAlive?: boolean; 38 | /** 路由查询参数 */ 39 | query?: any; 40 | } 41 | 42 | /** 43 | * 系统设置 44 | */ 45 | interface AppSettings { 46 | /** 系统标题 */ 47 | title: string; 48 | /** 系统版本 */ 49 | version: string; 50 | /** 是否显示设置 */ 51 | showSettings: boolean; 52 | /** 是否固定头部 */ 53 | fixedHeader: boolean; 54 | /** 是否显示多标签导航 */ 55 | tagsView: boolean; 56 | /** 是否显示侧边栏Logo */ 57 | sidebarLogo: boolean; 58 | /** 导航栏布局(left|top|mix) */ 59 | layout: string; 60 | /** 主题颜色 */ 61 | themeColor: string; 62 | /** 主题模式(dark|light) */ 63 | theme: string; 64 | /** 布局大小(default |large |small) */ 65 | size: string; 66 | /** 语言( zh-cn| en) */ 67 | language: string; 68 | /** 是否开启水印 */ 69 | watermarkEnabled: boolean; 70 | /** 水印内容 */ 71 | watermarkContent: string; 72 | } 73 | 74 | /** 75 | * 组件数据源 76 | */ 77 | interface OptionType { 78 | /** 值 */ 79 | value: string | number; 80 | /** 文本 */ 81 | label: string; 82 | /** 子列表 */ 83 | children?: OptionType[]; 84 | } 85 | } 86 | export {}; 87 | -------------------------------------------------------------------------------- /src/views/pms/category/index.vue: -------------------------------------------------------------------------------- 1 | 32 | 33 | 69 | -------------------------------------------------------------------------------- /src/assets/icons/password.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/api/system/menu/index.ts: -------------------------------------------------------------------------------- 1 | import request from "@/utils/request"; 2 | import { AxiosPromise } from "axios"; 3 | import { MenuQuery, MenuVO, MenuForm } from "./types"; 4 | 5 | /** 6 | * 获取路由列表 7 | */ 8 | export function listRoutes() { 9 | return request({ 10 | url: "/youlai-system/api/v1/menus/routes", 11 | method: "get", 12 | }); 13 | } 14 | 15 | /** 16 | * 获取菜单树形列表 17 | * 18 | * @param queryParams 19 | */ 20 | export function listMenus(queryParams: MenuQuery): AxiosPromise { 21 | return request({ 22 | url: "/youlai-system/api/v1/menus", 23 | method: "get", 24 | params: queryParams, 25 | }); 26 | } 27 | 28 | /** 29 | * 获取菜单下拉树形列表 30 | */ 31 | export function getMenuOptions(): AxiosPromise { 32 | return request({ 33 | url: "/youlai-system/api/v1/menus/options", 34 | method: "get", 35 | }); 36 | } 37 | 38 | /** 39 | * 获取菜单表单数据 40 | * 41 | * @param id 42 | */ 43 | export function getMenuForm(id: number): AxiosPromise { 44 | return request({ 45 | url: "/youlai-system/api/v1/menus/" + id + "/form", 46 | method: "get", 47 | }); 48 | } 49 | 50 | /** 51 | * 添加菜单 52 | * 53 | * @param data 54 | */ 55 | export function addMenu(data: MenuForm) { 56 | return request({ 57 | url: "/youlai-system/api/v1/menus", 58 | method: "post", 59 | data: data, 60 | }); 61 | } 62 | 63 | /** 64 | * 修改菜单 65 | * 66 | * @param id 67 | * @param data 68 | */ 69 | export function updateMenu(id: string, data: MenuForm) { 70 | return request({ 71 | url: "/youlai-system/api/v1/menus/" + id, 72 | method: "put", 73 | data: data, 74 | }); 75 | } 76 | 77 | /** 78 | * 删除菜单 79 | * 80 | * @param id 菜单ID 81 | */ 82 | export function deleteMenu(id: number) { 83 | return request({ 84 | url: "/youlai-system/api/v1/menus/" + id, 85 | method: "delete", 86 | }); 87 | } 88 | -------------------------------------------------------------------------------- /src/layout/components/Sidebar/components/SidebarMenu.vue: -------------------------------------------------------------------------------- 1 | 2 | 22 | 23 | 65 | -------------------------------------------------------------------------------- /src/components/Pagination/index.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 70 | 71 | 80 | -------------------------------------------------------------------------------- /src/views/dashboard/components/PieChart.vue: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 80 | -------------------------------------------------------------------------------- /src/api/pms/category/index.ts: -------------------------------------------------------------------------------- 1 | import request from "@/utils/request"; 2 | import { AxiosPromise } from "axios"; 3 | 4 | /** 5 | * 获取商品分类列表 6 | * 7 | * @param queryParams 8 | */ 9 | export function listCategories(queryParams: object) { 10 | return request({ 11 | url: "/mall-pms/api/v1/categories", 12 | method: "get", 13 | params: queryParams, 14 | }); 15 | } 16 | 17 | /** 18 | * 获取商品分类级联器树形列表 19 | * 20 | * @param queryParams 21 | */ 22 | export function getCategoryOptions(): AxiosPromise { 23 | return request({ 24 | url: "/mall-pms/api/v1/categories/options", 25 | method: "get", 26 | }); 27 | } 28 | 29 | /** 30 | * 获取商品分类详情 31 | * 32 | * @param id 33 | */ 34 | export function getCategoryDetail(id: number) { 35 | return request({ 36 | url: "/mall-pms/api/v1/categories/" + id, 37 | method: "get", 38 | }); 39 | } 40 | 41 | /** 42 | * 添加商品分类 43 | * 44 | * @param data 45 | */ 46 | export function addCategory(data: object) { 47 | return request({ 48 | url: "/mall-pms/api/v1/categories", 49 | method: "post", 50 | data: data, 51 | }); 52 | } 53 | 54 | /** 55 | * 修改商品分类 56 | * 57 | * @param id 58 | * @param data 59 | */ 60 | export function updateCategory(id: number, data: object) { 61 | return request({ 62 | url: "/mall-pms/api/v1/categories/" + id, 63 | method: "put", 64 | data: data, 65 | }); 66 | } 67 | 68 | /** 69 | * 删除商品分类 70 | * 71 | * @param ids 72 | */ 73 | export function deleteCategories(ids: string) { 74 | return request({ 75 | url: "/mall-pms/api/v1/categories/" + ids, 76 | method: "delete", 77 | }); 78 | } 79 | 80 | /** 81 | * 选择性修改商品分类 82 | * 83 | * @param id 84 | * @param data 85 | */ 86 | export function updateCategoryPart(id: number, data: object) { 87 | return request({ 88 | url: "/mall-pms/api/v1/categories/" + id, 89 | method: "patch", 90 | data: data, 91 | }); 92 | } 93 | -------------------------------------------------------------------------------- /src/components/Upload/SingleUpload.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 51 | 52 | 78 | -------------------------------------------------------------------------------- /src/assets/icons/goods-list.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/api/system/user/types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 登录用户信息 3 | */ 4 | export interface UserInfo { 5 | userId?: number; 6 | username?: string; 7 | nickname?: string; 8 | avatar?: string; 9 | roles: string[]; 10 | perms: string[]; 11 | } 12 | 13 | /** 14 | * 用户查询对象类型 15 | */ 16 | export interface UserQuery extends PageQuery { 17 | keywords?: string; 18 | status?: number; 19 | deptId?: number; 20 | } 21 | 22 | /** 23 | * 用户分页对象 24 | */ 25 | export interface UserPageVO { 26 | /** 27 | * 用户头像地址 28 | */ 29 | avatar?: string; 30 | /** 31 | * 创建时间 32 | */ 33 | createTime?: Date; 34 | /** 35 | * 部门名称 36 | */ 37 | deptName?: string; 38 | /** 39 | * 用户邮箱 40 | */ 41 | email?: string; 42 | /** 43 | * 性别 44 | */ 45 | genderLabel?: string; 46 | /** 47 | * 用户ID 48 | */ 49 | id?: number; 50 | /** 51 | * 手机号 52 | */ 53 | mobile?: string; 54 | /** 55 | * 用户昵称 56 | */ 57 | nickname?: string; 58 | /** 59 | * 角色名称,多个使用英文逗号(,)分割 60 | */ 61 | roleNames?: string; 62 | /** 63 | * 用户状态(1:启用;0:禁用) 64 | */ 65 | status?: number; 66 | /** 67 | * 用户名 68 | */ 69 | username?: string; 70 | } 71 | 72 | /** 73 | * 用户表单类型 74 | */ 75 | export interface UserForm { 76 | /** 77 | * 用户头像 78 | */ 79 | avatar?: string; 80 | /** 81 | * 部门ID 82 | */ 83 | deptId?: number; 84 | /** 85 | * 邮箱 86 | */ 87 | email?: string; 88 | /** 89 | * 性别 90 | */ 91 | gender?: number; 92 | /** 93 | * 用户ID 94 | */ 95 | id?: number; 96 | mobile?: string; 97 | /** 98 | * 昵称 99 | */ 100 | nickname?: string; 101 | /** 102 | * 角色ID集合 103 | */ 104 | roleIds?: number[]; 105 | /** 106 | * 用户状态(1:正常;0:禁用) 107 | */ 108 | status?: number; 109 | /** 110 | * 用户名 111 | */ 112 | username?: string; 113 | } 114 | -------------------------------------------------------------------------------- /src/utils/request.ts: -------------------------------------------------------------------------------- 1 | import axios, { InternalAxiosRequestConfig, AxiosResponse } from "axios"; 2 | import { useUserStoreHook } from "@/store/modules/user"; 3 | 4 | // 创建 axios 实例 5 | const service = axios.create({ 6 | baseURL: import.meta.env.VITE_APP_BASE_API, 7 | timeout: 50000, 8 | headers: { "Content-Type": "application/json;charset=utf-8" }, 9 | }); 10 | 11 | // 请求拦截器 12 | service.interceptors.request.use( 13 | (config: InternalAxiosRequestConfig) => { 14 | const accessToken = localStorage.getItem("accessToken"); 15 | 16 | if (accessToken) { 17 | config.headers.Authorization = accessToken; 18 | } 19 | return config; 20 | }, 21 | (error: any) => { 22 | return Promise.reject(error); 23 | } 24 | ); 25 | 26 | // 响应拦截器 27 | service.interceptors.response.use( 28 | (response: AxiosResponse) => { 29 | const { code, msg } = response.data; 30 | if (code === "00000") { 31 | return response.data; 32 | } 33 | // 响应数据为二进制流处理(Excel导出) 34 | if (response.data instanceof ArrayBuffer) { 35 | return response; 36 | } 37 | 38 | ElMessage.error(msg || "系统出错"); 39 | return Promise.reject(new Error(msg || "Error")); 40 | }, 41 | (error: any) => { 42 | if (error.response.data) { 43 | const { code, msg } = error.response.data; 44 | // token 过期,重新登录 45 | if (code === "A0230") { 46 | ElMessageBox.confirm("当前页面已失效,请重新登录", "提示", { 47 | confirmButtonText: "确定", 48 | cancelButtonText: "取消", 49 | type: "warning", 50 | }).then(() => { 51 | const userStore = useUserStoreHook(); 52 | userStore.resetToken().then(() => { 53 | location.reload(); 54 | }); 55 | }); 56 | } else { 57 | ElMessage.error(msg || "系统出错"); 58 | } 59 | } 60 | return Promise.reject(error.message); 61 | } 62 | ); 63 | 64 | // 导出 axios 实例 65 | export default service; 66 | -------------------------------------------------------------------------------- /src/components/WangEditor/index.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 73 | 74 | 75 | -------------------------------------------------------------------------------- /src/components/Dictionary/index.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 74 | -------------------------------------------------------------------------------- /src/router/index.ts: -------------------------------------------------------------------------------- 1 | import { createRouter, createWebHashHistory, RouteRecordRaw } from "vue-router"; 2 | 3 | export const Layout = () => import("@/layout/index.vue"); 4 | 5 | // 静态路由 6 | export const constantRoutes: RouteRecordRaw[] = [ 7 | { 8 | path: "/redirect", 9 | component: Layout, 10 | meta: { hidden: true }, 11 | children: [ 12 | { 13 | path: "/redirect/:path(.*)", 14 | component: () => import("@/views/redirect/index.vue"), 15 | }, 16 | ], 17 | }, 18 | 19 | { 20 | path: "/login", 21 | component: () => import("@/views/login/index.vue"), 22 | meta: { hidden: true }, 23 | }, 24 | 25 | { 26 | path: "/", 27 | name: "/", 28 | component: Layout, 29 | redirect: "/dashboard", 30 | children: [ 31 | { 32 | path: "dashboard", 33 | component: () => import("@/views/dashboard/index.vue"), 34 | name: "Dashboard", // 用于 keep-alive, 必须与SFC自动推导或者显示声明的组件name一致 35 | // https://cn.vuejs.org/guide/built-ins/keep-alive.html#include-exclude 36 | meta: { 37 | title: "dashboard", 38 | icon: "homepage", 39 | affix: true, 40 | keepAlive: true, 41 | alwaysShow: false, 42 | }, 43 | }, 44 | { 45 | path: "401", 46 | component: () => import("@/views/error-page/401.vue"), 47 | meta: { hidden: true }, 48 | }, 49 | { 50 | path: "404", 51 | component: () => import("@/views/error-page/404.vue"), 52 | meta: { hidden: true }, 53 | }, 54 | ], 55 | }, 56 | ]; 57 | 58 | /** 59 | * 创建路由 60 | */ 61 | const router = createRouter({ 62 | history: createWebHashHistory(), 63 | routes: constantRoutes as RouteRecordRaw[], 64 | // 刷新时,滚动条位置还原 65 | scrollBehavior: () => ({ left: 0, top: 0 }), 66 | }); 67 | 68 | /** 69 | * 重置路由 70 | */ 71 | export function resetRouter() { 72 | router.replace({ path: "/login" }); 73 | } 74 | 75 | export default router; 76 | -------------------------------------------------------------------------------- /src/plugins/permission.ts: -------------------------------------------------------------------------------- 1 | import router from "@/router"; 2 | import { useUserStore, usePermissionStore } from "@/store"; 3 | import NProgress from "@/utils/nprogress"; 4 | import { RouteRecordRaw } from "vue-router"; 5 | 6 | export function setupPermission() { 7 | // 白名单路由 8 | const whiteList = ["/login"]; 9 | 10 | router.beforeEach(async (to, from, next) => { 11 | NProgress.start(); 12 | const hasToken = localStorage.getItem("accessToken"); 13 | if (hasToken) { 14 | if (to.path === "/login") { 15 | // 如果已登录,跳转首页 16 | next({ path: "/" }); 17 | NProgress.done(); 18 | } else { 19 | const userStore = useUserStore(); 20 | const hasRoles = 21 | userStore.user.roles && userStore.user.roles.length > 0; 22 | if (hasRoles) { 23 | // 未匹配到任何路由,跳转404 24 | if (to.matched.length === 0) { 25 | from.name ? next({ name: from.name }) : next("/404"); 26 | } else { 27 | next(); 28 | } 29 | } else { 30 | const permissionStore = usePermissionStore(); 31 | try { 32 | const { roles } = await userStore.getUserInfo(); 33 | const accessRoutes = await permissionStore.generateRoutes(roles); 34 | accessRoutes.forEach((route: RouteRecordRaw) => { 35 | router.addRoute(route); 36 | }); 37 | next({ ...to, replace: true }); 38 | } catch (error) { 39 | // 移除 token 并跳转登录页 40 | await userStore.resetToken(); 41 | next(`/login?redirect=${to.path}`); 42 | NProgress.done(); 43 | } 44 | } 45 | } 46 | } else { 47 | // 未登录可以访问白名单页面 48 | if (whiteList.indexOf(to.path) !== -1) { 49 | next(); 50 | } else { 51 | next(`/login?redirect=${to.path}`); 52 | NProgress.done(); 53 | } 54 | } 55 | }); 56 | 57 | router.afterEach(() => { 58 | NProgress.done(); 59 | }); 60 | } 61 | -------------------------------------------------------------------------------- /src/assets/icons/homepage.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/api/sms/coupon/types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 优惠券查询参数类型 3 | */ 4 | export interface CouponQuery extends PageQuery { 5 | status?: number; 6 | keywords?: string; 7 | } 8 | 9 | /** 10 | * 优惠券分页列表项 11 | */ 12 | export interface Coupon { 13 | id: string; 14 | name: string; 15 | code: string; 16 | platformLabel: string; 17 | typeLabel: string; 18 | faceValueLabel: string; 19 | validityPeriodLabel: string; 20 | } 21 | 22 | /** 23 | *优惠券分页 24 | */ 25 | export type CouponPageResult = PageResult; 26 | 27 | /** 28 | * 优惠券表单类型 29 | */ 30 | export interface CouponForm { 31 | /** 32 | * ID 33 | */ 34 | id?: number; 35 | /** 36 | * 优惠券名称 37 | */ 38 | name: string; 39 | /** 40 | * 优惠券码 41 | */ 42 | code: string; 43 | /** 44 | * 使用平台(0:全平台;1:移动端;2:PC;) 45 | */ 46 | platform: number; 47 | /** 48 | * 优惠券类型(1:满减券;2:直减券;3:折扣券) 49 | */ 50 | type: number; 51 | 52 | /** 53 | * 优惠券面值类型 54 | */ 55 | faceValueType: number; 56 | /** 57 | * 优惠券面值 58 | */ 59 | faceValue: number; 60 | /** 61 | * 优惠券折扣 62 | */ 63 | discount: number; 64 | /** 65 | * 发行量 66 | */ 67 | circulation: number; 68 | /** 69 | * 使用门槛(0:无门槛) 70 | */ 71 | minPoint: number; 72 | /** 73 | * 每人限领张数(-1:无限制) 74 | */ 75 | perLimit: number; 76 | /** 77 | * 有效期类型(1:日期范围;2:固定天数) 78 | */ 79 | validityPeriodType: number; 80 | /** 81 | * 自领取之日起有效天数 82 | */ 83 | validityDays: number; 84 | /** 85 | * 有效期起始时间 86 | */ 87 | validityBeginTime: string; 88 | /** 89 | * 有效期截止时间 90 | */ 91 | validityEndTime: string; 92 | /** 93 | * 应用范围(0:全场通用;1:指定商品分类;2:指定商品) 94 | */ 95 | applicationScope: number; 96 | 97 | /** 98 | * 使用类型:指定商品分类 99 | */ 100 | spuCategoryIds: number[]; 101 | 102 | /** 103 | * 使用类型:指定商品 104 | */ 105 | spuIds: number[]; 106 | 107 | /** 108 | * 使用说明 109 | */ 110 | remark: string; 111 | } 112 | -------------------------------------------------------------------------------- /src/components/GithubCorner/index.vue: -------------------------------------------------------------------------------- 1 | 30 | 31 | 63 | -------------------------------------------------------------------------------- /src/api/system/menu/types.ts: -------------------------------------------------------------------------------- 1 | import { MenuTypeEnum } from "@/enums/MenuTypeEnum"; 2 | 3 | /** 4 | * 菜单查询参数类型 5 | */ 6 | export interface MenuQuery { 7 | keywords?: string; 8 | } 9 | 10 | /** 11 | * 菜单视图对象类型 12 | */ 13 | export interface MenuVO { 14 | /** 15 | * 子菜单 16 | */ 17 | children?: MenuVO[]; 18 | /** 19 | * 组件路径 20 | */ 21 | component?: string; 22 | /** 23 | * ICON 24 | */ 25 | icon?: string; 26 | /** 27 | * 菜单ID 28 | */ 29 | id?: number; 30 | /** 31 | * 菜单名称 32 | */ 33 | name?: string; 34 | /** 35 | * 父菜单ID 36 | */ 37 | parentId?: number; 38 | /** 39 | * 按钮权限标识 40 | */ 41 | perm?: string; 42 | /** 43 | * 跳转路径 44 | */ 45 | redirect?: string; 46 | /** 47 | * 路由名称 48 | */ 49 | routeName?: string; 50 | /** 51 | * 路由相对路径 52 | */ 53 | routePath?: string; 54 | /** 55 | * 菜单排序(数字越小排名越靠前) 56 | */ 57 | sort?: number; 58 | /** 59 | * 菜单类型 60 | */ 61 | type?: MenuTypeEnum; 62 | /** 63 | * 菜单是否可见(1:显示;0:隐藏) 64 | */ 65 | visible?: number; 66 | } 67 | 68 | /** 69 | * 菜单表单对象类型 70 | */ 71 | export interface MenuForm { 72 | /** 73 | * 菜单ID 74 | */ 75 | id?: string; 76 | /** 77 | * 父菜单ID 78 | */ 79 | parentId?: number; 80 | /** 81 | * 菜单名称 82 | */ 83 | name?: string; 84 | /** 85 | * 菜单是否可见(1:是;0:否;) 86 | */ 87 | visible: number; 88 | icon?: string; 89 | /** 90 | * 排序 91 | */ 92 | sort: number; 93 | /** 94 | * 组件路径 95 | */ 96 | component?: string; 97 | /** 98 | * 路由路径 99 | */ 100 | path?: string; 101 | /** 102 | * 跳转路由路径 103 | */ 104 | redirect?: string; 105 | 106 | /** 107 | * 菜单类型 108 | */ 109 | type: MenuTypeEnum; 110 | 111 | /** 112 | * 权限标识 113 | */ 114 | perm?: string; 115 | 116 | /** 117 | * 【菜单】是否开启页面缓存 118 | */ 119 | keepAlive?: number; 120 | 121 | /** 122 | * 【目录】只有一个子路由是否始终显示 123 | */ 124 | alwaysShow?: number; 125 | } 126 | -------------------------------------------------------------------------------- /src/assets/icons/peoples.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/coupon.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/utils/filter.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Show plural label if time is plural number 3 | * @param {number} time 4 | * @param {string} label 5 | * @return {string} 6 | */ 7 | function pluralize(time: number, label: string) { 8 | if (time === 1) { 9 | return time + label; 10 | } 11 | return time + label + "s"; 12 | } 13 | 14 | /** 15 | * @param {number} time 16 | */ 17 | export function timeAgo(time: number) { 18 | const between = Date.now() / 1000 - Number(time); 19 | if (between < 3600) { 20 | return pluralize(~~(between / 60), " minute"); 21 | } else if (between < 86400) { 22 | return pluralize(~~(between / 3600), " hour"); 23 | } else { 24 | return pluralize(~~(between / 86400), " day"); 25 | } 26 | } 27 | 28 | /** 29 | * Number formatting 30 | * like 10000 => 10k 31 | * @param {number} num 32 | * @param {number} digits 33 | */ 34 | export function numberFormatter(num: number, digits: number) { 35 | const si = [ 36 | { value: 1e18, symbol: "E" }, 37 | { value: 1e15, symbol: "P" }, 38 | { value: 1e12, symbol: "T" }, 39 | { value: 1e9, symbol: "G" }, 40 | { value: 1e6, symbol: "M" }, 41 | { value: 1e3, symbol: "k" }, 42 | ]; 43 | for (let i = 0; i < si.length; i++) { 44 | if (num >= si[i].value) { 45 | return ( 46 | (num / si[i].value) 47 | .toFixed(digits) 48 | .replace(/\.0+$|(\.[0-9]*[1-9])0+$/, "$1") + si[i].symbol 49 | ); 50 | } 51 | } 52 | return num.toString(); 53 | } 54 | 55 | /** 56 | * 10000 => "10,000" 57 | * @param {number} num 58 | */ 59 | export function toThousandFilter(num: number) { 60 | return (+num || 0) 61 | .toString() 62 | .replace(/^-?\d+/g, (m) => m.replace(/(?=(?!\b)(\d{3})+$)/g, ",")); 63 | } 64 | 65 | /** 66 | * Upper case first char 67 | * @param {String} string 68 | */ 69 | export function uppercaseFirst(string: string) { 70 | return string.charAt(0).toUpperCase() + string.slice(1); 71 | } 72 | 73 | /** 74 | * 金额转换(分->元) 75 | * 100 => 1 76 | * @param {number} num 77 | */ 78 | export function moneyFormatter(num: number) { 79 | return "¥" + (isNaN(num) ? 0.0 : parseFloat((num / 100).toFixed(2))); 80 | } 81 | -------------------------------------------------------------------------------- /src/layout/components/AppMain/index.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 23 | 24 | 83 | -------------------------------------------------------------------------------- /src/assets/icons/dict.svg: -------------------------------------------------------------------------------- 1 | 3 | 6 | 9 | 12 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /src/assets/icons/api.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/assets/icons/cascader.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/dashboard.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "./node_modules/typescript/lib", 3 | "npm.packageManager": "pnpm", 4 | "editor.tabSize": 2, 5 | "editor.formatOnSave": true, 6 | "editor.defaultFormatter": "esbenp.prettier-vscode", 7 | "editor.quickSuggestions": { 8 | "other": true, 9 | "comments": true, 10 | "strings": true 11 | }, 12 | "editor.codeActionsOnSave": { 13 | "source.fixAll": "explicit", 14 | "source.fixAll.eslint": "explicit", 15 | "source.fixAll.stylelint": "explicit" 16 | }, 17 | "files.eol": "\n", 18 | "search.exclude": { 19 | "**/node_modules": true, 20 | "**/*.log": true, 21 | "**/*.log*": true, 22 | "**/bower_components": true, 23 | "**/dist": true, 24 | "**/elehukouben": true, 25 | "**/.git": true, 26 | "**/.gitignore": true, 27 | "**/.svn": true, 28 | "**/.DS_Store": true, 29 | "**/.idea": true, 30 | "**/.vscode": false, 31 | "**/yarn.lock": true, 32 | "**/tmp": true, 33 | "out": true, 34 | "dist": true, 35 | "node_modules": true, 36 | "CHANGELOG.md": true, 37 | "examples": true, 38 | "res": true, 39 | "screenshots": true, 40 | "yarn-error.log": true, 41 | "**/.yarn": true 42 | }, 43 | "files.exclude": { 44 | "**/.cache": true, 45 | "**/.editorconfig": true, 46 | "**/.eslintcache": true, 47 | "**/bower_components": true, 48 | "**/.idea": true, 49 | "**/tmp": true, 50 | "**/.git": true, 51 | "**/.svn": true, 52 | "**/.hg": true, 53 | "**/CVS": true, 54 | "**/.DS_Store": true 55 | }, 56 | "files.watcherExclude": { 57 | "**/.git/objects/**": true, 58 | "**/.git/subtree-cache/**": true, 59 | "**/.vscode/**": true, 60 | "**/node_modules/**": true, 61 | "**/tmp/**": true, 62 | "**/bower_components/**": true, 63 | "**/dist/**": true, 64 | "**/yarn.lock": true 65 | }, 66 | "i18n-ally.keystyle": "nested", 67 | "i18n-ally.sortKeys": true, 68 | "i18n-ally.namespace": false, 69 | "i18n-ally.pathMatcher": "{locale}/{namespaces}.{ext}", 70 | "i18n-ally.enabledParsers": ["ts"], 71 | "i18n-ally.sourceLanguage": "en", 72 | "i18n-ally.displayLanguage": "zh-CN", 73 | "i18n-ally.enabledFrameworks": [ 74 | "vue", 75 | "react" 76 | ], 77 | "i18n-ally.localesPaths": [ 78 | "src/lang" 79 | ], 80 | } 81 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | 15 | mall-admin 16 | 17 | 18 | 19 |
20 |
21 |
22 | 23 | 24 | 25 | 96 | 97 | -------------------------------------------------------------------------------- /src/api/system/dict/types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 字典类型查询参数 3 | */ 4 | export interface DictTypeQuery extends PageQuery { 5 | /** 6 | * 关键字(字典类型名称/编码) 7 | */ 8 | keywords?: string; 9 | } 10 | 11 | /** 12 | * 字典类型分页对象 13 | */ 14 | export interface DictTypePageVO { 15 | /** 16 | * 字典类型ID 17 | */ 18 | id: number; 19 | /** 20 | * 类型编码 21 | */ 22 | code: string; 23 | /** 24 | * 类型名称 25 | */ 26 | name: string; 27 | /** 28 | * 状态(1:启用;0:禁用) 29 | */ 30 | status?: number; 31 | /** 32 | * 备注 33 | */ 34 | remark?: string; 35 | } 36 | 37 | /** 38 | * 字典分页项类型声明 39 | */ 40 | export type DictTypePageResult = PageResult; 41 | 42 | /** 43 | * 字典表单类型声明 44 | */ 45 | export interface DictTypeForm { 46 | /** 47 | * 字典类型ID 48 | */ 49 | id?: number; 50 | /** 51 | * 类型名称 52 | */ 53 | name?: string; 54 | /** 55 | * 类型编码 56 | */ 57 | code?: string; 58 | /** 59 | * 类型状态:1:启用;0:禁用 60 | */ 61 | status: number; 62 | /** 63 | * 备注 64 | */ 65 | remark?: string; 66 | } 67 | 68 | /** 69 | * 字典查询参数 70 | */ 71 | export interface DictQuery extends PageQuery { 72 | /** 73 | * 字典项名称 74 | */ 75 | // name?: string; 76 | keywords?: string; 77 | /** 78 | * 字典类型编码 79 | */ 80 | typeCode?: string; 81 | } 82 | 83 | /** 84 | * 字典分页对象 85 | */ 86 | export interface DictPageVO { 87 | /** 88 | * 字典ID 89 | */ 90 | id?: number; 91 | /** 92 | * 字典名称 93 | */ 94 | name?: string; 95 | /** 96 | * 状态(1:启用;0:禁用) 97 | */ 98 | status?: number; 99 | /** 100 | * 字典值 101 | */ 102 | value?: string; 103 | } 104 | 105 | /** 106 | * 字典分页 107 | */ 108 | export type DictPageResult = PageResult; 109 | 110 | /** 111 | * 字典表单 112 | */ 113 | export interface DictForm { 114 | /** 115 | * 字典ID 116 | */ 117 | id?: number; 118 | /** 119 | * 字典名称 120 | */ 121 | name?: string; 122 | /** 123 | * 排序 124 | */ 125 | sort?: number; 126 | /** 127 | * 状态(1:启用;0:禁用) 128 | */ 129 | status?: number; 130 | /** 131 | * 类型编码 132 | */ 133 | typeCode?: string; 134 | /** 135 | * 值 136 | */ 137 | value?: string; 138 | 139 | /** 140 | * 备注 141 | */ 142 | remark?: string; 143 | } 144 | -------------------------------------------------------------------------------- /src/assets/icons/security.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/captcha.svg: -------------------------------------------------------------------------------- 1 | 3 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /src/views/dashboard/components/RadarChart.vue: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 101 | -------------------------------------------------------------------------------- /src/store/modules/app.ts: -------------------------------------------------------------------------------- 1 | import defaultSettings from "@/settings"; 2 | 3 | // 导入 Element Plus 中英文语言包 4 | import zhCn from "element-plus/es/locale/lang/zh-cn"; 5 | import en from "element-plus/es/locale/lang/en"; 6 | import { store } from "@/store"; 7 | 8 | // setup 9 | export const useAppStore = defineStore("app", () => { 10 | // state 11 | const device = useStorage("device", "desktop"); 12 | const size = useStorage("size", defaultSettings.size); 13 | const language = useStorage("language", defaultSettings.language); 14 | 15 | const sidebarStatus = useStorage("sidebarStatus", "closed"); 16 | 17 | const sidebar = reactive({ 18 | opened: sidebarStatus.value !== "closed", 19 | withoutAnimation: false, 20 | }); 21 | const activeTopMenuPath = useStorage("activeTopMenuPath", ""); 22 | /** 23 | * 根据语言标识读取对应的语言包 24 | */ 25 | const locale = computed(() => { 26 | if (language?.value == "en") { 27 | return en; 28 | } else { 29 | return zhCn; 30 | } 31 | }); 32 | 33 | // actions 34 | function toggleSidebar() { 35 | sidebar.opened = !sidebar.opened; 36 | if (sidebar.opened) { 37 | sidebarStatus.value = "opened"; 38 | } else { 39 | sidebarStatus.value = "closed"; 40 | } 41 | } 42 | 43 | function closeSideBar() { 44 | sidebar.opened = false; 45 | sidebarStatus.value = "closed"; 46 | } 47 | 48 | function openSideBar() { 49 | sidebar.opened = true; 50 | sidebarStatus.value = "opened"; 51 | } 52 | 53 | function toggleDevice(val: string) { 54 | device.value = val; 55 | } 56 | 57 | function changeSize(val: string) { 58 | size.value = val; 59 | } 60 | /** 61 | * 切换语言 62 | * 63 | * @param val 64 | */ 65 | function changeLanguage(val: string) { 66 | language.value = val; 67 | } 68 | /** 69 | * 混合模式顶部切换 70 | */ 71 | function activeTopMenu(val: string) { 72 | activeTopMenuPath.value = val; 73 | } 74 | return { 75 | device, 76 | sidebar, 77 | language, 78 | locale, 79 | size, 80 | activeTopMenu, 81 | toggleDevice, 82 | changeSize, 83 | changeLanguage, 84 | toggleSidebar, 85 | closeSideBar, 86 | openSideBar, 87 | activeTopMenuPath, 88 | }; 89 | }); 90 | 91 | // 手动提供给 useStore() 函数 pinia 实例 92 | // https://pinia.vuejs.org/zh/core-concepts/outside-component-usage.html#using-a-store-outside-of-a-component 93 | export function useAppStoreHook() { 94 | return useAppStore(store); 95 | } 96 | -------------------------------------------------------------------------------- /src/views/dashboard/components/FunnelChart.vue: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 107 | -------------------------------------------------------------------------------- /src/layout/components/Sidebar/components/SidebarMixTopMenu.vue: -------------------------------------------------------------------------------- 1 | 2 | 33 | 34 | 84 | -------------------------------------------------------------------------------- /src/api/system/role/index.ts: -------------------------------------------------------------------------------- 1 | import request from "@/utils/request"; 2 | import { AxiosPromise } from "axios"; 3 | import { RoleQuery, RolePageResult, RoleForm } from "./types"; 4 | 5 | /** 6 | * 获取角色分页数据 7 | * 8 | * @param queryParams 9 | */ 10 | export function getRolePage( 11 | queryParams?: RoleQuery 12 | ): AxiosPromise { 13 | return request({ 14 | url: "/youlai-system/api/v1/roles/page", 15 | method: "get", 16 | params: queryParams, 17 | }); 18 | } 19 | 20 | /** 21 | * 获取角色下拉数据 22 | * 23 | * @param queryParams 24 | */ 25 | export function getRoleOptions( 26 | queryParams?: RoleQuery 27 | ): AxiosPromise { 28 | return request({ 29 | url: "/youlai-system/api/v1/roles/options", 30 | method: "get", 31 | params: queryParams, 32 | }); 33 | } 34 | 35 | /** 36 | * 获取角色的菜单ID集合 37 | * 38 | * @param queryParams 39 | */ 40 | export function getRoleMenuIds(roleId: number): AxiosPromise { 41 | return request({ 42 | url: "/youlai-system/api/v1/roles/" + roleId + "/menuIds", 43 | method: "get", 44 | }); 45 | } 46 | 47 | /** 48 | * 分配菜单权限给角色 49 | * 50 | * @param queryParams 51 | */ 52 | export function updateRoleMenus( 53 | roleId: number, 54 | data: number[] 55 | ): AxiosPromise { 56 | return request({ 57 | url: "/youlai-system/api/v1/roles/" + roleId + "/menus", 58 | method: "put", 59 | data: data, 60 | }); 61 | } 62 | 63 | /** 64 | * 获取角色详情 65 | * 66 | * @param id 67 | */ 68 | export function getRoleForm(id: number): AxiosPromise { 69 | return request({ 70 | url: "/youlai-system/api/v1/roles/" + id + "/form", 71 | method: "get", 72 | }); 73 | } 74 | 75 | /** 76 | * 添加角色 77 | * 78 | * @param data 79 | */ 80 | export function addRole(data: RoleForm) { 81 | return request({ 82 | url: "/youlai-system/api/v1/roles", 83 | method: "post", 84 | data: data, 85 | }); 86 | } 87 | 88 | /** 89 | * 更新角色 90 | * 91 | * @param id 92 | * @param data 93 | */ 94 | export function updateRole(id: number, data: RoleForm) { 95 | return request({ 96 | url: "/youlai-system/api/v1/roles/" + id, 97 | method: "put", 98 | data: data, 99 | }); 100 | } 101 | 102 | /** 103 | * 批量删除角色,多个以英文逗号(,)分割 104 | * 105 | * @param ids 106 | */ 107 | export function deleteRoles(ids: string) { 108 | return request({ 109 | url: "/youlai-system/api/v1/roles/" + ids, 110 | method: "delete", 111 | }); 112 | } 113 | -------------------------------------------------------------------------------- /src/store/modules/user.ts: -------------------------------------------------------------------------------- 1 | import { loginApi, logoutApi } from "@/api/auth"; 2 | import { getUserInfoApi } from "@/api/system/user"; 3 | import { resetRouter } from "@/router"; 4 | import { store } from "@/store"; 5 | 6 | import { LoginData } from "@/api/auth/types"; 7 | import { UserInfo } from "@/api/system/user/types"; 8 | 9 | export const useUserStore = defineStore("user", () => { 10 | const user: UserInfo = { 11 | roles: [], 12 | perms: [], 13 | }; 14 | 15 | /** 16 | * 登录 17 | * 18 | * @param {LoginData} 19 | * @returns 20 | */ 21 | function login(loginData: LoginData) { 22 | return new Promise((resolve, reject) => { 23 | loginApi(loginData) 24 | .then(({ data }) => { 25 | const { token_type, access_token } = data; 26 | localStorage.setItem("accessToken", token_type + " " + access_token); // Bearer eyJhbGciOiJIUzI1NiJ9.xxx.xxx 27 | resolve(); 28 | }) 29 | .catch((error) => { 30 | reject(error); 31 | }); 32 | }); 33 | } 34 | 35 | // 获取信息(用户昵称、头像、角色集合、权限集合) 36 | function getUserInfo() { 37 | return new Promise((resolve, reject) => { 38 | getUserInfoApi() 39 | .then(({ data }) => { 40 | if (!data) { 41 | reject("Verification failed, please Login again."); 42 | return; 43 | } 44 | if (!data.roles || data.roles.length <= 0) { 45 | reject("getUserInfo: roles must be a non-null array!"); 46 | return; 47 | } 48 | Object.assign(user, { ...data }); 49 | resolve(data); 50 | }) 51 | .catch((error) => { 52 | reject(error); 53 | }); 54 | }); 55 | } 56 | 57 | // user logout 58 | function logout() { 59 | return new Promise((resolve, reject) => { 60 | logoutApi() 61 | .then(() => { 62 | localStorage.setItem("accessToken", ""); 63 | location.reload(); // 清空路由 64 | resolve(); 65 | }) 66 | .catch((error) => { 67 | reject(error); 68 | }); 69 | }); 70 | } 71 | 72 | // remove token 73 | function resetToken() { 74 | return new Promise((resolve) => { 75 | localStorage.setItem("accessToken", ""); 76 | resetRouter(); 77 | resolve(); 78 | }); 79 | } 80 | 81 | return { 82 | user, 83 | login, 84 | getUserInfo, 85 | logout, 86 | resetToken, 87 | }; 88 | }); 89 | 90 | // 非setup 91 | export function useUserStoreHook() { 92 | return useUserStore(store); 93 | } 94 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 2.6.3 (2023/10/22) 2 | 3 | ### ✨ feat 4 | - 菜单管理新增目录只有一级子路由是否始终显示(alwaysShow)和路由页面是否缓存(keepAlive)的配置 5 | - 接口文档新增 swagger、knife4j 6 | - 引入和支持 tsx 7 | 8 | ### 🔄 refactor 9 | - 代码瘦身,整理并删除未使用的 svg 10 | - 控制台样式优化 11 | 12 | ### 🐛 fix 13 | - 菜单栏折叠和展开的图标暗黑模式显示问题修复 14 | 15 | 16 | # 2.6.2 (2023/10/11) 17 | 18 | ### 🐛 fix 19 | - 主题设置未持久化问题 20 | - UnoCSS 插件无智能提示 21 | 22 | ### 🔄 refactor 23 | - WebSocket 演示样式和代码优化 24 | - 用户管理代码重构 25 | 26 | # 2.6.1 (2023/9/4) 27 | 28 | ### 🐛 fix 29 | - 导航顶部模式、混合模式样式在固定 Header 出现的样式问题修复 30 | - 固定 Header 没有持久化问题修复 31 | - 字典回显兼容 String 和 Number 类型 32 | 33 | # 2.6.0 (2023/8/24)💥💥💥 34 | 35 | ### ✨ feat 36 | - 导航顶部模式、混合模式支持(author by [april-tong](https://april-tong.com/)) 37 | - 平台文档(内嵌)(author by [april-tong](https://april-tong.com/)) 38 | 39 | # 2.5.0 (2023/8/8) 40 | 41 | ### ✨ feat 42 | - 新增 Mock(author by [ygcaicn](https://github.com/ygcaicn)) 43 | - 图标 DEMO(author by [ygcaicn](https://github.com/ygcaicn)) 44 | 45 | ### 🐛 fix 46 | - 字典支持 Number 类型 47 | 48 | # 2.4.1 (2023/7/20) 49 | 50 | ### ✨ feat 51 | - 整合 vite-plugin-compression 插件打包优化(3.66MB → 1.58MB) (author by [april-tong](https://april-tong.com/)) 52 | - 字典组件封装(author by [haoxr](https://juejin.cn/user/4187394044331261/posts)) 53 | 54 | ### 🐛 fix 55 | - 分页组件hidden无效 56 | - 签名无法保存至后端 57 | - Git 提交 stylelint 校验部分机器报错 58 | 59 | # 2.4.0 (2023/6/17) 60 | 61 | ### ✨ feat 62 | - 新增组件标签输入框(author by [april-tong](https://april-tong.com/)) 63 | - 新增组件签名(author by [april-tong](https://april-tong.com/)) 64 | - 新增组件表格(author by [april-tong](https://april-tong.com/)) 65 | - Echarts 图表添加下载功能 author by [april-tong](https://april-tong.com/)) 66 | 67 | ### 🔄 refactor 68 | - 限制包管理器为 pnpm 和 node 版本16+ 69 | - 自定义组件自动导入配置 70 | - 搜索框样式写法优化 71 | 72 | ### 🐛 fix 73 | - 用户导入的部门回显成数字问题修复 74 | 75 | ### ⬆️ chore 76 | - element-plus 版本升级 2.3.5 → 2.3.6 77 | 78 | # 2.3.1 (2023/5/21) 79 | 80 | ### 🔄 refactor 81 | - 组件示例文件名称优化 82 | 83 | # 2.2.2 (2023/5/11) 84 | 85 | ### ✨ feat 86 | - 组件封装示例添加源码地址 87 | - 角色、菜单、部门、字段按钮添加权限控制 88 | 89 | 90 | # 2.3.0 (2023/5/12) 91 | 92 | ### ⬆️ chore 93 | - vue 版本升级 3.2.45 → 3.3.1 ([CHANGELOG](https://github.com/vuejs/core/blob/main/CHANGELOG.md)) 94 | - vite 版本升级 4.3.1 → 4.3.5 95 | 96 | ### 🔄 refactor 97 | - 使用 vue 3.3 版本新特性 `defineOptions` 在 `setup` 定义组件名称,移除重复的 `script` 标签 98 | 99 | # 2.2.2 (2023/5/11) 100 | 101 | ### ✨ feat 102 | - 用户新增提交添加 `vueUse` 的 `useDebounceFn` 函数实现按钮防抖节流 103 | 104 | 105 | # 2.2.1 (2023/4/25) 106 | 107 | ### 🐛 fix 108 | - 图标选择器组件使用 `onClickOutside` 未排除下拉弹出框元素导致无法输入搜索。 109 | 110 | -------------------------------------------------------------------------------- /src/components/Breadcrumb/index.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 90 | 91 | 98 | -------------------------------------------------------------------------------- /src/assets/icons/redis.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/layout/components/Settings/components/LayoutSelect.vue: -------------------------------------------------------------------------------- 1 | 37 | 38 | 51 | 52 | 109 | -------------------------------------------------------------------------------- /src/views/pms/goods/detail.vue: -------------------------------------------------------------------------------- 1 | 56 | 100 | 101 | 108 | -------------------------------------------------------------------------------- /src/views/error-page/401.vue: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 28 | 29 | 67 | 68 | 115 | --------------------------------------------------------------------------------