Merge remote-tracking branch 'origin/main' into feature/0909_主agent优化

# Conflicts:
#	src/views/home/components/history-conversation-drawer/index.vue
This commit is contained in:
rd
2025-09-12 15:13:51 +08:00
235 changed files with 5666 additions and 6193 deletions

View File

@ -1,2 +0,0 @@
export { default as TabBar } from './tab-bar/index.vue';
export { default as ModalSimple } from './modal/index.vue';

View File

@ -1,32 +0,0 @@
<script setup lang="ts">
import type { Component, DefineComponent } from 'vue';
import IconHover from '@arco-design/web-vue/es/_components/icon-hover';
defineProps<{
title?: string;
content?: string | (() => DefineComponent | Component);
}>();
defineEmits(['close']);
</script>
<template>
<slot name="header">
<div class="flex justify-end mb7">
<slot name="close">
<icon-hover @click="$emit('close')">
<icon-close />
</icon-hover>
</slot>
</div>
</slot>
<slot>
<div class="flex flex-col text-center">
<div v-if="title" class="mb4 text-lg font-600">{{ title }}</div>
<template v-else />
<component :is="content" v-if="typeof content === 'function'" />
<div v-else>{{ content }}</div>
</div>
</slot>
</template>

View File

@ -1,83 +0,0 @@
<script lang="ts" setup>
import type { RouteLocationNormalized } from 'vue-router';
import { listenerRouteChange, removeRouteListener } from '@/utils/route-listener';
import { useAppStore, useTabBarStore } from '@/stores';
import TabItem from './tab-item.vue';
const appStore = useAppStore();
const tabBarStore = useTabBarStore();
const affixRef = ref();
const tagList = computed(() => {
return tabBarStore.getTabList;
});
const offsetTop = computed(() => {
return appStore.navbar ? 60 : 0;
});
watch(
() => appStore.navbar,
() => {
affixRef.value.updatePosition();
},
);
listenerRouteChange((route: RouteLocationNormalized) => {
if (!route.meta.noAffix && !tagList.value.some((tag) => tag.fullPath === route.fullPath)) {
tabBarStore.updateTabList(route);
}
}, true);
onUnmounted(() => {
removeRouteListener();
});
</script>
<template>
<div class="tab-bar-container">
<a-affix ref="affixRef" :offset-top="offsetTop">
<div class="tab-bar-box">
<div class="tab-bar-scroll">
<div class="tags-wrap">
<tab-item v-for="(tag, index) in tagList" :key="tag.fullPath" :index="index" :item-data="tag" />
</div>
</div>
<div class="tag-bar-operation"></div>
</div>
</a-affix>
</div>
</template>
<style scoped lang="scss">
.tab-bar-container {
position: relative;
background-color: var(--color-bg-2);
.tab-bar-box {
display: flex;
padding: 0 0 0 20px;
background-color: var(--color-bg-2);
border-bottom: 1px solid var(--color-border);
.tab-bar-scroll {
height: 32px;
flex: 1;
overflow: hidden;
.tags-wrap {
padding: 4px 0;
height: 48px;
white-space: nowrap;
overflow-x: auto;
:deep(.arco-tag) {
display: inline-flex;
align-items: center;
margin-right: 6px;
cursor: pointer;
&:first-child {
.arco-tag-close-btn {
display: none;
}
}
}
}
}
}
.tag-bar-operation {
width: 100px;
height: 32px;
}
}
</style>

View File

