Commit 07c79315 by wuqian

商品注册主流程初版

parent 1f0e8ec6
......@@ -15,6 +15,7 @@ declare module 'vue' {
ElCard: typeof import('element-plus/es')['ElCard']
ElCarousel: typeof import('element-plus/es')['ElCarousel']
ElCarouselItem: typeof import('element-plus/es')['ElCarouselItem']
ElCascader: typeof import('element-plus/es')['ElCascader']
ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
ElCheckboxGroup: typeof import('element-plus/es')['ElCheckboxGroup']
ElCol: typeof import('element-plus/es')['ElCol']
......@@ -32,6 +33,7 @@ declare module 'vue' {
ElFormItem: typeof import('element-plus/es')['ElFormItem']
ElIcon: typeof import('element-plus/es')['ElIcon']
ElImage: typeof import('element-plus/es')['ElImage']
ElImageViewer: typeof import('element-plus/es')['ElImageViewer']
ElInput: typeof import('element-plus/es')['ElInput']
ElInputNumber: typeof import('element-plus/es')['ElInputNumber']
ElLink: typeof import('element-plus/es')['ElLink']
......@@ -49,12 +51,12 @@ declare module 'vue' {
ElSwitch: typeof import('element-plus/es')['ElSwitch']
ElTable: typeof import('element-plus/es')['ElTable']
ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
ElTableV2: typeof import('element-plus/es')['ElTableV2']
ElTabPane: typeof import('element-plus/es')['ElTabPane']
ElTabs: typeof import('element-plus/es')['ElTabs']
ElTag: typeof import('element-plus/es')['ElTag']
ElTimeline: typeof import('element-plus/es')['ElTimeline']
ElTimelineItem: typeof import('element-plus/es')['ElTimelineItem']
ElTimePicker: typeof import('element-plus/es')['ElTimePicker']
ElTooltip: typeof import('element-plus/es')['ElTooltip']
ElTree: typeof import('element-plus/es')['ElTree']
ElUpload: typeof import('element-plus/es')['ElUpload']
......@@ -70,6 +72,7 @@ declare module 'vue' {
Select: typeof import('./src/components/Form/Select.vue')['default']
ShipmentOrderDetail: typeof import('./src/components/ShipmentOrderDetail.vue')['default']
SideBar: typeof import('./src/components/SideBar.vue')['default']
SkuAttibute: typeof import('./src/components/skuAttibute.vue')['default']
SplitDiv: typeof import('./src/components/splitDiv/splitDiv.vue')['default']
'Switch ': typeof import('./src/components/Form/Switch .vue')['default']
TableView: typeof import('./src/components/TableView.vue')['default']
......
......@@ -27,6 +27,7 @@
"vue-dompurify-html": "^5.1.0",
"vue-router": "^4.3.0",
"vue-tsc": "^2.1.10",
"vuedraggable": "^4.1.0",
"vxe-table": "^4.13.31",
"xlsx": "^0.18.5"
},
......@@ -5840,6 +5841,12 @@
"node": ">=12.17.0"
}
},
"node_modules/sortablejs": {
"version": "1.14.0",
"resolved": "https://registry.npmmirror.com/sortablejs/-/sortablejs-1.14.0.tgz",
"integrity": "sha512-pBXvQCs5/33fdN1/39pPL0NZF20LeRbLQ5jtnheIPN9JQAaufGjKdWduZn4U7wCtVuzKhmRkI0DFYHYRbB2H1w==",
"license": "MIT"
},
"node_modules/source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmmirror.com/source-map/-/source-map-0.6.1.tgz",
......@@ -6637,6 +6644,18 @@
"typescript": ">=5.0.0"
}
},
"node_modules/vuedraggable": {
"version": "4.1.0",
"resolved": "https://registry.npmmirror.com/vuedraggable/-/vuedraggable-4.1.0.tgz",
"integrity": "sha512-FU5HCWBmsf20GpP3eudURW3WdWTKIbEIQxh9/8GE806hydR9qZqRRxRE3RjqX7PkuLuMQG/A7n3cfj9rCEchww==",
"license": "MIT",
"dependencies": {
"sortablejs": "1.14.0"
},
"peerDependencies": {
"vue": "^3.0.1"
}
},
"node_modules/vxe-pc-ui": {
"version": "4.6.12",
"resolved": "https://registry.npmmirror.com/vxe-pc-ui/-/vxe-pc-ui-4.6.12.tgz",
......
......@@ -29,6 +29,7 @@
"vue-dompurify-html": "^5.1.0",
"vue-router": "^4.3.0",
"vue-tsc": "^2.1.10",
"vuedraggable": "^4.1.0",
"vxe-table": "^4.13.31",
"xlsx": "^0.18.5"
},
......
......@@ -7,12 +7,26 @@
import { ref, computed, onMounted } from 'vue'
import zhCn from 'element-plus/es/locale/lang/zh-cn.mjs'
import en from 'element-plus/es/locale/lang/en.mjs'
import { setKeyCode } from '@/store/product'
const language = ref('zh-cn')
const locale = computed(() => (language.value === 'zh-cn' ? zhCn : en))
// 在组件挂载时清空 localStorage
onMounted(() => {
localStorage.removeItem('socket_connect')
document.addEventListener('keydown', (e: any) => {
setKeyCode(e.key)
})
document.addEventListener('keyup', () => {
setKeyCode(null)
})
})
onUnmounted(() => {
document.removeEventListener('keydown', (e) => {
setKeyCode(e.key)
})
document.removeEventListener('keyup', () => {
setKeyCode(null)
})
})
</script>
<style lang="scss">
......
......@@ -3,6 +3,7 @@ import axios from './axios'
import { LogisticsData } from '@/types/api/order'
import { LogisticBill } from '@/types/api/podMakeOrder'
import { UploadResponse } from '@/types/api/product'
import { userData } from '@/types/api/user'
import { VersionImageList } from '@/types/api/typesetting'
import { SupplierItem, WarehouseListData } from '@/types'
......@@ -26,7 +27,7 @@ export function getUserListApi() {
// 上传图片文件
export function uploadImageApi(data: FormData) {
return axios.post<never, BaseRespData<never> & VersionImageList>(
return axios.post<never, BaseRespData<never> & UploadResponse>(
'upload/ossUpload',
data,
)
......
import axios from '@/api/axios.ts'
import { BasePaginationData, BaseRespData } from '@/types/api'
import {
IntercategoryTree,
InterSearchCriteriaWithStatus,
InterCardItem,
ProductDetail,
InterCraftItem,
} from '@/types/api/product'
// 状态统计列表
export function getStatusCountApi() {
return axios.get<never, BaseRespData<IntercategoryTree[]>>(
'custom/product/info/getStatusCountList',
)
}
export function getCardListPageApi(
data: InterSearchCriteriaWithStatus,
currentPage: number,
pageSize: number,
) {
return axios.post<never, BasePaginationData<InterCardItem>>(
'custom/product/info/page',
{
...data,
currentPage,
pageSize,
},
)
}
// 类目树
export function getcategoryTreeApi() {
return axios.get<never, BaseRespData<IntercategoryTree[]>>(
'custom/product/info/categoryTree',
)
}
// 工艺列表
export function getcraftListApi() {
return axios.get<never, BaseRespData<InterCraftItem[]>>(
'custom/product/info/craftList',
)
}
// 所有币种
export function getAllCurrencyApi() {
return axios.get<never, BaseRespData<IntercategoryTree[]>>(
'custom/product/info/getAllCurrency',
)
}
// 根据类别获取属性值
export function getByCateIdApi(id: number) {
return axios.get<never, BaseRespData<never>>(
'custom/product/info/getByCateId',
{ params: { id } },
)
}
// 状态转换
export function updateStatusApi(data?: {
ids: string
status?: string | number
remark?: string | null
}) {
return axios.post<never, BaseRespData<never>>(
'custom/product/info/updateStatus',
data,
)
}
// 新增商品
export function createProductApi(from: ProductDetail) {
return axios.post<never, BaseRespData<never>>('custom/product/info/create', {
...from,
})
}
// 编辑商品
export function updateProductApi(from: ProductDetail) {
return axios.post<never, BaseRespData<never>>('custom/product/info/update', {
...from,
})
}
// 操作日志
export function getLogListApi(id: number) {
return axios.get<never, BaseRespData<never>>(
'custom/product/info/getLogList',
{ params: { id } },
)
}
// 查看详情
export function getByIdApi(id: number) {
return axios.get<never, BaseRespData<never>>('custom/product/info/getById', {
params: { id },
})
}
......@@ -141,6 +141,12 @@ const mainImageSrc = computed<string>(() => {
) {
return item[props.imageField] as string
}
if (
props.imageField === 'img_url' &&
typeof item[props.imageField] === 'string'
) {
return item[props.imageField] as string
}
// 默认返回空字符串
return ''
})
......
......@@ -124,6 +124,20 @@ export default defineComponent({
$table.setCurrentRow(row)
}
}
//滚动到指定行
const scrollToRowEvent = (row: TableRowData, top = true) => {
const $table = tableRef.value
if ($table) {
$table.scrollToRow(row, top ? 'top' : 'bottom')
}
}
//清除所有勾选
const clearCheckboxEvent = () => {
const $table = tableRef.value
if ($table) {
$table.clearCheckboxRow()
}
}
onMounted(() => {
getList()
......@@ -137,6 +151,8 @@ export default defineComponent({
getSelectEvent,
selectRowEvent,
setCheckboxRow,
scrollToRow: scrollToRowEvent,
clearCheckbox: clearCheckboxEvent,
attrs,
}
},
......
......@@ -5,13 +5,13 @@
style="border-bottom: 1px solid #ccc"
:editor="editorRef"
:default-config="toolbarConfig"
:mode="mode"
:mode="modeType"
/>
<Editor
v-model="html"
style="height: 300px; overflow-y: hidden"
:default-config="editorConfig"
:mode="mode"
:mode="modeType"
@on-created="onCreated"
/>
</div>
......@@ -21,7 +21,7 @@
// @ts-ignore
import { Editor, Toolbar } from '@wangeditor/editor-for-vue'
import '@wangeditor/editor/dist/css/style.css'
import { computed, onBeforeUnmount, ref, shallowRef } from 'vue'
import { computed, onBeforeUnmount, shallowRef } from 'vue'
import { uploadImg } from '@/utils/file'
const props = defineProps({
modelValue: String,
......@@ -33,6 +33,10 @@ const props = defineProps({
type: Boolean,
default: false,
},
modeType: {
type: String,
default: 'default',
},
insertData: {
type: Array,
default: () => [
......@@ -67,6 +71,7 @@ const toolbarConfig = computed(() => {
})
const editorConfig = computed(() => {
return {
readOnly: props.modeType === 'simple',
placeholder: props.placeholder,
MENU_CONF: {
uploadImage: {
......@@ -89,7 +94,6 @@ const editorConfig = computed(() => {
},
}
})
const mode = ref('default')
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const onCreated = (editor: any) => {
console.log(editor)
......
<template>
<ElDialog
v-model="visible"
title="属性选择"
width="65%"
:close-on-click-modal="false"
>
<div class="dialog-body" style="height: 60vh; overflow: auto">
<div class="header">
<slot name="header" />
</div>
<div v-if="skuOptions?.length > 0">
<div
v-for="(item, index) in skuOptions"
:key="index"
class="option_wrap"
>
<div class="title">
<span>{{ item.cnname }}</span>
<span>({{ item.enname }})</span>
</div>
<div v-if="checkSkuArr(index)">
<el-checkbox-group
v-model="localSkuArr[index].listCode"
class="attribute-checkbox"
>
<el-checkbox
v-for="(item2, index2) in item.valueList || []"
:key="index2"
:label="item2.code"
:disabled="isDisabled(item2.code)"
:style="{
background: item2.bgColor,
color: item2.fontColor,
padding: '0 10px',
}"
@change="(v:boolean) => optionChange(index, index2, v)"
>
{{ item2.isCome }}
{{ item2.cnname + '(' + item2.enname + ')' }}
</el-checkbox>
</el-checkbox-group>
</div>
</div>
</div>
</div>
<template #footer>
<span class="dialog-footer">
<el-button @click="visible = false">取消</el-button>
<el-button type="primary" @click="handleSubmit">确认</el-button>
</span>
</template>
</ElDialog>
</template>
<script setup lang="ts">
import { defineModel, defineEmits, defineExpose, defineProps } from 'vue'
const visible = defineModel<boolean>('visible', { default: false })
import { SkuPropertyValue, SkuProperty, InterSkuArr } from '@/types/api/product'
import { storeToRefs } from 'pinia'
import { useOptionsStore } from '@/store/product'
const store = useOptionsStore()
const { keyCode } = storeToRefs(store)
const emits = defineEmits<{
(event: 'success'): void
(e: 'update:skuArr', payload: { skuArr: InterSkuArr[] }): void
(event: 'close'): void
}>()
const props = defineProps({
skuOptions: {
type: Array as () => SkuProperty[],
required: true,
},
skuArr: {
type: Array as () => InterSkuArr[],
required: true,
},
save: {
type: Function,
required: true,
},
})
const mode = ref('add')
const currentIndex = ref<number>(-1)
const childrenIndex = ref<number>(-1)
const localSkuArr = ref(JSON.parse(JSON.stringify(props.skuArr)))
watch(
() => props.skuArr,
(newVal: InterSkuArr[]) => {
localSkuArr.value = JSON.parse(JSON.stringify(newVal)) || [] //留下一开始回显的值
},
)
const handleSubmit = async () => {
emits('update:skuArr', {
skuArr: JSON.parse(JSON.stringify(localSkuArr.value)),
}) // 只提交本地深拷贝
props.save() //提交属性的同时,表格数据变化
ElMessage.success('提交成功')
visible.value = false
}
const open = async (key = 'add') => {
mode.value = key
}
const checkSkuArr = (index: number) => {
const skuArr = localSkuArr.value
return skuArr && skuArr.length > 0 && skuArr[index]?.listCode !== undefined
}
const optionChange = (i: number, j: number, value: boolean) => {
const list = props.skuOptions
const skuArr = localSkuArr.value
if (keyCode.value === 'Shift' && value) {
if (currentIndex.value === i) {
let min: number, max: number
if (j > childrenIndex.value) {
min = childrenIndex.value
max = j
} else {
min = j
max = childrenIndex.value
}
const codeList = skuArr[i].listCode
for (let index = min; index <= max; index++) {
list[i].valueList[index].selected = value ? 1 : 0
if (!codeList.includes(list[i].valueList[index].code)) {
codeList.push(list[i].valueList[index].code)
}
}
} else {
currentIndex.value = i
childrenIndex.value = j
list[i].valueList[j].selected = value ? 1 : 0
}
} else {
if (value) {
currentIndex.value = i
childrenIndex.value = j
} else {
currentIndex.value = -1
childrenIndex.value = -1
}
list[i].valueList[j].selected = value ? 1 : 0
}
// 保证valueList和listCode一致
skuArr[i].valueList = list[i].valueList.filter((v: SkuPropertyValue) =>
skuArr[i].listCode.includes(v.code),
)
}
// const handleClose = () => {
// emit('close')
// }
// 判断某属性是否在父组件skuArr中已被选中(不区分index)
const isDisabled = (code: string) => {
return (props.skuArr ?? []).some((item: InterSkuArr) =>
item.listCode.includes(code),
)
}
defineExpose({
open,
})
</script>
<style lang="scss" scoped>
:deep(.el-checkbox__label) {
font-size: 12px;
width: 85px;
overflow: hidden;
white-space: nowrap;
vertical-align: middle;
text-overflow: ellipsis;
}
:deep(.el-checkbox) {
padding-top: 3px;
padding-bottom: 3px;
margin-bottom: 10px !important;
}
.title {
padding-left: 5px;
font-size: 16px;
line-height: 36px;
font-weight: bold;
border-bottom: 1px solid #efefef;
margin-bottom: 10px;
}
.attribute-checkbox {
padding-left: 5px;
}
.option_wrap + .option_wrap {
border-top: 1px solid #bbb;
}
.dialog-footer {
margin-top: 10px;
width: 100%;
display: flex;
justify-content: center;
}
</style>
......@@ -33,6 +33,7 @@ import issueDoc from '@/views/warehouse/issueDoc.vue'
import ExternalAuthorisationPage from '@/views/system/externalAuthorisationPage.vue'
import CustomersPage from '@/views/system/CustomersPage.vue'
import stockingPlan from '@/views/warehouse/stockingPlan.vue'
import productManagement from '@/views/product/productManagement.vue'
const router = createRouter({
history: createWebHistory(),
routes: [
......@@ -303,6 +304,13 @@ const router = createRouter({
component: stockingPlan,
},
{
path: '/product/product-management',
meta: {
title: '商品管理',
},
component: productManagement,
},
{
path: '/warehouse/stocking-application',
meta: {
title: '入库申请单',
......
......@@ -103,6 +103,11 @@ const menu: MenuItem[] = [
id: 124,
label: '备货计划',
},
{
index: '/product/product-management',
id: 125,
label: '商品管理',
},
],
},
......
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import {
getcategoryTreeApi,
getcraftListApi,
getAllCurrencyApi,
} from '@/api/product'
import { warehouseInfoGetAll } from '@/api/warehouse'
interface ApiResponse {
data: unknown
code?: number
message?: string
}
type OptionItem = Record<string, unknown>
export const getOptionsApi = (
name: string,
params?: Record<string, unknown>,
): Promise<ApiResponse> => {
switch (name) {
case 'category':
return getcategoryTreeApi() as Promise<ApiResponse>
case 'craft':
return getcraftListApi() as Promise<ApiResponse>
case 'currency':
return getAllCurrencyApi() as Promise<ApiResponse>
case 'warehouse':
return warehouseInfoGetAll() as Promise<ApiResponse>
default:
throw new Error(`Unknown option name: ${name}:params${params}`)
}
}
// 模块级共享状态
let _keyCode: string | null = null
export const useOptionsStore = defineStore('options', () => {
// 使用 computed 与模块级 _keyCode 保持同步
const keyCode = computed({
get: () => _keyCode,
set: (v: string | null) => {
_keyCode = v
},
})
const dataMap = ref<Partial<Record<string, OptionItem[]>>>({})
const extractList = (response: ApiResponse): OptionItem[] => {
const { data } = response
if (Array.isArray(data)) {
return data as OptionItem[]
}
if (
data &&
typeof data === 'object' &&
'data' in data &&
Array.isArray((data as { data: unknown }).data)
) {
return (data as { data: OptionItem[] }).data
}
console.warn('Unexpected response structure:', response)
return []
}
const fetchOption = async (
name: string,
params?: Record<string, unknown>,
): Promise<OptionItem[]> => {
if (dataMap.value[name] && name !== 'canlogistics') {
return dataMap.value[name]!
}
try {
const response = await getOptionsApi(name, params)
const list = extractList(response)
dataMap.value[name] = list
return list
} catch (error) {
console.error(`Failed to fetch ${name}:`, error)
throw error
}
}
const getOptions = async (
names: string | string[],
params?: Record<string, unknown>,
): Promise<Record<string, OptionItem[]>> => {
const nameArray = Array.isArray(names) ? names : [names]
const results = await Promise.all(
nameArray.map(async (name) => {
try {
const list = await fetchOption(
name,
name === 'canlogistics' ? params : undefined,
)
return { name, list }
} catch {
return { name, list: [] }
}
}),
)
return Object.fromEntries(results.map(({ name, list }) => [name, list]))
}
const clearCache = (name?: string) => {
if (name) {
delete dataMap.value[name]
} else {
dataMap.value = {}
}
}
const setKeyCode = (code: string | null) => {
keyCode.value = code
}
return {
keyCode,
dataMap,
getOptions,
clearCache,
setKeyCode,
}
})
// 导出供直接导入使用(与 store 内部共享 _keyCode)
export const keyCode = {
get value() {
return _keyCode
},
set value(v: string | null) {
_keyCode = v
},
}
export const setKeyCode = (code: string | null) => {
_keyCode = code
}
export interface IntercategoryTree {
count: number
remark: string
status: number | string
children: IntercategoryTree[] | null
}
export interface InterSearchCriteria {
category_id: number | null
diyUserId: number | null
name: string
print_type: number | null
product_type: string
sku: string
}
// 需要 status 的场景
export interface InterSearchCriteriaWithStatus extends InterSearchCriteria {
status: string
}
export interface InterCardItem {
colorImageList: string[]
remark: string
cnRemark: string
id: number
sku: string
product_no: string
name: string
namespace: string | null
affiliated_factory: string | null
third_sku: string | null
title: string
color_images: string
img_url: string
category_id: number
weight: number
purchasing_min: number | null
factory_price: number
sales_price: number
sales_price_max: number
status: number | string | null
property1_cate_id: number
property2_cate_id: number
property3_cate_id?: number | null
property1_enname: string
property2_enname: string
property3_enname?: string | null
material: string
print_type: number
origin_code: string
origin_name_cn: string
origin_name_en: string
currency_code: string
currency_name: string
product_type: string
factory_id: number | null
factory_code: string | null
processing: boolean
create_time: string
update_time: string
sort: number | null
diy_id: number
diy_sku: string
}
/** 图片信息 */
export interface ImageInfo {
id: number | null
product_id?: number | null
image_url: string
sort: number
type?: number
create_time?: string
}
/** 产品备注信息 */
export interface ProductRemark {
id: number
product_id: number
remark: string // HTML格式
create_time?: string
}
/** SKU属性值 */
export interface SkuPropertyValue {
id: number
cateCode: string
cateName: string
code: string
cnname: string
enname: string
fontColor: string
bgColor: string
battery: boolean
liquid: boolean
knife: boolean
selected: number
sort: number
enable: boolean
publicData: boolean
isCome?: number | null
}
/** SKU属性定义 */
export interface SkuProperty {
id: number
cnname: string
enname: string
sort: number
skuProperty: boolean
multi: boolean
enable: boolean
categoryInfoId: number | null
propertyValueIds: number[] | null
valueList: SkuPropertyValue[]
}
// 选中的sku
export interface InterSkuArr {
id: number
name: string
enname: string
valueList: SkuPropertyValue[]
listCode: string[]
selected?: number
}
/** 产品SKU */
export interface ProductSku {
id: number
product_id: number
sku: string
sku_name: string
product_no: string
image: string
// image_ary: string // JSON字符串
property_cate_id1: number
property_cate_id2: number
property_cate_id3: number | null
property1_id: number
property2_id: number
property3_id: number | null
property_code1: string
property_code2: string
property_code3: string | null
option_enname1: string
option_enname2: string
option_enname3: string | null
custom_value1: string
custom_value2: string
custom_value3: string | null
factory_price: number
sales_price: number
sku_weight: number | null
reg_count: number | null
print_type?: number | null
size_type?: number
sort: number
create_time?: string
_X_ROW_KEY?: string | null
}
/** 属性关联 */
export interface PropertyRelation {
id: number
info_id: number
property_id: number
value_id: number
sku_property: {
type: 'Buffer'
data: number[]
}
}
// 编辑接口
export interface ProductDetail {
// 图片相关
colorImageList?: string[]
color_images: string
img_url: string
imageList: ImageInfo[]
sizeList: ImageInfo[]
// 描述备注
// remark: string // HTML格式
cnRemark: string // HTML格式
productRemark?: ProductRemark
productCnRemark?: ProductRemark
// 基础信息
id: number | null
sku?: string
product_no: string
name: string
title: string
userMark: string | null
// 分类属性
category_id: number | null
property1_cate_id: number | null
property2_cate_id: number | null
property3_cate_id?: number | null
property1_enname: string
property2_enname: string
property3_enname?: string | null
// 价格重量
weight: number | null
factory_price: number | null
sales_price?: number | null
// sales_price_max?: number
// purchasing_min?: number | null
// 状态类型
print_type: number | null
product_type: 'platform' | 'customer' | string
processing: boolean
// 材质产地
material: string
// origin_code: string
// origin_name_cn: string
// origin_name_en: string
// 货币
currency_code: string
currency_name: string
// 工厂
factory_id?: number | null
factory_code?: string | null
affiliated_factory?: string | null
// DIY相关
diy_id?: number
diy_sku?: string
diyUserIds?: number[]
// 其他
namespace: string | null
third_sku?: string | null
sort?: number | null
create_time?: string
update_time?: string
// 工艺
craftIds: string[]
// 关联数据
productList: ProductSku[]
properties?: PropertyRelation[]
skuProperties: SkuProperty[]
factoryIds?: number[] | null
// warehouseIds: number[]
}
export interface InterCategoryNode {
id: number
name: string
enname: string
pid: number
pids: string
deep: number
sort: number
enpath: string
cnpath: string
leaf: boolean
createTime: string
publicData?: boolean
children?: InterCategoryNode[]
}
export interface InterCountryType {
id: number
nameCn: string
nameEn: string
countryCode: string
}
export interface InterCustomerItem {
id: number
name: string
sku?: string
user_mark?: string
}
export interface InterCraftItem {
id: string
craft_name: string
craft_code: string
craft_type: string
}
export interface CurrencyType {
id: number
currencyName: string
currencyCode: string
}
export interface ProcessTypeData {
label: string
value: string
}
export interface InterProductType {
name: string
code: string
}
export interface UploadResponse {
code: number
requestId: string
filePath: string
message: string
}
export interface TableRowData {
[key: string]: unknown
}
export interface InterReceiptItem {
inNo: string
warehouseId: string
warehouseName: string
remark: string
factoryCode: string
factoryId: number | null
productList: unknown[]
}
import { ref } from 'vue'
import {
IntercategoryTree,
ProductSku,
ProcessTypeData,
} from '@/types/api/product'
export const processType = ref<ProcessTypeData[]>([
{ label: '烫画', value: 'TH' },
{ label: '直喷', value: 'ZP' },
{ label: '刺绣', value: 'CX' },
{ label: '雕刻', value: 'DK' },
{ label: '白胚', value: 'BP' },
{ label: '其他', value: 'QT' },
])
export const clearNonEmptyChildren = (
nodes: IntercategoryTree[],
): IntercategoryTree[] => {
return nodes.map((node) => {
if (node.children && node.children.length > 0) {
// 有值的 children 置为空数组
return { ...node, children: [] }
}
// 原本就是 [] 或没有 children 的保持不变
return node
})
}
import { computed } from 'vue'
import { storeToRefs } from 'pinia'
import { useOptionsStore } from '@/store/product'
export function useOptions() {
const store = useOptionsStore()
const { dataMap } = storeToRefs(store)
// 获取选项数据(单个或批量统一接口)
const getOptions = async (
names: string | string[],
params?: Record<string, unknown>,
) => {
return store.getOptions(names, params)
}
// 清除缓存
const clearCache = (name?: string) => {
store.clearCache(name)
}
// 直接访问某个选项的响应式数据(用法类似 mapState)
const useOption = (name: string) => {
return computed(() => dataMap.value[name] ?? [])
}
// 批量获取响应式数据对象(解构使用)
const useOptionsData = (names: string[]) => {
return Object.fromEntries(
names.map((name) => [name, computed(() => dataMap.value[name] ?? [])]),
)
}
return {
getOptions,
clearCache,
useOption,
useOptionsData,
dataMap,
}
}
export function getImageUrl(imgUrl: string, width?: number, height?: number) {
const patt = new RegExp('http')
if (patt.test(imgUrl)) {
if (!width) return imgUrl
if (/oss/.test(imgUrl)) {
return imgUrl + '?x-oss-process=image/resize,l_' + width
}
if (width && !height) {
return imgUrl + '?imageView2/2/w/' + width
} else if (!width && height) {
return imgUrl + '?imageView2/2/h/' + height
} else if (width && height) {
return imgUrl + '?imageView2/1/w/' + width + '/h/' + height
} else {
return imgUrl
}
}
}
export function cartesianProduct<T>(spec: T[][]): T[][] {
return spec.reduce(
(acc, cur) => acc.flatMap((x) => cur.map((y) => [...x, y])),
[[]] as T[][],
)
}
export function emptyArray<T>(arr: T[], message?: string): boolean {
const isEmpty = arr.length === 0
if (isEmpty) {
ElMessage({
message: message || '请至少选择一条记录',
type: 'warning',
duration: 1000,
})
}
return isEmpty
}
export function cloneDeep<T>(value: T): T {
return JSON.parse(JSON.stringify(value))
}
/** 校验所有 SKU 项,返回错误信息数组 */
export function validateSkuItems(skuList: ProductSku[]): string[] {
const errors: string[] = []
skuList.forEach((item, index) => {
const rowErrors: string[] = []
if (!item.image) rowErrors.push('SKU图片不能为空')
if (!item.sku_weight) rowErrors.push('商品克重不能为空')
if (!item.reg_count) rowErrors.push('注册件数不能为空')
if (!item.factory_price || !item.sales_price)
rowErrors.push('对外报价不能为空')
if (rowErrors.length) {
errors.push(`第${index + 1}行的${rowErrors.join('、')}`)
}
})
return errors
}
/** 生成 color_images 字符串(去重) */
export function generateColorImages(colorMap: Record<string, string>): string {
const urls = Object.values(colorMap).filter(Boolean)
return [...new Set(urls)].join(',')
}
/** 最终处理 SKU 列表:排序、补充默认值、清除vxe-table内部字段 */
export function finalizeSkuItems(skuList: ProductSku[]) {
skuList.forEach((item, idx) => {
// 排序字段
item.sort = idx + 1
// 补充 custom_value
if (!item.custom_value1 && item.option_enname1) {
item.custom_value1 = item.option_enname1
}
if (!item.custom_value2 && item.option_enname2) {
item.custom_value2 = item.option_enname2
}
// 清除 vxe-table 内部字段
delete item._X_ROW_KEY
})
}
/** 计算最小重量 */
export function calculateMinWeight(skuList: ProductSku[]): number {
if (!skuList.length) return 0
const weights = skuList
.map((item) => item.sku_weight)
.filter((w): w is number => w !== null && w !== undefined)
return weights.length ? Math.min(...weights) : 0
}
// 比较大对象的异同,并将addList|updateList|deleteList返回
/* ---------- 类型声明 ---------- */
export type IdKey = string | number
export interface HasId {
id: IdKey
}
/** 数组元素可以是任意对象,但必须包含 id */
export type ArrayItem = { id: IdKey; [key: string]: unknown }
/** 新旧值对比结果 */
export type PatchResult<T extends ArrayItem> = {
addList?: T[]
updateList?: (Partial<T> & HasId)[]
removeList?: IdKey[]
}
/** 整个表单的对比结果 */
export type FormPatch<T extends Record<string, unknown>> = {
id: IdKey
} & {
[K in keyof T]?: T[K] extends ArrayItem[] ? PatchResult<T[K][number]> : T[K]
}
/* ---------- 工具函数 ---------- */
/** 判断对象中是否至少有一个属性是数组 */
function itemIsArray(obj: Record<string, unknown>): boolean {
return Object.values(obj).some(Array.isArray)
}
/** 比较两个数组是否发生变化(长度、id 对应关系) */
function isChange(arr: HasId[] = [], arr1: HasId[] = []): boolean {
if (arr.length !== arr1.length) return true
const ids = new Set(arr.map((i) => i.id))
return arr1.some((i) => !ids.has(i.id))
}
/* ---------- 核心逻辑 ---------- */
export function checkUpdateParams<T extends Record<string, unknown>>(
newParams: T,
oldParams: T | null | undefined,
id: string = 'id',
other: Record<string, string> = {},
bool: boolean = false,
): FormPatch<T> | null {
if (!oldParams)
return { id: newParams[id as keyof T] } as unknown as FormPatch<T>
if (newParams[id as keyof T] !== oldParams[id as keyof T])
return { id: newParams[id as keyof T] } as unknown as FormPatch<T>
const cloneOld = JSON.parse(JSON.stringify(oldParams)) as T
const params = { id: newParams[id as keyof T] } as unknown as FormPatch<T>
for (const key in newParams) {
const newVal = newParams[key]
const oldVal = cloneOld[key as keyof T]
/* ---------- 数组字段 ---------- */
if (Array.isArray(newVal)) {
/* 1.没指定主键 → 原样保留 */
if (!(key in other)) {
;(params as Record<string, unknown>)[key] = newVal // ← 直接写回
continue
}
const arr = newVal as ArrayItem[]
const arr1 = (oldVal as ArrayItem[]) || []
const addList: ArrayItem[] = []
const updateList: (ArrayItem & HasId)[] = []
let removeList: IdKey[] = []
const keyName = other[key] || id
const list: ArrayItem[] = []
for (const iterator of arr) {
const index = arr1.findIndex(
(item) => item[keyName] === iterator[keyName],
)
const isArr = itemIsArray(iterator)
if (index !== -1) {
const item = arr1[index]
arr1.splice(index, 1) // 从旧数组中移除,剩下的就是要删除的
const obj: Record<string, unknown> = {}
for (const k in iterator) {
if (k === keyName) continue
if (Array.isArray(iterator[k])) {
if (isChange(iterator[k] as HasId[], item[k] as HasId[]))
obj[k] = iterator[k]
} else if (iterator[k] !== item[k]) {
obj[k] = iterator[k]
}
}
if (Object.keys(obj).length) {
const patch = { [keyName]: iterator[keyName], id: item.id, ...obj }
isArr && !bool ? list.push(patch) : updateList.push(patch)
}
} else {
isArr && !bool ? list.push(iterator) : addList.push(iterator)
}
}
if (arr1.length) removeList = arr1.map((item) => item.id)
const changeKey = (key as string).replace(/List$/, 'Change') as keyof T
const res: PatchResult<ArrayItem> = {
addList: [],
updateList: [],
removeList: [],
}
// 下面原来逻辑不变,有值就覆盖空数组
if (addList.length) res.addList = addList
if (updateList.length) res.updateList = updateList
if (removeList.length) res.removeList = removeList
if (list.length)
(res as unknown as Record<string, unknown>).list = list
// 不管有没有变化,始终写回 xxChange
;(params as Record<string, unknown>)[changeKey as string] = res
} else {
/* ---------- 普通字段 ---------- */
;(params as Record<string, unknown>)[key] = newVal
}
}
/* ========== 追加:检测非数组字段差异 ========== */
let basicChanged = false
for (const key in newParams) {
const newVal = newParams[key]
const oldVal = cloneOld[key as keyof T]
// 只比对非数组、非对象、非函数的基础字段
if (
!Array.isArray(newVal) &&
(typeof newVal !== 'object' || newVal === null) &&
newVal !== oldVal
) {
basicChanged = true
break
}
}
if (basicChanged) {
;(params as Record<string, unknown>).__dirty = 'isChanged'
}
/* 仅 id 相同,无其他变动 */
if (Object.keys(params).length === 1) return null
return params
}
export function checkDataChange(
customChanges: Record<string, unknown> | null | undefined,
): boolean {
// 如果 customChanges 为 null 或 undefined,直接返回 false
if (!customChanges) return false
// 检查 __dirty 字段
const isDirty = customChanges.__dirty === 'isChanged'
// 检查是否有包含 "Change" 的键
const hasChangeKey = Object.keys(customChanges).some(
(k) => typeof k === 'string' && k.includes('Change'),
)
return isDirty || hasChangeKey
}
// 递归查找节点
export function findNodeById(
nodes: IntercategoryTree[],
targetId: number,
): IntercategoryTree | null {
for (const node of nodes) {
if (node.status === targetId) {
return node
}
if (node.children && node.children.length > 0) {
const found = findNodeById(node.children, targetId)
if (found) return found
}
}
return null
}
<template>
<div class="image-uploader">
<!-- 图片列表 -->
<div class="image-center">
<Draggable
v-if="imageLists.length"
v-model="imageLists"
:item-key="draggableKey"
class="image-list"
animation=""
ghost-class="sortable-ghost"
chosen-class="sortable-chosen"
>
<template #item="{ element: image }">
<div
class="image-card"
:class="{ selected: selectedIds.includes(image.image_url) }"
>
<div class="checkbox">
<el-checkbox
:model-value="selectedIds.includes(image.image_url)"
@change="toggleSelect(image.image_url)"
/>
</div>
<div class="btn_wrap">
<button type="button" @click.stop="seeImage(image.image_url)">
<el-icon><ZoomIn /></el-icon>
</button>
</div>
<div class="preview_img">
<img
:src="getImageUrl(image.image_url, 80, 80)"
:alt="image.id"
/>
</div>
<div v-if="image.id" class="actions">
{{ image.id }}
</div>
</div>
</template>
</Draggable>
<!-- 上传区域 -->
<div v-if="isShowUpload" class="upload-area">
<div class="color-image-item-icon-plus" @click="triggerFileInput">
<el-icon><Plus /></el-icon>
</div>
<input
ref="fileInputRef"
type="file"
accept="image/*"
multiple
style="display: none"
@change="handleFileUpload"
/>
</div>
</div>
<!-- 底部操作栏 -->
<div v-if="imageLists.length" class="actions-bar">
<el-button v-if="isMoreCheck" type="success" @click="toggleSelectAll">
{{ isAllSelected ? '取消全选' : '选择全部' }}
</el-button>
<el-button
v-if="isShowUpload"
type="danger"
plain
:disabled="!selectedIds.length"
@click="deleteSelected"
>
删除选中 ({{ selectedIds.length }})
</el-button>
</div>
<el-image-viewer
v-if="previewVisible"
:url-list="previewImageList"
:initial-index="previewIndex"
@close="previewVisible = false"
/>
</div>
</template>
<script setup lang="ts">
import {
ref,
computed,
watch,
onBeforeUnmount,
defineProps,
defineEmits,
defineExpose,
} from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Plus, ZoomIn } from '@element-plus/icons-vue'
import Draggable from 'vuedraggable'
const props = defineProps({
modelValue: {
type: Array as () => ImageInfo[],
required: true,
},
maxCount: {
type: Number,
},
maxSizeMB: {
type: Number,
},
// 是否显示上传按钮?仅作列表展示?默认显示上传
isShowUpload: {
type: Boolean,
default: true,
},
// 选中图片要单选?多选?默认多选
isMoreCheck: {
type: Boolean,
default: true,
},
})
const emit = defineEmits<{
(e: 'update:modelValue', value: ImageInfo[]): void
(e: 'change', value: ImageInfo[]): void
(e: 'list-change', value: (string | null)[]): void
}>()
const imageLists = ref<ImageInfo[]>(props.modelValue || [])
const selectedIds = ref<(string | null)[]>([])
const fileInputRef = ref<HTMLInputElement>()
// 监听外部 modelValue 变化(外部修改时同步到本地)
watch(
() => props.modelValue,
(newVal: ImageInfo[]) => {
imageLists.value = newVal || []
},
{ deep: true },
)
// 是否全选
const isAllSelected = computed(() => {
return (
imageLists.value.length > 0 &&
selectedIds.value.length === imageLists.value.length
)
})
// 拖拽组件的 key 函数
const draggableKey = (item: ImageInfo) => item.image_url
// 监听外部 modelValue 变化
const syncModelValue = () => {
emit('update:modelValue', imageLists.value)
emit('change', imageLists.value)
}
const seeImage = (item: string) => {
// 预览列表:所有图片的完整 URL
previewImageList.value = imageLists.value.map(
(img: ImageInfo) => getImageUrl(img.image_url) as string,
)
previewIndex.value = imageLists.value.findIndex(
(img: ImageInfo) => img.image_url === item,
)
previewVisible.value = true
}
const previewVisible = ref(false)
const previewIndex = ref(0)
const previewImageList = ref<string[]>([])
// 触发隐藏文件选择框
const triggerFileInput = () => {
fileInputRef.value?.click()
}
import { uploadImageApi } from '@/api/common'
import { ImageInfo, UploadResponse } from '@/types/api/product'
import { getImageUrl } from '@/utils/product'
import { ElLoading } from 'element-plus'
const uploadLoading = ref(false)
const handleFileUpload = async (files: Event) => {
const fileList = (files.target as HTMLInputElement).files
if (!fileList) return
const requestList: Promise<UploadResponse>[] = []
for (let i = 0; i < fileList.length; i++) {
const file = fileList[i]
const formData = new FormData()
formData.append('file', file)
formData.append('businessType', 'product')
requestList.push(uploadImageApi(formData))
}
const loadingInstance = ElLoading.service({
lock: true,
text: `正在上传 ${fileList.length} 个文件...`,
background: 'rgba(0, 0, 0, 0.7)',
})
uploadLoading.value = true
if (fileInputRef.value) {
fileInputRef.value.value = ''
}
try {
const res = await Promise.all(requestList)
const createImage: ImageInfo[] = []
res.forEach((item, index) => {
const imgInfo = {
sort: imageLists.value.length + index,
image_url: item.filePath,
id: null,
product_id: null,
}
createImage.push(imgInfo)
})
imageLists.value = [...imageLists.value, ...createImage]
syncModelValue()
} catch (e) {
console.error(e)
ElMessage.error('上传失败,请重试')
} finally {
// 关闭 loading 遮罩
loadingInstance.close()
uploadLoading.value = false
}
}
// const handleFileUpload = (event: Event) => {
// const input = event.target as HTMLInputElement
// const files = Array.from(input.files || [])
// if (!files.length) return
// // 校验数量
// if (
// props.maxCount &&
// imageLists.value.length + files.length > props.maxCount
// ) {
// ElMessage.warning(`最多只能上传 ${props.maxCount} 张图片`)
// return
// }
// // 逐个处理文件
// for (const file of files) {
// if (!file.type.startsWith('image/')) {
// ElMessage.warning(`文件 ${file.name} 不是图片,已跳过`)
// continue
// }
// const maxSize = (props.maxSizeMB || 5) * 1024 * 1024
// if (file.size > maxSize) {
// ElMessage.warning(
// `图片 ${file.name} 超过 ${props.maxSizeMB || 5}MB,已跳过`,
// )
// continue
// }
// const url = URL.createObjectURL(file)
// imageLists.value.push({
// id: null,
// image_url: url,
// })
// }
// syncModelValue()
// input.value = '' // 清空,允许重复上传同一文件
// }
// 切换单个图片选中状态
const toggleSelect = (imageUrl: string | null) => {
if (!imageUrl) return // 无效 URL 直接忽略
const isMoreCheck = props.isMoreCheck ?? true // 默认多选
const index = selectedIds.value.indexOf(imageUrl)
if (isMoreCheck) {
// 多选:切换选中状态
if (index === -1) {
selectedIds.value.push(imageUrl)
} else {
selectedIds.value.splice(index, 1)
}
} else {
// 单选:如果点击的是已选中项,则取消;否则清空后选中当前项
if (index !== -1) {
selectedIds.value = []
} else {
selectedIds.value = [imageUrl]
}
}
emit('list-change', selectedIds.value)
}
const clear = () => {
selectedIds.value = []
}
// 全选/取消全选
const toggleSelectAll = () => {
if (isAllSelected.value) {
selectedIds.value = []
} else {
selectedIds.value = imageLists.value.map((img: ImageInfo) => img.image_url)
}
emit('list-change', selectedIds.value)
}
// 删除选中的图片
const deleteSelected = async () => {
if (!selectedIds.value.length) return
try {
await ElMessageBox.confirm('确定将选中的图片删除?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
} catch {
return
}
// 过滤掉选中的图片
const newImages = imageLists.value.filter(
(img: ImageInfo) => !selectedIds.value.includes(img.image_url),
)
// 释放被删除图片的 URL
imageLists.value.forEach((img: ImageInfo) => {
if (selectedIds.value.includes(img.image_url)) {
URL.revokeObjectURL(img.image_url)
}
})
imageLists.value = newImages
selectedIds.value = []
syncModelValue()
}
// 组件卸载时释放所有 object URL
onBeforeUnmount(() => {
imageLists.value.forEach((img: ImageInfo) => {
URL.revokeObjectURL(img.image_url)
})
})
// 对外暴露方法(可选)
defineExpose({
clear,
imageLists,
reset: () => {
imageLists.value.forEach((img: ImageInfo) =>
URL.revokeObjectURL(img.image_url),
)
imageLists.value = []
selectedIds.value = []
syncModelValue()
},
})
</script>
<style scoped lang="scss">
.image-uploader {
.image-center {
display: flex;
align-items: flex-start;
gap: 16px;
padding-left: 16px;
}
.upload-area {
display: flex;
align-items: center;
gap: 12px;
margin: 16px 0;
.tip {
font-size: 12px;
color: #909399;
}
}
.image-list {
display: flex;
flex-wrap: wrap;
gap: 16px;
margin: 16px 0;
// 拖拽样式
:deep(.sortable-ghost) {
opacity: 0.4;
background: #409eff;
}
:deep(.sortable-chosen) {
cursor: move;
}
:deep(.image-card) {
cursor: grab;
&:active {
cursor: grabbing;
}
}
.image-card {
position: relative;
width: 100px;
border: 1px solid #dcdfe6;
border-radius: 8px;
overflow: hidden;
transition: all 0.2s;
&.selected {
border-color: #409eff;
box-shadow: 0 0 0 2px rgba(64, 158, 255, 0.2);
}
.checkbox {
position: absolute;
top: 4px;
left: 4px;
z-index: 1;
background: rgba(255, 255, 255, 0.7);
border-radius: 4px;
padding: 2px;
}
.btn_wrap {
display: none;
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
justify-content: center;
align-items: center;
background: rgba(0, 0, 0, 0.5);
button {
color: #fff;
font-size: 26px;
}
}
&:hover .btn_wrap {
display: flex;
}
.preview_img {
width: 100%;
height: 100px;
display: flex;
align-items: center;
justify-content: center;
background: #f5f7fa;
img {
max-width: 100%;
max-height: 100%;
object-fit: contain;
}
}
.actions {
display: flex;
justify-content: center;
padding: 8px;
border-top: 1px solid #ebeef5;
background: #fff;
}
}
}
.actions-bar {
display: flex;
justify-content: flex-end;
gap: 12px;
padding-top: 8px;
border-top: 1px solid #ebeef5;
}
}
.color-image-item-icon-plus {
width: 98px;
height: 98px;
border-radius: 8px;
border: 1px solid #ebeef5;
cursor: pointer;
position: relative;
display: flex;
align-items: center;
justify-content: center;
i {
color: #777;
font-size: 24px;
}
}
</style>
.pop-p {
position: relative;
min-height: 24px;
margin-top: 3px;
margin-bottom: 10px;
font-size: 14px;
line-height: 28px;
color: #333;
font-weight: bold;
text-align: center;
span {
font-size: 12px;
color: #666;
}
.pop-p-but {
position: absolute;
display: inline-block;
right: 0;
top: 0;
height: 20px;
}
}
.variants {
padding: 5px 10px;
.title {
color: #303133;
font-size: 16px;
font-weight: 600;
line-height: 36px;
padding-left: 10px;
border-bottom: 1px solid #efefef;
margin-bottom: 10px;
}
li {
display: inline-block;
position: relative;
width: 100px;
padding: 0 6px;
margin-left: 6px;
box-sizing: border-box;
border: 1px solid #dcdfe6;
background: #fff;
border-radius: 4px;
overflow: hidden;
white-space: nowrap;
font-weight: 500;
font-size: 12px;
line-height: 24px;
color: #606266;
cursor: pointer;
text-overflow: ellipsis;
padding-right: 10px;
}
li.active {
border: 1px solid blue;
}
li i {
position: absolute;
top: 7px;
right: 5px;
}
}
.variants + .variants {
border-top: 1px solid #bbb;
}
.form-footer {
margin-top: 10px;
width: 100%;
display: flex;
justify-content: center;
}
\ No newline at end of file
<template>
<ElDialog
v-model="visible"
:title="popupTitle"
width="70%"
:close-on-click-modal="false"
>
<el-form
ref="formRef"
:disabled="isLookOrConfirm"
:model="editForm"
label-width="100px"
:rules="formRules"
validate-on-rule-change="false"
>
<p class="pop-p">基础信息</p>
<ElRow>
<el-col :span="8">
<el-form-item label="商品名称" prop="name">
<el-input
v-model="editForm.name"
placeholder="请输入商品名称"
clearable
maxlength="40"
show-word-limit
/>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="商品类目" prop="category_id">
<el-cascader
ref="categoryCascader"
v-model="editForm.category_id"
:options="categoryTree"
:props="{
label: 'name',
value: 'id',
emitPath: false,
checkStrictly: true,
}"
:show-all-levels="false"
clearable
filterable
style="width: 100%"
@change="categoryChange"
></el-cascader>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="商品类型" prop="print_type">
<el-select
v-model="editForm.print_type"
style="width: 100%"
clearable
>
<el-option
v-for="item in PRINT_TYPE_OPTIONS"
:key="item.value"
:label="item.label"
:value="item.value"
></el-option>
</el-select>
</el-form-item>
</el-col>
</ElRow>
<ElRow>
<el-col :span="8">
<el-form-item label="款号" prop="product_no">
<el-input
v-model="editForm.product_no"
placeholder="请输入款号"
clearable
/>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="商品归属" prop="product_type">
<el-select
v-model="editForm.product_type"
style="width: 100%"
clearable
disabled
>
<el-option
v-for="item in productTypeList"
:key="item.code"
:label="item.name"
:value="item.code"
></el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="材质" prop="material">
<el-input
v-model="editForm.material"
placeholder="请输入材质"
clearable
/>
</el-form-item>
</el-col>
</ElRow>
<ElRow>
<el-col v-if="editForm.print_type === 1" :span="8">
<el-form-item label="支持工艺" prop="craftIds">
<LogisticsWaySelect
v-model="editForm.craftIds"
:company-list="craftList"
:start-width="'100%'"
search-placeholder="搜索工艺名称"
start-placeholder="请选择工艺名称"
></LogisticsWaySelect>
</el-form-item>
</el-col>
<el-col v-if="['edit'].includes(modeTitle)" :span="8">
<el-form-item label="客户编码" prop="editForm.userMark">
<el-input
v-model.number="editForm.userMark"
oninput="value=value.replace(/[^\d]/g,'')"
placeholder="请输入客户编码"
clearable
disabled
/>
</el-form-item>
</el-col>
</ElRow>
<!-- 编辑器 -->
<wangEditor
ref="wangeditorRef"
v-model="editForm.cnRemark"
placeholder="请输入中文描述"
height="400px"
filename="files"
:mode-type="isLookOrConfirm ? 'simple' : 'default'"
></wangEditor>
<p class="pop-p">商品图片</p>
<div style="border: 1px solid #ddd">
<ImageUploader
v-model="editForm.imageList"
:is-show-upload="isAddOrEdit"
@change="handleChange"
/>
</div>
<p class="pop-p">尺码表</p>
<div style="border: 1px solid #ddd">
<ImageUploader
v-model="editForm.sizeList"
:is-show-upload="isAddOrEdit"
@change="handleChange"
/>
</div>
<p class="pop-p">
商品SKU
<span class="pop-p-but">
<el-button
type="success"
size="small"
title="属性选择"
@click="addOption()"
>
添加销售变体
</el-button>
</span>
</p>
<div v-if="skuArr?.length > 0" style="min-height: 40px">
<div
v-for="(item, index) in skuArr"
v-show="item.valueList.length > 0"
:key="item.id"
class="variants"
>
<div class="title">{{ item.name }}</div>
<ul>
<Draggable
v-model="item.valueList"
item-key="id"
@change="dragChange(index)"
>
<template #item="{ element }">
<li
:key="element.id"
:style="{
background: element.bgColor,
color: element.fontColor,
}"
:class="{ active: variantsIndex === element.code }"
@click="optionSelection(element)"
>
{{ element.cnname }}({{ element.enname }})
<el-icon
v-show="popupTitle === '新增'"
@click="optionDelete(index, element.code)"
>
<CloseBold />
</el-icon>
</li>
</template>
</Draggable>
</ul>
</div>
</div>
<p class="pop-p">
<span class="pop-p-but">
<el-button type="success" @click="singleSetVariantImage">
更新图片
</el-button>
<el-button type="primary" @click="updateProductInfoBtn('对外报价')">
更新报价
</el-button>
<el-button type="success" @click="updateProductInfoBtn('商品重量')">
更新重量
</el-button>
<el-button type="primary" @click="updateProductInfoBtn('注册件数')">
更新件数
</el-button>
</span>
</p>
<!-- 表格 -->
<div
class="table_wrap"
style="
height: 250px;
width: 100%;
padding: 0;
box-sizing: border-box;
border: 1px solid #ddd;
overflow: hidden;
"
>
<CustomizeTable
ref="tableRef"
v-model="editForm.productList"
:config="tableConfig"
highlight-current-row
border="full"
@get-checkbox-records="handleCheckboxRecords"
></CustomizeTable>
</div>
</el-form>
<template v-if="!['look'].includes(modeTitle)" #footer>
<span class="form-footer">
<el-button @click="visible = false">取消</el-button>
<el-button v-if="isAddOrEdit" type="primary" @click="submitProduct"
>确定</el-button
>
<el-button
v-if="modeTitle === 'confirm'"
type="warning"
@click="failBtn"
>不通过</el-button
>
<el-button
v-if="modeTitle === 'confirm'"
type="primary"
@click="passBtn"
>通过</el-button
>
</span>
</template>
</ElDialog>
<!--属性选择弹窗 -->
<sku-attibute-popup
ref="skuAttibuteRef"
v-model:visible="attrVisible"
:sku-options="proSkuOptions"
:sku-arr="JSON.parse(JSON.stringify(skuArr))"
:save="saveOption"
@update:sku-arr="setOptions"
@close="skuClose"
>
</sku-attibute-popup>
<el-dialog
v-model="singleImagesVisible"
width="1000px"
title="选择图片"
append-to-body
>
<div v-if="editForm.imageList?.length > 0">
<div style="border: 1px solid #ddd">
<ImageUploader
ref="imageUploaderRef"
v-model="editForm.imageList"
:is-more-check="false"
:is-show-upload="false"
@list-change="listHandleChange"
/>
</div>
</div>
<template #footer>
<span class="dialog-footer">
<el-button @click="singleCancelBtn">取消</el-button>
<el-button type="primary" @click="singleImageSelectSave">
确定
</el-button>
</span>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { type FormInstance, ElMessage } from 'element-plus'
const formRef = shallowRef<FormInstance>()
import { CloseBold } from '@element-plus/icons-vue'
import Draggable from 'vuedraggable'
import wangEditor from '@/components/WangEditor.vue'
import ImageUploader from '../components/ImageUploader.vue'
const imageUploaderRef = shallowRef<InstanceType<typeof ImageUploader>>()
import LogisticsWaySelect from '../../logistics/components/LogisticsWaySelect.tsx'
import {
getByCateIdApi,
createProductApi,
updateProductApi,
updateStatusApi,
} from '@/api/product'
import CustomizeTable from '@/components/VxeTable.tsx'
const tableRef = ref<InstanceType<typeof CustomizeTable> | null>(null)
import { TableColumn } from '@/components/VxeTable'
import { defineModel, defineEmits, defineExpose, defineProps } from 'vue'
import {
ProductDetail,
ImageInfo,
SkuPropertyValue,
SkuProperty,
InterSkuArr,
ProductSku,
CurrencyType,
InterProductType,
} from '@/types/api/product'
import { IAllList } from '@/types/api/podUsOrder'
const visible = defineModel<boolean>('visible', { default: false })
const emits = defineEmits(['success'])
const props = defineProps({
categoryTree: {
type: Array,
required: true,
},
productTypeList: {
type: Array as () => InterProductType[],
required: true,
},
craftList: {
type: Array as () => IAllList[],
required: true,
},
currencyList: {
type: Array as () => CurrencyType[],
required: true,
},
})
const PRINT_TYPE_OPTIONS = [
{ label: '局部印', value: 1, type: 'warning' },
{ label: '代发普货', value: 2, type: 'danger' },
] as const
const createDefaultForm = (
overrides?: Partial<ProductDetail>,
): ProductDetail => ({
id: null,
name: '',
userMark: null,
category_id: null,
material: '',
print_type: null,
product_no: '',
currency_code: '',
currency_name: '',
product_type: 'platform',
title: '',
craftIds: [],
processing: true,
cnRemark: '',
imageList: [],
sizeList: [],
color_images: '',
img_url: '',
skuProperties: [],
productList: [],
factory_price: null,
sort: null,
property1_cate_id: null,
property1_enname: '',
property2_cate_id: null,
property2_enname: '',
weight: null,
namespace: '',
...overrides, // 支持部分覆盖
})
const editForm = reactive<ProductDetail>(createDefaultForm())
const formRules = reactive({
name: [
{
required: true,
message: '请输入商品名称',
trigger: 'blur',
},
],
title: [
{
required: true,
message: '请输入Title',
trigger: 'blur',
},
],
category_id: [
{
required: true,
message: '请输入商品类目',
trigger: 'change',
},
],
print_type: [
{
required: true,
message: '请输入商品类型',
trigger: 'change',
},
],
sales_price: [
{
required: true,
message: '请输入销售价格',
trigger: 'blur',
},
{
pattern: /^(\d+)?(\.\d+)?$/,
message: '只能输入数字',
},
],
weight: [
{
required: true,
message: '请输入商品克重',
trigger: 'blur',
},
],
origin_code: [
{
required: true,
message: '请选择产地',
trigger: 'change',
},
],
product_type: [
{
required: true,
message: '请选择商品归属',
trigger: 'change',
},
],
})
const titleMap = {
add: '新增',
edit: '编辑',
look: '查看详情',
success: '注册完成',
confirm: '供应链确定',
} as const
type ModeType = keyof typeof titleMap
const modeTitle = ref<ModeType>('add')
const isAddOrEdit = computed(() => ['add', 'edit'].includes(modeTitle.value))
const isLookOrConfirm = computed(() =>
['look', 'confirm'].includes(modeTitle.value),
)
import ImageView from '@/components/ImageView.vue'
const popupTitle = computed(() => titleMap[modeTitle.value as ModeType])
import skuAttibutePopup from '@/components/skuAttibute.vue'
const attrVisible = ref<boolean>(false)
const skuAttibuteRef = shallowRef<InstanceType<typeof skuAttibutePopup>>()
const proSkuOptions = ref<SkuProperty[]>([])
const categoryChange = async (id: number) => {
const res = await getByCateIdApi(id)
if (res.code === 200) {
proSkuOptions.value = res.data || []
const skuProperties = editForm?.skuProperties || []
getSkuarr(skuProperties)
} else {
proSkuOptions.value = []
}
}
const skuArr = ref<InterSkuArr[]>([]) // 回显属性用的数组
const getSkuarr = (skuProperties: SkuProperty[]) => {
if (!skuProperties || skuProperties.length === 0) {
skuArr.value = []
return
}
const skuArrs = proSkuOptions.value.map((option: SkuProperty) => {
const matched = skuProperties.find((item) => item.id === option.id)
const obj: InterSkuArr = {
id: option.id,
enname: option.enname,
name: `${option.cnname}(${option.enname})`,
listCode: [],
valueList: [],
selected: 0,
}
if (matched?.valueList && option.valueList) {
matched.valueList.forEach((matchedValue: SkuPropertyValue) => {
if (matchedValue?.code) {
obj.listCode.push(matchedValue.code)
obj.valueList.push(matchedValue)
}
})
option.valueList.forEach((optionValue) => {
const isSelected = matched.valueList.some(
(matchedValue) => matchedValue.id === optionValue.id,
)
if (isSelected) {
optionValue.selected = 1
}
})
}
return obj
})
skuArr.value = skuArrs || []
}
const addOption = async () => {
if (!editForm.category_id) {
ElMessage.error('请选择商品类目')
return
}
if (!editForm.name) {
ElMessage.error('请先填写商品名称')
return
}
attrVisible.value = true
skuAttibuteRef.value?.open('edit')
}
import {
cartesianProduct,
emptyArray,
cloneDeep,
validateSkuItems,
generateColorImages,
finalizeSkuItems,
calculateMinWeight,
checkUpdateParams,
checkDataChange,
} from '@/utils/product'
const MAX_CATE = 3
const saveOption = () => {
/* 1. 校验分类 */
const skuArrs = skuArr.value.filter((v: InterSkuArr) => v.listCode.length > 0)
if (!skuArrs.length) {
editForm.productList = []
return ElMessage.error('请选择分类')
}
if (skuArrs.length > MAX_CATE) return ElMessage.error('分类选择不得大于3类')
const checkList: SkuPropertyValue[][] = skuArrs.map(
(v: InterSkuArr) => v.valueList,
)
const cartesianList = cartesianProduct<SkuPropertyValue>(checkList)
/* 2. 提前建索引,加速后续匹配 */
const existMap: Map<string, ProductSku> = new Map(
editForm.productList.map((r: ProductSku) => [r.sku, r]), // 已存在 SKU
)
/* 3. 生成新列表 */
const newList = cartesianList.map((codes, idx) => {
const baseSku = popupTitle.value === '编辑' ? editForm.sku : 'SKU'
const item = {
sort: idx + 1,
sku: baseSku + codes.map((c: SkuPropertyValue) => `_${c.code}`).join(''),
sku_name:
editForm.name +
codes.map((c: SkuPropertyValue) => `_${c.cnname}`).join(''),
sku_weight: null,
reg_count: null,
} as ProductSku
/* 3.1 属性字段一次性写入 */
codes.forEach((c: SkuPropertyValue, j: number) => {
;(item as unknown as Record<string, unknown>)[`property${j + 1}_id`] =
c.id
;(item as unknown as Record<string, unknown>)[`option_enname${j + 1}`] =
c.enname
;(item as unknown as Record<string, unknown>)[`custom_value${j + 1}`] =
c.enname
;(item as unknown as Record<string, unknown>)[`property_code${j + 1}`] =
c.code
;(item as unknown as Record<string, unknown>)[
`property_cate_id${j + 1}`
] = skuArrs[j].id
})
/* 3.3 若已存在,保留 id 等信息 */
const old = existMap.get(item.sku)
const typedItem = item as ProductSku
if (old) {
typedItem.id = old.id // 保留主键
typedItem.sku_weight = old.sku_weight // 关键:旧重量优先
return { ...old, ...typedItem } // 其余字段用新数据
}
return typedItem
})
/* 4. 统一写回 + 价格兜底 */
editForm.productList = newList.map((it, idx) => ({
...it,
sort: idx + 1,
factory_price: it.factory_price ?? editForm.factory_price,
sales_price: it.sales_price ?? editForm.sales_price,
}))
attrVisible.value = false
}
const setOptions = (payload: { skuArr: InterSkuArr[] }) => {
skuArr.value = JSON.parse(JSON.stringify(payload.skuArr))
}
const skuClose = () => {}
const tableSkuArr = computed(() => {
return skuArr.value.filter((v: InterSkuArr) => {
return v.listCode.length > 0
})
})
const tableConfig = computed<TableColumn[]>(() => {
const baseColumns: TableColumn[] = [
{
prop: 'sku_name',
label: 'SKU名称',
attrs: { align: 'center' },
},
{
prop: 'sku',
label: 'SKU',
attrs: { align: 'center' },
},
{
prop: 'image',
label: 'SKU图片',
attrs: { align: 'center', width: 80 },
render: {
default: ({ row }: { row: ProductSku }) => {
return h(ImageView, {
src: row.image,
alt: 'SKU图片',
})
},
},
},
]
const skuProps = tableSkuArr.value.map(
(item: InterSkuArr, index: number) => ({
prop: `custom_value${index + 1}`,
label: item.enname,
attrs: { align: 'center' },
}),
)
const endColumns: TableColumn[] = [
{
prop: 'factory_price',
label: '',
attrs: { align: 'center', width: 250 },
render: {
header: () => [
'对外报价 ',
h(
ElSelect,
{
modelValue: editForm.currency_code,
'onUpdate:modelValue': (val: string) => {
editForm.currency_code = val
const item = props.currencyList.find(
(c: CurrencyType) => c.currencyCode === val,
)
if (item) editForm.currency_name = item.currencyName
},
placeholder: '币种',
style: 'width: 150px',
},
() =>
props.currencyList.map((c: CurrencyType) =>
h(ElOption, {
key: c.currencyCode,
label: c.currencyName,
value: c.currencyCode,
}),
),
),
],
},
},
{
prop: 'sku_weight',
label: '重量',
attrs: { align: 'center', width: 80 },
},
{
prop: 'reg_count',
label: '注册件数',
attrs: { align: 'center', width: 100 },
},
]
return [...baseColumns, ...skuProps, ...endColumns]
})
const tableSelection = ref<ProductSku[]>([])
// 定义字段值类型
type UpdatableField = 'sku_weight' | 'reg_count'
type PriceFields = 'factory_price' | 'sales_price'
const updateProductInfoBtn = async (content: string) => {
if (!tableSelection.value.length) return ElMessage.error('请选择要更新的商品')
type FieldOrArray = UpdatableField | PriceFields[]
const fieldMap: Record<string, FieldOrArray> = {
商品重量: 'sku_weight',
注册件数: 'reg_count',
对外报价: ['factory_price', 'sales_price'],
}
const fieldOrArray = fieldMap[content]
try {
const { value } = await ElMessageBox.prompt('', `批量更新${content}`, {
confirmButtonText: '确定',
cancelButtonText: '取消',
inputPlaceholder: `请输入${content}`,
inputPattern: /.+/,
inputErrorMessage: `${content}不能为空`,
})
tableSelection.value.forEach((item: ProductSku) => {
if (typeof fieldOrArray === 'string') {
// 单字段更新
;(item as unknown as Record<string, unknown>)[fieldOrArray] =
Number(value)
} else {
// 数组,同时更新两个价格字段
fieldOrArray.forEach((field) => {
;(item as unknown as Record<string, unknown>)[field] = Number(value)
})
}
})
} catch (error) {
// 用户取消
}
}
const singleImagesVisible = ref(false)
const singleSetVariantImage = () => {
if (emptyArray(tableSelection.value)) return
singleImagesVisible.value = true
}
const singleImage = ref<(string | null)[]>([])
const listHandleChange = (list: (string | null)[]) => {
singleImage.value = list
}
const singleImageSelectSave = () => {
if (Object.keys(singleImage.value).length === 0) {
return ElMessage.warning('请勾选图片')
}
tableSelection.value.forEach((p: ProductSku) => {
p.image = singleImage.value[0] as string
})
singleImagesVisible.value = false
imageUploaderRef.value?.clear()
}
const singleCancelBtn = () => {
singleImagesVisible.value = false
imageUploaderRef.value?.clear()
}
function handleCheckboxRecords(value: ProductSku[]) {
tableSelection.value = value
}
const handleChange = (list: ImageInfo[]) => {
console.log('图片列表变化', list)
}
const dragChange = (i: number) => {
const skuArrItem: InterSkuArr = skuArr.value[i]
const sortMap: { [key: string]: number } = {}
skuArrItem.listCode = skuArrItem.valueList
.map((e) => e.code)
.filter((code) => code !== null)
skuArrItem.valueList.forEach((item, index) => {
const code = item.code ?? 'defaultCode' // 如果 item.code 为 null,则使用 'defaultCode'
item.sort = index + 1
sortMap[code] = item.sort
})
const skuPropItem = proSkuOptions.value.find(
(e: SkuProperty) => e.enname === skuArrItem.enname,
)
if (skuPropItem) {
skuPropItem.valueList.sort(
(a: SkuPropertyValue, b: SkuPropertyValue) =>
sortMap[a.code] - sortMap[b.code],
)
skuPropItem.valueList.forEach((item: SkuPropertyValue, index: number) => {
item.sort = index + 1
})
}
sortSkuItemList()
}
const sortSkuItemList = () => {
// 构建每个属性维度的排序映射:{ code: sortIndex }
const skuArrSort: Array<Record<string, number>> = skuArr.value.map(
(item: InterSkuArr) => {
const map: Record<string, number> = {}
item.listCode.forEach((code, i) => {
if (code !== null) map[code] = i
})
return map
},
)
const propList = [
'property_code1',
'property_code2',
'property_code3',
] as const
editForm.productList.sort((a: ProductSku, b: ProductSku) => {
for (let i = 0; i < propList.length; i++) {
const prop = propList[i]
const aVal = a[prop as keyof ProductSku] as string | null | undefined
const bVal = b[prop as keyof ProductSku] as string | null | undefined
if (aVal == null && bVal == null) continue
if (aVal == null) return -1 // null 排前面
if (bVal == null) return 1
const sortA = skuArrSort[i]?.[aVal] ?? Number.MAX_SAFE_INTEGER
const sortB = skuArrSort[i]?.[bVal] ?? Number.MAX_SAFE_INTEGER
const diff = sortA - sortB
if (diff !== 0) return diff
}
return 0
})
editForm.productList.forEach((item: ProductSku, i: number) => {
item.sort = i + 1
})
}
const variantsIndex = ref<string | undefined>(undefined)
const optionSelection = (row: SkuPropertyValue) => {
variantsIndex.value = row.code
const list = editForm.productList
let arr: ProductSku[] = []
arr = list.filter(
(item: ProductSku) =>
item.property_code1 == row.code ||
item.property_code2 == row.code ||
item.property_code3 == row.code,
)
if (!arr.length) return
// 清除之前的勾选
tableRef.value?.clearCheckbox()
// arr是选中的表格行,此时表格高亮并滚动到arr的第一行,表格也勾选了
tableSelection.value = arr
// 滚动到第一行并高亮
tableRef.value?.scrollToRow(arr[0])
// 勾选所有匹配行
arr.forEach((item) => {
tableRef.value?.setCheckboxRow(item, true)
})
}
const optionDelete = (i: number, code: string) => {
const arr: SkuPropertyValue[] = []
const arrCode: string[] = []
proSkuOptions.value[i].valueList.map((v: SkuPropertyValue) => {
if (v.code == code) v.selected = 0
else if (v.selected !== 0) {
arrCode.push(v.code)
arr.push(v)
}
})
skuArr.value[i].valueList = arr
skuArr.value[i].listCode = arrCode
saveOption()
}
// 编辑时:用传入的数据覆盖
const getFormDetaild = (data: ProductDetail) => {
Object.assign(editForm, createDefaultForm(data)) // data 会覆盖默认值
originalForm.value = cloneDeep(editForm) // 保存快照
categoryChange(editForm.category_id as number)
}
const open = async (key: ModeType = 'add', data?: ProductDetail) => {
modeTitle.value = key
if (key === 'add') {
Object.assign(editForm, createDefaultForm())
formRef.value?.clearValidate()
skuArr.value = []
} else if (data) {
getFormDetaild(data)
}
}
const originalForm = ref<ProductDetail | null>(null)
const submitProduct = async () => {
// 1. 表单验证
await formRef.value?.validate()
// 2. 深拷贝原始数据(避免污染响应式源)
const formCopy = cloneDeep(toRaw(editForm))
const obj: ProductDetail = formCopy
// 3. 处理首图
obj.img_url = obj.img_url || obj.imageList[0]?.image_url || ''
// 4. 标题默认值
obj.title = obj.title || obj.name
// 5. 构建 skuProperties(基于当前选中的属性值)
const skuProperties = buildSkuProperties()
obj.skuProperties = skuProperties
// 6. 动态添加 propertyX_cate_id / propertyX_enname
assignPropertyMetadata(obj)
// 7. 准备商品列表与图片列表的深拷贝
if (!obj.productList) {
ElMessage.error('请添加销售属性')
}
const productListCopy = cloneDeep(obj.productList)
const imageListCopy = cloneDeep(obj.imageList)
// 8. 收集颜色与图片映射(用于 color_images)
const colorImageMap = buildColorImageMap()
// 9. 校验所有 SKU 并同时收集错误信息
const errs = validateSkuItems(productListCopy)
if (errs.length) {
ElMessage.error(errs.join('\n')) // 换行显示
return
}
const minPrice = productListCopy.reduce((min, item) => {
const price = Number(item.factory_price)
if (!isNaN(price)) {
return price < min ? price : min
}
return min
}, Infinity)
obj.factory_price = minPrice // 历史价格字段最小值
// 10. 校验币种
if (!obj.currency_code) {
ElMessage.error('请选择对外报价的币种')
return
}
// 11. 生成 color_images
obj.color_images = generateColorImages(colorImageMap)
// 12. 处理图片排序(如果 imageList 需要按 sort 字段)
obj.imageList = imageListCopy
.sort((a, b) => (a.sort ?? 0) - (b.sort ?? 0)) // 按 sort 升序,缺失时视为 0
.map((e: ImageInfo) => ({
image_url: e.image_url,
sort: e.sort,
id: e.id,
}))
// 13. 商品列表最终处理:排序等
finalizeSkuItems(productListCopy)
obj.productList = productListCopy
// 14. 计算最小重量
obj.weight = calculateMinWeight(productListCopy)
let res
if (['edit'].includes(modeTitle.value)) {
const keyMap = {
productList: 'id',
imageList: 'id',
sizeList: 'id',
}
const customChanges = checkUpdateParams(
obj,
originalForm.value,
'id',
keyMap,
true,
)
const shouldSubmit = checkDataChange(customChanges)
if (shouldSubmit) {
res = await updateProductApi({
...customChanges!, // 非空断言(或 ?? {})
id: editForm.id, // 覆盖/补充 id
})
} else {
return ElMessage.warning('未检测到数据变更,无需提交')
}
} else {
obj.namespace = 'factory'
res = await createProductApi(obj)
}
if (res.code === 200) {
visible.value = false
emits('success')
ElMessage.success('提交成功')
} else {
ElMessage.error(res.message)
}
}
// 公共更新状态方法
const updateStatus = async (remark: string | null) => {
try {
const res = await updateStatusApi({
ids: String(editForm.id), // 确保字符串类型
status: '-10', // 根据业务确定状态码,或作为参数传入
remark,
})
if (res.code === 200) {
visible.value = false
emits('success')
ElMessage.success(res.message)
} else {
ElMessage.error(res.message)
}
} catch (error) {
console.error(error)
ElMessage.error('操作失败,请稍后重试')
}
}
// 通过(无需原因)
const passBtn = async () => {
await updateStatus(null)
}
// 驳回(需要输入原因)
const failBtn = async () => {
try {
const { value } = await ElMessageBox.prompt('', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
inputPlaceholder: '请输入驳回原因',
inputPattern: /.+/,
inputErrorMessage: '请输入驳回原因',
})
await updateStatus(value)
} catch {
// 用户取消输入,不执行任何操作
}
}
/** 构建 skuProperties(基于当前选中的属性值) */
function buildSkuProperties() {
const result: SkuProperty[] = []
for (const iterator of tableSkuArr.value) {
const current = proSkuOptions.value.find(
(item: SkuProperty) => item.id === iterator.id,
)
if (current) {
// 保留当前选中的值列表
current.valueList = current.valueList.filter((val: SkuPropertyValue) =>
iterator.listCode.includes(val.code),
)
result.push(cloneDeep(current)) // 克隆避免修改原数据
}
}
return result
}
/** 动态添加属性分类字段(property1_cate_id, property1_enname ...) */
function assignPropertyMetadata(obj: ProductDetail) {
let idx = 1
for (const item of skuArr.value) {
if (item.valueList?.length) {
;(obj as unknown as Record<string, unknown>)[`property${idx}_cate_id`] =
item.id
;(obj as unknown as Record<string, unknown>)[`property${idx}_enname`] =
item.enname
idx++
}
}
}
/** 构建颜色与图片的映射(仅用于去重生成 color_images) */
function buildColorImageMap() {
const colorMap: Record<string, string> = {}
const colorProperty = skuArr.value.find((item: InterSkuArr) =>
item.enname?.toLowerCase().includes('color'),
)
if (!colorProperty) return {}
for (const item of editForm.productList) {
const code1 = item.property_code1
const code2 = item.property_code2
// 判断哪个属性是颜色(通常 property_code1 或 property_code2 对应颜色)
if (code1 && colorProperty.listCode.includes(code1) && !colorMap[code1]) {
colorMap[code1] = item.image
}
if (code2 && colorProperty.listCode.includes(code2) && !colorMap[code2]) {
colorMap[code2] = item.image
}
}
return colorMap
}
defineExpose({
open,
})
</script>
<style scoped lang="scss">
@use './create.scss';
</style>
<template>
<ElDialog
v-model="visible"
title="生成入库单"
width="70%"
:close-on-click-modal="false"
>
<div class="dialog-form">
<ElForm
ref="editFormRef"
:model="editForm"
:rules="rules"
inline
label-width="90px"
>
<ElFormItem label="入库单号" prop="account">
<ElInput v-model.trim="editForm.inNo" clearable disabled />
</ElFormItem>
<ElFormItem label="工厂:" prop="factoryCode">
<span>{{ editForm.factoryCode }}</span>
</ElFormItem>
<ElFormItem label="仓库" prop="warehouseId" required>
<ElSelect
v-model="editForm.warehouseId"
clearable
:disabled="formId"
placeholder="请选择仓库"
style="width: 160px"
@change="handleWarehouseChange(editForm.warehouseId)"
>
<ElOption
v-for="item in warehouseList"
:key="item.id"
:label="item.name"
:value="item.id"
></ElOption>
</ElSelect>
</ElFormItem>
<ElFormItem label="备注" prop="remark" style="width: 45%">
<ElInput
v-model.trim="editForm.remark"
placeholder="请输入备注"
clearable
/>
</ElFormItem>
</ElForm>
<div
style="
height: 250px;
width: 100%;
padding: 0;
box-sizing: border-box;
border: 1px solid #ddd;
overflow: hidden;
"
>
<CustomizeTable
ref="tableRef"
v-model="editForm.productList"
:config="tableConfig"
highlight-current-row
border="full"
@get-checkbox-records="productSelectionChange"
></CustomizeTable>
</div>
</div>
<template #footer>
<span class="dialog-footer">
<el-button @click="visible = false">取消</el-button>
<el-button type="primary" @click="submitProduct">确认</el-button>
</span>
</template>
</ElDialog>
</template>
<script setup lang="ts">
import { defineModel, defineEmits, defineProps, defineExpose } from 'vue'
const visible = defineModel<boolean>('visible', { default: false })
const emits = defineEmits(['success'])
import { InterReceiptItem, ProductSku } from '@/types/api/product'
import { warehouseInfo } from '@/api/warehouse'
const props = defineProps({
warehouseList: {
type: Array as () => warehouseInfo[],
required: true,
},
})
const createDefaultForm = (
overrides?: Partial<InterReceiptItem>,
): InterReceiptItem => ({
inNo: '',
warehouseId: '',
warehouseName: '',
remark: '',
factoryCode: '',
factoryId: 0,
productList: [],
...overrides, // 支持部分覆盖
})
const editForm = reactive<InterReceiptItem>(createDefaultForm())
const submitProduct = () => {
visible.value = false
emits('success')
}
const open = (data: ProductSku) => {
visible.value = true
editForm.productList = data
}
import CustomizeTable from '@/components/VxeTable.tsx'
import type { VxeTablePropTypes } from 'vxe-table'
const tableRef = ref<InstanceType<typeof CustomizeTable> | null>(null)
import { TableColumn } from '@/components/VxeTable'
import ImageView from '@/components/ImageView.vue'
const tableConfig = computed<TableColumn[]>(() => {
const baseColumns: TableColumn[] = [
{
prop: 'image',
label: 'SKU图片',
attrs: { align: 'center', width: 80 },
render: {
default: ({ row }: { row: ProductSku }) => {
return h(ImageView, {
src: row.image,
alt: 'SKU图片',
})
},
},
},
{
prop: 'sku_name',
label: '库存SKU',
attrs: { align: 'center' },
},
{
prop: 'sku',
label: '商品名称',
attrs: { align: 'center' },
},
{
prop: 'buyStored',
label: '入库数量',
attrs: { align: 'center', width: 120 },
// render: {
// default: ({ row }: { row: ProductSku }) =>
// h(ElInput, {
// modelValue: row.buyStored,
// 'onUpdate:modelValue': (val: number) => {
// row.buyStored = val
// // 触发 setCostPrice 逻辑
// setCostPrice(row)
// },
// placeholder: '入库数量',
// style: 'width: 120px',
// clearable: true,
// size: 'small',
// }),
// },
},
{
prop: 'currencyName',
label: '币种',
attrs: { align: 'center', width: 80 },
},
{
prop: 'costPrice',
label: '成本价',
attrs: { align: 'center', width: 100 },
},
{
prop: 'totalPrice',
label: '总成本',
attrs: { align: 'center', width: 100 },
},
{
prop: 'locationCode',
label: '库位',
attrs: { align: 'center', width: 120 },
// render: {
// default: ({ row }: { row: ProductSku }) =>
// h(
// ElSelect,
// {
// modelValue: row.locationId,
// 'onUpdate:modelValue': (val: number) => {
// row.locationId = val
// handleLocationChange(val, row)
// },
// clearable: true,
// placeholder: '请输入库位',
// style: 'width: 120px',
// filterable: true,
// },
// () =>
// locationList.value.map((item) =>
// h(ElOption, {
// key: item.locationId,
// label: item.locationCode,
// value: item.locationId,
// }),
// ),
// ),
// },
},
{
prop: 'userMark',
label: '所属客户',
attrs: { align: 'center', width: 100 },
},
// {
// prop: 'remark',
// label: '备注',
// attrs: { align: 'center', width: 140, showOverflowTooltip: true },
// render: {
// default: ({ row }: { row: ProductSku }) =>
// h(ElInput, {
// modelValue: row.remark,
// 'onUpdate:modelValue': (val: string) => (row.remark = val),
// clearable: true,
// size: 'small',
// }),
// },
// },
]
return baseColumns
})
const handleWarehouseChange = (val: number | string | undefined) => {
const found = props.warehouseList.find(
(item: warehouseInfo) => item.id === val,
)
editForm.warehouseName = found ? found.name : ''
}
const receiptTableSelection = ref([])
const productSelectionChange = (v) => {
receiptTableSelection.value = v
}
defineExpose({
open,
})
</script>
<style scoped lang="scss">
.dialog-footer {
margin-top: 10px;
width: 100%;
display: flex;
justify-content: center;
}
</style>
.left {
width: 160px;
:deep(.el-tree-node__content) {
height: 30px;
line-height: 30px;
}
:deep(.el-tree-node__label) {
font-size: 13px;
cursor: pointer;
display: inline-block;
width: 100%;
color: black !important;
padding: 3px 7px;
}
:deep(.el-tree-node__expand-icon) {
display: none;
}
:deep(.is-current) {
.tree-node-label,
.tree-node-count {
background-color: #ecf5ff;
color: #409eff !important;
}
.el-tree-node__children {
.tree-node-label,
.tree-node-count {
background-color: transparent !important;
color: black !important;
}
}
}
}
.tree-node {
display: flex;
color: #333;
font-weight: 500;
}
.right {
flex: 1;
flex-shrink: 0;
background: white;
overflow: hidden;
}
.card-list {
display: grid;
grid-template-columns: repeat(6, 1fr);
grid-template-rows: max-content;
gap: 10px;
height: 100%;
overflow-y: auto;
.card-list-item {
cursor: pointer;
.flex-between {
display: flex;
justify-content: space-between;
align-items: center;
.images-position {
display: flex;
height: 30px;
gap: 10px;
.item-image {
width: 30px;
height: 30px;
border: 1px solid #909399;
cursor: pointer;
img {
width: 100%;
height: 100%;
object-fit: cover;
}
}
}
b {
margin-right: 5px;
font-size: 15px;
}
}
.grid-container {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 10px;
font-size: 12px;
margin-top: 10px;
.grid-item {
display: flex;
overflow: hidden;
}
.tag-position {
justify-content: flex-end;
}
}
}
}
.pagination {
display: flex;
justify-content: center;
align-items: center;
gap: 15px;
margin: 10px 0;
:deep(.el-pagination) {
margin: 0 !important;
}
.total {
color: #606266;
font-size: 15px;
}
.pageSize {
line-height: 39px;
color: #606266;
font-size: 15px;
}
}
<template>
<div class="page card h-100 flex-gap-10 overflow-hidden flex">
<div class="left">
{{ nodeCode }}
<ElTree
ref="treeRef"
default-expand-all
:expand-on-click-node="false"
:default-expanded-keys="[]"
:highlight-current="true"
node-key="status"
:data="treeData"
:props="{ children: 'children', label: 'remark' }"
@node-click="nodeClick"
>
<template #default="{ data }">
<div class="tree-node">
<div class="tree-node-label">{{ data.remark }}</div>
<div v-if="data.count || data.count === 0" class="tree-node-count">
{{ `(${data.count})` }}
</div>
</div>
</template>
</ElTree>
</div>
<div class="right">
<div class="delivery-note-page flex-column card h-100 overflow-hidden">
<el-tabs
v-if="
nodeCode != null &&
['38', '33', '35', '40'].includes(String(nodeCode))
"
v-model="activeSuspendTab"
class="demo-tabs"
@tab-click="handTabClick"
>
<el-tab-pane
v-for="label in suspendTabs"
:key="label.status"
:label="`${label.remark} (${label.count || 0})`"
:name="label.status"
>
</el-tab-pane>
</el-tabs>
<div class="header-filter-form">
<ElForm ref="queryParamsRef" :model="queryParams" inline>
<el-form-item label="商品名称">
<el-input
v-model="queryParams.name"
clearable
maxlength="40"
style="width: 150px"
show-word-limit
placeholder="请输入商品名称"
/>
</el-form-item>
<el-form-item label="SKU">
<el-input
v-model="queryParams.sku"
clearable
style="width: 150px"
placeholder="请输入sku"
/>
</el-form-item>
<el-form-item label="商品类型">
<el-select
v-model="queryParams.print_type"
style="width: 150px"
clearable
placeholder="请选择"
>
<el-option
v-for="item in PRINT_TYPE_OPTIONS"
:key="item.value"
:label="item.label"
:value="item.value"
></el-option>
</el-select>
</el-form-item>
<el-form-item label="商品类目">
<el-cascader
ref="categoryCascader"
v-model="queryParams.category_id"
:options="categoryTree"
clearable
style="width: 150px"
:props="{
label: 'name',
value: 'id',
emitPath: false, // 只返回最后一级 id
checkStrictly: true, // ← 关键:允许点击任意一级
}"
:show-all-levels="false"
></el-cascader>
</el-form-item>
<el-form-item label="商品归属" prop="product_type">
<el-select
v-model="queryParams.product_type"
clearable
style="width: 150px"
>
<el-option
v-for="item in productTypeList"
:key="item.code"
:label="item.name"
:value="item.code"
></el-option>
</el-select>
</el-form-item>
<ElFormItem>
<ElButton type="primary" @click="searchCard"> 查询 </ElButton>
</ElFormItem>
<ElFormItem>
<ElButton @click="resetSearchForm()"> 重置 </ElButton>
</ElFormItem>
<ElFormItem v-if="['-20', '-10'].includes(nodeCode)">
<ElButton type="success" @click="addProductInfo()">
新增
</ElButton>
</ElFormItem>
<ElFormItem v-if="['-20'].includes(nodeCode)">
<ElButton
type="warning"
@click="
stateReasonBtn('将选中的商品转至挂起状态', '挂起原因', 33)
"
>
挂起
</ElButton>
</ElFormItem>
<ElFormItem
v-if="
['38', '33', '35'].includes(nodeCode) &&
['35'].includes(String(activeSuspendTab))
"
>
<ElButton type="success" @click="stateTransitionBtn('取消挂起')">
取消挂起
</ElButton>
</ElFormItem>
<ElFormItem v-if="['38', '33', '35'].includes(nodeCode)">
<ElButton
type="warning"
@click="
stateReasonBtn(
'对选中的商品进行取消注册操作',
'取消原因',
-10,
)
"
>
取消注册
</ElButton>
</ElFormItem>
<!-- v-if="['10,20,30,1'].includes(nodeCode)" -->
<ElFormItem>
<ElButton type="success" @click="generateWarehouseReceiptBtn">
生成入库单
</ElButton>
</ElFormItem>
</ElForm>
</div>
<div class="delivery-note-content flex-1 flex-column overflow-hidden">
<div class="card-wrapper flex-1 flex-column overflow-hidden">
<div v-if="tableData.length > 0" class="card-list">
<div
v-for="cardItem in tableData"
:key="cardItem.id"
class="card-list-item"
@click="cardClick(cardItem)"
@mouseleave="handleChangeImages(null, cardItem)"
>
<CommonCard
:card-item="cardItem"
:active="isSelectStatused(cardItem)"
:show-sku="false"
:show-product-info="false"
:image-field="'img_url'"
@contextmenu.prevent="(v: MouseEvent) => rightClick(v)"
>
<template #operations>
<img
title="操作日志"
width="26"
height="26"
src="@/assets/images/log.png"
alt=""
@click="operationLog(cardItem.id)"
/>
<img
style="margin: 0 5px"
title="查看详情"
width="28"
height="28"
src="@/assets/images/preview.png"
alt=""
@click="obtainProductInfoBtn(cardItem.id, 'look')"
/>
<img
v-if="['-20', '-10'].includes(nodeCode)"
title="编辑"
width="26"
height="26"
src="@/assets/images/edit.png"
alt=""
@click="obtainProductInfoBtn(cardItem.id, 'edit')"
/>
<img
v-if="['-20'].includes(nodeCode)"
style="margin: 0 5px"
title="供应链确认"
width="26"
height="26"
src="@/assets/images/registration.png"
alt=""
@click="obtainProductInfoBtn(cardItem.id, 'confirm')"
/>
</template>
<template #images>
<div class="flex-between">
<div
v-if="cardItem.colorImageList"
class="images-position"
>
<div
v-for="(item, index) in cardItem.colorImageList"
:key="index"
:title="item"
class="item-image"
@mousemove="handleChangeImages(item, cardItem)"
>
<img
:src="item"
height="28"
@click="handlePictureCardPreview(item)"
/>
</div>
</div>
</div>
</template>
<template #info>
<div class="grid-container">
<div class="grid-item">
{{ cardItem.create_time }}
</div>
</div>
<div class="grid-container">
<div class="grid-item">
{{ cardItem.sku }}
</div>
<div class="grid-item tag-position">
<el-tag :type="printTag(cardItem.print_type).type">
{{ printTag(cardItem.print_type).label }}
</el-tag>
</div>
</div>
</template>
</CommonCard>
</div>
</div>
<div v-else class="empty">暂无数据</div>
</div>
<div class="pagination">
<div class="total">
<span
>已选择
<span style="color: red">{{ cardSelection.length }}</span>
条数据</span
>
</div>
<ElPagination
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:page-sizes="[50, 100, 200, 300, 400, 500]"
background
layout="total, sizes, prev, pager, next, jumper"
:total="total"
style="margin: 10px auto 0; text-align: right"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
></ElPagination>
</div>
</div>
</div>
</div>
<createProduct
ref="shopRef"
v-model:visible="createVisible"
:category-tree="categoryTree"
:product-type-list="productTypeList"
:currency-list="currencyList"
:craft-list="craftList"
@success="loadTreeData"
@close="createVisible = false"
/>
</div>
<receiptOrder
ref="receiptRef"
v-model:visible="receiptVisible"
:warehouse-list="warehouseList"
@success="loadTreeData"
@close="receiptVisible = false"
/>
<RightClickMenu
ref="rightMenuRef"
:show-copy-count="false"
:show-copy-sub-shop-number="false"
@on-change="rightChange"
>
<!-- <template #default>
<div class="menu-item" @click="rightChange('order-number')">
复制订单号
</div>
<div class="menu-item" @click="rightChange('factorySubOrderNumber')">
复制生产单号
</div>
</template> -->
</RightClickMenu>
<el-dialog v-model="dialogVisible" width="35%">
<img :src="dialogImageUrl" alt="商品预览图片" />
</el-dialog>
<el-dialog
v-model="logVisible"
title="操作日志"
width="1000px"
:close-on-click-modal="false"
>
<LogList :log-list="logList" />
</el-dialog>
</template>
<script setup lang="ts">
import {
getStatusCountApi,
getCardListPageApi,
getLogListApi,
getByIdApi,
updateStatusApi,
} from '@/api/product'
import {
IntercategoryTree,
InterSearchCriteria,
InterCategoryNode,
InterCardItem,
InterCraftItem,
CurrencyType,
ProcessTypeData,
InterProductType,
} from '@/types/api/product'
import {
clearNonEmptyChildren,
findNodeById,
processType,
} from '@/utils/product'
import { ElTree } from 'element-plus'
import type { TabsPaneContext } from 'element-plus'
const treeRef = ref<InstanceType<typeof ElTree>>()
const treeData = ref<IntercategoryTree[]>()
const rawTreeData = ref<IntercategoryTree[]>([])
const nodeCode = ref<string>(
sessionStorage.getItem('Product_NodeCode') || '-20',
)
import { useValue } from '@/utils/hooks/useValue'
import usePageList from '@/utils/hooks/usePageList'
const [queryParams, resetSearchForm] = useValue<InterSearchCriteria>({
name: '',
sku: '',
print_type: null,
category_id: null,
product_type: '',
diyUserId: null,
})
const {
currentPage,
pageSize,
total,
data: tableData,
refresh: searchCard,
onCurrentPageChange: handleCurrentChange,
onPageSizeChange: handleSizeChange,
} = usePageList({
query: (page, pageSize) => {
return getCardListPageApi(
{ ...queryParams.value, status: nodeCode.value },
page,
pageSize,
).then((res) => {
return res.data
})
},
})
const loadTreeData = async () => {
try {
const res = await getStatusCountApi()
rawTreeData.value = res.data
// 生成渲染数据:将所有有值的 children 清空
treeData.value = clearNonEmptyChildren(res.data)
await nextTick(() => {
treeRef.value!.setCurrentKey(nodeCode.value, true)
searchCard()
})
} catch (e) {
console.error(e)
}
}
import { LogListData } from '@/types/api/podCnOrder'
const logList = ref<LogListData[]>([])
const logVisible = ref(false)
const operationLog = async (id: number) => {
try {
const res = await getLogListApi(id)
if (res.code !== 200) return
logList.value = res.data
logVisible.value = true
} catch (e) {
console.error(e)
}
}
const cardSelection = ref<InterCardItem[]>([])
const cardClick = (data: InterCardItem) => {
const status = isSelectStatused(data)
if (status) {
cardSelection.value = cardSelection.value.filter(
(item: InterCardItem) => item.id !== data.id,
)
} else {
cardSelection.value.push(data as InterCardItem)
}
}
const isSelectStatused = (data: InterCardItem) => {
const index = cardSelection.value.findIndex(
(item: InterCardItem) => item.id === data.id,
)
return index !== -1
}
const currentImage = ref('')
const handleChangeImages = (item: string | null, cardItem: InterCardItem) => {
currentImage.value = cardItem?.img_url || ''
}
const dialogVisible = ref(false)
const dialogImageUrl = ref('')
const handlePictureCardPreview = (fileUrl: string) => {
dialogImageUrl.value = fileUrl
dialogVisible.value = true
}
import RightClickMenu from '@/components/RightClickMenu.vue'
const rightMenuRef = ref()
const rightClick = (e: MouseEvent) => {
rightMenuRef.value.setPosition({
x: e.clientX,
y: e.clientY,
})
}
const rightChange = async (code: string) => {
console.log('code', code)
}
import { useOptions } from '@/utils/product'
const { getOptions } = useOptions()
const categoryTree = ref<InterCategoryNode[]>([])
const productTypeList = ref<InterProductType[]>([
{ name: '平台共享', code: 'platform' },
{ name: '客户私有', code: 'customer' },
])
const PRINT_TYPE_OPTIONS = [
{ label: '局部印', value: 1, type: 'warning' },
{ label: '代发普货', value: 2, type: 'danger' },
] as const
const printTag = computed(() => (val: number | string) => {
const numVal = typeof val === 'string' ? parseInt(val, 10) : val
const result = PRINT_TYPE_OPTIONS.find((item) => item.value === numVal)
return result || { label: '未知', type: 'danger' }
})
import { IAllList } from '@/types/api/podUsOrder'
const craftList = ref<IAllList[]>([])
const processTypeMap = processType.value.reduce(
(acc: Record<string, string>, cur: ProcessTypeData) => {
acc[cur.value] = cur.label
return acc
},
{},
)
const currencyList = ref<CurrencyType[]>([])
onMounted(async () => {
loadTreeData()
const result = await getOptions(['category', 'craft', 'currency'])
categoryTree.value = result.category
const data: InterCraftItem[] = result.craft
craftList.value = data.map((item) => ({
id: item.id,
name: item.craft_name,
warehouseName: processTypeMap[item.craft_type] ?? '其他',
})) as IAllList[]
currencyList.value = result.currency
})
const suspendTabs = ref<IntercategoryTree[]>([])
const activeSuspendTab = ref<string>('33')
const nodeClick = async (data: IntercategoryTree) => {
sessionStorage.setItem('Product_NodeCode', String(data.status))
// 如果状态为38(挂起),从原始树数据中查找该节点并获取其 children去渲染右侧的tabs
if (data.status === 38) {
const targetNode = findNodeById(rawTreeData.value, 38)
if (targetNode && targetNode.children) {
suspendTabs.value = targetNode.children
activeSuspendTab.value = String(suspendTabs.value[0]?.status) || '33' // 默认选中第一个子节点
} else {
console.warn('未找到 id 为 38 的节点或其没有子节点')
}
}
cardSelection.value = []
const statusValue =
data.status != 38 ? data.status : activeSuspendTab.value ?? 0
nodeCode.value = String(statusValue)
currentPage.value = 1
searchCard()
}
const handTabClick = (tab: TabsPaneContext) => {
nodeCode.value = String(tab.paneName)
currentPage.value = 1
searchCard()
}
import createProduct from './components/createProduct.vue'
const shopRef = shallowRef<InstanceType<typeof createProduct>>()
const createVisible = ref(false)
const addProductInfo = () => {
createVisible.value = true
shopRef.value?.open('add')
}
const obtainProductInfoBtn = async (id: number, type: string) => {
const loading = ElLoading.service({
lock: true,
text: '加载中...',
background: 'rgba(0, 0, 0, 0.7)',
})
try {
const res = await getByIdApi(id as number)
if (res.code === 200) {
createVisible.value = true
shopRef.value?.open(type, res.data)
}
} catch (error) {
ElMessage.error('加载数据失败,请重试')
} finally {
loading.close()
}
}
const stateTransitionBtn = async (name: string, toStatus?: number) => {
if (cardSelection.value.length == 0) {
return ElMessage.warning('请选择商品')
}
try {
await ElMessageBox.confirm(`确定对选中的商品进行${name}操作?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
} catch (e) {
return
}
const res = await updateStatusApi({
ids: cardSelection.value.toString(),
status: toStatus,
remark: null,
})
if (res.code === 200) {
loadTreeData()
cardSelection.value = []
ElMessage.success('提交成功')
} else {
ElMessage.error(res.message)
}
}
const stateReasonBtn = async (
name: string,
reason: string,
toStatus: number,
) => {
if (cardSelection.value.length == 0) {
return ElMessage.warning('请选择商品')
}
ElMessageBox.prompt(`确定${name}?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
inputPlaceholder: `请输入${reason}`,
inputPattern: /.+/,
inputErrorMessage: `${reason}不能为空`,
}).then(async ({ value }: { value: string }) => {
const res = await updateStatusApi({
ids: cardSelection.value.toString(),
status: toStatus,
remark: value,
})
if (res.code === 200) {
loadTreeData()
cardSelection.value = []
ElMessage.success('提交成功')
} else {
ElMessage.error(res.message)
}
})
}
import { warehouseInfo } from '@/api/warehouse'
import receiptOrder from './components/receiptOrder.vue'
const receiptRef = shallowRef<InstanceType<typeof receiptOrder>>()
const receiptVisible = ref(false)
const warehouseList = ref<warehouseInfo[]>([])
const generateWarehouseReceiptBtn = async () => {
try {
const warehouseResult = await getOptions(['warehouse'])
warehouseList.value = warehouseResult.warehouse
const loading = ElLoading.service({
lock: true,
text: '加载中...',
background: 'rgba(0, 0, 0, 0.7)',
})
try {
const id = cardSelection.value[0].id
const res = await getByIdApi(id)
if (res.code === 200) {
const productList = res.data?.productList || []
receiptVisible.value = true
receiptRef.value?.open?.(productList)
}
} catch (err) {
console.error(err)
ElMessage.error('加载数据失败,请重试')
} finally {
loading.close()
}
} catch (err) {
console.error(err)
ElMessage.error('获取仓库列表失败,请重试')
}
}
</script>
<style lang="scss" scoped>
@use './product.scss';
</style>
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment