feat: 添加文件分类功能

master
luoer 2023-11-02 17:37:40 +08:00
parent 93c9c3185a
commit c84da369cf
14 changed files with 804 additions and 186 deletions

2
.env
View File

@ -6,7 +6,7 @@ VITE_TITLE = 绝弹项目管理
# 网站副标题 # 网站副标题
VITE_SUBTITLE = 快速开发web应用的模板工具 VITE_SUBTITLE = 快速开发web应用的模板工具
# 接口前缀 说明:参见 axios 的 baseURL # 接口前缀 说明:参见 axios 的 baseURL
VITE_API = / VITE_API = http://127.0.0.1:3030/
# ===================================================================================== # =====================================================================================
# 开发设置 # 开发设置

View File

@ -152,7 +152,7 @@ export interface CreateRoleDto {
* *
* @example "admin" * @example "admin"
*/ */
slug: string; code: string;
/** /**
* *
* @example "一段很长的描述" * @example "一段很长的描述"
@ -175,7 +175,7 @@ export interface UpdateRoleDto {
* *
* @example "admin" * @example "admin"
*/ */
slug?: string; code?: string;
/** /**
* *
* @example "一段很长的描述" * @example "一段很长的描述"
@ -310,7 +310,7 @@ export interface File {
mimetype: string; mimetype: string;
/** /**
* *
* @example "/upload/2021/10/01/xxx.jpg" * @example "/upload/2021-10-01/xxx.jpg"
*/ */
path: string; path: string;
/** /**
@ -323,6 +323,11 @@ export interface File {
* @example ".jpg" * @example ".jpg"
*/ */
extension: string; extension: string;
/**
* ID
* @example 0
*/
categoryId: number;
/** /**
* ID * ID
* @example 1 * @example 1
@ -357,12 +362,110 @@ export interface UpdateFileDto {
* *
* @example "头像.jpg" * @example "头像.jpg"
*/ */
name: string; name?: string;
/** /**
* *
* @example "一段很长的描述" * @example "一段很长的描述"
*/ */
description?: string; description?: string;
/**
* ID
* @example 1
*/
categoryId?: number;
}
export interface CreateFileCategoryDto {
/**
*
* @example "风景"
*/
name: string;
/**
*
* @example "view"
*/
code: string;
/**
*
* @example "这是一段很长的描述"
*/
description?: string;
/**
* ID
* @example 0
*/
parentId?: number;
}
export interface FileCategory {
/**
*
* @example "风景"
*/
name: string;
/**
*
* @example "view"
*/
code: string;
/**
*
* @example "这是一段很长的描述"
*/
description?: string;
/** 父级ID */
parentId?: number;
/**
* ID
* @example 1
*/
id: number;
/**
*
* @format date-time
* @example "2022-01-01 10:10:10"
*/
createdAt: string;
/**
*
* @example "绝弹"
*/
createdBy: string;
/**
*
* @format date-time
* @example "2022-01-02 11:11:11"
*/
updatedAt: string;
/**
*
* @example "绝弹"
*/
updatedBy: string;
}
export interface UpdateFileCategoryDto {
/**
*
* @example "风景"
*/
name?: string;
/**
*
* @example "view"
*/
code?: string;
/**
*
* @example "这是一段很长的描述"
*/
description?: string;
/**
* ID
* @example 0
*/
parentId?: number;
} }
export interface CreatePostDto { export interface CreatePostDto {
@ -985,6 +1088,75 @@ export interface GetLoginLogsParams {
createdFrom?: string; createdFrom?: string;
} }
export interface GetFilesParams {
/**
*
* @example "风景"
*/
name?: string;
/**
* ID
* @example 1
*/
categoryId?: number;
/**
*
* @default "id:desc"
* @pattern /^(\w+:\w+,)*\w+:\w+$/
* @example "id:desc"
*/
sort?: string;
/**
*
* @min 1
* @example 1
*/
page?: number;
/**
*
* @min 0
* @example 10
*/
size?: number;
/**
*
* @example "2020-02-02 02:02:02"
*/
createdFrom?: string;
}
export interface GetFileCategorysParams {
/**
*
* @example "风景"
*/
name?: string;
/**
*
* @default "id:desc"
* @pattern /^(\w+:\w+,)*\w+:\w+$/
* @example "id:desc"
*/
sort?: string;
/**
*
* @min 1
* @example 1
*/
page?: number;
/**
*
* @min 0
* @example 10
*/
size?: number;
/**
*
* @example "2020-02-02 02:02:02"
*/
createdFrom?: string;
}
export interface GetPostsParams { export interface GetPostsParams {
/** /**
* *
@ -1685,10 +1857,84 @@ export namespace File {
*/ */
export namespace GetFiles { export namespace GetFiles {
export type RequestParams = {}; export type RequestParams = {};
export type RequestQuery = {}; export type RequestQuery = {
/**
*
* @example "风景"
*/
name?: string;
/**
* ID
* @example 1
*/
categoryId?: number;
/**
*
* @default "id:desc"
* @pattern /^(\w+:\w+,)*\w+:\w+$/
* @example "id:desc"
*/
sort?: string;
/**
*
* @min 1
* @example 1
*/
page?: number;
/**
*
* @min 0
* @example 10
*/
size?: number;
/**
*
* @example "2020-02-02 02:02:02"
*/
createdFrom?: string;
};
export type RequestBody = never; export type RequestBody = never;
export type RequestHeaders = {}; export type RequestHeaders = {};
export type ResponseBody = Response; export type ResponseBody = {
/**
*
* @format int32
* @example 2000
*/
code: number;
/**
*
* @example "请求成功"
*/
message: string;
data?: object;
};
}
/**
* @description
* @tags file
* @name DelFiles
* @request DELETE:/api/v1/file
*/
export namespace DelFiles {
export type RequestParams = {};
export type RequestQuery = {};
export type RequestBody = string[];
export type RequestHeaders = {};
export type ResponseBody = {
/**
*
* @format int32
* @example 2000
*/
code: number;
/**
*
* @example "请求成功"
*/
message: string;
data?: object;
};
} }
/** /**
* @description * @description
@ -1731,20 +1977,7 @@ export namespace File {
export type RequestQuery = {}; export type RequestQuery = {};
export type RequestBody = UpdateFileDto; export type RequestBody = UpdateFileDto;
export type RequestHeaders = {}; export type RequestHeaders = {};
export type ResponseBody = { export type ResponseBody = Response;
/**
*
* @format int32
* @example 2000
*/
code: number;
/**
*
* @example "请求成功"
*/
message: string;
data?: string;
};
} }
/** /**
* @description * @description
@ -1791,6 +2024,149 @@ export namespace File {
} }
} }
export namespace FileCategory {
/**
* @description
* @tags fileCategory
* @name AddFileCategory
* @request POST:/api/v1/fileCategorys
*/
export namespace AddFileCategory {
export type RequestParams = {};
export type RequestQuery = {};
export type RequestBody = CreateFileCategoryDto;
export type RequestHeaders = {};
export type ResponseBody = {
/**
*
* @format int32
* @example 2000
*/
code: number;
/**
*
* @example "请求成功"
*/
message: string;
data?: number;
};
}
/**
* @description
* @tags fileCategory
* @name GetFileCategorys
* @request GET:/api/v1/fileCategorys
*/
export namespace GetFileCategorys {
export type RequestParams = {};
export type RequestQuery = {
/**
*
* @example "风景"
*/
name?: string;
/**
*
* @default "id:desc"
* @pattern /^(\w+:\w+,)*\w+:\w+$/
* @example "id:desc"
*/
sort?: string;
/**
*
* @min 1
* @example 1
*/
page?: number;
/**
*
* @min 0
* @example 10
*/
size?: number;
/**
*
* @example "2020-02-02 02:02:02"
*/
createdFrom?: string;
};
export type RequestBody = never;
export type RequestHeaders = {};
export type ResponseBody = {
/**
*
* @format int32
* @example 2000
*/
code: number;
/**
*
* @example "请求成功"
*/
message: string;
data?: FileCategory[];
};
}
/**
* @description
* @tags fileCategory
* @name GetFileCategory
* @request GET:/api/v1/fileCategorys/{id}
*/
export namespace GetFileCategory {
export type RequestParams = {
id: number;
};
export type RequestQuery = {};
export type RequestBody = never;
export type RequestHeaders = {};
export type ResponseBody = {
/**
*
* @format int32
* @example 2000
*/
code: number;
/**
*
* @example "请求成功"
*/
message: string;
data?: FileCategory;
};
}
/**
* @description
* @tags fileCategory
* @name SetFileCategory
* @request PATCH:/api/v1/fileCategorys/{id}
*/
export namespace SetFileCategory {
export type RequestParams = {
id: number;
};
export type RequestQuery = {};
export type RequestBody = UpdateFileCategoryDto;
export type RequestHeaders = {};
export type ResponseBody = Response;
}
/**
* @description
* @tags fileCategory
* @name DelFileCategory
* @request DELETE:/api/v1/fileCategorys/{id}
*/
export namespace DelFileCategory {
export type RequestParams = {
id: number;
};
export type RequestQuery = {};
export type RequestBody = never;
export type RequestHeaders = {};
export type ResponseBody = Response;
}
}
export namespace Post { export namespace Post {
/** /**
* @description * @description
@ -3183,10 +3559,61 @@ export class Api<SecurityDataType extends unknown> extends HttpClient<SecurityDa
* @name GetFiles * @name GetFiles
* @request GET:/api/v1/file * @request GET:/api/v1/file
*/ */
getFiles: (params: RequestParams = {}) => { getFiles: (query: GetFilesParams, params: RequestParams = {}) => {
return this.request<Response, any>({ return this.request<
{
/**
*
* @format int32
* @example 2000
*/
code: number;
/**
*
* @example "请求成功"
*/
message: string;
data?: object;
},
any
>({
path: `/api/v1/file`, path: `/api/v1/file`,
method: "GET", method: "GET",
query: query,
format: "json",
...params,
});
},
/**
*
*
* @tags file
* @name DelFiles
* @request DELETE:/api/v1/file
*/
delFiles: (data: string[], params: RequestParams = {}) => {
return this.request<
{
/**
*
* @format int32
* @example 2000
*/
code: number;
/**
*
* @example "请求成功"
*/
message: string;
data?: object;
},
any
>({
path: `/api/v1/file`,
method: "DELETE",
body: data,
type: ContentType.Json,
format: "json", format: "json",
...params, ...params,
}); });
@ -3232,23 +3659,7 @@ export class Api<SecurityDataType extends unknown> extends HttpClient<SecurityDa
* @request PATCH:/api/v1/file/{id} * @request PATCH:/api/v1/file/{id}
*/ */
setFile: (id: number, data: UpdateFileDto, params: RequestParams = {}) => { setFile: (id: number, data: UpdateFileDto, params: RequestParams = {}) => {
return this.request< return this.request<Response, any>({
{
/**
*
* @format int32
* @example 2000
*/
code: number;
/**
*
* @example "请求成功"
*/
message: string;
data?: string;
},
any
>({
path: `/api/v1/file/${id}`, path: `/api/v1/file/${id}`,
method: "PATCH", method: "PATCH",
body: data, body: data,
@ -3306,6 +3717,140 @@ export class Api<SecurityDataType extends unknown> extends HttpClient<SecurityDa
}); });
}, },
}; };
fileCategory = {
/**
*
*
* @tags fileCategory
* @name AddFileCategory
* @request POST:/api/v1/fileCategorys
*/
addFileCategory: (data: CreateFileCategoryDto, params: RequestParams = {}) => {
return this.request<
{
/**
*
* @format int32
* @example 2000
*/
code: number;
/**
*
* @example "请求成功"
*/
message: string;
data?: number;
},
any
>({
path: `/api/v1/fileCategorys`,
method: "POST",
body: data,
type: ContentType.Json,
format: "json",
...params,
});
},
/**
*
*
* @tags fileCategory
* @name GetFileCategorys
* @request GET:/api/v1/fileCategorys
*/
getFileCategorys: (query: GetFileCategorysParams, params: RequestParams = {}) => {
return this.request<
{
/**
*
* @format int32
* @example 2000
*/
code: number;
/**
*
* @example "请求成功"
*/
message: string;
data?: FileCategory[];
},
any
>({
path: `/api/v1/fileCategorys`,
method: "GET",
query: query,
format: "json",
...params,
});
},
/**
*
*
* @tags fileCategory
* @name GetFileCategory
* @request GET:/api/v1/fileCategorys/{id}
*/
getFileCategory: (id: number, params: RequestParams = {}) => {
return this.request<
{
/**
*
* @format int32
* @example 2000
*/
code: number;
/**
*
* @example "请求成功"
*/
message: string;
data?: FileCategory;
},
any
>({
path: `/api/v1/fileCategorys/${id}`,
method: "GET",
format: "json",
...params,
});
},
/**
*
*
* @tags fileCategory
* @name SetFileCategory
* @request PATCH:/api/v1/fileCategorys/{id}
*/
setFileCategory: (id: number, data: UpdateFileCategoryDto, params: RequestParams = {}) => {
return this.request<Response, any>({
path: `/api/v1/fileCategorys/${id}`,
method: "PATCH",
body: data,
type: ContentType.Json,
format: "json",
...params,
});
},
/**
*
*
* @tags fileCategory
* @name DelFileCategory
* @request DELETE:/api/v1/fileCategorys/{id}
*/
delFileCategory: (id: number, params: RequestParams = {}) => {
return this.request<Response, any>({
path: `/api/v1/fileCategorys/${id}`,
method: "DELETE",
format: "json",
...params,
});
},
};
post = { post = {
/** /**
* *

View File

@ -75,3 +75,32 @@ export const config = {
return data; return data;
}, },
}; };
export function initOptions({ item, model }: any, key = "options") {
if (Array.isArray(item.options)) {
item.nodeProps[key] = item.options;
}
if (item.options && typeof item.options === "object") {
const { value, source } = item.options;
item._updateOptions = async () => {};
}
if (typeof item.options === "function") {
const loadData = item.options;
item.nodeProps[key] = reactive([]);
item._updateOptions = async () => {
let data = await loadData({ item, model });
if (Array.isArray(data?.data?.data)) {
data = data.data.data.map((i: any) => ({
...i,
label: i.name,
value: i.id,
}));
}
if (Array.isArray(data)) {
item.nodeProps[key].splice(0);
item.nodeProps[key].push(...data);
}
};
item._updateOptions();
}
}

View File

@ -16,27 +16,7 @@ import {
TimePicker, TimePicker,
TreeSelect, TreeSelect,
} from "@arco-design/web-vue"; } from "@arco-design/web-vue";
import { initOptions } from "./form-config";
const initOptions = ({ item, model }: any, key = "options") => {
if (Array.isArray(item.options)) {
item.nodeProps[key] = item.options;
}
if (typeof item.options === "function") {
const loadData = item.options;
item.nodeProps[key] = reactive([]);
item._updateOptions = async () => {
let data = await loadData({ item, model });
if (Array.isArray(data?.data?.data)) {
data = data.data.data.map((i: any) => ({ label: i.name, value: i.id }));
}
if (Array.isArray(data)) {
item.nodeProps[key].splice(0);
item.nodeProps[key].push(...data);
}
};
item._updateOptions();
}
};
/** /**
* *
@ -114,7 +94,10 @@ export const nodeMap = {
placeholder: "请选择", placeholder: "请选择",
allowClear: true, allowClear: true,
allowSearch: true, allowSearch: true,
options: [{}], options: [],
onChange(value) {
value;
},
} as InstanceType<typeof TreeSelect>["$props"], } as InstanceType<typeof TreeSelect>["$props"],
init: (arg: any) => initOptions(arg, "data"), init: (arg: any) => initOptions(arg, "data"),
}, },

View File

@ -1,11 +1,6 @@
import { import { TableColumnData as BaseColumn, TableData as BaseData, Table as BaseTable } from "@arco-design/web-vue";
TableColumnData as BaseColumn,
TableData as BaseData,
Table as BaseTable,
Message,
} from "@arco-design/web-vue";
import { merge } from "lodash-es"; import { merge } from "lodash-es";
import { PropType, computed, defineComponent, reactive, ref, watch } from "vue"; import { PropType, computed, defineComponent, reactive, ref } from "vue";
import { Form, FormInstance, FormModal, FormModalInstance, FormModalProps, FormProps } from "../form"; import { Form, FormInstance, FormModal, FormModalInstance, FormModalProps, FormProps } from "../form";
import { config } from "./table.config"; import { config } from "./table.config";
@ -72,7 +67,7 @@ export const Table = defineComponent({
}, },
setup(props) { setup(props) {
const loading = ref(false); const loading = ref(false);
const tableRef = ref<InstanceType<typeof BaseTable>>() const tableRef = ref<InstanceType<typeof BaseTable>>();
const searchRef = ref<FormInstance>(); const searchRef = ref<FormInstance>();
const createRef = ref<FormModalInstance>(); const createRef = ref<FormModalInstance>();
const modifyRef = ref<FormModalInstance>(); const modifyRef = ref<FormModalInstance>();
@ -81,10 +76,16 @@ export const Table = defineComponent({
const reloadData = () => loadData({ current: 1, pageSize: 10 }); const reloadData = () => loadData({ current: 1, pageSize: 10 });
const openModifyModal = (data: any) => modifyRef.value?.open(data); const openModifyModal = (data: any) => modifyRef.value?.open(data);
/**
*
* @param pagination
*/
const loadData = async (pagination: Partial<any> = {}) => { const loadData = async (pagination: Partial<any> = {}) => {
const merged = { ...props.pagination, ...pagination }; const merged = { ...props.pagination, ...pagination };
const paging = { page: merged.current, size: merged.pageSize }; const paging = { page: merged.current, size: merged.pageSize };
const model = searchRef.value?.getModel() ?? {}; const model = searchRef.value?.getModel() ?? {};
// 本地加载
if (Array.isArray(props.data)) { if (Array.isArray(props.data)) {
const filters = Object.entries(model); const filters = Object.entries(model);
const data = props.data.filter((item) => { const data = props.data.filter((item) => {
@ -99,6 +100,8 @@ export const Table = defineComponent({
props.pagination.total = renderData.value.length; props.pagination.total = renderData.value.length;
props.pagination.current = 1; props.pagination.current = 1;
} }
// 远程加载
if (typeof props.data === "function") { if (typeof props.data === "function") {
try { try {
loading.value = true; loading.value = true;
@ -107,30 +110,19 @@ export const Table = defineComponent({
renderData.value = data; renderData.value = data;
props.pagination.total = total; props.pagination.total = total;
props.pagination.current = paging.page; props.pagination.current = paging.page;
} catch (error) {
const message = config.getApiErrorMessage(error);
if (message) {
Message.error(`提示:${message}`);
}
} finally { } finally {
loading.value = false; loading.value = false;
} }
} }
}; };
watch( watchEffect(() => {
() => props.data, if (Array.isArray(props.data)) {
(data) => { renderData.value = props.data;
if (Array.isArray(data)) { props.pagination.total = props.data.length;
renderData.value = data; props.pagination.current = 1;
props.pagination.total = data.length;
props.pagination.current = 1;
}
},
{
immediate: true,
} }
); });
onMounted(() => { onMounted(() => {
loadData(); loadData();

View File

@ -248,12 +248,39 @@ export const useAniTable = (options: UseTableOptions): TableReturnType => {
props, props,
tableRef, tableRef,
refresh: () => tableRef.value?.reloadData(), refresh: () => tableRef.value?.reloadData(),
getTableInstance() {
return tableRef.value?.tableRef;
},
getSearchInstance() {
return tableRef.value?.searchRef;
},
getCreateInstance() {
return tableRef.value?.createRef;
},
/**
*
*/
getCreateFormInstance() {
return this.getCreateInstance()?.formRef;
},
/**
*
*/
getModifyInstance() {
return tableRef.value?.modifyRef;
},
/**
*
*/
getModifyFormInstance() {
return this.getModifyInstance()?.formRef;
},
}; };
const aniTable = defineComponent({ const aniTable = defineComponent({
name: "AniTableWrapper", name: "AniTableWrapper",
setup() { setup(p, { slots }) {
const onRef = (el: TableInstance) => (tableRef.value = el); const onRef = (el: TableInstance) => (tableRef.value = el);
return () => <Table ref={onRef} {...props}></Table>; return () => <Table ref={onRef} {...props}>{slots}</Table>;
}, },
}); });
return [aniTable, context]; return [aniTable, context];

View File

@ -1,8 +1,8 @@
<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"></a-input-search> <a-input-search allow-clear placeholder="文件分类" class="mb-2"></a-input-search>
<a-button @click="onCreateRow"> <a-button @click="formCtx.open">
<template #icon> <template #icon>
<i class="icon-park-outline-add"></i> <i class="icon-park-outline-add"></i>
</template> </template>
@ -13,15 +13,15 @@
<ul class="pl-0 mt-0"> <ul class="pl-0 mt-0">
<li <li
v-for="item in list" v-for="item in list"
:key="item.id" :key="item.code"
:class="{ active: item.id === current?.id }"
class="group flex items-center justify-between gap-1 h-8 rounded mb-2 pl-3 hover:bg-gray-100 cursor-pointer" class="group flex items-center justify-between gap-1 h-8 rounded mb-2 pl-3 hover:bg-gray-100 cursor-pointer"
> >
<div> <div class="flex-1 h-full flex items-center gap-2 overflow-hidden" @click="emit('change', item)">
<i class="icon-park-outline-folder-close align-[-2px]"></i> <i class="icon-park-outline-folder-close align-[-2px]"></i>
{{ item.title }} <span class="flex-1 truncate">{{ item.name }}</span>
<span class="text-xs text-gray-500"> ({{ item.count }}) </span>
</div> </div>
<div> <div class="">
<a-dropdown> <a-dropdown>
<a-button size="small" type="text"> <a-button size="small" type="text">
<template #icon> <template #icon>
@ -29,13 +29,13 @@
</template> </template>
</a-button> </a-button>
<template #content> <template #content>
<a-doption @click="onModifyRow(item)"> <a-doption @click="formCtx.open(item)">
<template #icon> <template #icon>
<i class="icon-park-outline-edit"></i> <i class="icon-park-outline-edit"></i>
</template> </template>
修改 修改
</a-doption> </a-doption>
<a-doption class="!text-red-500" @click="onDeleteRow"> <a-doption class="!text-red-500" @click="onDeleteRow(item)">
<template #icon> <template #icon>
<i class="icon-park-outline-delete"></i> <i class="icon-park-outline-delete"></i>
</template> </template>
@ -51,86 +51,78 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
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 { PropType } from "vue";
const data = [ defineProps({
{ current: {
id: 1, type: Object as PropType<FileCategory>,
title: "生活笔记",
count: 23,
}, },
{ });
id: 2,
title: "微信头像",
count: 52,
},
{
id: 3,
title: "文章封面",
count: 19,
},
{
id: 4,
title: "山水诗画",
count: 81,
},
{
id: 5,
title: "虾米沙雕",
count: 12,
},
];
const list = ref(data); const emit = defineEmits(["change"]);
const list = ref<FileCategory[]>([]);
const onModifyRow = (row: any) => { const updateFileCategories = async () => {
formCtx.props.title = "修改分组"; const res = await api.fileCategory.getFileCategorys({ size: 0 });
formCtx.open(row); list.value = res.data.data ?? [];
list.value.unshift({ id: undefined, name: '全部' } as any)
list.value.length && emit("change", list.value[0]);
}; };
const onCreateRow = () => { onMounted(updateFileCategories);
formCtx.props.title = "新建分组";
formCtx.open();
};
const onDeleteRow = async () => { const onDeleteRow = async (row: FileCategory) => {
await delConfirm(); await delConfirm();
const res = await api.dictType.delDictType(row.id);
Message.success(res.data.message);
}; };
const [formModal, formCtx] = useAniFormModal({ const [formModal, formCtx] = useAniFormModal({
title: "修改分组", title: ({ model }) => (!model.id ? "新建分类" : "修改分类"),
trigger: false, trigger: false,
modalProps: { modalProps: {
width: 432, width: 580,
}, },
model: { model: {
id: undefined, id: undefined,
}, },
items: [ items: [
{ {
field: "title", field: "name",
label: "分名称", label: "分名称",
type: "input", type: "input",
}, },
{
field: "code",
label: "分类编码",
type: "input",
},
{
field: "description",
label: "备注",
type: "textarea",
},
], ],
submit: async ({ model }) => { submit: async ({ model }) => {
let res;
if (model.id) { if (model.id) {
const item = list.value.find((i) => i.id === model.id); res = await api.fileCategory.setFileCategory(model.id, model);
if (item) {
item.title = model.title;
}
} else { } else {
const ids = list.value.map((i) => i.id); res = await api.fileCategory.addFileCategory(model);
const maxId = Math.max.apply(null, ids);
list.value.push({
id: maxId,
title: model.title,
count: 0,
});
} }
updateFileCategories();
return res;
}, },
}); });
</script> </script>
<style lang="less" scoped></style> <style lang="less" scoped>
.active {
color: rgb(var(--primary-6));
background-color: rgb(var(--primary-1));
}
</style>

View File

@ -36,11 +36,14 @@
<ul v-if="fileList.length" class="h-[424px] overflow-hidden p-0 m-0"> <ul v-if="fileList.length" class="h-[424px] overflow-hidden p-0 m-0">
<a-scrollbar outer-class="h-full overflow-hidden" class="h-full overflow-auto pr-[20px] divide-y"> <a-scrollbar outer-class="h-full overflow-hidden" class="h-full overflow-auto pr-[20px] divide-y">
<li v-for="item in fileList" :key="item.uid" class="flex items-center gap-2 py-3"> <li v-for="item in fileList" :key="item.uid" class="flex items-center gap-2 py-3">
<div class="text-2xl">
<i :class="getIconnameByMimetype(item.file?.type ?? 'video')"></i>
</div>
<div class="flex-1 overflow-hidden"> <div class="flex-1 overflow-hidden">
<div class="truncate text-slate-900"> <div class="truncate text-slate-900">
{{ item.name }} {{ item.name }}
</div> </div>
<div class="flex items-center justify-between gap-2 text-gray-400 mb-[-4px] mt-1"> <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>
@ -48,10 +51,8 @@
<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">
@ -99,6 +100,7 @@ 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 { getIconnameByMimetype } from "./util";
const emit = defineEmits<{ const emit = defineEmits<{
(event: "success", item: FileItem): void; (event: "success", item: FileItem): void;
@ -121,6 +123,9 @@ const fileMap = reactive<
> >
>(new Map()); >(new Map());
/**
* 统计信息
*/
const stat = computed(() => { const stat = computed(() => {
const result = { const result = {
initCount: 0, initCount: 0,

View File

@ -0,0 +1,20 @@
const typeIconMap: Record<string, string> = {
video: "icon-park-outline-video-file",
audio: "icon-park-outline-audio-file",
image: "icon-park-outline-file-pdf",
text: "icon-park-outline-file-txt",
application: "icon-park-outline-file-code",
unknown: "icon-park-outline-file-question",
};
const imageIconMap: Record<string, string> = {
jpg: "icon-park-outline-file-jpg",
png: "icon-park-outline-file-jpg",
};
function getIconnameByMimetype(mimetype: string) {
const [type, subtype] = mimetype.split("/");
return typeIconMap[type] || typeIconMap.unknown;
}
export { getIconnameByMimetype };

View File

@ -1,16 +1,16 @@
<template> <template>
<BreadPage> <BreadPage>
<div class="overflow-hidden h-full grid grid-cols-[auto_1fr] gap-4"> <div class="overflow-hidden h-full grid grid-cols-[auto_1fr] gap-4">
<ani-group></ani-group> <ani-group :current="current" @change="onCategoryChange"></ani-group>
<div> <div>
<Table v-bind="table"> <file-table>
<template #action> <template #action>
<ani-upload></ani-upload> <ani-upload></ani-upload>
<a-button type="outline" status="danger" :disabled="!selected.length" @click="onDeleteMany"> <a-button type="primary" status="danger" :disabled="!selected.length" @click="onDeleteMany">
批量删除 批量删除
</a-button> </a-button>
</template> </template>
</Table> </file-table>
<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>
@ -18,16 +18,18 @@
</template> </template>
<script setup lang="tsx"> <script setup lang="tsx">
import { api } from "@/api"; import { FileCategory, api } from "@/api";
import { Table, createColumn, updateColumn, useTable } from "@/components"; import { createColumn, updateColumn, useAniTable } from "@/components";
import { delConfirm } from "@/utils"; import { delConfirm } from "@/utils";
import numeral from "numeral"; import numeral from "numeral";
import AniGroup from "./components/group.vue"; import AniGroup from "./components/group.vue";
import AniUpload from "./components/upload.vue"; import AniUpload from "./components/upload.vue";
import { Message } from "@arco-design/web-vue";
const visible = ref(false); const visible = ref(false);
const image = ref(""); const image = ref("");
const selected = ref<number[]>([]); const selected = ref<number[]>([]);
const current = ref<FileCategory>();
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");
@ -39,6 +41,18 @@ const preview = (record: any) => {
const onDeleteMany = async () => { const onDeleteMany = async () => {
await delConfirm(); 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;
}
current.value = category;
fileCtx.refresh();
}; };
const getIcon = (mimetype: string) => { const getIcon = (mimetype: string) => {
@ -57,9 +71,9 @@ const getIcon = (mimetype: string) => {
return "icon-file-iunknown"; return "icon-file-iunknown";
}; };
const table = useTable({ const [fileTable, fileCtx] = useAniTable({
data: async (model, paging) => { data: async (model, paging) => {
return api.file.getFiles(); return api.file.getFiles({ ...model, ...paging });
}, },
tableProps: { tableProps: {
rowSelection: { rowSelection: {
@ -121,6 +135,9 @@ const table = useTable({
], ],
search: { search: {
button: false, button: false,
model: {
categoryId: undefined,
},
items: [ items: [
{ {
field: "name", field: "name",
@ -143,6 +160,12 @@ const table = useTable({
width: 580, width: 580,
}, },
items: [ items: [
{
field: "categoryId",
label: "分类",
type: "select",
options: () => api.fileCategory.getFileCategorys({ size: 0 }),
},
{ {
field: "name", field: "name",
label: "名称", label: "名称",

View File

@ -37,16 +37,14 @@ const [dictTable, dict] = useAniTable({
{ {
title: "字典项", title: "字典项",
dataIndex: "name", dataIndex: "name",
render: ({ record }) => { render: ({ record }) => (
return ( <div>
<div> <div>
<div> {record.name}<span class="text-gray-400 ml-2 text-xs">{record.code}</span>
<span class="text-gray-900">{record.name}</span>: {record.code}
</div>
<div class="text-gray-400 text-xs">{record.description}</div>
</div> </div>
); <div class="text-gray-400 text-xs">{record.description}</div>
}, </div>
),
}, },
createColumn, createColumn,
updateColumn, updateColumn,
@ -88,7 +86,7 @@ const [dictTable, dict] = useAniTable({
], ],
}, },
create: { create: {
title: '新增字典', title: "新增字典",
model: { model: {
typeId: undefined, typeId: undefined,
}, },

View File

@ -13,6 +13,7 @@ import { api } from "@/api";
import { createColumn, updateColumn, useAniTable } from "@/components"; import { createColumn, updateColumn, useAniTable } from "@/components";
import { MenuTypes, MenuType } from "@/constants/menu"; import { MenuTypes, MenuType } from "@/constants/menu";
import { flatedMenus } from "@/router"; import { flatedMenus } from "@/router";
import { listToTree } from "@/utils/listToTree";
const menuArr = flatedMenus.map((i) => ({ label: i.title, value: i.id })); const menuArr = flatedMenus.map((i) => ({ label: i.title, value: i.id }));
@ -24,23 +25,21 @@ const toggleExpand = () => {
const [menuTable, menu] = useAniTable({ const [menuTable, menu] = useAniTable({
data: (search, paging) => { data: (search, paging) => {
return api.menu.getMenus({ ...search, ...paging, tree: true }); return api.menu.getMenus({ ...search, ...paging, tree: true, size: 0 });
}, },
tableProps: { tableProps: {
defaultExpandAllRows: true, defaultExpandAllRows: true,
}, },
columns: [ columns: [
{ {
title: () => { title: () => (
return ( <span>
<span> 菜单名称
菜单名称 <a-link class="ml-1 select-none" onClick={toggleExpand}>
<a-link class="ml-1 select-none" onClick={toggleExpand}> {expanded.value ? "收起全部" : "展开全部"}
{expanded.value ? "收起全部" : "展开全部"} </a-link>
</a-link> </span>
</span> ),
);
},
dataIndex: "name", dataIndex: "name",
render({ record }) { render({ record }) {
let id = ""; let id = "";
@ -125,19 +124,24 @@ const [menuTable, menu] = useAniTable({
label: "父级", label: "父级",
type: "treeSelect", type: "treeSelect",
async options() { async options() {
const res = await api.menu.getMenus({ size: 0, tree: true }); const res = await api.menu.getMenus({ size: 0 });
const data = res.data.data; const data = res.data.data?.filter((i) => i.type !== MenuType.BUTTON) ?? [];
for (const item of data) {
const type = MenuTypes.fmt(item.type);
// @ts-ignore
item.icon = () => `[${type}]`;
}
const list = listToTree(data);
return [ return [
{ {
id: 0, id: 0,
name: "主类目", name: "主类目",
children: data, children: list,
}, },
]; ];
}, },
nodeProps: { nodeProps: {
fieldNames: { fieldNames: {
icon: undefined,
key: "id", key: "id",
title: "name", title: "name",
}, },