mirror of
https://github.com/cool-team-official/cool-admin-vue.git
synced 2025-12-16 16:42:50 +00:00
545 lines
9.7 KiB
Vue
545 lines
9.7 KiB
Vue
<template>
|
||
<div class="cl-view-group" :class="[isExpand ? 'is-expand' : 'is-collapse']">
|
||
<div class="cl-view-group__wrap">
|
||
<!-- 左侧 -->
|
||
<div class="cl-view-group__left">
|
||
<slot name="left">
|
||
<div class="scope">
|
||
<div class="head">
|
||
<span>{{ config.label }}</span>
|
||
|
||
<el-tooltip content="刷新">
|
||
<el-icon @click="refresh()">
|
||
<icon-refresh />
|
||
</el-icon>
|
||
</el-tooltip>
|
||
|
||
<el-tooltip content="添加">
|
||
<el-icon
|
||
@click="edit()"
|
||
v-permission="config.service.permission.add"
|
||
>
|
||
<plus />
|
||
</el-icon>
|
||
</el-tooltip>
|
||
</div>
|
||
|
||
<div class="list" v-loading="loading">
|
||
<el-scrollbar>
|
||
<ul
|
||
v-infinite-scroll="onMore"
|
||
:infinite-scroll-immediate="false"
|
||
:infinite-scroll-disabled="loaded"
|
||
>
|
||
<li
|
||
v-for="(item, index) in list"
|
||
:key="index"
|
||
@click="select(item)"
|
||
@contextmenu="
|
||
(e) => {
|
||
onContextMenu(e, item);
|
||
}
|
||
"
|
||
>
|
||
<slot
|
||
name="item"
|
||
:item="item"
|
||
:selected="selected"
|
||
:index="index"
|
||
>
|
||
<div
|
||
class="item"
|
||
:class="{
|
||
'is-active': selected?.id == item.id
|
||
}"
|
||
>
|
||
<slot
|
||
name="item-name"
|
||
:item="item"
|
||
:selected="selected"
|
||
:index="index"
|
||
>
|
||
<span>{{ item.name }}</span>
|
||
</slot>
|
||
|
||
<el-icon
|
||
class="arrow-right"
|
||
v-show="selected?.id == item.id"
|
||
>
|
||
<arrow-right-bold />
|
||
</el-icon>
|
||
</div>
|
||
</slot>
|
||
</li>
|
||
|
||
<el-empty v-if="list.length == 0" :image-size="80" />
|
||
</ul>
|
||
</el-scrollbar>
|
||
</div>
|
||
</div>
|
||
</slot>
|
||
|
||
<!-- 收起按钮 -->
|
||
<div class="collapse-btn" @click="expand(false)" v-if="browser.isMini">
|
||
<el-icon>
|
||
<arrow-right />
|
||
</el-icon>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 右侧 -->
|
||
<div class="cl-view-group__right">
|
||
<div class="head">
|
||
<div class="icon" @click="expand()">
|
||
<el-icon v-if="isExpand"><arrow-left /></el-icon>
|
||
<el-icon v-else><arrow-right /></el-icon>
|
||
</div>
|
||
|
||
<slot name="title" :selected="selected">
|
||
<span class="title">
|
||
{{ config.title }}({{ selected?.name || "未选择" }})
|
||
</span>
|
||
</slot>
|
||
</div>
|
||
|
||
<div class="content" v-if="selected">
|
||
<slot name="right"></slot>
|
||
</div>
|
||
|
||
<el-empty :image-size="80" v-else />
|
||
</div>
|
||
</div>
|
||
|
||
<cl-form ref="Form" />
|
||
</div>
|
||
</template>
|
||
|
||
<script lang="ts" name="cl-view-group" setup>
|
||
import { inject, nextTick, onMounted, reactive, ref, useSlots } from "vue";
|
||
import {
|
||
ArrowLeft,
|
||
ArrowRight,
|
||
ArrowRightBold,
|
||
Refresh as IconRefresh,
|
||
Plus
|
||
} from "@element-plus/icons-vue";
|
||
import { useBrowser } from "/@/cool";
|
||
import { ContextMenu, useForm, setFocus } from "@cool-vue/crud";
|
||
import { ElMessage, ElMessageBox } from "element-plus";
|
||
import { ClViewGroup } from "./hook";
|
||
import { isEmpty } from "lodash-es";
|
||
|
||
const { browser, onScreenChange } = useBrowser();
|
||
const slots = useSlots();
|
||
const Form = useForm();
|
||
|
||
// 配置
|
||
const config = reactive(
|
||
Object.assign(
|
||
{
|
||
label: "组",
|
||
title: "列表",
|
||
leftWidth: "300px",
|
||
service: {}
|
||
},
|
||
inject("useViewGroup__options")
|
||
)
|
||
) as ClViewGroup.Options;
|
||
|
||
// 左侧内容是否自定义
|
||
const isCustom = !!slots.left;
|
||
|
||
if (isEmpty(config.service) && !isCustom) {
|
||
console.error("[cl-view-group] 参数 service 不能为空");
|
||
}
|
||
|
||
// 加载中
|
||
const loading = ref(false);
|
||
|
||
// 列表
|
||
const list = ref<ClViewGroup.Item[]>([]);
|
||
|
||
// 是否展开
|
||
const isExpand = ref(true);
|
||
|
||
// 选中值
|
||
const selected = ref<ClViewGroup.Item>();
|
||
|
||
// 收起、展开
|
||
function expand(value?: boolean) {
|
||
isExpand.value = value === undefined ? !isExpand.value : value;
|
||
}
|
||
|
||
// 设置选中值
|
||
function select(data?: ClViewGroup.Item) {
|
||
if (!data) {
|
||
data = list.value[0];
|
||
}
|
||
|
||
selected.value = data;
|
||
|
||
nextTick(() => {
|
||
if (data) {
|
||
if (browser.isMini) {
|
||
expand(false);
|
||
}
|
||
|
||
if (config.onSelect) {
|
||
config.onSelect(data);
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
// 编辑
|
||
function edit(item?: ClViewGroup.Item) {
|
||
Form.value?.open(
|
||
{
|
||
title: (item ? "编辑" : "添加") + config.label,
|
||
form: {
|
||
...item
|
||
},
|
||
on: {
|
||
submit(data, { close, done }) {
|
||
config.service[item ? "update" : "add"](data)
|
||
.then(() => {
|
||
ElMessage.success("保存成功");
|
||
|
||
if (item) {
|
||
Object.assign(item, data);
|
||
} else {
|
||
refresh();
|
||
}
|
||
|
||
close();
|
||
})
|
||
.catch((err) => {
|
||
ElMessage.error(err.message);
|
||
done();
|
||
});
|
||
}
|
||
},
|
||
...config.onEdit?.(item)
|
||
},
|
||
[setFocus()]
|
||
);
|
||
}
|
||
|
||
// 删除
|
||
function remove(item: ClViewGroup.Item) {
|
||
ElMessageBox.confirm("此操作将会删除选择的数据,是否继续?", "提示", {
|
||
type: "warning"
|
||
})
|
||
.then(() => {
|
||
config.service
|
||
.delete({
|
||
ids: [item.id]
|
||
})
|
||
.then(async () => {
|
||
ElMessage.success("删除成功");
|
||
|
||
// 刷新列表
|
||
await refresh();
|
||
|
||
// 删除当前
|
||
if (selected.value?.id == item.id) {
|
||
select();
|
||
}
|
||
})
|
||
.catch((err) => {
|
||
ElMessage.error(err.message);
|
||
});
|
||
})
|
||
.catch(() => null);
|
||
}
|
||
|
||
// 请求参数
|
||
const reqParams = {
|
||
order: "createTime",
|
||
sort: "asc",
|
||
page: 1,
|
||
size: 20
|
||
};
|
||
|
||
// 是否加载完
|
||
const loaded = ref(false);
|
||
|
||
// 刷新
|
||
async function refresh(params?: any) {
|
||
Object.assign(reqParams, params);
|
||
|
||
loading.value = true;
|
||
|
||
await config.service
|
||
.page(reqParams)
|
||
.then((res) => {
|
||
const arr = config.onData?.(res.list) || res.list;
|
||
|
||
if (reqParams.page == 1) {
|
||
list.value = arr;
|
||
} else {
|
||
list.value.push(...arr);
|
||
}
|
||
|
||
if (!selected.value) {
|
||
select(list.value[0]);
|
||
}
|
||
|
||
loaded.value = res.pagination.total <= list.value.length;
|
||
})
|
||
.catch((err) => {
|
||
ElMessage.error(err.message);
|
||
});
|
||
|
||
loading.value = false;
|
||
}
|
||
|
||
// 加载更多
|
||
function onMore() {
|
||
refresh({
|
||
page: reqParams.page + 1
|
||
});
|
||
}
|
||
|
||
// 右键菜单
|
||
function onContextMenu(e: any, item: ClViewGroup.Item) {
|
||
ContextMenu.open(e, {
|
||
hover: {
|
||
target: "item"
|
||
},
|
||
list: [
|
||
{
|
||
label: "编辑",
|
||
hidden: !config.service._permission.update,
|
||
callback(done) {
|
||
done();
|
||
edit(item);
|
||
}
|
||
},
|
||
{
|
||
label: "删除",
|
||
hidden: !config.service._permission.delete,
|
||
callback(done) {
|
||
done();
|
||
remove(item);
|
||
}
|
||
}
|
||
],
|
||
...(config.onContextMenu && config.onContextMenu(item))
|
||
});
|
||
}
|
||
|
||
// 监听屏幕变化
|
||
onScreenChange(() => {
|
||
expand(!browser.isMini);
|
||
});
|
||
|
||
defineExpose({
|
||
selected,
|
||
isExpand,
|
||
expand,
|
||
select,
|
||
browser,
|
||
edit,
|
||
remove
|
||
});
|
||
|
||
onMounted(() => {
|
||
if (!isCustom) {
|
||
refresh();
|
||
}
|
||
});
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
.cl-view-group {
|
||
height: 100%;
|
||
width: 100%;
|
||
|
||
$left-width: v-bind("config.leftWidth");
|
||
$bg: var(--el-bg-color);
|
||
|
||
&__wrap {
|
||
display: flex;
|
||
height: 100%;
|
||
width: 100%;
|
||
position: relative;
|
||
}
|
||
|
||
&__left {
|
||
position: relative;
|
||
height: 100%;
|
||
width: $left-width;
|
||
background-color: $bg;
|
||
|
||
.collapse-btn {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
position: absolute;
|
||
right: 20px;
|
||
bottom: 30px;
|
||
height: 40px;
|
||
width: 40px;
|
||
border-radius: 100%;
|
||
background-color: var(--color-primary);
|
||
box-shadow: 0 0 10px 1px rgba(0, 0, 0, 0.3);
|
||
|
||
.el-icon {
|
||
color: #fff;
|
||
font-size: 24px;
|
||
}
|
||
}
|
||
|
||
.scope {
|
||
height: 100%;
|
||
width: 100%;
|
||
box-sizing: border-box;
|
||
white-space: nowrap;
|
||
|
||
.head {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
height: 40px;
|
||
font-size: 14px;
|
||
padding: 0 10px;
|
||
|
||
& > span {
|
||
&:nth-child(1) {
|
||
flex: 1;
|
||
}
|
||
}
|
||
|
||
.el-icon {
|
||
padding: 5px;
|
||
font-size: 18px;
|
||
margin-left: 5px;
|
||
cursor: pointer;
|
||
|
||
&:hover {
|
||
background-color: var(--el-fill-color-lighter);
|
||
}
|
||
}
|
||
}
|
||
|
||
.list {
|
||
height: calc(100% - 40px);
|
||
box-sizing: border-box;
|
||
|
||
ul {
|
||
height: 100%;
|
||
|
||
li {
|
||
.item {
|
||
display: flex;
|
||
align-items: center;
|
||
list-style: none;
|
||
box-sizing: border-box;
|
||
padding: 10px 35px 10px 10px;
|
||
margin: 0 10px;
|
||
cursor: pointer;
|
||
font-size: 13px;
|
||
margin-bottom: 10px;
|
||
border-radius: 3px;
|
||
color: #666;
|
||
position: relative;
|
||
background-color: #f7f7f7;
|
||
|
||
.arrow-right {
|
||
position: absolute;
|
||
right: 10px !important;
|
||
}
|
||
|
||
&.is-active {
|
||
background-color: var(--color-primary);
|
||
color: #fff;
|
||
}
|
||
|
||
&:hover {
|
||
opacity: 0.8;
|
||
}
|
||
}
|
||
}
|
||
|
||
&::after {
|
||
display: block;
|
||
content: "";
|
||
height: 1px;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
&__right {
|
||
display: flex;
|
||
flex-direction: column;
|
||
position: absolute;
|
||
right: 0;
|
||
top: 0;
|
||
height: 100%;
|
||
width: 100%;
|
||
transition: width 0.3s;
|
||
background-color: $bg;
|
||
|
||
.head {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
height: 40px;
|
||
position: relative;
|
||
font-size: 14px;
|
||
|
||
.title {
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.icon {
|
||
display: flex;
|
||
align-items: center;
|
||
position: absolute;
|
||
left: 0;
|
||
top: 0;
|
||
font-size: 18px;
|
||
cursor: pointer;
|
||
height: 100%;
|
||
width: 80px;
|
||
padding-left: 10px;
|
||
}
|
||
}
|
||
|
||
.content {
|
||
height: calc(100% - 40px);
|
||
}
|
||
}
|
||
|
||
&.is-expand {
|
||
.cl-view-group__right {
|
||
width: calc(100% - $left-width);
|
||
border-left: 1px solid var(--el-border-color);
|
||
}
|
||
}
|
||
|
||
@media only screen and (max-width: 768px) {
|
||
.cl-view-group__left {
|
||
overflow: hidden;
|
||
transition: width 0.2s;
|
||
width: 0;
|
||
z-index: 9;
|
||
}
|
||
|
||
.cl-view-group__right {
|
||
width: 100% !important;
|
||
}
|
||
|
||
&.is-expand {
|
||
.cl-view-group__left {
|
||
width: 100%;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
</style>
|