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 {
|
||||
return {
|
||||
title: '更新用户',
|
||||
title: '更新',
|
||||
dataIndex: 'createdAt',
|
||||
width: 190,
|
||||
width: 180,
|
||||
render: ({ record }) => (
|
||||
<div class="flex flex-col overflow-hidden">
|
||||
<span>{record.updatedBy ?? '无'}</span>
|
||||
<span class="text-gray-400 text-xs truncate">
|
||||
{dayjs(record.updatedAt).format()}
|
||||
<span class="text-gray-400 text-xs truncate" title={record.updatedAt}>
|
||||
更新于 {dayjs(record.updatedAt).fromNow()}
|
||||
</span>
|
||||
</div>
|
||||
),
|
||||
|
|
@ -20,14 +20,14 @@ export function useUpdateColumn(extra: TableColumn = {}): TableColumn {
|
|||
|
||||
export function useCreateColumn(extra: TableColumn = {}): TableColumn {
|
||||
return {
|
||||
title: '创建用户',
|
||||
title: '作者',
|
||||
dataIndex: 'createdAt',
|
||||
width: 190,
|
||||
width: 180,
|
||||
render: ({ record }) => (
|
||||
<div class="flex flex-col overflow-hidden">
|
||||
<span>{record.createdBy ?? '无'}</span>
|
||||
<span class="text-gray-400 text-xs truncate">
|
||||
{dayjs(record.createdAt).format()}
|
||||
<span class="text-gray-400 text-xs truncate" title={record.createdAt}>
|
||||
创建于 {dayjs(record.createdAt).fromNow()}
|
||||
</span>
|
||||
</div>
|
||||
),
|
||||
|
|
|
|||
|
|
@ -153,7 +153,7 @@ function useTableButtonColumn(column: TableButtonColumn & TableColumnData) {
|
|||
}
|
||||
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)}>
|
||||
{item.text}
|
||||
</Link>
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { delConfirm, delOptions } from '@/utils';
|
|||
import { AnTableContext } from '../components/Table';
|
||||
import { AnTablePlugin } from '../hooks/useTablePlugin';
|
||||
import { Message } from '@arco-design/web-vue';
|
||||
import { defaultsDeep } from 'lodash-es';
|
||||
|
||||
export function useRowDelete(): AnTablePlugin {
|
||||
let ctx: AnTableContext;
|
||||
|
|
@ -19,6 +20,11 @@ export function useRowDelete(): AnTablePlugin {
|
|||
if (!btn) {
|
||||
continue;
|
||||
}
|
||||
defaultsDeep(btn, {
|
||||
buttonProps: {
|
||||
status: 'danger',
|
||||
},
|
||||
});
|
||||
const onClick = btn.onClick;
|
||||
btn.onClick = async props => {
|
||||
let confirm = btn.confirm ?? {};
|
||||
|
|
|
|||
|
|
@ -6,15 +6,15 @@
|
|||
上传
|
||||
</a-button>
|
||||
<a-modal
|
||||
v-model:visible="visible"
|
||||
title="上传文件"
|
||||
title-align="start"
|
||||
v-model:visible="visible"
|
||||
:width="940"
|
||||
:mask-closable="false"
|
||||
:on-before-cancel="onBeforeCancel"
|
||||
@close="onClose"
|
||||
>
|
||||
<div class="mb-2 flex items-center gap-4">
|
||||
<div class="flex items-center gap-4 py-0">
|
||||
<a-upload
|
||||
ref="uploadRef"
|
||||
class="upload"
|
||||
|
|
@ -40,43 +40,55 @@
|
|||
|
||||
<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">
|
||||
<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">
|
||||
<i :class="getIcon(item.file?.type ?? 'video')"></i>
|
||||
</div>
|
||||
<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 }}
|
||||
</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') }}
|
||||
</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>
|
||||
<span class="text-xs text-gray-400 ml-2">{{ numeral(item.file?.size).format('0 b') }}</span>
|
||||
</div>
|
||||
<div v-show="item.status !== 'done'">
|
||||
<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 === '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>
|
||||
</li>
|
||||
</a-scrollbar>
|
||||
|
|
@ -132,9 +144,20 @@ const fileMap = reactive<
|
|||
>
|
||||
>(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 result = {
|
||||
initCount: 0,
|
||||
|
|
@ -151,17 +174,10 @@ const stat = computed(() => {
|
|||
return result;
|
||||
});
|
||||
|
||||
/**
|
||||
* 开始上传
|
||||
*/
|
||||
const startUpload = () => {
|
||||
uploadRef.value?.submit();
|
||||
};
|
||||
|
||||
/**
|
||||
* 中止上传
|
||||
* @param item 文件
|
||||
*/
|
||||
const pauseItem = (item: FileItem) => {
|
||||
uploadRef.value?.abort(item);
|
||||
const file = fileMap.get(item.uid);
|
||||
|
|
@ -170,10 +186,6 @@ const pauseItem = (item: FileItem) => {
|
|||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 移除文件
|
||||
* @param item 文件
|
||||
*/
|
||||
const removeItem = (item: FileItem) => {
|
||||
const index = fileList.value.findIndex(i => i.uid === item.uid);
|
||||
if (index > -1) {
|
||||
|
|
@ -181,17 +193,10 @@ const removeItem = (item: FileItem) => {
|
|||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 重新上传
|
||||
* @param item 文件
|
||||
*/
|
||||
const retryItem = (item: FileItem) => {
|
||||
uploadRef.value?.submit(item);
|
||||
};
|
||||
|
||||
/**
|
||||
* 清空已上传
|
||||
*/
|
||||
const clearUploaded = async () => {
|
||||
if (stat.value.doneCount !== fileList.value.length) {
|
||||
await delConfirm('当前有未上传完成的文件,是否继续清空?');
|
||||
|
|
@ -199,18 +204,10 @@ const clearUploaded = async () => {
|
|||
fileList.value = [];
|
||||
};
|
||||
|
||||
/**
|
||||
* 上传成功后处理
|
||||
* @param item 文件
|
||||
*/
|
||||
const onUploadSuccess = (item: FileItem) => {
|
||||
emit('success', item);
|
||||
};
|
||||
|
||||
/**
|
||||
* 上传失败后处理
|
||||
* @param item 文件
|
||||
*/
|
||||
const onUploadError = (item: FileItem) => {
|
||||
const file = fileMap.get(item.uid);
|
||||
if (file) {
|
||||
|
|
@ -218,9 +215,6 @@ const onUploadError = (item: FileItem) => {
|
|||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 关闭前检测
|
||||
*/
|
||||
const onBeforeCancel = () => {
|
||||
if (fileList.value.some(i => i.status === 'uploading')) {
|
||||
Message.warning('提示:文件上传中,请稍后再试!');
|
||||
|
|
@ -229,19 +223,12 @@ const onBeforeCancel = () => {
|
|||
return true;
|
||||
};
|
||||
|
||||
/**
|
||||
* 关闭后处理
|
||||
*/
|
||||
const onClose = () => {
|
||||
fileMap.clear();
|
||||
fileList.value = [];
|
||||
emit('close', stat.value.doneCount);
|
||||
};
|
||||
|
||||
/**
|
||||
* 自定义上传逻辑
|
||||
* @param option
|
||||
*/
|
||||
const upload = (option: RequestOption) => {
|
||||
const { fileItem, onError, onProgress, onSuccess } = option;
|
||||
const source = axios.CancelToken.source();
|
||||
|
|
@ -257,7 +244,6 @@ const upload = (option: RequestOption) => {
|
|||
}
|
||||
const item = fileMap.get(fileItem.uid)!;
|
||||
const startTime = Date.now();
|
||||
const up = async () => {
|
||||
const data = { file: fileItem.file as any };
|
||||
const params: RequestParams = {
|
||||
onUploadProgress(e) {
|
||||
|
|
@ -277,6 +263,7 @@ const upload = (option: RequestOption) => {
|
|||
},
|
||||
cancelToken: source.token,
|
||||
};
|
||||
const up = async () => {
|
||||
try {
|
||||
const res = await api.file.addFile(data, params);
|
||||
const currentTime = Date.now();
|
||||
|
|
|
|||
|
|
@ -18,11 +18,12 @@
|
|||
|
||||
<script setup lang="tsx">
|
||||
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 numeral from 'numeral';
|
||||
import AnGroup from './components/AnGroup.vue';
|
||||
import AnUpload from './components/AnUpload.vue';
|
||||
import { Message } from '@arco-design/web-vue';
|
||||
|
||||
const visible = ref(false);
|
||||
const current = ref<FileCategory>();
|
||||
|
|
@ -45,17 +46,23 @@ const onCategoryChange = (category: FileCategory) => {
|
|||
tableRef.value?.refresh();
|
||||
};
|
||||
|
||||
const copyLink = (record: Recordable) => {
|
||||
window.navigator.clipboard.writeText(record.path);
|
||||
Message.success(`提示:已复制 ${record.name} 的地址!`);
|
||||
};
|
||||
|
||||
const {
|
||||
component: MaterialTable,
|
||||
tableRef,
|
||||
props,
|
||||
} = useTable({
|
||||
plugins: [useTableDelete()],
|
||||
columns: [
|
||||
{
|
||||
title: '文件名称',
|
||||
dataIndex: 'name',
|
||||
render: ({ record }) => (
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="group flex items-center gap-2">
|
||||
<div class="w-8 flex justify-center">
|
||||
{record.mimetype.startsWith('image') ? (
|
||||
<a-avatar size={26} shape="square">
|
||||
|
|
@ -66,16 +73,24 @@ const {
|
|||
)}
|
||||
</div>
|
||||
<div class="flex flex-col overflow-hidden">
|
||||
<span class="flex items-center gap-2">
|
||||
<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)}
|
||||
>
|
||||
{record.name}
|
||||
</span>
|
||||
<span class="text-gray-400 text-xs truncate">
|
||||
{numeral(record.size).format('0 b')}
|
||||
<span class="ml-2">{record.category?.name}</span>
|
||||
<span class="hidden group-hover:inline text-xs text-gray-400 ml-0" title="复制地址" onClick={() => copyLink(record)}>
|
||||
<i class=" icon-park-outline-copy hover:text-gray-700 cursor-pointer"></i>
|
||||
</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>
|
||||
),
|
||||
|
|
@ -103,6 +118,9 @@ const {
|
|||
onClick: props => {
|
||||
return api.file.delFile(props.record.id);
|
||||
},
|
||||
buttonProps: {
|
||||
status: 'danger'
|
||||
}
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
@ -158,7 +176,7 @@ const {
|
|||
<route lang="json">
|
||||
{
|
||||
"meta": {
|
||||
"sort": 10305,
|
||||
"sort": 10300,
|
||||
"title": "素材管理",
|
||||
"icon": "icon-park-outline-movie-board"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,64 +1,103 @@
|
|||
<template>
|
||||
<div>
|
||||
<div class="bg-white px-4 pt-2">
|
||||
<bread-crumb></bread-crumb>
|
||||
<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>
|
||||
<BreadPage>
|
||||
<CategoryTable />
|
||||
</BreadPage>
|
||||
</template>
|
||||
|
||||
<script setup lang="tsx" name="PostPage">
|
||||
<script setup lang="tsx">
|
||||
import { api } from '@/api';
|
||||
import { useCreateColumn, useTable, useUpdateColumn } from '@/components/AnTable';
|
||||
import { listToTree } from '@/utils/listToTree';
|
||||
|
||||
const { component: CategoryTable } = useTable({
|
||||
columns: [
|
||||
{
|
||||
title: '名称',
|
||||
dataIndex: 'title',
|
||||
width: 240,
|
||||
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>
|
||||
|
||||
<style lang="less">
|
||||
.export-form {
|
||||
.arco-form-item-content {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<route lang="json">
|
||||
{
|
||||
"meta": {
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ const { component: DictTable, tableRef } = useTable({
|
|||
<div>
|
||||
<div>
|
||||
{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 class="text-gray-400 text-xs">{record.description}</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,52 +1,64 @@
|
|||
<template>
|
||||
<BreadPage>
|
||||
<Table v-bind="table">
|
||||
<LoginLogTable>
|
||||
<template #action>
|
||||
<a-button type="primary" @click="visible = true">添加</a-button>
|
||||
<ani-editor v-model:visible="visible"></ani-editor>
|
||||
</template>
|
||||
</Table>
|
||||
</LoginLogTable>
|
||||
</BreadPage>
|
||||
</template>
|
||||
|
||||
<script setup lang="tsx">
|
||||
import { api } from "@/api";
|
||||
import { Table, useTable } from "@/components";
|
||||
import { Editor as aniEditor } from "@/components/editor";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
defineOptions({ name: "SystemLoglPage" })
|
||||
import { api } from '@/api';
|
||||
import { useTable } from '@/components/AnTable';
|
||||
import { Editor as aniEditor } from '@/components/editor';
|
||||
import { TableColumnData } from '@arco-design/web-vue';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
defineOptions({ name: 'SystemLoglPage' });
|
||||
const visible = ref(false);
|
||||
const table = useTable({
|
||||
data: async (model, paging) => {
|
||||
return api.log.getLoginLogs({ ...model, ...paging });
|
||||
|
||||
const useTwoRowsColumn = (tkey: string, bkey: string): TableColumnData['render'] => {
|
||||
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: [
|
||||
{
|
||||
title: "登陆账号",
|
||||
dataIndex: "nickname",
|
||||
width: 200,
|
||||
title: '登陆账号',
|
||||
dataIndex: 'nickname',
|
||||
width: 140,
|
||||
render({ record }) {
|
||||
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 class="text-gray-400 text-xs truncate">{dayjs(record.createdAt).format()}</span>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "操作描述",
|
||||
dataIndex: "description",
|
||||
title: '操作描述',
|
||||
dataIndex: 'description',
|
||||
render: ({ record: { status, description } }) => {
|
||||
return (
|
||||
<span>
|
||||
<span
|
||||
class={
|
||||
status === null || status
|
||||
? "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-green-500 icon-park-outline-check-one mr-2'
|
||||
: 'text-base text-red-500 icon-park-outline-close-one mr-2'
|
||||
}
|
||||
></span>
|
||||
{description}
|
||||
|
|
@ -55,74 +67,69 @@ const table = useTable({
|
|||
},
|
||||
},
|
||||
{
|
||||
title: "登陆地址",
|
||||
dataIndex: "ip",
|
||||
title: '登陆地址',
|
||||
dataIndex: 'ip',
|
||||
width: 200,
|
||||
render({ record }) {
|
||||
return (
|
||||
<div class="flex flex-col overflow-hidden">
|
||||
<span>{record.addr || "未知"}</span>
|
||||
<span class="text-gray-400 text-xs truncate">{record.ip}</span>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
render: useTwoRowsColumn('addr', 'ip'),
|
||||
},
|
||||
{
|
||||
title: "操作系统",
|
||||
dataIndex: "os",
|
||||
title: '操作系统',
|
||||
dataIndex: 'os',
|
||||
width: 200,
|
||||
render({ record }) {
|
||||
const [os, version] = record.os.split(" ");
|
||||
const [os, version] = record.os.split(' ');
|
||||
return (
|
||||
<div class="flex flex-col overflow-hidden">
|
||||
<span>{os || "未知"}</span>
|
||||
<span>{os || '未知'}</span>
|
||||
<span class="text-gray-400 text-xs truncate">{version}</span>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "浏览器",
|
||||
dataIndex: "browser",
|
||||
title: '浏览器',
|
||||
dataIndex: 'browser',
|
||||
width: 200,
|
||||
render({ record }) {
|
||||
const [browser, version] = record.browser.split(" ");
|
||||
const [browser, version] = record.browser.split(' ');
|
||||
return (
|
||||
<div class="flex flex-col overflow-hidden">
|
||||
<span>{browser || "未知"}</span>
|
||||
<span>{browser || '未知'}</span>
|
||||
<span class="text-gray-400 text-xs truncate">v{version}</span>
|
||||
</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: {
|
||||
button: true,
|
||||
items: [
|
||||
{
|
||||
field: "[startDate, endDate]",
|
||||
label: "登陆账号",
|
||||
type: "dateRange",
|
||||
required: false,
|
||||
nodeProps: {
|
||||
field: '[startDate, endDate]',
|
||||
label: '登陆账号',
|
||||
setter: 'dateRange',
|
||||
setterProps: {
|
||||
placeholder: ['开始时间', '结束时间'],
|
||||
showTime: true,
|
||||
timePickerProps: { defaultValue: ["23:59:59", "00:00:00"] },
|
||||
},
|
||||
itemProps: {
|
||||
hideLabel: true,
|
||||
timePickerProps: { defaultValue: ['23:59:59', '00:00:00'] },
|
||||
},
|
||||
},
|
||||
{
|
||||
field: "nickname",
|
||||
label: "登陆账号",
|
||||
type: "input",
|
||||
required: false,
|
||||
nodeProps: {
|
||||
placeholder: "请输入登陆账号",
|
||||
},
|
||||
itemProps: {
|
||||
hideLabel: true,
|
||||
},
|
||||
field: 'nickname',
|
||||
label: '登陆账号',
|
||||
setter: 'input',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import { api } from "@/api";
|
||||
import { useAniFormModal } from "@/components";
|
||||
import { api } from '@/api';
|
||||
import { useAniFormModal } from '@/components';
|
||||
|
||||
export const usePassworModal = () => {
|
||||
return useAniFormModal({
|
||||
title: "重置密码",
|
||||
title: '重置密码',
|
||||
trigger: false,
|
||||
modalProps: {
|
||||
width: 432,
|
||||
|
|
@ -14,13 +14,9 @@ export const usePassworModal = () => {
|
|||
},
|
||||
items: [
|
||||
{
|
||||
field: "password",
|
||||
label: ({ model }) => (
|
||||
<span>
|
||||
设置 {model.nickname} 的新密码:
|
||||
</span>
|
||||
),
|
||||
type: "input",
|
||||
field: 'password',
|
||||
label: ({ model }) => `${model.nickname} 的新密码:`,
|
||||
type: 'input',
|
||||
},
|
||||
],
|
||||
submit: async ({ model }) => {
|
||||
|
|
|
|||
|
|
@ -22,22 +22,29 @@ const { component: UserTable } = useTable({
|
|||
title: '用户昵称',
|
||||
dataIndex: 'username',
|
||||
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">
|
||||
{record.avatar?.startsWith('/') ? <img src={record.avatar} alt="" /> : record.nickname?.[0]}
|
||||
</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 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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '用户邮箱',
|
||||
dataIndex: 'email',
|
||||
width: 200,
|
||||
},
|
||||
useCreateColumn(),
|
||||
useUpdateColumn(),
|
||||
{
|
||||
|
|
@ -46,15 +53,15 @@ const { component: UserTable } = useTable({
|
|||
width: 200,
|
||||
buttons: [
|
||||
{
|
||||
type: 'modify',
|
||||
text: '修改',
|
||||
},
|
||||
{
|
||||
text: '设置密码',
|
||||
text: '重置密码',
|
||||
onClick({ record }) {
|
||||
passCtx.open(record);
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'modify',
|
||||
text: '修改',
|
||||
},
|
||||
{
|
||||
type: 'delete',
|
||||
text: '删除',
|
||||
|
|
|
|||
|
|
@ -15,10 +15,6 @@ body {
|
|||
border: none;
|
||||
}
|
||||
|
||||
div.arco-divider-horizontal {
|
||||
border-color: var(--color-neutral-2);
|
||||
}
|
||||
|
||||
li.arco-dropdown-option {
|
||||
line-height: 32px;
|
||||
width: calc(100% - 8px);
|
||||
|
|
@ -36,10 +32,6 @@ body {
|
|||
border-radius: var(--border-radius-small);
|
||||
overflow: hidden;
|
||||
}
|
||||
.arco-modal-header {
|
||||
// background: var(--color-fill-3);
|
||||
// border-bottom: none;
|
||||
}
|
||||
.arco-modal-footer {
|
||||
padding-top: 0;
|
||||
border-top: none;
|
||||
|
|
|
|||
Loading…
Reference in New Issue