mirror of
https://github.com/KwiTsukasa/kt-template-online-web.git
synced 2026-05-27 16:35:47 +08:00
first commit
This commit is contained in:
commit
f439349933
2
.env
Normal file
2
.env
Normal file
@ -0,0 +1,2 @@
|
||||
VITE_APP_BASE_API="api.ppmark.cn/chart"
|
||||
VITE_APP_OSS_DOMAIN="api.ppmark.cn/chart-assets"
|
||||
4
.env.development
Normal file
4
.env.development
Normal file
@ -0,0 +1,4 @@
|
||||
NODE_ENV = depelopment
|
||||
VITE_APP_PLAY_GROUND = http://192.168.52.164:5173
|
||||
VITE_APP_PROXY = "http://192.168.1.206:48085/"
|
||||
|
||||
3
.env.production
Normal file
3
.env.production
Normal file
@ -0,0 +1,3 @@
|
||||
NODE_ENV = production
|
||||
VITE_APP_PLAY_GROUND = http://192.168.1.206:84/
|
||||
VITE_APP_PROXY = http://192.168.1.206:48085/
|
||||
24
.gitignore
vendored
Normal file
24
.gitignore
vendored
Normal file
@ -0,0 +1,24 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
16
README.md
Normal file
16
README.md
Normal file
@ -0,0 +1,16 @@
|
||||
# Vue 3 + TypeScript + Vite
|
||||
|
||||
This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
|
||||
|
||||
## Recommended IDE Setup
|
||||
|
||||
- [VS Code](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar)
|
||||
|
||||
## Type Support For `.vue` Imports in TS
|
||||
|
||||
Since TypeScript cannot handle type information for `.vue` imports, they are shimmed to be a generic Vue component type by default. In most cases this is fine if you don't really care about component prop types outside of templates. However, if you wish to get actual prop types in `.vue` imports (for example to get props validation when using manual `h(...)` calls), you can enable Volar's Take Over mode by following these steps:
|
||||
|
||||
1. Run `Extensions: Show Built-in Extensions` from VS Code's command palette, look for `TypeScript and JavaScript Language Features`, then right click and select `Disable (Workspace)`. By default, Take Over mode will enable itself if the default TypeScript extension is disabled.
|
||||
2. Reload the VS Code window by running `Developer: Reload Window` from the command palette.
|
||||
|
||||
You can learn more about Take Over mode [here](https://github.com/johnsoncodehk/volar/discussions/471).
|
||||
8
deploy.json
Normal file
8
deploy.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"host": "192.168.1.206",
|
||||
"port": "22",
|
||||
"username": "root",
|
||||
"password": "3h1admin",
|
||||
"localDir": "dist",
|
||||
"remoteDir": "/data/nginx/chart"
|
||||
}
|
||||
18
index.html
Normal file
18
index.html
Normal file
@ -0,0 +1,18 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>ShyTemplate - 模版库</title>
|
||||
<meta name="Keywords" content="echarts,Echarts,gallery,makeapie,make a pie,ppchart,PPChart" />
|
||||
<meta name="description" content="让图表更简单。PPChart 提供 Echarts 收录、图表制作等服务" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
30
package.json
Normal file
30
package.json
Normal file
@ -0,0 +1,30 @@
|
||||
{
|
||||
"name": "kt-template-online-web",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vue-tsc && vite build",
|
||||
"preview": "vite preview",
|
||||
"deploy": "pnpm build && npx shy ftp deploy -f ./deploy"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^1.1.3",
|
||||
"moment": "^2.29.4",
|
||||
"monaco-editor": "^0.34.1",
|
||||
"pinia": "^2.0.23",
|
||||
"vue": "^3.2.41",
|
||||
"vue-router": "^4.1.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@arco-design/web-vue": "^2.38.1",
|
||||
"@types/node": "^18.11.9",
|
||||
"@vitejs/plugin-vue": "^3.2.0",
|
||||
"sass": "^1.56.0",
|
||||
"typescript": "^4.6.4",
|
||||
"unocss": "^0.61.8",
|
||||
"vite": "^3.2.0",
|
||||
"vue-tsc": "^1.0.9"
|
||||
}
|
||||
}
|
||||
2628
pnpm-lock.yaml
Normal file
2628
pnpm-lock.yaml
Normal file
File diff suppressed because it is too large
Load Diff
BIN
public/favicon.ico
Normal file
BIN
public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
6
public/robots.txt
Normal file
6
public/robots.txt
Normal file
@ -0,0 +1,6 @@
|
||||
User-agent: *
|
||||
Sitemap: http://ppchart.com/index.html
|
||||
User-agent: Wandoujia Spider
|
||||
User-agent: Baiduspider
|
||||
User-agent: Mediapartners-Google
|
||||
|
||||
41
src/App.vue
Normal file
41
src/App.vue
Normal file
@ -0,0 +1,41 @@
|
||||
<script setup lang="ts">
|
||||
import { LayoutContent, Layout } from "@arco-design/web-vue";
|
||||
import Header from "@/modules/header/Header.vue";
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Layout class="!h-[calc(100%-60px)] !pt-60px">
|
||||
<Header></Header>
|
||||
<LayoutContent class="content">
|
||||
<router-view></router-view>
|
||||
</LayoutContent>
|
||||
</Layout>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
html,
|
||||
body {
|
||||
height: 100%;
|
||||
--color-logo: rgb(85, 26, 139);
|
||||
--color-logo-1: rgb(237, 221, 249);
|
||||
}
|
||||
|
||||
#app {
|
||||
font-family: Avenir, Helvetica, Arial, sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
color: #2c3e50;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.content {
|
||||
background-color: var(--color-bg-5);
|
||||
height: calc(100% - 60px);
|
||||
}
|
||||
|
||||
body[arco-theme="dark"] {
|
||||
--color-logo: var(--color-text-1);
|
||||
--color-logo-1: var(--color-bg-5);
|
||||
}
|
||||
</style>
|
||||
|
||||
1
src/assets/vue.svg
Normal file
1
src/assets/vue.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 496 B |
31
src/bus.ts
Normal file
31
src/bus.ts
Normal file
@ -0,0 +1,31 @@
|
||||
// 为保持和vue2版本中使用bus一致,emit,on,off前面都加了$
|
||||
export class Bus {
|
||||
list: Record<string, Array<(...params: any) => void>> = {}
|
||||
constructor() {
|
||||
// 收集订阅信息,调度中心
|
||||
this.list = {};
|
||||
}
|
||||
|
||||
// 订阅
|
||||
$on(name: string, fn: (...params: any) => void) {
|
||||
this.list[name] = this.list[name] || [];
|
||||
this.list[name].push(fn);
|
||||
}
|
||||
|
||||
// 发布
|
||||
$emit(name: string, data: any) {
|
||||
if (this.list[name]) {
|
||||
this.list[name].forEach((fn) => {
|
||||
fn(data);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 取消订阅
|
||||
$off(name: string) {
|
||||
if (this.list[name]) {
|
||||
delete this.list[name];
|
||||
}
|
||||
}
|
||||
}
|
||||
export default new Bus();
|
||||
16
src/components/logo/Logo.vue
Normal file
16
src/components/logo/Logo.vue
Normal file
@ -0,0 +1,16 @@
|
||||
<template>
|
||||
<a href="/" class="logo"> ShyTemplate </a>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-decoration: none; // 去除下划线
|
||||
color: var(--color-logo);
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
10
src/config.ts
Normal file
10
src/config.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import { os } from "@/utils/detect";
|
||||
|
||||
const config = (() => ({
|
||||
isMobileApp: !os.desktop,
|
||||
axiosBase: "/api",
|
||||
playground: import.meta.env.VITE_APP_PLAY_GROUND,
|
||||
}))();
|
||||
|
||||
export default config;
|
||||
|
||||
16
src/hooks/useModel.ts
Normal file
16
src/hooks/useModel.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import { ref, watch, getCurrentInstance, ExtractPropTypes } from "vue";
|
||||
|
||||
type EmitFn = (event: string, ...args: any[]) => void
|
||||
|
||||
function useModel(props: Readonly<ExtractPropTypes<{ [k: string]: StringConstructor }>>, key = "modelValue", emit?: EmitFn) {
|
||||
const proxy = ref(props[key]);
|
||||
const _emit = emit || getCurrentInstance()?.emit;
|
||||
watch(
|
||||
() => proxy.value,
|
||||
(v) => _emit && _emit(`update:${key}`, v)
|
||||
|
||||
);
|
||||
return proxy;
|
||||
}
|
||||
|
||||
export default useModel
|
||||
11
src/main.ts
Normal file
11
src/main.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { createApp } from "vue";
|
||||
import { createPinia } from "pinia";
|
||||
import "virtual:uno.css";
|
||||
|
||||
import App from "@/App.vue";
|
||||
import { router } from "@/router.js";
|
||||
|
||||
import "@arco-design/web-vue/dist/arco.css";
|
||||
|
||||
createApp(App).use(router).use(createPinia()).mount("#app");
|
||||
|
||||
13
src/models/chartList/chartListStore.ts
Normal file
13
src/models/chartList/chartListStore.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import { defineStore } from "pinia"
|
||||
|
||||
export const useChartListStore = defineStore('chartList', {
|
||||
state: () => ({ searchValue: '', chartList: [], loading: false }),
|
||||
getters: {
|
||||
// double: (state) => state.count * 2,
|
||||
},
|
||||
actions: {
|
||||
increment() {
|
||||
// this.count++
|
||||
},
|
||||
},
|
||||
})
|
||||
228
src/modules/chartList/ChartList.vue
Normal file
228
src/modules/chartList/ChartList.vue
Normal file
@ -0,0 +1,228 @@
|
||||
<script setup lang="ts">
|
||||
import { DescData, Message, Button, Empty } from "@arco-design/web-vue";
|
||||
import { onBeforeUnmount, onMounted, reactive, ref } from "vue";
|
||||
import axios from "axios";
|
||||
import moment from "moment";
|
||||
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";
|
||||
|
||||
enum HttpStatus {
|
||||
OK = 200,
|
||||
}
|
||||
|
||||
const chartData = reactive<any>({
|
||||
chartList: [] as Array<{
|
||||
title: string;
|
||||
image: string;
|
||||
id: string;
|
||||
desc: DescData[];
|
||||
}>,
|
||||
total: 0,
|
||||
pageIndex: 1,
|
||||
loading: false,
|
||||
type: "",
|
||||
searchValue: "",
|
||||
componentType: "",
|
||||
});
|
||||
|
||||
const convertTime = (timeStr: string) => {
|
||||
return moment(timeStr).format("YYYY-MM-DD");
|
||||
};
|
||||
|
||||
const getData = () => {
|
||||
chartData.loading = true;
|
||||
Bus.$emit("search-loading", true);
|
||||
const params: any = {
|
||||
pageSize: 20,
|
||||
pageNo: chartData.pageIndex,
|
||||
type: chartData.type,
|
||||
componentType: chartData.componentType,
|
||||
name: chartData.searchValue,
|
||||
};
|
||||
if (chartData.type) {
|
||||
params.type = chartData.type;
|
||||
}
|
||||
axios
|
||||
.get(`${config.axiosBase}/component/list`, { params })
|
||||
.then((res) => {
|
||||
const { code, data, msg } = res.data;
|
||||
if (code === HttpStatus.OK) {
|
||||
const { list, total } = data;
|
||||
chartData.total = total;
|
||||
chartData.chartList = list.map((item: any) => {
|
||||
const { id, createTime, componentTypeMsg, name, typeMsg, image } = item;
|
||||
return {
|
||||
id,
|
||||
title: name,
|
||||
image,
|
||||
desc: [
|
||||
{ value: convertTime(createTime), label: "创建时间" },
|
||||
{ value: componentTypeMsg, label: "组件类型" },
|
||||
{ value: typeMsg, label: "类型" },
|
||||
],
|
||||
};
|
||||
});
|
||||
} else {
|
||||
Message.error(msg || "服务器开小差了,请稍后再试...");
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
chartData.loading = false;
|
||||
Bus.$emit("search-loading", false);
|
||||
});
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
Bus.$on("search", (res: any) => {
|
||||
Object.keys(res).forEach((key) => {
|
||||
chartData[key] = res[key];
|
||||
});
|
||||
chartData.pageIndex = 1;
|
||||
getData();
|
||||
});
|
||||
});
|
||||
onBeforeUnmount(() => {
|
||||
Bus.$off("search-loading");
|
||||
});
|
||||
|
||||
const chartClick = async (params: string) => {
|
||||
const res = await axios.get(`${config.axiosBase}/component/detail?id=${params}`);
|
||||
const { template, componentType, type, id, name } = res?.data?.data;
|
||||
window.open(`${config.playground}/?id=${id}&name=${name}&componentType=${componentType}&type=${type}#${template}`);
|
||||
};
|
||||
|
||||
const pageChange = (pageIndex: number) => {
|
||||
chartData.pageIndex = pageIndex;
|
||||
getData();
|
||||
};
|
||||
|
||||
const openTab = async (params: string) => {
|
||||
const res = await axios.get(`${config.axiosBase}/component/detail?id=${params}`);
|
||||
const { template, componentType, type, id, name } = res?.data?.data;
|
||||
|
||||
const url = `${config.playground}/?id=${id}&name=${name}&componentType=${componentType}&type=${type}#${template}`;
|
||||
|
||||
const tempInput = document.createElement("textarea");
|
||||
|
||||
tempInput.style.position = "absolute"; // 确保它不影响布局
|
||||
tempInput.style.opacity = "0"; // 隐藏文本区域
|
||||
document.body.append(tempInput);
|
||||
|
||||
// 设置要复制的文本
|
||||
tempInput.value = url;
|
||||
tempInput.select(); // 选中文本
|
||||
|
||||
// 执行复制命令
|
||||
try {
|
||||
document.execCommand("copy");
|
||||
} catch (error) {
|
||||
console.error("Error copying text:", error);
|
||||
}
|
||||
|
||||
// 删除临时文本区域
|
||||
tempInput.remove();
|
||||
Message.success("分享链接已复制到剪贴板");
|
||||
};
|
||||
|
||||
const handleAdd = () => {
|
||||
window.open(`${config.playground}?type=${chartData.type}&componentType=${chartData.componentType}`);
|
||||
};
|
||||
|
||||
const handleRemove = async (id: string) => {
|
||||
try {
|
||||
const {
|
||||
data: { code, msg },
|
||||
} = await axios.post(`${config.axiosBase}/component/remove?id=${id}`);
|
||||
if (code !== HttpStatus.OK) return Message.error(msg);
|
||||
Message.success(msg);
|
||||
getData();
|
||||
} catch (error) {
|
||||
return Message.error(error as any);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-full !h-full flex flex-col gap-8px overflow-hidden">
|
||||
<div class="ml-auto">
|
||||
<Button type="primary" @click="handleAdd"> 新增 </Button>
|
||||
</div>
|
||||
<Spin class="flex-1 overflow-auto" :loading="chartData.loading" tip="加载中,请稍后...">
|
||||
<Card :bordered="false">
|
||||
<CardGrid
|
||||
v-for="(item, index) in chartData.chartList"
|
||||
class="chart-card"
|
||||
:key="index"
|
||||
:style="{
|
||||
width: `${config.isMobileApp ? 'calc(100% - 16px)' : 'calc(20% - 16px)'}`,
|
||||
}"
|
||||
>
|
||||
<Card class="w-full p-10px box-border" :title="item.title || '-'" hoverable>
|
||||
<template #extra>
|
||||
<div class="flex items-center gap-5 justify-center">
|
||||
<IconShareInternal size="20" @click="openTab(item.id)" />
|
||||
<Popconfirm content="确定要删除吗?" @before-ok="handleRemove(item.id)">
|
||||
<IconDelete size="20" />
|
||||
</Popconfirm>
|
||||
</div>
|
||||
</template>
|
||||
<template #cover>
|
||||
<div class="h-175px w-full" @click="chartClick(item.id)">
|
||||
<img :src="item.image" class="w-full h-full object-cover" v-if="item.image" />
|
||||
<Empty v-else class="w-full h-full flex justify-center items-center flex-col" description="暂无图片" />
|
||||
</div>
|
||||
</template>
|
||||
<CardMeta>
|
||||
<template #description>
|
||||
<Descriptions :data="item.desc" layout="inline-vertical" :column="3" @click="chartClick(item.id)" />
|
||||
</template>
|
||||
</CardMeta>
|
||||
</Card>
|
||||
</CardGrid>
|
||||
</Card>
|
||||
</Spin>
|
||||
<div class="pagination mt-15px">
|
||||
<Pagination
|
||||
:total="chartData.total"
|
||||
show-total
|
||||
@change="pageChange"
|
||||
:disabled="chartData.loading"
|
||||
:page-size="20"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.pagination {
|
||||
display: flex;
|
||||
justify-content: end;
|
||||
}
|
||||
:deep(.arco-card) {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
: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 {
|
||||
background: var(--color-bg-2);
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
|
||||
60
src/modules/chartList/components/ChartTypeGroup.vue
Normal file
60
src/modules/chartList/components/ChartTypeGroup.vue
Normal file
@ -0,0 +1,60 @@
|
||||
<script setup lang="ts">
|
||||
import { RadioGroup, Radio } from "@arco-design/web-vue";
|
||||
import axios from "axios";
|
||||
import { onMounted, ref } from "vue";
|
||||
import config from "@/config";
|
||||
|
||||
const emits = defineEmits(["change"]);
|
||||
|
||||
const handleChange = () => {
|
||||
emits("change", {
|
||||
type: type.value,
|
||||
componentType: componentType.value,
|
||||
});
|
||||
};
|
||||
|
||||
const typeList = ref<any>([]);
|
||||
const type = ref<string>("");
|
||||
|
||||
const componentTypeList = ref<any>([]);
|
||||
const componentType = ref("");
|
||||
|
||||
const getComponentTypeList = async () => {
|
||||
const list = (await axios.get(`${config.axiosBase}/dict/getComponentDictByType?type=${type.value}`)).data.data;
|
||||
list.unshift({ label: "全部", value: "" });
|
||||
componentTypeList.value = list;
|
||||
componentType.value = componentTypeList.value[0]?.value;
|
||||
|
||||
handleChange();
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
typeList.value = (await axios.get(`${config.axiosBase}/dict/getDictByKey?dictKey=COMPONENT_TYPE`)).data.data;
|
||||
type.value = typeList.value[0]?.value;
|
||||
getComponentTypeList();
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<RadioGroup
|
||||
type="button"
|
||||
v-model="type"
|
||||
@change="getComponentTypeList"
|
||||
size="large"
|
||||
class="w-[fit-content] flex flex-wrap"
|
||||
>
|
||||
<Radio v-for="item in typeList" :value="item.value" :key="item.value">{{ item.label }}</Radio>
|
||||
</RadioGroup>
|
||||
|
||||
<RadioGroup
|
||||
type="button"
|
||||
v-model="componentType"
|
||||
@change="handleChange"
|
||||
size="large"
|
||||
class="w-[fit-content] flex flex-wrap"
|
||||
>
|
||||
<Radio v-for="item in componentTypeList" :value="item.value" :key="item.value">
|
||||
{{ item.label }}
|
||||
</Radio>
|
||||
</RadioGroup>
|
||||
</template>
|
||||
|
||||
107
src/modules/header/Header.vue
Normal file
107
src/modules/header/Header.vue
Normal file
@ -0,0 +1,107 @@
|
||||
<script setup lang="ts">
|
||||
import { useRoute } from "vue-router";
|
||||
import { InputSearch } from "@arco-design/web-vue";
|
||||
import { computed, reactive, onBeforeUnmount, onMounted } from "vue";
|
||||
import Theme from "@/modules/theme/Theme.vue";
|
||||
|
||||
import Bus from "@/bus";
|
||||
import Logo from "@/components/logo/Logo.vue";
|
||||
|
||||
import { isSearchEnabled } from "./queries";
|
||||
|
||||
// 搜索输入框
|
||||
const pageData = reactive({
|
||||
loaded: false,
|
||||
});
|
||||
const searchData = reactive({
|
||||
loading: false,
|
||||
content: "",
|
||||
});
|
||||
const route = useRoute();
|
||||
const showSearch = computed<boolean>(() => isSearchEnabled(pageData.loaded, route.name));
|
||||
const searchClick = () => {
|
||||
Bus.$emit("home-search", searchData.content);
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
pageData.loaded = true;
|
||||
// 接收搜索 loading 变化事件
|
||||
Bus.$on("search-loading", (loading: boolean) => {
|
||||
searchData.loading = loading;
|
||||
});
|
||||
});
|
||||
onBeforeUnmount(() => {
|
||||
Bus.$off("home-search");
|
||||
Bus.$off("search-loading");
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<div class="nav-bar_wrapper">
|
||||
<div class="nav-bar_content">
|
||||
<div class="nav-bar_left">
|
||||
<Logo></Logo>
|
||||
</div>
|
||||
<div class="nav-bar_right">
|
||||
<div class="menu">
|
||||
<InputSearch
|
||||
class="search-input"
|
||||
v-model="searchData.content"
|
||||
v-if="showSearch"
|
||||
placeholder="输入关键词"
|
||||
button-text="搜索"
|
||||
search-button
|
||||
@search="searchClick"
|
||||
:loading="searchData.loading"
|
||||
allow-clear
|
||||
@press-enter="searchClick"
|
||||
/>
|
||||
</div>
|
||||
<Theme class="theme"></Theme>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.nav-bar_wrapper {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 999;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
background-color: var(--color-bg-2);
|
||||
}
|
||||
|
||||
.nav-bar_content {
|
||||
position: relative;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 60px;
|
||||
min-height: 60px;
|
||||
max-height: 60px;
|
||||
box-sizing: border-box;
|
||||
z-index: 999;
|
||||
}
|
||||
|
||||
.nav-bar_left {
|
||||
width: 180px;
|
||||
font-size: 30px;
|
||||
}
|
||||
|
||||
.nav-bar_right {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
align-items: center;
|
||||
padding-right: 1rem;
|
||||
|
||||
.menu {
|
||||
flex: 1;
|
||||
.search-input {
|
||||
width: 320px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
7
src/modules/header/queries.ts
Normal file
7
src/modules/header/queries.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import config from "@/config"
|
||||
import Home from '@/views/home.vue'
|
||||
import { RouteRecordName } from "vue-router"
|
||||
|
||||
export const isSearchEnabled = (pageLoaded: boolean, routeName?: RouteRecordName | null) => {
|
||||
return !config.isMobileApp && [Home.name].includes(String(routeName)) && pageLoaded
|
||||
}
|
||||
43
src/modules/theme/Theme.vue
Normal file
43
src/modules/theme/Theme.vue
Normal file
@ -0,0 +1,43 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref, } from "vue";
|
||||
import { IconSunFill, IconMoonFill } from "@arco-design/web-vue/es/icon";
|
||||
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) => {
|
||||
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>
|
||||
<template>
|
||||
<IconMoonFill v-if="theme === 'dark'" class="moon theme-icon" :size="size" @click="() => themeChange('light')" />
|
||||
<IconSunFill v-else class="sun theme-icon" :size="size" @click="() => themeChange('dark')" />
|
||||
</template>
|
||||
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.moon {
|
||||
color: var(--color-text-1)
|
||||
}
|
||||
|
||||
.theme-icon {
|
||||
cursor: pointer
|
||||
}
|
||||
</style>
|
||||
17
src/router.ts
Normal file
17
src/router.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { createRouter, createWebHashHistory } from "vue-router";
|
||||
import Home from "@/views/home.vue";
|
||||
|
||||
const routes = [
|
||||
{
|
||||
name: Home.name,
|
||||
path: "/",
|
||||
component: () => import("./views/home.vue"),
|
||||
},
|
||||
];
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHashHistory(),
|
||||
routes,
|
||||
});
|
||||
|
||||
export { router };
|
||||
81
src/style.css
Normal file
81
src/style.css
Normal file
@ -0,0 +1,81 @@
|
||||
:root {
|
||||
font-family: Inter, Avenir, Helvetica, Arial, sans-serif;
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
font-weight: 400;
|
||||
|
||||
color-scheme: light dark;
|
||||
color: rgba(255, 255, 255, 0.87);
|
||||
background-color: #242424;
|
||||
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
}
|
||||
|
||||
a {
|
||||
font-weight: 500;
|
||||
color: #646cff;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
a:hover {
|
||||
color: #535bf2;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
place-items: center;
|
||||
min-width: 320px;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 3.2em;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
button {
|
||||
border-radius: 8px;
|
||||
border: 1px solid transparent;
|
||||
padding: 0.6em 1.2em;
|
||||
font-size: 1em;
|
||||
font-weight: 500;
|
||||
font-family: inherit;
|
||||
background-color: #1a1a1a;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.25s;
|
||||
}
|
||||
button:hover {
|
||||
border-color: #646cff;
|
||||
}
|
||||
button:focus,
|
||||
button:focus-visible {
|
||||
outline: 4px auto -webkit-focus-ring-color;
|
||||
}
|
||||
|
||||
.card {
|
||||
padding: 2em;
|
||||
}
|
||||
|
||||
#app {
|
||||
max-width: 1280px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
:root {
|
||||
color: #213547;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
a:hover {
|
||||
color: #747bff;
|
||||
}
|
||||
button {
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
}
|
||||
31
src/utils/detect.ts
Normal file
31
src/utils/detect.ts
Normal file
@ -0,0 +1,31 @@
|
||||
const detectOS = () => {
|
||||
const ua = window.navigator.userAgent;
|
||||
const detectOther = {
|
||||
desktop: false,
|
||||
ios: null as null | RegExpMatchArray,
|
||||
android: ua.match(/(Android)\s+([\d.]+)/),
|
||||
tablet: /^(?=.*Android)(?!.*Mobile).*/.test(ua),
|
||||
ipod: /(iPod).*OS\s([\d_]+)/.test(ua),
|
||||
ipad: ua.match(/(iPad).*OS\s([\d_]+)/),
|
||||
iphone: ua.match(/(iPhone\sOS)\s([\d_]+)/),
|
||||
webkit: /WebKit\/([\d.]+)/.test(ua),
|
||||
iosVersion: null as null | string,
|
||||
androidVersion: null as null | string,
|
||||
blackberry: /^(?=.*BB10; Touch).*Version\/([0-9]+\.[0-9])/.test(ua),
|
||||
};
|
||||
|
||||
detectOther.ios = detectOther.ipad || detectOther.iphone;
|
||||
detectOther.desktop = !(detectOther.ios || detectOther.android || detectOther.tablet || detectOther.ipod || detectOther.blackberry);
|
||||
|
||||
if (detectOther.ios) {
|
||||
const [iosVersion] = detectOther.ios[2].split('_');
|
||||
detectOther.iosVersion = iosVersion
|
||||
}
|
||||
if (detectOther.android) {
|
||||
const [androidVersion] = (detectOther.android as RegExpMatchArray)[2].split('.');
|
||||
detectOther.androidVersion = androidVersion
|
||||
}
|
||||
return detectOther;
|
||||
}
|
||||
|
||||
export const os = detectOS();
|
||||
8
src/utils/transform.ts
Normal file
8
src/utils/transform.ts
Normal file
@ -0,0 +1,8 @@
|
||||
export function utoa(data: string): string {
|
||||
return btoa(unescape(encodeURIComponent(data)));
|
||||
}
|
||||
|
||||
export function atou(base64: string): string {
|
||||
return decodeURIComponent(escape(atob(base64)));
|
||||
}
|
||||
|
||||
53
src/views/home.vue
Normal file
53
src/views/home.vue
Normal file
@ -0,0 +1,53 @@
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: "home",
|
||||
};
|
||||
</script>
|
||||
<script setup lang="ts">
|
||||
import ChartTypeGroup from "@/modules/chartList/components/ChartTypeGroup.vue";
|
||||
import Bus from "@/bus";
|
||||
import ChartList from "@/modules/chartList/ChartList.vue";
|
||||
import { onBeforeUnmount, onMounted, ref } from "vue";
|
||||
import { unref } from "vue";
|
||||
|
||||
const tabData = ref<Tab>();
|
||||
|
||||
type Tab = {
|
||||
componentType: string;
|
||||
type: string;
|
||||
};
|
||||
|
||||
const searchValue = ref("");
|
||||
|
||||
const tabChange = (params: Tab) => {
|
||||
tabData.value = params;
|
||||
Bus.$emit("search", { ...tabData.value, searchValue: unref(searchValue) });
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
Bus.$on("home-search", (params) => {
|
||||
searchValue.value = params;
|
||||
Bus.$emit("search", { ...tabData.value, searchValue: unref(searchValue) });
|
||||
});
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
Bus.$off("search-loading");
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="h-full w-full flex flex-col overflow-hidden gap-8px">
|
||||
<ChartTypeGroup @change="tabChange"></ChartTypeGroup>
|
||||
<div class="flex-1 overflow-hidden">
|
||||
<ChartList />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.content {
|
||||
padding: 20px 30px;
|
||||
}
|
||||
</style>
|
||||
|
||||
11
src/vite-env.d.ts
vendored
Normal file
11
src/vite-env.d.ts
vendored
Normal file
@ -0,0 +1,11 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
declare module '*.vue' {
|
||||
import type { DefineComponent } from 'vue'
|
||||
const component: DefineComponent<{}, {}, any>
|
||||
export default component
|
||||
}
|
||||
|
||||
declare interface Window {
|
||||
MonacoEnvironment: any
|
||||
}
|
||||
38
tsconfig.json
Normal file
38
tsconfig.json
Normal file
@ -0,0 +1,38 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"useDefineForClassFields": true,
|
||||
"module": "ESNext",
|
||||
"strict": true,
|
||||
"jsx": "preserve",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"esModuleInterop": true,
|
||||
"lib": [
|
||||
"ESNext",
|
||||
"DOM"
|
||||
],
|
||||
"skipLibCheck": true,
|
||||
"noEmit": true,
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"./src/*"
|
||||
],
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts",
|
||||
"src/**/*.d.ts",
|
||||
"src/**/*.tsx",
|
||||
"src/**/*.vue"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"dist"
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.node.json"
|
||||
}
|
||||
],
|
||||
}
|
||||
9
tsconfig.node.json
Normal file
9
tsconfig.node.json
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Node",
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
6
uno.config.ts
Normal file
6
uno.config.ts
Normal file
@ -0,0 +1,6 @@
|
||||
// uno.config.ts
|
||||
import { defineConfig } from "unocss";
|
||||
|
||||
export default defineConfig({
|
||||
// ...UnoCSS options
|
||||
});
|
||||
27
vite.config.ts
Normal file
27
vite.config.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import { defineConfig, loadEnv } from "vite";
|
||||
import vue from "@vitejs/plugin-vue";
|
||||
import { resolve } from "path";
|
||||
import UnoCSS from "unocss/vite";
|
||||
|
||||
export default ({ mode }) => {
|
||||
const VITE_APP_PROXY: string = loadEnv(mode, process.cwd()).VITE_APP_PROXY;
|
||||
|
||||
return defineConfig({
|
||||
plugins: [vue(), UnoCSS()],
|
||||
server: {
|
||||
proxy: {
|
||||
"/api": {
|
||||
target: VITE_APP_PROXY,
|
||||
changeOrigin: true,
|
||||
rewrite: (path) => path.replace(/^\/api/, ""),
|
||||
},
|
||||
},
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": resolve(__dirname, "src"),
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
Loading…
Reference in New Issue
Block a user