@ -1,177 +0,0 @@
<script lang="ts" setup>
import type { PropType } from 'vue';
import type { TagProps } from '@/stores/modules/tab-bar/types';
import { useTabBarStore } from '@/stores';
import { DEFAULT_ROUTE_NAME, REDIRECT_ROUTE_NAME } from '@/router/constants';
const props = defineProps({
itemData: {
type: Object as PropType<TagProps>,
default() {
return [];
},
},
index: {
type: Number,
default: 0,
},
});
// eslint-disable-next-line no-shadow
enum Eaction {
reload = 'reload',
current = 'current',
left = 'left',
right = 'right',
others = 'others',
all = 'all',
}
const router = useRouter();
const route = useRoute();
const tabBarStore = useTabBarStore();
const goto = (tag: TagProps) => {
router.push({ ...tag });
};
const tagList = computed(() => {
return tabBarStore.getTabList;
});
const disabledReload = computed(() => {
return props.itemData.fullPath !== route.fullPath;
});
const disabledCurrent = computed(() => {
return props.index === 0;
});
const disabledLeft = computed(() => {
return [0, 1].includes(props.index);
});
const disabledRight = computed(() => {
return props.index === tagList.value.length - 1;
});
const tagClose = (tag: TagProps, idx: number) => {
tabBarStore.deleteTag(idx, tag);
if (props.itemData.fullPath === route.fullPath) {
const latest = tagList.value[idx - 1]; // 获取队列的前一个tab
router.push({ name: latest.name });
}
};
const findCurrentRouteIndex = () => {
return tagList.value.findIndex((el) => el.fullPath === route.fullPath);
};
const actionSelect = async (value: any) => {
const { itemData, index } = props;
const copyTagList = [...tagList.value];
if (value === Eaction.current) {
tagClose(itemData, index);
} else if (value === Eaction.left) {
const currentRouteIdx = findCurrentRouteIndex();
copyTagList.splice(1, props.index - 1);
tabBarStore.freshTabList(copyTagList);
if (currentRouteIdx < index) {
router.push({ name: itemData.name });
}
} else if (value === Eaction.right) {
const currentRouteIdx = findCurrentRouteIndex();
copyTagList.splice(props.index + 1);
tabBarStore.freshTabList(copyTagList);
if (currentRouteIdx > index) {
router.push({ name: itemData.name });
}
} else if (value === Eaction.others) {
const filterList = tagList.value.filter((el, idx) => {
return idx === 0 || idx === props.index;
});
tabBarStore.freshTabList(filterList);
router.push({ name: itemData.name });
} else if (value === Eaction.reload) {
tabBarStore.deleteCache(itemData);
await router.push({
name: REDIRECT_ROUTE_NAME,
params: {
path: route.fullPath,
},
});
tabBarStore.addCache(itemData.name);
} else {
tabBarStore.resetTabList();
router.push({ name: DEFAULT_ROUTE_NAME });
}
};
</script>
<template>
<a-dropdown trigger="contextMenu" :popup-max-height="false" @select="actionSelect">
<span
:class="[
'arco-tag arco-tag-size-medium arco-tag-checked',
{ 'link-activated': itemData.fullPath === $route.fullPath },
]"
@click="goto(itemData)"
>
<span class="tag-link">{{ itemData.title }}</span>
<span
class="arco-icon-hover arco-tag-icon-hover arco-icon-hover-size-medium arco-tag-close-btn"
@click.stop="tagClose(itemData, index)"
>
<icon-close />
</span>
</span>
<template #content>
<a-doption :disabled="disabledReload" :value="Eaction.reload">
<icon-refresh />
<span>重新加载</span>
</a-doption>
<a-doption class="sperate-line" :disabled="disabledCurrent" :value="Eaction.current">
<icon-close />
<span>关闭当前标签页</span>
</a-doption>
<a-doption :disabled="disabledLeft" :value="Eaction.left">
<icon-to-left />
<span>关闭左侧标签页</span>
</a-doption>
<a-doption class="sperate-line" :disabled="disabledRight" :value="Eaction.right">
<icon-to-right />
<span>关闭右侧标签页</span>
</a-doption>
<a-doption :value="Eaction.others">
<icon-swap />
<span>关闭其它标签页</span>
</a-doption>
<a-doption :value="Eaction.all">
<icon-folder-delete />
<span>关闭全部标签页</span>
</a-doption>
</template>
</a-dropdown>
</template>
<style scoped lang="scss">
.tag-link {
color: var(--color-text-2);
text-decoration: none;
}
.link-activated {
color: rgb(var(--link-6));
.tag-link {
color: rgb(var(--link-6));
}
& + .arco-tag-close-btn {
color: rgb(var(--link-6));
}
}
:deep(.arco-dropdown-option-content) {
span {
margin-left: 10px;
}
}
.arco-dropdown-open {
.tag-link {
color: rgb(var(--danger-6));
}
.arco-tag-close-btn {
color: rgb(var(--danger-6));
}
}
.sperate-line {
border-bottom: 1px solid var(--color-neutral-3);
}
</style>

