feat: 优化目录
parent
c489f3b0cf
commit
8968692073
4
.env
4
.env
|
|
@ -10,7 +10,7 @@ VITE_BASE = /
|
||||||
# 接口前缀:参见 axios 的 baseURL
|
# 接口前缀:参见 axios 的 baseURL
|
||||||
VITE_API = https://appnify.app.juetan.cn/
|
VITE_API = https://appnify.app.juetan.cn/
|
||||||
# 首页路径
|
# 首页路径
|
||||||
VITE_HOME_PATH = /home/home
|
VITE_HOME_PATH = /home
|
||||||
# 路由模式:web(路径) hash(锚点)
|
# 路由模式:web(路径) hash(锚点)
|
||||||
VITE_HISTORY = web
|
VITE_HISTORY = web
|
||||||
|
|
||||||
|
|
@ -24,7 +24,7 @@ VITE_PORT = 3020
|
||||||
# 代理前缀
|
# 代理前缀
|
||||||
VITE_PROXY_PREFIX = /api,/upload
|
VITE_PROXY_PREFIX = /api,/upload
|
||||||
# 代理地址
|
# 代理地址
|
||||||
VITE_PROXY = http://127.0.0.1:3030/
|
VITE_PROXY = https://appnify.app.juetan.cn/
|
||||||
# API文档 说明:需返回符合 OPENAPI 规范的json内容
|
# API文档 说明:需返回符合 OPENAPI 规范的json内容
|
||||||
VITE_OPENAPI = http://127.0.0.1:3030/openapi.json
|
VITE_OPENAPI = http://127.0.0.1:3030/openapi.json
|
||||||
# 文件后缀 说明:设为dev时会优先加载index.dev.vue文件,否则回退至index.vue文件
|
# 文件后缀 说明:设为dev时会优先加载index.dev.vue文件,否则回退至index.vue文件
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
16
src/App.vue
16
src/App.vue
|
|
@ -2,31 +2,31 @@
|
||||||
<a-config-provider>
|
<a-config-provider>
|
||||||
<router-view v-slot="{ Component, route }">
|
<router-view v-slot="{ Component, route }">
|
||||||
<keep-alive :include="menuStore.cacheTopNames">
|
<keep-alive :include="menuStore.cacheTopNames">
|
||||||
<component v-if="hasAuth(route, Component)" :is="Component"></component>
|
<component v-if="hasAuth(route)" :is="Component"></component>
|
||||||
<page-403 v-else></page-403>
|
<AnForbidden v-else></AnForbidden>
|
||||||
</keep-alive>
|
</keep-alive>
|
||||||
</router-view>
|
</router-view>
|
||||||
</a-config-provider>
|
</a-config-provider>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { RouteLocationNormalizedLoaded } from "vue-router";
|
import { RouteLocationNormalizedLoaded } from 'vue-router';
|
||||||
import { useUserStore } from "./store";
|
import { useUserStore } from './store';
|
||||||
import { useMenuStore } from "./store/menu";
|
import { useMenuStore } from './store/menu';
|
||||||
|
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
const menuStore = useMenuStore();
|
const menuStore = useMenuStore();
|
||||||
|
|
||||||
const hasAuth = (route: RouteLocationNormalizedLoaded, c: any) => {
|
const hasAuth = (route: RouteLocationNormalizedLoaded) => {
|
||||||
const aAuth = route.meta.auth;
|
const aAuth = route.meta.auth;
|
||||||
const uAuth = userStore.auth;
|
const uAuth = userStore.auth;
|
||||||
if (!aAuth?.length) {
|
if (!aAuth?.length) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (aAuth.some((i) => i === "*")) {
|
if (aAuth.some(i => i === '*')) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (uAuth.some((i) => aAuth.some((j) => j === i))) {
|
if (uAuth.some(i => aAuth.some(j => j === i))) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
export * from "./instance/api";
|
export * from './instance/api';
|
||||||
export * from "./generated/Api";
|
export * from './generated/Api';
|
||||||
export * from "./instance/useRequest";
|
export * from './instance/useRequest';
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import { Service } from "./service";
|
import { Service } from './service';
|
||||||
import { addToastInterceptor } from "../interceptors/toast";
|
import { addToastInterceptor } from '../interceptors/toast';
|
||||||
import { addAuthInterceptor } from "../interceptors/auth";
|
import { addAuthInterceptor } from '../interceptors/auth';
|
||||||
import { addExceptionInterceptor } from "../interceptors/exception";
|
import { addExceptionInterceptor } from '../interceptors/exception';
|
||||||
import { env } from "@/config/env";
|
import { env } from '@/config/env';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* API 接口实例
|
* API 接口实例
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { Api } from "../generated/Api";
|
import { Api } from '../generated/Api';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 扩展生成的API类
|
* 扩展生成的API类
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { Notification } from "@arco-design/web-vue";
|
import { Notification } from '@arco-design/web-vue';
|
||||||
import { AxiosInstance } from "axios";
|
import { AxiosInstance } from 'axios';
|
||||||
import { has, isString } from "lodash-es";
|
import { has, isString } from 'lodash-es';
|
||||||
|
|
||||||
const successCodes = [2000];
|
const successCodes = [2000];
|
||||||
const expiredCodes = [4050, 4051];
|
const expiredCodes = [4050, 4051];
|
||||||
|
|
@ -15,33 +15,32 @@ let logoutTipShowing = false;
|
||||||
* @param axios Axios实例
|
* @param axios Axios实例
|
||||||
*/
|
*/
|
||||||
export function addExceptionInterceptor(axios: AxiosInstance, exipreHandler?: (...args: any[]) => any) {
|
export function addExceptionInterceptor(axios: AxiosInstance, exipreHandler?: (...args: any[]) => any) {
|
||||||
axios.interceptors.request.use(null, (error) => {
|
axios.interceptors.request.use(null, error => {
|
||||||
const msg = error.response?.data?.message;
|
const msg = error.response?.data?.message;
|
||||||
Notification.error({
|
Notification.error({
|
||||||
title: "请求提示",
|
title: '请求提示',
|
||||||
content: msg ?? `发送请求失败,请检查参数或稍后重试!`,
|
content: msg ?? `发送请求失败,请检查参数或稍后重试!`,
|
||||||
});
|
});
|
||||||
return Promise.reject(error);
|
return Promise.reject(error);
|
||||||
});
|
});
|
||||||
|
|
||||||
axios.interceptors.response.use(
|
axios.interceptors.response.use(
|
||||||
(res) => {
|
res => {
|
||||||
const code = res.data?.code;
|
const code = res.data?.code;
|
||||||
if (code && !successCodes.includes(code)) {
|
if (code && !successCodes.includes(code)) {
|
||||||
return Promise.reject(res);
|
return Promise.reject(res);
|
||||||
}
|
}
|
||||||
return res;
|
return res;
|
||||||
},
|
},
|
||||||
(error) => {
|
error => {
|
||||||
// 服务端响应错误
|
|
||||||
if (error.response) {
|
if (error.response) {
|
||||||
const code = error.response.data?.code;
|
const code = error.response.data?.code;
|
||||||
if (expiredCodes.includes(code)) {
|
if (expiredCodes.includes(code)) {
|
||||||
if (!logoutTipShowing) {
|
if (!logoutTipShowing) {
|
||||||
logoutTipShowing = true;
|
logoutTipShowing = true;
|
||||||
Notification.warning({
|
Notification.warning({
|
||||||
title: "登陆提示",
|
title: '登陆提示',
|
||||||
content: "当前登陆已过期,请重新登陆!",
|
content: '当前登陆已过期,请重新登陆!',
|
||||||
onClose: () => (logoutTipShowing = false),
|
onClose: () => (logoutTipShowing = false),
|
||||||
});
|
});
|
||||||
exipreHandler?.(error);
|
exipreHandler?.(error);
|
||||||
|
|
@ -50,10 +49,10 @@ export function addExceptionInterceptor(axios: AxiosInstance, exipreHandler?: (.
|
||||||
}
|
}
|
||||||
const resMsg = error.response?.data?.message;
|
const resMsg = error.response?.data?.message;
|
||||||
let message: string | null = resMsg ?? resMessageTip;
|
let message: string | null = resMsg ?? resMessageTip;
|
||||||
if (error.config?.method === "get") {
|
if (error.config?.method === 'get') {
|
||||||
message = resGetMessage;
|
message = resGetMessage;
|
||||||
}
|
}
|
||||||
if (has(error.config, "resErrorTip")) {
|
if (has(error.config, 'resErrorTip')) {
|
||||||
const tip = error.config.resErrorTip;
|
const tip = error.config.resErrorTip;
|
||||||
if (tip) {
|
if (tip) {
|
||||||
message = isString(tip) ? tip : message;
|
message = isString(tip) ? tip : message;
|
||||||
|
|
@ -63,18 +62,15 @@ export function addExceptionInterceptor(axios: AxiosInstance, exipreHandler?: (.
|
||||||
}
|
}
|
||||||
if (message) {
|
if (message) {
|
||||||
Notification.error({
|
Notification.error({
|
||||||
title: "请求提示",
|
title: '请求提示',
|
||||||
content: message,
|
content: message,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return Promise.reject(error);
|
return Promise.reject(error);
|
||||||
}
|
} else if (error.request) {
|
||||||
|
|
||||||
// 客户端请求错误
|
|
||||||
if (error.request) {
|
|
||||||
const resMsg = error.response?.message;
|
const resMsg = error.response?.message;
|
||||||
let message: string | null = resMsg ?? reqMessageTip;
|
let message: string | null = resMsg ?? reqMessageTip;
|
||||||
if (has(error.config, "reqErrorTip")) {
|
if (has(error.config, 'reqErrorTip')) {
|
||||||
const tip = error.config.reqErrorTip;
|
const tip = error.config.reqErrorTip;
|
||||||
if (tip) {
|
if (tip) {
|
||||||
message = isString(tip) ? tip : message;
|
message = isString(tip) ? tip : message;
|
||||||
|
|
@ -84,7 +80,7 @@ export function addExceptionInterceptor(axios: AxiosInstance, exipreHandler?: (.
|
||||||
}
|
}
|
||||||
if (message) {
|
if (message) {
|
||||||
Notification.error({
|
Notification.error({
|
||||||
title: "请求提示",
|
title: '请求提示',
|
||||||
content: message,
|
content: message,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { IToastOptions, toast } from '@/components';
|
import { AnToastOptions, toast } from '@/components/AnToast';
|
||||||
import { Message } from '@arco-design/web-vue';
|
import { Message } from '@arco-design/web-vue';
|
||||||
import { AxiosInstance } from 'axios';
|
import { AxiosInstance } from 'axios';
|
||||||
|
|
||||||
|
|
@ -10,7 +10,7 @@ export function addToastInterceptor(axios: AxiosInstance) {
|
||||||
axios.interceptors.request.use(
|
axios.interceptors.request.use(
|
||||||
config => {
|
config => {
|
||||||
if (config.toast) {
|
if (config.toast) {
|
||||||
let options: IToastOptions = {};
|
let options: AnToastOptions = {};
|
||||||
if (typeof config.toast === 'string') {
|
if (typeof config.toast === 'string') {
|
||||||
options = { message: config.toast };
|
options = { message: config.toast };
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -80,7 +80,7 @@ export const AnForm = defineComponent({
|
||||||
},
|
},
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<Form layout="vertical" {...this.$attrs} {...this.formProps} ref="formRef" model={this.model}>
|
<Form layout="vertical" {...this.$attrs} {...this.formProps} class="an-form" ref="formRef" model={this.model}>
|
||||||
{this.items.map(item => (
|
{this.items.map(item => (
|
||||||
<AnFormItem key={item.field} item={item} items={this.items} model={this.model}></AnFormItem>
|
<AnFormItem key={item.field} item={item} items={this.items} model={this.model}></AnFormItem>
|
||||||
))}
|
))}
|
||||||
|
|
|
||||||
|
|
@ -89,6 +89,7 @@ export const AnFormItem = defineComponent({
|
||||||
return (
|
return (
|
||||||
<BaseFormItem
|
<BaseFormItem
|
||||||
{...props.item.itemProps}
|
{...props.item.itemProps}
|
||||||
|
class="an-form-item"
|
||||||
label={props.item.label as string}
|
label={props.item.label as string}
|
||||||
rules={rules.value}
|
rules={rules.value}
|
||||||
disabled={disabled.value}
|
disabled={disabled.value}
|
||||||
|
|
|
||||||
|
|
@ -149,6 +149,8 @@ export const AnFormModal = defineComponent({
|
||||||
onClose,
|
onClose,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
provide(AnFormModalContextKey, context);
|
||||||
|
|
||||||
return context;
|
return context;
|
||||||
},
|
},
|
||||||
render() {
|
render() {
|
||||||
|
|
@ -160,9 +162,10 @@ export const AnFormModal = defineComponent({
|
||||||
closable={false}
|
closable={false}
|
||||||
{...this.$attrs}
|
{...this.$attrs}
|
||||||
{...this.modalProps}
|
{...this.modalProps}
|
||||||
|
v-model:visible={this.visible}
|
||||||
|
class="an-form-modal"
|
||||||
maskClosable={false}
|
maskClosable={false}
|
||||||
onClose={this.onClose}
|
onClose={this.onClose}
|
||||||
v-model:visible={this.visible}
|
|
||||||
>
|
>
|
||||||
{{
|
{{
|
||||||
title: this.modalTitle,
|
title: this.modalTitle,
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { sleep } from "@/utils";
|
import { sleep } from '@/utils';
|
||||||
import { Message } from "@arco-design/web-vue";
|
import { Message } from '@arco-design/web-vue';
|
||||||
import { Ref } from "vue";
|
import { Ref } from 'vue';
|
||||||
|
|
||||||
export function useModalSubmit(props: any, formRef: any, visible: Ref<boolean>) {
|
export function useModalSubmit(props: any, formRef: any, visible: Ref<boolean>) {
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
|
|
@ -12,7 +12,6 @@ export function useModalSubmit(props: any, formRef: any, visible: Ref<boolean>)
|
||||||
try {
|
try {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
const data = formRef.value?.getModel() ?? {};
|
const data = formRef.value?.getModel() ?? {};
|
||||||
await sleep(5000);
|
|
||||||
const res = await props.submit?.(data, props.items);
|
const res = await props.submit?.(data, props.items);
|
||||||
const msg = res?.data?.message;
|
const msg = res?.data?.message;
|
||||||
msg && Message.success(msg);
|
msg && Message.success(msg);
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,58 @@
|
||||||
|
import { merge } from 'lodash-es';
|
||||||
import { AnFormModal, AnFormModalProps } from '../components/FormModal';
|
import { AnFormModal, AnFormModalProps } from '../components/FormModal';
|
||||||
import { useFormProps } from './useForm';
|
import { useFormProps } from './useForm';
|
||||||
import { FormItem } from './useItems';
|
import { FormItem } from './useItems';
|
||||||
|
|
||||||
export type FormModalUseOptions = Partial<Omit<AnFormModalProps, 'items'>> & {
|
export type FormModalUseOptions = Partial<Omit<AnFormModalProps, 'items'>> & {
|
||||||
|
/**
|
||||||
|
* 弹窗宽度
|
||||||
|
* @description 参数 `modalProps.width` 的便捷语法
|
||||||
|
* @example
|
||||||
|
* ```ts
|
||||||
|
* 580
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
width?: number;
|
||||||
|
/**
|
||||||
|
* 表单类名
|
||||||
|
* @description 参数 `formProps.class` 的便捷语法
|
||||||
|
* @example
|
||||||
|
* ```ts
|
||||||
|
* 'grid grid-cols-2'
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
formClass?: unknown;
|
||||||
|
/**
|
||||||
|
* 表单项
|
||||||
|
* @example
|
||||||
|
* ```tsx
|
||||||
|
* [{
|
||||||
|
* field: 'name',
|
||||||
|
* label: '昵称',
|
||||||
|
* setter: 'input'
|
||||||
|
* }]
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
items: FormItem[];
|
items: FormItem[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export function useFormModalProps(options: FormModalUseOptions): AnFormModalProps {
|
export function useFormModalProps(options: FormModalUseOptions): AnFormModalProps {
|
||||||
|
if (options.width) {
|
||||||
|
merge(options, { modalProps: { width: options.width } });
|
||||||
|
}
|
||||||
|
if (options.formClass) {
|
||||||
|
merge(options, { formProps: { class: options.formClass } });
|
||||||
|
}
|
||||||
const { items, model, formProps } = useFormProps({ ...options, submit: undefined });
|
const { items, model, formProps } = useFormProps({ ...options, submit: undefined });
|
||||||
const { trigger, title, submit, modalProps } = options;
|
const { trigger, title, submit, modalProps } = options;
|
||||||
return {
|
return {
|
||||||
|
trigger,
|
||||||
model,
|
model,
|
||||||
items,
|
items,
|
||||||
formProps,
|
|
||||||
trigger,
|
|
||||||
title,
|
title,
|
||||||
modalProps,
|
|
||||||
submit,
|
submit,
|
||||||
|
formProps,
|
||||||
|
modalProps,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -27,8 +63,7 @@ export function useFormModal(options: FormModalUseOptions) {
|
||||||
const rawProps = useFormModalProps(options);
|
const rawProps = useFormModalProps(options);
|
||||||
const props = reactive(rawProps);
|
const props = reactive(rawProps);
|
||||||
|
|
||||||
const component = () => {
|
const component = () => (
|
||||||
return (
|
|
||||||
<AnFormModal
|
<AnFormModal
|
||||||
ref={(el: any) => (modalRef.value = el)}
|
ref={(el: any) => (modalRef.value = el)}
|
||||||
title={props.title}
|
title={props.title}
|
||||||
|
|
@ -40,7 +75,6 @@ export function useFormModal(options: FormModalUseOptions) {
|
||||||
submit={props.submit}
|
submit={props.submit}
|
||||||
></AnFormModal>
|
></AnFormModal>
|
||||||
);
|
);
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
props,
|
props,
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,12 @@
|
||||||
import { AnForm, AnFormInstance, AnFormModal, AnFormModalInstance, AnFormModalProps, AnFormProps } from '@/components/AnForm';
|
import {
|
||||||
import AniEmpty from '@/components/empty/AniEmpty.vue';
|
AnForm,
|
||||||
|
AnFormInstance,
|
||||||
|
AnFormModal,
|
||||||
|
AnFormModalInstance,
|
||||||
|
AnFormModalProps,
|
||||||
|
AnFormProps,
|
||||||
|
} from '@/components/AnForm';
|
||||||
|
import AnEmpty from '@/components/AnEmpty/AnEmpty.vue';
|
||||||
import { Button, PaginationProps, Table, TableColumnData, TableData, TableInstance } from '@arco-design/web-vue';
|
import { Button, PaginationProps, Table, TableColumnData, TableData, TableInstance } from '@arco-design/web-vue';
|
||||||
import { isArray, isFunction, merge } from 'lodash-es';
|
import { isArray, isFunction, merge } from 'lodash-es';
|
||||||
import { InjectionKey, PropType, Ref, defineComponent, ref } from 'vue';
|
import { InjectionKey, PropType, Ref, defineComponent, ref } from 'vue';
|
||||||
|
|
@ -180,17 +187,17 @@ export const AnTable = defineComponent({
|
||||||
};
|
};
|
||||||
|
|
||||||
props.pluginer?.callSetupHook(context);
|
props.pluginer?.callSetupHook(context);
|
||||||
|
|
||||||
provide(AnTableContextKey, context);
|
provide(AnTableContextKey, context);
|
||||||
|
|
||||||
return context;
|
return context;
|
||||||
},
|
},
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div class="table w-full">
|
<div class="an-table table w-full">
|
||||||
<div class={`mb-3 flex gap-2 toolbar justify-between`}>
|
<div class={`mb-3 flex gap-2 toolbar justify-between`}>
|
||||||
{this.create && <AnFormModal {...this.create} ref="createRef"></AnFormModal>}
|
{this.create && <AnFormModal {...this.create} ref="createRef"></AnFormModal>}
|
||||||
{this.modify && <AnFormModal {...this.modify} trigger={false} ref="modifyRef"></AnFormModal>}
|
{this.modify && <AnFormModal {...this.modify} trigger={false} ref="modifyRef"></AnFormModal>}
|
||||||
|
{this.$slots.action?.(this.renderData)}
|
||||||
{this.pluginer?.actions && (
|
{this.pluginer?.actions && (
|
||||||
<div class={`flex-1 flex gap-2 items-center`}>
|
<div class={`flex-1 flex gap-2 items-center`}>
|
||||||
{this.pluginer.actions.map(Action => (
|
{this.pluginer.actions.map(Action => (
|
||||||
|
|
@ -242,7 +249,7 @@ export const AnTable = defineComponent({
|
||||||
onPageSizeChange={this.onPageSizeChange}
|
onPageSizeChange={this.onPageSizeChange}
|
||||||
>
|
>
|
||||||
{{
|
{{
|
||||||
empty: () => <AniEmpty />,
|
empty: () => <AnEmpty />,
|
||||||
...this.$slots,
|
...this.$slots,
|
||||||
}}
|
}}
|
||||||
</Table>
|
</Table>
|
||||||
|
|
|
||||||
|
|
@ -1,25 +1,6 @@
|
||||||
import { FormModalUseOptions, useFormModalProps } from '@/components/AnForm';
|
import { FormModalUseOptions, useFormModalProps } from '@/components/AnForm';
|
||||||
|
|
||||||
export type UseCreateFormOptions = FormModalUseOptions & {
|
export type UseCreateFormOptions = FormModalUseOptions & {};
|
||||||
/**
|
|
||||||
* 弹窗宽度
|
|
||||||
* @description 参数 `modalProps.width` 的便捷语法
|
|
||||||
* @example
|
|
||||||
* ```ts
|
|
||||||
* 580
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
width?: number;
|
|
||||||
/**
|
|
||||||
* 表单类名
|
|
||||||
* @description 参数 `formProps.class` 的便捷语法
|
|
||||||
* @example
|
|
||||||
* ```ts
|
|
||||||
* 'grid grid-cols-2'
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
formClass?: unknown;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function useCreateForm(options: UseCreateFormOptions) {
|
export function useCreateForm(options: UseCreateFormOptions) {
|
||||||
if (options.width) {
|
if (options.width) {
|
||||||
|
|
|
||||||
|
|
@ -4,24 +4,6 @@ import { ExtendFormItem } from './useSearchForm';
|
||||||
import { TableUseOptions } from './useTable';
|
import { TableUseOptions } from './useTable';
|
||||||
|
|
||||||
export type ModifyForm = Omit<FormModalUseOptions, 'items' | 'trigger'> & {
|
export type ModifyForm = Omit<FormModalUseOptions, 'items' | 'trigger'> & {
|
||||||
/**
|
|
||||||
* 弹窗宽度
|
|
||||||
* @description 参数 `modalProps.width` 的便捷语法
|
|
||||||
* @example
|
|
||||||
* ```ts
|
|
||||||
* 580
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
width?: number;
|
|
||||||
/**
|
|
||||||
* 表单类名
|
|
||||||
* @description 参数 `formProps.class` 的便捷语法
|
|
||||||
* @example
|
|
||||||
* ```ts
|
|
||||||
* 'grid grid-cols-2'
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
formClass?: unknown;
|
|
||||||
/**
|
/**
|
||||||
* 是否继承新建弹窗
|
* 是否继承新建弹窗
|
||||||
* @default
|
* @default
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import { SearchForm, useSearchForm } from './useSearchForm';
|
||||||
import { TableColumn, useTableColumns } from './useTableColumn';
|
import { TableColumn, useTableColumns } from './useTableColumn';
|
||||||
import { AnTablePlugin, PluginContainer } from './useTablePlugin';
|
import { AnTablePlugin, PluginContainer } from './useTablePlugin';
|
||||||
import { UseCreateFormOptions } from './useCreateForm';
|
import { UseCreateFormOptions } from './useCreateForm';
|
||||||
|
import { FunctionalComponent } from 'vue';
|
||||||
|
|
||||||
export interface TableUseOptions extends Pick<AnTableProps, 'source' | 'tableProps' | 'paging'> {
|
export interface TableUseOptions extends Pick<AnTableProps, 'source' | 'tableProps' | 'paging'> {
|
||||||
/**
|
/**
|
||||||
|
|
@ -103,7 +104,7 @@ export function useTable(options: TableUseOptions) {
|
||||||
const rawProps = useTableProps(options);
|
const rawProps = useTableProps(options);
|
||||||
const props = reactive(rawProps);
|
const props = reactive(rawProps);
|
||||||
|
|
||||||
const AnTabler = () => (
|
const AnTabler: FunctionalComponent = (_, { slots }) => (
|
||||||
<AnTable
|
<AnTable
|
||||||
ref={(el: any) => (tableRef.value = el)}
|
ref={(el: any) => (tableRef.value = el)}
|
||||||
tableProps={props.tableProps}
|
tableProps={props.tableProps}
|
||||||
|
|
@ -114,7 +115,9 @@ export function useTable(options: TableUseOptions) {
|
||||||
create={props.create as any}
|
create={props.create as any}
|
||||||
modify={props.modify as any}
|
modify={props.modify as any}
|
||||||
pluginer={pluginer}
|
pluginer={pluginer}
|
||||||
></AnTable>
|
>
|
||||||
|
{slots}
|
||||||
|
</AnTable>
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,10 @@ import { Divider, Link, TableColumnData } from '@arco-design/web-vue';
|
||||||
interface TableBaseColumn {
|
interface TableBaseColumn {
|
||||||
/**
|
/**
|
||||||
* 类型
|
* 类型
|
||||||
|
* @example
|
||||||
|
* ```tsx
|
||||||
|
* 'delete'
|
||||||
|
* ```
|
||||||
*/
|
*/
|
||||||
type?: undefined;
|
type?: undefined;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -76,7 +76,13 @@ export class PluginContainer {
|
||||||
widgets: any[] = [];
|
widgets: any[] = [];
|
||||||
|
|
||||||
constructor(private plugins: AnTablePlugin[]) {
|
constructor(private plugins: AnTablePlugin[]) {
|
||||||
this.plugins.unshift(useTableRefresh(), useColumnConfig(), useRowFormat(), useRowDelete(), useRowModify());
|
this.plugins.unshift(
|
||||||
|
useTableRefresh(),
|
||||||
|
useColumnConfig(),
|
||||||
|
useRowFormat(),
|
||||||
|
useRowDelete(),
|
||||||
|
useRowModify()
|
||||||
|
);
|
||||||
for (const plugin of plugins) {
|
for (const plugin of plugins) {
|
||||||
const action = plugin.action?.();
|
const action = plugin.action?.();
|
||||||
if (action) {
|
if (action) {
|
||||||
|
|
|
||||||
|
|
@ -47,12 +47,13 @@ export const TableColumnConfig = defineComponent({
|
||||||
dataIndex: column.dataIndex,
|
dataIndex: column.dataIndex,
|
||||||
title: column.title,
|
title: column.title,
|
||||||
enable: true,
|
enable: true,
|
||||||
autoWidth: !column.width,
|
autoWidth: false,
|
||||||
width: column.width ?? 60,
|
width: column.width ?? 60,
|
||||||
editable: !column.configable,
|
editable: !column.configable,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
items.value = list;
|
items.value = list;
|
||||||
|
onItemChange();
|
||||||
};
|
};
|
||||||
|
|
||||||
const onItemChange = () => {
|
const onItemChange = () => {
|
||||||
|
|
|
||||||
|
|
@ -8,18 +8,14 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
defineOptions({
|
|
||||||
name: "toast",
|
|
||||||
});
|
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
message: {
|
message: {
|
||||||
type: String,
|
type: String,
|
||||||
default: "正在操作中,请稍等...",
|
default: '正在操作中,请稍等...',
|
||||||
},
|
},
|
||||||
icon: {
|
icon: {
|
||||||
type: String,
|
type: String,
|
||||||
default: "icon-park-outline-loading-one",
|
default: 'icon-park-outline-loading-one',
|
||||||
},
|
},
|
||||||
iconRotate: {
|
iconRotate: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
|
|
@ -37,8 +33,8 @@ const props = defineProps({
|
||||||
|
|
||||||
const style = computed(() => {
|
const style = computed(() => {
|
||||||
return {
|
return {
|
||||||
pointerEvents: props.cover ? "initial" : "none",
|
pointerEvents: props.cover ? 'initial' : 'none',
|
||||||
backgroundColor: props.mask ? "rgba(0, 0, 0, 0.2)" : "transparent",
|
backgroundColor: props.mask ? 'rgba(0, 0, 0, 0.2)' : 'transparent',
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
@ -54,8 +50,8 @@ const style = computed(() => {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
pointer-events: v-bind("style.pointerEvents");
|
pointer-events: v-bind('style.pointerEvents');
|
||||||
background-color: v-bind("style.backgroundColor");
|
background-color: v-bind('style.backgroundColor');
|
||||||
}
|
}
|
||||||
.toast-content {
|
.toast-content {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
@ -0,0 +1,52 @@
|
||||||
|
import { createVNode, render } from 'vue';
|
||||||
|
import AnToast from './AnToast.vue';
|
||||||
|
|
||||||
|
export interface AnToastOptions {
|
||||||
|
/**
|
||||||
|
* 展示内容
|
||||||
|
* @default
|
||||||
|
* ```ts
|
||||||
|
* '正在操作中,请稍等...'
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
message?: string;
|
||||||
|
/**
|
||||||
|
* 图标
|
||||||
|
* @default
|
||||||
|
* ```ts
|
||||||
|
* 'icon-park-outline-loading-one'
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
icon?: string;
|
||||||
|
/**
|
||||||
|
* 是否显示遮罩层
|
||||||
|
* @default
|
||||||
|
* ```ts
|
||||||
|
* true
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
mask?: boolean;
|
||||||
|
/**
|
||||||
|
* 是否覆盖窗口(即不允许其他操作)
|
||||||
|
* @default
|
||||||
|
* ```ts
|
||||||
|
* true
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
cover?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const toast = (messageOrOptions?: string | AnToastOptions) => {
|
||||||
|
if (typeof messageOrOptions === 'string') {
|
||||||
|
messageOrOptions = { message: messageOrOptions };
|
||||||
|
}
|
||||||
|
const container = document.createElement('div');
|
||||||
|
const vnode = createVNode(AnToast, messageOrOptions as any);
|
||||||
|
render(vnode, container);
|
||||||
|
document.body.appendChild(container);
|
||||||
|
const close = () => {
|
||||||
|
render(null, container);
|
||||||
|
document.body.removeChild(container);
|
||||||
|
};
|
||||||
|
return close;
|
||||||
|
};
|
||||||
|
|
@ -1,156 +0,0 @@
|
||||||
<template>
|
|
||||||
<bread-page>
|
|
||||||
<template #content>
|
|
||||||
<div class="h-full w-full grid grid-cols-[auto_1fr] gap-4 p-4">
|
|
||||||
<div class="bg-white w-[256px]">
|
|
||||||
<div class="flex items-center justify-between gap-2 px-4 h-14">
|
|
||||||
<span class="text-base">菜单列表</span>
|
|
||||||
<div>
|
|
||||||
<a-button>
|
|
||||||
<template #icon>
|
|
||||||
<i class="icon-park-outline-plus"></i>
|
|
||||||
</template>
|
|
||||||
</a-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<a-tree
|
|
||||||
:data="menus"
|
|
||||||
:default-expand-all="true"
|
|
||||||
:block-node="true"
|
|
||||||
:field-names="{
|
|
||||||
icon: undefined,
|
|
||||||
title: 'name',
|
|
||||||
key: 'id',
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
<template #title="node">
|
|
||||||
<div class="group flex-1 flex items-center justify-between gap-2">
|
|
||||||
<div @click="onEdit(node)">
|
|
||||||
<!-- <a-tag :color="MenuTypes.fmt(node.type, 'color')" size="small" :bordered="true">
|
|
||||||
{{ MenuTypes.fmt(node.type) }}
|
|
||||||
</a-tag> -->
|
|
||||||
<i :class="node.icon" class="ml-2"></i>
|
|
||||||
{{ node.name }}
|
|
||||||
</div>
|
|
||||||
<div class="hidden group-hover:block">
|
|
||||||
<i
|
|
||||||
v-if="node.type === MenuType.MENU"
|
|
||||||
class="text-sm text-gray-400 hover:text-gray-700 icon-park-outline-plus"
|
|
||||||
></i>
|
|
||||||
<i class="text-sm text-gray-400 hover:text-gray-700 icon-park-outline-delete"></i>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</a-tree>
|
|
||||||
</div>
|
|
||||||
<div class="bg-white">
|
|
||||||
<a-card title="菜单信息" :bordered="false">
|
|
||||||
<Form ref="formRef" v-bind="form"></Form>
|
|
||||||
</a-card>
|
|
||||||
<a-divider :margin="0"></a-divider>
|
|
||||||
<div class="px-4 mt-4">
|
|
||||||
<btn-table></btn-table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</bread-page>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="tsx">
|
|
||||||
import { Menu, api } from "@/api";
|
|
||||||
import { useForm, Form, useAniTable, FormInstance } from "@/components";
|
|
||||||
import { MenuType, MenuTypes } from "@/constants/menu";
|
|
||||||
|
|
||||||
const formRef = ref<FormInstance | null>(null);
|
|
||||||
const menus = ref<any[]>([]);
|
|
||||||
const treeEach = (tree: any[], fn: any) => {
|
|
||||||
for (const item of tree) {
|
|
||||||
if (item.children) {
|
|
||||||
treeEach(item.children, fn);
|
|
||||||
}
|
|
||||||
fn(item);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const onEdit = (row: any) => {
|
|
||||||
formRef.value?.setModel(row);
|
|
||||||
(btn.props as any).data = row.buttons;
|
|
||||||
};
|
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
const res = await api.menu.getMenus({ tree: true });
|
|
||||||
const data = res.data.data ?? [];
|
|
||||||
treeEach(data, (item: Menu) => {
|
|
||||||
if (item.type === MenuType.BUTTON) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (item.type === MenuType.PAGE) {
|
|
||||||
(item as any).buttons = (item as any).children;
|
|
||||||
delete (item as any).children;
|
|
||||||
}
|
|
||||||
(item as any).iconRender = () => <i class={item.icon} />;
|
|
||||||
});
|
|
||||||
menus.value = data;
|
|
||||||
});
|
|
||||||
|
|
||||||
const form = useForm({
|
|
||||||
items: [
|
|
||||||
{
|
|
||||||
field: "name",
|
|
||||||
label: "菜单名称",
|
|
||||||
type: "input",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: "icon",
|
|
||||||
label: "菜单图标",
|
|
||||||
type: "input",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
async submit(arg) {
|
|
||||||
console.log(arg);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const [btnTable, btn] = useAniTable({
|
|
||||||
columns: [
|
|
||||||
{
|
|
||||||
title: " 名称",
|
|
||||||
dataIndex: "name",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "标识",
|
|
||||||
dataIndex: "code",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "操作",
|
|
||||||
type: "button",
|
|
||||||
width: 140,
|
|
||||||
buttons: [
|
|
||||||
{
|
|
||||||
type: "modify",
|
|
||||||
text: "修改",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: "删除",
|
|
||||||
type: "delete",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
create: {},
|
|
||||||
modify: {},
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="less" scoped></style>
|
|
||||||
|
|
||||||
<route lang="json">
|
|
||||||
{
|
|
||||||
"meta": {
|
|
||||||
"sort": 10302,
|
|
||||||
"title": "菜单管理",
|
|
||||||
"icon": "icon-park-outline-add-subtract"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</route>
|
|
||||||
|
|
@ -1,3 +1,2 @@
|
||||||
export * from "./form";
|
export * from './form';
|
||||||
export * from "./table";
|
export * from './table';
|
||||||
export * from "./toast";
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import AniEmpty from "@/components/empty/AniEmpty.vue";
|
import AniEmpty from "@/components/AnEmpty/AnEmpty.vue";
|
||||||
import { TableColumnData as BaseColumn, TableData as BaseData, Table as BaseTable } from "@arco-design/web-vue";
|
import { TableColumnData as BaseColumn, TableData as BaseData, Table as BaseTable } from "@arco-design/web-vue";
|
||||||
import { merge } from "lodash-es";
|
import { merge } from "lodash-es";
|
||||||
import { PropType, computed, defineComponent, reactive, ref } from "vue";
|
import { PropType, computed, defineComponent, reactive, ref } from "vue";
|
||||||
|
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
export * from "./toast";
|
|
||||||
|
|
@ -1,42 +0,0 @@
|
||||||
import { createVNode, render } from "vue";
|
|
||||||
import Toast from "./toast.vue";
|
|
||||||
|
|
||||||
export interface IToastOptions {
|
|
||||||
/**
|
|
||||||
* 展示内容
|
|
||||||
* @default '正在操作中,请稍等...'
|
|
||||||
*/
|
|
||||||
message?: string;
|
|
||||||
/**
|
|
||||||
* 图标
|
|
||||||
* @default 'icon-park-outline-loading-one'
|
|
||||||
*/
|
|
||||||
icon?: string;
|
|
||||||
/**
|
|
||||||
* 是否显示遮罩层
|
|
||||||
* @default true
|
|
||||||
*/
|
|
||||||
mask?: boolean;
|
|
||||||
/**
|
|
||||||
* 是否覆盖窗口(即不允许其他操作)
|
|
||||||
* @default true
|
|
||||||
*/
|
|
||||||
cover?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const toast = (messageOrOptions?: string | IToastOptions) => {
|
|
||||||
if (typeof messageOrOptions === "string") {
|
|
||||||
messageOrOptions = {
|
|
||||||
message: messageOrOptions,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
const container = document.createElement("div");
|
|
||||||
const vnode = createVNode(Toast, messageOrOptions as any);
|
|
||||||
render(vnode, container);
|
|
||||||
document.body.appendChild(container);
|
|
||||||
const close = () => {
|
|
||||||
render(null, container);
|
|
||||||
document.body.removeChild(container);
|
|
||||||
};
|
|
||||||
return close;
|
|
||||||
};
|
|
||||||
|
|
@ -9,30 +9,23 @@ class Constant<T extends Item[]> {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 格式化值
|
* 格式化值
|
||||||
* @param value 值
|
|
||||||
* @param key 对应属性名,默认为label
|
|
||||||
* @returns
|
|
||||||
*/
|
*/
|
||||||
fmt<K extends T[number]["value"]>(value: K, key?: keyof T[number]) {
|
fmt<K extends T[number]['value']>(value: K, key?: keyof T[number]) {
|
||||||
return this.raw.find((item) => item.value === value)?.[key ?? ("label" as any)];
|
return this.raw.find(item => item.value === value)?.[key ?? ('label' as any)];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取对应属性值组成的数组
|
* 获取对应属性值组成的数组
|
||||||
* @param key 属性名,默认为value
|
|
||||||
* @returns
|
|
||||||
*/
|
*/
|
||||||
val<K extends keyof T[number]>(key?: K) {
|
val<K extends keyof T[number]>(key?: K) {
|
||||||
return this.raw.map((item) => item[key ?? ("value" as any)]);
|
return this.raw.map(item => item[key ?? ('value' as any)]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取值对应的对象
|
* 获取值对应的对象
|
||||||
* @param value 值
|
|
||||||
* @returns
|
|
||||||
*/
|
*/
|
||||||
get(value: any) {
|
get(value: any) {
|
||||||
return this.raw.find((item) => item.value === value);
|
return this.raw.find(item => item.value === value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,28 +1,28 @@
|
||||||
import dayjs from "dayjs";
|
import dayjs from 'dayjs';
|
||||||
import "dayjs/locale/zh-cn";
|
import 'dayjs/locale/zh-cn';
|
||||||
import localData from "dayjs/plugin/localeData";
|
import localData from 'dayjs/plugin/localeData';
|
||||||
import relativeTime from "dayjs/plugin/relativeTime";
|
import relativeTime from 'dayjs/plugin/relativeTime';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* 默认日期时间格式
|
* 默认日期时间格式
|
||||||
*/
|
*/
|
||||||
const DATETIME = "YYYY-MM-DD HH:mm";
|
const DATETIME = 'YYYY-MM-DD HH:mm';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 默认日期格式
|
* 默认日期格式
|
||||||
*/
|
*/
|
||||||
const DATE = "YYYY-MM-DD";
|
const DATE = 'YYYY-MM-DD';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 默认时间格式
|
* 默认时间格式
|
||||||
*/
|
*/
|
||||||
const TIME = "HH:mm:ss";
|
const TIME = 'HH:mm:ss';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 中文语言包
|
* 中文语言包
|
||||||
*/
|
*/
|
||||||
dayjs.locale("zh-cn");
|
dayjs.locale('zh-cn');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 相对时间插件
|
* 相对时间插件
|
||||||
|
|
@ -37,7 +37,6 @@ dayjs.extend(relativeTime);
|
||||||
dayjs.extend(localData);
|
dayjs.extend(localData);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
|
||||||
* 默认时间格式
|
* 默认时间格式
|
||||||
*/
|
*/
|
||||||
dayjs.DATETIME = DATETIME;
|
dayjs.DATETIME = DATETIME;
|
||||||
|
|
@ -53,9 +52,13 @@ dayjs.DATE = DATE;
|
||||||
dayjs.TIME = TIME;
|
dayjs.TIME = TIME;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 重写format方法,如果没有传入format参数,则使用默认的时间格式
|
* 保留原方法
|
||||||
*/
|
*/
|
||||||
dayjs.prototype._format = dayjs.prototype.format;
|
dayjs.prototype._format = dayjs.prototype.format;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重写,设置默认时间格式
|
||||||
|
*/
|
||||||
dayjs.prototype.format = function (format?: string) {
|
dayjs.prototype.format = function (format?: string) {
|
||||||
if (format) {
|
if (format) {
|
||||||
return this._format(format);
|
return this._format(format);
|
||||||
|
|
@ -64,4 +67,3 @@ dayjs.prototype.format = function (format?: string) {
|
||||||
};
|
};
|
||||||
|
|
||||||
export { DATE, DATETIME, TIME, dayjs };
|
export { DATE, DATETIME, TIME, dayjs };
|
||||||
|
|
||||||
|
|
|
||||||
12
src/main.ts
12
src/main.ts
|
|
@ -1,8 +1,8 @@
|
||||||
import { createApp } from "vue";
|
import { createApp } from 'vue';
|
||||||
import App from "./App.vue";
|
import App from './App.vue';
|
||||||
import { router } from "./router";
|
import { router } from './router';
|
||||||
import { store } from "./store";
|
import { store } from './store';
|
||||||
import { style } from "./styles";
|
import { style } from './styles';
|
||||||
|
|
||||||
const run = async () => {
|
const run = async () => {
|
||||||
const app = createApp(App);
|
const app = createApp(App);
|
||||||
|
|
@ -10,7 +10,7 @@ const run = async () => {
|
||||||
app.use(style);
|
app.use(style);
|
||||||
app.use(router);
|
app.use(router);
|
||||||
await router.isReady();
|
await router.isReady();
|
||||||
app.mount("#app");
|
app.mount('#app');
|
||||||
};
|
};
|
||||||
|
|
||||||
run();
|
run();
|
||||||
|
|
|
||||||
|
|
@ -1,52 +1,45 @@
|
||||||
<template>
|
<template>
|
||||||
<BreadPage>
|
<BreadPage>
|
||||||
<Table v-bind="table"> </Table>
|
<CategoryTable />
|
||||||
</BreadPage>
|
</BreadPage>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="tsx">
|
<script setup lang="tsx">
|
||||||
import { api } from "@/api";
|
import { api } from '@/api';
|
||||||
import { Table, createColumn, updateColumn, useTable } from "@/components";
|
import { useCreateColumn, useTable, useUpdateColumn } from '@/components/AnTable';
|
||||||
import { listToTree } from "@/utils/listToTree";
|
import { listToTree } from '@/utils/listToTree';
|
||||||
|
|
||||||
const table = useTable({
|
const { component: CategoryTable } = useTable({
|
||||||
data: async (model, paging) => {
|
|
||||||
const res = await api.category.getCategories({ ...model, ...paging });
|
|
||||||
const data = listToTree(res.data.data ?? []);
|
|
||||||
return { data: { data, total: (res.data as any).total } };
|
|
||||||
},
|
|
||||||
columns: [
|
columns: [
|
||||||
{
|
{
|
||||||
title: "名称",
|
title: '名称',
|
||||||
dataIndex: "title",
|
dataIndex: 'title',
|
||||||
width: 240,
|
width: 240,
|
||||||
render({ record }) {
|
render: ({ record }) => (
|
||||||
return (
|
|
||||||
<div class="flex flex-col overflow-hidden">
|
<div class="flex flex-col overflow-hidden">
|
||||||
<span>{record.title}</span>
|
<span>{record.title}</span>
|
||||||
<span class="text-gray-400 text-xs truncate">@{record.slug}</span>
|
<span class="text-gray-400 text-xs truncate">#{record.slug}</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
),
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "描述",
|
title: '描述',
|
||||||
dataIndex: "description",
|
dataIndex: 'description',
|
||||||
},
|
},
|
||||||
createColumn,
|
useCreateColumn(),
|
||||||
updateColumn,
|
useUpdateColumn(),
|
||||||
{
|
{
|
||||||
type: "button",
|
type: 'button',
|
||||||
title: "操作",
|
title: '操作',
|
||||||
width: 120,
|
width: 120,
|
||||||
buttons: [
|
buttons: [
|
||||||
{
|
{
|
||||||
type: "modify",
|
type: 'modify',
|
||||||
text: "修改",
|
text: '修改',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "delete",
|
type: 'delete',
|
||||||
text: "删除",
|
text: '删除',
|
||||||
onClick({ record }) {
|
onClick({ record }) {
|
||||||
return api.category.delCategory(record.id);
|
return api.category.delCategory(record.id);
|
||||||
},
|
},
|
||||||
|
|
@ -54,76 +47,52 @@ const table = useTable({
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
search: {
|
source: async model => {
|
||||||
button: false,
|
const res = await api.category.getCategories(model);
|
||||||
items: [
|
const data = listToTree(res.data.data ?? []);
|
||||||
{
|
return { data: { data, total: (res.data as any).total } };
|
||||||
field: "nickname",
|
|
||||||
label: "登陆账号",
|
|
||||||
type: "search",
|
|
||||||
required: false,
|
|
||||||
enableLoad: true,
|
|
||||||
nodeProps: {
|
|
||||||
placeholder: "分类名称",
|
|
||||||
} as any,
|
|
||||||
itemProps: {
|
|
||||||
hideLabel: true,
|
|
||||||
},
|
},
|
||||||
|
search: [
|
||||||
|
{
|
||||||
|
field: 'nickname',
|
||||||
|
label: '登陆账号',
|
||||||
|
setter: 'search',
|
||||||
|
enterable: true,
|
||||||
|
searchable: true,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
|
||||||
create: {
|
create: {
|
||||||
title: "添加分类",
|
title: '添加分类',
|
||||||
modalProps: {
|
|
||||||
width: 580,
|
width: 580,
|
||||||
},
|
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
field: "parentId",
|
field: 'title',
|
||||||
label: "父级分类",
|
label: '分类名称',
|
||||||
type: "select",
|
setter: 'input',
|
||||||
options: async () => {
|
|
||||||
const res = await api.category.getCategories({ size: 0 });
|
|
||||||
return (res.data.data ?? []).map(({ id, title }: any) => ({ value: id, label: title }));
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: "title",
|
|
||||||
label: "分类名称",
|
|
||||||
type: "input",
|
|
||||||
required: true,
|
required: true,
|
||||||
nodeProps: {
|
|
||||||
placeholder: "请输入分类名称",
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: "slug",
|
field: 'slug',
|
||||||
label: "分类别名",
|
label: '分类别名',
|
||||||
type: "input",
|
setter: 'input',
|
||||||
required: true,
|
required: true,
|
||||||
nodeProps: {
|
|
||||||
placeholder: "请输入分类别名",
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: "description",
|
field: 'description',
|
||||||
label: "描述",
|
label: '描述',
|
||||||
type: "textarea",
|
setter: 'textarea',
|
||||||
required: false,
|
required: false,
|
||||||
nodeProps: {
|
|
||||||
placeholder: "请输入描述",
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
submit: async ({ model }) => {
|
submit: model => {
|
||||||
return api.category.addCategory(model);
|
return api.category.addCategory(model as any);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
modify: {
|
modify: {
|
||||||
extend: true,
|
extend: true,
|
||||||
title: "修改分类",
|
title: '修改分类',
|
||||||
submit: async ({ model }) => {
|
submit: model => {
|
||||||
return api.category.setCategory(model.id, model);
|
return api.category.setCategory(model.id, model as any);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,6 @@
|
||||||
<div></div>
|
<div></div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="tsx"></script>
|
|
||||||
|
|
||||||
<style lang="less"></style>
|
|
||||||
|
|
||||||
<route lang="json">
|
<route lang="json">
|
||||||
{
|
{
|
||||||
"meta": {
|
"meta": {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="w-[210px] h-full overflow-hidden grid grid-rows-[auto_1fr]">
|
<div class="w-[210px] h-full overflow-hidden grid grid-rows-[auto_1fr]">
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<a-input-search allow-clear placeholder="文件分类" class="mb-2" @search="updateFileCategories"></a-input-search>
|
<a-input-search allow-clear placeholder="分类名称" class="mb-2" @search="updateFileCategories"></a-input-search>
|
||||||
<a-button @click="formCtx.open">
|
<a-button @click="formCtx.open">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<i class="icon-park-outline-add"></i>
|
<i class="icon-park-outline-add"></i>
|
||||||
|
|
@ -47,18 +47,18 @@
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<ani-empty v-else></ani-empty>
|
<an-empty v-else></an-empty>
|
||||||
</a-spin>
|
</a-spin>
|
||||||
</a-scrollbar>
|
</a-scrollbar>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { FileCategory, api } from "@/api";
|
import { FileCategory, api } from '@/api';
|
||||||
import { useAniFormModal } from "@/components";
|
import { useAniFormModal } from '@/components';
|
||||||
import { delConfirm } from "@/utils";
|
import { delConfirm } from '@/utils';
|
||||||
import { Message } from "@arco-design/web-vue";
|
import { Message } from '@arco-design/web-vue';
|
||||||
import { PropType } from "vue";
|
import { PropType } from 'vue';
|
||||||
|
|
||||||
defineProps({
|
defineProps({
|
||||||
current: {
|
current: {
|
||||||
|
|
@ -66,7 +66,7 @@ defineProps({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits(["change"]);
|
const emit = defineEmits(['change']);
|
||||||
const list = ref<FileCategory[]>([]);
|
const list = ref<FileCategory[]>([]);
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
|
|
||||||
|
|
@ -75,8 +75,8 @@ const updateFileCategories = async () => {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
const res = await api.fileCategory.getFileCategorys({ size: 0 });
|
const res = await api.fileCategory.getFileCategorys({ size: 0 });
|
||||||
list.value = res.data.data ?? [];
|
list.value = res.data.data ?? [];
|
||||||
list.value.unshift({ id: undefined, name: "全部" } as any);
|
list.value.unshift({ id: undefined, name: '全部' } as any);
|
||||||
list.value.length && emit("change", list.value[0]);
|
list.value.length && emit('change', list.value[0]);
|
||||||
} catch {
|
} catch {
|
||||||
// nothing to do
|
// nothing to do
|
||||||
} finally {
|
} finally {
|
||||||
|
|
@ -93,7 +93,7 @@ const onDeleteRow = async (row: FileCategory) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const [formModal, formCtx] = useAniFormModal({
|
const [formModal, formCtx] = useAniFormModal({
|
||||||
title: ({ model }) => (!model.id ? "新建分类" : "修改分类"),
|
title: ({ model }) => (!model.id ? '新建分类' : '修改分类'),
|
||||||
trigger: false,
|
trigger: false,
|
||||||
modalProps: {
|
modalProps: {
|
||||||
width: 580,
|
width: 580,
|
||||||
|
|
@ -103,19 +103,19 @@ const [formModal, formCtx] = useAniFormModal({
|
||||||
},
|
},
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
field: "name",
|
field: 'name',
|
||||||
label: "分类名称",
|
label: '分类名称',
|
||||||
type: "input",
|
type: 'input',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: "code",
|
field: 'code',
|
||||||
label: "分类编码",
|
label: '分类编码',
|
||||||
type: "input",
|
type: 'input',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: "description",
|
field: 'description',
|
||||||
label: "备注",
|
label: '备注',
|
||||||
type: "textarea",
|
type: 'textarea',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
submit: async ({ model }) => {
|
submit: async ({ model }) => {
|
||||||
|
|
@ -1,10 +1,15 @@
|
||||||
<template>
|
<template>
|
||||||
<a-button type="primary" @click="visible = true"> 上传文件 </a-button>
|
<a-button type="primary" @click="visible = true">
|
||||||
|
<template #icon>
|
||||||
|
<i class="icon-park-outline-upload"></i>
|
||||||
|
</template>
|
||||||
|
上传
|
||||||
|
</a-button>
|
||||||
<a-modal
|
<a-modal
|
||||||
v-model:visible="visible"
|
v-model:visible="visible"
|
||||||
title="上传文件"
|
title="上传文件"
|
||||||
title-align="start"
|
title-align="start"
|
||||||
:width="860"
|
:width="940"
|
||||||
:mask-closable="false"
|
:mask-closable="false"
|
||||||
:on-before-cancel="onBeforeCancel"
|
:on-before-cancel="onBeforeCancel"
|
||||||
@close="onClose"
|
@close="onClose"
|
||||||
|
|
@ -22,7 +27,7 @@
|
||||||
@error="onUploadError"
|
@error="onUploadError"
|
||||||
>
|
>
|
||||||
<template #upload-button>
|
<template #upload-button>
|
||||||
<a-button type="outline"> 选择文件 </a-button>
|
<a-button type="primary"> 选择文件 </a-button>
|
||||||
</template>
|
</template>
|
||||||
</a-upload>
|
</a-upload>
|
||||||
<div class="flex-1 flex items-center text-gray-400">
|
<div class="flex-1 flex items-center text-gray-400">
|
||||||
|
|
@ -45,20 +50,21 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center justify-between gap-2 text-gray-400 mb-[-4px] mt-0.5">
|
<div class="flex items-center justify-between gap-2 text-gray-400 mb-[-4px] mt-0.5">
|
||||||
<span class="text-xs text-gray-400">
|
<span class="text-xs text-gray-400">
|
||||||
{{ numeral(item.file?.size).format("0 b") }}
|
{{ numeral(item.file?.size).format('0 b') }}
|
||||||
</span>
|
</span>
|
||||||
<span class="text-xs">
|
<span class="text-xs">
|
||||||
<span v-if="item.status === 'init'"> </span>
|
<span v-if="item.status === 'init'"> </span>
|
||||||
<span v-else-if="item.status === 'uploading'">
|
<span v-else-if="item.status === 'uploading'">
|
||||||
<span class="text-xs">
|
<span class="text-xs">
|
||||||
速度:{{ numeral(fileMap.get(item.uid)?.speed || 0).format("0 b") }}/s,
|
速度:{{ numeral(fileMap.get(item.uid)?.speed || 0).format('0 b') }}/s, 进度:{{
|
||||||
进度:{{ Math.floor((item.percent || 0) * 100) }}%
|
Math.floor((item.percent || 0) * 100)
|
||||||
|
}}%
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
<span v-else-if="item.status === 'done'" class="text-green-600">
|
<span v-else-if="item.status === 'done'" class="text-green-600">
|
||||||
完成(
|
完成( 耗时:{{ fileMap.get(item.uid)?.cost || 0 }}秒, 平均:{{
|
||||||
耗时:{{ fileMap.get(item.uid)?.cost || 0 }}秒,
|
numeral(fileMap.get(item.uid)?.aspeed || 0).format('0 b')
|
||||||
平均:{{ numeral(fileMap.get(item.uid)?.aspeed || 0).format("0 b") }}/s)
|
}}/s)
|
||||||
</span>
|
</span>
|
||||||
<span v-else="item.status === 'error'" class="text-red-500">
|
<span v-else="item.status === 'error'" class="text-red-500">
|
||||||
失败(原因:{{ fileMap.get(item.uid)?.error }})
|
失败(原因:{{ fileMap.get(item.uid)?.error }})
|
||||||
|
|
@ -77,7 +83,7 @@
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<div v-else class="h-[424px] flex items-center justify-center">
|
<div v-else class="h-[424px] flex items-center justify-center">
|
||||||
<ani-empty></ani-empty>
|
<an-empty></an-empty>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<template #footer>
|
<template #footer>
|
||||||
|
|
@ -97,16 +103,16 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { RequestParams, api } from "@/api";
|
import { RequestParams, api } from '@/api';
|
||||||
import { delConfirm } from "@/utils";
|
import { delConfirm } from '@/utils';
|
||||||
import { FileItem, Message, RequestOption, UploadInstance } from "@arco-design/web-vue";
|
import { FileItem, Message, RequestOption, UploadInstance } from '@arco-design/web-vue';
|
||||||
import axios from "axios";
|
import axios from 'axios';
|
||||||
import numeral from "numeral";
|
import numeral from 'numeral';
|
||||||
import { getIcon } from "./util";
|
import { getIcon } from './util';
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(event: "success", item: FileItem): void;
|
(event: 'success', item: FileItem): void;
|
||||||
(event: "close", count: number): void;
|
(event: 'close', count: number): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const visible = ref(false);
|
const visible = ref(false);
|
||||||
|
|
@ -137,10 +143,10 @@ const stat = computed(() => {
|
||||||
errorCount: 0,
|
errorCount: 0,
|
||||||
};
|
};
|
||||||
for (const item of fileList.value) {
|
for (const item of fileList.value) {
|
||||||
if (item.status === "init") result.initCount++;
|
if (item.status === 'init') result.initCount++;
|
||||||
if (item.status === "uploading") result.uploadingCount++;
|
if (item.status === 'uploading') result.uploadingCount++;
|
||||||
if (item.status === "done") result.doneCount++;
|
if (item.status === 'done') result.doneCount++;
|
||||||
if (item.status === "error") result.errorCount++;
|
if (item.status === 'error') result.errorCount++;
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
});
|
});
|
||||||
|
|
@ -160,7 +166,7 @@ const pauseItem = (item: FileItem) => {
|
||||||
uploadRef.value?.abort(item);
|
uploadRef.value?.abort(item);
|
||||||
const file = fileMap.get(item.uid);
|
const file = fileMap.get(item.uid);
|
||||||
if (file) {
|
if (file) {
|
||||||
file.error = "手动中止";
|
file.error = '手动中止';
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -169,7 +175,7 @@ const pauseItem = (item: FileItem) => {
|
||||||
* @param item 文件
|
* @param item 文件
|
||||||
*/
|
*/
|
||||||
const removeItem = (item: FileItem) => {
|
const removeItem = (item: FileItem) => {
|
||||||
const index = fileList.value.findIndex((i) => i.uid === item.uid);
|
const index = fileList.value.findIndex(i => i.uid === item.uid);
|
||||||
if (index > -1) {
|
if (index > -1) {
|
||||||
fileList.value.splice(index, 1);
|
fileList.value.splice(index, 1);
|
||||||
}
|
}
|
||||||
|
|
@ -188,7 +194,7 @@ const retryItem = (item: FileItem) => {
|
||||||
*/
|
*/
|
||||||
const clearUploaded = async () => {
|
const clearUploaded = async () => {
|
||||||
if (stat.value.doneCount !== fileList.value.length) {
|
if (stat.value.doneCount !== fileList.value.length) {
|
||||||
await delConfirm("当前有未上传完成的文件,是否继续清空?");
|
await delConfirm('当前有未上传完成的文件,是否继续清空?');
|
||||||
}
|
}
|
||||||
fileList.value = [];
|
fileList.value = [];
|
||||||
};
|
};
|
||||||
|
|
@ -198,7 +204,7 @@ const clearUploaded = async () => {
|
||||||
* @param item 文件
|
* @param item 文件
|
||||||
*/
|
*/
|
||||||
const onUploadSuccess = (item: FileItem) => {
|
const onUploadSuccess = (item: FileItem) => {
|
||||||
emit("success", item);
|
emit('success', item);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -208,7 +214,7 @@ const onUploadSuccess = (item: FileItem) => {
|
||||||
const onUploadError = (item: FileItem) => {
|
const onUploadError = (item: FileItem) => {
|
||||||
const file = fileMap.get(item.uid);
|
const file = fileMap.get(item.uid);
|
||||||
if (file) {
|
if (file) {
|
||||||
file.error = item.response?.data?.message || "网络异常";
|
file.error = item.response?.data?.message || '网络异常';
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -216,8 +222,8 @@ const onUploadError = (item: FileItem) => {
|
||||||
* 关闭前检测
|
* 关闭前检测
|
||||||
*/
|
*/
|
||||||
const onBeforeCancel = () => {
|
const onBeforeCancel = () => {
|
||||||
if (fileList.value.some((i) => i.status === "uploading")) {
|
if (fileList.value.some(i => i.status === 'uploading')) {
|
||||||
Message.warning("提示:文件上传中,请稍后再试!");
|
Message.warning('提示:文件上传中,请稍后再试!');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -229,7 +235,7 @@ const onBeforeCancel = () => {
|
||||||
const onClose = () => {
|
const onClose = () => {
|
||||||
fileMap.clear();
|
fileMap.clear();
|
||||||
fileList.value = [];
|
fileList.value = [];
|
||||||
emit("close", stat.value.doneCount);
|
emit('close', stat.value.doneCount);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -246,7 +252,7 @@ const upload = (option: RequestOption) => {
|
||||||
cost: 0,
|
cost: 0,
|
||||||
speed: 0,
|
speed: 0,
|
||||||
aspeed: 0,
|
aspeed: 0,
|
||||||
error: "网络异常",
|
error: '网络异常',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const item = fileMap.get(fileItem.uid)!;
|
const item = fileMap.get(fileItem.uid)!;
|
||||||
|
|
@ -262,7 +268,7 @@ const upload = (option: RequestOption) => {
|
||||||
const nowTime = Date.now();
|
const nowTime = Date.now();
|
||||||
const diff = (e.loaded - lastLoaded) / (nowTime - lastTime);
|
const diff = (e.loaded - lastLoaded) / (nowTime - lastTime);
|
||||||
const speed = Math.floor(diff * 1000);
|
const speed = Math.floor(diff * 1000);
|
||||||
item.aspeed = (item.speed + speed) / 2
|
item.aspeed = (item.speed + speed) / 2;
|
||||||
item.speed = speed;
|
item.speed = speed;
|
||||||
item.lastLoaded = e.loaded;
|
item.lastLoaded = e.loaded;
|
||||||
item.lastTime = nowTime;
|
item.lastTime = nowTime;
|
||||||
|
|
@ -297,15 +303,15 @@ defineExpose({
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO
|
// TODO
|
||||||
const group = ref("default");
|
const group = ref('default');
|
||||||
const groupOptions = [
|
const groupOptions = [
|
||||||
{
|
{
|
||||||
label: "默认分类",
|
label: '默认分类',
|
||||||
value: "default",
|
value: 'default',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "视频分类",
|
label: '视频分类',
|
||||||
value: "video",
|
value: 'video',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
</script>
|
</script>
|
||||||
|
|
@ -2,16 +2,13 @@
|
||||||
<BreadPage>
|
<BreadPage>
|
||||||
<template #content>
|
<template #content>
|
||||||
<div class="overflow-hidden grid grid-cols-[auto_1fr] gap-2 m-4">
|
<div class="overflow-hidden grid grid-cols-[auto_1fr] gap-2 m-4">
|
||||||
<ani-group class="bg-white p-4 w-[242px]" :current="current" @change="onCategoryChange"></ani-group>
|
<AnGroup class="bg-white p-4 w-[242px]" :current="current" @change="onCategoryChange"></AnGroup>
|
||||||
<div class="bg-white p-4">
|
<div class="bg-white p-4">
|
||||||
<file-table>
|
<MaterialTable>
|
||||||
<template #action>
|
<template #action>
|
||||||
<ani-upload @close="onUploadClose"></ani-upload>
|
<AnUpload></AnUpload>
|
||||||
<a-button type="primary" status="danger" :disabled="!selected.length" @click="onDeleteMany">
|
|
||||||
批量删除
|
|
||||||
</a-button>
|
|
||||||
</template>
|
</template>
|
||||||
</file-table>
|
</MaterialTable>
|
||||||
<a-image-preview v-model:visible="visible" :src="image"></a-image-preview>
|
<a-image-preview v-model:visible="visible" :src="image"></a-image-preview>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -20,70 +17,47 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="tsx">
|
<script setup lang="tsx">
|
||||||
import { FileCategory, api } from "@/api";
|
import { FileCategory, api } from '@/api';
|
||||||
import { createColumn, updateColumn, useAniTable } from "@/components";
|
import { useCreateColumn, useTable, useUpdateColumn } from '@/components/AnTable';
|
||||||
import { delConfirm } from "@/utils";
|
import { getIcon } from './components/util';
|
||||||
import { Message } from "@arco-design/web-vue";
|
import numeral from 'numeral';
|
||||||
import numeral from "numeral";
|
import AnGroup from './components/AnGroup.vue';
|
||||||
import AniGroup from "./components/group.vue";
|
import AnUpload from './components/AnUpload.vue';
|
||||||
import AniUpload from "./components/upload.vue";
|
|
||||||
import { getIcon } from "./components/util";
|
|
||||||
|
|
||||||
const visible = ref(false);
|
const visible = ref(false);
|
||||||
const image = ref("");
|
|
||||||
const selected = ref<number[]>([]);
|
|
||||||
const current = ref<FileCategory>();
|
const current = ref<FileCategory>();
|
||||||
|
const image = ref('');
|
||||||
|
|
||||||
const preview = (record: any) => {
|
const preview = (record: any) => {
|
||||||
if (!record.mimetype.startsWith("image")) {
|
if (!record.mimetype.startsWith('image')) {
|
||||||
window.open(record.path, "_blank");
|
window.open(record.path, '_blank');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
image.value = record.path;
|
image.value = record.path;
|
||||||
visible.value = true;
|
visible.value = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
const onUploadClose = (count: number) => {
|
|
||||||
if (count) {
|
|
||||||
fileCtx.refresh();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const onDeleteMany = async () => {
|
|
||||||
await delConfirm();
|
|
||||||
const res = await api.file.delFiles(selected.value as any[]);
|
|
||||||
selected.value = [];
|
|
||||||
Message.success(res.data.message);
|
|
||||||
fileCtx.refresh();
|
|
||||||
};
|
|
||||||
|
|
||||||
const onCategoryChange = (category: FileCategory) => {
|
const onCategoryChange = (category: FileCategory) => {
|
||||||
if (fileCtx.props.search?.model) {
|
if (props.search?.model) {
|
||||||
fileCtx.props.search.model.categoryId = category.id;
|
props.search.model.categoryId = category.id;
|
||||||
}
|
}
|
||||||
current.value = category;
|
current.value = category;
|
||||||
fileCtx.refresh();
|
tableRef.value?.refresh();
|
||||||
};
|
};
|
||||||
|
|
||||||
const [fileTable, fileCtx] = useAniTable({
|
const {
|
||||||
data: async (model, paging) => {
|
component: MaterialTable,
|
||||||
return api.file.getFiles({ ...model, ...paging });
|
tableRef,
|
||||||
},
|
props,
|
||||||
tableProps: {
|
} = useTable({
|
||||||
rowSelection: {
|
|
||||||
showCheckedAll: true,
|
|
||||||
},
|
|
||||||
onSelectionChange(rowKeys) {
|
|
||||||
selected.value = rowKeys as number[];
|
|
||||||
},
|
|
||||||
},
|
|
||||||
columns: [
|
columns: [
|
||||||
{
|
{
|
||||||
title: "文件名称",
|
title: '文件名称',
|
||||||
dataIndex: "name",
|
dataIndex: 'name',
|
||||||
render: ({ record }) => (
|
render: ({ record }) => (
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<div class="w-8 flex justify-center">
|
<div class="w-8 flex justify-center">
|
||||||
{record.mimetype.startsWith("image") ? (
|
{record.mimetype.startsWith('image') ? (
|
||||||
<a-avatar size={26} shape="square">
|
<a-avatar size={26} shape="square">
|
||||||
<img src={record.path}></img>
|
<img src={record.path}></img>
|
||||||
</a-avatar>
|
</a-avatar>
|
||||||
|
|
@ -99,80 +73,80 @@ const [fileTable, fileCtx] = useAniTable({
|
||||||
{record.name}
|
{record.name}
|
||||||
</span>
|
</span>
|
||||||
<span class="text-gray-400 text-xs truncate">
|
<span class="text-gray-400 text-xs truncate">
|
||||||
{numeral(record.size).format("0 b")}
|
{numeral(record.size).format('0 b')}
|
||||||
<span class="ml-2">{record.category?.name}</span>
|
<span class="ml-2">{record.category?.name}</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
createColumn,
|
useCreateColumn(),
|
||||||
updateColumn,
|
useUpdateColumn(),
|
||||||
{
|
{
|
||||||
type: "button",
|
type: 'button',
|
||||||
title: "操作",
|
title: '操作',
|
||||||
width: 120,
|
width: 160,
|
||||||
buttons: [
|
buttons: [
|
||||||
{
|
{
|
||||||
type: "modify",
|
text: '下载',
|
||||||
text: "修改",
|
onClick: props => {
|
||||||
|
window.open(props.record.path, '_blank');
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "delete",
|
type: 'modify',
|
||||||
text: "删除",
|
text: '修改',
|
||||||
onClick({ record }) {
|
},
|
||||||
return api.file.delFile(record.id);
|
{
|
||||||
|
type: 'delete',
|
||||||
|
text: '删除',
|
||||||
|
onClick: props => {
|
||||||
|
return api.file.delFile(props.record.id);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
source: async model => {
|
||||||
|
return api.file.getFiles(model);
|
||||||
|
},
|
||||||
search: {
|
search: {
|
||||||
button: false,
|
hideSearch: false,
|
||||||
model: {
|
model: {
|
||||||
categoryId: undefined,
|
categoryId: undefined,
|
||||||
},
|
},
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
field: "name",
|
field: 'name',
|
||||||
label: "文件名称",
|
label: '素材名称',
|
||||||
type: "search",
|
setter: 'search',
|
||||||
searchable: true,
|
searchable: true,
|
||||||
enterable: true,
|
enterable: true,
|
||||||
itemProps: {
|
|
||||||
hideLabel: true,
|
|
||||||
},
|
|
||||||
nodeProps: {
|
|
||||||
placeholder: "素材名称",
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
modify: {
|
modify: {
|
||||||
title: "修改素材",
|
title: '修改素材',
|
||||||
modalProps: {
|
|
||||||
width: 580,
|
width: 580,
|
||||||
},
|
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
field: "categoryId",
|
field: 'categoryId',
|
||||||
label: "分类",
|
label: '分类',
|
||||||
type: "select",
|
setter: 'select',
|
||||||
options: () => api.fileCategory.getFileCategorys({ size: 0 }),
|
options: () => api.fileCategory.getFileCategorys({ size: 0 }) as any,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: "name",
|
field: 'name',
|
||||||
label: "名称",
|
label: '名称',
|
||||||
type: "input",
|
setter: 'input',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: "description",
|
field: 'description',
|
||||||
label: "描述",
|
label: '描述',
|
||||||
type: "textarea",
|
setter: 'textarea',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
submit: ({ model }) => {
|
submit: model => {
|
||||||
console.log(model);
|
|
||||||
return api.file.setFile(model.id, model);
|
return api.file.setFile(model.id, model);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,131 +0,0 @@
|
||||||
<template>
|
|
||||||
<a-popover v-model:popup-visible="visible" position="br" trigger="click">
|
|
||||||
<a-button class="float-right">设置</a-button>
|
|
||||||
<template #content>
|
|
||||||
<div class="mb-1 leading-none border-b border-gray-100 pb-3">设置表格列</div>
|
|
||||||
<a-scrollbar outer-class="h-96 overflow-hidden" class="h-full overflow-auto">
|
|
||||||
<ul class="grid m-0 p-0 divide-y divide-gray-100 w-[700px] overflow-auto overscroll-contain">
|
|
||||||
<li
|
|
||||||
v-for="(item, index) in items"
|
|
||||||
:key="item.dataIndex"
|
|
||||||
class="group flex items-center justify-between py-2 pr-8 select-none"
|
|
||||||
>
|
|
||||||
<div class="flex gap-2">
|
|
||||||
<a-checkbox v-model="item.enable" :disabled="!item.editable" size="large" @change="onItemChange">
|
|
||||||
{{ item.dataIndex }}
|
|
||||||
</a-checkbox>
|
|
||||||
<span class="hidden group-hover:inline-block ml-4">
|
|
||||||
<i v-show="!item.editable" class="icon-park-outline-drag cursor-move"></i>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="flex gap-2 items-center">
|
|
||||||
<a-checkbox v-model="item.autoWidth" :disabled="!item.editable">
|
|
||||||
<template #checkbox="{ checked }">
|
|
||||||
<a-tag :checked="checked" :checkable="item.editable" color="blue">自适应</a-tag>
|
|
||||||
</template>
|
|
||||||
</a-checkbox>
|
|
||||||
<a-divider direction="vertical" :margin="8"></a-divider>
|
|
||||||
<a-input-number
|
|
||||||
size="small"
|
|
||||||
v-model="item.width"
|
|
||||||
:disabled="item.autoWidth || !item.editable"
|
|
||||||
:min="60"
|
|
||||||
:step="10"
|
|
||||||
class="!w-20"
|
|
||||||
/>
|
|
||||||
<span class="text-gray-400">像素</span>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</a-scrollbar>
|
|
||||||
<div class="mt-4 flex gap-2 items-center justify-between">
|
|
||||||
<div class="flex items-center">
|
|
||||||
<a-checkbox :indeterminate="indeterminate" v-model="checkAll" @change="onCheckAllChange"> 全选 </a-checkbox>
|
|
||||||
<span class="text-xs text-gray-400 ml-1">
|
|
||||||
({{ items.filter(i => i.enable).length }}/{{ items.length }})
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="space-x-2">
|
|
||||||
<a-button @click="onReset">重置</a-button>
|
|
||||||
<a-button type="primary" @click="onConfirm">确定</a-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</a-popover>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { computed, ref } from 'vue';
|
|
||||||
|
|
||||||
interface Item {
|
|
||||||
dataIndex: string;
|
|
||||||
enable: boolean;
|
|
||||||
autoWidth: boolean;
|
|
||||||
width: number;
|
|
||||||
editable: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
const checkAll = ref(false);
|
|
||||||
const visible = ref(false);
|
|
||||||
const items = ref<Item[]>([]);
|
|
||||||
const checked = computed(() => items.value.filter(i => i.enable));
|
|
||||||
const indeterminate = computed(() => {
|
|
||||||
const check = checked.value.length;
|
|
||||||
const total = items.value.length;
|
|
||||||
return 0 < check && check < total;
|
|
||||||
});
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
items.value.push({
|
|
||||||
dataIndex: '顺序',
|
|
||||||
enable: true,
|
|
||||||
autoWidth: false,
|
|
||||||
width: 80,
|
|
||||||
editable: false,
|
|
||||||
});
|
|
||||||
for (let i = 1; i <= 10; i++) {
|
|
||||||
items.value.push({
|
|
||||||
dataIndex: `测试${i}`,
|
|
||||||
enable: true,
|
|
||||||
autoWidth: false,
|
|
||||||
width: 80,
|
|
||||||
editable: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
items.value.push({
|
|
||||||
dataIndex: '操作',
|
|
||||||
enable: true,
|
|
||||||
autoWidth: false,
|
|
||||||
width: 80,
|
|
||||||
editable: false,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const onItemChange = () => {
|
|
||||||
if (checked.value.length === 0) {
|
|
||||||
checkAll.value = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (checked.value.length === items.value.length) {
|
|
||||||
checkAll.value = true;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const onCheckAllChange = (value: any) => {
|
|
||||||
for (const item of items.value) {
|
|
||||||
if (item.editable) {
|
|
||||||
item.enable = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const onReset = () => {
|
|
||||||
visible.value = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
const onConfirm = () => {
|
|
||||||
visible.value = false;
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped></style>
|
|
||||||
|
|
@ -1,205 +0,0 @@
|
||||||
<template>
|
|
||||||
<bread-page class="">
|
|
||||||
<a-card title="菜单权限">
|
|
||||||
<template #title>
|
|
||||||
菜单权限
|
|
||||||
<a-link>展开</a-link>
|
|
||||||
</template>
|
|
||||||
<template #extra>
|
|
||||||
<a-checkbox>全部选择</a-checkbox>
|
|
||||||
</template>
|
|
||||||
</a-card>
|
|
||||||
<a-modal v-model:visible="state.visible" :width="1280" :title="'选择素材'" title-align="start" :closable="false">
|
|
||||||
<div class="w-full h-[600px] flex gap-4">
|
|
||||||
<div class="w-64 p-2 pr-4 border">
|
|
||||||
<a-input-search placeholder="请输入关键字"></a-input-search>
|
|
||||||
<a-tree
|
|
||||||
:data="items"
|
|
||||||
:block-node="true"
|
|
||||||
:field-names="{ title: 'title' }"
|
|
||||||
:default-expand-all="true"
|
|
||||||
class="mt-2"
|
|
||||||
>
|
|
||||||
<template #extra="nodeData">
|
|
||||||
<div class="text-slate-400 mr-2">
|
|
||||||
10
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</a-tree>
|
|
||||||
</div>
|
|
||||||
<div class="flex-1 h-full">
|
|
||||||
<Table v-bind="table"></Table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</a-modal>
|
|
||||||
</bread-page>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="tsx">
|
|
||||||
import { api } from '@/api';
|
|
||||||
import { Table, useTable } from '@/components';
|
|
||||||
import { dayjs } from '@/libs/dayjs';
|
|
||||||
import { menus } from "@/router";
|
|
||||||
import { cloneDeep } from "lodash-es";
|
|
||||||
|
|
||||||
const items = cloneDeep(menus) as any;
|
|
||||||
for (const item of items) {
|
|
||||||
item.checked = false;
|
|
||||||
if (item.icon) {
|
|
||||||
const icon = item.icon;
|
|
||||||
item.icon = () => <i class={icon}></i>;
|
|
||||||
}
|
|
||||||
item.switcherIcon = () => null;
|
|
||||||
if (item.children) {
|
|
||||||
for (const child of item.children) {
|
|
||||||
if (child.icon) {
|
|
||||||
const icon = child.icon;
|
|
||||||
child.icon = () => <i class={icon}></i>;
|
|
||||||
}
|
|
||||||
child.checked = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const state = reactive({
|
|
||||||
menus: items,
|
|
||||||
visible: false
|
|
||||||
});
|
|
||||||
|
|
||||||
const indeter = (items: any[]) => {
|
|
||||||
if (!items) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const checked = items.filter((item) => item.checked);
|
|
||||||
return checked.length > 0 && checked.length < items.length;
|
|
||||||
};
|
|
||||||
|
|
||||||
const onItemChange = (item: any, menu: any) => {
|
|
||||||
const checked = menu.children.filter((item: any) => item.checked);
|
|
||||||
if (checked === 0) {
|
|
||||||
menu.checked = false;
|
|
||||||
} else if (checked === menu.children.length) {
|
|
||||||
menu.checked = true;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const table = useTable({
|
|
||||||
data: items,
|
|
||||||
columns: [
|
|
||||||
{
|
|
||||||
title: "角色名称",
|
|
||||||
dataIndex: "title",
|
|
||||||
width: 180,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "类型",
|
|
||||||
dataIndex: "description",
|
|
||||||
render: () => <a-tag color="blue">菜单</a-tag>,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "创建时间",
|
|
||||||
dataIndex: "createdAt",
|
|
||||||
width: 200,
|
|
||||||
render: ({ record }) => dayjs(record.createdAt).format(),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "操作",
|
|
||||||
type: "button",
|
|
||||||
width: 184,
|
|
||||||
buttons: [
|
|
||||||
{
|
|
||||||
type: "modify",
|
|
||||||
text: "修改",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: '分配权限',
|
|
||||||
onClick: ({ record }) => {
|
|
||||||
console.log(record);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: "删除",
|
|
||||||
type: "delete",
|
|
||||||
onClick: ({ record }) => {
|
|
||||||
return api.role.delRole(record.id);
|
|
||||||
},
|
|
||||||
}
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
search: {
|
|
||||||
items: [
|
|
||||||
{
|
|
||||||
extend: "name",
|
|
||||||
required: false,
|
|
||||||
nodeProps: {
|
|
||||||
placeholder: '请输入角色名称'
|
|
||||||
},
|
|
||||||
itemProps: {
|
|
||||||
hideLabel: true,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
create: {
|
|
||||||
title: "新建角色",
|
|
||||||
modalProps: {
|
|
||||||
width: 580,
|
|
||||||
maskClosable: false,
|
|
||||||
},
|
|
||||||
formProps: {
|
|
||||||
layout: "vertical",
|
|
||||||
},
|
|
||||||
items: [
|
|
||||||
{
|
|
||||||
field: "name",
|
|
||||||
label: "角色名称",
|
|
||||||
type: "input",
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: "slug",
|
|
||||||
label: "角色标识",
|
|
||||||
type: "input",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: "description",
|
|
||||||
label: "个人描述",
|
|
||||||
type: "textarea",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: "permissionIds",
|
|
||||||
label: "关联权限",
|
|
||||||
type: "select",
|
|
||||||
options: () => api.role.getRoles(),
|
|
||||||
nodeProps: {
|
|
||||||
multiple: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
submit: ({ model }) => {
|
|
||||||
return api.role.addRole(model);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
modify: {
|
|
||||||
extend: true,
|
|
||||||
title: "修改角色",
|
|
||||||
submit: ({ model }) => {
|
|
||||||
return api.role.updateRole(model.id, model);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="less">
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<route lang="json">
|
|
||||||
{
|
|
||||||
"meta": {
|
|
||||||
"sort": 10201,
|
|
||||||
"title": "表格组件",
|
|
||||||
"icon": "icon-park-outline-add-subtract"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</route>
|
|
||||||
|
|
@ -1,198 +0,0 @@
|
||||||
<template>
|
|
||||||
<bread-page id="list-page">
|
|
||||||
<template #default>
|
|
||||||
<div class="flex justify-between items-end gap-4">
|
|
||||||
<a-button type="primary" @click="visible = true">
|
|
||||||
<template #icon>
|
|
||||||
<i class="icon-park-outline-add"></i>
|
|
||||||
</template>
|
|
||||||
添加
|
|
||||||
</a-button>
|
|
||||||
</div>
|
|
||||||
<AList class="mt-2 bg-white" :bordered="true">
|
|
||||||
<template #header>
|
|
||||||
<div class="flex gap-2 items-center justify-between text-sm bg-[#fbfbfc] px-5 py-2">
|
|
||||||
<div class="flex gap-4 my-1.5">
|
|
||||||
<ACheckbox> 全选 </ACheckbox>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center text-gray-500">
|
|
||||||
<ADropdown>
|
|
||||||
<a-button type="text">
|
|
||||||
上传者
|
|
||||||
<i class="icon-park-outline-down ml-1"></i>
|
|
||||||
</a-button>
|
|
||||||
<template #content>
|
|
||||||
<ADoption class="!hover:bg-transparent !px-0 flex">
|
|
||||||
<div class="border-b border-gray-200 w-full pb-1">
|
|
||||||
<AInput placeholder="用户名关键字" />
|
|
||||||
</div>
|
|
||||||
</ADoption>
|
|
||||||
<ADoption v-for="j in 10">
|
|
||||||
<div class="flex items-center gap-1 w-48">
|
|
||||||
<AAvatar :size="20" class="mr-1 bg-slate-50">
|
|
||||||
<img :src="`https://picsum.photo1s/seed/picsum/200/300?${Math.random()}`" alt="" />
|
|
||||||
</AAvatar>
|
|
||||||
绝弹土豆
|
|
||||||
</div>
|
|
||||||
</ADoption>
|
|
||||||
</template>
|
|
||||||
</ADropdown>
|
|
||||||
<ADropdown>
|
|
||||||
<a-button type="text">
|
|
||||||
排序:默认
|
|
||||||
<i class="icon-park-outline-down ml-1"></i>
|
|
||||||
</a-button>
|
|
||||||
<template #content>
|
|
||||||
<ADoption>
|
|
||||||
<template #icon>
|
|
||||||
<i class="icon-park-outline-check"></i>
|
|
||||||
</template>
|
|
||||||
<div class="w-48">默认</div>
|
|
||||||
</ADoption>
|
|
||||||
<ADoption>
|
|
||||||
<template #icon> </template>
|
|
||||||
按创建时间升序
|
|
||||||
</ADoption>
|
|
||||||
<ADoption> 按创建时间降序 </ADoption>
|
|
||||||
<ADoption> 按文件大小升序 </ADoption>
|
|
||||||
<ADoption> 按文件大小降序 </ADoption>
|
|
||||||
</template>
|
|
||||||
</ADropdown>
|
|
||||||
<div class="space-x-1">
|
|
||||||
<a-button type="text">
|
|
||||||
<template #icon>
|
|
||||||
<i class="icon-park-outline-list"></i>
|
|
||||||
</template>
|
|
||||||
</a-button>
|
|
||||||
<a-button type="text">
|
|
||||||
<template #icon>
|
|
||||||
<i class="icon-park-outline-insert-table"></i>
|
|
||||||
</template>
|
|
||||||
</a-button>
|
|
||||||
<a-button type="text">
|
|
||||||
<template #icon>
|
|
||||||
<i class="icon-park-outline-refresh"></i>
|
|
||||||
</template>
|
|
||||||
</a-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<AListItem v-for="i in 10">
|
|
||||||
<AListItemMeta title="测试图片.png" description="image/png 1.2MB">
|
|
||||||
<template #avatar>
|
|
||||||
<ACheckbox class="mr-3"></ACheckbox>
|
|
||||||
<AImage
|
|
||||||
:src="`https://picsum.photos/200/300?${Math.random()}`"
|
|
||||||
height="32"
|
|
||||||
width="48"
|
|
||||||
class="bg-slate-50"
|
|
||||||
>
|
|
||||||
</AImage>
|
|
||||||
</template>
|
|
||||||
<template #title>
|
|
||||||
<span class="hover:text-blue-500 cursor-pointer">测试图片.png</span>
|
|
||||||
</template>
|
|
||||||
<template #description>
|
|
||||||
<div class="text-xs text-gray-400">image/png 1.2MB</div>
|
|
||||||
</template>
|
|
||||||
</AListItemMeta>
|
|
||||||
<template #actions>
|
|
||||||
<div class="flex items-center gap-6">
|
|
||||||
<span class="text-xs text-gray-400">
|
|
||||||
<i class="icon-park-outline-user !w-[14px] !h-[14px]"></i>
|
|
||||||
绝弹
|
|
||||||
</span>
|
|
||||||
<span class="text-xs text-gray-400">2023-08-17 17:00:01</span>
|
|
||||||
<ADropdown @select="onRowActionsSelect" position="br">
|
|
||||||
<a-button type="text">
|
|
||||||
<template #icon>
|
|
||||||
<i class="icon-park-outline-more"></i>
|
|
||||||
</template>
|
|
||||||
</a-button>
|
|
||||||
<template #content>
|
|
||||||
<ADoption value="detail">
|
|
||||||
<template #icon>
|
|
||||||
<i class="icon-park-outline-repair"></i>
|
|
||||||
</template>
|
|
||||||
<div>详情</div>
|
|
||||||
</ADoption>
|
|
||||||
<ADoption value="delete" class="!text-red-500 !hover-bg-red-50">
|
|
||||||
<template #icon>
|
|
||||||
<i class="icon-park-outline-delete"></i>
|
|
||||||
</template>
|
|
||||||
删除
|
|
||||||
</ADoption>
|
|
||||||
</template>
|
|
||||||
</ADropdown>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</AListItem>
|
|
||||||
</AList>
|
|
||||||
<div class="mt-4 flex justify-end">
|
|
||||||
<a-pagination :total="232" :show-total="true"></a-pagination>
|
|
||||||
</div>
|
|
||||||
<a-modal v-model:visible="visible" title="修改密码" :width="432" :footer="false" title-align="start">
|
|
||||||
<a-form :model="{}" layout="vertical">
|
|
||||||
<a-form-item label="原密码">
|
|
||||||
<a-input placeholder="请输入原密码"></a-input>
|
|
||||||
</a-form-item>
|
|
||||||
<a-form-item label="新密码">
|
|
||||||
<a-input placeholder="请输入新密码"></a-input>
|
|
||||||
</a-form-item>
|
|
||||||
<a-form-item label="确认新密码">
|
|
||||||
<a-input placeholder="请再次输入新密码"></a-input>
|
|
||||||
</a-form-item>
|
|
||||||
<a-button type="primary" class="w-full mt-2">修改密码</a-button>
|
|
||||||
</a-form>
|
|
||||||
</a-modal>
|
|
||||||
</template>
|
|
||||||
</bread-page>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="tsx">
|
|
||||||
import { Modal } from "@arco-design/web-vue";
|
|
||||||
|
|
||||||
const visible = ref(false);
|
|
||||||
|
|
||||||
const onRowActionsSelect = () => {
|
|
||||||
Modal.open({
|
|
||||||
title: "提示",
|
|
||||||
titleAlign: "start",
|
|
||||||
width: 432,
|
|
||||||
content: "确定删除该文件吗?该操作不可恢复。",
|
|
||||||
maskClosable: false,
|
|
||||||
closable: false,
|
|
||||||
okText: "确定删除",
|
|
||||||
okButtonProps: {
|
|
||||||
status: "danger",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="less">
|
|
||||||
#list-page {
|
|
||||||
.arco-list-header {
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.arco-list-medium .arco-list-content-wrapper .arco-list-content > .arco-list-item {
|
|
||||||
padding: 4px 20px;
|
|
||||||
}
|
|
||||||
button.arco-btn-text,
|
|
||||||
.arco-btn-text[type="button"],
|
|
||||||
.arco-btn-text[type="submit"] {
|
|
||||||
color: inherit;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<route lang="json">
|
|
||||||
{
|
|
||||||
"meta": {
|
|
||||||
"sort": 10202,
|
|
||||||
"title": "表单组件",
|
|
||||||
"icon": "icon-park-outline-aperture-priority"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</route>
|
|
||||||
|
|
@ -66,7 +66,7 @@ const { component: DictTable, tableRef } = useTable({
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
source(search) {
|
source: search => {
|
||||||
return api.dict.getDicts({ ...search, typeId: current.value?.id } as any);
|
return api.dict.getDicts({ ...search, typeId: current.value?.id } as any);
|
||||||
},
|
},
|
||||||
search: {
|
search: {
|
||||||
|
|
@ -84,19 +84,18 @@ const { component: DictTable, tableRef } = useTable({
|
||||||
create: {
|
create: {
|
||||||
title: '新增字典',
|
title: '新增字典',
|
||||||
width: 580,
|
width: 580,
|
||||||
model: {
|
|
||||||
typeId: undefined,
|
|
||||||
},
|
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
field: 'name',
|
field: 'name',
|
||||||
label: '字典名',
|
label: '字典名',
|
||||||
setter: 'input',
|
setter: 'input',
|
||||||
|
required: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'code',
|
field: 'code',
|
||||||
label: '字典值',
|
label: '字典值',
|
||||||
setter: 'input',
|
setter: 'input',
|
||||||
|
required: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'description',
|
field: 'description',
|
||||||
|
|
@ -105,14 +104,16 @@ const { component: DictTable, tableRef } = useTable({
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
submit: model => {
|
submit: model => {
|
||||||
return api.dict.addDict({ ...model, typeId: current.value?.id } as any);
|
const data = { ...model, typeId: current.value?.id } as any;
|
||||||
|
return api.dict.addDict(data);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
modify: {
|
modify: {
|
||||||
extend: true,
|
extend: true,
|
||||||
title: '修改字典',
|
title: '修改字典',
|
||||||
submit: model => {
|
submit: model => {
|
||||||
return api.dict.setDict(model.id, { ...model, typeId: current.value?.id } as any);
|
const data = { ...model, typeId: current.value?.id } as any;
|
||||||
|
return api.dict.setDict(model.id, data);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,4 @@
|
||||||
<template><div></div></template>
|
<template><div></div></template>
|
||||||
<script setup lang="tsx"></script>
|
|
||||||
<style scoped></style>
|
|
||||||
|
|
||||||
<route lang="json">
|
<route lang="json">
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,31 +1,30 @@
|
||||||
<template>
|
<template>
|
||||||
<BreadPage>
|
<BreadPage>
|
||||||
<Table v-bind="table"> </Table>
|
<UserTable />
|
||||||
<pass-modal></pass-modal>
|
<pass-modal></pass-modal>
|
||||||
</BreadPage>
|
</BreadPage>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="tsx">
|
<script setup lang="tsx">
|
||||||
import { api } from "@/api";
|
import { api } from '@/api';
|
||||||
import { Table, createColumn, updateColumn, useTable } from "@/components";
|
import { useCreateColumn, useTable, useUpdateColumn } from '@/components/AnTable';
|
||||||
import InputAvatar from "./components/avatar.vue";
|
import { usePassworModal } from './components/password';
|
||||||
import { usePassworModal } from "./components/password";
|
|
||||||
|
|
||||||
defineOptions({ name: "SystemUserPage" });
|
defineOptions({ name: 'SystemUserPage' });
|
||||||
const [passModal, passCtx] = usePassworModal();
|
const [passModal, passCtx] = usePassworModal();
|
||||||
|
|
||||||
const table = useTable({
|
const { component: UserTable } = useTable({
|
||||||
data: async (model, paging) => {
|
source: async model => {
|
||||||
return api.user.getUsers({ ...model, ...paging });
|
return api.user.getUsers(model);
|
||||||
},
|
},
|
||||||
columns: [
|
columns: [
|
||||||
{
|
{
|
||||||
title: "用户昵称",
|
title: '用户昵称',
|
||||||
dataIndex: "username",
|
dataIndex: 'username',
|
||||||
render: ({ record }) => (
|
render: ({ record }) => (
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<a-avatar size={32} class="!bg-brand-500">
|
<a-avatar size={32} class="!bg-brand-500">
|
||||||
{record.avatar?.startsWith("/") ? <img src={record.avatar} alt="" /> : record.nickname?.[0]}
|
{record.avatar?.startsWith('/') ? <img src={record.avatar} alt="" /> : record.nickname?.[0]}
|
||||||
</a-avatar>
|
</a-avatar>
|
||||||
<span class="ml-2 flex-1 flex flex-col overflow-hidden">
|
<span class="ml-2 flex-1 flex flex-col overflow-hidden">
|
||||||
<span>{record.nickname}</span>
|
<span>{record.nickname}</span>
|
||||||
|
|
@ -35,30 +34,30 @@ const table = useTable({
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "用户邮箱",
|
title: '用户邮箱',
|
||||||
dataIndex: "email",
|
dataIndex: 'email',
|
||||||
width: 200,
|
width: 200,
|
||||||
},
|
},
|
||||||
createColumn,
|
useCreateColumn(),
|
||||||
updateColumn,
|
useUpdateColumn(),
|
||||||
{
|
{
|
||||||
title: "操作",
|
title: '操作',
|
||||||
type: "button",
|
type: 'button',
|
||||||
width: 200,
|
width: 200,
|
||||||
buttons: [
|
buttons: [
|
||||||
{
|
{
|
||||||
type: "modify",
|
type: 'modify',
|
||||||
text: "修改",
|
text: '修改',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: "设置密码",
|
text: '设置密码',
|
||||||
onClick({ record }) {
|
onClick({ record }) {
|
||||||
passCtx.open(record);
|
passCtx.open(record);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "delete",
|
type: 'delete',
|
||||||
text: "删除",
|
text: '删除',
|
||||||
onClick: async ({ record }) => {
|
onClick: async ({ record }) => {
|
||||||
return api.user.delUser(record.id, { toast: true });
|
return api.user.delUser(record.id, { toast: true });
|
||||||
},
|
},
|
||||||
|
|
@ -67,115 +66,80 @@ const table = useTable({
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
search: {
|
search: {
|
||||||
button: true,
|
hideSearch: true,
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
field: "nickname",
|
field: 'nickname',
|
||||||
label: "用户昵称",
|
label: '用户昵称',
|
||||||
type: "input",
|
setter: 'input',
|
||||||
},
|
|
||||||
{
|
|
||||||
field: "nickname",
|
|
||||||
label: "用户昵称",
|
|
||||||
type: "input",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: "nickname",
|
|
||||||
label: "用户昵称",
|
|
||||||
type: "input",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: "nickname",
|
|
||||||
label: "用户昵称",
|
|
||||||
type: "input",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: "nickname",
|
|
||||||
label: "用户昵称",
|
|
||||||
type: "input",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: "nickname",
|
|
||||||
label: "用户昵称",
|
|
||||||
type: "input",
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
create: {
|
create: {
|
||||||
title: "新建用户",
|
title: '新建用户',
|
||||||
modalProps: {
|
|
||||||
width: 820,
|
width: 820,
|
||||||
maskClosable: false,
|
formClass: '!grid grid-cols-2 gap-x-6',
|
||||||
},
|
|
||||||
formProps: {
|
|
||||||
layout: "vertical",
|
|
||||||
class: "!grid grid-cols-2 gap-x-6",
|
|
||||||
},
|
|
||||||
model: {},
|
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
field: "avatar",
|
field: 'avatar',
|
||||||
label: "用户头像",
|
label: '用户头像',
|
||||||
type: "custom",
|
setter: 'input',
|
||||||
itemProps: {
|
setterProps: {
|
||||||
class: "col-span-2",
|
class: 'col-span-2',
|
||||||
},
|
|
||||||
component({ model }) {
|
|
||||||
return <InputAvatar v-model={model.avatar}></InputAvatar>;
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: "username",
|
field: 'username',
|
||||||
label: "登录账号",
|
label: '登录账号',
|
||||||
type: "input",
|
setter: 'input',
|
||||||
required: true,
|
required: true,
|
||||||
nodeProps: {
|
setterProps: {
|
||||||
placeholder: "英文字母+数组组成,5~10位",
|
placeholder: '英文字母+数组组成,5~10位',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: "password",
|
field: 'password',
|
||||||
label: "登陆密码",
|
label: '登陆密码',
|
||||||
type: "input",
|
setter: 'input',
|
||||||
nodeProps: {
|
setterProps: {
|
||||||
placeholder: "包含大小写,长度6 ~ 12位",
|
placeholder: '包含大小写,长度6 ~ 12位',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: "nickname",
|
field: 'nickname',
|
||||||
label: "用户昵称",
|
label: '用户昵称',
|
||||||
type: "input",
|
setter: 'input',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: "roleIds",
|
field: 'roleIds',
|
||||||
label: "关联角色",
|
label: '关联角色',
|
||||||
type: "select",
|
setter: 'select',
|
||||||
options: () => api.role.getRoles(),
|
options: () => api.role.getRoles() as any,
|
||||||
nodeProps: {
|
setterProps: {
|
||||||
multiple: true,
|
multiple: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: "description",
|
field: 'description',
|
||||||
label: "个人描述",
|
label: '个人描述',
|
||||||
type: "textarea",
|
setter: 'textarea',
|
||||||
itemProps: {
|
itemProps: {
|
||||||
class: "col-span-2",
|
class: 'col-span-2',
|
||||||
},
|
},
|
||||||
nodeProps: {
|
setterProps: {
|
||||||
class: "h-[96px]",
|
class: 'h-[96px]',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
submit: ({ model }) => {
|
submit: model => {
|
||||||
return api.user.addUser(model);
|
return api.user.addUser(model as any);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
modify: {
|
modify: {
|
||||||
extend: true,
|
extend: true,
|
||||||
title: "修改用户",
|
title: '修改用户',
|
||||||
submit: ({ model }) => {
|
submit: model => {
|
||||||
return api.user.setUser(model.id, model);
|
return api.user.setUser(model.id, model as any);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -19,11 +19,23 @@
|
||||||
<template #help> 支持 5MB 以内大小, png 或 jpg 格式的图片 </template>
|
<template #help> 支持 5MB 以内大小, png 或 jpg 格式的图片 </template>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item label="用户昵称">
|
<a-form-item label="用户昵称">
|
||||||
<a-input v-model="user.nickname" placeholder="请输入" class="!w-[432px]" :max-length="24" :show-word-limit="true"></a-input>
|
<a-input
|
||||||
|
v-model="user.nickname"
|
||||||
|
placeholder="请输入"
|
||||||
|
class="!w-[432px]"
|
||||||
|
:max-length="24"
|
||||||
|
:show-word-limit="true"
|
||||||
|
></a-input>
|
||||||
<template #help> 用作系统内显示的名称,可在后台修改 </template>
|
<template #help> 用作系统内显示的名称,可在后台修改 </template>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item label="个人描述">
|
<a-form-item label="个人描述">
|
||||||
<a-textarea v-model="user.description" placeholder="请输入" class="!w-[432px] h-24" :max-length="140" :show-word-limit="true"></a-textarea>
|
<a-textarea
|
||||||
|
v-model="user.description"
|
||||||
|
placeholder="请输入"
|
||||||
|
class="!w-[432px] h-24"
|
||||||
|
:max-length="140"
|
||||||
|
:show-word-limit="true"
|
||||||
|
></a-textarea>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item label="性别">
|
<a-form-item label="性别">
|
||||||
<a-radio-group v-model="user.gender" type="button">
|
<a-radio-group v-model="user.gender" type="button">
|
||||||
|
|
@ -173,16 +185,16 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="tsx">
|
<script setup lang="tsx">
|
||||||
import { reactive } from "vue";
|
import { reactive } from 'vue';
|
||||||
|
|
||||||
const user = reactive({
|
const user = reactive({
|
||||||
nickname: "绝弹",
|
nickname: '绝弹',
|
||||||
description: "选择在公开个人资料中显示私有项目的贡献,但不显示任何项目,仓库或组织信息",
|
description: '选择在公开个人资料中显示私有项目的贡献,但不显示任何项目,仓库或组织信息',
|
||||||
theme: "dark",
|
theme: 'dark',
|
||||||
email: "810335188@qq.com",
|
email: '810335188@qq.com',
|
||||||
msg: [2],
|
msg: [2],
|
||||||
gender: 1,
|
gender: 1,
|
||||||
birth: "1988-12-18",
|
birth: '1988-12-18',
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,16 @@
|
||||||
import { api } from "@/api";
|
import { api } from '@/api';
|
||||||
import { store, useUserStore } from "@/store";
|
import { store, useUserStore } from '@/store';
|
||||||
import { useMenuStore } from "@/store/menu";
|
import { useMenuStore } from '@/store/menu';
|
||||||
import { treeEach, treeFilter, treeFind } from "@/utils/listToTree";
|
import { treeEach, treeFilter, treeFind } from '@/utils/listToTree';
|
||||||
import { Notification } from "@arco-design/web-vue";
|
import { Notification } from '@arco-design/web-vue';
|
||||||
import { Router } from "vue-router";
|
import { Router } from 'vue-router';
|
||||||
import { MenuItem, menus } from "../menus";
|
import { menus } from '../menus';
|
||||||
import { APP_HOME_NAME } from "../routes/base";
|
import { APP_HOME_NAME } from '../routes/base';
|
||||||
import { APP_ROUTE_NAME, routes } from "../routes/page";
|
import { APP_ROUTE_NAME, routes } from '../routes/page';
|
||||||
import { env } from "@/config/env";
|
import { env } from '@/config/env';
|
||||||
|
|
||||||
const WHITE_LIST = ["/:all(.*)*"];
|
const WHITE_LIST = ['/:all(.*)*'];
|
||||||
const UNSIGNIN_LIST = ["/login"];
|
const UNSIGNIN_LIST = ['/login'];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 权限守卫
|
* 权限守卫
|
||||||
|
|
@ -23,7 +23,7 @@ export function useAuthGuard(router: Router) {
|
||||||
const userStore = useUserStore(store);
|
const userStore = useUserStore(store);
|
||||||
const redirect = router.currentRoute.value.path;
|
const redirect = router.currentRoute.value.path;
|
||||||
userStore.clearUser();
|
userStore.clearUser();
|
||||||
router.push({ path: "/login", query: { redirect } });
|
router.push({ path: '/login', query: { redirect } });
|
||||||
};
|
};
|
||||||
|
|
||||||
router.beforeEach(async function (to, from) {
|
router.beforeEach(async function (to, from) {
|
||||||
|
|
@ -31,7 +31,7 @@ export function useAuthGuard(router: Router) {
|
||||||
const menuStore = useMenuStore(store);
|
const menuStore = useMenuStore(store);
|
||||||
|
|
||||||
// 手动指定直接通过
|
// 手动指定直接通过
|
||||||
if (to.meta.auth?.some((i) => i === "*")) {
|
if (to.meta.auth?.some(i => i === '*')) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -49,13 +49,13 @@ export function useAuthGuard(router: Router) {
|
||||||
|
|
||||||
// 已登陆进行提示
|
// 已登陆进行提示
|
||||||
Notification.warning({
|
Notification.warning({
|
||||||
title: "跳转提示",
|
title: '跳转提示',
|
||||||
content: `您已登陆,如需重新登陆请退出后再操作!`,
|
content: `您已登陆,如需重新登陆请退出后再操作!`,
|
||||||
});
|
});
|
||||||
|
|
||||||
// 不是从路由跳转的,跳转回首页
|
// 不是从路由跳转的,跳转回首页
|
||||||
if (!from.matched.length) {
|
if (!from.matched.length) {
|
||||||
return "/";
|
return '/';
|
||||||
}
|
}
|
||||||
|
|
||||||
// 已登陆不允许
|
// 已登陆不允许
|
||||||
|
|
@ -64,15 +64,15 @@ export function useAuthGuard(router: Router) {
|
||||||
|
|
||||||
// 未登录跳转到登陆页面
|
// 未登录跳转到登陆页面
|
||||||
if (!userStore.accessToken) {
|
if (!userStore.accessToken) {
|
||||||
return { path: "/login", query: { redirect: to.path } };
|
return { path: '/login', query: { redirect: to.path } };
|
||||||
}
|
}
|
||||||
|
|
||||||
// 未获取菜单进行获取
|
// 未获取菜单进行获取
|
||||||
if (!menuStore.menus.length) {
|
if (!menuStore.menus.length) {
|
||||||
// 菜单处理
|
// 菜单处理
|
||||||
const authMenus = treeFilter(menus, (item) => {
|
const authMenus = treeFilter(menus, item => {
|
||||||
if (item.path === env.homePath) {
|
if (item.path === env.homePath) {
|
||||||
item.path = "/";
|
item.path = '/';
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
@ -101,9 +101,9 @@ export function useAuthGuard(router: Router) {
|
||||||
menuStore.setCacheAppNames(appNames);
|
menuStore.setCacheAppNames(appNames);
|
||||||
|
|
||||||
// 首页处理
|
// 首页处理
|
||||||
const home = treeFind(routes, (i) => i.path === menuStore.home);
|
const home = treeFind(routes, i => i.path === menuStore.home);
|
||||||
if (home) {
|
if (home) {
|
||||||
const route = { ...home, name: APP_HOME_NAME, alias: "/" };
|
const route = { ...home, name: APP_HOME_NAME, alias: '/' };
|
||||||
router.removeRoute(home.name!);
|
router.removeRoute(home.name!);
|
||||||
router.addRoute(APP_ROUTE_NAME, route);
|
router.addRoute(APP_ROUTE_NAME, route);
|
||||||
return router.replace(to.path);
|
return router.replace(to.path);
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { NProgress } from "@/libs/nprogress";
|
import { NProgress } from '@/libs/nprogress';
|
||||||
import { useAppStore } from "@/store";
|
import { useAppStore } from '@/store';
|
||||||
import { Router } from "vue-router";
|
import { Router } from 'vue-router';
|
||||||
|
|
||||||
const routeMap = new Map<string, boolean>();
|
const routeMap = new Map<string, boolean>();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { store, useAppStore } from "@/store";
|
import { store, useAppStore } from '@/store';
|
||||||
import { Router } from "vue-router";
|
import { Router } from 'vue-router';
|
||||||
|
|
||||||
export function useTitleGuard(router: Router) {
|
export function useTitleGuard(router: Router) {
|
||||||
router.beforeEach(function (to) {
|
router.beforeEach(function (to) {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
export * from "./menus";
|
export * from './menus';
|
||||||
export * from "./router";
|
export * from './router';
|
||||||
export * from "./routes/page";
|
export * from './routes/page';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { RouteRecordRaw } from "vue-router";
|
import { RouteRecordRaw } from 'vue-router';
|
||||||
import { appRoutes } from "../routes/page";
|
import { appRoutes } from '../routes/page';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 菜单项类型
|
* 菜单项类型
|
||||||
|
|
@ -20,7 +20,6 @@ export interface MenuItem {
|
||||||
/**
|
/**
|
||||||
* 转换页面路由为菜单项
|
* 转换页面路由为菜单项
|
||||||
* @param routes 路由配置
|
* @param routes 路由配置
|
||||||
* @returns
|
|
||||||
*/
|
*/
|
||||||
function routesToItems(routes: RouteRecordRaw[]): MenuItem[] {
|
function routesToItems(routes: RouteRecordRaw[]): MenuItem[] {
|
||||||
const items: MenuItem[] = [];
|
const items: MenuItem[] = [];
|
||||||
|
|
@ -29,8 +28,8 @@ function routesToItems(routes: RouteRecordRaw[]): MenuItem[] {
|
||||||
const { meta = {}, parentMeta, path } = route as any;
|
const { meta = {}, parentMeta, path } = route as any;
|
||||||
const { title, sort, icon, keepAlive = false, name } = meta;
|
const { title, sort, icon, keepAlive = false, name } = meta;
|
||||||
let id = path;
|
let id = path;
|
||||||
let paths = route.path.split("/");
|
let paths = route.path.split('/');
|
||||||
let parentId = paths.slice(0, -1).join("/");
|
let parentId = paths.slice(0, -1).join('/');
|
||||||
if (parentMeta) {
|
if (parentMeta) {
|
||||||
const { title, icon, sort } = parentMeta;
|
const { title, icon, sort } = parentMeta;
|
||||||
id = `${path}/index`;
|
id = `${path}/index`;
|
||||||
|
|
@ -42,11 +41,11 @@ function routesToItems(routes: RouteRecordRaw[]): MenuItem[] {
|
||||||
path,
|
path,
|
||||||
id: path,
|
id: path,
|
||||||
keepAlive: false,
|
keepAlive: false,
|
||||||
parentId: paths.slice(0, -1).join("/"),
|
parentId: paths.slice(0, -1).join('/'),
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
const p = paths.slice(0, -1).join("/");
|
const p = paths.slice(0, -1).join('/');
|
||||||
if (routes.some((i) => i.path === p) && parentMeta) {
|
if (routes.some(i => i.path === p) && parentMeta) {
|
||||||
parentId = p;
|
parentId = p;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -59,7 +58,6 @@ function routesToItems(routes: RouteRecordRaw[]): MenuItem[] {
|
||||||
/**
|
/**
|
||||||
* 转换菜单项为树形结构
|
* 转换菜单项为树形结构
|
||||||
* @param list 菜单项列表
|
* @param list 菜单项列表
|
||||||
* @returns
|
|
||||||
*/
|
*/
|
||||||
function listToTree(list: MenuItem[]) {
|
function listToTree(list: MenuItem[]) {
|
||||||
const map: Record<string, MenuItem> = {};
|
const map: Record<string, MenuItem> = {};
|
||||||
|
|
@ -85,9 +83,8 @@ function listToTree(list: MenuItem[]) {
|
||||||
* 排序菜单项
|
* 排序菜单项
|
||||||
* @param routes 菜单项列表
|
* @param routes 菜单项列表
|
||||||
* @param key 排序字段
|
* @param key 排序字段
|
||||||
* @returns
|
|
||||||
*/
|
*/
|
||||||
function sort<T extends { children?: T[]; [key: string]: any }>(routes: T[], key = "sort") {
|
function sort<T extends { children?: T[]; [key: string]: any }>(routes: T[], key = 'sort') {
|
||||||
return routes.sort((a, b) => {
|
return routes.sort((a, b) => {
|
||||||
if (Array.isArray(a.children)) {
|
if (Array.isArray(a.children)) {
|
||||||
a.children = sort(a.children);
|
a.children = sort(a.children);
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
import { createRouter } from "vue-router";
|
import { createRouter } from 'vue-router';
|
||||||
import { useAuthGuard } from "../guards/auth";
|
import { useAuthGuard } from '../guards/auth';
|
||||||
import { useProgressGard } from "../guards/progress";
|
import { useProgressGard } from '../guards/progress';
|
||||||
import { useTitleGuard } from "../guards/title";
|
import { useTitleGuard } from '../guards/title';
|
||||||
import { baseRoutes } from "../routes/base";
|
import { baseRoutes } from '../routes/base';
|
||||||
import { historyMode } from "./util";
|
import { historyMode } from './util';
|
||||||
import { routes } from "../routes/page";
|
import { routes } from '../routes/page';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 路由实例
|
* 路由实例
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { env } from "@/config/env";
|
import { env } from '@/config/env';
|
||||||
import { createWebHashHistory, createWebHistory } from "vue-router";
|
import { createWebHashHistory, createWebHistory } from 'vue-router';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 模式映射
|
* 模式映射
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,14 @@
|
||||||
import { RouteRecordRaw } from "vue-router";
|
import { RouteRecordRaw } from 'vue-router';
|
||||||
|
|
||||||
export const APP_HOME_NAME = "__APP_HOME__";
|
export const APP_HOME_NAME = '__APP_HOME__';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 基本路由
|
* 基本路由
|
||||||
*/
|
*/
|
||||||
export const baseRoutes: RouteRecordRaw[] = [
|
export const baseRoutes: RouteRecordRaw[] = [
|
||||||
{
|
{
|
||||||
path: "/",
|
path: '/',
|
||||||
name: APP_HOME_NAME,
|
name: APP_HOME_NAME,
|
||||||
component: () => "Home Page",
|
component: () => 'Home Page',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import generatedRoutes from "virtual:generated-pages";
|
import generatedRoutes from 'virtual:generated-pages';
|
||||||
import { RouteRecordRaw } from "vue-router";
|
import { RouteRecordRaw } from 'vue-router';
|
||||||
|
|
||||||
export const TOP_ROUTE_PREF = "_";
|
export const TOP_ROUTE_PREF = '_';
|
||||||
export const APP_ROUTE_NAME = "_layout";
|
export const APP_ROUTE_NAME = '_layout';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 转换路由
|
* 转换路由
|
||||||
|
|
@ -17,7 +17,7 @@ const transformRoutes = (routes: RouteRecordRaw[]) => {
|
||||||
if (route.name === APP_ROUTE_NAME) {
|
if (route.name === APP_ROUTE_NAME) {
|
||||||
route.children = appRoutes;
|
route.children = appRoutes;
|
||||||
}
|
}
|
||||||
route.path = route.path.replace(TOP_ROUTE_PREF, "");
|
route.path = route.path.replace(TOP_ROUTE_PREF, '');
|
||||||
topRoutes.push(route);
|
topRoutes.push(route);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
export * from "./app";
|
export * from './app';
|
||||||
export * from "./store";
|
export * from './store';
|
||||||
export * from "./user";
|
export * from './user';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ declare module '@vue/runtime-core' {
|
||||||
ACheckbox: typeof import('@arco-design/web-vue')['Checkbox']
|
ACheckbox: typeof import('@arco-design/web-vue')['Checkbox']
|
||||||
ACheckboxGroup: typeof import('@arco-design/web-vue')['CheckboxGroup']
|
ACheckboxGroup: typeof import('@arco-design/web-vue')['CheckboxGroup']
|
||||||
AConfigProvider: typeof import('@arco-design/web-vue')['ConfigProvider']
|
AConfigProvider: typeof import('@arco-design/web-vue')['ConfigProvider']
|
||||||
|
ADatePicker: typeof import('@arco-design/web-vue')['DatePicker']
|
||||||
ADivider: typeof import('@arco-design/web-vue')['Divider']
|
ADivider: typeof import('@arco-design/web-vue')['Divider']
|
||||||
ADoption: typeof import('@arco-design/web-vue')['Doption']
|
ADoption: typeof import('@arco-design/web-vue')['Doption']
|
||||||
ADrawer: typeof import('@arco-design/web-vue')['Drawer']
|
ADrawer: typeof import('@arco-design/web-vue')['Drawer']
|
||||||
|
|
@ -42,7 +43,10 @@ declare module '@vue/runtime-core' {
|
||||||
AMenu: typeof import('@arco-design/web-vue')['Menu']
|
AMenu: typeof import('@arco-design/web-vue')['Menu']
|
||||||
AMenuItem: typeof import('@arco-design/web-vue')['MenuItem']
|
AMenuItem: typeof import('@arco-design/web-vue')['MenuItem']
|
||||||
AModal: typeof import('@arco-design/web-vue')['Modal']
|
AModal: typeof import('@arco-design/web-vue')['Modal']
|
||||||
AniEmpty: typeof import('./../components/empty/AniEmpty.vue')['default']
|
AnEmpty: typeof import('./../components/AnEmpty/AnEmpty.vue')['default']
|
||||||
|
AnForbidden: typeof import('./../components/AnForbidden/AnForbidden.vue')['default']
|
||||||
|
AnForbiden: typeof import('./../components/AnForbidden/AnForbiden.vue')['default']
|
||||||
|
AnToast: typeof import('./../components/AnToast/AnToast.vue')['default']
|
||||||
APagination: typeof import('@arco-design/web-vue')['Pagination']
|
APagination: typeof import('@arco-design/web-vue')['Pagination']
|
||||||
APopover: typeof import('@arco-design/web-vue')['Popover']
|
APopover: typeof import('@arco-design/web-vue')['Popover']
|
||||||
AProgress: typeof import('@arco-design/web-vue')['Progress']
|
AProgress: typeof import('@arco-design/web-vue')['Progress']
|
||||||
|
|
@ -53,6 +57,8 @@ declare module '@vue/runtime-core' {
|
||||||
ASpace: typeof import('@arco-design/web-vue')['Space']
|
ASpace: typeof import('@arco-design/web-vue')['Space']
|
||||||
ASpin: typeof import('@arco-design/web-vue')['Spin']
|
ASpin: typeof import('@arco-design/web-vue')['Spin']
|
||||||
ASwitch: typeof import('@arco-design/web-vue')['Switch']
|
ASwitch: typeof import('@arco-design/web-vue')['Switch']
|
||||||
|
ATabPane: typeof import('@arco-design/web-vue')['TabPane']
|
||||||
|
ATabs: typeof import('@arco-design/web-vue')['Tabs']
|
||||||
ATag: typeof import('@arco-design/web-vue')['Tag']
|
ATag: typeof import('@arco-design/web-vue')['Tag']
|
||||||
ATextarea: typeof import('@arco-design/web-vue')['Textarea']
|
ATextarea: typeof import('@arco-design/web-vue')['Textarea']
|
||||||
ATooltip: typeof import('@arco-design/web-vue')['Tooltip']
|
ATooltip: typeof import('@arco-design/web-vue')['Tooltip']
|
||||||
|
|
@ -71,7 +77,7 @@ declare module '@vue/runtime-core' {
|
||||||
InputTexter: typeof import('./../components/editor/components/InputTexter.vue')['default']
|
InputTexter: typeof import('./../components/editor/components/InputTexter.vue')['default']
|
||||||
Marquee: typeof import('./../components/editor/blocks/text/marquee.vue')['default']
|
Marquee: typeof import('./../components/editor/blocks/text/marquee.vue')['default']
|
||||||
Option: typeof import('./../components/editor/blocks/date/option.vue')['default']
|
Option: typeof import('./../components/editor/blocks/date/option.vue')['default']
|
||||||
Page403: typeof import('./../components/error/page-403.vue')['default']
|
Page403: typeof import('./../components/AnForbiden/page-403.vue')['default']
|
||||||
PanelHeader: typeof import('./../components/editor/components/PanelHeader.vue')['default']
|
PanelHeader: typeof import('./../components/editor/components/PanelHeader.vue')['default']
|
||||||
PanelLeft: typeof import('./../components/editor/components/PanelLeft.vue')['default']
|
PanelLeft: typeof import('./../components/editor/components/PanelLeft.vue')['default']
|
||||||
PanelMain: typeof import('./../components/editor/components/PanelMain.vue')['default']
|
PanelMain: typeof import('./../components/editor/components/PanelMain.vue')['default']
|
||||||
|
|
@ -81,7 +87,5 @@ declare module '@vue/runtime-core' {
|
||||||
Render: typeof import('./../components/editor/blocks/date/render.vue')['default']
|
Render: typeof import('./../components/editor/blocks/date/render.vue')['default']
|
||||||
RouterLink: typeof import('vue-router')['RouterLink']
|
RouterLink: typeof import('vue-router')['RouterLink']
|
||||||
RouterView: typeof import('vue-router')['RouterView']
|
RouterView: typeof import('vue-router')['RouterView']
|
||||||
'Temp.dev1': typeof import('./../components/breadcrumb/temp.dev1.vue')['default']
|
|
||||||
Toast: typeof import('./../components/toast/toast.vue')['default']
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue