diff --git a/src/components/RichEditor/RichEditor.vue b/src/components/RichEditor/RichEditor.vue index e4c6a6d..08ad10e 100644 --- a/src/components/RichEditor/RichEditor.vue +++ b/src/components/RichEditor/RichEditor.vue @@ -48,18 +48,22 @@ async function uploadFile(file: File): Promise { throw new Error(data.msg || "图片上传失败") } -// === 外链图片转存: 公众号图片有防盗链, 下载后上传平台存储 === +// === 外链图片转存: 微信等 CDN 有防盗链且无 CORS 头, 前端 fetch 必败 —— 走后端服务端拉取转存 === async function remoteToLocal(src: string): Promise { try { - const resp = await fetch(src, { referrerPolicy: "no-referrer", mode: "cors" }) - if (!resp.ok) return src - const blob = await resp.blob() - if (!blob.type.startsWith("image/")) return src - const file = new File([blob], "paste-image." + (blob.type.split("/")[1] || "png"), { type: blob.type }) - return await uploadFile(file) - } catch { - return src + const token = localStorage.getItem("access_token") || "" + const resp = await fetch("/api/sys/file/uploadByUrl", { + method: "POST", + headers: { "Content-Type": "application/json", Authorization: token ? `Bearer ${token}` : "" }, + body: JSON.stringify({ url: src }), + }) + const data = await resp.json() + if (data.code === 0 && data.data?.url) return data.data.url + console.warn("图片转存失败:", src, data.msg) + } catch (e: any) { + console.warn("图片转存异常:", src, e?.message) } + return src } // === 处理粘贴 HTML: 转存外链图 -> 清洗危险/不兼容标签 -> 返回 [原始版, 编辑器版] === @@ -84,13 +88,27 @@ async function processPasteHtml(html: string): Promise<{ raw: string; editorSafe } } - // 编辑器版: 移除 section/iframe/script/style/class 等 slate 不兼容或危险标签, 保留排版 + // 编辑器版: 移除 section/iframe/script/style/svg/自定义标签(mp-xxx 等)/class —— 这些会导致 + // wangeditor(slate) HTML 解析丢节点(用户反馈"部分文本未粘贴成功"的根因), 保留文本与常规排版 editorSafe = editorSafe .replace(//gi, "") .replace(//gi, "") .replace(//gi, "") + .replace(//gi, "") .replace(//gi, "") + // 公众号自定义标签 (mp-common-*, mpprofile 等) 整块移除 + .replace(//gi, "") + .replace(/<\/?mp-[a-z-]+[^>]*>/gi, "") + // 其余非 HTML 标准标签转为 div 保留内部文本 + .replace(/<([a-z][a-z0-9-]*)((?:\s[^>]*)?)>/gi, (m0, tag, attrs) => { + if (/^(div|p|span|br|img|strong|b|em|i|u|s|ul|ol|li|h[1-6]|blockquote|table|thead|tbody|tr|td|th|a|hr|pre|code|font|center|section)$/i.test(tag)) return m0 + return "" + }) + .replace(/<\/([a-z][a-z0-9-]*)>/gi, (m0, tag) => { + if (/^(div|p|span|br|img|strong|b|em|i|u|s|ul|ol|li|h[1-6]|blockquote|table|thead|tbody|tr|td|th|a|hr|pre|code|font|center|section)$/i.test(tag)) return m0 + return "" + }) .replace(/\sclass=["\x27][^"\x27]*["\x27]/gi, "") .replace(//gi, "
") @@ -138,7 +156,14 @@ const editorConfig = { // 保存原始 HTML: 提交时用 (公众号排版完整保留) rawPasteHtml = raw programmaticSet = true - editor.dangerouslyInsertHtml(editorSafe) + try { + editor.dangerouslyInsertHtml(editorSafe) + } catch (insertErr: any) { + // slate 解析失败兜底: 插入纯文本, 保证文本内容不丢 + console.error("insert html failed, fallback to text:", insertErr) + const text = editorSafe.replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim() + editor.insertText(text) + } setTimeout(() => { programmaticSet = false }, 100) } catch (e: any) { console.error("paste html error:", e) @@ -222,14 +247,18 @@ onBeforeUnmount(() => { .rich-editor { border: 1px solid var(--el-border-color, #dcdfe6); border-radius: 6px; - overflow: hidden; } .rich-editor__toolbar { + /* 工具栏吸顶: 页面滚动编辑长内容时始终固定在可视区顶部 (2026-09-21) */ + position: sticky; + top: 0; + z-index: 100; + background: var(--el-bg-color, #fff); + border-radius: 6px 6px 0 0; border-bottom: 1px solid var(--el-border-color, #dcdfe6); } .rich-editor__body { min-height: 320px; - z-index: 100; } .rich-editor__body :deep(.w-e-text-container) { min-height: 320px; diff --git a/src/config/menu-labels.ts b/src/config/menu-labels.ts index 0da51e3..9198d61 100644 --- a/src/config/menu-labels.ts +++ b/src/config/menu-labels.ts @@ -74,6 +74,10 @@ export const MENU_LABELS: MenuLabel[] = [ { url: 'order/serviceorder-commission', label: '服务订单分佣统计', icon: 'Coin', path: '/order/serviceorder/commission', sort: 221 }, { url: 'order/orderbase-price-analysis', label: '基础价格分析表', icon: 'TrendCharts', path: '/order/base-price-analysis', sort: 230 }, { url: 'order/orderpay', label: '订单支付回调', icon: 'Wallet', path: '/order/orderpay', sort: 240 }, + { url: 'order/servicefee', label: '平台服务费管理', icon: 'Money', path: '/order/servicefee', sort: 245 }, + { url: 'order/backfill-manage', label: '补录管理', icon: 'DocumentAdd', path: '/order/backfill-manage', sort: 246 }, + { url: 'user/enterprise-score', label: '企业评分与等级', icon: 'Trophy', path: '/user/enterprise-score', sort: 150 }, + { url: 'sys/ai-manage', label: 'AI 能力管理', icon: 'Cpu', path: '/sys/ai-manage', sort: 155 }, { url: 'order/refund', label: '订单退款管理', icon: 'Refresh', path: '/order/refund', sort: 250 }, { url: 'order/aftersale', label: '售后订单', icon: 'Service', path: '/order/aftersale', sort: 260 }, diff --git a/src/modules/auth/LoginView.vue b/src/modules/auth/LoginView.vue index 7b74db7..5db8fc7 100644 --- a/src/modules/auth/LoginView.vue +++ b/src/modules/auth/LoginView.vue @@ -17,6 +17,7 @@ import qs from 'qs' import axios from 'axios' import { useUserStore } from '@/stores/user' import { useThemeStore } from '@/stores/theme' +import { View, Hide, Lock } from '@element-plus/icons-vue' import { computed } from 'vue' import logoUrl from '@/assets/logo-jubian.png' import logoLightUrl from '@/assets/logo-jubian-light.png' @@ -27,6 +28,7 @@ const userStore = useUserStore() const themeStore = useThemeStore() const logoSrc = computed(() => themeStore.current === 'dark' ? logoLightUrl : logoUrl) +const showPwd = ref(false) const form = reactive({ username: 'admin', password: 'admin', @@ -150,7 +152,8 @@ async function onLogin() {
- + +
@@ -312,6 +315,20 @@ async function onLogin() { color: var(--ink-3); pointer-events: none; } +.rr-input-eye { + position: absolute; + right: 10px; top: 50%; + transform: translateY(-50%); + font-size: 16px; + color: var(--ink-3); + cursor: pointer; + z-index: 1; + + &:hover { color: var(--brand); } +} +.rr-input.has-eye { + padding-right: 36px; +} .rr-input { width: 100%; height: 38px; padding: 0 12px 0 34px; diff --git a/src/modules/inquiry/InquiryDetailView.vue b/src/modules/inquiry/InquiryDetailView.vue index 01970f7..48a36fe 100644 --- a/src/modules/inquiry/InquiryDetailView.vue +++ b/src/modules/inquiry/InquiryDetailView.vue @@ -13,6 +13,7 @@ import { useRoute, useRouter } from 'vue-router' import { ElMessage } from 'element-plus' import { httpGet, httpPost } from '@/utils/request' import PageHeader from '@/components/PageHeader/PageHeader.vue' +import { ArrowRight } from '@element-plus/icons-vue' interface Inquiry { id: string @@ -105,7 +106,17 @@ const inquiryId = computed(() => String(route.params.id || '')) const PROC_TYPE: Record = { 1: '定量采购', 2: '框架采购' } const DEMAND: Record = { 1: '不允许部分报价', 2: '允许部分报价' } -const INVOICE: Record = { 1: '普通发票', 2: '增值税专用发票', 3: '无' } +// 发票要求展示 (2026-10-16 票种化) +function invoiceTypeLabel(v: any): string { + const n = Number(v) + if (v === null || v === undefined || v === '' || isNaN(n)) return '—' + if (n === 0) return '无' + if (n === 2) return '增值税专用发票' + if (n >= 200) return `增值税专用发票 ${n - 200}%` + if (n >= 100) return `增值税普通发票 ${n - 100}%` + if ([1, 3, 6, 9, 13].includes(n)) return `${n}% 发票` + return String(v) +} const QUOTE_TYPE: Record = { 1: '公开报价', 2: '邀请报价' } const mapLabel = (m: Record, v: any) => (v == null ? '--' : m[Number(v)] || String(v)) const mapLabel1 = (m: Record, v: any) => (v == null ? '--' : m[Number(v)] || String(v)) @@ -131,7 +142,7 @@ async function load() { quotationTypeName: mapLabel(QUOTE_TYPE, resp.quotationType), quotationDemandName: mapLabel(DEMAND, resp.quotationDemand), paymentMethodName: resp.paymentMethodName || '--', - invoiceTypeName: mapLabel(INVOICE, resp.invoiceType), + invoiceTypeName: invoiceTypeLabel(resp.invoiceType), inquiryValidDate: resp.inquiryValidDate || '--', note: resp.note || '', detailNote: '', // 清除 FALLBACK 占位文案,备注一律取真实数据 @@ -152,15 +163,28 @@ async function load() { } } -// 附件:发布时 images 为 URL 数组 + imageNames 名称数组 +// 附件:兼容两种返回形态——① images=URL 字符串数组 + imageNames ② images=[{url, imagesName}] 对象数组 function resetFiles(resp: any): any[] { - const urls: string[] = resp.images || [] - const names: string[] = resp.imageNames || [] - return urls.map((u: string, i: number) => ({ url: u, imagesName: names[i] || ('附件' + (i + 1)) })) + const raw: any[] = resp.images || [] + const names: string[] = Array.isArray(resp.imageNames) ? resp.imageNames : String(resp.imageNames || '').split(',') + const out: any[] = [] + for (let i = 0; i < raw.length; i++) { + const item = raw[i] + if (item && typeof item === 'object') { + if (!item.url) continue + out.push({ url: String(item.url), imagesName: item.imagesName || names[i] || ('附件' + (i + 1)) }) + } else if (typeof item === 'string' && item.trim()) { + out.push({ url: item.trim(), imagesName: names[i] || ('附件' + (i + 1)) }) + } + } + return out } const quoteGroups = ref([]) const quoteLoading = ref(false) +// 报价列表卡片展开索引(-1=全部收起) +const openSupIndex = ref(-1) +function toggleSup(i: number) { openSupIndex.value = openSupIndex.value === i ? -1 : i } // 供应商维度报价列表:受邀供应商(含报价状态)+ 主动报价供应商补充 const supplierRows = computed(() => { @@ -189,8 +213,8 @@ const supplierRows = computed(() => { const existing = rows.find(r => r.name === name) const quoted = Number(inv.quotationStatus) === 1 if (existing) { + // 已有真实报价行 = 已报价,状态不被邀请表状态覆盖(邀请表可能仍标记未响应) existing.invited = true - if (!quoted) existing.status = 0 continue } rows.push({ @@ -225,10 +249,18 @@ async function loadQuotes(orderId: any) { let g = groups.find(x => x.name === name) if (!g) { g = { name, rows: [], count: 0, total: 0, date: '', list: [] }; groups.push(g) } g.rows.push(q) - g.count++ - g.total += Number(q.unitPriceTaxIncluded || q.unitPrice || 0) * Number(q.quantity || 0) if (q.quotationDate && !g.date) g.date = q.quotationDate } + // 每企业仅保留最新报价轮:明细行 quotation_id 为 19 位雪花 id(超出 Number 安全整数,必须 BigInt 比较) + for (const g of groups) { + const withQid = g.rows.filter((r: any) => r.quotationId) + if (withQid.length) { + const latestQid = withQid.reduce((m: string, r: any) => (BigInt(String(r.quotationId)) > BigInt(m) ? String(r.quotationId) : m), String(withQid[0].quotationId)) + g.rows = g.rows.filter((r: any) => String(r.quotationId) === latestQid) + } + g.count = g.rows.length + g.total = g.rows.reduce((s: number, r: any) => s + Number(r.unitPriceTaxIncluded || r.unitPrice || 0) * Number(r.quantity || 0), 0) + } quoteGroups.value = groups } catch { quoteGroups.value = [] } finally { quoteLoading.value = false } } @@ -268,18 +300,35 @@ function quotationInfo(s: number) { return QUOTATION_STATUS_MAP[s] || { label: ` function inquiryInfo(s: number) { return INQUIRY_STATUS_MAP[s] || { label: `状态${s}`, type: 'info' } } function onBack() { router.push('/inquiry') } +// 平台 OSS 域名白名单:响应头为 attachment,走预览代理按扩展名 inline 输出 +const OSS_PREVIEW_PREFIX = 'https://jubianwangluo.oss-cn-wulanchabu.aliyuncs.com/' +// 页内预览弹窗(不再开新窗口,避免弹窗拦截/新标签空白) +const previewOpen = ref(false) +const previewItem = ref<{ url: string; name: string; kind: 'image' | 'pdf' | 'external' } | null>(null) +function fileKind(url: string): 'image' | 'pdf' | 'external' { + const lower = (url || '').toLowerCase() + if (/\.(png|jpe?g|gif|webp|bmp)(\?|$)/.test(lower)) return 'image' + if (/\.pdf(\?|$)/.test(lower)) return 'pdf' + return 'external' +} +function previewSrc(url: string): string { + return url.startsWith(OSS_PREVIEW_PREFIX) ? '/other/file/preview?url=' + encodeURIComponent(url) : url +} +function openExternal(url?: string) { if (url) window.open(url, '_blank') } +function onPreviewFile(url: string, name?: string) { + if (!url) return + previewItem.value = { url: previewSrc(url), name: name || '附件预览', kind: fileKind(url) } + previewOpen.value = true +} function onDownloadFile(url: string, name: string) { if (!url) return const a = document.createElement('a') - a.href = url + a.href = previewSrc(url) + if (url.startsWith(OSS_PREVIEW_PREFIX)) a.href = url // OSS 直接下载原始文件 a.download = name || '' a.target = '_blank' a.click() } -function onPreviewFile(url: string) { - // 走预览代理(inline 输出,浏览器原生预览图片/PDF) - window.open('/other/file/preview?url=' + encodeURIComponent(url), '_blank') -} function onExportPriceComparison() { ElMessage.info('导出功能开发中') } function onViewQuotation(row: Quotation) { ElMessage.info(`查看报价: ${row.quotationEnterpriseName} (请前往询价管理)`) } function onQuote(row: Quotation) { ElMessage.info('请前往前台发起报价') } @@ -356,73 +405,94 @@ watch(inquiryId, load) - +
-

询价附件

+

询价附件 ({{ detail.fileList.length }})

-
- - {{ f.imagesName }} - 预览 - 下载 +
+
+ +
{{ fileKind(f.url) === 'pdf' ? 'PDF' : '链接' }}
+
{{ f.imagesName }}
+
+ 预览 + 下载 +
+
- + + +
+ + +
+

该附件为外部链接,平台无法在页内预览:

+ + 在新窗口打开 +
+
+
+ +

报价列表 ({{ supplierRows.length }} 家供应商)

加载报价中...
暂无受邀供应商与报价
- - - +
该供应商尚未提交报价
+
+
+ +
点击供应商卡片展开/收起该企业的逐商品报价明细(仅显示当前最新一轮报价)
@@ -441,9 +511,34 @@ watch(inquiryId, load) .rr-title { font-size: 20px; font-weight: 600; color: var(--ink); margin: 0 0 4px 0; } .rr-sub { color: var(--ink-3); font-size: 13px; margin: 0; } .rr-header-actions { display: flex; gap: 8px; padding-top: 16px; margin-top: 16px; border-top: 1px solid var(--border); } -.rr-files { padding: 16px 20px; display: flex; flex-direction: column; gap: 8px; } -.rr-file-item { display: flex; align-items: center; gap: 8px; padding: 8px 12px; background: var(--bg-soft); border-radius: var(--radius); } -.rr-file-name { flex: 1; color: var(--ink); font-size: 13px; } +.rr-files { padding: 16px 20px; } +.rr-attach-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); gap: 14px; } +.rr-attach-item { border: 1px solid var(--border); border-radius: var(--radius); overflow: hidden; background: var(--bg-soft); cursor: pointer; transition: box-shadow .15s, border-color .15s; } +.rr-attach-item:hover { border-color: var(--el-color-primary-light-5); box-shadow: 0 2px 8px rgba(0,0,0,.06); } +.rr-attach-thumb { width: 100%; height: 150px; object-fit: cover; display: block; background: #fff; } +.rr-attach-icon { height: 150px; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 6px; color: var(--el-text-color-secondary); font-size: 13px; } +.rr-attach-icon .el-icon { font-size: 34px; } +.rr-attach-name { padding: 8px 10px 2px; font-size: 12px; color: var(--ink); line-height: 1.5; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; overflow: hidden; word-break: break-all; } +.rr-attach-actions { display: flex; justify-content: flex-end; padding: 0 6px 6px; } +.rr-preview-body { min-height: 200px; max-height: 84vh; overflow: auto; text-align: center; } +.rr-preview-img { max-width: 100%; max-height: 84vh; object-fit: contain; } +.rr-preview-frame { width: 100%; height: 82vh; border: 0; } +.rr-preview-ext { padding: 30px 20px; } +.rr-preview-link { color: var(--el-color-primary); word-break: break-all; margin: 10px 0 16px; } + +.rr-sup-list { padding: 12px 20px; display: flex; flex-direction: column; gap: 10px; } +.rr-sup-card { border: 1px solid var(--border); border-radius: var(--radius); overflow: hidden; transition: border-color .15s; } +.rr-sup-card.is-open { border-color: var(--el-color-primary-light-5); } +.rr-sup-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 12px 16px; background: var(--bg-soft); cursor: pointer; flex-wrap: wrap; } +.rr-sup-head:hover { background: var(--el-fill-color-light); } +.rr-sup-title { display: flex; align-items: center; gap: 10px; min-width: 0; } +.rr-sup-arrow { transition: transform .2s; color: var(--el-text-color-secondary); } +.rr-sup-card.is-open .rr-sup-arrow { transform: rotate(90deg); } +.rr-sup-name { font-size: 14px; font-weight: 600; color: var(--ink); } +.rr-sup-meta { display: flex; align-items: center; gap: 16px; font-size: 12px; color: var(--el-text-color-secondary); flex-wrap: wrap; } +.rr-sup-total b { color: var(--el-color-primary); font-size: 14px; } +.rr-sup-body { padding: 12px 16px; border-top: 1px solid var(--border); } +.rr-sup-empty { padding: 20px; text-align: center; } .rr-quote-groups { display: flex; flex-direction: column; gap: 12px; } .rr-quote-group { border: 1px solid var(--el-border-color); border-radius: 8px; overflow: hidden; } diff --git a/src/modules/inquiry/InquiryListView.vue b/src/modules/inquiry/InquiryListView.vue index 3d1f675..79487dc 100644 --- a/src/modules/inquiry/InquiryListView.vue +++ b/src/modules/inquiry/InquiryListView.vue @@ -9,19 +9,20 @@ */ import { onMounted, computed, reactive, ref } from 'vue' import { useRouter } from 'vue-router' -import { ElMessage } from 'element-plus' +import { ElMessage, ElMessageBox } from 'element-plus' import DataTable from '@/components/DataTable/DataTable.vue' import PageHeader from '@/components/PageHeader/PageHeader.vue' import SearchBar from '@/components/SearchBar/SearchBar.vue' import ListTabs from '@/components/ListTabs/ListTabs.vue' import { useInquiryStore, STATUS_OPTIONS } from '@/stores/inquiry' -import { httpGet } from '@/utils/request' +import { httpGet, httpPost } from '@/utils/request' interface InquiryRow { id: string customer: string product: string qty: string + hidden?: number status: 'pending' | 'quoting' | 'done' | 'cancelled' | string time: string | number raw: any @@ -78,6 +79,7 @@ async function load() { const resp = await httpGet('/supplier/inquiry/page', { page: page.value, limit: limit.value, + includeHidden: 1, ...(filters.value.apprStatus ? { apprStatus: filters.value.apprStatus } : {}) }) const arr = Array.isArray(resp) ? resp : (resp?.list || []) @@ -89,6 +91,7 @@ async function load() { qty: it.purchaseQuantity || it.qty || '--', status: it.status || (it.apprStatus != null ? (['pending','quoting','done','cancelled'][it.apprStatus] || 'pending') : 'pending'), apprStatus: it.apprStatus, + hidden: it.hidden, time: it.publishDate || it.createDate || it.time, raw: it })) @@ -122,6 +125,68 @@ function onStatusTab(v: string) { page.value = 1 load() } +// ===== 运营后台 隐藏/删除询价单 ===== +function reload() { load() } + +async function onHide(row: InquiryRow) { + try { + await ElMessageBox.confirm( + '隐藏后,该询价单将在平台前端(招采广场、结果公示、供应商邀请列表及详情页)完全不可见,运营后台仍可查看。', + '隐藏询价单', + { confirmButtonText: '确认隐藏', cancelButtonText: '取消', type: 'warning' } + ) + } catch { return } + try { + await httpPost('/supplier/inquiry/adminHide', { id: row.id, hidden: 1 }) + ElMessage.success('已隐藏,平台前端不再展示') + reload() + } catch (e: any) { + ElMessage.error(e?.msg || e?.message || '操作失败') + } +} + +async function onUnhide(row: InquiryRow) { + try { + await httpPost('/supplier/inquiry/adminHide', { id: row.id, hidden: 0 }) + ElMessage.success('已取消隐藏,恢复前端展示') + reload() + } catch (e: any) { + ElMessage.error(e?.msg || e?.message || '操作失败') + } +} + +async function onDelete(row: InquiryRow) { + try { + await ElMessageBox.confirm( + `
+
⚠️ 高危操作,删除后数据不可恢复
+
将彻底删除该询价单及其全部关联数据:
+
① 询价单与全部商品明细 —— 即前端展示的询价公告
+
② 全部供应商报价记录与报价明细
+
③ 邀请报价记录
+
一般场景建议仅做「隐藏」处理;确需删除(测试数据 / 错误录入 / 信息下架)请谨慎确认。
+
`, + '删除询价单', + { + confirmButtonText: '确认删除', + cancelButtonText: '取消,仅隐藏即可', + type: 'error', + confirmButtonClass: 'inq-del-confirm-btn', + dangerouslyUseHTMLString: true, + customStyle: { maxWidth: '520px' } as any + } + ) + } catch { return } + try { + const res: any = await httpPost('/supplier/inquiry/adminDelete', { id: row.id }) + const d = res || {} + ElMessage.success(`已删除:报价记录 ${d.quotation ?? 0} 条、报价明细 ${d.quotationDetail ?? 0} 条、邀请记录 ${d.quotationInvited ?? 0} 条、询价明细 ${d.inquiryDetail ?? 0} 条`) + reload() + } catch (e: any) { + ElMessage.error(e?.msg || e?.message || '删除失败') + } +} + function onRowClick(row: InquiryRow) { router.push(`/inquiry/${row.id}`) } function onView(row: InquiryRow) { router.push(`/inquiry/${row.id}`) } @@ -130,9 +195,9 @@ const columns = computed(() => [ { prop: 'customer', label: '客户', minWidth: 180 }, { prop: 'product', label: '商品', minWidth: 180 }, { prop: 'qty', label: '数量', width: 120 }, - { prop: 'apprStatus', label: '审核状态', width: 100, slot: 'apprStatus' }, + { prop: 'apprStatus', label: '审核状态', width: 175, slot: 'apprStatus' }, { prop: 'time', label: '时间', width: 170, slot: 'time' }, - { label: '操作', width: 100, slot: 'action' } + { label: '操作', width: 265, slot: 'action' } ]) onMounted(() => { load() }) @@ -174,12 +239,20 @@ onMounted(() => { load() }) {{ row.apprStatus === 1 ? '待审核' : (row.apprStatus === 2 ? '已通过' : (row.apprStatus === 3 ? '已驳回' : '--')) }} + 已隐藏 @@ -191,6 +264,12 @@ onMounted(() => { load() }) padding: 16px; } +.rr-action-cell { + display: flex; + justify-content: center; + align-items: center; +} + .rr-time { font-family: 'SF Mono', Menlo, Monaco, Consolas, monospace; font-size: 12px; @@ -216,4 +295,16 @@ onMounted(() => { load() }) .rr-badge-quoting { background: var(--brand-soft); color: var(--brand); } .rr-badge-done { background: var(--green-soft); color: var(--green); } .rr-badge-cancelled { background: var(--red-soft); color: var(--red); } - \ No newline at end of file + + + diff --git a/src/modules/order/BackfillManageListView.vue b/src/modules/order/BackfillManageListView.vue new file mode 100644 index 0000000..613b1a2 --- /dev/null +++ b/src/modules/order/BackfillManageListView.vue @@ -0,0 +1,312 @@ + + + + + diff --git a/src/modules/order/OrderDetailView.vue b/src/modules/order/OrderDetailView.vue index 1da4954..a0fdf33 100644 --- a/src/modules/order/OrderDetailView.vue +++ b/src/modules/order/OrderDetailView.vue @@ -1,337 +1,202 @@ @@ -640,4 +899,117 @@ onMounted(() => load()) @media (max-width: 1200px) { .batch-actions { margin-left: 0; } } + +/* ===== 内嵌审核视图 ===== */ +.audit-view { display: flex; flex-direction: column; gap: 14px; } +.audit-head { + display: flex; align-items: center; gap: 14px; + background: var(--surface); border-radius: 12px; padding: 12px 16px; +} +.audit-head-main { display: flex; align-items: center; gap: 10px; min-width: 0; flex: 1; } +.audit-ent { font-size: 15px; font-weight: 600; color: var(--ink); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } +.audit-card { background: var(--surface); border-radius: 12px; padding: 16px; } +.audit-card-title { + font-size: 14px; font-weight: 600; color: var(--ink); margin-bottom: 14px; + display: flex; align-items: center; gap: 10px; +} +.audit-change-count { + font-size: 11px; font-weight: 600; padding: 2px 10px; border-radius: 20px; + color: #b45309; background: var(--st-warn-bg, #fef3c7); +} +.chg-list { display: flex; flex-direction: column; } +.chg-item { display: flex; gap: 14px; padding: 12px 0; border-bottom: 1px dashed var(--border); } +.chg-item:last-child { border-bottom: none; padding-bottom: 0; } +.chg-field { flex: 0 0 120px; font-size: 13px; font-weight: 600; color: var(--ink-2); padding-top: 2px; } +.chg-body { flex: 1; display: flex; align-items: flex-start; gap: 10px; min-width: 0; } +.chg-cell { + flex: 1; min-width: 0; border-radius: 8px; padding: 8px 10px; + font-size: 13px; line-height: 1.6; word-break: break-all; +} +.chg-cell.old { background: var(--bg, #f6f8fb); border: 1px solid var(--border); } +.chg-cell.new { background: var(--st-ok-bg, #ecfdf5); border: 1px solid rgba(5, 150, 105, .25); } +.chg-tag { display: inline-block; font-size: 11px; font-weight: 600; padding: 0 8px; border-radius: 4px; margin-bottom: 6px; } +.chg-tag.old { color: var(--ink-3); background: var(--surface); border: 1px solid var(--border); } +.chg-tag.new { color: var(--st-ok-fg, #059669); background: var(--surface); border: 1px solid rgba(5, 150, 105, .35); } +.chg-arrow { flex: 0 0 auto; color: var(--ink-3); font-size: 16px; padding-top: 18px; } +.chg-imgs { display: flex; gap: 8px; flex-wrap: wrap; } +.chg-img { width: 84px; height: 84px; border-radius: 8px; border: 1px solid var(--border); background: #fff; cursor: zoom-in; } +.chg-empty { color: var(--ink-3); font-style: italic; } +.chg-none { + font-size: 13px; color: var(--ink-2); line-height: 1.7; + background: var(--bg, #f6f8fb); border-radius: 8px; padding: 14px; +} +.audit-actions { display: flex; justify-content: flex-end; gap: 10px; margin-top: 6px; } +@media (max-width: 900px) { + .chg-item { flex-direction: column; gap: 8px; } + .chg-body { flex-direction: column; } + .chg-arrow { display: none; } +} + +/* ===== 通用弹窗块样式(自 OrderListView 移植) ===== */ +.fee-section-title { font-size: 13px; font-weight: 700; color: var(--ink); margin: 16px 0 8px; } +.fee-type-cards { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; } +.fee-type-card { position: relative; border: 1.5px solid var(--border-1, #e5e7eb); border-radius: 10px; padding: 12px 14px; cursor: pointer; transition: all .15s; background: var(--bg-1, #fff); } +.fee-type-card:hover { border-color: var(--primary); } +.fee-type-card.active { border-color: var(--primary); background: var(--primary-soft, #eff6ff); box-shadow: 0 0 0 1px var(--primary) inset; } +.ftc-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 6px; } +.ftc-check { display: none; width: 18px; height: 18px; border-radius: 50%; background: var(--primary); color: #fff; font-size: 11px; text-align: center; line-height: 18px; } +.fee-type-card.active .ftc-check { display: block; } +.ftc-title { font-size: 13.5px; font-weight: 700; color: var(--ink); } +.ftc-desc { font-size: 12px; color: var(--ink-3); margin-top: 2px; } +.fee-unit { font-size: 14px; font-weight: 700; color: var(--ink); } +.fee-hint { font-size: 12px; color: var(--ink-3); } +.fee-result { margin-top: 14px; border-radius: 10px; padding: 14px 16px; background: linear-gradient(135deg, var(--primary-soft, #eff6ff), rgba(255,255,255,0)); border: 1px solid color-mix(in srgb, var(--primary) 18%, transparent); display: flex; gap: 20px; align-items: center; } +.fr-left { flex-shrink: 0; min-width: 150px; } +.fr-cap { font-size: 12px; color: var(--ink-3); } +.fr-val { font-size: 26px; font-weight: 800; color: var(--primary); line-height: 1.2; } +.fr-calc { font-size: 12px; color: var(--ink-2); margin-top: 2px; } +.fr-right { flex: 1; display: flex; flex-direction: column; gap: 7px; border-left: 1px dashed var(--border-1, #e5e7eb); padding-left: 18px; } +.fr-step { display: flex; align-items: center; gap: 8px; font-size: 12.5px; color: var(--ink-2); } +/* ===== 履约评分弹窗 ===== */ +.sc-hero { display: flex; align-items: center; gap: 14px; padding: 16px 18px; border-radius: 12px; background: var(--bg-2, #f8fafc); border: 1px solid var(--border-1, #e5e7eb); } +.sc-hero-left { flex: 1; min-width: 0; } +.sc-name { font-size: 16px; font-weight: 800; color: var(--ink); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.sc-sub { font-size: 12px; color: var(--ink-3); margin-top: 4px; } +.sc-hero-right { text-align: right; } +.sc-score-num { font-size: 34px; font-weight: 900; color: var(--primary); line-height: 1; font-family: 'SF Mono', Menlo, Consolas, monospace; } +.sc-score-cap { font-size: 11px; color: var(--ink-3); margin-top: 2px; } +.sc-badge { display: inline-flex; align-items: center; gap: 4px; padding: 6px 12px; border-radius: 999px; font-size: 13px; font-weight: 800; box-shadow: 0 1px 4px rgba(0,0,0,.08); white-space: nowrap; } +.sc-scale { position: relative; display: flex; gap: 4px; margin-top: 14px; } +.sc-seg { flex: 1; text-align: center; padding: 7px 0 6px; border-radius: 8px; opacity: .5; border: 1px solid transparent; } +.sc-seg span { font-size: 12.5px; font-weight: 800; display: block; } +.sc-seg i { font-style: normal; font-size: 10.5px; opacity: .8; } +.sc-seg.fail { background: linear-gradient(135deg,#fef2f2,#fee2e2); color: #b91c1c; } +.sc-seg.bronze { background: linear-gradient(135deg,#ffedd5,#fed7aa); color: #9a3412; } +.sc-seg.silver { background: linear-gradient(135deg,#f1f5f9,#e2e8f0); color: #475569; } +.sc-seg.gold { background: linear-gradient(135deg,#fef3c7,#fde68a); color: #b45309; } +.sc-seg.on { opacity: 1; border-color: currentColor; box-shadow: 0 0 0 1px currentColor inset; } +.sc-marker { position: absolute; top: -8px; transform: translateX(-50%); transition: left .3s; } +.sc-marker-dot { display: block; width: 14px; height: 14px; border-radius: 50%; background: var(--primary); border: 3px solid #fff; box-shadow: 0 1px 5px rgba(0,0,0,.35); } +.sc-marker-tip { position: absolute; top: -24px; left: 50%; transform: translateX(-50%); background: var(--primary); color: #fff; font-size: 11px; font-weight: 700; padding: 2px 7px; border-radius: 5px; white-space: nowrap; } +.sc-marker-tip::after { content: ''; position: absolute; top: 100%; left: 50%; transform: translateX(-50%); border: 4px solid transparent; border-top-color: var(--primary); } +.sc-scale-cap { margin-top: 6px; font-size: 11px; color: var(--ink-3); } + +.sc-dim-icon { display: inline-flex; align-items: center; justify-content: center; width: 30px; height: 30px; border-radius: 8px; background: var(--primary-soft, #dbeafe); color: var(--primary); font-size: 18px; } +.sc-weight-chip { display: inline-block; margin-left: 6px; padding: 1px 7px; font-size: 11px; font-weight: 800; color: var(--primary); background: var(--primary-soft, #dbeafe); border-radius: 4px; vertical-align: 1px; } +.sc-op-row { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; } +.sc-seg2 { display: inline-flex; border: 1px solid var(--border-1, #e5e7eb); border-radius: 10px; overflow: hidden; } +.sc-seg2-btn { display: inline-flex; align-items: center; gap: 4px; padding: 8px 18px; font-size: 13px; font-weight: 700; background: var(--bg-1, #fff); color: var(--ink-3); border: none; cursor: pointer; transition: all .15s; } +.sc-seg2-btn + .sc-seg2-btn { border-left: 1px solid var(--border-1, #e5e7eb); } +.sc-seg2-btn.plus { background: var(--el-color-success); color: #fff; } +.sc-seg2-btn.minus { background: var(--el-color-danger); color: #fff; } +.sc-arrow-text { font-size: 14px; color: var(--ink-2); } +.sc-arrow-text b { font-size: 17px; color: var(--ink); } +.sc-lv-change { font-size: 15px; font-weight: 800; } +.sc-lv-tip { font-size: 11px; font-weight: 400; opacity: .7; } +.sc-log-list { border: 1px solid var(--border-1, #e5e7eb); border-radius: 10px; overflow: hidden; } +.sc-log-item { display: flex; align-items: center; gap: 10px; padding: 8px 12px; font-size: 12px; border-bottom: 1px solid var(--border-1, #e5e7eb); } +.sc-log-item:last-child { border-bottom: none; } +.sc-log-time { color: var(--ink-3); font-family: 'SF Mono', Menlo, Monaco, Consolas, monospace; flex-shrink: 0; } +.sc-log-dim { color: var(--ink-2); font-weight: 700; flex-shrink: 0; } +.sc-log-chg { font-weight: 800; flex-shrink: 0; width: 52px; text-align: right; } +.sc-log-chg.up { color: var(--el-color-success); } +.sc-log-chg.down { color: var(--el-color-danger); } +.sc-log-range { color: var(--ink-3); flex-shrink: 0; font-family: 'SF Mono', Menlo, Monaco, Consolas, monospace; } +.sc-log-reason { color: var(--ink-3); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1; text-align: right; } \ No newline at end of file diff --git a/src/modules/user/EnterpriseScoreListView.vue b/src/modules/user/EnterpriseScoreListView.vue new file mode 100644 index 0000000..d6f5e71 --- /dev/null +++ b/src/modules/user/EnterpriseScoreListView.vue @@ -0,0 +1,453 @@ + + + + + diff --git a/src/modules/user/enterprise/EnterpriseDetailView.vue b/src/modules/user/enterprise/EnterpriseDetailView.vue index a34de7f..3850893 100644 --- a/src/modules/user/enterprise/EnterpriseDetailView.vue +++ b/src/modules/user/enterprise/EnterpriseDetailView.vue @@ -45,8 +45,13 @@ const STATUS_META: Record = { 1: '供应商', 2: '生产厂商', 3: '渠道商', 4: '服务商' } -const LEVEL_MAP: Record = { - 1: '优质供应商', 2: '钻石供应商', 3: '金牌供应商', 4: '银牌供应商', 5: '铜牌供应商' +// 企业类型 (主体类型): 1企业 2个体工商户 —— 与认证表单主体类型选择对应 +const TYPE_MAP: Record = { 1: '企业', 2: '个体工商户' } +// 平台履约等级(新体系): >=80金 / [70,80)银 / <70铜, 初始60 +const LEVEL_MAP: Record = { 3: '金牌供应商', 2: '银牌供应商', 1: '铜牌供应商' } +function levelOf(score: any): number { + const n = Number(score ?? 60) + return n >= 80 ? 3 : n >= 70 ? 2 : 1 } const status = computed(() => Number(detail.value.status ?? 0)) @@ -57,6 +62,26 @@ const statusMeta = computed(() => STATUS_META[status.value]) // 状态 → banner 修饰类 (CSS 里 banner-warning/success/danger 对应 warning/success/danger) const bannerClass = computed(() => `banner-${statusMeta.value?.type || 'unknown'}`) +// ===== 变更信息对比 (change_log: [{field, old, new}]) ===== +const auditChanges = computed(() => { + const raw = detail.value?.changeLog + if (!raw) return [] + try { + const parsed = typeof raw === 'string' ? JSON.parse(raw) : raw + if (!Array.isArray(parsed)) return [] + const list = parsed.filter((c: any) => c && c.field) + // 注册首审单的 changeLog 特征 = 全部条目 old 为空(空->填充值), 无对比意义, 隐藏 (2026-10-19) + if (list.length && list.every((c: any) => !String(c.old || '').trim())) return [] + return list + } catch { return [] } +}) +const isImageVal = (val: any) => { + if (!val) return false + const segs = String(val).split(' / ').map((x: string) => x.trim()).filter(Boolean) + return segs.length > 0 && segs.every((x: string) => /^https?:\/\//.test(x)) +} +const splitImgs = (val: any) => String(val || '').split(' / ').map((x: string) => x.trim()).filter(Boolean) + const logoText = computed(() => (detail.value.enterpriseName || '企').trim().charAt(0)) const registerYear = computed(() => { const t = detail.value.registerTime @@ -219,7 +244,7 @@ async function toggleHome() { function goStore() { const eid = detail.value.enterpriseId || detail.value.id if (!eid) return - window.open(`/v2/supplier/${eid}`, '_blank') + window.open(`/supplier/${eid}`, '_blank') } onMounted(load) @@ -234,6 +259,55 @@ onMounted(load) + +
+
+ + 本次提交变更内容 + 共 {{ auditChanges.length }} 项变更 +
+
+
+
{{ c.field }}
+
+
+ 变更前 +
+ + +
+
+
→
+
+ 变更后 +
+ + +
+
+
+
+
+
+