Merge pull request 'feature/v1.3_主agent_rxd' (#37) from feature/v1.3_主agent_rxd into main
Reviewed-on: ai-team/lingji-work-fe#37
@ -29,7 +29,10 @@ export function configAutoImport() {
|
|||||||
'merge',
|
'merge',
|
||||||
'debounce',
|
'debounce',
|
||||||
'isEqual',
|
'isEqual',
|
||||||
'isString'
|
'isString',
|
||||||
|
'isArray',
|
||||||
|
'findLast',
|
||||||
|
'findLastIndex',
|
||||||
],
|
],
|
||||||
'@/hooks': ['useModal'],
|
'@/hooks': ['useModal'],
|
||||||
},
|
},
|
||||||
|
|||||||
1
env.d.ts
vendored
@ -1,4 +1,5 @@
|
|||||||
/// <reference types="vite/client" />
|
/// <reference types="vite/client" />
|
||||||
|
|
||||||
declare module '*.vue' {
|
declare module '*.vue' {
|
||||||
import type { DefineComponent } from 'vue';
|
import type { DefineComponent } from 'vue';
|
||||||
const vueComponent: DefineComponent<{}, {}, any>;
|
const vueComponent: DefineComponent<{}, {}, any>;
|
||||||
|
|||||||
@ -12,9 +12,12 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@arco-design/web-vue": "^2.42.0",
|
"@arco-design/web-vue": "^2.42.0",
|
||||||
|
"@microsoft/fetch-event-source": "^2.0.1",
|
||||||
"@types/nprogress": "^0.2.0",
|
"@types/nprogress": "^0.2.0",
|
||||||
"@vueuse/core": "^9.12.0",
|
"@vueuse/core": "^9.12.0",
|
||||||
"ali-oss": "^6.17.1",
|
"ali-oss": "^6.17.1",
|
||||||
|
"ant-design-vue": "~4.2.6",
|
||||||
|
"ant-design-x-vue": "^1.3.2",
|
||||||
"axios": "^1.3.0",
|
"axios": "^1.3.0",
|
||||||
"dayjs": "^1.11.7",
|
"dayjs": "^1.11.7",
|
||||||
"dompurify": "^3.2.6",
|
"dompurify": "^3.2.6",
|
||||||
@ -23,6 +26,7 @@
|
|||||||
"html2canvas": "^1.4.1",
|
"html2canvas": "^1.4.1",
|
||||||
"jspdf": "^3.0.1",
|
"jspdf": "^3.0.1",
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
|
"markdown-it": "^14.1.0",
|
||||||
"marked": "^16.1.1",
|
"marked": "^16.1.1",
|
||||||
"mitt": "^3.0.0",
|
"mitt": "^3.0.0",
|
||||||
"normalize.css": "^8.0.1",
|
"normalize.css": "^8.0.1",
|
||||||
@ -30,12 +34,12 @@
|
|||||||
"sass": "^1.89.2",
|
"sass": "^1.89.2",
|
||||||
"swiper": "^11.2.8",
|
"swiper": "^11.2.8",
|
||||||
"update": "^0.7.4",
|
"update": "^0.7.4",
|
||||||
"vue": "^3.2.45",
|
"vue": "^3.5.0",
|
||||||
"vue-cropper": "^1.1.4",
|
"vue-cropper": "^1.1.4",
|
||||||
"vue-draggable-next": "^2.2.1",
|
"vue-draggable-next": "^2.2.1",
|
||||||
"vue-draggable-plus": "^0.6.0",
|
"vue-draggable-plus": "^0.6.0",
|
||||||
"vue-echarts": "^7.0.3",
|
"vue-echarts": "^7.0.3",
|
||||||
"vue-router": "^4.1.6",
|
"vue-router": "^4.4.0",
|
||||||
"vuedraggable": "^4.1.0"
|
"vuedraggable": "^4.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
642
pnpm-lock.yaml
generated
10
src/App.vue
@ -7,14 +7,17 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { useUserStore } from '@/stores';
|
import { useUserStore } from '@/stores';
|
||||||
|
import { useChatStore } from '@/stores/modules/chat';
|
||||||
|
|
||||||
import { getUserEnterpriseInfo } from '@/utils/user';
|
import { initApp } from '@/utils/user';
|
||||||
import { useSidebarStore } from '@/stores/modules/side-bar';
|
import { useSidebarStore } from '@/stores/modules/side-bar';
|
||||||
|
|
||||||
import zhCN from '@arco-design/web-vue/es/locale/lang/zh-cn';
|
import zhCN from '@arco-design/web-vue/es/locale/lang/zh-cn';
|
||||||
|
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
|
const route = useRoute();
|
||||||
const sidebarStore = useSidebarStore();
|
const sidebarStore = useSidebarStore();
|
||||||
|
const chatStore = useChatStore();
|
||||||
|
|
||||||
const redTheme = {
|
const redTheme = {
|
||||||
token: {
|
token: {
|
||||||
@ -24,11 +27,10 @@ const redTheme = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const init = async () => {
|
const init = async () => {
|
||||||
const { isLogin, getUserInfo } = userStore;
|
const { isLogin } = userStore;
|
||||||
|
|
||||||
if (isLogin) {
|
if (isLogin) {
|
||||||
await getUserInfo(); // 初始化用户信息
|
await initApp();
|
||||||
await getUserEnterpriseInfo();
|
|
||||||
|
|
||||||
sidebarStore.startUnreadInfoPolling();
|
sidebarStore.startUnreadInfoPolling();
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
62
src/api/all/chat.ts
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
import Http from '@/api';
|
||||||
|
import axios from 'axios';
|
||||||
|
import { glsWithCatch } from '@/utils/stroage';
|
||||||
|
import { useEnterpriseStore } from '@/stores/modules/enterprise';
|
||||||
|
export const BASE_PYTHON_URL = 'https://agent.lvfunai.com';
|
||||||
|
import { genRandomId } from '@/utils/tools';
|
||||||
|
|
||||||
|
// 历史记录-列表
|
||||||
|
export const getAgentHistory = (id: string) => {
|
||||||
|
return Http.get(`/v1/multi-agent/history/${id}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 历史记录-更新标题
|
||||||
|
export const postUpdateSessionTitle = (data: any) => {
|
||||||
|
return Http.post('/v1/multi-agent/edit-session-title', data);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 历史记录-置顶/取消置顶
|
||||||
|
export const postUpdateSessionSort = (data: any) => {
|
||||||
|
return Http.post('/v1/multi-agent/edit-session-sort', data);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 历史记录-删除
|
||||||
|
export const deleteHistoryItem = (id: string) => {
|
||||||
|
return Http.delete(`/v1/multi-agent/del-session/${id}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 历史记录-列表
|
||||||
|
export const getConversationList = (params: {}) => {
|
||||||
|
return Http.get(`/v1/conversation/message/list`, params);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getHeaders = () => {
|
||||||
|
const store = useEnterpriseStore();
|
||||||
|
return {
|
||||||
|
Authorization: glsWithCatch('accessToken'),
|
||||||
|
'enterprise-id': store.enterpriseInfo?.id,
|
||||||
|
Accept: 'application/json',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
requestid: genRandomId(),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取智能体信息
|
||||||
|
*/
|
||||||
|
export const getAgentData = async () => {
|
||||||
|
const { data } = await axios.get(`${BASE_PYTHON_URL}/api/agent/info`, {
|
||||||
|
headers: getHeaders(),
|
||||||
|
});
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成会话id
|
||||||
|
*/
|
||||||
|
export const createSession = async () => {
|
||||||
|
const { data } = await axios.get(`${BASE_PYTHON_URL}/api/agent/create_session`, {
|
||||||
|
headers: getHeaders(),
|
||||||
|
});
|
||||||
|
return data;
|
||||||
|
};
|
||||||
@ -150,3 +150,23 @@ export const deleteShareWorksComments = (id: string, commentId: string, shareCod
|
|||||||
headers: { 'share-code': shareCode },
|
headers: { 'share-code': shareCode },
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 原料库-分页
|
||||||
|
export const getRawMaterialsPage = (params = {}) => {
|
||||||
|
return Http.get('/v1/raw-materials', params);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 原料库-删除
|
||||||
|
export const deleteRawMaterial = (id: string) => {
|
||||||
|
return Http.delete(`/v1/raw-materials/${id}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 原料库-批量删除
|
||||||
|
export const batchDeleteRawMaterials = (params = {}) => {
|
||||||
|
return Http.delete(`/v1/raw-materials/batch`, { data: params });
|
||||||
|
};
|
||||||
|
|
||||||
|
// 原料库-AI添加
|
||||||
|
export const postRawMaterialsAI = (params = {}) => {
|
||||||
|
return Http.post('/v1/raw-materials/ai', params);
|
||||||
|
};
|
||||||
|
|||||||
@ -72,7 +72,6 @@ export class Request {
|
|||||||
const { response } = err;
|
const { response } = err;
|
||||||
const status = response?.status;
|
const status = response?.status;
|
||||||
let errMessage = response?.data?.message ?? err.message;
|
let errMessage = response?.data?.message ?? err.message;
|
||||||
|
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case HttpStatusCode.InternalServerError:
|
case HttpStatusCode.InternalServerError:
|
||||||
errMessage = '系统繁忙,请稍后再试或联系管理员。';
|
errMessage = '系统繁忙,请稍后再试或联系管理员。';
|
||||||
|
|||||||
@ -1,3 +0,0 @@
|
|||||||
<svg width="148" height="32" viewBox="0 0 148 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path d="M13.082 15.38L13.342 13.17H28.188L28.396 11.428H14.694L15.006 9.244H28.682L28.916 7.528H14.278L14.538 5.292H29.28C30.06 5.292 30.6407 5.49133 31.022 5.89C31.4033 6.27133 31.5507 6.83467 31.464 7.58L30.502 15.38H13.082ZM30.71 27.262C28.6647 26.3433 27.0007 25.4333 25.718 24.532C24.2273 23.492 22.91 22.2787 21.766 20.892C20.778 22.3653 19.3913 23.6307 17.606 24.688C16.15 25.5547 14.304 26.4127 12.068 27.262L10.274 25.442C11.9727 24.7487 13.316 24.1767 14.304 23.726C15.292 23.258 16.202 22.738 17.034 22.166C18.0047 21.5073 18.802 20.7707 19.426 19.956C20.0673 19.124 20.5093 18.1707 20.752 17.096L20.934 15.978H23.56L23.326 17.148C23.1873 17.7373 23.04 18.2313 22.884 18.63C24.1147 20.242 25.4927 21.542 27.018 22.53C27.7633 23.0153 28.5867 23.466 29.488 23.882C30.4067 24.2807 31.49 24.7053 32.738 25.156L30.71 27.262ZM28.942 16.732H31.49L28.63 20.944H25.978L28.942 16.732ZM17.684 20.944H15.11L12.77 16.914H15.24L17.684 20.944ZM40.98 15.926C39.9747 17.8327 38.9347 19.4793 37.86 20.866L35.884 19.722C37.8253 17.2607 39.5847 14.3833 41.162 11.09H37.808L38.068 8.802H41.864L42.41 4.46H44.828L44.282 8.802H47.09L46.856 11.09H43.996L43.918 11.688C44.802 13.1267 45.738 14.912 46.726 17.044L44.828 18.214C44.412 17.174 43.97 16.1427 43.502 15.12L42.046 26.82H39.628L40.98 15.926ZM55.046 7.996H50.99L49.976 16.342C49.404 21.1433 48.1647 24.714 46.258 27.054L43.866 26.144C44.8887 24.8267 45.686 23.3793 46.258 21.802C46.83 20.2073 47.246 18.37 47.506 16.29L48.546 7.554C48.6327 6.89533 48.8147 6.41867 49.092 6.124C49.3867 5.812 49.8373 5.656 50.444 5.656H55.774C56.3633 5.656 56.814 5.80333 57.126 6.098C57.438 6.39267 57.594 6.81733 57.594 7.372C57.594 7.56267 57.5853 7.71 57.568 7.814L55.748 23.622C55.7307 23.674 55.722 23.752 55.722 23.856C55.722 24.012 55.7653 24.116 55.852 24.168C55.9387 24.22 56.086 24.246 56.294 24.246C57.1953 24.246 58.114 23.9947 59.05 23.492L58.764 26.118C58.0533 26.4647 57.1607 26.638 56.086 26.638H55.124C53.824 26.638 53.174 26.0053 53.174 24.74C53.174 24.532 53.1827 24.3673 53.2 24.246L55.046 7.996ZM73.922 6.41C74.8927 6.41 75.456 6.878 75.612 7.814L78.576 25.936H75.326L74.468 20.294H66.07L63.808 25.936H60.454L67.942 7.606C68.2713 6.80867 68.878 6.41 69.762 6.41H73.922ZM70.516 9.27L67.214 17.434H74.026L72.778 9.27H70.516ZM83.2706 6.41H86.3386L83.9466 25.936H80.8786L83.2706 6.41ZM103.385 4.486H105.855L105.621 5.994H111.133L110.821 8.23H105.309L105.153 9.426H102.735L102.891 8.23H97.7169L97.5609 9.426H95.1169L95.2729 8.23H89.7869L90.0989 5.994H95.5589L95.7669 4.486H98.2369L98.0289 5.994H103.177L103.385 4.486ZM107.961 9.92C108.793 9.92 109.382 10.0587 109.729 10.336C110.093 10.6133 110.275 11.038 110.275 11.61C110.275 11.818 110.266 11.9827 110.249 12.104L109.989 14.626H107.493L107.805 11.974H92.5689L92.2569 14.626H89.7089L90.0209 12.052C90.1249 11.3067 90.3589 10.7693 90.7229 10.44C91.0869 10.0933 91.6589 9.92 92.4389 9.92H107.961ZM106.479 18.942H92.6209L93.0369 15.068C93.1235 14.3573 93.3229 13.8807 93.6349 13.638C93.9642 13.3953 94.5275 13.274 95.3249 13.274H104.919C105.664 13.274 106.202 13.456 106.531 13.82C106.878 14.1667 107.016 14.6173 106.947 15.172L106.479 18.942ZM95.3769 16.966H104.295L104.529 15.25H95.5329L95.3769 16.966ZM107.519 26.664H90.3849L90.9309 22.27C91.1215 20.8313 91.9882 20.112 93.5309 20.112H105.959C107.519 20.112 108.221 20.8227 108.065 22.244L107.519 26.664ZM93.2709 24.506H105.205L105.491 22.244H93.5569L93.2709 24.506ZM132.583 4.564L131.829 10.7H134.481C135.989 10.7 136.674 11.4453 136.535 12.936L135.157 25.13C135.07 25.6673 134.854 26.0833 134.507 26.378C134.178 26.6727 133.762 26.82 133.259 26.82C132.167 26.82 130.884 26.6727 129.411 26.378L129.645 24.168C130.616 24.376 131.56 24.4973 132.479 24.532H132.609C132.73 24.532 132.808 24.506 132.843 24.454C132.878 24.402 132.904 24.2893 132.921 24.116L133.207 21.62H125.927L125.329 26.716H123.015L124.627 12.806C124.696 12.1473 124.922 11.636 125.303 11.272C125.702 10.8907 126.239 10.7 126.915 10.7H129.437L130.165 4.564H132.583ZM124.211 6.826L123.769 9.088H117.373C116.801 9.088 116.515 8.88 116.515 8.464C116.515 8.308 116.558 8.11733 116.645 7.892L117.893 4.616H120.441L119.661 6.826H124.211ZM127.409 5.474L128.475 9.686H126.057L125.043 5.474H127.409ZM137.211 5.552L135.625 9.712H133.233L134.819 5.552H137.211ZM123.067 16.628L122.807 18.89H119.791L119.297 22.894V22.972C119.297 23.2147 119.375 23.336 119.531 23.336L119.739 23.284C120.779 22.92 121.689 22.556 122.469 22.192L122.183 24.766C120.952 25.286 119.73 25.702 118.517 26.014C118.344 26.066 118.17 26.092 117.997 26.092C117.564 26.092 117.226 25.9273 116.983 25.598C116.758 25.286 116.68 24.8527 116.749 24.298L117.399 18.89H114.851L115.111 16.628H117.685L118.023 13.846H115.631L115.891 11.61H123.223L122.963 13.846H120.415L120.051 16.628H123.067ZM134.195 12.884H126.967L126.707 15.068H133.935L134.195 12.884ZM133.701 17.2H126.447L126.187 19.488H133.441L133.701 17.2Z" fill="#6D4CFE"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 4.9 KiB |
|
Before Width: | Height: | Size: 5.4 KiB |
BIN
src/assets/img/agent/icon-end.png
Normal file
|
After Width: | Height: | Size: 777 B |
BIN
src/assets/img/agent/icon-history.png
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
BIN
src/assets/img/agent/icon-loading.png
Normal file
|
After Width: | Height: | Size: 702 B |
BIN
src/assets/img/agent/icon1.png
Normal file
|
After Width: | Height: | Size: 2.5 KiB |
BIN
src/assets/img/icon-app-bg.png
Normal file
|
After Width: | Height: | Size: 783 KiB |
BIN
src/assets/img/icon-app-header-bg.png
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
src/assets/img/icon-logo.png
Normal file
|
After Width: | Height: | Size: 4.5 KiB |
6
src/assets/svg/svg-accountManage-active.svg
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 18 18" fill="none">
|
||||||
|
<path d="M15.75 1.5C16.5784 1.5 17.25 2.17157 17.25 3V15C17.25 15.8284 16.5784 16.5 15.75 16.5H3.75C2.92157 16.5 2.25 15.8284 2.25 15V12.75H3.75C4.57843 12.75 5.25 12.0784 5.25 11.25C5.25 10.4216 4.57843 9.75 3.75 9.75H2.25V8.25H3.75C4.57843 8.25 5.25 7.57843 5.25 6.75C5.25 5.92157 4.57843 5.25 3.75 5.25H2.25V3C2.25 2.17157 2.92157 1.5 3.75 1.5H15.75Z" fill="#6D4CFE"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M10.1248 4.59375C11.7298 4.59375 13.031 5.89492 13.031 7.5C13.031 8.3054 12.7033 9.03394 12.1741 9.5603C13.4265 10.175 14.3629 11.3342 14.6724 12.7302C14.7518 13.0889 14.5249 13.4439 14.1663 13.5234C13.8076 13.6028 13.4526 13.3767 13.3731 13.0181C13.0428 11.5287 11.7128 10.4151 10.1248 10.415C8.53684 10.4152 7.20745 11.5288 6.87721 13.0181C6.79767 13.3768 6.44201 13.603 6.08326 13.5234C5.72474 13.4438 5.49846 13.0888 5.57789 12.7302C5.88742 11.3342 6.82321 10.1743 8.07545 9.55957C7.54658 9.03325 7.21852 8.30508 7.21852 7.5C7.21852 5.89501 8.51981 4.59389 10.1248 4.59375ZM10.1248 5.90625C9.24468 5.90639 8.53102 6.61988 8.53102 7.5C8.53102 8.33817 9.17843 9.02345 10.0003 9.08716C10.0416 9.08607 10.0831 9.08496 10.1248 9.08496C10.1664 9.08496 10.2079 9.08607 10.2493 9.08716C11.0713 9.02369 11.7185 8.33834 11.7185 7.5C11.7185 6.6198 11.005 5.90625 10.1248 5.90625Z" fill="white"/>
|
||||||
|
<path d="M3.75 10.5C4.16421 10.5 4.5 10.8358 4.5 11.25C4.5 11.6642 4.16421 12 3.75 12H1.5C1.08579 12 0.75 11.6642 0.75 11.25C0.75 10.8358 1.08579 10.5 1.5 10.5H3.75Z" fill="#39C6E9"/>
|
||||||
|
<path d="M3.75 6C4.16421 6 4.5 6.33579 4.5 6.75C4.5 7.16421 4.16421 7.5 3.75 7.5H1.5C1.08579 7.5 0.75 7.16421 0.75 6.75C0.75 6.33579 1.08579 6 1.5 6H3.75Z" fill="#39C6E9"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.7 KiB |
4
src/assets/svg/svg-accountManage.svg
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 18 18" fill="none">
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M10.125 4.59375C11.7301 4.59375 13.0312 5.89492 13.0312 7.5C13.0312 8.30533 12.7035 9.03395 12.1743 9.5603C13.4266 10.175 14.3631 11.3343 14.6726 12.7302C14.752 13.0888 14.525 13.4438 14.1665 13.5234C13.8078 13.6028 13.4528 13.3767 13.3733 13.0181C13.0431 11.5288 11.7129 10.4152 10.125 10.415C8.53709 10.4152 7.2077 11.5289 6.87744 13.0181C6.79792 13.3767 6.44216 13.6028 6.0835 13.5234C5.72497 13.4438 5.4987 13.0888 5.57812 12.7302C5.88761 11.3345 6.82306 10.1751 8.07495 9.5603C7.54601 9.03398 7.21875 8.30515 7.21875 7.5C7.21875 5.89492 8.51992 4.59375 10.125 4.59375ZM10.125 5.90625C9.2448 5.90625 8.53125 6.6198 8.53125 7.5C8.53125 8.33825 9.17854 9.02357 10.0005 9.08716C10.0418 9.08608 10.0835 9.08496 10.125 9.08496C10.1664 9.08496 10.2076 9.08608 10.2488 9.08716C11.0711 9.02392 11.7188 8.3385 11.7188 7.5C11.7188 6.6198 11.0052 5.90625 10.125 5.90625Z" fill="currentColor"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M15.75 1.5C16.5784 1.5 17.25 2.17157 17.25 3V15C17.25 15.8284 16.5784 16.5 15.75 16.5H3.75C2.92157 16.5 2.25 15.8284 2.25 15V12H1.5C1.08579 12 0.75 11.6642 0.75 11.25C0.75 10.8358 1.08579 10.5 1.5 10.5H2.25V7.5H1.5C1.08579 7.5 0.75 7.16421 0.75 6.75C0.75 6.33579 1.08579 6 1.5 6H2.25V3C2.25 2.17157 2.92157 1.5 3.75 1.5H15.75ZM3.75 6H4.5C4.91421 6 5.25 6.33579 5.25 6.75C5.25 7.16421 4.91421 7.5 4.5 7.5H3.75V10.5H4.5C4.91421 10.5 5.25 10.8358 5.25 11.25C5.25 11.6642 4.91421 12 4.5 12H3.75V15H15.75V3H3.75V6Z" fill="currentColor"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.6 KiB |
4
src/assets/svg/svg-finishProductsWareHouse.svg
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="none">
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.01884 6.07878C7.23558 5.96285 7.49856 5.97564 7.70308 6.11198L9.70308 7.44532C9.88855 7.56896 9.99995 7.77711 9.99996 8C9.99996 8.2229 9.88855 8.43105 9.70308 8.55469L7.70308 9.88803C7.49856 10.0244 7.23558 10.0372 7.01884 9.92123C6.80207 9.80521 6.66663 9.5792 6.66663 9.33334V6.66667L6.67314 6.57553C6.70203 6.36603 6.8292 6.18027 7.01884 6.07878ZM7.99996 8.08724L8.13147 8L7.99996 7.91211V8.08724Z" fill="currentColor"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M12.3333 1.66667C12.7015 1.66667 13 1.96515 13 2.33334V3.00001H14.6666C15.0348 3.00001 15.3333 3.29848 15.3333 3.66667V12.3333C15.3333 12.7015 15.0348 13 14.6666 13H13V13.6667C13 14.0349 12.7015 14.3333 12.3333 14.3333H3.66663C3.29844 14.3333 2.99996 14.0349 2.99996 13.6667V13H1.33329C0.965103 13 0.666626 12.7015 0.666626 12.3333V3.66667C0.666626 3.29848 0.965103 3.00001 1.33329 3.00001H2.99996V2.33334C2.99996 1.96515 3.29844 1.66667 3.66663 1.66667H12.3333ZM4.33329 13H11.6666V3.00001H4.33329V13ZM1.99996 11.6667H2.99996V4.33334H1.99996V11.6667ZM13 11.6667H14V4.33334H13V11.6667Z" fill="currentColor"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.2 KiB |
6
src/assets/svg/svg-management-active.svg
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 18 18" fill="none">
|
||||||
|
<path d="M7.19971 1.875C7.61392 1.875 7.94971 2.21079 7.94971 2.625V7.5C7.94971 7.91421 7.61392 8.25 7.19971 8.25H2.25C1.83579 8.25 1.5 7.91421 1.5 7.5V2.625C1.5 2.21079 1.83579 1.875 2.25 1.875H7.19971Z" fill="#6D4CFE"/>
|
||||||
|
<path d="M7.19971 9.75C7.61392 9.75 7.94971 10.0858 7.94971 10.5V15.375C7.94971 15.7892 7.61392 16.125 7.19971 16.125H2.25C1.83579 16.125 1.5 15.7892 1.5 15.375V10.5C1.5 10.0858 1.83579 9.75 2.25 9.75H7.19971Z" fill="#6D4CFE"/>
|
||||||
|
<path d="M15.0747 1.875C15.4889 1.875 15.8247 2.21079 15.8247 2.625V7.5C15.8247 7.91421 15.4889 8.25 15.0747 8.25H10.125C9.71079 8.25 9.375 7.91421 9.375 7.5V2.625C9.375 2.21079 9.71079 1.875 10.125 1.875H15.0747Z" fill="#6D4CFE"/>
|
||||||
|
<path d="M15.0747 9.75C15.4889 9.75 15.8247 10.0858 15.8247 10.5V15.375C15.8247 15.7892 15.4889 16.125 15.0747 16.125H10.125C9.71079 16.125 9.375 15.7892 9.375 15.375V10.5C9.375 10.0858 9.71079 9.75 10.125 9.75H15.0747Z" fill="#39C6E9"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.0 KiB |
3
src/assets/svg/svg-management.svg
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 18 18" fill="none">
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M2.625 1.875C2.21079 1.875 1.875 2.21079 1.875 2.625V7.5C1.875 7.91421 2.21079 8.25 2.625 8.25H7.5C7.91421 8.25 8.25 7.91421 8.25 7.5V2.625C8.25 2.21079 7.91421 1.875 7.5 1.875H2.625ZM3.375 6.75V3.375H6.75V6.75H3.375ZM10.5 1.875C10.0858 1.875 9.75 2.21079 9.75 2.625V7.5C9.75 7.91421 10.0858 8.25 10.5 8.25H15.375C15.7892 8.25 16.125 7.91421 16.125 7.5V2.625C16.125 2.21079 15.7892 1.875 15.375 1.875H10.5ZM11.25 6.75V3.375H14.625V6.75H11.25ZM1.875 10.5C1.875 10.0858 2.21079 9.75 2.625 9.75H7.5C7.91421 9.75 8.25 10.0858 8.25 10.5V15.375C8.25 15.7892 7.91421 16.125 7.5 16.125H2.625C2.21079 16.125 1.875 15.7892 1.875 15.375V10.5ZM3.375 11.25V14.625H6.75V11.25H3.375ZM10.5 9.75C10.0858 9.75 9.75 10.0858 9.75 10.5V15.375C9.75 15.7892 10.0858 16.125 10.5 16.125H15.375C15.7892 16.125 16.125 15.7892 16.125 15.375V10.5C16.125 10.0858 15.7892 9.75 15.375 9.75H10.5ZM11.25 14.625V11.25H14.625V14.625H11.25Z" fill="currentColor"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.1 KiB |
6
src/assets/svg/svg-managementAccount.svg
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="none">
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M12.6666 8.66671C13.7712 8.66671 14.6666 9.56214 14.6666 10.6667V12.6667C14.6666 13.7713 13.7712 14.6667 12.6666 14.6667H3.33325C2.22868 14.6667 1.33325 13.7713 1.33325 12.6667V10.6667C1.33325 9.56214 2.22868 8.66671 3.33325 8.66671H12.6666ZM3.33325 10C2.96506 10 2.66659 10.2985 2.66659 10.6667V12.6667C2.66659 13.0349 2.96506 13.3334 3.33325 13.3334H12.6666C13.0348 13.3334 13.3333 13.0349 13.3333 12.6667V10.6667C13.3333 10.2985 13.0348 10 12.6666 10H3.33325Z" fill="currentColor"/>
|
||||||
|
<path d="M11.3333 3.66671C11.8855 3.66671 12.3333 4.11442 12.3333 4.66671C12.3333 5.21899 11.8855 5.66671 11.3333 5.66671C10.781 5.66671 10.3333 5.21899 10.3333 4.66671C10.3333 4.11442 10.781 3.66671 11.3333 3.66671Z" fill="currentColor"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.3333 1.33337C13.1742 1.33337 14.6666 2.82576 14.6666 4.66671C14.6666 6.50766 13.1742 8.00004 11.3333 8.00004C9.4923 8.00004 7.99992 6.50766 7.99992 4.66671C7.99992 2.82576 9.4923 1.33337 11.3333 1.33337ZM11.3333 2.66671C10.2287 2.66671 9.33325 3.56214 9.33325 4.66671C9.33325 5.77128 10.2287 6.66671 11.3333 6.66671C12.4378 6.66671 13.3333 5.77128 13.3333 4.66671C13.3333 3.56214 12.4378 2.66671 11.3333 2.66671Z" fill="currentColor"/>
|
||||||
|
<path d="M6.66659 1.66671C7.03478 1.66671 7.33325 1.96518 7.33325 2.33337C7.33325 2.70156 7.03478 3.00004 6.66659 3.00004H3.33325C2.96506 3.00004 2.66659 3.29852 2.66659 3.66671V5.66671C2.66659 6.03489 2.96507 6.33337 3.33325 6.33337H6.66659C7.03478 6.33337 7.33325 6.63185 7.33325 7.00004C7.33325 7.36823 7.03478 7.66671 6.66659 7.66671H3.33325C2.22868 7.66671 1.33325 6.77126 1.33325 5.66671V3.66671C1.33325 2.56214 2.22868 1.66671 3.33325 1.66671H6.66659Z" fill="currentColor"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.8 KiB |
10
src/assets/svg/svg-managementEnterprise.svg
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="none">
|
||||||
|
<g clip-path="url(#clip0_1822_2135)">
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.69214 0.742281C6.91258 0.627508 7.17861 0.644734 7.38224 0.787203L10.7156 3.12054C10.8938 3.24529 11.0001 3.44924 11.0001 3.66676V7.657L13.0541 9.12444C13.2293 9.24959 13.3334 9.45149 13.3334 9.66676V14.0001H14.6667C15.0349 14.0001 15.3334 14.2986 15.3334 14.6668C15.3334 15.0349 15.0349 15.3334 14.6667 15.3334H1.33341C0.965261 15.3334 0.666807 15.0349 0.666748 14.6668C0.666748 14.2986 0.965225 14.0001 1.33341 14.0001H3.00008V6.66676C3.00008 6.44924 3.1064 6.24529 3.28459 6.12054L6.33341 3.98577V1.33343L6.33992 1.24163C6.36934 1.02986 6.49924 0.842793 6.69214 0.742281ZM4.33341 7.01377V14.0001H6.33341V5.61338L4.33341 7.01377ZM7.66675 14.0001H12.0001V10.0092L9.94604 8.54241C9.77088 8.4173 9.66679 8.21534 9.66675 8.00009V4.01377L7.66675 2.61338V14.0001Z" fill="currentColor"/>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<clipPath id="clip0_1822_2135">
|
||||||
|
<rect width="16" height="16" fill="white"/>
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.1 KiB |
6
src/assets/svg/svg-managementPerson.svg
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="none">
|
||||||
|
<path d="M6.66675 2.66663C7.03494 2.66663 7.33341 2.9651 7.33341 3.33329C7.33341 3.70148 7.03494 3.99996 6.66675 3.99996H2.00008V12.6666H14.0001V11.8333C14.0001 11.4651 14.2986 11.1666 14.6667 11.1666C15.0349 11.1666 15.3334 11.4651 15.3334 11.8333V12.6666C15.3334 13.403 14.7365 14 14.0001 14H2.00008C1.26371 14 0.666748 13.403 0.666748 12.6666V3.99996C0.666748 3.26356 1.26371 2.66663 2.00008 2.66663H6.66675Z" fill="currentColor"/>
|
||||||
|
<path d="M11.3334 9.66663C11.7016 9.66663 12.0001 9.9651 12.0001 10.3333C12.0001 10.7015 11.7016 11 11.3334 11H3.33341C2.96522 11 2.66675 10.7015 2.66675 10.3333C2.66675 9.9651 2.96522 9.66663 3.33341 9.66663H11.3334Z" fill="currentColor"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.3334 2.66663C12.8062 2.66663 14.0001 3.86053 14.0001 5.33329C14.0001 6.04266 13.722 6.68629 13.2703 7.16402C14.107 7.60518 14.8215 8.31147 15.2605 9.16923C15.428 9.49687 15.2983 9.89858 14.9708 10.0664C14.6431 10.2341 14.2414 10.1043 14.0736 9.77665C13.526 8.70664 12.3939 8.0001 11.3334 7.99996C10.7314 7.99999 10.3219 8.08478 10.0027 8.2161C9.68314 8.34763 9.41097 8.54307 9.09839 8.82678C8.82582 9.07418 8.40446 9.05368 8.15698 8.78121C7.90953 8.50858 7.92994 8.08726 8.20255 7.8398C8.52921 7.54331 8.87871 7.27263 9.30802 7.06702C8.90854 6.60077 8.66675 5.99542 8.66675 5.33329C8.66675 3.86053 9.86065 2.66663 11.3334 2.66663ZM11.3334 3.99996C10.597 3.99996 10.0001 4.59691 10.0001 5.33329C10.0001 6.06967 10.597 6.66663 11.3334 6.66663C12.0698 6.66663 12.6667 6.06967 12.6667 5.33329C12.6667 4.59691 12.0698 3.99996 11.3334 3.99996Z" fill="currentColor"/>
|
||||||
|
<path d="M6.00008 6.99996C6.36827 6.99996 6.66675 7.29844 6.66675 7.66663C6.66675 8.03482 6.36827 8.33329 6.00008 8.33329H3.33341C2.96522 8.33329 2.66675 8.03482 2.66675 7.66663C2.66675 7.29844 2.96522 6.99996 3.33341 6.99996H6.00008Z" fill="currentColor"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.9 KiB |
4
src/assets/svg/svg-materialCenter-active.svg
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 18 18" fill="none">
|
||||||
|
<path d="M9.12668 3.48014C9.26917 3.65113 9.48026 3.75 9.70284 3.75H15.375C16.2034 3.75 16.875 4.42156 16.875 5.25V6.75H1.125V3C1.125 2.17157 1.79657 1.5 2.625 1.5H7.12528C7.34787 1.5 7.55895 1.59887 7.70145 1.76986L9.12668 3.48014Z" fill="#39C6E9"/>
|
||||||
|
<path d="M16.875 15C16.875 15.8284 16.2034 16.5 15.375 16.5H2.625C1.79658 16.5 1.125 15.8284 1.125 15V8.25H16.875V15Z" fill="#6D4CFE"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 493 B |
3
src/assets/svg/svg-materialCenter.svg
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 18 18" fill="none">
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.20776 1.50439C7.39923 1.52564 7.57672 1.61993 7.70142 1.76953L9.35156 3.75H15.375C16.2034 3.75 16.875 4.42156 16.875 5.25V15C16.875 15.8284 16.2034 16.5 15.375 16.5H2.625C1.79658 16.5 1.125 15.8284 1.125 15V3C1.125 2.17157 1.79657 1.5 2.625 1.5H7.125L7.20776 1.50439ZM2.625 8.25V15H15.375V8.25H2.625ZM2.625 6.75H15.375V5.25H9C8.77745 5.25 8.56608 5.15142 8.42358 4.98047L6.77344 3H2.625V6.75Z" fill="currentColor"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 572 B |
6
src/assets/svg/svg-mediaAccountData.svg
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="none">
|
||||||
|
<path d="M10.2279 4.16463C10.505 3.92241 10.9263 3.95078 11.1687 4.22778C11.4109 4.50487 11.3826 4.92615 11.1055 5.16854L5.77222 9.83521C5.49512 10.0774 5.07385 10.0491 4.83146 9.77205C4.58924 9.49496 4.6176 9.07368 4.89461 8.8313L10.2279 4.16463Z" fill="currentColor"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.0001 6.66659C11.9206 6.66659 12.6667 7.41278 12.6667 8.33325C12.6667 9.25373 11.9206 9.99992 11.0001 9.99992C10.0796 9.99992 9.33341 9.25373 9.33341 8.33325C9.33341 7.41278 10.0796 6.66659 11.0001 6.66659ZM11.0001 7.99992C10.816 7.99992 10.6667 8.14916 10.6667 8.33325C10.6667 8.51735 10.816 8.66659 11.0001 8.66659C11.1842 8.66659 11.3334 8.51735 11.3334 8.33325C11.3334 8.14916 11.1842 7.99992 11.0001 7.99992Z" fill="currentColor"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.00008 3.99992C5.92056 3.99992 6.66675 4.74611 6.66675 5.66659C6.66675 6.58706 5.92056 7.33325 5.00008 7.33325C4.07961 7.33325 3.33341 6.58706 3.33341 5.66659C3.33341 4.74611 4.07961 3.99992 5.00008 3.99992ZM5.00008 5.33325C4.81599 5.33325 4.66675 5.48249 4.66675 5.66659C4.66675 5.85068 4.81599 5.99992 5.00008 5.99992C5.18418 5.99992 5.33341 5.85068 5.33341 5.66659C5.33341 5.48249 5.18418 5.33325 5.00008 5.33325Z" fill="currentColor"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M13.6667 1.33325C14.5872 1.33325 15.3334 2.07944 15.3334 2.99992V10.9999C15.3334 11.9204 14.5872 12.6666 13.6667 12.6666H8.66675V13.6666H12.6667C13.0349 13.6666 13.3334 13.9651 13.3334 14.3333C13.3334 14.7014 13.0349 14.9999 12.6667 14.9999H3.33341C2.96522 14.9999 2.66675 14.7014 2.66675 14.3333C2.66675 13.9651 2.96522 13.6666 3.33341 13.6666H7.33341V12.6666H2.33341C1.41294 12.6666 0.666748 11.9204 0.666748 10.9999V2.99992C0.666748 2.07944 1.41294 1.33325 2.33341 1.33325H13.6667ZM2.33341 2.66659C2.14932 2.66659 2.00008 2.81582 2.00008 2.99992V10.9999C2.00008 11.184 2.14932 11.3333 2.33341 11.3333H13.6667C13.8508 11.3333 14.0001 11.184 14.0001 10.9999V2.99992C14.0001 2.81582 13.8508 2.66659 13.6667 2.66659H2.33341Z" fill="currentColor"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.1 KiB |
4
src/assets/svg/svg-mediaAccountManage.svg
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="none">
|
||||||
|
<path d="M7.99992 1.33325C8.36811 1.33325 8.66659 1.63173 8.66659 1.99992C8.66659 2.36811 8.36811 2.66659 7.99992 2.66659H2.99992C2.81583 2.66659 2.66659 2.81583 2.66659 2.99992V10.3333C2.66659 10.5174 2.81582 10.6666 2.99992 10.6666H12.9999C13.184 10.6666 13.3333 10.5174 13.3333 10.3333V8.66659C13.3333 8.2984 13.6317 7.99992 13.9999 7.99992C14.3681 7.99992 14.6666 8.2984 14.6666 8.66659V10.3333C14.6666 11.2537 13.9204 11.9999 12.9999 11.9999H8.66659V13.3333H11.3333C11.7014 13.3333 11.9999 13.6317 11.9999 13.9999C11.9999 14.3681 11.7014 14.6666 11.3333 14.6666H4.66659C4.2984 14.6666 3.99992 14.3681 3.99992 13.9999C3.99992 13.6317 4.2984 13.3333 4.66659 13.3333H7.33325V11.9999H2.99992C2.07945 11.9999 1.33325 11.2537 1.33325 10.3333V2.99992C1.33325 2.07945 2.07945 1.33325 2.99992 1.33325H7.99992Z" fill="currentColor"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M12.3333 1.33325C12.7014 1.33325 12.9999 1.63173 12.9999 1.99992V2.80591C13.1143 2.8559 13.221 2.91954 13.3202 2.99276L14.0208 2.58911C14.3395 2.40525 14.7468 2.51462 14.9309 2.83325C15.115 3.15207 15.0062 3.55993 14.6874 3.74406L13.9889 4.14705C13.9957 4.20821 13.9999 4.27028 13.9999 4.33325C13.9999 4.396 13.9956 4.45785 13.9889 4.5188L14.6874 4.92244C15.0062 5.10658 15.115 5.51443 14.9309 5.83325C14.7468 6.15192 14.3395 6.26127 14.0208 6.07739L13.3202 5.6731C13.2209 5.74636 13.1143 5.80991 12.9999 5.85994V6.66659C12.9999 7.03478 12.7014 7.33325 12.3333 7.33325C11.9651 7.33325 11.6666 7.03478 11.6666 6.66659V5.85994C11.552 5.80984 11.445 5.74649 11.3456 5.6731L10.6458 6.07739C10.3269 6.26138 9.91901 6.15207 9.73495 5.83325C9.55094 5.51443 9.66031 5.10654 9.97909 4.92244L10.677 4.5188C10.6703 4.45787 10.6666 4.39598 10.6666 4.33325C10.6666 4.27031 10.6702 4.20819 10.677 4.14705L9.97909 3.74406C9.66025 3.55997 9.55089 3.1521 9.73495 2.83325C9.91903 2.51442 10.3269 2.40507 10.6458 2.58911L11.3456 2.99276C11.445 2.91941 11.552 2.85598 11.6666 2.80591V1.99992C11.6666 1.63173 11.9651 1.33325 12.3333 1.33325ZM12.3333 3.99992C12.1492 3.99992 11.9999 4.14916 11.9999 4.33325C11.9999 4.51735 12.1492 4.66659 12.3333 4.66659C12.5173 4.66659 12.6666 4.51735 12.6666 4.33325C12.6666 4.14916 12.5173 3.99992 12.3333 3.99992Z" fill="currentColor"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.3 KiB |
3
src/assets/svg/svg-pushpin.svg
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 14 14" fill="none">
|
||||||
|
<path d="M6.10963 5.99245L5.88754 6.00922L2.72457 6.2548L7.74479 11.275L7.99037 8.11205L8.00714 7.88996L10.5716 5.3255L8.67409 3.42799L6.10963 5.99245ZM9.17397 8.42741L8.8472 12.6525C8.80683 13.1737 8.17358 13.4078 7.80384 13.0383L4.80861 10.0431L1.9355 12.9162C1.85964 12.9921 1.7363 12.992 1.6604 12.9162L1.08404 12.3399C1.00814 12.264 1.00814 12.1407 1.08404 12.0648L3.95715 9.19165L0.961248 6.19575C0.592209 5.82604 0.826163 5.19293 1.34706 5.15239L5.57218 4.82562L7.82196 2.57585L6.9356 1.68949C6.86001 1.61358 6.86048 1.49086 6.93627 1.41506L7.51264 0.838698C7.58845 0.763107 7.71122 0.7625 7.78706 0.838028L13.1616 6.21253C13.2374 6.28835 13.2373 6.41172 13.1616 6.48763L12.5852 7.06399C12.5093 7.13989 12.386 7.13989 12.3101 7.06399L11.4237 6.17764L9.17397 8.42741Z" fill="currentColor"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 901 B |
6
src/assets/svg/svg-putAccountAccountDashboard.svg
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="none">
|
||||||
|
<path d="M4.37052 4C4.73864 4.00009 5.03719 4.29853 5.03719 4.66667C5.03719 5.0348 4.73864 5.33325 4.37052 5.33333H3.70386C3.33567 5.33333 3.03719 5.03486 3.03719 4.66667C3.03719 4.29848 3.33567 4 3.70386 4H4.37052Z" fill="currentColor"/>
|
||||||
|
<path d="M12.3705 4C12.7387 4 13.0372 4.29848 13.0372 4.66667C13.0372 5.03486 12.7387 5.33333 12.3705 5.33333H6.37052C6.00241 5.33325 5.70386 5.0348 5.70386 4.66667C5.70386 4.29853 6.00241 4.00009 6.37052 4H12.3705Z" fill="currentColor"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M13.6667 2C14.5872 2 15.3334 2.7462 15.3334 3.66667V12.3333C15.3334 13.2538 14.5872 14 13.6667 14H4.00008C3.63189 14 3.33341 13.7015 3.33341 13.3333C3.33341 12.9651 3.63189 12.6667 4.00008 12.6667H13.6667C13.8509 12.6667 14.0001 12.5174 14.0001 12.3333V7.33333H2.00008V9C2.00008 9.36819 1.7016 9.66667 1.33341 9.66667C0.965225 9.66667 0.666748 9.36819 0.666748 9V3.66667C0.666748 2.74619 1.41294 2 2.33341 2H13.6667ZM2.33341 3.33333C2.14932 3.33333 2.00008 3.48257 2.00008 3.66667V6H14.0001V3.66667C14.0001 3.48257 13.8509 3.33333 13.6667 3.33333H2.33341Z" fill="currentColor"/>
|
||||||
|
<path d="M9.91545 8.12435C10.1752 7.93809 10.5357 7.96212 10.769 8.18945L12.2631 9.64518C12.5264 9.90214 12.5315 10.3243 12.2748 10.5879C12.0179 10.8514 11.5958 10.857 11.3321 10.6003L10.2787 9.57357L8.06128 11.5117C7.80135 11.7389 7.41092 11.7302 7.16154 11.4915L5.44214 9.8444L1.84513 13.4792C1.58619 13.7408 1.16412 13.7425 0.902425 13.4837C0.640767 13.2247 0.638261 12.8027 0.897217 12.541L4.95581 8.44076L5.00594 8.39518C5.26355 8.1827 5.64443 8.19254 5.89071 8.42839L7.64331 10.1061L9.86532 8.16471L9.91545 8.12435Z" fill="currentColor"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.7 KiB |
4
src/assets/svg/svg-putAccountData.svg
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="none">
|
||||||
|
<path d="M11.3333 5.33325C11.3749 5.33325 11.4155 5.33767 11.455 5.34497C11.4567 5.34529 11.4585 5.34529 11.4602 5.34562C11.4774 5.34894 11.4943 5.35337 11.511 5.35799C11.515 5.35911 11.5193 5.3594 11.5234 5.3606C11.5607 5.37169 11.5965 5.38642 11.6308 5.40356C11.636 5.40616 11.6413 5.40864 11.6464 5.41138C11.658 5.41757 11.6691 5.42471 11.6803 5.43156C11.687 5.43567 11.6938 5.43956 11.7004 5.44393C11.7137 5.45269 11.7263 5.46226 11.7389 5.47192C11.7613 5.48918 11.7834 5.50741 11.804 5.52791C11.8564 5.58023 11.8976 5.63957 11.929 5.70239C11.9449 5.73417 11.9592 5.76693 11.97 5.80135L11.9875 5.87166C11.9892 5.88025 11.9895 5.88907 11.9908 5.89771C11.996 5.93109 11.9999 5.96509 11.9999 5.99992V8.66659C11.9999 9.03478 11.7014 9.33325 11.3333 9.33325C10.9651 9.33325 10.6666 9.03478 10.6666 8.66659V7.6132L8.30005 9.98885C8.17529 10.114 8.00606 10.1845 7.82935 10.1848C7.65249 10.1851 7.48255 10.1151 7.35734 9.99015L6.36515 9.00122L4.95109 10.4159C4.69074 10.6762 4.26872 10.6763 4.00838 10.4159C3.74821 10.1556 3.74812 9.73352 4.00838 9.47323L5.8938 7.58781L5.94458 7.54224C6.20618 7.32876 6.59231 7.34361 6.83651 7.58716L7.82674 8.57479L9.72778 6.66659H8.66659C8.2984 6.66659 7.99992 6.36811 7.99992 5.99992C7.99992 5.63173 8.2984 5.33325 8.66659 5.33325H11.3333Z" fill="currentColor"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M12.9999 1.33325C13.9204 1.33325 14.6666 2.07945 14.6666 2.99992V12.9999C14.6666 13.9204 13.9204 14.6666 12.9999 14.6666H2.99992C2.07945 14.6666 1.33325 13.9204 1.33325 12.9999V2.99992C1.33325 2.07945 2.07945 1.33325 2.99992 1.33325H12.9999ZM2.99992 2.66659C2.81583 2.66659 2.66659 2.81583 2.66659 2.99992V12.9999C2.66659 13.184 2.81582 13.3333 2.99992 13.3333H12.9999C13.184 13.3333 13.3333 13.184 13.3333 12.9999V2.99992C13.3333 2.81582 13.184 2.66659 12.9999 2.66659H2.99992Z" fill="currentColor"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.9 KiB |
3
src/assets/svg/svg-putAccountInvestmentGuidelines.svg
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="none">
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.00011 0.666748C8.36828 0.666775 8.66678 0.965242 8.66678 1.33341V2.00008H12.6668L12.7325 2.00334C12.8852 2.01847 13.0287 2.08598 13.1381 2.19539L14.4715 3.52873C14.7318 3.78907 14.7318 4.21109 14.4715 4.47144L13.1381 5.80477C13.0131 5.92978 12.8436 6.00007 12.6668 6.00008H8.66678V7.00008H12.6668C13.0349 7.00011 13.3334 7.29858 13.3334 7.66675V10.3334C13.3334 10.7016 13.0349 11 12.6668 11.0001H8.66678V14.0001H9.66678C10.0349 14.0001 10.3334 14.2986 10.3334 14.6667C10.3334 15.0349 10.0349 15.3334 9.66678 15.3334H6.33345C5.96526 15.3334 5.66678 15.0349 5.66678 14.6667C5.66678 14.2986 5.96526 14.0001 6.33345 14.0001H7.33345V11.0001H3.33345C3.15665 11.0001 2.98711 10.9298 2.86209 10.8048L1.52876 9.47143C1.26841 9.21109 1.26841 8.78908 1.52876 8.52873L2.86209 7.19539L2.91092 7.15112C3.02955 7.05392 3.17873 7.00008 3.33345 7.00008H7.33345V6.00008H3.33345C2.96526 6.00008 2.66678 5.7016 2.66678 5.33341V2.66675C2.66678 2.29856 2.96526 2.00008 3.33345 2.00008H7.33345V1.33341C7.33345 0.965225 7.63192 0.666748 8.00011 0.666748ZM2.94282 9.00008L3.60949 9.66675H12.0001V8.33341H3.60949L2.94282 9.00008ZM4.00011 4.66675H12.3907L13.0574 4.00008L12.3907 3.33341H4.00011V4.66675Z" fill="currentColor"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.3 KiB |
4
src/assets/svg/svg-putAccountManage.svg
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="none">
|
||||||
|
<path d="M11.1667 8.00008C11.4428 8.00008 11.6667 8.22394 11.6667 8.50008C11.6667 8.77622 11.4428 9.00008 11.1667 9.00008C10.8905 9.00008 10.6667 8.77622 10.6667 8.50008C10.6667 8.22394 10.8905 8.00008 11.1667 8.00008Z" fill="currentColor"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M9 1.66675C9.36913 1.66675 9.94634 1.78371 10.4447 2.19409C10.846 2.52467 11.1507 3.00935 11.2734 3.66675H13.6667C14.403 3.66675 15 4.2637 15 5.00008V12.3334C15 13.0698 14.403 13.6667 13.6667 13.6667H2.33333C1.59695 13.6667 1 13.0698 1 12.3334V5.00008C1 4.37823 1.42607 3.85717 2.00195 3.70972C2.01269 3.36141 2.12207 2.88367 2.43424 2.46948C2.78795 2.00022 3.36198 1.66675 4.16667 1.66675H9ZM2.33333 12.3334H13.6667V11.0001H8.33333C7.96514 11.0001 7.66667 10.7016 7.66667 10.3334V6.66675C7.66667 6.29856 7.96514 6.00008 8.33333 6.00008H13.6667V5.00008H2.33333V12.3334ZM9 9.66675H13.6667V7.33342H9V9.66675ZM4.16667 3.00008C3.77137 3.00008 3.59565 3.14446 3.49935 3.27222C3.40692 3.39484 3.35929 3.54618 3.3418 3.66675H9.89648C9.81547 3.4438 9.70073 3.30816 9.59701 3.22274C9.38703 3.0499 9.13084 3.00008 9 3.00008H4.16667Z" fill="currentColor"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.2 KiB |
3
src/assets/svg/svg-rawMaterialStorage.svg
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="none">
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M13.3334 1.33334C14.0698 1.33334 14.6667 1.9303 14.6667 2.66668V13.3333C14.6667 14.0697 14.0698 14.6667 13.3334 14.6667H2.66671C1.93033 14.6667 1.33337 14.0697 1.33337 13.3333V2.66668C1.33337 1.9303 1.93033 1.33334 2.66671 1.33334H13.3334ZM2.66671 12V13.3333H4.00004V12H2.66671ZM5.33337 13.3333H10.6667V8.66668H5.33337V13.3333ZM12 13.3333H13.3334V12H12V13.3333ZM2.66671 10.6667H4.00004V8.66668H2.66671V10.6667ZM12 10.6667H13.3334V8.66668H12V10.6667ZM2.66671 7.33334H4.00004V5.33334H2.66671V7.33334ZM5.33337 7.33334H10.6667V2.66668H5.33337V7.33334ZM12 7.33334H13.3334V5.33334H12V7.33334ZM2.66671 4.00001H4.00004V2.66668H2.66671V4.00001ZM12 4.00001H13.3334V2.66668H12V4.00001Z" fill="currentColor"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 851 B |
6
src/assets/svg/svg-taskManage-active.svg
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 18 18" fill="none">
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M15.375 2.25C16.2034 2.25 16.875 2.92158 16.875 3.75V15.375C16.875 16.2034 16.2034 16.875 15.375 16.875H2.625C1.79658 16.875 1.125 16.2034 1.125 15.375V3.75C1.125 2.92157 1.79657 2.25 2.625 2.25H15.375ZM2.625 6.375H15.375V3.75H2.625V6.375Z" fill="#6D4CFE"/>
|
||||||
|
<path d="M12.2197 8.84467C12.5126 8.55178 12.9873 8.55178 13.2802 8.84467C13.5731 9.13756 13.5731 9.61232 13.2802 9.90522L8.78022 14.4052C8.48732 14.6981 8.01256 14.6981 7.71967 14.4052L5.46967 12.1552C5.17678 11.8623 5.17678 11.3876 5.46967 11.0947C5.76256 10.8018 6.23732 10.8018 6.53022 11.0947L8.24994 12.8144L12.2197 8.84467Z" fill="white"/>
|
||||||
|
<path d="M6 1.125C6.41421 1.125 6.75 1.46079 6.75 1.875V4.875C6.75 5.28921 6.41421 5.625 6 5.625C5.58579 5.625 5.25 5.28921 5.25 4.875V1.875C5.25 1.46079 5.58579 1.125 6 1.125Z" fill="#39C6E9"/>
|
||||||
|
<path d="M12 1.125C12.4142 1.125 12.75 1.46079 12.75 1.875V4.875C12.75 5.28921 12.4142 5.625 12 5.625C11.5858 5.625 11.25 5.28921 11.25 4.875V1.875C11.25 1.46079 11.5858 1.125 12 1.125Z" fill="#39C6E9"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.1 KiB |
4
src/assets/svg/svg-taskManage.svg
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 18 18" fill="none">
|
||||||
|
<path d="M12.2197 8.84473C12.5126 8.55186 12.9874 8.55185 13.2803 8.84473C13.5732 9.13761 13.5731 9.61238 13.2803 9.90527L8.78027 14.4053C8.48738 14.6982 8.01262 14.6982 7.71973 14.4053L5.46973 12.1553C5.17685 11.8624 5.17684 11.3876 5.46973 11.0947C5.76262 10.8019 6.23739 10.8019 6.53027 11.0947L8.25 12.8145L12.2197 8.84473Z" fill="currentColor"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M12 1.125C12.4142 1.125 12.75 1.46079 12.75 1.875V2.25H15.375C16.2034 2.25 16.875 2.92158 16.875 3.75V15.375C16.875 16.2034 16.2034 16.875 15.375 16.875H2.625C1.79658 16.875 1.125 16.2034 1.125 15.375V3.75C1.125 2.92157 1.79657 2.25 2.625 2.25H5.25V1.875C5.25 1.46079 5.58579 1.125 6 1.125C6.41421 1.125 6.75 1.46079 6.75 1.875V2.25H11.25V1.875C11.25 1.46079 11.5858 1.125 12 1.125ZM2.625 15.375H15.375V7.875H2.625V15.375ZM2.625 6.375H15.375V3.75H12.75V4.875C12.75 5.28921 12.4142 5.625 12 5.625C11.5858 5.625 11.25 5.28921 11.25 4.875V3.75H6.75V4.875C6.75 5.28921 6.41421 5.625 6 5.625C5.58579 5.625 5.25 5.28921 5.25 4.875V3.75H2.625V6.375Z" fill="currentColor"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.1 KiB |
@ -1,52 +0,0 @@
|
|||||||
<!--
|
|
||||||
* @Author: 田鑫
|
|
||||||
* @Date: 2023-03-05 18:14:16
|
|
||||||
* @LastEditors: 田鑫
|
|
||||||
* @LastEditTime: 2023-03-05 19:17:52
|
|
||||||
* @Description:
|
|
||||||
-->
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import type { RouteLocationNormalized } from 'vue-router';
|
|
||||||
|
|
||||||
const router = useRouter();
|
|
||||||
const route = useRoute();
|
|
||||||
|
|
||||||
const matched = computed(() => {
|
|
||||||
if (route.matched.length === 1 && route.matched[0].path === '/') {
|
|
||||||
return [];
|
|
||||||
} else {
|
|
||||||
return route.matched.reduce((t: RouteLocationNormalized[], o) => {
|
|
||||||
const isExist = t.find((c) => c.name === o.name);
|
|
||||||
return isExist ? t : [...t, router.resolve(o)];
|
|
||||||
}, []);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<view></view>
|
|
||||||
<!-- <a-breadcrumb class="container-breadcrumb">
|
|
||||||
<a-breadcrumb-item v-for="{ meta, name } in matched" :key="name">
|
|
||||||
<router-link v-slot="{ href, navigate }" :to="{ name }" custom>
|
|
||||||
<a-link v-if="meta.needNavigate" :href="href" @click="navigate">{{
|
|
||||||
meta.locale ? meta.locale : '主页'
|
|
||||||
}}</a-link>
|
|
||||||
<a-link v-else disabled>{{ meta.locale ? meta.locale : '主页' }}</a-link>
|
|
||||||
</router-link>
|
|
||||||
</a-breadcrumb-item>
|
|
||||||
</a-breadcrumb> -->
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped lang="scss">
|
|
||||||
.container-breadcrumb {
|
|
||||||
margin: 16px 0;
|
|
||||||
:deep(.arco-breadcrumb-item) {
|
|
||||||
> a {
|
|
||||||
color: rgb(var(--gray-6));
|
|
||||||
}
|
|
||||||
&:last-child {
|
|
||||||
color: rgb(var(--gray-8));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@ -1,5 +1,2 @@
|
|||||||
export { default as Navbar } from './navbar/index.vue';
|
|
||||||
export { default as Menu } from './menu/index.vue';
|
|
||||||
export { default as TabBar } from './tab-bar/index.vue';
|
export { default as TabBar } from './tab-bar/index.vue';
|
||||||
export { default as Breadcrumb } from './breadcrumb/index.vue';
|
|
||||||
export { default as ModalSimple } from './modal/index.vue';
|
export { default as ModalSimple } from './modal/index.vue';
|
||||||
|
|||||||
@ -1,204 +0,0 @@
|
|||||||
<script lang="tsx">
|
|
||||||
import type { RouteMeta, RouteRecordRaw } from 'vue-router';
|
|
||||||
|
|
||||||
import { useAppStore } from '@/stores';
|
|
||||||
import { useSidebarStore } from '@/stores/modules/side-bar';
|
|
||||||
import { listenerRouteChange } from '@/utils/route-listener';
|
|
||||||
import { openWindow, regexUrl } from '@/utils';
|
|
||||||
import useMenuTree from './use-menu-tree';
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
emit: ['collapse'],
|
|
||||||
setup() {
|
|
||||||
const appStore = useAppStore();
|
|
||||||
const router = useRouter();
|
|
||||||
const route = useRoute();
|
|
||||||
const { menuTree } = useMenuTree();
|
|
||||||
const collapsed = computed({
|
|
||||||
get() {
|
|
||||||
if (appStore.device === 'desktop') return appStore.menuCollapse;
|
|
||||||
return false;
|
|
||||||
},
|
|
||||||
set(value: boolean) {
|
|
||||||
appStore.updateSettings({ menuCollapse: value });
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const topMenu = computed(() => appStore.topMenu);
|
|
||||||
const openKeys = ref<string[]>([]);
|
|
||||||
const selectedKey = ref<string[]>([]);
|
|
||||||
const sidebarStore = useSidebarStore();
|
|
||||||
const onMenuItemClick = (item: RouteRecordRaw) => {
|
|
||||||
if (regexUrl.test(item.path)) {
|
|
||||||
openWindow(item.path);
|
|
||||||
selectedKey.value = [item.name as string];
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Eliminate external link side effects
|
|
||||||
const { hideInMenu, activeMenu } = item.meta as RouteMeta;
|
|
||||||
if (route.name === item.name && !hideInMenu && !activeMenu) {
|
|
||||||
selectedKey.value = [item.name as string];
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Trigger router change
|
|
||||||
router.push({
|
|
||||||
name: item.name,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
const findMenuOpenKeys = (target: string) => {
|
|
||||||
const result: string[] = [];
|
|
||||||
let isFind = false;
|
|
||||||
const backtrack = (item: RouteRecordRaw, keys: string[]) => {
|
|
||||||
if (item.name === target) {
|
|
||||||
isFind = true;
|
|
||||||
result.push(...keys);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (item.children?.length) {
|
|
||||||
item.children.forEach((el) => {
|
|
||||||
backtrack(el, [...keys, el.name as string]);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
menuTree.value.forEach((el: RouteRecordRaw) => {
|
|
||||||
if (isFind) return; // Performance optimization
|
|
||||||
backtrack(el, [el.name as string]);
|
|
||||||
});
|
|
||||||
return result;
|
|
||||||
};
|
|
||||||
listenerRouteChange((newRoute) => {
|
|
||||||
const { requiresAuth, activeMenu, hideInMenu } = newRoute.meta;
|
|
||||||
// if (requiresAuth && (!hideInMenu || activeMenu)) {
|
|
||||||
if (!hideInMenu || activeMenu) {
|
|
||||||
const menuOpenKeys = findMenuOpenKeys((activeMenu || newRoute.name) as string);
|
|
||||||
const keySet = new Set([...menuOpenKeys, ...openKeys.value]);
|
|
||||||
openKeys.value = [...keySet];
|
|
||||||
selectedKey.value = [activeMenu || menuOpenKeys[menuOpenKeys.length - 1]];
|
|
||||||
|
|
||||||
// 自动设置 activeMenuId
|
|
||||||
sidebarStore.setActiveMenuIdByRoute(newRoute);
|
|
||||||
}
|
|
||||||
}, true);
|
|
||||||
const setCollapse = (val: boolean) => {
|
|
||||||
if (appStore.device === 'desktop') appStore.updateSettings({ menuCollapse: val });
|
|
||||||
};
|
|
||||||
const renderSubMenu = () => {
|
|
||||||
function travel(_route: RouteRecordRaw[] = [], nodes: any[] = []) {
|
|
||||||
if (!Array.isArray(_route)) return nodes;
|
|
||||||
_route.forEach((element) => {
|
|
||||||
// 跳过没有 name 的菜单项,防止 key 报错
|
|
||||||
if (!element?.name) return;
|
|
||||||
|
|
||||||
const icon = element?.meta?.icon
|
|
||||||
? (() => {
|
|
||||||
if (typeof element.meta.icon === 'string') {
|
|
||||||
return (
|
|
||||||
<svg class="w-16px h-16px">
|
|
||||||
<use xlinkHref={element.meta.icon} />
|
|
||||||
</svg>
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return h(element.meta.icon as object);
|
|
||||||
}
|
|
||||||
})()
|
|
||||||
: null;
|
|
||||||
|
|
||||||
if (element.children && element.children.length > 0) {
|
|
||||||
nodes.push(
|
|
||||||
<a-sub-menu
|
|
||||||
key={String(element.name)}
|
|
||||||
v-slots={{
|
|
||||||
icon,
|
|
||||||
title: () => element?.meta?.locale || '',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{travel(element.children)}
|
|
||||||
</a-sub-menu>,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
nodes.push(
|
|
||||||
<a-menu-item key={String(element.name)} v-slots={{ icon }} onClick={() => onMenuItemClick(element)}>
|
|
||||||
{element?.meta?.locale || ''}
|
|
||||||
</a-menu-item>,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return nodes;
|
|
||||||
}
|
|
||||||
return travel(menuTree.value ?? []);
|
|
||||||
};
|
|
||||||
return () => (
|
|
||||||
<a-menu
|
|
||||||
mode={topMenu.value ? 'horizontal' : 'vertical'}
|
|
||||||
v-model:collapsed={collapsed.value}
|
|
||||||
v-model:open-keys={openKeys.value}
|
|
||||||
show-collapse-button={appStore.device !== 'mobile'}
|
|
||||||
auto-open={false}
|
|
||||||
selected-keys={selectedKey.value}
|
|
||||||
auto-open-selected={true}
|
|
||||||
level-indent={24}
|
|
||||||
style="height: 100%;width:100%;"
|
|
||||||
onCollapse={setCollapse}
|
|
||||||
>
|
|
||||||
{renderSubMenu()}
|
|
||||||
</a-menu>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
:deep(.arco-menu-inner) {
|
|
||||||
padding: 20px 24px 0 12px !important;
|
|
||||||
.arco-menu-inline-header {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
height: 40px;
|
|
||||||
margin-bottom: 12px;
|
|
||||||
border-radius: 8px;
|
|
||||||
.arco-menu-icon {
|
|
||||||
margin-right: 8px;
|
|
||||||
}
|
|
||||||
.arco-menu-title {
|
|
||||||
color: var(--Text-2, #3c4043);
|
|
||||||
font-family: $font-family-medium;
|
|
||||||
font-size: 16px;
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 400;
|
|
||||||
line-height: 22px; /* 137.5% */
|
|
||||||
}
|
|
||||||
&:hover {
|
|
||||||
background: var(--BG-200, #f2f3f5) !important;
|
|
||||||
}
|
|
||||||
&.arco-menu-selected {
|
|
||||||
.arco-menu-title {
|
|
||||||
color: var(--Brand-Brand-6, #6d4cfe) !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.arco-icon {
|
|
||||||
&:not(.arco-icon-down) {
|
|
||||||
font-size: 18px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.arco-menu-item {
|
|
||||||
border-radius: 8px;
|
|
||||||
.arco-menu-item-inner {
|
|
||||||
color: var(--Text-3, #737478);
|
|
||||||
font-family: $font-family-regular;
|
|
||||||
font-size: 16px;
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 400;
|
|
||||||
line-height: 22px; /* 137.5% */
|
|
||||||
}
|
|
||||||
&:hover {
|
|
||||||
background: var(--BG-200, #f2f3f5) !important;
|
|
||||||
}
|
|
||||||
&.arco-menu-selected {
|
|
||||||
background: var(--Brand-Brand-1, #f0edff) !important;
|
|
||||||
.arco-menu-item-inner {
|
|
||||||
color: var(--Brand-Brand-6, #6d4cfe) !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@ -1,63 +0,0 @@
|
|||||||
/*
|
|
||||||
* @Author: RenXiaoDong
|
|
||||||
* @Date: 2025-06-19 01:45:53
|
|
||||||
*/
|
|
||||||
import type { RouteRecordRaw, RouteRecordNormalized } from 'vue-router';
|
|
||||||
import { useRouter } from 'vue-router';
|
|
||||||
import { useSidebarStore } from '@/stores/modules/side-bar';
|
|
||||||
|
|
||||||
export default function useMenuTree() {
|
|
||||||
const router = useRouter();
|
|
||||||
const appRoutes = router.options?.routes ?? [];
|
|
||||||
|
|
||||||
const sidebarStore = useSidebarStore();
|
|
||||||
const appRoute = computed(() => {
|
|
||||||
const _filterRoutes = appRoutes.filter((v) => v.meta?.id === sidebarStore.activeMenuId);
|
|
||||||
return _filterRoutes;
|
|
||||||
});
|
|
||||||
const menuTree = computed(() => {
|
|
||||||
const copyRouter = cloneDeep(appRoute.value) as RouteRecordNormalized[];
|
|
||||||
copyRouter.sort((a: RouteRecordNormalized, b: RouteRecordNormalized) => {
|
|
||||||
return (a.meta.order || 0) - (b.meta.order || 0);
|
|
||||||
});
|
|
||||||
function travel(_routes: RouteRecordRaw[], layer: number) {
|
|
||||||
if (!_routes) return null;
|
|
||||||
|
|
||||||
const collector: any = _routes.map((element) => {
|
|
||||||
// leaf node
|
|
||||||
if (element.meta?.hideChildrenInMenu || !element.children) {
|
|
||||||
element.children = [];
|
|
||||||
return element;
|
|
||||||
}
|
|
||||||
|
|
||||||
// route filter hideInMenu true
|
|
||||||
element.children = element.children.filter((x) => x.meta?.hideInMenu !== true);
|
|
||||||
|
|
||||||
// Associated child node
|
|
||||||
const subItem = travel(element.children, layer + 1);
|
|
||||||
|
|
||||||
if (subItem.length) {
|
|
||||||
element.children = subItem;
|
|
||||||
return element;
|
|
||||||
}
|
|
||||||
// the else logic
|
|
||||||
if (layer > 1) {
|
|
||||||
element.children = subItem;
|
|
||||||
return element;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (element.meta?.hideInMenu === false) {
|
|
||||||
return element;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
});
|
|
||||||
return collector.filter(Boolean);
|
|
||||||
}
|
|
||||||
return travel(copyRouter, 0);
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
menuTree,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@ -1,95 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="navbar-menu h-100%">
|
|
||||||
<a-menu mode="horizontal" :selected-keys="selectedKey">
|
|
||||||
<a-menu-item v-for="item in menuList" :key="String(item.id)">
|
|
||||||
<template v-if="item.children">
|
|
||||||
<a-dropdown :popup-max-height="false" class="layout-menu-item-dropdown">
|
|
||||||
<a-button type="text">
|
|
||||||
<span class="menu-item-text mr-2px"> {{ item.name }}</span>
|
|
||||||
<icon-caret-down size="16" class="arco-icon-down !mr-0" />
|
|
||||||
</a-button>
|
|
||||||
<template #content>
|
|
||||||
<a-doption
|
|
||||||
v-for="(child, ind) in item.children"
|
|
||||||
:key="ind"
|
|
||||||
@click="handleDropdownClick(child)"
|
|
||||||
:class="{ active: child.includeRouteNames.includes(route.name) }"
|
|
||||||
>
|
|
||||||
<span class="menu-item-text"> {{ child.name }}</span>
|
|
||||||
</a-doption>
|
|
||||||
</template>
|
|
||||||
</a-dropdown>
|
|
||||||
</template>
|
|
||||||
<template v-else>
|
|
||||||
<a-menu-item :key="String(item.id)" @click="handleDropdownClick(item)">
|
|
||||||
<span class="menu-item-text"> {{ item.name }}</span>
|
|
||||||
</a-menu-item>
|
|
||||||
</template>
|
|
||||||
</a-menu-item>
|
|
||||||
</a-menu>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { useRoute } from 'vue-router';
|
|
||||||
import { useSidebarStore } from '@/stores/modules/side-bar';
|
|
||||||
// import router from '@/router';
|
|
||||||
import { useRouter } from 'vue-router';
|
|
||||||
|
|
||||||
const sidebarStore = useSidebarStore();
|
|
||||||
const router = useRouter();
|
|
||||||
const route = useRoute();
|
|
||||||
|
|
||||||
const selectedKey = computed(() => {
|
|
||||||
return [String(sidebarStore.activeMenuId)];
|
|
||||||
});
|
|
||||||
const menuList = computed(() => {
|
|
||||||
return sidebarStore.menuList;
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleDropdownClick = (item) => {
|
|
||||||
router.push({ name: item.routeName });
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped lang="scss">
|
|
||||||
@import './style.scss';
|
|
||||||
</style>
|
|
||||||
<style lang="scss">
|
|
||||||
.layout-menu-item-dropdown {
|
|
||||||
.arco-dropdown {
|
|
||||||
border-radius: 8px;
|
|
||||||
border: 1px solid var(--BG-300, #e6e6e8);
|
|
||||||
background: var(--BG-white, #fff);
|
|
||||||
padding: 12px 0px;
|
|
||||||
.arco-dropdown-option {
|
|
||||||
padding: 0 12px;
|
|
||||||
margin-bottom: 4px;
|
|
||||||
&-content {
|
|
||||||
display: flex;
|
|
||||||
height: 40px;
|
|
||||||
width: 100%;
|
|
||||||
padding: 10px 0;
|
|
||||||
align-items: center;
|
|
||||||
.menu-item-text {
|
|
||||||
color: var(--Text-2, #3c4043);
|
|
||||||
font-family: $font-family-regular;
|
|
||||||
font-size: 16px;
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 400;
|
|
||||||
line-height: 22px; /* 137.5% */
|
|
||||||
}
|
|
||||||
}
|
|
||||||
&:not(.arco-dropdown-option-disabled):hover {
|
|
||||||
background: var(--BG-200, #f2f3f5);
|
|
||||||
}
|
|
||||||
&.active {
|
|
||||||
background: var(--Brand-Brand-1, #f0edff) !important;
|
|
||||||
.menu-item-text {
|
|
||||||
color: var(--Brand-Brand-6, #6d4cfe) !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@ -1,45 +0,0 @@
|
|||||||
.navbar-menu {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
margin-left: 40px;
|
|
||||||
.menu-item-text {
|
|
||||||
color: var(--Text-2, #3c4043);
|
|
||||||
font-family: $font-family-medium;
|
|
||||||
font-size: 16px;
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 400;
|
|
||||||
line-height: 22px;
|
|
||||||
}
|
|
||||||
:deep(.arco-menu) {
|
|
||||||
height: 100%;
|
|
||||||
.arco-menu-inner {
|
|
||||||
padding: 0 20px;
|
|
||||||
}
|
|
||||||
.arco-menu-item {
|
|
||||||
padding: 0;
|
|
||||||
position: relative;
|
|
||||||
&.arco-menu-selected {
|
|
||||||
.menu-item-text,
|
|
||||||
.arco-menu-selected-label {
|
|
||||||
color: #6d4cfe;
|
|
||||||
}
|
|
||||||
.arco-menu-selected-label {
|
|
||||||
background: var(--Brand-Brand-6, #6d4cfe);
|
|
||||||
height: 4px;
|
|
||||||
border-radius: 4px;
|
|
||||||
width: 50%;
|
|
||||||
position: absolute;
|
|
||||||
bottom: -8px;
|
|
||||||
left: 50%;
|
|
||||||
transform: translateX(-50%);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.arco-icon-down {
|
|
||||||
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
||||||
}
|
|
||||||
.arco-dropdown-open .arco-icon-down {
|
|
||||||
transform: rotate(180deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,58 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="navbar-wrap">
|
|
||||||
<div class="left-wrap">
|
|
||||||
<div class="h-full flex items-center cursor-pointer" @click="handleUserHome">
|
|
||||||
<img src="@/assets/LOGO.svg" alt="" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex-1">
|
|
||||||
<NavbarMenu v-if="!isAgentRoute"/>
|
|
||||||
</div>
|
|
||||||
<RightSide :isAgentRoute="isAgentRoute" v-if="userStore.isLogin"/>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import NavbarMenu from './components/navbar-menu';
|
|
||||||
import RightSide from './components/right-side';
|
|
||||||
|
|
||||||
import { useUserStore } from '@/stores';
|
|
||||||
import { handleUserHome } from '@/utils/user.ts';
|
|
||||||
import router from '@/router';
|
|
||||||
|
|
||||||
const route = useRoute();
|
|
||||||
const userStore = useUserStore();
|
|
||||||
|
|
||||||
const isAgentRoute = computed(() => {
|
|
||||||
return route.meta?.isAgentRoute;
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
<style scoped lang="scss">
|
|
||||||
.navbar-wrap {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
height: 100%;
|
|
||||||
background-color: var(--color-bg-2);
|
|
||||||
border-bottom: 1px solid var(--color-border);
|
|
||||||
.left-wrap {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
padding-left: 24px;
|
|
||||||
}
|
|
||||||
.arco-dropdown-option-suffix {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
.enterprises-doption {
|
|
||||||
.arco-dropdown-option-content {
|
|
||||||
padding: 0 !important;
|
|
||||||
border-radius: 8px;
|
|
||||||
}
|
|
||||||
&:not(.arco-dropdown-option-disabled):hover {
|
|
||||||
background-color: transparent;
|
|
||||||
.arco-dropdown-option-content {
|
|
||||||
background: var(--BG-200, #f2f3f5);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@ -9,6 +9,7 @@
|
|||||||
size="medium"
|
size="medium"
|
||||||
:placeholder="placeholder"
|
:placeholder="placeholder"
|
||||||
:allow-clear="allClear"
|
:allow-clear="allClear"
|
||||||
|
:allow-search="allowSearch"
|
||||||
:max-tag-count="maxTagCount"
|
:max-tag-count="maxTagCount"
|
||||||
@change="handleChange"
|
@change="handleChange"
|
||||||
>
|
>
|
||||||
@ -45,6 +46,10 @@ const props = defineProps({
|
|||||||
allClear: {
|
allClear: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true,
|
default: true,
|
||||||
|
},
|
||||||
|
allowSearch: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -17,7 +17,7 @@ const props = defineProps<{
|
|||||||
</script>
|
</script>
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.container {
|
.container {
|
||||||
border: 1px solid var(--BG-300, rgba(230, 230, 232, 1));
|
// border: 1px solid var(--BG-300, rgba(230, 230, 232, 1));
|
||||||
background: var(--BG-white, rgba(255, 255, 255, 1));
|
background: var(--BG-white, rgba(255, 255, 255, 1));
|
||||||
padding: 16px 24px 20px 24px;
|
padding: 16px 24px 20px 24px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
|
|||||||
205
src/components/expandable-tags/index.vue
Normal file
@ -0,0 +1,205 @@
|
|||||||
|
<script lang="tsx">
|
||||||
|
import { ref, defineComponent, onMounted, nextTick, watch, h } from 'vue';
|
||||||
|
|
||||||
|
export interface ExpandableTagsProps {
|
||||||
|
// 标签数据数组
|
||||||
|
tags: string[];
|
||||||
|
// 最大显示行数
|
||||||
|
maxLine: number;
|
||||||
|
// 是否可点击标签
|
||||||
|
clickable: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ExpandableTagsEmits {
|
||||||
|
(e: 'tagClick', tag: string, index: number): void;
|
||||||
|
(e: 'expandChange', isExpand: boolean): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'ExpandableTags',
|
||||||
|
props: {
|
||||||
|
tags: {
|
||||||
|
type: Array as () => string[],
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
maxLine: {
|
||||||
|
type: Number,
|
||||||
|
default: 2,
|
||||||
|
},
|
||||||
|
clickable: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
emits: ['tagClick', 'expandChange'],
|
||||||
|
setup(props: ExpandableTagsProps, { emit, expose }) {
|
||||||
|
const isExpand = ref(false);
|
||||||
|
const showExpandBtn = ref(false);
|
||||||
|
const displayedTags = ref(props.tags);
|
||||||
|
const hideLength = ref(0);
|
||||||
|
|
||||||
|
// 初始化计算标签显示状态
|
||||||
|
const init = async () => {
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
const listCon = document.querySelector('.expandable-tags-container .tag-list') as HTMLElement;
|
||||||
|
if (!listCon) return;
|
||||||
|
|
||||||
|
displayedTags.value = props.tags;
|
||||||
|
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
const labels = listCon.querySelectorAll('.tag-item:not(.expand-btn)');
|
||||||
|
if (labels.length === 0) return;
|
||||||
|
|
||||||
|
// 计算单行高度
|
||||||
|
const lineHeight = labels[0].getBoundingClientRect().height;
|
||||||
|
|
||||||
|
// 设置容器最大高度
|
||||||
|
const containerHeight = lineHeight * props.maxLine;
|
||||||
|
listCon.style.maxHeight = `${containerHeight}px`;
|
||||||
|
listCon.style.overflow = 'hidden';
|
||||||
|
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
// 检查是否有标签超出容器高度
|
||||||
|
let labelIndex = 0;
|
||||||
|
const listConBottom = listCon.getBoundingClientRect().bottom;
|
||||||
|
|
||||||
|
for (let i = 0; i < labels.length; i++) {
|
||||||
|
const _top = labels[i].getBoundingClientRect().top;
|
||||||
|
if (_top >= listConBottom) {
|
||||||
|
// 标签顶部超过容器底部,说明超出
|
||||||
|
showExpandBtn.value = true;
|
||||||
|
labelIndex = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重置容器样式
|
||||||
|
listCon.style.maxHeight = '';
|
||||||
|
listCon.style.overflow = '';
|
||||||
|
|
||||||
|
if (!showExpandBtn.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重新计算可显示的标签数量(考虑展开按钮占用的空间)
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
const listConRect = listCon.getBoundingClientRect();
|
||||||
|
const expandBtn = listCon.querySelector('.expand-btn');
|
||||||
|
const expandBtnWidth = expandBtn?.getBoundingClientRect()?.width || 0;
|
||||||
|
|
||||||
|
// 获取实际的column-gap值
|
||||||
|
const computedStyle = window.getComputedStyle(listCon);
|
||||||
|
const columnGap = parseInt(computedStyle.getPropertyValue('column-gap')) || 8;
|
||||||
|
|
||||||
|
// 从超出的位置向前查找,找到能容纳展开按钮的位置
|
||||||
|
for (let i = labelIndex - 1; i >= 0; i--) {
|
||||||
|
const labelRight = labels[i].getBoundingClientRect().right - listConRect.left;
|
||||||
|
// 使用columnGap代替marginRight
|
||||||
|
if (labelRight + columnGap + expandBtnWidth <= listConRect.width) {
|
||||||
|
hideLength.value = i + 1;
|
||||||
|
displayedTags.value = props.tags.slice(0, hideLength.value);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 切换展开/折叠状态
|
||||||
|
const toggleExpand = () => {
|
||||||
|
isExpand.value = !isExpand.value;
|
||||||
|
|
||||||
|
if (isExpand.value) {
|
||||||
|
displayedTags.value = props.tags;
|
||||||
|
} else {
|
||||||
|
displayedTags.value = props.tags.slice(0, hideLength.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
emit('expandChange', isExpand.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTagClick = (tag: string, index: number) => {
|
||||||
|
if (props.clickable) {
|
||||||
|
emit('tagClick', tag, index);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleResize = () => {
|
||||||
|
if (!isExpand.value) {
|
||||||
|
showExpandBtn.value = false;
|
||||||
|
nextTick(() => {
|
||||||
|
init();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.tags,
|
||||||
|
() => {
|
||||||
|
isExpand.value = false;
|
||||||
|
showExpandBtn.value = false;
|
||||||
|
nextTick(() => {
|
||||||
|
init();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
{ deep: true },
|
||||||
|
);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.maxLine,
|
||||||
|
() => {
|
||||||
|
isExpand.value = false;
|
||||||
|
showExpandBtn.value = false;
|
||||||
|
nextTick(() => {
|
||||||
|
init();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
nextTick(() => {
|
||||||
|
init();
|
||||||
|
});
|
||||||
|
|
||||||
|
window.addEventListener('resize', handleResize);
|
||||||
|
});
|
||||||
|
onUnmounted(() => {
|
||||||
|
window.removeEventListener('resize', handleResize);
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => (
|
||||||
|
<div class="expandable-tags-container">
|
||||||
|
<div class={`tag-list ${isExpand.value ? 'expand' : ''}`}>
|
||||||
|
{displayedTags.value.map((tag, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
class={`flex items-center h-24px tag-item ${props.clickable ? 'clickable' : ''}`}
|
||||||
|
onClick={() => handleTagClick(tag, index)}
|
||||||
|
>
|
||||||
|
<span class="cts color-#6D4CFE tag-text">{tag}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{showExpandBtn.value && (
|
||||||
|
<div
|
||||||
|
class="expand-btn flex items-center h-24px cursor-pointer"
|
||||||
|
onClick={(e: Event) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
toggleExpand();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span class="cts mr-2px color-#6D4CFE expand-text">{isExpand.value ? '收起' : '更多'}</span>
|
||||||
|
<icon-down size={12} class="color-#6D4CFE icon" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
@import './style.scss';
|
||||||
|
</style>
|
||||||
40
src/components/expandable-tags/style.scss
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
.expandable-tags-container {
|
||||||
|
width: 100%;
|
||||||
|
// overflow: hidden;
|
||||||
|
.cts {
|
||||||
|
font-family: $font-family-regular;
|
||||||
|
font-size: 12px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
.tag-list {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
transition: all 0.3s;
|
||||||
|
.tag-item {
|
||||||
|
padding: 0 8px;
|
||||||
|
background-color: #f6f4ff;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: all 0.3s;
|
||||||
|
&.clickable {
|
||||||
|
cursor: pointer;
|
||||||
|
&:hover {
|
||||||
|
box-shadow: 0 4px 8px 0 rgba(109, 76, 254, 0.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.expand-btn {
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
&.expand {
|
||||||
|
max-height: none !important;
|
||||||
|
.expand-btn {
|
||||||
|
.icon {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,9 +1,21 @@
|
|||||||
<template>
|
<template>
|
||||||
<a-tooltip :disabled="isShowBtn || (!isShowBtn && disabled)" :placement="props.placement">
|
<Tooltip
|
||||||
<template #content>
|
:open="open"
|
||||||
|
:placement="props.placement"
|
||||||
|
>
|
||||||
|
<template #title>
|
||||||
<div :style="contentStyle" class="tip-content">{{ props.context }}</div>
|
<div :style="contentStyle" class="tip-content">{{ props.context }}</div>
|
||||||
</template>
|
</template>
|
||||||
<div v-bind="$attrs" ref="Text" :class="`${isShow ? '' : `line-${props.line}`} `" class="overflow-text">
|
<div
|
||||||
|
v-bind="$attrs"
|
||||||
|
ref="Text"
|
||||||
|
:class="`${isShow ? '' : `line-${props.line}`} `"
|
||||||
|
class="overflow-text"
|
||||||
|
@mouseenter="handleMouseEnter"
|
||||||
|
@mouseleave="
|
||||||
|
handleMouseLeave
|
||||||
|
"
|
||||||
|
>
|
||||||
{{ props.context }}
|
{{ props.context }}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
@ -18,11 +30,23 @@
|
|||||||
{{ isShow ? '收起' : '展开' }}
|
{{ isShow ? '收起' : '展开' }}
|
||||||
<icon-up size="16" :class="{ active: isShow }" class="ml-2px color-#8C8C8C" />
|
<icon-up size="16" :class="{ active: isShow }" class="ml-2px color-#8C8C8C" />
|
||||||
</div>
|
</div>
|
||||||
</a-tooltip>
|
</Tooltip>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, reactive, toRefs, onBeforeMount, onMounted, watchEffect, computed, watch, nextTick, defineProps } from 'vue';
|
import { Tooltip } from 'ant-design-vue';
|
||||||
|
import {
|
||||||
|
ref,
|
||||||
|
reactive,
|
||||||
|
toRefs,
|
||||||
|
onBeforeMount,
|
||||||
|
onMounted,
|
||||||
|
watchEffect,
|
||||||
|
computed,
|
||||||
|
watch,
|
||||||
|
nextTick,
|
||||||
|
defineProps,
|
||||||
|
} from 'vue';
|
||||||
import elementResizeDetectorMaker from 'element-resize-detector';
|
import elementResizeDetectorMaker from 'element-resize-detector';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
@ -32,12 +56,17 @@ const props = defineProps({
|
|||||||
},
|
},
|
||||||
placement: {
|
placement: {
|
||||||
type: String,
|
type: String,
|
||||||
default: 'bottom',
|
default: 'top',
|
||||||
},
|
},
|
||||||
line: {
|
line: {
|
||||||
type: Number,
|
type: Number,
|
||||||
default: 1,
|
default: 1,
|
||||||
},
|
},
|
||||||
|
// 显示延迟时间,毫秒级
|
||||||
|
mouseEnterDelay: {
|
||||||
|
type: Number,
|
||||||
|
default: 100,
|
||||||
|
},
|
||||||
maxHeight: {
|
maxHeight: {
|
||||||
type: [String, Number],
|
type: [String, Number],
|
||||||
default: '',
|
default: '',
|
||||||
@ -51,10 +80,17 @@ const props = defineProps({
|
|||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const data = reactive({});
|
const data = reactive({});
|
||||||
const isShow = ref(false);
|
const isShow = ref(false);
|
||||||
|
const open = ref(false);
|
||||||
|
const disabled = ref(false);
|
||||||
|
const Text = ref(null);
|
||||||
|
const textWidth = ref();
|
||||||
|
const timer = ref(null);
|
||||||
|
|
||||||
const contentStyle = computed(() => {
|
const contentStyle = computed(() => {
|
||||||
let style = {
|
const style = {
|
||||||
'max-height': props.maxHeight + 'px',
|
'max-height': props.maxHeight + 'px',
|
||||||
'max-width': props.maxWidth + 'px',
|
'max-width': props.maxWidth + 'px',
|
||||||
overflow: 'auto',
|
overflow: 'auto',
|
||||||
@ -62,9 +98,10 @@ const contentStyle = computed(() => {
|
|||||||
};
|
};
|
||||||
return props.maxHeight || props.maxWidth ? style : {};
|
return props.maxHeight || props.maxWidth ? style : {};
|
||||||
});
|
});
|
||||||
const disabled = ref(true);
|
const notShowTooltip = computed(() => {
|
||||||
const Text = ref(null);
|
return props.isShowBtn || (!props.isShowBtn && disabled.value);
|
||||||
const textWidth = ref();
|
});
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
[() => props.context, textWidth],
|
[() => props.context, textWidth],
|
||||||
async () => {
|
async () => {
|
||||||
@ -92,7 +129,24 @@ watch(
|
|||||||
immediate: true,
|
immediate: true,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
onBeforeMount(() => {});
|
|
||||||
|
const clearTimer = () => {
|
||||||
|
if (timer.value) {
|
||||||
|
clearTimeout(timer.value);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const handleMouseEnter = () => {
|
||||||
|
clearTimer();
|
||||||
|
timer.value = setTimeout(() => {
|
||||||
|
open.value = !notShowTooltip.value;
|
||||||
|
}, props.mouseEnterDelay);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseLeave = () => {
|
||||||
|
clearTimer();
|
||||||
|
open.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await nextTick();
|
await nextTick();
|
||||||
const erd = elementResizeDetectorMaker();
|
const erd = elementResizeDetectorMaker();
|
||||||
@ -103,7 +157,10 @@ onMounted(async () => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
watchEffect(() => {});
|
onUnmounted(() => {
|
||||||
|
clearTimer();
|
||||||
|
});
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
...toRefs(data),
|
...toRefs(data),
|
||||||
});
|
});
|
||||||
|
|||||||
220
src/components/xt-chat/chat-view/components/right-view/index.vue
Normal file
@ -0,0 +1,220 @@
|
|||||||
|
<script lang="tsx">
|
||||||
|
import { Button } from '@arco-design/web-vue';
|
||||||
|
import { Bubble } from '@/components/xt-chat/xt-bubble';
|
||||||
|
|
||||||
|
import Http from '@/api';
|
||||||
|
import { downloadByUrl } from '@/utils/tools';
|
||||||
|
import markdownit from 'markdown-it';
|
||||||
|
import { message } from 'ant-design-vue';
|
||||||
|
import { FILE_TYPE } from '@/components/xt-chat/chat-view/constants';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
emits: ['close'],
|
||||||
|
props: {
|
||||||
|
dataSource: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
previewData: {
|
||||||
|
type: Object,
|
||||||
|
default: () => {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
setup(props: { dataSource: any[]; previewData: any }, { emit, expose }) {
|
||||||
|
const bubbleRef = ref(null);
|
||||||
|
|
||||||
|
const md = markdownit({
|
||||||
|
html: true,
|
||||||
|
breaks: true,
|
||||||
|
linkify: true,
|
||||||
|
typographer: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const tasks = computed(() => {
|
||||||
|
return props.previewData.payload?.tasks ?? [];
|
||||||
|
});
|
||||||
|
|
||||||
|
const isTaskManage = computed(() => {
|
||||||
|
return props.previewData.task_type === '任务管理';
|
||||||
|
});
|
||||||
|
const hasMediaCenter = computed(() => {
|
||||||
|
return props.dataSource.some((v) => v.task_type === '素材中心');
|
||||||
|
});
|
||||||
|
|
||||||
|
const onDownload = () => {
|
||||||
|
// downloadByUrl('');
|
||||||
|
message.success('下载成功!');
|
||||||
|
};
|
||||||
|
const onAddMediaCenter = () => {
|
||||||
|
const _data = props.dataSource.find((v) => v.task_type === '素材中心');
|
||||||
|
const {
|
||||||
|
api: { endpoint, method },
|
||||||
|
payload,
|
||||||
|
} = _data;
|
||||||
|
Http[method.toLowerCase()]?.(endpoint, payload).then((res) => {
|
||||||
|
const { code } = res;
|
||||||
|
if (code === 200) {
|
||||||
|
message.success('成功添加至“素材中心”模块。');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
const onAddTaskManage = () => {
|
||||||
|
const _data = props.dataSource.find((v) => v.task_type === '任务管理');
|
||||||
|
const {
|
||||||
|
api: { endpoint, method },
|
||||||
|
payload,
|
||||||
|
} = _data;
|
||||||
|
Http[method.toLowerCase()]?.(endpoint, payload).then((res) => {
|
||||||
|
const { code } = res;
|
||||||
|
if (code === 200) {
|
||||||
|
message.success('成功添加至“任务管理”模块。');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
const abortTyping = () => {
|
||||||
|
bubbleRef.value?.abortTyping?.();
|
||||||
|
};
|
||||||
|
const renderHeader = () => {
|
||||||
|
return (
|
||||||
|
<header class="header flex justify-end items-center mb-16px px-32px">
|
||||||
|
{hasMediaCenter.value && (
|
||||||
|
<Button
|
||||||
|
type="outline"
|
||||||
|
size="medium"
|
||||||
|
class="mr-16px"
|
||||||
|
v-slots={{ icon: () => <icon-plus size="14" /> }}
|
||||||
|
onClick={onAddMediaCenter}
|
||||||
|
>
|
||||||
|
素材中心
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Button
|
||||||
|
type="outline"
|
||||||
|
size="medium"
|
||||||
|
class="mr-16px"
|
||||||
|
v-slots={{ icon: () => <icon-plus size="14" /> }}
|
||||||
|
onClick={onAddTaskManage}
|
||||||
|
>
|
||||||
|
任务管理
|
||||||
|
</Button>
|
||||||
|
{/*<Button
|
||||||
|
type="outline"
|
||||||
|
size="medium"
|
||||||
|
class="mr-16px"
|
||||||
|
v-slots={{ icon: () => <icon-download size="14" /> }}
|
||||||
|
onClick={onDownload}
|
||||||
|
>
|
||||||
|
下载
|
||||||
|
</Button>*/}
|
||||||
|
<div class="line mr-24px w-1px h-16px bg-#B1B2B5"></div>
|
||||||
|
<icon-close size={20} class="color-#737478 cursor-pointer" onClick={() => emit('close')} />
|
||||||
|
</header>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
const renderTaskManage = () => {
|
||||||
|
const { file_type } = props.previewData;
|
||||||
|
return tasks.value.map((item) => {
|
||||||
|
const { params, execution_time, name } = item;
|
||||||
|
if (file_type === FILE_TYPE.topic_only) {
|
||||||
|
return (
|
||||||
|
<div class="mb-20px">
|
||||||
|
<p>{params?.media_account ?? '-'}</p>
|
||||||
|
<p>{`- ${execution_time}:${name}`}</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else if (file_type === FILE_TYPE.topic_with_content) {
|
||||||
|
return (
|
||||||
|
<div class="mb-20px">
|
||||||
|
<p>{`${params.media_account}(${params.platform})`}</p>
|
||||||
|
<p>{`日期:${execution_time}`}</p>
|
||||||
|
<p>{`选题:${params.topic}`}</p>
|
||||||
|
<p>{`标题:${name}`}</p>
|
||||||
|
<p>{`正文:${params.content}`}</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
// const renderMediaCenter = () => {
|
||||||
|
// return tasks.value.map((item) => {
|
||||||
|
// const { params, execution_time, name, content } = item;
|
||||||
|
// const { file_type } = dataSource.value;
|
||||||
|
// if (file_type === FILE_TYPE.content_only) {
|
||||||
|
// return (
|
||||||
|
// <div class="mb-20px">
|
||||||
|
// <p>{params?.media_account ?? '-'}</p>
|
||||||
|
// <p>📅 {`${execution_time}`}</p>
|
||||||
|
// <p class="mb-10">📝 {`${name}`}</p>
|
||||||
|
// <p>正文:</p>
|
||||||
|
// <p>📝 {`${content}`}</p>
|
||||||
|
// </div>
|
||||||
|
// );
|
||||||
|
// } else if (file_type === FILE_TYPE.topic_with_content) {
|
||||||
|
// return (
|
||||||
|
// <div class="mb-20px">
|
||||||
|
// <p>{`${params.media_account}(${params.platform})`}</p>
|
||||||
|
// <p>{`日期:${execution_time}`}</p>
|
||||||
|
// <p>{`选题:${params.topic}`}</p>
|
||||||
|
// <p>{`标题${name}`}</p>
|
||||||
|
// <p>{`正文:${params.content}`}</p>
|
||||||
|
// </div>
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
// };
|
||||||
|
const renderContainer = () => {
|
||||||
|
const renderMessage = () => {
|
||||||
|
if (isTaskManage.value) {
|
||||||
|
return renderTaskManage();
|
||||||
|
}
|
||||||
|
// if (isMediaCenter.value) {
|
||||||
|
// return renderMediaCenter();
|
||||||
|
// }
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<section class="flex-1 overflow-y-auto content flex justify-center px-32px">
|
||||||
|
<Bubble
|
||||||
|
ref={bubbleRef}
|
||||||
|
placement="start"
|
||||||
|
variant="borderless"
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
typing={{ step: 2, interval: 100 }}
|
||||||
|
onTypingComplete={() => {
|
||||||
|
console.log('onTypingComplete');
|
||||||
|
}}
|
||||||
|
messageRender={() => {
|
||||||
|
return renderMessage();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
expose({
|
||||||
|
abortTyping,
|
||||||
|
});
|
||||||
|
return () => (
|
||||||
|
<div class="right-view-wrap flex-1 flex flex-col overflow-hidden py-20px">
|
||||||
|
{renderHeader()}
|
||||||
|
{renderContainer()}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.right-view-wrap {
|
||||||
|
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.1);
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
.header,
|
||||||
|
.content {
|
||||||
|
padding-left: 16px !important;
|
||||||
|
padding-right: 16px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -0,0 +1,112 @@
|
|||||||
|
<script lang="tsx">
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import { Sender } from 'ant-design-x-vue';
|
||||||
|
import { Tooltip } from 'ant-design-vue';
|
||||||
|
|
||||||
|
interface SenderInputProps {
|
||||||
|
modelValue?: string;
|
||||||
|
loading?: boolean;
|
||||||
|
placeholder?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'SenderInput',
|
||||||
|
props: {
|
||||||
|
modelValue: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
placeholder: {
|
||||||
|
type: String,
|
||||||
|
default: '随时告诉我你想做什么,比如查数据、发任务、写内容,我会立刻帮你完成。',
|
||||||
|
},
|
||||||
|
loading: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
emits: ['update:modelValue', 'submit', 'cancel'],
|
||||||
|
setup(props: SenderInputProps, { emit, expose }) {
|
||||||
|
const senderRef = ref(null);
|
||||||
|
const localSearchValue = ref(props.modelValue);
|
||||||
|
|
||||||
|
const isEmptyValue = computed(() => !localSearchValue.value.trim());
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.modelValue,
|
||||||
|
(newValue) => {
|
||||||
|
localSearchValue.value = newValue || '';
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleSubmit = () => {
|
||||||
|
if (isEmptyValue.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
emit('submit', localSearchValue.value);
|
||||||
|
// localSearchValue.value = ''
|
||||||
|
};
|
||||||
|
const handleCancel = () => {
|
||||||
|
emit('cancel');
|
||||||
|
};
|
||||||
|
|
||||||
|
const focus = () => {
|
||||||
|
senderRef.value?.focus?.();
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderActions = () => {
|
||||||
|
if (props.loading) {
|
||||||
|
return (
|
||||||
|
<Tooltip title="停止生成" onClick={handleCancel}>
|
||||||
|
<div class="w-32px h-32px p-6px flex justify-center items-center rounded-50% bg-#6D4CFE cursor-pointer">
|
||||||
|
<div class="w-12px h-12px rounded-2px bg-#FFF"></div>
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
onClick={handleSubmit}
|
||||||
|
class={`submit-btn w-32px h-32px p-6px flex justify-center items-center rounded-50% cursor-pointer ${
|
||||||
|
isEmptyValue.value ? 'opacity-50' : ''
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<icon-arrow-right size={20} class="color-#FFFFFF" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
expose({
|
||||||
|
focus,
|
||||||
|
searchValue: computed(() => localSearchValue.value),
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => (
|
||||||
|
<div class="sender-input-wrap full h-120px">
|
||||||
|
<Sender
|
||||||
|
v-model:value={localSearchValue.value}
|
||||||
|
ref={senderRef}
|
||||||
|
onChange={(value: string) => emit('update:modelValue', value.trim())}
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
class="h-full w-full mb-24px"
|
||||||
|
placeholder={props.placeholder}
|
||||||
|
actions={() => renderActions()}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.sender-input-wrap {
|
||||||
|
:deep(.ant-sender) {
|
||||||
|
.submit-btn {
|
||||||
|
background: linear-gradient(125deg, #6d4cfe 32.25%, #3ba1f0 72.31%),
|
||||||
|
linear-gradient(113deg, #6d4cfe 0%, #b93bf0 100%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
58
src/components/xt-chat/chat-view/constants.ts
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
import type { Ref } from 'vue';
|
||||||
|
import type { BubbleListProps } from '@/components/xt-chat/xt-bubble/types';
|
||||||
|
|
||||||
|
export interface UseChatHandlerReturn {
|
||||||
|
roles?: BubbleListProps['roles'];
|
||||||
|
generateTeamRunTaskId?: Ref<string | null>;
|
||||||
|
handleMessage?: (parsedData: { event: string; data: MESSAGE.Answer }) => void;
|
||||||
|
handleOpen?: (data: Response) => void;
|
||||||
|
generateLoading?: Ref<boolean>;
|
||||||
|
conversationList?: Ref<any[]>;
|
||||||
|
showRightView?: Ref<boolean>;
|
||||||
|
rightViewDataSource?: Ref<any>;
|
||||||
|
rightPreviewData?: Ref<any>;
|
||||||
|
senderRef?: Ref<null>
|
||||||
|
}
|
||||||
|
export enum EnumTeamRunStatus {
|
||||||
|
TeamRunStarted = 'TeamRunStarted', // 对话开始
|
||||||
|
TeamRunResponseContent = 'TeamRunResponseContent', // 对话执行中
|
||||||
|
TeamRunCompleted = 'TeamRunCompleted', // 对话完成
|
||||||
|
RunStarted = 'RunStarted', // l2开始运行
|
||||||
|
RunResponseContent = 'RunResponseContent', // l2执行中
|
||||||
|
RunCompleted = 'RunCompleted', // l2完成
|
||||||
|
}
|
||||||
|
export interface UseChatHandlerOptions {
|
||||||
|
initSse: (inputInfo: CHAT.TInputInfo) => Promise<void>; // 明确 initSse 带参
|
||||||
|
}
|
||||||
|
|
||||||
|
// 定义角色常量
|
||||||
|
export const LOADING_ROLE = 'loading'; // 加载中
|
||||||
|
export const INTELLECTUAL_THINKING_ROLE = 'intellectual_thinking'; // 智能思考标题
|
||||||
|
export const QUESTION_ROLE = 'question'; // 问题
|
||||||
|
export const ANSWER_ROLE = 'answer'; // 回答
|
||||||
|
export const REMOTE_USER_ROLE = 'user'; // 接口返回用户
|
||||||
|
export const REMOTE_ASSISTANT_ROLE = 'assistant'; // 接口返回智能体
|
||||||
|
|
||||||
|
export const ROLE_STYLE = {
|
||||||
|
width: '600px',
|
||||||
|
margin: '0 auto',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ANSWER_STYLE = {
|
||||||
|
...ROLE_STYLE,
|
||||||
|
paddingLeft: '14px',
|
||||||
|
borderLeft: '1px solid #E6E6E8',
|
||||||
|
position: 'relative',
|
||||||
|
left: '6px',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const FILE_TYPE = {
|
||||||
|
topic_only: 'topic_only', // 排期&选题
|
||||||
|
topic_with_content: 'topic_with_content', // 选题&内容稿件
|
||||||
|
content_only: 'content_only', // 内容稿件
|
||||||
|
}
|
||||||
|
export const FILE_TYPE_MAP = {
|
||||||
|
[FILE_TYPE.topic_only]: '排期&选题',
|
||||||
|
[FILE_TYPE.topic_with_content]: '选题&内容稿件',
|
||||||
|
[FILE_TYPE.content_only]: '内容稿件',
|
||||||
|
}
|
||||||
196
src/components/xt-chat/chat-view/index.vue
Normal file
@ -0,0 +1,196 @@
|
|||||||
|
<script lang="tsx">
|
||||||
|
import { message as antdMessage } from 'ant-design-vue';
|
||||||
|
import { BubbleList } from '@/components/xt-chat/xt-bubble';
|
||||||
|
import SenderInput from './components/sender-input/index.vue';
|
||||||
|
import RightView from './components/right-view/index.vue';
|
||||||
|
|
||||||
|
import { useChatStore } from '@/stores/modules/chat';
|
||||||
|
import { getConversationList } from '@/api/all/chat';
|
||||||
|
import querySSE from '@/utils/querySSE';
|
||||||
|
import useChatHandler from './useChatHandler';
|
||||||
|
import { QUESTION_ROLE, LOADING_ROLE } from './constants';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
inputInfo: {
|
||||||
|
type: Object as () => CHAT.TInputInfo,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
conversationId: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
setup(props, { emit, expose }) {
|
||||||
|
const chatStore = useChatStore();
|
||||||
|
|
||||||
|
// const route = useRoute();
|
||||||
|
|
||||||
|
const rightViewRef = ref(null);
|
||||||
|
const bubbleListRef = ref<any>(null);
|
||||||
|
const sseController = ref<any>(null);
|
||||||
|
const searchValue = ref<string>('');
|
||||||
|
|
||||||
|
const handleSubmit = (message: string) => {
|
||||||
|
if (generateLoading.value) {
|
||||||
|
antdMessage.warning('停止生成后可发送');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
searchValue.value = '';
|
||||||
|
|
||||||
|
conversationList.value.push({
|
||||||
|
role: QUESTION_ROLE,
|
||||||
|
content: message,
|
||||||
|
});
|
||||||
|
|
||||||
|
initSse({ message });
|
||||||
|
};
|
||||||
|
|
||||||
|
const clearSseController = () => {
|
||||||
|
if (sseController.value) {
|
||||||
|
sseController.value.abort?.();
|
||||||
|
sseController.value = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancel = () => {
|
||||||
|
if (generateLoading.value) {
|
||||||
|
bubbleListRef.value?.abortTypingByKey(generateTeamRunTaskId.value);
|
||||||
|
sseController.value?.abort?.();
|
||||||
|
}
|
||||||
|
if (showRightView.value) {
|
||||||
|
rightViewRef.value?.abortTyping?.();
|
||||||
|
}
|
||||||
|
|
||||||
|
generateLoading.value = false;
|
||||||
|
antdMessage.info('取消生成');
|
||||||
|
};
|
||||||
|
|
||||||
|
const initSse = async (inputInfo: CHAT.TInputInfo): Promise<void> => {
|
||||||
|
clearSseController();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { message } = inputInfo;
|
||||||
|
|
||||||
|
generateLoading.value = true;
|
||||||
|
|
||||||
|
sseController.value = await querySSE({
|
||||||
|
method: 'POST',
|
||||||
|
handleMessage,
|
||||||
|
body: JSON.stringify({
|
||||||
|
content: message,
|
||||||
|
session_id: props.conversationId,
|
||||||
|
agent_id: chatStore.agentInfo.agent_id,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to initialize SSE:', error);
|
||||||
|
antdMessage.error('初始化连接失败');
|
||||||
|
generateLoading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getConversationInfo = async () => {
|
||||||
|
const { data, code } = await getConversationList({
|
||||||
|
session_id: props.conversationId,
|
||||||
|
agent_id: chatStore.agentInfo.agent_id,
|
||||||
|
});
|
||||||
|
if (code === 200) {
|
||||||
|
const remoteData = (data.list?.flat(1) ?? []).map((v: any) => ({
|
||||||
|
...v,
|
||||||
|
teamRunTaskId: v.step_run_id,
|
||||||
|
}));
|
||||||
|
conversationList.value = [...conversationList.value, ...remoteData];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const {
|
||||||
|
roles,
|
||||||
|
showRightView,
|
||||||
|
rightViewDataSource,
|
||||||
|
rightPreviewData,
|
||||||
|
generateTeamRunTaskId,
|
||||||
|
handleMessage,
|
||||||
|
conversationList,
|
||||||
|
generateLoading,
|
||||||
|
senderRef,
|
||||||
|
} = useChatHandler({
|
||||||
|
initSse,
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.inputInfo,
|
||||||
|
async (newVal) => {
|
||||||
|
if (newVal) {
|
||||||
|
const { message } = newVal;
|
||||||
|
conversationList.value.push({
|
||||||
|
role: QUESTION_ROLE,
|
||||||
|
content: message,
|
||||||
|
});
|
||||||
|
|
||||||
|
initSse(newVal);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ deep: true },
|
||||||
|
);
|
||||||
|
watch(
|
||||||
|
() => props.conversationId,
|
||||||
|
(newVal) => {
|
||||||
|
if (newVal) {
|
||||||
|
getConversationInfo();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
);
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
clearSseController();
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => (
|
||||||
|
<div class="chat-view-wrap w-full h-full flex">
|
||||||
|
<section class="flex-1 flex flex-col pt-20px justify-center relative pl-16px pr-2px">
|
||||||
|
<div class="flex-1 overflow-hidden pb-20px">
|
||||||
|
<BubbleList
|
||||||
|
ref={bubbleListRef}
|
||||||
|
roles={roles}
|
||||||
|
items={[
|
||||||
|
...conversationList.value,
|
||||||
|
generateLoading.value ? { loading: true, role: LOADING_ROLE } : null,
|
||||||
|
].filter(Boolean)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="w-full flex flex-col justify-center items-center">
|
||||||
|
<SenderInput
|
||||||
|
v-model={searchValue.value}
|
||||||
|
ref={senderRef}
|
||||||
|
class="w-600px"
|
||||||
|
placeholder="继续追问..."
|
||||||
|
loading={generateLoading.value}
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
onCancel={handleCancel}
|
||||||
|
/>
|
||||||
|
<p class="cts !color-#939499 !text-12px !lh-20px my-4px">内容由AI生成,仅供参考</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* 右侧展示区域 */}
|
||||||
|
{showRightView.value && (
|
||||||
|
<RightView
|
||||||
|
ref={rightViewRef}
|
||||||
|
dataSource={rightViewDataSource.value}
|
||||||
|
previewData={rightPreviewData.value}
|
||||||
|
showRightView={showRightView.value}
|
||||||
|
onClose={() => (showRightView.value = false)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@import './style.scss';
|
||||||
|
</style>
|
||||||
20
src/components/xt-chat/chat-view/style.scss
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
.chat-view-wrap {
|
||||||
|
.cts {
|
||||||
|
color: var(--Text-1, #737478);
|
||||||
|
font-size: 14px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 22px;
|
||||||
|
}
|
||||||
|
:deep(.xt-bubble) {
|
||||||
|
.file-card {
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 1px solid var(--Border-2, #e6e6e8);
|
||||||
|
background: linear-gradient(90deg, #f6f4ff 0%, #fff 100%);
|
||||||
|
padding: 13px 16px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
423
src/components/xt-chat/chat-view/useChatHandler.tsx
Normal file
@ -0,0 +1,423 @@
|
|||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
import type { BubbleListProps } from '@/components/xt-chat/xt-bubble/types';
|
||||||
|
import markdownit from 'markdown-it';
|
||||||
|
import { message as antdMessage } from 'ant-design-vue';
|
||||||
|
import {
|
||||||
|
IconFile,
|
||||||
|
IconCaretUp,
|
||||||
|
IconDownload,
|
||||||
|
IconCaretDown,
|
||||||
|
IconRefresh,
|
||||||
|
IconCopy,
|
||||||
|
} from '@arco-design/web-vue/es/icon';
|
||||||
|
|
||||||
|
import { Tooltip } from 'ant-design-vue';
|
||||||
|
import TextOverTips from '@/components/text-over-tips/index.vue';
|
||||||
|
import { genRandomId, exactFormatTime } from '@/utils/tools';
|
||||||
|
|
||||||
|
import icon1 from '@/assets/img/agent/icon-end.png';
|
||||||
|
import icon2 from '@/assets/img/agent/icon-loading.png';
|
||||||
|
|
||||||
|
import { useClipboard } from '@vueuse/core';
|
||||||
|
import {
|
||||||
|
QUESTION_ROLE,
|
||||||
|
ANSWER_ROLE,
|
||||||
|
INTELLECTUAL_THINKING_ROLE,
|
||||||
|
LOADING_ROLE,
|
||||||
|
ROLE_STYLE,
|
||||||
|
EnumTeamRunStatus,
|
||||||
|
REMOTE_USER_ROLE,
|
||||||
|
REMOTE_ASSISTANT_ROLE,
|
||||||
|
FILE_TYPE_MAP,
|
||||||
|
} from './constants';
|
||||||
|
import type { UseChatHandlerReturn, UseChatHandlerOptions } from './constants';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 聊天处理器Hook
|
||||||
|
* @returns 包含角色配置、消息处理函数和对话列表的对象
|
||||||
|
*/
|
||||||
|
export default function useChatHandler(options: UseChatHandlerOptions): UseChatHandlerReturn {
|
||||||
|
const { initSse } = options;
|
||||||
|
// 在内部定义对话列表
|
||||||
|
const { copy } = useClipboard();
|
||||||
|
|
||||||
|
const senderRef = ref(null);
|
||||||
|
const conversationList = ref<MESSAGE.Answer[]>([]);
|
||||||
|
const generateLoading = ref<boolean>(false);
|
||||||
|
const generateTeamRunTaskId = ref<string | null>(null);
|
||||||
|
|
||||||
|
const showRightView = ref(false);
|
||||||
|
const rightViewDataSource = ref<any>([]);
|
||||||
|
const rightPreviewData = ref<any>([]);
|
||||||
|
|
||||||
|
// 初始化markdown
|
||||||
|
const md = markdownit({
|
||||||
|
html: true,
|
||||||
|
breaks: true,
|
||||||
|
linkify: true,
|
||||||
|
typographer: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 定义角色配置
|
||||||
|
const roles: BubbleListProps['roles'] = {
|
||||||
|
[LOADING_ROLE]: {
|
||||||
|
placement: 'start',
|
||||||
|
variant: 'borderless',
|
||||||
|
style: { ...ROLE_STYLE, paddingLeft: '20px' },
|
||||||
|
},
|
||||||
|
[INTELLECTUAL_THINKING_ROLE]: {
|
||||||
|
placement: 'start',
|
||||||
|
variant: 'borderless',
|
||||||
|
typing: { step: 2, interval: 100 },
|
||||||
|
style: ROLE_STYLE,
|
||||||
|
},
|
||||||
|
[ANSWER_ROLE]: {
|
||||||
|
placement: 'start',
|
||||||
|
variant: 'borderless',
|
||||||
|
typing: { step: 2, interval: 100 },
|
||||||
|
// onTypingComplete: () => {
|
||||||
|
// generateTeamRunTaskId.value = null;
|
||||||
|
// },
|
||||||
|
style: ROLE_STYLE,
|
||||||
|
},
|
||||||
|
[QUESTION_ROLE]: {
|
||||||
|
placement: 'end',
|
||||||
|
shape: 'round',
|
||||||
|
style: ROLE_STYLE,
|
||||||
|
messageRender: (message: string) => {
|
||||||
|
return <div class="max-w-400px">
|
||||||
|
{message}
|
||||||
|
</div>;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
[REMOTE_USER_ROLE]: {
|
||||||
|
placement: 'end',
|
||||||
|
shape: 'round',
|
||||||
|
style: ROLE_STYLE,
|
||||||
|
messageRender: (message: string) => {
|
||||||
|
return <div class="max-w-400px">
|
||||||
|
{message}
|
||||||
|
</div>;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
[REMOTE_ASSISTANT_ROLE]: {
|
||||||
|
placement: 'start',
|
||||||
|
variant: 'borderless',
|
||||||
|
style: ROLE_STYLE,
|
||||||
|
messageRender: (message: string) => {
|
||||||
|
return <div class="max-w-400px markdown-wrap" v-html={md.render(message)} />;
|
||||||
|
},
|
||||||
|
footer: (params) => {
|
||||||
|
const { content, item } = params as { content: string; item: MESSAGE.Answer };
|
||||||
|
const isShow = conversationList.value[conversationList.value.length - 1].run_id === item.run_id;
|
||||||
|
return (
|
||||||
|
<div class="flex items-center">
|
||||||
|
<Tooltip title="复制" onClick={() => onCopy(content)} align={{ offset: [0, 4] }} >
|
||||||
|
<div class="action-box">
|
||||||
|
<IconCopy size={16} class="color-#737478" />
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
{isShow && (
|
||||||
|
<Tooltip title="重新生成" onClick={() => handleRemoteRefresh(item)} align={{ offset: [0, 4] }}>
|
||||||
|
<div class="action-box ml-12px">
|
||||||
|
<IconRefresh size={16} class="color-#737478 " />
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// 下载处理
|
||||||
|
const onDownload = () => {
|
||||||
|
console.log('onDownload', rightViewDataSource.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onCopy = (content: string) => {
|
||||||
|
copy(content);
|
||||||
|
antdMessage.success('复制成功!');
|
||||||
|
};
|
||||||
|
|
||||||
|
// 重置生成状态
|
||||||
|
const resetGenerateStatus = () => {
|
||||||
|
generateLoading.value = false;
|
||||||
|
generateTeamRunTaskId.value = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRemoteRefresh = (item: MESSAGE.Answer) => {
|
||||||
|
generateLoading.value = true;
|
||||||
|
|
||||||
|
const targetIndex = conversationList.value.findIndex(
|
||||||
|
(v) => v.teamRunTaskId === item.teamRunTaskId && v.run_id === item.run_id && v.role === REMOTE_ASSISTANT_ROLE,
|
||||||
|
);
|
||||||
|
const message = conversationList.value[targetIndex - 1]?.content;
|
||||||
|
conversationList.value.splice(targetIndex, 1);
|
||||||
|
|
||||||
|
initSse({ message });
|
||||||
|
};
|
||||||
|
|
||||||
|
const onRefresh = (run_id: string) => {
|
||||||
|
generateLoading.value = true;
|
||||||
|
|
||||||
|
const targetIndex = conversationList.value.findIndex((v) => v.teamRunTaskId === run_id);
|
||||||
|
conversationList.value = conversationList.value.filter((item) => item.teamRunTaskId !== run_id);
|
||||||
|
const message = conversationList.value[targetIndex - 1]?.content;
|
||||||
|
initSse({ message });
|
||||||
|
};
|
||||||
|
|
||||||
|
const getAllRunTask = (teamRunTaskId: string) => {
|
||||||
|
return conversationList.value.filter(
|
||||||
|
(item) => item.role === ANSWER_ROLE && item.teamRunTaskId === teamRunTaskId && !item.isTeamRunTask,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
const getRunTask = (run_id: string) => {
|
||||||
|
return conversationList.value.find((item) => item.run_id === run_id && !item.isTeamRunTask);
|
||||||
|
};
|
||||||
|
// 设置当前对话所有思考过程任务展开收起状态
|
||||||
|
const setRunTaskCollapse = (teamRunTaskId: string, isCollapse: boolean) => {
|
||||||
|
getAllRunTask(teamRunTaskId).forEach((item) => {
|
||||||
|
item.content.isCollapse = isCollapse;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
// 获取同一个对话下的最后一个run_task
|
||||||
|
const getLastRunTask = (teamRunTaskId: string) => {
|
||||||
|
const allRunTask = getAllRunTask(teamRunTaskId);
|
||||||
|
return allRunTask[allRunTask.length - 1] ?? {};
|
||||||
|
};
|
||||||
|
const getFirstRunTask = (teamRunTaskId: string) => {
|
||||||
|
const allRunTask = getAllRunTask(teamRunTaskId);
|
||||||
|
return allRunTask[0] ?? {};
|
||||||
|
};
|
||||||
|
// 判断当前对话是否含有过程任务
|
||||||
|
const hasRunTask = (teamRunTaskId: string) => {
|
||||||
|
return conversationList.value.some((item) => item.teamRunTaskId === teamRunTaskId && !item.isTeamRunTask);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getTeamRunTask = (teamRunTaskId: string) => {
|
||||||
|
return conversationList.value.find((item) => item.teamRunTaskId === teamRunTaskId);
|
||||||
|
};
|
||||||
|
const isLastRunTask = (data: MESSAGE.Answer): boolean => {
|
||||||
|
const { teamRunTaskId, run_id } = data;
|
||||||
|
return getLastRunTask(teamRunTaskId).run_id === run_id;
|
||||||
|
};
|
||||||
|
const isFirstRunTask = (data: MESSAGE.Answer): boolean => {
|
||||||
|
const { teamRunTaskId, run_id } = data;
|
||||||
|
return getFirstRunTask(teamRunTaskId).run_id === run_id;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 过程节点开始
|
||||||
|
const handleRunTaskStart = (data: MESSAGE.Answer) => {
|
||||||
|
const { run_id } = data;
|
||||||
|
// generateTeamRunTaskId.value = run_id;
|
||||||
|
conversationList.value.push({
|
||||||
|
run_id,
|
||||||
|
key: run_id,
|
||||||
|
teamRunTaskId: generateTeamRunTaskId.value,
|
||||||
|
content: { ...data, runStatus: EnumTeamRunStatus.RunStarted, teamRunTaskId: generateTeamRunTaskId.value },
|
||||||
|
output: data.output,
|
||||||
|
role: ANSWER_ROLE,
|
||||||
|
messageRender: (data: MESSAGE.Answer) => {
|
||||||
|
const { node, output, runStatus, isCollapse = true, customRender, teamRunTaskId } = data;
|
||||||
|
const isRulCompleted = runStatus === EnumTeamRunStatus.RunCompleted;
|
||||||
|
|
||||||
|
let outputEleClass: string = `thought-chain-output border-l-#E6E6E8 border-l-1px pl-12px relative left-8px mb-4px markdown-wrap`;
|
||||||
|
!isLastRunTask(data) && (outputEleClass += ' hasLine pb-12px pt-4px');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{isFirstRunTask(data) && (
|
||||||
|
<div
|
||||||
|
class="flex items-center mb-8px cursor-pointer"
|
||||||
|
onClick={() => setRunTaskCollapse(teamRunTaskId, !isCollapse)}
|
||||||
|
>
|
||||||
|
<span class="font-family-medium color-#211F24 text-14px font-400 lh-22px mr-4px">智能思考</span>
|
||||||
|
{isCollapse ? (
|
||||||
|
<IconCaretUp size={12} class="color-#211F24 " />
|
||||||
|
) : (
|
||||||
|
<IconCaretDown size={12} class="color-#211F24" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div class="relative thought-chain-item" style={{ display: isCollapse ? 'block' : 'none' }}>
|
||||||
|
<div class="flex items-center mb-4px">
|
||||||
|
<img src={isRulCompleted ? icon1 : icon2} width={16} height={16} class="mr-4px" />
|
||||||
|
<div class="color-#211F24 !lh-20px">{node}</div>
|
||||||
|
</div>
|
||||||
|
<div v-html={md.render(output)} class={outputEleClass} />
|
||||||
|
</div>
|
||||||
|
{customRender?.()}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
// 过程节点更新
|
||||||
|
const handleRunTaskUpdate = (data: MESSAGE.Answer) => {
|
||||||
|
const { run_id, output } = data;
|
||||||
|
|
||||||
|
const existingItem = conversationList.value.find((item) => item.run_id === run_id);
|
||||||
|
if (existingItem && output) {
|
||||||
|
existingItem.content.output += output;
|
||||||
|
existingItem.content.runStatus = EnumTeamRunStatus.RunResponseContent;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// 过程节点结束
|
||||||
|
const handleRunTaskEnd = (data: MESSAGE.Answer) => {
|
||||||
|
const { output } = data;
|
||||||
|
|
||||||
|
const existingItem = getRunTask(data.run_id);
|
||||||
|
|
||||||
|
if (existingItem) {
|
||||||
|
existingItem.content.output += output;
|
||||||
|
existingItem.content.runStatus = EnumTeamRunStatus.RunCompleted;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 任务开始
|
||||||
|
const handleTeamRunTaskStart = (data: MESSAGE.Answer) => {
|
||||||
|
const { run_id } = data;
|
||||||
|
generateTeamRunTaskId.value = run_id;
|
||||||
|
conversationList.value.push({
|
||||||
|
run_id,
|
||||||
|
isTeamRunTask: true,
|
||||||
|
teamRunTaskId: generateTeamRunTaskId.value,
|
||||||
|
key: run_id,
|
||||||
|
content: { ...data, teamRunStatus: EnumTeamRunStatus.TeamRunStarted, teamRunTaskId: run_id },
|
||||||
|
output: data.output,
|
||||||
|
role: ANSWER_ROLE,
|
||||||
|
messageRender: (data: MESSAGE.Answer) => {
|
||||||
|
return <div v-html={md.render(data.output ?? '')} class="markdown-wrap" />;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
// 任务更新
|
||||||
|
const handleTeamRunTaskUpdate = (data: MESSAGE.Answer) => {
|
||||||
|
const { run_id, output } = data;
|
||||||
|
const existingItem = conversationList.value.find((item) => item.run_id === run_id);
|
||||||
|
if (existingItem && output) {
|
||||||
|
existingItem.content.output += output;
|
||||||
|
existingItem.content.teamRunStatus = EnumTeamRunStatus.TeamRunResponseContent;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// 任务结束
|
||||||
|
const handleTeamRunTaskEnd = (data: MESSAGE.Answer) => {
|
||||||
|
resetGenerateStatus();
|
||||||
|
|
||||||
|
const { run_id: teamRunTaskId, extra_data, output } = data;
|
||||||
|
|
||||||
|
const _hasRunTask = hasRunTask(teamRunTaskId);
|
||||||
|
const _targetTask = _hasRunTask ? getLastRunTask(teamRunTaskId) : getTeamRunTask(teamRunTaskId);
|
||||||
|
|
||||||
|
if (isEmpty(_targetTask)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 含有思考过程,折叠思考过程,展示结果
|
||||||
|
if (_hasRunTask) {
|
||||||
|
setRunTaskCollapse(teamRunTaskId, false);
|
||||||
|
const _targetData = extra_data?.data?.find((item: any) => item.task_type === '任务管理')
|
||||||
|
if (_targetData) {
|
||||||
|
showRightView.value = true;
|
||||||
|
rightViewDataSource.value = extra_data.data;
|
||||||
|
rightPreviewData.value = _targetData;
|
||||||
|
}
|
||||||
|
|
||||||
|
_targetTask.content.customRender = () => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div v-html={md.render(output)} class="markdown-wrap" />
|
||||||
|
{_targetData && (
|
||||||
|
<div class="file-card mt-10px">
|
||||||
|
<IconFile class="w-24px h-24px mr-16px color-#6D4CFE" />
|
||||||
|
<div>
|
||||||
|
<TextOverTips
|
||||||
|
context={FILE_TYPE_MAP?.[_targetData.file_type] ?? '-'}
|
||||||
|
class="font-family-medium color-#211F24 text-14px font-400 lh-22px mb-4px"
|
||||||
|
/>
|
||||||
|
<span class="color-#939499 font-family-regular text-12px font-400 lh-22px">
|
||||||
|
创建时间:{exactFormatTime(dayjs().unix())}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
_targetTask.content.teamRunStatus = EnumTeamRunStatus.TeamRunCompleted;
|
||||||
|
}
|
||||||
|
|
||||||
|
_targetTask.footer = () => {
|
||||||
|
const isShow = conversationList.value[conversationList.value.length - 1].teamRunTaskId === teamRunTaskId;
|
||||||
|
return (
|
||||||
|
<div class="flex items-center">
|
||||||
|
{!extra_data && (
|
||||||
|
// ? (
|
||||||
|
// <Tooltip title="下载" onClick={onDownload} align={{ offset: [0, 4] }}>
|
||||||
|
// <div class="action-box">
|
||||||
|
// <IconDownload size={16} class="color-#737478 mr-12px" />
|
||||||
|
// </div>
|
||||||
|
// </Tooltip>
|
||||||
|
// ) :
|
||||||
|
<Tooltip title="复制" onClick={() => onCopy(_targetTask.content.output)} align={{ offset: [0, 4] }}>
|
||||||
|
<div class="action-box">
|
||||||
|
<IconCopy size={16} class="color-#737478" />
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
{isShow && (
|
||||||
|
<Tooltip title="重新生成" onClick={() => onRefresh(teamRunTaskId)} align={{ offset: [0, 4] }}>
|
||||||
|
<div class="action-box ml-12px">
|
||||||
|
<IconRefresh size={16} class="color-#737478 " />
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// 消息处理主函数
|
||||||
|
const handleMessage = (parsedData: { event: string; data: MESSAGE.Answer }) => {
|
||||||
|
const { data } = parsedData;
|
||||||
|
const { status } = data;
|
||||||
|
switch (status) {
|
||||||
|
case EnumTeamRunStatus.RunStarted:
|
||||||
|
handleRunTaskStart(data);
|
||||||
|
break;
|
||||||
|
case EnumTeamRunStatus.RunResponseContent:
|
||||||
|
handleRunTaskUpdate(data);
|
||||||
|
break;
|
||||||
|
case EnumTeamRunStatus.RunCompleted:
|
||||||
|
handleRunTaskEnd(data);
|
||||||
|
break;
|
||||||
|
case EnumTeamRunStatus.TeamRunStarted:
|
||||||
|
handleTeamRunTaskStart(data);
|
||||||
|
break;
|
||||||
|
case EnumTeamRunStatus.TeamRunResponseContent:
|
||||||
|
handleTeamRunTaskUpdate(data);
|
||||||
|
break;
|
||||||
|
case EnumTeamRunStatus.TeamRunCompleted:
|
||||||
|
handleTeamRunTaskEnd(data);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
roles,
|
||||||
|
senderRef,
|
||||||
|
generateTeamRunTaskId,
|
||||||
|
handleMessage,
|
||||||
|
generateLoading,
|
||||||
|
conversationList,
|
||||||
|
showRightView,
|
||||||
|
rightViewDataSource,
|
||||||
|
rightPreviewData
|
||||||
|
};
|
||||||
|
}
|
||||||
81
src/components/xt-chat/xt-bubble/context.ts
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
/*
|
||||||
|
* @Author: RenXiaoDong
|
||||||
|
* @Date: 2025-08-20 23:17:49
|
||||||
|
* @Description: 气泡上下文管理
|
||||||
|
* 提供气泡组件间的通信机制,支持全局状态管理和组件间数据传递
|
||||||
|
*/
|
||||||
|
import { computed, defineComponent, inject, provide, shallowRef, triggerRef, unref, watch } from "vue";
|
||||||
|
import type { ComputedRef, InjectionKey } from "vue";
|
||||||
|
import { objectType } from "@/utils/type";
|
||||||
|
import type { BubbleContextProps } from "./types";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 气泡上下文注入键
|
||||||
|
* 用于Vue的依赖注入系统,确保上下文的唯一性
|
||||||
|
*/
|
||||||
|
const BubbleContextKey: InjectionKey<ComputedRef<BubbleContextProps>> =
|
||||||
|
Symbol('BubbleContext');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 全局气泡上下文API
|
||||||
|
* 提供全局访问气泡上下文的能力,用于跨组件通信
|
||||||
|
*/
|
||||||
|
export const globalBubbleContextApi = shallowRef<BubbleContextProps>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 气泡上下文提供者Hook
|
||||||
|
* 向子组件提供气泡上下文,并同步更新全局API
|
||||||
|
*
|
||||||
|
* @param value - 要提供的上下文值(计算属性)
|
||||||
|
*/
|
||||||
|
export const useBubbleContextProvider = (value: ComputedRef<BubbleContextProps>) => {
|
||||||
|
// 向子组件提供上下文
|
||||||
|
provide(BubbleContextKey, value);
|
||||||
|
|
||||||
|
// 监听上下文变化,同步更新全局API
|
||||||
|
watch(
|
||||||
|
value,
|
||||||
|
() => {
|
||||||
|
globalBubbleContextApi.value = unref(value);
|
||||||
|
// 触发响应式更新
|
||||||
|
triggerRef(globalBubbleContextApi);
|
||||||
|
},
|
||||||
|
{ immediate: true, deep: true }, // 立即执行,深度监听
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 气泡上下文注入Hook
|
||||||
|
* 从父组件或全局API获取气泡上下文
|
||||||
|
*
|
||||||
|
* @returns 气泡上下文(计算属性)
|
||||||
|
*/
|
||||||
|
export const useBubbleContextInject = () => {
|
||||||
|
return inject(
|
||||||
|
BubbleContextKey,
|
||||||
|
// 如果没有找到注入的上下文,使用全局API作为后备
|
||||||
|
computed(() => globalBubbleContextApi.value || {}),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 气泡上下文提供者组件
|
||||||
|
* 用于在模板中提供气泡上下文,简化使用方式
|
||||||
|
*/
|
||||||
|
export const BubbleContextProvider = defineComponent({
|
||||||
|
props: {
|
||||||
|
// 上下文值,支持对象类型验证
|
||||||
|
value: objectType<BubbleContextProps>(),
|
||||||
|
},
|
||||||
|
setup(props, { slots }) {
|
||||||
|
// 使用计算属性包装props.value,确保响应式
|
||||||
|
useBubbleContextProvider(computed(() => props.value));
|
||||||
|
|
||||||
|
// 渲染默认插槽内容
|
||||||
|
return () => {
|
||||||
|
return slots.default?.();
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default BubbleContextProvider;
|
||||||
87
src/components/xt-chat/xt-bubble/hooks/useDisplayData.ts
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
/*
|
||||||
|
* @Author: RenXiaoDong
|
||||||
|
* @Date: 2025-08-20 23:12:42
|
||||||
|
* @Description: 聊天气泡列表数据显示逻辑Hook
|
||||||
|
* 用于控制气泡列表的渐进式显示,实现打字效果和动态加载
|
||||||
|
*/
|
||||||
|
import { computed, unref, watch } from 'vue';
|
||||||
|
import type { Ref } from 'vue';
|
||||||
|
import { useEventCallback } from '@/hooks/useEventCallback';
|
||||||
|
import useState from '@/hooks/useState';
|
||||||
|
import type { ListItemType } from './useListData';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook返回值类型定义
|
||||||
|
* @returns [displayList: 当前显示的数据列表, onTypingComplete: 打字完成回调函数]
|
||||||
|
*/
|
||||||
|
type UseDisplayDataReturn = [Ref<ListItemType[]>, (value: string | number) => void];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 数据显示逻辑Hook
|
||||||
|
* 实现气泡列表的渐进式显示,支持打字效果和动态加载
|
||||||
|
*
|
||||||
|
* @param items - 完整的数据列表引用
|
||||||
|
* @returns [displayList, onTypingComplete] - 当前显示列表和打字完成回调
|
||||||
|
*/
|
||||||
|
export default function useDisplayData(items: Ref<ListItemType[]>): UseDisplayDataReturn {
|
||||||
|
// 当前显示的数据条数,初始值为完整列表的长度
|
||||||
|
const [displayCount, setDisplayCount] = useState(items.value.length);
|
||||||
|
|
||||||
|
// 计算当前应该显示的数据列表(从完整列表中截取前displayCount条)
|
||||||
|
const displayList = computed(() => items.value.slice(0, unref(displayCount)));
|
||||||
|
|
||||||
|
// 获取当前显示列表中最后一条数据的key,用于判断是否继续显示下一条
|
||||||
|
const displayListLastKey = computed(() => {
|
||||||
|
const lastItem = unref(displayList)[unref(displayList).length - 1];
|
||||||
|
return lastItem ? lastItem.key : null;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 监听完整数据列表的变化,智能调整显示数量
|
||||||
|
watch(
|
||||||
|
items,
|
||||||
|
() => {
|
||||||
|
// 当数据列表变化时,先尝试显示所有数据
|
||||||
|
setDisplayCount(items.value.length);
|
||||||
|
|
||||||
|
// 检查当前显示列表是否与完整列表的前N项完全匹配
|
||||||
|
// 如果匹配,说明数据没有变化,保持当前显示状态
|
||||||
|
if (
|
||||||
|
unref(displayList).length &&
|
||||||
|
unref(displayList).every((item, index) => item.key === items.value[index]?.key)
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果当前没有显示任何数据,显示第一条
|
||||||
|
if (unref(displayList).length === 0) {
|
||||||
|
setDisplayCount(1);
|
||||||
|
} else {
|
||||||
|
// 找到第一个不匹配的位置,将显示数量设置为该位置
|
||||||
|
// 这样可以保持已显示数据的连续性,避免跳跃
|
||||||
|
for (let i = 0; i < unref(displayList).length; i += 1) {
|
||||||
|
if (unref(displayList)[i].key !== items.value[i]?.key) {
|
||||||
|
setDisplayCount(i);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true, deep: true }, // 立即执行,深度监听
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 打字完成回调函数
|
||||||
|
* 当某个气泡的打字效果完成时,显示下一条数据
|
||||||
|
*
|
||||||
|
* @param key - 完成打字的项目key
|
||||||
|
*/
|
||||||
|
const onTypingComplete = useEventCallback((key: string | number) => {
|
||||||
|
// 只有当完成打字的是当前显示列表的最后一条时,才显示下一条
|
||||||
|
// 这确保了显示的顺序性和连续性
|
||||||
|
if (key === unref(displayListLastKey)) {
|
||||||
|
setDisplayCount(unref(displayCount) + 1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return [displayList, onTypingComplete] as const;
|
||||||
|
}
|
||||||
79
src/components/xt-chat/xt-bubble/hooks/useListData.ts
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
/*
|
||||||
|
* @Author: RenXiaoDong
|
||||||
|
* @Date: 2025-08-20 23:14:55
|
||||||
|
* @Description: 气泡列表数据处理Hook
|
||||||
|
* 用于处理气泡数据列表,支持角色配置和属性合并
|
||||||
|
*/
|
||||||
|
import { computed, type Ref } from 'vue';
|
||||||
|
import type { BubbleDataType, BubbleListProps } from '../types';
|
||||||
|
import type { BubbleProps } from '../types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 类型工具:从Ref类型中提取原始类型
|
||||||
|
* @template T - Ref类型
|
||||||
|
* @returns 原始类型
|
||||||
|
*/
|
||||||
|
export type UnRef<T extends Ref<any>> = T extends Ref<infer R> ? R : never;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 列表项类型定义
|
||||||
|
* 从useListData返回值中提取单个列表项的类型
|
||||||
|
*/
|
||||||
|
export type ListItemType = UnRef<ReturnType<typeof useListData>>[number];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 气泡列表数据处理Hook
|
||||||
|
* 处理原始气泡数据,应用角色配置,生成最终的气泡属性
|
||||||
|
*
|
||||||
|
* @param items - 原始气泡数据列表
|
||||||
|
* @param roles - 角色配置,可以是对象或函数
|
||||||
|
* @returns 处理后的气泡数据列表
|
||||||
|
*/
|
||||||
|
export default function useListData(
|
||||||
|
items: Ref<BubbleListProps['items']>,
|
||||||
|
roles?: Ref<BubbleListProps['roles']>,
|
||||||
|
) {
|
||||||
|
/**
|
||||||
|
* 获取角色对应的气泡属性配置
|
||||||
|
* 根据气泡的角色类型,返回对应的样式和属性配置
|
||||||
|
*
|
||||||
|
* @param bubble - 气泡数据
|
||||||
|
* @param index - 气泡在列表中的索引
|
||||||
|
* @returns 角色对应的气泡属性
|
||||||
|
*/
|
||||||
|
const getRoleBubbleProps = (bubble: BubbleDataType, index: number): Partial<BubbleProps> => {
|
||||||
|
// 如果roles是函数,调用函数获取配置
|
||||||
|
if (typeof roles.value === 'function') {
|
||||||
|
return roles.value(bubble, index);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果roles是对象,根据bubble.role获取对应配置
|
||||||
|
if (roles) {
|
||||||
|
return roles.value?.[bubble.role!] || {};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果没有角色配置,返回空对象
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算处理后的列表数据
|
||||||
|
* 合并角色配置和原始数据,生成最终的气泡属性
|
||||||
|
*/
|
||||||
|
const listData = computed(() =>
|
||||||
|
(items.value || []).map((bubbleData, i) => {
|
||||||
|
// 生成唯一key:优先使用传入的 key,其次使用 id,最后回退到预设格式
|
||||||
|
const mergedKey = (bubbleData as any).key ?? (bubbleData as any).id ?? `preset_${i}`;
|
||||||
|
|
||||||
|
return {
|
||||||
|
// 先应用角色配置(作为默认值)
|
||||||
|
...getRoleBubbleProps(bubbleData, i),
|
||||||
|
// 再应用原始数据(会覆盖角色配置中的相同属性)
|
||||||
|
...bubbleData,
|
||||||
|
// 最后设置key(确保唯一性)
|
||||||
|
key: mergedKey,
|
||||||
|
};
|
||||||
|
}));
|
||||||
|
|
||||||
|
return listData as Ref<any[]>;
|
||||||
|
}
|
||||||
147
src/components/xt-chat/xt-bubble/hooks/useTypedEffect.ts
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
import useState from '@/hooks/useState';
|
||||||
|
import { computed, onUnmounted, unref, watch } from 'vue';
|
||||||
|
import type { Ref } from 'vue';
|
||||||
|
import type { BubbleContentType } from '../types';
|
||||||
|
|
||||||
|
function isString(content: BubbleContentType): content is string {
|
||||||
|
return typeof content === 'string';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 类型守卫:判断是否为非字符串类型(用于快速排除)
|
||||||
|
function isNonString(content: BubbleContentType): content is Exclude<BubbleContentType, string> {
|
||||||
|
return !isString(content);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 打字效果Hook
|
||||||
|
* 当启用打字效果时,返回渐进式显示的内容和打字状态
|
||||||
|
* 否则直接返回原始内容
|
||||||
|
*
|
||||||
|
* @param content - 原始内容
|
||||||
|
* @param typingEnabled - 是否启用打字效果
|
||||||
|
* @param typingStep - 每次显示的字符数
|
||||||
|
* @param typingInterval - 打字间隔时间(毫秒)
|
||||||
|
* @returns [typedContent: 当前显示的内容, isTyping: 是否正在打字]
|
||||||
|
*/
|
||||||
|
const useTypedEffect = (
|
||||||
|
content: Ref<BubbleContentType>,
|
||||||
|
typingEnabled: Ref<boolean>,
|
||||||
|
typingStep: Ref<number>,
|
||||||
|
typingInterval: Ref<number>,
|
||||||
|
abortRef?: Ref<boolean>,
|
||||||
|
): [typedContent: Ref<BubbleContentType>, isTyping: Ref<boolean>] => {
|
||||||
|
// 记录上一次的内容,用于检测内容变化
|
||||||
|
const [prevContent, setPrevContent] = useState<BubbleContentType>('');
|
||||||
|
// 当前打字位置(已显示的字符数)
|
||||||
|
const [typingIndex, setTypingIndex] = useState<number>(1);
|
||||||
|
let timer: number | null = null;
|
||||||
|
|
||||||
|
// 合并启用状态:仅当 启用打字 且 内容是字符串 时生效
|
||||||
|
const mergedTypingEnabled = computed(() => typingEnabled.value && isString(content.value));
|
||||||
|
|
||||||
|
// 清理定时器
|
||||||
|
const clearTypingTimer = () => {
|
||||||
|
if (timer) {
|
||||||
|
clearTimeout(timer);
|
||||||
|
timer = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 监听内容变化,重置打字状态
|
||||||
|
watch(
|
||||||
|
content,
|
||||||
|
(newVal) => {
|
||||||
|
const prevVal = unref(prevContent);
|
||||||
|
setPrevContent(newVal);
|
||||||
|
|
||||||
|
// 非字符串类型:直接返回原始值,不执行打字逻辑
|
||||||
|
if (isNonString(newVal)) {
|
||||||
|
setTypingIndex(0); // 重置索引(无意义,仅为状态一致)
|
||||||
|
clearTypingTimer();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 字符串类型的逻辑
|
||||||
|
if (!mergedTypingEnabled.value) {
|
||||||
|
if (!(abortRef && abortRef.value)) {
|
||||||
|
setTypingIndex(newVal.length); // 直接显示完整内容
|
||||||
|
}
|
||||||
|
} else if (
|
||||||
|
isString(prevVal) &&
|
||||||
|
newVal.indexOf(prevVal) !== 0 // 新内容不是旧内容的前缀,重置打字
|
||||||
|
) {
|
||||||
|
setTypingIndex(1);
|
||||||
|
}
|
||||||
|
clearTypingTimer();
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
);
|
||||||
|
|
||||||
|
// 打字逻辑(仅处理字符串类型)
|
||||||
|
const startTyping = () => {
|
||||||
|
clearTypingTimer();
|
||||||
|
|
||||||
|
// 终止条件:未启用打字 / 内容不是字符串 / 已中止
|
||||||
|
if (!mergedTypingEnabled.value || isNonString(content.value) || (abortRef && abortRef.value)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentContent = content.value; // 此时 TypeScript 可推断为 string(因为 mergedTypingEnabled 已过滤)
|
||||||
|
const currentIndex = unref(typingIndex);
|
||||||
|
|
||||||
|
// 已打完所有字符,停止
|
||||||
|
if (currentIndex >= currentContent.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算下一步索引(避免超出长度)
|
||||||
|
const nextIndex = Math.min(currentIndex + typingStep.value, currentContent.length);
|
||||||
|
|
||||||
|
timer = window.setTimeout(() => {
|
||||||
|
setTypingIndex(nextIndex);
|
||||||
|
startTyping(); // 递归触发下一次打字
|
||||||
|
}, typingInterval.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 监听打字相关依赖变化
|
||||||
|
watch(
|
||||||
|
[typingEnabled, abortRef, () => content.value], // 监听 content 变化
|
||||||
|
() => {
|
||||||
|
startTyping();
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
);
|
||||||
|
|
||||||
|
// 计算当前显示的内容(核心:仅对字符串调用 slice)
|
||||||
|
const mergedTypingContent = computed(() => {
|
||||||
|
const currentContent = content.value;
|
||||||
|
|
||||||
|
// 非字符串类型:直接返回原始值(不处理打字)
|
||||||
|
if (isNonString(currentContent)) {
|
||||||
|
return currentContent;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 字符串类型:根据打字状态返回部分/完整内容
|
||||||
|
if (mergedTypingEnabled.value || (abortRef && abortRef.value)) {
|
||||||
|
return currentContent.slice(0, unref(typingIndex)); // 此时确定是 string,可安全调用 slice
|
||||||
|
}
|
||||||
|
|
||||||
|
return currentContent; // 不启用打字时,返回完整字符串
|
||||||
|
});
|
||||||
|
|
||||||
|
// 是否正在打字(仅字符串类型可能为 true)
|
||||||
|
const isTyping = computed(
|
||||||
|
() =>
|
||||||
|
mergedTypingEnabled.value &&
|
||||||
|
unref(typingIndex) < (content.value as string).length && // 此时 content.value 已被 mergedTypingEnabled 限定为 string
|
||||||
|
!(abortRef && abortRef.value),
|
||||||
|
);
|
||||||
|
|
||||||
|
// 组件卸载时清理定时器
|
||||||
|
onUnmounted(() => {
|
||||||
|
clearTypingTimer();
|
||||||
|
});
|
||||||
|
|
||||||
|
return [mergedTypingContent, isTyping];
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useTypedEffect;
|
||||||
61
src/components/xt-chat/xt-bubble/hooks/useTypingConfig.ts
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
/*
|
||||||
|
* @Author: RenXiaoDong
|
||||||
|
* @Date: 2025-08-20 23:27:05
|
||||||
|
* @Description: 打字配置Hook
|
||||||
|
* 处理打字效果的配置参数,提供默认值和配置合并功能
|
||||||
|
*/
|
||||||
|
import { computed, toValue, type MaybeRefOrGetter } from 'vue';
|
||||||
|
import type { BubbleProps, TypingOption } from '../types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 打字配置Hook
|
||||||
|
* 解析打字配置参数,提供默认值,返回配置化的打字参数
|
||||||
|
*
|
||||||
|
* @param typing - 打字配置,可以是布尔值、配置对象或null
|
||||||
|
* @returns [typingEnabled, step, interval, suffix] - 打字启用状态、步长、间隔、后缀
|
||||||
|
*/
|
||||||
|
function useTypingConfig(typing: MaybeRefOrGetter<BubbleProps['typing']>) {
|
||||||
|
/**
|
||||||
|
* 计算是否启用打字效果
|
||||||
|
* 只有当typing为真值时才启用打字效果
|
||||||
|
*/
|
||||||
|
const typingEnabled = computed(() => {
|
||||||
|
if (!toValue(typing)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 基础配置:提供默认的打字参数
|
||||||
|
* 当用户没有提供完整配置时,使用这些默认值
|
||||||
|
*/
|
||||||
|
const baseConfig: Required<TypingOption> = {
|
||||||
|
step: 1, // 每次显示的字符数
|
||||||
|
interval: 50, // 打字间隔时间(毫秒)
|
||||||
|
suffix: null, // 打字时的后缀(默认为空)
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 合并配置:将用户配置与默认配置合并
|
||||||
|
* 用户配置会覆盖默认配置中的相同属性
|
||||||
|
*/
|
||||||
|
const config = computed(() => {
|
||||||
|
const typingRaw = toValue(typing);
|
||||||
|
return {
|
||||||
|
...baseConfig,
|
||||||
|
// 如果typing是对象,则合并对象属性;否则使用默认配置
|
||||||
|
...(typeof typingRaw === 'object' ? typingRaw : {})
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 返回解构的配置参数,方便使用
|
||||||
|
return [
|
||||||
|
typingEnabled, // 是否启用打字效果
|
||||||
|
computed(() => config.value.step), // 每次显示的字符数
|
||||||
|
computed(() => config.value.interval), // 打字间隔时间
|
||||||
|
computed(() => config.value.suffix) // 打字后缀
|
||||||
|
] as const;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default useTypingConfig;
|
||||||
9
src/components/xt-chat/xt-bubble/index.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
/*
|
||||||
|
* @Author: RenXiaoDong
|
||||||
|
* @Date: 2025-08-20 22:16:14
|
||||||
|
*/
|
||||||
|
export { default as Bubble } from './xt-bubble.vue';
|
||||||
|
export { default as BubbleList } from './xt-bubbleList.vue';
|
||||||
|
export * from './types';
|
||||||
|
|
||||||
|
|
||||||
66
src/components/xt-chat/xt-bubble/loading.vue
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
<!--
|
||||||
|
* @Author: RenXiaoDong
|
||||||
|
* @Date: 2025-08-20 23:20:21
|
||||||
|
-->
|
||||||
|
<script lang="tsx">
|
||||||
|
import { defineComponent } from 'vue';
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
setup(props, { emit, expose }) {
|
||||||
|
return () => (
|
||||||
|
<span class="xt-bubble-dot">
|
||||||
|
<i class="dot-item w-2px h-2px" key="item-1" />
|
||||||
|
<i class="dot-item w-3px h-3px" key="item-2" />
|
||||||
|
<i class="dot-item w-5px h-5px" key="item-3" />
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.xt-bubble-dot {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
column-gap: 4px;
|
||||||
|
|
||||||
|
@keyframes loadingMove {
|
||||||
|
0% {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
10% {
|
||||||
|
transform: translateY(4px);
|
||||||
|
}
|
||||||
|
20% {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
30% {
|
||||||
|
transform: translateY(4px);
|
||||||
|
}
|
||||||
|
40% {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dot-item {
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--Brand-6, #6d4cfe);
|
||||||
|
animation-name: loadingMove;
|
||||||
|
animation-duration: 2s;
|
||||||
|
animation-iteration-count: infinite;
|
||||||
|
animation-timing-function: linear;
|
||||||
|
|
||||||
|
&:nth-child(1) {
|
||||||
|
animation-delay: 0s;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:nth-child(2) {
|
||||||
|
animation-delay: 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:nth-child(3) {
|
||||||
|
animation-delay: 0.4s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
160
src/components/xt-chat/xt-bubble/style.scss
Normal file
@ -0,0 +1,160 @@
|
|||||||
|
.xt-bubble-list {
|
||||||
|
height: 100%;
|
||||||
|
gap: 8px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.xt-bubble {
|
||||||
|
display: flex;
|
||||||
|
justify-content: start;
|
||||||
|
column-gap: 12px;
|
||||||
|
@mixin cts {
|
||||||
|
color: var(--Text-1, #211f24);
|
||||||
|
font-family: $font-family-regular;
|
||||||
|
font-size: 14px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 22px;
|
||||||
|
}
|
||||||
|
&.xt-bubble-start {
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.xt-bubble-end {
|
||||||
|
justify-content: end;
|
||||||
|
flex-direction: row-reverse;
|
||||||
|
}
|
||||||
|
|
||||||
|
.xt-bubble-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
min-width: 0;
|
||||||
|
max-width: 100%;
|
||||||
|
// min-height: 36px;
|
||||||
|
word-break: break-all;
|
||||||
|
@include cts;
|
||||||
|
&-filled {
|
||||||
|
background-color: #f2f3f5;
|
||||||
|
padding: 6px 12px;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-outlined {
|
||||||
|
background-color: #fff;
|
||||||
|
border: 1px solid #e5e6eb;
|
||||||
|
padding: 6px 12px;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-shadow {
|
||||||
|
background-color: #fff;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||||
|
padding: 6px 12px;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-borderless {
|
||||||
|
background-color: transparent !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 形状样式
|
||||||
|
&-default {
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-round {
|
||||||
|
padding: 6px 12px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
border-radius: 16px;
|
||||||
|
background-color: var(--BG-200, #f2f3f5);
|
||||||
|
}
|
||||||
|
|
||||||
|
&-corner {
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thought-chain-item {
|
||||||
|
position: relative;
|
||||||
|
margin: 0;
|
||||||
|
font-size: 12px;
|
||||||
|
list-style: none;
|
||||||
|
.thought-chain-output {
|
||||||
|
position: relative;
|
||||||
|
&.hasLine {
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border-left: 1px solid #e6e6e8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置markdown返回的标签样式
|
||||||
|
:deep(.markdown-wrap) {
|
||||||
|
pre {
|
||||||
|
white-space: normal;
|
||||||
|
}
|
||||||
|
table {
|
||||||
|
border-collapse: collapse;
|
||||||
|
thead {
|
||||||
|
tr {
|
||||||
|
th {
|
||||||
|
@include cts;
|
||||||
|
padding: 6px 8px;
|
||||||
|
border: 1px solid #e6e6e8;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tbody {
|
||||||
|
tr {
|
||||||
|
td {
|
||||||
|
@include cts;
|
||||||
|
border: 1px solid #e6e6e8;
|
||||||
|
padding: 16px 8px;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-box {
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 2px;
|
||||||
|
border-radius: 2px;
|
||||||
|
transition: all 0.2s;
|
||||||
|
line-height: normal;
|
||||||
|
&:hover {
|
||||||
|
background-color: #f2f3f5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.xt-bubble-header {
|
||||||
|
@include cts;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.xt-bubble-footer {
|
||||||
|
@include cts;
|
||||||
|
margin-top: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.xt-bubble-typing {
|
||||||
|
display: inline-block;
|
||||||
|
min-width: 36px;
|
||||||
|
}
|
||||||
|
}
|
||||||
191
src/components/xt-chat/xt-bubble/types.ts
Normal file
@ -0,0 +1,191 @@
|
|||||||
|
/*
|
||||||
|
* @Author: RenXiaoDong
|
||||||
|
* @Date: 2025-08-20 22:04:54
|
||||||
|
* @Description: 聊天气泡组件的类型定义
|
||||||
|
*/
|
||||||
|
import type { AvatarProps } from 'ant-design-vue';
|
||||||
|
import type { CSSProperties, HTMLAttributes, VNode } from 'vue';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 避免类型验证的包装类型
|
||||||
|
* 用于包装可能引起TypeScript严格检查问题的类型
|
||||||
|
*/
|
||||||
|
export type AvoidValidation<T> = T;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 打字效果配置选项
|
||||||
|
* 控制消息的打字动画效果
|
||||||
|
*/
|
||||||
|
export interface TypingOption {
|
||||||
|
/**
|
||||||
|
* 每次打字的字符数
|
||||||
|
* @default 1
|
||||||
|
*/
|
||||||
|
step?: number;
|
||||||
|
/**
|
||||||
|
* 打字间隔时间(毫秒)
|
||||||
|
* @default 50
|
||||||
|
*/
|
||||||
|
interval?: number;
|
||||||
|
/**
|
||||||
|
* 打字时的后缀显示内容
|
||||||
|
* @default null
|
||||||
|
*/
|
||||||
|
suffix?: VNode | string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 语义化类型
|
||||||
|
* 定义气泡组件中各个部分的语义化标识
|
||||||
|
*/
|
||||||
|
export type SemanticType = 'avatar' | 'content' | 'header' | 'footer';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 气泡内容类型
|
||||||
|
* 支持多种内容格式:VNode、字符串、对象、数字等
|
||||||
|
*/
|
||||||
|
export type BubbleContentType = VNode | string | Record<PropertyKey, any> | number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 插槽信息类型
|
||||||
|
* 传递给插槽函数的额外信息
|
||||||
|
*/
|
||||||
|
export type SlotInfoType = {
|
||||||
|
/** 气泡的唯一标识键 */
|
||||||
|
key?: string | number;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 头像属性扩展接口
|
||||||
|
* 继承自ant-design-vue的AvatarProps,添加了class和style属性
|
||||||
|
*/
|
||||||
|
export interface _AvatarProps extends AvatarProps {
|
||||||
|
/** 自定义CSS类名 */
|
||||||
|
class: string;
|
||||||
|
/** 自定义内联样式 */
|
||||||
|
style: CSSProperties;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 气泡组件属性接口
|
||||||
|
* 定义气泡组件的所有可配置属性
|
||||||
|
*/
|
||||||
|
export interface BubbleProps<ContentType extends BubbleContentType = string>
|
||||||
|
extends /* @vue-ignore */ Omit<HTMLAttributes, 'content'> {
|
||||||
|
/** 组件前缀类名 */
|
||||||
|
prefixCls?: string;
|
||||||
|
/** 根元素的自定义类名 */
|
||||||
|
rootClassName?: string;
|
||||||
|
/** 各部分的样式配置 */
|
||||||
|
styles?: Partial<Record<SemanticType, CSSProperties>>;
|
||||||
|
/** 各部分的类名配置 */
|
||||||
|
classNames?: Partial<Record<SemanticType, string>>;
|
||||||
|
/** 头像配置:可以是属性对象、VNode或渲染函数 */
|
||||||
|
avatar?: Partial<_AvatarProps> | VNode | (() => VNode);
|
||||||
|
/** 气泡位置:start(左侧) | end(右侧) */
|
||||||
|
placement?: 'start' | 'end';
|
||||||
|
/** 是否显示加载状态 */
|
||||||
|
loading?: boolean;
|
||||||
|
/** 打字效果配置:可以是配置对象或布尔值 */
|
||||||
|
typing?: AvoidValidation<TypingOption | boolean>;
|
||||||
|
/** 气泡内容 */
|
||||||
|
content?: ContentType;
|
||||||
|
/** 自定义消息渲染函数 */
|
||||||
|
messageRender?: (content: ContentType) => VNode | string;
|
||||||
|
/** 自定义加载状态渲染函数 */
|
||||||
|
loadingRender?: () => VNode;
|
||||||
|
/** 气泡样式变体:filled(填充) | borderless(无边框) | outlined(轮廓) | shadow(阴影) */
|
||||||
|
variant?: 'filled' | 'borderless' | 'outlined' | 'shadow';
|
||||||
|
/** 气泡形状:round(圆角) | corner(直角) */
|
||||||
|
shape?: 'round' | 'corner';
|
||||||
|
/** 内部使用的唯一标识键 */
|
||||||
|
_key?: number | string;
|
||||||
|
/** 打字完成时的回调函数 */
|
||||||
|
onTypingComplete?: VoidFunction;
|
||||||
|
/** 头部内容:可以是VNode、字符串或渲染函数 */
|
||||||
|
header?: AvoidValidation<
|
||||||
|
VNode | string | ((params: { content: ContentType; info: SlotInfoType; item: BubbleProps<any> }) => VNode | string)
|
||||||
|
>;
|
||||||
|
/** 底部内容:可以是VNode、字符串或渲染函数 */
|
||||||
|
footer?: AvoidValidation<
|
||||||
|
VNode | string | ((params: { content: ContentType; info: SlotInfoType; item: BubbleProps<any> }) => VNode | string)
|
||||||
|
>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 气泡组件引用接口
|
||||||
|
* 提供对气泡组件DOM元素的访问
|
||||||
|
*/
|
||||||
|
export interface BubbleRef {
|
||||||
|
/** 气泡组件的原生DOM元素 */
|
||||||
|
bubbleElement: HTMLElement;
|
||||||
|
/** 中止当前打字效果并立即展示完整内容 */
|
||||||
|
abortTyping: VoidFunction;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 气泡上下文属性接口
|
||||||
|
* 用于气泡组件间的通信
|
||||||
|
*/
|
||||||
|
export interface BubbleContextProps {
|
||||||
|
/** 更新回调函数 */
|
||||||
|
onUpdate?: VoidFunction;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 气泡列表引用接口
|
||||||
|
* 提供对气泡列表组件的访问和控制方法
|
||||||
|
*/
|
||||||
|
export interface BubbleListRef {
|
||||||
|
/** 气泡列表的原生DOM元素 */
|
||||||
|
bubbleElement: HTMLDivElement;
|
||||||
|
/** 滚动到指定位置的方法 */
|
||||||
|
scrollTo: (info: {
|
||||||
|
/** 滚动偏移量 */
|
||||||
|
offset?: number;
|
||||||
|
/** 目标气泡的键值 */
|
||||||
|
key?: string | number;
|
||||||
|
/** 滚动行为:smooth(平滑) | auto(自动) */
|
||||||
|
behavior?: ScrollBehavior;
|
||||||
|
/** 滚动位置:start | center | end | nearest */
|
||||||
|
block?: ScrollLogicalPosition;
|
||||||
|
}) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 气泡数据类型
|
||||||
|
* 扩展了BubbleProps,添加了key和role属性
|
||||||
|
*/
|
||||||
|
export type BubbleDataType = BubbleProps<any> & {
|
||||||
|
/** 气泡的唯一标识键 */
|
||||||
|
key?: string | number;
|
||||||
|
/** 气泡的角色类型 */
|
||||||
|
role?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 角色类型
|
||||||
|
* 定义不同角色的气泡样式配置
|
||||||
|
*/
|
||||||
|
export type RoleType = Partial<Omit<BubbleProps<any>, 'content'>>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 角色配置类型
|
||||||
|
* 可以是角色配置对象或根据气泡数据和索引返回角色配置的函数
|
||||||
|
*/
|
||||||
|
export type RolesType = Record<string, RoleType> | ((bubbleDataP: BubbleDataType, index: number) => RoleType);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 气泡列表属性接口
|
||||||
|
* 定义气泡列表组件的所有可配置属性
|
||||||
|
*/
|
||||||
|
export interface BubbleListProps extends /* @vue-ignore */ HTMLAttributes {
|
||||||
|
/** 根元素的自定义类名 */
|
||||||
|
rootClassName?: string;
|
||||||
|
/** 气泡数据数组 */
|
||||||
|
items?: BubbleDataType[];
|
||||||
|
/** 是否自动滚动到最新消息 */
|
||||||
|
autoScroll?: boolean;
|
||||||
|
/** 角色配置:定义不同角色的气泡样式 */
|
||||||
|
roles?: AvoidValidation<RolesType>;
|
||||||
|
}
|
||||||
148
src/components/xt-chat/xt-bubble/xt-bubble.vue
Normal file
@ -0,0 +1,148 @@
|
|||||||
|
<script lang="tsx">
|
||||||
|
import { Avatar } from 'ant-design-vue';
|
||||||
|
import { computed, defineComponent, ref, toRef, unref, watch, watchEffect } from 'vue';
|
||||||
|
import Loading from './loading.vue';
|
||||||
|
import useTypingConfig from './hooks/useTypingConfig';
|
||||||
|
import useTypedEffect from './hooks/useTypedEffect';
|
||||||
|
import { useBubbleContextInject } from './context';
|
||||||
|
import type { BubbleProps, BubbleContentType, SlotInfoType } from './types';
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'Bubble',
|
||||||
|
inheritAttrs: false,
|
||||||
|
props: {},
|
||||||
|
setup(_, { attrs, slots, expose }) {
|
||||||
|
const props = attrs as unknown as BubbleProps<BubbleContentType> & { style?: any; class?: any };
|
||||||
|
|
||||||
|
const content = ref<BubbleContentType>(props.content ?? '');
|
||||||
|
const bubbleElement = ref<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.content,
|
||||||
|
(val) => {
|
||||||
|
content.value = val;
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
);
|
||||||
|
|
||||||
|
const { onUpdate } = unref(useBubbleContextInject());
|
||||||
|
|
||||||
|
const [typingEnabled, typingStep, typingInterval, typingSuffix] = useTypingConfig(() => props.typing);
|
||||||
|
const abortRef = ref(false);
|
||||||
|
const [typedContent, isTyping] = useTypedEffect(content, typingEnabled, typingStep, typingInterval, abortRef);
|
||||||
|
|
||||||
|
// 提供中止打字的能力:关闭typing并立即展示完整内容
|
||||||
|
const abortTyping = () => {
|
||||||
|
abortRef.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const triggerTypingCompleteRef = ref(false);
|
||||||
|
watch(typedContent, () => {
|
||||||
|
onUpdate?.();
|
||||||
|
});
|
||||||
|
watchEffect(() => {
|
||||||
|
if (!isTyping.value && !props.loading) {
|
||||||
|
if (!triggerTypingCompleteRef.value) {
|
||||||
|
triggerTypingCompleteRef.value = true;
|
||||||
|
props.onTypingComplete?.();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
triggerTypingCompleteRef.value = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const prefixCls = 'xt-bubble';
|
||||||
|
const mergedCls = computed(() => [
|
||||||
|
prefixCls,
|
||||||
|
`${prefixCls}-${props.placement ?? 'start'}`,
|
||||||
|
props.class,
|
||||||
|
{
|
||||||
|
[`${prefixCls}-typing`]:
|
||||||
|
isTyping.value && !props.loading && !props.messageRender && !slots.message && !typingSuffix.value,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const avatarNode = computed(() => {
|
||||||
|
if (slots.avatar) return slots.avatar();
|
||||||
|
const avatar = props.avatar as any;
|
||||||
|
return typeof avatar === 'function' ? avatar() : avatar && avatar.src ? <Avatar {...avatar} /> : null;
|
||||||
|
});
|
||||||
|
|
||||||
|
const mergedContent = computed(() => {
|
||||||
|
if (slots.message) return slots.message({ content: typedContent.value });
|
||||||
|
return props.messageRender ? props.messageRender(typedContent.value) : typedContent.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
const contentNode = computed(() => {
|
||||||
|
if (props.loading) {
|
||||||
|
if (slots.loading) {
|
||||||
|
return slots.loading();
|
||||||
|
}
|
||||||
|
return props.loadingRender ? props.loadingRender() : <Loading />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{mergedContent.value}
|
||||||
|
{isTyping.value && unref(typingSuffix)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const renderHeader = () => {
|
||||||
|
const info: SlotInfoType = { key: props._key };
|
||||||
|
if (slots.header) return slots.header({ content: typedContent.value, info, item: props });
|
||||||
|
const h = props.header;
|
||||||
|
return typeof h === 'function' ? h({ content: typedContent.value, info, item: props }) : h;
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderFooter = () => {
|
||||||
|
const info: SlotInfoType = { key: props._key };
|
||||||
|
if (slots.footer) return slots.footer({ content: typedContent.value, info, item: props });
|
||||||
|
const f = props.footer;
|
||||||
|
return typeof f === 'function' ? f({ content: typedContent.value, info, item: props }) : f;
|
||||||
|
};
|
||||||
|
|
||||||
|
expose({
|
||||||
|
abortTyping,
|
||||||
|
bubbleElement,
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => (
|
||||||
|
<div class={mergedCls.value} style={{ ...(props.style || {}) }} ref={bubbleElement}>
|
||||||
|
{(slots.avatar || props.avatar) && (
|
||||||
|
<div class={[`${prefixCls}-avatar`, props.classNames?.avatar]} style={props.styles?.avatar}>
|
||||||
|
{avatarNode.value}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div
|
||||||
|
class={[
|
||||||
|
`${prefixCls}-content`,
|
||||||
|
`${prefixCls}-content-${props.variant ?? 'filled'}`,
|
||||||
|
{ [`${prefixCls}-content-${props.shape}`]: props.shape },
|
||||||
|
props.classNames?.content,
|
||||||
|
]}
|
||||||
|
style={props.styles?.content}
|
||||||
|
>
|
||||||
|
{renderHeader() ? (
|
||||||
|
<div class={[`${prefixCls}-header`, props.classNames?.header]} style={props.styles?.header}>
|
||||||
|
{renderHeader()}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
{contentNode.value}
|
||||||
|
{renderFooter() ? (
|
||||||
|
<div class={[`${prefixCls}-footer`, props.classNames?.footer]} style={props.styles?.footer}>
|
||||||
|
{renderFooter()}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
@import './style.scss';
|
||||||
|
</style>
|
||||||
204
src/components/xt-chat/xt-bubble/xt-bubbleList.vue
Normal file
@ -0,0 +1,204 @@
|
|||||||
|
<script lang="tsx">
|
||||||
|
import Bubble from './xt-bubble.vue';
|
||||||
|
import BubbleContextProvider from './context';
|
||||||
|
import useDisplayData from './hooks/useDisplayData';
|
||||||
|
import useListData from './hooks/useListData';
|
||||||
|
import useState from '@/hooks/useState';
|
||||||
|
import pickAttrs from '@/utils/pick-attrs';
|
||||||
|
import { useEventCallback } from '@/hooks/useEventCallback';
|
||||||
|
import type { BubbleListProps, BubbleRef, RolesType } from './types';
|
||||||
|
import {
|
||||||
|
computed,
|
||||||
|
defineComponent,
|
||||||
|
mergeProps,
|
||||||
|
nextTick,
|
||||||
|
onWatcherCleanup,
|
||||||
|
ref,
|
||||||
|
unref,
|
||||||
|
useAttrs,
|
||||||
|
watch,
|
||||||
|
watchPostEffect,
|
||||||
|
} from 'vue';
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'BubbleList',
|
||||||
|
inheritAttrs: false,
|
||||||
|
// 正确声明 props,提供默认值,确保 TSX 下默认值生效
|
||||||
|
props: {
|
||||||
|
autoScroll: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
items: {
|
||||||
|
type: Array as () => BubbleListProps['items'],
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
roles: {
|
||||||
|
type: Object as () => RolesType,
|
||||||
|
default: () => ({} as RolesType),
|
||||||
|
},
|
||||||
|
rootClassName: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
style: {
|
||||||
|
type: Object as () => Record<string, any>,
|
||||||
|
default: () => ({}),
|
||||||
|
},
|
||||||
|
class: {
|
||||||
|
type: [String, Array, Object] as unknown as () => any,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
setup(props, { attrs, slots, expose }) {
|
||||||
|
const passThroughAttrs = useAttrs();
|
||||||
|
|
||||||
|
const TOLERANCE = 1;
|
||||||
|
|
||||||
|
const domProps = computed(() => pickAttrs(mergeProps(props as any, passThroughAttrs)));
|
||||||
|
|
||||||
|
const items = ref(props.items);
|
||||||
|
const roles = ref(props.roles as RolesType);
|
||||||
|
watch(
|
||||||
|
() => props.items,
|
||||||
|
(val) => (items.value = val),
|
||||||
|
{ immediate: true },
|
||||||
|
);
|
||||||
|
watch(
|
||||||
|
() => props.roles,
|
||||||
|
(val) => (roles.value = val as RolesType),
|
||||||
|
{ immediate: true },
|
||||||
|
);
|
||||||
|
|
||||||
|
const listRef = ref<HTMLDivElement | null>(null);
|
||||||
|
const bubbleRefs = ref<Record<string | number, BubbleRef>>({});
|
||||||
|
|
||||||
|
const listPrefixCls = 'xt-bubble-list';
|
||||||
|
|
||||||
|
const [initialized, setInitialized] = useState(false);
|
||||||
|
watchPostEffect(() => {
|
||||||
|
setInitialized(true);
|
||||||
|
onWatcherCleanup(() => setInitialized(false));
|
||||||
|
});
|
||||||
|
|
||||||
|
// data
|
||||||
|
const mergedData = useListData(items as any, roles as any);
|
||||||
|
const [displayData, onTypingComplete] = useDisplayData(mergedData);
|
||||||
|
|
||||||
|
// scroll
|
||||||
|
const [scrollReachEnd, setScrollReachEnd] = useState(true);
|
||||||
|
const [updateCount, setUpdateCount] = useState(0);
|
||||||
|
// 首次挂载后仅自动滚动一次
|
||||||
|
const didInitialAutoScroll = ref(false);
|
||||||
|
|
||||||
|
const onInternalScroll = (e: Event) => {
|
||||||
|
const target = e.target as HTMLElement;
|
||||||
|
setScrollReachEnd(target.scrollHeight - Math.abs(target.scrollTop) - target.clientHeight <= TOLERANCE);
|
||||||
|
};
|
||||||
|
|
||||||
|
watch([updateCount, scrollReachEnd, listRef], () => {
|
||||||
|
if (props.autoScroll && unref(listRef) && unref(scrollReachEnd)) {
|
||||||
|
nextTick(() => {
|
||||||
|
console.log('自然滚动')
|
||||||
|
unref(listRef)!.scrollTo({ top: unref(listRef)!.scrollHeight });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => unref(displayData).length,
|
||||||
|
(newLen, oldLen) => {
|
||||||
|
if (!props.autoScroll) return;
|
||||||
|
// 首次渲染:当有内容时滚到底部一次
|
||||||
|
if (!didInitialAutoScroll.value && newLen > 0) {
|
||||||
|
console.log('首次渲染滚动到底部-----')
|
||||||
|
scrollToBottom('auto');
|
||||||
|
didInitialAutoScroll.value = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 新增内容且当前在底部:继续粘底
|
||||||
|
if (oldLen !== undefined && newLen > (oldLen ?? 0) && unref(scrollReachEnd)) {
|
||||||
|
scrollToBottom();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
);
|
||||||
|
|
||||||
|
const onBubbleUpdate = useEventCallback<void>(() => {
|
||||||
|
if (props.autoScroll) setUpdateCount(unref(updateCount) + 1);
|
||||||
|
});
|
||||||
|
const context = computed(() => ({ onUpdate: onBubbleUpdate }));
|
||||||
|
|
||||||
|
// 暴露控制方法
|
||||||
|
const abortTypingByKey = (key: string | number) => {
|
||||||
|
bubbleRefs.value[key]?.abortTyping?.();
|
||||||
|
};
|
||||||
|
// 通用:滚动到底部
|
||||||
|
const scrollToBottom = (behavior: ScrollBehavior = 'smooth') => {
|
||||||
|
nextTick(() => {
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
const el = unref(listRef);
|
||||||
|
if (el) {
|
||||||
|
el.scrollTo({ top: el.scrollHeight, behavior });
|
||||||
|
setScrollReachEnd(true);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
};
|
||||||
|
// 对外暴露能力
|
||||||
|
expose({
|
||||||
|
nativeElement: listRef,
|
||||||
|
abortTypingByKey,
|
||||||
|
scrollTo: (info: any) => {
|
||||||
|
unref(listRef)?.scrollTo?.(info);
|
||||||
|
},
|
||||||
|
scrollToBottom,
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => (
|
||||||
|
<BubbleContextProvider value={context.value}>
|
||||||
|
<div
|
||||||
|
{...domProps.value}
|
||||||
|
class={[
|
||||||
|
listPrefixCls,
|
||||||
|
props.rootClassName,
|
||||||
|
props.class,
|
||||||
|
{ [`${listPrefixCls}-reach-end`]: unref(scrollReachEnd) },
|
||||||
|
]}
|
||||||
|
style={props.style}
|
||||||
|
ref={listRef}
|
||||||
|
onScroll={onInternalScroll}
|
||||||
|
>
|
||||||
|
{unref(displayData).map(({ key, onTypingComplete: onTypingCompleteBubble, ...bubble }) => (
|
||||||
|
<Bubble
|
||||||
|
{...bubble}
|
||||||
|
avatar={slots.avatar ? () => slots.avatar?.({ item: { key, ...bubble } }) : bubble.avatar}
|
||||||
|
header={slots.header?.({ item: { key, ...bubble } }) ?? bubble.header}
|
||||||
|
footer={slots.footer?.({ item: { key, ...bubble } }) ?? bubble.footer}
|
||||||
|
loadingRender={slots.loading ? () => slots.loading({ item: { key, ...bubble } }) : bubble.loadingRender}
|
||||||
|
content={slots.message?.({ item: { key, ...bubble } }) ?? bubble.content}
|
||||||
|
key={key}
|
||||||
|
ref={(node: any) => {
|
||||||
|
if (node) {
|
||||||
|
bubbleRefs.value[key] = node as BubbleRef;
|
||||||
|
} else {
|
||||||
|
delete bubbleRefs.value[key];
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
typing={unref(initialized) ? bubble.typing : false}
|
||||||
|
onTypingComplete={() => {
|
||||||
|
onTypingCompleteBubble?.();
|
||||||
|
onTypingComplete(key);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</BubbleContextProvider>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
@import './style.scss';
|
||||||
|
</style>
|
||||||
180
src/components/xt-chat/xt-conversations/index.vue
Normal file
@ -0,0 +1,180 @@
|
|||||||
|
<script lang="tsx">
|
||||||
|
import { ref, defineComponent, computed } from 'vue';
|
||||||
|
import { Input, Dropdown, Menu } from 'ant-design-vue';
|
||||||
|
import type { MenuProps } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import type { VNode } from 'vue';
|
||||||
|
import SvgIcon from '@/components/svg-icon/index.vue';
|
||||||
|
import TextoverTips from '@/components/text-over-tips/index.vue';
|
||||||
|
|
||||||
|
// 定义对话项类型
|
||||||
|
interface ConversationItem {
|
||||||
|
key: string;
|
||||||
|
label: string | VNode;
|
||||||
|
icon?: string | VNode;
|
||||||
|
disabled?: boolean;
|
||||||
|
maxlength?: number;
|
||||||
|
[key: string]: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DEFAULT_MENU_CONFIG = [
|
||||||
|
{
|
||||||
|
label: '置顶',
|
||||||
|
key: 'pin',
|
||||||
|
icon: () => <SvgIcon name="svg-pushpin" size={14} class="color-#737478 hover:color-#6D4CFE" />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '重命名',
|
||||||
|
key: 'rename',
|
||||||
|
icon: <icon-edit size={14} class="color-#737478" />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '删除',
|
||||||
|
key: 'delete',
|
||||||
|
icon: <icon-delete size={14} class="color-#F64B31" />,
|
||||||
|
status: 'danger',
|
||||||
|
},
|
||||||
|
] as ConversationItem[];
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'Conversations',
|
||||||
|
props: {
|
||||||
|
dataSource: {
|
||||||
|
type: Array as () => ConversationItem[],
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
activeKey: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
defaultActiveKey: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
menu: {
|
||||||
|
type: Array as () => ConversationItem[],
|
||||||
|
default: () => DEFAULT_MENU_CONFIG,
|
||||||
|
},
|
||||||
|
maxlength: {
|
||||||
|
type: Number,
|
||||||
|
default: 20,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
emits: ['activeChange', 'menuClick', 'update:dataSource', 'update:modelValue', 'rename'],
|
||||||
|
setup(props, { emit, expose }) {
|
||||||
|
const activeKey = ref(props.activeKey || props.defaultActiveKey || '');
|
||||||
|
const localDataSource = ref<ConversationItem[]>([]);
|
||||||
|
const inputRef = ref(null);
|
||||||
|
const menuConfigs = ref<ConversationItem[]>(props.menu ?? DEFAULT_MENU_CONFIG);
|
||||||
|
|
||||||
|
// 处理选中变更
|
||||||
|
const handleActiveChange = (item: ConversationItem) => {
|
||||||
|
const { value } = item;
|
||||||
|
activeKey.value = value;
|
||||||
|
emit('update:modelValue', value);
|
||||||
|
emit('activeChange', item);
|
||||||
|
};
|
||||||
|
const onMenuItemClick = ({ menuInfo, item }) => {
|
||||||
|
const { key } = menuInfo;
|
||||||
|
emit('menuClick', { menuInfo, item });
|
||||||
|
|
||||||
|
switch (key) {
|
||||||
|
case 'rename':
|
||||||
|
item.editing = true;
|
||||||
|
nextTick(() => {
|
||||||
|
inputRef.value.focus();
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const changeItems = () => {
|
||||||
|
emit('update:dataSource', localDataSource.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.dataSource,
|
||||||
|
(newItems) => {
|
||||||
|
if (newItems) {
|
||||||
|
localDataSource.value = cloneDeep(newItems);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ deep: true, immediate: true },
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderItems = () => {
|
||||||
|
return localDataSource.value.map((item, index) => (
|
||||||
|
<div
|
||||||
|
class={`group flex justify-between cursor-pointer items-center p-8px h-40px rounded-8px hover:bg-#F2F3F5 ${
|
||||||
|
activeKey.value === item.key ? 'bg-#F2F3F5' : ''
|
||||||
|
}`}
|
||||||
|
onClick={() => handleActiveChange(item)}
|
||||||
|
>
|
||||||
|
{item.editing ? (
|
||||||
|
<Input
|
||||||
|
ref={inputRef}
|
||||||
|
v-model:value={item.label}
|
||||||
|
maxlength={props.maxlength}
|
||||||
|
onBlur={() => {
|
||||||
|
item.editing = false;
|
||||||
|
changeItems();
|
||||||
|
emit('rename', item);
|
||||||
|
}}
|
||||||
|
onPressEnter={() => {
|
||||||
|
item.editing = false;
|
||||||
|
}}
|
||||||
|
class="flex-1"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<TextoverTips context={item.label} class="flex-1" placement="bottom" />
|
||||||
|
)}
|
||||||
|
<Dropdown
|
||||||
|
class="p-0"
|
||||||
|
overlayClassName="xt-conversations-dropdown"
|
||||||
|
placement="bottomRight"
|
||||||
|
v-slots={{
|
||||||
|
overlay: () => (
|
||||||
|
<Menu onClick={(menuInfo: MenuProps) => onMenuItemClick({ menuInfo, item })}>
|
||||||
|
{menuConfigs.value.map((menuItem) => (
|
||||||
|
<Menu.Item key={menuItem.key} icon={menuItem.icon} class={`${menuItem.status || ''}`}>
|
||||||
|
{menuItem.label}
|
||||||
|
</Menu.Item>
|
||||||
|
))}
|
||||||
|
</Menu>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<icon-more size={16} class="color-#737478 cursor-pointer ml-8px opacity-0 group-hover:opacity-100" />
|
||||||
|
</Dropdown>
|
||||||
|
</div>
|
||||||
|
));
|
||||||
|
};
|
||||||
|
|
||||||
|
return () => <div className="xt-conversations-container">{renderItems()}</div>;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
@import './style.scss';
|
||||||
|
</style>
|
||||||
|
<style lang="scss">
|
||||||
|
.xt-conversations-dropdown {
|
||||||
|
.ant-dropdown-menu {
|
||||||
|
padding: 4px 0;
|
||||||
|
.ant-dropdown-menu-item {
|
||||||
|
padding: 0 12px;
|
||||||
|
min-width: 124px;
|
||||||
|
height: 36px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
&:hover {
|
||||||
|
background: var(--BG-200, #f2f3f5);
|
||||||
|
}
|
||||||
|
&.danger {
|
||||||
|
color: var(--RED-600, #f64b31);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
13
src/components/xt-chat/xt-conversations/style.scss
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
.xt-conversations-container {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
// overflow-y: auto;
|
||||||
|
:deep(.overflow-text) {
|
||||||
|
color: var(--Text-1, #211f24);
|
||||||
|
font-family: $font-family-regular;
|
||||||
|
font-size: 14px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 22px;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -8,7 +8,8 @@
|
|||||||
"menuCollapse": false,
|
"menuCollapse": false,
|
||||||
"footer": true,
|
"footer": true,
|
||||||
"themeColor": "#165DFF",
|
"themeColor": "#165DFF",
|
||||||
"menuWidth": 220,
|
"menuWidth": 138,
|
||||||
|
"menuWidthFold": 74,
|
||||||
"globalSettings": false,
|
"globalSettings": false,
|
||||||
"device": "desktop",
|
"device": "desktop",
|
||||||
"tabBar": false,
|
"tabBar": false,
|
||||||
|
|||||||
15
src/hooks/useEventCallback.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
/*
|
||||||
|
* @Author: RenXiaoDong
|
||||||
|
* @Date: 2025-08-20 23:12:14
|
||||||
|
*/
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
export function useEventCallback<T>(handler?: (value: T) => void): (value: T) => void {
|
||||||
|
const callbackRef = ref(handler);
|
||||||
|
const fn = ref((value: T) => {
|
||||||
|
callbackRef.value && callbackRef.value(value);
|
||||||
|
});
|
||||||
|
callbackRef.value = handler;
|
||||||
|
|
||||||
|
return fn.value;
|
||||||
|
}
|
||||||
21
src/hooks/useState.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
/*
|
||||||
|
* @Author: RenXiaoDong
|
||||||
|
* @Date: 2025-08-20 23:14:22
|
||||||
|
*/
|
||||||
|
import type { Ref } from 'vue';
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
export default function useState<T, R = Ref<T>>(
|
||||||
|
defaultStateValue?: T | (() => T),
|
||||||
|
): [R, (val: T) => void] {
|
||||||
|
const initValue: T =
|
||||||
|
typeof defaultStateValue === 'function' ? (defaultStateValue as any)() : defaultStateValue;
|
||||||
|
|
||||||
|
const innerValue = ref(initValue) as Ref<T>;
|
||||||
|
|
||||||
|
function triggerChange(newValue: T) {
|
||||||
|
innerValue.value = newValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [innerValue as unknown as R, triggerChange];
|
||||||
|
}
|
||||||
@ -1,5 +1,10 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
|
import { Layout } from 'ant-design-vue';
|
||||||
|
import Navbar from './components/navbar';
|
||||||
|
import SiderBar from './components/siderBar';
|
||||||
|
|
||||||
import { useAppStore } from '@/stores';
|
import { useAppStore } from '@/stores';
|
||||||
|
import { useSidebarStore } from '@/stores/modules/side-bar';
|
||||||
import { useResponsive } from '@/hooks';
|
import { useResponsive } from '@/hooks';
|
||||||
import JoinModal from '@/components/join-modal.vue';
|
import JoinModal from '@/components/join-modal.vue';
|
||||||
import { getQueryParam } from '@/utils/helper';
|
import { getQueryParam } from '@/utils/helper';
|
||||||
@ -11,38 +16,33 @@ import { useRoute } from 'vue-router';
|
|||||||
const joinEnterpriseVisible = ref(false);
|
const joinEnterpriseVisible = ref(false);
|
||||||
const joinModalRef = ref(null);
|
const joinModalRef = ref(null);
|
||||||
const appStore = useAppStore();
|
const appStore = useAppStore();
|
||||||
|
const sidebarStore = useSidebarStore();
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
|
|
||||||
useResponsive(true);
|
useResponsive(true);
|
||||||
const navbarHeight = `72px`;
|
|
||||||
const navbar = computed(() => appStore.navbar);
|
|
||||||
const renderMenu = computed(() => appStore.menu && !appStore.topMenu);
|
|
||||||
const hideMenu = computed(() => appStore.hideMenu);
|
|
||||||
|
|
||||||
const menuWidth = computed(() => {
|
const isHomeRoute = computed(() => {
|
||||||
return appStore.menuCollapse ? 48 : appStore.menuWidth;
|
return route.name === 'Home';
|
||||||
});
|
});
|
||||||
const collapsed = computed(() => {
|
const showInOnePage = computed(() => {
|
||||||
return appStore.menuCollapse;
|
return isHomeRoute.value;
|
||||||
});
|
});
|
||||||
const showSidebar = computed(() => {
|
|
||||||
return !(route.meta && route.meta.hideSidebar);
|
const layoutPageClass = computed(() => {
|
||||||
});
|
let result = showInOnePage.value ? 'overflow-hidden' : '';
|
||||||
const paddingStyle = computed(() => {
|
if (isHomeRoute.value) {
|
||||||
const paddingLeft =
|
result += ' pb-8px pr-8px';
|
||||||
showSidebar.value && renderMenu.value && !hideMenu.value ? { paddingLeft: `${menuWidth.value}px` } : {};
|
} else {
|
||||||
const paddingTop = navbar.value ? { paddingTop: navbarHeight } : {};
|
result += ' pb-24px pr-24px';
|
||||||
return { ...paddingLeft, ...paddingTop };
|
}
|
||||||
|
return result;
|
||||||
});
|
});
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
checkHasInviteCode();
|
checkHasInviteCode();
|
||||||
});
|
});
|
||||||
const setCollapsed = (val) => {
|
|
||||||
appStore.updateSettings({ menuCollapse: val });
|
|
||||||
};
|
|
||||||
|
|
||||||
const checkHasInviteCode = () => {
|
const checkHasInviteCode = () => {
|
||||||
const inviteCode = getQueryParam('invite_code');
|
const inviteCode = getQueryParam('invite_code');
|
||||||
@ -51,121 +51,95 @@ const checkHasInviteCode = () => {
|
|||||||
joinModalRef.value?.getEnterprise?.();
|
joinModalRef.value?.getEnterprise?.();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const drawerVisible = ref(false);
|
|
||||||
const drawerCancel = () => {
|
|
||||||
drawerVisible.value = false;
|
|
||||||
};
|
|
||||||
provide('toggleDrawerMenu', () => {
|
|
||||||
drawerVisible.value = !drawerVisible.value;
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<a-layout :class="['layout', { mobile: appStore.hideMenu }]" class="h-100vh flex flex-col w-full">
|
<Layout :class="['layout-wrap', { mobile: appStore.hideMenu }]" class="h-full flex flex-col w-full">
|
||||||
<JoinModal v-model:visible="joinEnterpriseVisible" ref="joinModalRef" />
|
<JoinModal v-model:visible="joinEnterpriseVisible" ref="joinModalRef" />
|
||||||
<div v-if="navbar" class="layout-navbar">
|
<Layout.Header class="layout-header-wrap">
|
||||||
<base-navbar />
|
<Navbar />
|
||||||
</div>
|
</Layout.Header>
|
||||||
<a-layout>
|
<Layout class="flex app-content-layout">
|
||||||
<a-layout>
|
<div class="flex flex-1 app-content-scroll">
|
||||||
<a-layout-sider
|
<div class="app-content-inner">
|
||||||
v-if="renderMenu && showSidebar"
|
<SiderBar />
|
||||||
v-show="!hideMenu"
|
<Layout
|
||||||
class="layout-sider"
|
class="layout-content"
|
||||||
breakpoint="xl"
|
:style="{
|
||||||
:collapsed="collapsed"
|
width: `calc(100vw - ${sidebarStore.sidebarWidth}px)`,
|
||||||
:width="menuWidth"
|
}"
|
||||||
:style="{ paddingTop: navbar ? '72px' : '' }"
|
>
|
||||||
collapsible
|
<Layout.Content :class="layoutPageClass" class="!min-h-initial w-full">
|
||||||
hide-trigger
|
<layout-page />
|
||||||
@collapse="setCollapsed"
|
</Layout.Content>
|
||||||
>
|
</Layout>
|
||||||
<div class="menu-wrapper">
|
</div>
|
||||||
<base-menu />
|
</div>
|
||||||
</div>
|
</Layout>
|
||||||
</a-layout-sider>
|
</Layout>
|
||||||
<a-drawer
|
|
||||||
v-if="hideMenu"
|
|
||||||
:visible="drawerVisible"
|
|
||||||
placement="left"
|
|
||||||
:footer="false"
|
|
||||||
mask-closable
|
|
||||||
:closable="false"
|
|
||||||
@cancel="drawerCancel"
|
|
||||||
>
|
|
||||||
<base-menu />
|
|
||||||
</a-drawer>
|
|
||||||
<a-layout class="layout-content" :style="paddingStyle">
|
|
||||||
<base-tab-bar v-if="appStore.tabBar" />
|
|
||||||
<a-layout-content class="px-5 py-5">
|
|
||||||
<!-- <base-breadcrumb /> -->
|
|
||||||
<layout-page />
|
|
||||||
</a-layout-content>
|
|
||||||
</a-layout>
|
|
||||||
</a-layout>
|
|
||||||
</a-layout>
|
|
||||||
</a-layout>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
$layout-max-width: 1100px;
|
.layout-wrap {
|
||||||
|
font-family: inherit;
|
||||||
.layout-navbar {
|
background: transparent;
|
||||||
position: fixed;
|
min-width: $layout-min-width;
|
||||||
top: 0;
|
.layout-header-wrap {
|
||||||
left: 0;
|
background: transparent;
|
||||||
z-index: 1000;
|
height: $navbar-height;
|
||||||
width: 100%;
|
line-height: $navbar-height;
|
||||||
height: $navbar-height;
|
padding-inline: inherit;
|
||||||
}
|
color: inherit;
|
||||||
.layout-sider {
|
// position: fixed;
|
||||||
position: fixed;
|
// top: 0;
|
||||||
top: 0;
|
// left: 0;
|
||||||
left: 0;
|
// z-index: 1000;
|
||||||
z-index: 99;
|
width: 100%;
|
||||||
height: 100%;
|
min-width: $layout-min-width;
|
||||||
transition: all 0.2s cubic-bezier(0.34, 0.69, 0.1, 1);
|
}
|
||||||
|
.app-content-layout {
|
||||||
&::after {
|
width: 100%;
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
right: -1px;
|
|
||||||
display: block;
|
|
||||||
width: 1px;
|
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background-color: var(--color-border);
|
background: transparent;
|
||||||
content: '';
|
min-height: calc(100vh - $navbar-height);
|
||||||
}
|
.app-content-scroll {
|
||||||
> :deep(.arco-layout-sider-children) {
|
min-height: calc(100vh - $navbar-height);
|
||||||
overflow-y: hidden;
|
height: calc(100vh - $navbar-height);
|
||||||
}
|
overflow-y: auto;
|
||||||
}
|
overflow-x: auto;
|
||||||
.menu-wrapper {
|
.app-content-inner {
|
||||||
height: 100%;
|
width: 100%;
|
||||||
overflow: auto;
|
height: calc(100vh - $navbar-height);
|
||||||
overflow-x: hidden;
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
:deep(.arco-menu) {
|
}
|
||||||
::-webkit-scrollbar {
|
|
||||||
width: 12px;
|
|
||||||
height: 4px;
|
|
||||||
}
|
}
|
||||||
::-webkit-scrollbar-thumb {
|
:deep(.ant-layout-sider) {
|
||||||
border: 4px solid transparent;
|
background: none;
|
||||||
background-clip: padding-box;
|
box-shadow: none;
|
||||||
border-radius: 7px;
|
// padding-top: $navbar-height;
|
||||||
background-color: var(--color-text-4);
|
padding-bottom: 0;
|
||||||
|
// position: fixed;
|
||||||
|
// top: 0;
|
||||||
|
// left: 0;
|
||||||
|
// z-index: 999;
|
||||||
|
height: 100%;
|
||||||
|
transition: all 0.2s cubic-bezier(0.34, 0.69, 0.1, 1);
|
||||||
|
.ant-layout-sider-trigger {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.ant-layout-sider-children {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
::-webkit-scrollbar-thumb:hover {
|
.layout-content {
|
||||||
background-color: var(--color-text-3);
|
background: transparent;
|
||||||
|
transition: padding 0.2s cubic-bezier(0.34, 0.69, 0.1, 1);
|
||||||
|
flex: 1;
|
||||||
|
height: 100%;
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.layout-content {
|
|
||||||
min-width: 1366px;
|
|
||||||
height: 100%;
|
|
||||||
overflow-y: hidden;
|
|
||||||
background-color: $color-background;
|
|
||||||
transition: padding 0.2s cubic-bezier(0.34, 0.69, 0.1, 1);
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -8,7 +8,7 @@ const route = useRoute();
|
|||||||
const routerKey = computed(() => {
|
const routerKey = computed(() => {
|
||||||
return route.path + Math.random();
|
return route.path + Math.random();
|
||||||
});
|
});
|
||||||
const hideFooter = computed(() => route.meta?.hideFooter);
|
// const hideFooter = computed(() => route.meta?.hideFooter);
|
||||||
/*** - end */
|
/*** - end */
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -20,10 +20,10 @@ const hideFooter = computed(() => route.meta?.hideFooter);
|
|||||||
<component :is="Component" :key="route.fullPath" />
|
<component :is="Component" :key="route.fullPath" />
|
||||||
</keep-alive>
|
</keep-alive>
|
||||||
</transition>
|
</transition>
|
||||||
<view class="footer" v-if="!hideFooter">
|
<!-- <view class="footer" v-if="!hideFooter">
|
||||||
<view>闽公网安备 352018502850842号 闽ICP备20250520582号 © 2025小题科技,All Rights Reserved.</view>
|
<view>闽公网安备 352018502850842号 闽ICP备20250520582号 © 2025小题科技,All Rights Reserved.</view>
|
||||||
<view>* 数据通过公开渠道获取,灵机进行统计分析</view>
|
<view>* 数据通过公开渠道获取,灵机进行统计分析</view>
|
||||||
</view>
|
</view> -->
|
||||||
</router-view>
|
</router-view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 639 B After Width: | Height: | Size: 639 B |
@ -0,0 +1,40 @@
|
|||||||
|
<script lang="jsx">
|
||||||
|
import { Input } from 'ant-design-vue';
|
||||||
|
// import { handleUserHome } from '@/utils/user.ts';
|
||||||
|
import { useChatStore } from '@/stores/modules/chat';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
setup(props, { emit, expose }) {
|
||||||
|
const chatStore = useChatStore();
|
||||||
|
const keyWord = ref('');
|
||||||
|
|
||||||
|
const handleSearch = () => {
|
||||||
|
chatStore.setSearchValue(keyWord.value);
|
||||||
|
chatStore.onCreateSession();
|
||||||
|
keyWord.value = '';
|
||||||
|
};
|
||||||
|
return () => (
|
||||||
|
<div class="middle-wrap h-100% flex items-center justify-center">
|
||||||
|
<Input
|
||||||
|
v-model:value={keyWord.value}
|
||||||
|
onPressEnter={handleSearch}
|
||||||
|
size="large"
|
||||||
|
class="sender-input-wrap"
|
||||||
|
placeholder="随时告诉我你想做什么,比如查数据、发任务、写内容,我会立刻帮你完成。"
|
||||||
|
v-slots={{
|
||||||
|
suffix: () => (
|
||||||
|
<div class=" rounded-16px w-32px h-32px flex justify-center items-center icon " onClick={handleSearch}>
|
||||||
|
<icon-arrow-right size={20} class="color-#6D4CFE" />
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
@import './style.scss';
|
||||||
|
</style>
|
||||||
@ -0,0 +1,59 @@
|
|||||||
|
.middle-wrap {
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
:deep(.ant-input-affix-wrapper) {
|
||||||
|
width: 560px;
|
||||||
|
height: 36px;
|
||||||
|
padding: 0 2px 0 16px;
|
||||||
|
border-radius: 50px;
|
||||||
|
background: rgba(255, 255, 255, 0.6);
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
border-color: #fff;
|
||||||
|
box-shadow: none;
|
||||||
|
transition: all 0.3s;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
&.ant-input-affix-wrapper-focused {
|
||||||
|
border-color: #6d4cfe;
|
||||||
|
caret-color: #6d4cfe;
|
||||||
|
}
|
||||||
|
&:hover {
|
||||||
|
border-color: #6d4cfe;
|
||||||
|
}
|
||||||
|
.ant-input-suffix {
|
||||||
|
margin-inline-start: 0;
|
||||||
|
}
|
||||||
|
.ant-input {
|
||||||
|
padding-right: 16px;
|
||||||
|
border: none !important;
|
||||||
|
background-color: transparent;
|
||||||
|
box-shadow: none;
|
||||||
|
font-family: $font-family-regular;
|
||||||
|
color: #211f24;
|
||||||
|
font-size: 12px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 20px;
|
||||||
|
&::placeholder {
|
||||||
|
color: #939499;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus-within {
|
||||||
|
&::after {
|
||||||
|
border-width: 1px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.icon {
|
||||||
|
cursor: pointer;
|
||||||
|
background: #f0edff;
|
||||||
|
transition: background 0.3s;
|
||||||
|
&:hover {
|
||||||
|
background: linear-gradient(0deg, rgba(0, 0, 0, 0.04) 0%, rgba(0, 0, 0, 0.04) 100%), var(--Brand-1, #f0edff);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,19 +1,14 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="right-wrap">
|
<div class="right-wrap">
|
||||||
<!-- 灵机空间入口 -->
|
|
||||||
<div class="agent-entry" :class="isAgentRoute ? 'agent' : ''" @click="handleAgentClick"></div>
|
|
||||||
|
|
||||||
<!-- 任务中心 -->
|
<!-- 任务中心 -->
|
||||||
<div class="relative mx-16px" @click="setUnread">
|
<div class="relative p-6px rounded-30px flex items-center justify-center task-icon" @click="setUnread">
|
||||||
<SvgIcon
|
<SvgIcon name="svg-taskCenter" size="20" class="color-#737478" @click="openDownloadCenter" />
|
||||||
name="svg-taskCenter"
|
<div class="w-6px h-6px rounded-50% bg-#F64B31 absolute top-6px right-6px" v-if="hasUnreadInfo"></div>
|
||||||
size="16"
|
|
||||||
class="cursor-pointer color-#737478 hover:color-#6D4CFE"
|
|
||||||
@click="openDownloadCenter"
|
|
||||||
/>
|
|
||||||
<div class="w-4px h-4px rounded-50% bg-#F64B31 absolute top-1px right-1px" v-if="hasUnreadInfo"></div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 灵机空间入口 -->
|
||||||
|
<div class="agent-entry mx-16px" :class="isAgentRoute ? 'agent' : ''" @click="handleAgentClick"></div>
|
||||||
|
|
||||||
<!-- 头像设置 -->
|
<!-- 头像设置 -->
|
||||||
<a-dropdown trigger="click" class="layout-avatar-dropdown">
|
<a-dropdown trigger="click" class="layout-avatar-dropdown">
|
||||||
<a-avatar class="cursor-pointer" :size="32">
|
<a-avatar class="cursor-pointer" :size="32">
|
||||||
@ -80,8 +75,9 @@ import router from '@/router';
|
|||||||
import { useEnterpriseStore } from '@/stores/modules/enterprise';
|
import { useEnterpriseStore } from '@/stores/modules/enterprise';
|
||||||
import { useSidebarStore } from '@/stores/modules/side-bar';
|
import { useSidebarStore } from '@/stores/modules/side-bar';
|
||||||
import { useUserStore } from '@/stores';
|
import { useUserStore } from '@/stores';
|
||||||
|
import { handleUserHome } from '@/utils/user';
|
||||||
|
|
||||||
import ExitAccountModal from '@/components/_base/exit-account-modal';
|
import ExitAccountModal from '../exit-account-modal';
|
||||||
import DownloadCenterModal from '../task-center-modal';
|
import DownloadCenterModal from '../task-center-modal';
|
||||||
|
|
||||||
import icon1 from '@/assets/option.svg';
|
import icon1 from '@/assets/option.svg';
|
||||||
@ -131,7 +127,7 @@ const setUnread = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
const handleAgentClick = () => {
|
const handleAgentClick = () => {
|
||||||
router.push({ name: props.isAgentRoute ? 'Home' : 'AgentIndex' });
|
props.isAgentRoute ? handleUserHome() : router.push({ name: 'AgentIndex' });
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -1,6 +1,5 @@
|
|||||||
.right-wrap {
|
.right-wrap {
|
||||||
display: flex;
|
display: flex;
|
||||||
padding-right: 20px;
|
|
||||||
list-style: none;
|
list-style: none;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
.agent-entry {
|
.agent-entry {
|
||||||
@ -24,4 +23,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.task-icon {
|
||||||
|
cursor: pointer;
|
||||||
|
background: rgba(255, 255, 255, 0.6);
|
||||||
|
transition: background 0.2s;
|
||||||
|
&:hover {
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
77
src/layouts/components/navbar/index.vue
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
<template>
|
||||||
|
<div class="navbar-wrap px-24px">
|
||||||
|
<div class="w-full h-full relative flex justify-between">
|
||||||
|
|
||||||
|
<div class="left-wrap flex items-center cursor-pointer" @click="handleUserHome">
|
||||||
|
<img src="@/assets/img/icon-logo.png" alt="" width="96" height="24" />
|
||||||
|
</div>
|
||||||
|
<!-- <div class="flex-1"> -->
|
||||||
|
<MiddleSide v-if="!isHomeRoute" />
|
||||||
|
<!-- </div> -->
|
||||||
|
<RightSide :isAgentRoute="isAgentRoute" v-if="userStore.isLogin" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import MiddleSide from './components/middle-side';
|
||||||
|
import RightSide from './components/right-side';
|
||||||
|
|
||||||
|
import { useUserStore } from '@/stores';
|
||||||
|
import { handleUserHome } from '@/utils/user.ts';
|
||||||
|
import router from '@/router';
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
const userStore = useUserStore();
|
||||||
|
|
||||||
|
const isAgentRoute = computed(() => {
|
||||||
|
return route.meta?.isAgentRoute;
|
||||||
|
});
|
||||||
|
|
||||||
|
const isHomeRoute = computed(() => {
|
||||||
|
return route.name === 'Home';
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.navbar-wrap {
|
||||||
|
position: relative;
|
||||||
|
height: 100%;
|
||||||
|
// &::before {
|
||||||
|
// width: 100%;
|
||||||
|
// height: 100%;
|
||||||
|
// background: url('@/assets/img/icon-app-header-bg.png') center top no-repeat !important;
|
||||||
|
// background-size: cover !important;
|
||||||
|
// bottom: 0;
|
||||||
|
// content: '';
|
||||||
|
// display: block;
|
||||||
|
// left: 0;
|
||||||
|
// position: absolute;
|
||||||
|
// right: 0;
|
||||||
|
// top: 0;
|
||||||
|
// z-index: -998;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// background-color: var(--color-bg-2);
|
||||||
|
// border-bottom: 1px solid var(--color-border);
|
||||||
|
.left-wrap {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.arco-dropdown-option-suffix {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.enterprises-doption {
|
||||||
|
.arco-dropdown-option-content {
|
||||||
|
padding: 0 !important;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
&:not(.arco-dropdown-option-disabled):hover {
|
||||||
|
background-color: transparent;
|
||||||
|
.arco-dropdown-option-content {
|
||||||
|
background: var(--BG-200, #f2f3f5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
238
src/layouts/components/siderBar/index.vue
Normal file
@ -0,0 +1,238 @@
|
|||||||
|
<script lang="tsx">
|
||||||
|
import { Dropdown, Menu, Layout } from 'ant-design-vue';
|
||||||
|
import type { RouteMeta, RouteRecordRaw } from 'vue-router';
|
||||||
|
import { useRoute } from 'vue-router';
|
||||||
|
import SvgIcon from '@/components/svg-icon/index.vue';
|
||||||
|
|
||||||
|
import { useAppStore } from '@/stores';
|
||||||
|
import { useSidebarStore } from '@/stores/modules/side-bar';
|
||||||
|
import { MENU_LIST } from './menu-list';
|
||||||
|
import type { typeMenuItem } from './menu-list';
|
||||||
|
import { handleUserHome } from '@/utils/user';
|
||||||
|
|
||||||
|
import icon1 from '@/assets/img/agent/icon1.png';
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
emit: ['collapse'],
|
||||||
|
setup() {
|
||||||
|
// const appStore = useAppStore();
|
||||||
|
const router = useRouter();
|
||||||
|
const route = useRoute();
|
||||||
|
|
||||||
|
const sidebarStore = useSidebarStore();
|
||||||
|
const appStore = useAppStore();
|
||||||
|
|
||||||
|
const currentMenuList = ref<typeMenuItem[]>([]);
|
||||||
|
const currentMenuModInfo = ref<typeMenuItem>({});
|
||||||
|
|
||||||
|
const currentRouteName = computed(() => route.name as string);
|
||||||
|
const currentRouteGroup = computed(() => route.meta?.group ?? 'GroupMain');
|
||||||
|
const isHomeRoute = computed(() => currentRouteName.value === 'Home');
|
||||||
|
const showAiSearch = computed(() => !route.meta?.hideAiSearch);
|
||||||
|
|
||||||
|
const collapsed = computed(() => {
|
||||||
|
return sidebarStore.menuCollapse;
|
||||||
|
});
|
||||||
|
|
||||||
|
const setCollapsed = (val) => {
|
||||||
|
appStore.updateSettings({ menuCollapse: val });
|
||||||
|
};
|
||||||
|
const getCollapseMenuKey = (routeName: string): string => {
|
||||||
|
let _key: string;
|
||||||
|
for (let i = 0; i < currentMenuList.value.length; i++) {
|
||||||
|
const menuItem = currentMenuList.value[i];
|
||||||
|
|
||||||
|
// 检查是否有list子级
|
||||||
|
if (menuItem.children?.length > 0) {
|
||||||
|
for (let j = 0; j < menuItem.children.length; j++) {
|
||||||
|
const subMenuItem = menuItem.children[j];
|
||||||
|
if (subMenuItem.activeMatch?.includes(routeName)) {
|
||||||
|
currentMenuModInfo.value = menuItem;
|
||||||
|
_key = menuItem.key;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 没有list子级,直接检查当前项
|
||||||
|
if (menuItem.routeName === routeName) {
|
||||||
|
currentMenuModInfo.value = menuItem;
|
||||||
|
_key = menuItem.key;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return _key;
|
||||||
|
};
|
||||||
|
const onClickItem = (item: typeMenuItem) => {
|
||||||
|
let targetRoute = item.routeName;
|
||||||
|
if (item.children?.length) {
|
||||||
|
targetRoute = item.children[0].routeName;
|
||||||
|
}
|
||||||
|
router.push({ name: targetRoute });
|
||||||
|
};
|
||||||
|
const renderMenuItem = (item: typeMenuItem, hideLabel = false) => {
|
||||||
|
const getMenuItemClass = () => {
|
||||||
|
const hasChildren = item.children?.length;
|
||||||
|
let target = !hasChildren ? 'sub-menu-item ' : '';
|
||||||
|
if (hasChildren) {
|
||||||
|
target += getCollapseMenuKey(currentRouteName.value) === item.key ? 'active' : '';
|
||||||
|
} else {
|
||||||
|
target += item.activeMatch?.includes(currentRouteName.value) ? 'active' : '';
|
||||||
|
}
|
||||||
|
return target;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Menu.Item class={`menu-item ${getMenuItemClass()}`} onClick={() => onClickItem(item)}>
|
||||||
|
{(() => {
|
||||||
|
const isActive = getMenuItemClass() === 'active';
|
||||||
|
const iconName = Array.isArray(item.icon)
|
||||||
|
? isActive
|
||||||
|
? item.icon[1] ?? item.icon[0]
|
||||||
|
: item.icon[0]
|
||||||
|
: item.icon;
|
||||||
|
return <SvgIcon size="18" name={iconName as any} alt="状态图标" class="color-#55585F flex-shrink-0" />;
|
||||||
|
})()}
|
||||||
|
{!hideLabel && <span class="cts label">{item.label}</span>}
|
||||||
|
</Menu.Item>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
const renderMenuList = () => {
|
||||||
|
return currentMenuList.value.map((item) => {
|
||||||
|
if (!item.children) {
|
||||||
|
return renderMenuItem(item, collapsed.value);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Dropdown
|
||||||
|
overlayClassName="layout-sider-dropdown-xt"
|
||||||
|
placement="rightTop"
|
||||||
|
align={{ offset: [8, 0] }}
|
||||||
|
v-slots={{
|
||||||
|
overlay: () => {
|
||||||
|
return (
|
||||||
|
<div class="p-8px bg-#fff container w-139px">
|
||||||
|
{item.children.map((child) => {
|
||||||
|
return renderMenuItem(child);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{renderMenuItem(item, collapsed.value)}
|
||||||
|
</Dropdown>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const initMenuList = () => {
|
||||||
|
const groupMenuList = MENU_LIST?.[currentRouteGroup.value as string] ?? [];
|
||||||
|
currentMenuList.value = cloneDeep(groupMenuList);
|
||||||
|
sidebarStore.setCurrentMenuList(groupMenuList);
|
||||||
|
};
|
||||||
|
const initCollapse = () => {
|
||||||
|
getCollapseMenuKey(currentRouteName.value);
|
||||||
|
|
||||||
|
if (currentMenuModInfo.value) {
|
||||||
|
sidebarStore.setActiveMenuKey(currentMenuModInfo.value?.key);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const init = () => {
|
||||||
|
// 初始化菜单数据
|
||||||
|
initMenuList();
|
||||||
|
|
||||||
|
// 初始化菜单展开项
|
||||||
|
initCollapse();
|
||||||
|
};
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => currentRouteGroup.value,
|
||||||
|
() => {
|
||||||
|
init();
|
||||||
|
},
|
||||||
|
{ immediate: false, deep: true },
|
||||||
|
);
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
init();
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => (
|
||||||
|
<Layout.Sider
|
||||||
|
v-model={collapsed.value}
|
||||||
|
width={sidebarStore.sidebarWidth}
|
||||||
|
collapsible
|
||||||
|
trigger
|
||||||
|
onCollapse={setCollapsed}
|
||||||
|
>
|
||||||
|
<Menu class={`siderBar-wrap w-full flex flex-col px-16px pt-16px ${collapsed.value ? 'menu-fold' : ''}`}>
|
||||||
|
{showAiSearch.value && (
|
||||||
|
<>
|
||||||
|
<Menu.Item class={`menu-item !mb-0 ${isHomeRoute.value ? 'active' : ''}`} onClick={handleUserHome}>
|
||||||
|
<img src={icon1} width={18} height={18} />
|
||||||
|
{!collapsed.value && <span class="cts label">开始工作</span>}
|
||||||
|
</Menu.Item>
|
||||||
|
<div class="line w-full h-1px bg-#211F24 my-12px"></div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<div class="flex flex-col flex-1">
|
||||||
|
<div class="menu-list flex-1">{renderMenuList()}</div>
|
||||||
|
</div>
|
||||||
|
</Menu>
|
||||||
|
<div
|
||||||
|
class={`bg-#F6F5FC flex items-center absolute bottom-0 w-full pt-8px px-16px pb-16px right-0 ${
|
||||||
|
collapsed.value ? 'justify-center' : 'justify-end'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="flex fold-btn items-center cursor-pointer h-22px "
|
||||||
|
onClick={() => {
|
||||||
|
sidebarStore.setMenuCollapse();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{collapsed.value ? (
|
||||||
|
<icon-menu-unfold size={16} class="color-#55585F icon mr-4px" />
|
||||||
|
) : (
|
||||||
|
<icon-menu-fold size={16} class="color-#55585F icon mr-4px" />
|
||||||
|
)}
|
||||||
|
{!collapsed.value && <span class="cts !color-#55585F flex-shrink-0">收起</span>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Layout.Sider>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@import './style.scss';
|
||||||
|
</style>
|
||||||
|
<style lang="scss">
|
||||||
|
@import './style.scss';
|
||||||
|
.layout-sider-dropdown-xt {
|
||||||
|
.container {
|
||||||
|
border-radius: 8px;
|
||||||
|
background: var(--BG-White, #fff);
|
||||||
|
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.1);
|
||||||
|
.menu-item {
|
||||||
|
@include menu-item;
|
||||||
|
padding: 8px;
|
||||||
|
&:hover {
|
||||||
|
background-color: rgba(109, 76, 254, 0.08) !important;
|
||||||
|
color: #6d4cfe !important;
|
||||||
|
.svg-icon {
|
||||||
|
color: #6d4cfe !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// &.active {
|
||||||
|
// background-color: rgba(109, 76, 254, 0.08);
|
||||||
|
// color: #6d4cfe;
|
||||||
|
// .label,
|
||||||
|
// .svg-icon {
|
||||||
|
// color: #6d4cfe;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
177
src/layouts/components/siderBar/menu-list.ts
Normal file
@ -0,0 +1,177 @@
|
|||||||
|
export const GROUP_WRITER_NAME = 'GroupWriterMaterialCenter';
|
||||||
|
export const GROUP_MANAGEMENT_NAME = 'GroupManagement';
|
||||||
|
export const GROUP_MAIN_NAME = 'GroupMain';
|
||||||
|
|
||||||
|
export interface typeMenuItem {
|
||||||
|
key?: string; // 菜单组key
|
||||||
|
label?: string; // 菜单组标题
|
||||||
|
icon?: string | [string, string]; // 菜单组图标
|
||||||
|
routeName?: string; // 路由名称
|
||||||
|
requireLogin?: boolean; // 是否需要登录
|
||||||
|
requireAuth?: boolean; // 是否需要权限验证
|
||||||
|
activeMatch?: string[]; // 菜单高亮路由组匹配
|
||||||
|
children?: typeMenuItem[]; // 子菜单列表
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MENU_LIST = <Record<string, typeMenuItem[]>>{
|
||||||
|
[GROUP_MAIN_NAME]: [
|
||||||
|
{
|
||||||
|
key: 'ModAccountManage',
|
||||||
|
label: '账号管理',
|
||||||
|
icon: ['svg-accountManage', 'svg-accountManage-active'],
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
key: 'ModMediaAccountManage',
|
||||||
|
icon: 'svg-mediaAccountManage',
|
||||||
|
label: '账号管理',
|
||||||
|
routeName: 'MediaAccountAccountManagement',
|
||||||
|
requireLogin: true,
|
||||||
|
activeMatch: ['MediaAccountAccountManagement'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'ModMediaAccountData',
|
||||||
|
icon: 'svg-mediaAccountData',
|
||||||
|
label: '账号数据',
|
||||||
|
routeName: 'MediaAccountAccountDashboard',
|
||||||
|
requireLogin: true,
|
||||||
|
activeMatch: ['MediaAccountAccountDashboard', 'MediaAccountAccountDetails'],
|
||||||
|
},
|
||||||
|
// {
|
||||||
|
// key: 'ModPutAccountManage',
|
||||||
|
// icon: 'svg-putAccountManage',
|
||||||
|
// label: '账户管理',
|
||||||
|
// routeName: 'PutAccountAccountManagement',
|
||||||
|
// requireLogin: true,
|
||||||
|
// activeMatch: ['PutAccountAccountManagement'],
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// key: 'ModPutAccountData',
|
||||||
|
// icon: 'svg-putAccountData',
|
||||||
|
// label: '账户数据',
|
||||||
|
// routeName: 'PutAccountAccountData',
|
||||||
|
// requireLogin: true,
|
||||||
|
// activeMatch: ['PutAccountAccountData'],
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// key: 'ModPutAccountAccountDashboard',
|
||||||
|
// icon: 'svg-putAccountAccountDashboard',
|
||||||
|
// label: '投放表现分析',
|
||||||
|
// routeName: 'PutAccountAccountDashboard',
|
||||||
|
// requireLogin: true,
|
||||||
|
// activeMatch: ['PutAccountAccountDashboard'],
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// key: 'ModInvestmentGuidelines',
|
||||||
|
// icon: 'svg-putAccountInvestmentGuidelines',
|
||||||
|
// label: '投放指南',
|
||||||
|
// routeName: 'PutAccountInvestmentGuidelines',
|
||||||
|
// requireLogin: true,
|
||||||
|
// activeMatch: ['PutAccountInvestmentGuidelines', 'PutAccountInvestmentGuidelinesDetail'],
|
||||||
|
// },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'ModMaterialCenter',
|
||||||
|
label: '素材中心',
|
||||||
|
icon: ['svg-materialCenter', 'svg-materialCenter-active'],
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
key: 'ModMaterialCenterFinishedProductsWareHouse',
|
||||||
|
icon: 'svg-finishProductsWareHouse',
|
||||||
|
label: '成品库',
|
||||||
|
routeName: 'MaterialCenterFinishedProducts',
|
||||||
|
requireLogin: true,
|
||||||
|
activeMatch: [
|
||||||
|
'MaterialCenterFinishedProducts',
|
||||||
|
'ManuscriptUpload',
|
||||||
|
'ManuscriptEdit',
|
||||||
|
'ManuscriptDetail',
|
||||||
|
'ManuscriptCheckListDetail',
|
||||||
|
'ManuscriptCheck',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'ModMaterialCenterRawMaterialStorage',
|
||||||
|
icon: 'svg-rawMaterialStorage',
|
||||||
|
label: '原料库',
|
||||||
|
routeName: 'MaterialCenterRawMaterial',
|
||||||
|
requireLogin: true,
|
||||||
|
activeMatch: ['MaterialCenterRawMaterial'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'ModTaskManage',
|
||||||
|
label: '任务管理',
|
||||||
|
icon: ['svg-taskManage', 'svg-taskManage-active'],
|
||||||
|
routeName: 'TaskManagement',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[GROUP_WRITER_NAME]: [
|
||||||
|
{
|
||||||
|
key: 'ModWriterMaterialCenter',
|
||||||
|
label: '素材中心',
|
||||||
|
icon: 'svg-materialCenter',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
key: 'ModWriterMaterialCenterFinishedProductsWareHouse',
|
||||||
|
icon: 'svg-finishProductsWareHouse',
|
||||||
|
label: '成品库',
|
||||||
|
routeName: 'WriterMaterialCenterFinishedProducts',
|
||||||
|
requireLogin: true,
|
||||||
|
activeMatch: [
|
||||||
|
'WriterMaterialCenterFinishedProducts',
|
||||||
|
'WriterManuscriptUpload',
|
||||||
|
'WriterManuscriptEdit',
|
||||||
|
'WriterManuscriptDetail',
|
||||||
|
'WriterManuscriptCheckListDetail',
|
||||||
|
'WriterManuscriptCheck',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
// {
|
||||||
|
// key: 'ModWriterMaterialCenterRawMaterialStorage',
|
||||||
|
// icon: 'svg-rawMaterialStorage',
|
||||||
|
// label: '原料库',
|
||||||
|
// routeName: 'WriterMaterialCenterRawMaterial',
|
||||||
|
// requireLogin: true,
|
||||||
|
// activeMatch: ['WriterMaterialCenterRawMaterial'],
|
||||||
|
// },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[GROUP_MANAGEMENT_NAME]:[
|
||||||
|
{
|
||||||
|
key: 'ModManagement',
|
||||||
|
label: '管理中心',
|
||||||
|
icon: ['svg-management', 'svg-management-active'],
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
key: 'ModManagementPerson',
|
||||||
|
icon: 'svg-managementPerson',
|
||||||
|
label: '个人信息',
|
||||||
|
routeName: 'ManagementPerson',
|
||||||
|
requireLogin: true,
|
||||||
|
activeMatch: [
|
||||||
|
'ManagementPerson',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'ModManagementEnterprise',
|
||||||
|
icon: 'svg-managementEnterprise',
|
||||||
|
label: '企业信息',
|
||||||
|
routeName: 'ManagementEnterprise',
|
||||||
|
requireLogin: true,
|
||||||
|
activeMatch: ['ManagementEnterprise'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'ModManagementAccount',
|
||||||
|
icon: 'svg-managementAccount',
|
||||||
|
label: '账号管理',
|
||||||
|
routeName: 'ManagementAccount',
|
||||||
|
requireLogin: true,
|
||||||
|
activeMatch: ['ManagementAccount'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
82
src/layouts/components/siderBar/style.scss
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
@mixin menu-item {
|
||||||
|
border-radius: 8px;
|
||||||
|
backdrop-filter: blur(100px);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
// justify-content: center;
|
||||||
|
padding: 8px 12px;
|
||||||
|
margin: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
width: 100%;
|
||||||
|
.label {
|
||||||
|
transition: all 0.3s;
|
||||||
|
margin-left: 8px;
|
||||||
|
flex: auto;
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
.ant-menu-title-content {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
&:hover {
|
||||||
|
background-color: rgba(109, 76, 254, 0.08) !important;
|
||||||
|
color: #6d4cfe !important;
|
||||||
|
.label,
|
||||||
|
.svg-icon {
|
||||||
|
color: #6d4cfe !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&:not(.sub-menu-item) {
|
||||||
|
&.active {
|
||||||
|
background-color: #fff !important;
|
||||||
|
.label,
|
||||||
|
.svg-icon {
|
||||||
|
color: #6d4cfe !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&:not(:last-child) {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
&.ant-menu-item-selected {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.ant-layout-sider {
|
||||||
|
.ant-layout-sider-children {
|
||||||
|
position: relative;
|
||||||
|
// padding: 16px 0;
|
||||||
|
.siderBar-wrap {
|
||||||
|
background: transparent !important;
|
||||||
|
border: none !important;
|
||||||
|
color: inherit;
|
||||||
|
.cts {
|
||||||
|
color: #211f24;
|
||||||
|
font-family: $font-family-regular;
|
||||||
|
font-size: 14px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 22px;
|
||||||
|
}
|
||||||
|
.menu-item {
|
||||||
|
@include menu-item;
|
||||||
|
}
|
||||||
|
.line {
|
||||||
|
opacity: 0.06;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
.fold-btn {
|
||||||
|
&:hover {
|
||||||
|
.cts,
|
||||||
|
.icon {
|
||||||
|
color: #6d4cfe !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
62
src/layouts/components/siderBar/use-menu-tree.ts
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
// /*
|
||||||
|
// * @Author: RenXiaoDong
|
||||||
|
// * @Date: 2025-06-19 01:45:53
|
||||||
|
// */
|
||||||
|
// import type { RouteRecordRaw, RouteRecordNormalized } from 'vue-router';
|
||||||
|
// import { useRouter } from 'vue-router';
|
||||||
|
// import { useSidebarStore } from '@/stores/modules/side-bar';
|
||||||
|
|
||||||
|
// export default function useMenuTree() {
|
||||||
|
// const router = useRouter();
|
||||||
|
// const appRoutes = router.options?.routes ?? [];
|
||||||
|
// const sidebarStore = useSidebarStore();
|
||||||
|
// const appRoute = computed(() => {
|
||||||
|
// const _filterRoutes = appRoutes.filter((v) => v.meta?.id === sidebarStore.activeMenuKey);
|
||||||
|
// return _filterRoutes;
|
||||||
|
// });
|
||||||
|
// const menuTree = computed(() => {
|
||||||
|
// const copyRouter = cloneDeep(appRoute.value) as RouteRecordNormalized[];
|
||||||
|
// copyRouter.sort((a: RouteRecordNormalized, b: RouteRecordNormalized) => {
|
||||||
|
// return (a.meta.order || 0) - (b.meta.order || 0);
|
||||||
|
// });
|
||||||
|
// function travel(_routes: RouteRecordRaw[], layer: number) {
|
||||||
|
// if (!_routes) return null;
|
||||||
|
|
||||||
|
// const collector: any = _routes.map((element) => {
|
||||||
|
// // leaf node
|
||||||
|
// if (element.meta?.hideChildrenInMenu || !element.children) {
|
||||||
|
// element.children = [];
|
||||||
|
// return element;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // route filter hideInMenu true
|
||||||
|
// element.children = element.children.filter((x) => x.meta?.hideInMenu !== true);
|
||||||
|
|
||||||
|
// // Associated child node
|
||||||
|
// const subItem = travel(element.children, layer + 1);
|
||||||
|
|
||||||
|
// if (subItem.length) {
|
||||||
|
// element.children = subItem;
|
||||||
|
// return element;
|
||||||
|
// }
|
||||||
|
// // the else logic
|
||||||
|
// if (layer > 1) {
|
||||||
|
// element.children = subItem;
|
||||||
|
// return element;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if (element.meta?.hideInMenu === false) {
|
||||||
|
// return element;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// return null;
|
||||||
|
// });
|
||||||
|
// return collector.filter(Boolean);
|
||||||
|
// }
|
||||||
|
// return travel(copyRouter, 0);
|
||||||
|
// });
|
||||||
|
|
||||||
|
// return {
|
||||||
|
// menuTree,
|
||||||
|
// };
|
||||||
|
// }
|
||||||
@ -32,4 +32,7 @@ Object.values(directives).forEach((directive) => {
|
|||||||
app.use(directive);
|
app.use(directive);
|
||||||
}); // 注册指令
|
}); // 注册指令
|
||||||
|
|
||||||
app.mount('#app');
|
// 解决mounted中获取不到route信息
|
||||||
|
router.isReady().then(() => {
|
||||||
|
app.mount('#app');
|
||||||
|
});
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
import { useUserStore } from '@/stores/modules/user';
|
// import { useUserStore } from '@/stores/modules/user';
|
||||||
|
|
||||||
export function checkRoutePermission(routeName: string) {
|
// export function checkRoutePermission(routeName: string) {
|
||||||
const userStore = useUserStore();
|
// // const userStore = useUserStore();
|
||||||
const allowAccessRoutes = userStore.allowAccessRoutes;
|
// // const allowAccessRoutes = userStore.allowAccessRoutes;
|
||||||
|
|
||||||
if (!routeName) return false;
|
// // if (!routeName) return false;
|
||||||
return allowAccessRoutes.includes(routeName);
|
// // return allowAccessRoutes.includes(routeName);
|
||||||
}
|
// }
|
||||||
|
|||||||
@ -6,7 +6,7 @@ import type { Router } from 'vue-router';
|
|||||||
import NProgress from 'nprogress';
|
import NProgress from 'nprogress';
|
||||||
import { goUserLogin } from '@/utils/user';
|
import { goUserLogin } from '@/utils/user';
|
||||||
// import router from '@/router';
|
// import router from '@/router';
|
||||||
import { checkRoutePermission } from '@/permission/permission';
|
// import { checkRoutePermission } from '@/permission/permission';
|
||||||
|
|
||||||
import { useUserStore } from '@/stores/modules/user';
|
import { useUserStore } from '@/stores/modules/user';
|
||||||
|
|
||||||
@ -26,16 +26,16 @@ export default function setupUserLoginInfoGuard(router: Router) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (requiresAuth) {
|
// if (requiresAuth) {
|
||||||
const hasPermission = checkRoutePermission(routeName);
|
// const hasPermission = checkRoutePermission(routeName);
|
||||||
if (!hasPermission) {
|
// if (!hasPermission) {
|
||||||
AMessage.error('您没有权限访问该页面');
|
// AMessage.error('您没有权限访问该页面');
|
||||||
next('/');
|
// next('/');
|
||||||
return;
|
// return;
|
||||||
}
|
// }
|
||||||
next();
|
// next();
|
||||||
return;
|
// return;
|
||||||
}
|
// }
|
||||||
|
|
||||||
next();
|
next();
|
||||||
});
|
});
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import NProgress from 'nprogress';
|
|||||||
import 'nprogress/nprogress.css';
|
import 'nprogress/nprogress.css';
|
||||||
import { MENU_GROUP_IDS } from './constants';
|
import { MENU_GROUP_IDS } from './constants';
|
||||||
import createRouteGuard from './guard';
|
import createRouteGuard from './guard';
|
||||||
|
import { GROUP_MAIN_NAME } from '@/layouts/components/siderBar/menu-list';
|
||||||
|
|
||||||
NProgress.configure({ showSpinner: false }); // NProgress Configuration
|
NProgress.configure({ showSpinner: false }); // NProgress Configuration
|
||||||
|
|
||||||
@ -25,16 +26,24 @@ export const router = createRouter({
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/',
|
path: '/',
|
||||||
name: 'Home',
|
name: 'Index',
|
||||||
component: () => import('@/views/components/workplace/index.vue'),
|
redirect: '/chat',
|
||||||
|
meta: {
|
||||||
|
requiresAuth: false,
|
||||||
|
requireLogin: true,
|
||||||
|
group: GROUP_MAIN_NAME,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/chat/:conversationId?',
|
||||||
|
name: 'Home',
|
||||||
|
component: () => import('@/views/home/index.vue'),
|
||||||
meta: {
|
meta: {
|
||||||
hideSidebar: true,
|
|
||||||
requiresAuth: false,
|
requiresAuth: false,
|
||||||
requireLogin: true,
|
requireLogin: true,
|
||||||
id: MENU_GROUP_IDS.WORK_BENCH_ID,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
...appRoutes,
|
...appRoutes,
|
||||||
{
|
{
|
||||||
path: '/:pathMatch(.*)*',
|
path: '/:pathMatch(.*)*',
|
||||||
|
|||||||
@ -15,6 +15,7 @@ const COMPONENTS: AppRouteRecordRaw[] = [
|
|||||||
requireLogin: true,
|
requireLogin: true,
|
||||||
roles: ['*'],
|
roles: ['*'],
|
||||||
id: MENU_GROUP_IDS.AGENT,
|
id: MENU_GROUP_IDS.AGENT,
|
||||||
|
group: 'agent'
|
||||||
},
|
},
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
@ -26,7 +27,9 @@ const COMPONENTS: AppRouteRecordRaw[] = [
|
|||||||
requiresAuth: false,
|
requiresAuth: false,
|
||||||
requireLogin: true,
|
requireLogin: true,
|
||||||
hideFooter: true,
|
hideFooter: true,
|
||||||
isAgentRoute: true
|
isAgentRoute: true,
|
||||||
|
group: 'agent',
|
||||||
|
hideAiSearch: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -41,6 +44,8 @@ const COMPONENTS: AppRouteRecordRaw[] = [
|
|||||||
id: MENU_GROUP_IDS.AGENT,
|
id: MENU_GROUP_IDS.AGENT,
|
||||||
isAgentRoute: true,
|
isAgentRoute: true,
|
||||||
hideInMenu: true,
|
hideInMenu: true,
|
||||||
|
group: 'agent',
|
||||||
|
hideAiSearch: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -55,7 +60,9 @@ const COMPONENTS: AppRouteRecordRaw[] = [
|
|||||||
hideFooter: true,
|
hideFooter: true,
|
||||||
id: MENU_GROUP_IDS.AGENT,
|
id: MENU_GROUP_IDS.AGENT,
|
||||||
isAgentRoute: true,
|
isAgentRoute: true,
|
||||||
hideInMenu: true,
|
hideInMenu: true,
|
||||||
|
group: 'agent',
|
||||||
|
hideAiSearch: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||