一键发布
Some checks failed
CI / build (20.x) (push) Waiting to run
CI / lint (push) Waiting to run
CI / test (push) Waiting to run
CI / deploy-preview (push) Blocked by required conditions
CI / security (push) Waiting to run
CI / build (18.x) (push) Has been cancelled

This commit is contained in:
97694731 2026-06-30 15:19:07 +08:00
parent 02373f243e
commit 40660f5beb
18 changed files with 642 additions and 50 deletions

View File

@ -58,7 +58,7 @@ export const kongfzLogin = (username, password, ip, port) => {
/**
* 批量提交 Token 到核价器
* @param {Array<{username: string, token: string}>} tokens - 账号 Token 列表
* @param {Array<{username: string, token: string, login_name: string}>} tokens - 账号 Token 列表
* @param {string} ip - 核价器 IP
* @param {string} port - 核价器端口
* @returns {Promise}

View File

@ -63,13 +63,16 @@ export const fetchInventoryStats = async (warehouse_id) => {
* 按库位分组获取商品库存列表 GET /api/inventory/grouped-list
* 返回按库位分组的库存数据每组包含库位信息和该库位下的商品列表
* @param {Object} params - 请求参数
* @param {string} [params.keyword] - 搜索关键词库位编号/商品名称/条码
* @param {number} [params.warehouse_id] - 仓库ID
* @param {number} [params.location_id] - 库位ID
* @param {number} [params.page] - 当前页码
* @param {number} [params.pageSize] - 每页条数
* @returns {Promise<{ list: Array, total: number }>}
*/
export const fetchGoodsListByLocation = async ({ keyword, page, pageSize } = {}) => {
export const fetchGoodsListByLocation = async ({ warehouse_id, location_id, keyword, page, pageSize } = {}) => {
const params = {
warehouse_id: warehouse_id || undefined,
location_id: location_id || undefined,
keyword: keyword || undefined,
page,
page_size: pageSize

View File

@ -43,11 +43,12 @@ const normalizeListResponse = (payload) => {
* @param {string} [params.page_size] - 每页条数
* @returns {Promise<{ list: Array, total: number }>} 标准化后的出库单列表
*/
export const fetchOutboundList = async ({ out_no, status, warehouse_id, customer_id, shop_type, sales_order_id, start_date, end_date, association_order_no, logistics_no, page, page_size }) => {
export const fetchOutboundList = async ({ out_no, status, warehouse_id, location_id, customer_id, shop_type, sales_order_id, start_date, end_date, association_order_no, logistics_no, page, page_size }) => {
const params = {
out_no: out_no || undefined,
status: status || undefined,
warehouse_id: warehouse_id || undefined,
location_id: location_id || undefined,
customer_id: customer_id || undefined,
shop_type: shop_type || undefined,
sales_order_id: sales_order_id || undefined,

View File

@ -1,8 +1,13 @@
import request from '@/utils/request'
import axios from 'axios'
/** 商品模块 API 基础路径 */
const API_BASE = '/product'
/** 不知鱼书 API 基础地址 */
// const BZY_API_BASE = 'http://192.168.101.127:8080'
const BZY_API_BASE = 'https://api.buzhiyushu.cn'
/**
* 标准化列表接口返回的数据格式
* @param {Object} payload - 接口返回的原始响应对象通常包含 data 字段
@ -397,4 +402,27 @@ export const restoreProduct = async ({ destroy_log_id }) => {
*/
export const syncGoodsFromWdt = async ({ start_time, end_time }) => {
return request.get('/wangdian/query-goods', { params: { start_time, end_time } })
}
/**
* 发布商品到选中店铺
* @param {Object} params
* @param {number} params.oneClick - 是否一键发布 1= 0=
* @param {number} params.userId - 用户的 about_id
* @param {string} [params.shopIds] - 选中的店铺ID逗号分隔一键发布时使用
* @param {string} [params.productIds] - 选中的商品ID逗号分隔普通发布时使用
* @returns {Promise}
*/
export const releaseGoods = async ({ oneClick, userId, shopIds, productIds }) => {
const res = await axios.post(`${BZY_API_BASE}/zhishu/product/releaseGoods`, {
oneClick,
userId,
...(shopIds !== undefined && { shopIds }),
...(productIds !== undefined && { productIds })
})
const data = res.data
if (data && data.code !== 200) {
throw { response: { data } }
}
return data
}

View File

@ -25,6 +25,7 @@ const normalizeListResponse = (payload) => {
* @param {number} [params.status] - 订单状态筛选
* @param {number} [params.customer_id] - 平台ID筛选
* @param {number} [params.warehouse_id] - 仓库ID筛选
* @param {number} [params.location_id] - 库位ID筛选
* @param {number} [params.page] - 当前页码
* @param {number} [params.pageSize] - 每页条数
* @param {string} [params.sort_by] - 排序字段
@ -33,12 +34,13 @@ const normalizeListResponse = (payload) => {
* @param {string} [params.logistics_no] - 快递单号
* @returns {Promise<{ list: Array, total: number }>} 标准化后的销售订单列表
*/
export const fetchSalesOrderList = async ({ keyword, status, shop_type, warehouse_id, page, pageSize, sort_by, sort_order, association_order_no, logistics_no }) => {
export const fetchSalesOrderList = async ({ keyword, status, shop_type, warehouse_id, location_id, page, pageSize, sort_by, sort_order, association_order_no, logistics_no }) => {
const params = {
so_no: keyword || undefined,
status,
shop_type: shop_type || undefined,
warehouse_id,
location_id: location_id || undefined,
page,
page_size: pageSize,
sort_by: sort_by || 'updated_at',
@ -139,14 +141,22 @@ export const returnSalesOrderItem = async (data) => {
* @param {Object} params - 请求参数
* @param {number} params.page - 当前页码
* @param {number} params.pageSize - 每页条数
* @param {number} [params.warehouse_id] - 仓库ID
* @param {number} [params.location_id] - 库位ID
* @param {number} [params.status] - 状态
* @param {string} [params.keyword] - 销售订单号
* @param {string} [params.association_order_no] - 第三方订单编号
* @param {string} [params.logistics_no] - 快递单号
* @returns {Promise<{list: Array, total: number}>}
*/
export const fetchSalesOrderDetails = async ({ page, pageSize, shop_type, association_order_no, logistics_no }) => {
export const fetchSalesOrderDetails = async ({ page, pageSize, warehouse_id, location_id, status, keyword, shop_type, association_order_no, logistics_no }) => {
const params = {
page,
page_size: pageSize,
warehouse_id: warehouse_id || undefined,
location_id: location_id || undefined,
status: status || undefined,
so_no: keyword || undefined,
shop_type: shop_type || undefined,
association_order_no: association_order_no || undefined,
logistics_no: logistics_no || undefined

View File

@ -38,12 +38,13 @@ const normalizeListResponse = (payload) => {
* @param {number} [params.pageSize] - 每页条数
* @returns {Promise<{ list: Array, total: number }>} 标准化后的发货单列表
*/
export const fetchShippingOrderList = async ({ check_no, status, shop_type, warehouse_id, sales_order_id, wave_task_id, association_order_no, logistics_no, page, pageSize }) => {
export const fetchShippingOrderList = async ({ check_no, status, shop_type, warehouse_id, location_id, sales_order_id, wave_task_id, association_order_no, logistics_no, page, pageSize }) => {
const params = {
check_no: check_no || undefined,
status,
shop_type: shop_type || undefined,
warehouse_id,
location_id: location_id || undefined,
sales_order_id,
wave_task_id,
association_order_no: association_order_no || undefined,

View File

@ -62,9 +62,7 @@
<Search />
</el-icon></template>
</el-input>
<el-select v-model="summaryParams.warehouse_id" placeholder="仓库" clearable style="width: 140px">
<el-option v-for="item in warehouseOptions" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
<WarehouseSelect v-model="summaryParams.location_id" @warehouse-change="onSummaryWarehouseChange" />
<el-button type="primary" :icon="Search" @click="loadSummaryList">搜索</el-button>
<el-button :icon="Refresh" @click="resetSummaryParams">重置</el-button>
</div>
@ -74,8 +72,7 @@
<el-table-column prop="barcode" label="ISBN/条码" min-width="130" show-overflow-tooltip align="center" />
<el-table-column label="货位" min-width="100" align="center">
<template #default="{ row }">{{ row.warehouse_code || warehouseMap[row.warehouse_code] || '-' }}##{{
row.location_code || warehouseMap[row.location_code] || '-' }}</template>
<template #default="{ row }">{{ row.location_code || '-' }}</template>
</el-table-column>
<el-table-column label="仓库" min-width="100" align="center">
<template #default="{ row }">{{ row.warehouse_name || warehouseMap[row.warehouse_id] || '-' }}</template>
@ -173,6 +170,7 @@ import { Search, Refresh, Box, Goods, CircleCheckFilled, CircleCheck, Location }
import dayjs from 'dayjs'
import { fetchInventoryList, fetchInventoryDetailList, inventorySummary } from '@/api/inventory'
import { fetchWarehouseList } from '@/api/warehouse'
import WarehouseSelect from '@/components/warehouseSelect/index.vue'
defineOptions({ name: 'InventoryByGoods' })
@ -207,7 +205,8 @@ const summaryList = ref<any[]>([])
const summaryParams = reactive({
isbn: '',
name: '',
warehouse_id: null as number | null
warehouse_id: null as number | null,
location_id: null as number | null
})
const summaryPagination = reactive({ current: 1, pageSize: 20, total: 0 })
@ -248,6 +247,7 @@ const loadSummaryList = async (): Promise<void> => {
isbn: summaryParams.isbn || undefined,
name: summaryParams.name || undefined,
warehouse_id: summaryParams.warehouse_id || undefined,
location_id: summaryParams.location_id || undefined,
page: summaryPagination.current,
page_size: summaryPagination.pageSize
})
@ -309,10 +309,16 @@ const navigateToShippingOrder = (shippingNo: string) => {
}
// ==================== ====================
const onSummaryWarehouseChange = (warehouseId: number | null) => {
summaryParams.warehouse_id = warehouseId
summaryParams.location_id = null
}
const resetSummaryParams = (): void => {
summaryParams.isbn = ''
summaryParams.name = ''
summaryParams.warehouse_id = null
summaryParams.location_id = null
summaryPagination.current = 1
loadSummaryList()
}

View File

@ -39,6 +39,12 @@
</div>
</div>
<div class="filter-bar">
<WarehouseSelect v-model="queryParams.location_id" @warehouse-change="onLocationWarehouseChange" />
<el-button type="primary" :icon="Search" @click="loadList">搜索</el-button>
<el-button :icon="Refresh" @click="resetQueryParams">重置</el-button>
</div>
<el-table
:data="summaryList"
v-loading="summaryLoading"
@ -165,10 +171,17 @@ import { Box, Goods, CircleCheckFilled, CircleCheck, Location, Search, Refresh }
import dayjs from 'dayjs'
import { fetchGoodsListByLocation, fetchInventoryDetailList, inventorySummary } from '@/api/inventory'
import { fetchWarehouseList } from '@/api/warehouse'
import WarehouseSelect from '@/components/warehouseSelect/index.vue'
const summaryLoading = ref(false)
const summaryList = ref<any[]>([])
//
const queryParams = reactive({
warehouse_id: null as number | null,
location_id: null as number | null
})
//
const warehouseOptions = ref<any[]>([])
const warehouseMap = ref<Record<string, string>>({})
@ -267,10 +280,24 @@ const loadOptions = async (): Promise<void> => {
}
}
const onLocationWarehouseChange = (warehouseId: number | null) => {
queryParams.warehouse_id = warehouseId
queryParams.location_id = null
}
const resetQueryParams = (): void => {
queryParams.warehouse_id = null
queryParams.location_id = null
loadList()
}
const loadList = async (): Promise<void> => {
summaryLoading.value = true
try {
const res = await fetchGoodsListByLocation()
const res = await fetchGoodsListByLocation({
warehouse_id: queryParams.warehouse_id || undefined,
location_id: queryParams.location_id || undefined
})
summaryList.value = res.list || []
} catch (error) {
ElMessage.error({ message: '加载库位库存失败', customClass: 'scan-error-message' })

View File

@ -48,13 +48,13 @@
<el-button type="primary">导出</el-button>
<el-button type="primary">导出模板</el-button>
<el-button type="primary" @click="handleImportClick">导入</el-button>
<el-button type="primary">发布</el-button>
<el-button type="primary">一键发布</el-button>
<el-button type="primary" @click="handlePublish">发布</el-button>
<el-button type="primary" @click="handleOneClickPublish">一键发布</el-button>
<el-button type="primary">库存同步</el-button>
<el-button type="primary">批量修改货区</el-button>
</div>
<el-table :data="tableData" v-loading="loading" border stripe style="width: 100%"
<el-table ref="productTableRef" :data="tableData" v-loading="loading" border stripe style="width: 100%"
@selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column prop="name" label="商品名称" min-width="100" show-overflow-tooltip align="center" />
@ -244,6 +244,49 @@
</span>
</template>
</el-dialog>
<!-- 发布弹窗 -->
<el-dialog v-model="publishDialogVisible" :title="publishMode === 'oneClick' ? '一键发布 - 选择店铺' : '发布 - 选择店铺'" width="750px" destroy-on-close
@open="loadPublishShops">
<div v-loading="publishLoading" class="publish-dialog-body">
<template v-if="publishShopGroups.length > 0">
<div v-for="group in publishShopGroups" :key="group.shop_type" class="publish-shop-group">
<div class="publish-group-header">
<el-checkbox
:model-value="group.checkedIds.length === group.shops.length && group.shops.length > 0"
:indeterminate="group.checkedIds.length > 0 && group.checkedIds.length < group.shops.length"
@change="(val: boolean) => toggleGroupAll(group, val)"
/>
<span class="publish-group-title">{{ group.label }}</span>
<span class="publish-group-count">({{ group.shops.length }} 个店铺)</span>
</div>
<el-checkbox-group v-model="group.checkedIds" class="publish-shop-cards">
<el-checkbox
v-for="shop in group.shops"
:key="shop.shop_id"
:label="shop.shop_id"
class="publish-shop-card"
border
>
<div class="shop-card-content">
<div class="shop-card-name">{{ shop.shop_alias_name }}</div>
<div class="shop-card-type">{{ shop.shop_type_name }}</div>
</div>
</el-checkbox>
</el-checkbox-group>
</div>
</template>
<el-empty v-else description="暂无可用店铺" />
</div>
<template #footer>
<span class="dialog-footer">
<el-button @click="publishDialogVisible = false">取消</el-button>
<el-button type="primary" :loading="publishSubmitting" @click="confirmPublish">
确认发布
</el-button>
</span>
</template>
</el-dialog>
</div>
</template>
@ -253,8 +296,9 @@ import { ElMessage, ElMessageBox } from 'element-plus'
import { Search, Refresh, MoreFilled, Upload, Edit, Delete } from '@element-plus/icons-vue'
import GoodsPop from '@/components/goodsPop/index.vue'
import dayjs from 'dayjs'
import { fetchProductList, retryProductPublish, importProductsByExcel, deleteProduct, destroyProduct, syncGoodsFromWdt } from '@/api/product'
import { fetchProductList, retryProductPublish, importProductsByExcel, deleteProduct, destroyProduct, syncGoodsFromWdt, releaseGoods } from '@/api/product'
import { fetchWarehouseList } from '@/api/warehouse'
import { fetchShopList } from '@/api/shop'
interface ShopListItem {
shop_alias_name: string
@ -305,6 +349,7 @@ export default defineComponent({
const loading = ref<boolean>(false)
const tableData = ref<ProductItem[]>([])
const selectedRows = ref<ProductItem[]>([])
const productTableRef = ref<InstanceType<typeof ElTable> | null>(null)
const loadedOnce = ref<boolean>(false)
const searchParams = reactive<{
@ -568,6 +613,153 @@ export default defineComponent({
}
// ========== Excel End ==========
// ========== / ==========
const publishMode = ref<'normal' | 'oneClick'>('normal')
const publishDialogVisible = ref(false)
const publishLoading = ref(false)
const publishSubmitting = ref(false)
interface PublishShopItem {
shop_id: number
shop_alias_name: string
shop_name: string
shop_type: number
shop_type_name: string
}
interface PublishShopGroup {
shop_type: number
label: string
shops: PublishShopItem[]
checkedIds: number[]
}
const publishShopGroups = ref<PublishShopGroup[]>([])
const SHOP_TYPE_MAP: Record<number, string> = {
1: '拼多多',
2: '孔夫子',
5: '闲鱼'
}
const TARGET_SHOP_TYPES = [1, 2, 5]
const toggleGroupAll = (group: PublishShopGroup, val: boolean) => {
group.checkedIds = val ? group.shops.map(s => s.shop_id) : []
}
const loadPublishShops = async () => {
publishLoading.value = true
try {
const res = await fetchShopList({ pageSize: 100 })
const allShops = (res as any).list || []
const groups: PublishShopGroup[] = TARGET_SHOP_TYPES.map(type => ({
shop_type: type,
label: SHOP_TYPE_MAP[type] || `类型${type}`,
shops: [],
checkedIds: []
}))
for (const shop of allShops) {
const rawType = Number(shop.shopType ?? shop.shop_type)
const group = groups.find(g => g.shop_type === rawType)
if (group) {
group.shops.push({
shop_id: Number(shop.id || shop.shop_id),
shop_alias_name: shop.shopAliasName || shop.shop_alias_name || shop.shopName || shop.shop_name || '',
shop_name: shop.shopName || shop.shop_name || '',
shop_type: rawType,
shop_type_name: SHOP_TYPE_MAP[rawType] || ''
})
}
}
publishShopGroups.value = groups.filter(g => g.shops.length > 0)
} catch {
ElMessage.error({ message: '获取店铺列表失败', customClass: 'scan-error-message' })
publishShopGroups.value = []
} finally {
publishLoading.value = false
}
}
const handlePublish = () => {
if (selectedRows.value.length === 0) {
ElMessage.warning({ message: '请先选择要发布的商品', customClass: 'scan-error-message' })
return
}
//
const unplacedRows = selectedRows.value.filter(r => !r.warehouse_name)
if (unplacedRows.length > 0) {
ElMessage.warning({
message: `当前勾选 ${unplacedRows.length} 个未落位商品,请重新选择`,
customClass: 'scan-error-message'
})
return
}
publishMode.value = 'normal'
publishShopGroups.value = []
publishDialogVisible.value = true
}
const handleOneClickPublish = () => {
publishMode.value = 'oneClick'
publishShopGroups.value = []
publishDialogVisible.value = true
}
const confirmPublish = async () => {
const allIds = publishShopGroups.value.flatMap(g => g.checkedIds)
if (allIds.length === 0) {
ElMessage.warning({ message: '请至少选择一个店铺', customClass: 'scan-error-message' })
return
}
const userInfoStr = localStorage.getItem('admin_userInfo')
if (!userInfoStr) {
ElMessage.error({ message: '未获取到用户信息', customClass: 'scan-error-message' })
return
}
let aboutId: string
try {
const userInfo = JSON.parse(userInfoStr)
aboutId = userInfo.about_id
if (!aboutId) {
ElMessage.error({ message: '用户信息中缺少 about_id', customClass: 'scan-error-message' })
return
}
} catch {
ElMessage.error({ message: '用户信息解析失败', customClass: 'scan-error-message' })
return
}
const baseParams: Record<string, any> = {
userId: Number(aboutId),
shopIds: allIds.join(',')
}
if (publishMode.value === 'normal') {
const productIds = selectedRows.value.map(r => r.id).join(',')
baseParams.oneClick = 0
baseParams.productIds = productIds
} else {
baseParams.oneClick = 1
}
publishSubmitting.value = true
try {
await releaseGoods(baseParams)
ElMessage.success({ message: `已成功发布到 ${allIds.length} 个店铺`, customClass: 'scan-success-message' })
publishDialogVisible.value = false
} catch (err: any) {
console.error('发布失败', err)
const msg = err?.response?.data?.msg || err?.response?.data?.message || '发布失败,请稍后重试'
ElMessage.error({ message: msg, customClass: 'scan-error-message' })
} finally {
publishSubmitting.value = false
}
}
// ========== End ==========
onMounted(() => {
void loadProductList()
})
@ -583,7 +775,13 @@ export default defineComponent({
refreshList, selectedRows, handleSelectionChange,
//
importDialogVisible, importWarehouseId, importFile, importLoading, uploadRef, warehouseOptions,
handleImportClick, handleFileChange, handleImportSubmit
handleImportClick, handleFileChange, handleImportSubmit,
// /
publishMode, publishDialogVisible, publishLoading, publishSubmitting, publishShopGroups,
handleOneClickPublish, loadPublishShops, toggleGroupAll, confirmPublish,
//
handlePublish,
productTableRef
}
}
})
@ -667,4 +865,79 @@ export default defineComponent({
.shop-item {
line-height: 1.4;
}
/* ========== 一键发布弹窗 ========== */
.publish-dialog-body {
min-height: 200px;
max-height: 55vh;
overflow-y: auto;
}
.publish-shop-group {
margin-bottom: 24px;
}
.publish-group-header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 12px;
padding: 8px 12px;
background: #f5f7fa;
border-radius: 8px;
}
.publish-group-title {
font-size: 15px;
font-weight: 600;
color: #303133;
}
.publish-group-count {
font-size: 13px;
color: #909399;
}
.publish-shop-cards {
display: flex;
flex-wrap: wrap;
gap: 12px;
padding-left: 4px;
}
.publish-shop-card {
width: 200px;
height: auto !important;
margin-right: 0 !important;
}
.publish-shop-card :deep(.el-checkbox__input) {
align-self: center;
margin-top: 0;
}
.publish-shop-card :deep(.el-checkbox__label) {
width: 100%;
padding: 0;
display: flex;
align-items: center;
}
.shop-card-content {
padding: 12px 8px 12px 8px;
width: 100%;
}
.shop-card-name {
font-size: 14px;
font-weight: 500;
color: #303133;
word-break: break-all;
margin-bottom: 4px;
}
.shop-card-type {
font-size: 12px;
color: #909399;
}
</style>

View File

@ -0,0 +1,143 @@
<template>
<div class="warehouse-select-wrapper">
<el-select
v-model="selectedWarehouseId"
placeholder="选择仓库"
clearable
filterable
:loading="warehouseLoading"
style="width: 130px"
@change="onWarehouseChange"
@clear="onClear"
@update:model-value="$emit('warehouseChange', $event)"
>
<el-option
v-for="item in warehouseList"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
<el-select
:model-value="modelValue"
placeholder="选择库位"
clearable
filterable
:disabled="!selectedWarehouseId"
:loading="locationLoading"
style="width: 150px"
@update:model-value="$emit('update:modelValue', $event)"
>
<el-option
v-for="item in locationList"
:key="item.id"
:label="item.code"
:value="item.id"
/>
</el-select>
</div>
</template>
<script setup lang="ts">
/**
* WarehouseSelect 仓库库位联动选择器
*
* 先选仓库再选库位v-model 绑定库位 ID
*
* 用法
* <WarehouseSelect v-model="locationId" />
*/
import { ref, watch } from 'vue'
import { fetchWarehouseList } from '@/api/warehouse'
import { fetchLocationList } from '@/api/location'
interface WarehouseItem {
id: number
name: string
code?: string
status?: number
}
interface LocationItem {
id: number
code: string
name?: string
warehouse_id?: number
warehouse_name?: string
status?: number
}
const props = defineProps<{
modelValue?: number | null
}>()
const emit = defineEmits<{
'update:modelValue': [value: number | null]
warehouseChange: [value: number | null]
}>()
const selectedWarehouseId = ref<number | null>(null)
const warehouseList = ref<WarehouseItem[]>([])
const locationList = ref<LocationItem[]>([])
const warehouseLoading = ref(false)
const locationLoading = ref(false)
/** 加载仓库列表 */
async function loadWarehouses() {
warehouseLoading.value = true
try {
const res = await fetchWarehouseList({ keyword: '', page: 1, pageSize: 9999 })
warehouseList.value = res.list || []
} catch {
warehouseList.value = []
} finally {
warehouseLoading.value = false
}
}
/** 加载指定仓库下的库位 */
async function loadLocations(warehouseId: number) {
locationLoading.value = true
try {
const res = await fetchLocationList({
warehouseId,
page: 1,
pageSize: 9999,
})
locationList.value = res.list || []
} catch {
locationList.value = []
} finally {
locationLoading.value = false
}
}
function onWarehouseChange(val: number | null) {
if (val) {
loadLocations(val)
//
emit('update:modelValue', null)
} else {
locationList.value = []
emit('update:modelValue', null)
}
}
function onClear() {
selectedWarehouseId.value = null
locationList.value = []
emit('update:modelValue', null)
emit('warehouseChange', null)
}
//
loadWarehouses()
</script>
<style scoped>
.warehouse-select-wrapper {
display: inline-flex;
gap: 8px;
align-items: center;
}
</style>

View File

@ -237,10 +237,12 @@ async function refreshSalePrices() {
}
})
//
//
const newPriceStr = localStorage.getItem('new_price')
const failPrice = newPriceStr ? parseFloat(newPriceStr) * 100 : 999900
if (list.length > 0) {
const lastItem = list[list.length - 1]
if (lastItem.sale_price === 999900) {
if (lastItem.sale_price === failPrice) {
const productId = lastItem.id
if (productId !== lastFailedProductId) {
isAlertShowing = true

View File

@ -115,6 +115,10 @@ async function printWaybillForGroup(group, logisticsCompany = 'YUNDA') {
})
console.log('createOrderBatch 返回:', createRes)
// 报错之后需要停下来
if (createRes?.code !== 0) {
throw new Error(createRes?.msg || createRes?.message || '创建快递面单失败,请检查参数后重试')
}
// 校验快递打印机配置
const expressPrinter = localStorage.getItem('printer_express')

View File

@ -522,7 +522,7 @@ export default {
try {
//
const successfulAccounts: { username: string; token: string }[] = []
const successfulAccounts: { username: string; token: string; login_name: string }[] = []
for (let i = 0; i < accounts.value.length; i++) {
const item = accounts.value[i]
if (item.account && item.password) {
@ -530,7 +530,7 @@ export default {
const loginRes = await kongfzLogin(item.account, item.password, ip.value, port.value)
console.log('核价器登录响应:', loginRes)
if (loginRes?.code === 200 && loginRes?.data) {
const { token, nickname: username } = loginRes.data
const { token, nickname: username, mobile } = loginRes.data
accounts.value[i].token = token
accounts.value[i].username = username
localStorage.setItem(username, token)
@ -539,7 +539,7 @@ export default {
username,
token
})
successfulAccounts.push({ username, token })
successfulAccounts.push({ username, token, login_name: mobile })
console.log(`账号 ${item.account} 登录成功username: ${username}`)
} else {
console.log(`账号 ${item.account} 登录失败:`, loginRes?.message)
@ -605,14 +605,14 @@ export default {
appendLoading.value = true
try {
const successfulAccounts: { username: string; token: string }[] = []
const successfulAccounts: { username: string; token: string; login_name: string }[] = []
for (let i = 0; i < appendAccounts.value.length; i++) {
const item = appendAccounts.value[i]
if (item.account && item.password) {
try {
const loginRes = await kongfzLogin(item.account, item.password, ip.value, port.value)
if (loginRes?.code === 200 && loginRes?.data) {
const { token, nickname: username } = loginRes.data
const { token, nickname: username, mobile } = loginRes.data
appendAccounts.value[i].token = token
appendAccounts.value[i].username = username
localStorage.setItem(username, token)
@ -621,7 +621,7 @@ export default {
username,
token
})
successfulAccounts.push({ username, token })
successfulAccounts.push({ username, token, login_name: mobile })
}
} catch (loginError: any) {
console.log(`追加账号 ${item.account} 登录失败:`, loginError.message)

View File

@ -53,9 +53,7 @@
<Search />
</el-icon></template>
</el-input>
<el-select v-model="logParams.warehouse_id" placeholder="仓库" clearable style="width: 140px">
<el-option v-for="item in warehouseOptions" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
<WarehouseSelect v-model="logParams.location_id" @warehouse-change="onLogWarehouseChange" />
<el-select v-model="logParams.change_type" placeholder="变动类型" clearable style="width: 120px">
<el-option v-for="item in changeTypeOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
@ -134,6 +132,7 @@ import { fetchInventoryLogList } from '@/api/inventory'
import { fetchWarehouseList } from '@/api/warehouse'
import InventoryByGoods from '@/components/inventory/byGoods/index.vue'
import InventoryByLocation from '@/components/inventory/byLocation/index.vue'
import WarehouseSelect from '@/components/warehouseSelect/index.vue'
/** 变动类型映射(对齐后端 change_type 枚举) */
const CHANGE_TYPE_MAP: Record<number, { label: string; type: string }> = {
@ -147,7 +146,7 @@ const CHANGE_TYPE_MAP: Record<number, { label: string; type: string }> = {
export default defineComponent({
name: 'Inventory',
components: { Search, Refresh, InventoryByGoods, InventoryByLocation },
components: { Search, Refresh, InventoryByGoods, InventoryByLocation, WarehouseSelect },
setup() {
const activeTab = ref<string>('summary')
const queryMode = ref<'goods' | 'location'>('goods')
@ -167,6 +166,7 @@ export default defineComponent({
book_name: '',
related_order_no: '',
warehouse_id: null as number | null,
location_id: null as number | null,
change_type: null as number | null
})
const logDateRange = ref<string[]>([])
@ -201,6 +201,10 @@ export default defineComponent({
}
// ==================== ====================
const onLogWarehouseChange = (warehouseId: number | null) => {
logParams.warehouse_id = warehouseId
}
const loadLogList = async (): Promise<void> => {
logLoading.value = true
try {
@ -210,6 +214,7 @@ export default defineComponent({
isbn: logParams.isbn || undefined,
book_name: logParams.book_name || undefined,
warehouse_id: logParams.warehouse_id || undefined,
location_id: logParams.location_id || undefined,
change_type: logParams.change_type || undefined,
related_order_no: logParams.related_order_no || undefined
}
@ -234,6 +239,7 @@ export default defineComponent({
logParams.book_name = ''
logParams.related_order_no = ''
logParams.warehouse_id = null
logParams.location_id = null
logParams.change_type = null
logDateRange.value = []
logPagination.current = 1
@ -256,6 +262,7 @@ export default defineComponent({
warehouseOptions,
warehouseMap,
//
onLogWarehouseChange,
logLoading,
logList,
logParams,

View File

@ -36,9 +36,11 @@
<el-select v-model="searchParams.status" placeholder="状态" clearable style="width: 140px">
<el-option v-for="item in statusOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
<el-select v-model="searchParams.warehouse_id" placeholder="仓库" clearable style="width: 160px">
<el-option v-for="item in warehouseOptions" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
<WarehouseSelect
:model-value="searchParams.location_id"
@update:model-value="searchParams.location_id = $event"
@warehouse-change="searchParams.warehouse_id = $event"
/>
<el-select v-model="searchParams.shop_type" placeholder="平台" clearable style="width: 180px">
<el-option v-for="item in shopTypeOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
@ -58,7 +60,7 @@
<template #default="{ row }">
<div v-if="detailCache[row.id]" style="padding: 12px 20px">
<h4 style="margin: 0 0 10px; font-size: 14px; color: #303133">出库单明细</h4>
<el-table :data="detailCache[row.id].items || []" border size="small">
<el-table :data="getFilteredDetailItems(row.id)" border size="small">
<el-table-column prop="product_name" label="商品名称" min-width="120" show-overflow-tooltip align="center" />
<el-table-column prop="product_code" label="ISBN/条码" min-width="100" show-overflow-tooltip
align="center" />
@ -151,6 +153,11 @@
</el-icon>
</template>
</el-input>
<WarehouseSelect
:model-value="outboundLocationId"
@update:model-value="outboundLocationId = $event"
@warehouse-change="outboundWarehouseId = $event"
/>
<span class="selected-tip">已选 <strong>{{ selectedOutboundOrders.length }}</strong> </span>
</div>
<!-- 出库单列表 -->
@ -216,6 +223,7 @@ import {
} from '@/api/outbound'
import { createShippingOrder } from '@/api/shippingOrder'
import { fetchWarehouseList } from '@/api/warehouse'
import WarehouseSelect from '@/components/warehouseSelect/index.vue'
/** 状态映射 */
const STATUS_MAP: Record<number, { label: string; type: string }> = {
1: { label: '待审核', type: 'warning' },
@ -247,6 +255,7 @@ interface OutboundForm {
export default defineComponent({
name: 'Outbound',
components: { WarehouseSelect },
setup() {
const loading = ref<boolean>(false)
const submitLoading = ref<boolean>(false)
@ -261,6 +270,7 @@ export default defineComponent({
keyword: string
status: number | null
warehouse_id: number | null
location_id: number | null
shop_type: number | null
association_order_no: string
logistics_no: string
@ -268,6 +278,7 @@ export default defineComponent({
keyword: '',
status: null,
warehouse_id: null,
location_id: null,
shop_type: null,
association_order_no: '',
logistics_no: ''
@ -328,6 +339,8 @@ export default defineComponent({
const outboundOrderList = ref<any[]>([])
const outboundLoading = ref<boolean>(false)
const outboundTableRef = ref<any>(null)
const outboundWarehouseId = ref<number | null>(null)
const outboundLocationId = ref<number | null>(null)
const outboundPagination = reactive({
current: 1,
pageSize: 10,
@ -390,6 +403,7 @@ export default defineComponent({
out_no: searchParams.keyword || undefined,
status: searchParams.status !== null ? String(searchParams.status) : undefined,
warehouse_id: searchParams.warehouse_id !== null ? String(searchParams.warehouse_id) : undefined,
location_id: searchParams.location_id || undefined,
shop_type: searchParams.shop_type !== null ? searchParams.shop_type : undefined,
association_order_no: searchParams.association_order_no || undefined,
logistics_no: searchParams.logistics_no || undefined,
@ -431,6 +445,12 @@ export default defineComponent({
if (outboundSearchKeyword.value) {
params.out_no = outboundSearchKeyword.value
}
if (outboundWarehouseId.value) {
params.warehouse_id = String(outboundWarehouseId.value)
}
if (outboundLocationId.value) {
params.location_id = String(outboundLocationId.value)
}
const res = await fetchOutboundList(params)
outboundOrderList.value = res.list || []
outboundPagination.total = res.total || 0
@ -548,6 +568,15 @@ export default defineComponent({
}
}
/** 按当前搜索条件过滤明细(库位筛选时仅展示匹配库位的商品) */
const getFilteredDetailItems = (rowId: number) => {
const items = detailCache.value[rowId]?.items || []
if (searchParams.location_id) {
return items.filter((item: any) => String(item.location_id) === String(searchParams.location_id))
}
return items
}
/** 添加商品明细行 */
const addItem = (): void => {
formData.items.push({
@ -665,6 +694,7 @@ export default defineComponent({
handleSizeChange,
handleCurrentChange,
handleExpandChange,
getFilteredDetailItems,
addItem,
removeItem,
handleApprove,
@ -677,6 +707,8 @@ export default defineComponent({
outboundOrderList,
outboundLoading,
outboundTableRef,
outboundWarehouseId,
outboundLocationId,
formatAmount,
handleOutboundSearch,
handleOutboundSelectionChange,

View File

@ -15,10 +15,11 @@
<el-select v-model="searchParams.status" placeholder="状态" clearable style="width: 140px">
<el-option v-for="item in statusOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
<el-select v-model="searchParams.warehouse_id" placeholder="仓库" clearable filterable remote reserve-keyword
:remote-method="loadWarehouses" :loading="warehouseLoading" style="width: 160px">
<el-option v-for="item in warehouseOptions" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
<WarehouseSelect
:model-value="searchParams.location_id"
@update:model-value="searchParams.location_id = $event"
@warehouse-change="searchParams.warehouse_id = $event"
/>
<el-select v-model="searchParams.shop_type" placeholder="平台" clearable style="width: 180px">
<el-option label="闲鱼" :value="5" />
<el-option label="孔夫子" :value="2" />
@ -95,6 +96,9 @@
</el-table-column>
<el-table-column label="仓库" min-width="100" align="center">
<template #default="{ row }">{{ row.warehouse_name || '-' }}</template>
</el-table-column>
<el-table-column label="库位" min-width="100" align="center">
<template #default="{ row }">{{ row.items?.[0]?.location_code || '-' }}</template>
</el-table-column>
<el-table-column label="店铺/类型" min-width="140" align="center">
@ -200,6 +204,7 @@ import { useRoute } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus'
// added by copilot: useRoute for query params
import { Search, Refresh, Plus, Edit, Delete, View, Check } from '@element-plus/icons-vue'
import WarehouseSelect from '@/components/warehouseSelect/index.vue'
import dayjs from 'dayjs'
import {
fetchSalesOrderList,
@ -242,6 +247,7 @@ interface SalesOrderForm {
export default defineComponent({
name: 'SalesOrder',
components: { WarehouseSelect },
setup() {
const loading = ref<boolean>(false)
const submitLoading = ref<boolean>(false)
@ -256,6 +262,7 @@ export default defineComponent({
keyword: string
status: number | null
warehouse_id: number | null
location_id: number | null
shop_type: number | null
association_order_no: string
logistics_no: string
@ -263,6 +270,7 @@ export default defineComponent({
keyword: '',
status: null,
warehouse_id: null,
location_id: null,
shop_type: null,
association_order_no: '',
logistics_no: ''
@ -377,6 +385,10 @@ export default defineComponent({
const res = await fetchSalesOrderDetails({
page: pagination.current,
pageSize: pagination.pageSize,
warehouse_id: searchParams.warehouse_id || undefined,
location_id: searchParams.location_id || undefined,
status: searchParams.status || undefined,
keyword: searchParams.keyword,
shop_type: searchParams.shop_type !== null ? searchParams.shop_type : undefined,
association_order_no: searchParams.association_order_no,
logistics_no: searchParams.logistics_no
@ -402,6 +414,7 @@ export default defineComponent({
searchParams.keyword = ''
searchParams.status = null
searchParams.warehouse_id = null
searchParams.location_id = null
searchParams.shop_type = null
searchParams.association_order_no = ''
searchParams.logistics_no = ''
@ -585,8 +598,6 @@ export default defineComponent({
formRef,
formData,
formRules,
warehouseOptions,
warehouseLoading,
loadWarehouses,
warehouseMap,
customerMap,

View File

@ -37,7 +37,11 @@
<el-select v-model="searchParams.status" placeholder="状态" clearable style="width: 140px">
<el-option v-for="item in statusOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
<LocationSelect v-model="searchParams.warehouse_id" style="width: 160px" />
<WarehouseSelect
:model-value="searchParams.location_id"
@update:model-value="searchParams.location_id = $event"
@warehouse-change="searchParams.warehouse_id = $event"
/>
<el-select v-model="searchParams.shop_type" placeholder="平台" clearable style="width: 180px">
<el-option label="闲鱼" :value="5" />
<el-option label="孔夫子" :value="2" />
@ -124,6 +128,9 @@
<el-table-column label="仓库" min-width="120" align="center">
<template #default="{ row }">{{ warehouseMap[row.warehouse_id] || row.warehouse_id || '-' }}</template>
</el-table-column>
<el-table-column label="库位" min-width="120" align="center">
<template #default="{ row }">{{ row.location_code || '-' }}</template>
</el-table-column>
<el-table-column label="订单日期" min-width="110" align="center">
<template #default="{ row }">{{ formatDate(row.order_date) }}</template>
</el-table-column>
@ -212,7 +219,7 @@
@close="outboundGenerated = false">
<!-- 搜索区 -->
<div class="outbound-search-bar">
<el-input v-model="outboundSearchKeyword" placeholder="搜索销售订单号 / 平台 / 仓库" clearable style="width: 320px"
<el-input v-model="outboundSearchKeyword" placeholder="搜索销售订单号 / 平台" clearable style="width: 280px"
@input="handleOutboundSearch" @keyup.enter="handleOutboundSearch">
<template #prefix>
<el-icon>
@ -220,6 +227,11 @@
</el-icon>
</template>
</el-input>
<WarehouseSelect
:model-value="outboundLocationId"
@update:model-value="outboundLocationId = $event"
@warehouse-change="outboundWarehouseId = $event"
/>
<span class="selected-tip">已选 <strong>{{ selectedOutboundOrders.length }}</strong> </span>
</div>
@ -282,7 +294,7 @@ import { useRoute } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus'
// added by copilot: useRoute for query params
import { Search, Refresh, Plus, Edit, Delete, View, Loading, Check } from '@element-plus/icons-vue'
import LocationSelect from '@/components/locationSelect/index.vue'
import WarehouseSelect from '@/components/warehouseSelect/index.vue'
import dayjs from 'dayjs'
import {
fetchSalesOrderList,
@ -330,7 +342,7 @@ interface SalesOrderForm {
export default defineComponent({
name: 'SalesOrder',
components: { LocationSelect },
components: { WarehouseSelect },
setup() {
const loading = ref<boolean>(false)
const submitLoading = ref<boolean>(false)
@ -345,6 +357,7 @@ export default defineComponent({
keyword: string
status: number | null
warehouse_id: number | null
location_id: number | null
shop_type: number | null
association_order_no: string
logistics_no: string
@ -352,6 +365,7 @@ export default defineComponent({
keyword: '',
status: null,
warehouse_id: null,
location_id: null,
shop_type: null,
association_order_no: '',
logistics_no: ''
@ -403,6 +417,8 @@ export default defineComponent({
const generateLoading = ref<boolean>(false)
const outboundOrderList = ref<any[]>([])
const outboundSearchKeyword = ref<string>('')
const outboundWarehouseId = ref<number | null>(null)
const outboundLocationId = ref<number | null>(null)
const selectedOutboundOrders = ref<any[]>([])
const outboundTableRef = ref<any>(null)
const outboundRemark = ref('')
@ -467,6 +483,7 @@ export default defineComponent({
keyword: searchParams.keyword,
status: searchParams.status || undefined,
warehouse_id: searchParams.warehouse_id || undefined,
location_id: searchParams.location_id || undefined,
shop_type: searchParams.shop_type !== null ? searchParams.shop_type : undefined,
association_order_no: searchParams.association_order_no || undefined,
logistics_no: searchParams.logistics_no || undefined,
@ -496,6 +513,7 @@ export default defineComponent({
searchParams.keyword = ''
searchParams.status = null
searchParams.warehouse_id = null
searchParams.location_id = null
searchParams.shop_type = null
searchParams.association_order_no = ''
searchParams.logistics_no = ''
@ -577,12 +595,16 @@ export default defineComponent({
outboundDialogVisible.value = true
outboundGenerated.value = false
outboundSearchKeyword.value = ''
outboundWarehouseId.value = null
outboundLocationId.value = null
selectedOutboundOrders.value = []
outboundLoading.value = true
try {
const res = await fetchSalesOrderList({
keyword: '',
status: 3, //
warehouse_id: outboundWarehouseId.value || undefined,
location_id: outboundLocationId.value || undefined,
page: 1,
pageSize: 100
})
@ -602,6 +624,8 @@ export default defineComponent({
const res = await fetchSalesOrderList({
keyword: outboundSearchKeyword.value,
status: 3,
warehouse_id: outboundWarehouseId.value || undefined,
location_id: outboundLocationId.value || undefined,
page: 1,
pageSize: 100
})
@ -723,6 +747,8 @@ export default defineComponent({
outboundLoading,
outboundOrderList,
outboundSearchKeyword,
outboundWarehouseId,
outboundLocationId,
selectedOutboundOrders,
outboundTableRef,
outboundRemark,

View File

@ -152,9 +152,11 @@
<el-select v-model="searchParams.status" placeholder="状态" clearable style="width: 120px">
<el-option v-for="item in statusOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
<el-select v-model="searchParams.warehouse_id" placeholder="仓库" clearable style="width: 140px">
<el-option v-for="item in warehouseOptions" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
<WarehouseSelect
:model-value="searchParams.location_id"
@update:model-value="searchParams.location_id = $event"
@warehouse-change="searchParams.warehouse_id = $event"
/>
<el-select v-model="searchParams.shop_type" placeholder="平台" clearable style="width: 160px">
<el-option label="闲鱼" :value="5" />
<el-option label="孔夫子" :value="2" />
@ -177,7 +179,7 @@
<template #default="{ row }">
<div v-if="detailCache[row.id]" style="padding: 12px 20px">
<h4 style="margin: 0 0 10px; font-size: 14px; color: #303133">发货明细</h4>
<el-table :data="detailCache[row.id].items || []" border size="small"
<el-table :data="getFilteredDetailItems(row.id)" border size="small"
@row-click="handleDetailItemClick">
<el-table-column prop="product_name" label="商品名称" min-width="200" show-overflow-tooltip
align="center" />
@ -412,6 +414,7 @@ import { copyToClipboard } from '@/utils/clipboard'
import { fetchShippingOrderList, fetchShippingOrderDetail, updateShippingOrderLogistics, fetchFastMailList, fetchExpressInfo, createBmOrderDaYin, recycleWaybillNo } from '@/api/shippingOrder'
import { createPrintTask } from '@/api/print'
import { fetchWarehouseList } from '@/api/warehouse'
import WarehouseSelect from '@/components/warehouseSelect/index.vue'
import { getBookDetails } from '@/api/product'
import { executeMergePrint } from '@/utils/printFlow/printFlow'
@ -425,6 +428,7 @@ const STATUS_MAP: Record<number, { label: string; type: string }> = {
export default defineComponent({
name: 'ShippingOrder',
components: { WarehouseSelect },
setup() {
const router = useRouter()
const loading = ref<boolean>(false)
@ -477,13 +481,15 @@ export default defineComponent({
keyword: string
status: number | null
warehouse_id: number | null
location_id: number | null
shop_type: number | null
association_order_no: string
logistics_no: string
}>({
keyword: '',
status: 1, // ""
status: 1,
warehouse_id: null,
location_id: null,
shop_type: null,
association_order_no: '',
logistics_no: ''
@ -1109,6 +1115,7 @@ export default defineComponent({
check_no: searchParams.keyword,
status: searchParams.status || undefined,
warehouse_id: searchParams.warehouse_id || undefined,
location_id: searchParams.location_id || undefined,
shop_type: searchParams.shop_type !== null ? searchParams.shop_type : undefined,
association_order_no: searchParams.association_order_no || undefined,
logistics_no: searchParams.logistics_no || undefined,
@ -1136,6 +1143,7 @@ export default defineComponent({
searchParams.keyword = ''
searchParams.status = 1
searchParams.warehouse_id = null
searchParams.location_id = null
searchParams.shop_type = null
searchParams.association_order_no = ''
searchParams.logistics_no = ''
@ -1168,6 +1176,15 @@ export default defineComponent({
}
}
/** 按当前搜索条件过滤明细(库位筛选时仅展示匹配库位的商品) */
const getFilteredDetailItems = (rowId: number) => {
const items = detailCache.value[rowId]?.items || []
if (searchParams.location_id) {
return items.filter((item: any) => String(item.location_id) === String(searchParams.location_id))
}
return items
}
onMounted(() => {
const route = useRoute()
if (route.query.keyword) {
@ -1215,6 +1232,7 @@ export default defineComponent({
handleSizeChange,
handleCurrentChange,
handleExpandChange,
getFilteredDetailItems,
navigateToSalesOrder,
navigateToOutbound,
copyToClipboard,