提交 74c9719d 编写于 作者: View Design's avatar View Design

Thu Jan 2 11:20:00 CST 2025 inscode

上级 669eac6d
.DS_Store # Logs
node_modules logs
/dist *.log
npm-debug.log*
yarn-debug.log*
# local env files yarn-error.log*
.env.local pnpm-debug.log*
.env.*.local lerna-debug.log*
# Log files node_modules
npm-debug.log* dist
yarn-debug.log* dist-ssr
yarn-error.log* *.local
pnpm-debug.log*
package-lock.json # Editor directories and files
.vscode/*
# Editor directories and files !.vscode/extensions.json
.idea .idea
.vscode/* .DS_Store
!.vscode/preview.yml *.suo
*.suo *.ntvs*
*.ntvs* *.njsproj
*.njsproj *.sln
*.sln *.sw?
*.sw?
run = "npm i && npm run dev"
language = "node"
[env]
PATH = "/root/${PROJECT_DIR}/.config/npm/node_global/bin:/root/${PROJECT_DIR}/node_modules/.bin:${PATH}"
XDG_CONFIG_HOME = "/root/.config"
npm_config_prefix = "/root/${PROJECT_DIR}/.config/npm/node_global"
[debugger]
program = "main.js"
{
"recommendations": ["Vue.volar"]
}
# Vue 3 + Vite
This template should help get you started developing with Vue 3 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.
Learn more about IDE Support for Vue in the [Vue Docs Scaling up Guide](https://vuejs.org/guide/scaling-up/tooling.html#ide-support).
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="./src/assets/devui-logo.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>DevUI Admin</title>
<script src="https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js"></script>
</head>
<body>
<div id="app" style="height: 100%;"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>
console.log("欢迎来到 InsCode");
\ No newline at end of file
此差异已折叠。
{ {
"name": "nodejs", "name": "mate-chat-template",
"version": "1.0.0", "private": true,
"description": "", "version": "0.0.0",
"main": "index.js", "type": "module",
"scripts": { "scripts": {
"dev": "node index.js", "dev": "vite",
"test": "echo \"Error: no test specified\" && exit 1" "build": "vite build",
}, "preview": "vite preview"
"keywords": [], },
"author": "", "dependencies": {
"license": "ISC", "@devui-design/icons": "^1.4.0",
"dependencies": { "@floating-ui/dom": "^1.6.12",
"@types/node": "^18.0.6", "clipboard-copy": "^4.0.1",
"node-fetch": "^3.2.6" "devui-theme": "^0.0.7",
} "echarts": "^5.6.0",
} "highlight.js": "^11.11.0",
"lodash": "^4.17.21",
\ No newline at end of file "markdown-it": "^14.1.0",
"openai": "^4.77.0",
"sass": "^1.83.0",
"vue": "^3.5.13",
"vue-devui": "^1.6.29",
"vue-i18n": "^11.0.0-beta.2",
"vue3-sfc-loader": "^0.9.5",
"vuedraggable": "^2.24.3"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.2.1",
"sass-embedded": "^1.83.0",
"vite": "^6.0.3"
}
}
<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>
import Page from './components/Page.vue';
import * as Vue from 'vue';
import * as echarts from 'echarts';
window.Vue = Vue;
window.echarts = echarts;
</script>
<template>
<Page/>
</template>
<style scoped>
</style>
<svg viewBox="0 0 26 26" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<defs>
<linearGradient x1="89.5364583%" y1="21.60078%" x2="7.57349918%" y2="65.7395747%" id="linearGradient-1">
<stop stop-color="#2954C8" offset="0%"></stop>
<stop stop-color="#5170FF" offset="100%"></stop>
</linearGradient>
<linearGradient x1="89.5364583%" y1="21.4573588%" x2="7.57349918%" y2="65.8190624%" id="linearGradient-2">
<stop stop-color="#2954C8" offset="0%"></stop>
<stop stop-color="#5170FF" offset="100%"></stop>
</linearGradient>
<linearGradient x1="-11.5260417%" y1="24.3907324%" x2="87.1145833%" y2="74.8850926%" id="linearGradient-3">
<stop stop-color="#89D2FF" offset="0%"></stop>
<stop stop-color="#5170FF" offset="100%"></stop>
</linearGradient>
<linearGradient x1="0%" y1="18.4813953%" x2="75.9513522%" y2="81.5186047%" id="linearGradient-4">
<stop stop-color="#89D2FF" offset="0%"></stop>
<stop stop-color="#5170FF" offset="100%"></stop>
</linearGradient>
</defs>
<g id="Devui-Logo" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Group-2" transform="translate(3.000000, 0.000000)">
<g>
<path
d="M4.28576801,9.22873192 L9.32098286,6.02205882 L13.1143389,8.49673203 L0.010890596,17.0193525 L0.010890596,17.0193525 C0.010890596,13.8625823 1.62310848,10.9244448 4.28576801,9.22873192 Z"
id="Path-39-Copy-3"
fill="url(#linearGradient-1)"
></path>
<path
d="M8.76945593,17.4828869 L14.1939212,14.0196078 L18.2867527,16.6963836 L4.14882163,25.9150327 L4.14882163,25.9150327 C4.14882163,22.4998846 5.89095741,19.3206798 8.76945593,17.4828869 Z"
id="Path-39-Copy-2"
fill="url(#linearGradient-2)"
transform="translate(11.217787, 19.967320) scale(-1, 1) translate(-11.217787, -19.967320) "
></path>
<path
d="M0.183304389,2.48689958e-13 L13.1143389,8.49673203 L9.42310099,10.9017055 L9.42310099,10.9017055 C4.36778167,11.0371959 0.159798979,7.04888649 0.0243085926,1.99356717 C0.00937841938,1.43650334 0.0453320846,0.879239543 0.13172203,0.32871271 L0.183304389,2.48689958e-13 Z"
id="Path-38-Copy-3"
fill="url(#linearGradient-3)"
></path>
<path
d="M4.54131151,6.55708742 L19.8945577,16.6535948 L16.2033199,19.0585682 L11.1830136,17.9470593 C6.34348625,16.8755752 3.2888836,12.0837536 4.36036765,7.24422626 C4.41158805,7.01288123 4.47194996,6.78365535 4.54131151,6.55708742 L4.54131151,6.55708742 Z"
id="Path-38-Copy-2"
fill="url(#linearGradient-4)"
transform="translate(12.021690, 12.807828) scale(-1, 1) translate(-12.021690, -12.807828) "
></path>
</g>
</g>
</g>
</svg>
\ No newline at end of file
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 261.76 226.69"><path d="M161.096.001l-30.225 52.351L100.647.001H-.005l130.877 226.688L261.749.001z" fill="#41b883"/><path d="M161.096.001l-30.225 52.351L100.647.001H52.346l78.526 136.01L209.398.001z" fill="#34495e"/></svg>
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.27413 23.9492C5.35011 24.002 4.76293 22.9766 5.27617 22.2064L8.12463 17.9319C8.19666 17.8238 8.22896 17.6941 8.21604 17.5649L7.78881 13.2908C7.72637 12.666 8.18863 12.1116 8.81443 12.0606L19.7344 11.1714L13.845 23.5164L6.27413 23.9492V23.9492Z" fill="url(#paint0_linear_0_201)"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M27.562 22.7377C28.093 23.4959 27.5294 24.5347 26.6043 24.5031L21.4724 24.3273C21.3425 24.3229 21.2151 24.3637 21.112 24.4427L17.7033 27.0531C17.2048 27.4349 16.49 27.3334 16.1175 26.8279L9.61586 18.005L23.2107 16.5243L27.562 22.7377V22.7377Z" fill="url(#paint1_linear_0_201)"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M16.0849 4.87764C16.4883 4.0445 17.6701 4.0305 18.0932 4.85382L20.4401 9.42097C20.4994 9.53655 20.5971 9.62792 20.7163 9.67952L24.6567 11.3843C25.233 11.6336 25.4926 12.3073 25.2327 12.8789L20.6955 22.8555L12.7789 11.705L16.0849 4.87764V4.87764Z" fill="url(#paint2_linear_0_201)"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M16.0849 4.87764C16.4883 4.0445 17.6701 4.0305 18.0932 4.85382L20.4401 9.42097C20.4994 9.53655 20.5971 9.62792 20.7163 9.67952L24.6567 11.3843C25.233 11.6336 25.4926 12.3073 25.2327 12.8789L20.6955 22.8555L12.7789 11.705L16.0849 4.87764V4.87764Z" fill="url(#paint3_linear_0_201)"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M19.7344 11.1713L16.6931 17.2345L8.77569 17.7308C8.47445 17.7497 8.21224 17.5269 8.18223 17.2266L7.7888 13.2907C7.72635 12.666 8.18862 12.1115 8.81442 12.0605L19.7344 11.1713V11.1713Z" fill="url(#paint4_linear_0_201)"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M4.66794 15.9878C4.0491 15.9878 3.54742 16.4897 3.54742 17.1088C3.54742 17.7279 4.0491 18.2298 4.66794 18.2298C4.96882 18.2298 5.242 18.1112 5.4433 17.9181V17.9223L6.29747 17.0678H5.78772C5.76614 16.4677 5.27305 15.9878 4.66794 15.9878Z" fill="url(#paint5_linear_0_201)"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M23.7432 7.31465C24.0528 6.7786 23.8693 6.0931 23.3334 5.78355C22.7975 5.474 22.112 5.65761 21.8024 6.19366C21.653 6.45236 21.6184 6.74588 21.6828 7.01515L21.679 7.01293L21.9914 8.18009L22.2441 7.74253C22.775 8.02692 23.4397 7.84011 23.7432 7.31465Z" fill="url(#paint6_linear_0_201)"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M21.6784 28.0533C21.988 28.5894 22.6734 28.773 23.2094 28.4634C23.7453 28.1539 23.9288 27.4684 23.6191 26.9323C23.4686 26.6716 23.2291 26.4943 22.9611 26.4165L22.9633 26.4153L21.7962 26.1026L22.0513 26.5444C21.5439 26.8636 21.3759 27.5297 21.6784 28.0533Z" fill="url(#paint7_linear_0_201)"/>
<defs>
<linearGradient id="paint0_linear_0_201" x1="19.7344" y1="11.1714" x2="4.0293" y2="11.1714" gradientUnits="userSpaceOnUse">
<stop stop-color="#3EB9FC"/>
<stop offset="1" stop-color="#3AE5F6"/>
</linearGradient>
<linearGradient id="paint1_linear_0_201" x1="27.0039" y1="23.0876" x2="19.7513" y2="11.6611" gradientUnits="userSpaceOnUse">
<stop stop-color="#3CB2FD"/>
<stop offset="1" stop-color="#2170F3"/>
</linearGradient>
<linearGradient id="paint2_linear_0_201" x1="28.3377" y1="9.12939" x2="17.0651" y2="2.85327" gradientUnits="userSpaceOnUse">
<stop stop-color="#50D3AB"/>
<stop offset="1" stop-color="#6DBFFF"/>
</linearGradient>
<linearGradient id="paint3_linear_0_201" x1="18.0504" y1="5.12277" x2="11.8945" y2="16.7948" gradientUnits="userSpaceOnUse">
<stop stop-color="#F280FF"/>
<stop offset="1" stop-color="#A723E4"/>
</linearGradient>
<linearGradient id="paint4_linear_0_201" x1="19.7344" y1="11.1713" x2="7.67511" y2="11.1713" gradientUnits="userSpaceOnUse">
<stop stop-color="#3EB9FC"/>
<stop offset="1" stop-color="#3AE5F6"/>
</linearGradient>
<linearGradient id="paint5_linear_0_201" x1="3.54715" y1="15.9878" x2="3.54715" y2="18.2298" gradientUnits="userSpaceOnUse">
<stop stop-color="#3EB9FC"/>
<stop offset="1" stop-color="#3AE5F6"/>
</linearGradient>
<linearGradient id="paint6_linear_0_201" x1="21.3846" y1="7.34636" x2="23.0292" y2="8.26318" gradientUnits="userSpaceOnUse">
<stop stop-color="#F280FF"/>
<stop offset="1" stop-color="#A723E4"/>
</linearGradient>
<linearGradient id="paint7_linear_0_201" x1="22.611" y1="25.8261" x2="20.9466" y2="26.8049" gradientUnits="userSpaceOnUse">
<stop stop-color="#3CB2FD"/>
<stop offset="1" stop-color="#2170F3"/>
</linearGradient>
</defs>
</svg>
<svg width="64" height="64" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M12.5483 47.8994C10.7002 48.005 9.52586 45.9542 10.5523 44.4138L16.2493 35.8647C16.3933 35.6486 16.4579 35.3892 16.4321 35.1308L15.5776 26.5826C15.4527 25.333 16.3773 24.2241 17.6289 24.1222L39.4688 22.3438L27.6899 47.0337L12.5483 47.8994V47.8994Z" fill="url(#paint0_linear_0_201)"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M55.124 45.4762C56.1859 46.9926 55.0588 49.0703 53.2086 49.007L42.9447 48.6555C42.685 48.6467 42.4303 48.7282 42.224 48.8862L35.4066 54.1071C34.4095 54.8707 32.98 54.6676 32.235 53.6566L19.2317 36.0109L46.4213 33.0494L55.124 45.4762V45.4762Z" fill="url(#paint1_linear_0_201)"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M32.1698 9.75638C32.9767 8.0901 35.3402 8.06209 36.1864 9.70873L40.8801 18.843C40.9989 19.0742 41.1942 19.2569 41.4327 19.3601L49.3134 22.7697C50.466 23.2684 50.9852 24.6157 50.4653 25.7589L41.3911 45.7121L25.5578 23.411L32.1698 9.75638V9.75638Z" fill="url(#paint2_linear_0_201)"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M32.1698 9.75638C32.9767 8.0901 35.3402 8.06209 36.1864 9.70873L40.8801 18.843C40.9989 19.0742 41.1942 19.2569 41.4327 19.3601L49.3134 22.7697C50.466 23.2684 50.9852 24.6157 50.4653 25.7589L41.3911 45.7121L25.5578 23.411L32.1698 9.75638V9.75638Z" fill="url(#paint3_linear_0_201)"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M39.4687 22.3438L33.3862 34.4701L17.5514 35.4628C16.9489 35.5006 16.4245 35.055 16.3645 34.4543L15.5776 26.5826C15.4527 25.333 16.3772 24.2241 17.6288 24.1222L39.4687 22.3438V22.3438Z" fill="url(#paint4_linear_0_201)"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M9.33589 31.9765C8.0982 31.9765 7.09485 32.9802 7.09485 34.2185C7.09485 35.4567 8.0982 36.4604 9.33589 36.4604C9.93765 36.4604 10.484 36.2232 10.8866 35.837V35.8456L12.5949 34.1365H11.5755C11.5323 32.9362 10.5461 31.9765 9.33589 31.9765Z" fill="url(#paint5_linear_0_201)"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M47.4867 14.6305C48.1059 13.5584 47.739 12.1874 46.6671 11.5683C45.5952 10.9492 44.2243 11.3164 43.6051 12.3885C43.3062 12.9059 43.2371 13.4929 43.366 14.0314L43.358 14.0268L43.9828 16.3611L44.4882 15.4861C45.5502 16.0551 46.8796 15.6815 47.4867 14.6305Z" fill="url(#paint6_linear_0_201)"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M43.3568 56.1077C43.976 57.1798 45.347 57.547 46.4188 56.9279C47.4907 56.3088 47.8576 54.9378 47.2384 53.8657C46.9372 53.3442 46.4582 52.9895 45.9222 52.834L45.9265 52.8315L43.5923 52.2062L44.1027 53.0899C43.0879 53.7282 42.752 55.0605 43.3568 56.1077Z" fill="url(#paint7_linear_0_201)"/>
<defs>
<linearGradient id="paint0_linear_0_201" x1="39.4688" y1="22.3438" x2="8.05859" y2="22.3438" gradientUnits="userSpaceOnUse">
<stop stop-color="#3EB9FC"/>
<stop offset="1" stop-color="#3AE5F6"/>
</linearGradient>
<linearGradient id="paint1_linear_0_201" x1="54.0079" y1="46.1761" x2="39.5026" y2="23.3232" gradientUnits="userSpaceOnUse">
<stop stop-color="#3CB2FD"/>
<stop offset="1" stop-color="#2170F3"/>
</linearGradient>
<linearGradient id="paint2_linear_0_201" x1="56.6755" y1="18.2599" x2="34.1303" y2="5.70763" gradientUnits="userSpaceOnUse">
<stop stop-color="#50D3AB"/>
<stop offset="1" stop-color="#6DBFFF"/>
</linearGradient>
<linearGradient id="paint3_linear_0_201" x1="36.1009" y1="10.2466" x2="23.789" y2="33.5907" gradientUnits="userSpaceOnUse">
<stop stop-color="#F280FF"/>
<stop offset="1" stop-color="#A723E4"/>
</linearGradient>
<linearGradient id="paint4_linear_0_201" x1="39.4687" y1="22.3437" x2="15.3502" y2="22.3437" gradientUnits="userSpaceOnUse">
<stop stop-color="#3EB9FC"/>
<stop offset="1" stop-color="#3AE5F6"/>
</linearGradient>
<linearGradient id="paint5_linear_0_201" x1="7.0949" y1="31.9764" x2="7.0949" y2="36.4604" gradientUnits="userSpaceOnUse">
<stop stop-color="#3EB9FC"/>
<stop offset="1" stop-color="#3AE5F6"/>
</linearGradient>
<linearGradient id="paint6_linear_0_201" x1="42.7688" y1="14.6941" x2="46.058" y2="16.5277" gradientUnits="userSpaceOnUse">
<stop stop-color="#F280FF"/>
<stop offset="1" stop-color="#A723E4"/>
</linearGradient>
<linearGradient id="paint7_linear_0_201" x1="45.2215" y1="51.6527" x2="41.8927" y2="53.6102" gradientUnits="userSpaceOnUse">
<stop stop-color="#3CB2FD"/>
<stop offset="1" stop-color="#2170F3"/>
</linearGradient>
</defs>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>
\ No newline at end of file
<template>
<svg width="56px" height="56px" viewBox="0 0 56 56" version="1.1" xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink">
<title>用户数量(UV)</title>
<defs>
<linearGradient x1="0%" y1="17.2335601%" x2="79.0455666%" y2="72.9606973%" id="linearGradient-1">
<stop stop-color="#FFD240" offset="0%"></stop>
<stop stop-color="#FA9841" offset="100%"></stop>
</linearGradient>
<linearGradient x1="50%" y1="19.2345794%" x2="77.0508104%" y2="71.7407152%" id="linearGradient-2">
<stop stop-color="#FF854A" offset="0%"></stop>
<stop stop-color="#FA8E5A" stop-opacity="0" offset="100%"></stop>
</linearGradient>
<filter x="-81.6%" y="-60.0%" width="263.1%" height="220.0%" filterUnits="objectBoundingBox" id="filter-3">
<feGaussianBlur stdDeviation="3.2" in="SourceGraphic"></feGaussianBlur>
</filter>
<linearGradient x1="50%" y1="0%" x2="50%" y2="100%" id="linearGradient-4">
<stop stop-color="#FFFFFF" offset="0%"></stop>
<stop stop-color="#FFFFFF" stop-opacity="0" offset="100%"></stop>
</linearGradient>
<path
d="M23.3194685,22 C25.5882337,22 27.478256,23.7390716 27.6666667,26 L27.8796616,28.5559394 C27.9408146,29.2897754 27.3954973,29.9342413 26.6616612,29.9953943 C26.6248319,29.9984635 26.5878909,30 26.5509339,30 L17.4490661,30 C16.7126864,30 16.1157327,29.4030463 16.1157327,28.6666667 C16.1157327,28.6297097 16.1172693,28.5927687 16.1203384,28.5559394 L16.3333333,26 C16.521744,23.7390716 18.4117663,22 20.6805315,22 L23.3194685,22 Z M22,14 C24.209139,14 26,15.790861 26,18 C26,20.209139 24.209139,22 22,22 C19.790861,22 18,20.209139 18,18 C18,15.790861 19.790861,14 22,14 Z"
id="path-5"></path>
<filter x="-36.8%" y="-27.1%" width="173.6%" height="154.2%" filterUnits="objectBoundingBox" id="filter-6">
<feGaussianBlur stdDeviation="3.5" in="SourceAlpha" result="shadowBlurInner1"></feGaussianBlur>
<feOffset dx="0" dy="1" in="shadowBlurInner1" result="shadowOffsetInner1"></feOffset>
<feComposite in="shadowOffsetInner1" in2="SourceAlpha" operator="arithmetic" k2="-1" k3="1"
result="shadowInnerInner1"></feComposite>
<feColorMatrix values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.801981466 0" type="matrix"
in="shadowInnerInner1"></feColorMatrix>
</filter>
<path
d="M23.3194685,22 C23.5835058,22 23.8424134,22.0235542 24.094106,22.0687441 L24.3570597,22.3333333 L23.3330144,23.3573107 L23.3333333,26.6666667 C23.3333333,27.4030463 22.7363797,28 22,28 C21.2636203,28 20.6666667,27.4030463 20.6666667,26.6666667 L20.6660144,23.3563107 L19.6430144,22.3333333 L19.9068928,22.0685649 C20.1582716,22.023492 20.4168434,22 20.6805315,22 L23.3194685,22 Z"
id="path-7"></path>
<filter x="-74.2%" y="-41.7%" width="248.5%" height="216.7%" filterUnits="objectBoundingBox" id="filter-8">
<feOffset dx="0" dy="1" in="SourceAlpha" result="shadowOffsetOuter1"></feOffset>
<feGaussianBlur stdDeviation="1" in="shadowOffsetOuter1" result="shadowBlurOuter1"></feGaussianBlur>
<feColorMatrix values="0 0 0 0 0.980392157 0 0 0 0 0.596078431 0 0 0 0 0.254901961 0 0 0 0.19630955 0"
type="matrix" in="shadowBlurOuter1"></feColorMatrix>
</filter>
</defs>
<g id="页面-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="小卡片" transform="translate(-75.000000, -788.000000)">
<g id="编组-5备份-4" transform="translate(51.000000, 764.000000)">
<g id="编组" transform="translate(0.000000, 0.000000)">
<g id="用户数量(UV)" transform="translate(24.000000, 24.000000)">
<path
d="M28,56 C43.463973,56 56,43.463973 56,28 C56,12.536027 43.463973,0 28,0 C12.536027,0 0,12.536027 0,28 C0,43.463973 12.536027,56 28,56 Z"
id="Ellipse-1" fill="#FFF3DC"></path>
<g id="UV" transform="translate(12.000000, 12.000000)">
<rect id="矩形" opacity="0.200000003" x="0" y="0" width="32" height="32"></rect>
<path
d="M30,12 L30,25.7333333 C30,26.6169889 29.2836556,27.3333333 28.4,27.3333333 L3.6,27.3333333 C2.7163444,27.3333333 2,26.6169889 2,25.7333333 L2,12 L30,12 Z M10,18 L5.33333333,18 C4.9651435,18 4.66666667,18.2984768 4.66666667,18.6666667 C4.66666667,19.0348565 4.9651435,19.3333333 5.33333333,19.3333333 L5.33333333,19.3333333 L10,19.3333333 C10.3681898,19.3333333 10.6666667,19.0348565 10.6666667,18.6666667 C10.6666667,18.2984768 10.3681898,18 10,18 L10,18 Z M10,14.6666667 L5.33333333,14.6666667 C4.9651435,14.6666667 4.66666667,14.9651435 4.66666667,15.3333333 C4.66666667,15.7015232 4.9651435,16 5.33333333,16 L5.33333333,16 L10,16 C10.3681898,16 10.6666667,15.7015232 10.6666667,15.3333333 C10.6666667,14.9651435 10.3681898,14.6666667 10,14.6666667 L10,14.6666667 Z M28.4,4.66666667 C29.2836556,4.66666667 30,5.38301107 30,6.26666667 L30,10.6666667 L2,10.6666667 L2,6.26666667 C2,5.38301107 2.7163444,4.66666667 3.6,4.66666667 L28.4,4.66666667 Z M5,6.66666667 C4.44771525,6.66666667 4,7.11438192 4,7.66666667 C4,8.21895142 4.44771525,8.66666667 5,8.66666667 C5.55228475,8.66666667 6,8.21895142 6,7.66666667 C6,7.11438192 5.55228475,6.66666667 5,6.66666667 Z M9,6.66666667 C8.44771525,6.66666667 8,7.11438192 8,7.66666667 C8,8.21895142 8.44771525,8.66666667 9,8.66666667 C9.55228475,8.66666667 10,8.21895142 10,7.66666667 C10,7.11438192 9.55228475,6.66666667 9,6.66666667 Z M13,6.66666667 C12.4477153,6.66666667 12,7.11438192 12,7.66666667 C12,8.21895142 12.4477153,8.66666667 13,8.66666667 C13.5522847,8.66666667 14,8.21895142 14,7.66666667 C14,7.11438192 13.5522847,6.66666667 13,6.66666667 Z"
id="形状结合" fill="url(#linearGradient-1)" fill-rule="nonzero"></path>
<path
d="M20.6528018,20.6666667 C22.9215671,20.6666667 24.8115893,22.4057383 25,24.6666667 L25.2129949,27.222606 C25.2741479,27.9564421 24.7288306,28.600908 23.9949946,28.662061 C23.9581653,28.6651301 23.9212242,28.6666667 23.8842673,28.6666667 L14.7823994,28.6666667 C14.0460197,28.6666667 13.4490661,28.069713 13.4490661,27.3333333 C13.4490661,27.2963764 13.4506026,27.2594353 13.4536717,27.222606 L13.6666667,24.6666667 C13.8550774,22.4057383 15.7450996,20.6666667 18.0138649,20.6666667 L20.6528018,20.6666667 Z M19.3333333,12.6666667 C21.5424723,12.6666667 23.3333333,14.4575277 23.3333333,16.6666667 C23.3333333,18.8758057 21.5424723,20.6666667 19.3333333,20.6666667 C17.1241943,20.6666667 15.3333333,18.8758057 15.3333333,16.6666667 C15.3333333,14.4575277 17.1241943,12.6666667 19.3333333,12.6666667 Z"
id="形状结合" fill="url(#linearGradient-2)" filter="url(#filter-3)"></path>
<g id="形状结合" fill-rule="nonzero">
<use fill-opacity="0.3" fill="#FFBE80" xlink:href="#path-5"></use>
<use fill="black" fill-opacity="1" filter="url(#filter-6)" xlink:href="#path-5"></use>
<use stroke="url(#linearGradient-4)" stroke-width="0.666666667" xlink:href="#path-5"></use>
</g>
<g id="形状结合">
<use fill="black" fill-opacity="1" filter="url(#filter-8)" xlink:href="#path-7"></use>
<use fill="#FFFFFF" fill-rule="evenodd" xlink:href="#path-7"></use>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</svg>
</template>
<script>
</script>
<style scoped></style>
\ No newline at end of file
@import 'devui-theme/styles-var/devui-var.scss';
.content-card {
margin: 8px;
overflow: hidden;
background-color: $devui-base-bg;
border-radius: $devui-border-radius-card;
box-shadow: $devui-shadow-length-base $devui-light-shadow;
height: 400px;
transition: transform 0.3s, box-shadow 0.3s;
&:hover {
transform: translateY(-5px);
box-shadow: $devui-shadow-length-base $devui-shadow;
}
}
.content-card-sm {
margin: 8px;
overflow: hidden;
background-color: $devui-base-bg;
border-radius: $devui-border-radius-card;
box-shadow: $devui-shadow-length-base $devui-light-shadow;
height: 192px;
transition: transform 0.3s, box-shadow 0.3s;
&:hover {
transform: translateY(-5px);
box-shadow: $devui-shadow-length-base $devui-shadow;
}
}
.dark-card {
position: relative;
background-image: linear-gradient(-50deg, #242222, #555454);
border-radius: 16px;
box-shadow: $devui-shadow-length-base $devui-light-shadow;
padding: 20px;
overflow: hidden !important;
}
.grade-number {
display: inline-block;
font-size: 26px;
vertical-align: text-top;
background-image: linear-gradient(to right, $devui-yellow-20, $devui-yellow-80);
background-clip: text;
color: transparent;
font-weight: bold;
margin-right: 20px;
}
.radar-descript {
position: absolute;
top: 24%;
right: 16px;
width: 30%;
color: #ced1db;
padding-right: 16px;
font-size: 14px;
line-height: 22px;
&-title {
color: #ffffff;
line-height: 22px;
margin: 0 8px;
font-weight: bold;
}
}
.indicator-card {
padding: 24px;
background: $devui-base-bg;
display: flex;
border-radius: 16px;
width: 100%;
height: 100%;
.indicator-content {
display: flex;
flex-direction: column;
text-align: left;
flex: 1;
font-size: 12px;
.indicator-content-item {
margin-bottom: 4px;
padding: 4px 0;
}
}
.indicator-charts {
display: flex;
flex-direction: column;
justify-content: space-between;
flex: 1;
}
}
.trend-number {
line-height: 1em;
font-weight: 600;
color: $devui-text;
font-size: 24px;
}
.chart-card {
box-shadow: $devui-shadow-length-base $devui-light-shadow;
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
padding: 20px;
position: relative;
background-color: $devui-base-bg;
.card-title {
font-size: 14px;
line-height: 22px;
font-weight: 700;
}
.card-subtitle {
font-size: 12px;
line-height: 18px;
margin-top: 4px;
color: $devui-aide-text-stress;
}
.card-content {
flex: 1;
margin-top: 12px;
}
}
\ No newline at end of file
<template>
<d-row class="docs-devui-row">
<d-col :span="8">
<div class="content-card dark-card">
<div style="display: flex; flex-direction: column; align-items: flex-start;">
<p style="color: #ced1db; font-weight: bold; font-size: 16px">性能评分</p>
<div class="grade-number">{{ 58 }}</div>
</div>
<d-chart id="chart2" :option="useRadarOptions" style="width: 300px; height: 350px;"></d-chart>
<div class="radar-descript">
<span style="font-size: 40px"></span><br />
<span class="radar-descript-title">成功率</span>
<span>低于电商场景的通用指标SLA水平,建议从失败请求的业务日志中分析</span><br />
<span style="margin-top: 8px" class="radar-descript-title">异常数</span>
<span>建议分析微服务的调用关系,是否存在性能瓶颈</span><br />
<span style="font-size: 40px; float: right; margin-top: 20px"></span>
</div>
</div>
</d-col>
<d-col :span="16">
<d-row>
<d-col :span="6">
<div class="content-card-sm">
<div class="indicator-card">
<div class="indicator-content">
<div>
<AvatarSvg />
</div>
<div class="indicator-content-item">
用户数量
</div>
<div class="indicator-content-item trend-number">
95%
</div>
</div>
<div class="indicator-charts">
<p style="color: #babbc0">this week <span style="color: #66cb9f">+263</span></p>
<d-chart id="chart2" :option="useSimpleBarOptions" style="width: 100%; height: 100px;"></d-chart>
</div>
</div>
</div>
</d-col>
<d-col :span="9">
<div class="content-card-sm">
<div class="indicator-card">
<div class="indicator-content">
<div class="indicator-content-item">
<AvatarSvg />
</div>
<div class="indicator-content-item">
增长趋势
</div>
<div class="indicator-content-item trend-number">
95%
</div>
</div>
<div class="indicator-charts" style="flex: 2;">
<p style="color: #babbc0">this week <span style="color: #66cb9f">+263</span></p>
<d-chart id="chart2" :option="usesimpleLineOptions2" style="width: 100%; height: 100px;"></d-chart>
</div>
</div>
</div>
</d-col>
<d-col :span="9">
<div class="content-card-sm">
<div class="indicator-card">
<div class="indicator-content">
<div class="indicator-content-item">
<AvatarSvg />
</div>
<div class="indicator-content-item">
季度变化
</div>
<div class="indicator-content-item trend-number">
95%
</div>
</div>
<div class="indicator-charts" style="flex: 2;">
<p style="color: #babbc0">this week <span style="color: #66cb9f">+263</span></p>
<d-chart id="chart2" :option="usesimpleBarOptions2" style="width: 100%; height: 100px;"></d-chart>
</div>
</div>
</div>
</d-col>
</d-row>
<d-row>
<d-col :span="6">
<div class="content-card-sm">
<div class="indicator-card">
<div class="indicator-content">
<div class="indicator-content-item">
<AvatarSvg />
</div>
<div class="indicator-content-item">
季度变化
</div>
<div class="indicator-content-item trend-number">
95%
</div>
</div>
<div class="indicator-charts" style="flex: 2;">
<p style="color: #babbc0">this week <span style="color: #66cb9f">+263</span></p>
<d-chart id="chart2" :option="usesimpleBarOptions2" style="width: 100%; height: 100px;"></d-chart>
</div>
</div>
</div>
</d-col>
<d-col :span="9">
<div class="content-card-sm">
<div class="indicator-card">
<div class="indicator-content">
<div class="indicator-content-item">
<AvatarSvg />
</div>
<div class="indicator-content-item">
全年趋势
</div>
<div class="indicator-content-item trend-number">
95%
</div>
</div>
<div class="indicator-charts" style="flex: 2;">
<p style="color: #babbc0">this week <span style="color: #66cb9f">+263</span></p>
<d-chart id="chart2" :option="usesimpleLineOptions2" style="width: 100%; height: 100px;"></d-chart>
</div>
</div>
</div>
</d-col>
<d-col :span="9">
<div class="content-card-sm">
<div class="indicator-card">
<div class="indicator-content">
<div class="indicator-content-item">
<AvatarSvg />
</div>
<div class="indicator-content-item">
年度变化
</div>
<div class="indicator-content-item trend-number">
70%
</div>
</div>
<div class="indicator-charts" style="flex: 2;">
<p style="color: #babbc0">this week <span style="color: #66cb9f">+263</span></p>
<d-chart id="chart2" :option="usesimpleBarOptions2" style="width: 100%; height: 100px;"></d-chart>
</div>
</div>
</div>
</d-col>
</d-row>
</d-col>
</d-row>
<d-row class="docs-devui-row">
<d-col :span="8">
<div class="content-card">
<div class="chart-card">
<div class="card-title">告警统计</div>
<div class="card-subtitle">华南华北大区外数据正在建设中</div>
<div class="card-content">
<d-chart id="chart2" :option="useBarOption" style="width: 100%; height: 300px;"></d-chart>
</div>
</div>
</div>
</d-col>
<d-col :span="8">
<div class="content-card">
<div class="chart-card">
<div class="card-title">地域数据</div>
<div class="card-subtitle">各地域响应数据统计</div>
<div class="card-content">
<d-chart id="chart2" :option="usePieOption" style="width: 100%; height: 300px;"></d-chart>
</div>
</div>
</div>
</d-col>
<d-col :span="8">
<div class="content-card">
<div class="chart-card">
<div class="card-title">CPU负载</div>
<div class="card-subtitle">各地域响应数据统计</div>
<div class="card-content">
<d-chart id="chart2" :option="usegGugeOption" style="width: 100%; height: 300px;"></d-chart>
</div>
</div>
</div>
</d-col>
</d-row>
<d-row class="docs-devui-row">
<d-col :span="8">
<div class="content-card">
<div class="chart-card">
<div class="card-title">问题统计</div>
<div class="card-subtitle">各类型问题统计</div>
<div class="card-content">
<d-chart id="chart2" :option="useHorizontalOption" style="width: 100%; height: 300px;"></d-chart>
</div>
</div>
</div>
</d-col>
<d-col :span="8">
<div class="content-card">
<div class="chart-card">
<div class="card-title">本周变化</div>
<div class="card-subtitle">各类型问题统计</div>
<div class="card-content">
<d-chart id="chart3" :option="useLineOption" style="width: 100%; height: 300px;"></d-chart>
</div>
</div>
</div>
</d-col>
<d-col :span="8">
<div class="content-card">
<div class="chart-card">
<div class="card-title">未来趋势</div>
<div class="card-subtitle">各类型问题统计</div>
<div class="card-content">
<d-chart id="chart3" :option="usetrendLineOption" style="width: 100%; height: 300px;"></d-chart>
</div>
</div>
</div>
</d-col>
</d-row>
</template>
<script>
import AvatarSvg from './AvatarSvg.vue';
import { defineComponent, ref, reactive } from 'vue'
import { barOption, gaugeOption, lineOption, horizontalOption, pieOption, radarOptions, simpleBarOptions, trendLineOption, simpleLineOptions2, simpleBarOptions2 } from './mockData.ts';
export default defineComponent({
components: {
AvatarSvg,
},
setup() {
const useRadarOptions = reactive({...radarOptions})
const useSimpleBarOptions = reactive({...simpleBarOptions})
const useBarOption = reactive({...barOption})
const usePieOption = reactive({...pieOption})
const usegGugeOption = reactive({...gaugeOption})
const useHorizontalOption = reactive({...horizontalOption})
const useLineOption = reactive({...lineOption})
const usetrendLineOption = reactive({...trendLineOption})
const usesimpleLineOptions2 = reactive({...simpleLineOptions2})
const usesimpleBarOptions2 = reactive({...simpleBarOptions2})
return {
useRadarOptions,useSimpleBarOptions,useBarOption,usePieOption,usegGugeOption,useHorizontalOption, useLineOption, usetrendLineOption,usesimpleLineOptions2,usesimpleBarOptions2
}
}
})
</script>
<style lang="scss" scoped>
@import './Content.scss';
</style>
\ No newline at end of file
<template>
<div class="da-footer">
<div class="da-footer-intro">
<span class="da-production-name"><a href="https://devui.design/admin-page/home" target="_blank">DevUI Admin</a></span>
<a
href="https://github.com/DevCloudFE/ng-devui-admin"
rel="noopener noreferrer"
target="_blank"
aria-label="Star DevCloudFE/ng-devui-admin on GitHub"
><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="20px" height="20px">
<path
d="M10.9,2.1c-4.6,0.5-8.3,4.2-8.8,8.7c-0.6,5,2.5,9.3,6.9,10.7v-2.3c0,0-0.4,0.1-0.9,0.1c-1.4,0-2-1.2-2.1-1.9 c-0.1-0.4-0.3-0.7-0.6-1C5.1,16.3,5,16.3,5,16.2C5,16,5.3,16,5.4,16c0.6,0,1.1,0.7,1.3,1c0.5,0.8,1.1,1,1.4,1c0.4,0,0.7-0.1,0.9-0.2 c0.1-0.7,0.4-1.4,1-1.8c-2.3-0.5-4-1.8-4-4c0-1.1,0.5-2.2,1.2-3C7.1,8.8,7,8.3,7,7.6C7,7.2,7,6.6,7.3,6c0,0,1.4,0,2.8,1.3 C10.6,7.1,11.3,7,12,7s1.4,0.1,2,0.3C15.3,6,16.8,6,16.8,6C17,6.6,17,7.2,17,7.6c0,0.8-0.1,1.2-0.2,1.4c0.7,0.8,1.2,1.8,1.2,3 c0,2.2-1.7,3.5-4,4c0.6,0.5,1,1.4,1,2.3v3.3c4.1-1.3,7-5.1,7-9.5C22,6.1,16.9,1.4,10.9,2.1z"
/>
</svg>
</a>
<span class="da-organization">
<a href="https://devui.design/home" rel="noopener noreferrer" target="_blank" aria-label="Learn more about ng-devui">
DevUI Design
</a>
</span>
</div>
<div class="da-presented">DevUI Design 出品</div>
</div>
</template>
<style scoped lang="scss">
@import 'devui-theme/styles-var/devui-var.scss';
.da-footer {
font-size: 12px;
margin-top: 20px;
.da-footer-intro {
display: flex;
align-items: center;
justify-content: center;
.da-production-name {
a {
color: $devui-text-weak;
cursor: pointer;
transition: color 0.2s ease-in-out;
&:hover {
color: $devui-text;
}
}
}
.da-organization {
a {
color: $devui-text-weak;
cursor: pointer;
transition: color 0.2s ease-in-out;
&:hover {
color: $devui-text;
}
}
}
.da-homepage-active {
outline: none;
}
a {
display: flex;
align-items: center;
svg {
fill: $devui-text-weak;
margin: 0 20px;
transition: fill 0.2s ease-in-out;
&:hover {
fill: $devui-text;
}
}
}
}
.da-presented {
text-align: center;
color: $devui-text-weak;
padding-top: 12px;
}
}
</style>
\ No newline at end of file
<template>
<section class="header-container header">
<div class="left-nav">
</div>
<div class="right-nav">
<d-search id="search" v-model="search" icon-position="left" size="sm" :no-border="true"></d-search>
<d-button class="nav-drop-btn" icon="icon-project-nav" variant="text"></d-button>
<div class="theme">
<img :class="['opt', {'active': props.drawerOpen}]" @click="openDrawer()" src="../assets/logo.svg">
<div class="opt">
<d-icon name="dark" color="var(--devui-text)" @click="changeTheme()">
<template #suffix>
</template>
</d-icon>
</div>
<div style="display: flex; align-items: center; gap: 20px; margin-left: 20px; cursor: pointer; font-size: 14px;">
<i class="icon-helping"></i>
<d-badge :count="5" >
<i class="icon icon-notice"></i>
</d-badge>
<div style="display: flex; align-items: center; gap: 8px;">
<d-avatar :name="'admin'" :width="22" :height="22"></d-avatar>
<span>Admin</span>
</div>
</div>
</div>
</div>
</section>
</template>
<script setup lang="ts">
import { HtmlHTMLAttributes, onMounted, ref, defineProps } from 'vue';
import { themeServiceInstance } from '../main';
import { infinityTheme, galaxyTheme } from 'devui-theme';
const props = defineProps(['drawerOpen']);
const theme = ref('dark');
const emit = defineEmits(['openDrawer']);
const openDrawer = () => {
emit('openDrawer');
}
const search = ref('');
const changeTheme = () => {
const currentTheme = localStorage.getItem('theme');
if (currentTheme === 'infinity-theme' || !currentTheme) {
theme.value = 'light';
localStorage.setItem('theme', 'galaxy-theme');
themeServiceInstance?.applyTheme(galaxyTheme);
} else {
theme.value = 'dark';
localStorage.setItem('theme', 'infinity-theme');
themeServiceInstance?.applyTheme(infinityTheme);
}
}
onMounted(() => {
let currentTheme = localStorage.getItem('theme');
theme.value = (!currentTheme || currentTheme === 'infinity-theme') ? 'dark' : 'light ';
})
</script>
<style lang="scss" scoped>
@import 'devui-theme/styles-var/devui-var.scss';
.header {
z-index: 20;
display: flex;
justify-content: space-between;
align-items: center;
top: 0;
width: calc(100% - 240px);
height: 48px;
background-color: $devui-base-bg;
transition: 0.5s;
img {
height: 32px;
margin: 8px 20px;
cursor: pointer;
}
.left-nav {
display: flex;
align-items: center;
button {
display: none;
}
}
.right-nav {
display: flex;
align-items: center;
.nav-drop-btn {
display: none;
}
.nav-list {
margin-right: 24px;
& > a:not(:first-child) {
margin-left: 12px;
}
& > a {
text-decoration: none;
}
.nav-active {
color: $devui-link-active;
}
}
}
.theme {
display: flex;
margin-right: 20px;
align-items: center;
.opt {
padding: 5px;
cursor: pointer;
transition: 0.5s;
border-radius: 25%;
}
}
}
.active {
background: $devui-list-item-hover-bg;
box-shadow: var(--devui-shadow-length-base, 0 2px 6px 0) var(--devui-light-shadow, rgba(37, 43, 58, .12));
border-radius: $devui-border-radius-card;
}
</style>
<style>
.devui-drawer {
top: 49px;
}
</style>
\ No newline at end of file
<template>
<div class="mc-bubble" :class="bubbleClasses">
<div v-if="avatarConfig" class="mc-bubble-avatar" :class="{ 'empty-avatar': isEmptyAvatar }">
<d-avatar
v-bind="
isEmptyAvatar
? {
width: avatarConfig?.width || DEFAULT_AVATAR_WIDTH,
height: avatarConfig?.height || DEFAULT_AVATAR_HEIGHT,
}
: avatarConfig
"
></d-avatar>
<span class="mc-bubble-avatar-name">{{ avatarConfig?.displayName }}</span>
</div>
<div class="mc-bubble-content-container" :class="{ 'with-avatar': avatarConfig }">
<slot v-if="!loading" name="top"></slot>
<div v-if="loading" class="loading-container">
<slot v-if="slots.loadingTpl" name="loadingTpl"></slot>
<BubbleLoading v-else></BubbleLoading>
</div>
<div
v-if="(slots.default || content) && !loading"
class="mc-bubble-content"
:class="[variant]"
>
<slot v-if="slots.default"></slot>
<template v-else>
{{ content }}
</template>
</div>
<slot v-if="!loading" name="bottom"></slot>
</div>
</div>
</template>
<script setup lang="ts">
import { computed, onMounted, toRefs, useSlots } from 'vue';
import { props } from './bubble-types';
import { DEFAULT_AVATAR_WIDTH, DEFAULT_AVATAR_HEIGHT } from './bubble-constants';
import BubbleLoading from './BubbleLoading.vue';
/**
* avatar - 头像
* top - 气泡顶部区域
* loadingTpl - 自定义 Loading 样式
* default - 内容区
* bottom - 气泡底部区域
*/
const slots = useSlots();
const bubbleProps = defineProps(props);
const bubbleClasses = computed(() => {
return [
`mc-bubble-avatar-${bubbleProps.avatarPosition}`,
bubbleProps.loading ? 'mc-bubble-loading' : '',
];
});
const isEmptyAvatar = computed(() => {
if (bubbleProps.avatarConfig) {
return Object.keys(bubbleProps.avatarConfig).length < 1;
} else {
return true;
}
});
</script>
<style scoped lang="scss">
@import 'devui-theme/styles-var/devui-var.scss';
.mc-bubble {
display: flex;
flex-direction: column;
gap: 8px;
.mc-bubble-content {
word-wrap: break-word;
border-radius: 12px;
&.filled {
background-color: $devui-global-bg;
padding: 12px 16px;
}
&.bordered {
border: 1px solid $devui-dividing-line;
padding: 12px 16px;
}
}
.mc-bubble-avatar {
flex-shrink: 0;
display: flex;
gap: 4px;
.mc-bubble-avatar-name {
font-size: 14px;
}
&.empty-avatar {
visibility: hidden;
}
}
.mc-bubble-content-container {
max-width: 100%;
}
&.mc-bubble-loading.mc-bubble-avatar-side-left {
align-items: center;
}
&.mc-bubble-avatar-side-left {
flex-direction: row;
}
&.mc-bubble-avatar-side-right {
flex-direction: row-reverse;
justify-content: end;
}
&.mc-bubble-avatar-side-left,
&.mc-bubble-avatar-side-right {
.mc-bubble-content-container {
max-width: calc(100% - 40px);
}
.mc-bubble-content-container.with-avatar {
max-width: calc(100% - 72px);
}
}
&.mc-bubble-avatar-top-right {
.mc-bubble-avatar,
.mc-bubble-content-container {
display: flex;
justify-content: end;
}
}
&.mc-bubble-avatar-top-right,
&.mc-bubble-avatar-top-left {
.mc-bubble-avatar {
align-items: center;
}
}
}
</style>
<template>
<div class="mc-bubble-loading">
<div class="loading-dot dot-start"></div>
<div class="loading-dot dot-middle"></div>
<div class="loading-dot dot-end"></div>
</div>
</template>
<script setup lang="ts"></script>
<style scoped lang="scss">
.mc-bubble-loading {
display: flex;
align-items: center;
gap: 8px;
.loading-dot {
width: 8px;
height: 8px;
border-radius: 5px;
background-color: #9880ff;
&.dot-start {
animation: dotFlashing 1s infinite linear alternate;
animation-delay: 0s;
}
&.dot-middle {
animation: dotFlashing 1s infinite linear alternate;
animation-delay: 0.5s;
}
&.dot-end {
animation: dotFlashing 1s infinite linear alternate;
animation-delay: 1s;
}
}
}
@keyframes dotFlashing {
0% {
background-color: #9880ff;
}
100% {
background-color: #ebe6ff;
}
}
</style>
export const DEFAULT_AVATAR_WIDTH = 40;
export const DEFAULT_AVATAR_HEIGHT = 40;
import type { PropType, ExtractPropTypes } from 'vue';
export type BubbleVariant = 'filled' | 'none' | 'bordered';
export type AvatarPosition = 'side-left' | 'side-right' | 'top-left' | 'top-right';
export interface BubbleAvatar {
name?: string;
gender?: string;
width?: number;
height?: number;
isRound?: boolean;
imgSrc?: string;
displayName?: string;
}
export const props = {
content: {
type: String,
default: '',
},
loading: {
type: Boolean,
default: false,
},
avatarPosition: {
type: String as PropType<AvatarPosition>,
default: 'top-left',
},
variant: {
type: String as PropType<BubbleVariant>,
default: 'filled',
},
avatarConfig: {
type: Object as PropType<BubbleAvatar>,
},
};
<template>
<div class="demo-test">
<McHeader :logoImg="'/src/assets/logo.svg'" :title="'MateChat'">
<template #operationArea>
<div class="operations">
<McHistory></McHistory>
<i class="icon-close" @click="closeDrawer()"></i>
</div>
</template>
</McHeader>
<div v-if="startChat" ref="conversationRef" class="conversation-area">
<template v-for="(msg, idx) in messages" :key="idx">
<McBubble
v-if="msg.from === 'user'"
:content="msg.content"
:avatarPosition="msg.avatarPosition"
:avatarConfig="msg.avatarConfig"
></McBubble>
<McBubble v-else :loading="msg.loading ?? false" :avatarPosition="msg.avatarPosition" :avatarConfig="msg.avatarConfig">
<!-- <RenderMarkdown :content="msg.content"></RenderMarkdown> -->
<span v-if="msg.type === 'common'">{{ msg.content }}</span>
<div :id="'demoChart'+idx"
draggable="true"
v-if="msg.type === 'card'"
@dragstart="ondragstart($event)"
@dragover.prevent="ondragover($event)"
@dragleave.prevent="ondragleave($event)"
@drop.prevent="ondrop($event)"
:class="[{ 'content-card': msg.content }]"
>
<DemoCard :chartStr="msg.content" :containerId="'demoChart'+idx"></DemoCard>
</div>
<template #bottom>
<div class="bubble-bottom-operations">
<i class="icon-copy-new"></i>
<i class="icon-like"></i>
<i class="icon-dislike"></i>
<i class="icon-refresh" v-if="chartStr || oldChartStr" @click="changeType(msg, idx)"></i>
</div>
</template>
</McBubble>
</template>
<!-- <teleport to="body" v-if="isDragging">
<div
style="opacity: 1 !important"
class="floating-card"
:style="{
position: 'fixed',
pointerEvents: 'none',
zIndex: 99999,
left: dragPosition.x + 'px',
top: dragPosition.y + 'px',
transform: 'translate(-50%, -50%)',
opacity: '1',
width: '400px',
background: 'var(--devui-base-bg)',
padding: '16px'
}"
>
<DemoCard :chartStr="chartStr"></DemoCard>
</div>
</teleport> -->
</div>
<div v-else class="welcome-page">
<McIntroduction
class="intro"
logo-img="/src/assets/logo2x.svg"
title="MateChat"
sub-title="Hi,欢迎使用 MateChat"
:description="[
'MateChat 可以辅助研发人员编码、查询知识和相关作业信息、编写文档等。',
'作为AI模型,MateChat 提供的答案可能不总是确定或准确的,但您的反馈可以帮助 MateChat 做的更好。',
]"
></McIntroduction>
<McPrompt :list="introPrompt.list" :direction="'horizontal'" class="intro-prompt" @onItemClick="onItemClick($event)"></McPrompt>
<div class="guess-question">
<div class="guess-title">
<div>猜你想问</div>
<div>
<i class="icon-recover"></i>
<span>换一批</span>
</div>
</div>
<div class="guess-content">
<span v-for="(item, index) in guessQuestions" :key="index">{{ item.label }}</span>
</div>
</div>
</div>
<div class="new-convo-button">
<McPrompt
v-if="startChat"
class="simple-prompt"
style="flex: 1"
:list="simplePrompt"
:direction="'horizontal'"
@onItemClick="onItemClick($event)"
></McPrompt>
<div v-else class="agent-knowledge">
<div class="agent-wrapper">
<img src="/src/assets/logo.svg" />
<span>MateChat</span>
<i class="icon-infrastructure"></i>
<i class="icon-chevron-down-2"></i>
</div>
<span class="agent-knowledge-dividing-line"></span>
<div class="knowledge-wrapper">
<i class="icon-operation-log"></i>
<span>添加知识</span>
</div>
</div>
<d-button icon="add" shape="circle" title="新建对话" size="sm" @click="onNewConvo" />
</div>
<div style="padding: 0 12px 12px 12px">
<McInput :value="inputValue" :maxLength="2000" @change="onInputChange" @submit="onSubmit">
<template #extra>
<div class="input-foot-wrapper">
<div class="input-foot-left">
<span v-for="(item, index) in inputFootIcons" :key="index">
<i :class="item.icon"></i>
{{ item.text }}
</span>
<span class="input-foot-dividing-line"></span>
<span class="input-foot-maxlength">{{ inputValue.length }}/2000</span>
</div>
<div class="input-foot-right">
<d-button icon="op-clearup" shape="round" :disabled="!inputValue" @click="inputValue = ''">清空输入</d-button>
<d-button icon="theme-color" shape="round" :disabled="!inputValue">优化输入</d-button>
</div>
</div>
</template>
</McInput>
</div>
</div>
</template>
<script>
import { ref, defineComponent, nextTick, defineEmits, onMounted, onUnmounted, watch, onActivated, onDeactivated } from 'vue';
import { introPrompt, simplePrompt, mockAnswer, guessQuestions } from './mock.constant';
import { chartComponentStr, lineStr, rader } from './DemoCard/MockCardData';
import DemoCard from './DemoCard/DemoCard.vue';
import draggable from 'vuedraggable';
import OpenAI from 'openai';
export default defineComponent({
components: {
DemoCard
},
emits: ['closeDrawer', 'chartStrChange'],
setup(props, { emit }) {
// 组件被激活(显示)时恢复状态
onActivated(() => {
console.log('恢复组件状态', window.savedState);
if (window.savedState) {
messages.value = window.savedState.messages;
inputValue.value = window.savedState.inputValue;
startChat.value = window.savedState.startChat;
chartStr.value = window.savedState.chartStr;
// 恢复滚动位置
nextTick(() => {
if (conversationRef.value) {
conversationRef.value.scrollTop = window.savedState.scrollPosition || 0;
}
});
}
});
// 组件被停用(隐藏)时保存状态
onDeactivated(() => {
console.log('保存组件状态');
window.savedState = {
messages: [...messages.value], // 深拷贝数组
inputValue: inputValue.value,
startChat: startChat.value,
chartStr: chartStr.value,
scrollPosition: conversationRef.value?.scrollTop || 0
};
});
const inputValue = ref('');
const startChat = ref(false);
const conversationRef = ref();
const aiModelAvatar = {
imgSrc: '../logo.svg',
width: 32,
height: 32,
};
const customerAvatar = {
name: 'Me',
width: 32,
height: 32,
};
const inputFootIcons = [
{ icon: 'icon-at', text: '智能体' },
{ icon: 'icon-standard', text: '词库' },
{ icon: 'icon-add', text: '附件' },
];
const messages = ref([]);
const chartStr = ref('');
const oldChartStr = ref('');
const onInputChange = (e) => {
inputValue.value = e;
};
const onSubmit = (e, answer = undefined, type = 'common') => {
fetchData(e);
chartStr.value = '';
if (e === '饼图') {
type = 'card'
setTimeout(() => {
chartStr.value = chartComponentStr;
}, 2000);
answer = chartComponentStr;
} else if (e === '折线图') {
type = 'card'
setTimeout(() => {
chartStr.value = lineStr;
}, 2000);
answer = lineStr;
} else if (e === '雷达图') {
type = 'card'
setTimeout(() => {
chartStr.value = rader;
}, 2000);
answer = rader;
}
inputValue.value = '';
if (!messages.value.length) {
startChat.value = true;
}
messages.value.push({
from: 'user',
content: e,
type,
avatarPosition: 'side-right',
avatarConfig: { ...customerAvatar },
});
nextTick(() => {
conversationRef.value?.scrollTo({
top: conversationRef.value.scrollHeight,
behavior: 'smooth',
});
});
getAIAnswer(answer ?? e, type);
};
const getAIAnswer = (content, type) => {
messages.value.push({
from: 'ai-model',
content,
type,
avatarPosition: 'side-left',
avatarConfig: { ...aiModelAvatar },
loading: true,
});
setTimeout(() => {
messages.value.at(-1).loading = false;
nextTick(() => {
conversationRef.value?.scrollTo({
top: conversationRef.value.scrollHeight,
behavior: 'smooth',
});
});
}, 1000);
};
const onNewConvo = () => {
startChat.value = false;
messages.value = [];
};
const onItemClick = (item) => {
if (mockAnswer[item.value]) {
// 使用 mock 数据
onSubmit(item.label, mockAnswer[item.value]);
}
};
const closeDrawer = () => {
emit('closeDrawer')
};
// 拖动事件
const isDragging = ref(false);
const handleDragOver = (e) => {
e.preventDefault();
if (isDragging.value) {
e.dataTransfer.dropEffect = 'move';
}
};
const handleDrop = (e) => {
e.preventDefault();
isDragging.value = false;
};
// 添加全局事件监听
onMounted(() => {
document.addEventListener('dragover', handleDragOver);
document.addEventListener('drop', handleDrop);
});
// 清理事件监听
onUnmounted(() => {
document.removeEventListener('dragover', handleDragOver);
document.removeEventListener('drop', handleDrop);
});
const ondragstart = (e) => {
e.target.style.backgroundColor = 'var(--devui-float-block-shadow)';
emit('closeDrawer');
emit('chartStrChange', chartStr.value);
};
// 组件级别的事件处理可以保留
const ondragover = (e) => {
e.preventDefault();
if (isDragging.value) {
e.dataTransfer.dropEffect = 'move';
}
};
const ondragleave = (e) => {
};
const ondrop = (e) => {
isDragging.value = false;
};
const changeType = (msg, index) => {
if (msg.type === 'card') {
oldChartStr.value = chartStr.value;
chartStr.value = '';
} else {
setTimeout(() => {
chartStr.value = oldChartStr.value;
}, 500);
}
messages.value[index] = {
...msg,
type: msg.type === 'card' ? 'common' : 'card'
};
}
const client = new OpenAI(
{
apiKey: `sk-1da33fb5253e4bc884b3e0c167012d65`,
baseURL: 'https://dashscope.aliyuncs.com/compatible-mode/v1',
dangerouslyAllowBrowser: true,
}
);
console.log('client', client)
const fetchData = async (ques) => {
console.log('开始请求AI')
const completion = await client.chat.completions.create({
model: 'qwen-plus', //
messages: [
{ role: 'user', content: ques },
],
stream: true, // 为 true 则开启接口的流逝返回
});
for await (const chunk of completion) {
console.log(chunk, 'chunk')
}
}
return {
inputValue,
conversationRef,
inputFootIcons,
messages,
onSubmit,
onInputChange,
introPrompt,
simplePrompt,
onNewConvo,
onItemClick,
startChat,
guessQuestions,
closeDrawer,
chartStr,
oldChartStr,
isDragging,
ondragstart,
ondragover,
ondragleave,
ondrop,
changeType,
fetchData
};
},
});
</script>
<style scoped lang="scss">
.demo-test {
height: 100%;
width: 100%;
display: flex;
flex-direction: column;
gap: 8px;
.conversation-area,
.welcome-page {
flex: 1;
display: flex;
flex-direction: column;
overflow: scroll;
padding: 0 12px;
}
.conversation-area {
gap: 8px;
}
.welcome-page {
gap: 24px;
justify-content: space-between;
.intro {
padding-top: 40px;
flex: 1;
}
}
.guess-question {
width: 100%;
padding: 16px 12px;
border-radius: var(--devui-border-radius-card);
background-color: var(--devui-gray-form-control-bg);
.guess-title {
display: flex;
justify-content: space-between;
align-items: center;
color: var(--devui-text);
margin-bottom: 12px;
& > div:first-child {
font-weight: 700;
font-size: var(--devui-font-size);
}
& > div:last-child {
font-size: var(--devui-font-size-sm);
cursor: pointer;
span {
margin-left: 4px;
}
}
}
.guess-content {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 8px;
span {
font-size: var(--devui-font-size);
color: var(--devui-text);
padding: 4px 12px;
border-radius: var(--devui-border-radius-full);
background-color: var(--devui-gray-form-control-hover-bg);
cursor: pointer;
}
}
}
.operations {
i {
padding: 4px;
border-radius: 4px;
cursor: pointer;
&:hover {
background: var(--devui-global-bg);
}
}
}
.bubble-bottom-operations {
margin-top: 8px;
i {
padding: 4px;
border-radius: 4px;
cursor: pointer;
&:hover {
background-color: var(--devui-icon-hover-bg);
}
}
}
.new-convo-button {
padding: 0 12px;
display: flex;
justify-content: flex-end;
align-items: center;
height: 39px;
gap: 4px;
}
:deep(.simple-prompt .mc-list) {
justify-content: unset;
}
:deep(.intro-prompt .mc-list .content-container) {
flex: 1;
padding: 16px 12px;
height: 100%;
}
.agent-knowledge {
flex: 1;
display: flex;
align-items: center;
.agent-wrapper {
display: flex;
align-items: center;
padding: 4px 8px;
border-radius: var(--devui-border-radius-full);
background-color: var(--devui-area);
cursor: pointer;
img {
width: 16px;
height: 16px;
margin-right: 4px;
}
span {
font-size: var(--devui-font-size);
color: var(--devui-text);
margin-right: 8px;
}
i {
font-size: var(--devui-font-size);
color: var(--devui-text);
&:last-child {
margin-left: 4px;
}
}
}
.agent-knowledge-dividing-line {
width: 1px;
height: 14px;
margin: 0 12px;
background-color: var(--devui-line);
}
.knowledge-wrapper {
font-size: var(--devui-font-size);
color: var(--devui-text);
cursor: pointer;
span {
margin-left: 4px;
}
}
}
.input-foot-wrapper {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
height: 100%;
margin-right: 8px;
.input-foot-left {
display: flex;
align-items: center;
gap: 8px;
span {
font-size: var(--devui-font-size);
color: var(--devui-text);
}
.input-foot-dividing-line {
width: 1px;
height: 14px;
background-color: var(--devui-line);
}
.input-foot-maxlength {
font-size: var(--devui-font-size-sm);
color: var(--devui-aide-text);
}
}
.input-foot-right {
& > *:not(:first-child) {
margin-left: 8px;
}
}
}
}
.content-card {
margin: 8px;
overflow: hidden;
background-color: var(--devui-base-bg);
border-radius: var(--devui-border-radius-card);
box-shadow: var(--devui-shadow-length-base) var(--devui-light-shadow);
height: 400px;
width: 400px;
transition: transform 0.3s, box-shadow 0.3s;
&.dragging {
opacity: 1 !important;
}
&:hover {
transform: translateY(-5px);
}
}
.floating-card {
pointer-events: none;
position: fixed;
transform: translate(-50%, -50%);
background: var(--devui-base-bg);
border-radius: var(--devui-border-radius-card);
box-shadow: var(--devui-shadow-length-connected) var(--devui-light-shadow);
will-change: transform;
pointer-events: none;
opacity: 1 !important;
-webkit-user-drag: none;
}
</style>
\ No newline at end of file
<template>
</template>
<script setup>
import { createApp, defineProps, watch, onBeforeUnmount } from "vue";
import { loadModule } from 'vue3-sfc-loader'
import * as echarts from 'echarts'
const processComponentString = (str) => {
return str.replace(/import.*from.*['"]echarts['"];?\n?/g, '');
};
const props = defineProps({
chartStr: {
type: String,
default: ''
},
containerId: {
type: String,
default: ''
}
})
let currentApp = null;
let currentStyle = null;
onBeforeUnmount(() => {
if (currentApp) {
currentApp.unmount();
currentApp = null;
}
if (currentStyle) {
currentStyle.remove();
currentStyle = null;
}
})
watch(
() => props.chartStr,
async (newVal) => {
if (newVal) {
try {
// 清理之前的实例
if (currentApp) {
currentApp.unmount();
currentApp = null;
}
const options = {
moduleCache: {
vue: Vue
},
getFile(url) {
if (url === 'file.vue') {
const processedStr = processComponentString(props.chartStr);
return Promise.resolve(processedStr)
}
},
addStyle(textContent) {
const style = document.createElement('style');
style.textContent = textContent;
document.head.appendChild(style);
return style;
},
handleModule: async (type, source, path, options) => {
if (type === '.vue') {
return Vue.defineComponent(source.default);
}
}
};
// 加载组件
const component = await loadModule('file.vue', options);
// 创建新实例
const app = createApp(component).use(echarts);
app.config.globalProperties.$echarts = echarts;
app.config.globalProperties.echarts = echarts;
app.mount(`#${props.containerId}`);
currentApp = app;
} catch (error) {
console.error('创建图表失败:', error);
}
}
},
{ immediate: true }
)
</script>
<style scoped>
#demoChart {
width: 100%;
height: 100%;
}
</style>
\ No newline at end of file
export const chartComponentStr = `
<template>
<div ref="pieChart" class="pie-chart"></div>
</template>
<script>
export default {
name: 'PieChart',
props: {
data: {
type: Array,
default: () => [
{ value: 335, name: '直接访问' },
{ value: 310, name: '邮件营销' },
{ value: 234, name: '联盟广告' },
{ value: 135, name: '视频广告' },
{ value: 1548, name: '搜索引擎' }
]
}
},
data() {
return {
chart: null
}
},
mounted() {
this.initChart()
},
methods: {
initChart() {
// 使用全局的 echarts 对象
this.chart = echarts.init(this.$refs.pieChart)
const option = {
tooltip: {
trigger: 'item',
formatter: '{a} <br/>{b}: {c} ({d}%)'
},
legend: {
orient: 'vertical',
left: 'left'
},
series: [
{
name: '访问来源',
type: 'pie',
radius: ['50%', '70%'],
avoidLabelOverlap: false,
itemStyle: {
borderRadius: 10,
borderColor: '#fff',
borderWidth: 2
},
label: {
show: true,
position: 'outside'
},
labelLine: {
show: true
},
data: this.data
}
]
}
this.chart.setOption(option)
}
},
watch: {
data: {
handler(newValue) {
this.chart && this.chart.setOption({
series: [{ data: newValue }]
})
},
deep: true
}
},
beforeDestroy() {
if (this.chart) {
this.chart.dispose()
this.chart = null
}
}
}
<\/script>
<style scoped>
.pie-chart {
width: 100%;
height: 400px;
}
</style>
`;
export const lineStr = `
<template>
<div ref="lineChart" class="line-chart"></div>
</template>
<script>
export default {
name: 'LineChart',
props: {
data: {
type: Array,
default: () => [150, 230, 224, 218, 135, 147, 260]
},
xAxisData: {
type: Array,
default: () => ['周一', '周二', '周三', '周四', '周五', '周六', '周日']
}
},
data() {
return {
chart: null
}
},
mounted() {
this.initChart()
},
methods: {
initChart() {
this.chart = echarts.init(this.$refs.lineChart)
const option = {
tooltip: {
trigger: 'axis',
backgroundColor: 'rgba(255, 255, 255, 0.9)',
borderColor: '#ccc',
borderWidth: 1,
textStyle: {
color: '#333'
}
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true
},
xAxis: {
type: 'category',
boundaryGap: false,
data: this.xAxisData,
axisLine: {
lineStyle: {
color: '#999'
}
}
},
yAxis: {
type: 'value',
axisLine: {
lineStyle: {
color: '#999'
}
},
splitLine: {
lineStyle: {
type: 'dashed'
}
}
},
series: [
{
name: '访问量',
type: 'line',
data: this.data,
smooth: true,
symbol: 'circle',
symbolSize: 8,
itemStyle: {
color: '#409EFF'
},
lineStyle: {
width: 3,
color: '#409EFF'
},
areaStyle: {
color: {
type: 'linear',
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [
{
offset: 0,
color: 'rgba(64,158,255,0.3)'
},
{
offset: 1,
color: 'rgba(64,158,255,0.1)'
}
]
}
}
}
]
}
this.chart.setOption(option)
}
},
watch: {
data: {
handler(newValue) {
this.chart && this.chart.setOption({
series: [{ data: newValue }]
})
},
deep: true
}
},
beforeDestroy() {
if (this.chart) {
this.chart.dispose()
this.chart = null
}
}
}
<\/script>
<style scoped>
.line-chart {
width: 100%;
height: 400px;
}
</style>
`;
export const rader = `
<template>
<div ref="radarChart" :style="{ width: width, height: height }"></div>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue';
import * as echarts from 'echarts';
// 定义组件属性
const props = defineProps({
width: {
type: String,
default: '100%'
},
height: {
type: String,
default: '400px'
},
data: {
type: Array,
default: () => [
{ value: [4200, 3000, 20000, 35000, 50000, 18000], name: '预算分配' },
{ value: [5000, 14000, 28000, 26000, 42000, 21000], name: '实际开销' }
]
}
});
const radarChart = ref<HTMLElement | null>(null);
let chart: echarts.ECharts | null = null;
onMounted(() => {
if (radarChart.value) {
chart = echarts.init(radarChart.value);
const option = {
title: {
text: '基础雷达图'
},
tooltip: {},
legend: {
data: ['预算分配', '实际开销']
},
radar: {
indicator: [
{ name: '销售', max: 6000 },
{ name: '管理', max: 16000 },
{ name: '信息技术', max: 30000 },
{ name: '客服', max: 38000 },
{ name: '研发', max: 52000 },
{ name: '市场', max: 25000 }
]
},
series: [{
name: '预算 vs 开销',
type: 'radar',
data: props.data
}]
};
chart.setOption(option);
}
});
// 监听窗口大小变化,调整图表大小
const handleResize = () => {
chart?.resize();
};
window.addEventListener('resize', handleResize);
onUnmounted(() => {
window.removeEventListener('resize', handleResize);
chart?.dispose();
});
</script>
`
\ No newline at end of file
<template>
<div class="mc-header">
<div class="mc-header-logo-container">
<img class="mc-header-logo" :src="logoImg" :alt="title" />
<div class="mc-header-title">{{ title }}</div>
</div>
<div class="mc-header-operation">
<slot name="operationArea"></slot>
</div>
</div>
</template>
<script setup lang="ts">
import { toRefs } from 'vue';
import { props } from './header-types';
const headerProps = defineProps(props);
</script>
<style scoped lang="scss">
@import 'devui-theme/styles-var/devui-var.scss';
.mc-header {
display: flex;
justify-content: space-between;
padding: 8px 12px;
align-items: center;
.mc-header-logo-container {
display: flex;
align-items: center;
gap: 4px;
.mc-header-title {
letter-spacing: 1px;
font-weight: 500;
font-size: 20px;
}
}
}
</style>
import type { PropType, ExtractPropTypes } from 'vue';
export const props = {
logoImg: {
type: String,
default: '',
},
title: {
type: String,
default: '',
},
};
@import 'devui-theme/styles-var/devui-var.scss';
.mc-history-container {
padding: 8px;
}
.mc-history-item {
padding: 8px;
border-radius: 8px;
background: $devui-global-bg;
border: 1px solid $devui-line;
line-height: 22px;
display: flex;
flex-direction: column;
row-gap: 4px;
margin-bottom: 8px;
cursor: pointer;
&:hover .mc-history-operation {
display: flex;
}
}
.mc-history-item-editing {
background: $devui-base-bg;
}
.mc-history-item-main {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.mc-history-operation {
display: none;
}
.mc-history-content {
color: $devui-text;
display: flex;
display: inline-block;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.mc-history-icon {
cursor: pointer;
padding: 4px 2px;
}
.mc-history-editing {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
input {
flex: 1
}
.mc-history-icon {
scale: 1.4;
}
}
.mc-history-list {
max-height: 600px;
}
.mc-history-title {
color: $devui-text;
font-weight: 700;
margin-bottom: 8px;
}
.mc-history-footer {
display: flex;
justify-content: flex-end;
margin-top: 8px;
border-top: 1px solid $devui-line;
padding-top: 12px;
}
.mc-history-batch {
display: flex;
gap: 8px;
height: 42px;
align-items: center;
cursor: pointer;
}
.mc-history-item-sub {
:hover .mc-history-operation {
display: flex;
}
}
.mc-history-item-title-container {
position: relative;
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
height: 40px;
line-height: 40px;
color: $devui-text-weak;
border-radius: $devui-border-radius;
font-size: $devui-font-size;
box-sizing: border-box;
transition:
font-weight $devui-animation-duration-fast $devui-animation-ease-in-out-smooth,
background-color $devui-animation-duration-fast $devui-animation-ease-in-out-smooth;
cursor: pointer;
.mc-history-item-title {
display: flex;
gap: 8px;
align-items: center;
}
}
.mc-collapse-content {
line-height: 1.5;
color: $devui-text-weak;
}
.mc-collapse-transition {
&-enter-from,
&-leave-to {
opacity: 0;
}
&-enter-to,
&-leave-from {
opacity: 1;
}
&-enter-active,
&-leave-active {
transition:
max-height 0.3s cubic-bezier(0.5, 0.05, 0.5, 0.95),
opacity 0.3s cubic-bezier(0.5, 0.05, 0.5, 0.95);
}
}
\ No newline at end of file
<template>
<d-dropdown
:visible="isOpen"
:trigger="trigger"
:position="position"
:close-scope="'blank'"
:overlay-class="overlayClass"
@toggle="handleToggle($event)"
>
<i class="mc-history-icon icon-history" ref="historyIcon" @click="drop()"></i>
<template class="mc-history-drop" #menu>
<div class="mc-history-container">
<div class="mc-history-title">对话历史</div>
<d-search></d-search>
<List
:listClasses="'mc-history-list'"
:data="historyList"
:variant="ListVariant.None"
style="width: 300px; max-height: 500px; padding-right: 18px"
class="devui-scrollbar devui-scroll-overlay"
>
<template #item="{ item }">
<div>
<div
class="mc-history-item-title-container devui-text-ellipsis"
@click="item.expand = !item.expand"
>
<div class="mc-history-item-title">
<i class="icon-history"></i>
<span :title="item.label">{{ item.label }}</span>
</div>
<i
:class="[
{ 'icon-chevron-up-2': item.expand, 'icon-chevron-down-2': !item.expand },
]"
></i>
</div>
<Transition
name="mc-collapse-transition"
@beforeEnter="beforeEnter"
@enter="enter"
@afterEnter="afterEnter"
@beforeLeave="beforeLeave"
@leave="leave"
@afterLeave="afterLeave"
>
<div v-if="item.expand" class="mc-collapse-content">
<div
v-for="subItem of item.content"
class="mc-history-item"
:class="{ 'mc-history-item-editing': subItem.isEditing }"
>
<div class="mc-history-item-main">
<div v-if="!subItem.isEditing" class="mc-history-content">
<span> {{ subItem.label }}</span>
</div>
<div v-if="subItem.isEditing" class="mc-history-editing">
<input v-model="subItem.label" />
<div>
<i
class="mc-history-icon icon-right"
@click.stop="hadleEditOp(true, subItem)"
></i>
<i
class="mc-history-icon icon-error"
@click.stop="hadleEditOp(false, subItem)"
></i>
</div>
</div>
<div v-if="!subItem.isEditing" class="mc-history-operation">
<i class="mc-history-icon icon-edit" @click.stop="hadleEdit(subItem)"></i>
<i class="mc-history-icon icon-share"></i>
<i class="mc-history-icon icon-delete"></i>
</div>
</div>
<div class="mc-history-item-sub">
<HistorySub></HistorySub>
</div>
</div>
</div>
</Transition>
</div>
</template>
</List>
</div>
</template>
</d-dropdown>
</template>
<script setup lang="ts">
import { onMounted, ref } from 'vue';
// import { props } from './history-types';
import List from '../List/List.vue';
import { ListVariant, ListDirection } from '../List/list-types';
import HistorySub from './HistorySub.vue';
import { computed, RendererElement } from 'vue';
const props = defineProps({
modelValue: { type: Boolean, default: true },
title: { type: String, default: '' },
});
const emits = defineEmits(['update:modelValue']);
const isExpand = computed({
get: () => props.modelValue,
set: (val: boolean) => emits('update:modelValue', val),
});
const beforeEnter = (el: RendererElement) => {
if (!el.dataset) {
el.dataset = {};
}
if (el.style.height) {
el.dataset.height = el.style.height;
}
el.style.maxHeight = 0;
};
const enter = (el: RendererElement) => {
requestAnimationFrame(() => {
el.dataset.oldOverflow = el.style.overflow;
if (el.dataset.height) {
el.style.maxHeight = el.dataset.height;
} else if (el.scrollHeight !== 0) {
el.style.maxHeight = `${el.scrollHeight}px`;
} else {
el.style.maxHeight = 0;
}
el.style.overflow = 'hidden';
});
};
const afterEnter = (el: RendererElement) => {
el.style.maxHeight = '';
el.style.overflow = el.dataset.oldOverflow;
};
const beforeLeave = (el: RendererElement) => {
if (!el.dataset) {
el.dataset = {};
}
el.dataset.oldOverflow = el.style.overflow;
el.style.maxHeight = `${el.scrollHeight}px`;
el.style.overflow = 'hidden';
};
const leave = (el: RendererElement) => {
if (el.scrollHeight !== 0) {
el.style.maxHeight = 0;
}
};
const afterLeave = (el: RendererElement) => {
el.style.maxHeight = '';
el.style.overflow = el.dataset.oldOverflow;
};
// const historyProps = defineProps(props);
const historyIcon = ref(null);
const isOpen = ref(false);
const currentOpItem: any = ref(null);
const trigger = ref('manually');
const position = ref(['bottom-end']);
const overlayClass = ref('overlayClass');
const historyList = ref([
{
key: 1,
label: '2024/12/20',
value: '',
expand: true,
content: [
{
key: 2,
content: '我是对话1',
type: 'history',
label:
'我是对话1我是对话1我是对话1我是对话1我是对话1我是对话1我是对话1我是对话1我是对话1我是对话1我是对话1我是对话1我是对话1我是对话1',
value: '我是对话1',
isEditing: false,
},
{
key: 2,
content: '我是对话2',
type: 'history',
label: '我是对话2',
value: '我是对话2',
isEditing: false,
},
],
},
{
key: 2,
label: '2024/12/20',
value: '',
expand: true,
content: [
{
key: 2,
content: '我是对话1',
type: 'history',
label: '我是对话1',
value: '我是对话1',
isEditing: false,
},
{
key: 2,
content: '我是对话1',
type: 'history',
label: '我是对话1',
value: '我是对话1',
isEditing: false,
},
{
key: 2,
content: '我是对话1',
type: 'history',
label: '我是对话1',
value: '我是对话1',
isEditing: false,
},
],
},
]);
const hadleEdit = (event: any) => {
if (currentOpItem.value) {
currentOpItem.value.isEditing = false;
}
event.isEditing = !event.isEditing;
currentOpItem.value = event;
};
const handleToggle = (event: boolean) => {
isOpen.value = event;
if (event) {
if (currentOpItem.value) {
currentOpItem.value.isEditing = false;
}
}
};
const drop = () => {
if (isOpen.value) {
isOpen.value = false;
} else {
isOpen.value = true;
}
};
const hadleEditOp = (event: boolean, item: any) => {
if (event) {
console.log('编辑确认');
item.isEditing = false;
} else {
console.log('编辑取消');
item.isEditing = false;
}
};
</script>
<style scoped lang="scss">
@import './History.scss';
</style>
<template>
<div class="sub-text" style="display: flex; justify-content: space-between;align-items: center;">
<div style="display: flex; align-items: center;">
<img style="width: 16px;" src="/src/assets/logo.svg">
<span>MateChat</span>
</div>
<span>11:11</span>
</div>
</template>
<script setup lang="ts">
</script>
<style scoped lang="scss">
@import 'devui-theme/styles-var/devui-var.scss';
.sub-text {
font-size: $devui-font-size-sm;
color: $devui-aide-text;
line-height: 18px;
}
</style>
\ No newline at end of file
import type { PropType, ExtractPropTypes } from 'vue';
export enum HistoryType {
Title = 'title',
History = 'history'
}
export enum HistoryOperation {
Check = 'check',
Edit = 'edit',
Share = 'share',
Delete = 'delete',
Cancel = 'cancel',
Confirm = 'confirm'
}
export interface History {
key: string,
content: string,
type: HistoryType,
isEditing?: boolean,
editDisable?: boolean,
shareDisable?: boolean,
deleteDisable?: boolean
}
export const props = {
historyList: {
type: Array as PropType<History[]>,
required: true
},
noDataMessage: {
type: String,
default: '暂无历史记录'
},
};
export const historyEmits = ['operateHistory', { opration: HistoryOperation, historyItem: History }];
<template>
<div :class="['mc-input', { 'mc-input-disabled': disabled }]">
<slot name="head" />
<div class="mc-input-content">
<slot name="prefix" />
<Textarea />
<slot v-if="displayType === DisplayType.Simple" name="button">
<Button />
</slot>
</div>
<div v-if="displayType === DisplayType.Full" class="mc-input-foot">
<div class="mc-input-foot-left">
<slot name="extra" />
<span v-if="showCount" class="mc-input-foot-count">
{{ inputValue.length }}{{ !(maxLength ?? false) ? '' : `/${maxLength}` }}
</span>
</div>
<slot name="button">
<Button />
</slot>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, watch, provide } from 'vue';
import Textarea from './components/textarea.vue';
import Button from './components/button.vue';
import { inputProps, inputEmits, inputInjectionKey, DisplayType } from './input-types';
const props = defineProps(inputProps);
const emits = defineEmits(inputEmits);
const inputValue = ref('');
const clearInput = () => {
inputValue.value = '';
};
const getInput = () => inputValue.value;
watch(
() => props.value,
() => {
inputValue.value = props.value;
},
{ immediate: true },
);
defineExpose({ clearInput, getInput });
provide(inputInjectionKey, { inputValue, rootProps: props, rootEmits: emits });
</script>
<style lang="scss">
@import './input.scss';
</style>
<template>
<d-button
variant="solid"
icon="publish-new"
shape="round"
:disabled="rootProps.disabled || (!rootProps.loading && !inputValue)"
style="border: none"
@click="onConfirm"
>
{{ rootProps.loading ? '暂停回答' : '发送' }}
</d-button>
</template>
<script setup lang="ts">
import { inject } from 'vue';
import { inputInjectionKey } from '../input-types';
import type { InputContext } from '../input-types';
const { inputValue, rootProps, rootEmits } = inject(inputInjectionKey) as InputContext;
const onConfirm = () => {
if (rootProps.loading) {
rootEmits('cancel');
} else {
rootEmits('submit', inputValue.value);
inputValue.value = '';
}
};
</script>
<template>
<d-textarea
v-model="inputValue"
:placeholder="placeholder"
:showGlowStyle="false"
:disabled="rootProps.disabled"
:maxlength="rootProps.maxLength"
class="mc-textarea"
@input="onInput"
@compositionstart="onCompositionStart"
@compositionend="onCompositionEnd"
@keydown="onKeydown"
></d-textarea>
</template>
<script setup lang="ts">
import { nextTick, inject, computed } from 'vue';
import { inputInjectionKey, SubmitShortKey } from '../input-types';
import type { InputContext } from '../input-types';
const { inputValue, rootProps, rootEmits } = inject(inputInjectionKey) as InputContext;
const placeholder = computed(() => {
let enterKey = '';
let shiftEnterKey = '';
if (rootProps.submitShortKey === SubmitShortKey.Enter) {
enterKey = 'Enter';
shiftEnterKey = 'Shift + Enter';
}
if (rootProps.submitShortKey === SubmitShortKey.ShiftEnter) {
enterKey = 'Shift + Enter';
shiftEnterKey = 'Enter';
}
return rootProps.placeholder ?? (enterKey ? `请输入您的问题,并按${enterKey}发送,按${shiftEnterKey}换行` : '请输入您的问题...');
});
let lock = false;
const emitChange = () => {
nextTick(() => {
rootEmits('change', inputValue.value);
});
};
const onInput = () => {
if (!lock) {
emitChange();
}
};
const onCompositionStart = () => {
lock = true;
};
const onCompositionEnd = () => {
lock = false;
emitChange();
};
const onKeydown = (e: KeyboardEvent) => {
if (rootProps.submitShortKey === null) {
return;
}
const shiftKey =
rootProps.submitShortKey === SubmitShortKey.Enter
? !e.shiftKey
: rootProps.submitShortKey === SubmitShortKey.ShiftEnter
? e.shiftKey
: false;
if (shiftKey && e.code === 'Enter') {
e.preventDefault();
rootEmits('submit', inputValue.value);
inputValue.value = '';
}
};
</script>
<style lang="scss">
@import 'devui-theme/styles-var/devui-var.scss';
.mc-textarea {
height: 63px;
border: none;
&::placeholder {
color: $devui-placeholder;
}
}
</style>
import type { ExtractPropTypes, PropType, Ref } from 'vue';
export enum DisplayType {
Simple = 'simple',
Full = 'full',
}
export enum SubmitShortKey {
Enter = 'enter',
ShiftEnter = 'shiftEnter',
}
export const inputProps = {
value: {
type: String,
default: '',
},
placeholder: {
type: String,
},
disabled: {
type: Boolean,
default: false,
},
displayType: {
type: String as PropType<DisplayType>,
default: DisplayType.Full,
},
loading: {
type: Boolean,
default: false,
},
showCount: {
type: Boolean,
default: false,
},
maxLength: {
type: Number,
},
submitShortKey: {
type: [String, null] as PropType<SubmitShortKey | null>,
default: SubmitShortKey.Enter,
},
};
export type InputProps = ExtractPropTypes<typeof inputProps>;
export interface InputContext {
inputValue: Ref<string>;
rootProps: InputProps;
rootEmits: (event: string, ...args: any[]) => void;
}
export const inputEmits = ['change', 'submit', 'cancel'];
export const inputInjectionKey = 'mc-input';
@import 'devui-theme/styles-var/devui-var.scss';
.mc-input {
display: flex;
flex-direction: column;
width: 100%;
padding: 4px 0;
border: 1px solid $devui-form-control-line;
border-radius: 14px;
box-sizing: border-box;
&.mc-input-disabled {
background-color: $devui-disabled-bg;
cursor: not-allowed;
}
.mc-input-content {
display: flex;
align-items: flex-end;
padding: 0 8px;
}
.mc-input-foot {
display: flex;
justify-content: space-between;
align-items: center;
height: 32px;
padding: 0 8px;
.mc-input-foot-left {
flex: 1;
height: 100%;
display: flex;
align-items: center;
.mc-input-foot-count {
margin-left: 8px;
color: $devui-text;
font-size: $devui-font-size;
}
}
}
}
<template>
<div class="mc-introduction" :class="[align, background]">
<div class="mc-introduction-logo-container">
<img :src="logoImg" :alt="title" />
<div class="mc-introduction-title">{{ title }}</div>
</div>
<div class="mc-introduction-sub-title">{{ subTitle }}</div>
<div class="mc-introduction-description">
<div v-for="(item, index) in description" :key="index">{{ item }}</div>
</div>
<slot></slot>
</div>
</template>
<script setup lang="ts">
import { props } from './introduction-types';
defineProps(props);
</script>
<style scoped lang="scss">
@import 'devui-theme/styles-var/devui-var.scss';
.mc-introduction {
display: flex;
gap: 12px;
flex-direction: column;
color: $devui-text;
.mc-introduction-logo-container {
display: flex;
align-items: center;
gap: 8px;
img {
width: 80px;
height: 80px;
}
.mc-introduction-title {
font-weight: 700;
font-size: 36px;
letter-spacing: 1px;
}
}
.mc-introduction-sub-title {
font-weight: 500;
font-size: 18px;
}
.mc-introduction-description {
font-size: $devui-font-size-sm;
text-align: center;
& > div {
line-height: 1.5;
}
}
&.filled {
background-color: $devui-global-bg;
border-radius: 8px;
padding: 8px 12px;
}
&.center {
align-items: center;
}
&.left {
align-items: flex-start;
}
&.right {
align-items: flex-end;
}
}
</style>
import type { PropType } from 'vue';
export type IntroductionBackground = 'filled' | 'transparent';
export type IntroductionAlign = 'left' | 'center' | 'right';
export const props = {
logoImg: {
type: String,
default: '',
},
title: {
type: String,
default: '',
},
subTitle: {
type: String,
default: '',
},
description: {
type: Array as PropType<string[]>,
default: '',
},
background: {
type: String as PropType<IntroductionBackground>,
default: 'transparent',
},
align: {
type: String as PropType<IntroductionAlign>,
default: 'center',
},
};
<template>
<div class="mt-layout-aside">
<slot></slot>
</div>
</template>
<script setup lang="ts"></script>
<style scoped lang="scss">
</style>
\ No newline at end of file
<template>
<div class="mt-layout-content">
<slot></slot>
</div>
</template>
<script setup lang="ts"></script>
<style scoped lang="scss">
.mt-layout-content {
flex: auto;
/* fix firefox can't set height smaller than content on flex item */
min-height: 0;
}
</style>
\ No newline at end of file
<template>
<div class="mt-layout-header">
<slot></slot>
</div>
</template>
<script setup lang="ts"></script>
<style scoped lang="scss">
.mt-layout-header {
flex: 0 0 auto;
min-height: 40px;
}
</style>
\ No newline at end of file
<template>
<div class="mt-layout">
<slot></slot>
</div>
</template>
<script setup lang="ts">
// 怎么获取slot是否有aside
</script>
<style scoped lang="scss">
.mt-layout {
display: flex;
flex: auto;
flex-direction: column;
&-aside {
flex-direction: row
}
}
</style>
<template>
<div class="mt-layout-sender">
<slot></slot>
</div>
</template>
<script setup lang="ts"></script>
<style scoped lang="scss"></style>
\ No newline at end of file
<template>
<div :class="listClasses" @scroll="onListScroll">
<template v-for="(item, index) in data" :key="index">
<div
v-if="variant === ListVariant.Default"
:class="[
'mc-list-item',
{ 'mc-list-item-disabled': item.disabled, 'mc-list-item-active': item.active },
]"
@click="() => onItemClick(item)"
>
<slot name="item" :item="item">
{{ item.label }}
</slot>
</div>
<slot v-if="variant === ListVariant.None" name="item" :item="item"></slot>
</template>
</div>
</template>
<script setup lang="ts">
import { listProps, ListVariant } from './list-types';
import { useList, useListRender } from './use-list';
const props = defineProps(listProps);
const emits = defineEmits(['select', 'load-more']);
const { listClasses } = useListRender(props);
const { onItemClick, onListScroll } = useList(props, emits);
</script>
<style scoped lang="scss">
@import './list.scss';
</style>
import type { ExtractPropTypes, PropType } from 'vue';
export enum ListDirection {
Horizontal = 'horizontal',
Vertical = 'vertical',
}
export enum ListVariant {
Default = 'default',
None = 'none',
}
export interface ListItemData {
label: string;
value: string | number;
disabled?: boolean;
active?: boolean;
[key: string]: any;
}
export const listProps = {
direction: {
type: String as PropType<ListDirection>,
default: ListDirection.Vertical,
},
autoWrap: {
type: Boolean,
default: true,
},
variant: {
type: String as PropType<ListVariant>,
default: ListVariant.Default,
},
enableLazyLoad: {
type: Boolean,
default: false,
},
data: {
type: Array as PropType<ListItemData[]>,
default: () => [],
},
};
export type ListProps = ExtractPropTypes<typeof listProps>;
@import 'devui-theme/styles-var/devui-var.scss';
.mc-list {
width: 100%;
max-height: 300px;
padding: 12px;
box-sizing: border-box;
overflow: auto;
// 临时样式
justify-content: center;
//
.mc-list-item {
width: 100%;
min-height: 36px;
line-height: 20px;
padding: 8px 12px;
color: $devui-text;
font-size: $devui-font-size;
border-radius: $devui-border-radius;
box-sizing: border-box;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
cursor: pointer;
transition:
color $devui-animation-duration-fast $devui-animation-ease-in-out-smooth,
background-color $devui-animation-duration-fast $devui-animation-ease-in-out-smooth;
&:not(:first-child) {
margin-top: 4px;
}
&:hover {
color: $devui-list-item-hover-text;
background-color: $devui-list-item-hover-bg;
}
&.mc-list-item-active {
color: $devui-list-item-active-text;
background-color: $devui-list-item-active-bg;
}
&.mc-list-item-disabled {
color: $devui-disabled-text;
background-color: $devui-disabled-bg;
cursor: not-allowed;
}
}
}
.mc-list-horizontal {
display: flex;
flex-wrap: wrap;
gap: 12px;
.mc-list-item {
width: 300px;
}
&.mc-list-nowrap {
flex-wrap: nowrap;
.mc-list-item {
flex: none;
}
}
}
import { computed } from 'vue';
import { ListDirection } from './list-types';
import type { ListProps, ListItemData } from './list-types';
export function useList(
props: ListProps,
emits: (event: 'select' | 'load-more', ...args: any[]) => void,
) {
const LazyLoadThreshold = 50;
const onItemClick = (item: ListItemData) => {
if (item.disabled) {
return;
}
for (let i = 0; i < props.data.length; i++) {
props.data[i].active = props.data[i].value === item.value;
}
emits('select', { ...item });
};
const onListScroll = (e: Event) => {
if (!props.enableLazyLoad || props.direction !== ListDirection.Vertical) {
return;
}
const targetEl = e.target as HTMLElement;
const scrollHeight = targetEl.scrollHeight;
const clientHeight = targetEl.clientHeight;
const scrollTop = targetEl.scrollTop;
if (scrollHeight - clientHeight - scrollTop < LazyLoadThreshold) {
emits('load-more', e);
}
};
return { onItemClick, onListScroll };
}
export function useListRender(props: ListProps) {
const listClasses = computed(() => ({
'mc-list': true,
'mc-list-horizontal': props.direction === ListDirection.Horizontal,
'mc-list-nowrap': props.direction === ListDirection.Horizontal && !props.autoWrap,
}));
return { listClasses };
}
import { render, h } from 'vue';
import Mention from './Mention.vue';
const InstanceKey = Symbol('mc-mention');
export default {
mounted(el, binding, vnode) {
const { trigger } = vnode.props;
const container = document.createElement('div');
const mentionVNode = h(
Mention,
{ originEl: el, trigger },
{ default: () => vnode.ctx.slots.mention() },
);
mentionVNode.appContext = vnode.ctx.appContext;
render(mentionVNode, container);
document.body.appendChild(container.firstElementChild!);
el[InstanceKey] = {
instance: mentionVNode.component,
};
},
updated(el, binding) {
if (binding.value) {
el[InstanceKey].instance.exposed.toggleVisible(true);
}
},
};
<template>
<Transition name="mc-mention-fade">
<div ref="overlayEl" v-show="isVisible" class="mc-mention" :style="overlayStyle"><slot /></div>
</Transition>
</template>
<script setup lang="ts">
import { mentionProps } from './mention-types';
import { useMention } from './use-mention';
const props = defineProps(mentionProps);
const { isVisible, overlayEl, overlayStyle, toggleVisible } = useMention(props);
defineExpose({ toggleVisible });
</script>
<style scoped lang="scss">
@import 'devui-theme/styles-var/devui-var.scss';
.mc-mention {
position: fixed;
max-height: 300px;
border-radius: $devui-border-radius;
background-color: $devui-connected-overlay-bg;
box-shadow: $devui-shadow-length-connected-overlay $devui-shadow;
transform-origin: 0% 100%;
z-index: 1000;
}
.mc-mention-fade {
&-enter-from,
&-leave-to {
opacity: 0.8;
transform: scaleY(0.8) translateY(4px);
}
&-enter-to,
&-leave-from {
opacity: 1;
transform: scaleY(0.9999) translateY(0);
}
&-enter-active {
transition:
transform 0.2s cubic-bezier(0.16, 0.75, 0.5, 1),
opacity 0.2s cubic-bezier(0.16, 0.75, 0.5, 1);
}
&-leave-active {
transition:
transform 0.2s cubic-bezier(0.5, 0, 0.84, 0.25),
opacity 0.2s cubic-bezier(0.5, 0, 0.84, 0.25);
}
}
</style>
export const MentionSeparator = ' ';
import type { ExtractPropTypes, PropType } from 'vue';
export interface Trigger {
key: string;
onlyInputStart?: boolean;
}
export const mentionProps = {
originEl: {
type: Object as PropType<HTMLElement>,
},
trigger: {
type: Array as PropType<Array<string | Trigger>>,
default: () => [],
},
/* 弹出面板的宽度如何设计 */
menuWidth: {
type: [Number, String] as PropType<number | 'auto'>,
},
onSearchChange: {
type: Function as PropType<(e: { value: string; trigger: string }) => void>,
},
onToggleChange: {
type: Function as PropType<(val: boolean) => void>,
},
};
export type MentionProps = ExtractPropTypes<typeof mentionProps>;
import { onMounted, ref, reactive, watch, nextTick } from 'vue';
import { computePosition, offset } from '@floating-ui/dom';
import { debounce, isObject } from 'lodash';
import type { MentionProps } from './mention-types';
import { MentionSeparator } from './const';
export function useMention(props: MentionProps) {
const overlayEl = ref<HTMLElement>();
const overlayStyle = reactive({ top: '0px', left: '0px', width: 'auto' });
const isVisible = ref(false);
let inputEl: HTMLInputElement | HTMLTextAreaElement;
let currentTrigger: string | null; // 当前匹配到的trigger
let currentTriggerPos: number; // 当前匹配到的trigger的位置
let cursorPos: number; // 光标的位置
let inputValue: string = ''; // trigger到光标位置的字符串
let previousValue: string = '';
const updatePosition = async () => {
if (!props.originEl || !overlayEl.value) {
return;
}
const { x, y } = await computePosition(props.originEl, overlayEl.value, {
strategy: 'fixed',
placement: 'top',
middleware: [offset(4)],
});
overlayStyle.top = `${y}px`;
overlayStyle.left = `${x}px`;
};
const updateOverlayWidth = () => {
if (typeof props.menuWidth === 'undefined') {
const { width } = props.originEl!.getBoundingClientRect();
overlayStyle.width = `${width}px`;
} else {
}
};
watch(isVisible, (val: boolean) => {
if (val) {
updateOverlayWidth();
nextTick(updatePosition);
}
props.onToggleChange?.(val);
});
const toggleVisible = (val?: boolean) => {
if (typeof val === 'undefined') {
isVisible.value = !isVisible.value;
} else {
isVisible.value = val;
}
};
const checkMention = () => {
if (!inputEl) {
return;
}
const value = inputEl.value.replace(/[\r\n]/g, MentionSeparator) || '';
const selectionStart = inputEl.selectionStart; // 光标位置
if (!value.trim() || !selectionStart) {
return;
}
for (let i = 0; i < props.trigger.length; i++) {
const itemTrigger = props.trigger[i];
let triggerStr = '';
let isOnlyInputStart = false;
if (typeof itemTrigger === 'string') {
triggerStr = itemTrigger;
} else if (isObject(itemTrigger)) {
triggerStr = itemTrigger.key;
isOnlyInputStart = Boolean(itemTrigger.onlyInputStart);
} else {
continue;
}
const startPos = value.lastIndexOf(triggerStr, selectionStart); // trigger位置
const mentionStr = value.substring(startPos, selectionStart); // trigger到光标的字符串,eg:@abc
if (startPos < 0 || (startPos > 0 && isOnlyInputStart)) {
currentTrigger = null;
currentTriggerPos = -1;
cursorPos = -1;
inputValue = '';
} else {
currentTrigger = triggerStr;
currentTriggerPos = startPos;
cursorPos = selectionStart;
inputValue = mentionStr.slice(triggerStr.length);
return;
}
}
};
const suggestionsFilter = (value: string) => {
if (previousValue === value && value !== currentTrigger[0]) {
return;
}
previousValue = value;
props.onSearchChange?.({ value, trigger: currentTrigger });
};
const resetMention = () => {
checkMention();
if (!currentTrigger) {
isVisible.value = false;
return;
}
suggestionsFilter(currentTrigger);
isVisible.value = true;
};
const onInput = debounce(resetMention, 300);
const onDocumentClick = (e: Event) => {
if (isVisible.value) {
if (!props.originEl?.contains(e.target)) {
isVisible.value = false;
}
} else if (props.originEl?.contains(e.target)) {
//isVisible.value = true;
}
};
const initEvent = () => {
if (props.originEl) {
inputEl = props.originEl.querySelector('textarea') || props.originEl.querySelector('input');
if (inputEl) {
inputEl.addEventListener('input', onInput);
}
}
document.addEventListener('click', onDocumentClick);
};
onMounted(() => {
initEvent();
});
return { isVisible, overlayEl, overlayStyle, toggleVisible };
}
<template>
<div class="mc-prompt">
<div class="mc-prompt-head" v-if="props.icon || props.title">
<d-icon v-if="props.icon" :name="props.icon" :color="props.color"></d-icon>
<span>{{ props.title }}</span>
</div>
<List :data="props.list" :variant="ListVariant.None" :direction="props.direction" class="list-container">
<template #item="{ item }">
<div class="content-container" @click.stop="onItemClick(item)" :class="{ hover: !item.subList }">
<div class="content">
<List v-if="item.subList" :data="item.subList" :variant="ListVariant.None" class="subList-container">
<template #item="{ item }">
<div class="sub-content-container hover" @click.stop="emit('onItemClick', { ...item })">
<div class="sub-content">
<div v-if="item.icon" class="mc-prompt-icon">
<d-icon :name="item.icon" :color="item.color"></d-icon>
</div>
<div v-if="item.label || item.desc" class="mc-prompt-text">
<div class="mc-prompt-title">{{ item.label }}</div>
<div class="mc-prompt-sub-title">{{ item.desc }}</div>
</div>
</div>
</div>
</template>
</List>
<template v-else>
<div v-if="item.icon" class="mc-prompt-icon">
<d-icon :name="item.icon" :color="item.color"></d-icon>
</div>
<div v-if="item.label || item.desc" class="mc-prompt-text">
<div class="mc-prompt-title">{{ item.label }}</div>
<div class="mc-prompt-sub-title">{{ item.desc }}</div>
</div>
</template>
</div>
</div>
</template>
</List>
</div>
</template>
<script setup lang="ts">
import List from '../List/List.vue';
import { promptProps } from './promp-types';
import type { ListItem } from './promp-types';
import { ListVariant } from '../List/list-types';
const emit = defineEmits(['onItemClick']);
const props = defineProps(promptProps);
function onItemClick(item: ListItem) {
if (item.subList && item.subList.length > 0) {
return;
}
emit('onItemClick', { ...item });
}
</script>
<style scoped lang="scss">
@import './prompt.scss';
</style>
import type { PropType } from 'vue';
import { ListDirection } from '../List/list-types';
export interface PromptItem {
value: String | number;
label?: string;
icon?: string;
color?: string;
desc?: string;
}
export interface ListItem extends PromptItem {
subList?: PromptItem[];
}
export const promptProps = {
icon: {
type: String
},
color: {
type: String
},
title: {
type: String
},
direction: {
type: String as PropType<ListDirection>,
default: ListDirection.Vertical,
},
list: {
type: Array as PropType<ListItem[]>,
default: () => [],
}
};
\ No newline at end of file
@import 'devui-theme/styles-var/devui-var.scss';
.mc-prompt {
width: 100%;
font-size: $devui-font-size;
// 临时样式
display: flex;
flex-direction: column;
align-items: center;
//
.list-container {
border-radius: 8px;
padding: 0;
display: flex;
flex-direction: column;
gap: 12px;
&.mc-list-horizontal {
flex-direction: row;
}
}
.subList-container {
display: flex;
flex-direction: column;
gap: 12px;
}
.mc-prompt-head {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 8px;
span {
color: $devui-aide-text;
}
}
.content-container,
.sub-content-container {
max-width: 50%;
height: fit-content;
border-radius: 5px;
border: 1px solid $devui-dividing-line;
}
.sub-content-container {
max-width: 100%;
border: none;
background-color: $devui-gray-form-control-bg;
}
.hover {
padding: 8px 12px;
cursor: pointer;
&:hover {
background-color: $devui-gray-form-control-hover-bg;
}
}
.content-container {
border: none;
border-radius: $devui-border-radius-card;
background-color: $devui-gray-form-control-bg;
transition: background-color $devui-animation-duration-slow $devui-animation-ease-in-out-smooth;
&:hover {
background-color: $devui-gray-form-control-hover-bg;
}
}
.content,
.sub-content {
display: flex;
gap: 8px;
.mc-prompt-icon {
margin-top: 2px;
line-height: 1em;
}
.mc-prompt-text {
word-break: break-all;
color: $devui-aide-text;
.mc-prompt-title {
font-weight: bold;
color: $devui-text;
}
.mc-prompt-sub-title {
font-size: $devui-font-size-sm;
margin-top: 4px;
}
}
}
}
# MateChat
## 组件列表
- Header 组件 - tzj
- 对话历史组件 - jj
- 对话气泡组件 - tzj
- 输入框组件 - yp
- 布局组件 - zp
- Prompt 列表 - wy
- 快捷操作组件 - yp
- Introduction 组件 - tzj
- List组件 - yp
## 开发注意事项
- 每个组件源码放在 `/components` 下,并为组件新建一个文件夹如 `Bubble`,在该文件夹下新建`Bubble.vue`文件作为入口文件
- html 中类名以 `mc-xxx` 的格式进行命名 (mc -> matechat)
- 组件中涉及到的颜色尽量以 `devui变量` 优先使用
## 输入框组件
1. 最大字数参数,是否显示,extra插槽的右边 -- done
2. head插槽 -- done
3. prefix插槽,simple形态,对齐方式和按钮保持一致 -- done
4. Enter/ShiftEnter,发送与换行互斥,null为不设置快捷键 -- done
5. 支持自定义按钮,相关方法要暴露 -- done
6. 支持loading,加到按钮上 -- 交互待定,功能done
7. 支持自定义输入框 - 遗留,只提供插槽,如何跟按钮等元素结合,由用户自行控制
8. 支持取消,暂停回答 -- done
9. 附件需要怎么搞 - 遗留
10. 混合模式,可插入tag,和文本为两种形态
11. 禁用 -- done
12. simple形态 -- done
## 快捷操作组件
1. 回调事件,抛出触发的关键字 -- done
2. prefix,触发快捷操作的关键字,数组类型,如何识别关键字参考ng的mention组件
3. 整个面板的插槽 -- done
4. toggleChange,面板显示状态变化时触发的事件 -- done
5. 弹出位置是否固定
6. 面板宽度是否铺满
7. 支持自定义类名
8. 是否需要提供控制显隐的方法
## List组件
1. 纵向,横向,仅在横向时支持自动换行 -- done
2. 样式 - 圆角,hover等 -- done
3. 每一项插槽 -- done
4. variant参数,default和none -- done
5. 快捷键,支持上下预选中,回车事件,抛出预选中的数据,支持禁用 -- 放到快捷组件里面
6. 数据结构,label,value,disabled,active -- done
7. 默认形态只展示label -- done
8. 懒加载,仅在纵向支持 -- done
## Bubble
1. 头像如何封装,包括头像名字 -- 封装avatar组件,参数透传
2. 气泡样式 variant -- filled, bordered, none
3. 气泡圆角 rounded -- 与 variant 关联
4. type 打字效果
5. content 嵌套 content
以下是我可以为你做的一些事情:
1. **语言交流**:我可以用中文和英文与你进行流畅的对话。
2. **文件阅读**:你可以上传TXT、PDF、Word文档、PPT幻灯片、Excel电子表格等格式的文件,我可以阅读文件内容后回复你。
3. **网页内容解析**:你可以发送网址给我,我会先解析网页内容,然后结合这些内容回答你的问题。
4. **搜索能力**:当你的问题需要额外信息时,我可以利用搜索能力为你提供答案。
5. **数学计算**:我可以帮你进行数学计算和逻辑推理。
6. **编程帮助**:我可以帮助你理解和编写代码,包括但不限于JavaScript、Python等。
7. **信息整理**:我可以帮助整理和总结信息,让你更容易理解复杂的概念或数据。
8. **教育辅导**:我可以提供学习资料和解释,帮助你学习新知识。
9. **日常咨询**:我可以回答你的日常问题,比如天气、新闻等。
10. **保持幽默**:在提供帮助的同时,我也会尽量保持幽默感,让对话更有趣。
如果你有任何问题或需要帮助,随时告诉我!
import type { App } from 'vue';
import Header from './Header/Header.vue';
import Bubble from './Bubble/Bubble.vue';
import History from './History/History.vue';
import Input from './Input/Input.vue';
import Introduction from './Introduction/Introduction.vue';
import Prompt from './Prompt/Prompt.vue';
import List from './List/List.vue';
import MentionDirective from './Mention/Mention';
export default {
install(app: App): void {
app.component('McHeader', Header);
app.component('McBubble', Bubble);
app.component('McHistory', History);
app.component('McInput', Input);
app.component('McIntroduction', Introduction);
app.component('McPrompt', Prompt);
app.component('McList', List);
app.directive('mention', MentionDirective);
},
};
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 261.76 226.69"><path d="M161.096.001l-30.225 52.351L100.647.001H-.005l130.877 226.688L261.749.001z" fill="#41b883"/><path d="M161.096.001l-30.225 52.351L100.647.001H52.346l78.526 136.01L209.398.001z" fill="#34495e"/></svg>
<svg width="64" height="64" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M12.5483 47.8994C10.7002 48.005 9.52586 45.9542 10.5523 44.4138L16.2493 35.8647C16.3933 35.6486 16.4579 35.3892 16.4321 35.1308L15.5776 26.5826C15.4527 25.333 16.3773 24.2241 17.6289 24.1222L39.4688 22.3438L27.6899 47.0337L12.5483 47.8994V47.8994Z" fill="url(#paint0_linear_0_201)"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M55.124 45.4762C56.1859 46.9926 55.0588 49.0703 53.2086 49.007L42.9447 48.6555C42.685 48.6467 42.4303 48.7282 42.224 48.8862L35.4066 54.1071C34.4095 54.8707 32.98 54.6676 32.235 53.6566L19.2317 36.0109L46.4213 33.0494L55.124 45.4762V45.4762Z" fill="url(#paint1_linear_0_201)"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M32.1698 9.75638C32.9767 8.0901 35.3402 8.06209 36.1864 9.70873L40.8801 18.843C40.9989 19.0742 41.1942 19.2569 41.4327 19.3601L49.3134 22.7697C50.466 23.2684 50.9852 24.6157 50.4653 25.7589L41.3911 45.7121L25.5578 23.411L32.1698 9.75638V9.75638Z" fill="url(#paint2_linear_0_201)"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M32.1698 9.75638C32.9767 8.0901 35.3402 8.06209 36.1864 9.70873L40.8801 18.843C40.9989 19.0742 41.1942 19.2569 41.4327 19.3601L49.3134 22.7697C50.466 23.2684 50.9852 24.6157 50.4653 25.7589L41.3911 45.7121L25.5578 23.411L32.1698 9.75638V9.75638Z" fill="url(#paint3_linear_0_201)"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M39.4687 22.3438L33.3862 34.4701L17.5514 35.4628C16.9489 35.5006 16.4245 35.055 16.3645 34.4543L15.5776 26.5826C15.4527 25.333 16.3772 24.2241 17.6288 24.1222L39.4687 22.3438V22.3438Z" fill="url(#paint4_linear_0_201)"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M9.33589 31.9765C8.0982 31.9765 7.09485 32.9802 7.09485 34.2185C7.09485 35.4567 8.0982 36.4604 9.33589 36.4604C9.93765 36.4604 10.484 36.2232 10.8866 35.837V35.8456L12.5949 34.1365H11.5755C11.5323 32.9362 10.5461 31.9765 9.33589 31.9765Z" fill="url(#paint5_linear_0_201)"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M47.4867 14.6305C48.1059 13.5584 47.739 12.1874 46.6671 11.5683C45.5952 10.9492 44.2243 11.3164 43.6051 12.3885C43.3062 12.9059 43.2371 13.4929 43.366 14.0314L43.358 14.0268L43.9828 16.3611L44.4882 15.4861C45.5502 16.0551 46.8796 15.6815 47.4867 14.6305Z" fill="url(#paint6_linear_0_201)"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M43.3568 56.1077C43.976 57.1798 45.347 57.547 46.4188 56.9279C47.4907 56.3088 47.8576 54.9378 47.2384 53.8657C46.9372 53.3442 46.4582 52.9895 45.9222 52.834L45.9265 52.8315L43.5923 52.2062L44.1027 53.0899C43.0879 53.7282 42.752 55.0605 43.3568 56.1077Z" fill="url(#paint7_linear_0_201)"/>
<defs>
<linearGradient id="paint0_linear_0_201" x1="39.4688" y1="22.3438" x2="8.05859" y2="22.3438" gradientUnits="userSpaceOnUse">
<stop stop-color="#3EB9FC"/>
<stop offset="1" stop-color="#3AE5F6"/>
</linearGradient>
<linearGradient id="paint1_linear_0_201" x1="54.0079" y1="46.1761" x2="39.5026" y2="23.3232" gradientUnits="userSpaceOnUse">
<stop stop-color="#3CB2FD"/>
<stop offset="1" stop-color="#2170F3"/>
</linearGradient>
<linearGradient id="paint2_linear_0_201" x1="56.6755" y1="18.2599" x2="34.1303" y2="5.70763" gradientUnits="userSpaceOnUse">
<stop stop-color="#50D3AB"/>
<stop offset="1" stop-color="#6DBFFF"/>
</linearGradient>
<linearGradient id="paint3_linear_0_201" x1="36.1009" y1="10.2466" x2="23.789" y2="33.5907" gradientUnits="userSpaceOnUse">
<stop stop-color="#F280FF"/>
<stop offset="1" stop-color="#A723E4"/>
</linearGradient>
<linearGradient id="paint4_linear_0_201" x1="39.4687" y1="22.3437" x2="15.3502" y2="22.3437" gradientUnits="userSpaceOnUse">
<stop stop-color="#3EB9FC"/>
<stop offset="1" stop-color="#3AE5F6"/>
</linearGradient>
<linearGradient id="paint5_linear_0_201" x1="7.0949" y1="31.9764" x2="7.0949" y2="36.4604" gradientUnits="userSpaceOnUse">
<stop stop-color="#3EB9FC"/>
<stop offset="1" stop-color="#3AE5F6"/>
</linearGradient>
<linearGradient id="paint6_linear_0_201" x1="42.7688" y1="14.6941" x2="46.058" y2="16.5277" gradientUnits="userSpaceOnUse">
<stop stop-color="#F280FF"/>
<stop offset="1" stop-color="#A723E4"/>
</linearGradient>
<linearGradient id="paint7_linear_0_201" x1="45.2215" y1="51.6527" x2="41.8927" y2="53.6102" gradientUnits="userSpaceOnUse">
<stop stop-color="#3CB2FD"/>
<stop offset="1" stop-color="#2170F3"/>
</linearGradient>
</defs>
</svg>
import quickSortMd from './quicksort.md?raw';
import helpMd from './help.md?raw';
export const introPrompt = {
direction: 'horizontal',
list: [
{
value: 'quickSort',
label: '帮我写一个快速排序',
icon: 'icon-info-o',
color: 'rgb(255, 215, 0)',
desc: '使用 js 快速实现一个可用的快速排序',
},
{
value: 'helpMd',
label: '你可以帮我做些什么?',
icon: 'icon-star',
color: 'rgb(255, 215, 0)',
desc: '了解当前大模型可以帮你做的事',
},
{
value: 'helpMd',
label: '你可以帮我做些什么?',
icon: 'icon-star',
color: 'rgb(255, 215, 0)',
desc: '了解当前大模型可以帮你做的事',
},
],
};
export const guessQuestions = [
{ label: '怎么绑定项目空间' },
{ label: '最近执行流水线列表' },
{ label: '帮我写一个快速排序' },
{ label: '使用 js 格式化时间' },
];
export const simplePrompt = [
{
value: 'quickSort',
icon: 'icon-info-o',
color: 'rgb(255, 215, 0)',
label: '帮我写一个快速排序',
},
{
value: 'helpMd',
icon: 'icon-star',
color: 'rgb(255, 215, 0)',
label: '你可以帮我做些什么?',
},
];
export const mockAnswer = {
quickSort: quickSortMd,
helpMd: helpMd,
};
当然可以。快速排序(Quick Sort)是一种高效的排序算法,它采用分治法(Divide and Conquer)的思想。以下是使用JavaScript实现的快速排序算法:
```javascript
function quickSort(arr) {
if (arr.length < 2) {
return arr;
}
const pivot = arr[0];
const left = [];
const right = [];
for (let i = 1; i < arr.length; i++) {
if (arr[i] < pivot) {
left.push(arr[i]);
} else {
right.push(arr[i]);
}
}
return [...quickSort(left), pivot, ...quickSort(right)];
}
// 使用示例
const arr = [3, 6, 8, 10, 1, 2, 1];
console.log(quickSort(arr)); // 输出排序后的数组
```
这个快速排序的实现使用了递归。它首先选择数组的第一个元素作为基准(pivot),然后将数组分为两部分:一部分包含所有小于基准的元素,另一部分包含所有大于或等于基准的元素。这个过程递归地对左右两部分进行,直到数组被分割成单个元素,然后合并排序后的子数组。
请注意,这个实现不是原地排序(in-place),因为它创建了新的数组来存储排序过程中的中间结果。如果你需要一个原地排序的版本,可以稍作修改来实现。
<template>
<d-layout style="height: 100%">
<d-aside class="daside">
<div class="sideLogo">
<img src="../assets/devui-logo.svg">
<span>DevUI Admin</span>
</div>
<d-menu style="height: 100%;" :collapsed-indent="48" mode="vertical" width="240px" :default-select-keys="['MateChat']">
<d-menu-item key="MateChat">
<template #icon>
<i class="icon-homepage"></i>
</template>
<span>MateChat 演示</span>
</d-menu-item>
</d-menu>
</d-aside>
<d-layout style="overflow: auto">
<d-header class="dheader">
<Header @openDrawer="openDrawer()" class="page-header" :drawerOpen="visible"></Header>
</d-header>
<d-content class="main-content">
<Content></Content>
<Footer></Footer>
</d-content>
</d-layout>
</d-layout>
<d-drawer v-model="visible" position="right" style="padding: 20px; width: 650px" :show-overlay="false" :close-on-click-overlay="false">
<keep-alive>
<Demo @closeDrawer="visible = false" @chartStrChange="chartStrChange($event)"/>
</keep-alive>
</d-drawer>
</template>
<script>
import { defineComponent, ref, onMounted, onUnmounted, createApp, onActivated, onDeactivated } from 'vue'
import Content from './Content.vue'
import Header from './Header.vue';
import Footer from './Footer.vue';
import Demo from './MateChat/Demo.vue';
import test from './test.vue';
import { loadModule } from 'vue3-sfc-loader'
export default defineComponent({
components: {
Content, Header, Footer, Demo, test
},
setup() {
const openDrawer = () => {
visible.value = true;
}
const menu = ref([
{
title: 'Dashboard',
open: true,
children: [{ title: 'MateChat Demo' }],
},
])
const chartStr = ref('');
const visible = ref(false);
// 拖动事件监听
const isDragging = ref(false);
const position = ref({ x: 0, y: 0 });
const chartStrChange = (event) => {
chartStr.value = event;
}
const handleDragOver = (e) => {
e.preventDefault();
if (isDragging.value) {
position.value = {
x: e.clientX,
y: e.clientY
};
}
};
const handleDrop = (e) => {
e.preventDefault();
const randomId = `chart-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
// 获取拖动的数据
const draggedHTML = e.dataTransfer.getData('text/html');
// 获取放下位置的元素
const dropTarget = document.elementFromPoint(e.clientX, e.clientY);
const nearestContentCard = dropTarget?.closest('.devui-col');
if (nearestContentCard) {
// 创建新的元素
const newCard = document.createElement('div');
newCard.className = nearestContentCard.className;
// 使用拖动元素的内容
if (nearestContentCard.offsetHeight > 300) {
newCard.innerHTML = `<div class='content-card'><div class='chart-card'id='${randomId}'></div></div>`;
} else {
newCard.innerHTML = `<div class='content-card-sm'><div class='chart-card'id='${randomId}'></div></div>`;
}
// 将新元素插入到目标元素之前
nearestContentCard.parentNode.insertBefore(newCard, nearestContentCard);
// nearestContentCard.remove();
}
isDragging.value = false;
createChart(randomId);
};
onMounted(() => {
document.addEventListener('dragover', handleDragOver);
document.addEventListener('drop', handleDrop);
});
onUnmounted(() => {
document.removeEventListener('dragover', handleDragOver);
document.removeEventListener('drop', handleDrop);
});
// 预处理组件字符串,移除 import 语句
const processComponentString = (str) => {
return str.replace(/import.*from.*['"]echarts['"];?\n?/g, '');
};
const createChart = async (id) => {
const options = {
moduleCache: {
vue: Vue
},
getFile(url) {
if (url === 'file.vue') {
const processedStr = processComponentString(chartStr.value);
return Promise.resolve(processedStr)
}
},
addStyle(textContent) {
const style = document.createElement('style');
style.textContent = textContent;
document.head.appendChild(style);
return style;
},
handleModule: async (type, source, path, options) => {
if (type === '.vue') {
return Vue.defineComponent(source.default);
}
}
};
// 加载组件
const component = await loadModule('file.vue', options);
// 创建新实例
const app = createApp(component);
app.config.globalProperties.$echarts = echarts;
app.config.globalProperties.echarts = echarts;
app.mount(`#${id}`);
}
return {
menu, openDrawer, visible, createChart, chartStrChange
}
}
})
</script>
<style lang="scss" scoped>
@import 'devui-theme/styles-var/devui-var.scss';
.floating-card {
position: fixed;
transform: translate(-50%, -50%);
pointer-events: none;
opacity: 1;
z-index: 9999;
}
.daside {
display: flex;
flex-direction: column;
z-index: 21;
background-color: $devui-base-bg;;
width: 240px;
min-height: 200px;
display: flex;
align-items: center;
justify-content: center;
box-shadow: var(--devui-shadow-length-connected-overlay, 0 2px 12px 0) var(--devui-light-shadow, rgba(37, 43, 58, 0.12));
}
.dheader {
z-index: 20;
position: fixed;
width: calc(100%);
}
.dfooter {
text-align: center;
line-height: 40px;
min-height: 40px;
}
.main-content {
flex: 1;
margin-top: 60px;
text-align: center;
padding: 12px;
}
.page-header {
border-bottom: 1px solid $devui-dividing-line;
box-shadow: var(--devui-shadow-length-connected-overlay, 0 2px 4px) var(--devui-light-shadow, rgba(37, 43, 58, 0.12));
}
.sideLogo {
display: flex;
align-items: center;
cursor: pointer;
width: 100%;
height: 80px;
padding-left: 16px;
gap: 8px;
span {
font-size: var(--devui-font-size-modal-title, 18px);
font-weight: 600;
white-space: nowrap;
};
img {
height: 30px;
width: 30px;
};
}
</style>
\ No newline at end of file
此差异已折叠。
<template>
</template>
<script setup>
import {
parse,
compileScript,
compileStyle,
rewriteDefault,
} from "@vue/compiler-sfc";
import { createApp } from "vue";
let tStr = `
<template>
<div ref="pieChart" class="pie-chart"></div>
</template>
<script>
export default {
name: 'PieChart',
props: {
data: {
type: Array,
default: () => [
{ value: 335, name: '直接访问' },
{ value: 310, name: '邮件营销' },
{ value: 234, name: '联盟广告' },
{ value: 135, name: '视频广告' },
{ value: 1548, name: '搜索引擎' }
]
}
},
data() {
return {
chart: null
}
},
mounted() {
this.initChart()
},
methods: {
initChart() {
// 使用全局的 echarts 对象
this.chart = echarts.init(this.$refs.pieChart)
const option = {
tooltip: {
trigger: 'item',
formatter: '{a} <br/>{b}: {c} ({d}%)'
},
legend: {
orient: 'vertical',
left: 'left'
},
series: [
{
name: '访问来源',
type: 'pie',
radius: ['50%', '70%'],
avoidLabelOverlap: false,
itemStyle: {
borderRadius: 10,
borderColor: '#fff',
borderWidth: 2
},
label: {
show: true,
position: 'outside'
},
labelLine: {
show: true
},
data: this.data
}
]
}
this.chart.setOption(option)
}
},
watch: {
data: {
handler(newValue) {
this.chart && this.chart.setOption({
series: [{ data: newValue }]
})
},
deep: true
}
},
beforeDestroy() {
if (this.chart) {
this.chart.dispose()
this.chart = null
}
}
}
<\/script>
<style scoped>
.pie-chart {
width: 100%;
height: 400px;
}
</style>
`;
//解析vue单文件字符串
const { descriptor } = parse(tStr);
//获取模板字符串
const template = descriptor.template.content;
//获取样式字符串并添加到<head/>
const style = descriptor.styles.reduce(
(pre, cur) => pre + "\n" + cur.content,
"",
);
const styleTag = document.createElement("style");
styleTag.innerHTML = style;
document.head.appendChild(styleTag);
//将 export default 改成 const 定义的变量,并return出来
const scriptDefault =
rewriteDefault(descriptor.script.content, "__sfc_main__") +
"return __sfc_main__;";
const defineComponent = new Function(scriptDefault)();
//创建实例
let testApp = createApp({
template: template,
...defineComponent,
});
//混入
testApp.mixin({
unmounted() {
//实例卸载时把style删除
styleTag.remove();
},
methods: {
unmount() {
testApp.unmount();
},
},
});
setTimeout(() => {
testApp.mount("#demoChart");
}, 3000);
</script>
import { createApp } from 'vue'
import DevUI from 'vue-devui';
import 'vue-devui/style.css';
import '@devui-design/icons/icomoon/devui-icon.css';
import { ThemeServiceInit, infinityTheme, galaxyTheme } from 'devui-theme';
import './style.scss'
import App from './App.vue'
import MateChat from '../src/components/MateChat/index'
import * as Vue from 'vue'
import * as echarts from 'echarts'
window.Vue = Vue
window.echarts = echarts
// 默认使用无限主题
export const themeServiceInstance = ThemeServiceInit({
infinityTheme, galaxyTheme
}, 'infinityTheme');
if (localStorage.getItem('theme') === 'galaxy-theme') {
themeServiceInstance.applyTheme(galaxyTheme);
}
createApp(App).use(DevUI).use(MateChat).mount('#app');
@import 'devui-theme/styles-var/devui-var.scss';
body {
height: 100%;
min-height: 100vh;
background-color: $devui-global-bg;
}
html {
height: 100%;
}
#app {
height: 100%;
}
.content-card {
margin: 8px;
overflow: hidden;
background-color: $devui-base-bg;
border-radius: $devui-border-radius-card;
box-shadow: $devui-shadow-length-base $devui-light-shadow;
height: 400px;
transition: transform 0.3s, box-shadow 0.3s;
&:hover {
transform: translateY(-5px);
box-shadow: $devui-shadow-length-base $devui-shadow;
}
}
.content-card-sm {
margin: 8px;
overflow: hidden;
background-color: $devui-base-bg;
border-radius: $devui-border-radius-card;
box-shadow: $devui-shadow-length-base $devui-light-shadow;
height: 192px;
transition: transform 0.3s, box-shadow 0.3s;
&:hover {
transform: translateY(-5px);
box-shadow: $devui-shadow-length-base $devui-shadow;
}
}
.chart-card {
box-shadow: $devui-shadow-length-base $devui-light-shadow;
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
padding: 20px;
position: relative;
background-color: $devui-base-bg;
.card-title {
font-size: 14px;
line-height: 22px;
font-weight: 700;
}
.card-subtitle {
font-size: 12px;
line-height: 18px;
margin-top: 4px;
color: $devui-aide-text-stress;
}
.card-content {
flex: 1;
margin-top: 12px;
}
}
* {
-webkit-user-drag: none;
}
[draggable="true"] {
-webkit-user-drag: element;
}
\ No newline at end of file
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
// https://vite.dev/config/
export default defineConfig({
resolve: {
alias: {
'vue': 'vue/dist/vue.esm-bundler.js'
}
},
plugins: [vue()],
})
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册