feat: 钉钉、企业微信webbook、企业微信应用api,问题:交互

This commit is contained in:
2026-07-21 14:01:21 +08:00
parent 2e7bc80a11
commit 4224130ad4
21 changed files with 1915 additions and 513 deletions

View File

@@ -42,6 +42,14 @@ const pageSize = ref(24);
const pageSizeOptions = ['12', '24', '48'];
const selected = ref<string[]>([]);
/** 按 acceptTypes 隔离 localStorage,避免图片/视频选择器互相污染 */
const storageKey = computed(
() =>
`file-picker-type-${
props.acceptTypes?.length ? props.acceptTypes.join('-') : 'all'
}`,
);
const {
activeType,
keyword,
@@ -50,12 +58,10 @@ const {
buildListParams,
fileTypes,
typesLoading,
} = useFileGalleryFilter('file-picker-active-type');
} = useFileGalleryFilter(() => storageKey.value);
/**
* 实际渲染的 tabs(按 acceptTypes 过滤)
* - acceptTypes 为空:返回 tabs 原始值(含「全部」+ 所有类型)
* - acceptTypes 非空:只保留匹配的类型 tab(不含「全部」,强制选具体类型)
*/
const visibleTabs = computed(() => {
if (!props.acceptTypes || props.acceptTypes.length === 0) {
@@ -67,30 +73,40 @@ const visibleTabs = computed(() => {
});
/**
* acceptTypes 变化时锁定到第一个匹配的类型(避免停留在「全部」导致看到非目标类型文件)
* 强制锁定到 acceptTypes 首项(打开弹窗 / 类型字典就绪时调用)
*/
watch(
() => props.acceptTypes,
(arr) => {
if (!arr || arr.length === 0) {
return;
}
if (arr.includes(activeType.value)) {
return;
}
function lockAcceptType() {
const arr = props.acceptTypes;
if (!arr || arr.length === 0) {
return;
}
if (activeType.value !== arr[0]) {
setActiveType(arr[0]!);
page.value = 1;
void load();
},
{ immediate: true },
);
}
}
let searchTimer: ReturnType<typeof setTimeout> | null = null;
/**
* 是否允许拉列表:有 acceptTypes 时需等类型字典加载完且 visibleTabs 非空
*/
function canLoadList() {
if (typesLoading.value) {
return false;
}
if (props.acceptTypes && props.acceptTypes.length > 0) {
return visibleTabs.value.length > 0;
}
return true;
}
async function load() {
if (!canLoadList()) {
return;
}
loading.value = true;
try {
const res = await getFileGalleryList(buildListParams(page.value, pageSize.value));
const res = await getFileGalleryList(
buildListParams(page.value, pageSize.value),
);
const data = (res as any)?.data ?? res;
items.value = data?.items ?? [];
total.value = data?.total ?? 0;
@@ -99,21 +115,37 @@ async function load() {
}
}
watch(
() => props.acceptTypes,
(arr) => {
if (!arr || arr.length === 0) {
return;
}
lockAcceptType();
page.value = 1;
void load();
},
{ immediate: true },
);
let searchTimer: ReturnType<typeof setTimeout> | null = null;
watch(
() => props.open,
(val) => {
if (val) {
selected.value = [];
page.value = 1;
if (!typesLoading.value) {
void load();
}
// 打开时再次按 acceptTypes 重锁,避免沿用其它选择器的视频 tab
lockAcceptType();
void load();
}
},
);
watch(typesLoading, (val) => {
if (!val && props.open) {
lockAcceptType();
void load();
}
});
@@ -184,6 +216,11 @@ function onSearch(value: string) {
function itemIcon(item: FileGalleryItem) {
return item.type_icon || resolveTypeIcon(item.type, fileTypes.value);
}
/** 图片类型展示缩略图(xk_file_type:1=图片) */
function isImageItem(item: FileGalleryItem) {
return Number(item.type) === 1 || Number(item.type) === 0;
}
</script>
<template>
@@ -217,7 +254,7 @@ function itemIcon(item: FileGalleryItem) {
/>
</div>
<Spin :spinning="loading">
<Spin :spinning="loading || typesLoading">
<div v-if="items.length" class="gallery-grid">
<div
v-for="item in items"
@@ -226,10 +263,17 @@ function itemIcon(item: FileGalleryItem) {
:class="{ active: isSelected(item.url) }"
@click="toggleSelect(item.url)"
>
<img v-if="item.type === 0" :src="item.url" alt="" class="gallery-thumb" />
<img
v-if="isImageItem(item)"
:src="item.url"
alt=""
class="gallery-thumb"
/>
<div v-else class="gallery-file-icon">
<MIcon :icon="itemIcon(item)" size="28" />
<span class="file-name">{{ item.file_name || item.original_name || '未命名' }}</span>
<span class="file-name">{{
item.file_name || item.original_name || '未命名'
}}</span>
</div>
<div class="gallery-meta">
<div class="file-title">

View File

@@ -24,12 +24,17 @@ interface Props {
modelValue: string[];
multiple?: boolean;
maxCount?: number;
/**
* 素材库可选类型(透传 GalleryPickLink),默认仅图片
*/
acceptTypes?: number[];
}
const props = withDefaults(defineProps<Props>(), {
modelValue: () => [],
multiple: true,
maxCount: 9,
acceptTypes: () => [1],
});
const emit = defineEmits<{
@@ -184,6 +189,7 @@ const galleryRemainCount = computed(() => Math.max(props.maxCount - fileList.val
v-if="showUploadButton"
:multiple="multiple"
:max-count="galleryRemainCount"
:accept-types="acceptTypes"
@select="onGallerySelect"
/>

View File

@@ -1,8 +1,14 @@
<script setup lang="ts">
/**
* OSS 文件上传(单文件 URL 字符串)
* 可选:从素材库选择(acceptTypes 过滤,如文件=6、语音=3)
*/
import { computed, ref, watch } from 'vue';
import { preferences } from '@vben/preferences';
import { Button, Upload } from 'ant-design-vue';
import GalleryPickLink from '#/components/form/components/gallery-pick-link.vue';
import { Icon } from '#/components/icon';
import { uploadFile } from '#/api/core/upload';
import { uploadToOss } from '#/utils/oss-upload';
@@ -17,11 +23,17 @@ interface FileItem {
interface Props {
modelValue?: string;
maxCount?: number;
/** 素材库类型过滤,如 [6] 文件、[3] 语音 */
acceptTypes?: number[];
/** input accept,如 .pdf,.doc 或 audio/* */
accept?: string;
}
const props = withDefaults(defineProps<Props>(), {
modelValue: '',
maxCount: 1,
acceptTypes: () => [6],
accept: '.pdf,.doc,.docx,.zip,.rar,.txt,application/pdf,audio/*,*/*',
});
const emit = defineEmits<{
@@ -131,26 +143,50 @@ function handleRemove() {
return true;
}
function onGallerySelect(urls: string[]) {
const url = urls[0] || '';
if (!url) return;
syncFileListFromValue(url);
updateModelValue();
}
const showUploadButton = computed(
() => fileList.value.filter((item) => item.status !== 'error').length === 0,
);
</script>
<template>
<Upload
:file-list="fileList"
:before-upload="beforeUpload"
:custom-request="customRequest"
:max-count="maxCount"
list-type="text"
accept=".pdf,.jpg,.jpeg,.png,.doc,.docx,image/*,application/pdf"
@remove="handleRemove"
>
<Button v-if="showUploadButton">
上传文件
<template #icon>
<Icon icon="ant-design:cloud-upload-outlined" />
</template>
</Button>
</Upload>
<div class="upload-oss-file-wrap">
<Upload
:file-list="fileList"
:before-upload="beforeUpload"
:custom-request="customRequest"
:max-count="maxCount"
list-type="text"
:accept="accept"
@remove="handleRemove"
>
<Button v-if="showUploadButton">
上传文件
<template #icon>
<Icon icon="ant-design:cloud-upload-outlined" />
</template>
</Button>
</Upload>
<GalleryPickLink
v-if="showUploadButton"
:max-count="1"
:accept-types="acceptTypes"
@select="onGallerySelect"
/>
</div>
</template>
<style scoped>
.upload-oss-file-wrap {
display: flex;
flex-direction: column;
gap: 8px;
align-items: flex-start;
}
</style>