feat: 优化上传组件
自动部署 / build (push) Successful in 1m40s
Details
自动部署 / build (push) Successful in 1m40s
Details
parent
e5bf0bfb8b
commit
261f2490ec
|
|
@ -3,14 +3,14 @@ import { TableColumn } from '../hooks/useTableColumn';
|
||||||
|
|
||||||
export function useUpdateColumn(extra: TableColumn = {}): TableColumn {
|
export function useUpdateColumn(extra: TableColumn = {}): TableColumn {
|
||||||
return {
|
return {
|
||||||
title: '更新用户',
|
title: '更新',
|
||||||
dataIndex: 'createdAt',
|
dataIndex: 'createdAt',
|
||||||
width: 190,
|
width: 180,
|
||||||
render: ({ record }) => (
|
render: ({ record }) => (
|
||||||
<div class="flex flex-col overflow-hidden">
|
<div class="flex flex-col overflow-hidden">
|
||||||
<span>{record.updatedBy ?? '无'}</span>
|
<span>{record.updatedBy ?? '无'}</span>
|
||||||
<span class="text-gray-400 text-xs truncate">
|
<span class="text-gray-400 text-xs truncate" title={record.updatedAt}>
|
||||||
{dayjs(record.updatedAt).format()}
|
更新于 {dayjs(record.updatedAt).fromNow()}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
|
|
@ -20,14 +20,14 @@ export function useUpdateColumn(extra: TableColumn = {}): TableColumn {
|
||||||
|
|
||||||
export function useCreateColumn(extra: TableColumn = {}): TableColumn {
|
export function useCreateColumn(extra: TableColumn = {}): TableColumn {
|
||||||
return {
|
return {
|
||||||
title: '创建用户',
|
title: '作者',
|
||||||
dataIndex: 'createdAt',
|
dataIndex: 'createdAt',
|
||||||
width: 190,
|
width: 180,
|
||||||
render: ({ record }) => (
|
render: ({ record }) => (
|
||||||
<div class="flex flex-col overflow-hidden">
|
<div class="flex flex-col overflow-hidden">
|
||||||
<span>{record.createdBy ?? '无'}</span>
|
<span>{record.createdBy ?? '无'}</span>
|
||||||
<span class="text-gray-400 text-xs truncate">
|
<span class="text-gray-400 text-xs truncate" title={record.createdAt}>
|
||||||
{dayjs(record.createdAt).format()}
|
创建于 {dayjs(record.createdAt).fromNow()}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -153,7 +153,7 @@ function useTableButtonColumn(column: TableButtonColumn & TableColumnData) {
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{index !== 0 && <Divider direction="vertical" margin={4} />}
|
{index !== 0 && <Divider direction="vertical" margin={2} />}
|
||||||
<Link {...item.buttonProps} disabled={item.disable?.(props)} onClick={() => item.onClick?.(props)}>
|
<Link {...item.buttonProps} disabled={item.disable?.(props)} onClick={() => item.onClick?.(props)}>
|
||||||
{item.text}
|
{item.text}
|
||||||
</Link>
|
</Link>
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import { delConfirm, delOptions } from '@/utils';
|
||||||
import { AnTableContext } from '../components/Table';
|
import { AnTableContext } from '../components/Table';
|
||||||
import { AnTablePlugin } from '../hooks/useTablePlugin';
|
import { AnTablePlugin } from '../hooks/useTablePlugin';
|
||||||
import { Message } from '@arco-design/web-vue';
|
import { Message } from '@arco-design/web-vue';
|
||||||
|
import { defaultsDeep } from 'lodash-es';
|
||||||
|
|
||||||
export function useRowDelete(): AnTablePlugin {
|
export function useRowDelete(): AnTablePlugin {
|
||||||
let ctx: AnTableContext;
|
let ctx: AnTableContext;
|
||||||
|
|
@ -19,6 +20,11 @@ export function useRowDelete(): AnTablePlugin {
|
||||||
if (!btn) {
|
if (!btn) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
defaultsDeep(btn, {
|
||||||
|
buttonProps: {
|
||||||
|
status: 'danger',
|
||||||
|
},
|
||||||
|
});
|
||||||
const onClick = btn.onClick;
|
const onClick = btn.onClick;
|
||||||
btn.onClick = async props => {
|
btn.onClick = async props => {
|
||||||
let confirm = btn.confirm ?? {};
|
let confirm = btn.confirm ?? {};
|
||||||
|
|
|
||||||
|
|
@ -6,15 +6,15 @@
|
||||||
上传
|
上传
|
||||||
</a-button>
|
</a-button>
|
||||||
<a-modal
|
<a-modal
|
||||||
v-model:visible="visible"
|
|
||||||
title="上传文件"
|
title="上传文件"
|
||||||
title-align="start"
|
title-align="start"
|
||||||
|
v-model:visible="visible"
|
||||||
:width="940"
|
:width="940"
|
||||||
:mask-closable="false"
|
:mask-closable="false"
|
||||||
:on-before-cancel="onBeforeCancel"
|
:on-before-cancel="onBeforeCancel"
|
||||||
@close="onClose"
|
@close="onClose"
|
||||||
>
|
>
|
||||||
<div class="mb-2 flex items-center gap-4">
|
<div class="flex items-center gap-4 py-0">
|
||||||
<a-upload
|
<a-upload
|
||||||
ref="uploadRef"
|
ref="uploadRef"
|
||||||
class="upload"
|
class="upload"
|
||||||
|
|
@ -40,43 +40,55 @@
|
||||||
|
|
||||||
<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-4 py-3">
|
||||||
<div class="text-4xl rounded pr-0.5 flex justify-center">
|
<div class="text-4xl rounded pr-0.5 flex justify-center">
|
||||||
<i :class="getIcon(item.file?.type ?? 'video')"></i>
|
<i :class="getIcon(item.file?.type ?? 'video')"></i>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-1 overflow-hidden">
|
<div class="flex-1 overflow-hidden">
|
||||||
<div class="truncate text-slate-900">
|
<div class="h-8 truncate text-slate-900 flex justify-between items-center gap-2">
|
||||||
|
<div>
|
||||||
{{ item.name }}
|
{{ item.name }}
|
||||||
</div>
|
<span class="text-xs text-gray-400 ml-2">{{ numeral(item.file?.size).format('0 b') }}</span>
|
||||||
<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') }}
|
|
||||||
</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)
|
|
||||||
}}%
|
|
||||||
</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)
|
|
||||||
</span>
|
|
||||||
<span v-else="item.status === 'error'" class="text-red-500">
|
|
||||||
失败(原因:{{ fileMap.get(item.uid)?.error }})
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<a-progress :percent="Math.floor((item.percent || 0) * 100) / 100" :show-text="false"></a-progress>
|
|
||||||
</div>
|
</div>
|
||||||
<div v-show="item.status !== 'done'">
|
<div v-show="item.status !== 'done'">
|
||||||
<a-link v-show="item.status === 'uploading'" @click="pauseItem(item)">停止</a-link>
|
<a-link v-show="item.status === 'uploading'" @click="pauseItem(item)">停止</a-link>
|
||||||
<a-link v-show="item.status === 'error'" @click="retryItem(item)">重试</a-link>
|
<a-link v-show="item.status === 'error'" @click="retryItem(item)">重试</a-link>
|
||||||
<a-link v-show="item.status === 'init' || item.status === 'error'" @click="removeItem(item)">删除</a-link>
|
<a-link v-show="item.status === 'init' || item.status === 'error'" @click="removeItem(item)">
|
||||||
|
删除
|
||||||
|
</a-link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<a-progress :percent="formatProgress(item, true)" :show-text="false" class="block!"></a-progress>
|
||||||
|
<div class="flex items-center justify-between gap-2 text-gray-400 mt-1.5 text-xs">
|
||||||
|
<span class="text-xs">
|
||||||
|
<span v-if="item.status === 'init'">
|
||||||
|
<i class="icon-park-outline-hourglass-full"></i>
|
||||||
|
等待上传
|
||||||
|
</span>
|
||||||
|
<span v-else-if="item.status === 'uploading'" class="text-[rgb(var(--primary-6))]">
|
||||||
|
<i class="icon-park-outline-upload-one"></i>
|
||||||
|
正在上传
|
||||||
|
</span>
|
||||||
|
<span v-else-if="item.status === 'done'" class="text-[rgb(var(--success-6))]">
|
||||||
|
<i class="icon-park-outline-check"></i>
|
||||||
|
上传成功
|
||||||
|
</span>
|
||||||
|
<span v-else="item.status === 'error'" class="text-red-500">
|
||||||
|
<i class="icon-park-outline-close"></i>
|
||||||
|
上传失败
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
<span v-if="item.status === 'init'"> </span>
|
||||||
|
<span v-else-if="item.status === 'uploading'">
|
||||||
|
速度:{{ formatSpeed(item.uid) }}/s, 进度:{{ formatProgress(item) }} %
|
||||||
|
</span>
|
||||||
|
<span v-else-if="item.status === 'done'">
|
||||||
|
耗时:{{ fileMap.get(item.uid)?.cost || 0 }} 秒, 平均:{{ formatAspeed(item.uid) }}/s
|
||||||
|
</span>
|
||||||
|
<span v-else="item.status === 'error'"> 原因:{{ fileMap.get(item.uid)?.error }} </span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
</a-scrollbar>
|
</a-scrollbar>
|
||||||
|
|
@ -132,9 +144,20 @@ const fileMap = reactive<
|
||||||
>
|
>
|
||||||
>(new Map());
|
>(new Map());
|
||||||
|
|
||||||
/**
|
const formatProgress = (item: FileItem, small?: boolean) => {
|
||||||
* 统计信息
|
let percent = Math.floor((item.percent || 0) * 100);
|
||||||
*/
|
percent = percent < 100 ? percent : item.response ? percent : 99;
|
||||||
|
return small ? percent / 100 : percent;
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatSpeed = (uid: string) => {
|
||||||
|
return numeral(fileMap.get(uid)?.speed || 0).format('0 b');
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatAspeed = (uid: string) => {
|
||||||
|
return numeral(fileMap.get(uid)?.aspeed || 0).format('0 b');
|
||||||
|
};
|
||||||
|
|
||||||
const stat = computed(() => {
|
const stat = computed(() => {
|
||||||
const result = {
|
const result = {
|
||||||
initCount: 0,
|
initCount: 0,
|
||||||
|
|
@ -151,17 +174,10 @@ const stat = computed(() => {
|
||||||
return result;
|
return result;
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
|
||||||
* 开始上传
|
|
||||||
*/
|
|
||||||
const startUpload = () => {
|
const startUpload = () => {
|
||||||
uploadRef.value?.submit();
|
uploadRef.value?.submit();
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* 中止上传
|
|
||||||
* @param item 文件
|
|
||||||
*/
|
|
||||||
const pauseItem = (item: FileItem) => {
|
const pauseItem = (item: FileItem) => {
|
||||||
uploadRef.value?.abort(item);
|
uploadRef.value?.abort(item);
|
||||||
const file = fileMap.get(item.uid);
|
const file = fileMap.get(item.uid);
|
||||||
|
|
@ -170,10 +186,6 @@ const pauseItem = (item: FileItem) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* 移除文件
|
|
||||||
* @param item 文件
|
|
||||||
*/
|
|
||||||
const removeItem = (item: FileItem) => {
|
const removeItem = (item: FileItem) => {
|
||||||
const index = fileList.value.findIndex(i => i.uid === item.uid);
|
const index = fileList.value.findIndex(i => i.uid === item.uid);
|
||||||
if (index > -1) {
|
if (index > -1) {
|
||||||
|
|
@ -181,17 +193,10 @@ const removeItem = (item: FileItem) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* 重新上传
|
|
||||||
* @param item 文件
|
|
||||||
*/
|
|
||||||
const retryItem = (item: FileItem) => {
|
const retryItem = (item: FileItem) => {
|
||||||
uploadRef.value?.submit(item);
|
uploadRef.value?.submit(item);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* 清空已上传
|
|
||||||
*/
|
|
||||||
const clearUploaded = async () => {
|
const clearUploaded = async () => {
|
||||||
if (stat.value.doneCount !== fileList.value.length) {
|
if (stat.value.doneCount !== fileList.value.length) {
|
||||||
await delConfirm('当前有未上传完成的文件,是否继续清空?');
|
await delConfirm('当前有未上传完成的文件,是否继续清空?');
|
||||||
|
|
@ -199,18 +204,10 @@ const clearUploaded = async () => {
|
||||||
fileList.value = [];
|
fileList.value = [];
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* 上传成功后处理
|
|
||||||
* @param item 文件
|
|
||||||
*/
|
|
||||||
const onUploadSuccess = (item: FileItem) => {
|
const onUploadSuccess = (item: FileItem) => {
|
||||||
emit('success', item);
|
emit('success', item);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* 上传失败后处理
|
|
||||||
* @param item 文件
|
|
||||||
*/
|
|
||||||
const onUploadError = (item: FileItem) => {
|
const onUploadError = (item: FileItem) => {
|
||||||
const file = fileMap.get(item.uid);
|
const file = fileMap.get(item.uid);
|
||||||
if (file) {
|
if (file) {
|
||||||
|
|
@ -218,9 +215,6 @@ const onUploadError = (item: FileItem) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* 关闭前检测
|
|
||||||
*/
|
|
||||||
const onBeforeCancel = () => {
|
const onBeforeCancel = () => {
|
||||||
if (fileList.value.some(i => i.status === 'uploading')) {
|
if (fileList.value.some(i => i.status === 'uploading')) {
|
||||||
Message.warning('提示:文件上传中,请稍后再试!');
|
Message.warning('提示:文件上传中,请稍后再试!');
|
||||||
|
|
@ -229,19 +223,12 @@ const onBeforeCancel = () => {
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* 关闭后处理
|
|
||||||
*/
|
|
||||||
const onClose = () => {
|
const onClose = () => {
|
||||||
fileMap.clear();
|
fileMap.clear();
|
||||||
fileList.value = [];
|
fileList.value = [];
|
||||||
emit('close', stat.value.doneCount);
|
emit('close', stat.value.doneCount);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* 自定义上传逻辑
|
|
||||||
* @param option
|
|
||||||
*/
|
|
||||||
const upload = (option: RequestOption) => {
|
const upload = (option: RequestOption) => {
|
||||||
const { fileItem, onError, onProgress, onSuccess } = option;
|
const { fileItem, onError, onProgress, onSuccess } = option;
|
||||||
const source = axios.CancelToken.source();
|
const source = axios.CancelToken.source();
|
||||||
|
|
@ -257,7 +244,6 @@ const upload = (option: RequestOption) => {
|
||||||
}
|
}
|
||||||
const item = fileMap.get(fileItem.uid)!;
|
const item = fileMap.get(fileItem.uid)!;
|
||||||
const startTime = Date.now();
|
const startTime = Date.now();
|
||||||
const up = async () => {
|
|
||||||
const data = { file: fileItem.file as any };
|
const data = { file: fileItem.file as any };
|
||||||
const params: RequestParams = {
|
const params: RequestParams = {
|
||||||
onUploadProgress(e) {
|
onUploadProgress(e) {
|
||||||
|
|
@ -277,6 +263,7 @@ const upload = (option: RequestOption) => {
|
||||||
},
|
},
|
||||||
cancelToken: source.token,
|
cancelToken: source.token,
|
||||||
};
|
};
|
||||||
|
const up = async () => {
|
||||||
try {
|
try {
|
||||||
const res = await api.file.addFile(data, params);
|
const res = await api.file.addFile(data, params);
|
||||||
const currentTime = Date.now();
|
const currentTime = Date.now();
|
||||||
|
|
|
||||||
|
|
@ -18,11 +18,12 @@
|
||||||
|
|
||||||
<script setup lang="tsx">
|
<script setup lang="tsx">
|
||||||
import { FileCategory, api } from '@/api';
|
import { FileCategory, api } from '@/api';
|
||||||
import { useCreateColumn, useTable, useUpdateColumn } from '@/components/AnTable';
|
import { useCreateColumn, useTable, useTableDelete, useUpdateColumn } from '@/components/AnTable';
|
||||||
import { getIcon } from './components/util';
|
import { getIcon } from './components/util';
|
||||||
import numeral from 'numeral';
|
import numeral from 'numeral';
|
||||||
import AnGroup from './components/AnGroup.vue';
|
import AnGroup from './components/AnGroup.vue';
|
||||||
import AnUpload from './components/AnUpload.vue';
|
import AnUpload from './components/AnUpload.vue';
|
||||||
|
import { Message } from '@arco-design/web-vue';
|
||||||
|
|
||||||
const visible = ref(false);
|
const visible = ref(false);
|
||||||
const current = ref<FileCategory>();
|
const current = ref<FileCategory>();
|
||||||
|
|
@ -45,17 +46,23 @@ const onCategoryChange = (category: FileCategory) => {
|
||||||
tableRef.value?.refresh();
|
tableRef.value?.refresh();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const copyLink = (record: Recordable) => {
|
||||||
|
window.navigator.clipboard.writeText(record.path);
|
||||||
|
Message.success(`提示:已复制 ${record.name} 的地址!`);
|
||||||
|
};
|
||||||
|
|
||||||
const {
|
const {
|
||||||
component: MaterialTable,
|
component: MaterialTable,
|
||||||
tableRef,
|
tableRef,
|
||||||
props,
|
props,
|
||||||
} = useTable({
|
} = useTable({
|
||||||
|
plugins: [useTableDelete()],
|
||||||
columns: [
|
columns: [
|
||||||
{
|
{
|
||||||
title: '文件名称',
|
title: '文件名称',
|
||||||
dataIndex: 'name',
|
dataIndex: 'name',
|
||||||
render: ({ record }) => (
|
render: ({ record }) => (
|
||||||
<div class="flex items-center gap-2">
|
<div class="group flex items-center gap-2">
|
||||||
<div class="w-8 flex justify-center">
|
<div class="w-8 flex justify-center">
|
||||||
{record.mimetype.startsWith('image') ? (
|
{record.mimetype.startsWith('image') ? (
|
||||||
<a-avatar size={26} shape="square">
|
<a-avatar size={26} shape="square">
|
||||||
|
|
@ -66,16 +73,24 @@ const {
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col overflow-hidden">
|
<div class="flex flex-col overflow-hidden">
|
||||||
|
<span class="flex items-center gap-2">
|
||||||
<span
|
<span
|
||||||
class="hover:text-brand-500 hover:decoration-underline underline-offset-2 cursor-pointer"
|
class="truncate hover:text-brand-500 hover:decoration-underline underline-offset-2 cursor-pointer"
|
||||||
onClick={() => preview(record)}
|
onClick={() => preview(record)}
|
||||||
>
|
>
|
||||||
{record.name}
|
{record.name}
|
||||||
</span>
|
</span>
|
||||||
<span class="text-gray-400 text-xs truncate">
|
<span class="hidden group-hover:inline text-xs text-gray-400 ml-0" title="复制地址" onClick={() => copyLink(record)}>
|
||||||
{numeral(record.size).format('0 b')}
|
<i class=" icon-park-outline-copy hover:text-gray-700 cursor-pointer"></i>
|
||||||
<span class="ml-2">{record.category?.name}</span>
|
|
||||||
</span>
|
</span>
|
||||||
|
</span>
|
||||||
|
<div class="h-5 inline-flex items-center text-xs text-gray-400 space-x-4">
|
||||||
|
<span>
|
||||||
|
<i class="icon-park-outline-folder-close mr-1"></i>
|
||||||
|
{record.category || '默认分类'}
|
||||||
|
</span>
|
||||||
|
<span>{numeral(record.size).format('0 b')}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
|
|
@ -103,6 +118,9 @@ const {
|
||||||
onClick: props => {
|
onClick: props => {
|
||||||
return api.file.delFile(props.record.id);
|
return api.file.delFile(props.record.id);
|
||||||
},
|
},
|
||||||
|
buttonProps: {
|
||||||
|
status: 'danger'
|
||||||
|
}
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
@ -158,7 +176,7 @@ const {
|
||||||
<route lang="json">
|
<route lang="json">
|
||||||
{
|
{
|
||||||
"meta": {
|
"meta": {
|
||||||
"sort": 10305,
|
"sort": 10300,
|
||||||
"title": "素材管理",
|
"title": "素材管理",
|
||||||
"icon": "icon-park-outline-movie-board"
|
"icon": "icon-park-outline-movie-board"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,63 +1,102 @@
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<BreadPage>
|
||||||
<div class="bg-white px-4 pt-2">
|
<CategoryTable />
|
||||||
<bread-crumb></bread-crumb>
|
</BreadPage>
|
||||||
<div class="flex justify-between items-end gap-4 bg-white px-1 py-3">
|
|
||||||
<div>
|
|
||||||
<div class="text-lg font-semibold">新增文章</div>
|
|
||||||
<div class="text-gray-400 mt-1.5">新增的文章需审核才能展现</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<a-button class="mr-2">保存为草稿</a-button>
|
|
||||||
<a-button type="primary">保存发布</a-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex gap-4">
|
|
||||||
<div class="flex-1 bg-white p-4">
|
|
||||||
<a-form :model="{}" layout="vertical">
|
|
||||||
<a-form-item label="标题">
|
|
||||||
<a-input placeholder="请输入标题" :max-length="120" :show-word-limit="true"></a-input>
|
|
||||||
</a-form-item>
|
|
||||||
<a-form-item label="文章内容">
|
|
||||||
<a-textarea placeholder="说点啥" :max-length="1000" :show-word-limit="true"></a-textarea>
|
|
||||||
</a-form-item>
|
|
||||||
</a-form>
|
|
||||||
</div>
|
|
||||||
<div class="w-64 bg-white p-4">
|
|
||||||
<a-form :model="{}" layout="vertical">
|
|
||||||
<a-form-item label="别名">
|
|
||||||
<a-input placeholder="请输入"></a-input>
|
|
||||||
<template #help>
|
|
||||||
用作URL的别名, 只能包含字母、数字、下划线和破折号
|
|
||||||
</template>
|
|
||||||
</a-form-item>
|
|
||||||
<a-form-item label="分类">
|
|
||||||
<a-checkbox-group direction="vertical">
|
|
||||||
<a-checkbox>开发工具</a-checkbox>
|
|
||||||
<a-checkbox>日常记录</a-checkbox>
|
|
||||||
<a-checkbox>心得体验</a-checkbox>
|
|
||||||
</a-checkbox-group>
|
|
||||||
</a-form-item>
|
|
||||||
<a-form-item label="封面图">
|
|
||||||
<a-upload draggable></a-upload>
|
|
||||||
</a-form-item>
|
|
||||||
</a-form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="tsx" name="PostPage">
|
<script setup lang="tsx">
|
||||||
</script>
|
import { api } from '@/api';
|
||||||
|
import { useCreateColumn, useTable, useUpdateColumn } from '@/components/AnTable';
|
||||||
|
import { listToTree } from '@/utils/listToTree';
|
||||||
|
|
||||||
<style lang="less">
|
const { component: CategoryTable } = useTable({
|
||||||
.export-form {
|
columns: [
|
||||||
.arco-form-item-content {
|
{
|
||||||
display: block;
|
title: '名称',
|
||||||
}
|
dataIndex: 'title',
|
||||||
}
|
width: 240,
|
||||||
</style>
|
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',
|
||||||
|
},
|
||||||
|
useCreateColumn(),
|
||||||
|
useUpdateColumn(),
|
||||||
|
{
|
||||||
|
type: 'button',
|
||||||
|
title: '操作',
|
||||||
|
width: 120,
|
||||||
|
buttons: [
|
||||||
|
{
|
||||||
|
type: 'modify',
|
||||||
|
text: '修改',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'delete',
|
||||||
|
text: '删除',
|
||||||
|
onClick({ record }) {
|
||||||
|
return api.category.delCategory(record.id);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
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 } };
|
||||||
|
},
|
||||||
|
search: [
|
||||||
|
{
|
||||||
|
field: 'nickname',
|
||||||
|
label: '登陆账号',
|
||||||
|
setter: 'search',
|
||||||
|
enterable: true,
|
||||||
|
searchable: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
create: {
|
||||||
|
title: '添加分类',
|
||||||
|
width: 580,
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
field: 'title',
|
||||||
|
label: '分类名称',
|
||||||
|
setter: 'input',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'slug',
|
||||||
|
label: '分类别名',
|
||||||
|
setter: 'input',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'description',
|
||||||
|
label: '描述',
|
||||||
|
setter: 'textarea',
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
submit: model => {
|
||||||
|
return api.category.addCategory(model as any);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
modify: {
|
||||||
|
extend: true,
|
||||||
|
title: '修改分类',
|
||||||
|
submit: model => {
|
||||||
|
return api.category.setCategory(model.id, model as any);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
<route lang="json">
|
<route lang="json">
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,7 @@ const { component: DictTable, tableRef } = useTable({
|
||||||
<div>
|
<div>
|
||||||
<div>
|
<div>
|
||||||
{record.name}
|
{record.name}
|
||||||
<span class="text-gray-400 ml-2 text-xs">{record.code}</span>
|
<span class="text-gray-400 ml-2 text-xs">@{record.code}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-gray-400 text-xs">{record.description}</div>
|
<div class="text-gray-400 text-xs">{record.description}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,52 +1,64 @@
|
||||||
<template>
|
<template>
|
||||||
<BreadPage>
|
<BreadPage>
|
||||||
<Table v-bind="table">
|
<LoginLogTable>
|
||||||
<template #action>
|
<template #action>
|
||||||
<a-button type="primary" @click="visible = true">添加</a-button>
|
<a-button type="primary" @click="visible = true">添加</a-button>
|
||||||
<ani-editor v-model:visible="visible"></ani-editor>
|
<ani-editor v-model:visible="visible"></ani-editor>
|
||||||
</template>
|
</template>
|
||||||
</Table>
|
</LoginLogTable>
|
||||||
</BreadPage>
|
</BreadPage>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="tsx">
|
<script setup lang="tsx">
|
||||||
import { api } from "@/api";
|
import { api } from '@/api';
|
||||||
import { Table, useTable } from "@/components";
|
import { useTable } from '@/components/AnTable';
|
||||||
import { Editor as aniEditor } from "@/components/editor";
|
import { Editor as aniEditor } from '@/components/editor';
|
||||||
import dayjs from "dayjs";
|
import { TableColumnData } from '@arco-design/web-vue';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
defineOptions({ name: "SystemLoglPage" })
|
|
||||||
|
|
||||||
|
defineOptions({ name: 'SystemLoglPage' });
|
||||||
const visible = ref(false);
|
const visible = ref(false);
|
||||||
const table = useTable({
|
|
||||||
data: async (model, paging) => {
|
const useTwoRowsColumn = (tkey: string, bkey: string): TableColumnData['render'] => {
|
||||||
return api.log.getLoginLogs({ ...model, ...paging });
|
return ({ record }) => {
|
||||||
|
return (
|
||||||
|
<div class="flex flex-col overflow-hidden">
|
||||||
|
<span>{record[tkey] || '未知'}</span>
|
||||||
|
<span class="text-gray-400 text-xs truncate">{record[bkey]}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const { component: LoginLogTable } = useTable({
|
||||||
|
source: async model => {
|
||||||
|
return api.log.getLoginLogs(model);
|
||||||
},
|
},
|
||||||
columns: [
|
columns: [
|
||||||
{
|
{
|
||||||
title: "登陆账号",
|
title: '登陆账号',
|
||||||
dataIndex: "nickname",
|
dataIndex: 'nickname',
|
||||||
width: 200,
|
width: 140,
|
||||||
render({ record }) {
|
render({ record }) {
|
||||||
return (
|
return (
|
||||||
<div class="flex flex-col overflow-hidden">
|
<div class="overflow-hidden">
|
||||||
|
<i class="icon-park-outline-user mr-2"></i>
|
||||||
<span>{record.nickname}</span>
|
<span>{record.nickname}</span>
|
||||||
<span class="text-gray-400 text-xs truncate">{dayjs(record.createdAt).format()}</span>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "操作描述",
|
title: '操作描述',
|
||||||
dataIndex: "description",
|
dataIndex: 'description',
|
||||||
render: ({ record: { status, description } }) => {
|
render: ({ record: { status, description } }) => {
|
||||||
return (
|
return (
|
||||||
<span>
|
<span>
|
||||||
<span
|
<span
|
||||||
class={
|
class={
|
||||||
status === null || status
|
status === null || status
|
||||||
? "text-base text-green-500 icon-park-outline-check-one mr-2"
|
? 'text-base text-green-500 icon-park-outline-check-one mr-2'
|
||||||
: "text-base text-red-500 icon-park-outline-close-one mr-2"
|
: 'text-base text-red-500 icon-park-outline-close-one mr-2'
|
||||||
}
|
}
|
||||||
></span>
|
></span>
|
||||||
{description}
|
{description}
|
||||||
|
|
@ -55,74 +67,69 @@ const table = useTable({
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "登陆地址",
|
title: '登陆地址',
|
||||||
dataIndex: "ip",
|
dataIndex: 'ip',
|
||||||
width: 200,
|
width: 200,
|
||||||
render({ record }) {
|
render: useTwoRowsColumn('addr', 'ip'),
|
||||||
return (
|
|
||||||
<div class="flex flex-col overflow-hidden">
|
|
||||||
<span>{record.addr || "未知"}</span>
|
|
||||||
<span class="text-gray-400 text-xs truncate">{record.ip}</span>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "操作系统",
|
title: '操作系统',
|
||||||
dataIndex: "os",
|
dataIndex: 'os',
|
||||||
width: 200,
|
width: 200,
|
||||||
render({ record }) {
|
render({ record }) {
|
||||||
const [os, version] = record.os.split(" ");
|
const [os, version] = record.os.split(' ');
|
||||||
return (
|
return (
|
||||||
<div class="flex flex-col overflow-hidden">
|
<div class="flex flex-col overflow-hidden">
|
||||||
<span>{os || "未知"}</span>
|
<span>{os || '未知'}</span>
|
||||||
<span class="text-gray-400 text-xs truncate">{version}</span>
|
<span class="text-gray-400 text-xs truncate">{version}</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "浏览器",
|
title: '浏览器',
|
||||||
dataIndex: "browser",
|
dataIndex: 'browser',
|
||||||
width: 200,
|
width: 200,
|
||||||
render({ record }) {
|
render({ record }) {
|
||||||
const [browser, version] = record.browser.split(" ");
|
const [browser, version] = record.browser.split(' ');
|
||||||
return (
|
return (
|
||||||
<div class="flex flex-col overflow-hidden">
|
<div class="flex flex-col overflow-hidden">
|
||||||
<span>{browser || "未知"}</span>
|
<span>{browser || '未知'}</span>
|
||||||
<span class="text-gray-400 text-xs truncate">v{version}</span>
|
<span class="text-gray-400 text-xs truncate">v{version}</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: '登陆时间',
|
||||||
|
dataIndex: 'createAt',
|
||||||
|
width: 200,
|
||||||
|
render({ record }) {
|
||||||
|
return (
|
||||||
|
<div class="flex flex-col overflow-hidden">
|
||||||
|
<span>{dayjs(record.createdAt).fromNow()}</span>
|
||||||
|
<span class="text-gray-400 text-xs truncate">{dayjs(record.createdAt).format('YYYY-MM-DD HH:mm:ss')}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
search: {
|
search: {
|
||||||
button: true,
|
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
field: "[startDate, endDate]",
|
field: '[startDate, endDate]',
|
||||||
label: "登陆账号",
|
label: '登陆账号',
|
||||||
type: "dateRange",
|
setter: 'dateRange',
|
||||||
required: false,
|
setterProps: {
|
||||||
nodeProps: {
|
placeholder: ['开始时间', '结束时间'],
|
||||||
showTime: true,
|
showTime: true,
|
||||||
timePickerProps: { defaultValue: ["23:59:59", "00:00:00"] },
|
timePickerProps: { defaultValue: ['23:59:59', '00:00:00'] },
|
||||||
},
|
|
||||||
itemProps: {
|
|
||||||
hideLabel: true,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: "nickname",
|
field: 'nickname',
|
||||||
label: "登陆账号",
|
label: '登陆账号',
|
||||||
type: "input",
|
setter: 'input',
|
||||||
required: false,
|
|
||||||
nodeProps: {
|
|
||||||
placeholder: "请输入登陆账号",
|
|
||||||
},
|
|
||||||
itemProps: {
|
|
||||||
hideLabel: true,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
import { api } from "@/api";
|
import { api } from '@/api';
|
||||||
import { useAniFormModal } from "@/components";
|
import { useAniFormModal } from '@/components';
|
||||||
|
|
||||||
export const usePassworModal = () => {
|
export const usePassworModal = () => {
|
||||||
return useAniFormModal({
|
return useAniFormModal({
|
||||||
title: "重置密码",
|
title: '重置密码',
|
||||||
trigger: false,
|
trigger: false,
|
||||||
modalProps: {
|
modalProps: {
|
||||||
width: 432,
|
width: 432,
|
||||||
|
|
@ -14,13 +14,9 @@ export const usePassworModal = () => {
|
||||||
},
|
},
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
field: "password",
|
field: 'password',
|
||||||
label: ({ model }) => (
|
label: ({ model }) => `${model.nickname} 的新密码:`,
|
||||||
<span>
|
type: 'input',
|
||||||
设置 {model.nickname} 的新密码:
|
|
||||||
</span>
|
|
||||||
),
|
|
||||||
type: "input",
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
submit: async ({ model }) => {
|
submit: async ({ model }) => {
|
||||||
|
|
|
||||||
|
|
@ -22,22 +22,29 @@ const { component: UserTable } = useTable({
|
||||||
title: '用户昵称',
|
title: '用户昵称',
|
||||||
dataIndex: 'username',
|
dataIndex: 'username',
|
||||||
render: ({ record }) => (
|
render: ({ record }) => (
|
||||||
<div class="flex items-center">
|
<div class="flex items-center gap-4 w-full overflow-hidden">
|
||||||
<a-avatar size={32} class="!bg-brand-500">
|
<a-avatar size={32} class="!bg-brand-500">
|
||||||
{record.avatar?.startsWith('/') ? <img src={record.avatar} alt="" /> : record.nickname?.[0]}
|
{record.avatar?.startsWith('/') ? <img src={record.avatar} alt="" /> : record.nickname?.[0]}
|
||||||
</a-avatar>
|
</a-avatar>
|
||||||
<span class="ml-2 flex-1 flex flex-col overflow-hidden">
|
<div class="w-full flex-1 overflow-hidden">
|
||||||
|
<div>
|
||||||
<span>{record.nickname}</span>
|
<span>{record.nickname}</span>
|
||||||
<span class="text-gray-400 text-xs truncate">@{record.username}</span>
|
<span class="text-gray-400 text-xs truncate ml-2">@{record.username}</span>
|
||||||
|
</div>
|
||||||
|
<div class="w-full text-gray-400 space-x-4 text-xs">
|
||||||
|
<span>
|
||||||
|
<i class="icon-park-outline-mail mr-1 align-[-3px]"></i>
|
||||||
|
contact@juetan.cn
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
<i class="icon-park-outline-phone-telephone mr-1"></i>
|
||||||
|
1591234568
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
|
||||||
title: '用户邮箱',
|
|
||||||
dataIndex: 'email',
|
|
||||||
width: 200,
|
|
||||||
},
|
|
||||||
useCreateColumn(),
|
useCreateColumn(),
|
||||||
useUpdateColumn(),
|
useUpdateColumn(),
|
||||||
{
|
{
|
||||||
|
|
@ -46,15 +53,15 @@ const { component: UserTable } = useTable({
|
||||||
width: 200,
|
width: 200,
|
||||||
buttons: [
|
buttons: [
|
||||||
{
|
{
|
||||||
type: 'modify',
|
text: '重置密码',
|
||||||
text: '修改',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: '设置密码',
|
|
||||||
onClick({ record }) {
|
onClick({ record }) {
|
||||||
passCtx.open(record);
|
passCtx.open(record);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
type: 'modify',
|
||||||
|
text: '修改',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
type: 'delete',
|
type: 'delete',
|
||||||
text: '删除',
|
text: '删除',
|
||||||
|
|
|
||||||
|
|
@ -15,10 +15,6 @@ body {
|
||||||
border: none;
|
border: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
div.arco-divider-horizontal {
|
|
||||||
border-color: var(--color-neutral-2);
|
|
||||||
}
|
|
||||||
|
|
||||||
li.arco-dropdown-option {
|
li.arco-dropdown-option {
|
||||||
line-height: 32px;
|
line-height: 32px;
|
||||||
width: calc(100% - 8px);
|
width: calc(100% - 8px);
|
||||||
|
|
@ -36,10 +32,6 @@ body {
|
||||||
border-radius: var(--border-radius-small);
|
border-radius: var(--border-radius-small);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
.arco-modal-header {
|
|
||||||
// background: var(--color-fill-3);
|
|
||||||
// border-bottom: none;
|
|
||||||
}
|
|
||||||
.arco-modal-footer {
|
.arco-modal-footer {
|
||||||
padding-top: 0;
|
padding-top: 0;
|
||||||
border-top: none;
|
border-top: none;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue