feat: 优化目录
parent
c489f3b0cf
commit
8968692073
4
.env
4
.env
|
|
@ -10,7 +10,7 @@ VITE_BASE = /
|
|||
# 接口前缀:参见 axios 的 baseURL
|
||||
VITE_API = https://appnify.app.juetan.cn/
|
||||
# 首页路径
|
||||
VITE_HOME_PATH = /home/home
|
||||
VITE_HOME_PATH = /home
|
||||
# 路由模式:web(路径) hash(锚点)
|
||||
VITE_HISTORY = web
|
||||
|
||||
|
|
@ -24,7 +24,7 @@ VITE_PORT = 3020
|
|||
# 代理前缀
|
||||
VITE_PROXY_PREFIX = /api,/upload
|
||||
# 代理地址
|
||||
VITE_PROXY = http://127.0.0.1:3030/
|
||||
VITE_PROXY = https://appnify.app.juetan.cn/
|
||||
# API文档 说明:需返回符合 OPENAPI 规范的json内容
|
||||
VITE_OPENAPI = http://127.0.0.1:3030/openapi.json
|
||||
# 文件后缀 说明:设为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>
|
||||
<router-view v-slot="{ Component, route }">
|
||||
<keep-alive :include="menuStore.cacheTopNames">
|
||||
<component v-if="hasAuth(route, Component)" :is="Component"></component>
|
||||
<page-403 v-else></page-403>
|
||||
<component v-if="hasAuth(route)" :is="Component"></component>
|
||||
<AnForbidden v-else></AnForbidden>
|
||||
</keep-alive>
|
||||
</router-view>
|
||||
</a-config-provider>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { RouteLocationNormalizedLoaded } from "vue-router";
|
||||
import { useUserStore } from "./store";
|
||||
import { useMenuStore } from "./store/menu";
|
||||
import { RouteLocationNormalizedLoaded } from 'vue-router';
|
||||
import { useUserStore } from './store';
|
||||
import { useMenuStore } from './store/menu';
|
||||
|
||||
const userStore = useUserStore();
|
||||
const menuStore = useMenuStore();
|
||||
|
||||
const hasAuth = (route: RouteLocationNormalizedLoaded, c: any) => {
|
||||
const hasAuth = (route: RouteLocationNormalizedLoaded) => {
|
||||
const aAuth = route.meta.auth;
|
||||
const uAuth = userStore.auth;
|
||||
if (!aAuth?.length) {
|
||||
return true;
|
||||
}
|
||||
if (aAuth.some((i) => i === "*")) {
|
||||
if (aAuth.some(i => i === '*')) {
|
||||
return true;
|
||||
}
|
||||
if (uAuth.some((i) => aAuth.some((j) => j === i))) {
|
||||
if (uAuth.some(i => aAuth.some(j => j === i))) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
export * from "./instance/api";
|
||||
export * from "./generated/Api";
|
||||
export * from "./instance/useRequest";
|
||||
export * from './instance/api';
|
||||
export * from './generated/Api';
|
||||
export * from './instance/useRequest';
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import { Service } from "./service";
|
||||
import { addToastInterceptor } from "../interceptors/toast";
|
||||
import { addAuthInterceptor } from "../interceptors/auth";
|
||||
import { addExceptionInterceptor } from "../interceptors/exception";
|
||||
import { env } from "@/config/env";
|
||||
import { Service } from './service';
|
||||
import { addToastInterceptor } from '../interceptors/toast';
|
||||
import { addAuthInterceptor } from '../interceptors/auth';
|
||||
import { addExceptionInterceptor } from '../interceptors/exception';
|
||||
import { env } from '@/config/env';
|
||||
|
||||
/**
|
||||
* API 接口实例
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { Api } from "../generated/Api";
|
||||
import { Api } from '../generated/Api';
|
||||
|
||||
/**
|
||||
* 扩展生成的API类
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { Notification } from "@arco-design/web-vue";
|
||||
import { AxiosInstance } from "axios";
|
||||
import { has, isString } from "lodash-es";
|
||||
import { Notification } from '@arco-design/web-vue';
|
||||
import { AxiosInstance } from 'axios';
|
||||
import { has, isString } from 'lodash-es';
|
||||
|
||||
const successCodes = [2000];
|
||||
const expiredCodes = [4050, 4051];
|
||||
|
|
@ -15,33 +15,32 @@ let logoutTipShowing = false;
|
|||
* @param axios Axios实例
|
||||
*/
|
||||
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;
|
||||
Notification.error({
|
||||
title: "请求提示",
|
||||
title: '请求提示',
|
||||
content: msg ?? `发送请求失败,请检查参数或稍后重试!`,
|
||||
});
|
||||
return Promise.reject(error);
|
||||
});
|
||||
|
||||
axios.interceptors.response.use(
|
||||
(res) => {
|
||||
res => {
|
||||
const code = res.data?.code;
|
||||
if (code && !successCodes.includes(code)) {
|
||||
return Promise.reject(res);
|
||||
}
|
||||
return res;
|
||||
},
|
||||
(error) => {
|
||||
// 服务端响应错误
|
||||
error => {
|
||||
if (error.response) {
|
||||
const code = error.response.data?.code;
|
||||
if (expiredCodes.includes(code)) {
|
||||
if (!logoutTipShowing) {
|
||||
logoutTipShowing = true;
|
||||
Notification.warning({
|
||||
title: "登陆提示",
|
||||
content: "当前登陆已过期,请重新登陆!",
|
||||
title: '登陆提示',
|
||||
content: '当前登陆已过期,请重新登陆!',
|
||||
onClose: () => (logoutTipShowing = false),
|
||||
});
|
||||
exipreHandler?.(error);
|
||||
|
|
@ -50,10 +49,10 @@ export function addExceptionInterceptor(axios: AxiosInstance, exipreHandler?: (.
|
|||
}
|
||||
const resMsg = error.response?.data?.message;
|
||||
let message: string | null = resMsg ?? resMessageTip;
|
||||
if (error.config?.method === "get") {
|
||||
if (error.config?.method === 'get') {
|
||||
message = resGetMessage;
|
||||
}
|
||||
if (has(error.config, "resErrorTip")) {
|
||||
if (has(error.config, 'resErrorTip')) {
|
||||
const tip = error.config.resErrorTip;
|
||||
if (tip) {
|
||||
message = isString(tip) ? tip : message;
|
||||
|
|
@ -63,18 +62,15 @@ export function addExceptionInterceptor(axios: AxiosInstance, exipreHandler?: (.
|
|||
}
|
||||
if (message) {
|
||||
Notification.error({
|
||||
title: "请求提示",
|
||||
title: '请求提示',
|
||||
content: message,
|
||||
});
|
||||
}
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
||||
// 客户端请求错误
|
||||
if (error.request) {
|
||||
} else if (error.request) {
|
||||
const resMsg = error.response?.message;
|
||||
let message: string | null = resMsg ?? reqMessageTip;
|
||||
if (has(error.config, "reqErrorTip")) {
|
||||
if (has(error.config, 'reqErrorTip')) {
|
||||
const tip = error.config.reqErrorTip;
|
||||
if (tip) {
|
||||
message = isString(tip) ? tip : message;
|
||||
|
|
@ -84,7 +80,7 @@ export function addExceptionInterceptor(axios: AxiosInstance, exipreHandler?: (.
|
|||
}
|
||||
if (message) {
|
||||
Notification.error({
|
||||
title: "请求提示",
|
||||
title: '请求提示',
|
||||
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 { AxiosInstance } from 'axios';
|
||||
|
||||
|
|
@ -10,7 +10,7 @@ export function addToastInterceptor(axios: AxiosInstance) {
|
|||
axios.interceptors.request.use(
|
||||
config => {
|
||||
if (config.toast) {
|
||||
let options: IToastOptions = {};
|
||||
let options: AnToastOptions = {};
|
||||
if (typeof config.toast === 'string') {
|
||||
options = { message: config.toast };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -80,7 +80,7 @@ export const AnForm = defineComponent({
|
|||
},
|
||||
render() {
|
||||
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 => (
|
||||
<AnFormItem key={item.field} item={item} items={this.items} model={this.model}></AnFormItem>
|
||||
))}
|
||||
|
|
|
|||
|
|
@ -89,6 +89,7 @@ export const AnFormItem = defineComponent({
|
|||
return (
|
||||
<BaseFormItem
|
||||
{...props.item.itemProps}
|
||||
class="an-form-item"
|
||||
label={props.item.label as string}
|
||||
rules={rules.value}
|
||||
disabled={disabled.value}
|
||||
|
|
|
|||
|
|
@ -149,6 +149,8 @@ export const AnFormModal = defineComponent({
|
|||
onClose,
|
||||
};
|
||||
|
||||
provide(AnFormModalContextKey, context);
|
||||
|
||||
return context;
|
||||
},
|
||||
render() {
|
||||
|
|
@ -160,9 +162,10 @@ export const AnFormModal = defineComponent({
|
|||
closable={false}
|
||||
{...this.$attrs}
|
||||
{...this.modalProps}
|
||||
v-model:visible={this.visible}
|
||||
class="an-form-modal"
|
||||
maskClosable={false}
|
||||
onClose={this.onClose}
|
||||
v-model:visible={this.visible}
|
||||
>
|
||||
{{
|
||||
title: this.modalTitle,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { sleep } from "@/utils";
|
||||
import { Message } from "@arco-design/web-vue";
|
||||
import { Ref } from "vue";
|
||||
import { sleep } from '@/utils';
|
||||
import { Message } from '@arco-design/web-vue';
|
||||
import { Ref } from 'vue';
|
||||
|
||||
export function useModalSubmit(props: any, formRef: any, visible: Ref<boolean>) {
|
||||
const loading = ref(false);
|
||||
|
|
@ -12,7 +12,6 @@ export function useModalSubmit(props: any, formRef: any, visible: Ref<boolean>)
|
|||
try {
|
||||
loading.value = true;
|
||||
const data = formRef.value?.getModel() ?? {};
|
||||
await sleep(5000);
|
||||
const res = await props.submit?.(data, props.items);
|
||||
const msg = res?.data?.message;
|
||||
msg && Message.success(msg);
|
||||
|
|
|
|||
|
|
@ -1,22 +1,58 @@
|
|||
import { merge } from 'lodash-es';
|
||||
import { AnFormModal, AnFormModalProps } from '../components/FormModal';
|
||||
import { useFormProps } from './useForm';
|
||||
import { FormItem } from './useItems';
|
||||
|
||||
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[];
|
||||
};
|
||||
|
||||
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 { trigger, title, submit, modalProps } = options;
|
||||
return {
|
||||
trigger,
|
||||
model,
|
||||
items,
|
||||
formProps,
|
||||
trigger,
|
||||
title,
|
||||
modalProps,
|
||||
submit,
|
||||
formProps,
|
||||
modalProps,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -27,20 +63,18 @@ export function useFormModal(options: FormModalUseOptions) {
|
|||
const rawProps = useFormModalProps(options);
|
||||
const props = reactive(rawProps);
|
||||
|
||||
const component = () => {
|
||||
return (
|
||||
<AnFormModal
|
||||
ref={(el: any) => (modalRef.value = el)}
|
||||
title={props.title}
|
||||
trigger={props.title}
|
||||
modalProps={props.modalProps as any}
|
||||
model={props.model}
|
||||
items={props.items}
|
||||
formProps={props.formProps}
|
||||
submit={props.submit}
|
||||
></AnFormModal>
|
||||
);
|
||||
};
|
||||
const component = () => (
|
||||
<AnFormModal
|
||||
ref={(el: any) => (modalRef.value = el)}
|
||||
title={props.title}
|
||||
trigger={props.title}
|
||||
modalProps={props.modalProps as any}
|
||||
model={props.model}
|
||||
items={props.items}
|
||||
formProps={props.formProps}
|
||||
submit={props.submit}
|
||||
></AnFormModal>
|
||||
);
|
||||
|
||||
return {
|
||||
props,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,12 @@
|
|||
import { AnForm, AnFormInstance, AnFormModal, AnFormModalInstance, AnFormModalProps, AnFormProps } from '@/components/AnForm';
|
||||
import AniEmpty from '@/components/empty/AniEmpty.vue';
|
||||
import {
|
||||
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 { isArray, isFunction, merge } from 'lodash-es';
|
||||
import { InjectionKey, PropType, Ref, defineComponent, ref } from 'vue';
|
||||
|
|
@ -180,17 +187,17 @@ export const AnTable = defineComponent({
|
|||
};
|
||||
|
||||
props.pluginer?.callSetupHook(context);
|
||||
|
||||
provide(AnTableContextKey, context);
|
||||
|
||||
return context;
|
||||
},
|
||||
render() {
|
||||
return (
|
||||
<div class="table w-full">
|
||||
<div class="an-table table w-full">
|
||||
<div class={`mb-3 flex gap-2 toolbar justify-between`}>
|
||||
{this.create && <AnFormModal {...this.create} ref="createRef"></AnFormModal>}
|
||||
{this.modify && <AnFormModal {...this.modify} trigger={false} ref="modifyRef"></AnFormModal>}
|
||||
{this.$slots.action?.(this.renderData)}
|
||||
{this.pluginer?.actions && (
|
||||
<div class={`flex-1 flex gap-2 items-center`}>
|
||||
{this.pluginer.actions.map(Action => (
|
||||
|
|
@ -242,7 +249,7 @@ export const AnTable = defineComponent({
|
|||
onPageSizeChange={this.onPageSizeChange}
|
||||
>
|
||||
{{
|
||||
empty: () => <AniEmpty />,
|
||||
empty: () => <AnEmpty />,
|
||||
...this.$slots,
|
||||
}}
|
||||
</Table>
|
||||
|
|
|
|||
|
|
@ -1,25 +1,6 @@
|
|||
import { FormModalUseOptions, useFormModalProps } from '@/components/AnForm';
|
||||
|
||||
export type UseCreateFormOptions = FormModalUseOptions & {
|
||||
/**
|
||||
* 弹窗宽度
|
||||
* @description 参数 `modalProps.width` 的便捷语法
|
||||
* @example
|
||||
* ```ts
|
||||
* 580
|
||||
* ```
|
||||
*/
|
||||
width?: number;
|
||||
/**
|
||||
* 表单类名
|
||||
* @description 参数 `formProps.class` 的便捷语法
|
||||
* @example
|
||||
* ```ts
|
||||
* 'grid grid-cols-2'
|
||||
* ```
|
||||
*/
|
||||
formClass?: unknown;
|
||||
};
|
||||
export type UseCreateFormOptions = FormModalUseOptions & {};
|
||||
|
||||
export function useCreateForm(options: UseCreateFormOptions) {
|
||||
if (options.width) {
|
||||
|
|
|
|||
|
|
@ -4,24 +4,6 @@ import { ExtendFormItem } from './useSearchForm';
|
|||
import { TableUseOptions } from './useTable';
|
||||
|
||||
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
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import { SearchForm, useSearchForm } from './useSearchForm';
|
|||
import { TableColumn, useTableColumns } from './useTableColumn';
|
||||
import { AnTablePlugin, PluginContainer } from './useTablePlugin';
|
||||
import { UseCreateFormOptions } from './useCreateForm';
|
||||
import { FunctionalComponent } from 'vue';
|
||||
|
||||
export interface TableUseOptions extends Pick<AnTableProps, 'source' | 'tableProps' | 'paging'> {
|
||||
/**
|
||||
|
|
@ -103,7 +104,7 @@ export function useTable(options: TableUseOptions) {
|
|||
const rawProps = useTableProps(options);
|
||||
const props = reactive(rawProps);
|
||||
|
||||
const AnTabler = () => (
|
||||
const AnTabler: FunctionalComponent = (_, { slots }) => (
|
||||
<AnTable
|
||||
ref={(el: any) => (tableRef.value = el)}
|
||||
tableProps={props.tableProps}
|
||||
|
|
@ -114,7 +115,9 @@ export function useTable(options: TableUseOptions) {
|
|||
create={props.create as any}
|
||||
modify={props.modify as any}
|
||||
pluginer={pluginer}
|
||||
></AnTable>
|
||||
>
|
||||
{slots}
|
||||
</AnTable>
|
||||
);
|
||||
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -3,6 +3,10 @@ import { Divider, Link, TableColumnData } from '@arco-design/web-vue';
|
|||
interface TableBaseColumn {
|
||||
/**
|
||||
* 类型
|
||||
* @example
|
||||
* ```tsx
|
||||
* 'delete'
|
||||
* ```
|
||||
*/
|
||||
type?: undefined;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -76,7 +76,13 @@ export class PluginContainer {
|
|||
widgets: any[] = [];
|
||||
|
||||
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) {
|
||||
const action = plugin.action?.();
|
||||
if (action) {
|
||||
|
|
|
|||
|
|
@ -47,12 +47,13 @@ export const TableColumnConfig = defineComponent({
|
|||
dataIndex: column.dataIndex,
|
||||
title: column.title,
|
||||
enable: true,
|
||||
autoWidth: !column.width,
|
||||
autoWidth: false,
|
||||
width: column.width ?? 60,
|
||||
editable: !column.configable,
|
||||
});
|
||||
}
|
||||
items.value = list;
|
||||
onItemChange();
|
||||
};
|
||||
|
||||
const onItemChange = () => {
|
||||
|
|
|
|||
|
|
@ -8,18 +8,14 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
defineOptions({
|
||||
name: "toast",
|
||||
});
|
||||
|
||||
const props = defineProps({
|
||||
message: {
|
||||
type: String,
|
||||
default: "正在操作中,请稍等...",
|
||||
default: '正在操作中,请稍等...',
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
default: "icon-park-outline-loading-one",
|
||||
default: 'icon-park-outline-loading-one',
|
||||
},
|
||||
iconRotate: {
|
||||
type: Boolean,
|
||||
|
|
@ -37,8 +33,8 @@ const props = defineProps({
|
|||
|
||||
const style = computed(() => {
|
||||
return {
|
||||
pointerEvents: props.cover ? "initial" : "none",
|
||||
backgroundColor: props.mask ? "rgba(0, 0, 0, 0.2)" : "transparent",
|
||||
pointerEvents: props.cover ? 'initial' : 'none',
|
||||
backgroundColor: props.mask ? 'rgba(0, 0, 0, 0.2)' : 'transparent',
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
|
@ -54,8 +50,8 @@ const style = computed(() => {
|
|||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
pointer-events: v-bind("style.pointerEvents");
|
||||
background-color: v-bind("style.backgroundColor");
|
||||
pointer-events: v-bind('style.pointerEvents');
|
||||
background-color: v-bind('style.backgroundColor');
|
||||
}
|
||||
.toast-content {
|
||||
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 "./table";
|
||||
export * from "./toast";
|
||||
export * from './form';
|
||||
export * from './table';
|
||||
|
|
@ -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 { merge } from "lodash-es";
|
||||
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]) {
|
||||
return this.raw.find((item) => item.value === value)?.[key ?? ("label" as any)];
|
||||
fmt<K extends T[number]['value']>(value: K, key?: keyof T[number]) {
|
||||
return this.raw.find(item => item.value === value)?.[key ?? ('label' as any)];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取对应属性值组成的数组
|
||||
* @param key 属性名,默认为value
|
||||
* @returns
|
||||
*/
|
||||
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) {
|
||||
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/locale/zh-cn";
|
||||
import localData from "dayjs/plugin/localeData";
|
||||
import relativeTime from "dayjs/plugin/relativeTime";
|
||||
import dayjs from 'dayjs';
|
||||
import 'dayjs/locale/zh-cn';
|
||||
import localData from 'dayjs/plugin/localeData';
|
||||
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.DATETIME = DATETIME;
|
||||
|
|
@ -53,9 +52,13 @@ dayjs.DATE = DATE;
|
|||
dayjs.TIME = TIME;
|
||||
|
||||
/**
|
||||
* 重写format方法,如果没有传入format参数,则使用默认的时间格式
|
||||
* 保留原方法
|
||||
*/
|
||||
dayjs.prototype._format = dayjs.prototype.format;
|
||||
|
||||
/**
|
||||
* 重写,设置默认时间格式
|
||||
*/
|
||||
dayjs.prototype.format = function (format?: string) {
|
||||
if (format) {
|
||||
return this._format(format);
|
||||
|
|
@ -64,4 +67,3 @@ dayjs.prototype.format = function (format?: string) {
|
|||
};
|
||||
|
||||
export { DATE, DATETIME, TIME, dayjs };
|
||||
|
||||
|
|
|
|||
12
src/main.ts
12
src/main.ts
|
|
@ -1,8 +1,8 @@
|
|||
import { createApp } from "vue";
|
||||
import App from "./App.vue";
|
||||
import { router } from "./router";
|
||||
import { store } from "./store";
|
||||
import { style } from "./styles";
|
||||
import { createApp } from 'vue';
|
||||
import App from './App.vue';
|
||||
import { router } from './router';
|
||||
import { store } from './store';
|
||||
import { style } from './styles';
|
||||
|
||||
const run = async () => {
|
||||
const app = createApp(App);
|
||||
|
|
@ -10,7 +10,7 @@ const run = async () => {
|
|||
app.use(style);
|
||||
app.use(router);
|
||||
await router.isReady();
|
||||
app.mount("#app");
|
||||
app.mount('#app');
|
||||
};
|
||||
|
||||
run();
|
||||
|
|
|
|||
|
|
@ -1,52 +1,45 @@
|
|||
<template>
|
||||
<BreadPage>
|
||||
<Table v-bind="table"> </Table>
|
||||
<CategoryTable />
|
||||
</BreadPage>
|
||||
</template>
|
||||
|
||||
<script setup lang="tsx">
|
||||
import { api } from "@/api";
|
||||
import { Table, createColumn, updateColumn, useTable } from "@/components";
|
||||
import { listToTree } from "@/utils/listToTree";
|
||||
import { api } from '@/api';
|
||||
import { useCreateColumn, useTable, useUpdateColumn } from '@/components/AnTable';
|
||||
import { listToTree } from '@/utils/listToTree';
|
||||
|
||||
const table = 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 } };
|
||||
},
|
||||
const { component: CategoryTable } = useTable({
|
||||
columns: [
|
||||
{
|
||||
title: "名称",
|
||||
dataIndex: "title",
|
||||
title: '名称',
|
||||
dataIndex: 'title',
|
||||
width: 240,
|
||||
render({ record }) {
|
||||
return (
|
||||
<div class="flex flex-col overflow-hidden">
|
||||
<span>{record.title}</span>
|
||||
<span class="text-gray-400 text-xs truncate">@{record.slug}</span>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
render: ({ record }) => (
|
||||
<div class="flex flex-col overflow-hidden">
|
||||
<span>{record.title}</span>
|
||||
<span class="text-gray-400 text-xs truncate">#{record.slug}</span>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "描述",
|
||||
dataIndex: "description",
|
||||
title: '描述',
|
||||
dataIndex: 'description',
|
||||
},
|
||||
createColumn,
|
||||
updateColumn,
|
||||
useCreateColumn(),
|
||||
useUpdateColumn(),
|
||||
{
|
||||
type: "button",
|
||||
title: "操作",
|
||||
type: 'button',
|
||||
title: '操作',
|
||||
width: 120,
|
||||
buttons: [
|
||||
{
|
||||
type: "modify",
|
||||
text: "修改",
|
||||
type: 'modify',
|
||||
text: '修改',
|
||||
},
|
||||
{
|
||||
type: "delete",
|
||||
text: "删除",
|
||||
type: 'delete',
|
||||
text: '删除',
|
||||
onClick({ record }) {
|
||||
return api.category.delCategory(record.id);
|
||||
},
|
||||
|
|
@ -54,76 +47,52 @@ const table = useTable({
|
|||
],
|
||||
},
|
||||
],
|
||||
search: {
|
||||
button: false,
|
||||
items: [
|
||||
{
|
||||
field: "nickname",
|
||||
label: "登陆账号",
|
||||
type: "search",
|
||||
required: false,
|
||||
enableLoad: true,
|
||||
nodeProps: {
|
||||
placeholder: "分类名称",
|
||||
} as any,
|
||||
itemProps: {
|
||||
hideLabel: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
source: async model => {
|
||||
const res = await api.category.getCategories(model);
|
||||
const data = listToTree(res.data.data ?? []);
|
||||
return { data: { data, total: (res.data as any).total } };
|
||||
},
|
||||
create: {
|
||||
title: "添加分类",
|
||||
modalProps: {
|
||||
width: 580,
|
||||
search: [
|
||||
{
|
||||
field: 'nickname',
|
||||
label: '登陆账号',
|
||||
setter: 'search',
|
||||
enterable: true,
|
||||
searchable: true,
|
||||
},
|
||||
],
|
||||
create: {
|
||||
title: '添加分类',
|
||||
width: 580,
|
||||
items: [
|
||||
{
|
||||
field: "parentId",
|
||||
label: "父级分类",
|
||||
type: "select",
|
||||
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",
|
||||
field: 'title',
|
||||
label: '分类名称',
|
||||
setter: 'input',
|
||||
required: true,
|
||||
nodeProps: {
|
||||
placeholder: "请输入分类名称",
|
||||
},
|
||||
},
|
||||
{
|
||||
field: "slug",
|
||||
label: "分类别名",
|
||||
type: "input",
|
||||
field: 'slug',
|
||||
label: '分类别名',
|
||||
setter: 'input',
|
||||
required: true,
|
||||
nodeProps: {
|
||||
placeholder: "请输入分类别名",
|
||||
},
|
||||
},
|
||||
{
|
||||
field: "description",
|
||||
label: "描述",
|
||||
type: "textarea",
|
||||
field: 'description',
|
||||
label: '描述',
|
||||
setter: 'textarea',
|
||||
required: false,
|
||||
nodeProps: {
|
||||
placeholder: "请输入描述",
|
||||
},
|
||||
},
|
||||
],
|
||||
submit: async ({ model }) => {
|
||||
return api.category.addCategory(model);
|
||||
submit: model => {
|
||||
return api.category.addCategory(model as any);
|
||||
},
|
||||
},
|
||||
modify: {
|
||||
extend: true,
|
||||
title: "修改分类",
|
||||
submit: async ({ model }) => {
|
||||
return api.category.setCategory(model.id, model);
|
||||
title: '修改分类',
|
||||
submit: model => {
|
||||
return api.category.setCategory(model.id, model as any);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -2,10 +2,6 @@
|
|||
<div></div>
|
||||
</template>
|
||||
|
||||
<script setup lang="tsx"></script>
|
||||
|
||||
<style lang="less"></style>
|
||||
|
||||
<route lang="json">
|
||||
{
|
||||
"meta": {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<div class="w-[210px] h-full overflow-hidden grid grid-rows-[auto_1fr]">
|
||||
<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">
|
||||
<template #icon>
|
||||
<i class="icon-park-outline-add"></i>
|
||||
|
|
@ -47,18 +47,18 @@
|
|||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<ani-empty v-else></ani-empty>
|
||||
<an-empty v-else></an-empty>
|
||||
</a-spin>
|
||||
</a-scrollbar>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { FileCategory, api } from "@/api";
|
||||
import { useAniFormModal } from "@/components";
|
||||
import { delConfirm } from "@/utils";
|
||||
import { Message } from "@arco-design/web-vue";
|
||||
import { PropType } from "vue";
|
||||
import { FileCategory, api } from '@/api';
|
||||
import { useAniFormModal } from '@/components';
|
||||
import { delConfirm } from '@/utils';
|
||||
import { Message } from '@arco-design/web-vue';
|
||||
import { PropType } from 'vue';
|
||||
|
||||
defineProps({
|
||||
current: {
|
||||
|
|
@ -66,7 +66,7 @@ defineProps({
|
|||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(["change"]);
|
||||
const emit = defineEmits(['change']);
|
||||
const list = ref<FileCategory[]>([]);
|
||||
const loading = ref(false);
|
||||
|
||||
|
|
@ -75,8 +75,8 @@ const updateFileCategories = async () => {
|
|||
loading.value = true;
|
||||
const res = await api.fileCategory.getFileCategorys({ size: 0 });
|
||||
list.value = res.data.data ?? [];
|
||||
list.value.unshift({ id: undefined, name: "全部" } as any);
|
||||
list.value.length && emit("change", list.value[0]);
|
||||
list.value.unshift({ id: undefined, name: '全部' } as any);
|
||||
list.value.length && emit('change', list.value[0]);
|
||||
} catch {
|
||||
// nothing to do
|
||||
} finally {
|
||||
|
|
@ -93,7 +93,7 @@ const onDeleteRow = async (row: FileCategory) => {
|
|||
};
|
||||
|
||||
const [formModal, formCtx] = useAniFormModal({
|
||||
title: ({ model }) => (!model.id ? "新建分类" : "修改分类"),
|
||||
title: ({ model }) => (!model.id ? '新建分类' : '修改分类'),
|
||||
trigger: false,
|
||||
modalProps: {
|
||||
width: 580,
|
||||
|
|
@ -103,19 +103,19 @@ const [formModal, formCtx] = useAniFormModal({
|
|||
},
|
||||
items: [
|
||||
{
|
||||
field: "name",
|
||||
label: "分类名称",
|
||||
type: "input",
|
||||
field: 'name',
|
||||
label: '分类名称',
|
||||
type: 'input',
|
||||
},
|
||||
{
|
||||
field: "code",
|
||||
label: "分类编码",
|
||||
type: "input",
|
||||
field: 'code',
|
||||
label: '分类编码',
|
||||
type: 'input',
|
||||
},
|
||||
{
|
||||
field: "description",
|
||||
label: "备注",
|
||||
type: "textarea",
|
||||
field: 'description',
|
||||
label: '备注',
|
||||
type: 'textarea',
|
||||
},
|
||||
],
|
||||
submit: async ({ model }) => {
|
||||
|
|
@ -1,10 +1,15 @@
|
|||
<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
|
||||
v-model:visible="visible"
|
||||
title="上传文件"
|
||||
title-align="start"
|
||||
:width="860"
|
||||
:width="940"
|
||||
:mask-closable="false"
|
||||
:on-before-cancel="onBeforeCancel"
|
||||
@close="onClose"
|
||||
|
|
@ -22,7 +27,7 @@
|
|||
@error="onUploadError"
|
||||
>
|
||||
<template #upload-button>
|
||||
<a-button type="outline"> 选择文件 </a-button>
|
||||
<a-button type="primary"> 选择文件 </a-button>
|
||||
</template>
|
||||
</a-upload>
|
||||
<div class="flex-1 flex items-center text-gray-400">
|
||||
|
|
@ -45,20 +50,21 @@
|
|||
</div>
|
||||
<div class="flex items-center justify-between gap-2 text-gray-400 mb-[-4px] mt-0.5">
|
||||
<span class="text-xs text-gray-400">
|
||||
{{ numeral(item.file?.size).format("0 b") }}
|
||||
{{ numeral(item.file?.size).format('0 b') }}
|
||||
</span>
|
||||
<span class="text-xs">
|
||||
<span v-if="item.status === 'init'"> </span>
|
||||
<span v-else-if="item.status === 'uploading'">
|
||||
<span class="text-xs">
|
||||
速度:{{ numeral(fileMap.get(item.uid)?.speed || 0).format("0 b") }}/s,
|
||||
进度:{{ Math.floor((item.percent || 0) * 100) }}%
|
||||
速度:{{ numeral(fileMap.get(item.uid)?.speed || 0).format('0 b') }}/s, 进度:{{
|
||||
Math.floor((item.percent || 0) * 100)
|
||||
}}%
|
||||
</span>
|
||||
</span>
|
||||
<span v-else-if="item.status === 'done'" class="text-green-600">
|
||||
完成(
|
||||
耗时:{{ fileMap.get(item.uid)?.cost || 0 }}秒,
|
||||
平均:{{ numeral(fileMap.get(item.uid)?.aspeed || 0).format("0 b") }}/s)
|
||||
完成( 耗时:{{ fileMap.get(item.uid)?.cost || 0 }}秒, 平均:{{
|
||||
numeral(fileMap.get(item.uid)?.aspeed || 0).format('0 b')
|
||||
}}/s)
|
||||
</span>
|
||||
<span v-else="item.status === 'error'" class="text-red-500">
|
||||
失败(原因:{{ fileMap.get(item.uid)?.error }})
|
||||
|
|
@ -77,7 +83,7 @@
|
|||
</ul>
|
||||
|
||||
<div v-else class="h-[424px] flex items-center justify-center">
|
||||
<ani-empty></ani-empty>
|
||||
<an-empty></an-empty>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
|
|
@ -97,16 +103,16 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { RequestParams, api } from "@/api";
|
||||
import { delConfirm } from "@/utils";
|
||||
import { FileItem, Message, RequestOption, UploadInstance } from "@arco-design/web-vue";
|
||||
import axios from "axios";
|
||||
import numeral from "numeral";
|
||||
import { getIcon } from "./util";
|
||||
import { RequestParams, api } from '@/api';
|
||||
import { delConfirm } from '@/utils';
|
||||
import { FileItem, Message, RequestOption, UploadInstance } from '@arco-design/web-vue';
|
||||
import axios from 'axios';
|
||||
import numeral from 'numeral';
|
||||
import { getIcon } from './util';
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: "success", item: FileItem): void;
|
||||
(event: "close", count: number): void;
|
||||
(event: 'success', item: FileItem): void;
|
||||
(event: 'close', count: number): void;
|
||||
}>();
|
||||
|
||||
const visible = ref(false);
|
||||
|
|
@ -137,10 +143,10 @@ const stat = computed(() => {
|
|||
errorCount: 0,
|
||||
};
|
||||
for (const item of fileList.value) {
|
||||
if (item.status === "init") result.initCount++;
|
||||
if (item.status === "uploading") result.uploadingCount++;
|
||||
if (item.status === "done") result.doneCount++;
|
||||
if (item.status === "error") result.errorCount++;
|
||||
if (item.status === 'init') result.initCount++;
|
||||
if (item.status === 'uploading') result.uploadingCount++;
|
||||
if (item.status === 'done') result.doneCount++;
|
||||
if (item.status === 'error') result.errorCount++;
|
||||
}
|
||||
return result;
|
||||
});
|
||||
|
|
@ -160,7 +166,7 @@ const pauseItem = (item: FileItem) => {
|
|||
uploadRef.value?.abort(item);
|
||||
const file = fileMap.get(item.uid);
|
||||
if (file) {
|
||||
file.error = "手动中止";
|
||||
file.error = '手动中止';
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -169,7 +175,7 @@ const pauseItem = (item: FileItem) => {
|
|||
* @param item 文件
|
||||
*/
|
||||
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) {
|
||||
fileList.value.splice(index, 1);
|
||||
}
|
||||
|
|
@ -188,7 +194,7 @@ const retryItem = (item: FileItem) => {
|
|||
*/
|
||||
const clearUploaded = async () => {
|
||||
if (stat.value.doneCount !== fileList.value.length) {
|
||||
await delConfirm("当前有未上传完成的文件,是否继续清空?");
|
||||
await delConfirm('当前有未上传完成的文件,是否继续清空?');
|
||||
}
|
||||
fileList.value = [];
|
||||
};
|
||||
|
|
@ -198,7 +204,7 @@ const clearUploaded = async () => {
|
|||
* @param item 文件
|
||||
*/
|
||||
const onUploadSuccess = (item: FileItem) => {
|
||||
emit("success", item);
|
||||
emit('success', item);
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -208,7 +214,7 @@ const onUploadSuccess = (item: FileItem) => {
|
|||
const onUploadError = (item: FileItem) => {
|
||||
const file = fileMap.get(item.uid);
|
||||
if (file) {
|
||||
file.error = item.response?.data?.message || "网络异常";
|
||||
file.error = item.response?.data?.message || '网络异常';
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -216,8 +222,8 @@ const onUploadError = (item: FileItem) => {
|
|||
* 关闭前检测
|
||||
*/
|
||||
const onBeforeCancel = () => {
|
||||
if (fileList.value.some((i) => i.status === "uploading")) {
|
||||
Message.warning("提示:文件上传中,请稍后再试!");
|
||||
if (fileList.value.some(i => i.status === 'uploading')) {
|
||||
Message.warning('提示:文件上传中,请稍后再试!');
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
|
|
@ -229,7 +235,7 @@ const onBeforeCancel = () => {
|
|||
const onClose = () => {
|
||||
fileMap.clear();
|
||||
fileList.value = [];
|
||||
emit("close", stat.value.doneCount);
|
||||
emit('close', stat.value.doneCount);
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -246,7 +252,7 @@ const upload = (option: RequestOption) => {
|
|||
cost: 0,
|
||||
speed: 0,
|
||||
aspeed: 0,
|
||||
error: "网络异常",
|
||||
error: '网络异常',
|
||||
});
|
||||
}
|
||||
const item = fileMap.get(fileItem.uid)!;
|
||||
|
|
@ -262,7 +268,7 @@ const upload = (option: RequestOption) => {
|
|||
const nowTime = Date.now();
|
||||
const diff = (e.loaded - lastLoaded) / (nowTime - lastTime);
|
||||
const speed = Math.floor(diff * 1000);
|
||||
item.aspeed = (item.speed + speed) / 2
|
||||
item.aspeed = (item.speed + speed) / 2;
|
||||
item.speed = speed;
|
||||
item.lastLoaded = e.loaded;
|
||||
item.lastTime = nowTime;
|
||||
|
|
@ -297,15 +303,15 @@ defineExpose({
|
|||
});
|
||||
|
||||
// TODO
|
||||
const group = ref("default");
|
||||
const group = ref('default');
|
||||
const groupOptions = [
|
||||
{
|
||||
label: "默认分类",
|
||||
value: "default",
|
||||
label: '默认分类',
|
||||
value: 'default',
|
||||
},
|
||||
{
|
||||
label: "视频分类",
|
||||
value: "video",
|
||||
label: '视频分类',
|
||||
value: 'video',
|
||||
},
|
||||
];
|
||||
</script>
|
||||
|
|
@ -2,16 +2,13 @@
|
|||
<BreadPage>
|
||||
<template #content>
|
||||
<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">
|
||||
<file-table>
|
||||
<MaterialTable>
|
||||
<template #action>
|
||||
<ani-upload @close="onUploadClose"></ani-upload>
|
||||
<a-button type="primary" status="danger" :disabled="!selected.length" @click="onDeleteMany">
|
||||
批量删除
|
||||
</a-button>
|
||||
<AnUpload></AnUpload>
|
||||
</template>
|
||||
</file-table>
|
||||
</MaterialTable>
|
||||
<a-image-preview v-model:visible="visible" :src="image"></a-image-preview>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -20,70 +17,47 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="tsx">
|
||||
import { FileCategory, api } from "@/api";
|
||||
import { createColumn, updateColumn, useAniTable } from "@/components";
|
||||
import { delConfirm } from "@/utils";
|
||||
import { Message } from "@arco-design/web-vue";
|
||||
import numeral from "numeral";
|
||||
import AniGroup from "./components/group.vue";
|
||||
import AniUpload from "./components/upload.vue";
|
||||
import { getIcon } from "./components/util";
|
||||
import { FileCategory, api } from '@/api';
|
||||
import { useCreateColumn, useTable, useUpdateColumn } from '@/components/AnTable';
|
||||
import { getIcon } from './components/util';
|
||||
import numeral from 'numeral';
|
||||
import AnGroup from './components/AnGroup.vue';
|
||||
import AnUpload from './components/AnUpload.vue';
|
||||
|
||||
const visible = ref(false);
|
||||
const image = ref("");
|
||||
const selected = ref<number[]>([]);
|
||||
const current = ref<FileCategory>();
|
||||
const image = ref('');
|
||||
|
||||
const preview = (record: any) => {
|
||||
if (!record.mimetype.startsWith("image")) {
|
||||
window.open(record.path, "_blank");
|
||||
if (!record.mimetype.startsWith('image')) {
|
||||
window.open(record.path, '_blank');
|
||||
return;
|
||||
}
|
||||
image.value = record.path;
|
||||
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) => {
|
||||
if (fileCtx.props.search?.model) {
|
||||
fileCtx.props.search.model.categoryId = category.id;
|
||||
if (props.search?.model) {
|
||||
props.search.model.categoryId = category.id;
|
||||
}
|
||||
current.value = category;
|
||||
fileCtx.refresh();
|
||||
tableRef.value?.refresh();
|
||||
};
|
||||
|
||||
const [fileTable, fileCtx] = useAniTable({
|
||||
data: async (model, paging) => {
|
||||
return api.file.getFiles({ ...model, ...paging });
|
||||
},
|
||||
tableProps: {
|
||||
rowSelection: {
|
||||
showCheckedAll: true,
|
||||
},
|
||||
onSelectionChange(rowKeys) {
|
||||
selected.value = rowKeys as number[];
|
||||
},
|
||||
},
|
||||
const {
|
||||
component: MaterialTable,
|
||||
tableRef,
|
||||
props,
|
||||
} = useTable({
|
||||
columns: [
|
||||
{
|
||||
title: "文件名称",
|
||||
dataIndex: "name",
|
||||
title: '文件名称',
|
||||
dataIndex: 'name',
|
||||
render: ({ record }) => (
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="w-8 flex justify-center">
|
||||
{record.mimetype.startsWith("image") ? (
|
||||
{record.mimetype.startsWith('image') ? (
|
||||
<a-avatar size={26} shape="square">
|
||||
<img src={record.path}></img>
|
||||
</a-avatar>
|
||||
|
|
@ -99,80 +73,80 @@ const [fileTable, fileCtx] = useAniTable({
|
|||
{record.name}
|
||||
</span>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
createColumn,
|
||||
updateColumn,
|
||||
useCreateColumn(),
|
||||
useUpdateColumn(),
|
||||
{
|
||||
type: "button",
|
||||
title: "操作",
|
||||
width: 120,
|
||||
type: 'button',
|
||||
title: '操作',
|
||||
width: 160,
|
||||
buttons: [
|
||||
{
|
||||
type: "modify",
|
||||
text: "修改",
|
||||
text: '下载',
|
||||
onClick: props => {
|
||||
window.open(props.record.path, '_blank');
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "delete",
|
||||
text: "删除",
|
||||
onClick({ record }) {
|
||||
return api.file.delFile(record.id);
|
||||
type: 'modify',
|
||||
text: '修改',
|
||||
},
|
||||
{
|
||||
type: 'delete',
|
||||
text: '删除',
|
||||
onClick: props => {
|
||||
return api.file.delFile(props.record.id);
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
source: async model => {
|
||||
return api.file.getFiles(model);
|
||||
},
|
||||
search: {
|
||||
button: false,
|
||||
hideSearch: false,
|
||||
model: {
|
||||
categoryId: undefined,
|
||||
},
|
||||
items: [
|
||||
{
|
||||
field: "name",
|
||||
label: "文件名称",
|
||||
type: "search",
|
||||
field: 'name',
|
||||
label: '素材名称',
|
||||
setter: 'search',
|
||||
searchable: true,
|
||||
enterable: true,
|
||||
itemProps: {
|
||||
hideLabel: true,
|
||||
},
|
||||
nodeProps: {
|
||||
placeholder: "素材名称",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
modify: {
|
||||
title: "修改素材",
|
||||
modalProps: {
|
||||
width: 580,
|
||||
},
|
||||
title: '修改素材',
|
||||
width: 580,
|
||||
items: [
|
||||
{
|
||||
field: "categoryId",
|
||||
label: "分类",
|
||||
type: "select",
|
||||
options: () => api.fileCategory.getFileCategorys({ size: 0 }),
|
||||
field: 'categoryId',
|
||||
label: '分类',
|
||||
setter: 'select',
|
||||
options: () => api.fileCategory.getFileCategorys({ size: 0 }) as any,
|
||||
},
|
||||
{
|
||||
field: "name",
|
||||
label: "名称",
|
||||
type: "input",
|
||||
field: 'name',
|
||||
label: '名称',
|
||||
setter: 'input',
|
||||
},
|
||||
{
|
||||
field: "description",
|
||||
label: "描述",
|
||||
type: "textarea",
|
||||
field: 'description',
|
||||
label: '描述',
|
||||
setter: 'textarea',
|
||||
},
|
||||
],
|
||||
submit: ({ model }) => {
|
||||
console.log(model);
|
||||
submit: 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);
|
||||
},
|
||||
search: {
|
||||
|
|
@ -84,19 +84,18 @@ const { component: DictTable, tableRef } = useTable({
|
|||
create: {
|
||||
title: '新增字典',
|
||||
width: 580,
|
||||
model: {
|
||||
typeId: undefined,
|
||||
},
|
||||
items: [
|
||||
{
|
||||
field: 'name',
|
||||
label: '字典名',
|
||||
setter: 'input',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
field: 'code',
|
||||
label: '字典值',
|
||||
setter: 'input',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
field: 'description',
|
||||
|
|
@ -105,14 +104,16 @@ const { component: DictTable, tableRef } = useTable({
|
|||
},
|
||||
],
|
||||
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: {
|
||||
extend: true,
|
||||
title: '修改字典',
|
||||
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>
|
||||
<script setup lang="tsx"></script>
|
||||
<style scoped></style>
|
||||
|
||||
<route lang="json">
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,31 +1,30 @@
|
|||
<template>
|
||||
<BreadPage>
|
||||
<Table v-bind="table"> </Table>
|
||||
<UserTable />
|
||||
<pass-modal></pass-modal>
|
||||
</BreadPage>
|
||||
</template>
|
||||
|
||||
<script setup lang="tsx">
|
||||
import { api } from "@/api";
|
||||
import { Table, createColumn, updateColumn, useTable } from "@/components";
|
||||
import InputAvatar from "./components/avatar.vue";
|
||||
import { usePassworModal } from "./components/password";
|
||||
import { api } from '@/api';
|
||||
import { useCreateColumn, useTable, useUpdateColumn } from '@/components/AnTable';
|
||||
import { usePassworModal } from './components/password';
|
||||
|
||||
defineOptions({ name: "SystemUserPage" });
|
||||
defineOptions({ name: 'SystemUserPage' });
|
||||
const [passModal, passCtx] = usePassworModal();
|
||||
|
||||
const table = useTable({
|
||||
data: async (model, paging) => {
|
||||
return api.user.getUsers({ ...model, ...paging });
|
||||
const { component: UserTable } = useTable({
|
||||
source: async model => {
|
||||
return api.user.getUsers(model);
|
||||
},
|
||||
columns: [
|
||||
{
|
||||
title: "用户昵称",
|
||||
dataIndex: "username",
|
||||
title: '用户昵称',
|
||||
dataIndex: 'username',
|
||||
render: ({ record }) => (
|
||||
<div class="flex items-center">
|
||||
<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>
|
||||
<span class="ml-2 flex-1 flex flex-col overflow-hidden">
|
||||
<span>{record.nickname}</span>
|
||||
|
|
@ -35,30 +34,30 @@ const table = useTable({
|
|||
),
|
||||
},
|
||||
{
|
||||
title: "用户邮箱",
|
||||
dataIndex: "email",
|
||||
title: '用户邮箱',
|
||||
dataIndex: 'email',
|
||||
width: 200,
|
||||
},
|
||||
createColumn,
|
||||
updateColumn,
|
||||
useCreateColumn(),
|
||||
useUpdateColumn(),
|
||||
{
|
||||
title: "操作",
|
||||
type: "button",
|
||||
title: '操作',
|
||||
type: 'button',
|
||||
width: 200,
|
||||
buttons: [
|
||||
{
|
||||
type: "modify",
|
||||
text: "修改",
|
||||
type: 'modify',
|
||||
text: '修改',
|
||||
},
|
||||
{
|
||||
text: "设置密码",
|
||||
text: '设置密码',
|
||||
onClick({ record }) {
|
||||
passCtx.open(record);
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "delete",
|
||||
text: "删除",
|
||||
type: 'delete',
|
||||
text: '删除',
|
||||
onClick: async ({ record }) => {
|
||||
return api.user.delUser(record.id, { toast: true });
|
||||
},
|
||||
|
|
@ -67,115 +66,80 @@ const table = useTable({
|
|||
},
|
||||
],
|
||||
search: {
|
||||
button: true,
|
||||
hideSearch: true,
|
||||
items: [
|
||||
{
|
||||
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",
|
||||
},
|
||||
{
|
||||
field: "nickname",
|
||||
label: "用户昵称",
|
||||
type: "input",
|
||||
field: 'nickname',
|
||||
label: '用户昵称',
|
||||
setter: 'input',
|
||||
},
|
||||
],
|
||||
},
|
||||
create: {
|
||||
title: "新建用户",
|
||||
modalProps: {
|
||||
width: 820,
|
||||
maskClosable: false,
|
||||
},
|
||||
formProps: {
|
||||
layout: "vertical",
|
||||
class: "!grid grid-cols-2 gap-x-6",
|
||||
},
|
||||
model: {},
|
||||
title: '新建用户',
|
||||
width: 820,
|
||||
formClass: '!grid grid-cols-2 gap-x-6',
|
||||
items: [
|
||||
{
|
||||
field: "avatar",
|
||||
label: "用户头像",
|
||||
type: "custom",
|
||||
itemProps: {
|
||||
class: "col-span-2",
|
||||
},
|
||||
component({ model }) {
|
||||
return <InputAvatar v-model={model.avatar}></InputAvatar>;
|
||||
field: 'avatar',
|
||||
label: '用户头像',
|
||||
setter: 'input',
|
||||
setterProps: {
|
||||
class: 'col-span-2',
|
||||
},
|
||||
},
|
||||
{
|
||||
field: "username",
|
||||
label: "登录账号",
|
||||
type: "input",
|
||||
field: 'username',
|
||||
label: '登录账号',
|
||||
setter: 'input',
|
||||
required: true,
|
||||
nodeProps: {
|
||||
placeholder: "英文字母+数组组成,5~10位",
|
||||
setterProps: {
|
||||
placeholder: '英文字母+数组组成,5~10位',
|
||||
},
|
||||
},
|
||||
{
|
||||
field: "password",
|
||||
label: "登陆密码",
|
||||
type: "input",
|
||||
nodeProps: {
|
||||
placeholder: "包含大小写,长度6 ~ 12位",
|
||||
field: 'password',
|
||||
label: '登陆密码',
|
||||
setter: 'input',
|
||||
setterProps: {
|
||||
placeholder: '包含大小写,长度6 ~ 12位',
|
||||
},
|
||||
},
|
||||
{
|
||||
field: "nickname",
|
||||
label: "用户昵称",
|
||||
type: "input",
|
||||
field: 'nickname',
|
||||
label: '用户昵称',
|
||||
setter: 'input',
|
||||
},
|
||||
{
|
||||
field: "roleIds",
|
||||
label: "关联角色",
|
||||
type: "select",
|
||||
options: () => api.role.getRoles(),
|
||||
nodeProps: {
|
||||
field: 'roleIds',
|
||||
label: '关联角色',
|
||||
setter: 'select',
|
||||
options: () => api.role.getRoles() as any,
|
||||
setterProps: {
|
||||
multiple: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
field: "description",
|
||||
label: "个人描述",
|
||||
type: "textarea",
|
||||
field: 'description',
|
||||
label: '个人描述',
|
||||
setter: 'textarea',
|
||||
itemProps: {
|
||||
class: "col-span-2",
|
||||
class: 'col-span-2',
|
||||
},
|
||||
nodeProps: {
|
||||
class: "h-[96px]",
|
||||
setterProps: {
|
||||
class: 'h-[96px]',
|
||||
},
|
||||
},
|
||||
],
|
||||
submit: ({ model }) => {
|
||||
return api.user.addUser(model);
|
||||
submit: model => {
|
||||
return api.user.addUser(model as any);
|
||||
},
|
||||
},
|
||||
modify: {
|
||||
extend: true,
|
||||
title: "修改用户",
|
||||
submit: ({ model }) => {
|
||||
return api.user.setUser(model.id, model);
|
||||
title: '修改用户',
|
||||
submit: model => {
|
||||
return api.user.setUser(model.id, model as any);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -19,11 +19,23 @@
|
|||
<template #help> 支持 5MB 以内大小, png 或 jpg 格式的图片 </template>
|
||||
</a-form-item>
|
||||
<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>
|
||||
</a-form-item>
|
||||
<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 label="性别">
|
||||
<a-radio-group v-model="user.gender" type="button">
|
||||
|
|
@ -173,16 +185,16 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="tsx">
|
||||
import { reactive } from "vue";
|
||||
import { reactive } from 'vue';
|
||||
|
||||
const user = reactive({
|
||||
nickname: "绝弹",
|
||||
description: "选择在公开个人资料中显示私有项目的贡献,但不显示任何项目,仓库或组织信息",
|
||||
theme: "dark",
|
||||
email: "810335188@qq.com",
|
||||
nickname: '绝弹',
|
||||
description: '选择在公开个人资料中显示私有项目的贡献,但不显示任何项目,仓库或组织信息',
|
||||
theme: 'dark',
|
||||
email: '810335188@qq.com',
|
||||
msg: [2],
|
||||
gender: 1,
|
||||
birth: "1988-12-18",
|
||||
birth: '1988-12-18',
|
||||
});
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,16 +1,16 @@
|
|||
import { api } from "@/api";
|
||||
import { store, useUserStore } from "@/store";
|
||||
import { useMenuStore } from "@/store/menu";
|
||||
import { treeEach, treeFilter, treeFind } from "@/utils/listToTree";
|
||||
import { Notification } from "@arco-design/web-vue";
|
||||
import { Router } from "vue-router";
|
||||
import { MenuItem, menus } from "../menus";
|
||||
import { APP_HOME_NAME } from "../routes/base";
|
||||
import { APP_ROUTE_NAME, routes } from "../routes/page";
|
||||
import { env } from "@/config/env";
|
||||
import { api } from '@/api';
|
||||
import { store, useUserStore } from '@/store';
|
||||
import { useMenuStore } from '@/store/menu';
|
||||
import { treeEach, treeFilter, treeFind } from '@/utils/listToTree';
|
||||
import { Notification } from '@arco-design/web-vue';
|
||||
import { Router } from 'vue-router';
|
||||
import { menus } from '../menus';
|
||||
import { APP_HOME_NAME } from '../routes/base';
|
||||
import { APP_ROUTE_NAME, routes } from '../routes/page';
|
||||
import { env } from '@/config/env';
|
||||
|
||||
const WHITE_LIST = ["/:all(.*)*"];
|
||||
const UNSIGNIN_LIST = ["/login"];
|
||||
const WHITE_LIST = ['/:all(.*)*'];
|
||||
const UNSIGNIN_LIST = ['/login'];
|
||||
|
||||
/**
|
||||
* 权限守卫
|
||||
|
|
@ -23,7 +23,7 @@ export function useAuthGuard(router: Router) {
|
|||
const userStore = useUserStore(store);
|
||||
const redirect = router.currentRoute.value.path;
|
||||
userStore.clearUser();
|
||||
router.push({ path: "/login", query: { redirect } });
|
||||
router.push({ path: '/login', query: { redirect } });
|
||||
};
|
||||
|
||||
router.beforeEach(async function (to, from) {
|
||||
|
|
@ -31,7 +31,7 @@ export function useAuthGuard(router: Router) {
|
|||
const menuStore = useMenuStore(store);
|
||||
|
||||
// 手动指定直接通过
|
||||
if (to.meta.auth?.some((i) => i === "*")) {
|
||||
if (to.meta.auth?.some(i => i === '*')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -49,13 +49,13 @@ export function useAuthGuard(router: Router) {
|
|||
|
||||
// 已登陆进行提示
|
||||
Notification.warning({
|
||||
title: "跳转提示",
|
||||
title: '跳转提示',
|
||||
content: `您已登陆,如需重新登陆请退出后再操作!`,
|
||||
});
|
||||
|
||||
// 不是从路由跳转的,跳转回首页
|
||||
if (!from.matched.length) {
|
||||
return "/";
|
||||
return '/';
|
||||
}
|
||||
|
||||
// 已登陆不允许
|
||||
|
|
@ -64,15 +64,15 @@ export function useAuthGuard(router: Router) {
|
|||
|
||||
// 未登录跳转到登陆页面
|
||||
if (!userStore.accessToken) {
|
||||
return { path: "/login", query: { redirect: to.path } };
|
||||
return { path: '/login', query: { redirect: to.path } };
|
||||
}
|
||||
|
||||
// 未获取菜单进行获取
|
||||
if (!menuStore.menus.length) {
|
||||
// 菜单处理
|
||||
const authMenus = treeFilter(menus, (item) => {
|
||||
const authMenus = treeFilter(menus, item => {
|
||||
if (item.path === env.homePath) {
|
||||
item.path = "/";
|
||||
item.path = '/';
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
|
@ -101,9 +101,9 @@ export function useAuthGuard(router: Router) {
|
|||
menuStore.setCacheAppNames(appNames);
|
||||
|
||||
// 首页处理
|
||||
const home = treeFind(routes, (i) => i.path === menuStore.home);
|
||||
const home = treeFind(routes, i => i.path === menuStore.home);
|
||||
if (home) {
|
||||
const route = { ...home, name: APP_HOME_NAME, alias: "/" };
|
||||
const route = { ...home, name: APP_HOME_NAME, alias: '/' };
|
||||
router.removeRoute(home.name!);
|
||||
router.addRoute(APP_ROUTE_NAME, route);
|
||||
return router.replace(to.path);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { NProgress } from "@/libs/nprogress";
|
||||
import { useAppStore } from "@/store";
|
||||
import { Router } from "vue-router";
|
||||
import { NProgress } from '@/libs/nprogress';
|
||||
import { useAppStore } from '@/store';
|
||||
import { Router } from 'vue-router';
|
||||
|
||||
const routeMap = new Map<string, boolean>();
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { store, useAppStore } from "@/store";
|
||||
import { Router } from "vue-router";
|
||||
import { store, useAppStore } from '@/store';
|
||||
import { Router } from 'vue-router';
|
||||
|
||||
export function useTitleGuard(router: Router) {
|
||||
router.beforeEach(function (to) {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
export * from "./menus";
|
||||
export * from "./router";
|
||||
export * from "./routes/page";
|
||||
|
||||
export * from './menus';
|
||||
export * from './router';
|
||||
export * from './routes/page';
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { RouteRecordRaw } from "vue-router";
|
||||
import { appRoutes } from "../routes/page";
|
||||
import { RouteRecordRaw } from 'vue-router';
|
||||
import { appRoutes } from '../routes/page';
|
||||
|
||||
/**
|
||||
* 菜单项类型
|
||||
|
|
@ -20,7 +20,6 @@ export interface MenuItem {
|
|||
/**
|
||||
* 转换页面路由为菜单项
|
||||
* @param routes 路由配置
|
||||
* @returns
|
||||
*/
|
||||
function routesToItems(routes: RouteRecordRaw[]): MenuItem[] {
|
||||
const items: MenuItem[] = [];
|
||||
|
|
@ -29,8 +28,8 @@ function routesToItems(routes: RouteRecordRaw[]): MenuItem[] {
|
|||
const { meta = {}, parentMeta, path } = route as any;
|
||||
const { title, sort, icon, keepAlive = false, name } = meta;
|
||||
let id = path;
|
||||
let paths = route.path.split("/");
|
||||
let parentId = paths.slice(0, -1).join("/");
|
||||
let paths = route.path.split('/');
|
||||
let parentId = paths.slice(0, -1).join('/');
|
||||
if (parentMeta) {
|
||||
const { title, icon, sort } = parentMeta;
|
||||
id = `${path}/index`;
|
||||
|
|
@ -42,11 +41,11 @@ function routesToItems(routes: RouteRecordRaw[]): MenuItem[] {
|
|||
path,
|
||||
id: path,
|
||||
keepAlive: false,
|
||||
parentId: paths.slice(0, -1).join("/"),
|
||||
parentId: paths.slice(0, -1).join('/'),
|
||||
});
|
||||
} else {
|
||||
const p = paths.slice(0, -1).join("/");
|
||||
if (routes.some((i) => i.path === p) && parentMeta) {
|
||||
const p = paths.slice(0, -1).join('/');
|
||||
if (routes.some(i => i.path === p) && parentMeta) {
|
||||
parentId = p;
|
||||
}
|
||||
}
|
||||
|
|
@ -59,7 +58,6 @@ function routesToItems(routes: RouteRecordRaw[]): MenuItem[] {
|
|||
/**
|
||||
* 转换菜单项为树形结构
|
||||
* @param list 菜单项列表
|
||||
* @returns
|
||||
*/
|
||||
function listToTree(list: MenuItem[]) {
|
||||
const map: Record<string, MenuItem> = {};
|
||||
|
|
@ -85,9 +83,8 @@ function listToTree(list: MenuItem[]) {
|
|||
* 排序菜单项
|
||||
* @param routes 菜单项列表
|
||||
* @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) => {
|
||||
if (Array.isArray(a.children)) {
|
||||
a.children = sort(a.children);
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import { createRouter } from "vue-router";
|
||||
import { useAuthGuard } from "../guards/auth";
|
||||
import { useProgressGard } from "../guards/progress";
|
||||
import { useTitleGuard } from "../guards/title";
|
||||
import { baseRoutes } from "../routes/base";
|
||||
import { historyMode } from "./util";
|
||||
import { routes } from "../routes/page";
|
||||
import { createRouter } from 'vue-router';
|
||||
import { useAuthGuard } from '../guards/auth';
|
||||
import { useProgressGard } from '../guards/progress';
|
||||
import { useTitleGuard } from '../guards/title';
|
||||
import { baseRoutes } from '../routes/base';
|
||||
import { historyMode } from './util';
|
||||
import { routes } from '../routes/page';
|
||||
|
||||
/**
|
||||
* 路由实例
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { env } from "@/config/env";
|
||||
import { createWebHashHistory, createWebHistory } from "vue-router";
|
||||
import { env } from '@/config/env';
|
||||
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[] = [
|
||||
{
|
||||
path: "/",
|
||||
path: '/',
|
||||
name: APP_HOME_NAME,
|
||||
component: () => "Home Page",
|
||||
component: () => 'Home Page',
|
||||
},
|
||||
];
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import generatedRoutes from "virtual:generated-pages";
|
||||
import { RouteRecordRaw } from "vue-router";
|
||||
import generatedRoutes from 'virtual:generated-pages';
|
||||
import { RouteRecordRaw } from 'vue-router';
|
||||
|
||||
export const TOP_ROUTE_PREF = "_";
|
||||
export const APP_ROUTE_NAME = "_layout";
|
||||
export const TOP_ROUTE_PREF = '_';
|
||||
export const APP_ROUTE_NAME = '_layout';
|
||||
|
||||
/**
|
||||
* 转换路由
|
||||
|
|
@ -17,7 +17,7 @@ const transformRoutes = (routes: RouteRecordRaw[]) => {
|
|||
if (route.name === APP_ROUTE_NAME) {
|
||||
route.children = appRoutes;
|
||||
}
|
||||
route.path = route.path.replace(TOP_ROUTE_PREF, "");
|
||||
route.path = route.path.replace(TOP_ROUTE_PREF, '');
|
||||
topRoutes.push(route);
|
||||
continue;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
export * from "./app";
|
||||
export * from "./store";
|
||||
export * from "./user";
|
||||
|
||||
export * from './app';
|
||||
export * from './store';
|
||||
export * from './user';
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ declare module '@vue/runtime-core' {
|
|||
ACheckbox: typeof import('@arco-design/web-vue')['Checkbox']
|
||||
ACheckboxGroup: typeof import('@arco-design/web-vue')['CheckboxGroup']
|
||||
AConfigProvider: typeof import('@arco-design/web-vue')['ConfigProvider']
|
||||
ADatePicker: typeof import('@arco-design/web-vue')['DatePicker']
|
||||
ADivider: typeof import('@arco-design/web-vue')['Divider']
|
||||
ADoption: typeof import('@arco-design/web-vue')['Doption']
|
||||
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']
|
||||
AMenuItem: typeof import('@arco-design/web-vue')['MenuItem']
|
||||
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']
|
||||
APopover: typeof import('@arco-design/web-vue')['Popover']
|
||||
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']
|
||||
ASpin: typeof import('@arco-design/web-vue')['Spin']
|
||||
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']
|
||||
ATextarea: typeof import('@arco-design/web-vue')['Textarea']
|
||||
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']
|
||||
Marquee: typeof import('./../components/editor/blocks/text/marquee.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']
|
||||
PanelLeft: typeof import('./../components/editor/components/PanelLeft.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']
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
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