mirror of
https://github.com/yangjian102621/geekai.git
synced 2025-11-13 20:53:47 +08:00
fix conflicts
This commit is contained in:
37
new-ui/projects/vue-admin/src/components/ConfirmSwitch.vue
Normal file
37
new-ui/projects/vue-admin/src/components/ConfirmSwitch.vue
Normal file
@@ -0,0 +1,37 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed } from "vue";
|
||||
import { Message, type SwitchInstance } from "@arco-design/web-vue";
|
||||
import type { BaseResponse } from "@gpt-vue/packages/type";
|
||||
|
||||
type OriginProps = SwitchInstance["$props"];
|
||||
|
||||
interface Props extends /* @vue-ignore */ OriginProps {
|
||||
modelValue: boolean | string | number;
|
||||
api: (params?: any) => Promise<BaseResponse<any>>;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
|
||||
const emits = defineEmits(["update:modelValue"]);
|
||||
|
||||
const _value = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (v) => {
|
||||
emits("update:modelValue", v);
|
||||
},
|
||||
});
|
||||
|
||||
const onBeforeChange = async (params) => {
|
||||
try {
|
||||
await props.api({ ...params, value: !_value.value });
|
||||
Message.success("操作成功");
|
||||
return true;
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<a-switch v-bind="{ ...props, ...$attrs }" v-model="_value" :before-change="onBeforeChange" />
|
||||
</template>
|
||||
132
new-ui/projects/vue-admin/src/components/CustomLayout.vue
Normal file
132
new-ui/projects/vue-admin/src/components/CustomLayout.vue
Normal file
@@ -0,0 +1,132 @@
|
||||
<script lang="ts" setup>
|
||||
import { IconDown, IconExport } from "@arco-design/web-vue/es/icon";
|
||||
import { useAuthStore } from "@/stores/auth";
|
||||
import useState from "@/composables/useState";
|
||||
import Logo from "/images/logo.png";
|
||||
import avatar from "/images/user-info.jpg";
|
||||
import donateImg from "/images/wechat-pay.png";
|
||||
|
||||
import SystemMenu from "./SystemMenu.vue";
|
||||
import PageWrapper from "./PageWrapper.vue";
|
||||
|
||||
const logoWidth = "200px";
|
||||
const authStore = useAuthStore();
|
||||
const [visible, setVisible] = useState(false);
|
||||
</script>
|
||||
<template>
|
||||
<ALayout class="custom-layout">
|
||||
<ALayoutHeader class="custom-layout-header">
|
||||
<div class="logo">
|
||||
<img :src="Logo" alt="logo" />
|
||||
<span>ChatPlus 控制台</span>
|
||||
</div>
|
||||
<div class="action">
|
||||
<ADropdown>
|
||||
<ASpace align="center" :size="4">
|
||||
<a-avatar class="user-avatar" :size="30">
|
||||
<img :src="avatar" />
|
||||
</a-avatar>
|
||||
<IconDown />
|
||||
</ASpace>
|
||||
<template #content>
|
||||
<a
|
||||
class="dropdown-link"
|
||||
href="https://github.com/yangjian102621/chatgpt-plus"
|
||||
target="_blank"
|
||||
>
|
||||
<ADoption value="1">
|
||||
<template #icon>
|
||||
<icon-github />
|
||||
</template>
|
||||
<span>ChatPlus-AI 创作系统</span>
|
||||
</ADoption>
|
||||
</a>
|
||||
<ADoption value="2" @click="setVisible(true)">
|
||||
<template #icon>
|
||||
<icon-wechatpay />
|
||||
</template>
|
||||
<span>打赏作者</span>
|
||||
</ADoption>
|
||||
</template>
|
||||
<template #footer>
|
||||
<APopconfirm content="确认退出?" position="br" @ok="authStore.logout">
|
||||
<ASpace align="center" class="logout-area">
|
||||
<IconExport size="16" />
|
||||
<span>退出</span>
|
||||
</ASpace>
|
||||
</APopconfirm>
|
||||
</template>
|
||||
</ADropdown>
|
||||
</div>
|
||||
</ALayoutHeader>
|
||||
<ALayout>
|
||||
<SystemMenu :width="logoWidth" />
|
||||
<ALayoutContent>
|
||||
<PageWrapper>
|
||||
<slot />
|
||||
</PageWrapper>
|
||||
</ALayoutContent>
|
||||
</ALayout>
|
||||
</ALayout>
|
||||
<a-modal
|
||||
v-model:visible="visible"
|
||||
class="donate-dialog"
|
||||
width="400px"
|
||||
title="请作者喝杯咖啡"
|
||||
:footer="false"
|
||||
>
|
||||
<a-alert :closable="false" :show-icon="false">
|
||||
如果你觉得这个项目对你有帮助,并且情况允许的话,可以请作者喝杯咖啡,非常感谢你的支持~
|
||||
</a-alert>
|
||||
<p>
|
||||
<a-image :src="donateImg" />
|
||||
</p>
|
||||
</a-modal>
|
||||
</template>
|
||||
<style lang="less" scoped>
|
||||
.custom-layout {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
&-header {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 60px;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid var(--color-neutral-2);
|
||||
.logo {
|
||||
display: flex;
|
||||
width: v-bind("logoWidth");
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
img {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
}
|
||||
}
|
||||
.action {
|
||||
display: flex;
|
||||
padding: 0 12px;
|
||||
flex: 1;
|
||||
justify-content: right;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
.dropdown-link {
|
||||
text-decoration: none;
|
||||
}
|
||||
.donate-dialog {
|
||||
p {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
.logout-area {
|
||||
padding: 8px 0;
|
||||
display: flex;
|
||||
width: 80px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
</style>
|
||||
23
new-ui/projects/vue-admin/src/components/PageWrapper.vue
Normal file
23
new-ui/projects/vue-admin/src/components/PageWrapper.vue
Normal file
@@ -0,0 +1,23 @@
|
||||
<template>
|
||||
<div class="page-wrapper">
|
||||
<div class="page-content">
|
||||
<RouterView />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<style scoped>
|
||||
.page-wrapper {
|
||||
height: calc(100vh - 60px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: #f7f8fa;
|
||||
overflow: hidden;
|
||||
}
|
||||
.page-content {
|
||||
margin: 12px;
|
||||
padding: 12px;
|
||||
flex: 1;
|
||||
background: #fff;
|
||||
overflow-y: auto;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,113 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref, type PropType } from "vue";
|
||||
import { getDefaultFormData, useComponentConfig } from "./utils";
|
||||
import { ValueType } from "./type.d";
|
||||
import type { SearchTableColumns, SearchColumns } from "./type";
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
columns: {
|
||||
type: Array as PropType<SearchTableColumns[]>,
|
||||
default: () => [],
|
||||
},
|
||||
submitting: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
const emits = defineEmits(["update:modelValue", "request"]);
|
||||
|
||||
const size = "small";
|
||||
|
||||
const collapsed = ref(false);
|
||||
|
||||
const formData = computed({
|
||||
get: () => props.modelValue,
|
||||
set(value) {
|
||||
emits("update:modelValue", value);
|
||||
},
|
||||
});
|
||||
|
||||
const searchColumns = computed(() => {
|
||||
return props.columns?.filter((item) => item.dataIndex && item.search) as (SearchColumns & {
|
||||
dataIndex: string;
|
||||
})[];
|
||||
});
|
||||
|
||||
const optionsEvent = {
|
||||
onReset: () => {
|
||||
formData.value = getDefaultFormData(props.columns);
|
||||
emits("request");
|
||||
},
|
||||
onSearch: () => emits("request"),
|
||||
onCollapse: (value: boolean) => {
|
||||
collapsed.value = value ?? !collapsed.value;
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<AForm
|
||||
v-if="searchColumns?.length"
|
||||
class="search-form-conteiner"
|
||||
:model="formData"
|
||||
:size="size"
|
||||
:label-col-props="{ span: 0 }"
|
||||
:wrapper-col-props="{ span: 24 }"
|
||||
@submit="optionsEvent.onSearch"
|
||||
>
|
||||
<AGrid
|
||||
:cols="{ md: 1, lg: 3, xl: 4, xxl: 5 }"
|
||||
:row-gap="12"
|
||||
:col-gap="12"
|
||||
:collapsed="collapsed"
|
||||
>
|
||||
<AGridItem
|
||||
v-for="item in searchColumns"
|
||||
:key="item.dataIndex"
|
||||
style="transition: all 0.3s ease-in-out"
|
||||
>
|
||||
<AFormItem :field="item.dataIndex" :label="item.title as string">
|
||||
<slot :name="item.search.slotsName">
|
||||
<component
|
||||
v-model="formData[item.dataIndex]"
|
||||
:is="ValueType[item.search.valueType ?? 'input'] ?? item.search.render"
|
||||
v-bind="useComponentConfig(size, item)"
|
||||
/>
|
||||
</slot>
|
||||
</AFormItem>
|
||||
</AGridItem>
|
||||
<AGridItem>
|
||||
<ASpace>
|
||||
<slot name="search-options" :option="optionsEvent">
|
||||
<AButton type="primary" html-type="submit" :size="size" :loading="submitting">
|
||||
<icon-search />
|
||||
<span>查询</span>
|
||||
</AButton>
|
||||
<AButton :size="size" @click="optionsEvent.onReset" :loading="submitting">
|
||||
<icon-refresh />
|
||||
<span>重置</span>
|
||||
</AButton>
|
||||
</slot>
|
||||
</ASpace>
|
||||
</AGridItem>
|
||||
<AGridItem suffix>
|
||||
<ASpace class="flex-end">
|
||||
<slot name="search-extra" />
|
||||
</ASpace>
|
||||
</AGridItem>
|
||||
</AGrid>
|
||||
</AForm>
|
||||
</template>
|
||||
<style scoped>
|
||||
.search-form-conteiner {
|
||||
padding: 8px 0px 0px;
|
||||
}
|
||||
.flex-end {
|
||||
display: flex;
|
||||
justify-content: end;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,89 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref, onActivated } from "vue";
|
||||
import useAsyncTable from "./useAsyncTable";
|
||||
import FormSection from "./FormSection.vue";
|
||||
import type { SearchTableProps } from "./type";
|
||||
import { useTableScroll, getDefaultFormData, useRequestParams } from "./utils";
|
||||
import { Message } from "@arco-design/web-vue";
|
||||
|
||||
const props = defineProps<SearchTableProps>();
|
||||
const formData = ref({ ...getDefaultFormData(props.columns) });
|
||||
const tableContainerRef = ref<HTMLElement>();
|
||||
|
||||
// 表格请求参数
|
||||
const requestParams = computed(() => ({
|
||||
...useRequestParams(props.columns, formData.value),
|
||||
...props.params,
|
||||
}));
|
||||
|
||||
const [tableConfig, getList] = useAsyncTable(props.request, requestParams);
|
||||
|
||||
const _columns = computed(() => {
|
||||
return props.columns
|
||||
.filter((item) => !item.hideInTable)
|
||||
.map((item) => ({
|
||||
ellipsis: true,
|
||||
tooltip: true,
|
||||
...item,
|
||||
}));
|
||||
});
|
||||
|
||||
const handleSearch = async (tips?: boolean) => {
|
||||
tips && Message.success("操作成功");
|
||||
await getList();
|
||||
};
|
||||
|
||||
onActivated(handleSearch);
|
||||
</script>
|
||||
<template>
|
||||
<div class="search-table">
|
||||
<div class="search-table-header">
|
||||
<div>
|
||||
<slot name="header-title">{{ props.headerTitle }}</slot>
|
||||
</div>
|
||||
<div class="header-option">
|
||||
<slot name="header-option" :formData="formData" :reload="handleSearch" />
|
||||
</div>
|
||||
</div>
|
||||
<FormSection
|
||||
v-model="formData"
|
||||
:columns="columns"
|
||||
:submitting="tableConfig.loading as boolean"
|
||||
@request="handleSearch"
|
||||
>
|
||||
<template v-for="slot in Object.keys($slots)" #[slot]="config">
|
||||
<slot :name="slot" v-bind="{ ...config, reload: handleSearch }" />
|
||||
</template>
|
||||
</FormSection>
|
||||
<div ref="tableContainerRef" class="search-table-container">
|
||||
<ATable
|
||||
v-bind="{
|
||||
...$attrs,
|
||||
...tableConfig,
|
||||
...props,
|
||||
scroll: useTableScroll(_columns, tableContainerRef as HTMLElement),
|
||||
columns: _columns,
|
||||
}"
|
||||
>
|
||||
<template v-for="slot in Object.keys($slots)" #[slot]="config">
|
||||
<slot :name="slot" v-bind="{ ...config, reload: handleSearch }" />
|
||||
</template>
|
||||
</ATable>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<style scoped>
|
||||
.search-table {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
.search-table-container {
|
||||
flex: 1;
|
||||
}
|
||||
.search-table-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
</style>
|
||||
49
new-ui/projects/vue-admin/src/components/SearchTable/type.d.ts
vendored
Normal file
49
new-ui/projects/vue-admin/src/components/SearchTable/type.d.ts
vendored
Normal file
@@ -0,0 +1,49 @@
|
||||
import type { Component } from "vue";
|
||||
import type { JsxElement } from "typescript";
|
||||
import {
|
||||
DatePicker,
|
||||
Input,
|
||||
InputNumber,
|
||||
RadioGroup,
|
||||
RangePicker,
|
||||
Select,
|
||||
Switch,
|
||||
type TableColumnData,
|
||||
} from "@arco-design/web-vue";
|
||||
import type { TableOriginalProps, TableRequest } from "./useAsyncTable";
|
||||
|
||||
type Object = Record<string, unknown>;
|
||||
|
||||
export enum ValueType {
|
||||
"input" = Input,
|
||||
"select" = Select,
|
||||
"number" = InputNumber,
|
||||
"date" = DatePicker,
|
||||
"range" = RangePicker,
|
||||
"radio" = RadioGroup,
|
||||
"switch" = Switch,
|
||||
}
|
||||
|
||||
export type SearchConfig = {
|
||||
valueType?: keyof typeof ValueType;
|
||||
fieldProps?: Object;
|
||||
render?: Component | JsxElement;
|
||||
slotsName?: string;
|
||||
defaultValue?: any;
|
||||
transform?: (value) => Record<string, any>;
|
||||
};
|
||||
|
||||
export interface SearchTableColumns extends TableColumnData {
|
||||
search?: SearchConfig;
|
||||
hideInTable?: boolean;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export type SearchColumns = SearchTableColumns & { search: SearchConfig };
|
||||
|
||||
export interface SearchTableProps extends /* @vue-ignore */ TableOriginalProps {
|
||||
request: TableRequest<Object>;
|
||||
params?: Object;
|
||||
columns: SearchTableColumns[];
|
||||
headerTitle?: string;
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
import { computed, onMounted, reactive, unref, type Ref } from "vue";
|
||||
import type { TableInstance } from "@arco-design/web-vue";
|
||||
import type { BaseResponse, ListResponse } from "@gpt-vue/packages/type";
|
||||
|
||||
export type TableOriginalProps = TableInstance["$props"];
|
||||
export type TableRequest<T extends Record<string, unknown>> = (params?: any) => Promise<BaseResponse<ListResponse<T>>>
|
||||
export type TableReturn = [TableOriginalProps, () => Promise<void>];
|
||||
function useAsyncTable<T extends Record<string, unknown>>(
|
||||
request: TableRequest<T>,
|
||||
params?: Ref<Record<string, unknown>>
|
||||
): TableReturn {
|
||||
const paginationState = reactive({
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
});
|
||||
|
||||
const tableState = reactive({
|
||||
loading: false,
|
||||
data: []
|
||||
})
|
||||
|
||||
const tableConfig = computed<TableOriginalProps>(() => {
|
||||
return {
|
||||
...tableState,
|
||||
rowKey: "id",
|
||||
pagination: {
|
||||
...paginationState,
|
||||
showTotal: true,
|
||||
showPageSize: true,
|
||||
},
|
||||
onPageChange: (page) => {
|
||||
paginationState.current = page;
|
||||
getTableData();
|
||||
},
|
||||
onPageSizeChange(pageSize) {
|
||||
paginationState.pageSize = pageSize;
|
||||
getTableData();
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const getTableData = async () => {
|
||||
tableState.loading = true
|
||||
try {
|
||||
const { data } = await request({
|
||||
...unref(params ?? {}),
|
||||
page: paginationState.current,
|
||||
page_size: paginationState.pageSize,
|
||||
});
|
||||
tableState.data = (data as any)?.items;
|
||||
paginationState.total = (data as any)?.total;
|
||||
} finally {
|
||||
tableState.loading = false
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(getTableData);
|
||||
|
||||
return [tableConfig, getTableData] as TableReturn;
|
||||
}
|
||||
|
||||
export default useAsyncTable;
|
||||
@@ -0,0 +1,53 @@
|
||||
import type { TableColumnData } from "@arco-design/web-vue";
|
||||
import type { SearchTableColumns, SearchColumns } from "./type";
|
||||
|
||||
export function useTableXScroll(columns: TableColumnData[]) {
|
||||
return columns.reduce((prev, curr) => {
|
||||
const width = curr.width ?? 150;
|
||||
return prev + width;
|
||||
}, 0);
|
||||
}
|
||||
|
||||
export function useTableScroll(columns: SearchTableColumns[], container?: HTMLElement) {
|
||||
const x = columns.reduce((prev, curr) => {
|
||||
const width = curr.hideInTable ? 0 : curr.width ?? 150;
|
||||
return prev + width;
|
||||
}, 0);
|
||||
const y = container?.clientHeight ?? undefined;
|
||||
return { x, y };
|
||||
}
|
||||
|
||||
export function getDefaultFormData(columns: SearchTableColumns[]) {
|
||||
return columns?.reduce((field, curr) => {
|
||||
if (curr.dataIndex && curr?.search?.defaultValue) {
|
||||
field[curr.dataIndex] = curr.search.defaultValue;
|
||||
}
|
||||
return field;
|
||||
}, {});
|
||||
}
|
||||
|
||||
export function useRequestParams(
|
||||
columns: SearchTableColumns[],
|
||||
originFormData: Record<string, any>
|
||||
) {
|
||||
const filterFormData = columns?.reduce((prev, curr) => {
|
||||
if (!curr.dataIndex || !curr.search) {
|
||||
return prev;
|
||||
}
|
||||
if (curr?.search?.transform) {
|
||||
const filters = curr.search.transform(originFormData[curr.dataIndex]);
|
||||
return Object.assign(prev, filters);
|
||||
}
|
||||
return Object.assign(prev, { [curr.dataIndex]: originFormData[curr.dataIndex] });
|
||||
}, {});
|
||||
return filterFormData as Record<string, any>;
|
||||
}
|
||||
|
||||
export function useComponentConfig(size: string, item: SearchColumns) {
|
||||
return {
|
||||
size,
|
||||
placeholder: item.search.valueType === "range" ? ["开始时间", "结束时间"] : item.title,
|
||||
allowClear: true,
|
||||
...(item.search.fieldProps ?? {}),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref, onActivated } from "vue";
|
||||
import useAsyncTable from "./useAsyncTable";
|
||||
import { useTableScroll } from "@/components/SearchTable/utils";
|
||||
import { Message } from "@arco-design/web-vue";
|
||||
import type { TableRequest, TableOriginalProps } from "./useAsyncTable";
|
||||
|
||||
interface SimpleTable extends /* @vue-ignore */ TableOriginalProps {
|
||||
request: TableRequest<Record<string, unknown>>;
|
||||
params?: Record<string, unknown>;
|
||||
columns?: TableOriginalProps["columns"];
|
||||
}
|
||||
|
||||
const props = defineProps<SimpleTable>();
|
||||
const tableContainerRef = ref<HTMLElement>();
|
||||
|
||||
// 表格请求参数
|
||||
const [tableConfig, getList] = useAsyncTable(props.request, props.params);
|
||||
|
||||
const _columns = computed(() => {
|
||||
return props.columns?.map((item) => ({
|
||||
ellipsis: true,
|
||||
tooltip: true,
|
||||
...item,
|
||||
}));
|
||||
});
|
||||
|
||||
const handleSearch = async (tips?: boolean) => {
|
||||
tips && Message.success("操作成功");
|
||||
await getList();
|
||||
};
|
||||
|
||||
onActivated(handleSearch);
|
||||
</script>
|
||||
<template>
|
||||
<div class="simple-table">
|
||||
<div class="simple-header">
|
||||
<a-space class="flex-end">
|
||||
<slot name="header" v-bind="{ reload: handleSearch }" />
|
||||
</a-space>
|
||||
</div>
|
||||
<div ref="tableContainerRef" class="simple-table-container">
|
||||
<ATable
|
||||
v-bind="{
|
||||
...$attrs,
|
||||
...tableConfig,
|
||||
...props,
|
||||
scroll: useTableScroll(_columns || [], tableContainerRef as HTMLElement),
|
||||
columns: _columns,
|
||||
}"
|
||||
>
|
||||
<template v-for="slot in Object.keys($slots)" #[slot]="config">
|
||||
<slot :name="slot" v-bind="{ ...config, reload: handleSearch }" />
|
||||
</template>
|
||||
</ATable>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<style scoped>
|
||||
.simple-table {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
.simple-table-container {
|
||||
flex: 1;
|
||||
}
|
||||
.simple-table-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.simple-header {
|
||||
padding: 8px 0px 16px;
|
||||
}
|
||||
.flex-end {
|
||||
display: flex;
|
||||
justify-content: end;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,43 @@
|
||||
import { computed, onMounted, reactive, unref } from "vue";
|
||||
import type { TableInstance } from "@arco-design/web-vue";
|
||||
import type { BaseResponse } from "@gpt-vue/packages/type";
|
||||
|
||||
export type TableOriginalProps = TableInstance["$props"];
|
||||
export type TableRequest<T extends Record<string, unknown>> = (
|
||||
params?: any
|
||||
) => Promise<BaseResponse<T[]>>;
|
||||
export type TableReturn = [TableOriginalProps, () => Promise<void>];
|
||||
function useAsyncTable<T extends Record<string, unknown>>(
|
||||
request: TableRequest<T>,
|
||||
params?: Record<string, unknown>
|
||||
): TableReturn {
|
||||
const tableState = reactive<{ loading: Boolean; data: T[] }>({
|
||||
loading: false,
|
||||
data: [],
|
||||
});
|
||||
|
||||
const tableConfig = computed<TableOriginalProps>(() => {
|
||||
return {
|
||||
...tableState,
|
||||
rowKey: "id",
|
||||
};
|
||||
});
|
||||
|
||||
const getTableData = async () => {
|
||||
tableState.loading = true;
|
||||
try {
|
||||
const { data } = await request({
|
||||
...unref(params ?? {}),
|
||||
});
|
||||
tableState.data = data as any;
|
||||
} finally {
|
||||
tableState.loading = false;
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(getTableData);
|
||||
|
||||
return [tableConfig, getTableData] as TableReturn;
|
||||
}
|
||||
|
||||
export default useAsyncTable;
|
||||
29
new-ui/projects/vue-admin/src/components/SystemMenu.vue
Normal file
29
new-ui/projects/vue-admin/src/components/SystemMenu.vue
Normal file
@@ -0,0 +1,29 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed } from "vue";
|
||||
import { useRoute } from "vue-router";
|
||||
import router from "@/router";
|
||||
import showMenu from "@/router/menu";
|
||||
|
||||
defineProps({
|
||||
width: {
|
||||
type: [Number, String],
|
||||
default: 200,
|
||||
},
|
||||
});
|
||||
const route = useRoute();
|
||||
const goto = (name: string) => router.push({ name });
|
||||
const selectedKeys = computed(() => [route.name]);
|
||||
|
||||
</script>
|
||||
<template>
|
||||
<ALayoutSider :style="{ width, height: '100%' }">
|
||||
<AMenu :selected-keys="selectedKeys" @menu-item-click="goto">
|
||||
<AMenuItem v-for="item in showMenu" :key="item.name">
|
||||
<template #icon>
|
||||
<component :is="item.meta?.icon" />
|
||||
</template>
|
||||
{{ item.meta.title }}
|
||||
</AMenuItem>
|
||||
</AMenu>
|
||||
</ALayoutSider>
|
||||
</template>
|
||||
Reference in New Issue
Block a user