首次提交

This commit is contained in:
JenniferW 2026-01-15 15:50:49 +08:00
commit ed9536f6ab
1282 changed files with 115024 additions and 0 deletions

4
.browserslistrc Normal file
View File

@ -0,0 +1,4 @@
> 1%
last 2 versions
not dead
not ie 11

5
.changeset/README.md Normal file
View File

@ -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)

18
.changeset/config.json Normal file
View File

@ -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": []
}

1
.commitlintrc.js Normal file
View File

@ -0,0 +1 @@
export { default } from '@aiflowy/commitlint-config';

7
.dockerignore Normal file
View File

@ -0,0 +1,7 @@
node_modules
.git
.gitignore
*.md
dist
.turbo
dist.zip

18
.editorconfig Normal file
View File

@ -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

11
.gitattributes vendored Normal file
View File

@ -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

2
.gitconfig Normal file
View File

@ -0,0 +1,2 @@
[core]
ignorecase = false

52
.gitignore vendored Normal file
View File

@ -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

6
.gitpod.yml Normal file
View File

@ -0,0 +1,6 @@
ports:
- port: 5555
onOpen: open-preview
tasks:
- init: npm i -g corepack && pnpm install
command: pnpm run dev:play

1
.node-version Normal file
View File

@ -0,0 +1 @@
22.1.0

13
.npmrc Normal file
View File

@ -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

18
.prettierignore Normal file
View File

@ -0,0 +1,18 @@
dist
dev-dist
.local
.output.js
node_modules
.nvmrc
coverage
CODEOWNERS
.nitro
.output
**/*.svg
**/*.sh
public
.npmrc
*-lock.yaml

1
.prettierrc.mjs Normal file
View File

@ -0,0 +1 @@
export { default } from '@aiflowy/prettier-config';

4
.stylelintignore Normal file
View File

@ -0,0 +1,4 @@
dist
public
__tests__
coverage

152
aiflowy.code-workspace Normal file
View File

@ -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",
},
],
}

8
app/.env Normal file
View File

@ -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

7
app/.env.analyze Normal file
View File

@ -0,0 +1,7 @@
# public path
VITE_BASE=/
# Basic interface address SPA
VITE_GLOB_API_URL=/api
VITE_VISUALIZER=true

13
app/.env.development Normal file
View File

@ -0,0 +1,13 @@
# 端口号
VITE_PORT=5090
VITE_BASE=/
# 接口地址
VITE_GLOB_API_URL=http://127.0.0.1:8080
# 是否打开 devtoolstrue 为打开false 为关闭
VITE_DEVTOOLS=false
# 是否注入全局loading
VITE_INJECT_APP_LOADING=true

26
app/.env.production Normal file
View File

@ -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

32
app/index.html Normal file
View File

@ -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>

59
app/package.json Normal file
View File

@ -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:"
}
}

1
app/postcss.config.mjs Normal file
View File

@ -0,0 +1 @@
export { default } from '@aiflowy/tailwind-config/postcss';

BIN
app/public/empty-dark.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

BIN
app/public/empty.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

BIN
app/public/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

11
app/public/favicon.svg Normal file
View File

@ -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

1
app/public/load.min.js vendored Normal file
View File

@ -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;

22
app/public/logo.svg Normal file
View File

@ -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

22
app/public/logoDark.svg Normal file
View File

@ -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

11
app/public/logoMini.svg Normal file
View File

@ -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

BIN
app/public/slogan.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 988 KiB

View File

@ -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}

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

1
app/public/tac/js/tac.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,331 @@
/**
* 使 adapter/form 使便使
* aiflowy-formaiflowy-modalaiflowy-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 };

41
app/src/adapter/form.ts Normal file
View File

@ -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 };

View File

@ -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';

138
app/src/api/ai/bot.ts Normal file
View File

@ -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,
}));
};

2
app/src/api/ai/index.ts Normal file
View File

@ -0,0 +1,2 @@
export * from './bot';
export * from './llm';

View File

44
app/src/api/ai/llm.ts Normal file
View File

@ -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;
}

View File

@ -0,0 +1,27 @@
/**
* B/KB/MB/GB/TB
* @param bytes - Byte
* @param decimalPlaces - 2
* @returns 1.23 MB456 B7.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 MB1.50 KB → 1.5 KB
const sizeWithoutTrailingZeros = Number.parseFloat(formattedSize).toString();
// 返回格式化结果(单位与大小拼接)
return `${sizeWithoutTrailingZeros} ${units[unitIndex]}`;
}

View File

@ -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;
}

52
app/src/api/core/auth.ts Normal file
View File

@ -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');
}

View File

@ -0,0 +1,3 @@
export * from './auth';
export * from './menu';
export * from './user';

12
app/src/api/core/menu.ts Normal file
View File

@ -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',
);
}

10
app/src/api/core/user.ts Normal file
View File

@ -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');
}

2
app/src/api/index.ts Normal file
View File

@ -0,0 +1,2 @@
export * from './ai';
export * from './core';

221
app/src/api/request.ts Normal file
View File

@ -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();

17
app/src/app.vue Normal file
View File

@ -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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -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

View File

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 408 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 668 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 173 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

90
app/src/bootstrap.ts Normal file
View File

@ -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 };

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>-->

View File

@ -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>
&lt;!&ndash; <ElIcon color="#0066FF"><RecordingIcon /></ElIcon> &ndash;&gt;
</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>

View File

@ -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.definePropertyProxy 可以监听动态添加的属性。',
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>

View File

@ -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>

View File

@ -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)[]>([]);
// idtool.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) => {
// parentIdtool.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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

Some files were not shown because too many files have changed in this diff Show More