Compare commits
10 Commits
b490b6c9c5
...
7f02e3bc97
| Author | SHA1 | Date |
|---|---|---|
|
|
7f02e3bc97 | |
|
|
7bea445253 | |
|
|
ce93e87e38 | |
|
|
2a55bc0fcc | |
|
|
751102f4ad | |
|
|
687f6250eb | |
|
|
1d572cf8e4 | |
|
|
1133555ca2 | |
|
|
09498ec02e | |
|
|
5b9c14184e |
2
.env
2
.env
|
|
@ -2,7 +2,7 @@
|
|||
# 应用配置
|
||||
# =====================================================================================
|
||||
# 网站标题
|
||||
VITE_TITLE = 绝弹管理后台
|
||||
VITE_TITLE = 绝弹项目管理
|
||||
# 网站副标题
|
||||
VITE_SUBTITLE = 快速开发web应用的模板工具
|
||||
# 接口前缀 说明:参见 axios 的 baseURL
|
||||
|
|
|
|||
|
|
@ -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 }}
|
||||
|
|
|
|||
13
Dockerfile
13
Dockerfile
|
|
@ -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
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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}>
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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)}
|
||||
</>
|
||||
) : (
|
||||
|
|
|
|||
|
|
@ -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 } });
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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: "个人描述",
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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",
|
||||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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']
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue