feat: 修改 layout 布局,增加 demo 页面

This commit is contained in:
wangzhengkun 2022-08-10 18:55:07 +08:00
parent c4ab7c158d
commit 44eb1aa0de
46 changed files with 1968 additions and 745 deletions

View file

@ -1,6 +1,6 @@
module.exports = {
// 一行最多 80 字符
printWidth: 80,
// 一行最多 120 字符
printWidth: 120,
// 使用 4 个空格缩进
tabWidth: 4,
// 不使用 tab 缩进,而使用空格

File diff suppressed because it is too large Load diff

View file

@ -26,11 +26,14 @@
"echarts": "^5.3.0",
"echarts-liquidfill": "^3.1.0",
"element-plus": "^2.2.6",
"fit2cloud-ui-plus": "^0.0.1-beta.12",
"js-md5": "^0.7.3",
"nprogress": "^0.2.0",
"pinia": "^2.0.12",
"pinia-plugin-persistedstate": "^1.6.1",
"qs": "^6.10.3",
"sass-loader": "^13.0.2",
"unplugin-vue-define-options": "^0.7.3",
"vue": "^3.2.25",
"vue-i18n": "^9.1.9",
"vue-router": "^4.0.12",

View file

@ -11,17 +11,22 @@ export interface ResultData<T = any> extends Result {
// * 分页响应参数
export interface ResPage<T> {
datalist: T[];
pageNum: number;
pageSize: number;
items: T[];
total: number;
code: number;
msg?: '';
}
// * 分页请求参数
export interface ReqPage {
pageNum: number;
currentPage: number;
pageSize: number;
}
export interface CommonModel {
ID: number;
CreatedAt: string;
UpdatedAt: string;
}
// * 登录模块
export namespace Login {
@ -36,41 +41,6 @@ export namespace Login {
[propName: string]: any;
}
}
// * 用户管理模块
export namespace User {
export interface ReqGetUserParams extends ReqPage {
username: string;
gender: number;
idCard: string;
email: string;
address: string;
createTime: string[];
status: number;
}
export interface ResUserList {
id: string;
username: string;
gender: string;
age: number;
idCard: string;
email: string;
address: string;
createTime: string;
status: number;
avatar: string;
children?: ResUserList[];
}
export interface ResStatus {
userLabel: string;
userValue: number;
}
export interface ResGender {
genderLabel: string;
genderValue: number;
}
}
// * 文件上传模块
export namespace Upload {
export interface ResFileUrl {

View file

@ -0,0 +1,17 @@
import { CommonModel, ReqPage } from '.';
export namespace User {
export interface User extends CommonModel {
username: string;
email: string;
}
export interface UserCreate {
username: string;
email: string;
}
export interface ReqGetUserParams extends ReqPage {
username?: string;
email?: string;
}
}

View file

@ -1,59 +1,65 @@
import { ResPage, User } from '@/api/interface/index';
import { PORT1 } from '@/api/config/servicePort';
import http from '@/api';
import { User } from '../interface/user';
import UserDataList from '@/assets/json/user.json';
/**
* @name
*/
// * 获取用户列表
export const getUserList = (params: User.ReqGetUserParams) => {
return http.post<ResPage<User.ResUserList>>(PORT1 + `/user/list`, params);
console.log(params);
return UserDataList;
// return http.post<ResPage<User.User>>(`/users/list`, params);
};
// * 新增用户
export const addUser = (params: { id: string }) => {
return http.post(PORT1 + `/user/add`, params);
export const addUser = (params: User.UserCreate) => {
return http.post(`/users/add`, params);
};
// * 批量添加用户
export const BatchAddUser = (params: FormData) => {
return http.post(PORT1 + `/user/import`, params);
export const getUserById = (id: string) => {
return http.get(`/users/detail/${id}`);
};
// // * 批量添加用户
// export const BatchAddUser = (params: FormData) => {
// return http.post(`/users/import`, params);
// };
// * 编辑用户
export const editUser = (params: { id: string }) => {
return http.post(PORT1 + `/user/edit`, params);
export const editUser = (params: User.User) => {
return http.post(`/users/edit`, params);
};
// * 删除用户
export const deleteUser = (params: { id: string[] }) => {
return http.post(PORT1 + `/user/delete`, params);
// * 批量删除用户
export const deleteUser = (params: { ids: number[] }) => {
return http.post(`/users/delete`, params);
};
// * 切换用户状态
export const changeUserStatus = (params: { id: string; status: number }) => {
return http.post(PORT1 + `/user/change`, params);
};
// export const changeUserStatus = (params: { id: string; status: number }) => {
// return http.post(`/users/change`, params);
// * 重置用户密码
export const resetUserPassWord = (params: { id: string }) => {
return http.post(PORT1 + `/user/rest_password`, params);
};
// };
// * 导出用户数据
export const exportUserInfo = (params: User.ReqGetUserParams) => {
return http.post<BlobPart>(PORT1 + `/user/export`, params, {
responseType: 'blob',
});
};
// // * 重置用户密码
// export const resetUserPassWord = (params: { id: string }) => {
// return http.post(`/users/rest_password`, params);
// };
// * 获取用户状态
export const getUserStatus = () => {
return http.get<User.ResStatus>(PORT1 + `/user/status`);
};
// // * 导出用户数据
// export const exportUserInfo = (params: User.ReqGetUserParams) => {
// return http.post<BlobPart>(`/user/export`, params, {
// responseType: 'blob',
// });
// };
// * 获取用户性别字典
export const getUserGender = () => {
return http.get<User.ResGender>(PORT1 + `/user/gender`);
};
// // * 获取用户状态
// export const getUserStatus = () => {
// return http.get<User.ResStatus>(`/user/status`);
// };
// // * 获取用户性别字典
// export const getUserGender = () => {
// return http.get<User.ResGender>(`/user/gender`);
// };

View file

@ -0,0 +1,19 @@
{
"code": 200,
"items": [
{
"ID": 11232,
"name": "admin",
"email": "admin@fit2cloud.com",
"createdAt": "2022-08-10T00:00:20+08:00"
},
{
"ID": 11222232,
"name": "admin2",
"email": "admin2@fit2cloud.com",
"createdAt": "2022-08-10T00:00:20+08:00"
}
],
"total": 100,
"msg": ""
}

View file

@ -1,4 +1,7 @@
<template>
<div>
<slot></slot>
</div>
<div class="footer flx-center">
<a href="http://www.spicyboy.cn/" target="_blank">
2022 © 1Panel By 飞致云.

View file

@ -21,7 +21,6 @@
</div>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue';
import CollapseIcon from './components/CollapseIcon.vue';

View file

@ -0,0 +1,19 @@
<template>
<Layout>
<template #menu>
<Menu></Menu>
</template>
<template #header>
<Header></Header>
</template>
<template #footer>
<Footer></Footer>
</template>
</Layout>
</template>
<script setup lang="ts">
import Layout from '@/layout/index.vue';
import Header from './header/index.vue';
import Footer from './footer/index.vue';
import Menu from './menu/index.vue';
</script>

View file

@ -2,7 +2,6 @@
<div
class="menu"
:style="{ width: isCollapse ? '65px' : '220px' }"
v-loading="loading"
element-loading-text="Loading..."
:element-loading-spinner="loadingSvg"
element-loading-svg-view-box="-10, -10, 50, 50"

View file

@ -0,0 +1,39 @@
<template>
<el-icon class="back-button" @click="jump">
<Back />
</el-icon>
</template>
<script setup lang="ts">
import { useRouter } from 'vue-router';
const router = useRouter();
const props = defineProps({
path: String,
name: String,
to: Object,
});
function jump() {
const { path, name, to } = props;
if (path) {
router.push(path);
}
if (name) {
router.push({ name: name });
}
if (to) {
router.push(to);
}
}
</script>
<style lang="scss">
.back-button {
cursor: pointer;
margin-right: 10px;
font-weight: 600;
&:active {
transform: scale(0.85);
}
}
</style>

View file

@ -0,0 +1,93 @@
<template>
<div class="complex-table">
<div class="complex-table__header" v-if="$slots.header || header">
<slot name="header">{{ header }}</slot>
</div>
<div v-if="$slots.toolbar && !searchConfig" style="margin-bottom: 10px">
<slot name="toolbar"></slot>
</div>
<template v-if="searchConfig">
<fu-filter-bar v-bind="searchConfig" @exec="search">
<template #tl>
<slot name="toolbar"></slot>
</template>
<template #default>
<slot name="complex"></slot>
</template>
<template #buttons>
<slot name="buttons"></slot>
</template>
</fu-filter-bar>
</template>
<div class="complex-table__body">
<fu-table v-bind="$attrs" @selection-change="handleSelectionChange">
<slot></slot>
</fu-table>
</div>
<div
class="complex-table__pagination"
v-if="$slots.pagination || paginationConfig"
>
<slot name="pagination">
<fu-table-pagination
v-model:current-page="paginationConfig.currentPage"
v-model:page-size="paginationConfig.pageSize"
v-bind="paginationConfig"
@change="search"
/>
</slot>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
defineOptions({ name: 'ComplexTable' }); //
defineProps({
header: String,
searchConfig: Object,
paginationConfig: {
type: Object,
default: () => {},
},
});
const emit = defineEmits(['search', 'update:selects']);
const condition = ref({});
function search(conditions: any, e: any) {
if (conditions) {
condition.value = conditions;
}
emit('search', condition.value, e);
}
function handleSelectionChange(row: any) {
emit('update:selects', row);
}
</script>
<style lang="scss">
@use '@/styles/mixins.scss' as *;
.complex-table {
.complex-table__header {
@include flex-row(flex-start, center);
line-height: 60px;
font-size: 18px;
}
.complex-table__toolbar {
@include flex-row(space-between, center);
.fu-search-bar {
width: auto;
}
}
.complex-table__pagination {
margin-top: 20px;
@include flex-row(flex-end);
}
}
</style>

View file

@ -1,32 +1,37 @@
import { ElMessageBox, ElMessage } from 'element-plus';
import { HandleData } from './interface';
import i18n from '@/lang';
/**
* @description ()
* @description 使
* @param {Function} api api方法()
* @param {Object} params {id,params}()
* @param {String} message ()
* @param {String} confirmType icon类型(, warning)
* @return Promise
*/
export const useHandleData = <P = any, R = any>(
export const useDeleteData = <P = any, R = any>(
api: (params: P) => Promise<R>,
params: Parameters<typeof api>[0],
message: string,
confirmType: HandleData.MessageType = 'warning',
confirmType: HandleData.MessageType = 'error',
) => {
return new Promise((resolve, reject) => {
ElMessageBox.confirm(`是否${message}?`, '温馨提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: confirmType,
draggable: true,
}).then(async () => {
ElMessageBox.confirm(
i18n.global.t(`${message}`) + '?',
i18n.global.t('commons.msg.title'),
{
confirmButtonText: i18n.global.t('commons.button.confirm'),
cancelButtonText: i18n.global.t('commons.button.cancel'),
type: confirmType,
draggable: true,
},
).then(async () => {
const res = await api(params);
if (!res) return reject(false);
ElMessage({
type: 'success',
message: `${message}成功!`,
message: i18n.global.t('commons.msg.deleteSuccss'),
});
resolve(true);
});

View file

@ -1,6 +1,27 @@
export default {
commons: {
button: {
create: 'Create',
delete: 'Delete',
edit: 'Edit',
confirm: 'Confirm',
cancel: 'Cancel',
},
table: {
name: 'Name',
createdAt: 'Creation Time',
updatedAt: 'Update Time',
operate: 'Operations',
},
msg: {
delete: 'This operation cannot be rolled back. Do you want to continue',
title: 'Delete',
deleteSuccess: 'Delete Success',
},
},
menu: {
home: 'Dashboard',
demo: 'Demo',
},
home: {
welcome: 'Welcome',

View file

@ -1,6 +1,27 @@
export default {
commons: {
button: {
create: '创建',
delete: '删除',
edit: '编辑',
confirm: '确认',
cancel: '取消',
},
table: {
name: '名称',
createdAt: '创建时间',
updatedAt: '更新时间',
operate: '操作',
},
msg: {
delete: '此操作不可回滚,是否继续',
title: '删除',
deleteSuccess: '删除成功',
},
},
menu: {
home: '概览',
demo: '样例',
},
home: {

View file

@ -0,0 +1,52 @@
<template>
<div class="main-box">
<div class="content-container__header" v-if="slots.header || header">
<slot name="header">
<back-button
:path="backPath"
:name="backName"
:to="backTo"
v-if="showBack"
></back-button>
{{ header }}
</slot>
</div>
<div class="content-container__toolbar" v-if="slots.toolbar">
<slot name="toolbar"></slot>
</div>
<slot></slot>
</div>
</template>
<script setup lang="ts">
import { computed, useSlots } from 'vue';
import BackButton from '@/components/back-button/index.vue';
defineOptions({ name: 'LayoutContent' }); //
const slots = useSlots();
const prop = defineProps({
header: String,
backPath: String,
backName: String,
backTo: Object,
});
const showBack = computed(() => {
const { backPath, backName, backTo } = prop;
return backPath || backName || backTo;
});
</script>
<style lang="scss">
@use '@/styles/mixins.scss' as *;
.content-container__header {
font-weight: 700;
padding: 5px 0 25px;
font-size: 18px;
}
.content-container__toolbar {
@include flex-row(space-between, center);
margin-bottom: 10px;
}
</style>

View file

@ -0,0 +1,3 @@
<template>
<slot></slot>
</template>

View file

@ -0,0 +1,3 @@
<template>
<slot></slot>
</template>

View file

@ -0,0 +1,3 @@
<template>
<slot></slot>
</template>

View file

@ -0,0 +1,13 @@
<template>
<router-view v-slot="{ Component, route }">
<transition appear name="fade-transform" mode="out-in">
<keep-alive :include="cacheRouter">
<component :is="Component" :key="route.path"></component>
</keep-alive>
</transition>
</router-view>
</template>
<script setup lang="ts">
import cacheRouter from '@/routers/cacheRouter';
</script>

View file

@ -23,12 +23,12 @@
box-sizing: border-box;
width: 100%;
height: 100%;
padding: 20px;
padding: 15px;
overflow: auto;
overflow-x: hidden !important;
background-color: #ffffff;
border-radius: 4px;
box-shadow: 0 2px 12px 0 rgb(0 0 0 / 10%);
// box-shadow: 0 2px 12px 0 rgb(0 0 0 / 10%);
&::-webkit-scrollbar {
background-color: white;
}

View file

@ -1,28 +1,25 @@
<template>
<el-container>
<el-aside>
<Menu></Menu>
<Menu>
<slot name="menu"></slot>
</Menu>
</el-aside>
<el-container>
<el-header>
<Header></Header>
<Header>
<slot name="header"></slot>
</Header>
</el-header>
<el-main>
<section class="main-box">
<router-view v-slot="{ Component, route }">
<transition appear name="fade-transform" mode="out-in">
<keep-alive :include="cacheRouter">
<component
:is="Component"
:key="route.path"
></component>
</keep-alive>
</transition>
</router-view>
</section>
<Content>
<View></View>
</Content>
</el-main>
<el-footer v-if="themeConfig.footer">
<Footer></Footer>
<Footer>
<slot name="footer"></slot>
</Footer>
</el-footer>
</el-container>
</el-container>
@ -30,10 +27,11 @@
<script setup lang="ts">
import { computed } from 'vue';
import Menu from './Menu/index.vue';
import Header from './Header/index.vue';
import Footer from './Footer/index.vue';
import cacheRouter from '@/routers/cacheRouter';
import Menu from './LayoutMenu.vue';
import Header from './LayoutHeader.vue';
import Footer from './LayoutFooter.vue';
import View from './LayoutView.vue';
import Content from './LayoutContent.vue';
import { GlobalStore } from '@/store';
const globalStore = GlobalStore();
const themeConfig = computed(() => globalStore.themeConfig);

View file

@ -5,6 +5,7 @@ import '@/styles/common.scss';
import '@/assets/iconfont/iconfont.scss';
import '@/assets/fonts/font.scss';
import ElementPlus from 'element-plus';
import Fit2CloudPlus from 'fit2cloud-ui-plus';
import * as Icons from '@element-plus/icons-vue';
import 'element-plus/dist/index.css';
import 'element-plus/theme-chalk/dark/css-vars.css';
@ -26,4 +27,5 @@ app.use(router)
.use(pinia)
.use(directives)
.use(ElementPlus)
.use(Fit2CloudPlus)
.mount('#app');

View file

@ -1,4 +1,5 @@
/**
* @description: default layout
*/
export const Layout = () => import('@/layout/index.vue');
// export const Layout = () => import('@/layout/index.vue');
export const Layout = () => import('@/components/app-layout/index.vue');

View file

@ -1,21 +0,0 @@
// import { Layout } from '@/routers/constant';
// const homeRouter = {
// sort: 1,
// path: '/',
// component: Layout,
// redirect: '/home/index',
// meta: {
// keepAlive: true,
// requiresAuth: true,
// title: '首页',
// key: 'home',
// },
// children: [
// {
// path: '/home/index',
// name: 'home',
// component: () => import('@/views/home/index.vue'),
// },
// ],
// };
// export default homeRouter;

View file

@ -0,0 +1,26 @@
import { Layout } from '@/routers/constant';
// demo
const demoRouter = {
sort: 1,
path: '/demos',
component: Layout,
redirect: '/demos/table',
meta: {
title: 'menu.demo',
},
children: [
{
path: '/demos/table',
name: 'table',
component: () => import('@/views/demos/table/index.vue'),
meta: {
keepAlive: true,
requiresAuth: true,
key: 'table',
},
},
],
};
export default demoRouter;

View file

@ -0,0 +1,15 @@
@mixin flex-row($justify: flex-start, $align: stretch) {
display: flex;
@if $justify != flex-start {
justify-content: $justify;
}
@if $align != stretch {
align-items: $align;
}
}
@mixin variant($color, $background-color, $border-color) {
color: $color;
background-color: $background-color;
border-color: $border-color;
}

View file

@ -205,3 +205,26 @@ export function filterEnum(
if (type == 'tag') return filterData?.tagType ? filterData.tagType : '';
return filterData ? filterData[label] : '--';
}
/**
* yyyy-MM-dd HH:mm:ss
* @param dataStr
* @return String
*/
export function dateFromat(row: number, col: number, dataStr: any) {
const date = new Date(dataStr);
const y = date.getFullYear();
let m: string | number = date.getMonth() + 1;
m = m < 10 ? `0${String(m)}` : m;
let d: string | number = date.getDate();
d = d < 10 ? `0${String(d)}` : d;
let h: string | number = date.getHours();
h = h < 10 ? `0${String(h)}` : h;
let minute: string | number = date.getMinutes();
minute = minute < 10 ? `0${String(minute)}` : minute;
let second: string | number = date.getSeconds();
second = second < 10 ? `0${String(second)}` : second;
return `${String(y)}-${String(m)}-${String(d)} ${String(h)}:${String(
minute,
)}:${String(second)}`;
}

View file

@ -0,0 +1,121 @@
<template>
<LayoutContent :header="'样例'">
<ComplexTable
:pagination-config="paginationConfig"
v-model:selects="selects"
:data="data"
@search="search"
>
<template #toolbar>
<el-button type="primary">{{
$t('commons.button.create')
}}</el-button>
<el-button type="primary" plain>{{ '其他操作' }}</el-button>
<el-button
type="danger"
plain
:disabled="selects.length === 0"
@click="batchDelete"
>{{ $t('commons.button.delete') }}</el-button
>
</template>
<el-table-column type="selection" fix />
<el-table-column label="ID" min-width="100" prop="ID" fix />
<el-table-column
:label="$t('commons.table.name')"
min-width="100"
prop="name"
fix
>
<template #default="{ row }">
<fu-input-rw-switch v-model="row.name" size="mini" />
</template>
</el-table-column>
<el-table-column label="Email" min-width="100" prop="email" />
<el-table-column
prop="createdAt"
:label="$t('commons.table.createdAt')"
:formatter="dateFromat"
show-overflow-tooltip
width="200"
/>
<fu-table-operations
:buttons="buttons"
:label="$t('commons.table.operate')"
fix
/>
</ComplexTable>
</LayoutContent>
</template>
<script setup lang="ts">
import LayoutContent from '@/layout/LayoutContent.vue';
import ComplexTable from '@/components/complex-table/index.vue';
import { dateFromat } from '@/utils/util';
import { User } from '@/api/interface/user';
import { deleteUser, getUserList } from '@/api/modules/user';
import { onMounted, reactive, ref } from '@vue/runtime-core';
import { useDeleteData } from '@/hooks/useDeleteData';
import i18n from '@/lang';
const data = ref();
const selects = ref<any>([]);
const paginationConfig = reactive({
currentPage: 1,
pageSize: 5,
total: 0,
});
const buttons = [
{
label: '编辑',
click: edit,
},
// {
// label: '',
// click: buttonClick,
// },
// {
// label: '',
// type: 'danger',
// click: buttonClick,
// },
// {
// label: '',
// click: buttonClick,
// },
// {
// label: '',
// click: buttonClick,
// },
];
// function select(row: any) {
// console.log(row);
// selects.value.push(row.ID);
// console.log(selects);
// }
function edit(row: User.User) {
console.log(row);
}
const batchDelete = async () => {
let ids: Array<number> = [];
selects.value.forEach((item: User.User) => {
ids.push(item.ID);
});
await useDeleteData(
deleteUser,
{ ids: ids },
i18n.global.t('commons.msg.delete'),
);
};
const search = async () => {
const { currentPage, pageSize } = paginationConfig;
const res = getUserList({ currentPage, pageSize });
data.value = res.items;
paginationConfig.total = res.total;
};
onMounted(() => {
search();
});
</script>

View file

@ -0,0 +1,4 @@
<template>
<div></div>
</template>
<script setup lang="ts"></script>

View file

@ -107,7 +107,7 @@ import { ref, reactive } from 'vue';
import { ElMessage } from 'element-plus';
import { User } from '@/api/interface';
import { ColumnProps } from '@/components/ProTable/interface';
import { useHandleData } from '@/hooks/useHandleData';
import { useHandleData } from '@/hooks/useDeleteData';
import { useDownload } from '@/hooks/useDownload';
import ProTable from '@/components/ProTable/index.vue';
import ImportExcel from '@/components/ImportExcel/index.vue';

View file

@ -268,9 +268,9 @@ import { genderType } from '@/utils/serviceDict';
import { defaultFormat } from '@/utils/util';
import { User } from '@/api/interface';
import { useDownload } from '@/hooks/useDownload';
import { useHandleData } from '@/hooks/useHandleData';
import { useHandleData } from '@/hooks/useDeleteData';
import { useSelection } from '@/hooks/useSelection';
import { useAuthButtons } from '@/hooks/useAuthButtons';
// import { useAuthButtons } from '@/hooks/useAuthButtons';
import { useTable } from '@/hooks/useTable';
import ImportExcel from '@/components/ImportExcel/index.vue';
import UserDrawer from '@/views/proTable/components/UserDrawer.vue';
@ -325,8 +325,15 @@ const { isSelected, selectedListIds, selectionChange, getRowKeys } =
useSelection();
//
const { BUTTONS } = useAuthButtons();
// const { BUTTONS } = useAuthButtons();
const BUTTONS = {
status: true,
view: true,
reset: true,
edit: true,
delete: true,
};
//
searchInitParam.value = {
createTime: ['2022-04-05 00:00:00', '2022-05-10 23:59:59'],

View file

@ -12,6 +12,7 @@ import importToCDN from 'vite-plugin-cdn-import';
// import AutoImport from "unplugin-auto-import/vite";
// import Components from "unplugin-vue-components/vite";
// import { ElementPlusResolver } from "unplugin-vue-components/resolvers";
import DefineOptions from 'unplugin-vue-define-options/vite';
// @see: https://vitejs.dev/config/
export default defineConfig(({ mode }: ConfigEnv): UserConfig => {
@ -31,7 +32,7 @@ export default defineConfig(({ mode }: ConfigEnv): UserConfig => {
css: {
preprocessorOptions: {
scss: {
additionalData: `@import "@/styles/var.scss";`,
additionalData: `@use "@/styles/var.scss" as *;`,
},
},
},
@ -62,6 +63,7 @@ export default defineConfig(({ mode }: ConfigEnv): UserConfig => {
},
},
}),
DefineOptions(),
// * EsLint 报错信息显示在浏览器界面上
eslintPlugin(),
// * vite 可以使用 jsx/tsx 语法