Compare commits

...

10 Commits

Author SHA1 Message Date
luoer 7f02e3bc97 feat: 优化部署配置 2023-11-01 09:32:57 +08:00
绝弹 7bea445253 feat: 优化上传组件 2023-10-31 22:35:35 +08:00
luoer ce93e87e38 feat: 优化上传组件事件类型提示 2023-10-31 17:53:17 +08:00
luoer 2a55bc0fcc feat: 优化上传组件样式 2023-10-31 17:30:01 +08:00
绝弹 751102f4ad feat: 优化上传弹窗 2023-10-30 22:12:32 +08:00
luoer 687f6250eb feat: 恢复直角 2023-10-30 17:36:07 +08:00
绝弹 1d572cf8e4 feat: 日常更新 2023-10-29 21:12:20 +08:00
luoer 1133555ca2 feat: 添加字典管理 2023-10-27 17:34:26 +08:00
绝弹 09498ec02e feat: 优化右侧样式 2023-10-26 22:42:13 +08:00
luoer 5b9c14184e feat: 优化菜单管理页面 2023-10-26 17:48:48 +08:00
38 changed files with 2184 additions and 777 deletions

2
.env
View File

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

View File

@ -32,7 +32,7 @@ env:
# 部署服务器密码, 例如: 123456
deploy_pass: ${{ secrets.DEPLOY_PASS }}
# 要更新的 docker 服务名称, 例如: demo_web
deploy_name: demo_web
deploy_name: appnify_web
jobs:
build:
@ -42,43 +42,40 @@ jobs:
steps:
- name: 检出代码
id: checkout
uses: https://git.dev.juetan.cn/mirror/checkout@v3
uses: actions/checkout@v3
- name: 设置环境
uses: https://git.dev.juetan.cn/mirror/setup-node@v2
# - name: 设置NodeJS环境
# uses: actions/setup-node@v2
- name: 安装依赖
run: |
npm install --registry https://registry.npmmirror.com/
# - name: 安装Npm依赖
# run: npm install --registry https://registry.npmmirror.com/
- name: 构建产物
run: npm run build
# - name: 构建产物
# run: npm run build
- name: 打印目录
run: ls ./dist
# - name: 打印产物目录
# run: ls ./dist
- name: 构建镜像
run: |
docker build -t ${{ env.docker_name }}:latest .
- name: 构建Docker镜像
run: docker build -t ${{ env.docker_name }}:latest .
- name: 登陆镜像
run: |
docker login -u "${{ env.docker_user }}" -p "${{ env.docker_pass }}" ${{ env.docker_host }}
- name: 登陆Docker镜像仓库
run: docker login -u "${{ env.docker_user }}" -p "${{ env.docker_pass }}" ${{ env.docker_host }}
- name: 推送镜像
- name: 推送Docker镜像到仓库
shell: bash
run: |
docker push ${{ env.docker_name }}:latest
run: docker push ${{ env.docker_name }}:latest
- name: 标记镜像
- name: 打上Docker镜像版本标签并推送到仓库
if: gitea.ref_type == 'tag'
run: |
echo "当前推送版本:${{ gitea.ref_name }}"
docker tag ${{ env.docker_name }}:latest ${{ env.docker_name }}:${{ gitea.ref_name }}
docker push ${{ env.docker_name }}:${{ gitea.ref_name }}
- name: 更新服务
uses: https://git.dev.juetan.cn/mirror/ssh-action@v1.0.0
- name: 登陆到部署环境执行更新命令
uses: appleboy/ssh-action@v1.0.0
if: false
with:
host: ${{ env.deploy_host }}
port: ${{ env.deploy_port }}

View File

@ -1,15 +1,16 @@
FROM node:20-alpine as build
FROM node:20-alpine as builder
WORKDIR /app
COPY ./package.json .
COPY ./pnpm-lock.yaml .
COPY package.json .
COPY pnpm-lock.yaml .
COPY .npmrc .
RUN corepack enable
RUN pnpm install --registry https://registry.npmmirror.com/
RUN pnpm install
COPY . .
RUN pnpm build
FROM nginx:alpine
COPY --from=build /app/dist /usr/share/nginx/html
COPY --from=build /app/.github/nginx.conf /etc/nginx/conf.d/default.conf
COPY --from=builder /app/dist /usr/share/nginx/html
COPY --from=builder /app/.github/nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,5 @@
<template>
<div>
<div class="h-full overflow-hidden grid grid-rows-[auto_1fr]">
<div class="bg-white px-4 py-2">
<div class="flex justify-between gap-4">
<BreadCrumb></BreadCrumb>
@ -9,9 +9,11 @@
</div>
</div>
<slot name="content">
<div class="m-4 p-4 bg-white">
<slot></slot>
</div>
<a-scrollbar outer-class="h-full overflow-hidden" class="h-full overflow-auto" type="track">
<div class="m-4 p-4 bg-white rounded overflow-hidden">
<slot></slot>
</div>
</a-scrollbar>
</slot>
</div>
</template>
@ -20,4 +22,4 @@
import BreadCrumb from "./bread-crumb.vue";
</script>
<style scoped></style>
<style scoped></style>

View File

