feat: 会议批量审核 + 公示按发布时间倒序 + 现场照片 dialog 预览 + 重复报错精确
- 会议列表 manager 批量合规审核 (批量通过/退回, best-effort 逐条回执) - 公示列表按 publish_time 倒序, 发布/开通写本地 publish_time (修 toISOString UTC 时区 bug) - 签到表/前全景/后全景「查看照片」改 dialog 预览 (复用 Preview.vue) - 手机号/用户名重复报错精确提示 (后端 fieldMap + 前端 duplicateTip) - 医生首页欢迎语改「欢迎使用合规系统」
This commit is contained in:
@@ -21,11 +21,14 @@
|
||||
<el-button type="primary" size="small" :disabled="readonly" @click="openDialog">
|
||||
<el-icon><CameraFilled /></el-icon> 扫码拍照
|
||||
</el-button>
|
||||
<a v-if="modelValue" :href="modelValue" target="_blank" class="cam-link">查看照片</a>
|
||||
<el-button v-if="modelValue" link type="primary" size="small" @click="openPreview">查看照片</el-button>
|
||||
<span v-else class="cam-empty">未上传</span>
|
||||
<span v-if="polling" class="cam-polling">等待手机回传…</span>
|
||||
</div>
|
||||
|
||||
<!-- 照片预览: 复用全局 Preview.vue (dialog 内预览, 图片/PDF 自适应, 非新窗口) -->
|
||||
<Preview v-model="previewOpen" :url="previewUrl" :title="previewTitle" />
|
||||
|
||||
<el-dialog
|
||||
v-model="visible"
|
||||
:title="`扫码拍照 - ${label}`"
|
||||
@@ -52,6 +55,7 @@ import request from '@/utils/request'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { CameraFilled } from '@element-plus/icons-vue'
|
||||
import QRCode from 'qrcode'
|
||||
import Preview from '@/components/Preview.vue'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: { type: String, default: '' },
|
||||
@@ -73,6 +77,9 @@ const visible = ref(false)
|
||||
const qrUrl = ref('')
|
||||
const qrLoading = ref(false)
|
||||
const polling = ref(false)
|
||||
const previewOpen = ref(false)
|
||||
const previewUrl = ref('')
|
||||
const previewTitle = ref('')
|
||||
let pollTimer = null
|
||||
let baseline = ''
|
||||
|
||||
@@ -142,6 +149,14 @@ function onClosed() {
|
||||
stopPolling()
|
||||
}
|
||||
|
||||
/** 打开照片预览 dialog (复用全局 Preview.vue, 图片/PDF 自适应) */
|
||||
function openPreview() {
|
||||
if (!props.modelValue) return
|
||||
previewUrl.value = props.modelValue
|
||||
previewTitle.value = props.label
|
||||
previewOpen.value = true
|
||||
}
|
||||
|
||||
onBeforeUnmount(stopPolling)
|
||||
</script>
|
||||
|
||||
@@ -151,8 +166,6 @@ onBeforeUnmount(stopPolling)
|
||||
display: flex; align-items: center; gap: 12px;
|
||||
min-height: 36px;
|
||||
}
|
||||
.cam-link { color: var(--brand-primary); text-decoration: none; font-size: 13px; }
|
||||
.cam-link:hover { text-decoration: underline; }
|
||||
.cam-empty { color: #c0c4cc; font-size: 13px; }
|
||||
.cam-polling { color: #e6a23c; font-size: 12px; }
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<div class="welcome-bar">
|
||||
<div class="welcome-text">
|
||||
<h2>下午好,{{ displayName }}专家</h2>
|
||||
<p>欢迎使用项目管理系统</p>
|
||||
<p>欢迎使用合规系统</p>
|
||||
</div>
|
||||
<div class="welcome-time">
|
||||
<div class="now">{{ nowTime }}</div>
|
||||
|
||||
@@ -213,6 +213,12 @@ function isDuplicateError(msg) {
|
||||
return /已存在|重复|duplicate|uk_person_phone/i.test(msg || '')
|
||||
}
|
||||
|
||||
// 按后端原因精确提示: 登录账号(用户名) vs 手机号 (后端分别抛 "登录账号已存在" / DB uk_person_phone)
|
||||
function duplicateTip(msg) {
|
||||
if (/登录账号|用户名|login/i.test(msg || '')) return '登录账号已存在,请更换'
|
||||
return '该手机号已存在,请更换'
|
||||
}
|
||||
|
||||
async function onSave() {
|
||||
try {
|
||||
await formRef.value.validate()
|
||||
@@ -248,12 +254,12 @@ async function onSave() {
|
||||
}
|
||||
} else {
|
||||
const msg = res?.msg || '保存失败'
|
||||
if (isDuplicateError(msg)) ElMessage.warning('该手机号已存在,请更换')
|
||||
if (isDuplicateError(msg)) ElMessage.warning(duplicateTip(msg))
|
||||
else ElMessage.error(msg)
|
||||
}
|
||||
} catch (e) {
|
||||
const msg = e?.msg || e?.message || '保存失败'
|
||||
if (isDuplicateError(msg)) ElMessage.warning('该手机号已存在,请更换')
|
||||
if (isDuplicateError(msg)) ElMessage.warning(duplicateTip(msg))
|
||||
else ElMessage.error(msg)
|
||||
} finally {
|
||||
saving.value = false
|
||||
|
||||
@@ -145,6 +145,12 @@ const fileInputRef = ref(null)
|
||||
|
||||
function defaultRoleRow() { return { role: '', customName: '', amount: 0 } }
|
||||
function defaultNotice(key, label) { return { key, label, url: '', name: '' } }
|
||||
// 当前本地时间 "yyyy-MM-dd HH:mm:ss" (发布用, 与 el-date-picker 的本地时间口径一致; 不能用 toISOString 的 UTC)
|
||||
function nowLocalDatetime() {
|
||||
const d = new Date()
|
||||
const p = n => String(n).padStart(2, '0')
|
||||
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`
|
||||
}
|
||||
|
||||
// 项目负责人候选 (合规管理员, sys_user.role_type='manager')
|
||||
const managerOptions = ref([])
|
||||
@@ -333,7 +339,7 @@ async function submit(mode = 'save') {
|
||||
// 发布时设置 is_published='1' + publish_time (同步后端字段)
|
||||
if (mode === 'publish') {
|
||||
payload.isPublished = '1'
|
||||
payload.publishTime = new Date().toISOString().slice(0, 19).replace('T', ' ')
|
||||
payload.publishTime = nowLocalDatetime()
|
||||
}
|
||||
// 多执行方分配已移至"项目分配"页面单独管理
|
||||
try {
|
||||
|
||||
@@ -35,9 +35,9 @@
|
||||
|
||||
<!-- ========== 批量按钮 (admin/manager 共用: 批量提交 + 批量删除; sponsor 隐藏) ========== -->
|
||||
<div v-if="isRole('admin', 'manager')" class="batch-bar">
|
||||
<el-button type="danger" :disabled="!selectedIds.length" @click="onBatchDelete">批量删除</el-button>
|
||||
<el-button type="primary" :disabled="!selectedIds.length" @click="onBatchDownloadService">批量下载会务</el-button>
|
||||
<el-button type="primary" :disabled="!selectedIds.length" @click="onBatchDownloadLabor">批量下载劳务</el-button>
|
||||
<el-button v-if="isRole('manager')" type="primary" :disabled="!selectedIds.length" @click="onBatchAudit">批量审核</el-button>
|
||||
<span v-if="selectedIds.length" class="filter-tip">已选 {{ selectedIds.length }} 条</span>
|
||||
</div>
|
||||
|
||||
@@ -119,6 +119,28 @@
|
||||
<el-button type="primary" :loading="approvalSaving" @click="onApprovalConfirm">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 合规人员(manager) 批量审核 dialog: 对选中的「待合规审核」会议批量通过/退回 -->
|
||||
<el-dialog v-model="batchAuditOpen" title="批量合规审核" width="560px">
|
||||
<el-form label-width="100px">
|
||||
<el-form-item label="审核范围">
|
||||
<span>已选 {{ selectedIds.length }} 个会议, 其中待合规审核 {{ batchEligibleIds.length }} 个</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="审核结果">
|
||||
<el-radio-group v-model="batchAuditForm.approved">
|
||||
<el-radio :label="true">通过</el-radio>
|
||||
<el-radio :label="false">退回</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="审核意见">
|
||||
<el-input v-model="batchAuditForm.opinion" type="textarea" :rows="4" maxlength="500" show-word-limit placeholder="退回时意见必填, 将通知执行方整改" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="batchAuditOpen = false">取消</el-button>
|
||||
<el-button type="primary" :loading="batchAuditSaving" @click="onBatchAuditConfirm">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -364,6 +386,66 @@ async function onApprovalConfirm() {
|
||||
}
|
||||
}
|
||||
|
||||
// ========== 合规人员(manager) 批量审核 ==========
|
||||
const batchAuditOpen = ref(false)
|
||||
const batchAuditSaving = ref(false)
|
||||
const batchAuditForm = reactive({ approved: true, opinion: '' })
|
||||
const batchEligibleIds = ref([])
|
||||
|
||||
// 待合规审核判据: material_audit_stage=SUBMITTED 且 compliance_approved≠1 (与详情页 canComplianceAudit 一致)
|
||||
function isCompliancePending(row) {
|
||||
return row.materialAuditStage === 'SUBMITTED' && !isOneVal(row.materialComplianceApproved)
|
||||
}
|
||||
|
||||
function onBatchAudit() {
|
||||
if (!selectedIds.value.length) return ElMessage.warning('请先勾选会议')
|
||||
// 从当前页 rows 里过滤出「待合规审核」的选中会议 (selection 只来自当前页)
|
||||
const eligible = rows.value.filter(r => selectedIds.value.includes(r.meetingId) && isCompliancePending(r))
|
||||
batchEligibleIds.value = eligible.map(r => r.meetingId)
|
||||
if (!batchEligibleIds.value.length) {
|
||||
ElMessage.warning('所选会议中没有处于「待合规审核」状态的, 无法批量审核')
|
||||
return
|
||||
}
|
||||
batchAuditForm.approved = true
|
||||
batchAuditForm.opinion = ''
|
||||
batchAuditOpen.value = true
|
||||
}
|
||||
|
||||
async function onBatchAuditConfirm() {
|
||||
if (!batchAuditForm.approved && !batchAuditForm.opinion.trim()) {
|
||||
ElMessage.warning('退回时意见不能为空')
|
||||
return
|
||||
}
|
||||
batchAuditSaving.value = true
|
||||
try {
|
||||
const r = await request.post('/business/meeting/batch-audit-compliance', {
|
||||
meetingIds: batchEligibleIds.value,
|
||||
approved: batchAuditForm.approved,
|
||||
opinion: batchAuditForm.opinion
|
||||
})
|
||||
const d = r?.data || {}
|
||||
const successCount = d.successCount ?? 0
|
||||
const failCount = d.failCount ?? 0
|
||||
const failures = d.failures || []
|
||||
if (failCount > 0) {
|
||||
const names = failures.map(f => `会议ID ${f.meetingId}: ${f.reason}`).join('\n')
|
||||
ElMessageBox.alert(
|
||||
`批量审核完成\n成功 ${successCount} 个, 失败 ${failCount} 个\n\n失败明细:\n${names}`,
|
||||
'批量审核结果',
|
||||
{ type: successCount > 0 ? 'warning' : 'error', confirmButtonText: '知道了' }
|
||||
)
|
||||
} else {
|
||||
ElMessage.success(`批量审核完成, 成功 ${successCount} 个`)
|
||||
}
|
||||
batchAuditOpen.value = false
|
||||
load()
|
||||
} catch (e) {
|
||||
ElMessage.error(e?.msg || e?.message || '批量审核失败')
|
||||
} finally {
|
||||
batchAuditSaving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// ========== 会务下载 (OSS 端打 zip: 后端 staging copy + 阿里云 FC 打包) ==========
|
||||
async function onDownloadService(row) {
|
||||
try {
|
||||
|
||||
@@ -42,30 +42,48 @@
|
||||
</div>
|
||||
|
||||
<div class="filter-bar">
|
||||
<input v-model="currentKeyword" @keydown.enter="applyFilter" type="text" class="filter-input" placeholder="">
|
||||
<div class="filter-input-wrap">
|
||||
<svg class="filter-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
|
||||
<circle cx="11" cy="11" r="7"/>
|
||||
<path d="M21 21l-4.3-4.3"/>
|
||||
</svg>
|
||||
<input v-model="currentKeyword" @keydown.enter="applyFilter" type="text" class="filter-input" placeholder="搜索项目名称">
|
||||
</div>
|
||||
<button class="filter-btn" @click="applyFilter">查询</button>
|
||||
<button class="filter-btn" style="background: #fff; color: var(--brand-primary); border: 1px solid var(--brand-primary);" @click="resetFilter">重置</button>
|
||||
<button class="filter-btn filter-btn-plain" @click="resetFilter">重置</button>
|
||||
</div>
|
||||
|
||||
<div class="notice-list">
|
||||
<template v-if="pageItems.length === 0">
|
||||
<div style="padding: 40px 0; text-align: center; color: #9ca3af; font-size: 13px;">暂无数据</div>
|
||||
<div class="notice-empty">
|
||||
<svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
|
||||
<path d="M14 2v6h6"/>
|
||||
</svg>
|
||||
<span>暂无公示项目</span>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="notice-header">
|
||||
<span class="notice-header-no">序号</span>
|
||||
<span class="notice-header-title">项目</span>
|
||||
<span class="notice-header-date">开始日期</span>
|
||||
<span class="notice-header-end">截止日期</span>
|
||||
<span class="notice-header-title">项目名称</span>
|
||||
<span class="notice-header-date">发布日期</span>
|
||||
</div>
|
||||
<div v-for="(n, i) in pageItems" :key="n.annId || i" class="notice-item" @click="goDetail(n)">
|
||||
<span class="notice-item-no">{{ String((currentPage - 1) * PAGE_SIZE + i + 1).padStart(3, '0') }}</span>
|
||||
<span class="notice-title">
|
||||
<span v-if="n.projectNo" style="color:#909399;margin-right:8px;font-size:13px">【{{ n.projectNo }}】</span>
|
||||
{{ n.title }}
|
||||
<svg class="notice-arrow" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M9 6l6 6-6 6"/>
|
||||
</svg>
|
||||
</span>
|
||||
<span class="notice-date">
|
||||
<svg class="notice-date-icon" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<rect x="3" y="4" width="18" height="18" rx="2"/>
|
||||
<line x1="16" y1="2" x2="16" y2="6"/>
|
||||
<line x1="8" y1="2" x2="8" y2="6"/>
|
||||
<line x1="3" y1="10" x2="21" y2="10"/>
|
||||
</svg>
|
||||
{{ fmtDate(n.publishTime) }}
|
||||
</span>
|
||||
<span class="notice-date">{{ fmtDate(n.date) }}</span>
|
||||
<span class="notice-end">{{ fmtDate(n.endTime) }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
@@ -121,8 +139,7 @@ async function load() {
|
||||
allRows.value = list.map(r => {
|
||||
return {
|
||||
annId: r.projectId,
|
||||
date: r.startTime,
|
||||
endTime: r.endTime,
|
||||
publishTime: r.publishTime,
|
||||
type: '公示',
|
||||
projectNo: r.projectNo || '',
|
||||
projectName: r.projectName,
|
||||
@@ -430,30 +447,47 @@ main.container {
|
||||
|
||||
/* ========== 筛选条 ========== */
|
||||
.filter-bar {
|
||||
padding: 12px 0;
|
||||
border-bottom: 1px solid #f3f4f6;
|
||||
margin-bottom: 0;
|
||||
padding: 14px 0;
|
||||
border-bottom: 1px solid #f0f2f5;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
gap: 12px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.filter-input-wrap {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.filter-icon {
|
||||
position: absolute;
|
||||
left: 13px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
color: #9ca3af;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.filter-input {
|
||||
height: 30px;
|
||||
padding: 0 10px;
|
||||
border: 1px solid #d1d5db;
|
||||
background: #fff;
|
||||
height: 34px;
|
||||
padding: 0 12px 0 34px;
|
||||
border: 1px solid #dde1e6;
|
||||
border-radius: 17px;
|
||||
background: #f7f8fa;
|
||||
font-size: 13px;
|
||||
color: #1f2937;
|
||||
font-family: inherit;
|
||||
outline: none;
|
||||
width: 220px;
|
||||
transition: border-color 0.2s;
|
||||
width: 240px;
|
||||
transition: border-color 0.2s, background 0.2s, box-shadow 0.2s;
|
||||
}
|
||||
|
||||
.filter-input:focus {
|
||||
border-color: var(--brand-primary);
|
||||
background: #fff;
|
||||
box-shadow: 0 0 0 3px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.filter-input::placeholder {
|
||||
@@ -461,20 +495,33 @@ main.container {
|
||||
}
|
||||
|
||||
.filter-btn {
|
||||
height: 30px;
|
||||
padding: 0 18px;
|
||||
height: 34px;
|
||||
padding: 0 20px;
|
||||
background: var(--brand-primary);
|
||||
color: #fff;
|
||||
border: none;
|
||||
font-size: 12px;
|
||||
border: 1px solid var(--brand-primary);
|
||||
border-radius: 17px;
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
font-family: inherit;
|
||||
letter-spacing: 1px;
|
||||
transition: background 0.2s;
|
||||
transition: background 0.2s, border-color 0.2s, color 0.2s;
|
||||
}
|
||||
|
||||
.filter-btn:hover {
|
||||
background: var(--brand-primary-deep);
|
||||
border-color: var(--brand-primary-deep);
|
||||
}
|
||||
|
||||
.filter-btn-plain {
|
||||
background: #fff;
|
||||
color: var(--brand-primary);
|
||||
}
|
||||
|
||||
.filter-btn-plain:hover {
|
||||
background: #f7f8fa;
|
||||
border-color: var(--brand-primary);
|
||||
color: var(--brand-primary);
|
||||
}
|
||||
|
||||
/* ========== 公示列表 ========== */
|
||||
@@ -482,40 +529,14 @@ main.container {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.notice-item {
|
||||
.notice-empty {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
padding: 14px 0;
|
||||
border-bottom: 1px solid #f3f4f6;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.notice-item:hover {
|
||||
background: #fafbfc;
|
||||
}
|
||||
|
||||
.notice-item:hover .notice-title {
|
||||
color: var(--brand-primary);
|
||||
}
|
||||
|
||||
.notice-date {
|
||||
flex-shrink: 0;
|
||||
width: 100px;
|
||||
font-size: 13px;
|
||||
color: #6b7280;
|
||||
font-family: ui-monospace, "Courier New", monospace;
|
||||
padding-top: 2px;
|
||||
}
|
||||
|
||||
.notice-title {
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 64px 0;
|
||||
color: #c0c6cf;
|
||||
font-size: 14px;
|
||||
color: #1f2937;
|
||||
line-height: 1.7;
|
||||
transition: color 0.2s;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/* 表头: 与 notice-item 相同的 flex + gap + 列宽, 保证列对齐 */
|
||||
@@ -523,23 +544,12 @@ main.container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 24px;
|
||||
padding: 12px 0;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
background: #f9fafb;
|
||||
font-size: 13px;
|
||||
padding: 14px 20px;
|
||||
border-bottom: 1px solid #eef0f3;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #4b5563;
|
||||
}
|
||||
|
||||
.notice-header-no {
|
||||
flex-shrink: 0;
|
||||
width: 32px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.notice-header-date {
|
||||
flex-shrink: 0;
|
||||
width: 100px;
|
||||
color: #9ca3af;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.notice-header-title {
|
||||
@@ -547,29 +557,88 @@ main.container {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.notice-header-end {
|
||||
.notice-header-date {
|
||||
flex-shrink: 0;
|
||||
width: 100px;
|
||||
width: 130px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
/* 右侧截止时间: 等宽数字 + 灰色, 与日期列同宽对齐 */
|
||||
.notice-end {
|
||||
flex-shrink: 0;
|
||||
width: 100px;
|
||||
font-size: 13px;
|
||||
color: #6b7280;
|
||||
font-family: ui-monospace, "Courier New", monospace;
|
||||
padding-top: 2px;
|
||||
.notice-item {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 24px;
|
||||
padding: 16px 20px;
|
||||
border-bottom: 1px solid #f3f4f6;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.notice-item-no {
|
||||
.notice-item::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 3px;
|
||||
height: 52%;
|
||||
border-radius: 2px;
|
||||
background: var(--brand-primary);
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.notice-item:hover {
|
||||
background: #f7f9fb;
|
||||
}
|
||||
|
||||
.notice-item:hover::before {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.notice-item:hover .notice-title {
|
||||
color: var(--brand-primary);
|
||||
}
|
||||
|
||||
.notice-title {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
color: #1f2937;
|
||||
line-height: 1.6;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.notice-arrow {
|
||||
flex-shrink: 0;
|
||||
width: 32px;
|
||||
text-align: center;
|
||||
color: var(--brand-primary);
|
||||
opacity: 0;
|
||||
transform: translateX(-6px);
|
||||
transition: opacity 0.2s ease, transform 0.2s ease;
|
||||
}
|
||||
|
||||
.notice-item:hover .notice-arrow {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
.notice-date {
|
||||
flex-shrink: 0;
|
||||
width: 130px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 6px;
|
||||
font-size: 12px;
|
||||
color: #9ca3af;
|
||||
font-family: ui-monospace, "Courier New", monospace;
|
||||
padding-top: 2px;
|
||||
color: #8a919c;
|
||||
}
|
||||
|
||||
.notice-date-icon {
|
||||
color: #b6bcc6;
|
||||
}
|
||||
|
||||
/* ========== 分页 ========== */
|
||||
|
||||
@@ -212,6 +212,12 @@ function isDuplicateError(msg) {
|
||||
return /已存在|重复|duplicate|uk_person_phone/i.test(msg || '')
|
||||
}
|
||||
|
||||
// 按后端原因精确提示: 登录账号(用户名) vs 手机号 (后端分别抛 "登录账号已存在" / DB uk_person_phone)
|
||||
function duplicateTip(msg) {
|
||||
if (/登录账号|用户名|login/i.test(msg || '')) return '登录账号已存在,请更换'
|
||||
return '该手机号已存在,请更换'
|
||||
}
|
||||
|
||||
async function onSave() {
|
||||
try {
|
||||
await formRef.value.validate()
|
||||
@@ -247,12 +253,12 @@ async function onSave() {
|
||||
}
|
||||
} else {
|
||||
const msg = res?.msg || '保存失败'
|
||||
if (isDuplicateError(msg)) ElMessage.warning('该手机号已存在,请更换')
|
||||
if (isDuplicateError(msg)) ElMessage.warning(duplicateTip(msg))
|
||||
else ElMessage.error(msg)
|
||||
}
|
||||
} catch (e) {
|
||||
const msg = e?.msg || e?.message || '保存失败'
|
||||
if (isDuplicateError(msg)) ElMessage.warning('该手机号已存在,请更换')
|
||||
if (isDuplicateError(msg)) ElMessage.warning(duplicateTip(msg))
|
||||
else ElMessage.error(msg)
|
||||
} finally {
|
||||
saving.value = false
|
||||
|
||||
@@ -241,6 +241,12 @@ const detail = ref({})
|
||||
const detailOpen = ref(false)
|
||||
|
||||
function formatMoney(v) { return Number(v || 0).toFixed(2) }
|
||||
// 当前本地时间 "yyyy-MM-dd HH:mm:ss" (发布用, 与后端 GMT+8 一致; 不能用 toISOString 的 UTC)
|
||||
function nowLocalDatetime() {
|
||||
const d = new Date()
|
||||
const p = n => String(n).padStart(2, '0')
|
||||
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`
|
||||
}
|
||||
|
||||
async function load() {
|
||||
loading.value = true
|
||||
@@ -387,7 +393,7 @@ async function onBatchOpen() {
|
||||
try { await ElMessageBox.confirm(`确定对选中的 ${selected.value.length} 个项目批量开通吗?`, '批量开通', { type: 'warning' }) } catch { return }
|
||||
let ok = 0
|
||||
for (const r of selected.value) {
|
||||
try { await bizUpdate('project', { projectId: r.projectId, isPublished: '1' }); ok++ } catch {}
|
||||
try { await bizUpdate('project', { projectId: r.projectId, isPublished: '1', publishTime: nowLocalDatetime() }); ok++ } catch {}
|
||||
}
|
||||
ElMessage.success(`已批量开通 ${ok} 项`)
|
||||
load()
|
||||
@@ -405,7 +411,7 @@ async function onFinish(row) {
|
||||
async function onOpen(row) {
|
||||
try { await ElMessageBox.confirm(`确定开通「${row.projectName}」吗?`, '开通', { type: 'warning' }) } catch { return }
|
||||
try {
|
||||
await bizUpdate('project', { projectId: row.projectId, isPublished: '1' })
|
||||
await bizUpdate('project', { projectId: row.projectId, isPublished: '1', publishTime: nowLocalDatetime() })
|
||||
ElMessage.success('已开通')
|
||||
load()
|
||||
} catch (e) { ElMessage.error(e?.msg || '操作失败') }
|
||||
|
||||
Reference in New Issue
Block a user