Merge branch 'dev' of https://gitee.com/yudaocode/yudao-ui-admin-vben into dev
This commit is contained in:
@@ -95,7 +95,7 @@ export function getOtaTaskRecordStatusStatistics(
|
||||
taskId?: number,
|
||||
) {
|
||||
return requestClient.get<Record<string, number>>(
|
||||
'/iot/ota/task/record/status-statistics',
|
||||
'/iot/ota/task/record/get-status-statistics',
|
||||
{ params: { firmwareId, taskId } },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -18,8 +18,7 @@ const routes: RouteRecordRaw[] = [
|
||||
title: '产品详情',
|
||||
activePath: '/iot/device/product',
|
||||
},
|
||||
component: () =>
|
||||
import('#/views/iot/product/product/modules/detail/index.vue'),
|
||||
component: () => import('#/views/iot/product/product/modules/detail/index.vue'),
|
||||
},
|
||||
{
|
||||
path: 'device/detail/:id',
|
||||
@@ -28,11 +27,20 @@ const routes: RouteRecordRaw[] = [
|
||||
title: '设备详情',
|
||||
activePath: '/iot/device/device',
|
||||
},
|
||||
component: () =>
|
||||
import('#/views/iot/device/device/modules/detail/index.vue'),
|
||||
component: () => import('#/views/iot/device/device/modules/detail/index.vue'),
|
||||
},
|
||||
{
|
||||
path: 'ota/firmware/detail/:id',
|
||||
name: 'IoTOtaFirmwareDetail',
|
||||
meta: {
|
||||
title: '固件详情',
|
||||
activePath: '/iot/ota',
|
||||
},
|
||||
component: () => import('#/views/iot/ota/modules/firmware-detail/index.vue'),
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export default routes;
|
||||
|
||||
|
||||
@@ -76,7 +76,7 @@ export function useFormSchema(): VbenFormSchema[] {
|
||||
label: '接收的用户',
|
||||
component: 'ApiSelect',
|
||||
componentProps: {
|
||||
api: getSimpleUserList,
|
||||
api: () => getSimpleUserList(),
|
||||
labelField: 'nickname',
|
||||
valueField: 'id',
|
||||
mode: 'multiple',
|
||||
|
||||
@@ -17,7 +17,7 @@ export function useGridFormSchema(): VbenFormSchema[] {
|
||||
label: '告警配置',
|
||||
component: 'ApiSelect',
|
||||
componentProps: {
|
||||
api: getSimpleAlertConfigList,
|
||||
api: () => getSimpleAlertConfigList(),
|
||||
labelField: 'name',
|
||||
valueField: 'id',
|
||||
placeholder: '请选择告警配置',
|
||||
@@ -40,7 +40,7 @@ export function useGridFormSchema(): VbenFormSchema[] {
|
||||
label: '产品',
|
||||
component: 'ApiSelect',
|
||||
componentProps: {
|
||||
api: getSimpleProductList,
|
||||
api: () => getSimpleProductList(),
|
||||
labelField: 'name',
|
||||
valueField: 'id',
|
||||
placeholder: '请选择产品',
|
||||
@@ -53,7 +53,7 @@ export function useGridFormSchema(): VbenFormSchema[] {
|
||||
label: '设备',
|
||||
component: 'ApiSelect',
|
||||
componentProps: {
|
||||
api: getSimpleDeviceList,
|
||||
api: () => getSimpleDeviceList(),
|
||||
labelField: 'deviceName',
|
||||
valueField: 'id',
|
||||
placeholder: '请选择设备',
|
||||
|
||||
@@ -28,7 +28,7 @@ export function useFormSchema(): VbenFormSchema[] {
|
||||
label: '产品',
|
||||
component: 'ApiSelect',
|
||||
componentProps: {
|
||||
api: getSimpleProductList,
|
||||
api: () => getSimpleProductList(),
|
||||
labelField: 'name',
|
||||
valueField: 'id',
|
||||
placeholder: '请选择产品',
|
||||
@@ -89,7 +89,7 @@ export function useFormSchema(): VbenFormSchema[] {
|
||||
label: '设备分组',
|
||||
component: 'ApiSelect',
|
||||
componentProps: {
|
||||
api: getSimpleDeviceGroupList,
|
||||
api: () => getSimpleDeviceGroupList(),
|
||||
labelField: 'name',
|
||||
valueField: 'id',
|
||||
mode: 'multiple',
|
||||
@@ -156,7 +156,7 @@ export function useGroupFormSchema(): VbenFormSchema[] {
|
||||
label: '设备分组',
|
||||
component: 'ApiSelect',
|
||||
componentProps: {
|
||||
api: getSimpleDeviceGroupList,
|
||||
api: () => getSimpleDeviceGroupList(),
|
||||
labelField: 'name',
|
||||
valueField: 'id',
|
||||
mode: 'multiple',
|
||||
@@ -199,7 +199,7 @@ export function useGridFormSchema(): VbenFormSchema[] {
|
||||
label: '产品',
|
||||
component: 'ApiSelect',
|
||||
componentProps: {
|
||||
api: getSimpleProductList,
|
||||
api: () => getSimpleProductList(),
|
||||
labelField: 'name',
|
||||
valueField: 'id',
|
||||
placeholder: '请选择产品',
|
||||
@@ -249,7 +249,7 @@ export function useGridFormSchema(): VbenFormSchema[] {
|
||||
label: '设备分组',
|
||||
component: 'ApiSelect',
|
||||
componentProps: {
|
||||
api: getSimpleDeviceGroupList,
|
||||
api: () => getSimpleDeviceGroupList(),
|
||||
labelField: 'name',
|
||||
valueField: 'id',
|
||||
placeholder: '请选择设备分组',
|
||||
|
||||
@@ -481,6 +481,10 @@ onMounted(async () => {
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
:deep(.vxe-toolbar div) {
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* 隐藏 VxeGrid 自带的搜索表单区域 */
|
||||
:deep(.vxe-grid--form-wrapper) {
|
||||
display: none !important;
|
||||
|
||||
@@ -7,7 +7,6 @@ import { downloadFileFromBlobPart } from '@vben/utils';
|
||||
import { message } from 'ant-design-vue';
|
||||
|
||||
import { importDeviceTemplate } from '#/api/iot/device/device';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useImportFormSchema } from '../data';
|
||||
|
||||
@@ -65,7 +64,7 @@ const [Modal, modalApi] = useVbenModal({
|
||||
const result = await response.json();
|
||||
|
||||
if (result.code !== 0) {
|
||||
message.error(result.msg || $t('ui.actionMessage.operationFailed'));
|
||||
message.error(result.msg || '导入失败');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -95,7 +94,7 @@ const [Modal, modalApi] = useVbenModal({
|
||||
await modalApi.close();
|
||||
emit('success');
|
||||
} catch (error: any) {
|
||||
message.error(error.message || $t('ui.actionMessage.operationFailed'));
|
||||
message.error(error.message || '导入失败');
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ export function useFormSchema(): VbenFormSchema[] {
|
||||
label: '父级分组',
|
||||
component: 'ApiTreeSelect',
|
||||
componentProps: {
|
||||
api: getSimpleDeviceGroupList,
|
||||
api: () => getSimpleDeviceGroupList(),
|
||||
labelField: 'name',
|
||||
valueField: 'id',
|
||||
placeholder: '请选择父级分组',
|
||||
|
||||
@@ -29,7 +29,7 @@ export function useFormSchema(): VbenFormSchema[] {
|
||||
label: '所属产品',
|
||||
component: 'ApiSelect',
|
||||
componentProps: {
|
||||
api: getSimpleProductList,
|
||||
api: () => getSimpleProductList(),
|
||||
labelField: 'name',
|
||||
valueField: 'id',
|
||||
placeholder: '请选择产品',
|
||||
@@ -85,7 +85,7 @@ export function useGridFormSchema(): VbenFormSchema[] {
|
||||
label: '产品',
|
||||
component: 'ApiSelect',
|
||||
componentProps: {
|
||||
api: getSimpleProductList,
|
||||
api: () => getSimpleProductList(),
|
||||
labelField: 'name',
|
||||
valueField: 'id',
|
||||
placeholder: '请选择产品',
|
||||
|
||||
@@ -29,7 +29,7 @@ export function useFormSchema(): VbenFormSchema[] {
|
||||
label: '所属产品',
|
||||
component: 'ApiSelect',
|
||||
componentProps: {
|
||||
api: getSimpleProductList,
|
||||
api: () => getSimpleProductList(),
|
||||
labelField: 'name',
|
||||
valueField: 'id',
|
||||
placeholder: '请选择产品',
|
||||
@@ -86,7 +86,7 @@ export function useGridFormSchema(): VbenFormSchema[] {
|
||||
label: '产品',
|
||||
component: 'ApiSelect',
|
||||
componentProps: {
|
||||
api: getSimpleProductList,
|
||||
api: () => getSimpleProductList(),
|
||||
labelField: 'name',
|
||||
valueField: 'id',
|
||||
placeholder: '请选择产品',
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { IoTOtaFirmwareApi } from '#/api/iot/ota/firmware';
|
||||
|
||||
import { Page, useVbenModal } from '@vben/common-ui';
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import { message } from 'ant-design-vue';
|
||||
|
||||
@@ -16,6 +17,8 @@ import { useGridColumns, useGridFormSchema } from './data';
|
||||
|
||||
defineOptions({ name: 'IoTOtaFirmware' });
|
||||
|
||||
const { push } = useRouter();
|
||||
|
||||
const [FormModal, formModalApi] = useVbenModal({
|
||||
connectedComponent: Form,
|
||||
destroyOnClose: true,
|
||||
@@ -36,10 +39,6 @@ function handleEdit(row: IoTOtaFirmwareApi.Firmware) {
|
||||
formModalApi.setData({ type: 'update', id: row.id }).open();
|
||||
}
|
||||
|
||||
/** 查看固件详情 */
|
||||
function handleDetail(row: IoTOtaFirmwareApi.Firmware) {
|
||||
formModalApi.setData({ type: 'view', id: row.id }).open();
|
||||
}
|
||||
|
||||
/** 删除固件 */
|
||||
async function handleDelete(row: IoTOtaFirmwareApi.Firmware) {
|
||||
@@ -58,6 +57,11 @@ async function handleDelete(row: IoTOtaFirmwareApi.Firmware) {
|
||||
}
|
||||
}
|
||||
|
||||
/** 查看固件详情 */
|
||||
function handleDetail(row: IoTOtaFirmwareApi.Firmware) {
|
||||
push({ name: 'IoTOtaFirmwareDetail', params: { id: row.id } });
|
||||
}
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
schema: useGridFormSchema(),
|
||||
@@ -113,18 +117,19 @@ const [Grid, gridApi] = useVbenVxeGrid({
|
||||
|
||||
<!-- 固件文件列 -->
|
||||
<template #fileUrl="{ row }">
|
||||
<a
|
||||
v-if="row.fileUrl"
|
||||
:href="row.fileUrl"
|
||||
target="_blank"
|
||||
download
|
||||
class="text-primary cursor-pointer hover:underline"
|
||||
>
|
||||
<IconifyIcon icon="ant-design:download-outlined" class="mr-1" />
|
||||
下载固件
|
||||
</a>
|
||||
<span v-else class="text-gray-400">无文件</span>
|
||||
</template>
|
||||
<div v-if="row.fileUrl" class="inline-flex items-center gap-1.5 align-middle leading-none">
|
||||
<IconifyIcon icon="ant-design:download-outlined" class="shrink-0 text-base align-middle text-primary" />
|
||||
<a
|
||||
:href="row.fileUrl"
|
||||
target="_blank"
|
||||
download
|
||||
class="text-primary cursor-pointer hover:underline align-middle"
|
||||
>
|
||||
下载固件
|
||||
</a>
|
||||
</div>
|
||||
<span v-else class="text-gray-400">无文件</span>
|
||||
</template>
|
||||
|
||||
<!-- 操作列 -->
|
||||
<template #actions="{ row }">
|
||||
|
||||
@@ -59,8 +59,8 @@ onMounted(() => {
|
||||
<template>
|
||||
<div class="p-4">
|
||||
<!-- 固件信息 -->
|
||||
<Card title="固件信息" class="mb-5" :loading="firmwareLoading">
|
||||
<Descriptions :column="3" bordered>
|
||||
<Card title="固件信息" class="mb-3" :loading="firmwareLoading">
|
||||
<Descriptions :column="3" bordered size="small">
|
||||
<Descriptions.Item label="固件名称">
|
||||
{{ firmware?.name }}
|
||||
</Descriptions.Item>
|
||||
@@ -86,15 +86,15 @@ onMounted(() => {
|
||||
<!-- 升级设备统计 -->
|
||||
<Card
|
||||
title="升级设备统计"
|
||||
class="mb-5"
|
||||
class="mb-3"
|
||||
:loading="firmwareStatisticsLoading"
|
||||
>
|
||||
<Row :gutter="20" class="py-5">
|
||||
<Row :gutter="20" class="py-3">
|
||||
<Col :span="6">
|
||||
<div
|
||||
class="rounded border border-solid border-gray-200 bg-gray-50 p-5 text-center"
|
||||
class="rounded border border-solid border-gray-200 bg-gray-50 p-3 text-center"
|
||||
>
|
||||
<div class="mb-2 text-3xl font-bold text-blue-500">
|
||||
<div class="mb-1 text-3xl font-bold text-blue-500">
|
||||
{{
|
||||
Object.values(firmwareStatistics).reduce(
|
||||
(sum: number, count) => sum + (count || 0),
|
||||
@@ -107,9 +107,9 @@ onMounted(() => {
|
||||
</Col>
|
||||
<Col :span="3">
|
||||
<div
|
||||
class="rounded border border-solid border-gray-200 bg-gray-50 p-5 text-center"
|
||||
class="rounded border border-solid border-gray-200 bg-gray-50 p-3 text-center"
|
||||
>
|
||||
<div class="mb-2 text-3xl font-bold text-gray-400">
|
||||
<div class="mb-1 text-3xl font-bold text-gray-400">
|
||||
{{
|
||||
firmwareStatistics[IoTOtaTaskRecordStatusEnum.PENDING.value] ||
|
||||
0
|
||||
@@ -120,9 +120,9 @@ onMounted(() => {
|
||||
</Col>
|
||||
<Col :span="3">
|
||||
<div
|
||||
class="rounded border border-solid border-gray-200 bg-gray-50 p-5 text-center"
|
||||
class="rounded border border-solid border-gray-200 bg-gray-50 p-3 text-center"
|
||||
>
|
||||
<div class="mb-2 text-3xl font-bold text-blue-400">
|
||||
<div class="mb-1 text-3xl font-bold text-blue-400">
|
||||
{{
|
||||
firmwareStatistics[IoTOtaTaskRecordStatusEnum.PUSHED.value] || 0
|
||||
}}
|
||||
@@ -132,9 +132,9 @@ onMounted(() => {
|
||||
</Col>
|
||||
<Col :span="3">
|
||||
<div
|
||||
class="rounded border border-solid border-gray-200 bg-gray-50 p-5 text-center"
|
||||
class="rounded border border-solid border-gray-200 bg-gray-50 p-3 text-center"
|
||||
>
|
||||
<div class="mb-2 text-3xl font-bold text-yellow-500">
|
||||
<div class="mb-1 text-3xl font-bold text-yellow-500">
|
||||
{{
|
||||
firmwareStatistics[
|
||||
IoTOtaTaskRecordStatusEnum.UPGRADING.value
|
||||
@@ -146,9 +146,9 @@ onMounted(() => {
|
||||
</Col>
|
||||
<Col :span="3">
|
||||
<div
|
||||
class="rounded border border-solid border-gray-200 bg-gray-50 p-5 text-center"
|
||||
class="rounded border border-solid border-gray-200 bg-gray-50 p-3 text-center"
|
||||
>
|
||||
<div class="mb-2 text-3xl font-bold text-green-500">
|
||||
<div class="mb-1 text-3xl font-bold text-green-500">
|
||||
{{
|
||||
firmwareStatistics[IoTOtaTaskRecordStatusEnum.SUCCESS.value] ||
|
||||
0
|
||||
@@ -159,9 +159,9 @@ onMounted(() => {
|
||||
</Col>
|
||||
<Col :span="3">
|
||||
<div
|
||||
class="rounded border border-solid border-gray-200 bg-gray-50 p-5 text-center"
|
||||
class="rounded border border-solid border-gray-200 bg-gray-50 p-3 text-center"
|
||||
>
|
||||
<div class="mb-2 text-3xl font-bold text-red-500">
|
||||
<div class="mb-1 text-3xl font-bold text-red-500">
|
||||
{{
|
||||
firmwareStatistics[IoTOtaTaskRecordStatusEnum.FAILURE.value] ||
|
||||
0
|
||||
@@ -172,9 +172,9 @@ onMounted(() => {
|
||||
</Col>
|
||||
<Col :span="3">
|
||||
<div
|
||||
class="rounded border border-solid border-gray-200 bg-gray-50 p-5 text-center"
|
||||
class="rounded border border-solid border-gray-200 bg-gray-50 p-3 text-center"
|
||||
>
|
||||
<div class="mb-2 text-3xl font-bold text-gray-400">
|
||||
<div class="mb-1 text-3xl font-bold text-gray-400">
|
||||
{{
|
||||
firmwareStatistics[IoTOtaTaskRecordStatusEnum.CANCELED.value] ||
|
||||
0
|
||||
|
||||
@@ -34,7 +34,7 @@ export function useFormSchema(): VbenFormSchema[] {
|
||||
label: '父级分类',
|
||||
component: 'ApiTreeSelect',
|
||||
componentProps: {
|
||||
api: getSimpleProductCategoryList,
|
||||
api: () => getSimpleProductCategoryList(),
|
||||
labelField: 'name',
|
||||
valueField: 'id',
|
||||
placeholder: '请选择父级分类',
|
||||
|
||||
@@ -93,7 +93,7 @@ export function useFormSchema(formApi?: any): VbenFormSchema[] {
|
||||
label: '产品分类',
|
||||
component: 'ApiSelect',
|
||||
componentProps: {
|
||||
api: getSimpleProductCategoryList,
|
||||
api: () => getSimpleProductCategoryList(),
|
||||
labelField: 'name',
|
||||
valueField: 'id',
|
||||
placeholder: '请选择产品分类',
|
||||
@@ -246,7 +246,7 @@ export function useBasicFormSchema(formApi?: any): VbenFormSchema[] {
|
||||
label: '产品分类',
|
||||
component: 'ApiSelect',
|
||||
componentProps: {
|
||||
api: getSimpleProductCategoryList,
|
||||
api: () => getSimpleProductCategoryList(),
|
||||
labelField: 'name',
|
||||
valueField: 'id',
|
||||
placeholder: '请选择产品分类',
|
||||
|
||||
@@ -333,6 +333,10 @@ onMounted(() => {
|
||||
</Page>
|
||||
</template>
|
||||
<style scoped>
|
||||
:deep(.vxe-toolbar div) {
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* 隐藏 VxeGrid 自带的搜索表单区域 */
|
||||
:deep(.vxe-grid--form-wrapper) {
|
||||
display: none !important;
|
||||
|
||||
@@ -24,7 +24,7 @@ export function useGridFormSchema(): VbenFormSchema[] {
|
||||
label: '产品',
|
||||
component: 'ApiSelect',
|
||||
componentProps: {
|
||||
api: getSimpleProductList,
|
||||
api: () => getSimpleProductList(),
|
||||
labelField: 'name',
|
||||
valueField: 'id',
|
||||
placeholder: '请选择产品',
|
||||
|
||||
@@ -4,7 +4,6 @@ import { DeliveryTypeEnum, DICT_TYPE } from '@vben/constants';
|
||||
import { getDictOptions } from '@vben/hooks';
|
||||
import { handleTree } from '@vben/utils';
|
||||
|
||||
import { z } from '#/adapter/form';
|
||||
import { getSimpleBrandList } from '#/api/mall/product/brand';
|
||||
import { getCategoryList } from '#/api/mall/product/category';
|
||||
import { getSimpleTemplateList } from '#/api/mall/trade/delivery/expressTemplate';
|
||||
@@ -33,7 +32,6 @@ export function useInfoFormSchema(): VbenFormSchema[] {
|
||||
{
|
||||
fieldName: 'categoryId',
|
||||
label: '分类名称',
|
||||
// component: 'ApiCascader',
|
||||
component: 'ApiTreeSelect',
|
||||
componentProps: {
|
||||
api: async () => {
|
||||
@@ -285,7 +283,7 @@ export function useOtherFormSchema(): VbenFormSchema[] {
|
||||
componentProps: {
|
||||
min: 0,
|
||||
},
|
||||
rules: z.number().min(0).optional().default(0),
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'giveIntegral',
|
||||
@@ -294,7 +292,7 @@ export function useOtherFormSchema(): VbenFormSchema[] {
|
||||
componentProps: {
|
||||
min: 0,
|
||||
},
|
||||
rules: z.number().min(0).optional().default(0),
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'virtualSalesCount',
|
||||
@@ -303,7 +301,7 @@ export function useOtherFormSchema(): VbenFormSchema[] {
|
||||
componentProps: {
|
||||
min: 0,
|
||||
},
|
||||
rules: z.number().min(0).optional().default(0),
|
||||
rules: 'required',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||
import type { MallSpuApi } from '#/api/mall/product/spu';
|
||||
|
||||
// TODO @puhui999:这个是不是 api 后端有定义类似的?如果是,是不是放到 api 哈?
|
||||
export interface PropertyAndValues {
|
||||
id: number;
|
||||
name: string;
|
||||
@@ -23,12 +24,8 @@ export interface RuleConfig {
|
||||
message: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获得商品的规格列表 - 商品相关的公共函数
|
||||
*
|
||||
* @param spu
|
||||
* @return PropertyAndValues 规格列表
|
||||
*/
|
||||
// TODO @puhui999:这个是只有 index.ts 在用么?还是别的模块也会用
|
||||
/** 获得商品的规格列表 - 商品相关的公共函数 */
|
||||
const getPropertyList = (spu: MallSpuApi.Spu): PropertyAndValues[] => {
|
||||
// 直接拿返回的 skus 属性逆向生成出 propertyList
|
||||
const properties: PropertyAndValues[] = [];
|
||||
@@ -62,4 +59,5 @@ const getPropertyList = (spu: MallSpuApi.Spu): PropertyAndValues[] => {
|
||||
export { getPropertyList };
|
||||
|
||||
// 导出组件
|
||||
// TODO @puhui999:如果 sku-list.vue 要对外,可以考虑在 spu 下面,搞个 components 模块;(目前看,别的模块应该会用到哈。);modules 是当前模块用到的,components 是跨模块要用到的。
|
||||
export { default as SkuList } from './modules/sku-list.vue';
|
||||
|
||||
@@ -8,7 +8,7 @@ import { useRoute } from 'vue-router';
|
||||
|
||||
import { Page, useVbenModal } from '@vben/common-ui';
|
||||
import { useTabs } from '@vben/hooks';
|
||||
import { convertToInteger, floatToFixed2, formatToFraction } from '@vben/utils';
|
||||
import { convertToInteger, formatToFraction } from '@vben/utils';
|
||||
|
||||
import { Button, Card, message } from 'ant-design-vue';
|
||||
|
||||
@@ -31,11 +31,6 @@ const spuId = ref<number>();
|
||||
const { params, name } = useRoute();
|
||||
const { closeCurrentTab } = useTabs();
|
||||
const activeTabName = ref('info');
|
||||
|
||||
function onTabChange(key: string) {
|
||||
activeTabName.value = key;
|
||||
}
|
||||
|
||||
const tabList = ref([
|
||||
{
|
||||
key: 'info',
|
||||
@@ -58,44 +53,43 @@ const tabList = ref([
|
||||
tab: '其它设置',
|
||||
},
|
||||
]);
|
||||
// spu 表单数据
|
||||
const formData = ref<MallSpuApi.Spu>({
|
||||
name: '', // 商品名称
|
||||
categoryId: undefined, // 商品分类
|
||||
keyword: '', // 关键字
|
||||
picUrl: '', // 商品封面图
|
||||
sliderPicUrls: [], // 商品轮播图
|
||||
introduction: '', // 商品简介
|
||||
deliveryTypes: [], // 配送方式数组
|
||||
deliveryTemplateId: undefined, // 运费模版
|
||||
brandId: undefined, // 商品品牌
|
||||
specType: false, // 商品规格
|
||||
subCommissionType: false, // 分销类型
|
||||
skus: [
|
||||
{
|
||||
price: 0, // 商品价格
|
||||
marketPrice: 0, // 市场价
|
||||
costPrice: 0, // 成本价
|
||||
barCode: '', // 商品条码
|
||||
picUrl: '', // 图片地址
|
||||
stock: 0, // 库存
|
||||
weight: 0, // 商品重量
|
||||
volume: 0, // 商品体积
|
||||
firstBrokeragePrice: 0, // 一级分销的佣金
|
||||
secondBrokeragePrice: 0, // 二级分销的佣金
|
||||
},
|
||||
],
|
||||
description: '', // 商品详情
|
||||
sort: 0, // 商品排序
|
||||
giveIntegral: 0, // 赠送积分
|
||||
virtualSalesCount: 0, // 虚拟销量
|
||||
});
|
||||
const propertyList = ref<PropertyAndValues[]>([]); // 商品属性列表
|
||||
const formLoading = ref(true); // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
|
||||
const isDetail = ref(false); // 是否查看详情
|
||||
|
||||
const formLoading = ref(false); // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
|
||||
const isDetail = ref(name === 'ProductSpuDetail'); // 是否查看详情
|
||||
const skuListRef = ref(); // 商品属性列表 Ref
|
||||
|
||||
// sku 相关属性校验规则
|
||||
const formData = ref<MallSpuApi.Spu>({
|
||||
name: '',
|
||||
categoryId: undefined,
|
||||
keyword: '',
|
||||
picUrl: '',
|
||||
sliderPicUrls: [],
|
||||
introduction: '',
|
||||
deliveryTypes: [],
|
||||
deliveryTemplateId: undefined,
|
||||
brandId: undefined,
|
||||
specType: false,
|
||||
subCommissionType: false,
|
||||
skus: [
|
||||
{
|
||||
price: 0,
|
||||
marketPrice: 0,
|
||||
costPrice: 0,
|
||||
barCode: '',
|
||||
picUrl: '',
|
||||
stock: 0,
|
||||
weight: 0,
|
||||
volume: 0,
|
||||
firstBrokeragePrice: 0,
|
||||
secondBrokeragePrice: 0,
|
||||
},
|
||||
],
|
||||
description: '',
|
||||
sort: 0,
|
||||
giveIntegral: 0,
|
||||
virtualSalesCount: 0,
|
||||
}); // spu 表单数据
|
||||
const propertyList = ref<PropertyAndValues[]>([]); // 商品属性列表
|
||||
const ruleConfig: RuleConfig[] = [
|
||||
{
|
||||
name: 'stock',
|
||||
@@ -117,7 +111,7 @@ const ruleConfig: RuleConfig[] = [
|
||||
rule: (arg) => arg >= 0.01,
|
||||
message: '商品成本价格必须大于等于 0.00 元!!!',
|
||||
},
|
||||
];
|
||||
]; // sku 相关属性校验规则
|
||||
|
||||
const [InfoForm, infoFormApi] = useVbenForm({
|
||||
commonConfig: {
|
||||
@@ -146,11 +140,11 @@ const [SkuForm, skuFormApi] = useVbenForm({
|
||||
handleValuesChange: (values, fieldsChanged) => {
|
||||
if (fieldsChanged.includes('subCommissionType')) {
|
||||
formData.value.subCommissionType = values.subCommissionType;
|
||||
changeSubCommissionType();
|
||||
handleChangeSubCommissionType();
|
||||
}
|
||||
if (fieldsChanged.includes('specType')) {
|
||||
formData.value.specType = values.specType;
|
||||
onChangeSpec();
|
||||
handleChangeSpec();
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -199,7 +193,13 @@ const [OtherForm, otherFormApi] = useVbenForm({
|
||||
showDefaultActions: false,
|
||||
});
|
||||
|
||||
async function onSubmit() {
|
||||
/** tab 切换 */
|
||||
function handleTabChange(key: string) {
|
||||
activeTabName.value = key;
|
||||
}
|
||||
|
||||
/** 提交表单 */
|
||||
async function handleSubmit() {
|
||||
const values: MallSpuApi.Spu = await infoFormApi
|
||||
.merge(skuFormApi)
|
||||
.merge(deliveryFormApi)
|
||||
@@ -216,7 +216,7 @@ async function onSubmit() {
|
||||
return;
|
||||
}
|
||||
values.skus.forEach((item) => {
|
||||
// sku相关价格元转分
|
||||
// 金额转换:元转分
|
||||
item.price = convertToInteger(item.price);
|
||||
item.marketPrice = convertToInteger(item.marketPrice);
|
||||
item.costPrice = convertToInteger(item.costPrice);
|
||||
@@ -224,7 +224,7 @@ async function onSubmit() {
|
||||
item.secondBrokeragePrice = convertToInteger(item.secondBrokeragePrice);
|
||||
});
|
||||
}
|
||||
// 处理轮播图列表
|
||||
// 处理轮播图列表 TODO @puhui999:这个是必须的哇?
|
||||
const newSliderPicUrls: any[] = [];
|
||||
values.sliderPicUrls!.forEach((item: any) => {
|
||||
// 如果是前端选的图
|
||||
@@ -234,12 +234,13 @@ async function onSubmit() {
|
||||
});
|
||||
values.sliderPicUrls = newSliderPicUrls;
|
||||
|
||||
// 提交数据
|
||||
await (spuId.value ? updateSpu(values) : createSpu(values));
|
||||
}
|
||||
|
||||
/** 获得详情 */
|
||||
async function getDetail() {
|
||||
if (name === 'ProductSpuDetail') {
|
||||
if (isDetail.value) {
|
||||
isDetail.value = true;
|
||||
infoFormApi.setDisabled(true);
|
||||
skuFormApi.setDisabled(true);
|
||||
@@ -247,45 +248,36 @@ async function getDetail() {
|
||||
descriptionFormApi.setDisabled(true);
|
||||
otherFormApi.setDisabled(true);
|
||||
}
|
||||
const id = params.id as unknown as number;
|
||||
if (id) {
|
||||
try {
|
||||
const res = await getSpu(spuId.value!);
|
||||
res.skus?.forEach((item) => {
|
||||
if (isDetail.value) {
|
||||
item.price = floatToFixed2(item.price);
|
||||
item.marketPrice = floatToFixed2(item.marketPrice);
|
||||
item.costPrice = floatToFixed2(item.costPrice);
|
||||
item.firstBrokeragePrice = floatToFixed2(item.firstBrokeragePrice);
|
||||
item.secondBrokeragePrice = floatToFixed2(item.secondBrokeragePrice);
|
||||
} else {
|
||||
// 回显价格分转元
|
||||
item.price = formatToFraction(item.price);
|
||||
item.marketPrice = formatToFraction(item.marketPrice);
|
||||
item.costPrice = formatToFraction(item.costPrice);
|
||||
item.firstBrokeragePrice = formatToFraction(item.firstBrokeragePrice);
|
||||
item.secondBrokeragePrice = formatToFraction(
|
||||
item.secondBrokeragePrice,
|
||||
);
|
||||
}
|
||||
});
|
||||
formData.value = res;
|
||||
// 初始化各表单值(异步)
|
||||
infoFormApi.setValues(res);
|
||||
skuFormApi.setValues(res);
|
||||
deliveryFormApi.setValues(res);
|
||||
descriptionFormApi.setValues(res);
|
||||
otherFormApi.setValues(res);
|
||||
} finally {
|
||||
formLoading.value = false;
|
||||
}
|
||||
}
|
||||
// 将 SKU 的属性,整理成 PropertyAndValues 数组
|
||||
propertyList.value = getPropertyList(formData.value);
|
||||
formLoading.value = true;
|
||||
try {
|
||||
const res = await getSpu(spuId.value!);
|
||||
// 金额转换:元转分
|
||||
res.skus?.forEach((item) => {
|
||||
item.price = formatToFraction(item.price);
|
||||
item.marketPrice = formatToFraction(item.marketPrice);
|
||||
item.costPrice = formatToFraction(item.costPrice);
|
||||
item.firstBrokeragePrice = formatToFraction(item.firstBrokeragePrice);
|
||||
item.secondBrokeragePrice = formatToFraction(item.secondBrokeragePrice);
|
||||
});
|
||||
formData.value = res;
|
||||
// 初始化各表单值
|
||||
infoFormApi.setValues(res).then();
|
||||
skuFormApi.setValues(res).then();
|
||||
deliveryFormApi.setValues(res).then();
|
||||
descriptionFormApi.setValues(res).then();
|
||||
otherFormApi.setValues(res).then();
|
||||
// 将 SKU 的属性,整理成 PropertyAndValues 数组
|
||||
propertyList.value = getPropertyList(formData.value);
|
||||
} finally {
|
||||
formLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// =========== sku form 逻辑 ===========
|
||||
|
||||
/** 打开属性添加表单 */
|
||||
function openPropertyAddForm() {
|
||||
productPropertyAddFormApi.open();
|
||||
}
|
||||
@@ -296,7 +288,7 @@ function generateSkus(propertyList: any[]) {
|
||||
}
|
||||
|
||||
/** 分销类型 */
|
||||
function changeSubCommissionType() {
|
||||
function handleChangeSubCommissionType() {
|
||||
// 默认为零,类型切换后也要重置为零
|
||||
for (const item of formData.value.skus!) {
|
||||
item.firstBrokeragePrice = 0;
|
||||
@@ -305,10 +297,10 @@ function changeSubCommissionType() {
|
||||
}
|
||||
|
||||
/** 选择规格 */
|
||||
function onChangeSpec() {
|
||||
function handleChangeSpec() {
|
||||
// 重置商品属性列表
|
||||
propertyList.value = [];
|
||||
// 重置sku列表
|
||||
// 重置 sku 列表
|
||||
formData.value.skus = [
|
||||
{
|
||||
price: 0,
|
||||
@@ -325,7 +317,7 @@ function onChangeSpec() {
|
||||
];
|
||||
}
|
||||
|
||||
// 监听 sku form schema 变化,更新表单
|
||||
/** 监听 sku form schema 变化,更新表单 */
|
||||
watch(
|
||||
propertyList,
|
||||
() => {
|
||||
@@ -336,6 +328,7 @@ watch(
|
||||
{ deep: true },
|
||||
);
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(async () => {
|
||||
spuId.value = params.id as unknown as number;
|
||||
if (!spuId.value) {
|
||||
@@ -355,18 +348,18 @@ onMounted(async () => {
|
||||
:loading="formLoading"
|
||||
:tab-list="tabList"
|
||||
:active-key="activeTabName"
|
||||
@tab-change="onTabChange"
|
||||
@tab-change="handleTabChange"
|
||||
>
|
||||
<template #tabBarExtraContent>
|
||||
<Button type="primary" v-if="!isDetail" @click="onSubmit">
|
||||
<Button type="primary" v-if="!isDetail" @click="handleSubmit">
|
||||
保存
|
||||
</Button>
|
||||
<Button type="default" v-else @click="() => closeCurrentTab()">
|
||||
返回列表
|
||||
</Button>
|
||||
</template>
|
||||
<InfoForm class="w-3/5" v-show="activeTabName === 'info'" />
|
||||
|
||||
<InfoForm class="w-3/5" v-show="activeTabName === 'info'" />
|
||||
<SkuForm class="w-full" v-show="activeTabName === 'sku'">
|
||||
<template #singleSkuList>
|
||||
<SkuList
|
||||
@@ -418,6 +411,7 @@ onMounted(async () => {
|
||||
</div>
|
||||
</template>
|
||||
<style lang="scss" scoped>
|
||||
// TODO @puhui999:这个样式是必须的哇?
|
||||
:deep(.ant-tabs-tab-btn) {
|
||||
font-size: 14px !important;
|
||||
}
|
||||
|
||||
@@ -21,7 +21,6 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
isDetail: false,
|
||||
});
|
||||
|
||||
/** 输入框失去焦点或点击回车时触发 */
|
||||
const emit = defineEmits(['success']);
|
||||
|
||||
interface Props {
|
||||
@@ -30,12 +29,15 @@ interface Props {
|
||||
}
|
||||
|
||||
const inputValue = ref<string[]>([]); // 输入框值(tags 模式使用数组)
|
||||
const attributeIndex = ref<null | number>(null); // 获取焦点时记录当前属性项的index
|
||||
// 输入框显隐控制
|
||||
const attributeIndex = ref<null | number>(null); // 获取焦点时记录当前属性项的 index
|
||||
const inputVisible = computed(() => (index: number) => {
|
||||
if (attributeIndex.value === null) return false;
|
||||
if (attributeIndex.value === index) return true;
|
||||
});
|
||||
if (attributeIndex.value === null) {
|
||||
return false;
|
||||
}
|
||||
if (attributeIndex.value === index) {
|
||||
return true;
|
||||
}
|
||||
}); // 输入框显隐控制
|
||||
|
||||
interface InputRefItem {
|
||||
inputRef?: {
|
||||
@@ -46,7 +48,10 @@ interface InputRefItem {
|
||||
focus: () => void;
|
||||
}
|
||||
|
||||
const inputRef = ref<InputRefItem[]>([]); // 标签输入框Ref
|
||||
const inputRef = ref<InputRefItem[]>([]); // 标签输入框 Ref
|
||||
const attributeList = ref<PropertyAndValues[]>([]); // 商品属性列表
|
||||
const attributeOptions = ref<MallPropertyApi.PropertyValue[]>([]); // 商品属性值下拉框
|
||||
|
||||
/** 解决 ref 在 v-for 中的获取问题*/
|
||||
function setInputRef(el: any) {
|
||||
if (el === null || el === undefined) return;
|
||||
@@ -59,13 +64,13 @@ function setInputRef(el: any) {
|
||||
inputRef.value.push(el);
|
||||
}
|
||||
}
|
||||
const attributeList = ref<PropertyAndValues[]>([]); // 商品属性列表
|
||||
const attributeOptions = ref<MallPropertyApi.PropertyValue[]>([]); // 商品属性值下拉框
|
||||
|
||||
watch(
|
||||
() => props.propertyList,
|
||||
(data) => {
|
||||
if (!data) return;
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
attributeList.value = data;
|
||||
},
|
||||
{
|
||||
@@ -74,12 +79,12 @@ watch(
|
||||
},
|
||||
);
|
||||
|
||||
/** 删除属性值*/
|
||||
/** 删除属性值 */
|
||||
function handleCloseValue(index: number, valueIndex: number) {
|
||||
attributeList.value?.[index]?.values?.splice(valueIndex, 1);
|
||||
}
|
||||
|
||||
/** 删除属性*/
|
||||
/** 删除属性 */
|
||||
function handleCloseProperty(index: number) {
|
||||
attributeList.value?.splice(index, 1);
|
||||
emit('success', attributeList.value);
|
||||
@@ -93,7 +98,7 @@ async function showInput(index: number) {
|
||||
await getAttributeOptions(attributeList.value?.[index]?.id!);
|
||||
}
|
||||
|
||||
// 定义 success 事件,用于操作成功后的回调
|
||||
/** 定义 success 事件,用于操作成功后的回调 */
|
||||
async function handleInputConfirm(index: number, propertyId: number) {
|
||||
// 从数组中取最后一个输入的值(tags 模式下 inputValue 是数组)
|
||||
const currentValue = inputValue.value?.[inputValue.value.length - 1]?.trim();
|
||||
@@ -154,6 +159,7 @@ async function getAttributeOptions(propertyId: number) {
|
||||
|
||||
<template>
|
||||
<Col v-for="(item, index) in attributeList" :key="index">
|
||||
<!-- TODO @puhui999:1)间隙可以看看;2)vue3 + element-plus 添加属性这个按钮,是和属性名在一排,感觉更好看点。 -->
|
||||
<div>
|
||||
<span class="mx-1">属性名:</span>
|
||||
<Tag
|
||||
@@ -174,6 +180,7 @@ async function getAttributeOptions(propertyId: number) {
|
||||
class="mx-1"
|
||||
@close="handleCloseValue(index, valueIndex)"
|
||||
>
|
||||
<!-- TODO @puhui999:这里貌似爆红?!idea -->
|
||||
{{ value.name }}
|
||||
</Tag>
|
||||
<Select
|
||||
|
||||
@@ -35,7 +35,9 @@ const attributeOptions = ref<MallPropertyApi.Property[]>([]); // 商品属性名
|
||||
watch(
|
||||
() => props.propertyList,
|
||||
(data) => {
|
||||
if (!data) return;
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
attributeList.value = data as any[];
|
||||
},
|
||||
{
|
||||
@@ -44,7 +46,6 @@ watch(
|
||||
},
|
||||
);
|
||||
|
||||
// 表单配置
|
||||
const formSchema: VbenFormSchema[] = [
|
||||
{
|
||||
fieldName: 'name',
|
||||
@@ -62,7 +63,6 @@ const formSchema: VbenFormSchema[] = [
|
||||
showSearch: true,
|
||||
filterOption: true,
|
||||
placeholder: '请选择属性名称。如果不存在,可手动输入选择',
|
||||
// 支持手动输入新选项
|
||||
mode: 'tags',
|
||||
maxTagCount: 1,
|
||||
allowClear: true,
|
||||
@@ -71,7 +71,6 @@ const formSchema: VbenFormSchema[] = [
|
||||
},
|
||||
];
|
||||
|
||||
// 初始化表单
|
||||
const [Form, formApi] = useVbenForm({
|
||||
commonConfig: {
|
||||
componentProps: {
|
||||
@@ -85,16 +84,15 @@ const [Form, formApi] = useVbenForm({
|
||||
showDefaultActions: false,
|
||||
});
|
||||
|
||||
// 初始化弹窗
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
destroyOnClose: true,
|
||||
async onConfirm() {
|
||||
const { valid } = await formApi.validate();
|
||||
if (!valid) return;
|
||||
|
||||
if (!valid) {
|
||||
return;
|
||||
}
|
||||
const values = await formApi.getValues();
|
||||
const name = Array.isArray(values.name) ? values.name[0] : values.name;
|
||||
|
||||
// 重复添加校验
|
||||
for (const attrItem of attributeList.value) {
|
||||
if (attrItem.name === name) {
|
||||
@@ -103,6 +101,8 @@ const [Modal, modalApi] = useVbenModal({
|
||||
}
|
||||
}
|
||||
|
||||
// TODO @puhui999:modalApi.lock(); 这种写法;
|
||||
|
||||
// 情况一:属性名已存在,则直接使用
|
||||
const existProperty = attributeOptions.value.find(
|
||||
(item: MallPropertyApi.Property) => item.name === name,
|
||||
@@ -113,6 +113,7 @@ const [Modal, modalApi] = useVbenModal({
|
||||
name,
|
||||
values: [],
|
||||
});
|
||||
// TODO @puhui999:这里要不 if else;这样 await modalApi.close(); emit('success'); 可以复用?另外,感觉甚至可以情况二:add 后,成为 existProperty,可以进一步简化?
|
||||
await modalApi.close();
|
||||
emit('success');
|
||||
return;
|
||||
@@ -132,7 +133,6 @@ const [Modal, modalApi] = useVbenModal({
|
||||
await modalApi.close();
|
||||
emit('success');
|
||||
} catch (error) {
|
||||
// 发生错误时不关闭弹窗
|
||||
console.error('添加属性失败:', error);
|
||||
}
|
||||
},
|
||||
@@ -140,7 +140,6 @@ const [Modal, modalApi] = useVbenModal({
|
||||
if (!isOpen) {
|
||||
return;
|
||||
}
|
||||
// 重置表单
|
||||
await formApi.resetForm();
|
||||
},
|
||||
});
|
||||
|
||||
@@ -46,16 +46,16 @@ const { isBatch, isDetail, isComponent, isActivityComponent } = props;
|
||||
const formData: Ref<MallSpuApi.Spu | undefined> = ref<MallSpuApi.Spu>(); // 表单数据
|
||||
const skuList = ref<MallSpuApi.Sku[]>([
|
||||
{
|
||||
price: 0, // 商品价格
|
||||
marketPrice: 0, // 市场价
|
||||
costPrice: 0, // 成本价
|
||||
barCode: '', // 商品条码
|
||||
picUrl: '', // 图片地址
|
||||
stock: 0, // 库存
|
||||
weight: 0, // 商品重量
|
||||
volume: 0, // 商品体积
|
||||
firstBrokeragePrice: 0, // 一级分销的佣金
|
||||
secondBrokeragePrice: 0, // 二级分销的佣金
|
||||
price: 0,
|
||||
marketPrice: 0,
|
||||
costPrice: 0,
|
||||
barCode: '',
|
||||
picUrl: '',
|
||||
stock: 0,
|
||||
weight: 0,
|
||||
volume: 0,
|
||||
firstBrokeragePrice: 0,
|
||||
secondBrokeragePrice: 0,
|
||||
},
|
||||
]); // 批量添加时的临时数据
|
||||
|
||||
@@ -91,9 +91,7 @@ function deleteSku(row: MallSpuApi.Sku) {
|
||||
|
||||
const tableHeaders = ref<{ label: string; prop: string }[]>([]); // 多属性表头
|
||||
|
||||
/**
|
||||
* 保存时,每个商品规格的表单要校验下。例如说,销售金额最低是 0.01 这种。
|
||||
*/
|
||||
/** 保存时,每个商品规格的表单要校验下。例如说,销售金额最低是 0.01 这种 */
|
||||
function validateSku() {
|
||||
validateProperty();
|
||||
let warningInfo = '请检查商品各行相关属性配置,';
|
||||
@@ -116,6 +114,7 @@ function validateSku() {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO @puhui999:是不是可以通过 getNestedValue 简化?
|
||||
function getValue(obj: any, arg: string): unknown {
|
||||
const keys = arg.split('.');
|
||||
let value: any = obj;
|
||||
@@ -132,19 +131,20 @@ function getValue(obj: any, arg: string): unknown {
|
||||
|
||||
/**
|
||||
* 选择时触发
|
||||
*
|
||||
* @param records 传递过来的选中的 sku 是一个数组
|
||||
*/
|
||||
function handleSelectionChange({ records }: { records: MallSpuApi.Sku[] }) {
|
||||
emit('selectionChange', records);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将传进来的值赋值给 skuList
|
||||
*/
|
||||
/** 将传进来的值赋值给 skuList */
|
||||
watch(
|
||||
() => props.propFormData,
|
||||
(data) => {
|
||||
if (!data) return;
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
formData.value = data;
|
||||
},
|
||||
{
|
||||
@@ -196,9 +196,7 @@ function generateTableData(propertyList: PropertyAndValues[]) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成 skus 前置校验
|
||||
*/
|
||||
/** 生成 skus 前置校验 */
|
||||
function validateData(propertyList: PropertyAndValues[]): boolean {
|
||||
const skuPropertyIds: number[] = [];
|
||||
formData.value!.skus!.forEach((sku: MallSpuApi.Sku) =>
|
||||
@@ -302,13 +300,13 @@ function getSkuTableRef() {
|
||||
return activitySkuListRef.value;
|
||||
}
|
||||
|
||||
// 暴露出生成 sku 方法,给添加属性成功时调用
|
||||
defineExpose({ generateTableData, validateSku, getSkuTableRef });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<!-- 情况一:添加/修改 -->
|
||||
<!-- TODO @puhui999:有可以通过 grid 来做么?主要考虑,这样不直接使用 vxe 标签,抽象程度更高; -->
|
||||
<VxeTable
|
||||
v-if="!isDetail && !isActivityComponent"
|
||||
:data="isBatch ? skuList : formData?.skus || []"
|
||||
@@ -328,7 +326,7 @@ defineExpose({ generateTableData, validateSku, getSkuTableRef });
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<template v-if="formData?.specType && !isBatch">
|
||||
<!-- 根据商品属性动态添加 -->
|
||||
<!-- 根据商品属性动态添加 -->
|
||||
<VxeColumn
|
||||
v-for="(item, index) in tableHeaders"
|
||||
:key="index"
|
||||
@@ -481,7 +479,7 @@ defineExpose({ generateTableData, validateSku, getSkuTableRef });
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<template v-if="formData?.specType && !isBatch">
|
||||
<!-- 根据商品属性动态添加 -->
|
||||
<!-- 根据商品属性动态添加 -->
|
||||
<VxeColumn
|
||||
v-for="(item, index) in tableHeaders"
|
||||
:key="index"
|
||||
@@ -565,7 +563,7 @@ defineExpose({ generateTableData, validateSku, getSkuTableRef });
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<template v-if="formData?.specType">
|
||||
<!-- 根据商品属性动态添加 -->
|
||||
<!-- 根据商品属性动态添加 -->
|
||||
<VxeColumn
|
||||
v-for="(item, index) in tableHeaders"
|
||||
:key="index"
|
||||
@@ -605,7 +603,7 @@ defineExpose({ generateTableData, validateSku, getSkuTableRef });
|
||||
{{ row.stock }}
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<!-- 方便扩展每个活动配置的属性不一样 -->
|
||||
<!-- 方便扩展每个活动配置的属性不一样 -->
|
||||
<slot name="extension"></slot>
|
||||
</VxeTable>
|
||||
</div>
|
||||
|
||||
@@ -24,7 +24,8 @@ const emit = defineEmits<{
|
||||
const selectedSkuId = ref<number>();
|
||||
const spuId = ref<number>();
|
||||
|
||||
// 配置列
|
||||
/** 配置列 */
|
||||
// TODO @puhui999:貌似列太宽了?
|
||||
const gridColumns = computed<VxeGridProps['columns']>(() => [
|
||||
{
|
||||
field: 'id',
|
||||
@@ -65,7 +66,6 @@ const gridColumns = computed<VxeGridProps['columns']>(() => [
|
||||
},
|
||||
]);
|
||||
|
||||
// 初始化表格
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
gridOptions: {
|
||||
columns: gridColumns.value,
|
||||
@@ -95,14 +95,13 @@ const [Grid, gridApi] = useVbenVxeGrid({
|
||||
},
|
||||
});
|
||||
|
||||
// 处理选中
|
||||
/** 处理选中 */
|
||||
function handleSelected(row: MallSpuApi.Sku) {
|
||||
emit('change', row);
|
||||
modalApi.close();
|
||||
selectedSkuId.value = undefined;
|
||||
}
|
||||
|
||||
// 初始化弹窗
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
destroyOnClose: true,
|
||||
onOpenChange: async (isOpen: boolean) => {
|
||||
@@ -111,8 +110,8 @@ const [Modal, modalApi] = useVbenModal({
|
||||
spuId.value = undefined;
|
||||
return;
|
||||
}
|
||||
|
||||
const data = modalApi.getData<SpuData>();
|
||||
// TODO @puhui999:这里要不 if return,让括号的层级简单点。
|
||||
if (data?.spuId) {
|
||||
spuId.value = data.spuId;
|
||||
// 触发数据查询
|
||||
@@ -125,7 +124,6 @@ const [Modal, modalApi] = useVbenModal({
|
||||
<template>
|
||||
<Modal class="w-[700px]" title="选择规格">
|
||||
<Grid>
|
||||
<!-- 单选列 -->
|
||||
<template #radio-column="{ row }">
|
||||
<Input
|
||||
v-model="selectedSkuId"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<!-- SPU 商品选择弹窗组件 -->
|
||||
<script lang="ts" setup>
|
||||
// TODO @puhui999:这个是不是可以放到 components 里?,和商品发布,关系不大
|
||||
import type { CheckboxChangeEvent } from 'ant-design-vue/es/checkbox/interface';
|
||||
|
||||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
@@ -30,33 +31,15 @@ const emit = defineEmits<{
|
||||
change: [spu: MallSpuApi.Spu | MallSpuApi.Spu[]];
|
||||
}>();
|
||||
|
||||
// 单选:选中的 SPU ID
|
||||
const selectedSpuId = ref<number>();
|
||||
// 多选:选中状态 map
|
||||
const checkedStatus = ref<Record<number, boolean>>({});
|
||||
// 多选:选中的 SPU 列表
|
||||
const checkedSpus = ref<MallSpuApi.Spu[]>([]);
|
||||
// 多选:全选状态
|
||||
const isCheckAll = ref(false);
|
||||
// 多选:半选状态
|
||||
const isIndeterminate = ref(false);
|
||||
const selectedSpuId = ref<number>(); // 单选:选中的 SPU ID
|
||||
const checkedStatus = ref<Record<number, boolean>>({}); // 多选:选中状态 map
|
||||
const checkedSpus = ref<MallSpuApi.Spu[]>([]); // 多选:选中的 SPU 列表
|
||||
const isCheckAll = ref(false); // 多选:全选状态
|
||||
const isIndeterminate = ref(false); // 多选:半选状态
|
||||
|
||||
// 分类列表(扁平)
|
||||
const categoryList = ref<any[]>([]);
|
||||
// 分类树
|
||||
const categoryTreeList = ref<any[]>([]);
|
||||
const categoryList = ref<any[]>([]); // 分类列表(扁平)
|
||||
const categoryTreeList = ref<any[]>([]); // 分类树
|
||||
|
||||
// 初始化分类数据
|
||||
onMounted(async () => {
|
||||
try {
|
||||
categoryList.value = await getCategoryList({});
|
||||
categoryTreeList.value = handleTree(categoryList.value, 'id', 'parentId');
|
||||
} catch (error) {
|
||||
console.error('加载分类数据失败:', error);
|
||||
}
|
||||
});
|
||||
|
||||
// 搜索表单配置
|
||||
const formSchema = computed<VbenFormSchema[]>(() => {
|
||||
return [
|
||||
{
|
||||
@@ -97,7 +80,6 @@ const formSchema = computed<VbenFormSchema[]>(() => {
|
||||
];
|
||||
});
|
||||
|
||||
// 列配置
|
||||
const gridColumns = computed<VxeGridProps['columns']>(() => {
|
||||
const columns: VxeGridProps['columns'] = [];
|
||||
|
||||
@@ -121,7 +103,7 @@ const gridColumns = computed<VxeGridProps['columns']>(() => {
|
||||
});
|
||||
}
|
||||
|
||||
// 其他列
|
||||
// 其它列
|
||||
columns.push(
|
||||
{
|
||||
field: 'id',
|
||||
@@ -157,7 +139,6 @@ const gridColumns = computed<VxeGridProps['columns']>(() => {
|
||||
return columns;
|
||||
});
|
||||
|
||||
// 初始化表格
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
schema: formSchema.value,
|
||||
@@ -172,6 +153,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
async query({ page }: any, formValues: any) {
|
||||
// TODO @puhui999:这里是不是不 try catch?
|
||||
try {
|
||||
const params = {
|
||||
pageNo: page.currentPage,
|
||||
@@ -182,6 +164,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
|
||||
createTime: formValues.createTime || undefined,
|
||||
};
|
||||
|
||||
// TODO @puhui999:一次性的,是不是不声明 params,直接放到 getSpuPage 里?
|
||||
const data = await getSpuPage(params);
|
||||
|
||||
// 初始化多选状态
|
||||
@@ -208,14 +191,15 @@ const [Grid, gridApi] = useVbenVxeGrid({
|
||||
},
|
||||
});
|
||||
|
||||
// 单选:处理选中
|
||||
// TODO @puhui999:如下的选中方法,可以因为 Grid 做简化么?
|
||||
/** 单选:处理选中 */
|
||||
function handleSingleSelected(row: MallSpuApi.Spu) {
|
||||
selectedSpuId.value = row.id;
|
||||
emit('change', row);
|
||||
modalApi.close();
|
||||
}
|
||||
|
||||
// 多选:全选/全不选
|
||||
/** 多选:全选/全不选 */
|
||||
function handleCheckAll(e: CheckboxChangeEvent) {
|
||||
const checked = e.target.checked;
|
||||
isCheckAll.value = checked;
|
||||
@@ -228,7 +212,7 @@ function handleCheckAll(e: CheckboxChangeEvent) {
|
||||
calculateIsCheckAll();
|
||||
}
|
||||
|
||||
// 多选:选中单个
|
||||
/** 多选:选中单个 */
|
||||
function handleCheckOne(
|
||||
checked: boolean,
|
||||
spu: MallSpuApi.Spu,
|
||||
@@ -255,7 +239,7 @@ function handleCheckOne(
|
||||
}
|
||||
}
|
||||
|
||||
// 多选:计算全选状态
|
||||
/** 多选:计算全选状态 */
|
||||
function calculateIsCheckAll() {
|
||||
const currentList = gridApi.grid.getData();
|
||||
if (currentList.length === 0) {
|
||||
@@ -272,7 +256,6 @@ function calculateIsCheckAll() {
|
||||
isIndeterminate.value = checkedCount > 0 && checkedCount < currentList.length;
|
||||
}
|
||||
|
||||
// 初始化弹窗
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
destroyOnClose: true,
|
||||
// 多选模式时显示确认按钮
|
||||
@@ -284,7 +267,7 @@ const [Modal, modalApi] = useVbenModal({
|
||||
: undefined,
|
||||
async onOpenChange(isOpen: boolean) {
|
||||
if (!isOpen) {
|
||||
// 关闭时清理状态
|
||||
// TODO @puhui999:是不是直接清理,不要判断 selectedSpuId.value
|
||||
if (!props.multiple) {
|
||||
selectedSpuId.value = undefined;
|
||||
}
|
||||
@@ -300,7 +283,6 @@ const [Modal, modalApi] = useVbenModal({
|
||||
checkedStatus.value = {};
|
||||
isCheckAll.value = false;
|
||||
isIndeterminate.value = false;
|
||||
|
||||
// 恢复已选中的数据
|
||||
if (Array.isArray(data) && data.length > 0) {
|
||||
checkedSpus.value = [...data];
|
||||
@@ -316,9 +298,16 @@ const [Modal, modalApi] = useVbenModal({
|
||||
}
|
||||
|
||||
// 触发查询
|
||||
// TODO @puhui999:貌似不用这里再查询一次,100% 会查询的,记忆中是;
|
||||
await gridApi.query();
|
||||
},
|
||||
});
|
||||
|
||||
/** 初始化分类数据 */
|
||||
onMounted(async () => {
|
||||
categoryList.value = await getCategoryList({});
|
||||
categoryTreeList.value = handleTree(categoryList.value, 'id', 'parentId');
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -145,7 +145,9 @@ const [Modal, modalApi] = useVbenModal({
|
||||
// "确认"按钮的回调
|
||||
async onConfirm() {
|
||||
const { valid } = await formApi.validate();
|
||||
if (!valid) return;
|
||||
if (!valid) {
|
||||
return;
|
||||
}
|
||||
|
||||
modalApi.lock();
|
||||
try {
|
||||
|
||||
@@ -53,7 +53,9 @@ const rewardRuleRef = ref<InstanceType<typeof RewardRule>>();
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
async onConfirm() {
|
||||
const { valid } = await formApi.validate();
|
||||
if (!valid) return;
|
||||
if (!valid) {
|
||||
return;
|
||||
}
|
||||
|
||||
modalApi.lock();
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user