├── src ├── assets │ ├── css │ │ ├── variables.less │ │ ├── global.less │ │ ├── antd-variables.less │ │ └── mixin.less │ ├── images │ │ ├── logo.png │ │ └── bg-smooth.jpg │ └── iconsvg │ │ ├── arrow-down.svg │ │ ├── arrow-left.svg │ │ ├── arrow-right.svg │ │ ├── svgo.yml │ │ ├── tick.svg │ │ ├── refresh.svg │ │ ├── close.svg │ │ ├── home.svg │ │ ├── list.svg │ │ ├── arrow-left2.svg │ │ ├── arrow-right2.svg │ │ ├── icon.svg │ │ ├── more.svg │ │ ├── editor.svg │ │ ├── close2.svg │ │ ├── menu-fold.svg │ │ ├── menu-unfold.svg │ │ ├── chart.svg │ │ ├── control.svg │ │ ├── message.svg │ │ ├── detail.svg │ │ ├── language-outline.svg │ │ ├── edit.svg │ │ ├── page.svg │ │ ├── theme.svg │ │ ├── permissions.svg │ │ ├── set.svg │ │ └── components.svg ├── layouts │ ├── IndexLayout │ │ ├── css │ │ │ ├── mixins.less │ │ │ └── var.less │ │ ├── components │ │ │ ├── Icon.vue │ │ │ ├── RightTopMessage.vue │ │ │ ├── RightFooter.vue │ │ │ ├── Left.vue │ │ │ ├── RightTopUser.vue │ │ │ ├── SiderMenu.vue │ │ │ └── SiderMenuItem.vue │ │ ├── composables │ │ │ └── useTopMenuWidth.ts │ │ └── locales │ │ │ ├── zh-CN.ts │ │ │ ├── zh-TW.ts │ │ │ └── en-US.ts │ ├── BlankLayout.vue │ ├── UserLayout │ │ ├── locales │ │ │ ├── zh-CN.ts │ │ │ ├── zh-TW.ts │ │ │ └── en-US.ts │ │ ├── routes.ts │ │ └── index.vue │ ├── UniversalLayout │ │ ├── css │ │ │ ├── mixins.less │ │ │ └── var.less │ │ ├── components │ │ │ ├── RightFooter.vue │ │ │ ├── Icon.vue │ │ │ ├── RightTopMessage.vue │ │ │ ├── LeftSider.vue │ │ │ ├── SiderMenuItem.vue │ │ │ ├── RightTopUser.vue │ │ │ └── SiderMenu.vue │ │ ├── locales │ │ │ ├── zh-CN.ts │ │ │ ├── zh-TW.ts │ │ │ └── en-US.ts │ │ └── index.vue │ └── SecurityLayout.vue ├── components │ ├── IconFont │ │ ├── index.ts │ │ └── index.vue │ ├── ScreenTable │ │ └── data.d.ts │ ├── IconSvg │ │ ├── index.ts │ │ └── index.vue │ ├── ALink │ │ └── index.vue │ ├── FormFooterToolbar │ │ └── index.vue │ ├── BreadCrumbs │ │ └── index.vue │ ├── Permission │ │ └── index.vue │ ├── TuiEditor │ │ └── viewer.vue │ └── SelectLang │ │ └── index.vue ├── views │ ├── 404 │ │ └── index.vue │ ├── component │ │ ├── icon │ │ │ ├── svg │ │ │ │ ├── locales │ │ │ │ │ ├── zh-CN.ts │ │ │ │ │ ├── zh-TW.ts │ │ │ │ │ └── en-US.ts │ │ │ │ └── index.vue │ │ │ └── font │ │ │ │ └── index.vue │ │ └── editor │ │ │ ├── ckeditor │ │ │ └── index.vue │ │ │ └── tui-editor │ │ │ └── index.vue │ ├── custom-breadcrumbs │ │ ├── locales │ │ │ ├── zh-CN.ts │ │ │ ├── zh-TW.ts │ │ │ └── en-US.ts │ │ └── index.vue │ ├── user │ │ ├── login │ │ │ ├── data.d.ts │ │ │ ├── service.ts │ │ │ ├── locales │ │ │ │ ├── zh-CN.ts │ │ │ │ ├── zh-TW.ts │ │ │ │ └── en-US.ts │ │ │ └── store.ts │ │ └── register │ │ │ ├── data.d.ts │ │ │ ├── service.ts │ │ │ ├── locales │ │ │ ├── zh-CN.ts │ │ │ ├── zh-TW.ts │ │ │ └── en-US.ts │ │ │ └── store.ts │ ├── home │ │ ├── components │ │ │ ├── HotSearchCard │ │ │ │ ├── data.d.ts │ │ │ │ └── index.vue │ │ │ ├── ArticleHitCard │ │ │ │ ├── data.d.ts │ │ │ │ └── index.vue │ │ │ ├── WorksHitCard │ │ │ │ ├── data.d.ts │ │ │ │ └── index.vue │ │ │ ├── HotTagsCard │ │ │ │ ├── data.d.ts │ │ │ │ └── index.vue │ │ │ ├── PageLoading │ │ │ │ └── index.vue │ │ │ ├── WorksChartCard │ │ │ │ ├── service.ts │ │ │ │ └── data.d.ts │ │ │ ├── LinksChartCard │ │ │ │ ├── service.ts │ │ │ │ └── data.d.ts │ │ │ ├── TopicsChartCard │ │ │ │ ├── service.ts │ │ │ │ └── data.d.ts │ │ │ └── ArticleChartCard │ │ │ │ ├── service.ts │ │ │ │ └── data.d.ts │ │ ├── service.ts │ │ ├── data.d.ts │ │ ├── locales │ │ │ ├── zh-CN.ts │ │ │ ├── zh-TW.ts │ │ │ └── en-US.ts │ │ └── index.vue │ ├── pagesample │ │ ├── form │ │ │ ├── complex │ │ │ │ ├── components │ │ │ │ │ └── TableForm │ │ │ │ │ │ └── data.d.ts │ │ │ │ ├── service.ts │ │ │ │ ├── data.d.ts │ │ │ │ └── store.ts │ │ │ └── basic │ │ │ │ ├── data.d.ts │ │ │ │ ├── service.ts │ │ │ │ └── store.ts │ │ ├── detail │ │ │ ├── basic │ │ │ │ ├── service.ts │ │ │ │ ├── data.d.ts │ │ │ │ └── store.ts │ │ │ ├── module │ │ │ │ ├── service.ts │ │ │ │ ├── data.d.ts │ │ │ │ └── store.ts │ │ │ └── table │ │ │ │ ├── service.ts │ │ │ │ ├── data.d.ts │ │ │ │ └── store.ts │ │ └── list │ │ │ ├── basic │ │ │ ├── data.d.ts │ │ │ ├── service.ts │ │ │ └── components │ │ │ │ └── TypeSelect.vue │ │ │ ├── table │ │ │ ├── data.d.ts │ │ │ ├── service.ts │ │ │ └── components │ │ │ │ └── TypeSelect.vue │ │ │ ├── search │ │ │ └── table │ │ │ │ ├── data.d.ts │ │ │ │ ├── service.ts │ │ │ │ └── components │ │ │ │ └── TypeSelect.vue │ │ │ └── highly-adaptive-table │ │ │ ├── data.d.ts │ │ │ ├── service.ts │ │ │ └── components │ │ │ ├── TypeSelect.vue │ │ │ └── SearchDrawer.vue │ ├── roles │ │ ├── test │ │ │ └── index.vue │ │ └── user │ │ │ └── index.vue │ └── refresh │ │ └── index.vue ├── locales │ ├── zh-CN.ts │ ├── zh-TW.ts │ └── en-US.ts ├── utils │ ├── validate.ts │ ├── array.ts │ ├── localToken.ts │ ├── mock │ │ ├── require-context.js │ │ └── server.js │ ├── object.ts │ └── store.ts ├── config │ ├── store.ts │ ├── i18n.ts │ ├── settings.ts │ └── routes.ts ├── shims-vue.d.ts ├── services │ └── user.ts ├── main.ts ├── App.vue ├── composables │ ├── useTitle.ts │ ├── useI18nAntdFormVaildateInfos.ts │ └── useEcharts.ts ├── directives │ └── permission │ │ └── index.ts └── store │ ├── user.ts │ └── global.ts ├── .browserslistrc ├── public ├── favicon.ico └── index.html ├── babel.config.js ├── jest.config.js ├── .env.development ├── .env.production ├── .gitignore ├── tests └── unit │ └── example.spec.ts ├── tsconfig.json ├── mock ├── global.js ├── user.js ├── pagesample.js └── home.js ├── .eslintrc.js ├── LICENSE ├── package.json └── vue.config.js /src/assets/css/variables.less: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/layouts/IndexLayout/css/mixins.less: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | not dead 4 | ie 11 5 | -------------------------------------------------------------------------------- /src/layouts/BlankLayout.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lqsong/admin-antd-vue/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /src/components/IconFont/index.ts: -------------------------------------------------------------------------------- 1 | import IconFont from './index.vue'; 2 | 3 | export default IconFont; -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/cli-plugin-babel/preset' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /src/assets/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lqsong/admin-antd-vue/HEAD/src/assets/images/logo.png -------------------------------------------------------------------------------- /src/views/component/icon/svg/locales/zh-CN.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'page.icon.svg.remark.title': '说明:', 3 | }; 4 | -------------------------------------------------------------------------------- /src/views/component/icon/svg/locales/zh-TW.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'page.icon.svg.remark.title': '說明:', 3 | }; 4 | -------------------------------------------------------------------------------- /src/assets/images/bg-smooth.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lqsong/admin-antd-vue/HEAD/src/assets/images/bg-smooth.jpg -------------------------------------------------------------------------------- /src/views/component/icon/svg/locales/en-US.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'page.icon.svg.remark.title': 'Remark:', 3 | }; 4 | -------------------------------------------------------------------------------- /src/views/custom-breadcrumbs/locales/zh-CN.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'page.custom-breadcrumbs.msg': '请看上方面包屑。', 3 | }; 4 | -------------------------------------------------------------------------------- /src/views/custom-breadcrumbs/locales/zh-TW.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'page.custom-breadcrumbs.msg': '請看上方面包屑。', 3 | }; 4 | -------------------------------------------------------------------------------- /src/views/user/login/data.d.ts: -------------------------------------------------------------------------------- 1 | export interface LoginParamsType { 2 | username: string; 3 | password: string; 4 | } 5 | -------------------------------------------------------------------------------- /src/views/home/components/HotSearchCard/data.d.ts: -------------------------------------------------------------------------------- 1 | export interface TableListItem { 2 | name: string; 3 | hit: number; 4 | } 5 | -------------------------------------------------------------------------------- /src/layouts/UserLayout/locales/zh-CN.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'user-layout.menu.login': '登录', 3 | 'user-layout.menu.register': '注册', 4 | }; 5 | -------------------------------------------------------------------------------- /src/layouts/UserLayout/locales/zh-TW.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'user-layout.menu.login': '登錄', 3 | 'user-layout.menu.register': '註冊', 4 | }; 5 | -------------------------------------------------------------------------------- /src/views/custom-breadcrumbs/locales/en-US.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'page.custom-breadcrumbs.msg': 'Look at the crumbs on the top.', 3 | }; 4 | -------------------------------------------------------------------------------- /src/views/home/components/ArticleHitCard/data.d.ts: -------------------------------------------------------------------------------- 1 | export interface TableListItem { 2 | id: number; 3 | title: string; 4 | hit: number; 5 | } 6 | -------------------------------------------------------------------------------- /src/views/home/components/WorksHitCard/data.d.ts: -------------------------------------------------------------------------------- 1 | export interface TableListItem { 2 | id: number; 3 | title: string; 4 | hit: number; 5 | } 6 | -------------------------------------------------------------------------------- /src/views/user/register/data.d.ts: -------------------------------------------------------------------------------- 1 | export interface RegisterParamsType { 2 | username: string; 3 | password: string; 4 | confirm: string; 5 | } 6 | -------------------------------------------------------------------------------- /src/layouts/UserLayout/locales/en-US.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'user-layout.menu.login': 'Login', 3 | 'user-layout.menu.register': 'Register', 4 | }; 5 | -------------------------------------------------------------------------------- /src/views/home/components/HotTagsCard/data.d.ts: -------------------------------------------------------------------------------- 1 | export interface TableListItem { 2 | id: number; 3 | name: string; 4 | hit: number; 5 | pinyin?: string; 6 | } 7 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: '@vue/cli-plugin-unit-jest/presets/typescript-and-babel', 3 | transform: { 4 | '^.+\\.vue$': 'vue-jest' 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/locales/zh-CN.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'empty': 'empty', 3 | 'app.global.menu.notfound': 'Not Found', 4 | 'app.global.form.validatefields.catch': '验证不通过,请检查输入', 5 | }; -------------------------------------------------------------------------------- /src/locales/zh-TW.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'empty': 'empty', 3 | 'app.global.menu.notfound': 'Not Found', 4 | 'app.global.form.validatefields.catch': '驗證不通過,請檢查輸入', 5 | }; -------------------------------------------------------------------------------- /src/views/home/components/PageLoading/index.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/views/pagesample/form/complex/components/TableForm/data.d.ts: -------------------------------------------------------------------------------- 1 | export interface TableFormDataType { 2 | key: string; 3 | name?: string; 4 | workId?: string; 5 | edit?: boolean; 6 | isNew?: boolean; 7 | } -------------------------------------------------------------------------------- /src/locales/en-US.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'empty': 'empty', 3 | 'app.global.menu.notfound': 'Not Found', 4 | 'app.global.form.validatefields.catch': 'The validation did not pass, please check the input', 5 | }; -------------------------------------------------------------------------------- /src/views/roles/test/index.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/views/roles/user/index.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.env.development: -------------------------------------------------------------------------------- 1 | #运行环境 2 | NODE_ENV = 'development' 3 | 4 | # devServer port 5 | VUE_APP_PORT=8000 6 | 7 | # mock 是否开启 true|false , development环境有效 8 | VUE_APP_MOCK = true 9 | 10 | #api接口域名 11 | VUE_APP_APIHOST = /api 12 | -------------------------------------------------------------------------------- /src/views/pagesample/detail/basic/service.ts: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request'; 2 | 3 | export async function queryDetail(): Promise { 4 | return request({ 5 | url: '/pages/detail', 6 | method: 'get' 7 | }); 8 | } 9 | -------------------------------------------------------------------------------- /src/views/pagesample/detail/module/service.ts: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request'; 2 | 3 | export async function queryDetail(): Promise { 4 | return request({ 5 | url: '/pages/detail', 6 | method: 'get' 7 | }); 8 | } 9 | -------------------------------------------------------------------------------- /src/views/pagesample/detail/table/service.ts: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request'; 2 | 3 | export async function queryDetail(): Promise { 4 | return request({ 5 | url: '/pages/detail', 6 | method: 'get' 7 | }); 8 | } 9 | -------------------------------------------------------------------------------- /src/assets/iconsvg/arrow-down.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/iconsvg/arrow-left.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/iconsvg/arrow-right.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/views/home/components/WorksChartCard/service.ts: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request'; 2 | 3 | export async function weeknewWorks(): Promise { 4 | return request({ 5 | url: '/home/works/weeknew', 6 | method: 'get' 7 | }); 8 | } -------------------------------------------------------------------------------- /src/views/pagesample/form/basic/data.d.ts: -------------------------------------------------------------------------------- 1 | export interface FormDataType { 2 | title: string; 3 | date: string[]; 4 | select: string; 5 | radio1: string; 6 | radio2: string; 7 | checkbox: string[]; 8 | remark: string; 9 | } 10 | -------------------------------------------------------------------------------- /src/views/home/components/LinksChartCard/service.ts: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request'; 2 | 3 | export async function annualnewLinks(): Promise { 4 | return request({ 5 | url: '/home/links/annualnew', 6 | method: 'get' 7 | }); 8 | } -------------------------------------------------------------------------------- /.env.production: -------------------------------------------------------------------------------- 1 | #运行环境 2 | NODE_ENV = 'production' 3 | 4 | # devServer port 5 | VUE_APP_PORT=8000 6 | 7 | # mock 是否开启 true|false , development环境有效 8 | VUE_APP_MOCK = false 9 | 10 | #api接口域名 11 | VUE_APP_APIHOST = http://yapi.liqingsong.cc/mock/11 12 | -------------------------------------------------------------------------------- /src/utils/validate.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 判断是否是外链 3 | * @param {string} path 4 | * @returns {Boolean} 5 | * @author LiQingSong 6 | */ 7 | export const isExternal = (path: string): boolean => { 8 | return /^(https?:|mailto:|tel:)/.test(path); 9 | }; 10 | -------------------------------------------------------------------------------- /src/views/home/components/TopicsChartCard/service.ts: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request'; 2 | 3 | export async function monthnewTopics(): Promise { 4 | return request({ 5 | url: '/home/topics/monthnew', 6 | method: 'get' 7 | }); 8 | } 9 | -------------------------------------------------------------------------------- /src/views/home/components/ArticleChartCard/service.ts: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request'; 2 | 3 | export async function dailynewArticles(): Promise { 4 | return request({ 5 | url: '/home/articles/dailynew', 6 | method: 'get' 7 | }); 8 | } 9 | -------------------------------------------------------------------------------- /src/views/home/components/LinksChartCard/data.d.ts: -------------------------------------------------------------------------------- 1 | export interface ChartDataType { 2 | day: string[]; 3 | num: number[]; 4 | } 5 | 6 | export interface LinksChartDataType { 7 | total: number; 8 | num: number; 9 | chart: ChartDataType; 10 | } 11 | -------------------------------------------------------------------------------- /src/components/ScreenTable/data.d.ts: -------------------------------------------------------------------------------- 1 | 2 | export interface PaginationConfig { 3 | total: number; 4 | current: number; 5 | pageSize: number; 6 | showSizeChanger: boolean; 7 | showQuickJumper: boolean; 8 | onChange: (page: number, pageSize: number) => void; 9 | } -------------------------------------------------------------------------------- /src/views/home/components/TopicsChartCard/data.d.ts: -------------------------------------------------------------------------------- 1 | export interface ChartDataType { 2 | day: string[]; 3 | num: number[]; 4 | } 5 | 6 | export interface TopicsChartDataType { 7 | total: number; 8 | num: number; 9 | chart: ChartDataType; 10 | } 11 | -------------------------------------------------------------------------------- /src/views/home/components/WorksChartCard/data.d.ts: -------------------------------------------------------------------------------- 1 | export interface ChartDataType { 2 | day: string[]; 3 | num: number[]; 4 | } 5 | 6 | 7 | export interface WorksChartDataType { 8 | total: number; 9 | num: number; 10 | chart: ChartDataType; 11 | } 12 | -------------------------------------------------------------------------------- /src/config/store.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Store 入口 3 | * @author LiQingSong 4 | */ 5 | import { createStore } from 'vuex'; 6 | import { importAllStore } from '@/utils/store'; 7 | 8 | 9 | export default createStore({ 10 | modules: importAllStore(), 11 | getters: {} 12 | }) 13 | -------------------------------------------------------------------------------- /src/views/home/components/ArticleChartCard/data.d.ts: -------------------------------------------------------------------------------- 1 | export interface ChartDataType { 2 | day: string[]; 3 | num: number[]; 4 | } 5 | 6 | export interface ArticleChartDataType { 7 | total: number; 8 | num: number; 9 | week: number; 10 | day: number; 11 | } 12 | -------------------------------------------------------------------------------- /src/shims-vue.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.vue' { 2 | import type { DefineComponent } from 'vue' 3 | const component: DefineComponent<{}, {}, any> 4 | export default component 5 | } 6 | 7 | declare module '@ckeditor/ckeditor5-vue'; 8 | declare module '@ckeditor/ckeditor5-build-decoupled-document'; 9 | -------------------------------------------------------------------------------- /src/assets/css/global.less: -------------------------------------------------------------------------------- 1 | @import '~ant-design-vue/dist/antd.less'; 2 | @import './antd-variables.less'; 3 | @import './variables.less'; 4 | @import './mixin.less'; 5 | 6 | #nprogress .bar { 7 | background: @primary-color !important; 8 | } 9 | 10 | .text-align-right{ 11 | text-align: right; 12 | } 13 | 14 | -------------------------------------------------------------------------------- /src/views/pagesample/form/basic/service.ts: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request'; 2 | import { FormDataType } from './data.d'; 3 | 4 | export async function createData(params: FormDataType): Promise { 5 | return request({ 6 | url: '/pages/form', 7 | method: 'POST', 8 | data: params, 9 | }); 10 | } 11 | -------------------------------------------------------------------------------- /src/views/pagesample/form/complex/service.ts: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request'; 2 | import { FormDataType } from './data.d'; 3 | 4 | export async function createData(params: FormDataType): Promise { 5 | return request({ 6 | url: '/pages/form', 7 | method: 'POST', 8 | data: params, 9 | }); 10 | } 11 | -------------------------------------------------------------------------------- /src/views/user/login/service.ts: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request'; 2 | import { LoginParamsType } from './data.d'; 3 | 4 | export async function accountLogin(params: LoginParamsType): Promise { 5 | return request({ 6 | url: '/user/login', 7 | method: 'POST', 8 | data: params, 9 | }); 10 | } -------------------------------------------------------------------------------- /src/views/user/register/service.ts: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request'; 2 | import { RegisterParamsType } from './data.d'; 3 | 4 | export async function accountReg(params: RegisterParamsType): Promise { 5 | return request({ 6 | url: '/user/register', 7 | method: 'POST', 8 | data: params, 9 | }); 10 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | 6 | # local env files 7 | .env.local 8 | .env.*.local 9 | 10 | # Log files 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | pnpm-debug.log* 15 | 16 | # Editor directories and files 17 | .idea 18 | .vscode 19 | *.suo 20 | *.ntvs* 21 | *.njsproj 22 | *.sln 23 | *.sw? 24 | -------------------------------------------------------------------------------- /src/views/pagesample/form/complex/data.d.ts: -------------------------------------------------------------------------------- 1 | import { TableFormDataType } from "./components/TableForm/data.d"; 2 | export interface FormDataType { 3 | title: string; 4 | date: string[]; 5 | select: string; 6 | radio1: string; 7 | radio2: string; 8 | checkbox: string[]; 9 | remark: string; 10 | users: TableFormDataType[]; 11 | } 12 | -------------------------------------------------------------------------------- /src/services/user.ts: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request'; 2 | 3 | export async function queryCurrent(): Promise { 4 | return request({ 5 | url: '/user/info', 6 | method: 'get' 7 | }); 8 | } 9 | 10 | export async function queryMessage(): Promise { 11 | return request({ 12 | url: '/user/message' 13 | }); 14 | } -------------------------------------------------------------------------------- /src/layouts/IndexLayout/css/var.less: -------------------------------------------------------------------------------- 1 | // Menu 2 | @menu-collapsed-width: 54px; 3 | 4 | // dark theme 5 | @menu-dark-bg: #222834; 6 | 7 | 8 | // 主窗口背景色 9 | @mainBgColor: #f0f3f4; 10 | // 左边宽度 11 | @leftSideBarWidth: 200px; 12 | // 头部高度 13 | @headerHeight: 50px; 14 | // 头部下方面包屑高度 15 | @headerBreadcrumbHeight: 34px; 16 | // 头部面包屑下tab导航高度 17 | @headerTabNavHeight: 36px; -------------------------------------------------------------------------------- /src/assets/iconsvg/svgo.yml: -------------------------------------------------------------------------------- 1 | # replace default config 2 | 3 | # multipass: true 4 | # full: true 5 | 6 | plugins: 7 | 8 | # - name 9 | # 10 | # or: 11 | # - name: false 12 | # - name: true 13 | # 14 | # or: 15 | # - name: 16 | # param1: 1 17 | # param2: 2 18 | 19 | - removeAttrs: 20 | attrs: 21 | - 'fill' 22 | - 'fill-rule' 23 | -------------------------------------------------------------------------------- /src/views/404/index.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/unit/example.spec.ts: -------------------------------------------------------------------------------- 1 | import { shallowMount } from '@vue/test-utils' 2 | import HelloWorld from '@/components/HelloWorld.vue' 3 | 4 | describe('HelloWorld.vue', () => { 5 | it('renders props.msg when passed', () => { 6 | const msg = 'new message' 7 | const wrapper = shallowMount(HelloWorld, { 8 | props: { msg } 9 | }) 10 | expect(wrapper.text()).toMatch(msg) 11 | }) 12 | }) 13 | -------------------------------------------------------------------------------- /src/layouts/UniversalLayout/css/mixins.less: -------------------------------------------------------------------------------- 1 | .layout-menu() { 2 | &.ant-menu-horizontal { 3 | line-height: 48px; 4 | } 5 | 6 | } 7 | 8 | .light-menu() { 9 | --universallayout-menu-bg-color: #FFF; 10 | --universallayout-submenu-bg-color: #fafafa; 11 | --universallayout-menu-color: rgba(0,0,0,.85); 12 | --universallayout-menu-higlight-bg-color: #e6f7ff; 13 | --universallayout-menu-highlight-color: @primary-color; 14 | } 15 | -------------------------------------------------------------------------------- /src/assets/iconsvg/tick.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/iconsvg/refresh.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/layouts/UniversalLayout/components/RightFooter.vue: -------------------------------------------------------------------------------- 1 | 4 | 9 | 19 | -------------------------------------------------------------------------------- /src/views/refresh/index.vue: -------------------------------------------------------------------------------- 1 | 18 | -------------------------------------------------------------------------------- /src/layouts/UserLayout/routes.ts: -------------------------------------------------------------------------------- 1 | import { RoutesDataItem } from "@/utils/routes"; 2 | 3 | const UserLayoutRoutes: RoutesDataItem[] = [ 4 | { 5 | title: 'user-layout.menu.login', 6 | path: 'login', 7 | component: () => import('@/views/user/login/index.vue'), 8 | }, 9 | { 10 | title: 'user-layout.menu.register', 11 | path: 'register', 12 | component: () => import('@/views/user/register/index.vue'), 13 | } 14 | ]; 15 | 16 | export default UserLayoutRoutes; -------------------------------------------------------------------------------- /src/assets/iconsvg/close.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/IconSvg/index.ts: -------------------------------------------------------------------------------- 1 | import iconsvg from "./index.vue"; 2 | 3 | /** 4 | * 自动导入 @/assets/iconsvg 下svg文件 5 | * @author LiQingSong 6 | */ 7 | export function importAllSvg (): void { 8 | try { 9 | const requireContext: __WebpackModuleApi.RequireContext = require.context('../../assets/iconsvg', false, /\.svg$/); 10 | requireContext.keys().forEach(requireContext); 11 | } catch (error) { 12 | // eslint-disable-next-line no-console 13 | console.log(error); 14 | } 15 | } 16 | 17 | export default iconsvg; -------------------------------------------------------------------------------- /src/assets/iconsvg/home.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/views/user/login/locales/zh-CN.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'page.user.login.form-item-username': '用户名: admin or test or user', 3 | 'page.user.login.form-item-username.required': '请输入用户名', 4 | 'page.user.login.form-item-password': '密码:123456', 5 | 'page.user.login.form-item-password.required': '请输入密码', 6 | 'page.user.login.form.title': '账户登录', 7 | 'page.user.login.form.btn-submit': '登录', 8 | 'page.user.login.form.btn-jump': '还没有账户?现在注册!', 9 | 'page.user.login.form.login-error': '用户名或密码错误!', 10 | 'page.user.login.form.login-success': '登录成功!', 11 | }; 12 | -------------------------------------------------------------------------------- /src/views/user/login/locales/zh-TW.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'page.user.login.form-item-username': '用戶名: admin or test or user', 3 | 'page.user.login.form-item-username.required': '請輸入用戶名', 4 | 'page.user.login.form-item-password': '密碼: 123456', 5 | 'page.user.login.form-item-password.required': '請輸入密碼', 6 | 'page.user.login.form.title': '賬戶登錄', 7 | 'page.user.login.form.btn-submit': '登錄', 8 | 'page.user.login.form.btn-jump': '還沒有賬戶?現在註冊!', 9 | 'page.user.login.form.login-error': '用戶名或密碼錯誤!', 10 | 'page.user.login.form.login-success': '登錄成功!', 11 | }; 12 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue'; 2 | 3 | // 全局样式 4 | import '@/assets/css/global.less'; 5 | 6 | // 引入 Antd 7 | import Antd from 'ant-design-vue'; 8 | 9 | // 导入 svg 10 | import { importAllSvg } from "@/components/IconSvg/index"; 11 | importAllSvg(); 12 | 13 | import App from '@/App.vue'; 14 | import router from '@/config/routes'; 15 | import store from '@/config/store'; 16 | import i18n from '@/config/i18n'; 17 | 18 | 19 | const app = createApp(App) 20 | app.use(store); 21 | app.use(router) 22 | app.use(Antd); 23 | app.use(i18n); 24 | app.mount('#app'); 25 | -------------------------------------------------------------------------------- /src/views/pagesample/list/basic/data.d.ts: -------------------------------------------------------------------------------- 1 | export interface TableListQueryParams { 2 | page: number; 3 | per: number; 4 | } 5 | 6 | export interface PaginationConfig { 7 | total: number; 8 | current: number; 9 | pageSize: number; 10 | showSizeChanger: boolean; 11 | showQuickJumper: boolean; 12 | } 13 | 14 | export interface TableListItem { 15 | id: number; 16 | name: string; 17 | desc: string; 18 | href: string; 19 | type: string; 20 | } 21 | 22 | export interface TableDataType { 23 | list: TableListItem[]; 24 | pagination: PaginationConfig; 25 | } 26 | -------------------------------------------------------------------------------- /src/views/pagesample/list/table/data.d.ts: -------------------------------------------------------------------------------- 1 | export interface TableListQueryParams { 2 | page: number; 3 | per: number; 4 | } 5 | 6 | export interface PaginationConfig { 7 | total: number; 8 | current: number; 9 | pageSize: number; 10 | showSizeChanger: boolean; 11 | showQuickJumper: boolean; 12 | } 13 | 14 | export interface TableListItem { 15 | id: number; 16 | name: string; 17 | desc: string; 18 | href: string; 19 | type: string; 20 | } 21 | 22 | export interface TableDataType { 23 | list: TableListItem[]; 24 | pagination: PaginationConfig; 25 | } 26 | -------------------------------------------------------------------------------- /src/views/pagesample/list/search/table/data.d.ts: -------------------------------------------------------------------------------- 1 | export interface TableListQueryParams { 2 | page: number; 3 | per: number; 4 | } 5 | 6 | export interface PaginationConfig { 7 | total: number; 8 | current: number; 9 | pageSize: number; 10 | showSizeChanger: boolean; 11 | showQuickJumper: boolean; 12 | } 13 | 14 | export interface TableListItem { 15 | id: number; 16 | name: string; 17 | desc: string; 18 | href: string; 19 | type: string; 20 | } 21 | 22 | export interface TableDataType { 23 | list: TableListItem[]; 24 | pagination: PaginationConfig; 25 | } 26 | -------------------------------------------------------------------------------- /src/assets/iconsvg/list.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/views/pagesample/list/highly-adaptive-table/data.d.ts: -------------------------------------------------------------------------------- 1 | export interface TableListQueryParams { 2 | page: number; 3 | per: number; 4 | } 5 | 6 | export interface PaginationConfig { 7 | total: number; 8 | current: number; 9 | pageSize: number; 10 | showSizeChanger: boolean; 11 | showQuickJumper: boolean; 12 | } 13 | 14 | export interface TableListItem { 15 | id: number; 16 | name: string; 17 | desc: string; 18 | href: string; 19 | type: string; 20 | } 21 | 22 | export interface TableDataType { 23 | list: TableListItem[]; 24 | pagination: PaginationConfig; 25 | } 26 | -------------------------------------------------------------------------------- /src/assets/iconsvg/arrow-left2.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/iconsvg/arrow-right2.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/iconsvg/icon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/iconsvg/more.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/views/user/login/locales/en-US.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'page.user.login.form-item-username': 'username: admin or test or user', 3 | 'page.user.login.form-item-username.required': 'Please input your username', 4 | 'page.user.login.form-item-password': 'password: 123465', 5 | 'page.user.login.form-item-password.required': 'Please input your password', 6 | 'page.user.login.form.title': 'Account Login', 7 | 'page.user.login.form.btn-submit': 'Sign in', 8 | 'page.user.login.form.btn-jump': 'or register now!', 9 | 'page.user.login.form.login-error': 'Wrong username or password!', 10 | 'page.user.login.form.login-success': 'Login successful!', 11 | }; 12 | -------------------------------------------------------------------------------- /src/views/user/register/locales/zh-CN.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'page.user.register.form-item-username': '用户名', 3 | 'page.user.register.form-item-username.required': '请输入用户名', 4 | 'page.user.register.form-item-password': '密码', 5 | 'page.user.register.form-item-password.required': '请输入密码', 6 | 'page.user.register.form-item-confirmpassword': '确认密码', 7 | 'page.user.register.form-item-confirmpassword.compare': 8 | '您输入的两个密码不匹配!', 9 | 'page.user.register.form.title': '注册账户', 10 | 'page.user.register.form.btn-submit': '注册', 11 | 'page.user.register.form.btn-jump': '已有账户?现在登录!', 12 | 'page.user.register.form.register-success': '注册成功,请登录!', 13 | }; 14 | -------------------------------------------------------------------------------- /src/views/user/register/locales/zh-TW.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'page.user.register.form-item-username': '用戶名', 3 | 'page.user.register.form-item-username.required': '請輸入用戶名', 4 | 'page.user.register.form-item-password': '密碼', 5 | 'page.user.register.form-item-password.required': '請輸入密碼', 6 | 'page.user.register.form-item-confirmpassword': '確認密碼', 7 | 'page.user.register.form-item-confirmpassword.compare': 8 | '您輸入的兩個密碼不匹配!', 9 | 'page.user.register.form.title': '註冊賬戶', 10 | 'page.user.register.form.btn-submit': '註冊', 11 | 'page.user.register.form.btn-jump': '已有賬戶?現在登錄!', 12 | 'page.user.register.form.register-success': '註冊成功,請登錄!', 13 | }; 14 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | <%= htmlWebpackPlugin.options.title %> 9 | 10 | 11 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/assets/iconsvg/editor.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /src/views/custom-breadcrumbs/index.vue: -------------------------------------------------------------------------------- 1 | 9 | -------------------------------------------------------------------------------- /src/utils/array.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 数组合并并去重 3 | * @param arr1 数组1 4 | * @param arr2 数组2 5 | */ 6 | export function mergeUnique(arr1: Array, arr2: Array): Array { 7 | const arr: Array = arr1; 8 | for (let index = 0, len = arr2.length; index < len; index += 1) { 9 | if (!arr.includes(arr2[index])) { 10 | arr.push(arr2[index]); 11 | } 12 | } 13 | 14 | return arr; 15 | } 16 | 17 | /** 18 | * 数组去重 19 | * @param arr 数组 20 | */ 21 | export function unique(arr: Array): Array { 22 | const array: Array = []; 23 | for (let index = 0, len = arr.length; index < len; index += 1) { 24 | if (!array.includes(arr[index])) { 25 | array.push(arr[index]); 26 | } 27 | } 28 | return array; 29 | } 30 | -------------------------------------------------------------------------------- /src/layouts/IndexLayout/components/Icon.vue: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /src/assets/css/antd-variables.less: -------------------------------------------------------------------------------- 1 | // 此文件针对 ant-design 定制主题: 2 | 3 | /* @primary-color: #1890ff; // 全局主色 */ 4 | @primary-color: #009688; 5 | @link-color: #1890ff; // 链接色 6 | @success-color: #52c41a; // 成功色 7 | @warning-color: #faad14; // 警告色 8 | @error-color: #f5222d; // 错误色 9 | @font-size-base: 14px; // 主字号 10 | @heading-color: rgba(0, 0, 0, 0.85); // 标题色 11 | @text-color: rgba(0, 0, 0, 0.65); // 主文本色 12 | @text-color-secondary: rgba(0, 0, 0, 0.45); // 次文本色 13 | @disabled-color: rgba(0, 0, 0, 0.25); // 失效色 14 | @border-radius-base: 2px; // 组件/浮层圆角 15 | @border-color-base: #d9d9d9; // 边框色 16 | @box-shadow-base: 0 3px 6px -4px rgba(0, 0, 0, 0.12), 17 | 0 6px 16px 0 rgba(0, 0, 0, 0.08), 0 9px 28px 8px rgba(0, 0, 0, 0.05); // 浮层阴影 18 | 19 | // Menu 20 | @menu-collapsed-width: 54px; -------------------------------------------------------------------------------- /src/layouts/UniversalLayout/components/Icon.vue: -------------------------------------------------------------------------------- 1 | 4 | 29 | -------------------------------------------------------------------------------- /src/composables/useTitle.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 设置 html Title composables 3 | * @author LiQingSong 4 | */ 5 | import { ComputedRef, onMounted, Ref, watch } from 'vue'; 6 | import { useI18n } from 'vue-i18n'; 7 | import settings from '@/config/settings'; 8 | import { RoutesDataItem } from '@/utils/routes'; 9 | 10 | export default function useTitle(route: ComputedRef | Ref): void { 11 | const{ t } = useI18n(); 12 | 13 | const setTitle = (title: string): void => { 14 | document.title = `${t(title)} - ${settings.siteTitle}`; 15 | } 16 | 17 | watch(route,() => { 18 | setTitle(route.value.title || ''); 19 | }) 20 | 21 | onMounted(()=> { 22 | setTitle(route.value.title); 23 | }) 24 | 25 | } -------------------------------------------------------------------------------- /src/assets/iconsvg/close2.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/ALink/index.vue: -------------------------------------------------------------------------------- 1 | 9 | -------------------------------------------------------------------------------- /src/layouts/UniversalLayout/css/var.less: -------------------------------------------------------------------------------- 1 | :root { 2 | // 网页背景色 3 | --universallayout-bg-color: #FFFFFF; 4 | // 网页字体色 5 | --universallayout-color: rgba(0,0,0,.85); 6 | // 主窗口背景色 7 | --universallayout-main-bg-color: #f0f3f4; 8 | 9 | // 菜单 10 | --universallayout-menu-collapsed-width: @menu-collapsed-width; 11 | --universallayout-menu-bg-color: #001529; 12 | --universallayout-submenu-bg-color: #000c17; 13 | --universallayout-menu-color: rgba(255,255,255, 0.65); 14 | --universallayout-menu-higlight-bg-color: @primary-color; 15 | --universallayout-menu-highlight-color: #FFFFFF; 16 | 17 | 18 | // 头部高度 19 | --universallayout-header-height: 48px; 20 | // 左边宽度 21 | --universallayout-left-side-bar-width: 200px; 22 | // 头部Tab导航高度 23 | --universallayout-header-tab-nav-height: 36px; 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/assets/css/mixin.less: -------------------------------------------------------------------------------- 1 | .scrollbar( 2 | @thumb-background: hsla(0,0%,100%,.2), 3 | @thumb-shadow: hsla(0,0%,100%,.05), 4 | @track-background:hsla(0,0%,100%,.15), 5 | @track-shadow: rgba(37,37,37,.05) 6 | ) { 7 | ::-webkit-scrollbar { 8 | width: 6px; 9 | height: 6px; 10 | } 11 | ::-webkit-scrollbar-thumb { 12 | background: @thumb-background; 13 | border-radius: 3px; 14 | box-shadow: inset 0 0 5px @thumb-shadow; 15 | } 16 | ::-webkit-scrollbar-track { 17 | background: @track-background; 18 | border-radius: 3px; 19 | box-shadow: inset 0 0 5px rgba(37,37,37,.05); 20 | } 21 | } 22 | 23 | .scrollbar-light { 24 | .scrollbar( 25 | hsla(0,0%,0%,.2), hsla(0,0%,0%,.05), 26 | hsla(0,0%,0%,.15), rgba(255,255,255,.05) 27 | ) 28 | } 29 | -------------------------------------------------------------------------------- /src/views/component/editor/ckeditor/index.vue: -------------------------------------------------------------------------------- 1 | 11 | 27 | -------------------------------------------------------------------------------- /src/views/user/register/locales/en-US.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'page.user.register.form-item-username': 'Username', 3 | 'page.user.register.form-item-username.required': 4 | 'Please input your username', 5 | 'page.user.register.form-item-password': 'Password', 6 | 'page.user.register.form-item-password.required': 7 | 'Please input your password', 8 | 'page.user.register.form-item-confirmpassword': 'Confirm Password', 9 | 'page.user.register.form-item-confirmpassword.compare': 10 | 'The two passwords that you entered do not match!', 11 | 'page.user.register.form.title': 'Account Registration', 12 | 'page.user.register.form.btn-submit': 'Register', 13 | 'page.user.register.form.btn-jump': 'Already have an account?', 14 | 'page.user.register.form.register-success': 15 | 'Registered successfully, please log in!', 16 | }; 17 | -------------------------------------------------------------------------------- /src/components/FormFooterToolbar/index.vue: -------------------------------------------------------------------------------- 1 | 12 | 17 | 35 | -------------------------------------------------------------------------------- /src/utils/localToken.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 自定义 token 操作 3 | * @author LiQingSong 4 | */ 5 | import localforage from 'localforage'; 6 | import settings from '@/config/settings'; 7 | 8 | /** 9 | * 获取本地Token 10 | */ 11 | export const getToken = async (): Promise => { 12 | return await localforage.getItem(settings.siteTokenKey); 13 | }; 14 | 15 | /** 16 | * 设置存储本地Token 17 | */ 18 | export const setToken = async (token: string): Promise => { 19 | try { 20 | await localforage.setItem(settings.siteTokenKey, token); 21 | return true; 22 | } catch (error) { 23 | return false; 24 | } 25 | }; 26 | 27 | /** 28 | * 移除本地Token 29 | */ 30 | export const removeToken = async (): Promise => { 31 | try { 32 | await localforage.removeItem(settings.siteTokenKey); 33 | return true; 34 | } catch (error) { 35 | return false; 36 | } 37 | }; -------------------------------------------------------------------------------- /src/assets/iconsvg/menu-fold.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/iconsvg/menu-unfold.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/layouts/UniversalLayout/components/RightTopMessage.vue: -------------------------------------------------------------------------------- 1 | 11 | 25 | -------------------------------------------------------------------------------- /src/assets/iconsvg/chart.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/views/component/editor/tui-editor/index.vue: -------------------------------------------------------------------------------- 1 | 11 | 29 | -------------------------------------------------------------------------------- /src/views/home/service.ts: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request'; 2 | import { TableListQueryParams } from './data.d'; 3 | 4 | export async function hotSearchQueryList(params?: TableListQueryParams): Promise { 5 | return request({ 6 | url: '/home/searchs/keywords', 7 | method: 'get', 8 | params, 9 | }); 10 | } 11 | 12 | export async function hotTagsQueryList(params?: TableListQueryParams): Promise { 13 | return request({ 14 | url: '/home/tags', 15 | method: 'get', 16 | params, 17 | }); 18 | } 19 | 20 | export async function articleHitQueryList(params?: TableListQueryParams): Promise { 21 | return request({ 22 | url: '/home/articles', 23 | method: 'get', 24 | params, 25 | }); 26 | } 27 | 28 | export async function worksHitQueryList(params?: TableListQueryParams): Promise { 29 | return request({ 30 | url: '/home/works', 31 | method: 'get', 32 | params, 33 | }); 34 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "strict": true, 6 | "jsx": "preserve", 7 | "importHelpers": true, 8 | "moduleResolution": "node", 9 | "experimentalDecorators": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "sourceMap": true, 14 | "noImplicitAny": false, 15 | "baseUrl": ".", 16 | "types": [ 17 | "webpack-env", 18 | "jest" 19 | ], 20 | "paths": { 21 | "@/*": [ 22 | "src/*" 23 | ] 24 | }, 25 | "lib": [ 26 | "esnext", 27 | "dom", 28 | "dom.iterable", 29 | "scripthost" 30 | ] 31 | }, 32 | "include": [ 33 | "src/**/*.ts", 34 | "src/**/*.tsx", 35 | "src/**/*.vue", 36 | "tests/**/*.ts", 37 | "tests/**/*.tsx" 38 | ], 39 | "exclude": [ 40 | "node_modules" 41 | ] 42 | } 43 | -------------------------------------------------------------------------------- /src/views/pagesample/detail/basic/data.d.ts: -------------------------------------------------------------------------------- 1 | export interface UserInfoDataType { 2 | name: string; 3 | tel: string; 4 | courier: string; 5 | address: string; 6 | remark: string; 7 | } 8 | 9 | export interface RefundApplicationDataType { 10 | ladingNo: string; 11 | saleNo: string; 12 | state: string; 13 | childOrders: string; 14 | } 15 | 16 | export interface ReturnGoodsDataType { 17 | id: string; 18 | name?: string; 19 | barcode?: string; 20 | price?: string; 21 | num?: string | number; 22 | amount?: string | number; 23 | } 24 | 25 | export interface ReturnProgressDataType { 26 | key: string; 27 | time: string; 28 | rate: string; 29 | status: string; 30 | operator: string; 31 | cost: string; 32 | } 33 | 34 | export interface DetailDataType { 35 | userInfo: UserInfoDataType; 36 | refundApplication: RefundApplicationDataType; 37 | returnGoods: ReturnGoodsDataType[]; 38 | returnProgress: ReturnProgressDataType[]; 39 | } 40 | -------------------------------------------------------------------------------- /src/views/pagesample/detail/module/data.d.ts: -------------------------------------------------------------------------------- 1 | export interface UserInfoDataType { 2 | name: string; 3 | tel: string; 4 | courier: string; 5 | address: string; 6 | remark: string; 7 | } 8 | 9 | export interface RefundApplicationDataType { 10 | ladingNo: string; 11 | saleNo: string; 12 | state: string; 13 | childOrders: string; 14 | } 15 | 16 | export interface ReturnGoodsDataType { 17 | id: string; 18 | name?: string; 19 | barcode?: string; 20 | price?: string; 21 | num?: string | number; 22 | amount?: string | number; 23 | } 24 | 25 | export interface ReturnProgressDataType { 26 | key: string; 27 | time: string; 28 | rate: string; 29 | status: string; 30 | operator: string; 31 | cost: string; 32 | } 33 | 34 | export interface DetailDataType { 35 | userInfo: UserInfoDataType; 36 | refundApplication: RefundApplicationDataType; 37 | returnGoods: ReturnGoodsDataType[]; 38 | returnProgress: ReturnProgressDataType[]; 39 | } 40 | -------------------------------------------------------------------------------- /src/views/pagesample/detail/table/data.d.ts: -------------------------------------------------------------------------------- 1 | export interface UserInfoDataType { 2 | name: string; 3 | tel: string; 4 | courier: string; 5 | address: string; 6 | remark: string; 7 | } 8 | 9 | export interface RefundApplicationDataType { 10 | ladingNo: string; 11 | saleNo: string; 12 | state: string; 13 | childOrders: string; 14 | } 15 | 16 | export interface ReturnGoodsDataType { 17 | id: string; 18 | name?: string; 19 | barcode?: string; 20 | price?: string; 21 | num?: string | number; 22 | amount?: string | number; 23 | } 24 | 25 | export interface ReturnProgressDataType { 26 | key: string; 27 | time: string; 28 | rate: string; 29 | status: string; 30 | operator: string; 31 | cost: string; 32 | } 33 | 34 | export interface DetailDataType { 35 | userInfo: UserInfoDataType; 36 | refundApplication: RefundApplicationDataType; 37 | returnGoods: ReturnGoodsDataType[]; 38 | returnProgress: ReturnProgressDataType[]; 39 | } 40 | -------------------------------------------------------------------------------- /mock/global.js: -------------------------------------------------------------------------------- 1 | const { VUE_APP_APIHOST } = process.env; 2 | const mock = {}; 3 | 4 | mock[`POST ${VUE_APP_APIHOST || ''}/uploads`] = (req, res) => { 5 | res.send({ 6 | code: 0, 7 | data: { 8 | id: 1, 9 | url: 10 | 'http://uploads.liqingsong.cc/20200531/583057e8-8bab-4eee-b5a0-bec915089c0c.jpg', 11 | name: 'xcx.jpg', 12 | }, 13 | msg: '', 14 | }); 15 | }; 16 | 17 | mock[`GET ${VUE_APP_APIHOST}/500`] = (req, res) => { 18 | res.status(500).send({ 19 | timestamp: 1513932555104, 20 | status: 500, 21 | error: 'error', 22 | message: 'error', 23 | path: '/500', 24 | }); 25 | }; 26 | 27 | mock[`GET ${VUE_APP_APIHOST}/404`] = (req, res) => { 28 | res.status(404).send({ 29 | timestamp: 1513932643431, 30 | status: 404, 31 | error: 'Not Found', 32 | message: 'No message available', 33 | path: '/404', 34 | }); 35 | }; 36 | 37 | module.exports = { 38 | ...mock 39 | }; -------------------------------------------------------------------------------- /src/composables/useI18nAntdFormVaildateInfos.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 重置 Antd Form VaildateInfos 为 I18n 3 | * @author LiQingSong 4 | */ 5 | import { computed, ComputedRef } from 'vue'; 6 | import { useI18n } from 'vue-i18n'; 7 | import { validateInfos } from 'ant-design-vue/lib/form/useForm'; 8 | 9 | export default function useI18nAntdFormVaildateInfos(infos: validateInfos): ComputedRef { 10 | const{ t } = useI18n(); 11 | 12 | const infosNew = computed(() => { 13 | const vInfos: validateInfos = {}; 14 | for (const index in infos) { 15 | vInfos[index] = JSON.parse(JSON.stringify(infos[index])); 16 | if(vInfos[index] && vInfos[index]['help']) { 17 | vInfos[index]['help'] = vInfos[index]['help'].map((item: any)=> typeof(item)=='string' ? t(item) : item.map((item2: any)=> item2 ? t(item2):'')); 18 | } 19 | } 20 | return vInfos; 21 | }); 22 | 23 | return infosNew; 24 | } -------------------------------------------------------------------------------- /src/assets/iconsvg/control.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/iconsvg/message.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/views/pagesample/list/basic/service.ts: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request'; 2 | import { TableListQueryParams, TableListItem } from './data.d'; 3 | 4 | export async function queryList(params?: TableListQueryParams): Promise { 5 | return request({ 6 | url: '/pages/list', 7 | method: 'get', 8 | params, 9 | }); 10 | } 11 | 12 | export async function createData(params: Omit): Promise { 13 | return request({ 14 | url: '/pages/list', 15 | method: 'POST', 16 | data: params, 17 | }); 18 | } 19 | 20 | export async function updateData(id: number, params: Omit): Promise { 21 | return request({ 22 | url: `/pages/list/${id}`, 23 | method: 'PUT', 24 | data: params, 25 | }); 26 | } 27 | 28 | export async function removeData(id: number): Promise { 29 | return request({ 30 | url: `/pages/list/${id}`, 31 | method: 'delete', 32 | }); 33 | } 34 | 35 | export async function detailData(id: number): Promise { 36 | return request({url: `/pages/list/${id}`}); 37 | } 38 | -------------------------------------------------------------------------------- /src/views/pagesample/list/table/service.ts: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request'; 2 | import { TableListQueryParams, TableListItem } from './data.d'; 3 | 4 | export async function queryList(params?: TableListQueryParams): Promise { 5 | return request({ 6 | url: '/pages/list', 7 | method: 'get', 8 | params, 9 | }); 10 | } 11 | 12 | export async function createData(params: Omit): Promise { 13 | return request({ 14 | url: '/pages/list', 15 | method: 'POST', 16 | data: params, 17 | }); 18 | } 19 | 20 | export async function updateData(id: number, params: Omit): Promise { 21 | return request({ 22 | url: `/pages/list/${id}`, 23 | method: 'PUT', 24 | data: params, 25 | }); 26 | } 27 | 28 | export async function removeData(id: number): Promise { 29 | return request({ 30 | url: `/pages/list/${id}`, 31 | method: 'delete', 32 | }); 33 | } 34 | 35 | export async function detailData(id: number): Promise { 36 | return request({url: `/pages/list/${id}`}); 37 | } 38 | -------------------------------------------------------------------------------- /src/views/pagesample/list/search/table/service.ts: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request'; 2 | import { TableListQueryParams, TableListItem } from './data.d'; 3 | 4 | export async function queryList(params?: TableListQueryParams): Promise { 5 | return request({ 6 | url: '/pages/list', 7 | method: 'get', 8 | params, 9 | }); 10 | } 11 | 12 | export async function createData(params: Omit): Promise { 13 | return request({ 14 | url: '/pages/list', 15 | method: 'POST', 16 | data: params, 17 | }); 18 | } 19 | 20 | export async function updateData(id: number, params: Omit): Promise { 21 | return request({ 22 | url: `/pages/list/${id}`, 23 | method: 'PUT', 24 | data: params, 25 | }); 26 | } 27 | 28 | export async function removeData(id: number): Promise { 29 | return request({ 30 | url: `/pages/list/${id}`, 31 | method: 'delete', 32 | }); 33 | } 34 | 35 | export async function detailData(id: number): Promise { 36 | return request({url: `/pages/list/${id}`}); 37 | } 38 | -------------------------------------------------------------------------------- /src/components/BreadCrumbs/index.vue: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /src/views/pagesample/list/highly-adaptive-table/service.ts: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request'; 2 | import { TableListQueryParams, TableListItem } from './data.d'; 3 | 4 | export async function queryList(params?: TableListQueryParams): Promise { 5 | return request({ 6 | url: '/pages/list', 7 | method: 'get', 8 | params, 9 | }); 10 | } 11 | 12 | export async function createData(params: Omit): Promise { 13 | return request({ 14 | url: '/pages/list', 15 | method: 'POST', 16 | data: params, 17 | }); 18 | } 19 | 20 | export async function updateData(id: number, params: Omit): Promise { 21 | return request({ 22 | url: `/pages/list/${id}`, 23 | method: 'PUT', 24 | data: params, 25 | }); 26 | } 27 | 28 | export async function removeData(id: number): Promise { 29 | return request({ 30 | url: `/pages/list/${id}`, 31 | method: 'delete', 32 | }); 33 | } 34 | 35 | export async function detailData(id: number): Promise { 36 | return request({url: `/pages/list/${id}`}); 37 | } 38 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true 5 | }, 6 | 'extends': [ 7 | 'plugin:vue/vue3-essential', 8 | 'eslint:recommended', 9 | '@vue/typescript/recommended' 10 | ], 11 | parserOptions: { 12 | ecmaVersion: 2020 13 | }, 14 | rules: { 15 | 'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off', 16 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off', 17 | '@typescript-eslint/no-explicit-any': ['off'], 18 | '@typescript-eslint/no-unused-vars': ['off'], 19 | '@typescript-eslint/ban-types': ['off'] 20 | }, 21 | overrides: [ 22 | { 23 | files: [ 24 | '**/__tests__/*.{j,t}s?(x)', 25 | '**/tests/unit/**/*.spec.{j,t}s?(x)' 26 | ], 27 | env: { 28 | jest: true 29 | } 30 | }, 31 | { 32 | files: [ 33 | './mock/*.{j,t}s?(x)', 34 | './src/utils/mock/*.{j,t}s?(x)' 35 | ], 36 | rules: { 37 | 'no-var': ['off'], 38 | '@typescript-eslint/no-var-requires': ['off'], 39 | } 40 | } 41 | ] 42 | } 43 | -------------------------------------------------------------------------------- /src/assets/iconsvg/detail.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/views/pagesample/form/basic/store.ts: -------------------------------------------------------------------------------- 1 | import { Mutation, Action } from 'vuex'; 2 | import { StoreModuleType } from "@/utils/store"; 3 | import { createData } from './service'; 4 | import { FormDataType } from "./data.d"; 5 | 6 | /* eslint-disable @typescript-eslint/no-empty-interface */ 7 | export interface StateType {} 8 | 9 | export interface ModuleType extends StoreModuleType { 10 | state: StateType; 11 | mutations: { 12 | }; 13 | actions: { 14 | create: Action; 15 | }; 16 | } 17 | 18 | const initState: StateType = {}; 19 | 20 | const StoreModel: ModuleType = { 21 | namespaced: true, 22 | name: 'FormBasic', 23 | state: { 24 | ...initState 25 | }, 26 | mutations: { 27 | }, 28 | actions: { 29 | async create({ commit }, payload: FormDataType) { 30 | try { 31 | await createData(payload); 32 | return true; 33 | } catch (error) { 34 | return false; 35 | } 36 | } 37 | } 38 | } 39 | 40 | export default StoreModel; -------------------------------------------------------------------------------- /src/views/pagesample/form/complex/store.ts: -------------------------------------------------------------------------------- 1 | import { Mutation, Action } from 'vuex'; 2 | import { StoreModuleType } from "@/utils/store"; 3 | import { createData } from './service'; 4 | import { FormDataType } from "./data.d"; 5 | 6 | /* eslint-disable @typescript-eslint/no-empty-interface */ 7 | export interface StateType {} 8 | 9 | export interface ModuleType extends StoreModuleType { 10 | state: StateType; 11 | mutations: { 12 | }; 13 | actions: { 14 | create: Action; 15 | }; 16 | } 17 | 18 | const initState: StateType = {}; 19 | 20 | const StoreModel: ModuleType = { 21 | namespaced: true, 22 | name: 'FormComplex', 23 | state: { 24 | ...initState 25 | }, 26 | mutations: { 27 | }, 28 | actions: { 29 | async create({ commit }, payload: FormDataType) { 30 | try { 31 | await createData(payload); 32 | return true; 33 | } catch (error) { 34 | return false; 35 | } 36 | } 37 | } 38 | } 39 | 40 | export default StoreModel; -------------------------------------------------------------------------------- /src/directives/permission/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 自定义指令 权限验证 3 | * @author LiQingSong 4 | * 使用Demo: 5 | * import permission from '@/directives/permission'; 6 | * import { defineComponent } from "vue"; 7 | * export default defineComponent({ 8 | * directives: { 9 | * permission 10 | * } 11 | * }) 12 | * 删除 13 | * 删除 14 | */ 15 | import { Directive } from "vue"; 16 | import UserModel from "@/store/user"; 17 | import { hasPermissionRouteRoles } from "@/utils/routes"; 18 | 19 | const permission: Directive = (el, binding, vnode, prevVNode) => { 20 | const { value } = binding; 21 | if(value) { 22 | const userRoles = UserModel.state.currentUser.roles; 23 | if(!hasPermissionRouteRoles(userRoles, value)){ 24 | el.parentNode && el.parentNode.removeChild(el); 25 | } 26 | } else { 27 | throw new Error(`need roles! Like v-permission="['admin','test']" or v-permission="'test'"`); 28 | } 29 | } 30 | 31 | export default permission; -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 LiQingSong 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/views/pagesample/list/basic/components/TypeSelect.vue: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /src/views/pagesample/list/table/components/TypeSelect.vue: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /src/components/IconSvg/index.vue: -------------------------------------------------------------------------------- 1 | 6 | 34 | -------------------------------------------------------------------------------- /src/views/pagesample/list/search/table/components/TypeSelect.vue: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /src/views/pagesample/list/highly-adaptive-table/components/TypeSelect.vue: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /src/views/home/data.d.ts: -------------------------------------------------------------------------------- 1 | import { TableListItem as HotSearchTableListItem } from './components/HotSearchCard/data.d'; 2 | import { TableListItem as HotTagsTableListItem } from './components/HotTagsCard/data.d'; 3 | import { TableListItem as ArticleHitTableListItem } from './components/ArticleHitCard/data.d'; 4 | import { TableListItem as WorksHitTableListItem } from './components/WorksHitCard/data.d'; 5 | 6 | export interface TableListQueryParams { 7 | page: number; 8 | per: number; 9 | sort?: number; 10 | } 11 | 12 | export interface PaginationConfig { 13 | total: number; 14 | current: number; 15 | pageSize: number; 16 | showSizeChanger: boolean; 17 | } 18 | 19 | export interface HotSearchDataType { 20 | list: HotSearchTableListItem[]; 21 | pagination: PaginationConfig; 22 | } 23 | 24 | export interface HotTagsDataType { 25 | list: HotTagsTableListItem[]; 26 | pagination: PaginationConfig; 27 | } 28 | 29 | export interface ArticleHitDataType { 30 | list: ArticleHitTableListItem[]; 31 | pagination: PaginationConfig; 32 | } 33 | 34 | export interface WorksHitDataType { 35 | list: WorksHitTableListItem[]; 36 | pagination: PaginationConfig; 37 | } 38 | -------------------------------------------------------------------------------- /src/config/i18n.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 国际化 入口 3 | * @author LiQingSong 4 | */ 5 | 6 | import { createI18n } from "vue-i18n"; 7 | import { getLocale, setLocale, importAllLocales, defaultLang } from "@/utils/i18n"; 8 | 9 | /** 10 | * antd 多语言 配置 11 | */ 12 | import zhCN from 'ant-design-vue/es/locale/zh_CN'; 13 | import zhTW from 'ant-design-vue/es/locale/zh_TW'; 14 | import enUS from 'ant-design-vue/es/locale/en_US'; 15 | export const antdMessages: { [key: string]: any} = { 16 | 'zh-CN': zhCN, 17 | 'zh-TW': zhTW, 18 | 'en-US': enUS, 19 | } 20 | 21 | 22 | /** 23 | * 框架 多语言 配置 24 | */ 25 | export const messages = importAllLocales(); 26 | const sysLocale = getLocale(); 27 | const i18n = createI18n({ 28 | legacy: false, 29 | locale: antdMessages[sysLocale] ? sysLocale : defaultLang, 30 | messages, 31 | }); 32 | 33 | 34 | /** 35 | * 设置语言 36 | * @param locale 37 | */ 38 | export function setI18nLanguage(locale: string, realReload = false): void { 39 | setLocale(locale,realReload, function() { 40 | // i18n.global.locale = locale // legacy: true 41 | i18n.global.locale.value = locale; 42 | }) 43 | } 44 | 45 | export default i18n; 46 | -------------------------------------------------------------------------------- /src/utils/mock/require-context.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 自定义 require.context 3 | * @author LiQingSong 4 | */ 5 | module.exports = function(directory, recursive, regExp) { 6 | const dir = require('node-dir') 7 | const path = require('path') 8 | 9 | // Assume absolute path by default 10 | let basepath = directory 11 | 12 | if (directory[0] === '.') { 13 | // Relative path 14 | basepath = path.join(__dirname, directory) 15 | } else if (!path.isAbsolute(directory)) { 16 | // Module path 17 | basepath = require.resolve(directory) 18 | } 19 | 20 | const keys = dir 21 | .files(basepath, { 22 | sync: true, 23 | recursive: recursive || false 24 | }) 25 | .filter(function(file) { 26 | return file.match(regExp || /\.(json|js)$/) 27 | }) 28 | .map(function(file) { 29 | return path.join('.', file.slice(basepath.length + 1)) 30 | }) 31 | 32 | var context = function(key) { 33 | return require(context.resolve(key)) 34 | } 35 | 36 | context.resolve = function(key) { 37 | return path.join(directory, key) 38 | } 39 | 40 | context.keys = function() { 41 | return keys 42 | } 43 | 44 | return context 45 | } -------------------------------------------------------------------------------- /src/layouts/IndexLayout/components/RightTopMessage.vue: -------------------------------------------------------------------------------- 1 | 11 | -------------------------------------------------------------------------------- /src/utils/object.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 浅比较两个object, json的key是否一致 3 | * @param obj1 4 | * @param obj2 5 | * @returns 6 | */ 7 | export function equalObjectKey(obj1: Object, obj2: Object): boolean{ 8 | const obj1Keys: string[] = Object.keys(obj1); 9 | const obj2Keys: string[] = Object.keys(obj2); 10 | const obj1KeysLen: number = obj1Keys.length; 11 | if(obj1KeysLen!==obj2Keys.length) { 12 | return false; 13 | } 14 | let is = true; 15 | for (let index = 0; index < obj1KeysLen; index++) { 16 | const element: string = obj1Keys[index]; 17 | if(!Object.prototype.hasOwnProperty.call(obj2, element)) { 18 | is = false; 19 | break; 20 | } 21 | } 22 | return is; 23 | } 24 | 25 | /** 26 | * 浅比较两个对象是否相等,这两个对象的值只能是数字或字符串 27 | * @param obj1 28 | * @param obj2 29 | * @returns 30 | */ 31 | export function equalObject(obj1: Object, obj2: Object): boolean { 32 | const obj1Keys: string[] = Object.keys(obj1); 33 | const obj2Keys: string[] = Object.keys(obj2); 34 | const obj1KeysLen: number = obj1Keys.length; 35 | const obj2KeysLen: number = obj2Keys.length; 36 | if(obj1KeysLen!==obj2KeysLen) { 37 | return false; 38 | } 39 | 40 | if(obj1KeysLen===0 && obj2KeysLen===0) { 41 | return true; 42 | } 43 | 44 | return !obj1Keys.some(key => obj1[key] != obj2[key]) 45 | 46 | } -------------------------------------------------------------------------------- /src/layouts/IndexLayout/composables/useTopMenuWidth.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 设置 IndexLayout TopMenuWidth 3 | * @author LiQingSong 4 | */ 5 | import { ComputedRef, onMounted, Ref, ref, watch, nextTick } from 'vue'; 6 | 7 | export default function useTopMenuWidth(topNavEnable: ComputedRef | Ref): { 8 | topMenuCon: Ref; 9 | topMenuWidth: Ref; 10 | } { 11 | 12 | const topMenuCon = ref(null); 13 | 14 | const topMenuWidth = ref('auto'); 15 | 16 | const setWidth = async () => { 17 | await nextTick(); 18 | if (topMenuCon.value && topNavEnable.value) { 19 | let width = 0; 20 | const child = topMenuCon.value.children; 21 | for (let index = 0, len = child.length; index < len; index++) { 22 | const element = child[index] as HTMLElement; 23 | width = width + element.offsetWidth + 0.5; 24 | } 25 | topMenuWidth.value = width + 'px'; 26 | } else { 27 | topMenuWidth.value = 'auto'; 28 | } 29 | }; 30 | 31 | 32 | watch(topNavEnable,(topNavEnable)=> { 33 | setWidth(); 34 | }) 35 | 36 | onMounted(()=> { 37 | 38 | setWidth(); 39 | }) 40 | 41 | 42 | return { 43 | topMenuCon, 44 | topMenuWidth 45 | } 46 | 47 | 48 | 49 | } -------------------------------------------------------------------------------- /src/views/component/icon/font/index.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/iconsvg/language-outline.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/iconsvg/edit.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/Permission/index.vue: -------------------------------------------------------------------------------- 1 | 16 | -------------------------------------------------------------------------------- /src/views/home/locales/zh-CN.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'page.home.text-day': '日', 3 | 'page.home.text-week': '周', 4 | 'page.home.text-month': '月', 5 | 'page.home.text-years': '年', 6 | 'page.home.text-total': '总数量', 7 | 'page.home.text-daycompare': '日同比', 8 | 'page.home.text-weekcompare': '周同比', 9 | 'page.home.articlechartcard.card-title': '随笔', 10 | 'page.home.workschartcard.card-title': '作品', 11 | 'page.home.topicschartcard.card-title': '专题', 12 | 'page.home.linkschartcard.card-title': '左邻右舍', 13 | 14 | 'page.home.hotsearchcard.card-title': '热门搜索', 15 | 'page.home.hotsearchcard.card.table-column-number': '序号', 16 | 'page.home.hotsearchcard.card.table-column-name': '关键词', 17 | 'page.home.hotsearchcard.card.table-column-hit': '次数', 18 | 19 | 'page.home.hottagscard.card-title': '热门标签', 20 | 'page.home.hottagscard.card.table-column-number': '序号', 21 | 'page.home.hottagscard.card.table-column-name': '标签', 22 | 'page.home.hottagscard.card.table-column-hit': '次数', 23 | 24 | 'page.home.articlehitcard.card-title': '随笔浏览量排行', 25 | 'page.home.articlehitcard.card.table-column-number': '序号', 26 | 'page.home.articlehitcard.card.table-column-title': '标题', 27 | 'page.home.articlehitcard.card.table-column-hit': '浏览量', 28 | 29 | 'page.home.workshitcard.card-title': '作品浏览量排行', 30 | 'page.home.workshitcard.card.table-column-number': '序号', 31 | 'page.home.workshitcard.card.table-column-title': '标题', 32 | 'page.home.workshitcard.card.table-column-hit': '浏览量', 33 | }; 34 | -------------------------------------------------------------------------------- /src/views/home/locales/zh-TW.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'page.home.text-day': '日', 3 | 'page.home.text-week': '周', 4 | 'page.home.text-month': '月', 5 | 'page.home.text-years': '年', 6 | 'page.home.text-total': '總數量', 7 | 'page.home.text-daycompare': '日同比', 8 | 'page.home.text-weekcompare': '周同比', 9 | 'page.home.articlechartcard.card-title': '隨筆', 10 | 'page.home.workschartcard.card-title': '作品', 11 | 'page.home.topicschartcard.card-title': '專題', 12 | 'page.home.linkschartcard.card-title': '左鄰右舍', 13 | 14 | 'page.home.hotsearchcard.card-title': '熱門搜索', 15 | 'page.home.hotsearchcard.card.table-column-number': '序號', 16 | 'page.home.hotsearchcard.card.table-column-name': '關鍵詞', 17 | 'page.home.hotsearchcard.card.table-column-hit': '次數', 18 | 19 | 'page.home.hottagscard.card-title': '熱門標簽', 20 | 'page.home.hottagscard.card.table-column-number': '序號', 21 | 'page.home.hottagscard.card.table-column-name': '標簽', 22 | 'page.home.hottagscard.card.table-column-hit': '次數', 23 | 24 | 'page.home.articlehitcard.card-title': '隨筆瀏覽量排行', 25 | 'page.home.articlehitcard.card.table-column-number': '序號', 26 | 'page.home.articlehitcard.card.table-column-title': '標題', 27 | 'page.home.articlehitcard.card.table-column-hit': '瀏覽量', 28 | 29 | 'page.home.workshitcard.card-title': '作品瀏覽量排行', 30 | 'page.home.workshitcard.card.table-column-number': '序號', 31 | 'page.home.workshitcard.card.table-column-title': '標題', 32 | 'page.home.workshitcard.card.table-column-hit': '瀏覽量', 33 | }; 34 | -------------------------------------------------------------------------------- /src/assets/iconsvg/page.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/iconsvg/theme.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/layouts/IndexLayout/components/RightFooter.vue: -------------------------------------------------------------------------------- 1 | 28 | 36 | -------------------------------------------------------------------------------- /src/assets/iconsvg/permissions.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/TuiEditor/viewer.vue: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /src/views/home/locales/en-US.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'page.home.text-day': 'Day', 3 | 'page.home.text-week': 'Weeks', 4 | 'page.home.text-month': 'Month', 5 | 'page.home.text-years': 'Years', 6 | 'page.home.text-total': 'Total', 7 | 'page.home.text-daycompare': 'Day Year-on-year', 8 | 'page.home.text-weekcompare': 'Week Year-on-year', 9 | 'page.home.articlechartcard.card-title': 'Essays', 10 | 'page.home.workschartcard.card-title': 'Works', 11 | 'page.home.topicschartcard.card-title': 'Topics', 12 | 'page.home.linkschartcard.card-title': 'Neighbors', 13 | 14 | 'page.home.hotsearchcard.card-title': 'Top search', 15 | 'page.home.hotsearchcard.card.table-column-number': 'Serial number', 16 | 'page.home.hotsearchcard.card.table-column-name': 'Keywords', 17 | 'page.home.hotsearchcard.card.table-column-hit': 'Hits', 18 | 19 | 'page.home.hottagscard.card-title': 'Hot labels', 20 | 'page.home.hottagscard.card.table-column-number': 'Serial number', 21 | 'page.home.hottagscard.card.table-column-name': 'label', 22 | 'page.home.hottagscard.card.table-column-hit': 'Hits', 23 | 24 | 'page.home.articlehitcard.card-title': 'Essay Traffic Ranking', 25 | 'page.home.articlehitcard.card.table-column-number': 'Serial number', 26 | 'page.home.articlehitcard.card.table-column-title': 'Title', 27 | 'page.home.articlehitcard.card.table-column-hit': 'Hits', 28 | 29 | 'page.home.workshitcard.card-title': 'Works Traffic Ranking', 30 | 'page.home.workshitcard.card.table-column-number': 'Serial number', 31 | 'page.home.workshitcard.card.table-column-title': 'Title', 32 | 'page.home.workshitcard.card.table-column-hit': 'Hits', 33 | }; 34 | -------------------------------------------------------------------------------- /src/layouts/UniversalLayout/components/LeftSider.vue: -------------------------------------------------------------------------------- 1 | 21 | 48 | -------------------------------------------------------------------------------- /src/layouts/UserLayout/index.vue: -------------------------------------------------------------------------------- 1 | 9 | 37 | -------------------------------------------------------------------------------- /src/layouts/IndexLayout/locales/zh-CN.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'index-layout.topmenu.userinfo': '个人信息', 3 | 'index-layout.topmenu.logout': '退出', 4 | 5 | 'index-layout.menu.home': '首页', 6 | 'index-layout.menu.home.workplace': '工作台', 7 | 'index-layout.menu.home.custom-breadcrumbs': '自定义面包屑', 8 | 'index-layout.menu.home.custom-breadcrumbs.liqingsong.cc': 'liqingsong.cc', 9 | 'index-layout.menu.home.docs': '使用文档', 10 | 11 | 'index-layout.menu.component': '组件', 12 | 'index-layout.menu.component.icon': '图标', 13 | 'index-layout.menu.component.icon.svg': 'IconSvg', 14 | 'index-layout.menu.component.icon.font': 'IconFont', 15 | 'index-layout.menu.component.editor': '编辑器', 16 | 'index-layout.menu.component.editor.tui-editor': 'tui-editor', 17 | 'index-layout.menu.component.editor.ckeditor': 'CKEditor', 18 | 19 | 'index-layout.menu.pages': '页面示例', 20 | 'index-layout.menu.pages.list': '列表页面', 21 | 'index-layout.menu.pages.list.basic': '标准列表', 22 | 'index-layout.menu.pages.list.table': '表格列表', 23 | 'index-layout.menu.pages.list.highly-adaptive-table': '高度自适应表格', 24 | 'index-layout.menu.pages.list.search': '搜索列表', 25 | 'index-layout.menu.pages.list.search.table': '查询表格', 26 | 'index-layout.menu.pages.form': '表单页面', 27 | 'index-layout.menu.pages.form.basic': '基础表单', 28 | 'index-layout.menu.pages.form.complex': '高级表单', 29 | 'index-layout.menu.pages.detail': '详情页面', 30 | 'index-layout.menu.pages.detail.basic': '基础详情', 31 | 'index-layout.menu.pages.detail.module': '模块详情', 32 | 'index-layout.menu.pages.detail.table': '表格详情', 33 | 34 | 'index-layout.menu.roles': '权限验证', 35 | 'index-layout.menu.roles.all': '用户都有权限', 36 | 'index-layout.menu.roles.user': 'Users有权限', 37 | 'index-layout.menu.roles.test': 'Tests有权限', 38 | }; 39 | -------------------------------------------------------------------------------- /src/layouts/IndexLayout/locales/zh-TW.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'index-layout.topmenu.userinfo': '個人信息', 3 | 'index-layout.topmenu.logout': '退出', 4 | 5 | 'index-layout.menu.home': '首頁', 6 | 'index-layout.menu.home.workplace': '工作臺', 7 | 'index-layout.menu.home.custom-breadcrumbs': '自定義面包屑', 8 | 'index-layout.menu.home.custom-breadcrumbs.liqingsong.cc': 'liqingsong.cc', 9 | 'index-layout.menu.home.docs': '使用文檔', 10 | 11 | 'index-layout.menu.component': '組件', 12 | 'index-layout.menu.component.icon': '圖標', 13 | 'index-layout.menu.component.icon.svg': 'IconSvg', 14 | 'index-layout.menu.component.icon.font': 'IconFont', 15 | 'index-layout.menu.component.editor': '編輯器', 16 | 'index-layout.menu.component.editor.tui-editor': 'tui-editor', 17 | 'index-layout.menu.component.editor.ckeditor': 'CKEditor', 18 | 19 | 'index-layout.menu.pages': '頁面示例', 20 | 'index-layout.menu.pages.list': '列表頁面', 21 | 'index-layout.menu.pages.list.basic': '標淮列表', 22 | 'index-layout.menu.pages.list.table': '表格列表', 23 | 'index-layout.menu.pages.list.highly-adaptive-table': '高度自適應表格', 24 | 'index-layout.menu.pages.list.search': '搜索列表', 25 | 'index-layout.menu.pages.list.search.table': '查詢表格', 26 | 'index-layout.menu.pages.form': '表單頁面', 27 | 'index-layout.menu.pages.form.basic': '基礎表單', 28 | 'index-layout.menu.pages.form.complex': '高級表單', 29 | 'index-layout.menu.pages.detail': '詳情頁面', 30 | 'index-layout.menu.pages.detail.basic': '基礎詳情', 31 | 'index-layout.menu.pages.detail.module': '模塊詳情', 32 | 'index-layout.menu.pages.detail.table': '表格詳情', 33 | 34 | 'index-layout.menu.roles': '權限驗證', 35 | 'index-layout.menu.roles.all': '用戶都有權限', 36 | 'index-layout.menu.roles.user': 'Users有權限', 37 | 'index-layout.menu.roles.test': 'Tests有權限', 38 | }; 39 | -------------------------------------------------------------------------------- /src/layouts/UniversalLayout/components/SiderMenuItem.vue: -------------------------------------------------------------------------------- 1 | 19 | 50 | -------------------------------------------------------------------------------- /src/composables/useEcharts.ts: -------------------------------------------------------------------------------- 1 | import { onMounted, onBeforeUnmount, Ref, ShallowRef, shallowRef } from 'vue'; 2 | import debounce from 'lodash.debounce'; 3 | import * as echarts from 'echarts'; 4 | 5 | export type EChartsOption = echarts.EChartsOption; 6 | 7 | /** 8 | * Echarts composables 9 | * @param labRef Ref HTMLDivElement ref 10 | * @param initOption EChartOption echarts option init 11 | * @param cb Function|undefined 回调函数 读取数据 12 | * @param theme string|undefined 使用的主题 13 | * @returns 14 | * @author LiQingSong 15 | */ 16 | export default function useEcharts( 17 | labRef: Ref, 18 | initOption: EChartsOption, 19 | cb?: (ec:echarts.ECharts) => any, 20 | theme = '' 21 | ):{ 22 | echart: ShallowRef; 23 | cb: () => void; 24 | } { 25 | 26 | 27 | const chart = shallowRef(); 28 | 29 | const resizeHandler = debounce(() => { 30 | chart.value?.resize(); 31 | }, 100); 32 | 33 | const callback = () => { 34 | if(typeof cb === 'function' && chart.value) { 35 | cb(chart.value) 36 | } 37 | } 38 | 39 | onMounted(()=> { 40 | if(labRef.value) { 41 | chart.value = echarts.init(labRef.value, theme); 42 | chart.value.setOption(initOption); 43 | callback() 44 | } 45 | 46 | window.addEventListener('resize', resizeHandler); 47 | }) 48 | 49 | onBeforeUnmount(()=> { 50 | chart.value?.dispose(); 51 | window.removeEventListener('resize', resizeHandler); 52 | }); 53 | 54 | return { 55 | echart: chart, 56 | cb: callback 57 | }; 58 | } -------------------------------------------------------------------------------- /src/assets/iconsvg/set.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/views/pagesample/detail/basic/store.ts: -------------------------------------------------------------------------------- 1 | import { Mutation, Action } from 'vuex'; 2 | import { StoreModuleType } from "@/utils/store"; 3 | import { queryDetail } from './service'; 4 | import { DetailDataType } from './data.d'; 5 | import { ResponseData } from '@/utils/request'; 6 | 7 | export interface StateType { 8 | detail: DetailDataType; 9 | } 10 | 11 | export interface ModuleType extends StoreModuleType { 12 | state: StateType; 13 | mutations: { 14 | setDetail: Mutation; 15 | }; 16 | actions: { 17 | queryDetail: Action; 18 | }; 19 | } 20 | 21 | const initState: StateType = { 22 | detail: { 23 | userInfo: { 24 | name: '', 25 | tel: '', 26 | courier: '', 27 | address: '', 28 | remark: '', 29 | }, 30 | refundApplication: { 31 | ladingNo: '', 32 | saleNo: '', 33 | state: '', 34 | childOrders: '', 35 | }, 36 | returnGoods: [], 37 | returnProgress: [], 38 | }, 39 | }; 40 | 41 | const StoreModel: ModuleType = { 42 | namespaced: true, 43 | name: 'DetailBasic', 44 | state: { 45 | ...initState 46 | }, 47 | mutations: { 48 | setDetail(state, payload) { 49 | state.detail = payload; 50 | }, 51 | }, 52 | actions: { 53 | async queryDetail({ commit }) { 54 | try { 55 | const response: ResponseData = await queryDetail(); 56 | const { data } = response; 57 | commit('setDetail',{ 58 | ...initState.detail, 59 | ...data 60 | }); 61 | return true; 62 | } catch (error) { 63 | return false; 64 | } 65 | } 66 | } 67 | }; 68 | 69 | export default StoreModel; 70 | -------------------------------------------------------------------------------- /src/views/pagesample/detail/table/store.ts: -------------------------------------------------------------------------------- 1 | import { Mutation, Action } from 'vuex'; 2 | import { StoreModuleType } from "@/utils/store"; 3 | import { queryDetail } from './service'; 4 | import { DetailDataType } from './data.d'; 5 | import { ResponseData } from '@/utils/request'; 6 | 7 | export interface StateType { 8 | detail: DetailDataType; 9 | } 10 | 11 | export interface ModuleType extends StoreModuleType { 12 | state: StateType; 13 | mutations: { 14 | setDetail: Mutation; 15 | }; 16 | actions: { 17 | queryDetail: Action; 18 | }; 19 | } 20 | 21 | const initState: StateType = { 22 | detail: { 23 | userInfo: { 24 | name: '', 25 | tel: '', 26 | courier: '', 27 | address: '', 28 | remark: '', 29 | }, 30 | refundApplication: { 31 | ladingNo: '', 32 | saleNo: '', 33 | state: '', 34 | childOrders: '', 35 | }, 36 | returnGoods: [], 37 | returnProgress: [], 38 | }, 39 | }; 40 | 41 | const StoreModel: ModuleType = { 42 | namespaced: true, 43 | name: 'DetailTable', 44 | state: { 45 | ...initState 46 | }, 47 | mutations: { 48 | setDetail(state, payload) { 49 | state.detail = payload; 50 | }, 51 | }, 52 | actions: { 53 | async queryDetail({ commit }) { 54 | try { 55 | const response: ResponseData = await queryDetail(); 56 | const { data } = response; 57 | commit('setDetail',{ 58 | ...initState.detail, 59 | ...data 60 | }); 61 | return true; 62 | } catch (error) { 63 | return false; 64 | } 65 | } 66 | } 67 | }; 68 | 69 | export default StoreModel; 70 | -------------------------------------------------------------------------------- /src/views/pagesample/detail/module/store.ts: -------------------------------------------------------------------------------- 1 | import { Mutation, Action } from 'vuex'; 2 | import { StoreModuleType } from "@/utils/store"; 3 | import { queryDetail } from './service'; 4 | import { DetailDataType } from './data.d'; 5 | import { ResponseData } from '@/utils/request'; 6 | 7 | export interface StateType { 8 | detail: DetailDataType; 9 | } 10 | 11 | export interface ModuleType extends StoreModuleType { 12 | state: StateType; 13 | mutations: { 14 | setDetail: Mutation; 15 | }; 16 | actions: { 17 | queryDetail: Action; 18 | }; 19 | } 20 | 21 | const initState: StateType = { 22 | detail: { 23 | userInfo: { 24 | name: '', 25 | tel: '', 26 | courier: '', 27 | address: '', 28 | remark: '', 29 | }, 30 | refundApplication: { 31 | ladingNo: '', 32 | saleNo: '', 33 | state: '', 34 | childOrders: '', 35 | }, 36 | returnGoods: [], 37 | returnProgress: [], 38 | }, 39 | }; 40 | 41 | const StoreModel: ModuleType = { 42 | namespaced: true, 43 | name: 'DetailModule', 44 | state: { 45 | ...initState 46 | }, 47 | mutations: { 48 | setDetail(state, payload) { 49 | state.detail = payload; 50 | }, 51 | }, 52 | actions: { 53 | async queryDetail({ commit }) { 54 | try { 55 | const response: ResponseData = await queryDetail(); 56 | const { data } = response; 57 | commit('setDetail',{ 58 | ...initState.detail, 59 | ...data 60 | }); 61 | return true; 62 | } catch (error) { 63 | return false; 64 | } 65 | } 66 | } 67 | }; 68 | 69 | export default StoreModel; 70 | -------------------------------------------------------------------------------- /src/layouts/UniversalLayout/components/RightTopUser.vue: -------------------------------------------------------------------------------- 1 | 19 | 54 | -------------------------------------------------------------------------------- /src/views/user/register/store.ts: -------------------------------------------------------------------------------- 1 | import { Mutation, Action } from 'vuex'; 2 | import { StoreModuleType } from "@/utils/store"; 3 | import { RegisterParamsType } from "./data.d"; 4 | import { accountReg } from "./service"; 5 | 6 | export interface StateType { 7 | errorMsg?: string; 8 | } 9 | 10 | export interface ModuleType extends StoreModuleType { 11 | state: StateType; 12 | mutations: { 13 | changeErrorMsg: Mutation; 14 | }; 15 | actions: { 16 | register: Action; 17 | }; 18 | } 19 | 20 | const initState: StateType = { 21 | errorMsg: undefined, 22 | }; 23 | 24 | const StoreModel: ModuleType = { 25 | namespaced: true, 26 | name: 'userregister', 27 | state: { 28 | ...initState 29 | }, 30 | mutations: { 31 | changeErrorMsg(state, payload) { 32 | state.errorMsg = payload; 33 | }, 34 | }, 35 | actions: { 36 | async register({ commit }, payload: RegisterParamsType) { 37 | let msg: string | undefined = undefined; 38 | try { 39 | await accountReg(payload); 40 | msg = ''; 41 | } catch (error) { 42 | if (error.message && error.message === 'CustomError') { 43 | const { response } = error; 44 | const { data } = response; 45 | msg = data.msg || 'error'; 46 | } 47 | } 48 | 49 | commit('changeErrorMsg',msg); 50 | 51 | if (msg === '') { 52 | return true; // 成功 53 | } else if (typeof msg === 'undefined') { 54 | return undefined; // 服务器错误 55 | } else { 56 | return false; // 自定义错误 57 | } 58 | } 59 | } 60 | }; 61 | 62 | export default StoreModel; 63 | -------------------------------------------------------------------------------- /src/layouts/UniversalLayout/locales/zh-CN.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'universal-layout.topmenu.userinfo': '个人信息', 3 | 'universal-layout.topmenu.logout': '退出', 4 | 5 | 'universal-layout.menu.home': '首页', 6 | 'universal-layout.menu.home.workplace': '工作台', 7 | 'universal-layout.menu.home.custom-breadcrumbs': '自定义面包屑', 8 | 'universal-layout.menu.home.custom-breadcrumbs.liqingsong.cc': 'liqingsong.cc', 9 | 'universal-layout.menu.home.docs': '使用文档', 10 | 11 | 'universal-layout.menu.component': '组件', 12 | 'universal-layout.menu.component.icon': '图标', 13 | 'universal-layout.menu.component.icon.svg': 'IconSvg', 14 | 'universal-layout.menu.component.icon.font': 'IconFont', 15 | 'universal-layout.menu.component.editor': '编辑器', 16 | 'universal-layout.menu.component.editor.tui-editor': 'tui-editor', 17 | 'universal-layout.menu.component.editor.ckeditor': 'CKEditor', 18 | 19 | 'universal-layout.menu.pages': '页面示例', 20 | 'universal-layout.menu.pages.list': '列表页面', 21 | 'universal-layout.menu.pages.list.basic': '标准列表', 22 | 'universal-layout.menu.pages.list.table': '表格列表', 23 | 'universal-layout.menu.pages.list.highly-adaptive-table': '高度自适应表格', 24 | 'universal-layout.menu.pages.list.search': '搜索列表', 25 | 'universal-layout.menu.pages.list.search.table': '查询表格', 26 | 'universal-layout.menu.pages.form': '表单页面', 27 | 'universal-layout.menu.pages.form.basic': '基础表单', 28 | 'universal-layout.menu.pages.form.complex': '高级表单', 29 | 'universal-layout.menu.pages.detail': '详情页面', 30 | 'universal-layout.menu.pages.detail.basic': '基础详情', 31 | 'universal-layout.menu.pages.detail.module': '模块详情', 32 | 'universal-layout.menu.pages.detail.table': '表格详情', 33 | 34 | 'universal-layout.menu.roles': '权限验证', 35 | 'universal-layout.menu.roles.all': '用户都有权限', 36 | 'universal-layout.menu.roles.user': 'Users有权限', 37 | 'universal-layout.menu.roles.test': 'Tests有权限', 38 | }; 39 | -------------------------------------------------------------------------------- /src/layouts/UniversalLayout/locales/zh-TW.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'universal-layout.topmenu.userinfo': '個人信息', 3 | 'universal-layout.topmenu.logout': '退出', 4 | 5 | 'universal-layout.menu.home': '首頁', 6 | 'universal-layout.menu.home.workplace': '工作臺', 7 | 'universal-layout.menu.home.custom-breadcrumbs': '自定義面包屑', 8 | 'universal-layout.menu.home.custom-breadcrumbs.liqingsong.cc': 'liqingsong.cc', 9 | 'universal-layout.menu.home.docs': '使用文檔', 10 | 11 | 'universal-layout.menu.component': '組件', 12 | 'universal-layout.menu.component.icon': '圖標', 13 | 'universal-layout.menu.component.icon.svg': 'IconSvg', 14 | 'universal-layout.menu.component.icon.font': 'IconFont', 15 | 'universal-layout.menu.component.editor': '編輯器', 16 | 'universal-layout.menu.component.editor.tui-editor': 'tui-editor', 17 | 'universal-layout.menu.component.editor.ckeditor': 'CKEditor', 18 | 19 | 'universal-layout.menu.pages': '頁面示例', 20 | 'universal-layout.menu.pages.list': '列表頁面', 21 | 'universal-layout.menu.pages.list.basic': '標淮列表', 22 | 'universal-layout.menu.pages.list.table': '表格列表', 23 | 'universal-layout.menu.pages.list.highly-adaptive-table': '高度自適應表格', 24 | 'universal-layout.menu.pages.list.search': '搜索列表', 25 | 'universal-layout.menu.pages.list.search.table': '查詢表格', 26 | 'universal-layout.menu.pages.form': '表單頁面', 27 | 'universal-layout.menu.pages.form.basic': '基礎表單', 28 | 'universal-layout.menu.pages.form.complex': '高級表單', 29 | 'universal-layout.menu.pages.detail': '詳情頁面', 30 | 'universal-layout.menu.pages.detail.basic': '基礎詳情', 31 | 'universal-layout.menu.pages.detail.module': '模塊詳情', 32 | 'universal-layout.menu.pages.detail.table': '表格詳情', 33 | 34 | 'universal-layout.menu.roles': '權限驗證', 35 | 'universal-layout.menu.roles.all': '用戶都有權限', 36 | 'universal-layout.menu.roles.user': 'Users有權限', 37 | 'universal-layout.menu.roles.test': 'Tests有權限', 38 | }; 39 | -------------------------------------------------------------------------------- /src/views/user/login/store.ts: -------------------------------------------------------------------------------- 1 | import { Mutation, Action } from 'vuex'; 2 | import { StoreModuleType } from "@/utils/store"; 3 | import { ResponseData } from '@/utils/request'; 4 | import { setToken } from '@/utils/localToken'; 5 | import { accountLogin } from './service'; 6 | import { LoginParamsType } from "./data.d"; 7 | 8 | export interface StateType { 9 | loginStatus?: 'ok' | 'error'; 10 | } 11 | 12 | export interface ModuleType extends StoreModuleType { 13 | state: StateType; 14 | mutations: { 15 | changeLoginStatus: Mutation; 16 | }; 17 | actions: { 18 | login: Action; 19 | }; 20 | } 21 | 22 | const initState: StateType = { 23 | loginStatus: undefined, 24 | } 25 | 26 | const StoreModel: ModuleType = { 27 | namespaced: true, 28 | name: 'userlogin', 29 | state: { 30 | ...initState 31 | }, 32 | mutations: { 33 | changeLoginStatus(state, payload) { 34 | state.loginStatus = payload; 35 | }, 36 | }, 37 | actions: { 38 | async login({ commit }, payload: LoginParamsType) { 39 | let status: string | undefined = undefined; 40 | try { 41 | const response: ResponseData = await accountLogin(payload); 42 | const { data } = response; 43 | setToken(data.token || ''); 44 | status = 'ok'; 45 | } catch (error) { 46 | if (error.message && error.message === 'CustomError') { 47 | status = 'error'; 48 | } 49 | } 50 | 51 | commit('changeLoginStatus',status); 52 | 53 | if (status === 'ok') { 54 | return true; 55 | } else if (status === 'error') { 56 | return false; 57 | } 58 | return undefined; 59 | } 60 | } 61 | } 62 | 63 | export default StoreModel; -------------------------------------------------------------------------------- /src/layouts/IndexLayout/locales/en-US.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'index-layout.topmenu.userinfo': 'Personal Info', 3 | 'index-layout.topmenu.logout': 'Logout', 4 | 5 | 'index-layout.menu.home': 'Home', 6 | 'index-layout.menu.home.workplace': 'Workplace', 7 | 'index-layout.menu.home.custom-breadcrumbs': 'Custom bread crumbs', 8 | 'index-layout.menu.home.custom-breadcrumbs.liqingsong.cc': 'liqingsong.cc', 9 | 'index-layout.menu.home.docs': 'Using document', 10 | 11 | 'index-layout.menu.component': 'Component', 12 | 'index-layout.menu.component.icon': 'Icon', 13 | 'index-layout.menu.component.icon.svg': 'IconSvg', 14 | 'index-layout.menu.component.icon.font': 'IconFont', 15 | 'index-layout.menu.component.editor': 'Editor', 16 | 'index-layout.menu.component.editor.tui-editor': 'tui-editor', 17 | 'index-layout.menu.component.editor.ckeditor': 'CKEditor', 18 | 19 | 'index-layout.menu.pages': 'Page sample', 20 | 'index-layout.menu.pages.list': 'List page', 21 | 'index-layout.menu.pages.list.basic': 'Basis list', 22 | 'index-layout.menu.pages.list.table': 'Table list', 23 | 'index-layout.menu.pages.list.highly-adaptive-table': 'Highly adaptive table', 24 | 'index-layout.menu.pages.list.search': 'Search list', 25 | 'index-layout.menu.pages.list.search.table': 'Query list', 26 | 'index-layout.menu.pages.form': 'Form page', 27 | 'index-layout.menu.pages.form.basic': 'Basis Form', 28 | 'index-layout.menu.pages.form.complex': 'Advanced Form', 29 | 'index-layout.menu.pages.detail': 'Details page', 30 | 'index-layout.menu.pages.detail.basic': 'Basic details', 31 | 'index-layout.menu.pages.detail.module': 'Module details', 32 | 'index-layout.menu.pages.detail.table': 'Table details', 33 | 34 | 'index-layout.menu.roles': 'Permission to verify', 35 | 'index-layout.menu.roles.all': 'All users have permissions', 36 | 'index-layout.menu.roles.user': 'Users have permission', 37 | 'index-layout.menu.roles.test': 'Tests have permission', 38 | }; 39 | -------------------------------------------------------------------------------- /src/utils/store.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Store utils 3 | * @author LiQingSong 4 | */ 5 | import { Module, ModuleTree } from 'vuex'; 6 | 7 | /** 8 | * 自定义项目 Store Module 类型 9 | * 为自动导入的 store 做类型限制 10 | * [@/store文件夹下定义的 store]与[@/views文件夹下定义的`文件store.ts`] 必须继承此类型 11 | * @author LiQingSong 12 | */ 13 | export interface StoreModuleType extends Module { 14 | namespaced: true; 15 | name: string; 16 | } 17 | 18 | /** 19 | * 自动导入 Store 20 | * @author LiQingSong 21 | */ 22 | export function importAllStore (): ModuleTree { 23 | const modules: ModuleTree = {}; 24 | try { 25 | // 导入 @/views 下文件,包含子目录,文件名为:store.ts 26 | const viewsRequireContext: __WebpackModuleApi.RequireContext = require.context('../views', true, /[/\\]store\.ts$/); 27 | viewsRequireContext.keys().forEach(fileName => { 28 | // 获取内容 29 | const modulesConent = viewsRequireContext(fileName); 30 | if(modulesConent.default) { 31 | const { name, ...module } = modulesConent.default; 32 | // 获取 PascalCase 命名 33 | const modulesName = name || fileName.replace(/^\.\/(.*)\.\w+$/, "$1"); 34 | 35 | modules[modulesName] = { ...module }; 36 | } 37 | 38 | }); 39 | 40 | // 导入 @/store 下文件 41 | const requireContext: __WebpackModuleApi.RequireContext = require.context('../store', false, /\.ts$/); 42 | requireContext.keys().forEach(fileName => { 43 | // 获取内容 44 | const modulesConent = requireContext(fileName); 45 | if(modulesConent.default) { 46 | const { name, ...module } = modulesConent.default; 47 | // 获取 PascalCase 命名 48 | const modulesName = name || fileName.replace(/^\.\/(.*)\.\w+$/, "$1"); 49 | 50 | modules[modulesName] = { ...module }; 51 | } 52 | 53 | }); 54 | } catch (error) { 55 | // eslint-disable-next-line no-console 56 | console.log(error); 57 | } 58 | 59 | return modules; 60 | } 61 | -------------------------------------------------------------------------------- /src/layouts/UniversalLayout/locales/en-US.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'universal-layout.topmenu.userinfo': 'Personal Info', 3 | 'universal-layout.topmenu.logout': 'Logout', 4 | 5 | 'universal-layout.menu.home': 'Home', 6 | 'universal-layout.menu.home.workplace': 'Workplace', 7 | 'universal-layout.menu.home.custom-breadcrumbs': 'Custom bread crumbs', 8 | 'universal-layout.menu.home.custom-breadcrumbs.liqingsong.cc': 'liqingsong.cc', 9 | 'universal-layout.menu.home.docs': 'Using document', 10 | 11 | 'universal-layout.menu.component': 'Component', 12 | 'universal-layout.menu.component.icon': 'Icon', 13 | 'universal-layout.menu.component.icon.svg': 'IconSvg', 14 | 'universal-layout.menu.component.icon.font': 'IconFont', 15 | 'universal-layout.menu.component.editor': 'Editor', 16 | 'universal-layout.menu.component.editor.tui-editor': 'tui-editor', 17 | 'universal-layout.menu.component.editor.ckeditor': 'CKEditor', 18 | 19 | 'universal-layout.menu.pages': 'Page sample', 20 | 'universal-layout.menu.pages.list': 'List page', 21 | 'universal-layout.menu.pages.list.basic': 'Basis list', 22 | 'universal-layout.menu.pages.list.table': 'Table list', 23 | 'universal-layout.menu.pages.list.highly-adaptive-table': 'Highly adaptive table', 24 | 'universal-layout.menu.pages.list.search': 'Search list', 25 | 'universal-layout.menu.pages.list.search.table': 'Query list', 26 | 'universal-layout.menu.pages.form': 'Form page', 27 | 'universal-layout.menu.pages.form.basic': 'Basis Form', 28 | 'universal-layout.menu.pages.form.complex': 'Advanced Form', 29 | 'universal-layout.menu.pages.detail': 'Details page', 30 | 'universal-layout.menu.pages.detail.basic': 'Basic details', 31 | 'universal-layout.menu.pages.detail.module': 'Module details', 32 | 'universal-layout.menu.pages.detail.table': 'Table details', 33 | 34 | 'universal-layout.menu.roles': 'Permission to verify', 35 | 'universal-layout.menu.roles.all': 'All users have permissions', 36 | 'universal-layout.menu.roles.user': 'Users have permission', 37 | 'universal-layout.menu.roles.test': 'Tests have permission', 38 | }; 39 | -------------------------------------------------------------------------------- /src/layouts/IndexLayout/components/Left.vue: -------------------------------------------------------------------------------- 1 | 23 | 69 | -------------------------------------------------------------------------------- /src/layouts/SecurityLayout.vue: -------------------------------------------------------------------------------- 1 | 9 | -------------------------------------------------------------------------------- /src/layouts/UniversalLayout/index.vue: -------------------------------------------------------------------------------- 1 | 23 | 61 | 64 | -------------------------------------------------------------------------------- /src/components/IconFont/index.vue: -------------------------------------------------------------------------------- 1 | 6 | 61 | -------------------------------------------------------------------------------- /src/layouts/UniversalLayout/components/SiderMenu.vue: -------------------------------------------------------------------------------- 1 | 53 | 72 | -------------------------------------------------------------------------------- /src/config/settings.ts: -------------------------------------------------------------------------------- 1 | import { RoutesDataItem } from "@/utils/routes"; 2 | 3 | export type Theme = 'dark' | 'light'; 4 | 5 | export type NavMode = 'inline' | 'horizontal'; 6 | 7 | /** 8 | * 站点配置 9 | * @author LiQingSong 10 | */ 11 | export interface SettingsType { 12 | /** 13 | * 站点名称 14 | */ 15 | siteTitle: string; 16 | 17 | /** 18 | * 站点首页路由 19 | */ 20 | homeRouteItem: RoutesDataItem; 21 | 22 | /** 23 | * 站点本地存储Token 的 Key值 24 | */ 25 | siteTokenKey: string; 26 | 27 | /** 28 | * Ajax请求头发送Token 的 Key值 29 | */ 30 | ajaxHeadersTokenKey: string; 31 | 32 | /** 33 | * Ajax返回值不参加统一验证的api地址 34 | */ 35 | ajaxResponseNoVerifyUrl: string[]; 36 | 37 | /** 38 | * iconfont.cn 项目在线生成的 js 地址 39 | */ 40 | iconfontUrl: string[]; 41 | 42 | /** 43 | * Layout 头部固定开启 44 | */ 45 | headFixed: boolean; 46 | 47 | /** 48 | * Layout tab菜单开启 49 | */ 50 | tabNavEnable: boolean; 51 | 52 | /** 53 | * IndexLayout 顶部菜单开启 54 | */ 55 | topNavEnable: boolean; 56 | 57 | /** 58 | * UniversalLayout 模板主题 59 | */ 60 | theme: Theme; 61 | 62 | /** 63 | * UniversalLayout 导航模式 64 | */ 65 | navMode: NavMode; 66 | 67 | /** 68 | * UniversalLayout 左侧侧边固定开启 69 | */ 70 | leftSiderFixed: boolean; 71 | } 72 | 73 | const settings: SettingsType = { 74 | siteTitle: 'ADMIN-ANTD-VUE', 75 | homeRouteItem: { 76 | icon: 'control', 77 | title: 'index-layout.menu.home.workplace', 78 | path: '/home/workplace', 79 | component: ()=> import('@/views/home/index.vue') 80 | }, 81 | siteTokenKey: 'admin_antd_vue_token', 82 | ajaxHeadersTokenKey: 'x-token', 83 | ajaxResponseNoVerifyUrl: [ 84 | '/user/login', // 用户登录 85 | '/user/info', // 获取用户信息 86 | ], 87 | iconfontUrl: [], 88 | 89 | /* 以下是针对所有 Layout 扩展字段 */ 90 | headFixed: true, 91 | tabNavEnable: true, 92 | 93 | /* 以下是针对 IndexLayout 扩展字段 */ 94 | topNavEnable: true, 95 | 96 | /* 以下是针对 UniversalLayout 扩展字段 */ 97 | theme: 'dark', 98 | navMode: 'inline', 99 | leftSiderFixed: true, 100 | }; 101 | 102 | export default settings; 103 | -------------------------------------------------------------------------------- /src/config/routes.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 路由入口 3 | * @author LiQingSong 4 | */ 5 | import NProgress from 'nprogress'; // progress bar 6 | import 'nprogress/nprogress.css'; // progress bar style 7 | NProgress.configure({ showSpinner: false, easing: 'ease', speed: 1000 }); // NProgress Configuration 8 | 9 | import { createRouter, createWebHashHistory } from 'vue-router'; 10 | import { RoutesDataItem } from "@/utils/routes"; 11 | import settings from "@/config/settings"; 12 | 13 | import SecurityLayout from '@/layouts/SecurityLayout.vue'; 14 | 15 | /* UniversalLayout 通用布局,可以与 IndexLayout 互相代替 */ 16 | // import UniversalLayoutRoutes from '@/layouts/UniversalLayout/routes'; 17 | // import UniversalLayout from '@/layouts/UniversalLayout/index.vue'; 18 | 19 | /* IndexLayout 自定义布局,可以与 UniversalLayout 互相代替 */ 20 | import IndexLayoutRoutes from '@/layouts/IndexLayout/routes'; 21 | import IndexLayout from '@/layouts/IndexLayout/index.vue'; 22 | 23 | import UserLayoutRoutes from '@/layouts/UserLayout/routes'; 24 | import UserLayout from '@/layouts/UserLayout/index.vue'; 25 | 26 | const routes: RoutesDataItem[] = [ 27 | { 28 | title: 'empty', 29 | path: '/', 30 | component: SecurityLayout, 31 | children: [ 32 | { 33 | title: 'empty', 34 | path: '/', 35 | redirect: settings.homeRouteItem.path, 36 | component: IndexLayout, 37 | children: IndexLayoutRoutes 38 | }, 39 | { 40 | title: 'empty', 41 | path: '/refresh', 42 | component: () => import('@/views/refresh/index.vue'), 43 | }, 44 | ] 45 | }, 46 | { 47 | title: 'empty', 48 | path: '/user', 49 | redirect: '/user/login', 50 | component: UserLayout, 51 | children: UserLayoutRoutes 52 | }, 53 | { 54 | title: 'app.global.menu.notfound', 55 | path: '/:pathMatch(.*)*', 56 | component: () => import('@/views/404/index.vue'), 57 | } 58 | ] 59 | 60 | const router = createRouter({ 61 | scrollBehavior(/* to, from, savedPosition */) { 62 | return { top: 0 } 63 | }, 64 | history: createWebHashHistory(process.env.BASE_URL), 65 | routes: routes, 66 | }); 67 | 68 | router.beforeEach((/* to, from */) => { 69 | // start progress bar 70 | NProgress.start(); 71 | }); 72 | 73 | router.afterEach(() => { 74 | // finish progress bar 75 | NProgress.done(); 76 | }); 77 | 78 | export default router; 79 | -------------------------------------------------------------------------------- /src/components/SelectLang/index.vue: -------------------------------------------------------------------------------- 1 | 18 | 65 | -------------------------------------------------------------------------------- /mock/user.js: -------------------------------------------------------------------------------- 1 | const mockjs= require('mockjs'); 2 | const { VUE_APP_APIHOST } = process.env; 3 | const ajaxHeadersTokenKey = 'x-token'; 4 | const mock = {}; 5 | 6 | mock[`GET ${VUE_APP_APIHOST}/user/info`] = (req, res) => { 7 | const headers = req.headers; 8 | if (headers[ajaxHeadersTokenKey] === 'admin') { 9 | res.send({ 10 | code: 0, 11 | data: { 12 | id: 1, 13 | name: 'Admins', 14 | avatar: '', 15 | roles: ['admin'], 16 | }, 17 | }); 18 | } else if (headers[ajaxHeadersTokenKey] === 'user') { 19 | res.send({ 20 | code: 0, 21 | data: { 22 | id: 2, 23 | name: 'Users', 24 | avatar: '', 25 | roles: ['user'], 26 | }, 27 | }); 28 | } else if (headers[ajaxHeadersTokenKey] === 'test') { 29 | res.send({ 30 | code: 0, 31 | data: { 32 | id: 3, 33 | name: 'Tests', 34 | avatar: '', 35 | roles: ['test'], 36 | }, 37 | }); 38 | } else { 39 | res.send({ 40 | code: 10002, 41 | data: {}, 42 | msg: '未登录', 43 | }); 44 | } 45 | 46 | }; 47 | 48 | mock[`GET ${VUE_APP_APIHOST || ''}/user/message`] = (req, res) => { 49 | res.send({ 50 | code: 0, 51 | data: mockjs.mock('@integer(0,99)'), 52 | }); 53 | }; 54 | 55 | mock[`POST ${VUE_APP_APIHOST || ''}/user/login`] = (req, res) => { 56 | const { password, username } = req.body; 57 | const send = { code: 0, data: {}, msg: '' }; 58 | if (username === 'admin' && password === '123456') { 59 | send['data'] = { 60 | token: 'admin', 61 | }; 62 | } else if (username === 'user' && password === '123456') { 63 | send['data'] = { 64 | token: 'user', 65 | }; 66 | } else if (username === 'test' && password === '123456') { 67 | send['data'] = { 68 | token: 'test', 69 | }; 70 | } else { 71 | send['code'] = 201; 72 | send['msg'] = 'Wrong username or password'; 73 | } 74 | 75 | res.send(send); 76 | }; 77 | 78 | mock[`POST ${VUE_APP_APIHOST || ''}/user/register`] = (req, res) => { 79 | res.send({ 80 | code: 0, 81 | data: '', 82 | msg: '', 83 | }); 84 | }; 85 | 86 | 87 | module.exports = { 88 | ...mock 89 | }; -------------------------------------------------------------------------------- /src/store/user.ts: -------------------------------------------------------------------------------- 1 | import { Mutation, Action } from 'vuex'; 2 | import { StoreModuleType } from "@/utils/store"; 3 | import { ResponseData } from '@/utils/request'; 4 | import { queryCurrent, queryMessage } from "@/services/user"; 5 | import { removeToken } from "@/utils/localToken"; 6 | 7 | export interface CurrentUser { 8 | id: number; 9 | name: string; 10 | avatar: string; 11 | roles: string[]; 12 | } 13 | 14 | export interface StateType { 15 | currentUser: CurrentUser; 16 | message: number; 17 | } 18 | 19 | export interface ModuleType extends StoreModuleType { 20 | state: StateType; 21 | mutations: { 22 | saveCurrentUser: Mutation; 23 | saveMessage: Mutation; 24 | }; 25 | actions: { 26 | fetchCurrent: Action; 27 | fetchMessage: Action; 28 | logout: Action; 29 | }; 30 | } 31 | 32 | const initState: StateType = { 33 | currentUser: { 34 | id: 0, 35 | name: '', 36 | avatar: '', 37 | roles: [], 38 | }, 39 | message: 0, 40 | } 41 | 42 | const StoreModel: ModuleType = { 43 | namespaced: true, 44 | name: 'user', 45 | state: { 46 | ...initState 47 | }, 48 | mutations: { 49 | saveCurrentUser(state, payload) { 50 | state.currentUser = { 51 | ...initState.currentUser, 52 | ...payload, 53 | } 54 | }, 55 | saveMessage(state, payload) { 56 | state.message = payload; 57 | } 58 | }, 59 | actions: { 60 | async fetchCurrent({ commit }) { 61 | try { 62 | const response: ResponseData = await queryCurrent(); 63 | const { data } = response; 64 | commit('saveCurrentUser', data || {}); 65 | return true; 66 | } catch (error) { 67 | return false; 68 | } 69 | }, 70 | async fetchMessage({ commit }) { 71 | try { 72 | const response: ResponseData = await queryMessage(); 73 | const { data } = response; 74 | commit('saveMessage', data || 0); 75 | return true; 76 | } catch (error) { 77 | return false; 78 | } 79 | }, 80 | async logout({ commit }) { 81 | try { 82 | await removeToken(); 83 | commit('saveCurrentUser', { ...initState.currentUser }); 84 | return true; 85 | } catch (error) { 86 | return false; 87 | } 88 | } 89 | } 90 | } 91 | 92 | 93 | 94 | export default StoreModel; 95 | -------------------------------------------------------------------------------- /src/layouts/IndexLayout/components/RightTopUser.vue: -------------------------------------------------------------------------------- 1 | 18 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "admin-antd-vue", 3 | "description": "Vue3.x Antd Admin", 4 | "version": "1.0.0", 5 | "private": true, 6 | "scripts": { 7 | "serve": "vue-cli-service serve", 8 | "build": "vue-cli-service build", 9 | "test:unit": "vue-cli-service test:unit", 10 | "lint": "vue-cli-service lint", 11 | "svgo": "svgo -f src/assets/iconsvg --config=src/assets/iconsvg/svgo.yml" 12 | }, 13 | "dependencies": { 14 | "@ckeditor/ckeditor5-build-decoupled-document": "^23.1.0", 15 | "@ckeditor/ckeditor5-vue": "^4.0.0", 16 | "@toast-ui/editor": "^2.5.4", 17 | "ant-design-vue": "^2.2.8", 18 | "axios": "^0.21.4", 19 | "core-js": "^3.22.4", 20 | "echarts": "^5.3.2", 21 | "localforage": "^1.10.0", 22 | "lodash.debounce": "^4.0.8", 23 | "nprogress": "^0.2.0", 24 | "vue": "^3.2.39", 25 | "vue-i18n": "~9.1.10", 26 | "vue-router": "^4.1.5", 27 | "vuex": "^4.0.2" 28 | }, 29 | "devDependencies": { 30 | "@types/jest": "^24.9.1", 31 | "@types/lodash.debounce": "^4.0.7", 32 | "@typescript-eslint/eslint-plugin": "^4.33.0", 33 | "@typescript-eslint/parser": "^4.33.0", 34 | "@vue/cli-plugin-babel": "~4.5.19", 35 | "@vue/cli-plugin-eslint": "~4.5.19", 36 | "@vue/cli-plugin-router": "~4.5.19", 37 | "@vue/cli-plugin-typescript": "~4.5.19", 38 | "@vue/cli-plugin-unit-jest": "~4.5.19", 39 | "@vue/cli-plugin-vuex": "~4.5.19", 40 | "@vue/cli-service": "~4.5.19", 41 | "@vue/compiler-sfc": "^3.2.37", 42 | "@vue/eslint-config-typescript": "^7.0.0", 43 | "@vue/test-utils": "^2.0.0-rc.21", 44 | "body-parser": "^1.20.0", 45 | "chokidar": "^3.5.3", 46 | "eslint": "^6.8.0", 47 | "eslint-plugin-vue": "^7.20.0", 48 | "less": "^3.13.1", 49 | "less-loader": "^5.0.0", 50 | "lint-staged": "^9.5.0", 51 | "mockjs": "^1.1.0", 52 | "node-dir": "^0.1.17", 53 | "svg-sprite-loader": "^5.2.1", 54 | "svgo": "^1.3.2", 55 | "svgo-loader": "^2.2.2", 56 | "typescript": "~4.1.6", 57 | "vue-jest": "^5.0.0-alpha.10" 58 | }, 59 | "gitHooks": { 60 | "pre-commit": "lint-staged" 61 | }, 62 | "keywords": [ 63 | "vue", 64 | "vue3", 65 | "vue3.0", 66 | "vue3.x", 67 | "typescript", 68 | "admin", 69 | "template", 70 | "antd", 71 | "antdv", 72 | "Ant Design", 73 | "Ant Design Vue" 74 | ], 75 | "lint-staged": { 76 | "*.{js,jsx,vue,ts,tsx}": [ 77 | "vue-cli-service lint", 78 | "git add" 79 | ] 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /vue.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | const bodyParser = require('body-parser') 3 | const mockServer = require('./src/utils/mock/server'); 4 | const { NODE_ENV, VUE_APP_PORT, VUE_APP_MOCK } = process.env; 5 | module.exports = { 6 | publicPath: '/', 7 | outputDir: 'dist', 8 | productionSourceMap: false, 9 | devServer: { 10 | port: VUE_APP_PORT || 8000, 11 | // 配置反向代理 12 | /* 13 | proxy: { 14 | '/api': { 15 | target: '', 16 | ws: true, 17 | changeOrigin: true 18 | }, 19 | '/foo': { 20 | target: '' 21 | } 22 | }, 23 | */ 24 | before: function(app, server) { 25 | if(NODE_ENV === 'development' && VUE_APP_MOCK === 'true') { 26 | // parse app.body 27 | // https://expressjs.com/en/4x/api.html#req.body 28 | // create application/json parser 29 | app.use(bodyParser.json()); 30 | // create application/x-www-form-urlencoded parser 31 | app.use(bodyParser.urlencoded({ extended: false})); 32 | mockServer(app); 33 | } 34 | } 35 | }, 36 | css: { 37 | loaderOptions: { 38 | less: { 39 | javascriptEnabled: true, 40 | } 41 | } 42 | }, 43 | // 修改webpack的配置 44 | configureWebpack: { 45 | // 不需要打包的插件 46 | externals: { 47 | // 'vue': 'Vue', 48 | // 'vue-router': 'VueRouter', 49 | } 50 | }, 51 | chainWebpack(config) { 52 | 53 | // 内置的 svg Rule 添加 exclude 54 | config.module 55 | .rule('svg') 56 | .exclude.add(/iconsvg/) 57 | .end(); 58 | 59 | // 添加 svg-sprite-loader Rule 60 | config.module 61 | .rule('svg-sprite-loader') 62 | .test(/.svg$/) 63 | .include.add(/iconsvg/) 64 | .end() 65 | .use('svg-sprite-loader') 66 | .loader('svg-sprite-loader'); 67 | 68 | // 添加 svgo Rule 69 | config.module 70 | .rule('svgo') 71 | .test(/.svg$/) 72 | .include.add(/iconsvg/) 73 | .end() 74 | .use('svgo-loader') 75 | .loader('svgo-loader') 76 | .options({ 77 | // externalConfig 配置特殊不是相对路径,起始路径是根目录 78 | externalConfig: './src/assets/iconsvg/svgo.yml', 79 | }); 80 | 81 | } 82 | } -------------------------------------------------------------------------------- /src/assets/iconsvg/components.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/views/home/index.vue: -------------------------------------------------------------------------------- 1 | 38 | 39 | 90 | -------------------------------------------------------------------------------- /src/views/component/icon/svg/index.vue: -------------------------------------------------------------------------------- 1 | 46 | 75 | -------------------------------------------------------------------------------- /src/layouts/IndexLayout/components/SiderMenu.vue: -------------------------------------------------------------------------------- 1 | 23 | -------------------------------------------------------------------------------- /src/layouts/IndexLayout/components/SiderMenuItem.vue: -------------------------------------------------------------------------------- 1 | 35 | -------------------------------------------------------------------------------- /mock/pagesample.js: -------------------------------------------------------------------------------- 1 | const mockjs= require('mockjs'); 2 | const { VUE_APP_APIHOST } = process.env; 3 | const mock = {}; 4 | 5 | 6 | mock[`GET ${VUE_APP_APIHOST || ''}/pages/detail`] = (req, res) => { 7 | res.send({ 8 | code: 0, 9 | data: mockjs.mock({ 10 | userInfo: { 11 | name: '小李', 12 | tel: '13770779817', 13 | courier: '宇宙快递', 14 | address: '宇宙地球', 15 | remark: '无', 16 | }, 17 | refundApplication: { 18 | ladingNo: '1000000000', 19 | saleNo: '1234123421', 20 | state: '已取货', 21 | childOrders: '3214321432', 22 | }, 23 | 'returnGoods|5': [ 24 | { 25 | id: '@integer(1,99999)', 26 | name: '@ctitle(5,10)', 27 | barcode: '@integer(100000000000000,999999999999999)', 28 | price: '@float(1,15,0,2)', 29 | num: '@integer(1,5)', 30 | amount: function() { 31 | return Number(this.price) * Number(this.num); 32 | }, 33 | }, 34 | ], 35 | 'returnProgress|5': [ 36 | { 37 | key: '@integer(1,99999)', 38 | time: '@datetime', 39 | rate: '@csentence(3, 5)', 40 | statuskey: '@boolean', 41 | status: function() { 42 | return this.statuskey ? 'success' : 'processing'; 43 | }, 44 | operator: '取货员 ID @integer(1000,9999)', 45 | cost: '@integer(1,5) h', 46 | }, 47 | ], 48 | }), 49 | }); 50 | }; 51 | 52 | mock[`POST ${VUE_APP_APIHOST || ''}/pages/form`] = (req, res) => { 53 | res.send({ 54 | code: 0, 55 | data: '', 56 | msg: '', 57 | }); 58 | }; 59 | 60 | mock[`GET ${VUE_APP_APIHOST || ''}/pages/list`] = (req, res) => { 61 | res.send({ 62 | code: 0, 63 | data: mockjs.mock({ 64 | total: 1000, 65 | currentPage: 1, 66 | 'list|10': [ 67 | { 68 | id: '@integer(1)', 69 | 'name|1': ['个人博客', '网页小功能'], 70 | 'desc|1': ['李庆松的个人博客', '原创定制最好的网页插件小功能'], 71 | 'href|1': ['http://liqingsong.cc', 'http://wyxgn.com'], 72 | 'type|1': ['header', 'footer'], 73 | }, 74 | ], 75 | }), 76 | }); 77 | }; 78 | 79 | mock[`POST ${VUE_APP_APIHOST || ''}/pages/list`] = (req, res) => { 80 | res.send({ 81 | code: 0, 82 | data: '', 83 | }); 84 | }; 85 | 86 | mock[`PUT ${VUE_APP_APIHOST || ''}/pages/list/*`] = (req, res) => { 87 | res.send({ 88 | code: 0, 89 | data: '', 90 | }); 91 | }; 92 | 93 | mock[`DELETE ${VUE_APP_APIHOST || ''}/pages/list/*`] = ( req, res) => { 94 | res.send({ 95 | code: 0, 96 | data: '', 97 | }); 98 | }; 99 | 100 | mock[`GET ${VUE_APP_APIHOST || ''}/pages/list/*`] = (req, res) => { 101 | res.send({ 102 | code: 0, 103 | data: mockjs.mock({ 104 | id: '@integer(1)', 105 | 'name|1': ['个人博客', '网页小功能'], 106 | 'desc|1': ['李庆松的个人博客', '原创定制最好的网页插件小功能'], 107 | 'href|1': ['http://liqingsong.cc', 'http://wyxgn.com'], 108 | 'type|1': ['header', 'footer'], 109 | }), 110 | }); 111 | }; 112 | 113 | module.exports = { 114 | ...mock 115 | }; -------------------------------------------------------------------------------- /src/utils/mock/server.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Mock Server 3 | * @author LiQingSong 4 | */ 5 | const requireContext = require('./require-context'); 6 | const chokidar = require('chokidar'); 7 | const path = require('path'); 8 | 9 | // 注册 mock 路由 10 | function registerMockRoutes(mockDir, app) { 11 | let mockLastIndex = 0; 12 | let mockRoutesLength = 0; 13 | try { 14 | // 导入 /mock 下文件 15 | const context = requireContext(mockDir, false, /\.js$/); 16 | context.keys().forEach(fileName => { 17 | // 获取内容 18 | const mocks = context(fileName); 19 | for (const key in mocks) { 20 | const keys = key.replace(/(^\s*)|(\s*$)/g, '').split(' '); 21 | let ajaxType = 'get'; 22 | let ajaxUrl = ''; 23 | if(keys.length<2) { 24 | ajaxUrl = keys[0]; 25 | } else { 26 | ajaxType = keys[0].toLowerCase(); 27 | ajaxUrl = keys[1]; 28 | if(!['get','post','put','patch','delete','head','options'].includes(ajaxType)) { 29 | ajaxType = 'get'; 30 | } 31 | } 32 | if(typeof mocks[key] === 'function') { 33 | app[ajaxType](ajaxUrl, mocks[key]); 34 | mockLastIndex = app._router.stack.length; 35 | mockRoutesLength = mockRoutesLength + 1; 36 | // console.log(app._router.stack.length); 37 | } 38 | } 39 | }); 40 | } catch (error) { 41 | console.log(error); 42 | } 43 | 44 | return { 45 | mockRoutesLength: mockRoutesLength, 46 | mockStartIndex: mockLastIndex - mockRoutesLength 47 | } 48 | 49 | } 50 | 51 | // 删除 mock 路由 52 | function unRegisterMockRoutes(mockDir) { 53 | Object.keys(require.cache).forEach(i => { 54 | if (i.includes(mockDir)) { 55 | delete require.cache[require.resolve(i)] 56 | } 57 | }) 58 | } 59 | 60 | 61 | module.exports = app => { 62 | const mockDir = path.resolve('./mock'); 63 | 64 | const mockRoutes = registerMockRoutes(mockDir,app); 65 | let mockRoutesLength = mockRoutes.mockRoutesLength; 66 | let mockStartIndex = mockRoutes.mockStartIndex; 67 | 68 | // watch files, hot reload mock server 69 | chokidar.watch(mockDir, { 70 | ignoreInitial: true 71 | }).on('all', (event, path) => { 72 | if (event === 'change' || event === 'add' || event === 'unlink') { 73 | // console.log(event, path); 74 | try { 75 | // remove mock routes stack 76 | app._router.stack.splice(mockStartIndex, mockRoutesLength); 77 | 78 | // clear routes cache 79 | unRegisterMockRoutes(mockDir); 80 | 81 | // rest routes 82 | const mockRoutes = registerMockRoutes(mockDir,app); 83 | mockRoutesLength = mockRoutes.mockRoutesLength; 84 | mockStartIndex = mockRoutes.mockStartIndex; 85 | 86 | 87 | console.log(`\n > Mock Server hot reload success! changed ${path}`) 88 | } catch (error) { 89 | console.log(error); 90 | } 91 | 92 | } 93 | }) 94 | 95 | 96 | 97 | 98 | } -------------------------------------------------------------------------------- /src/views/home/components/HotTagsCard/index.vue: -------------------------------------------------------------------------------- 1 | 19 | 95 | -------------------------------------------------------------------------------- /src/views/home/components/WorksHitCard/index.vue: -------------------------------------------------------------------------------- 1 | 19 | 95 | -------------------------------------------------------------------------------- /src/store/global.ts: -------------------------------------------------------------------------------- 1 | 2 | import { Mutation/* , Action */ } from 'vuex'; 3 | import { StoreModuleType } from "@/utils/store"; 4 | import { TabNavItem, equalTabNavRoute } from '@/utils/routes'; 5 | import settings, { Theme, NavMode } from '@/config/settings'; 6 | import router from '@/config/routes'; 7 | import { RouteLocationRaw } from 'vue-router'; 8 | 9 | export interface StateType { 10 | /* 以下是针对所有 Layout 扩展字段 */ 11 | // 左侧展开收起 12 | collapsed: boolean; 13 | // 头部固定开启 14 | headFixed: boolean; 15 | // tab菜单开启 16 | tabNavEnable: boolean; 17 | // 头部tab导航列表 18 | headTabNavList: TabNavItem[]; 19 | 20 | /* 以下是针对 IndexLayout 扩展字段 */ 21 | // 顶部菜单开启 22 | topNavEnable: boolean; 23 | 24 | /* 以下是针对 UniversalLayout 扩展字段 */ 25 | // 模板主题 26 | theme: Theme; 27 | // 头部固定开启 28 | navMode: NavMode; 29 | // 左侧侧边固定开启 30 | leftSiderFixed: boolean; 31 | } 32 | 33 | export interface ModuleType extends StoreModuleType { 34 | state: StateType; 35 | mutations: { 36 | changeLayoutCollapsed: Mutation; 37 | setHeadFixed: Mutation; 38 | setTabNavEnable: Mutation; 39 | setHeadTabNavList: Mutation; 40 | closeCurrentHeadTabNav: Mutation; 41 | setTopNavEnable: Mutation; 42 | setTheme: Mutation; 43 | setNavMode: Mutation; 44 | setLeftSiderFixed: Mutation; 45 | }; 46 | actions: { 47 | }; 48 | } 49 | 50 | const homeRoute = router.resolve(settings.homeRouteItem.path); 51 | 52 | const initState: StateType = { 53 | collapsed: false, 54 | headFixed: settings.headFixed, 55 | tabNavEnable: settings.tabNavEnable, 56 | headTabNavList: [ 57 | { 58 | route: homeRoute, 59 | menu: settings.homeRouteItem 60 | } 61 | ], 62 | topNavEnable: settings.topNavEnable, 63 | theme: settings.theme, 64 | navMode: settings.navMode, 65 | leftSiderFixed: settings.leftSiderFixed, 66 | }; 67 | 68 | const StoreModel: ModuleType = { 69 | namespaced: true, 70 | name: 'global', 71 | state: { 72 | ...initState 73 | }, 74 | mutations: { 75 | changeLayoutCollapsed(state, payload) { 76 | state.collapsed = payload; 77 | }, 78 | setHeadFixed(state, payload) { 79 | state.headFixed = payload; 80 | }, 81 | setTabNavEnable(state, payload) { 82 | state.tabNavEnable = payload; 83 | }, 84 | setHeadTabNavList(state, payload) { 85 | state.headTabNavList = payload; 86 | }, 87 | /** 88 | * 关闭当前tabNav,并跳转自定义路由 89 | * @param state 90 | * @param payload RouteLocationRaw 跳转路由参数,必须 91 | */ 92 | closeCurrentHeadTabNav(state, payload: RouteLocationRaw) { 93 | const navList: TabNavItem[] = state.headTabNavList.filter((item2: TabNavItem) => !equalTabNavRoute(router.currentRoute.value, item2.route, item2.menu.tabNavType)) 94 | state.headTabNavList = [ 95 | ...navList 96 | ] 97 | 98 | router.push(payload) 99 | }, 100 | setTopNavEnable(state, payload) { 101 | state.topNavEnable = payload; 102 | }, 103 | setTheme(state, payload) { 104 | state.theme = payload; 105 | }, 106 | setNavMode(state, payload) { 107 | state.navMode = payload; 108 | }, 109 | setLeftSiderFixed(state, payload) { 110 | state.leftSiderFixed = payload; 111 | }, 112 | }, 113 | actions: {} 114 | } 115 | 116 | 117 | 118 | export default StoreModel; 119 | -------------------------------------------------------------------------------- /src/views/home/components/HotSearchCard/index.vue: -------------------------------------------------------------------------------- 1 | 19 | 95 | -------------------------------------------------------------------------------- /src/views/home/components/ArticleHitCard/index.vue: -------------------------------------------------------------------------------- 1 | 19 | 95 | -------------------------------------------------------------------------------- /mock/home.js: -------------------------------------------------------------------------------- 1 | const mockjs= require('mockjs'); 2 | const { VUE_APP_APIHOST } = process.env; 3 | const mock = {}; 4 | 5 | mock[`GET ${VUE_APP_APIHOST || ''}/home/articles/dailynew`] = (req, res) => { 6 | res.send({ 7 | code: 0, 8 | data: { 9 | total: mockjs.mock('@integer(1000,10000)'), 10 | num: mockjs.mock('@integer(10,100)'), 11 | day: mockjs.mock('@float(-10,15,0,2)'), 12 | week: mockjs.mock('@float(-10,15,0,2)'), 13 | }, 14 | }); 15 | }; 16 | 17 | mock[`GET ${VUE_APP_APIHOST || ''}/home/works/weeknew`] = (req, res) => { 18 | res.send({ 19 | code: 0, 20 | data: { 21 | total: mockjs.mock('@integer(1000,10000)'), 22 | num: mockjs.mock('@integer(10,100)'), 23 | chart: mockjs.mock({ 24 | 'day|7': ['03-01'], 25 | 'num|7': ['@integer(0,3)'], 26 | }), 27 | }, 28 | }); 29 | }; 30 | 31 | mock[`GET ${VUE_APP_APIHOST || ''}/home/topics/monthnew`] = (req, res) => { 32 | res.send({ 33 | code: 0, 34 | data: { 35 | total: mockjs.mock('@integer(1000,10000)'), 36 | num: mockjs.mock('@integer(10,100)'), 37 | chart: mockjs.mock({ 38 | 'day|30': ['03-01'], 39 | 'num|30': ['@integer(0,2)'], 40 | }), 41 | }, 42 | }); 43 | }; 44 | 45 | mock[`GET ${VUE_APP_APIHOST || ''}/home/links/annualnew`] = (req, res) => { 46 | res.send({ 47 | code: 0, 48 | data: { 49 | total: mockjs.mock('@integer(1000,10000)'), 50 | num: mockjs.mock('@integer(10,100)'), 51 | chart: mockjs.mock({ 52 | 'day|12': ['2019-03'], 53 | 'num|12': ['@integer(0,8)'], 54 | }), 55 | }, 56 | }); 57 | }; 58 | 59 | mock[`GET ${VUE_APP_APIHOST || ''}/home/searchs/keywords`] = (req, res) => { 60 | res.send({ 61 | code: 0, 62 | data: mockjs.mock({ 63 | total: 1000, 64 | currentPage: 1, 65 | 'list|5': [ 66 | { 67 | name: '@ctitle(4,8)', 68 | hit: '@integer(1000,10000)', 69 | }, 70 | ], 71 | }), 72 | }); 73 | }; 74 | 75 | mock[`GET ${VUE_APP_APIHOST || ''}/home/tags`] = (req, res) => { 76 | res.send({ 77 | code: 0, 78 | data: mockjs.mock({ 79 | total: 1000, 80 | currentPage: 1, 81 | 'list|5': [ 82 | { 83 | name: '@ctitle(4,6)', 84 | id: '@integer(1)', 85 | pinyin: '@word(10,20)', 86 | hit: '@integer(1000,10000)', 87 | }, 88 | ], 89 | }), 90 | }); 91 | }; 92 | 93 | mock[`GET ${VUE_APP_APIHOST || ''}/home/articles`] = (req, res) => { 94 | res.send({ 95 | code: 0, 96 | data: mockjs.mock({ 97 | total: 1000, 98 | currentPage: 1, 99 | 'list|5': [ 100 | { 101 | category: { 102 | id: '@integer(1)', 103 | alias: '@word(4)', 104 | name: '@cword(4)', 105 | }, 106 | title: '@ctitle(20,30)', 107 | id: '@integer(1)', 108 | addtime: '@datetime', 109 | 'tag|0-3': '@ctitle(4,6),', 110 | hit: '@integer(100,1000)', 111 | }, 112 | ], 113 | }), 114 | }); 115 | }; 116 | 117 | mock[`GET ${VUE_APP_APIHOST || ''}/home/works`] = (req, res) => { 118 | res.send({ 119 | code: 0, 120 | data: mockjs.mock({ 121 | total: 1000, 122 | currentPage: 1, 123 | 'list|5': [ 124 | { 125 | title: '@ctitle(20,30)', 126 | id: '@integer(1)', 127 | addtime: '@datetime', 128 | 'tag|0-3': '@ctitle(4,6),', 129 | hit: '@integer(100,1000)', 130 | }, 131 | ], 132 | }), 133 | }); 134 | }; 135 | 136 | 137 | 138 | module.exports = { 139 | ...mock 140 | }; -------------------------------------------------------------------------------- /src/views/pagesample/list/highly-adaptive-table/components/SearchDrawer.vue: -------------------------------------------------------------------------------- 1 | 51 | --------------------------------------------------------------------------------