liu
3 天以前 fe2d6627b8f1002af7a31b08f6f43bec034d04ee
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
<script lang="ts" setup>
import type { TableColumnsType } from 'ant-design-vue';
import type { DefaultOptionType } from 'ant-design-vue/es/select';
import type { ResearchApplicationApi } from '#/api/research/application';
 
import { computed, ref } from 'vue';
 
import { downloadFileFromBlobPart } from '@vben/utils';
 
import { DICT_TYPE } from '@vben/constants';
import { getDictOptions } from '@vben/hooks';
 
import {
  Button,
  Input,
  Modal,
  Select,
  Table,
  message,
} from 'ant-design-vue';
 
import {
  exportMemberStatistics,
  getMemberStatistics,
} from '#/api/research/application';
 
defineOptions({ name: 'ResearchMemberStatisticsModal' });
 
const open = ref(false);
const loading = ref(false);
const keyword = ref('');
const status = ref<number | undefined>();
const rows = ref<ResearchApplicationApi.MemberStatistics[]>([]);
 
const statusOptions = getDictOptions(
  DICT_TYPE.RESEARCH_APPLICATION_STATUS,
  'number',
) as DefaultOptionType[];
 
const columns: TableColumnsType<ResearchApplicationApi.MemberStatistics> = [
  { title: '成员姓名', dataIndex: 'name', key: 'name', width: 110, fixed: 'left' },
  { title: '联系电话', dataIndex: 'contactPhone', key: 'contactPhone', width: 130 },
  { title: '所属单位', dataIndex: 'workUnit', key: 'workUnit', width: 180 },
  { title: '职称/职务', dataIndex: 'jobTitle', key: 'jobTitle', width: 120 },
  { title: '成员角色', dataIndex: 'memberRoles', key: 'memberRoles', width: 140 },
  { title: '参与课题数', dataIndex: 'projectCount', key: 'projectCount', width: 110 },
  { title: '参与课题', dataIndex: 'projectTitles', key: 'projectTitles', ellipsis: true },
];
 
const totalMembers = computed(() => rows.value.length);
 
const queryParams = computed(() => ({
  keyword: keyword.value.trim() || undefined,
  status: status.value,
}));
 
async function loadData() {
  loading.value = true;
  try {
    rows.value = (await getMemberStatistics(queryParams.value)) ?? [];
  } finally {
    loading.value = false;
  }
}
 
function handleOpen() {
  open.value = true;
  keyword.value = '';
  status.value = undefined;
  loadData();
}
 
async function handleExport() {
  const data = await exportMemberStatistics(queryParams.value);
  downloadFileFromBlobPart({ fileName: '课题成员统计.xls', source: data });
}
 
/** 生成可直接发到微信群/企业微信的名单文本 */
function buildPublishText() {
  const lines = ['【课题成员名单】'];
  rows.value.forEach((row, index) => {
    const meta = [row.workUnit, row.jobTitle].filter(Boolean).join('/');
    const metaText = meta ? `(${meta})` : '';
    const roles = row.memberRoles ? `[${row.memberRoles}]` : '';
    const projects = row.projectTitles ? `:${row.projectTitles}` : '';
    lines.push(
      `${index + 1}. ${row.name}${metaText}${roles} 参与课题 ${row.projectCount ?? 0} 项${projects}`,
    );
  });
  return lines.join('\n');
}
 
async function handleCopy() {
  if (rows.value.length === 0) {
    message.warning('暂无可发布的成员数据');
    return;
  }
  const text = buildPublishText();
  try {
    await navigator.clipboard.writeText(text);
    message.success('成员名单已复制,可直接粘贴到群内发布');
  } catch {
    // 非 HTTPS 环境下 clipboard 不可用时,退化为手动选择复制
    Modal.info({
      title: '请手动复制以下名单',
      content: text,
      width: 640,
    });
  }
}
 
defineExpose({ open: handleOpen });
</script>
 
<template>
  <Modal
    v-model:open="open"
    title="课题成员统计(按成员维度)"
    width="1080px"
    :footer="null"
  >
    <div class="mb-3 flex flex-wrap items-center gap-2">
      <Input
        v-model:value="keyword"
        placeholder="姓名 / 电话 / 单位"
        allow-clear
        style="width: 220px"
        @press-enter="loadData"
      />
      <Select
        v-model:value="status"
        :options="statusOptions"
        placeholder="申报状态(全部)"
        allow-clear
        style="width: 180px"
      />
      <Button type="primary" @click="loadData">查询</Button>
      <div class="flex-1" />
      <span class="text-sm text-gray-500">共 {{ totalMembers }} 名成员</span>
      <Button @click="handleCopy">复制群发名单</Button>
      <Button @click="handleExport">导出 Excel</Button>
    </div>
 
    <Table
      :columns="columns"
      :data-source="rows"
      :loading="loading"
      :pagination="{ pageSize: 10, showSizeChanger: true }"
      :scroll="{ x: 1100, y: 420 }"
      bordered
      size="small"
    />
 
    <div class="mt-2 text-xs text-gray-400">
      统计口径:同一「姓名 + 电话 + 单位」视为同一成员;参与课题数按不重复课题计数。
    </div>
  </Modal>
</template>