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

This commit is contained in:
2026-07-23 14:15:17 +08:00
parent 2b13a19731
commit 777737d063
32 changed files with 6047 additions and 774 deletions

View File

@@ -0,0 +1,170 @@
<script setup lang="ts">
/**
* 通用 Tag 选择器(带头像)
*
* - 基于 antd Selectmultiple 模式默认开启,单选传 multiple=false
* - 已选项以 Tag 形式展示头像+名字:
* - 有 avatar 用图片头像
* - 无 avatar 用文本头像(取 label 首字符)
* - 下拉选项同样展示头像+名字
* - 选项数据options: { value, label, avatar? }[]
*/
import { computed } from 'vue';
import { useVModel } from '@vueuse/core';
import { Avatar, Select, Tag } from 'ant-design-vue';
defineOptions({
name: 'UserTagSelect',
inheritAttrs: false,
});
/** 单个选项结构 */
export interface UserTagOption {
value: string | number;
label: string;
avatar?: string;
}
const props = withDefaults(
defineProps<{
/** 已选值数组(多选)或单值(单选时仍用数组包装) */
modelValue?: (string | number)[];
/** 选项列表 */
options: UserTagOption[];
/** 是否多选,默认 true */
multiple?: boolean;
/** 最大选择数,超出截断 */
maxCount?: number;
placeholder?: string;
allowClear?: boolean;
showSearch?: boolean;
disabled?: boolean;
}>(),
{
modelValue: () => [],
multiple: true,
maxCount: 99,
placeholder: '请选择',
allowClear: true,
showSearch: true,
disabled: false,
},
);
const emit = defineEmits<{
'update:modelValue': [v: (string | number)[]];
}>();
// 双向绑定useVModel 自动处理 modelValue 的读写
const innerValue = useVModel(props, 'modelValue', emit, {
defaultValue: [] as (string | number)[],
passive: true,
});
/** 用于 Select 的 mode单选时不传 mode */
const selectMode = computed(() => (props.multiple ? 'multiple' : undefined));
/** 按 value 索引选项,方便 tagRender/option 插槽快速查头像与名字 */
const optionMap = computed(() => {
const map = new Map<string | number, UserTagOption>();
for (const opt of props.options) {
map.set(opt.value, opt);
}
return map;
});
/** 取某 value 对应的展示名 */
function labelOf(value: string | number) {
return optionMap.value.get(value)?.label || String(value);
}
/** 取某 value 对应的头像 */
function avatarOf(value: string | number) {
return optionMap.value.get(value)?.avatar;
}
/** 取首字符(文本头像) */
function firstChar(label?: string) {
return (label || '?').trim().charAt(0) || '?';
}
/** 选项过滤:按 label 包含输入关键字 */
function filterOption(input: string, option: any) {
const v = String(option?.label || '').toLowerCase();
return v.includes((input || '').toLowerCase());
}
/**
* change 处理:
* - 单选:把单值包成数组
* - 多选:超 maxCount 截断
*/
function onChange(v: any) {
if (!props.multiple) {
innerValue.value = v == null ? [] : [v];
return;
}
if (Array.isArray(v) && v.length > props.maxCount) {
v = v.slice(0, props.maxCount);
}
innerValue.value = v;
}
</script>
<template>
<Select
:value="innerValue as any"
:mode="selectMode"
:options="options"
:placeholder="placeholder"
:allow-clear="allowClear"
:show-search="showSearch"
:filter-option="filterOption"
:disabled="disabled"
:max-tag-count="10"
option-filter-prop="label"
style="width: 100%"
@change="onChange"
>
<!-- 多选已选项展示头像+名字 -->
<template v-if="multiple" #tagRender="{ value, closable, onClose }">
<Tag :closable="closable" @close="onClose" class="user-tag-chip">
<Avatar :size="16" :src="avatarOf(value)">
{{ firstChar(labelOf(value)) }}
</Avatar>
<span class="user-tag-name">{{ labelOf(value) }}</span>
</Tag>
</template>
<!-- 下拉选项展示头像+名字 -->
<template #option="{ label, avatar }">
<div class="user-tag-option">
<Avatar :size="20" :src="avatar">{{ firstChar(label) }}</Avatar>
<span>{{ label }}</span>
</div>
</template>
</Select>
</template>
<style scoped>
.user-tag-chip {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 2px 6px;
}
.user-tag-name {
max-width: 80px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.user-tag-option {
display: flex;
align-items: center;
gap: 8px;
}
</style>

View File

@@ -40,8 +40,8 @@ defineProps<{
flex-direction: column;
width: 200px;
height: 100%;
border-right: 1px solid #e5e6eb;
background: #fafbfc;
border-right: 1px solid var(--ant-color-border-secondary, #e5e6eb);
background: var(--ant-color-fill-quaternary, #fafbfc);
}
.panel-header {
@@ -49,16 +49,16 @@ defineProps<{
align-items: center;
justify-content: space-between;
padding: 12px 16px;
border-bottom: 1px solid #e5e6eb;
border-bottom: 1px solid var(--ant-color-border-secondary, #e5e6eb);
font-size: 13px;
.title {
font-weight: 600;
color: #1d2129;
color: var(--ant-color-text, #1d2129);
}
.hint {
color: #86909c;
color: var(--ant-color-text-tertiary, #86909c);
font-size: 11px;
}
}
@@ -75,19 +75,19 @@ defineProps<{
gap: 8px;
padding: 10px 12px;
margin-bottom: 6px;
border: 1px dashed #c9cdd4;
border: 1px dashed var(--ant-color-border, #c9cdd4);
border-radius: 4px;
background: #fff;
background: var(--ant-color-bg-container, #fff);
font-size: 13px;
color: #4e5969;
color: var(--ant-color-text-secondary, #4e5969);
cursor: grab;
user-select: none;
transition: all 0.15s;
&:hover {
border-color: #165dff;
color: #165dff;
background: #f2f7ff;
border-color: var(--ant-color-primary, #165dff);
color: var(--ant-color-primary, #165dff);
background: var(--ant-color-primary-bg, #f2f7ff);
}
&:active {
@@ -102,34 +102,7 @@ defineProps<{
.empty-hint {
padding: 24px 12px;
text-align: center;
color: #c9cdd4;
color: var(--ant-color-text-quaternary, #c9cdd4);
font-size: 12px;
}
.dark {
.block-library {
background: #1f1f1f;
border-right-color: #374151;
}
.panel-header {
border-bottom-color: #374151;
.title {
color: #e5e7eb;
}
}
.block-item {
background: #374151;
border-color: #4b5563;
color: #d1d5db;
&:hover {
border-color: #60a5fa;
color: #60a5fa;
background: rgba(96, 165, 250, 0.1);
}
}
}
</style>

View File

@@ -159,7 +159,7 @@ function handleRemove(instanceId: string) {
flex-direction: column;
flex: 1;
min-width: 0;
background: #fff;
background: var(--ant-color-bg-container, #fff);
}
.panel-header {
@@ -167,16 +167,16 @@ function handleRemove(instanceId: string) {
align-items: center;
justify-content: space-between;
padding: 12px 16px;
border-bottom: 1px solid #e5e6eb;
border-bottom: 1px solid var(--ant-color-border-secondary, #e5e6eb);
font-size: 13px;
.title {
font-weight: 600;
color: #1d2129;
color: var(--ant-color-text, #1d2129);
}
.hint {
color: #86909c;
color: var(--ant-color-text-tertiary, #86909c);
font-size: 11px;
}
}
@@ -199,22 +199,22 @@ function handleRemove(instanceId: string) {
font-size: 12px;
.group-label {
color: #4e5969;
color: var(--ant-color-text-secondary, #4e5969);
font-weight: 500;
font-family: 'JetBrains Mono', monospace;
}
.group-count {
color: #86909c;
color: var(--ant-color-text-tertiary, #86909c);
}
}
.group-dropzone {
min-height: 60px;
padding: 8px;
border: 2px dashed #d9d9d9;
border: 2px dashed var(--ant-color-border, #d9d9d9);
border-radius: 6px;
background: #fafbfc;
background: var(--ant-color-fill-quaternary, #fafbfc);
}
.empty-dropzone {
@@ -222,7 +222,7 @@ function handleRemove(instanceId: string) {
align-items: center;
justify-content: center;
min-height: 44px;
color: #c9cdd4;
color: var(--ant-color-text-quaternary, #c9cdd4);
font-size: 12px;
}
@@ -232,9 +232,9 @@ function handleRemove(instanceId: string) {
justify-content: space-between;
padding: 8px 10px;
margin-bottom: 6px;
border: 1px solid #e5e6eb;
border: 1px solid var(--ant-color-border-secondary, #e5e6eb);
border-radius: 4px;
background: #fff;
background: var(--ant-color-bg-container, #fff);
cursor: pointer;
transition: all 0.15s;
@@ -243,12 +243,12 @@ function handleRemove(instanceId: string) {
}
&:hover {
border-color: #165dff;
border-color: var(--ant-color-primary, #165dff);
}
&.selected {
border-color: #165dff;
background: #f2f7ff;
border-color: var(--ant-color-primary, #165dff);
background: var(--ant-color-primary-bg, #f2f7ff);
box-shadow: 0 0 0 2px rgba(22, 93, 255, 0.15);
}
}
@@ -268,13 +268,13 @@ function handleRemove(instanceId: string) {
.instance-label {
font-size: 13px;
color: #1d2129;
color: var(--ant-color-text, #1d2129);
font-weight: 500;
}
.instance-summary {
font-size: 11px;
color: #86909c;
color: var(--ant-color-text-tertiary, #86909c);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
@@ -286,52 +286,15 @@ function handleRemove(instanceId: string) {
border: none;
border-radius: 50%;
background: transparent;
color: #86909c;
color: var(--ant-color-text-tertiary, #86909c);
font-size: 16px;
line-height: 1;
cursor: pointer;
transition: all 0.15s;
&:hover {
background: #f53f3f;
background: var(--ant-color-error, #f53f3f);
color: #fff;
}
}
.dark {
.canvas {
background: #1f1f1f;
}
.panel-header {
border-bottom-color: #374151;
.title {
color: #e5e7eb;
}
}
.group-dropzone {
background: #1f1f1f;
border-color: #374151;
}
.instance-item {
background: #374151;
border-color: #4b5563;
&:hover {
border-color: #60a5fa;
}
&.selected {
border-color: #60a5fa;
background: rgba(96, 165, 250, 0.1);
}
}
.instance-label {
color: #e5e7eb;
}
}
</style>

View File

@@ -68,7 +68,8 @@ async function handleCopy() {
flex-direction: column;
width: 280px;
height: 100%;
border-left: 1px solid #e5e6eb;
border-left: 1px solid var(--ant-color-border-secondary, #e5e6eb);
/* JSON 代码面板保留 VS Code Dark+ 风格暗底(无论浅色/暗色主题) */
background: #1e1e1e;
}

View File

@@ -181,7 +181,7 @@ watch(
read-only
/>
<GalleryPickLink
:accept-types="field.acceptTypes || [1]"
:accept-types="field.acceptTypes || [0]"
@select="(urls) => setFixedValue(field.name, urls[0] || '')"
/>
</div>
@@ -254,7 +254,7 @@ watch(
read-only
/>
<GalleryPickLink
:accept-types="field.acceptTypes || [1]"
:accept-types="field.acceptTypes || [0]"
@select="(urls) => setInstanceValue(field.name, urls[0] || '')"
/>
</div>
@@ -278,8 +278,8 @@ watch(
flex-direction: column;
width: 280px;
height: 100%;
border-left: 1px solid #e5e6eb;
background: #fff;
border-left: 1px solid var(--ant-color-border-secondary, #e5e6eb);
background: var(--ant-color-bg-container, #fff);
}
.panel-header {
@@ -287,16 +287,16 @@ watch(
align-items: center;
justify-content: space-between;
padding: 12px 16px;
border-bottom: 1px solid #e5e6eb;
border-bottom: 1px solid var(--ant-color-border-secondary, #e5e6eb);
font-size: 13px;
.title {
font-weight: 600;
color: #1d2129;
color: var(--ant-color-text, #1d2129);
}
.hint {
color: #86909c;
color: var(--ant-color-text-tertiary, #86909c);
font-size: 11px;
max-width: 120px;
overflow: hidden;
@@ -318,10 +318,10 @@ watch(
.section-title {
margin-bottom: 12px;
padding-bottom: 6px;
border-bottom: 1px solid #f2f3f5;
border-bottom: 1px solid var(--ant-color-split, #f2f3f5);
font-size: 12px;
font-weight: 600;
color: #4e5969;
color: var(--ant-color-text-secondary, #4e5969);
}
.form-row {
@@ -332,11 +332,11 @@ watch(
display: block;
margin-bottom: 4px;
font-size: 12px;
color: #4e5969;
color: var(--ant-color-text-secondary, #4e5969);
.required {
margin-left: 2px;
color: #f53f3f;
color: var(--ant-color-error, #f53f3f);
}
}
@@ -345,28 +345,4 @@ watch(
flex-direction: column;
gap: 4px;
}
.dark {
.property-panel {
background: #1f1f1f;
border-left-color: #374151;
}
.panel-header {
border-bottom-color: #374151;
.title {
color: #e5e7eb;
}
}
.section-title {
border-bottom-color: #374151;
color: #d1d5db;
}
.form-label {
color: #d1d5db;
}
}
</style>

View File

@@ -493,10 +493,10 @@ defineExpose({
flex-direction: column;
width: 100%;
height: 480px;
border: 1px solid #e5e6eb;
border: 1px solid var(--ant-color-border-secondary, #e5e6eb);
border-radius: 6px;
overflow: hidden;
background: #fff;
background: var(--ant-color-bg-container, #fff);
}
.editor-toolbar {
@@ -504,13 +504,13 @@ defineExpose({
align-items: center;
justify-content: space-between;
padding: 8px 12px;
border-bottom: 1px solid #e5e6eb;
background: #fafbfc;
border-bottom: 1px solid var(--ant-color-border-secondary, #e5e6eb);
background: var(--ant-color-fill-quaternary, #fafbfc);
font-size: 13px;
.title {
font-weight: 600;
color: #1d2129;
color: var(--ant-color-text, #1d2129);
}
}
@@ -519,20 +519,4 @@ defineExpose({
flex: 1;
min-height: 0;
}
.dark {
.template-editor {
background: #1f1f1f;
border-color: #374151;
}
.editor-toolbar {
background: #1f1f1f;
border-bottom-color: #374151;
.title {
color: #e5e7eb;
}
}
}
</style>