首次提交
|
|
@ -0,0 +1,4 @@
|
|||
> 1%
|
||||
last 2 versions
|
||||
not dead
|
||||
not ie 11
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
# Changesets
|
||||
|
||||
Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works with multi-package repos, or single-package repos to help you version and publish your code. You can find the full documentation for it [in our repository](https://github.com/changesets/changesets)
|
||||
|
||||
We have a quick list of common questions to get you started engaging with this project in [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md)
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"$schema": "https://unpkg.com/@changesets/config@3.0.0/schema.json",
|
||||
"changelog": [
|
||||
"@changesets/changelog-github",
|
||||
{ "repo": "aiflowy/aiflowy" }
|
||||
],
|
||||
"commit": false,
|
||||
"fixed": [["@aiflowy-core/*", "@aiflowy/*"]],
|
||||
"snapshot": {
|
||||
"prereleaseTemplate": "{tag}-{datetime}"
|
||||
},
|
||||
"privatePackages": { "version": true, "tag": true },
|
||||
"linked": [],
|
||||
"access": "public",
|
||||
"baseBranch": "main",
|
||||
"updateInternalDependencies": "patch",
|
||||
"ignore": []
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from '@aiflowy/commitlint-config';
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
node_modules
|
||||
.git
|
||||
.gitignore
|
||||
*.md
|
||||
dist
|
||||
.turbo
|
||||
dist.zip
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
root = true
|
||||
|
||||
[*]
|
||||
charset=utf-8
|
||||
end_of_line=lf
|
||||
insert_final_newline=true
|
||||
indent_style=space
|
||||
indent_size=2
|
||||
max_line_length = 100
|
||||
trim_trailing_whitespace = true
|
||||
quote_type = single
|
||||
|
||||
[*.{yml,yaml,json}]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
# https://docs.github.com/cn/get-started/getting-started-with-git/configuring-git-to-handle-line-endings
|
||||
|
||||
# Automatically normalize line endings (to LF) for all text-based files.
|
||||
* text=auto eol=lf
|
||||
|
||||
# Declare files that will always have CRLF line endings on checkout.
|
||||
*.{cmd,[cC][mM][dD]} text eol=crlf
|
||||
*.{bat,[bB][aA][tT]} text eol=crlf
|
||||
|
||||
# Denote all files that are truly binary and should not be modified.
|
||||
*.{ico,png,jpg,jpeg,gif,webp,svg,woff,woff2} binary
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
[core]
|
||||
ignorecase = false
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
node_modules
|
||||
.DS_Store
|
||||
dist
|
||||
dist-ssr
|
||||
dist.zip
|
||||
dist.tar
|
||||
dist.war
|
||||
.nitro
|
||||
.output
|
||||
*-dist.zip
|
||||
*-dist.tar
|
||||
*-dist.war
|
||||
coverage
|
||||
*.local
|
||||
**/.vitepress/cache
|
||||
.cache
|
||||
.turbo
|
||||
.temp
|
||||
dev-dist
|
||||
.stylelintcache
|
||||
yarn.lock
|
||||
package-lock.json
|
||||
.VSCodeCounter
|
||||
**/backend-mock/data
|
||||
|
||||
# local env files
|
||||
.env.local
|
||||
.env.*.local
|
||||
.eslintcache
|
||||
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
vite.config.mts.*
|
||||
vite.config.mjs.*
|
||||
vite.config.js.*
|
||||
vite.config.ts.*
|
||||
|
||||
# Editor directories and files
|
||||
.idea
|
||||
# .vscode
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
.history
|
||||
.cursor
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
ports:
|
||||
- port: 5555
|
||||
onOpen: open-preview
|
||||
tasks:
|
||||
- init: npm i -g corepack && pnpm install
|
||||
command: pnpm run dev:play
|
||||
|
|
@ -0,0 +1 @@
|
|||
22.1.0
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
registry=https://registry.npmmirror.com
|
||||
public-hoist-pattern[]=lefthook
|
||||
public-hoist-pattern[]=eslint
|
||||
public-hoist-pattern[]=prettier
|
||||
public-hoist-pattern[]=prettier-plugin-tailwindcss
|
||||
public-hoist-pattern[]=stylelint
|
||||
public-hoist-pattern[]=*postcss*
|
||||
public-hoist-pattern[]=@commitlint/*
|
||||
public-hoist-pattern[]=czg
|
||||
|
||||
strict-peer-dependencies=false
|
||||
auto-install-peers=true
|
||||
dedupe-peer-dependents=true
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
dist
|
||||
dev-dist
|
||||
.local
|
||||
.output.js
|
||||
node_modules
|
||||
.nvmrc
|
||||
coverage
|
||||
CODEOWNERS
|
||||
.nitro
|
||||
.output
|
||||
|
||||
|
||||
**/*.svg
|
||||
**/*.sh
|
||||
|
||||
public
|
||||
.npmrc
|
||||
*-lock.yaml
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from '@aiflowy/prettier-config';
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
dist
|
||||
public
|
||||
__tests__
|
||||
coverage
|
||||
|
|
@ -0,0 +1,152 @@
|
|||
{
|
||||
"folders": [
|
||||
{
|
||||
"name": "@aiflowy/app",
|
||||
"path": "app",
|
||||
},
|
||||
{
|
||||
"name": "@aiflowy/commitlint-config",
|
||||
"path": "internal/lint-configs/commitlint-config",
|
||||
},
|
||||
{
|
||||
"name": "@aiflowy/eslint-config",
|
||||
"path": "internal/lint-configs/eslint-config",
|
||||
},
|
||||
{
|
||||
"name": "@aiflowy/prettier-config",
|
||||
"path": "internal/lint-configs/prettier-config",
|
||||
},
|
||||
{
|
||||
"name": "@aiflowy/stylelint-config",
|
||||
"path": "internal/lint-configs/stylelint-config",
|
||||
},
|
||||
{
|
||||
"name": "@aiflowy/node-utils",
|
||||
"path": "internal/node-utils",
|
||||
},
|
||||
{
|
||||
"name": "@aiflowy/tailwind-config",
|
||||
"path": "internal/tailwind-config",
|
||||
},
|
||||
{
|
||||
"name": "@aiflowy/tsconfig",
|
||||
"path": "internal/tsconfig",
|
||||
},
|
||||
{
|
||||
"name": "@aiflowy/vite-config",
|
||||
"path": "internal/vite-config",
|
||||
},
|
||||
{
|
||||
"name": "@aiflowy-core/design",
|
||||
"path": "packages/@core/base/design",
|
||||
},
|
||||
{
|
||||
"name": "@aiflowy-core/icons",
|
||||
"path": "packages/@core/base/icons",
|
||||
},
|
||||
{
|
||||
"name": "@aiflowy-core/shared",
|
||||
"path": "packages/@core/base/shared",
|
||||
},
|
||||
{
|
||||
"name": "@aiflowy-core/typings",
|
||||
"path": "packages/@core/base/typings",
|
||||
},
|
||||
{
|
||||
"name": "@aiflowy-core/composables",
|
||||
"path": "packages/@core/composables",
|
||||
},
|
||||
{
|
||||
"name": "@aiflowy-core/preferences",
|
||||
"path": "packages/@core/preferences",
|
||||
},
|
||||
{
|
||||
"name": "@aiflowy-core/form-ui",
|
||||
"path": "packages/@core/ui-kit/form-ui",
|
||||
},
|
||||
{
|
||||
"name": "@aiflowy-core/layout-ui",
|
||||
"path": "packages/@core/ui-kit/layout-ui",
|
||||
},
|
||||
{
|
||||
"name": "@aiflowy-core/menu-ui",
|
||||
"path": "packages/@core/ui-kit/menu-ui",
|
||||
},
|
||||
{
|
||||
"name": "@aiflowy-core/popup-ui",
|
||||
"path": "packages/@core/ui-kit/popup-ui",
|
||||
},
|
||||
{
|
||||
"name": "@aiflowy-core/shadcn-ui",
|
||||
"path": "packages/@core/ui-kit/shadcn-ui",
|
||||
},
|
||||
{
|
||||
"name": "@aiflowy-core/tabs-ui",
|
||||
"path": "packages/@core/ui-kit/tabs-ui",
|
||||
},
|
||||
{
|
||||
"name": "@aiflowy/constants",
|
||||
"path": "packages/constants",
|
||||
},
|
||||
{
|
||||
"name": "@aiflowy/access",
|
||||
"path": "packages/effects/access",
|
||||
},
|
||||
{
|
||||
"name": "@aiflowy/common-ui",
|
||||
"path": "packages/effects/common-ui",
|
||||
},
|
||||
{
|
||||
"name": "@aiflowy/hooks",
|
||||
"path": "packages/effects/hooks",
|
||||
},
|
||||
{
|
||||
"name": "@aiflowy/layouts",
|
||||
"path": "packages/effects/layouts",
|
||||
},
|
||||
{
|
||||
"name": "@aiflowy/plugins",
|
||||
"path": "packages/effects/plugins",
|
||||
},
|
||||
{
|
||||
"name": "@aiflowy/request",
|
||||
"path": "packages/effects/request",
|
||||
},
|
||||
{
|
||||
"name": "@aiflowy/icons",
|
||||
"path": "packages/icons",
|
||||
},
|
||||
{
|
||||
"name": "@aiflowy/locales",
|
||||
"path": "packages/locales",
|
||||
},
|
||||
{
|
||||
"name": "@aiflowy/preferences",
|
||||
"path": "packages/preferences",
|
||||
},
|
||||
{
|
||||
"name": "@aiflowy/stores",
|
||||
"path": "packages/stores",
|
||||
},
|
||||
{
|
||||
"name": "@aiflowy/styles",
|
||||
"path": "packages/styles",
|
||||
},
|
||||
{
|
||||
"name": "@aiflowy/types",
|
||||
"path": "packages/types",
|
||||
},
|
||||
{
|
||||
"name": "@aiflowy/utils",
|
||||
"path": "packages/utils",
|
||||
},
|
||||
{
|
||||
"name": "@aiflowy/turbo-run",
|
||||
"path": "scripts/turbo-run",
|
||||
},
|
||||
{
|
||||
"name": "@aiflowy/vsh",
|
||||
"path": "scripts/vsh",
|
||||
},
|
||||
],
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
# 应用标题
|
||||
VITE_APP_TITLE=AIFlowy
|
||||
|
||||
# 应用命名空间,用于缓存、store等功能的前缀,确保隔离
|
||||
VITE_APP_NAMESPACE=aiflowy-web
|
||||
|
||||
# 对store进行加密的密钥,在将store持久化到localStorage时会使用该密钥进行加密
|
||||
VITE_APP_STORE_SECURE_KEY=please-replace-me-with-your-own-key
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
# public path
|
||||
VITE_BASE=/
|
||||
|
||||
# Basic interface address SPA
|
||||
VITE_GLOB_API_URL=/api
|
||||
|
||||
VITE_VISUALIZER=true
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
# 端口号
|
||||
VITE_PORT=5090
|
||||
|
||||
VITE_BASE=/
|
||||
|
||||
# 接口地址
|
||||
VITE_GLOB_API_URL=http://127.0.0.1:8080
|
||||
|
||||
# 是否打开 devtools,true 为打开,false 为关闭
|
||||
VITE_DEVTOOLS=false
|
||||
|
||||
# 是否注入全局loading
|
||||
VITE_INJECT_APP_LOADING=true
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
VITE_BASE=/
|
||||
|
||||
# 接口地址
|
||||
VITE_GLOB_API_URL=/
|
||||
|
||||
# 是否开启压缩,可以设置为 none, brotli, gzip
|
||||
VITE_COMPRESS=none
|
||||
|
||||
# 是否开启 PWA
|
||||
VITE_PWA=false
|
||||
|
||||
# vue-router 的模式
|
||||
VITE_ROUTER_HISTORY=hash
|
||||
|
||||
# 是否注入全局loading
|
||||
VITE_INJECT_APP_LOADING=true
|
||||
|
||||
# 打包后是否生成dist.zip
|
||||
VITE_ARCHIVER=true
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
<!doctype html>
|
||||
<html lang="zh">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
|
||||
<meta name="renderer" content="webkit" />
|
||||
<meta name="description" content="A Modern Back-end Management System" />
|
||||
<meta name="keywords" content="AIFlowy Admin Vue3 Vite" />
|
||||
<meta name="author" content="AIFlowy" />
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width,initial-scale=1.0,minimum-scale=1.0,maximum-scale=1.0,user-scalable=0"
|
||||
/>
|
||||
<title><%= VITE_APP_TITLE %></title>
|
||||
<link rel="icon" href="/favicon.svg" />
|
||||
<script>
|
||||
var _hmt = _hmt || [];
|
||||
(function() {
|
||||
var hm = document.createElement("script");
|
||||
hm.src = "https://hm.baidu.com/hm.js?42639a1503843c26bc4d8b35185616d9";
|
||||
var s = document.getElementsByTagName("script")[0];
|
||||
s.parentNode.insertBefore(hm, s);
|
||||
})();
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<!-- 引入验证码初始化js -->
|
||||
<script src="/load.min.js"></script>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
{
|
||||
"name": "@aiflowy/app",
|
||||
"version": "1.0.0",
|
||||
"homepage": "https://aiflowy.tech",
|
||||
"bugs": "https://github.com/aiflowy/aiflowy/issues",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/aiflowy/aiflowy.git",
|
||||
"directory": "app"
|
||||
},
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "pnpm vite build --mode production",
|
||||
"build:analyze": "pnpm vite build --mode analyze",
|
||||
"dev": "pnpm vite --mode development",
|
||||
"preview": "vite preview",
|
||||
"typecheck": "vue-tsc --noEmit --skipLibCheck"
|
||||
},
|
||||
"imports": {
|
||||
"#/*": "./src/*"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aiflowy-core/shadcn-ui": "workspace:*",
|
||||
"@aiflowy/access": "workspace:*",
|
||||
"@aiflowy/common-ui": "workspace:*",
|
||||
"@aiflowy/constants": "workspace:*",
|
||||
"@aiflowy/hooks": "workspace:*",
|
||||
"@aiflowy/icons": "workspace:*",
|
||||
"@aiflowy/layouts": "workspace:*",
|
||||
"@aiflowy/locales": "workspace:*",
|
||||
"@aiflowy/plugins": "workspace:*",
|
||||
"@aiflowy/preferences": "workspace:*",
|
||||
"@aiflowy/request": "workspace:*",
|
||||
"@aiflowy/stores": "workspace:*",
|
||||
"@aiflowy/styles": "workspace:*",
|
||||
"@aiflowy/types": "workspace:*",
|
||||
"@aiflowy/utils": "workspace:*",
|
||||
"@element-plus/icons-vue": "^2.3.2",
|
||||
"@tinyflow-ai/vue": "^1.1.10",
|
||||
"@vueuse/core": "catalog:",
|
||||
"dayjs": "catalog:",
|
||||
"dompurify": "^3.3.1",
|
||||
"element-plus": "catalog:",
|
||||
"fetch-event-stream": "^0.1.6",
|
||||
"highlight.js": "^11.11.1",
|
||||
"markdown-it": "^14.1.0",
|
||||
"pinia": "catalog:",
|
||||
"radash": "^12.1.1",
|
||||
"vue": "catalog:",
|
||||
"vue-cropper": "^1.1.4",
|
||||
"vue-element-plus-x": "^1.3.7",
|
||||
"vue-router": "catalog:",
|
||||
"vue3-json-viewer": "^2.4.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"cssnano": "catalog:",
|
||||
"unplugin-element-plus": "catalog:"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from '@aiflowy/tailwind-config/postcss';
|
||||
|
After Width: | Height: | Size: 50 KiB |
|
After Width: | Height: | Size: 43 KiB |
|
After Width: | Height: | Size: 44 KiB |
|
|
@ -0,0 +1,11 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="31.909757px" height="29.09091px" viewBox="0 0 31.909757 29.09091" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<title>编组备份</title>
|
||||
<g id="页面-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="编组-13">
|
||||
<path d="M4.733643,29.026415 L6.86382,23.948418 C7.530846,22.377681 8.778828,21.108181 10.349565,20.441157 C15.341497,18.310979 25.66963,13.620285 26.422724,12.974776 C27.43402,12.114098 28.725035,9.639649 24.89502,8.994141 C24.89502,8.994141 28.961724,9.037175 29.392064,12.028031 C29.908469,15.771979 28.595934,18.547665 27.283402,19.257724 C26.250588,19.838682 6.519549,29.047934 6.519549,29.047934 L4.733643,29.026415 Z" id="Fill-228" fill="#00B8A9"></path>
|
||||
<path d="M3.356644,28.918773 L5.551372,23.474987 C6.261431,21.796665 8.047338,20.204412 9.725659,19.494352 C9.790211,19.472835 9.962346,19.408285 10.026896,19.365251 L15.061862,7.789135 C15.42765,6.820871 16.783218,6.820871 17.127489,7.789135 L20.075312,14.631523 C22.958582,13.340506 24.87359,12.458311 25.583649,12.071006 L21.968801,4.02367 C21.06509,1.592254 18.762777,0 16.202261,0 C13.620228,0 11.339431,1.592254 10.435719,4.002151 L0,29.069395 L3.292093,29.09091 C3.31361,29.047876 3.335127,28.961807 3.356644,28.918773 Z" id="Fill-229" fill="#0066FF"></path>
|
||||
<path d="M28.488561,19.816895 C28.251874,20.053581 28.015191,20.247233 27.778504,20.376335 C27.477267,20.548471 25.540741,21.4737 22.980225,22.678649 L23.690283,24.830343 C24.550961,27.369343 26.939344,29.069182 29.607445,29.069182 L31.909757,29.069182 L28.488561,19.816895 Z" id="Fill-230" fill="#0066FF"></path>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
|
|
@ -0,0 +1 @@
|
|||
const Math=window.Math,head=document.getElementsByTagName("head")[0],TIMEOUT=1e4,TAC_LOADING_DIV='<div id="tac-loading" style="\n border: 1px solid #eee;\n /* background-color: #409EFF; */\n border-radius: 5px;\n width: 318px;\n height: 318px;\n line-height: 318px;\n color: #606266;\n text-align: center;\n position: relative;\n box-sizing: border-box;\n">请稍等...</div>';function showLoading(e){var t=document.querySelector(e);t&&(t.innerHTML=TAC_LOADING_DIV)}function hideLoading(e){let t=document.querySelector(e);t&&(t.innerHTML="")}function loadCaptchaScript(e,t,n,r,o){const i=e.scriptUrls,c=e.cssUrls,l=e.timeout||TIMEOUT;let s=i.length+c.length;function d(e,i){if(s--,e&&0===s){if(hideLoading(t.bindEl),!window.TAC)throw new Error("TAC未加载,请检查地址是否正确");r(new TAC(t,n))}else e||(hideLoading(t.bindEl),o(i))}setTimeout(()=>{0!==s&&showLoading(t.bindEl)},10),i.forEach(function(e){loadResource("string"==typeof e?{url:e}:e,d,"script",l)}),c.forEach(function(e){loadResource("string"==typeof e?{url:e}:e,d,"link",l)})}function loadResource(e,t,n="script",r){if(document.querySelector(`${n}[${"script"===n?"src":"href"}="${e.url}"]`))return void t(!0,e);let o=!1;const i=document.createElement(n);"link"===n?i.rel="stylesheet":i.async=!0,i["script"===n?"src":"href"]=e.url;let c;i.onload=i.onreadystatechange=(()=>{o||i.readyState&&"loaded"!==i.readyState&&"complete"!==i.readyState||function t(n){e.checkOnReady?c=setTimeout(()=>{e.checkOnReady()?n():t(n)},10):n()}(()=>{o=!0,setTimeout(()=>t(o,e),0)})}),i.onerror=(()=>{t(o=!1,e)}),head.appendChild(i),setTimeout(()=>{o||(c&&clearTimeout(c),i.onload=i.onerror=null,i.remove&&i.remove(),t(o,e))},r||TIMEOUT)}function loadTAC(e,t,n){return new Promise((r,o)=>{let i={..."string"==typeof e?{url:e}:e};i.url&&(i.url.endsWith("/")||(i.url+="/"),i.scriptUrls||(i.scriptUrls=[i.url+"js/tac.min.js"]),i.cssUrls||(i.cssUrls=[i.url+"css/tac.css"])),i.scriptUrls&&i.cssUrls?loadCaptchaScript(i,t,n,r,o):o("请按照文档配置tac")})}setTimeout(()=>{let e=document.scripts,t=null;for(let n=0;n<e.length;n++)if(e[n].src.indexOf("load.js")>1||e[n].src.indexOf("load.min.js")>1){t=e[n].src.substring(e[n].src.indexOf("/"),e[n].src.lastIndexOf("/"));break}},100),window.loadCaptchaScript=loadCaptchaScript,window.loadTAC=loadTAC,window.initTAC=loadTAC;
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="120px" height="27px" viewBox="0 0 120 27" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<title>编组备份 2</title>
|
||||
<g id="页面-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="编组备份-2">
|
||||
<path d="M79.8815982,19.2506193 L76.6540538,6.04303586 L79.5588436,6.04303586 C80.8175862,6.04303586 81.8826748,6.93653709 82.1247405,8.18744003 L83.8837514,17.431125 L86.17531,8.26866844 C87.0467477,4.80837921 91.9203387,4.8083777 92.7917771,8.2524214 L95.1317468,17.4148787 L97.2296507,8.07372281 C97.5039912,6.87155572 98.5529421,6.02679033 99.7794076,6.02679033 L102.47441,6.02679033 L99.1016223,19.088164 C98.0688077,23.0683095 92.4367464,23.0033281 91.5007576,19.0069371 L89.4674099,10.3968278 L87.5147391,19.1693924 C86.6110266,23.2307648 80.8821372,23.2957469 79.8815982,19.2506193 Z" id="Fill-223" fill="#1A1A1A"></path>
|
||||
<path d="M28.7088751,1.42965135 L30.9520204,1.42965135 L30.9520204,19.2347098 C30.9520204,20.6805583 29.7739674,21.8664799 28.3377095,21.8664799 L26.094565,21.8664799 L26.094565,4.06141997 C26.094565,2.61557216 27.2564801,1.42965135 28.7088751,1.42965135 Z" id="Fill-224" fill="#1A1A1A"></path>
|
||||
<path d="M55.2394116,1.42965135 L57.4825592,1.42965135 L57.4825592,19.2347098 C57.4825592,20.6805583 56.3045032,21.8664799 54.8682461,21.8664799 L52.6251038,21.8664799 L52.6251038,4.06141997 C52.6251038,2.61557216 53.7870167,1.42965135 55.2394116,1.42965135 Z" id="Fill-225" fill="#1A1A1A"></path>
|
||||
<path d="M47.2511589,14.2795152 L39.5050543,14.2795152 L39.5050543,19.039444 C39.5050543,20.4852926 38.6497559,21.6712126 37.213498,21.6712126 L34.6153241,21.6712126 L34.2925696,4.02860953 C34.2925696,2.58276097 35.4706233,1.39684092 36.9068797,1.39684092 L50.8821458,1.39684092 L50.8821458,3.26507256 C50.8821458,4.71092112 49.7040959,5.89684092 48.2678387,5.89684092 L39.5050543,5.89684092 L39.5050543,10.0069509 L49.865472,10.0069509 L49.865472,11.6639922 C49.865472,13.10984 48.6874161,14.2795152 47.2511589,14.2795152 Z" id="Fill-226" fill="#1A1A1A"></path>
|
||||
<path d="M108.735872,21.8339337 L103.00698,6.09205332 L106.202248,6.09205332 C107.348026,6.09205332 108.348569,6.83934703 108.68746,7.92779464 L111.317907,16.310468 L114.561596,7.55414817 C114.948901,6.53068192 115.917156,5.84837109 117.014521,5.84837109 L120,5.84837109 L112.608925,25.31047 C112.22162,26.3339362 111.253361,27 110.172133,27 L106.557279,27 L108.735872,21.8339337 Z" id="Fill-227" fill="#1A1A1A"></path>
|
||||
<g id="编组-2">
|
||||
<path d="M3.55023513,21.9152089 L5.14786918,18.0812747 C5.64813909,16.8953539 6.58412635,15.9368698 7.76218006,15.4332606 C11.5061321,13.8249567 19.2522381,10.2834398 19.8170591,9.7960746 C20.5755317,9.14625484 21.5437938,7.2780232 18.6712802,6.79065875 C18.6712802,6.79065875 21.7213106,6.82314982 22.0440659,9.08127346 C22.43137,11.9079885 21.4469679,14.0036568 20.4625681,14.5397578 C19.687957,14.9783864 4.88966572,21.931456 4.88966572,21.931456 L3.55023513,21.9152089 Z" id="Fill-228" fill="#00B8A9"></path>
|
||||
<path d="M2.51748505,21.8339382 L4.16353238,17.72383 C4.69607707,16.4566815 6.0355084,15.2545159 7.29425018,14.7184141 C7.34266422,14.7021686 7.47176557,14.6534328 7.52017811,14.6209417 L11.2964057,5.8808682 C11.5707469,5.14982002 12.5874237,5.14982002 12.8456272,5.8808682 L15.0564962,11.0469337 C17.2189505,10.0722041 18.6552077,9.4061388 19.1877523,9.11371998 L16.4766141,3.03790767 C15.7988303,1.20216634 14.0720942,0 12.1517056,0 C10.2151793,0 8.50458016,1.20216634 7.82679561,3.02166063 L0,21.9476592 L2.46907176,21.9639032 C2.48520952,21.9314122 2.50134728,21.8664293 2.51748505,21.8339382 Z" id="Fill-229" fill="#0066FF"></path>
|
||||
<path d="M21.3664381,14.9619371 C21.1889227,15.1406371 21.0114103,15.2868462 20.8338949,15.3843194 C20.607967,15.5142836 19.1555713,16.21284 17.2351828,17.1225875 L17.7677267,18.7471362 C18.4132357,20.6641044 20.2045244,21.9474984 22.2056018,21.9474984 L23.9323372,21.9474984 L21.3664381,14.9619371 Z" id="Fill-230" fill="#0066FF"></path>
|
||||
</g>
|
||||
<path d="M74.507936,11.4045234 C75.5328869,11.4045234 76.3637735,10.5680888 76.3637735,9.53629256 C76.3637735,8.50449706 75.5328869,7.66806168 74.507936,7.66806168 C73.4829845,7.66806168 72.6520978,8.50449706 72.6520978,9.53629256 C72.6520978,10.5680888 73.4829845,11.4045234 74.507936,11.4045234 Z" id="Fill-231" fill="#1A1A1A"></path>
|
||||
<path d="M60.2098434,19.6246138 C61.1278434,19.6246138 61.872027,18.8754575 61.872027,17.9513278 C61.872027,17.0271974 61.1278434,16.2780418 60.2098434,16.2780418 C59.2918442,16.2780418 58.5476553,17.0271974 58.5476553,17.9513278 C58.5476553,18.8754575 59.2918442,19.6246138 60.2098434,19.6246138 Z" id="Fill-232" fill="#1A1A1A"></path>
|
||||
<path d="M65.9387336,8.77233806 C67.9236739,7.83009948 70.4572937,8.13876383 72.1194788,8.6911104 C72.2647187,8.15501012 72.7649901,7.47269929 73.3782244,7.21277153 C71.9096909,5.84814988 69.6342723,4.80843885 67.4718173,4.80843885 L67.4556788,4.80843885 C62.7273262,4.75970226 58.7897203,8.99977476 58.7897203,13.5810034 C58.7897203,14.377033 58.9026884,15.1568155 59.0963408,15.8716174 C59.3868198,15.7254068 59.7579838,15.530462 60.4034928,15.530462 C61.6138213,15.5467075 62.5820848,16.4402095 62.5820848,17.6261303 C62.5820848,17.7723401 62.5820878,17.853567 62.5498123,17.9997768 C64.5347519,18.4871412 66.5358271,18.243459 66.5358271,18.243459 C61.8074745,17.3174667 61.0812796,11.0467072 65.9387336,8.77233806 Z" id="Fill-233" fill="#1A1A1A"></path>
|
||||
<path d="M74.507864,12.1191872 C73.1522957,12.1191872 72.0387893,11.0469843 72.0387893,9.68236343 C72.0387893,9.58489099 72.0387893,9.55239993 72.0387893,9.45492674 C70.1506755,8.87008985 68.6660058,8.93507123 68.6660058,8.93507123 C70.7639097,9.47117227 71.5223824,11.5505943 71.7160348,12.6227971 C71.7321725,12.7202696 71.7483133,12.8339891 71.7644503,12.9639533 C71.7967266,13.2076355 71.8290014,13.4675633 71.8290014,13.7274903 C71.8290014,14.2798376 71.7321733,14.8159379 71.5707964,15.3195486 C71.102802,16.9278518 70.1022622,18.3899459 66.713341,19.0072746 C65.0027418,19.3159389 63.6149009,19.2184657 62.4045717,18.8285745 C62.2593326,19.234712 61.9849869,19.6408488 61.4524422,19.9170213 C62.9371126,21.3791154 65.2125335,22.2563726 67.4556773,22.2563726 L67.4879543,22.2563726 C72.0387923,22.2563726 75.9118419,18.3899466 75.9118419,13.808718 C75.9118419,13.1588982 75.8311554,12.4440971 75.7020548,11.8430139 C75.3792995,11.9567326 74.8467556,12.1191872 74.507864,12.1191872 Z" id="Fill-234" fill="#1A1A1A"></path>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 6.6 KiB |
|
|
@ -0,0 +1,22 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="120px" height="27px" viewBox="0 0 120 27" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<title>编组备份 2</title>
|
||||
<g id="页面-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="编组备份-2">
|
||||
<path d="M79.8815982,19.2506193 L76.6540538,6.04303586 L79.5588436,6.04303586 C80.8175862,6.04303586 81.8826748,6.93653709 82.1247405,8.18744003 L83.8837514,17.431125 L86.17531,8.26866844 C87.0467477,4.80837921 91.9203387,4.8083777 92.7917771,8.2524214 L95.1317468,17.4148787 L97.2296507,8.07372281 C97.5039912,6.87155572 98.5529421,6.02679033 99.7794076,6.02679033 L102.47441,6.02679033 L99.1016223,19.088164 C98.0688077,23.0683095 92.4367464,23.0033281 91.5007576,19.0069371 L89.4674099,10.3968278 L87.5147391,19.1693924 C86.6110266,23.2307648 80.8821372,23.2957469 79.8815982,19.2506193 Z" id="Fill-223" fill="#FFFFFF"></path>
|
||||
<path d="M28.7088751,1.42965135 L30.9520204,1.42965135 L30.9520204,19.2347098 C30.9520204,20.6805583 29.7739674,21.8664799 28.3377095,21.8664799 L26.094565,21.8664799 L26.094565,4.06141997 C26.094565,2.61557216 27.2564801,1.42965135 28.7088751,1.42965135 Z" id="Fill-224" fill="#FFFFFF"></path>
|
||||
<path d="M55.2394116,1.42965135 L57.4825592,1.42965135 L57.4825592,19.2347098 C57.4825592,20.6805583 56.3045032,21.8664799 54.8682461,21.8664799 L52.6251038,21.8664799 L52.6251038,4.06141997 C52.6251038,2.61557216 53.7870167,1.42965135 55.2394116,1.42965135 Z" id="Fill-225" fill="#FFFFFF"></path>
|
||||
<path d="M47.2511589,14.2795152 L39.5050543,14.2795152 L39.5050543,19.039444 C39.5050543,20.4852926 38.6497559,21.6712126 37.213498,21.6712126 L34.6153241,21.6712126 L34.2925696,4.02860953 C34.2925696,2.58276097 35.4706233,1.39684092 36.9068797,1.39684092 L50.8821458,1.39684092 L50.8821458,3.26507256 C50.8821458,4.71092112 49.7040959,5.89684092 48.2678387,5.89684092 L39.5050543,5.89684092 L39.5050543,10.0069509 L49.865472,10.0069509 L49.865472,11.6639922 C49.865472,13.10984 48.6874161,14.2795152 47.2511589,14.2795152 Z" id="Fill-226" fill="#FFFFFF"></path>
|
||||
<path d="M108.735872,21.8339337 L103.00698,6.09205332 L106.202248,6.09205332 C107.348026,6.09205332 108.348569,6.83934703 108.68746,7.92779464 L111.317907,16.310468 L114.561596,7.55414817 C114.948901,6.53068192 115.917156,5.84837109 117.014521,5.84837109 L120,5.84837109 L112.608925,25.31047 C112.22162,26.3339362 111.253361,27 110.172133,27 L106.557279,27 L108.735872,21.8339337 Z" id="Fill-227" fill="#FFFFFF"></path>
|
||||
<g id="编组-2">
|
||||
<path d="M3.55023513,21.9152089 L5.14786918,18.0812747 C5.64813909,16.8953539 6.58412635,15.9368698 7.76218006,15.4332606 C11.5061321,13.8249567 19.2522381,10.2834398 19.8170591,9.7960746 C20.5755317,9.14625484 21.5437938,7.2780232 18.6712802,6.79065875 C18.6712802,6.79065875 21.7213106,6.82314982 22.0440659,9.08127346 C22.43137,11.9079885 21.4469679,14.0036568 20.4625681,14.5397578 C19.687957,14.9783864 4.88966572,21.931456 4.88966572,21.931456 L3.55023513,21.9152089 Z" id="Fill-228" fill="#00B8A9"></path>
|
||||
<path d="M2.51748505,21.8339382 L4.16353238,17.72383 C4.69607707,16.4566815 6.0355084,15.2545159 7.29425018,14.7184141 C7.34266422,14.7021686 7.47176557,14.6534328 7.52017811,14.6209417 L11.2964057,5.8808682 C11.5707469,5.14982002 12.5874237,5.14982002 12.8456272,5.8808682 L15.0564962,11.0469337 C17.2189505,10.0722041 18.6552077,9.4061388 19.1877523,9.11371998 L16.4766141,3.03790767 C15.7988303,1.20216634 14.0720942,0 12.1517056,0 C10.2151793,0 8.50458016,1.20216634 7.82679561,3.02166063 L0,21.9476592 L2.46907176,21.9639032 C2.48520952,21.9314122 2.50134728,21.8664293 2.51748505,21.8339382 Z" id="Fill-229" fill="#0066FF"></path>
|
||||
<path d="M21.3664381,14.9619371 C21.1889227,15.1406371 21.0114103,15.2868462 20.8338949,15.3843194 C20.607967,15.5142836 19.1555713,16.21284 17.2351828,17.1225875 L17.7677267,18.7471362 C18.4132357,20.6641044 20.2045244,21.9474984 22.2056018,21.9474984 L23.9323372,21.9474984 L21.3664381,14.9619371 Z" id="Fill-230" fill="#0066FF"></path>
|
||||
</g>
|
||||
<path d="M74.507936,11.4045234 C75.5328869,11.4045234 76.3637735,10.5680888 76.3637735,9.53629256 C76.3637735,8.50449706 75.5328869,7.66806168 74.507936,7.66806168 C73.4829845,7.66806168 72.6520978,8.50449706 72.6520978,9.53629256 C72.6520978,10.5680888 73.4829845,11.4045234 74.507936,11.4045234 Z" id="Fill-231" fill="#FFFFFF"></path>
|
||||
<path d="M60.2098434,19.6246138 C61.1278434,19.6246138 61.872027,18.8754575 61.872027,17.9513278 C61.872027,17.0271974 61.1278434,16.2780418 60.2098434,16.2780418 C59.2918442,16.2780418 58.5476553,17.0271974 58.5476553,17.9513278 C58.5476553,18.8754575 59.2918442,19.6246138 60.2098434,19.6246138 Z" id="Fill-232" fill="#FFFFFF"></path>
|
||||
<path d="M65.9387336,8.77233806 C67.9236739,7.83009948 70.4572937,8.13876383 72.1194788,8.6911104 C72.2647187,8.15501012 72.7649901,7.47269929 73.3782244,7.21277153 C71.9096909,5.84814988 69.6342723,4.80843885 67.4718173,4.80843885 L67.4556788,4.80843885 C62.7273262,4.75970226 58.7897203,8.99977476 58.7897203,13.5810034 C58.7897203,14.377033 58.9026884,15.1568155 59.0963408,15.8716174 C59.3868198,15.7254068 59.7579838,15.530462 60.4034928,15.530462 C61.6138213,15.5467075 62.5820848,16.4402095 62.5820848,17.6261303 C62.5820848,17.7723401 62.5820878,17.853567 62.5498123,17.9997768 C64.5347519,18.4871412 66.5358271,18.243459 66.5358271,18.243459 C61.8074745,17.3174667 61.0812796,11.0467072 65.9387336,8.77233806 Z" id="Fill-233" fill="#FFFFFF"></path>
|
||||
<path d="M74.507864,12.1191872 C73.1522957,12.1191872 72.0387893,11.0469843 72.0387893,9.68236343 C72.0387893,9.58489099 72.0387893,9.55239993 72.0387893,9.45492674 C70.1506755,8.87008985 68.6660058,8.93507123 68.6660058,8.93507123 C70.7639097,9.47117227 71.5223824,11.5505943 71.7160348,12.6227971 C71.7321725,12.7202696 71.7483133,12.8339891 71.7644503,12.9639533 C71.7967266,13.2076355 71.8290014,13.4675633 71.8290014,13.7274903 C71.8290014,14.2798376 71.7321733,14.8159379 71.5707964,15.3195486 C71.102802,16.9278518 70.1022622,18.3899459 66.713341,19.0072746 C65.0027418,19.3159389 63.6149009,19.2184657 62.4045717,18.8285745 C62.2593326,19.234712 61.9849869,19.6408488 61.4524422,19.9170213 C62.9371126,21.3791154 65.2125335,22.2563726 67.4556773,22.2563726 L67.4879543,22.2563726 C72.0387923,22.2563726 75.9118419,18.3899466 75.9118419,13.808718 C75.9118419,13.1588982 75.8311554,12.4440971 75.7020548,11.8430139 C75.3792995,11.9567326 74.8467556,12.1191872 74.507864,12.1191872 Z" id="Fill-234" fill="#FFFFFF"></path>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 6.6 KiB |
|
|
@ -0,0 +1,11 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="23.9323372px" height="21.9639032px" viewBox="0 0 23.9323372 21.9639032" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<title>编组 13备份</title>
|
||||
<g id="页面-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="编组-13备份">
|
||||
<path d="M3.55023513,21.9152089 L5.14786918,18.0812747 C5.64813909,16.8953539 6.58412635,15.9368698 7.76218006,15.4332606 C11.5061321,13.8249567 19.2522381,10.2834398 19.8170591,9.7960746 C20.5755317,9.14625484 21.5437938,7.2780232 18.6712802,6.79065875 C18.6712802,6.79065875 21.7213106,6.82314982 22.0440659,9.08127346 C22.43137,11.9079885 21.4469679,14.0036568 20.4625681,14.5397578 C19.687957,14.9783864 4.88966572,21.931456 4.88966572,21.931456 L3.55023513,21.9152089 Z" id="Fill-228" fill="#00B8A9"></path>
|
||||
<path d="M2.51748505,21.8339382 L4.16353238,17.72383 C4.69607707,16.4566815 6.0355084,15.2545159 7.29425018,14.7184141 C7.34266422,14.7021686 7.47176557,14.6534328 7.52017811,14.6209417 L11.2964057,5.8808682 C11.5707469,5.14982002 12.5874237,5.14982002 12.8456272,5.8808682 L15.0564962,11.0469337 C17.2189505,10.0722041 18.6552077,9.4061388 19.1877523,9.11371998 L16.4766141,3.03790767 C15.7988303,1.20216634 14.0720942,0 12.1517056,0 C10.2151793,0 8.50458016,1.20216634 7.82679561,3.02166063 L0,21.9476592 L2.46907176,21.9639032 C2.48520952,21.9314122 2.50134728,21.8664293 2.51748505,21.8339382 Z" id="Fill-229" fill="#0066FF"></path>
|
||||
<path d="M21.3664381,14.9619371 C21.1889227,15.1406371 21.0114103,15.2868462 20.8338949,15.3843194 C20.607967,15.5142836 19.1555713,16.21284 17.2351828,17.1225875 L17.7677267,18.7471362 C18.4132357,20.6641044 20.2045244,21.9474984 22.2056018,21.9474984 L23.9323372,21.9474984 L21.3664381,14.9619371 Z" id="Fill-230" fill="#0066FF"></path>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 988 KiB |
|
|
@ -0,0 +1,7 @@
|
|||
#tianai-captcha-parent{box-shadow:0 0 11px 0 #999;width:318px;height:318px;overflow:hidden;position:relative;z-index:997;box-sizing:border-box;border-radius:5px;padding:8px}#tianai-captcha-parent #tianai-captcha-box{height:260px;width:100%;position:relative;overflow:hidden}#tianai-captcha-parent #tianai-captcha-box .loading{width:120px;height:20px;-webkit-mask:linear-gradient(90deg, #000 70%, rgba(0, 0, 0, 0) 0) 0/20%;background:linear-gradient(#f7b645 0 0) 0/0% no-repeat rgba(221,221,221,.4196078431);animation:cartoon 1s infinite steps(6);margin:120px auto}@keyframes cartoon{100%{background-size:120%}}#tianai-captcha-parent #tianai-captcha-box #tianai-captcha{transform-style:preserve-3d;will-change:transform;transition-duration:.45s;transform:translateX(-300px)}#tianai-captcha-parent #tianai-captcha-bg-img{background-color:#fff;background-position:top;background-size:cover;z-index:-1;width:100%;height:100%;top:0;left:0;position:absolute;border-radius:6px}#tianai-captcha-parent .slider-bottom{height:19px;width:100%}#tianai-captcha-parent .slider-bottom .close-btn{width:20px;height:20px;background-image:url(../images/icon.png);background-repeat:no-repeat;background-position:0 -14px;float:right;margin-right:2px;cursor:pointer}#tianai-captcha-parent .slider-bottom .refresh-btn{width:20px;height:20px;background-image:url(../images/icon.png);background-position:0 -167px;background-repeat:no-repeat;float:right;margin-right:10px;cursor:pointer}#tianai-captcha-parent .slider-bottom .logo{height:30px;float:left}#tianai-captcha-parent .slider-move-shadow{animation:myanimation 2s infinite;height:100%;width:5px;background-color:#fff;position:absolute;top:0;left:0;filter:opacity(0.5);box-shadow:1px 1px 1px #fff;border-radius:50%}#tianai-captcha-parent #tianai-captcha-slider-move-track-mask{border-width:1px;border-style:solid;border-color:#00f4ab;width:0;height:32px;background-color:#a9ffe5;opacity:.5;position:absolute;top:-1px;left:-1px;border-radius:5px}
|
||||
#tianai-captcha{text-align:left;box-sizing:content-box;width:300px;height:260px;z-index:999}#tianai-captcha .slider-bottom .logo{height:30px}#tianai-captcha .slider-bottom{height:19px;width:100%}#tianai-captcha .content .tianai-captcha-tips{height:25px;width:100%;position:absolute;bottom:-25px;left:0;z-index:999;font-size:15px;line-height:25px;color:#fff;text-align:center;transition:bottom .3s ease-in-out}#tianai-captcha .content .tianai-captcha-tips.tianai-captcha-tips-error{background-color:#ff5d39}#tianai-captcha .content .tianai-captcha-tips.tianai-captcha-tips-success{background-color:#39c522}#tianai-captcha .content .tianai-captcha-tips.tianai-captcha-tips-on{bottom:0}#tianai-captcha .content #tianai-captcha-loading{z-index:9999;background-color:#f5f5f5;text-align:center;height:100%;overflow:hidden;position:relative;display:flex;justify-content:center;align-items:center}#tianai-captcha .content #tianai-captcha-loading img{display:block;width:45px;height:45px}#tianai-captcha #tianai-captcha-slider-bg-canvas{position:absolute;left:0;top:0;width:100%;height:100%;border-radius:5px}#tianai-captcha #tianai-captcha-slider-bg-div{position:absolute;left:0;top:0;width:100%;height:100%;border-radius:5px}#tianai-captcha #tianai-captcha-slider-bg-div .tianai-captcha-slider-bg-div-slice{position:absolute}@keyframes myanimation{from{left:0}to{left:289px}}
|
||||
#tianai-captcha.tianai-captcha-slider{z-index:999;position:absolute;left:0;top:0;user-select:none}#tianai-captcha.tianai-captcha-slider .content{width:100%;height:180px;position:relative;overflow:hidden}#tianai-captcha.tianai-captcha-slider .bg-img-div{width:100%;height:100%;position:absolute;transform:translate(0px, 0px)}#tianai-captcha.tianai-captcha-slider .bg-img-div img{height:100%;width:100%;border-radius:5px}#tianai-captcha.tianai-captcha-slider .slider-img-div{height:100%;position:absolute;left:0;transform:translate(0px, 0px)}#tianai-captcha.tianai-captcha-slider .slider-img-div #tianai-captcha-slider-move-img{height:100%}#tianai-captcha.tianai-captcha-slider .slider-move{height:34px;width:100%;margin:11px 0;position:relative}#tianai-captcha.tianai-captcha-slider .slider-move-track{position:relative;height:32px;line-height:32px;text-align:center;background:#f5f5f5;color:#999;transition:0s;font-size:14px;box-sizing:content-box;border:1px solid #f5f5f5;border-radius:4px}#tianai-captcha.tianai-captcha-slider .refresh-btn,#tianai-captcha.tianai-captcha-slider .close-btn{display:inline-block}#tianai-captcha.tianai-captcha-slider .slider-move{line-height:38px;font-size:14px;text-align:center;white-space:nowrap;color:#88949d;-moz-user-select:none;-webkit-user-select:none;user-select:none;filter:opacity(0.8)}#tianai-captcha.tianai-captcha-slider .slider-move .slider-move-btn{transform:translate(0px, 0px);position:absolute;top:-6px;left:0;width:63px;height:45px;background-color:#fff;background-repeat:no-repeat;background-size:contain;border-radius:5px}#tianai-captcha.tianai-captcha-slider .slider-tip{margin-bottom:5px;font-weight:bold;font-size:15px;line-height:normal;color:#000}#tianai-captcha.tianai-captcha-slider .slider-move-btn:hover{cursor:pointer}
|
||||
#tianai-captcha.tianai-captcha-rotate .rotate-img-div{height:100%;text-align:center}#tianai-captcha.tianai-captcha-rotate .rotate-img-div img{height:100%;transform:rotate(0deg);display:inline-block}
|
||||
#tianai-captcha.tianai-captcha-concat .tianai-captcha-slider-concat-img-div{background-size:100% 180px;position:absolute;transform:translate(0px, 0px);z-index:1;width:100%}#tianai-captcha.tianai-captcha-concat .tianai-captcha-slider-concat-bg-img{width:100%;height:100%;position:absolute;transform:translate(0px, 0px);background-size:100% 180px}
|
||||
#tianai-captcha.tianai-captcha-disable{z-index:999;position:absolute;left:0;top:0}#tianai-captcha.tianai-captcha-disable .content{width:100%;height:180px;position:relative;overflow:hidden}#tianai-captcha.tianai-captcha-disable .content .bg-img-div{background-image:url(../images/dun.jpeg);width:100%;height:100%;overflow:hidden}#tianai-captcha.tianai-captcha-disable .content .bg-img-div #content-span{color:#fff;overflow:hidden;margin-top:132px;display:block;text-align:center}
|
||||
#tianai-captcha.tianai-captcha-word-click{box-sizing:border-box}#tianai-captcha.tianai-captcha-word-click .click-tip{position:relative;height:40px;width:100%}#tianai-captcha.tianai-captcha-word-click .click-tip .tip-img{height:35px;position:absolute;right:15px}#tianai-captcha.tianai-captcha-word-click .click-tip #tianai-captcha-click-track-font{font-size:18px;display:inline-block;height:40px;line-height:40px;position:absolute}#tianai-captcha.tianai-captcha-word-click .slider-bottom{position:relative;top:6px}#tianai-captcha.tianai-captcha-word-click .content #bg-img-click-mask{width:100%;height:100%;position:absolute;left:0;top:0}#tianai-captcha.tianai-captcha-word-click .content #bg-img-click-mask .click-span{position:absolute;left:0;top:0;border-radius:50px;background-color:#409eff;width:20px;height:20px;text-align:center;line-height:20px;color:#fff;border:2px solid #fff;box-sizing:content-box}#tianai-captcha.tianai-captcha-word-click .click-confirm-btn{width:100%;height:35px;border-radius:4px;background-image:linear-gradient(173deg, hsl(38.09, 91%, 57.89%) 0%, hsl(38.09, 89.38%, 71.74%) 100%);font-size:15px;text-align:center;box-sizing:border-box;line-height:35px;color:#fff;margin-top:3px}#tianai-captcha.tianai-captcha-word-click .click-confirm-btn:hover{cursor:pointer}
|
||||
|
After Width: | Height: | Size: 8.1 KiB |
|
After Width: | Height: | Size: 3.0 KiB |
|
|
@ -0,0 +1,331 @@
|
|||
/**
|
||||
* 通用组件共同的使用的基础组件,原先放在 adapter/form 内部,限制了使用范围,这里提取出来,方便其他地方使用
|
||||
* 可用于 aiflowy-form、aiflowy-modal、aiflowy-drawer 等组件使用,
|
||||
*/
|
||||
|
||||
import type { Component } from 'vue';
|
||||
|
||||
import type { BaseFormComponentType } from '@aiflowy/common-ui';
|
||||
import type { Recordable } from '@aiflowy/types';
|
||||
|
||||
import { defineAsyncComponent, defineComponent, h, ref } from 'vue';
|
||||
|
||||
import { ApiComponent, globalShareState, IconPicker } from '@aiflowy/common-ui';
|
||||
import { $t } from '@aiflowy/locales';
|
||||
|
||||
import { ElNotification } from 'element-plus';
|
||||
|
||||
const ElButton = defineAsyncComponent(() =>
|
||||
Promise.all([
|
||||
import('element-plus/es/components/button/index'),
|
||||
import('element-plus/es/components/button/style/css'),
|
||||
]).then(([res]) => res.ElButton),
|
||||
);
|
||||
const ElCheckbox = defineAsyncComponent(() =>
|
||||
Promise.all([
|
||||
import('element-plus/es/components/checkbox/index'),
|
||||
import('element-plus/es/components/checkbox/style/css'),
|
||||
]).then(([res]) => res.ElCheckbox),
|
||||
);
|
||||
const ElCheckboxButton = defineAsyncComponent(() =>
|
||||
Promise.all([
|
||||
import('element-plus/es/components/checkbox/index'),
|
||||
import('element-plus/es/components/checkbox-button/style/css'),
|
||||
]).then(([res]) => res.ElCheckboxButton),
|
||||
);
|
||||
const ElCheckboxGroup = defineAsyncComponent(() =>
|
||||
Promise.all([
|
||||
import('element-plus/es/components/checkbox/index'),
|
||||
import('element-plus/es/components/checkbox-group/style/css'),
|
||||
]).then(([res]) => res.ElCheckboxGroup),
|
||||
);
|
||||
const ElDatePicker = defineAsyncComponent(() =>
|
||||
Promise.all([
|
||||
import('element-plus/es/components/date-picker/index'),
|
||||
import('element-plus/es/components/date-picker/style/css'),
|
||||
]).then(([res]) => res.ElDatePicker),
|
||||
);
|
||||
const ElDivider = defineAsyncComponent(() =>
|
||||
Promise.all([
|
||||
import('element-plus/es/components/divider/index'),
|
||||
import('element-plus/es/components/divider/style/css'),
|
||||
]).then(([res]) => res.ElDivider),
|
||||
);
|
||||
const ElInput = defineAsyncComponent(() =>
|
||||
Promise.all([
|
||||
import('element-plus/es/components/input/index'),
|
||||
import('element-plus/es/components/input/style/css'),
|
||||
]).then(([res]) => res.ElInput),
|
||||
);
|
||||
const ElInputNumber = defineAsyncComponent(() =>
|
||||
Promise.all([
|
||||
import('element-plus/es/components/input-number/index'),
|
||||
import('element-plus/es/components/input-number/style/css'),
|
||||
]).then(([res]) => res.ElInputNumber),
|
||||
);
|
||||
const ElRadio = defineAsyncComponent(() =>
|
||||
Promise.all([
|
||||
import('element-plus/es/components/radio/index'),
|
||||
import('element-plus/es/components/radio/style/css'),
|
||||
]).then(([res]) => res.ElRadio),
|
||||
);
|
||||
const ElRadioButton = defineAsyncComponent(() =>
|
||||
Promise.all([
|
||||
import('element-plus/es/components/radio/index'),
|
||||
import('element-plus/es/components/radio-button/style/css'),
|
||||
]).then(([res]) => res.ElRadioButton),
|
||||
);
|
||||
const ElRadioGroup = defineAsyncComponent(() =>
|
||||
Promise.all([
|
||||
import('element-plus/es/components/radio/index'),
|
||||
import('element-plus/es/components/radio-group/style/css'),
|
||||
]).then(([res]) => res.ElRadioGroup),
|
||||
);
|
||||
const ElSelectV2 = defineAsyncComponent(() =>
|
||||
Promise.all([
|
||||
import('element-plus/es/components/select-v2/index'),
|
||||
import('element-plus/es/components/select-v2/style/css'),
|
||||
]).then(([res]) => res.ElSelectV2),
|
||||
);
|
||||
const ElSpace = defineAsyncComponent(() =>
|
||||
Promise.all([
|
||||
import('element-plus/es/components/space/index'),
|
||||
import('element-plus/es/components/space/style/css'),
|
||||
]).then(([res]) => res.ElSpace),
|
||||
);
|
||||
const ElSwitch = defineAsyncComponent(() =>
|
||||
Promise.all([
|
||||
import('element-plus/es/components/switch/index'),
|
||||
import('element-plus/es/components/switch/style/css'),
|
||||
]).then(([res]) => res.ElSwitch),
|
||||
);
|
||||
const ElTimePicker = defineAsyncComponent(() =>
|
||||
Promise.all([
|
||||
import('element-plus/es/components/time-picker/index'),
|
||||
import('element-plus/es/components/time-picker/style/css'),
|
||||
]).then(([res]) => res.ElTimePicker),
|
||||
);
|
||||
const ElTreeSelect = defineAsyncComponent(() =>
|
||||
Promise.all([
|
||||
import('element-plus/es/components/tree-select/index'),
|
||||
import('element-plus/es/components/tree-select/style/css'),
|
||||
]).then(([res]) => res.ElTreeSelect),
|
||||
);
|
||||
const ElUpload = defineAsyncComponent(() =>
|
||||
Promise.all([
|
||||
import('element-plus/es/components/upload/index'),
|
||||
import('element-plus/es/components/upload/style/css'),
|
||||
]).then(([res]) => res.ElUpload),
|
||||
);
|
||||
|
||||
const withDefaultPlaceholder = <T extends Component>(
|
||||
component: T,
|
||||
type: 'input' | 'select',
|
||||
componentProps: Recordable<any> = {},
|
||||
) => {
|
||||
return defineComponent({
|
||||
name: component.name,
|
||||
inheritAttrs: false,
|
||||
setup: (props: any, { attrs, expose, slots }) => {
|
||||
const placeholder =
|
||||
props?.placeholder ||
|
||||
attrs?.placeholder ||
|
||||
$t(`ui.placeholder.${type}`);
|
||||
// 透传组件暴露的方法
|
||||
const innerRef = ref();
|
||||
expose(
|
||||
new Proxy(
|
||||
{},
|
||||
{
|
||||
get: (_target, key) => innerRef.value?.[key],
|
||||
has: (_target, key) => key in (innerRef.value || {}),
|
||||
},
|
||||
),
|
||||
);
|
||||
return () =>
|
||||
h(
|
||||
component,
|
||||
{ ...componentProps, placeholder, ...props, ...attrs, ref: innerRef },
|
||||
slots,
|
||||
);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// 这里需要自行根据业务组件库进行适配,需要用到的组件都需要在这里类型说明
|
||||
export type ComponentType =
|
||||
| 'ApiSelect'
|
||||
| 'ApiTreeSelect'
|
||||
| 'Checkbox'
|
||||
| 'CheckboxGroup'
|
||||
| 'DatePicker'
|
||||
| 'Divider'
|
||||
| 'IconPicker'
|
||||
| 'Input'
|
||||
| 'InputNumber'
|
||||
| 'RadioGroup'
|
||||
| 'Select'
|
||||
| 'Space'
|
||||
| 'Switch'
|
||||
| 'TimePicker'
|
||||
| 'TreeSelect'
|
||||
| 'Upload'
|
||||
| BaseFormComponentType;
|
||||
|
||||
async function initComponentAdapter() {
|
||||
const components: Partial<Record<ComponentType, Component>> = {
|
||||
// 如果你的组件体积比较大,可以使用异步加载
|
||||
// Button: () =>
|
||||
// import('xxx').then((res) => res.Button),
|
||||
ApiSelect: withDefaultPlaceholder(
|
||||
{
|
||||
...ApiComponent,
|
||||
name: 'ApiSelect',
|
||||
},
|
||||
'select',
|
||||
{
|
||||
component: ElSelectV2,
|
||||
loadingSlot: 'loading',
|
||||
visibleEvent: 'onVisibleChange',
|
||||
},
|
||||
),
|
||||
ApiTreeSelect: withDefaultPlaceholder(
|
||||
{
|
||||
...ApiComponent,
|
||||
name: 'ApiTreeSelect',
|
||||
},
|
||||
'select',
|
||||
{
|
||||
component: ElTreeSelect,
|
||||
props: { label: 'label', children: 'children' },
|
||||
nodeKey: 'value',
|
||||
loadingSlot: 'loading',
|
||||
optionsPropName: 'data',
|
||||
visibleEvent: 'onVisibleChange',
|
||||
},
|
||||
),
|
||||
Checkbox: ElCheckbox,
|
||||
CheckboxGroup: (props, { attrs, slots }) => {
|
||||
let defaultSlot;
|
||||
if (Reflect.has(slots, 'default')) {
|
||||
defaultSlot = slots.default;
|
||||
} else {
|
||||
const { options, isButton } = attrs;
|
||||
if (Array.isArray(options)) {
|
||||
defaultSlot = () =>
|
||||
options.map((option) =>
|
||||
h(isButton ? ElCheckboxButton : ElCheckbox, option),
|
||||
);
|
||||
}
|
||||
}
|
||||
return h(
|
||||
ElCheckboxGroup,
|
||||
{ ...props, ...attrs },
|
||||
{ ...slots, default: defaultSlot },
|
||||
);
|
||||
},
|
||||
// 自定义默认按钮
|
||||
DefaultButton: (props, { attrs, slots }) => {
|
||||
return h(ElButton, { ...props, attrs, type: 'info' }, slots);
|
||||
},
|
||||
// 自定义主要按钮
|
||||
PrimaryButton: (props, { attrs, slots }) => {
|
||||
return h(ElButton, { ...props, attrs, type: 'primary' }, slots);
|
||||
},
|
||||
Divider: ElDivider,
|
||||
IconPicker: withDefaultPlaceholder(IconPicker, 'select', {
|
||||
iconSlot: 'append',
|
||||
modelValueProp: 'model-value',
|
||||
inputComponent: ElInput,
|
||||
}),
|
||||
Input: withDefaultPlaceholder(ElInput, 'input'),
|
||||
InputNumber: withDefaultPlaceholder(ElInputNumber, 'input'),
|
||||
RadioGroup: (props, { attrs, slots }) => {
|
||||
let defaultSlot;
|
||||
if (Reflect.has(slots, 'default')) {
|
||||
defaultSlot = slots.default;
|
||||
} else {
|
||||
const { options } = attrs;
|
||||
if (Array.isArray(options)) {
|
||||
defaultSlot = () =>
|
||||
options.map((option) =>
|
||||
h(attrs.isButton ? ElRadioButton : ElRadio, option),
|
||||
);
|
||||
}
|
||||
}
|
||||
return h(
|
||||
ElRadioGroup,
|
||||
{ ...props, ...attrs },
|
||||
{ ...slots, default: defaultSlot },
|
||||
);
|
||||
},
|
||||
Select: (props, { attrs, slots }) => {
|
||||
return h(ElSelectV2, { ...props, attrs }, slots);
|
||||
},
|
||||
Space: ElSpace,
|
||||
Switch: ElSwitch,
|
||||
TimePicker: (props, { attrs, slots }) => {
|
||||
const { name, id, isRange } = props;
|
||||
const extraProps: Recordable<any> = {};
|
||||
if (isRange) {
|
||||
if (name && !Array.isArray(name)) {
|
||||
extraProps.name = [name, `${name}_end`];
|
||||
}
|
||||
if (id && !Array.isArray(id)) {
|
||||
extraProps.id = [id, `${id}_end`];
|
||||
}
|
||||
}
|
||||
return h(
|
||||
ElTimePicker,
|
||||
{
|
||||
...props,
|
||||
...attrs,
|
||||
...extraProps,
|
||||
},
|
||||
slots,
|
||||
);
|
||||
},
|
||||
DatePicker: (props, { attrs, slots }) => {
|
||||
const { name, id, type } = props;
|
||||
const extraProps: Recordable<any> = {};
|
||||
if (type && type.includes('range')) {
|
||||
if (name && !Array.isArray(name)) {
|
||||
extraProps.name = [name, `${name}_end`];
|
||||
}
|
||||
if (id && !Array.isArray(id)) {
|
||||
extraProps.id = [id, `${id}_end`];
|
||||
}
|
||||
}
|
||||
return h(
|
||||
ElDatePicker,
|
||||
{
|
||||
...props,
|
||||
...attrs,
|
||||
...extraProps,
|
||||
},
|
||||
slots,
|
||||
);
|
||||
},
|
||||
TreeSelect: withDefaultPlaceholder(ElTreeSelect, 'select'),
|
||||
Upload: ElUpload,
|
||||
};
|
||||
|
||||
// 将组件注册到全局共享状态中
|
||||
globalShareState.setComponents(components);
|
||||
|
||||
// 定义全局共享状态中的消息提示
|
||||
globalShareState.defineMessage({
|
||||
// 复制成功消息提示
|
||||
copyPreferencesSuccess: (title, content) => {
|
||||
ElNotification({
|
||||
title,
|
||||
message: content,
|
||||
position: 'bottom-right',
|
||||
duration: 0,
|
||||
type: 'success',
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export { initComponentAdapter };
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
import type {
|
||||
AIFlowyFormSchema as FormSchema,
|
||||
AIFlowyFormProps,
|
||||
} from '@aiflowy/common-ui';
|
||||
|
||||
import type { ComponentType } from './component';
|
||||
|
||||
import { setupAIFlowyForm, useAIFlowyForm as useForm, z } from '@aiflowy/common-ui';
|
||||
import { $t } from '@aiflowy/locales';
|
||||
|
||||
async function initSetupAIFlowyForm() {
|
||||
setupAIFlowyForm<ComponentType>({
|
||||
config: {
|
||||
modelPropNameMap: {
|
||||
Upload: 'fileList',
|
||||
CheckboxGroup: 'model-value',
|
||||
},
|
||||
},
|
||||
defineRules: {
|
||||
required: (value, _params, ctx) => {
|
||||
if (value === undefined || value === null || value.length === 0) {
|
||||
return $t('ui.formRules.required', [ctx.label]);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
selectRequired: (value, _params, ctx) => {
|
||||
if (value === undefined || value === null) {
|
||||
return $t('ui.formRules.selectRequired', [ctx.label]);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const useAIFlowyForm = useForm<ComponentType>;
|
||||
|
||||
export { initSetupAIFlowyForm, useAIFlowyForm, z };
|
||||
|
||||
export type AIFlowyFormSchema = FormSchema<ComponentType>;
|
||||
export type { AIFlowyFormProps };
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
import type { VxeTableGridOptions } from '@aiflowy/plugins/vxe-table';
|
||||
|
||||
import { h } from 'vue';
|
||||
|
||||
import { setupAIFlowyVxeTable, useAIFlowyVxeGrid } from '@aiflowy/plugins/vxe-table';
|
||||
|
||||
import { ElButton, ElImage } from 'element-plus';
|
||||
|
||||
import { useAIFlowyForm } from './form';
|
||||
|
||||
setupAIFlowyVxeTable({
|
||||
configVxeTable: (vxeUI) => {
|
||||
vxeUI.setConfig({
|
||||
grid: {
|
||||
align: 'center',
|
||||
border: false,
|
||||
columnConfig: {
|
||||
resizable: true,
|
||||
},
|
||||
minHeight: 180,
|
||||
formConfig: {
|
||||
// 全局禁用vxe-table的表单配置,使用formOptions
|
||||
enabled: false,
|
||||
},
|
||||
proxyConfig: {
|
||||
autoLoad: true,
|
||||
response: {
|
||||
result: 'items',
|
||||
total: 'total',
|
||||
list: 'items',
|
||||
},
|
||||
showActiveMsg: true,
|
||||
showResponseMsg: false,
|
||||
},
|
||||
round: true,
|
||||
showOverflow: true,
|
||||
size: 'small',
|
||||
} as VxeTableGridOptions,
|
||||
});
|
||||
|
||||
// 表格配置项可以用 cellRender: { name: 'CellImage' },
|
||||
vxeUI.renderer.add('CellImage', {
|
||||
renderTableDefault(_renderOpts, params) {
|
||||
const { column, row } = params;
|
||||
const src = row[column.field];
|
||||
return h(ElImage, { src, previewSrcList: [src] });
|
||||
},
|
||||
});
|
||||
|
||||
// 表格配置项可以用 cellRender: { name: 'CellLink' },
|
||||
vxeUI.renderer.add('CellLink', {
|
||||
renderTableDefault(renderOpts) {
|
||||
const { props } = renderOpts;
|
||||
return h(
|
||||
ElButton,
|
||||
{ size: 'small', link: true },
|
||||
{ default: () => props?.text },
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
// 这里可以自行扩展 vxe-table 的全局配置,比如自定义格式化
|
||||
// vxeUI.formats.add
|
||||
},
|
||||
useAIFlowyForm,
|
||||
});
|
||||
|
||||
export { useAIFlowyVxeGrid };
|
||||
|
||||
export type * from '@aiflowy/plugins/vxe-table';
|
||||
|
|
@ -0,0 +1,138 @@
|
|||
import type {
|
||||
AiLlm,
|
||||
BotInfo,
|
||||
ChatMessage,
|
||||
RequestResult,
|
||||
Session,
|
||||
} from '@aiflowy/types';
|
||||
|
||||
import { api } from '#/api/request.js';
|
||||
|
||||
/** 获取bot详情 */
|
||||
export const getBotDetails = (id: string) => {
|
||||
return api.get<RequestResult<BotInfo>>('/api/v1/bot/getDetail', {
|
||||
params: { id },
|
||||
});
|
||||
};
|
||||
|
||||
export interface GetSessionListParams {
|
||||
botId: string;
|
||||
tempUserId: string;
|
||||
}
|
||||
/** 获取bot对话列表 */
|
||||
export const getSessionList = (params: GetSessionListParams) => {
|
||||
return api.get<RequestResult<{ cons: Session[] }>>(
|
||||
'/api/v1/conversation/externalList',
|
||||
{ params },
|
||||
);
|
||||
};
|
||||
|
||||
export interface SaveBotParams {
|
||||
icon: string;
|
||||
title: string;
|
||||
alias: string;
|
||||
description: string;
|
||||
categoryId: any;
|
||||
status: number;
|
||||
}
|
||||
/** 创建Bot */
|
||||
export const saveBot = (params: SaveBotParams) => {
|
||||
return api.post<RequestResult>('/api/v1/bot/save', { ...params });
|
||||
};
|
||||
|
||||
export interface UpdateBotParams extends SaveBotParams {
|
||||
id: string;
|
||||
}
|
||||
/** 修改Bot */
|
||||
export const updateBotApi = (params: UpdateBotParams) => {
|
||||
return api.post<RequestResult>('/api/v1/bot/update', { ...params });
|
||||
};
|
||||
|
||||
/** 删除Bot */
|
||||
export const removeBotFromId = (id: string) => {
|
||||
return api.post<RequestResult>('/api/v1/bot/update', { id });
|
||||
};
|
||||
|
||||
export interface GetMessageListParams {
|
||||
conversationId: string;
|
||||
botId: string;
|
||||
tempUserId: string;
|
||||
}
|
||||
/** 获取单个对话的信息列表 */
|
||||
export const getMessageList = (params: GetMessageListParams) => {
|
||||
return api.get<RequestResult<ChatMessage[]>>(
|
||||
'/api/v1/botMessage/messageList',
|
||||
{
|
||||
params,
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
/** 更新Bot的LLM配置 */
|
||||
export interface UpdateLlmOptionsParams {
|
||||
id: string;
|
||||
llmOptions: {
|
||||
[key: string]: any;
|
||||
};
|
||||
}
|
||||
export interface UpdateBotOptionsParams {
|
||||
id: string;
|
||||
options: {
|
||||
[key: string]: any;
|
||||
};
|
||||
}
|
||||
|
||||
export const updateLlmOptions = (params: UpdateLlmOptionsParams) => {
|
||||
return api.post<RequestResult>('/api/v1/bot/updateLlmOptions', {
|
||||
...params,
|
||||
});
|
||||
};
|
||||
|
||||
export const updateBotOptions = (params: UpdateBotOptionsParams) => {
|
||||
return api.post<RequestResult>('/api/v1/bot/updateOptions', {
|
||||
...params,
|
||||
});
|
||||
};
|
||||
|
||||
/** 更新Bot的LLM配置 */
|
||||
export interface GetAiLlmListParams {
|
||||
[key: string]: any;
|
||||
}
|
||||
export const getAiLlmList = (params: GetAiLlmListParams) => {
|
||||
return api.get<RequestResult<AiLlm[]>>('/api/v1/model/list', {
|
||||
params,
|
||||
});
|
||||
};
|
||||
|
||||
/** 更新modelId */
|
||||
export interface UpdateLlmIdParams {
|
||||
id: string;
|
||||
modelId: string;
|
||||
}
|
||||
export const updateLlmId = (params: UpdateLlmIdParams) => {
|
||||
return api.post<RequestResult>('/api/v1/bot/updateLlmId', {
|
||||
...params,
|
||||
});
|
||||
};
|
||||
|
||||
export const doPostBotPluginTools = (botId: string) => {
|
||||
return api.post<RequestResult<any[]>>('/api/v1/pluginItem/tool/list', {
|
||||
id: botId,
|
||||
});
|
||||
};
|
||||
|
||||
export const getPerQuestions = (presetQuestions: any[]) => {
|
||||
if (!presetQuestions) {
|
||||
return [];
|
||||
}
|
||||
return presetQuestions
|
||||
.filter((item: any) => {
|
||||
return (
|
||||
typeof item.description === 'string' && item.description.trim() !== ''
|
||||
);
|
||||
})
|
||||
.map((item: any) => ({
|
||||
key: item.key,
|
||||
description: item.description,
|
||||
}));
|
||||
};
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
export * from './bot';
|
||||
export * from './llm';
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
import { api } from '#/api/request.js';
|
||||
|
||||
// 获取LLM供应商
|
||||
export async function getLlmProviderList() {
|
||||
return api.get('/api/v1/modelProvider/list');
|
||||
}
|
||||
|
||||
// 保存LLM
|
||||
export async function saveLlm(data: string) {
|
||||
return api.post('/api/v1/model/save', data);
|
||||
}
|
||||
|
||||
// 删除LLM
|
||||
export async function deleteLlm(data: any) {
|
||||
return api.post(`/api/v1/model/remove`, data);
|
||||
}
|
||||
|
||||
// 修改LLM
|
||||
export async function updateLlm(data: any) {
|
||||
return api.post(`/api/v1/model/update`, data);
|
||||
}
|
||||
|
||||
// 一键添加LLM
|
||||
export async function quickAddLlm(data: any) {
|
||||
return api.post(`/api/v1/model/quickAdd`, data);
|
||||
}
|
||||
|
||||
export interface llmType {
|
||||
id: string;
|
||||
title: string;
|
||||
modelProvider: {
|
||||
icon: string;
|
||||
providerName: string;
|
||||
providerType: string;
|
||||
};
|
||||
withUsed: boolean;
|
||||
llmModel: string;
|
||||
icon: string;
|
||||
description: string;
|
||||
modelType: string;
|
||||
groupName: string;
|
||||
added: boolean;
|
||||
aiLlmProvider: any;
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
/**
|
||||
* 格式化文件大小(字节转 B/KB/MB/GB/TB)
|
||||
* @param bytes - 文件大小(单位:字节 Byte)
|
||||
* @param decimalPlaces - 保留小数位数(默认 2 位)
|
||||
* @returns 格式化后的大小字符串(如:1.23 MB、456 B、7.8 GB)
|
||||
*/
|
||||
export function formatFileSize(
|
||||
bytes: number,
|
||||
decimalPlaces: number = 2,
|
||||
): string {
|
||||
// 处理特殊情况:bytes 为 0 或非数字
|
||||
if (Number.isNaN(bytes) || bytes < 0) return '0 B';
|
||||
if (bytes === 0) return '0 B';
|
||||
|
||||
// 单位数组(从 Byte 到 TB)
|
||||
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||
// 计算合适的单位索引(1 KB = 1024 B,每次除以 1024 切换单位)
|
||||
const unitIndex = Math.floor(Math.log(bytes) / Math.log(1024));
|
||||
// 计算对应单位的大小(保留指定小数位)
|
||||
const formattedSize = (bytes / 1024 ** unitIndex).toFixed(decimalPlaces);
|
||||
|
||||
// 移除末尾多余的 .00(如 2.00 MB → 2 MB,1.50 KB → 1.5 KB)
|
||||
const sizeWithoutTrailingZeros = Number.parseFloat(formattedSize).toString();
|
||||
|
||||
// 返回格式化结果(单位与大小拼接)
|
||||
return `${sizeWithoutTrailingZeros} ${units[unitIndex]}`;
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
import { useAccessStore } from '@aiflowy/stores';
|
||||
|
||||
export function hasPermission(codes: string[]) {
|
||||
const accessStore = useAccessStore();
|
||||
const userCodesSet = new Set(accessStore.accessCodes);
|
||||
|
||||
const intersection = codes.filter((item) => userCodesSet.has(item));
|
||||
return intersection.length > 0;
|
||||
}
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
import { baseRequestClient, requestClient } from '#/api/request';
|
||||
|
||||
export namespace AuthApi {
|
||||
/** 登录接口参数 */
|
||||
export interface LoginParams {
|
||||
password?: string;
|
||||
username?: string;
|
||||
}
|
||||
|
||||
/** 登录接口返回值 */
|
||||
export interface LoginResult {
|
||||
accessToken: string;
|
||||
token: string;
|
||||
}
|
||||
|
||||
export interface RefreshTokenResult {
|
||||
data: string;
|
||||
status: number;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 登录
|
||||
*/
|
||||
export async function loginApi(data: AuthApi.LoginParams) {
|
||||
return requestClient.post<AuthApi.LoginResult>('/api/v1/auth/login', data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新accessToken
|
||||
*/
|
||||
export async function refreshTokenApi() {
|
||||
return baseRequestClient.post<AuthApi.RefreshTokenResult>('/auth/refresh', {
|
||||
withCredentials: true,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 退出登录
|
||||
*/
|
||||
export async function logoutApi() {
|
||||
return requestClient.post('/api/v1/auth/logout', {
|
||||
withCredentials: true,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户权限码
|
||||
*/
|
||||
export async function getAccessCodesApi() {
|
||||
return requestClient.get<string[]>('/api/v1/auth/getPermissions');
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
export * from './auth';
|
||||
export * from './menu';
|
||||
export * from './user';
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
import type { RouteRecordStringComponent } from '@aiflowy/types';
|
||||
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
/**
|
||||
* 获取用户所有菜单
|
||||
*/
|
||||
export async function getAllMenusApi() {
|
||||
return requestClient.get<RouteRecordStringComponent[]>(
|
||||
'/api/v1/sysMenu/treeV2',
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
import type { UserInfo } from '@aiflowy/types';
|
||||
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
/**
|
||||
* 获取用户信息
|
||||
*/
|
||||
export async function getUserInfoApi() {
|
||||
return requestClient.get<UserInfo>('/api/v1/sysAccount/myProfile');
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
export * from './ai';
|
||||
export * from './core';
|
||||
|
|
@ -0,0 +1,221 @@
|
|||
import type { ServerSentEventMessage } from 'fetch-event-stream';
|
||||
|
||||
/**
|
||||
* 该文件可自行根据业务逻辑进行调整
|
||||
*/
|
||||
import type { RequestClientOptions } from '@aiflowy/request';
|
||||
|
||||
import { useAppConfig } from '@aiflowy/hooks';
|
||||
import { preferences } from '@aiflowy/preferences';
|
||||
import {
|
||||
authenticateResponseInterceptor,
|
||||
defaultResponseInterceptor,
|
||||
errorMessageResponseInterceptor,
|
||||
RequestClient,
|
||||
} from '@aiflowy/request';
|
||||
import { useAccessStore } from '@aiflowy/stores';
|
||||
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { events } from 'fetch-event-stream';
|
||||
|
||||
import { useAuthStore } from '#/store';
|
||||
|
||||
import { refreshTokenApi } from './core';
|
||||
|
||||
const { apiURL } = useAppConfig(import.meta.env, import.meta.env.PROD);
|
||||
|
||||
function createRequestClient(baseURL: string, options?: RequestClientOptions) {
|
||||
const client = new RequestClient({
|
||||
...options,
|
||||
baseURL,
|
||||
});
|
||||
|
||||
/**
|
||||
* 重新认证逻辑
|
||||
*/
|
||||
async function doReAuthenticate() {
|
||||
console.warn('Access token or refresh token is invalid or expired. ');
|
||||
const accessStore = useAccessStore();
|
||||
const authStore = useAuthStore();
|
||||
accessStore.setAccessToken(null);
|
||||
if (
|
||||
preferences.app.loginExpiredMode === 'modal' &&
|
||||
accessStore.isAccessChecked
|
||||
) {
|
||||
accessStore.setLoginExpired(true);
|
||||
} else {
|
||||
await authStore.logout();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新token逻辑
|
||||
*/
|
||||
async function doRefreshToken() {
|
||||
const accessStore = useAccessStore();
|
||||
const resp = await refreshTokenApi();
|
||||
const newToken = resp.data;
|
||||
accessStore.setAccessToken(newToken);
|
||||
return newToken;
|
||||
}
|
||||
|
||||
function formatToken(token: null | string) {
|
||||
return token ? `${token}` : null;
|
||||
}
|
||||
|
||||
// 请求头处理
|
||||
client.addRequestInterceptor({
|
||||
fulfilled: async (config) => {
|
||||
const accessStore = useAccessStore();
|
||||
|
||||
config.headers['aiflowy-token'] = formatToken(accessStore.accessToken);
|
||||
config.headers['Accept-Language'] = preferences.app.locale;
|
||||
return config;
|
||||
},
|
||||
});
|
||||
|
||||
// 处理返回的响应数据格式
|
||||
client.addResponseInterceptor(
|
||||
defaultResponseInterceptor({
|
||||
codeField: 'errorCode',
|
||||
dataField: 'data',
|
||||
showErrorMessage: (message) => {
|
||||
ElMessage.error(message);
|
||||
},
|
||||
successCode: 0,
|
||||
}),
|
||||
);
|
||||
|
||||
// token过期的处理
|
||||
client.addResponseInterceptor(
|
||||
authenticateResponseInterceptor({
|
||||
client,
|
||||
doReAuthenticate,
|
||||
doRefreshToken,
|
||||
enableRefreshToken: preferences.app.enableRefreshToken,
|
||||
formatToken,
|
||||
}),
|
||||
);
|
||||
|
||||
// 通用的错误处理,如果没有进入上面的错误处理逻辑,就会进入这里
|
||||
client.addResponseInterceptor(
|
||||
errorMessageResponseInterceptor((msg: string, error) => {
|
||||
// 这里可以根据业务进行定制,你可以拿到 error 内的信息进行定制化处理,根据不同的 code 做不同的提示,而不是直接使用 message.error 提示 msg
|
||||
// 当前mock接口返回的错误字段是 error 或者 message
|
||||
const responseData = error?.response?.data ?? {};
|
||||
const errorMessage = responseData?.error ?? responseData?.message ?? '';
|
||||
// 如果没有错误信息,则会根据状态码进行提示
|
||||
ElMessage.error(errorMessage || msg);
|
||||
}),
|
||||
);
|
||||
|
||||
return client;
|
||||
}
|
||||
|
||||
export const requestClient = createRequestClient(apiURL, {
|
||||
responseReturn: 'data',
|
||||
});
|
||||
|
||||
export const api = createRequestClient(apiURL, {
|
||||
responseReturn: 'body',
|
||||
});
|
||||
|
||||
export const baseRequestClient = new RequestClient({ baseURL: apiURL });
|
||||
|
||||
export interface SseOptions {
|
||||
onMessage?: (message: ServerSentEventMessage) => void;
|
||||
onError?: (err: any) => void;
|
||||
onFinished?: () => void;
|
||||
}
|
||||
export class SseClient {
|
||||
private controller: AbortController | null = null;
|
||||
private currentRequestId = 0;
|
||||
|
||||
abort(): void {
|
||||
if (this.controller) {
|
||||
this.controller.abort();
|
||||
this.controller = null;
|
||||
}
|
||||
}
|
||||
|
||||
isActive(): boolean {
|
||||
return this.controller !== null;
|
||||
}
|
||||
|
||||
async post(url: string, data?: any, options?: SseOptions): Promise<void> {
|
||||
// 生成唯一的请求ID
|
||||
const requestId = ++this.currentRequestId;
|
||||
const currentRequestId = requestId;
|
||||
|
||||
// 如果已有请求,先取消
|
||||
this.abort();
|
||||
|
||||
// 创建新的控制器
|
||||
const controller = new AbortController();
|
||||
this.controller = controller;
|
||||
|
||||
// 保存信号的引用到局部变量
|
||||
const signal = controller.signal;
|
||||
|
||||
try {
|
||||
const res = await fetch(apiURL + url, {
|
||||
method: 'POST',
|
||||
signal, // 使用局部变量 signal
|
||||
headers: this.getHeaders(),
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const error = new Error(`HTTP ${res.status}: ${res.statusText}`);
|
||||
options?.onError?.(error);
|
||||
return;
|
||||
}
|
||||
|
||||
// 在开始事件流之前检查是否还是同一个请求
|
||||
if (this.currentRequestId !== currentRequestId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const msgEvents = events(res, signal);
|
||||
|
||||
try {
|
||||
for await (const event of msgEvents) {
|
||||
// 每次迭代都检查是否还是同一个请求
|
||||
if (this.currentRequestId !== currentRequestId) {
|
||||
break;
|
||||
}
|
||||
options?.onMessage?.(event);
|
||||
}
|
||||
} catch (innerError) {
|
||||
options?.onError?.(innerError);
|
||||
}
|
||||
|
||||
// 只有在还是同一个请求的情况下才调用 onFinished
|
||||
if (this.currentRequestId === currentRequestId) {
|
||||
options?.onFinished?.();
|
||||
}
|
||||
} catch (error) {
|
||||
if (this.currentRequestId !== currentRequestId) {
|
||||
return;
|
||||
}
|
||||
console.error('SSE错误:', error);
|
||||
options?.onError?.(error);
|
||||
} finally {
|
||||
// 只有当还是当前请求时才清除 controller
|
||||
if (this.currentRequestId === currentRequestId) {
|
||||
this.controller = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private getHeaders() {
|
||||
const accessStore = useAccessStore();
|
||||
return {
|
||||
Accept: 'text/event-stream',
|
||||
'Content-Type': 'application/json',
|
||||
'aiflowy-token': accessStore.accessToken || '',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export const sseClient = new SseClient();
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
<script lang="ts" setup>
|
||||
import { useElementPlusDesignTokens } from '@aiflowy/hooks';
|
||||
|
||||
import { ElConfigProvider } from 'element-plus';
|
||||
|
||||
import { elementLocale } from '#/locales';
|
||||
|
||||
defineOptions({ name: 'App' });
|
||||
|
||||
useElementPlusDesignTokens();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ElConfigProvider :locale="elementLocale">
|
||||
<RouterView />
|
||||
</ElConfigProvider>
|
||||
</template>
|
||||
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
|
@ -0,0 +1,17 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="36px" height="36px" viewBox="0 0 36 36" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<title>编组备份</title>
|
||||
<g id="页面-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="知识库-数据集" transform="translate(-264, -116)">
|
||||
<g id="编组备份" transform="translate(264, 116)">
|
||||
<path d="M18,0 C27.9411255,-1.82615513e-15 36,8.0588745 36,18 C36,27.9411255 27.9411255,36 18,36 C8.0588745,36 1.21743675e-15,27.9411255 0,18 C-1.21743675e-15,8.0588745 8.0588745,1.82615513e-15 18,0 Z" id="Fill-335" fill="#EAF1FF"></path>
|
||||
<g id="编组-25" transform="translate(7.5, 9)">
|
||||
<rect id="矩形" x="0" y="0" width="21" height="21"></rect>
|
||||
<path d="M9.45,19.95 L11.8384873,5.86086586 C11.8384873,5.86086586 14.402169,2.48291329 19.4944931,3.26848327 C19.7572846,3.31078357 19.95,3.54645402 19.95,3.81838281 L19.1090644,14.8437913 C18.9630681,16.1490466 18.1922128,17.2125873 16.9658506,17.3878294 C15.1146272,17.6537151 10.9041156,17.4603461 9.45,19.95 Z" id="Fill-265" fill="#0066FF"></path>
|
||||
<path d="M9.40161931,19.95 L9.45,4.78125568 C9.45,4.78125568 6.79513951,1.44019973 1.52170595,2.2171891 C1.24956828,2.25902629 1.05,2.49212364 1.05,2.76108221 L1.05,13.8835004 C1.05,15.1505916 1.99945962,16.2264248 3.26943869,16.3997538 C5.19254882,16.6687113 7.89578781,17.4935171 9.40161931,19.95 Z" id="Fill-266" fill="#0066FF"></path>
|
||||
<path d="M9.49837959,19.95 L9.45,4.78125568 C9.45,4.78125568 12.1048605,1.44019973 17.3782941,2.2171891 C17.6504317,2.25902629 17.85,2.49212364 17.85,2.76108221 L17.85,13.8835004 C17.85,15.1505916 16.9005382,16.2264248 15.6305602,16.3997538 C13.7074501,16.6687113 11.0042111,17.4935171 9.49837959,19.95 Z" id="Fill-267" fill="#8AB9FF"></path>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.0 KiB |
|
|
@ -0,0 +1,27 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<title>编组 9</title>
|
||||
<defs>
|
||||
<linearGradient x1="50%" y1="100%" x2="50%" y2="0%" id="linearGradient-1">
|
||||
<stop stop-color="#009FFF" offset="0%"></stop>
|
||||
<stop stop-color="#0066FF" offset="100%"></stop>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<g id="页面-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="知识库-数据集" transform="translate(-482, -323)">
|
||||
<g id="编组-34备份-3" transform="translate(458, 313)">
|
||||
<g id="编组-9" transform="translate(24, 10)">
|
||||
<path d="M2.88,0 L21.12,0 C22.7105801,1.56462329e-15 24,1.28941992 24,2.88 L24,21.12 C24,22.7105801 22.7105801,24 21.12,24 L2.88,24 C1.28941992,24 2.75795744e-15,22.7105801 0,21.12 L0,2.88 C2.4929933e-16,1.28941992 1.28941992,7.3627403e-16 2.88,0 Z" id="矩形" fill="#E3F3FF"></path>
|
||||
<g id="编组-33" transform="translate(4.32, 4.2337)">
|
||||
<path d="M3.08240956,0.809720058 L12.2915386,0.806869334 C13.2458864,0.806039085 14.0197778,1.57945149 14.0200733,2.53379947 C14.0200733,2.53397777 14.0200733,2.53415608 14.0195385,2.53433438 L14.0195385,4.91283382 L14.0195385,4.91283382 L14.0137691,12.9913951 C14.0118457,13.9490909 13.2352873,14.725095 12.2775904,14.7263344 L3.08240965,14.7263344 C2.12422887,14.7263344 1.34746988,13.9495754 1.34746988,12.9913947 L1.34746988,2.54519688 C1.34800702,1.58722583 2.12443873,0.810553743 3.08240956,0.809720058 Z" id="矩形" fill="url(#linearGradient-1)"></path>
|
||||
<path d="M2.29973554,0.531039382 L9.00664031,0.523209269 L9.00664031,0.523209269 L13.1748628,4.5511648 L13.1442992,12.4704581 C13.1406418,13.4181231 12.3717348,14.1846126 11.424063,14.1852859 L2.29565016,14.1917712 C1.34490763,14.1924466 0.573630977,13.4222651 0.572955526,12.4715226 C0.572954944,12.4707039 0.572954946,12.4698853 0.572955533,12.4690666 L0.580273692,2.25127731 C0.580953724,1.30180091 1.35025954,0.532147867 2.29973554,0.531039382 Z" id="矩形" fill-opacity="0.232025787" fill="#FFFFFF" transform="translate(6.8733, 7.3581) rotate(5) translate(-6.8733, -7.3581)"></path>
|
||||
<path d="M9.41174744,0.884008985 L9.40983026,3.56082175 C9.40928551,4.32141576 10.0254272,4.93844065 10.7860212,4.9389854 C10.7866761,4.93898587 10.787331,4.93898587 10.787986,4.9389854 L13.579952,4.93700186 L13.579952,4.93700186 L9.41174744,0.884008985 Z" id="路径-6" fill-opacity="0.232025787" fill="#FFFFFF" transform="translate(11.4944, 2.912) rotate(5) translate(-11.4944, -2.912)"></path>
|
||||
<rect id="矩形" fill="#FFFFFF" transform="translate(6.12, 5.8629) rotate(6) translate(-6.12, -5.8629)" x="3.6" y="5.52" width="5.04" height="1" rx="0.346987953"></rect>
|
||||
<rect id="矩形备份-7" fill="#FFFFFF" transform="translate(4.5745, 7.7829) rotate(6) translate(-4.5745, -7.7829)" x="3.36" y="7.44" width="2.42891566" height="1" rx="0.346987953"></rect>
|
||||
<rect id="矩形备份-9" fill="#FFFFFF" transform="translate(5.16, 9.7029) rotate(6) translate(-5.16, -9.7029)" x="3.12" y="9.36" width="4.08" height="1" rx="0.346987953"></rect>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.4 KiB |
|
After Width: | Height: | Size: 4.2 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 408 B |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 668 B |
|
After Width: | Height: | Size: 9.4 KiB |
|
After Width: | Height: | Size: 173 KiB |
|
After Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 9.3 KiB |
|
After Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
|
|
@ -0,0 +1,90 @@
|
|||
import { createApp, watchEffect } from 'vue';
|
||||
import {
|
||||
BubbleList,
|
||||
Conversations,
|
||||
Sender,
|
||||
XMarkdown,
|
||||
} from 'vue-element-plus-x';
|
||||
|
||||
import { registerAccessDirective } from '@aiflowy/access';
|
||||
import { registerLoadingDirective } from '@aiflowy/common-ui';
|
||||
import { preferences } from '@aiflowy/preferences';
|
||||
import { initStores } from '@aiflowy/stores';
|
||||
import '@aiflowy/styles';
|
||||
import '@aiflowy/styles/ele';
|
||||
|
||||
import { useTitle } from '@vueuse/core';
|
||||
import { ElLoading } from 'element-plus';
|
||||
|
||||
import { $t, setupI18n } from '#/locales';
|
||||
|
||||
import { initComponentAdapter } from './adapter/component';
|
||||
import { initSetupAIFlowyForm } from './adapter/form';
|
||||
import App from './app.vue';
|
||||
import { router } from './router';
|
||||
|
||||
async function bootstrap(namespace: string) {
|
||||
// 初始化组件适配器
|
||||
await initComponentAdapter();
|
||||
|
||||
// 初始化表单组件
|
||||
await initSetupAIFlowyForm();
|
||||
|
||||
// // 设置弹窗的默认配置
|
||||
// setDefaultModalProps({
|
||||
// fullscreenButton: false,
|
||||
// });
|
||||
// // 设置抽屉的默认配置
|
||||
// setDefaultDrawerProps({
|
||||
// zIndex: 2000,
|
||||
// });
|
||||
const app = createApp(App);
|
||||
|
||||
// 注册Element Plus提供的v-loading指令
|
||||
app.directive('loading', ElLoading.directive);
|
||||
|
||||
app.component('ElBubbleList', BubbleList);
|
||||
app.component('ElConversations', Conversations);
|
||||
app.component('ElSender', Sender);
|
||||
app.component('XMarkdown', XMarkdown);
|
||||
|
||||
// 注册AIFlowy提供的v-loading和v-spinning指令
|
||||
registerLoadingDirective(app, {
|
||||
loading: false, // AIFlowy提供的v-loading指令和Element Plus提供的v-loading指令二选一即可,此处false表示不注册AIFlowy提供的v-loading指令
|
||||
spinning: 'spinning',
|
||||
});
|
||||
|
||||
// 国际化 i18n 配置
|
||||
await setupI18n(app);
|
||||
|
||||
// 配置 pinia-tore
|
||||
await initStores(app, { namespace });
|
||||
|
||||
// 安装权限指令
|
||||
registerAccessDirective(app);
|
||||
|
||||
// 初始化 tippy
|
||||
const { initTippy } = await import('@aiflowy/common-ui/es/tippy');
|
||||
initTippy(app);
|
||||
|
||||
// 配置路由及路由守卫
|
||||
app.use(router);
|
||||
|
||||
// 配置Motion插件
|
||||
const { MotionPlugin } = await import('@aiflowy/plugins/motion');
|
||||
app.use(MotionPlugin);
|
||||
|
||||
// 动态更新标题
|
||||
watchEffect(() => {
|
||||
if (preferences.app.dynamicTitle) {
|
||||
const routeTitle = router.currentRoute.value.meta?.title;
|
||||
const pageTitle =
|
||||
(routeTitle ? `${$t(routeTitle)} - ` : '') + preferences.app.name;
|
||||
useTitle(pageTitle);
|
||||
}
|
||||
});
|
||||
|
||||
app.mount('#app');
|
||||
}
|
||||
|
||||
export { bootstrap };
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
<script setup lang="ts">
|
||||
import { ElAvatar } from 'element-plus';
|
||||
|
||||
import defaultBotAvatar from '#/assets/ai/bot/defaultBotAvatar.png';
|
||||
|
||||
const props = defineProps<{
|
||||
size?: number;
|
||||
src?: string;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ElAvatar :src="props.src ?? defaultBotAvatar" :size="props.size ?? 36" />
|
||||
</template>
|
||||
|
|
@ -0,0 +1,355 @@
|
|||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
|
||||
import { preferences } from '@aiflowy/preferences';
|
||||
|
||||
import { MoreFilled } from '@element-plus/icons-vue';
|
||||
import {
|
||||
ElAvatar,
|
||||
ElButton,
|
||||
ElCard,
|
||||
ElDropdown,
|
||||
ElDropdownItem,
|
||||
ElDropdownMenu,
|
||||
ElEmpty,
|
||||
ElIcon,
|
||||
} from 'element-plus';
|
||||
|
||||
import { hasPermission } from '#/api/common/hasPermission.ts';
|
||||
|
||||
const props = defineProps({
|
||||
titleKey: {
|
||||
type: String,
|
||||
default: 'title',
|
||||
},
|
||||
avatarKey: {
|
||||
type: String,
|
||||
default: 'avatar',
|
||||
},
|
||||
/** 默认头像 */
|
||||
defaultAvatar: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
},
|
||||
descriptionKey: {
|
||||
type: String,
|
||||
default: 'description',
|
||||
},
|
||||
data: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
// 操作按钮配置
|
||||
actions: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
});
|
||||
// 定义组件事件
|
||||
const emit = defineEmits(['actionClick']);
|
||||
|
||||
// 可见的操作按钮(最多3个)
|
||||
const visibleActions = computed(() => {
|
||||
return props.actions.slice(0, 3);
|
||||
});
|
||||
|
||||
// 下拉菜单中的操作按钮
|
||||
const dropdownActions = computed(() => {
|
||||
return props.actions.slice(3);
|
||||
});
|
||||
|
||||
// 处理操作按钮点击
|
||||
const handleActionClick = (action, item) => {
|
||||
emit('actionClick', { action, item });
|
||||
};
|
||||
|
||||
const filteredActions = computed(() => {
|
||||
return dropdownActions.value.filter((action) => {
|
||||
return hasPermission([action.permission]);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="card-list-container">
|
||||
<div class="card-list">
|
||||
<ElCard v-for="item in data" :key="item.id" class="card-item">
|
||||
<div class="card-content">
|
||||
<!-- 卡片头部:头像和基本信息 -->
|
||||
<div class="card-header">
|
||||
<ElAvatar :src="item[avatarKey] ?? defaultAvatar" />
|
||||
<div class="card-info">
|
||||
<h3 class="card-title">{{ item[titleKey] }}</h3>
|
||||
<p class="card-desc">{{ item[descriptionKey] }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 操作按钮区域 -->
|
||||
<div class="card-actions">
|
||||
<!-- 最多显示3个操作按钮 -->
|
||||
<ElButton
|
||||
v-for="(action, index) in visibleActions"
|
||||
:key="index"
|
||||
link
|
||||
class="action-btn"
|
||||
:icon="action.icon"
|
||||
v-access:code="action.permission"
|
||||
@click="handleActionClick(action, item)"
|
||||
>
|
||||
{{ action.label }}
|
||||
<span
|
||||
v-if="
|
||||
index < visibleActions.length - 1 ||
|
||||
(index === visibleActions.length - 1 &&
|
||||
filteredActions.length > 0)
|
||||
"
|
||||
class="divider"
|
||||
></span>
|
||||
</ElButton>
|
||||
|
||||
<!-- 更多操作下拉菜单 -->
|
||||
<ElDropdown
|
||||
v-if="filteredActions.length > 0"
|
||||
class="action-btn"
|
||||
@command="(command) => handleActionClick(command, item)"
|
||||
>
|
||||
<ElIcon class="el-icon--right">
|
||||
<MoreFilled />
|
||||
</ElIcon>
|
||||
<template #dropdown>
|
||||
<ElDropdownMenu>
|
||||
<ElDropdownItem
|
||||
v-for="action in filteredActions"
|
||||
:key="action.name"
|
||||
:command="action"
|
||||
:icon="action.icon"
|
||||
:class="{
|
||||
'delete-dropdown-item': action.name === 'delete',
|
||||
}"
|
||||
>
|
||||
{{ action.label }}
|
||||
</ElDropdownItem>
|
||||
</ElDropdownMenu>
|
||||
</template>
|
||||
</ElDropdown>
|
||||
</div>
|
||||
</div>
|
||||
</ElCard>
|
||||
</div>
|
||||
|
||||
<div v-if="data.length === 0" class="empty-state">
|
||||
<ElEmpty
|
||||
:image="`/empty${preferences.theme.mode === 'dark' ? '-dark' : ''}.png`"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.card-list-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.card-list {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||
gap: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
:deep(.el-card__body) {
|
||||
padding: 20px 0 0 0;
|
||||
}
|
||||
.card-item {
|
||||
transition: all 0.3s ease;
|
||||
border-radius: 8px;
|
||||
flex-shrink: 0;
|
||||
height: 165px;
|
||||
min-width: 0;
|
||||
}
|
||||
:deep(.delete-dropdown-item) {
|
||||
color: #ff4d4f !important;
|
||||
}
|
||||
:deep(.delete-dropdown-item:hover) {
|
||||
color: #ff7875 !important;
|
||||
}
|
||||
:deep(.delete-dropdown-item .el-icon) {
|
||||
color: inherit !important;
|
||||
}
|
||||
.card-item:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.card-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
background-color: var(--el-bg-color);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 15px;
|
||||
margin-bottom: 15px;
|
||||
flex: 1;
|
||||
padding: 0 20px 0 20px;
|
||||
}
|
||||
|
||||
.card-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
margin: 0 0 8px 0;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-primary);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.card-desc {
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
font-size: 14px;
|
||||
color: #606266;
|
||||
line-height: 1.5;
|
||||
height: 42px;
|
||||
min-height: 42px;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.card-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 48px;
|
||||
background:
|
||||
linear-gradient(
|
||||
180deg,
|
||||
var(--el-color-primary-light-9),
|
||||
var(--el-bg-color)
|
||||
),
|
||||
var(--el-bg-color);
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
color: #acb7c6;
|
||||
position: relative;
|
||||
&:hover {
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.more-btn {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.divider {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 1px;
|
||||
height: 20px;
|
||||
background-color: #e0e0e0;
|
||||
}
|
||||
|
||||
/* 确保按钮内容不换行 */
|
||||
.action-btn .el-button {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
.empty-state {
|
||||
padding: 40px 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
/* 超大屏幕 */
|
||||
@media (min-width: 1920px) {
|
||||
.card-list {
|
||||
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
|
||||
gap: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 大屏幕 */
|
||||
@media (min-width: 1200px) and (max-width: 1919px) {
|
||||
.card-list {
|
||||
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
||||
gap: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 中等屏幕 */
|
||||
@media (min-width: 992px) and (max-width: 1199px) {
|
||||
.card-list {
|
||||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
padding: 0 16px 0 16px;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.card-desc {
|
||||
font-size: 13px;
|
||||
height: 36px;
|
||||
min-height: 36px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 小屏幕 - 平板 */
|
||||
@media (min-width: 768px) and (max-width: 991px) {
|
||||
.card-list {
|
||||
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
.card-header {
|
||||
padding: 0 12px 0 12px;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 14px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.card-desc {
|
||||
font-size: 12px;
|
||||
height: 32px;
|
||||
min-height: 32px;
|
||||
-webkit-line-clamp: 2;
|
||||
}
|
||||
|
||||
.card-actions {
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.action-btn .el-button {
|
||||
font-size: 11px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,325 @@
|
|||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref, toRefs } from 'vue';
|
||||
|
||||
import { $t } from '@aiflowy/locales';
|
||||
import { preferences } from '@aiflowy/preferences';
|
||||
|
||||
import { Delete, Edit, MoreFilled, Plus } from '@element-plus/icons-vue';
|
||||
import {
|
||||
ElButton,
|
||||
ElDialog,
|
||||
ElDropdown,
|
||||
ElDropdownItem,
|
||||
ElDropdownMenu,
|
||||
ElEmpty,
|
||||
ElForm,
|
||||
ElFormItem,
|
||||
ElIcon,
|
||||
ElInput,
|
||||
ElMessage,
|
||||
ElMessageBox,
|
||||
} from 'element-plus';
|
||||
|
||||
const props = defineProps({
|
||||
title: {
|
||||
type: String,
|
||||
default: '数据管理',
|
||||
},
|
||||
categoryData: {
|
||||
type: Array<Record<string, any>>,
|
||||
default: () => [],
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
},
|
||||
titleKey: {
|
||||
type: String,
|
||||
default: 'title',
|
||||
},
|
||||
panelWidth: {
|
||||
type: Number,
|
||||
default: 200,
|
||||
},
|
||||
valueKey: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
},
|
||||
defaultSelectedCategory: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
defaultFormData: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
});
|
||||
|
||||
// 暴露给父组件的事件
|
||||
const emit = defineEmits([
|
||||
'add', // 新增提交时触发
|
||||
'edit', // 编辑提交时触发
|
||||
'delete', // 删除时触发
|
||||
'click', // 点击时触发
|
||||
]);
|
||||
|
||||
const finalValueKey = computed(() => {
|
||||
// 父组件传递了 valueKey 就用 valueKey,否则用 titleKey
|
||||
return props.valueKey ?? props.titleKey;
|
||||
});
|
||||
|
||||
const { defaultFormData } = toRefs(props);
|
||||
|
||||
// 状态管理
|
||||
const dialogVisible = ref(false); // 弹窗显隐
|
||||
const isEdit = ref(false); // 是否为编辑模式
|
||||
const formData = ref({
|
||||
...defaultFormData,
|
||||
}); // 表单数据
|
||||
|
||||
/**
|
||||
* 从 Proxy 对象提取原始数据
|
||||
*/
|
||||
const extractFromProxy = (proxyObj: any) => {
|
||||
try {
|
||||
if (proxyObj && typeof proxyObj === 'object') {
|
||||
const raw = (proxyObj as any).__v_raw || proxyObj;
|
||||
|
||||
if (raw && typeof raw === 'object') {
|
||||
const result: any = Array.isArray(raw) ? [] : {};
|
||||
for (const key in raw) {
|
||||
if (
|
||||
key !== '__v_skip' &&
|
||||
Object.prototype.hasOwnProperty.call(raw, key)
|
||||
) {
|
||||
result[key] = extractFromProxy(raw[key]);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
return raw;
|
||||
}
|
||||
return proxyObj;
|
||||
} catch (error) {
|
||||
console.warn('提取 Proxy 对象失败,使用浅拷贝:', error);
|
||||
return { ...proxyObj };
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 新增按钮点击事件
|
||||
*/
|
||||
const handleAdd = () => {
|
||||
isEdit.value = false;
|
||||
formData.value = {
|
||||
...defaultFormData,
|
||||
};
|
||||
dialogVisible.value = true;
|
||||
};
|
||||
|
||||
/**
|
||||
* 编辑按钮点击事件
|
||||
* @param {object} row - 表格当前行数据
|
||||
*/
|
||||
const handleEdit = (row: any) => {
|
||||
isEdit.value = true;
|
||||
// 使用提取原始数据的方法
|
||||
formData.value = extractFromProxy(row);
|
||||
dialogVisible.value = true;
|
||||
};
|
||||
|
||||
/**
|
||||
* 删除按钮点击事件
|
||||
* @param {object} row - 表格当前行数据
|
||||
*/
|
||||
const handleDelete = (row: any) => {
|
||||
// 先提取原始数据
|
||||
const rawData = extractFromProxy(row);
|
||||
|
||||
ElMessageBox.confirm(`此操作将永久删除该${props.title}, 是否继续?`, '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
})
|
||||
.then(() => {
|
||||
// 触发父组件删除事件,传递原始数据
|
||||
emit('delete', rawData);
|
||||
})
|
||||
.catch(() => {
|
||||
ElMessage({ type: 'info', message: '已取消删除' });
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 表单提交事件
|
||||
*/
|
||||
const handleSubmit = () => {
|
||||
// 触发对应事件,传递表单数据
|
||||
if (isEdit.value) {
|
||||
emit('edit', formData.value);
|
||||
} else {
|
||||
emit('add', formData.value);
|
||||
}
|
||||
// 提交后关闭弹窗
|
||||
dialogVisible.value = false;
|
||||
};
|
||||
|
||||
const selectedCategory = ref();
|
||||
|
||||
onMounted(() => {
|
||||
// 初始化时,检查是否有默认选中的分类
|
||||
if (props.defaultSelectedCategory) {
|
||||
selectedCategory.value = props.defaultSelectedCategory;
|
||||
}
|
||||
});
|
||||
|
||||
const handleCategoryClick = (category: any) => {
|
||||
// 选中值:用 finalValueKey(父组件指定的字段)
|
||||
selectedCategory.value = category[finalValueKey.value];
|
||||
emit('click', {
|
||||
item: extractFromProxy(category),
|
||||
value: category[finalValueKey.value], // 父组件指定字段的值
|
||||
label: category[props.titleKey], // 分类名称
|
||||
});
|
||||
};
|
||||
|
||||
const handleEditClick = (event: any, item: any) => {
|
||||
event.stopPropagation();
|
||||
handleEdit(item);
|
||||
};
|
||||
|
||||
const handleDeleteClick = (event: any, item: any) => {
|
||||
event.stopPropagation();
|
||||
handleDelete(item);
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="flex h-full flex-col rounded-lg border border-[var(--el-border-color)] bg-[var(--el-bg-color)] p-2"
|
||||
:style="{ width: `${panelWidth}px` }"
|
||||
>
|
||||
<div class="flex flex-1 flex-col gap-5">
|
||||
<!-- <h3 class="text-base font-medium">{{ title }}</h3> -->
|
||||
|
||||
<div class="flex-1 overflow-scroll">
|
||||
<div v-for="(item, index) in categoryData" :key="index">
|
||||
<div
|
||||
:class="{ selected: selectedCategory === item[finalValueKey] }"
|
||||
class="crud-category-item"
|
||||
@click="handleCategoryClick(item)"
|
||||
>
|
||||
<div>{{ item[titleKey] }}</div>
|
||||
<div v-if="item.id !== '0'">
|
||||
<ElDropdown @click.stop>
|
||||
<span class="dropdown-trigger">
|
||||
<ElIcon><MoreFilled /></ElIcon>
|
||||
</span>
|
||||
<template #dropdown>
|
||||
<ElDropdownMenu>
|
||||
<ElDropdownItem @click="handleEditClick($event, item)">
|
||||
<ElButton :icon="Edit" link>
|
||||
{{ $t('button.edit') }}
|
||||
</ElButton>
|
||||
</ElDropdownItem>
|
||||
<ElDropdownItem @click="handleDeleteClick($event, item)">
|
||||
<ElButton type="danger" :icon="Delete" link>
|
||||
{{ $t('button.delete') }}
|
||||
</ElButton>
|
||||
</ElDropdownItem>
|
||||
</ElDropdownMenu>
|
||||
</template>
|
||||
</ElDropdown>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ElButton @click="handleAdd" :icon="Plus" :disabled="disabled" plain>
|
||||
{{ $t('button.add') }}
|
||||
</ElButton>
|
||||
|
||||
<!-- 无数据提示 -->
|
||||
<div v-if="categoryData.length === 0 && !loading" class="no-data">
|
||||
<ElEmpty
|
||||
:image="`/empty${preferences.theme.mode === 'dark' ? '-dark' : ''}.png`"
|
||||
:description="$t('common.noDataAvailable')"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 新增/编辑弹窗 -->
|
||||
<ElDialog
|
||||
:title="
|
||||
isEdit ? `${$t('button.edit')}${title}` : `${$t('button.add')}${title}`
|
||||
"
|
||||
v-model="dialogVisible"
|
||||
width="500px"
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<!-- 表单内容(父组件通过插槽配置) -->
|
||||
<slot name="form" :form-data="formData" :is-edit="isEdit"></slot>
|
||||
<ElForm :model="formData" status-icon>
|
||||
<ElFormItem :prop="titleKey">
|
||||
<ElInput v-model.trim="formData[titleKey]" />
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
|
||||
<template #footer>
|
||||
<ElButton @click="dialogVisible = false">
|
||||
{{ $t('button.cancel') }}
|
||||
</ElButton>
|
||||
<ElButton type="primary" @click="handleSubmit">
|
||||
{{ $t('button.confirm') }}
|
||||
</ElButton>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.crud-category-item:hover {
|
||||
background-color: var(--el-color-primary-light-9);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.crud-category-item.selected {
|
||||
background-color: var(--el-color-primary-light-9);
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
|
||||
.no-data {
|
||||
text-align: center;
|
||||
padding: 40px 0;
|
||||
}
|
||||
|
||||
.crud-category-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 8px 12px;
|
||||
height: 40px;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.crud-category-item :deep(.el-icon) {
|
||||
outline: none !important;
|
||||
}
|
||||
|
||||
.dropdown-trigger {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 4px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s;
|
||||
}
|
||||
|
||||
.dropdown-trigger:hover {
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,370 @@
|
|||
<script setup>
|
||||
import { computed, isVNode, onMounted, ref } from 'vue';
|
||||
|
||||
import { ArrowLeft, ArrowRight } from '@element-plus/icons-vue';
|
||||
import { ElIcon } from 'element-plus';
|
||||
|
||||
// 定义组件属性
|
||||
const props = defineProps({
|
||||
// 分类数据,格式示例:[{ name: '分类1', icon: SomeIcon }, { name: '分类2' }]
|
||||
categories: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
required: true,
|
||||
},
|
||||
valueKey: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
},
|
||||
titleKey: {
|
||||
type: String,
|
||||
default: 'name',
|
||||
},
|
||||
needHideCollapse: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
iconKey: {
|
||||
type: String,
|
||||
default: 'icon',
|
||||
},
|
||||
// 自定义展开状态宽度(默认300px)
|
||||
expandWidth: {
|
||||
type: Number,
|
||||
default: 120,
|
||||
},
|
||||
// 自定义收缩状态宽度(默认48px)
|
||||
collapseWidth: {
|
||||
type: Number,
|
||||
default: 48,
|
||||
},
|
||||
// 默认选中的分类(用于初始化) 指定key
|
||||
defaultSelectedCategory: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
iconSize: { type: [Number, String], default: 18 },
|
||||
iconColor: { type: String, default: 'var(--el-text-color-primary)' },
|
||||
// 新增:是否用 img 标签渲染 SVG 字符串(默认 false)
|
||||
useImgForSvg: { type: Boolean, default: false },
|
||||
});
|
||||
|
||||
// 定义事件
|
||||
const emit = defineEmits([
|
||||
'click', // 分类项点击事件
|
||||
'panelToggle', // 面板收缩状态改变事件
|
||||
]);
|
||||
|
||||
const finalValueKey = computed(() => {
|
||||
// 父组件传递了 valueKey 就用 valueKey,否则用 titleKey
|
||||
return props.valueKey ?? props.titleKey;
|
||||
});
|
||||
|
||||
// -------------------------- 核心工具函数 --------------------------
|
||||
/**
|
||||
* SVG 字符串转 Data URL(供 img 标签使用)
|
||||
* @param {string} svgString - 清理后的 SVG 字符串
|
||||
* @returns {string} Data URL
|
||||
*/
|
||||
const svgToDataUrl = (svgString) => {
|
||||
// 1. 去除 SVG 中的换行和多余空格(优化编码后体积)
|
||||
const cleanedSvg = svgString
|
||||
.replaceAll('\n', '')
|
||||
.replaceAll(/\s+/g, ' ')
|
||||
.trim();
|
||||
// 2. URL 编码 + 拼接 Data URL 格式
|
||||
return `data:image/svg+xml;utf8,${encodeURIComponent(cleanedSvg)}`;
|
||||
};
|
||||
|
||||
/**
|
||||
* 判断是否为组件(Element Plus 图标 / 自定义 SVG 组件)
|
||||
*/
|
||||
const isComponent = (icon) => {
|
||||
return (
|
||||
typeof icon === 'object' && (typeof icon === 'object' || isVNode(icon))
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* 判断是否为 SVG 字符串
|
||||
*/
|
||||
const isSvgString = (icon) => {
|
||||
return typeof icon === 'string' && icon.trim().startsWith('<svg');
|
||||
};
|
||||
|
||||
/**
|
||||
* 判断是否为图片 URL
|
||||
*/
|
||||
const isImageUrl = (icon) => {
|
||||
return (
|
||||
typeof icon === 'string' &&
|
||||
(icon.endsWith('.svg') ||
|
||||
icon.endsWith('.png') ||
|
||||
icon.endsWith('.jpg') ||
|
||||
icon.startsWith('http://') ||
|
||||
icon.startsWith('https://'))
|
||||
);
|
||||
};
|
||||
|
||||
// 面板收缩状态
|
||||
const isCollapsed = ref(false);
|
||||
|
||||
// 检查是否有分类包含图标
|
||||
const hasIcons = computed(() => {
|
||||
return props.categories.some((item) => item[props.iconKey]);
|
||||
});
|
||||
|
||||
// 动态计算面板宽度
|
||||
const panelWidth = computed(() => {
|
||||
if (isCollapsed.value) {
|
||||
// 收缩状态:有图标用自定义收缩宽度,无图标保持最小适配宽度
|
||||
return hasIcons.value ? props.collapseWidth : 120;
|
||||
} else {
|
||||
// 展开状态:使用自定义展开宽度
|
||||
return props.expandWidth;
|
||||
}
|
||||
});
|
||||
|
||||
// 切换面板收缩状态
|
||||
const togglePanel = () => {
|
||||
isCollapsed.value = !isCollapsed.value;
|
||||
emit('panelToggle', {
|
||||
collapsed: isCollapsed.value,
|
||||
currentWidth: panelWidth.value,
|
||||
});
|
||||
};
|
||||
const selectedCategory = ref(null);
|
||||
// 处理分类项点击
|
||||
const handleCategoryClick = (category) => {
|
||||
// 选中值:用 finalValueKey(父组件指定的字段)
|
||||
selectedCategory.value = category[finalValueKey.value];
|
||||
emit('click', {
|
||||
item: category,
|
||||
value: category[finalValueKey.value], // 父组件指定字段的值
|
||||
label: category[props.titleKey], // 分类名称
|
||||
});
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
// 初始化时,检查是否有默认选中的分类
|
||||
if (props.defaultSelectedCategory) {
|
||||
selectedCategory.value = props.defaultSelectedCategory;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="category-panel" :style="{ width: `${panelWidth}px` }">
|
||||
<!-- 右上角收缩/展开按钮 -->
|
||||
<div class="toggle-panel-btn" @click="togglePanel" v-if="!needHideCollapse">
|
||||
<ElIcon>
|
||||
<ArrowLeft v-if="!isCollapsed" />
|
||||
<ArrowRight v-else />
|
||||
</ElIcon>
|
||||
</div>
|
||||
<div style="margin-bottom: 48px" v-if="!needHideCollapse"></div>
|
||||
<!-- 分类列表容器 -->
|
||||
<div class="category-list" :class="{ collapsed: isCollapsed }">
|
||||
<!-- 遍历一级分类数据 -->
|
||||
<div
|
||||
v-for="(category, index) in categories"
|
||||
:key="index"
|
||||
class="category-item"
|
||||
>
|
||||
<div
|
||||
class="category-item-content"
|
||||
:class="{ selected: selectedCategory === category[finalValueKey] }"
|
||||
@click="handleCategoryClick(category)"
|
||||
>
|
||||
<!-- 图标 -->
|
||||
<div v-if="category[iconKey]" class="category-icon">
|
||||
<!-- 1. 组件类型图标(Element Plus / 自定义 SVG 组件) -->
|
||||
<ElIcon v-if="isComponent(category[iconKey])">
|
||||
<component :is="category[iconKey]" />
|
||||
</ElIcon>
|
||||
<!-- 2. SVG 字符串:支持 v-html 或 img 两种渲染方式 -->
|
||||
<template v-else-if="isSvgString(category[iconKey])">
|
||||
<div
|
||||
v-if="!useImgForSvg"
|
||||
v-html="category[iconKey]"
|
||||
class="custom-svg"
|
||||
></div>
|
||||
<img
|
||||
v-else
|
||||
:src="svgToDataUrl(category[iconKey])"
|
||||
:alt="category[titleKey]"
|
||||
class="svg-image"
|
||||
/>
|
||||
</template>
|
||||
<!-- 3. 图片 URL(本地/网络 SVG/PNG) -->
|
||||
<img
|
||||
v-else-if="isImageUrl(category[iconKey])"
|
||||
:src="category[iconKey]"
|
||||
:alt="category[titleKey]"
|
||||
class="svg-image"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 分类名称(收缩状态且有图标时隐藏文字) -->
|
||||
<span
|
||||
class="category-name"
|
||||
:class="{ hidden: isCollapsed && category[iconKey] }"
|
||||
>
|
||||
{{ category[titleKey] }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.category-panel {
|
||||
position: relative; /* 相对定位,用于按钮绝对定位 */
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
transition: width 0.3s cubic-bezier(0.4, 0, 0.2, 1); /* 平滑宽度过渡 */
|
||||
box-sizing: border-box;
|
||||
background: #ffffff;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #f0f0f0;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
/* 右上角收缩/展开按钮 */
|
||||
.toggle-panel-btn {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
z-index: 10; /* 确保按钮在最上层 */
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
background-color: var(--el-color-white);
|
||||
border: 1px solid #e5e7eb;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
/* 按钮不随面板收缩移动 */
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
.toggle-panel-btn:hover {
|
||||
background-color: #f3f4f6;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.toggle-panel-btn .el-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
/* 分类列表容器 */
|
||||
.category-list {
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1px;
|
||||
}
|
||||
|
||||
.category-item {
|
||||
}
|
||||
|
||||
.category-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.category-item-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
gap: 12px;
|
||||
padding: 6px 0 6px 14px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.category-item-content:hover {
|
||||
background-color: var(--el-color-primary-light-9);
|
||||
}
|
||||
|
||||
.category-icon {
|
||||
width: v-bind(iconSize);
|
||||
height: v-bind(iconSize);
|
||||
color: v-bind(iconColor);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.category-icon .el-icon {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.category-item-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.category-name {
|
||||
transition:
|
||||
opacity 0.2s,
|
||||
transform 0.2s;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
font-family:
|
||||
PingFangSC,
|
||||
PingFang SC;
|
||||
font-weight: 400;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
text-align: left;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
/* 收缩状态样式 */
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.collapsed .category-item-content {
|
||||
justify-content: center;
|
||||
padding: 12px 0;
|
||||
}
|
||||
|
||||
/* 收缩状态下文字强制隐藏(避免无图标时文字溢出) */
|
||||
.collapsed .category-name {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* 新增:选中态样式 */
|
||||
.category-item-content.selected {
|
||||
font-weight: 600;
|
||||
background: rgba(0, 102, 255, 0.06);
|
||||
color: #0066ff;
|
||||
}
|
||||
|
||||
.category-item-content.selected:hover {
|
||||
background: rgba(0, 102, 255, 0.06);
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,91 @@
|
|||
<script setup lang="ts">
|
||||
import type { Ref } from 'vue';
|
||||
|
||||
import { nextTick, ref } from 'vue';
|
||||
|
||||
import { $t } from '@aiflowy/locales';
|
||||
|
||||
import { ElButton, ElDialog, ElForm, ElFormItem, ElInput } from 'element-plus';
|
||||
|
||||
interface BasicFormItem {
|
||||
key: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
const emit = defineEmits(['success']);
|
||||
const dialogVisible = ref(false);
|
||||
|
||||
const generateDefaultFormItems = (
|
||||
data: BasicFormItem[] = [],
|
||||
): BasicFormItem[] => {
|
||||
return Array.from({ length: 5 }, (_, i) => ({
|
||||
key: (i + 1).toString(),
|
||||
description: data[i]?.description || '',
|
||||
}));
|
||||
};
|
||||
|
||||
const openDialog = (data: BasicFormItem[]) => {
|
||||
nextTick(() => {
|
||||
basicFormRef.value?.resetFields();
|
||||
});
|
||||
basicForm.value = generateDefaultFormItems(data);
|
||||
dialogVisible.value = true;
|
||||
};
|
||||
|
||||
const basicForm: Ref<BasicFormItem[]> = ref(generateDefaultFormItems());
|
||||
const basicFormRef = ref();
|
||||
|
||||
defineExpose({
|
||||
openDialog(data: BasicFormItem[]) {
|
||||
openDialog(data);
|
||||
},
|
||||
});
|
||||
|
||||
const handleConfirm = () => {
|
||||
basicFormRef.value?.validate((valid: boolean) => {
|
||||
if (valid) {
|
||||
emit('success', basicForm.value);
|
||||
dialogVisible.value = false;
|
||||
}
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ElDialog
|
||||
v-model="dialogVisible"
|
||||
:title="$t('button.add')"
|
||||
width="700"
|
||||
align-center
|
||||
>
|
||||
<ElForm
|
||||
ref="basicFormRef"
|
||||
style="width: 100%; margin-top: 20px"
|
||||
:model="basicForm"
|
||||
label-width="auto"
|
||||
>
|
||||
<template v-for="(item, index) in basicForm" :key="item.key">
|
||||
<ElFormItem
|
||||
:label="`${$t('bot.problemPresupposition')}${item.key}`"
|
||||
:prop="`${index}.description`"
|
||||
label-position="right"
|
||||
>
|
||||
<ElInput v-model="item.description" />
|
||||
</ElFormItem>
|
||||
</template>
|
||||
</ElForm>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<ElButton @click="dialogVisible = false">
|
||||
{{ $t('button.cancel') }}
|
||||
</ElButton>
|
||||
<ElButton type="primary" @click="handleConfirm">
|
||||
{{ $t('button.confirm') }}
|
||||
</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
|
|
@ -0,0 +1,146 @@
|
|||
<script setup lang="ts">
|
||||
import type { FormInstance } from 'element-plus';
|
||||
|
||||
import type { Ref } from 'vue';
|
||||
|
||||
import { nextTick, ref } from 'vue';
|
||||
|
||||
import { $t } from '@aiflowy/locales';
|
||||
|
||||
import {
|
||||
ElButton,
|
||||
ElDialog,
|
||||
ElForm,
|
||||
ElFormItem,
|
||||
ElInput,
|
||||
ElMessage,
|
||||
} from 'element-plus';
|
||||
|
||||
import { api } from '#/api/request';
|
||||
|
||||
interface BasicFormItem {
|
||||
weChatMpAppId: string;
|
||||
weChatMpSecret: string;
|
||||
weChatMpToken: string;
|
||||
EncodingAESKey: string;
|
||||
}
|
||||
const emit = defineEmits(['reload']);
|
||||
const dialogVisible = ref(false);
|
||||
const botId = ref('');
|
||||
const openDialog = (newBotId: string, options: BasicFormItem) => {
|
||||
nextTick(() => {
|
||||
basicFormRef.value?.resetFields();
|
||||
});
|
||||
botId.value = newBotId;
|
||||
basicForm.value = { ...options };
|
||||
dialogVisible.value = true;
|
||||
};
|
||||
|
||||
const basicForm: Ref<BasicFormItem> = ref({
|
||||
weChatMpAppId: '',
|
||||
weChatMpSecret: '',
|
||||
weChatMpToken: '',
|
||||
EncodingAESKey: '',
|
||||
});
|
||||
const basicFormRef = ref<FormInstance>();
|
||||
defineExpose({
|
||||
openDialog(botId: string, options: BasicFormItem) {
|
||||
openDialog(botId, options);
|
||||
},
|
||||
});
|
||||
|
||||
const rules = ref({
|
||||
weChatMpAppId: [
|
||||
{
|
||||
required: true,
|
||||
message: $t('message.required'),
|
||||
trigger: 'blur',
|
||||
},
|
||||
],
|
||||
weChatMpSecret: [
|
||||
{
|
||||
required: true,
|
||||
message: $t('message.required'),
|
||||
trigger: 'blur',
|
||||
},
|
||||
],
|
||||
weChatMpToken: [
|
||||
{
|
||||
required: true,
|
||||
message: $t('message.required'),
|
||||
},
|
||||
],
|
||||
});
|
||||
const handleConfirm = () => {
|
||||
basicFormRef.value?.validate((valid: boolean) => {
|
||||
if (valid) {
|
||||
api
|
||||
.post('/api/v1/bot/updateOptions', {
|
||||
id: botId.value,
|
||||
options: {
|
||||
weChatMpAppId: basicForm.value?.weChatMpAppId,
|
||||
weChatMpSecret: basicForm.value?.weChatMpSecret,
|
||||
weChatMpToken: basicForm.value?.weChatMpToken,
|
||||
EncodingAESKey: basicForm.value?.EncodingAESKey,
|
||||
},
|
||||
})
|
||||
.then((res) => {
|
||||
if (res.errorCode === 0) {
|
||||
ElMessage.success($t('message.updateOkMessage'));
|
||||
emit('reload');
|
||||
} else {
|
||||
ElMessage.error(res.message);
|
||||
}
|
||||
});
|
||||
dialogVisible.value = false;
|
||||
}
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ElDialog
|
||||
v-model="dialogVisible"
|
||||
:title="$t('bot.weChatOfficialAccountConfiguration')"
|
||||
width="700"
|
||||
align-center
|
||||
>
|
||||
<ElForm
|
||||
ref="basicFormRef"
|
||||
style="width: 100%; margin-top: 20px"
|
||||
:model="basicForm"
|
||||
label-width="auto"
|
||||
:rules="rules"
|
||||
>
|
||||
<ElFormItem label="AppId" prop="weChatMpAppId" label-position="right">
|
||||
<ElInput v-model="basicForm.weChatMpAppId" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="Secret" prop="weChatMpSecret" label-position="right">
|
||||
<ElInput v-model="basicForm.weChatMpSecret" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="Token" prop="weChatMpToken" label-position="right">
|
||||
<ElInput v-model="basicForm.weChatMpToken" />
|
||||
</ElFormItem>
|
||||
<ElFormItem
|
||||
label="EncodingAESKey"
|
||||
prop="EncodingAESKey"
|
||||
label-position="right"
|
||||
>
|
||||
<ElInput v-model="basicForm.EncodingAESKey" />
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<ElButton @click="dialogVisible = false">
|
||||
{{ $t('button.cancel') }}
|
||||
</ElButton>
|
||||
<ElButton type="primary" @click="handleConfirm">
|
||||
{{ $t('button.confirm') }}
|
||||
</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
|
|
@ -0,0 +1,126 @@
|
|||
<!--<script setup lang="ts">-->
|
||||
<!--import type { FilesCardProps } from 'vue-element-plus-x/types/FilesCard';-->
|
||||
|
||||
<!--import { ref } from 'vue';-->
|
||||
<!--import { Attachments } from 'vue-element-plus-x';-->
|
||||
|
||||
<!--import { ElMessage } from 'element-plus';-->
|
||||
|
||||
<!--const senderRef = ref();-->
|
||||
<!--const showHeaderFlog = ref(false);-->
|
||||
|
||||
<!--type SelfFilesCardProps = FilesCardProps & {-->
|
||||
<!-- id?: number | string;-->
|
||||
<!--};-->
|
||||
|
||||
<!--const files = ref<SelfFilesCardProps[]>([]);-->
|
||||
<!--defineExpose( {-->
|
||||
<!-- init(firstFile: File) {-->
|
||||
<!-- console.log('firstFile', firstFile);-->
|
||||
<!-- showHeaderFlog.value = true;-->
|
||||
<!-- files.value = [-->
|
||||
<!-- {-->
|
||||
<!-- id: 0,-->
|
||||
<!-- uid: `${firstFile.name}_${firstFile.size}`,-->
|
||||
<!-- name: firstFile.name,-->
|
||||
<!-- fileSize: firstFile.size,-->
|
||||
<!-- imgFile: firstFile,-->
|
||||
<!-- showDelIcon: true,-->
|
||||
<!-- imgVariant: 'square',-->
|
||||
<!-- },-->
|
||||
<!-- ];-->
|
||||
<!-- },-->
|
||||
<!--});-->
|
||||
|
||||
<!--function closeHeader() {-->
|
||||
<!-- showHeaderFlog.value = false;-->
|
||||
<!-- senderRef.value.closeHeader();-->
|
||||
<!--}-->
|
||||
|
||||
<!--function handlePasteFile(firstFile: File, fileList: FileList) {-->
|
||||
<!-- showHeaderFlog.value = true;-->
|
||||
<!-- senderRef.value.openHeader();-->
|
||||
<!-- const fileArray = [...fileList];-->
|
||||
|
||||
<!-- fileArray.forEach((file, index) => {-->
|
||||
<!-- files.value.push({-->
|
||||
<!-- id: index,-->
|
||||
<!-- uid: `${index}_${file.name}_${file.size}`,-->
|
||||
<!-- name: file.name,-->
|
||||
<!-- fileSize: file.size,-->
|
||||
<!-- imgFile: file,-->
|
||||
<!-- showDelIcon: true,-->
|
||||
<!-- imgVariant: 'square',-->
|
||||
<!-- });-->
|
||||
<!-- });-->
|
||||
<!--}-->
|
||||
|
||||
<!--async function handleHttpRequest(options: any) {-->
|
||||
<!-- const formData = new FormData();-->
|
||||
<!-- formData.append('file', options.file);-->
|
||||
<!-- ElMessage.info('上传中...');-->
|
||||
|
||||
<!-- setTimeout(() => {-->
|
||||
<!-- const res = {-->
|
||||
<!-- message: '文件上传成功',-->
|
||||
<!-- fileName: options.file.name,-->
|
||||
<!-- uid: options.file.uid,-->
|
||||
<!-- fileSize: options.file.size,-->
|
||||
<!-- imgFile: options.file,-->
|
||||
<!-- };-->
|
||||
<!-- files.value.push({-->
|
||||
<!-- id: files.value.length,-->
|
||||
<!-- uid: res.uid,-->
|
||||
<!-- name: res.fileName,-->
|
||||
<!-- fileSize: res.fileSize,-->
|
||||
<!-- imgFile: res.imgFile,-->
|
||||
<!-- showDelIcon: true,-->
|
||||
<!-- imgVariant: 'square',-->
|
||||
<!-- });-->
|
||||
|
||||
<!-- ElMessage.success('上传成功');-->
|
||||
<!-- }, 1000);-->
|
||||
<!--}-->
|
||||
|
||||
<!--function handleDeleteCard(item: SelfFilesCardProps) {-->
|
||||
<!-- files.value = files.value.filter((items: any) => items.id !== item.id);-->
|
||||
<!-- ElMessage.success('删除成功');-->
|
||||
<!--}-->
|
||||
<!--</script>-->
|
||||
|
||||
<!--<template>-->
|
||||
<!-- <div class="header-self-wrap">-->
|
||||
<!-- <Attachments-->
|
||||
<!-- :items="files"-->
|
||||
<!-- :http-request="handleHttpRequest"-->
|
||||
<!-- @delete-card="handleDeleteCard"-->
|
||||
<!-- />-->
|
||||
<!-- </div>-->
|
||||
<!--</template>-->
|
||||
|
||||
<!--<style scoped>-->
|
||||
<!--.header-self-wrap {-->
|
||||
<!-- display: flex;-->
|
||||
<!-- flex-direction: row;-->
|
||||
<!-- padding: 16px;-->
|
||||
<!-- width: 100%;-->
|
||||
<!-- overflow: auto;-->
|
||||
<!-- .header-self-title {-->
|
||||
<!-- width: 100%;-->
|
||||
<!-- display: flex;-->
|
||||
<!-- height: 30px;-->
|
||||
<!-- align-items: center;-->
|
||||
<!-- justify-content: space-between;-->
|
||||
<!-- padding-bottom: 8px;-->
|
||||
<!-- }-->
|
||||
<!-- .header-self-content {-->
|
||||
<!-- flex: 1;-->
|
||||
<!-- display: flex;-->
|
||||
<!-- align-items: center;-->
|
||||
<!-- justify-content: center;-->
|
||||
<!-- font-size: 20px;-->
|
||||
<!-- color: #626aef;-->
|
||||
<!-- font-weight: 600;-->
|
||||
<!-- }-->
|
||||
<!--}-->
|
||||
<!--</style>-->
|
||||
|
|
@ -0,0 +1,407 @@
|
|||
<script setup lang="ts">
|
||||
import type { Sender } from 'vue-element-plus-x';
|
||||
import type { BubbleListProps } from 'vue-element-plus-x/types/BubbleList';
|
||||
import type { TypewriterInstance } from 'vue-element-plus-x/types/Typewriter';
|
||||
|
||||
import type { BotInfo, ChatMessage } from '@aiflowy/types';
|
||||
|
||||
import { onMounted, ref, watchEffect } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
|
||||
import { $t } from '@aiflowy/locales';
|
||||
import { useBotStore } from '@aiflowy/stores';
|
||||
import { cn, uuid } from '@aiflowy/utils';
|
||||
|
||||
import { CopyDocument, RefreshRight } from '@element-plus/icons-vue';
|
||||
import { ElButton, ElIcon, ElMessage, ElSpace } from 'element-plus';
|
||||
import { tryit } from 'radash';
|
||||
|
||||
import { getMessageList, getPerQuestions } from '#/api';
|
||||
import { api, sseClient } from '#/api/request';
|
||||
import SendEnableIcon from '#/components/icons/SendEnableIcon.vue';
|
||||
import SendIcon from '#/components/icons/SendIcon.vue';
|
||||
|
||||
import BotAvatar from '../botAvatar/botAvatar.vue';
|
||||
import SendingIcon from '../icons/SendingIcon.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
bot?: BotInfo;
|
||||
conversationId?: string;
|
||||
// 是否显示对话列表
|
||||
showChatConversations?: boolean;
|
||||
}>();
|
||||
const botStore = useBotStore();
|
||||
interface historyMessageType {
|
||||
role: string;
|
||||
content: string;
|
||||
}
|
||||
interface presetQuestionsType {
|
||||
key: string;
|
||||
description: string;
|
||||
}
|
||||
const route = useRoute();
|
||||
const botId = ref<string>((route.params.id as string) || '');
|
||||
const router = useRouter();
|
||||
|
||||
const bubbleItems = ref<BubbleListProps<ChatMessage>['list']>([]);
|
||||
const senderRef = ref<InstanceType<typeof Sender>>();
|
||||
const senderValue = ref('');
|
||||
const sending = ref(false);
|
||||
const getConversationId = async () => {
|
||||
const res = await api.get('/api/v1/bot/generateConversationId');
|
||||
return res.data;
|
||||
};
|
||||
const localeConversationId = ref<any>('');
|
||||
|
||||
const presetQuestions = ref<presetQuestionsType[]>([]);
|
||||
defineExpose({
|
||||
clear() {
|
||||
bubbleItems.value = [];
|
||||
messages.value = [];
|
||||
},
|
||||
});
|
||||
const getPresetQuestions = () => {
|
||||
api
|
||||
.get('/api/v1/bot/detail', {
|
||||
params: {
|
||||
id: botId.value,
|
||||
},
|
||||
})
|
||||
.then((res) => {
|
||||
if (res.data.options?.presetQuestions) {
|
||||
presetQuestions.value = res.data.options?.presetQuestions
|
||||
.filter(
|
||||
(item: presetQuestionsType) =>
|
||||
item.description && item.description.trim() !== '',
|
||||
)
|
||||
.map((item: presetQuestionsType) => ({
|
||||
key: item.key,
|
||||
description: item.description,
|
||||
}));
|
||||
}
|
||||
});
|
||||
};
|
||||
onMounted(async () => {
|
||||
// 初始化 conversationId
|
||||
localeConversationId.value =
|
||||
props.conversationId && props.conversationId.length > 0
|
||||
? props.conversationId
|
||||
: await getConversationId();
|
||||
getPresetQuestions();
|
||||
});
|
||||
watchEffect(async () => {
|
||||
if (props.bot && props.conversationId) {
|
||||
const [, res] = await tryit(getMessageList)({
|
||||
conversationId: props.conversationId,
|
||||
botId: props.bot.id,
|
||||
tempUserId: uuid() + props.bot.id,
|
||||
});
|
||||
|
||||
if (res?.errorCode === 0) {
|
||||
bubbleItems.value = res.data.map((item) => ({
|
||||
...item,
|
||||
content:
|
||||
item.role === 'assistant'
|
||||
? item.content.replace(/^Final Answer:\s*/i, '')
|
||||
: item.content,
|
||||
placement: item.role === 'assistant' ? 'start' : 'end',
|
||||
}));
|
||||
}
|
||||
} else {
|
||||
bubbleItems.value = [];
|
||||
}
|
||||
});
|
||||
const lastUserMessage = ref('');
|
||||
const messages = ref<historyMessageType[]>([]);
|
||||
const stopSse = () => {
|
||||
sseClient.abort();
|
||||
sending.value = false;
|
||||
const lastBubbleItem = bubbleItems.value[bubbleItems.value.length - 1];
|
||||
if (lastBubbleItem) {
|
||||
bubbleItems.value[bubbleItems.value.length - 1] = {
|
||||
...lastBubbleItem,
|
||||
content: lastBubbleItem.content,
|
||||
loading: false,
|
||||
typing: false,
|
||||
};
|
||||
}
|
||||
};
|
||||
const handleSubmit = async (refreshContent: string) => {
|
||||
const currentPrompt = refreshContent || senderValue.value.trim();
|
||||
if (!currentPrompt) {
|
||||
return;
|
||||
}
|
||||
sending.value = true;
|
||||
lastUserMessage.value = currentPrompt;
|
||||
messages.value.push({
|
||||
role: 'user',
|
||||
content: currentPrompt,
|
||||
});
|
||||
const copyMessages = [...messages.value];
|
||||
const data = {
|
||||
botId: botId.value,
|
||||
prompt: currentPrompt,
|
||||
conversationId: localeConversationId.value,
|
||||
messages: copyMessages,
|
||||
};
|
||||
messages.value.pop();
|
||||
const mockMessages = generateMockMessages(refreshContent);
|
||||
bubbleItems.value.push(...mockMessages);
|
||||
senderRef.value?.clear();
|
||||
sseClient.post('/api/v1/bot/chat', data, {
|
||||
onMessage(message) {
|
||||
const event = message.event;
|
||||
const lastBubbleItem = bubbleItems.value[bubbleItems.value.length - 1];
|
||||
|
||||
// finish
|
||||
if (event === 'done') {
|
||||
sending.value = false;
|
||||
return;
|
||||
}
|
||||
if (!message.data) {
|
||||
return;
|
||||
}
|
||||
// 处理系统错误
|
||||
const sseData = JSON.parse(message.data);
|
||||
if (
|
||||
sseData?.domain === 'SYSTEM' &&
|
||||
sseData.payload?.code === 'SYSTEM_ERROR'
|
||||
) {
|
||||
const errorMessage = sseData.payload.message;
|
||||
const lastBubbleItem = bubbleItems.value[bubbleItems.value.length - 1];
|
||||
if (!lastBubbleItem) return;
|
||||
bubbleItems.value[bubbleItems.value.length - 1] = {
|
||||
...lastBubbleItem,
|
||||
content: errorMessage,
|
||||
loading: false,
|
||||
typing: true,
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
// 处理流式消息
|
||||
const delta = sseData.payload?.delta;
|
||||
const role = sseData.payload?.role;
|
||||
if (lastBubbleItem && delta) {
|
||||
if (delta === lastBubbleItem.content) {
|
||||
sending.value = false;
|
||||
} else {
|
||||
bubbleItems.value[bubbleItems.value.length - 1] = {
|
||||
...lastBubbleItem,
|
||||
content: lastBubbleItem.content + delta,
|
||||
loading: false,
|
||||
typing: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
// 是否需要保存聊天记录
|
||||
if (event === 'needSaveMessage') {
|
||||
messages.value.push({
|
||||
role,
|
||||
content: sseData.payload?.content,
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
const handleComplete = (_: TypewriterInstance, index: number) => {
|
||||
if (
|
||||
index === bubbleItems.value.length - 1 &&
|
||||
props.conversationId &&
|
||||
props.conversationId.length <= 0 &&
|
||||
sending.value === false
|
||||
) {
|
||||
setTimeout(() => {
|
||||
router.replace({
|
||||
params: { conversationId: localeConversationId.value },
|
||||
});
|
||||
}, 100);
|
||||
}
|
||||
};
|
||||
|
||||
const generateMockMessages = (refreshContent: string) => {
|
||||
const userMessage: ChatMessage = {
|
||||
role: 'user',
|
||||
id: Date.now().toString(),
|
||||
fileList: [],
|
||||
content: refreshContent || senderValue.value,
|
||||
created: Date.now(),
|
||||
updateAt: Date.now(),
|
||||
placement: 'end',
|
||||
};
|
||||
|
||||
const assistantMessage: ChatMessage = {
|
||||
role: 'assistant',
|
||||
id: Date.now().toString(),
|
||||
content: '',
|
||||
loading: true,
|
||||
created: Date.now(),
|
||||
updateAt: Date.now(),
|
||||
placement: 'start',
|
||||
};
|
||||
|
||||
return [userMessage, assistantMessage];
|
||||
};
|
||||
|
||||
const handleCopy = (content: string) => {
|
||||
navigator.clipboard
|
||||
.writeText(content)
|
||||
.then(() => ElMessage.success($t('message.copySuccess')))
|
||||
.catch(() => ElMessage.error($t('message.copyFail')));
|
||||
};
|
||||
|
||||
const handleRefresh = () => {
|
||||
handleSubmit(lastUserMessage.value);
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="mx-auto h-full max-w-[780px]">
|
||||
<div
|
||||
:class="
|
||||
cn(
|
||||
'flex h-full w-full flex-col gap-3',
|
||||
!localeConversationId && 'items-center justify-center gap-8',
|
||||
)
|
||||
"
|
||||
>
|
||||
<!-- 对话列表 -->
|
||||
<div
|
||||
v-if="localeConversationId || bubbleItems.length > 0"
|
||||
class="message-container w-full flex-1 overflow-hidden"
|
||||
>
|
||||
<el-bubble-list
|
||||
class="!h-full"
|
||||
:list="bubbleItems"
|
||||
max-height="none"
|
||||
@complete="handleComplete"
|
||||
>
|
||||
<template #header="{ item }">
|
||||
<span class="chat-bubble-item-time-style">
|
||||
{{ new Date(item.created).toLocaleString() }}
|
||||
</span>
|
||||
</template>
|
||||
<!-- 自定义头像 -->
|
||||
<template #avatar="{ item }">
|
||||
<BotAvatar
|
||||
v-if="item.role === 'assistant'"
|
||||
:src="bot?.icon"
|
||||
:size="40"
|
||||
/>
|
||||
</template>
|
||||
<template #content="{ item }">
|
||||
<XMarkdown :markdown="item.content" />
|
||||
</template>
|
||||
<!-- 自定义底部 -->
|
||||
<template #footer="{ item }">
|
||||
<ElSpace :size="10">
|
||||
<ElSpace>
|
||||
<span @click="handleRefresh()" style="cursor: pointer">
|
||||
<ElIcon>
|
||||
<RefreshRight />
|
||||
</ElIcon>
|
||||
</span>
|
||||
<span @click="handleCopy(item.content)" style="cursor: pointer">
|
||||
<ElIcon>
|
||||
<CopyDocument />
|
||||
</ElIcon>
|
||||
</span>
|
||||
</ElSpace>
|
||||
</ElSpace>
|
||||
</template>
|
||||
</el-bubble-list>
|
||||
</div>
|
||||
|
||||
<!-- 新对话显示bot信息 -->
|
||||
<div v-else class="flex flex-col items-center gap-3.5">
|
||||
<BotAvatar :src="bot?.icon" :size="88" />
|
||||
<h1 class="text-base font-medium text-black/85">
|
||||
{{ bot?.title }}
|
||||
</h1>
|
||||
<span class="text-sm text-[#757575]">{{ bot?.description }}</span>
|
||||
</div>
|
||||
|
||||
<!--问题预设-->
|
||||
<div
|
||||
class="questions-preset-container"
|
||||
v-if="botStore.presetQuestions.length > 0"
|
||||
>
|
||||
<ElButton
|
||||
v-for="item in getPerQuestions(botStore.presetQuestions)"
|
||||
:key="item.key"
|
||||
@click="handleSubmit(item.description)"
|
||||
>
|
||||
{{ item.description }}
|
||||
</ElButton>
|
||||
</div>
|
||||
<!-- Sender -->
|
||||
<el-sender
|
||||
ref="senderRef"
|
||||
class="w-full"
|
||||
v-model="senderValue"
|
||||
:placeholder="$t('message.pleaseInputContent')"
|
||||
variant="updown"
|
||||
:auto-size="{ minRows: 3, maxRows: 6 }"
|
||||
allow-speech
|
||||
@submit="handleSubmit"
|
||||
>
|
||||
<!-- 自定义头部内容 -->
|
||||
<!-- <template #header>
|
||||
<div class="header-self-wrap">
|
||||
<SenderHeader ref="senderHeader" />
|
||||
</div>
|
||||
</template>-->
|
||||
|
||||
<template #action-list>
|
||||
<ElSpace>
|
||||
<!--<ElButton circle @click="uploadRef.triggerFileSelect()">
|
||||
<ElIcon><Paperclip /></ElIcon>
|
||||
</ElButton>
|
||||
<ElButton circle>
|
||||
<ElIcon><Microphone /></ElIcon>
|
||||
<!– <ElIcon color="#0066FF"><RecordingIcon /></ElIcon> –>
|
||||
</ElButton>-->
|
||||
<ElButton v-if="sending" circle @click="stopSse">
|
||||
<ElIcon size="30" color="#409eff"><SendingIcon /></ElIcon>
|
||||
</ElButton>
|
||||
<template v-else>
|
||||
<ElButton v-if="!senderValue" circle disabled>
|
||||
<SendIcon />
|
||||
</ElButton>
|
||||
<ElButton v-else circle @click="handleSubmit('')">
|
||||
<SendEnableIcon />
|
||||
</ElButton>
|
||||
</template>
|
||||
</ElSpace>
|
||||
</template>
|
||||
</el-sender>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.questions-preset-container {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
width: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.message-container {
|
||||
padding: 8px;
|
||||
background-color: var(--bot-chat-message-container);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
:deep(.el-bubble-content-wrapper .el-bubble-content-filled[data-v-a52d8fe0]) {
|
||||
background-color: var(--bot-chat-message-item-back);
|
||||
}
|
||||
|
||||
.chat-bubble-item-time-style {
|
||||
font-size: 12px;
|
||||
color: var(--common-font-placeholder-color);
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,309 @@
|
|||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
|
||||
interface AccordionItem {
|
||||
id: number;
|
||||
title: string;
|
||||
content: string;
|
||||
isOpen: boolean;
|
||||
}
|
||||
|
||||
const accordionData = ref<AccordionItem[]>([
|
||||
{
|
||||
id: 1,
|
||||
title: 'Vue.js 简介',
|
||||
content:
|
||||
'Vue.js 是一套用于构建用户界面的渐进式框架。与其它大型框架不同的是,Vue 被设计为可以自底向上逐层应用。',
|
||||
isOpen: true,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: 'Composition API',
|
||||
content:
|
||||
'Composition API 是 Vue 3 中引入的一组 API,允许您使用函数而不是通过选项来组织组件的逻辑。',
|
||||
isOpen: false,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: '响应式原理',
|
||||
content:
|
||||
'Vue 3 使用 Proxy 对象来实现响应式,相比 Vue 2 的 Object.defineProperty,Proxy 可以监听动态添加的属性。',
|
||||
isOpen: false,
|
||||
},
|
||||
]);
|
||||
|
||||
const allowMultiple = ref(false);
|
||||
|
||||
const togglePanel = (index: number) => {
|
||||
const item = accordionData.value[index];
|
||||
|
||||
if (!item) return;
|
||||
|
||||
if (!allowMultiple.value) {
|
||||
accordionData.value.forEach((otherItem, i) => {
|
||||
if (i !== index && otherItem) {
|
||||
otherItem.isOpen = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
item.isOpen = !item.isOpen;
|
||||
};
|
||||
|
||||
const expandAll = () => {
|
||||
accordionData.value.forEach((item) => {
|
||||
if (item) {
|
||||
item.isOpen = true;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const collapseAll = () => {
|
||||
accordionData.value.forEach((item) => {
|
||||
if (item) {
|
||||
item.isOpen = false;
|
||||
}
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="accordion-container">
|
||||
<h1 class="title">Vue3 折叠面板</h1>
|
||||
<p class="subtitle">使用 Vue3 Composition API 实现</p>
|
||||
|
||||
<!-- 控制面板 -->
|
||||
<div class="controls">
|
||||
<div class="control-group">
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" v-model="allowMultiple" class="checkbox" />
|
||||
允许多个同时展开
|
||||
</label>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<button @click="expandAll" class="control-btn">展开全部</button>
|
||||
<button @click="collapseAll" class="control-btn">收起全部</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 折叠面板列表 -->
|
||||
<div class="accordion-list">
|
||||
<div
|
||||
v-for="(item, index) in accordionData"
|
||||
:key="item.id"
|
||||
class="accordion-item"
|
||||
:class="{ 'accordion-item--active': item.isOpen }"
|
||||
>
|
||||
<!-- 面板头部 -->
|
||||
<div class="accordion-header" @click="togglePanel(index)">
|
||||
<div class="column-header-container">
|
||||
<div
|
||||
class="accordion-icon"
|
||||
:class="{ 'accordion-icon--rotated': item.isOpen }"
|
||||
>
|
||||
▼
|
||||
</div>
|
||||
<h3 class="accordion-title">{{ item.title }}</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 面板内容 -->
|
||||
<div
|
||||
class="accordion-content"
|
||||
:class="{ 'accordion-content--open': item.isOpen }"
|
||||
>
|
||||
<div class="accordion-content-inner">
|
||||
<p>{{ item.content }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.accordion-container {
|
||||
max-width: 100%;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
}
|
||||
|
||||
.title {
|
||||
text-align: center;
|
||||
color: var(--el-text-color-secondary);
|
||||
margin-bottom: 8px;
|
||||
font-size: 2rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
text-align: center;
|
||||
color: var(--el-text-color-secondary);
|
||||
margin-bottom: 30px;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
/* 控制面板样式 */
|
||||
.controls {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 30px;
|
||||
padding: 20px;
|
||||
background: var(--el-bg-color);
|
||||
border-radius: 8px;
|
||||
border: 1px solid #e9ecef;
|
||||
}
|
||||
|
||||
.control-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.checkbox-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 14px;
|
||||
color: var(--el-text-color-secondary);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.checkbox {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.control-btn {
|
||||
padding: 8px 16px;
|
||||
border: 1px solid #3498db;
|
||||
background: var(--el-bg-color);
|
||||
color: var(--el-text-color-secondary);
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.control-btn:hover {
|
||||
background: #3498db;
|
||||
background: var(--el-color-primary-light-9);
|
||||
}
|
||||
|
||||
/* 折叠面板列表 */
|
||||
.accordion-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.accordion-item {
|
||||
border: 1px solid #e1e5e9;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
background: var(--el-bg-color);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.accordion-item:hover {
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.accordion-item--active {
|
||||
border-color: #3498db;
|
||||
}
|
||||
|
||||
/* 面板头部 */
|
||||
.accordion-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px 20px;
|
||||
background: var(--el-bg-color);
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s ease;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.accordion-header:hover {
|
||||
background: var(--el-color-primary-light-9);
|
||||
}
|
||||
|
||||
.accordion-title {
|
||||
margin: 0;
|
||||
font-size: 1.1rem;
|
||||
font-weight: 500;
|
||||
color: var(--el-text-color-secondary);
|
||||
padding-left: 12px;
|
||||
}
|
||||
|
||||
.accordion-icon {
|
||||
transition: transform 0.3s ease;
|
||||
color: #7f8c8d;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.accordion-icon--rotated {
|
||||
transform: rotate(180deg);
|
||||
color: #3498db;
|
||||
}
|
||||
|
||||
.accordion-content {
|
||||
max-height: 0;
|
||||
overflow: hidden;
|
||||
transition: max-height 0.4s ease;
|
||||
background: var(--el-bg-color);
|
||||
}
|
||||
|
||||
.accordion-content--open {
|
||||
max-height: 200px;
|
||||
}
|
||||
|
||||
.accordion-content-inner {
|
||||
padding: 20px;
|
||||
border-top: 1px solid #e1e5e9;
|
||||
}
|
||||
|
||||
.accordion-content-inner p {
|
||||
margin: 0;
|
||||
line-height: 1.6;
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 14px;
|
||||
}
|
||||
.column-header-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.accordion-container {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.controls {
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.control-group {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.accordion-header {
|
||||
padding: 14px 16px;
|
||||
}
|
||||
|
||||
.accordion-title {
|
||||
font-size: 1rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,119 @@
|
|||
<script setup lang="ts">
|
||||
import { $t } from '@aiflowy/locales';
|
||||
|
||||
import { Delete } from '@element-plus/icons-vue';
|
||||
import { ElAvatar, ElIcon, ElMessageBox } from 'element-plus';
|
||||
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: Array as any,
|
||||
default: () => [],
|
||||
},
|
||||
titleKey: {
|
||||
type: String,
|
||||
default: 'title',
|
||||
},
|
||||
descriptionKey: {
|
||||
type: String,
|
||||
default: 'description',
|
||||
},
|
||||
});
|
||||
const emits = defineEmits(['delete']);
|
||||
const handleDelete = (item: any) => {
|
||||
ElMessageBox.confirm($t('message.deleteAlert'), $t('message.noticeTitle'), {
|
||||
confirmButtonText: $t('button.confirm'),
|
||||
cancelButtonText: $t('button.cancel'),
|
||||
type: 'warning',
|
||||
}).then(() => {
|
||||
emits('delete', item);
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="collapse-item-container">
|
||||
<div
|
||||
v-for="(item, index) in props.data"
|
||||
:key="index"
|
||||
class="el-list-item-max-container"
|
||||
>
|
||||
<div class="el-list-item-container">
|
||||
<div class="flex-center">
|
||||
<ElAvatar :src="item.icon" v-if="item.icon" />
|
||||
<ElAvatar v-else src="/favicon.png" shape="circle" />
|
||||
</div>
|
||||
<div class="el-list-item-content">
|
||||
<div class="title">{{ item[titleKey] }}</div>
|
||||
<div class="description">{{ item[descriptionKey] }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<ElIcon
|
||||
color="var(--el-color-danger)"
|
||||
size="20px"
|
||||
@click="handleDelete(item)"
|
||||
class="el-list-item-delete-container"
|
||||
>
|
||||
<Delete />
|
||||
</ElIcon>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.el-list-item-max-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 12px 24px 12px 12px;
|
||||
background-color: var(--el-bg-color);
|
||||
border-radius: 8px;
|
||||
}
|
||||
.collapse-item-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
background-color: #f5f5f5;
|
||||
padding: 10px;
|
||||
}
|
||||
.el-list-item-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
.el-list-item-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
.el-list-item-delete-container {
|
||||
cursor: pointer;
|
||||
}
|
||||
.title {
|
||||
font-family:
|
||||
PingFangSC,
|
||||
PingFang SC,
|
||||
sans-serif;
|
||||
font-weight: 500;
|
||||
font-size: 12px;
|
||||
color: rgba(0, 0, 0, 0.85);
|
||||
line-height: 24px;
|
||||
text-align: left;
|
||||
font-style: normal;
|
||||
text-transform: none;
|
||||
}
|
||||
.description {
|
||||
font-family:
|
||||
PingFangSC,
|
||||
PingFang SC,
|
||||
sans-serif;
|
||||
font-weight: 400;
|
||||
font-size: 12px;
|
||||
color: rgba(0, 0, 0, 0.45);
|
||||
line-height: 22px;
|
||||
text-align: left;
|
||||
font-style: normal;
|
||||
text-transform: none;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,471 @@
|
|||
<script setup lang="ts">
|
||||
import type { PropType } from 'vue';
|
||||
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { $t } from '@aiflowy/locales';
|
||||
|
||||
import {
|
||||
ElAvatar,
|
||||
ElButton,
|
||||
ElCheckbox,
|
||||
ElCollapse,
|
||||
ElCollapseItem,
|
||||
ElDialog,
|
||||
ElText,
|
||||
} from 'element-plus';
|
||||
|
||||
import HeaderSearch from '#/components/headerSearch/HeaderSearch.vue';
|
||||
import PageData from '#/components/page/PageData.vue';
|
||||
|
||||
interface SelectedMcpTool {
|
||||
name: string;
|
||||
description: string;
|
||||
}
|
||||
const props = defineProps({
|
||||
title: { type: String, default: '' },
|
||||
width: { type: String, default: '80%' },
|
||||
extraQueryParams: { type: Object, default: () => ({}) },
|
||||
searchParams: {
|
||||
type: Array as PropType<string[]>,
|
||||
default: () => [],
|
||||
},
|
||||
titleKey: { type: String, default: 'name' },
|
||||
pageUrl: { type: String, default: '' },
|
||||
hasParent: { type: Boolean, default: false },
|
||||
isSelectMcp: { type: Boolean, default: false },
|
||||
});
|
||||
|
||||
const emit = defineEmits(['getData']);
|
||||
const dialogVisible = ref(false);
|
||||
const pageDataRef = ref();
|
||||
const loading = ref(false);
|
||||
const selectedIds = ref<(number | string)[]>([]);
|
||||
// 存储上一级id与选中tool.name的关联关系
|
||||
const selectedToolMap = ref<Record<number | string, SelectedMcpTool[]>>({});
|
||||
defineExpose({
|
||||
openDialog(defaultSelectedIds: (number | string)[]) {
|
||||
selectedIds.value = defaultSelectedIds ? [...defaultSelectedIds] : [];
|
||||
dialogVisible.value = true;
|
||||
},
|
||||
/**
|
||||
* MCP专属弹窗打开方法(适配MCP回显,传递格式化后的MCP数据)
|
||||
* @param selectMcpMap - MCP已选数据映射(键:MCP父级ID,值:工具名称+描述数组)
|
||||
*/
|
||||
openMcpDialog(selectMcpMap: Record<number | string, SelectedMcpTool[]>) {
|
||||
selectedIds.value = [];
|
||||
selectedToolMap.value = structuredClone(selectMcpMap);
|
||||
dialogVisible.value = true;
|
||||
},
|
||||
});
|
||||
const isSelected = (id: number | string) => {
|
||||
return selectedIds.value.includes(id);
|
||||
};
|
||||
|
||||
const isSelectedMcp = (parentId: number | string, toolName: string) => {
|
||||
// 查找当前parentId下是否存在该tool.name的工具
|
||||
return !!selectedToolMap.value[parentId]?.some(
|
||||
(tool) => tool.name === toolName,
|
||||
);
|
||||
};
|
||||
|
||||
const toggleSelectionMcp = (
|
||||
mcpId: number | string,
|
||||
toolName: string,
|
||||
toolDescription: string,
|
||||
checked: any,
|
||||
) => {
|
||||
if (checked) {
|
||||
if (!selectedToolMap.value[mcpId]) {
|
||||
selectedToolMap.value[mcpId] = []; // 初始化空数组
|
||||
}
|
||||
const isExisted = selectedToolMap.value[mcpId]?.some(
|
||||
(tool) => tool.name === toolName,
|
||||
);
|
||||
if (!isExisted) {
|
||||
selectedToolMap.value[mcpId]?.push({
|
||||
name: toolName,
|
||||
description: toolDescription,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
if (selectedToolMap.value[mcpId]) {
|
||||
selectedToolMap.value[mcpId] = selectedToolMap.value[mcpId].filter(
|
||||
(tool) => tool.name !== toolName,
|
||||
);
|
||||
if (selectedToolMap.value[mcpId].length === 0) {
|
||||
delete selectedToolMap.value[mcpId];
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const toggleSelection = (id: number | string, checked: any) => {
|
||||
if (checked) {
|
||||
selectedIds.value.push(id);
|
||||
} else {
|
||||
selectedIds.value = selectedIds.value.filter((i) => i !== id);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 封装:获取MCP选中的结构化信息(包含name和description)
|
||||
* @returns {Record<number | string, string[][]>[]} 符合要求的数据:[{ 上一级id: [[name1, description1], [name2, description2]] }]
|
||||
*/
|
||||
const getMcpSelectedInfo = (): Record<number | string, string[][]>[] => {
|
||||
const mcpSelectedResult: Record<number | string, string[][]>[] = [];
|
||||
|
||||
Object.entries(selectedToolMap.value).forEach(([parentId, selectedTools]) => {
|
||||
// 转换每个工具为 [name, description] 一维数组
|
||||
const formattedToolList: string[][] = selectedTools.map((tool) => [
|
||||
tool.name,
|
||||
tool.description,
|
||||
]);
|
||||
|
||||
mcpSelectedResult.push({
|
||||
[parentId]: formattedToolList,
|
||||
});
|
||||
});
|
||||
|
||||
return mcpSelectedResult;
|
||||
};
|
||||
|
||||
const handleSubmitRun = () => {
|
||||
if (props?.isSelectMcp) {
|
||||
emit('getData', getMcpSelectedInfo());
|
||||
} else {
|
||||
emit('getData', selectedIds.value);
|
||||
}
|
||||
dialogVisible.value = false;
|
||||
return selectedIds.value;
|
||||
};
|
||||
|
||||
const handleSearch = (query: string) => {
|
||||
const tempParams = {} as Record<string, string>;
|
||||
props.searchParams.forEach((paramName) => {
|
||||
tempParams[paramName] = query;
|
||||
});
|
||||
|
||||
pageDataRef.value?.setQuery({
|
||||
isQueryOr: true,
|
||||
...tempParams,
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ElDialog
|
||||
v-model="dialogVisible"
|
||||
draggable
|
||||
:close-on-click-modal="false"
|
||||
:width="props.width"
|
||||
align-center
|
||||
>
|
||||
<template #header>
|
||||
<div>
|
||||
<p class="el-dialog__title mb-4">{{ props.title }}</p>
|
||||
<HeaderSearch @search="handleSearch" />
|
||||
</div>
|
||||
</template>
|
||||
<div class="select-modal-container p-5">
|
||||
<PageData
|
||||
ref="pageDataRef"
|
||||
:page-url="pageUrl"
|
||||
:page-size="10"
|
||||
:extra-query-params="extraQueryParams"
|
||||
>
|
||||
<template #default="{ pageList }">
|
||||
<template v-if="hasParent">
|
||||
<div class="container-second">
|
||||
<ElCollapse
|
||||
accordion
|
||||
v-for="(item, index) in pageList"
|
||||
:key="index"
|
||||
>
|
||||
<ElCollapseItem>
|
||||
<template #title="{ isActive }">
|
||||
<div
|
||||
class="title-wrapper"
|
||||
:class="[{ 'is-active': isActive }]"
|
||||
>
|
||||
<div>
|
||||
<ElAvatar :src="item.icon" v-if="item.icon" />
|
||||
<ElAvatar v-else src="/favicon.png" shape="circle" />
|
||||
</div>
|
||||
<div class="title-right-container">
|
||||
<ElText truncated class="title">
|
||||
{{ item[titleKey] }}
|
||||
</ElText>
|
||||
<div class="desc">{{ item.description }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<!--选择插件-->
|
||||
<div v-if="!isSelectMcp">
|
||||
<div v-for="tool in item.tools" :key="tool.id">
|
||||
<div class="content-title-wrapper">
|
||||
<div class="content-left-container">
|
||||
<div class="title-right-container">
|
||||
<ElText truncated class="title">
|
||||
{{ tool.name }}
|
||||
</ElText>
|
||||
<div class="desc">{{ tool.description }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<ElCheckbox
|
||||
:model-value="isSelected(tool.id)"
|
||||
@change="(val) => toggleSelection(tool.id, val)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!--选择MCP-->
|
||||
<div v-if="isSelectMcp">
|
||||
<div v-for="tool in item.tools" :key="tool.name">
|
||||
<div class="content-title-wrapper">
|
||||
<div class="content-left-container">
|
||||
<div class="title-right-container">
|
||||
<ElText truncated class="title">
|
||||
{{ tool.name }}
|
||||
</ElText>
|
||||
<div class="desc">{{ tool.description }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<ElCheckbox
|
||||
:model-value="isSelectedMcp(item.id, tool.name)"
|
||||
@change="
|
||||
(val) =>
|
||||
toggleSelectionMcp(
|
||||
item.id,
|
||||
tool.name,
|
||||
tool.description,
|
||||
val,
|
||||
)
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ElCollapseItem>
|
||||
</ElCollapse>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="container-second">
|
||||
<div v-for="(item, index) in pageList" :key="index">
|
||||
<div class="content-title-wrapper">
|
||||
<div class="content-sec-left-container">
|
||||
<div>
|
||||
<ElAvatar :src="item.icon" v-if="item.icon" />
|
||||
<ElAvatar v-else src="/favicon.png" shape="circle" />
|
||||
</div>
|
||||
<div class="title-sec-right-container">
|
||||
<ElText truncated class="title">
|
||||
{{ item.title }}
|
||||
</ElText>
|
||||
<div class="desc">{{ item.description }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<ElCheckbox
|
||||
:model-value="isSelected(item.id)"
|
||||
@change="(val) => toggleSelection(item.id, val)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
</PageData>
|
||||
</div>
|
||||
<template #footer>
|
||||
<ElButton @click="dialogVisible = false">
|
||||
{{ $t('button.cancel') }}
|
||||
</ElButton>
|
||||
<ElButton type="primary" @click="handleSubmitRun" :loading="loading">
|
||||
{{ $t('button.confirm') }}
|
||||
</ElButton>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.select-modal-container {
|
||||
/* height: 100%;
|
||||
overflow: auto; */
|
||||
background-color: var(--bot-select-data-item-back);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.title-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.content-title-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 113px;
|
||||
padding: 20px 50px 20px 20px;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
background-color: var(--el-bg-color);
|
||||
border: 1px solid hsl(var(--border));
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-family: PingFangSC, 'PingFang SC';
|
||||
font-size: 16px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
line-height: 24px;
|
||||
color: rgb(0 0 0 / 85%);
|
||||
text-align: left;
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
.content-left-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.content-sec-left-container {
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.desc {
|
||||
display: -webkit-box;
|
||||
width: 100%;
|
||||
|
||||
/* height: 42px;
|
||||
min-height: 42px; */
|
||||
margin-top: 12px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
-webkit-line-clamp: 2;
|
||||
font-family: PingFangSC, 'PingFang SC';
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 22px;
|
||||
color: rgb(0 0 0 / 45%);
|
||||
text-align: left;
|
||||
text-transform: none;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
|
||||
.title-right-container {
|
||||
/* display: flex;
|
||||
flex-direction: column;
|
||||
align-items: first baseline;
|
||||
justify-content: center; */
|
||||
padding-right: 10px;
|
||||
margin-left: 10px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.title-sec-right-container {
|
||||
/* display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start; */
|
||||
padding-right: 10px;
|
||||
margin-left: 10px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.container-second {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
|
||||
/* padding: 20px 20px; */
|
||||
}
|
||||
|
||||
.select-modal-container
|
||||
:deep(.el-collapse-item__header .el-collapse-item__arrow) {
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.select-modal-container
|
||||
:deep(.el-collapse-item.is-active .el-collapse-item__arrow) {
|
||||
color: #1976d2;
|
||||
}
|
||||
|
||||
.select-modal-container
|
||||
:deep(.el-collapse-item__content)
|
||||
.content-title-wrapper:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.select-modal-container :deep(.el-collapse-item__header) {
|
||||
height: auto;
|
||||
padding: 12px;
|
||||
line-height: normal;
|
||||
color: #333;
|
||||
background-color: #fff !important;
|
||||
}
|
||||
|
||||
.select-modal-container :deep(.el-collapse-item__header:hover) {
|
||||
background-color: #fff !important;
|
||||
border-color: #e4e7ed;
|
||||
}
|
||||
|
||||
.select-modal-container
|
||||
:deep(.el-collapse-item.is-active .el-collapse-item__header) {
|
||||
color: #1976d2;
|
||||
background-color: #fff !important;
|
||||
border: none;
|
||||
border-bottom-color: transparent;
|
||||
}
|
||||
|
||||
.select-modal-container :deep(.el-collapse-item__content) {
|
||||
padding: 12px;
|
||||
background-color: #fff !important;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.select-modal-container :deep(.el-collapse-item__wrap) {
|
||||
background-color: #fff;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.select-modal-container :deep(.el-collapse-item) {
|
||||
margin-bottom: 8px;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.select-modal-container
|
||||
:deep(.el-collapse-item__content)
|
||||
.content-title-wrapper {
|
||||
margin-top: 12px;
|
||||
margin-bottom: 8px;
|
||||
background-color: #f9fafc;
|
||||
border: 1px solid hsl(var(--border));
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.select-modal-container
|
||||
:deep(.el-collapse-item__content)
|
||||
.content-title-wrapper:hover {
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
.select-modal-container :deep(.el-collapse) {
|
||||
overflow: hidden;
|
||||
background-color: #fff;
|
||||
border: 1px solid hsl(var(--border));
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.select-modal-container :deep(.el-checkbox__inner) {
|
||||
--el-checkbox-input-border: 1px solid #c7c7c7;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,281 @@
|
|||
<script setup lang="ts">
|
||||
import type { CronItemState } from './CronTabPane.vue';
|
||||
|
||||
import { computed, reactive, ref } from 'vue';
|
||||
|
||||
import {
|
||||
ElButton,
|
||||
ElCard,
|
||||
ElDivider,
|
||||
ElInput,
|
||||
ElTable,
|
||||
ElTableColumn,
|
||||
ElTabPane,
|
||||
ElTabs,
|
||||
} from 'element-plus';
|
||||
|
||||
import { api } from '#/api/request';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import CrontabPane from './CronTabPane.vue';
|
||||
|
||||
const emit = defineEmits(['useCron']);
|
||||
const activeTab = ref('second');
|
||||
|
||||
// 默认状态工厂函数
|
||||
const defaultState = (min: number, _: number): CronItemState => ({
|
||||
type: 'every',
|
||||
rangeStart: min,
|
||||
rangeEnd: min + 1,
|
||||
loopStart: min,
|
||||
loopStep: 1,
|
||||
specificList: [],
|
||||
});
|
||||
|
||||
const state = reactive({
|
||||
second: defaultState(0, 59),
|
||||
minute: defaultState(0, 59),
|
||||
hour: defaultState(0, 23),
|
||||
day: { ...defaultState(1, 31), type: 'every' } as CronItemState,
|
||||
month: defaultState(1, 12),
|
||||
week: { ...defaultState(1, 7), type: 'none' } as CronItemState, // 默认为?
|
||||
});
|
||||
|
||||
const weekAlias: Record<number, string> = {
|
||||
1: $t('common.Sun'),
|
||||
2: $t('common.Mon'),
|
||||
3: $t('common.Tue'),
|
||||
4: $t('common.Wed'),
|
||||
5: $t('common.Thu'),
|
||||
6: $t('common.Fri'),
|
||||
7: $t('common.Sat'),
|
||||
};
|
||||
|
||||
// 核心:格式化单个字段
|
||||
const formatItem = (item: CronItemState): string => {
|
||||
switch (item.type) {
|
||||
case 'every': {
|
||||
return '*';
|
||||
}
|
||||
case 'loop': {
|
||||
return `${item.loopStart}/${item.loopStep}`;
|
||||
}
|
||||
case 'none': {
|
||||
return '?';
|
||||
}
|
||||
case 'range': {
|
||||
return `${item.rangeStart}-${item.rangeEnd}`;
|
||||
}
|
||||
case 'specific': {
|
||||
if (item.specificList.length === 0) return '*';
|
||||
return item.specificList.sort((a, b) => a - b).join(',');
|
||||
}
|
||||
default: {
|
||||
return '*';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 互斥逻辑
|
||||
const handleDayChange = () => {
|
||||
if (state.day.type !== 'none') {
|
||||
state.week.type = 'none';
|
||||
}
|
||||
};
|
||||
|
||||
const handleWeekChange = () => {
|
||||
if (state.week.type !== 'none') {
|
||||
state.day.type = 'none';
|
||||
}
|
||||
};
|
||||
|
||||
// 计算最终 Cron 字符串
|
||||
const cronResult = computed(() => {
|
||||
const s = formatItem(state.second);
|
||||
const m = formatItem(state.minute);
|
||||
const h = formatItem(state.hour);
|
||||
const d = formatItem(state.day);
|
||||
const M = formatItem(state.month);
|
||||
const w = formatItem(state.week);
|
||||
return `${s} ${m} ${h} ${d} ${M} ${w}`;
|
||||
});
|
||||
|
||||
// 表格展示数据
|
||||
const resultTableData = computed(() => [
|
||||
{
|
||||
second: formatItem(state.second),
|
||||
minute: formatItem(state.minute),
|
||||
hour: formatItem(state.hour),
|
||||
day: formatItem(state.day),
|
||||
month: formatItem(state.month),
|
||||
week: formatItem(state.week),
|
||||
},
|
||||
]);
|
||||
|
||||
const copyCron = () => {
|
||||
// if (navigator.clipboard) {
|
||||
// navigator.clipboard.writeText(cronResult.value).then(() => {
|
||||
// ElMessage.success('Cron 表达式已复制');
|
||||
// });
|
||||
// } else {
|
||||
// ElMessage.warning('浏览器不支持剪贴板 API');
|
||||
// }
|
||||
emit('useCron', cronResult.value);
|
||||
};
|
||||
const nextTimes = ref<any[]>([]);
|
||||
function getNextTimes() {
|
||||
api
|
||||
.get('/api/v1/sysJob/getNextTimes', {
|
||||
params: {
|
||||
cronExpression: cronResult.value,
|
||||
},
|
||||
})
|
||||
.then((res: any) => {
|
||||
nextTimes.value = res.errorCode === 0 ? res.data : [];
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="cron-generator">
|
||||
<ElCard class="box-card" shadow="never">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>{{ $t('cron.cronExpressionGenerator') }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<ElTabs v-model="activeTab" type="border-card">
|
||||
<!-- 秒 -->
|
||||
<ElTabPane :label="$t('common.Second')" name="second">
|
||||
<CrontabPane
|
||||
v-model="state.second"
|
||||
:min="0"
|
||||
:max="59"
|
||||
:label="$t('common.Second')"
|
||||
/>
|
||||
</ElTabPane>
|
||||
|
||||
<!-- 分 -->
|
||||
<ElTabPane :label="$t('common.Min')" name="minute">
|
||||
<CrontabPane
|
||||
v-model="state.minute"
|
||||
:min="0"
|
||||
:max="59"
|
||||
:label="$t('common.Min')"
|
||||
/>
|
||||
</ElTabPane>
|
||||
|
||||
<!-- 时 -->
|
||||
<ElTabPane :label="$t('common.Hour')" name="hour">
|
||||
<CrontabPane
|
||||
v-model="state.hour"
|
||||
:min="0"
|
||||
:max="23"
|
||||
:label="$t('common.Hour')"
|
||||
/>
|
||||
</ElTabPane>
|
||||
|
||||
<!-- 日 -->
|
||||
<ElTabPane :label="$t('common.Day')" name="day">
|
||||
<CrontabPane
|
||||
v-model="state.day"
|
||||
:min="1"
|
||||
:max="31"
|
||||
:label="$t('common.Day')"
|
||||
week-mode-check
|
||||
@change="handleDayChange"
|
||||
/>
|
||||
</ElTabPane>
|
||||
|
||||
<!-- 月 -->
|
||||
<ElTabPane :label="$t('common.Month')" name="month">
|
||||
<CrontabPane
|
||||
v-model="state.month"
|
||||
:min="1"
|
||||
:max="12"
|
||||
:label="$t('common.Month')"
|
||||
/>
|
||||
</ElTabPane>
|
||||
|
||||
<!-- 周 -->
|
||||
<ElTabPane :label="$t('common.Week')" name="week">
|
||||
<CrontabPane
|
||||
v-model="state.week"
|
||||
:min="1"
|
||||
:max="7"
|
||||
:label="$t('common.Week')"
|
||||
:alias-map="weekAlias"
|
||||
day-mode-check
|
||||
@change="handleWeekChange"
|
||||
/>
|
||||
</ElTabPane>
|
||||
</ElTabs>
|
||||
|
||||
<!-- 结果展示区域 -->
|
||||
<div class="result-area">
|
||||
<ElDivider content-position="left">
|
||||
{{ $t('cron.GenerateResult') }}
|
||||
</ElDivider>
|
||||
<div class="result-row">
|
||||
<ElInput
|
||||
v-model="cronResult"
|
||||
readonly
|
||||
:placeholder="$t('cron.CronExpression')"
|
||||
>
|
||||
<template #prepend>{{ $t('cron.CronExpression') }}</template>
|
||||
</ElInput>
|
||||
<ElButton type="primary" @click="copyCron" style="margin-left: 10px">
|
||||
{{ $t('cron.UseThisValue') }}
|
||||
</ElButton>
|
||||
<ElButton
|
||||
type="primary"
|
||||
@click="getNextTimes"
|
||||
style="margin-left: 10px"
|
||||
>
|
||||
{{ $t('cron.CheckLast5ExecutionTimes') }}
|
||||
</ElButton>
|
||||
</div>
|
||||
|
||||
<div class="preview-table">
|
||||
<ElTable
|
||||
:data="resultTableData"
|
||||
border
|
||||
style="width: 100%"
|
||||
size="small"
|
||||
>
|
||||
<ElTableColumn prop="second" :label="$t('common.Second')" />
|
||||
<ElTableColumn prop="minute" :label="$t('common.Min')" />
|
||||
<ElTableColumn prop="hour" :label="$t('common.Hour')" />
|
||||
<ElTableColumn prop="day" :label="$t('common.Day')" />
|
||||
<ElTableColumn prop="month" :label="$t('common.Month')" />
|
||||
<ElTableColumn prop="week" :label="$t('common.Week')" />
|
||||
</ElTable>
|
||||
</div>
|
||||
<ElDivider content-position="left">
|
||||
{{ $t('cron.Last5ExecutionTimes') }}
|
||||
</ElDivider>
|
||||
<div v-for="(item, idx) in nextTimes" :key="idx">
|
||||
{{ item }}
|
||||
</div>
|
||||
</div>
|
||||
</ElCard>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.cron-generator {
|
||||
width: 100%;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.result-area {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.result-row {
|
||||
display: flex;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { ElButton, ElDialog } from 'element-plus';
|
||||
|
||||
import CronGenerator from './CronGenerator.vue';
|
||||
|
||||
defineProps({
|
||||
modelValue: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
});
|
||||
// 定义 emit 事件
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', value: string): void;
|
||||
}>();
|
||||
|
||||
// 使用本地变量来管理对话框显示
|
||||
const showCron = ref(false);
|
||||
function useCron(value: string) {
|
||||
emit('update:modelValue', value);
|
||||
showCron.value = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<ElDialog draggable title="Cron" v-model="showCron" width="60%">
|
||||
<CronGenerator @use-cron="useCron" />
|
||||
</ElDialog>
|
||||
<ElButton class="mt-2" @click="showCron = true">
|
||||
{{ $t('cron.ClickGenerate') }}
|
||||
</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
|
|
@ -0,0 +1,174 @@
|
|||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
|
||||
import {
|
||||
ElInputNumber,
|
||||
ElOption,
|
||||
ElRadio,
|
||||
ElRadioGroup,
|
||||
ElSelect,
|
||||
} from 'element-plus';
|
||||
// 定义接口,方便父组件引用(如果使用 TS)
|
||||
export interface CronItemState {
|
||||
type: 'every' | 'loop' | 'none' | 'range' | 'specific';
|
||||
rangeStart: number;
|
||||
rangeEnd: number;
|
||||
loopStart: number;
|
||||
loopStep: number;
|
||||
specificList: number[];
|
||||
}
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Object as () => CronItemState,
|
||||
required: true,
|
||||
},
|
||||
label: { type: String, default: '' },
|
||||
min: { type: Number, default: 0 },
|
||||
max: { type: Number, default: 59 },
|
||||
aliasMap: {
|
||||
type: Object as () => Record<number, string>,
|
||||
default: () => ({}),
|
||||
}, // 用于周的转换
|
||||
weekModeCheck: { type: Boolean, default: false }, // 是否是日Tab
|
||||
dayModeCheck: { type: Boolean, default: false }, // 是否是周Tab
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'change']);
|
||||
|
||||
// 计算属性处理单选框的 v-model
|
||||
const radioType = computed({
|
||||
get: () => props.modelValue.type,
|
||||
set: (val) => {
|
||||
// 更新 type 时,触发 update
|
||||
emit('update:modelValue', { ...props.modelValue, type: val });
|
||||
emit('change', val);
|
||||
},
|
||||
});
|
||||
|
||||
// 通用更新函数
|
||||
const updateVal = (key: keyof CronItemState, val: any) => {
|
||||
emit('update:modelValue', { ...props.modelValue, [key]: val });
|
||||
// 这里是否触发 change 取决于你的需求,通常数值改变不需要触发 tab 互斥检查,所以这里不一定非要 emit('change')
|
||||
// 但为了保险起见,可以保留
|
||||
};
|
||||
|
||||
// 生成下拉选项
|
||||
const specificOptions = computed(() => {
|
||||
const options = [];
|
||||
for (let i = props.min; i <= props.max; i++) {
|
||||
const label = props.aliasMap[i] ? `${props.aliasMap[i]} (${i})` : `${i}`;
|
||||
options.push({ value: i, label });
|
||||
}
|
||||
return options;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="crontab-pane">
|
||||
<ElRadioGroup class="cron-radio-group" v-model="radioType">
|
||||
<!-- 1. 每 xxx -->
|
||||
<div class="radio-line">
|
||||
<ElRadio value="every">{{ $t('cron.Per') }}{{ label }} (*)</ElRadio>
|
||||
</div>
|
||||
|
||||
<!-- 2. 不指定 (?) - 仅用于日和周 -->
|
||||
<div class="radio-line" v-if="weekModeCheck || dayModeCheck">
|
||||
<ElRadio value="none">{{ $t('cron.NotSpecified') }} (?)</ElRadio>
|
||||
</div>
|
||||
|
||||
<!-- 3. 周期 (Loop) -->
|
||||
<div class="radio-line">
|
||||
<ElRadio value="loop">{{ $t('cron.Cycle') }}</ElRadio>
|
||||
<span class="text">{{ $t('cron.From') }}</span>
|
||||
<ElInputNumber
|
||||
:model-value="modelValue.loopStart"
|
||||
@update:model-value="(v) => updateVal('loopStart', v)"
|
||||
:min="min"
|
||||
:max="max"
|
||||
size="small"
|
||||
controls-position="right"
|
||||
/>
|
||||
<span class="text">{{ label }}{{ $t('cron.StartPer') }}</span>
|
||||
<ElInputNumber
|
||||
:model-value="modelValue.loopStep"
|
||||
@update:model-value="(v) => updateVal('loopStep', v)"
|
||||
:min="1"
|
||||
:max="max"
|
||||
size="small"
|
||||
controls-position="right"
|
||||
/>
|
||||
<span class="text">{{ label }}{{ $t('cron.ExecuteOnce') }}</span>
|
||||
</div>
|
||||
|
||||
<!-- 4. 区间 (Range) -->
|
||||
<div class="radio-line">
|
||||
<ElRadio value="range">{{ $t('cron.Rang') }}</ElRadio>
|
||||
<span class="text">{{ $t('cron.From') }}</span>
|
||||
<ElInputNumber
|
||||
:model-value="modelValue.rangeStart"
|
||||
@update:model-value="(v) => updateVal('rangeStart', v)"
|
||||
:min="min"
|
||||
:max="max"
|
||||
size="small"
|
||||
controls-position="right"
|
||||
/>
|
||||
<span class="text">{{ $t('cron.To') }}</span>
|
||||
<ElInputNumber
|
||||
:model-value="modelValue.rangeEnd"
|
||||
@update:model-value="(v) => updateVal('rangeEnd', v)"
|
||||
:min="min"
|
||||
:max="max"
|
||||
size="small"
|
||||
controls-position="right"
|
||||
/>
|
||||
<span class="text">{{ label }}</span>
|
||||
</div>
|
||||
|
||||
<!-- 5. 指定 (Specific) -->
|
||||
<div class="radio-line">
|
||||
<ElRadio value="specific">{{ $t('cron.Specify') }}</ElRadio>
|
||||
<ElSelect
|
||||
:model-value="modelValue.specificList"
|
||||
@update:model-value="(v) => updateVal('specificList', v)"
|
||||
multiple
|
||||
:placeholder="$t('dictSelect.placeholder')"
|
||||
style="width: 100%; min-width: 200px; margin-left: 10px"
|
||||
size="small"
|
||||
>
|
||||
<ElOption
|
||||
v-for="item in specificOptions"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</ElSelect>
|
||||
</div>
|
||||
</ElRadioGroup>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.cron-radio-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.radio-line {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.text {
|
||||
margin: 0 5px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
:deep(.ElInputNumber) {
|
||||
width: 100px;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,190 @@
|
|||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref, watch } from 'vue';
|
||||
|
||||
import { ElMessage, ElOption, ElSelect } from 'element-plus';
|
||||
|
||||
import { api } from '#/api/request';
|
||||
import { $t } from '#/locales';
|
||||
// 字典项接口
|
||||
interface DictItem {
|
||||
value: number | string;
|
||||
label: string;
|
||||
disabled?: boolean;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
modelValue: Array<number | string> | null | number | string | undefined;
|
||||
dictCode: string; // 字典编码
|
||||
placeholder?: string;
|
||||
clearable?: boolean;
|
||||
filterable?: boolean;
|
||||
disabled?: boolean;
|
||||
multiple?: boolean;
|
||||
collapseTags?: boolean;
|
||||
collapseTagsTooltip?: boolean;
|
||||
showCode?: boolean; // 是否显示字典编码前缀
|
||||
immediate?: boolean; // 是否立即加载
|
||||
extraOptions?: DictItem[];
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(
|
||||
e: 'update:modelValue',
|
||||
value: Array<number | string> | null | number | string,
|
||||
): void;
|
||||
(
|
||||
e: 'change',
|
||||
value: Array<number | string> | null | number | string,
|
||||
dictItem?: DictItem | DictItem[],
|
||||
): void;
|
||||
(e: 'blur'): void;
|
||||
(e: 'dictLoaded', options: DictItem[]): void;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
placeholder: undefined,
|
||||
clearable: true,
|
||||
filterable: true,
|
||||
disabled: false,
|
||||
multiple: false,
|
||||
collapseTags: false,
|
||||
collapseTagsTooltip: false,
|
||||
showCode: false,
|
||||
immediate: true,
|
||||
extraOptions: () => [],
|
||||
});
|
||||
|
||||
const emit = defineEmits<Emits>();
|
||||
// 使用计算属性处理placeholder
|
||||
const placeholderText = computed(() => {
|
||||
// 如果父组件传入了placeholder,优先使用
|
||||
if (props.placeholder !== undefined) {
|
||||
return props.placeholder;
|
||||
}
|
||||
// 否则使用默认的国际化文本
|
||||
return $t('dictSelect.placeholder');
|
||||
});
|
||||
// 响应式数据
|
||||
const dictOptions = ref<DictItem[]>([]);
|
||||
const loading = ref(false);
|
||||
const loadedCodes = ref<Set<string>>(new Set()); // 已加载的字典编码缓存
|
||||
|
||||
// 处理值变化
|
||||
const handleChange = (
|
||||
value: Array<number | string> | null | number | string,
|
||||
) => {
|
||||
emit('update:modelValue', value);
|
||||
|
||||
// 找到对应的字典项
|
||||
const selectedItems: DictItem | DictItem[] | undefined =
|
||||
props.multiple && Array.isArray(value)
|
||||
? (value
|
||||
.map((v) => dictOptions.value.find((item) => item.value === v))
|
||||
.filter(Boolean) as DictItem[])
|
||||
: dictOptions.value.find((item) => item.value === value);
|
||||
|
||||
emit('change', value, selectedItems);
|
||||
};
|
||||
|
||||
// 处理失去焦点
|
||||
const handleBlur = () => {
|
||||
emit('blur');
|
||||
};
|
||||
|
||||
// 获取字典数据
|
||||
const fetchDictData = async (code: string) => {
|
||||
// 如果已经加载过,直接返回缓存
|
||||
if (loadedCodes.value.has(code)) {
|
||||
return;
|
||||
}
|
||||
|
||||
loading.value = true;
|
||||
try {
|
||||
// 这里调用你的后端API
|
||||
const data = await getDictListByCode(code);
|
||||
// extraOptions 放最前面
|
||||
dictOptions.value = [...props.extraOptions, ...data];
|
||||
loadedCodes.value.add(code);
|
||||
emit('dictLoaded', data);
|
||||
} catch (error) {
|
||||
console.error(`${$t('dictSelect.getError')}: ${code}`, error);
|
||||
ElMessage.error(`${$t('dictSelect.getError')}: ${code}`);
|
||||
dictOptions.value = [];
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 模拟后端API调用 - 实际项目中替换为你的真实API
|
||||
const getDictListByCode = async (code: string): Promise<DictItem[]> => {
|
||||
const requestPromise = api.get(`/api/v1/dict/items/${code}`);
|
||||
const dictData = await requestPromise;
|
||||
return dictData.data;
|
||||
};
|
||||
|
||||
// 重新加载字典
|
||||
const reloadDict = () => {
|
||||
if (props.dictCode) {
|
||||
fetchDictData(props.dictCode);
|
||||
}
|
||||
};
|
||||
|
||||
// 监听字典编码变化
|
||||
watch(
|
||||
() => props.dictCode,
|
||||
(newCode) => {
|
||||
if (newCode) {
|
||||
fetchDictData(newCode);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// 组件挂载时加载字典
|
||||
onMounted(() => {
|
||||
if (props.immediate && props.dictCode) {
|
||||
fetchDictData(props.dictCode);
|
||||
}
|
||||
});
|
||||
|
||||
// 暴露方法给父组件
|
||||
defineExpose({
|
||||
reloadDict,
|
||||
getOptions: () => dictOptions.value,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ElSelect
|
||||
:model-value="modelValue"
|
||||
@update:model-value="handleChange"
|
||||
@blur="handleBlur"
|
||||
:placeholder="placeholderText"
|
||||
:clearable="clearable"
|
||||
:filterable="filterable"
|
||||
:disabled="disabled || loading"
|
||||
:loading="loading"
|
||||
:multiple="multiple"
|
||||
:collapse-tags="collapseTags"
|
||||
:collapse-tags-tooltip="collapseTagsTooltip"
|
||||
>
|
||||
<ElOption
|
||||
v-for="item in dictOptions"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
:disabled="item.disabled"
|
||||
/>
|
||||
<template #prefix v-if="showCode && dictCode">
|
||||
<span class="dict-select__prefix">{{ dictCode }}</span>
|
||||
</template>
|
||||
</ElSelect>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.dict-select__prefix {
|
||||
margin-right: 4px;
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,221 @@
|
|||
<script setup>
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { ArrowDown } from '@element-plus/icons-vue';
|
||||
import {
|
||||
ElButton,
|
||||
ElDropdown,
|
||||
ElDropdownItem,
|
||||
ElDropdownMenu,
|
||||
ElIcon,
|
||||
ElInput,
|
||||
} from 'element-plus';
|
||||
|
||||
import { hasPermission } from '#/api/common/hasPermission.ts';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
// 定义组件属性
|
||||
const props = defineProps({
|
||||
// 按钮配置数组
|
||||
buttons: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
validator: (value) => {
|
||||
return value.every((button) => {
|
||||
return (
|
||||
typeof button.text === 'string' &&
|
||||
(button.key || typeof button.key === 'string')
|
||||
);
|
||||
});
|
||||
},
|
||||
},
|
||||
// 最大显示按钮数量(不包括下拉菜单)
|
||||
maxVisibleButtons: {
|
||||
type: Number,
|
||||
default: 3,
|
||||
},
|
||||
// 搜索框占位符
|
||||
searchPlaceholder: {
|
||||
type: String,
|
||||
default: $t('common.searchPlaceholder'),
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['search', 'button-click', 'buttonClick']);
|
||||
|
||||
// 搜索值
|
||||
const searchValue = ref('');
|
||||
|
||||
// 计算显示的按钮
|
||||
const visibleButtons = computed(() => {
|
||||
return props.buttons.slice(0, props.maxVisibleButtons);
|
||||
});
|
||||
|
||||
// 计算下拉菜单中的按钮
|
||||
const dropdownButtons = computed(() => {
|
||||
const dropdownButtonsTemp = props.buttons.slice(props.maxVisibleButtons);
|
||||
if (dropdownButtonsTemp.length === 0) {
|
||||
return [];
|
||||
}
|
||||
return dropdownButtonsTemp.value.filter((action) => {
|
||||
return hasPermission([action.permission]);
|
||||
});
|
||||
});
|
||||
|
||||
// 处理搜索
|
||||
const handleSearch = () => {
|
||||
emit('search', searchValue.value);
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
searchValue.value = '';
|
||||
emit('search', '');
|
||||
};
|
||||
|
||||
// 处理按钮点击
|
||||
const handleButtonClick = (button) => {
|
||||
emit('buttonClick', {
|
||||
type: 'button',
|
||||
key: button.key,
|
||||
button,
|
||||
data: button.data,
|
||||
});
|
||||
};
|
||||
|
||||
// 处理下拉菜单点击
|
||||
const handleDropdownClick = (button) => {
|
||||
emit('buttonClick', {
|
||||
type: 'dropdown',
|
||||
key: button.key,
|
||||
button,
|
||||
data: button.data,
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="custom-header">
|
||||
<!-- 左侧搜索区域 -->
|
||||
<div class="header-left">
|
||||
<div class="search-container">
|
||||
<div>
|
||||
<ElInput
|
||||
v-model="searchValue"
|
||||
:placeholder="$t('common.searchPlaceholder')"
|
||||
class="search-input"
|
||||
@keyup.enter="handleSearch"
|
||||
clearable
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<ElButton type="primary" auto-insert-space @click="handleSearch">
|
||||
{{ $t('button.query') }}
|
||||
</ElButton>
|
||||
</div>
|
||||
<div>
|
||||
<ElButton auto-insert-space @click="handleReset">
|
||||
{{ $t('button.reset') }}
|
||||
</ElButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧按钮区域 -->
|
||||
<div class="header-right">
|
||||
<!-- 显示的按钮(最多3个) -->
|
||||
<template
|
||||
v-for="(button, index) in visibleButtons"
|
||||
:key="button.key || index"
|
||||
>
|
||||
<ElButton
|
||||
:type="button.type || 'default'"
|
||||
:icon="button.icon"
|
||||
:disabled="button.disabled"
|
||||
v-access:code="button.permission"
|
||||
@click="handleButtonClick(button)"
|
||||
>
|
||||
{{ button.text }}
|
||||
</ElButton>
|
||||
</template>
|
||||
|
||||
<!-- 下拉菜单(隐藏的按钮) -->
|
||||
<ElDropdown
|
||||
v-if="dropdownButtons.length > 0"
|
||||
@command="handleDropdownClick"
|
||||
>
|
||||
<ElButton>
|
||||
{{ $t('button.more')
|
||||
}}<ElIcon class="el-icon--right"><ArrowDown /></ElIcon>
|
||||
</ElButton>
|
||||
<template #dropdown>
|
||||
<ElDropdownMenu>
|
||||
<ElDropdownItem
|
||||
v-for="button in dropdownButtons"
|
||||
:key="button.key"
|
||||
:command="button"
|
||||
:disabled="button.disabled"
|
||||
>
|
||||
<ElIcon v-if="button.icon">
|
||||
<component :is="button.icon" />
|
||||
</ElIcon>
|
||||
<span style="margin-left: 8px">{{ button.text }}</span>
|
||||
</ElDropdownItem>
|
||||
</ElDropdownMenu>
|
||||
</template>
|
||||
</ElDropdown>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.custom-header {
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
padding: 12px 16px;
|
||||
}
|
||||
|
||||
.header-left,
|
||||
.header-right {
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.search-container {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.header-right {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
}
|
||||
|
||||
.custom-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.search-container {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
width: 300px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.header-right {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
<script setup>
|
||||
const props = defineProps({
|
||||
className: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<svg
|
||||
:class="props.className"
|
||||
width="13.6333333px"
|
||||
height="13.6333333px"
|
||||
viewBox="0 0 13.6333333 13.6333333"
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M7.66166667,10.1666667 C7.66166667,11.55014 8.78319337,12.6716667 10.1666667,12.6716667 C11.55014,12.6716667 12.6716667,11.55014 12.6716667,10.1666667 C12.6716667,8.78319337 11.55014,7.66166667 10.1666667,7.66166667 L8.04,7.66166667 C7.82966667,7.66166667 7.662,7.82966667 7.662,8.03966667 L7.662,10.1666667 L7.66166667,10.1666667 Z M10.1666667,6.99999866 C11.9155684,6.99999866 13.3333333,8.41776496 13.3333333,10.1666667 C13.3333333,11.9155684 11.9155684,13.3333333 10.1666667,13.3333333 C8.41776496,13.3333333 6.99999807,11.9155684 6.99999807,10.1666667 L6.99999807,8.04 C6.99946799,7.76406988 7.10882245,7.49928316 7.3039036,7.30413947 C7.49898474,7.10899578 7.7637364,6.99955644 8.03966667,6.99999866 L10.1666667,6.99999866 L10.1666667,6.99999866 Z M6.33333469,10.1666667 C6.33333469,11.9155684 4.91556838,13.3333333 3.16666667,13.3333333 C1.41776496,13.3333333 0,11.9155684 0,10.1666667 C0,8.41776496 1.41776496,6.99999866 3.16666667,6.99999866 L5.29366667,6.99999866 C5.56953908,6.99955669 5.83424002,7.10895032 6.02931152,7.30402182 C6.22438303,7.49909332 6.33377666,7.76379426 6.33333469,8.03966667 L6.33333469,10.1666667 Z M3.16666667,7.66166667 C1.78319337,7.66166667 0.661666672,8.78319337 0.661666672,10.1666667 C0.661666672,11.55014 1.78319337,12.6716667 3.16666667,12.6716667 C4.55013997,12.6716667 5.67166667,11.55014 5.67166667,10.1666667 L5.67166667,8.04 C5.67166667,7.83 5.504,7.662 5.29366667,7.662 L3.16666667,7.662 L3.16666667,7.66166667 Z M6.99999866,3.16633334 C7.00018409,1.41747504 8.41803055,-0.000122713956 10.1668889,7.79661757e-09 C11.9157472,0.000122729549 13.3333333,1.41791945 13.3333333,3.16677777 C13.3333333,4.91563609 11.915525,6.33333528 10.1666667,6.33333528 L8.04,6.33333528 C7.76406988,6.33386536 7.49928316,6.22451089 7.30413947,6.02942974 C7.10899578,5.8343486 6.99955644,5.56959695 6.99999866,5.29366667 L6.99999866,3.16666667 L6.99999866,3.16633334 Z M10.1666667,5.67166667 C11.55014,5.67166667 12.6716667,4.55013997 12.6716667,3.16666667 C12.6716667,1.78319337 11.55014,0.661666672 10.1666667,0.661666672 C8.78319337,0.661666672 7.66166667,1.78319337 7.66166667,3.16666667 L7.66166667,5.29366667 C7.66166667,5.50366667 7.82933334,5.67166667 8.03966667,5.67166667 L10.1666667,5.67166667 L10.1666667,5.67166667 Z M5.67166667,3.16666667 C5.67166667,1.78319337 4.55013997,0.661666672 3.16666667,0.661666672 C1.78319337,0.661666672 0.661666672,1.78319337 0.661666672,3.16666667 C0.661666672,4.55013997 1.78319337,5.67166667 3.16666667,5.67166667 L5.29366667,5.67166667 C5.504,5.67166667 5.67166667,5.50366667 5.67166667,5.29366667 L5.67166667,3.16666667 Z M3.16666667,6.33333469 C1.41776496,6.33333469 0,4.91556838 0,3.16666667 C0,1.41776496 1.41776496,0 3.16666667,0 C4.91556838,0 6.33333469,1.41776496 6.33333469,3.16666667 L6.33333469,5.29366667 C6.33377666,5.56953908 6.22438303,5.83424002 6.02931152,6.02931152 C5.83424002,6.22438303 5.56953908,6.33377666 5.29366667,6.33333469 L3.16666667,6.33333469 Z"
|
||||
id="形状"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
svg {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
<template>
|
||||
<svg
|
||||
t="1765503751020"
|
||||
class="icon"
|
||||
viewBox="0 0 1024 1024"
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
p-id="9797"
|
||||
width="1em"
|
||||
height="1em"
|
||||
>
|
||||
<path
|
||||
d="M827.712 341.312H452.288a25.6 25.6 0 0 1-25.6-25.6V256H320c-29.44 0-54.592 10.432-75.456 31.232a102.784 102.784 0 0 0-31.232 75.456c0 29.44 10.432 54.592 31.232 75.392A102.784 102.784 0 0 0 320 469.312h384c53.056 0 98.24 18.816 135.744 56.256A184.96 184.96 0 0 1 896 661.312c0 53.056-18.752 98.304-56.256 135.808a184.96 184.96 0 0 1-135.744 56.192H554.688v59.776a25.6 25.6 0 0 1-25.6 25.6H153.6a25.6 25.6 0 0 1-25.6-25.6v-204.8a25.6 25.6 0 0 1 25.6-25.6h375.488a25.6 25.6 0 0 1 25.6 25.6V768H704c29.44 0 54.592-10.432 75.456-31.232a102.912 102.912 0 0 0 31.232-75.456c0-29.44-10.432-54.592-31.232-75.392A102.848 102.848 0 0 0 704 554.688H320a185.088 185.088 0 0 1-135.744-56.256A184.96 184.96 0 0 1 128 362.688c0-53.056 18.752-98.304 56.256-135.808A184.96 184.96 0 0 1 320 170.688h106.688v-59.776a25.6 25.6 0 0 1 25.6-25.6h375.424a25.6 25.6 0 0 1 25.6 25.6v204.8a25.6 25.6 0 0 1-25.6 25.6zM537.6 256h204.8a25.6 25.6 0 0 0 25.6-25.6v-34.112a25.6 25.6 0 0 0-25.6-25.6H537.6a25.6 25.6 0 0 0-25.6 25.6V230.4a25.6 25.6 0 0 0 25.6 25.6z m-68.288 571.712V793.6a25.6 25.6 0 0 0-25.6-25.6h-204.8a25.6 25.6 0 0 0-25.6 25.6v34.112a25.6 25.6 0 0 0 25.6 25.6h204.8a25.6 25.6 0 0 0 25.6-25.6z"
|
||||
p-id="9798"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
<script setup>
|
||||
const props = defineProps({
|
||||
className: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<svg
|
||||
:class="props.className"
|
||||
width="18px"
|
||||
height="18px"
|
||||
viewBox="0 0 16 16"
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
>
|
||||
<rect id="矩形" opacity="0" x="0" y="0" width="16" height="16" />
|
||||
<path
|
||||
d="M8.93,2.138 C9.12713347,1.95017863 9.4383122,1.95462404 9.63,2.148 L12.355,4.8965 C12.4522665,4.99447053 12.5046974,5.12829734 12.499868,5.26626717 C12.4950385,5.404237 12.4333787,5.53407024 12.3295,5.625 L5.7635,11.376 C5.66962616,11.4583382 5.54834097,11.5025716 5.4235,11.4999997 L2.9895,11.448 C2.71746669,11.442286 2.49999999,11.2200933 2.49999999,10.948 L2.49999999,8.476 C2.49999999,8.33921115 2.55599094,8.20838431 2.655,8.114 L8.93,2.138 Z M13.341,10.25 C13.5196328,10.2534833 13.682838,10.3520015 13.7691377,10.5084437 C13.8554375,10.664886 13.8517208,10.8554849 13.7593877,11.0084438 C13.6670547,11.1614026 13.5001328,11.2534833 13.3215,11.25 L9.574,11.177 C9.39536721,11.1734273 9.23220968,11.0748266 9.1459873,10.9183397 C9.05976491,10.7618529 9.06357689,10.571254 9.1559873,10.4183397 C9.24839771,10.2654255 9.41536721,10.1734273 9.594,10.177 L13.341,10.25 Z M9.265,3.2 L3.5,8.69 L3.5,10.4585 L5.25,10.496 L11.2705,5.223 L9.265,3.2 Z M13.3165,12.677 C13.5926424,12.6749289 13.8181789,12.8971076 13.8202644,13.17325 C13.8223211,13.4493924 13.6001424,13.6749289 13.324,13.677 L3.504,13.75 C3.22785763,13.7520711 3.00232108,13.5298924 3.00023565,13.25375 C2.99817894,12.9776076 3.22035763,12.7520711 3.4965,12.75 L13.3165,12.677 Z"
|
||||
id="形状"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
svg {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
<template>
|
||||
<svg
|
||||
width="1em"
|
||||
height="1em"
|
||||
viewBox="0 0 16 16"
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
>
|
||||
<title>wand</title>
|
||||
<defs>
|
||||
<linearGradient
|
||||
x1="99.8250786%"
|
||||
y1="6.53996342e-13%"
|
||||
x2="12.639287%"
|
||||
y2="77.6796996%"
|
||||
id="linearGradient-1"
|
||||
>
|
||||
<stop stop-color="#F17E47" offset="0%" />
|
||||
<stop stop-color="#D85ABF" offset="49.9043925%" />
|
||||
<stop stop-color="#717AFF" offset="100%" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<g
|
||||
id="页面-1"
|
||||
stroke="none"
|
||||
stroke-width="1"
|
||||
fill="none"
|
||||
fill-rule="evenodd"
|
||||
>
|
||||
<g id="设置bot" transform="translate(-444, -80)" fill-rule="nonzero">
|
||||
<g id="编组-5备份-2" transform="translate(96, 64)">
|
||||
<g id="编组-3" transform="translate(336, 10)">
|
||||
<g id="wand" transform="translate(12, 6)">
|
||||
<rect
|
||||
id="矩形"
|
||||
fill="#000000"
|
||||
opacity="0"
|
||||
x="0"
|
||||
y="0"
|
||||
width="16"
|
||||
height="16"
|
||||
/>
|
||||
<path
|
||||
d="M7.06,7.81 C7.28982243,8.20717228 7.64510124,8.51672214 8.07,8.69 L3.89,13.75 C3.77849314,13.8884057 3.61587696,13.9759881 3.43895054,13.9929279 C3.26202413,14.0098676 3.08574352,13.9547328 2.95,13.84 C2.67186371,13.6019746 2.63208206,13.1864774 2.86,12.9 Z M10.0751606,10.342259 C10.1007529,10.3593205 10.1142973,10.3895436 10.11,10.42 L10.05,11 C10.0349786,11.0209157 10.0349786,11.0490843 10.05,11.07 L10.5,11.46 C10.5414214,11.4627614 10.5727614,11.4985786 10.57,11.54 C10.5672386,11.5814214 10.5314214,11.6127614 10.49,11.61 L9.92,11.73 L9.86,11.73 L9.63,12.27 C9.61591049,12.2954653 9.58910316,12.3112702 9.56,12.3112702 C9.53089684,12.3112702 9.50408951,12.2954653 9.49,12.27 L9.2,11.75 C9.18101383,11.7409941 9.15898617,11.7409941 9.14,11.75 L8.55,11.69 C8.52147194,11.6857655 8.49743138,11.6665036 8.4870782,11.6395853 C8.47672503,11.612667 8.48166207,11.5822599 8.5,11.56 L8.9,11.13 C8.90944584,11.1076226 8.90944584,11.0823774 8.9,11.06 L8.77,10.48 C8.76794796,10.4523802 8.78032765,10.4256638 8.80272638,10.4093738 C8.82512511,10.3930838 8.85435607,10.3895379 8.88,10.4 L9.42,10.64 C9.44091574,10.6550214 9.46908426,10.6550214 9.49,10.64 L9.99,10.34 C10.0164608,10.3243193 10.0495684,10.3251975 10.0751606,10.342259 Z M11.6310879,2.06789708 C11.723319,2.12580967 11.773603,2.23194717 11.76,2.34 L11.52,4.48 C11.5119641,4.57445213 11.5490415,4.66714566 11.62,4.73 L13.24,6.14 C13.3130482,6.21823611 13.3351022,6.33127196 13.2968207,6.4312292 C13.2585392,6.53118644 13.1666211,6.6005734 13.06,6.61 L11,7 C10.9100357,7.02152566 10.835738,7.0846787 10.8,7.17 L9.96,9.17 C9.91461323,9.27007429 9.81488549,9.33434966 9.705,9.33434966 C9.59511451,9.33434966 9.49538677,9.27007429 9.45,9.17 L8.34,7.35 C8.29373222,7.26570734 8.2061264,7.21238206 8.11,7.21 L6,7 C5.89344307,6.98628907 5.8031975,6.91479202 5.76547441,6.8141971 C5.72775131,6.71360218 5.74873279,6.60039499 5.82,6.52 L7.23,5 C7.31119031,4.92297534 7.33871717,4.80500308 7.3,4.7 L6.82,2.61 C6.79547628,2.50301623 6.8334527,2.39135455 6.91811751,2.32150608 C7.00278233,2.2516576 7.11962606,2.23559266 7.22,2.28 L9.22,3.17 C9.30451125,3.21445166 9.40548875,3.21445166 9.49,3.17 L11.33,2.07 C11.4214132,2.01080476 11.5388567,2.00998449 11.6310879,2.06789708 Z M4.38174625,4.39636653 C4.40885368,4.41513321 4.42353002,4.44721982 4.42,4.48 L4.35,5.09 C4.33924996,5.11209516 4.33924996,5.13790484 4.35,5.16 L4.82,5.57 C4.84386802,5.58382847 4.86007635,5.60783875 4.8639774,5.63514609 C4.86787845,5.66245342 4.85904132,5.69004162 4.84,5.71 L4.23,5.83 L4.17,5.83 L3.93,6.4 C3.91332529,6.42513807 3.88516568,6.44025063 3.855,6.44025063 C3.82483432,6.44025063 3.79667471,6.42513807 3.78,6.4 L3.47,5.86 C3.45075739,5.85221598 3.42924261,5.85221598 3.41,5.86 L2.79,5.8 C2.75945815,5.7951621 2.73353862,5.77498342 2.72135759,5.74656103 C2.70917657,5.71813864 2.71243996,5.68545299 2.73,5.66 L3.15,5.21 C3.16429062,5.18524791 3.16429062,5.15475209 3.15,5.13 L3,4.55 C2.99439094,4.51822247 3.00622377,4.48587394 3.03101332,4.46521598 C3.05580287,4.44455803 3.08975488,4.43875251 3.12,4.45 L3.68,4.71 C3.70475209,4.72429062 3.73524791,4.72429062 3.76,4.71 L4.29,4.39 C4.31943968,4.37515728 4.35463882,4.37759984 4.38174625,4.39636653 Z"
|
||||
id="形状结合"
|
||||
fill="url(#linearGradient-1)"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
<script setup>
|
||||
const props = defineProps({
|
||||
className: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<svg
|
||||
width="16px"
|
||||
height="16px"
|
||||
:class="props.className"
|
||||
viewBox="0 0 16 16"
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
>
|
||||
<g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g transform="translate(-483, -587)" fill="#FFFFFF" fill-rule="nonzero">
|
||||
<g transform="translate(468, 579)">
|
||||
<g transform="translate(15, 8)">
|
||||
<rect opacity="0" x="0" y="0" width="16" height="16" />
|
||||
<path
|
||||
d="M2.08296875,4.633625 L13.9143281,4.633625 C14.2234219,4.633625 14.4760625,4.38098438 14.4760625,4.07190625 C14.4760625,3.7628125 14.2261094,3.51015625 13.9143438,3.51015625 L2.08296875,3.51015625 C1.773875,3.51015625 1.52123437,3.7628125 1.52123437,4.07189063 C1.52123437,4.38098438 1.77389062,4.633625 2.08295313,4.633625 L2.08296875,4.633625 Z M13.9143281,7.43960938 L2.08298438,7.43960938 C1.77389063,7.43960938 1.52125,7.69226563 1.52125,8.00134375 C1.52125,8.3104375 1.77390625,8.56307813 2.08296875,8.56307813 L13.9143281,8.56307813 C14.2234219,8.56307813 14.4760625,8.31042188 14.4760625,8.00134375 C14.4760625,7.69225 14.2234063,7.439625 13.9143437,7.439625 L13.9143281,7.43960938 Z M13.9143281,11.366375 L2.08298438,11.366375 C1.77389063,11.366375 1.52125,11.6163281 1.52125,11.9280938 C1.52125,12.2398594 1.77390625,12.4898438 2.08296875,12.4898438 L13.9143281,12.4898438 C14.2234219,12.4898438 14.4760625,12.239875 14.4760625,11.9281094 C14.4760625,11.6163438 14.2234063,11.366375 13.9143437,11.366375 L13.9143281,11.366375 Z"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
svg {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
<script setup>
|
||||
const props = defineProps({
|
||||
className: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<svg
|
||||
:class="props.className"
|
||||
width="18px"
|
||||
height="18px"
|
||||
viewBox="0 0 18 18"
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<title>编组 12</title>
|
||||
<g
|
||||
id="页面-1"
|
||||
stroke="none"
|
||||
stroke-width="1"
|
||||
fill="none"
|
||||
fill-rule="currentColor"
|
||||
>
|
||||
<g id="知识库" transform="translate(-25, -161)">
|
||||
<g id="编组-12" transform="translate(24, 160)">
|
||||
<rect id="矩形" x="0" y="0" width="20" height="20" />
|
||||
<path
|
||||
d="M16.4912888,10.7980951 C15.7969114,10.7980951 15.3264751,11.0844769 15.3264751,11.0844769 C14.9748359,11.2984761 14.687007,11.1435874 14.687007,10.7402321 L14.687007,6.42579246 C14.687007,6.0224371 14.3475878,5.6924189 13.9327401,5.6924189 L8.1353671,5.6924189 C7.72051942,5.6924189 7.59855437,5.4390385 7.86435829,5.12933468 C7.86435829,5.12933468 8.43699845,4.46211097 8.43699845,3.61543104 C8.43699845,2.17097789 7.23265922,1 5.74698006,1 C4.26137547,1 3.05703726,2.17097789 3.05703726,3.61543104 C3.05703726,4.46211097 3.62960182,5.12933468 3.62960182,5.12933468 C3.89540472,5.4390385 3.77344069,5.6924189 3.35859301,5.6924189 L1.75426685,5.6924189 C1.33942018,5.6924189 1,6.0224371 1,6.42579246 L1,9.6803585 C1,10.0837149 1.29439047,10.2495305 1.65425121,10.0487328 C1.65425121,10.0487328 1.86348477,9.93197883 2.39124538,9.93197883 C3.60735002,9.93197883 4.59317781,10.8904256 4.59317781,12.0728439 C4.59317781,13.2552633 3.60735002,14.2137826 2.39124538,14.2137826 C1.86348477,14.2137826 1.65425121,14.097103 1.65425121,14.097103 C1.29439047,13.8965249 1,14.0621945 1,14.4655509 L1,18.2666264 C1,18.6699818 1.33942018,19 1.75426685,19 L4.66566218,19 C5.08050986,19 5.24682528,18.7161113 5.03517879,18.3690793 C5.03517879,18.3690793 4.90536911,18.1563257 4.90536911,17.6363653 C4.90536911,16.4705942 5.87716669,15.525935 7.07584745,15.525935 C8.27460483,15.525935 9.24640241,16.4708137 9.24640241,17.6363653 C9.24640241,18.1563257 9.11659274,18.3690793 9.11659274,18.3690793 C8.90502081,18.7158908 9.07133623,19 9.4861829,19 L13.9327401,19 C14.3475878,19 14.687007,18.6699818 14.687007,18.2666264 L14.687007,16.0863791 C14.687007,15.6830238 14.9746101,15.5281351 15.3263239,15.7422803 C15.3263239,15.7422803 15.7969114,16.0285896 16.4912888,16.0285896 C17.9767432,16.0285896 18.8728875,14.0825824 18.8728875,14.0825824 C19.0423708,13.7144286 19.0423708,13.1120347 18.8728875,12.7438818 C18.8728875,12.7441013 17.9767432,10.7980951 16.4912888,10.7980951 Z"
|
||||
id="Fill-257"
|
||||
fill="#ACB7C6"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
svg {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
<script setup>
|
||||
const props = defineProps({
|
||||
className: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<svg
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 14 14"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
:class="props.className"
|
||||
>
|
||||
<path
|
||||
d="M9.702 14L1.335 14C0.586 14 0 13.393 0 12.69L0 10.676C0 10.516 0.065 10.388 0.195 10.292C0.326 10.196 0.488 10.164 0.619 10.196C0.781 10.228 0.944 10.26 1.074 10.26C2.019 10.26 2.8 9.493 2.8 8.566C2.8 7.639 2.019 6.872 1.074 6.872C0.944 6.872 0.781 6.904 0.619 6.936C0.456 6.968 0.326 6.936 0.195 6.84C0.065 6.744 0 6.616 0 6.457L0 4.443C0 3.708 0.619 3.132 1.335 3.132L2.833 3.132C2.8 2.973 2.8 2.813 2.8 2.653C2.8 1.183 4.005 0 5.502 0C7 0 8.205 1.183 8.205 2.653C8.205 2.813 8.205 2.973 8.172 3.132L9.67 3.132C10.419 3.132 11.005 3.74 11.005 4.443L11.005 5.913L11.298 5.913C12.795 5.913 14 7.096 14 8.566C14 10.037 12.795 11.219 11.298 11.219L11.005 11.219L11.005 12.69C11.07 13.393 10.451 14 9.702 14ZM0.977 11.219L0.977 12.658C0.977 12.849 1.14 13.009 1.335 13.009L9.702 13.009C9.898 13.009 10.06 12.849 10.06 12.658L10.06 10.58C10.06 10.42 10.126 10.26 10.256 10.196C10.386 10.1 10.549 10.1 10.712 10.132C10.907 10.196 11.102 10.26 11.298 10.26C12.242 10.26 13.023 9.493 13.023 8.566C13.023 7.639 12.242 6.872 11.298 6.872C11.102 6.872 10.907 6.904 10.712 7C10.549 7.064 10.386 7.032 10.256 6.936C10.126 6.84 10.06 6.712 10.06 6.553L10.06 4.443C10.06 4.251 9.898 4.091 9.702 4.091L7.521 4.091C7.358 4.091 7.195 3.995 7.098 3.868C7 3.74 7 3.548 7.065 3.42C7.195 3.164 7.228 2.941 7.228 2.685C7.228 1.758 6.447 0.991 5.502 0.991C4.558 0.991 3.777 1.758 3.777 2.685C3.777 2.941 3.842 3.196 3.94 3.42C4.005 3.58 4.005 3.74 3.907 3.868C3.81 3.995 3.647 4.091 3.484 4.091L1.335 4.091C1.14 4.091 0.977 4.251 0.977 4.443L0.977 5.881C2.507 5.817 3.777 7.032 3.777 8.534C3.777 10.005 2.572 11.187 1.074 11.187C1.042 11.219 1.009 11.219 0.977 11.219Z"
|
||||
fill="currentColor"
|
||||
fill-rule="nonzero"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
svg {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,129 @@
|
|||
<template>
|
||||
<svg
|
||||
data-v-dd795da4=""
|
||||
data-v-5179693f=""
|
||||
class="loading-svg"
|
||||
color="currentColor"
|
||||
viewBox="0 0 1000 1000"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
>
|
||||
<title data-v-dd795da4="">Speech Recording</title>
|
||||
<rect
|
||||
data-v-dd795da4=""
|
||||
fill="currentColor"
|
||||
rx="70"
|
||||
ry="70"
|
||||
height="250"
|
||||
width="140"
|
||||
x="0"
|
||||
y="375"
|
||||
>
|
||||
<animate
|
||||
data-v-dd795da4=""
|
||||
attributeName="height"
|
||||
values="250; 500; 250"
|
||||
keyTimes="0; 0.5; 1"
|
||||
dur="0.8s"
|
||||
begin="0s"
|
||||
repeatCount="indefinite"
|
||||
/>
|
||||
<animate
|
||||
data-v-dd795da4=""
|
||||
attributeName="y"
|
||||
values="375; 250; 375"
|
||||
keyTimes="0; 0.5; 1"
|
||||
dur="0.8s"
|
||||
begin="0s"
|
||||
repeatCount="indefinite"
|
||||
/>
|
||||
</rect>
|
||||
<rect
|
||||
data-v-dd795da4=""
|
||||
fill="currentColor"
|
||||
rx="70"
|
||||
ry="70"
|
||||
height="250"
|
||||
width="140"
|
||||
x="286.66666666666663"
|
||||
y="375"
|
||||
>
|
||||
<animate
|
||||
data-v-dd795da4=""
|
||||
attributeName="height"
|
||||
values="250; 500; 250"
|
||||
keyTimes="0; 0.5; 1"
|
||||
dur="0.8s"
|
||||
begin="0.2s"
|
||||
repeatCount="indefinite"
|
||||
/>
|
||||
<animate
|
||||
data-v-dd795da4=""
|
||||
attributeName="y"
|
||||
values="375; 250; 375"
|
||||
keyTimes="0; 0.5; 1"
|
||||
dur="0.8s"
|
||||
begin="0.2s"
|
||||
repeatCount="indefinite"
|
||||
/>
|
||||
</rect>
|
||||
<rect
|
||||
data-v-dd795da4=""
|
||||
fill="currentColor"
|
||||
rx="70"
|
||||
ry="70"
|
||||
height="250"
|
||||
width="140"
|
||||
x="573.3333333333333"
|
||||
y="375"
|
||||
>
|
||||
<animate
|
||||
data-v-dd795da4=""
|
||||
attributeName="height"
|
||||
values="250; 500; 250"
|
||||
keyTimes="0; 0.5; 1"
|
||||
dur="0.8s"
|
||||
begin="0.4s"
|
||||
repeatCount="indefinite"
|
||||
/>
|
||||
<animate
|
||||
data-v-dd795da4=""
|
||||
attributeName="y"
|
||||
values="375; 250; 375"
|
||||
keyTimes="0; 0.5; 1"
|
||||
dur="0.8s"
|
||||
begin="0.4s"
|
||||
repeatCount="indefinite"
|
||||
/>
|
||||
</rect>
|
||||
<rect
|
||||
data-v-dd795da4=""
|
||||
fill="currentColor"
|
||||
rx="70"
|
||||
ry="70"
|
||||
height="250"
|
||||
width="140"
|
||||
x="859.9999999999999"
|
||||
y="375"
|
||||
>
|
||||
<animate
|
||||
data-v-dd795da4=""
|
||||
attributeName="height"
|
||||
values="250; 500; 250"
|
||||
keyTimes="0; 0.5; 1"
|
||||
dur="0.8s"
|
||||
begin="0.6000000000000001s"
|
||||
repeatCount="indefinite"
|
||||
/>
|
||||
<animate
|
||||
data-v-dd795da4=""
|
||||
attributeName="y"
|
||||
values="375; 250; 375"
|
||||
keyTimes="0; 0.5; 1"
|
||||
dur="0.8s"
|
||||
begin="0.6000000000000001s"
|
||||
repeatCount="indefinite"
|
||||
/>
|
||||
</rect>
|
||||
</svg>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
<template>
|
||||
<svg
|
||||
width="30px"
|
||||
height="30px"
|
||||
viewBox="0 0 30 30"
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
>
|
||||
<g
|
||||
id="页面-1"
|
||||
stroke="none"
|
||||
stroke-width="1"
|
||||
fill="none"
|
||||
fill-rule="evenodd"
|
||||
>
|
||||
<g id="运行Bot备份-2" transform="translate(-1185, -791)">
|
||||
<g id="编组-11" transform="translate(1185, 791)">
|
||||
<g id="矩形-2" fill="#0066FF">
|
||||
<rect id="矩形" x="0" y="0" width="30" height="30" rx="15" />
|
||||
</g>
|
||||
<g
|
||||
id="发送-实色"
|
||||
transform="translate(9, 10)"
|
||||
fill="#FFFFFF"
|
||||
fill-rule="nonzero"
|
||||
>
|
||||
<path
|
||||
d="M11.6691674,0.239363769 C11.3947722,0.00461392042 11.0102658,-0.0631322672 10.6652017,0.0613280807 L0.636969787,3.6945732 C0.263984798,3.83008097 0.0191472071,4.15148685 0.00107884118,4.53436129 C-0.0170055712,4.91722033 0.193315985,5.25912086 0.553174932,5.42769394 L2.11910533,6.16191613 C2.15033183,6.17608698 2.1782688,6.19658162 2.20291623,6.21863029 C2.36557966,6.62039919 3.00806572,8.19439408 3.29396632,8.76316664 C3.43856139,9.05307662 3.70475688,9.25631519 3.95123121,9.34455602 C3.93479959,9.34297122 3.91673122,9.3398324 3.89701007,9.3366782 C3.94466819,9.35400325 3.99396306,9.36818948 4.04489467,9.37763671 C4.37023758,9.44065929 4.7037963,9.34455602 4.93712855,9.1192534 L5.52045114,8.55992807 C5.62396715,8.46065521 5.78664663,8.43860655 5.91481008,8.50792214 L8.42721154,9.87552427 C8.57836963,9.9574413 8.74597539,10 8.91358115,10 C9.04503412,10 9.17813989,9.97476635 9.3030138,9.92279119 C9.58893045,9.80462387 9.79596247,9.57142805 9.87154152,9.28153345 L11.9698789,1.1705315 C12.0602529,0.830200382 11.9435948,0.472544209 11.6691835,0.239363769 L11.6691674,0.239363769 Z M2.82731788,6.02483896 L8.20373185,3.07537951 L5.16882405,6.05635025 C5.11952919,6.10518658 5.08337641,6.16033134 5.05872897,6.2249387 C5.05707618,6.22809291 5.05707618,6.22966232 5.05543944,6.23281652 C5.04886037,6.25172637 4.44910616,7.94859682 4.09418954,8.71589971 C4.02188398,8.67020219 3.93479959,8.59616298 3.88386798,8.4937513 C3.62096202,7.96435246 3.00804967,6.46599696 2.82731788,6.02483896 L2.82731788,6.02483896 Z"
|
||||
id="形状"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
<template>
|
||||
<svg
|
||||
width="30px"
|
||||
height="30px"
|
||||
viewBox="0 0 30 30"
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<g
|
||||
id="页面-1"
|
||||
stroke="none"
|
||||
stroke-width="1"
|
||||
fill="none"
|
||||
fill-rule="evenodd"
|
||||
>
|
||||
<g id="设置bot" transform="translate(-1370, -830)">
|
||||
<g id="编组-7" transform="translate(992, 64)">
|
||||
<g id="编组-11" transform="translate(378, 766)">
|
||||
<g id="矩形-2" fill="#DFDFDF">
|
||||
<rect id="矩形" x="0" y="0" width="30" height="30" rx="15" />
|
||||
</g>
|
||||
<g
|
||||
id="发送-实色"
|
||||
transform="translate(9, 10)"
|
||||
fill="#FFFFFF"
|
||||
fill-rule="nonzero"
|
||||
>
|
||||
<path
|
||||
d="M11.6691674,0.239363769 C11.3947722,0.00461392042 11.0102658,-0.0631322672 10.6652017,0.0613280807 L0.636969787,3.6945732 C0.263984798,3.83008097 0.0191472071,4.15148685 0.00107884118,4.53436129 C-0.0170055712,4.91722033 0.193315985,5.25912086 0.553174932,5.42769394 L2.11910533,6.16191613 C2.15033183,6.17608698 2.1782688,6.19658162 2.20291623,6.21863029 C2.36557966,6.62039919 3.00806572,8.19439408 3.29396632,8.76316664 C3.43856139,9.05307662 3.70475688,9.25631519 3.95123121,9.34455602 C3.93479959,9.34297122 3.91673122,9.3398324 3.89701007,9.3366782 C3.94466819,9.35400325 3.99396306,9.36818948 4.04489467,9.37763671 C4.37023758,9.44065929 4.7037963,9.34455602 4.93712855,9.1192534 L5.52045114,8.55992807 C5.62396715,8.46065521 5.78664663,8.43860655 5.91481008,8.50792214 L8.42721154,9.87552427 C8.57836963,9.9574413 8.74597539,10 8.91358115,10 C9.04503412,10 9.17813989,9.97476635 9.3030138,9.92279119 C9.58893045,9.80462387 9.79596247,9.57142805 9.87154152,9.28153345 L11.9698789,1.1705315 C12.0602529,0.830200382 11.9435948,0.472544209 11.6691835,0.239363769 L11.6691674,0.239363769 Z M2.82731788,6.02483896 L8.20373185,3.07537951 L5.16882405,6.05635025 C5.11952919,6.10518658 5.08337641,6.16033134 5.05872897,6.2249387 C5.05707618,6.22809291 5.05707618,6.22966232 5.05543944,6.23281652 C5.04886037,6.25172637 4.44910616,7.94859682 4.09418954,8.71589971 C4.02188398,8.67020219 3.93479959,8.59616298 3.88386798,8.4937513 C3.62096202,7.96435246 3.00804967,6.46599696 2.82731788,6.02483896 L2.82731788,6.02483896 Z"
|
||||
id="形状"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
<template>
|
||||
<svg
|
||||
data-v-cabe7c8e=""
|
||||
viewBox="0 0 1000 1000"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
class="loading-svg"
|
||||
>
|
||||
<title>Loading</title>
|
||||
<rect
|
||||
fill="currentColor"
|
||||
height="250"
|
||||
rx="24"
|
||||
ry="24"
|
||||
width="250"
|
||||
x="375"
|
||||
y="375"
|
||||
/>
|
||||
<circle
|
||||
cx="500"
|
||||
cy="500"
|
||||
fill="none"
|
||||
r="450"
|
||||
stroke="currentColor"
|
||||
stroke-width="100"
|
||||
opacity="0.45"
|
||||
/>
|
||||
<circle
|
||||
cx="500"
|
||||
cy="500"
|
||||
fill="none"
|
||||
r="450"
|
||||
stroke="currentColor"
|
||||
stroke-width="100"
|
||||
stroke-dasharray="600 9999999"
|
||||
>
|
||||
<animateTransform
|
||||
attributeName="transform"
|
||||
dur="1s"
|
||||
from="0 500 500"
|
||||
repeatCount="indefinite"
|
||||
to="360 500 500"
|
||||
type="rotate"
|
||||
/>
|
||||
</circle>
|
||||
</svg>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue';
|
||||
|
||||
import { preferences } from '@aiflowy/preferences';
|
||||
|
||||
import { ElEmpty } from 'element-plus';
|
||||
import { JsonViewer } from 'vue3-json-viewer';
|
||||
|
||||
import 'vue3-json-viewer/dist/vue3-json-viewer.css';
|
||||
|
||||
defineProps({
|
||||
value: {
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const themeMode = ref(preferences.theme.mode);
|
||||
watch(
|
||||
() => preferences.theme.mode,
|
||||
(newVal) => {
|
||||
themeMode.value = newVal;
|
||||
},
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="res-container">
|
||||
<JsonViewer v-if="value" :value="value" copyable :theme="themeMode" />
|
||||
<ElEmpty
|
||||
:image="`/empty${preferences.theme.mode === 'dark' ? '-dark' : ''}.png`"
|
||||
v-else
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.res-container {
|
||||
padding: 10px;
|
||||
border: 1px solid var(--el-border-color);
|
||||
border-radius: var(--el-border-radius-base);
|
||||
}
|
||||
</style>
|
||||