├── src ├── views │ ├── role │ │ ├── 角色管理 │ │ ├── Role.vue │ │ └── components │ │ │ └── RoleUser.vue │ ├── setting │ │ ├── 系统设置 │ │ ├── components │ │ │ └── Theme.vue │ │ └── Setting.vue │ ├── article │ │ ├── Article.vue │ │ ├── edit │ │ │ └── Edit.vue │ │ └── home │ │ │ └── Home.vue │ ├── blank │ │ └── Blank.vue │ ├── common │ │ ├── reload │ │ │ └── Reload.vue │ │ └── forbidden │ │ │ └── Forbidden.vue │ ├── individual │ │ ├── Individual.vue │ │ └── components │ │ │ ├── BaseInfo.vue │ │ │ ├── BaseEdit.vue │ │ │ └── Password.vue │ ├── home │ │ ├── components │ │ │ ├── ProgressBar.vue │ │ │ ├── NewGoods.vue │ │ │ ├── Statistics.vue │ │ │ ├── LineChart.vue │ │ │ └── TodoList.vue │ │ └── Home.vue │ ├── more │ │ └── file-to-base64 │ │ │ └── FileToBase64.vue │ ├── portal │ │ ├── login │ │ │ └── Login.vue │ │ ├── password │ │ │ └── Password.vue │ │ └── register │ │ │ └── Register.vue │ └── user │ │ ├── components │ │ └── UserEdit.vue │ │ └── User.vue ├── model │ ├── article.ts │ ├── user.ts │ ├── home.ts │ └── common.ts ├── hooks │ ├── index.ts │ ├── base │ │ ├── index.ts │ │ ├── useConfirm.ts │ │ └── useCountDown.ts │ └── business │ │ ├── index.ts │ │ ├── usePermission.ts │ │ ├── useSmsCaptcha.ts │ │ └── useImageCaptcha.ts ├── config │ ├── constant.ts │ ├── ui.ts │ ├── domain.ts │ ├── regexp.ts │ └── enum.ts ├── assets │ ├── styles │ │ ├── app.less │ │ ├── tailwind.less │ │ ├── element-plus.less │ │ └── reset.less │ ├── images │ │ ├── common │ │ │ ├── 403.gif │ │ │ ├── 404.jpg │ │ │ ├── success.png │ │ │ ├── warning.png │ │ │ └── avatar_default.png │ │ └── layout │ │ │ └── portal_bg.jpg │ └── icons │ │ ├── minus.svg │ │ ├── blank.svg │ │ ├── filter.svg │ │ ├── close.svg │ │ ├── chart.svg │ │ ├── plus.svg │ │ ├── edit.svg │ │ ├── user.svg │ │ ├── back.svg │ │ ├── home.svg │ │ ├── upload.svg │ │ ├── download.svg │ │ ├── exit.svg │ │ ├── more.svg │ │ ├── eye-open.svg │ │ ├── lock.svg │ │ ├── notice.svg │ │ ├── article.svg │ │ ├── user-fill.svg │ │ ├── menu-fold.svg │ │ ├── menu-unfold.svg │ │ ├── search.svg │ │ ├── permission.svg │ │ ├── eye-close.svg │ │ ├── tab.svg │ │ ├── role.svg │ │ ├── refresh.svg │ │ ├── message.svg │ │ ├── view.svg │ │ ├── logo.svg │ │ ├── setting.svg │ │ ├── goods.svg │ │ ├── loading.svg │ │ └── empty.svg ├── apis │ ├── modules │ │ ├── role.ts │ │ ├── individual.ts │ │ ├── portal.ts │ │ ├── user.ts │ │ ├── common.ts │ │ └── home.ts │ └── index.ts ├── utils │ ├── type.ts │ ├── crypto.ts │ ├── event-bus.ts │ ├── message.ts │ ├── storage-mng.ts │ ├── file.ts │ ├── request.ts │ ├── color.ts │ ├── core.ts │ └── excle.js ├── mock │ ├── modules │ │ ├── individual.ts │ │ ├── role.ts │ │ ├── portal.ts │ │ ├── user.ts │ │ ├── common.ts │ │ ├── home.ts │ │ └── article.ts │ ├── index.ts │ └── util.ts ├── @types │ ├── global.d.ts │ └── env.d.ts ├── App.vue ├── store │ ├── index.ts │ └── modules │ │ ├── ui.ts │ │ ├── enum.ts │ │ └── app.ts ├── directives │ ├── index.ts │ └── modules │ │ └── auth.ts ├── router │ ├── modules │ │ ├── home.ts │ │ ├── role.ts │ │ ├── user.ts │ │ ├── blank.ts │ │ ├── setting.ts │ │ ├── more.ts │ │ ├── individule.ts │ │ ├── portal.ts │ │ ├── common.ts │ │ └── article.ts │ ├── index.ts │ └── routes.ts ├── components │ ├── base │ │ ├── BaseTitle.vue │ │ ├── BaseTag.vue │ │ ├── BaseEmpty.vue │ │ ├── BaseBack.vue │ │ ├── Avatar.vue │ │ ├── BaseIcon.vue │ │ ├── BaseConfirm.vue │ │ ├── BaseDot.vue │ │ ├── Pagination.vue │ │ ├── Overflow.vue │ │ ├── CountTo.vue │ │ ├── BaseDialog.vue │ │ ├── BaseButton.vue │ │ └── BaseDrawer.vue │ └── business │ │ ├── ImageCaptcha.vue │ │ └── AvatarUpload.vue ├── main.ts └── layouts │ ├── portal │ └── Portal.vue │ └── platform │ ├── Platform.vue │ └── components │ ├── UserInfo.vue │ ├── MenuItem.vue │ ├── SideBar.vue │ └── HeaderBar.vue ├── public └── favicon.ico ├── tsconfig.node.json ├── .gitignore ├── .prettierrc.js ├── tailwind.config.js ├── tsconfig.json ├── index.html ├── package.json ├── README.md ├── components.d.ts ├── vite.config.ts └── auto-imports.d.ts /src/views/role/角色管理: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/views/setting/系统设置: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/model/article.ts: -------------------------------------------------------------------------------- 1 | export interface IArticle {} 2 | -------------------------------------------------------------------------------- /src/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export * from './base' 2 | export * from './business' 3 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wluyao/vue-element-manage/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /src/views/article/Article.vue: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /src/views/blank/Blank.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/hooks/base/index.ts: -------------------------------------------------------------------------------- 1 | import useCountDown from './useCountDown' 2 | 3 | export { useCountDown } 4 | -------------------------------------------------------------------------------- /src/config/constant.ts: -------------------------------------------------------------------------------- 1 | export const TokenName = 'vue_element_ts_admin_token' 2 | export const DesKey = '123abc' 3 | -------------------------------------------------------------------------------- /src/assets/styles/app.less: -------------------------------------------------------------------------------- 1 | @import './reset.less'; 2 | @import './tailwind.less'; 3 | @import './element-plus.less'; 4 | -------------------------------------------------------------------------------- /src/assets/images/common/403.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wluyao/vue-element-manage/HEAD/src/assets/images/common/403.gif -------------------------------------------------------------------------------- /src/assets/images/common/404.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wluyao/vue-element-manage/HEAD/src/assets/images/common/404.jpg -------------------------------------------------------------------------------- /src/assets/images/common/success.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wluyao/vue-element-manage/HEAD/src/assets/images/common/success.png -------------------------------------------------------------------------------- /src/assets/images/common/warning.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wluyao/vue-element-manage/HEAD/src/assets/images/common/warning.png -------------------------------------------------------------------------------- /src/assets/images/layout/portal_bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wluyao/vue-element-manage/HEAD/src/assets/images/layout/portal_bg.jpg -------------------------------------------------------------------------------- /src/views/role/Role.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/assets/images/common/avatar_default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wluyao/vue-element-manage/HEAD/src/assets/images/common/avatar_default.png -------------------------------------------------------------------------------- /src/views/article/edit/Edit.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/views/article/home/Home.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/views/role/components/RoleUser.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/views/setting/components/Theme.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/apis/modules/role.ts: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | // 获取用户列表(不分页) 4 | export const getAllRoleList = () => request.post('/role/all') 5 | -------------------------------------------------------------------------------- /src/utils/type.ts: -------------------------------------------------------------------------------- 1 | // 方便定义字面量类型 2 | export const tupleStr = (...args: T) => args 3 | 4 | export const tupleNum = (...args: T) => args 5 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "esnext", 5 | "moduleResolution": "node" 6 | }, 7 | "include": ["vite.config.ts"] 8 | } 9 | -------------------------------------------------------------------------------- /src/mock/modules/individual.ts: -------------------------------------------------------------------------------- 1 | import Mock from 'mockjs' 2 | 3 | const editUserInfo = (config) => { 4 | return { 5 | code: 200, 6 | } 7 | } 8 | 9 | Mock.mock(/\/individual\/edit\/userInfo/, 'post', editUserInfo) 10 | -------------------------------------------------------------------------------- /src/views/setting/Setting.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/hooks/business/index.ts: -------------------------------------------------------------------------------- 1 | import useImageCaptcha from './useImageCaptcha' 2 | import usePermission from './usePermission' 3 | import useSmsCaptcha from './useSmsCaptcha' 4 | 5 | export { useImageCaptcha, usePermission, useSmsCaptcha } 6 | -------------------------------------------------------------------------------- /src/@types/global.d.ts: -------------------------------------------------------------------------------- 1 | declare global { 2 | interface Window { 3 | $message: typeof import('@/utils/message')['default'] 4 | } 5 | 6 | type Status = 'primary' | 'success' | 'warning' | 'danger' | 'info' 7 | } 8 | 9 | export {} 10 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 14 | -------------------------------------------------------------------------------- /src/views/common/reload/Reload.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 13 | -------------------------------------------------------------------------------- /src/store/index.ts: -------------------------------------------------------------------------------- 1 | import { createPinia } from 'pinia' 2 | 3 | const store = createPinia() 4 | 5 | export default store 6 | 7 | export * from './modules/app' 8 | export * from './modules/enum' 9 | export * from './modules/ui' 10 | export * from './modules/enum' 11 | -------------------------------------------------------------------------------- /src/apis/modules/individual.ts: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | export const editUserInfo = (payload) => request.post('/individual/edit/userInfo', payload) 4 | // 修改密码 5 | export const modifyPassword = (payload: any) => request.post('/system/password/modify', payload) 6 | -------------------------------------------------------------------------------- /src/assets/icons/minus.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/icons/blank.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /src/directives/index.ts: -------------------------------------------------------------------------------- 1 | import type { App } from 'vue' 2 | import auth from './modules/auth' 3 | 4 | const directives = { auth } 5 | 6 | export const setupDirectives = (app: App) => { 7 | Object.keys(directives).forEach((key) => { 8 | app.directive(key, directives[key]) 9 | }) 10 | } 11 | -------------------------------------------------------------------------------- /src/assets/styles/tailwind.less: -------------------------------------------------------------------------------- 1 | @import 'tailwindcss/base'; 2 | @import 'tailwindcss/components'; 3 | @import 'tailwindcss/utilities'; 4 | 5 | @layer base { 6 | button, 7 | [type='button'], 8 | [type='reset'], 9 | [type='submit'] { 10 | background-color: var(--el-button-bg-color); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/assets/icons/filter.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /src/@types/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | declare module '*.vue' { 4 | import type { 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 | -------------------------------------------------------------------------------- /src/mock/index.ts: -------------------------------------------------------------------------------- 1 | import Mock from 'mockjs' 2 | import './modules/common' 3 | import './modules/article' 4 | import './modules/home' 5 | import './modules/portal' 6 | import './modules/role' 7 | import './modules/user' 8 | import './modules/individual' 9 | 10 | //延时数据返回 11 | Mock.setup({ 12 | timeout: '100-500', 13 | }) 14 | -------------------------------------------------------------------------------- /src/assets/styles/element-plus.less: -------------------------------------------------------------------------------- 1 | :root:root { 2 | --el-color-primary: @primary; 3 | --el-color-warning: @warning; 4 | --el-color-danger: @danger; 5 | --el-color-success: @success; 6 | --el-text-color-primary: @info-primary; 7 | --el-text-color-regular: @info-regular; 8 | --el-text-color-secondary: @info-secondary; 9 | } 10 | -------------------------------------------------------------------------------- /src/config/ui.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | // 主题色 3 | themeColor: '#373737', 4 | // 不同状态的颜色 5 | statusColor: { 6 | success: '#00caa2', 7 | warning: '#ffae00', 8 | error: '#fe1e67', 9 | info: '#999', 10 | }, 11 | // 是否折叠侧边菜单 12 | sideCollapse: false, 13 | // 组件是否圆角 14 | round: true, 15 | // 组件大小 16 | size: 'medium', 17 | } 18 | -------------------------------------------------------------------------------- /src/model/user.ts: -------------------------------------------------------------------------------- 1 | export interface IUser { 2 | id: number 3 | name: string 4 | username: string 5 | mobile: string 6 | email: string 7 | avatar: string 8 | gender: '1' | '2' 9 | age: number 10 | role: number 11 | roleName: string 12 | // 状态 0:停用 1:启用 13 | status: '0' | '1' 14 | createTime: string 15 | remark: string 16 | } 17 | -------------------------------------------------------------------------------- /src/router/modules/home.ts: -------------------------------------------------------------------------------- 1 | import { RouteRecordRaw } from 'vue-router' 2 | 3 | const routes: RouteRecordRaw[] = [ 4 | { 5 | name: 'Home', 6 | path: '/home', 7 | component: () => import('@/views/home/Home.vue'), 8 | meta: { 9 | title: '首页', 10 | activePath: '/home', 11 | }, 12 | }, 13 | ] 14 | 15 | export default routes 16 | -------------------------------------------------------------------------------- /src/router/modules/role.ts: -------------------------------------------------------------------------------- 1 | import { RouteRecordRaw } from 'vue-router' 2 | 3 | const routes: RouteRecordRaw[] = [ 4 | { 5 | name: 'Role', 6 | path: '/role', 7 | component: () => import('@/views/role/Role.vue'), 8 | meta: { 9 | title: '角色管理', 10 | activePath: '/role', 11 | }, 12 | }, 13 | ] 14 | 15 | export default routes 16 | -------------------------------------------------------------------------------- /src/router/modules/user.ts: -------------------------------------------------------------------------------- 1 | import { RouteRecordRaw } from 'vue-router' 2 | 3 | const routes: RouteRecordRaw[] = [ 4 | { 5 | name: 'User', 6 | path: '/user', 7 | component: () => import('@/views/user/User.vue'), 8 | meta: { 9 | title: '用户管理', 10 | activePath: '/user', 11 | }, 12 | }, 13 | ] 14 | 15 | export default routes 16 | -------------------------------------------------------------------------------- /src/router/modules/blank.ts: -------------------------------------------------------------------------------- 1 | import { RouteRecordRaw } from 'vue-router' 2 | 3 | const routes: RouteRecordRaw[] = [ 4 | { 5 | name: 'Blank', 6 | path: '/blank', 7 | component: () => import('@/views/blank/Blank.vue'), 8 | meta: { 9 | title: '空白页', 10 | activePath: '/blank', 11 | }, 12 | }, 13 | ] 14 | 15 | export default routes 16 | -------------------------------------------------------------------------------- /src/apis/index.ts: -------------------------------------------------------------------------------- 1 | import * as common from './modules/common' 2 | import * as home from './modules/home' 3 | import * as portal from './modules/portal' 4 | import * as role from './modules/role' 5 | import * as user from './modules/user' 6 | import * as individual from './modules/individual' 7 | 8 | export default { common, home, portal, role, user, individual } 9 | -------------------------------------------------------------------------------- /src/model/home.ts: -------------------------------------------------------------------------------- 1 | export interface IStatistics { 2 | visit: number 3 | user: number 4 | goods: number 5 | comment: number 6 | } 7 | 8 | export interface ITask { 9 | id: number 10 | content: string 11 | status: boolean 12 | } 13 | 14 | export interface IGoods { 15 | id: number 16 | name: string 17 | address: string 18 | price: number 19 | } 20 | -------------------------------------------------------------------------------- /src/hooks/business/usePermission.ts: -------------------------------------------------------------------------------- 1 | import { useAppStore } from '@/store' 2 | 3 | const usePermission = () => { 4 | const appStore = useAppStore() 5 | 6 | const checkPermission = (value: string = '') => { 7 | return appStore.permissions.includes(value) 8 | } 9 | 10 | return { 11 | checkPermission, 12 | } 13 | } 14 | 15 | export default usePermission 16 | -------------------------------------------------------------------------------- /src/router/modules/setting.ts: -------------------------------------------------------------------------------- 1 | import { RouteRecordRaw } from 'vue-router' 2 | 3 | const routes: RouteRecordRaw[] = [ 4 | { 5 | name: 'Setting', 6 | path: '/setting', 7 | component: () => import('@/views/setting/Setting.vue'), 8 | meta: { 9 | title: '系统设置', 10 | activePath: '/setting', 11 | }, 12 | }, 13 | ] 14 | 15 | export default routes 16 | -------------------------------------------------------------------------------- /src/router/modules/more.ts: -------------------------------------------------------------------------------- 1 | import { RouteRecordRaw } from 'vue-router' 2 | 3 | const routes: RouteRecordRaw[] = [ 4 | { 5 | name: 'FileToBase64', 6 | path: '/more/fileToBase64', 7 | component: () => import('@/views/more/file-to-base64/FileToBase64.vue'), 8 | meta: { 9 | title: 'fileToBase64', 10 | }, 11 | }, 12 | ] 13 | 14 | export default routes 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /src/apis/modules/portal.ts: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | // 登录 4 | export const login = (payload) => request.post('/system/login', payload) 5 | 6 | // 注册 7 | export const register = (payload) => request.post('/system/register', payload) 8 | 9 | // 重置密码 10 | export const resetPassword = (payload: any) => request.post('/system/password/reset', payload) 11 | -------------------------------------------------------------------------------- /src/views/individual/Individual.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/router/modules/individule.ts: -------------------------------------------------------------------------------- 1 | import { RouteRecordRaw } from 'vue-router' 2 | 3 | const routes: RouteRecordRaw[] = [ 4 | { 5 | name: 'Individual', 6 | path: '/individual', 7 | component: () => import('@/views/individual/Individual.vue'), 8 | meta: { 9 | title: '个人中心', 10 | activePath: '/individual', 11 | }, 12 | }, 13 | ] 14 | 15 | export default routes 16 | -------------------------------------------------------------------------------- /src/directives/modules/auth.ts: -------------------------------------------------------------------------------- 1 | import type { Directive } from 'vue' 2 | import { usePermission } from '@/hooks/business' 3 | 4 | const auth: Directive = { 5 | mounted(el, binding) { 6 | const { checkPermission } = usePermission() 7 | if (!checkPermission(binding.value)) { 8 | el.remove() 9 | } 10 | }, 11 | } 12 | 13 | export default auth 14 | -------------------------------------------------------------------------------- /src/assets/icons/close.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/mock/modules/role.ts: -------------------------------------------------------------------------------- 1 | import Mock from 'mockjs' 2 | 3 | const getAllRoleList = () => { 4 | return { 5 | code: 200, 6 | data: [ 7 | { 8 | id: 1, 9 | name: '管理员', 10 | }, 11 | { 12 | id: 2, 13 | name: '运营', 14 | }, 15 | { 16 | id: 3, 17 | name: '游客', 18 | }, 19 | ], 20 | } 21 | } 22 | 23 | Mock.mock(/\/role\/all/, 'post', getAllRoleList) 24 | -------------------------------------------------------------------------------- /src/assets/icons/chart.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /src/assets/icons/plus.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | -------------------------------------------------------------------------------- /src/assets/icons/edit.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // 超过多少字符后换行 3 | printWidth: 120, 4 | // 行末分号 5 | semi: false, 6 | // 单引号 7 | singleQuote: true, 8 | // 缩进 9 | tabWidth: 2, 10 | // 使用tab缩进还是空格 11 | useTabs: true, 12 | // 是否使用尾逗号 13 | trailingComma: "es5", 14 | // > 标签放在最后一行的末尾,而不是单独放在下一行 15 | jsxBracketSameLine: false, 16 | // (x) => {} 箭头函数参数只有一个时是否要有小括号。 always:总是带括号,avoid:省略括号 17 | arrowParens: "always", 18 | }; 19 | -------------------------------------------------------------------------------- /src/apis/modules/user.ts: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | export const getUserList = (payload) => request.post('/user/list', payload) 4 | export const getAllUserList = () => request.post('/user/all') 5 | 6 | export const getUserDetail = (id: number) => request.post(`/user/detail/${id}`) 7 | 8 | export const editUser = (payload) => request.post('/user/edit', payload) 9 | 10 | export const deleteUser = (id: number) => request.post(`/user/delete/${id}`) 11 | -------------------------------------------------------------------------------- /src/assets/icons/user.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 7 | -------------------------------------------------------------------------------- /src/config/domain.ts: -------------------------------------------------------------------------------- 1 | export const environment = location.pathname.split('/')[1] || 'test' 2 | 3 | const ENV_CONFIG_MAP: any = { 4 | development: location.origin + '/api', 5 | test: 'https://www.api.com/test', 6 | production: `https://www.api.com/${environment}`, 7 | } 8 | 9 | const nodeEnv = process.env.NODE_ENV || 'development' 10 | 11 | export const baseURL: string = ENV_CONFIG_MAP[nodeEnv] 12 | 13 | export const wsBaseURL = baseURL.replace('http:', 'ws:').replace('https:', 'wss:') 14 | -------------------------------------------------------------------------------- /src/config/regexp.ts: -------------------------------------------------------------------------------- 1 | // 密码需要为6~20位,且需要包含数字和字母 2 | export const regPassword = /^(?![^a-zA-Z]+$)(?!\D+$).{6,20}$/ 3 | 4 | // 手机号 5 | export const regMobile = /(^1[3456789]\d{9}$)/ 6 | 7 | // 电子邮箱 8 | export const regEmail = /^\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$/ 9 | 10 | // 身份证号 11 | export const regIdCard = /^(\d{6})(\d{4})(\d{2})(\d{2})(\d{3})([0-9]|X)$/ 12 | 13 | // 港澳通行证 14 | export const regHkMacao = /^C\d{8}$/ 15 | 16 | // 中文 17 | export const regChinese = /[\u4E00-\u9FA5]/ 18 | -------------------------------------------------------------------------------- /src/assets/icons/back.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 7 | -------------------------------------------------------------------------------- /src/assets/icons/home.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/assets/icons/upload.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/assets/icons/download.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/assets/icons/exit.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/config/enum.ts: -------------------------------------------------------------------------------- 1 | import { enumMng } from '@/utils/core' 2 | import uiConfig from '@/config/ui' 3 | 4 | const { success, warning, error, info } = uiConfig.statusColor 5 | 6 | // 性别 7 | export const enumGender = enumMng([ 8 | { 9 | id: '1', 10 | name: '男', 11 | }, 12 | { 13 | id: '2', 14 | name: '女', 15 | }, 16 | ]) 17 | 18 | // 用户状态 19 | export const enumUserStatus = enumMng([ 20 | { 21 | id: '0', 22 | name: '停用', 23 | status: 'danger', 24 | }, 25 | { 26 | id: '1', 27 | name: '启用', 28 | status: 'success', 29 | }, 30 | ]) 31 | -------------------------------------------------------------------------------- /src/assets/icons/more.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/model/common.ts: -------------------------------------------------------------------------------- 1 | // 用户信息 2 | 3 | export interface IUser { 4 | id: number 5 | name: string 6 | username: string 7 | mobile: string 8 | email: string 9 | avatar: string 10 | gender: '1' | '2' 11 | age: number 12 | // 角色 13 | role: number 14 | createDate: string 15 | remark?: string 16 | } 17 | 18 | // 菜单 19 | export interface IMenu { 20 | id: number 21 | name: string 22 | // menu:菜单权限 button:按钮权限 23 | type: 'menu' | 'button' 24 | // 菜单图标 25 | icon?: string 26 | // 权限码 27 | permission?: string 28 | // 页面路径 29 | path?: string 30 | children?: IMenu[] 31 | } 32 | -------------------------------------------------------------------------------- /src/assets/icons/eye-open.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | -------------------------------------------------------------------------------- /src/assets/icons/lock.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 7 | 8 | -------------------------------------------------------------------------------- /src/router/modules/portal.ts: -------------------------------------------------------------------------------- 1 | import { RouteRecordRaw } from 'vue-router' 2 | 3 | const routes: RouteRecordRaw[] = [ 4 | { 5 | path: '/login', 6 | component: () => import('@/views/portal/login/Login.vue'), 7 | meta: { 8 | title: '登录', 9 | }, 10 | }, 11 | { 12 | path: '/password', 13 | component: () => import('@/views/portal/password/Password.vue'), 14 | meta: { 15 | title: '忘记密码', 16 | }, 17 | }, 18 | { 19 | path: '/register', 20 | component: () => import('@/views/portal/register/Register.vue'), 21 | meta: { 22 | title: '注册', 23 | }, 24 | }, 25 | ] 26 | 27 | export default routes 28 | -------------------------------------------------------------------------------- /src/assets/icons/notice.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | 6 | 8 | -------------------------------------------------------------------------------- /src/assets/icons/article.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/router/modules/common.ts: -------------------------------------------------------------------------------- 1 | import { RouteRecordRaw } from 'vue-router' 2 | 3 | const routes: RouteRecordRaw[] = [ 4 | { 5 | path: '/not-found', 6 | component: () => import('@/views/common/not-found/NotFound.vue'), 7 | meta: { 8 | title: '页面不存在', 9 | activePath: '/home', 10 | }, 11 | }, 12 | { 13 | path: '/forbidden', 14 | component: () => import('@/views/common/forbidden/Forbidden.vue'), 15 | meta: { 16 | title: '无权限', 17 | activePath: '/home', 18 | }, 19 | }, 20 | { 21 | path: '/reload', 22 | component: () => import('@/views/common/reload/Reload.vue'), 23 | meta: {}, 24 | }, 25 | ] 26 | 27 | export default routes 28 | -------------------------------------------------------------------------------- /src/components/base/BaseTitle.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 14 | 15 | 35 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | import ElementPlus from 'element-plus' 3 | import 'element-plus/dist/index.css' 4 | import zhCn from 'element-plus/es/locale/lang/zh-cn' 5 | import message from '@/utils/message' 6 | import App from './App.vue' 7 | import router from './router' 8 | import store from './store' 9 | import { setupDirectives } from './directives' 10 | import './assets/styles/app.less' 11 | import './mock' 12 | 13 | window.$message = message 14 | 15 | const app = createApp(App) 16 | 17 | setupDirectives(app) 18 | app.use(router) 19 | app.use(store) 20 | app.use(ElementPlus, { 21 | locale: zhCn, 22 | size: 'large', 23 | }) 24 | 25 | app.mount('#app') 26 | -------------------------------------------------------------------------------- /src/assets/icons/user-fill.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 11 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | const colors = require('tailwindcss/colors') 2 | 3 | module.exports = { 4 | content: ['./src/**/*.{js,jsx,ts,tsx,vue}'], 5 | theme: { 6 | colors: { 7 | transparent: 'transparent', 8 | current: 'currentColor', 9 | white: colors.white, 10 | black: colors.black, 11 | gray: colors.gray, 12 | red: colors.red, 13 | yellow: colors.yellow, 14 | green: colors.green, 15 | blue: colors.blue, 16 | pink: colors.pink, 17 | primary: '#0084ff', 18 | warning: '#ffae00', 19 | danger: '#fe1e67', 20 | success: '#00caa2', 21 | info: { 22 | primary: '#333', 23 | regular: '#666', 24 | secondary: '#999', 25 | }, 26 | }, 27 | }, 28 | plugins: [], 29 | } 30 | -------------------------------------------------------------------------------- /src/store/modules/ui.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia' 2 | import _ from 'lodash' 3 | import { localMng } from '@/utils/storage-mng' 4 | import uiConfig from '@/config/ui' 5 | 6 | interface IState { 7 | // 是否折叠侧边菜单 8 | sideCollapse: boolean 9 | // 系统主题色 10 | theme: string 11 | } 12 | 13 | export const useUiStore = defineStore('ui', { 14 | state: (): IState => ({ 15 | sideCollapse: localMng.getItem('sideCollapse') || uiConfig.sideCollapse, 16 | theme: localMng.getItem('theme') || uiConfig.themeColor, 17 | }), 18 | getters: {}, 19 | actions: { 20 | setSideCollapse(collapse: boolean) { 21 | this.sideCollapse = collapse 22 | localMng.setItem('sideCollapse', collapse) 23 | }, 24 | }, 25 | }) 26 | -------------------------------------------------------------------------------- /src/views/home/components/ProgressBar.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 29 | -------------------------------------------------------------------------------- /src/assets/icons/menu-fold.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/assets/icons/menu-unfold.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/apis/modules/common.ts: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | // 获取用户信息 4 | export const getUserInfo = () => request.post('/system/user/info') 5 | export const getMenuList = () => request.post('/system/menu/list') 6 | 7 | // 获取验证码 8 | export const sendEmailCaptcha = (payload: any) => request.post('/system/captcha/sms', payload) 9 | export const sendSmsCaptcha = (payload: any) => request.post('/system/captcha/email', payload) 10 | 11 | // 校验验证码 12 | export const validateCaptcha = (payload: any) => request.post('/system/captcha/validate', payload) 13 | 14 | // 退出登录 15 | export const logout = () => request.post('/system/logout') 16 | 17 | // 上传图片 18 | export const uploadImage = (payload) => request.post('/system/image/upload', payload) 19 | -------------------------------------------------------------------------------- /src/assets/icons/search.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | 7 | 9 | 11 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "useDefineForClassFields": true, 5 | "module": "esnext", 6 | "moduleResolution": "node", 7 | "strict": true, 8 | "jsx": "preserve", 9 | "sourceMap": true, 10 | "resolveJsonModule": true, 11 | "esModuleInterop": true, 12 | "lib": ["esnext", "dom"], 13 | "noImplicitAny": false, 14 | "strictNullChecks": true, 15 | "types": ["element-plus/global", "unplugin-icons/types/vue", "node"], 16 | "baseUrl": ".", 17 | "paths": { 18 | "@/*": ["src/*"], 19 | "@img/*": ["src/assets/images/*"] 20 | } 21 | }, 22 | "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue", "auto-imports.d.ts", "components.d.ts"], 23 | "references": [{ "path": "./tsconfig.node.json" }] 24 | } 25 | -------------------------------------------------------------------------------- /src/router/modules/article.ts: -------------------------------------------------------------------------------- 1 | import { RouteRecordRaw } from 'vue-router' 2 | 3 | const routes: RouteRecordRaw[] = [ 4 | { 5 | name: 'Article', 6 | path: '/article', 7 | component: () => import('@/views/article/home/Home.vue'), 8 | meta: { 9 | title: '文章列表', 10 | activePath: '/article', 11 | }, 12 | }, 13 | { 14 | name: 'ArticleAdd', 15 | path: '/article/add', 16 | component: () => import('@/views/article/edit/Edit.vue'), 17 | meta: { 18 | title: '新增文章', 19 | activePath: '/article', 20 | }, 21 | }, 22 | { 23 | name: 'ArticleEdit', 24 | path: '/article/edit/:articleId', 25 | component: () => import('@/views/article/edit/Edit.vue'), 26 | meta: { 27 | title: '编辑文章', 28 | activePath: '/article', 29 | }, 30 | }, 31 | ] 32 | 33 | export default routes 34 | -------------------------------------------------------------------------------- /src/assets/icons/permission.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 9 | -------------------------------------------------------------------------------- /src/assets/icons/eye-close.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /src/components/base/BaseTag.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 18 | 19 | 44 | -------------------------------------------------------------------------------- /src/layouts/portal/Portal.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 15 | 16 | 38 | -------------------------------------------------------------------------------- /src/components/base/BaseEmpty.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 25 | 26 | 42 | -------------------------------------------------------------------------------- /src/assets/icons/tab.svg: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/assets/icons/role.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 9 | 11 | -------------------------------------------------------------------------------- /src/assets/icons/refresh.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | 6 | 7 | 8 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/mock/modules/portal.ts: -------------------------------------------------------------------------------- 1 | import Mock from 'mockjs' 2 | 3 | const login = (config) => { 4 | const data = Mock.mock({ 5 | token: '@lower(@guid)', 6 | }) 7 | return { 8 | code: 200, 9 | data: data.token, 10 | } 11 | } 12 | 13 | const logout = () => { 14 | return { 15 | code: 200, 16 | data: {}, 17 | } 18 | } 19 | 20 | const getCaptcha = () => { 21 | return { 22 | code: 200, 23 | data: {}, 24 | } 25 | } 26 | 27 | const register = () => { 28 | return { 29 | code: 200, 30 | body: {}, 31 | } 32 | } 33 | 34 | const modifyPassword = () => { 35 | return { 36 | code: 200, 37 | body: {}, 38 | } 39 | } 40 | 41 | Mock.mock(/\/system\/login/, 'post', login) 42 | Mock.mock(/\/system\/logout/, 'post', logout) 43 | Mock.mock(/\/system\/captcha/, 'post', getCaptcha) 44 | Mock.mock(/\/system\/register/, 'post', register) 45 | Mock.mock(/\/system\/password\/modify/, 'post', modifyPassword) 46 | -------------------------------------------------------------------------------- /src/views/home/components/NewGoods.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 31 | -------------------------------------------------------------------------------- /src/apis/modules/home.ts: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | export const getStatistics = () => request.post('/home/statistics') 4 | 5 | export const getVisitTrend = () => request.post('/home/chart/visit') 6 | export const getUserTrend = () => request.post('/home/chart/user') 7 | export const getGoodsTrend = () => request.post('/home/chart/goods') 8 | export const getCommentTrend = () => request.post('/home/chart/comment') 9 | 10 | export const getTaskList = () => request.post('/home/task/list') 11 | export const addTask = (payload) => request.post('/home/task/add', payload) 12 | export const editTask = (payload) => request.post('/home/task/edit', payload) 13 | export const deleteTask = (id: number) => request.post(`/home/task/delete/${id}`) 14 | export const finishTask = (payload) => request.post('/home/task/finish', payload) 15 | 16 | export const getGoodsList = () => request.post('/home/goods/list') 17 | -------------------------------------------------------------------------------- /src/components/business/ImageCaptcha.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 39 | 40 | -------------------------------------------------------------------------------- /src/views/home/Home.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 31 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 管理系统 13 | 14 | 15 | 16 |
17 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/layouts/platform/Platform.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 26 | 27 | 49 | -------------------------------------------------------------------------------- /src/assets/icons/message.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 11 | -------------------------------------------------------------------------------- /src/router/index.ts: -------------------------------------------------------------------------------- 1 | import { createRouter, createWebHashHistory, RouteRecordRaw } from 'vue-router' 2 | import _ from 'lodash' 3 | import { useAppStore } from '@/store' 4 | import { getTreeNodeValue } from '@/utils/core' 5 | 6 | import routes from './routes' 7 | import portalRoutes from './modules/portal' 8 | 9 | const initRouter = () => 10 | createRouter({ 11 | history: createWebHashHistory(), 12 | routes, 13 | scrollBehavior: () => ({ left: 0, top: 0 }), 14 | }) 15 | 16 | const router = initRouter() 17 | 18 | // 导航守卫 19 | router.beforeEach(async (to) => { 20 | const title = to.meta && (to.meta.title as string) 21 | if (title) { 22 | document.title = '管理系统-' + title 23 | } 24 | 25 | const appStore = useAppStore() 26 | const outerPaths = getTreeNodeValue(portalRoutes, 'path') 27 | 28 | if (!outerPaths.includes(to.path)) { 29 | if (!appStore.token) { 30 | return '/login' 31 | } 32 | if (!appStore.userInfo.id) { 33 | await Promise.all([appStore.getUserInfo(), appStore.getMenuList()]) 34 | } 35 | } 36 | }) 37 | 38 | export default router 39 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue3-element-ts-admin", 3 | "private": true, 4 | "version": "0.0.1", 5 | "scripts": { 6 | "serve": "vite", 7 | "build": " vite build", 8 | "preview": "vite preview" 9 | }, 10 | "dependencies": { 11 | "@antv/g2": "^4.1.50", 12 | "axios": "^0.26.1", 13 | "colord": "^2.9.2", 14 | "cropperjs": "^1.5.12", 15 | "crypto-js": "^4.1.1", 16 | "dayjs": "^1.11.0", 17 | "element-plus": "^2.3.3", 18 | "immutability-helper": "^3.1.1", 19 | "lodash": "^4.17.21", 20 | "mockjs": "^1.1.0", 21 | "pinia": "^2.0.13", 22 | "vue": "^3.2.38", 23 | "vue-router": "^4.0.14", 24 | "xlsx": "^0.18.5" 25 | }, 26 | "devDependencies": { 27 | "@types/node": "^17.0.23", 28 | "@vitejs/plugin-vue": "^2.3.0", 29 | "less": "^4.1.2", 30 | "less-loader": "^10.2.0", 31 | "postcss": "^8.4.21", 32 | "tailwindcss": "^3.2.4", 33 | "typescript": "^4.5.4", 34 | "unplugin-auto-import": "^0.12.0", 35 | "unplugin-icons": "^0.14.8", 36 | "unplugin-vue-components": "^0.18.5", 37 | "vite": "^4.1.1", 38 | "vue-tsc": "^0.29.8" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/router/routes.ts: -------------------------------------------------------------------------------- 1 | import { RouteRecordRaw } from 'vue-router' 2 | import portal from './modules/portal' 3 | import article from './modules/article' 4 | import blank from './modules/blank' 5 | import common from './modules/common' 6 | import home from './modules/home' 7 | import individule from './modules/individule' 8 | import more from './modules/more' 9 | import role from './modules/role' 10 | import setting from './modules/setting' 11 | import user from './modules/user' 12 | 13 | const routes: RouteRecordRaw[] = [ 14 | { 15 | path: '/', 16 | redirect: '/home', 17 | }, 18 | { 19 | path: '/platform', 20 | component: () => import('@/layouts/platform/Platform.vue'), 21 | redirect: '/home', 22 | children: [...article, ...blank, ...common, ...home, ...individule, ...more, ...role, ...setting, ...user], 23 | }, 24 | { 25 | path: '/portal', 26 | component: () => import('@/layouts/portal/Portal.vue'), 27 | redirect: '/login', 28 | children: portal, 29 | }, 30 | { 31 | path: '/:pathMatch(.*)*', 32 | redirect: '/not-found', 33 | }, 34 | ] 35 | 36 | export default routes 37 | -------------------------------------------------------------------------------- /src/components/base/BaseBack.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 29 | 30 | 55 | -------------------------------------------------------------------------------- /src/store/modules/enum.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 管理需要通过独立的接口获取的枚举 3 | */ 4 | import { defineStore } from 'pinia' 5 | import apis from '@/apis' 6 | import { enumMng, EnumResult } from '@/utils/core' 7 | 8 | interface IItem { 9 | id: number 10 | name: string 11 | } 12 | 13 | interface IState { 14 | enumUser: EnumResult 15 | enumRole: EnumResult 16 | } 17 | export const useEnumStore = defineStore('enum', { 18 | state: (): IState => ({ 19 | enumUser: {} as EnumResult, 20 | enumRole: {} as EnumResult, 21 | }), 22 | actions: { 23 | initEnum() { 24 | this.getUserList() 25 | this.getRoleList() 26 | }, 27 | // 获取用户列表 28 | async getUserList() { 29 | const data = await apis.user.getAllUserList() 30 | const userList = data.map((item) => ({ 31 | id: item.id, 32 | name: item.name, 33 | })) 34 | this.enumUser = enumMng(userList) 35 | }, 36 | // 获取角色列表 37 | async getRoleList() { 38 | const data = await apis.role.getAllRoleList() 39 | const roleList = data.map((item) => ({ 40 | id: item.id, 41 | name: item.name, 42 | })) 43 | this.enumRole = enumMng(roleList) 44 | }, 45 | }, 46 | }) 47 | -------------------------------------------------------------------------------- /src/mock/util.ts: -------------------------------------------------------------------------------- 1 | import _ from 'lodash' 2 | 3 | /** 4 | * 过滤对象的属性,数据表中有些字段前端不需要,可以使用此方法过滤掉 5 | * @param fields 需要的字段组成的数组 6 | */ 7 | const pickFromRow = (row, fields) => { 8 | const keys = Object.keys(row) 9 | const newRow = {} 10 | keys.forEach((key) => { 11 | if (fields.includes(key)) { 12 | newRow[key] = row[key] 13 | } 14 | }) 15 | return newRow 16 | } 17 | 18 | const pickFromTable = (table, fields) => { 19 | return table.map((row) => pickFromRow(row, fields)) 20 | } 21 | 22 | /** 23 | * 新增/修改 24 | */ 25 | const update = (table, row) => { 26 | if (row.id) { 27 | const index = table.findIndex((item) => item.id === row.id) 28 | Object.assign(table[index], row) 29 | } else { 30 | row.id = _.uniqueId() 31 | table.unshift(row) 32 | } 33 | } 34 | 35 | /** 36 | * 单个删除/批量删除 37 | * @param {Array} ids 要删除的项的id组成的数组 38 | */ 39 | const remove = (table, ids) => { 40 | ids.forEach((id) => { 41 | const index = table.findIndex((row) => row.id === id) 42 | table.splice(index, 1) 43 | }) 44 | } 45 | 46 | /** 47 | * 根据id查找表的某一项 48 | */ 49 | const find = (table, id) => { 50 | return table.find((row) => row.id === id) 51 | } 52 | 53 | export default { 54 | pickFromRow, 55 | pickFromTable, 56 | update, 57 | remove, 58 | find, 59 | } 60 | -------------------------------------------------------------------------------- /src/components/base/Avatar.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 24 | 25 | 60 | -------------------------------------------------------------------------------- /src/layouts/platform/components/UserInfo.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 34 | 35 | 49 | 54 | -------------------------------------------------------------------------------- /src/assets/styles/reset.less: -------------------------------------------------------------------------------- 1 | body, 2 | div, 3 | dl, 4 | dt, 5 | dd, 6 | ul, 7 | ol, 8 | li, 9 | h1, 10 | h2, 11 | h3, 12 | h4, 13 | h5, 14 | h6, 15 | pre, 16 | code, 17 | form, 18 | fieldset, 19 | legend, 20 | input, 21 | textarea, 22 | p, 23 | blockquote, 24 | th, 25 | td, 26 | hr, 27 | button, 28 | article, 29 | aside, 30 | details, 31 | figcaption, 32 | figure, 33 | footer, 34 | header, 35 | hgroup, 36 | menu, 37 | nav, 38 | section { 39 | margin: 0; 40 | padding: 0; 41 | } 42 | 43 | body { 44 | font-size: 14px; 45 | font-family: 'Microsoft YaHei', Verdana, Arial, Helvetica, sans-serif; 46 | color: @info-primary; 47 | } 48 | 49 | a { 50 | text-decoration: none; 51 | } 52 | 53 | ol, 54 | ul { 55 | list-style: none; 56 | } 57 | 58 | button, 59 | input, 60 | select, 61 | textarea { 62 | font-family: inherit; 63 | font-size: inherit; 64 | line-height: inherit; 65 | color: inherit; 66 | } 67 | svg { 68 | display: inline-block; 69 | } 70 | 71 | // 滚动条整体宽度 72 | ::-webkit-scrollbar { 73 | width: 6px; 74 | height: 6px; 75 | } 76 | 77 | // 滚动条样式 78 | ::-webkit-scrollbar-thumb { 79 | border-radius: 6px; 80 | background-color: rgba(0, 0, 0, 0.06); 81 | box-shadow: inset 0 0 3px rgba(0, 0, 0, 0.03); 82 | } 83 | 84 | // 滚动条滑槽样式 85 | ::-webkit-scrollbar-track { 86 | background-color: transparent; 87 | border-radius: 8px; 88 | } 89 | -------------------------------------------------------------------------------- /src/assets/icons/view.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 11 | -------------------------------------------------------------------------------- /src/assets/icons/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 11 | 14 | -------------------------------------------------------------------------------- /src/utils/crypto.ts: -------------------------------------------------------------------------------- 1 | import CryptoJS from 'crypto-js' 2 | 3 | // DES加密 4 | export const encryptByDES = (message: string, key: string) => { 5 | const keyHex = CryptoJS.enc.Utf8.parse(key) 6 | const encrypted = CryptoJS.DES.encrypt(message, keyHex, { 7 | mode: CryptoJS.mode.ECB, 8 | padding: CryptoJS.pad.Pkcs7, 9 | }) 10 | return encrypted.toString() 11 | } 12 | 13 | // DES解密 14 | export const decryptByDES = (message: string, key: string) => { 15 | var keyHex = CryptoJS.enc.Utf8.parse(key) 16 | var decrypted = CryptoJS.DES.decrypt(message, keyHex, { 17 | mode: CryptoJS.mode.ECB, 18 | padding: CryptoJS.pad.Pkcs7, 19 | }) 20 | return decrypted.toString(CryptoJS.enc.Utf8) 21 | } 22 | 23 | // AES加密 24 | // 使用AES秘钥必须为:8/16/32位 25 | export const encryptByAES = (message: string, key: string) => { 26 | var keyHex = CryptoJS.enc.Utf8.parse(key) 27 | var encrypted = CryptoJS.AES.encrypt(message, keyHex, { 28 | mode: CryptoJS.mode.ECB, 29 | padding: CryptoJS.pad.Pkcs7, 30 | }) 31 | return encrypted.toString() 32 | } 33 | 34 | // AES解密 35 | export const decryptByAES = (message: string, key: string) => { 36 | var keyHex = CryptoJS.enc.Utf8.parse(key) 37 | var decrypted = CryptoJS.AES.decrypt(message, keyHex, { 38 | mode: CryptoJS.mode.ECB, 39 | padding: CryptoJS.pad.Pkcs7, 40 | }) 41 | return decrypted.toString(CryptoJS.enc.Utf8) 42 | } 43 | -------------------------------------------------------------------------------- /src/assets/icons/setting.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 9 | -------------------------------------------------------------------------------- /src/utils/event-bus.ts: -------------------------------------------------------------------------------- 1 | import { tupleStr } from './type' 2 | 3 | // 项目中所有自定义事件名称 4 | const eventName = tupleStr( 5 | // 6 | 'changeEvent' 7 | ) 8 | 9 | type EventName = typeof eventName[number] 10 | 11 | class Event { 12 | public eventName: EventName 13 | public handler: (...args: any[]) => void 14 | 15 | constructor(eventName: EventName, handler: (...args: any[]) => void) { 16 | this.eventName = eventName 17 | this.handler = handler 18 | } 19 | } 20 | 21 | class EventBus { 22 | private events: Set = new Set() 23 | 24 | public on(eventName: EventName, handler: (...args: any[]) => void) { 25 | const event = new Event(eventName, handler) 26 | this.events.add(event) 27 | return () => this.events.delete(event) 28 | } 29 | 30 | public off(eventName: EventName) { 31 | this.events.forEach((event) => { 32 | if (eventName === event.eventName) { 33 | this.events.delete(event) 34 | } 35 | }) 36 | } 37 | 38 | public emit(eventName: EventName, ...args: any[]) { 39 | this.events.forEach((event) => { 40 | if (eventName === event.eventName) { 41 | event.handler(...args) 42 | } 43 | }) 44 | } 45 | } 46 | 47 | const eventBus = new EventBus() 48 | 49 | /** 50 | * @example 51 | 52 | ``` 53 | useEffect(() => { 54 | const removeHandler = eventBus.on('changeMode', () => {}) 55 | return removeHandler 56 | }, []) 57 | ``` 58 | */ 59 | 60 | export default eventBus 61 | -------------------------------------------------------------------------------- /src/utils/message.ts: -------------------------------------------------------------------------------- 1 | import { h, AppContext, isVNode } from 'vue' 2 | import { ElMessage, MessageParams } from 'element-plus' 3 | import IconLoading from '~icons/custom/loading' 4 | 5 | const info = (options?: MessageParams, appContext?: AppContext | null) => { 6 | ElMessage.closeAll() 7 | return ElMessage.info(options, appContext) 8 | } 9 | 10 | const success = (options?: MessageParams, appContext?: AppContext | null) => { 11 | ElMessage.closeAll() 12 | return ElMessage.success(options, appContext) 13 | } 14 | 15 | const warning = (options?: MessageParams, appContext?: AppContext | null) => { 16 | ElMessage.closeAll() 17 | return ElMessage.warning(options, appContext) 18 | } 19 | 20 | const error = (options?: MessageParams, appContext?: AppContext | null) => { 21 | ElMessage.closeAll() 22 | return ElMessage.error(options, appContext) 23 | } 24 | 25 | const loading = (options?: MessageParams, appContext?: AppContext | null) => { 26 | ElMessage.closeAll() 27 | let params: MessageParams | undefined = undefined 28 | if (typeof options === 'string' || isVNode(options)) { 29 | params = { 30 | message: options, 31 | } 32 | } else { 33 | params = options 34 | } 35 | 36 | return ElMessage( 37 | { 38 | icon: h(IconLoading, { spin: true }), 39 | ...params, 40 | }, 41 | appContext 42 | ) 43 | } 44 | 45 | export default { 46 | info, 47 | success, 48 | warning, 49 | error, 50 | loading, 51 | } 52 | -------------------------------------------------------------------------------- /src/layouts/platform/components/MenuItem.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 47 | -------------------------------------------------------------------------------- /src/components/base/BaseIcon.vue: -------------------------------------------------------------------------------- 1 | 36 | 37 | 42 | 43 | 71 | -------------------------------------------------------------------------------- /src/components/base/BaseConfirm.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 52 | 53 | 76 | -------------------------------------------------------------------------------- /src/components/base/BaseDot.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 34 | 35 | 76 | -------------------------------------------------------------------------------- /src/hooks/business/useSmsCaptcha.ts: -------------------------------------------------------------------------------- 1 | import { ref, computed, onBeforeUnmount } from 'vue' 2 | import apis from '@/apis' 3 | import { useCountDown } from '@/hooks/base' 4 | 5 | /** 6 | * 发送短信验证码 7 | */ 8 | const useCaptcha = () => { 9 | // 禁用发送按钮 10 | const captchaDisabled = ref(false) 11 | 12 | const { count, startCount, resetCount } = useCountDown({ 13 | time: 6000, 14 | onFinish() { 15 | resetCaptcha() 16 | }, 17 | }) 18 | 19 | const captchaCount = computed(() => count.value.seconds) 20 | 21 | const sendCaptcha = async (params: { mobile?: string; email?: string }) => { 22 | try { 23 | captchaDisabled.value = true 24 | if (params.email) { 25 | await apis.common.sendEmailCaptcha({ 26 | email: params.email, 27 | }) 28 | } else if (params.mobile) { 29 | await apis.common.sendSmsCaptcha({ 30 | mobile: params.mobile, 31 | }) 32 | } 33 | window.$message.success('验证码已发送') 34 | startCount() 35 | } catch (err) { 36 | captchaDisabled.value = false 37 | console.error(err) 38 | } 39 | } 40 | 41 | // 校验验证码 42 | const validateCaptcha = async (params: { mobile?: string; email?: string; code: string }) => { 43 | const captchaToken = await apis.common.validateCaptcha(params) 44 | return captchaToken 45 | } 46 | 47 | const resetCaptcha = () => { 48 | captchaDisabled.value = false 49 | resetCount() 50 | } 51 | 52 | onBeforeUnmount(() => { 53 | resetCaptcha() 54 | }) 55 | 56 | return { 57 | captchaDisabled, 58 | captchaCount, 59 | sendCaptcha, 60 | validateCaptcha, 61 | resetCaptcha, 62 | } 63 | } 64 | 65 | export default useCaptcha 66 | -------------------------------------------------------------------------------- /src/assets/icons/goods.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 11 | -------------------------------------------------------------------------------- /src/mock/modules/user.ts: -------------------------------------------------------------------------------- 1 | import Mock from 'mockjs' 2 | 3 | const userData = Mock.mock({ 4 | 'list|127': [ 5 | { 6 | id: '@lower(@guid)', 7 | name: '@cname', 8 | mobile: /^1[345789]\d{9}$/, 9 | gender: '@pick(["1", "2"])', 10 | status: '@pick(["0", "1"])', 11 | age: '@natural(20,60)', 12 | role: '@pick([1, 2])', 13 | roleName: '@pick(["管理员", "游客"])', 14 | createTime: '@datetime("yyyy-MM-dd HH:mm:ss")', 15 | consume: '@natural(0,10000)', 16 | username: /^[a-zA-Z0-9_]{5,10}$/, 17 | avatar: 'https://picsum.photos/200/200/?random', 18 | email: '@email', 19 | }, 20 | ], 21 | }) 22 | 23 | const getList = (config) => { 24 | const { page } = JSON.parse(config.body) 25 | const startNumber = (page.number - 1) * page.size 26 | const endNumber = startNumber + page.size 27 | const list = userData.list 28 | return { 29 | code: 200, 30 | data: { 31 | list: list.slice(startNumber, endNumber), 32 | total: list.length, 33 | }, 34 | } 35 | } 36 | 37 | const getAllUserList = () => { 38 | return { 39 | code: 200, 40 | data: userData.list, 41 | } 42 | } 43 | 44 | const getDetail = (config) => { 45 | return { 46 | code: 200, 47 | data: userData.list[0], 48 | } 49 | } 50 | 51 | const edit = () => { 52 | return { 53 | code: 200, 54 | data: {}, 55 | } 56 | } 57 | 58 | const remove = () => { 59 | return { 60 | code: 200, 61 | data: {}, 62 | } 63 | } 64 | 65 | Mock.mock(/\/user\/list/, 'post', getList) 66 | Mock.mock(/\/user\/all/, 'post', getAllUserList) 67 | Mock.mock(/\/user\/detail\/[a-z0-9]/, 'post', getDetail) 68 | Mock.mock(/\/user\/edit/, 'post', edit) 69 | Mock.mock(/\/user\/delete\/[a-z0-9]*/, 'post', remove) 70 | -------------------------------------------------------------------------------- /src/hooks/base/useConfirm.ts: -------------------------------------------------------------------------------- 1 | import { h, render } from 'vue' 2 | import BaseConfirm from '@/components/base/BaseConfirm.vue' 3 | 4 | const container = document.createElement('div') 5 | 6 | interface IOptions { 7 | type: Status 8 | title: string 9 | message: string | ((data: T) => void) 10 | description: string | ((data: T) => void) 11 | onConfirm: (data: T) => Promise 12 | onClose: () => void 13 | } 14 | 15 | const useConfirm = (options: Partial>) => { 16 | const handler = (data: T) => { 17 | const { type, title = '提示', message, description, onConfirm, onClose } = options 18 | 19 | const handleClose = () => { 20 | onClose && onClose() 21 | render(null, container) 22 | } 23 | 24 | const getMessage = (data: T) => { 25 | if (!message) return '' 26 | if (typeof message === 'string') { 27 | return message 28 | } 29 | return message(data) 30 | } 31 | 32 | const getDescription = (data: T) => { 33 | if (!description) return '' 34 | if (typeof description === 'string') { 35 | return description 36 | } 37 | return description(data) 38 | } 39 | 40 | const confirmNode = h(BaseConfirm, { 41 | type, 42 | title, 43 | visible: true, 44 | message: getMessage(data), 45 | description: getDescription(data), 46 | async onConfirm() { 47 | const props = confirmNode.component?.props! 48 | 49 | props.confirmLoading = true 50 | if (onConfirm) { 51 | await onConfirm(data) 52 | } 53 | props.confirmLoading = false 54 | handleClose() 55 | }, 56 | onClose: handleClose, 57 | }) 58 | 59 | render(confirmNode, container) 60 | } 61 | 62 | return handler 63 | } 64 | 65 | export default useConfirm 66 | -------------------------------------------------------------------------------- /src/views/individual/components/BaseInfo.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## 项目简介 2 | 3 | 后台管理系统。 4 | 5 | ## 技术依赖 6 | 7 | 主体:Vue、ElementPlus、TypeScript、Vite 8 | 9 | ## 功能 10 | 11 | - 登录/退出 12 | - 全屏浏览 13 | - 一键换肤 14 | - 系统风格 15 | - 元素大小 16 | - 个人中心 17 | - 侧边菜单 18 | - 标签导航 19 | - 图表 20 | - 折线图 21 | - 面积图 22 | - 柱状图 23 | - 条形图 24 | - 饼图 25 | - 散点图 26 | - 表单 27 | - 基础表单 28 | - 步骤表单 29 | - 动态表单 30 | - 表格 31 | - Tab 选项卡 32 | - 权限控制 33 | - 用户管理 34 | - 文章管理 35 | - 创建文章 36 | - 文章列表 37 | - pdf 38 | - 上传 39 | - 头像上传 40 | - 文件上传 41 | - 错误处理 42 | - 403 43 | - 404 44 | - 其他功能 45 | - 导入/导出 excel 46 | - 滚动条 47 | - 打印 48 | - html2canvas 49 | - 拖拽 Dialog 50 | - 地图 51 | - 快捷复制 52 | - 文本溢出 53 | 54 | ## 目录结构 55 | 56 | ``` 57 | 58 | |-- public 59 | |-- src 源码 60 | | |-- @types 全局类型定义 61 | | |-- apis 接口 62 | | |-- assets 静态资源文件 63 | | |-- images 图片 64 | | |-- icons 图标 65 | | |-- styles 样式 66 | | |-- components 组件 67 | | |-- base 基础组件 68 | | |-- business 业务组件 69 | | |-- directive 通用指令 70 | | |-- layouts 基础布局 71 | | |-- platform 平台布局 72 | | |-- portal 门户布局 73 | | |-- mock 数据模拟 74 | | |-- views 页面 75 | | |-- router 路由管理 76 | | |-- store 状态管理 77 | | |-- utils 全局公用方法 78 | | |-- App.vue 根组件 79 | | |-- main.ts 入口文件 80 | ``` 81 | 82 | ## 使用 83 | 84 | #### 安装依赖 85 | 86 | ``` 87 | yarn 88 | ``` 89 | 90 | #### 运行 91 | 92 | ``` 93 | yarn serve 94 | ``` 95 | 96 | #### 构建 97 | 98 | ``` 99 | yarn build 100 | ``` 101 | -------------------------------------------------------------------------------- /components.d.ts: -------------------------------------------------------------------------------- 1 | // generated by unplugin-vue-components 2 | // We suggest you to commit this file into source control 3 | // Read more: https://github.com/vuejs/vue-next/pull/3399 4 | 5 | declare module 'vue' { 6 | export interface GlobalComponents { 7 | Avatar: typeof import('./src/components/base/Avatar.vue')['default'] 8 | AvatarUpload: typeof import('./src/components/business/AvatarUpload.vue')['default'] 9 | BaseBack: typeof import('./src/components/base/BaseBack.vue')['default'] 10 | BaseButton: typeof import('./src/components/base/BaseButton.vue')['default'] 11 | BaseConfirm: typeof import('./src/components/base/BaseConfirm.vue')['default'] 12 | BaseDialog: typeof import('./src/components/base/BaseDialog.vue')['default'] 13 | BaseDot: typeof import('./src/components/base/BaseDot.vue')['default'] 14 | BaseDrawer: typeof import('./src/components/base/BaseDrawer.vue')['default'] 15 | BaseEmpty: typeof import('./src/components/base/BaseEmpty.vue')['default'] 16 | BaseIcon: typeof import('./src/components/base/BaseIcon.vue')['default'] 17 | BaseTag: typeof import('./src/components/base/BaseTag.vue')['default'] 18 | BaseTitle: typeof import('./src/components/base/BaseTitle.vue')['default'] 19 | CountTo: typeof import('./src/components/base/CountTo.vue')['default'] 20 | IconCustomLock: typeof import('~icons/custom/lock')['default'] 21 | IconCustomLogo: typeof import('~icons/custom/logo')['default'] 22 | IconCustomUser: typeof import('~icons/custom/user')['default'] 23 | ImageCaptcha: typeof import('./src/components/business/ImageCaptcha.vue')['default'] 24 | Overflow: typeof import('./src/components/base/Overflow.vue')['default'] 25 | Pagination: typeof import('./src/components/base/Pagination.vue')['default'] 26 | } 27 | } 28 | 29 | export { } 30 | -------------------------------------------------------------------------------- /src/layouts/platform/components/SideBar.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 36 | 37 | 73 | -------------------------------------------------------------------------------- /src/layouts/platform/components/HeaderBar.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 43 | 44 | 79 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import Vue from '@vitejs/plugin-vue' 3 | import Components from 'unplugin-vue-components/vite' 4 | import Icons from 'unplugin-icons/vite' 5 | import IconsResolver from 'unplugin-icons/resolver' 6 | import { FileSystemIconLoader } from 'unplugin-icons/loaders' 7 | import AutoImport from 'unplugin-auto-import/vite' 8 | 9 | const path = require('path') 10 | const resolve = (dir: string) => path.resolve(process.cwd(), dir) 11 | 12 | export default defineConfig({ 13 | base: './', 14 | plugins: [ 15 | Vue(), 16 | Icons({ 17 | compiler: 'vue3', 18 | customCollections: { 19 | custom: FileSystemIconLoader(resolve('src/assets/icons')), 20 | }, 21 | }), 22 | AutoImport({ 23 | imports: ['vue', 'vue-router'], 24 | }), 25 | Components({ 26 | dts: true, 27 | resolvers: [ 28 | IconsResolver({ 29 | prefix: 'icon', 30 | customCollections: ['custom'], 31 | }), 32 | ], 33 | }), 34 | ], 35 | resolve: { 36 | alias: { 37 | '@': resolve('src'), 38 | '@img': resolve('src/assets/images'), 39 | }, 40 | dedupe: ['vue'], 41 | }, 42 | css: { 43 | postcss: { 44 | plugins: [require('tailwindcss')('tailwind.config.js')], 45 | }, 46 | preprocessorOptions: { 47 | less: { 48 | javascriptEnabled: true, 49 | globalVars: { 50 | primary: '#409EFF', 51 | warning: '#E6A23C', 52 | danger: '#F56C6C', 53 | success: '#67C23A', 54 | 'info-primary': '#333', 55 | 'info-regular': '#666', 56 | 'info-secondary': '#999', 57 | }, 58 | }, 59 | }, 60 | }, 61 | server: { 62 | open: true, 63 | proxy: { 64 | '/api': { 65 | target: '', 66 | changeOrigin: true, 67 | ws: true, 68 | rewrite: (path) => path.replace(/^\/api/, ''), 69 | }, 70 | }, 71 | }, 72 | build: { 73 | rollupOptions: { 74 | output: { 75 | manualChunks: { 76 | vue: ['vue', 'vue-router', 'pinia'], 77 | lodash: ['lodash'], 78 | element: ['element-plus'], 79 | }, 80 | chunkFileNames: 'js/[name]-[hash].js', 81 | entryFileNames: 'js/[name]-[hash].js', 82 | assetFileNames: '[ext]/[name]-[hash].[ext]', 83 | }, 84 | }, 85 | }, 86 | }) 87 | -------------------------------------------------------------------------------- /src/components/base/Pagination.vue: -------------------------------------------------------------------------------- 1 | 45 | 46 | 59 | 60 | 79 | 80 | 100 | -------------------------------------------------------------------------------- /src/utils/storage-mng.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 本地存储的读取往往分散在各个不同的地方,会显得很乱。 3 | * 使用本地存储的时候推荐统一采用该,同时在这里记录每个key和它的作用。 4 | */ 5 | 6 | import { tupleStr } from '@/utils/type' 7 | 8 | import { TokenName } from '@/config/constant' 9 | 10 | // 项目中所有存储在localStorage中的数据 11 | const localKeys = tupleStr( 12 | // token 13 | TokenName, 14 | // 是否折叠侧边菜单 15 | 'sideCollapse', 16 | // 主题色 17 | 'theme' 18 | ) 19 | 20 | // 项目中所有存在sessionStorage中的数据的名称 21 | const sessionKeys = tupleStr() 22 | 23 | type localKeyName = typeof localKeys[number] 24 | type sessionKeyName = typeof sessionKeys[number] 25 | type keyName = localKeyName | sessionKeyName 26 | 27 | class StorageMng { 28 | // key名称前缀 29 | private prefix: string 30 | // 使用localStorage还是sessionStorage 31 | private mode: Storage 32 | 33 | constructor(mode: Storage, prefix: string = '') { 34 | this.prefix = prefix 35 | this.mode = mode 36 | } 37 | 38 | public setItem(key: keyName, value: any) { 39 | try { 40 | this.mode.setItem(`${this.prefix}${key}`, window.JSON.stringify(value)) 41 | } catch (err) { 42 | console.warn(`Storage ${key} set error`, err) 43 | } 44 | } 45 | 46 | public getItem(key: keyName) { 47 | const result = this.mode.getItem(`${this.prefix}${key}`) 48 | try { 49 | return result ? window.JSON.parse(result) : result 50 | } catch (err) { 51 | console.warn(`Storage ${key} get error`, err) 52 | } 53 | } 54 | 55 | public removeItem(key: keyName) { 56 | this.mode.removeItem(`${this.prefix}${key}`) 57 | } 58 | 59 | public clear() { 60 | this.mode.clear() 61 | } 62 | 63 | public getKey(index: number) { 64 | return this.getKeys()[index] 65 | } 66 | 67 | // 获取所有数据的名称 68 | public getKeys() { 69 | const keys: keyName[] = [] 70 | Array.from({ length: this.mode.length }).forEach((item, index) => { 71 | const key = this.mode.key(index) 72 | if (key?.startsWith(this.prefix)) { 73 | keys.push(key.slice(this.prefix.length) as keyName) 74 | } 75 | }) 76 | return keys 77 | } 78 | 79 | // 获取所有数据 80 | public getAll() { 81 | return Object.fromEntries(this.getKeys().map((key) => [key, this.getItem(key)])) 82 | } 83 | } 84 | 85 | const localMng = new StorageMng(localStorage) 86 | const sessionMng = new StorageMng(sessionStorage) 87 | 88 | export { StorageMng, localMng, sessionMng } 89 | -------------------------------------------------------------------------------- /src/components/base/Overflow.vue: -------------------------------------------------------------------------------- 1 | 61 | 62 | 79 | 80 | 95 | -------------------------------------------------------------------------------- /src/views/more/file-to-base64/FileToBase64.vue: -------------------------------------------------------------------------------- 1 | 46 | 47 | 58 | 59 | 84 | -------------------------------------------------------------------------------- /src/components/base/CountTo.vue: -------------------------------------------------------------------------------- 1 | 84 | 85 | 88 | -------------------------------------------------------------------------------- /src/hooks/business/useImageCaptcha.ts: -------------------------------------------------------------------------------- 1 | import { ref, onMounted } from 'vue' 2 | 3 | function randomNum(min: number, max: number) { 4 | const num = Math.floor(Math.random() * (max - min) + min) 5 | return num 6 | } 7 | 8 | function randomColor(min: number, max: number) { 9 | const r = randomNum(min, max) 10 | const g = randomNum(min, max) 11 | const b = randomNum(min, max) 12 | return `rgb(${r},${g},${b})` 13 | } 14 | 15 | function draw(dom: HTMLCanvasElement, width: number, height: number) { 16 | let imgCode = '' 17 | 18 | const NUMBER_STRING = '0123456789' 19 | 20 | const ctx = dom.getContext('2d') 21 | if (!ctx) return imgCode 22 | 23 | ctx.fillStyle = randomColor(180, 230) 24 | ctx.fillRect(0, 0, width, height) 25 | for (let i = 0; i < 4; i += 1) { 26 | const text = NUMBER_STRING[randomNum(0, NUMBER_STRING.length)] 27 | imgCode += text 28 | const fontSize = randomNum(18, 41) 29 | const deg = randomNum(-30, 30) 30 | ctx.font = `${fontSize}px Simhei` 31 | ctx.textBaseline = 'top' 32 | ctx.fillStyle = randomColor(80, 150) 33 | ctx.save() 34 | ctx.translate(30 * i + 23, 15) 35 | ctx.rotate((deg * Math.PI) / 180) 36 | ctx.fillText(text, -15 + 5, -15) 37 | ctx.restore() 38 | } 39 | for (let i = 0; i < 5; i += 1) { 40 | ctx.beginPath() 41 | ctx.moveTo(randomNum(0, width), randomNum(0, height)) 42 | ctx.lineTo(randomNum(0, width), randomNum(0, height)) 43 | ctx.strokeStyle = randomColor(180, 230) 44 | ctx.closePath() 45 | ctx.stroke() 46 | } 47 | for (let i = 0; i < 41; i += 1) { 48 | ctx.beginPath() 49 | ctx.arc(randomNum(0, width), randomNum(0, height), 1, 0, 2 * Math.PI) 50 | ctx.closePath() 51 | ctx.fillStyle = randomColor(150, 200) 52 | ctx.fill() 53 | } 54 | return imgCode 55 | } 56 | 57 | /** 58 | * 绘制图形验证码 59 | * @param width - 图形宽度 60 | * @param height - 图形高度 61 | */ 62 | const useImageCaptcha = (width = 152, height = 40) => { 63 | const domRef = ref() 64 | const imgCode = ref('') 65 | 66 | function setImgCode(code: string) { 67 | imgCode.value = code 68 | } 69 | 70 | function getImgCode() { 71 | if (!domRef.value) return 72 | imgCode.value = draw(domRef.value, width, height) 73 | } 74 | 75 | onMounted(() => { 76 | getImgCode() 77 | }) 78 | 79 | return { 80 | domRef, 81 | imgCode, 82 | setImgCode, 83 | getImgCode, 84 | } 85 | } 86 | 87 | export default useImageCaptcha 88 | -------------------------------------------------------------------------------- /src/views/individual/components/BaseEdit.vue: -------------------------------------------------------------------------------- 1 | 71 | 72 | 92 | -------------------------------------------------------------------------------- /src/store/modules/app.ts: -------------------------------------------------------------------------------- 1 | import { h } from 'vue' 2 | import { defineStore } from 'pinia' 3 | import _ from 'lodash' 4 | import apis from '@/apis' 5 | import router from '@/router' 6 | import store from '@/store' 7 | import { TokenName } from '@/config/constant' 8 | import request from '@/utils/request' 9 | import { getTreeNodeValue } from '@/utils/core' 10 | import { localMng, sessionMng } from '@/utils/storage-mng' 11 | import { IUser, IMenu } from '@/model/common' 12 | 13 | interface IState { 14 | token: string 15 | userInfo: IUser 16 | menuList: IMenu[] 17 | permissions: string[] 18 | } 19 | 20 | export const useAppStore = defineStore('app', { 21 | state: (): IState => ({ 22 | token: localMng.getItem(TokenName), 23 | userInfo: {} as IUser, 24 | menuList: [], 25 | permissions: [], 26 | }), 27 | actions: { 28 | setToken(token: string) { 29 | this.token = token 30 | }, 31 | // 登录 32 | async login(payload) { 33 | const data = await apis.portal.login(payload) 34 | this.token = data 35 | localMng.setItem(TokenName, data) 36 | request.setHeader({ 37 | Authorization: data, 38 | }) 39 | router.replace('/') 40 | }, 41 | // 获取用户信息 42 | async getUserInfo() { 43 | const data = await apis.common.getUserInfo() 44 | this.userInfo = data 45 | }, 46 | // 获取菜单和权限 47 | async getMenuList() { 48 | const data = await apis.common.getMenuList() 49 | this.menuList = getMenuList(data) 50 | this.permissions = getPermissions(data) 51 | }, 52 | // 退出 53 | async logout(isRequest = true) { 54 | if (isRequest) { 55 | await apis.common.logout() 56 | } 57 | router.replace('/login') 58 | sessionMng.clear() 59 | localMng.removeItem(TokenName) 60 | request.setHeader({ 61 | Authorization: '', 62 | }) 63 | this.$reset() 64 | }, 65 | }, 66 | }) 67 | 68 | // 获取菜单 69 | const getMenuList = (menus: IMenu[]) => 70 | _.cloneDeep(menus).filter((menu) => { 71 | if (menu.type === 'menu') { 72 | if (menu.children) { 73 | const children = getMenuList(menu.children) 74 | menu.children = children.length > 0 ? children : undefined 75 | } 76 | return true 77 | } else { 78 | return false 79 | } 80 | }) 81 | 82 | // 获取用户拥有的权限 83 | const getPermissions = (menus: IMenu[]) => 84 | getTreeNodeValue(menus, 'permission').reduce( 85 | (accumulate: string[], current: string) => accumulate.concat(current), 86 | [] 87 | ) 88 | -------------------------------------------------------------------------------- /src/mock/modules/common.ts: -------------------------------------------------------------------------------- 1 | import Mock from 'mockjs' 2 | 3 | const userInfo = { 4 | id: 1, 5 | name: '@cname', 6 | gender: '@pick(["1", "2"])', 7 | avatar: 'https://s2.ax1x.com/2019/08/02/edRc1P.jpg', 8 | email: '@email', 9 | phone: /^1[3456789]\d{9}$/, 10 | roles: [ 11 | { 12 | id: 'admin', 13 | name: '管理员', 14 | }, 15 | ], 16 | } 17 | 18 | export const menuList = [ 19 | { 20 | id: 1, 21 | name: '首页', 22 | type: 'menu', 23 | icon: 'home', 24 | path: '/home', 25 | }, 26 | { 27 | id: 2, 28 | name: '空白页', 29 | type: 'menu', 30 | icon: 'blank', 31 | path: '/blank', 32 | }, 33 | { 34 | id: 3, 35 | name: '用户管理', 36 | type: 'menu', 37 | icon: 'user', 38 | path: '/user', 39 | children: [ 40 | { 41 | id: 31, 42 | name: '新增用户', 43 | type: 'component', 44 | permission: 'userAdd', 45 | }, 46 | { 47 | id: 32, 48 | name: '编辑用户', 49 | type: 'component', 50 | permission: 'userEdit', 51 | }, 52 | { 53 | id: 33, 54 | name: '删除用户', 55 | type: 'component', 56 | permission: 'userDelete', 57 | }, 58 | { 59 | id: 34, 60 | name: '导出用户', 61 | type: 'component', 62 | permission: 'userExport', 63 | }, 64 | ], 65 | }, 66 | { 67 | id: 4, 68 | name: '文章管理', 69 | type: 'menu', 70 | icon: 'article', 71 | path: '/article', 72 | }, 73 | { 74 | id: 5, 75 | name: '角色管理', 76 | type: 'menu', 77 | icon: 'role', 78 | path: '/role', 79 | }, 80 | { 81 | id: 10, 82 | name: '其他功能', 83 | type: 'menu', 84 | icon: 'more', 85 | path: '/more', 86 | children: [ 87 | { 88 | id: 101, 89 | name: 'fileToBase64', 90 | type: 'menu', 91 | path: '/more/fileToBase64', 92 | }, 93 | ], 94 | }, 95 | ] 96 | 97 | const getUserInfo = (config) => { 98 | return Mock.mock({ 99 | code: 200, 100 | data: userInfo, 101 | }) 102 | } 103 | 104 | const getMenuList = (config) => { 105 | return Mock.mock({ 106 | code: 200, 107 | data: menuList, 108 | }) 109 | } 110 | 111 | const uploadImage = (config) => { 112 | return { 113 | code: 200, 114 | data: { 115 | imgUrl: 'https://s2.ax1x.com/2019/08/02/edRc1P.jpg', 116 | }, 117 | } 118 | } 119 | 120 | Mock.mock(/\/system\/image\/upload/, 'post', uploadImage) 121 | Mock.mock(/\/system\/user\/info/, 'post', getUserInfo) 122 | Mock.mock(/\/system\/menu\/list/, 'post', getMenuList) 123 | -------------------------------------------------------------------------------- /src/mock/modules/home.ts: -------------------------------------------------------------------------------- 1 | import Mock from 'mockjs' 2 | 3 | const getChartData = () => { 4 | const dataSource = Mock.mock({ 5 | 'data|50': [ 6 | { 7 | value: '@natural(0,200)', 8 | }, 9 | ], 10 | }) 11 | 12 | const currentDate = Date.now() 13 | const ONE_DAY = 24 * 60 * 60 * 1000 14 | const getDate = (date) => `${date.getFullYear()}/${date.getMonth() + 1}/${date.getDate()}` 15 | 16 | const week = dataSource.data.slice(-7).map((item, index) => { 17 | const date = new Date(currentDate - (7 - index) * ONE_DAY) 18 | return { 19 | date: getDate(date), 20 | value: item.value, 21 | } 22 | }) 23 | const month = dataSource.data.slice(-30).map((item, index) => { 24 | const date = new Date(currentDate - (30 - index) * ONE_DAY) 25 | return { 26 | date: getDate(date), 27 | value: item.value, 28 | } 29 | }) 30 | 31 | return { 32 | week, 33 | month, 34 | } 35 | } 36 | 37 | const getStatistics = () => { 38 | return { 39 | code: 200, 40 | data: Mock.mock({ 41 | visit: '@natural(15000,30000)', 42 | user: '@natural(2500,5000)', 43 | goods: '@natural(500,1200)', 44 | comment: '@natural(1600,3000)', 45 | }), 46 | } 47 | } 48 | 49 | const getTrendData = () => { 50 | return { 51 | code: 200, 52 | data: getChartData(), 53 | } 54 | } 55 | 56 | const getTaskList = () => { 57 | return Mock.mock({ 58 | code: 200, 59 | 'data|10': [ 60 | { 61 | id: '@natural', 62 | status: '@boolean(3,7,true)', 63 | 'content|1-5': '待办事项 ', 64 | }, 65 | ], 66 | }) 67 | } 68 | 69 | const addTask = () => { 70 | return { 71 | code: 200, 72 | } 73 | } 74 | 75 | const editTask = () => { 76 | return { 77 | code: 200, 78 | } 79 | } 80 | 81 | const deleteTask = () => { 82 | return { 83 | code: 200, 84 | } 85 | } 86 | 87 | const finishTask = () => { 88 | return { 89 | code: 200, 90 | } 91 | } 92 | 93 | const getGoodsList = () => { 94 | return Mock.mock({ 95 | code: 200, 96 | 'data|10': [ 97 | { 98 | id: '@natural', 99 | name: '@string', 100 | address: '@city', 101 | price: '@natural(10,50)', 102 | }, 103 | ], 104 | }) 105 | } 106 | 107 | Mock.mock(/\/home\/statistics/, 'post', getStatistics) 108 | Mock.mock(/\/home\/chart\/visit/, 'post', getTrendData) 109 | Mock.mock(/\/home\/chart\/user/, 'post', getTrendData) 110 | Mock.mock(/\/home\/chart\/goods/, 'post', getTrendData) 111 | Mock.mock(/\/home\/chart\/comment/, 'post', getTrendData) 112 | Mock.mock(/\/home\/task\/list/, 'post', getTaskList) 113 | Mock.mock(/\/home\/task\/add/, 'post', addTask) 114 | Mock.mock(/\/home\/task\/edit/, 'post', editTask) 115 | Mock.mock(/\/home\/task\/delete/, 'post', deleteTask) 116 | Mock.mock(/\/home\/task\/finish/, 'post', finishTask) 117 | Mock.mock(/\/home\/goods\/list/, 'post', getGoodsList) 118 | -------------------------------------------------------------------------------- /src/assets/icons/loading.svg: -------------------------------------------------------------------------------- 1 | 3 | 6 | -------------------------------------------------------------------------------- /src/views/common/forbidden/Forbidden.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 58 | 59 | 103 | 104 | 111 | -------------------------------------------------------------------------------- /src/hooks/base/useCountDown.ts: -------------------------------------------------------------------------------- 1 | import { ref, computed, onActivated, onDeactivated, onBeforeUnmount } from 'vue' 2 | 3 | export type CurrentTime = { 4 | days: number 5 | hours: number 6 | total: number 7 | minutes: number 8 | seconds: number 9 | milliseconds: number 10 | } 11 | 12 | const SECOND = 1000 13 | const MINUTE = 60 * SECOND 14 | const HOUR = 60 * MINUTE 15 | const DAY = 24 * HOUR 16 | 17 | const parseTime = (time: number): CurrentTime => { 18 | const days = Math.floor(time / DAY) 19 | const hours = Math.floor((time % DAY) / HOUR) 20 | const minutes = Math.floor((time % HOUR) / MINUTE) 21 | const seconds = Math.floor((time % MINUTE) / SECOND) 22 | const milliseconds = Math.floor(time % SECOND) 23 | 24 | return { 25 | total: time, 26 | days, 27 | hours, 28 | minutes, 29 | seconds, 30 | milliseconds, 31 | } 32 | } 33 | 34 | const isSameSecond = (time1: number, time2: number) => { 35 | return Math.floor(time1 / 1000) === Math.floor(time2 / 1000) 36 | } 37 | 38 | const useCountDown = (options: { 39 | // 倒计时时长,单位毫秒 40 | time: number 41 | onChange?: (current: CurrentTime) => void 42 | onFinish?: () => void 43 | }) => { 44 | let rafId: number 45 | let endTime: number 46 | let counting: boolean 47 | let deactivated: boolean 48 | 49 | const remain = ref(options.time) 50 | 51 | const count = computed(() => parseTime(remain.value)) 52 | 53 | const getCurrentRemain = () => Math.max(endTime - Date.now(), 0) 54 | 55 | const setRemain = (value: number) => { 56 | remain.value = value 57 | options.onChange?.(count.value) 58 | 59 | // 倒计时到0,自动暂停 60 | if (value === 0) { 61 | pauseCount() 62 | options.onFinish?.() 63 | } 64 | } 65 | 66 | const tick = () => { 67 | rafId = requestAnimationFrame(() => { 68 | if (counting) { 69 | const remainRemain = getCurrentRemain() 70 | if (!isSameSecond(remainRemain, remain.value) || remainRemain === 0) { 71 | setRemain(remainRemain) 72 | } 73 | if (remain.value > 0) { 74 | tick() 75 | } 76 | } 77 | }) 78 | } 79 | 80 | // 开始 81 | const startCount = () => { 82 | if (!counting) { 83 | endTime = Date.now() + remain.value 84 | counting = true 85 | tick() 86 | } 87 | } 88 | 89 | // 暂停 90 | const pauseCount = () => { 91 | counting = false 92 | cancelAnimationFrame(rafId) 93 | } 94 | 95 | // 重新开始 96 | const resetCount = (totalTime: number = options.time) => { 97 | pauseCount() 98 | remain.value = totalTime 99 | } 100 | 101 | onBeforeUnmount(() => { 102 | resetCount() 103 | }) 104 | 105 | onActivated(() => { 106 | if (deactivated) { 107 | counting = true 108 | deactivated = false 109 | tick() 110 | } 111 | }) 112 | 113 | onDeactivated(() => { 114 | if (counting) { 115 | pauseCount() 116 | deactivated = true 117 | } 118 | }) 119 | 120 | return { 121 | count, 122 | startCount, 123 | pauseCount, 124 | resetCount, 125 | } 126 | } 127 | 128 | export default useCountDown 129 | -------------------------------------------------------------------------------- /src/utils/file.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 将base64数据转换为Bolb 3 | */ 4 | export const base64toBlob = (dataURL: string, filename: string = 'file') => { 5 | const arr = dataURL.split(',') 6 | const mime = arr[0]!.match(/:(.*?);/)![1] 7 | const suffix = mime.split('/')[1] 8 | const bstr = window.atob(arr[1]) 9 | let n = bstr.length 10 | const u8arr = new Uint8Array(n) 11 | while (n--) { 12 | u8arr[n] = bstr.charCodeAt(n) 13 | } 14 | return new File([u8arr], `${filename}.${suffix}`, { 15 | type: mime, 16 | }) 17 | } 18 | 19 | /** 20 | * 将Blob数据转换为base64 21 | */ 22 | export const blobToBase64 = (blob: Blob) => { 23 | return new Promise((resolve, reject) => { 24 | const reader = new FileReader() 25 | reader.onload = (e) => { 26 | resolve(e.target?.result as string) 27 | } 28 | reader.onerror = reject 29 | reader.readAsDataURL(blob) 30 | }) 31 | } 32 | 33 | /** 34 | * 下载文件。数据源是Blob对象 35 | */ 36 | export const downloadBlobFile = (binaryString: string, fileName: string) => { 37 | const blob = new Blob([binaryString], { type: 'application/octet-stream' }) 38 | const link = document.createElement('a') 39 | link.href = window.URL.createObjectURL(blob) 40 | link.download = fileName 41 | link.click() 42 | //延时释放 43 | setTimeout(function () { 44 | window.URL.revokeObjectURL(link.href) 45 | }, 100) 46 | } 47 | 48 | /** 49 | * 下载文件。数据源是base64 50 | */ 51 | export const downloadBase64File = (data: string, fileName: string, headerType?: 'txt' | 'png' | 'jpg' | 'xlsx') => { 52 | const headerMap = { 53 | txt: 'data:text/plain', 54 | png: 'data:image/png', 55 | jpg: 'data:image/jpeg', 56 | xlsx: 'data:application/vnd.ms-excel', 57 | } 58 | const link = document.createElement('a') 59 | const header = headerMap[headerType || ''] || '' 60 | link.href = header + ';base64,' + data 61 | link.download = fileName 62 | link.click() 63 | } 64 | 65 | /** 66 | * 通过文件地址下载文件 67 | */ 68 | export const downloadURLFile = (url: string, fileName: string) => { 69 | const xhr = new XMLHttpRequest() 70 | xhr.open('GET', url.replace(/\\/g, '/'), true) 71 | xhr.responseType = 'blob' 72 | xhr.onload = () => { 73 | if (xhr.status === 200) { 74 | downloadBlobFile(xhr.response, fileName) 75 | } 76 | } 77 | xhr.send() 78 | } 79 | 80 | /** 81 | * 计算文件大小 82 | */ 83 | export const getFileSize = (value: string | number) => { 84 | const sizeMap = { 85 | PiB: 1024 * 1024 * 1024 * 1024 * 1024, 86 | TiB: 1024 * 1024 * 1024 * 1024, 87 | GiB: 1024 * 1024 * 1024, 88 | MiB: 1024 * 1024, 89 | KiB: 1024, 90 | Byte: 1, 91 | } 92 | for (let i in sizeMap) { 93 | if (Number(value) >= sizeMap[i]) { 94 | return (Number(value) / sizeMap[i]).toFixed(2) + ` ${i}` 95 | } 96 | } 97 | return '0 Byte' 98 | } 99 | 100 | /** 101 | * 获取不带后缀的文件名 102 | */ 103 | export const getFileName = (fileName: string) => { 104 | return fileName.replace(/(.*\/)*([^.]+).*/gi, '$2') 105 | } 106 | 107 | /** 108 | * 获取文件的后缀 109 | */ 110 | export const getFileSuffix = (fileName: string) => { 111 | let suffix = '' 112 | const pointIndex = fileName.lastIndexOf('.') 113 | if (pointIndex > -1) { 114 | suffix = fileName.slice(pointIndex + 1) 115 | } 116 | return suffix 117 | } 118 | -------------------------------------------------------------------------------- /src/mock/modules/article.ts: -------------------------------------------------------------------------------- 1 | import Mock from 'mockjs' 2 | import { getURLParams } from '@/utils/core' 3 | import util from '../util' 4 | 5 | const articleData = Mock.mock({ 6 | 'list|213': [ 7 | { 8 | id: '@lower(@guid)', 9 | name: '@ctitle', 10 | author: '@cname', 11 | createDate: '@datetime("yyyy-MM-dd HH:mm:ss")', 12 | type: '@pick(["1", "2", "3", "4", "5"])', 13 | browseNum: '@natural(1000,9999)', 14 | imageURL: 'https://source.unsplash.com/random/200x200', 15 | brief: '@cparagraph(1,3)', 16 | content: '@cparagraph', 17 | accessory: [ 18 | { 19 | id: '1', 20 | name: '图片图片.jpg', 21 | url: 'https://s2.ax1x.com/2019/08/02/edRc1P.jpg', 22 | }, 23 | { 24 | id: '2', 25 | name: '营业执照副本.pdf', 26 | url: 'http://www.xdocin.com/xdoc?_key=fedii4dtyfhmvgryqyntfjavte&_func=down&_dir=document.pdf', 27 | }, 28 | { 29 | id: '3', 30 | name: '数据采集表', 31 | url: 'http://www.xdocin.com/xdoc?_key=fedii4dtyfhmvgryqyntfjavte&_func=down&_dir=data.xlsx', 32 | }, 33 | ], 34 | }, 35 | ], 36 | }) 37 | 38 | const table = articleData.list 39 | 40 | const getList = (config) => { 41 | const { type = '', author = '', pageNumber = 1, pageSize = table.length, name = '' } = getURLParams(config.url) 42 | const types = type.split(',') 43 | const typesLength = types.length 44 | const result = table.filter((item) => { 45 | let validAuthor = false 46 | let validType = false 47 | let validName = false 48 | 49 | if (typesLength === 0) { 50 | validType = true 51 | } else { 52 | validType = types.some((item1) => { 53 | return item.type.includes(item1) 54 | }) 55 | } 56 | validName = item.name.includes(name) 57 | validAuthor = item.author.includes(author) 58 | return validAuthor && validName && validType 59 | }) 60 | const startNumber = (Number(pageNumber) - 1) * Number(pageSize) 61 | const endNumber = startNumber + Number(pageSize) 62 | return { 63 | code: 200, 64 | data: { 65 | list: util.pickFromTable(result.slice(startNumber, endNumber), [ 66 | 'id', 67 | 'name', 68 | 'author', 69 | 'createDate', 70 | 'type', 71 | 'browseNum', 72 | ]), 73 | total: result.length, 74 | }, 75 | } 76 | } 77 | 78 | const getDetail = (config) => { 79 | const { id } = getURLParams(config.url) 80 | // 刷新编辑页面时会重新Mock数据,根据之前的id找不到对应的文章 81 | const detail = util.find(table, id) || table[0] 82 | return { 83 | code: 200, 84 | data: detail, 85 | } 86 | } 87 | 88 | const update = (config) => { 89 | const { detail } = window.JSON.parse(config.body) 90 | if (!detail.id) { 91 | const initRow = { 92 | createDate: Date.now(), 93 | browseNum: 0, 94 | author: '本人', 95 | } 96 | Object.assign(detail, initRow) 97 | } 98 | util.update(table, detail) 99 | return { 100 | code: 200, 101 | data: {}, 102 | } 103 | } 104 | 105 | const remove = (config) => { 106 | const { id } = window.JSON.parse(config.body) 107 | util.remove(table, id) 108 | return { 109 | code: 200, 110 | data: {}, 111 | } 112 | } 113 | 114 | Mock.mock(/\/article\/list/, 'post', getList) 115 | Mock.mock(/\/article\/detail/, 'post', getDetail) 116 | Mock.mock(/\/article\/update/, 'post', update) 117 | Mock.mock(/\/article\/remove/, 'post', remove) 118 | -------------------------------------------------------------------------------- /src/components/base/BaseDialog.vue: -------------------------------------------------------------------------------- 1 | 63 | 64 | 108 | 158 | -------------------------------------------------------------------------------- /auto-imports.d.ts: -------------------------------------------------------------------------------- 1 | // Generated by 'unplugin-auto-import' 2 | export {} 3 | declare global { 4 | const EffectScope: typeof import('vue')['EffectScope'] 5 | const computed: typeof import('vue')['computed'] 6 | const createApp: typeof import('vue')['createApp'] 7 | const customRef: typeof import('vue')['customRef'] 8 | const defineAsyncComponent: typeof import('vue')['defineAsyncComponent'] 9 | const defineComponent: typeof import('vue')['defineComponent'] 10 | const effectScope: typeof import('vue')['effectScope'] 11 | const getCurrentInstance: typeof import('vue')['getCurrentInstance'] 12 | const getCurrentScope: typeof import('vue')['getCurrentScope'] 13 | const h: typeof import('vue')['h'] 14 | const inject: typeof import('vue')['inject'] 15 | const isProxy: typeof import('vue')['isProxy'] 16 | const isReactive: typeof import('vue')['isReactive'] 17 | const isReadonly: typeof import('vue')['isReadonly'] 18 | const isRef: typeof import('vue')['isRef'] 19 | const markRaw: typeof import('vue')['markRaw'] 20 | const nextTick: typeof import('vue')['nextTick'] 21 | const onActivated: typeof import('vue')['onActivated'] 22 | const onBeforeMount: typeof import('vue')['onBeforeMount'] 23 | const onBeforeRouteLeave: typeof import('vue-router')['onBeforeRouteLeave'] 24 | const onBeforeRouteUpdate: typeof import('vue-router')['onBeforeRouteUpdate'] 25 | const onBeforeUnmount: typeof import('vue')['onBeforeUnmount'] 26 | const onBeforeUpdate: typeof import('vue')['onBeforeUpdate'] 27 | const onDeactivated: typeof import('vue')['onDeactivated'] 28 | const onErrorCaptured: typeof import('vue')['onErrorCaptured'] 29 | const onMounted: typeof import('vue')['onMounted'] 30 | const onRenderTracked: typeof import('vue')['onRenderTracked'] 31 | const onRenderTriggered: typeof import('vue')['onRenderTriggered'] 32 | const onScopeDispose: typeof import('vue')['onScopeDispose'] 33 | const onServerPrefetch: typeof import('vue')['onServerPrefetch'] 34 | const onUnmounted: typeof import('vue')['onUnmounted'] 35 | const onUpdated: typeof import('vue')['onUpdated'] 36 | const provide: typeof import('vue')['provide'] 37 | const reactive: typeof import('vue')['reactive'] 38 | const readonly: typeof import('vue')['readonly'] 39 | const ref: typeof import('vue')['ref'] 40 | const resolveComponent: typeof import('vue')['resolveComponent'] 41 | const resolveDirective: typeof import('vue')['resolveDirective'] 42 | const shallowReactive: typeof import('vue')['shallowReactive'] 43 | const shallowReadonly: typeof import('vue')['shallowReadonly'] 44 | const shallowRef: typeof import('vue')['shallowRef'] 45 | const toRaw: typeof import('vue')['toRaw'] 46 | const toRef: typeof import('vue')['toRef'] 47 | const toRefs: typeof import('vue')['toRefs'] 48 | const triggerRef: typeof import('vue')['triggerRef'] 49 | const unref: typeof import('vue')['unref'] 50 | const useAttrs: typeof import('vue')['useAttrs'] 51 | const useCssModule: typeof import('vue')['useCssModule'] 52 | const useCssVars: typeof import('vue')['useCssVars'] 53 | const useLink: typeof import('vue-router')['useLink'] 54 | const useRoute: typeof import('vue-router')['useRoute'] 55 | const useRouter: typeof import('vue-router')['useRouter'] 56 | const useSlots: typeof import('vue')['useSlots'] 57 | const watch: typeof import('vue')['watch'] 58 | const watchEffect: typeof import('vue')['watchEffect'] 59 | const watchPostEffect: typeof import('vue')['watchPostEffect'] 60 | const watchSyncEffect: typeof import('vue')['watchSyncEffect'] 61 | } 62 | -------------------------------------------------------------------------------- /src/utils/request.ts: -------------------------------------------------------------------------------- 1 | import axios, { AxiosRequestConfig, AxiosInstance } from 'axios' 2 | import { baseURL } from '@/config/domain' 3 | import { TokenName } from '@/config/constant' 4 | import { useAppStore } from '@/store' 5 | import { localMng } from '@/utils/storage-mng' 6 | 7 | class Request { 8 | private baseConfig: AxiosRequestConfig = { 9 | baseURL, 10 | headers: {}, 11 | timeout: 10000, 12 | } 13 | 14 | private instance: AxiosInstance = axios.create(this.baseConfig) 15 | 16 | public constructor() { 17 | const token = localMng.getItem(TokenName) 18 | if (token) { 19 | this.setHeader({ 20 | Authorization: token, 21 | }) 22 | } else { 23 | this.initInstance() 24 | } 25 | } 26 | 27 | private initInstance() { 28 | this.instance = axios.create(this.baseConfig) 29 | this.setReqInterceptors() 30 | this.setResInterceptors() 31 | } 32 | 33 | // 请求拦截器 34 | private setReqInterceptors = () => { 35 | this.instance.interceptors.request.use( 36 | (config) => { 37 | // 避免复制接口url的时候多复制了空格 38 | config.url = config.url?.trim() 39 | return config 40 | }, 41 | (err) => { 42 | window.$message.error('请求失败') 43 | return Promise.reject(err) 44 | } 45 | ) 46 | } 47 | 48 | // 响应拦截器 49 | private setResInterceptors = () => { 50 | this.instance.interceptors.response.use( 51 | (res) => { 52 | const { code = 200, data = res, message } = res.data 53 | switch (code) { 54 | case 200: 55 | return Promise.resolve(data) 56 | case 401: 57 | window.$message.warning(message || '无权限') 58 | const appStore = useAppStore() 59 | appStore.logout(false) 60 | return Promise.reject(res) 61 | default: 62 | window.$message.error(message || '响应失败') 63 | return Promise.reject(res) 64 | } 65 | }, 66 | (err) => { 67 | if (!axios.isCancel(err)) { 68 | window.$message.error('响应失败') 69 | } 70 | return Promise.reject(err) 71 | } 72 | ) 73 | } 74 | 75 | // 设置请求头 76 | public setHeader = (headers: any) => { 77 | this.baseConfig.headers = { ...this.baseConfig.headers, ...headers } 78 | this.initInstance() 79 | } 80 | 81 | // get请求 82 | public get = (url: string, data = {}, config: AxiosRequestConfig = {}): Promise => 83 | this.instance({ url, method: 'get', params: data, ...config }) 84 | 85 | // post请求 86 | public post = (url: string, data = {}, config: AxiosRequestConfig = {}): Promise => 87 | this.instance({ url, method: 'post', data, ...config }) 88 | 89 | // 不经过统一的axios实例的get请求 90 | public postOnly = (url: string, data = {}, config: AxiosRequestConfig = {}): Promise => 91 | axios({ 92 | ...this.baseConfig, 93 | url, 94 | method: 'post', 95 | data, 96 | ...config, 97 | }) 98 | 99 | // 不经过统一的axios实例的post请求 100 | public getOnly = (url: string, data = {}, config: AxiosRequestConfig = {}): Promise => 101 | axios({ 102 | ...this.baseConfig, 103 | url, 104 | method: 'get', 105 | params: data, 106 | ...config, 107 | }) 108 | 109 | // delete请求 110 | public deleteBody = (url: string, data = {}, config: AxiosRequestConfig = {}): Promise => 111 | this.instance({ url, method: 'delete', data, ...config }) 112 | 113 | public deleteParam = (url: string, data = {}, config: AxiosRequestConfig = {}): Promise => 114 | this.instance({ url, method: 'delete', params: data, ...config }) 115 | } 116 | 117 | export default new Request() 118 | -------------------------------------------------------------------------------- /src/views/portal/login/Login.vue: -------------------------------------------------------------------------------- 1 | 56 | 57 | 105 | 106 | 130 | -------------------------------------------------------------------------------- /src/views/home/components/Statistics.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 68 | 69 | 143 | -------------------------------------------------------------------------------- /src/views/home/components/LineChart.vue: -------------------------------------------------------------------------------- 1 | 118 | 119 | 132 | 133 | 150 | -------------------------------------------------------------------------------- /src/views/individual/components/Password.vue: -------------------------------------------------------------------------------- 1 | 80 | 81 | 126 | -------------------------------------------------------------------------------- /src/views/home/components/TodoList.vue: -------------------------------------------------------------------------------- 1 | 77 | 78 | 128 | 129 | 147 | -------------------------------------------------------------------------------- /src/utils/color.ts: -------------------------------------------------------------------------------- 1 | import { colord, extend } from 'colord' 2 | import mixPlugin from 'colord/plugins/mix' 3 | import type { HsvColor } from 'colord' 4 | 5 | extend([mixPlugin]) 6 | 7 | type ColorIndex = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 8 | 9 | const hueStep = 2 10 | const saturationStep = 16 11 | const saturationStep2 = 5 12 | const brightnessStep1 = 5 13 | const brightnessStep2 = 15 14 | const lightColorCount = 5 15 | const darkColorCount = 4 16 | 17 | /** 18 | * 获取色相渐变 19 | * @param hsv - hsv格式颜色值 20 | * @param i - 与6的相对距离 21 | * @param isLight - 是否是亮颜色 22 | */ 23 | const getHue = (hsv: HsvColor, i: number, isLight: boolean) => { 24 | let hue: number 25 | if (hsv.h >= 60 && hsv.h <= 240) { 26 | // 冷色调 27 | // 减淡变亮 色相顺时针旋转 更暖 28 | // 加深变暗 色相逆时针旋转 更冷 29 | hue = isLight ? hsv.h - hueStep * i : hsv.h + hueStep * i 30 | } else { 31 | // 暖色调 32 | // 减淡变亮 色相逆时针旋转 更暖 33 | // 加深变暗 色相顺时针旋转 更冷 34 | hue = isLight ? hsv.h + hueStep * i : hsv.h - hueStep * i 35 | } 36 | if (hue < 0) { 37 | hue += 360 38 | } else if (hue >= 360) { 39 | hue -= 360 40 | } 41 | return hue 42 | } 43 | 44 | /** 45 | * 获取饱和度渐变 46 | * @param hsv - hsv格式颜色值 47 | * @param i - 与6的相对距离 48 | * @param isLight - 是否是亮颜色 49 | */ 50 | const getSaturation = (hsv: HsvColor, i: number, isLight: boolean) => { 51 | let saturation: number 52 | if (isLight) { 53 | saturation = hsv.s - saturationStep * i 54 | } else if (i === darkColorCount) { 55 | saturation = hsv.s + saturationStep 56 | } else { 57 | saturation = hsv.s + saturationStep2 * i 58 | } 59 | if (saturation > 100) { 60 | saturation = 100 61 | } 62 | if (isLight && i === lightColorCount && saturation > 10) { 63 | saturation = 10 64 | } 65 | if (saturation < 6) { 66 | saturation = 6 67 | } 68 | return saturation 69 | } 70 | 71 | /** 72 | * 获取明度渐变 73 | * @param hsv - hsv格式颜色值 74 | * @param i - 与6的相对距离 75 | * @param isLight - 是否是亮颜色 76 | */ 77 | const getValue = (hsv: HsvColor, i: number, isLight: boolean) => { 78 | let value: number 79 | if (isLight) { 80 | value = hsv.v + brightnessStep1 * i 81 | } else { 82 | value = hsv.v - brightnessStep2 * i 83 | } 84 | if (value > 100) { 85 | value = 100 86 | } 87 | return value 88 | } 89 | 90 | /** 91 | * 根据颜色获取调色板颜色(从左至右颜色从浅到深,6为主色号) 92 | * @param color - 颜色 93 | * @param index - 调色板的对应的色号(6为主色号) 94 | * @description 算法实现从ant-design调色板算法中借鉴 https://github.com/ant-design/ant-design/blob/master/components/style/color/colorPalette.less 95 | */ 96 | export const getColorPalette = (color: string, index: ColorIndex) => { 97 | if (index === 6) return color 98 | 99 | const isLight = index < 6 100 | const hsv = colord(color).toHsv() 101 | const i = isLight ? lightColorCount + 1 - index : index - lightColorCount - 1 102 | 103 | const newHsv: HsvColor = { 104 | h: getHue(hsv, i, isLight), 105 | s: getSaturation(hsv, i, isLight), 106 | v: getValue(hsv, i, isLight), 107 | } 108 | 109 | return colord(newHsv).toHex() 110 | } 111 | 112 | /** 113 | * 根据颜色获取调色板颜色所有颜色 114 | * @param color - 颜色 115 | */ 116 | export const getAllColorPalette = (color: string) => { 117 | const indexs: ColorIndex[] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] 118 | return indexs.map((index) => getColorPalette(color, index)) 119 | } 120 | 121 | /** 122 | * 给颜色加透明度 123 | * @param color - 颜色 124 | * @param alpha - 透明度(0 - 1) 125 | */ 126 | export const addColorAlpha = (color: string, alpha: number) => { 127 | return colord(color).alpha(alpha).toHex() 128 | } 129 | 130 | /** 131 | * 颜色混合 132 | * @param firstColor - 第一个颜色 133 | * @param secondColor - 第二个颜色 134 | * @param ratio - 第二个颜色占比 135 | */ 136 | export const mixColor = (firstColor: string, secondColor: string, ratio: number) => { 137 | return colord(firstColor).mix(secondColor, ratio).toHex() 138 | } 139 | 140 | /** 141 | * 是否是白颜色 142 | * @param color - 颜色 143 | */ 144 | export const isWhiteColor = (color: string) => { 145 | return colord(color).isEqual('#ffffff') 146 | } 147 | 148 | /** 149 | * 随机生成十六进制颜色 150 | */ 151 | export const randomHexColor = () => '#' + ('00000' + ((Math.random() * 0x1000000) << 0).toString(16)).substr(-6) 152 | -------------------------------------------------------------------------------- /src/components/base/BaseButton.vue: -------------------------------------------------------------------------------- 1 | 39 | 40 | 61 | 62 | 188 | -------------------------------------------------------------------------------- /src/components/business/AvatarUpload.vue: -------------------------------------------------------------------------------- 1 | 95 | 96 | 134 | 135 | 173 | -------------------------------------------------------------------------------- /src/views/user/components/UserEdit.vue: -------------------------------------------------------------------------------- 1 | 106 | 107 | 150 | -------------------------------------------------------------------------------- /src/views/portal/password/Password.vue: -------------------------------------------------------------------------------- 1 | 92 | 93 | 134 | 135 | 154 | -------------------------------------------------------------------------------- /src/utils/core.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 获取url中的查询字符串参数 3 | * @param {String} url url字符串 4 | */ 5 | export const getURLParams = (url: string) => { 6 | const search = url.split('?')[1] 7 | if (!search) { 8 | return {} 9 | } 10 | return JSON.parse( 11 | '{"' + decodeURIComponent(search).replace(/"/g, '\\"').replace(/&/g, '","').replace(/=/g, '":"') + '"}' 12 | ) 13 | } 14 | 15 | /** 16 | * 延迟执行 17 | */ 18 | export const sleep = (interval: number) => new Promise((resolve) => setTimeout(resolve, interval)) 19 | 20 | /** 21 | * 构造枚举字段管理对象 22 | */ 23 | interface EnumModel { 24 | id: string | number 25 | name: string | number 26 | color?: string 27 | status?: Status 28 | [key: string]: any 29 | [key: number]: any 30 | } 31 | 32 | export interface EnumResult { 33 | ids: Array 34 | names: Array 35 | origin: Array 36 | [key: string]: any 37 | [key: number]: any 38 | getColor: (id: string) => string 39 | getStatus: (id: string) => Status | undefined 40 | getNamesByIds: (ids: Array) => Array 41 | format: (idAlias: string, nameAlias: string) => any[] 42 | omit: (hides: Array) => T[] 43 | } 44 | 45 | export const enumMng = (data: Array): EnumResult => { 46 | const result: EnumResult = {} as EnumResult 47 | const ids: Array = [] 48 | const names: Array = [] 49 | 50 | data.forEach((item) => { 51 | result[item.id] = item.name 52 | ids.push(item.id) 53 | names.push(item.name) 54 | }) 55 | 56 | result.ids = ids 57 | result.names = names 58 | result.origin = data 59 | result.getColor = (id) => { 60 | const row = data.find((item) => item.id === id) 61 | return row ? row.color! : '' 62 | } 63 | result.getStatus = (id: string) => { 64 | const row = data.find((item) => item.id === id) 65 | return row ? row.status : undefined 66 | } 67 | result.getNamesByIds = (ids) => { 68 | const names: Array = [] 69 | ids.forEach((id) => { 70 | const row = data.find((item) => item.id === id) 71 | row && names.push(row.name) 72 | }) 73 | return names 74 | } 75 | result.format = (idAlias, nameAlias) => 76 | data.map((item) => ({ 77 | [idAlias]: item.id, 78 | [nameAlias]: item.name, 79 | })) 80 | result.omit = (hides) => data.filter((item) => !hides.includes(item.id)) 81 | 82 | return result 83 | } 84 | 85 | /** 86 | * 数值录入 87 | * @param {String} source 输入的值 88 | * @param {Number} decimals 保留几位小数,0表示整数,默认两位小数。 89 | * @param {Nubmer} min 最小值 90 | * @param {Nubmer} max 最大值 91 | */ 92 | export const inputNumber = ( 93 | source: string | number, 94 | decimals: number = 2, 95 | min: number = Number.NEGATIVE_INFINITY, 96 | max: number = Number.POSITIVE_INFINITY 97 | ): string => { 98 | if (source == null) return '' 99 | let value = source.toString() 100 | value = value.replace(/^(\-)*\D*(\d*(?:\.\d*)?).*$/g, '$1$2') 101 | // 只能输入正数 102 | if (min >= 0) { 103 | value = value.replace('-', '') 104 | } else if (max < 0) { 105 | // 只能输入负数 106 | value = '-' + value 107 | } 108 | // 小数处理 109 | const decimalIndex = value.indexOf('.') 110 | if (decimals !== undefined && decimalIndex > -1) { 111 | if (decimals === 0) { 112 | value = value.slice(0, decimalIndex + decimals) 113 | } else if (decimals > 0) { 114 | value = value.slice(0, decimalIndex + decimals + 1) 115 | } 116 | } 117 | const numberValue = Number(value) 118 | if (typeof numberValue === 'number') { 119 | if (numberValue < min) { 120 | value = min.toString() 121 | } else if (numberValue > max) { 122 | value = max.toString() 123 | } 124 | } 125 | return value 126 | } 127 | 128 | /** 129 | * 获取树的所有节点的某个属性值 130 | */ 131 | export const getTreeNodeValue = (tree: any, filed: string) => 132 | tree 133 | .map((node: any) => { 134 | const result: any = [] 135 | node[filed] && result.push(node[filed]) 136 | if (node.children) { 137 | result.push(...getTreeNodeValue(node.children, filed)) 138 | } 139 | return result 140 | }) 141 | .flat() 142 | 143 | /** 144 | * 获取动态图片路径 145 | */ 146 | export const getImageUrl = (url: string) => new URL(`../assets/images/${url}`, import.meta.url).href 147 | -------------------------------------------------------------------------------- /src/components/base/BaseDrawer.vue: -------------------------------------------------------------------------------- 1 | 65 | 66 | 119 | 120 | 171 | 172 | 190 | -------------------------------------------------------------------------------- /src/views/portal/register/Register.vue: -------------------------------------------------------------------------------- 1 | 98 | 99 | 139 | 140 | 159 | -------------------------------------------------------------------------------- /src/views/user/User.vue: -------------------------------------------------------------------------------- 1 | 81 | 82 | 168 | -------------------------------------------------------------------------------- /src/assets/icons/empty.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 9 | 12 | 14 | 17 | 20 | 22 | 24 | 26 | 28 | 31 | 34 | 37 | 41 | 45 | -------------------------------------------------------------------------------- /src/utils/excle.js: -------------------------------------------------------------------------------- 1 | import { utils, SSF, write, read } from 'xlsx' 2 | 3 | /** 4 | * 导入/导出excel 5 | */ 6 | class ExcelHandler { 7 | constructor() { 8 | this.exportExcel = this.exportExcel.bind(this) 9 | } 10 | /** 11 | * 将json数据导出为excel文件 12 | * @param {Array} header 表头 如:['姓名', '年龄', '性别', '电话', '电子邮箱'] 13 | * @param {Array} dataSource json数据 如:[{name:'wly',age:12, gender:'男',mobile:'54321',email:'123@qq.com}] 14 | * @param {String} fileName 文件名称 15 | * @param {Boolean} autoWidth excel中的每个格子是否自动被内容撑开 16 | */ 17 | exportExcel(header, dataSource, fileName, autoWidth = true) { 18 | let sheet 19 | if (autoWidth) { 20 | const data = dataSource.map((item) => Object.values(item)) 21 | data.unshift(header) 22 | sheet = this.sheet_from_array_of_arrays(data) 23 | this.adaptWidth(data, sheet) 24 | } else { 25 | const data = dataSource.map((i) => { 26 | const values = Object.values(i) 27 | const newItem = {} 28 | header.forEach((item, index) => { 29 | newItem[header[index]] = values[index] 30 | }) 31 | return newItem 32 | }) 33 | sheet = utils.json_to_sheet(data) 34 | } 35 | 36 | const sheetName = 'Sheet1' 37 | const wb = { 38 | SheetNames: [], 39 | Sheets: {}, 40 | Props: {}, 41 | } 42 | wb.SheetNames.push(sheetName) 43 | wb.Sheets[sheetName] = sheet 44 | const wbout = write(wb, { 45 | bookType: 'xlsx', 46 | bookSST: false, 47 | type: 'binary', 48 | }) 49 | if (window.btoa) { 50 | this.downloadURI(wbout, fileName) 51 | } else { 52 | this.downloadBlob(wbout, fileName) 53 | } 54 | } 55 | 56 | sheet_from_array_of_arrays(data) { 57 | const sheet = {} 58 | const range = { 59 | s: { 60 | c: 10000000, 61 | r: 10000000, 62 | }, 63 | e: { 64 | c: 0, 65 | r: 0, 66 | }, 67 | } 68 | for (let R = 0; R != data.length; ++R) { 69 | for (let C = 0; C != data[R].length; ++C) { 70 | if (range.s.r > R) range.s.r = R 71 | if (range.s.c > C) range.s.c = C 72 | if (range.e.r < R) range.e.r = R 73 | if (range.e.c < C) range.e.c = C 74 | const cell = { 75 | v: data[R][C], 76 | } 77 | if (cell.v == null) continue 78 | const cell_ref = utils.encode_cell({ 79 | c: C, 80 | r: R, 81 | }) 82 | if (typeof cell.v === 'number') { 83 | cell.t = 'n' 84 | } else if (typeof cell.v === 'boolean') { 85 | cell.t = 'b' 86 | } else if (cell.v instanceof Date) { 87 | cell.t = 'n' 88 | cell.z = SSF._table[14] 89 | cell.v = this.datenum(cell.v) 90 | } else { 91 | cell.t = 's' 92 | } 93 | sheet[cell_ref] = cell 94 | } 95 | } 96 | if (range.s.c < 10000000) { 97 | sheet['!ref'] = utils.encode_range(range) 98 | } 99 | return sheet 100 | } 101 | 102 | datenum(v, date1904) { 103 | if (date1904) { 104 | v += 1462 105 | } 106 | const epoch = Date.parse(v) 107 | return (epoch - new Date(Date.UTC(1899, 11, 30))) / (24 * 60 * 60 * 1000) 108 | } 109 | 110 | // 调整excel表格宽度 111 | adaptWidth(data, sheet) { 112 | // 设置worksheet每列的最大宽度 113 | const colWidth = data.map((row) => 114 | row.map((val) => { 115 | const width = { 116 | wch: 0, 117 | } 118 | // 先判断是否为null/undefined 119 | if (val == null) { 120 | width.wch = 10 121 | } else if (val.toString().charCodeAt(0) > 255) { 122 | // 再判断是否为中文 123 | width.wch = val.toString().length * 2 124 | } else { 125 | width.wch = val.toString().length 126 | } 127 | return width 128 | }) 129 | ) 130 | // 以第一行为初始值 131 | const result = colWidth[0] 132 | for (let i = 1; i < colWidth.length; i++) { 133 | for (let j = 0; j < colWidth[i].length; j++) { 134 | if (result[j]['wch'] < colWidth[i][j]['wch']) { 135 | result[j]['wch'] = colWidth[i][j]['wch'] 136 | } 137 | } 138 | } 139 | sheet['!cols'] = result 140 | } 141 | 142 | //二进制字符串转字节流 143 | s2ab(s) { 144 | const buffer = new ArrayBuffer(s.length) 145 | const view = new Uint8Array(buffer) 146 | for (let i = 0; i < s.length; i++) { 147 | view[i] = s.charCodeAt(i) & 0xff 148 | } 149 | return buffer 150 | } 151 | 152 | // 使用blob对象下载文件 153 | downloadBlob(binaryString, name) { 154 | const stream = this.s2ab(binaryString) 155 | const blob = new Blob([stream], { type: 'application/octet-stream' }) 156 | const link = document.createElement('a') 157 | const fileName = name || '数据' 158 | link.href = window.URL.createObjectURL(blob) 159 | link.download = fileName + '.xlsx' 160 | link.click() 161 | // 延时释放 162 | setTimeout(function () { 163 | window.URL.revokeObjectURL(link.href) 164 | }, 100) 165 | } 166 | 167 | // 使用base64下载文件 168 | downloadURI(binaryString, name) { 169 | const header = 'data:application/octet-stream;base64,' 170 | const dataURI = window.btoa(binaryString) 171 | const link = document.createElement('a') 172 | const fileName = name || '数据' 173 | link.href = header + dataURI 174 | link.download = fileName + '.xlsx' 175 | link.click() 176 | } 177 | 178 | // 将excel表格中的数据读取为json数据 179 | readExcelData(file, callback) { 180 | const fileReader = new FileReader() 181 | let result = {} 182 | fileReader.onload = (event) => { 183 | const dataSource = event.target.result 184 | // 以二进制流方式读取得到整份excel表格对象 185 | const workbook = read(dataSource, { 186 | type: 'binary', 187 | }) 188 | // 只读取第一个sheet中的数据 189 | const firstSheetName = workbook.SheetNames[0] 190 | const worksheet = workbook.Sheets[firstSheetName] 191 | const header = this.getHeaderRow(worksheet) 192 | const data = utils.sheet_to_json(worksheet) 193 | callback({ 194 | header, 195 | data, 196 | }) 197 | } 198 | fileReader.readAsBinaryString(file) 199 | } 200 | 201 | // 获取表头 202 | getHeaderRow(sheet) { 203 | const header = [] 204 | const range = utils.decode_range(sheet['!ref']) 205 | const R = range.s.r 206 | for (let C = range.s.c; C <= range.e.c; C++) { 207 | const cell = 208 | sheet[ 209 | utils.encode_cell({ 210 | c: C, 211 | r: R, 212 | }) 213 | ] 214 | let hdr = 'UNKNOWN' + (C + 1) //设置表头不存在时的默认值 215 | if (cell && cell.t) { 216 | hdr = utils.format_cell(cell) 217 | } 218 | header.push(hdr) 219 | } 220 | return header 221 | } 222 | } 223 | 224 | const excelHandler = new ExcelHandler() 225 | const { exportExcel, readExcelData } = excelHandler 226 | 227 | export { exportExcel, readExcelData } 228 | --------------------------------------------------------------------------------