feat(purchase): 采购申请弹窗支持批量建单与商家链接一键跳转
批量建单:
openCreateDialog 新增 batchItems 分支 —— 待采购清单勾选多个物料时,
明细表一次性铺成 N 行,实现一单多品的合并采购。单条与批量共用
fillRowFromPrefill 做字段映射,不各写一份。
商家链接一键跳转:
输入框加后置按钮,点击在新标签页打开,方便采购员比对价格。
★ 链接先过 safeHref 白名单(只放行 http/https)—— 该字段由用户自由填写,
直接交给 window.open 会让 javascript: 之类的伪协议被当成代码执行。
非法时按钮置灰,不做静默失败。
交接方式:
单行与批量统一走 localStorage(query 里只传一次性 key,取完即删),
不经过 URL。原因:material_base.purchase_link 是 text,实测已有 516 字符的
真实外链,走 URL 会撞上长度上限并被静默截断。曾经单行走 query 时被迫引入
一个「链接多长就不带」的硬编码阈值,导致同一物料单行跳转带链接、批量跳转
不带 —— 现在两条路径统一,无长度上限。
This commit is contained in:
@ -252,9 +252,19 @@
|
|||||||
</el-select>
|
</el-select>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="商家链接" min-width="160">
|
<el-table-column label="商家链接" min-width="210">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-input v-model="row.supplier_link" placeholder="https://" clearable />
|
<!-- 后置跳转按钮:链接非法(非 http/https)时置灰,不做静默失败 -->
|
||||||
|
<el-input v-model="row.supplier_link" placeholder="https://" clearable>
|
||||||
|
<template #append>
|
||||||
|
<el-button
|
||||||
|
:icon="Link"
|
||||||
|
:disabled="!safeHref(row.supplier_link)"
|
||||||
|
title="在新标签页打开链接"
|
||||||
|
@click="openLink(row.supplier_link)"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</el-input>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="备注" min-width="150">
|
<el-table-column label="备注" min-width="150">
|
||||||
@ -458,7 +468,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, onMounted } from 'vue'
|
import { ref, computed, onMounted } from 'vue'
|
||||||
import { useRoute } from 'vue-router'
|
import { useRoute } from 'vue-router'
|
||||||
import { Refresh, Plus, Delete, CopyDocument, Picture } from '@element-plus/icons-vue'
|
import { Refresh, Plus, Delete, CopyDocument, Picture, Link } from '@element-plus/icons-vue'
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
import { useUserStore } from '@/stores/user'
|
import { useUserStore } from '@/stores/user'
|
||||||
import { searchMaterialPurchase } from '@/api/purchase'
|
import { searchMaterialPurchase } from '@/api/purchase'
|
||||||
@ -916,29 +926,98 @@ const handleStatusChange = () => {
|
|||||||
const handlePageChange = (p: number) => { page.value = p; fetchData() }
|
const handlePageChange = (p: number) => { page.value = p; fetchData() }
|
||||||
const handleSizeChange = (s: number) => { pageSize.value = s; page.value = 1; fetchData() }
|
const handleSizeChange = (s: number) => { pageSize.value = s; page.value = 1; fetchData() }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 链接白名单:只放行 http/https。
|
||||||
|
*
|
||||||
|
* ★ 商家链接由用户自由填写,直接交给 window.open 会让 `javascript:` 之类的
|
||||||
|
* 伪协议被当成可执行代码。与物料列表 / 待采购清单的 safeHref 同一约定。
|
||||||
|
*/
|
||||||
|
const safeHref = (url: any): string | null => {
|
||||||
|
const s = String(url || '').trim()
|
||||||
|
return /^https?:\/\//i.test(s) ? s : null
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 明细行「商家链接」的跳转按钮:新标签页打开,方便采购员比对价格 */
|
||||||
|
const openLink = (url: any) => {
|
||||||
|
const href = safeHref(url)
|
||||||
|
if (!href) {
|
||||||
|
ElMessage.warning('链接格式无效,仅支持 http / https 地址')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
window.open(href, '_blank', 'noopener,noreferrer')
|
||||||
|
}
|
||||||
|
|
||||||
// --- 新建 ---
|
// --- 新建 ---
|
||||||
const openCreateDialog = (prefill?: { materialId?: number; name?: string; spec?: string; unit?: string }) => {
|
/** 把一条预填数据灌进明细行(单条与批量共用同一套字段映射) */
|
||||||
|
const fillRowFromPrefill = (
|
||||||
|
row: FormItemRow,
|
||||||
|
src: { materialId: number; name?: string; spec?: string; quantity?: number; supplierLink?: string }
|
||||||
|
) => {
|
||||||
|
row.materialBaseId = src.materialId
|
||||||
|
row.name = src.name || ''
|
||||||
|
row.spec_model = src.spec || ''
|
||||||
|
// 待采购清单带来的默认数量与供应商链接(都是 FormItemRow 已有字段,
|
||||||
|
// 不需要动接口/模板/工厂函数)。unit 仍不带入 —— 明细行没有该字段。
|
||||||
|
if (src.quantity && src.quantity > 0) {
|
||||||
|
row.quantity = src.quantity
|
||||||
|
}
|
||||||
|
if (src.supplierLink) {
|
||||||
|
row.supplier_link = src.supplierLink
|
||||||
|
}
|
||||||
|
return row
|
||||||
|
}
|
||||||
|
|
||||||
|
const openCreateDialog = (prefill?: {
|
||||||
|
materialId?: number
|
||||||
|
name?: string
|
||||||
|
spec?: string
|
||||||
|
unit?: string
|
||||||
|
quantity?: number
|
||||||
|
supplierLink?: string
|
||||||
|
/** 批量:待采购清单勾选多个物料时,一次性铺成多行 */
|
||||||
|
batchItems?: Array<{
|
||||||
|
materialId: number
|
||||||
|
name?: string
|
||||||
|
spec?: string
|
||||||
|
quantity?: number
|
||||||
|
supplierLink?: string
|
||||||
|
}>
|
||||||
|
}) => {
|
||||||
dialogTitle.value = '新建采购申请'
|
dialogTitle.value = '新建采购申请'
|
||||||
form.value = {
|
form.value = {
|
||||||
approver_id: undefined,
|
approver_id: undefined,
|
||||||
remark: ''
|
remark: ''
|
||||||
}
|
}
|
||||||
|
|
||||||
// ★ 初始化明细行:若有预填物料则第一行带入,否则给一行空行
|
if (prefill?.batchItems && prefill.batchItems.length > 0) {
|
||||||
const firstRow = createEmptyFormItem()
|
// ★ 批量建单:勾选 N 个物料 → 明细表直接铺 N 行,一单多品合并采购。
|
||||||
if (prefill?.materialId) {
|
// materialOptions 必须把每一行的物料都塞进去,否则 el-select 找到不到
|
||||||
firstRow.materialBaseId = prefill.materialId
|
// 选项,已填的物料会显示成空白(单条预填时也有这个约束)。
|
||||||
firstRow.name = prefill.name || ''
|
formItems.value = prefill.batchItems.map(it =>
|
||||||
firstRow.spec_model = prefill.spec || ''
|
fillRowFromPrefill(createEmptyFormItem(), it)
|
||||||
|
)
|
||||||
|
materialOptions.value = prefill.batchItems.map(it => ({
|
||||||
|
id: it.materialId,
|
||||||
|
name: it.name || '',
|
||||||
|
spec_model: it.spec || ''
|
||||||
|
}))
|
||||||
|
} else if (prefill?.materialId) {
|
||||||
|
formItems.value = [fillRowFromPrefill(createEmptyFormItem(), {
|
||||||
|
materialId: prefill.materialId,
|
||||||
|
name: prefill.name,
|
||||||
|
spec: prefill.spec,
|
||||||
|
quantity: prefill.quantity,
|
||||||
|
supplierLink: prefill.supplierLink
|
||||||
|
})]
|
||||||
materialOptions.value = [{
|
materialOptions.value = [{
|
||||||
id: prefill.materialId,
|
id: prefill.materialId,
|
||||||
name: prefill.name || '',
|
name: prefill.name || '',
|
||||||
spec_model: prefill.spec || ''
|
spec_model: prefill.spec || ''
|
||||||
}]
|
}]
|
||||||
} else {
|
} else {
|
||||||
|
formItems.value = [createEmptyFormItem()]
|
||||||
materialOptions.value = []
|
materialOptions.value = []
|
||||||
}
|
}
|
||||||
formItems.value = [firstRow]
|
|
||||||
|
|
||||||
formDialogVisible.value = true
|
formDialogVisible.value = true
|
||||||
|
|
||||||
@ -1393,14 +1472,52 @@ onMounted(() => {
|
|||||||
fetchData()
|
fetchData()
|
||||||
fetchApprovers()
|
fetchApprovers()
|
||||||
|
|
||||||
// 从基础信息跳转过来的参数
|
// 跨页跳转过来的预填参数。生产端有两处:
|
||||||
|
// - 基础信息 / 基础信息(Odoo) 的「发起采购申请」按钮
|
||||||
|
// - 待采购清单的「生成采购申请」按钮(额外带 quantity / supplier_link)
|
||||||
|
//
|
||||||
|
// ★ 本段依赖**组件重建**:两个生产端都用 window.open 开新标签页,
|
||||||
|
// 每次都是全新实例,onMounted 必然触发。
|
||||||
|
// 若将来有人改成「同 path 的 router.push」(如 /purchase?material_id=2 → material_id=3),
|
||||||
|
// 本段不会重跑,预填会静默失效 —— 届时必须改造为
|
||||||
|
// watch(() => route.query.material_id, ..., { immediate: true })。
|
||||||
const query = route.query
|
const query = route.query
|
||||||
|
|
||||||
|
// 批次 A:待采购清单「批量生成采购申请」——物料较多(含可长达数百字符的
|
||||||
|
// 商家链接),塞进 URL 有长度风险,故走 localStorage 中转,query 里只传一个键。
|
||||||
|
if (query.batch_key) {
|
||||||
|
const key = String(query.batch_key)
|
||||||
|
try {
|
||||||
|
const raw = localStorage.getItem(key)
|
||||||
|
// ★ 读完立刻删除:这是一次性交接数据,留着重进页面会重复弹出;
|
||||||
|
// 多个标签页各自用独立的 key,互不干扰。
|
||||||
|
localStorage.removeItem(key)
|
||||||
|
const items = raw ? JSON.parse(raw) : []
|
||||||
|
if (Array.isArray(items) && items.length > 0) {
|
||||||
|
openCreateDialog({ batchItems: items })
|
||||||
|
} else {
|
||||||
|
ElMessage.warning('批量数据已失效,请回到待采购清单重新勾选')
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
localStorage.removeItem(key)
|
||||||
|
ElMessage.error('批量数据解析失败,请重新勾选')
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 批次 B:单条预填,走 URL query。
|
||||||
|
// 生产端只有基础信息 / 基础信息(Odoo) 的「发起采购申请」按钮
|
||||||
|
// (它们只带 id/名称/规格,不带链接,所以没有 URL 长度风险)。
|
||||||
|
// 待采购清单的单行与批量按钮统一走上面的 batch_key,两条路径共用一套回填。
|
||||||
if (query.material_id) {
|
if (query.material_id) {
|
||||||
|
const rawQty = Number(query.quantity)
|
||||||
openCreateDialog({
|
openCreateDialog({
|
||||||
materialId: Number(query.material_id),
|
materialId: Number(query.material_id),
|
||||||
name: (query.name as string) || '',
|
name: (query.name as string) || '',
|
||||||
spec: (query.spec as string) || '',
|
spec: (query.spec as string) || '',
|
||||||
unit: (query.unit as string) || ''
|
unit: (query.unit as string) || '',
|
||||||
|
quantity: rawQty > 0 ? rawQty : undefined,
|
||||||
|
supplierLink: (query.supplier_link as string) || ''
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user