@ -0,0 +1,156 @@
<template>
<bread-page>
<template #content>
<div class="h-full w-full grid grid-cols-[auto_1fr] gap-4 p-4">
<div class="bg-white w-[256px]">
<div class="flex items-center justify-between gap-2 px-4 h-14">
<span class="text-base">菜单列表</span>
<div>
<a-button>
<template #icon>
<i class="icon-park-outline-plus"></i>
</template>
</a-button>
</div>
</div>
<a-tree
:data="menus"
:default-expand-all="true"
:block-node="true"
:field-names="{
icon: undefined,
title: 'name',
key: 'id',
}"
>
<template #title="node">
<div class="group flex-1 flex items-center justify-between gap-2">
<div @click="onEdit(node)">
<!-- <a-tag :color="MenuTypes.fmt(node.type, 'color')" size="small" :bordered="true">
{{ MenuTypes.fmt(node.type) }}
</a-tag> -->
<i :class="node.icon" class="ml-2"></i>
{{ node.name }}
</div>
<div class="hidden group-hover:block">
<i
v-if="node.type === MenuType.MENU"
class="text-sm text-gray-400 hover:text-gray-700 icon-park-outline-plus"
></i>
<i class="text-sm text-gray-400 hover:text-gray-700 icon-park-outline-delete"></i>
</div>
</div>
</template>
</a-tree>
</div>
<div class="bg-white">
<a-card title="菜单信息" :bordered="false">
<Form ref="formRef" v-bind="form"></Form>
</a-card>
<a-divider :margin="0"></a-divider>
<div class="px-4 mt-4">
<btn-table></btn-table>
</div>
</div>
</div>
</template>
</bread-page>
</template>
<script setup lang="tsx">
import { Menu, api } from "@/api";
import { useForm, Form, useAniTable, FormInstance } from "@/components";
import { MenuType, MenuTypes } from "@/constants/menu";
const formRef = ref<FormInstance | null>(null);
const menus = ref<any[]>([]);
const treeEach = (tree: any[], fn: any) => {
for (const item of tree) {
if (item.children) {
treeEach(item.children, fn);
}
fn(item);
}
};
const onEdit = (row: any) => {
formRef.value?.setModel(row);
(btn.props as any).data = row.buttons;
};
onMounted(async () => {
const res = await api.menu.getMenus({ tree: true });
const data = res.data.data ?? [];
treeEach(data, (item: Menu) => {
if (item.type === MenuType.BUTTON) {
return;
}
if (item.type === MenuType.PAGE) {
(item as any).buttons = (item as any).children;
delete (item as any).children;
}
(item as any).iconRender = () => <i class={item.icon} />;
});
menus.value = data;
});
const form = useForm({
items: [
{
field: "name",
label: "菜单名称",
type: "input",
},
{
field: "icon",
label: "菜单图标",
type: "input",
},
],
async submit(arg) {
console.log(arg);
},
});
const [btnTable, btn] = useAniTable({
columns: [
{
title: " 名称",
dataIndex: "name",
},
{
title: "标识",
dataIndex: "code",
},
{
title: "操作",
type: "button",
width: 140,
buttons: [
{
type: "modify",
text: "修改",
},
{
text: "删除",
type: "delete",
},
],
},
],
create: {},
modify: {},
});
</script>
<style lang="less" scoped></style>
<route lang="json">
{
"meta": {
"sort": 10302,
"title": "菜单管理",
"icon": "icon-park-outline-add-subtract"
}
}
</route>

View File

@ -16,7 +16,7 @@
<a-doption>保存为图片</a-doption>
</template>
</a-dropdown-button>
<a-button status="danger">退出</a-button>
<a-button type="outline" status="danger">退出</a-button>
</div>
</div>
</template>

View File

