批量推送

This commit is contained in:
郭庆泰
2026-08-16 21:24:53 +08:00
parent 0c093076a4
commit ccfd03a6eb
55 changed files with 3772 additions and 577 deletions
+45 -142
View File
@@ -69,7 +69,7 @@
</el-col>
<el-col :span="12">
<el-form-item label="支持单位">
<el-select v-model="form.orgId" placeholder="请选择赞助公司" filterable clearable style="width:100%" @change="onSupporterPick">
<el-select v-model="form.sponsorAdminUserId" placeholder="请选择赞助方负责人" filterable clearable style="width:100%" @change="onSupporterPick">
<el-option v-for="u in supporterOptions" :key="u.userId"
:label="`${u.userName}${u.nickName ? ' (' + u.nickName + ')' : ''}`"
:value="u.userId" />
@@ -104,37 +104,21 @@
<!-- ========== 第三区块公告文件 ========== -->
<div class="new-card-title">公告文件</div>
<p class="hint-text">*勾选公告类型后上传对应文件发布公告时可同时发布多个类型</p>
<p class="hint-text">*支持 PDF / 图片, 单文件 10MB; 名称规则: 公告文件类型 + 项目编号后两段 + 项目名称</p>
<div class="notice-list">
<div v-for="(n, idx) in form.notices" :key="idx" class="notice-row">
<el-checkbox v-model="n.enabled" :label="n.key" class="notice-check">
{{ n.label }}
</el-checkbox>
<el-upload
v-if="n.enabled"
<span class="notice-label">{{ n.label }}</span>
<oss-file-uploader
class="notice-uploader"
:show-file-list="true"
:file-list="getFileList(n)"
:before-upload="(file) => beforeUpload(file, n)"
:http-request="(opts) => httpUpload(opts, n, idx)"
:on-remove="(file) => onRemove(n, file)"
:on-preview="(file) => onPreview(file)"
accept=".pdf,.doc,.docx"
drag
action="#"
>
<i class="el-icon-upload"></i>
<div class="el-upload__text">
将文件拖到此处,或<em>点击上传</em>
</div>
<div class="el-upload__tip" slot="tip">
支持 PDF / Word,单文件 ≤ 20MB
</div>
</el-upload>
<div v-else class="notice-disabled">勾选「{{ n.label }}」后可上传文件</div>
v-model="n.url"
:dir="`ry8080/project/${n.key}/`"
accept=".pdf,.png,.jpg,.jpeg,.gif,.webp"
:placeholder="`点击上传${n.label}`"
hint="支持 PDF / 图片"
block
/>
</div>
</div>
<p class="hint-text">*名称规则:公告文件类型+取项目编号的后两段+项目名称</p>
</el-form>
<!-- 底部按钮 -->
@@ -152,8 +136,8 @@ import { useRoute, useRouter } from 'vue-router'
import { bizAdd, bizUpdate, bizGet } from '@/api/public'
import { listExecutor, listSupporters } from '@/api/system'
import { ElMessage, ElMessageBox } from 'element-plus'
import { uploadToOss } from '@/utils/oss'
import request from '@/utils/request'
import OssFileUploader from '@/components/OssFileUploader.vue'
const router = useRouter()
const route = useRoute()
@@ -161,12 +145,10 @@ const isEdit = !!route.params.projectId
const projectId = route.params.projectId || null
const formRef = ref(null)
const submitting = ref(false)
const uploadingIdx = ref(-1)
const currentUploadN = ref(null)
const fileInputRef = ref(null)
function defaultRoleRow() { return { role: '', customName: '', amount: 0 } }
function defaultNotice(key, label) { return { key, label, url: '', enabled: false, name: '' } }
function defaultNotice(key, label) { return { key, label, url: '', name: '' } }
// 支持方候选 (sponsor 角色)
const supporterOptions = ref([])
@@ -175,19 +157,24 @@ const form = reactive({
projectName: '', projectNo: '', projectForm: '',
totalSessions: 0, totalAmount: 0, manageFee: 0,
startTime: '', endTime: '',
// 赞助公司 (TODO 后续: 下拉源应改为 biz_org,orgType='sponsor' 列表, 当前是 sys_user)
orgId: null,
orgName: '',
// 赞助方负责人 (sys_user.role_type='sponsor', 原 org_id/org_name 字段已重命名)
sponsorAdminUserId: null,
sponsorAdminUserName: '',
// 提交截止(天)
submitDeadlineDays: 30,
roleRows: Array(6).fill(0).map(() => defaultRoleRow()),
// 公告附件(每个 notice 含 enabled/type/label/url/name)
// enabled: 是否启用该公告类型(对应原型 checkbox 邀请函/支持函/通知/日程)
roleRows: [
{ role: '主席', customName: '', amount: 0 },
{ role: '主持', customName: '', amount: 0 },
{ role: '讲者', customName: '', amount: 0 },
{ role: '点评/评审', customName: '', amount: 0 },
{ role: '讨论', customName: '', amount: 0 }
],
// 公告附件 (key/label/url/name, 没有 url 即为空, 不参与提交)
notices: [
{ key: 'invitation', label: '邀请函', enabled: false, url: '', name: '' },
{ key: 'supportLetter', label: '支持函', enabled: false, url: '', name: '' },
{ key: 'notice', label: '通知', enabled: false, url: '', name: '' },
{ key: 'schedule', label: '日程', enabled: false, url: '', name: '' }
{ key: 'invitation', label: '邀请函', url: '', name: '' },
{ key: 'supportLetter', label: '支持函', url: '', name: '' },
{ key: 'notice', label: '通知', url: '', name: '' },
{ key: 'schedule', label: '日程', url: '', name: '' }
]
})
@@ -235,8 +222,8 @@ async function loadProject() {
form.manageFee = p.manageFee || 0
form.startTime = p.startTime || ''
form.endTime = p.endTime || ''
form.orgId = p.orgId || null
form.orgName = p.orgName || ''
form.sponsorAdminUserId = p.sponsorAdminUserId || null
form.sponsorAdminUserName = p.sponsorAdminUserName || ''
form.submitDeadlineDays = p.submitDeadlineDays || 30
// 公告文件反显: 优先 publicityFiles JSON 数组, 否则兼容旧字段
const files = []
@@ -248,7 +235,7 @@ async function loadProject() {
// 按 type 反显到对应 notice 行
for (const n of form.notices) {
const m = files.find(f => f.type === n.key)
if (m) { n.url = m.url || ''; n.name = m.name || ''; n.enabled = !!m.url }
if (m) { n.url = m.url || ''; n.name = m.name || '' }
}
} else {
// 兼容老数据
@@ -262,7 +249,6 @@ async function loadProject() {
for (const n of form.notices) {
n.url = noticeMap[n.key] || ''
n.name = n.url ? (n.url.split('/').pop() || '') : ''
n.enabled = !!n.url
}
}
// 加载已有执行方分配 (暂不实现, 留 hook)
@@ -284,7 +270,7 @@ async function loadSupporters(query = '') {
function onSupporterPick(userId) {
const u = supporterOptions.value.find(x => x.userId === userId)
if (u) form.orgName = u.userName || ''
if (u) form.sponsorAdminUserName = u.userName || ''
}
@@ -294,81 +280,6 @@ onMounted(() => {
loadProject()
loadSupporters('')
})
function beforeUpload(file, n) {
// 文件大小校验: 限制 20MB
const maxSize = 20 * 1024 * 1024
if (file.size > maxSize) {
ElMessage.error('文件大小不能超过 20MB')
return false
}
// 文件类型校验
const accept = ['.pdf', '.doc', '.docx', '.png', '.jpg', '.jpeg']
const ext = '.' + file.name.split('.').pop().toLowerCase()
if (!accept.includes(ext)) {
ElMessage.error('仅支持 PDF/Word/图片格式')
return false
}
return true
}
async function httpUpload(opts, n, idx) {
const file = opts.file
uploadingIdx.value = idx
currentUploadN.value = n
const dir = 'ry8080/project/' + n.key + '/'
try {
const url = await uploadToOss(file, dir)
n.url = url
n.name = file.name
n.enabled = true
ElMessage.success(`${n.label} 上传成功`)
} catch (err) {
ElMessage.error(`上传失败: ${err.message || err}`)
n.url = ''
n.name = ''
} finally {
uploadingIdx.value = -1
currentUploadN.value = null
}
}
// el-upload 内置 file-list 需要从 n.url 转换成 {name, url} 对象
function getFileList(n) {
if (!n.url) return []
// 从 url 截取文件名
const url = n.url
const name = url.split('/').pop() || `${n.label}.pdf`
return [{ name, url }]
}
function onPreview(file) {
if (file.url) window.open(file.url, '_blank')
}
function onRemove(n, file) {
n.url = ''
}
function previewFile(n) {
if (n.url) window.open(n.url, '_blank')
}
function downloadFile(n) {
if (!n.url) return
// 触发浏览器下载
const a = document.createElement('a')
a.href = n.url
a.download = n.name || (n.label + '.pdf')
a.target = '_blank'
document.body.appendChild(a)
a.click()
document.body.removeChild(a)
}
function removeNotice(idx) {
const n = form.notices[idx]
if (!n) return
ElMessageBox.confirm(`确定删除"${n.label}"附件?`, '提示', { type: 'warning' })
.then(() => { n.url = ''; ElMessage.success('已删除') })
.catch(() => {})
}
function goBack() {
// 带 _t 强制 Projects.vue watch 触发 reload, 即便路由一样也重载
router.push({ path: '/manager/projects', query: { _t: Date.now() } })
@@ -390,8 +301,8 @@ async function submit(mode = 'save') {
manageFee: form.manageFee,
startTime: form.startTime,
endTime: form.endTime,
orgId: form.orgId,
orgName: form.orgName,
sponsorAdminUserId: form.sponsorAdminUserId,
sponsorAdminUserName: form.sponsorAdminUserName,
submitDeadlineDays: form.submitDeadlineDays
}
// 公告文件以 publicityFiles JSON 数组存储 (替代旧 invitationUrl/supportLetterUrl/noticeUrl/scheduleUrl)
@@ -473,30 +384,22 @@ async function submit(mode = 'save') {
.role-custom { flex: 1; max-width: 180px; }
.role-amount { width: 180px; }
.row-actions { display: flex; gap: 8px; }
.notice-list { display: flex; flex-direction: column; gap: 12px; }
.notice-row { display: flex; align-items: stretch; gap: 16px; margin-bottom: 16px; }
.notice-list { display: flex; flex-direction: column; gap: 10px; }
.notice-row { display: flex; align-items: stretch; gap: 12px; margin-bottom: 12px; }
.notice-label {
width: 100px;
width: 90px;
display: flex; align-items: center; justify-content: flex-end;
color: #606266; font-size: 14px;
}
.notice-uploader { flex: 1; }
.notice-uploader :deep(.el-upload) { width: 100%; }
.notice-uploader :deep(.el-upload-dragger) {
padding: 12px 16px;
display: flex; align-items: center; gap: 12px;
}
.notice-uploader :deep(.el-upload-dragger .el-upload__text) {
font-size: 13px; color: #606266;
}
.notice-uploader :deep(.el-upload-dragger .el-upload__tip) {
font-size: 12px; color: #909399; margin-top: 2px;
}
.notice-uploader :deep(.el-upload__tip) { margin-top: 0; }
.notice-uploader :deep(.el-upload-list) { margin: 0; }
.notice-actions { display: flex; flex-direction: column; justify-content: center; gap: 4px; min-width: 70px; }
.notice-actions .el-button { padding: 2px 6px; font-size: 12px; }
.notice-disabled { flex: 1; color: #c0c4cc; font-size: 13px; padding: 8px 0; }
/* 紧凑版 (覆盖 OssFileUploader 默认尺寸) */
.notice-uploader :deep(.ht-file-upload) { padding: 6px 12px; min-height: 40px; gap: 8px; }
.notice-uploader :deep(.placeholder-text) { font-size: 13px; }
.notice-uploader :deep(.placeholder-hint) { font-size: 11px; }
.notice-uploader :deep(.file-name) { font-size: 13px; }
.notice-uploader :deep(.file-type) { font-size: 11px; }
.notice-uploader :deep(.file-icon) { font-size: 18px; }
.notice-uploader :deep(.file-icon svg) { width: 18px; height: 18px; }
/* 底部按钮 */
.form-actions { display: flex; justify-content: center; gap: 16px; padding: 20px 0 4px; border-top: 1px solid #f0f0f0; margin-top: 8px; }