View File

@ -3,23 +3,26 @@
* @Date: 2025-06-25 14:02:40
-->
<template>
<a-select
v-model="selectedValues"
:multiple="multiple"
size="medium"
<Select
v-model:value="selectedValues"
:mode="multiple ? 'multiple' : undefined"
size="middle"
:placeholder="placeholder"
:allow-clear="allClear"
:allow-search="allowSearch"
:max-tag-count="maxTagCount"
:allowClear="allClear"
:showSearch="allowSearch"
showArrow
:maxTagCount="maxTagCount"
@change="handleChange"
>
<a-option v-for="(item, index) in options" :key="index" :value="item.id" :label="item.name">
<Option v-for="(item, index) in options" :key="index" :value="item.id" :label="item.name">
{{ item.name }}
</a-option>
</a-select>
</Option>
</Select>
</template>
<script setup>
import { Select } from 'ant-design-vue';
const { Option } = Select;
import { ref, watch } from 'vue';
const props = defineProps({

View File

@ -3,14 +3,15 @@
* @Date: 2025-06-30 10:54:49
-->
<template>
<a-modal
v-model:visible="visible"
<Modal
v-model:open="visible"
title="自定义列"
width="960px"
unmountOnClose
titleAlign="start"
class="custom-table-column-modal"
@close="close"
centered
wrapClassName="custom-table-column-modal"
@cancel="close"
>
<div class="modal-body">
<!-- 左侧分组 -->
@ -20,16 +21,16 @@
<span class="text">{{ group.label }}</span>
</div>
<div class="fields">
<a-checkbox
<Checkbox
v-for="option in group.columns"
:key="option.value"
:model-value="isCheck(option)"
:checked="isCheck(option)"
:value="option.value"
:disabled="option.is_require === ENUM_STATUS.NO"
@change="(checked) => onCheckChange(checked, option)"
@change="(e) => onCheckChange(e.target.checked, option)"
>
{{ option.label }}
</a-checkbox>
</Checkbox>
</div>
</div>
</div>
@ -64,15 +65,16 @@
</div>
<template #footer>
<div style="text-align: right">
<a-button class="mr-8px" size="medium" @click="close">取消</a-button>
<a-button type="primary" size="medium" @click="onSubmit">确定</a-button>
<div class="flex">
<Button @click="close">取消</Button>
<Button type="primary" @click="onSubmit">确定</Button>
</div>
</template>
</a-modal>
</Modal>
</template>
<script setup>
import { Checkbox, Modal, Button } from 'ant-design-vue';
import { ref, defineExpose } from 'vue';
import { VueDraggable } from 'vue-draggable-plus';

View File

@ -1,5 +1,5 @@
.custom-table-column-modal {
.arco-modal-body {
.ant-modal-body {
.modal-body {
height: 504px;
border-radius: 8px;

View File

@ -1,13 +1,31 @@
<template>
<a-modal modal-class="delete-modal" body-class="body" cancel-text="返回" ok-text="确定删除" v-bind="$attrs">
<Modal wrapClassName="delete-modal" body-class="body" v-bind="$attrs" centered title="删除账号">
<h2 class="delete-modal-title flex item-center">
<img src="@/assets/warning.svg" alt="" />
{{ $attrs.title }}
{{ $attrs.content }}
</h2>
<slot></slot>
</a-modal>
<p class="delete-modal-content">删除后该账号将无法登录您的企业</p>
<template #footer>
<div style="text-align: right">
<Button @click="close">返回</Button>
<Button type="primary" danger @click="onSubmit">确定删除</Button>
</div>
</template>
</Modal>
</template>
<script setup lang="ts"></script>
<script setup lang="ts">
import { Modal, Button } from 'ant-design-vue';
const emit = defineEmits(['close', 'ok']);
const close = () => {
emit('close');
};
const onSubmit = () => {
emit('ok');
};
</script>
<style lang="scss">
:deep(.arco-btn-status-danger) {
background-color: red !important;
@ -18,7 +36,7 @@
display: none;
}
.delete-modal-title {
margin-top: 24px;
// margin-top: 24px;
font-family: $font-family-medium;
font-weight: 400;
font-size: 14px;
@ -29,6 +47,14 @@
margin-right: 12px;
}
}
.delete-modal-content {
margin-left: 34px;
margin-top: 16px;
font-family: $font-family-medium;
font-weight: 400;
font-size: 12px;
color: var(--Text-2, rgba(60, 64, 67, 1));
}
.arco-modal-footer {
border-top: none;
:first-child {
@ -53,5 +79,6 @@
}
.body {
padding: 0 24px;
}
</style>

View File

@ -3,32 +3,45 @@
* @Date: 2025-08-11 22:15:35
-->
<template>
<a-popover
:trigger="'hover'"
class="hover-big-image-preview-popover"
:position="props.position"
:mouse-enter-delay="props.enterDelay"
:mouse-leave-delay="props.leaveDelay"
:disabled="!props.src"
<Popover
trigger="hover"
:placement="props.position"
:mouseEnterDelay="props.enterDelay / 1000"
:mouseLeaveDelay="props.leaveDelay / 1000"
:open="props.src ? undefined : false"
overlayClassName="hover-big-image-preview-popover"
>
<template #content>
<div class="preview-container">
<img :src="props.src" alt="preview" class="preview-image" />
</div>
</template>
<slot />
</a-popover>
</Popover>
</template>
<script lang="ts" setup>
import { Popover } from 'ant-design-vue';
// import { computed, onMounted, ref, watch } from 'vue';
// import type { ImageOrientation } from '@/utils/tools';
// import { getImageOrientationByUrl } from '@/utils/tools';
interface Props {
src: string;
position?: 'top' | 'tl' | 'tr' | 'bottom' | 'bl' | 'br' | 'left' | 'lt' | 'lb' | 'right' | 'rt' | 'rb';
position?:
| 'top'
| 'topLeft'
| 'topRight'
| 'bottom'
| 'bottomLeft'
| 'bottomRight'
| 'left'
| 'leftTop'
| 'leftBottom'
| 'right'
| 'rightTop'
| 'rightBottom';
enterDelay?: number;
leaveDelay?: number;
}
@ -37,6 +50,7 @@ const props = withDefaults(defineProps<Props>(), {
position: 'right',
enterDelay: 100,
leaveDelay: 200,
src: '',
});
// const orientation = ref<ImageOrientation>('landscape');
@ -67,7 +81,11 @@ const props = withDefaults(defineProps<Props>(), {
<style lang="scss">
.hover-big-image-preview-popover {
.arco-popover-popup-content {
.ant-popover-content {
padding: 0 !important;
}
.ant-popover-inner {
padding: 16px !important;
border-radius: 8px;
background: #fff;

View File

@ -0,0 +1,90 @@
<template>
<div class="img-lazy" v-lazy:background-image="imgSrc" :key="src" :class="imgClass" :style="style" />
</template>
<script setup>
import { ref, onMounted, watch, computed } from 'vue';
const emit = defineEmits(['click']);
const props = defineProps({
width: {
type: [String, Number],
},
height: {
type: [String, Number],
},
loadingSize: {
type: [String],
default: '5',
},
errorSize: {
type: [String],
default: '5',
},
fit: {
type: [String],
default: 'cover',
},
src: {
type: String,
},
customImg: {
type: String,
},
});
const style = computed(() => {
return {
'background-size': props.fit,
width: props.width ? parseInt(props.width) + 'px' : undefined,
height: props.height ? parseInt(props.height) + 'px' : undefined,
};
});
const imgClass = computed(() => {
return {
['loading-size-' + props.loadingSize]: true,
};
});
const imgSrc = computed(() => {
return props.innerSrc || props.src;
});
const innerSrc = ref('');
watch(
() => props.customImg,
() => {
innerSrc.value = '';
if (props.customImg) {
const img = new Image();
img.src = props.src;
img.onerror = () => {
innerSrc.value = props.customImg;
};
img.onload = () => {
innerSrc.value = props.src;
};
}
},
{ immediate: true },
);
onMounted(() => {});
</script>
<style scoped lang="scss">
.img-lazy.block {
display: block;
}
.img-lazy {
display: inline-block;
overflow: hidden;
background-size: 100% 100%;
background-position: center center;
background-repeat: no-repeat;
@for $i from 0 to 10 {
&.loading-size-#{$i}[lazy='loading'] {
background-size: #{$i * 10 + '%'} !important;
}
}
}
</style>

View File

@ -9,6 +9,7 @@
<script setup lang="ts">
import Modal from '@components/modal.vue';
import { ref, onMounted } from 'vue';
import { message } from 'ant-design-vue';
import { getQueryParam } from '@/utils/helper';
import { getEnterpriseByInviteCode, joinEnterpriseByInviteCode } from '@/api/all';
@ -26,7 +27,7 @@ async function getEnterprise() {
async function handleJoin() {
await joinEnterpriseByInviteCode(inviteCode.value);
AMessage.success('加入成功');
message.success('加入成功');
}
// onMounted(() => {
// getEnterprise();

View File

@ -1,9 +1,11 @@
<template>
<a-modal title-align="start" modal-class="modal" body-class="body" v-bind="$attrs">
<Modal title-align="start" wrapClassName="modal" cancelText="取消" okText="确定" body-class="body" v-bind="$attrs" centered>
<slot></slot>
</a-modal>
</Modal>
</template>
<script setup lang="ts"></script>
<script setup lang="ts">
import { Modal } from 'ant-design-vue';
</script>
<style lang="scss">
.modal {
.arco-modal-header {

View File

@ -1,18 +1,16 @@
<template>
<a-upload
:custom-request="customRequest"
<Upload
:customRequest="customRequest"
action="/"
:limit="limit"
:maxCount="limit"
:fileList="fileList"
@change="onChange"
@success="handleSuccess"
@error="handleError"
/>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { Message } from '@arco-design/web-vue';
import { Upload, message } from 'ant-design-vue';
import { fetchImageUploadFile, fetchUploadFile } from '@/api/all';
import axios from 'axios';
@ -70,8 +68,10 @@ watch(
);
let previousFileListLength = 0;
//删除图片
const onChange = (fileList) => {
const onChange = (info) => {
const { fileList } = info;
// 如果删除了文件
if (fileList.length < previousFileListLength) {
if (props.limit === 1) {
if (fileList.length === 0) {
@ -86,28 +86,35 @@ const onChange = (fileList) => {
}
}
}
// 处理上传成功的文件
if (info.file.status === 'done' && info.file.response) {
handleSuccess(info.file);
} else if (info.file.status === 'error') {
handleError(info.file.error);
}
previousFileListLength = fileList.length;
};
const beforeUpload = (file, files) => {
if (props.limit > 0 && files.length >= props.limit) {
Message.warning(`最多只能上传 ${props.limit} 张图片`);
message.warning(`最多只能上传 ${props.limit} 张图片`);
return false; // 阻止上传
}
return true;
};
const handleError = (error) => {
Message.error('上传失败');
message.error('上传失败');
console.error(error);
};
const customRequest = async (option) => {
const { onProgress, onError, onSuccess, fileItem, name } = option;
const { onProgress, onError, onSuccess, file, name } = option;
try {
// 1. 获取预签名上传URL
const response = await fetchUploadFile({ suffix: getFileExtension(fileItem.file.name) });
const response = await fetchUploadFile({ suffix: getFileExtension(file.name) });
const preSignedUrl = response?.data?.upload_url;
if (!preSignedUrl) {
@ -115,9 +122,9 @@ const customRequest = async (option) => {
}
console.log('preSignedUrl', preSignedUrl);
// 2. 使用预签名URL上传文件
const blob = new Blob([fileItem.file], { type: fileItem.file.type });
const blob = new Blob([file], { type: file.type });
await axios.put(preSignedUrl, blob, {
headers: { 'Content-Type': fileItem.file.type },
headers: { 'Content-Type': file.type },
});
onSuccess(JSON.stringify(response));

View File

@ -1,20 +1,19 @@
<template>
<a-upload
:custom-request="customRequest"
list-type="picture-card"
<Upload
:customRequest="customRequest"
listType="picture-card"
action="/"
:limit="limit"
:maxCount="limit"
:fileList="fileList"
image-preview
:showUploadList="{ showPreviewIcon: true, showRemoveIcon: true }"
@change="onChange"
@success="handleSuccess"
@error="handleError"
@preview="handlePreview"
/>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { Message } from '@arco-design/web-vue';
import { Upload, message } from 'ant-design-vue';
import { fetchImageUploadFile } from '@/api/all';
import axios from 'axios';
@ -72,8 +71,14 @@ watch(
);
let previousFileListLength = 0;
//删除图片
const onChange = (fileList) => {
const handlePreview = (file) => {
console.log('Preview file:', file);
};
const onChange = (info) => {
const { fileList } = info;
// 如果删除了文件
if (fileList.length < previousFileListLength) {
if (props.limit === 1) {
if (fileList.length === 0) {
@ -88,28 +93,35 @@ const onChange = (fileList) => {
}
}
}
// 处理上传成功的文件
if (info.file.status === 'done' && info.file.response) {
handleSuccess(info.file);
} else if (info.file.status === 'error') {
handleError(info.file.error);
}
previousFileListLength = fileList.length;
};
const beforeUpload = (file, files) => {
if (props.limit > 0 && files.length >= props.limit) {
Message.warning(`最多只能上传 ${props.limit} 张图片`);
message.warning(`最多只能上传 ${props.limit} 张图片`);
return false; // 阻止上传
}
return true;
};
const handleError = (error) => {
Message.error('上传失败');
message.error('上传失败');
console.error(error);
};
const customRequest = async (option) => {
const { onProgress, onError, onSuccess, fileItem, name } = option;
const { onProgress, onError, onSuccess, file, name } = option;
try {
// 1. 获取预签名上传URL
const response = await fetchImageUploadFile({ suffix: getFileExtension(fileItem.file.name) });
const response = await fetchImageUploadFile({ suffix: getFileExtension(file.name) });
const preSignedUrl = response?.data?.upload_url;
if (!preSignedUrl) {
@ -117,9 +129,9 @@ const customRequest = async (option) => {
}
console.log('preSignedUrl', preSignedUrl);
// 2. 使用预签名URL上传文件
const blob = new Blob([fileItem.file], { type: fileItem.file.type });
const blob = new Blob([file], { type: file.type });
await axios.put(preSignedUrl, blob, {
headers: { 'Content-Type': fileItem.file.type },
headers: { 'Content-Type': file.type },
});
onSuccess(JSON.stringify(response));

View File

@ -1,5 +1,5 @@
<script lang="tsx">
import { Button } from '@arco-design/web-vue';
import { Button } from 'ant-design-vue';
import { Bubble } from '@/components/xt-chat/xt-bubble';
import Http from '@/api';
@ -79,8 +79,8 @@ export default {
<header class="header flex justify-end items-center mb-16px px-16px">
{hasMediaCenter.value && (
<Button
type="outline"
size="medium"
type="primary"
ghost
class="mr-16px"
v-slots={{ icon: () => <icon-plus size="14" /> }}
onClick={onAddMediaCenter}
@ -90,8 +90,8 @@ export default {
)}
<Button
type="outline"
size="medium"
type="primary"
ghost
class="mr-16px"
v-slots={{ icon: () => <icon-plus size="14" /> }}
onClick={onAddTaskManage}