@ -143,7 +143,7 @@ export const FormModal = defineComponent({
}
if (typeof props.trigger === "object") {
content = (
<Button type="primary" {...omit(props.trigger, "text")}>
<Button type="primary" {...props.trigger.buttonProps}>
{props.trigger?.text || "新增"}
</Button>
);
@ -176,6 +176,7 @@ export const FormModal = defineComponent({
onBeforeOk={this.onBeforeOk}
onClose={this.onClose}
title={this.modalTitle}
class="ani-form-modal"
>
{this.visible && (
<Form ref={(el: any) => (this.formRef = el)} {...this.formProps} model={this.model} items={this.items}>

View File

@ -8,7 +8,7 @@ const defineColumn = <T extends TableColumn>(column: T) => {
export const updateColumn = defineColumn({
title: "更新者",
dataIndex: "createdAt",
width: 200,
width: 190,
render({ record }) {
return (
<div class="flex flex-col overflow-hidden">
@ -24,7 +24,7 @@ export const updateColumn = defineColumn({
export const createColumn = defineColumn({
title: "创建者",
dataIndex: "createdAt",
width: 200,
width: 190,
render({ record }) {
return (
<div class="flex flex-col overflow-hidden">

View File

@ -72,6 +72,7 @@ export const Table = defineComponent({
},
setup(props) {
const loading = ref(false);
const tableRef = ref<InstanceType<typeof BaseTable>>()
const searchRef = ref<FormInstance>();
const createRef = ref<FormModalInstance>();
const modifyRef = ref<FormModalInstance>();
@ -142,6 +143,7 @@ export const Table = defineComponent({
const state = {
loading,
inlined,
tableRef,
searchRef,
createRef,
modifyRef,
@ -160,12 +162,12 @@ export const Table = defineComponent({
return (
<div class="table w-full">
{!this.inlined && (
<div class="border-b pb-2 border-slate-200 mb-5">
<div class="border-b pb-0 border-slate-200 mb-3">
<Form ref="searchRef" class="!grid grid-cols-4 gap-x-6" {...this.search}></Form>
</div>
)}
<div class={`mb-3 flex justify-between ${!this.inlined && "mt-2"}`}>
<div class={`mb-3 flex toolbar justify-between ${!this.inlined && "mt-2"}`}>
<div class={`${this.create || this.$slots.action ? null : "!hidden"} flex-1 flex gap-2 `}>
{this.create && (
<FormModal {...(this.create as any)} ref="createRef" onSubmited={this.reloadData}></FormModal>
@ -184,6 +186,7 @@ export const Table = defineComponent({
</div>
<BaseTable
ref="tableRef"
row-key="id"
bordered={false}
{...this.$attrs}

View File

@ -1,5 +1,5 @@
import { delConfirm } from "@/utils";
import { Doption, Dropdown, Link, Message, TableColumnData } from "@arco-design/web-vue";
import { Divider, Doption, Dropdown, Link, Message, TableColumnData } from "@arco-design/web-vue";
import { isArray, merge } from "lodash-es";
import { Component, Ref, reactive } from "vue";
import { useFormModal } from "../form";
@ -75,18 +75,21 @@ export const useTable = (optionsOrFn: UseTableOptions | (() => UseTableOptions))
buttons.push(merge({}, config.columnButtonBase));
}
column.render = (columnData) => {
return column.buttons?.map((btn) => {
return column.buttons?.map((btn, index) => {
if (btn.visible?.(columnData) === false) {
return null;
}
return (
<Link
{...btn.buttonProps}
onClick={() => onClick(btn, columnData, getTable)}
disabled={btn.disabled?.(columnData)}
>
{btn.text}
</Link>
<>
{index !== 0 ? <Divider direction="vertical" margin={2} class="!border-gray-300"></Divider> : null}
<Link
{...btn.buttonProps}
onClick={() => onClick(btn, columnData, getTable)}
disabled={btn.disabled?.(columnData)}
>
{btn.text}
</Link>
</>
);
});
};

View File

@ -31,7 +31,7 @@ export default defineComponent({
const icon = route.icon ? () => <i class={route.icon} /> : null;
const node: any = route.children?.length ? (
<>
<div class="px-2"><a-divider margin={6}></a-divider></div>
<div class="px-2"><a-divider margin={6} class="!border-slate-100"></a-divider></div>
{this.renderItem(route?.children)}
</>
) : (

View File

@ -49,6 +49,7 @@
<script setup lang="ts">
import { useAniFormModal } from "@/components";
import { useUserStore } from "@/store";
import { delConfirm } from "@/utils";
import { Message } from "@arco-design/web-vue";
const userStore = useUserStore();
@ -56,6 +57,7 @@ const route = useRoute();
const router = useRouter();
const logout = async () => {
await delConfirm('退出后将跳转到登录页面,确定退出吗?')
userStore.clearUser();
Message.success("提示:已退出登陆!");
router.push({ path: "/login", query: { redirect: route.path } });

View File

@ -3,17 +3,17 @@
<a-layout-header
class="h-13 overflow-hidden flex justify-between items-center gap-4 px-2 pr-4 border-b border-slate-200 bg-white dark:bg-slate-800 dark:border-slate-700"
>
<div class="h-13 flex items-center border-b border-slate-200 dark:border-slate-800">
<router-link to="/" class="px-2 py-1 rounded flex items-center gap-2 text-slate-700 hover:bg-slate-100">
<div class="h-13 flex items-center">
<router-link to="/" class="px-2 py-1 rounded flex items-center gap-2 text-slate-700">
<img src="/favicon.ico" alt="" width="22" height="22" class="" />
<h1 class="relative text-lg leading-[19px] dark:text-white m-0 p-0">
{{ appStore.title }}
<span
<!-- <span
v-if="isDev"
class="absolute -right-14 -top-1 text-xs font-normal text-brand-500 bg-brand-50 px-1.5 rounded-full"
>
本地版
</span>
</span> -->
</h1>
</router-link>
</div>
@ -35,7 +35,7 @@
<a-layout class="flex flex-1 overflow-hidden">
<a-layout-sider
class="h-full overflow-hidden dark:bg-slate-800 border-r border-slate-200 dark:border-slate-700"
class="h-full overflow-hidden dark:bg-slate-800 border-r border-slate-100 dark:border-slate-700"
:width="224"
:collapsed-width="52"
:collapsible="true"
@ -43,9 +43,12 @@
:hide-trigger="false"
@collapse="onCollapse"
>
<a-scrollbar outer-class="h-full overflow-hidden" class="h-full overflow-hidden pt-2">
<a-scrollbar outer-class="h-full overflow-hidden" class="h-full overflow-hidden pt-1">
<Menu />
</a-scrollbar>
<template #trigger="{ collapsed }">
<i :class="collapsed ? `icon-park-outline-expand-left` : 'icon-park-outline-expand-right'" class="text-gray-400 text-base hover:text-gray-700"></i>
</template>
</a-layout-sider>
<a-layout class="layout-content flex-1">
<a-layout-content class="overflow-x-auto">
@ -201,7 +204,7 @@ const tagItems = [
//
// min-height: 100vh;
overflow-y: hidden;
background-color: var(--color-fill-2);
background-color: #e4ebf1;
transition: padding 0.2s cubic-bezier(0.34, 0.69, 0.1, 1);
}
</style>

View File

@ -101,10 +101,10 @@ const onSubmitForm = async () => {
try {
loading.value = true;
const res = await api.auth.login(model);
userStore.setUser(res.data.data);
userStore.setAccessToken(res.data.data as unknown as string);
Notification.success({
title: "提示",
content: `欢迎回来,${res.data.data.nickname}!`,
content: `登陆成功!`,
});
router.push({ path: (route.query.redirect as string) || "/" });
} catch (error: any) {

View File

@ -65,10 +65,10 @@
</template>
<script setup lang="ts">
import doc from "@/dd.json";
import editorModal from "./editor.vue";
import ejs from "ejs";
import template from "./page.ejs?raw";
import doc from "./components/data.json";
import editorModal from "./components/editor.vue";
import template from "./components/page.ejs?raw";
const content = ref("");
const { tags, routes } = doc;
@ -85,8 +85,8 @@ const onChange = (value: string | number) => {
const onOpen = () => {
const data = {
tag: '',
operationId: '',
tag: "",
operationId: "",
create: {},
select: {},
modify: {},
@ -106,7 +106,6 @@ const onOpen = () => {
data.delete = route;
}
}
console.log(data);
content.value = ejs.render(template, data);
};
</script>

View File

@ -1,10 +1,12 @@
<template>
<bread-page>
<iframe
src="https://apifox.com/apidoc/shared-f1ea65e6-cee8-4fe3-949f-288a7cd1af49"
frameborder="0"
class="w-full h-full"
></iframe>
<template #content>
<iframe
src="https://apifox.com/apidoc/shared-f1ea65e6-cee8-4fe3-949f-288a7cd1af49"
frameborder="0"
class="w-full h-full"
></iframe>
</template>
</bread-page>
</template>

View File

@ -17,7 +17,7 @@
class="group flex items-center justify-between gap-1 h-8 rounded mb-2 pl-3 hover:bg-gray-100 cursor-pointer"
>
<div>
<i class="icon-park-outline-folder-close"></i>
<i class="icon-park-outline-folder-close align-[-2px]"></i>
{{ item.title }}
<span class="text-xs text-gray-500"> ({{ item.count }}) </span>
</div>

View File

@ -1,44 +1,277 @@
<template>
<a-modal v-model:visible="modal.visible" title="上传文件" title-align="start" :footer="false">
<a-upload :custom-request="upload" draggable action="/api/v1/upload"></a-upload>
<a-button type="primary" @click="visible = true"> 上传文件 </a-button>
<a-modal
v-model:visible="visible"
title="上传文件"
title-align="start"
:width="860"
:mask-closable="false"
:on-before-cancel="onBeforeCancel"
@close="onClose"
>
<div class="mb-2 flex items-center gap-4">
<a-upload
ref="uploadRef"
class="upload"
v-model:file-list="fileList"
:multiple="true"
:custom-request="upload"
:auto-upload="false"
:show-file-list="false"
@success="onUploadSuccess"
@error="onUploadError"
>
<template #upload-button>
<a-button type="outline"> 选择文件 </a-button>
</template>
</a-upload>
<div class="flex-1 flex items-center text-gray-400">
归类为:
<span>
<a-select v-model="group" :bordered="false" :options="groupOptions"></a-select>
</span>
</div>
</div>
<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">
<div class="flex-1 overflow-hidden">
<div class="truncate text-slate-900">
{{ item.name }}
</div>
<div class="flex items-center justify-between gap-2 text-gray-400 mb-[-4px] mt-1">
<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 }})
</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 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>
</div>
</li>
</a-scrollbar>
</ul>
<div v-else class="h-[424px] flex items-center justify-center">
<a-empty description="选择文件后显示"></a-empty>
</div>
<template #footer>
<div class="flex justify-between gap-2 items-center">
<div class="text-gray-400">已上传 {{ stat.doneCount }}/{{ fileList.length }} </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>
</div>
</div>
</template>
</a-modal>
</template>
<script setup lang="ts">
import { api } from "@/api";
import { RequestOption } from "@arco-design/web-vue";
import { RequestParams, api } from "@/api";
import { delConfirm } from "@/utils";
import { FileItem, Message, RequestOption, UploadInstance } from "@arco-design/web-vue";
import axios from "axios";
import numeral from "numeral";
const modal = ref({
visible: false,
const emit = defineEmits<{
(event: "success", item: FileItem): void;
(event: "close", count: number): void;
}>();
const visible = ref(false);
const uploadRef = ref<UploadInstance | null>(null);
const fileList = ref<FileItem[]>([]);
const fileMap = reactive<
Map<
string,
{
lastTime: number;
lastLoaded: number;
speed: number;
cost: number;
error: string;
} | null
>
>(new Map());
const stat = computed(() => {
const result = {
initCount: 0,
doneCount: 0,
uploadingCount: 0,
errorCount: 0,
};
for (const item of fileList.value) {
if (item.status === "init") result.initCount++;
if (item.status === "uploading") result.uploadingCount++;
if (item.status === "done") result.doneCount++;
if (item.status === "error") result.errorCount++;
}
return result;
});
/**
* 开始上传
*/
const startUpload = () => {
uploadRef.value?.submit();
};
/**
* 中止上传
* @param item 文件
*/
const pauseItem = (item: FileItem) => {
uploadRef.value?.abort(item);
const file = fileMap.get(item.uid);
if (file) {
file.error = "手动中止";
}
};
/**
* 移除文件
* @param item 文件
*/
const removeItem = (item: FileItem) => {
const index = fileList.value.findIndex((i) => i.uid === item.uid);
if (index > -1) {
fileList.value.splice(index, 1);
}
};
/**
* 重新上传
* @param item 文件
*/
const retryItem = (item: FileItem) => {
uploadRef.value?.submit(item);
};
/**
* 清空已上传
*/
const clearUploaded = async () => {
if (stat.value.doneCount !== fileList.value.length) {
await delConfirm("当前有未上传完成的文件,是否继续清空?");
}
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) {
file.error = item.response?.data?.message || "网络异常";
}
};
/**
* 关闭前检测
*/
const onBeforeCancel = () => {
if (fileList.value.some((i) => i.status === "uploading")) {
Message.warning("提示:文件上传中,请稍后再试!");
return false;
}
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();
if (fileItem.file) {
api.file
.addFile(
{
file: fileItem.file,
},
{
onUploadProgress(e) {
let percent = 0;
if (e.total && e.total > 0) {
percent = e.loaded / e.total;
}
onProgress(percent, e as any);
},
cancelToken: source.token,
if (!fileMap.has(fileItem.uid)) {
fileMap.set(fileItem.uid, {
lastTime: Date.now(),
lastLoaded: 0,
cost: 0,
speed: 0,
error: "网络异常",
});
}
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) {
let percent = 0;
const { lastTime, lastLoaded } = item;
if (e.total && e.total > 0) {
percent = e.loaded / e.total;
const nowTime = Date.now();
const diff = (e.loaded - lastLoaded) / (nowTime - lastTime);
const speed = Math.floor(diff * 1000);
item.speed = speed;
item.lastLoaded = e.loaded;
item.lastTime = nowTime;
}
)
.then((res) => {
onSuccess(res);
})
.catch((e) => {
onError(e);
});
onProgress(percent, e as any);
},
cancelToken: source.token,
};
try {
const res = await api.file.addFile(data, params);
const currentTime = Date.now();
item.cost = Math.floor((currentTime - startTime) / 1000);
onSuccess(res);
} catch (e) {
onError(e);
}
};
if (fileItem.file) {
up();
}
return {
abort() {
@ -49,9 +282,22 @@ const upload = (option: RequestOption) => {
defineExpose({
open: () => {
modal.value.visible = true;
visible.value = true;
},
});
// TODO
const group = ref("default");
const groupOptions = [
{
label: "默认分类",
value: "default",
},
{
label: "视频分类",
value: "video",
},
];
</script>
<style scoped></style>
<style lang="less" scoped></style>

View File

@ -1,20 +1,17 @@
<template>
<BreadPage>
<div class="overflow-hidden grid grid-cols-[auto_auto_1fr]">
<div class="overflow-hidden h-full grid grid-cols-[auto_1fr] gap-4">
<ani-group></ani-group>
<a-divider direction="vertical" :margin="16"></a-divider>
<div>
<Table v-bind="table">
<template #action>
<a-button type="primary" @click="uploadRef?.open()">
<template #icon>
<i class="icon-park-outline-upload"></i>
</template>
上传
<ani-upload></ani-upload>
<a-button type="outline" status="danger" :disabled="!selected.length" @click="onDeleteMany">
批量删除
</a-button>
<ani-upload ref="uploadRef"></ani-upload>
</template>
</Table>
<a-image-preview v-model:visible="visible" :src="image"></a-image-preview>
</div>
</div>
</BreadPage>
@ -22,29 +19,27 @@
<script setup lang="tsx">
import { api } from "@/api";
import { Table, useAniFormModal, useTable } from "@/components";
import { dayjs } from "@/libs/dayjs";
import { Table, createColumn, updateColumn, useTable } from "@/components";
import { delConfirm } from "@/utils";
import numeral from "numeral";
import AniGroup from './components/group.vue';
import AniGroup from "./components/group.vue";
import AniUpload from "./components/upload.vue";
const [typeModal, typeCtx] = useAniFormModal({
title: "修改分组",
trigger: false,
modalProps: {
width: 432,
},
items: [
{
field: "name",
label: "分组名称",
type: "input",
},
],
submit: async () => {},
});
const visible = ref(false);
const image = ref("");
const selected = ref<number[]>([]);
const preview = (record: any) => {
if (!record.mimetype.startsWith("image")) {
window.open(record.path, "_blank");
return;
}
image.value = record.path;
visible.value = true;
};
const uploadRef = ref<InstanceType<typeof AniUpload>>();
const onDeleteMany = async () => {
await delConfirm();
};
const getIcon = (mimetype: string) => {
if (mimetype.startsWith("image")) {
@ -66,32 +61,45 @@ const table = useTable({
data: async (model, paging) => {
return api.file.getFiles();
},
tableProps: {
rowSelection: {
showCheckedAll: true,
},
onSelectionChange(rowKeys) {
selected.value = rowKeys as number[];
},
},
columns: [
{
title: "文件名称",
dataIndex: "name",
render({ record }) {
return (
<div class="flex items-center">
<div class="flex items-center gap-2">
<div>
<i class={`${getIcon(record.mimetype)} text-3xl mr-2`}></i>
{record.mimetype.startsWith("image") ? (
<a-avatar size={32} shape="square">
<img src={record.path}></img>
</a-avatar>
) : (
<i class={`${getIcon(record.mimetype)} text-3xl mr-2`}></i>
)}
</div>
<div class="flex flex-col overflow-hidden">
<span>{record.name}</span>
<span class="text-gray-400 text-xs truncate">
{numeral(record.size).format("0 b")}
<span
class="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>
</div>
</div>
);
},
},
{
title: "上传时间",
dataIndex: "createdAt",
width: 200,
render: ({ record }) => dayjs(record.createdAt).format(),
},
createColumn,
updateColumn,
{
type: "button",
title: "操作",
@ -118,7 +126,8 @@ const table = useTable({
field: "name",
label: "文件名称",
type: "search",
enableLoad: true,
searchable: true,
enterable: true,
itemProps: {
hideLabel: true,
},
@ -128,6 +137,28 @@ const table = useTable({
},
],
},
modify: {
title: "修改素材",
modalProps: {
width: 580,
},
items: [
{
field: "name",
label: "名称",
type: "input",
},
{
field: "description",
label: "描述",
type: "textarea",
},
],
submit: ({ model }) => {
console.log(model);
return api.file.setFile(model.id, model);
},
},
});
</script>

View File

@ -0,0 +1,127 @@
<template>
<div class="w-[210px] h-full overflow-hidden grid grid-rows-[auto_1fr]">
<div class="flex gap-2">
<a-input-search allow-clear placeholder="字典类型" class="mb-2"></a-input-search>
<a-button @click="formCtx.open">
<template #icon>
<i class="icon-park-outline-add"></i>
</template>
</a-button>
<form-modal></form-modal>
</div>
<a-scrollbar outer-class="h-full overflow-hidden" class="h-full overflow-auto">
<ul class="pl-0 mt-0">
<li
v-for="item in list"
:key="item.code"
:class="{ active: item.id === current?.id }"
class="group flex items-center justify-between gap-1 h-8 rounded mb-2 pl-3 hover:bg-gray-100 cursor-pointer"
>
<div class="flex-1 h-full flex items-center gap-2 overflow-hidden" @click="emit('change', item)">
<i class="icon-park-outline-folder-close align-[-2px]"></i>
<span class="flex-1 truncate">{{ item.name }}</span>
</div>
<div class="">
<a-dropdown>
<a-button size="small" type="text">
<template #icon>
<i class="icon-park-outline-more-one text-gray-400 hover:text-gray-700"></i>
</template>
</a-button>
<template #content>
<a-doption @click="formCtx.open(item)">
<template #icon>
<i class="icon-park-outline-edit"></i>
</template>
修改
</a-doption>
<a-doption class="!text-red-500" @click="onDeleteRow(item)">
<template #icon>
<i class="icon-park-outline-delete"></i>
</template>
删除
</a-doption>
</template>
</a-dropdown>
</div>
</li>
</ul>
</a-scrollbar>
</div>
</template>
<script setup lang="ts">
import { DictType, api } from "@/api";
import { useAniFormModal } from "@/components";
import { delConfirm } from "@/utils";
import { Message } from "@arco-design/web-vue";
import { PropType } from "vue";
defineProps({
current: {
type: Object as PropType<DictType>,
},
});
const emit = defineEmits(["change"]);
const list = ref<DictType[]>([]);
const updateDictTypes = async () => {
const res = await api.dictType.getDictTypes({ size: 0 });
list.value = res.data.data ?? [];
list.value.length && emit("change", list.value[0]);
};
onMounted(updateDictTypes);
const onDeleteRow = async (row: DictType) => {
await delConfirm();
const res = await api.dictType.delDictType(row.id);
Message.success(res.data.message);
};
const [formModal, formCtx] = useAniFormModal({
title: ({ model }) => (!model.id ? "新建字典类型" : "修改字典类型"),
trigger: false,
modalProps: {
width: 580,
},
model: {
id: undefined,
},
items: [
{
field: "name",
label: "名称",
type: "input",
},
{
field: "code",
label: "唯一编码",
type: "input",
},
{
field: "description",
label: "备注信息",
type: "textarea",
},
],
submit: async ({ model }) => {
let res;
if (model.id) {
res = await api.dictType.setDictType(model.id, model);
} else {
res = await api.dictType.addDictType(model);
}
updateDictTypes();
return res;
},
});
</script>
<style lang="less" scoped>
.active {
color: rgb(var(--primary-6));
background-color: rgb(var(--primary-1));
}
</style>

View File

@ -0,0 +1,139 @@
<template>
<div class="h-full w-full grid grid-rows-[auto_1fr] overflow-hidden">
<div class="py-2 px-4 bg-white">
<bread-crumb></bread-crumb>
</div>
<div class="grid grid-cols-[auto_1fr] gap-4 overflow-hidden bg-white p-4 m-4 rounded">
<div>
<ani-group :current="current" @change="onTypeChange"></ani-group>
</div>
<div>
<a-alert :show-icon="false" class="mb-3 !border-brand-500">
<span class="text-brand-500 font-bold">{{ current?.name }}</span>
<div class="mt-1">描述{{ current?.description }}</div>
</a-alert>
<dict-table></dict-table>
</div>
</div>
</div>
</template>
<script setup lang="tsx">
import { DictType, api } from "@/api";
import { createColumn, updateColumn, useAniTable } from "@/components";
import aniGroup from "./components/group.vue";
const current = ref<DictType>();
const onTypeChange = (item: DictType) => {
current.value = item;
dict.refresh();
};
const [dictTable, dict] = useAniTable({
async data(search, paging) {
return api.dict.getDicts({ ...search, ...paging, typeId: current.value?.id } as any);
},
columns: [
{
title: "字典项",
dataIndex: "name",
render: ({ record }) => {
return (
<div>
<div>
<span class="text-gray-900">{record.name}</span>: {record.code}
</div>
<div class="text-gray-400 text-xs">{record.description}</div>
</div>
);
},
},
createColumn,
updateColumn,
{
title: "操作",
type: "button",
width: 140,
buttons: [
{
type: "modify",
text: "修改",
},
{
type: "delete",
text: "删除",
onClick: ({ record }) => {
return api.dict.delDict(record.id);
},
},
],
},
],
search: {
button: false,
items: [
{
field: "name",
label: "名称",
type: "search",
searchable: true,
enterable: true,
nodeProps: {
placeholder: "字典名称",
},
itemProps: {
hideLabel: true,
},
},
],
},
create: {
title: '新增字典',
model: {
typeId: undefined,
},
modalProps: {
width: 580,
},
items: [
{
field: "name",
label: "字典名",
type: "input",
},
{
field: "code",
label: "字典值",
type: "input",
},
{
field: "description",
label: "备注",
type: "textarea",
},
],
submit: async ({ model }) => {
return api.dict.addDict({ ...model, typeId: current.value?.id });
},
},
modify: {
extend: true,
title: "修改字典",
submit: async ({ model }) => {
return api.dict.setDict(model.id, { ...model, typeId: current.value?.id });
},
},
});
</script>
<style lang="less" scoped></style>
<route lang="json">
{
"meta": {
"sort": 20010,
"title": "字典管理",
"icon": "icon-park-outline-spanner"
}
}
</route>

View File

@ -1,16 +1,11 @@
<template>
<BreadPage>
<div class="">
<div class="">
<a-alert :closable="true" class="mb-4"> 仅展示近 90 天内的数据如需查看更多数据请联系管理员 </a-alert>
<Table v-bind="table">
<template #action>
<a-button type="primary" @click="visible = true">添加</a-button>
<ani-editor v-model:visible="visible"></ani-editor>
</template>
</Table>
</div>
</div>
<Table v-bind="table">
<template #action>
<a-button type="primary" @click="visible = true">添加</a-button>
<ani-editor v-model:visible="visible"></ani-editor>
</template>
</Table>
</BreadPage>
</template>
@ -57,21 +52,46 @@ const table = useTable({
);
},
},
{
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>
);
},
},
{
title: "操作系统",
dataIndex: "os",
width: 160,
width: 200,
render({ record }) {
const [os, version] = record.os.split(" ");
return (
<div class="flex flex-col overflow-hidden">
<span>{os || "未知"}</span>
<span class="text-gray-400 text-xs truncate">{version}</span>
</div>
);
},
},
{
title: "浏览器",
dataIndex: "browser",
width: 160,
},
{
title: "登陆地址",
dataIndex: "ip",
width: 200,
render: ({ record }) => `${record.addr || "未知"}(${record.ip})`,
render({ record }) {
const [browser, version] = record.browser.split(" ");
return (
<div class="flex flex-col overflow-hidden">
<span>{browser || "未知"}</span>
<span class="text-gray-400 text-xs truncate">v{version}</span>
</div>
);
},
},
],
search: {

View File

@ -1,69 +1,87 @@
<template>
<bread-page class="">
<Table v-bind="table">
<menu-table>
<template #action>
<a-button type="outline">展开/折叠</a-button>
</template>
</Table>
</menu-table>
</bread-page>
</template>
<script setup lang="tsx">
import { api } from "@/api";
import { Table, createColumn, updateColumn, useTable } from "@/components";
import { createColumn, updateColumn, useAniTable } from "@/components";
import { MenuTypes, MenuType } from "@/constants/menu";
import { flatedMenus } from "@/router";
const menuArr = flatedMenus.map((i) => ({ label: i.title, value: i.id }));
const table = useTable({
const expanded = ref(false);
const toggleExpand = () => {
expanded.value = !expanded.value;
menu.tableRef.value?.tableRef?.expandAll(expanded.value);
};
const [menuTable, menu] = useAniTable({
data: (search, paging) => {
return api.menu.getMenus({ ...search, ...paging, tree: true });
},
tableProps: {
defaultExpandAllRows: true,
},
columns: [
{
title: "菜单名称",
title: () => {
return (
<span>
菜单名称
<a-link class="ml-1 select-none" onClick={toggleExpand}>
{expanded.value ? "收起全部" : "展开全部"}
</a-link>
</span>
);
},
dataIndex: "name",
width: 180,
},
{
title: "类型",
dataIndex: "description",
align: "center",
width: 120,
render: ({ record }) => (
<a-tag color={MenuTypes.fmt(record.type, "color")}>
{{
icon: <i class={record.icon}></i>,
default: () => MenuTypes.fmt(record.type),
}}
</a-tag>
),
},
{
title: "访问路径",
dataIndex: "path",
},
{
title: "启用",
dataIndex: "createdAt",
width: 80,
align: "center",
render: ({ record }) => <a-switch size="small" checked-color="#3c9"></a-switch>,
render({ record }) {
let id = "";
if (record.type === MenuType.PAGE) {
id = ` => ${record.path}`;
}
if (record.type === MenuType.BUTTON) {
id = ` => ${record.code}`;
}
return (
<div class="flex items-center gap-1">
<a-tag bordered color={MenuTypes.fmt(record.type, "color")}>
{{
default: () => MenuTypes.fmt(record.type),
}}
</a-tag>
<div class="flex-1 flex overflow-hidden ml-1">
<div class="flex-1">
<i class={`${record.icon} mr-1`}></i>
<span>{record.name ?? "无"}</span>
<span class="text-gray-400 text-xs truncate">{id}</span>
</div>
<a-switch checked-color="#3c9" size="small"></a-switch>
</div>
</div>
);
},
},
createColumn,
updateColumn,
{
title: "操作",
type: "button",
width: 184,
width: 200,
buttons: [
{
type: "modify",
text: "修改",
},
{
text: "新增下级",
text: "新增子项",
disabled: ({ record }) => record.type === MenuType.BUTTON,
onClick: ({ record }) => {
console.log(record);
@ -79,9 +97,6 @@ const table = useTable({
],
},
],
pagination: {
visible: false,
},
search: {
items: [
{
@ -104,26 +119,14 @@ const table = useTable({
class: "!grid grid-cols-2 gap-x-4",
},
items: [
{
field: "type",
initial: 1,
label: "类型",
type: "radio",
options: MenuTypes.raw,
nodeProps: {
type: "button",
class: "w-full",
},
},
{
field: "parentId",
initial: 0,
label: "父级",
type: "treeSelect",
async options(arg) {
async options() {
const res = await api.menu.getMenus({ size: 0, tree: true });
const data = res.data.data;
console.log(arg);
return [
{
id: 0,
@ -140,6 +143,17 @@ const table = useTable({
},
},
},
{
field: "type",
initial: 1,
label: "类型",
type: "radio",
options: MenuTypes.raw,
nodeProps: {
type: "button",
class: "w-full",
},
},
{
field: "name",
label: "名称",
@ -209,12 +223,18 @@ const table = useTable({
});
</script>
<style lang="less"></style>
<style lang="less">
.arco-table-cell-expand-icon {
span.arco-table-cell-inline-icon {
margin-right: 6px;
}
}
</style>
<route lang="json">
{
"meta": {
"sort": 10201,
"sort": 10302,
"title": "菜单管理",
"icon": "icon-park-outline-add-subtract"
}

View File

@ -1,118 +0,0 @@
<template>
<BreadPage>
<Table v-bind="table"></Table>
</BreadPage>
</template>
<script setup lang="tsx">
import { api } from "@/api";
import { Table, createColumn, updateColumn, useTable } from "@/components";
const table = useTable({
data: async (model, paging) => {
return api.permission.getPermissions();
},
columns: [
{
title: "权限名称",
dataIndex: "username",
width: 200,
render({ record }) {
return (
<div class="flex flex-col overflow-hidden">
<span>{record.name}</span>
<span class="text-gray-400 text-xs truncate">@{record.slug}</span>
</div>
);
},
},
{
title: "权限描述",
dataIndex: "description",
},
createColumn,
updateColumn,
{
title: "操作",
type: "button",
width: 110,
buttons: [
{
type: "modify",
text: "修改",
},
{
type: 'delete',
text: '删除',
}
],
},
],
search: {
items: [
{
field: "name",
label: "权限名称",
type: "input",
required: false,
nodeProps: {
placeholder: '请输入名称关键字'
},
itemProps: {
hideLabel: true,
}
},
],
},
create: {
title: "添加权限",
items: [
{
field: "name",
label: "角色名称",
type: "input",
required: true,
},
{
field: "slug",
label: "角色标识",
type: "input",
},
{
field: "description",
label: "个人描述",
type: "textarea",
},
],
modalProps: {
width: 580,
maskClosable: false,
},
formProps: {
layout: "vertical",
},
submit: ({ model }) => {
return api.permission.addPermission(model);
},
},
modify: {
extend: true,
title: "修改权限",
submit: ({ model }) => {
return api.permission.setPermission(model.id, model);
},
},
});
</script>
<style scoped></style>
<route lang="json">
{
"meta": {
"sort": 10303,
"title": "权限管理",
"icon": "icon-park-outline-permissions"
}
}
</route>

View File

@ -21,7 +21,7 @@ const [roleTable, roleCtx] = useAniTable({
return (
<div class="flex flex-col overflow-hidden">
<span>{record.name}</span>
<span class="text-gray-400 text-xs truncate">@{record.slug}</span>
<span class="text-gray-400 text-xs truncate">#{record.slug}</span>
</div>
);
},
@ -35,7 +35,7 @@ const [roleTable, roleCtx] = useAniTable({
{
title: "操作",
type: "button",
width: 184,
width: 200,
buttons: [
{
type: "modify",
@ -88,19 +88,19 @@ const [roleTable, roleCtx] = useAniTable({
required: true,
},
{
field: "slug",
field: "code",
label: "角色标识",
type: "input",
},
{
field: "permissionIds",
label: "关联权限",
type: "select",
options: () => api.permission.getPermissions(),
nodeProps: {
multiple: true,
},
},
// {
// field: "menuIds",
// label: "",
// type: "select",
// options: () => api.menu.getMenus({ size: 0 }),
// nodeProps: {
// multiple: true,
// },
// },
{
field: "description",
label: "个人描述",

View File

@ -14,10 +14,12 @@
@progress="onProgress"
>
<template #upload-button>
<a-link>选择文件...</a-link>
<a-link>选择文件</a-link>
<a-divider direction="vertical" :margin="4"></a-divider>
<a-link>上传文件</a-link>
</template>
</a-upload>
<div class="text-gray-400 text-xs">请选择不超过5MB.png, .jpg, .webp格式的图片</div>
<div class="text-gray-400 text-xs">请选择大小不超过5MB.png, .jpg, .webp格式的图片</div>
</div>
</div>
</template>

View File

@ -17,8 +17,7 @@ export const usePassworModal = () => {
field: "password",
label: ({ model }) => (
<span>
<span class="text-brand-500 font-semibold">{model.nickname}</span>
<span class="text-brand-500 font-semibold">{model.nickname}</span> :
</span>
),
type: "input",

View File

@ -8,9 +8,8 @@
<script setup lang="tsx">
import { api } from "@/api";
import { Table, createColumn, updateColumn, useTable } from "@/components";
import InputAvatar from "./avatar.vue";
import { usePassworModal } from "./password";
import { MenuType } from "@/constants/menu";
import InputAvatar from "./components/avatar.vue";
import { usePassworModal } from "./components/password";
const [passModal, passCtx] = usePassworModal();
@ -22,11 +21,10 @@ const table = useTable({
{
title: "用户昵称",
dataIndex: "username",
width: 180,
render: ({ record }) => (
<div class="flex items-center">
<a-avatar size={32}>
<img src={record.avatar} alt="" />
<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">
<span>{record.nickname}</span>
@ -35,10 +33,6 @@ const table = useTable({
</div>
),
},
{
title: "用户描述",
dataIndex: "description",
},
{
title: "用户邮箱",
dataIndex: "email",
@ -49,7 +43,7 @@ const table = useTable({
{
title: "操作",
type: "button",
width: 180,
width: 200,
buttons: [
{
type: "modify",
@ -74,25 +68,53 @@ const table = useTable({
search: {
button: true,
items: [
// {
// field: "nickname",
// label: "",
// type: "input",
// nodeProps: {
// placeholder: ''
// },
// itemProps: {
// hideLabel: true
// }
// },
{
field: "nickname",
label: "用户昵称",
type: "search",
searchable: true,
enterable: true,
itemProps: {
hideLabel: true,
},
nodeProps: {
placeholder: "用户昵称",
},
type: "input",
},
{
field: "nickname",
label: "用户昵称",
type: "input",
},
{
field: "nickname",
label: "用户昵称",
type: "input",
},
{
field: "nickname",
label: "用户昵称",
type: "input",
},
{
field: "nickname",
label: "用户昵称",
type: "input",
},
{
field: "nickname",
label: "用户昵称",
type: "input",
},
],
},
create: {
title: "新建用户",
modalProps: {
width: 732,
width: 820,
maskClosable: false,
},
formProps: {

View File

@ -1,4 +1,3 @@
import { LoginedUserVo } from "@/api";
import { defineStore } from "pinia";
export const useUserStore = defineStore({
@ -17,7 +16,7 @@ export const useUserStore = defineStore({
*
*/
nickname: "绝弹",
/** `
/** `
*
*/
avatar: "https://github.com/juetan.png",
@ -39,17 +38,21 @@ export const useUserStore = defineStore({
this.accessToken = token;
},
setAccessToken(token: string) {
this.accessToken = token;
},
/**
*
*/
clearUser() {
this.$reset()
this.$reset();
},
/**
*
*/
setUser(user: LoginedUserVo) {
setUser(user: any) {
this.id = user.id;
this.username = user.username;
this.nickname = user.nickname;

View File

@ -5,6 +5,20 @@
body {
// --border-radius-small: 4px;
.arco-icon-hover::before {
width: 28px;
height: 28px;
border-radius: var(--border-radius-small);
}
div.arco-dropdown {
border: none;
}
div.arco-divider-horizontal {
border-color: var(--color-neutral-2);
}
li.arco-dropdown-option {
line-height: 32px;
width: calc(100% - 8px);
@ -23,7 +37,7 @@ body {
overflow: hidden;
}
.arco-modal-header {
background: var(--color-fill-2);
background: var(--color-fill-3);
border-bottom: none;
}
.arco-modal-footer {
@ -59,7 +73,7 @@ body {
margin-top: 8px;
}
[class^="icon-"] {
font-size: 16px;
font-size: 14px;
vertical-align: -2px;
}
.arco-menu-item {
@ -80,7 +94,7 @@ body {
.arco-menu-inner {
padding: 0;
.arco-menu-icon {
margin-right: 8px;
margin-right: 10px;
}
.arco-menu-inline-header:hover {
background-color: var(--color-fill-2);
@ -121,6 +135,10 @@ body {
.arco-form-item-layout-inline:last-child {
margin-right: 0;
}
.ani-form-modal .arco-modal-body {
padding-bottom: 8px;
}
}
.dark {
.arco-menu-item.arco-menu-selected {

View File

@ -8,10 +8,14 @@
[class*=" icon-"],
[class^="icon-"] {
display: inline-block;
vertical-align: middle;
vertical-align: -2px;
}
.table .arco-form-item-layout-inline {
margin-right: 8px;
margin-bottom: 18px;
}
div.toolbar .arco-form-item-layout-inline {
margin-bottom: 0;
}

View File

@ -7,28 +7,22 @@ export {}
declare module '@vue/runtime-core' {
export interface GlobalComponents {
AAlert: typeof import('@arco-design/web-vue')['Alert']
AAutoComplete: typeof import('@arco-design/web-vue')['AutoComplete']
AAvatar: typeof import('@arco-design/web-vue')['Avatar']
ABreadcrumb: typeof import('@arco-design/web-vue')['Breadcrumb']
ABreadcrumbItem: typeof import('@arco-design/web-vue')['BreadcrumbItem']
AButton: typeof import('@arco-design/web-vue')['Button']
ACard: typeof import('@arco-design/web-vue')['Card']
ACheckbox: typeof import('@arco-design/web-vue')['Checkbox']
ACheckboxGroup: typeof import('@arco-design/web-vue')['CheckboxGroup']
AConfigProvider: typeof import('@arco-design/web-vue')['ConfigProvider']
ADatePicker: typeof import('@arco-design/web-vue')['DatePicker']
ADivider: typeof import('@arco-design/web-vue')['Divider']
ADoption: typeof import('@arco-design/web-vue')['Doption']
ADrawer: typeof import('@arco-design/web-vue')['Drawer']
ADropdown: typeof import('@arco-design/web-vue')['Dropdown']
ADropdownButton: typeof import('@arco-design/web-vue')['DropdownButton']
AEmpty: typeof import('@arco-design/web-vue')['Empty']
AForm: typeof import('@arco-design/web-vue')['Form']
AFormItem: typeof import('@arco-design/web-vue')['FormItem']
AImage: typeof import('@arco-design/web-vue')['Image']
AImagePreview: typeof import('@arco-design/web-vue')['ImagePreview']
AInput: typeof import('@arco-design/web-vue')['Input']
AInputNumber: typeof import('@arco-design/web-vue')['InputNumber']
AInputPassword: typeof import('@arco-design/web-vue')['InputPassword']
AInputSearch: typeof import('@arco-design/web-vue')['InputSearch']
ALayout: typeof import('@arco-design/web-vue')['Layout']
@ -36,28 +30,15 @@ declare module '@vue/runtime-core' {
ALayoutHeader: typeof import('@arco-design/web-vue')['LayoutHeader']
ALayoutSider: typeof import('@arco-design/web-vue')['LayoutSider']
ALink: typeof import('@arco-design/web-vue')['Link']
AList: typeof import('@arco-design/web-vue')['List']
AListItem: typeof import('@arco-design/web-vue')['ListItem']
AListItemMeta: typeof import('@arco-design/web-vue')['ListItemMeta']
AMenu: typeof import('@arco-design/web-vue')['Menu']
AMenuItem: typeof import('@arco-design/web-vue')['MenuItem']
AModal: typeof import('@arco-design/web-vue')['Modal']
APagination: typeof import('@arco-design/web-vue')['Pagination']
APopover: typeof import('@arco-design/web-vue')['Popover']
AProgress: typeof import('@arco-design/web-vue')['Progress']
ARadio: typeof import('@arco-design/web-vue')['Radio']
ARadioGroup: typeof import('@arco-design/web-vue')['RadioGroup']
AScrollbar: typeof import('@arco-design/web-vue')['Scrollbar']
ASelect: typeof import('@arco-design/web-vue')['Select']
ASpace: typeof import('@arco-design/web-vue')['Space']
ASpin: typeof import('@arco-design/web-vue')['Spin']
ASwitch: typeof import('@arco-design/web-vue')['Switch']
ATabPane: typeof import('@arco-design/web-vue')['TabPane']
ATabs: typeof import('@arco-design/web-vue')['Tabs']
ATag: typeof import('@arco-design/web-vue')['Tag']
ATextarea: typeof import('@arco-design/web-vue')['Textarea']
ATooltip: typeof import('@arco-design/web-vue')['Tooltip']
ATree: typeof import('@arco-design/web-vue')['Tree']
AUpload: typeof import('@arco-design/web-vue')['Upload']
BaseOption: typeof import('./../components/editor/components/BaseOption.vue')['default']
Block: typeof import('./../components/editor/panel-main/components/block.vue')['default']
@ -81,6 +62,7 @@ declare module '@vue/runtime-core' {
Render: typeof import('./../components/editor/blocks/date/render.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
'Temp.dev1': typeof import('./../components/breadcrumb/temp.dev1.vue')['default']
Texter: typeof import('./../components/editor/panel-main/components/texter.vue')['default']
Toast: typeof import('./../components/toast/toast.vue')['default']
}

View File

@ -20,11 +20,10 @@ const delConfirm = (config: DelOptions = {}) => {
if (typeof config === "string") {
config = { content: config };
}
const merged = merge(delOptions, config);
const merged = merge({}, delOptions, config);
return new Promise<void>((onOk: () => void, onCancel) => {
Modal.open({ ...merged, onOk, onCancel });
});
};
export { delConfirm };

View File

@ -1,3 +1,11 @@
/**
*
* @param list
* @param id ID key
* @param pid key
* @param cid key
* @returns
*/
export const listToTree = (list: any[], id = "id", pid = "parentId", cid = "children") => {
const map = list.reduce((res, v) => ((res[v[id]] = v), res), {});
return list.filter((item) => {
@ -10,11 +18,18 @@ export const listToTree = (list: any[], id = "id", pid = "parentId", cid = "chil
});
};
export function treeEach(tree: any[], fn: (item: any) => void) {
/**
*
* @param tree
* @param fn
* @param before 广
*/
export function treeEach(tree: any[], fn: (item: any) => void, before = true) {
for (const item of tree) {
fn(item);
before && fn(item);
if (item.children) {
treeEach(item.children, fn);
}
!before && fn(item);
}
}