feat: 优化布局
自动部署 / build (push) Successful in 3m13s Details

master
绝弹 2024-02-27 23:40:10 +08:00
parent 53ddf5fb20
commit 2a27f67b85
21 changed files with 70 additions and 1898 deletions

2
.env
View File

@ -2,7 +2,7 @@
# 应用配置
# =====================================================================================
# 网站标题
VITE_TITLE = 管理中心
VITE_TITLE = 绝弹管理中心
# 网站副标题
VITE_SUBTITLE = 绝弹管理中心
# 部署路径: 当为 ./ 时路由模式需为 hash

View File

@ -1,6 +1,6 @@
<template>
<div class="h-full overflow-hidden grid grid-rows-[auto_1fr]">
<div class="bg-white px-4 py-2 border-b border-gray-200">
<div class="bg-white px-4 py-2">
<div class="flex justify-between gap-4">
<BreadCrumb></BreadCrumb>
<div>

View File

@ -1,18 +1,18 @@
import { dayjs } from '@/libs/dayjs';
import { TableColumn } from '../hooks/useTableColumn';
import { Avatar } from '@arco-design/web-vue';
import { TableColumn } from '../hooks/useTableColumn';
export function useUpdateColumn(extra: TableColumn = {}): TableColumn {
return {
title: '修改',
title: '最近修改',
dataIndex: 'createdAt',
width: 180,
render: ({ record }) => (
<div class="flex items-center gap-2 overflow-hidden">
<span>
<Avatar size={24}>{record.updatedBy?.[0] ?? '无'}</Avatar>
<Avatar size={22}>{record.updatedBy?.substr(0,1) ?? '无'}</Avatar>
</span>
<span class="ttruncate" title={record.updatedAt}>
<span class="truncate" title={record.updatedAt}>
{dayjs(record.updatedAt).fromNow()}
</span>
</div>
@ -27,9 +27,9 @@ export function useCreateColumn(extra: TableColumn = {}): TableColumn {
dataIndex: 'createdAt',
width: 180,
render: ({ record }) => (
<div class="flex items-center gap-2 overflow-hidden">
<div class="flex direction-col items-center gap-2 overflow-hidden">
<span>
<Avatar size={24}>{record.createdBy?.[0] ?? '无'}</Avatar>
{record.createdBy ?? '无'}
</span>
<span class="text-gray-400 text-xs truncate" title={record.createdAt}>
{dayjs(record.createdAt).fromNow()}

View File

@ -16,14 +16,16 @@
<a-avatar :size="32">
<img :src="userStore.avatar || 'https://github.com/juetan.png'" :alt="userStore.nickname" />
</a-avatar>
<div class="leading-4 my-2">
<div class="leading-4 text-base my-2">
{{ userStore.nickname }}
<span class="text-xs text-gray-400">({{ userStore.username }})</span>
<div class="text-xs text-gray-400">管理员</div>
<a-tag color="red" size="small" >管理员</a-tag>
<div class="text-xs text-gray-400">
<span class="text-gray-400">@{{ userStore.username }}</span>
</div>
</div>
</div>
</a-doption>
<a-divider :margin="4"></a-divider>
<a-divider :margin="4" class="border-gray-100!"></a-divider>
<a-doption @click="open()">
<template #icon>
<i class="icon-park-outline-lock"></i>
@ -36,7 +38,7 @@
</template>
账号信息
</a-doption>
<a-divider :margin="4"></a-divider>
<!-- <a-divider :margin="4" class="border-gray-100!"></a-divider> -->
<a-doption @click="router.push('/user')">
<template #icon>
<i class="icon-park-outline-config"></i>
@ -49,7 +51,7 @@
</template>
关于
</a-doption>
<a-divider :margin="4"></a-divider>
<a-divider :margin="4" class="border-gray-100!"></a-divider>
<a-doption @click="logout">
<template #icon>
<i class="icon-park-outline-power"></i>
@ -63,7 +65,7 @@
<script setup lang="ts">
import { useFormModal } from '@/components/AnForm';
import { useUserStore } from '@/store/user';
import { delConfirm } from '@/utils';
import { delConfirm, sleep } from '@/utils';
import { Message } from '@arco-design/web-vue';
const userStore = useUserStore();
@ -74,10 +76,13 @@ const logout = async () => {
await delConfirm({
content: '退出后将跳转到登录页面,确定退出吗?',
okText: '确定退出',
});
async onBeforeOk() {
await sleep(2000);
userStore.clearUser();
Message.success('提示:已退出登陆!');
router.push({ path: '/login', query: { redirect: route.path } });
},
});
};
const { component: PasswordModal, open } = useFormModal({

View File

@ -13,7 +13,7 @@
</div>
<div class="flex items-center justify-center w-full overflow-hidden">
<div
class="login-box w-[960px] h-[560px] relative mx-4 grid md:grid-cols-2 rounded overflow-hidden border border-blue-100"
class="login-box w-[960px] h-[560px] relative mx-4 grid md:grid-cols-2 rounded-lg overflow-hidden border border-blue-100"
>
<div
class="login-left relative hidden md:block w-full h-full overflow-hidden bg-[rgb(var(--primary-6))] px-4"

View File

@ -5,16 +5,8 @@
</template>
上传
</a-button>
<a-modal
title="上传文件"
title-align="start"
v-model:visible="visible"
:width="960"
:mask-closable="false"
:on-before-cancel="onBeforeCancel"
@close="onClose"
>
<div class="flex items-center gap-4 py-0">
<a-modal class="an-upload" title="上传文件" title-align="start" v-model:visible="visible" :width="960" :mask-closable="false" :on-before-cancel="onBeforeCancel" @close="onClose">
<div class="flex items-center justify-between gap-4 py-0">
<a-upload
ref="uploadRef"
class="upload"
@ -27,7 +19,7 @@
@error="onUploadError"
>
<template #upload-button>
<a-button type="primary">
<a-button type="outline">
<template #icon>
<i class="icon-park-outline-upload-one"></i>
</template>
@ -35,8 +27,8 @@
</a-button>
</template>
</a-upload>
<div class="flex-1 flex items-center text-gray-400">
归类为:
<div class="flex items-center text-gray-400">
已选择 {{ fileList.length }} 归类为:
<span>
<a-select v-model="group" :bordered="false" :options="groupOptions"></a-select>
</span>
@ -59,14 +51,12 @@
<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>
<a-progress :percent="formatProgress(item, true)" :show-text="false" class="block! mt-0.5"></a-progress>
<div class="flex items-center justify-between gap-2 text-gray-400 mt-1.5 text-xs">
<span class="text-xs">
<!-- <span class="text-xs">
<span v-if="item.status === 'init'">
<i class="icon-park-outline-lightning"></i>
等待上传
@ -86,14 +76,10 @@
</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-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>
</span> -->
</div>
</div>
</li>
@ -106,14 +92,10 @@
<template #footer>
<div class="flex justify-between gap-2 items-center">
<div class="text-gray-400">已上传 {{ stat.doneCount }}/{{ fileList.length }} </div>
<div class="text-gray-400"></div>
<div class="space-x-2">
<a-button type="text" :disabled="!fileList.length || Boolean(stat.uploadingCount)" @click="clearUploaded">
清空
</a-button>
<a-button type="primary" :disabled="!fileList.length || !stat.initCount" @click="startUpload">
开始上传
</a-button>
<a-button type="text" :disabled="!fileList.length || Boolean(stat.uploadingCount)" @click="clearUploaded"> </a-button>
<a-button type="primary" :disabled="!fileList.length || !stat.initCount" @click="startUpload"> </a-button>
</div>
</div>
</template>
@ -306,4 +288,10 @@ const groupOptions = [
];
</script>
<style lang="less" scoped></style>
<style lang="less">
.an-upload {
.arco-modal-body {
padding-top: 16px;
}
}
</style>

View File

@ -25,14 +25,14 @@
</template>
<script setup lang="tsx">
import { FileCategory, api } from '@/api';
import { useTable, useTableDelete, useUpdateColumn } from '@/components/AnTable';
import { FileTypes } from '@/constants/file';
import { Message } from '@arco-design/web-vue';
import numeral from 'numeral';
import AnCategory from './AnCategory.vue';
import AnPreview from './AnPreview.vue';
import AnUpload from './AnUpload.vue';
import { FileCategory, api } from '@/api';
import { useCreateColumn, useTable, useTableDelete, useUpdateColumn } from '@/components/AnTable';
import { FileTypes } from '@/constants/file';
import { Message } from '@arco-design/web-vue';
import { getIcon } from './util';
const current = ref<FileCategory>();
@ -116,7 +116,7 @@ const {
width: 150,
render: ({ record }) => numeral(record.size).format('0 b'),
},
useCreateColumn(),
// useCreateColumn(),
useUpdateColumn(),
{
type: 'button',

File diff suppressed because it is too large Load Diff

View File

@ -1,59 +0,0 @@
<template>
<div ref="editorRef" class="w-full h-full border"></div>
</template>
<script setup lang="ts">
import * as monaco from "monaco-editor";
import editorWorker from "monaco-editor/esm/vs/editor/editor.worker?worker";
import cssWorker from "monaco-editor/esm/vs/language/css/css.worker?worker";
import htmlWorker from "monaco-editor/esm/vs/language/html/html.worker?worker";
import jsonWorker from "monaco-editor/esm/vs/language/json/json.worker?worker";
import tsWorker from "monaco-editor/esm/vs/language/typescript/ts.worker?worker";
self.MonacoEnvironment = {
getWorker(_, label) {
if (label === "json") {
return new jsonWorker();
}
if (label === "css" || label === "scss" || label === "less") {
return new cssWorker();
}
if (label === "html" || label === "handlebars" || label === "razor") {
return new htmlWorker();
}
if (label === "typescript" || label === "javascript") {
return new tsWorker();
}
return new editorWorker();
},
};
let editor: monaco.editor.IStandaloneCodeEditor | undefined;
const editorRef = ref<HTMLElement | null>(null);
onMounted(async () => {
await nextTick();
if (editorRef.value) {
editor = monaco.editor.create(editorRef.value, {
value: "",
language: "html",
});
}
});
const props = defineProps({
content: {
type: String,
default: "",
},
});
watch(
() => props.content,
(value) => {
editor?.setValue(value);
}
);
</script>
<style lang="less" scoped></style>

View File

@ -1,124 +0,0 @@
<template>
<bread-page>
<div class="h-full grid grid-cols-[1fr_auto_1fr]">
<div>
<a-tabs @change="onChange">
<a-tab-pane v-for="tag in tags" :key="tag.name" :title="tag.description">
<a-form :model="{}" layout="vertical">
<a-form-item label="新增接口">
<a-radio-group type="button" v-model="type.create">
<a-radio
v-for="route in routes.filter(i => i.tag === tag.name)"
:value="route.operationId"
:key="route.path"
>
{{ route.description }}
</a-radio>
</a-radio-group>
</a-form-item>
<a-form-item label="修改接口">
<a-radio-group type="button" v-model="type.modify">
<a-radio
v-for="route in routes.filter(i => i.tag === tag.name)"
:value="route.operationId"
:key="route.path"
>
{{ route.description }}
</a-radio>
</a-radio-group>
</a-form-item>
<a-form-item label="查询接口">
<a-radio-group type="button" v-model="type.select">
<a-radio
v-for="route in routes.filter(i => i.tag === tag.name)"
:value="route.operationId"
:key="route.path"
>
{{ route.description }}
</a-radio>
</a-radio-group>
</a-form-item>
<a-form-item label="删除接口">
<a-radio-group type="button" v-model="type.delete">
<a-radio
v-for="route in routes.filter(i => i.tag === tag.name)"
:value="route.operationId"
:key="route.path"
>
{{ route.description }}
</a-radio>
</a-radio-group>
</a-form-item>
</a-form>
</a-tab-pane>
</a-tabs>
</div>
<a-divider direction="vertical"></a-divider>
<div class="h-full grid grid-rows-[auto_1fr] gap-2">
<div>
<a-button type="primary" @click="onOpen"></a-button>
</div>
<editor-modal class="bg-gray-100" :content="content"></editor-modal>
</div>
</div>
</bread-page>
</template>
<script setup lang="ts">
import ejs from 'ejs';
import doc from './data.json';
import editorModal from './editor.vue';
import template from './page.ejs?raw';
const content = ref('');
const { tags, routes } = doc;
const type = ref({
create: undefined,
select: undefined,
modify: undefined,
delete: undefined,
});
const onChange = (value: string | number) => {
console.log(value);
};
const onOpen = () => {
const data = {
tag: '',
operationId: '',
create: {},
select: {},
modify: {},
delete: {},
};
for (const route of doc.routes) {
if (route.operationId === type.value.create) {
data.create = route;
}
if (route.operationId === type.value.select) {
data.select = route;
}
if (route.operationId === type.value.modify) {
data.modify = route;
}
if (route.operationId === type.value.delete) {
data.delete = route;
}
}
content.value = ejs.render(template, data);
};
</script>
<style lang="less" scoped></style>
<route lang="json">
{
"meta": {
"sort": 20010,
"hide": "prod",
"title": "接口生成",
"icon": "icon-park-outline-code"
}
}
</route>

View File

@ -1,103 +0,0 @@
<template>
<BreadPage>
<ani-table> </ani-table>
</BreadPage>
</template>
<script setup lang="tsx">
import { api } from "@/api";
import { createColumn, updateColumn, useAniTable } from "@/components";
const [aniTable, aniCtx] = useAniTable({
data: async (model, paging) => {
return api.<%= select.tag %>.<%= operationId %>({ ...model, ...paging });
},
columns: [
{
title: "用户描述",
dataIndex: "description",
},
createColumn,
updateColumn,
{
title: "操作",
type: "button",
width: 180,
buttons: [
{
type: "modify",
text: "修改",
},
{
type: "delete",
text: "删除",
onClick: async ({ record }) => {
return api.<%= tag %>.<%= operationId %>(record.id);
},
},
],
},
],
search: {
items: [
{
extend: "nickname",
required: false,
type: 'search',
enableLoad: true,
itemProps: {
hideLabel: true,
},
nodeProps: {
placeholder: "用户昵称",
},
},
],
},
create: {
title: "新建用户",
modalProps: {
width: 732,
maskClosable: false,
},
formProps: {
layout: "vertical",
class: "!grid grid-cols-2 gap-x-6",
},
items: [
<%_ for(const item of create.bodyParams) { _%>
{
field: "<%= item.name %>",
label: "<%= item.description %>",
type: "<%= item.type %>",
required: <%= item.required %>,
},
<%_ } _%>
],
submit: ({ model }) => {
return api.<%= create.tag %>.<%= create.operationId %>(model);
},
},
modify: {
extend: true,
title: "修改用户",
submit: ({ model }) => {
return api.<%= modify.tag %>.<%= modify.operationId %>(model.id, model);
},
},
});
</script>
<style scoped></style>
<%_ if(false) { _%>
<route lang="json">
{
"meta": {
"sort": 10301,
"title": "用户管理",
"icon": "icon-park-outline-user"
}
}
</route>
<%_ } _%>

View File

@ -1,15 +1,13 @@
<template>
<div></div>
<iframe src="https://nav.juetan.cn" frameborder="0" class="w-full h-full overflow-hidden"></iframe>
</template>
<route lang="json">
{
"component": null,
"meta": {
"sort": 120012,
"hide": "prod",
"title": "前端导航",
"link": "https://nav.juetan.cn",
"icon": "icon-park-outline-mail"
}
}

View File

@ -112,7 +112,7 @@ const stat = {
"alias": "/",
"meta": {
"sort": 1000,
"title": "首页",
"title": "概览",
"icon": "icon-park-outline-home"
}
}

View File

@ -5,7 +5,7 @@
<route lang="json">
{
"redirect": "/setting/common",
// "component": null,
"component": null,
"meta": {
"sort": 30401,
"title": "系统设置",

View File

@ -4,7 +4,7 @@
<div class="w-full">
<div class="flex item-center justify-between gap-4">
<div>
<h2 class="m-0 text-lg font-semibold flex items-center gap-2">
<h2 class="m-0 text-lg font-normal flex items-center gap-2">
邮件设置
<a-tag :color="mail.enable ? 'green' : 'red'">
<template #icon>
@ -16,15 +16,10 @@
<p class="text-gray-500 mt-1.5 p-0 m0 m-0">首次为你的帐户添加密码时你需要前往密码重置页面以便我们验证你的身份</p>
</div>
<div class="flex items-center pr-6">
<a-button type="primary">
<template #icon>
<i class="icon-park-outline-send-email"></i>
</template>
测试
</a-button>
</div>
</div>
<a-form :model="{}" :disabled="!mail.enable" label-align="left" class="col-form divide-y mt-8 space-y-6">
<a-form :model="{}" :disabled="!mail.enable" label-align="left" class="col-form divide-y divide-gray-100 mt-8 space-y-6">
<a-form-item label="是否启用" :disabled="false">
<a-switch v-model="mail.enable"> </a-switch>
<template #help> 启用后其他服务可发送邮件通知 </template>
@ -58,6 +53,9 @@
</a-form-item>
<a-form-item :disabled="false" class="pt-6">
<a-button type="primary"> 保存修改 </a-button>
<a-button class="ml-4">
测试
</a-button>
</a-form-item>
</a-form>
</div>

View File

@ -8,7 +8,7 @@
<script setup lang="tsx">
import { api } from '@/api';
import { useFormModal } from '@/components/AnForm';
import { TableColumnRender, useCreateColumn, useTable, useUpdateColumn } from '@/components/AnTable';
import { TableColumnRender, useCreateColumn, useTable } from '@/components/AnTable';
defineOptions({ name: 'SystemDepartmentPage' });
@ -64,9 +64,6 @@ const { component: UserTable } = useTable({
{
...useCreateColumn(),
},
{
...useUpdateColumn(),
},
{
title: '操作',
type: 'button',

View File

@ -8,12 +8,16 @@
<an-group :current="current" @change="onTypeChange"></an-group>
</div>
<div class="bg-white p-4">
<div :show-icon="false" class="rounded mb-3 bg-gray-200 px-4 py-3">
<div :show-icon="false" class="rounded mb-3 bg-gray-100 px-4 py-3">
<span class="text-base">
<i class="icon-park-outline-folder-close"></i>
{{ current?.name }}
</span>
<div class="mt-1.5 text-gray-500">描述{{ current?.description }}</div>
<div class="mt-2 flex gap-1">
<a-link>修改</a-link>
<a-link status="danger">删除</a-link>
</div>
</div>
<dict-table></dict-table>
</div>

View File

@ -8,7 +8,7 @@
<script setup lang="tsx">
import { api } from '@/api';
import { useFormModal } from '@/components/AnForm';
import { TableColumnRender, useCreateColumn, useTable, useUpdateColumn } from '@/components/AnTable';
import { TableColumnRender, useTable } from '@/components/AnTable';
defineOptions({ name: 'SystemUserPage' });
@ -37,7 +37,7 @@ const usernameRender: TableColumnRender = ({ record }) => (
</a-avatar>
<div class="w-full flex-1 overflow-hidden">
<div>
<span class="cursor-pointer text-brand-600 hover:text-brand-400">{record.nickname}</span>
<span class="cursor-pointer ">{record.nickname}</span>
</div>
</div>
@ -65,7 +65,6 @@ const { component: UserTable } = useTable({
title: '操作',
type: 'button',
width: 200,
align: 'right',
buttons: [
{
text: '重置密码',

View File

@ -3,7 +3,7 @@
@arcoblue-6: #08f;
body {
// --border-radius-small: 4px;
--border-radius-small: 2px;
.arco-table .arco-table-element {
table-layout: fixed;
@ -62,7 +62,7 @@ body {
.arco-menu {
&.arco-menu-vertical .arco-menu-item {
line-height: 36px;
margin-top: 2px;
margin-top: 4px;
}
&.arco-menu-vertical .arco-menu-group-title {
line-height: 28px;

View File

@ -50,12 +50,11 @@ declare module 'vue-router/auto/routes' {
'/content/material-category/': RouteRecordInfo<'/content/material-category/', '/content/material-category', Record<never, never>, Record<never, never>>,
'/content/post/': RouteRecordInfo<'/content/post/', '/content/post', Record<never, never>, Record<never, never>>,
'/dev/': RouteRecordInfo<'/dev/', '/dev', Record<never, never>, Record<never, never>>,
'/dev/editor/': RouteRecordInfo<'/dev/editor/', '/dev/editor', Record<never, never>, Record<never, never>>,
'/dev/nav/': RouteRecordInfo<'/dev/nav/', '/dev/nav', Record<never, never>, Record<never, never>>,
'/dev/openapi/': RouteRecordInfo<'/dev/openapi/', '/dev/openapi', Record<never, never>, Record<never, never>>,
'/home/': RouteRecordInfo<'/home/', '/home', Record<never, never>, Record<never, never>>,
'/log/': RouteRecordInfo<'/log/', '/log', Record<never, never>, Record<never, never>>,
'/log/login/': RouteRecordInfo<'/log/login/', '/log/login', Record<never, never>, Record<never, never>>,
'/log/auth/': RouteRecordInfo<'/log/auth/', '/log/auth', Record<never, never>, Record<never, never>>,
'/log/operation/': RouteRecordInfo<'/log/operation/', '/log/operation', Record<never, never>, Record<never, never>>,
'/setting/': RouteRecordInfo<'/setting/', '/setting', Record<never, never>, Record<never, never>>,
'/setting/common/': RouteRecordInfo<'/setting/common/', '/setting/common', Record<never, never>, Record<never, never>>,