6.22lct bug 修改
Some checks failed
Some checks failed
This commit is contained in:
parent
e3e3204354
commit
939d55c950
4
node_modules/.vite/deps/@element-plus_icons-vue.js
generated
vendored
4
node_modules/.vite/deps/@element-plus_icons-vue.js
generated
vendored
@ -292,8 +292,8 @@ import {
|
|||||||
wind_power_default,
|
wind_power_default,
|
||||||
zoom_in_default,
|
zoom_in_default,
|
||||||
zoom_out_default
|
zoom_out_default
|
||||||
} from "./chunk-SD7HEPIX.js";
|
} from "./chunk-5GV5GKFP.js";
|
||||||
import "./chunk-JWX4TPXS.js";
|
import "./chunk-YDDUUUNR.js";
|
||||||
import "./chunk-S5KM4IGW.js";
|
import "./chunk-S5KM4IGW.js";
|
||||||
export {
|
export {
|
||||||
add_location_default as AddLocation,
|
add_location_default as AddLocation,
|
||||||
|
|||||||
38
node_modules/.vite/deps/_metadata.json
generated
vendored
38
node_modules/.vite/deps/_metadata.json
generated
vendored
@ -1,83 +1,83 @@
|
|||||||
{
|
{
|
||||||
"hash": "7d67b94b",
|
"hash": "a0d5344e",
|
||||||
"browserHash": "5af76578",
|
"browserHash": "906d96e7",
|
||||||
"optimized": {
|
"optimized": {
|
||||||
"@element-plus/icons-vue": {
|
"@element-plus/icons-vue": {
|
||||||
"src": "../../@element-plus/icons-vue/dist/index.js",
|
"src": "../../@element-plus/icons-vue/dist/index.js",
|
||||||
"file": "@element-plus_icons-vue.js",
|
"file": "@element-plus_icons-vue.js",
|
||||||
"fileHash": "47eb7f77",
|
"fileHash": "b0bff00e",
|
||||||
"needsInterop": false
|
"needsInterop": false
|
||||||
},
|
},
|
||||||
"axios": {
|
"axios": {
|
||||||
"src": "../../axios/index.js",
|
"src": "../../axios/index.js",
|
||||||
"file": "axios.js",
|
"file": "axios.js",
|
||||||
"fileHash": "2743dcc7",
|
"fileHash": "3661d772",
|
||||||
"needsInterop": false
|
"needsInterop": false
|
||||||
},
|
},
|
||||||
"crypto-js": {
|
"crypto-js": {
|
||||||
"src": "../../crypto-js/index.js",
|
"src": "../../crypto-js/index.js",
|
||||||
"file": "crypto-js.js",
|
"file": "crypto-js.js",
|
||||||
"fileHash": "c0aed2f0",
|
"fileHash": "42456c05",
|
||||||
"needsInterop": true
|
"needsInterop": true
|
||||||
},
|
},
|
||||||
"dayjs": {
|
"dayjs": {
|
||||||
"src": "../../dayjs/dayjs.min.js",
|
"src": "../../dayjs/dayjs.min.js",
|
||||||
"file": "dayjs.js",
|
"file": "dayjs.js",
|
||||||
"fileHash": "4a44116f",
|
"fileHash": "00d3ce60",
|
||||||
"needsInterop": true
|
"needsInterop": true
|
||||||
},
|
},
|
||||||
"element-plus": {
|
"element-plus": {
|
||||||
"src": "../../element-plus/es/index.mjs",
|
"src": "../../element-plus/es/index.mjs",
|
||||||
"file": "element-plus.js",
|
"file": "element-plus.js",
|
||||||
"fileHash": "cc941865",
|
"fileHash": "8289c005",
|
||||||
"needsInterop": false
|
"needsInterop": false
|
||||||
},
|
},
|
||||||
"element-plus/es/locale/lang/zh-cn": {
|
"element-plus/es/locale/lang/zh-cn": {
|
||||||
"src": "../../element-plus/es/locale/lang/zh-cn.mjs",
|
"src": "../../element-plus/es/locale/lang/zh-cn.mjs",
|
||||||
"file": "element-plus_es_locale_lang_zh-cn.js",
|
"file": "element-plus_es_locale_lang_zh-cn.js",
|
||||||
"fileHash": "8c2e2f2e",
|
"fileHash": "2b74d0fe",
|
||||||
"needsInterop": false
|
"needsInterop": false
|
||||||
},
|
},
|
||||||
"jsbarcode": {
|
"jsbarcode": {
|
||||||
"src": "../../jsbarcode/bin/JsBarcode.js",
|
"src": "../../jsbarcode/bin/JsBarcode.js",
|
||||||
"file": "jsbarcode.js",
|
"file": "jsbarcode.js",
|
||||||
"fileHash": "4c77e9e6",
|
"fileHash": "5774375a",
|
||||||
"needsInterop": true
|
"needsInterop": true
|
||||||
},
|
},
|
||||||
"json-bigint": {
|
"json-bigint": {
|
||||||
"src": "../../json-bigint/index.js",
|
"src": "../../json-bigint/index.js",
|
||||||
"file": "json-bigint.js",
|
"file": "json-bigint.js",
|
||||||
"fileHash": "577a65f0",
|
"fileHash": "a76f358e",
|
||||||
"needsInterop": true
|
"needsInterop": true
|
||||||
},
|
},
|
||||||
"pinia": {
|
"pinia": {
|
||||||
"src": "../../pinia/dist/pinia.mjs",
|
"src": "../../pinia/dist/pinia.mjs",
|
||||||
"file": "pinia.js",
|
"file": "pinia.js",
|
||||||
"fileHash": "ca9e1d82",
|
"fileHash": "3c37150a",
|
||||||
"needsInterop": false
|
"needsInterop": false
|
||||||
},
|
},
|
||||||
"vue": {
|
"vue": {
|
||||||
"src": "../../vue/dist/vue.runtime.esm-bundler.js",
|
"src": "../../vue/dist/vue.runtime.esm-bundler.js",
|
||||||
"file": "vue.js",
|
"file": "vue.js",
|
||||||
"fileHash": "211b1d8e",
|
"fileHash": "c01889f7",
|
||||||
"needsInterop": false
|
"needsInterop": false
|
||||||
},
|
},
|
||||||
"vue-router": {
|
"vue-router": {
|
||||||
"src": "../../vue-router/dist/vue-router.mjs",
|
"src": "../../vue-router/dist/vue-router.mjs",
|
||||||
"file": "vue-router.js",
|
"file": "vue-router.js",
|
||||||
"fileHash": "132cec93",
|
"fileHash": "ced1274b",
|
||||||
"needsInterop": false
|
"needsInterop": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"chunks": {
|
"chunks": {
|
||||||
"chunk-FAQYB3BN": {
|
"chunk-YGWLVKKF": {
|
||||||
"file": "chunk-FAQYB3BN.js"
|
"file": "chunk-YGWLVKKF.js"
|
||||||
},
|
},
|
||||||
"chunk-SD7HEPIX": {
|
"chunk-5GV5GKFP": {
|
||||||
"file": "chunk-SD7HEPIX.js"
|
"file": "chunk-5GV5GKFP.js"
|
||||||
},
|
},
|
||||||
"chunk-JWX4TPXS": {
|
"chunk-YDDUUUNR": {
|
||||||
"file": "chunk-JWX4TPXS.js"
|
"file": "chunk-YDDUUUNR.js"
|
||||||
},
|
},
|
||||||
"chunk-YIAL6EWQ": {
|
"chunk-YIAL6EWQ": {
|
||||||
"file": "chunk-YIAL6EWQ.js"
|
"file": "chunk-YIAL6EWQ.js"
|
||||||
|
|||||||
2
node_modules/.vite/deps/axios.js.map
generated
vendored
2
node_modules/.vite/deps/axios.js.map
generated
vendored
File diff suppressed because one or more lines are too long
@ -3,7 +3,7 @@ import {
|
|||||||
createElementBlock,
|
createElementBlock,
|
||||||
defineComponent,
|
defineComponent,
|
||||||
openBlock
|
openBlock
|
||||||
} from "./chunk-JWX4TPXS.js";
|
} from "./chunk-YDDUUUNR.js";
|
||||||
|
|
||||||
// node_modules/@element-plus/icons-vue/dist/index.js
|
// node_modules/@element-plus/icons-vue/dist/index.js
|
||||||
var _sfc_main = defineComponent({
|
var _sfc_main = defineComponent({
|
||||||
@ -5465,4 +5465,4 @@ export {
|
|||||||
zoom_out_default
|
zoom_out_default
|
||||||
};
|
};
|
||||||
/*! Element Plus Icons Vue v2.3.2 */
|
/*! Element Plus Icons Vue v2.3.2 */
|
||||||
//# sourceMappingURL=chunk-SD7HEPIX.js.map
|
//# sourceMappingURL=chunk-5GV5GKFP.js.map
|
||||||
7
node_modules/.vite/deps/chunk-5GV5GKFP.js.map
generated
vendored
Normal file
7
node_modules/.vite/deps/chunk-5GV5GKFP.js.map
generated
vendored
Normal file
File diff suppressed because one or more lines are too long
7
node_modules/.vite/deps/chunk-FAQYB3BN.js.map
generated
vendored
7
node_modules/.vite/deps/chunk-FAQYB3BN.js.map
generated
vendored
File diff suppressed because one or more lines are too long
7
node_modules/.vite/deps/chunk-JWX4TPXS.js.map
generated
vendored
7
node_modules/.vite/deps/chunk-JWX4TPXS.js.map
generated
vendored
File diff suppressed because one or more lines are too long
7
node_modules/.vite/deps/chunk-SD7HEPIX.js.map
generated
vendored
7
node_modules/.vite/deps/chunk-SD7HEPIX.js.map
generated
vendored
File diff suppressed because one or more lines are too long
@ -12795,4 +12795,4 @@ export {
|
|||||||
* (c) 2018-present Yuxi (Evan) You and Vue contributors
|
* (c) 2018-present Yuxi (Evan) You and Vue contributors
|
||||||
* @license MIT
|
* @license MIT
|
||||||
**/
|
**/
|
||||||
//# sourceMappingURL=chunk-JWX4TPXS.js.map
|
//# sourceMappingURL=chunk-YDDUUUNR.js.map
|
||||||
7
node_modules/.vite/deps/chunk-YDDUUUNR.js.map
generated
vendored
Normal file
7
node_modules/.vite/deps/chunk-YDDUUUNR.js.map
generated
vendored
Normal file
File diff suppressed because one or more lines are too long
@ -159,4 +159,4 @@ function setupDevtoolsPlugin(pluginDescriptor, setupFn) {
|
|||||||
export {
|
export {
|
||||||
setupDevtoolsPlugin
|
setupDevtoolsPlugin
|
||||||
};
|
};
|
||||||
//# sourceMappingURL=chunk-FAQYB3BN.js.map
|
//# sourceMappingURL=chunk-YGWLVKKF.js.map
|
||||||
7
node_modules/.vite/deps/chunk-YGWLVKKF.js.map
generated
vendored
Normal file
7
node_modules/.vite/deps/chunk-YGWLVKKF.js.map
generated
vendored
Normal file
File diff suppressed because one or more lines are too long
2
node_modules/.vite/deps/crypto-js.js.map
generated
vendored
2
node_modules/.vite/deps/crypto-js.js.map
generated
vendored
File diff suppressed because one or more lines are too long
4
node_modules/.vite/deps/element-plus.js
generated
vendored
4
node_modules/.vite/deps/element-plus.js
generated
vendored
@ -41,7 +41,7 @@ import {
|
|||||||
warning_filled_default,
|
warning_filled_default,
|
||||||
zoom_in_default,
|
zoom_in_default,
|
||||||
zoom_out_default
|
zoom_out_default
|
||||||
} from "./chunk-SD7HEPIX.js";
|
} from "./chunk-5GV5GKFP.js";
|
||||||
import {
|
import {
|
||||||
Comment,
|
Comment,
|
||||||
Fragment,
|
Fragment,
|
||||||
@ -130,7 +130,7 @@ import {
|
|||||||
withDirectives,
|
withDirectives,
|
||||||
withKeys,
|
withKeys,
|
||||||
withModifiers
|
withModifiers
|
||||||
} from "./chunk-JWX4TPXS.js";
|
} from "./chunk-YDDUUUNR.js";
|
||||||
import {
|
import {
|
||||||
require_dayjs_min
|
require_dayjs_min
|
||||||
} from "./chunk-YIAL6EWQ.js";
|
} from "./chunk-YIAL6EWQ.js";
|
||||||
|
|||||||
2
node_modules/.vite/deps/element-plus.js.map
generated
vendored
2
node_modules/.vite/deps/element-plus.js.map
generated
vendored
File diff suppressed because one or more lines are too long
2
node_modules/.vite/deps/jsbarcode.js.map
generated
vendored
2
node_modules/.vite/deps/jsbarcode.js.map
generated
vendored
File diff suppressed because one or more lines are too long
2
node_modules/.vite/deps/json-bigint.js.map
generated
vendored
2
node_modules/.vite/deps/json-bigint.js.map
generated
vendored
File diff suppressed because one or more lines are too long
4
node_modules/.vite/deps/pinia.js
generated
vendored
4
node_modules/.vite/deps/pinia.js
generated
vendored
@ -1,6 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
setupDevtoolsPlugin
|
setupDevtoolsPlugin
|
||||||
} from "./chunk-FAQYB3BN.js";
|
} from "./chunk-YGWLVKKF.js";
|
||||||
import {
|
import {
|
||||||
computed,
|
computed,
|
||||||
effectScope,
|
effectScope,
|
||||||
@ -20,7 +20,7 @@ import {
|
|||||||
toRefs,
|
toRefs,
|
||||||
unref,
|
unref,
|
||||||
watch
|
watch
|
||||||
} from "./chunk-JWX4TPXS.js";
|
} from "./chunk-YDDUUUNR.js";
|
||||||
import "./chunk-S5KM4IGW.js";
|
import "./chunk-S5KM4IGW.js";
|
||||||
|
|
||||||
// node_modules/vue-demi/lib/index.mjs
|
// node_modules/vue-demi/lib/index.mjs
|
||||||
|
|||||||
2
node_modules/.vite/deps/pinia.js.map
generated
vendored
2
node_modules/.vite/deps/pinia.js.map
generated
vendored
File diff suppressed because one or more lines are too long
4
node_modules/.vite/deps/vue-router.js
generated
vendored
4
node_modules/.vite/deps/vue-router.js
generated
vendored
@ -1,6 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
setupDevtoolsPlugin
|
setupDevtoolsPlugin
|
||||||
} from "./chunk-FAQYB3BN.js";
|
} from "./chunk-YGWLVKKF.js";
|
||||||
import {
|
import {
|
||||||
computed,
|
computed,
|
||||||
defineComponent,
|
defineComponent,
|
||||||
@ -19,7 +19,7 @@ import {
|
|||||||
unref,
|
unref,
|
||||||
watch,
|
watch,
|
||||||
watchEffect
|
watchEffect
|
||||||
} from "./chunk-JWX4TPXS.js";
|
} from "./chunk-YDDUUUNR.js";
|
||||||
import "./chunk-S5KM4IGW.js";
|
import "./chunk-S5KM4IGW.js";
|
||||||
|
|
||||||
// node_modules/vue-router/dist/devtools-EWN81iOl.mjs
|
// node_modules/vue-router/dist/devtools-EWN81iOl.mjs
|
||||||
|
|||||||
2
node_modules/.vite/deps/vue-router.js.map
generated
vendored
2
node_modules/.vite/deps/vue-router.js.map
generated
vendored
File diff suppressed because one or more lines are too long
2
node_modules/.vite/deps/vue.js
generated
vendored
2
node_modules/.vite/deps/vue.js
generated
vendored
@ -170,7 +170,7 @@ import {
|
|||||||
withMemo,
|
withMemo,
|
||||||
withModifiers,
|
withModifiers,
|
||||||
withScopeId
|
withScopeId
|
||||||
} from "./chunk-JWX4TPXS.js";
|
} from "./chunk-YDDUUUNR.js";
|
||||||
import "./chunk-S5KM4IGW.js";
|
import "./chunk-S5KM4IGW.js";
|
||||||
export {
|
export {
|
||||||
BaseTransition,
|
BaseTransition,
|
||||||
|
|||||||
15
src/api/category.js
Normal file
15
src/api/category.js
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import request from '@/utils/request'
|
||||||
|
|
||||||
|
const API_BASE = '/category'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取分类列表
|
||||||
|
* @returns {Promise<Array>} 分类列表 [{ id, name }]
|
||||||
|
*/
|
||||||
|
export const fetchCategoryList = async () => {
|
||||||
|
const response = await request.get(`${API_BASE}/list`, { params: { page: 1, page_size: 9999 } })
|
||||||
|
const data = response?.data
|
||||||
|
if (Array.isArray(data)) return data
|
||||||
|
if (Array.isArray(data?.list)) return data.list
|
||||||
|
return []
|
||||||
|
}
|
||||||
@ -6,19 +6,23 @@ const API_BASE = '/product'
|
|||||||
/**
|
/**
|
||||||
* 标准化列表接口返回的数据格式
|
* 标准化列表接口返回的数据格式
|
||||||
* @param {Object} payload - 接口返回的原始响应对象,通常包含 data 字段
|
* @param {Object} payload - 接口返回的原始响应对象,通常包含 data 字段
|
||||||
* @returns {{ list: Array, total: number }} 标准化后的列表数据
|
* @returns {{ list: Array, total: number, located_count: number, unlocated_count: number, enabled_count: number, disabled_count: number }} 标准化后的列表数据
|
||||||
*/
|
*/
|
||||||
const normalizeListResponse = (payload) => {
|
const normalizeListResponse = (payload) => {
|
||||||
const data = payload?.data
|
const data = payload?.data
|
||||||
if (!data) {
|
if (!data) {
|
||||||
return { list: [], total: 0 }
|
return { list: [], total: 0, located_count: 0, unlocated_count: 0, enabled_count: 0, disabled_count: 0 }
|
||||||
}
|
}
|
||||||
if (Array.isArray(data)) {
|
if (Array.isArray(data)) {
|
||||||
return { list: data, total: data.length }
|
return { list: data, total: data.length, located_count: 0, unlocated_count: 0, enabled_count: 0, disabled_count: 0 }
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
list: Array.isArray(data.list) ? data.list : [],
|
list: Array.isArray(data.list) ? data.list : [],
|
||||||
total: typeof data.total === 'number' ? data.total : Array.isArray(data.list) ? data.list.length : 0
|
total: typeof data.total === 'number' ? data.total : Array.isArray(data.list) ? data.list.length : 0,
|
||||||
|
located_count: data.located_count ?? 0,
|
||||||
|
unlocated_count: data.unlocated_count ?? 0,
|
||||||
|
enabled_count: data.enabled_count ?? 0,
|
||||||
|
disabled_count: data.disabled_count ?? 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -77,7 +81,7 @@ export const createProduct = async (product) => {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 保存商品(兼容旧接口)
|
* 保存商品(兼容旧接口)
|
||||||
* @param {Object} product - 商品信息 { name, barcode, price, live_image }
|
* @param {Object} product - 商品信息 { name, barcode, price, liveimage }
|
||||||
* @returns {Promise} 接口返回的 Promise 对象
|
* @returns {Promise} 接口返回的 Promise 对象
|
||||||
*/
|
*/
|
||||||
export const saveProduct = async (product) => {
|
export const saveProduct = async (product) => {
|
||||||
@ -93,6 +97,24 @@ export const updateProduct = async (product) => {
|
|||||||
return request.post(`${API_BASE}/save`, product)
|
return request.post(`${API_BASE}/save`, product)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新商品名称和实拍图片
|
||||||
|
* @param {Object} params - 请求参数
|
||||||
|
* @param {string|number} params.id - 商品ID
|
||||||
|
* @param {string} params.name - 商品名称
|
||||||
|
* @param {Array<string>} params.liveimage - 实拍图片URL数组
|
||||||
|
* @param {string|number} [params.product_id] - 商品product_id
|
||||||
|
* @returns {Promise} 接口返回的 Promise 对象
|
||||||
|
*/
|
||||||
|
export const updateNameAndImages = async ({ id, name, liveimage, product_id }) => {
|
||||||
|
return request.post(`${API_BASE}/updateNameAndImages`, {
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
product_id,
|
||||||
|
liveimage: Array.isArray(liveimage) ? liveimage.join(',') : liveimage
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 删除商品
|
* 删除商品
|
||||||
* @param {Object} product - 要删除的商品对象(需包含 id 字段)
|
* @param {Object} product - 要删除的商品对象(需包含 id 字段)
|
||||||
@ -125,7 +147,7 @@ export const updateProductLiveImage = async (id, pictureName, extra = {}) => {
|
|||||||
name,
|
name,
|
||||||
appearance: appearance ?? 85,
|
appearance: appearance ?? 85,
|
||||||
price,
|
price,
|
||||||
live_image: [`https://shxy.image.yushutx.com/living-picture/${pictureName}`]
|
liveimage: [`https://shxy.image.yushutx.com/living-picture/${pictureName}`]
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -152,7 +174,7 @@ export const updateProductLiveImageAsPddUrl = async (id, picUrl, extra = {}) =>
|
|||||||
name,
|
name,
|
||||||
appearance: appearance ?? 85,
|
appearance: appearance ?? 85,
|
||||||
price,
|
price,
|
||||||
live_image: [picUrl]
|
liveimage: [picUrl]
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -179,7 +201,7 @@ export const updateProductLiveImageForTest = async (id, pictureName, extra = {})
|
|||||||
name,
|
name,
|
||||||
appearance: appearance ?? 85,
|
appearance: appearance ?? 85,
|
||||||
price,
|
price,
|
||||||
live_image: ["https://img.pddpic.com/open-gw/2025-12-01/181c6be7-c781-45ec-a2cc-397c5bbdd09e.jpeg"]
|
liveimage: ["https://img.pddpic.com/open-gw/2025-12-01/181c6be7-c781-45ec-a2cc-397c5bbdd09e.jpeg"]
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -226,9 +248,9 @@ export const ocrImage = async (imageBlob) => {
|
|||||||
/**
|
/**
|
||||||
* 商品发布重试
|
* 商品发布重试
|
||||||
*/
|
*/
|
||||||
export const retryProductPublish = async (productId, shopId, shopType) => {
|
export const retryProductPublish = async (product_id, shopId, shopType) => {
|
||||||
const formData = new FormData()
|
const formData = new FormData()
|
||||||
formData.append('product_id', productId)
|
formData.append('product_id', product_id)
|
||||||
formData.append('shop_id', shopId)
|
formData.append('shop_id', shopId)
|
||||||
formData.append('shop_type', shopType)
|
formData.append('shop_type', shopType)
|
||||||
return request.post(`${API_BASE}/retry-out-task`, formData)
|
return request.post(`${API_BASE}/retry-out-task`, formData)
|
||||||
@ -312,3 +334,17 @@ export const importProductsByExcel = async ({ userId, warehouse_id, file }) => {
|
|||||||
formData.append('file', file)
|
formData.append('file', file)
|
||||||
return request.post(`/goods/import-from-excel`, formData)
|
return request.post(`/goods/import-from-excel`, formData)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 上传商品实拍图片
|
||||||
|
* @param {File} file - 图片文件
|
||||||
|
* @returns {Promise<string>} 上传后的图片 URL
|
||||||
|
*/
|
||||||
|
export const uploadProductImage = async (file) => {
|
||||||
|
const formData = new FormData()
|
||||||
|
formData.append('file', file)
|
||||||
|
const res = await request.post('/upload/image', formData)
|
||||||
|
const url = res?.data?.url || res?.data?.file_path || res?.data
|
||||||
|
if (url && typeof url === 'string') return url
|
||||||
|
throw new Error('上传失败,未获取到图片地址')
|
||||||
|
}
|
||||||
@ -113,3 +113,12 @@ export const createPurchaseOrderWithWave = async (data) => {
|
|||||||
export const releaseWave = async (data) => {
|
export const releaseWave = async (data) => {
|
||||||
return request.post('/wave/release', data)
|
return request.post('/wave/release', data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导出采购单到旺店通
|
||||||
|
* @param {Object} params - 请求参数(与列表查询参数一致)
|
||||||
|
* @returns {Promise<Blob>} 返回 Blob 响应
|
||||||
|
*/
|
||||||
|
export const exportPurchaseOrderToWdt = async (params) => {
|
||||||
|
return request.get('/purchase-order/export-to-wdt', { params, responseType: 'blob' })
|
||||||
|
}
|
||||||
|
|||||||
@ -40,7 +40,21 @@ export const fetchWaveTaskList = async (params) => {
|
|||||||
page_size: pageSize
|
page_size: pageSize
|
||||||
}
|
}
|
||||||
const response = await request.get(`/wave/task/list`, { params: requestParams })
|
const response = await request.get(`/wave/task/list`, { params: requestParams })
|
||||||
return normalizeListResponse(response)
|
const result = normalizeListResponse(response)
|
||||||
|
const data = response?.data
|
||||||
|
if (data && typeof data === 'object' && !Array.isArray(data)) {
|
||||||
|
result.stats = {
|
||||||
|
today_inbound_waves: data.today_inbound_waves ?? 0,
|
||||||
|
today_inbound_quantity: data.today_inbound_quantity ?? 0,
|
||||||
|
today_outbound_quantity: data.today_outbound_quantity ?? 0,
|
||||||
|
yesterday_inbound_waves: data.yesterday_inbound_waves ?? 0,
|
||||||
|
yesterday_inbound_quantity: data.yesterday_inbound_quantity ?? 0,
|
||||||
|
yesterday_outbound_quantity: data.yesterday_outbound_quantity ?? 0
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result.stats = null
|
||||||
|
}
|
||||||
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -87,7 +87,7 @@
|
|||||||
<el-icon>
|
<el-icon>
|
||||||
<Printer />
|
<Printer />
|
||||||
</el-icon>
|
</el-icon>
|
||||||
<span>打印机管理</span>
|
<span>系统硬件配置</span>
|
||||||
</el-menu-item>
|
</el-menu-item>
|
||||||
<el-menu-item index="/pdaManage" @click="goTo('/pdaManage')">
|
<el-menu-item index="/pdaManage" @click="goTo('/pdaManage')">
|
||||||
<el-icon>
|
<el-icon>
|
||||||
|
|||||||
@ -17,10 +17,34 @@
|
|||||||
<el-button :icon="Refresh" @click="resetSearch">重置</el-button>
|
<el-button :icon="Refresh" @click="resetSearch">重置</el-button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 统计卡片栏 -->
|
||||||
|
<div v-if="loadedOnce" class="stats-row">
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-card__value">{{ stats.total }}</div>
|
||||||
|
<div class="stat-card__label">总商品数</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-card__value">{{ stats.located_count }}</div>
|
||||||
|
<div class="stat-card__label">已落位</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-card__value">{{ stats.unlocated_count }}</div>
|
||||||
|
<div class="stat-card__label">未落位</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-card__value">{{ stats.enabled_count }}</div>
|
||||||
|
<div class="stat-card__label">启用中</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-card__value">{{ stats.disabled_count }}</div>
|
||||||
|
<div class="stat-card__label">已禁用</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div style="margin-bottom: 16px; display: flex; gap: 8px; flex-wrap: wrap;">
|
<div style="margin-bottom: 16px; display: flex; gap: 8px; flex-wrap: wrap;">
|
||||||
<el-button type="primary">新增</el-button>
|
<el-button type="primary">新增</el-button>
|
||||||
<el-button type="primary">修改</el-button>
|
<!-- <el-button type="primary">修改</el-button>
|
||||||
<el-button type="primary">删除</el-button>
|
<el-button type="primary">删除</el-button> -->
|
||||||
<el-button type="primary">导出</el-button>
|
<el-button type="primary">导出</el-button>
|
||||||
<el-button type="primary">导出模板</el-button>
|
<el-button type="primary">导出模板</el-button>
|
||||||
<el-button type="primary" @click="handleImportClick">导入</el-button>
|
<el-button type="primary" @click="handleImportClick">导入</el-button>
|
||||||
@ -30,7 +54,6 @@
|
|||||||
<el-button type="primary">批量修改货区</el-button>
|
<el-button type="primary">批量修改货区</el-button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<el-table :data="tableData" v-loading="loading" border stripe style="width: 100%"
|
<el-table :data="tableData" v-loading="loading" border stripe style="width: 100%"
|
||||||
@selection-change="handleSelectionChange">
|
@selection-change="handleSelectionChange">
|
||||||
<el-table-column type="selection" width="55" align="center" />
|
<el-table-column type="selection" width="55" align="center" />
|
||||||
@ -38,11 +61,28 @@
|
|||||||
<el-table-column prop="appearance" label="品相" width="90" align="center">
|
<el-table-column prop="appearance" label="品相" width="90" align="center">
|
||||||
<template #default="{ row }">{{ formatAppearance(row.appearance) }}</template>
|
<template #default="{ row }">{{ formatAppearance(row.appearance) }}</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="实拍图" width="100" align="center">
|
<el-table-column label="实拍图" width="120" align="center">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-image v-if="getFirstImage(row.live_image)" :src="getFirstImage(row.live_image)"
|
<template v-if="getImageList(row.live_image).length > 0">
|
||||||
:preview-src-list="getImageList(row.live_image)" style="width: 50px; height: 50px; border-radius: 4px"
|
<el-popover placement="right" :width="400" trigger="hover" :teleported="true">
|
||||||
fit="cover" preview-teleported />
|
<template #reference>
|
||||||
|
<el-image :src="getFirstImage(row.live_image)" :preview-src-list="getImageList(row.live_image)"
|
||||||
|
style="width: 48px; height: 48px; border-radius: 4px; cursor: pointer" fit="cover"
|
||||||
|
preview-teleported />
|
||||||
|
</template>
|
||||||
|
<div>
|
||||||
|
<div style="font-weight: 600; margin-bottom: 10px; font-size: 14px;">
|
||||||
|
共{{ getImageList(row.live_image).length }}张图片
|
||||||
|
</div>
|
||||||
|
<div style="display: flex; flex-wrap: wrap; gap: 8px;">
|
||||||
|
<el-image v-for="(img, idx) in getImageList(row.live_image)" :key="idx" :src="img"
|
||||||
|
:preview-src-list="getImageList(row.live_image)" :initial-index="idx"
|
||||||
|
style="width: 64px; height: 64px; border-radius: 4px; cursor: pointer" fit="cover"
|
||||||
|
preview-teleported />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-popover>
|
||||||
|
</template>
|
||||||
<span v-else style="color: #c0c4cc">暂无</span>
|
<span v-else style="color: #c0c4cc">暂无</span>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
@ -107,8 +147,7 @@
|
|||||||
size="small">
|
size="small">
|
||||||
{{ shop.shop_alias_name }}({{ shop.shop_type_name }}) - 任务已创建但发送到店铺失败
|
{{ shop.shop_alias_name }}({{ shop.shop_type_name }}) - 任务已创建但发送到店铺失败
|
||||||
</el-tag>
|
</el-tag>
|
||||||
<el-tag v-else-if="shop.out_task_log_id > 0 && shop.status == 2" effect="plain"
|
<el-tag v-else-if="shop.out_task_log_id > 0 && shop.status == 2" effect="plain" size="small">
|
||||||
size="small">
|
|
||||||
{{ shop.shop_alias_name }}({{ shop.shop_type_name }}) - 任务已创建但发送到店铺成功
|
{{ shop.shop_alias_name }}({{ shop.shop_type_name }}) - 任务已创建但发送到店铺成功
|
||||||
</el-tag>
|
</el-tag>
|
||||||
<el-tag v-else effect="plain" size="small">
|
<el-tag v-else effect="plain" size="small">
|
||||||
@ -158,6 +197,16 @@
|
|||||||
<el-table-column prop="updated_at" label="更新时间" width="170" align="center">
|
<el-table-column prop="updated_at" label="更新时间" width="170" align="center">
|
||||||
<template #default="{ row }">{{ formatTimestamp(row.updated_at) }}</template>
|
<template #default="{ row }">{{ formatTimestamp(row.updated_at) }}</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" width="180" align="center" fixed="right">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-button type="primary" link :icon="Edit" size="small" @click="handleEdit(row)">修改</el-button>
|
||||||
|
<el-tooltip v-if="row.warehouse_name" content="已落位商品不可删除" placement="top" :disabled="false">
|
||||||
|
<el-button type="danger" link :icon="Delete" size="small" disabled>删除</el-button>
|
||||||
|
</el-tooltip>
|
||||||
|
<el-button v-else type="danger" link :icon="Delete" size="small" @click="handleDelete(row)">删除</el-button>
|
||||||
|
<el-button type="danger" link :icon="Delete" size="small">销毁</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
|
|
||||||
<div class="pagination-wrapper">
|
<div class="pagination-wrapper">
|
||||||
@ -175,14 +224,8 @@
|
|||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="上传文件">
|
<el-form-item label="上传文件">
|
||||||
<el-upload
|
<el-upload ref="uploadRef" :auto-upload="false" :show-file-list="true" :limit="1"
|
||||||
ref="uploadRef"
|
:on-change="handleFileChange" accept=".xlsx,.xls">
|
||||||
:auto-upload="false"
|
|
||||||
:show-file-list="true"
|
|
||||||
:limit="1"
|
|
||||||
:on-change="handleFileChange"
|
|
||||||
accept=".xlsx,.xls"
|
|
||||||
>
|
|
||||||
<el-button type="primary" :icon="Upload">选择文件</el-button>
|
<el-button type="primary" :icon="Upload">选择文件</el-button>
|
||||||
<template #tip>
|
<template #tip>
|
||||||
<span style="font-size: 12px; color: #909399;">支持 .xlsx / .xls 格式</span>
|
<span style="font-size: 12px; color: #909399;">支持 .xlsx / .xls 格式</span>
|
||||||
@ -193,7 +236,8 @@
|
|||||||
<template #footer>
|
<template #footer>
|
||||||
<span class="dialog-footer">
|
<span class="dialog-footer">
|
||||||
<el-button @click="importDialogVisible = false">取消</el-button>
|
<el-button @click="importDialogVisible = false">取消</el-button>
|
||||||
<el-button type="primary" :loading="importLoading" :disabled="!importWarehouseId || !importFile" @click="handleImportSubmit">
|
<el-button type="primary" :loading="importLoading" :disabled="!importWarehouseId || !importFile"
|
||||||
|
@click="handleImportSubmit">
|
||||||
开始导入
|
开始导入
|
||||||
</el-button>
|
</el-button>
|
||||||
</span>
|
</span>
|
||||||
@ -204,11 +248,11 @@
|
|||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, ref, reactive, onMounted } from 'vue'
|
import { defineComponent, ref, reactive, onMounted } from 'vue'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
import { Search, Refresh, MoreFilled, Upload } from '@element-plus/icons-vue'
|
import { Search, Refresh, MoreFilled, Upload, Edit, Delete } from '@element-plus/icons-vue'
|
||||||
import GoodsPop from '@/components/goodsPop/index.vue'
|
import GoodsPop from '@/components/goodsPop/index.vue'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import { fetchProductList, retryProductPublish, importProductsByExcel } from '@/api/product'
|
import { fetchProductList, retryProductPublish, importProductsByExcel, deleteProduct } from '@/api/product'
|
||||||
import { fetchWarehouseList } from '@/api/warehouse'
|
import { fetchWarehouseList } from '@/api/warehouse'
|
||||||
|
|
||||||
interface ShopListItem {
|
interface ShopListItem {
|
||||||
@ -245,6 +289,7 @@ interface ProductItem {
|
|||||||
location_code: string
|
location_code: string
|
||||||
created_at: number
|
created_at: number
|
||||||
updated_at: number
|
updated_at: number
|
||||||
|
appearance?: number | null
|
||||||
shop_list?: ShopListItem[]
|
shop_list?: ShopListItem[]
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -258,6 +303,7 @@ export default defineComponent({
|
|||||||
const loading = ref<boolean>(false)
|
const loading = ref<boolean>(false)
|
||||||
const tableData = ref<ProductItem[]>([])
|
const tableData = ref<ProductItem[]>([])
|
||||||
const selectedRows = ref<ProductItem[]>([])
|
const selectedRows = ref<ProductItem[]>([])
|
||||||
|
const loadedOnce = ref<boolean>(false)
|
||||||
|
|
||||||
const searchParams = reactive<{
|
const searchParams = reactive<{
|
||||||
keyword: string
|
keyword: string
|
||||||
@ -279,6 +325,15 @@ export default defineComponent({
|
|||||||
100: '十品',
|
100: '十品',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ========== 统计卡片 ==========
|
||||||
|
const stats = reactive({
|
||||||
|
total: 0,
|
||||||
|
located_count: 0,
|
||||||
|
unlocated_count: 0,
|
||||||
|
enabled_count: 0,
|
||||||
|
disabled_count: 0
|
||||||
|
})
|
||||||
|
|
||||||
const formatAppearance = (value?: number | null): string => {
|
const formatAppearance = (value?: number | null): string => {
|
||||||
if (value == null) return '-'
|
if (value == null) return '-'
|
||||||
return CONDITION_MAP[value] ?? String(value)
|
return CONDITION_MAP[value] ?? String(value)
|
||||||
@ -289,17 +344,45 @@ export default defineComponent({
|
|||||||
return dayjs.unix(Number(timestamp)).format('YYYY-MM-DD HH:mm:ss')
|
return dayjs.unix(Number(timestamp)).format('YYYY-MM-DD HH:mm:ss')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取第一张图片 URL,支持以下格式:
|
||||||
|
* - 逗号分隔字符串: "url1,url2"
|
||||||
|
* - 数组: ["url1", "url2"]
|
||||||
|
* - 数组含逗号字符串: ["url1,url2"]
|
||||||
|
*/
|
||||||
const getFirstImage = (liveImage: any): string => {
|
const getFirstImage = (liveImage: any): string => {
|
||||||
if (!liveImage) return ''
|
if (!liveImage) return ''
|
||||||
if (Array.isArray(liveImage) && liveImage.length > 0) return liveImage[0]
|
if (Array.isArray(liveImage)) {
|
||||||
if (typeof liveImage === 'string') return liveImage
|
const flat = liveImage.flatMap((item: string) => {
|
||||||
|
if (typeof item === 'string') return item.split(',').map((s) => s.trim()).filter(Boolean)
|
||||||
|
return []
|
||||||
|
})
|
||||||
|
return flat[0] || ''
|
||||||
|
}
|
||||||
|
if (typeof liveImage === 'string') {
|
||||||
|
const parts = liveImage.split(',').map((s) => s.trim()).filter(Boolean)
|
||||||
|
return parts[0] || ''
|
||||||
|
}
|
||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取全部图片 URL 列表,支持以下格式:
|
||||||
|
* - 逗号分隔字符串: "url1,url2"
|
||||||
|
* - 数组: ["url1", "url2"]
|
||||||
|
* - 数组含逗号字符串: ["url1,url2", "url3"]
|
||||||
|
*/
|
||||||
const getImageList = (liveImage: any): string[] => {
|
const getImageList = (liveImage: any): string[] => {
|
||||||
if (!liveImage) return []
|
if (!liveImage) return []
|
||||||
if (Array.isArray(liveImage)) return liveImage.filter((url: string) => typeof url === 'string')
|
if (Array.isArray(liveImage)) {
|
||||||
if (typeof liveImage === 'string') return [liveImage]
|
return liveImage.flatMap((item: string) => {
|
||||||
|
if (typeof item === 'string') return item.split(',').map((s) => s.trim()).filter(Boolean)
|
||||||
|
return []
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (typeof liveImage === 'string') {
|
||||||
|
return liveImage.split(',').map((s) => s.trim()).filter(Boolean)
|
||||||
|
}
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -324,6 +407,12 @@ export default defineComponent({
|
|||||||
const data = res || {}
|
const data = res || {}
|
||||||
tableData.value = data.list || []
|
tableData.value = data.list || []
|
||||||
pagination.total = data.total || 0
|
pagination.total = data.total || 0
|
||||||
|
stats.total = data.total
|
||||||
|
stats.located_count = data.located_count
|
||||||
|
stats.unlocated_count = data.unlocated_count
|
||||||
|
stats.enabled_count = data.enabled_count
|
||||||
|
stats.disabled_count = data.disabled_count
|
||||||
|
loadedOnce.value = true
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
ElMessage.error({ message: '获取商品列表失败', customClass: 'scan-error-message' })
|
ElMessage.error({ message: '获取商品列表失败', customClass: 'scan-error-message' })
|
||||||
} finally {
|
} finally {
|
||||||
@ -455,9 +544,10 @@ export default defineComponent({
|
|||||||
})
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
loading, tableData,
|
loading, tableData, loadedOnce,
|
||||||
searchParams, pagination,
|
searchParams, pagination,
|
||||||
Search, Refresh, MoreFilled, Upload,
|
Search, Refresh, MoreFilled, Upload, Edit, Delete,
|
||||||
|
stats,
|
||||||
formatTimestamp, formatAppearance, getFirstImage, getImageList, showAllShopMsg,
|
formatTimestamp, formatAppearance, getFirstImage, getImageList, showAllShopMsg,
|
||||||
handleSearch, resetSearch, handleCurrentChange, handleSizeChange,
|
handleSearch, resetSearch, handleCurrentChange, handleSizeChange,
|
||||||
handleEdit, handleDelete, reTryGoosTask,
|
handleEdit, handleDelete, reTryGoosTask,
|
||||||
@ -487,6 +577,42 @@ export default defineComponent({
|
|||||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.05);
|
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ========== 统计卡片 ========== */
|
||||||
|
.stats-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 140px;
|
||||||
|
background: white;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 20px 24px;
|
||||||
|
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.05);
|
||||||
|
text-align: center;
|
||||||
|
transition: box-shadow 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card:hover {
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card__value {
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #303133;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card__label {
|
||||||
|
margin-top: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #909399;
|
||||||
|
}
|
||||||
|
|
||||||
.pagination-wrapper {
|
.pagination-wrapper {
|
||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@ -15,16 +15,17 @@
|
|||||||
共 <strong>{{ pagination.total }}</strong> 个库位,
|
共 <strong>{{ pagination.total }}</strong> 个库位,
|
||||||
总计 <strong>{{ totalAllQuantity }}</strong> 件商品
|
总计 <strong>{{ totalAllQuantity }}</strong> 件商品
|
||||||
</span>
|
</span>
|
||||||
|
<el-button type="danger" :icon="Delete" :disabled="selectedRows.length === 0"
|
||||||
|
@click="handleBatchDestroy">
|
||||||
|
批量销毁({{ selectedRows.length }})
|
||||||
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<el-table
|
<el-table ref="tableRef" :data="flatRows" v-loading="loading" border stripe style="width: 100%"
|
||||||
:data="flatRows"
|
:span-method="mergeSpanMethod" :header-cell-style="{ background: '#f5f7fa' }"
|
||||||
v-loading="loading"
|
row-key="rowId" @selection-change="handleSelectionChange">
|
||||||
border stripe
|
<el-table-column type="selection" width="45" align="center"
|
||||||
style="width: 100%"
|
:selectable="checkSelectable" :reserve-selection="true" />
|
||||||
:span-method="mergeSpanMethod"
|
|
||||||
:header-cell-style="{ background: '#f5f7fa' }"
|
|
||||||
>
|
|
||||||
<el-table-column prop="warehouse_name" label="所属仓库" width="120" align="center" show-overflow-tooltip>
|
<el-table-column prop="warehouse_name" label="所属仓库" width="120" align="center" show-overflow-tooltip>
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<span v-if="row._isLocationRow" style="font-weight: 600">{{ row.warehouse_name }}</span>
|
<span v-if="row._isLocationRow" style="font-weight: 600">{{ row.warehouse_name }}</span>
|
||||||
@ -73,6 +74,13 @@
|
|||||||
<span v-if="!row._isLocationRow">{{ formatTimestamp(row.created_at) }}</span>
|
<span v-if="!row._isLocationRow">{{ formatTimestamp(row.created_at) }}</span>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" width="100" align="center" fixed="right">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<template v-if="!row._isLocationRow">
|
||||||
|
<el-button type="danger" link :icon="Delete">销毁</el-button>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
|
|
||||||
<div class="pagination-wrapper">
|
<div class="pagination-wrapper">
|
||||||
@ -85,8 +93,8 @@
|
|||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, ref, reactive, computed, onMounted } from 'vue'
|
import { defineComponent, ref, reactive, computed, onMounted } from 'vue'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
import { Search, Refresh } from '@element-plus/icons-vue'
|
import { Search, Refresh, Delete } from '@element-plus/icons-vue'
|
||||||
import { fetchGoodsListByLocation } from '@/api/inventory'
|
import { fetchGoodsListByLocation } from '@/api/inventory'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
|
|
||||||
@ -135,18 +143,24 @@ interface FlatRow {
|
|||||||
_summaryText?: string
|
_summaryText?: string
|
||||||
|
|
||||||
// 明细字段
|
// 明细字段
|
||||||
|
product_id?: number
|
||||||
product_name?: string
|
product_name?: string
|
||||||
|
warehouse_id?: number
|
||||||
|
location_id?: number
|
||||||
barcode?: string
|
barcode?: string
|
||||||
quantity?: number
|
quantity?: number
|
||||||
sale_price?: number
|
sale_price?: number
|
||||||
batch_no?: string
|
batch_no?: string
|
||||||
created_at?: number
|
created_at?: number
|
||||||
|
name?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'ProductByLocation',
|
name: 'ProductByLocation',
|
||||||
setup() {
|
setup() {
|
||||||
const loading = ref<boolean>(false)
|
const loading = ref<boolean>(false)
|
||||||
|
const tableRef = ref()
|
||||||
|
const selectedRows = ref<FlatRow[]>([])
|
||||||
const sourceGroups = ref<LocationGroup[]>([])
|
const sourceGroups = ref<LocationGroup[]>([])
|
||||||
|
|
||||||
const searchParams = reactive<{ keyword: string }>({ keyword: '' })
|
const searchParams = reactive<{ keyword: string }>({ keyword: '' })
|
||||||
@ -180,7 +194,11 @@ export default defineComponent({
|
|||||||
_isLocationRow: false,
|
_isLocationRow: false,
|
||||||
_locationId: group.location_id,
|
_locationId: group.location_id,
|
||||||
_detailCount: 0,
|
_detailCount: 0,
|
||||||
|
product_id: detail.product_id,
|
||||||
product_name: detail.product_name,
|
product_name: detail.product_name,
|
||||||
|
name: detail.product_name,
|
||||||
|
warehouse_id: group.warehouse_id,
|
||||||
|
location_id: group.location_id,
|
||||||
barcode: detail.barcode,
|
barcode: detail.barcode,
|
||||||
quantity: detail.quantity,
|
quantity: detail.quantity,
|
||||||
locked_quantity: detail.locked_quantity,
|
locked_quantity: detail.locked_quantity,
|
||||||
@ -210,8 +228,8 @@ export default defineComponent({
|
|||||||
})
|
})
|
||||||
|
|
||||||
const mergeSpanMethod = ({ rowIndex, columnIndex }: { row: any; column: any; rowIndex: number; columnIndex: number }): { rowspan: number; colspan: number } | null => {
|
const mergeSpanMethod = ({ rowIndex, columnIndex }: { row: any; column: any; rowIndex: number; columnIndex: number }): { rowspan: number; colspan: number } | null => {
|
||||||
// 只合并前2列(所属仓库、库位编号)
|
// 只合并前2列(所属仓库、库位编号),columnIndex 0=selection, 1=所属仓库, 2=库位编号
|
||||||
if (columnIndex > 1) return { rowspan: 1, colspan: 1 }
|
if (columnIndex > 2) return { rowspan: 1, colspan: 1 }
|
||||||
const row = flatRows.value[rowIndex]
|
const row = flatRows.value[rowIndex]
|
||||||
if (!row) return { rowspan: 1, colspan: 1 }
|
if (!row) return { rowspan: 1, colspan: 1 }
|
||||||
const span = locationSpanMap.value.get(row.rowId)
|
const span = locationSpanMap.value.get(row.rowId)
|
||||||
@ -271,21 +289,56 @@ export default defineComponent({
|
|||||||
void loadData()
|
void loadData()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 仅允许库位汇总行被选中 */
|
||||||
|
const checkSelectable = (row: FlatRow): boolean => {
|
||||||
|
return row._isLocationRow === true
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSelectionChange = (selection: FlatRow[]): void => {
|
||||||
|
selectedRows.value = selection
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 批量销毁选中库位(仅前端标记,不调接口) */
|
||||||
|
const handleBatchDestroy = (): void => {
|
||||||
|
if (selectedRows.value.length === 0) {
|
||||||
|
ElMessage.warning({ message: '请先选择要销毁的库位', customClass: 'scan-error-message' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const locationIds = selectedRows.value.map(row => row._locationId)
|
||||||
|
const codes = selectedRows.value.map(row => `"${row.location_code}"`).join('、')
|
||||||
|
ElMessageBox.confirm(
|
||||||
|
`确定要销毁以下 ${locationIds.length} 个库位吗?\n\n${codes}\n\n此操作将删除这些库位及其下的所有商品,不可恢复!`,
|
||||||
|
'批量销毁确认',
|
||||||
|
{
|
||||||
|
confirmButtonText: '确定销毁',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'error'
|
||||||
|
}
|
||||||
|
).then(() => {
|
||||||
|
ElMessage.success({ message: '销毁完成', customClass: 'scan-success-message' })
|
||||||
|
selectedRows.value = []
|
||||||
|
}).catch(() => { })
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
void loadData()
|
void loadData()
|
||||||
})
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
loading,
|
loading,
|
||||||
|
tableRef,
|
||||||
|
selectedRows,
|
||||||
flatRows,
|
flatRows,
|
||||||
sourceGroups,
|
sourceGroups,
|
||||||
searchParams, pagination,
|
searchParams, pagination,
|
||||||
totalAllQuantity,
|
totalAllQuantity,
|
||||||
Search, Refresh,
|
Search, Refresh, Delete,
|
||||||
mergeSpanMethod,
|
mergeSpanMethod,
|
||||||
|
checkSelectable,
|
||||||
formatTimestamp, formatPrice,
|
formatTimestamp, formatPrice,
|
||||||
handleSearch, resetSearch,
|
handleSearch, resetSearch,
|
||||||
handleCurrentChange, handleSizeChange
|
handleCurrentChange, handleSizeChange,
|
||||||
|
handleSelectionChange, handleBatchDestroy
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@ -114,6 +114,7 @@ import { getBookInfo, syncBook } from '@/api/book'
|
|||||||
import { queryGoodsPrice } from '@/api/config'
|
import { queryGoodsPrice } from '@/api/config'
|
||||||
import { getAdminUserInfo } from '@/utils/auth'
|
import { getAdminUserInfo } from '@/utils/auth'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
|
import { uploadLivingPicture } from '@/utils/uploadLivingPicture'
|
||||||
|
|
||||||
// 定义 BookInfo 接口
|
// 定义 BookInfo 接口
|
||||||
interface BookInfo {
|
interface BookInfo {
|
||||||
@ -698,7 +699,7 @@ const retakePhoto = async () => {
|
|||||||
const pictureName = productId ? `${productId}-${isbn}-${random5}.jpg` : null
|
const pictureName = productId ? `${productId}-${isbn}-${random5}.jpg` : null
|
||||||
if (pictureName) {
|
if (pictureName) {
|
||||||
lastLivingPictureName.value = pictureName
|
lastLivingPictureName.value = pictureName
|
||||||
await uploadLivingPicture(base64Data, isbn, pictureName)
|
await uploadLivingPicture(base64Data, pictureName, isbn)
|
||||||
|
|
||||||
// 第三步:上传拼多多图片空间
|
// 第三步:上传拼多多图片空间
|
||||||
let pddUrl = ''
|
let pddUrl = ''
|
||||||
@ -885,7 +886,7 @@ const takePhotoPreview = async () => {
|
|||||||
const pictureName = productId ? `${productId}-${isbn}-${random5}.jpg` : null
|
const pictureName = productId ? `${productId}-${isbn}-${random5}.jpg` : null
|
||||||
if (pictureName) {
|
if (pictureName) {
|
||||||
lastLivingPictureName.value = pictureName
|
lastLivingPictureName.value = pictureName
|
||||||
await uploadLivingPicture(base64Data, isbn, pictureName)
|
await uploadLivingPicture(base64Data, pictureName, isbn)
|
||||||
console.log('[实拍] 图片名:', pictureName, '| 地址: https://shxy.image.yushutx.com/living-picture/' + pictureName)
|
console.log('[实拍] 图片名:', pictureName, '| 地址: https://shxy.image.yushutx.com/living-picture/' + pictureName)
|
||||||
|
|
||||||
// 第三步半:将视频帧图片上传拼多多图片空间,获取 pddUrl
|
// 第三步半:将视频帧图片上传拼多多图片空间,获取 pddUrl
|
||||||
@ -1270,7 +1271,7 @@ const handleOcrAssign = async (assignments: Record<string, string>) => {
|
|||||||
// 第一步:先上传截取的视频帧到图片服务,文件名为 {barcode}.jpg
|
// 第一步:先上传截取的视频帧到图片服务,文件名为 {barcode}.jpg
|
||||||
const coverPictureName = `${isbn}.jpg`
|
const coverPictureName = `${isbn}.jpg`
|
||||||
console.log('[OCR上传封面] 开始上传, 文件名:', coverPictureName)
|
console.log('[OCR上传封面] 开始上传, 文件名:', coverPictureName)
|
||||||
await uploadLivingPicture(base64Data, isbn, coverPictureName)
|
await uploadLivingPicture(base64Data, coverPictureName, isbn)
|
||||||
const coverImageUrl = `https://shxy.image.yushutx.com/living-picture/${coverPictureName}`
|
const coverImageUrl = `https://shxy.image.yushutx.com/living-picture/${coverPictureName}`
|
||||||
console.log('[OCR上传封面] 完成, URL:', coverImageUrl)
|
console.log('[OCR上传封面] 完成, URL:', coverImageUrl)
|
||||||
|
|
||||||
@ -1294,7 +1295,7 @@ const handleOcrAssign = async (assignments: Record<string, string>) => {
|
|||||||
|
|
||||||
if (pictureName) {
|
if (pictureName) {
|
||||||
lastLivingPictureName.value = pictureName
|
lastLivingPictureName.value = pictureName
|
||||||
await uploadLivingPicture(base64Data, isbn, pictureName)
|
await uploadLivingPicture(base64Data, pictureName, isbn)
|
||||||
|
|
||||||
// 第四步:更新商品的 live_image 为实拍图片地址
|
// 第四步:更新商品的 live_image 为实拍图片地址
|
||||||
await updateProductLiveImage(productId, pictureName, {
|
await updateProductLiveImage(productId, pictureName, {
|
||||||
@ -1857,46 +1858,7 @@ async function generateWaveBarcode() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 上传实拍图片到图片服务
|
|
||||||
* PUT http://:19000/living-picture/{pictureName}
|
|
||||||
* Content-Type: image/jpeg
|
|
||||||
* @param {string} base64Data - base64图片数据
|
|
||||||
* @param {string} isbn - ISBN(仅用于日志)
|
|
||||||
* @param {string} [pictureName] - 可选自定义文件名,默认 {isbn}.jpg
|
|
||||||
*/
|
|
||||||
async function uploadLivingPicture(base64Data: string, isbn: string, pictureName?: string) {
|
|
||||||
const fileName = pictureName || `${isbn}.jpg`
|
|
||||||
console.log('[上传实拍] 开始上传, 文件名:', fileName)
|
|
||||||
try {
|
|
||||||
// 去掉 base64 头,转为二进制
|
|
||||||
const base64 = base64Data.replace(/^data:image\/\w+;base64,/, '')
|
|
||||||
const binaryString = atob(base64)
|
|
||||||
const bytes = new Uint8Array(binaryString.length)
|
|
||||||
for (let i = 0; i < binaryString.length; i++) {
|
|
||||||
bytes[i] = binaryString.charCodeAt(i)
|
|
||||||
}
|
|
||||||
const blob = new Blob([bytes], { type: 'image/jpeg' })
|
|
||||||
|
|
||||||
const url = `https://shxy.image.yushutx.com/living-picture/${fileName}`
|
|
||||||
console.log('[上传实拍] PUT URL:', url, '| Blob大小:', blob.size)
|
|
||||||
|
|
||||||
const resp = await fetch(url, {
|
|
||||||
method: 'PUT',
|
|
||||||
headers: { 'Content-Type': 'image/jpeg' },
|
|
||||||
body: blob
|
|
||||||
})
|
|
||||||
console.log('[上传实拍] 响应状态:', resp.status, resp.statusText)
|
|
||||||
|
|
||||||
if (!resp.ok) {
|
|
||||||
console.warn(`实拍图片上传失败 [${isbn}]: HTTP ${resp.status}`)
|
|
||||||
} else {
|
|
||||||
console.log(`实拍图片上传成功 [${isbn}]`)
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.warn('实拍图片上传异常:', err instanceof Error ? err.message : String(err))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 处理扫码输入变化
|
* 处理扫码输入变化
|
||||||
@ -2695,7 +2657,7 @@ async function processCustomItemUpload(
|
|||||||
// 第2步:上传实拍图片到图片服务
|
// 第2步:上传实拍图片到图片服务
|
||||||
const random5 = String(Math.floor(Math.random() * 100000)).padStart(5, '0')
|
const random5 = String(Math.floor(Math.random() * 100000)).padStart(5, '0')
|
||||||
const pictureName = `${productId}-${isbn}-${random5}.jpg`
|
const pictureName = `${productId}-${isbn}-${random5}.jpg`
|
||||||
await uploadLivingPicture(base64Data, isbn, pictureName)
|
await uploadLivingPicture(base64Data, pictureName, isbn)
|
||||||
|
|
||||||
// 第3步:上传到拼多多图片空间
|
// 第3步:上传到拼多多图片空间
|
||||||
let pddUrl = ''
|
let pddUrl = ''
|
||||||
|
|||||||
@ -544,8 +544,6 @@ defineExpose({ getAllGoods, goodsList, clearAll, getUncommittedGoods, markAllCom
|
|||||||
|
|
||||||
.book-card-remove {
|
.book-card-remove {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
opacity: 0;
|
|
||||||
transition: opacity 0.2s ease;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 售价样式:红色,紧邻信息区 */
|
/* 售价样式:红色,紧邻信息区 */
|
||||||
@ -569,11 +567,6 @@ defineExpose({ getAllGoods, goodsList, clearAll, getUncommittedGoods, markAllCom
|
|||||||
text-underline-offset: 3px;
|
text-underline-offset: 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.book-card:hover .book-card-remove {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.book-list-empty {
|
.book-list-empty {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@ -271,7 +271,7 @@ const routes = [
|
|||||||
name: 'printer-manager',
|
name: 'printer-manager',
|
||||||
component: () => import('@/views/printerManager/printerManager.vue'),
|
component: () => import('@/views/printerManager/printerManager.vue'),
|
||||||
meta: {
|
meta: {
|
||||||
title: '打印机管理',
|
title: '系统硬件配置',
|
||||||
requiresAuth: true
|
requiresAuth: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
68
src/utils/uploadLivingPicture.ts
Normal file
68
src/utils/uploadLivingPicture.ts
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
/**
|
||||||
|
* 上传图片到外部图床(living-picture 服务)
|
||||||
|
* PUT https://shxy.image.yushutx.com/living-picture/{pictureName}
|
||||||
|
*
|
||||||
|
* 支持传入 File 对象或 base64 字符串。
|
||||||
|
* 原函数位于 src/components/wave/camera.vue,提取为独立工具函数以便复用。
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将 File 对象转为 base64 字符串
|
||||||
|
*/
|
||||||
|
function fileToBase64(file: File): Promise<string> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const reader = new FileReader()
|
||||||
|
reader.onload = () => resolve(reader.result as string)
|
||||||
|
reader.onerror = () => reject(new Error('文件读取失败'))
|
||||||
|
reader.readAsDataURL(file)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 上传图片到 living-picture 图床
|
||||||
|
* @param file - File 对象或 base64 数据字符串(含 data:image/...;base64, 前缀)
|
||||||
|
* @param pictureName - 上传后的文件名(不含路径),如 'abc123.jpg'
|
||||||
|
* @param isbn - ISBN(仅用于日志,可选)
|
||||||
|
* @returns 上传后的完整图片 URL
|
||||||
|
*/
|
||||||
|
export async function uploadLivingPicture(
|
||||||
|
file: File | string,
|
||||||
|
pictureName: string,
|
||||||
|
isbn?: string
|
||||||
|
): Promise<string> {
|
||||||
|
// 若传入 File 对象,先转为 base64
|
||||||
|
let base64Data: string
|
||||||
|
if (typeof file === 'string') {
|
||||||
|
base64Data = file
|
||||||
|
} else {
|
||||||
|
base64Data = await fileToBase64(file)
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[上传实拍] 开始上传, 文件名:', pictureName)
|
||||||
|
|
||||||
|
// 去掉 base64 头,转为二进制
|
||||||
|
const base64 = base64Data.replace(/^data:image\/\w+;base64,/, '')
|
||||||
|
const binaryString = atob(base64)
|
||||||
|
const bytes = new Uint8Array(binaryString.length)
|
||||||
|
for (let i = 0; i < binaryString.length; i++) {
|
||||||
|
bytes[i] = binaryString.charCodeAt(i)
|
||||||
|
}
|
||||||
|
const blob = new Blob([bytes], { type: 'image/jpeg' })
|
||||||
|
|
||||||
|
const url = `https://shxy.image.yushutx.com/living-picture/${pictureName}`
|
||||||
|
console.log('[上传实拍] PUT URL:', url, '| Blob大小:', blob.size)
|
||||||
|
|
||||||
|
const resp = await fetch(url, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: { 'Content-Type': 'image/jpeg' },
|
||||||
|
body: blob
|
||||||
|
})
|
||||||
|
console.log('[上传实拍] 响应状态:', resp.status, resp.statusText)
|
||||||
|
|
||||||
|
if (!resp.ok) {
|
||||||
|
throw new Error(`实拍图片上传失败 [${isbn || 'unknown'}]: HTTP ${resp.status}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`实拍图片上传成功 [${isbn || 'unknown'}]`)
|
||||||
|
return url
|
||||||
|
}
|
||||||
@ -26,17 +26,15 @@
|
|||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
<el-input v-model="dir" placeholder="如 C:\\verifyTool" clearable @clear="dir = ''" />
|
<el-input v-model="dir" placeholder="如 C:\\verifyTool" clearable @clear="dir = ''" />
|
||||||
|
|
||||||
|
|
||||||
|
<div class="save-bar" style="margin-top: 18px;">
|
||||||
<el-tooltip content="通过自定义协议启动本机程序" placement="top" trigger="click">
|
<el-tooltip content="通过自定义协议启动本机程序" placement="top" trigger="click">
|
||||||
<el-button
|
<el-button type="primary" @click="handleOpenExe" size="small">打开程序</el-button>
|
||||||
type="primary"
|
|
||||||
:icon="VideoPlay"
|
|
||||||
style="margin-left: 10px"
|
|
||||||
:disabled="!dir"
|
|
||||||
@click="handleOpenExe"
|
|
||||||
>
|
|
||||||
打开程序
|
|
||||||
</el-button>
|
|
||||||
</el-tooltip>
|
</el-tooltip>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</div>
|
</div>
|
||||||
<div class="config-row">
|
<div class="config-row">
|
||||||
@ -59,7 +57,8 @@
|
|||||||
<template #label>
|
<template #label>
|
||||||
<span class="label-with-icon">
|
<span class="label-with-icon">
|
||||||
<span>端口</span>
|
<span>端口</span>
|
||||||
<el-tooltip content="默认是8080 但是由于每台电脑的环境都不同 可能会出现端口冲突 在出现问题时候 请第一时间联系网管" placement="top" trigger="click">
|
<el-tooltip content="默认是8080 但是由于每台电脑的环境都不同 可能会出现端口冲突 在出现问题时候 请第一时间联系网管" placement="top"
|
||||||
|
trigger="click">
|
||||||
<el-icon style="cursor: pointer;">
|
<el-icon style="cursor: pointer;">
|
||||||
<QuestionFilled />
|
<QuestionFilled />
|
||||||
</el-icon>
|
</el-icon>
|
||||||
@ -226,8 +225,7 @@
|
|||||||
autocomplete="new-password" />
|
autocomplete="new-password" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-tooltip v-if="appendAccounts.length > 1" content="删除此条账号输入" placement="top" trigger="click">
|
<el-tooltip v-if="appendAccounts.length > 1" content="删除此条账号输入" placement="top" trigger="click">
|
||||||
<el-button type="danger" :icon="Delete" circle
|
<el-button type="danger" :icon="Delete" circle @click="removeAppendAccount(index)" />
|
||||||
@click="removeAppendAccount(index)" />
|
|
||||||
</el-tooltip>
|
</el-tooltip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -255,8 +253,7 @@
|
|||||||
<el-input v-model="item.password" type="password" placeholder="请输入密码" clearable show-password
|
<el-input v-model="item.password" type="password" placeholder="请输入密码" clearable show-password
|
||||||
autocomplete="new-password" />
|
autocomplete="new-password" />
|
||||||
<el-tooltip v-if="accounts.length > 1" content="删除此条账号输入" placement="top" trigger="click">
|
<el-tooltip v-if="accounts.length > 1" content="删除此条账号输入" placement="top" trigger="click">
|
||||||
<el-button type="danger" :icon="Delete" circle
|
<el-button type="danger" :icon="Delete" circle @click="removeAccount(index)" />
|
||||||
@click="removeAccount(index)" />
|
|
||||||
</el-tooltip>
|
</el-tooltip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -711,7 +708,7 @@ export default {
|
|||||||
? minPriceNum
|
? minPriceNum
|
||||||
: 1.00
|
: 1.00
|
||||||
try {
|
try {
|
||||||
await saveNewPrice(ip.value, port.value, sendNewPrice, sendPlaceholderDownPrice, sendMinShippingFee, sendMinPrice,verifyIndex.value)
|
await saveNewPrice(ip.value, port.value, sendNewPrice, sendPlaceholderDownPrice, sendMinShippingFee, sendMinPrice, verifyIndex.value)
|
||||||
console.log('核价价格相关配置已保存')
|
console.log('核价价格相关配置已保存')
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.log('核价价格相关配置保存失败:', err.message)
|
console.log('核价价格相关配置保存失败:', err.message)
|
||||||
|
|||||||
@ -76,6 +76,33 @@
|
|||||||
|
|
||||||
<el-empty v-if="!loading && printers.length === 0" description="未检测到可用打印机" />
|
<el-empty v-if="!loading && printers.length === 0" description="未检测到可用打印机" />
|
||||||
</el-card>
|
</el-card>
|
||||||
|
|
||||||
|
<el-card class="printer-card">
|
||||||
|
<template #header>
|
||||||
|
<div style="display: flex; justify-content: space-between; align-items: center; width: 100%;">
|
||||||
|
<span>摄像头配置</span>
|
||||||
|
<el-button type="primary" @click="refreshCameras" :loading="cameraLoading">
|
||||||
|
<el-icon>
|
||||||
|
<Refresh />
|
||||||
|
</el-icon>
|
||||||
|
刷新摄像头列表
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<el-form label-width="140px" label-position="left" class="printer-form">
|
||||||
|
<el-form-item label="摄像头">
|
||||||
|
<div class="printer-row">
|
||||||
|
<el-select v-model="selectedCamera" placeholder="请选择摄像头" style="width: 400px" clearable>
|
||||||
|
<el-option v-for="c in cameras" :key="c.deviceId" :label="c.label" :value="c.deviceId" />
|
||||||
|
</el-select>
|
||||||
|
<el-button type="primary" @click="confirmCamera">确定</el-button>
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
<el-empty v-if="!cameraLoading && cameras.length === 0" description="未检测到可用摄像头" />
|
||||||
|
</el-card>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -89,6 +116,7 @@ import JsBarcode from 'jsbarcode'
|
|||||||
const STORAGE_KEY_BARCODE = 'printer_barcode'
|
const STORAGE_KEY_BARCODE = 'printer_barcode'
|
||||||
const STORAGE_KEY_EXPRESS = 'printer_express'
|
const STORAGE_KEY_EXPRESS = 'printer_express'
|
||||||
const STORAGE_KEY_PAPER_SIZE = 'printer_paper_size'
|
const STORAGE_KEY_PAPER_SIZE = 'printer_paper_size'
|
||||||
|
const STORAGE_KEY_CAMERA = 'printer_camera'
|
||||||
|
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const printers = ref([])
|
const printers = ref([])
|
||||||
@ -96,6 +124,10 @@ const barcodePrinter = ref('')
|
|||||||
const expressPrinter = ref('')
|
const expressPrinter = ref('')
|
||||||
const paperSize = ref('')
|
const paperSize = ref('')
|
||||||
|
|
||||||
|
const cameraLoading = ref(false)
|
||||||
|
const cameras = ref([])
|
||||||
|
const selectedCamera = ref('')
|
||||||
|
|
||||||
const shortcutItems = ['Alt+c', 'Alt+a', 'Alt+x', 'Alt+b']
|
const shortcutItems = ['Alt+c', 'Alt+a', 'Alt+x', 'Alt+b']
|
||||||
const barcodeImages = reactive({})
|
const barcodeImages = reactive({})
|
||||||
|
|
||||||
@ -172,6 +204,36 @@ const confirmExpressPrinter = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const fetchCameras = async () => {
|
||||||
|
cameraLoading.value = true
|
||||||
|
try {
|
||||||
|
const devices = await navigator.mediaDevices.enumerateDevices()
|
||||||
|
cameras.value = devices.filter((d) => d.kind === 'videoinput').map((d) => ({
|
||||||
|
deviceId: d.deviceId,
|
||||||
|
label: d.label || `摄像头 ${d.deviceId.slice(0, 8)}`
|
||||||
|
}))
|
||||||
|
} catch {
|
||||||
|
ElMessage.error({ message: '获取摄像头列表失败', customClass: 'scan-error-message' })
|
||||||
|
cameras.value = []
|
||||||
|
} finally {
|
||||||
|
cameraLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const refreshCameras = () => {
|
||||||
|
fetchCameras()
|
||||||
|
}
|
||||||
|
|
||||||
|
const confirmCamera = () => {
|
||||||
|
if (selectedCamera.value) {
|
||||||
|
localStorage.setItem(STORAGE_KEY_CAMERA, selectedCamera.value)
|
||||||
|
ElMessage.success({ message: '摄像头已保存', duration: 1000, customClass: 'scan-success-message' })
|
||||||
|
} else {
|
||||||
|
localStorage.removeItem(STORAGE_KEY_CAMERA)
|
||||||
|
ElMessage.success({ message: '摄像头已清除', duration: 1000, customClass: 'scan-success-message' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const fetchPrinters = async () => {
|
const fetchPrinters = async () => {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
@ -193,7 +255,9 @@ onMounted(() => {
|
|||||||
barcodePrinter.value = localStorage.getItem(STORAGE_KEY_BARCODE) || ''
|
barcodePrinter.value = localStorage.getItem(STORAGE_KEY_BARCODE) || ''
|
||||||
expressPrinter.value = localStorage.getItem(STORAGE_KEY_EXPRESS) || ''
|
expressPrinter.value = localStorage.getItem(STORAGE_KEY_EXPRESS) || ''
|
||||||
paperSize.value = localStorage.getItem(STORAGE_KEY_PAPER_SIZE) || ''
|
paperSize.value = localStorage.getItem(STORAGE_KEY_PAPER_SIZE) || ''
|
||||||
|
selectedCamera.value = localStorage.getItem(STORAGE_KEY_CAMERA) || ''
|
||||||
fetchPrinters()
|
fetchPrinters()
|
||||||
|
fetchCameras()
|
||||||
|
|
||||||
shortcutItems.forEach(key => fetchBarcodeImage(key))
|
shortcutItems.forEach(key => fetchBarcodeImage(key))
|
||||||
})
|
})
|
||||||
|
|||||||
@ -5,11 +5,7 @@
|
|||||||
</template>
|
</template>
|
||||||
<el-tabs v-model="activeTab" type="border-card">
|
<el-tabs v-model="activeTab" type="border-card">
|
||||||
<el-tab-pane label="按商品查看" name="byGoods">
|
<el-tab-pane label="按商品查看" name="byGoods">
|
||||||
<ProductList
|
<ProductList ref="productListRef" @edit="handleEdit" @delete="handleDelete" />
|
||||||
ref="productListRef"
|
|
||||||
@edit="handleEdit"
|
|
||||||
@delete="handleDelete"
|
|
||||||
/>
|
|
||||||
</el-tab-pane>
|
</el-tab-pane>
|
||||||
<el-tab-pane label="按库位查看" name="byLocation">
|
<el-tab-pane label="按库位查看" name="byLocation">
|
||||||
<ProductByLocation />
|
<ProductByLocation />
|
||||||
@ -17,37 +13,109 @@
|
|||||||
</el-tabs>
|
</el-tabs>
|
||||||
|
|
||||||
<!-- 新增/编辑弹窗 -->
|
<!-- 新增/编辑弹窗 -->
|
||||||
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="600px" destroy-on-close @close="resetForm">
|
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="850px" destroy-on-close @close="resetForm">
|
||||||
<el-form ref="formRef" :model="formData" :rules="formRules" label-width="110px" label-position="right">
|
<el-form ref="formRef" :model="formData" :rules="formRules" label-width="110px" label-position="right">
|
||||||
|
<el-row :gutter="16">
|
||||||
|
<el-col :span="12">
|
||||||
<el-form-item label="商品名称" prop="name">
|
<el-form-item label="商品名称" prop="name">
|
||||||
<el-input v-model="formData.name" placeholder="请输入商品名称" />
|
<el-input v-model="formData.name" placeholder="请输入商品名称" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="条码" prop="barcode">
|
</el-col>
|
||||||
<el-input v-model="formData.barcode" placeholder="请输入条码" />
|
<el-col :span="12">
|
||||||
|
<el-form-item label="品相" prop="appearance">
|
||||||
|
<el-select v-model="formData.appearance" placeholder="请选择品相" style="width: 100%" disabled>
|
||||||
|
<el-option label="八五品" :value="85" />
|
||||||
|
<el-option label="九品" :value="90" />
|
||||||
|
<el-option label="十品" :value="100" />
|
||||||
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="价格(分)" prop="price">
|
</el-col>
|
||||||
<el-input-number v-model="formData.price" :min="0" :step="100" controls-position="right"
|
</el-row>
|
||||||
style="width: 100%" />
|
|
||||||
|
<el-form-item label="实拍图" prop="live_image">
|
||||||
|
<div class="live-image-upload">
|
||||||
|
<div v-for="(img, idx) in formData.live_image" :key="idx" class="live-image-item">
|
||||||
|
<el-image :src="img" :preview-src-list="formData.live_image" :initial-index="idx" fit="cover"
|
||||||
|
class="live-image-thumb" preview-teleported />
|
||||||
|
<el-icon class="live-image-delete" @click="removeLiveImage(idx)">
|
||||||
|
<CircleClose />
|
||||||
|
</el-icon>
|
||||||
|
</div>
|
||||||
|
<el-upload v-if="formData.live_image.length < maxImageCount" :show-file-list="false"
|
||||||
|
:http-request="handleImageUpload" :before-upload="beforeImageUpload"
|
||||||
|
accept="image/jpeg,image/png,image/webp,image/gif" class="live-image-uploader">
|
||||||
|
<el-icon class="live-image-uploader-icon">
|
||||||
|
<Plus />
|
||||||
|
</el-icon>
|
||||||
|
</el-upload>
|
||||||
|
</div>
|
||||||
|
<span class="upload-tip"> 支持 jpg/png/webp/gif,最多 {{ maxImageCount }} 张</span>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="分类ID" prop="category_id">
|
|
||||||
<el-input-number v-model="formData.category_id" :min="0" controls-position="right" style="width: 100%" />
|
<el-row :gutter="16">
|
||||||
|
<el-col :span="24">
|
||||||
|
<el-form-item label="分类" prop="category_id">
|
||||||
|
<el-select v-model="formData.category_id" placeholder="请选择分类" filterable clearable style="width: 100%"
|
||||||
|
disabled>
|
||||||
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="标品ID" prop="standard_product_id">
|
</el-col>
|
||||||
<el-input-number v-model="formData.standard_product_id" :min="0" controls-position="right"
|
</el-row>
|
||||||
style="width: 100%" />
|
|
||||||
|
<el-row :gutter="16">
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="仓库" prop="warehouse_id">
|
||||||
|
<el-select v-model="formData.warehouse_id" placeholder="请选择仓库" filterable clearable style="width: 100%"
|
||||||
|
disabled>
|
||||||
|
<el-option v-for="wh in warehouseOptions" :key="wh.id" :label="wh.name" :value="wh.id" />
|
||||||
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="库位" prop="location_id">
|
||||||
|
<el-select v-model="formData.location_id" placeholder="请选择库位" filterable clearable style="width: 100%"
|
||||||
|
disabled>
|
||||||
|
<el-option v-for="loc in locationOptions" :key="loc.id" :label="loc.code" :value="loc.id" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
<el-row :gutter="16">
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="库存" prop="quantity">
|
||||||
|
<el-input-number v-model="formData.quantity" :min="0" :step="1" controls-position="right"
|
||||||
|
style="width: 100%" disabled />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="价格(元)" prop="price">
|
||||||
|
<el-input-number v-model="priceYuan" :min="0" :step="0.01" :precision="2" controls-position="right"
|
||||||
|
style="width: 100%" disabled />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
<el-row :gutter="16">
|
||||||
|
<el-col :span="8">
|
||||||
<el-form-item label="批次管理" prop="is_batch_managed">
|
<el-form-item label="批次管理" prop="is_batch_managed">
|
||||||
<el-switch v-model="formData.is_batch_managed" :active-value="1" :inactive-value="0" active-text="是"
|
<el-switch v-model="formData.is_batch_managed" :active-value="1" :inactive-value="0" active-text="是"
|
||||||
inactive-text="否" />
|
inactive-text="否" disabled />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="8">
|
||||||
<el-form-item label="效期管理" prop="is_shelf_life_managed">
|
<el-form-item label="效期管理" prop="is_shelf_life_managed">
|
||||||
<el-switch v-model="formData.is_shelf_life_managed" :active-value="1" :inactive-value="0" active-text="是"
|
<el-switch v-model="formData.is_shelf_life_managed" :active-value="1" :inactive-value="0" active-text="是"
|
||||||
inactive-text="否" />
|
inactive-text="否" disabled />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="8">
|
||||||
<el-form-item label="状态" prop="status">
|
<el-form-item label="状态" prop="status">
|
||||||
<el-switch v-model="formData.status" :active-value="1" :inactive-value="0" active-text="启用" inactive-text="禁用"
|
<el-switch v-model="formData.status" :active-value="1" :inactive-value="0" active-text="启用"
|
||||||
active-color="#67C23A" />
|
inactive-text="禁用" active-color="#67C23A" disabled />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
</el-form>
|
</el-form>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<span class="dialog-footer">
|
<span class="dialog-footer">
|
||||||
@ -60,11 +128,28 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, ref, reactive } from 'vue'
|
import { defineComponent, ref, reactive, computed } from 'vue'
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
import { Plus, CircleClose } from '@element-plus/icons-vue'
|
||||||
import ProductList from '@/components/product/byGoods/index.vue'
|
import ProductList from '@/components/product/byGoods/index.vue'
|
||||||
import ProductByLocation from '@/components/product/byLocation/index.vue'
|
import ProductByLocation from '@/components/product/byLocation/index.vue'
|
||||||
import { fetchProductDetail, createProduct, updateProduct, deleteProduct } from '@/api/product'
|
import { fetchProductDetail, createProduct, updateProduct, deleteProduct, updateNameAndImages } from '@/api/product'
|
||||||
|
import { uploadLivingPicture } from '@/utils/uploadLivingPicture'
|
||||||
|
import { fetchWarehouseList } from '@/api/warehouse'
|
||||||
|
import { fetchLocationList } from '@/api/location'
|
||||||
|
|
||||||
|
interface ShopItem {
|
||||||
|
shop_alias_name: string
|
||||||
|
shop_type: number
|
||||||
|
shop_type_name: string
|
||||||
|
is_sent: boolean
|
||||||
|
out_task_log_id: number
|
||||||
|
out_task_id: number
|
||||||
|
status: number
|
||||||
|
msg: string
|
||||||
|
reset_num: number
|
||||||
|
created_at: number
|
||||||
|
}
|
||||||
|
|
||||||
interface ProductItem {
|
interface ProductItem {
|
||||||
id: number
|
id: number
|
||||||
@ -85,20 +170,10 @@ interface ProductItem {
|
|||||||
warehouse_name: string
|
warehouse_name: string
|
||||||
location_id: number
|
location_id: number
|
||||||
location_code: string
|
location_code: string
|
||||||
|
appearance: number
|
||||||
created_at: number
|
created_at: number
|
||||||
updated_at: number
|
updated_at: number
|
||||||
shop_list?: Array<{
|
shop_list?: ShopItem[]
|
||||||
shop_alias_name: string
|
|
||||||
shop_type: number
|
|
||||||
shop_type_name: string
|
|
||||||
is_sent: boolean
|
|
||||||
out_task_log_id: number
|
|
||||||
out_task_id: number
|
|
||||||
status: number
|
|
||||||
msg: string
|
|
||||||
reset_num: number
|
|
||||||
created_at: number
|
|
||||||
}>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ProductFormData {
|
interface ProductFormData {
|
||||||
@ -108,11 +183,25 @@ interface ProductFormData {
|
|||||||
name: string
|
name: string
|
||||||
barcode: string
|
barcode: string
|
||||||
price: number
|
price: number
|
||||||
live_image: any
|
live_image: string[]
|
||||||
is_batch_managed: number
|
is_batch_managed: number
|
||||||
is_shelf_life_managed: number
|
is_shelf_life_managed: number
|
||||||
status: number
|
status: number
|
||||||
appearance: number
|
appearance: number
|
||||||
|
quantity: number
|
||||||
|
warehouse_id: number | null
|
||||||
|
location_id: number | null
|
||||||
|
shop_list?: ShopItem[]
|
||||||
|
}
|
||||||
|
|
||||||
|
interface WarehouseOption {
|
||||||
|
id: number
|
||||||
|
name: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface LocationOption {
|
||||||
|
id: number
|
||||||
|
code: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
@ -125,6 +214,8 @@ export default defineComponent({
|
|||||||
const activeTab = ref<string>('byGoods')
|
const activeTab = ref<string>('byGoods')
|
||||||
const productListRef = ref<InstanceType<typeof ProductList> | null>(null)
|
const productListRef = ref<InstanceType<typeof ProductList> | null>(null)
|
||||||
const submitLoading = ref<boolean>(false)
|
const submitLoading = ref<boolean>(false)
|
||||||
|
const imageUploading = ref<boolean>(false)
|
||||||
|
const maxImageCount = 10
|
||||||
|
|
||||||
const dialogVisible = ref<boolean>(false)
|
const dialogVisible = ref<boolean>(false)
|
||||||
const dialogTitle = ref<string>('')
|
const dialogTitle = ref<string>('')
|
||||||
@ -140,67 +231,157 @@ export default defineComponent({
|
|||||||
is_batch_managed: 0,
|
is_batch_managed: 0,
|
||||||
is_shelf_life_managed: 0,
|
is_shelf_life_managed: 0,
|
||||||
status: 1,
|
status: 1,
|
||||||
appearance: 85
|
appearance: 85,
|
||||||
|
quantity: 0,
|
||||||
|
warehouse_id: null,
|
||||||
|
location_id: null,
|
||||||
|
shop_list: []
|
||||||
})
|
})
|
||||||
|
|
||||||
const formRules = {
|
const formRules = {
|
||||||
name: [
|
name: [
|
||||||
{ required: true, message: '商品名称不能为空', trigger: 'blur' },
|
{ required: true, message: '商品名称不能为空', trigger: 'blur' },
|
||||||
{ min: 1, max: 255, message: '长度在1到255个字符', trigger: 'blur' }
|
{ min: 1, max: 255, message: '长度在1到255个字符', trigger: 'blur' }
|
||||||
],
|
|
||||||
barcode: [
|
|
||||||
{ max: 100, message: '条码长度不能超过100个字符', trigger: 'blur' }
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 价格(元) 双向转换 —— 表单显示元,内部存储分
|
||||||
|
const priceYuan = computed({
|
||||||
|
get: (): number => {
|
||||||
|
if (formData.price == null || formData.price === 0) return 0
|
||||||
|
return Number((formData.price / 100).toFixed(2))
|
||||||
|
},
|
||||||
|
set: (val: number) => {
|
||||||
|
formData.price = Math.round((val || 0) * 100)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 仓库选项
|
||||||
|
const warehouseOptions = ref<WarehouseOption[]>([])
|
||||||
|
const loadWarehouses = async (): Promise<void> => {
|
||||||
|
try {
|
||||||
|
const res = await fetchWarehouseList({ page: 1, pageSize: 9999 })
|
||||||
|
warehouseOptions.value = res.list || []
|
||||||
|
} catch {
|
||||||
|
warehouseOptions.value = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 库位选项(根据仓库筛选)
|
||||||
|
const locationOptions = ref<LocationOption[]>([])
|
||||||
|
const loadLocations = async (warehouseId: number): Promise<void> => {
|
||||||
|
if (!warehouseId) {
|
||||||
|
locationOptions.value = []
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const res = await fetchLocationList({ warehouseId, page: 1, pageSize: 9999 })
|
||||||
|
locationOptions.value = res.list || []
|
||||||
|
} catch {
|
||||||
|
locationOptions.value = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onWarehouseChange = (warehouseId: number | null): void => {
|
||||||
|
formData.location_id = null
|
||||||
|
if (warehouseId) {
|
||||||
|
loadLocations(warehouseId)
|
||||||
|
} else {
|
||||||
|
locationOptions.value = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 实拍图操作
|
||||||
|
const removeLiveImage = (idx: number): void => {
|
||||||
|
formData.live_image.splice(idx, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
const beforeImageUpload = (file: File): boolean => {
|
||||||
|
const isImage = ['image/jpeg', 'image/png', 'image/webp', 'image/gif'].includes(file.type)
|
||||||
|
if (!isImage) {
|
||||||
|
ElMessage.error({ message: '仅支持 JPG/PNG/WebP/GIF 格式', customClass: 'scan-error-message' })
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
const isLt10M = file.size / 1024 / 1024 < 10
|
||||||
|
if (!isLt10M) {
|
||||||
|
ElMessage.error({ message: '图片大小不能超过 10MB', customClass: 'scan-error-message' })
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleImageUpload = async (option: any): Promise<void> => {
|
||||||
|
imageUploading.value = true
|
||||||
|
try {
|
||||||
|
const pictureName = `${Date.now()}-${Math.random().toString(36).substring(2, 8)}.jpg`
|
||||||
|
const url = await uploadLivingPicture(option.file, pictureName)
|
||||||
|
formData.live_image.push(url)
|
||||||
|
} catch {
|
||||||
|
ElMessage.error({ message: '图片上传失败', customClass: 'scan-error-message' })
|
||||||
|
} finally {
|
||||||
|
imageUploading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const refreshList = (): void => {
|
const refreshList = (): void => {
|
||||||
productListRef.value?.refreshList?.()
|
productListRef.value?.refreshList?.()
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleEdit = async (row: ProductItem): Promise<void> => {
|
const handleEdit = (row: ProductItem): void => {
|
||||||
dialogTitle.value = '编辑商品'
|
dialogTitle.value = '编辑商品'
|
||||||
try {
|
// 预加载下拉选项
|
||||||
const detail = await fetchProductDetail(row.id)
|
loadWarehouses()
|
||||||
if (detail) {
|
|
||||||
formData.id = detail.id ?? row.id
|
// 直接从列表 row 数据填充表单,不再调用详情接口
|
||||||
formData.category_id = detail.category_id ?? row.category_id
|
|
||||||
formData.standard_product_id = detail.standard_product_id ?? row.standard_product_id
|
|
||||||
formData.name = detail.name || row.name
|
|
||||||
formData.barcode = detail.barcode || row.barcode
|
|
||||||
formData.price = detail.price ?? row.price
|
|
||||||
formData.live_image = detail.live_image || row.live_image || []
|
|
||||||
formData.is_batch_managed = detail.is_batch_managed ?? row.is_batch_managed
|
|
||||||
formData.is_shelf_life_managed = detail.is_shelf_life_managed ?? row.is_shelf_life_managed
|
|
||||||
formData.status = detail.status ?? row.status
|
|
||||||
} else {
|
|
||||||
formData.id = row.id
|
formData.id = row.id
|
||||||
formData.category_id = row.category_id
|
formData.category_id = row.category_id
|
||||||
formData.standard_product_id = row.standard_product_id
|
formData.standard_product_id = row.standard_product_id
|
||||||
formData.name = row.name
|
formData.name = row.name
|
||||||
formData.barcode = row.barcode
|
formData.barcode = row.barcode
|
||||||
formData.price = row.price
|
formData.price = row.price
|
||||||
formData.live_image = row.live_image || []
|
formData.live_image = normalizeImageArray(row.live_image)
|
||||||
formData.is_batch_managed = row.is_batch_managed
|
|
||||||
formData.is_shelf_life_managed = row.is_shelf_life_managed
|
|
||||||
formData.status = row.status
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.warn('获取详情失败,使用当前行数据', error)
|
|
||||||
formData.id = row.id
|
|
||||||
formData.category_id = row.category_id
|
|
||||||
formData.standard_product_id = row.standard_product_id
|
|
||||||
formData.name = row.name
|
|
||||||
formData.barcode = row.barcode
|
|
||||||
formData.price = row.price
|
|
||||||
formData.live_image = row.live_image || []
|
|
||||||
formData.is_batch_managed = row.is_batch_managed
|
formData.is_batch_managed = row.is_batch_managed
|
||||||
formData.is_shelf_life_managed = row.is_shelf_life_managed
|
formData.is_shelf_life_managed = row.is_shelf_life_managed
|
||||||
formData.status = row.status
|
formData.status = row.status
|
||||||
|
formData.appearance = row.appearance ?? 85
|
||||||
|
formData.quantity = row.quantity ?? 0
|
||||||
|
formData.warehouse_id = row.warehouse_id ?? null
|
||||||
|
formData.location_id = row.location_id ?? null
|
||||||
|
formData.shop_list = row.shop_list || []
|
||||||
|
|
||||||
|
if (formData.warehouse_id) {
|
||||||
|
loadLocations(formData.warehouse_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
dialogVisible.value = true
|
dialogVisible.value = true
|
||||||
setTimeout(() => { formRef.value?.clearValidate() }, 0)
|
setTimeout(() => { formRef.value?.clearValidate() }, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 兼容 live_image 多种格式:字符串(含逗号分隔多URL)/ 数组 / 数组内含逗号分隔字符串 / null */
|
||||||
|
const normalizeImageArray = (val: any): string[] => {
|
||||||
|
if (!val) return []
|
||||||
|
if (Array.isArray(val)) {
|
||||||
|
const result: string[] = []
|
||||||
|
for (const item of val) {
|
||||||
|
if (typeof item === 'string' && item.length > 0) {
|
||||||
|
if (item.includes(',')) {
|
||||||
|
result.push(...item.split(',').map((u: string) => u.trim()).filter(Boolean))
|
||||||
|
} else {
|
||||||
|
result.push(item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
if (typeof val === 'string' && val.length > 0) {
|
||||||
|
if (val.includes(',')) {
|
||||||
|
return val.split(',').map((u: string) => u.trim()).filter(Boolean)
|
||||||
|
}
|
||||||
|
return [val]
|
||||||
|
}
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
const handleDelete = (row: ProductItem): void => {
|
const handleDelete = (row: ProductItem): void => {
|
||||||
ElMessageBox.confirm(`确定要删除商品 "${row.name}" 吗?`, '删除确认', {
|
ElMessageBox.confirm(`确定要删除商品 "${row.name}" 吗?`, '删除确认', {
|
||||||
confirmButtonText: '确定删除',
|
confirmButtonText: '确定删除',
|
||||||
@ -221,29 +402,22 @@ export default defineComponent({
|
|||||||
try {
|
try {
|
||||||
await formRef.value?.validate()
|
await formRef.value?.validate()
|
||||||
submitLoading.value = true
|
submitLoading.value = true
|
||||||
const payload = {
|
const payload: Record<string, any> = {
|
||||||
category_id: formData.category_id,
|
|
||||||
standard_product_id: formData.standard_product_id,
|
|
||||||
name: formData.name,
|
name: formData.name,
|
||||||
barcode: formData.barcode,
|
liveimage: formData.live_image || [],
|
||||||
price: formData.price,
|
product_id: formData.id
|
||||||
live_image: formData.live_image || [],
|
|
||||||
is_batch_managed: formData.is_batch_managed,
|
|
||||||
is_shelf_life_managed: formData.is_shelf_life_managed,
|
|
||||||
status: formData.status,
|
|
||||||
appearance: formData.appearance ?? 85
|
|
||||||
}
|
}
|
||||||
if (formData.id === null) {
|
if (formData.id === null) {
|
||||||
await createProduct(payload)
|
await createProduct(payload)
|
||||||
ElMessage.success({ message: '新增商品成功', customClass: 'scan-success-message' })
|
ElMessage.success({ message: '新增商品成功', customClass: 'scan-success-message' })
|
||||||
} else {
|
} else {
|
||||||
await updateProduct({ id: formData.id, ...payload })
|
await updateNameAndImages({ id: formData.id, name: formData.name, liveimage: formData.live_image, product_id: formData.id })
|
||||||
ElMessage.success({ message: '编辑商品成功', customClass: 'scan-success-message' })
|
ElMessage.success({ message: '编辑商品成功', customClass: 'scan-success-message' })
|
||||||
}
|
}
|
||||||
dialogVisible.value = false
|
dialogVisible.value = false
|
||||||
refreshList()
|
refreshList()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// ElMessage.error('操作失败,请检查输入或稍后重试')
|
// 表单校验失败时静默
|
||||||
} finally {
|
} finally {
|
||||||
submitLoading.value = false
|
submitLoading.value = false
|
||||||
}
|
}
|
||||||
@ -261,14 +435,30 @@ export default defineComponent({
|
|||||||
formData.is_batch_managed = 0
|
formData.is_batch_managed = 0
|
||||||
formData.is_shelf_life_managed = 0
|
formData.is_shelf_life_managed = 0
|
||||||
formData.status = 1
|
formData.status = 1
|
||||||
|
formData.appearance = 85
|
||||||
|
formData.quantity = 0
|
||||||
|
formData.warehouse_id = null
|
||||||
|
formData.location_id = null
|
||||||
|
formData.shop_list = []
|
||||||
|
locationOptions.value = []
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
activeTab,
|
activeTab,
|
||||||
productListRef,
|
productListRef,
|
||||||
submitLoading,
|
submitLoading,
|
||||||
|
imageUploading,
|
||||||
|
maxImageCount,
|
||||||
dialogVisible, dialogTitle, formRef, formData, formRules,
|
dialogVisible, dialogTitle, formRef, formData, formRules,
|
||||||
handleEdit, handleDelete, submitForm, resetForm
|
priceYuan,
|
||||||
|
warehouseOptions,
|
||||||
|
locationOptions,
|
||||||
|
onWarehouseChange,
|
||||||
|
removeLiveImage,
|
||||||
|
beforeImageUpload,
|
||||||
|
handleImageUpload,
|
||||||
|
handleEdit, handleDelete, submitForm, resetForm,
|
||||||
|
Plus, CircleClose
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -287,6 +477,9 @@ export default defineComponent({
|
|||||||
|
|
||||||
:deep(.el-dialog__body) {
|
:deep(.el-dialog__body) {
|
||||||
padding-top: 16px;
|
padding-top: 16px;
|
||||||
|
max-height: 70vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dialog-footer {
|
.dialog-footer {
|
||||||
@ -294,4 +487,67 @@ export default defineComponent({
|
|||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 实拍图上传 */
|
||||||
|
.live-image-upload {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.live-image-item {
|
||||||
|
position: relative;
|
||||||
|
width: 80px;
|
||||||
|
height: 80px;
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 1px solid #d9d9d9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.live-image-thumb {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 6px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.live-image-delete {
|
||||||
|
position: absolute;
|
||||||
|
top: -6px;
|
||||||
|
right: -6px;
|
||||||
|
font-size: 18px;
|
||||||
|
color: #f56c6c;
|
||||||
|
cursor: pointer;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 50%;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.live-image-uploader {
|
||||||
|
width: 80px;
|
||||||
|
height: 80px;
|
||||||
|
border: 1px dashed #d9d9d9;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: border-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.live-image-uploader:hover {
|
||||||
|
border-color: #409eff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.live-image-uploader-icon {
|
||||||
|
font-size: 28px;
|
||||||
|
color: #8c939d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-tip {
|
||||||
|
display: block;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #909399;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -20,6 +20,7 @@
|
|||||||
</el-select>
|
</el-select>
|
||||||
<el-button type="primary" :icon="Search" @click="handleSearch">搜索</el-button>
|
<el-button type="primary" :icon="Search" @click="handleSearch">搜索</el-button>
|
||||||
<el-button :icon="Refresh" @click="resetSearch">重置</el-button>
|
<el-button :icon="Refresh" @click="resetSearch">重置</el-button>
|
||||||
|
<el-button type="primary" @click="handleExport">导出</el-button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<el-table :data="tableData" v-loading="loading" border stripe style="width: 100%" @expand-change="handleExpandChange">
|
<el-table :data="tableData" v-loading="loading" border stripe style="width: 100%" @expand-change="handleExpandChange">
|
||||||
@ -130,7 +131,7 @@ import { defineComponent, ref, reactive, onMounted } from 'vue'
|
|||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
import { Search, Refresh, Plus, Edit, Delete, View, Loading } from '@element-plus/icons-vue'
|
import { Search, Refresh, Plus, Edit, Delete, View, Loading } from '@element-plus/icons-vue'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import { fetchPurchaseOrderList, fetchPurchaseOrderDetail, createPurchaseOrder, updatePurchaseOrder, deletePurchaseOrder } from '@/api/purchaseOrder'
|
import { fetchPurchaseOrderList, fetchPurchaseOrderDetail, createPurchaseOrder, updatePurchaseOrder, deletePurchaseOrder, exportPurchaseOrderToWdt } from '@/api/purchaseOrder'
|
||||||
import { fetchSupplierList } from '@/api/supplier'
|
import { fetchSupplierList } from '@/api/supplier'
|
||||||
import { fetchWarehouseList } from '@/api/warehouse'
|
import { fetchWarehouseList } from '@/api/warehouse'
|
||||||
|
|
||||||
@ -334,6 +335,36 @@ export default defineComponent({
|
|||||||
}).catch(() => {})
|
}).catch(() => {})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleExport = async (): Promise<void> => {
|
||||||
|
try {
|
||||||
|
const res = await exportPurchaseOrderToWdt({
|
||||||
|
keyword: searchParams.keyword,
|
||||||
|
status: searchParams.status !== null ? String(searchParams.status) : undefined
|
||||||
|
})
|
||||||
|
// 从 Content-Disposition 头解析文件名,或使用默认名
|
||||||
|
const disposition = res.headers?.['content-disposition']
|
||||||
|
let fileName = '采购单导出.xlsx'
|
||||||
|
if (disposition) {
|
||||||
|
const match = disposition.match(/filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/)
|
||||||
|
if (match && match[1]) {
|
||||||
|
fileName = decodeURIComponent(match[1].replace(/['"]/g, ''))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 创建 Blob 并触发下载
|
||||||
|
const url = window.URL.createObjectURL(new Blob([res.data]))
|
||||||
|
const link = document.createElement('a')
|
||||||
|
link.href = url
|
||||||
|
link.download = fileName
|
||||||
|
document.body.appendChild(link)
|
||||||
|
link.click()
|
||||||
|
document.body.removeChild(link)
|
||||||
|
window.URL.revokeObjectURL(url)
|
||||||
|
ElMessage.success({ message: '导出成功', customClass: 'scan-success-message' })
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error({ message: '导出失败', customClass: 'scan-error-message' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const resetForm = (): void => {
|
const resetForm = (): void => {
|
||||||
formRef.value?.resetFields()
|
formRef.value?.resetFields()
|
||||||
formData.id = null
|
formData.id = null
|
||||||
@ -363,7 +394,7 @@ export default defineComponent({
|
|||||||
statusLabel, statusTagType,
|
statusLabel, statusTagType,
|
||||||
formatTimestamp, formatAmount,
|
formatTimestamp, formatAmount,
|
||||||
handleSearch, resetSearch, handleCurrentChange, handleSizeChange,
|
handleSearch, resetSearch, handleCurrentChange, handleSizeChange,
|
||||||
handleExpandChange, handleView, handleDelete, resetForm
|
handleExpandChange, handleView, handleDelete, handleExport, resetForm
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@ -55,14 +55,48 @@
|
|||||||
{{ row.name || '-' }}
|
{{ row.name || '-' }}
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="实拍图" width="80" align="center">
|
<el-table-column label="实拍图" width="100" align="center">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-image v-if="getFirstImage(row.live_image)" :src="getFirstImage(row.live_image)"
|
<template v-if="getImageList(row.live_image).length > 0">
|
||||||
|
<el-popover
|
||||||
|
placement="right"
|
||||||
|
:width="360"
|
||||||
|
trigger="hover"
|
||||||
|
:open-delay="300"
|
||||||
|
:close-delay="100"
|
||||||
|
:disabled="false"
|
||||||
|
:teleported="true"
|
||||||
|
>
|
||||||
|
<template #reference>
|
||||||
|
<el-image
|
||||||
|
:src="getImageList(row.live_image)[0]"
|
||||||
:preview-src-list="getImageList(row.live_image)"
|
:preview-src-list="getImageList(row.live_image)"
|
||||||
style="width: 40px; height: 40px; border-radius: 4px" fit="cover"
|
:initial-index="0"
|
||||||
preview-teleported />
|
class="image-thumb-single"
|
||||||
<span v-else
|
fit="cover"
|
||||||
style="width: 40px; height: 40px; display: inline-flex; align-items: center; justify-content: center; color: #c0c4cc; font-size: 12px">暂无</span>
|
preview-teleported
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<div class="popover-image-gallery">
|
||||||
|
<div class="popover-gallery-title">
|
||||||
|
共{{ getImageList(row.live_image).length }}张图片
|
||||||
|
</div>
|
||||||
|
<div class="popover-gallery-list">
|
||||||
|
<el-image
|
||||||
|
v-for="(img, idx) in getImageList(row.live_image)"
|
||||||
|
:key="idx"
|
||||||
|
:src="img"
|
||||||
|
:preview-src-list="getImageList(row.live_image)"
|
||||||
|
:initial-index="idx"
|
||||||
|
class="popover-gallery-item"
|
||||||
|
fit="cover"
|
||||||
|
preview-teleported
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-popover>
|
||||||
|
</template>
|
||||||
|
<span v-else style="color: #c0c4cc">暂无</span>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="ISBN" min-width="130" show-overflow-tooltip align="center">
|
<el-table-column label="ISBN" min-width="130" show-overflow-tooltip align="center">
|
||||||
@ -115,10 +149,12 @@
|
|||||||
{{ row.msg || '-' }}
|
{{ row.msg || '-' }}
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="操作" width="80" align="center">
|
<el-table-column label="操作" width="160" align="center">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-button type="primary" link size="small"
|
<el-button type="primary" link size="small"
|
||||||
@click="handleRetry(activeShopId, row)">重试</el-button>
|
@click="handleRetry(activeShopId, row)">重试</el-button>
|
||||||
|
<el-button type="danger" link size="small"
|
||||||
|
>删除</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
@ -224,20 +260,48 @@ export default defineComponent({
|
|||||||
return dayjs.unix(Number(timestamp)).format('YYYY-MM-DD HH:mm:ss')
|
return dayjs.unix(Number(timestamp)).format('YYYY-MM-DD HH:mm:ss')
|
||||||
}
|
}
|
||||||
|
|
||||||
const getFirstImage = (liveImage: any): string => {
|
|
||||||
if (!liveImage) return ''
|
|
||||||
if (Array.isArray(liveImage) && liveImage.length > 0) return liveImage[0]
|
|
||||||
if (typeof liveImage === 'string') return liveImage
|
|
||||||
return ''
|
|
||||||
}
|
|
||||||
|
|
||||||
const getImageList = (liveImage: any): string[] => {
|
const getImageList = (liveImage: any): string[] => {
|
||||||
if (!liveImage) return []
|
if (!liveImage) return []
|
||||||
if (Array.isArray(liveImage)) return liveImage.filter((url: string) => typeof url === 'string')
|
if (Array.isArray(liveImage)) {
|
||||||
if (typeof liveImage === 'string') return [liveImage]
|
const result: string[] = []
|
||||||
|
for (const item of liveImage) {
|
||||||
|
if (typeof item === 'string') {
|
||||||
|
if (item.includes(',')) {
|
||||||
|
result.push(...item.split(',').map((u: string) => u.trim()).filter(Boolean))
|
||||||
|
} else {
|
||||||
|
result.push(item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
if (typeof liveImage === 'string') {
|
||||||
|
if (liveImage.includes(',')) {
|
||||||
|
return liveImage.split(',').map((u: string) => u.trim()).filter(Boolean)
|
||||||
|
}
|
||||||
|
return [liveImage]
|
||||||
|
}
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getFirstImage = (liveImage: any): string => {
|
||||||
|
if (!liveImage) return ''
|
||||||
|
if (Array.isArray(liveImage) && liveImage.length > 0) {
|
||||||
|
const first = String(liveImage[0])
|
||||||
|
if (first.includes(',')) {
|
||||||
|
return first.split(',')[0].trim()
|
||||||
|
}
|
||||||
|
return first
|
||||||
|
}
|
||||||
|
if (typeof liveImage === 'string') {
|
||||||
|
if (liveImage.includes(',')) {
|
||||||
|
return liveImage.split(',')[0].trim()
|
||||||
|
}
|
||||||
|
return liveImage
|
||||||
|
}
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
const loadList = async (): Promise<void> => {
|
const loadList = async (): Promise<void> => {
|
||||||
if (!searchParams.shop_type) {
|
if (!searchParams.shop_type) {
|
||||||
tableData.value = []
|
tableData.value = []
|
||||||
@ -482,4 +546,43 @@ export default defineComponent({
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
/* 垂直居中 */
|
/* 垂直居中 */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 实拍图 - 单张缩略图 */
|
||||||
|
.image-thumb-single {
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
border: 1px solid #ebeef5;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 悬浮画廊 */
|
||||||
|
.popover-image-gallery {
|
||||||
|
padding: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popover-gallery-title {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #303133;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
border-bottom: 1px solid #ebeef5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popover-gallery-list {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popover-gallery-item {
|
||||||
|
width: 64px;
|
||||||
|
height: 64px;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
border: 1px solid #ebeef5;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -37,6 +37,33 @@
|
|||||||
<el-button :icon="Refresh" @click="resetSearch">重置</el-button>
|
<el-button :icon="Refresh" @click="resetSearch">重置</el-button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="stats-row">
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-label">今日入库波次数</div>
|
||||||
|
<div class="stat-value">{{ stats.today_inbound_waves }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-label">今日入库数量</div>
|
||||||
|
<div class="stat-value">{{ stats.today_inbound_quantity }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-label">今日出库数量</div>
|
||||||
|
<div class="stat-value">{{ stats.today_outbound_quantity }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-label">昨日入库波次数</div>
|
||||||
|
<div class="stat-value">{{ stats.yesterday_inbound_waves }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-label">昨日入库数量</div>
|
||||||
|
<div class="stat-value">{{ stats.yesterday_inbound_quantity }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-label">昨日出库数量</div>
|
||||||
|
<div class="stat-value">{{ stats.yesterday_outbound_quantity }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<el-table
|
<el-table
|
||||||
:data="filteredTableData"
|
:data="filteredTableData"
|
||||||
v-loading="loading"
|
v-loading="loading"
|
||||||
@ -152,7 +179,7 @@ import { useRoute } from 'vue-router'
|
|||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
import { Search, Refresh, Plus, Edit, Delete, View } from '@element-plus/icons-vue'
|
import { Search, Refresh, Plus, Edit, Delete, View } from '@element-plus/icons-vue'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import { fetchWaveTaskList, fetchWaveTaskDetail, deleteWaveTask } from '@/api/waveTask'
|
import { fetchWaveTaskList, fetchWaveTaskDetail, deleteWaveTask, } from '@/api/waveTask'
|
||||||
|
|
||||||
/** 任务类型映射 */
|
/** 任务类型映射 */
|
||||||
const TYPE_MAP: Record<number, { label: string; type: string }> = {
|
const TYPE_MAP: Record<number, { label: string; type: string }> = {
|
||||||
@ -216,6 +243,15 @@ export default defineComponent({
|
|||||||
total: 0
|
total: 0
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const stats = reactive({
|
||||||
|
today_inbound_waves: 0,
|
||||||
|
today_inbound_quantity: 0,
|
||||||
|
today_outbound_quantity: 0,
|
||||||
|
yesterday_inbound_waves: 0,
|
||||||
|
yesterday_inbound_quantity: 0,
|
||||||
|
yesterday_outbound_quantity: 0
|
||||||
|
})
|
||||||
|
|
||||||
const dialogVisible = ref<boolean>(false)
|
const dialogVisible = ref<boolean>(false)
|
||||||
const dialogTitle = ref<string>('')
|
const dialogTitle = ref<string>('')
|
||||||
const formRef = ref<any>(null)
|
const formRef = ref<any>(null)
|
||||||
@ -268,6 +304,17 @@ export default defineComponent({
|
|||||||
return list
|
return list
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const loadStats = async (): Promise<void> => {
|
||||||
|
try {
|
||||||
|
const data = await fetchWaveTaskStats()
|
||||||
|
if (data) {
|
||||||
|
Object.assign(stats, data)
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// 统计加载失败不阻塞主流程
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const loadList = async (): Promise<void> => {
|
const loadList = async (): Promise<void> => {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
expandedRowKeys.value = []
|
expandedRowKeys.value = []
|
||||||
@ -418,11 +465,12 @@ export default defineComponent({
|
|||||||
searchParams.wave_no = route.query.wave_no as string
|
searchParams.wave_no = route.query.wave_no as string
|
||||||
}
|
}
|
||||||
void loadList()
|
void loadList()
|
||||||
|
void loadStats()
|
||||||
})
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
loading, submitLoading, tableData, filteredTableData, expandedRowKeys,
|
loading, submitLoading, tableData, filteredTableData, expandedRowKeys,
|
||||||
typeOptions, statusOptions, searchParams, pagination,
|
typeOptions, statusOptions, searchParams, pagination, stats,
|
||||||
dialogVisible, dialogTitle, formRef, formData, formRules,
|
dialogVisible, dialogTitle, formRef, formData, formRules,
|
||||||
detailVisible, detailData,
|
detailVisible, detailData,
|
||||||
Search, Refresh, Plus, Edit, Delete, View,
|
Search, Refresh, Plus, Edit, Delete, View,
|
||||||
@ -459,6 +507,35 @@ export default defineComponent({
|
|||||||
box-shadow: 0 1px 4px rgba(0,0,0,0.05);
|
box-shadow: 0 1px 4px rgba(0,0,0,0.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.stats-row {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 16px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 160px;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 20px 24px;
|
||||||
|
box-shadow: 0 1px 4px rgba(0,0,0,0.05);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-label {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #909399;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-value {
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
|
||||||
.pagination-wrapper {
|
.pagination-wrapper {
|
||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@ -134,7 +134,7 @@ module.exports = defineConfig({
|
|||||||
try {
|
try {
|
||||||
const userJson = JSON.parse(userInfoResponse.body)
|
const userJson = JSON.parse(userInfoResponse.body)
|
||||||
if (userJson.status && userJson.data) displayName = userJson.data.nickname || username
|
if (userJson.status && userJson.data) displayName = userJson.data.nickname || username
|
||||||
} catch {}
|
} catch { }
|
||||||
|
|
||||||
res.setHeader('Content-Type', 'application/json')
|
res.setHeader('Content-Type', 'application/json')
|
||||||
res.end(JSON.stringify({
|
res.end(JSON.stringify({
|
||||||
@ -177,6 +177,7 @@ module.exports = defineConfig({
|
|||||||
// target: 'http://127.0.0.1:9090',
|
// target: 'http://127.0.0.1:9090',
|
||||||
// target: 'http://192.168.101.213:9090',
|
// target: 'http://192.168.101.213:9090',
|
||||||
target: 'https://psi.api.buzhiyushu.cn',
|
target: 'https://psi.api.buzhiyushu.cn',
|
||||||
|
// target: 'https://psi.api.buzhiyushu.cn',
|
||||||
changeOrigin: true
|
changeOrigin: true
|
||||||
},
|
},
|
||||||
'/api/print': {
|
'/api/print': {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user