liu
2026-09-29 982c97ab82f3beb1b4febcf014562eda34b9fc05
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
<script lang="ts" setup>
import type { WxCpMemberApi } from '#/api/wxcp/member';
 
import { computed, onMounted, ref, watch } from 'vue';
 
import { Alert, Radio, Select, Spin } from 'ant-design-vue';
 
import {
  getWxCpDepartmentList,
  getWxCpMemberList,
} from '#/api/wxcp/member';
 
import { PUSH_SCOPE_OPTIONS, PUSH_SCOPE } from '../data';
 
defineOptions({ name: 'WxCpPushScopePicker' });
 
interface Props {
  /** 推送范围:0全员 1指定部门 2指定成员 */
  scope?: number;
  /** 推送目标,逗号分隔;范围是全员时无意义 */
  target?: string;
  disabled?: boolean;
}
 
const props = withDefaults(defineProps<Props>(), {
  scope: PUSH_SCOPE.ALL,
  target: '',
  disabled: false,
});
 
const emit = defineEmits<{
  'update:scope': [value: number];
  'update:target': [value: string];
}>();
 
/** 通讯录数据:只在首次需要时拉取,避免每次打开弹窗都打企业微信接口 */
const departments = ref<WxCpMemberApi.Department[]>([]);
const members = ref<WxCpMemberApi.Member[]>([]);
const loading = ref(false);
const loaded = ref(false);
const loadError = ref('');
 
async function ensureContacts() {
  if (loaded.value || loading.value) {
    return;
  }
  loading.value = true;
  loadError.value = '';
  try {
    const [deptList, memberList] = await Promise.all([
      getWxCpDepartmentList(),
      getWxCpMemberList({ fetchChild: true }),
    ]);
    departments.value = deptList ?? [];
    members.value = memberList ?? [];
    loaded.value = true;
  } catch (error: unknown) {
    loadError.value =
      (error as Error)?.message || '加载企业微信通讯录失败,请稍后重试';
  } finally {
    loading.value = false;
  }
}
 
// 选到需要通讯录的范围时才加载
watch(
  () => props.scope,
  (scope) => {
    if (scope === PUSH_SCOPE.PARTY || scope === PUSH_SCOPE.USER) {
      ensureContacts();
    }
  },
);
 
onMounted(() => {
  if (props.scope === PUSH_SCOPE.PARTY || props.scope === PUSH_SCOPE.USER) {
    ensureContacts();
  }
});
 
const deptOptions = computed(() =>
  departments.value.map((d) => ({ label: d.name || `部门${d.id}`, value: d.id })),
);
 
const memberOptions = computed(() =>
  members.value.map((m) => ({
    label: m.name ? `${m.name}(${m.userId})` : m.userId,
    value: m.userId,
  })),
);
 
/** 目标字符串 <-> 数组 */
const selected = computed<string[] | number[]>({
  get() {
    const raw = (props.target || '').trim();
    if (!raw) {
      return [];
    }
    const parts = raw
      .split(/[,,]/)
      .map((s) => s.trim())
      .filter(Boolean);
    // 部门用数字,成员用字符串(成员账号是 LiuYi 这种)
    return props.scope === PUSH_SCOPE.PARTY
      ? parts.map((p) => Number(p)).filter((n) => Number.isFinite(n))
      : parts;
  },
  set(value) {
    emit('update:target', (value as (number | string)[]).join(','));
  },
});
 
function handleScopeChange(e: unknown) {
  const value = Number((e as { target?: { value?: number } })?.target?.value);
  emit('update:scope', Number.isFinite(value) ? value : PUSH_SCOPE.ALL);
  // 切换范围时清掉旧目标,避免把成员账号当成部门ID提交
  emit('update:target', '');
}
</script>
 
<template>
  <div>
    <Radio.Group
      :value="scope"
      :disabled="disabled"
      button-style="solid"
      @change="handleScopeChange"
    >
      <Radio.Button
        v-for="opt in PUSH_SCOPE_OPTIONS"
        :key="opt.value"
        :value="opt.value"
      >
        {{ opt.label }}
      </Radio.Button>
    </Radio.Group>
 
    <div v-if="scope !== PUSH_SCOPE.ALL" class="mt-3">
      <Spin :spinning="loading">
        <Alert
          v-if="loadError"
          :message="loadError"
          type="warning"
          show-icon
          class="mb-2"
        />
        <Select
          v-model:value="selected"
          :options="scope === PUSH_SCOPE.PARTY ? deptOptions : memberOptions"
          :disabled="disabled || !!loadError"
          :mode="scope === PUSH_SCOPE.PARTY ? 'multiple' : 'multiple'"
          :placeholder="
            scope === PUSH_SCOPE.PARTY
              ? '请选择部门,可多选'
              : '请选择成员,可多选'
          "
          option-filter-prop="label"
          show-search
          allow-clear
          class="w-full"
        />
      </Spin>
      <div class="mt-2 text-xs text-gray-400">
        {{
          scope === PUSH_SCOPE.PARTY
            ? '推送给所选部门下的全部成员'
            : '推送给所选成员,账号来自企业微信通讯录'
        }}
      </div>
    </div>
  </div>
</template>