9 Commits
v1.2 ... v1.4

Author SHA1 Message Date
Ah jung
fa8b33acbe fix Bug or add example 2021-07-21 18:33:02 +08:00
啊俊
54e68db0c2 新增-系统设置-菜单权限-角色权限页面 2021-07-20 22:53:08 +08:00
Ah jung
9542345b54 版本号更新 2021-07-19 17:09:44 +08:00
Ah jung
b689fabfdd fix Bug Features CHANGELOG.ms 2021-07-19 16:42:11 +08:00
啊俊
46dc7eb69e fix Bug 2021-07-17 20:07:56 +08:00
Ah jung
c1e741dad6 fix Bug 2021-07-17 17:09:28 +08:00
Ah jung
ab4063e75e fix Bug 2021-07-17 16:34:04 +08:00
Ah jung
770d39871a fix Bug 2021-07-17 16:24:49 +08:00
Ah jung
3cb7a7f54f fix Bug or add example 2021-07-16 21:14:40 +08:00
168 changed files with 7909 additions and 4412 deletions

View File

@@ -19,5 +19,8 @@ VITE_APP_API_URL = /
# 图片上传地址 # 图片上传地址
VITE_GLOB_UPLOAD_URL= / VITE_GLOB_UPLOAD_URL= /
# 图片前缀地址
VITE_GLOB_IMG_URL= /
# 接口前缀 # 接口前缀
VITE_GLOB_API_URL_PREFIX = /api VITE_GLOB_API_URL_PREFIX = /api

View File

@@ -16,5 +16,8 @@ VITE_APP_API_URL = /
# 图片上传地址 # 图片上传地址
VITE_GLOB_UPLOAD_URL= / VITE_GLOB_UPLOAD_URL= /
# 图片前缀地址
VITE_GLOB_IMG_URL= /
# 接口前缀 # 接口前缀
VITE_GLOB_API_URL_PREFIX = /api VITE_GLOB_API_URL_PREFIX = /api

View File

@@ -1,3 +1,28 @@
# 1.4 (2021-07-21)
### 🐛 Bug Fixes
- vite降至2.3.6
- 多标签页交互优化
- ### ✨ Features
- 新增 `TableAction` 组件
- 新增 `菜单权限管理` 示例
- 新增 `角色权限管理` 示例
- 持续更新更多实用组件及示例感谢Star
# 1.3 (2021-07-19)
### 🐛 Bug Fixes
- 修复多标签页左右切换按钮自适应展示
- 修复登录页面出现多标签页
- ### ✨ Features
- 新增 `Upload` 组件及配置
- 新增 `VITE_GLOB_IMG_URL` 图片前缀地址配合Upload
- 新增 `滑块验证码` 组件
- 新增 `登录页面-滑块验证码` 示例
- 持续更新更多实用组件及示例感谢Star
# 1.2 (2021-07-16) # 1.2 (2021-07-16)
### 🐛 Bug Fixes ### 🐛 Bug Fixes
- 修复面包屑显示登录页面 - 修复面包屑显示登录页面

View File

