Commit 9c46c67b by qianmo

框架搭建完成

parent eabd9836
VITE_APP_NAME=demo_app
VITE_ROUTE_MODE=history
VITE_ROUTE_BASE_URL=/
VITE_ASSETS_BASE_URL=/
VITE_TITLE=产品检索管理器
VITE_TITLE_APP=测试后台app
VITE_TITLE_SIMPLE=测试
VITE_APP_NAME=demo_app_dev
# VITE_REQUEST_BASE_URL=http://192.168.0.210:10000/hy-web
VITE_REQUEST_BASE_URL=http://192.168.0.214:8000
VITE_REQUEST_BASE_URL=http://192.168.110.200:10000/hy-web
# 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?
{
"recommendations": ["Vue.volar", "Vue.vscode-typescript-vue-plugin"]
}
# 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) (and disable Vetur) + [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin).
## Type Support For `.vue` Imports in TS
TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin) to make the TypeScript language service aware of `.vue` types.
If the standalone TypeScript plugin doesn't feel fast enough to you, Volar has also implemented a [Take Over Mode](https://github.com/johnsoncodehk/volar/discussions/471#discussioncomment-1361669) that is more performant. You can enable it by the following steps:
1. Disable the built-in TypeScript Extension
1. Run `Extensions: Show Built-in Extensions` from VSCode's command palette
2. Find `TypeScript and JavaScript Language Features`, right click and select `Disable (Workspace)`
2. Reload the VSCode window by running `Developer: Reload Window` from the command palette.
// 如果运行报错 Cannot use import statement outside a module 换成
// const fs = require("fs");
import fs from "fs";
const htmlPath = "./dist/index.html"; // 打包后的html文件路径
const htmlText = fs.readFileSync(htmlPath, 'utf8');
const htmlArr = htmlText.match(/.*\n/g) || [];
let result = "";
let count = 1;
htmlArr.forEach(v => {
v = v
.replace(/script ?nomodule\s?/g, "script ")
.replace(/\s?crossorigin\s?/g, " ")
.replace(/data-src/g, 'src');
if (!v.includes(`script type="module"`)) {
result += v;
}
});
let result_arr = result.match(/.*\n/g) || [];
result = ""
result_arr.forEach(v => {
// 处理内联脚本
if (v.includes(`script`)) {
let start_index = v.indexOf('>');
let end_index = v.lastIndexOf('<')
let script_str = v.substring(start_index + 1, end_index)
if (count === 1) {
let path = "./dist/assets/" + String(count) + ".js";
fs.writeFileSync(path, script_str, 'utf8');
path = "." + path.substring(6)
v = v.substring(0, start_index) + " src=\"" + path + "\">" + v.substring(end_index);
}
else if (count === 3) {
let path = "./dist/assets/" + String(count) + ".js";
v = v.substring(0, start_index + 1) + v.substring(end_index);
fs.writeFileSync(path, script_str, 'utf8');
path = "." + path.substring(6)
v += "\n <script src=\"" + path + "\"></script>";
}
count++;
// console.log(v);
}
if (!v.includes(`script type="module"`)) {
result += v;
}
})
fs.writeFileSync(htmlPath, result, 'utf8');
console.log("处理完成");
\ No newline at end of file
<!doctype html>
<html lang="en">
<!DOCTYPE html>
<html lang="en" translate="no">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + Vue + TS</title>
<script>
if (!!window.ActiveXObject || "ActiveXObject" in window) {
alert("当前浏览器不支持,请使用360浏览器极速模式或谷歌浏览器等现代浏览器");
}
// todo https://github.com/vitejs/vite/issues/2618
window.global = window;
</script>
<script type="text/javascript" src="https://gosspublic.alicdn.com/aliyun-oss-sdk-6.17.1.min.js"></script>
<title>协议适配器</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
<script type="module" src="/src/main.js"></script>
</body>
</html>
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg t="1641437247641" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"
p-id="12533" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200">
<defs>
<style type="text/css"></style>
</defs>
<path d="M614.213818 71.819636a58.181818 58.181818 0 0 1 77.940364 86.318546l-3.095273 2.792727-379.880727 319.092364a34.909091 34.909091 0 0 0-4.282182 49.198545l1.861818 2.024727 1.978182 1.861819 381.021091 330.589091A58.181818 58.181818 0 0 1 616.750545 954.181818l-3.258181-2.629818L232.494545 621.032727a151.272727 151.272727 0 0 1-2.56-226.280727l4.398546-3.816727L614.213818 71.819636z"
p-id="12534"></path>
</svg>
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg t="1641019428859" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="9394"
xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200">
<defs>
<style type="text/css"></style>
</defs>
<path d="M819.2 729.088V757.76c0 33.792-27.648 61.44-61.44 61.44H266.24c-33.792 0-61.44-27.648-61.44-61.44v-28.672c0-74.752 87.04-119.808 168.96-155.648 3.072-1.024 5.12-2.048 8.192-4.096 6.144-3.072 13.312-3.072 19.456 1.024C434.176 591.872 472.064 604.16 512 604.16c39.936 0 77.824-12.288 110.592-32.768 6.144-4.096 13.312-4.096 19.456-1.024 3.072 1.024 5.12 2.048 8.192 4.096 81.92 34.816 168.96 79.872 168.96 154.624z"
p-id="9395"></path>
<path d="M359.424 373.76a168.96 152.576 90 1 0 305.152 0 168.96 152.576 90 1 0-305.152 0Z" p-id="9396"></path>
</svg>
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg t="1645088065459" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4925"
xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200">
<defs>
<style type="text/css"></style>
</defs>
<path d="M979.792374 404.577188 574.183101 83.942886c-34.918864-27.694272-89.619352-27.694272-124.538216 0L44.207626 404.577188c-13.933143 11.008903-16.169326 31.134554-5.332437 44.895683s30.618512 16.169326 44.551655 5.332437l12.55703-10.320847 0 387.547791c0 54.872501 57.968755 95.983874 108.712918 95.983874l639.892491 0c50.22812 0 83.254829-38.531161 83.254829-95.983874L927.844112 445.860575l11.69696 8.944734c5.84848 4.644381 13.073072 6.880564 20.125651 6.880564 9.460776 0 18.921552-4.128339 25.286074-12.213002C995.9617 435.711742 993.725517 415.586091 979.792374 404.577188zM479.919368 864.026877 479.919368 686.508315c0-8.77272 15.997312-13.245087 31.994625-13.245087s31.994625 4.472367 31.994625 13.245087l0 177.346548L479.919368 864.026877 479.919368 864.026877zM864.026877 832.032253c0 21.157736-5.84848 31.994625-19.26558 31.994625L608.585923 864.026877c0-0.516042-0.688056-0.860071-0.688056-1.376113L607.897867 686.508315c0-37.155048-29.930455-77.234336-95.983874-77.234336s-95.983874 40.079288-95.983874 77.234336l0 176.142449c0 0.516042 0.860071 0.860071 0.860071 1.376113L204.868806 864.026877c-20.125651 0-44.723669-17.373425-44.723669-31.994625L160.145137 393.740299 488.864102 134.171006c11.868974-9.288762 33.198723-9.288762 44.895683 0l330.095078 261.11742L863.854863 832.032253z"
p-id="4926"></path>
</svg>
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg t="1638237486545" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2518"
xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200">
<defs>
<style type="text/css"></style>
</defs>
<path d="M853.333 247.467H170.667c-17.067 0-34.134-17.067-34.134-34.134S153.6 179.2 170.667 179.2h682.666c17.067 0 34.134 17.067 34.134 34.133s-17.067 34.134-34.134 34.134z m0 298.666H170.667c-17.067 0-34.134-17.066-34.134-34.133s17.067-34.133 34.134-34.133h682.666c17.067 0 34.134 17.066 34.134 34.133s-17.067 34.133-34.134 34.133z m0 298.667H170.667c-17.067 0-34.134-17.067-34.134-34.133s17.067-34.134 34.134-34.134h682.666c17.067 0 34.134 17.067 34.134 34.134S870.4 844.8 853.333 844.8z"
p-id="2519"></path>
</svg>
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg t="1638237461076" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2347"
xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200">
<defs>
<style type="text/css"></style>
</defs>
<path d="M908.70574649 831.22148693H134.16925867v-52.07344924h774.54813867v52.07344924zM531.80092872 442.96292125H130.86041885V390.889472h400.94050987zM531.80092872 634.17075485H130.86041885v-52.05597298h400.94050987zM901.4996992 245.91801458H126.9690368v-52.06762383h774.54231325v52.06762383zM669.95081672 668.29607822V367.76254578l225.4205383 150.2609408z"
p-id="2348"></path>
</svg>
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg t="1640766239094" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="8346"
xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200">
<defs>
<style type="text/css"></style>
</defs>
<path d="M469.333333 768c-166.4 0-298.666667-132.266667-298.666666-298.666667s132.266667-298.666667 298.666666-298.666666 298.666667 132.266667 298.666667 298.666666-132.266667 298.666667-298.666667 298.666667z m0-85.333333c119.466667 0 213.333333-93.866667 213.333334-213.333334s-93.866667-213.333333-213.333334-213.333333-213.333333 93.866667-213.333333 213.333333 93.866667 213.333333 213.333333 213.333334z m251.733334 0l119.466666 119.466666-59.733333 59.733334-119.466667-119.466667 59.733334-59.733333z"
fill="#444444" p-id="8347"></path>
</svg>
<template>
<div class="h-screen w-screen overflow-hidden bg-gray-100">
<div
class="mb-1 flex w-full items-center justify-between shadow-md"
:style="{
height: headerHeight,
backgroundColor: '#23479C',
color: 'white',
}"
>
<div
class="flex w-1/4 cursor-pointer items-center justify-center"
@click="routeTo('index')"
>
<div class="text-center">{{ configKit.title }}</div>
</div>
<el-menu
:class="menuWith === '' ? 'grow' : ''"
:style="menuWith === '' ? {} : { width: menuWith }"
activeTextColor="#00d0FF"
textColor="white"
:unique-opened="true"
:collapse-transition="false"
backgroundColor="#23479C"
mode="horizontal"
:default-active="storeCurrentRoute.name"
@select="routeTo"
>
<template v-for="(item, index) in storePageMenu" :key="item.name">
<el-sub-menu
v-if="menuItemFilter(item.children).length > 0"
:index="item.name"
>
<template #title>
<div class="flex items-center">
<kit-icon class="h-4 w-4" :name="item.menuIcon"></kit-icon>
<span class="ml-2">{{ item.menuTitle }}</span>
</div>
</template>
<el-menu-item
v-for="child in menuItemFilter(item.children)"
:key="child.name"
:index="child.name"
>
<template #title>
{{ child.menuTitle }}
</template>
</el-menu-item>
</el-sub-menu>
<el-menu-item
v-else-if="
item.name && item.component && (!item.authFunc || item.authFunc())
"
:index="item.name"
>
<div class="flex h-full items-center justify-center">
<kit-icon class="h-4 w-4" :name="item.menuIcon"></kit-icon>
</div>
<template #title>
<span class="ml-2">{{ item.menuTitle }}</span>
</template>
</el-menu-item>
</template>
</el-menu>
<div class="mx-4 rounded-full border border-solid border-white">
<kit-icon
name="common-avatar"
class="h-7 w-7 text-white"
@click="usercenter = true"
></kit-icon>
</div>
<el-drawer
v-model="usercenter"
title="个人中心"
size="200px"
direction="rtl"
>
<user-center class="text-black"/>
</el-drawer>
</div>
<div
class="w-full overflow-auto px-4 pb-4 pt-2"
:style="{ height: 'calc(100vh - ' + headerHeight + ')' }"
>
<el-breadcrumb separator="/" v-if="navigator.length > 0" class="mb-4">
<el-breadcrumb-item v-for="(item, index) in navigator"
><span :class="index === 1 ? 'text-blue-500' : ''">{{
item.menuTitle
}}</span></el-breadcrumb-item
>
</el-breadcrumb>
<router-view/>
</div>
</div>
</template>
<script setup>
import {ref, onMounted, computed} from "vue"
import {storePageMenu} from "/lib/router"
import {useRouter} from "vue-router"
import {configKit, storeCurrentRoute} from "/lib/store"
import UserCenter from "./user-center.vue"
defineProps({
menuWith: {
type: String,
default: "",
},
})
const router = useRouter()
// 顶部高
const headerHeight = ref("60px")
const usercenter = ref(false)
function routeTo(name) {
router.push({name})
}
// menu
function menuItemFilter(itemChildren) {
if (!itemChildren) itemChildren = []
return itemChildren.filter((child) => !child.authFunc || child.authFunc())
}
const navigator = computed(() => {
// {name, menuTitle}
const navigatorArray = []
for (let e of storePageMenu) {
if (!e.children || e.children.length === 0) continue
for (let ee of e.children) {
if (storeCurrentRoute.name === ee.name) {
navigatorArray.push(e)
navigatorArray.push(ee)
}
}
}
return navigatorArray
})
onMounted(() => {
})
</script>
<style>
.el-menu--horizontal {
border-bottom-width: 0;
}
</style>
import KitLayoutAdmin from "./kit-layout-admin.vue"
export default {
install: function (Vue) {
Vue.component("kit-layout-admin", KitLayoutAdmin)
},
}
<template>
<div class="flex h-screen w-screen overflow-hidden" id="home_page">
<div :style="{ width: menuWidth }">
<el-menu
:collapse="isCollapse"
class="h-screen"
activeTextColor="#00d0FF"
textColor="white"
:unique-opened="true"
:collapse-transition="false"
backgroundColor="#23479C"
mode="vertical"
:default-active="storeCurrentRoute.meta[RouteMetaKey.parentName] ||storeCurrentRoute.name"
@select="routeTo">
<div
class="flex cursor-pointer items-center justify-center bg-blue-900 p-2 text-white shadow-md"
@click="routeTo('index')"
:style="{ height: headerHeight }">
<div v-if="!isCollapse" class="text-center">
{{ configKit.title }}
</div>
<div v-else class="text-center">{{ configKit.titleSimple }}</div>
</div>
<div :style="{height: 'calc(100vh - '+headerHeight+')'}">
<el-scrollbar max-height="100%">
<template v-for="(item, index) in storePageMenu" :key="item.name">
<el-sub-menu
v-if="menuItemFilter(item.children).length > 0"
:index="item.name">
<template #title>
<el-icon>
<kit-icon class="h-4 w-4" :name="item.menuIcon"></kit-icon>
</el-icon>
<span v-if="!isCollapse">{{ item.menuTitle }}</span>
</template>
<el-menu-item
v-for="child in menuItemFilter(item.children)"
:key="child.name"
:index="child.name">
<template #title>
{{ child.menuTitle }}
</template>
</el-menu-item>
</el-sub-menu>
<el-menu-item
v-else-if="
item.name && item.component && (!item.authFunc || item.authFunc())
"
:index="item.name"
>
<el-icon>
<kit-icon class="h-4 w-4" :name="item.menuIcon"></kit-icon>
</el-icon>
<!-- <div class="flex justify-center items-center h-full">-->
<!-- </div>-->
<template #title>
<span>{{ item.menuTitle }}</span>
</template>
</el-menu-item>
</template>
</el-scrollbar>
</div>
</el-menu>
</div>
<div class="h-screen bg-gray-100">
<div
class="mb-1 flex w-full items-center justify-between bg-white shadow-md"
:style="{ height: headerHeight }"
>
<kit-icon
:name="isCollapse ? 'common-menu-open' : 'common-menu-close'"
class="ml-2 h-5 w-5"
@click="setCollapse(!isCollapse)"
/>
<div class="mr-4 rounded-full border border-solid border-blue-600">
<kit-icon
name="common-avatar"
class="h-7 w-7 text-blue-600"
@click="usercenter = true"
></kit-icon>
</div>
<el-drawer
v-model="usercenter"
title="个人中心"
size="200px"
direction="rtl"
>
<user-center/>
</el-drawer>
</div>
<div
class="overflow-auto p-2"
:style="{
width: 'calc(100vw - ' + menuWidth + ')',
height: 'calc(100vh - ' + headerHeight + ')',
}">
<router-view/>
</div>
</div>
</div>
</template>
<script setup>
import {ref, onMounted, computed} from "vue"
import {RouteMetaKey, storePageMenu} from "/lib/router"
import {useRouter} from "vue-router"
import {configKit, storeCurrentRoute} from "/lib/store"
import UserCenter from "./user-center.vue"
const router = useRouter()
// 顶部高
const headerHeight = ref("60px")
// menu width
const menuWidth = ref("200px")
const isCollapse = ref(false)
const usercenter = ref(false)
function routeTo(name) {
router.push({name})
}
function menuChange() {
const element = document.getElementById("home_page")
setCollapse(element.offsetWidth < 1200)
}
function setCollapse(collapse) {
isCollapse.value = collapse
if (isCollapse.value) {
menuWidth.value = "63px"
} else {
menuWidth.value = "200px"
}
}
// menu
function menuItemFilter(itemChildren) {
if (!itemChildren) itemChildren = []
return itemChildren.filter((child) => !child.authFunc || child.authFunc())
}
onMounted(() => {
menuChange()
// window.onresize = menuChange
})
</script>
<template>
<div></div>
</template>
<script setup>
import {ref, onMounted} from "vue"
import {useRouter} from "vue-router"
import {useLoading} from "/lib/service"
const router = useRouter()
const loading = ref(false)
onMounted(useLoading(loading, async () => {
}))
</script>
<template>
<div class="flex h-screen w-screen flex-col bg-gray-100">
<div
class="flex h-[48px] items-center bg-blue-500 text-lg text-white shadow-md"
>
<div class="w-[25%] pl-4" @click="back">
<kit-icon name="common-arrow-left" class="h-[20px] w-[20px]"/>
</div>
<div class="w-[50%] text-center">
{{ router.currentRoute.value.meta.menuTitle }}
</div>
<div class="w-[25%]"></div>
</div>
<div class="overflow-auto" style="height: calc(100% - 48px)">
<router-view/>
</div>
</div>
</template>
<script setup>
import {ref, onMounted} from "vue"
import {useRouter} from "vue-router"
import {useLoading} from "/lib/service"
const router = useRouter()
const loading = ref(false)
function back() {
router.back()
}
onMounted(useLoading(loading, async () => {
}))
</script>
<template>
<div
class="flex h-screen w-screen flex-col place-content-between bg-gray-100"
>
<div
class="flex h-[48px] items-center justify-center bg-blue-500 text-lg text-white shadow-md"
>
{{ configKit.titleApp }}
</div>
<div class="overflow-auto" style="height: calc(100% - 48px - 50px)">
<router-view/>
</div>
<div class="flex w-full bg-white shadow">
<div
v-for="(item, index) in storeAppMenu"
:key="item.name"
class="flex h-[50px] flex-col items-center justify-center"
:class="
storeCurrentRoute.name === item.name
? 'text-blue-500'
: 'text-gray-600'
"
:style="{ width: 'calc(100% / ' + storeAppMenu.length + ')' }"
@click="jump(item.name)"
>
<kit-icon :name="item.menuIcon" class="mb-[3px] h-[22px] w-[22px]"/>
<div class="text-xs">{{ item.menuTitle }}</div>
</div>
</div>
</div>
</template>
<script setup>
import {ref, onMounted} from "vue"
import {useRouter} from "vue-router"
import {useLoading} from "/lib/service"
import {storeAppMenu} from "../router"
import {configKit, storeCurrentRoute} from "../store"
const props = defineProps({
// todo
noPadding: {
type: Boolean,
default: false,
},
})
const router = useRouter()
const loading = ref(false)
function jump(name) {
router.push({name})
}
onMounted(useLoading(loading, async () => {
}))
</script>
<template>
<div class="flex flex-col items-center justify-center gap-2">
<div class="mb-4 flex w-full flex-col items-center">
<div>欢迎</div>
<div class="mt-1 text-xl font-bold">
{{ storeUserInfo.user ? storeUserInfo.user.name : "--" }}
</div>
</div>
<div class="w-full">
<el-button class="w-full" type="primary" @click="showUpdatePwd"
>修改密码
</el-button
>
</div>
<div class="w-full">
<el-button class="w-full" type="danger" @click="logout"
>退出登录
</el-button
>
</div>
<kit-modal
id="update-pwd"
:modal="modal"
width="400px"
:confirm="updatePwd"
>
<template #title>账户密码修改</template>
<el-form
ref="form"
label-width="100px"
:model="modal.data"
v-if="modal.data"
>
<el-form-item
label="原密码:"
prop="oldPwd"
:rules="[{ required: true, message: '请填写密码' }]"
>
<el-input
clearable
v-model="modal.data.oldPwd"
type="password"
autocomplete="new-password"
/>
</el-form-item>
<el-form-item
label="新密码:"
prop="newPwd"
:rules="[
{ required: true, message: '请填写密码' },
{ type: 'string', min: 6, message: '密码长度不能小于6位' },
]"
>
<el-input
clearable
v-model="modal.data.newPwd"
type="password"
autocomplete="new-password"
/>
</el-form-item>
<el-form-item
label="确认密码:"
prop="pwdCheck"
:rules="[{ required: true, validator: passwordCheck }]"
>
<el-input clearable v-model="modal.data.pwdCheck" type="password"/>
</el-form-item>
</el-form>
</kit-modal>
</div>
</template>
<script setup>
import {ref} from "vue"
import {useRouter} from "vue-router"
import {storeUserInfo} from "../store"
import {UserLogout, UserUpdatePwd} from "../dao/user"
import {RouteName} from "../router"
import {useLoadingModal} from "../service"
import {ElMessage} from "element-plus"
const router = useRouter()
const modal = ref({
visible: false,
loading: false,
data: null,
})
const form = ref()
let props = defineProps({
beforeLogout: {
type: Function,
default: async () => {
},
},
})
function passwordCheck(rule, value, callback) {
if (!value && modal.value.data.newPwd && modal.value.data.newPwd !== "") {
callback(new Error("请再次输入密码"))
} else if (value !== modal.value.data.newPwd) {
callback(new Error("两次输入密码不一致!"))
} else {
callback()
}
}
function showUpdatePwd() {
modal.value = {
visible: true,
data: {},
}
}
async function updatePwd() {
const valid = await form.value.validate()
if (!valid) return
await useLoadingModal(modal, async () => {
await UserUpdatePwd({
oldPwd: modal.value.data.oldPwd,
newPwd: modal.value.data.newPwd,
})
ElMessage.success("修改成功")
modal.value.visible = false
})()
}
async function logout() {
// todo global loading
await props.beforeLogout()
await UserLogout()
await router.push({name: RouteName.login})
}
</script>
## components
### todo
外部项目采用unplugin-vue-components实现按需自动加载,但其原理是直接在源文件中加入了import语句,故不能影响到node-modules中的依赖源文件?
所以在本组件中,如果依赖了第三方的组件,需要用use的方式,全局的方式局部加载进来。
// todo 使用component时,全局注册组件中所有依赖的组件,那么在外部工程中即可用on-demand
// todo unplugin-vue-components resolver
export const WebkitUnpluginResolver = (name) => {
// where `name` is always CapitalCase
if (name.startsWith("Kit")) {
console.log(name)
}
// return { importName: name.slice(3), path: '/lib/components' }
}
import KitEmpty from "./kit-empty.vue"
export default {
install: function (Vue) {
// Vue.use(ElEmpty)
Vue.component("KitEmpty", KitEmpty)
},
}
<template>
<el-empty :image="img" :image-size="size">
<template #description>
<div class="text-gray-700">
<slot></slot>
</div>
</template>
<!-- <template #default></template>-->
</el-empty>
</template>
<script setup>
// 可以传入图片地址
defineProps({
img: {
type: String,
default: null,
},
size: {
type: Number,
default: 100,
},
})
</script>
import KitErrChannel from "./kit-err-channel.vue"
import {ElAlert} from "element-plus"
export default {
install: function (Vue) {
Vue.use(ElAlert)
Vue.component("KitErrChannel", KitErrChannel)
},
}
<template>
<div
v-if="
storeErrMsg.submitId === id && storeErrMsg.msg && storeErrMsg.msg !== ''
"
>
<el-alert :title="storeErrMsg.msg" type="error" show-icon></el-alert>
</div>
</template>
<script setup>
import {storeErrMsg} from "../store"
defineProps({
id: {
type: String,
default: null,
},
})
// watch(() => storeErrMsg.time, () => {
// console.log(storeErrMsg.submitId)
// if (storeErrMsg.submitId === props.id) {
// console.log(msg.value)
// msg.value = storeErrMsg.msg;
// }
// });
</script>
import KitFabricShow from "./kit-fabric-show.vue"
export default {
install: function (Vue) {
Vue.component("KitFabricShow", KitFabricShow)
},
}
<template>
<div class="w-full h-full" ref="container">
<canvas :id="'fabric-canvas-'+id"></canvas>
</div>
</template>
<script setup>
/***
* fabric 展示组件
*/
import {fabric} from "fabric";
import {onMounted, ref, watch} from "vue";
const props = defineProps({
id:{
type: Number,
default:0
},
objects: {
type: Array,
default: ()=>[],
},
backgroundColor:{
type: String,
default: '#d4e6ff'
}
})
const canvas = ref({})
const panning = ref()
const container = ref()
const first = ref(false)
function initObjs(){
let topMax=undefined, leftMax=undefined,topMin=undefined,leftMin = undefined
let firstFlag = props.objects && props.objects.length>0 && !first.value
if(firstFlag){
// 画布长宽
let width = window.getComputedStyle(container.value, null)["width"]
let height = window.getComputedStyle(container.value, null)["height"]
canvas.value.setWidth(parseInt(width))
canvas.value.setHeight(parseInt(height))
}
for(let e of props.objects){
// 元素不可选中
e.selectable = false
// 缩放自适应:取元素中最大left和top
if(firstFlag){
if(topMax===undefined){
topMax = e["top"]+e["height"]
leftMax = e["left"]+e["width"]
topMin = e["top"]
leftMin = e["left"]
continue
}
if(e["top"]<topMin){
topMin = e["top"]
}
if(e["left"]<leftMin){
leftMin = e["left"]
}
if(e["top"]+e["height"]>topMax){
topMax = e["top"]+e["height"]
}
if(e["left"]+e["width"]>leftMax){
leftMax = e["left"]+e["width"]
}
}
}
// 图形所在的有效区域,包含边距 todo 按理应该是leftMin*2
let objWidth = Math.abs(leftMax-leftMin)+Math.abs(leftMin*1/3)
let objHeight = Math.abs(topMax-topMin)+Math.abs(topMin*1/3)
canvas.value.loadFromJSON(JSON.stringify({
version: "5.2.1",
background: props.backgroundColor,
objects: props.objects
}), canvas.value.renderAll.bind(canvas.value), function (o, object) {
object.toObject = (function (toObject) {
return function () {
return fabric.util.object.extend(toObject.call(this), {
__id: o.__id,
__type: o.__type
})
}
})(object.toObject)
})
if(firstFlag) {
// 根据宽高比
if (objWidth * canvas.value.height > objHeight * canvas.value.width) {
// 自适应-缩放
canvas.value.zoomToPoint({x: 0, y: 0}, canvas.value.width / objWidth)
// 自适应-移动中心点-y轴
canvas.value.relativePan(new fabric.Point(0, canvas.value.height / 2 - (canvas.value.width * objHeight / objWidth) / 2));
} else if (objWidth * canvas.value.height < objHeight * canvas.value.width) {
// 自适应-缩放
canvas.value.zoomToPoint({x: 0, y: 0}, canvas.value.height / objHeight)
// todo 缩放的有点多
// console.log(canvas.value.height/objHeight)
// 自适应-移动中心点-x轴
canvas.value.relativePan(new fabric.Point(canvas.value.width / 2 - (canvas.value.height * objWidth / objHeight) / 2, 0));
} else {
canvas.value.zoomToPoint({x: 0, y: 0}, canvas.value.width / objWidth)
}
first.value = true
}
}
watch(()=>props.objects, initObjs)
onMounted(function (){
canvas.value = new fabric.Canvas('fabric-canvas-'+props.id);
canvas.value.on('mouse:wheel', function (opt) {
let delta = opt.e.deltaY;
let zoom = canvas.value.getZoom();
zoom *= 0.999 ** delta;
// 最大最小放大倍数
if (zoom > 5) zoom = 5;
if (zoom < 0.2) zoom = 0.2;
canvas.value.zoomToPoint({ x: opt.e.offsetX, y: opt.e.offsetY }, zoom);
opt.e.preventDefault();
opt.e.stopPropagation();
});
canvas.value.on('mouse:down', function (e) {
if (!canvas.value.getActiveObject()) {
panning.value = true;
canvas.value.selection = false;
}
});
//鼠标抬起
canvas.value.on('mouse:up', function (e) {
panning.value = false;
canvas.value.selection = true;
});
//鼠标移动
canvas.value.on('mouse:move', function (e) {
if (panning.value && e && e.e) {
let delta = new fabric.Point(e.e.movementX, e.e.movementY);
canvas.value.relativePan(delta);
}
});
// group select
canvas.value.selection = false
initObjs()
})
</script>
import KitGantt from "./kit-gantt.vue"
import "dhtmlx-gantt/codebase/dhtmlxgantt.css"
export default {
install: function (Vue) {
Vue.component("KitGantt", KitGantt)
},
}
<template>
<div class="w-full" ref="ganttRef"></div>
</template>
<script setup>
/***
* 甘特图组件
* https://github.com/DHTMLX/gantt
* https://docs.dhtmlx.com/gantt/desktop__loading.html#dataproperties
* tasks data 参数:
* id, text, start_date:Date/string, end_date(duration),id, progress(0-1),row_height,bar_height
* parent 父id,
* color, textColor, progressColor
*/
import {onMounted, onUnmounted, ref} from "vue";
import { gantt } from 'dhtmlx-gantt'
const props = defineProps({
// 任务对象
tasks: {
type: Object,
default: () => {
return {
data: [], // 数据数组
links: [] // 连接关系
}
}
},
// 显示列设置
columns: {
type: Array,
default: () => {
return []
}
},
// 显示单位
scaleUnit: {
type: String,
default: 'day' // “minute”, “hour”, “day”, “week”, “quarter”, “month”, “year”
},
// 时间显示格式
dateScale: {
type: String,
default: '%Y-%m-%d'
}
})
const ganttRef = ref()
function init(){
// 默认配置
gantt.config.xml_date = '%Y-%m-%d'
gantt.i18n.setLocale('cn') // 设置中文
gantt.config.readonly = true // 设置为只读
// 显示列配置
gantt.config.columns = props.columns
gantt.config.scale_unit = props.scaleUnit
gantt.config.date_scale = props.dateScale
gantt.init(ganttRef.value)
refresh()
}
function refresh(){
// 渲染数据
gantt.clearAll()
gantt.parse(props.tasks)
}
onMounted(init)
// 不能destructor,否则$services为null
// onUnmounted(()=>gantt.destructor())
defineExpose({refresh})
</script>
// icon雪碧图
import "virtual:svg-icons-register"
import KitIcon from "./kit-icon.vue"
export default {
install: function (Vue) {
Vue.component("KitIcon", KitIcon)
},
}
<template>
<svg aria-hidden="true">
<use v-if="symbolId" :xlink:href="symbolId" fill="currentColor"/>
</svg>
</template>
<script setup>
// vite-plugin-svg-icons,引入svg的icon
import {computed} from "vue"
// name: <dir>-<iconName>
const props = defineProps({
name: {
type: String,
default: null,
},
})
const symbolId = computed(() => {
return "#icon-" + props.name
})
</script>
import KitModal from "./kit-modal.vue"
import KitErrChannel from "./kit-err-channel"
export default {
install: function (Vue) {
// Vue.use(ElButton)
// Vue.use(ElDialog)
Vue.use(KitErrChannel)
Vue.component("KitModal", KitModal)
},
}
<template>
<el-dialog
v-model="modal.visible"
:draggable="draggable"
:close-on-click-modal="closeOnClickModal"
:close-on-press-escape="showClose"
:show-close="showClose"
:before-close="cancel"
:width="width"
append-to-body
top="8vh"
>
<template #header>
<div class="flex items-center justify-center">
<slot name="title"/>
</div>
</template>
<el-scrollbar max-height="62vh" v-loading="modal.loading">
<slot />
</el-scrollbar>
<KitErrChannel class="mt-2" :id="id"/>
<template #footer v-if="!noFooter">
<div class="flex justify-end">
<el-button
type="default"
plain
@click="cancel"
:loading="
Object.keys(modal).indexOf('loading') > -1 ? modal.loading : false
"
>取消
</el-button>
<el-button
type="primary"
@click="ok"
:loading="
Object.keys(modal).indexOf('loading') > -1 ? modal.loading : false
"
>
{{ confirmText }}
</el-button>
</div>
</template>
</el-dialog>
</template>
<script setup>
import {submitErrChanel} from "../store"
import KitErrChannel from "./kit-err-channel.vue"
import {toRef} from "vue"
import {useLoadingModal, useLoadingObject} from "../service"
const props = defineProps({
confirm: {
type: Function,
default: async () => {
},
},
close: {
type: Function,
default: () => {
},
},
modal: {
type: Object,
default: {visible: true, loading: false},
},
width: {
type: String,
default: "40%",
},
noFooter: {
type: Boolean,
default: false,
},
showClose: {
type: Boolean,
default: true,
},
draggable:{
type: Boolean,
default:true
},
id: {
type: String,
default: null,
},
closeOnClickModal: {
type: Boolean,
default: false,
},
confirmText: {
type: String,
default: "确定",
},
})
function cancel() {
if (props.id) {
// clearErrMsg(props.id);
// 关闭后还原err channel
submitErrChanel("")
}
props.close()
props.modal.visible = false
}
async function ok() {
if (props.id) {
submitErrChanel(props.id)
}
await useLoadingObject(props.modal, props.confirm)()
props.modal.visible = false
}
</script>
import KitPaginationPage from "./kit-pagination-page.vue"
export default {
install: function (Vue) {
Vue.component("KitPaginationPage", KitPaginationPage)
},
}
<template>
<div>
<slot/>
<kit-empty v-if="data.length === 0">暂无内容</kit-empty>
<div class="flex justify-center">
<el-pagination
v-if="fromServer"
background
class="mt-1"
layout="total, sizes, prev, pager, next, jumper"
:total="total"
:current-page.sync="currentPageInner"
@current-change="pageServerHandle0"
@size-change="handleSizeChange"
:page-sizes="pageSizes"
/>
<el-pagination
v-else
background
layout="total, sizes, prev, pager, next, jumper"
class="mt-2"
:total="data.length"
:page-sizes="pageSizes"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</div>
</template>
<script setup>
import {computed, onMounted, ref, watch} from "vue"
const props = defineProps({
data: {
type: Array,
default: () => [],
},
// 分页
pageSize: {
type: Number,
default: 10,
},
pageSizes: {
type: Array,
default: () => [10, 20, 30, 50],
},
currentPage: {
type: Number,
default: 1,
},
// 服务端分页开关:在服务端分页模式,data不需要填
// eg: ref="table" :from-server="true" :page-server-handle="pageHandle"
// 通过refresh触发初始和刷新
fromServer: {
type: Boolean,
default: false,
},
// 服务端分页处理函数,
// 传入参数:page-当前页;countInPage-一页的个数
// 返回值 {data, currentPage-当前页, total-总数, totalPage-总页数}
pageServerHandle: {
type: Function,
default: () => {
},
},
})
const emit = defineEmits(["update:modelValue"])
const pageCount = ref(1)
// 正常操作变化
const currentPageInner = ref(props.currentPage)
watch(
() => props.currentPage,
(currentPage) => (currentPageInner.value = currentPage)
)
const pageSizeInner = ref(props.pageSize)
watch(
() => props.pageSize,
(pageSize) => (pageSizeInner.value = pageSize)
)
function handleSizeChange(val) {
pageSizeInner.value = val
}
function handleCurrentChange(val) {
currentPageInner.value = val
}
function _displayData() {
if (props.fromServer) {
return dataList.value
} else {
return props.data.slice(
(currentPageInner.value - 1) * pageSizeInner.value,
currentPageInner.value * pageSizeInner.value
)
}
}
const displayData = computed(() => _displayData())
watch(displayData, (d) => emit("update:modelValue", _displayData()))
onMounted(() => {
emit("update:modelValue", _displayData())
})
// 服务端分页处理函数包装
const dataList = ref([])
const currentPage = ref(1)
const total = ref(0)
async function pageServerHandle0(page) {
currentPageInner.value = page
const data = await props.pageServerHandle(page, pageSizeInner.value)
dataList.value = data.data
currentPage.value = data.currentPage
total.value = data.total
total.totalPage = data.totalPage
}
// 服务端分页时外部调用刷新,也是初始触发的接口
async function refresh(pageNo) {
await pageServerHandle0(pageNo ? pageNo : currentPageInner.value)
}
defineExpose({refresh})
</script>
import KitRichText from "./kit-rich-text.vue"
export default {
install: function (Vue) {
Vue.component("KitRichText", KitRichText)
},
}
<template>
<div v-loading="loading">
<Editor
:api-key="configKit.tinymceApiKey"
:disabled="disabled"
ref="editor"
v-model="content"
:init="option"
/>
</div>
</template>
<script setup>
import Editor from "@tinymce/tinymce-vue"
import {onMounted, ref, watch} from "vue"
import {publicUrl, putObjectCommon} from "../service3/oss/oss-helper"
import {useLoading} from "../service"
import {configKit} from "../store"
const props = defineProps({
modelValue: {
type: String,
default: "",
},
disabled: {
type: Boolean,
default: false,
},
})
const emit = defineEmits(["update:modelValue"])
const loading = ref(false)
const content = ref()
const editor = ref()
const option = ref({
height: 500,
menubar: false,
branding: false, // 隐藏右下角技术支持
plugins:
"print preview importcss searchreplace autolink autosave save directionality visualblocks visualchars fullscreen image link media template codesample table charmap hr nonbreaking anchor toc insertdatetime advlist lists wordcount imagetools textpattern noneditable help charmap quickbars emoticons",
language: "zh_CN",
// language_url: process.env.BASE_URL + 'tinymce/langs/zh_CN.js',
// toolbar: 'undo redo | bold italic underline strikethrough | fontselect fontsizeselect formatselect | alignleft aligncenter alignright alignjustify | outdent indent | numlist bullist checklist | forecolor backcolor casechange permanentpen formatpainter removeformat | charmap emoticons | fullscreen preview | insertfile image media link anchor | a11ycheck',
// https://www.tiny.cloud/docs/api/tinymce/tinymce.editor/#setcontent
toolbar:
"undo redo | bold italic underline strikethrough | fontselect | fontsizeselect | formatselect | alignleft aligncenter| outdent indent| forecolor backcolor casechange permanentpen formatpainter removeformat| insertfile image link anchor | a11ycheck",
async images_upload_handler(blobInfo, success, failure) {
const key = await putObjectCommon(blobInfo.blob())
success(await publicUrl(key))
},
})
watch(
() => props.modelValue,
function () {
if (props.modelValue !== content.value) {
content.value = props.modelValue
}
}
)
watch(content, function () {
if (content.value !== props.modelValue) {
emit("update:modelValue", content.value)
}
})
// 初始化时赋值, editor需要等待初始化完成
async function checkInit() {
if (editor.value.editor && editor.value.editor.initialized) {
editor.value.editor.setContent(props.modelValue)
} else {
console.log("tinymce init error", editor.value)
setTimeout(checkInit, 1000)
}
}
onMounted(
useLoading(loading, async () => {
// await checkInit();
content.value = props.modelValue
})
)
</script>
<style>
.tox-tinymce-aux {
z-index: 9999 !important;
}
</style>
import KitRollTitle from "./kit-roll-title.vue"
export default {
install: function (Vue) {
// Vue.use(ElEmpty)
Vue.component("KitRollTitle", KitRollTitle)
},
}
<template>
<div class="marquee w-full">
<div class="marquee-wrap">
<div class="marquee-content">
<slot></slot>
</div>
</div>
</div>
</template>
<script setup></script>
<style scoped>
.marquee {
overflow: hidden;
}
.marquee .marquee-wrap {
width: 100%;
animation: marquee-wrap 4s infinite linear;
}
.marquee .marquee-content {
float: left;
white-space: nowrap;
min-width: 100%;
animation: marquee-content 4s infinite linear;
}
@keyframes marquee-wrap {
0%,
30% {
transform: translateX(0);
}
70%,
100% {
transform: translateX(100%);
}
}
@keyframes marquee-content {
0%,
30% {
transform: translateX(0);
}
70%,
100% {
transform: translateX(-100%);
}
}
</style>
import KitTable from "./kit-table.vue"
export default {
install: function (Vue) {
Vue.component("KitTable", KitTable)
},
}
<template>
<div>
<el-table
ref="table"
stripe
:data="displayData"
@selection-change="selectChange"
@sort-change="sortChange"
>
<slot/>
</el-table>
<div class="flex justify-center">
<el-pagination
v-if="fromServer"
background
class="mt-1"
layout="total, sizes, prev, pager, next, jumper"
:total="total"
v-model:currentPage="currentPageInner"
@current-change="pageServerHandle0"
@size-change="handleSizeChange"
v-model:page-size="pageSizeInner"
:page-sizes="pageSizes"
/>
<el-pagination
v-else-if="!noPagination"
background
layout="total, sizes, prev, pager, next, jumper"
class="mt-1"
:total="data?.length||0"
:page-sizes="pageSizes"
v-model:page-size="pageSizeInner"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</div>
</template>
<script setup>
import {computed, ref, watch} from "vue"
const props = defineProps({
data: {
type: Array,
default: () => [],
},
tableRef:{
type: Object,
default:()=>null
},
selectChange: {
type: Function,
default: () => {},
},
// 分页数
pageSize: {
type: Number,
default: 10,
},
pageSizes: {
type: Array,
default: () => [10, 20, 30, 50],
},
currentPage: {
type: Number,
default: 1,
},
// 服务端分页开关:在服务端分页模式,data不需要填
// eg: ref="table" :from-server="true" :page-server-handle="pageHandle"
// 通过refresh触发初始和刷新
fromServer: {
type: Boolean,
default: false,
},
// 服务端分页处理函数,
// 传入参数:page-当前页;countInPage-一页的个数
// 返回值 {data, currentPage-当前页, total-总数, totalPage-总页数}
pageServerHandle: {
type: Function,
default: () => {
},
},
noPagination: {
type: Boolean,
default: false,
},
})
const emit = defineEmits(["update:currentPage", "update:pageSize", "update:tableRef"])
// 正常操作变化
const currentPageInner = ref(props.currentPage)
watch(
() => props.currentPage,
(currentPage) => (currentPageInner.value = currentPage)
)
watch(currentPageInner, (currentPageInner) =>
emit("update:currentPage", currentPageInner)
)
const pageSizeInner = ref(props.pageSize)
watch(
() => props.pageSize,
(pageSize) => (pageSizeInner.value = pageSize)
)
watch(pageSizeInner, (pageSizeInner) => emit("update:pageSize", pageSizeInner))
function handleSizeChange(val) {
// 每页个数更改
pageSizeInner.value = val
if (props.fromServer) {
refresh(1)
}
}
function handleCurrentChange(val) {
currentPageInner.value = val
}
// table展示的数据
const displayData = computed(() => {
if (props.fromServer) {
return dataList.value
} else if (props.noPagination) {
return props.data || []
} else {
if (props.data) {
return props.data.slice(
(currentPageInner.value - 1) * pageSizeInner.value,
currentPageInner.value * pageSizeInner.value
)
} else {
return []
}
}
})
// 因为用了分页,所以需要sortChange重新排序
function sortChange(p) {
if (!p.prop) {
return
}
// todo 存在children, 联合排序key
const ps = p.prop.split(".")
props.data?.sort((a, b) => {
let aa = a[ps[0]]
let bb = b[ps[0]]
for (let i = 1; i < ps.length; i++) {
aa = aa[ps[i]]
bb = bb[ps[i]]
}
let res
if(p.column.sortMethod){
// 调用el-table-column的sort-method函数,注意参数是row
res = p.column.sortMethod(a,b)
}else{
if (aa > bb) {
res = 1
} else if (aa < bb) {
res = -1
} else {
res = 0
}
}
if (p.order === "ascending") {
return res
} else {
return 0-res
}
})
}
// 使用:v-on:ref="table = $event"
const table = ref(null)
watch(table, () => {
emit("update:tableRef", table.value)
})
// 服务端分页处理函数包装
const dataList = ref([])
const currentPage = ref(1)
const total = ref(0)
const totalPage = ref(1)
async function pageServerHandle0(page) {
currentPageInner.value = page
const data = await props.pageServerHandle(page, pageSizeInner.value)
dataList.value = data.data
currentPage.value = data.currentPage
total.value = data.total
total.totalPage = data.totalPage
}
// 服务端分页时外部调用刷新,也是初始触发的接口
async function refresh(pageNo) {
await pageServerHandle0(pageNo ? pageNo : currentPageInner.value)
}
defineExpose({refresh})
</script>
import KitUpload from "./kit-upload.vue"
export default {
install: function (Vue) {
Vue.component("KitUpload", KitUpload)
},
}
<template>
<div class="flex">
<el-upload
v-if="!disabled"
action=""
list-type="picture-card"
:accept="accept"
:http-request="action"
:file-list="fileList"
:on-preview="handlePictureCardPreview"
:on-remove="handleRm"
>
<el-icon>
<plus/>
</el-icon>
</el-upload>
<!-- <div v-for="f in files" :key="f" class="_flex_center ml-1 gap-0.5">-->
<!-- <img :src="f" alt="" :style="{ maxHeight: fileMaxHeight + 'px' }" />-->
<!-- </div>-->
<el-dialog v-model="modal.visible">
<img :src="modal.data" alt="Preview Image"/>
</el-dialog>
</div>
</template>
<script setup>
import {Plus} from "@element-plus/icons-vue"
import {onMounted, ref, watch} from "vue"
import _ from 'lodash'
/**
* action demo:
* async function parseJson(option){
* if (option.file.size > 1024 * 1024) {
* ElMessage.error('图片大小请小于1M');
* throw Error('图片大小请小于1M');
* }
* await useLoadingModal(modal, async ()=>{
* const key = await putObjectCommon(option.file);
* modal.value.data.img = publicUrl(key)
* ElMessage.success('上传成功');
* })()
* }
*/
const props = defineProps({
action: {
type: Function,
default: async () => {
},
},
accept: {
type: String,
default: "image/png, image/jpeg",
},
files: {
type: Array,
default: () => [],
},
disabled: {
type: Boolean,
default: false,
},
fileMaxHeight: {
type: Number,
default: 148,
},
})
const fileList = ref([])
const modal = ref({
visible: false,
data: null
})
const updateFiles = _.debounce(() => {
fileList.value = []
if (!props.files) return
for (let e of props.files) {
if(e){
fileList.value.push({
url: e
})
}
}
}, 300, {leading: true, trailing: false})
watch(() => props.files, updateFiles)
onMounted(updateFiles)
const handlePictureCardPreview = (uploadFile) => {
modal.value = {visible: true, data: uploadFile.url}
}
const handleRm = (uploadFile) => {
_.remove(props.files, (n) => n === uploadFile.url)
}
</script>
import {download, request, upload} from "/lib/request"
import {configKit} from "../store";
/// 列表所有的省市
export async function CommonAdministrativeListAllProvinceCity() {
const {data} = await request(
"/rest/common/administrative/listAllProvinceCity"
)
return data.data
}
/// 按市列出区
// * cityCode : string :
export async function CommonAdministrativeListAreaByCity(params) {
const {data} = await request(
"/rest/common/administrative/listAreaByCity",
params
)
return data.data
}
/// ali sts 获取
export async function StsGet() {
const {data} = await request("/rest/sts/get")
return data.data
}
/// 公共下载
// * name : string :, config.filename
export async function CommonDownload(params,filename){
await download('/rest/common/download', params,{filename})
}
/// 获取服务器时间
export async function CommonTime(){
const {data} = await request('/rest/common/time')
return data.data
}
/// 私有下载
// * name : string :; config.filename
export async function Download(params,filename){
await download('/rest/download', params,{filename})
}
/// 私有上传
// * file :
// path: 相对项目目录路径
export async function Upload(params){
await upload('/rest/upload', params)
}
/// 获取目录文件列表
// path: 相对项目目录路径
export async function ListFileNames(params){
const {data} = await request('/rest/file/list', params)
return data.data
}
/// 删除文件
// path: 相对项目目录路径
export async function DelFile(params){
const {data} = await request('/rest/file/del', params)
return data.data
}
// 私有下载url
export function FileLinkPrivate(path, dt) {
if (path) { return configKit.requestBaseUrl + '/rest/download?name=' + path + (dt ? ('&dt=' + dt?.getTime()) : ''); }
}
// 公共下载url
export function FileLink(path, dt) {
if (path) { return configKit.requestBaseUrl + '/rest/common/download?name=' + path + (dt ? ('&dt=' + dt?.getTime()) : ''); }
}
import {request} from "../request"
import {configKit, rmStoreUserInfo} from "../store"
/// 用户信息. auth: request 处理中是否对未登录处理
export async function UserInfo(auth) {
const config = {showMsg: false, throwable: false, auth}
const {data} = await request("/rest/user/info", {schema: configKit.schema}, config)
return data
}
/// 登出
export async function UserLogout() {
rmStoreUserInfo()
const config = {showMsg: false, throwable: false}
await request("/rest/user/logout", config)
}
/// 密码修改
// * oldPwd : string :
// * newPwd : string :
export async function UserUpdatePwd(params) {
const {data} = await request("/rest/user/updatePwd", params)
return data.data
}
/// 登录
// username : string : 用户名
// phone : string : 手机号
// * pwd : string :
// schema : string :
export async function UserLogin(params){
const {data} = await request('/rest/user/login', params)
return data.data
}
// https://v3.cn.vuejs.org/api/application-api.html#directive
// v-dialogDrag: 弹窗拖拽
export const directive = {
mounted(el) {
console.log(el)
},
}
// export function directiveDialogDrag(app){
// app.directive('dialogDrag', (el, binding, vnode, oldVnode)=> {
// const dialogHeaderEl = el.querySelector('._drag')
// const dragDom = el.querySelector('.el-dialog')
// dialogHeaderEl.style.cursor = 'move'
//
// // 获取原有属性 ie dom元素.currentStyle 火狐谷歌 window.getComputedStyle(dom元素, null);
// const sty = dragDom.currentStyle || window.getComputedStyle(dragDom, null)
//
// dialogHeaderEl.onmousedown = (e) => {
// // 鼠标按下,计算当前元素距离可视区的距离
// const disX = e.clientX - dialogHeaderEl.offsetLeft
// const disY = e.clientY - dialogHeaderEl.offsetTop
// // 获取到的值带px 正则匹配替换
// let styL, styT
// // 注意在ie中 第一次获取到的值为组件自带50% 移动之后赋值为px
// // if (sty.left.includes('%')) {
// // styL = +document.body.clientWidth * (+sty.left.replace(/\%/g, '') / 100)
// // styT = +document.body.clientHeight * (+sty.top.replace(/\%/g, '') / 100)
// // } else {
// // styL = +sty.left.replace(/\px/g, '')
// // styT = +sty.top.replace(/\px/g, '')
// // }
// document.onmousemove = function(e) {
// // 通过事件委托,计算移动的距离
// const l = e.clientX - disX
// const t = e.clientY - disY
//
// // 移动当前元素
// dragDom.style.left = `${l + styL}px`
// dragDom.style.top = `${t + styT}px`
//
// // 将此时的位置传出去
// // binding.value({x:e.pageX,y:e.pageY})
// }
//
// document.onmouseup = function(e) {
// document.onmousemove = null
// document.onmouseup = null
// }
// }
// })
// }
# pc
## bridge的使用
demo:
```js
// bridge; router中需要query=port,将port传入init
init(router.currentRoute.query.port?parseInt(router.currentRoute.query.port):8000)
// 添加数据接收逻辑处理
addHandler("monitor-data", function (msg){
console.log(msg)
})
```
import io from "socket.io-client"
import {pushErrMsg, storeErrMsg} from "../store"
import _ from "lodash"
let socket = null
// client的req或是server的req
// export interface MsgReq {
// code;
// data;
// }
//
// export interface MsgRes {
// result: boolean;
// message?;
// data;
// }
const EventPublic = "event:public"
// 需要在最外层的main的onMounted中初始化
export function init(port) {
// socket = io(window.location.host);
socket = io(window.location.hostname + ":" + port)
socket.on("connect", function () {
console.log("connect")
})
// 接收server传回的数据
socket.on(EventPublic, function (data) {
const json = JSON.parse(data)
if (eventHandlers[json.code]) {
eventHandlers[json.code](json)
}
})
}
/// key:code
const eventHandlers = {}
export function addHandler(code, fun) {
eventHandlers[code] = fun
}
/// 重载配置项
export const RequestConfig = {
errShowFunc(msg) {
// Message.error(msg);
},
}
export function request(code, data, config) {
return new Promise(function (resolve, reject) {
const data = {code: "test", data: "1"}
socket.emit(EventPublic, JSON.stringify(data), function (json) {
const ret = JSON.parse(json)
if (ret.result) {
resolve(ret.data)
} else {
const showMsg = _.isNil(config.showMsg) || config.showMsg
const throwable = _.isNil(config.throwable) || config.throwable
const msg = ret.message
if (showMsg && msg) {
pushErrMsg({
src: code,
msg,
})
// 如果未配置errMsgChannel则
if (storeErrMsg.submitId === "") {
RequestConfig.errShowFunc(msg)
}
}
if (throwable) {
throw new Error(msg)
}
}
})
})
}
// todo request和await的处理,以及http后await接收处理
// https://github.com/ecomfe/vue-echarts
import ECharts from "vue-echarts"
import {use} from "echarts/core"
// import ECharts modules manually to reduce bundle size
import {CanvasRenderer, SVGRenderer} from "echarts/renderers"
import {
BarChart,
PieChart,
CustomChart,
LineChart,
ScatterChart,
} from "echarts/charts"
import {
DataZoomComponent,
ToolboxComponent,
LegendComponent,
VisualMapComponent,
GridComponent,
TooltipComponent,
} from "echarts/components"
use([
CanvasRenderer,
SVGRenderer,
BarChart,
PieChart,
CustomChart,
LineChart,
ScatterChart,
GridComponent,
TooltipComponent,
DataZoomComponent,
ToolboxComponent,
LegendComponent,
VisualMapComponent,
])
export default {
install: function (Vue) {
Vue.component("v-chart", ECharts)
},
}
export {default as VueEcharts} from "./echarts"
// help for vite.config.js
import _ from "lodash"
import {loadEnv} from "vite"
import {createSvgIconsPlugin} from "vite-plugin-svg-icons"
import path from "path"
import AutoImport from "unplugin-auto-import/vite"
import Components from "unplugin-vue-components/vite"
import {ElementPlusResolver, VantResolver} from "unplugin-vue-components/resolvers"
const envResolve = (mode) => {
return loadEnv(mode, process.cwd())
}
// 获取env变量,todo 只能获取VITE_?
export const getEnv = function (env) {
return envResolve(_.last(process.argv))[env]
}
/**
* id=icon: 需要配合KitIcon组件
* @param plugins
* @param id
* @param config
*/
export function pluginAdd(plugins, id, config) {
switch (id) {
case "icon":
if (!config) config = "src/assets/icons"
plugins.push(
createSvgIconsPlugin({
iconDirs: [path.resolve(process.cwd(), config)],
})
)
break
case "element":
// 自动按需导入:原理是直接在源文件中加入了import语句
plugins.push(
AutoImport({
// todo 已经在plugin/webkit.js中导入了所有的css
// resolvers: [ElementPlusResolver()],
resolvers: [
ElementPlusResolver(_.merge({importStyle: false}, config)),
],
})
)
plugins.push(
Components({
resolvers: [
ElementPlusResolver(_.merge({importStyle: false}, config)),
],
})
)
break
case "vant":
// todo 已经在plugin/webkit中导入了所有的css
// todo 4.0 https://vant-ui.github.io/vant/#/zh-CN/quickstart
plugins.push(Components({
resolvers: [VantResolver()],
}))
break
default:
throw new Error("不支持的plugin id")
}
}
export const HttpHeader = {
contentTypeKey: "Content-Type",
contentTypeForm: "application/x-www-form-urlencoded",
contentTypeJson: "application/json",
contentTypeMultipart: "multipart/form-data",
tokenKey: "Token"
}
import Qs from "qs"
import axios from "axios"
import {HttpHeader} from "./const"
import {pushErrMsg, storeErrMsg, configKit, storeUserInfo} from "../store"
import {ElMessage} from "element-plus"
export function setAxiosDefaultBaseUrl() {
let baseUrl = configKit.requestBaseUrl
if (baseUrl) {
axios.defaults.baseURL = baseUrl
}
}
/// 重载配置项
export const RequestConfig = {
// 错误信息展示func
errShowFunc(msg) {
ElMessage({
showClose: true,
message: msg,
type: "error",
})
},
// 错误信息拦截处理
errDataHandleFunc(e, config) {
let auth = true
if (config.auth === false) auth = false
// 只处理result==2,其他交给catch
if (auth && e.response && e.response.data && e.response.data.result === 2) {
window.location.reload()
}
return true
},
}
function errHandle(e, url, config) {
const er = RequestConfig.errDataHandleFunc(e, config)
if (!er) return
let showMsg = true
if (config.showMsg === false) showMsg = false
let throwable = true
if (config.throwable === false) throwable = false
let response = e.response
if (!response) {
response = {}
}
const data = response.data
const msg = data ? data.message : e.message
// 接口错误后的json数据处理
if (data) {
if (showMsg && msg) {
pushErrMsg({
src: url,
msg,
})
// 如果未配置errMsgChannel则
if (storeErrMsg.submitId === "") {
RequestConfig.errShowFunc(msg)
}
}
if (throwable) {
throw new Error(msg)
}
} else if (throwable) {
// 有可能是cancel todo
throw new Error(msg)
}
response.data = {
message: msg,
result: response.status,
}
return response
}
/**
* 如果是上传,设置content-type
* @param url
* @param data object
* @param config: showMsg, throwable, auth, axios_configs(https://github.com/axios/axios)
*/
export function request(url, data, config) {
// 补全headers
if (!config) config = {}
if (!config.headers) config.headers = {}
config.headers[HttpHeader.tokenKey] = storeUserInfo.token||''
if (!config.headers[HttpHeader.contentTypeKey]) {
config.headers[HttpHeader.contentTypeKey] = HttpHeader.contentTypeForm
}
if (!config.method) {
config.method = "post"
}
let defaultConfig = {
url,
data,
method: config.method,
withCredentials: true,
transformRequest: [
function (data, headers) {
// Do whatever you want to transform the data
if (headers[HttpHeader.contentTypeKey] === HttpHeader.contentTypeForm) {
data = Qs.stringify(data, {
skipNulls: true,
})
} else if (
headers[HttpHeader.contentTypeKey] === HttpHeader.contentTypeMultipart
) {
const param = new FormData()
for (const key of Object.keys(data)) {
param.append(key, data[key])
}
data = param
}
return data
},
],
}
// 构建 axios config
for (let k of Object.keys(config)) {
if (k !== "showMsg" && k !== "throwable") {
defaultConfig[k] = config[k]
}
}
return axios(defaultConfig).catch((e) => {
return errHandle(e, url, config)
})
}
/**
* Download file
* @param url request url
* @param data request data
* @param config filename, showMsg, throwable, axios_configs(https://github.com/axios/axios)
*/
export function download(url, data, config) {
if(!config) config = {}
config.responseType = 'blob'
return request(url, data, config).then((response)=>{
if ('download' in document.createElement('a')) { // 非IE下载
const elink = document.createElement('a');
elink.download = config.filename;
elink.style.display = 'none';
elink.href = window.URL.createObjectURL(response.data);
// console.log(blob)
document.body.appendChild(elink);
elink.click();
window.URL.revokeObjectURL(elink.href); // 释放URL 对象
document.body.removeChild(elink);
} else { // IE10+下载
navigator.msSaveBlob(response.data, config.filename);
}
})
// todo error handle
}
/**
*
* @param url
* @param data file, etc
* @param config showMsg, throwable, axios_configs(https://github.com/axios/axios)
*/
export function upload(url, data, config) {
if (!config) config = {}
if (!config.headers) config.headers = {}
config.headers[HttpHeader.contentTypeKey] = HttpHeader.contentTypeMultipart
if (config.throwable !== false) config.throwable = true
return request(url, data, config)
}
# router
## menu
### 一级二级菜单
因为menu结构信息是和router信息绑定的,所以二级菜单所属的一级菜单也会存在于router中,只是这个route没有component
### router meta 自定义字段
定义于RouteMetaKey:
- authFunc:访问权限判断函数,return bool。***在路由拦截时执行***
- authDisable: 禁用登录拦截。***在路由拦截时执行***
- menu:bool,只用于主路由的meta,表示此主路由将用于menu数据(注:以下的key都是作用于主路由内的子路由)。***menu相关***
- menuIcon:图标name,应用于KitIcon组件。***menu相关***
- menuTitle:string,在main.vue中使用。***menu相关***
- menuBelong:string,一级菜单的name,用于标识此为二级菜单。***menu相关***
- menuEx:bool, 表改路由信息不纳入menu。***menu相关***
// todo
未在menu中的子页面处理
import {
createRouter,
createWebHashHistory,
createWebHistory,
} from "vue-router"
import {buildMenu} from "./menu"
import {configKit} from "../store"
// env: VITE_ROUTE_MODE,BASE_URL
export function useRouter(app, routes) {
let mode
if (
configKit.routeMode === "history" ||
configKit.routeMode === "" ||
!configKit.routeMode
) {
mode = createWebHistory(configKit.routeBaseUrl)
} else {
mode = createWebHashHistory(configKit.routeBaseUrl)
}
const router = createRouter({
history: mode,
routes,
// 跳转时 回到顶部
// scrollBehavior() {
// // savedPosition
// return {
// x: 0,
// y: 0,
// };
// },
})
app.use(router)
// 构建菜单数据
buildMenu(routes)
return router
}
export * from "./menu"
import NProgress from "nprogress"
import "nprogress/nprogress.css"
import {
updateStoreCurrentRoute,
submitErrChanel,
storeUserInfo,
updateStoreUserInfo, initToken,
} from "../store"
import {RouteMetaKey, RouteName, storeAppMenu, storePageMenu} from "./menu"
import {UserInfo} from "../dao/user"
// route定义见menu.js
export function checkAuth(route) {
if (route && route[RouteMetaKey.authFunc]) {
return route[RouteMetaKey.authFunc]()
}
return true
}
// 寻找本用户对应权限的第一个route; storePageMenu/storeAppMenu
export function getMainRoute(store) {
for (const menu of store) {
if (!menu) {
continue
}
if (menu.children && menu.children.length > 0) {
if (!menu) {
continue
}
for (const m of menu.children) {
if (checkAuth(m)) {
return m.name ? {name: m.name} : null
}
}
} else if (checkAuth(menu)) {
return menu.name ? {name: menu.name} : null
}
}
}
// 正常逻辑下的next
function _next(to, next) {
if (to.name === RouteName.index || to.name === RouteName.indexApp) {
if (to.name === RouteName.index) next(getMainRoute(storePageMenu))
else next(getMainRoute(storeAppMenu))
} else {
next()
}
}
// 基础的路由前拦截
export async function routeBaseBefore(to, from, next) {
if (to.name !== from.name) {
NProgress.start()
}
// 区分路由的所属,按前缀,如app:,web-admin无前缀
if (to.name === RouteName.login || to.name === RouteName.loginApp) {
next()
return
}
// 无需登录
if (to.meta[RouteMetaKey.authDisable]) {
_next(to, next)
return
}
// 需要登录,先检查登录信息
await RouteInterceptorConfig.checkUserLogin()
submitErrChanel("")
if (!storeUserInfo.user) {
storeUserInfo.redirect = to
if (to.name.indexOf("app:") === 0) {
next({name: RouteName.loginApp})
} else {
next({name: RouteName.login})
}
} else {
// 用户权限
if (checkAuth(to)) {
_next(to, next)
} else {
// todo
next({name: "404", params: {msg: "当前页面无权限查看"}})
}
}
}
// 基础的路由后拦截
export function routeBaseAfter(to) {
const lastMatch = to.matched[to.matched.length - 1]
if (lastMatch) {
// 先保存当前路由至store
updateStoreCurrentRoute(to)
// 路由完即结束
NProgress.done()
}
}
// 用于router interceptor的自定义代码块
export const RouteInterceptorConfig = {
// 校验用户登录
checkUserLogin: async function () {
if (!storeUserInfo.user) {
initToken();
if(!storeUserInfo.token) return
// 校验登录用户
let data = await UserInfo(false)
if (data.data) {
updateStoreUserInfo(data.data)
}
}
},
}
import {reactive} from "vue"
// item定义:
// name?: string;
// path?: string;
// children?: IMenuItem[];
/// meta定义下:
// icon?: string;
// authFunc?: Function;
// menuTitle?: string;
export const storePageMenu = reactive([])
export const storeAppMenu = reactive([])
// route meta中的定义字段
export const RouteMetaKey = {
menuIcon: "menuIcon", // 图标信息
authFunc: "authFunc", // 访问权限,return bool
authDisable: "authDisable", // 禁用登录拦截
menuTitle: "menuTitle",
menuBelong: "menuBelong", // 值为父menu的名称,将以此作为字段划分到group
menuEx: "menuEx", // bool, 不纳入menu
menuTreeIndex: "menu", // bool, 表示此主路由将用于menu数据
menuAppTreeIndex: "menuApp", // bool, 表示此主路由将用于app-menu数据
menuAppIndex: "menuAppIndex", // bool, 表示app-menu的主菜单之一
parentName: "parentName", // 用于子页面,所属的菜单中的父页面(主要用于menu active显示),同时也将同步置menuEx为false
}
// route 中name
export const RouteName = {
login: "login", // 作为menu的登录页面
loginApp: "app:login", // 作为app-menu的登录页面
registerApp: "app:register", // 作为app-menu的注册页面
index: "index", // 作为menu的主入口
indexApp: "app:index", // 作为app-menu的主入口
subApp: "app:sub", // 作为app-menu-sub子页面入口
}
// 根据路由生成 web admin menu 数据
export function buildMenu(routes) {
_buildWebMenu(routes)
_buildAppMenu(routes)
}
function _buildWebMenu(routes) {
// 筛选menu所在的主路由
let rs = routes.filter((r) => r.meta && r.meta[RouteMetaKey.menuTreeIndex])
if (rs.length === 0) return
let mainRoute = rs[0]
// 取主路由的children作为menu的数据源
let children = mainRoute.children
if (!children || children.length === 0) return
// 临时存放主menu
let pool = {}
// todo 目前只支持二级目录
for (let item of children) {
if (!item.meta) item.meta = {}
if (item.meta[RouteMetaKey.menuEx] || item.meta[RouteMetaKey.parentName]) continue
if (!item.meta[RouteMetaKey.menuBelong]) {
let newItem = _genNewItem(item)
pool[newItem.name] = newItem
storePageMenu.push(newItem)
} else {
// 子级
if (pool[item.meta[RouteMetaKey.menuBelong]]) {
let newItem = _genNewItem(item)
pool[item.meta[RouteMetaKey.menuBelong]].children.push(newItem)
}
}
}
}
function _buildAppMenu(routes) {
// 筛选menu所在的主路由
let rs = routes.filter((r) => r.meta && r.meta[RouteMetaKey.menuAppTreeIndex])
if (rs.length === 0) return
let mainRoute = rs[0]
let children = mainRoute.children
if (!children || children.length === 0) return
// 只支持一级菜单
for (let item of children) {
if (!item.meta) item.meta = {}
if (item.meta[RouteMetaKey.menuAppIndex]) {
let newItem = _genNewItem(item)
storeAppMenu.push(newItem)
}
}
}
function _genNewItem(item) {
return {
name: item.name,
path: item.path,
component: item.component,
// 表示此item为分组,children为分组内的menu子项
children: [],
menuIcon: item.meta[RouteMetaKey.menuIcon],
authFunc: item.meta[RouteMetaKey.authFunc],
menuTitle: item.meta[RouteMetaKey.menuTitle],
}
}
import * as echarts from "echarts/core"
import _ from 'lodash'
// 提供默认grid
export function gridDemo(change){
return {
top: _.isNil(change?.top)?30: change.top,
left: _.isNil(change?.left)?15: change.left,
right: _.isNil(change?.right)?20: change.right,
bottom: _.isNil(change?.bottom)?5: change.bottom,
}
}
// config.darkTheme boolean 是否用暗色模式
export function chartConfig(option, config) {
option.grid = _.merge( {
containLabel:true
},option.grid)
if (!config) config = {}
// tooltip
let tooltipval = false
let tooltippoin = false
if (option.series && option.series.length === 1 && option.series[0].type === "pie") {
tooltipval = true
}
for (const e of option.series) {
if (e.type === "bar") {
tooltippoin = true
e.barMaxWidth = 40
break
}
}
option.tooltip = _.merge( {
trigger: tooltipval ? 'item' : 'axis',
axisPointer: {
type: tooltippoin ? 'shadow' : 'line'
}
},option.tooltip)
// dark theme
if(config.darkTheme){
let themeObj = {}
if (option.legend){
themeObj.legend = {
textStyle: {
color: '#c7d1dd'
}
}
}
if (option.xAxis){
themeObj.xAxis = {
axisLine: {
lineStyle: {
color: '#c7d1dd'
}
},
axisLabel:{
color: '#c7d1dd'
},
nameTextStyle: {
color: '#c7d1dd'
}
}
}
if (option.yAxis){
themeObj.yAxis = {
axisLine: {
lineStyle: {
color: '#c7d1dd'
}
},
axisLabel:{
color: '#c7d1dd'
},
nameTextStyle: {
color: '#c7d1dd'
}
}
}
option = _.merge(option,themeObj)
}
return option
}
// 判断series中是否包含数据
export function chartSeriesDataExist(option) {
if (option && option.series) {
for (let d of option.series) {
if (d.data && d.data.length > 0) {
return true
}
}
}
return false
}
// 计算最大最小值 {max,min}
export function computeMaxMin(data) {
let max = undefined
let min = undefined
for (let e of data) {
if (e instanceof Array) {
for (let ee of e) {
if (max === undefined || max < ee) max = ee
if (min === undefined || min > ee) min = ee
}
} else {
if (max === undefined || max < e) max = e
if (min === undefined || min > e) min = e
}
}
return {max, min}
}
export function chartColor1(startIndex) {
if (startIndex >= 8) startIndex = startIndex % 8
let origin = [
"#0ea5e9",
"#f97316",
"#14b8a6",
"#fb7185",
"#4ade80",
"#d946ef",
"#84cc16",
"#8b5cf6",
]
if (!startIndex) return origin
else {
let fin = [origin[startIndex]]
let start = startIndex + 1
while (start % 8 !== startIndex) {
fin.push(origin[start % 8])
start++
}
return fin
}
}
// 从chartColor1中取隔2的两个颜色
export function linearColor1(chartColor1StartIndex) {
let colors = chartColor1()
return new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{offset: 0, color: colors[chartColor1StartIndex]},
{offset: 1, color: colors[(chartColor1StartIndex + 4) % colors.length]},
])
}
export function linearColorGreen() {
return new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{offset: 0, color: "#14b8a6"},
{offset: 1, color: "#84cc16"},
])
}
export function linearColorCyan() {
return new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{offset: 0, color: "#09aeb9"},
{offset: 1, color: "#43eec6"},
])
}
export function linearColorOrange() {
return new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{offset: 0, color: "#ff6c6c"},
{offset: 1, color: "#ffc326"},
])
}
export function linearColorBlue() {
return new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{offset: 0, color: "#0059e1"},
{offset: 1, color: "#00d2d9"},
])
}
export * from "./loading"
export * from "./search"
import {Toast} from "vant"
export function useAppLoading(fn) {
return async function (...rest) {
let t = Toast.loading({
message: "加载中...",
forbidClick: true,
})
try {
await fn.apply(this, rest)
} catch (e) {
throw e
} finally {
t.clear()
}
}
}
export function useLoading(loadingValue, fn) {
// 环境变量不同
// return useLoadingDirect(loadingValue.value, fn);
return async function (...rest) {
loadingValue.value = true
try {
await fn.apply(this, rest)
} catch (e) {
throw e
} finally {
loadingValue.value = false
}
}
}
// modal.value.loading需要存在
export function useLoadingModal(modalValue, fn) {
return async function (...rest) {
modalValue.value.loading = true
try {
await fn.apply(this, rest)
} catch (e) {
throw e
} finally {
modalValue.value.loading = false
}
}
}
// object.loading
export function useLoadingObject(object, fn) {
return async function (...rest) {
object.loading = true
try {
await fn.apply(this, rest)
} catch (e) {
throw e
} finally {
object.loading = false
}
}
}
import {ref, watch} from "vue"
import {mapToPinyin} from "../utils"
import _ from "lodash"
function search(row, keywords, option, path = "") {
// const excludeId = option.excludeId === undefined ? true : option.excludeId;
// const excludeProps = option.excludeProps || [];
const includeProps = option.includeProps || []
if (!row) {
return false
}
for (const [prop, col] of Object.entries(row)) {
const propPath = path === "" ? prop : path + "." + prop
if (
// includeProps.length > 0 &&
includeProps.every(
(includeProp) => !includeProp.match(new RegExp(`^${propPath}`))
)
// todo ???
// col !== undefined &&
// col !== null &&
// (excludeId && propPath.toLowerCase().endsWith('id') ||
// excludeProps.includes(prop))
) {
continue
} else if (typeof col !== "object" && searchToken(col, keywords)) {
return true
} else if (Array.isArray(col)) {
for (const item of col) {
if (typeof item !== "object") {
if (searchToken(item, keywords)) {
return true
}
} else if (search(item, keywords, option, propPath)) {
return true
}
}
} else if (typeof col === "object") {
if (search(col, keywords, option, propPath)) {
return true
}
}
}
function searchToken(token, keywords) {
const lower = token.toString().toLowerCase()
const lowerKeyords = keywords.toLowerCase()
return (
lower.includes(lowerKeyords) || toPinyin(lower).includes(lowerKeyords)
)
}
}
export function toPinyin(str) {
const pinyinArr = []
for (const letter of str) {
pinyinArr.push(mapToPinyin(letter) || letter)
}
return pinyinArr.join("")
}
// interface IFilterOption {
// excludeId?: boolean;
// excludeProps?: string[];
// includeProps?: string[];
// delay?: number;
// }
// interface SearchOptions {
// excludeId?: boolean;
// excludeProps?: string[];
// includeProps?: string[];
// delay?: number;
// separator?: string;
// }
/**
* 多关键词搜索,中文搜索
* @param {Ref<any[]>} data source
* @param {Object} opt options
* @param {boolean} opt.excludeId specified if exclude property 'id', default: false
* @param {string[]} opt.excludeProps specified which properties won't be searched, support index-path like 'a.b.c'
* @param {string[]} opt.includeProps specified which properties searched, support index-path like 'a.b.c'
* @param {number} opt.delay if provided, searching will be delayed by this millisecond(s), default: 300
* @return {[]} [keywords, result]
*/
export function useSearch(data, opt) {
if (!data.value) {
data.value = []
}
const delay =
_.isNumber(opt.delay) && !Number.isNaN(opt.delay) ? opt.delay : 150
const separator = opt.separator || " "
const keywords = ref("")
const searchFn = (row) => {
for (const keyword of keywords.value.split(separator).filter(Boolean)) {
if (!search(row, keyword, opt)) {
return false
}
}
return true
}
const result = ref(data.value)
let timer = 0
watch(keywords, () => {
if (!data.value) {
data.value = []
}
clearTimeout(timer)
timer = setTimeout(() => {
result.value = data.value.filter(searchFn)
}, delay)
})
watch(data, () => {
if (!data.value) {
data.value = []
}
result.value = data.value.filter(searchFn)
})
return [keywords, result]
}
import {ElMessage} from "element-plus"
import {generateUUID} from "../../utils"
import {configKit} from "../../store"
import {StsGet} from "../../dao/common"
// todo 打包后出现 is not a constructor
// import * as OSS from 'ali-oss'
const sts = {
client: null,
expire: undefined,
}
// stsGet:Function
// 注意bucket需要默认设置为私有
async function ossClient() {
if (!sts.expire || sts.expire.getTime() < new Date().getTime()) {
const data = await StsGet()
try {
sts.client = new OSS({
region: data.region,
accessKeyId: data.accessKey,
accessKeySecret: data.accessKeySecret,
stsToken: data.stsToken,
bucket: data.bucket,
})
sts.expire = new Date(data.expiration)
} catch (e) {
console.log(e)
}
}
return sts.client
}
export async function putObjectCommon(file, folder) {
let pre = "common/"
if (folder) {
pre = pre + folder + "/"
}
const key =
pre + generateUUID() + file.name.substring(file.name.lastIndexOf("."))
await putObject(key, file, true)
return key
}
export async function putObjectPrivate(file, folder) {
let pre = "private/"
if (folder) {
pre = folder
if (pre.lastIndexOf("/") !== pre.length - 1) pre += "/"
}
const key =
pre + generateUUID() + file.name.substring(file.name.lastIndexOf("."))
await putObject(key, file)
return key
}
export async function putObjectUserPrivate(uid, file) {
const key =
"user/" +
uid +
"/" +
generateUUID() +
file.name.substring(file.name.lastIndexOf("."))
await putObject(key, file)
return key
}
export async function putObject(key, file, pub) {
const client = await ossClient()
try {
const res = await client.put(key, file)
if (pub) {
await client.putACL(key, "public-read")
}
return res
} catch (e) {
ElMessage.error("上传失败")
throw e
}
}
export async function privateUrl(key) {
const client = await ossClient()
return client.signatureUrl(key)
}
export function publicUrl(key) {
return configKit.ossUrlPrefix + key
}
import {reactive} from "vue"
import {setAxiosDefaultBaseUrl} from "../request"
/**
* webkit需要的环境配置项,允许从import.meta.env中加载.
*/
export const configKit = reactive({
appName: undefined,
requestBaseUrl: undefined, // request中使用的base url
routeMode: undefined, // router
routeBaseUrl: undefined, // router中的base url
assetsBaseUrl: undefined, // assets中的base url
title: undefined, // 用于web页面的title
titleSimple: undefined,
titleApp: undefined, // 用于app页面的title,如果不存在将替换为TITLE
ossUrlPrefix: undefined, // oss url 地址的前缀
tinymceApiKey: undefined, // 富文本编辑器tinymce api key
schema: undefined, // 用于后端
})
export function configKitInit(env) {
configKit.appName = env.VITE_APP_NAME
configKit.requestBaseUrl = env.VITE_REQUEST_BASE_URL
configKit.routeMode = env.VITE_ROUTE_MODE
configKit.routeBaseUrl = env.VITE_ROUTE_BASE_URL
configKit.assetsBaseUrl = env.VITE_ASSETS_BASE_URL
configKit.title = env.VITE_TITLE
configKit.titleSimple = env.VITE_TITLE_SIMPLE
configKit.titleApp = env.VITE_TITLE_APP
if (!configKit.titleApp || configKit.titleApp === "") {
configKit.titleApp = configKit.title
}
configKit.ossUrlPrefix = env.VITE_OSS_PREFIX
configKit.tinymceApiKey = env.VITE_WEB_TINYMCE_KEY
configKit.schema = env.VITE_SCHEMA
// request set
setAxiosDefaultBaseUrl()
}
// 实时保存当前路由对象
import {reactive} from "vue"
export const storeCurrentRoute = reactive({
name: "index",
path: "/",
meta: undefined,
query: undefined,
params: undefined,
})
export function updateStoreCurrentRoute(route) {
storeCurrentRoute.meta = route.meta
storeCurrentRoute.name = route.name
storeCurrentRoute.path = route.path
storeCurrentRoute.query = route.query
storeCurrentRoute.params = route.params
}
/** 错误信息的临时通道 */
import {reactive} from "vue"
export const storeErrMsg = reactive({
msg: null,
// 来源,如接口:/xx/xxx
src: null,
// 1-serious, 2-warning
level: 1,
submitId: "",
// 用于触发watch
time: new Date(),
})
export function pushErrMsg(obj) {
storeErrMsg.msg = obj.msg
storeErrMsg.src = obj.src
storeErrMsg.level = obj.level !== undefined ? obj.level : 1
storeErrMsg.time = new Date()
}
// 只发送msg。外部api
export function pushMsgErr(msg) {
pushErrMsg({
msg,
src: "",
})
}
// 外部api
export function submitErrChanel(id) {
clearErrMsg()
storeErrMsg.submitId = id
}
export function clearErrMsg(id) {
// id可用于特定的清空
if (id && storeErrMsg.submitId !== id) {
return
}
storeErrMsg.msg = null
storeErrMsg.src = null
storeErrMsg.time = new Date()
// 不能清空 不然无法在目标位置触发清空
// storeErrMsg.submitId='';
// return ret;
}
// 可用于全局触发el-message-box的形式
import {reactive} from "vue"
export const storeGlobalMessage = reactive({
trigger: false,
title: null,
content: null,
handleOK: function () {
},
handleCancel: function () {
},
})
export function clearStoreGlobalMessage() {
storeGlobalMessage.trigger = false
storeGlobalMessage.title = null
storeGlobalMessage.content = null
storeGlobalMessage.handleOK = function () {
}
storeGlobalMessage.handleCancel = function () {
}
}
export * from "./configkit"
export * from "./currentRoute"
export * from "./errorMsgChannel"
export * from "./userInfo"
import {reactive} from "vue"
import {configKit} from "./configkit";
export const storeUserInfo = reactive({
user: undefined,
token: undefined,
redirect: undefined,
// expire: undefined,
// setting: undefined,
})
export function initToken() {
const token = localStorage.getItem(configKit.appName + '-token');
if (token) {
storeUserInfo.token = token
}
}
export function updateStoreUserInfo(data) {
storeUserInfo.user = data.user
storeUserInfo.token = data.token
// storeUserInfo.expire = new Date().getTime() + 1000 * 60 * 60 * 20;
localStorage.setItem(configKit.appName + '-token', storeUserInfo.token);
}
export function rmStoreUserInfo() {
for (const key in storeUserInfo) {
delete storeUserInfo[key]
}
localStorage.removeItem(configKit.appName + '-token');
}
export function checkPrivilege(ps, isAnd){
if(ps && ps.length>0){
if(storeUserInfo.user && storeUserInfo.user.role && storeUserInfo.user.role.privileges){
if(isAnd){
// 并的关系
for (let p of ps){
if(storeUserInfo.user.role.privileges.indexOf(p)<0) return false
}
return true
}else{
// 或的关系
for (let p of ps){
if(storeUserInfo.user.role.privileges.indexOf(p)>=0) return true
}
return false
}
}else{
return false
}
}else{
return true
}
}
/** 如果返回-1 表示未找到 */
export function findIndexOfArray(array, val, key) {
for (let i = 0; i < array.length; i++) {
const item = array[i]
if (item instanceof Object) {
if (!key) {
throw Error("findIndexOfArray_key_null")
}
if (item[key] === val) {
return i
}
} else {
if (item === val) {
return i
}
}
}
return -1
}
/**
* 只要list中含有后面传入的所有参数中的一项就返回true
* @param list
* @param strs 第二个以及之后的所有参数集合
*/
export function listContainsOr(list, ...strs) {
return strs.some((str) => list.includes(str))
}
export function listContainAnd(list, ...strs) {
for (const e of strs) {
if (list.indexOf(e) < 0) {
return false
}
}
return true
}
// time.value, finish:async function
export function clockStart(time, finish) {
let f = function () {
setTimeout(async function () {
if (time.value <= 0 || isNaN(time.value)) {
await finish()
} else {
time.value--
f()
}
}, 1000)
}
f()
}
import {leftFill0} from "./index"
const {floor} = Math
function _resolveFormatter(formatter) {
const tokens = formatter
.split(/[\.\:]/)
.filter((token) => token.trim())
.filter(Boolean)
.map((token) => [token, true])
return new Map(tokens)
}
/**
* 格式化毫秒数
* @param ms
* @param zh:是否中文
* @param formatterStr
*/
export function formatMilliseconds(ms, zh, formatterStr = "hh:mm:ss") {
if (isNaN(Number(ms))) {
return "- -"
}
const formatter = _resolveFormatter(formatterStr)
const MM = formatter.get("MM")
const DD = formatter.get("DD")
const hh = formatter.get("hh")
const mm = formatter.get("mm")
const ss = formatter.get("ss")
const mmm = formatter.get("mmm")
const m = formatter.get("m")
ms = floor(ms)
let day = floor(floor(ms / 3600000) / 24)
let hour = floor(ms / 3600000)
let min = floor(ms / 60000)
let sec = floor(ms / 1000)
day = MM ? day % 30 : day
hour = DD ? hour % 24 : hour
min = mm ? min % 60 : min
sec = ss ? sec % 60 : sec
ms = mmm ? ms % 1000 : ms
ms = m ? floor((ms % 1000) / 100) : ms
let ret = ""
if (zh) {
DD && (ret += day + "天")
hh && (ret += hour + "时")
mm && (ret += min + "分")
ss && (ret += sec + "秒")
mmm && (ret += ms + "毫秒")
} else {
DD && (ret += leftFill0(day))
DD && hh && (ret += ":")
hh && (ret += leftFill0(hour))
hh && mm && (ret += ":")
mm && (ret += leftFill0(min))
mm && ss && (ret += ":")
ss && (ret += leftFill0(sec))
ss && (mmm || m) && (ret += ".")
mmm && (ret += leftFill0(ms, 3))
m && (ret += leftFill0(ms, 1))
}
return ret
}
export function date(str){
if (!str) return null
if (!(str instanceof Date)) {
// .replace(/\-/g, '/'
return new Date(str)
}else{
return str
}
}
export function formatYearMonth(str) {
let d = date(str)
if(!d) return null
return (
d.getFullYear() +
"-" +
leftFill0(d.getMonth() + 1)
)
}
export function formatDate(str) {
let d = date(str)
if(!d) return null
return (
d.getFullYear() +
"-" +
leftFill0(d.getMonth() + 1) +
"-" +
leftFill0(d.getDate())
)
}
export function formatTime(str) {
let d = date(str)
if(!d) return null
const h = leftFill0(d.getHours())
const m = leftFill0(d.getMinutes())
const s = leftFill0(d.getSeconds())
return h + ":" + m + ":" + s
}
export function formatTimeHM(str) {
let d = date(str)
if(!d) return null
const h = leftFill0(d.getHours())
const m = leftFill0(d.getMinutes())
return h + ":" + m
}
export function formatDateTime(str) {
let d = date(str)
if(!d) return null
return formatDate(d) + " " + formatTime(d)
}
export function clearHMS(dt) {
dt.setHours(0)
dt.setMinutes(0)
dt.setSeconds(0)
dt.setMilliseconds(0)
}
/**
* 一个对象,根据数字得到中文, 0为星期日
*/
export const weekMap = ["周日", "周一", "周二", "周三", "周四", "周五", "周六"]
/**
* 获取当前时间的格式化对象
*/
export function getClock() {
const dt = new Date()
return {
dt: formatDate(dt),
week: weekMap[dt.getDay()],
time: formatTime(dt),
}
}
/**
* 根据dt获取其所在的一周时间范围 7天
* @param dt
*/
export function getWeekDaysRange(dt) {
clearHMS(dt)
// 0-周日,6-周一
const dt1 = new Date()
dt1.setTime(dt.getTime())
let day = dt.getDay()
if (day === 0) {
day = 7
}
dt1.setDate(dt.getDate() - day + 1)
const res = [dt1]
for (let i = 1; i < 7; i++) {
res.push(datePlus(dt1, i))
}
return res
}
function datePlus(dt, plus) {
const res = new Date(dt.getTime())
res.setDate(res.getDate() + plus)
return res
}
/*
* 将时间转换为指定时区的时间
* @param dt
*/
export function getTimeByZone(timezone = 8, date) {
// 本地时间距离(GMT时间)毫秒数
let nowDate = !date ? new Date().getTime() : new Date(date).getTime()
// 本地时间和格林威治时间差,单位分钟
let offset_GMT = new Date().getTimezoneOffset()
// 反推到格林尼治时间
let GMT = nowDate + offset_GMT * 60 * 1000
// 获取指定时区时间
let targetDate = new Date(GMT + timezone * 60 * 60 * 1000)
return targetDate
}
\ No newline at end of file
export function string2Download(content, title) {
const blob = new Blob([content], {
type: "text/plain",
})
if ("download" in document.createElement("a")) {
// 非IE下载
const elink = document.createElement("a")
elink.download = title
elink.style.display = "none"
elink.href = URL.createObjectURL(blob)
document.body.appendChild(elink)
elink.click()
URL.revokeObjectURL(elink.href) // 释放URL 对象
document.body.removeChild(elink)
} else {
// IE10+下载
navigator.msSaveBlob(blob, title)
}
}
export * from "./array"
export * from "./date"
export * from "./download"
export * from "./logic"
export * from "./number"
export * from "./pinyin-map"
export * from "./regex"
export * from "./uuid"
/**
* 返回一个延迟给定时间resolve的Promise对象,用于使当前异步函数休眠
* @param duration 持续时间(毫秒)
*/
export function sleep(duration) {
return new Promise((resolve) => setTimeout(resolve, duration))
}
// 获取url query中的值,无则返回''
export function getUrlQueryString(name) {
let reg = new RegExp("(^|&)" + name + "=([^&]*)(&|$)", "i");
let r = window.location.search.substring(1).match(reg); // 获取url中"?"符后的字符串并正则匹配
let context = "";
if (r != null)
context = decodeURIComponent(r[2]);
reg = null;
r = null;
return context == null || context === "" || context === "undefined" ? "" : context;
}
/**
* 截取给定位数的数字, 不足则左补0
* @param num 需要截取的数字
* @param n 截取的长度
*/
export function leftFill0(num, n = 2) {
return (new Array(n).join("0") + num).slice(-n)
}
/** m进制的num 转为n进制的 */
export function hexTransfer(num, m, n) {
return parseInt(num, m).toString(n)
}
export function rand(min, max) {
return parseInt(Math.random() * (max - min + 1) + min, 10)
}
// 倒计时 val.value
export function countDown(val, initValue, time) {
val.value = initValue
let fun = () =>
setTimeout(function () {
if (val.value > 0) {
val.value--
fun()
}
}, time)
fun()
}
// 用于规范percent的值
export function percent(a,b){
if(b===0) return 0
if(a/b>1) return 100
return parseFloat(((a / b) * 100).toFixed(2))
}
/**
* Test a string with phone number Reg.
* @param phone
*/
export function regexPhone(phone) {
return /^1[34578]\d{9}$/.test(phone)
}
export function regexIP(ip) {
return /^((2(5[0-5]|[0-4]\d))|[0-1]?\d{1,2})(\.((2(5[0-5]|[0-4]\d))|[0-1]?\d{1,2})){3}$/.test(
ip
)
}
/**
* 生成随机的长度为35的字符串id
*/
export function generateID() {
let ID = ""
for (let i = 0; i < 4; i++) {
for (let j = 0; j < 8; j++) {
const random = Math.random()
const char = charset[floor(random * charset.length)]
ID += char
}
if (i !== 3) {
ID += "-"
}
}
return ID
}
export function generateUUID() {
let d = new Date().getTime()
if (window.performance && typeof window.performance.now === "function") {
d += performance.now() // use high-precision timer if available
}
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function (c) {
const r = (d + Math.random() * 16) % 16 | 0
d = Math.floor(d / 16)
return (c === "x" ? r : (r & 0x3) | 0x8).toString(16)
})
}
This source diff could not be displayed because it is too large. You can view the blob instead.
{
"name": "frontend",
"private": true,
"version": "0.0.0",
"type": "module",
"version": "1.1.0",
"name": "webkit1412-sample",
"author": "mizuki1412",
"license": "MIT",
"repository": "https://github.com/mizuki1412/webkit-sample",
"scripts": {
"dev": "vite",
"build": "vue-tsc && vite build && node handleHTML.js",
"preview": "vite preview",
"html": "node handleHTML.js"
"dev": "vite --mode dev",
"buildDev": "vite build --mode dev",
"build": "vite build --mode pro",
"update-dep": "yarn upgrade-interactive --latest",
"demo_up2lib": "rm -r ../../webkit-sample/lib/* && cp -r lib/* ../../webkit-sample/lib/",
"demo_up4lib": "rm -r lib/* && cp -r ../../webkit-sample/lib/* lib/"
},
"dependencies": {
"@vitejs/plugin-legacy": "^4.1.1",
"axios": "^1.4.0",
"element-plus": "^2.3.9",
"path": "^0.12.7",
"process": "^0.11.10",
"terser": "^5.19.2",
"vue": "^3.3.4",
"vue-router": "^4.2.4"
"@element-plus/icons-vue": "^2.0.9",
"@tinymce/tinymce-vue": "^5.0.0",
"ali-oss": "^6.17.1",
"axios": "^0.27.2",
"browserslist": "^4.21.9",
"caniuse-lite": "^1.0.30001517",
"dhtmlx-gantt": "^7.1.12",
"echarts": "^5.3.3",
"element-plus": "2.2.13",
"fabric": "^5.2.1",
"lodash": "^4.17.21",
"nprogress": "^0.2.0",
"qs": "^6.11.0",
"socket.io-client": "^4.5.1",
"vant": "^3.5.4",
"vue": "^3.2.37",
"vue-echarts": "^6.2.3",
"vue-router": "^4.1.3"
},
"devDependencies": {
"@vitejs/plugin-vue": "^4.2.3",
"typescript": "^5.0.2",
"vite": "^4.4.5",
"vue-tsc": "^1.8.5"
},
"main": "index.js",
"license": "MIT"
"@vitejs/plugin-vue": "^3.0.2",
"@vue/compiler-sfc": "^3.2.37",
"autoprefixer": "^10.4.8",
"postcss": "^8.4.16",
"prettier-plugin-tailwindcss": "^0.1.13",
"tailwindcss": "^3.1.8",
"unplugin-auto-import": "^0.11.1",
"unplugin-icons": "^0.14.8",
"unplugin-vue-components": "^0.22.4",
"vite": "^3.0.6",
"vite-plugin-svg-icons": "^2.0.1"
}
}
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}
module.exports = {
plugins: [require("prettier-plugin-tailwindcss")],
semi: false,
};
<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="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
\ No newline at end of file
<script setup lang="ts">
import { ref } from 'vue'
import { ElButton } from 'element-plus'
import axios from 'axios'
import Test from '@/components/Test'
const count = ref(0)
const add = () => {
count.value++
}
const msg = ref<string>('')
const get_msg = () => {
axios.get('test/')
.then(res => {
console.log(res)
msg.value = res.data.message
})
.catch(err => {
console.error(err)
})
}
</script>
<template>
<el-button @click="add">Button</el-button>
{{ count }}
<br>
<test></test>
<el-button @click="get_msg">get data</el-button>
{{ msg }}
<el-config-provider :locale="zhCn">
<router-view/>
</el-config-provider>
</template>
<script setup>
import {zhCn} from "element-plus/lib/locales"
</script>
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg t="1641437247641" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"
p-id="12533" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200">
<defs>
<style type="text/css"></style>
</defs>
<path d="M614.213818 71.819636a58.181818 58.181818 0 0 1 77.940364 86.318546l-3.095273 2.792727-379.880727 319.092364a34.909091 34.909091 0 0 0-4.282182 49.198545l1.861818 2.024727 1.978182 1.861819 381.021091 330.589091A58.181818 58.181818 0 0 1 616.750545 954.181818l-3.258181-2.629818L232.494545 621.032727a151.272727 151.272727 0 0 1-2.56-226.280727l4.398546-3.816727L614.213818 71.819636z"
p-id="12534"></path>
</svg>
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg t="1641019428859" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="9394"
xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200">
<defs>
<style type="text/css"></style>
</defs>
<path d="M819.2 729.088V757.76c0 33.792-27.648 61.44-61.44 61.44H266.24c-33.792 0-61.44-27.648-61.44-61.44v-28.672c0-74.752 87.04-119.808 168.96-155.648 3.072-1.024 5.12-2.048 8.192-4.096 6.144-3.072 13.312-3.072 19.456 1.024C434.176 591.872 472.064 604.16 512 604.16c39.936 0 77.824-12.288 110.592-32.768 6.144-4.096 13.312-4.096 19.456-1.024 3.072 1.024 5.12 2.048 8.192 4.096 81.92 34.816 168.96 79.872 168.96 154.624z"
p-id="9395"></path>
<path d="M359.424 373.76a168.96 152.576 90 1 0 305.152 0 168.96 152.576 90 1 0-305.152 0Z" p-id="9396"></path>
</svg>
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg t="1645088065459" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4925"
xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200">
<defs>
<style type="text/css"></style>
</defs>
<path d="M979.792374 404.577188 574.183101 83.942886c-34.918864-27.694272-89.619352-27.694272-124.538216 0L44.207626 404.577188c-13.933143 11.008903-16.169326 31.134554-5.332437 44.895683s30.618512 16.169326 44.551655 5.332437l12.55703-10.320847 0 387.547791c0 54.872501 57.968755 95.983874 108.712918 95.983874l639.892491 0c50.22812 0 83.254829-38.531161 83.254829-95.983874L927.844112 445.860575l11.69696 8.944734c5.84848 4.644381 13.073072 6.880564 20.125651 6.880564 9.460776 0 18.921552-4.128339 25.286074-12.213002C995.9617 435.711742 993.725517 415.586091 979.792374 404.577188zM479.919368 864.026877 479.919368 686.508315c0-8.77272 15.997312-13.245087 31.994625-13.245087s31.994625 4.472367 31.994625 13.245087l0 177.346548L479.919368 864.026877 479.919368 864.026877zM864.026877 832.032253c0 21.157736-5.84848 31.994625-19.26558 31.994625L608.585923 864.026877c0-0.516042-0.688056-0.860071-0.688056-1.376113L607.897867 686.508315c0-37.155048-29.930455-77.234336-95.983874-77.234336s-95.983874 40.079288-95.983874 77.234336l0 176.142449c0 0.516042 0.860071 0.860071 0.860071 1.376113L204.868806 864.026877c-20.125651 0-44.723669-17.373425-44.723669-31.994625L160.145137 393.740299 488.864102 134.171006c11.868974-9.288762 33.198723-9.288762 44.895683 0l330.095078 261.11742L863.854863 832.032253z"
p-id="4926"></path>
</svg>
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg t="1638237486545" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2518"
xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200">
<defs>
<style type="text/css"></style>
</defs>
<path d="M853.333 247.467H170.667c-17.067 0-34.134-17.067-34.134-34.134S153.6 179.2 170.667 179.2h682.666c17.067 0 34.134 17.067 34.134 34.133s-17.067 34.134-34.134 34.134z m0 298.666H170.667c-17.067 0-34.134-17.066-34.134-34.133s17.067-34.133 34.134-34.133h682.666c17.067 0 34.134 17.066 34.134 34.133s-17.067 34.133-34.134 34.133z m0 298.667H170.667c-17.067 0-34.134-17.067-34.134-34.133s17.067-34.134 34.134-34.134h682.666c17.067 0 34.134 17.067 34.134 34.134S870.4 844.8 853.333 844.8z"
p-id="2519"></path>
</svg>
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg t="1638237461076" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2347"
xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200">
<defs>
<style type="text/css"></style>
</defs>
<path d="M908.70574649 831.22148693H134.16925867v-52.07344924h774.54813867v52.07344924zM531.80092872 442.96292125H130.86041885V390.889472h400.94050987zM531.80092872 634.17075485H130.86041885v-52.05597298h400.94050987zM901.4996992 245.91801458H126.9690368v-52.06762383h774.54231325v52.06762383zM669.95081672 668.29607822V367.76254578l225.4205383 150.2609408z"
p-id="2348"></path>
</svg>
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg t="1640766239094" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="8346"
xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200">
<defs>
<style type="text/css"></style>
</defs>
<path d="M469.333333 768c-166.4 0-298.666667-132.266667-298.666666-298.666667s132.266667-298.666667 298.666666-298.666666 298.666667 132.266667 298.666667 298.666666-132.266667 298.666667-298.666667 298.666667z m0-85.333333c119.466667 0 213.333333-93.866667 213.333334-213.333334s-93.866667-213.333333-213.333334-213.333333-213.333333 93.866667-213.333333 213.333333 93.866667 213.333333 213.333333 213.333334z m251.733334 0l119.466666 119.466666-59.733333 59.733334-119.466667-119.466667 59.733334-59.733333z"
fill="#444444" p-id="8347"></path>
</svg>
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg t="1635572409144" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2213"
xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200">
<defs>
<style type="text/css"></style>
</defs>
<path d="M512 0C230.4 0 0 230.4 0 512s230.4 512 512 512c281.6 0 512-230.4 512-512S793.6 0 512 0zM713.142857 449.828571c25.6 3.657143 40.228571 14.628571 43.885714 40.228571-3.657143 25.6-18.285714 36.571429-43.885714 40.228571l-135.314286 0 0 62.171429 135.314286 0c25.6 3.657143 40.228571 14.628571 43.885714 40.228571-3.657143 25.6-18.285714 36.571429-43.885714 40.228571l-135.314286 0 0 109.714286C577.828571 822.857143 555.885714 841.142857 512 841.142857c-43.885714 0-65.828571-21.942857-69.485714-62.171429l0-109.714286L310.857143 669.257143c-25.6-3.657143-40.228571-14.628571-43.885714-40.228571 3.657143-25.6 18.285714-36.571429 43.885714-40.228571l135.314286 0 0-62.171429L310.857143 526.628571c-25.6-3.657143-40.228571-14.628571-43.885714-40.228571 3.657143-25.6 18.285714-36.571429 43.885714-40.228571l76.8 0L270.628571 288.914286C259.657143 277.942857 256 263.314286 256 252.342857 259.657143 208.457143 285.257143 186.514286 325.485714 182.857143c25.6 3.657143 43.885714 10.971429 54.857143 29.257143L512 402.285714l135.314286-190.171429C658.285714 193.828571 676.571429 186.514286 698.514286 182.857143c40.228571 3.657143 62.171429 29.257143 69.485714 69.485714 0 14.628571-3.657143 29.257143-14.628571 40.228571l-117.028571 160.914286L713.142857 453.485714z"
p-id="2214"></path>
</svg>
This source diff could not be displayed because it is too large. You can view the blob instead.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment