diff --git a/.env b/.env index 229d031..0c6e7c8 100644 --- a/.env +++ b/.env @@ -1,3 +1,4 @@ +VUE_APP_PUBLIC_PATH=/ VUE_APP_NAME=Admin VUE_APP_ROUTES_KEY=admin.routes VUE_APP_PERMISSIONS_KEY=admin.permissions @@ -5,4 +6,5 @@ VUE_APP_ROLES_KEY=admin.roles VUE_APP_USER_KEY=admin.user VUE_APP_SETTING_KEY=admin.setting VUE_APP_TBAS_KEY=admin.tabs +VUE_APP_TBAS_TITLES_KEY=admin.tabs.titles VUE_APP_API_BASE_URL=http://api.iczer.com diff --git a/.gitignore b/.gitignore index 7595136..5542012 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,4 @@ selenium-debug.log *.njsproj *.sln package-lock.json +.env.production.local diff --git a/README.en-US.md b/README.en-US.md index f36a2c4..a8f1bd0 100644 --- a/README.en-US.md +++ b/README.en-US.md @@ -18,7 +18,7 @@ Multiple theme modes available: - Preview:https://iczer.gitee.io/vue-antd-admin - Documentation:https://iczer.gitee.io/vue-antd-admin-docs -- FAQ:https://iczer.github.io/vue-antd-admin/start/faq.html +- FAQ:https://iczer.gitee.io/vue-antd-admin-docs/start/faq.html - Mirror Repo in China:https://gitee.com/iczer/vue-antd-admin ## Browsers support @@ -43,11 +43,11 @@ $ yarn serve $ npm install $ npm run serve ``` -More instructions at [documentation](https://iczer.github.io/vue-antd-admin). +More instructions at [documentation](https://iczer.gitee.io/vue-antd-admin-docs). ## Contributing Any type of contribution is welcome, here are some examples of how you may contribute to this project: :star2:: - Use Vue Antd Admin in your daily work. - Submit [Issue](https://github.com/iczer/vue-antd-admin/issues) to report :bug: or ask questions. - Propose [Pull Request](https://github.com/iczer/vue-antd-admin/pulls) to improve our code. -- Join the community and share your experiences with us. QQ Group: 610090280 +- Join the community and share your experiences with us. QQ Group:942083829、812277510(full)、610090280(full) diff --git a/README.md b/README.md index c842f02..7bac601 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ - 预览地址:https://iczer.gitee.io/vue-antd-admin - 使用文档:https://iczer.gitee.io/vue-antd-admin-docs -- 常见问题:https://iczer.github.io/vue-antd-admin/start/faq.html +- 常见问题:https://iczer.gitee.io/vue-antd-admin-docs/start/faq.html - 国内镜像:https://gitee.com/iczer/vue-antd-admin ## 浏览器支持 @@ -43,11 +43,18 @@ $ yarn serve $ npm install $ npm run serve ``` -更多信息参考 [使用文档](https://iczer.github.io/vue-antd-admin) +更多信息参考 [使用文档](https://iczer.gitee.io/vue-antd-admin-docs) ## 参与贡献 我们非常欢迎你的贡献,你可以通过以下方式和我们一起共建 :star2:: - 在你的公司或个人项目中使用 Vue Antd Admin。 - 通过 [Issue](https://github.com/iczer/vue-antd-admin/issues) 报告:bug:或进行咨询。 - 提交 [Pull Request](https://github.com/iczer/vue-antd-admin/pulls) 改进 Admin 的代码。 -- 加入社群,与小伙伴们一同交流心得。QQ群:610090280 +- 加入社群,与小伙伴们一同交流心得。QQ群:942083829、 812277510(已满)、610090280(已满) + +## 打赏 +如果该项目对您有所帮助,可以请作者喝一杯咖啡。 +

+ + +