@@ -20,10 +20,15 @@ Naive Ui Admin 是一个免费开源的中后台模版,使用了最新的`vue3
- [x] 异常页面 - [x] 异常页面
- [x] 结果页面 - [x] 结果页面
- [x] 设置页面 - [x] 设置页面
- [x] 系统设置
- [x] 菜单权限
- [x] 角色权限
### 页面组件 ### 页面组件
#### ProTable #### ProTable
- [x] 表格 - [x] 基础表格
- [x] 上传图片
- [x] 滑块验证码
- 持续开发中... - 持续开发中...
## 在线预览 ## 在线预览

93
mock/system/menu.ts Normal file
View File

@@ -0,0 +1,93 @@
import { resultSuccess } from '../_util'
const menuList = (() => {
const result: any[] = [
{
label: 'Dashboard',
key: 'dashboard',
type: 1,
subtitle:'dashboard',
openType:1,
auth:'dashboard',
path:'/dashboard',
children: [
{
label: '主控台',
key: 'console',
type: 1,
subtitle:'console',
openType:1,
auth:'console',
path:'/dashboard/console',
},
{
label: '工作台',
key: 'workplace',
type: 1,
subtitle:'workplace',
openType:1,
auth:'workplace',
path:'/dashboard/workplace',
}
]
},
{
label: '表单管理',
key: 'form',
type: 1,
subtitle:'form',
openType:1,
auth:'form',
path:'/form',
children: [
{
label: '基础表单',
key: 'basic-form',
type: 1,
subtitle:'basic-form',
openType:1,
auth:'basic-form',
path:'/form/basic-form',
},
{
label: '分步表单',
key: 'step-form',
type: 1,
subtitle:'step-form',
openType:1,
auth:'step-form',
path:'/form/step-form',
},
{
label: '表单详情',
key: 'detail',
type: 1,
subtitle:'detail',
openType:1,
auth:'detail',
path:'/form/detail',
}
]
}
]
return result
});
export default [
{
url: '/api/menu/list',
timeout: 1000,
method: 'get',
response: () => {
const list = menuList()
return resultSuccess({
list
}
);
},
}
]

50
mock/system/role.ts Normal file
View File

@@ -0,0 +1,50 @@
import { resultSuccess, doCustomTimes } from '../_util'
function getMenuKeys() {
let keys = ['dashboard', 'console', 'workplace', 'basic-form', 'step-form', 'detail']
let newKeys = []
doCustomTimes(parseInt(Math.random()*6), () => {
let key = keys[Math.floor(Math.random() * keys.length)];
newKeys.push(key)
})
return Array.from(new Set(newKeys));
}
const roleList = ((pageSize) => {
const result: any[] = []
doCustomTimes(pageSize, () => {
result.push({
id: '@integer(10,100)',
name: '@cname()',
explain: '@cname()',
isDefault: '@boolean()',
menu_keys: getMenuKeys(),
create_date: `@date('yyyy-MM-dd hh:mm:ss')`,
'status|1': ['normal', 'enable', 'disable'],
});
})
return result
});
export default [
{
url: '/api/role/list',
timeout: 1000,
method: 'get',
response: ({ query }) => {
const { page = 1, pageSize = 10 } = query;
const list = roleList(Number(pageSize))
return resultSuccess({
page: Number(page),
pageSize: Number(pageSize),
pageCount: 60,
list
}
);
},
}
]

View File

@@ -25,6 +25,14 @@ const adminInfo = {
{ {
roleName: '工作台', roleName: '工作台',
value: 'dashboard_workplace', value: 'dashboard_workplace',
},
{
roleName: '基础列表',
value: 'basic_list',
},
{
roleName: '基础列表删除',
value: 'basic_list_delete',
} }
], ],
} }

View File

@@ -1,6 +1,6 @@
{ {
"name": "naive-ui-admin", "name": "naive-ui-admin",
"version": "1.2", "version": "1.4",
"author": { "author": {
"name": "Ahjung", "name": "Ahjung",
"email": "735878602@qq.com", "email": "735878602@qq.com",
@@ -31,6 +31,7 @@
"element-resize-detector": "^1.2.3", "element-resize-detector": "^1.2.3",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"makeit-captcha": "^1.2.5",
"mitt": "^2.1.0", "mitt": "^2.1.0",
"mockjs": "^1.1.0", "mockjs": "^1.1.0",
"naive-ui": "^2.15.5", "naive-ui": "^2.15.5",
@@ -79,9 +80,9 @@
"stylelint-scss": "^3.19.0", "stylelint-scss": "^3.19.0",
"tailwindcss": "^2.2.4", "tailwindcss": "^2.2.4",
"typescript": "^4.3.2", "typescript": "^4.3.2",
"vite": "^2.4.2", "vite": "2.3.6",
"vite-plugin-html": "^2.0.7", "vite-plugin-html": "^2.0.7",
"vite-plugin-mock": "^2.9.1", "vite-plugin-mock": "^2.9.3",
"vite-plugin-style-import": "^1.0.1", "vite-plugin-style-import": "^1.0.1",
"vue-eslint-parser": "^7.8.0" "vue-eslint-parser": "^7.8.0"
}, },

View File

@@ -1,32 +1,23 @@
import http from '@/utils/http/axios' import http from '@/utils/http/axios'
import {
GetByUserIdParams,
GetMenuListByUserIdResult,
GetAuthCodeByUserIdResult
} from './model/menuModel'
enum Api {
adminMenus = '/menus',
GetBtnCodeListByUserId = '/getBtnCodeListByUserId'
}
/** /**
* @description: 根据用户id获取用户菜单 * @description: 根据用户id获取用户菜单
*/ */
export function adminMenus() { export function adminMenus() {
return http.request<GetMenuListByUserIdResult>({ return http.request({
url: Api.adminMenus, url: '/menus',
method: 'GET' method: 'GET'
}) })
} }
/** /**
* 根据用户Id获取权限编码 * 获取tree菜单列表
* @param params * @param params
*/ */
export function getBtnCodeListByUserId(params: GetByUserIdParams) { export function getMenuList(params) {
return http.request<GetAuthCodeByUserIdResult>({ return http.request({
url: Api.GetBtnCodeListByUserId, url: '/menu/list',
method: 'GET', method: 'GET',
params params
}) })

11
src/api/system/role.ts Normal file
View File

@@ -0,0 +1,11 @@
import http from '@/utils/http/axios'
/**
* @description: 角色列表
*/
export function getRoleList() {
return http.request({
url: '/role/list',
method: 'GET'
})
}

View File

@@ -1,5 +1,4 @@
import http from '@/utils/http/axios' import http from '@/utils/http/axios'
import { LoginParams, LoginResultModel } from './model/userModel'
export interface BasicResponseModel<T = any> { export interface BasicResponseModel<T = any> {
code: number code: number
@@ -28,8 +27,8 @@ export function getUserInfo() {
/** /**
* @description: 用户登录 * @description: 用户登录
*/ */
export function login(params: LoginParams) { export function login(params) {
return http.request<BasicResponseModel<LoginResultModel>>( return http.request<BasicResponseModel>(
{ {
url: '/login', url: '/login',
method: 'POST', method: 'POST',

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 20 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 32 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 34 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 26 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 33 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 21 KiB

View File

@@ -1 +0,0 @@
export { default as ProTable } from './src/ProTable.vue';

View File

@@ -1,95 +0,0 @@
import { ref, Ref, ComputedRef, unref, computed, watch, toRaw } from 'vue';
import type { BasicColumn, BasicTableProps } from '../types/table';
import { isEqual, cloneDeep } from 'lodash-es';
import { isArray, isString } from '@/utils/is';
export function useColumns(propsRef: ComputedRef<BasicTableProps>) {
const columnsRef = ref(unref(propsRef).columns) as unknown as Ref<BasicColumn[]>;
let cacheColumns = unref(propsRef).columns;
const getColumnsRef = computed(() => {
const columns = cloneDeep(unref(columnsRef));
return columns;
})
const getPageColumns = computed(() => {
const pageColumns = unref(getColumnsRef);
const columns = cloneDeep(pageColumns);
return columns
})
watch(
() => unref(propsRef).columns,
(columns) => {
columnsRef.value = columns;
cacheColumns = columns;
}
);
//设置
function setColumns(columnList: string[]) {
const columns: any[] = cloneDeep(columnList);
if (!isArray(columns)) return;
if (!columns.length) {
columnsRef.value = [];
return;
}
const cacheKeys = cacheColumns.map((item) => item.key);
//针对拖拽排序
if (!isString(columns[0])) {
columnsRef.value = columns;
} else {
const newColumns: any[] = []
cacheColumns.forEach(item => {
if (columnList.includes(item.key)) {
newColumns.push({ ...item })
}
})
if (!isEqual(cacheKeys, columns)) {
newColumns.sort((prev, next) => {
return (
cacheKeys.indexOf(prev.key) - cacheKeys.indexOf(next.key)
);
});
}
columnsRef.value = newColumns
}
}
//获取
function getColumns() {
let columns = toRaw(unref(getColumnsRef));
return columns.map(item => {
return { ...item, title: item.title, key: item.key, fixed: item.fixed || undefined }
})
}
//获取原始
function getCacheColumns(isKey?: boolean): any[] {
return isKey ? cacheColumns.map(item => item.key) : cacheColumns;
}
//更新原始数据单个字段
function setCacheColumnsField(dataIndex: string | undefined, value: Partial<BasicColumn>) {
if (!dataIndex || !value) {
return;
}
cacheColumns.forEach((item) => {
if (item.key === dataIndex) {
Object.assign(item, value);
return;
}
});
}
return {
getColumnsRef,
getCacheColumns,
setCacheColumnsField,
setColumns,
getColumns,
getPageColumns
};
}

View File

@@ -1,141 +0,0 @@
import { ref, ComputedRef, unref, computed, onMounted, watchEffect, watch } from 'vue';
import type { BasicTableProps } from '../types/table';
import type { PaginationProps } from '../types/pagination';
import { isBoolean } from '@/utils/is';
import { APISETTING } from '../const';
export function useDataSource(
propsRef: ComputedRef<BasicTableProps>,
{
getPaginationInfo,
setPagination,
setLoading,
tableData
},
emit
) {
const dataSourceRef = ref([]);
watchEffect(() => {
tableData.value = unref(dataSourceRef);
});
watch(
() => unref(propsRef).dataSource,
() => {
const { dataSource }: any = unref(propsRef);
dataSource && (dataSourceRef.value = dataSource);
},
{
immediate: true,
}
);
const getRowKey = computed(() => {
const { rowKey }:any = unref(propsRef);
return rowKey ? rowKey : () => {
return 'key'
};
});
const getDataSourceRef = computed(() => {
const dataSource = unref(dataSourceRef);
if (!dataSource || dataSource.length === 0) {
return unref(dataSourceRef);
}
return unref(dataSourceRef);
});
async function fetch(opt?) {
try {
setLoading(true);
const { request, pagination }: any = unref(propsRef);
//组装分页信息
const pageField = APISETTING.pageField
const sizeField = APISETTING.sizeField
const totalField = APISETTING.totalField
const listField = APISETTING.listField
let pageParams = {};
const { page = 1, pageSize = 10 } = unref(getPaginationInfo) as PaginationProps;
if ((isBoolean(pagination) && !pagination) || isBoolean(getPaginationInfo)) {
pageParams = {};
} else {
pageParams[pageField] = (opt && opt[pageField]) || page;
pageParams[sizeField] = pageSize;
}
let params = {
...pageParams,
}
const res = await request(params);
const resultTotal = res[totalField] || 0
const currentPage = res[pageField]
// 如果数据异常,需获取正确的页码再次执行
if (resultTotal) {
const currentTotalPage = Math.ceil(resultTotal / pageSize);
if (page > currentTotalPage) {
setPagination({
[pageField]: currentTotalPage,
});
fetch(opt);
}
}
let resultInfo = res[listField] ? res[listField] : []
dataSourceRef.value = resultInfo;
setPagination({
[pageField]: currentPage,
[totalField]: resultTotal,
});
if (opt && opt[pageField]) {
setPagination({
[pageField]: opt[pageField] || 1,
});
}
emit('fetch-success', {
items: unref(resultInfo),
resultTotal
});
} catch (error) {
console.error(error)
emit('fetch-error', error);
dataSourceRef.value = [];
// setPagination({
// pageCount: 0,
// });
} finally {
setLoading(false);
}
}
onMounted(() => {
setTimeout(() => {
fetch();
}, 16)
});
function setTableData(values) {
dataSourceRef.value = values;
}
function getDataSource() :any[] {
return getDataSourceRef.value;
}
async function reload(opt?) {
await fetch(opt);
}
return {
fetch,
getRowKey,
getDataSourceRef,
getDataSource,
setTableData,
reload
}
}

View File

@@ -1,21 +0,0 @@
import { ref, ComputedRef, unref, computed, watch } from 'vue';
import type { BasicTableProps } from '../types/table';
export function useLoading(props: ComputedRef<BasicTableProps>) {
const loadingRef = ref(unref(props).loading);
watch(
() => unref(props).loading,
(loading) => {
loadingRef.value = loading;
}
);
const getLoading = computed(() => unref(loadingRef));
function setLoading(loading: boolean) {
loadingRef.value = loading;
}
return { getLoading, setLoading };
}

View File

@@ -1,48 +0,0 @@
import type { PaginationProps } from '../types/pagination';
import type { BasicTableProps } from '../types/table';
import { computed, unref, ref, ComputedRef } from 'vue';
import { isBoolean } from '@/utils/is';
import { DEFAULTPAGESIZE, PAGESIZES } from '../const';
export function usePagination(refProps: ComputedRef<BasicTableProps>) {
const configRef = ref<PaginationProps>({});
const show = ref(true);
const getPaginationInfo = computed((): PaginationProps | boolean => {
const { pagination } = unref(refProps);
if (!unref(show) || (isBoolean(pagination) && !pagination)) {
return false;
}
return {
pageSize: DEFAULTPAGESIZE,
pageSizes: PAGESIZES,
showSizePicker: true,
showQuickJumper: true,
...(isBoolean(pagination) ? {} : pagination),
...unref(configRef),
};
});
function setPagination(info: Partial<PaginationProps>) {
const paginationInfo = unref(getPaginationInfo);
configRef.value = {
...(!isBoolean(paginationInfo) ? paginationInfo : {}),
...info,
};
}
function getPagination() {
return unref(getPaginationInfo);
}
function getShowPagination() {
return unref(show);
}
async function setShowPagination(flag: boolean) {
show.value = flag;
}
return { getPagination, getPaginationInfo, setShowPagination, getShowPagination, setPagination };
}

View File

@@ -1,45 +0,0 @@
import type { PropType } from 'vue'
import { BasicColumn } from './types/table'
export const basicProps = {
title: {
type: String,
default: null,
},
titleTooltip: {
type: String,
default: null,
},
size: {
type: String,
default: 'medium',
},
tableData: {
type: [Object],
default: () => {
},
},
columns: {
type: [Array] as PropType<BasicColumn[]>,
default: () => [],
required: true,
},
request: {
type: Function as PropType<(...arg: any[]) => Promise<any>>,
default: null,
required: true
},
rowKey: {
type: [String, Function] as PropType<string | ((record) => string)>,
default: undefined,
},
pagination: {
type: [Object, Boolean],
default: () => {
}
},
showPagination: {
type: [String, Boolean],
default: 'auto'
}
}

View File

@@ -1,11 +0,0 @@
import Pagination from 'naive-ui/lib/pagination';
import { VNodeChild } from 'vue';
export interface PaginationProps {
page?: number;
pageCount?: number,
pageSize?: number,
pageSizes?: number[],
showSizePicker?: boolean,
showQuickJumper?: boolean,
}

View File

@@ -1,22 +0,0 @@
import type {
TableBaseColumn,
} from 'naive-ui/lib/data-table/src/interface';
export interface BasicColumn extends TableBaseColumn {
}
export interface TableActionType {
reload: (opt) => Promise<void>;
emit?: any;
getColumns: (opt) => BasicColumn[];
setColumns: (columns: BasicColumn[] | string[]) => void;
}
export interface BasicTableProps<T = any> {
title?: string,
dataSource: Function,
columns: any[],
pagination: object,
showPagination: boolean
}

View File

@@ -1,4 +1,4 @@
ProTable 重封装组件说明 BasicTable 重封装组件说明
==== ====
封装说明 封装说明
@@ -6,7 +6,7 @@ ProTable 重封装组件说明
> 基础的使用方式与 API 与 [官方版(data-table)](https://www.naiveui.com/zh-CN/os-theme/components/data-table#tree) 本一致,在其基础上,封装了加载数据的方法。 > 基础的使用方式与 API 与 [官方版(data-table)](https://www.naiveui.com/zh-CN/os-theme/components/data-table#tree) 本一致,在其基础上,封装了加载数据的方法。
> >
> 你无需在你是用表格的页面进行分页逻辑处理,仅需向 ProTable 组件传递绑定 `:api="Promise"` 对象即可 > 你无需在你是用表格的页面进行分页逻辑处理,仅需向 BasicTable 组件传递绑定 `:api="Promise"` 对象即可
> >
> 例子1 > 例子1
---- ----
@@ -15,7 +15,7 @@ ProTable 重封装组件说明
```vue ```vue
<template> <template>
<ProTable <BasicTable
title="表格列表" title="表格列表"
:columns="columns" :columns="columns"
:api="loadDataTable" :api="loadDataTable"
@@ -25,12 +25,12 @@ ProTable 重封装组件说明
<template #toolbar> <template #toolbar>
<n-button type="primary">添加会员</n-button> <n-button type="primary">添加会员</n-button>
</template> </template>
</ProTable> </BasicTable>
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent } from 'vue' import { defineComponent } from 'vue'
import { ProTable } from '@/components/ProTable' import { BasicTable } from '@/components/Table'
import { getTableList } from '@/api/table/list' import { getTableList } from '@/api/table/list'
const columns = [ const columns = [
{ {
@@ -43,7 +43,11 @@ const columns = [
}, },
{ {
title: '地址', title: '地址',
key: 'address' key: 'address',
auth: ['amdin'], // 同时根据权限控制是否显示
ifShow: (row) => {
return true; // 根据业务控制是否显示
},
}, },
{ {
title: '日期', title: '日期',
@@ -51,7 +55,7 @@ const columns = [
}, },
] ]
export default defineComponent({ export default defineComponent({
components: { ProTable }, components: { BasicTable },
setup() { setup() {
const loadDataTable = async (params) => { const loadDataTable = async (params) => {
const data = await getTableList(params); const data = await getTableList(params);
@@ -68,7 +72,7 @@ export default defineComponent({
API API
---- ----
ProTable 在 NaiveUi 的 data-table 上进行了一层封装,支持了一些预设,并且封装了一些行为。这里只列出与 data-table 不同的 api。 BasicTable 在 NaiveUi 的 data-table 上进行了一层封装,支持了一些预设,并且封装了一些行为。这里只列出与 data-table 不同的 api。
> requestPromise 参考上面例子写法 > requestPromise 参考上面例子写法
> ref可绑定ref 调用组件内部方法data-table本身的方法和参数 > ref可绑定ref 调用组件内部方法data-table本身的方法和参数

View File

@@ -0,0 +1,5 @@
export { default as BasicTable } from './src/Table.vue';
export { default as TableAction } from './src/components/TableAction.vue';
export * from './src/types/table';
export * from './src/types/tableAction';

View File

@@ -282,6 +282,7 @@ export default defineComponent({
font-size: 16px; font-size: 16px;
cursor: pointer; cursor: pointer;
color: var(--text-color); color: var(--text-color);
:hover { :hover {
color: #1890ff; color: #1890ff;
} }

View File

@@ -0,0 +1,145 @@
<template>
<div class="tableAction">
<div class="flex items-center justify-center">
<template v-for="(action, index) in getActions" :key="`${index}-${action.label}`">
<n-button v-bind="action" class="mx-2">{{ action.label }}</n-button>
</template>
<n-dropdown
v-if="dropDownActions && getDropdownList.length"
trigger="hover"
:options="getDropdownList"
@select="select"
>
<slot name="more"></slot>
<n-button v-bind="getMoreProps" class="mx-2" v-if="!$slots.more" icon-placement="right">
<div class="flex items-center">
<span>更多</span>
<n-icon size="14" class="ml-1">
<DownOutlined/>
</n-icon>
</div>
<!-- <template #icon>-->
<!-- -->
<!-- </template>-->
</n-button>
</n-dropdown>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent, PropType, computed, toRaw } from 'vue';
import { ActionItem } from '@/components/Table';
import { usePermission } from '@/hooks/web/usePermission';
import { isString, isBoolean, isFunction } from "@/utils/is";
import { DownOutlined } from '@vicons/antd'
export default defineComponent({
name: 'TableAction',
components: { DownOutlined },
props: {
actions: {
type: Array as PropType<ActionItem[]>,
default: null,
},
dropDownActions: {
type: Array as PropType<ActionItem[]>,
default: null,
},
style: {
type: String as PropType<String>,
default: 'button'
},
select:{
type: Function as PropType<Function>,
default: () =>{ }
}
},
setup(props, { emit }) {
const { hasPermission } = usePermission();
const getTooltip = computed(() => {
return (data: string | TooltipProps): TooltipProps => {
if (isString(data)) {
return { title: data, placement: 'bottom' };
} else {
return Object.assign({ placement: 'bottom' }, data);
}
};
});
const actionType = props.style === 'button' ? 'default' : props.style === 'text' ? 'primary' : 'default'
const actionText = props.style === 'button' ? undefined : props.style === 'text' ? true : undefined
const getMoreProps = computed(() => {
return {
text: actionText,
type: actionType,
size: "small"
}
})
const getDropdownList = computed(() => {
return (toRaw(props.dropDownActions) || [])
.filter((action) => {
return hasPermission(action.auth) && isIfShow(action);
})
.map((action, index) => {
const { label, popConfirm } = action;
return {
size: 'small',
text: actionText,
type: actionType,
...action,
...popConfirm,
onConfirm: popConfirm?.confirm,
onCancel: popConfirm?.cancel
};
});
});
function isIfShow(action: ActionItem): boolean {
const ifShow = action.ifShow;
let isIfShow = true;
if (isBoolean(ifShow)) {
isIfShow = ifShow;
}
if (isFunction(ifShow)) {
isIfShow = ifShow(action);
}
return isIfShow;
}
const getActions = computed(() => {
return (toRaw(props.actions) || [])
.filter((action) => {
return hasPermission(action.auth) && isIfShow(action);
})
.map((action) => {
const { popConfirm } = action;
//需要展示什么风格,自己修改一下参数
return {
size: 'small',
text: actionText,
type: actionType,
...action,
...(popConfirm || {}),
onConfirm: popConfirm?.confirm,
onCancel: popConfirm?.cancel,
enable: !!popConfirm,
};
});
});
return {
getActions,
getDropdownList,
getTooltip,
getMoreProps
}
}
})
</script>

View File

@@ -21,7 +21,8 @@
<n-checkbox-group v-model:value="checkList" @update:value="onChange"> <n-checkbox-group v-model:value="checkList" @update:value="onChange">
<Draggable v-model="columnsList" animation="300" item-key="key" @end="draggableEnd"> <Draggable v-model="columnsList" animation="300" item-key="key" @end="draggableEnd">
<template #item="{element, index}"> <template #item="{element, index}">
<div class="table-toolbar-inner-checkbox" :class="{'table-toolbar-inner-checkbox-dark':getDarkTheme === true}"> <div class="table-toolbar-inner-checkbox"
:class="{'table-toolbar-inner-checkbox-dark':getDarkTheme === true}">
<span class="drag-icon"> <span class="drag-icon">
<n-icon size="18"> <n-icon size="18">
<DragOutlined/> <DragOutlined/>
@@ -220,12 +221,14 @@ export default defineComponent({
&-inner-popover-title { &-inner-popover-title {
padding: 3px 0; padding: 3px 0;
} }
&-right { &-right {
&-icon { &-icon {
margin-left: 12px; margin-left: 12px;
font-size: 16px; font-size: 16px;
color: var(--text-color); color: var(--text-color);
cursor: pointer; cursor: pointer;
:hover { :hover {
color: #1890ff; color: #1890ff;
} }
@@ -238,14 +241,17 @@ export default defineComponent({
display: flex; display: flex;
align-items: center; align-items: center;
padding: 10px 14px; padding: 10px 14px;
&:hover { &:hover {
background: #e6f7ff; background: #e6f7ff;
} }
.drag-icon { .drag-icon {
display: inline-flex; display: inline-flex;
margin-right: 8px; margin-right: 8px;
cursor: move; cursor: move;
} }
.fixed-item { .fixed-item {
display: flex; display: flex;
align-items: center; align-items: center;
@@ -261,12 +267,14 @@ export default defineComponent({
} }
} }
} }
&-checkbox-dark { &-checkbox-dark {
&:hover { &:hover {
background: hsla(0, 0%, 100%, .08); background: hsla(0, 0%, 100%, .08);
} }
} }
} }
.toolbar-popover { .toolbar-popover {
.n-popover__content { .n-popover__content {
padding: 0; padding: 0;

View File

@@ -0,0 +1,126 @@
import { ref, Ref, ComputedRef, unref, computed, watch, toRaw } from 'vue';
import type { BasicColumn, BasicTableProps } from '../types/table';
import { isEqual, cloneDeep } from 'lodash-es';
import { isArray, isString } from '@/utils/is';
import { usePermission } from '@/hooks/web/usePermission';
import { isString, isBoolean, isFunction } from "@/utils/is";
import { ActionItem } from "@/components/Table";
export function useColumns(propsRef: ComputedRef<BasicTableProps>) {
const columnsRef = ref(unref(propsRef).columns) as unknown as Ref<BasicColumn[]>;
let cacheColumns = unref(propsRef).columns;
const getColumnsRef = computed(() => {
const columns = cloneDeep(unref(columnsRef));
handleActionColumn(propsRef, columns);
if (!columns) return [];
return columns;
})
const { hasPermission } = usePermission();
function isIfShow(action: ActionItem): boolean {
const ifShow = action.ifShow;
let isIfShow = true;
if (isBoolean(ifShow)) {
isIfShow = ifShow;
}
if (isFunction(ifShow)) {
isIfShow = ifShow(action);
}
return isIfShow;
}
const getPageColumns = computed(() => {
const pageColumns = unref(getColumnsRef);
const columns = cloneDeep(pageColumns);
return columns.filter((column) => {
return hasPermission(column.auth) && isIfShow(column);
})
})
watch(
() => unref(propsRef).columns,
(columns) => {
columnsRef.value = columns;
cacheColumns = columns;
}
);
function handleActionColumn(propsRef: ComputedRef<BasicTableProps>, columns: BasicColumn[]) {
const { actionColumn } = unref(propsRef);
if (!actionColumn) return;
columns.push({
...actionColumn
});
}
//设置
function setColumns(columnList: string[]) {
const columns: any[] = cloneDeep(columnList);
if (!isArray(columns)) return;
if (!columns.length) {
columnsRef.value = [];
return;
}
const cacheKeys = cacheColumns.map((item) => item.key);
//针对拖拽排序
if (!isString(columns[0])) {
columnsRef.value = columns;
} else {
const newColumns: any[] = []
cacheColumns.forEach(item => {
if (columnList.includes(item.key)) {
newColumns.push({ ...item })
}
})
if (!isEqual(cacheKeys, columns)) {
newColumns.sort((prev, next) => {
return (
cacheKeys.indexOf(prev.key) - cacheKeys.indexOf(next.key)
);
});
}
columnsRef.value = newColumns
}
}
//获取
function getColumns() {
let columns = toRaw(unref(getColumnsRef));
return columns.map(item => {
return { ...item, title: item.title, key: item.key, fixed: item.fixed || undefined }
})
}
//获取原始
function getCacheColumns(isKey?: boolean): any[] {
return isKey ? cacheColumns.map(item => item.key) : cacheColumns;
}
//更新原始数据单个字段
function setCacheColumnsField(dataIndex: string | undefined, value: Partial<BasicColumn>) {
if (!dataIndex || !value) {
return;
}
cacheColumns.forEach((item) => {
if (item.key === dataIndex) {
Object.assign(item, value);
return;
}
});
}
return {
getColumnsRef,
getCacheColumns,
setCacheColumnsField,
setColumns,
getColumns,
getPageColumns
};
}

View File

@@ -0,0 +1,141 @@
import { ref, ComputedRef, unref, computed, onMounted, watchEffect, watch } from 'vue';
import type { BasicTableProps } from '../types/table';
import type { PaginationProps } from '../types/pagination';
import { isBoolean } from '@/utils/is';
import { APISETTING } from '../const';
export function useDataSource(
propsRef: ComputedRef<BasicTableProps>,
{
getPaginationInfo,
setPagination,
setLoading,
tableData
},
emit
) {
const dataSourceRef = ref([]);
watchEffect(() => {
tableData.value = unref(dataSourceRef);
});
watch(
() => unref(propsRef).dataSource,
() => {
const { dataSource }: any = unref(propsRef);
dataSource && (dataSourceRef.value = dataSource);
},
{
immediate: true,
}
);
const getRowKey = computed(() => {
const { rowKey }: any = unref(propsRef);
return rowKey ? rowKey : () => {
return 'key'
};
});
const getDataSourceRef = computed(() => {
const dataSource = unref(dataSourceRef);
if (!dataSource || dataSource.length === 0) {
return unref(dataSourceRef);
}
return unref(dataSourceRef);
});
async function fetch(opt?) {
try {
setLoading(true);
const { request, pagination }: any = unref(propsRef);
//组装分页信息
const pageField = APISETTING.pageField
const sizeField = APISETTING.sizeField
const totalField = APISETTING.totalField
const listField = APISETTING.listField
let pageParams = {};
const { page = 1, pageSize = 10 } = unref(getPaginationInfo) as PaginationProps;
if ((isBoolean(pagination) && !pagination) || isBoolean(getPaginationInfo)) {
pageParams = {};
} else {
pageParams[pageField] = (opt && opt[pageField]) || page;
pageParams[sizeField] = pageSize;
}
let params = {
...pageParams,
}
const res = await request(params);
const resultTotal = res[totalField] || 0
const currentPage = res[pageField]
// 如果数据异常,需获取正确的页码再次执行
if (resultTotal) {
const currentTotalPage = Math.ceil(resultTotal / pageSize);
if (page > currentTotalPage) {
setPagination({
[pageField]: currentTotalPage,
});
fetch(opt);
}
}
let resultInfo = res[listField] ? res[listField] : []
dataSourceRef.value = resultInfo;
setPagination({
[pageField]: currentPage,
[totalField]: resultTotal,
});
if (opt && opt[pageField]) {
setPagination({
[pageField]: opt[pageField] || 1,
});
}
emit('fetch-success', {
items: unref(resultInfo),
resultTotal
});
} catch (error) {
console.error(error)
emit('fetch-error', error);
dataSourceRef.value = [];
// setPagination({
// pageCount: 0,
// });
} finally {
setLoading(false);
}
}
onMounted(() => {
setTimeout(() => {
fetch();
}, 16)
});
function setTableData(values) {
dataSourceRef.value = values;
}
function getDataSource(): any[] {
return getDataSourceRef.value;
}
async function reload(opt?) {
await fetch(opt);
}
return {
fetch,
getRowKey,
getDataSourceRef,
getDataSource,
setTableData,
reload
}
}

View File

@@ -0,0 +1,21 @@
import { ref, ComputedRef, unref, computed, watch } from 'vue';
import type { BasicTableProps } from '../types/table';
export function useLoading(props: ComputedRef<BasicTableProps>) {
const loadingRef = ref(unref(props).loading);
watch(
() => unref(props).loading,
(loading) => {
loadingRef.value = loading;
}
);
const getLoading = computed(() => unref(loadingRef));
function setLoading(loading: boolean) {
loadingRef.value = loading;
}
return { getLoading, setLoading };
}

View File

@@ -0,0 +1,48 @@
import type { PaginationProps } from '../types/pagination';
import type { BasicTableProps } from '../types/table';
import { computed, unref, ref, ComputedRef } from 'vue';
import { isBoolean } from '@/utils/is';
import { DEFAULTPAGESIZE, PAGESIZES } from '../const';
export function usePagination(refProps: ComputedRef<BasicTableProps>) {
const configRef = ref<PaginationProps>({});
const show = ref(true);
const getPaginationInfo = computed((): PaginationProps | boolean => {
const { pagination } = unref(refProps);
if (!unref(show) || (isBoolean(pagination) && !pagination)) {
return false;
}
return {
pageSize: DEFAULTPAGESIZE,
pageSizes: PAGESIZES,
showSizePicker: true,
showQuickJumper: true,
...(isBoolean(pagination) ? {} : pagination),
...unref(configRef),
};
});
function setPagination(info: Partial<PaginationProps>) {
const paginationInfo = unref(getPaginationInfo);
configRef.value = {
...(!isBoolean(paginationInfo) ? paginationInfo : {}),
...info,
};
}
function getPagination() {
return unref(getPaginationInfo);
}
function getShowPagination() {
return unref(show);
}
async function setShowPagination(flag: boolean) {
show.value = flag;
}
return { getPagination, getPaginationInfo, setShowPagination, getShowPagination, setPagination };
}

View File

@@ -0,0 +1,49 @@
import type { PropType } from 'vue'
import { BasicColumn } from './types/table'
export const basicProps = {
title: {
type: String,
default: null,
},
titleTooltip: {
type: String,
default: null,
},
size: {
type: String,
default: 'medium',
},
tableData: {
type: [Object],
default: () => {
},
},
columns: {
type: [Array] as PropType<BasicColumn[]>,
default: () => [],
required: true,
},
request: {
type: Function as PropType<(...arg: any[]) => Promise<any>>,
default: null,
required: true
},
rowKey: {
type: [String, Function] as PropType<string | ((record) => string)>,
default: undefined,
},
pagination: {
type: [Object, Boolean],
default: () => {
}
},
showPagination: {
type: [String, Boolean],
default: 'auto'
},
actionColumn: {
type: Object as PropType<BasicColumn>,
default: null,
},
}

View File

@@ -0,0 +1,11 @@
import Pagination from 'naive-ui/lib/pagination';
import { VNodeChild } from 'vue';
export interface PaginationProps {
page?: number;
pageCount?: number,
pageSize?: number,
pageSizes?: number[],
showSizePicker?: boolean,
showQuickJumper?: boolean,
}

View File

@@ -0,0 +1,22 @@
import type {
TableBaseColumn,
} from 'naive-ui/lib/data-table/src/interface';
export interface BasicColumn extends TableBaseColumn {
}
export interface TableActionType {
reload: (opt) => Promise<void>;
emit?: any;
getColumns: (opt) => BasicColumn[];
setColumns: (columns: BasicColumn[] | string[]) => void;
}
export interface BasicTableProps<T = any> {
title?: string,
dataSource: Function,
columns: any[],
pagination: object,
showPagination: boolean
}

View File

@@ -0,0 +1,26 @@
import { NButton, NTooltip } from 'naive-ui';
import { RoleEnum } from '/@/enums/roleEnum';
export interface ActionItem extends NButton.props {
onClick?: Fn;
label?: string;
color?: 'success' | 'error' | 'warning';
icon?: string;
popConfirm?: PopConfirm;
disabled?: boolean;
divider?: boolean;
// 权限编码控制是否显示
auth?: RoleEnum | RoleEnum[] | string | string[];
// 业务控制是否显示
ifShow?: boolean | ((action: ActionItem) => boolean);
tooltip?: string | TooltipProps;
}
export interface PopConfirm {
title: string;
okText?: string;
cancelText?: string;
confirm: Fn;
cancel?: Fn;
icon?: string;
}

View File

@@ -0,0 +1 @@
export { default as BasicUpload } from './src/BasicUpload.vue';

View File

@@ -0,0 +1,313 @@
<template>
<div class="w-full">
<div class="upload">
<div class="upload-card">
<!--图片列表-->
<div class="upload-card-item" :style="getCSSProperties" v-for="(item,index) in imgList">
<div class="upload-card-item-info">
<div class="img-box">
<img :src="item"/>
</div>
<div class="img-box-actions">
<n-icon size="18" class="action-icon mx-2" @click="preview(item)">
<EyeOutlined/>
</n-icon>
<n-icon size="18" class="action-icon mx-2" @click="remove(index)">
<DeleteOutlined/>
</n-icon>
</div>
</div>
</div>
<!--上传图片-->
<div class="upload-card-item upload-card-item-select-picture" :style="getCSSProperties"
v-if="imgList.length < maxNumber">
<n-upload
v-bind="$props"
:file-list-style="{ display:'none'}"
@before-upload="beforeUpload"
@finish="finish"
>
<div class="flex justify-center flex-col">
<n-icon size="18" class="m-auto">
<PlusOutlined/>
</n-icon>
<span class="upload-title">上传图片</span>
</div>
</n-upload>
</div>
</div>
</div>
<!--上传图片-->
<n-space>
<n-alert title="提示" type="info" v-if="helpText" class="flex w-full">
{{ helpText }}
</n-alert>
</n-space>
</div>
<!--预览图片-->
<n-modal
v-model:show="showModal"
preset="card"
title="预览"
:bordered="false"
:style="{width: '520px'}"
>
<img :src="previewUrl"/>
</n-modal>
</template>
<script lang="ts">
import { defineComponent, toRefs, reactive, computed } from "vue";
import { EyeOutlined, DeleteOutlined, PlusOutlined } from "@vicons/antd";
import { NUpload } from 'naive-ui'
import { basicProps } from "./props";
import { useMessage, useDialog } from 'naive-ui'
import { ResultEnum } from '@/enums/httpEnum'
import componentSetting from '@/settings/componentSetting'
import { useGlobSetting } from '@/hooks/setting'
import { isString } from '@/utils/is'
const globSetting = useGlobSetting()
export default defineComponent({
name: 'BasicUpload',
components: { EyeOutlined, DeleteOutlined, PlusOutlined },
props: {
...NUpload.props, // 这里继承原 UI 组件的 props
...basicProps
},
emits: ['uploadChange', 'delete'],
setup(props, { emit }) {
const { value, width, height } = props
const getCSSProperties = computed(() => {
return {
width: `${ width }px`,
height: `${ height }px`,
}
})
const message = useMessage()
const dialog = useDialog()
const state = reactive({
showModal: false,
previewUrl: '',
originalImgList: [],
imgList: []
})
//赋值默认图片显示
if (value.length) {
state.imgList = value.map(item => {
return getImgUrl(item)
})
}
//预览
function preview(url: string) {
state.showModal = true
state.previewUrl = url
}
//删除
function remove(index: number) {
dialog.info({
title: '提示',
content: '你确定要删除吗?',
positiveText: '确定',
negativeText: '取消',
onPositiveClick: () => {
state.imgList.splice(index, 1)
state.originalImgList.splice(index, 1)
emit('uploadChange', state.originalImgList)
emit('delete', state.originalImgList)
},
onNegativeClick: () => {
}
})
}
//组装完整图片地址
function getImgUrl(url: string) {
const { imgUrl } = globSetting
return (/(^http|https:\/\/)/g).test(url) ? url : `${ imgUrl }${ url }`
}
function checkFileType(fileType: string) {
return componentSetting.upload.fileType.includes(fileType)
}
//上传之前
function beforeUpload({ file, fileList }) {
const fileInfo = file.file;
const { maxSize, accept, maxNumber } = props;
const acceptRef = (isString(accept) && accept.split(',')) || [];
// 设置最大值,则判断
if (maxSize && fileInfo.size / 1024 / 1024 >= maxSize) {
message.error(`上传文件最大值不能超过${ maxSize }M`);
return false;
}
// 设置类型,则判断
const fileType = componentSetting.upload.fileType
if (acceptRef.length > 0 && !checkFileType(fileInfo.type)) {
message.error(`只能上传文件类型为${ fileType.join(',') }`);
return false;
}
return true
}
//上传结束
function finish({ event: Event }) {
const res = eval('(' + Event.target.response + ')');
const infoField = componentSetting.upload.apiSetting.infoField
const { code } = res
const message = (res.msg || res.message) || '上传失败'
const result = res[infoField]
//成功
if (code === ResultEnum.SUCCESS) {
let imgUrl: string = getImgUrl(result.photo)
state.imgList.push(imgUrl)
state.originalImgList.push(result.photo)
emit('uploadChange', state.originalImgList)
} else message.error(message)
}
return {
...toRefs(state),
finish,
preview,
remove,
beforeUpload,
getCSSProperties
}
}
})
</script>
<style lang="less">
.upload {
width: 100%;
overflow: hidden;
&-card {
width: auto;
height: auto;
display: flex;
flex-wrap: wrap;
align-items: center;
&-item {
margin: 0 8px 8px 0;
position: relative;
padding: 8px;
border: 1px solid #d9d9d9;
border-radius: 2px;
display: flex;
justify-content: center;
flex-direction: column;
align-items: center;
&:hover {
background: 0 0;
&-info::before {
opacity: 1
}
}
&-info {
position: relative;
height: 100%;
padding: 0;
overflow: hidden;
&:hover {
.img-box-actions {
opacity: 1;
}
}
&::before {
position: absolute;
z-index: 1;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, .5);
opacity: 0;
transition: all .3s;
content: ' ';
}
.img-box {
position: relative;
//padding: 8px;
//border: 1px solid #d9d9d9;
border-radius: 2px;
}
.img-box-actions {
position: absolute;
top: 50%;
left: 50%;
z-index: 10;
white-space: nowrap;
transform: translate(-50%, -50%);
opacity: 0;
transition: all .3s;
display: flex;
align-items: center;
justify-content: space-between;
&:hover {
background: 0 0;
}
.action-icon {
color: rgba(255, 255, 255, .85);
&:hover {
cursor: pointer;
color: #fff
}
}
}
}
}
&-item-select-picture {
border: 1px dashed #d9d9d9;
border-radius: 2px;
cursor: pointer;
background: #fafafa;
color: #666;
.upload-title {
color: #666;
}
}
&-item:hover {
background: 0 0;
.upload-card-item-info::before {
opacity: 1
}
}
}
}
</style>

View File

@@ -0,0 +1,34 @@
import type { PropType } from 'vue'
import { NUpload } from 'naive-ui'
export const basicProps = {
...NUpload.props,
accept: {
type: String,
default: '.jpg,.png,.jpeg,.svg,.gif',
},
helpText: {
type: String as PropType<string>,
default: '',
},
maxSize: {
type: Number as PropType<number>,
default: 2
},
maxNumber: {
type: Number as PropType<number>,
default: Infinity
},
value: {
type: Array as PropType<string[]>,
default: () => []
},
width: {
type: Number as PropType<number>,
default: 104
},
height: {
type: Number as PropType<number>,
default: 104 //建议不小于这个尺寸 太小页面可能显示有异常
},
}

View File

@@ -0,0 +1,7 @@
export interface BasicProps<T = any> {
title?: string,
dataSource: Function,
columns: any[],
pagination: object,
showPagination: boolean
}

View File

@@ -1,10 +1,14 @@
export enum PageEnum { export enum PageEnum {
// 登录 // 登录
BASE_LOGIN = '/login', BASE_LOGIN = '/login',
BASE_LOGIN_NAME = 'Login',
//重定向 //重定向
REDIRECT = '/Redirect', REDIRECT = '/redirect',
REDIRECT_NAME = 'Redirect',
// 首页 // 首页
BASE_HOME = '/dashboard', BASE_HOME = '/dashboard',
//首页跳转默认路由
BASE_HOME_REDIRECT = '/dashboard/console',
// 错误 // 错误
ERROR_PAGE = '/exception', ERROR_PAGE_NAME = 'ErrorPage',
} }

View File

@@ -10,8 +10,8 @@ export const useGlobSetting = (): Readonly<GlobConfig> => {
VITE_GLOB_APP_SHORT_NAME, VITE_GLOB_APP_SHORT_NAME,
VITE_GLOB_API_URL_PREFIX, VITE_GLOB_API_URL_PREFIX,
VITE_GLOB_UPLOAD_URL, VITE_GLOB_UPLOAD_URL,
VITE_GLOB_PERMISSION_MODE, VITE_GLOB_PROD_MOCK,
VITE_GLOB_PROD_MOCK VITE_GLOB_IMG_URL
} = getAppEnvConfig(); } = getAppEnvConfig();
if (!/[a-zA-Z\_]*/.test(VITE_GLOB_APP_SHORT_NAME)) { if (!/[a-zA-Z\_]*/.test(VITE_GLOB_APP_SHORT_NAME)) {
@@ -27,8 +27,9 @@ export const useGlobSetting = (): Readonly<GlobConfig> => {
shortName: VITE_GLOB_APP_SHORT_NAME, shortName: VITE_GLOB_APP_SHORT_NAME,
urlPrefix: VITE_GLOB_API_URL_PREFIX, urlPrefix: VITE_GLOB_API_URL_PREFIX,
uploadUrl: VITE_GLOB_UPLOAD_URL, uploadUrl: VITE_GLOB_UPLOAD_URL,
permissionMode: VITE_GLOB_PERMISSION_MODE, prodMock: VITE_GLOB_PROD_MOCK,
prodMock: VITE_GLOB_PROD_MOCK imgUrl: VITE_GLOB_IMG_URL
}; };
return glob as Readonly<GlobConfig>; return glob as Readonly<GlobConfig>;
}; };

View File

@@ -19,7 +19,7 @@ export function usePermission() {
* 可用于 v-if 显示逻辑 * 可用于 v-if 显示逻辑
* */ * */
function hasPermission(accesses: string[]): boolean { function hasPermission(accesses: string[]): boolean {
if (!accesses.length) return true if (!accesses ||!accesses.length) return true
return _someRoles(accesses) return _someRoles(accesses)
} }

View File

@@ -12,7 +12,7 @@
</a> </a>
</div> </div>
<div class="copyright"> <div class="copyright">
naive-ui-admin 1.2 · Made by Ah jung naive-ui-admin 1.4 · Made by Ah jung
</div> </div>
</div> </div>

View File

@@ -73,7 +73,9 @@
<div class="avatar"> <div class="avatar">
<n-avatar> <n-avatar>
{{ username }} {{ username }}
<template #icon><UserOutlined/></template> <template #icon>
<UserOutlined/>
</template>
</n-avatar> </n-avatar>
</div> </div>
</n-dropdown> </n-dropdown>
@@ -188,7 +190,7 @@ export default defineComponent({
// 退出登录 // 退出登录
const doLogout = () => { const doLogout = () => {
dialog.warning({ dialog.info({
title: '提示', title: '提示',
content: '您确定要退出登录吗', content: '您确定要退出登录吗',
positiveText: '确定', positiveText: '确定',

View File

@@ -1,7 +1,7 @@
<template> <template>
<div class="logo"> <div class="logo">
<img src="~@/assets/images/logo.png" alt=""/> <img src="~@/assets/images/logo.png" alt=""/>
<h2 v-show="!collapsed" class="title">NaiveUiAdmin</h2> <h2 v-show="!collapsed" class="title">&nbsp;NaiveUiAdmin</h2>
</div> </div>
</template> </template>
@@ -27,6 +27,7 @@ export default {
white-space: nowrap; white-space: nowrap;
img { img {
width: auto;
height: 32px; height: 32px;
} }

View File

@@ -74,6 +74,9 @@ export default defineComponent({
watch( watch(
() => currentRoute.fullPath, () => currentRoute.fullPath,
() => { () => {
const matched = currentRoute.matched
const getOpenKeys = matched && matched.length ? [matched[0]?.name] : []
state.openKeys = getOpenKeys
state.selectedKeys = currentRoute.name state.selectedKeys = currentRoute.name
} }
) )
@@ -89,6 +92,7 @@ export default defineComponent({
//展开菜单 //展开菜单
function menuExpanded(openKeys: string[]) { function menuExpanded(openKeys: string[]) {
console.log(openKeys)
if (!openKeys) return if (!openKeys) return
const latestOpenKey = openKeys.pop(); const latestOpenKey = openKeys.pop();
state.openKeys = latestOpenKey ? [latestOpenKey] : [] state.openKeys = latestOpenKey ? [latestOpenKey] : []

View File

@@ -20,14 +20,14 @@
</span> </span>
<div ref="navScroll" class="tabs-card-scroll"> <div ref="navScroll" class="tabs-card-scroll">
<div ref="navRef" class="tabs-card-nav" :style="getNavStyle"> <div ref="navRef" class="tabs-card-nav" :style="getNavStyle">
<Draggable :list="tabsList" animation="300" item-key="fullPath"> <Draggable :list="tabsList" animation="300" item-key="fullPath" class="flex">
<template #item="{element}"> <template #item="{element}">
<div class="tabs-card-scroll-item" <div class="tabs-card-scroll-item"
:class="{'active-item':activeKey === element.path }" :class="{'active-item':activeKey === element.path }"
@click.stop="goPage(element)" @click.stop="goPage(element)"
@contextmenu="handleContextMenu"> @contextmenu="handleContextMenu($event,element)">
<span>{{ element.meta.title }}</span> <span>{{ element.meta.title }}</span>
<n-icon size="14" @click.stop="closeTabItem(element)"> <n-icon size="14" @click.stop="closeTabItem(element)" v-if="element.path != baseHome">
<CloseOutlined /> <CloseOutlined />
</n-icon> </n-icon>
</div> </div>
@@ -52,7 +52,19 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent, reactive, computed, ref, toRefs, toRaw, unref, provide, watch, onMounted, nextTick } from 'vue' import {
defineComponent,
reactive,
computed,
ref,
toRefs,
toRaw,
unref,
provide,
watch,
onMounted,
nextTick
} from 'vue'
import { useRoute, useRouter } from 'vue-router' import { useRoute, useRouter } from 'vue-router'
import { storage } from '@/utils/Storage' import { storage } from '@/utils/Storage'
import { TABS_ROUTES } from '@/store/mutation-types' import { TABS_ROUTES } from '@/store/mutation-types'
@@ -61,6 +73,7 @@ import { useAsyncRouteStore } from '@/store/modules/asyncRoute'
import { RouteItem } from '@/store/modules/tabsView' import { RouteItem } from '@/store/modules/tabsView'
import { useProjectSetting } from '@/hooks/setting/useProjectSetting' import { useProjectSetting } from '@/hooks/setting/useProjectSetting'
import { useMessage } from 'naive-ui' import { useMessage } from 'naive-ui'
// @ts-ignore
import Draggable from 'vuedraggable/src/vuedraggable' import Draggable from 'vuedraggable/src/vuedraggable'
import { PageEnum } from '@/enums/pageEnum' import { PageEnum } from '@/enums/pageEnum'
import { import {
@@ -74,7 +87,6 @@ import {
} from '@vicons/antd' } from '@vicons/antd'
import { renderIcon } from '@/utils/index' import { renderIcon } from '@/utils/index'
import elementResizeDetectorMaker from 'element-resize-detector' import elementResizeDetectorMaker from 'element-resize-detector'
import { useProjectSettingStore } from "@/store/modules/projectSetting";
import { useDesignSetting } from '@/hooks/setting/useDesignSetting' import { useDesignSetting } from '@/hooks/setting/useDesignSetting'
export default defineComponent({ export default defineComponent({
@@ -106,6 +118,7 @@ export default defineComponent({
const navRef: any = ref(null) const navRef: any = ref(null)
const navScroll: any = ref(null) const navScroll: any = ref(null)
const navWrap: any = ref(null) const navWrap: any = ref(null)
const isCurrent = ref(false)
const state = reactive({ const state = reactive({
activeKey: route.fullPath, activeKey: route.fullPath,
@@ -140,28 +153,34 @@ export default defineComponent({
}) })
//tags 右侧下拉菜单 //tags 右侧下拉菜单
const TabsMenuOptions = [ const TabsMenuOptions = computed(() => {
const isDisabled = unref(tabsList).length <= 1 ? true : false
return [
{ {
label: '刷新当前', label: '刷新当前',
key: '1', key: '1',
icon: renderIcon(ReloadOutlined) icon: renderIcon(ReloadOutlined)
}, },
{ {
label: '关闭当前', label: `关闭当前`,
key: '2', key: '2',
disabled: unref(isCurrent) || isDisabled,
icon: renderIcon(CloseOutlined) icon: renderIcon(CloseOutlined)
}, },
{ {
label: '关闭其他', label: '关闭其他',
key: '3', key: '3',
disabled:isDisabled,
icon: renderIcon(ColumnWidthOutlined) icon: renderIcon(ColumnWidthOutlined)
}, },
{ {
label: '关闭全部', label: '关闭全部',
key: '4', key: '4',
disabled:isDisabled,
icon: renderIcon(MinusOutlined) icon: renderIcon(MinusOutlined)
} }
] ]
})
let routes: RouteItem[] = [] let routes: RouteItem[] = []
@@ -203,14 +222,15 @@ export default defineComponent({
// 标签页列表 // 标签页列表
const tabsList: any = computed(() => tabsViewStore.tabsList) const tabsList: any = computed(() => tabsViewStore.tabsList)
const whiteList = [PageEnum.REDIRECT, PageEnum.BASE_LOGIN] const whiteList: string[] = [PageEnum.BASE_LOGIN_NAME, PageEnum.REDIRECT_NAME, PageEnum.ERROR_PAGE_NAME]
watch( watch(
() => route.fullPath, () => route.fullPath,
(to) => { (to) => {
if (whiteList.includes(route.name as string) || ['ErrorPage'].includes(route.name as string)) return if (whiteList.includes(route.name as string)) return
state.activeKey = to state.activeKey = to
tabsViewStore.addTabs(getSimpleRoute(route)) tabsViewStore.addTabs(getSimpleRoute(route))
updateNavScroll()
}, },
{ immediate: true } { immediate: true }
) )
@@ -233,6 +253,7 @@ export default defineComponent({
state.activeKey = currentRoute.fullPath state.activeKey = currentRoute.fullPath
router.push(currentRoute) router.push(currentRoute)
} }
updateNavScroll()
} }
// 刷新页面 // 刷新页面
@@ -251,6 +272,7 @@ export default defineComponent({
tabsViewStore.closeLeftTabs(route) tabsViewStore.closeLeftTabs(route)
state.activeKey = route.fullPath state.activeKey = route.fullPath
router.replace(route.fullPath) router.replace(route.fullPath)
updateNavScroll()
} }
// 关闭右侧 // 关闭右侧
@@ -258,6 +280,7 @@ export default defineComponent({
tabsViewStore.closeRightTabs(route) tabsViewStore.closeRightTabs(route)
state.activeKey = route.fullPath state.activeKey = route.fullPath
router.replace(route.fullPath) router.replace(route.fullPath)
updateNavScroll()
} }
// 关闭其他 // 关闭其他
@@ -265,13 +288,15 @@ export default defineComponent({
tabsViewStore.closeOtherTabs(route) tabsViewStore.closeOtherTabs(route)
state.activeKey = route.fullPath state.activeKey = route.fullPath
router.replace(route.fullPath) router.replace(route.fullPath)
updateNavScroll()
} }
// 关闭全部 // 关闭全部
const closeAll = () => { const closeAll = () => {
localStorage.removeItem('routes') localStorage.removeItem('routes')
tabsViewStore.closeAllTabs() tabsViewStore.closeAllTabs()
router.replace('/') router.replace(PageEnum.BASE_HOME)
updateNavScroll()
} }
//tab 操作 //tab 操作
@@ -294,6 +319,7 @@ export default defineComponent({
closeAll() closeAll()
break break
} }
updateNavScroll()
} }
function getCurrentScrollOffset() { function getCurrentScrollOffset() {
@@ -315,7 +341,7 @@ export default defineComponent({
} }
function scrollNext() { function scrollNext() {
const navWidth = navRef.value.offsetWidth const navWidth = navRef.value.scrollWidth
const containerWidth = navScroll.value.offsetWidth const containerWidth = navScroll.value.offsetWidth
const currentOffset = getCurrentScrollOffset() const currentOffset = getCurrentScrollOffset()
if (navWidth - currentOffset <= containerWidth) return if (navWidth - currentOffset <= containerWidth) return
@@ -329,8 +355,9 @@ export default defineComponent({
} }
function updateNavScroll() { function updateNavScroll() {
const navWidth = navRef.value.offsetWidth if (!navRef.value) return
const containerWidth = navScroll.value.offsetWidth let navWidth = navRef.value.scrollWidth
let containerWidth = navScroll.value.offsetWidth
const currentOffset = getCurrentScrollOffset() const currentOffset = getCurrentScrollOffset()
if (containerWidth < navWidth) { if (containerWidth < navWidth) {
state.scrollable = true state.scrollable = true
@@ -353,8 +380,9 @@ export default defineComponent({
return state.navStyle return state.navStyle
}) })
function handleContextMenu(e) { function handleContextMenu(e,item) {
e.preventDefault() e.preventDefault()
isCurrent.value = PageEnum.BASE_HOME_REDIRECT === item.path
state.showDropdown = false state.showDropdown = false
nextTick().then(() => { nextTick().then(() => {
state.showDropdown = true state.showDropdown = true
@@ -383,10 +411,14 @@ export default defineComponent({
} }
onMounted(() => { onMounted(() => {
onElementResize()
})
function onElementResize() {
let observer let observer
observer = elementResizeDetectorMaker() observer = elementResizeDetectorMaker()
observer.listenTo(navWrap.value, handleResize) observer.listenTo(navWrap.value, handleResize)
}) }
return { return {
...toRefs(state), ...toRefs(state),
@@ -395,6 +427,7 @@ export default defineComponent({
navScroll, navScroll,
route, route,
tabsList, tabsList,
baseHome:PageEnum.BASE_HOME_REDIRECT,
goPage, goPage,
closeTabItem, closeTabItem,
closeLeft, closeLeft,
@@ -419,7 +452,7 @@ export default defineComponent({
<style lang="less" scoped> <style lang="less" scoped>
.tabs-view { .tabs-view {
width: 100%; width: 100%;
padding: 6px 0px; padding: 6px 0;
display: flex; display: flex;
transition: all 0.2s ease-in-out; transition: all 0.2s ease-in-out;
@@ -519,6 +552,7 @@ export default defineComponent({
} }
} }
} }
.active-item { .active-item {
color: #2d8cf0 color: #2d8cf0
} }
@@ -564,6 +598,6 @@ export default defineComponent({
} }
.tabs-view-fixed-header { .tabs-view-fixed-header {
top: 0px; top: 0;
} }
</style> </style>

View File

@@ -3,11 +3,15 @@ import { createApp } from 'vue'
import App from './App.vue' import App from './App.vue'
import router, { setupRouter } from './router' import router, { setupRouter } from './router'
import { setupStore } from '@/store' import { setupStore } from '@/store'
import MakeitCaptcha from 'makeit-captcha'
import 'makeit-captcha/dist/captcha.min.css'
import { setupNaive, setupDirectives, setupGlobalMethods, setupCustomComponents } from '@/plugins' import { setupNaive, setupDirectives, setupGlobalMethods, setupCustomComponents } from '@/plugins'
async function bootstrap() { async function bootstrap() {
const app = createApp(App) const app = createApp(App)
app.use(MakeitCaptcha)
// 注册全局常用的 naive-ui 组件 // 注册全局常用的 naive-ui 组件
setupNaive(app) setupNaive(app)

View File

@@ -59,7 +59,10 @@ import {
NTable, NTable,
NInputNumber, NInputNumber,
NLoadingBarProvider, NLoadingBarProvider,
NModal NModal,
NUpload,
NTree,
NSpin
} from 'naive-ui' } from 'naive-ui'
const naive = create({ const naive = create({
@@ -122,7 +125,10 @@ const naive = create({
NTable, NTable,
NInputNumber, NInputNumber,
NLoadingBarProvider, NLoadingBarProvider,
NModal NModal,
NUpload,
NTree,
NSpin
] ]
}) })

View File

@@ -45,7 +45,7 @@ export const asyncRoutes = [ErrorPageRoute, ...routeModuleList];
//普通路由 无需验证权限 //普通路由 无需验证权限
export const constantRouter = [LoginRoute, RootRoute, RedirectRoute] export const constantRouter:any[] = [LoginRoute, RootRoute, RedirectRoute]
const router = createRouter({ const router = createRouter({
history: createWebHashHistory(''), history: createWebHashHistory(''),

View File

@@ -36,6 +36,15 @@ const routes: Array<RouteRecordRaw> = [
title: '表格', title: '表格',
}, },
component: () => import('@/views/comp/table/list.vue') component: () => import('@/views/comp/table/list.vue')
},
{
path: 'upload',
name: `${ routeName }_upload`,
meta: {
title: '上传',
},
component: () => import('@/views/comp/upload/index.vue')
} }
], ],
} }

View File

@@ -33,6 +33,15 @@ const routes: Array<RouteRecordRaw> = [
title: '基础列表', title: '基础列表',
}, },
component: () => import('@/views/list/basicList/index.vue') component: () => import('@/views/list/basicList/index.vue')
},
{
path: 'basic-info/:id?',
name: 'basic-info',
meta: {
title: '基础详情',
hidden:true
},
component: () => import('@/views/list/basicList/info.vue')
} }
], ],
} }

View File

@@ -0,0 +1,50 @@
import { RouteRecordRaw } from 'vue-router'
import { Layout } from '@/router/constant';
import { ToolOutlined } from '@vicons/antd'
import { OptionsSharp } from '@vicons/ionicons5'
import { renderIcon } from '@/utils/index'
/**
* @param name 路由名称, 必须设置,且不能重名
* @param meta 路由元信息(路由附带扩展信息)
* @param redirect 重定向地址, 访问这个路由时,自定进行重定向
* @param meta.disabled 禁用整个菜单
* @param meta.title 菜单名称
* @param meta.icon 菜单图标
* @param meta.keepAlive 缓存该路由
* @param meta.sort 排序越小越排前
*
* */
const routes: Array<RouteRecordRaw> = [
{
path: '/system',
name: 'System',
redirect: '/system/menu',
component: Layout,
meta: {
title: '系统设置',
icon: renderIcon(OptionsSharp),
sort: 1
},
children: [
{
path: 'menu',
name: 'system_menu',
meta: {
title: '菜单权限管理',
},
component: () => import('@/views/system/menu/menu.vue')
},
{
path: 'role',
name: 'system_role',
meta: {
title: '角色权限管理',
},
component: () => import('@/views/system/role/role.vue')
}
],
}
]
export default routes

View File

@@ -1,11 +1,9 @@
import { toRefs } from 'vue'
import { isNavigationFailure, Router } from 'vue-router' import { isNavigationFailure, Router } from 'vue-router'
import { useUserStoreWidthOut } from '@/store/modules/user' import { useUserStoreWidthOut } from '@/store/modules/user'
import { useAsyncRouteStoreWidthOut } from '@/store/modules/asyncRoute' import { useAsyncRouteStoreWidthOut } from '@/store/modules/asyncRoute'
import NProgress from 'nprogress' // progress bar import NProgress from 'nprogress' // progress bar
import { ACCESS_TOKEN } from '@/store/mutation-types' import { ACCESS_TOKEN } from '@/store/mutation-types'
import { storage } from '@/utils/Storage' import { storage } from '@/utils/Storage'
import { debounce } from '@/utils/lodashChunk'
import { PageEnum } from '@/enums/pageEnum' import { PageEnum } from '@/enums/pageEnum'
@@ -15,39 +13,6 @@ const LOGIN_PATH = PageEnum.BASE_LOGIN
const whitePathList = [LOGIN_PATH] // no redirect whitelist const whitePathList = [LOGIN_PATH] // no redirect whitelist
// 是否需要从后端获取菜单
const isGetMenus = debounce(
({ to, from, next, hasRoute, router }) => {
const userStore = useUserStoreWidthOut();
const asyncRouteStore = useAsyncRouteStoreWidthOut();
userStore.GetInfo().then(res => {
asyncRouteStore.generateRoutes(res).then(() => {
// 根据roles权限生成可访问的路由表
// 动态添加可访问路由表
asyncRouteStore.getRouters().forEach((item) => {
router.addRoute(item)
});
// if (whitePathList.includes(to.name as string)) return
if (!hasRoute) {
// 请求带有 redirect 重定向时,登录自动重定向到该地址
const redirect = decodeURIComponent((from.query.redirect || '') as string)
if (to.path === redirect) {
next({ ...to, replace: true })
} else {
// 跳转到目的路由
next({ ...to, replace: true })
}
} else {
next()
}
}).catch(() => next({ path: defaultRoutePath }))
})
},
1800,
{ leading: true }
)
export function createRouterGuards(router: Router) { export function createRouterGuards(router: Router) {
const userStore = useUserStoreWidthOut(); const userStore = useUserStoreWidthOut();
const asyncRouteStore = useAsyncRouteStoreWidthOut(); const asyncRouteStore = useAsyncRouteStoreWidthOut();
@@ -65,7 +30,6 @@ export function createRouterGuards(router: Router) {
} }
const token = storage.get(ACCESS_TOKEN) const token = storage.get(ACCESS_TOKEN)
const roles = storage.get('roles')
if (!token) { if (!token) {
@@ -111,7 +75,7 @@ export function createRouterGuards(router: Router) {
NProgress.done() NProgress.done()
}) })
router.afterEach((to, from, failure) => { router.afterEach((to, _, failure) => {
document.title = (to?.meta?.title as string) || document.title document.title = (to?.meta?.title as string) || document.title
if (isNavigationFailure(failure)) { if (isNavigationFailure(failure)) {
//console.log('failed navigation', failure) //console.log('failed navigation', failure)

View File

@@ -1,5 +1,23 @@
import type { RouteRecordRaw, RouteMeta } from 'vue-router';
import { defineComponent } from 'vue';
import { RoleEnum } from '@/enums/roleEnum' import { RoleEnum } from '@/enums/roleEnum'
export type Component<T extends any = any> =
| ReturnType<typeof defineComponent>
| (() => Promise<typeof import('*.vue')>)
| (() => Promise<T>);
// @ts-ignore
export interface AppRouteRecordRaw extends Omit<RouteRecordRaw, 'meta'> {
name: string;
meta: RouteMeta;
component?: Component | string;
components?: Component;
children?: AppRouteRecordRaw[];
props?: Recordable;
fullPath?: string;
}
export interface Meta { export interface Meta {
// 名称 // 名称
title: string title: string

View File

@@ -14,5 +14,18 @@ export default {
defaultPageSize: 10, defaultPageSize: 10,
//可切换每页数量集合 //可切换每页数量集合
pageSizes: [10, 20, 30, 40, 50], pageSizes: [10, 20, 30, 40, 50],
},
upload: {
//考虑接口规范不同
apiSetting: {
// 集合字段名
infoField: 'data',
// 图片地址字段名
imgField: 'photo'
},
//最大上传图片大小
maxSize: 2,
//图片上传类型
fileType: ['image/png', 'image/jpg', 'image/jpeg', 'image/gif', 'image/svg+xml'],
} }
} }

View File

@@ -2,14 +2,10 @@ import { toRaw, unref } from 'vue'
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import { RouteRecordRaw } from 'vue-router' import { RouteRecordRaw } from 'vue-router'
import { store } from '@/store' import { store } from '@/store'
import { createStorage } from '@/utils/Storage'
import { asyncRoutes, constantRouter } from '@/router/index' import { asyncRoutes, constantRouter } from '@/router/index'
import { generatorDynamicRouter } from '@/router/generator-routers' import { generatorDynamicRouter } from '@/router/generator-routers'
import { useProjectSetting } from '@/hooks/setting/useProjectSetting' import { useProjectSetting } from '@/hooks/setting/useProjectSetting'
const Storage = createStorage({ storage: localStorage })
interface TreeHelperConfig { interface TreeHelperConfig {
id: string; id: string;
children: string; children: string;
@@ -64,7 +60,7 @@ export const useAsyncRouteStore = defineStore({
isDynamicAddedRoute: false, isDynamicAddedRoute: false,
}), }),
getters: { getters: {
getMenus(state: AsyncRouteState): RouteRecordRaw[] { getMenus(): RouteRecordRaw[] {
return this.menus return this.menus
}, },
getIsDynamicAddedRoute(): boolean { getIsDynamicAddedRoute(): boolean {

View File

@@ -1,4 +1,4 @@
import type { GlobEnvConfig } from '#/config'; import type { GlobEnvConfig } from '/#/config';
import { warn } from '@/utils/log'; import { warn } from '@/utils/log';
import pkg from '../../package.json'; import pkg from '../../package.json';
@@ -28,7 +28,8 @@ export function getAppEnvConfig() {
VITE_GLOB_APP_SHORT_NAME, VITE_GLOB_APP_SHORT_NAME,
VITE_GLOB_API_URL_PREFIX, VITE_GLOB_API_URL_PREFIX,
VITE_GLOB_UPLOAD_URL, VITE_GLOB_UPLOAD_URL,
VITE_GLOB_PROD_MOCK VITE_GLOB_PROD_MOCK,
VITE_GLOB_IMG_URL
} = ENV; } = ENV;
if (!/^[a-zA-Z\_]*$/.test(VITE_GLOB_APP_SHORT_NAME)) { if (!/^[a-zA-Z\_]*$/.test(VITE_GLOB_APP_SHORT_NAME)) {
@@ -43,7 +44,8 @@ export function getAppEnvConfig() {
VITE_GLOB_APP_SHORT_NAME, VITE_GLOB_APP_SHORT_NAME,
VITE_GLOB_API_URL_PREFIX, VITE_GLOB_API_URL_PREFIX,
VITE_GLOB_UPLOAD_URL, VITE_GLOB_UPLOAD_URL,
VITE_GLOB_PROD_MOCK VITE_GLOB_PROD_MOCK,
VITE_GLOB_IMG_URL
}; };
} }

View File

@@ -18,9 +18,8 @@ import { useUserStoreWidthOut } from '@/store/modules/user'
const globSetting = useGlobSetting() const globSetting = useGlobSetting()
const urlPrefix = globSetting.urlPrefix || ''; const urlPrefix = globSetting.urlPrefix || '';
// @ts-ignore
const Message = window.$message const { $message: Message, $dialog: Modal } = window
const Modal = window.$dialog
import router from '@/router' import router from '@/router'
import { storage } from '@/utils/Storage' import { storage } from '@/utils/Storage'

View File

@@ -32,6 +32,8 @@ export interface RequestOptions {
apiUrl?: string apiUrl?: string
// 错误消息提示类型 // 错误消息提示类型
errorMessageMode?: 'none' | 'modal' errorMessageMode?: 'none' | 'modal'
// 是否添加时间戳
joinTime?: boolean;
} }
export interface Result<T = any> { export interface Result<T = any> {

View File

@@ -1,6 +1,7 @@
import { h } from 'vue'; import { h } from 'vue';
import type { App, Plugin } from 'vue'; import type { App, Plugin } from 'vue';
import { NIcon } from 'naive-ui' import { NIcon } from 'naive-ui'
import { PageEnum } from "@/enums/pageEnum";
/** /**
* render 图标 * render 图标
@@ -12,9 +13,9 @@ export function renderIcon(icon) {
/** /**
* 递归组装菜单格式 * 递归组装菜单格式
*/ */
export function generatorMenu(routerMap: Array<any>, parent?: object) { export function generatorMenu(routerMap: Array<any>) {
return routerMap.filter(item => { return routerMap.filter(item => {
return item.meta.hidden != true && !['/:path(.*)*', '/', '/redirect', '/login'].includes(item.path) return item.meta.hidden != true && !['/:path(.*)*', '/', PageEnum.REDIRECT, PageEnum.BASE_LOGIN].includes(item.path)
}).map(item => { }).map(item => {
const currentMenu = { const currentMenu = {
...item, ...item,
@@ -25,7 +26,7 @@ export function generatorMenu(routerMap: Array<any>, parent?: object) {
// 是否有子菜单,并递归处理 // 是否有子菜单,并递归处理
if (item.children && item.children.length > 0) { if (item.children && item.children.length > 0) {
// Recursion // Recursion
currentMenu.children = generatorMenu(item.children, currentMenu) currentMenu.children = generatorMenu(item.children)
} }
return currentMenu return currentMenu
}) })
@@ -41,3 +42,34 @@ export const withInstall = <T>(component: T, alias?: string) => {
}; };
return component as T & Plugin; return component as T & Plugin;
}; };
/**
* 找到对应的节点
* */
let result = null
export function getTreeItem(data: any[], key?: string | number) {
data.map(item => {
if (item.key === key) {
result = item;
} else {
if (item.children && item.children.length) {
getTreeItem(item.children, key);
}
}
});
return result
}
/**
* 找到所有节点
* */
let treeAll = []
export function getTreeAll(data: any[]) {
data.map(item => {
treeAll.push(item.key)
if (item.children && item.children.length) {
getTreeAll(item.children);
}
});
return treeAll
}

View File

@@ -1,6 +1,6 @@
<template> <template>
<n-card :bordered="false" class="proCard"> <n-card :bordered="false" class="proCard">
<ProTable <BasicTable
title="表格列表" title="表格列表"
titleTooltip="这是一个提示" titleTooltip="这是一个提示"
:columns="columns" :columns="columns"
@@ -12,19 +12,19 @@
<template #toolbar> <template #toolbar>
<n-button type="primary" @click="reloadTable">刷新数据</n-button> <n-button type="primary" @click="reloadTable">刷新数据</n-button>
</template> </template>
</ProTable> </BasicTable>
</n-card> </n-card>
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent, onMounted, reactive, toRefs, ref, h } from 'vue' import { defineComponent, onMounted, reactive, toRefs, ref, h } from 'vue'
import { NTag, NButton, useMessage } from 'naive-ui' import { NTag, NButton, useMessage } from 'naive-ui'
import { ProTable } from '@/components/ProTable' import { BasicTable } from '@/components/Table'
import { getTableList } from '@/api/table/list' import { getTableList } from '@/api/table/list'
import { columns } from './columns' import { columns } from './columns'
export default defineComponent({ export default defineComponent({
components: { ProTable }, components: { BasicTable },
setup() { setup() {
const message = useMessage() const message = useMessage()
const actionRef = ref() const actionRef = ref()

View File

@@ -0,0 +1,128 @@
<template>
<div>
<div class="n-layout-page-header">
<n-card :bordered="false" title="上传图片">
上传图片用于向用户收集图片信息
</n-card>
</div>
<n-card :bordered="false" class="proCard mt-4">
<n-grid cols="2 s:1 m:3 l:3 xl:3 2xl:3" responsive="screen">
<n-grid-item offset="0 s:0 m:1 l:1 xl:1 2xl:1">
<n-form
:label-width="80"
:model="formValue"
:rules="rules"
label-placement="left"
ref="formRef"
class="py-8"
>
<n-form-item label="预约姓名" path="name">
<n-input v-model:value="formValue.name" placeholder="输入姓名"/>
</n-form-item>
<n-form-item label="预约号码" path="mobile">
<n-input placeholder="电话号码" v-model:value="formValue.mobile"/>
</n-form-item>
<n-form-item label="病例图片" path="images">
<BasicUpload
:action="`${uploadUrl}/v1.0/files`"
:headers="uploadHeaders"
:data="{ type: 0 }"
name="files"
:width="100"
:height="100"
@uploadChange="uploadChange"
v-model:value="formValue.images"
helpText="单个文件不超过2MB最多只能上传10个文件"/>
</n-form-item>
<div style="margin-left:80px">
<n-space>
<n-button type="primary" @click="formSubmit">提交预约</n-button>
<n-button @click="resetForm">重置</n-button>
</n-space>
</div>
</n-form>
</n-grid-item>
</n-grid>
</n-card>
</div>
</template>
<script lang="ts">
import { defineComponent, ref, unref, reactive, toRefs, } from 'vue'
import { useMessage } from 'naive-ui'
import { BasicUpload } from '@/components/Upload'
import { useGlobSetting } from '@/hooks/setting'
const globSetting = useGlobSetting()
const rules = {
name: {
required: true,
message: '请输入预约姓名',
trigger: 'blur'
},
remark: {
required: true,
message: '请输入预约备注',
trigger: 'blur'
},
images: {
required: true,
type: 'array',
message: '请上传病例图片',
trigger: 'change'
},
}
export default defineComponent({
components: { BasicUpload },
setup() {
const formRef: any = ref(null)
const message = useMessage()
const { uploadUrl } = globSetting
const state = reactive({
formValue: {
name: '',
mobile: '',
//图片列表 通常查看和编辑使用 绝对路径 | 相对路径都可以
images: ['https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png'],
},
uploadHeaders: {
platform: "miniPrograms",
timestamp: new Date().getTime(),
token: 'Q6fFCuhc1vkKn5JNFWaCLf6gRAc5n0LQHd08dSnG4qo=',
}
})
function formSubmit() {
formRef.value.validate((errors) => {
if (!errors) {
message.success('验证成功')
} else {
message.error('验证失败,请填写完整信息')
}
})
}
function resetForm() {
formRef.value.restoreValidation()
}
function uploadChange(list: string[]) {
state.formValue.images = unref(list)
}
return {
...toRefs(state),
formRef,
uploadUrl,
rules,
formSubmit,
resetForm,
uploadChange
}
}
})
</script>

View File

@@ -32,9 +32,14 @@ export default defineComponent({
background-color: white; background-color: white;
border-radius: 4px; border-radius: 4px;
padding: 50px 0; padding: 50px 0;
.text-center { .text-center {
h1{ color: #666;padding: 20px 0} h1 {
color: #666;
padding: 20px 0
} }
}
img { img {
width: 350px; width: 350px;
margin: 0 auto; margin: 0 auto;

View File

@@ -32,9 +32,14 @@ export default defineComponent({
background-color: white; background-color: white;
border-radius: 4px; border-radius: 4px;
padding: 50px 0; padding: 50px 0;
.text-center { .text-center {
h1{ color: #666;padding: 20px 0} h1 {
color: #666;
padding: 20px 0
} }
}
img { img {
width: 350px; width: 350px;
margin: 0 auto; margin: 0 auto;

View File

@@ -32,9 +32,14 @@ export default defineComponent({
background-color: white; background-color: white;
border-radius: 4px; border-radius: 4px;
padding: 50px 0; padding: 50px 0;
.text-center { .text-center {
h1{ color: #666;padding: 20px 0} h1 {
color: #666;
padding: 20px 0
} }
}
img { img {
width: 350px; width: 350px;
margin: 0 auto; margin: 0 auto;

View File

@@ -5,7 +5,7 @@
表单页用于向用户收集或验证信息基础表单常见于数据项较少的表单场景表单域标签也可支持响应式 表单页用于向用户收集或验证信息基础表单常见于数据项较少的表单场景表单域标签也可支持响应式
</n-card> </n-card>
</div> </div>
<n-card :bordered="false" class="proCard mt-4"> <n-card :bordered="false" class="mt-4 proCard">
<n-grid cols="2 s:1 m:3 l:3 xl:3 2xl:3" responsive="screen"> <n-grid cols="2 s:1 m:3 l:3 xl:3 2xl:3" responsive="screen">
<n-grid-item offset="0 s:0 m:1 l:1 xl:1 2xl:1"> <n-grid-item offset="0 s:0 m:1 l:1 xl:1 2xl:1">
<n-form <n-form
@@ -16,7 +16,7 @@
ref="formRef" ref="formRef"
class="py-8" class="py-8"
> >
<n-form-item label="预约姓名" path="name"> <n-form-item label="预约姓名1" path="name">
<n-input v-model:value="formValue.name" placeholder="输入姓名"/> <n-input v-model:value="formValue.name" placeholder="输入姓名"/>
</n-form-item> </n-form-item>
<n-form-item label="预约号码" path="mobile"> <n-form-item label="预约号码" path="mobile">
@@ -55,6 +55,18 @@
placeholder="请输入预约备注" placeholder="请输入预约备注"
/> />
</n-form-item> </n-form-item>
<n-form-item label="图片" path="img">
<BasicUpload
:action="`${uploadUrl}/v1.0/files`"
:headers="uploadHeaders"
:data="{ type: 0 }"
name="files"
:width="100"
:height="100"
@uploadChange="uploadChange"
v-model:value="uploadList"
helpText="单个文件不超过20MB最多只能上传10个文件"/>
</n-form-item>
<div style="margin-left:80px"> <div style="margin-left:80px">
<n-space> <n-space>
<n-button type="primary" @click="formSubmit">提交预约</n-button> <n-button type="primary" @click="formSubmit">提交预约</n-button>
@@ -69,8 +81,12 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent, ref } from 'vue' import { defineComponent, ref, reactive, toRefs, } from 'vue'
import { useMessage } from 'naive-ui' import { useMessage } from 'naive-ui'
import { BasicUpload } from '@/components/Upload'
import { useGlobSetting } from '@/hooks/setting'
const globSetting = useGlobSetting()
const matterList = [ const matterList = [
{ {
@@ -102,23 +118,7 @@ const doctorList = [
} }
] ]
export default defineComponent({ const rules = {
setup() {
const formRef = ref(null)
const message = useMessage()
return {
formRef,
formValue: ref({
name: '',
mobile: '',
remark: '',
sex: 1,
matter: null,
doctor: null,
datetime: [],
}),
rules: {
name: { name: {
required: true, required: true,
message: '请输入预约姓名', message: '请输入预约姓名',
@@ -146,10 +146,39 @@ export default defineComponent({
message: '请选择预约医生', message: '请选择预约医生',
trigger: 'change' trigger: 'change'
}, },
}, }
doctorList,
matterList, export default defineComponent({
formSubmit(e) { components: { BasicUpload },
setup() {
const formRef: any = ref(null)
const message = useMessage()
const { uploadUrl } = globSetting
const defaultValueRef = () => ({
name: '',
mobile: '',
remark: '',
sex: 1,
matter: null,
doctor: null,
datetime: [],
})
const state = reactive({
formValue: defaultValueRef(),
//图片列表 通常查看和编辑使用 绝对路径 | 相对路径都可以
uploadList: [
'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png',
],
uploadHeaders: {
platform: "miniPrograms",
timestamp: new Date().getTime(),
token: 'Q6fFCuhc1vkKn5JNFWaCLf6gRAc5n0LQHd08dSnG4qo=',
}
})
function formSubmit() {
formRef.value.validate((errors) => { formRef.value.validate((errors) => {
if (!errors) { if (!errors) {
message.success('验证成功') message.success('验证成功')
@@ -157,10 +186,27 @@ export default defineComponent({
message.error('验证失败,请填写完整信息') message.error('验证失败,请填写完整信息')
} }
}) })
},
resetForm() {
formRef.value.restoreValidation()
} }
function resetForm() {
formRef.value.restoreValidation()
state.formValue = Object.assign(state.formValue, defaultValueRef())
}
function uploadChange(list: string[]) {
console.log(list)
}
return {
...toRefs(state),
formRef,
uploadUrl,
rules,
doctorList,
matterList,
formSubmit,
resetForm,
uploadChange
} }
} }
}) })

View File

@@ -7,22 +7,32 @@
</div> </div>
<n-card :bordered="false" title="基本信息" class="proCard mt-4" size="small" :segmented="{content: 'hard'}"> <n-card :bordered="false" title="基本信息" class="proCard mt-4" size="small" :segmented="{content: 'hard'}">
<n-descriptions label-placement="left" class="py-2"> <n-descriptions label-placement="left" class="py-2">
<n-descriptions-item><template #label>收款人姓名</template>啊俊</n-descriptions-item> <n-descriptions-item>
<template #label>收款人姓名</template>
啊俊
</n-descriptions-item>
<n-descriptions-item label="收款账户">NaiveUiAdmin@qq.com</n-descriptions-item> <n-descriptions-item label="收款账户">NaiveUiAdmin@qq.com</n-descriptions-item>
<n-descriptions-item label="付款类型">支付宝</n-descriptions-item> <n-descriptions-item label="付款类型">支付宝</n-descriptions-item>
<n-descriptions-item label="付款账户">NaiveUiAdmin@163.com</n-descriptions-item> <n-descriptions-item label="付款账户">NaiveUiAdmin@163.com</n-descriptions-item>
<n-descriptions-item label="转账金额">1980.00</n-descriptions-item> <n-descriptions-item label="转账金额">1980.00</n-descriptions-item>
<n-descriptions-item label="状态"><n-tag type="success"> 已到账 </n-tag></n-descriptions-item> <n-descriptions-item label="状态">
<n-tag type="success"> 已到账</n-tag>
</n-descriptions-item>
</n-descriptions> </n-descriptions>
</n-card> </n-card>
<n-card :bordered="false" title="其它信息" class="proCard mt-4" size="small" :segmented="{content: 'hard'}"> <n-card :bordered="false" title="其它信息" class="proCard mt-4" size="small" :segmented="{content: 'hard'}">
<n-descriptions label-placement="left" class="py-2"> <n-descriptions label-placement="left" class="py-2">
<n-descriptions-item><template #label>城市</template>深圳</n-descriptions-item> <n-descriptions-item>
<template #label>城市</template>
深圳
</n-descriptions-item>
<n-descriptions-item label="性别"></n-descriptions-item> <n-descriptions-item label="性别"></n-descriptions-item>
<n-descriptions-item label="邮箱">NaiveUiAdmin@qq.com</n-descriptions-item> <n-descriptions-item label="邮箱">NaiveUiAdmin@qq.com</n-descriptions-item>
<n-descriptions-item label="地址">广东省深圳市南山区</n-descriptions-item> <n-descriptions-item label="地址">广东省深圳市南山区</n-descriptions-item>
<n-descriptions-item label="生日">1991-06-04</n-descriptions-item> <n-descriptions-item label="生日">1991-06-04</n-descriptions-item>
<n-descriptions-item label="认证"><n-tag type="success"> 已认证 </n-tag></n-descriptions-item> <n-descriptions-item label="认证">
<n-tag type="success"> 已认证</n-tag>
</n-descriptions-item>
</n-descriptions> </n-descriptions>
</n-card> </n-card>
<n-card :bordered="false" title="表格信息" class="proCard mt-4" size="small" :segmented="{content: 'hard'}"> <n-card :bordered="false" title="表格信息" class="proCard mt-4" size="small" :segmented="{content: 'hard'}">

View File

@@ -25,7 +25,11 @@ export const columns = [
}, },
{ {
title: '地址', title: '地址',
key: 'address' key: 'address',
auth: ['basic_list'], // 同时根据权限控制是否显示
ifShow: (_column) => {
return true; // 根据业务控制是否显示
},
}, },
{ {
title: '开始日期', title: '开始日期',
@@ -39,34 +43,35 @@ export const columns = [
title: '创建时间', title: '创建时间',
key: 'date', key: 'date',
}, },
{ // {
title: '操作', // title: '操作',
key: 'actions', // key: 'actions',
width:150, // width: 150,
//简单写一下例子,不建议这么写,过段时间,这里封二次封装 // //简单写一下例子,不建议这么写,过段时间,这里封二次封装
render() { // render() {
return [ // return [
h( // h(
NButton, // NButton,
{ // {
size: 'small', // size: 'small',
type:'error', // type: 'error',
style:'margin-right:10px', // style: 'margin-right:10px',
onClick: () => { // onClick: () => {
} // }
}, // },
{ default: () => '删除' } // { default: () => '删除' }
), // ),
h( // h(
NButton, // NButton,
{ // {
size: 'small', // size: 'small',
onClick: () => { // onClick: () => {
} //
}, // }
{ default: () => '编辑' } // },
) // { default: () => '编辑' }
] // )
} // ]
} // }
// }
] ]

View File

@@ -1,10 +1,11 @@
<template> <template>
<n-card :bordered="false" class="proCard"> <n-card :bordered="false" class="proCard">
<ProTable <BasicTable
:columns="columns" :columns="columns"
:request="loadDataTable" :request="loadDataTable"
:row-key="row => row.id" :row-key="row => row.id"
ref="actionRef" ref="actionRef"
:actionColumn="actionColumn"
@update:checked-row-keys="onCheckedRow" @update:checked-row-keys="onCheckedRow"
> >
<template #tableTitle> <template #tableTitle>
@@ -17,10 +18,15 @@
新建 新建
</n-button> </n-button>
</template> </template>
<template #toolbar> <template #toolbar>
<n-button type="primary" @click="reloadTable">刷新数据</n-button> <n-button type="primary" @click="reloadTable">刷新数据</n-button>
</template> </template>
</ProTable>
<template #action="{ record }">
<TableAction></TableAction>
</template>
</BasicTable>
<n-modal v-model:show="showModal" :show-icon="false" preset="dialog" title="新建"> <n-modal v-model:show="showModal" :show-icon="false" preset="dialog" title="新建">
<n-form <n-form
@@ -57,10 +63,11 @@
<script lang="ts"> <script lang="ts">
import { defineComponent, reactive, toRefs, ref, h } from 'vue' import { defineComponent, reactive, toRefs, ref, h } from 'vue'
import { useMessage } from 'naive-ui' import { useMessage } from 'naive-ui'
import { ProTable } from '@/components/ProTable' import { BasicTable, TableAction } from '@/components/Table'
import { getTableList } from '@/api/table/list' import { getTableList } from '@/api/table/list'
import { columns } from './columns' import { columns } from './columns'
import { PlusOutlined } from '@vicons/antd' import { PlusOutlined } from '@vicons/antd'
import { useRouter } from 'vue-router'
const rules = { const rules = {
name: { name: {
@@ -82,8 +89,9 @@ const rules = {
} }
export default defineComponent({ export default defineComponent({
components: { ProTable, PlusOutlined }, components: { BasicTable, PlusOutlined, TableAction },
setup() { setup() {
const router = useRouter()
const formRef: any = ref(null) const formRef: any = ref(null)
const message = useMessage() const message = useMessage()
const actionRef = ref() const actionRef = ref()
@@ -99,6 +107,62 @@ export default defineComponent({
pageSize: 5, pageSize: 5,
name: 'xiaoMa' name: 'xiaoMa'
}, },
actionColumn: {
width: 250,
title: '操作',
dataIndex: 'action',
fixed: 'right',
render(record) {
return h(
TableAction,
{
style: 'button',
actions: [
{
label: '删除',
icon: 'ic:outline-delete-outline',
onClick: handleDelete.bind(null, record),
// 根据业务控制是否显示 isShow 和 auth 是并且关系
ifShow: () => {
return true;
},
// 根据权限控制是否显示: 有权限,会显示,支持多个
auth: ['basic_list'],
},
{
label: '编辑',
onClick: handleEdit.bind(null, record),
ifShow: () => {
return true;
},
auth: ['basic_list'],
},
],
dropDownActions: [
{
label: '启用',
key: 'enabled',
// 根据业务控制是否显示: 非enable状态的不显示启用按钮
ifShow: (record) => {
return true;
},
},
{
label: '禁用',
key: 'disabled',
ifShow: () => {
return true
},
}
],
select:(key) => {
message.info(`您点击了,${key} 按钮`)
}
}
)
}
},
}) })
function addTable() { function addTable() {
@@ -135,6 +199,21 @@ export default defineComponent({
}) })
} }
function handleEdit(record: Recordable) {
console.log('点击了编辑', record);
router.push({ name: 'basic-info', params: { id: record.id } })
}
function handleDelete(record: Recordable) {
console.log('点击了删除', record);
message.info('点击了删除')
}
function handleOpen(record: Recordable) {
console.log('点击了启用', record);
message.info('点击了删除')
}
return { return {
...toRefs(state), ...toRefs(state),
formRef, formRef,
@@ -145,7 +224,10 @@ export default defineComponent({
loadDataTable, loadDataTable,
onCheckedRow, onCheckedRow,
reloadTable, reloadTable,
addTable addTable,
handleEdit,
handleDelete,
handleOpen
} }
} }
}) })

View File

@@ -0,0 +1,38 @@
<template>
<div>
<div class="n-layout-page-header">
<n-card :bordered="false" title="基础详情">
基础详情有时也用于显示只读信息
</n-card>
</div>
<n-card :bordered="false" class="proCard mt-4" size="small" :segmented="{content: 'hard'}">
<n-descriptions label-placement="left" class="py-2">
<n-descriptions-item>
<template #label>收款人姓名</template>
啊俊
</n-descriptions-item>
<n-descriptions-item label="收款账户">NaiveUiAdmin@qq.com</n-descriptions-item>
<n-descriptions-item label="付款类型">支付宝</n-descriptions-item>
<n-descriptions-item label="付款账户">NaiveUiAdmin@163.com</n-descriptions-item>
<n-descriptions-item label="转账金额">1980.00</n-descriptions-item>
<n-descriptions-item label="状态">
<n-tag type="success"> 已到账</n-tag>
</n-descriptions-item>
</n-descriptions>
</n-card>
</div>
</template>
<script>
import { defineComponent, ref } from 'vue'
export default defineComponent({
setup() {
return {}
}
})
</script>
<style lang="less" scoped>
</style>

View File

@@ -28,6 +28,17 @@
</template> </template>
</n-input> </n-input>
</n-form-item> </n-form-item>
<n-form-item
path="isCaptcha">
<div class="w-full">
<mi-captcha
width="384"
theme-color="#2d8cf0"
:logo="logo"
@success="onAuthCode"
></mi-captcha>
</div>
</n-form-item>
<n-form-item class="default-color"> <n-form-item class="default-color">
<div class="flex justify-between"> <div class="flex justify-between">
<div class="flex-initial"> <div class="flex-initial">
@@ -80,6 +91,7 @@ import { useRoute, useRouter } from 'vue-router'
import { useUserStore } from '@/store/modules/user' import { useUserStore } from '@/store/modules/user'
import { useMessage } from 'naive-ui' import { useMessage } from 'naive-ui'
import { ResultEnum } from '@/enums/httpEnum' import { ResultEnum } from '@/enums/httpEnum'
import logo from '@/assets/images/logo.png'
interface FormState { interface FormState {
username: string username: string
@@ -96,12 +108,18 @@ export default defineComponent({
autoLogin: true, autoLogin: true,
formInline: { formInline: {
username: 'admin', username: 'admin',
password: '123456' password: '123456',
isCaptcha: true
} }
}) })
const rules = { const rules = {
username: { required: true, message: '请输入用户名', trigger: 'blur' }, username: { required: true, message: '请输入用户名', trigger: 'blur' },
password: { required: true, message: '请输入密码', trigger: 'blur' } password: { required: true, message: '请输入密码', trigger: 'blur' },
isCaptcha: {
required: true, type: 'boolean', trigger: 'change',
message: '请点击按钮进行验证码校验',
validator: (_, value) => value === true,
},
} }
const userStore = useUserStore(); const userStore = useUserStore();
@@ -135,15 +153,22 @@ export default defineComponent({
message.info(msg || '登录失败') message.info(msg || '登录失败')
} }
} else { } else {
message.error('请填写完整信息') message.error('请填写完整信息,并且进行验证码校验')
} }
}) })
} }
function onAuthCode() {
state.formInline.isCaptcha = true
}
return { return {
...toRefs(state), ...toRefs(state),
formRef, formRef,
rules, rules,
handleSubmit logo,
handleSubmit,
onAuthCode
} }
} }
}) })
@@ -167,14 +192,9 @@ export default defineComponent({
padding: 32px 0; padding: 32px 0;
text-align: center; text-align: center;
&-logo {
height: 75px;
}
&-desc { &-desc {
font-size: 14px; font-size: 14px;
color: #808695; color: #808695;
margin-top: 20px;
} }
} }

View File

@@ -59,6 +59,7 @@ export default defineComponent({
margin: 0 auto; margin: 0 auto;
text-align: center; text-align: center;
padding-top: 5px; padding-top: 5px;
&-extra { &-extra {
padding: 24px 40px; padding: 24px 40px;
text-align: left; text-align: left;

View File

@@ -59,6 +59,7 @@ export default defineComponent({
margin: 0 auto; margin: 0 auto;
text-align: center; text-align: center;
padding-top: 5px; padding-top: 5px;
&-extra { &-extra {
padding: 24px 40px; padding: 24px 40px;
text-align: left; text-align: left;

View File

@@ -39,6 +39,7 @@ export default defineComponent({
margin: 0 auto; margin: 0 auto;
text-align: center; text-align: center;
padding-top: 5px; padding-top: 5px;
&-extra { &-extra {
padding: 24px 40px; padding: 24px 40px;
text-align: left; text-align: left;

View File

@@ -0,0 +1,88 @@
<template>
<div>
<n-grid :x-gap="24">
<n-grid-item span="6">
<n-card :bordered="false" size="small" class="proCard">
<n-thing
class="thing-cell"
v-for="item in typeTabList" :key="item.key"
:class="{'thing-cell-on':type===item.key}"
@click="switchType(item)"
>
<template #header>{{ item.name }}</template>
<template #description>{{ item.desc }}</template>
</n-thing>
</n-card>
</n-grid-item>
<n-grid-item span="18">
<n-card :bordered="false" size="small" :title="typeTitle" class="proCard">
<BasicSetting v-if="type === 1"></BasicSetting>
<SafetySetting v-if="type === 2"></SafetySetting>
</n-card>
</n-grid-item>
</n-grid>
</div>
</template>
<script lang="ts">
import { defineComponent, reactive, toRefs } from 'vue'
import BasicSetting from "./BasicSetting.vue"
import SafetySetting from "./SafetySetting.vue"
const typeTabList = [
{
name: '基本设置',
desc: '个人账户信息设置',
key: 1
},
{
name: '安全设置',
desc: '密码,邮箱等设置',
key: 2
}
]
export default defineComponent({
components: { BasicSetting, SafetySetting },
setup() {
const state = reactive({
type: 1,
typeTitle: '基本设置'
})
function switchType(e) {
state.type = e.key
state.typeTitle = e.name
}
return {
...toRefs(state),
switchType,
typeTabList
}
}
})
</script>
<style lang="less" scoped>
.thing-cell {
margin: 0 -16px 10px;
padding: 5px 16px;
&:hover {
background: #f3f3f3;
cursor: pointer
}
}
.thing-cell-on {
background: #f0faff;
color: #2d8cf0;
::v-deep(.n-thing-main .n-thing-header .n-thing-header__title) {
color: #2d8cf0
}
&:hover {
background: #f0faff;
}
}
</style>

View File

@@ -0,0 +1,138 @@
<template>
<n-grid cols="2 s:2 m:2 l:3 xl:3 2xl:3" responsive="screen">
<n-grid-item>
<n-form
:label-width="80"
:model="formValue"
:rules="rules"
ref="formRef"
>
<n-form-item label="网站名称" path="name">
<n-input v-model:value="formValue.name" placeholder="请输入网站名称"/>
</n-form-item>
<n-form-item label="备案编号" path="icpCode">
<n-input placeholder="请输入备案编号" v-model:value="formValue.icpCode"/>
</n-form-item>
<n-form-item label="联系电话" path="mobile">
<n-input placeholder="请输入联系电话" v-model:value="formValue.mobile"/>
</n-form-item>
<n-form-item label="联系地址" path="address">
<n-input
v-model:value="formValue.address"
type="textarea"
placeholder="请输入联系地址"
/>
</n-form-item>
<n-form-item label="登录验证码" path="loginCode">
<n-radio-group v-model:value="formValue.loginCode" name="loginCode">
<n-space>
<n-radio :value="1">开启</n-radio>
<n-radio :value="0">关闭</n-radio>
</n-space>
</n-radio-group>
</n-form-item>
<n-form-item label="网站开启访问" path="systemOpen">
<n-switch size="large" v-model:value="formValue.systemOpen" @update:value="systemOpenChange"/>
</n-form-item>
<n-form-item label="网站关闭提示" path="closeText">
<n-input
v-model:value="formValue.closeText"
type="textarea"
placeholder="请输入网站关闭提示"
/>
</n-form-item>
<div>
<n-space>
<n-button type="primary" @click="formSubmit">更新基本信息</n-button>
</n-space>
</div>
</n-form>
</n-grid-item>
</n-grid>
</template>
<script lang="ts">
import { defineComponent, reactive, ref, toRefs } from 'vue'
import { useDialog, useMessage } from 'naive-ui'
const rules = {
name: {
required: true,
message: '请输入网站名称',
trigger: 'blur'
},
mobile: {
required: true,
message: '请输入联系电话',
trigger: 'input'
},
}
export default defineComponent({
setup() {
const formRef = ref(null)
const message = useMessage()
const dialog = useDialog()
const state = reactive({
formValue: {
name: '',
mobile: '',
icpCode: '',
address: '',
loginCode: 0,
closeText: '网站维护中,暂时无法访问!本网站正在进行系统维护和技术升级,网站暂时无法访问,敬请谅解!',
systemOpen: true
}
})
function systemOpenChange(value) {
if (!value) {
dialog.warning({
title: '提示',
content: '您确定要关闭系统访问吗?该操作立马生效,请慎重操作!',
positiveText: '确定',
negativeText: '取消',
onPositiveClick: () => {
message.success('操作成功')
},
onNegativeClick: () => {
state.formValue.systemOpen = true
}
})
}
}
function formSubmit(e) {
formRef.value.validate((errors) => {
if (!errors) {
message.success('验证成功')
} else {
message.error('验证失败,请填写完整信息')
}
})
}
function resetForm() {
formRef.value.restoreValidation()
}
return {
formRef,
...toRefs(state),
rules,
formSubmit,
resetForm,
systemOpenChange
}
}
})
</script>

View File

@@ -0,0 +1,85 @@
<template>
<n-grid cols="2 s:2 m:2 l:3 xl:3 2xl:3" responsive="screen">
<n-grid-item>
<n-form
:label-width="120"
:model="formValue"
:rules="rules"
ref="formRef"
>
<n-form-item label="发件人邮箱" path="originator">
<n-input v-model:value="formValue.originator" placeholder="请输入发件人邮箱"/>
</n-form-item>
<n-form-item label="SMTP服务器地址">
<n-input placeholder="请输入SMTP服务器地址"/>
</n-form-item>
<n-form-item label="SMTP服务器端口">
<n-input placeholder="请输入SMTP服务器端口"/>
</n-form-item>
<n-form-item label="SMTP用户名">
<n-input placeholder="请输入SMTP用户名"/>
</n-form-item>
<n-form-item label="SMTP密码">
<n-input type="password" placeholder="请输入SMTP密码"/>
</n-form-item>
<n-form-item label="邮件测试">
<n-button>邮件测试</n-button>
</n-form-item>
<div>
<n-space>
<n-button type="primary" @click="formSubmit">更新邮件信息</n-button>
</n-space>
</div>
</n-form>
</n-grid-item>
</n-grid>
</template>
<script lang="ts">
import { defineComponent, reactive, ref, toRefs } from 'vue'
import { useMessage } from 'naive-ui'
const rules = {
originator: {
required: true,
message: '请输入发件人邮箱',
trigger: 'blur'
}
}
export default defineComponent({
setup() {
const formRef: any = ref(null)
const message = useMessage()
const state = reactive({
formValue: {
originator: '',
}
})
function formSubmit() {
formRef.value.validate((errors) => {
if (!errors) {
message.success('验证成功')
} else {
message.error('验证失败,请填写完整信息')
}
})
}
return {
formRef,
...toRefs(state),
rules,
formSubmit
}
}
})
</script>

View File

@@ -0,0 +1,200 @@
<template>
<n-grid cols="2 s:2 m:2 l:3 xl:3 2xl:3" responsive="screen">
<n-grid-item>
<n-form
:label-width="120"
:model="formValue"
:rules="rules"
ref="formRef"
>
<n-form-item label="商品图片(大)">
<n-space align="center">
<span>宽度</span>
<n-input v-model:value="formValue.bigWidth" style="width:80px" placeholder="宽度像素"/>
<span>高度</span>
<n-input v-model:value="formValue.bigHeight" style="width:80px" placeholder="高度像素"/>
</n-space>
</n-form-item>
<n-form-item label="商品图片(小)">
<n-space align="center">
<span>宽度</span>
<n-input v-model:value="formValue.smallWidth" style="width:80px" placeholder="宽度像素"/>
<span>高度</span>
<n-input v-model:value="formValue.smallHeight" style="width:80px" placeholder="高度像素"/>
</n-space>
</n-form-item>
<n-form-item label="水印透明度" path="watermarkClarity">
<n-input-number v-model:value="formValue.watermarkClarity" :show-button="false" placeholder="请输入水印透明度"/>
</n-form-item>
<n-form-item label="水印图片" path="watermarkClarity">
<n-upload action="http://www.mocky.io/v2/5e4bafc63100007100d8b70f">
<n-button>上传文件</n-button>
</n-upload>
</n-form-item>
<n-form-item label="水印位置" path="watermarkPlace">
<n-select placeholder="请选择价格精确方式"
:options="watermarkPlaceList"
v-model:value="formValue.watermarkPlace"
/>
</n-form-item>
<n-form-item label="价格精确位数" path="pricePreciseNum">
<n-select placeholder="请选择价格精确位数"
:options="pricePreciseNumList"
v-model:value="formValue.pricePreciseNum"
/>
</n-form-item>
<n-form-item label="价格精确方式" path="pricePrecise">
<n-select placeholder="请选择价格精确方式"
:options="pricePreciseList"
v-model:value="formValue.pricePrecise"
/>
</n-form-item>
<n-form-item label="前台显示市场价" path="isMarketPrice">
<n-switch size="large" v-model:value="formValue.isMarketPrice"/>
</n-form-item>
<div>
<n-space>
<n-button type="primary" @click="formSubmit">更新显示信息</n-button>
</n-space>
</div>
</n-form>
</n-grid-item>
</n-grid>
</template>
<script lang="ts">
import { defineComponent, reactive, ref, toRefs } from 'vue'
import { useDialog, useMessage } from 'naive-ui'
const rules = {
name: {
required: true,
message: '请输入网站名称',
trigger: 'blur'
},
mobile: {
required: true,
message: '请输入联系电话',
trigger: 'input'
},
}
const watermarkPlaceList = [
{
label: '左上',
value: 1
},
{
label: '右上',
value: 2
},
{
label: '居中',
value: 3
},
{
label: '右下',
value: 4
},
]
const pricePreciseNumList = [
{
label: '2位',
value: 1
},
{
label: '3位',
value: 2
},
{
label: '4位',
value: 3
},
]
const pricePreciseList = [
{
label: '四舍五入',
value: 1
},
{
label: '向上取整',
value: 2
},
{
label: '向下取整',
value: 3
},
]
export default defineComponent({
setup() {
const formRef: any = ref(null)
const message = useMessage()
const dialog = useDialog()
const state = reactive({
formValue: {
bigWidth: '',
bigHeight: '',
smallWidth: '',
smallHeight: '',
watermarkClarity: null,
pricePrecise: 1,
isMarketPrice: true,
pricePreciseNum: null,
}
})
function systemOpenChange(value) {
if (!value) {
dialog.warning({
title: '提示',
content: '您确定要关闭系统访问吗?该操作立马生效,请慎重操作!',
positiveText: '确定',
negativeText: '取消',
onPositiveClick: () => {
message.success('操作成功')
},
onNegativeClick: () => {
state.formValue.systemOpen = true
}
})
}
}
function formSubmit(e) {
formRef.value.validate((errors) => {
if (!errors) {
message.success('验证成功')
} else {
message.error('验证失败,请填写完整信息')
}
})
}
function resetForm() {
formRef.value.restoreValidation()
}
return {
formRef,
...toRefs(state),
pricePreciseList,
watermarkPlaceList,
pricePreciseNumList,
rules,
formSubmit,
resetForm,
systemOpenChange
}
}
})
</script>

View File

@@ -0,0 +1,96 @@
<template>
<div>
<n-grid :x-gap="24">
<n-grid-item span="6">
<n-card :bordered="false" size="small" class="proCard">
<n-thing
class="thing-cell"
v-for="item in typeTabList" :key="item.key"
:class="{'thing-cell-on':type===item.key}"
@click="switchType(item)"
>
<template #header>{{ item.name }}</template>
<template #description>{{ item.desc }}</template>
</n-thing>
</n-card>
</n-grid-item>
<n-grid-item span="18">
<n-card :bordered="false" size="small" :title="typeTitle" class="proCard">
<BasicSetting v-if="type === 1"></BasicSetting>
<RevealSetting v-if="type === 2"></RevealSetting>
<EmailSetting v-if="type === 3"></EmailSetting>
</n-card>
</n-grid-item>
</n-grid>
</div>
</template>
<script lang="ts">
import { defineComponent, reactive, toRefs } from 'vue'
import { useRouter } from 'vue-router'
import BasicSetting from "./BasicSetting.vue"
import RevealSetting from "./RevealSetting.vue"
import EmailSetting from "./EmailSetting.vue"
const typeTabList = [
{
name: '基本设置',
desc: '系统常规设置',
key: 1
},
{
name: '显示设置',
desc: '系统显示设置',
key: 2
},
{
name: '邮件设置',
desc: '系统邮件设置',
key: 3
}
]
export default defineComponent({
components: { BasicSetting, RevealSetting, EmailSetting },
setup() {
const router = useRouter()
const state = reactive({
type: 1,
typeTitle: '基本设置'
})
function switchType(e) {
state.type = e.key
state.typeTitle = e.name
}
return {
...toRefs(state),
switchType,
typeTabList
}
}
})
</script>
<style lang="less" scoped>
.thing-cell {
margin: 0 -16px 10px;
padding: 5px 16px;
&:hover {
background: #f3f3f3;
cursor: pointer
}
}
.thing-cell-on {
background: #f0faff;
color: #2d8cf0;
::v-deep(.n-thing-main .n-thing-header .n-thing-header__title) {
color: #2d8cf0
}
&:hover {
background: #f0faff;
}
}
</style>

View File

@@ -0,0 +1,137 @@
<template>
<n-drawer v-model:show="isDrawer" :width="width" :placement="placement">
<n-drawer-content :title="title" closable>
<n-form
:model="formParams"
:rules="rules"
ref="formRef"
label-placement="left"
:label-width="100"
>
<n-form-item label="类型" path="type">
<span>{{ formParams.type === 1 ? '侧边栏菜单' : '' }}</span>
</n-form-item>
<n-form-item label="标题" path="label">
<n-input placeholder="请输入标题" v-model:value="formParams.label"/>
</n-form-item>
<n-form-item label="副标题" path="subtitle">
<n-input placeholder="请输入副标题" v-model:value="formParams.subtitle"/>
</n-form-item>
<n-form-item label="路径" path="path">
<n-input placeholder="请输入路径" v-model:value="formParams.path"/>
</n-form-item>
<n-form-item label="打开方式" path="openType">
<n-radio-group v-model:value="formParams.openType" name="openType">
<n-space>
<n-radio :value="1">当前窗口</n-radio>
<n-radio :value="2">新窗口</n-radio>
</n-space>
</n-radio-group>
</n-form-item>
<n-form-item label="菜单权限" path="auth">
<n-input placeholder="请输入权限,多个权限用,分割" v-model:value="formParams.auth"/>
</n-form-item>
<n-form-item label="隐藏侧边栏" path="hidden">
<n-switch v-model:value="formParams.hidden"/>
</n-form-item>
</n-form>
<template #footer>
<n-space>
<n-button type="primary" :loading="subLoading" @click="formSubmit">提交</n-button>
<n-button @click="handleReset">重置</n-button>
</n-space>
</template>
</n-drawer-content>
</n-drawer>
</template>
<script lang="ts">
import { defineComponent, reactive, ref, toRefs, watch, createVNode, computed, unref } from 'vue'
import { CheckOutlined } from '@vicons/antd'
import { useMessage } from 'naive-ui'
const rules = {
label: {
required: true,
message: '请输入标题',
trigger: 'blur'
},
path: {
required: true,
message: '请输入路径',
trigger: 'blur'
}
}
export default defineComponent({
name: 'CreateDrawer',
props: {
title: {
type: String,
default: '添加顶级菜单'
},
width: {
type: Number,
default: 450
},
},
components: { CheckOutlined },
setup(props, { emit }) {
const message = useMessage()
const formRef: any = ref(null)
const defaultValueRef = () => ({
label: '',
type: 1,
subtitle: '',
openType: 1,
auth: '',
path: '',
hidden: false
})
const state = reactive({
isDrawer: false,
subLoading: false,
formParams: defaultValueRef(),
placement: "right",
alertText: '该功能主要实时预览各种布局效果,更多完整配置在 projectSetting.ts 中设置,建议在生产环境关闭该布局预览功能。',
})
function openDrawer(isDrawer) {
state.isDrawer = true
}
function closeDrawer() {
state.isDrawer = false
}
function formSubmit() {
formRef.value.validate((errors) => {
if (!errors) {
message.success('添加成功')
handleReset()
closeDrawer()
} else {
message.error('请填写完整信息')
}
})
}
function handleReset() {
formRef.value.restoreValidation()
state.formParams = Object.assign(state.formParams, defaultValueRef())
}
return {
...toRefs(state),
formRef,
rules,
formSubmit,
handleReset,
openDrawer,
closeDrawer
}
}
})
</script>

View File

@@ -0,0 +1,268 @@
<template>
<div>
<div class="n-layout-page-header">
<n-card :bordered="false" title="菜单权限管理">
页面数据为 Mock 示例数据非真实数据
</n-card>
</div>
<n-grid class="mt-4" cols="1 s:1 m:1 l:3 xl:3 2xl:3" responsive="screen" :x-gap="12">
<n-gi span="1">
<n-card :segmented="{ content: 'hard' }" :bordered="false" size="small">
<template #header>
<n-space>
<n-dropdown trigger="hover" @select="selectAddMenu" :options="addMenuOptions">
<n-button type="info" ghost icon-placement="right">
添加菜单
<template #icon>
<div class="flex items-center">
<n-icon size="14">
<DownOutlined/>
</n-icon>
</div>
</template>
</n-button>
</n-dropdown>
<n-button type="info" ghost icon-placement="left" @click="packHandle">
全部{{ expandedKeys.length ? '收起' : '展开' }}
<template #icon>
<div class="flex items-center">
<n-icon size="14">
<AlignLeftOutlined/>
</n-icon>
</div>
</template>
</n-button>
</n-space>
</template>
<div class="w-full menu">
<n-input type="input" v-model:value="pattern" placeholder="输入菜单名称搜索">
<template #suffix>
<n-icon size="18" class="cursor-pointer">
<SearchOutlined/>
</n-icon>
</template>
</n-input>
<div class="py-3 menu-list">
<template v-if="loading">
<div class="flex items-center justify-center py-4">
<n-spin size="medium"/>
</div>
</template>
<template v-else>
<n-tree
block-line
cascade
checkable
:virtual-scroll="true"
:pattern="pattern"
:data="treeData"
:expandedKeys="expandedKeys"
style="max-height: 650px;overflow: hidden"
@update:selected-keys="selectedTree"
/>
</template>
</div>
</div>
</n-card>
</n-gi>
<n-gi span="2">
<n-card :segmented="{ content: 'hard' }" :bordered="false" size="small">
<template #header>
<n-space>
<n-icon size="18">
<FormOutlined />
</n-icon>
<span>编辑菜单{{ treeItemTitle ? `${treeItemTitle}`:''}}</span>
</n-space>
</template>
<n-alert type="info" closable>
从菜单列表选择一项后进行编辑
</n-alert>
<n-form
:model="formParams"
:rules="rules"
ref="formRef"
label-placement="left"
:label-width="100"
v-if="isEditMenu"
class="py-4"
>
<n-form-item label="类型" path="type">
<span>{{ formParams.type === 1 ? '侧边栏菜单' : '' }}</span>
</n-form-item>
<n-form-item label="标题" path="label">
<n-input placeholder="请输入标题" v-model:value="formParams.label"/>
</n-form-item>
<n-form-item label="副标题" path="subtitle">
<n-input placeholder="请输入副标题" v-model:value="formParams.subtitle"/>
</n-form-item>
<n-form-item label="路径" path="path">
<n-input placeholder="请输入路径" v-model:value="formParams.path"/>
</n-form-item>
<n-form-item label="打开方式" path="openType">
<n-radio-group v-model:value="formParams.openType" name="openType">
<n-space>
<n-radio :value="1">当前窗口</n-radio>
<n-radio :value="2">新窗口</n-radio>
</n-space>
</n-radio-group>
</n-form-item>
<n-form-item label="菜单权限" path="auth">
<n-input placeholder="请输入权限,多个权限用,分割" v-model:value="formParams.auth"/>
</n-form-item>
<n-form-item path="auth" style="margin-left: 100px">
<n-space>
<n-button type="primary" :loading="subLoading" @click="formSubmit">保存修改</n-button>
<n-button @click="handleReset">重置</n-button>
</n-space>
</n-form-item>
</n-form>
</n-card>
</n-gi>
</n-grid>
<CreateDrawer ref="createDrawerRef" :title="drawerTitle"></CreateDrawer>
</div>
</template>
<script lang="ts">
import { defineComponent, ref, unref, reactive, toRefs, onMounted, computed } from 'vue'
import { useMessage } from 'naive-ui'
import { DownOutlined, AlignLeftOutlined, SearchOutlined, FormOutlined } from '@vicons/antd'
import { getMenuList } from '@/api/system/menu'
import { getTreeItem } from '@/utils/index'
import CreateDrawer from './CreateDrawer.vue'
const rules = {
label: {
required: true,
message: '请输入标题',
trigger: 'blur'
},
path: {
required: true,
message: '请输入路径',
trigger: 'blur'
}
}
export default defineComponent({
components: { DownOutlined, AlignLeftOutlined, SearchOutlined, FormOutlined, CreateDrawer },
setup() {
const formRef: any = ref(null)
const createDrawerRef = ref()
const message = useMessage()
const isAddSon = computed(() => {
return state.treeItemKey.length ? false : true
})
const state = reactive({
addMenuOptions: [
{
label: '添加顶级菜单',
key: 'home',
disabled: false
},
{
label: '添加子菜单',
key: 'son',
disabled: isAddSon
},
],
loading: true,
subLoading: false,
isEditMenu: false,
treeData: [],
treeItemKey: [],
treeItemTitle: '',
expandedKeys: [],
formParams: {},
pattern: '',
drawerTitle: ''
})
function selectAddMenu(key) {
state.drawerTitle = key === 'home' ? '添加顶栏菜单' : `添加子菜单:${ state.treeItemTitle }`
openCreateDrawer()
}
function openCreateDrawer() {
const { openDrawer } = createDrawerRef.value
openDrawer()
}
function selectedTree(keys) {
if (keys.length) {
const treeItem = getTreeItem(unref(state.treeData), keys[0])
state.treeItemKey = keys
state.treeItemTitle = treeItem.label
state.formParams = Object.assign(state.formParams, treeItem)
state.isEditMenu = true
} else {
state.isEditMenu = false
state.treeItemKey = []
state.treeItemTitle = ''
}
}
function handleReset() {
const treeItem = getTreeItem(unref(state.treeData), state.treeItemKey[0])
state.formParams = Object.assign(state.formParams, treeItem)
}
function formSubmit() {
formRef.value.validate((errors) => {
if (!errors) {
message.error('抱歉,您没有该权限')
} else {
message.error('请填写完整信息')
}
})
}
function packHandle() {
if (state.expandedKeys.length) {
state.expandedKeys = []
} else {
state.expandedKeys = state.treeData.map(item => item.key)
}
}
function onExpandedKeys(keys) {
// let key = keys[0]
// console.log(state.expandedKeys)
// if(state.expandedKeys.includes(key)){
// state.expandedKeys.splice(state.expandedKeys.findIndex(item => item === key), 1)
// console.log(state.expandedKeys)
// }else{
// state.expandedKeys.push(key)
// }
}
onMounted(async () => {
const treeMenuList = await getMenuList()
state.expandedKeys = treeMenuList.list.map(item => item.key)
state.treeData = treeMenuList.list
state.loading = false
})
return {
...toRefs(state),
createDrawerRef,
formRef,
rules,
selectedTree,
handleReset,
formSubmit,
packHandle,
onExpandedKeys,
selectAddMenu
}
}
})
</script>

View File

@@ -0,0 +1,36 @@
import { h } from 'vue'
import { NTag, NButton } from 'naive-ui'
export const columns = [
{
title: 'id',
key: 'id'
},
{
title: '角色名称',
key: 'name'
},
{
title: '说明',
key: 'explain'
},
{
title: '是否默认角色',
key: 'isDefault',
render(row) {
return h(
NTag,
{
type: row.isDefault ? 'success' : 'error'
},
{
default: () => row.isDefault ? '是' : '否'
}
)
}
},
{
title: '创建时间',
key: 'create_date'
}
]

View File

@@ -0,0 +1,274 @@
<template>
<div>
<div class="n-layout-page-header">
<n-card :bordered="false" title="角色权限管理">
页面数据为 Mock 示例数据非真实数据
</n-card>
</div>
<n-card :bordered="false" class="mt-4 proCard">
<BasicTable
:columns="columns"
:request="loadDataTable"
:row-key="row => row.id"
ref="actionRef"
:actionColumn="actionColumn"
@update:checked-row-keys="onCheckedRow"
>
<template #tableTitle>
<n-button type="primary">
<template #icon>
<n-icon>
<PlusOutlined/>
</n-icon>
</template>
添加角色
</n-button>
</template>
<template #action="{ record }">
<TableAction></TableAction>
</template>
</BasicTable>
</n-card>
<n-modal v-model:show="showModal" :show-icon="false" preset="dialog" :title="editRoleTitle">
<div class="py-3 menu-list">
<n-tree
block-line
cascade
checkable
:virtual-scroll="true"
:data="treeData"
:expandedKeys="expandedKeys"
:checked-keys="checkedKeys"
style="max-height: 950px;overflow: hidden"
@update:checked-keys="checkedTree"
/>
</div>
<template #action>
<n-space>
<n-button type="info" ghost icon-placement="left" @click="packHandle">
全部{{ expandedKeys.length ? '收起' : '展开' }}
</n-button>
<n-button type="info" ghost icon-placement="left" @click="checkedAllHandle">
全部{{ checkedAll ? '取消' : '选择' }}
</n-button>
<n-button type="primary" :loading="formBtnLoading" @click="confirmForm">提交</n-button>
</n-space>
</template>
</n-modal>
</div>
</template>
<script lang="ts">
import { defineComponent, reactive, toRefs, ref, h, onMounted } from 'vue'
import { useMessage } from 'naive-ui'
import { DownOutlined, AlignLeftOutlined, SearchOutlined, FormOutlined } from '@vicons/antd'
import { BasicTable, TableAction } from '@/components/Table'
import { getRoleList } from '@/api/system/role'
import { getMenuList } from '@/api/system/menu'
import { columns } from './columns'
import { PlusOutlined } from '@vicons/antd'
import { getTreeAll } from "@/utils";
const rules = {
name: {
required: true,
trigger: ['blur', 'input'],
message: '请输入名称'
},
address: {
required: true,
trigger: ['blur', 'input'],
message: '请输入地址'
},
date: {
type: 'number',
required: true,
trigger: ['blur', 'change'],
message: '请选择日期'
},
}
export default defineComponent({
components: { BasicTable, TableAction, PlusOutlined, AlignLeftOutlined },
setup() {
const formRef: any = ref(null)
const message = useMessage()
const actionRef = ref()
const state = reactive({
showModal: false,
formBtnLoading: false,
checkedAll: false,
editRoleTitle: '',
treeData: [],
expandedKeys: [],
checkedKeys: ['console', 'step-form'],
formParams: {
name: '',
address: '',
date: []
},
params: {
pageSize: 5,
name: 'xiaoMa'
},
actionColumn: {
width: 250,
title: '操作',
dataIndex: 'action',
fixed: 'right',
render(record) {
return h(
TableAction,
{
style: 'button',
actions: [
{
label: '菜单权限',
onClick: handleMenuAuth.bind(null, record),
// 根据业务控制是否显示 isShow 和 auth 是并且关系
ifShow: () => {
return true;
},
// 根据权限控制是否显示: 有权限,会显示,支持多个
auth: ['basic_list'],
},
{
label: '编辑',
onClick: handleEdit.bind(null, record),
ifShow: () => {
return true;
},
auth: ['basic_list'],
},
{
label: '删除',
icon: 'ic:outline-delete-outline',
onClick: handleDelete.bind(null, record),
// 根据业务控制是否显示 isShow 和 auth 是并且关系
ifShow: () => {
return true;
},
// 根据权限控制是否显示: 有权限,会显示,支持多个
auth: ['basic_list'],
},
]
}
)
}
},
})
const loadDataTable = async (params) => {
const data = await getRoleList(params);
return data
}
function onCheckedRow(rowKeys) {
console.log(rowKeys)
}
function reloadTable() {
actionRef.value.reload()
}
function confirmForm(e) {
e.preventDefault()
state.formBtnLoading = true
formRef.value.validate((errors) => {
if (!errors) {
message.success('新建成功')
setTimeout(() => {
state.showModal = false
reloadTable()
})
} else {
message.error('请填写完整信息')
}
state.formBtnLoading = false
})
}
function handleEdit(record: Recordable) {
console.log('点击了编辑', record);
router.push({ name: 'basic-info', params: { id: record.id } })
}
function handleDelete(record: Recordable) {
console.log('点击了删除', record);
message.info('点击了删除')
}
function handleOpen(record: Recordable) {
console.log('点击了启用', record);
message.info('点击了删除')
}
function handleMenuAuth(record: Recordable) {
state.editRoleTitle = `分配 ${ record.name } 的菜单权限`
state.checkedKeys = record.menu_keys
state.showModal = true
}
function checkedTree(keys) {
state.checkedKeys = [state.checkedKeys, ...keys]
}
function packHandle() {
if (state.expandedKeys.length) {
state.expandedKeys = []
} else {
state.expandedKeys = state.treeData.map(item => item.key)
}
}
function checkedAllHandle() {
if (!state.checkedAll) {
state.checkedKeys = getTreeAll(state.treeData)
state.checkedAll = true
} else {
state.checkedKeys = []
state.checkedAll = false
}
}
onMounted(async () => {
const treeMenuList = await getMenuList()
state.expandedKeys = treeMenuList.list.map(item => item.key)
state.treeData = treeMenuList.list
})
return {
...toRefs(state),
formRef,
columns,
rules,
actionRef,
confirmForm,
loadDataTable,
onCheckedRow,
reloadTable,
handleEdit,
handleDelete,
handleOpen,
handleMenuAuth,
checkedTree,
packHandle,
checkedAllHandle
}
}
})
</script>
<style lang='less' scoped>
</style>

View File

@@ -3,7 +3,7 @@ module.exports = {
// darkMode: 'class', // darkMode: 'class',
plugins: [createEnterPlugin()], plugins: [createEnterPlugin()],
purge: { purge: {
enabled: false, enable: process.env.NODE_ENV === 'production',
content: ['./index.html', './src/**/*.{vue,ts,tsx}'], content: ['./index.html', './src/**/*.{vue,ts,tsx}'],
}, },
theme: { theme: {
@@ -11,7 +11,6 @@ module.exports = {
zIndex: { zIndex: {
'-1': '-1', '-1': '-1',
}, },
},
colors: { colors: {
primary: { primary: {
DEFAULT: '#0960bd', DEFAULT: '#0960bd',
@@ -26,6 +25,7 @@ module.exports = {
'2xl': '1600px', '2xl': '1600px',
}, },
}, },
},
}; };
/** /**
* Used for animation when the element is displayed * Used for animation when the element is displayed

View File

@@ -31,9 +31,8 @@
"noImplicitAny": false, "noImplicitAny": false,
"skipLibCheck": true, "skipLibCheck": true,
"paths": { "paths": {
"@/*": [ "@/*": ["src/*"],
"src/*" "/#/*": ["types/*"]
]
} }
}, },
"include": [ "include": [

51
types/config.d.ts vendored
View File

@@ -1,10 +1,45 @@
export interface ProjectSettingState { export interface ProjectSettingState {
navMode: string,//导航模式 //导航模式
navTheme: string,//导航风格 navMode: string;
headerSetting: object,//顶部设置 //导航风格
showFooter: boolean, //页脚 navTheme: string;
menuSetting: object, //多标签 //顶部设置
multiTabsSetting: object,//多标签 headerSetting: object;
crumbsSetting: object,//面包屑 //页脚
permissionMode: string//权限模式 showFooter: boolean;
//菜单设置
menuSetting: object;
//多标签
multiTabsSetting: object;
//面包屑
crumbsSetting: object;
//权限模式
permissionMode: string;
}
export interface GlobConfig {
title: string;
apiUrl: string;
shortName: string;
urlPrefix?: string;
uploadUrl?: string;
prodMock: boolean;
imgUrl?: string;
}
export interface GlobEnvConfig {
// 标题
VITE_GLOB_APP_TITLE: string;
// 接口地址
VITE_GLOB_API_URL: string;
// 接口前缀
VITE_GLOB_API_URL_PREFIX?: string;
// Project abbreviation
VITE_GLOB_APP_SHORT_NAME: string;
// 图片上传地址
VITE_GLOB_UPLOAD_URL?: string;
//图片前缀地址
VITE_GLOB_IMG_URL?: string;
//生产环境开启mock
VITE_GLOB_PROD_MOCK: boolean;
} }

2
types/global.d.ts vendored
View File

@@ -64,6 +64,8 @@ declare global {
VITE_GLOB_APP_TITLE: string; VITE_GLOB_APP_TITLE: string;
VITE_GLOB_APP_SHORT_NAME: string; VITE_GLOB_APP_SHORT_NAME: string;
VITE_DROP_CONSOLE: boolean; VITE_DROP_CONSOLE: boolean;
VITE_GLOB_PROD_MOCK: boolean;
VITE_GLOB_IMG_URL: string;
} }
declare function parseInt(s: string | number, radix?: number): number; declare function parseInt(s: string | number, radix?: number): number;

View File

@@ -13,11 +13,11 @@ export default ({ command, mode }: ConfigEnv): UserConfig => {
const root = process.cwd(); const root = process.cwd();
const env = loadEnv(mode, root); const env = loadEnv(mode, root);
const viteEnv = wrapperEnv(env); const viteEnv = wrapperEnv(env);
const { VITE_BASE_URL, VITE_DROP_CONSOLE, VITE_PORT, VITE_GLOB_PROD_MOCK } = viteEnv const { VITE_PUBLIC_PATH, VITE_DROP_CONSOLE, VITE_PORT, VITE_GLOB_PROD_MOCK } = viteEnv
const prodMock = VITE_GLOB_PROD_MOCK const prodMock = VITE_GLOB_PROD_MOCK
const isBuild = command === 'build'; const isBuild = command === 'build';
return { return {
base: VITE_BASE_URL, base: VITE_PUBLIC_PATH,
esbuild: {}, esbuild: {},
resolve: { resolve: {
alias: [ alias: [

188
yarn.lock
View File

@@ -2,6 +2,29 @@
# yarn lockfile v1 # yarn lockfile v1
"@ant-design/colors@^5.0.0":
version "5.1.1"
resolved "https://registry.npm.taobao.org/@ant-design/colors/download/@ant-design/colors-5.1.1.tgz?cache=0&sync_timestamp=1612935637470&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2F%40ant-design%2Fcolors%2Fdownload%2F%40ant-design%2Fcolors-5.1.1.tgz#800b2186b1e27e66432e67d03ed96af3e21d8940"
integrity sha1-gAshhrHifmZDLmfQPtlq8+IdiUA=
dependencies:
"@ctrl/tinycolor" "^3.3.1"
"@ant-design/icons-svg@^4.0.0":
version "4.1.0"
resolved "https://registry.nlark.com/@ant-design/icons-svg/download/@ant-design/icons-svg-4.1.0.tgz#480b025f4b20ef7fe8f47d4a4846e4fee84ea06c"
integrity sha1-SAsCX0sg73/o9H1KSEbk/uhOoGw=
"@ant-design/icons-vue@^5.1.9":
version "5.1.9"
resolved "https://registry.npm.taobao.org/@ant-design/icons-vue/download/@ant-design/icons-vue-5.1.9.tgz#8d741a3290be61af7c71618c308cc1a946c4e434"
integrity sha1-jXQaMpC+Ya98cWGMMIzBqUbE5DQ=
dependencies:
"@ant-design/colors" "^5.0.0"
"@ant-design/icons-svg" "^4.0.0"
"@babel/runtime" "^7.10.4"
"@types/lodash" "^4.14.165"
lodash "^4.17.15"
"@babel/code-frame@7.12.11": "@babel/code-frame@7.12.11":
version "7.12.11" version "7.12.11"
resolved "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.11.tgz#f4ad435aa263db935b8f10f2c552d23fb716a63f" resolved "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.11.tgz#f4ad435aa263db935b8f10f2c552d23fb716a63f"
@@ -230,6 +253,13 @@
"@babel/helper-plugin-utils" "^7.14.5" "@babel/helper-plugin-utils" "^7.14.5"
"@babel/plugin-syntax-typescript" "^7.14.5" "@babel/plugin-syntax-typescript" "^7.14.5"
"@babel/runtime@^7.10.4":
version "7.14.6"
resolved "https://registry.nlark.com/@babel/runtime/download/@babel/runtime-7.14.6.tgz?cache=0&sync_timestamp=1623708247115&other_urls=https%3A%2F%2Fregistry.nlark.com%2F%40babel%2Fruntime%2Fdownload%2F%40babel%2Fruntime-7.14.6.tgz#535203bc0892efc7dec60bdc27b2ecf6e409062d"
integrity sha1-U1IDvAiS78fexgvcJ7Ls9uQJBi0=
dependencies:
regenerator-runtime "^0.13.4"
"@babel/template@^7.0.0", "@babel/template@^7.14.5": "@babel/template@^7.0.0", "@babel/template@^7.14.5":
version "7.14.5" version "7.14.5"
resolved "https://registry.npmjs.org/@babel/template/-/template-7.14.5.tgz#a9bc9d8b33354ff6e55a9c60d1109200a68974f4" resolved "https://registry.npmjs.org/@babel/template/-/template-7.14.5.tgz#a9bc9d8b33354ff6e55a9c60d1109200a68974f4"
@@ -411,6 +441,11 @@
resolved "https://registry.nlark.com/@css-render/vue3-ssr/download/@css-render/vue3-ssr-0.15.4.tgz#ae29bc76f7eb40830d0ecd8086ce3726a1c6e8e3" resolved "https://registry.nlark.com/@css-render/vue3-ssr/download/@css-render/vue3-ssr-0.15.4.tgz#ae29bc76f7eb40830d0ecd8086ce3726a1c6e8e3"
integrity sha1-rim8dvfrQIMNDs2Ahs43JqHG6OM= integrity sha1-rim8dvfrQIMNDs2Ahs43JqHG6OM=
"@ctrl/tinycolor@^3.3.1":
version "3.4.0"
resolved "https://registry.nlark.com/@ctrl/tinycolor/download/@ctrl/tinycolor-3.4.0.tgz#c3c5ae543c897caa9c2a68630bed355be5f9990f"
integrity sha1-w8WuVDyJfKqcKmhjC+01W+X5mQ8=
"@emotion/hash@~0.8.0": "@emotion/hash@~0.8.0":
version "0.8.0" version "0.8.0"
resolved "https://registry.npm.taobao.org/@emotion/hash/download/@emotion/hash-0.8.0.tgz#bbbff68978fefdbe68ccb533bc8cbe1d1afb5413" resolved "https://registry.npm.taobao.org/@emotion/hash/download/@emotion/hash-0.8.0.tgz#bbbff68978fefdbe68ccb533bc8cbe1d1afb5413"
@@ -470,10 +505,10 @@
"@nodelib/fs.scandir" "2.1.5" "@nodelib/fs.scandir" "2.1.5"
fastq "^1.6.0" fastq "^1.6.0"
"@rollup/plugin-node-resolve@^13.0.0": "@rollup/plugin-node-resolve@^13.0.2":
version "13.0.0" version "13.0.2"
resolved "https://registry.nlark.com/@rollup/plugin-node-resolve/download/@rollup/plugin-node-resolve-13.0.0.tgz#352f07e430ff377809ec8ec8a6fd636547162dc4" resolved "https://registry.nlark.com/@rollup/plugin-node-resolve/download/@rollup/plugin-node-resolve-13.0.2.tgz?cache=0&sync_timestamp=1626393688566&other_urls=https%3A%2F%2Fregistry.nlark.com%2F%40rollup%2Fplugin-node-resolve%2Fdownload%2F%40rollup%2Fplugin-node-resolve-13.0.2.tgz#bfe58e9bfc7284ee0fabc6da7fabcb268f04e0a4"
integrity sha1-NS8H5DD/N3gJ7I7Ipv1jZUcWLcQ= integrity sha1-v+WOm/xyhO4Pq8baf6vLJo8E4KQ=
dependencies: dependencies:
"@rollup/pluginutils" "^3.1.0" "@rollup/pluginutils" "^3.1.0"
"@types/resolve" "1.17.1" "@types/resolve" "1.17.1"
@@ -563,6 +598,11 @@
resolved "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.170.tgz#0d67711d4bf7f4ca5147e9091b847479b87925d6" resolved "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.170.tgz#0d67711d4bf7f4ca5147e9091b847479b87925d6"
integrity sha512-bpcvu/MKHHeYX+qeEN8GE7DIravODWdACVA1ctevD8CN24RhPZIKMn9ntfAsrvLfSX3cR5RrBKAbYm9bGs0A+Q== integrity sha512-bpcvu/MKHHeYX+qeEN8GE7DIravODWdACVA1ctevD8CN24RhPZIKMn9ntfAsrvLfSX3cR5RrBKAbYm9bGs0A+Q==
"@types/lodash@^4.14.165":
version "4.14.171"
resolved "https://registry.nlark.com/@types/lodash/download/@types/lodash-4.14.171.tgz?cache=0&sync_timestamp=1625609589008&other_urls=https%3A%2F%2Fregistry.nlark.com%2F%40types%2Flodash%2Fdownload%2F%40types%2Flodash-4.14.171.tgz#f01b3a5fe3499e34b622c362a46a609fdb23573b"
integrity sha1-8Bs6X+NJnjS2IsNipGpgn9sjVzs=
"@types/mdast@^3.0.0": "@types/mdast@^3.0.0":
version "3.0.3" version "3.0.3"
resolved "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.3.tgz#2d7d671b1cd1ea3deb306ea75036c2a0407d2deb" resolved "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.3.tgz#2d7d671b1cd1ea3deb306ea75036c2a0407d2deb"
@@ -580,10 +620,10 @@
resolved "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.1.tgz#283f669ff76d7b8260df8ab7a4262cc83d988256" resolved "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.1.tgz#283f669ff76d7b8260df8ab7a4262cc83d988256"
integrity sha512-fZQQafSREFyuZcdWFAExYjBiCL7AUCdgsk80iO0q4yihYYdcIiH28CcuPTGFgLOCC8RlW49GSQxdHwZP+I7CNg== integrity sha512-fZQQafSREFyuZcdWFAExYjBiCL7AUCdgsk80iO0q4yihYYdcIiH28CcuPTGFgLOCC8RlW49GSQxdHwZP+I7CNg==
"@types/mockjs@^1.0.3": "@types/mockjs@^1.0.4":
version "1.0.3" version "1.0.4"
resolved "https://registry.nlark.com/@types/mockjs/download/@types/mockjs-1.0.3.tgz?cache=0&sync_timestamp=1621241889026&other_urls=https%3A%2F%2Fregistry.nlark.com%2F%40types%2Fmockjs%2Fdownload%2F%40types%2Fmockjs-1.0.3.tgz#bd8ee3c7cbbd9a18788ab677b9e4f97c8d0bb0bf" resolved "https://registry.nlark.com/@types/mockjs/download/@types/mockjs-1.0.4.tgz?cache=0&sync_timestamp=1625771765667&other_urls=https%3A%2F%2Fregistry.nlark.com%2F%40types%2Fmockjs%2Fdownload%2F%40types%2Fmockjs-1.0.4.tgz#e706951d5e33b4f0a4bb73b1f8b124e26f081de0"
integrity sha1-vY7jx8u9mhh4irZ3ueT5fI0LsL8= integrity sha1-5waVHV4ztPCku3Ox+LEk4m8IHeA=
"@types/node@*": "@types/node@*":
version "15.12.4" version "15.12.4"
@@ -778,6 +818,17 @@
estree-walker "^2.0.1" estree-walker "^2.0.1"
source-map "^0.6.1" source-map "^0.6.1"
"@vue/compiler-core@3.1.5":
version "3.1.5"
resolved "https://registry.nlark.com/@vue/compiler-core/download/@vue/compiler-core-3.1.5.tgz#298f905b6065d6d81ff63756f98c60876b393c87"
integrity sha1-KY+QW2Bl1tgf9jdW+Yxgh2s5PIc=
dependencies:
"@babel/parser" "^7.12.0"
"@babel/types" "^7.12.0"
"@vue/shared" "3.1.5"
estree-walker "^2.0.1"
source-map "^0.6.1"
"@vue/compiler-dom@3.1.1": "@vue/compiler-dom@3.1.1":
version "3.1.1" version "3.1.1"
resolved "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.1.1.tgz#ef60d856ac2ede5b2ad5c72a7a68122895e3d652" resolved "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.1.1.tgz#ef60d856ac2ede5b2ad5c72a7a68122895e3d652"
@@ -794,6 +845,14 @@
"@vue/compiler-core" "3.1.2" "@vue/compiler-core" "3.1.2"
"@vue/shared" "3.1.2" "@vue/shared" "3.1.2"
"@vue/compiler-dom@3.1.5":
version "3.1.5"
resolved "https://registry.nlark.com/@vue/compiler-dom/download/@vue/compiler-dom-3.1.5.tgz#cbb97020c62a5faa3fbc2a97916bd98041ac9856"
integrity sha1-y7lwIMYqX6o/vCqXkWvZgEGsmFY=
dependencies:
"@vue/compiler-core" "3.1.5"
"@vue/shared" "3.1.5"
"@vue/compiler-sfc@3.1.1": "@vue/compiler-sfc@3.1.1":
version "3.1.1" version "3.1.1"
resolved "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.1.1.tgz#d4e4507c013d0b219f0b106b317ec5bb1cde3398" resolved "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.1.1.tgz#d4e4507c013d0b219f0b106b317ec5bb1cde3398"
@@ -843,6 +902,13 @@
dependencies: dependencies:
"@vue/shared" "3.1.2" "@vue/shared" "3.1.2"
"@vue/reactivity@3.1.5":
version "3.1.5"
resolved "https://registry.nlark.com/@vue/reactivity/download/@vue/reactivity-3.1.5.tgz#dbec4d9557f7c8f25c2635db1e23a78a729eb991"
integrity sha1-2+xNlVf3yPJcJjXbHiOninKeuZE=
dependencies:
"@vue/shared" "3.1.5"
"@vue/runtime-core@3.1.2": "@vue/runtime-core@3.1.2":
version "3.1.2" version "3.1.2"
resolved "https://registry.nlark.com/@vue/runtime-core/download/@vue/runtime-core-3.1.2.tgz#f4dbc503cfc9a02ab5f1ebe002c3322512064a54" resolved "https://registry.nlark.com/@vue/runtime-core/download/@vue/runtime-core-3.1.2.tgz#f4dbc503cfc9a02ab5f1ebe002c3322512064a54"
@@ -851,6 +917,14 @@
"@vue/reactivity" "3.1.2" "@vue/reactivity" "3.1.2"
"@vue/shared" "3.1.2" "@vue/shared" "3.1.2"
"@vue/runtime-core@3.1.5":
version "3.1.5"
resolved "https://registry.nlark.com/@vue/runtime-core/download/@vue/runtime-core-3.1.5.tgz#a545b7f146092929cb5e833e85439150f17ac87b"
integrity sha1-pUW38UYJKSnLXoM+hUORUPF6yHs=
dependencies:
"@vue/reactivity" "3.1.5"
"@vue/shared" "3.1.5"
"@vue/runtime-dom@3.1.2": "@vue/runtime-dom@3.1.2":
version "3.1.2" version "3.1.2"
resolved "https://registry.nlark.com/@vue/runtime-dom/download/@vue/runtime-dom-3.1.2.tgz#0fd8724f14bc7ba64b6c954d874a8d8a4fcb5fe9" resolved "https://registry.nlark.com/@vue/runtime-dom/download/@vue/runtime-dom-3.1.2.tgz#0fd8724f14bc7ba64b6c954d874a8d8a4fcb5fe9"
@@ -860,6 +934,15 @@
"@vue/shared" "3.1.2" "@vue/shared" "3.1.2"
csstype "^2.6.8" csstype "^2.6.8"
"@vue/runtime-dom@3.1.5":
version "3.1.5"
resolved "https://registry.nlark.com/@vue/runtime-dom/download/@vue/runtime-dom-3.1.5.tgz#4fa28947d408aa368fa17ea0edc1beb9af1472a1"
integrity sha1-T6KJR9QIqjaPoX6g7cG+ua8UcqE=
dependencies:
"@vue/runtime-core" "3.1.5"
"@vue/shared" "3.1.5"
csstype "^2.6.8"
"@vue/shared@3.1.1": "@vue/shared@3.1.1":
version "3.1.1" version "3.1.1"
resolved "https://registry.npmjs.org/@vue/shared/-/shared-3.1.1.tgz#2287cfc3dc20e5b20aeb65c2c3a56533bdca801c" resolved "https://registry.npmjs.org/@vue/shared/-/shared-3.1.1.tgz#2287cfc3dc20e5b20aeb65c2c3a56533bdca801c"
@@ -870,6 +953,11 @@
resolved "https://registry.nlark.com/@vue/shared/download/@vue/shared-3.1.2.tgz#1069c0bc7d6f4bd15ccf3a5f3be29450aca368f9" resolved "https://registry.nlark.com/@vue/shared/download/@vue/shared-3.1.2.tgz#1069c0bc7d6f4bd15ccf3a5f3be29450aca368f9"
integrity sha1-EGnAvH1vS9FczzpfO+KUUKyjaPk= integrity sha1-EGnAvH1vS9FczzpfO+KUUKyjaPk=
"@vue/shared@3.1.5":
version "3.1.5"
resolved "https://registry.nlark.com/@vue/shared/download/@vue/shared-3.1.5.tgz#74ee3aad995d0a3996a6bb9533d4d280514ede03"
integrity sha1-dO46rZldCjmWpruVM9TSgFFO3gM=
"@vueuse/core@^5.0.3": "@vueuse/core@^5.0.3":
version "5.0.3" version "5.0.3"
resolved "https://registry.nlark.com/@vueuse/core/download/@vueuse/core-5.0.3.tgz#8f3170e2a51ae62fb1725c84d4cc02a7552aad0b" resolved "https://registry.nlark.com/@vueuse/core/download/@vueuse/core-5.0.3.tgz#8f3170e2a51ae62fb1725c84d4cc02a7552aad0b"
@@ -1928,6 +2016,11 @@ esbuild@0.11.3:
resolved "https://registry.nlark.com/esbuild/download/esbuild-0.11.3.tgz#b57165b907be4ffba651f6450538ce8d8c1d5eb0" resolved "https://registry.nlark.com/esbuild/download/esbuild-0.11.3.tgz#b57165b907be4ffba651f6450538ce8d8c1d5eb0"
integrity sha1-tXFluQe+T/umUfZFBTjOjYwdXrA= integrity sha1-tXFluQe+T/umUfZFBTjOjYwdXrA=
esbuild@^0.12.5:
version "0.12.15"
resolved "https://registry.nlark.com/esbuild/download/esbuild-0.12.15.tgz?cache=0&sync_timestamp=1625545660518&other_urls=https%3A%2F%2Fregistry.nlark.com%2Fesbuild%2Fdownload%2Fesbuild-0.12.15.tgz#9d99cf39aeb2188265c5983e983e236829f08af0"
integrity sha1-nZnPOa6yGIJlxZg+mD4jaCnwivA=
esbuild@^0.12.6, esbuild@^0.12.8: esbuild@^0.12.6, esbuild@^0.12.8:
version "0.12.14" version "0.12.14"
resolved "https://registry.nlark.com/esbuild/download/esbuild-0.12.14.tgz?cache=0&sync_timestamp=1625183314696&other_urls=https%3A%2F%2Fregistry.nlark.com%2Fesbuild%2Fdownload%2Fesbuild-0.12.14.tgz#43157dbd0b36d939247d4eb4909a4886ac40f82e" resolved "https://registry.nlark.com/esbuild/download/esbuild-0.12.14.tgz?cache=0&sync_timestamp=1625183314696&other_urls=https%3A%2F%2Fregistry.nlark.com%2Fesbuild%2Fdownload%2Fesbuild-0.12.14.tgz#43157dbd0b36d939247d4eb4909a4886ac40f82e"
@@ -2209,7 +2302,7 @@ fast-glob@^3.1.1, fast-glob@^3.2.5:
micromatch "^4.0.2" micromatch "^4.0.2"
picomatch "^2.2.1" picomatch "^2.2.1"
fast-glob@^3.2.6: fast-glob@^3.2.7:
version "3.2.7" version "3.2.7"
resolved "https://registry.nlark.com/fast-glob/download/fast-glob-3.2.7.tgz#fd6cb7a2d7e9aa7a7846111e85a196d6b2f766a1" resolved "https://registry.nlark.com/fast-glob/download/fast-glob-3.2.7.tgz#fd6cb7a2d7e9aa7a7846111e85a196d6b2f766a1"
integrity sha1-/Wy3otfpqnp4RhEehaGW1rL3ZqE= integrity sha1-/Wy3otfpqnp4RhEehaGW1rL3ZqE=
@@ -2961,6 +3054,11 @@ is-plain-obj@^2.0.0:
resolved "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz#45e42e37fccf1f40da8e5f76ee21515840c09287" resolved "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz#45e42e37fccf1f40da8e5f76ee21515840c09287"
integrity sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA== integrity sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==
is-plain-object@3.0.1:
version "3.0.1"
resolved "https://registry.nlark.com/is-plain-object/download/is-plain-object-3.0.1.tgz#662d92d24c0aa4302407b0d45d21f2251c85f85b"
integrity sha1-Zi2S0kwKpDAkB7DUXSHyJRyF+Fs=
is-plain-object@5.0.0: is-plain-object@5.0.0:
version "5.0.0" version "5.0.0"
resolved "https://registry.nlark.com/is-plain-object/download/is-plain-object-5.0.0.tgz#4427f50ab3429e9025ea7d52e9043a9ef4159344" resolved "https://registry.nlark.com/is-plain-object/download/is-plain-object-5.0.0.tgz#4427f50ab3429e9025ea7d52e9043a9ef4159344"
@@ -3351,6 +3449,25 @@ make-dir@^3.0.2:
dependencies: dependencies:
semver "^6.0.0" semver "^6.0.0"
makeit-captcha@^1.2.5:
version "1.2.5"
resolved "https://registry.nlark.com/makeit-captcha/download/makeit-captcha-1.2.5.tgz#f5055edbff35d5e1a0dccede7953f24f6aabc20c"
integrity sha1-9QVe2/811eGg3M7eeVPyT2qrwgw=
dependencies:
"@ant-design/icons-vue" "^5.1.9"
axios "^0.21.1"
makeit-tooltip "^1.1.2"
vue "^3.0.4"
vue-types "^3.0.1"
makeit-tooltip@^1.1.2:
version "1.1.2"
resolved "https://registry.nlark.com/makeit-tooltip/download/makeit-tooltip-1.1.2.tgz#b265f8586c2b1ef9bab4e6d141cbaccb8eba73bc"
integrity sha1-smX4WGwrHvm6tObRQcusy466c7w=
dependencies:
vue "^3.0.4"
vue-types "^3.0.1"
map-obj@^1.0.0: map-obj@^1.0.0:
version "1.0.1" version "1.0.1"
resolved "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz#d933ceb9205d82bdcf4886f6742bdc2b4dea146d" resolved "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz#d933ceb9205d82bdcf4886f6742bdc2b4dea146d"
@@ -4092,7 +4209,7 @@ postcss@^8.1.10:
nanoid "^3.1.23" nanoid "^3.1.23"
source-map-js "^0.6.2" source-map-js "^0.6.2"
postcss@^8.1.6, postcss@^8.2.1, postcss@^8.3.5: postcss@^8.1.6, postcss@^8.2.1, postcss@^8.2.10, postcss@^8.3.5:
version "8.3.5" version "8.3.5"
resolved "https://registry.nlark.com/postcss/download/postcss-8.3.5.tgz#982216b113412bc20a86289e91eb994952a5b709" resolved "https://registry.nlark.com/postcss/download/postcss-8.3.5.tgz#982216b113412bc20a86289e91eb994952a5b709"
integrity sha1-mCIWsRNBK8IKhiiekeuZSVKltwk= integrity sha1-mCIWsRNBK8IKhiiekeuZSVKltwk=
@@ -4274,6 +4391,11 @@ reduce-css-calc@^2.1.8:
css-unit-converter "^1.1.1" css-unit-converter "^1.1.1"
postcss-value-parser "^3.3.0" postcss-value-parser "^3.3.0"
regenerator-runtime@^0.13.4:
version "0.13.7"
resolved "https://registry.nlark.com/regenerator-runtime/download/regenerator-runtime-0.13.7.tgz#cac2dacc8a1ea675feaabaeb8ae833898ae46f55"
integrity sha1-ysLazIoepnX+qrrriugziYrkb1U=
regexpp@^3.1.0: regexpp@^3.1.0:
version "3.1.0" version "3.1.0"
resolved "https://registry.npmjs.org/regexpp/-/regexpp-3.1.0.tgz#206d0ad0a5648cffbdb8ae46438f3dc51c9f78e2" resolved "https://registry.npmjs.org/regexpp/-/regexpp-3.1.0.tgz#206d0ad0a5648cffbdb8ae46438f3dc51c9f78e2"
@@ -5190,19 +5312,19 @@ vite-plugin-html@^2.0.7:
fs-extra "^9.1.0" fs-extra "^9.1.0"
html-minifier-terser "^5.1.1" html-minifier-terser "^5.1.1"
vite-plugin-mock@^2.9.1: vite-plugin-mock@^2.9.3:
version "2.9.1" version "2.9.3"
resolved "https://registry.nlark.com/vite-plugin-mock/download/vite-plugin-mock-2.9.1.tgz?cache=0&sync_timestamp=1625545918347&other_urls=https%3A%2F%2Fregistry.nlark.com%2Fvite-plugin-mock%2Fdownload%2Fvite-plugin-mock-2.9.1.tgz#bc67868082c0a7bfa13a3deb97f6a1bf57be12ff" resolved "https://registry.nlark.com/vite-plugin-mock/download/vite-plugin-mock-2.9.3.tgz#1afedd17a5a0a64c71acb7355f86977398f46912"
integrity sha1-vGeGgILAp7+hOj3rl/ahv1e+Ev8= integrity sha1-Gv7dF6WgpkxxrLc1X4aXc5j0aRI=
dependencies: dependencies:
"@rollup/plugin-node-resolve" "^13.0.0" "@rollup/plugin-node-resolve" "^13.0.2"
"@types/mockjs" "^1.0.3" "@types/mockjs" "^1.0.4"
chalk "^4.1.1" chalk "^4.1.1"
chokidar "^3.5.2" chokidar "^3.5.2"
connect "^3.7.0" connect "^3.7.0"
debug "^4.3.2" debug "^4.3.2"
esbuild "0.11.3" esbuild "0.11.3"
fast-glob "^3.2.6" fast-glob "^3.2.7"
path-to-regexp "^6.2.0" path-to-regexp "^6.2.0"
vite-plugin-style-import@^1.0.1: vite-plugin-style-import@^1.0.1:
@@ -5216,17 +5338,17 @@ vite-plugin-style-import@^1.0.1:
es-module-lexer "^0.6.0" es-module-lexer "^0.6.0"
magic-string "^0.25.7" magic-string "^0.25.7"
vite@^2.4.2: vite@2.3.6:
version "2.4.2" version "2.3.6"
resolved "https://registry.nlark.com/vite/download/vite-2.4.2.tgz#07d00615775c808530bc9f65641062b349b67929" resolved "https://registry.nlark.com/vite/download/vite-2.3.6.tgz#1f7cfde88a51a802d69000c7bac85d481c2e871c"
integrity sha1-B9AGFXdcgIUwvJ9lZBBis0m2eSk= integrity sha1-H3z96IpRqALWkADHushdSBwuhxw=
dependencies: dependencies:
esbuild "^0.12.8" esbuild "^0.12.5"
postcss "^8.3.5" postcss "^8.2.10"
resolve "^1.20.0" resolve "^1.19.0"
rollup "^2.38.5" rollup "^2.38.5"
optionalDependencies: optionalDependencies:
fsevents "~2.3.2" fsevents "~2.3.1"
vooks@^0.2.4, vooks@^0.2.6: vooks@^0.2.4, vooks@^0.2.6:
version "0.2.6" version "0.2.6"
@@ -5272,6 +5394,13 @@ vue-router@^4.0.10:
dependencies: dependencies:
"@vue/devtools-api" "^6.0.0-beta.14" "@vue/devtools-api" "^6.0.0-beta.14"
vue-types@^3.0.1:
version "3.0.2"
resolved "https://registry.nlark.com/vue-types/download/vue-types-3.0.2.tgz#ec16e05d412c038262fc1efa4ceb9647e7fb601d"
integrity sha1-7BbgXUEsA4Ji/B76TOuWR+f7YB0=
dependencies:
is-plain-object "3.0.1"
vue-types@^4.0.0: vue-types@^4.0.0:
version "4.0.0" version "4.0.0"
resolved "https://registry.nlark.com/vue-types/download/vue-types-4.0.0.tgz#da13ccca0f979d3cfd076ce3a2b7050a9627ed5a" resolved "https://registry.nlark.com/vue-types/download/vue-types-4.0.0.tgz#da13ccca0f979d3cfd076ce3a2b7050a9627ed5a"
@@ -5279,6 +5408,15 @@ vue-types@^4.0.0:
dependencies: dependencies:
is-plain-object "5.0.0" is-plain-object "5.0.0"
vue@^3.0.4:
version "3.1.5"
resolved "https://registry.nlark.com/vue/download/vue-3.1.5.tgz#12879b11d0685ee4478c8869551799630a52f9fe"
integrity sha1-EoebEdBoXuRHjIhpVReZYwpS+f4=
dependencies:
"@vue/compiler-dom" "3.1.5"
"@vue/runtime-dom" "3.1.5"
"@vue/shared" "3.1.5"
vue@^3.1.2: vue@^3.1.2:
version "3.1.2" version "3.1.2"
resolved "https://registry.nlark.com/vue/download/vue-3.1.2.tgz#647f8e3949a3d600771dca25d50225dc3e594c64" resolved "https://registry.nlark.com/vue/download/vue-3.1.2.tgz#647f8e3949a3d600771dca25d50225dc3e594c64"