feat: 完善登陆页面的部分功能
parent
f7818bda5c
commit
acd815736e
20
.env
20
.env
|
|
@ -2,29 +2,27 @@
|
|||
# 应用配置
|
||||
# =====================================================================================
|
||||
# 网站标题
|
||||
VITE_APP_TITLE = 绝弹管理系统
|
||||
VITE_TITLE = 绝弹管理系统
|
||||
# 网站副标题
|
||||
VITE_APP_SUBTITLE = 快速开发web应用的模板工具
|
||||
# Axios基本URL
|
||||
VITE_APP_API_BASE_URL = /api
|
||||
VITE_SUBTITLE = 快速开发web应用的模板工具
|
||||
# API接口前缀
|
||||
VITE_API_PREFIX = http://127.0.0.1:3030/
|
||||
# API文档地址 备注:需为符合 OPENAPI 规范的json文件
|
||||
VITE_API_SWAGGER = http://127.0.0.1:3030/openapi.json
|
||||
|
||||
# =====================================================================================
|
||||
# 开发设置
|
||||
# =====================================================================================
|
||||
# API接口地址(开发环境)
|
||||
VITE_API_BASE_URL = http://127.0.0.1:3030
|
||||
# API代理地址(开发环境)
|
||||
VITE_API_PROXY_URL = /api
|
||||
# API文档地址(开发环境) 备注:需为openapi规范的json文件
|
||||
VITE_API_DOCS_URL = http://127.0.0.1:3030/openapi.json
|
||||
# 端口号(开发环境)
|
||||
VITE_DEV_PORT = 3020
|
||||
VITE_PORT = 3020
|
||||
# 主机地址(开发环境)
|
||||
VITE_DEV_HOST = 0.0.0.0
|
||||
VITE_HOST = 0.0.0.0
|
||||
|
||||
# =====================================================================================
|
||||
# 构建设置
|
||||
# =====================================================================================
|
||||
# 构建时加载的文件后缀.
|
||||
# 例如:设置为todo则会首先尝试加载index.todo.vue文件,不存在时再加载index.vue文件
|
||||
VITE_BUILD_EXTENSION = todo
|
||||
VITE_EXTENSION = todo
|
||||
|
|
@ -4,7 +4,7 @@
|
|||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="./favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>%VITE_APP_TITLE% - %VITE_APP_SUBTITLE%</title>
|
||||
<title>%VITE_TITLE% - %VITE_SUBTITLE%</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app" class="dark:bg-slate-900 dark:text-slate-200">
|
||||
|
|
@ -51,7 +51,7 @@
|
|||
</style>
|
||||
<div class="loading">
|
||||
<img src="/assets/loading.svg" alt="loading" class="loading-image" />
|
||||
<h1 class="loading-title">欢迎访问%VITE_APP_TITLE%</h1>
|
||||
<h1 class="loading-title">欢迎访问%VITE_TITLE%</h1>
|
||||
<div class="loading-tip">正在加载中, 请稍后...</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@
|
|||
"nprogress": "^0.2.0",
|
||||
"numeral": "^2.0.6",
|
||||
"pinia": "^2.0.33",
|
||||
"pinia-plugin-persistedstate": "^3.2.0",
|
||||
"plop": "^3.1.2",
|
||||
"release-it": "^15.10.1",
|
||||
"swagger-typescript-api": "^12.0.4",
|
||||
|
|
|
|||
|
|
@ -46,6 +46,9 @@ devDependencies:
|
|||
pinia:
|
||||
specifier: ^2.0.33
|
||||
version: 2.1.4(typescript@4.9.5)(vue@3.3.4)
|
||||
pinia-plugin-persistedstate:
|
||||
specifier: ^3.2.0
|
||||
version: 3.2.0(pinia@2.1.4)
|
||||
plop:
|
||||
specifier: ^3.1.2
|
||||
version: 3.1.2
|
||||
|
|
@ -5833,6 +5836,14 @@ packages:
|
|||
dev: true
|
||||
optional: true
|
||||
|
||||
/pinia-plugin-persistedstate@3.2.0(pinia@2.1.4):
|
||||
resolution: {integrity: sha512-tZbNGf2vjAQcIm7alK40sE51Qu/m9oWr+rEgNm/2AWr1huFxj72CjvpQcIQzMknDBJEkQznCLAGtJTIcLKrKdw==}
|
||||
peerDependencies:
|
||||
pinia: ^2.0.0
|
||||
dependencies:
|
||||
pinia: 2.1.4(typescript@4.9.5)(vue@3.3.4)
|
||||
dev: true
|
||||
|
||||
/pinia@2.1.4(typescript@4.9.5)(vue@3.3.4):
|
||||
resolution: {integrity: sha512-vYlnDu+Y/FXxv1ABo1vhjC+IbqvzUdiUC3sfDRrRyY2CQSrqqaa+iiHmqtARFxJVqWQMCJfXx1PBvFs9aJVLXQ==}
|
||||
peerDependencies:
|
||||
|
|
|
|||
|
|
@ -1,4 +0,0 @@
|
|||
## 修改
|
||||
|
||||
route-docs.ejs
|
||||
- 移除 `@description` 关键字
|
||||
|
|
@ -8,17 +8,17 @@ const env = loadEnv("development", process.cwd());
|
|||
|
||||
const run = async () => {
|
||||
const output = await generateApi({
|
||||
url: env.VITE_API_DOCS_URL,
|
||||
url: env.VITE_API_SWAGGER,
|
||||
templates: path.resolve(__dirname, "./template"),
|
||||
output: path.resolve(process.cwd(), "src/api/service"),
|
||||
name: "index.ts",
|
||||
name: "Api.ts",
|
||||
singleHttpClient: false,
|
||||
httpClientType: "axios",
|
||||
unwrapResponseData: false,
|
||||
moduleNameIndex: 1,
|
||||
moduleNameFirstTag: true,
|
||||
cleanOutput: true,
|
||||
// generateRouteTypes: true,
|
||||
generateRouteTypes: true,
|
||||
extractRequestParams: true,
|
||||
modular: false,
|
||||
prettier: {
|
||||
|
|
@ -61,3 +61,11 @@ const run = async () => {
|
|||
};
|
||||
|
||||
run();
|
||||
|
||||
|
||||
/**
|
||||
* 模板修改备注:
|
||||
*
|
||||
* route-docs.ejs
|
||||
* - 移除 `@description` 关键字
|
||||
*/
|
||||
|
|
@ -57,7 +57,6 @@ module.exports = {
|
|||
return order.indexOf(a.title) - order.indexOf(b.title);
|
||||
},
|
||||
commitPartial: loadTemplate("commit"),
|
||||
// headerPartial: loadTemplate('header'),
|
||||
mainTemplate: loadTemplate("main"),
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -8,13 +8,13 @@ import pkg from "../../package.json";
|
|||
* @description 内容:APPTIFY
|
||||
*/
|
||||
const LOGO = `
|
||||
________ ______ ______ _________ ________ ______ __ __
|
||||
/_______/\\\\ /_____/\\\\ /_____/\\\\ /________/\\\\/_______/\\\\/_____/\\\\ /_/\\\\/_/\\\\
|
||||
\\\\::: _ \\\\ \\\\\\\\:::_ \\\\ \\\\\\\\:::_ \\\\ \\\\\\\\__.::.__\\\\/\\\\__.::._\\\\/\\\\::::_\\\\/_\\\\ \\\\ \\\\ \\\\ \\\\
|
||||
\\\\::(_) \\\\ \\\\\\\\:(_) \\\\ \\\\\\\\:(_) \\\\ \\\\ \\\\::\\\\ \\\\ \\\\::\\\\ \\\\ \\\\:\\\\/___/\\\\\\\\:\\\\_\\\\ \\\\ \\\\
|
||||
\\\\:: __ \\\\ \\\\\\\\: ___\\\\/ \\\\: ___\\\\/ \\\\::\\\\ \\\\ _\\\\::\\\\ \\\\__\\\\:::._\\\\/ \\\\::::_\\\\/
|
||||
\\\\:.\\\\ \\\\ \\\\ \\\\\\\\ \\\\ \\\\ \\\\ \\\\ \\\\ \\\\::\\\\ \\\\ /__\\\\::\\\\__/\\\\\\\\:\\\\ \\\\ \\\\::\\\\ \\\\
|
||||
\\\\__\\\\/\\\\__\\\\/ \\\\_\\\\/ \\\\_\\\\/ \\\\__\\\\/ \\\\________\\\\/ \\\\_\\\\/ \\\\__\\\\/
|
||||
________ ______ ______ _________ ________ ______ __ __
|
||||
/_______/\\\\ /_____/\\\\ /_____/\\\\ /________/\\\\/_______/\\\\/_____/\\\\ /_/\\\\/_/\\\\
|
||||
\\\\::: _ \\\\ \\\\\\\\:::_ \\\\ \\\\\\\\:::_ \\\\ \\\\\\\\__.::.__\\\\/\\\\__.::._\\\\/\\\\::::_\\\\/_\\\\ \\\\ \\\\ \\\\ \\\\
|
||||
\\\\::(_) \\\\ \\\\\\\\:(_) \\\\ \\\\\\\\:(_) \\\\ \\\\ \\\\::\\\\ \\\\ \\\\::\\\\ \\\\ \\\\:\\\\/___/\\\\\\\\:\\\\_\\\\ \\\\ \\\\
|
||||
\\\\:: __ \\\\ \\\\\\\\: ___\\\\/ \\\\: ___\\\\/ \\\\::\\\\ \\\\ _\\\\::\\\\ \\\\__\\\\:::._\\\\/ \\\\::::_\\\\/
|
||||
\\\\:.\\\\ \\\\ \\\\ \\\\\\\\ \\\\ \\\\ \\\\ \\\\ \\\\ \\\\::\\\\ \\\\ /__\\\\::\\\\__/\\\\\\\\:\\\\ \\\\ \\\\::\\\\ \\\\
|
||||
\\\\__\\\\/\\\\__\\\\/ \\\\_\\\\/ \\\\_\\\\/ \\\\__\\\\/ \\\\________\\\\/ \\\\_\\\\/ \\\\__\\\\/
|
||||
`;
|
||||
|
||||
/**
|
||||
|
|
@ -68,7 +68,7 @@ export default function plugin(): Plugin {
|
|||
configResolved(resolvedConfig) {
|
||||
config = resolvedConfig;
|
||||
const defaultExt = config.mode === "development" ? "dev" : "prod";
|
||||
extension = config.env.VITE_BUILD_EXTENTION || defaultExt;
|
||||
extension = config.env.VITE_EXTENTION || defaultExt;
|
||||
},
|
||||
|
||||
async transformIndexHtml(html) {
|
||||
|
|
|
|||
|
|
@ -1,2 +1,3 @@
|
|||
export * from "./instance";
|
||||
export * from "./service";
|
||||
export * from "./instance/api";
|
||||
export * from "./service/Api";
|
||||
export * from "./helper/useRequest";
|
||||
|
|
|
|||
|
|
@ -1,34 +1,15 @@
|
|||
import { Api } from "../service";
|
||||
import { Api } from "../service/Api";
|
||||
import { toast, IToastOptions } from "@/components";
|
||||
import { useUserStore, store } from "@/store";
|
||||
|
||||
const userStore = useUserStore(store);
|
||||
|
||||
/**
|
||||
* 自定义扩展, 例如添加额外的请求函数
|
||||
*/
|
||||
class Service extends Api<unknown> {
|
||||
github = {
|
||||
/**
|
||||
* 获取当前仓库信息
|
||||
*/
|
||||
getRepoInfo: async () => {
|
||||
const info: Record<string, any> = await this.request({
|
||||
baseURL: "https://api.github.com",
|
||||
path: "/repos/juetan/apptify-admin",
|
||||
method: "GET",
|
||||
});
|
||||
return info;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* api实例
|
||||
* API 接口实例
|
||||
* @see src/api/instance/instance.ts
|
||||
*/
|
||||
const api = new Service({
|
||||
baseURL: import.meta.env.VITE_API_BASE_URL,
|
||||
export const api = new Api({
|
||||
baseURL: import.meta.env.VITE_API_PREFIX,
|
||||
});
|
||||
|
||||
/**
|
||||
|
|
@ -42,19 +23,17 @@ api.instance.interceptors.request.use(
|
|||
if (config.toast) {
|
||||
let options: IToastOptions = {};
|
||||
if (typeof config.toast === "string") {
|
||||
options = {
|
||||
message: config.toast,
|
||||
};
|
||||
options = { message: config.toast };
|
||||
}
|
||||
if (typeof config.toast === "object") {
|
||||
options = config.toast;
|
||||
}
|
||||
config._closeToast = toast(options);
|
||||
config.closeToast = toast(options);
|
||||
}
|
||||
return config;
|
||||
},
|
||||
(error) => {
|
||||
error.config?._closeToast?.();
|
||||
error.config?.closeToast?.();
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
|
@ -64,14 +43,14 @@ api.instance.interceptors.request.use(
|
|||
*/
|
||||
api.instance.interceptors.response.use(
|
||||
(res) => {
|
||||
res.config?._closeToast?.();
|
||||
res.config.closeToast?.();
|
||||
if (res.data?.code && res.data.code !== 2000) {
|
||||
return Promise.reject(res);
|
||||
}
|
||||
return res;
|
||||
},
|
||||
(error) => {
|
||||
error.config?._closeToast?.();
|
||||
error.config.closeToast?.();
|
||||
if (error.request) {
|
||||
console.log("request error", error.request);
|
||||
}
|
||||
|
|
@ -81,5 +60,3 @@ api.instance.interceptors.response.use(
|
|||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
export { api };
|
||||
|
|
@ -4,14 +4,14 @@ import { IToastOptions } from "@/components";
|
|||
declare module "axios" {
|
||||
interface AxiosRequestConfig {
|
||||
/**
|
||||
* toast config
|
||||
* 请求弹窗配置
|
||||
* @default false
|
||||
*/
|
||||
toast?: boolean | string | IToastOptions;
|
||||
/**
|
||||
* close toast(internal)
|
||||
* 关闭弹窗
|
||||
* @private
|
||||
*/
|
||||
_closeToast?: () => void;
|
||||
closeToast?: () => void;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +0,0 @@
|
|||
export * from "./instance";
|
||||
export * from "./useRequest";
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -26,6 +26,7 @@ export interface TableColumnButton {
|
|||
|
||||
/**
|
||||
* 操作类型
|
||||
* @description `delete` 需配置`onClick`属性,`modify` 需配置根对象下的 `modify` 属性
|
||||
*/
|
||||
type?: "delete" | "modify";
|
||||
|
||||
|
|
@ -79,7 +80,7 @@ export interface UseTableOptions extends Omit<TableProps, "search" | "create" |
|
|||
*/
|
||||
items?: (Partial<IFormItem> & {
|
||||
/**
|
||||
* 继承common.items中指定field值的项
|
||||
* 继承`create.items`中指定field值的项
|
||||
*/
|
||||
extend?: string;
|
||||
})[];
|
||||
|
|
@ -93,7 +94,7 @@ export interface UseTableOptions extends Omit<TableProps, "search" | "create" |
|
|||
/**
|
||||
* 新建弹窗配置
|
||||
*/
|
||||
create?: FormModalProps;
|
||||
create?: Partial<FormModalProps>;
|
||||
/**
|
||||
* 新建弹窗配置
|
||||
*/
|
||||
|
|
@ -105,7 +106,7 @@ export interface UseTableOptions extends Omit<TableProps, "search" | "create" |
|
|||
extend: boolean;
|
||||
items?: (FormModalProps["items"][number] & {
|
||||
/**
|
||||
* 继承`create`弹窗配置中指定field值的项
|
||||
* 继承`create.items`弹窗配置中指定field值的项
|
||||
*/
|
||||
extend?: string;
|
||||
})[];
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import { UseTableOptions } from "./use-interface";
|
|||
|
||||
/**
|
||||
* 表格组件hook
|
||||
* @see src/components/table/use-table.tsx
|
||||
* @see `src/components/table/use-table.tsx`
|
||||
*/
|
||||
export const useTable = (optionsOrFn: UseTableOptions | (() => UseTableOptions)): any => {
|
||||
const options: UseTableOptions = typeof optionsOrFn === "function" ? optionsOrFn() : optionsOrFn;
|
||||
|
|
@ -51,9 +51,16 @@ export const useTable = (optionsOrFn: UseTableOptions | (() => UseTableOptions))
|
|||
Modal.warning({
|
||||
...config.columnButtonDelete,
|
||||
onOk: async () => {
|
||||
const resData: any = await action?.onClick?.(data);
|
||||
resData.msg && Message.success(resData?.msg || "");
|
||||
getTable()?.loadData();
|
||||
try {
|
||||
const resData: any = await action?.onClick?.(data);
|
||||
resData.msg && Message.success(resData?.msg || "");
|
||||
getTable()?.loadData();
|
||||
} catch (error: any) {
|
||||
const message = error.response?.data?.message;
|
||||
if (message) {
|
||||
Message.warning(`提示:${message}`);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
|
@ -97,8 +104,7 @@ export const useTable = (optionsOrFn: UseTableOptions | (() => UseTableOptions))
|
|||
if (item.extend) {
|
||||
const createItem = createItems.find((i) => i.field === item.extend);
|
||||
if (createItem) {
|
||||
const mergedItem = merge({}, createItem, item);
|
||||
searchItems.push(mergedItem);
|
||||
searchItems.push(merge({}, createItem, item));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { createApp } from "vue";
|
|||
import App from "./App.vue";
|
||||
import { router } from "./router";
|
||||
import { store } from "./store";
|
||||
import { style } from "./style";
|
||||
import { style } from "./styles";
|
||||
|
||||
const run = async () => {
|
||||
const app = createApp(App);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<div class="page-login w-full h-full grid grid-rows-[auto_1fr_auto]">
|
||||
<div class=" top-0 m-0 h-20 w-full px-10 z-10">
|
||||
<div class="top-0 m-0 h-20 w-full px-10 z-10">
|
||||
<div class="md:hidden flex items-center justify-between h-13">
|
||||
<div class="flex items-center">
|
||||
<img src="/favicon.ico" alt="" width="20" height="20" class="mr-1" />
|
||||
|
|
@ -55,13 +55,15 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { api } from "@/api";
|
||||
import { dayjs } from "@/plugins";
|
||||
import { useAppStore } from "@/store";
|
||||
import { useAppStore, useUserStore } from "@/store";
|
||||
import { FieldRule, Form, Modal } from "@arco-design/web-vue";
|
||||
import { reactive } from "vue";
|
||||
|
||||
const meridiem = dayjs.localeData().meridiem(dayjs().hour(), dayjs().minute());
|
||||
const appStore = useAppStore();
|
||||
const userStore = useUserStore();
|
||||
const model = reactive({ username: "admin", password: "admin" });
|
||||
const router = useRouter();
|
||||
const loading = ref(false);
|
||||
|
|
@ -93,11 +95,19 @@ const onForgetPasswordClick = () => {
|
|||
|
||||
const onSubmitClick = async () => {
|
||||
const errors = await formRef.value?.validate();
|
||||
if (errors) return;
|
||||
loading.value = true;
|
||||
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||
loading.value = false;
|
||||
router.push({ path: "/" });
|
||||
if (errors) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
loading.value = true;
|
||||
const res = await api.auth.login(model);
|
||||
userStore.setUser(res.data.data);
|
||||
router.push({ path: "/" });
|
||||
} catch {
|
||||
console.log(1);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -52,7 +52,23 @@ const table = useTable({
|
|||
],
|
||||
},
|
||||
],
|
||||
common: {
|
||||
search: {
|
||||
items: [
|
||||
{
|
||||
extend: "name",
|
||||
required: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
create: {
|
||||
title: "新建角色",
|
||||
modalProps: {
|
||||
width: 580,
|
||||
maskClosable: false,
|
||||
},
|
||||
formProps: {
|
||||
layout: "vertical",
|
||||
},
|
||||
items: [
|
||||
{
|
||||
field: "name",
|
||||
|
|
@ -77,26 +93,7 @@ const table = useTable({
|
|||
options: () => api.permission.getPermissions(),
|
||||
},
|
||||
],
|
||||
modalProps: {
|
||||
width: 580,
|
||||
maskClosable: false,
|
||||
},
|
||||
formProps: {
|
||||
layout: "vertical",
|
||||
},
|
||||
},
|
||||
search: {
|
||||
items: [
|
||||
{
|
||||
field: "name",
|
||||
label: "角色名称",
|
||||
type: "input",
|
||||
required: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
create: {
|
||||
title: "新建角色",
|
||||
|
||||
submit: ({ model }) => {
|
||||
return api.role.addRole(model as any);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -10,142 +10,136 @@ import { Table, useTable } from "@/components";
|
|||
import { dayjs } from "@/plugins";
|
||||
import { Avatar, Button } from "@arco-design/web-vue";
|
||||
|
||||
const table = useTable(() => {
|
||||
const nicknameRender = ({ record }: any) => {
|
||||
return (
|
||||
<div class="flex items-center">
|
||||
<Avatar size={32}>
|
||||
<img src={record.avatar} alt="" />
|
||||
</Avatar>
|
||||
<span class="ml-2 flex-1 flex flex-col overflow-hidden">
|
||||
<span>{record.nickname}</span>
|
||||
<span class="text-gray-400 text-xs truncate">账号:{record.username}</span>
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
return {
|
||||
data: async (model, paging) => {
|
||||
return api.user.getUsers({ ...model, ...paging });
|
||||
const table = useTable({
|
||||
data: async (model, paging) => {
|
||||
return api.user.getUsers({ ...model, ...paging });
|
||||
},
|
||||
columns: [
|
||||
{
|
||||
type: "index",
|
||||
},
|
||||
columns: [
|
||||
{
|
||||
type: "index",
|
||||
{
|
||||
title: "用户昵称",
|
||||
dataIndex: "username",
|
||||
width: 200,
|
||||
render: ({ record }) => {
|
||||
return (
|
||||
<div class="flex items-center">
|
||||
<Avatar size={32}>
|
||||
<img src={record.avatar} alt="" />
|
||||
</Avatar>
|
||||
<span class="ml-2 flex-1 flex flex-col overflow-hidden">
|
||||
<span>{record.nickname}</span>
|
||||
<span class="text-gray-400 text-xs truncate">账号:{record.username}</span>
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
{
|
||||
title: "用户昵称",
|
||||
dataIndex: "username",
|
||||
width: 200,
|
||||
render: nicknameRender,
|
||||
},
|
||||
{
|
||||
title: "用户描述",
|
||||
dataIndex: "description",
|
||||
},
|
||||
{
|
||||
title: "用户邮箱",
|
||||
dataIndex: "email",
|
||||
},
|
||||
{
|
||||
title: "创建时间",
|
||||
dataIndex: "createdAt",
|
||||
width: 200,
|
||||
render: ({ record }) => dayjs(record.createdAt).format(),
|
||||
},
|
||||
{
|
||||
title: "操作",
|
||||
type: "button",
|
||||
width: 148,
|
||||
buttons: [
|
||||
{
|
||||
type: "modify",
|
||||
text: "修改",
|
||||
},
|
||||
{
|
||||
title: "用户描述",
|
||||
dataIndex: "description",
|
||||
},
|
||||
{
|
||||
title: "用户邮箱",
|
||||
dataIndex: "email",
|
||||
},
|
||||
{
|
||||
title: "创建时间",
|
||||
dataIndex: "createdAt",
|
||||
width: 200,
|
||||
render: ({ record }) => dayjs(record.createdAt).format(),
|
||||
},
|
||||
{
|
||||
title: "操作",
|
||||
type: "button",
|
||||
width: 148,
|
||||
buttons: [
|
||||
{
|
||||
type: "modify",
|
||||
text: "修改",
|
||||
},
|
||||
{
|
||||
type: "delete",
|
||||
text: "删除",
|
||||
onClick: async ({ record }) => {
|
||||
return api.user.delUser(record.id);
|
||||
},
|
||||
{
|
||||
type: "delete",
|
||||
text: "删除",
|
||||
onClick: async (data) => {
|
||||
return api.user.deleteUser(data.record.id);
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
search: {
|
||||
items: [
|
||||
{
|
||||
extend: "username",
|
||||
required: false,
|
||||
},
|
||||
],
|
||||
search: {
|
||||
items: [
|
||||
{
|
||||
extend: "username",
|
||||
required: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
create: {
|
||||
title: "新建用户",
|
||||
trigger: () => (
|
||||
<Button type="primary">
|
||||
{{
|
||||
icon: () => <i class="icon-park-outline-people-plus-one" />,
|
||||
default: () => "添加",
|
||||
}}
|
||||
</Button>
|
||||
),
|
||||
modalProps: {
|
||||
width: 772,
|
||||
maskClosable: false,
|
||||
},
|
||||
create: {
|
||||
title: "新建用户",
|
||||
trigger: () => (
|
||||
<Button type="primary">
|
||||
{{
|
||||
icon: () => <i class="icon-park-outline-people-plus-one" />,
|
||||
default: () => "添加",
|
||||
}}
|
||||
</Button>
|
||||
),
|
||||
modalProps: {
|
||||
width: 772,
|
||||
maskClosable: false,
|
||||
},
|
||||
formProps: {
|
||||
layout: "vertical",
|
||||
class: "!grid grid-cols-2 gap-x-3",
|
||||
},
|
||||
model: {
|
||||
avatarUrl: "",
|
||||
},
|
||||
items: [
|
||||
{
|
||||
field: "username",
|
||||
label: "登录账号",
|
||||
type: "input",
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
field: "nickname",
|
||||
label: "用户昵称",
|
||||
type: "input",
|
||||
},
|
||||
{
|
||||
field: "description",
|
||||
label: "个人描述",
|
||||
type: "input",
|
||||
},
|
||||
{
|
||||
field: "password",
|
||||
label: "密码",
|
||||
type: "password",
|
||||
},
|
||||
{
|
||||
label: "头像",
|
||||
field: "avatar?avatarUrl",
|
||||
type: "select",
|
||||
},
|
||||
{
|
||||
field: "startTime:endTime",
|
||||
label: "日期范围",
|
||||
type: "dateRange",
|
||||
nodeProps: {},
|
||||
},
|
||||
],
|
||||
submit: ({ model }) => {
|
||||
console.log(model);
|
||||
},
|
||||
formProps: {
|
||||
layout: "vertical",
|
||||
class: "!grid grid-cols-2 gap-x-3",
|
||||
},
|
||||
modify: {
|
||||
extend: true,
|
||||
title: "修改用户",
|
||||
submit: ({ model }) => {
|
||||
return api.user.updateUser(model.id, model);
|
||||
items: [
|
||||
{
|
||||
field: "username",
|
||||
label: "登录账号",
|
||||
type: "input",
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
field: "nickname",
|
||||
label: "用户昵称",
|
||||
type: "input",
|
||||
},
|
||||
{
|
||||
field: "description",
|
||||
label: "个人描述",
|
||||
type: "input",
|
||||
},
|
||||
{
|
||||
field: "password",
|
||||
label: "密码",
|
||||
type: "password",
|
||||
},
|
||||
{
|
||||
label: "头像",
|
||||
field: "avatar",
|
||||
type: "select",
|
||||
},
|
||||
{
|
||||
field: "startTime:endTime",
|
||||
label: "日期范围",
|
||||
type: "dateRange",
|
||||
nodeProps: {},
|
||||
},
|
||||
],
|
||||
submit: ({ model }) => {
|
||||
console.log(model);
|
||||
},
|
||||
};
|
||||
},
|
||||
modify: {
|
||||
extend: true,
|
||||
title: "修改用户",
|
||||
submit: ({ model }) => {
|
||||
return api.user.updateUser(model.id, model);
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,34 @@
|
|||
import { NavigationGuardWithThis } from "vue-router";
|
||||
import { store, useUserStore } from "@/store";
|
||||
import { Message } from "@arco-design/web-vue";
|
||||
|
||||
const userStore = useUserStore(store);
|
||||
|
||||
/**
|
||||
* 权限守卫
|
||||
* @description 校验用户是否有路由的访问权限
|
||||
*/
|
||||
export function useAuthGuard() {
|
||||
const whitelist = ["/404"];
|
||||
const signoutlist = ["/login"];
|
||||
const authGuard: NavigationGuardWithThis<undefined> = async function (to, from, next) {
|
||||
if (to.meta.auth === false) {
|
||||
return next();
|
||||
}
|
||||
if (whitelist.includes(to.fullPath)) {
|
||||
return next();
|
||||
}
|
||||
if (signoutlist.includes(to.fullPath)) {
|
||||
if (userStore.id) {
|
||||
Message.warning(`提示:您已登陆,如需重新请退出后再操作!`);
|
||||
return next(false);
|
||||
}
|
||||
return next();
|
||||
}
|
||||
if (!userStore.id) {
|
||||
return next("/login");
|
||||
}
|
||||
next();
|
||||
};
|
||||
return authGuard;
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
import { NavigationGuardWithThis } from "vue-router";
|
||||
import { NProgress } from "@/plugins";
|
||||
|
||||
/**
|
||||
* 进度条守卫
|
||||
* @description 在路由跳转时显示进度条
|
||||
*/
|
||||
export const useNprogressGuard = () => {
|
||||
const before: NavigationGuardWithThis<undefined> = function (to, from, next) {
|
||||
NProgress.start();
|
||||
next();
|
||||
};
|
||||
const after: NavigationGuardWithThis<undefined> = function (to, from, next) {
|
||||
NProgress.done();
|
||||
};
|
||||
return { before, after };
|
||||
};
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
import { NavigationGuardWithThis } from "vue-router";
|
||||
|
||||
/**
|
||||
* 标题首位
|
||||
* @description 路由跳转后更新页面标题
|
||||
*/
|
||||
export const useTitleGuard = () => {
|
||||
const titleGuard: NavigationGuardWithThis<undefined> = function (to, from, next) {
|
||||
const title = to.meta.title || import.meta.env.VITE_APP_TITLE;
|
||||
const subtitle = import.meta.env.VITE_APP_SUBTITLE;
|
||||
document.title = `${title} | ${subtitle}`;
|
||||
next();
|
||||
};
|
||||
return titleGuard;
|
||||
};
|
||||
|
|
@ -1,42 +0,0 @@
|
|||
import { Router } from 'vue-router';
|
||||
import { NProgress } from '@/plugins';
|
||||
|
||||
/**
|
||||
* 设置进度条
|
||||
* @param router 路由实例
|
||||
*/
|
||||
export const useNprogress = (router: Router) => {
|
||||
router.beforeEach((to, from, next) => {
|
||||
NProgress.start();
|
||||
next();
|
||||
});
|
||||
|
||||
router.afterEach(() => {
|
||||
NProgress.done();
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 设置文档标题
|
||||
* @param router 路由实例
|
||||
*/
|
||||
export const useTitle = (router: Router) => {
|
||||
router.beforeEach((to, from, next) => {
|
||||
const title = to.meta.title || import.meta.env.VITE_APP_TITLE;
|
||||
const subtitle = import.meta.env.VITE_APP_SUBTITLE;
|
||||
document.title = `${title} | ${subtitle}`;
|
||||
next();
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 设置权限
|
||||
* @param router 路由实例
|
||||
*/
|
||||
export const useAuth = (router: Router) => {
|
||||
router.beforeEach((to, from, next) => {
|
||||
if (to.meta.auth) {
|
||||
}
|
||||
next();
|
||||
});
|
||||
};
|
||||
|
|
@ -1,6 +1,12 @@
|
|||
import { createRouter, createWebHashHistory } from "vue-router";
|
||||
import { useAuth, useNprogress, useTitle } from "../guards";
|
||||
import { routes } from "../routes";
|
||||
import { useAuthGuard } from "../guards/guard-auth";
|
||||
import { useNprogressGuard } from "../guards/guard-nprogress";
|
||||
import { useTitleGuard } from "../guards/guard-title";
|
||||
|
||||
const nprogressGuard = useNprogressGuard();
|
||||
const titleGuard = useTitleGuard();
|
||||
const authGuard = useAuthGuard();
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHashHistory(),
|
||||
|
|
@ -13,8 +19,9 @@ const router = createRouter({
|
|||
],
|
||||
});
|
||||
|
||||
useNprogress(router);
|
||||
useTitle(router);
|
||||
useAuth(router);
|
||||
router.beforeEach(nprogressGuard.before);
|
||||
router.beforeEach(nprogressGuard.after);
|
||||
router.beforeEach(titleGuard);
|
||||
router.beforeEach(authGuard);
|
||||
|
||||
export { router };
|
||||
|
|
|
|||
|
|
@ -1,38 +1,35 @@
|
|||
import { useDark } from "@vueuse/core";
|
||||
import { defineStore } from "pinia";
|
||||
|
||||
export const useAppStore = defineStore({
|
||||
id: "app",
|
||||
state: () => {
|
||||
const isDark = useDark({
|
||||
onChanged: (isDark) => {
|
||||
if (isDark) {
|
||||
document.body.setAttribute("arco-theme", "dark");
|
||||
document.body.classList.add("dark");
|
||||
return;
|
||||
}
|
||||
document.body.setAttribute("arco-theme", "light");
|
||||
document.body.classList.remove("dark");
|
||||
},
|
||||
});
|
||||
return {
|
||||
count: 0,
|
||||
isDark,
|
||||
title: import.meta.env.VITE_APP_TITLE,
|
||||
subtitle: import.meta.env.VITE_APP_SUBTITLE,
|
||||
};
|
||||
},
|
||||
getters: {
|
||||
doubleCount(state) {
|
||||
return state.count * 2;
|
||||
},
|
||||
},
|
||||
state: () => ({
|
||||
isDarkMode: false,
|
||||
title: import.meta.env.VITE_TITLE,
|
||||
subtitle: import.meta.env.VITE_SUBTITLE,
|
||||
}),
|
||||
actions: {
|
||||
increment() {
|
||||
this.count++;
|
||||
},
|
||||
/**
|
||||
* 切换暗/亮模式
|
||||
*/
|
||||
toggleDark() {
|
||||
this.isDark = !this.isDark;
|
||||
this.isDarkMode ? this.setLight() : this.setDark();
|
||||
},
|
||||
/**
|
||||
* 切换为亮模式
|
||||
*/
|
||||
setLight() {
|
||||
document.body.setAttribute("arco-theme", "light");
|
||||
document.body.classList.remove("dark");
|
||||
this.isDarkMode = false;
|
||||
},
|
||||
/**
|
||||
* 切换为暗模式
|
||||
*/
|
||||
setDark() {
|
||||
document.body.setAttribute("arco-theme", "dark");
|
||||
document.body.classList.add("dark");
|
||||
this.isDarkMode = true;
|
||||
},
|
||||
},
|
||||
persist: true,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import { createPinia } from "pinia";
|
||||
import persistedstatePlugin from "pinia-plugin-persistedstate";
|
||||
|
||||
export const store = createPinia();
|
||||
store.use(persistedstatePlugin);
|
||||
|
|
|
|||
|
|
@ -1,14 +1,18 @@
|
|||
import { useStorage } from "@vueuse/core";
|
||||
import { LoginedUserVo } from "@/api";
|
||||
import { defineStore } from "pinia";
|
||||
|
||||
export const useUserStore = defineStore({
|
||||
id: "user",
|
||||
state: () => {
|
||||
const user = useStorage("APP_USER", {
|
||||
return {
|
||||
/**
|
||||
* 用户ID
|
||||
*/
|
||||
id: 0,
|
||||
/**
|
||||
* 用户名称
|
||||
*/
|
||||
name: "绝弹",
|
||||
username: "绝弹",
|
||||
/**
|
||||
* 用户头像地址
|
||||
*/
|
||||
|
|
@ -17,22 +21,35 @@ export const useUserStore = defineStore({
|
|||
* JWT令牌
|
||||
*/
|
||||
accessToken: "",
|
||||
});
|
||||
return user;
|
||||
/**
|
||||
* 刷新令牌
|
||||
*/
|
||||
refreshToken: "",
|
||||
};
|
||||
},
|
||||
actions: {
|
||||
/**
|
||||
* 设置令牌
|
||||
*/
|
||||
setToken(token: string) {
|
||||
this.accessToken = token;
|
||||
},
|
||||
/**
|
||||
* 清除用户信息
|
||||
*/
|
||||
clearUser() {
|
||||
this.name = "";
|
||||
this.username = "";
|
||||
this.avatar = "";
|
||||
this.accessToken = "";
|
||||
},
|
||||
setUser(user: { name: string; avatar: string; accessToken: string }) {
|
||||
this.name = user.name;
|
||||
/**
|
||||
* 设置用户信息
|
||||
*/
|
||||
setUser(user: LoginedUserVo) {
|
||||
this.username = user.username;
|
||||
this.avatar = user.avatar;
|
||||
this.accessToken = user.accessToken;
|
||||
this.accessToken = user.token;
|
||||
},
|
||||
},
|
||||
persist: true,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -11,16 +11,23 @@ declare module '@vue/runtime-core' {
|
|||
ABreadcrumb: typeof import('@arco-design/web-vue')['Breadcrumb']
|
||||
ABreadcrumbItem: typeof import('@arco-design/web-vue')['BreadcrumbItem']
|
||||
AButton: typeof import('@arco-design/web-vue')['Button']
|
||||
ACheckbox: typeof import('@arco-design/web-vue')['Checkbox']
|
||||
AConfigProvider: typeof import('@arco-design/web-vue')['ConfigProvider']
|
||||
ADoption: typeof import('@arco-design/web-vue')['Doption']
|
||||
ADrawer: typeof import('@arco-design/web-vue')['Drawer']
|
||||
ADropdown: typeof import('@arco-design/web-vue')['Dropdown']
|
||||
AForm: typeof import('@arco-design/web-vue')['Form']
|
||||
AFormItem: typeof import('@arco-design/web-vue')['FormItem']
|
||||
AInput: typeof import('@arco-design/web-vue')['Input']
|
||||
AInputPassword: typeof import('@arco-design/web-vue')['InputPassword']
|
||||
ALayout: typeof import('@arco-design/web-vue')['Layout']
|
||||
ALayoutContent: typeof import('@arco-design/web-vue')['LayoutContent']
|
||||
ALayoutHeader: typeof import('@arco-design/web-vue')['LayoutHeader']
|
||||
ALayoutSider: typeof import('@arco-design/web-vue')['LayoutSider']
|
||||
ALink: typeof import('@arco-design/web-vue')['Link']
|
||||
AMenu: typeof import('@arco-design/web-vue')['Menu']
|
||||
AMenuItem: typeof import('@arco-design/web-vue')['MenuItem']
|
||||
ASpace: typeof import('@arco-design/web-vue')['Space']
|
||||
ASubMenu: typeof import('@arco-design/web-vue')['SubMenu']
|
||||
ATag: typeof import('@arco-design/web-vue')['Tag']
|
||||
ATooltip: typeof import('@arco-design/web-vue')['Tooltip']
|
||||
|
|
|
|||
|
|
@ -4,15 +4,15 @@ interface ImportMetaEnv {
|
|||
/**
|
||||
* 网站标题
|
||||
*/
|
||||
VITE_APP_TITLE: string;
|
||||
VITE_TITLE: string;
|
||||
/**
|
||||
* 网站副标题
|
||||
*/
|
||||
VITE_APP_SUBTITLE: string;
|
||||
VITE_SUBTITLE: string;
|
||||
/**
|
||||
* 自定义的文件后缀
|
||||
*/
|
||||
VITE_BUILD_EXTENSION: string;
|
||||
VITE_EXTENSION: string;
|
||||
/**
|
||||
* API 地址
|
||||
*/
|
||||
|
|
@ -20,11 +20,11 @@ interface ImportMetaEnv {
|
|||
/**
|
||||
* 开发服务器主机
|
||||
*/
|
||||
VITE_DEV_HOST: string;
|
||||
VITE_HOST: string;
|
||||
/**
|
||||
* 开发服务器端口
|
||||
*/
|
||||
VITE_DEV_PORT: number;
|
||||
VITE_PORT: number;
|
||||
}
|
||||
|
||||
interface ImportMeta {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,7 @@
|
|||
import { Message } from "@arco-design/web-vue";
|
||||
|
||||
export const message = {
|
||||
warning() {
|
||||
Message.warning(``)
|
||||
}
|
||||
}
|
||||
|
|
@ -17,8 +17,8 @@ import plugin from "./scripts/vite/plugin";
|
|||
export default defineConfig(({ mode }) => {
|
||||
// 加载顺序,后者优先级高:.env .env.locale .env.[mode] .env.[mode].locale
|
||||
const env = loadEnv(mode, process.cwd());
|
||||
const host = env.VITE_DEV_HOST || "0.0.0.0";
|
||||
const port = Number(env.VITE_DEV_PORT || 3020);
|
||||
const host = env.VITE_HOST || "0.0.0.0";
|
||||
const port = Number(env.VITE_PORT || 3020);
|
||||
|
||||
return {
|
||||
base: "./",
|
||||
|
|
@ -39,7 +39,7 @@ export default defineConfig(({ mode }) => {
|
|||
less: {
|
||||
javascriptEnabled: true,
|
||||
modifyVars: {
|
||||
hack: `true; @import (reference) "${resolve("src/style/css-arco.less")}";`,
|
||||
hack: `true; @import (reference) "${resolve("src/styles/css-arco.less")}";`,
|
||||
arcoblue: "#66f",
|
||||
},
|
||||
},
|
||||
|
|
|
|||
Loading…
Reference in New Issue