feat: 优化上传组件
自动部署 / build (push) Successful in 1m40s Details

master
luoer 2023-11-23 17:10:25 +08:00
parent e5bf0bfb8b
commit 261f2490ec
11 changed files with 306 additions and 254 deletions

View File

@ -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>
), ),

View File

@ -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>

View File

@ -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 ?? {};

View File

@ -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();

View File

@ -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"
} }

View File

@ -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">
{ {

View File

@ -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>

View File

@ -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,
},
}, },
], ],
}, },

View File

@ -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 }) => {

View File

@ -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: '删除',

View File

@ -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;