diff --git a/docs/.vuepress/config.js b/docs/.vuepress/config.js index 156ab7d..3bd9f53 100644 --- a/docs/.vuepress/config.js +++ b/docs/.vuepress/config.js @@ -36,7 +36,8 @@ module.exports = { title: '进阶', collapsable: false, children: [ - '/advance/i18n', '/advance/async', '/advance/authority', '/advance/login', '/advance/guard', '/advance/interceptors' + '/advance/i18n', '/advance/async', '/advance/authority', '/advance/login', '/advance/guard', '/advance/interceptors', + '/advance/api' ] }, { diff --git a/docs/advance/api.md b/docs/advance/api.md new file mode 100644 index 0000000..47183d7 --- /dev/null +++ b/docs/advance/api.md @@ -0,0 +1,42 @@ +--- +title: 全局API +lang: zn-CN +--- +# 全局API +我们提供了一些全局Api,在日常功能开发中或许会有帮助,它们均被绑定到了页面组件或子组件实例上。 +在组件内可以直接通过`this.$[apiName]`的方式调用。如下: + +## 多页签 +### $closePage(closeRoute, nextRoute) +该api用于关闭当前已打开的页签,接收两个参数: +* **closeRoute** +要关闭的页签对应的 route 对象,可简写为路由的 fullPath 字符串值。 +* **nextRoute** +关闭页签要后跳转的 route 对象,可不传,不传则会自动选择打开页签(临近原则)。 + +### $refreshPage(route) +该api用于刷新路由对应的页签,接收一个参数: +* **route** +要刷新的页签对应的 route 对象,可简写为路由的 fullPath 字符串值。 + +### $openPage(route, title) +该api用于打开一个新页签,接收两个参数: +* **route** +要打开的页签对应的 route 对象,可简写为路由的 fullPath 字符串值。 +* **title** +设置打开页签的标题,可不传。 + +### $setPageTitle(route, title) +该api用于设置页签的标题,接收两个参数: +* **route** +要设置的页签对应的 route 对象,可简写为路由的 fullPath 字符串值。 +* **title** +页签的标题。 + +## 权限 +### $auth(check, type) +该api可以用于操作权限校验,接收两个参数: +* **check** +需要要校验的操作权限 +* **type** +操作权限校验类别,可选 `permission` 和 `role`,即通过权限校验还是角色进行校验,可不传(不传的话,会对两种类型都进行匹配,任意一种匹配成功即校验通过)。 \ No newline at end of file diff --git a/docs/advance/i18n.md b/docs/advance/i18n.md index 630221d..024e667 100644 --- a/docs/advance/i18n.md +++ b/docs/advance/i18n.md @@ -3,5 +3,127 @@ title: 国际化 lang: zn-CN --- # 国际化 +vue-antd-admin 采用 [vue-i18n](https://kazupon.github.io/vue-i18n/) 插件来实现国际化,该项目已经内置并且加载好了基础配置。可以直接上手使用。 -### 作者还没来得及编辑该页面,如果你感兴趣,可以点击下方链接,帮助作者完善此页 +> 如果你还没有看快速入门,请先移步查看: [页面 -> i18n国际化配置](../develop/page.html#i18n国际化配置) + + +## 菜单和路由 + +### 默认情况 +如果你没有对菜单进行国际化配置,admin 默认会从路由数据中提取数据作为国际化配置。route.name 作为中文语言,route.path 作为英文语言。 +国际化提取函数定义在 `@/utils/i18n.js` 文件中,会在路由加载时调用,如下: +```js +/** + * 从路由提取国际化数据 + * @param i18n + * @param routes + */ +function mergeI18nFromRoutes(i18n, routes) { + formatFullPath(routes) + const CN = generateI18n(new Object(), routes, 'name') + const US = generateI18n(new Object(), routes, 'path') + i18n.mergeLocaleMessage('CN', CN) + i18n.mergeLocaleMessage('US', US) + const messages = routesI18n.messages + Object.keys(messages).forEach(lang => { + i18n.mergeLocaleMessage(lang, messages[lang]) + }) +} +``` +### 自定义 +如果你想自定义菜单国际化数据,可在 `@/router/i18n.js` 文件中配置。我们以路由的 path 作为 key(嵌套path 的写法也会被解析),name 作为 国际化语言的值。 +假设你有一个路由的配置如下: +```js +[{ + path: 'parent', + ... + children: [{ + path: 'self', + ... + }] +}] + +or + +[{ + path: 'other', + ... + children: [{ + path: '/parent/self', // 在国际化配置中 key 会解析为 parent.self + ... + }] +}] +``` +那么你需要在 `@/router/i18n.js` 中这样配置: +```jsx + messages: { + CN: { + parent: { + name: '父級菜單', + self: {name: '菜單名'}, + }, + US: { + parent: { + name: 'parent menu', + self: {name: 'menu name'}, + }, + HK: { + parent: { + name: '父級菜單', + self: {name: '菜單名'}, + }, +``` + +## 添加语言 + +首先在 `@/layouts/header/AdminHeader.vue` ,新增一门语言 (多个同理)。 + +```vue {15} + + +``` + +> TIP: 后续开发建议把这里改成动态配置的方式! + +然后开始往 `@/router/i18n.js` 和 `@/pages/你的页面/i18n.js` 里面分别添加上语言的翻译。 + +```vue {12,13,14} +module.exports = { + messages: { + CN: { + home: {name: '首页'}, + }, + US: { + home: {name: 'home'}, + }, + HK: { + home: {name: '首頁'}, + }, + JP: { + home: {name: '最初のページ'}, + }, + } +} +``` + +> Notice: 更多用法请移步到 [vue-i18n](https://kazupon.github.io/vue-i18n/) 。 \ No newline at end of file diff --git a/docs/other/community.md b/docs/other/community.md index 8e55350..a74a5b7 100644 --- a/docs/other/community.md +++ b/docs/other/community.md @@ -5,4 +5,4 @@ lang: zh-CN # 社区 ## 交流学习 -### QQ群:610090280 +### QQ群:812277510、610090280(已满) diff --git a/package.json b/package.json index 9834a23..09d383e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "vue-antd-admin", - "version": "0.6.1", + "version": "0.7.2", "homepage": "https://iczer.github.io/vue-antd-admin", "private": true, "scripts": { @@ -16,7 +16,7 @@ "dependencies": { "@antv/data-set": "^0.11.4", "animate.css": "^4.1.0", - "ant-design-vue": "^1.6.2", + "ant-design-vue": "1.7.2", "axios": "^0.19.2", "clipboard": "^2.0.6", "core-js": "^3.6.5", diff --git a/public/index.html b/public/index.html index 5aeeef3..803dbb4 100644 --- a/public/index.html +++ b/public/index.html @@ -11,11 +11,13 @@ <% } %> - + -
+
+
+
<% for (var i in htmlWebpackPlugin.options.cdn && htmlWebpackPlugin.options.cdn.js) { %> diff --git a/src/App.vue b/src/App.vue index 925dab1..31e6587 100644 --- a/src/App.vue +++ b/src/App.vue @@ -1,5 +1,5 @@ @@ -31,6 +31,7 @@ export default { }, lang(val) { this.setLanguage(val) + this.setHtmlTitle() }, $route() { this.setHtmlTitle() @@ -42,10 +43,13 @@ export default { 'theme.color': function(val) { let closeMessage = this.$message.loading(`您选择了主题色 ${val}, 正在切换...`) themeUtil.changeThemeColor(val, this.theme.mode).then(closeMessage) + }, + 'layout': function() { + window.dispatchEvent(new Event('resize')) } }, computed: { - ...mapState('setting', ['theme', 'weekMode', 'lang']) + ...mapState('setting', ['layout', 'theme', 'weekMode', 'lang']) }, methods: { ...mapMutations('setting', ['setDevice']), @@ -76,6 +80,9 @@ export default { const key = route.path === '/' ? 'home.name' : getI18nKey(route.matched[route.matched.length - 1].path) document.title = process.env.VUE_APP_NAME + ' | ' + this.$t(key) }, + popContainer() { + return document.getElementById("popContainer") + } } } diff --git a/src/assets/img/alipay.png b/src/assets/img/alipay.png new file mode 100644 index 0000000..be6a86d Binary files /dev/null and b/src/assets/img/alipay.png differ diff --git a/src/assets/img/wechatpay.png b/src/assets/img/wechatpay.png new file mode 100644 index 0000000..ca82de7 Binary files /dev/null and b/src/assets/img/wechatpay.png differ diff --git a/src/components/cache/AKeepAlive.js b/src/components/cache/AKeepAlive.js index 8c0eb8b..b04660a 100644 --- a/src/components/cache/AKeepAlive.js +++ b/src/components/cache/AKeepAlive.js @@ -4,7 +4,16 @@ const patternTypes = [String, RegExp, Array] function matches (pattern, name) { if (Array.isArray(pattern)) { - return pattern.indexOf(name) > -1 + if (pattern.indexOf(name) > -1) { + return true + } else { + for (let item of pattern) { + if (isRegExp(item) && item.test(name)) { + return true + } + } + return false + } } else if (typeof pattern === 'string') { return pattern.split(',').indexOf(name) > -1 } else if (isRegExp(pattern)) { @@ -18,6 +27,13 @@ function getComponentName (opts) { return opts && (opts.Ctor.options.name || opts.tag) } +function getComponentKey (vnode) { + const {componentOptions, key} = vnode + return key == null + ? componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '') + : key + componentOptions.Ctor.cid +} + function getFirstComponentChild (children) { if (Array.isArray(children)) { for (let i = 0; i < children.length; i++) { @@ -35,7 +51,8 @@ function pruneCache (keepAliveInstance, filter) { const cachedNode = cache[key] if (cachedNode) { const name = getComponentName(cachedNode.componentOptions) - if (name && !filter(name)) { + const componentKey = getComponentKey(cachedNode) + if (name && !filter(name, componentKey)) { pruneCacheEntry(cache, key, keys, _vnode) } } @@ -70,6 +87,7 @@ export default { props: { include: patternTypes, exclude: patternTypes, + excludeKeys: patternTypes, max: [String, Number], clearCaches: Array }, @@ -98,10 +116,13 @@ export default { mounted () { this.$watch('include', val => { - pruneCache(this, name => matches(val, name)) + pruneCache(this, (name) => matches(val, name)) }) this.$watch('exclude', val => { - pruneCache(this, name => !matches(val, name)) + pruneCache(this, (name) => !matches(val, name)) + }) + this.$watch('excludeKeys', val => { + pruneCache(this, (name, key) => !matches(val, key)) }) }, @@ -112,12 +133,14 @@ export default { if (componentOptions) { // check pattern const name = getComponentName(componentOptions) - const { include, exclude } = this + const componentKey = getComponentKey(vnode) + const { include, exclude, excludeKeys } = this if ( // not included (include && (!name || !matches(include, name))) || // excluded - (exclude && name && matches(exclude, name)) + (exclude && name && matches(exclude, name)) || + (excludeKeys && componentKey && matches(excludeKeys, componentKey)) ) { return vnode } @@ -127,7 +150,7 @@ export default { // same constructor may get registered as different local components // so cid alone is not enough (#3269) ? componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '') - : vnode.key + : vnode.key + componentOptions.Ctor.cid if (cache[key]) { vnode.componentInstance = cache[key].componentInstance // make current key freshest diff --git a/src/components/menu/menu.js b/src/components/menu/menu.js index e560e97..87075a9 100644 --- a/src/components/menu/menu.js +++ b/src/components/menu/menu.js @@ -200,9 +200,10 @@ export default { }) }, updateMenu () { - const menuRoutes = this.$route.matched.filter(item => item.path !== '') + const matchedRoutes = this.$route.matched.filter(item => item.path !== '') this.selectedKeys = this.getSelectedKey(this.$route) - let openKeys = menuRoutes.map(item => item.path) + let openKeys = matchedRoutes.map(item => item.path) + openKeys = openKeys.slice(0, openKeys.length -1) if (!fastEqual(openKeys, this.sOpenKeys)) { this.collapsed || this.mode === 'horizontal' ? this.cachedOpenKeys = openKeys : this.sOpenKeys = openKeys } diff --git a/src/components/table/StandardTable.vue b/src/components/table/StandardTable.vue index 3f54e41..44c4574 100644 --- a/src/components/table/StandardTable.vue +++ b/src/components/table/StandardTable.vue @@ -95,7 +95,14 @@ export default { return { ...item, total: selectedRows.reduce((sum, val) => { - return sum + val[item.dataIndex] + let v + try{ + v = val[item.dataIndex] ? val[item.dataIndex] : eval(`val.${item.dataIndex}`); + }catch(_){ + v = val[item.dataIndex]; + } + v = !isNaN(parseFloat(v)) ? parseFloat(v) : 0; + return sum + v }, 0) } }) diff --git a/src/components/table/advance/ActionColumns.vue b/src/components/table/advance/ActionColumns.vue index 8e357c4..3a97ec9 100644 --- a/src/components/table/advance/ActionColumns.vue +++ b/src/components/table/advance/ActionColumns.vue @@ -49,31 +49,23 @@ checkedCounts(val) { this.checkAll = val === this.columns.length this.indeterminate = val > 0 && val < this.columns.length + }, + columns(newVal, oldVal) { + if (newVal != oldVal) { + this.checkedCounts = newVal.length + this.formatColumns(newVal) + } } }, created() { - this.$emit('update:visibleColumns', [...this.columns]) - for (let col of this.columns) { - if (col.visible === undefined) { - this.$set(col, 'visible', true) - } - if (!col.visible) { - this.checkedCounts -= 1 - this.$set(col, 'colSpan', 0) - this.$set(col, 'customCell', () => ({style: 'display: none;'})) - } - } + this.formatColumns(this.columns) }, methods: { onCheckChange(e, col) { if (!col.visible) { this.checkedCounts -= 1 - this.$set(col, 'colSpan', 0) - this.$set(col, 'customCell', () => ({style: 'display: none;'})) } else { this.checkedCounts += 1 - this.$set(col, 'colSpan', undefined) - this.$set(col, 'customCell', undefined) } }, fixColumn(fixed, col) { @@ -85,7 +77,6 @@ }, setSearch(col) { this.$set(col, 'searchAble', !col.searchAble) - console.log(col) if (!col.searchAble && col.search) { this.resetSearch(col) } @@ -101,13 +92,8 @@ backColumns.forEach((back, index) => { const column = columns[index] column.visible = back.visible === undefined || back.visible - if (column.visible) { - this.$set(column, 'colSpan', undefined) - this.$set(column, 'customCell', undefined) - } else { + if (!column.visible) { counts -= 1 - this.$set(column, 'colSpan', 0) - this.$set(column, 'customCell', () => ({style: 'display: none;'})) } if (back.fixed !== undefined) { column.fixed = back.fixed @@ -125,18 +111,10 @@ onCheckAllChange(e) { if (e.target.checked) { this.checkedCounts = this.columns.length - this.columns.forEach(col => { - col.visible = true - this.$set(col, 'colSpan', undefined) - this.$set(col, 'customCell', undefined) - }) + this.columns.forEach(col => col.visible = true) } else { this.checkedCounts = 0 - this.columns.forEach(col => { - col.visible = false - this.$set(col, 'colSpan', 0) - this.$set(col, 'customCell', () => ({style: 'display: none;'})) - }) + this.columns.forEach(col => col.visible = false) } }, getConditions(columns) { @@ -146,6 +124,16 @@ conditions[col.dataIndex] = col.search.value }) return conditions + }, + formatColumns(columns) { + for (let col of columns) { + if (col.visible === undefined) { + this.$set(col, 'visible', true) + } + if (!col.visible) { + this.checkedCounts -= 1 + } + } } } } diff --git a/src/components/table/advance/AdvanceTable.vue b/src/components/table/advance/AdvanceTable.vue index 573ab2a..73a3c2e 100644 --- a/src/components/table/advance/AdvanceTable.vue +++ b/src/components/table/advance/AdvanceTable.vue @@ -32,7 +32,7 @@ slot !== 'expandedRowRender' && slot !== 'title') + }, + visibleColumns(){ + return this.columns.filter(col => col.visible) } }, created() { diff --git a/src/components/table/advance/SearchArea.vue b/src/components/table/advance/SearchArea.vue index 3556158..cba17af 100644 --- a/src/components/table/advance/SearchArea.vue +++ b/src/components/table/advance/SearchArea.vue @@ -7,7 +7,10 @@ {{col.title}}: - +
@@ -70,12 +73,14 @@ props: ['columns', 'formatConditions'], inject: ['table'], created() { - this.columns.forEach(item => { - this.$set(item, 'search', {...item.search, visible: false, value: undefined, format: this.getFormat(item)}) - }) - console.log(this.columns) + this.formatColumns(this.columns) }, watch: { + columns(newVal, oldVal) { + if (newVal != oldVal) { + this.formatColumns(newVal) + } + }, searchCols(newVal, oldVal) { if (newVal.length != oldVal.length) { const newConditions = this.getConditions(newVal) @@ -218,6 +223,11 @@ return true } return false + }, + formatColumns(columns) { + columns.forEach(item => { + this.$set(item, 'search', {...item.search, visible: false, value: undefined, format: this.getFormat(item)}) + }) } } } diff --git a/src/components/tool/AvatarList.vue b/src/components/tool/AvatarList.vue index f2c0715..7390cce 100644 --- a/src/components/tool/AvatarList.vue +++ b/src/components/tool/AvatarList.vue @@ -35,7 +35,7 @@ const Item = { return h( 'li', {class: 'avatar-item'}, - [!this.$props.tips ? h(ATooltip, {props: {title: this.$props.tips}}, [avatar]) : avatar] + [this.$props.tips ? h(ATooltip, {props: {title: this.$props.tips}}, [avatar]) : avatar] ) } } diff --git a/src/config/default/setting.config.js b/src/config/default/setting.config.js index 361f158..19e736a 100644 --- a/src/config/default/setting.config.js +++ b/src/config/default/setting.config.js @@ -6,7 +6,7 @@ module.exports = { mode: 'dark', //主题模式 可选 dark、 light 和 night success: '#52c41a', //成功色 warning: '#faad14', //警告色 - error: '#f5222d', //错误色 + error: '#f5222f', //错误色 }, layout: 'side', //导航布局,可选 side 和 head,分别为侧边导航和顶部导航 fixedHeader: false, //固定头部状态栏,true:固定,false:不固定 @@ -15,6 +15,7 @@ module.exports = { pageWidth: 'fixed', //内容区域宽度,fixed:固定宽度,fluid:流式宽度 weekMode: false, //色弱模式,true:开启,false:不开启 multiPage: false, //多页签模式,true:开启,false:不开启 + cachePage: true, //是否缓存页面数据,仅多页签模式下生效,true 缓存, false 不缓存 hideSetting: false, //隐藏设置抽屉,true:隐藏,false:不隐藏 systemName: 'Vue Antd Admin', //系统名称 copyright: '2018 ICZER 工作室出品', //copyright diff --git a/src/config/replacer/resolve.config.js b/src/config/replacer/resolve.config.js index 6159da4..34c0db6 100644 --- a/src/config/replacer/resolve.config.js +++ b/src/config/replacer/resolve.config.js @@ -19,12 +19,24 @@ const cssResolve = { return cssObj.toText() } }, + '.ant-tree-checkbox-checked .ant-tree-checkbox-inner::after': { + resolve(cssText, cssObj) { + cssObj.rules.push('border-top:0', 'border-left:0') + return cssObj.toText() + } + }, '.ant-checkbox-checked .ant-checkbox-inner:after': { resolve(cssText, cssObj) { cssObj.rules.push('border-top:0', 'border-left:0') return cssObj.toText() } }, + '.ant-tree-checkbox-checked .ant-tree-checkbox-inner:after': { + resolve(cssText, cssObj) { + cssObj.rules.push('border-top:0', 'border-left:0') + return cssObj.toText() + } + }, '.ant-menu-dark .ant-menu-inline.ant-menu-sub': { resolve(cssText, cssObj) { cssObj.rules = cssObj.rules.filter(rule => rule.indexOf('box-shadow') == -1) diff --git a/src/layouts/PageLayout.vue b/src/layouts/PageLayout.vue index e936145..6ce29a8 100644 --- a/src/layouts/PageLayout.vue +++ b/src/layouts/PageLayout.vue @@ -60,10 +60,10 @@ export default { this.updatePageHeight(0) }, computed: { - ...mapState('setting', ['layout', 'multiPage', 'pageMinHeight', 'pageWidth']), + ...mapState('setting', ['layout', 'multiPage', 'pageMinHeight', 'pageWidth', 'customTitles']), pageTitle() { let pageTitle = this.page && this.page.title - return pageTitle === undefined ? (this.title || this.routeName) : this.$t(pageTitle) + return this.customTitle || (pageTitle && this.$t(pageTitle)) || this.title || this.routeName }, routeName() { const route = this.$route @@ -90,11 +90,17 @@ export default { ...mapMutations('setting', ['correctPageMinHeight']), getRouteBreadcrumb() { let routes = this.$route.matched + const path = this.$route.path let breadcrumb = [] - routes.forEach(route => { + routes.filter(item => path.includes(item.path)) + .forEach(route => { const path = route.path.length === 0 ? '/home' : route.path breadcrumb.push(this.$t(getI18nKey(path))) }) + let pageTitle = this.page && this.page.title + if (this.customTitle || pageTitle) { + breadcrumb[breadcrumb.length - 1] = this.customTitle || pageTitle + } return breadcrumb }, /** diff --git a/src/layouts/footer/PageFooter.vue b/src/layouts/footer/PageFooter.vue index 6689c1c..7c0d079 100644 --- a/src/layouts/footer/PageFooter.vue +++ b/src/layouts/footer/PageFooter.vue @@ -26,6 +26,9 @@ export default { .copyright{ color: @text-color-second; font-size: 14px; + i { + margin: 0 4px; + } } .links{ margin-bottom: 8px; diff --git a/src/layouts/tabs/TabsHead.vue b/src/layouts/tabs/TabsHead.vue index 958662f..a660728 100644 --- a/src/layouts/tabs/TabsHead.vue +++ b/src/layouts/tabs/TabsHead.vue @@ -63,7 +63,7 @@ this.affixed = this.fixedTabs }, computed: { - ...mapState('setting', ['layout', 'pageWidth', 'fixedHeader', 'fixedTabs']), + ...mapState('setting', ['layout', 'pageWidth', 'fixedHeader', 'fixedTabs', 'customTitles']), lockTitle() { return this.$t(this.fixedTabs ? 'unlock' : 'lock') } @@ -95,7 +95,9 @@ this.$emit('contextmenu', pageKey, e) }, pageName(page) { - return this.$t(getI18nKey(page.keyPath)) + const pagePath = page.fullPath.split('?')[0] + const custom = this.customTitles.find(item => item.path === pagePath) + return (custom && custom.title) || page.title || this.$t(getI18nKey(page.keyPath)) } } } @@ -115,7 +117,7 @@ .icon-close{ font-size: 12px; margin-left: 6px; - margin-right: -4px; + margin-right: -4px !important; color: @text-color-second; &:hover{ color: @text-color; diff --git a/src/layouts/tabs/TabsView.vue b/src/layouts/tabs/TabsView.vue index b8461bc..beb8ae7 100644 --- a/src/layouts/tabs/TabsView.vue +++ b/src/layouts/tabs/TabsView.vue @@ -12,10 +12,10 @@ />
- + - +
@@ -40,11 +40,12 @@ export default { pageList: [], activePage: '', menuVisible: false, - refreshing: false + refreshing: false, + excludeKeys: [] } }, computed: { - ...mapState('setting', ['multiPage', 'animate', 'layout', 'pageWidth']), + ...mapState('setting', ['multiPage', 'cachePage', 'animate', 'layout', 'pageWidth']), menuItemList() { return [ { key: '1', icon: 'vertical-right', text: this.$t('closeLeft') }, @@ -58,6 +59,7 @@ export default { } }, created () { + this.loadCacheConfig(this.$router?.options?.routes) this.loadCachedTabs() const route = this.$route if (this.pageList.findIndex(item => item.fullPath === route.fullPath) === -1) { @@ -79,6 +81,10 @@ export default { this.correctPageMinHeight(this.tabsOffset) }, watch: { + '$router.options.routes': function (val) { + this.excludeKeys = [] + this.loadCacheConfig(val) + }, '$route': function (newRoute) { this.activePage = newRoute.fullPath if (!this.multiPage) { @@ -249,6 +255,7 @@ export default { return { keyPath: route.matched[route.matched.length - 1].path, fullPath: route.fullPath, loading: false, + title: route.meta && route.meta.page && route.meta.page.title, unclose: route.meta && route.meta.page && (route.meta.page.closable === false), } }, @@ -260,7 +267,8 @@ export default { const page = this.pageList.find(item => item.fullPath === route.fullPath) page.unclose = route.meta && route.meta.page && (route.meta.page.closable === false) if (!page._init_) { - page.cachedKey = this.$refs.tabContent.$vnode.key + const vnode = this.$refs.tabContent.$vnode + page.cachedKey = vnode.key + vnode.componentOptions.Ctor.cid page._init_ = true } }, @@ -282,6 +290,17 @@ export default { } } }, + loadCacheConfig(routes, pCache = true) { + routes.forEach(item => { + const cacheAble = item.meta?.page?.cacheAble ?? pCache ?? true + if (!cacheAble) { + this.excludeKeys.push(new RegExp(`${item.fullPath}\\d+$`)) + } + if (item.children) { + this.loadCacheConfig(item.children, cacheAble) + } + }) + }, ...mapMutations('setting', ['correctPageMinHeight']) } } diff --git a/src/main.js b/src/main.js index 21e5667..9c8b12b 100644 --- a/src/main.js +++ b/src/main.js @@ -10,6 +10,7 @@ import 'animate.css/source/animate.css' import Plugins from '@/plugins' import {initI18n} from '@/utils/i18n' import bootstrap from '@/bootstrap' +import 'moment/locale/zh-cn' const router = initRouter(store.state.setting.asyncRoutes) const i18n = initI18n('CN', 'US') diff --git a/src/mock/goods/index.js b/src/mock/goods/index.js index 4bdfa17..9af9e53 100644 --- a/src/mock/goods/index.js +++ b/src/mock/goods/index.js @@ -48,4 +48,59 @@ Mock.mock(RegExp(`${process.env.VUE_APP_API_BASE_URL}/goods` + '.*'),'get', ({ur list: result } } +}) + +const columnsConfig = [ + { + title: '商品名称', + dataIndex: 'name', + searchAble: true + }, + { + title: '订单号', + dataIndex: 'orderId' + }, + { + searchAble: true, + dataIndex: 'status', + dataType: 'select', + slots: {title: 'statusTitle'}, + scopedSlots: {customRender: 'status'}, + search: { + selectOptions: [ + {title: '已下单', value: 1}, + {title: '已付款', value: 2}, + {title: '已审核', value: 3}, + // {title: '已发货', value: 4} + ] + } + }, + { + title: '发货', + searchAble: true, + dataIndex: 'send', + dataType: 'boolean', + scopedSlots: {customRender: 'send'} + }, + { + title: '发货时间', + dataIndex: 'sendTime', + dataType: 'datetime' + }, + { + title: '下单日期', + searchAble: true, + dataIndex: 'orderDate', + dataType: 'date', + visible: false + }, + { + title: '审核时间', + dataIndex: 'auditTime', + dataType: 'time', + }, +] + +Mock.mock(`${process.env.VUE_APP_API_BASE_URL}/columns`, 'get', () => { + return columnsConfig }) \ No newline at end of file diff --git a/src/mock/user/login.js b/src/mock/user/login.js index c8b0279..97db1a1 100644 --- a/src/mock/user/login.js +++ b/src/mock/user/login.js @@ -8,21 +8,32 @@ const user = Mock.mock({ position: '@POSITION' }) Mock.mock(`${process.env.VUE_APP_API_BASE_URL}/login`, 'post', ({body}) => { - let result = {} + let result = {data: {}} const {name, password} = JSON.parse(body) - if (name !== 'admin' || password !== '888888') { - result.code = -1 - result.message = '账户名或密码错误(admin/888888)' + let success = false + + if (name === 'admin' && password === '888888') { + success = true + result.data.permissions = [{id: 'queryForm', operation: ['add', 'edit']}] + result.data.roles = [{id: 'admin', operation: ['add', 'edit', 'delete']}] + } else if (name === 'test' || password === '888888') { + success = true + result.data.permissions = [{id: 'queryForm', operation: ['add', 'edit']}] + result.data.roles = [{id: 'test', operation: ['add', 'edit', 'delete']}] } else { + success = false + } + + if (success) { result.code = 0 result.message = Mock.mock('@TIMEFIX').CN + ',欢迎回来' - result.data = {} result.data.user = user result.data.token = 'Authorization:' + Math.random() result.data.expireAt = new Date(new Date().getTime() + 30 * 60 * 1000) - result.data.permissions = [{id: 'queryForm', operation: ['add', 'edit']}] - result.data.roles = [{id: 'admin', operation: ['add', 'edit', 'delete']}] + } else { + result.code = -1 + result.message = '账户名或密码错误(admin/888888 or test/888888)' } return result }) diff --git a/src/pages/components/table/Table.vue b/src/pages/components/table/Table.vue index 7625a10..be829c4 100644 --- a/src/pages/components/table/Table.vue +++ b/src/pages/components/table/Table.vue @@ -90,24 +90,19 @@ searchAble: true, dataIndex: 'send', dataType: 'boolean', - scopedSlots: {customRender: 'send'} - }, - { - title: '发货时间', - dataIndex: 'sendTime', - dataType: 'datetime' - }, - { - title: '下单日期', - searchAble: true, - dataIndex: 'orderDate', - dataType: 'date' + scopedSlots: {customRender: 'send'}, + search: { + switchOptions: { + checkedText: '开', + uncheckedText: '关' + } + } }, { title: '审核时间', dataIndex: 'auditTime', dataType: 'time', - }, + } ], dataSource: [], conditions: {} @@ -115,6 +110,7 @@ }, created() { this.getGoodList() + this.getColumns() }, methods: { getGoodList() { @@ -129,8 +125,12 @@ this.loading = false }) }, + getColumns() { + ds.goodsColumns().then(res => { + this.columns = res.data + }) + }, onSearch(conditions, searchOptions) { - console.log(conditions) console.log(searchOptions) this.page = 1 this.conditions = conditions diff --git a/src/pages/list/QueryList.vue b/src/pages/list/QueryList.vue index 0f9e58d..f5a1f4f 100644 --- a/src/pages/list/QueryList.vue +++ b/src/pages/list/QueryList.vue @@ -79,7 +79,7 @@
-
+ 新建 批量操作 @@ -91,7 +91,7 @@ 更多操作 -
+ item.path === path) + return custom && custom.title } } }) diff --git a/src/router/config.js b/src/router/config.js index 80c66d6..88a89ff 100644 --- a/src/router/config.js +++ b/src/router/config.js @@ -56,6 +56,9 @@ const options = { name: '表单页', meta: { icon: 'form', + page: { + cacheAble: false + } }, component: PageView, children: [ diff --git a/src/services/api.js b/src/services/api.js index 26eee91..f74208a 100644 --- a/src/services/api.js +++ b/src/services/api.js @@ -6,4 +6,5 @@ module.exports = { LOGIN: `${BASE_URL}/login`, ROUTES: `${BASE_URL}/routes`, GOODS: `${BASE_URL}/goods`, + GOODS_COLUMNS: `${BASE_URL}/columns`, } diff --git a/src/services/dataSource.js b/src/services/dataSource.js index 1846189..22bb960 100644 --- a/src/services/dataSource.js +++ b/src/services/dataSource.js @@ -1,8 +1,12 @@ -import {GOODS} from './api' +import {GOODS, GOODS_COLUMNS} from './api' import {METHOD, request} from '@/utils/request' export async function goodsList(params) { return request(GOODS, METHOD.GET, params) } -export default {goodsList} \ No newline at end of file +export async function goodsColumns() { + return request(GOODS_COLUMNS, METHOD.GET) +} + +export default {goodsList, goodsColumns} \ No newline at end of file diff --git a/src/store/modules/setting.js b/src/store/modules/setting.js index ce3857d..6fbabf2 100644 --- a/src/store/modules/setting.js +++ b/src/store/modules/setting.js @@ -3,8 +3,11 @@ import {ADMIN} from '@/config/default' import {formatFullPath} from '@/utils/i18n' import {filterMenu} from '@/utils/authority-utils' import {getLocalSetting} from '@/utils/themeUtil' +import deepClone from 'lodash.clonedeep' const localSetting = getLocalSetting(true) +const customTitlesStr = sessionStorage.getItem(process.env.VUE_APP_TBAS_TITLES_KEY) +const customTitles = (customTitlesStr && JSON.parse(customTitlesStr)) || [] export default { namespaced: true, @@ -15,6 +18,7 @@ export default { pageMinHeight: 0, menuData: [], activatedFirst: undefined, + customTitles, ...config, ...localSetting }, @@ -22,7 +26,7 @@ export default { menuData(state, getters, rootState) { if (state.filterMenu) { const {permissions, roles} = rootState.account - filterMenu(state.menuData, permissions, roles) + return filterMenu(deepClone(state.menuData), permissions, roles) } return state.menuData }, @@ -39,11 +43,11 @@ export default { }, subMenu(state) { const {menuData, activatedFirst} = state - if (!menuData[0].fullPath) { + if (menuData.length > 0 && !menuData[0].fullPath) { formatFullPath(menuData) } const current = menuData.find(menu => menu.fullPath === activatedFirst) - return current && current.children ? current.children : [] + return current && current.children || [] } }, mutations: { @@ -94,6 +98,17 @@ export default { }, setFixedTabs(state, fixedTabs) { state.fixedTabs = fixedTabs + }, + setCustomTitle(state, {path, title}) { + if (title) { + const obj = state.customTitles.find(item => item.path === path) + if (obj) { + obj.title = title + } else { + state.customTitles.push({path, title}) + } + sessionStorage.setItem(process.env.VUE_APP_TBAS_TITLES_KEY, JSON.stringify(state.customTitles)) + } } } } diff --git a/src/utils/authority-utils.js b/src/utils/authority-utils.js index 71516b7..f9c171e 100644 --- a/src/utils/authority-utils.js +++ b/src/utils/authority-utils.js @@ -69,15 +69,16 @@ function hasAuthority(route, permissions, roles) { * @param roles */ function filterMenu(menuData, permissions, roles) { - menuData.forEach(menu => { + return menuData.filter(menu => { if (menu.meta && menu.meta.invisible === undefined) { if (!hasAuthority(menu, permissions, roles)) { - menu.meta.invisible = true - } - if (menu.children && menu.children.length > 0) { - filterMenu(menu.children, permissions, roles) + return false } } + if (menu.children && menu.children.length > 0) { + menu.children = filterMenu(menu.children, permissions, roles) + } + return true }) } diff --git a/src/utils/axios-interceptors.js b/src/utils/axios-interceptors.js index 53c3554..ec6f12f 100644 --- a/src/utils/axios-interceptors.js +++ b/src/utils/axios-interceptors.js @@ -9,8 +9,8 @@ const resp401 = { */ onFulfilled(response, options) { const {message} = options - if (response.status === 401) { - message.error('无此接口权限') + if (response.code === 401) { + message.error('无此权限') } return response }, @@ -22,7 +22,10 @@ const resp401 = { */ onRejected(error, options) { const {message} = options - message.error(error.message) + const {response} = error + if (response.status === 401) { + message.error('无此权限') + } return Promise.reject(error) } } @@ -30,10 +33,18 @@ const resp401 = { const resp403 = { onFulfilled(response, options) { const {message} = options - if (response.status === 403) { - message.error(`请求被拒绝`) + if (response.code === 403) { + message.error('请求被拒绝') } return response + }, + onRejected(error, options) { + const {message} = options + const {response} = error + if (response.status === 403) { + message.error('请求被拒绝') + } + return Promise.reject(error) } } diff --git a/src/utils/request.js b/src/utils/request.js index df43642..b7bf84d 100644 --- a/src/utils/request.js +++ b/src/utils/request.js @@ -30,14 +30,14 @@ const METHOD = { * @param params 请求参数 * @returns {Promise>} */ -async function request(url, method, params) { +async function request(url, method, params, config) { switch (method) { case METHOD.GET: - return axios.get(url, {params}) + return axios.get(url, {params, ...config}) case METHOD.POST: - return axios.post(url, params) + return axios.post(url, params, config) default: - return axios.get(url, {params}) + return axios.get(url, {params, ...config}) } } diff --git a/src/utils/routerUtil.js b/src/utils/routerUtil.js index 2f46a7f..f2d04a3 100644 --- a/src/utils/routerUtil.js +++ b/src/utils/routerUtil.js @@ -32,9 +32,9 @@ function parseRoutes(routesConfig, routerMap) { routesConfig.forEach(item => { // 获取注册在 routerMap 中的 router,初始化 routeCfg let router = undefined, routeCfg = {} - if (typeof item === 'string' && routerMap[item]) { + if (typeof item === 'string') { router = routerMap[item] - routeCfg = {path: router.path || item, router: item} + routeCfg = {path: (router && router.path) || item, router: item} } else if (typeof item === 'object') { router = routerMap[item.router] routeCfg = item @@ -50,10 +50,10 @@ function parseRoutes(routesConfig, routerMap) { component: router.component, redirect: routeCfg.redirect || router.redirect, meta: { - authority: routeCfg.authority || router.authority || '*', - icon: routeCfg.icon || router.icon, - page: routeCfg.page || router.page, - link: routeCfg.link || router.link + authority: routeCfg.authority || router.authority || routeCfg.meta?.authority || router.meta?.authority || '*', + icon: routeCfg.icon || router.icon || routeCfg.meta?.icon || router.meta?.icon, + page: routeCfg.page || router.page || routeCfg.meta?.page || router.meta?.page, + link: routeCfg.link || router.link || routeCfg.meta?.link || router.meta?.link } } if (routeCfg.invisible || router.invisible) { diff --git a/vue.config.js b/vue.config.js index ba70c7d..f77ee5d 100644 --- a/vue.config.js +++ b/vue.config.js @@ -110,7 +110,7 @@ module.exports = { } } }, - publicPath: isProd ? '/vue-antd-admin/' : '/', + publicPath: process.env.VUE_APP_PUBLIC_PATH, outputDir: 'dist', assetsDir: 'static', productionSourceMap: false diff --git a/yarn.lock b/yarn.lock index a43fd90..db6f055 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1146,6 +1146,14 @@ resolved "https://registry.npm.taobao.org/@nodelib/fs.stat/download/@nodelib/fs.stat-1.1.3.tgz#2b5a3ab3f918cca48a8c754c08168e3f03eba61b" integrity sha1-K1o6s/kYzKSKjHVMCBaOPwPrphs= +"@simonwep/pickr@~1.7.0": + version "1.7.4" + resolved "https://registry.npm.taobao.org/@simonwep/pickr/download/@simonwep/pickr-1.7.4.tgz#b14fcd945890388b870cd6db4d6c78d531f25141" + integrity sha1-sU/NlFiQOIuHDNbbTWx41THyUUE= + dependencies: + core-js "^3.6.5" + nanopop "^2.1.0" + "@sindresorhus/is@^0.14.0": version "0.14.0" resolved "https://registry.npm.taobao.org/@sindresorhus/is/download/@sindresorhus/is-0.14.0.tgz#9fb3a3cf3132328151f353de4632e01e52102bea" @@ -1959,13 +1967,14 @@ ansi-styles@^4.0.0, ansi-styles@^4.1.0: "@types/color-name" "^1.1.1" color-convert "^2.0.1" -ant-design-vue@^1.6.2: - version "1.6.2" - resolved "https://registry.npm.taobao.org/ant-design-vue/download/ant-design-vue-1.6.2.tgz?cache=0&sync_timestamp=1591081225900&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fant-design-vue%2Fdownload%2Fant-design-vue-1.6.2.tgz#983634caac9cdaca0b3a095b540105b4f76da29f" - integrity sha1-mDY0yqyc2soLOglbVAEFtPdtop8= +ant-design-vue@1.7.2: + version "1.7.2" + resolved "https://registry.npm.taobao.org/ant-design-vue/download/ant-design-vue-1.7.2.tgz#aac7ff802205711631c8698e2a0c7b4e61dfd73e" + integrity sha1-qsf/gCIFcRYxyGmOKgx7TmHf1z4= dependencies: "@ant-design/icons" "^2.1.1" "@ant-design/icons-vue" "^2.0.0" + "@simonwep/pickr" "~1.7.0" add-dom-event-listener "^1.0.2" array-tree-filter "^2.1.0" async-validator "^3.0.3" @@ -6947,6 +6956,11 @@ nanomatch@^1.2.9: snapdragon "^0.8.1" to-regex "^3.0.1" +nanopop@^2.1.0: + version "2.1.0" + resolved "https://registry.npm.taobao.org/nanopop/download/nanopop-2.1.0.tgz#23476513cee2405888afd2e8a4b54066b70b9e60" + integrity sha1-I0dlE87iQFiIr9LopLVAZrcLnmA= + natural-compare@^1.4.0: version "1.4.0" resolved "https://registry.npm.taobao.org/natural-compare/download/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"