mirror of
https://github.com/KwiTsukasa/kt-template-online-web.git
synced 2026-05-27 16:35:47 +08:00
feat: migrate web component list UI
This commit is contained in:
parent
f439349933
commit
1aaafb75a3
@ -1,4 +1,4 @@
|
|||||||
NODE_ENV = depelopment
|
NODE_ENV = development
|
||||||
VITE_APP_PLAY_GROUND = http://192.168.52.164:5173
|
VITE_APP_PLAY_GROUND = "http://192.168.0.49:5173"
|
||||||
VITE_APP_PROXY = "http://192.168.1.206:48085/"
|
VITE_APP_PROXY = "http://192.168.0.49:48085/"
|
||||||
|
|
||||||
|
|||||||
@ -5,7 +5,7 @@
|
|||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" href="/favicon.ico" />
|
<link rel="icon" href="/favicon.ico" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>ShyTemplate - 模版库</title>
|
<title>KT-Template - 模版库</title>
|
||||||
<meta name="Keywords" content="echarts,Echarts,gallery,makeapie,make a pie,ppchart,PPChart" />
|
<meta name="Keywords" content="echarts,Echarts,gallery,makeapie,make a pie,ppchart,PPChart" />
|
||||||
<meta name="description" content="让图表更简单。PPChart 提供 Echarts 收录、图表制作等服务" />
|
<meta name="description" content="让图表更简单。PPChart 提供 Echarts 收录、图表制作等服务" />
|
||||||
</head>
|
</head>
|
||||||
|
|||||||
@ -10,15 +10,16 @@
|
|||||||
"deploy": "pnpm build && npx shy ftp deploy -f ./deploy"
|
"deploy": "pnpm build && npx shy ftp deploy -f ./deploy"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@ant-design/icons-vue": "^7.0.1",
|
||||||
|
"ant-design-vue": "^4.2.6",
|
||||||
"axios": "^1.1.3",
|
"axios": "^1.1.3",
|
||||||
"moment": "^2.29.4",
|
"dayjs": "^1.11.20",
|
||||||
"monaco-editor": "^0.34.1",
|
"monaco-editor": "^0.34.1",
|
||||||
"pinia": "^2.0.23",
|
"pinia": "^2.0.23",
|
||||||
"vue": "^3.2.41",
|
"vue": "^3.2.41",
|
||||||
"vue-router": "^4.1.6"
|
"vue-router": "^4.1.6"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@arco-design/web-vue": "^2.38.1",
|
|
||||||
"@types/node": "^18.11.9",
|
"@types/node": "^18.11.9",
|
||||||
"@vitejs/plugin-vue": "^3.2.0",
|
"@vitejs/plugin-vue": "^3.2.0",
|
||||||
"sass": "^1.56.0",
|
"sass": "^1.56.0",
|
||||||
|
|||||||
4416
pnpm-lock.yaml
4416
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
49
src/App.vue
49
src/App.vue
@ -1,41 +1,70 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { LayoutContent, Layout } from "@arco-design/web-vue";
|
import { ConfigProvider, Layout, LayoutContent } from "ant-design-vue";
|
||||||
|
import { onMounted } from "vue";
|
||||||
import Header from "@/modules/header/Header.vue";
|
import Header from "@/modules/header/Header.vue";
|
||||||
|
import { useTheme } from "@/hooks/useTheme";
|
||||||
|
|
||||||
|
const { initTheme, themeConfig } = useTheme();
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
initTheme();
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Layout class="!h-[calc(100%-60px)] !pt-60px">
|
<ConfigProvider :theme="themeConfig">
|
||||||
|
<Layout class="app-layout">
|
||||||
<Header></Header>
|
<Header></Header>
|
||||||
<LayoutContent class="content">
|
<LayoutContent class="content">
|
||||||
<router-view></router-view>
|
<router-view></router-view>
|
||||||
</LayoutContent>
|
</LayoutContent>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
</ConfigProvider>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
html,
|
html,
|
||||||
body {
|
body {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
margin: 0;
|
||||||
--color-logo: rgb(85, 26, 139);
|
--color-logo: rgb(85, 26, 139);
|
||||||
--color-logo-1: rgb(237, 221, 249);
|
--color-logo-1: rgb(237, 221, 249);
|
||||||
|
--app-bg: #f5f5f5;
|
||||||
|
--app-surface: #ffffff;
|
||||||
|
--app-border: #f0f0f0;
|
||||||
|
--app-text: rgba(0, 0, 0, 0.88);
|
||||||
|
--app-muted: rgba(0, 0, 0, 0.45);
|
||||||
|
--app-cover-bg: linear-gradient(180deg, #fafafa 0%, #f5f7fb 100%);
|
||||||
}
|
}
|
||||||
|
|
||||||
#app {
|
#app {
|
||||||
font-family: Avenir, Helvetica, Arial, sans-serif;
|
font-family: Avenir, Helvetica, Arial, sans-serif;
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
-moz-osx-font-smoothing: grayscale;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
color: #2c3e50;
|
color: var(--app-text);
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.content {
|
.app-layout {
|
||||||
background-color: var(--color-bg-5);
|
height: 100vh;
|
||||||
height: calc(100% - 60px);
|
padding-top: 56px;
|
||||||
|
background: var(--app-bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
body[arco-theme="dark"] {
|
.content {
|
||||||
--color-logo: var(--color-text-1);
|
background-color: var(--app-bg);
|
||||||
--color-logo-1: var(--color-bg-5);
|
height: calc(100vh - 56px);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
body[data-theme="dark"] {
|
||||||
|
--app-bg: #141414;
|
||||||
|
--app-surface: #1f1f1f;
|
||||||
|
--app-border: #303030;
|
||||||
|
--app-text: rgba(255, 255, 255, 0.85);
|
||||||
|
--app-muted: rgba(255, 255, 255, 0.45);
|
||||||
|
--app-cover-bg: #262626;
|
||||||
|
--color-logo: var(--app-text);
|
||||||
|
--color-logo-1: var(--app-bg);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|||||||
43
src/api/component.ts
Normal file
43
src/api/component.ts
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import { get, post } from "./request";
|
||||||
|
|
||||||
|
export interface ComponentListParams {
|
||||||
|
pageSize: number;
|
||||||
|
pageNo: number;
|
||||||
|
type?: string;
|
||||||
|
componentType?: string;
|
||||||
|
name?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ComponentItem {
|
||||||
|
id: string;
|
||||||
|
createTime: string;
|
||||||
|
componentTypeMsg: string;
|
||||||
|
name: string;
|
||||||
|
typeMsg: string;
|
||||||
|
image: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ComponentDetail {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
type: string | number;
|
||||||
|
componentType: string | number;
|
||||||
|
template: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ComponentListResult {
|
||||||
|
list: ComponentItem[];
|
||||||
|
total: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getComponentList = (params: ComponentListParams) => {
|
||||||
|
return get<ComponentListResult>("/component/list", { params });
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getComponentDetail = (id: string) => {
|
||||||
|
return get<ComponentDetail>("/component/detail", { params: { id } });
|
||||||
|
};
|
||||||
|
|
||||||
|
export const removeComponent = (id: string) => {
|
||||||
|
return post<null>("/component/remove", undefined, { params: { id } });
|
||||||
|
};
|
||||||
14
src/api/dict.ts
Normal file
14
src/api/dict.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { get } from "./request";
|
||||||
|
|
||||||
|
export interface DictItem {
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getComponentDictByType = (type: string) => {
|
||||||
|
return get<DictItem[]>("/dict/getComponentDictByType", { params: { type } });
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getDictByKey = (dictKey: string) => {
|
||||||
|
return get<DictItem[]>("/dict/getDictByKey", { params: { dictKey } });
|
||||||
|
};
|
||||||
25
src/api/request.ts
Normal file
25
src/api/request.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import axios, { AxiosRequestConfig } from "axios";
|
||||||
|
import config from "@/config";
|
||||||
|
|
||||||
|
export interface ApiResponse<T = any> {
|
||||||
|
code: number;
|
||||||
|
data: T;
|
||||||
|
msg: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const request = axios.create({
|
||||||
|
baseURL: config.axiosBase,
|
||||||
|
timeout: 1000 * 30,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const get = <T = any>(url: string, config?: AxiosRequestConfig) => {
|
||||||
|
return request.get<any, ApiResponse<T>>(url, config);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const post = <T = any>(url: string, data?: any, config?: AxiosRequestConfig) => {
|
||||||
|
return request.post<any, ApiResponse<T>>(url, data, config);
|
||||||
|
};
|
||||||
|
|
||||||
|
request.interceptors.response.use((response) => response.data);
|
||||||
|
|
||||||
|
export default request;
|
||||||
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<a href="/" class="logo"> ShyTemplate </a>
|
<a href="/" class="logo"> KT-Template </a>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
|
|||||||
36
src/hooks/useTheme.ts
Normal file
36
src/hooks/useTheme.ts
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import { computed, ref } from "vue";
|
||||||
|
import { theme as antTheme } from "ant-design-vue";
|
||||||
|
|
||||||
|
type ThemeType = "light" | "dark";
|
||||||
|
|
||||||
|
const theme = ref<ThemeType>("light");
|
||||||
|
|
||||||
|
const applyDocumentTheme = (value: ThemeType) => {
|
||||||
|
if (value === "dark") {
|
||||||
|
document.body.setAttribute("data-theme", "dark");
|
||||||
|
} else {
|
||||||
|
document.body.removeAttribute("data-theme");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const themeConfig = computed(() => ({
|
||||||
|
algorithm: theme.value === "dark" ? antTheme.darkAlgorithm : antTheme.defaultAlgorithm,
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const setTheme = (value: ThemeType) => {
|
||||||
|
theme.value = value;
|
||||||
|
localStorage.setItem("theme", value);
|
||||||
|
applyDocumentTheme(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const initTheme = () => {
|
||||||
|
const themeValue = (localStorage.getItem("theme") as ThemeType | null) || "light";
|
||||||
|
setTheme(themeValue);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useTheme = () => ({
|
||||||
|
theme,
|
||||||
|
themeConfig,
|
||||||
|
setTheme,
|
||||||
|
initTheme,
|
||||||
|
});
|
||||||
@ -5,7 +5,6 @@ import "virtual:uno.css";
|
|||||||
import App from "@/App.vue";
|
import App from "@/App.vue";
|
||||||
import { router } from "@/router.js";
|
import { router } from "@/router.js";
|
||||||
|
|
||||||
import "@arco-design/web-vue/dist/arco.css";
|
import "ant-design-vue/dist/reset.css";
|
||||||
|
|
||||||
createApp(App).use(router).use(createPinia()).mount("#app");
|
createApp(App).use(router).use(createPinia()).mount("#app");
|
||||||
|
|
||||||
|
|||||||
@ -1,17 +1,21 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { DescData, Message, Button, Empty } from "@arco-design/web-vue";
|
import { Button, Card, Descriptions, DescriptionsItem, Empty, message, Pagination, Popconfirm, Spin, Tooltip } from "ant-design-vue";
|
||||||
import { onBeforeUnmount, onMounted, reactive, ref } from "vue";
|
import { DeleteOutlined, ShareAltOutlined } from "@ant-design/icons-vue";
|
||||||
import axios from "axios";
|
import { onBeforeUnmount, onMounted, reactive } from "vue";
|
||||||
import moment from "moment";
|
import dayjs from "dayjs";
|
||||||
import Bus from "@/bus";
|
import Bus from "@/bus";
|
||||||
import { Spin, Card, CardGrid, CardMeta, Pagination, Descriptions, Popconfirm } from "@arco-design/web-vue";
|
|
||||||
import { IconShareInternal, IconDelete } from "@arco-design/web-vue/es/icon";
|
|
||||||
import config from "@/config";
|
import config from "@/config";
|
||||||
|
import { getComponentDetail, getComponentList, removeComponent } from "@/api/component";
|
||||||
|
|
||||||
enum HttpStatus {
|
enum HttpStatus {
|
||||||
OK = 200,
|
OK = 200,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface DescData {
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
||||||
const chartData = reactive<any>({
|
const chartData = reactive<any>({
|
||||||
chartList: [] as Array<{
|
chartList: [] as Array<{
|
||||||
title: string;
|
title: string;
|
||||||
@ -28,7 +32,7 @@ const chartData = reactive<any>({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const convertTime = (timeStr: string) => {
|
const convertTime = (timeStr: string) => {
|
||||||
return moment(timeStr).format("YYYY-MM-DD");
|
return dayjs(timeStr).format("YYYY-MM-DD");
|
||||||
};
|
};
|
||||||
|
|
||||||
const getData = () => {
|
const getData = () => {
|
||||||
@ -44,10 +48,9 @@ const getData = () => {
|
|||||||
if (chartData.type) {
|
if (chartData.type) {
|
||||||
params.type = chartData.type;
|
params.type = chartData.type;
|
||||||
}
|
}
|
||||||
axios
|
getComponentList(params)
|
||||||
.get(`${config.axiosBase}/component/list`, { params })
|
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
const { code, data, msg } = res.data;
|
const { code, data, msg } = res;
|
||||||
if (code === HttpStatus.OK) {
|
if (code === HttpStatus.OK) {
|
||||||
const { list, total } = data;
|
const { list, total } = data;
|
||||||
chartData.total = total;
|
chartData.total = total;
|
||||||
@ -65,7 +68,7 @@ const getData = () => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
Message.error(msg || "服务器开小差了,请稍后再试...");
|
message.error(msg || "服务器开小差了,请稍后再试...");
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
@ -88,9 +91,9 @@ onBeforeUnmount(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const chartClick = async (params: string) => {
|
const chartClick = async (params: string) => {
|
||||||
const res = await axios.get(`${config.axiosBase}/component/detail?id=${params}`);
|
const res = await getComponentDetail(params);
|
||||||
const { template, componentType, type, id, name } = res?.data?.data;
|
const { template, componentType, type, id, name } = res.data;
|
||||||
window.open(`${config.playground}/?id=${id}&name=${name}&componentType=${componentType}&type=${type}#${template}`);
|
window.open(buildPlaygroundUrl({ id, name, componentType, type, template }));
|
||||||
};
|
};
|
||||||
|
|
||||||
const pageChange = (pageIndex: number) => {
|
const pageChange = (pageIndex: number) => {
|
||||||
@ -99,10 +102,10 @@ const pageChange = (pageIndex: number) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const openTab = async (params: string) => {
|
const openTab = async (params: string) => {
|
||||||
const res = await axios.get(`${config.axiosBase}/component/detail?id=${params}`);
|
const res = await getComponentDetail(params);
|
||||||
const { template, componentType, type, id, name } = res?.data?.data;
|
const { template, componentType, type, id, name } = res.data;
|
||||||
|
|
||||||
const url = `${config.playground}/?id=${id}&name=${name}&componentType=${componentType}&type=${type}#${template}`;
|
const url = buildPlaygroundUrl({ id, name, componentType, type, template });
|
||||||
|
|
||||||
const tempInput = document.createElement("textarea");
|
const tempInput = document.createElement("textarea");
|
||||||
|
|
||||||
@ -123,70 +126,94 @@ const openTab = async (params: string) => {
|
|||||||
|
|
||||||
// 删除临时文本区域
|
// 删除临时文本区域
|
||||||
tempInput.remove();
|
tempInput.remove();
|
||||||
Message.success("分享链接已复制到剪贴板");
|
message.success("分享链接已复制到剪贴板");
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleAdd = () => {
|
const handleAdd = () => {
|
||||||
window.open(`${config.playground}?type=${chartData.type}&componentType=${chartData.componentType}`);
|
window.open(
|
||||||
|
buildPlaygroundUrl({
|
||||||
|
type: chartData.type,
|
||||||
|
componentType: chartData.componentType,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildPlaygroundUrl = (params: {
|
||||||
|
id?: string;
|
||||||
|
name?: string;
|
||||||
|
type?: string | number;
|
||||||
|
componentType?: string | number;
|
||||||
|
template?: string;
|
||||||
|
}) => {
|
||||||
|
const query = new URLSearchParams();
|
||||||
|
|
||||||
|
Object.entries(params).forEach(([key, value]) => {
|
||||||
|
if (key === "template" || value === undefined || value === null || value === "") return;
|
||||||
|
query.set(key, String(value));
|
||||||
|
});
|
||||||
|
|
||||||
|
const search = query.toString() ? `?${query.toString()}` : "";
|
||||||
|
const hash = params.template ? `#${params.template}` : "";
|
||||||
|
|
||||||
|
return `${config.playground}/${search}${hash}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleRemove = async (id: string) => {
|
const handleRemove = async (id: string) => {
|
||||||
try {
|
try {
|
||||||
const {
|
const { code, msg } = await removeComponent(id);
|
||||||
data: { code, msg },
|
if (code !== HttpStatus.OK) return message.error(msg);
|
||||||
} = await axios.post(`${config.axiosBase}/component/remove?id=${id}`);
|
message.success(msg);
|
||||||
if (code !== HttpStatus.OK) return Message.error(msg);
|
|
||||||
Message.success(msg);
|
|
||||||
getData();
|
getData();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return Message.error(error as any);
|
return message.error(error as any);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="w-full !h-full flex flex-col gap-8px overflow-hidden">
|
<div class="chart-list">
|
||||||
<div class="ml-auto">
|
<div class="chart-list_toolbar">
|
||||||
|
<div class="chart-list_total">共 {{ chartData.total }} 个模板</div>
|
||||||
<Button type="primary" @click="handleAdd"> 新增 </Button>
|
<Button type="primary" @click="handleAdd"> 新增 </Button>
|
||||||
</div>
|
</div>
|
||||||
<Spin class="flex-1 overflow-auto" :loading="chartData.loading" tip="加载中,请稍后...">
|
|
||||||
<Card :bordered="false">
|
<Spin class="chart-list_body" :spinning="chartData.loading" tip="加载中,请稍后...">
|
||||||
<CardGrid
|
<div class="chart-grid">
|
||||||
v-for="(item, index) in chartData.chartList"
|
<Card v-for="(item, index) in chartData.chartList" class="chart-card" :key="index" hoverable>
|
||||||
class="chart-card"
|
<template #title>
|
||||||
:key="index"
|
<span class="chart-title" @click="chartClick(item.id)">{{ item.title || "-" }}</span>
|
||||||
:style="{
|
</template>
|
||||||
width: `${config.isMobileApp ? 'calc(100% - 16px)' : 'calc(20% - 16px)'}`,
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
<Card class="w-full p-10px box-border" :title="item.title || '-'" hoverable>
|
|
||||||
<template #extra>
|
<template #extra>
|
||||||
<div class="flex items-center gap-5 justify-center">
|
<div class="chart-actions">
|
||||||
<IconShareInternal size="20" @click="openTab(item.id)" />
|
<Tooltip title="复制分享链接">
|
||||||
<Popconfirm content="确定要删除吗?" @before-ok="handleRemove(item.id)">
|
<ShareAltOutlined class="action-icon" @click="openTab(item.id)" />
|
||||||
<IconDelete size="20" />
|
</Tooltip>
|
||||||
|
<Popconfirm title="确定要删除吗?" @confirm="handleRemove(item.id)">
|
||||||
|
<Tooltip title="删除">
|
||||||
|
<DeleteOutlined class="action-icon danger" />
|
||||||
|
</Tooltip>
|
||||||
</Popconfirm>
|
</Popconfirm>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template #cover>
|
<template #cover>
|
||||||
<div class="h-175px w-full" @click="chartClick(item.id)">
|
<div class="chart-cover" @click="chartClick(item.id)">
|
||||||
<img :src="item.image" class="w-full h-full object-cover" v-if="item.image" />
|
<img :src="item.image" class="chart-image" v-if="item.image" />
|
||||||
<Empty v-else class="w-full h-full flex justify-center items-center flex-col" description="暂无图片" />
|
<Empty v-else class="chart-empty" description="暂无图片" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<CardMeta>
|
<Descriptions layout="vertical" :column="3" size="small" @click="chartClick(item.id)">
|
||||||
<template #description>
|
<DescriptionsItem v-for="desc in item.desc" :key="desc.label" :label="desc.label">
|
||||||
<Descriptions :data="item.desc" layout="inline-vertical" :column="3" @click="chartClick(item.id)" />
|
{{ desc.value }}
|
||||||
</template>
|
</DescriptionsItem>
|
||||||
</CardMeta>
|
</Descriptions>
|
||||||
</Card>
|
|
||||||
</CardGrid>
|
|
||||||
</Card>
|
</Card>
|
||||||
|
</div>
|
||||||
</Spin>
|
</Spin>
|
||||||
<div class="pagination mt-15px">
|
|
||||||
|
<div class="pagination">
|
||||||
<Pagination
|
<Pagination
|
||||||
:total="chartData.total"
|
:total="chartData.total"
|
||||||
show-total
|
:show-total="(total: number) => `共 ${total} 条`"
|
||||||
@change="pageChange"
|
@change="pageChange"
|
||||||
:disabled="chartData.loading"
|
:disabled="chartData.loading"
|
||||||
:page-size="20"
|
:page-size="20"
|
||||||
@ -196,33 +223,152 @@ const handleRemove = async (id: string) => {
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
.chart-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
flex: 1;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
min-height: 0;
|
||||||
|
padding: 16px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
border: 1px solid var(--app-border);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: var(--app-surface);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-list_toolbar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 0 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-list_total {
|
||||||
|
color: var(--app-muted);
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-list_body {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.chart-list_body.ant-spin-nested-loading),
|
||||||
|
:deep(.chart-list_body .ant-spin-container) {
|
||||||
|
height: 100%;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.chart-list_body .ant-spin-container) {
|
||||||
|
overflow: auto;
|
||||||
|
padding: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
|
||||||
|
gap: 16px;
|
||||||
|
align-content: start;
|
||||||
|
}
|
||||||
|
|
||||||
.pagination {
|
.pagination {
|
||||||
|
position: sticky;
|
||||||
|
bottom: 0;
|
||||||
|
z-index: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: end;
|
justify-content: end;
|
||||||
}
|
flex-shrink: 0;
|
||||||
:deep(.arco-card) {
|
margin-top: auto;
|
||||||
background: transparent;
|
padding: 12px 2px 0;
|
||||||
}
|
border-top: 1px solid var(--app-border);
|
||||||
|
background: var(--app-surface);
|
||||||
:deep(.arco-card-body) {
|
|
||||||
box-sizing: border-box;
|
|
||||||
margin: 0 !important;
|
|
||||||
padding: 0 2px 0 0 !important;
|
|
||||||
width: 100%;
|
|
||||||
gap: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.arco-card-grid {
|
|
||||||
box-shadow: unset !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
::-webkit-scrollbar {
|
|
||||||
display: none;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.chart-card {
|
.chart-card {
|
||||||
background: var(--color-bg-2);
|
overflow: hidden;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
</style>
|
|
||||||
|
|
||||||
|
:deep(.chart-card .ant-card-head) {
|
||||||
|
min-height: 44px;
|
||||||
|
padding: 0 16px;
|
||||||
|
border-bottom-color: var(--app-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.chart-card .ant-card-body) {
|
||||||
|
padding: 14px 16px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-title {
|
||||||
|
display: block;
|
||||||
|
overflow: hidden;
|
||||||
|
color: var(--app-text);
|
||||||
|
font-weight: 600;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
color: var(--app-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-cover {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 160px;
|
||||||
|
border-bottom: 1px solid var(--app-border);
|
||||||
|
background: var(--app-cover-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-image {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-empty {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.ant-descriptions-item-label) {
|
||||||
|
color: var(--app-muted);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.ant-descriptions-item-content) {
|
||||||
|
color: var(--app-text);
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.ant-descriptions-row > th),
|
||||||
|
:deep(.ant-descriptions-row > td) {
|
||||||
|
padding-bottom: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-icon {
|
||||||
|
font-size: 20px;
|
||||||
|
transition: color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-icon:hover {
|
||||||
|
color: #1677ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-icon.danger:hover {
|
||||||
|
color: #ff4d4f;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.chart-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@ -1,8 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { RadioGroup, Radio } from "@arco-design/web-vue";
|
import { RadioButton, RadioGroup } from "ant-design-vue";
|
||||||
import axios from "axios";
|
|
||||||
import { onMounted, ref } from "vue";
|
import { onMounted, ref } from "vue";
|
||||||
import config from "@/config";
|
import { getComponentDictByType, getDictByKey, DictItem } from "@/api/dict";
|
||||||
|
|
||||||
const emits = defineEmits(["change"]);
|
const emits = defineEmits(["change"]);
|
||||||
|
|
||||||
@ -13,48 +12,79 @@ const handleChange = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const typeList = ref<any>([]);
|
const typeList = ref<DictItem[]>([]);
|
||||||
const type = ref<string>("");
|
const type = ref<string>("");
|
||||||
|
|
||||||
const componentTypeList = ref<any>([]);
|
const componentTypeList = ref<DictItem[]>([]);
|
||||||
const componentType = ref("");
|
const componentType = ref("");
|
||||||
|
|
||||||
const getComponentTypeList = async () => {
|
const getComponentTypeList = async () => {
|
||||||
const list = (await axios.get(`${config.axiosBase}/dict/getComponentDictByType?type=${type.value}`)).data.data;
|
const list = (await getComponentDictByType(type.value)).data;
|
||||||
list.unshift({ label: "全部", value: "" });
|
componentTypeList.value = [{ label: "全部", value: "" }, ...list];
|
||||||
componentTypeList.value = list;
|
|
||||||
componentType.value = componentTypeList.value[0]?.value;
|
componentType.value = componentTypeList.value[0]?.value;
|
||||||
|
|
||||||
handleChange();
|
handleChange();
|
||||||
};
|
};
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
typeList.value = (await axios.get(`${config.axiosBase}/dict/getDictByKey?dictKey=COMPONENT_TYPE`)).data.data;
|
typeList.value = (await getDictByKey("COMPONENT_TYPE")).data;
|
||||||
type.value = typeList.value[0]?.value;
|
type.value = typeList.value[0]?.value;
|
||||||
getComponentTypeList();
|
getComponentTypeList();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
|
<div class="filter-panel">
|
||||||
<RadioGroup
|
<RadioGroup
|
||||||
type="button"
|
v-model:value="type"
|
||||||
v-model="type"
|
|
||||||
@change="getComponentTypeList"
|
@change="getComponentTypeList"
|
||||||
size="large"
|
size="default"
|
||||||
class="w-[fit-content] flex flex-wrap"
|
option-type="button"
|
||||||
|
button-style="solid"
|
||||||
|
class="filter-row"
|
||||||
>
|
>
|
||||||
<Radio v-for="item in typeList" :value="item.value" :key="item.value">{{ item.label }}</Radio>
|
<RadioButton v-for="item in typeList" :value="item.value" :key="item.value">{{ item.label }}</RadioButton>
|
||||||
</RadioGroup>
|
</RadioGroup>
|
||||||
|
|
||||||
<RadioGroup
|
<RadioGroup
|
||||||
type="button"
|
v-model:value="componentType"
|
||||||
v-model="componentType"
|
|
||||||
@change="handleChange"
|
@change="handleChange"
|
||||||
size="large"
|
size="default"
|
||||||
class="w-[fit-content] flex flex-wrap"
|
option-type="button"
|
||||||
|
button-style="solid"
|
||||||
|
class="filter-row"
|
||||||
>
|
>
|
||||||
<Radio v-for="item in componentTypeList" :value="item.value" :key="item.value">
|
<RadioButton v-for="item in componentTypeList" :value="item.value" :key="item.value">
|
||||||
{{ item.label }}
|
{{ item.label }}
|
||||||
</Radio>
|
</RadioButton>
|
||||||
</RadioGroup>
|
</RadioGroup>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.filter-panel {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 14px 16px;
|
||||||
|
border: 1px solid var(--app-border);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: var(--app-surface);
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-row {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.ant-radio-button-wrapper) {
|
||||||
|
height: 30px;
|
||||||
|
line-height: 28px;
|
||||||
|
border-inline-start-width: 1px;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.ant-radio-button-wrapper::before) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useRoute } from "vue-router";
|
import { useRoute } from "vue-router";
|
||||||
import { InputSearch } from "@arco-design/web-vue";
|
import { InputSearch } from "ant-design-vue";
|
||||||
import { computed, reactive, onBeforeUnmount, onMounted } from "vue";
|
import { computed, reactive, onBeforeUnmount, onMounted } from "vue";
|
||||||
import Theme from "@/modules/theme/Theme.vue";
|
import Theme from "@/modules/theme/Theme.vue";
|
||||||
|
|
||||||
@ -45,15 +45,13 @@ onBeforeUnmount(() => {
|
|||||||
<div class="menu">
|
<div class="menu">
|
||||||
<InputSearch
|
<InputSearch
|
||||||
class="search-input"
|
class="search-input"
|
||||||
v-model="searchData.content"
|
v-model:value="searchData.content"
|
||||||
v-if="showSearch"
|
v-if="showSearch"
|
||||||
placeholder="输入关键词"
|
placeholder="输入关键词"
|
||||||
button-text="搜索"
|
enter-button="搜索"
|
||||||
search-button
|
|
||||||
@search="searchClick"
|
@search="searchClick"
|
||||||
:loading="searchData.loading"
|
:loading="searchData.loading"
|
||||||
allow-clear
|
allow-clear
|
||||||
@press-enter="searchClick"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Theme class="theme"></Theme>
|
<Theme class="theme"></Theme>
|
||||||
@ -69,24 +67,27 @@ onBeforeUnmount(() => {
|
|||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
z-index: 999;
|
z-index: 999;
|
||||||
border-bottom: 1px solid var(--color-border);
|
border-bottom: 1px solid var(--app-border);
|
||||||
background-color: var(--color-bg-2);
|
background-color: var(--app-surface);
|
||||||
|
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.04);
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-bar_content {
|
.nav-bar_content {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 60px;
|
height: 56px;
|
||||||
min-height: 60px;
|
min-height: 56px;
|
||||||
max-height: 60px;
|
max-height: 56px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
z-index: 999;
|
z-index: 999;
|
||||||
|
padding: 0 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-bar_left {
|
.nav-bar_left {
|
||||||
width: 180px;
|
width: 190px;
|
||||||
font-size: 30px;
|
font-size: 26px;
|
||||||
|
line-height: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-bar_right {
|
.nav-bar_right {
|
||||||
@ -94,14 +95,25 @@ onBeforeUnmount(() => {
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding-right: 1rem;
|
gap: 16px;
|
||||||
|
|
||||||
.menu {
|
.menu {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
.search-input {
|
.search-input {
|
||||||
width: 320px;
|
width: 360px;
|
||||||
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.nav-bar_content {
|
||||||
|
padding: 0 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-bar_left {
|
||||||
|
width: 150px;
|
||||||
|
font-size: 22px;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|||||||
@ -1,43 +1,22 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onMounted, ref, } from "vue";
|
import { BulbFilled, BulbOutlined } from "@ant-design/icons-vue";
|
||||||
import { IconSunFill, IconMoonFill } from "@arco-design/web-vue/es/icon";
|
import { useTheme } from "@/hooks/useTheme";
|
||||||
type ThemeType = 'light' | 'dark'
|
|
||||||
const size = 24
|
|
||||||
const theme = ref("light");
|
|
||||||
const themeChange = (value: ThemeType) => {
|
|
||||||
theme.value = value;
|
|
||||||
localStorage.setItem("theme", value);
|
|
||||||
themeJudge(value);
|
|
||||||
};
|
|
||||||
|
|
||||||
const themeJudge = (theme: ThemeType) => {
|
const { theme, setTheme } = useTheme();
|
||||||
if (theme === "dark") {
|
|
||||||
document.body.setAttribute("arco-theme", "dark");
|
|
||||||
} else {
|
|
||||||
document.body.removeAttribute("arco-theme");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 加载数据
|
|
||||||
onMounted(() => {
|
|
||||||
// 主题色初始化
|
|
||||||
const themeValue = localStorage.getItem("theme") as (ThemeType | null) || "light";
|
|
||||||
themeJudge(themeValue);
|
|
||||||
theme.value = themeValue;
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<IconMoonFill v-if="theme === 'dark'" class="moon theme-icon" :size="size" @click="() => themeChange('light')" />
|
<BulbFilled v-if="theme === 'dark'" class="moon theme-icon" @click="() => setTheme('light')" />
|
||||||
<IconSunFill v-else class="sun theme-icon" :size="size" @click="() => themeChange('dark')" />
|
<BulbOutlined v-else class="sun theme-icon" @click="() => setTheme('dark')" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.moon {
|
.moon {
|
||||||
color: var(--color-text-1)
|
color: var(--app-text);
|
||||||
}
|
}
|
||||||
|
|
||||||
.theme-icon {
|
.theme-icon {
|
||||||
cursor: pointer
|
cursor: pointer;
|
||||||
|
font-size: 24px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@ -37,9 +37,9 @@ onBeforeUnmount(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="h-full w-full flex flex-col overflow-hidden gap-8px">
|
<div class="home-page">
|
||||||
<ChartTypeGroup @change="tabChange"></ChartTypeGroup>
|
<ChartTypeGroup @change="tabChange"></ChartTypeGroup>
|
||||||
<div class="flex-1 overflow-hidden">
|
<div class="home-list">
|
||||||
<ChartList />
|
<ChartList />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -47,7 +47,28 @@ onBeforeUnmount(() => {
|
|||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.content {
|
.content {
|
||||||
padding: 20px 30px;
|
padding: 16px 24px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.home-page {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 14px;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.home-list {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.content {
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|||||||
@ -6,6 +6,7 @@
|
|||||||
"strict": true,
|
"strict": true,
|
||||||
"jsx": "preserve",
|
"jsx": "preserve",
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
|
"moduleResolution": "node",
|
||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"lib": [
|
"lib": [
|
||||||
|
|||||||
@ -2,7 +2,6 @@
|
|||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"composite": true,
|
"composite": true,
|
||||||
"module": "ESNext",
|
"module": "ESNext",
|
||||||
"moduleResolution": "Node",
|
|
||||||
"allowSyntheticDefaultImports": true
|
"allowSyntheticDefaultImports": true
|
||||||
},
|
},
|
||||||
"include": ["vite.config.ts"]
|
"include": ["vite.config.ts"]
|
||||||
|
|||||||
@ -9,6 +9,7 @@ export default ({ mode }) => {
|
|||||||
return defineConfig({
|
return defineConfig({
|
||||||
plugins: [vue(), UnoCSS()],
|
plugins: [vue(), UnoCSS()],
|
||||||
server: {
|
server: {
|
||||||
|
port: 48088,
|
||||||
proxy: {
|
proxy: {
|
||||||
"/api": {
|
"/api": {
|
||||||
target: VITE_APP_PROXY,
|
target: VITE_APP_PROXY,
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user